mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 09:19:56 -05:00
Chore: Restructure Repository for SvelteKit
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { createSorter } from "@core/algorithms/sort";
|
||||
import type { Manga } from "@types";
|
||||
import type { LibrarySortMode, LibrarySortDir } from "@store/state.svelte";
|
||||
|
||||
export const librarySorter = createSorter<Manga>({
|
||||
defaultField: "az",
|
||||
defaultDir: "asc",
|
||||
fields: [
|
||||
{
|
||||
key: "az",
|
||||
comparator: (a, b) => a.title.localeCompare(b.title, undefined, { sensitivity: "base" }),
|
||||
},
|
||||
{
|
||||
key: "unreadCount",
|
||||
comparator: (a, b) => (a.unreadCount ?? 0) - (b.unreadCount ?? 0),
|
||||
},
|
||||
{
|
||||
key: "totalChapters",
|
||||
comparator: (a, b) => (a.chapters?.totalCount ?? 0) - (b.chapters?.totalCount ?? 0),
|
||||
},
|
||||
{
|
||||
key: "recentlyAdded",
|
||||
comparator: (a, b) => Number(a.inLibraryAt ?? 0) - Number(b.inLibraryAt ?? 0),
|
||||
},
|
||||
{
|
||||
key: "recentlyRead",
|
||||
comparator: (a, b, ctx) => {
|
||||
const map = ctx?.recentlyReadMap as Map<number, number> | undefined;
|
||||
const ra = map?.get(a.id) ?? 0;
|
||||
const rb = map?.get(b.id) ?? 0;
|
||||
return ra - rb;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "latestFetched",
|
||||
comparator: (a, b) => Number(a.latestFetchedChapter?.uploadDate ?? 0) - Number(b.latestFetchedChapter?.uploadDate ?? 0),
|
||||
},
|
||||
{
|
||||
key: "latestUploaded",
|
||||
comparator: (a, b) => Number(a.latestUploadedChapter?.uploadDate ?? 0) - Number(b.latestUploadedChapter?.uploadDate ?? 0),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
export function sortLibrary(
|
||||
items: Manga[],
|
||||
mode: LibrarySortMode,
|
||||
dir: LibrarySortDir,
|
||||
recentlyReadMap?: Map<number, number>,
|
||||
): Manga[] {
|
||||
return librarySorter.sort(items, mode, dir, recentlyReadMap ? { recentlyReadMap } : undefined);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { gql } from "@api/client";
|
||||
import { LIBRARY_UPDATE_STATUS } from "@api/queries/manga";
|
||||
import { UPDATE_LIBRARY, FETCH_MANGA } from "@api/mutations/manga";
|
||||
import { GET_LIBRARY } from "@api/queries/manga";
|
||||
import type { LibraryUpdateEntry } from "@store/state.svelte";
|
||||
|
||||
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: LibraryUpdateEntry[];
|
||||
totalUpdated: number;
|
||||
newChapters: number;
|
||||
}
|
||||
|
||||
export interface LibraryUpdaterCallbacks {
|
||||
onProgress: (p: UpdateProgress) => void;
|
||||
onDone: (r: UpdateResult) => void;
|
||||
onError: (e?: unknown) => void;
|
||||
}
|
||||
|
||||
export async function refreshLibraryMetadata(
|
||||
onProgress?: (done: number, total: number) => void,
|
||||
): Promise<void> {
|
||||
const data = await gql<{ mangas: { nodes: { id: number }[] } }>(GET_LIBRARY, {});
|
||||
const ids = data.mangas.nodes.map(m => m.id);
|
||||
let done = 0;
|
||||
for (const id of ids) {
|
||||
try {
|
||||
await gql(FETCH_MANGA, { id });
|
||||
} catch {}
|
||||
onProgress?.(++done, ids.length);
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
|
||||
function buildEntries(
|
||||
mangaUpdates: { status: string; manga: { id: number; title: string; thumbnailUrl: string; unreadCount: number } }[]
|
||||
): LibraryUpdateEntry[] {
|
||||
const byManga = new Map<number, LibraryUpdateEntry>();
|
||||
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()];
|
||||
}
|
||||
|
||||
async function run() {
|
||||
let jobsStarted = false;
|
||||
|
||||
try {
|
||||
const res = await gql<{
|
||||
updateLibrary: {
|
||||
updateStatus: {
|
||||
jobsInfo: {
|
||||
isRunning: boolean;
|
||||
totalJobs: number;
|
||||
finishedJobs: number;
|
||||
skippedMangasCount: number;
|
||||
skippedCategoriesCount: number;
|
||||
}
|
||||
}
|
||||
}
|
||||
}>(UPDATE_LIBRARY, {});
|
||||
if (cancelled) return;
|
||||
|
||||
const { jobsInfo } = res.updateLibrary.updateStatus;
|
||||
jobsStarted = jobsInfo.totalJobs > 0;
|
||||
|
||||
callbacks.onProgress({
|
||||
finished: jobsInfo.finishedJobs,
|
||||
total: jobsInfo.totalJobs,
|
||||
skippedManga: jobsInfo.skippedMangasCount,
|
||||
skippedCategories: jobsInfo.skippedCategoriesCount,
|
||||
});
|
||||
|
||||
if (!jobsStarted) {
|
||||
callbacks.onDone({ entries: [], totalUpdated: 0, newChapters: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
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() {
|
||||
gql<{
|
||||
libraryUpdateStatus: {
|
||||
jobsInfo: {
|
||||
isRunning: boolean;
|
||||
finishedJobs: number;
|
||||
totalJobs: number;
|
||||
skippedMangasCount: number;
|
||||
skippedCategoriesCount: number;
|
||||
};
|
||||
mangaUpdates: {
|
||||
status: string;
|
||||
manga: { id: number; title: string; thumbnailUrl: string; unreadCount: number };
|
||||
}[];
|
||||
}
|
||||
}>(LIBRARY_UPDATE_STATUS, {})
|
||||
.then(async d => {
|
||||
if (cancelled) return;
|
||||
const { jobsInfo, mangaUpdates } = d.libraryUpdateStatus;
|
||||
|
||||
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