From c30441b9493d5e75fbe82a608b034c6239fa493f Mon Sep 17 00:00:00 2001 From: Rajarshee Chatterjee Date: Sat, 25 Jul 2026 14:29:41 +0530 Subject: [PATCH] feat: Add Smart Library Update Filters Closes #423 --- src/database/queries/LibraryQueries.ts | 29 ++++- .../queries/__tests__/LibraryQueries.test.ts | 122 +++++++++--------- src/hooks/persisted/__mocks__/useSettings.ts | 4 +- src/hooks/persisted/useSettings.ts | 14 +- src/i18n/languages/af_ZA/strings.json | 1 - src/i18n/languages/ar_SA/strings.json | 1 - src/i18n/languages/as_IN/strings.json | 1 - src/i18n/languages/ca_ES/strings.json | 1 - src/i18n/languages/cs_CZ/strings.json | 1 - src/i18n/languages/da_DK/strings.json | 1 - src/i18n/languages/de_DE/strings.json | 1 - src/i18n/languages/el_GR/strings.json | 1 - src/i18n/languages/en/strings.json | 5 +- src/i18n/languages/es_ES/strings.json | 1 - src/i18n/languages/fi_FI/strings.json | 1 - src/i18n/languages/fr_FR/strings.json | 1 - src/i18n/languages/he_IL/strings.json | 1 - src/i18n/languages/hi_IN/strings.json | 1 - src/i18n/languages/hu_HU/strings.json | 1 - src/i18n/languages/id_ID/strings.json | 1 - src/i18n/languages/it_IT/strings.json | 1 - src/i18n/languages/ja_JP/strings.json | 1 - src/i18n/languages/ko_KR/strings.json | 1 - src/i18n/languages/nl_NL/strings.json | 1 - src/i18n/languages/no_NO/strings.json | 1 - src/i18n/languages/or_IN/strings.json | 1 - src/i18n/languages/pl_PL/strings.json | 1 - src/i18n/languages/pt_BR/strings.json | 1 - src/i18n/languages/pt_PT/strings.json | 1 - src/i18n/languages/ro_RO/strings.json | 1 - src/i18n/languages/ru_RU/strings.json | 1 - src/i18n/languages/sq_AL/strings.json | 1 - src/i18n/languages/sr_SP/strings.json | 1 - src/i18n/languages/sv_SE/strings.json | 1 - src/i18n/languages/tr_TR/strings.json | 1 - src/i18n/languages/uk_UA/strings.json | 1 - src/i18n/languages/vi_VN/strings.json | 1 - src/i18n/languages/zh_CN/strings.json | 1 - src/i18n/languages/zh_TW/strings.json | 1 - src/i18n/types/index.ts | 5 +- .../SettingsLibraryScreen.tsx | 65 ++++++++-- .../SmartUpdateDialog.tsx | 73 +++++++++++ src/services/updates/__tests__/index.test.ts | 32 ++++- src/services/updates/index.ts | 22 ++-- 44 files changed, 276 insertions(+), 129 deletions(-) create mode 100644 src/screens/settings/SettingsLibraryScreen/SmartUpdateDialog.tsx diff --git a/src/database/queries/LibraryQueries.ts b/src/database/queries/LibraryQueries.ts index 92f36aaa..b92555bf 100644 --- a/src/database/queries/LibraryQueries.ts +++ b/src/database/queries/LibraryQueries.ts @@ -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(); diff --git a/src/database/queries/__tests__/LibraryQueries.test.ts b/src/database/queries/__tests__/LibraryQueries.test.ts index 91c75274..69f3006f 100644 --- a/src/database/queries/__tests__/LibraryQueries.test.ts +++ b/src/database/queries/__tests__/LibraryQueries.test.ts @@ -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']); }); diff --git a/src/hooks/persisted/__mocks__/useSettings.ts b/src/hooks/persisted/__mocks__/useSettings.ts index a491cb34..8588187a 100644 --- a/src/hooks/persisted/__mocks__/useSettings.ts +++ b/src/hooks/persisted/__mocks__/useSettings.ts @@ -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, diff --git a/src/hooks/persisted/useSettings.ts b/src/hooks/persisted/useSettings.ts index f88a0308..abeed85e 100644 --- a/src/hooks/persisted/useSettings.ts +++ b/src/hooks/persisted/useSettings.ts @@ -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, diff --git a/src/i18n/languages/af_ZA/strings.json b/src/i18n/languages/af_ZA/strings.json index 1896da6b..4402c28d 100644 --- a/src/i18n/languages/af_ZA/strings.json +++ b/src/i18n/languages/af_ZA/strings.json @@ -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" }, diff --git a/src/i18n/languages/ar_SA/strings.json b/src/i18n/languages/ar_SA/strings.json index 120d3e26..e070a1bd 100644 --- a/src/i18n/languages/ar_SA/strings.json +++ b/src/i18n/languages/ar_SA/strings.json @@ -298,7 +298,6 @@ "sortOrder": "ترتيب الفرز", "updateLibrary": "تحديث المكتبة عند التشغيل", "updateLibraryDesc": "لا ينصح به للأجهزة الضعيفة", - "updateOngoing": "تحديث الروايات المستمرة فقط", "updateTime": "إظهار وقت آخر تحديث", "useFAB": "استخدام الزر العائم (FAB) في المكتبة" }, diff --git a/src/i18n/languages/as_IN/strings.json b/src/i18n/languages/as_IN/strings.json index 03216e19..635a2cf5 100644 --- a/src/i18n/languages/as_IN/strings.json +++ b/src/i18n/languages/as_IN/strings.json @@ -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" }, diff --git a/src/i18n/languages/ca_ES/strings.json b/src/i18n/languages/ca_ES/strings.json index 1896da6b..4402c28d 100644 --- a/src/i18n/languages/ca_ES/strings.json +++ b/src/i18n/languages/ca_ES/strings.json @@ -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" }, diff --git a/src/i18n/languages/cs_CZ/strings.json b/src/i18n/languages/cs_CZ/strings.json index 147e2f1f..e2fb978c 100644 --- a/src/i18n/languages/cs_CZ/strings.json +++ b/src/i18n/languages/cs_CZ/strings.json @@ -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" }, diff --git a/src/i18n/languages/da_DK/strings.json b/src/i18n/languages/da_DK/strings.json index 1896da6b..4402c28d 100644 --- a/src/i18n/languages/da_DK/strings.json +++ b/src/i18n/languages/da_DK/strings.json @@ -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" }, diff --git a/src/i18n/languages/de_DE/strings.json b/src/i18n/languages/de_DE/strings.json index 6cd9b263..a5ca7d4e 100644 --- a/src/i18n/languages/de_DE/strings.json +++ b/src/i18n/languages/de_DE/strings.json @@ -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" }, diff --git a/src/i18n/languages/el_GR/strings.json b/src/i18n/languages/el_GR/strings.json index 78524afc..0e81c344 100644 --- a/src/i18n/languages/el_GR/strings.json +++ b/src/i18n/languages/el_GR/strings.json @@ -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" }, diff --git a/src/i18n/languages/en/strings.json b/src/i18n/languages/en/strings.json index d250f531..703f2a0b 100644 --- a/src/i18n/languages/en/strings.json +++ b/src/i18n/languages/en/strings.json @@ -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", diff --git a/src/i18n/languages/es_ES/strings.json b/src/i18n/languages/es_ES/strings.json index 4229ca3f..5b9b9a1f 100644 --- a/src/i18n/languages/es_ES/strings.json +++ b/src/i18n/languages/es_ES/strings.json @@ -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" }, diff --git a/src/i18n/languages/fi_FI/strings.json b/src/i18n/languages/fi_FI/strings.json index 1896da6b..4402c28d 100644 --- a/src/i18n/languages/fi_FI/strings.json +++ b/src/i18n/languages/fi_FI/strings.json @@ -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" }, diff --git a/src/i18n/languages/fr_FR/strings.json b/src/i18n/languages/fr_FR/strings.json index e36d6a1f..44f136d9 100644 --- a/src/i18n/languages/fr_FR/strings.json +++ b/src/i18n/languages/fr_FR/strings.json @@ -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" }, diff --git a/src/i18n/languages/he_IL/strings.json b/src/i18n/languages/he_IL/strings.json index 7f255a7a..4b170333 100644 --- a/src/i18n/languages/he_IL/strings.json +++ b/src/i18n/languages/he_IL/strings.json @@ -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" }, diff --git a/src/i18n/languages/hi_IN/strings.json b/src/i18n/languages/hi_IN/strings.json index 1896da6b..4402c28d 100644 --- a/src/i18n/languages/hi_IN/strings.json +++ b/src/i18n/languages/hi_IN/strings.json @@ -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" }, diff --git a/src/i18n/languages/hu_HU/strings.json b/src/i18n/languages/hu_HU/strings.json index 1cf41b81..b491518a 100644 --- a/src/i18n/languages/hu_HU/strings.json +++ b/src/i18n/languages/hu_HU/strings.json @@ -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" }, diff --git a/src/i18n/languages/id_ID/strings.json b/src/i18n/languages/id_ID/strings.json index 27c96657..be1d5381 100644 --- a/src/i18n/languages/id_ID/strings.json +++ b/src/i18n/languages/id_ID/strings.json @@ -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" }, diff --git a/src/i18n/languages/it_IT/strings.json b/src/i18n/languages/it_IT/strings.json index 605b044a..78ae43ce 100644 --- a/src/i18n/languages/it_IT/strings.json +++ b/src/i18n/languages/it_IT/strings.json @@ -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" }, diff --git a/src/i18n/languages/ja_JP/strings.json b/src/i18n/languages/ja_JP/strings.json index ea618eea..5d297ba7 100644 --- a/src/i18n/languages/ja_JP/strings.json +++ b/src/i18n/languages/ja_JP/strings.json @@ -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" }, diff --git a/src/i18n/languages/ko_KR/strings.json b/src/i18n/languages/ko_KR/strings.json index 447217f1..26bffdac 100644 --- a/src/i18n/languages/ko_KR/strings.json +++ b/src/i18n/languages/ko_KR/strings.json @@ -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" }, diff --git a/src/i18n/languages/nl_NL/strings.json b/src/i18n/languages/nl_NL/strings.json index d422423e..1a6102b6 100644 --- a/src/i18n/languages/nl_NL/strings.json +++ b/src/i18n/languages/nl_NL/strings.json @@ -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" }, diff --git a/src/i18n/languages/no_NO/strings.json b/src/i18n/languages/no_NO/strings.json index 1896da6b..4402c28d 100644 --- a/src/i18n/languages/no_NO/strings.json +++ b/src/i18n/languages/no_NO/strings.json @@ -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" }, diff --git a/src/i18n/languages/or_IN/strings.json b/src/i18n/languages/or_IN/strings.json index 47e76912..920c9e34 100644 --- a/src/i18n/languages/or_IN/strings.json +++ b/src/i18n/languages/or_IN/strings.json @@ -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" }, diff --git a/src/i18n/languages/pl_PL/strings.json b/src/i18n/languages/pl_PL/strings.json index 46b38b8b..d6ebee02 100644 --- a/src/i18n/languages/pl_PL/strings.json +++ b/src/i18n/languages/pl_PL/strings.json @@ -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" }, diff --git a/src/i18n/languages/pt_BR/strings.json b/src/i18n/languages/pt_BR/strings.json index b5a68c54..5ee503ba 100644 --- a/src/i18n/languages/pt_BR/strings.json +++ b/src/i18n/languages/pt_BR/strings.json @@ -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" }, diff --git a/src/i18n/languages/pt_PT/strings.json b/src/i18n/languages/pt_PT/strings.json index 1896da6b..4402c28d 100644 --- a/src/i18n/languages/pt_PT/strings.json +++ b/src/i18n/languages/pt_PT/strings.json @@ -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" }, diff --git a/src/i18n/languages/ro_RO/strings.json b/src/i18n/languages/ro_RO/strings.json index 1896da6b..4402c28d 100644 --- a/src/i18n/languages/ro_RO/strings.json +++ b/src/i18n/languages/ro_RO/strings.json @@ -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" }, diff --git a/src/i18n/languages/ru_RU/strings.json b/src/i18n/languages/ru_RU/strings.json index bd4cfa87..276e027a 100644 --- a/src/i18n/languages/ru_RU/strings.json +++ b/src/i18n/languages/ru_RU/strings.json @@ -298,7 +298,6 @@ "sortOrder": "Порядок сортировки", "updateLibrary": "Обновлять библиотеку при запуске", "updateLibraryDesc": "Не рекомендуется для слабых устройств", - "updateOngoing": "Обновлять только онгоинги", "updateTime": "Показать время последнего обновления", "useFAB": "Использовать FAB в библиотеке" }, diff --git a/src/i18n/languages/sq_AL/strings.json b/src/i18n/languages/sq_AL/strings.json index 1896da6b..4402c28d 100644 --- a/src/i18n/languages/sq_AL/strings.json +++ b/src/i18n/languages/sq_AL/strings.json @@ -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" }, diff --git a/src/i18n/languages/sr_SP/strings.json b/src/i18n/languages/sr_SP/strings.json index 1896da6b..4402c28d 100644 --- a/src/i18n/languages/sr_SP/strings.json +++ b/src/i18n/languages/sr_SP/strings.json @@ -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" }, diff --git a/src/i18n/languages/sv_SE/strings.json b/src/i18n/languages/sv_SE/strings.json index 1896da6b..4402c28d 100644 --- a/src/i18n/languages/sv_SE/strings.json +++ b/src/i18n/languages/sv_SE/strings.json @@ -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" }, diff --git a/src/i18n/languages/tr_TR/strings.json b/src/i18n/languages/tr_TR/strings.json index 95d3a416..69beefd1 100644 --- a/src/i18n/languages/tr_TR/strings.json +++ b/src/i18n/languages/tr_TR/strings.json @@ -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" }, diff --git a/src/i18n/languages/uk_UA/strings.json b/src/i18n/languages/uk_UA/strings.json index 2166c4c1..8fe060eb 100644 --- a/src/i18n/languages/uk_UA/strings.json +++ b/src/i18n/languages/uk_UA/strings.json @@ -298,7 +298,6 @@ "sortOrder": "Порядок сортування", "updateLibrary": "Оновлювати бібліотеку при запуску", "updateLibraryDesc": "Не рекомендується для слабких пристроїв", - "updateOngoing": "Оновлювати лише новели, що оновлюються", "updateTime": "Показувати час останнього оновлення", "useFAB": "Використовувати FAB в бібліотеці" }, diff --git a/src/i18n/languages/vi_VN/strings.json b/src/i18n/languages/vi_VN/strings.json index a2e2e075..946312ad 100644 --- a/src/i18n/languages/vi_VN/strings.json +++ b/src/i18n/languages/vi_VN/strings.json @@ -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" }, diff --git a/src/i18n/languages/zh_CN/strings.json b/src/i18n/languages/zh_CN/strings.json index 467c9a82..a00af4a8 100644 --- a/src/i18n/languages/zh_CN/strings.json +++ b/src/i18n/languages/zh_CN/strings.json @@ -298,7 +298,6 @@ "sortOrder": "排序", "updateLibrary": "打开应用时更新书库", "updateLibraryDesc": "不推荐在低端设备上启用", - "updateOngoing": "仅更新连载小说", "updateTime": "显示上次更新时间", "useFAB": "在书库中使用浮动操作按钮" }, diff --git a/src/i18n/languages/zh_TW/strings.json b/src/i18n/languages/zh_TW/strings.json index 1c43df47..150ef16f 100644 --- a/src/i18n/languages/zh_TW/strings.json +++ b/src/i18n/languages/zh_TW/strings.json @@ -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" }, diff --git a/src/i18n/types/index.ts b/src/i18n/types/index.ts index d3408751..5fbad791 100644 --- a/src/i18n/types/index.ts +++ b/src/i18n/types/index.ts @@ -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'; diff --git a/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx b/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx index 9655db66..95a2920c 100644 --- a/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx +++ b/src/screens/settings/SettingsLibraryScreen/SettingsLibraryScreen.tsx @@ -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) => { {getString('generalSettingsScreen.globalUpdate')} - - setAppSettings({ - onlyUpdateOngoingNovels: !onlyUpdateOngoingNovels, - }) - } - theme={theme} - /> { onPress={showGlobalUpdateCategoriesDialog} theme={theme} /> + { onChange={updateDraftCategoryFilters} onSave={saveGlobalUpdateCategories} /> + ); diff --git a/src/screens/settings/SettingsLibraryScreen/SmartUpdateDialog.tsx b/src/screens/settings/SettingsLibraryScreen/SmartUpdateDialog.tsx new file mode 100644 index 00000000..3e3c61a9 --- /dev/null +++ b/src/screens/settings/SettingsLibraryScreen/SmartUpdateDialog.tsx @@ -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 ( + + + {getString('generalSettingsScreen.smartUpdate')} + + + + onChange({ + ...filters, + skipWithUnread: !filters.skipWithUnread, + }) + } + theme={theme} + /> + + onChange({ + ...filters, + skipUnstarted: !filters.skipUnstarted, + }) + } + theme={theme} + /> + + onChange({ + ...filters, + skipCompleted: !filters.skipCompleted, + }) + } + theme={theme} + /> + + + + {getString('common.cancel')} + + {getString('common.ok')} + + + ); +}; + +export default SmartUpdateDialog; diff --git a/src/services/updates/__tests__/index.test.ts b/src/services/updates/__tests__/index.test.ts index 0e9bf7ab..2286bb73 100644 --- a/src/services/updates/__tests__/index.test.ts +++ b/src/services/updates/__tests__/index.test.ts @@ -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 @@ -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, + }, + ); + }); }); diff --git a/src/services/updates/index.ts b/src/services/updates/index.ts index f5af65da..37a338fb 100644 --- a/src/services/updates/index.ts +++ b/src/services/updates/index.ts @@ -52,8 +52,18 @@ const updateLibrary = async ( progress: 0, })); - const { downloadNewChapters, refreshNovelMetadata, onlyUpdateOngoingNovels } = - getMMKVObject(APP_SETTINGS) || {}; + const { + downloadNewChapters, + refreshNovelMetadata, + smartUpdateSkipCompleted, + smartUpdateSkipUnstarted, + smartUpdateSkipWithUnread, + } = getMMKVObject(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, ); }