@@ -8,11 +8,28 @@ import {
|
||||
inArray,
|
||||
exists,
|
||||
notExists,
|
||||
isNotNull,
|
||||
ne,
|
||||
} from 'drizzle-orm';
|
||||
import { dbManager } from '@database/db';
|
||||
import { novelSchema, novelCategorySchema } from '@database/schema';
|
||||
import { castInt } from '@database/manager/manager';
|
||||
import type { GlobalUpdateCategoryFilters } from '@hooks/persisted/useSettings';
|
||||
import type {
|
||||
GlobalUpdateCategoryFilters,
|
||||
SmartUpdateFilters,
|
||||
} from '@hooks/persisted/useSettings';
|
||||
|
||||
const smartUpdateConditions = ({
|
||||
skipCompleted,
|
||||
skipUnstarted,
|
||||
skipWithUnread,
|
||||
}: SmartUpdateFilters) => [
|
||||
skipWithUnread
|
||||
? sql`COALESCE(${novelSchema.chaptersUnread}, 0) = 0`
|
||||
: undefined,
|
||||
skipUnstarted ? isNotNull(novelSchema.lastReadAt) : undefined,
|
||||
skipCompleted ? ne(novelSchema.status, 'Completed') : undefined,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get library novels with optional filtering and sorting using Drizzle ORM
|
||||
@@ -55,8 +72,12 @@ export const getLibraryNovelsFromDb = (
|
||||
* An empty include list means all categories, while exclusions always win.
|
||||
*/
|
||||
export const getLibraryNovelsForGlobalUpdate = (
|
||||
onlyUpdateOngoingNovels: boolean,
|
||||
{ includedCategoryIds, excludedCategoryIds }: GlobalUpdateCategoryFilters,
|
||||
smartUpdateFilters: SmartUpdateFilters = {
|
||||
skipCompleted: false,
|
||||
skipUnstarted: false,
|
||||
skipWithUnread: false,
|
||||
},
|
||||
) => {
|
||||
const includedNovel = includedCategoryIds.length
|
||||
? dbManager
|
||||
@@ -88,9 +109,9 @@ export const getLibraryNovelsForGlobalUpdate = (
|
||||
and(
|
||||
eq(novelSchema.inLibrary, true),
|
||||
eq(novelSchema.isLocal, false),
|
||||
onlyUpdateOngoingNovels ? eq(novelSchema.status, 'Ongoing') : undefined,
|
||||
includedNovel ? exists(includedNovel) : undefined,
|
||||
excludedNovel ? notExists(excludedNovel) : undefined,
|
||||
...smartUpdateConditions(smartUpdateFilters),
|
||||
),
|
||||
)
|
||||
.all();
|
||||
@@ -101,7 +122,6 @@ export const getLibraryNovelsForGlobalUpdate = (
|
||||
*/
|
||||
export const getLibraryWithCategory = async (
|
||||
categoryId?: number | null,
|
||||
onlyUpdateOngoingNovels?: boolean,
|
||||
excludeLocalNovels?: boolean,
|
||||
) => {
|
||||
// First, get novel IDs associated with the specified category
|
||||
@@ -131,7 +151,6 @@ export const getLibraryWithCategory = async (
|
||||
eq(novelSchema.inLibrary, true),
|
||||
inArray(novelSchema.id, novelIds),
|
||||
excludeLocalNovels ? eq(novelSchema.isLocal, false) : undefined,
|
||||
onlyUpdateOngoingNovels ? eq(novelSchema.status, 'Ongoing') : undefined,
|
||||
),
|
||||
)
|
||||
.all();
|
||||
|
||||
@@ -158,7 +158,7 @@ describe('LibraryQueries', () => {
|
||||
await insertTestNovelCategory(testDb, novelId1, categoryId1);
|
||||
await insertTestNovelCategory(testDb, novelId2, categoryId1);
|
||||
|
||||
const novels = await getLibraryWithCategory(categoryId1, undefined, true);
|
||||
const novels = await getLibraryWithCategory(categoryId1, true);
|
||||
expect(novels).toHaveLength(1);
|
||||
expect(novels[0].name).toBe('Novel 2');
|
||||
});
|
||||
@@ -180,11 +180,7 @@ describe('LibraryQueries', () => {
|
||||
await insertTestNovelCategory(testDb, novelId1, categoryId1);
|
||||
await insertTestNovelCategory(testDb, novelId2, categoryId1);
|
||||
|
||||
const novels = await getLibraryWithCategory(
|
||||
categoryId1,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const novels = await getLibraryWithCategory(categoryId1, false);
|
||||
expect(novels).toHaveLength(2);
|
||||
expect(novels.map(n => n.name).sort()).toEqual(['Novel 1', 'Novel 2']);
|
||||
});
|
||||
@@ -309,7 +305,7 @@ describe('LibraryQueries', () => {
|
||||
);
|
||||
await insertTestNovelCategory(testDb, otherNovelId, otherCategoryId);
|
||||
|
||||
const novels = await getLibraryNovelsForGlobalUpdate(false, {
|
||||
const novels = await getLibraryNovelsForGlobalUpdate({
|
||||
includedCategoryIds: [includedCategoryId],
|
||||
excludedCategoryIds: [],
|
||||
});
|
||||
@@ -348,7 +344,7 @@ describe('LibraryQueries', () => {
|
||||
includedCategoryId,
|
||||
);
|
||||
|
||||
const novels = await getLibraryNovelsForGlobalUpdate(false, {
|
||||
const novels = await getLibraryNovelsForGlobalUpdate({
|
||||
includedCategoryIds: [includedCategoryId],
|
||||
excludedCategoryIds: [excludedCategoryId],
|
||||
});
|
||||
@@ -356,7 +352,7 @@ describe('LibraryQueries', () => {
|
||||
expect(novels.map(novel => novel.name)).toEqual(['Included Novel']);
|
||||
});
|
||||
|
||||
it('applies the ongoing and local novel filters', async () => {
|
||||
it('excludes local novels without filtering by status', async () => {
|
||||
await insertTestNovel(testDb, {
|
||||
inLibrary: true,
|
||||
isLocal: false,
|
||||
@@ -376,12 +372,64 @@ describe('LibraryQueries', () => {
|
||||
status: 'Ongoing',
|
||||
});
|
||||
|
||||
const novels = await getLibraryNovelsForGlobalUpdate(true, {
|
||||
const novels = await getLibraryNovelsForGlobalUpdate({
|
||||
includedCategoryIds: [],
|
||||
excludedCategoryIds: [],
|
||||
});
|
||||
|
||||
expect(novels.map(novel => novel.name)).toEqual(['Ongoing Novel']);
|
||||
expect(novels.map(novel => novel.name)).toEqual([
|
||||
'Ongoing Novel',
|
||||
'Completed Novel',
|
||||
]);
|
||||
});
|
||||
|
||||
it('applies all smart update skip filters', async () => {
|
||||
await insertTestNovel(testDb, {
|
||||
chaptersUnread: 0,
|
||||
inLibrary: true,
|
||||
isLocal: false,
|
||||
lastReadAt: '2026-07-25',
|
||||
name: 'Eligible Novel',
|
||||
status: 'Ongoing',
|
||||
});
|
||||
await insertTestNovel(testDb, {
|
||||
chaptersUnread: 2,
|
||||
inLibrary: true,
|
||||
isLocal: false,
|
||||
lastReadAt: '2026-07-25',
|
||||
name: 'Unread Chapters',
|
||||
status: 'Ongoing',
|
||||
});
|
||||
await insertTestNovel(testDb, {
|
||||
chaptersUnread: 0,
|
||||
inLibrary: true,
|
||||
isLocal: false,
|
||||
lastReadAt: null,
|
||||
name: 'Unstarted Novel',
|
||||
status: 'Ongoing',
|
||||
});
|
||||
await insertTestNovel(testDb, {
|
||||
chaptersUnread: 0,
|
||||
inLibrary: true,
|
||||
isLocal: false,
|
||||
lastReadAt: '2026-07-25',
|
||||
name: 'Completed Novel',
|
||||
status: 'Completed',
|
||||
});
|
||||
|
||||
const novels = await getLibraryNovelsForGlobalUpdate(
|
||||
{
|
||||
includedCategoryIds: [],
|
||||
excludedCategoryIds: [],
|
||||
},
|
||||
{
|
||||
skipCompleted: true,
|
||||
skipUnstarted: true,
|
||||
skipWithUnread: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(novels.map(novel => novel.name)).toEqual(['Eligible Novel']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -477,50 +525,6 @@ describe('LibraryQueries', () => {
|
||||
expect(novels).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should filter by onlyUpdateOngoingNovels = true', async () => {
|
||||
const novelId1 = await insertTestNovel(testDb, {
|
||||
inLibrary: true,
|
||||
name: 'Novel 1',
|
||||
status: 'Ongoing',
|
||||
});
|
||||
const novelId2 = await insertTestNovel(testDb, {
|
||||
inLibrary: true,
|
||||
name: 'Novel 2',
|
||||
status: 'Completed',
|
||||
});
|
||||
const categoryId1 = await insertTestCategory(testDb, {
|
||||
name: 'Category A',
|
||||
});
|
||||
await insertTestNovelCategory(testDb, novelId1, categoryId1);
|
||||
await insertTestNovelCategory(testDb, novelId2, categoryId1);
|
||||
|
||||
const novels = await getLibraryWithCategory(categoryId1, true);
|
||||
expect(novels).toHaveLength(1);
|
||||
expect(novels[0].name).toBe('Novel 1');
|
||||
});
|
||||
|
||||
it('should not filter by onlyUpdateOngoingNovels = false', async () => {
|
||||
const novelId1 = await insertTestNovel(testDb, {
|
||||
inLibrary: true,
|
||||
name: 'Novel 1',
|
||||
status: 'Ongoing',
|
||||
});
|
||||
const novelId2 = await insertTestNovel(testDb, {
|
||||
inLibrary: true,
|
||||
name: 'Novel 2',
|
||||
status: 'Completed',
|
||||
});
|
||||
const categoryId1 = await insertTestCategory(testDb, {
|
||||
name: 'Category A',
|
||||
});
|
||||
await insertTestNovelCategory(testDb, novelId1, categoryId1);
|
||||
await insertTestNovelCategory(testDb, novelId2, categoryId1);
|
||||
|
||||
const novels = await getLibraryWithCategory(categoryId1, false);
|
||||
expect(novels).toHaveLength(2);
|
||||
expect(novels.map(n => n.name).sort()).toEqual(['Novel 1', 'Novel 2']);
|
||||
});
|
||||
|
||||
it('should filter by excludeLocalNovels = true', async () => {
|
||||
const novelId1 = await insertTestNovel(testDb, {
|
||||
inLibrary: true,
|
||||
@@ -538,7 +542,7 @@ describe('LibraryQueries', () => {
|
||||
await insertTestNovelCategory(testDb, novelId1, categoryId1);
|
||||
await insertTestNovelCategory(testDb, novelId2, categoryId1);
|
||||
|
||||
const novels = await getLibraryWithCategory(categoryId1, undefined, true);
|
||||
const novels = await getLibraryWithCategory(categoryId1, true);
|
||||
expect(novels).toHaveLength(1);
|
||||
expect(novels[0].name).toBe('Novel 2');
|
||||
});
|
||||
@@ -560,11 +564,7 @@ describe('LibraryQueries', () => {
|
||||
await insertTestNovelCategory(testDb, novelId1, categoryId1);
|
||||
await insertTestNovelCategory(testDb, novelId2, categoryId1);
|
||||
|
||||
const novels = await getLibraryWithCategory(
|
||||
categoryId1,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
const novels = await getLibraryWithCategory(categoryId1, false);
|
||||
expect(novels).toHaveLength(2);
|
||||
expect(novels.map(n => n.name).sort()).toEqual(['Novel 1', 'Novel 2']);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,9 @@ export const initialAppSettings = {
|
||||
relativeTimestamps: true,
|
||||
downloadedOnlyMode: false,
|
||||
useLibraryFAB: false,
|
||||
onlyUpdateOngoingNovels: false,
|
||||
smartUpdateSkipCompleted: false,
|
||||
smartUpdateSkipUnstarted: false,
|
||||
smartUpdateSkipWithUnread: false,
|
||||
updateLibraryOnLaunch: false,
|
||||
downloadNewChapters: false,
|
||||
refreshNovelMetadata: false,
|
||||
|
||||
@@ -66,7 +66,9 @@ export interface AppSettings {
|
||||
* Update settings
|
||||
*/
|
||||
|
||||
onlyUpdateOngoingNovels: boolean;
|
||||
smartUpdateSkipCompleted: boolean;
|
||||
smartUpdateSkipUnstarted: boolean;
|
||||
smartUpdateSkipWithUnread: boolean;
|
||||
updateLibraryOnLaunch: boolean;
|
||||
downloadNewChapters: boolean;
|
||||
refreshNovelMetadata: boolean;
|
||||
@@ -123,6 +125,12 @@ export interface GlobalUpdateCategoryFilters {
|
||||
includedCategoryIds: number[];
|
||||
}
|
||||
|
||||
export interface SmartUpdateFilters {
|
||||
skipCompleted: boolean;
|
||||
skipUnstarted: boolean;
|
||||
skipWithUnread: boolean;
|
||||
}
|
||||
|
||||
const normalizeCategoryIds = (categoryIds?: number[]): number[] =>
|
||||
Array.from(
|
||||
new Set(
|
||||
@@ -238,7 +246,9 @@ const initialAppSettings: AppSettings = {
|
||||
* Update settings
|
||||
*/
|
||||
|
||||
onlyUpdateOngoingNovels: false,
|
||||
smartUpdateSkipCompleted: false,
|
||||
smartUpdateSkipUnstarted: false,
|
||||
smartUpdateSkipWithUnread: false,
|
||||
updateLibraryOnLaunch: false,
|
||||
downloadNewChapters: false,
|
||||
refreshNovelMetadata: false,
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "ترتيب الفرز",
|
||||
"updateLibrary": "تحديث المكتبة عند التشغيل",
|
||||
"updateLibraryDesc": "لا ينصح به للأجهزة الضعيفة",
|
||||
"updateOngoing": "تحديث الروايات المستمرة فقط",
|
||||
"updateTime": "إظهار وقت آخر تحديث",
|
||||
"useFAB": "استخدام الزر العائم (FAB) في المكتبة"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Bibliothek beim Starten aktualisieren",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Nur laufende Bücher aktualisieren",
|
||||
"updateTime": "Zeige den Zeitpunkt der letzten Aktualisierung",
|
||||
"useFAB": "FAB in der Bibliothek verwenden"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -332,10 +332,13 @@
|
||||
"novelSort": "Novel Sort",
|
||||
"refreshMetadata": "Automatically refresh metadata",
|
||||
"refreshMetadataDescription": "Check for new cover and details when updating library",
|
||||
"smartUpdate": "Smart update",
|
||||
"smartUpdateSkipCompleted": "Skip entries with \"Completed\" status",
|
||||
"smartUpdateSkipUnstarted": "Skip unstarted entries",
|
||||
"smartUpdateSkipWithUnread": "Skip entries with unread chapter(s)",
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library",
|
||||
"timeTracking": "Time Tracking",
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Actualizar biblioteca al iniciar",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Solo actualizar novelas en curso",
|
||||
"updateTime": "Mostrar la última actualización",
|
||||
"useFAB": "Usar FAB en la biblioteca"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Ordre de tri",
|
||||
"updateLibrary": "Mettre à jour la bibliothèque au lancement",
|
||||
"updateLibraryDesc": "Non recommandé pour les appareils ????",
|
||||
"updateOngoing": "Ne mettre à jour que les romans en cours",
|
||||
"updateTime": "Afficher la date de dernière mise à jour",
|
||||
"useFAB": "Utiliser FAB dans la bibliothèque"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Könyvtár frissítése indításkor",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Utolsó frissítés idejének mutatása",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Perbarui perpustakaan saat diluncurkan\n",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Hanya memperbarui novel yang sedang berlangsung\n",
|
||||
"updateTime": "Perlihatkan waktu pembaruan terakhir\n",
|
||||
"useFAB": "Gunakan FAB di perpustakaan\n"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Aggiorna la libreria all'avvio",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Aggiorna solo le novel in corso",
|
||||
"updateTime": "Mostra l'orario dell'ultimo aggiornamento",
|
||||
"useFAB": "Usa FAB nella libreria"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Ordenar",
|
||||
"updateLibrary": "Atualizar biblioteca ao iniciar",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Atualizar somente novels em andamento",
|
||||
"updateTime": "Mostrar horário da última atualização",
|
||||
"useFAB": "Usar Botões flutuantes na Biblioteca"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Порядок сортировки",
|
||||
"updateLibrary": "Обновлять библиотеку при запуске",
|
||||
"updateLibraryDesc": "Не рекомендуется для слабых устройств",
|
||||
"updateOngoing": "Обновлять только онгоинги",
|
||||
"updateTime": "Показать время последнего обновления",
|
||||
"useFAB": "Использовать FAB в библиотеке"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sıralama Düzeni",
|
||||
"updateLibrary": "Açılışta kitaplığı güncelle",
|
||||
"updateLibraryDesc": "Eski cihazlarda önerilmez",
|
||||
"updateOngoing": "Yalnızca devam eden novel'leri güncelle",
|
||||
"updateTime": "Son güncelleme süresini göster",
|
||||
"useFAB": "Kitaplıkta FAB kullan"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Порядок сортування",
|
||||
"updateLibrary": "Оновлювати бібліотеку при запуску",
|
||||
"updateLibraryDesc": "Не рекомендується для слабких пристроїв",
|
||||
"updateOngoing": "Оновлювати лише новели, що оновлюються",
|
||||
"updateTime": "Показувати час останнього оновлення",
|
||||
"useFAB": "Використовувати FAB в бібліотеці"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Thứ tự sắp xếp",
|
||||
"updateLibrary": "Cập nhật thư viện khi khởi động",
|
||||
"updateLibraryDesc": "Không khuyến khích cho thiết bị yếu",
|
||||
"updateOngoing": "Chỉ cập nhật truyện đang thực hiện",
|
||||
"updateTime": "Hiện lần cập nhật trước",
|
||||
"useFAB": "Sử dụng FAB trong thư viện"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "排序",
|
||||
"updateLibrary": "打开应用时更新书库",
|
||||
"updateLibraryDesc": "不推荐在低端设备上启用",
|
||||
"updateOngoing": "仅更新连载小说",
|
||||
"updateTime": "显示上次更新时间",
|
||||
"useFAB": "在书库中使用浮动操作按钮"
|
||||
},
|
||||
|
||||
@@ -298,7 +298,6 @@
|
||||
"sortOrder": "Sort Order",
|
||||
"updateLibrary": "Update library on launch",
|
||||
"updateLibraryDesc": "Not recommended for low devices",
|
||||
"updateOngoing": "Only update ongoing novels",
|
||||
"updateTime": "Show last update time",
|
||||
"useFAB": "Use FAB in Library"
|
||||
},
|
||||
|
||||
@@ -295,10 +295,13 @@ export interface StringMap {
|
||||
'generalSettingsScreen.novelSort': 'string';
|
||||
'generalSettingsScreen.refreshMetadata': 'string';
|
||||
'generalSettingsScreen.refreshMetadataDescription': 'string';
|
||||
'generalSettingsScreen.smartUpdate': 'string';
|
||||
'generalSettingsScreen.smartUpdateSkipCompleted': 'string';
|
||||
'generalSettingsScreen.smartUpdateSkipUnstarted': 'string';
|
||||
'generalSettingsScreen.smartUpdateSkipWithUnread': 'string';
|
||||
'generalSettingsScreen.sortOrder': 'string';
|
||||
'generalSettingsScreen.updateLibrary': 'string';
|
||||
'generalSettingsScreen.updateLibraryDesc': 'string';
|
||||
'generalSettingsScreen.updateOngoing': 'string';
|
||||
'generalSettingsScreen.updateTime': 'string';
|
||||
'generalSettingsScreen.useFAB': 'string';
|
||||
'generalSettingsScreen.timeTracking': 'string';
|
||||
|
||||
@@ -24,6 +24,7 @@ import DefaultChapterSortModal from '../components/DefaultChapterSortModal';
|
||||
import SettingSwitch from '../components/SettingSwitch';
|
||||
import DefaultCategoryDialog from './DefaultCategoryDialog';
|
||||
import GlobalUpdateCategoriesDialog from './GlobalUpdateCategoriesDialog';
|
||||
import SmartUpdateDialog from './SmartUpdateDialog';
|
||||
import DisplayModeModal from './modals/DisplayModeModal';
|
||||
import GridSizeModal from './modals/GridSizeModal';
|
||||
import NovelBadgesModal from './modals/NovelBadgesModal';
|
||||
@@ -60,9 +61,11 @@ const SettingsLibraryScreen = ({ navigation }: LibrarySettingsScreenProps) => {
|
||||
const {
|
||||
defaultChapterSort,
|
||||
downloadNewChapters,
|
||||
onlyUpdateOngoingNovels,
|
||||
refreshNovelMetadata,
|
||||
setAppSettings,
|
||||
smartUpdateSkipCompleted = false,
|
||||
smartUpdateSkipUnstarted = false,
|
||||
smartUpdateSkipWithUnread = false,
|
||||
updateLibraryOnLaunch,
|
||||
useLibraryFAB,
|
||||
} = useAppSettings();
|
||||
@@ -75,6 +78,7 @@ const SettingsLibraryScreen = ({ navigation }: LibrarySettingsScreenProps) => {
|
||||
const gridSizeModal = useBoolean();
|
||||
const novelBadgesModal = useBoolean();
|
||||
const novelSortModal = useBoolean();
|
||||
const smartUpdateDialog = useBoolean();
|
||||
const defaultChapterSortModal = useBoolean();
|
||||
const appDefaultCategory = categories.find(category => category.id === 1);
|
||||
const selectedDefaultCategory = categories.find(
|
||||
@@ -92,6 +96,11 @@ const SettingsLibraryScreen = ({ navigation }: LibrarySettingsScreenProps) => {
|
||||
const [draftExcludedCategoryIds, setDraftExcludedCategoryIds] = useState<
|
||||
number[]
|
||||
>([]);
|
||||
const [draftSmartUpdateFilters, setDraftSmartUpdateFilters] = useState({
|
||||
skipCompleted: false,
|
||||
skipUnstarted: false,
|
||||
skipWithUnread: false,
|
||||
});
|
||||
|
||||
const setDefaultCategory = (categoryId: number) => {
|
||||
setLibrarySettings({
|
||||
@@ -122,6 +131,24 @@ const SettingsLibraryScreen = ({ navigation }: LibrarySettingsScreenProps) => {
|
||||
globalUpdateCategoriesDialog.setFalse();
|
||||
};
|
||||
|
||||
const showSmartUpdateDialog = () => {
|
||||
setDraftSmartUpdateFilters({
|
||||
skipCompleted: smartUpdateSkipCompleted,
|
||||
skipUnstarted: smartUpdateSkipUnstarted,
|
||||
skipWithUnread: smartUpdateSkipWithUnread,
|
||||
});
|
||||
smartUpdateDialog.setTrue();
|
||||
};
|
||||
|
||||
const saveSmartUpdateFilters = () => {
|
||||
setAppSettings({
|
||||
smartUpdateSkipCompleted: draftSmartUpdateFilters.skipCompleted,
|
||||
smartUpdateSkipUnstarted: draftSmartUpdateFilters.skipUnstarted,
|
||||
smartUpdateSkipWithUnread: draftSmartUpdateFilters.skipWithUnread,
|
||||
});
|
||||
smartUpdateDialog.setFalse();
|
||||
};
|
||||
|
||||
const getCategoryFilterDescription = (
|
||||
categoryIds: number[],
|
||||
emptyLabel: string,
|
||||
@@ -144,6 +171,19 @@ const SettingsLibraryScreen = ({ navigation }: LibrarySettingsScreenProps) => {
|
||||
globalUpdateExcludeCategoryIds,
|
||||
getString('common.none'),
|
||||
);
|
||||
const smartUpdateDescription = [
|
||||
smartUpdateSkipWithUnread
|
||||
? getString('generalSettingsScreen.smartUpdateSkipWithUnread')
|
||||
: null,
|
||||
smartUpdateSkipUnstarted
|
||||
? getString('generalSettingsScreen.smartUpdateSkipUnstarted')
|
||||
: null,
|
||||
smartUpdateSkipCompleted
|
||||
? getString('generalSettingsScreen.smartUpdateSkipCompleted')
|
||||
: null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
|
||||
const sortOrderParts = sortOrder.split(' ');
|
||||
const sortOrderLabel =
|
||||
@@ -247,16 +287,6 @@ const SettingsLibraryScreen = ({ navigation }: LibrarySettingsScreenProps) => {
|
||||
<List.SubHeader theme={theme}>
|
||||
{getString('generalSettingsScreen.globalUpdate')}
|
||||
</List.SubHeader>
|
||||
<SettingSwitch
|
||||
label={getString('generalSettingsScreen.updateOngoing')}
|
||||
value={onlyUpdateOngoingNovels}
|
||||
onPress={() =>
|
||||
setAppSettings({
|
||||
onlyUpdateOngoingNovels: !onlyUpdateOngoingNovels,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('generalSettingsScreen.globalUpdateCategories')}
|
||||
description={`${getString(
|
||||
@@ -268,6 +298,12 @@ const SettingsLibraryScreen = ({ navigation }: LibrarySettingsScreenProps) => {
|
||||
onPress={showGlobalUpdateCategoriesDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<List.Item
|
||||
title={getString('generalSettingsScreen.smartUpdate')}
|
||||
description={smartUpdateDescription || getString('common.none')}
|
||||
onPress={showSmartUpdateDialog}
|
||||
theme={theme}
|
||||
/>
|
||||
<SettingSwitch
|
||||
label={getString('generalSettingsScreen.refreshMetadata')}
|
||||
description={getString(
|
||||
@@ -344,6 +380,13 @@ const SettingsLibraryScreen = ({ navigation }: LibrarySettingsScreenProps) => {
|
||||
onChange={updateDraftCategoryFilters}
|
||||
onSave={saveGlobalUpdateCategories}
|
||||
/>
|
||||
<SmartUpdateDialog
|
||||
filters={draftSmartUpdateFilters}
|
||||
visible={smartUpdateDialog.value}
|
||||
onCancel={smartUpdateDialog.setFalse}
|
||||
onChange={setDraftSmartUpdateFilters}
|
||||
onSave={saveSmartUpdateFilters}
|
||||
/>
|
||||
</Portal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Checkbox, Dialog } from '@components';
|
||||
import type { SmartUpdateFilters } from '@hooks/persisted/useSettings';
|
||||
import { useTheme } from '@hooks/persisted';
|
||||
import { getString } from '@i18n/translations';
|
||||
|
||||
interface SmartUpdateDialogProps {
|
||||
filters: SmartUpdateFilters;
|
||||
visible: boolean;
|
||||
onCancel: () => void;
|
||||
onChange: (filters: SmartUpdateFilters) => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
const SmartUpdateDialog = ({
|
||||
filters,
|
||||
visible,
|
||||
onCancel,
|
||||
onChange,
|
||||
onSave,
|
||||
}: SmartUpdateDialogProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Dialog.Root visible={visible} onDismiss={onCancel}>
|
||||
<Dialog.Title>
|
||||
{getString('generalSettingsScreen.smartUpdate')}
|
||||
</Dialog.Title>
|
||||
<Dialog.List>
|
||||
<Checkbox
|
||||
label={getString('generalSettingsScreen.smartUpdateSkipWithUnread')}
|
||||
status={filters.skipWithUnread}
|
||||
onPress={() =>
|
||||
onChange({
|
||||
...filters,
|
||||
skipWithUnread: !filters.skipWithUnread,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<Checkbox
|
||||
label={getString('generalSettingsScreen.smartUpdateSkipUnstarted')}
|
||||
status={filters.skipUnstarted}
|
||||
onPress={() =>
|
||||
onChange({
|
||||
...filters,
|
||||
skipUnstarted: !filters.skipUnstarted,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
<Checkbox
|
||||
label={getString('generalSettingsScreen.smartUpdateSkipCompleted')}
|
||||
status={filters.skipCompleted}
|
||||
onPress={() =>
|
||||
onChange({
|
||||
...filters,
|
||||
skipCompleted: !filters.skipCompleted,
|
||||
})
|
||||
}
|
||||
theme={theme}
|
||||
/>
|
||||
</Dialog.List>
|
||||
<Dialog.Actions>
|
||||
<Dialog.Action onPress={onCancel}>
|
||||
{getString('common.cancel')}
|
||||
</Dialog.Action>
|
||||
<Dialog.Action onPress={onSave}>{getString('common.ok')}</Dialog.Action>
|
||||
</Dialog.Actions>
|
||||
</Dialog.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmartUpdateDialog;
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
getLibraryWithCategory,
|
||||
} from '@database/queries/LibraryQueries';
|
||||
import type { BackgroundTaskMetadata } from '@services/backgroundTasks/contracts';
|
||||
import { getMMKVObject } from '@utils/mmkv/mmkv';
|
||||
import { updateNovel } from '../LibraryUpdateQueries';
|
||||
import { updateLibrary } from '../index';
|
||||
|
||||
@@ -37,6 +38,7 @@ jest.mock('@hooks/persisted/useSettings', () => ({
|
||||
const mockedGetLibraryNovels = jest.mocked(getLibraryNovelsForGlobalUpdate);
|
||||
const mockedGetLibraryWithCategory = jest.mocked(getLibraryWithCategory);
|
||||
const mockedUpdateNovel = jest.mocked(updateNovel);
|
||||
const mockedGetMMKVObject = jest.mocked(getMMKVObject);
|
||||
|
||||
type LibraryNovel = Awaited<
|
||||
ReturnType<typeof getLibraryNovelsForGlobalUpdate>
|
||||
@@ -55,6 +57,7 @@ const flushPromises = () => new Promise(resolve => setImmediate(resolve));
|
||||
describe('updateLibrary', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockedGetMMKVObject.mockReturnValue({});
|
||||
});
|
||||
|
||||
it('updates at most three different sources while keeping each source sequential', async () => {
|
||||
@@ -126,11 +129,30 @@ describe('updateLibrary', () => {
|
||||
|
||||
await updateLibrary({ categoryId: 4 }, jest.fn(), jest.fn());
|
||||
|
||||
expect(mockedGetLibraryWithCategory).toHaveBeenCalledWith(
|
||||
4,
|
||||
undefined,
|
||||
true,
|
||||
);
|
||||
expect(mockedGetLibraryWithCategory).toHaveBeenCalledWith(4, true);
|
||||
expect(mockedGetLibraryNovels).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes smart update preferences to the global update query', async () => {
|
||||
mockedGetMMKVObject.mockReturnValue({
|
||||
smartUpdateSkipCompleted: true,
|
||||
smartUpdateSkipUnstarted: true,
|
||||
smartUpdateSkipWithUnread: true,
|
||||
});
|
||||
mockedGetLibraryNovels.mockResolvedValue([]);
|
||||
|
||||
await updateLibrary({}, jest.fn(), jest.fn());
|
||||
|
||||
expect(mockedGetLibraryNovels).toHaveBeenCalledWith(
|
||||
{
|
||||
excludedCategoryIds: [],
|
||||
includedCategoryIds: [],
|
||||
},
|
||||
{
|
||||
skipCompleted: true,
|
||||
skipUnstarted: true,
|
||||
skipWithUnread: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,8 +52,18 @@ const updateLibrary = async (
|
||||
progress: 0,
|
||||
}));
|
||||
|
||||
const { downloadNewChapters, refreshNovelMetadata, onlyUpdateOngoingNovels } =
|
||||
getMMKVObject<AppSettings>(APP_SETTINGS) || {};
|
||||
const {
|
||||
downloadNewChapters,
|
||||
refreshNovelMetadata,
|
||||
smartUpdateSkipCompleted,
|
||||
smartUpdateSkipUnstarted,
|
||||
smartUpdateSkipWithUnread,
|
||||
} = getMMKVObject<AppSettings>(APP_SETTINGS) || {};
|
||||
const smartUpdateFilters = {
|
||||
skipCompleted: Boolean(smartUpdateSkipCompleted),
|
||||
skipUnstarted: Boolean(smartUpdateSkipUnstarted),
|
||||
skipWithUnread: Boolean(smartUpdateSkipWithUnread),
|
||||
};
|
||||
const options: UpdateNovelOptions = {
|
||||
downloadNewChapters: downloadNewChapters || false,
|
||||
refreshNovelMetadata: refreshNovelMetadata || false,
|
||||
@@ -62,15 +72,11 @@ const updateLibrary = async (
|
||||
|
||||
let libraryNovels: DBNovelInfo[] = [];
|
||||
if (categoryId) {
|
||||
libraryNovels = await getLibraryWithCategory(
|
||||
categoryId,
|
||||
onlyUpdateOngoingNovels,
|
||||
true,
|
||||
);
|
||||
libraryNovels = await getLibraryWithCategory(categoryId, true);
|
||||
} else {
|
||||
libraryNovels = await getLibraryNovelsForGlobalUpdate(
|
||||
Boolean(onlyUpdateOngoingNovels),
|
||||
getGlobalUpdateCategoryFilters(),
|
||||
smartUpdateFilters,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user