Chore: Restructure Repository for SvelteKit

This commit is contained in:
Youwes09
2026-05-22 04:04:59 -05:00
parent bf071dcfc7
commit 8cef74bb98
266 changed files with 5093 additions and 396 deletions
+176
View File
@@ -0,0 +1,176 @@
import type { Tracker, TrackRecord } from "@types/index";
import type { Chapter } from "@types/index";
import { MARK_CHAPTERS_READ } from "@api/mutations/chapters";
import { buildChapterList, type ChapterDisplayPrefs } from "@features/series/lib/chapterList";
export interface TrackerWithRecords extends Tracker {
trackRecords: { nodes: TrackRecord[] };
}
export interface FlatRecord extends TrackRecord {
tracker: Tracker;
}
export type SortKey = "title" | "status" | "score" | "progress";
export function flattenRecords(trackers: TrackerWithRecords[]): FlatRecord[] {
return trackers
.filter((t) => t.isLoggedIn)
.flatMap((t) =>
t.trackRecords.nodes.map((r) => ({
...r,
trackerId: r.trackerId ?? t.id,
tracker: t as Tracker,
}))
);
}
export function dedupeStatuses(trackers: TrackerWithRecords[]): { value: number; name: string }[] {
const seen = new Map<string, { value: number; name: string }>();
for (const t of trackers.filter((t) => t.isLoggedIn))
for (const s of t.statuses ?? [])
seen.set(`${s.value}:${s.name}`, s);
return [...seen.values()];
}
export function filterRecords(
records: FlatRecord[],
trackerId: number | "all",
statusFilter: number | "all",
query: string,
): FlatRecord[] {
let list = trackerId === "all"
? records
: records.filter((r) => Number(r.trackerId) === Number(trackerId));
if (statusFilter !== "all")
list = list.filter((r) => Number(r.status) === Number(statusFilter));
if (query.trim()) {
const q = query.toLowerCase();
list = list.filter((r) =>
r.title.toLowerCase().includes(q) ||
r.manga?.title?.toLowerCase().includes(q)
);
}
return list;
}
export function sortRecords(records: FlatRecord[], sortBy: SortKey): FlatRecord[] {
return [...records].sort((a, b) => {
if (sortBy === "title") return a.title.localeCompare(b.title);
if (sortBy === "status") return a.status - b.status;
if (sortBy === "score") return parseFloat(b.displayScore ?? "0") - parseFloat(a.displayScore ?? "0");
if (sortBy === "progress") {
const ap = a.totalChapters > 0 ? a.lastChapterRead / a.totalChapters : 0;
const bp = b.totalChapters > 0 ? b.lastChapterRead / b.totalChapters : 0;
return bp - ap;
}
return 0;
});
}
export function scoreToStars(score: string | undefined, scores: string[] | undefined): number {
if (!score || !scores || scores.length === 0) return 0;
const idx = scores.indexOf(score);
if (idx < 0) return 0;
return Math.round((idx / (scores.length - 1)) * 5);
}
export function calcProgress(lastChapterRead: number, totalChapters: number): number | null {
if (totalChapters <= 0) return null;
return Math.min(100, (lastChapterRead / totalChapters) * 100);
}
export function patchTracker(
trackers: TrackerWithRecords[],
trackerId: number,
updated: Partial<TrackRecord> & { id: number },
): TrackerWithRecords[] {
return trackers.map((t) =>
t.id !== trackerId ? t : {
...t,
trackRecords: {
nodes: t.trackRecords.nodes.map((r) =>
r.id === updated.id ? { ...r, ...updated } : r
),
},
}
);
}
export function removeRecord(
trackers: TrackerWithRecords[],
trackerId: number,
recordId: number,
): TrackerWithRecords[] {
return trackers.map((t) =>
t.id !== trackerId ? t : {
...t,
trackRecords: { nodes: t.trackRecords.nodes.filter((r) => r.id !== recordId) },
}
);
}
export interface SyncBackOptions {
threshold: number | null;
respectScanlatorFilter: boolean;
chapterPrefs: ChapterDisplayPrefs;
}
export async function syncBackFromTracker(
records: TrackRecord[],
chapters: Chapter[],
opts: SyncBackOptions,
gqlFn: (query: string, vars: Record<string, unknown>) => Promise<unknown>,
): Promise<number[]> {
const eligible = buildChapterList(chapters, {
...opts.chapterPrefs,
sortDir: "asc",
...(opts.respectScanlatorFilter ? {} : {
scanlatorFilter: [],
scanlatorBlacklist: [],
scanlatorForce: false,
}),
});
const seenInt = new Map<number, Chapter>();
for (const ch of eligible) {
const key = Math.floor(ch.chapterNumber);
if (!Number.isInteger(ch.chapterNumber)) continue;
if (!seenInt.has(key)) seenInt.set(key, ch);
}
const dedupedEligible = [...seenInt.values()];
const decimalsByFloor = new Map<number, Chapter[]>();
for (const ch of eligible) {
if (Number.isInteger(ch.chapterNumber)) continue;
const key = Math.floor(ch.chapterNumber);
const arr = decimalsByFloor.get(key) ?? [];
arr.push(ch);
decimalsByFloor.set(key, arr);
}
const toMarkRead: number[] = [];
for (const record of records) {
const remote = record.lastChapterRead;
if (!remote || remote <= 0) continue;
for (const chapter of dedupedEligible) {
if (chapter.isRead) continue;
if (chapter.chapterNumber > remote) continue;
if (opts.threshold !== null && remote - chapter.chapterNumber > opts.threshold) continue;
toMarkRead.push(chapter.id);
for (const dec of decimalsByFloor.get(chapter.chapterNumber) ?? []) {
if (!dec.isRead) toMarkRead.push(dec.id);
}
}
}
const ids = [...new Set(toMarkRead)];
if (ids.length > 0) {
await gqlFn(MARK_CHAPTERS_READ, { ids, isRead: true });
}
return ids;
}