diff --git a/App.tsx b/App.tsx index 1a97dfca8e..d96ec2d59f 100644 --- a/App.tsx +++ b/App.tsx @@ -9,13 +9,13 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; import LottieSplashScreen from 'react-native-lottie-splash-screen'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { Provider as PaperProvider } from 'react-native-paper'; +import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import * as Notifications from 'expo-notifications'; import { createTables } from '@database/db'; import AppErrorBoundary from '@components/AppErrorBoundary/AppErrorBoundary'; import Main from './src/navigators/Main'; -import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; // Rozenite DevTools import { useReactNavigationDevTools } from '@rozenite/react-navigation-plugin'; @@ -26,6 +26,13 @@ import { store } from '@plugins/helpers/storage'; import { MMKVStorage } from '@utils/mmkv/mmkv'; import { NavigationContainerRef } from '@react-navigation/native'; +declare global { + interface ObjectConstructor { + typedKeys(obj: T): Array; + } +} +Object.typedKeys = Object.keys as any; + Notifications.setNotificationHandler({ handleNotification: async () => { return { diff --git a/android/app/src/main/assets/js/core.js b/android/app/src/main/assets/js/core.js index 5ce1763490..8869792f93 100644 --- a/android/app/src/main/assets/js/core.js +++ b/android/app/src/main/assets/js/core.js @@ -58,7 +58,7 @@ window.reader = new (function () { const settings = this.readerSettings.val; document.documentElement.style.setProperty( '--readerSettings-theme', - settings.theme, + settings.backgroundColor, ); document.documentElement.style.setProperty( '--readerSettings-padding', diff --git a/flake.nix b/flake.nix index affae74499..b0edb479fe 100644 --- a/flake.nix +++ b/flake.nix @@ -67,7 +67,6 @@ which rsync scrcpy - ] ++ pkgs.lib.optionals enableEmulator [ libglvnd @@ -175,7 +174,7 @@ echo " scrcpy" echo "--------------------------" ''; - }; + }; }; }); } \ No newline at end of file diff --git a/ln b/ln deleted file mode 160000 index 7522993bcb..0000000000 --- a/ln +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 7522993bcb70a745181dd5b390d6c079c9d5666e diff --git a/metro.config.js b/metro.config.js index 52c8e9be8d..998a9192f8 100644 --- a/metro.config.js +++ b/metro.config.js @@ -26,6 +26,14 @@ const customConfig = { resolver: { unstable_enableSymlinks: true, // For pnpm symlinks }, + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: false, // temporarily disable to rule out bundle fetch issue + }, + }), + }, server: { port: 8081, enhanceMiddleware: (metroMiddleware, metroServer) => { @@ -52,4 +60,4 @@ const customConfig = { }, }, }; -module.exports = withRozenite(mergeConfig(defaultConfig, customConfig)); \ No newline at end of file +module.exports = withRozenite(mergeConfig(defaultConfig, customConfig)); diff --git a/package.json b/package.json index f05d9af434..1211e88f47 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,6 @@ "@react-navigation/bottom-tabs": "^7.4.7", "@react-navigation/native": "^7.1.17", "@react-navigation/native-stack": "^7.3.26", - "@react-navigation/stack": "^7.4.8", "@shopify/flash-list": "2.0.2", "cheerio": "1.0.0-rc.12", "color": "^5.0.0", @@ -137,5 +136,8 @@ }, "engines": { "node": ">=20" - } + }, + "trustedDependencies": [ + "protobufjs" + ] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4335446e71..5a45dd3e8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,9 +44,6 @@ importers: '@react-navigation/native-stack': specifier: ^7.3.26 version: 7.3.26(@react-navigation/native@7.1.17(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) - '@react-navigation/stack': - specifier: ^7.4.8 - version: 7.4.8(aa18acaa8df835834183132a0ed97cd5) '@shopify/flash-list': specifier: 2.0.2 version: 2.0.2(@babel/runtime@7.28.4)(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) @@ -1523,16 +1520,6 @@ packages: '@react-navigation/routers@7.5.1': resolution: {integrity: sha512-pxipMW/iEBSUrjxz2cDD7fNwkqR4xoi0E/PcfTQGCcdJwLoaxzab5kSadBLj1MTJyT0YRrOXL9umHpXtp+Dv4w==} - '@react-navigation/stack@7.4.8': - resolution: {integrity: sha512-zZsX52Nw1gsq33Hx4aNgGV2RmDJgVJM71udomCi3OdlntqXDguav3J2t5oe/Acf/9uU8JiJE9W8JGtoRZ6nXIg==} - peerDependencies: - '@react-navigation/native': ^7.1.17 - react: '>= 18.2.0' - react-native: '*' - react-native-gesture-handler: '>= 2.0.0' - react-native-safe-area-context: '>= 4.0.0' - react-native-screens: '>= 4.0.0' - '@rock-js/config@0.11.1': resolution: {integrity: sha512-BFAxWChmLyxwdWB/nL9oMihnTHIBxYR15V+/MhU0rf9p1KCeU6wh0pP2LNi7eB9zTwDiLUEGveSwjTVeygM32g==} @@ -7107,19 +7094,6 @@ snapshots: dependencies: nanoid: 3.3.11 - '@react-navigation/stack@7.4.8(aa18acaa8df835834183132a0ed97cd5)': - dependencies: - '@react-navigation/elements': 2.6.4(@react-navigation/native@7.1.17(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.1(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) - '@react-navigation/native': 7.1.17(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) - color: 4.2.3 - react: 19.1.0 - react-native: 0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0) - react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) - react-native-safe-area-context: 5.6.1(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.4)(@react-native-community/cli@20.0.2(typescript@5.9.2))(@react-native/metro-config@0.81.1(@babel/core@7.28.4))(@types/react@19.1.12)(react@19.1.0))(react@19.1.0) - transitivePeerDependencies: - - '@react-native-masked-view/masked-view' - '@rock-js/config@0.11.1': dependencies: '@babel/code-frame': 7.27.1 diff --git a/src/components/ColorPickerModal/ColorPickerModal.tsx b/src/components/ColorPickerModal/ColorPickerModal.tsx deleted file mode 100644 index a59c7d1cc5..0000000000 --- a/src/components/ColorPickerModal/ColorPickerModal.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import React, { useState } from 'react'; -import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native'; - -import { Portal, TextInput } from 'react-native-paper'; -import { Modal } from '@components'; -import { ThemeColors } from '../../theme/types'; - -interface ColorPickerModalProps { - visible: boolean; - title: string; - color: string; - onSubmit: (val: string) => void; - closeModal: () => void; - theme: ThemeColors; - showAccentColors?: boolean; -} - -const ColorPickerModal: React.FC = ({ - theme, - color, - title, - onSubmit, - closeModal, - visible, - showAccentColors, -}) => { - const [text, setText] = useState(color); - const [error, setError] = useState(); - - const onDismiss = () => { - closeModal(); - if (error) { - setText(color); - } - setError(null); - }; - - const onChangeText = (txt: string) => setText(txt); - - const onSubmitEditing = () => { - const re = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3})$/i; - - if (text.match(re)) { - onSubmit(text); - closeModal(); - } else { - setError('Enter a valid hex color code'); - } - }; - - const accentColors = [ - '#EF5350', - '#EC407A', - '#AB47BC', - '#7E57C2', - '#5C6BC0', - '#42A5F5', - '#29B6FC', - '#26C6DA', - '#26A69A', - '#66BB6A', - '#9CCC65', - '#D4E157', - '#FFEE58', - '#FFCA28', - '#FFA726', - '#FF7043', - '#8D6E63', - '#BDBDBD', - '#78909C', - '#000000', - ]; - - return ( - - - - {title} - - {showAccentColors ? ( - item} - renderItem={({ item }) => ( - - { - onSubmit(item); - closeModal(); - }} - /> - - )} - /> - ) : null} - - {error} - - - ); -}; - -export default ColorPickerModal; - -const styles = StyleSheet.create({ - errorText: { - color: '#FF0033', - paddingTop: 8, - }, - modalTitle: { - fontSize: 24, - marginBottom: 16, - }, - item: { - borderRadius: 4, - overflow: 'hidden', - - flex: 1 / 4, - height: 40, - marginHorizontal: 4, - marginVertical: 4, - }, - flex: { flex: 1 }, - marginBottom: { marginBottom: 8 }, -}); diff --git a/src/components/ColorPreferenceItem/ColorPreferenceItem.tsx b/src/components/ColorPreferenceItem/ColorPreferenceItem.tsx deleted file mode 100644 index d30bee1220..0000000000 --- a/src/components/ColorPreferenceItem/ColorPreferenceItem.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import { Pressable, StyleSheet, Text, View } from 'react-native'; - -import { ThemeColors } from '../../theme/types'; - -interface ColorPreferenceItemProps { - label: string; - description?: string; - onPress: () => void; - theme: ThemeColors; -} - -const ColorPreferenceItem: React.FC = ({ - label, - description, - theme, - onPress, -}) => ( - - - {label} - - {description?.toUpperCase?.()} - - - - -); - -export default ColorPreferenceItem; - -const styles = StyleSheet.create({ - colorPreview: { - borderRadius: 50, - height: 24, - marginRight: 16, - width: 24, - }, - container: { - alignItems: 'center', - flexDirection: 'row', - justifyContent: 'space-between', - padding: 16, - }, - label: { - fontSize: 16, - }, -}); diff --git a/src/components/Context/LibraryContext.tsx b/src/components/Context/LibraryContext.tsx index 16abe82c84..0e22b24dfc 100644 --- a/src/components/Context/LibraryContext.tsx +++ b/src/components/Context/LibraryContext.tsx @@ -3,14 +3,10 @@ import { useLibrary, UseLibraryReturnType, } from '@screens/library/hooks/useLibrary'; -import { useLibrarySettings } from '@hooks/persisted'; -import { LibrarySettings } from '@hooks/persisted/useSettings'; // type Library = Category & { novels: LibraryNovelInfo[] }; -type LibraryContextType = UseLibraryReturnType & { - settings: LibrarySettings; -}; +type LibraryContextType = UseLibraryReturnType; const defaultValue = {} as LibraryContextType; const LibraryContext = createContext(defaultValue); @@ -21,10 +17,9 @@ export function LibraryContextProvider({ children: React.ReactNode; }) { const useLibraryParams = useLibrary(); - const settings = useLibrarySettings(); return ( - + {children} ); diff --git a/src/components/Context/SettingsContext.tsx b/src/components/Context/SettingsContext.tsx new file mode 100644 index 0000000000..4f37910a00 --- /dev/null +++ b/src/components/Context/SettingsContext.tsx @@ -0,0 +1,27 @@ +import React, { createContext, useContext } from 'react'; + +import { useSettings } from '@hooks/persisted/useSettings'; +import { defaultSettings } from '@screens/settings/constants/defaultValues'; + +type SettingsContextType = ReturnType; + +const defaultValue = defaultSettings as any as SettingsContextType; +const SettingsContext = createContext(defaultValue); + +export function SettingsContextProvider({ + children, +}: { + children: React.ReactNode; +}) { + const settings = useSettings(); + + return ( + + {children} + + ); +} + +export const useSettingsContext = (): SettingsContextType => { + return useContext(SettingsContext); +}; diff --git a/src/components/List/List.tsx b/src/components/List/List.tsx index 119e8a3038..1fd52a58a3 100644 --- a/src/components/List/List.tsx +++ b/src/components/List/List.tsx @@ -17,13 +17,22 @@ interface ListItemProps { description?: string | null; icon?: string; onPress?: () => void; + onPressIn?: () => void; theme: ThemeColors; disabled?: boolean; right?: string; } -const Section = ({ children }: { children: ReactNode }) => ( - {children} +const Section = ({ + children, + style, +}: { + style?: ViewStyle; + children: ReactNode; +}) => ( + + {children} + ); const SubHeader = ({ @@ -43,6 +52,7 @@ const Item: React.FC = ({ description, icon, onPress, + onPressIn, theme, disabled, right, @@ -88,6 +98,7 @@ const Item: React.FC = ({ onPress={onPress} rippleColor={theme.rippleColor} style={styles.listItemCtn} + onPressIn={onPressIn} /> ); }; @@ -124,7 +135,7 @@ const Icon = ({ icon, theme }: { icon: string; theme: ThemeColors }) => ( interface ColorItemProps { title: string; - description: string; + description?: string; theme: ThemeColors; onPress: () => void; } @@ -200,7 +211,8 @@ const styles = StyleSheet.create({ marginVertical: 0, }, pressable: { - padding: 16, + paddingHorizontal: 16, + paddingVertical: 12, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', diff --git a/src/components/NovelCover.tsx b/src/components/NovelCover.tsx index 458b3c7477..52b25fc25f 100644 --- a/src/components/NovelCover.tsx +++ b/src/components/NovelCover.tsx @@ -17,12 +17,12 @@ import { DisplayModes } from '@screens/library/constants/constants'; import { DBNovelInfo, NovelInfo } from '@database/types'; import { NovelItem } from '@plugins/types'; import { ThemeColors } from '@theme/types'; -import { useLibrarySettings } from '@hooks/persisted'; import { getUserAgent } from '@hooks/persisted/useUserAgent'; import { getString } from '@strings/translations'; import SourceScreenSkeletonLoading from '@screens/browse/loadingAnimation/SourceScreenSkeletonLoading'; import { defaultCover } from '@plugins/helpers/constants'; import { ActivityIndicator } from 'react-native-paper'; +import { useSettingsContext } from './Context/SettingsContext'; interface UnreadBadgeProps { chaptersDownloaded: number; @@ -86,12 +86,8 @@ function NovelCover< globalSearch, selectedNovelIds, }: INovelCover) { - const { - displayMode = DisplayModes.Comfortable, - showDownloadBadges = true, - showUnreadBadges = true, - novelsPerRow = 3, - } = useLibrarySettings(); + const { displayMode, showDownloadBadges, showUnreadBadges, novelsPerRow } = + useSettingsContext(); const window = useWindowDimensions(); diff --git a/src/components/NovelList.tsx b/src/components/NovelList.tsx index 7c502bc359..6dcaadc879 100644 --- a/src/components/NovelList.tsx +++ b/src/components/NovelList.tsx @@ -1,4 +1,3 @@ -import { useLibrarySettings } from '@hooks/persisted'; import { DisplayModes } from '@screens/library/constants/constants'; import React, { useMemo } from 'react'; import { @@ -10,6 +9,7 @@ import { import { NovelItem } from '@plugins/types'; import { NovelInfo } from '../database/types'; import { useDeviceOrientation } from '@hooks'; +import { useSettingsContext } from './Context/SettingsContext'; export type NovelListRenderItem = ListRenderItem; @@ -24,8 +24,7 @@ interface NovelListProps extends FlatListProps { } const NovelList: React.FC = props => { - const { displayMode = DisplayModes.Comfortable, novelsPerRow = 3 } = - useLibrarySettings(); + const { displayMode, novelsPerRow } = useSettingsContext(); const orientation = useDeviceOrientation(); const isListView = displayMode === DisplayModes.List; diff --git a/src/components/Skeleton/Skeleton.tsx b/src/components/Skeleton/Skeleton.tsx index acc9194941..14b832ef98 100644 --- a/src/components/Skeleton/Skeleton.tsx +++ b/src/components/Skeleton/Skeleton.tsx @@ -1,4 +1,4 @@ -import { useAppSettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import * as React from 'react'; import { StyleProp, ViewStyle, StyleSheet, View } from 'react-native'; import Animated, { @@ -10,12 +10,13 @@ import Animated, { } from 'react-native-reanimated'; import useLoadingColors from './useLoadingColors'; import { LinearGradient } from 'expo-linear-gradient'; +import { useSettingsContext } from '@components/Context/SettingsContext'; const duration = 1000; function useSetupLoadingAnimations() { const sv = useSharedValue(0); - const { disableLoadingAnimations } = useAppSettings(); + const { disableLoadingAnimations } = useSettingsContext(); const theme = useTheme(); const [highlightColor, backgroundColor] = useLoadingColors(theme); @@ -159,7 +160,7 @@ function NovelMetaSkeleton() { const ChapterListSkeleton = ({ img }: { img?: boolean }) => { const sv = useSharedValue(0); - const { disableLoadingAnimations } = useAppSettings(); + const { disableLoadingAnimations } = useSettingsContext(); React.useEffect(() => { if (disableLoadingAnimations) return; diff --git a/src/components/Switch/SwitchItem.tsx b/src/components/Switch/SwitchItem.tsx index 0727329116..0dff52b079 100644 --- a/src/components/Switch/SwitchItem.tsx +++ b/src/components/Switch/SwitchItem.tsx @@ -18,6 +18,8 @@ interface SwitchItemProps { theme: ThemeColors; size?: number; style?: StyleProp; + endOfLine?: () => React.ReactNode; + quickSettingsItem?: boolean; } const SwitchItem: React.FC = ({ @@ -28,50 +30,61 @@ const SwitchItem: React.FC = ({ value, size, style, -}) => ( - - - {label} - {description ? ( - - {description} + endOfLine, + quickSettingsItem, +}) => { + const color = quickSettingsItem ? theme.onSurfaceVariant : theme.onSurface; + const fontSize = quickSettingsItem ? 14 : 16; + + return ( + + + + {label} - ) : null} - - - -); + {description && !quickSettingsItem ? ( + + {description} + + ) : null} + + + {endOfLine ? endOfLine() : null} + + ); +}; export default SwitchItem; const styles = StyleSheet.create({ container: { - alignItems: 'center', flexDirection: 'row', - justifyContent: 'space-between', + alignItems: 'center', + gap: 16, paddingVertical: 12, }, - description: { - fontSize: 12, - lineHeight: 20, - }, - label: { - fontSize: 16, - }, labelContainer: { flex: 1, - justifyContent: 'center', + flexDirection: 'column', + }, + description: { + fontSize: 12, }, switch: { - marginLeft: 8, + alignSelf: 'center', }, }); diff --git a/src/components/TextInput/TextInput.tsx b/src/components/TextInput/TextInput.tsx new file mode 100644 index 0000000000..bed4cd713e --- /dev/null +++ b/src/components/TextInput/TextInput.tsx @@ -0,0 +1,68 @@ +import React, { useRef, useState } from 'react'; +import { + StyleSheet, + TextInput as RNTextInput, + TextInputProps as RNTextInputProps, +} from 'react-native'; + +import { useTheme } from '@hooks/persisted'; + +interface TextInputProps extends RNTextInputProps { + error?: boolean; + value?: never; +} + +const TextInput = (props: TextInputProps) => { + const theme = useTheme(); + + const inputRef = useRef(null); + const [inputFocused, setInputFocused] = useState(false); + + const onFocus: RNTextInputProps['onFocus'] = e => { + setInputFocused(true); + props.onFocus?.(e); + }; + const onBlur: RNTextInputProps['onBlur'] = e => { + setInputFocused(false); + props.onBlur?.(e); + }; + + const borderWidth = inputFocused || props.error ? 2 : 1; + const margin = inputFocused || props.error ? 0 : 1; + return ( + + ); +}; + +export default TextInput; + +const styles = StyleSheet.create({ + textInput: { + borderRadius: 4, + borderStyle: 'solid', + fontSize: 16, + paddingHorizontal: 16, + paddingVertical: 10, + }, +}); diff --git a/src/components/ThemePicker/ThemePicker.tsx b/src/components/ThemePicker/ThemePicker.tsx index 35d21d8cde..ab5b24dcda 100644 --- a/src/components/ThemePicker/ThemePicker.tsx +++ b/src/components/ThemePicker/ThemePicker.tsx @@ -128,7 +128,7 @@ const styles = StyleSheet.create({ width: '33%', }, horizontalContainer: { - width: undefined, + width: 'auto', marginHorizontal: 4, }, card: { diff --git a/src/components/index.ts b/src/components/index.ts index 31fdc8c422..198f411b27 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -8,7 +8,6 @@ export { default as Button } from './Button/Button'; export { default as Appbar } from './Appbar/Appbar'; export { default as SwitchItem } from './Switch/SwitchItem'; export { default as List } from './List/List'; -export { default as ColorPreferenceItem } from './ColorPreferenceItem/ColorPreferenceItem'; export { default as LoadingMoreIndicator } from './LoadingMoreIndicator/LoadingMoreIndicator'; export { Checkbox } from './Checkbox/Checkbox'; export { RadioButton } from './RadioButton/RadioButton'; diff --git a/src/hooks/common/useFullscreenMode.ts b/src/hooks/common/useFullscreenMode.ts index ed3f45c48d..ae535ef921 100644 --- a/src/hooks/common/useFullscreenMode.ts +++ b/src/hooks/common/useFullscreenMode.ts @@ -1,11 +1,7 @@ import { useCallback, useEffect } from 'react'; import { StatusBar } from 'react-native'; import { useNavigation } from '@react-navigation/native'; -import { - useChapterGeneralSettings, - useChapterReaderSettings, - useTheme, -} from '../persisted'; +import { useTheme } from '../persisted'; import Color from 'color'; import * as NavigationBar from 'expo-navigation-bar'; import { @@ -13,11 +9,11 @@ import { setStatusBarColor, } from '@theme/utils/setBarColor'; import { SystemBars } from 'react-native-edge-to-edge'; +import { useSettingsContext } from '@components/Context/SettingsContext'; const useFullscreenMode = () => { const { addListener } = useNavigation(); - const { theme: backgroundColor } = useChapterReaderSettings(); - const { fullScreenMode } = useChapterGeneralSettings(); + const { backgroundColor, fullScreenMode } = useSettingsContext(); const theme = useTheme(); const setImmersiveMode = useCallback(() => { diff --git a/src/hooks/common/useKeyboardHeight.ts b/src/hooks/common/useKeyboardHeight.ts new file mode 100644 index 0000000000..3af2afc9c4 --- /dev/null +++ b/src/hooks/common/useKeyboardHeight.ts @@ -0,0 +1,31 @@ +import { useEffect, useState } from 'react'; +import { Keyboard, KeyboardEvent } from 'react-native'; + +export const useKeyboardHeight = () => { + const [keyboardHeight, setKeyboardHeight] = useState(0); + + useEffect(() => { + function onKeyboardDidShow(e: KeyboardEvent) { + setKeyboardHeight(e.endCoordinates.height); + } + + function onKeyboardDidHide() { + setKeyboardHeight(0); + } + + const showSubscription = Keyboard.addListener( + 'keyboardDidShow', + onKeyboardDidShow, + ); + const hideSubscription = Keyboard.addListener( + 'keyboardDidHide', + onKeyboardDidHide, + ); + return () => { + showSubscription.remove(); + hideSubscription.remove(); + }; + }, []); + + return keyboardHeight; +}; diff --git a/src/hooks/persisted/index.ts b/src/hooks/persisted/index.ts index 566413b08a..d89c19f643 100644 --- a/src/hooks/persisted/index.ts +++ b/src/hooks/persisted/index.ts @@ -2,13 +2,7 @@ export { useTheme } from './useTheme'; export { useUpdates, useLastUpdate } from './useUpdates'; export { default as useCategories } from './useCategories'; export { default as useHistory } from './useHistory'; -export { - useAppSettings, - useBrowseSettings, - useLibrarySettings, - useChapterGeneralSettings, - useChapterReaderSettings, -} from './useSettings'; +export { useSettings } from './useSettings'; export { default as usePlugins } from './usePlugins'; export { getTracker, useTracker } from './useTracker'; export { useTrackedNovel, useNovel } from './useNovel'; diff --git a/src/hooks/persisted/useNovel.ts b/src/hooks/persisted/useNovel.ts index 5102d17a4e..548296b7c2 100644 --- a/src/hooks/persisted/useNovel.ts +++ b/src/hooks/persisted/useNovel.ts @@ -33,9 +33,9 @@ import { getString } from '@strings/translations'; import dayjs from 'dayjs'; import { parseChapterNumber } from '@utils/parseChapterNumber'; import { NOVEL_STORAGE } from '@utils/Storages'; -import { useAppSettings } from './useSettings'; import NativeFile from '@specs/NativeFile'; import { useLibraryContext } from '@components/Context/LibraryContext'; +import { useSettingsContext } from '@components/Context/SettingsContext'; // #region constants @@ -147,7 +147,7 @@ export const useNovel = (novelOrPath: string | NovelInfo, pluginId: string) => { novel ? calculatePages(novel) : [], ); - const { defaultChapterSort } = useAppSettings(); + const { defaultChapterSort } = useSettingsContext(); const novelPath = novel?.path ?? (novelOrPath as string); diff --git a/src/hooks/persisted/useSettings.ts b/src/hooks/persisted/useSettings.ts index 22d6b6a77d..c191199b5d 100644 --- a/src/hooks/persisted/useSettings.ts +++ b/src/hooks/persisted/useSettings.ts @@ -1,10 +1,10 @@ import { - DisplayModes, - LibraryFilter, - LibrarySortOrder, -} from '@screens/library/constants/constants'; + defaultSettings, + DefaultSettings, + ReaderTheme, +} from '@screens/settings/constants/defaultValues'; +import { useCallback, useEffect, useMemo } from 'react'; import { useMMKVObject } from 'react-native-mmkv'; -import { Voice } from 'expo-speech'; export const APP_SETTINGS = 'APP_SETTINGS'; export const BROWSE_SETTINGS = 'BROWSE_SETTINGS'; @@ -12,290 +12,98 @@ export const LIBRARY_SETTINGS = 'LIBRARY_SETTINGS'; export const CHAPTER_GENERAL_SETTINGS = 'CHAPTER_GENERAL_SETTINGS'; export const CHAPTER_READER_SETTINGS = 'CHAPTER_READER_SETTINGS'; -export interface AppSettings { - /** - * General settings - */ - - incognitoMode: boolean; - disableHapticFeedback: boolean; - - /** - * Appearence settings - */ - - showHistoryTab: boolean; - showUpdatesTab: boolean; - showLabelsInNav: boolean; - useFabForContinueReading: boolean; - disableLoadingAnimations: boolean; - - /** - * Library settings - */ - - downloadedOnlyMode: boolean; - useLibraryFAB: boolean; - - /** - * Update settings - */ - - onlyUpdateOngoingNovels: boolean; - updateLibraryOnLaunch: boolean; - downloadNewChapters: boolean; - refreshNovelMetadata: boolean; - - /** - * Novel settings - */ - - hideBackdrop: boolean; - defaultChapterSort: string; -} - -export interface BrowseSettings { - showMyAnimeList: boolean; - showAniList: boolean; - globalSearchConcurrency?: number; -} - -export interface LibrarySettings { - sortOrder?: LibrarySortOrder; - filter?: LibraryFilter; - showDownloadBadges?: boolean; - showUnreadBadges?: boolean; - showNumberOfNovels?: boolean; - displayMode?: DisplayModes; - novelsPerRow?: number; - incognitoMode?: boolean; - downloadedOnlyMode?: boolean; -} - -export interface ChapterGeneralSettings { - keepScreenOn: boolean; - fullScreenMode: boolean; - pageReader: boolean; - swipeGestures: boolean; - showScrollPercentage: boolean; - useVolumeButtons: boolean; - showBatteryAndTime: boolean; - autoScroll: boolean; - autoScrollInterval: number; - autoScrollOffset: number | null; - verticalSeekbar: boolean; - removeExtraParagraphSpacing: boolean; - bionicReading: boolean; - tapToScroll: boolean; - TTSEnable: boolean; -} - -export interface ReaderTheme { - backgroundColor: string; - textColor: string; -} - -export interface ChapterReaderSettings { - theme: string; - textColor: string; - textSize: number; - textAlign: string; - padding: number; - fontFamily: string; - lineHeight: number; - customCSS: string; - customJS: string; - customThemes: ReaderTheme[]; - tts?: { - voice?: Voice; - rate?: number; - pitch?: number; - }; - epubLocation: string; - epubUseAppTheme: boolean; - epubUseCustomCSS: boolean; - epubUseCustomJS: boolean; -} - -const initialAppSettings: AppSettings = { - /** - * General settings - */ - - incognitoMode: false, - disableHapticFeedback: false, - - /** - * Appearence settings - */ - - showHistoryTab: true, - showUpdatesTab: true, - showLabelsInNav: true, - useFabForContinueReading: false, - disableLoadingAnimations: false, - - /** - * Library settings - */ - - downloadedOnlyMode: false, - useLibraryFAB: false, - - /** - * Update settings - */ - - onlyUpdateOngoingNovels: false, - updateLibraryOnLaunch: false, - downloadNewChapters: false, - refreshNovelMetadata: false, - - /** - * Novel settings - */ - - hideBackdrop: false, - defaultChapterSort: 'ORDER BY position ASC', -}; - -const initialBrowseSettings: BrowseSettings = { - showMyAnimeList: true, - showAniList: true, - globalSearchConcurrency: 3, -}; - -export const initialChapterGeneralSettings: ChapterGeneralSettings = { - keepScreenOn: true, - fullScreenMode: true, - pageReader: false, - swipeGestures: false, - showScrollPercentage: true, - useVolumeButtons: false, - showBatteryAndTime: false, - autoScroll: false, - autoScrollInterval: 10, - autoScrollOffset: null, - verticalSeekbar: true, - removeExtraParagraphSpacing: false, - bionicReading: false, - tapToScroll: false, - TTSEnable: false, -}; - -export const initialChapterReaderSettings: ChapterReaderSettings = { - theme: '#292832', - textColor: '#CCCCCC', - textSize: 16, - textAlign: 'left', - padding: 16, - fontFamily: '', - lineHeight: 1.5, - customCSS: '', - customJS: '', - customThemes: [], - tts: { - rate: 1, - pitch: 1, - }, - epubLocation: '', - epubUseAppTheme: false, - epubUseCustomCSS: false, - epubUseCustomJS: false, -}; - -export const useAppSettings = () => { - const [appSettings = initialAppSettings, setSettings] = - useMMKVObject(APP_SETTINGS); - - const setAppSettings = (values: Partial) => - setSettings({ ...appSettings, ...values }); - - return { - ...appSettings, - setAppSettings, - }; -}; - -export const useBrowseSettings = () => { - const [browseSettings = initialBrowseSettings, setSettings] = - useMMKVObject(BROWSE_SETTINGS); - - const setBrowseSettings = (values: Partial) => - setSettings({ ...browseSettings, ...values }); - - return { - ...browseSettings, - setBrowseSettings, - }; -}; - -const defaultLibrarySettings: LibrarySettings = { - showNumberOfNovels: false, - downloadedOnlyMode: false, - incognitoMode: false, - displayMode: DisplayModes.Comfortable, - showDownloadBadges: true, - showUnreadBadges: true, - novelsPerRow: 3, - sortOrder: LibrarySortOrder.DateAdded_DESC, -}; - -export const useLibrarySettings = () => { - const [librarySettings, setSettings] = - useMMKVObject(LIBRARY_SETTINGS); - - const setLibrarySettings = (value: Partial) => - setSettings({ ...librarySettings, ...value }); - - return { - ...{ ...defaultLibrarySettings, ...librarySettings }, - setLibrarySettings, - }; -}; - -export const useChapterGeneralSettings = () => { - const [chapterGeneralSettings = initialChapterGeneralSettings, setSettings] = - useMMKVObject(CHAPTER_GENERAL_SETTINGS); - - const setChapterGeneralSettings = (values: Partial) => - setSettings({ ...chapterGeneralSettings, ...values }); - - return { - ...chapterGeneralSettings, - setChapterGeneralSettings, - }; -}; - -export const useChapterReaderSettings = () => { - const [chapterReaderSettings = initialChapterReaderSettings, setSettings] = - useMMKVObject(CHAPTER_READER_SETTINGS); - - const setChapterReaderSettings = (values: Partial) => - setSettings({ ...chapterReaderSettings, ...values }); - - const saveCustomReaderTheme = (theme: ReaderTheme) => - setSettings({ +export const SETTINGS = 'SETTINGS'; + +export const useSettings = () => { + const [settings, _setSettings] = useMMKVObject(SETTINGS); + + // Only read partial settings if main settings is undefined + const [appSettings] = useMMKVObject>(APP_SETTINGS); + const [browseSettings] = + useMMKVObject>(BROWSE_SETTINGS); + const [librarySettings] = + useMMKVObject>(LIBRARY_SETTINGS); + const [chapterGeneralSettings] = useMMKVObject>( + CHAPTER_GENERAL_SETTINGS, + ); + const [chapterReaderSettings] = useMMKVObject>( + CHAPTER_READER_SETTINGS, + ); + + // Memoize the merged settings to avoid unnecessary recalculations + const mergedSettings = useMemo(() => { + if (settings !== undefined) return settings; + + return { + ...defaultSettings, + ...appSettings, + ...browseSettings, + ...librarySettings, + ...chapterGeneralSettings, ...chapterReaderSettings, - customThemes: [theme, ...chapterReaderSettings.customThemes], - }); - - const deleteCustomReaderTheme = (theme: ReaderTheme) => - setSettings({ - ...chapterReaderSettings, - customThemes: chapterReaderSettings.customThemes.filter( - v => - !( - v.backgroundColor === theme.backgroundColor && - v.textColor === theme.textColor - ), - ), - }); - - return { - ...chapterReaderSettings, - setChapterReaderSettings, - saveCustomReaderTheme, - deleteCustomReaderTheme, - }; + }; + }, [ + settings, + appSettings, + browseSettings, + librarySettings, + chapterGeneralSettings, + chapterReaderSettings, + ]); + + useEffect(() => { + if (settings === undefined) { + _setSettings(mergedSettings); + } + }, [settings, _setSettings, mergedSettings]); + + const setSettings = useCallback( + (values: Partial) => + _setSettings(prev => ({ ...prev, ...values } as DefaultSettings)), + [_setSettings], + ); + + const saveCustomReaderTheme = useCallback( + (theme: ReaderTheme) => { + const themes = settings?.customThemes || []; + setSettings({ + customThemes: [theme, ...themes], + }); + }, + [setSettings, settings?.customThemes], + ); + + const deleteCustomReaderTheme = useCallback( + (theme: ReaderTheme) => { + const themes = settings?.customThemes || []; + setSettings({ + customThemes: themes.filter( + v => + !( + v.backgroundColor === theme.backgroundColor && + v.textColor === theme.textColor + ), + ), + }); + }, + [setSettings, settings?.customThemes], + ); + + // Memoize the final settings object to provide a stable default + const value = useMemo( + () => ({ + ...mergedSettings, // Use merged settings + setSettings, + saveCustomReaderTheme, + deleteCustomReaderTheme, + }), + [ + mergedSettings, + setSettings, + saveCustomReaderTheme, + deleteCustomReaderTheme, + ], + ); + + return value; }; diff --git a/src/navigators/BottomNavigator.tsx b/src/navigators/BottomNavigator.tsx index eee21c02c7..c83183c9e2 100644 --- a/src/navigators/BottomNavigator.tsx +++ b/src/navigators/BottomNavigator.tsx @@ -11,23 +11,21 @@ import Browse from '../screens/browse/BrowseScreen'; import More from '../screens/more/MoreScreen'; import { getString } from '@strings/translations'; -import { useAppSettings, usePlugins, useTheme } from '@hooks/persisted'; +import { usePlugins, useTheme } from '@hooks/persisted'; import { BottomNavigatorParamList } from './types'; import Icon from '@react-native-vector-icons/material-design-icons'; import { MaterialDesignIconName } from '@type/icon'; import { CommonActions } from '@react-navigation/native'; import { BottomNavigation } from 'react-native-paper'; +import { useSettingsContext } from '@components/Context/SettingsContext'; const Tab = createBottomTabNavigator(); const BottomNavigator = () => { const theme = useTheme(); - const { - showHistoryTab = true, - showUpdatesTab = true, - showLabelsInNav = false, - } = useAppSettings(); + const { showHistoryTab, showUpdatesTab, showLabelsInNav } = + useSettingsContext(); const { filteredInstalledPlugins } = usePlugins(); const pluginsWithUpdate = useMemo( diff --git a/src/navigators/Main.tsx b/src/navigators/Main.tsx index aa552f2dca..3fc0c5a1b3 100644 --- a/src/navigators/Main.tsx +++ b/src/navigators/Main.tsx @@ -11,7 +11,7 @@ import { changeNavigationBarColor, setStatusBarColor, } from '@theme/utils/setBarColor'; -import { useAppSettings, usePlugins, useTheme } from '@hooks/persisted'; +import { usePlugins, useTheme } from '@hooks/persisted'; import { useGithubUpdateChecker } from '@hooks/common/githubUpdateChecker'; /** @@ -29,6 +29,7 @@ import GlobalSearchScreen from '../screens/GlobalSearchScreen/GlobalSearchScreen import Migration from '../screens/browse/migration/Migration'; import SourceNovels from '../screens/browse/SourceNovels'; import MigrateNovel from '../screens/browse/migration/MigrationNovels'; +import { Provider as PaperProvider } from 'react-native-paper'; import MalTopNovels from '../screens/browse/discover/MalTopNovels'; import AniListTopNovels from '../screens/browse/discover/AniListTopNovels'; @@ -43,6 +44,11 @@ import ServiceManager from '@services/ServiceManager'; import ReaderStack from './ReaderStack'; import { LibraryContextProvider } from '@components/Context/LibraryContext'; import { UpdateContextProvider } from '@components/Context/UpdateContext'; +import { + SettingsContextProvider, + useSettingsContext, +} from '@components/Context/SettingsContext'; +import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; const Stack = createNativeStackNavigator(); const MainNavigator = ({ @@ -51,7 +57,7 @@ const MainNavigator = ({ ref: React.Ref | null>; }) => { const theme = useTheme(); - const { updateLibraryOnLaunch } = useAppSettings(); + const { updateLibraryOnLaunch } = useSettingsContext(); const { refreshPlugins } = usePlugins(); const [isOnboarded] = useMMKVBoolean('IS_ONBOARDED'); @@ -114,28 +120,46 @@ const MainNavigator = ({ }, }} > - - - {isNewVersion && } - - - - - - - - - - - - - - - - + + + + + + {isNewVersion && } + + + + + + + + + + + + + + + + + + + ); }; diff --git a/src/navigators/MoreStack.tsx b/src/navigators/MoreStack.tsx index 98b4c012ed..f0bb126699 100644 --- a/src/navigators/MoreStack.tsx +++ b/src/navigators/MoreStack.tsx @@ -5,19 +5,16 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; // Screens import About from '../screens/more/About'; import Settings from '../screens/settings/SettingsScreen'; -import TrackerSettings from '../screens/settings/SettingsTrackerScreen'; -import ReaderSettings from '../screens/settings/SettingsReaderScreen/SettingsReaderScreen'; import BackupSettings from '../screens/settings/SettingsBackupScreen'; -import AdvancedSettings from '../screens/settings/SettingsAdvancedScreen'; -import GeneralSettings from '../screens/settings/SettingsGeneralScreen/SettingsGeneralScreen'; +import AdvancedSettings from '../screens/settings/settingsScreens/SettingsAdvancedScreen'; +import SettingsSubScreen from '../screens/settings/settingsScreens/SettingsSubScreen'; import TaskQueue from '../screens/more/TaskQueueScreen'; import Downloads from '../screens/more/DownloadsScreen'; -import AppearanceSettings from '../screens/settings/SettingsAppearanceScreen'; + import CategoriesScreen from '@screens/Categories/CategoriesScreen'; -import RespositorySettings from '@screens/settings/SettingsRepositoryScreen/SettingsRepositoryScreen'; -// import LibrarySettings from '@screens/settings/SettingsLibraryScreen/SettingsLibraryScreen'; import StatsScreen from '@screens/StatsScreen/StatsScreen'; import { MoreStackParamList, SettingsStackParamList } from './types'; +import ReaderSettingsSubScreen from '@screens/settings/settingsScreens/ReaderSettingsSubScreen'; const Stack = createNativeStackNavigator< MoreStackParamList & SettingsStackParamList @@ -28,14 +25,10 @@ const stackNavigatorConfig = { headerShown: false }; const SettingsStack = () => ( - - - + + - - - {/* */} ); diff --git a/src/navigators/types/index.ts b/src/navigators/types/index.ts index 1b7932ccd9..46a8f4c32d 100644 --- a/src/navigators/types/index.ts +++ b/src/navigators/types/index.ts @@ -3,7 +3,8 @@ import { CompositeScreenProps, NavigatorScreenParams, } from '@react-navigation/native'; -import { StackScreenProps } from '@react-navigation/stack'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { Settings } from '@screens/settings/Settings'; import { MaterialBottomTabScreenProps } from 'react-native-paper'; export type RootStackParamList = { @@ -41,27 +42,27 @@ export type BottomNavigatorParamList = { export type LibraryScreenProps = CompositeScreenProps< MaterialBottomTabScreenProps, - StackScreenProps + NativeStackScreenProps >; export type HistoryScreenProps = CompositeScreenProps< MaterialBottomTabScreenProps, - StackScreenProps + NativeStackScreenProps >; export type UpdateScreenProps = CompositeScreenProps< MaterialBottomTabScreenProps, - StackScreenProps + NativeStackScreenProps >; export type BrowseScreenProps = CompositeScreenProps< MaterialBottomTabScreenProps, - StackScreenProps + NativeStackScreenProps >; export type MoreStackScreenProps = CompositeScreenProps< MaterialBottomTabScreenProps, - StackScreenProps + NativeStackScreenProps >; export type MoreStackParamList = { SettingsStack: NavigatorScreenParams; @@ -72,23 +73,26 @@ export type MoreStackParamList = { Statistics: undefined; }; +type SettingsProps = { + settingsSource: T; +}; + export type SettingsStackParamList = { Settings: undefined; - GeneralSettings: undefined; - ReaderSettings: undefined; - TrackerSettings: undefined; - BackupSettings: undefined; - AppearanceSettings: undefined; - AdvancedSettings: undefined; - LibrarySettings: undefined; - RespositorySettings: { url?: string } | undefined; + SubScreen: SettingsProps; + ReaderSettings: SettingsProps; + TrackerSettings: SettingsProps; + BackupSettings: SettingsProps; + AdvancedSettings: SettingsProps; + LibrarySettings: SettingsProps; + RespositorySettings: SettingsProps<'repo'> & { url: string }; }; -export type NovelScreenProps = StackScreenProps< +export type NovelScreenProps = NativeStackScreenProps< ReaderStackParamList & RootStackParamList, 'Novel' >; -export type ChapterScreenProps = StackScreenProps< +export type ChapterScreenProps = NativeStackScreenProps< ReaderStackParamList & RootStackParamList, 'Chapter' >; @@ -108,75 +112,74 @@ export type ReaderStackParamList = { }; }; -export type AboutScreenProps = StackScreenProps; -export type DownloadsScreenProps = StackScreenProps< +export type AboutScreenProps = NativeStackScreenProps< + MoreStackParamList, + 'About' +>; +export type DownloadsScreenProps = NativeStackScreenProps< MoreStackParamList, 'Downloads' >; -export type TaskQueueScreenProps = StackScreenProps< +export type TaskQueueScreenProps = NativeStackScreenProps< MoreStackParamList, 'TaskQueue' >; -export type BrowseSourceScreenProps = StackScreenProps< +export type BrowseSourceScreenProps = NativeStackScreenProps< RootStackParamList, 'SourceScreen' >; -export type BrowseMalScreenProps = StackScreenProps< +export type BrowseMalScreenProps = NativeStackScreenProps< RootStackParamList, 'BrowseMal' >; -export type BrowseALScreenProps = StackScreenProps< +export type BrowseALScreenProps = NativeStackScreenProps< RootStackParamList, 'BrowseAL' >; -export type BrowseSettingsScreenProp = StackScreenProps< +export type BrowseSettingsScreenProp = NativeStackScreenProps< RootStackParamList, 'BrowseSettings' >; -export type GlobalSearchScreenProps = StackScreenProps< +export type GlobalSearchScreenProps = NativeStackScreenProps< RootStackParamList, 'GlobalSearchScreen' >; -export type MigrationScreenProps = StackScreenProps< +export type MigrationScreenProps = NativeStackScreenProps< RootStackParamList, 'Migration' >; -export type MigrateNovelScreenProps = StackScreenProps< +export type MigrateNovelScreenProps = NativeStackScreenProps< RootStackParamList, 'MigrateNovel' >; -export type SourceNovelsScreenProps = StackScreenProps< +export type SourceNovelsScreenProps = NativeStackScreenProps< RootStackParamList, 'SourceNovels' >; -export type WebviewScreenProps = StackScreenProps< +export type WebviewScreenProps = NativeStackScreenProps< RootStackParamList, 'WebviewScreen' >; export type SettingsScreenProps = CompositeScreenProps< - StackScreenProps, - StackScreenProps ->; -export type AppearanceSettingsScreenProps = StackScreenProps< - SettingsStackParamList, - 'AppearanceSettings' + NativeStackScreenProps, + NativeStackScreenProps >; -export type TrackerSettingsScreenProps = StackScreenProps< +export type TrackerSettingsScreenProps = NativeStackScreenProps< SettingsStackParamList, 'TrackerSettings' >; -export type BackupSettingsScreenProps = StackScreenProps< +export type BackupSettingsScreenProps = NativeStackScreenProps< SettingsStackParamList, 'BackupSettings' >; -export type AdvancedSettingsScreenProps = StackScreenProps< +export type AdvancedSettingsScreenProps = NativeStackScreenProps< SettingsStackParamList, 'AdvancedSettings' >; export type RespositorySettingsScreenProps = CompositeScreenProps< - StackScreenProps, - StackScreenProps + NativeStackScreenProps, + NativeStackScreenProps >; declare global { diff --git a/src/screens/Categories/components/CategorySkeletonLoading.tsx b/src/screens/Categories/components/CategorySkeletonLoading.tsx index b1e631fdcf..1ed0bb959f 100644 --- a/src/screens/Categories/components/CategorySkeletonLoading.tsx +++ b/src/screens/Categories/components/CategorySkeletonLoading.tsx @@ -4,7 +4,7 @@ import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'; import { LinearGradient } from 'expo-linear-gradient'; import { ThemeColors } from '@theme/types'; import useLoadingColors from '@utils/useLoadingColors'; -import { useAppSettings } from '@hooks/persisted/index'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface Props { width: number; @@ -13,7 +13,7 @@ interface Props { } const CategorySkeletonLoading: React.FC = ({ height, width, theme }) => { - const { disableLoadingAnimations } = useAppSettings(); + const { disableLoadingAnimations } = useSettingsContext(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const [highlightColor, backgroundColor] = useLoadingColors(theme); diff --git a/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx index 2fb96afb24..7da9c6910f 100644 --- a/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx +++ b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx @@ -2,7 +2,7 @@ import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native'; import React, { useCallback, useMemo, useState } from 'react'; import color from 'color'; -import { StackNavigationProp } from '@react-navigation/stack'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { useNavigation } from '@react-navigation/native'; import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; @@ -44,7 +44,7 @@ const GlobalSearchSourceResults: React.FC<{ item: GlobalSearchResult }> = ({ item, }) => { const theme = useTheme(); - const navigation = useNavigation>(); + const navigation = useNavigation>(); const [inActivity, setInActivity] = useState>({}); const { novelInLibrary, switchNovelToLibrary } = useLibraryContext(); diff --git a/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts b/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts index 96ab71cdae..c67d0b0da3 100644 --- a/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts +++ b/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts @@ -2,8 +2,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { NovelItem, PluginItem } from '@plugins/types'; import { getPlugin } from '@plugins/pluginManager'; -import { useBrowseSettings, usePlugins } from '@hooks/persisted'; +import { usePlugins } from '@hooks/persisted'; import { useFocusEffect } from '@react-navigation/native'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface Props { defaultSearchText?: string; @@ -39,7 +40,7 @@ export const useGlobalSearch = ({ defaultSearchText }: Props) => { const [searchResults, setSearchResults] = useState([]); const [progress, setProgress] = useState(0); - const { globalSearchConcurrency = 1 } = useBrowseSettings(); + const { globalSearchConcurrency } = useSettingsContext(); const globalSearch = useCallback( (searchText: string) => { diff --git a/src/screens/browse/components/AvailableTab.tsx b/src/screens/browse/components/AvailableTab.tsx index 040f9f6a48..980113db13 100644 --- a/src/screens/browse/components/AvailableTab.tsx +++ b/src/screens/browse/components/AvailableTab.tsx @@ -196,7 +196,8 @@ export const AvailableTab = memo(({ searchText, theme }: AvailableTabProps) => { navigation.navigate('MoreStack', { screen: 'SettingsStack', params: { - screen: 'RespositorySettings', + screen: 'SubScreen', + params: { settingsSource: 'repo' }, }, }), }, diff --git a/src/screens/browse/components/InstalledTab.tsx b/src/screens/browse/components/InstalledTab.tsx index 46d9147650..2056da86c8 100644 --- a/src/screens/browse/components/InstalledTab.tsx +++ b/src/screens/browse/components/InstalledTab.tsx @@ -10,7 +10,7 @@ import { StyleProp, TextStyle, } from 'react-native'; -import { useBrowseSettings, usePlugins } from '@hooks/persisted'; +import { usePlugins } from '@hooks/persisted'; import { PluginItem } from '@plugins/types'; import { coverPlaceholderColor } from '@theme/colors'; import { ThemeColors } from '@theme/types'; @@ -25,6 +25,7 @@ import { useBoolean, UseBooleanReturnType } from '@hooks'; import { getPlugin } from '@plugins/pluginManager'; import Swipeable from 'react-native-gesture-handler/ReanimatedSwipeable'; import { FlashList, ListRenderItem } from '@shopify/flash-list'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface InstalledTabProps { navigation: BrowseScreenProps['navigation']; @@ -331,7 +332,7 @@ export const InstalledTab = memo( ({ navigation, theme, searchText }: InstalledTabProps) => { const { filteredInstalledPlugins, lastUsedPlugin, setLastUsedPlugin } = usePlugins(); - const { showMyAnimeList, showAniList } = useBrowseSettings(); + const { showMyAnimeList, showAniList } = useSettingsContext(); const settingsModal = useBoolean(); const [selectedPluginId, setSelectedPluginId] = useState(''); diff --git a/src/screens/browse/loadingAnimation/LoadingNovel.tsx b/src/screens/browse/loadingAnimation/LoadingNovel.tsx index 52f5863f54..320e96f4e2 100644 --- a/src/screens/browse/loadingAnimation/LoadingNovel.tsx +++ b/src/screens/browse/loadingAnimation/LoadingNovel.tsx @@ -3,7 +3,7 @@ import { View, StyleSheet } from 'react-native'; import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'; import { LinearGradient } from 'expo-linear-gradient'; import { DisplayModes } from '@screens/library/constants/constants'; -import { useAppSettings } from '@hooks/persisted/index'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface Props { backgroundColor: string; @@ -20,7 +20,7 @@ const LoadingNovel: React.FC = ({ pictureWidth, displayMode, }) => { - const { disableLoadingAnimations } = useAppSettings(); + const { disableLoadingAnimations } = useSettingsContext(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); let randomNumber = Math.random(); if (randomNumber < 0.1) { diff --git a/src/screens/browse/loadingAnimation/MalLoading.tsx b/src/screens/browse/loadingAnimation/MalLoading.tsx index 8fc8388fed..8c83d556a7 100644 --- a/src/screens/browse/loadingAnimation/MalLoading.tsx +++ b/src/screens/browse/loadingAnimation/MalLoading.tsx @@ -5,14 +5,14 @@ import { LinearGradient } from 'expo-linear-gradient'; import { ThemeColors } from '@theme/types'; import useLoadingColors from '@utils/useLoadingColors'; -import { useAppSettings } from '@hooks/persisted/index'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface Props { theme: ThemeColors; } const MalLoading: React.FC = ({ theme }) => { - const { disableLoadingAnimations } = useAppSettings(); + const { disableLoadingAnimations } = useSettingsContext(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const styles = createStyleSheet(theme); diff --git a/src/screens/browse/loadingAnimation/SourceScreenSkeletonLoading.tsx b/src/screens/browse/loadingAnimation/SourceScreenSkeletonLoading.tsx index 6d2aa79c1b..ff4c3fb1b4 100644 --- a/src/screens/browse/loadingAnimation/SourceScreenSkeletonLoading.tsx +++ b/src/screens/browse/loadingAnimation/SourceScreenSkeletonLoading.tsx @@ -3,9 +3,9 @@ import { View, StyleSheet, useWindowDimensions } from 'react-native'; import { ThemeColors } from '@theme/types'; import useLoadingColors from '@utils/useLoadingColors'; import LoadingNovel from '@screens/browse/loadingAnimation/LoadingNovel'; -import { useLibrarySettings } from '@hooks/persisted'; import { DisplayModes } from '@screens/library/constants/constants'; import { useDeviceOrientation } from '@hooks'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface Props { theme: ThemeColors; @@ -18,8 +18,7 @@ const SourceScreenSkeletonLoading: React.FC = ({ }) => { const [highlightColor, backgroundColor] = useLoadingColors(theme); - const { displayMode = DisplayModes.Comfortable, novelsPerRow = 3 } = - useLibrarySettings(); + const { displayMode, novelsPerRow } = useSettingsContext(); const window = useWindowDimensions(); const styles = createStyleSheet(); diff --git a/src/screens/browse/loadingAnimation/TrackerLoading.tsx b/src/screens/browse/loadingAnimation/TrackerLoading.tsx index 988f6fc895..ce3613094e 100644 --- a/src/screens/browse/loadingAnimation/TrackerLoading.tsx +++ b/src/screens/browse/loadingAnimation/TrackerLoading.tsx @@ -5,14 +5,14 @@ import { LinearGradient } from 'expo-linear-gradient'; import { ThemeColors } from '@theme/types'; import useLoadingColors from '@utils/useLoadingColors'; -import { useAppSettings } from '@hooks/persisted/index'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface Props { theme: ThemeColors; } const MalLoading: React.FC = ({ theme }) => { - const { disableLoadingAnimations } = useAppSettings(); + const { disableLoadingAnimations } = useSettingsContext(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const styles = createStyleSheet(theme); diff --git a/src/screens/browse/settings/BrowseSettings.tsx b/src/screens/browse/settings/BrowseSettings.tsx index d0aecd79a0..5bfb8605df 100644 --- a/src/screens/browse/settings/BrowseSettings.tsx +++ b/src/screens/browse/settings/BrowseSettings.tsx @@ -2,16 +2,13 @@ import { FlatList, StyleSheet } from 'react-native'; import React from 'react'; import { Appbar, List, SwitchItem } from '@components'; -import { - useBrowseSettings, - usePlugins, - useTheme, -} from '@hooks/persisted/index'; +import { usePlugins, useTheme } from '@hooks/persisted/index'; import { getString } from '@strings/translations'; import { getLocaleLanguageName, languages } from '@utils/constants/languages'; import { BrowseSettingsScreenProp } from '@navigators/types/index'; import { useBoolean } from '@hooks'; import ConcurrentSearchesModal from '@screens/browse/settings/modals/ConcurrentSearchesModal'; +import { useSettingsContext } from '@components/Context/SettingsContext'; const BrowseSettings = ({ navigation }: BrowseSettingsScreenProp) => { const theme = useTheme(); @@ -22,8 +19,8 @@ const BrowseSettings = ({ navigation }: BrowseSettingsScreenProp) => { showMyAnimeList, showAniList, globalSearchConcurrency, - setBrowseSettings, - } = useBrowseSettings(); + setSettings: setBrowseSettings, + } = useSettingsContext(); const globalSearchConcurrencyModal = useBoolean(); diff --git a/src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx b/src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx index 30b39b7da2..b9df6c2159 100644 --- a/src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx +++ b/src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx @@ -6,8 +6,8 @@ import { Portal } from 'react-native-paper'; import { RadioButton } from '@components/RadioButton/RadioButton'; import { ThemeColors } from '@theme/types'; import { getString } from '@strings/translations'; -import { useBrowseSettings } from '@hooks/persisted/index'; import { Modal } from '@components'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface DisplayModeModalProps { globalSearchConcurrency: number; @@ -22,7 +22,7 @@ const ConcurrentSearchesModal: React.FC = ({ hideModal, modalVisible, }) => { - const { setBrowseSettings } = useBrowseSettings(); + const { setSettings: setBrowseSettings } = useSettingsContext(); return ( diff --git a/src/screens/history/components/HistorySkeletonLoading.tsx b/src/screens/history/components/HistorySkeletonLoading.tsx index 6ed8779bf8..3c277616e9 100644 --- a/src/screens/history/components/HistorySkeletonLoading.tsx +++ b/src/screens/history/components/HistorySkeletonLoading.tsx @@ -4,14 +4,14 @@ import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'; import { LinearGradient } from 'expo-linear-gradient'; import { ThemeColors } from '@theme/types'; import useLoadingColors from '@utils/useLoadingColors'; -import { useAppSettings } from '@hooks/persisted/index'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface Props { theme: ThemeColors; } const HistorySkeletonLoading: React.FC = ({ theme }) => { - const { disableLoadingAnimations } = useAppSettings(); + const { disableLoadingAnimations } = useSettingsContext(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const [highlightColor, backgroundColor] = useLoadingColors(theme); diff --git a/src/screens/library/LibraryScreen.tsx b/src/screens/library/LibraryScreen.tsx index 14ca73cf70..d6405c5389 100644 --- a/src/screens/library/LibraryScreen.tsx +++ b/src/screens/library/LibraryScreen.tsx @@ -28,7 +28,7 @@ import LibraryBottomSheet from './components/LibraryBottomSheet/LibraryBottomShe import { Banner } from './components/Banner'; import { Actionbar } from '@components/Actionbar/Actionbar'; -import { useAppSettings, useHistory, useTheme } from '@hooks/persisted'; +import { useHistory, useTheme } from '@hooks/persisted'; import { useSearch, useBackHandler, useBoolean } from '@hooks'; import { getString } from '@strings/translations'; import { FAB, Portal } from 'react-native-paper'; @@ -48,6 +48,7 @@ import ServiceManager from '@services/ServiceManager'; import useImport from '@hooks/persisted/useImport'; import { ThemeColors } from '@theme/types'; import { useLibraryContext } from '@components/Context/LibraryContext'; +import { useSettingsContext } from '@components/Context/SettingsContext'; type State = NavigationState<{ key: string; @@ -75,16 +76,13 @@ const LibraryScreen = ({ navigation }: LibraryScreenProps) => { const theme = useTheme(); const styles = createStyles(theme); const { left: leftInset, right: rightInset } = useSafeAreaInsets(); - const { - library, - categories, - refetchLibrary, - isLoading, - settings: { showNumberOfNovels, downloadedOnlyMode, incognitoMode }, - } = useLibraryContext(); + const { library, categories, refetchLibrary, isLoading } = + useLibraryContext(); + const { showNumberOfNovels, downloadedOnlyMode, incognitoMode } = + useSettingsContext(); const { importNovel } = useImport(); - const { useLibraryFAB = false } = useAppSettings(); + const { useLibraryFAB } = useSettingsContext(); const { isLoading: isHistoryLoading, history, error } = useHistory(); diff --git a/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx b/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx index 438dcc80ba..7fa364c16a 100644 --- a/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx +++ b/src/screens/library/components/LibraryBottomSheet/LibraryBottomSheet.tsx @@ -15,15 +15,13 @@ import { } from 'react-native-tab-view'; import color from 'color'; -import { useLibrarySettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import { getString } from '@strings/translations'; import { Checkbox, SortItem } from '@components/Checkbox/Checkbox'; import { - DisplayModes, displayModesList, LibraryFilter, libraryFilterList, - LibrarySortOrder, librarySortOrderList, } from '@screens/library/constants/constants'; import { RadioButton } from '@components/RadioButton/RadioButton'; @@ -33,6 +31,7 @@ import BottomSheet from '@components/BottomSheet/BottomSheet'; import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; import { FlashList } from '@shopify/flash-list'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface LibraryBottomSheetProps { bottomSheetRef: RefObject; @@ -43,9 +42,9 @@ const FirstRoute = () => { const theme = useTheme(); const { filter, - setLibrarySettings, - downloadedOnlyMode = false, - } = useLibrarySettings(); + setSettings: setLibrarySettings, + downloadedOnlyMode, + } = useSettingsContext(); return ( @@ -74,8 +73,7 @@ const FirstRoute = () => { const SecondRoute = () => { const theme = useTheme(); - const { sortOrder = LibrarySortOrder.DateAdded_DESC, setLibrarySettings } = - useLibrarySettings(); + const { sortOrder, setSettings: setLibrarySettings } = useSettingsContext(); return ( @@ -108,12 +106,12 @@ const SecondRoute = () => { const ThirdRoute = () => { const theme = useTheme(); const { - showDownloadBadges = true, - showNumberOfNovels = false, - showUnreadBadges = true, - displayMode = DisplayModes.Comfortable, - setLibrarySettings, - } = useLibrarySettings(); + showDownloadBadges, + showNumberOfNovels, + showUnreadBadges, + displayMode, + setSettings: setLibrarySettings, + } = useSettingsContext(); return ( diff --git a/src/screens/library/constants/constants.ts b/src/screens/library/constants/constants.ts index 493b977939..7f16445bdd 100644 --- a/src/screens/library/constants/constants.ts +++ b/src/screens/library/constants/constants.ts @@ -1,3 +1,4 @@ +import { FilteredSettings } from '@screens/settings/constants/defaultValues'; import { getString } from '@strings/translations'; export enum LibraryFilter { @@ -76,6 +77,19 @@ export const librarySortOrderList = [ }, ]; +export enum ChapterSortOrder { + BySource_ASC = 'ORDER BY position ASC', + BySource_DESC = 'ORDER BY position DESC', +} + +export const chapterSortOrderList = [ + { + label: getString('generalSettingsScreen.bySource'), + ASC: ChapterSortOrder.BySource_ASC, + DESC: ChapterSortOrder.BySource_DESC, + }, +]; + export enum DisplayModes { Compact, Comfortable, @@ -101,3 +115,52 @@ export const displayModesList = [ value: DisplayModes.List, }, ]; + +export const GridSizes = { + XL: 1, + L: 2, + M: 3, + S: 4, + XS: 5, +} as const; + +export const gridSizeList = [ + { + label: 'XL', + value: GridSizes.XL, + }, + { + label: 'L', + value: GridSizes.L, + }, + { + label: 'M', + value: GridSizes.M, + }, + { + label: 'S', + value: GridSizes.S, + }, + { + label: 'XS', + value: GridSizes.XS, + }, +]; + +export const badgesList: { + label: string; + key: FilteredSettings; +}[] = [ + { + label: getString('libraryScreen.bottomSheet.display.downloadBadges'), + key: 'showDownloadBadges', + }, + { + label: getString('libraryScreen.bottomSheet.display.unreadBadges'), + key: 'showUnreadBadges', + }, + { + label: getString('libraryScreen.bottomSheet.display.showNoOfItems'), + key: 'showNumberOfNovels', + }, +]; diff --git a/src/screens/library/hooks/useLibrary.ts b/src/screens/library/hooks/useLibrary.ts index bce2d76899..9eb4a54fbe 100644 --- a/src/screens/library/hooks/useLibrary.ts +++ b/src/screens/library/hooks/useLibrary.ts @@ -6,9 +6,8 @@ import { getLibraryNovelsFromDb } from '@database/queries/LibraryQueries'; import { Category, NovelInfo } from '@database/types'; -import { useLibrarySettings } from '@hooks/persisted'; -import { LibrarySortOrder } from '../constants/constants'; import { switchNovelToLibraryQuery } from '@database/queries/NovelQueries'; +import { useSettingsContext } from '@components/Context/SettingsContext'; // type Library = Category & { novels: LibraryNovelInfo[] }; export type ExtendedCategory = Category & { novelIds: number[] }; @@ -26,11 +25,7 @@ export type UseLibraryReturnType = { }; export const useLibrary = (): UseLibraryReturnType => { - const { - filter, - sortOrder = LibrarySortOrder.DateAdded_DESC, - downloadedOnlyMode = false, - } = useLibrarySettings(); + const { filter, sortOrder, downloadedOnlyMode } = useSettingsContext(); const [library, setLibrary] = useState([]); const [categories, setCategories] = useState([]); diff --git a/src/screens/more/MoreScreen.tsx b/src/screens/more/MoreScreen.tsx index c429b73244..57316a3a96 100644 --- a/src/screens/more/MoreScreen.tsx +++ b/src/screens/more/MoreScreen.tsx @@ -5,11 +5,12 @@ import { getString } from '@strings/translations'; import { List, SafeAreaView } from '@components'; import { MoreHeader } from './components/MoreHeader'; -import { useLibrarySettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import { MoreStackScreenProps } from '@navigators/types'; import Switch from '@components/Switch/Switch'; import { useMMKVObject } from 'react-native-mmkv'; import ServiceManager, { BackgroundTask } from '@services/ServiceManager'; +import { useSettingsContext } from '@components/Context/SettingsContext'; const MoreScreen = ({ navigation }: MoreStackScreenProps) => { const theme = useTheme(); @@ -17,10 +18,10 @@ const MoreScreen = ({ navigation }: MoreStackScreenProps) => { ServiceManager.manager.STORE_KEY, ); const { - incognitoMode = false, - downloadedOnlyMode = false, - setLibrarySettings, - } = useLibrarySettings(); + incognitoMode, + downloadedOnlyMode, + setSettings: setLibrarySettings, + } = useSettingsContext(); const enableDownloadedOnlyMode = () => setLibrarySettings({ downloadedOnlyMode: !downloadedOnlyMode }); diff --git a/src/screens/novel/components/ChooseEpubLocationModal.tsx b/src/screens/novel/components/ChooseEpubLocationModal.tsx index 1dca5cf4e9..6f48df4f3c 100644 --- a/src/screens/novel/components/ChooseEpubLocationModal.tsx +++ b/src/screens/novel/components/ChooseEpubLocationModal.tsx @@ -7,8 +7,9 @@ import { Button, List, Modal, SwitchItem } from '@components'; import { useBoolean } from '@hooks'; import { getString } from '@strings/translations'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import { showToast } from '@utils/showToast'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface ChooseEpubLocationModalProps { isVisible: boolean; @@ -23,12 +24,12 @@ const ChooseEpubLocationModal: React.FC = ({ }) => { const theme = useTheme(); const { - epubLocation = '', - epubUseAppTheme = false, - epubUseCustomCSS = false, - epubUseCustomJS = false, - setChapterReaderSettings, - } = useChapterReaderSettings(); + epubLocation, + epubUseAppTheme, + epubUseCustomCSS, + epubUseCustomJS, + setSettings: setChapterReaderSettings, + } = useSettingsContext(); const [uri, setUri] = useState(epubLocation); const useAppTheme = useBoolean(epubUseAppTheme); diff --git a/src/screens/novel/components/EpubIconButton.tsx b/src/screens/novel/components/EpubIconButton.tsx index d0d99898f9..2ad5be666f 100644 --- a/src/screens/novel/components/EpubIconButton.tsx +++ b/src/screens/novel/components/EpubIconButton.tsx @@ -7,12 +7,12 @@ import { ThemeColors } from '@theme/types'; import EpubBuilder from '@cd-z/react-native-epub-creator'; import { ChapterInfo, NovelInfo } from '@database/types'; -import { useChapterReaderSettings } from '@hooks/persisted'; import { useBoolean } from '@hooks/index'; import { showToast } from '@utils/showToast'; import { NOVEL_STORAGE } from '@utils/Storages'; import NativeFile from '@specs/NativeFile'; import { MaterialDesignIconName } from '@type/icon'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface EpubIconButtonProps { theme: ThemeColors; @@ -37,12 +37,8 @@ const EpubIconButton: React.FC = ({ setTrue: showModal, setFalse: hideModal, } = useBoolean(false); - const readerSettings = useChapterReaderSettings(); - const { - epubUseAppTheme = false, - epubUseCustomCSS = false, - epubUseCustomJS = false, - } = useChapterReaderSettings(); + const readerSettings = useSettingsContext(); + const { epubUseAppTheme, epubUseCustomCSS, epubUseCustomJS } = readerSettings; const epubStyle = useMemo( () => @@ -66,7 +62,7 @@ const EpubIconButton: React.FC = ({ text-align: ${readerSettings.textAlign}; line-height: ${readerSettings.lineHeight}; font-family: "${readerSettings.fontFamily}"; - background-color: "${readerSettings.theme}"; + background-color: "${readerSettings.backgroundColor}"; } hr { margin-top: 20px; @@ -102,7 +98,7 @@ const EpubIconButton: React.FC = ({ readerSettings.textAlign, readerSettings.lineHeight, readerSettings.fontFamily, - readerSettings.theme, + readerSettings.backgroundColor, readerSettings.customCSS, theme.primary, epubUseCustomCSS, diff --git a/src/screens/novel/components/Info/NovelInfoHeader.tsx b/src/screens/novel/components/Info/NovelInfoHeader.tsx index 8ab46dd23b..fb732a77f9 100644 --- a/src/screens/novel/components/Info/NovelInfoHeader.tsx +++ b/src/screens/novel/components/Info/NovelInfoHeader.tsx @@ -28,7 +28,6 @@ import { ThemeColors } from '@theme/types'; import { GlobalSearchScreenProps } from '@navigators/types'; import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; import { UseBooleanReturnType } from '@hooks'; -import { useAppSettings } from '@hooks/persisted'; import { NovelStatus, PluginItem } from '@plugins/types'; import { translateNovelStatus } from '@utils/translateEnum'; import { getMMKVObject } from '@utils/mmkv/mmkv'; @@ -39,6 +38,7 @@ import { VerticalBarSkeleton, } from '@components/Skeleton/Skeleton'; import { useNovelContext } from '@screens/novel/NovelContext'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface NovelInfoHeaderProps { chapters: ChapterInfo[]; @@ -91,7 +91,7 @@ const NovelInfoHeader = ({ totalChapters, trackerSheetRef, }: NovelInfoHeaderProps) => { - const { hideBackdrop = false } = useAppSettings(); + const { hideBackdrop = false } = useSettingsContext(); const { followNovel } = useNovelContext(); const pluginName = useMemo( diff --git a/src/screens/novel/components/Info/ReadButton.tsx b/src/screens/novel/components/Info/ReadButton.tsx index 2fdb8337fc..e53d33ac51 100644 --- a/src/screens/novel/components/Info/ReadButton.tsx +++ b/src/screens/novel/components/Info/ReadButton.tsx @@ -3,9 +3,9 @@ import React from 'react'; import { Button } from '@components'; import { getString } from '@strings/translations'; import { ChapterInfo } from '@database/types'; -import { useAppSettings } from '@hooks/persisted'; import Animated, { ZoomIn } from 'react-native-reanimated'; import { StyleSheet } from 'react-native'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface ReadButtonProps { chapters: ChapterInfo[]; @@ -18,7 +18,7 @@ const ReadButton = ({ lastRead, navigateToChapter, }: ReadButtonProps) => { - const { useFabForContinueReading = false } = useAppSettings(); + const { useFabForContinueReading = false } = useSettingsContext(); const navigateToLastReadChapter = () => { if (lastRead) { diff --git a/src/screens/novel/components/LoadingAnimation/NovelScreenLoading.tsx b/src/screens/novel/components/LoadingAnimation/NovelScreenLoading.tsx index 5ab2243f49..08448a86ba 100644 --- a/src/screens/novel/components/LoadingAnimation/NovelScreenLoading.tsx +++ b/src/screens/novel/components/LoadingAnimation/NovelScreenLoading.tsx @@ -4,8 +4,9 @@ import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'; import { LinearGradient } from 'expo-linear-gradient'; import { ThemeColors } from '@theme/types'; import useLoadingColors from '@utils/useLoadingColors'; -import { useAppSettings, useTheme } from '@hooks/persisted/index'; +import { useTheme } from '@hooks/persisted/index'; import { WINDOW_WIDTH } from '@gorhom/bottom-sheet'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface Props { theme: ThemeColors; @@ -25,7 +26,7 @@ export const LoadingShimmer = memo( width: number | string; visible?: boolean; }) => { - const { disableLoadingAnimations } = useAppSettings(); + const { disableLoadingAnimations } = useSettingsContext(); const theme = useTheme(); const [highlightColor, backgroundColor] = useLoadingColors(theme); if (!visible) { diff --git a/src/screens/novel/components/NovelScreenList.tsx b/src/screens/novel/components/NovelScreenList.tsx index aef02d0341..11b18db849 100644 --- a/src/screens/novel/components/NovelScreenList.tsx +++ b/src/screens/novel/components/NovelScreenList.tsx @@ -5,7 +5,7 @@ import { useRef, useState } from 'react'; import { pickCustomNovelCover } from '@database/queries/NovelQueries'; import { ChapterInfo, NovelInfo } from '@database/types'; import { useBoolean } from '@hooks/index'; -import { useAppSettings, useDownload, useTheme } from '@hooks/persisted'; +import { useDownload, useTheme } from '@hooks/persisted'; import { updateNovel, updateNovelPage, @@ -31,6 +31,7 @@ import { FlashList, FlashListRef } from '@shopify/flash-list'; import FileManager from '@specs/NativeFile'; import { downloadFile } from '@plugins/helpers/fetch'; import { StorageAccessFramework } from 'expo-file-system/legacy'; +import { useSettingsContext } from '@components/Context/SettingsContext'; type NovelScreenListProps = { headerOpacity: SharedValue; @@ -94,7 +95,7 @@ const NovelScreenList = ({ disableHapticFeedback, downloadNewChapters, refreshNovelMetadata, - } = useAppSettings(); + } = useSettingsContext(); const { sort = defaultChapterSort, diff --git a/src/screens/reader/ChapterLoadingScreen/ChapterLoadingScreen.tsx b/src/screens/reader/ChapterLoadingScreen/ChapterLoadingScreen.tsx index c83d8102df..563f788a15 100644 --- a/src/screens/reader/ChapterLoadingScreen/ChapterLoadingScreen.tsx +++ b/src/screens/reader/ChapterLoadingScreen/ChapterLoadingScreen.tsx @@ -3,15 +3,11 @@ import { View } from 'react-native'; import color from 'color'; import SkeletonLines from '../components/SkeletonLines'; -import { useChapterReaderSettings } from '@hooks/persisted'; +import { useSettingsContext } from '@components/Context/SettingsContext'; const ChapterLoadingScreen = () => { - const { - theme: backgroundColor, - padding, - textSize, - lineHeight, - } = useChapterReaderSettings(); + const { backgroundColor, padding, textSize, lineHeight } = + useSettingsContext(); return ( diff --git a/src/screens/reader/ReaderScreen.tsx b/src/screens/reader/ReaderScreen.tsx index 1677e1ece9..fe62dd071f 100644 --- a/src/screens/reader/ReaderScreen.tsx +++ b/src/screens/reader/ReaderScreen.tsx @@ -1,5 +1,5 @@ import React, { useRef, useCallback, useState, useEffect } from 'react'; -import { useChapterGeneralSettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import ReaderAppbar from './components/ReaderAppbar'; import ReaderFooter from './components/ReaderFooter'; @@ -18,6 +18,7 @@ import { useBackHandler } from '@hooks/index'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { StyleSheet, View } from 'react-native'; import { Drawer } from 'react-native-drawer-layout'; +import { useSettingsContext } from '@components/Context/SettingsContext'; const Chapter = ({ route, navigation }: ChapterScreenProps) => { const [open, setOpen] = useState(false); @@ -67,7 +68,7 @@ export const ChapterContent = ({ const { novel, chapter } = useChapterContext(); const readerSheetRef = useRef(null); const theme = useTheme(); - const { pageReader = false, keepScreenOn } = useChapterGeneralSettings(); + const { pageReader, keepScreenOn } = useSettingsContext(); const [bookmarked, setBookmarked] = useState(chapter.bookmark); useEffect(() => { diff --git a/src/screens/reader/components/ChapterDrawer/index.tsx b/src/screens/reader/components/ChapterDrawer/index.tsx index c5dc7972ac..e2ce4afe38 100644 --- a/src/screens/reader/components/ChapterDrawer/index.tsx +++ b/src/screens/reader/components/ChapterDrawer/index.tsx @@ -7,7 +7,7 @@ import React, { } from 'react'; import { StyleSheet, View } from 'react-native'; import { Text } from 'react-native-paper'; -import { useAppSettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import { Button, LoadingScreenV2 } from '@components/index'; import { EdgeInsets, useSafeAreaInsets } from 'react-native-safe-area-context'; import { getString } from '@strings/translations'; @@ -17,6 +17,7 @@ import { useChapterContext } from '@screens/reader/ChapterContext'; import { useNovelContext } from '@screens/novel/NovelContext'; import { FlashList, FlashListRef, ViewToken } from '@shopify/flash-list'; import { ChapterInfo } from '@database/types'; +import { useSettingsContext } from '@components/Context/SettingsContext'; type ButtonProperties = { text: string; @@ -33,7 +34,7 @@ const ChapterDrawer = () => { const { chapters, novelSettings, pages, setPageIndex } = useNovelContext(); const theme = useTheme(); const insets = useSafeAreaInsets(); - const { defaultChapterSort } = useAppSettings(); + const { defaultChapterSort } = useSettingsContext(); const listRef = useRef | null>(null); const styles = createStylesheet(theme, insets); diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx index b1a54df811..3e3e967b99 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderBottomSheet.tsx @@ -6,26 +6,32 @@ import { useWindowDimensions, View, } from 'react-native'; -import React, { RefObject, useMemo, useState, useCallback } from 'react'; +import React, { + RefObject, + useMemo, + useState, + useCallback, + Suspense, +} from 'react'; import Color from 'color'; import { BottomSheetFlashList, BottomSheetView } from '@gorhom/bottom-sheet'; import BottomSheet from '@components/BottomSheet/BottomSheet'; -import { useChapterGeneralSettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import { SceneMap, TabBar, TabView } from 'react-native-tab-view'; import { getString } from '@strings/translations'; -import ReaderSheetPreferenceItem from './ReaderSheetPreferenceItem'; -import TextSizeSlider from './TextSizeSlider'; -import ReaderThemeSelector from './ReaderThemeSelector'; -import ReaderTextAlignSelector from './ReaderTextAlignSelector'; -import ReaderValueChange from './ReaderValueChange'; -import ReaderFontPicker from './ReaderFontPicker'; import { overlay } from 'react-native-paper'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { BottomSheetModalMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; -import { StringMap } from '@strings/types'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import RenderSettings from '@screens/settings/dynamic/RenderSettings'; +import ReaderSettings from '@screens/settings/settingsGroups/readerSettingsGroup'; +import { useSettingsContext } from '@components/Context/SettingsContext'; +import SETTINGS, { + QuickSettingsItem, + readerIds, +} from '@screens/settings/Settings'; type TabViewLabelProps = { route: { @@ -40,89 +46,47 @@ type TabViewLabelProps = { }; const ReaderTab: React.FC = React.memo(() => { + const settings = ReaderSettings.subGroup.filter( + v => v.id === 'readerTheme', + )[0].settings; return ( - - - - - - - - + }> + + {settings.map((v, i) => ( + + ))} + + ); }); const GeneralTab: React.FC = React.memo(() => { - const theme = useTheme(); - const { setChapterGeneralSettings, ...settings } = - useChapterGeneralSettings(); - - const toggleSetting = useCallback( - (key: keyof typeof settings) => - setChapterGeneralSettings({ [key]: !settings[key] }), - [setChapterGeneralSettings, settings], - ); - - const preferences = useMemo( - () => [ - { key: 'fullScreenMode', label: 'fullscreen' }, - { key: 'autoScroll', label: 'autoscroll' }, - { key: 'verticalSeekbar', label: 'verticalSeekbar' }, - { key: 'showBatteryAndTime', label: 'showBatteryAndTime' }, - { key: 'showScrollPercentage', label: 'showProgressPercentage' }, - { key: 'swipeGestures', label: 'swipeGestures' }, - { key: 'pageReader', label: 'pageReader' }, - { key: 'removeExtraParagraphSpacing', label: 'removeExtraSpacing' }, - { key: 'useVolumeButtons', label: 'volumeButtonsScroll' }, - { key: 'bionicReading', label: 'bionicReading' }, - { key: 'tapToScroll', label: 'tapToScroll' }, - { key: 'keepScreenOn', label: 'keepScreenOn' }, - ], - [], - ); + const settings = useSettingsContext(); + + const quickSettings = useMemo(() => { + const ids: readerIds[] = ['general', 'autoScroll', 'display']; + const selectedSettings = SETTINGS.reader.subGroup + .filter(sg => ids.includes(sg.id)) + .flatMap(sg => sg.settings); + return (selectedSettings?.filter(s => s.quickSettings) ?? + []) as Array; + }, []); const renderItem = useCallback( - ({ - item, - }: { - item: { - key: string; - label: string; - }; - }) => ( - toggleSetting(item.key as keyof typeof settings)} // @ts-ignore - value={settings[item.key]} - theme={theme} - /> + ({ item }: { item: QuickSettingsItem }) => ( + ), - [settings, theme, toggleSetting], + [], ); return ( - - item.key} - renderItem={renderItem} - /> - + `general${i}`} + renderItem={renderItem} + estimatedItemSize={60} + /> ); }); diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx index cc72df00b9..cab73beccf 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderFontPicker.tsx @@ -4,10 +4,11 @@ import color from 'color'; import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons'; import { getString } from '@strings/translations'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import { Font, readerFonts } from '@utils/constants/readerConstants'; import { FlatList } from 'react-native-gesture-handler'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface FontChipProps { item: Font; @@ -15,7 +16,8 @@ interface FontChipProps { const ReaderFontPicker = () => { const theme = useTheme(); - const { fontFamily, setChapterReaderSettings } = useChapterReaderSettings(); + const { fontFamily, setSettings: setChapterReaderSettings } = + useSettingsContext(); const isSelected = useCallback( (item: Font) => item.fontFamily === fontFamily, diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx index cce2301ed5..f6bb4ebd7d 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderTextAlignSelector.tsx @@ -1,10 +1,11 @@ import { StyleSheet, Text, TextStyle, View } from 'react-native'; import React from 'react'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import { textAlignments } from '@utils/constants/readerConstants'; import { ToggleButton } from '@components/Common/ToggleButton'; import { getString } from '@strings/translations'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface ReaderTextAlignSelectorProps { labelStyle?: TextStyle | TextStyle[]; @@ -14,7 +15,8 @@ const ReaderTextAlignSelector: React.FC = ({ labelStyle, }) => { const theme = useTheme(); - const { textAlign, setChapterReaderSettings } = useChapterReaderSettings(); + const { textAlign, setSettings: setChapterReaderSettings } = + useSettingsContext(); return ( diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx index 15cb1c0fab..1b566bdf6a 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderThemeSelector.tsx @@ -1,11 +1,12 @@ import { StyleSheet, Text, TextStyle, View } from 'react-native'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { ToggleColorButton } from '@components/Common/ToggleButton'; import { getString } from '@strings/translations'; import { presetReaderThemes } from '@utils/constants/readerConstants'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import { FlatList } from 'react-native-gesture-handler'; -import { ReaderTheme } from '@hooks/persisted/useSettings'; +import { ReaderTheme } from '@screens/settings/constants/defaultValues'; +import { useSettingsContext } from '@components/Context/SettingsContext'; interface ReaderThemeSelectorProps { label?: string; @@ -19,11 +20,21 @@ const ReaderThemeSelector: React.FC = ({ const theme = useTheme(); const { - theme: backgroundColor, + backgroundColor, textColor, customThemes, - setChapterReaderSettings, - } = useChapterReaderSettings(); + setSettings: setChapterReaderSettings, + } = useSettingsContext(); + + const [listWidth, setListWidth] = React.useState(0); + + const data = [...customThemes, ...presetReaderThemes] as ReaderTheme[]; + const spacing = useMemo( + () => ({ width: listWidth - 56 * data.length }), + [data.length, listWidth], + ); + + const Spacer = useCallback(() => , [spacing]); return ( @@ -33,7 +44,7 @@ const ReaderThemeSelector: React.FC = ({ {label || getString('readerScreen.bottomSheet.color')} ( = ({ textColor={item.textColor} onPress={() => setChapterReaderSettings({ - theme: item.backgroundColor, + backgroundColor: item.backgroundColor, textColor: item.textColor, }) } @@ -54,6 +65,8 @@ const ReaderThemeSelector: React.FC = ({ keyExtractor={(item, index) => item.textColor + '_' + index} horizontal={true} showsHorizontalScrollIndicator={false} + onLayout={e => setListWidth(e.nativeEvent.layout.width)} + ListHeaderComponent={Spacer} /> ); diff --git a/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx b/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx index d3b4b9b925..206e4936d2 100644 --- a/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/ReaderValueChange.tsx @@ -1,9 +1,10 @@ import { StyleSheet, Text, TextStyle, View } from 'react-native'; import React from 'react'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import { IconButtonV2 } from '@components'; -import { ChapterReaderSettings } from '@hooks/persisted/useSettings'; +import { DefaultSettings } from '@screens/settings/constants/defaultValues'; +import { useSettingsContext } from '@components/Context/SettingsContext'; type ValueKey = Exclude< { @@ -16,7 +17,7 @@ interface ReaderValueChangeProps { labelStyle?: TextStyle | TextStyle[]; valueChange?: number; label: string; - valueKey: ValueKey; + valueKey: ValueKey; decimals?: number; min?: number; max?: number; @@ -34,7 +35,7 @@ const ReaderValueChange: React.FC = ({ unit = '%', }) => { const theme = useTheme(); - const { setChapterReaderSettings, ...settings } = useChapterReaderSettings(); + const { setSettings, ...settings } = useSettingsContext(); return ( @@ -48,7 +49,7 @@ const ReaderValueChange: React.FC = ({ size={26} disabled={settings[valueKey] <= min} onPress={() => - setChapterReaderSettings({ + setSettings({ [valueKey]: Math.max(min, settings[valueKey] - valueChange), }) } @@ -63,7 +64,7 @@ const ReaderValueChange: React.FC = ({ size={26} disabled={settings[valueKey] >= max} onPress={() => - setChapterReaderSettings({ + setSettings({ [valueKey]: Math.min(max, settings[valueKey] + valueChange), }) } diff --git a/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx b/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx index 73225c7b09..54f0eef39e 100644 --- a/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx +++ b/src/screens/reader/components/ReaderBottomSheet/TextSizeSlider.tsx @@ -1,16 +1,18 @@ import { StyleSheet, Text, View } from 'react-native'; import React from 'react'; -import { useChapterReaderSettings, useTheme } from '@hooks/persisted'; +import { useTheme } from '@hooks/persisted'; import Slider from '@react-native-community/slider'; import { getString } from '@strings/translations'; +import { useSettingsContext } from '@components/Context/SettingsContext'; const TRACK_TINT_COLOR = '#000000'; const TextSizeSlider: React.FC = () => { const theme = useTheme(); - const { textSize, setChapterReaderSettings } = useChapterReaderSettings(); + const { textSize, setSettings: setChapterReaderSettings } = + useSettingsContext(); return ( diff --git a/src/screens/reader/components/SkeletonLines.tsx b/src/screens/reader/components/SkeletonLines.tsx index 40e4303e2c..aa22838d13 100644 --- a/src/screens/reader/components/SkeletonLines.tsx +++ b/src/screens/reader/components/SkeletonLines.tsx @@ -2,7 +2,7 @@ import React, { memo } from 'react'; import { View, Dimensions, StyleSheet, DimensionValue } from 'react-native'; import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'; import { LinearGradient } from 'expo-linear-gradient'; -import { useAppSettings } from '@hooks/persisted/index'; +import { useSettingsContext } from '@components/Context/SettingsContext'; const SkeletonLines = ({ width, @@ -23,7 +23,7 @@ const SkeletonLines = ({ color?: string; highlightColor?: string; }) => { - const { disableLoadingAnimations } = useAppSettings(); + const { disableLoadingAnimations } = useSettingsContext(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const styles = createStyleSheet( containerWidth, diff --git a/src/screens/reader/components/WebViewReader.tsx b/src/screens/reader/components/WebViewReader.tsx index 82d07d9ba9..4ff8c2aba9 100644 --- a/src/screens/reader/components/WebViewReader.tsx +++ b/src/screens/reader/components/WebViewReader.tsx @@ -7,19 +7,17 @@ import { useTheme } from '@hooks/persisted'; import { getString } from '@strings/translations'; import { getPlugin } from '@plugins/pluginManager'; -import { MMKVStorage, getMMKVObject } from '@utils/mmkv/mmkv'; +import { MMKVStorage } from '@utils/mmkv/mmkv'; import { CHAPTER_GENERAL_SETTINGS, CHAPTER_READER_SETTINGS, - ChapterGeneralSettings, - ChapterReaderSettings, - initialChapterGeneralSettings, - initialChapterReaderSettings, } from '@hooks/persisted/useSettings'; + import { getBatteryLevelSync } from 'react-native-device-info'; import * as Speech from 'expo-speech'; import { PLUGIN_STORAGE } from '@utils/Storages'; import { useChapterContext } from '../ChapterContext'; +import { useSettingsContext } from '@components/Context/SettingsContext'; type WebViewPostEvent = { type: string; @@ -59,22 +57,8 @@ const WebViewReader: React.FC = ({ onPress }) => { webViewRef, } = useChapterContext(); const theme = useTheme(); - const readerSettings = useMemo( - () => - getMMKVObject(CHAPTER_READER_SETTINGS) || - initialChapterReaderSettings, - // needed to preserve settings during chapter change - // eslint-disable-next-line react-hooks/exhaustive-deps - [chapter.id], - ); - const chapterGeneralSettings = useMemo( - () => - getMMKVObject(CHAPTER_GENERAL_SETTINGS) || - initialChapterGeneralSettings, - // needed to preserve settings during chapter change - // eslint-disable-next-line react-hooks/exhaustive-deps - [chapter.id], - ); + const settings = useSettingsContext(); + const batteryLevel = useMemo(() => getBatteryLevelSync(), []); const plugin = getPlugin(novel?.pluginId); const pluginCustomJS = `file://${PLUGIN_STORAGE}/${plugin?.id}/custom.js`; @@ -86,7 +70,7 @@ const WebViewReader: React.FC = ({ onPress }) => { switch (key) { case CHAPTER_READER_SETTINGS: webViewRef.current?.injectJavaScript( - `reader.readerSettings.val = ${MMKVStorage.getString( + `reader.settings.val = ${MMKVStorage.getString( CHAPTER_READER_SETTINGS, )}`, ); @@ -117,7 +101,7 @@ const WebViewReader: React.FC = ({ onPress }) => { return ( = ({ onPress }) => { onDone() { webViewRef.current?.injectJavaScript('tts.next?.()'); }, - voice: readerSettings.tts?.voice?.identifier, - pitch: readerSettings.tts?.pitch || 1, - rate: readerSettings.tts?.rate || 1, + voice: settings.tts?.voice?.identifier, + pitch: settings.tts?.pitch || 1, + rate: settings.tts?.rate || 1, }); } else { webViewRef.current?.injectJavaScript('tts.next?.()'); @@ -178,13 +162,13 @@ const WebViewReader: React.FC = ({ onPress }) => { - + - +
${chapter.name}
${html} @@ -235,8 +217,8 @@ const WebViewReader: React.FC = ({ onPress }) => { var initialReaderConfig = ${JSON.stringify({ - readerSettings, - chapterGeneralSettings, + readerSettings: settings, + chapterGeneralSettings: settings, novel, chapter, nextChapter, @@ -262,7 +244,16 @@ const WebViewReader: React.FC = ({ onPress }) => { `, diff --git a/src/screens/reader/hooks/useChapter.ts b/src/screens/reader/hooks/useChapter.ts index 24653ac016..12c07f66e6 100644 --- a/src/screens/reader/hooks/useChapter.ts +++ b/src/screens/reader/hooks/useChapter.ts @@ -5,12 +5,7 @@ import { } from '@database/queries/ChapterQueries'; import { insertHistory } from '@database/queries/HistoryQueries'; import { ChapterInfo, NovelInfo } from '@database/types'; -import { - useChapterGeneralSettings, - useLibrarySettings, - useTrackedNovel, - useTracker, -} from '@hooks/persisted'; +import { useTrackedNovel, useTracker } from '@hooks/persisted'; import { fetchChapter } from '@services/plugin/fetch'; import { NOVEL_STORAGE } from '@utils/Storages'; import { @@ -27,12 +22,12 @@ import WebView from 'react-native-webview'; import { useFullscreenMode } from '@hooks'; import { Dimensions, NativeEventEmitter } from 'react-native'; import * as Speech from 'expo-speech'; -import { defaultTo } from 'lodash-es'; import { showToast } from '@utils/showToast'; import { getString } from '@strings/translations'; import NativeVolumeButtonListener from '@specs/NativeVolumeButtonListener'; import NativeFile from '@specs/NativeFile'; import { useNovelContext } from '@screens/novel/NovelContext'; +import { useSettingsContext } from '@components/Context/SettingsContext'; const emmiter = new NativeEventEmitter(NativeVolumeButtonListener); @@ -55,9 +50,13 @@ export default function useChapter( const [[nextChapter, prevChapter], setAdjacentChapter] = useState< ChapterInfo[] | undefined[] >([]); - const { autoScroll, autoScrollInterval, autoScrollOffset, useVolumeButtons } = - useChapterGeneralSettings(); - const { incognitoMode } = useLibrarySettings(); + const { + autoScroll, + autoScrollInterval, + autoScrollOffsetPercent, + useVolumeButtons, + incognitoMode, + } = useSettingsContext(); const [error, setError] = useState(); const { tracker } = useTracker(); const { trackedNovel, updateNovelProgess } = useTrackedNovel(novel.id); @@ -167,10 +166,9 @@ export default function useChapter( if (autoScroll) { scrollInterval.current = setInterval(() => { webViewRef.current?.injectJavaScript(`(()=>{ - window.scrollBy({top:${defaultTo( - autoScrollOffset, - Dimensions.get('window').height, - )},behavior:'smooth'}) + window.scrollBy({top:${ + Dimensions.get('window').height * (autoScrollOffsetPercent / 100) + },behavior:'smooth'}) })()`); }, autoScrollInterval * 1000); } else { @@ -184,7 +182,7 @@ export default function useChapter( clearInterval(scrollInterval.current); } }; - }, [autoScroll, autoScrollInterval, autoScrollOffset, webViewRef]); + }, [autoScroll, autoScrollInterval, autoScrollOffsetPercent, webViewRef]); const updateTracker = useCallback(() => { const chapterNumber = parseChapterNumber(novel.name, chapter.name); diff --git a/src/screens/settings/Settings.d.ts b/src/screens/settings/Settings.d.ts new file mode 100644 index 0000000000..dabb16c4c2 --- /dev/null +++ b/src/screens/settings/Settings.d.ts @@ -0,0 +1,168 @@ +import type { FilteredSettings, ReaderTheme } from './constants/defaultValues'; +import type { ThemeColors } from '@theme/types'; +import InfoItem from './dynamic/components/InfoItem'; + +type settingsGroupTypes = + | 'GeneralSettings' + | 'ReaderSettings' + | 'TrackerSettings' + | 'BackupSettings' + | 'AppearanceSettings' + | 'AdvancedSettings' + | 'LibrarySettings' + | 'RespositorySettings' + | 'RepoSettings' + | undefined; + +export type SettingsTypeModes = 'single' | 'multiple' | 'order'; + +export type SettingOrigin = 'lastUpdateTime' | 'MMKV'; + +export type ModalSettingsType = + | { + mode: 'single'; + valueKey: FilteredSettings; + description?: (value: number) => string; + options: Array<{ + label: string; + value: number; + }>; + } + | { + mode: 'multiple'; + valueKey?: never; + description?: (value: Array) => string; + options: Array<{ + label: string; + key: FilteredSettings; + }>; + } + | { + mode: 'order'; + valueKey: FilteredSettings; + description?: (value: string) => string; + options: Array<{ + label: string; + ASC: string; + DESC: string; + }>; + }; + +export type ModalSetting = ModalSettingsType & { + title: string; + type: 'Modal'; +}; +export type _SwitchSetting = { + title: string; + description?: string; + type: 'Switch'; + settingsOrigin?: T; + valueKey: T extends undefined ? FilteredSettings : undefined; + dependents?: Array; +}; +export type SwitchSetting = + | _SwitchSetting<'lastUpdateTime'> + | _SwitchSetting; + +export type NumberInputSetting = { + title: string; + description?: string; + type: 'NumberInput'; + valueKey: FilteredSettings; +}; + +export type TextAreaSetting = { + title: string; + placeholder?: string; + description?: string; + openFileLabel: string; + clearDialog: string; + type: 'TextArea'; + valueKey: FilteredSettings; +}; + +export type ThemePickerSetting = { + title: string; + type: 'ThemePicker'; + options: Array; +}; + +type _ColorPickerSetting = { + title: string; + description?: (val: string) => string; + type: 'ColorPicker'; + settingsOrigin?: T; + valueKey: T extends undefined ? keyof ReaderTheme : undefined; +}; +export type ColorPickerSetting = + | _ColorPickerSetting<'MMKV'> + | _ColorPickerSetting; + +export type ReaderThemeSetting = { type: 'ReaderTheme' }; +export type ReaderTTSSetting = { type: 'TTS' }; +export type RepoSetting = { type: 'Repo' }; +export type TrackerSetting = { + type: 'Tracker'; + trackerName: 'AniList' | 'MyAnimeList'; +}; +export type InfoItem = { type: 'InfoItem'; title: string }; + +export type BaseSetting = { + quickSettings?: boolean; +}; + +export type SettingsItem = BaseSetting & + ( + | ModalSetting + | SwitchSetting + | ThemePickerSetting + | ColorPickerSetting + | NumberInputSetting + | TextAreaSetting + | ReaderThemeSetting + | ReaderTTSSetting + | RepoSetting + | TrackerSetting + | InfoItem + ); + + export type QuickSettingsItem = { quickSettings: true } & SettingsItem; + +export interface SettingSubGroup { + subGroupTitle: string; + id: T; + settings: Array; +} + +export interface SettingsGroup { + groupTitle: string; + icon: string; + navigateParam: settingsGroupTypes; + subGroup: SettingSubGroup[]; +} + +type generalIds = + | 'display' + | 'library' + | 'novel' + | 'globalUpdate' + | 'autoDownload' + | 'general'; +type appearanceIds = 'appTheme' | 'novelInfo' | 'navbar'; +type readerIds = + | 'readerTheme' + | 'customCSS' + | 'customJS' + | 'tts' + | 'general' + | 'autoScroll' + | 'display'; +type repoIds = ''; +type trackerIds = 'services'; +export interface Settings { + general: SettingsGroup; + appearance: SettingsGroup; + reader: SettingsGroup; + repo: SettingsGroup; + tracker: SettingsGroup; +} diff --git a/src/screens/settings/Settings.ts b/src/screens/settings/Settings.ts new file mode 100644 index 0000000000..c8090b4859 --- /dev/null +++ b/src/screens/settings/Settings.ts @@ -0,0 +1,23 @@ +import GeneralSettings from './settingsGroups/generalSettingsGroup'; +import AppearanceSettings from './settingsGroups/appearanceSettingsGroup'; +import ReaderSettings from './settingsGroups/readerSettingsGroup'; +import RepoSettings from './settingsGroups/repoSettingsGroup'; +import TrackerSettings from './settingsGroups/trackerSettingsGroup'; +import { + defaultSettings as de, + DefaultSettings, +} from './constants/defaultValues'; +import { Settings } from './Settings.d'; + +export * from './Settings.d'; + +const SETTINGS: Settings = { + general: GeneralSettings, + appearance: AppearanceSettings, + reader: ReaderSettings, + repo: RepoSettings, + tracker: TrackerSettings, +} as const; +export default SETTINGS; + +export const DEFAULTSETTINGS: DefaultSettings = de; diff --git a/src/screens/settings/SettingsAdvancedScreen.tsx b/src/screens/settings/SettingsAdvancedScreen.tsx deleted file mode 100644 index 84b8375719..0000000000 --- a/src/screens/settings/SettingsAdvancedScreen.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import React, { useState } from 'react'; - -import { Portal, Text, TextInput } from 'react-native-paper'; - -import { useTheme, useUserAgent } from '@hooks/persisted'; -import { showToast } from '@utils/showToast'; - -import { deleteCachedNovels } from '@hooks/persisted/useNovel'; -import { getString } from '@strings/translations'; -import { useBoolean } from '@hooks'; -import ConfirmationDialog from '@components/ConfirmationDialog/ConfirmationDialog'; -import { - deleteReadChaptersFromDb, - clearUpdates, -} from '@database/queries/ChapterQueries'; - -import { Appbar, Button, List, Modal, SafeAreaView } from '@components'; -import { AdvancedSettingsScreenProps } from '@navigators/types'; -import { StyleSheet, View } from 'react-native'; -import { getUserAgentSync } from 'react-native-device-info'; -import CookieManager from '@react-native-cookies/cookies'; -import { store } from '@plugins/helpers/storage'; -import { recreateDBIndex } from '@database/db'; - -const AdvancedSettings = ({ navigation }: AdvancedSettingsScreenProps) => { - const theme = useTheme(); - const clearCookies = () => { - CookieManager.clearAll(); - store.clearAll(); - showToast(getString('webview.cookiesCleared')); - }; - - const { userAgent, setUserAgent } = useUserAgent(); - const [userAgentInput, setUserAgentInput] = useState(userAgent); - /** - * Confirm Clear Database Dialog - */ - const [clearDatabaseDialog, setClearDatabaseDialog] = useState(false); - const showClearDatabaseDialog = () => setClearDatabaseDialog(true); - const hideClearDatabaseDialog = () => setClearDatabaseDialog(false); - - const [clearUpdatesDialog, setClearUpdatesDialog] = useState(false); - const showClearUpdatesDialog = () => setClearUpdatesDialog(true); - const hideClearUpdatesDialog = () => setClearUpdatesDialog(false); - - const { - value: deleteReadChaptersDialog, - setTrue: showDeleteReadChaptersDialog, - setFalse: hideDeleteReadChaptersDialog, - } = useBoolean(); - - const { - value: userAgentModalVisible, - setTrue: showUserAgentModal, - setFalse: hideUserAgentModal, - } = useBoolean(); - - const { - value: recreateDBIndexDialog, - setTrue: showRecreateDBIndexDialog, - setFalse: hideRecreateDBIndexDialog, - } = useBoolean(); - - return ( - - navigation.goBack()} - theme={theme} - /> - - - {getString('advancedSettingsScreen.dataManagement')} - - - - - - - - - - - { - recreateDBIndex(); - showToast( - getString('advancedSettingsScreen.recreateDBIndexesToast'), - ); - }} - onDismiss={hideRecreateDBIndexDialog} - theme={theme} - /> - - { - clearUpdates(); - showToast(getString('advancedSettingsScreen.clearUpdatesMessage')); - hideClearUpdatesDialog(); - }} - onDismiss={hideClearUpdatesDialog} - theme={theme} - /> - - - - {getString('advancedSettingsScreen.userAgent')} - - {userAgent} - setUserAgentInput(text.trim())} - placeholderTextColor={theme.onSurfaceDisabled} - underlineColor={theme.outline} - style={[{ color: theme.onSurface }, styles.textInput]} - theme={{ colors: { ...theme } }} - /> - - - - - ); -}; - -export default DefaultCategoryDialog; - -const styles = StyleSheet.create({ - scrollArea: { - maxHeight: 480, - }, -}); diff --git a/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx b/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx deleted file mode 100644 index 3f33d7b30b..0000000000 --- a/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import { Appbar, List } from '@components'; -import { getString } from '@strings/translations'; -import { useBoolean } from '@hooks'; -import { useCategories, useTheme } from '@hooks/persisted'; -import { useNavigation } from '@react-navigation/native'; -import { Portal } from 'react-native-paper'; -import DefaultCategoryDialog from './DefaultCategoryDialog'; - -const SettingsLibraryScreen = () => { - const theme = useTheme(); - const { goBack, navigate } = useNavigation(); - const { categories } = useCategories(); - - const defaultCategoryDialog = useBoolean(); - - const setDefaultCategoryId = (categoryId: number) => { - // TODO: update default category - - categoryId; - }; - - return ( - <> - - - navigate('MoreStack', { screen: 'Categories' })} - theme={theme} - /> - category.sort === 1)?.name} - onPress={defaultCategoryDialog.setTrue} - theme={theme} - /> - - - - - - ); -}; - -export default SettingsLibraryScreen; diff --git a/src/screens/settings/SettingsReaderScreen/Modals/CustomFileModal.tsx b/src/screens/settings/SettingsReaderScreen/Modals/CustomFileModal.tsx deleted file mode 100644 index 3a1acb50be..0000000000 --- a/src/screens/settings/SettingsReaderScreen/Modals/CustomFileModal.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, { useState } from 'react'; -import { StyleSheet, Text, View } from 'react-native'; -import { TextInput } from 'react-native-paper'; -import { StorageAccessFramework } from 'expo-file-system/legacy'; -import * as DocumentPicker from 'expo-document-picker'; - -import { Button, Modal } from '@components/index'; - -import { showToast } from '@utils/showToast'; -import { useTheme } from '@hooks/persisted'; -import { getString } from '@strings/translations'; - -interface CustomFileModal { - visible: boolean; - onDismiss: () => void; - defaultValue: string; - onSave: (val: string) => void; - title: string; - mimeType: 'text/css' | 'application/javascript'; - openFileLabel: string; - placeholder?: string; - description?: string; -} - -const CustomFileModal: React.FC = ({ - onDismiss, - visible, - defaultValue, - onSave, - title, - mimeType, - openFileLabel, - placeholder, - description, -}) => { - const theme = useTheme(); - const [text, setText] = useState(''); - - const openDocumentPicker = async () => { - try { - const file = await DocumentPicker.getDocumentAsync({ - copyToCacheDirectory: false, - type: mimeType, - }); - - if (file.assets) { - const content = await StorageAccessFramework.readAsStringAsync( - file.assets[0].uri, - ); - - onSave(content.trim()); - onDismiss(); - } - } catch (error: any) { - showToast(error.message); - } - }; - - return ( - - - {title} - - {description} - - - - - - - - - - - ); -}; - -export default TrackerScreen; - -const styles = StyleSheet.create({ - flex1: { - flex: 1, - }, - screenPadding: { - paddingVertical: 8, - }, - modalText: { - fontSize: 18, - }, - modalButtonRow: { - flexDirection: 'row', - justifyContent: 'flex-end', - }, - modalButton: { - marginTop: 30, - }, - modalButtonLabel: { - letterSpacing: 0, - textTransform: 'none', - }, -}); diff --git a/src/screens/settings/components/ConnectionModal.tsx b/src/screens/settings/components/ConnectionModal.tsx deleted file mode 100644 index 845a1805c4..0000000000 --- a/src/screens/settings/components/ConnectionModal.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Button, Modal } from '@components'; -import { getString } from '@strings/translations'; -import { ThemeColors } from '@theme/types'; -import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; -import { TextInput } from 'react-native-paper'; - -interface ConnectionModalProps { - title: string; - ipv4: string; - port: string; - visible: boolean; - theme: ThemeColors; - closeModal: () => void; - handle: (ipv4: string, port: string) => Promise; - setIpv4: React.Dispatch>; - setPort: React.Dispatch>; -} - -const ConnectionModal: React.FC = ({ - title, - ipv4, - port, - visible, - theme, - closeModal, - handle, - setIpv4, - setPort, -}) => { - return ( - - - {title} - - - - - + + + + + + ); +}; +export default memo(TrackerButton); + +const styles = StyleSheet.create({ + letter: { + letterSpacing: 0, + textTransform: 'none', + }, + marginTop: { + marginTop: 30, + }, + row: { flexDirection: 'row', justifyContent: 'flex-end' }, + fontSize: { fontSize: 18 }, + container: { padding: 20, margin: 20, borderRadius: 6 }, +}); diff --git a/src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx b/src/screens/settings/dynamic/modals/AddRepositoryModal.tsx similarity index 100% rename from src/screens/settings/SettingsRepositoryScreen/components/AddRepositoryModal.tsx rename to src/screens/settings/dynamic/modals/AddRepositoryModal.tsx diff --git a/src/screens/settings/dynamic/modals/ColorPickerModal.tsx b/src/screens/settings/dynamic/modals/ColorPickerModal.tsx new file mode 100644 index 0000000000..cc3126d36c --- /dev/null +++ b/src/screens/settings/dynamic/modals/ColorPickerModal.tsx @@ -0,0 +1,250 @@ +import React, { useState } from 'react'; +import { FlatList, Pressable, StyleSheet, Text, View } from 'react-native'; + +import { Modal, overlay, Portal } from 'react-native-paper'; +import { Button, List } from '@components'; +import { ThemeColors } from '@theme/types'; +import { useBoolean } from '@hooks/index'; +import { BaseSetting, ColorPickerSetting } from '@screens/settings/Settings'; +import { useMMKVString } from 'react-native-mmkv'; +import { useKeyboardHeight } from '@hooks/common/useKeyboardHeight'; +import TextInput from '@components/TextInput/TextInput'; +import { getString } from '@strings/translations'; +import { useSettingsContext } from '@components/Context/SettingsContext'; + +const accentColors = [ + '#EF5350', + '#EC407A', + '#AB47BC', + '#7E57C2', + '#5C6BC0', + '#42A5F5', + '#29B6FC', + '#26C6DA', + '#26A69A', + '#66BB6A', + '#9CCC65', + '#D4E157', + '#FFEE58', + '#FFCA28', + '#FFA726', + '#FF7043', + '#8D6E63', + '#BDBDBD', + '#78909C', + '#000000', +]; + +interface ColorPickerModalProps { + settings: ColorPickerSetting & BaseSetting; + theme: ThemeColors; + showAccentColors?: boolean; + quickSettings?: boolean; +} + +const ColorPickerModal: React.FC = ({ + theme, + settings, + showAccentColors, + quickSettings, +}) => { + const [error, setError] = useState(); + const modalRef = useBoolean(); + const keyboardHeight = useKeyboardHeight(); + + const [, setCustomAccentColor] = useMMKVString('CUSTOM_ACCENT_COLOR'); + const { setSettings, ...currentSettings } = useSettingsContext(); + + const currentValue = + settings.settingsOrigin === 'MMKV' + ? rgbToHex(theme.primary) + : currentSettings[settings.valueKey!]; + + const [text, setText] = useState(currentValue); + + const update = (val: string) => { + setText(val); + if (settings.settingsOrigin === 'MMKV') { + setCustomAccentColor(val); + } else { + setSettings({ + [settings.valueKey!]: val, + }); + } + }; + + const onDismiss = () => { + modalRef.setFalse(); + if (error) { + setText(currentValue); + } + setError(null); + }; + + const onOpen = () => { + modalRef.setTrue(); + setText(currentValue); + }; + + const onSubmitEditing = () => { + setError(null); + const re = /^#([0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3})$/i; + let temp; + try { + temp = rgbToHex(text); + } catch (e) { + setError(typeof e === 'string' ? e : ''); + return; + } + + if (temp.match(re)) { + update(temp); + onDismiss(); + } else { + setError('Enter a valid hex color code'); + } + }; + + return ( + <> + + + + + {settings.title} + + {showAccentColors ? ( + item} + renderItem={({ item }) => ( + + { + update(item); + onDismiss(); + }} + /> + + )} + /> + ) : null} + + {error} + + + )} + ListEmptyComponent={emptyComponent} + /> + + + )} + ListEmptyComponent={EmptyComponent} + /> + +