diff --git a/Todo b/Todo index e34e861..a9a9593 100644 --- a/Todo +++ b/Todo @@ -2,22 +2,15 @@ Major Revisions: - Moku-Share to Easily Migrate/Share Manga (Investigate Usecase/Feasibility) Minor Revisions: - - Improve Deduplication Algorithm with Advanced Option Settings (Implement Chapter & Author-based Comparisons, Resource Intensive) - - Investigate feasibility of Multi-Page Screenshot (Reader) - Add Hover Info on Library (Make sure doesn't conflict with additional clicks) - Revise Migration (https://github.com/Suwayomi/Suwayomi-WebUI/pull/1073) - Look at how Manga are Organized in WebUI and Implement into Series-Detail (Chapter Display is Off) - - Adjustment in Settings for Theme Editor: - - Patch Color-Picker to Work Properly - - Integrate Download Directory Changes (Settings) Priority Bugs: - - Fix Library Build not Updating - - Loading Buffer for Pictures (Due to Auth Lag) - + - Fix Library-Refresh System (TESTING) General/Misc Bugs: - Fix Highlightable Elements @@ -29,18 +22,32 @@ General/Misc Bugs: In-Progress: - Enable Cloudflare Bypass (Suwayomi Config) (Requires Patching) - - Working on 3D Display Cards - - - Add Small QOL Animations where Appropriate - - Search Animations - - Card Hover - - SeriesDetail Animations - - 3D Card - - Add Flathub Support (Pending Video) - - Currently Moku Migration is on Series. Finish Modularizing then fix rest of files. + - QOL Animations & Revamps + - Extensions QOL Animations + - Folders Slide + - Dropdown Formatting (Repositories, Etc) + - Extensions Revamps + - Notification on Extension Added + - Notification on Extension Refresh + - Notification on Extension Update + - Fix Pill-Shaped Language Filter + - Fix ALL ALL EN Tag Issue + - Search QOL Animations + - Languages Dropdown Animations + - Search Revamps + - Custom Language Selector Modal + - Change Tab Selector to match Extensions & Library Folders (Design) + - Filter Genre should Filter Tags as well + - Tracking Revamp + - Completely Revamp Tracking + + - Fix Search Folder Tabs (Right-Align) -Testing: \ No newline at end of file +Testing Bugs: + - Reader Zoom does not work (Dropdown Slider, Value Adjustment); Goes to NaN + - Fix Library Folders (Uneven Padding + Bleed into Other Folders); Appears Constraints are Off + - \ No newline at end of file diff --git a/src/api/queries/chapters.ts b/src/api/queries/chapters.ts index aca9946..f5d973f 100644 --- a/src/api/queries/chapters.ts +++ b/src/api/queries/chapters.ts @@ -1,3 +1,15 @@ +export const GET_RECENTLY_UPDATED = ` + query GetRecentlyUpdated { + chapters(orderBy: FETCHED_AT, orderByType: DESC, first: 300) { + nodes { + mangaId + fetchedAt + manga { id title thumbnailUrl inLibrary } + } + } + } +`; + export const GET_CHAPTERS = ` query GetChapters($mangaId: Int!) { chapters(condition: { mangaId: $mangaId }) { @@ -7,4 +19,4 @@ export const GET_CHAPTERS = ` } } } -`; +`; \ No newline at end of file diff --git a/src/features/library/components/Library.svelte b/src/features/library/components/Library.svelte index 32ccd73..3be08a9 100644 --- a/src/features/library/components/Library.svelte +++ b/src/features/library/components/Library.svelte @@ -5,12 +5,13 @@ import { GET_CATEGORIES, GET_LIBRARY, UPDATE_MANGA, UPDATE_MANGAS, GET_CHAPTERS, DELETE_DOWNLOADED_CHAPTERS, DEQUEUE_DOWNLOAD, - CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES, UPDATE_LIBRARY, LIBRARY_UPDATE_STATUS, + CREATE_CATEGORY, UPDATE_MANGA_CATEGORIES, UPDATE_CATEGORY_ORDER, } from "@api"; import { cache, CACHE_KEYS, CACHE_GROUPS, DEFAULT_TTL_MS } from "@core/cache/queryCache"; import { dedupeMangaById, dedupeMangaByTitle, shouldHideNsfw } from "@core/util"; import { sortLibrary } from "../lib/librarySort"; + import { startLibraryUpdate } from "../lib/libraryUpdater"; import { createPaginator } from "@core/algorithms/paginate"; import { store, setCategories, setLibraryUpdates, addToast, @@ -57,7 +58,7 @@ let refreshing: boolean = $state(false); let refreshProgress = $state({ finished: 0, total: 0 }); - let pollTimer: ReturnType | null = null; + let cancelUpdate: (() => void) | null = null; let refreshDone: boolean = $state(false); let refreshDoneTimer: ReturnType | null = null; @@ -378,47 +379,27 @@ if (refreshing) return; refreshing = true; refreshProgress = { finished: 0, total: 0 }; - const prevCounts = new Map(allManga.map(m => [m.id, m.unreadCount ?? 0])); - let seenWork = false; - try { - const res = await gql<{ updateLibrary: { updateStatus: { jobsInfo: { isRunning: boolean; totalJobs: number } } } }>(UPDATE_LIBRARY, {}); - seenWork = res.updateLibrary.updateStatus.jobsInfo.totalJobs > 0; - } catch { refreshing = false; return; } - - pollTimer = setTimeout(function poll() { - gql<{ libraryUpdateStatus: { - jobsInfo: { isRunning: boolean; finishedJobs: number; totalJobs: number }; - mangaUpdates: { status: string; manga: { id: number; title: string; thumbnailUrl: string; unreadCount: number } }[]; - } }>(LIBRARY_UPDATE_STATUS, {}) - .then(d => { - const { jobsInfo, mangaUpdates } = d.libraryUpdateStatus; - refreshProgress = { finished: jobsInfo.finishedJobs, total: jobsInfo.totalJobs }; - if (jobsInfo.totalJobs > 0) seenWork = true; - - if (!jobsInfo.isRunning && seenWork) { - refreshing = false; - pollTimer = null; - const entries: LibraryUpdateEntry[] = mangaUpdates - .filter(u => u.status === "FINISHED") - .reduce((acc, u) => { - const newChapters = Math.max(0, (u.manga.unreadCount ?? 0) - (prevCounts.get(u.manga.id) ?? 0)); - if (newChapters > 0) acc.push({ mangaId: u.manga.id, mangaTitle: u.manga.title, thumbnailUrl: u.manga.thumbnailUrl, newChapters, checkedAt: Date.now() }); - return acc; - }, []); - setLibraryUpdates(entries); - cache.clearGroup(CACHE_GROUPS.LIBRARY); - loadData(); - refreshDone = true; - if (refreshDoneTimer) clearTimeout(refreshDoneTimer); - refreshDoneTimer = setTimeout(() => { refreshDone = false; }, 2500); - showToast(entries.reduce((s, e) => s + e.newChapters, 0), entries.length); - return; - } - pollTimer = setTimeout(poll, 3000); - }) - .catch(() => { refreshing = false; pollTimer = null; }); - }, 2000); + cancelUpdate = startLibraryUpdate({ + onProgress(p) { + refreshProgress = p; + }, + async onDone({ entries, totalUpdated, newChapters }) { + refreshing = false; + cancelUpdate = null; + setLibraryUpdates(entries); + cache.clearGroup(CACHE_GROUPS.LIBRARY); + await loadData(); + refreshDone = true; + if (refreshDoneTimer) clearTimeout(refreshDoneTimer); + refreshDoneTimer = setTimeout(() => { refreshDone = false; }, 2500); + showToast(newChapters, totalUpdated); + }, + onError() { + refreshing = false; + cancelUpdate = null; + }, + }); } function onTabDragStart(e: DragEvent, cat: Category) { @@ -482,7 +463,7 @@ return () => { ro.disconnect(); unsub(); - if (pollTimer) clearTimeout(pollTimer); + cancelUpdate?.(); window.removeEventListener("keydown", onKeyDown); document.removeEventListener("mousedown", onDocMouseDown, true); }; diff --git a/src/features/library/lib/libraryUpdater.ts b/src/features/library/lib/libraryUpdater.ts new file mode 100644 index 0000000..e2de95e --- /dev/null +++ b/src/features/library/lib/libraryUpdater.ts @@ -0,0 +1,109 @@ +import { gql } from "@api/client"; +import { LIBRARY_UPDATE_STATUS } from "@api/queries/manga"; +import { UPDATE_LIBRARY } from "@api/mutations/manga"; +import { GET_RECENTLY_UPDATED } from "@api/queries/chapters"; +import type { LibraryUpdateEntry } from "@store/state.svelte"; + +const POLL_INTERVAL_MS = 3000; +const POLL_INITIAL_MS = 2000; + +export interface UpdateProgress { + finished: number; + total: number; +} + +export interface UpdateResult { + entries: LibraryUpdateEntry[]; + totalUpdated: number; + newChapters: number; +} + +export interface LibraryUpdaterCallbacks { + onProgress: (p: UpdateProgress) => void; + onDone: (r: UpdateResult) => void; + onError: () => void; +} + +export function startLibraryUpdate(callbacks: LibraryUpdaterCallbacks): () => void { + let timer: ReturnType | null = null; + let cancelled = false; + const startedAt = Math.floor(Date.now() / 1000); + + function cancel() { + cancelled = true; + if (timer) { clearTimeout(timer); timer = null; } + } + + async function run() { + let seenWork = false; + + try { + const res = await gql<{ + updateLibrary: { updateStatus: { jobsInfo: { isRunning: boolean; totalJobs: number } } } + }>(UPDATE_LIBRARY, {}); + if (cancelled) return; + seenWork = res.updateLibrary.updateStatus.jobsInfo.totalJobs > 0; + } catch { + if (!cancelled) callbacks.onError(); + return; + } + + function poll() { + gql<{ + libraryUpdateStatus: { + jobsInfo: { isRunning: boolean; finishedJobs: number; totalJobs: number }; + mangaUpdates: { status: string; manga: { id: number } }[]; + } + }>(LIBRARY_UPDATE_STATUS, {}) + .then(async d => { + if (cancelled) return; + const { jobsInfo } = d.libraryUpdateStatus; + + if (jobsInfo.totalJobs > 0) seenWork = true; + callbacks.onProgress({ finished: jobsInfo.finishedJobs, total: jobsInfo.totalJobs }); + + if (!jobsInfo.isRunning && seenWork) { + const recent = await gql<{ + chapters: { nodes: { mangaId: number; mangaTitle: string; thumbnailUrl: string; fetchedAt: string }[] } + }>(GET_RECENTLY_UPDATED, {}).catch(() => ({ chapters: { nodes: [] } })); + + if (cancelled) return; + + const byManga = new Map(); + for (const ch of recent.chapters.nodes) { + if (!ch.manga.inLibrary) continue; + if (Number(ch.fetchedAt) < startedAt) continue; + const existing = byManga.get(ch.mangaId); + if (existing) { + existing.newChapters++; + } else { + byManga.set(ch.mangaId, { + mangaId: ch.mangaId, + mangaTitle: ch.mangaTitle, + thumbnailUrl: ch.thumbnailUrl, + newChapters: 1, + checkedAt: Date.now(), + }); + } + } + + const entries = [...byManga.values()]; + 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(() => { + if (!cancelled) callbacks.onError(); + }); + } + + timer = setTimeout(poll, POLL_INITIAL_MS); + } + + run(); + return cancel; +} \ No newline at end of file