Refactor components for consistent styling and improve button animations

This commit is contained in:
zimplexing
2025-07-08 19:52:20 +08:00
parent 504f12067b
commit 74ad0872cb
7 changed files with 89 additions and 66 deletions

View File

@@ -1,21 +1,21 @@
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
import { useFonts } from 'expo-font'; import { useFonts } from "expo-font";
import { Stack } from 'expo-router'; import { Stack } from "expo-router";
import * as SplashScreen from 'expo-splash-screen'; import * as SplashScreen from "expo-splash-screen";
import { useEffect } from 'react'; import { useEffect } from "react";
import { Platform, useColorScheme } from 'react-native'; import { Platform, useColorScheme } from "react-native";
import { useSettingsStore } from '@/stores/settingsStore'; import { useSettingsStore } from "@/stores/settingsStore";
// Prevent the splash screen from auto-hiding before asset loading is complete. // Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync(); SplashScreen.preventAutoHideAsync();
export default function RootLayout() { export default function RootLayout() {
const colorScheme = useColorScheme(); const colorScheme = useColorScheme() ?? "dark";
const [loaded, error] = useFonts({ const [loaded, error] = useFonts({
SpaceMono: require('../assets/fonts/SpaceMono-Regular.ttf'), SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
}); });
const initializeSettings = useSettingsStore(state => state.loadSettings); const initializeSettings = useSettingsStore((state) => state.loadSettings);
useEffect(() => { useEffect(() => {
initializeSettings(); initializeSettings();
@@ -35,11 +35,11 @@ export default function RootLayout() {
} }
return ( return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <ThemeProvider value={colorScheme === "dark" ? DarkTheme : DefaultTheme}>
<Stack> <Stack>
<Stack.Screen name="index" options={{ headerShown: false }} /> <Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="detail" options={{ headerShown: false }} /> <Stack.Screen name="detail" options={{ headerShown: false }} />
{Platform.OS !== 'web' && <Stack.Screen name="play" options={{ headerShown: false }} />} {Platform.OS !== "web" && <Stack.Screen name="play" options={{ headerShown: false }} />}
<Stack.Screen name="search" options={{ headerShown: false }} /> <Stack.Screen name="search" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" /> <Stack.Screen name="+not-found" />
</Stack> </Stack>

View File

@@ -182,7 +182,7 @@ export default function DetailScreen() {
{item.episodes.length > 1 && ( {item.episodes.length > 1 && (
<View style={styles.badge}> <View style={styles.badge}>
<Text style={styles.badgeText}> <Text style={styles.badgeText}>
{item.episodes.length > 99 ? "99+" : `${item.episodes.length}`} {item.episodes.length > 99 ? "99+" : `${item.episodes.length}`}
</Text> </Text>
</View> </View>
)} )}
@@ -275,7 +275,7 @@ const styles = StyleSheet.create({
flexWrap: "wrap", flexWrap: "wrap",
}, },
sourceButton: { sourceButton: {
margin: 5, margin: 8,
}, },
sourceButtonText: { sourceButtonText: {
color: "white", color: "white",

View File

@@ -58,7 +58,6 @@ export default function HomeScreen() {
text={item.title} text={item.title}
onPress={() => handleCategorySelect(item)} onPress={() => handleCategorySelect(item)}
isSelected={isSelected} isSelected={isSelected}
variant="primary"
style={styles.categoryButton} style={styles.categoryButton}
textStyle={styles.categoryText} textStyle={styles.categoryText}
/> />
@@ -190,16 +189,16 @@ const styles = StyleSheet.create({
}, },
// Category Selector // Category Selector
categoryContainer: { categoryContainer: {
paddingBottom: 10, paddingBottom: 6,
}, },
categoryListContent: { categoryListContent: {
paddingHorizontal: 16, paddingHorizontal: 16,
}, },
categoryButton: { categoryButton: {
paddingHorizontal: 12, paddingHorizontal: 2,
paddingVertical: 6, paddingVertical: 6,
borderRadius: 8, borderRadius: 8,
marginHorizontal: 5, marginHorizontal: 6,
}, },
categoryText: { categoryText: {
fontSize: 16, fontSize: 16,

View File

@@ -45,7 +45,6 @@ export const EpisodeSelectionModal: React.FC<EpisodeSelectionModalProps> = () =>
isSelected={selectedEpisodeGroup === groupIndex} isSelected={selectedEpisodeGroup === groupIndex}
style={styles.episodeGroupButton} style={styles.episodeGroupButton}
textStyle={styles.episodeGroupButtonText} textStyle={styles.episodeGroupButtonText}
variant="primary"
/> />
))} ))}
</View> </View>
@@ -56,6 +55,7 @@ export const EpisodeSelectionModal: React.FC<EpisodeSelectionModalProps> = () =>
(selectedEpisodeGroup + 1) * episodeGroupSize (selectedEpisodeGroup + 1) * episodeGroupSize
)} )}
numColumns={5} numColumns={5}
contentContainerStyle={styles.episodeList}
keyExtractor={(_, index) => `episode-${selectedEpisodeGroup * episodeGroupSize + index}`} keyExtractor={(_, index) => `episode-${selectedEpisodeGroup * episodeGroupSize + index}`}
renderItem={({ item, index }) => { renderItem={({ item, index }) => {
const absoluteIndex = selectedEpisodeGroup * episodeGroupSize + index; const absoluteIndex = selectedEpisodeGroup * episodeGroupSize + index;
@@ -71,8 +71,6 @@ export const EpisodeSelectionModal: React.FC<EpisodeSelectionModalProps> = () =>
); );
}} }}
/> />
<StyledButton text="关闭" onPress={onClose} style={styles.closeButton} />
</View> </View>
</View> </View>
</Modal> </Modal>
@@ -87,22 +85,25 @@ const styles = StyleSheet.create({
backgroundColor: "transparent", backgroundColor: "transparent",
}, },
modalContent: { modalContent: {
width: 400, width: 600,
height: "100%", height: "100%",
backgroundColor: "rgba(0, 0, 0, 0.85)", backgroundColor: "rgba(0, 0, 0, 0.85)",
padding: 20, padding: 20,
}, },
modalTitle: { modalTitle: {
color: "white", color: "white",
marginBottom: 20, marginBottom: 12,
textAlign: "center", textAlign: "center",
fontSize: 18, fontSize: 18,
fontWeight: "bold", fontWeight: "bold",
}, },
episodeList: {
justifyContent: "flex-start",
},
episodeItem: { episodeItem: {
paddingVertical: 12, paddingVertical: 2,
margin: 4, margin: 4,
flex: 1, width: "18%",
}, },
episodeItemText: { episodeItemText: {
fontSize: 14, fontSize: 14,
@@ -111,20 +112,13 @@ const styles = StyleSheet.create({
flexDirection: "row", flexDirection: "row",
flexWrap: "wrap", flexWrap: "wrap",
justifyContent: "center", justifyContent: "center",
marginBottom: 15,
paddingHorizontal: 10, paddingHorizontal: 10,
}, },
episodeGroupButton: { episodeGroupButton: {
paddingHorizontal: 12, paddingHorizontal: 6,
paddingVertical: 6, margin: 8,
borderRadius: 15,
margin: 5,
}, },
episodeGroupButtonText: { episodeGroupButtonText: {
fontSize: 12, fontSize: 12,
}, },
closeButton: {
padding: 15,
marginTop: 20,
},
}); });

View File

@@ -1,7 +1,17 @@
import React from "react"; import React from "react";
import { Pressable, StyleSheet, StyleProp, ViewStyle, PressableProps, TextStyle, useColorScheme } from "react-native"; import {
Animated,
Pressable,
StyleSheet,
StyleProp,
ViewStyle,
PressableProps,
TextStyle,
useColorScheme,
} from "react-native";
import { ThemedText } from "./ThemedText"; import { ThemedText } from "./ThemedText";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import { useButtonAnimation } from "@/hooks/useButtonAnimation";
interface StyledButtonProps extends PressableProps { interface StyledButtonProps extends PressableProps {
children?: React.ReactNode; children?: React.ReactNode;
@@ -21,8 +31,10 @@ export const StyledButton: React.FC<StyledButtonProps> = ({
textStyle, textStyle,
...rest ...rest
}) => { }) => {
const colorScheme = useColorScheme() ?? "light"; const colorScheme = useColorScheme() ?? "dark";
const colors = Colors[colorScheme]; const colors = Colors[colorScheme];
const [isFocused, setIsFocused] = React.useState(false);
const animationStyle = useButtonAnimation(isSelected, isFocused);
const variantStyles = { const variantStyles = {
default: StyleSheet.create({ default: StyleSheet.create({
@@ -56,7 +68,6 @@ export const StyledButton: React.FC<StyledButtonProps> = ({
}, },
selectedButton: { selectedButton: {
backgroundColor: "rgba(0, 122, 255, 0.3)", backgroundColor: "rgba(0, 122, 255, 0.3)",
transform: [{ scale: 1.1 }],
}, },
selectedText: { selectedText: {
color: colors.link, color: colors.link,
@@ -79,10 +90,9 @@ export const StyledButton: React.FC<StyledButtonProps> = ({
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
paddingHorizontal: 15, paddingHorizontal: 16,
paddingVertical: 10, paddingVertical: 10,
borderRadius: 8, borderRadius: 8,
margin: 5,
borderWidth: 2, borderWidth: 2,
borderColor: "transparent", borderColor: "transparent",
flexDirection: "row", flexDirection: "row",
@@ -92,7 +102,6 @@ export const StyledButton: React.FC<StyledButtonProps> = ({
focusedButton: { focusedButton: {
backgroundColor: colors.link, backgroundColor: colors.link,
borderColor: colors.background, borderColor: colors.background,
transform: [{ scale: 1.1 }],
elevation: 5, elevation: 5,
shadowColor: colors.link, shadowColor: colors.link,
shadowOffset: { width: 0, height: 0 }, shadowOffset: { width: 0, height: 0 },
@@ -113,30 +122,33 @@ export const StyledButton: React.FC<StyledButtonProps> = ({
}); });
return ( return (
<Pressable <Animated.View style={[animationStyle, style]}>
style={({ focused }) => [ <Pressable
styles.button, onFocus={() => setIsFocused(true)}
variantStyles[variant].button, onBlur={() => setIsFocused(false)}
isSelected && (variantStyles[variant].selectedButton ?? styles.selectedButton), style={({ focused }) => [
focused && (variantStyles[variant].focusedButton ?? styles.focusedButton), styles.button,
style, variantStyles[variant].button,
]} isSelected && (variantStyles[variant].selectedButton ?? styles.selectedButton),
{...rest} focused && (variantStyles[variant].focusedButton ?? styles.focusedButton),
> ]}
{text ? ( {...rest}
<ThemedText >
style={[ {text ? (
styles.text, <ThemedText
variantStyles[variant].text, style={[
isSelected && (variantStyles[variant].selectedText ?? styles.selectedText), styles.text,
textStyle, variantStyles[variant].text,
]} isSelected && (variantStyles[variant].selectedText ?? styles.selectedText),
> textStyle,
{text} ]}
</ThemedText> >
) : ( {text}
children </ThemedText>
)} ) : (
</Pressable> children
)}
</Pressable>
</Animated.View>
); );
}; };

View File

@@ -0,0 +1,18 @@
import { useRef, useEffect } from 'react';
import { Animated } from 'react-native';
export const useButtonAnimation = (isSelected: boolean, isFocused: boolean) => {
const scaleValue = useRef(new Animated.Value(1)).current;
useEffect(() => {
Animated.spring(scaleValue, {
toValue: isSelected || isFocused ? 1.1 : 1,
friction: 5,
useNativeDriver: true,
}).start();
}, [isSelected, isFocused, scaleValue]);
return {
transform: [{ scale: scaleValue }],
};
};

View File

@@ -11,7 +11,7 @@ export function useThemeColor(
props: {light?: string; dark?: string}, props: {light?: string; dark?: string},
colorName: keyof typeof Colors.light & keyof typeof Colors.dark, colorName: keyof typeof Colors.light & keyof typeof Colors.dark,
) { ) {
const theme = useColorScheme() ?? 'light'; const theme = useColorScheme() ?? 'dark';
const colorFromProps = props[theme]; const colorFromProps = props[theme];
if (colorFromProps) { if (colorFromProps) {