From 79e55488790bd723b395364765cc6bf51a1ab9a0 Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Sun, 7 Jun 2026 00:18:45 -0500 Subject: [PATCH] Fix: Library Filtering + GQL Cleanup P.1 --- src/lib/components/library/Library.svelte | 79 ++++++++--- .../components/library/LibraryToolbar.svelte | 12 +- .../components/library/lib/libraryUpdater.ts | 125 ------------------ src/lib/components/reader/Reader.svelte | 15 +-- .../components/reader/ReaderControls.svelte | 54 ++++---- src/lib/server-adapters/suwayomi/downloads.ts | 9 -- src/lib/server-adapters/suwayomi/index.ts | 65 +++------ src/lib/server-adapters/suwayomi/meta.ts | 44 ++++++ src/lib/state/library.svelte.ts | 2 +- 9 files changed, 160 insertions(+), 245 deletions(-) delete mode 100644 src/lib/components/library/lib/libraryUpdater.ts diff --git a/src/lib/components/library/Library.svelte b/src/lib/components/library/Library.svelte index 26bbece..b6ee6f0 100644 --- a/src/lib/components/library/Library.svelte +++ b/src/lib/components/library/Library.svelte @@ -2,9 +2,9 @@ import { getAdapter } from '$lib/request-manager' import { libraryState } from '$lib/state/library.svelte' import type { LibrarySortOption, LibraryContentFilter, LibraryStatusFilter } from '$lib/state/library.svelte' - import { startLibraryUpdate } from '$lib/components/library/lib/libraryUpdater' import { addToast } from '$lib/state/notifications.svelte' import { updateSettings, settingsState } from '$lib/state/settings.svelte' + import { readerState } from '$lib/state/reader.svelte' import { goto } from '$app/navigation' import LibraryToolbar from '$lib/components/library/LibraryToolbar.svelte' import LibraryGrid from '$lib/components/library/LibraryGrid.svelte' @@ -23,13 +23,17 @@ const DT_TAB = 'application/x-moku-tab' const COMPLETED_NAME = 'Completed' - let cancelUpdate: (() => void) | null = null + let statusPollTimer: ReturnType | null = null let refreshDoneTimer: ReturnType | null = null + const UPDATE_STATUS_POLL_MS = 2_000 + let ctx: { x: number; y: number; manga: Manga } | null = $state(null) let emptyCtx: { x: number; y: number } | null = $state(null) - let bulkWorking: boolean = $state(false) + let bulkWorking: boolean = $state(false) + let sortPanelOpen: boolean = $state(false) + let filterPanelOpen: boolean = $state(false) let activeDragKind: 'tab' | null = $state(null) let dragInsertIdx = $state(-1) let dragTabId: string|null = $state(null) @@ -42,6 +46,9 @@ $effect(() => { libraryState.syncFromSettings(settingsState.settings) }) $effect(() => { libraryState.tab; libraryState.exitSelect() }) $effect(() => { libraryState.guardTab() }) + $effect(() => { + if (readerState.activeManga === null) loadLibrary() + }) async function loadLibrary() { libraryState.loading = true @@ -197,33 +204,57 @@ } finally { bulkWorking = false } } + function stopStatusPolling() { + if (!statusPollTimer) return + clearTimeout(statusPollTimer) + statusPollTimer = null + } + async function startRefresh() { if (libraryState.refreshing) return libraryState.refreshing = true libraryState.refreshProgress = { finished: 0, total: 0 } - cancelUpdate = startLibraryUpdate({ - onProgress(p) { libraryState.refreshProgress = p }, - async onDone({ newChapters, totalUpdated }) { - cancelUpdate = null - await loadLibrary() - libraryState.refreshing = false - libraryState.refreshDone = true - if (refreshDoneTimer) clearTimeout(refreshDoneTimer) - refreshDoneTimer = setTimeout(() => { libraryState.refreshDone = false }, 2500) - if (newChapters > 0) { - addToast({ kind: 'success', title: 'Library updated', body: `${newChapters} new chapter${newChapters !== 1 ? 's' : ''} across ${totalUpdated} series` }) - } else { - addToast({ kind: 'info', title: 'Already up to date' }) + try { + await getAdapter().checkForUpdates() + } catch (e) { + libraryState.refreshing = false + addToast({ kind: 'error', title: 'Update failed', body: String(e) }) + return + } + + const tick = async () => { + statusPollTimer = null + try { + const statusRes = await getAdapter().getLibraryUpdateStatus() + const wasRunning = libraryState.refreshing + + libraryState.refreshProgress = { + finished: statusRes.finishedJobs ?? 0, + total: statusRes.totalJobs ?? 0, } - }, - onError() { libraryState.refreshing = false; cancelUpdate = null }, - }) + + if (statusRes.isRunning) { + statusPollTimer = setTimeout(tick, UPDATE_STATUS_POLL_MS) + } else if (wasRunning) { + libraryState.refreshing = false + libraryState.refreshDone = true + if (refreshDoneTimer) clearTimeout(refreshDoneTimer) + refreshDoneTimer = setTimeout(() => { libraryState.refreshDone = false }, 2500) + await loadLibrary() + addToast({ kind: 'info', title: 'Library updated' }) + } + } catch { + if (libraryState.refreshing) statusPollTimer = setTimeout(tick, UPDATE_STATUS_POLL_MS) + } + } + + statusPollTimer = setTimeout(tick, UPDATE_STATUS_POLL_MS) } async function cancelRefresh() { if (!libraryState.refreshing) return - cancelUpdate?.(); cancelUpdate = null + stopStatusPolling() try { await getAdapter().stopLibraryUpdate() } catch {} libraryState.refreshing = false libraryState.refreshProgress = { finished: 0, total: 0 } @@ -370,7 +401,7 @@ visibleCategories={libraryState.visibleCategories} visibleTabIds={libraryState.visibleTabIds} counts={libraryState.counts} - query={libraryState.filter.query} + search={libraryState.filter.query} refreshing={libraryState.refreshing} refreshProgress={libraryState.refreshProgress} refreshDone={libraryState.refreshDone} @@ -379,13 +410,17 @@ {dragInsertIdx} {dragTabId} {dragOverTabId} + {sortPanelOpen} + {filterPanelOpen} onTabChange={(t) => libraryState.tab = t} - onQuery={(q) => libraryState.filter.query = q} + onSearchChange={(q) => libraryState.filter.query = q} onSortChange={(mode) => libraryState.setTabSort(libraryState.tab, mode)} onSortDirToggle={() => libraryState.toggleTabSortDir(libraryState.tab)} + onSortPanelToggle={() => sortPanelOpen = !sortPanelOpen} onStatusChange={(s) => libraryState.setTabStatus(libraryState.tab, s)} onFilterToggle={(f) => libraryState.toggleTabFilter(libraryState.tab, f)} onFiltersClear={() => libraryState.clearTabFilters(libraryState.tab)} + onFilterPanelToggle={() => filterPanelOpen = !filterPanelOpen} onRefresh={startRefresh} onCancelRefresh={cancelRefresh} onRefreshCategory={refreshCategory} diff --git a/src/lib/components/library/LibraryToolbar.svelte b/src/lib/components/library/LibraryToolbar.svelte index 5519e2b..11c6aa3 100644 --- a/src/lib/components/library/LibraryToolbar.svelte +++ b/src/lib/components/library/LibraryToolbar.svelte @@ -212,14 +212,14 @@ diff --git a/src/lib/components/library/lib/libraryUpdater.ts b/src/lib/components/library/lib/libraryUpdater.ts deleted file mode 100644 index 3b8061d..0000000 --- a/src/lib/components/library/lib/libraryUpdater.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { getAdapter } from '$lib/request-manager' - -const POLL_INTERVAL_MS = 2000 -const POLL_INITIAL_MS = 500 - -export interface UpdateProgress { - finished: number - total: number - skippedManga: number - skippedCategories: number -} - -export interface UpdateResult { - entries: UpdateEntry[] - totalUpdated: number - newChapters: number -} - -export interface UpdateEntry { - mangaId: number - mangaTitle: string - thumbnailUrl: string - newChapters: number - checkedAt: number -} - -export interface LibraryUpdaterCallbacks { - onProgress: (p: UpdateProgress) => void - onDone: (r: UpdateResult) => void - onError: (e?: unknown) => void -} - -function buildEntries( - mangaUpdates: { status: string; manga: { id: number; title: string; thumbnailUrl: string; unreadCount: number } }[] -): UpdateEntry[] { - const byManga = new Map() - for (const u of mangaUpdates) { - if (u.status !== 'UPDATED') continue - const existing = byManga.get(u.manga.id) - if (existing) { - existing.newChapters++ - } else { - byManga.set(u.manga.id, { - mangaId: u.manga.id, - mangaTitle: u.manga.title, - thumbnailUrl: u.manga.thumbnailUrl, - newChapters: 1, - checkedAt: Date.now(), - }) - } - } - return [...byManga.values()] -} - -export function startLibraryUpdate(callbacks: LibraryUpdaterCallbacks): () => void { - let timer: ReturnType | null = null - let cancelled = false - - function cancel() { - cancelled = true - if (timer) { clearTimeout(timer); timer = null } - } - - async function run() { - let jobsStarted = false - - try { - const status = await getAdapter().checkForUpdates() - if (cancelled) return - - const { jobsInfo } = status - jobsStarted = jobsInfo.totalJobs > 0 - - callbacks.onProgress({ - finished: jobsInfo.finishedJobs, - total: jobsInfo.totalJobs, - skippedManga: jobsInfo.skippedMangasCount, - skippedCategories: jobsInfo.skippedCategoriesCount, - }) - - if (!jobsStarted || !jobsInfo.isRunning) { - callbacks.onDone({ entries: [], totalUpdated: 0, newChapters: 0 }) - return - } - } catch (e) { - console.error('[libraryUpdater] failed to start update', e) - if (!cancelled) callbacks.onError(e) - return - } - - function poll() { - getAdapter().getLibraryUpdateStatus() - .then(d => { - if (cancelled) return - const { jobsInfo, mangaUpdates } = d - - if (jobsInfo.totalJobs > 0) jobsStarted = true - callbacks.onProgress({ - finished: jobsInfo.finishedJobs, - total: jobsInfo.totalJobs, - skippedManga: jobsInfo.skippedMangasCount, - skippedCategories: jobsInfo.skippedCategoriesCount, - }) - - if (!jobsInfo.isRunning && jobsStarted) { - const entries = buildEntries(mangaUpdates) - const newChapters = entries.reduce((s, e) => s + e.newChapters, 0) - callbacks.onDone({ entries, totalUpdated: entries.length, newChapters }) - return - } - - timer = setTimeout(poll, POLL_INTERVAL_MS) - }) - .catch(e => { - console.error('[libraryUpdater] poll error', e) - if (!cancelled) callbacks.onError(e) - }) - } - - timer = setTimeout(poll, POLL_INITIAL_MS) - } - - run() - return cancel -} \ No newline at end of file diff --git a/src/lib/components/reader/Reader.svelte b/src/lib/components/reader/Reader.svelte index 8cbeb61..c82e1b5 100644 --- a/src/lib/components/reader/Reader.svelte +++ b/src/lib/components/reader/Reader.svelte @@ -12,6 +12,7 @@ import { clampZoom, captureZoomAnchor, restoreZoomAnchor } from "$lib/components/reader/lib/zoomHelpers"; import { loadChapter, scheduleResumeDismiss } from "$lib/components/reader/lib/chapterLoader"; import { historyState } from "$lib/state/history.svelte"; + import { getAdapter } from "$lib/request-manager"; import type { ReaderSettings } from "$lib/state/reader.svelte"; import ReaderControls from "$lib/components/reader/ReaderControls.svelte"; import PageView from "$lib/components/reader/PageView.svelte"; @@ -380,19 +381,7 @@ const toQueue = list.slice(idx + 1, idx + 1 + prefs.downloadAhead) .filter(c => !c.downloaded && !c.read) .map(c => c.id); - if (toQueue.length) { - const DL = `mutation EnqueueDl($ids: [Int!]!) { enqueueChaptersDownloads(input: { ids: $ids }) { downloadStatus { queue { chapter { id } } } } }`; - const base = settingsState.settings.serverUrl ?? "http://localhost:4567"; - const headers: Record = { "Content-Type": "application/json" }; - const mode = settingsState.settings.serverAuthMode ?? "NONE"; - if (mode === "BASIC_AUTH") { - const u = settingsState.settings.serverAuthUser?.trim() ?? ""; - const p = settingsState.settings.serverAuthPass?.trim() ?? ""; - if (u && p) headers["Authorization"] = `Basic ${btoa(`${u}:${p}`)}`; - } - fetch(`${base}/api/graphql`, { method: "POST", headers, body: JSON.stringify({ query: DL, variables: { ids: toQueue } }) }) - .catch(console.error); - } + if (toQueue.length) getAdapter().enqueueDownloads(toQueue.map(String)).catch(console.error); } } }); diff --git a/src/lib/components/reader/ReaderControls.svelte b/src/lib/components/reader/ReaderControls.svelte index a817020..db500dd 100644 --- a/src/lib/components/reader/ReaderControls.svelte +++ b/src/lib/components/reader/ReaderControls.svelte @@ -3,10 +3,11 @@ X, CaretLeft, CaretRight, CaretUp, CaretDown, MagnifyingGlassMinus, MagnifyingGlassPlus, Bookmark, MapPin, Download, Check, GearSix, Sliders, - ArrowsOut, ArrowsIn, + ArrowsOut, ArrowsIn, Minus, } from "phosphor-svelte"; import { readerState, MARKER_COLORS, MARKER_COLOR_HEX, ZOOM_STEP, ZOOM_MIN, ZOOM_MAX } from "$lib/state/reader.svelte"; - import { settingsState } from "$lib/state/settings.svelte"; + import { getAdapter } from "$lib/request-manager"; + import { platformService } from "$lib/platform-service"; import { fly } from "svelte/transition"; import { cubicOut, cubicIn } from "svelte/easing"; import type { Chapter } from "$lib/types"; @@ -52,24 +53,6 @@ const queueable = $derived(adjacent.remaining.filter(c => !c.downloaded)); - async function gqlMutation(query: string, variables: Record): Promise { - const base = settingsState.settings.serverUrl ?? "http://localhost:4567"; - const headers: Record = { "Content-Type": "application/json" }; - const mode = settingsState.settings.serverAuthMode ?? "NONE"; - if (mode === "BASIC_AUTH") { - const u = settingsState.settings.serverAuthUser?.trim() ?? ""; - const p = settingsState.settings.serverAuthPass?.trim() ?? ""; - if (u && p) headers["Authorization"] = `Basic ${btoa(`${u}:${p}`)}`; - } - const res = await fetch(`${base}/api/graphql`, { method: "POST", headers, body: JSON.stringify({ query, variables }) }); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const json = await res.json(); - if (json.errors?.length) throw new Error(json.errors[0].message); - } - - const ENQUEUE_ONE = `mutation EnqueueOne($id: Int!) { fetchChapterPages(input: { chapterId: $id }) { chapter { id } } }`; - const ENQUEUE_MANY = `mutation EnqueueMany($ids: [Int!]!) { enqueueChaptersDownloads(input: { ids: $ids }) { downloadStatus { queue { chapter { id } } } } }`; - async function runDl(fn: () => Promise) { readerState.dlBusy = true; try { await fn(); } catch (e) { console.error(e); } @@ -77,6 +60,14 @@ readerState.dlOpen = false; } + function enqueueOne(chapterId: number) { + return getAdapter().enqueueDownload(String(chapterId)); + } + + function enqueueMany(chapterIds: number[]) { + return getAdapter().enqueueDownloads(chapterIds.map(String)); + } + const isVertical = $derived(barPosition === "left" || barPosition === "right"); const popoverSide = $derived( barPosition === "left" ? "right" : @@ -96,9 +87,10 @@ onRestoreZoomAnchor(); } + const isTauri = platformService.platform === "tauri"; + async function toggleFullscreen() { - if (!document.fullscreenElement) await document.documentElement.requestFullscreen(); - else await document.exitFullscreen(); + await platformService.toggleFullscreen(); } function closeAllPopovers() { @@ -337,6 +329,16 @@ Fullscreen {/if} + {#if isTauri} + + + {/if} {/if} @@ -345,13 +347,13 @@ @@ -661,8 +663,10 @@ transition: background var(--t-fast), color var(--t-fast); } .action-row:hover { background: var(--bg-overlay); color: var(--text-primary); } + .action-row.action-row-danger:hover { background: color-mix(in srgb, #c0392b 15%, transparent); color: var(--color-error, #e57373); } .action-row svg, .action-row :global(svg) { flex-shrink: 0; color: var(--text-faint); } .action-row:hover svg, .action-row:hover :global(svg) { color: var(--text-muted); } + .action-row-danger:hover svg, .action-row-danger:hover :global(svg) { color: var(--color-error, #e57373); } .action-divider { height: 1px; background: var(--border-dim); margin: var(--sp-1) 0; } diff --git a/src/lib/server-adapters/suwayomi/downloads.ts b/src/lib/server-adapters/suwayomi/downloads.ts index 5954078..42629c7 100644 --- a/src/lib/server-adapters/suwayomi/downloads.ts +++ b/src/lib/server-adapters/suwayomi/downloads.ts @@ -79,15 +79,6 @@ export const CLEAR_DOWNLOADER = ` } ` -export const FETCH_SOURCE_MANGA = ` - mutation FetchSourceManga($source: LongString!, $type: FetchSourceMangaType!, $page: Int!, $query: String, $filters: [FilterChangeInput!]) { - fetchSourceManga(input: { source: $source, type: $type, page: $page, query: $query, filters: $filters }) { - mangas { id title thumbnailUrl inLibrary } - hasNextPage - } - } -` - export const SET_DOWNLOADS_PATH = ` mutation SetDownloadsPath($path: String!) { setSettings(input: { settings: { downloadsPath: $path } }) { diff --git a/src/lib/server-adapters/suwayomi/index.ts b/src/lib/server-adapters/suwayomi/index.ts index 205c788..0697cff 100644 --- a/src/lib/server-adapters/suwayomi/index.ts +++ b/src/lib/server-adapters/suwayomi/index.ts @@ -23,12 +23,14 @@ import { GET_LIBRARY, GET_MANGA, GET_CATEGORIES, + GET_DOWNLOADS_PATH, FETCH_MANGA, UPDATE_MANGA, UPDATE_MANGAS, UPDATE_MANGA_CATEGORIES, UPDATE_MANGAS_CATEGORIES, CREATE_CATEGORY, + UPDATE_CATEGORY, DELETE_CATEGORY, UPDATE_CATEGORY_ORDER, UPDATE_CATEGORY_MANGA, @@ -37,6 +39,8 @@ import { UPDATE_STOP, SET_MANGA_META, DELETE_MANGA_META, + CREATE_BACKUP, + RESTORE_BACKUP, FETCH_SOURCE_MANGA, LIBRARY_UPDATE_STATUS, MANGAS_BY_GENRE, @@ -64,16 +68,26 @@ import { START_DOWNLOADER, STOP_DOWNLOADER, CLEAR_DOWNLOADER, + SET_DOWNLOADS_PATH, + SET_LOCAL_SOURCE_PATH, } from './downloads' import { GET_EXTENSIONS, GET_SOURCES, + GET_SOURCE_SETTINGS, + GET_SETTINGS, GET_SERVER_SECURITY, FETCH_EXTENSIONS, UPDATE_EXTENSION, UPDATE_EXTENSIONS, INSTALL_EXTERNAL_EXTENSION, + UPDATE_SOURCE_PREFERENCE, + SET_SOURCE_META, + DELETE_SOURCE_META, + SET_EXTENSION_REPOS, SET_SERVER_AUTH, + CLEAR_CACHED_IMAGES, + RESET_SETTINGS, } from './extensions' import { GET_TRACKERS, @@ -85,10 +99,17 @@ import { UNLINK_TRACK, TRACK_PROGRESS, UPDATE_TRACK, + LOGIN_TRACKER_CREDENTIALS, + LOGOUT_TRACKER, } from './tracking' import { GET_ABOUT_SERVER, GET_ABOUT_WEBUI, + CHECK_FOR_SERVER_UPDATES, + GET_META, + GET_METAS, + SET_SOCKS_PROXY, + SET_FLARE_SOLVERR, } from './meta' import { type GQLResponse, @@ -100,50 +121,6 @@ import { } from './types' import { initPageCache, clearPageCache as _clearPageCache } from './pageCache' -const SET_SOCKS_PROXY = ` - mutation SetSocksProxy( - $socksProxyEnabled: Boolean! - $socksProxyHost: String! - $socksProxyPort: String! - $socksProxyVersion: Int! - $socksProxyUsername: String! - $socksProxyPassword: String! - ) { - setSettings(input: { settings: { - socksProxyEnabled: $socksProxyEnabled - socksProxyHost: $socksProxyHost - socksProxyPort: $socksProxyPort - socksProxyVersion: $socksProxyVersion - socksProxyUsername: $socksProxyUsername - socksProxyPassword: $socksProxyPassword - }}) { - settings { socksProxyEnabled socksProxyHost socksProxyPort } - } - } -` - -const SET_FLARE_SOLVERR = ` - mutation SetFlareSolverr( - $flareSolverrEnabled: Boolean! - $flareSolverrUrl: String! - $flareSolverrTimeout: Int! - $flareSolverrSessionName: String! - $flareSolverrSessionTtl: Int! - $flareSolverrAsResponseFallback: Boolean! - ) { - setSettings(input: { settings: { - flareSolverrEnabled: $flareSolverrEnabled - flareSolverrUrl: $flareSolverrUrl - flareSolverrTimeout: $flareSolverrTimeout - flareSolverrSessionName: $flareSolverrSessionName - flareSolverrSessionTtl: $flareSolverrSessionTtl - flareSolverrAsResponseFallback: $flareSolverrAsResponseFallback - }}) { - settings { flareSolverrEnabled flareSolverrUrl } - } - } -` - type RawQueueItem = Record function mapDownloadStatus(raw: { state: string; queue: RawQueueItem[] }): DownloadStatus { diff --git a/src/lib/server-adapters/suwayomi/meta.ts b/src/lib/server-adapters/suwayomi/meta.ts index 8718ffe..d0227c1 100644 --- a/src/lib/server-adapters/suwayomi/meta.ts +++ b/src/lib/server-adapters/suwayomi/meta.ts @@ -36,4 +36,48 @@ export const GET_METAS = ` nodes { key value } } } +` + +export const SET_SOCKS_PROXY = ` + mutation SetSocksProxy( + $socksProxyEnabled: Boolean! + $socksProxyHost: String! + $socksProxyPort: String! + $socksProxyVersion: Int! + $socksProxyUsername: String! + $socksProxyPassword: String! + ) { + setSettings(input: { settings: { + socksProxyEnabled: $socksProxyEnabled + socksProxyHost: $socksProxyHost + socksProxyPort: $socksProxyPort + socksProxyVersion: $socksProxyVersion + socksProxyUsername: $socksProxyUsername + socksProxyPassword: $socksProxyPassword + }}) { + settings { socksProxyEnabled socksProxyHost socksProxyPort } + } + } +` + +export const SET_FLARE_SOLVERR = ` + mutation SetFlareSolverr( + $flareSolverrEnabled: Boolean! + $flareSolverrUrl: String! + $flareSolverrTimeout: Int! + $flareSolverrSessionName: String! + $flareSolverrSessionTtl: Int! + $flareSolverrAsResponseFallback: Boolean! + ) { + setSettings(input: { settings: { + flareSolverrEnabled: $flareSolverrEnabled + flareSolverrUrl: $flareSolverrUrl + flareSolverrTimeout: $flareSolverrTimeout + flareSolverrSessionName: $flareSolverrSessionName + flareSolverrSessionTtl: $flareSolverrSessionTtl + flareSolverrAsResponseFallback: $flareSolverrAsResponseFallback + }}) { + settings { flareSolverrEnabled flareSolverrUrl } + } + } ` \ No newline at end of file diff --git a/src/lib/state/library.svelte.ts b/src/lib/state/library.svelte.ts index 6785d45..4aaf7b3 100644 --- a/src/lib/state/library.svelte.ts +++ b/src/lib/state/library.svelte.ts @@ -149,7 +149,7 @@ class LibraryState { const f = this.tabFilters[tab] ?? {}; if (f.unread) items = items.filter(m => (m.unreadCount ?? 0) > 0); - if (f.started) items = items.filter(m => (m.unreadCount ?? 0) > 0 && (m.chapters?.totalCount ?? 0) > (m.unreadCount ?? 0)); + if (f.started) items = items.filter(m => (m.unreadCount ?? 0) > 0 && (m.totalChapters ?? 0) > (m.unreadCount ?? 0)); if (f.downloaded) items = items.filter(m => (m.downloadCount ?? 0) > 0); if (f.bookmarked) items = items.filter(m => (m.bookmarkCount ?? 0) > 0);