Initial commit
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user