mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -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 { getAdapter } from '$lib/request-manager'
|
||||||
import { libraryState } from '$lib/state/library.svelte'
|
import { libraryState } from '$lib/state/library.svelte'
|
||||||
import type { LibrarySortOption, LibraryContentFilter, LibraryStatusFilter } 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 { addToast } from '$lib/state/notifications.svelte'
|
||||||
import { updateSettings, settingsState } from '$lib/state/settings.svelte'
|
import { updateSettings, settingsState } from '$lib/state/settings.svelte'
|
||||||
|
import { readerState } from '$lib/state/reader.svelte'
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
import LibraryToolbar from '$lib/components/library/LibraryToolbar.svelte'
|
import LibraryToolbar from '$lib/components/library/LibraryToolbar.svelte'
|
||||||
import LibraryGrid from '$lib/components/library/LibraryGrid.svelte'
|
import LibraryGrid from '$lib/components/library/LibraryGrid.svelte'
|
||||||
@@ -23,13 +23,17 @@
|
|||||||
const DT_TAB = 'application/x-moku-tab'
|
const DT_TAB = 'application/x-moku-tab'
|
||||||
const COMPLETED_NAME = 'Completed'
|
const COMPLETED_NAME = 'Completed'
|
||||||
|
|
||||||
let cancelUpdate: (() => void) | null = null
|
let statusPollTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let refreshDoneTimer: 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 ctx: { x: number; y: number; manga: Manga } | null = $state(null)
|
||||||
let emptyCtx: { x: number; y: number } | 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 activeDragKind: 'tab' | null = $state(null)
|
||||||
let dragInsertIdx = $state(-1)
|
let dragInsertIdx = $state(-1)
|
||||||
let dragTabId: string|null = $state(null)
|
let dragTabId: string|null = $state(null)
|
||||||
@@ -42,6 +46,9 @@
|
|||||||
$effect(() => { libraryState.syncFromSettings(settingsState.settings) })
|
$effect(() => { libraryState.syncFromSettings(settingsState.settings) })
|
||||||
$effect(() => { libraryState.tab; libraryState.exitSelect() })
|
$effect(() => { libraryState.tab; libraryState.exitSelect() })
|
||||||
$effect(() => { libraryState.guardTab() })
|
$effect(() => { libraryState.guardTab() })
|
||||||
|
$effect(() => {
|
||||||
|
if (readerState.activeManga === null) loadLibrary()
|
||||||
|
})
|
||||||
|
|
||||||
async function loadLibrary() {
|
async function loadLibrary() {
|
||||||
libraryState.loading = true
|
libraryState.loading = true
|
||||||
@@ -197,33 +204,57 @@
|
|||||||
} finally { bulkWorking = false }
|
} finally { bulkWorking = false }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stopStatusPolling() {
|
||||||
|
if (!statusPollTimer) return
|
||||||
|
clearTimeout(statusPollTimer)
|
||||||
|
statusPollTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
async function startRefresh() {
|
async function startRefresh() {
|
||||||
if (libraryState.refreshing) return
|
if (libraryState.refreshing) return
|
||||||
libraryState.refreshing = true
|
libraryState.refreshing = true
|
||||||
libraryState.refreshProgress = { finished: 0, total: 0 }
|
libraryState.refreshProgress = { finished: 0, total: 0 }
|
||||||
|
|
||||||
cancelUpdate = startLibraryUpdate({
|
try {
|
||||||
onProgress(p) { libraryState.refreshProgress = p },
|
await getAdapter().checkForUpdates()
|
||||||
async onDone({ newChapters, totalUpdated }) {
|
} catch (e) {
|
||||||
cancelUpdate = null
|
libraryState.refreshing = false
|
||||||
await loadLibrary()
|
addToast({ kind: 'error', title: 'Update failed', body: String(e) })
|
||||||
libraryState.refreshing = false
|
return
|
||||||
libraryState.refreshDone = true
|
}
|
||||||
if (refreshDoneTimer) clearTimeout(refreshDoneTimer)
|
|
||||||
refreshDoneTimer = setTimeout(() => { libraryState.refreshDone = false }, 2500)
|
const tick = async () => {
|
||||||
if (newChapters > 0) {
|
statusPollTimer = null
|
||||||
addToast({ kind: 'success', title: 'Library updated', body: `${newChapters} new chapter${newChapters !== 1 ? 's' : ''} across ${totalUpdated} series` })
|
try {
|
||||||
} else {
|
const statusRes = await getAdapter().getLibraryUpdateStatus()
|
||||||
addToast({ kind: 'info', title: 'Already up to date' })
|
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() {
|
async function cancelRefresh() {
|
||||||
if (!libraryState.refreshing) return
|
if (!libraryState.refreshing) return
|
||||||
cancelUpdate?.(); cancelUpdate = null
|
stopStatusPolling()
|
||||||
try { await getAdapter().stopLibraryUpdate() } catch {}
|
try { await getAdapter().stopLibraryUpdate() } catch {}
|
||||||
libraryState.refreshing = false
|
libraryState.refreshing = false
|
||||||
libraryState.refreshProgress = { finished: 0, total: 0 }
|
libraryState.refreshProgress = { finished: 0, total: 0 }
|
||||||
@@ -370,7 +401,7 @@
|
|||||||
visibleCategories={libraryState.visibleCategories}
|
visibleCategories={libraryState.visibleCategories}
|
||||||
visibleTabIds={libraryState.visibleTabIds}
|
visibleTabIds={libraryState.visibleTabIds}
|
||||||
counts={libraryState.counts}
|
counts={libraryState.counts}
|
||||||
query={libraryState.filter.query}
|
search={libraryState.filter.query}
|
||||||
refreshing={libraryState.refreshing}
|
refreshing={libraryState.refreshing}
|
||||||
refreshProgress={libraryState.refreshProgress}
|
refreshProgress={libraryState.refreshProgress}
|
||||||
refreshDone={libraryState.refreshDone}
|
refreshDone={libraryState.refreshDone}
|
||||||
@@ -379,13 +410,17 @@
|
|||||||
{dragInsertIdx}
|
{dragInsertIdx}
|
||||||
{dragTabId}
|
{dragTabId}
|
||||||
{dragOverTabId}
|
{dragOverTabId}
|
||||||
|
{sortPanelOpen}
|
||||||
|
{filterPanelOpen}
|
||||||
onTabChange={(t) => libraryState.tab = t}
|
onTabChange={(t) => libraryState.tab = t}
|
||||||
onQuery={(q) => libraryState.filter.query = q}
|
onSearchChange={(q) => libraryState.filter.query = q}
|
||||||
onSortChange={(mode) => libraryState.setTabSort(libraryState.tab, mode)}
|
onSortChange={(mode) => libraryState.setTabSort(libraryState.tab, mode)}
|
||||||
onSortDirToggle={() => libraryState.toggleTabSortDir(libraryState.tab)}
|
onSortDirToggle={() => libraryState.toggleTabSortDir(libraryState.tab)}
|
||||||
|
onSortPanelToggle={() => sortPanelOpen = !sortPanelOpen}
|
||||||
onStatusChange={(s) => libraryState.setTabStatus(libraryState.tab, s)}
|
onStatusChange={(s) => libraryState.setTabStatus(libraryState.tab, s)}
|
||||||
onFilterToggle={(f) => libraryState.toggleTabFilter(libraryState.tab, f)}
|
onFilterToggle={(f) => libraryState.toggleTabFilter(libraryState.tab, f)}
|
||||||
onFiltersClear={() => libraryState.clearTabFilters(libraryState.tab)}
|
onFiltersClear={() => libraryState.clearTabFilters(libraryState.tab)}
|
||||||
|
onFilterPanelToggle={() => filterPanelOpen = !filterPanelOpen}
|
||||||
onRefresh={startRefresh}
|
onRefresh={startRefresh}
|
||||||
onCancelRefresh={cancelRefresh}
|
onCancelRefresh={cancelRefresh}
|
||||||
onRefreshCategory={refreshCategory}
|
onRefreshCategory={refreshCategory}
|
||||||
|
|||||||
@@ -212,14 +212,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LibraryFilters
|
<LibraryFilters
|
||||||
{tabStatus}
|
status={tabStatus}
|
||||||
{tabFilters}
|
filters={tabFilters}
|
||||||
{hasActiveFilters}
|
hasActive={hasActiveFilters}
|
||||||
{filterPanelOpen}
|
open={filterPanelOpen}
|
||||||
|
onToggle={onFilterPanelToggle}
|
||||||
{onStatusChange}
|
{onStatusChange}
|
||||||
{onFilterToggle}
|
{onFilterToggle}
|
||||||
{onFiltersClear}
|
onClear={onFiltersClear}
|
||||||
{onFilterPanelToggle}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
import { clampZoom, captureZoomAnchor, restoreZoomAnchor } from "$lib/components/reader/lib/zoomHelpers";
|
import { clampZoom, captureZoomAnchor, restoreZoomAnchor } from "$lib/components/reader/lib/zoomHelpers";
|
||||||
import { loadChapter, scheduleResumeDismiss } from "$lib/components/reader/lib/chapterLoader";
|
import { loadChapter, scheduleResumeDismiss } from "$lib/components/reader/lib/chapterLoader";
|
||||||
import { historyState } from "$lib/state/history.svelte";
|
import { historyState } from "$lib/state/history.svelte";
|
||||||
|
import { getAdapter } from "$lib/request-manager";
|
||||||
import type { ReaderSettings } from "$lib/state/reader.svelte";
|
import type { ReaderSettings } from "$lib/state/reader.svelte";
|
||||||
import ReaderControls from "$lib/components/reader/ReaderControls.svelte";
|
import ReaderControls from "$lib/components/reader/ReaderControls.svelte";
|
||||||
import PageView from "$lib/components/reader/PageView.svelte";
|
import PageView from "$lib/components/reader/PageView.svelte";
|
||||||
@@ -380,19 +381,7 @@
|
|||||||
const toQueue = list.slice(idx + 1, idx + 1 + prefs.downloadAhead)
|
const toQueue = list.slice(idx + 1, idx + 1 + prefs.downloadAhead)
|
||||||
.filter(c => !c.downloaded && !c.read)
|
.filter(c => !c.downloaded && !c.read)
|
||||||
.map(c => c.id);
|
.map(c => c.id);
|
||||||
if (toQueue.length) {
|
if (toQueue.length) getAdapter().enqueueDownloads(toQueue.map(String)).catch(console.error);
|
||||||
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<string, string> = { "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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
X, CaretLeft, CaretRight, CaretUp, CaretDown,
|
X, CaretLeft, CaretRight, CaretUp, CaretDown,
|
||||||
MagnifyingGlassMinus, MagnifyingGlassPlus,
|
MagnifyingGlassMinus, MagnifyingGlassPlus,
|
||||||
Bookmark, MapPin, Download, Check, GearSix, Sliders,
|
Bookmark, MapPin, Download, Check, GearSix, Sliders,
|
||||||
ArrowsOut, ArrowsIn,
|
ArrowsOut, ArrowsIn, Minus,
|
||||||
} from "phosphor-svelte";
|
} from "phosphor-svelte";
|
||||||
import { readerState, MARKER_COLORS, MARKER_COLOR_HEX, ZOOM_STEP, ZOOM_MIN, ZOOM_MAX } from "$lib/state/reader.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 { fly } from "svelte/transition";
|
||||||
import { cubicOut, cubicIn } from "svelte/easing";
|
import { cubicOut, cubicIn } from "svelte/easing";
|
||||||
import type { Chapter } from "$lib/types";
|
import type { Chapter } from "$lib/types";
|
||||||
@@ -52,24 +53,6 @@
|
|||||||
|
|
||||||
const queueable = $derived(adjacent.remaining.filter(c => !c.downloaded));
|
const queueable = $derived(adjacent.remaining.filter(c => !c.downloaded));
|
||||||
|
|
||||||
async function gqlMutation(query: string, variables: Record<string, unknown>): Promise<void> {
|
|
||||||
const base = settingsState.settings.serverUrl ?? "http://localhost:4567";
|
|
||||||
const headers: Record<string, string> = { "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<void>) {
|
async function runDl(fn: () => Promise<void>) {
|
||||||
readerState.dlBusy = true;
|
readerState.dlBusy = true;
|
||||||
try { await fn(); } catch (e) { console.error(e); }
|
try { await fn(); } catch (e) { console.error(e); }
|
||||||
@@ -77,6 +60,14 @@
|
|||||||
readerState.dlOpen = false;
|
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 isVertical = $derived(barPosition === "left" || barPosition === "right");
|
||||||
const popoverSide = $derived(
|
const popoverSide = $derived(
|
||||||
barPosition === "left" ? "right" :
|
barPosition === "left" ? "right" :
|
||||||
@@ -96,9 +87,10 @@
|
|||||||
onRestoreZoomAnchor();
|
onRestoreZoomAnchor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isTauri = platformService.platform === "tauri";
|
||||||
|
|
||||||
async function toggleFullscreen() {
|
async function toggleFullscreen() {
|
||||||
if (!document.fullscreenElement) await document.documentElement.requestFullscreen();
|
await platformService.toggleFullscreen();
|
||||||
else await document.exitFullscreen();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeAllPopovers() {
|
function closeAllPopovers() {
|
||||||
@@ -337,6 +329,16 @@
|
|||||||
<span>Fullscreen</span>
|
<span>Fullscreen</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
{#if isTauri}
|
||||||
|
<button class="action-row" onclick={() => { readerState.actionsOpen = false; platformService.minimize(); }}>
|
||||||
|
<Minus size={13} weight="regular" />
|
||||||
|
<span>Minimize</span>
|
||||||
|
</button>
|
||||||
|
<button class="action-row action-row-danger" onclick={() => { readerState.actionsOpen = false; platformService.close(); }}>
|
||||||
|
<X size={13} weight="regular" />
|
||||||
|
<span>Close window</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -345,13 +347,13 @@
|
|||||||
<div class="popover dl-popover popover-{popoverSide}" role="presentation" onclick={(e) => e.stopPropagation()}>
|
<div class="popover dl-popover popover-{popoverSide}" role="presentation" onclick={(e) => e.stopPropagation()}>
|
||||||
<p class="dl-title">Download</p>
|
<p class="dl-title">Download</p>
|
||||||
<button class="dl-option" disabled={readerState.dlBusy || !!chapter.downloaded}
|
<button class="dl-option" disabled={readerState.dlBusy || !!chapter.downloaded}
|
||||||
onclick={() => runDl(() => gqlMutation(ENQUEUE_ONE, { id: chapter.id }))}>
|
onclick={() => runDl(() => enqueueOne(chapter.id))}>
|
||||||
This chapter
|
This chapter
|
||||||
<span class="dl-sub">{chapter.downloaded ? "Already downloaded" : chapter.name}</span>
|
<span class="dl-sub">{chapter.downloaded ? "Already downloaded" : chapter.name}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="dl-row">
|
<div class="dl-row">
|
||||||
<button class="dl-option" disabled={readerState.dlBusy || queueable.length === 0}
|
<button class="dl-option" disabled={readerState.dlBusy || queueable.length === 0}
|
||||||
onclick={() => runDl(() => gqlMutation(ENQUEUE_MANY, { ids: queueable.slice(0, readerState.nextN).map(c => c.id) }))}>
|
onclick={() => runDl(() => enqueueMany(queueable.slice(0, readerState.nextN).map(c => c.id)))}>
|
||||||
Next chapters
|
Next chapters
|
||||||
<span class="dl-sub">{Math.min(readerState.nextN, queueable.length)} not yet downloaded</span>
|
<span class="dl-sub">{Math.min(readerState.nextN, queueable.length)} not yet downloaded</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -362,7 +364,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="dl-option" disabled={readerState.dlBusy || queueable.length === 0}
|
<button class="dl-option" disabled={readerState.dlBusy || queueable.length === 0}
|
||||||
onclick={() => runDl(() => gqlMutation(ENQUEUE_MANY, { ids: queueable.map(c => c.id) }))}>
|
onclick={() => runDl(() => enqueueMany(queueable.map(c => c.id)))}>
|
||||||
All remaining
|
All remaining
|
||||||
<span class="dl-sub">{queueable.length} not yet downloaded</span>
|
<span class="dl-sub">{queueable.length} not yet downloaded</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -661,8 +663,10 @@
|
|||||||
transition: background var(--t-fast), color var(--t-fast);
|
transition: background var(--t-fast), color var(--t-fast);
|
||||||
}
|
}
|
||||||
.action-row:hover { background: var(--bg-overlay); color: var(--text-primary); }
|
.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 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: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; }
|
.action-divider { height: 1px; background: var(--border-dim); margin: var(--sp-1) 0; }
|
||||||
|
|
||||||
|
|||||||
@@ -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 = `
|
export const SET_DOWNLOADS_PATH = `
|
||||||
mutation SetDownloadsPath($path: String!) {
|
mutation SetDownloadsPath($path: String!) {
|
||||||
setSettings(input: { settings: { downloadsPath: $path } }) {
|
setSettings(input: { settings: { downloadsPath: $path } }) {
|
||||||
|
|||||||
@@ -23,12 +23,14 @@ import {
|
|||||||
GET_LIBRARY,
|
GET_LIBRARY,
|
||||||
GET_MANGA,
|
GET_MANGA,
|
||||||
GET_CATEGORIES,
|
GET_CATEGORIES,
|
||||||
|
GET_DOWNLOADS_PATH,
|
||||||
FETCH_MANGA,
|
FETCH_MANGA,
|
||||||
UPDATE_MANGA,
|
UPDATE_MANGA,
|
||||||
UPDATE_MANGAS,
|
UPDATE_MANGAS,
|
||||||
UPDATE_MANGA_CATEGORIES,
|
UPDATE_MANGA_CATEGORIES,
|
||||||
UPDATE_MANGAS_CATEGORIES,
|
UPDATE_MANGAS_CATEGORIES,
|
||||||
CREATE_CATEGORY,
|
CREATE_CATEGORY,
|
||||||
|
UPDATE_CATEGORY,
|
||||||
DELETE_CATEGORY,
|
DELETE_CATEGORY,
|
||||||
UPDATE_CATEGORY_ORDER,
|
UPDATE_CATEGORY_ORDER,
|
||||||
UPDATE_CATEGORY_MANGA,
|
UPDATE_CATEGORY_MANGA,
|
||||||
@@ -37,6 +39,8 @@ import {
|
|||||||
UPDATE_STOP,
|
UPDATE_STOP,
|
||||||
SET_MANGA_META,
|
SET_MANGA_META,
|
||||||
DELETE_MANGA_META,
|
DELETE_MANGA_META,
|
||||||
|
CREATE_BACKUP,
|
||||||
|
RESTORE_BACKUP,
|
||||||
FETCH_SOURCE_MANGA,
|
FETCH_SOURCE_MANGA,
|
||||||
LIBRARY_UPDATE_STATUS,
|
LIBRARY_UPDATE_STATUS,
|
||||||
MANGAS_BY_GENRE,
|
MANGAS_BY_GENRE,
|
||||||
@@ -64,16 +68,26 @@ import {
|
|||||||
START_DOWNLOADER,
|
START_DOWNLOADER,
|
||||||
STOP_DOWNLOADER,
|
STOP_DOWNLOADER,
|
||||||
CLEAR_DOWNLOADER,
|
CLEAR_DOWNLOADER,
|
||||||
|
SET_DOWNLOADS_PATH,
|
||||||
|
SET_LOCAL_SOURCE_PATH,
|
||||||
} from './downloads'
|
} from './downloads'
|
||||||
import {
|
import {
|
||||||
GET_EXTENSIONS,
|
GET_EXTENSIONS,
|
||||||
GET_SOURCES,
|
GET_SOURCES,
|
||||||
|
GET_SOURCE_SETTINGS,
|
||||||
|
GET_SETTINGS,
|
||||||
GET_SERVER_SECURITY,
|
GET_SERVER_SECURITY,
|
||||||
FETCH_EXTENSIONS,
|
FETCH_EXTENSIONS,
|
||||||
UPDATE_EXTENSION,
|
UPDATE_EXTENSION,
|
||||||
UPDATE_EXTENSIONS,
|
UPDATE_EXTENSIONS,
|
||||||
INSTALL_EXTERNAL_EXTENSION,
|
INSTALL_EXTERNAL_EXTENSION,
|
||||||
|
UPDATE_SOURCE_PREFERENCE,
|
||||||
|
SET_SOURCE_META,
|
||||||
|
DELETE_SOURCE_META,
|
||||||
|
SET_EXTENSION_REPOS,
|
||||||
SET_SERVER_AUTH,
|
SET_SERVER_AUTH,
|
||||||
|
CLEAR_CACHED_IMAGES,
|
||||||
|
RESET_SETTINGS,
|
||||||
} from './extensions'
|
} from './extensions'
|
||||||
import {
|
import {
|
||||||
GET_TRACKERS,
|
GET_TRACKERS,
|
||||||
@@ -85,10 +99,17 @@ import {
|
|||||||
UNLINK_TRACK,
|
UNLINK_TRACK,
|
||||||
TRACK_PROGRESS,
|
TRACK_PROGRESS,
|
||||||
UPDATE_TRACK,
|
UPDATE_TRACK,
|
||||||
|
LOGIN_TRACKER_CREDENTIALS,
|
||||||
|
LOGOUT_TRACKER,
|
||||||
} from './tracking'
|
} from './tracking'
|
||||||
import {
|
import {
|
||||||
GET_ABOUT_SERVER,
|
GET_ABOUT_SERVER,
|
||||||
GET_ABOUT_WEBUI,
|
GET_ABOUT_WEBUI,
|
||||||
|
CHECK_FOR_SERVER_UPDATES,
|
||||||
|
GET_META,
|
||||||
|
GET_METAS,
|
||||||
|
SET_SOCKS_PROXY,
|
||||||
|
SET_FLARE_SOLVERR,
|
||||||
} from './meta'
|
} from './meta'
|
||||||
import {
|
import {
|
||||||
type GQLResponse,
|
type GQLResponse,
|
||||||
@@ -100,50 +121,6 @@ import {
|
|||||||
} from './types'
|
} from './types'
|
||||||
import { initPageCache, clearPageCache as _clearPageCache } from './pageCache'
|
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<string, unknown>
|
type RawQueueItem = Record<string, unknown>
|
||||||
|
|
||||||
function mapDownloadStatus(raw: { state: string; queue: RawQueueItem[] }): DownloadStatus {
|
function mapDownloadStatus(raw: { state: string; queue: RawQueueItem[] }): DownloadStatus {
|
||||||
|
|||||||
@@ -37,3 +37,47 @@ export const GET_METAS = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -149,7 +149,7 @@ class LibraryState {
|
|||||||
|
|
||||||
const f = this.tabFilters[tab] ?? {};
|
const f = this.tabFilters[tab] ?? {};
|
||||||
if (f.unread) items = items.filter(m => (m.unreadCount ?? 0) > 0);
|
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.downloaded) items = items.filter(m => (m.downloadCount ?? 0) > 0);
|
||||||
if (f.bookmarked) items = items.filter(m => (m.bookmarkCount ?? 0) > 0);
|
if (f.bookmarked) items = items.filter(m => (m.bookmarkCount ?? 0) > 0);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user