diff --git a/app/detail.tsx b/app/detail.tsx
index ed9915b..aad0c49 100644
--- a/app/detail.tsx
+++ b/app/detail.tsx
@@ -1,11 +1,11 @@
-import React, { useEffect, useState, useRef } from 'react';
-import { View, Text, StyleSheet, Image, ScrollView, ActivityIndicator } from 'react-native';
-import { useLocalSearchParams, useRouter } from 'expo-router';
-import { ThemedView } from '@/components/ThemedView';
-import { ThemedText } from '@/components/ThemedText';
-import { api, SearchResult } from '@/services/api';
-import { getResolutionFromM3U8 } from '@/services/m3u8';
-import { DetailButton } from '@/components/DetailButton';
+import React, { useEffect, useState, useRef } from "react";
+import { View, Text, StyleSheet, Image, ScrollView, ActivityIndicator } from "react-native";
+import { useLocalSearchParams, useRouter } from "expo-router";
+import { ThemedView } from "@/components/ThemedView";
+import { ThemedText } from "@/components/ThemedText";
+import { api, SearchResult } from "@/services/api";
+import { getResolutionFromM3U8 } from "@/services/m3u8";
+import { StyledButton } from "@/components/StyledButton";
export default function DetailScreen() {
const { source, q } = useLocalSearchParams();
@@ -24,7 +24,7 @@ export default function DetailScreen() {
controllerRef.current = new AbortController();
const signal = controllerRef.current.signal;
- if (typeof q === 'string') {
+ if (typeof q === "string") {
const fetchDetailData = async () => {
setLoading(true);
setSearchResults([]);
@@ -35,15 +35,15 @@ export default function DetailScreen() {
try {
const resources = await api.getResources(signal);
if (!resources || resources.length === 0) {
- setError('没有可用的播放源');
+ setError("没有可用的播放源");
setLoading(false);
return;
}
let foundFirstResult = false;
// Prioritize source from params if available
- if (typeof source === 'string') {
- const index = resources.findIndex(r => r.key === source);
+ if (typeof source === "string") {
+ const index = resources.findIndex((r) => r.key === source);
if (index > 0) {
resources.unshift(resources.splice(index, 1)[0]);
}
@@ -61,14 +61,14 @@ export default function DetailScreen() {
resolution = await getResolutionFromM3U8(searchResult.episodes[0], signal);
}
} catch (e) {
- if ((e as Error).name !== 'AbortError') {
+ if ((e as Error).name !== "AbortError") {
console.error(`Failed to get resolution for ${resource.name}`, e);
}
}
const resultWithResolution = { ...searchResult, resolution };
- setSearchResults(prev => [...prev, resultWithResolution]);
+ setSearchResults((prev) => [...prev, resultWithResolution]);
if (!foundFirstResult) {
setDetail(resultWithResolution);
@@ -77,19 +77,19 @@ export default function DetailScreen() {
}
}
} catch (e) {
- if ((e as Error).name !== 'AbortError') {
+ if ((e as Error).name !== "AbortError") {
console.error(`Error searching in resource ${resource.name}:`, e);
}
}
}
if (!foundFirstResult) {
- setError('未找到播放源');
+ setError("未找到播放源");
setLoading(false);
}
} catch (e) {
- if ((e as Error).name !== 'AbortError') {
- setError(e instanceof Error ? e.message : '获取资源列表失败');
+ if ((e as Error).name !== "AbortError") {
+ setError(e instanceof Error ? e.message : "获取资源列表失败");
setLoading(false);
}
} finally {
@@ -108,7 +108,7 @@ export default function DetailScreen() {
if (!detail) return;
controllerRef.current?.abort(); // Cancel any ongoing fetches
router.push({
- pathname: '/play',
+ pathname: "/play",
params: {
source: detail.source,
id: detail.id.toString(),
@@ -171,26 +171,27 @@ export default function DetailScreen() {
{searchResults.map((item, index) => (
- setDetail(item)}
hasTVPreferredFocus={index === 0}
- style={[styles.sourceButton, detail?.source === item.source && styles.sourceButtonSelected]}
+ isSelected={detail?.source === item.source}
+ style={styles.sourceButton}
>
{item.source_name}
{item.episodes.length > 1 && (
- {item.episodes.length > 99 ? '99+' : `${item.episodes.length}`}
+ {item.episodes.length > 99 ? "99+" : `${item.episodes.length}`}
)}
{item.resolution && (
-
+
{item.resolution}
)}
-
+
))}
@@ -198,9 +199,13 @@ export default function DetailScreen() {
播放列表
{detail.episodes.map((episode, index) => (
- handlePlay(episode, index)}>
- {`第 ${index + 1} 集`}
-
+ handlePlay(episode, index)}
+ text={`第 ${index + 1} 集`}
+ textStyle={styles.episodeButtonText}
+ />
))}
@@ -212,9 +217,9 @@ export default function DetailScreen() {
const styles = StyleSheet.create({
container: { flex: 1 },
- centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
+ centered: { flex: 1, justifyContent: "center", alignItems: "center" },
topContainer: {
- flexDirection: 'row',
+ flexDirection: "row",
padding: 20,
},
poster: {
@@ -225,20 +230,20 @@ const styles = StyleSheet.create({
infoContainer: {
flex: 1,
marginLeft: 20,
- justifyContent: 'flex-start',
+ justifyContent: "flex-start",
},
title: {
fontSize: 28,
- fontWeight: 'bold',
+ fontWeight: "bold",
marginBottom: 10,
paddingTop: 20,
},
metaContainer: {
- flexDirection: 'row',
+ flexDirection: "row",
marginBottom: 10,
},
metaText: {
- color: '#aaa',
+ color: "#aaa",
marginRight: 10,
fontSize: 14,
},
@@ -247,7 +252,7 @@ const styles = StyleSheet.create({
},
description: {
fontSize: 14,
- color: '#ccc',
+ color: "#ccc",
lineHeight: 22,
},
bottomContainer: {
@@ -257,70 +262,53 @@ const styles = StyleSheet.create({
marginTop: 20,
},
sourcesTitleContainer: {
- flexDirection: 'row',
- alignItems: 'center',
+ flexDirection: "row",
+ alignItems: "center",
marginBottom: 10,
},
sourcesTitle: {
fontSize: 20,
- fontWeight: 'bold',
+ fontWeight: "bold",
},
sourceList: {
- flexDirection: 'row',
- flexWrap: 'wrap',
+ flexDirection: "row",
+ flexWrap: "wrap",
},
sourceButton: {
- backgroundColor: '#333',
- paddingHorizontal: 15,
- paddingVertical: 10,
- borderRadius: 8,
margin: 5,
- flexDirection: 'row',
- alignItems: 'center',
- borderWidth: 2,
- borderColor: 'transparent',
- },
- sourceButtonSelected: {
- backgroundColor: '#007bff',
},
sourceButtonText: {
- color: 'white',
+ color: "white",
fontSize: 16,
},
badge: {
- backgroundColor: 'red',
+ backgroundColor: "red",
borderRadius: 10,
paddingHorizontal: 6,
paddingVertical: 2,
marginLeft: 8,
},
badgeText: {
- color: 'white',
+ color: "white",
fontSize: 12,
- fontWeight: 'bold',
+ fontWeight: "bold",
},
episodesContainer: {
marginTop: 20,
},
episodesTitle: {
fontSize: 20,
- fontWeight: 'bold',
+ fontWeight: "bold",
marginBottom: 10,
},
episodeList: {
- flexDirection: 'row',
- flexWrap: 'wrap',
+ flexDirection: "row",
+ flexWrap: "wrap",
},
episodeButton: {
- backgroundColor: '#333',
- paddingHorizontal: 20,
- paddingVertical: 10,
- borderRadius: 8,
margin: 5,
- borderWidth: 2,
- borderColor: 'transparent',
},
episodeButtonText: {
- color: 'white',
+ color: "white",
},
});
diff --git a/app/index.tsx b/app/index.tsx
index 7b1d9a1..c177471 100644
--- a/app/index.tsx
+++ b/app/index.tsx
@@ -8,6 +8,7 @@ import { useFocusEffect, useRouter } from "expo-router";
import { useColorScheme } from "react-native";
import { Search, Settings } from "lucide-react-native";
import { SettingsModal } from "@/components/SettingsModal";
+import { StyledButton } from "@/components/StyledButton";
import useHomeStore, { RowItem, Category } from "@/stores/homeStore";
import { useSettingsStore } from "@/stores/settingsStore";
@@ -53,16 +54,14 @@ export default function HomeScreen() {
const renderCategory = ({ item }: { item: Category }) => {
const isSelected = selectedCategory?.title === item.title;
return (
- [
- styles.categoryButton,
- isSelected && styles.categoryButtonSelected,
- focused && styles.categoryButtonFocused,
- ]}
+ handleCategorySelect(item)}
- >
- {item.title}
-
+ isSelected={isSelected}
+ variant="primary"
+ style={styles.categoryButton}
+ textStyle={styles.categoryText}
+ />
);
};
@@ -97,18 +96,16 @@ export default function HomeScreen() {
首页
- [styles.searchButton, focused && styles.searchButtonFocused]}
+ router.push({ pathname: "/search" })}
+ variant="ghost"
>
-
- [styles.searchButton, focused && styles.searchButtonFocused]}
- onPress={showSettingsModal}
- >
+
+
-
+
@@ -191,10 +188,6 @@ const styles = StyleSheet.create({
borderRadius: 30,
marginLeft: 10,
},
- searchButtonFocused: {
- backgroundColor: "#007AFF",
- transform: [{ scale: 1.1 }],
- },
// Category Selector
categoryContainer: {
paddingBottom: 10,
@@ -208,20 +201,10 @@ const styles = StyleSheet.create({
borderRadius: 8,
marginHorizontal: 5,
},
- categoryButtonSelected: {
- backgroundColor: "#007AFF", // A bright blue for selected state
- },
- categoryButtonFocused: {
- backgroundColor: "#0056b3", // A darker blue for focused state
- elevation: 5,
- },
categoryText: {
fontSize: 16,
fontWeight: "500",
},
- categoryTextSelected: {
- color: "#FFFFFF",
- },
// Content Grid
listContent: {
paddingHorizontal: 16,
diff --git a/app/search.tsx b/app/search.tsx
index bf0528c..5cd00bc 100644
--- a/app/search.tsx
+++ b/app/search.tsx
@@ -1,23 +1,14 @@
-import React, { useState, useRef, useEffect } from 'react';
-import {
- View,
- TextInput,
- StyleSheet,
- FlatList,
- ActivityIndicator,
- Pressable,
- Text,
- Keyboard,
- useColorScheme,
-} from 'react-native';
-import { ThemedView } from '@/components/ThemedView';
-import { ThemedText } from '@/components/ThemedText';
-import VideoCard from '@/components/VideoCard.tv';
-import { api, SearchResult } from '@/services/api';
-import { Search } from 'lucide-react-native';
+import React, { useState, useRef, useEffect } from "react";
+import { View, TextInput, StyleSheet, FlatList, ActivityIndicator, Text, Keyboard, useColorScheme } from "react-native";
+import { ThemedView } from "@/components/ThemedView";
+import { ThemedText } from "@/components/ThemedText";
+import VideoCard from "@/components/VideoCard.tv";
+import { api, SearchResult } from "@/services/api";
+import { Search } from "lucide-react-native";
+import { StyledButton } from "@/components/StyledButton";
export default function SearchScreen() {
- const [keyword, setKeyword] = useState('');
+ const [keyword, setKeyword] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
@@ -46,11 +37,11 @@ export default function SearchScreen() {
if (response.results.length > 0) {
setResults(response.results);
} else {
- setError('没有找到相关内容');
+ setError("没有找到相关内容");
}
} catch (err) {
- setError('搜索失败,请稍后重试。');
- console.error('Search failed:', err);
+ setError("搜索失败,请稍后重试。");
+ console.error("Search failed:", err);
} finally {
setLoading(false);
}
@@ -76,13 +67,13 @@ export default function SearchScreen() {
style={[
styles.input,
{
- backgroundColor: colorScheme === 'dark' ? '#2c2c2e' : '#f0f0f0',
- color: colorScheme === 'dark' ? 'white' : 'black',
- borderColor: isInputFocused ? '#007bff' : 'transparent',
+ backgroundColor: colorScheme === "dark" ? "#2c2c2e" : "#f0f0f0",
+ color: colorScheme === "dark" ? "white" : "black",
+ borderColor: isInputFocused ? "#007bff" : "transparent",
},
]}
placeholder="搜索电影、剧集..."
- placeholderTextColor={colorScheme === 'dark' ? '#888' : '#555'}
+ placeholderTextColor={colorScheme === "dark" ? "#888" : "#555"}
value={keyword}
onChangeText={setKeyword}
onFocus={() => setIsInputFocused(true)}
@@ -90,18 +81,9 @@ export default function SearchScreen() {
onSubmitEditing={handleSearch} // Allow searching with remote 'enter' button
returnKeyType="search"
/>
- [
- styles.searchButton,
- {
- backgroundColor: colorScheme === 'dark' ? '#3a3a3c' : '#e0e0e0',
- },
- focused && styles.focusedButton,
- ]}
- onPress={handleSearch}
- >
-
-
+
+
+
{loading ? (
@@ -136,39 +118,35 @@ const styles = StyleSheet.create({
paddingTop: 50,
},
searchContainer: {
- flexDirection: 'row',
+ flexDirection: "row",
paddingHorizontal: 20,
marginBottom: 20,
- alignItems: 'center',
+ alignItems: "center",
},
input: {
flex: 1,
height: 50,
- backgroundColor: '#2c2c2e', // Default for dark mode, overridden inline
+ backgroundColor: "#2c2c2e", // Default for dark mode, overridden inline
borderRadius: 8,
paddingHorizontal: 15,
- color: 'white', // Default for dark mode, overridden inline
+ color: "white", // Default for dark mode, overridden inline
fontSize: 18,
marginRight: 10,
borderWidth: 2,
- borderColor: 'transparent', // Default, overridden for focus
+ borderColor: "transparent", // Default, overridden for focus
},
searchButton: {
padding: 12,
// backgroundColor is now set dynamically
borderRadius: 8,
},
- focusedButton: {
- backgroundColor: '#007bff',
- transform: [{ scale: 1.1 }],
- },
centerContainer: {
flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
+ justifyContent: "center",
+ alignItems: "center",
},
errorText: {
- color: 'red',
+ color: "red",
},
listContent: {
paddingHorizontal: 10,
diff --git a/components/DetailButton.tsx b/components/DetailButton.tsx
deleted file mode 100644
index b7c7c6b..0000000
--- a/components/DetailButton.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from "react";
-import {
- Pressable,
- StyleSheet,
- StyleProp,
- ViewStyle,
- PressableProps,
-} from "react-native";
-
-interface DetailButtonProps extends PressableProps {
- children: React.ReactNode;
- style?: StyleProp;
-}
-
-export const DetailButton: React.FC = ({
- children,
- style,
- ...rest
-}) => {
- return (
- [
- styles.button,
- style,
- focused && styles.buttonFocused,
- ]}
- {...rest}
- >
- {children}
-
- );
-};
-
-const styles = StyleSheet.create({
- button: {
- backgroundColor: "#333",
- paddingHorizontal: 15,
- paddingVertical: 10,
- borderRadius: 8,
- margin: 5,
- borderWidth: 2,
- borderColor: "transparent",
- flexDirection: "row",
- alignItems: "center",
- },
- buttonFocused: {
- backgroundColor: "#0056b3",
- borderColor: "#fff",
- elevation: 5,
- shadowColor: "#fff",
- shadowOffset: { width: 0, height: 0 },
- shadowOpacity: 1,
- shadowRadius: 15,
- },
-});
diff --git a/components/EpisodeSelectionModal.tsx b/components/EpisodeSelectionModal.tsx
index b20cee3..5c81b83 100644
--- a/components/EpisodeSelectionModal.tsx
+++ b/components/EpisodeSelectionModal.tsx
@@ -1,8 +1,8 @@
-import React from 'react';
-import { View, Text, StyleSheet, Modal, FlatList, Pressable, TouchableOpacity } from 'react-native';
-
-import usePlayerStore from '@/stores/playerStore';
-import { useState } from 'react';
+import React from "react";
+import { View, Text, StyleSheet, Modal, FlatList, Pressable } from "react-native";
+import { StyledButton } from "./StyledButton";
+import usePlayerStore from "@/stores/playerStore";
+import { useState } from "react";
interface Episode {
title?: string;
@@ -35,21 +35,18 @@ export const EpisodeSelectionModal: React.FC = () =>
{episodes.length > episodeGroupSize && (
{Array.from({ length: Math.ceil(episodes.length / episodeGroupSize) }, (_, groupIndex) => (
- setSelectedEpisodeGroup(groupIndex)}
- >
-
- {`${groupIndex * episodeGroupSize + 1}-${Math.min(
- (groupIndex + 1) * episodeGroupSize,
- episodes.length
- )}`}
-
-
+ isSelected={selectedEpisodeGroup === groupIndex}
+ style={styles.episodeGroupButton}
+ textStyle={styles.episodeGroupButtonText}
+ variant="primary"
+ />
))}
)}
@@ -63,24 +60,19 @@ export const EpisodeSelectionModal: React.FC = () =>
renderItem={({ item, index }) => {
const absoluteIndex = selectedEpisodeGroup * episodeGroupSize + index;
return (
- [
- styles.episodeItem,
- currentEpisodeIndex === absoluteIndex && styles.episodeItemSelected,
- focused && styles.focusedButton,
- ]}
+ onSelectEpisode(absoluteIndex)}
+ isSelected={currentEpisodeIndex === absoluteIndex}
hasTVPreferredFocus={currentEpisodeIndex === absoluteIndex}
- >
- {item.title || `第 ${absoluteIndex + 1} 集`}
-
+ style={styles.episodeItem}
+ textStyle={styles.episodeItemText}
+ />
);
}}
/>
- [styles.closeButton, focused && styles.focusedButton]} onPress={onClose}>
- 关闭
-
+
@@ -90,69 +82,49 @@ export const EpisodeSelectionModal: React.FC = () =>
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
- flexDirection: 'row',
- justifyContent: 'flex-end',
- backgroundColor: 'transparent',
+ flexDirection: "row",
+ justifyContent: "flex-end",
+ backgroundColor: "transparent",
},
modalContent: {
width: 400,
- height: '100%',
- backgroundColor: 'rgba(0, 0, 0, 0.85)',
+ height: "100%",
+ backgroundColor: "rgba(0, 0, 0, 0.85)",
padding: 20,
},
modalTitle: {
- color: 'white',
+ color: "white",
marginBottom: 20,
- textAlign: 'center',
+ textAlign: "center",
fontSize: 18,
- fontWeight: 'bold',
+ fontWeight: "bold",
},
episodeItem: {
- backgroundColor: '#333',
paddingVertical: 12,
- borderRadius: 8,
margin: 4,
flex: 1,
- alignItems: 'center',
- justifyContent: 'center',
- },
- episodeItemSelected: {
- backgroundColor: '#007bff',
},
episodeItemText: {
- color: 'white',
fontSize: 14,
},
episodeGroupContainer: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- justifyContent: 'center',
+ flexDirection: "row",
+ flexWrap: "wrap",
+ justifyContent: "center",
marginBottom: 15,
paddingHorizontal: 10,
},
episodeGroupButton: {
- backgroundColor: '#444',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 15,
margin: 5,
},
- episodeGroupButtonSelected: {
- backgroundColor: '#007bff',
- },
episodeGroupButtonText: {
- color: 'white',
fontSize: 12,
},
closeButton: {
- backgroundColor: '#333',
padding: 15,
- borderRadius: 8,
- alignItems: 'center',
marginTop: 20,
},
- focusedButton: {
- backgroundColor: 'rgba(119, 119, 119, 0.9)',
- transform: [{ scale: 1.1 }],
- },
});
diff --git a/components/MediaButton.tsx b/components/MediaButton.tsx
index 415cb82..6f67c68 100644
--- a/components/MediaButton.tsx
+++ b/components/MediaButton.tsx
@@ -1,53 +1,16 @@
-import React from "react";
-import { Pressable, StyleSheet, StyleProp, ViewStyle } from "react-native";
+import React, { ComponentProps } from "react";
+import { StyledButton } from "./StyledButton";
+import { StyleSheet } from "react-native";
-interface MediaButtonProps {
- onPress: () => void;
- children: React.ReactNode;
- isDisabled?: boolean;
- hasTVPreferredFocus?: boolean;
- style?: StyleProp;
-}
+type StyledButtonProps = ComponentProps;
-export const MediaButton: React.FC = ({
- onPress,
- children,
- isDisabled = false,
- hasTVPreferredFocus = false,
- style,
-}) => {
- return (
- [
- styles.mediaControlButton,
- focused && styles.focusedButton,
- isDisabled && styles.disabledButton,
- style,
- ]}
- >
- {children}
-
- );
-};
+export const MediaButton = (props: StyledButtonProps) => (
+
+);
const styles = StyleSheet.create({
mediaControlButton: {
- backgroundColor: "rgba(51, 51, 51, 0.8)",
padding: 12,
- borderRadius: 8,
- alignItems: "center",
- justifyContent: "center",
minWidth: 80,
- margin: 5,
- },
- focusedButton: {
- backgroundColor: "rgba(119, 119, 119, 0.9)",
- transform: [{ scale: 1.1 }],
- },
- disabledButton: {
- opacity: 0.5,
},
});
diff --git a/components/NextEpisodeOverlay.tsx b/components/NextEpisodeOverlay.tsx
index c67b02e..bce49b7 100644
--- a/components/NextEpisodeOverlay.tsx
+++ b/components/NextEpisodeOverlay.tsx
@@ -1,16 +1,14 @@
import React from "react";
-import { View, StyleSheet, TouchableOpacity } from "react-native";
+import { View, StyleSheet } from "react-native";
import { ThemedText } from "@/components/ThemedText";
+import { StyledButton } from "./StyledButton";
interface NextEpisodeOverlayProps {
visible: boolean;
onCancel: () => void;
}
-export const NextEpisodeOverlay: React.FC = ({
- visible,
- onCancel,
-}) => {
+export const NextEpisodeOverlay: React.FC = ({ visible, onCancel }) => {
if (!visible) {
return null;
}
@@ -18,12 +16,13 @@ export const NextEpisodeOverlay: React.FC = ({
return (
-
- 即将播放下一集...
-
-
- 取消
-
+ 即将播放下一集...
+
);
@@ -48,10 +47,8 @@ const styles = StyleSheet.create({
marginBottom: 10,
},
nextEpisodeButton: {
- backgroundColor: "#333",
padding: 8,
paddingHorizontal: 15,
- borderRadius: 5,
},
nextEpisodeButtonText: {
fontSize: 14,
diff --git a/components/PlayerControls.tsx b/components/PlayerControls.tsx
index e327826..c41ce42 100644
--- a/components/PlayerControls.tsx
+++ b/components/PlayerControls.tsx
@@ -88,7 +88,7 @@ export const PlayerControls: React.FC = ({ showControls, se
)}
-
+
diff --git a/components/SettingsModal.tsx b/components/SettingsModal.tsx
index ade29eb..3e675fd 100644
--- a/components/SettingsModal.tsx
+++ b/components/SettingsModal.tsx
@@ -1,8 +1,9 @@
-import React, { useState, useEffect, useRef } from 'react';
-import { Modal, View, Text, TextInput, StyleSheet, Pressable, useColorScheme } from 'react-native';
-import { ThemedText } from './ThemedText';
-import { ThemedView } from './ThemedView';
-import { useSettingsStore } from '@/stores/settingsStore';
+import React, { useState, useEffect, useRef } from "react";
+import { Modal, View, Text, TextInput, StyleSheet, useColorScheme } from "react-native";
+import { ThemedText } from "./ThemedText";
+import { ThemedView } from "./ThemedView";
+import { useSettingsStore } from "@/stores/settingsStore";
+import { StyledButton } from "./StyledButton";
export const SettingsModal: React.FC = () => {
const { isModalVisible, hideModal, apiBaseUrl, setApiBaseUrl, saveSettings, loadSettings } = useSettingsStore();
@@ -28,12 +29,12 @@ export const SettingsModal: React.FC = () => {
const styles = StyleSheet.create({
modalContainer: {
flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: 'rgba(0, 0, 0, 0.6)',
+ justifyContent: "center",
+ alignItems: "center",
+ backgroundColor: "rgba(0, 0, 0, 0.6)",
},
modalContent: {
- width: '80%',
+ width: "80%",
maxWidth: 500,
padding: 24,
borderRadius: 12,
@@ -41,9 +42,9 @@ export const SettingsModal: React.FC = () => {
},
title: {
fontSize: 24,
- fontWeight: 'bold',
+ fontWeight: "bold",
marginBottom: 20,
- textAlign: 'center',
+ textAlign: "center",
},
input: {
height: 50,
@@ -52,47 +53,28 @@ export const SettingsModal: React.FC = () => {
paddingHorizontal: 15,
fontSize: 16,
marginBottom: 24,
- backgroundColor: colorScheme === 'dark' ? '#3a3a3c' : '#f0f0f0',
- color: colorScheme === 'dark' ? 'white' : 'black',
- borderColor: 'transparent',
+ backgroundColor: colorScheme === "dark" ? "#3a3a3c" : "#f0f0f0",
+ color: colorScheme === "dark" ? "white" : "black",
+ borderColor: "transparent",
},
inputFocused: {
- borderColor: '#007AFF',
- shadowColor: '#007AFF',
+ borderColor: "#007AFF",
+ shadowColor: "#007AFF",
shadowOffset: { width: 0, height: 0 },
shadowOpacity: 0.8,
shadowRadius: 10,
elevation: 5,
},
buttonContainer: {
- flexDirection: 'row',
- justifyContent: 'space-around',
+ flexDirection: "row",
+ justifyContent: "space-around",
},
button: {
flex: 1,
- paddingVertical: 12,
- borderRadius: 8,
- alignItems: 'center',
marginHorizontal: 8,
},
- buttonSave: {
- backgroundColor: '#007AFF',
- },
- buttonCancel: {
- backgroundColor: colorScheme === 'dark' ? '#444' : '#ccc',
- },
buttonText: {
- color: 'white',
fontSize: 18,
- fontWeight: '500',
- },
- focusedButton: {
- transform: [{ scale: 1.05 }],
- shadowColor: '#000',
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.3,
- shadowRadius: 5,
- elevation: 8,
},
});
@@ -107,25 +89,27 @@ export const SettingsModal: React.FC = () => {
value={apiBaseUrl}
onChangeText={setApiBaseUrl}
placeholder="输入 API 地址"
- placeholderTextColor={colorScheme === 'dark' ? '#888' : '#555'}
+ placeholderTextColor={colorScheme === "dark" ? "#888" : "#555"}
autoCapitalize="none"
autoCorrect={false}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
/>
- [styles.button, styles.buttonCancel, focused && styles.focusedButton]}
+
- 取消
-
- [styles.button, styles.buttonSave, focused && styles.focusedButton]}
+ style={styles.button}
+ textStyle={styles.buttonText}
+ variant="default"
+ />
+
- 保存
-
+ style={styles.button}
+ textStyle={styles.buttonText}
+ variant="primary"
+ />
diff --git a/components/StyledButton.tsx b/components/StyledButton.tsx
new file mode 100644
index 0000000..3789956
--- /dev/null
+++ b/components/StyledButton.tsx
@@ -0,0 +1,142 @@
+import React from "react";
+import { Pressable, StyleSheet, StyleProp, ViewStyle, PressableProps, TextStyle, useColorScheme } from "react-native";
+import { ThemedText } from "./ThemedText";
+import { Colors } from "@/constants/Colors";
+
+interface StyledButtonProps extends PressableProps {
+ children?: React.ReactNode;
+ text?: string;
+ variant?: "default" | "primary" | "ghost";
+ isSelected?: boolean;
+ style?: StyleProp;
+ textStyle?: StyleProp;
+}
+
+export const StyledButton: React.FC = ({
+ children,
+ text,
+ variant = "default",
+ isSelected = false,
+ style,
+ textStyle,
+ ...rest
+}) => {
+ const colorScheme = useColorScheme() ?? "light";
+ const colors = Colors[colorScheme];
+
+ const variantStyles = {
+ default: StyleSheet.create({
+ button: {
+ backgroundColor: colors.border,
+ },
+ text: {
+ color: colors.text,
+ },
+ selectedButton: {
+ backgroundColor: colors.tint,
+ },
+ focusedButton: {
+ backgroundColor: colors.link,
+ borderColor: colors.background,
+ },
+ selectedText: {
+ color: Colors.dark.text,
+ },
+ }),
+ primary: StyleSheet.create({
+ button: {
+ backgroundColor: "transparent",
+ },
+ text: {
+ color: colors.text,
+ },
+ focusedButton: {
+ backgroundColor: colors.link,
+ borderColor: colors.background,
+ },
+ selectedButton: {
+ backgroundColor: "rgba(0, 122, 255, 0.3)",
+ transform: [{ scale: 1.1 }],
+ },
+ selectedText: {
+ color: colors.link,
+ },
+ }),
+ ghost: StyleSheet.create({
+ button: {
+ backgroundColor: "transparent",
+ },
+ text: {
+ color: colors.text,
+ },
+ focusedButton: {
+ backgroundColor: "rgba(119, 119, 119, 0.9)",
+ },
+ selectedButton: {},
+ selectedText: {},
+ }),
+ };
+
+ const styles = StyleSheet.create({
+ button: {
+ paddingHorizontal: 15,
+ paddingVertical: 10,
+ borderRadius: 8,
+ margin: 5,
+ borderWidth: 2,
+ borderColor: "transparent",
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "center",
+ },
+ focusedButton: {
+ backgroundColor: colors.link,
+ borderColor: colors.background,
+ transform: [{ scale: 1.1 }],
+ elevation: 5,
+ shadowColor: colors.link,
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 1,
+ shadowRadius: 15,
+ },
+ selectedButton: {
+ backgroundColor: colors.tint,
+ },
+ text: {
+ fontSize: 16,
+ fontWeight: "500",
+ color: colors.text,
+ },
+ selectedText: {
+ color: Colors.dark.text,
+ },
+ });
+
+ return (
+ [
+ styles.button,
+ variantStyles[variant].button,
+ isSelected && (variantStyles[variant].selectedButton ?? styles.selectedButton),
+ focused && (variantStyles[variant].focusedButton ?? styles.focusedButton),
+ style,
+ ]}
+ {...rest}
+ >
+ {text ? (
+
+ {text}
+
+ ) : (
+ children
+ )}
+
+ );
+};
diff --git a/docs/components/StyledButton.md b/docs/components/StyledButton.md
new file mode 100644
index 0000000..5b77871
--- /dev/null
+++ b/docs/components/StyledButton.md
@@ -0,0 +1,77 @@
+# StyledButton 组件设计文档
+
+## 1. 目的
+
+为了统一整个应用中的按钮样式和行为,减少代码重复,并提高开发效率和一致性,我们设计了一个通用的 `StyledButton` 组件。
+
+该组件将取代以下位置的自定义 `Pressable` 和 `TouchableOpacity` 实现:
+
+- `app/index.tsx` (分类按钮, 头部图标按钮)
+- `components/DetailButton.tsx`
+- `components/EpisodeSelectionModal.tsx` (剧集分组按钮, 剧集项按钮, 关闭按钮)
+- `components/SettingsModal.tsx` (取消和保存按钮)
+- `app/search.tsx` (清除按钮)
+- `components/MediaButton.tsx` (媒体控制按钮)
+- `components/NextEpisodeOverlay.tsx` (取消按钮)
+
+## 2. API 设计
+
+`StyledButton` 组件将基于 React Native 的 `Pressable` 构建,并提供以下 props:
+
+```typescript
+import { PressableProps, StyleProp, ViewStyle, TextStyle } from "react-native";
+
+interface StyledButtonProps extends PressableProps {
+ // 按钮的主要内容,可以是文本或图标等 React 节点
+ children?: React.ReactNode;
+
+ // 如果按钮只包含文本,可以使用此 prop 快速设置
+ text?: string;
+
+ // 按钮的视觉变体,用于应用不同的预设样式
+ // 'default': 默认灰色背景
+ // 'primary': 主题色背景,用于关键操作
+ // 'ghost': 透明背景,通常用于图标按钮
+ variant?: "default" | "primary" | "ghost";
+
+ // 按钮是否处于选中状态
+ isSelected?: boolean;
+
+ // 覆盖容器的样式
+ style?: StyleProp;
+
+ // 覆盖文本的样式 (当使用 `text` prop 时生效)
+ textStyle?: StyleProp;
+}
+```
+
+## 3. 样式和行为
+
+### 状态样式:
+
+- **默认状态 (`default`)**:
+ - 背景色: `#333`
+ - 边框: `transparent`
+- **聚焦状态 (`focused`)**:
+ - 背景色: `#0056b3` (深蓝色)
+ - 边框: `#fff`
+ - 阴影/光晕效果
+ - 轻微放大 (`transform: scale(1.1)`)
+- **选中状态 (`isSelected`)**:
+ - 背景色: `#007AFF` (亮蓝色)
+- **主操作 (`primary`)**:
+ - 默认背景色: `#007AFF`
+- **透明背景 (`ghost`)**:
+ - 默认背景色: `transparent`
+
+### 结构:
+
+组件内部将使用 `Pressable` 作为根元素,并根据 `focused` 和 `isSelected` props 动态计算样式。如果 `children` 和 `text` prop 都提供了,`children` 将优先被渲染。
+
+## 4. 实现计划
+
+1. **创建 `components/StyledButton.tsx` 文件**。
+2. **实现上述 API 和样式逻辑**。
+3. **逐个重构目标文件**,将原有的 `Pressable`/`TouchableOpacity` 替换为新的 `StyledButton` 组件。
+4. **删除旧的、不再需要的样式**。
+5. **测试所有被修改的界面**,确保按钮的功能和视觉效果符合预期。