diff --git a/app/detail.tsx b/app/detail.tsx
index bcc6382..ddae6bc 100644
--- a/app/detail.tsx
+++ b/app/detail.tsx
@@ -4,6 +4,7 @@ import { useLocalSearchParams, useRouter } from "expo-router";
import { ThemedView } from "@/components/ThemedView";
import { ThemedText } from "@/components/ThemedText";
import { StyledButton } from "@/components/StyledButton";
+import VideoLoadingAnimation from "@/components/VideoLoadingAnimation";
import useDetailStore from "@/stores/detailStore";
import { FontAwesome } from "@expo/vector-icons";
@@ -49,11 +50,7 @@ export default function DetailScreen() {
};
if (loading) {
- return (
-
-
-
- );
+ return ;
}
if (error) {
diff --git a/app/play.tsx b/app/play.tsx
index 3a9faf7..b6de405 100644
--- a/app/play.tsx
+++ b/app/play.tsx
@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from "react";
-import { StyleSheet, TouchableOpacity, ActivityIndicator, BackHandler, AppState, AppStateStatus } from "react-native";
+import { StyleSheet, TouchableOpacity, BackHandler, AppState, AppStateStatus, View } from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
import { Video, ResizeMode } from "expo-av";
import { useKeepAwake } from "expo-keep-awake";
@@ -9,7 +9,7 @@ import { EpisodeSelectionModal } from "@/components/EpisodeSelectionModal";
import { SourceSelectionModal } from "@/components/SourceSelectionModal";
import { SeekingBar } from "@/components/SeekingBar";
import { NextEpisodeOverlay } from "@/components/NextEpisodeOverlay";
-import { LoadingOverlay } from "@/components/LoadingOverlay";
+import VideoLoadingAnimation from "@/components/VideoLoadingAnimation";
import useDetailStore from "@/stores/detailStore";
import { useTVRemoteHandler } from "@/hooks/useTVRemoteHandler";
import Toast from "react-native-toast-message";
@@ -116,11 +116,7 @@ export default function PlayScreen() {
}, [isLoading]);
if (!detail) {
- return (
-
-
-
- );
+ return ;
}
return (
@@ -150,7 +146,11 @@ export default function PlayScreen() {
-
+ {isLoading && (
+
+
+
+ )}
setShowNextEpisodeOverlay(false)} />
diff --git a/app/search.tsx b/app/search.tsx
index 9672b70..8c9c54c 100644
--- a/app/search.tsx
+++ b/app/search.tsx
@@ -1,8 +1,9 @@
import React, { useState, useRef, useEffect } from "react";
-import { View, TextInput, StyleSheet, FlatList, ActivityIndicator, Alert, Keyboard } from "react-native";
+import { View, TextInput, StyleSheet, FlatList, Alert, Keyboard } from "react-native";
import { ThemedView } from "@/components/ThemedView";
import { ThemedText } from "@/components/ThemedText";
import VideoCard from "@/components/VideoCard.tv";
+import VideoLoadingAnimation from "@/components/VideoLoadingAnimation";
import { api, SearchResult } from "@/services/api";
import { Search, QrCode } from "lucide-react-native";
import { StyledButton } from "@/components/StyledButton";
@@ -121,9 +122,7 @@ export default function SearchScreen() {
{loading ? (
-
-
-
+
) : error ? (
{error}
diff --git a/components/VideoLoadingAnimation.tsx b/components/VideoLoadingAnimation.tsx
new file mode 100644
index 0000000..8687fc5
--- /dev/null
+++ b/components/VideoLoadingAnimation.tsx
@@ -0,0 +1,335 @@
+import React, { useEffect, useRef } from "react";
+import { View, StyleSheet, Animated, Easing } from "react-native";
+import { LinearGradient } from "expo-linear-gradient";
+
+interface VideoLoadingAnimationProps {
+ showProgressBar?: boolean;
+}
+
+const VideoLoadingAnimation: React.FC = ({ showProgressBar = true }) => {
+ const floatAnim = useRef(new Animated.Value(0)).current;
+ const pulseAnim = useRef(new Animated.Value(0)).current;
+ const bounceAnims = [
+ useRef(new Animated.Value(0)).current,
+ useRef(new Animated.Value(0)).current,
+ useRef(new Animated.Value(0)).current,
+ ];
+ const progressAnim = useRef(new Animated.Value(0)).current;
+ const gradientAnim = useRef(new Animated.Value(0)).current;
+ const textFadeAnim = useRef(new Animated.Value(0)).current;
+ const shapeAnims = [
+ useRef(new Animated.Value(0)).current,
+ useRef(new Animated.Value(0)).current,
+ useRef(new Animated.Value(0)).current,
+ useRef(new Animated.Value(0)).current,
+ ];
+
+ useEffect(() => {
+ const floatAnimation = Animated.loop(
+ Animated.sequence([
+ Animated.timing(floatAnim, {
+ toValue: -20,
+ duration: 1500,
+ useNativeDriver: true,
+ easing: Easing.inOut(Easing.ease),
+ }),
+ Animated.timing(floatAnim, {
+ toValue: 0,
+ duration: 1500,
+ useNativeDriver: true,
+ easing: Easing.inOut(Easing.ease),
+ }),
+ ])
+ );
+
+ const pulseAnimation = Animated.loop(
+ Animated.sequence([
+ Animated.timing(pulseAnim, {
+ toValue: 1,
+ duration: 1000,
+ useNativeDriver: true,
+ easing: Easing.inOut(Easing.ease),
+ }),
+ Animated.timing(pulseAnim, {
+ toValue: 0,
+ duration: 1000,
+ useNativeDriver: true,
+ easing: Easing.inOut(Easing.ease),
+ }),
+ ])
+ );
+
+ const bounceAnimations = bounceAnims.map((anim, i) =>
+ Animated.loop(
+ Animated.sequence([
+ Animated.delay(i * 160),
+ Animated.timing(anim, {
+ toValue: 1,
+ duration: 700,
+ useNativeDriver: true,
+ easing: Easing.inOut(Easing.ease),
+ }),
+ Animated.timing(anim, {
+ toValue: 0,
+ duration: 700,
+ useNativeDriver: true,
+ easing: Easing.inOut(Easing.ease),
+ }),
+ ])
+ )
+ );
+
+ const progressAnimation = Animated.loop(
+ Animated.timing(progressAnim, {
+ toValue: 1,
+ duration: 4000,
+ useNativeDriver: false, // width animation not supported by native driver
+ easing: Easing.inOut(Easing.ease),
+ })
+ );
+
+ const gradientAnimation = Animated.loop(
+ Animated.timing(gradientAnim, {
+ toValue: 1,
+ duration: 2000,
+ useNativeDriver: false, // gradient animation not supported by native driver
+ easing: Easing.inOut(Easing.ease),
+ })
+ );
+
+ const textFadeAnimation = Animated.loop(
+ Animated.sequence([
+ Animated.timing(textFadeAnim, {
+ toValue: 1,
+ duration: 1000,
+ useNativeDriver: true,
+ easing: Easing.inOut(Easing.ease),
+ }),
+ Animated.timing(textFadeAnim, {
+ toValue: 0,
+ duration: 1000,
+ useNativeDriver: true,
+ easing: Easing.inOut(Easing.ease),
+ }),
+ ])
+ );
+
+ const shapeAnimations = shapeAnims.map((anim, i) =>
+ Animated.loop(
+ Animated.sequence([
+ Animated.delay(i * 2000),
+ Animated.timing(anim, {
+ toValue: 1,
+ duration: 8000,
+ useNativeDriver: true,
+ easing: Easing.inOut(Easing.ease),
+ }),
+ ])
+ )
+ );
+
+ Animated.parallel([
+ floatAnimation,
+ pulseAnimation,
+ ...bounceAnimations,
+ progressAnimation,
+ gradientAnimation,
+ textFadeAnimation,
+ ...shapeAnimations,
+ ]).start();
+ }, []);
+
+ const animatedStyles = {
+ float: {
+ transform: [{ translateY: floatAnim }],
+ },
+ pulse: {
+ opacity: pulseAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 0.7] }),
+ transform: [
+ { translateX: -12.5 },
+ { translateY: -15 },
+ {
+ scale: pulseAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 1.1] }),
+ },
+ ],
+ },
+ bounce: bounceAnims.map((anim) => ({
+ transform: [{ scale: anim.interpolate({ inputRange: [0, 1], outputRange: [0.8, 1.2] }) }],
+ opacity: anim.interpolate({ inputRange: [0, 1], outputRange: [0.5, 1] }),
+ })),
+ progress: {
+ width: progressAnim.interpolate({
+ inputRange: [0, 0.7, 1],
+ outputRange: ["0%", "100%", "100%"],
+ }),
+ },
+ textFade: {
+ opacity: textFadeAnim.interpolate({ inputRange: [0, 1], outputRange: [0.6, 1] }),
+ },
+ shapes: shapeAnims.map((anim, i) => ({
+ transform: [
+ {
+ translateY: anim.interpolate({
+ inputRange: [0, 0.33, 0.66, 1],
+ outputRange: [0, -30, 10, 0],
+ }),
+ },
+ {
+ rotate: anim.interpolate({
+ inputRange: [0, 1],
+ outputRange: ["0deg", "360deg"],
+ }),
+ },
+ ],
+ })),
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showProgressBar && (
+
+
+
+
+
+ )}
+
+ 正在加载视频
+
+
+ );
+};
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "#2d3748",
+ overflow: "hidden",
+ },
+ loadingContainer: {
+ alignItems: "center",
+ zIndex: 10,
+ },
+ videoIcon: {
+ width: 120,
+ height: 120,
+ marginBottom: 30,
+ },
+ videoFrame: {
+ width: "100%",
+ height: "100%",
+ backgroundColor: "rgba(255, 255, 255, 0.05)",
+ borderWidth: 3,
+ borderColor: "rgba(255, 255, 255, 0.2)",
+ borderRadius: 12,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ playButton: {
+ width: 0,
+ height: 0,
+ borderStyle: "solid",
+ borderLeftWidth: 25,
+ borderLeftColor: "rgba(255, 255, 255, 0.9)",
+ borderTopWidth: 15,
+ borderTopColor: "transparent",
+ borderBottomWidth: 15,
+ borderBottomColor: "transparent",
+ },
+ loadingDots: {
+ flexDirection: "row",
+ justifyContent: "center",
+ gap: 8,
+ marginBottom: 20,
+ },
+ dot: {
+ width: 12,
+ height: 12,
+ backgroundColor: "rgba(255, 255, 255, 0.9)",
+ borderRadius: 6,
+ },
+ progressBar: {
+ width: 300,
+ height: 6,
+ backgroundColor: "rgba(255, 255, 255, 0.1)",
+ borderRadius: 3,
+ marginVertical: 20,
+ overflow: "hidden",
+ },
+ progressFill: {
+ height: "100%",
+ borderRadius: 3,
+ },
+ loadingText: {
+ color: "rgba(255, 255, 255, 0.9)",
+ fontSize: 18,
+ fontWeight: "300",
+ letterSpacing: 2,
+ marginTop: 10,
+ },
+ bgShapes: {
+ position: "absolute",
+ top: 0,
+ left: 0,
+ width: "100%",
+ height: "100%",
+ zIndex: 1,
+ },
+ shape: {
+ position: "absolute",
+ backgroundColor: "rgba(255, 255, 255, 0.03)",
+ borderRadius: 50,
+ },
+ shape1: {
+ width: 80,
+ height: 80,
+ top: "20%",
+ left: "10%",
+ },
+ shape2: {
+ width: 60,
+ height: 60,
+ top: "60%",
+ right: "15%",
+ },
+ shape3: {
+ width: 100,
+ height: 100,
+ bottom: "20%",
+ left: "20%",
+ },
+ shape4: {
+ width: 40,
+ height: 40,
+ top: "30%",
+ right: "30%",
+ },
+});
+
+export default VideoLoadingAnimation;
diff --git a/package.json b/package.json
index d70b157..a2dc3a0 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"expo-build-properties": "~0.12.3",
"expo-constants": "~16.0.2",
"expo-font": "~12.0.7",
+ "expo-linear-gradient": "~13.0.2",
"expo-linking": "~6.3.1",
"expo-router": "~3.5.16",
"expo-splash-screen": "~0.27.5",
diff --git a/yarn.lock b/yarn.lock
index 84be445..831db06 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4587,6 +4587,11 @@ expo-keep-awake@~13.0.2:
resolved "https://registry.yarnpkg.com/expo-keep-awake/-/expo-keep-awake-13.0.2.tgz#5ef31311a339671eec9921b934fdd90ab9652b0e"
integrity sha512-kKiwkVg/bY0AJ5q1Pxnm/GvpeB6hbNJhcFsoOWDh2NlpibhCLaHL826KHUM+WsnJRbVRxJ+K9vbPRHEMvFpVyw==
+expo-linear-gradient@~13.0.2:
+ version "13.0.2"
+ resolved "https://registry.yarnpkg.com/expo-linear-gradient/-/expo-linear-gradient-13.0.2.tgz#21bd7bc7c71ef4f7c089521daa16db729d2aec5f"
+ integrity sha512-EDcILUjRKu4P1rtWcwciN6CSyGtH7Bq4ll3oTRV7h3h8oSzSilH1g6z7kTAMlacPBKvMnkkWOGzW6KtgMKEiTg==
+
expo-linking@~6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/expo-linking/-/expo-linking-6.3.1.tgz#05aef8a42bd310391d0b00644be40d80ece038d9"
@@ -8804,7 +8809,16 @@ string-length@^5.0.1:
char-regex "^2.0.0"
strip-ansi "^7.0.1"
-"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
+"string-width-cjs@npm:string-width@^4.2.0":
+ version "4.2.3"
+ resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -8895,7 +8909,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -8909,6 +8923,13 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0:
dependencies:
ansi-regex "^4.1.0"
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -9740,7 +9761,7 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -9758,6 +9779,15 @@ wrap-ansi@^6.2.0:
string-width "^4.1.0"
strip-ansi "^6.0.0"
+wrap-ansi@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"