Fix: pre-alpha-patch-1 (#905)
* fix: wrong version checker, update lastused * fix: only show update dialog if newer version * fix: remove chapter id in chapter item * fix: date in chapter drawer * fix: decode uri for local epub * fix: simpler SetCategoriesModal * fix: not allow local as default * fix: no delay on selfhost backup/restore
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import * as SQLite from 'expo-sqlite';
|
||||
import { noop } from 'lodash-es';
|
||||
import { BackupCategory, Category, NovelCategory } from '../types';
|
||||
import { BackupCategory, Category, NovelCategory, CCategory } from '../types';
|
||||
import { showToast } from '@utils/showToast';
|
||||
import { txnErrorCallback } from '../utils/helpers';
|
||||
const db = SQLite.openDatabase('lnreader.db');
|
||||
@@ -22,6 +22,33 @@ export const getCategoriesFromDb = async (): Promise<Category[]> => {
|
||||
);
|
||||
};
|
||||
|
||||
export const getCategoriesWithCount = async (
|
||||
novelIds: number[],
|
||||
): Promise<CCategory[]> => {
|
||||
const getCategoriesWithCountQuery = `
|
||||
SELECT *, novelsCount
|
||||
FROM Category LEFT JOIN
|
||||
(
|
||||
SELECT categoryId, COUNT(novelId) as novelsCount
|
||||
FROM NovelCategory WHERE novelId in (${novelIds.join(
|
||||
',',
|
||||
)}) GROUP BY categoryId
|
||||
) as NC ON Category.id = NC.categoryId
|
||||
ORDER BY sort
|
||||
`;
|
||||
|
||||
return new Promise(resolve =>
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
getCategoriesWithCountQuery,
|
||||
[],
|
||||
(txObj, { rows }) => resolve((rows as any)._array),
|
||||
txnErrorCallback,
|
||||
);
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const createCategoryQuery = 'INSERT INTO Category (name) VALUES (?)';
|
||||
|
||||
export const createCategory = (categoryName: string): void =>
|
||||
@@ -94,7 +121,11 @@ export const isCategoryNameDuplicate = (
|
||||
|
||||
const updateCategoryOrderQuery = 'UPDATE Category SET sort = ? WHERE id = ?';
|
||||
|
||||
export const updateCategoryOrderInDb = (categories: Category[]): void =>
|
||||
export const updateCategoryOrderInDb = (categories: Category[]): void => {
|
||||
// Do not set local as default one
|
||||
if (categories.length && categories[0].id == 2) {
|
||||
return;
|
||||
}
|
||||
db.transaction(tx => {
|
||||
categories.map(category => {
|
||||
tx.executeSql(
|
||||
@@ -105,6 +136,7 @@ export const updateCategoryOrderInDb = (categories: Category[]): void =>
|
||||
);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const getAllNovelCategories = (): Promise<NovelCategory[]> => {
|
||||
return new Promise(resolve =>
|
||||
|
||||
@@ -100,9 +100,6 @@ export const switchNovelToLibrary = async (
|
||||
) => {
|
||||
const novel = await getNovel(novelUrl);
|
||||
if (novel) {
|
||||
if (novel.isLocal) {
|
||||
return;
|
||||
}
|
||||
db.transaction(tx => {
|
||||
tx.executeSql(
|
||||
'UPDATE Novel SET inLibrary = ? WHERE id = ?',
|
||||
@@ -288,45 +285,25 @@ export const updateNovelCategoryById = async (
|
||||
export const updateNovelCategories = async (
|
||||
novelIds: number[],
|
||||
categoryIds: number[],
|
||||
option: string,
|
||||
): Promise<void> => {
|
||||
let queries: string[] = [];
|
||||
// allow local novels have other categories, but not the revesre
|
||||
// not allow others have local id;
|
||||
categoryIds = categoryIds.filter(id => id !== 2);
|
||||
if (option === 'KEEP_OLD') {
|
||||
novelIds.forEach(novelId => {
|
||||
categoryIds.forEach(categoryId =>
|
||||
queries.push(
|
||||
`INSERT OR REPLACE INTO NovelCategory (novelId, categoryId) VALUES (${novelId}, ${categoryId})`,
|
||||
),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
novelIds.forEach(novelId =>
|
||||
queries.push(`DELETE FROM NovelCategory WHERE novelId = ${novelId}`),
|
||||
queries.push(
|
||||
`DELETE FROM NovelCategory WHERE novelId IN (${novelIds.join(
|
||||
',',
|
||||
)}) AND categoryId != 2`,
|
||||
);
|
||||
novelIds.forEach(novelId => {
|
||||
categoryIds.forEach(categoryId =>
|
||||
queries.push(
|
||||
`INSERT INTO NovelCategory (novelId, categoryId) VALUES (${novelId}, ${categoryId})`,
|
||||
),
|
||||
);
|
||||
novelIds.forEach(novelId => {
|
||||
categoryIds.forEach(categoryId =>
|
||||
queries.push(
|
||||
`INSERT INTO NovelCategory (novelId, categoryId) VALUES (${novelId}, ${categoryId})`,
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
db.transaction(tx => {
|
||||
queries.forEach((query, index) => {
|
||||
tx.executeSql(
|
||||
query,
|
||||
[],
|
||||
() => {
|
||||
if (index === queries.length - 1) {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
txnErrorCallback,
|
||||
);
|
||||
});
|
||||
});
|
||||
db.transaction(tx => {
|
||||
queries.forEach(query => {
|
||||
tx.executeSql(query);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -73,6 +73,10 @@ export interface NovelCategory {
|
||||
categoryId: number;
|
||||
}
|
||||
|
||||
export interface CCategory extends Category {
|
||||
novelsCount: number;
|
||||
}
|
||||
|
||||
export interface LibraryStats {
|
||||
novelsCount?: number;
|
||||
chaptersCount?: number;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { appVersion } from '../../utils/versionUtils';
|
||||
import { newer } from '@utils/compareVersion';
|
||||
|
||||
interface GithubUpdate {
|
||||
isNewVersion: boolean;
|
||||
@@ -32,9 +33,8 @@ export const useGithubUpdateChecker = (): GithubUpdate => {
|
||||
const regex = /[^\\d.]/;
|
||||
|
||||
let newVersion = versionTag.replace(regex, '');
|
||||
currentVersion = currentVersion;
|
||||
|
||||
return newVersion !== currentVersion;
|
||||
return newer(newVersion, currentVersion);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -73,6 +73,10 @@ export default function usePlugins() {
|
||||
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;
|
||||
}
|
||||
@@ -161,16 +165,17 @@ export default function usePlugins() {
|
||||
if (plugin.id !== plg.id) {
|
||||
return plg;
|
||||
}
|
||||
return {
|
||||
id: plugin.id,
|
||||
url: plugin.url,
|
||||
lang: plugin.lang,
|
||||
iconUrl: plugin.iconUrl,
|
||||
const newPlugin: PluginItem = {
|
||||
...plugin,
|
||||
site: _plg.site,
|
||||
name: _plg.name,
|
||||
version: _plg.version,
|
||||
hasUpdate: false,
|
||||
};
|
||||
if (newPlugin.id === lastUsedPlugin?.id) {
|
||||
setLastUsedPlugin(newPlugin);
|
||||
}
|
||||
return newPlugin;
|
||||
}),
|
||||
);
|
||||
filterPlugins(languagesFilter);
|
||||
|
||||
@@ -70,8 +70,7 @@ const installPlugin = async (url: string): Promise<Plugin | undefined> => {
|
||||
const oldPlugin = plugins[plugin.id];
|
||||
if (oldPlugin) {
|
||||
if (newer(plugin.version, oldPlugin.version)) {
|
||||
delete plugins[oldPlugin.id];
|
||||
plugins[plugin.id] = plugin;
|
||||
plugins[oldPlugin.id] = plugin;
|
||||
await RNFS.writeFile(plugin.path, rawCode, 'utf8');
|
||||
return plugin;
|
||||
} else {
|
||||
|
||||
@@ -43,10 +43,22 @@ const CategoriesScreen = () => {
|
||||
getCategories();
|
||||
}, []);
|
||||
const updateCategorySort = (currentIndex: number, newIndex: number) => {
|
||||
// Do not set local as default one
|
||||
if (
|
||||
(newIndex === 0 &&
|
||||
currentIndex == 1 &&
|
||||
categories?.[currentIndex].id === 2) ||
|
||||
(newIndex === 1 && currentIndex == 0 && categories?.[newIndex].id === 2)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const updatedOrderCategories = orderBy(
|
||||
categories?.map((category, index) => {
|
||||
// + 1 because in db, sort start from 1
|
||||
|
||||
// swap adjacent cards
|
||||
if (index === currentIndex) {
|
||||
return { ...category, sort: newIndex + 1 }; // + 1 because in db, sort start from 1
|
||||
return { ...category, sort: newIndex + 1 };
|
||||
}
|
||||
if (index === newIndex) {
|
||||
return { ...category, sort: currentIndex + 1 };
|
||||
|
||||
@@ -44,11 +44,14 @@ const CategoryCard: React.FC<CategoryCardProps> = ({
|
||||
style={[
|
||||
styles.cardCtn,
|
||||
{
|
||||
backgroundColor: theme.isDark
|
||||
? overlay(2, category.sort === 1 ? theme.surface2 : theme.surface)
|
||||
: category.sort === 1
|
||||
? theme.primaryContainer
|
||||
: theme.secondaryContainer,
|
||||
backgroundColor:
|
||||
category.sort === 1
|
||||
? theme.isDark
|
||||
? overlay(2, theme.surface2)
|
||||
: theme.primaryContainer
|
||||
: category.id === 2
|
||||
? theme.tertiaryContainer
|
||||
: theme.secondaryContainer,
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -114,7 +114,7 @@ const ChapterItem: React.FC<ChapterItemProps> = ({
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{showChapterTitles ? name : `Chapter ${chapNum} • ID: ${id}`}
|
||||
{showChapterTitles ? name : `Chapter ${chapNum}`}
|
||||
</Text>
|
||||
</Row>
|
||||
<View style={styles.textRow}>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Dimensions, FlatList, StyleSheet, Text, View } from 'react-native';
|
||||
import { Divider, Modal, overlay, Portal } from 'react-native-paper';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||
|
||||
import { Button } from '@components/index';
|
||||
|
||||
import { useTheme } from '@hooks/persisted';
|
||||
|
||||
import { getString } from '@strings/translations';
|
||||
import { getCategoriesFromDb } from '@database/queries/CategoryQueries';
|
||||
import { getCategoriesWithCount } from '@database/queries/CategoryQueries';
|
||||
import { updateNovelCategories } from '@database/queries/NovelQueries';
|
||||
import { Category } from '@database/types';
|
||||
import { CCategory, Category } from '@database/types';
|
||||
import { Checkbox } from '@components/Checkbox/Checkbox';
|
||||
import { xor } from 'lodash-es';
|
||||
import { RootStackParamList } from '@navigators/types';
|
||||
|
||||
interface SetCategoryModalProps {
|
||||
novelIds: number[];
|
||||
@@ -30,15 +31,14 @@ const SetCategoryModal: React.FC<SetCategoryModalProps> = ({
|
||||
onEditCategories,
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const { navigate } = useNavigation();
|
||||
const [option, setOption] = useState<string>('KEEP_OLD');
|
||||
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const [selectedCategories, setSelectedCategories] = useState<Category[]>([]);
|
||||
const [categories = [], setCategories] = useState<Category[]>();
|
||||
const [categories = [], setCategories] = useState<CCategory[]>();
|
||||
|
||||
const getCategories = async () => {
|
||||
const res = await getCategoriesFromDb();
|
||||
|
||||
const res = await getCategoriesWithCount(novelIds);
|
||||
setCategories(res);
|
||||
setSelectedCategories(res.filter(c => c.novelsCount));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -95,35 +95,13 @@ const SetCategoryModal: React.FC<SetCategoryModalProps> = ({
|
||||
backgroundColor: theme.onSurfaceDisabled,
|
||||
}}
|
||||
/>
|
||||
<View>
|
||||
<Text style={[styles.modelOption, { color: theme.onSurface }]}>
|
||||
{'Option'}
|
||||
</Text>
|
||||
<Checkbox
|
||||
status={option === 'KEEP_OLD'}
|
||||
label={'Keep old categories'}
|
||||
onPress={() => setOption('KEEP_OLD')}
|
||||
viewStyle={styles.checkboxView}
|
||||
theme={theme}
|
||||
/>
|
||||
<Checkbox
|
||||
status={option === 'REMOVE_OLD'}
|
||||
label={'Remove old categories (not selected)'}
|
||||
onPress={() => setOption('REMOVE_OLD')}
|
||||
viewStyle={styles.checkboxView}
|
||||
theme={theme}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.btnContainer}>
|
||||
<Button
|
||||
title={getString('common.edit')}
|
||||
onPress={() => {
|
||||
navigate(
|
||||
'MoreStack' as never,
|
||||
{
|
||||
screen: 'Categories',
|
||||
} as never,
|
||||
);
|
||||
navigate('MoreStack', {
|
||||
screen: 'Categories',
|
||||
});
|
||||
closeModal();
|
||||
onEditCategories?.();
|
||||
}}
|
||||
@@ -142,7 +120,6 @@ const SetCategoryModal: React.FC<SetCategoryModalProps> = ({
|
||||
await updateNovelCategories(
|
||||
novelIds,
|
||||
selectedCategories.map(category => category.id),
|
||||
option,
|
||||
);
|
||||
closeModal();
|
||||
setSelectedCategories([]);
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getString } from '@strings/translations';
|
||||
import { ChapterScreenProps } from '@navigators/types';
|
||||
import { ChapterInfo } from '@database/types';
|
||||
import { ThemeColors } from '@theme/types';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const ChapterDrawer = ({ route, navigation }: ChapterScreenProps) => {
|
||||
const theme = useTheme();
|
||||
@@ -60,29 +61,32 @@ const ChapterDrawer = ({ route, navigation }: ChapterScreenProps) => {
|
||||
chapter: item,
|
||||
});
|
||||
};
|
||||
const renderItem: FlashListProps<ChapterInfo>['renderItem'] = ({ item }) => (
|
||||
<View
|
||||
style={[
|
||||
styles.drawerElementContainer,
|
||||
item.id === chapter.id && {
|
||||
backgroundColor: color(theme.primary).alpha(0.12).string(),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() => changeChapter(item)}
|
||||
style={styles.chapterCtn}
|
||||
const renderItem: FlashListProps<ChapterInfo>['renderItem'] = ({ item }) => {
|
||||
const parsedTime = dayjs(item.releaseTime);
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.drawerElementContainer,
|
||||
item.id === chapter.id && {
|
||||
backgroundColor: color(theme.primary).alpha(0.12).string(),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Text numberOfLines={1} style={styles.chapterNameCtn}>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.readTime ? (
|
||||
<Text style={styles.releaseDateCtn}>{item.releaseTime}</Text>
|
||||
) : null}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
<Pressable
|
||||
android_ripple={{ color: theme.rippleColor }}
|
||||
onPress={() => changeChapter(item)}
|
||||
style={styles.chapterCtn}
|
||||
>
|
||||
<Text numberOfLines={1} style={styles.chapterNameCtn}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<Text style={styles.releaseDateCtn}>
|
||||
{parsedTime.isValid() ? parsedTime.format('LL') : item.readTime}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const ListFooter = () => {
|
||||
return (
|
||||
|
||||
@@ -58,7 +58,6 @@ const remoteBackupAction = async (taskData?: TaskData) => {
|
||||
})
|
||||
.then(() => subtasks[j]())
|
||||
.then(backupPackage => upload(host, backupPackage))
|
||||
.then(() => sleep(delay))
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
@@ -144,7 +143,6 @@ const remoteRestoreAction = async (taskData?: TaskData) => {
|
||||
},
|
||||
})
|
||||
.then(() => subtasks[j]())
|
||||
.then(() => sleep(delay))
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ const insertLocalChapter = (
|
||||
'INSERT INTO Chapter(novelId, name, url, releaseTime) VALUES(?, ?, ?, ?)',
|
||||
[
|
||||
novelId,
|
||||
name,
|
||||
decodeURI(name),
|
||||
// use fakeid just for make the url is unique :D
|
||||
NovelDownloadFolder + '/local/' + novelId + '/' + fakeId,
|
||||
releaseTime,
|
||||
@@ -184,7 +184,6 @@ const importEpubAction = async (taskData?: TaskData) => {
|
||||
throw e;
|
||||
});
|
||||
filePaths.forEach(filePath => filePathSet.add(filePath));
|
||||
await sleep(500);
|
||||
}
|
||||
}
|
||||
await sleep(taskData.delay);
|
||||
|
||||
@@ -4,10 +4,13 @@ export const older = (a: string, b: string): boolean => {
|
||||
const arrA = a.split('.').map(value => Number(value));
|
||||
const arrB = b.split('.').map(value => Number(value));
|
||||
try {
|
||||
for (let i = 0; i < Math.max(arrA.length, arrB.length); i++) {
|
||||
for (let i = 0; i < Math.min(arrA.length, arrB.length); i++) {
|
||||
if (arrA[i] < arrB[i]) {
|
||||
return true;
|
||||
}
|
||||
if (arrA[i] > arrB[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
return arrA.length < arrB.length;
|
||||
|
||||
Reference in New Issue
Block a user