chore: fix all lint warnings (#1510)

* fix all lint warnings

* fix types
This commit is contained in:
Patrick Loser
2025-05-20 00:02:24 +02:00
committed by GitHub
parent 21611de3aa
commit 5e0f375f7f
59 changed files with 1090 additions and 831 deletions
+2 -1
View File
@@ -68,7 +68,7 @@ interface SortItemProps {
export const SortItem = ({ label, status, onPress, theme }: SortItemProps) => (
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={[styles.pressable, { paddingVertical: 16, paddingLeft: 64 }]}
style={[styles.pressable, styles.sortItem]}
onPress={onPress}
>
{status ? (
@@ -98,4 +98,5 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
paddingVertical: 6,
},
sortItem: { paddingVertical: 16, paddingLeft: 64 },
});
@@ -79,24 +79,14 @@ const ColorPickerModal: React.FC<ColorPickerModalProps> = ({
</Text>
{showAccentColors ? (
<FlatList
contentContainerStyle={{ marginBottom: 8 }}
contentContainerStyle={styles.marginBottom}
data={accentColors}
numColumns={4}
keyExtractor={item => item}
renderItem={({ item }) => (
<View
style={{
borderRadius: 4,
overflow: 'hidden',
backgroundColor: item,
flex: 1 / 4,
height: 40,
marginHorizontal: 4,
marginVertical: 4,
}}
>
<View style={[styles.item, { backgroundColor: item }]}>
<Pressable
style={{ flex: 1 }}
style={styles.flex}
android_ripple={{
color: 'rgba(0,0,0,0.12)',
}}
@@ -138,4 +128,15 @@ const styles = StyleSheet.create({
fontSize: 24,
marginBottom: 16,
},
item: {
borderRadius: 4,
overflow: 'hidden',
flex: 1 / 4,
height: 40,
marginHorizontal: 4,
marginVertical: 4,
},
flex: { flex: 1 },
marginBottom: { marginBottom: 8 },
});
+54 -32
View File
@@ -1,10 +1,27 @@
import React from 'react';
import { Pressable, View } from 'react-native';
import { Pressable, View, StyleSheet } from 'react-native';
import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
import { ThemeColors } from '../../theme/types';
import Color from 'color';
import { MaterialDesignIconName } from '@type/icon';
// --- Dynamic style helpers ---
const getToggleButtonPressableStyle = (
selected: boolean,
theme: ThemeColors,
) => ({
backgroundColor: selected
? Color(theme.primary).alpha(0.12).string()
: 'transparent',
});
const getToggleColorButtonPressableStyle = (backgroundColor: string) => ({
backgroundColor,
});
// --- Components ---
interface ToggleButtonProps {
icon: MaterialDesignIconName;
selected: boolean;
@@ -20,23 +37,13 @@ export const ToggleButton: React.FC<ToggleButtonProps> = ({
color,
onPress,
}) => (
<View
style={{
borderRadius: 6,
overflow: 'hidden',
marginHorizontal: 6,
}}
>
<View style={styles.toggleButtonContainer}>
<Pressable
android_ripple={{ color: theme.rippleColor }}
style={{
padding: 8,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: selected
? Color(theme.primary).alpha(0.12).string()
: 'transparent',
}}
style={[
styles.toggleButtonPressable,
getToggleButtonPressableStyle(selected, theme),
]}
onPress={onPress}
>
<MaterialCommunityIcons
@@ -61,24 +68,13 @@ export const ToggleColorButton: React.FC<ToggleColorButtonProps> = ({
textColor,
onPress,
}) => (
<View
style={{
borderRadius: 50,
overflow: 'hidden',
marginHorizontal: 6,
height: 44,
width: 44,
}}
>
<View style={styles.toggleColorButtonContainer}>
<Pressable
android_ripple={{ color: textColor }}
style={{
flex: 1,
padding: 10,
backgroundColor: backgroundColor,
alignItems: 'center',
justifyContent: 'center',
}}
style={[
styles.toggleColorButtonPressable,
getToggleColorButtonPressableStyle(backgroundColor),
]}
onPress={onPress}
>
<MaterialCommunityIcons
@@ -89,3 +85,29 @@ export const ToggleColorButton: React.FC<ToggleColorButtonProps> = ({
</Pressable>
</View>
);
const styles = StyleSheet.create({
toggleButtonContainer: {
borderRadius: 6,
overflow: 'hidden',
marginHorizontal: 6,
},
toggleButtonPressable: {
padding: 8,
alignItems: 'center',
justifyContent: 'center',
},
toggleColorButtonContainer: {
borderRadius: 50,
overflow: 'hidden',
marginHorizontal: 6,
height: 44,
width: 44,
},
toggleColorButtonPressable: {
flex: 1,
padding: 10,
alignItems: 'center',
justifyContent: 'center',
},
});
+42 -31
View File
@@ -3,8 +3,6 @@ import React from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { IconButton } from 'react-native-paper';
// const icons = ['(・o・;)', 'Σ(ಠ_ಠ)', 'ಥ_ಥ', '(˘・_・˘)', '(; ̄Д ̄)', '(・Д・。'];
interface ErrorAction {
name: string;
icon: string;
@@ -17,45 +15,37 @@ interface ErrorViewProps {
theme: ThemeColors;
}
// Dynamic style helpers
const getOutlineColor = (theme: ThemeColors) => ({ color: theme.outline });
const getRipple = (theme: ThemeColors) => ({
color: theme.rippleColor,
borderless: false,
});
const getActionTextColor = (theme: ThemeColors) => ({ color: theme.outline });
export const ErrorView = ({ errorName, actions, theme }: ErrorViewProps) => (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={[styles.emptyViewIcon, { color: theme.outline }]}>
<View style={styles.emptyViewContainer}>
<Text style={[styles.emptyViewIcon, getOutlineColor(theme)]}>
{/* {icons[Math.floor(Math.random() * 5)]} */}
ಥ_ಥ
</Text>
<Text style={[styles.emptyViewText, { color: theme.outline }]}>
<Text style={[styles.emptyViewText, getOutlineColor(theme)]}>
{errorName}
</Text>
<View style={{ flexDirection: 'row' }}>
<View style={styles.actionsRow}>
{actions.map(action => (
<View
style={{
borderRadius: 4,
overflow: 'hidden',
margin: 16,
}}
>
<View key={action.name} style={styles.actionContainer}>
<Pressable
android_ripple={{
color: theme.rippleColor,
borderless: false,
}}
android_ripple={getRipple(theme)}
onPress={action.onPress}
style={{
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 8,
paddingHorizontal: 20,
minWidth: 100,
}}
style={styles.actionPressable}
>
<IconButton icon={action.icon} size={24} style={{ margin: 0 }} />
<Text
style={{
fontSize: 12,
color: theme.outline,
}}
>
<IconButton
icon={action.icon}
size={24}
style={styles.iconButton}
/>
<Text style={[styles.actionText, getActionTextColor(theme)]}>
{action.name}
</Text>
</Pressable>
@@ -80,4 +70,25 @@ const styles = StyleSheet.create({
paddingHorizontal: 30,
textAlign: 'center',
},
actionsRow: {
flexDirection: 'row',
},
actionContainer: {
borderRadius: 4,
overflow: 'hidden',
margin: 16,
},
actionPressable: {
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 8,
paddingHorizontal: 20,
minWidth: 100,
},
iconButton: {
margin: 0,
},
actionText: {
fontSize: 12,
},
});
+71 -53
View File
@@ -1,4 +1,4 @@
import React, { ReactNode } from 'react';
import React, { ReactNode, useCallback } from 'react';
import {
Pressable,
StyleProp,
@@ -46,45 +46,51 @@ const Item: React.FC<ListItemProps> = ({
theme,
disabled,
right,
}) => (
<PaperList.Item
title={title}
titleStyle={{ color: disabled ? theme.onSurfaceDisabled : theme.onSurface }}
description={description}
descriptionStyle={[
styles.description,
{
color: disabled ? theme.onSurfaceDisabled : theme.onSurfaceVariant,
},
]}
left={
icon
? () => (
<PaperList.Icon
color={theme.primary}
icon={icon}
style={styles.iconCtn}
/>
)
: undefined
}) => {
const left = useCallback(() => {
if (icon) {
return (
<PaperList.Icon
color={theme.primary}
icon={icon}
style={styles.iconCtn}
/>
);
}
right={
right
? () => (
<PaperList.Icon
color={theme.primary}
icon={right}
style={styles.iconCtn}
/>
)
: undefined
}, [icon, theme.primary]);
const rightIcon = useCallback(() => {
if (right) {
return (
<PaperList.Icon
color={theme.primary}
icon={right}
style={styles.iconCtn}
/>
);
}
disabled={disabled}
onPress={onPress}
rippleColor={theme.rippleColor}
style={styles.listItemCtn}
/>
);
}, [right, theme.primary]);
return (
<PaperList.Item
title={title}
titleStyle={{
color: disabled ? theme.onSurfaceDisabled : theme.onSurface,
}}
description={description}
descriptionStyle={[
styles.description,
{
color: disabled ? theme.onSurfaceDisabled : theme.onSurfaceVariant,
},
]}
left={left}
right={rightIcon}
disabled={disabled}
onPress={onPress}
rippleColor={theme.rippleColor}
style={styles.listItemCtn}
/>
);
};
const Divider = ({ theme }: { theme: ThemeColors }) => (
<PaperDivider style={[styles.divider, { backgroundColor: theme.outline }]} />
@@ -113,7 +119,7 @@ const InfoItem = ({
);
const Icon = ({ icon, theme }: { icon: string; theme: ThemeColors }) => (
<PaperList.Icon color={theme.primary} icon={icon} style={{ margin: 0 }} />
<PaperList.Icon color={theme.primary} icon={icon} style={styles.margin0} />
);
interface ColorItemProps {
@@ -125,27 +131,23 @@ interface ColorItemProps {
const ColorItem = ({ title, description, theme, onPress }: ColorItemProps) => (
<Pressable
style={{
padding: 16,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}}
style={styles.pressable}
android_ripple={{ color: theme.rippleColor }}
onPress={onPress}
>
<View>
<Text style={{ color: theme.onSurface, fontSize: 16 }}>{title}</Text>
<Text style={[{ color: theme.onSurface }, styles.fontSize16]}>
{title}
</Text>
<Text style={{ color: theme.onSurfaceVariant }}>{description}</Text>
</View>
<View
style={{
backgroundColor: description,
height: 24,
width: 24,
borderRadius: 50,
marginRight: 16,
}}
style={[
{
backgroundColor: description,
},
styles.descriptionView,
]}
/>
</Pressable>
);
@@ -161,6 +163,16 @@ export default {
};
const styles = StyleSheet.create({
margin0: { margin: 0 },
fontSize16: {
fontSize: 16,
},
descriptionView: {
height: 24,
width: 24,
borderRadius: 50,
marginRight: 16,
},
description: {
fontSize: 12,
lineHeight: 20,
@@ -187,4 +199,10 @@ const styles = StyleSheet.create({
flex: 1,
marginVertical: 0,
},
pressable: {
padding: 16,
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
});
+2 -1
View File
@@ -29,6 +29,7 @@ const ListView = ({
isSelected,
onLongPress,
}: ListViewProps) => {
const fadedImage = { opacity: inLibraryBadge ? 0.5 : 1 };
return (
<Pressable
android_ripple={{ color: theme.rippleColor }}
@@ -45,7 +46,7 @@ const ListView = ({
source={{
uri: item.cover,
}}
style={[styles.extensionIcon, inLibraryBadge ? { opacity: 0.5 } : {}]}
style={[styles.extensionIcon, fadedImage]}
/>
<Text
style={[{ color: theme.onSurface }, styles.novelName]}
+2 -2
View File
@@ -164,7 +164,7 @@ const ChapterListSkeleton = ({ img }: { img?: boolean }) => {
React.useEffect(() => {
if (disableLoadingAnimations) return;
sv.value = withRepeat(withSequence(0, withTiming(160, { duration })), -1);
}, [disableLoadingAnimations]);
}, [disableLoadingAnimations, sv]);
const skeletonItems = React.useMemo(() => Array.from({ length: 7 }), []);
@@ -178,7 +178,7 @@ const ChapterListSkeleton = ({ img }: { img?: boolean }) => {
const LGC = React.useMemo(
() => createLGC(highlightColor, animatedProps, disableLoadingAnimations),
[disableLoadingAnimations, highlightColor],
[animatedProps, disableLoadingAnimations, highlightColor],
);
const backgroundStyle = React.useMemo(
() => ({ backgroundColor }),
+164 -128
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { View, Text, /* StyleSheet, */ Pressable } from 'react-native';
import { View, Text, StyleSheet, Pressable } from 'react-native';
import { overlay } from 'react-native-paper';
import color from 'color';
import MaterialCommunityIcons from '@react-native-vector-icons/material-design-icons';
@@ -18,172 +18,208 @@ export const ThemePicker = ({
onPress,
horizontal = false,
}: ThemePickerProps) => (
<View
style={{
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 8,
width: horizontal ? 'auto' : '33%',
marginHorizontal: horizontal ? 4 : 'auto',
}}
>
<View style={[styles.container, horizontal && styles.horizontalContainer]}>
<View
style={{
backgroundColor: theme.background,
borderWidth: 3.6,
borderColor:
currentTheme.id === theme.id
? theme.primary
: currentTheme.background,
width: 95,
height: 140,
borderRadius: 16,
overflow: 'hidden',
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.2)',
}}
style={[
styles.card,
{
backgroundColor: theme.background,
borderColor:
currentTheme.id === theme.id
? theme.primary
: currentTheme.background,
},
]}
>
<Pressable style={{ flex: 1 }} onPress={onPress}>
<Pressable style={styles.flex1} onPress={onPress}>
{currentTheme.id === theme.id ? (
<MaterialCommunityIcons
name="check"
color={theme.onPrimary}
size={15}
style={{
backgroundColor: theme.primary,
position: 'absolute',
top: 5,
right: 5,
borderRadius: 50,
padding: 1.6,
zIndex: 1,
}}
style={[styles.checkIcon, { backgroundColor: theme.primary }]}
/>
) : null}
<View
style={{
height: 20,
backgroundColor: overlay(2, theme.surface),
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
justifyContent: 'center',
}}
style={[
styles.topBar,
{
backgroundColor: overlay(2, theme.surface),
},
]}
>
<View
style={{
backgroundColor: theme.onSurface,
width: 44,
height: 10,
marginLeft: 8,
borderRadius: 50,
}}
style={[styles.topBarAccent, { backgroundColor: theme.onSurface }]}
/>
</View>
<View style={{ padding: 8 }}>
<View style={styles.content}>
<View
style={{
height: 18,
backgroundColor: theme.onSurfaceVariant,
borderRadius: 4,
}}
style={[
styles.titleBar,
{ backgroundColor: theme.onSurfaceVariant },
]}
/>
<View style={{ paddingVertical: 4, flexDirection: 'row' }}>
<View style={styles.row}>
<View
style={{
height: 10,
width: 44,
backgroundColor: theme.onSurface,
borderRadius: 50,
}}
style={[styles.rowAccent, { backgroundColor: theme.onSurface }]}
/>
<View
style={{
height: 10,
width: 16,
marginLeft: 4,
backgroundColor: theme.primary,
borderRadius: 50,
}}
style={[
styles.rowAccentSmall,
{ backgroundColor: theme.primary },
]}
/>
</View>
<View style={{ flexDirection: 'row' }}>
<View style={styles.row}>
<View
style={{
height: 10,
width: 24,
backgroundColor: theme.onSurfaceVariant,
borderRadius: 50,
}}
style={[
styles.rowAccentShort,
{ backgroundColor: theme.onSurfaceVariant },
]}
/>
<View
style={{
height: 10,
width: 24,
backgroundColor: theme.onSurfaceVariant,
borderRadius: 50,
marginLeft: 4,
}}
style={[
styles.rowAccentShort,
styles.marginLeft,
{ backgroundColor: theme.onSurfaceVariant },
]}
/>
</View>
</View>
<View
style={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 24,
backgroundColor: color(theme.primary).alpha(0.08).string(),
justifyContent: 'center',
}}
style={[
styles.bottomBar,
{
backgroundColor: color(theme.primary).alpha(0.08).string(),
},
]}
>
<View
style={{
flex: 1,
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row',
paddingHorizontal: 16,
}}
>
<View style={styles.bottomBarContent}>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.onSurface,
opacity: 0.54,
}}
style={[
styles.dot,
styles.opacityDot,
{ backgroundColor: theme.onSurface },
]}
/>
<View style={[styles.dot, { backgroundColor: theme.primary }]} />
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.primary,
}}
/>
<View
style={{
height: 12,
width: 12,
borderRadius: 50,
backgroundColor: theme.onSurface,
opacity: 0.54,
}}
style={[
styles.dot,
styles.opacityDot,
{ backgroundColor: theme.onSurface },
]}
/>
</View>
</View>
</Pressable>
</View>
<Text
style={{
color: currentTheme.onSurface,
fontSize: 12,
paddingVertical: 4,
}}
>
<Text style={[styles.themeName, { color: currentTheme.onSurface }]}>
{theme.name}
</Text>
</View>
);
// const styles = StyleSheet.create({});
const styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 8,
width: '33%',
},
horizontalContainer: {
width: undefined,
marginHorizontal: 4,
},
card: {
borderWidth: 3.6,
width: 95,
height: 140,
borderRadius: 16,
overflow: 'hidden',
// Shadow for iOS
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 4,
// Elevation for Android
elevation: 2,
},
flex1: {
flex: 1,
},
checkIcon: {
position: 'absolute',
top: 5,
right: 5,
borderRadius: 50,
padding: 1.6,
zIndex: 1,
},
topBar: {
height: 20,
justifyContent: 'center',
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
},
topBarAccent: {
width: 44,
height: 10,
marginLeft: 8,
borderRadius: 50,
},
content: {
padding: 8,
},
titleBar: {
height: 18,
borderRadius: 4,
},
row: {
paddingVertical: 4,
flexDirection: 'row',
},
rowAccent: {
height: 10,
width: 44,
borderRadius: 50,
},
rowAccentSmall: {
height: 10,
width: 16,
marginLeft: 4,
borderRadius: 50,
},
rowAccentShort: {
height: 10,
width: 24,
borderRadius: 50,
},
bottomBar: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
height: 24,
justifyContent: 'center',
},
bottomBarContent: {
flex: 1,
justifyContent: 'space-between',
alignItems: 'center',
flexDirection: 'row',
paddingHorizontal: 16,
},
dot: {
height: 12,
width: 12,
borderRadius: 50,
},
opacityDot: {
opacity: 0.54,
},
themeName: {
fontSize: 12,
paddingVertical: 4,
},
marginLeft: { marginLeft: 4 },
});
+1 -1
View File
@@ -14,7 +14,7 @@ const useSearch = (defaultSearchText?: string, clearSearchOnUnfocus = true) => {
clearSearchbar();
}
}
}, [isFocused, clearSearchbar]);
}, [isFocused, clearSearchbar, clearSearchOnUnfocus]);
return { searchText, setSearchText, clearSearchbar };
};
+7 -7
View File
@@ -20,16 +20,16 @@ const useHistory = () => {
getHistoryFromDb()
.then(res =>
setHistory(
res.map(history => {
const parsedTime = dayjs(history.releaseTime);
res.map(localHistory => {
const parsedTime = dayjs(localHistory.releaseTime);
return {
...history,
...localHistory,
releaseTime: parsedTime.isValid()
? parsedTime.format('LL')
: history.releaseTime,
chapterNumber: history.chapterNumber
? history.chapterNumber
: parseChapterNumber(history.novelName, history.name),
: localHistory.releaseTime,
chapterNumber: localHistory.chapterNumber
? localHistory.chapterNumber
: parseChapterNumber(localHistory.novelName, localHistory.name),
};
}),
),
+26 -23
View File
@@ -37,28 +37,31 @@ export default function usePlugins() {
* We cant use the languagesFilter directly because it is updated only after component's lifecycle end.
* And toggleLanguagFilter triggers filterPlugins before lifecycle end.
*/
const filterPlugins = useCallback((filter: string[]) => {
const installedPlugins =
getMMKVObject<PluginItem[]>(INSTALLED_PLUGINS) || [];
const availablePlugins =
getMMKVObject<PluginItem[]>(AVAILABLE_PLUGINS) || [];
setFilteredInstalledPlugins(
installedPlugins.filter(plg => filter.includes(plg.lang)),
);
setFilteredAvailablePlugins(
orderBy(
availablePlugins
.filter(
avalilablePlugin =>
!installedPlugins.some(
installedPlugin => installedPlugin.id === avalilablePlugin.id,
),
)
.filter(plg => filter.includes(plg.lang)),
'name',
),
);
}, []);
const filterPlugins = useCallback(
(filter: string[]) => {
const installedPlugins =
getMMKVObject<PluginItem[]>(INSTALLED_PLUGINS) || [];
const availablePlugins =
getMMKVObject<PluginItem[]>(AVAILABLE_PLUGINS) || [];
setFilteredInstalledPlugins(
installedPlugins.filter(plg => filter.includes(plg.lang)),
);
setFilteredAvailablePlugins(
orderBy(
availablePlugins
.filter(
avalilablePlugin =>
!installedPlugins.some(
installedPlugin => installedPlugin.id === avalilablePlugin.id,
),
)
.filter(plg => filter.includes(plg.lang)),
'name',
),
);
},
[setFilteredAvailablePlugins, setFilteredInstalledPlugins],
);
const refreshPlugins = useCallback(() => {
const installedPlugins =
@@ -83,7 +86,7 @@ export default function usePlugins() {
setMMKVObject(AVAILABLE_PLUGINS, fetchedPlugins);
filterPlugins(languagesFilter);
});
}, [languagesFilter]);
}, [filterPlugins, languagesFilter, lastUsedPlugin?.id, setLastUsedPlugin]);
const toggleLanguageFilter = (lang: string) => {
const newFilter = languagesFilter.includes(lang)
+1 -1
View File
@@ -55,7 +55,7 @@ export const useTheme = (): ThemeColors => {
};
return colors;
}, [appTheme?.id, isAmoledBlack, customAccent]);
}, [appTheme, isAmoledBlack, customAccent]);
return theme;
};
+1 -1
View File
@@ -65,7 +65,7 @@ const MainNavigator = () => {
// hack this helps app has enough time to initialize database;
refreshPlugins();
}
}, [isOnboarded]);
}, [isOnboarded, refreshPlugins, updateLibraryOnLaunch]);
const { isNewVersion, latestRelease } = useGithubUpdateChecker();
+1
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-bitwise */
import { getUserAgent } from '@hooks/persisted/useUserAgent';
import NativeFile from '@specs/NativeFile';
import { parse as parseProto } from 'protobufjs';
@@ -44,8 +44,8 @@ export const useBrowseSource = (
setHasNextPage(false);
}
})
.catch(error => {
setError(error.message);
.catch(e => {
setError(e.message);
setHasNextPage(false);
});
setFilterValues(plugin.filters);
@@ -56,7 +56,7 @@ export const useBrowseSource = (
}
}
},
[pluginId],
[pluginId, showLatestNovels],
);
const fetchNextPage = () => {
@@ -74,7 +74,7 @@ export const useBrowseSource = (
useEffect(() => {
fetchNovels(currentPage, selectedFilters);
}, [fetchNovels, currentPage]);
}, [fetchNovels, currentPage, selectedFilters]);
const refetchNovels = () => {
setError('');
@@ -128,14 +128,14 @@ export const useSearchSource = (pluginId: string) => {
const isScreenMounted = useRef(true);
const fetchNovels = useCallback(
async (searchText: string, page: number) => {
async (localSearchText: string, page: number) => {
if (isScreenMounted.current === true) {
try {
const plugin = getPlugin(pluginId);
if (!plugin) {
throw new Error(`Unknown plugin: ${pluginId}`);
}
const res = await plugin.searchNovels(searchText, page);
const res = await plugin.searchNovels(localSearchText, page);
setSearchResults(prevState =>
page === 1 ? res : [...prevState, ...res],
);
@@ -38,6 +38,8 @@ const CategoryCard: React.FC<CategoryCardProps> = ({
setFalse: closeDeleteCategoryModal,
} = useBoolean();
const fontWeight = category.sort === 1 ? '700' : '300';
return (
<>
<View
@@ -67,7 +69,7 @@ const CategoryCard: React.FC<CategoryCardProps> = ({
styles.name,
{
color: theme.onSurface,
fontWeight: category.sort === 1 ? '700' : '300',
fontWeight,
},
]}
onPress={showCategoryModal}
@@ -41,113 +41,117 @@ export const useGlobalSearch = ({ defaultSearchText }: Props) => {
const { globalSearchConcurrency = 1 } = useBrowseSettings();
const globalSearch = (searchText: string) => {
if (lastSearch.current === searchText) {
return;
}
lastSearch.current = searchText;
const defaultResult: GlobalSearchResult[] = filteredInstalledPlugins.map(
plugin => ({
isLoading: true,
plugin,
novels: [],
error: null,
}),
);
setSearchResults(defaultResult.sort(novelResultSorter));
setProgress(0);
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 =>
prevResult.plugin.id === plugin.id
? { ...prevResult, novels: res, isLoading: false }
: { ...prevResult },
)
.sort(novelResultSorter),
);
} catch (error: any) {
const errorMessage = error?.message || String(error);
setSearchResults(prevState =>
prevState
.map(prevResult =>
prevResult.plugin.id === _plugin.id
? {
...prevResult,
novels: [],
isLoading: false,
error: errorMessage,
}
: { ...prevResult },
)
.sort(novelResultSorter),
);
const globalSearch = useCallback(
(searchText: string) => {
if (lastSearch.current === searchText) {
return;
}
}
lastSearch.current = searchText;
const defaultResult: GlobalSearchResult[] = filteredInstalledPlugins.map(
plugin => ({
isLoading: true,
plugin,
novels: [],
error: null,
}),
);
//Sort so we load the plugins results in the same order as they show on the list
const filteredSortedInstalledPlugins = [...filteredInstalledPlugins].sort(
(a, b) => a.name.localeCompare(b.name),
);
setSearchResults(defaultResult.sort(novelResultSorter));
setProgress(0);
(async () => {
if (globalSearchConcurrency > 1) {
for (const _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 (const _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,
);
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 =>
prevResult.plugin.id === plugin.id
? { ...prevResult, novels: res, isLoading: false }
: { ...prevResult },
)
.sort(novelResultSorter),
);
} catch (error: any) {
const errorMessage = error?.message || String(error);
setSearchResults(prevState =>
prevState
.map(prevResult =>
prevResult.plugin.id === _plugin.id
? {
...prevResult,
novels: [],
isLoading: false,
error: errorMessage,
}
: { ...prevResult },
)
.sort(novelResultSorter),
);
}
}
})();
};
//Sort so we load the plugins results in the same order as they show on the list
const filteredSortedInstalledPlugins = [...filteredInstalledPlugins].sort(
(a, b) => a.name.localeCompare(b.name),
);
(async () => {
if (globalSearchConcurrency > 1) {
for (const _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 (const _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,
);
}
}
}
})();
},
[filteredInstalledPlugins, globalSearchConcurrency],
);
useEffect(() => {
if (defaultSearchText) {
globalSearch(defaultSearchText);
}
}, []);
}, [defaultSearchText, globalSearch]);
return { searchResults, globalSearch, progress };
};
+7 -5
View File
@@ -41,11 +41,12 @@ const SourceNovels = ({ navigation, route }: SourceNovelsScreenProps) => {
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{
color: theme.onSurfaceVariant,
padding: 20,
textAlign: 'center',
}}
style={[
{
color: theme.onSurfaceVariant,
},
styles.text,
]}
>
{getString('browseScreen.noSource')}
</Text>
@@ -59,4 +60,5 @@ export default SourceNovels;
const styles = StyleSheet.create({
container: { flex: 1 },
text: { padding: 20, textAlign: 'center' },
});
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import {
StyleSheet,
View,
@@ -90,50 +90,53 @@ const BrowseALScreen = ({ navigation }: BrowseALScreenProps) => {
const anilistUrl =
'https://anilist.co/search/manga?format=NOVEL&sort=POPULARITY_DESC';
const searchAniList = async (onlyTop: boolean, page = 1) => {
try {
if (!tracker) {
const searchAniList = useCallback(
async (onlyTop: boolean, page = 1) => {
try {
if (!tracker) {
setLoading(false);
setError('Please login!');
return;
}
const { data } = await queryAniList(
anilistSearchQuery,
{
search: onlyTop ? undefined : searchText,
page,
},
tracker.auth,
);
const results = data.Page.media.map((m: any) => {
return {
id: m.id,
novelName: m.title.userPreferred,
novelCover: m.coverImage.extraLarge,
score: `${m.averageScore}%`,
info: [
'', // MAL returns an item we don't care about first, so the component ignores the first element
`Light Novel (${m.volumes || '?'} Vols)`,
`${formatDate(m.startDate)}${
datesEqual(m.startDate, m.endDate)
? ''
: `- ${formatDate(m.endDate)}`
}`,
],
};
});
setHasNextPage(data.Page.pageInfo.hasNextPage);
setNovels(onlyTop ? before => before.concat(results) : results);
setLoading(false);
setError('Please login!');
return;
} catch (err: any) {
setError(err.message);
setNovels([]);
setLoading(false);
showToast(err.message);
}
const { data } = await queryAniList(
anilistSearchQuery,
{
search: onlyTop ? undefined : searchText,
page,
},
tracker.auth,
);
const results = data.Page.media.map((m: any) => {
return {
id: m.id,
novelName: m.title.userPreferred,
novelCover: m.coverImage.extraLarge,
score: `${m.averageScore}%`,
info: [
'', // MAL returns an item we don't care about first, so the component ignores the first element
`Light Novel (${m.volumes || '?'} Vols)`,
`${formatDate(m.startDate)}${
datesEqual(m.startDate, m.endDate)
? ''
: `- ${formatDate(m.endDate)}`
}`,
],
};
});
setHasNextPage(data.Page.pageInfo.hasNextPage);
setNovels(onlyTop ? before => before.concat(results) : results);
setLoading(false);
} catch (err: any) {
setError(err.message);
setNovels([]);
setLoading(false);
showToast(err.message);
}
};
},
[anilistSearchQuery, searchText, tracker],
);
const clearSearchbar = () => {
setNovels([]);
@@ -145,7 +148,7 @@ const BrowseALScreen = ({ navigation }: BrowseALScreenProps) => {
useEffect(() => {
searchAniList(true);
}, []);
}, [searchAniList]);
const renderItem: ListRenderItem<ALNovel> = ({ item }) => (
<TrackerNovelCard
@@ -159,22 +162,25 @@ const BrowseALScreen = ({ navigation }: BrowseALScreenProps) => {
/>
);
const ListEmptyComponent = () => (
<ErrorView
errorName={error || 'No results found'}
actions={[
{
name: 'Retry',
onPress: () => {
setLoading(true);
setError(undefined);
searchAniList(true);
const ListEmptyComponent = useCallback(
() => (
<ErrorView
errorName={error || 'No results found'}
actions={[
{
name: 'Retry',
onPress: () => {
setLoading(true);
setError(undefined);
searchAniList(true);
},
icon: 'reload',
},
icon: 'reload',
},
]}
theme={theme}
/>
]}
theme={theme}
/>
),
[error, searchAniList, theme],
);
return (
@@ -213,7 +219,7 @@ const BrowseALScreen = ({ navigation }: BrowseALScreenProps) => {
}}
ListFooterComponent={
!searchText ? (
<View style={{ paddingVertical: 16 }}>
<View style={styles.paddingVertical}>
<ActivityIndicator color={theme.primary} />
</View>
) : null
@@ -235,4 +241,5 @@ const styles = StyleSheet.create({
paddingBottom: 8,
paddingHorizontal: 4,
},
paddingVertical: { paddingVertical: 16 },
});
+2 -1
View File
@@ -153,7 +153,7 @@ const BrowseMalScreen = ({ navigation }: BrowseMalScreenProps) => {
}}
ListFooterComponent={
!searchText ? (
<View style={{ paddingVertical: 16 }}>
<View style={styles.paddingVertical}>
<ActivityIndicator color={theme.primary} />
</View>
) : null
@@ -175,4 +175,5 @@ const styles = StyleSheet.create({
paddingBottom: 8,
paddingHorizontal: 4,
},
paddingVertical: { paddingVertical: 16 },
});
@@ -23,6 +23,8 @@ const GlobalSearchNovelCover = ({
const uri = cover;
const opacity = inLibrary ? 0.5 : 1;
return (
<View style={styles.container}>
<Pressable
@@ -33,7 +35,7 @@ const GlobalSearchNovelCover = ({
>
<Image
source={{ uri }}
style={[styles.novelCover, inLibrary && { opacity: 0.5 }]}
style={[styles.novelCover, { opacity }]}
progressiveRenderingEnabled={true}
/>
<Text
+2 -1
View File
@@ -47,7 +47,7 @@ const Migration = ({ navigation }: MigrationScreenProps) => {
/>
<FlatList
data={plugins}
contentContainerStyle={{ paddingBottom: 48 }}
contentContainerStyle={styles.paddingBottom}
keyExtractor={item => item.id}
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
@@ -62,6 +62,7 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
paddingBottom: { paddingBottom: 48 },
listHeader: {
padding: 20,
@@ -78,11 +78,12 @@ const MigrationNovelList = ({
renderItem={renderItem}
ListEmptyComponent={
<Text
style={{
color: theme.onSurfaceVariant,
padding: 8,
paddingVertical: 4,
}}
style={[
{
color: theme.onSurfaceVariant,
},
styles.padding,
]}
>
{getString('sourceScreen.noResultsFound')}
</Text>
@@ -91,22 +92,18 @@ const MigrationNovelList = ({
<Portal>
<Modal visible={migrateNovelDialog} onDismiss={hideMigrateNovelDialog}>
<Text
style={{
color: theme.onSurface,
fontSize: 18,
marginBottom: 16,
}}
style={[
{
color: theme.onSurface,
},
styles.text,
]}
>
{getString('browseScreen.migration.dialogMessage', {
url: selectedNovel.name,
})}
</Text>
<View
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
}}
>
<View style={styles.row}>
<Button
onPress={hideMigrateNovelDialog}
title={getString('common.cancel')}
@@ -140,4 +137,13 @@ const styles = StyleSheet.create({
paddingHorizontal: 4,
paddingVertical: 8,
},
row: {
flexDirection: 'row',
justifyContent: 'space-between',
},
text: {
fontSize: 18,
marginBottom: 16,
},
padding: { padding: 8, paddingVertical: 4 },
});
@@ -99,9 +99,9 @@ const MigrationNovels = ({ navigation, route }: MigrateNovelScreenProps) => {
item,
}) => (
<>
<View style={{ padding: 8, paddingVertical: 16 }}>
<View style={styles.padding}>
<Text style={{ color: theme.onSurface }}>{item.name}</Text>
<Text style={{ color: theme.onSurfaceVariant, fontSize: 12 }}>
<Text style={[{ color: theme.onSurfaceVariant }, styles.fontSize]}>
{item.lang}
</Text>
</View>
@@ -135,7 +135,7 @@ const MigrationNovels = ({ navigation, route }: MigrateNovelScreenProps) => {
/>
) : null}
<FlatList
contentContainerStyle={{ flexGrow: 1, padding: 4 }}
contentContainerStyle={styles.flexGrow}
data={searchResults}
keyExtractor={item => item.id}
renderItem={renderItem}
@@ -160,4 +160,7 @@ const styles = StyleSheet.create({
paddingHorizontal: 8,
paddingVertical: 16,
},
flexGrow: { flexGrow: 1, padding: 4 },
padding: { padding: 8, paddingVertical: 16 },
fontSize: { fontSize: 12 },
});
@@ -29,18 +29,22 @@ const MigrationSourceCard = ({
<Image source={{ uri: iconUrl }} style={styles.sourceIcon} />
<View style={styles.sourceDetailsContainer}>
<Text
style={{
color: theme.onSurface,
fontSize: 14,
}}
style={[
{
color: theme.onSurface,
},
styles.fontSize14,
]}
>
{name} {` (${noOfNovels || 0})`}
</Text>
<Text
style={{
color: theme.onSurfaceVariant,
fontSize: 12,
}}
style={[
{
color: theme.onSurfaceVariant,
},
styles.fontSize12,
]}
>
{lang}
</Text>
@@ -72,4 +76,6 @@ const styles = StyleSheet.create({
height: 40,
width: 40,
},
fontSize14: { fontSize: 14 },
fontSize12: { fontSize: 12 },
});
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { RefreshControl, View } from 'react-native';
import { RefreshControl, StyleSheet, View } from 'react-native';
import { xor } from 'lodash-es';
import { EmptyView } from '@components/index';
@@ -77,7 +77,7 @@ export const LibraryView: React.FC<Props> = ({
};
return (
<View style={{ flex: 1 }}>
<View style={styles.flex}>
<NovelList
data={novels}
extraData={[selectedNovelIds]}
@@ -114,3 +114,7 @@ export const LibraryView: React.FC<Props> = ({
</View>
);
};
const styles = StyleSheet.create({
flex: { flex: 1 },
});
+1 -1
View File
@@ -86,7 +86,7 @@ export const useLibrary = (): UseLibraryReturnType => {
setLibrary(novels);
},
[downloadedOnlyMode, filter, searchText, sortOrder],
[downloadedOnlyMode, filter, refreshCategories, searchText, sortOrder],
);
useFocusEffect(() => {
+6 -2
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { ScrollView } from 'react-native';
import { ScrollView, StyleSheet } from 'react-native';
import * as Linking from 'expo-linking';
@@ -37,7 +37,7 @@ const AboutScreen = ({ navigation }: AboutScreenProps) => {
theme={theme}
goBack={true}
/>
<ScrollView style={{ flex: 1 }}>
<ScrollView style={styles.flex}>
<List.Section>
<List.Item
title={getString('aboutScreen.version')}
@@ -100,3 +100,7 @@ const AboutScreen = ({ navigation }: AboutScreenProps) => {
};
export default AboutScreen;
const styles = StyleSheet.create({
flex: { flex: 1 },
});
+13 -10
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { FlatList, StyleSheet } from 'react-native';
import { Appbar as MaterialAppbar } from 'react-native-paper';
@@ -30,9 +30,9 @@ const Downloads = ({ navigation }: DownloadsScreenProps) => {
const [loading, setLoading] = useState(true);
const [chapters, setChapters] = useState<DownloadedChapter[]>([]);
const groupUpdatesByDate = (
chapters: DownloadedChapter[],
localChapters: DownloadedChapter[],
): DownloadedChapter[][] => {
const dateGroups = chapters.reduce((groups, item) => {
const dateGroups = localChapters.reduce((groups, item) => {
const novelId = item.novelId;
if (!groups[novelId]) {
groups[novelId] = [];
@@ -70,13 +70,16 @@ const Downloads = ({ navigation }: DownloadsScreenProps) => {
);
};
const ListEmptyComponent = () =>
!loading ? (
<EmptyView
icon="(˘・_・˘)"
description={getString('downloadScreen.noDownloads')}
/>
) : null;
const ListEmptyComponent = useCallback(
() =>
!loading ? (
<EmptyView
icon="(˘・_・˘)"
description={getString('downloadScreen.noDownloads')}
/>
) : null,
[loading],
);
useEffect(() => {
getChapters().finally(() => setLoading(false));
+6 -3
View File
@@ -71,11 +71,11 @@ const DownloadQueue = ({ navigation }: TaskQueueScreenProps) => {
</Appbar>
<FlatList
contentContainerStyle={{ flexGrow: 1, paddingBottom: 100 }}
contentContainerStyle={styles.paddingBottom}
keyExtractor={(item, index) => 'task_' + index}
data={taskQueue || []}
renderItem={({ item }) => (
<View style={{ padding: 16 }}>
<View style={styles.padding}>
<Text style={{ color: theme.onSurface }}>{item.meta.name}</Text>
{item.meta.progressText ? (
<Text style={{ color: theme.onSurfaceVariant }}>
@@ -88,7 +88,7 @@ const DownloadQueue = ({ navigation }: TaskQueueScreenProps) => {
}
progress={item.meta.progress}
color={theme.primary}
style={{ marginTop: 8, backgroundColor: theme.surface2 }}
style={[{ backgroundColor: theme.surface2 }, styles.marginTop]}
/>
</View>
)}
@@ -136,4 +136,7 @@ const styles = StyleSheet.create({
position: 'absolute',
right: 0,
},
marginTop: { marginTop: 8 },
paddingBottom: { paddingBottom: 100, flexGrow: 1 },
padding: { padding: 16 },
});
+5 -1
View File
@@ -27,7 +27,7 @@ export const MoreHeader = ({
mode="small"
theme={theme}
/>
<View style={{ overflow: 'hidden', paddingBottom: 4 }}>
<View style={styles.overflow}>
<View style={[styles.logoContainer, { backgroundColor: theme.surface }]}>
<Image
source={require('../../../../assets/logo.png')}
@@ -49,4 +49,8 @@ const styles = StyleSheet.create({
paddingBottom: 24,
paddingTop: 4,
},
overflow: {
overflow: 'hidden',
paddingBottom: 4,
},
});
@@ -4,6 +4,7 @@ import { Button } from '@components';
import { Dialog, overlay, Portal } from 'react-native-paper';
import { ThemeColors } from '@theme/types';
import { getString } from '@strings/translations';
import { StyleSheet } from 'react-native';
interface RemoveDownloadsDialogProps {
dialogVisible: boolean;
@@ -23,17 +24,20 @@ const RemoveDownloadsDialog = ({
<Dialog
visible={dialogVisible}
onDismiss={hideDialog}
style={{
borderRadius: 6,
backgroundColor: overlay(2, theme.surface),
}}
style={[
{
backgroundColor: overlay(2, theme.surface),
},
styles.borderRadius,
]}
>
<Dialog.Title
style={{
letterSpacing: 0,
fontSize: 16,
color: theme.onSurface,
}}
style={[
{
color: theme.onSurface,
},
styles.fontSize,
]}
>
{getString('downloadScreen.removeDownloadsWarning')}
</Dialog.Title>
@@ -47,3 +51,11 @@ const RemoveDownloadsDialog = ({
};
export default RemoveDownloadsDialog;
const styles = StyleSheet.create({
fontSize: {
letterSpacing: 0,
fontSize: 16,
},
borderRadius: { borderRadius: 6 },
});
@@ -51,7 +51,7 @@ export const DownloadButton: React.FC<DownloadButtonProps> = ({
setDownloaded(isDownloadedValue);
setChapterDownloaded?.(isDownloadedValue);
}
}, [chapterId, isDownloading]);
}, [chapterId, isDownloading, setChapterDownloaded]);
if (isDownloading || downloaded === undefined) {
return <ChapterDownloadingButton theme={theme} />;
}
@@ -53,7 +53,7 @@ const DownloadCustomChapterModal = ({
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
{getString('novelScreen.download.customAmount')}
</Text>
<View style={{ flexDirection: 'row', justifyContent: 'center' }}>
<View style={styles.row}>
<IconButton
icon="chevron-double-left"
animated
@@ -70,7 +70,7 @@ const DownloadCustomChapterModal = ({
/>
<TextInput
value={text.toString()}
style={{ color: theme.onSurface, marginHorizontal: 4 }}
style={[{ color: theme.onSurface }, styles.marginHorizontal]}
keyboardType="numeric"
onChangeText={onChangeText}
onSubmitEditing={onSubmit}
@@ -113,4 +113,6 @@ const styles = StyleSheet.create({
fontSize: 16,
marginBottom: 16,
},
row: { flexDirection: 'row', justifyContent: 'center' },
marginHorizontal: { marginHorizontal: 4 },
});
+89 -54
View File
@@ -27,6 +27,24 @@ interface EditInfoModalProps {
setNovel: (novel: NovelInfo | undefined) => void;
}
// --- Dynamic style helpers ---
const getModalTitleColor = (theme: ThemeColors) => ({ color: theme.onSurface });
const getStatusLabelColor = (theme: ThemeColors) => ({
color: theme.onSurfaceVariant,
});
const getScrollViewStyle = () => styles.statusScrollView;
const getStatusChipContainer = () => styles.statusChipContainer;
const getStatusChipPressable = (selected: boolean, theme: ThemeColors) => ({
backgroundColor: selected ? theme.rippleColor : 'transparent',
});
const getStatusChipText = (selected: boolean, theme: ThemeColors) => ({
color: selected ? theme.primary : theme.onSurfaceVariant,
});
const getGenreListStyle = () => styles.genreList;
const getButtonRowStyle = () => styles.buttonRow;
const getFlex1 = () => styles.flex1;
// --- Main Component ---
const EditInfoModal = ({
theme,
hideModal,
@@ -54,50 +72,32 @@ const EditInfoModal = ({
return (
<Portal>
<Modal visible={modalVisible} onDismiss={hideModal}>
<Text style={[styles.modalTitle, { color: theme.onSurface }]}>
<Text style={[styles.modalTitle, getModalTitleColor(theme)]}>
{getString('novelScreen.edit.info')}
</Text>
<View
style={{
marginVertical: 8,
flexDirection: 'row',
alignItems: 'center',
}}
>
<Text style={{ color: theme.onSurfaceVariant }}>
<View style={styles.statusRow}>
<Text style={getStatusLabelColor(theme)}>
{getString('novelScreen.edit.status')}
</Text>
<ScrollView
style={{ marginLeft: 8 }}
style={getScrollViewStyle()}
horizontal
showsHorizontalScrollIndicator={false}
>
{status.map((item, index) => (
<View
style={{ borderRadius: 8, overflow: 'hidden' }}
key={'novelInfo' + index}
>
<View style={getStatusChipContainer()} key={'novelInfo' + index}>
<Pressable
style={{
backgroundColor:
novelInfo.status === item
? theme.rippleColor
: 'transparent',
paddingVertical: 6,
paddingHorizontal: 12,
}}
style={[
styles.statusChipPressable,
getStatusChipPressable(novelInfo.status === item, theme),
]}
android_ripple={{
color: theme.rippleColor,
}}
onPress={() => setNovelInfo({ ...novel, status: item })}
>
<Text
style={{
color:
novelInfo.status === item
? theme.primary
: theme.onSurfaceVariant,
}}
style={getStatusChipText(novelInfo.status === item, theme)}
>
{translateNovelStatus(item)}
</Text>
@@ -185,7 +185,7 @@ const EditInfoModal = ({
{novelInfo.genres !== undefined && novelInfo.genres !== '' ? (
<FlatList
style={{ marginVertical: 8 }}
style={getGenreListStyle()}
horizontal
data={novelInfo.genres?.split(',')}
keyExtractor={(_, index) => 'novelTag' + index}
@@ -197,7 +197,7 @@ const EditInfoModal = ({
showsHorizontalScrollIndicator={false}
/>
) : null}
<View style={{ flexDirection: 'row' }}>
<View style={getButtonRowStyle()}>
<Button
onPress={() => {
setNovelInfo(initialNovelInfo);
@@ -206,7 +206,7 @@ const EditInfoModal = ({
>
{getString('common.reset')}
</Button>
<View style={{ flex: 1 }} />
<View style={getFlex1()} />
<Button
onPress={() => {
setNovel(novelInfo);
@@ -225,6 +225,17 @@ const EditInfoModal = ({
export default EditInfoModal;
// --- GenreChip with split styles ---
const getGenreChipContainer = (theme: ThemeColors) => ({
backgroundColor: theme.secondaryContainer,
});
const getGenreChipText = (theme: ThemeColors) => ({
color: theme.onSecondaryContainer,
});
const getGenreChipIcon = (theme: ThemeColors) => ({
color: theme.onSecondaryContainer,
});
const GenreChip = ({
children,
theme,
@@ -234,35 +245,16 @@ const GenreChip = ({
theme: ThemeColors;
onPress: () => void;
}) => (
<View
style={{
flex: 1,
flexDirection: 'row',
borderRadius: 8,
paddingVertical: 6,
paddingHorizontal: 16,
marginBottom: 4,
marginRight: 8,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: theme.secondaryContainer,
}}
>
<Text
style={{
fontSize: 12,
color: theme.onSecondaryContainer,
textTransform: 'capitalize',
}}
>
<View style={[styles.genreChipContainer, getGenreChipContainer(theme)]}>
<Text style={[styles.genreChipText, getGenreChipText(theme)]}>
{children}
</Text>
<MaterialCommunityIcons
name="close"
color={theme.onSecondaryContainer}
size={18}
onPress={onPress}
style={{ marginLeft: 4 }}
style={styles.genreChipIcon}
{...getGenreChipIcon(theme)}
/>
</View>
);
@@ -280,4 +272,47 @@ const styles = StyleSheet.create({
fontSize: 24,
marginBottom: 16,
},
statusRow: {
marginVertical: 8,
flexDirection: 'row',
alignItems: 'center',
},
statusScrollView: {
marginLeft: 8,
},
statusChipContainer: {
borderRadius: 8,
overflow: 'hidden',
},
statusChipPressable: {
paddingVertical: 6,
paddingHorizontal: 12,
},
genreList: {
marginVertical: 8,
},
buttonRow: {
flexDirection: 'row',
},
flex1: {
flex: 1,
},
genreChipContainer: {
flex: 1,
flexDirection: 'row',
borderRadius: 8,
paddingVertical: 6,
paddingHorizontal: 16,
marginBottom: 4,
marginRight: 8,
justifyContent: 'center',
alignItems: 'center',
},
genreChipText: {
fontSize: 12,
textTransform: 'capitalize',
},
genreChipIcon: {
marginLeft: 4,
},
});
@@ -55,10 +55,10 @@ const CoverImage = ({
return (
<ImageBackground source={source} style={styles.coverImage}>
<View
style={{
flex: 1,
backgroundColor: color(theme.background).alpha(0.7).string(),
}}
style={[
{ backgroundColor: color(theme.background).alpha(0.7).string() },
styles.flex1,
]}
>
{source.uri ? (
<LinearGradient
@@ -97,44 +97,29 @@ const NovelThumbnail = ({
<Portal>
<IconButton
icon="content-save"
style={{
position: 'absolute',
top: top + 6,
right: right + 6,
zIndex: 10,
}}
style={[
styles.absoluteIcon,
styles.zIndex,
{ top: top + 6, right: right + 6 },
]}
iconColor={theme.onBackground}
onPress={saveNovelCover}
/>
<IconButton
icon="pencil-outline"
style={{
position: 'absolute',
top: top + 6,
right: right + 60,
zIndex: 10,
}}
style={[
styles.absoluteIcon,
styles.zIndex,
{ top: top + 6, right: right + 60 },
]}
iconColor={theme.onBackground}
onPress={setCustomNovelCover}
/>
<Pressable
style={{
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
backgroundColor: 'rgba(0,0,0,0.7)',
}}
style={[styles.expandedOverlay]}
onPress={() => setExpanded(false)}
>
<Image
source={source}
resizeMode="contain"
style={{
flex: 1,
}}
/>
<Image source={source} resizeMode="contain" style={styles.flex1} />
</Pressable>
</Portal>
)}
@@ -151,7 +136,7 @@ const NovelTitle = ({
<Text
onLongPress={onLongPress}
onPress={onPress}
style={[styles.novelTitle, { color: theme.onBackground }]}
style={[{ color: theme.onBackground }, styles.novelTitle]}
numberOfLines={4}
>
{children}
@@ -166,7 +151,7 @@ const NovelInfo = ({
children: React.ReactNode;
}) => (
<Text
style={[styles.novelInfo, { color: theme.onSurfaceVariant }]}
style={[{ color: theme.onSurfaceVariant }, styles.novelInfo]}
numberOfLines={1}
>
{children}
@@ -182,30 +167,26 @@ const FollowButton = ({
onPress: () => void;
followed: boolean;
}) => (
<View style={{ borderRadius: 4, overflow: 'hidden', flex: 1 }}>
<View style={styles.followButtonContainer}>
<Pressable
android_ripple={{
color: color(theme.primary).alpha(0.12).string(),
borderless: false,
}}
onPress={onPress}
style={{
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 8,
}}
style={styles.followButtonPressable}
>
<IconButton
icon={followed ? 'heart' : 'heart-outline'}
iconColor={followed ? theme.primary : theme.outline}
size={24}
style={{ margin: 0 }}
style={styles.iconButton}
/>
<Text
style={{
fontSize: 12,
color: followed ? theme.primary : theme.outline,
}}
style={[
{ color: followed ? theme.primary : theme.outline },
styles.followButtonText,
]}
>
{followed
? getString('novelScreen.inLibaray')
@@ -224,30 +205,26 @@ const TrackerButton = ({
onPress: () => void;
isTracked: boolean;
}) => (
<View style={{ borderRadius: 4, overflow: 'hidden', flex: 1 }}>
<View style={styles.followButtonContainer}>
<Pressable
android_ripple={{
color: theme.rippleColor,
borderless: false,
}}
onPress={onPress}
style={{
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 8,
}}
style={styles.followButtonPressable}
>
<IconButton
icon={isTracked ? 'check' : 'sync'}
iconColor={isTracked ? theme.primary : theme.outline}
size={24}
style={{ margin: 0 }}
style={styles.iconButton}
/>
<Text
style={{
fontSize: 12,
color: isTracked ? theme.primary : theme.outline,
}}
style={[
{ color: isTracked ? theme.primary : theme.outline },
styles.followButtonText,
]}
>
{isTracked
? getString('novelScreen.tracked')
@@ -290,15 +267,36 @@ export {
};
const styles = StyleSheet.create({
flex1: {
flex: 1,
},
coverImage: {},
followButton: {
alignItems: 'center',
borderWidth: 0,
elevation: 0,
height: 32,
absoluteIcon: {
position: 'absolute',
},
expandedOverlay: {
position: 'absolute',
width: '100%',
height: '100%',
flex: 1,
justifyContent: 'center',
marginLeft: 16,
paddingLeft: 4,
backgroundColor: 'rgba(0,0,0,0.7)',
},
followButtonContainer: {
borderRadius: 4,
overflow: 'hidden',
flex: 1,
},
followButtonPressable: {
justifyContent: 'center',
alignItems: 'center',
paddingBottom: 8,
},
followButtonText: {
fontSize: 12,
},
iconButton: {
margin: 0,
},
genreChip: {
borderRadius: 50,
@@ -342,4 +340,5 @@ const styles = StyleSheet.create({
novelTitle: {
fontSize: 20,
},
zIndex: { zIndex: 10 },
});
@@ -5,6 +5,7 @@ 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';
interface ReadButtonProps {
chapters: ChapterInfo[];
@@ -38,7 +39,7 @@ const ReadButton = ({
name: chapters[0].name,
})
}
style={{ margin: 16 }}
style={styles.margin}
onPress={navigateToLastReadChapter}
mode="contained"
/>
@@ -50,3 +51,7 @@ const ReadButton = ({
};
export default ReadButton;
const styles = StyleSheet.create({
margin: { margin: 16 },
});
@@ -112,18 +112,14 @@ export const LoadingChapterItem = memo(() => (
<LoadingShimmer style={styles.text} height={20} width={FULL_WIDTH - 50} />
<LoadingShimmer style={styles.text} height={16} width={FULL_WIDTH - 50} />
</View>
<LoadingShimmer
style={{ borderRadius: 20, alignSelf: 'center', marginLeft: 20 }}
height={30}
width={30}
/>
<LoadingShimmer style={styles.loadingChapterItem} height={30} width={30} />
</View>
));
const Chapters = memo(() => (
<View>
<LoadingShimmer
style={[styles.text, { marginBottom: 5, marginHorizontal: 16 }]}
style={[styles.text, styles.chapters]}
height={30}
width={FULL_WIDTH}
/>
@@ -144,6 +140,8 @@ const NovelScreenLoading: React.FC<Props> = () => {
};
const styles = StyleSheet.create({
loadingChapterItem: { borderRadius: 20, alignSelf: 'center', marginLeft: 20 },
chapters: { marginBottom: 5, marginHorizontal: 16 },
chapter: {
flexDirection: 'row',
marginHorizontal: 16,
@@ -46,7 +46,7 @@ const ChaptersSettingsSheet = ({
const FirstRoute = useCallback(
() => (
<View style={{ flex: 1 }}>
<View style={styles.flex}>
<Checkbox
theme={theme}
label={getString('novelScreen.bottomSheet.filters.downloaded')}
@@ -110,7 +110,7 @@ const ChaptersSettingsSheet = ({
const SecondRoute = useCallback(
() => (
<View style={{ flex: 1 }}>
<View style={styles.flex}>
<SortItem
label={getString('novelScreen.bottomSheet.order.bySource')}
status={
@@ -150,7 +150,7 @@ const ChaptersSettingsSheet = ({
const ThirdRoute = useCallback(
() => (
<View style={{ flex: 1 }}>
<View style={styles.flex}>
<Checkbox
status={showChapterTitles}
label={getString('novelScreen.bottomSheet.displays.sourceTitle')}
@@ -187,18 +187,25 @@ const ChaptersSettingsSheet = ({
<TabBar
{...props}
indicatorStyle={{ backgroundColor: theme.primary }}
style={{
backgroundColor: overlay(2, theme.surface),
borderBottomWidth: 1,
borderBottomColor: theme.outline,
elevation: 0,
}}
style={[
{
backgroundColor: overlay(2, theme.surface),
borderBottomColor: theme.outline,
},
styles.tabBar,
]}
inactiveColor={theme.onSurfaceVariant}
activeColor={theme.primary}
pressColor={color(theme.primary).alpha(0.12).string()}
/>
);
const renderLabel = useCallback(
({ route, color: localColor }: { route: any; color: string }) => {
return <Text style={{ color: localColor }}>{route.title}</Text>;
},
[],
);
return (
<BottomSheet
snapPoints={[240]}
@@ -217,9 +224,7 @@ const ChaptersSettingsSheet = ({
>
<TabView
commonOptions={{
label: ({ route, color }) => (
<Text style={{ color }}>{route.title}</Text>
),
label: renderLabel,
}}
navigationState={{ index, routes }}
renderTabBar={renderTabBar}
@@ -248,4 +253,6 @@ const styles = StyleSheet.create({
transparent: {
backgroundColor: 'transparent',
},
flex: { flex: 1 },
tabBar: { borderBottomWidth: 1, elevation: 0 },
});
@@ -27,6 +27,7 @@ const NovelSummary: React.FC<NovelSummaryProps> = ({
const bottom = expanded ? 0 : 4;
const containerBottomPadding = expanded ? 24 : 8;
const opacity = expanded ? 1 : 0.7;
return (
<Pressable
@@ -49,7 +50,7 @@ const NovelSummary: React.FC<NovelSummaryProps> = ({
{
backgroundColor: theme.background,
bottom,
opacity: expanded ? 1 : 0.7,
opacity,
},
]}
>
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { FlatList, StyleSheet, Text, View } from 'react-native';
import { Divider, Portal } from 'react-native-paper';
import { NavigationProp, useNavigation } from '@react-navigation/native';
@@ -35,17 +35,17 @@ const SetCategoryModal: React.FC<SetCategoryModalProps> = ({
const [selectedCategories, setSelectedCategories] = useState<Category[]>([]);
const [categories = [], setCategories] = useState<CCategory[]>();
const getCategories = async () => {
const getCategories = useCallback(async () => {
const res = getCategoriesWithCount(novelIds);
setCategories(res);
setSelectedCategories(res.filter(c => c.novelsCount));
};
}, [novelIds]);
useEffect(() => {
if (visible) {
getCategories();
}
}, [visible]);
}, [getCategories, visible]);
return (
<Portal>
@@ -82,12 +82,12 @@ const SetCategoryModal: React.FC<SetCategoryModalProps> = ({
}
/>
<Divider
style={{
height: 1,
width: '90%',
marginLeft: '5%',
backgroundColor: theme.onSurfaceDisabled,
}}
style={[
{
backgroundColor: theme.onSurfaceDisabled,
},
styles.divider,
]}
/>
<View style={styles.btnContainer}>
<Button
@@ -127,6 +127,7 @@ const SetCategoryModal: React.FC<SetCategoryModalProps> = ({
export default SetCategoryModal;
const styles = StyleSheet.create({
divider: { height: 1, width: '90%', marginLeft: '5%' },
btnContainer: {
flexDirection: 'row',
marginTop: 20,
@@ -1,23 +1,13 @@
import React from 'react';
import { StyleSheet, View, Text, Image, Pressable } from 'react-native';
import { TouchableRipple, IconButton } from 'react-native-paper';
import { getScoreFormatting } from './AniList';
import color from 'color';
import { getScoreFormatting } from './AniList';
export const AddTrackingCard = ({ theme, setTrackSearchDialog, icon }) => (
<View style={styles.addCardContainer}>
<Image source={icon} style={styles.trackerIcon} />
<View
style={{
flex: 1,
flexDirection: 'row',
borderRadius: 4,
overflow: 'hidden',
marginHorizontal: 16,
}}
>
<View style={styles.addCardPressableContainer}>
<Pressable
style={styles.rippleContainer}
android_ripple={{
@@ -26,12 +16,7 @@ export const AddTrackingCard = ({ theme, setTrackSearchDialog, icon }) => (
}}
onPress={setTrackSearchDialog}
>
<Text
style={{
textAlignVertical: 'center',
color: theme.primary,
}}
>
<Text style={[{ color: theme.primary }, styles.addTrackingText]}>
Add Tracking
</Text>
</Pressable>
@@ -51,13 +36,13 @@ export const TrackedItemCard = ({
icon,
}) => {
return (
<View style={[styles.cardContainer, { backgroundColor: theme.surface }]}>
<View style={[{ backgroundColor: theme.surface }, styles.cardContainer]}>
<View
style={[styles.titleContainer, { borderBottomColor: theme.outline }]}
>
<Image source={icon} style={styles.trackerIcon} />
<View style={styles.listItemContainer}>
<Text style={[styles.listItem, { color: theme.onSurfaceVariant }]}>
<Text style={[{ color: theme.onSurfaceVariant }, styles.listItem]}>
{trackItem.title}
</Text>
<IconButton
@@ -68,36 +53,36 @@ export const TrackedItemCard = ({
/>
</View>
</View>
<View style={{ height: 50, flexDirection: 'row' }}>
<View style={styles.trackedItemRow}>
<TouchableRipple
style={[styles.listItemLeft, { borderRightColor: theme.outline }]}
style={[{ borderRightColor: theme.outline }, styles.listItemLeft]}
borderless
onPress={handSetTrackStatusDialog}
rippleColor={color(theme.primary).alpha(0.12).string()}
>
<Text style={[styles.listItem, { color: theme.onSurfaceVariant }]}>
<Text style={[{ color: theme.onSurfaceVariant }, styles.listItem]}>
{getStatus(trackItem.status)}
</Text>
</TouchableRipple>
<TouchableRipple
style={{ flex: 1 }}
style={styles.flex1}
borderless
onPress={handleSetTrackChaptersDialog}
rippleColor={color(theme.primary).alpha(0.12).string()}
>
<Text style={[styles.listItem, { color: theme.onSurfaceVariant }]}>
<Text style={[{ color: theme.onSurfaceVariant }, styles.listItem]}>
{`${trackItem.progress}/${
trackItem.totalChapters ? trackItem.totalChapters : '-'
}`}
</Text>
</TouchableRipple>
<TouchableRipple
style={[styles.listItemRight, { borderLeftColor: theme.outline }]}
style={[{ borderLeftColor: theme.outline }, styles.listItemRight]}
borderless
onPress={handleSetTrackScoreDialog}
rippleColor={color(theme.primary).alpha(0.12).string()}
>
<Text style={[styles.listItem, { color: theme.onSurfaceVariant }]}>
<Text style={[{ color: theme.onSurfaceVariant }, styles.listItem]}>
{tracker.name === 'AniList'
? getScoreFormatting(tracker.auth.meta.scoreFormat, true).label(
trackItem.score,
@@ -118,11 +103,28 @@ const styles = StyleSheet.create({
flexDirection: 'row',
margin: 16,
},
addCardPressableContainer: {
flex: 1,
flexDirection: 'row',
borderRadius: 4,
overflow: 'hidden',
marginHorizontal: 16,
},
addTrackingText: {
textAlignVertical: 'center',
},
cardContainer: {
borderRadius: 8,
boxShadow: '0 0 4px 0 rgba(0, 0, 0, 0.1)',
margin: 8,
},
trackedItemRow: {
height: 50,
flexDirection: 'row',
},
flex1: {
flex: 1,
},
listItem: {
flex: 1,
textAlign: 'center',
+18 -18
View File
@@ -15,6 +15,7 @@ enum OnboardingStep {
export default function OnboardingScreen() {
const theme = useTheme();
const [step] = useState<OnboardingStep>(OnboardingStep.PICK_THEME);
const renderStep = () => {
switch (step) {
case OnboardingStep.PICK_THEME:
@@ -33,38 +34,25 @@ export default function OnboardingScreen() {
};
return (
<SafeAreaView style={[styles.root, { backgroundColor: theme.background }]}>
<SafeAreaView style={[{ backgroundColor: theme.background }, styles.root]}>
<Image
source={require('../../../assets/logo.png')}
tintColor={theme.primary}
style={{
width: 90,
height: 90,
}}
style={styles.logo}
/>
<Text
variant="headlineLarge"
style={{
fontWeight: '600',
paddingBottom: 8,
color: theme.onBackground,
}}
style={[{ color: theme.onBackground }, styles.headline]}
>
{getString('onboardingScreen.welcome')}
</Text>
<Text
style={{
fontWeight: '600',
paddingBottom: 8,
color: theme.onBackground,
}}
>
<Text style={[{ color: theme.onBackground }, styles.helpText]}>
{renderHelptext()}
</Text>
<View
style={[
styles.stepContainer,
{ backgroundColor: theme.surfaceVariant },
styles.stepContainer,
]}
>
{renderStep()}
@@ -88,6 +76,18 @@ const styles = StyleSheet.create({
paddingHorizontal: 16,
paddingTop: 40,
},
logo: {
width: 90,
height: 90,
},
headline: {
fontWeight: '600',
paddingBottom: 8,
},
helpText: {
fontWeight: '600',
paddingBottom: 8,
},
stepContainer: {
borderRadius: 8,
flexBasis: '20%',
+8 -6
View File
@@ -17,11 +17,7 @@ const ThemeList = ({
const [, setTheme] = useMMKVObject('APP_THEME');
return (
<FlatList
contentContainerStyle={{
margin: 'auto',
paddingHorizontal: 8,
paddingBottom: 8,
}}
contentContainerStyle={styles.flatListContainer}
data={list}
renderItem={({ item }) => (
<ThemePicker
@@ -92,7 +88,7 @@ export default function PickThemeStep() {
const theme = useTheme();
const [, setTheme] = useMMKVObject('APP_THEME');
return (
<View style={{ flex: 1 }}>
<View style={styles.flex}>
<View style={styles.buttonContainer}>
<SchemeButton
onPress={() => {
@@ -140,4 +136,10 @@ const styles = StyleSheet.create({
borderBottomWidth: 1,
borderTopWidth: 1,
},
flatListContainer: {
margin: 'auto',
paddingHorizontal: 8,
paddingBottom: 8,
},
flex: { flex: 1 },
});
@@ -185,7 +185,7 @@ const ReaderBottomSheetV2: React.FC<ReaderBottomSheetV2Props> = ({
{ marginLeft: left, marginRight: right },
]}
>
<BottomSheetView style={{ flex: 1 }}>
<BottomSheetView style={styles.flex}>
<TabView
commonOptions={{
label: renderLabel,
@@ -219,4 +219,5 @@ const styles = StyleSheet.create({
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
},
flex: { flex: 1 },
});
+4
View File
@@ -290,6 +290,8 @@ export default function useChapter(
hideHeader,
navigateChapter,
refetch,
setChapter,
setLoading,
}),
[
hidden,
@@ -304,6 +306,8 @@ export default function useChapter(
hideHeader,
navigateChapter,
refetch,
setChapter,
setLoading,
],
);
}
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { ScrollView, Text } from 'react-native';
import { ScrollView, Text, StyleSheet } from 'react-native';
import { ThemePicker } from '@components/ThemePicker/ThemePicker';
import SettingSwitch from './components/SettingSwitch';
@@ -47,28 +47,18 @@ const AppearanceSettings = ({ navigation }: AppearanceSettingsScreenProps) => {
theme={theme}
/>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ paddingBottom: 40 }}
style={styles.flex1}
contentContainerStyle={styles.scrollContent}
>
<List.Section>
<List.SubHeader theme={theme}>
{getString('appearanceScreen.appTheme')}
</List.SubHeader>
<Text
style={{
color: theme.onSurface,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
<Text style={[{ color: theme.onSurface }, styles.themeSectionText]}>
{getString('appearanceScreen.lightTheme')}
</Text>
<ScrollView
contentContainerStyle={{
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: 'row',
}}
contentContainerStyle={styles.themePickerRow}
horizontal={true}
showsHorizontalScrollIndicator={false}
>
@@ -85,21 +75,11 @@ const AppearanceSettings = ({ navigation }: AppearanceSettingsScreenProps) => {
/>
))}
</ScrollView>
<Text
style={{
color: theme.onSurface,
paddingHorizontal: 16,
paddingVertical: 8,
}}
>
<Text style={[{ color: theme.onSurface }, styles.themeSectionText]}>
{getString('appearanceScreen.darkTheme')}
</Text>
<ScrollView
contentContainerStyle={{
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: 'row',
}}
contentContainerStyle={styles.themePickerRow}
horizontal={true}
showsHorizontalScrollIndicator={false}
>
@@ -191,3 +171,21 @@ const AppearanceSettings = ({ navigation }: AppearanceSettingsScreenProps) => {
};
export default AppearanceSettings;
const styles = StyleSheet.create({
flex1: {
flex: 1,
},
scrollContent: {
paddingBottom: 40,
},
themeSectionText: {
paddingHorizontal: 16,
paddingVertical: 8,
},
themePickerRow: {
paddingHorizontal: 16,
paddingVertical: 8,
flexDirection: 'row',
},
});
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { ThemeColors } from '@theme/types';
import { StyleSheet, Text, View, Image } from 'react-native';
import { Portal, TextInput } from 'react-native-paper';
@@ -166,6 +166,15 @@ function RestoreBackup({
});
}, []);
const emptyComponent = useCallback(() => {
return (
<EmptyView
description={getString('backupScreen.noBackupFound')}
theme={theme}
/>
);
}, [theme]);
return (
<>
<FlatList
@@ -187,17 +196,12 @@ function RestoreBackup({
<Text style={{ color: theme.primary }}>
{item.name?.replace(/\.backup$/, ' ')}
</Text>
<Text style={{ color: theme.secondary, fontSize: 12 }}>
<Text style={[{ color: theme.secondary }, styles.fontSize]}>
{'(' + dayjs(item.createdTime).format('LL') + ')'}
</Text>
</Button>
)}
ListEmptyComponent={() => (
<EmptyView
description={getString('backupScreen.noBackupFound')}
theme={theme}
/>
)}
ListEmptyComponent={emptyComponent}
/>
<View style={styles.footerContainer}>
<Button
@@ -228,9 +232,9 @@ export default function GoogleDriveModal({
});
const isSignedIn = GoogleSignin.hasPreviousSignIn();
if (isSignedIn) {
const user = GoogleSignin.getCurrentUser();
if (user) {
setUser(user);
const localUser = GoogleSignin.getCurrentUser();
if (localUser) {
setUser(localUser);
setBackupModal(BackupModal.AUTHORIZED);
}
} else {
@@ -350,4 +354,5 @@ const styles = StyleSheet.create({
marginBottom: 16,
textAlignVertical: 'center',
},
fontSize: { fontSize: 12 },
});
@@ -5,7 +5,7 @@ import ServiceManager from '@services/ServiceManager';
import { getString } from '@strings/translations';
import { ThemeColors } from '@theme/types';
import { fetchTimeout } from '@utils/fetch/fetch';
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { Portal, TextInput } from 'react-native-paper';
@@ -87,7 +87,16 @@ function RestoreBackup({
list(host).then(items =>
setBackupList(items.filter(item => item.endsWith('.backup'))),
);
}, []);
}, [host]);
const emptyComponent = useCallback(() => {
return (
<EmptyView
description={getString('backupScreen.noBackupFound')}
theme={theme}
/>
);
}, [theme]);
return (
<>
@@ -115,12 +124,7 @@ function RestoreBackup({
</Text>
</Button>
)}
ListEmptyComponent={() => (
<EmptyView
description={getString('backupScreen.noBackupFound')}
theme={theme}
/>
)}
ListEmptyComponent={emptyComponent}
/>
<View style={styles.footerContainer}>
<Button
@@ -181,8 +185,8 @@ function SetHost({
throw new Error(getString('backupScreen.remote.unknownHost'));
}
})
.catch((error: any) => {
setError(error.message);
.catch((e: any) => {
setError(e.message);
})
.finally(() => {
setFetching(false);
@@ -11,6 +11,7 @@ import {
} from '@services/backup/legacy';
import { ScrollView } from 'react-native-gesture-handler';
import { getString } from '@strings/translations';
import { StyleSheet } from 'react-native';
const BackupSettings = ({ navigation }: BackupSettingsScreenProps) => {
const theme = useTheme();
@@ -33,7 +34,7 @@ const BackupSettings = ({ navigation }: BackupSettingsScreenProps) => {
handleGoBack={() => navigation.goBack()}
theme={theme}
/>
<ScrollView style={{ paddingBottom: 40 }}>
<ScrollView style={styles.paddingBottom}>
<List.Section>
<List.SubHeader theme={theme}>
{getString('backupScreen.remoteBackup')}
@@ -95,3 +96,7 @@ const BackupSettings = ({ navigation }: BackupSettingsScreenProps) => {
};
export default BackupSettings;
const styles = StyleSheet.create({
paddingBottom: { paddingBottom: 40 },
});
@@ -1,5 +1,5 @@
import React from 'react';
import { ScrollView } from 'react-native';
import { ScrollView, StyleSheet } from 'react-native';
import DisplayModeModal from './modals/DisplayModeModal';
import GridSizeModal from './modals/GridSizeModal';
@@ -108,7 +108,7 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
handleGoBack={navigation.goBack}
theme={theme}
/>
<ScrollView contentContainerStyle={{ paddingBottom: 32 }}>
<ScrollView contentContainerStyle={styles.paddingBottom}>
<List.Section>
<List.SubHeader theme={theme}>
{getString('common.display')}
@@ -285,3 +285,7 @@ const GenralSettings: React.FC<GenralSettingsProps> = ({ navigation }) => {
};
export default GenralSettings;
const styles = StyleSheet.create({
paddingBottom: { paddingBottom: 32 },
});
@@ -52,7 +52,7 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
placeholder="Search voice"
/>
}
ListHeaderComponentStyle={{ paddingHorizontal: 12 }}
ListHeaderComponentStyle={styles.paddingHorizontal}
data={searchText ? searchedVoices : voices}
extraData={tts?.voice}
renderItem={({ item }) => (
@@ -73,7 +73,7 @@ const VoicePickerModal: React.FC<VoicePickerModalProps> = ({
ListEmptyComponent={
<ActivityIndicator
size={24}
style={{ marginTop: 16 }}
style={styles.marginTop}
color={theme.primary}
/>
}
@@ -89,4 +89,6 @@ const styles = StyleSheet.create({
containerStyle: {
flex: 1,
},
paddingHorizontal: { paddingHorizontal: 12 },
marginTop: { marginTop: 16 },
});
@@ -104,7 +104,7 @@ export default function TextToSpeechSettings() {
</List.Section>
</>
) : null}
<View style={{ height: 16 }} />
<View style={styles.height} />
<Portal>
<VoicePickerModal
visible={voiceModalVisible}
@@ -130,4 +130,5 @@ const styles = StyleSheet.create({
flex: 1,
height: 40,
},
height: { height: 16 },
});
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { FlatList, StyleSheet } from 'react-native';
import { FAB, Portal } from 'react-native-paper';
@@ -45,30 +45,35 @@ const SettingsBrowseScreen = ({
setFalse: closeAddRepositoryModal,
} = useBoolean();
const upsertRepository = (repositoryUrl: string, repository?: Repository) => {
if (!new RegExp(/https?:\/\/(.*)plugins\.min\.json/).test(repositoryUrl)) {
showToast('Repository URL is invalid');
return;
}
if (isRepoUrlDuplicated(repositoryUrl)) {
showToast('A respository with this url already exists!');
} else {
if (repository) {
updateRepository(repository.id, repositoryUrl);
} else {
createRepository(repositoryUrl);
const upsertRepository = useCallback(
(repositoryUrl: string, repository?: Repository) => {
if (
!new RegExp(/https?:\/\/(.*)plugins\.min\.json/).test(repositoryUrl)
) {
showToast('Repository URL is invalid');
return;
}
getRepositories();
refreshPlugins();
}
};
if (isRepoUrlDuplicated(repositoryUrl)) {
showToast('A respository with this url already exists!');
} else {
if (repository) {
updateRepository(repository.id, repositoryUrl);
} else {
createRepository(repositoryUrl);
}
getRepositories();
refreshPlugins();
}
},
[refreshPlugins],
);
useEffect(() => {
if (params?.url) {
upsertRepository(params.url);
}
}, [params]);
}, [params, upsertRepository]);
useBackHandler(() => {
if (!navigation.canGoBack()) {
+6 -2
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { ScrollView } from 'react-native';
import { ScrollView, StyleSheet } from 'react-native';
import { Appbar, List, SafeAreaView } from '@components';
import { useTheme } from '@hooks/persisted';
@@ -17,7 +17,7 @@ const SettingsScreen = ({ navigation }: SettingsScreenProps) => {
handleGoBack={navigation.goBack}
theme={theme}
/>
<ScrollView style={{ flex: 1, backgroundColor: theme.background }}>
<ScrollView style={[{ backgroundColor: theme.background }, styles.flex]}>
<List.Item
title={getString('generalSettings')}
icon="tune"
@@ -94,3 +94,7 @@ const SettingsScreen = ({ navigation }: SettingsScreenProps) => {
};
export default SettingsScreen;
const styles = StyleSheet.create({
flex: { flex: 1 },
});
+41 -30
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { View } from 'react-native';
import { View, StyleSheet } from 'react-native';
import { Portal, Text, Button, Provider } from 'react-native-paper';
import { getTracker, useTheme, useTracker } from '@hooks/persisted';
@@ -25,11 +25,11 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => {
theme={theme}
/>
<View
style={{
flex: 1,
backgroundColor: theme.background,
paddingVertical: 8,
}}
style={[
{ backgroundColor: theme.background },
styles.flex1,
styles.screenPadding,
]}
>
<List.Section>
<List.SubHeader theme={theme}>
@@ -91,40 +91,28 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => {
<Portal>
<Modal visible={visible} onDismiss={hideModal}>
<Text
style={{
color: theme.onSurface,
fontSize: 18,
}}
>
<Text style={[{ color: theme.onSurface }, styles.modalText]}>
{getString('trackingScreen.logOutMessage', {
name: tracker?.name,
})}
</Text>
<View
style={{
flexDirection: 'row',
justifyContent: 'flex-end',
}}
>
<View style={styles.modalButtonRow}>
<Button
style={{ marginTop: 30 }}
labelStyle={{
color: theme.primary,
letterSpacing: 0,
textTransform: 'none',
}}
style={styles.modalButton}
labelStyle={[
{ color: theme.primary },
styles.modalButtonLabel,
]}
onPress={hideModal}
>
{getString('common.cancel')}
</Button>
<Button
style={{ marginTop: 30 }}
labelStyle={{
color: theme.primary,
letterSpacing: 0,
textTransform: 'none',
}}
style={styles.modalButton}
labelStyle={[
{ color: theme.primary },
styles.modalButtonLabel,
]}
onPress={() => {
removeTracker();
hideModal();
@@ -142,3 +130,26 @@ const TrackerScreen = ({ navigation }: TrackerSettingsScreenProps) => {
};
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',
},
});
@@ -1,5 +1,6 @@
import { SwitchItem } from '@components';
import { ThemeColors } from '@theme/types';
import { StyleSheet } from 'react-native';
interface SettingSwitchProps {
value: boolean;
@@ -23,7 +24,11 @@ export default function SettingSwitch({
description={description}
onPress={onPress}
theme={theme}
style={{ paddingHorizontal: 16 }}
style={styles.paddingHorizontal}
/>
);
}
const styles = StyleSheet.create({
paddingHorizontal: { paddingHorizontal: 16 },
});
+1 -1
View File
@@ -47,7 +47,7 @@ const UpdatesScreen = ({ navigation }: UpdateScreenProps) => {
getUpdates();
setIsLoading(isLoadingUpdates);
}, 0);
}, []),
}, [getUpdates, isLoadingUpdates]),
);
useEffect(
@@ -53,7 +53,7 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
const theme = useTheme();
const updateList = async () => {
const updateList = useCallback(async () => {
getDetailedUpdates(chapterListInfo.novelId, onlyDownloadedChapters).then(
res => {
if (res.length) {
@@ -61,10 +61,10 @@ const UpdateNovelCard: React.FC<UpdateCardProps> = ({
}
},
);
};
}, [chapterListInfo.novelId, getDetailedUpdates, onlyDownloadedChapters]);
useEffect(() => {
updateList();
}, []);
}, [updateList]);
const handleDownloadChapter = (chapter: Update | DownloadedChapter) => {
if (chapterListInfo.updatesPerDay) {