diff --git a/.prettierrc.js b/.prettierrc.js index d161a9d27..37c20c51f 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -7,4 +7,5 @@ module.exports = { trailingComma: 'all', arrowParens: 'avoid', quoteProps: 'preserve', + endOfLine: 'auto', // stop prettier from getting mad on windows }; diff --git a/src/hooks/common/useSearch.ts b/src/hooks/common/useSearch.ts index 8bc46e6ea..1b8278b6a 100644 --- a/src/hooks/common/useSearch.ts +++ b/src/hooks/common/useSearch.ts @@ -1,7 +1,7 @@ import { useIsFocused } from '@react-navigation/native'; import { useCallback, useEffect, useState } from 'react'; -const useSearch = (defaultSearchText?: string) => { +const useSearch = (defaultSearchText?: string, clearSearchOnUnfocus = true) => { const isFocused = useIsFocused(); const [searchText, setSearchText] = useState(defaultSearchText || ''); @@ -9,8 +9,10 @@ const useSearch = (defaultSearchText?: string) => { const clearSearchbar = useCallback(() => setSearchText(''), []); useEffect(() => { - if (!isFocused) { - clearSearchbar(); + if (clearSearchOnUnfocus) { + if (!isFocused) { + clearSearchbar(); + } } }, [isFocused, clearSearchbar]); diff --git a/src/hooks/persisted/useSettings.ts b/src/hooks/persisted/useSettings.ts index b3bff2b0c..565802519 100644 --- a/src/hooks/persisted/useSettings.ts +++ b/src/hooks/persisted/useSettings.ts @@ -28,6 +28,7 @@ export interface AppSettings { showUpdatesTab: boolean; showLabelsInNav: boolean; useFabForContinueReading: boolean; + disableLoadingAnimations: boolean; /** * Library settings @@ -56,6 +57,7 @@ export interface AppSettings { export interface BrowseSettings { showMyAnimeList: boolean; showAniList: boolean; + globalSearchConcurrency?: number; } export interface LibrarySettings { @@ -125,6 +127,7 @@ const initialAppSettings: AppSettings = { showUpdatesTab: true, showLabelsInNav: true, useFabForContinueReading: false, + disableLoadingAnimations: false, /** * Library settings @@ -153,6 +156,7 @@ const initialAppSettings: AppSettings = { const initialBrowseSettings: BrowseSettings = { showMyAnimeList: true, showAniList: true, + globalSearchConcurrency: 1, }; export const initialChapterGeneralSettings: ChapterGeneralSettings = { diff --git a/src/navigators/Main.tsx b/src/navigators/Main.tsx index c65277884..67ebbbc65 100644 --- a/src/navigators/Main.tsx +++ b/src/navigators/Main.tsx @@ -31,7 +31,7 @@ import MigrateNovel from '../screens/browse/migration/MigrationNovels'; import MalTopNovels from '../screens/browse/discover/MalTopNovels'; import AniListTopNovels from '../screens/browse/discover/AniListTopNovels'; import NewUpdateDialog from '../components/NewUpdateDialog'; -import BrowseSettings from '../screens/browse/BrowseSettings'; +import BrowseSettings from '../screens/browse/settings/BrowseSettings'; import WebviewScreen from '@screens/WebviewScreen/WebviewScreen'; import { RootStackParamList } from './types'; import Color from 'color'; diff --git a/src/screens/Categories/components/CategorySkeletonLoading.tsx b/src/screens/Categories/components/CategorySkeletonLoading.tsx index a190990a2..a45efb18a 100644 --- a/src/screens/Categories/components/CategorySkeletonLoading.tsx +++ b/src/screens/Categories/components/CategorySkeletonLoading.tsx @@ -4,6 +4,7 @@ import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'; import { LinearGradient } from 'expo-linear-gradient'; import { ThemeColors } from '@theme/types'; import getLoadingColors from '@utils/getLoadingColors'; +import { useAppSettings } from '@hooks/persisted/index'; interface Props { width: number; @@ -12,6 +13,7 @@ interface Props { } const CategorySkeletonLoading: React.FC = ({ height, width, theme }) => { + const { disableLoadingAnimations } = useAppSettings(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const [highlightColor, backgroundColor] = getLoadingColors(theme); @@ -24,6 +26,7 @@ const CategorySkeletonLoading: React.FC = ({ height, width, theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={height} width={width} + stopAutoRun={disableLoadingAnimations} /> ); diff --git a/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx b/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx index 97da23772..020ca3022 100644 --- a/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx +++ b/src/screens/GlobalSearchScreen/GlobalSearchScreen.tsx @@ -23,6 +23,7 @@ const GlobalSearchScreen = (props: Props) => { const theme = useTheme(); const { searchText, setSearchText, clearSearchbar } = useSearch( props?.route?.params?.searchText, + false, ); const onChangeText = (text: string) => setSearchText(text); const onSubmitEditing = () => globalSearch(searchText); diff --git a/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx index 2c7667f27..ac47dbef6 100644 --- a/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx +++ b/src/screens/GlobalSearchScreen/components/GlobalSearchResultsList.tsx @@ -13,8 +13,9 @@ import { GlobalSearchResult } from '../hooks/useGlobalSearch'; import GlobalSearchNovelItem from './GlobalSearchNovelItem'; import { useLibraryNovels } from '@screens/library/hooks/useLibrary'; import { LibraryNovelInfo } from '@database/types'; -import GlobalSearchSkeletonLoading from '@screens/browse/loadingAnimation/GlobalSearchSkeletonLoading'; import { switchNovelToLibrary } from '@database/queries/NovelQueries'; +import GlobalSearchSkeletonLoading from '@screens/browse/loadingAnimation/GlobalSearchSkeletonLoading'; +import { interpolateColor } from 'react-native-reanimated'; interface GlobalSearchResultsListProps { searchResults: GlobalSearchResult[]; @@ -25,12 +26,27 @@ const GlobalSearchResultsList: React.FC = ({ searchResults, ListEmptyComponent, }) => { - const theme = useTheme(); - const navigation = useNavigation>(); const keyExtractor = useCallback( (item: GlobalSearchResult) => item.plugin.id, [], ); + + return ( + + keyExtractor={keyExtractor} + data={searchResults} + contentContainerStyle={styles.resultList} + renderItem={({ item }) => } + ListEmptyComponent={ListEmptyComponent} + /> + ); +}; + +const GlobalSearchSourceResults: React.FC<{ item: GlobalSearchResult }> = ({ + item, +}) => { + const theme = useTheme(); + const navigation = useNavigation>(); const { library, setLibrary } = useLibraryNovels(); const novelInLibrary = (pluginId: string, novelPath: string) => @@ -38,7 +54,12 @@ const GlobalSearchResultsList: React.FC = ({ novel => novel.pluginId === pluginId && novel.path === novelPath, ); - const errorColor = useMemo(() => (theme.isDark ? '#B3261E' : '#F2B8B5'), []); + const errorColor = theme.isDark ? '#B3261E' : '#F2B8B5'; + const noResultsColor = interpolateColor( + 0.8, + [0, 1], + ['transparent', theme.onSurfaceVariant], + ); const navigateToNovel = useCallback( (item: { name: string; path: string; pluginId: string }) => @@ -46,109 +67,97 @@ const GlobalSearchResultsList: React.FC = ({ [], ); - return ( - - keyExtractor={keyExtractor} - data={searchResults} - contentContainerStyle={styles.resultList} - renderItem={({ item }) => ( - <> - - - navigation.navigate('SourceScreen', { - pluginId: item.plugin.id, - pluginName: item.plugin.name, - site: item.plugin.site, - }) - } - > - - - {item.plugin.name} - - - {item.plugin.lang} - - - - - {item.isLoading ? ( - - ) : item.error ? ( - - {item.error} + return useMemo( + () => ( + <> + + + navigation.navigate('SourceScreen', { + pluginId: item.plugin.id, + pluginName: item.plugin.name, + site: item.plugin.site, + }) + } + > + + + {item.plugin.name} - ) : ( - - item.plugin.id + '_' + novelItem.path - } - data={item.novels} - ListEmptyComponent={ - - {getString('sourceScreen.noResultsFound')} - - } - renderItem={({ item: novelItem }) => { - const inLibrary = novelInLibrary( - item.plugin.id, - novelItem.path, - ); + + {item.plugin.lang} + + + + + {item.isLoading ? ( + + ) : item.error ? ( + + {item.error} + + ) : ( + item.plugin.id + '_' + novelItem.path} + data={item.novels} + ListEmptyComponent={ + + {getString('sourceScreen.noResultsFound')} + + } + renderItem={({ item: novelItem }) => { + const inLibrary = novelInLibrary( + item.plugin.id, + novelItem.path, + ); - return ( - { - setLibrary(prevValues => { - if (inLibrary) { - return [ - ...prevValues.filter( - novel => novel.path !== novelItem.path, - ), - ]; - } else { - return [ - ...prevValues, - { - path: novelItem.path, - } as LibraryNovelInfo, - ]; - } - }); - switchNovelToLibrary(novelItem.path, item.plugin.id); - }} - /> - ); - }} - /> - )} - - - )} - ListEmptyComponent={ListEmptyComponent} - /> + return ( + { + setLibrary(prevValues => { + if (inLibrary) { + return [ + ...prevValues.filter( + novel => novel.path !== novelItem.path, + ), + ]; + } else { + return [ + ...prevValues, + { + path: novelItem.path, + } as LibraryNovelInfo, + ]; + } + }); + switchNovelToLibrary(novelItem.path, item.plugin.id); + }} + /> + ); + }} + /> + )} + + + ), + [item.isLoading], ); }; diff --git a/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts b/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts index c445d66d6..1ab3172ae 100644 --- a/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts +++ b/src/screens/GlobalSearchScreen/hooks/useGlobalSearch.ts @@ -1,8 +1,9 @@ -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { NovelItem, PluginItem } from '@plugins/types'; import { getPlugin } from '@plugins/pluginManager'; -import { usePlugins } from '@hooks/persisted'; +import { useBrowseSettings, usePlugins } from '@hooks/persisted'; +import { useFocusEffect } from '@react-navigation/native'; interface Props { defaultSearchText?: string; @@ -16,14 +17,35 @@ export interface GlobalSearchResult { } export const useGlobalSearch = ({ defaultSearchText }: Props) => { - const isMounted = useRef(true); + const isMounted = useRef(true); //if user closes the search screen, cancel the search + const isFocused = useRef(true); //if the user opens a sub-screen (e.g. novel screen), pause the search + const lastSearch = useRef(''); //if the user changes search, cancel running searches + useEffect( + () => () => { + isMounted.current = false; + }, + [], + ); + useFocusEffect( + useCallback(() => { + isFocused.current = true; + + return () => (isFocused.current = false); + }, []), + ); const { filteredInstalledPlugins } = usePlugins(); const [searchResults, setSearchResults] = useState([]); const [progress, setProgress] = useState(0); + const { globalSearchConcurrency = 1 } = useBrowseSettings(); + const globalSearch = (searchText: string) => { + if (lastSearch.current === searchText) { + return; + } + lastSearch.current = searchText; const defaultResult: GlobalSearchResult[] = filteredInstalledPlugins.map( plugin => ({ isLoading: true, @@ -33,46 +55,33 @@ export const useGlobalSearch = ({ defaultSearchText }: Props) => { }), ); - setSearchResults(defaultResult); + setSearchResults(defaultResult.sort(novelResultSorter)); + setProgress(0); - filteredInstalledPlugins.forEach(async _plugin => { - if (isMounted.current) { - try { - const plugin = getPlugin(_plugin.id); - if (!plugin) { - throw new Error(`Unknown plugin: ${_plugin.id}`); - } - const res = await plugin.searchNovels(searchText, 1); + let running = 0; + + async function searchInPlugin(_plugin: PluginItem) { + try { + const plugin = getPlugin(_plugin.id); + if (!plugin) { + throw new Error(`Unknown plugin: ${_plugin.id}`); + } + const res = await plugin.searchNovels(searchText, 1); - setSearchResults(prevState => - prevState.map(prevResult => + setSearchResults(prevState => + prevState + .map(prevResult => prevResult.plugin.id === plugin.id ? { ...prevResult, novels: res, isLoading: false } : { ...prevResult }, - ), - ); - - setSearchResults(prevState => - prevState.sort( - ( - { novels: a, plugin: { name: aName } }, - { novels: b, plugin: { name: bName } }, - ) => { - if (!a.length) { - return 1; - } - if (!b.length) { - return -1; - } - - return aName.localeCompare(bName); - }, - ), - ); - } catch (error: any) { - const errorMessage = error?.message || String(error); - setSearchResults(prevState => - prevState.map(prevResult => + ) + .sort(novelResultSorter), + ); + } catch (error: any) { + const errorMessage = error?.message || String(error); + setSearchResults(prevState => + prevState + .map(prevResult => prevResult.plugin.id === _plugin.id ? { ...prevResult, @@ -81,15 +90,57 @@ export const useGlobalSearch = ({ defaultSearchText }: Props) => { error: errorMessage, } : { ...prevResult }, - ), - ); - } finally { - setProgress( - prevState => prevState + 1 / filteredInstalledPlugins.length, - ); + ) + .sort(novelResultSorter), + ); + } + } + + //Sort so we load the plugins results in the same order as they show on the list + let filteredSortedInstalledPlugins = [...filteredInstalledPlugins].sort( + (a, b) => a.name.localeCompare(b.name), + ); + + (async () => { + if (globalSearchConcurrency > 1) { + for (let _plugin of filteredSortedInstalledPlugins) { + while (running >= globalSearchConcurrency || !isFocused.current) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + if (!isMounted.current || lastSearch.current !== searchText) { + break; + } + running++; + searchInPlugin(_plugin) + .then(() => { + running--; + if (lastSearch.current === searchText) { + setProgress( + prevState => prevState + 1 / filteredInstalledPlugins.length, + ); + } + }) + .catch(() => { + running--; + }); + } + } else { + for (let _plugin of filteredSortedInstalledPlugins) { + if (!isMounted.current || lastSearch.current !== searchText) { + break; + } + while (!isFocused.current) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + await searchInPlugin(_plugin); + if (lastSearch.current === searchText) { + setProgress( + prevState => prevState + 1 / filteredInstalledPlugins.length, + ); + } } } - }); + })(); }; useEffect(() => { @@ -100,3 +151,20 @@ export const useGlobalSearch = ({ defaultSearchText }: Props) => { return { searchResults, globalSearch, progress }; }; + +function novelResultSorter( + { novels: a, plugin: { name: aName } }: GlobalSearchResult, + { novels: b, plugin: { name: bName } }: GlobalSearchResult, +) { + if (!a.length && !b.length) { + return aName.localeCompare(bName); + } + if (!a.length) { + return 1; + } + if (!b.length) { + return -1; + } + + return aName.localeCompare(bName); +} diff --git a/src/screens/browse/loadingAnimation/LoadingNovel.tsx b/src/screens/browse/loadingAnimation/LoadingNovel.tsx index 2bc87bc69..86ebb1f60 100644 --- a/src/screens/browse/loadingAnimation/LoadingNovel.tsx +++ b/src/screens/browse/loadingAnimation/LoadingNovel.tsx @@ -3,6 +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'; interface Props { backgroundColor: string; @@ -19,6 +20,7 @@ const LoadingNovel: React.FC = ({ pictureWidth, displayMode, }) => { + const { disableLoadingAnimations } = useAppSettings(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); let randomNumber = Math.random(); randomNumber < 0.1 ? (randomNumber = 0) : null; @@ -35,6 +37,7 @@ const LoadingNovel: React.FC = ({ shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={pictureHeight} width={pictureWidth} + stopAutoRun={disableLoadingAnimations} /> {displayMode === 2 || displayMode === 0 ? null : ( <> @@ -43,12 +46,14 @@ const LoadingNovel: React.FC = ({ shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={16} width={pictureWidth} + stopAutoRun={disableLoadingAnimations} /> )} @@ -64,6 +69,7 @@ const LoadingNovel: React.FC = ({ shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={40} width={40} + stopAutoRun={disableLoadingAnimations} /> = ({ shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={18} width={textWidth} + stopAutoRun={disableLoadingAnimations} /> ); diff --git a/src/screens/browse/loadingAnimation/MalLoading.tsx b/src/screens/browse/loadingAnimation/MalLoading.tsx index f709f049a..9e17099f4 100644 --- a/src/screens/browse/loadingAnimation/MalLoading.tsx +++ b/src/screens/browse/loadingAnimation/MalLoading.tsx @@ -5,12 +5,14 @@ import { LinearGradient } from 'expo-linear-gradient'; import { ThemeColors } from '@theme/types'; import getLoadingColors from '@utils/getLoadingColors'; +import { useAppSettings } from '@hooks/persisted/index'; interface Props { theme: ThemeColors; } const MalLoading: React.FC = ({ theme }) => { + const { disableLoadingAnimations } = useAppSettings(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const styles = createStyleSheet(theme); @@ -25,6 +27,7 @@ const MalLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={120 + Math.random() * 28} width={100} + stopAutoRun={disableLoadingAnimations} /> = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={16} width={Dimensions.get('window').width - 140} + stopAutoRun={disableLoadingAnimations} /> diff --git a/src/screens/browse/loadingAnimation/TrackerLoading.tsx b/src/screens/browse/loadingAnimation/TrackerLoading.tsx index f709f049a..9e17099f4 100644 --- a/src/screens/browse/loadingAnimation/TrackerLoading.tsx +++ b/src/screens/browse/loadingAnimation/TrackerLoading.tsx @@ -5,12 +5,14 @@ import { LinearGradient } from 'expo-linear-gradient'; import { ThemeColors } from '@theme/types'; import getLoadingColors from '@utils/getLoadingColors'; +import { useAppSettings } from '@hooks/persisted/index'; interface Props { theme: ThemeColors; } const MalLoading: React.FC = ({ theme }) => { + const { disableLoadingAnimations } = useAppSettings(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const styles = createStyleSheet(theme); @@ -25,6 +27,7 @@ const MalLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={120 + Math.random() * 28} width={100} + stopAutoRun={disableLoadingAnimations} /> = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={16} width={Dimensions.get('window').width - 140} + stopAutoRun={disableLoadingAnimations} /> diff --git a/src/screens/browse/BrowseSettings.tsx b/src/screens/browse/settings/BrowseSettings.tsx similarity index 71% rename from src/screens/browse/BrowseSettings.tsx rename to src/screens/browse/settings/BrowseSettings.tsx index 3e14b1dd6..774efa2fd 100644 --- a/src/screens/browse/BrowseSettings.tsx +++ b/src/screens/browse/settings/BrowseSettings.tsx @@ -2,18 +2,30 @@ import { FlatList, StyleSheet } from 'react-native'; import React from 'react'; import { Appbar, List, SwitchItem } from '@components'; -import { useBrowseSettings, usePlugins, useTheme } from '@hooks/persisted'; +import { + useBrowseSettings, + usePlugins, + useTheme, +} from '@hooks/persisted/index'; import { getString } from '@strings/translations'; import { languages } from '@utils/constants/languages'; -import { BrowseSettingsScreenProp } from '@navigators/types'; +import { BrowseSettingsScreenProp } from '@navigators/types/index'; +import { useBoolean } from '@hooks'; +import ConcurrentSearchesModal from '@screens/browse/settings/modals/ConcurrentSearchesModal'; const BrowseSettings = ({ navigation }: BrowseSettingsScreenProp) => { const theme = useTheme(); const { goBack } = navigation; const { languagesFilter, toggleLanguageFilter } = usePlugins(); - const { showMyAnimeList, showAniList, setBrowseSettings } = - useBrowseSettings(); + const { + showMyAnimeList, + showAniList, + globalSearchConcurrency, + setBrowseSettings, + } = useBrowseSettings(); + + const globalSearchConcurrencyModal = useBoolean(); return ( <> @@ -22,6 +34,12 @@ const BrowseSettings = ({ navigation }: BrowseSettingsScreenProp) => { handleGoBack={goBack} theme={theme} /> + { {getString('browseScreen.globalSearch')} - diff --git a/src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx b/src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx new file mode 100644 index 000000000..f06ec84f6 --- /dev/null +++ b/src/screens/browse/settings/modals/ConcurrentSearchesModal.tsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { Text, StyleSheet } from 'react-native'; + +import { Portal, Modal, overlay } 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'; + +interface DisplayModeModalProps { + globalSearchConcurrency: number; + modalVisible: boolean; + hideModal: () => void; + theme: ThemeColors; +} + +const ConcurrentSearchesModal: React.FC = ({ + theme, + globalSearchConcurrency, + hideModal, + modalVisible, +}) => { + const { setBrowseSettings } = useBrowseSettings(); + + return ( + + + + {getString('browseSettingsScreen.concurrentSearches')} + + {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(concurrency => ( + + setBrowseSettings({ globalSearchConcurrency: concurrency }) + } + label={concurrency.toString()} + theme={theme} + /> + ))} + + + ); +}; + +export default ConcurrentSearchesModal; + +const styles = StyleSheet.create({ + containerStyle: { + paddingVertical: 20, + margin: 20, + borderRadius: 28, + }, + modalHeader: { + paddingHorizontal: 24, + fontSize: 24, + marginBottom: 10, + }, +}); diff --git a/src/screens/history/components/HistorySkeletonLoading.tsx b/src/screens/history/components/HistorySkeletonLoading.tsx index 8fd4bcd4b..3846053de 100644 --- a/src/screens/history/components/HistorySkeletonLoading.tsx +++ b/src/screens/history/components/HistorySkeletonLoading.tsx @@ -4,12 +4,14 @@ import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'; import { LinearGradient } from 'expo-linear-gradient'; import { ThemeColors } from '@theme/types'; import getLoadingColors from '@utils/getLoadingColors'; +import { useAppSettings } from '@hooks/persisted/index'; interface Props { theme: ThemeColors; } const HistorySkeletonLoading: React.FC = ({ theme }) => { + const { disableLoadingAnimations } = useAppSettings(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const [highlightColor, backgroundColor] = getLoadingColors(theme); @@ -23,6 +25,7 @@ const HistorySkeletonLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={19.3} width={Math.random() * 40 + 50} + stopAutoRun={disableLoadingAnimations} /> ) : null} @@ -31,6 +34,7 @@ const HistorySkeletonLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={80} width={56} + stopAutoRun={disableLoadingAnimations} /> = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={16} width={208.7} + stopAutoRun={disableLoadingAnimations} /> @@ -52,6 +58,7 @@ const HistorySkeletonLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={24} width={24} + stopAutoRun={disableLoadingAnimations} /> @@ -60,6 +67,7 @@ const HistorySkeletonLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={24} width={24} + stopAutoRun={disableLoadingAnimations} /> diff --git a/src/screens/novel/components/LoadingAnimation/NovelScreenLoading.tsx b/src/screens/novel/components/LoadingAnimation/NovelScreenLoading.tsx index 684abe27e..a212e4d28 100644 --- a/src/screens/novel/components/LoadingAnimation/NovelScreenLoading.tsx +++ b/src/screens/novel/components/LoadingAnimation/NovelScreenLoading.tsx @@ -4,12 +4,14 @@ import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'; import { LinearGradient } from 'expo-linear-gradient'; import { ThemeColors } from '@theme/types'; import getLoadingColors from '@utils/getLoadingColors'; +import { useAppSettings } from '@hooks/persisted/index'; interface Props { theme: ThemeColors; } const NovelScreenLoading: React.FC = ({ theme }) => { + const { disableLoadingAnimations } = useAppSettings(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const styles = createStyleSheet(); @@ -23,6 +25,7 @@ const NovelScreenLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={150} width={100} + stopAutoRun={disableLoadingAnimations} /> = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={25} width={240} + stopAutoRun={disableLoadingAnimations} /> @@ -57,18 +63,21 @@ const NovelScreenLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={56} width={90} + stopAutoRun={disableLoadingAnimations} /> @@ -77,12 +86,14 @@ const NovelScreenLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={16} width={350} + stopAutoRun={disableLoadingAnimations} /> @@ -91,12 +102,14 @@ const NovelScreenLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={32} width={60} + stopAutoRun={disableLoadingAnimations} /> @@ -111,12 +124,14 @@ const NovelScreenLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={20} width={350} + stopAutoRun={disableLoadingAnimations} /> ); @@ -131,6 +146,7 @@ const NovelScreenLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={30} width={350} + stopAutoRun={disableLoadingAnimations} /> {items.map(renderLoadingChapter)} diff --git a/src/screens/reader/components/SkeletonLines.tsx b/src/screens/reader/components/SkeletonLines.tsx index c6bd7aa52..dbbc528ed 100644 --- a/src/screens/reader/components/SkeletonLines.tsx +++ b/src/screens/reader/components/SkeletonLines.tsx @@ -2,6 +2,7 @@ import React, { memo } from 'react'; import { View, Dimensions, StyleSheet } from 'react-native'; import { createShimmerPlaceholder } from 'react-native-shimmer-placeholder'; import { LinearGradient } from 'expo-linear-gradient'; +import { useAppSettings } from '@hooks/persisted/index'; const SkeletonLines = ({ width, @@ -22,6 +23,7 @@ const SkeletonLines = ({ color?: string; highlightColor?: string; }) => { + const { disableLoadingAnimations } = useAppSettings(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const styles = createStyleSheet( containerWidth, @@ -77,6 +79,7 @@ const SkeletonLines = ({ : randomNumber * skeletonWidth } height={skeletonHeight} + stopAutoRun={disableLoadingAnimations} /> ); } @@ -89,6 +92,7 @@ const SkeletonLines = ({ shimmerColors={[color, highlightColor, color]} width={skeletonWidth} height={skeletonHeight} + stopAutoRun={disableLoadingAnimations} /> ); } else { diff --git a/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx b/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx index bdf2dbc51..2679f7431 100644 --- a/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx +++ b/src/screens/settings/SettingsGeneralScreen/SettingsGeneralScreen.tsx @@ -50,6 +50,7 @@ const GenralSettings: React.FC = ({ navigation }) => { ['lastUpdatedAt', 'libraryScreen.bottomSheet.sortOrders.lastUpdated'], ]); const { + disableLoadingAnimations, updateLibraryOnLaunch, downloadNewChapters, onlyUpdateOngoingNovels, @@ -232,6 +233,19 @@ const GenralSettings: React.FC = ({ navigation }) => { } theme={theme} /> + + setAppSettings({ + disableLoadingAnimations: !disableLoadingAnimations, + }) + } + theme={theme} + /> = ({ theme }) => { + const { disableLoadingAnimations } = useAppSettings(); const ShimmerPlaceHolder = createShimmerPlaceholder(LinearGradient); const [highlightColor, backgroundColor] = getLoadingColors(theme); @@ -22,6 +24,7 @@ const UpdatesSkeletonLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={42} width={42} + stopAutoRun={disableLoadingAnimations} /> = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={16} width={257.5} + stopAutoRun={disableLoadingAnimations} /> @@ -43,6 +48,7 @@ const UpdatesSkeletonLoading: React.FC = ({ theme }) => { shimmerColors={[backgroundColor, highlightColor, backgroundColor]} height={25} width={25} + stopAutoRun={disableLoadingAnimations} /> diff --git a/strings/languages/en/strings.json b/strings/languages/en/strings.json index f67a336f8..2a38ff84b 100644 --- a/strings/languages/en/strings.json +++ b/strings/languages/en/strings.json @@ -143,6 +143,7 @@ }, "browseSettings": "Browse Settings", "browseSettingsScreen": { + "concurrentSearches": "Concurrent Source Searches", "languages": "Languages", "onlyShowPinnedSources": "Only show pinned sources", "searchAllSources": "Search all sources", @@ -254,6 +255,8 @@ "bySource": "By source", "chapterSort": "Default chapter sort", "desc": "(Descending)", + "disableLoadingAnimations": "Disable loading animations", + "disableLoadingAnimationsDesc": "May improve performance on slower devices", "disableHapticFeedback": "Disable haptic feedback", "displayMode": "Display Mode", "downloadNewChapters": "Download new chapters", diff --git a/strings/types/index.ts b/strings/types/index.ts index 99a8dde48..8f4f51f8b 100644 --- a/strings/types/index.ts +++ b/strings/types/index.ts @@ -126,6 +126,7 @@ export interface StringMap { 'browseScreen.updateFailed': 'string'; 'browseScreen.updatedTo': 'string'; 'browseSettings': 'string'; + 'browseSettingsScreen.concurrentSearches': 'string'; 'browseSettingsScreen.languages': 'string'; 'browseSettingsScreen.onlyShowPinnedSources': 'string'; 'browseSettingsScreen.searchAllSources': 'string'; @@ -237,6 +238,8 @@ export interface StringMap { 'generalSettingsScreen.refreshMetadata': 'string'; 'generalSettingsScreen.refreshMetadataDescription': 'string'; 'generalSettingsScreen.sortOrder': 'string'; + 'generalSettingsScreen.disableLoadingAnimations': 'string'; + 'generalSettingsScreen.disableLoadingAnimationsDesc': 'string'; 'generalSettingsScreen.updateLibrary': 'string'; 'generalSettingsScreen.updateLibraryDesc': 'string'; 'generalSettingsScreen.updateOngoing': 'string';