Feat: Automated Tracking + Proper Sync

This commit is contained in:
Youwes09
2026-04-26 12:11:45 -05:00
parent 361a145702
commit c0efbba4df
8 changed files with 358 additions and 64 deletions
+58
View File
@@ -1,4 +1,8 @@
import type { Tracker, TrackRecord } from "@types/index";
import type { Chapter } from "@types/index";
import { FETCH_TRACK } from "@api/mutations/tracking";
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[] };
@@ -109,3 +113,57 @@ export function removeRecord(
}
);
}
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 = opts.respectScanlatorFilter
? buildChapterList(chapters, opts.chapterPrefs)
: chapters;
const toMark: number[] = [];
for (const record of records) {
let fresh: TrackRecord;
try {
const res = await gqlFn(FETCH_TRACK, { recordId: record.id }) as { fetchTrack: { trackRecord: TrackRecord } };
fresh = res.fetchTrack.trackRecord;
} catch {
continue;
}
const remote = fresh.lastChapterRead;
if (!remote || remote <= 0) continue;
let ceiling: number;
if (opts.threshold === null) {
ceiling = remote;
} else {
const match = eligible
.filter(c => Math.abs(c.chapterNumber - remote) <= opts.threshold!)
.sort((a, b) => Math.abs(a.chapterNumber - remote) - Math.abs(b.chapterNumber - remote))[0];
if (!match) continue;
ceiling = match.chapterNumber;
}
const unread = eligible.filter(c => !c.isRead && c.chapterNumber <= ceiling);
toMark.push(...unread.map(c => c.id));
}
const ids = [...new Set(toMark)];
if (ids.length > 0) {
await gqlFn(MARK_CHAPTERS_READ, { ids, isRead: true });
}
return ids;
}
@@ -0,0 +1,92 @@
import { gql } from "@api/client";
import { GET_MANGA_TRACK_RECORDS } from "@api/queries/tracking";
import { UPDATE_TRACK, FETCH_TRACK } from "@api/mutations/tracking";
import { buildChapterList } from "@features/series/lib/chapterList";
import type { TrackRecord } from "@types/index";
import type { Chapter } from "@types/index";
class TrackingState {
records: TrackRecord[] = $state([]);
mangaId: number | null = $state(null);
loading: boolean = $state(false);
error: string | null = $state(null);
async loadForManga(id: number) {
if (this.mangaId === id && this.records.length > 0) return;
this.mangaId = id;
this.loading = true;
this.error = null;
try {
const res = await gql<{ manga: { trackRecords: { nodes: TrackRecord[] } } }>(
GET_MANGA_TRACK_RECORDS, { mangaId: id }
);
if (this.mangaId !== id) return;
this.records = res.manga.trackRecords.nodes;
} catch (e: any) {
this.error = e?.message ?? "Failed to load tracking";
} finally {
this.loading = false;
}
}
async refresh() {
if (this.mangaId === null) return;
const id = this.mangaId;
this.loading = true;
try {
const res = await gql<{ manga: { trackRecords: { nodes: TrackRecord[] } } }>(
GET_MANGA_TRACK_RECORDS, { mangaId: id }
);
if (this.mangaId !== id) return;
this.records = res.manga.trackRecords.nodes;
} catch (e: any) {
this.error = e?.message ?? "Failed to refresh tracking";
} finally {
this.loading = false;
}
}
patchRecord(updated: Partial<TrackRecord> & { id: number }) {
this.records = this.records.map(r => r.id === updated.id ? { ...r, ...updated } : r);
}
removeRecord(id: number) {
this.records = this.records.filter(r => r.id !== id);
}
clear() {
this.records = [];
this.mangaId = null;
this.error = null;
}
async syncRecordFromRemote(recordId: number) {
try {
const res = await gql<{ fetchTrack: { trackRecord: TrackRecord } }>(
FETCH_TRACK, { recordId }
);
this.patchRecord(res.fetchTrack.trackRecord);
return res.fetchTrack.trackRecord;
} catch {
return null;
}
}
async updateFromRead(chapter: Chapter, chapterList: Chapter[], prefs?: import("@features/series/lib/chapterList").ChapterDisplayPrefs) {
const filtered = prefs ? buildChapterList(chapterList, { ...prefs, sortDir: "asc" }) : [...chapterList].sort((a, b) => a.sourceOrder - b.sourceOrder);
const idx = filtered.findIndex(c => c.id === chapter.id);
if (idx < 0) return;
const position = idx + 1;
const eligible = this.records.filter(r => position > (r.lastChapterRead ?? 0));
for (const record of eligible) {
try {
const res = await gql<{ updateTrack: { trackRecord: TrackRecord } }>(
UPDATE_TRACK, { recordId: record.id, lastChapterRead: position }
);
this.patchRecord(res.updateTrack.trackRecord);
} catch {}
}
}
}
export const trackingState = new TrackingState();