chore: fix all lint warnings (#1510)
* fix all lint warnings * fix types
This commit is contained in:
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -14,7 +14,7 @@ const useSearch = (defaultSearchText?: string, clearSearchOnUnfocus = true) => {
|
||||
clearSearchbar();
|
||||
}
|
||||
}
|
||||
}, [isFocused, clearSearchbar]);
|
||||
}, [isFocused, clearSearchbar, clearSearchOnUnfocus]);
|
||||
|
||||
return { searchText, setSearchText, clearSearchbar };
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -55,7 +55,7 @@ export const useTheme = (): ThemeColors => {
|
||||
};
|
||||
|
||||
return colors;
|
||||
}, [appTheme?.id, isAmoledBlack, customAccent]);
|
||||
}, [appTheme, isAmoledBlack, customAccent]);
|
||||
|
||||
return theme;
|
||||
};
|
||||
|
||||
@@ -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,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 };
|
||||
};
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ export const useLibrary = (): UseLibraryReturnType => {
|
||||
|
||||
setLibrary(novels);
|
||||
},
|
||||
[downloadedOnlyMode, filter, searchText, sortOrder],
|
||||
[downloadedOnlyMode, filter, refreshCategories, searchText, sortOrder],
|
||||
);
|
||||
|
||||
useFocusEffect(() => {
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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%',
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user