Feat: Add features from main and fixes (#972)
* fix: download chapters filter * fix: deletechapters missing * fix: tweak chapter * optimise: chapters preprocess * fix: bookmark chapter * fix: not showToast when no network * fix: preprocess updates|history|download * fix: ScrollView for morescreen * feat: exclude download chapter filter * fix: show labels in nav by default
This commit is contained in:
@@ -8,6 +8,8 @@ import {
|
||||
deleteChapterHistory,
|
||||
getHistoryFromDb,
|
||||
} from '@database/queries/HistoryQueries';
|
||||
import dayjs from 'dayjs';
|
||||
import { parseChapterNumber } from '@utils/parseChapterNumber';
|
||||
|
||||
const useHistory = () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
@@ -16,7 +18,22 @@ const useHistory = () => {
|
||||
|
||||
const getHistory = () =>
|
||||
getHistoryFromDb()
|
||||
.then(res => setHistory(res))
|
||||
.then(res =>
|
||||
setHistory(
|
||||
res.map(history => {
|
||||
const parsedTime = dayjs(history.releaseTime);
|
||||
return {
|
||||
...history,
|
||||
releaseTime: parsedTime.isValid()
|
||||
? parsedTime.format('LL')
|
||||
: history.releaseTime,
|
||||
chapterNumber: history.chapterNumber
|
||||
? history.chapterNumber
|
||||
: parseChapterNumber(history.novelName, history.name),
|
||||
};
|
||||
}),
|
||||
),
|
||||
)
|
||||
.catch((err: Error) => setError(err.message))
|
||||
.finally(() => setIsLoading(false));
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import { NovelDownloadFolder } from '@utils/constants/download';
|
||||
import * as RNFS from 'react-native-fs';
|
||||
import { getString } from '@strings/translations';
|
||||
import { ChapterItem } from '@plugins/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { parseChapterNumber } from '@utils/parseChapterNumber';
|
||||
|
||||
// store key: '<PREFIX>_<novel.pluginId>_<novel.path>',
|
||||
// store key: '<PREFIX>_<novel.id>',
|
||||
@@ -110,9 +112,29 @@ export const useTrackedNovel = (novelId: number) => {
|
||||
export const useNovel = (novelPath: string, pluginId: string) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [novel, setNovel] = useState<NovelInfo>();
|
||||
const [chapters, setChapters] = useState<ChapterInfo[]>([]);
|
||||
const [chapters, _setChapters] = useState<ChapterInfo[]>([]);
|
||||
const [pages, setPages] = useState<string[]>([]);
|
||||
|
||||
const setChapters = useCallback(
|
||||
(chapters: ChapterInfo[]) => {
|
||||
if (novel) {
|
||||
_setChapters(
|
||||
chapters.map(chapter => {
|
||||
const parsedTime = dayjs(chapter.releaseTime);
|
||||
return {
|
||||
...chapter,
|
||||
releaseTime: parsedTime.isValid()
|
||||
? parsedTime.format('LL')
|
||||
: chapter.releaseTime,
|
||||
chapterNumber: chapter.chapterNumber
|
||||
? chapter.chapterNumber
|
||||
: parseChapterNumber(novel.name, chapter.name),
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
[novel, _setChapters],
|
||||
);
|
||||
const [pageIndex = defaultPageIndex, setPageIndex] = useMMKVNumber(`
|
||||
${NOVEL_PAGE_INDEX_PREFIX}_${pluginId}_${novelPath}
|
||||
`);
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from '@plugins/pluginManager';
|
||||
import { newer } from '@utils/compareVersion';
|
||||
import { MMKVStorage, getMMKVObject, setMMKVObject } from '@utils/mmkv/mmkv';
|
||||
import { showToast } from '@utils/showToast';
|
||||
import { useCallback } from 'react';
|
||||
import { getString } from '@strings/translations';
|
||||
|
||||
@@ -60,31 +59,29 @@ export default function usePlugins() {
|
||||
const refreshPlugins = useCallback(() => {
|
||||
const installedPlugins =
|
||||
getMMKVObject<PluginItem[]>(INSTALLED_PLUGINS) || [];
|
||||
return fetchPlugins()
|
||||
.then(fetchedPluginsMap => {
|
||||
for (const key in fetchedPluginsMap) {
|
||||
const lang = key as Language;
|
||||
fetchedPluginsMap[lang] = fetchedPluginsMap[lang].filter(plg => {
|
||||
const finded = installedPlugins.find(v => v.id === plg.id);
|
||||
if (finded) {
|
||||
if (newer(plg.version, finded.version)) {
|
||||
finded.hasUpdate = true;
|
||||
finded.iconUrl = plg.iconUrl;
|
||||
finded.url = plg.url;
|
||||
if (finded.id === lastUsedPlugin?.id) {
|
||||
setLastUsedPlugin(finded);
|
||||
}
|
||||
return fetchPlugins().then(fetchedPluginsMap => {
|
||||
for (const key in fetchedPluginsMap) {
|
||||
const lang = key as Language;
|
||||
fetchedPluginsMap[lang] = fetchedPluginsMap[lang].filter(plg => {
|
||||
const finded = installedPlugins.find(v => v.id === plg.id);
|
||||
if (finded) {
|
||||
if (newer(plg.version, finded.version)) {
|
||||
finded.hasUpdate = true;
|
||||
finded.iconUrl = plg.iconUrl;
|
||||
finded.url = plg.url;
|
||||
if (finded.id === lastUsedPlugin?.id) {
|
||||
setLastUsedPlugin(finded);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setMMKVObject(INSTALLED_PLUGINS, installedPlugins);
|
||||
setMMKVObject(AVAILABLE_PLUGINS, fetchedPluginsMap);
|
||||
filterPlugins(languagesFilter);
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => showToast(error.message));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setMMKVObject(INSTALLED_PLUGINS, installedPlugins);
|
||||
setMMKVObject(AVAILABLE_PLUGINS, fetchedPluginsMap);
|
||||
filterPlugins(languagesFilter);
|
||||
}
|
||||
});
|
||||
}, [languagesFilter]);
|
||||
|
||||
const toggleLanguageFilter = (lang: Language) => {
|
||||
|
||||
@@ -115,7 +115,7 @@ const initialAppSettings: AppSettings = {
|
||||
|
||||
showHistoryTab: true,
|
||||
showUpdatesTab: true,
|
||||
showLabelsInNav: false,
|
||||
showLabelsInNav: true,
|
||||
useFabForContinueReading: false,
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getUpdatesFromDb } from '@database/queries/ChapterQueries';
|
||||
import { Update } from '@database/types';
|
||||
import { useMMKVBoolean, useMMKVString } from 'react-native-mmkv';
|
||||
import dayjs from 'dayjs';
|
||||
import { parseChapterNumber } from '@utils/parseChapterNumber';
|
||||
|
||||
export const SHOW_LAST_UPDATE_TIME = 'SHOW_LAST_UPDATE_TIME';
|
||||
export const LAST_UPDATE_TIME = 'LAST_UPDATE_TIME';
|
||||
@@ -33,7 +34,20 @@ export const useUpdates = () => {
|
||||
const getUpdates = () =>
|
||||
getUpdatesFromDb()
|
||||
.then(res => {
|
||||
setUpdates(res);
|
||||
setUpdates(
|
||||
res.map(update => {
|
||||
const parsedTime = dayjs(update.releaseTime);
|
||||
return {
|
||||
...update,
|
||||
releaseTime: parsedTime.isValid()
|
||||
? parsedTime.format('LL')
|
||||
: update.releaseTime,
|
||||
chapterNumber: update.chapterNumber
|
||||
? update.chapterNumber
|
||||
: parseChapterNumber(update.novelName, update.name),
|
||||
};
|
||||
}),
|
||||
);
|
||||
if (res.length) {
|
||||
if (
|
||||
!lastUpdateTime ||
|
||||
|
||||
@@ -123,15 +123,14 @@ const updatePlugin = async (plugin: PluginItem) => {
|
||||
return installPlugin(plugin.url);
|
||||
};
|
||||
|
||||
const fetchPlugins = async () => {
|
||||
const fetchPlugins = (): Promise<Record<Language, Array<PluginItem>>> => {
|
||||
// plugins host
|
||||
const githubUsername = 'LNReader';
|
||||
const githubRepository = 'lnreader-sources';
|
||||
|
||||
const availablePlugins: Record<Language, Array<PluginItem>> = await fetch(
|
||||
return fetch(
|
||||
`https://raw.githubusercontent.com/${githubUsername}/${githubRepository}/beta-dist/.dist/plugins.min.json`,
|
||||
).then(res => res.json());
|
||||
return availablePlugins;
|
||||
};
|
||||
|
||||
const getPlugin = (pluginId: string) => plugins[pluginId];
|
||||
|
||||
@@ -7,6 +7,7 @@ import PluginCard from './PluginCard';
|
||||
import { usePlugins } from '@hooks/persisted';
|
||||
import { BrowseScreenProps } from '@navigators/types';
|
||||
import TrackerCard from '../discover/TrackerCard';
|
||||
import { showToast } from '@utils/showToast';
|
||||
|
||||
interface PluginSectionProps {
|
||||
sections: { header: string; data: PluginItem[] }[];
|
||||
@@ -108,7 +109,11 @@ const PluginSection = ({
|
||||
refreshing={refreshing}
|
||||
onRefresh={() => {
|
||||
setRefreshing(true);
|
||||
refreshPlugins().finally(() => setRefreshing(false));
|
||||
refreshPlugins()
|
||||
.finally(() => setRefreshing(false))
|
||||
.catch(e => {
|
||||
showToast(e);
|
||||
});
|
||||
}}
|
||||
colors={[theme.onPrimary]}
|
||||
progressBackgroundColor={theme.primary}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { Image } from 'react-native';
|
||||
|
||||
import { IconButtonV2 } from '@components';
|
||||
import { parseChapterNumber } from '@utils/parseChapterNumber';
|
||||
|
||||
import { History, NovelInfo } from '@database/types';
|
||||
import { ThemeColors } from '@theme/types';
|
||||
@@ -26,20 +25,6 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
|
||||
handleRemoveFromHistory,
|
||||
theme,
|
||||
}) => {
|
||||
const chapterNoAndTime = useMemo(
|
||||
() =>
|
||||
`${getString('historyScreen.chapter')} ${parseChapterNumber(
|
||||
history.novelName,
|
||||
history.name,
|
||||
)} • ${dayjs(history.readTime).format('LT').toUpperCase()}` +
|
||||
`${
|
||||
history.progress && history.progress > 0
|
||||
? ' • ' + history.progress + '%'
|
||||
: ''
|
||||
}`,
|
||||
[history, history.readTime],
|
||||
);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={styles.container}
|
||||
@@ -75,7 +60,14 @@ const HistoryCard: React.FC<HistoryCardProps> = ({
|
||||
{history.novelName}
|
||||
</Text>
|
||||
<Text style={{ color: theme.onSurfaceVariant }}>
|
||||
{chapterNoAndTime}
|
||||
{`${getString('historyScreen.chapter')} ${
|
||||
history.chapterNumber
|
||||
} • ${dayjs(history.readTime).format('LT').toUpperCase()}` +
|
||||
`${
|
||||
history.progress && history.progress > 0
|
||||
? ' • ' + history.progress + '%'
|
||||
: ''
|
||||
}`}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -21,6 +21,8 @@ import { getString } from '@strings/translations';
|
||||
import { DownloadsScreenProps } from '@navigators/types';
|
||||
import { DownloadedChapter } from '@database/types';
|
||||
import { showToast } from '@utils/showToast';
|
||||
import dayjs from 'dayjs';
|
||||
import { parseChapterNumber } from '@utils/parseChapterNumber';
|
||||
|
||||
type DownloadGroup = Record<number, DownloadedChapter[]>;
|
||||
|
||||
@@ -53,7 +55,20 @@ const Downloads = ({ navigation }: DownloadsScreenProps) => {
|
||||
|
||||
const getChapters = async () => {
|
||||
const res = await getDownloadedChapters();
|
||||
setChapters(res);
|
||||
setChapters(
|
||||
res.map(download => {
|
||||
const parsedTime = dayjs(download.releaseTime);
|
||||
return {
|
||||
...download,
|
||||
releaseTime: parsedTime.isValid()
|
||||
? parsedTime.format('LL')
|
||||
: download.releaseTime,
|
||||
chapterNumber: download.chapterNumber
|
||||
? download.chapterNumber
|
||||
: parseChapterNumber(download.novelName, download.name),
|
||||
};
|
||||
}),
|
||||
);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
||||
+133
-135
@@ -30,149 +30,147 @@ const MoreScreen = ({ navigation }: MoreStackScreenProps) => {
|
||||
setLibrarySettings({ incognitoMode: !incognitoMode });
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollView>
|
||||
<MoreHeader
|
||||
title={getString('more')}
|
||||
navigation={navigation}
|
||||
theme={theme}
|
||||
/>
|
||||
<ScrollView style={{ flex: 1 }}>
|
||||
<List.Section>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
onPress={enableDownloadedOnlyMode}
|
||||
>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<List.Icon theme={theme} icon="cloud-off-outline" />
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.onSurface,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{getString('moreScreen.downloadOnly')}
|
||||
</Text>
|
||||
<Text style={{ color: theme.onSurfaceVariant }}>
|
||||
{getString('moreScreen.downloadOnlyDesc')}
|
||||
</Text>
|
||||
</View>
|
||||
<List.Section>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
onPress={enableDownloadedOnlyMode}
|
||||
>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<List.Icon theme={theme} icon="cloud-off-outline" />
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.onSurface,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{getString('moreScreen.downloadOnly')}
|
||||
</Text>
|
||||
<Text style={{ color: theme.onSurfaceVariant }}>
|
||||
{getString('moreScreen.downloadOnlyDesc')}
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={downloadedOnlyMode}
|
||||
onValueChange={enableDownloadedOnlyMode}
|
||||
color={theme.primary}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
onPress={enableIncognitoMode}
|
||||
>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<List.Icon theme={theme} icon="incognito" />
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.onSurface,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{getString('moreScreen.incognitoMode')}
|
||||
</Text>
|
||||
<Text style={{ color: theme.onSurfaceVariant }}>
|
||||
{getString('moreScreen.incognitoModeDesc')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={downloadedOnlyMode}
|
||||
onValueChange={enableDownloadedOnlyMode}
|
||||
color={theme.primary}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
onPress={enableIncognitoMode}
|
||||
>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<List.Icon theme={theme} icon="incognito" />
|
||||
<View style={{ marginLeft: 16 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.onSurface,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{getString('moreScreen.incognitoMode')}
|
||||
</Text>
|
||||
<Text style={{ color: theme.onSurfaceVariant }}>
|
||||
{getString('moreScreen.incognitoModeDesc')}
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
value={incognitoMode}
|
||||
onValueChange={enableIncognitoMode}
|
||||
color={theme.primary}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title={getString('moreScreen.downloadQueue')}
|
||||
description={queue.length > 0 ? queue.length + ' remaining' : ''}
|
||||
icon="progress-download"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'DownloadQueue',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
</View>
|
||||
<Switch
|
||||
value={incognitoMode}
|
||||
onValueChange={enableIncognitoMode}
|
||||
color={theme.primary}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('common.downloads')}
|
||||
icon="folder-download"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Downloads',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('common.categories')}
|
||||
icon="label-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Categories',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('statsScreen.title')}
|
||||
icon="chart-line"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Statistics',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title={getString('common.settings')}
|
||||
icon="cog-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'SettingsStack',
|
||||
params: {
|
||||
screen: 'Settings',
|
||||
},
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('common.about')}
|
||||
icon="information-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'About',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
</ScrollView>
|
||||
</>
|
||||
</Pressable>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title={getString('moreScreen.downloadQueue')}
|
||||
description={queue.length > 0 ? queue.length + ' remaining' : ''}
|
||||
icon="progress-download"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'DownloadQueue',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('common.downloads')}
|
||||
icon="folder-download"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Downloads',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('common.categories')}
|
||||
icon="label-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Categories',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('statsScreen.title')}
|
||||
icon="chart-line"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'Statistics',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Divider theme={theme} />
|
||||
<List.Item
|
||||
title={getString('common.settings')}
|
||||
icon="cog-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'SettingsStack',
|
||||
params: {
|
||||
screen: 'Settings',
|
||||
},
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('common.about')}
|
||||
icon="information-outline"
|
||||
onPress={() =>
|
||||
navigation.navigate('MoreStack', {
|
||||
screen: 'About',
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</List.Section>
|
||||
</ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -158,7 +158,10 @@ const Novel = ({ route, navigation }: NovelScreenProps) => {
|
||||
icon: 'download-outline',
|
||||
onPress: () => {
|
||||
if (novel) {
|
||||
downloadChapters(novel, selected);
|
||||
downloadChapters(
|
||||
novel,
|
||||
selected.filter(chapter => !chapter.isDownloaded),
|
||||
);
|
||||
}
|
||||
setSelected([]);
|
||||
},
|
||||
@@ -168,6 +171,7 @@ const Novel = ({ route, navigation }: NovelScreenProps) => {
|
||||
list.push({
|
||||
icon: 'trash-can-outline',
|
||||
onPress: () => {
|
||||
deleteChapters(selected.filter(chapter => chapter.isDownloaded));
|
||||
setSelected([]);
|
||||
},
|
||||
});
|
||||
@@ -454,7 +458,10 @@ const Novel = ({ route, navigation }: NovelScreenProps) => {
|
||||
if (novel) {
|
||||
downloadChapters(
|
||||
novel,
|
||||
chapters.filter(chapter => chapter.unread),
|
||||
chapters.filter(
|
||||
chapter =>
|
||||
chapter.unread && !chapter.isDownloaded,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
@@ -467,7 +474,10 @@ const Novel = ({ route, navigation }: NovelScreenProps) => {
|
||||
}}
|
||||
onPress={() => {
|
||||
if (novel) {
|
||||
downloadChapters(novel, chapters);
|
||||
downloadChapters(
|
||||
novel,
|
||||
chapters.filter(chapter => !chapter.isDownloaded),
|
||||
);
|
||||
}
|
||||
showDownloadMenu(false);
|
||||
}}
|
||||
|
||||
@@ -1,20 +1,15 @@
|
||||
import React, { memo, ReactNode, useState } from 'react';
|
||||
import React, { memo, ReactNode } from 'react';
|
||||
import { View, Text, StyleSheet, Pressable } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
import { Row } from '@components/Common';
|
||||
|
||||
import {
|
||||
ChapterBookmarkButton,
|
||||
DownloadButton,
|
||||
} from './Chapter/ChapterDownloadButtons';
|
||||
import { parseChapterNumber } from '@utils/parseChapterNumber';
|
||||
|
||||
import { ThemeColors } from '@theme/types';
|
||||
import { ChapterInfo } from '@database/types';
|
||||
import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import dayjs from 'dayjs';
|
||||
import { getString } from '@strings/translations';
|
||||
import { useBoolean } from '@hooks';
|
||||
|
||||
interface ChapterItemProps {
|
||||
isDownloading?: boolean;
|
||||
@@ -51,14 +46,11 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
}) => {
|
||||
const { id, name, unread, releaseTime, bookmark, chapterNumber, progress } =
|
||||
chapter;
|
||||
const [deleteChapterMenuVisible, setDeleteChapterMenuVisible] =
|
||||
useState(false);
|
||||
const showDeleteChapterMenu = () => setDeleteChapterMenuVisible(true);
|
||||
const hideDeleteChapterMenu = () => setDeleteChapterMenuVisible(false);
|
||||
const chapNum = chapterNumber
|
||||
? chapterNumber
|
||||
: parseChapterNumber(novelName, name);
|
||||
const parsedTime = dayjs(releaseTime);
|
||||
const {
|
||||
value: isMenuVisible,
|
||||
setTrue: showMenu,
|
||||
setFalse: hideMenu,
|
||||
} = useBoolean();
|
||||
return (
|
||||
<Pressable
|
||||
key={'chapterItem' + id}
|
||||
@@ -74,9 +66,9 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
onLongPress={() => onSelectLongPress?.(chapter)}
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
>
|
||||
<Row style={styles.row}>
|
||||
<View style={styles.row}>
|
||||
{left}
|
||||
{!!bookmark && <ChapterBookmarkButton theme={theme} />}
|
||||
{bookmark ? <ChapterBookmarkButton theme={theme} /> : null}
|
||||
<View style={{ flex: 1 }}>
|
||||
{isUpdateCard && (
|
||||
<Text
|
||||
@@ -91,7 +83,7 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
{novelName}
|
||||
</Text>
|
||||
)}
|
||||
<Row style={styles.row}>
|
||||
<View style={styles.row}>
|
||||
{unread ? (
|
||||
<MaterialCommunityIcons
|
||||
name="circle"
|
||||
@@ -116,9 +108,11 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
>
|
||||
{showChapterTitles
|
||||
? name
|
||||
: getString('novelScreen.chapterChapnum', { num: chapNum })}
|
||||
: getString('novelScreen.chapterChapnum', {
|
||||
num: chapterNumber,
|
||||
})}
|
||||
</Text>
|
||||
</Row>
|
||||
</View>
|
||||
<View style={styles.textRow}>
|
||||
{releaseTime && !isUpdateCard ? (
|
||||
<Text
|
||||
@@ -134,7 +128,7 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{parsedTime.isValid() ? parsedTime.format('LL') : releaseTime}
|
||||
{releaseTime}
|
||||
</Text>
|
||||
) : null}
|
||||
{!isUpdateCard && progress && progress > 0 && chapter.unread ? (
|
||||
@@ -152,7 +146,7 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</Row>
|
||||
</View>
|
||||
{!isLocal && (
|
||||
<DownloadButton
|
||||
isDownloading={isDownloading}
|
||||
@@ -160,9 +154,9 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
theme={theme}
|
||||
deleteChapter={deleteChapter}
|
||||
downloadChapter={downloadChapter}
|
||||
hideDeleteChapterMenu={hideDeleteChapterMenu}
|
||||
showDeleteChapterMenu={showDeleteChapterMenu}
|
||||
deleteChapterMenuVisible={deleteChapterMenuVisible}
|
||||
hideDeleteChapterMenu={hideMenu}
|
||||
showDeleteChapterMenu={showMenu}
|
||||
deleteChapterMenuVisible={isMenuVisible}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
@@ -187,7 +181,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
marginTop: 5,
|
||||
},
|
||||
row: { flex: 1, overflow: 'hidden' },
|
||||
row: { flex: 1, flexDirection: 'row', alignItems: 'center' },
|
||||
unreadIcon: {
|
||||
marginRight: 4,
|
||||
},
|
||||
|
||||
@@ -41,12 +41,24 @@ const ChaptersSettingsSheet = ({
|
||||
<Checkbox
|
||||
theme={theme}
|
||||
label={getString('novelScreen.bottomSheet.filters.downloaded')}
|
||||
status={!!filter.match('AND isDownloaded = 1')}
|
||||
onPress={() =>
|
||||
filter.match('AND isDownloaded = 1')
|
||||
? filterChapters(filter.replace(' AND isDownloaded = 1', ''))
|
||||
: filterChapters(filter + ' AND isDownloaded = 1')
|
||||
status={
|
||||
filter.match('AND isDownloaded=1')
|
||||
? true
|
||||
: filter.match('AND isDownloaded=0')
|
||||
? 'indeterminate'
|
||||
: false
|
||||
}
|
||||
onPress={() => {
|
||||
if (filter.match('AND isDownloaded=1')) {
|
||||
filterChapters(
|
||||
filter.replace(' AND isDownloaded=1', ' AND isDownloaded=0'),
|
||||
);
|
||||
} else if (filter.match('AND isDownloaded=0')) {
|
||||
filterChapters(filter.replace(' AND isDownloaded=0', ''));
|
||||
} else {
|
||||
filterChapters(filter + ' AND isDownloaded=1');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Checkbox
|
||||
theme={theme}
|
||||
|
||||
@@ -359,7 +359,8 @@ export const ChapterContent = ({
|
||||
<>
|
||||
<ReaderAppbar
|
||||
novelName={novel.name}
|
||||
chapter={chapter}
|
||||
chapter={sourceChapter}
|
||||
setChapter={setChapter}
|
||||
goBack={navigation.goBack}
|
||||
theme={theme}
|
||||
/>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { getString } from '@strings/translations';
|
||||
import { ChapterScreenProps } from '@navigators/types';
|
||||
import { ChapterInfo } from '@database/types';
|
||||
import { ThemeColors } from '@theme/types';
|
||||
import dayjs from 'dayjs';
|
||||
import { NovelSettings } from '@hooks/persisted/useNovel';
|
||||
|
||||
type ChapterDrawerProps = ChapterScreenProps & {
|
||||
@@ -73,7 +72,6 @@ const ChapterDrawer = ({
|
||||
});
|
||||
};
|
||||
const renderItem: FlashListProps<ChapterInfo>['renderItem'] = ({ item }) => {
|
||||
const parsedTime = dayjs(item.releaseTime);
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -88,12 +86,25 @@ const ChapterDrawer = ({
|
||||
onPress={() => changeChapter(item)}
|
||||
style={styles.chapterCtn}
|
||||
>
|
||||
<Text numberOfLines={1} style={styles.chapterNameCtn}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
styles.chapterNameCtn,
|
||||
{ color: item.unread ? theme.onSurface : theme.outline },
|
||||
]}
|
||||
>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text style={styles.releaseDateCtn}>
|
||||
{parsedTime.isValid() ? parsedTime.format('LL') : item.readTime}
|
||||
</Text>
|
||||
{item.releaseTime ? (
|
||||
<Text
|
||||
style={[
|
||||
styles.releaseDateCtn,
|
||||
{ color: item.unread ? theme.onSurfaceVariant : theme.outline },
|
||||
]}
|
||||
>
|
||||
{item.releaseTime}
|
||||
</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import color from 'color';
|
||||
|
||||
@@ -11,9 +11,10 @@ import { bookmarkChapter } from '@database/queries/ChapterQueries';
|
||||
|
||||
interface ReaderAppbarProps {
|
||||
novelName: string;
|
||||
chapter: ChapterInfo;
|
||||
chapter: ChapterInfo & { chapterText: string };
|
||||
theme: ThemeColors;
|
||||
goBack: () => void;
|
||||
setChapter: (chapter: ChapterInfo & { chapterText: string }) => void;
|
||||
}
|
||||
|
||||
const ReaderAppbar = ({
|
||||
@@ -21,9 +22,8 @@ const ReaderAppbar = ({
|
||||
chapter,
|
||||
goBack,
|
||||
theme,
|
||||
setChapter,
|
||||
}: ReaderAppbarProps) => {
|
||||
const [bookmarked, setBookmarked] = useState(chapter.bookmark);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(150)}
|
||||
@@ -59,11 +59,14 @@ const ReaderAppbar = ({
|
||||
</Text>
|
||||
</View>
|
||||
<IconButtonV2
|
||||
name={bookmarked ? 'bookmark' : 'bookmark-outline'}
|
||||
name={chapter.bookmark ? 'bookmark' : 'bookmark-outline'}
|
||||
size={24}
|
||||
onPress={() => {
|
||||
bookmarkChapter(chapter.id).then(() =>
|
||||
setBookmarked(!bookmarked),
|
||||
setChapter({
|
||||
...chapter,
|
||||
bookmark: !chapter.bookmark,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
color={theme.onSurface}
|
||||
|
||||
Reference in New Issue
Block a user