mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-14 09:49:58 -05:00
Fix: Library Filtering + GQL Cleanup P.1
This commit is contained in:
@@ -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<typeof setTimeout> | null = null
|
||||
let refreshDoneTimer: ReturnType<typeof setTimeout> | 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}
|
||||
|
||||
@@ -212,14 +212,14 @@
|
||||
</div>
|
||||
|
||||
<LibraryFilters
|
||||
{tabStatus}
|
||||
{tabFilters}
|
||||
{hasActiveFilters}
|
||||
{filterPanelOpen}
|
||||
status={tabStatus}
|
||||
filters={tabFilters}
|
||||
hasActive={hasActiveFilters}
|
||||
open={filterPanelOpen}
|
||||
onToggle={onFilterPanelToggle}
|
||||
{onStatusChange}
|
||||
{onFilterToggle}
|
||||
{onFiltersClear}
|
||||
{onFilterPanelToggle}
|
||||
onClear={onFiltersClear}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<number, UpdateEntry>()
|
||||
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<typeof setTimeout> | 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
|
||||
}
|
||||
Reference in New Issue
Block a user