Initial commit

This commit is contained in:
ponzischeme89
2026-01-17 21:49:22 +13:00
commit 3ad3d9bfe0
118 changed files with 18586 additions and 0 deletions
@@ -0,0 +1,242 @@
<script>
import { AlertTriangle, ArrowRight } from "lucide-svelte";
export let settings = {};
export let saving = false;
export let onSave;
let stripKeywords = settings.strip_keywords !== false;
let cleanSubtitleContent = settings.clean_subtitle_content !== false;
// Example filenames to demonstrate the cleaning
const filenameExamples = [
{ before: "Movie.2024.1080p.BluRay.x264-YTS", after: "Movie (2024)" },
{
before: "The.Matrix.1999.REMASTERED.2160p.UHD",
after: "The Matrix (1999)",
},
{ before: "Inception.2010.HDRip.XviD-RARBG", after: "Inception (2010)" },
];
// Example subtitle content cleaning
const contentExamples = [
{ before: "Hello there. www.YTS.mx", after: "Hello there." },
{ before: "Subtitles by OpenSubtitles", after: "(removed)" },
{ before: "Downloaded from RARBG", after: "(removed)" },
];
// Keywords that get stripped from filenames
const filenameKeywords = [
{ name: "Quality", examples: ["480p", "720p", "1080p", "4K", "HDR"] },
{ name: "Source", examples: ["BluRay", "WEBRip", "DVDRip", "HDTV"] },
{ name: "Codecs", examples: ["x264", "x265", "HEVC", "AAC", "DTS"] },
{ name: "Groups", examples: ["YTS", "RARBG", "EZTV", "PSA"] },
];
// Keywords removed from subtitle content
const contentKeywords = [
{
name: "Sites",
examples: ["YTS.mx", "RARBG", "OpenSubtitles", "Subscene"],
},
{
name: "Watermarks",
examples: ["Subtitles by", "Synced by", "Downloaded from"],
},
{
name: "Promo",
examples: ["Support us", "Get more subtitles", "Visit us at"],
},
];
function handleSubmit() {
onSave({
strip_keywords: stripKeywords,
clean_subtitle_content: cleanSubtitleContent,
});
}
</script>
<form on:submit|preventDefault={handleSubmit} class="space-y-10">
<!-- Section 1: Filename Cleaning -->
<div>
<h2 class="text-lg font-semibold text-text-primary">Filename Cleaning</h2>
<p class="text-[13px] text-text-secondary mb-6">
Clean up movie filenames before searching for metadata.
</p>
<!-- Toggle -->
<div class="p-5 bg-bg-secondary border border-border rounded-xl mb-5">
<label class="flex items-start justify-between gap-4 rounded-xl border border-border bg-bg-secondary/40 px-4 py-3">
<div class="flex-1">
<div class="text-[14px] font-medium mb-1">
Strip Filename Keywords
</div>
<div class="text-[12px] text-text-tertiary leading-relaxed">
Remove quality indicators (1080p, BluRay), codecs (x264, HEVC), and
release group names from filenames before looking up movie
information.
</div>
</div>
<span class="relative mt-0.5 inline-flex items-center">
<input type="checkbox" bind:checked={stripKeywords} class="sr-only peer" />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</span>
</label>
</div>
<!-- Collapsible Details -->
{#if stripKeywords}
<div class="space-y-4 pl-2 border-l-2 border-border ml-2">
<!-- Example Transformations -->
<div class="bg-bg-card border border-border rounded-xl overflow-hidden">
<div class="px-4 py-2.5 border-b border-border bg-bg-secondary">
<span
class="text-[11px] font-medium text-text-secondary uppercase tracking-wide"
>Examples</span
>
</div>
<div class="divide-y divide-border">
{#each filenameExamples as example}
<div class="px-4 py-2.5 flex items-center gap-3">
<div class="flex-1 min-w-0">
<code class="text-[11px] text-text-tertiary break-all"
>{example.before}</code
>
</div>
<ArrowRight class="w-3.5 h-3.5 text-text-tertiary flex-shrink-0" />
<div class="flex-shrink-0">
<code class="text-[11px] text-green-400 font-medium"
>{example.after}</code
>
</div>
</div>
{/each}
</div>
</div>
<!-- Keywords List -->
<div class="bg-bg-card border border-border rounded-xl p-4">
<div class="flex flex-wrap gap-x-6 gap-y-2">
{#each filenameKeywords as category}
<div>
<span
class="text-[10px] font-medium text-text-secondary uppercase"
>{category.name}:</span
>
<span class="text-[11px] text-text-tertiary ml-1.5"
>{category.examples.join(", ")}</span
>
</div>
{/each}
</div>
</div>
</div>
{/if}
</div>
<!-- Section 2: Subtitle Content Cleaning -->
<div>
<h2 class="text-lg font-semibold text-text-primary">Subtitle Content Cleaning</h2>
<p class="text-[13px] text-text-secondary mb-6">
Remove embedded ads and watermarks from inside subtitle text.
</p>
<!-- Toggle -->
<div class="p-5 bg-bg-secondary border border-border rounded-xl mb-5">
<label class="flex items-start justify-between gap-4 rounded-xl border border-border bg-bg-secondary/40 px-4 py-3">
<div class="flex-1">
<div class="text-[14px] font-medium mb-1">Remove Subtitle Ads</div>
<div class="text-[12px] text-text-tertiary leading-relaxed">
Automatically remove release group watermarks (YTS, RARBG), subtitle
site ads (OpenSubtitles), and promotional text embedded in the
actual subtitle dialogue.
</div>
</div>
<span class="relative mt-0.5 inline-flex items-center">
<input type="checkbox" bind:checked={cleanSubtitleContent} class="sr-only peer" />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</span>
</label>
</div>
<!-- Collapsible Details -->
{#if cleanSubtitleContent}
<div class="space-y-4 pl-2 border-l-2 border-border ml-2">
<!-- Example Transformations -->
<div class="bg-bg-card border border-border rounded-xl overflow-hidden">
<div class="px-4 py-2.5 border-b border-border bg-bg-secondary">
<span
class="text-[11px] font-medium text-text-secondary uppercase tracking-wide"
>Examples</span
>
</div>
<div class="divide-y divide-border">
{#each contentExamples as example}
<div class="px-4 py-2.5 flex items-center gap-3">
<div class="flex-1 min-w-0">
<code class="text-[11px] text-text-tertiary break-all"
>{example.before}</code
>
</div>
<ArrowRight class="w-3.5 h-3.5 text-text-tertiary flex-shrink-0" />
<div class="flex-shrink-0">
<code
class="text-[11px] {example.after === '(removed)'
? 'text-red-400'
: 'text-green-400'} font-medium">{example.after}</code
>
</div>
</div>
{/each}
</div>
</div>
<!-- Keywords List -->
<div class="bg-bg-card border border-border rounded-xl p-4">
<div class="flex flex-wrap gap-x-6 gap-y-2">
{#each contentKeywords as category}
<div>
<span
class="text-[10px] font-medium text-text-secondary uppercase"
>{category.name}:</span
>
<span class="text-[11px] text-text-tertiary ml-1.5"
>{category.examples.join(", ")}</span
>
</div>
{/each}
</div>
</div>
<!-- Note about timing -->
<div
class="flex items-start gap-3 p-4 bg-amber-500/5 border border-amber-500/20 rounded-xl"
>
<AlertTriangle class="w-4 h-4 text-amber-400 flex-shrink-0 mt-0.5" />
<div>
<div class="text-[12px] font-medium text-amber-300 mb-1">
Modifies subtitle content
</div>
<div class="text-[11px] text-amber-300/70 leading-relaxed">
This setting will modify the actual text inside your subtitle file
to remove ads. Subtitle timing is never changed - only ad text is
removed or entire ad blocks are deleted.
</div>
</div>
</div>
</div>
{/if}
</div>
<button
type="submit"
disabled={saving}
class="px-7 py-3.5 bg-text-primary hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed
text-bg-primary text-[13px] font-medium rounded-xl transition-all"
>
{saving ? "Saving..." : "Save Changes"}
</button>
</form>
@@ -0,0 +1,363 @@
<script>
export let settings = {};
export let saving = false;
export let onSave;
let defaultDirectory = settings.default_directory || '';
let duration = settings.duration ?? 40;
let insertionPosition = settings.insertion_position || 'start';
let quoteStyle = settings.quote_style || 'sarcastic';
// Subtitle formatting options
let titleBold = settings.subtitle_title_bold !== false;
let plotItalic = settings.subtitle_plot_italic !== false;
let showDirector = settings.subtitle_show_director === true;
let showActors = settings.subtitle_show_actors === true;
let showReleased = settings.subtitle_show_released === true;
let showGenre = settings.subtitle_show_genre === true;
let showPreview = false;
function handleSubmit() {
onSave({
default_directory: defaultDirectory,
duration: parseInt(duration),
insertion_position: insertionPosition,
quote_style: quoteStyle,
subtitle_title_bold: titleBold,
subtitle_plot_italic: plotItalic,
subtitle_show_director: showDirector,
subtitle_show_actors: showActors,
subtitle_show_released: showReleased,
subtitle_show_genre: showGenre
});
}
</script>
<form on:submit|preventDefault={handleSubmit} class="space-y-8">
<div>
<div class="rounded-xl border border-red-500/30 bg-red-500/5 p-6 space-y-4 mb-6">
<div>
<h3 class="text-sm font-semibold text-red-200">Support Sublogue ❤️</h3>
<p class="text-[12px] text-red-200/70">
Matt is a single developer of Sublogue and Shelfarr — your support and stars on GitHub help me keep going.
</p>
</div>
<div class="flex flex-wrap gap-3">
<button type="button" class="flex-1 min-w-[220px] px-4 py-3 rounded-xl text-[12px] font-medium border border-red-500/30 bg-bg-card/70 text-text-secondary hover:text-red-100 hover:bg-red-500/10 hover:border-red-500/60 transition-all">
<div class="text-[13px] font-semibold text-text-primary">Buy me coffee</div>
<div class="text-[11px] text-text-tertiary">Small tip · $4.99</div>
</button>
<button type="button" class="flex-1 min-w-[220px] px-4 py-3 rounded-xl text-[12px] font-medium border border-red-500/30 bg-bg-card/70 text-text-secondary hover:text-red-100 hover:bg-red-500/10 hover:border-red-500/60 transition-all">
<div class="text-[13px] font-semibold text-text-primary">Buy me lunch</div>
<div class="text-[11px] text-text-tertiary">Medium tip · $9.99</div>
</button>
<button type="button" class="flex-1 min-w-[220px] px-4 py-3 rounded-xl text-[12px] font-medium border border-red-500/30 bg-bg-card/70 text-text-secondary hover:text-red-100 hover:bg-red-500/10 hover:border-red-500/60 transition-all">
<div class="text-[13px] font-semibold text-text-primary">Buy me dinner</div>
<div class="text-[11px] text-text-tertiary">Large tip · $19.99</div>
</button>
</div>
<div class="flex items-center justify-between">
<p class="text-[11px] text-text-tertiary">
Every bit keeps Sublogue shipping.
</p>
<a
href="https://www.buymeacoffee.com/sublogue"
target="_blank"
rel="noopener noreferrer"
class="px-4 py-2 rounded-lg border border-red-500/30 text-[12px] text-text-secondary hover:text-white hover:bg-bg-hover transition-all"
>
Donate
</a>
</div>
</div>
<h2 class="text-lg font-semibold text-text-primary">General Settings</h2>
<p class="text-[13px] text-text-secondary mb-6">
Control how scans run and where summaries are placed.
</p>
<div class="space-y-6">
<div class="space-y-3">
<label for="directory" class="block text-[13px] font-medium text-text-primary">
Default Directory
</label>
<input
id="directory"
type="text"
bind:value={defaultDirectory}
placeholder="C:\Movies"
class="w-full px-4 py-3.5 bg-bg-card border border-border rounded-xl
text-[13px] font-mono placeholder:text-text-tertiary
focus:outline-none focus:border-white/25 focus:ring-2 focus:ring-ring transition-all"
/>
<p class="text-[12px] text-text-secondary">
Default directory to scan for subtitle files
</p>
</div>
<div class="space-y-3">
<label class="block text-[13px] font-medium text-text-primary">
Subtitle Item Duration
</label>
<div class="flex flex-wrap gap-2">
{#each [
{ value: 0, label: 'Auto', desc: 'Smart' },
{ value: 15, label: '15s', desc: 'Quick' },
{ value: 30, label: '30s', desc: 'Short' },
{ value: 45, label: '45s', desc: 'Standard' },
{ value: 60, label: '60s', desc: 'Extended' },
{ value: 90, label: '90s', desc: 'Long' }
] as preset}
<button
type="button"
on:click={() => duration = preset.value}
class="px-3 py-1.5 rounded-full text-[12px] font-medium transition-all border
{duration === preset.value
? 'bg-text-primary text-bg-primary border-white'
: 'bg-bg-card text-text-secondary hover:bg-bg-hover hover:text-text-primary border-border'}"
>
{preset.label}
</button>
{/each}
</div>
<p class="text-[12px] text-text-secondary">
Sets the target duration per generated subtitle item. Auto lets Sublogue choose based on reading speed.
</p>
</div>
<div class="space-y-3">
<label class="block text-[13px] font-medium text-text-primary">
Insertion Position
</label>
<div class="grid grid-cols-3 gap-2">
<button
type="button"
on:click={() => insertionPosition = 'start'}
class="flex flex-col items-center gap-1 px-3 py-2.5 rounded-xl text-center transition-all border
{insertionPosition === 'start'
? 'bg-text-primary text-bg-primary border-white'
: 'bg-bg-card text-text-secondary hover:bg-bg-hover border-border'}"
>
<span class="text-[12px] font-medium">Start</span>
<span class="text-[10px] {insertionPosition === 'start' ? 'text-bg-primary/70' : 'text-text-tertiary'}">Before all subs</span>
</button>
<button
type="button"
on:click={() => insertionPosition = 'end'}
class="flex flex-col items-center gap-1 px-3 py-2.5 rounded-xl text-center transition-all border
{insertionPosition === 'end'
? 'bg-text-primary text-bg-primary border-white'
: 'bg-bg-card text-text-secondary hover:bg-bg-hover border-border'}"
>
<span class="text-[12px] font-medium">End</span>
<span class="text-[10px] {insertionPosition === 'end' ? 'text-bg-primary/70' : 'text-text-tertiary'}">After credits</span>
</button>
<button
type="button"
on:click={() => insertionPosition = 'index'}
class="flex flex-col items-center gap-1 px-3 py-2.5 rounded-xl text-center transition-all border
{insertionPosition === 'index'
? 'bg-text-primary text-bg-primary border-white'
: 'bg-bg-card text-text-secondary hover:bg-bg-hover border-border'}"
>
<span class="text-[12px] font-medium">Index 1</span>
<span class="text-[10px] {insertionPosition === 'index' ? 'text-bg-primary/70' : 'text-text-tertiary'}">First position</span>
</button>
</div>
<p class="text-[12px] text-text-secondary">
Where to insert the plot summary in the subtitle file
</p>
</div>
<div class="space-y-3">
<label class="block text-[13px] font-medium text-text-primary">
Waiting Quote Style
</label>
<div class="flex gap-2">
<button
type="button"
on:click={() => quoteStyle = 'sarcastic'}
class="flex-1 px-4 py-3 rounded-lg text-[13px] font-medium transition-all border
{quoteStyle === 'sarcastic'
? 'bg-text-primary text-bg-primary border-white shadow-[0_1px_3px_rgba(0,0,0,0.3)]'
: 'bg-bg-card text-text-secondary hover:bg-bg-hover hover:text-text-primary border-border'}"
>
Sarcastic
</button>
<button
type="button"
on:click={() => quoteStyle = 'rude'}
class="flex-1 px-4 py-3 rounded-lg text-[13px] font-medium transition-all border
{quoteStyle === 'rude'
? 'bg-text-primary text-bg-primary border-white shadow-[0_1px_3px_rgba(0,0,0,0.3)]'
: 'bg-bg-card text-text-secondary hover:bg-bg-hover hover:text-text-primary border-border'}"
>
Rude
</button>
<button
type="button"
on:click={() => quoteStyle = 'nice'}
class="flex-1 px-4 py-3 rounded-lg text-[13px] font-medium transition-all border
{quoteStyle === 'nice'
? 'bg-text-primary text-bg-primary border-white shadow-[0_1px_3px_rgba(0,0,0,0.3)]'
: 'bg-bg-card text-text-secondary hover:bg-bg-hover hover:text-text-primary border-border'}"
>
Nice
</button>
</div>
<p class="text-[12px] text-text-secondary">
Tone of random quotes shown while waiting for scans
</p>
</div>
</div>
</div>
<!-- Subtitle Formatting Section -->
<div>
<h2 class="text-lg font-semibold text-text-primary">Subtitle Formatting</h2>
<p class="text-[13px] text-text-secondary mb-6">
Tune how metadata appears inside subtitle headers.
</p>
<div class="space-y-6">
<!-- Text Styling -->
<div class="space-y-3">
<label class="block text-[13px] font-medium mb-3 text-text-primary">Text Styling</label>
<label class="flex items-center justify-between gap-4 rounded-xl border border-border bg-bg-secondary/40 px-4 py-3">
<div>
<div class="text-[13px] font-medium">Bold Title</div>
<div class="text-[11px] text-text-tertiary mt-0.5">
Display movie title in <strong>bold</strong> text
</div>
</div>
<span class="relative inline-flex items-center">
<input type="checkbox" bind:checked={titleBold} class="sr-only peer" />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</span>
</label>
<label class="flex items-center justify-between gap-4 rounded-xl border border-border bg-bg-secondary/40 px-4 py-3">
<div>
<div class="text-[13px] font-medium">Italic Plot</div>
<div class="text-[11px] text-text-tertiary mt-0.5">
Display plot summary in <em>italic</em> text
</div>
</div>
<span class="relative inline-flex items-center">
<input type="checkbox" bind:checked={plotItalic} class="sr-only peer" />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</span>
</label>
</div>
<!-- Additional Info -->
<div class="space-y-3">
<label class="block text-[13px] font-medium mb-3 text-text-primary">Additional Information</label>
<p class="text-[12px] text-text-secondary -mt-2 mb-3">
Show extra metadata in the subtitle header
</p>
<label class="flex items-center justify-between gap-4 rounded-xl border border-border bg-bg-secondary/40 px-4 py-3">
<div class="text-[13px] font-medium">Director</div>
<span class="relative inline-flex items-center">
<input type="checkbox" bind:checked={showDirector} class="sr-only peer" />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</span>
</label>
<label class="flex items-center justify-between gap-4 rounded-xl border border-border bg-bg-secondary/40 px-4 py-3">
<div class="text-[13px] font-medium">Cast</div>
<span class="relative inline-flex items-center">
<input type="checkbox" bind:checked={showActors} class="sr-only peer" />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</span>
</label>
<label class="flex items-center justify-between gap-4 rounded-xl border border-border bg-bg-secondary/40 px-4 py-3">
<div class="text-[13px] font-medium">Release Date</div>
<span class="relative inline-flex items-center">
<input type="checkbox" bind:checked={showReleased} class="sr-only peer" />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</span>
</label>
<label class="flex items-center justify-between gap-4 rounded-xl border border-border bg-bg-secondary/40 px-4 py-3">
<div class="text-[13px] font-medium">Genre</div>
<span class="relative inline-flex items-center">
<input type="checkbox" bind:checked={showGenre} class="sr-only peer" />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</span>
</label>
</div>
</div>
</div>
<div class="rounded-xl border border-border bg-bg-secondary/30 p-6 space-y-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<h3 class="text-sm font-semibold text-text-primary">Preview formatting</h3>
<p class="text-[12px] text-text-tertiary">
Preview how the header and plot will look with your current choices.
</p>
</div>
<button
type="button"
on:click={() => (showPreview = !showPreview)}
class="px-4 py-2 rounded-lg border border-border text-[12px] text-text-secondary hover:text-white hover:bg-bg-hover transition-all"
>
{showPreview ? 'Hide preview' : 'Show preview'}
</button>
</div>
{#if showPreview}
<div class="rounded-xl border border-border bg-bg-card p-4 text-[12px] text-text-secondary space-y-3">
<div class="space-y-1">
<div class="text-text-primary">
{#if titleBold}
<strong>Sample Movie (2024)</strong>
{:else}
Sample Movie (2024)
{/if}
</div>
<div class="text-text-tertiary">IMDb: 7.8 • RT: 91% • 118 min</div>
{#if showDirector}
<div>Director: Jane Doe</div>
{/if}
{#if showActors}
<div>Cast: Alex Actor, Casey Star, Morgan Lee</div>
{/if}
{#if showReleased}
<div>Release Date: May 12, 2024</div>
{/if}
{#if showGenre}
<div>Genre: Drama, Sci-Fi</div>
{/if}
</div>
<div class="border-t border-border pt-3 text-text-secondary">
{#if plotItalic}
<em>Plot: A calm AI awakens in a distant archive and learns to rewrite forgotten stories.</em>
{:else}
Plot: A calm AI awakens in a distant archive and learns to rewrite forgotten stories.
{/if}
</div>
</div>
{/if}
</div>
<button
type="submit"
disabled={saving}
class="px-7 py-3.5 bg-text-primary hover:opacity-90 disabled:opacity-30 disabled:cursor-not-allowed
text-bg-primary text-[13px] font-medium rounded-xl transition-all"
>
{saving ? 'Saving...' : 'Save Changes'}
</button>
</form>
@@ -0,0 +1,572 @@
<script>
import { onMount } from "svelte";
import { getIntegrationUsage } from "../../lib/api.js";
import { Info } from "lucide-svelte";
export let settings = {};
export let saving = false;
export let onSave;
let omdbApiKey = settings.omdb_api_key || settings.api_key || "";
let tmdbApiKey = settings.tmdb_api_key || "";
let omdbEnabled = settings.omdb_enabled ?? false;
let tmdbEnabled = settings.tmdb_enabled ?? false;
let tvmazeEnabled = settings.tvmaze_enabled ?? false;
let usage = null;
let loadingUsage = false;
let showOmdbHelp = false;
let showTmdbHelp = false;
let showTvmazeHelp = false;
/* ===============================
Lifecycle
=============================== */
onMount(async () => {
await loadUsage();
});
function toggleOmdbHelp() {
showOmdbHelp = !showOmdbHelp;
if (showOmdbHelp) {
showTmdbHelp = false;
}
}
function toggleTmdbHelp() {
showTmdbHelp = !showTmdbHelp;
if (showTmdbHelp) {
showOmdbHelp = false;
}
}
function toggleTvmazeHelp() {
showTvmazeHelp = !showTvmazeHelp;
if (showTvmazeHelp) {
showOmdbHelp = false;
showTmdbHelp = false;
}
}
function clickOutside(node, handler) {
if (typeof handler !== "function") return { destroy() {} };
const onPointerDown = (event) => {
if (!node.contains(event.target)) handler(event);
};
document.addEventListener("mousedown", onPointerDown, true);
document.addEventListener("touchstart", onPointerDown, true);
return {
destroy() {
document.removeEventListener("mousedown", onPointerDown, true);
document.removeEventListener("touchstart", onPointerDown, true);
},
};
}
async function loadUsage() {
loadingUsage = true;
try {
const response = await getIntegrationUsage();
usage = response.usage;
} catch (err) {
console.error("Failed to load usage stats:", err);
} finally {
loadingUsage = false;
}
}
function handleSubmit() {
onSave({
omdb_api_key: omdbApiKey,
tmdb_api_key: tmdbApiKey,
omdb_enabled: omdbEnabled,
tmdb_enabled: tmdbEnabled,
tvmaze_enabled: tvmazeEnabled,
});
}
/* ===============================
Usage helpers
=============================== */
function usagePercent(current, limit) {
if (!limit || limit <= 0) return 0;
return Math.min(100, Math.round((current / limit) * 100));
}
function usageState(percent) {
if (percent < 80) return "ok";
if (percent < 95) return "warn";
return "critical";
}
function usageBarColor(state) {
if (state === "ok") return "bg-green-500";
if (state === "warn") return "bg-yellow-500";
return "bg-red-500";
}
function usageLabel(state) {
if (state === "ok") return "OK";
if (state === "warn") return "Warning";
return "Critical";
}
function formatResetTime(isoString) {
const date = new Date(isoString);
const now = new Date();
const diff = date - now;
if (diff <= 0) return "Now";
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
/* ===============================
Reactive usage values
=============================== */
$: omdbUsage = usage?.omdb;
$: tmdbUsage = usage?.tmdb;
$: tvmazeUsage = usage?.tvmaze;
$: omdbPercent = omdbUsage
? usagePercent(omdbUsage.total_calls_24h, omdbUsage.limit)
: 0;
$: tmdbPercent = tmdbUsage
? usagePercent(tmdbUsage.total_calls_24h, tmdbUsage.limit)
: 0;
$: tvmazePercent = tvmazeUsage
? usagePercent(tvmazeUsage.total_calls_24h, tvmazeUsage.limit)
: 0;
$: omdbState = usageState(omdbPercent);
$: tmdbState = usageState(tmdbPercent);
$: tvmazeState = usageState(tvmazePercent);
$: enabledCount =
(omdbEnabled ? 1 : 0) + (tmdbEnabled ? 1 : 0) + (tvmazeEnabled ? 1 : 0);
</script>
<form on:submit|preventDefault={handleSubmit} class="space-y-8">
<div>
<h2 class="text-lg font-semibold text-text-primary">Integrations</h2>
<p class="text-[13px] text-text-secondary mb-8">
Connect services to fetch movie and TV metadata
</p>
{#if enabledCount === 0}
<div class="mb-8 rounded-xl border border-border bg-bg-card p-6">
<h3 class="text-[13px] font-medium text-text-primary mb-2">
No integrations enabled
</h3>
<p class="text-[11px] text-text-tertiary mb-4">
Add a provider to start fetching metadata.
</p>
<div class="flex flex-col sm:flex-row gap-3">
<button
type="button"
class="px-4 py-2.5 rounded-lg border border-border bg-white/5 text-[12px] text-text-secondary hover:text-white hover:bg-bg-hover transition-colors"
on:click={() => (omdbEnabled = true)}
>
Add OMDb
</button>
<button
type="button"
class="px-4 py-2.5 rounded-lg border border-border bg-white/5 text-[12px] text-text-secondary hover:text-white hover:bg-bg-hover transition-colors"
on:click={() => (tmdbEnabled = true)}
>
Add TMDb
</button>
<button
type="button"
class="px-4 py-2.5 rounded-lg border border-border bg-white/5 text-[12px] text-text-secondary hover:text-white hover:bg-bg-hover transition-colors"
on:click={() => (tvmazeEnabled = true)}
>
Add TVmaze
</button>
</div>
</div>
{/if}
<div class="space-y-8">
{#if enabledCount > 0}
<div class="rounded-xl border border-border bg-bg-card p-4 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div>
<p class="text-[12px] font-medium text-text-primary">Add another integration</p>
<p class="text-[11px] text-text-tertiary">Choose a provider to enable.</p>
</div>
<div class="flex flex-wrap gap-2">
{#if !omdbEnabled}
<button
type="button"
class="px-3 py-2 rounded-lg border border-border bg-white/5 text-[12px] text-text-secondary hover:text-white hover:bg-bg-hover transition-colors"
on:click={() => (omdbEnabled = true)}
>
Add OMDb
</button>
{/if}
{#if !tmdbEnabled}
<button
type="button"
class="px-3 py-2 rounded-lg border border-border bg-white/5 text-[12px] text-text-secondary hover:text-white hover:bg-bg-hover transition-colors"
on:click={() => (tmdbEnabled = true)}
>
Add TMDb
</button>
{/if}
{#if !tvmazeEnabled}
<button
type="button"
class="px-3 py-2 rounded-lg border border-border bg-white/5 text-[12px] text-text-secondary hover:text-white hover:bg-bg-hover transition-colors"
on:click={() => (tvmazeEnabled = true)}
>
Add TVmaze
</button>
{/if}
</div>
</div>
{/if}
<!-- ===============================
OMDb Integration
=============================== -->
{#if omdbEnabled}
<div class="border border-border rounded-xl p-6 space-y-4">
<div class="flex items-start justify-between gap-4">
<div>
<h3 class="text-[13px] font-medium">OMDb API</h3>
<p class="text-[11px] text-text-tertiary mt-1">
The Open Movie Database - Movie and series metadata
</p>
</div>
<div class="flex items-center gap-2">
<div class="relative" use:clickOutside={() => (showOmdbHelp = false)}>
<button
type="button"
class="h-8 w-8 rounded-full border border-border text-text-secondary hover:text-white hover:bg-bg-hover transition-all"
on:click={toggleOmdbHelp}
aria-label="How to get an OMDb API key"
>
<Info class="h-4 w-4 mx-auto" />
</button>
{#if showOmdbHelp}
<div class="absolute right-0 mt-2 w-64 rounded-xl border border-border bg-bg-card p-4 text-[12px] text-text-secondary shadow-[0_12px_30px_rgba(0,0,0,0.35)] z-10">
<p class="text-[12px] text-text-primary font-medium mb-1">Get an OMDb API key</p>
<p class="text-[11px] text-text-tertiary mb-3">Create a free key in minutes.</p>
<ol class="space-y-1 text-[11px] text-text-secondary">
<li>Visit <a class="text-white hover:underline" href="https://www.omdbapi.com/apikey.aspx" target="_blank" rel="noopener noreferrer">omdbapi.com</a></li>
<li>Request a key and confirm the email</li>
<li>Paste the key here and save</li>
</ol>
</div>
{/if}
</div>
<span
class="px-2 py-1 text-[10px] font-medium text-text-secondary border border-border rounded-lg uppercase tracking-wide"
>
Movies & Series
</span>
</div>
</div>
<div class="flex items-center justify-between gap-4 rounded-lg border border-border bg-bg-card px-4 py-3">
<div>
<p class="text-[12px] font-medium text-text-primary">Enable OMDb</p>
<p class="text-[11px] text-text-tertiary">Use OMDb for movie and series metadata.</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" bind:checked={omdbEnabled} />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</label>
</div>
<div class="space-y-2">
<label
class="block text-[11px] font-medium text-text-secondary uppercase tracking-wide"
>
API Key
</label>
<input
type="text"
bind:value={omdbApiKey}
placeholder="Enter your OMDb API key"
disabled={!omdbEnabled}
class="w-full px-4 py-3 bg-bg-card border border-border rounded-lg
text-[13px] placeholder:text-text-tertiary
focus:outline-none focus:border-white/30 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
/>
</div>
{#if omdbUsage && omdbEnabled}
<div class="pt-4 border-t border-border space-y-2">
<div class="flex items-center justify-between text-[11px]">
<span class="text-text-secondary uppercase tracking-wide">
{usageLabel(omdbState)} · Usage (24h)
</span>
<span class="text-text-tertiary">
Resets in {formatResetTime(omdbUsage.reset_time)}
</span>
</div>
<div class="space-y-1.5">
<div class="flex items-center justify-between text-[11px]">
<span class="text-text-tertiary">
{omdbUsage.total_calls_24h} / {omdbUsage.limit} calls
</span>
<span class="text-text-tertiary">
{omdbPercent}%
</span>
</div>
<div
class="relative h-3 bg-bg-primary rounded-full overflow-hidden"
>
<div
class="absolute inset-y-0 left-0 transition-all {usageBarColor(
omdbState,
)}"
style="width: {omdbPercent}%"
></div>
<div class="absolute inset-0 flex items-center justify-center">
<span class="text-[10px] font-medium text-black/80">
{omdbPercent}%
</span>
</div>
</div>
</div>
</div>
{/if}
</div>
{/if}
<!-- ===============================
TVmaze Integration
=============================== -->
{#if tvmazeEnabled}
<div class="border border-border rounded-xl p-6 space-y-4">
<div class="flex items-start justify-between gap-4">
<div>
<h3 class="text-[13px] font-medium">TVmaze API</h3>
<p class="text-[11px] text-text-tertiary mt-1">
Free TV metadata for series and episodes (no API key required)
</p>
</div>
<div class="flex items-center gap-2">
<div class="relative" use:clickOutside={() => (showTvmazeHelp = false)}>
<button
type="button"
class="h-8 w-8 rounded-full border border-border text-text-secondary hover:text-white hover:bg-bg-hover transition-all"
on:click={toggleTvmazeHelp}
aria-label="TVmaze integration info"
>
<Info class="h-4 w-4 mx-auto" />
</button>
{#if showTvmazeHelp}
<div class="absolute right-0 mt-2 w-64 rounded-xl border border-border bg-bg-card p-4 text-[12px] text-text-secondary shadow-[0_12px_30px_rgba(0,0,0,0.35)] z-10">
<p class="text-[12px] text-text-primary font-medium mb-1">TVmaze quick start</p>
<p class="text-[11px] text-text-tertiary mb-3">No account needed. Toggle on to enable TV lookups.</p>
<ol class="space-y-1 text-[11px] text-text-secondary">
<li>Turn on the TVmaze integration</li>
<li>Pick TVmaze as a metadata source</li>
<li>Scan and enrich TV episodes</li>
</ol>
</div>
{/if}
</div>
<span
class="px-2 py-1 text-[10px] font-medium text-text-secondary border border-border rounded-lg uppercase tracking-wide"
>
TV Series
</span>
</div>
</div>
<div class="flex items-center justify-between gap-4 rounded-lg border border-border bg-bg-card px-4 py-3">
<div>
<p class="text-[12px] font-medium text-text-primary">Enable TVmaze</p>
<p class="text-[11px] text-text-tertiary">Use TVmaze for series + episode plots.</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" bind:checked={tvmazeEnabled} />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</label>
</div>
{#if tvmazeUsage && tvmazeEnabled}
<div class="pt-4 border-t border-border space-y-2">
<div class="flex items-center justify-between text-[11px]">
<span class="text-text-secondary uppercase tracking-wide">
{usageLabel(tvmazeState)} · Usage (24h)
</span>
<span class="text-text-tertiary">
Resets in {formatResetTime(tvmazeUsage.reset_time)}
</span>
</div>
<div class="space-y-1.5">
<div class="flex items-center justify-between text-[11px]">
<span class="text-text-tertiary">
{tvmazeUsage.total_calls_24h} / {tvmazeUsage.limit} calls
</span>
<span class="text-text-tertiary">
{tvmazePercent}%
</span>
</div>
<div
class="relative h-3 bg-bg-primary rounded-full overflow-hidden"
>
<div
class="absolute inset-y-0 left-0 transition-all {usageBarColor(
tvmazeState,
)}"
style="width: {tvmazePercent}%"
></div>
<div class="absolute inset-0 flex items-center justify-center">
<span class="text-[10px] font-medium text-black/80">
{tvmazePercent}%
</span>
</div>
</div>
</div>
</div>
{/if}
</div>
{/if}
<!-- ===============================
TMDb Integration
=============================== -->
{#if tmdbEnabled}
<div class="border border-border rounded-xl p-6 space-y-4">
<div class="flex items-start justify-between gap-4">
<div>
<h3 class="text-[13px] font-medium">TMDb API</h3>
<p class="text-[11px] text-text-tertiary mt-1">
The Movie Database - Comprehensive media database
</p>
</div>
<div class="flex items-center gap-2">
<div class="relative" use:clickOutside={() => (showTmdbHelp = false)}>
<button
type="button"
class="h-8 w-8 rounded-full border border-border text-text-secondary hover:text-white hover:bg-bg-hover transition-all"
on:click={toggleTmdbHelp}
aria-label="How to get a TMDb API key"
>
<Info class="h-4 w-4 mx-auto" />
</button>
{#if showTmdbHelp}
<div class="absolute right-0 mt-2 w-64 rounded-xl border border-border bg-bg-card p-4 text-[12px] text-text-secondary shadow-[0_12px_30px_rgba(0,0,0,0.35)] z-10">
<p class="text-[12px] text-text-primary font-medium mb-1">Get a TMDb API key</p>
<p class="text-[11px] text-text-tertiary mb-3">Requires a free TMDb account.</p>
<ol class="space-y-1 text-[11px] text-text-secondary">
<li>Sign in at <a class="text-white hover:underline" href="https://www.themoviedb.org" target="_blank" rel="noopener noreferrer">themoviedb.org</a></li>
<li>Go to Settings > API and request a key</li>
<li>Paste the key here and save</li>
</ol>
</div>
{/if}
</div>
<span
class="px-2 py-1 text-[10px] font-medium text-text-secondary border border-border rounded-lg uppercase tracking-wide"
>
Movies & Series
</span>
</div>
</div>
<div class="flex items-center justify-between gap-4 rounded-lg border border-border bg-bg-card px-4 py-3">
<div>
<p class="text-[12px] font-medium text-text-primary">Enable TMDb</p>
<p class="text-[11px] text-text-tertiary">Use TMDb for movies and TV metadata.</p>
</div>
<label class="relative inline-flex items-center cursor-pointer">
<input type="checkbox" class="sr-only peer" bind:checked={tmdbEnabled} />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</label>
</div>
<div class="space-y-2">
<label
class="block text-[11px] font-medium text-text-secondary uppercase tracking-wide"
>
API Key
</label>
<input
type="text"
bind:value={tmdbApiKey}
placeholder="Enter your TMDb API key"
disabled={!tmdbEnabled}
class="w-full px-4 py-3 bg-bg-card border border-border rounded-lg
text-[13px] placeholder:text-text-tertiary
focus:outline-none focus:border-white/30 transition-all disabled:opacity-40 disabled:cursor-not-allowed"
/>
</div>
{#if tmdbUsage && tmdbEnabled}
<div class="pt-4 border-t border-border space-y-2">
<div class="flex items-center justify-between text-[11px]">
<span class="text-text-secondary uppercase tracking-wide">
{usageLabel(tmdbState)} · Usage (24h)
</span>
<span class="text-text-tertiary">
Resets in {formatResetTime(tmdbUsage.reset_time)}
</span>
</div>
<div class="space-y-1.5">
<div class="flex items-center justify-between text-[11px]">
<span class="text-text-tertiary">
{tmdbUsage.total_calls_24h} / {tmdbUsage.limit} calls
</span>
<span class="text-text-tertiary">
{tmdbPercent}%
</span>
</div>
<div
class="relative h-3 bg-bg-primary rounded-full overflow-hidden"
>
<div
class="absolute inset-y-0 left-0 transition-all {usageBarColor(
tmdbState,
)}"
style="width: {tmdbPercent}%"
></div>
<div class="absolute inset-0 flex items-center justify-center">
<span class="text-[10px] font-medium text-black/80">
{tmdbPercent}%
</span>
</div>
</div>
</div>
</div>
{/if}
</div>
{/if}
</div>
</div>
<button
type="submit"
disabled={saving}
class="px-7 py-3.5 bg-white hover:bg-white/90 disabled:opacity-30 disabled:cursor-not-allowed
text-black text-[13px] font-medium rounded-xl transition-all"
>
{saving ? "Saving…" : "Save Changes"}
</button>
</form>
@@ -0,0 +1,123 @@
<script>
import { onMount } from 'svelte'
import { Button } from '../../lib/components/ui/button'
import { Input } from '../../lib/components/ui/input'
import { createScheduledScan } from '../../lib/api.js'
import { addToast } from '../../lib/toastStore.js'
export let settings = {}
let directory = settings.default_directory || ''
let scheduledFor = ''
let creating = false
let error = null
let successMessage = null
function getLocalDateTimeValue(date) {
const pad = (value) => String(value).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(
date.getDate()
)}T${pad(date.getHours())}:${pad(date.getMinutes())}`
}
async function scheduleScan() {
if (!directory || !scheduledFor) {
error = 'Please choose a directory and scheduled time'
successMessage = null
return
}
creating = true
error = null
try {
const scheduledIso = new Date(scheduledFor).toISOString()
await createScheduledScan(directory, scheduledIso)
successMessage = 'Scan scheduled. Track results in Scheduled Scans.'
addToast({ message: 'Scheduled scan created.', tone: 'success' })
} catch (err) {
error = `Failed to schedule scan: ${err.message}`
addToast({ message: error, tone: 'error' })
} finally {
creating = false
}
}
onMount(() => {
const nextHour = new Date()
nextHour.setHours(nextHour.getHours() + 1)
scheduledFor = getLocalDateTimeValue(nextHour)
})
</script>
<div class="space-y-6">
<div>
<h2 class="text-lg font-semibold text-text-primary">Scheduled Scans</h2>
<p class="text-[13px] text-text-secondary">
Plan scans ahead of time and let them run in the background.
</p>
</div>
{#if error}
<div class="px-5 py-4 bg-red-500/5 border border-red-500/20 rounded-xl">
<p class="text-[13px] text-red-300">{error}</p>
</div>
{/if}
{#if successMessage}
<div class="px-5 py-4 bg-green-500/5 border border-green-500/20 rounded-xl">
<p class="text-[13px] text-green-300">{successMessage}</p>
</div>
{/if}
<div class="rounded-xl border border-border bg-card p-6 space-y-4">
<div>
<h3 class="text-sm font-semibold text-text-primary">Schedule a scan</h3>
<p class="text-[12px] text-text-tertiary">
Use your default directory or customize a one-off scan target.
</p>
</div>
<div class="grid gap-4 sm:grid-cols-2">
<div class="space-y-2">
<label class="block text-[12px] font-medium text-text-primary">
Directory
</label>
<Input
type="text"
bind:value={directory}
placeholder="C:\Movies or /media/movies"
className="h-10 text-[12px] font-mono"
/>
<p class="text-[11px] text-text-tertiary">
Default directory comes from General Settings.
</p>
</div>
<div class="space-y-2">
<label class="block text-[12px] font-medium text-text-primary">
Scheduled for
</label>
<Input
type="datetime-local"
bind:value={scheduledFor}
className="h-10 text-[12px]"
/>
<p class="text-[11px] text-text-tertiary">
Scheduled scans run once at the chosen time.
</p>
</div>
</div>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<p class="text-[11px] text-text-tertiary">
Results show in Scheduled Scans and the History tab.
</p>
<Button
size="sm"
className="h-9 px-4"
on:click={scheduleScan}
disabled={creating}
>
{creating ? 'Scheduling...' : 'Schedule Scan'}
</Button>
</div>
</div>
</div>
@@ -0,0 +1,199 @@
<script>
import { Button } from '../../lib/components/ui/button'
import { resetSettings, clearHistory, clearCaches } from '../../lib/api.js'
import { scanResults } from '../../lib/scanStore.js'
import { addToast } from '../../lib/toastStore.js'
let keepApiKeys = true
let runningReset = false
let runningHistory = false
let runningCaches = false
let error = null
let successMessage = null
function resetMessages() {
error = null
successMessage = null
}
async function handleResetSettings() {
resetMessages()
runningReset = true
try {
await resetSettings(keepApiKeys)
successMessage = 'Settings cleared successfully.'
addToast({ message: successMessage, tone: 'success' })
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('sublogue_onboarding_complete')
}
scanResults.clearResults()
} catch (err) {
error = `Failed to clear settings: ${err.message}`
addToast({ message: error, tone: 'error' })
} finally {
runningReset = false
}
}
async function handleClearHistory() {
resetMessages()
runningHistory = true
try {
await clearHistory()
successMessage = 'History and logs cleared.'
addToast({ message: successMessage, tone: 'success' })
} catch (err) {
error = `Failed to clear history: ${err.message}`
addToast({ message: error, tone: 'error' })
} finally {
runningHistory = false
}
}
async function handleClearCaches() {
resetMessages()
runningCaches = true
try {
await clearCaches()
successMessage = 'Caches cleared.'
addToast({ message: successMessage, tone: 'success' })
} catch (err) {
error = `Failed to clear caches: ${err.message}`
addToast({ message: error, tone: 'error' })
} finally {
runningCaches = false
}
}
</script>
<div class="space-y-6">
<div>
<h2 class="text-lg font-semibold text-text-primary">Maintenance Tasks</h2>
<p class="text-[13px] text-text-secondary">
Run cleanup tasks to keep the app lean and reset state when needed.
</p>
</div>
{#if error}
<div class="px-5 py-4 bg-red-500/5 border border-red-500/20 rounded-xl">
<p class="text-[13px] text-red-300">{error}</p>
</div>
{/if}
{#if successMessage}
<div class="px-5 py-4 bg-green-500/5 border border-green-500/20 rounded-xl">
<p class="text-[13px] text-green-300">{successMessage}</p>
</div>
{/if}
<div class="grid gap-4">
<div class="rounded-xl border border-border bg-card p-6 space-y-4">
<div>
<h3 class="text-sm font-semibold text-text-primary">Clear history and logs</h3>
<p class="text-[12px] text-text-tertiary">
Removes processing runs, scan history, scheduled scans, and API usage logs.
</p>
</div>
<div class="flex items-center justify-between">
<p class="text-[11px] text-text-tertiary">
Useful if you want a clean slate for reporting.
</p>
<Button
size="sm"
variant="outline"
className="h-9 px-4"
on:click={handleClearHistory}
disabled={runningHistory}
>
{#if runningHistory}
<span class="inline-flex items-center gap-2">
<span class="h-3 w-3 rounded-full border border-white/60 border-t-transparent animate-spin"></span>
Clearing...
</span>
{:else}
Clear History
{/if}
</Button>
</div>
</div>
<div class="rounded-xl border border-border bg-card p-6 space-y-4">
<div>
<h3 class="text-sm font-semibold text-text-primary">Clear caches</h3>
<p class="text-[12px] text-text-tertiary">
Clears cached matches and resets the current scan state.
</p>
</div>
<div class="flex items-center justify-between">
<p class="text-[11px] text-text-tertiary">
Recommended after large directory changes.
</p>
<Button
size="sm"
variant="outline"
className="h-9 px-4"
on:click={handleClearCaches}
disabled={runningCaches}
>
{#if runningCaches}
<span class="inline-flex items-center gap-2">
<span class="h-3 w-3 rounded-full border border-white/60 border-t-transparent animate-spin"></span>
Clearing...
</span>
{:else}
Clear Caches
{/if}
</Button>
</div>
</div>
</div>
<div class="rounded-xl border border-red-500/30 bg-red-500/5 p-6 space-y-4">
<div>
<h3 class="text-sm font-semibold text-red-200">Danger Zone</h3>
<p class="text-[12px] text-red-200/70">
Destructive actions that reset your configuration.
</p>
</div>
<div class="rounded-xl border border-red-500/30 bg-bg-card/60 p-4 space-y-4">
<div>
<h4 class="text-[13px] font-semibold text-text-primary">Reset settings</h4>
<p class="text-[12px] text-text-tertiary">
Clears all saved settings and resets preferences to defaults.
</p>
</div>
<label class="flex items-center justify-between gap-4 rounded-xl border border-border bg-bg-secondary/40 px-4 py-3 text-[12px] text-text-secondary">
<span>Keep OMDb and TMDb API keys</span>
<span class="relative inline-flex items-center">
<input type="checkbox" bind:checked={keepApiKeys} class="sr-only peer" />
<span class="h-6 w-11 rounded-full border border-border bg-bg-card transition-colors peer-checked:bg-accent peer-checked:border-accent/60"></span>
<span class="absolute left-0.5 h-5 w-5 rounded-full bg-text-tertiary transition-transform peer-checked:translate-x-5 peer-checked:bg-bg-primary"></span>
</span>
</label>
<div class="flex items-center justify-between">
<p class="text-[11px] text-text-tertiary">
This does not delete scan history or logs.
</p>
<Button
size="sm"
variant="destructive"
className="h-9 px-4"
on:click={handleResetSettings}
disabled={runningReset}
>
{#if runningReset}
<span class="inline-flex items-center gap-2">
<span class="h-3 w-3 rounded-full border border-white/60 border-t-transparent animate-spin"></span>
Clearing...
</span>
{:else}
Clear Settings
{/if}
</Button>
</div>
</div>
</div>
</div>