Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e478b2cc8 | |||
| dcf667b176 | |||
| 3a313b9441 | |||
| ae8211a4c0 | |||
| 3452478aa1 | |||
| 0c14018996 |
@@ -45,7 +45,7 @@ body:
|
||||
label: LNReader version
|
||||
description: You can find your LNReader version in **More → About**.
|
||||
placeholder: |
|
||||
Example: "2.0.0"
|
||||
Example: "2.0.1"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -88,7 +88,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/lnreader/lnreader-sources/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[2.0.0](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[2.0.1](https://github.com/lnreader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
@@ -34,7 +34,7 @@ body:
|
||||
required: true
|
||||
- label: If this is an issue with a source, I should be opening an issue in the [sources repository](https://github.com/LNReader/lnreader-sources/issues/new/choose).
|
||||
required: true
|
||||
- label: I have updated the app to version **[2.0.0](https://github.com/LNReader/lnreader/releases/latest)**.
|
||||
- label: I have updated the app to version **[2.0.1](https://github.com/LNReader/lnreader/releases/latest)**.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "lnreader",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev:android": "pnpm run generate:env:debug && react-native run-android --appIdSuffix \"debug\" --active-arch-only",
|
||||
@@ -84,7 +84,7 @@
|
||||
"react-native-draggable-flatlist": "^4.0.3",
|
||||
"react-native-drawer-layout": "^4.1.13",
|
||||
"react-native-edge-to-edge": "^1.7.0",
|
||||
"react-native-error-boundary": "^2.0.0",
|
||||
"react-native-error-boundary": "^2.0.1",
|
||||
"react-native-file-access": "^3.2.0",
|
||||
"react-native-gesture-handler": "^2.28.0",
|
||||
"react-native-lottie-splash-screen": "^1.1.2",
|
||||
@@ -97,12 +97,12 @@
|
||||
"react-native-screens": "^4.16.0",
|
||||
"react-native-shimmer-placeholder": "^2.0.9",
|
||||
"react-native-tab-view": "^4.1.3",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-url-polyfill": "^2.0.1",
|
||||
"react-native-webview": "13.15.0",
|
||||
"react-native-worklets": "^0.5.1",
|
||||
"react-native-zip-archive": "^7.0.2",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"urlencode": "^2.0.0"
|
||||
"urlencode": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.4",
|
||||
|
||||
+50
-23
@@ -1,35 +1,62 @@
|
||||
import { SQLiteDatabase } from 'expo-sqlite';
|
||||
|
||||
import { Migration } from '../types/migration';
|
||||
|
||||
const columnExists = (
|
||||
db: SQLiteDatabase,
|
||||
tableName: string,
|
||||
columnName: string,
|
||||
): boolean => {
|
||||
const columns = db.getAllSync<{ name: string }>(
|
||||
`PRAGMA table_info(${tableName})`,
|
||||
);
|
||||
return columns.some(col => col.name === columnName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Migration 1: Add counter columns to Novel table
|
||||
* Migration 2: Add counter columns to Novel table
|
||||
* - Adds chaptersDownloaded, chaptersUnread, totalChapters columns
|
||||
* - Adds lastReadAt, lastUpdatedAt timestamp columns
|
||||
* - Populates columns with existing data
|
||||
*/
|
||||
export const migration001: Migration = {
|
||||
version: 1,
|
||||
export const migration002: Migration = {
|
||||
version: 2,
|
||||
description: 'Add counter columns and triggers to Novel table',
|
||||
migrate: db => {
|
||||
db.runSync(`
|
||||
ALTER TABLE Novel
|
||||
ADD COLUMN chaptersDownloaded INTEGER DEFAULT 0
|
||||
`);
|
||||
db.runSync(`
|
||||
ALTER TABLE Novel
|
||||
ADD COLUMN chaptersUnread INTEGER DEFAULT 0
|
||||
`);
|
||||
db.runSync(`
|
||||
ALTER TABLE Novel
|
||||
ADD COLUMN totalChapters INTEGER DEFAULT 0
|
||||
`);
|
||||
db.runSync(`
|
||||
ALTER TABLE Novel
|
||||
ADD COLUMN lastReadAt TEXT
|
||||
`);
|
||||
db.runSync(`
|
||||
ALTER TABLE Novel
|
||||
ADD COLUMN lastUpdatedAt TEXT
|
||||
`);
|
||||
if (!columnExists(db, 'Novel', 'chaptersDownloaded')) {
|
||||
db.runSync(`
|
||||
ALTER TABLE Novel
|
||||
ADD COLUMN chaptersDownloaded INTEGER DEFAULT 0
|
||||
`);
|
||||
}
|
||||
|
||||
if (!columnExists(db, 'Novel', 'chaptersUnread')) {
|
||||
db.runSync(`
|
||||
ALTER TABLE Novel
|
||||
ADD COLUMN chaptersUnread INTEGER DEFAULT 0
|
||||
`);
|
||||
}
|
||||
|
||||
if (!columnExists(db, 'Novel', 'totalChapters')) {
|
||||
db.runSync(`
|
||||
ALTER TABLE Novel
|
||||
ADD COLUMN totalChapters INTEGER DEFAULT 0
|
||||
`);
|
||||
}
|
||||
|
||||
if (!columnExists(db, 'Novel', 'lastReadAt')) {
|
||||
db.runSync(`
|
||||
ALTER TABLE Novel
|
||||
ADD COLUMN lastReadAt TEXT
|
||||
`);
|
||||
}
|
||||
|
||||
if (!columnExists(db, 'Novel', 'lastUpdatedAt')) {
|
||||
db.runSync(`
|
||||
ALTER TABLE Novel
|
||||
ADD COLUMN lastUpdatedAt TEXT
|
||||
`);
|
||||
}
|
||||
|
||||
db.runSync(`
|
||||
UPDATE Novel
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Migration } from '../types/migration';
|
||||
import { migration001 } from './001_add_novel_counters';
|
||||
import { migration002 } from './002_add_novel_counters';
|
||||
|
||||
/**
|
||||
* Registry of all database migrations
|
||||
@@ -10,4 +10,4 @@ import { migration001 } from './001_add_novel_counters';
|
||||
* 3. Import and add it to the migrations array below
|
||||
* 4. Ensure version numbers are sequential
|
||||
*/
|
||||
export const migrations: Migration[] = [migration001];
|
||||
export const migrations: Migration[] = [migration002];
|
||||
|
||||
@@ -104,7 +104,7 @@ export const getAllNovelCategories = () =>
|
||||
|
||||
export const _restoreCategory = (category: BackupCategory) => {
|
||||
const d = category.novelIds.map(novelId => [
|
||||
'INSERT INTO NovelCategory (categoryId, novelId) VALUES (?, ?)',
|
||||
'INSERT OR IGNORE INTO NovelCategory (categoryId, novelId) VALUES (?, ?)',
|
||||
[category.id, novelId],
|
||||
]) as Array<[string, SQLite.SQLiteBindParams]>;
|
||||
|
||||
@@ -114,7 +114,7 @@ export const _restoreCategory = (category: BackupCategory) => {
|
||||
[category.id, category.sort],
|
||||
],
|
||||
[
|
||||
'INSERT INTO Category (id, name, sort) VALUES (?, ?, ?)',
|
||||
'INSERT OR IGNORE INTO Category (id, name, sort) VALUES (?, ?, ?)',
|
||||
[category.id, category.name, category.sort],
|
||||
],
|
||||
...d,
|
||||
|
||||
@@ -22,34 +22,54 @@ export const insertChapters = async (
|
||||
return;
|
||||
}
|
||||
|
||||
await db
|
||||
.withTransactionAsync(async () => {
|
||||
const statement = db.prepareSync(`
|
||||
INSERT INTO Chapter (path, name, releaseTime, novelId, chapterNumber, page, position)
|
||||
VALUES (?, ?, ?, ${novelId}, ?, ?, ?)
|
||||
ON CONFLICT(path, novelId) DO UPDATE SET
|
||||
page = excluded.page,
|
||||
position = excluded.position,
|
||||
name = excluded.name,
|
||||
releaseTime = excluded.releaseTime,
|
||||
chapterNumber = excluded.chapterNumber;
|
||||
`);
|
||||
try {
|
||||
chapters.map((chapter, index) =>
|
||||
statement.executeSync(
|
||||
chapter.path,
|
||||
chapter.name ?? 'Chapter ' + (index + 1),
|
||||
chapter.releaseTime || '',
|
||||
chapter.chapterNumber || null,
|
||||
chapter.page || '1',
|
||||
index,
|
||||
),
|
||||
await db.withExclusiveTransactionAsync(async tx => {
|
||||
for (let index = 0; index < chapters.length; index++) {
|
||||
const chapter = chapters[index];
|
||||
const chapterName = chapter.name ?? 'Chapter ' + (index + 1);
|
||||
const chapterPage = chapter.page || '1';
|
||||
|
||||
const result = await tx.runAsync(
|
||||
`
|
||||
INSERT INTO Chapter (path, name, releaseTime, novelId, chapterNumber, page, position)
|
||||
SELECT ?, ?, ?, ?, ?, ?, ?
|
||||
WHERE NOT EXISTS (SELECT id FROM Chapter WHERE path = ? AND novelId = ?);
|
||||
`,
|
||||
chapter.path,
|
||||
chapterName,
|
||||
chapter.releaseTime || '',
|
||||
novelId,
|
||||
chapter.chapterNumber || null,
|
||||
chapterPage,
|
||||
index,
|
||||
chapter.path,
|
||||
novelId,
|
||||
);
|
||||
|
||||
const insertId = result.lastInsertRowId;
|
||||
|
||||
if (!insertId || insertId < 0) {
|
||||
await tx.runAsync(
|
||||
`
|
||||
UPDATE Chapter SET
|
||||
page = ?, position = ?, name = ?, releaseTime = ?, chapterNumber = ?
|
||||
WHERE path = ? AND novelId = ? AND (page != ? OR position != ? OR name != ? OR releaseTime != ? OR chapterNumber != ?);
|
||||
`,
|
||||
chapterPage,
|
||||
index,
|
||||
chapterName,
|
||||
chapter.releaseTime || '',
|
||||
chapter.chapterNumber || null,
|
||||
chapter.path,
|
||||
novelId,
|
||||
chapterPage,
|
||||
index,
|
||||
chapterName,
|
||||
chapter.releaseTime || '',
|
||||
chapter.chapterNumber || null,
|
||||
);
|
||||
} finally {
|
||||
statement.finalizeSync();
|
||||
}
|
||||
})
|
||||
.catch();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const markChapterRead = (chapterId: number) =>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const insertNovelAndChapters = async (
|
||||
sourceNovel: SourceNovel,
|
||||
): Promise<number | undefined> => {
|
||||
const insertNovelQuery =
|
||||
'INSERT INTO Novel (path, pluginId, name, cover, summary, author, artist, status, genres, totalPages) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
'INSERT OR IGNORE INTO Novel (path, pluginId, name, cover, summary, author, artist, status, genres, totalPages) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)';
|
||||
const novelId: number | undefined = db.runSync(insertNovelQuery, [
|
||||
sourceNovel.path,
|
||||
pluginId,
|
||||
|
||||
@@ -17,7 +17,7 @@ export const createCategoryTriggerQuery = `
|
||||
|
||||
// if category with id = 2 exists, nothing in db.ts file is executed
|
||||
export const createCategoryDefaultQuery = `
|
||||
INSERT INTO Category (id, name, sort) VALUES
|
||||
INSERT OR IGNORE INTO Category (id, name, sort) VALUES
|
||||
(1, "${getString('categories.default')}", 1),
|
||||
(2, "${getString('categories.local')}", 2)
|
||||
`;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
|
||||
import { getCategoriesFromDb } from '@database/queries/CategoryQueries';
|
||||
@@ -9,6 +9,8 @@ import { Category, NovelInfo } from '@database/types';
|
||||
import { useLibrarySettings } from '@hooks/persisted';
|
||||
import { LibrarySortOrder } from '../constants/constants';
|
||||
import { switchNovelToLibraryQuery } from '@database/queries/NovelQueries';
|
||||
import ServiceManager, { BackgroundTask } from '@services/ServiceManager';
|
||||
import { useMMKVObject } from 'react-native-mmkv';
|
||||
|
||||
// type Library = Category & { novels: LibraryNovelInfo[] };
|
||||
export type ExtendedCategory = Category & { novelIds: number[] };
|
||||
@@ -104,6 +106,28 @@ export const useLibrary = (): UseLibraryReturnType => {
|
||||
getLibrary();
|
||||
});
|
||||
|
||||
const [taskQueue] = useMMKVObject<BackgroundTask[]>(
|
||||
ServiceManager.manager.STORE_KEY,
|
||||
);
|
||||
const restoreTasksCount = useMemo(
|
||||
() =>
|
||||
taskQueue?.filter(
|
||||
t =>
|
||||
t.name === 'LOCAL_RESTORE' ||
|
||||
t.name === 'DRIVE_RESTORE' ||
|
||||
t.name === 'SELF_HOST_RESTORE',
|
||||
).length || 0,
|
||||
[taskQueue],
|
||||
);
|
||||
const prevRestoreTasksCountRef = useRef(restoreTasksCount);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevRestoreTasksCountRef.current > 0 && restoreTasksCount === 0) {
|
||||
getLibrary();
|
||||
}
|
||||
prevRestoreTasksCountRef.current = restoreTasksCount;
|
||||
}, [restoreTasksCount]);
|
||||
|
||||
return {
|
||||
library,
|
||||
categories,
|
||||
|
||||
@@ -329,7 +329,7 @@ const NovelScreenList = ({
|
||||
ListFooterComponent={
|
||||
!fetching ? (
|
||||
hasMultiplePages ? (
|
||||
<View style={styles.pageNavigationWrapper}>
|
||||
<View>
|
||||
<PagePaginationControl
|
||||
currentPage={pageIndex + 1}
|
||||
totalPages={pages.length}
|
||||
|
||||
@@ -5,10 +5,7 @@ import { useBoolean } from '@hooks';
|
||||
import { BackupSettingsScreenProps } from '@navigators/types';
|
||||
import GoogleDriveModal from './Components/GoogleDriveModal';
|
||||
import SelfHostModal from './Components/SelfHostModal';
|
||||
import {
|
||||
createBackup as createLocalBackup,
|
||||
restoreBackup as restoreLocalBackup,
|
||||
} from '@services/backup/local';
|
||||
import ServiceManager from '@services/ServiceManager';
|
||||
import { ScrollView } from 'react-native-gesture-handler';
|
||||
import { getString } from '@strings/translations';
|
||||
import { StyleSheet } from 'react-native';
|
||||
@@ -58,13 +55,17 @@ const BackupSettings = ({ navigation }: BackupSettingsScreenProps) => {
|
||||
<List.Item
|
||||
title={getString('backupScreen.createBackup')}
|
||||
description={getString('backupScreen.createBackupDesc')}
|
||||
onPress={createLocalBackup}
|
||||
onPress={() => {
|
||||
ServiceManager.manager.addTask({ name: 'LOCAL_BACKUP' });
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('backupScreen.restoreBackup')}
|
||||
description={getString('backupScreen.restoreBackupDesc')}
|
||||
onPress={restoreLocalBackup}
|
||||
onPress={() => {
|
||||
ServiceManager.manager.addTask({ name: 'LOCAL_RESTORE' });
|
||||
}}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.InfoItem
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
SelfHostData,
|
||||
selfHostRestore,
|
||||
} from './backup/selfhost';
|
||||
import { createBackup, restoreBackup } from './backup/local';
|
||||
import { migrateNovel, MigrateNovelData } from './migrate/migrateNovel';
|
||||
import { downloadChapter } from './download/downloadChapter';
|
||||
import { askForPostNotificationsPermission } from '@utils/askForPostNoftificationsPermission';
|
||||
@@ -23,6 +24,8 @@ type taskNames =
|
||||
| 'DRIVE_RESTORE'
|
||||
| 'SELF_HOST_BACKUP'
|
||||
| 'SELF_HOST_RESTORE'
|
||||
| 'LOCAL_BACKUP'
|
||||
| 'LOCAL_RESTORE'
|
||||
| 'MIGRATE_NOVEL'
|
||||
| 'DOWNLOAD_CHAPTER';
|
||||
|
||||
@@ -45,6 +48,8 @@ export type BackgroundTask =
|
||||
| { name: 'DRIVE_RESTORE'; data: DriveFile }
|
||||
| { name: 'SELF_HOST_BACKUP'; data: SelfHostData }
|
||||
| { name: 'SELF_HOST_RESTORE'; data: SelfHostData }
|
||||
| { name: 'LOCAL_BACKUP' }
|
||||
| { name: 'LOCAL_RESTORE' }
|
||||
| { name: 'MIGRATE_NOVEL'; data: MigrateNovelData }
|
||||
| DownloadChapterTask;
|
||||
export type DownloadChapterTask = {
|
||||
@@ -233,6 +238,10 @@ export default class ServiceManager {
|
||||
return createSelfHostBackup(task.task.data, this.setMeta.bind(this));
|
||||
case 'SELF_HOST_RESTORE':
|
||||
return selfHostRestore(task.task.data, this.setMeta.bind(this));
|
||||
case 'LOCAL_BACKUP':
|
||||
return createBackup(this.setMeta.bind(this));
|
||||
case 'LOCAL_RESTORE':
|
||||
return restoreBackup(this.setMeta.bind(this));
|
||||
case 'MIGRATE_NOVEL':
|
||||
return migrateNovel(task.task.data, this.setMeta.bind(this));
|
||||
case 'DOWNLOAD_CHAPTER':
|
||||
@@ -250,6 +259,8 @@ export default class ServiceManager {
|
||||
'DRIVE_RESTORE': 0,
|
||||
'SELF_HOST_BACKUP': 0,
|
||||
'SELF_HOST_RESTORE': 0,
|
||||
'LOCAL_BACKUP': 0,
|
||||
'LOCAL_RESTORE': 0,
|
||||
'MIGRATE_NOVEL': 0,
|
||||
'DOWNLOAD_CHAPTER': 0,
|
||||
};
|
||||
@@ -286,7 +297,7 @@ export default class ServiceManager {
|
||||
if (manager.getTaskList().length === 0) {
|
||||
await Notifications.scheduleNotificationAsync({
|
||||
content: {
|
||||
title: 'Background tasks done',
|
||||
title: getString('common.done'),
|
||||
body: Object.keys(doneTasks)
|
||||
.filter(key => doneTasks[key as BackgroundTask['name']] > 0)
|
||||
.map(
|
||||
@@ -305,24 +316,36 @@ export default class ServiceManager {
|
||||
getTaskName(task: BackgroundTask) {
|
||||
switch (task.name) {
|
||||
case 'DOWNLOAD_CHAPTER':
|
||||
return 'Download ' + task.data.novelName;
|
||||
return `${getString('notifications.DOWNLOAD_CHAPTER')}: ${
|
||||
task.data.novelName
|
||||
}`;
|
||||
case 'IMPORT_EPUB':
|
||||
return 'Import Epub ' + task.data.filename;
|
||||
return `${getString('notifications.IMPORT_EPUB')}: ${
|
||||
task.data.filename
|
||||
}`;
|
||||
case 'MIGRATE_NOVEL':
|
||||
return 'Migrate Novel ' + task.data.fromNovel.name;
|
||||
return `${getString('notifications.MIGRATE_NOVEL')}: ${
|
||||
task.data.fromNovel.name
|
||||
}`;
|
||||
case 'UPDATE_LIBRARY':
|
||||
if (task.data !== undefined) {
|
||||
return 'Update Category ' + task.data.categoryName;
|
||||
return `${getString('notifications.UPDATE_LIBRARY')}: ${
|
||||
task.data.categoryName
|
||||
}`;
|
||||
}
|
||||
return 'Update Library';
|
||||
return getString('notifications.UPDATE_LIBRARY');
|
||||
case 'DRIVE_BACKUP':
|
||||
return 'Drive Backup';
|
||||
return getString('notifications.DRIVE_BACKUP');
|
||||
case 'DRIVE_RESTORE':
|
||||
return 'Drive Restore';
|
||||
return getString('notifications.DRIVE_RESTORE');
|
||||
case 'SELF_HOST_BACKUP':
|
||||
return 'Self Host Backup';
|
||||
return getString('notifications.SELF_HOST_BACKUP');
|
||||
case 'SELF_HOST_RESTORE':
|
||||
return 'Self Host Restore';
|
||||
return getString('notifications.SELF_HOST_RESTORE');
|
||||
case 'LOCAL_BACKUP':
|
||||
return getString('notifications.LOCAL_BACKUP');
|
||||
case 'LOCAL_RESTORE':
|
||||
return getString('notifications.LOCAL_RESTORE');
|
||||
default:
|
||||
return 'Unknown Task';
|
||||
}
|
||||
|
||||
@@ -12,17 +12,53 @@ import { ROOT_STORAGE } from '@utils/Storages';
|
||||
import { ZipBackupName } from '../types';
|
||||
import NativeFile from '@specs/NativeFile';
|
||||
import { getString } from '@strings/translations';
|
||||
import { BackgroundTaskMetadata } from '@services/ServiceManager';
|
||||
import { sleep } from '@utils/sleep';
|
||||
|
||||
export const createBackup = async () => {
|
||||
export const createBackup = async (
|
||||
setMeta?: (
|
||||
transformer: (meta: BackgroundTaskMetadata) => BackgroundTaskMetadata,
|
||||
) => void,
|
||||
) => {
|
||||
try {
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
isRunning: true,
|
||||
progress: 0 / 4,
|
||||
progressText: getString('backupScreen.preparingData'),
|
||||
}));
|
||||
|
||||
await prepareBackupData(CACHE_DIR_PATH);
|
||||
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
progress: 1 / 4,
|
||||
progressText: getString('backupScreen.uploadingDownloadedFiles'),
|
||||
}));
|
||||
|
||||
await sleep(200);
|
||||
|
||||
await NativeZipArchive.zip(
|
||||
ROOT_STORAGE,
|
||||
CACHE_DIR_PATH + '/' + ZipBackupName.DOWNLOAD,
|
||||
);
|
||||
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
progress: 2 / 4,
|
||||
progressText: getString('backupScreen.uploadingData'),
|
||||
}));
|
||||
|
||||
await sleep(200);
|
||||
|
||||
await NativeZipArchive.zip(CACHE_DIR_PATH, CACHE_DIR_PATH + '.zip');
|
||||
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
progress: 3 / 4,
|
||||
progressText: getString('backupScreen.savingBackup'),
|
||||
}));
|
||||
|
||||
const datetime = dayjs().format('YYYY-MM-DD_HH_mm');
|
||||
const fileName = 'lnreader_backup_' + datetime + '.zip';
|
||||
|
||||
@@ -33,14 +69,35 @@ export const createBackup = async () => {
|
||||
fileName,
|
||||
});
|
||||
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
progress: 4 / 4,
|
||||
isRunning: false,
|
||||
}));
|
||||
|
||||
showToast(getString('backupScreen.backupCreated'));
|
||||
} catch (error: any) {
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
isRunning: false,
|
||||
}));
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
export const restoreBackup = async () => {
|
||||
export const restoreBackup = async (
|
||||
setMeta?: (
|
||||
transformer: (meta: BackgroundTaskMetadata) => BackgroundTaskMetadata,
|
||||
) => void,
|
||||
) => {
|
||||
try {
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
isRunning: true,
|
||||
progress: 0 / 4,
|
||||
progressText: getString('backupScreen.downloadingData'),
|
||||
}));
|
||||
|
||||
const [result] = await pick({
|
||||
mode: 'import',
|
||||
type: [types.zip],
|
||||
@@ -66,19 +123,52 @@ export const restoreBackup = async () => {
|
||||
|
||||
const localPath = localRes.localUri.replace(/^file:(\/\/)?\//, '/');
|
||||
|
||||
// TODO: error handling
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
progress: 1 / 4,
|
||||
progressText: getString('backupScreen.restoringData'),
|
||||
}));
|
||||
|
||||
await sleep(200);
|
||||
|
||||
await NativeZipArchive.unzip(localPath, CACHE_DIR_PATH);
|
||||
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
progress: 2 / 4,
|
||||
progressText: getString('backupScreen.restoringData'),
|
||||
}));
|
||||
|
||||
await sleep(200);
|
||||
|
||||
await restoreData(CACHE_DIR_PATH);
|
||||
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
progress: 3 / 4,
|
||||
progressText: getString('backupScreen.downloadingDownloadedFiles'),
|
||||
}));
|
||||
|
||||
await sleep(200);
|
||||
|
||||
// TODO: unlink here too?
|
||||
await NativeZipArchive.unzip(
|
||||
CACHE_DIR_PATH + '/' + ZipBackupName.DOWNLOAD,
|
||||
ROOT_STORAGE,
|
||||
);
|
||||
|
||||
await restoreData(CACHE_DIR_PATH);
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
progress: 4 / 4,
|
||||
isRunning: false,
|
||||
}));
|
||||
|
||||
showToast(getString('backupScreen.backupRestored'));
|
||||
} catch (error: any) {
|
||||
setMeta?.(meta => ({
|
||||
...meta,
|
||||
isRunning: false,
|
||||
}));
|
||||
showToast(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -69,30 +69,25 @@ const updateNovelChapters = (
|
||||
chapterNumber,
|
||||
} = chapters[position];
|
||||
const chapterPage = page || customPage || '1';
|
||||
const st = await tx.prepareAsync(
|
||||
|
||||
const result = await tx.runAsync(
|
||||
`
|
||||
INSERT INTO Chapter (path, name, releaseTime, novelId, updatedTime, chapterNumber, page, position)
|
||||
SELECT ?, ?, ?, ?, datetime('now','localtime'), ?, ?, ?
|
||||
WHERE NOT EXISTS (SELECT id FROM Chapter WHERE path = ? AND novelId = ?);
|
||||
`,
|
||||
path,
|
||||
name,
|
||||
releaseTime || null,
|
||||
novelId,
|
||||
chapterNumber || null,
|
||||
chapterPage,
|
||||
position,
|
||||
path,
|
||||
novelId,
|
||||
);
|
||||
let insertId = -1;
|
||||
try {
|
||||
const result = await st.executeAsync([
|
||||
path,
|
||||
name,
|
||||
releaseTime || null,
|
||||
novelId,
|
||||
chapterNumber || null,
|
||||
chapterPage,
|
||||
position,
|
||||
path,
|
||||
novelId,
|
||||
]);
|
||||
insertId = result.lastInsertRowId;
|
||||
} finally {
|
||||
await st.finalizeAsync();
|
||||
}
|
||||
|
||||
const insertId = result.lastInsertRowId;
|
||||
|
||||
if (insertId && insertId >= 0) {
|
||||
if (downloadNewChapters) {
|
||||
@@ -108,26 +103,25 @@ const updateNovelChapters = (
|
||||
} else {
|
||||
await tx.runAsync(
|
||||
`
|
||||
UPDATE Chapter SET
|
||||
name = ?, releaseTime = ?, updatedTime = datetime('now','localtime'), page = ?, position = ?
|
||||
WHERE path = ? AND novelId = ? AND (name != ? OR releaseTime != ? OR page != ? OR position != ?);
|
||||
`,
|
||||
[
|
||||
name,
|
||||
releaseTime || null,
|
||||
chapterPage,
|
||||
position,
|
||||
path,
|
||||
novelId,
|
||||
name,
|
||||
releaseTime || null,
|
||||
chapterPage,
|
||||
position,
|
||||
],
|
||||
UPDATE Chapter SET
|
||||
name = ?, releaseTime = ?, updatedTime = datetime('now','localtime'), page = ?, position = ?
|
||||
WHERE path = ? AND novelId = ? AND (name != ? OR releaseTime != ? OR page != ? OR position != ?);
|
||||
`,
|
||||
name,
|
||||
releaseTime || null,
|
||||
chapterPage,
|
||||
position,
|
||||
path,
|
||||
novelId,
|
||||
name,
|
||||
releaseTime || null,
|
||||
chapterPage,
|
||||
position,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export interface UpdateNovelOptions {
|
||||
downloadNewChapters?: boolean;
|
||||
refreshNovelMetadata?: boolean;
|
||||
|
||||
@@ -74,24 +74,25 @@
|
||||
"backupName": "Backup name",
|
||||
"backupCreated": "Backup created successfully",
|
||||
"backupRestored": "Backup restored successfully",
|
||||
"savingBackup": "Saving Backup",
|
||||
"categoriesRestored": "Restored %{count} categories",
|
||||
"createBackup": "Create backup",
|
||||
"createBackupDesc": "Can be used to restore current library",
|
||||
"createBackupWarning": "Create backup may not work on devices with Android 9 or lower.",
|
||||
"downloadingData": "Downloading Data",
|
||||
"downloadingDownloadedFiles": "Downloading Downloaded files",
|
||||
"downloadingDownloadedFiles": "Downloading Downloaded Files",
|
||||
"failed": "Backup failed",
|
||||
"novelsRestored": "Restored %{count} novels",
|
||||
"restoringCategories": "Restoring categories...",
|
||||
"restoringNovels": "Restoring novels...",
|
||||
"restoringSettings": "Restoring settings...",
|
||||
"restoringCategories": "Restoring Categories",
|
||||
"restoringNovels": "Restoring Novels",
|
||||
"restoringSettings": "Restoring Settings",
|
||||
"settingsRestored": "Settings restored",
|
||||
"drive": {
|
||||
"backup": "Drive Backup",
|
||||
"backupInterruped": "Drive Backup Interruped",
|
||||
"backupInterruped": "Drive Backup Interrupted",
|
||||
"googleDriveBackup": "Google Drive Backup",
|
||||
"restore": "Drive Restore",
|
||||
"restoreInterruped": "Drive Restore Interruped"
|
||||
"restoreInterruped": "Drive Restore Interrupted"
|
||||
},
|
||||
"googeDrive": "Googe Drive",
|
||||
"googeDriveDesc": "Backup to your Google Drive",
|
||||
@@ -113,7 +114,7 @@
|
||||
"selfHost": "Self Host",
|
||||
"selfHostDesc": "Backup to your server",
|
||||
"uploadingData": "Uploading Data",
|
||||
"uploadingDownloadedFiles": "Uploading Downloaded files"
|
||||
"uploadingDownloadedFiles": "Uploading Downloaded Files"
|
||||
},
|
||||
"browse": "Browse",
|
||||
"browseScreen": {
|
||||
@@ -545,13 +546,15 @@
|
||||
"complete": "Complete"
|
||||
},
|
||||
"notifications": {
|
||||
"IMPORT_EPUB": "Imported epub",
|
||||
"UPDATE_LIBRARY": "Updated library",
|
||||
"DRIVE_BACKUP": "Drive backup",
|
||||
"DRIVE_RESTORE": "Drive restore",
|
||||
"SELF_HOST_BACKUP": "Self host backup",
|
||||
"SELF_HOST_RESTORE": "Self host restore",
|
||||
"MIGRATE_NOVEL": "Migrated novel",
|
||||
"DOWNLOAD_CHAPTER": "Downloaded chapter"
|
||||
"IMPORT_EPUB": "Importing EPUB",
|
||||
"UPDATE_LIBRARY": "Updating Library",
|
||||
"DRIVE_BACKUP": "Google Drive Backup",
|
||||
"DRIVE_RESTORE": "Google Drive Restore",
|
||||
"SELF_HOST_BACKUP": "Self-Host Backup",
|
||||
"SELF_HOST_RESTORE": "Self-Host Restore",
|
||||
"LOCAL_BACKUP": "Local Backup",
|
||||
"LOCAL_RESTORE": "Local Restore",
|
||||
"MIGRATE_NOVEL": "Migrating Novel",
|
||||
"DOWNLOAD_CHAPTER": "Downloading Chapter"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface StringMap {
|
||||
'backupScreen.backupName': 'string';
|
||||
'backupScreen.backupCreated': 'string';
|
||||
'backupScreen.backupRestored': 'string';
|
||||
'backupScreen.savingBackup': 'string';
|
||||
'backupScreen.categoriesRestored': 'string';
|
||||
'backupScreen.createBackup': 'string';
|
||||
'backupScreen.createBackupDesc': 'string';
|
||||
@@ -452,6 +453,8 @@ export interface StringMap {
|
||||
'notifications.DRIVE_RESTORE': 'string';
|
||||
'notifications.SELF_HOST_BACKUP': 'string';
|
||||
'notifications.SELF_HOST_RESTORE': 'string';
|
||||
'notifications.LOCAL_BACKUP': 'string';
|
||||
'notifications.LOCAL_RESTORE': 'string';
|
||||
'notifications.MIGRATE_NOVEL': 'string';
|
||||
'notifications.DOWNLOAD_CHAPTER': 'string';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user