Refactor components to use StyledButton for consistent button styling

- Replaced custom button implementations with StyledButton in various components including DetailScreen, HomeScreen, SearchScreen, and SettingsModal.
- Updated button styles and behaviors to align with the new StyledButton component.
- Removed the obsolete DetailButton component to streamline the codebase.
This commit is contained in:
zimplexing
2025-07-08 17:24:55 +08:00
parent 9f721c22d5
commit 504f12067b
11 changed files with 395 additions and 366 deletions

View File

@@ -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() {
</View>
<View style={styles.sourceList}>
{searchResults.map((item, index) => (
<DetailButton
<StyledButton
key={index}
onPress={() => setDetail(item)}
hasTVPreferredFocus={index === 0}
style={[styles.sourceButton, detail?.source === item.source && styles.sourceButtonSelected]}
isSelected={detail?.source === item.source}
style={styles.sourceButton}
>
<ThemedText style={styles.sourceButtonText}>{item.source_name}</ThemedText>
{item.episodes.length > 1 && (
<View style={styles.badge}>
<Text style={styles.badgeText}>
{item.episodes.length > 99 ? '99+' : `${item.episodes.length}`}
{item.episodes.length > 99 ? "99+" : `${item.episodes.length}`}
</Text>
</View>
)}
{item.resolution && (
<View style={[styles.badge, { backgroundColor: '#28a745' }]}>
<View style={[styles.badge, { backgroundColor: "#28a745" }]}>
<Text style={styles.badgeText}>{item.resolution}</Text>
</View>
)}
</DetailButton>
</StyledButton>
))}
</View>
</View>
@@ -198,9 +199,13 @@ export default function DetailScreen() {
<ThemedText style={styles.episodesTitle}></ThemedText>
<ScrollView contentContainerStyle={styles.episodeList}>
{detail.episodes.map((episode, index) => (
<DetailButton key={index} style={styles.episodeButton} onPress={() => handlePlay(episode, index)}>
<ThemedText style={styles.episodeButtonText}>{`${index + 1}`}</ThemedText>
</DetailButton>
<StyledButton
key={index}
style={styles.episodeButton}
onPress={() => handlePlay(episode, index)}
text={`${index + 1}`}
textStyle={styles.episodeButtonText}
/>
))}
</ScrollView>
</View>
@@ -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",
},
});

View File

@@ -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 (
<Pressable
style={({ focused }) => [
styles.categoryButton,
isSelected && styles.categoryButtonSelected,
focused && styles.categoryButtonFocused,
]}
<StyledButton
text={item.title}
onPress={() => handleCategorySelect(item)}
>
<ThemedText style={[styles.categoryText, isSelected && styles.categoryTextSelected]}>{item.title}</ThemedText>
</Pressable>
isSelected={isSelected}
variant="primary"
style={styles.categoryButton}
textStyle={styles.categoryText}
/>
);
};
@@ -97,18 +96,16 @@ export default function HomeScreen() {
<View style={styles.headerContainer}>
<ThemedText style={styles.headerTitle}></ThemedText>
<View style={styles.rightHeaderButtons}>
<Pressable
style={({ focused }) => [styles.searchButton, focused && styles.searchButtonFocused]}
<StyledButton
style={styles.searchButton}
onPress={() => router.push({ pathname: "/search" })}
variant="ghost"
>
<Search color={colorScheme === "dark" ? "white" : "black"} size={24} />
</Pressable>
<Pressable
style={({ focused }) => [styles.searchButton, focused && styles.searchButtonFocused]}
onPress={showSettingsModal}
>
</StyledButton>
<StyledButton style={styles.searchButton} onPress={showSettingsModal} variant="ghost">
<Settings color={colorScheme === "dark" ? "white" : "black"} size={24} />
</Pressable>
</StyledButton>
</View>
</View>
@@ -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,

View File

@@ -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<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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"
/>
<Pressable
style={({ focused }) => [
styles.searchButton,
{
backgroundColor: colorScheme === 'dark' ? '#3a3a3c' : '#e0e0e0',
},
focused && styles.focusedButton,
]}
onPress={handleSearch}
>
<Search size={24} color={colorScheme === 'dark' ? 'white' : 'black'} />
</Pressable>
<StyledButton style={styles.searchButton} onPress={handleSearch}>
<Search size={24} color={colorScheme === "dark" ? "white" : "black"} />
</StyledButton>
</View>
{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,