From 0beddaef68046446d38f2b5bb48aafbfbcbaaefa Mon Sep 17 00:00:00 2001 From: DH Date: Sat, 23 Aug 2025 21:07:23 +0300 Subject: [PATCH] Simplify navigation, improve animations --- package.json | 2 +- rpcsx-ui-kit/src/generators.ts | 127 +++--- rpcsx-ui/src/core/renderer/ViewSelector.tsx | 426 +++++++++--------- .../server/registerBuiltinLaunchers.web.ts | 3 +- .../src/explorer/renderer/views/Explorer.tsx | 88 ++-- .../src/settings/renderer/views/Settings.tsx | 42 +- 6 files changed, 346 insertions(+), 342 deletions(-) diff --git a/package.json b/package.json index 8d65e5d..ef7b569 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "build:all": "npm run build && npm run build:web && npm run build:android", "validate": "", "dev:ui": "npx expo start --dev-client", - "dev:web:server": "electron build/main.js --dev", + "dev:web:server": "electron electron/build/main.js --dev", "install:android:release": "adb install android/app/build/outputs/apk/release/app-release.apk", "install:android": "adb install android/app/build/outputs/apk/debug/app-debug.apk" }, diff --git a/rpcsx-ui-kit/src/generators.ts b/rpcsx-ui-kit/src/generators.ts index 7012210..25ab535 100644 --- a/rpcsx-ui-kit/src/generators.ts +++ b/rpcsx-ui-kit/src/generators.ts @@ -1908,40 +1908,46 @@ declare global { navigationFile.content = `${generatedHeader} ${Object.keys(views).map(x => `import { ${x} } from '${pathWithoutExt(views[x])}'`).join(';\n')}; import * as bridge from '$core/bridge'; +import { TopViewSelector } from '$core/ViewSelector'; +import * as SplashScreen from 'expo-splash-screen'; +import { BackHandler } from 'react-native'; +import { useEffect, useState } from 'react'; + +SplashScreen.preventAutoHideAsync(); const views: Record React.JSX.Element> = { ${Object.keys(views).map(x => ` "${x}": ${x}`).join(',\n')} }; -let onViewChangeCb: ((view: React.JSX.Element) => void) | undefined; +let onViewChangeCb: (() => void) | undefined; let viewStack: React.JSX.Element[] = []; -export function onViewChange(cb?: (view: React.JSX.Element | undefined) => void) { - onViewChangeCb = cb; - if (cb && viewStack.length != 0) { - cb(viewStack[viewStack.length - 1]); - } -} - function update() { if (onViewChangeCb) { - onViewChangeCb(viewStack[viewStack.length - 1]); + onViewChangeCb(); } } -export function viewPush(name: string, props: any) { +function renderView(name: string, props: any) { const View = views[name]; - viewStack.push(); + return ; +} + +function viewPush(name: string, props: any) { + if (viewStack.length == 0) { + SplashScreen.hideAsync(); + } + + viewStack.push(renderView(name, props)); update(); } -export function viewSet(name: string, props: any) { - const View = views[name]; - viewStack = []; +function viewSet(name: string, props: any) { + viewStack = [renderView(name, props)]; update(); } -export function viewPop() { +function viewPop() { if (viewStack.length < 2) { return false; } @@ -1951,6 +1957,36 @@ export function viewPop() { return true; } +export function Navigation() { + const [renderItem, setRenderItem] = useState(viewStack.length - 1); + + useEffect(() => { + onViewChangeCb = () => { + if (viewStack.length > 0) { + const item = viewStack.length - 1; + if (item != renderItem) { + setRenderItem(item); + } + } + }; + }); + + useEffect(() => { + const backAction = () => { + return viewPop(); + }; + + const backHandler = BackHandler.addEventListener( + 'hardwareBackPress', + backAction + ); + + return () => backHandler.remove(); + }, []); + + return ; +} + bridge.onViewPush(viewPush); bridge.onViewSet(viewSet); bridge.onViewPop(viewPop); @@ -1985,63 +2021,28 @@ export async function startup() { if (indexFile) { indexFile.content = `${generatedHeader} import '@expo/metro-runtime'; -// import { Asset } from 'expo-asset'; -import * as SplashScreen from 'expo-splash-screen'; -import { SafeAreaProvider } from 'react-native-safe-area-context'; -import { StrictMode } from 'react'; -import { registerRootComponent } from 'expo'; -import * as navigation from './navigation'; -import { useEffect, useState } from 'react'; -import * as bridge from '$core/bridge'; -import { BackHandler } from 'react-native'; -import { TopViewSelector } from '$core/ViewSelector'; -import { startup } from './startup'; -SplashScreen.preventAutoHideAsync(); +// import { Asset } from 'expo-asset'; +import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { registerRootComponent } from 'expo'; +import { Navigation } from './navigation'; +import { useEffect } from 'react'; +import * as bridge from '$core/bridge'; +import { startup } from './startup'; const startupPromise = startup(); function App() { - const [renderElement, setRenderElement] = useState(undefined); - const [initialized, setInitialized] = useState(false); - const [viewInitializationSent, setViewInitializationSent] = useState(false); - useEffect(() => { - navigation.onViewChange(view => { - setRenderElement(view); - - if (!initialized) { - SplashScreen.hideAsync(); - setInitialized(true); - } + startupPromise.then(() => { + bridge.sendViewInitializationComplete(); }); - - if (!viewInitializationSent) { - startupPromise.then(() => { - bridge.sendViewInitializationComplete(); - }); - - setViewInitializationSent(true); - } - - const backAction = () => { - return navigation.viewPop(); - }; - - const backHandler = BackHandler.addEventListener( - 'hardwareBackPress', - backAction - ); - - return () => backHandler.remove(); - }); + }, []); return ( - - - - - + + + ) } diff --git a/rpcsx-ui/src/core/renderer/ViewSelector.tsx b/rpcsx-ui/src/core/renderer/ViewSelector.tsx index d32b3da..014b021 100644 --- a/rpcsx-ui/src/core/renderer/ViewSelector.tsx +++ b/rpcsx-ui/src/core/renderer/ViewSelector.tsx @@ -1,7 +1,5 @@ -import { scheduleOnMainThread } from './scheduleOnMainThread'; import { useEffect, useState } from 'react'; -import { StyleProp, StyleSheet, View, ViewStyle, useWindowDimensions } from 'react-native'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { StyleProp, View, ViewStyle, useWindowDimensions } from 'react-native'; import Animated, { Easing, @@ -9,250 +7,244 @@ import Animated, { useSharedValue, interpolate, withTiming, - cancelAnimation, - withSequence + withSequence, + FadeIn, + FadeOut, } from 'react-native-reanimated'; import { useThemeColor } from './useThemeColor'; -export function LeftRightViewSelector(props: { selectedItem: number, list: T[], renderItem: (item: T) => React.JSX.Element, style?: StyleProp }) { - const [selectedItem, setSelectedItem] = useState(props.selectedItem); - const [prevSelectedItem, setPrevSelectedItem] = useState(-1); +export function ViewSelector(props: { + selectedItem: number, + list: T[], + renderItem: (item: T) => React.JSX.Element, + style?: StyleProp, + showStyle: (indexIncreased: boolean, value: number) => ShowStyle, + hideStyle: (indexIncreased: boolean, value: number) => HideStyle, + activate: () => number +}) { const animation = useSharedValue(100); - const layout = useWindowDimensions(); - const directionRight = selectedItem > prevSelectedItem; - const hideStyle = useAnimatedStyle(() => ({ - flexWrap: 'nowrap', - transform: [{ - translateX: directionRight ? interpolate(animation.value, [0, 100], [0, -layout.width]) : interpolate(animation.value, [0, 100], [0, layout.width]) - }], - opacity: interpolate(animation.value, [0, 100], [1, 0]) - })); + type SelectorState = { + views: (React.JSX.Element | undefined)[]; + index: number; + itemIndex: number[]; + }; - const showStyle = useAnimatedStyle(() => ({ - flexWrap: 'nowrap', - transform: [{ - translateX: directionRight ? interpolate(animation.value, [0, 100], [layout.width, 0]) : interpolate(animation.value, [0, 100], [-layout.width, 0]) - }], - opacity: interpolate(animation.value, [0, 100], [0, 1]) - })); + const currentView = props.list[props.selectedItem]; - useEffect(() => { - if (selectedItem != props.selectedItem) { - setPrevSelectedItem(selectedItem); - setSelectedItem(props.selectedItem); - animation.value = 0; - } - - if (prevSelectedItem != -1) { - animation.value = withTiming(100, { - duration: 400, - easing: Easing.inOut(Easing.ease) - }, (finished) => { - if (finished) { - scheduleOnMainThread(setPrevSelectedItem, -1); - } - }); - } + const [state, setState] = useState({ + views: [currentView ? props.renderItem(currentView) : undefined, undefined], + index: 0, + itemIndex: [currentView ? props.selectedItem : -1, -1] }); - const item = props.renderItem(props.list[selectedItem]); + const [indexIncreased, setIndexIncreased] = useState(false); + const prevItemIndex = state.itemIndex[state.index]; - if (prevSelectedItem == -1) { - return ({item}) - } + useEffect(() => { + if (props.selectedItem != prevItemIndex) { + if (state.itemIndex[0] >= 0) { + setIndexIncreased(props.selectedItem > prevItemIndex); + animation.value = 0; + animation.value = props.activate(); + } + + const nextRenderIndex = (state.index + 1) & 1; + state.views[nextRenderIndex] = props.renderItem(currentView); + state.itemIndex[nextRenderIndex] = props.selectedItem; + + setState({ + views: state.views, + index: nextRenderIndex, + itemIndex: state.itemIndex, + }); + } + + }, [props.selectedItem]); + + const waitAnimation = props.selectedItem != state.itemIndex[state.index]; + + const hideStyle = useAnimatedStyle(() => props.hideStyle(indexIncreased, waitAnimation ? 0 : animation.value)); + const showStyle = useAnimatedStyle(() => props.showStyle(indexIncreased, waitAnimation ? 0 : animation.value)); + + const drawIndex = waitAnimation ? 1 - state.index : state.index; + + const styles = [ + drawIndex == 0 ? showStyle : hideStyle, + drawIndex == 1 ? showStyle : hideStyle, + ]; + + const staticStyle: StyleProp = { position: 'absolute', height: "100%", width: "100%" }; return ( - {prevSelectedItem != -1 && - {props.renderItem(props.list[prevSelectedItem])} - } - { - {item} - } + + {state.views[0]} + + + {state.views[1]} + ); } + +export function LeftRightViewSelector(props: { selectedItem: number, list: T[], renderItem: (item: T) => React.JSX.Element, style?: StyleProp }) { + const layout = useWindowDimensions(); + + function activate() { + return withTiming(100, { + duration: 400, + easing: Easing.inOut(Easing.ease) + }); + } + + function hideStyle(directionRight: boolean, animationValue: number): ViewStyle { + 'worklet'; + + return { + transform: [{ + translateX: directionRight ? interpolate(animationValue, [0, 100], [0, -layout.width]) : interpolate(animationValue, [0, 100], [0, layout.width]) + }], + opacity: interpolate(animationValue, [0, 100], [1, 0]) + }; + } + + function showStyle(directionRight: boolean, animationValue: number): ViewStyle { + 'worklet'; + return { + transform: [{ + translateX: directionRight ? interpolate(animationValue, [0, 100], [layout.width, 0]) : interpolate(animationValue, [0, 100], [-layout.width, 0]) + }], + opacity: interpolate(animationValue, [0, 100], [0, 1]) + }; + } + + return ; +} + export function UpDownViewSelector(props: { selectedItem: number, list: T[], renderItem: (item: T) => React.JSX.Element, style?: StyleProp }) { - const [selectedItem, setSelectedItem] = useState(props.selectedItem); - const [prevSelectedItem, setPrevSelectedItem] = useState(-1); - const animation = useSharedValue(100); const layout = useWindowDimensions(); - const directionDown = selectedItem > prevSelectedItem; - const hideStyle = useAnimatedStyle(() => ({ - flexWrap: 'nowrap', - transform: [{ - translateY: directionDown ? interpolate(animation.value, [0, 100], [0, -layout.height]) : interpolate(animation.value, [0, 100], [0, layout.height]) - }], - opacity: interpolate(animation.value, [0, 100], [1, 0]) - })); - - const showStyle = useAnimatedStyle(() => ({ - flexWrap: 'nowrap', - transform: [{ - translateY: directionDown ? interpolate(animation.value, [0, 100], [layout.height, 0]) : interpolate(animation.value, [0, 100], [-layout.height, 0]) - }], - opacity: interpolate(animation.value, [0, 100], [0, 1]) - })); - - useEffect(() => { - if (selectedItem != props.selectedItem) { - setPrevSelectedItem(selectedItem); - setSelectedItem(props.selectedItem); - animation.value = 0; - } - - if (prevSelectedItem != -1) { - animation.value = withTiming(100, { - duration: 400, - easing: Easing.inOut(Easing.ease) - }, (finished) => { - if (finished) { - scheduleOnMainThread(setPrevSelectedItem, -1); - } - }); - } - }); - - const item = props.renderItem(props.list[selectedItem]); - - if (prevSelectedItem == -1) { - return ({item}) + function activate() { + return withTiming(100, { + duration: 400, + easing: Easing.inOut(Easing.ease) + }); } - return ( - - {prevSelectedItem != -1 && - {props.renderItem(props.list[prevSelectedItem])} - } - { - {item} - } - - ); + function hideStyle(directionDown: boolean, animationValue: number): ViewStyle { + 'worklet'; + const y = directionDown ? interpolate(animationValue, [0, 100], [0, -layout.height]) : interpolate(animationValue, [0, 100], [0, layout.height]); + + return { + transform: [{ + translateY: y + }], + height: Math.max(layout.height, layout.height - y), + opacity: interpolate(animationValue, [0, 100], [1, 0]) + }; + } + + function showStyle(directionDown: boolean, animationValue: number): ViewStyle { + 'worklet'; + const y = directionDown ? interpolate(animationValue, [0, 100], [layout.height, 0]) : interpolate(animationValue, [0, 100], [-layout.height, 0]); + return { + transform: [{ + translateY: y + }], + height: Math.max(layout.height, y + layout.height), + opacity: interpolate(animationValue, [0, 100], [0, 1]) + }; + } + + return ; } export function DownShowViewSelector(props: { selectedItem: number, list: T[], renderItem: (item: T) => React.JSX.Element, style?: StyleProp }) { - const [selectedItem, setSelectedItem] = useState(props.selectedItem); - const [prevSelectedItem, setPrevSelectedItem] = useState(-1); - const animation = useSharedValue(200); const layout = useWindowDimensions(); - const hideStyle = useAnimatedStyle(() => ({ - flexWrap: 'nowrap', - transform: [{ - translateY: animation.value < 100 ? interpolate(animation.value, [0, 100], [0, layout.height], 'clamp') : layout.height - }], - opacity: animation.value < 100 ? interpolate(animation.value * 10, [0, 100], [1, 0], 'clamp') : 0, - display: animation.value < 100 ? undefined : 'none' - })); - - const showStyle = useAnimatedStyle(() => ({ - flexWrap: 'nowrap', - opacity: interpolate(animation.value, [100, 200], [0, 1], 'clamp') - })); - - - useEffect(() => { - if (selectedItem != props.selectedItem) { - cancelAnimation(animation); - - setPrevSelectedItem(selectedItem); - setSelectedItem(props.selectedItem); - animation.value = 0; - } - - if (prevSelectedItem != -1) { - animation.value = withSequence( - withTiming(100, { - duration: 900, - easing: Easing.inOut(Easing.ease) - }), - withTiming(200, { - duration: 600, - easing: Easing.inOut(Easing.ease) - }, (finished) => { - if (finished) { - scheduleOnMainThread(setPrevSelectedItem, -1); - } - }) - ); - } - }); - - const showItem = props.renderItem(props.list[selectedItem]); - - if (prevSelectedItem == -1) { - return ({showItem}) - } - - const hideItem = props.renderItem(props.list[prevSelectedItem]); - - return ( - - - {hideItem} - - - {showItem} - - - ); -} - -export function TopViewSelector(props: { view?: React.JSX.Element }) { - const animation = useSharedValue(0); - const [prevView, setPrevView] = useState(undefined); - const [currentView, setCurrentView] = useState(undefined); - - const hideStyle = useAnimatedStyle(() => ({ - opacity: interpolate(animation.value * 5, [0, 100], [1, 0], 'clamp'), - })); - - const showStyle = useAnimatedStyle(() => ({ - opacity: interpolate(animation.value, [0, 100], [0, 1], 'clamp'), - })); - - - useEffect(() => { - if (props.view != currentView) { - setCurrentView(props.view); - if (currentView) { - setPrevView(currentView); - cancelAnimation(animation); - animation.value = 0; - } - } - - animation.value = withSequence( + function activate() { + return withSequence( + withTiming(50, { + duration: 900, + easing: Easing.inOut(Easing.ease) + }), withTiming(100, { - duration: 500, - easing: Easing.ease - }, (finished) => { - if (finished) { - animation.value = 0; - scheduleOnMainThread(setPrevView, undefined); - } + duration: 600, + easing: Easing.inOut(Easing.ease) }) ); - }); - - const backgroundColor = useThemeColor("background"); - - if (!prevView) { - return currentView; } - return ( - - - {prevView} - - - {currentView} - - - ); + function hideStyle(_indexIncreased: boolean, animationValue: number): ViewStyle { + 'worklet'; + + return { + transform: [{ + translateY: interpolate(animationValue, [0, 50], [0, layout.height], 'clamp') + }], + opacity: interpolate(animationValue * 10, [0, 50], [1, 0], 'clamp') + }; + } + + function showStyle(_indexIncreased: boolean, animationValue: number): ViewStyle { + 'worklet'; + + return { + transform: [{ + translateY: 0 + }], + opacity: interpolate(animationValue, [50, 100], [0, 1], 'clamp') + }; + } + + return ; } + +export function TopViewSelector(props: { stack: React.JSX.Element[], index: number }) { + const currentView = props.stack[props.index]; + + type SelectorState = { + views: (React.JSX.Element | undefined)[]; + index: number; + }; + + const [state, setState] = useState({ views: [currentView, undefined], index: currentView == undefined ? -1 : 0 }); + + useEffect(() => { + const nextRenderIndex = (state.index + 1) & 1; + state.views[nextRenderIndex] = currentView; + + setState({ + views: state.views, + index: nextRenderIndex + }); + }, [props.index]); + + const backgroundColor = useThemeColor("background"); + const style: StyleProp = { height: "100%", width: "100%" }; + + return ( + + {state.index == 0 && + {state.views[0]} + } + {state.index == 1 && + {state.views[1]} + } + + ) +}; diff --git a/rpcsx-ui/src/core/server/registerBuiltinLaunchers.web.ts b/rpcsx-ui/src/core/server/registerBuiltinLaunchers.web.ts index 0ec58eb..1490d45 100644 --- a/rpcsx-ui/src/core/server/registerBuiltinLaunchers.web.ts +++ b/rpcsx-ui/src/core/server/registerBuiltinLaunchers.web.ts @@ -2,7 +2,8 @@ import { dirname } from "path"; import { addLauncher, Launcher, LaunchParams, Process } from "./Launcher"; import { Target } from "./Target"; import { fork, spawn } from "child_process"; -import { Duplex, EventEmitter } from "stream"; +import { Duplex } from "stream"; +import { EventEmitter } from "events"; const nativeLauncher: Launcher = { launch: async (path: string, args: string[], params: LaunchParams) => { diff --git a/rpcsx-ui/src/explorer/renderer/views/Explorer.tsx b/rpcsx-ui/src/explorer/renderer/views/Explorer.tsx index 591fbf9..efff4bc 100644 --- a/rpcsx-ui/src/explorer/renderer/views/Explorer.tsx +++ b/rpcsx-ui/src/explorer/renderer/views/Explorer.tsx @@ -1,4 +1,4 @@ -import { ComponentProps, ReactElement, useEffect, useState } from 'react'; +import { ComponentProps, memo, ReactElement, useEffect, useState } from 'react'; import { Image, Pressable, ScrollView, StyleSheet, View, FlatList } from 'react-native'; import { useThemeColor } from '$core/useThemeColor' import { } from '@react-navigation/elements'; @@ -19,7 +19,6 @@ import Animated, { cancelAnimation, interpolateColor, } from 'react-native-reanimated'; -import { useColorScheme } from '$core/useColorScheme'; const games: (ExecutableInfo & ExplorerItem)[] = [ @@ -223,7 +222,7 @@ type ScreenTabProps = { const AnimatedThemedText = Animated.createAnimatedComponent(ThemedText); -function ScreenTab(props: Omit, "children"> & ScreenTabProps) { +const ScreenTab = memo(function (props: Omit, "children"> & ScreenTabProps) { const [activeState, setActiveState] = useState(props.active); const animation = useSharedValue(props.active ? 100 : 0); @@ -258,11 +257,11 @@ function ScreenTab(props: Omit, "children } /> ); -} +}); const AnimatedPressable = Animated.createAnimatedComponent(Pressable); -function ExplorerItemHeader({ item, active, ...rest }: { item: ExplorerItem, active: boolean, } & ComponentProps) { +const ExplorerItemHeader = memo(function ({ item, active, ...rest }: { item: ExplorerItem, active: boolean, } & ComponentProps) { const [activeState, setActiveState] = useState(active); const animation = useSharedValue(active ? 100 : 0); @@ -311,30 +310,32 @@ function ExplorerItemHeader({ item, active, ...rest }: { item: ExplorerItem, act {getName(item)} } -} +}); -function ExplorerItemBody({ item }: { item: ExplorerItem }) { - return ( - {item.description && {item.description && getLocalizedString(item.description)}} - ) -} +const ExplorerItemBody = memo(function ({ item }: { item: ExplorerItem }) { + return ( + + {item.description && {item.description && getLocalizedString(item.description)}} + + ) +}); -function ExplorerView({ items }: { items: ExplorerItem[] }) { +const ExplorerView = function ({ items }: { items: ExplorerItem[] }) { const styles = StyleSheet.create({ topContainer: { - flex: 1, - flexGrow: 1, + width: "100%", + height: "100%" }, scrollContainer: { + minHeight: 150, + maxHeight: 250, flex: 1, - flexGrow: 1, - marginHorizontal: 60, + marginTop: 30, + marginBottom: 60, }, scrollContentContainer: { }, descriptionContainer: { - flex: 1, - flexGrow: 1, flexDirection: 'column', } }); @@ -342,21 +343,25 @@ function ExplorerView({ items }: { items: ExplorerItem[] }) { const [selectedItem, selectItem] = useState(0); return ( - - - { - const name = getName(item); - return selectItem(index)} active={index == selectedItem}> - } - }> - - - } selectedItem={selectedItem} /> + + + + { + const name = getName(item); + return selectItem(index)} active={index == selectedItem}> + } + }> + + } + selectedItem={selectedItem} /> + ); -} +}; type Screen = { title: string; @@ -371,10 +376,16 @@ type Props = { }; const ExplorerStyles = StyleSheet.create({ - rootContainer: { height: "100%", width: "100%" }, + rootContainer: { + width: "100%", + height: "100%" + }, + headerContainer: { + flex: 1, + }, menuContainer: { + height: 40, flexDirection: 'row', - minHeight: 50, marginLeft: 60, marginRight: 40, marginTop: 60 @@ -422,12 +433,11 @@ const screens: Screen[] = [ export function Explorer(props?: Props) { const [background, setBackground] = useState(undefined); - const [activeTab, setActiveTab] = useState(1); - const theme = useColorScheme(); + const [activeTab, setActiveTab] = useState(0); return ( - - + + { @@ -437,7 +447,7 @@ export function Explorer(props?: Props) { } - + settings.pushSettingsView({})}> @@ -446,9 +456,9 @@ export function Explorer(props?: Props) { - item.view((image) => setBackground(image))} selectedItem={activeTab} /> + item.view((image) => setBackground(image))} selectedItem={activeTab} /> ) -} +}; diff --git a/rpcsx-ui/src/settings/renderer/views/Settings.tsx b/rpcsx-ui/src/settings/renderer/views/Settings.tsx index f24e628..505206f 100644 --- a/rpcsx-ui/src/settings/renderer/views/Settings.tsx +++ b/rpcsx-ui/src/settings/renderer/views/Settings.tsx @@ -1,4 +1,4 @@ -import { ComponentProps, useEffect, useState } from 'react'; +import { ComponentProps, memo, useEffect, useState } from 'react'; import { ScrollView, StyleSheet, View, Switch, Alert, TextInput, Modal, useWindowDimensions, BackHandler } from 'react-native'; import { useThemeColor } from '$core/useThemeColor' import ThemedIcon from '$core/ThemedIcon'; @@ -25,7 +25,7 @@ type SettingsTabProps = { iconSet: 'Ionicons' | 'FontAwesome6' | 'MaterialIcons'; }; -function SettingsTab(props: Omit, "children"> & SettingsTabProps) { +const SettingsTab = memo(function (props: Omit, "children"> & SettingsTabProps) { const [activeState, setActiveState] = useState(props.active); const animation = useSharedValue(props.active ? 100 : 0); @@ -60,7 +60,7 @@ function SettingsTab(props: Omit, "childr ); -} +}) enum SettingItemType { Text, @@ -159,7 +159,7 @@ type SettingsItemProps = SettingItemSingleSelectionProps | SettingItemMultipleSelectionProps; -function TextInputModal({ visible, onClose, value, onSave, title, placeholder, mode = SettingTextMode.Plain }: { +const TextInputModal = memo(function ({ visible, onClose, value, onSave, title, placeholder, mode = SettingTextMode.Plain }: { visible: boolean; onClose: () => void; value: string; @@ -187,7 +187,7 @@ function TextInputModal({ visible, onClose, value, onSave, title, placeholder, m try { const result = await pickDirectory({ requestLongTermAccess: true }); setInputValue(result.uri); - } catch {} + } catch { } }; return ( @@ -231,9 +231,9 @@ function TextInputModal({ visible, onClose, value, onSave, title, placeholder, m ); -} +}); -function NumberInputModal({ visible, onClose, value, onSave, title, placeholder }: { +const NumberInputModal = memo(function ({ visible, onClose, value, onSave, title, placeholder }: { visible: boolean; onClose: () => void; value: number; @@ -289,9 +289,9 @@ function NumberInputModal({ visible, onClose, value, onSave, title, placeholder ); -} +}); -function DateTimeModal({ visible, onClose, value, onSave, title, mode }: { +const DateTimeModal = memo(function ({ visible, onClose, value, onSave, title, mode }: { visible: boolean; onClose: () => void; value: Date; @@ -380,9 +380,9 @@ function DateTimeModal({ visible, onClose, value, onSave, title, mode }: { ); -} +}); -function ListManagerModal({ visible, onClose, items, onSave, title, placeholder }: { +const ListManagerModal = memo(function ({ visible, onClose, items, onSave, title, placeholder }: { visible: boolean; onClose: () => void; items: string[]; @@ -472,9 +472,9 @@ function ListManagerModal({ visible, onClose, items, onSave, title, placeholder ); -} +}); -function SingleSelectionModal({ visible, onClose, options, selectedIndex, onSave, title }: { +const SingleSelectionModal = memo(function ({ visible, onClose, options, selectedIndex, onSave, title }: { visible: boolean; onClose: () => void; options: string[]; @@ -542,9 +542,9 @@ function SingleSelectionModal({ visible, onClose, options, selectedIndex, onSave ); -} +}); -function MultipleSelectionModal({ visible, onClose, options, selectedIndices, onSave, title }: { +const MultipleSelectionModal = memo(function ({ visible, onClose, options, selectedIndices, onSave, title }: { visible: boolean; onClose: () => void; options: string[]; @@ -617,9 +617,9 @@ function MultipleSelectionModal({ visible, onClose, options, selectedIndices, on ); -} +}); -function SettingsItem(props: SettingsItemProps & { isLast: boolean }) { +const SettingsItem = memo(function (props: SettingsItemProps & { isLast: boolean }) { const borderColor = useThemeColor("outline"); const backgroundColor = useThemeColor("surface"); const primaryColor = useThemeColor('primary'); @@ -779,7 +779,7 @@ function SettingsItem(props: SettingsItemProps & { isLast: boolean }) { )} ); -} +}); type SettingsSection = { title: string; @@ -1032,11 +1032,11 @@ const settingsCategories: SettingsCategory[] = [ } ]; -function SettingsContent({ category }: { category: SettingsCategory }) { +const SettingsContent = memo(function ({ category }: { category: SettingsCategory }) { return ( {category.sections.map((section, sectionIndex) => ( - + {section.title} @@ -1049,7 +1049,7 @@ function SettingsContent({ category }: { category: SettingsCategory }) { ))} ); -} +}); type Props = { category?: string;