Chore: Port over Settings (Barely Works)

This commit is contained in:
Youwes09
2026-05-24 20:31:46 -05:00
parent ae5d9748c7
commit d9a9427e3b
87 changed files with 8821 additions and 615 deletions
@@ -0,0 +1,274 @@
<script lang="ts">
import { settingsState, updateSettings } from "$lib/state/settings.svelte";
import { toast } from "$lib/state/notifications.svelte";
import { getAdapter } from "$lib/request-manager";
import { syncBackFromTracker } from "$lib/state/tracking.svelte";
import type { Tracker, TrackRecord } from "$lib/types/index";
let trackers = $state<Tracker[]>([]);
let trackersLoading = $state(false);
let trackersError = $state<string | null>(null);
let oauthTrackerId = $state<number | null>(null);
let oauthCallbackInput = $state("");
let oauthSubmitting = $state(false);
let oauthError = $state<string | null>(null);
let credsTrackerId = $state<number | null>(null);
let credsUsername = $state("");
let credsPassword = $state("");
let credsSubmitting = $state(false);
let credsError = $state<string | null>(null);
let loggingOut = $state<number | null>(null);
let syncing = $state(false);
$effect(() => {
if (trackers.length === 0 && !trackersLoading) loadTrackers();
});
async function loadTrackers() {
trackersLoading = true; trackersError = null;
try {
trackers = await getAdapter().getTrackers();
} catch (e: any) {
trackersError = e?.message ?? "Failed to load trackers";
} finally { trackersLoading = false; }
}
async function startOAuth(tracker: Tracker) {
if (!tracker.authUrl) return;
oauthTrackerId = tracker.id; oauthCallbackInput = "";
window.open(tracker.authUrl, "_blank");
}
async function submitOAuth() {
if (!oauthTrackerId || !oauthCallbackInput.trim()) return;
oauthSubmitting = true;
try {
await getAdapter().loginTrackerOAuth(oauthTrackerId, oauthCallbackInput.trim());
await loadTrackers();
oauthTrackerId = null; oauthCallbackInput = "";
} catch (e: any) {
oauthError = e?.message ?? "Login failed";
} finally { oauthSubmitting = false; }
}
function cancelOAuth() { oauthTrackerId = null; oauthCallbackInput = ""; oauthError = null; }
function startCredentials(tracker: Tracker) { credsTrackerId = tracker.id; credsUsername = ""; credsPassword = ""; }
async function submitCredentials() {
if (!credsTrackerId || !credsUsername.trim() || !credsPassword.trim()) return;
credsSubmitting = true;
try {
await getAdapter().loginTrackerCredentials(credsTrackerId, credsUsername.trim(), credsPassword.trim());
await loadTrackers();
credsTrackerId = null; credsUsername = ""; credsPassword = "";
} catch (e: any) {
credsError = e?.message ?? "Login failed";
} finally { credsSubmitting = false; }
}
function cancelCredentials() { credsTrackerId = null; credsUsername = ""; credsPassword = ""; credsError = null; }
async function logoutTracker(trackerId: number) {
loggingOut = trackerId;
try {
await getAdapter().logoutTracker(trackerId);
await loadTrackers();
} catch (e: any) {
trackersError = e?.message ?? "Logout failed";
} finally { loggingOut = null; }
}
function focusEl(node: HTMLElement) { setTimeout(() => node.focus(), 0); }
async function runSyncAll() {
syncing = true;
try {
const adapter = getAdapter();
const allTrackers = await adapter.getTrackersWithRecords();
const loggedIn = allTrackers.filter((t: any) => t.isLoggedIn);
const settings = settingsState.settings;
let totalMarked = 0;
for (const tracker of loggedIn) {
for (const record of tracker.trackRecords.nodes as TrackRecord[]) {
if (!record.manga?.id) continue;
const mangaId = record.manga.id;
const chapters = await adapter.getChapters(mangaId);
const prefs = settings.mangaPrefs?.[mangaId] ?? {};
const marked = await syncBackFromTracker(
[record],
chapters,
{
threshold: settings.trackerSyncBackThreshold ?? null,
respectScanlatorFilter: settings.trackerRespectScanlatorFilter ?? true,
chapterPrefs: prefs,
},
adapter.markChaptersRead.bind(adapter),
);
totalMarked += marked.length;
}
}
toast({ kind: "success", message: "Sync complete", detail: `${totalMarked} chapter${totalMarked !== 1 ? "s" : ""} marked read` });
} catch (e: any) {
toast({ kind: "error", message: "Sync failed", detail: e?.message });
} finally { syncing = false; }
}
</script>
<div class="s-panel">
<div class="s-section">
<p class="s-section-title">Connected Trackers</p>
<div class="s-section-body">
{#if trackersError}
<div class="s-banner s-banner-error s-banner-dismissible" onclick={() => trackersError = null} role="button" tabindex="0" onkeydown={(e) => e.key === "Enter" && (trackersError = null)}>{trackersError}</div>
{/if}
{#if trackersLoading}
<p class="s-empty">Loading trackers…</p>
{:else}
{#each trackers as tracker}
<div class="s-tracker-row" class:expanded={oauthTrackerId === tracker.id || credsTrackerId === tracker.id}>
<div class="s-tracker-identity">
<img src={tracker.icon} alt={tracker.name} class="s-tracker-logo" />
<div class="s-row-info">
<span class="s-label">{tracker.name}</span>
<div class="s-tracker-status-row">
<span class="s-pill" class:on={tracker.isLoggedIn && !tracker.isTokenExpired}>
{tracker.isLoggedIn ? "Connected" : "Not connected"}
</span>
{#if tracker.isLoggedIn && tracker.isTokenExpired}
<span class="s-pill s-pill-warn">Token expired — reconnect</span>
{/if}
</div>
</div>
</div>
<div class="s-tracker-action">
{#if tracker.isLoggedIn && tracker.isTokenExpired}
<button class="s-btn s-btn-accent" onclick={() => tracker.authUrl ? startOAuth(tracker) : startCredentials(tracker)}>
Reconnect
</button>
<button class="s-btn s-btn-danger" onclick={() => logoutTracker(tracker.id)} disabled={loggingOut === tracker.id}>
{loggingOut === tracker.id ? "Disconnecting…" : "Disconnect"}
</button>
{:else if tracker.isLoggedIn}
<button class="s-btn s-btn-danger" onclick={() => logoutTracker(tracker.id)} disabled={loggingOut === tracker.id}>
{loggingOut === tracker.id ? "Disconnecting…" : "Disconnect"}
</button>
{:else if oauthTrackerId !== tracker.id && credsTrackerId !== tracker.id}
<button class="s-btn" onclick={() => tracker.authUrl ? startOAuth(tracker) : startCredentials(tracker)}>
{tracker.authUrl ? "Connect via browser →" : "Connect"}
</button>
{/if}
</div>
{#if oauthTrackerId === tracker.id}
<div class="s-tracker-expand">
{#if oauthError}
<div class="s-banner s-banner-error s-banner-dismissible" onclick={() => oauthError = null} role="button" tabindex="0" onkeydown={(e) => e.key === "Enter" && (oauthError = null)}>{oauthError}</div>
{/if}
<p class="s-oauth-hint">Browser opened {tracker.name} login — authorise then paste the callback URL below.</p>
<input class="s-input full" placeholder="https://suwayomi.org/tracker-oauth#access_token=…"
bind:value={oauthCallbackInput}
onkeydown={(e) => { if (e.key === "Enter") submitOAuth(); if (e.key === "Escape") cancelOAuth(); }}
use:focusEl />
<div class="s-oauth-btns">
<button class="s-btn s-btn-accent" onclick={submitOAuth} disabled={oauthSubmitting || !oauthCallbackInput.trim()}>
{oauthSubmitting ? "Connecting…" : "Connect"}
</button>
<button class="s-btn" onclick={cancelOAuth}>Cancel</button>
</div>
</div>
{/if}
{#if credsTrackerId === tracker.id}
<div class="s-tracker-expand">
{#if credsError}
<div class="s-banner s-banner-error s-banner-dismissible" onclick={() => credsError = null} role="button" tabindex="0" onkeydown={(e) => e.key === "Enter" && (credsError = null)}>{credsError}</div>
{/if}
<input class="s-input full" placeholder="Username / Email" bind:value={credsUsername}
onkeydown={(e) => e.key === "Escape" && cancelCredentials()} use:focusEl />
<input class="s-input full" type="password" placeholder="Password" bind:value={credsPassword}
onkeydown={(e) => { if (e.key === "Enter") submitCredentials(); if (e.key === "Escape") cancelCredentials(); }} />
<div class="s-oauth-btns">
<button class="s-btn s-btn-accent" onclick={submitCredentials} disabled={credsSubmitting || !credsUsername.trim() || !credsPassword.trim()}>
{credsSubmitting ? "Connecting…" : "Connect"}
</button>
<button class="s-btn" onclick={cancelCredentials}>Cancel</button>
</div>
</div>
{/if}
</div>
{/each}
{/if}
</div>
</div>
<div class="s-section">
<p class="s-section-title">Sync back from tracker</p>
<div class="s-row">
<div class="s-row-info">
<span class="s-label">Enable sync back</span>
<span class="s-desc">Mark chapters read locally based on tracker progress</span>
</div>
<button class="s-toggle" class:on={settings.trackerSyncBack}
onclick={() => updateSettings({ trackerSyncBack: !settings.trackerSyncBack })}
role="switch" aria-checked={settings.trackerSyncBack} aria-label="Enable sync back">
<span class="s-toggle-thumb"></span>
</button>
</div>
{#if settings.trackerSyncBack}
<label class="s-row">
<div class="s-row-info">
<span class="s-label">Chapter number tolerance</span>
<span class="s-desc">Allow source and tracker chapter numbers to differ by up to the set amount. When off, the tracker number is used as-is with no range check.</span>
</div>
<button role="switch" aria-checked={settings.trackerSyncBackThreshold !== null} aria-label="Chapter number tolerance" class="s-toggle" class:on={settings.trackerSyncBackThreshold !== null}
onclick={() => updateSettings({ trackerSyncBackThreshold: settings.trackerSyncBackThreshold !== null ? null : 20 })}>
<span class="s-toggle-thumb"></span>
</button>
</label>
{#if settings.trackerSyncBackThreshold !== null}
<div class="s-row">
<div class="s-row-info"><span class="s-label">Tolerance</span><span class="s-desc">Max chapter number difference allowed (120)</span></div>
<div class="s-stepper">
<button class="s-step-btn" onclick={() => updateSettings({ trackerSyncBackThreshold: Math.max(1, (settings.trackerSyncBackThreshold ?? 20) - 1) })}></button>
<span class="s-step-val">{settings.trackerSyncBackThreshold}</span>
<button class="s-step-btn" onclick={() => updateSettings({ trackerSyncBackThreshold: Math.min(20, (settings.trackerSyncBackThreshold ?? 20) + 1) })}>+</button>
</div>
</div>
{/if}
<div class="s-row">
<div class="s-row-info">
<span class="s-label">Respect scanlator filter</span>
<span class="s-desc">Only mark chapters matching the series' active scanlator filter</span>
</div>
<button class="s-toggle" class:on={settings.trackerRespectScanlatorFilter}
onclick={() => updateSettings({ trackerRespectScanlatorFilter: !settings.trackerRespectScanlatorFilter })}
role="switch" aria-checked={settings.trackerRespectScanlatorFilter} aria-label="Respect scanlator filter">
<span class="s-toggle-thumb"></span>
</button>
</div>
<div class="s-row">
<div class="s-row-info">
<span class="s-label">Sync now</span>
<span class="s-desc">Apply tracker progress to all linked manga in your library</span>
</div>
<button class="s-btn" onclick={runSyncAll} disabled={syncing}>
{syncing ? "Syncing…" : "Sync all"}
</button>
</div>
{/if}
</div>
</div>
<style>
.s-tracker-status-row { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; }
.s-pill-warn { background: color-mix(in srgb, var(--color-warn, #c97c2b) 15%, transparent); color: var(--color-warn, #c97c2b); border-color: color-mix(in srgb, var(--color-warn, #c97c2b) 35%, transparent); }
.s-banner-dismissible { cursor: pointer; max-height: 8rem; overflow-y: auto; }
.s-banner-dismissible:hover { opacity: 0.85; }
</style>