From 58f9427008503c0744b7dff0eef19859d7c8f88d Mon Sep 17 00:00:00 2001 From: Patrick Loser <69157453+CD-Z@users.noreply.github.com> Date: Sat, 24 May 2025 18:18:16 +0200 Subject: [PATCH 1/3] Small fixes & rendering time improvements (#1511) * fix not being able to change from chapter drawer * add UpdatesContext * fix library search * Add Switch for plugin settings * improved BrowseScreen rendering time * improved rendering time further * fix download button --- App.tsx | 4 +- src/components/Button/Button.tsx | 16 +- src/components/Context/UpdateContext.tsx | 31 + src/hooks/common/useSearch.ts | 9 +- src/hooks/persisted/useUpdates.ts | 95 ++-- src/navigators/Main.tsx | 39 +- src/screens/browse/BrowseScreen.tsx | 3 +- .../browse/components/AvailableTab.tsx | 264 +++++++++ src/screens/browse/components/BrowseTabs.tsx | 532 ------------------ .../browse/components/InstalledTab.tsx | 503 +++++++++++++++++ .../components/Modals/SourceSettings.tsx | 52 +- src/screens/library/LibraryScreen.tsx | 20 +- .../Chapter/ChapterDownloadButtons.tsx | 57 +- src/screens/novel/components/ChapterItem.tsx | 8 +- .../reader/components/ChapterDrawer/index.tsx | 4 +- src/screens/reader/hooks/useChapter.ts | 3 +- src/screens/updates/UpdatesScreen.tsx | 26 +- 17 files changed, 992 insertions(+), 674 deletions(-) create mode 100644 src/components/Context/UpdateContext.tsx create mode 100644 src/screens/browse/components/AvailableTab.tsx delete mode 100644 src/screens/browse/components/BrowseTabs.tsx create mode 100644 src/screens/browse/components/InstalledTab.tsx diff --git a/App.tsx b/App.tsx index a9264c6e..90d1aa64 100644 --- a/App.tsx +++ b/App.tsx @@ -27,10 +27,10 @@ Notifications.setNotificationHandler({ }; }, }); +createTables(); +LottieSplashScreen.hide(); const App = () => { - createTables(); - LottieSplashScreen.hide(); return ( diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 3356aa0d..c531c1cb 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -5,19 +5,21 @@ import { } from 'react-native-paper'; import { useTheme } from '@hooks/persisted'; +import { ThemeProp } from 'react-native-paper/lib/typescript/types'; interface ButtonProps extends Partial { title?: string; } const Button: React.FC = props => { - const theme = useTheme(); - - return ( - - {props.title || props.children} - + const t = useTheme(); + const theme: ThemeProp = React.useMemo(() => ({ colors: t }), [t]); + const Children = React.useMemo( + () => props.title || props.children, + [props.children, props.title], ); + + return ; }; -export default Button; +export default React.memo(Button); diff --git a/src/components/Context/UpdateContext.tsx b/src/components/Context/UpdateContext.tsx new file mode 100644 index 00000000..c91ad25b --- /dev/null +++ b/src/components/Context/UpdateContext.tsx @@ -0,0 +1,31 @@ +import { useUpdates } from '@hooks/persisted'; +import React, { createContext, useContext, useMemo } from 'react'; + +type UpdateContextType = ReturnType; + +const defaultValue = {} as UpdateContextType; +const UpdateContext = createContext(defaultValue); + +export function UpdateContextProvider({ + children, +}: { + children: React.ReactNode; +}) { + const useUpdateParams = useUpdates(); + const contextValue = useMemo( + () => ({ + ...useUpdateParams, + }), + [useUpdateParams], + ); + + return ( + + {children} + + ); +} + +export const useUpdateContext = (): UpdateContextType => { + return useContext(UpdateContext); +}; diff --git a/src/hooks/common/useSearch.ts b/src/hooks/common/useSearch.ts index a5ca3888..33174ead 100644 --- a/src/hooks/common/useSearch.ts +++ b/src/hooks/common/useSearch.ts @@ -1,5 +1,5 @@ import { useIsFocused } from '@react-navigation/native'; -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; const useSearch = (defaultSearchText?: string, clearSearchOnUnfocus = true) => { const isFocused = useIsFocused(); @@ -14,9 +14,12 @@ const useSearch = (defaultSearchText?: string, clearSearchOnUnfocus = true) => { clearSearchbar(); } } - }, [isFocused, clearSearchbar, clearSearchOnUnfocus]); + }, [clearSearchbar, clearSearchOnUnfocus, isFocused]); - return { searchText, setSearchText, clearSearchbar }; + return useMemo( + () => ({ searchText, setSearchText, clearSearchbar }), + [searchText, setSearchText, clearSearchbar], + ); }; export default useSearch; diff --git a/src/hooks/persisted/useUpdates.ts b/src/hooks/persisted/useUpdates.ts index 99273aec..44b8653b 100644 --- a/src/hooks/persisted/useUpdates.ts +++ b/src/hooks/persisted/useUpdates.ts @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { getDetailedUpdatesFromDb, getUpdatedOverviewFromDb, @@ -8,6 +8,7 @@ import { Update, UpdateOverview } from '@database/types'; import { useMMKVBoolean, useMMKVString } from 'react-native-mmkv'; import dayjs from 'dayjs'; import { parseChapterNumber } from '@utils/parseChapterNumber'; +import { useFocusEffect } from '@react-navigation/native'; export const SHOW_LAST_UPDATE_TIME = 'SHOW_LAST_UPDATE_TIME'; export const LAST_UPDATE_TIME = 'LAST_UPDATE_TIME'; @@ -33,33 +34,33 @@ export const useUpdates = () => { useLastUpdate(); const [error, setError] = useState(''); - const getDetailedUpdates = async ( - novelId: number, - onlyDownloadedChapters: boolean = false, - ) => { - setIsLoading(true); + const getDetailedUpdates = useCallback( + async (novelId: number, onlyDownloadedChapters: boolean = false) => { + setIsLoading(true); - let result: Update[] = await getDetailedUpdatesFromDb( - novelId, - onlyDownloadedChapters, - ); - result = result.map(update => { - const parsedTime = dayjs(update.releaseTime); - return { - ...update, - releaseTime: parsedTime.isValid() - ? parsedTime.format('LL') - : update.releaseTime, - chapterNumber: update.chapterNumber - ? update.chapterNumber - : parseChapterNumber(update.novelName, update.name), - }; - }); - setIsLoading(false); - return result; - }; + let result: Update[] = await getDetailedUpdatesFromDb( + novelId, + onlyDownloadedChapters, + ); + result = result.map(update => { + const parsedTime = dayjs(update.releaseTime); + return { + ...update, + releaseTime: parsedTime.isValid() + ? parsedTime.format('LL') + : update.releaseTime, + chapterNumber: update.chapterNumber + ? update.chapterNumber + : parseChapterNumber(update.novelName, update.name), + }; + }); + setIsLoading(false); + return result; + }, + [], + ); - const getUpdates = () => { + const getUpdates = useCallback(async () => { setIsLoading(true); getUpdatedOverviewFromDb() .then(res => { @@ -75,15 +76,37 @@ export const useUpdates = () => { }) .catch((err: Error) => setError(err.message)) .finally(() => setIsLoading(false)); - }; + }, [lastUpdateTime, setLastUpdateTime]); - return { - isLoading, - updatesOverview, - getUpdates, - getDetailedUpdates, - lastUpdateTime, - showLastUpdateTime, - error, - }; + useFocusEffect( + useCallback(() => { + setIsLoading(true); + //? Push updates to the end of the stack to avoid lag + setTimeout(async () => { + await getUpdates(); + setIsLoading(false); + }, 0); + }, [getUpdates]), + ); + + return useMemo( + () => ({ + isLoading, + updatesOverview, + getUpdates, + getDetailedUpdates, + lastUpdateTime, + showLastUpdateTime, + error, + }), + [ + isLoading, + updatesOverview, + getUpdates, + getDetailedUpdates, + lastUpdateTime, + showLastUpdateTime, + error, + ], + ); }; diff --git a/src/navigators/Main.tsx b/src/navigators/Main.tsx index 40010967..ed08f5b2 100644 --- a/src/navigators/Main.tsx +++ b/src/navigators/Main.tsx @@ -38,6 +38,7 @@ import OnboardingScreen from '@screens/onboarding/OnboardingScreen'; import ServiceManager from '@services/ServiceManager'; import ReaderStack from './ReaderStack'; import { LibraryContextProvider } from '@components/Context/LibraryContext'; +import { UpdateContextProvider } from '@components/Context/UpdateContext'; const Stack = createNativeStackNavigator(); const MainNavigator = () => { @@ -105,24 +106,26 @@ const MainNavigator = () => { }} > - {isNewVersion && } - - - - - - - - - - - - - - + + {isNewVersion && } + + + + + + + + + + + + + + + ); diff --git a/src/screens/browse/BrowseScreen.tsx b/src/screens/browse/BrowseScreen.tsx index b1baab36..4f80881f 100644 --- a/src/screens/browse/BrowseScreen.tsx +++ b/src/screens/browse/BrowseScreen.tsx @@ -8,7 +8,8 @@ import { getString } from '@strings/translations'; import { EmptyView, SafeAreaView, SearchbarV2 } from '@components'; import { BrowseScreenProps } from '@navigators/types'; -import { AvailableTab, InstalledTab } from './components/BrowseTabs'; +import { AvailableTab } from './components/AvailableTab'; +import { InstalledTab } from './components/InstalledTab'; const routes = [ { key: 'installedRoute', title: getString('browseScreen.installed') }, diff --git a/src/screens/browse/components/AvailableTab.tsx b/src/screens/browse/components/AvailableTab.tsx new file mode 100644 index 00000000..b842dced --- /dev/null +++ b/src/screens/browse/components/AvailableTab.tsx @@ -0,0 +1,264 @@ +import React, { useCallback, useMemo, useState, memo } from 'react'; +import { View, Text, StyleSheet, RefreshControl } from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { usePlugins } from '@hooks/persisted'; +import { PluginItem } from '@plugins/types'; +import { coverPlaceholderColor } from '@theme/colors'; +import { ThemeColors } from '@theme/types'; +import { getString } from '@strings/translations'; +import { MoreStackScreenProps } from '@navigators/types'; +import { EmptyView, IconButtonV2 } from '@components'; +import { showToast } from '@utils/showToast'; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated'; +import { getLocaleLanguageName } from '@utils/constants/languages'; +import { FlashList, ListRenderItem } from '@shopify/flash-list'; +interface AvailableTabProps { + searchText: string; + theme: ThemeColors; +} +interface AvailablePluginCardProps { + plugin: PluginItem & { header: boolean }; + theme: ThemeColors; + installPlugin: (plugin: PluginItem) => Promise; +} + +const AvailablePluginCard = ({ + plugin, + theme, + installPlugin, +}: AvailablePluginCardProps) => { + const ratio = useSharedValue(1); + const imageStyles = useAnimatedStyle(() => ({ + height: ratio.value * 40, + })); + const viewStyles = useAnimatedStyle(() => ({ + height: ratio.value * 64, + paddingVertical: ratio.value * 12, + })); + const textStyles = useAnimatedStyle(() => ({ + lineHeight: ratio.value * 20, + })); + return ( + + {plugin.header ? ( + + {getLocaleLanguageName(plugin.lang)} + + ) : null} + + + + + + {plugin.name} + + + {`${getLocaleLanguageName(plugin.lang)} - ${plugin.version}`} + + + + { + ratio.value = withTiming(0, { duration: 500 }); + installPlugin(plugin) + .then(() => { + showToast( + getString('browseScreen.installedPlugin', { + name: plugin.name, + }), + ); + }) + .catch((error: Error) => { + showToast(error.message); + ratio.value = 1; + }); + }} + size={22} + theme={theme} + /> + + + ); +}; + +export const AvailableTab = memo(({ searchText, theme }: AvailableTabProps) => { + const navigation = useNavigation(); + + const [refreshing, setRefreshing] = useState(false); + const { filteredAvailablePlugins, refreshPlugins, installPlugin } = + usePlugins(); + + const searchedPlugins = useMemo(() => { + let res = filteredAvailablePlugins; + if (searchText) { + const lowerCaseSearchText = searchText.toLocaleLowerCase(); + res = filteredAvailablePlugins.filter( + plg => + plg.name.toLocaleLowerCase().includes(lowerCaseSearchText) || + plg.id.includes(lowerCaseSearchText), + ); + } + + return res + .sort((a, b) => a.lang.localeCompare(b.lang)) + .map((plg, i) => { + return { + ...plg, + header: i === 0 ? true : plg.lang !== res[i - 1].lang, + }; + }); + }, [searchText, filteredAvailablePlugins]); + + const renderItem: ListRenderItem = + useCallback( + ({ item }) => { + return ( + + ); + }, + [theme, installPlugin], + ); + + return ( + item.id + '_available'} + refreshControl={ + { + setRefreshing(true); + refreshPlugins() + .finally(() => setRefreshing(false)) + .catch(e => { + showToast(e); + }); + }} + colors={[theme.onPrimary]} + progressBackgroundColor={theme.primary} + /> + } + ListEmptyComponent={ + !filteredAvailablePlugins.length ? ( + + + navigation.navigate('MoreStack', { + screen: 'SettingsStack', + params: { + screen: 'RespositorySettings', + }, + }), + }, + ]} + theme={theme} + /> + + ) : ( + + + + ) + } + /> + ); +}); + +const styles = StyleSheet.create({ + margintTop100: { marginTop: 100 }, + addition: { + fontSize: 12, + lineHeight: 20, + }, + buttonGroup: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 8, + }, + container: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 12, + }, + details: { + marginLeft: 16, + }, + icon: { + backgroundColor: coverPlaceholderColor, + borderRadius: 4, + height: 40, + width: 40, + }, + listHeader: { + fontSize: 14, + fontWeight: '600', + paddingHorizontal: 16, + paddingVertical: 8, + }, + name: { + fontWeight: 'bold', + lineHeight: 20, + }, + row: { + flexDirection: 'row', + }, + center: { alignItems: 'center' }, + flex: { flex: 1 }, +}); diff --git a/src/screens/browse/components/BrowseTabs.tsx b/src/screens/browse/components/BrowseTabs.tsx deleted file mode 100644 index 205419c4..00000000 --- a/src/screens/browse/components/BrowseTabs.tsx +++ /dev/null @@ -1,532 +0,0 @@ -import React, { useCallback, useMemo, useState, memo } from 'react'; -import { - Pressable, - Image, - View, - Text, - StyleSheet, - RefreshControl, -} from 'react-native'; -import { useNavigation } from '@react-navigation/native'; -import { useBrowseSettings, usePlugins } from '@hooks/persisted'; -import { PluginItem } from '@plugins/types'; -import { coverPlaceholderColor } from '@theme/colors'; -import { ThemeColors } from '@theme/types'; -import { getString } from '@strings/translations'; -import { BrowseScreenProps, MoreStackScreenProps } from '@navigators/types'; -import { Button, EmptyView, IconButtonV2 } from '@components'; -import TrackerCard from '../discover/TrackerCard'; -import { showToast } from '@utils/showToast'; -import Animated, { - useAnimatedStyle, - useSharedValue, - withTiming, -} from 'react-native-reanimated'; -import { Portal } from 'react-native-paper'; -import SourceSettingsModal from './Modals/SourceSettings'; -import { useBoolean } from '@hooks'; -import { getPlugin } from '@plugins/pluginManager'; -import { getLocaleLanguageName } from '@utils/constants/languages'; -import Swipeable from 'react-native-gesture-handler/ReanimatedSwipeable'; -import { FlashList, ListRenderItem } from '@shopify/flash-list'; -interface AvailableTabProps { - searchText: string; - theme: ThemeColors; -} - -interface InstalledTabProps { - navigation: BrowseScreenProps['navigation']; - theme: ThemeColors; - searchText: string; -} - -export const InstalledTab = memo( - ({ navigation, theme, searchText }: InstalledTabProps) => { - const { - filteredInstalledPlugins, - lastUsedPlugin, - setLastUsedPlugin, - uninstallPlugin, - updatePlugin, - } = usePlugins(); - const { showMyAnimeList, showAniList } = useBrowseSettings(); - const settingsModal = useBoolean(); - const [selectedPluginId, setSelectedPluginId] = useState(''); - - const pluginSettings = selectedPluginId - ? getPlugin(selectedPluginId)?.pluginSettings - : undefined; - - const navigateToSource = useCallback( - (plugin: PluginItem, showLatestNovels?: boolean) => { - navigation.navigate('SourceScreen', { - pluginId: plugin.id, - pluginName: plugin.name, - site: plugin.site, - showLatestNovels, - }); - setLastUsedPlugin(plugin); - }, - [navigation, setLastUsedPlugin], - ); - - const searchedPlugins = useMemo(() => { - const sortedInstalledPlugins = filteredInstalledPlugins.sort( - (plgFirst, plgSecond) => plgFirst.name.localeCompare(plgSecond.name), - ); - if (searchText) { - const lowerCaseSearchText = searchText.toLocaleLowerCase(); - return sortedInstalledPlugins.filter( - plg => - plg.name.toLocaleLowerCase().includes(lowerCaseSearchText) || - plg.id.includes(lowerCaseSearchText), - ); - } else { - return sortedInstalledPlugins; - } - }, [searchText, filteredInstalledPlugins]); - - const renderItem: ListRenderItem = useCallback( - ({ item }) => { - return ( - { - return ( - - { - ref.close(); - navigation.navigate('WebviewScreen', { - name: item.name, - url: item.site, - pluginId: item.id, - }); - }} - theme={theme} - /> - - ); - }} - renderRightActions={(_progress, _dragX, ref) => ( - - { - ref.close(); - uninstallPlugin(item).then(() => - showToast( - getString('browseScreen.uninstalledPlugin', { - name: item.name, - }), - ), - ); - }} - theme={theme} - /> - - )} - > - navigateToSource(item)} - > - - - - - {item.name} - - - {`${item.lang} - ${item.version}`} - - - - - {item.hasSettings ? ( - { - setSelectedPluginId(item.id); - settingsModal.setTrue(); - }} - theme={theme} - /> - ) : null} - {item.hasUpdate || __DEV__ ? ( - { - updatePlugin(item) - .then(version => - showToast( - getString('browseScreen.updatedTo', { version }), - ), - ) - .catch((error: Error) => showToast(error.message)); - }} - theme={theme} - /> - ) : null} -