From 74ad0872cbcf4a48d3d4bf714ea4ff902c3b8490 Mon Sep 17 00:00:00 2001 From: zimplexing Date: Tue, 8 Jul 2025 19:52:20 +0800 Subject: [PATCH] Refactor components for consistent styling and improve button animations --- app/_layout.tsx | 24 ++++----- app/detail.tsx | 4 +- app/index.tsx | 7 ++- components/EpisodeSelectionModal.tsx | 26 ++++------ components/StyledButton.tsx | 74 ++++++++++++++++------------ hooks/useButtonAnimation.ts | 18 +++++++ hooks/useThemeColor.ts | 2 +- 7 files changed, 89 insertions(+), 66 deletions(-) create mode 100644 hooks/useButtonAnimation.ts diff --git a/app/_layout.tsx b/app/_layout.tsx index 85b7af4..36adf11 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,21 +1,21 @@ -import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; -import { useFonts } from 'expo-font'; -import { Stack } from 'expo-router'; -import * as SplashScreen from 'expo-splash-screen'; -import { useEffect } from 'react'; -import { Platform, useColorScheme } from 'react-native'; +import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native"; +import { useFonts } from "expo-font"; +import { Stack } from "expo-router"; +import * as SplashScreen from "expo-splash-screen"; +import { useEffect } from "react"; +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. SplashScreen.preventAutoHideAsync(); export default function RootLayout() { - const colorScheme = useColorScheme(); + const colorScheme = useColorScheme() ?? "dark"; 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(() => { initializeSettings(); @@ -35,11 +35,11 @@ export default function RootLayout() { } return ( - + - {Platform.OS !== 'web' && } + {Platform.OS !== "web" && } diff --git a/app/detail.tsx b/app/detail.tsx index aad0c49..4ce1f08 100644 --- a/app/detail.tsx +++ b/app/detail.tsx @@ -182,7 +182,7 @@ export default function DetailScreen() { {item.episodes.length > 1 && ( - {item.episodes.length > 99 ? "99+" : `${item.episodes.length}`} + {item.episodes.length > 99 ? "99+" : `${item.episodes.length}`} 集 )} @@ -275,7 +275,7 @@ const styles = StyleSheet.create({ flexWrap: "wrap", }, sourceButton: { - margin: 5, + margin: 8, }, sourceButtonText: { color: "white", diff --git a/app/index.tsx b/app/index.tsx index c177471..6128a3b 100644 --- a/app/index.tsx +++ b/app/index.tsx @@ -58,7 +58,6 @@ export default function HomeScreen() { text={item.title} onPress={() => handleCategorySelect(item)} isSelected={isSelected} - variant="primary" style={styles.categoryButton} textStyle={styles.categoryText} /> @@ -190,16 +189,16 @@ const styles = StyleSheet.create({ }, // Category Selector categoryContainer: { - paddingBottom: 10, + paddingBottom: 6, }, categoryListContent: { paddingHorizontal: 16, }, categoryButton: { - paddingHorizontal: 12, + paddingHorizontal: 2, paddingVertical: 6, borderRadius: 8, - marginHorizontal: 5, + marginHorizontal: 6, }, categoryText: { fontSize: 16, diff --git a/components/EpisodeSelectionModal.tsx b/components/EpisodeSelectionModal.tsx index 5c81b83..b82b7b2 100644 --- a/components/EpisodeSelectionModal.tsx +++ b/components/EpisodeSelectionModal.tsx @@ -45,7 +45,6 @@ export const EpisodeSelectionModal: React.FC = () => isSelected={selectedEpisodeGroup === groupIndex} style={styles.episodeGroupButton} textStyle={styles.episodeGroupButtonText} - variant="primary" /> ))} @@ -56,6 +55,7 @@ export const EpisodeSelectionModal: React.FC = () => (selectedEpisodeGroup + 1) * episodeGroupSize )} numColumns={5} + contentContainerStyle={styles.episodeList} keyExtractor={(_, index) => `episode-${selectedEpisodeGroup * episodeGroupSize + index}`} renderItem={({ item, index }) => { const absoluteIndex = selectedEpisodeGroup * episodeGroupSize + index; @@ -71,8 +71,6 @@ export const EpisodeSelectionModal: React.FC = () => ); }} /> - - @@ -87,22 +85,25 @@ const styles = StyleSheet.create({ backgroundColor: "transparent", }, modalContent: { - width: 400, + width: 600, height: "100%", backgroundColor: "rgba(0, 0, 0, 0.85)", padding: 20, }, modalTitle: { color: "white", - marginBottom: 20, + marginBottom: 12, textAlign: "center", fontSize: 18, fontWeight: "bold", }, + episodeList: { + justifyContent: "flex-start", + }, episodeItem: { - paddingVertical: 12, + paddingVertical: 2, margin: 4, - flex: 1, + width: "18%", }, episodeItemText: { fontSize: 14, @@ -111,20 +112,13 @@ const styles = StyleSheet.create({ flexDirection: "row", flexWrap: "wrap", justifyContent: "center", - marginBottom: 15, paddingHorizontal: 10, }, episodeGroupButton: { - paddingHorizontal: 12, - paddingVertical: 6, - borderRadius: 15, - margin: 5, + paddingHorizontal: 6, + margin: 8, }, episodeGroupButtonText: { fontSize: 12, }, - closeButton: { - padding: 15, - marginTop: 20, - }, }); diff --git a/components/StyledButton.tsx b/components/StyledButton.tsx index 3789956..55192e5 100644 --- a/components/StyledButton.tsx +++ b/components/StyledButton.tsx @@ -1,7 +1,17 @@ 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 { Colors } from "@/constants/Colors"; +import { useButtonAnimation } from "@/hooks/useButtonAnimation"; interface StyledButtonProps extends PressableProps { children?: React.ReactNode; @@ -21,8 +31,10 @@ export const StyledButton: React.FC = ({ textStyle, ...rest }) => { - const colorScheme = useColorScheme() ?? "light"; + const colorScheme = useColorScheme() ?? "dark"; const colors = Colors[colorScheme]; + const [isFocused, setIsFocused] = React.useState(false); + const animationStyle = useButtonAnimation(isSelected, isFocused); const variantStyles = { default: StyleSheet.create({ @@ -56,7 +68,6 @@ export const StyledButton: React.FC = ({ }, selectedButton: { backgroundColor: "rgba(0, 122, 255, 0.3)", - transform: [{ scale: 1.1 }], }, selectedText: { color: colors.link, @@ -79,10 +90,9 @@ export const StyledButton: React.FC = ({ const styles = StyleSheet.create({ button: { - paddingHorizontal: 15, + paddingHorizontal: 16, paddingVertical: 10, borderRadius: 8, - margin: 5, borderWidth: 2, borderColor: "transparent", flexDirection: "row", @@ -92,7 +102,6 @@ export const StyledButton: React.FC = ({ focusedButton: { backgroundColor: colors.link, borderColor: colors.background, - transform: [{ scale: 1.1 }], elevation: 5, shadowColor: colors.link, shadowOffset: { width: 0, height: 0 }, @@ -113,30 +122,33 @@ export const StyledButton: React.FC = ({ }); return ( - [ - styles.button, - variantStyles[variant].button, - isSelected && (variantStyles[variant].selectedButton ?? styles.selectedButton), - focused && (variantStyles[variant].focusedButton ?? styles.focusedButton), - style, - ]} - {...rest} - > - {text ? ( - - {text} - - ) : ( - children - )} - + + setIsFocused(true)} + onBlur={() => setIsFocused(false)} + style={({ focused }) => [ + styles.button, + variantStyles[variant].button, + isSelected && (variantStyles[variant].selectedButton ?? styles.selectedButton), + focused && (variantStyles[variant].focusedButton ?? styles.focusedButton), + ]} + {...rest} + > + {text ? ( + + {text} + + ) : ( + children + )} + + ); }; diff --git a/hooks/useButtonAnimation.ts b/hooks/useButtonAnimation.ts new file mode 100644 index 0000000..229516b --- /dev/null +++ b/hooks/useButtonAnimation.ts @@ -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 }], + }; +}; \ No newline at end of file diff --git a/hooks/useThemeColor.ts b/hooks/useThemeColor.ts index a42a34c..3f019d1 100644 --- a/hooks/useThemeColor.ts +++ b/hooks/useThemeColor.ts @@ -11,7 +11,7 @@ export function useThemeColor( props: {light?: string; dark?: string}, colorName: keyof typeof Colors.light & keyof typeof Colors.dark, ) { - const theme = useColorScheme() ?? 'light'; + const theme = useColorScheme() ?? 'dark'; const colorFromProps = props[theme]; if (colorFromProps) {