feat: Add Smart Library Update Filters

Closes #423
This commit is contained in:
Rajarshee Chatterjee
2026-07-25 14:29:41 +05:30
parent 6d2a9f8e15
commit c30441b949
44 changed files with 276 additions and 129 deletions
+24 -5
View File
@@ -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']);
});
+3 -1
View File
@@ -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,
+12 -2
View File
@@ -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,
-1
View File
@@ -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"
},
-1
View File
@@ -298,7 +298,6 @@
"sortOrder": "ترتيب الفرز",
"updateLibrary": "تحديث المكتبة عند التشغيل",
"updateLibraryDesc": "لا ينصح به للأجهزة الضعيفة",
"updateOngoing": "تحديث الروايات المستمرة فقط",
"updateTime": "إظهار وقت آخر تحديث",
"useFAB": "استخدام الزر العائم (FAB) في المكتبة"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
+4 -1
View File
@@ -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",
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -298,7 +298,6 @@
"sortOrder": "Порядок сортировки",
"updateLibrary": "Обновлять библиотеку при запуске",
"updateLibraryDesc": "Не рекомендуется для слабых устройств",
"updateOngoing": "Обновлять только онгоинги",
"updateTime": "Показать время последнего обновления",
"useFAB": "Использовать FAB в библиотеке"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -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"
},
-1
View File
@@ -298,7 +298,6 @@
"sortOrder": "Порядок сортування",
"updateLibrary": "Оновлювати бібліотеку при запуску",
"updateLibraryDesc": "Не рекомендується для слабких пристроїв",
"updateOngoing": "Оновлювати лише новели, що оновлюються",
"updateTime": "Показувати час останнього оновлення",
"useFAB": "Використовувати FAB в бібліотеці"
},
-1
View File
@@ -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"
},
-1
View File
@@ -298,7 +298,6 @@
"sortOrder": "排序",
"updateLibrary": "打开应用时更新书库",
"updateLibraryDesc": "不推荐在低端设备上启用",
"updateOngoing": "仅更新连载小说",
"updateTime": "显示上次更新时间",
"useFAB": "在书库中使用浮动操作按钮"
},
-1
View File
@@ -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"
},
+4 -1
View File
@@ -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;
+27 -5
View File
@@ -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,
},
);
});
});
+14 -8
View File
@@ -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,
);
}