1.1.0 - automations, clean only mode, bug fixes
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
Settings,
|
||||
History,
|
||||
Library,
|
||||
Zap,
|
||||
} from "lucide-svelte";
|
||||
import ThemeSelector from "./ThemeSelector.svelte";
|
||||
import sublogueLogo from "../assets/sublogue_v2.png";
|
||||
@@ -89,6 +90,24 @@
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={`w-full rounded-md py-1.5 text-[13px] font-semibold leading-none ${
|
||||
collapsed ? "justify-center px-0" : "justify-start px-2 gap-2"
|
||||
} ${
|
||||
currentView === "automation"
|
||||
? "bg-[color:var(--bg-hover)] text-white font-bold"
|
||||
: "text-text-secondary hover:text-white hover:bg-[color:var(--bg-hover)]"
|
||||
}`}
|
||||
on:click={() => onNavigate("automation")}
|
||||
aria-current={currentView === "automation" ? "page" : undefined}
|
||||
>
|
||||
<Zap class="h-4 w-4" />
|
||||
{#if !collapsed}
|
||||
Automations
|
||||
{/if}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={`w-full rounded-md py-1.5 text-[13px] font-semibold leading-none ${
|
||||
@@ -150,28 +169,32 @@
|
||||
<ThemeSelector className="w-full" />
|
||||
{/if}
|
||||
<div
|
||||
class={`flex items-center rounded-md bg-white/5 px-3 py-2 text-xs ${collapsed ? "justify-center" : "justify-between"}`}
|
||||
class={`flex items-center px-3 py-2 text-xs ${collapsed ? "justify-center" : "justify-between"}`}
|
||||
>
|
||||
{#if !collapsed}
|
||||
<Badge className="bg-white/10 text-text-secondary"
|
||||
>v1.0.10 Release Candiate</Badge
|
||||
>
|
||||
<Badge className="text-text-secondary">Version: v1.1.0</Badge>
|
||||
{:else}
|
||||
<Badge className="bg-white/10 text-text-secondary">v</Badge>
|
||||
<Badge className="text-text-secondary">v</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<a
|
||||
href="https://github.com/ponzischeme89/Sublogue"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class={`inline-flex items-center rounded-md bg-white/5 px-3 py-2 text-xs text-text-tertiary hover:text-white hover:bg-[color:var(--bg-hover)] transition-colors ${collapsed ? "justify-center" : "gap-2"}`}
|
||||
<div
|
||||
class={`flex items-center px-3 py-2 text-xs ${collapsed ? "justify-center" : "justify-between"}`}
|
||||
>
|
||||
<Github class="h-4 w-4" />
|
||||
{#if !collapsed}
|
||||
Star on GitHub
|
||||
<span class="text-text-tertiary">© 2026 ponzischeme89</span>
|
||||
<a
|
||||
href="https://github.com/ponzischeme89/Sublogue"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center text-text-tertiary hover:text-white transition-colors"
|
||||
aria-label="GitHub"
|
||||
>
|
||||
<Github class="h-4 w-4" />
|
||||
</a>
|
||||
{:else}
|
||||
<span class="text-text-tertiary">©</span>
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -1,76 +1,76 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import { getSettings, updateSettings } from '../lib/api.js'
|
||||
import { Skeleton } from '../lib/components/ui/skeleton'
|
||||
import GeneralSettings from './settings/GeneralSettings.svelte'
|
||||
import IntegrationsSettings from './settings/IntegrationsSettings.svelte'
|
||||
import FilenameCleaningSettings from './settings/FilenameCleaningSettings.svelte'
|
||||
import ScheduledScansSettings from './settings/ScheduledScansSettings.svelte'
|
||||
import FolderRulesSettings from './settings/FolderRulesSettings.svelte'
|
||||
import TasksSettings from './settings/TasksSettings.svelte'
|
||||
import { addToast } from '../lib/toastStore.js'
|
||||
import { Bolt, Calendar, Folder, Plug, Settings, Wand2 } from 'lucide-svelte'
|
||||
import { onMount } from "svelte";
|
||||
import { getSettings, updateSettings } from "../lib/api.js";
|
||||
import { Skeleton } from "../lib/components/ui/skeleton";
|
||||
import GeneralSettings from "./settings/GeneralSettings.svelte";
|
||||
import IntegrationsSettings from "./settings/IntegrationsSettings.svelte";
|
||||
import FilenameCleaningSettings from "./settings/FilenameCleaningSettings.svelte";
|
||||
import FolderRulesSettings from "./settings/FolderRulesSettings.svelte";
|
||||
import TasksSettings from "./settings/TasksSettings.svelte";
|
||||
import { addToast } from "../lib/toastStore.js";
|
||||
import { Bolt, Calendar, Folder, Plug, Settings, Wand2 } from "lucide-svelte";
|
||||
|
||||
let currentSection = 'general'
|
||||
let settings = {}
|
||||
let loading = false
|
||||
let saving = false
|
||||
let error = null
|
||||
let successMessage = null
|
||||
let currentSection = "general";
|
||||
let settings = {};
|
||||
let loading = false;
|
||||
let saving = false;
|
||||
let error = null;
|
||||
let successMessage = null;
|
||||
|
||||
const sections = [
|
||||
{ id: 'general', label: 'General', icon: 'settings' },
|
||||
{ id: 'folder-rules', label: 'Folder Rules', icon: 'folder' },
|
||||
{ id: 'scheduled', label: 'Scheduled Scans', icon: 'calendar' },
|
||||
{ id: 'cleanup', label: 'Cleanup', icon: 'wand' },
|
||||
{ id: 'integrations', label: 'Integrations', icon: 'plug' },
|
||||
{ id: 'tasks', label: 'Tasks', icon: 'bolt' }
|
||||
]
|
||||
{ id: "general", label: "General", icon: "settings" },
|
||||
{ id: "folder-rules", label: "Folder Rules", icon: "folder" },
|
||||
{ id: "cleanup", label: "Cleanup", icon: "wand" },
|
||||
{ id: "integrations", label: "Integrations", icon: "plug" },
|
||||
{ id: "tasks", label: "Tasks", icon: "bolt" },
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
await loadSettings()
|
||||
})
|
||||
await loadSettings();
|
||||
});
|
||||
|
||||
async function loadSettings() {
|
||||
loading = true
|
||||
error = null
|
||||
const loadingStart = Date.now()
|
||||
loading = true;
|
||||
error = null;
|
||||
const loadingStart = Date.now();
|
||||
|
||||
try {
|
||||
settings = await getSettings()
|
||||
settings = await getSettings();
|
||||
} catch (err) {
|
||||
error = `Failed to load settings: ${err.message}`
|
||||
error = `Failed to load settings: ${err.message}`;
|
||||
} finally {
|
||||
const elapsed = Date.now() - loadingStart
|
||||
const minDelayMs = 500
|
||||
const elapsed = Date.now() - loadingStart;
|
||||
const minDelayMs = 500;
|
||||
if (elapsed < minDelayMs) {
|
||||
await new Promise((resolve) => setTimeout(resolve, minDelayMs - elapsed))
|
||||
await new Promise((resolve) =>
|
||||
setTimeout(resolve, minDelayMs - elapsed),
|
||||
);
|
||||
}
|
||||
loading = false
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave(updates) {
|
||||
saving = true
|
||||
error = null
|
||||
successMessage = null
|
||||
saving = true;
|
||||
error = null;
|
||||
successMessage = null;
|
||||
|
||||
try {
|
||||
const result = await updateSettings(updates)
|
||||
successMessage = result.message || 'Settings saved successfully'
|
||||
addToast({ message: successMessage, tone: 'success' })
|
||||
const result = await updateSettings(updates);
|
||||
successMessage = result.message || "Settings saved successfully";
|
||||
addToast({ message: successMessage, tone: "success" });
|
||||
|
||||
// Reload settings
|
||||
await loadSettings()
|
||||
await loadSettings();
|
||||
|
||||
setTimeout(() => {
|
||||
successMessage = null
|
||||
}, 3000)
|
||||
successMessage = null;
|
||||
}, 3000);
|
||||
} catch (err) {
|
||||
error = `Failed to save settings: ${err.message}`
|
||||
addToast({ message: error, tone: 'error' })
|
||||
error = `Failed to save settings: ${err.message}`;
|
||||
addToast({ message: error, tone: "error" });
|
||||
} finally {
|
||||
saving = false
|
||||
saving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -116,66 +116,72 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col lg:flex-row gap-8 lg:gap-12">
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="w-full lg:w-48 flex-shrink-0">
|
||||
<nav class="space-y-0.5">
|
||||
{#each sections as section}
|
||||
<button
|
||||
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-all border
|
||||
<!-- Sidebar Navigation -->
|
||||
<aside class="w-full lg:w-48 flex-shrink-0">
|
||||
<nav class="space-y-0.5">
|
||||
{#each sections as section}
|
||||
<button
|
||||
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-all border
|
||||
{currentSection === section.id
|
||||
? 'bg-white text-black border-white'
|
||||
: 'text-text-secondary hover:text-white hover:bg-bg-hover border-transparent'}"
|
||||
on:click={() => currentSection = section.id}
|
||||
? 'bg-white text-black border-white'
|
||||
: 'text-text-secondary hover:text-white hover:bg-bg-hover border-transparent'}"
|
||||
on:click={() => (currentSection = section.id)}
|
||||
>
|
||||
{#if section.icon === "settings"}
|
||||
<Settings class="w-4 h-4" />
|
||||
{:else if section.icon === "calendar"}
|
||||
<Calendar class="w-4 h-4" />
|
||||
{:else if section.icon === "bolt"}
|
||||
<Bolt class="w-4 h-4" />
|
||||
{:else if section.icon === "wand"}
|
||||
<Wand2 class="w-4 h-4" />
|
||||
{:else if section.icon === "plug"}
|
||||
<Plug class="w-4 h-4" />
|
||||
{:else if section.icon === "folder"}
|
||||
<Folder class="w-4 h-4" />
|
||||
{/if}
|
||||
<span class="text-[13px] font-medium">{section.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
{#if error}
|
||||
<div
|
||||
class="mb-6 px-5 py-4 bg-red-500/5 border border-red-500/20 rounded-xl"
|
||||
>
|
||||
{#if section.icon === 'settings'}
|
||||
<Settings class="w-4 h-4" />
|
||||
{:else if section.icon === 'calendar'}
|
||||
<Calendar class="w-4 h-4" />
|
||||
{:else if section.icon === 'bolt'}
|
||||
<Bolt class="w-4 h-4" />
|
||||
{:else if section.icon === 'wand'}
|
||||
<Wand2 class="w-4 h-4" />
|
||||
{:else if section.icon === 'plug'}
|
||||
<Plug class="w-4 h-4" />
|
||||
{:else if section.icon === 'folder'}
|
||||
<Folder class="w-4 h-4" />
|
||||
{/if}
|
||||
<span class="text-[13px] font-medium">{section.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
{#if error}
|
||||
<div class="mb-6 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="mb-6 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/60 p-6 lg:p-8 shadow-sm">
|
||||
{#if currentSection === 'general'}
|
||||
<GeneralSettings {settings} {saving} onSave={handleSave} />
|
||||
{:else if currentSection === 'folder-rules'}
|
||||
<FolderRulesSettings {settings} />
|
||||
{:else if currentSection === 'scheduled'}
|
||||
<ScheduledScansSettings {settings} />
|
||||
{:else if currentSection === 'cleanup'}
|
||||
<FilenameCleaningSettings {settings} {saving} onSave={handleSave} />
|
||||
{:else if currentSection === 'integrations'}
|
||||
<IntegrationsSettings {settings} {saving} onSave={handleSave} />
|
||||
{:else if currentSection === 'tasks'}
|
||||
<TasksSettings />
|
||||
<p class="text-[13px] text-red-300">{error}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if successMessage}
|
||||
<div
|
||||
class="mb-6 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/60 p-6 lg:p-8 shadow-sm"
|
||||
>
|
||||
{#if currentSection === "general"}
|
||||
<GeneralSettings {settings} {saving} onSave={handleSave} />
|
||||
{:else if currentSection === "folder-rules"}
|
||||
<FolderRulesSettings {settings} />
|
||||
{:else if currentSection === "scheduled"}
|
||||
<ScheduledScansSettings {settings} />
|
||||
{:else if currentSection === "cleanup"}
|
||||
<FilenameCleaningSettings {settings} {saving} onSave={handleSave} />
|
||||
{:else if currentSection === "integrations"}
|
||||
<IntegrationsSettings {settings} {saving} onSave={handleSave} />
|
||||
{:else if currentSection === "tasks"}
|
||||
<TasksSettings />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
const toneStyles = {
|
||||
info: 'border-white/10 bg-bg-card text-text-primary',
|
||||
success: 'border-green-500/30 bg-green-500/10 text-green-100',
|
||||
success: 'border-emerald-500/50 bg-emerald-900/60 text-emerald-50',
|
||||
error: 'border-red-500/30 bg-red-500/10 text-red-100'
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
export let disabled = false;
|
||||
export let metadataProvider = "omdb";
|
||||
export let metadataLanguage = "";
|
||||
export let cleanMode = false;
|
||||
export let activeIntegrations = {
|
||||
omdb: true,
|
||||
tmdb: true,
|
||||
@@ -43,6 +44,12 @@
|
||||
);
|
||||
$: pageStart = (currentPage - 1) * itemsPerPage + 1;
|
||||
$: pageEnd = Math.min(currentPage * itemsPerPage, files.length);
|
||||
$: cleanModeWithKeywords = paginatedFiles.filter(
|
||||
(file) => file.clean_keywords && file.clean_keywords.length > 0,
|
||||
);
|
||||
$: cleanModeNoKeywords = paginatedFiles.filter(
|
||||
(file) => !file.clean_keywords || file.clean_keywords.length === 0,
|
||||
);
|
||||
|
||||
function goToPage(page) {
|
||||
currentPage = Math.max(1, Math.min(page, totalPages));
|
||||
@@ -814,6 +821,12 @@
|
||||
$: if (!hasActiveIntegrations && showMetadataDropup) {
|
||||
showMetadataDropup = false;
|
||||
}
|
||||
$: if (cleanMode && showMetadataDropup) {
|
||||
showMetadataDropup = false;
|
||||
}
|
||||
$: if (cleanMode && openSearchDropdown) {
|
||||
openSearchDropdown = null;
|
||||
}
|
||||
$: metadataDropdownOptions = [
|
||||
{
|
||||
value: "omdb",
|
||||
@@ -972,6 +985,7 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
{#if !cleanMode}
|
||||
<!-- Metadata Source Selector -->
|
||||
<div class="relative">
|
||||
<button
|
||||
@@ -1120,6 +1134,7 @@
|
||||
Bulk Match
|
||||
{/if}
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
<Button
|
||||
on:click={handleBulkProcess}
|
||||
@@ -1127,7 +1142,7 @@
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-white/90"
|
||||
>
|
||||
Add Subtitles to Selected
|
||||
{cleanMode ? "Clean Selected" : "Add Subtitles to Selected"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1151,7 +1166,9 @@
|
||||
<TableHead className="w-10"></TableHead>
|
||||
<TableHead className="w-28">Status</TableHead>
|
||||
<TableHead className="w-[38%]">Filename</TableHead>
|
||||
<TableHead className="w-[28%]">Matched Result</TableHead>
|
||||
<TableHead className="w-[28%]">
|
||||
{cleanMode ? "Clean Scan" : "Matched Result"}
|
||||
</TableHead>
|
||||
<TableHead className="w-64 text-right"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -1182,6 +1199,206 @@
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
{:else if cleanMode}
|
||||
{#if cleanModeWithKeywords.length > 0}
|
||||
<TableRow className="bg-bg-secondary/60">
|
||||
<TableCell colspan="6">
|
||||
<div class="text-[11px] uppercase tracking-wide text-text-tertiary">
|
||||
Results With Keywords
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{#each cleanModeWithKeywords as file (file.path)}
|
||||
<TableRow data-state={file.selected ? "selected" : undefined}>
|
||||
<TableCell className="w-12">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={file.selected}
|
||||
on:change={() => toggleSelection(file)}
|
||||
class="h-4 w-4 rounded border-input bg-background text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="w-10">
|
||||
<button
|
||||
on:click={() => toggleRowExpand(file.path)}
|
||||
class="text-text-tertiary hover:text-foreground transition-colors"
|
||||
title="Toggle details"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform {expandedRows[
|
||||
file.path
|
||||
]
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell className="w-28">
|
||||
<StatusBadge status={file.status} />
|
||||
</TableCell>
|
||||
<TableCell className="min-w-0">
|
||||
<span
|
||||
class="text-[13px] font-mono truncate block"
|
||||
title={file.name}
|
||||
>
|
||||
{file.name}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="min-w-0">
|
||||
<div class="text-[12px] text-text-secondary">
|
||||
Detected: {file.clean_keywords.join(", ")}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-64">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<span class="text-[11px] text-text-tertiary">
|
||||
Clean only
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{#if expandedRows[file.path]}
|
||||
<TableRow className="bg-muted/40">
|
||||
<TableCell colspan="6">
|
||||
<div class="ml-16 space-y-3">
|
||||
<div>
|
||||
<div
|
||||
class="text-[10px] text-text-tertiary uppercase tracking-wide mb-1.5"
|
||||
>
|
||||
Detected Keywords
|
||||
</div>
|
||||
<div class="text-[13px] text-text-secondary">
|
||||
{file.clean_keywords.join(", ")}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="text-[10px] text-text-tertiary uppercase tracking-wide mb-1.5"
|
||||
>
|
||||
Cleanup Summary
|
||||
</div>
|
||||
<div
|
||||
class="text-[13px] text-text-secondary leading-relaxed bg-bg-primary/50 border border-white/[0.08] rounded-lg p-3"
|
||||
>
|
||||
{file.summary || "No changes needed"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{#if cleanModeNoKeywords.length > 0}
|
||||
<TableRow className="bg-bg-secondary/60">
|
||||
<TableCell colspan="6">
|
||||
<div class="text-[11px] uppercase tracking-wide text-text-tertiary">
|
||||
Clean Files (No Keywords Found)
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{#each cleanModeNoKeywords as file (file.path)}
|
||||
<TableRow data-state={file.selected ? "selected" : undefined}>
|
||||
<TableCell className="w-12">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={file.selected}
|
||||
on:change={() => toggleSelection(file)}
|
||||
class="h-4 w-4 rounded border-input bg-background text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="w-10">
|
||||
<button
|
||||
on:click={() => toggleRowExpand(file.path)}
|
||||
class="text-text-tertiary hover:text-foreground transition-colors"
|
||||
title="Toggle details"
|
||||
>
|
||||
<svg
|
||||
class="w-4 h-4 transition-transform {expandedRows[
|
||||
file.path
|
||||
]
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell className="w-28">
|
||||
<StatusBadge status={file.status} />
|
||||
</TableCell>
|
||||
<TableCell className="min-w-0">
|
||||
<span
|
||||
class="text-[13px] font-mono truncate block"
|
||||
title={file.name}
|
||||
>
|
||||
{file.name}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="min-w-0">
|
||||
<div class="text-[12px] text-text-secondary">
|
||||
Detected: None
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="w-64">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<span class="text-[11px] text-text-tertiary">
|
||||
Clean only
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{#if expandedRows[file.path]}
|
||||
<TableRow className="bg-muted/40">
|
||||
<TableCell colspan="6">
|
||||
<div class="ml-16 space-y-3">
|
||||
<div>
|
||||
<div
|
||||
class="text-[10px] text-text-tertiary uppercase tracking-wide mb-1.5"
|
||||
>
|
||||
Detected Keywords
|
||||
</div>
|
||||
<div class="text-[13px] text-text-secondary">
|
||||
None
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="text-[10px] text-text-tertiary uppercase tracking-wide mb-1.5"
|
||||
>
|
||||
Cleanup Summary
|
||||
</div>
|
||||
<div
|
||||
class="text-[13px] text-text-secondary leading-relaxed bg-bg-primary/50 border border-white/[0.08] rounded-lg p-3"
|
||||
>
|
||||
{file.summary || "No changes needed"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{:else}
|
||||
{#each paginatedFiles as file (file.path)}
|
||||
<TableRow data-state={file.selected ? "selected" : undefined}>
|
||||
@@ -1230,6 +1447,15 @@
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="min-w-0">
|
||||
{#if cleanMode}
|
||||
<div class="text-[12px] text-text-secondary">
|
||||
{#if file.clean_keywords && file.clean_keywords.length > 0}
|
||||
Detected: {file.clean_keywords.join(", ")}
|
||||
{:else}
|
||||
Detected: None
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -1359,9 +1585,15 @@
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</TableCell>
|
||||
<TableCell className="w-64">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
{#if cleanMode}
|
||||
<span class="text-[11px] text-text-tertiary">
|
||||
Clean only
|
||||
</span>
|
||||
{:else}
|
||||
{#if suggestedMatches[file.path]}
|
||||
<!-- Show quick apply button for suggested matches -->
|
||||
<Button
|
||||
@@ -1414,6 +1646,7 @@
|
||||
? "Update"
|
||||
: "Add Plot"}
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@@ -1422,6 +1655,36 @@
|
||||
{#if expandedRows[file.path]}
|
||||
<TableRow className="bg-muted/40">
|
||||
<TableCell colspan="6">
|
||||
{#if cleanMode}
|
||||
<div class="ml-16 space-y-3">
|
||||
<div>
|
||||
<div
|
||||
class="text-[10px] text-text-tertiary uppercase tracking-wide mb-1.5"
|
||||
>
|
||||
Detected Keywords
|
||||
</div>
|
||||
<div class="text-[13px] text-text-secondary">
|
||||
{#if file.clean_keywords && file.clean_keywords.length > 0}
|
||||
{file.clean_keywords.join(", ")}
|
||||
{:else}
|
||||
None
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="text-[10px] text-text-tertiary uppercase tracking-wide mb-1.5"
|
||||
>
|
||||
Cleanup Summary
|
||||
</div>
|
||||
<div
|
||||
class="text-[13px] text-text-secondary leading-relaxed bg-bg-primary/50 border border-white/[0.08] rounded-lg p-3"
|
||||
>
|
||||
{file.summary || "No changes needed"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-3 gap-6 ml-16">
|
||||
<!-- Rating -->
|
||||
<div>
|
||||
@@ -1483,6 +1746,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/if}
|
||||
@@ -1569,7 +1833,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Bulk Apply Section -->
|
||||
{#if hasMatches || bulkApplying}
|
||||
{#if !cleanMode && (hasMatches || bulkApplying)}
|
||||
<div
|
||||
class="bg-blue-500/10 border border-blue-500/30 rounded-xl px-6 py-4"
|
||||
>
|
||||
@@ -2029,17 +2293,19 @@
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
<button
|
||||
on:click={() => {
|
||||
handleProcessSingle(previewFile);
|
||||
closePreview();
|
||||
}}
|
||||
disabled={disabled || previewFile.status === "Has Plot"}
|
||||
class="px-5 py-2.5 bg-white hover:bg-white/90 disabled:opacity-30 disabled:cursor-not-allowed
|
||||
text-black text-[13px] font-medium rounded-xl transition-all"
|
||||
>
|
||||
Add Subtitles
|
||||
</button>
|
||||
{#if !cleanMode}
|
||||
<button
|
||||
on:click={() => {
|
||||
handleProcessSingle(previewFile);
|
||||
closePreview();
|
||||
}}
|
||||
disabled={disabled || previewFile.status === "Has Plot"}
|
||||
class="px-5 py-2.5 bg-white hover:bg-white/90 disabled:opacity-30 disabled:cursor-not-allowed
|
||||
text-black text-[13px] font-medium rounded-xl transition-all"
|
||||
>
|
||||
Add Subtitles
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -82,7 +82,8 @@
|
||||
let pendingTitleOverride = null;
|
||||
let resultsListRef = null;
|
||||
let pendingForceReprocess = false;
|
||||
let quoteStyle = 'sarcastic';
|
||||
let cleanOnly = false;
|
||||
let quoteStyle = "sarcastic";
|
||||
let initialLoading = true;
|
||||
let scanToastedError = false;
|
||||
let metadataOptions = [];
|
||||
@@ -273,7 +274,12 @@
|
||||
files = [...files, ...data.batch];
|
||||
|
||||
// Trigger auto-matching for the new batch if ResultsList is ready
|
||||
if (resultsListRef && data.batch && data.batch.length > 0) {
|
||||
if (
|
||||
!cleanOnly &&
|
||||
resultsListRef &&
|
||||
data.batch &&
|
||||
data.batch.length > 0
|
||||
) {
|
||||
console.log(
|
||||
"Auto-matching new batch of",
|
||||
data.batch.length,
|
||||
@@ -327,7 +333,7 @@
|
||||
Object.keys(existingMatches).length,
|
||||
);
|
||||
|
||||
if (resultsListRef) {
|
||||
if (!cleanOnly && resultsListRef) {
|
||||
// ALWAYS load existing matches from database first (even if empty, to reset state)
|
||||
console.log(
|
||||
"Loading existing matches into component:",
|
||||
@@ -372,7 +378,8 @@
|
||||
}
|
||||
scanToastedError = true;
|
||||
addToast({
|
||||
message: err.name === "AbortError" ? "Scan cancelled." : "Scan failed.",
|
||||
message:
|
||||
err.name === "AbortError" ? "Scan cancelled." : "Scan failed.",
|
||||
tone: err.name === "AbortError" ? "info" : "error",
|
||||
});
|
||||
},
|
||||
@@ -387,7 +394,8 @@
|
||||
}
|
||||
if (!scanToastedError) {
|
||||
addToast({
|
||||
message: err.name === "AbortError" ? "Scan cancelled." : "Scan failed.",
|
||||
message:
|
||||
err.name === "AbortError" ? "Scan cancelled." : "Scan failed.",
|
||||
tone: err.name === "AbortError" ? "info" : "error",
|
||||
});
|
||||
}
|
||||
@@ -502,6 +510,7 @@
|
||||
duration,
|
||||
pendingTitleOverride,
|
||||
forceReprocess,
|
||||
cleanOnly,
|
||||
);
|
||||
processingResults = response.results;
|
||||
|
||||
@@ -510,6 +519,15 @@
|
||||
files = files.map((file) => {
|
||||
const result = processingResults.find((r) => r.file === file.path);
|
||||
if (result) {
|
||||
if (result.clean_only) {
|
||||
return {
|
||||
...file,
|
||||
status: result.status || (result.success ? "Cleaned" : "Error"),
|
||||
summary: result.summary || file.summary || "",
|
||||
clean_only: true,
|
||||
clean_keywords: result.clean_keywords || [],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...file,
|
||||
status: result.status || (result.success ? "Processed" : "Error"),
|
||||
@@ -554,10 +572,7 @@
|
||||
}
|
||||
|
||||
function normalizePath(path) {
|
||||
return (path || "")
|
||||
.replace(/\//g, "\\")
|
||||
.replace(/\\+$/, "")
|
||||
.toLowerCase();
|
||||
return (path || "").replace(/\//g, "\\").replace(/\\+$/, "").toLowerCase();
|
||||
}
|
||||
|
||||
function findFolderRuleForDirectory(path, rules) {
|
||||
@@ -668,370 +683,427 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Scan Panel -->
|
||||
<Card className="bg-card" skeletonFlash={false}>
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-base">Scan your SRT files</CardTitle>
|
||||
<CardDescription className="text-[13px]">
|
||||
Scan folders for subtitles missing plot summaries
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<!-- Scan Panel -->
|
||||
<Card className="bg-card" skeletonFlash={false}>
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-base">Scan Files</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
<div class="relative w-full">
|
||||
<Folder class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary" />
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={directory}
|
||||
placeholder="C:\Movies or /media/movies"
|
||||
disabled={scanning}
|
||||
className="h-11 font-mono text-[13px] w-full pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
on:click={handleScan}
|
||||
disabled={scanning || !directory}
|
||||
size="lg"
|
||||
className="h-11 px-6 whitespace-nowrap w-full sm:w-auto"
|
||||
>
|
||||
<Scan class="h-4 w-4" />
|
||||
{scanning ? "Scanning..." : "Scan Directory"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<span class="text-[13px] text-text-secondary whitespace-nowrap inline-flex items-center gap-2">
|
||||
<Plug class="h-4 w-4 text-text-tertiary" />
|
||||
Metadata Source
|
||||
</span>
|
||||
|
||||
<Combobox
|
||||
items={metadataOptions}
|
||||
value={metadataProvider}
|
||||
disabled={scanning || activeMetadataOptions.length === 0}
|
||||
placeholder={
|
||||
activeMetadataOptions.length === 0
|
||||
? "Enable a provider"
|
||||
: "Select source"
|
||||
}
|
||||
className="w-full sm:min-w-[220px]"
|
||||
on:change={(event) => selectMetadataSource(event.detail.value)}
|
||||
>
|
||||
<Plug slot="icon" class="h-4 w-4 text-text-tertiary" />
|
||||
</Combobox>
|
||||
|
||||
<span class="text-[11px] text-text-tertiary sm:ml-2">
|
||||
{activeMetadataOptions.length === 0
|
||||
? "Enable an integration in Settings to select a source."
|
||||
: "Choose which API to use for fetching plot summaries"}
|
||||
</span>
|
||||
</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 scanning}
|
||||
<div
|
||||
class="px-6 py-5 bg-blue-500/5 border border-blue-500/20 rounded-xl"
|
||||
>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-blue-500/40 border-t-blue-500 rounded-full animate-spin"
|
||||
></div>
|
||||
<span class="text-[13px] font-medium text-blue-300"
|
||||
>{scanProgress.message}</span
|
||||
>
|
||||
</div>
|
||||
<Button
|
||||
on:click={cancelScan}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-red-500/60 text-red-400 hover:bg-red-500/10 w-full sm:w-auto"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<CardContent className="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
<div class="relative w-full">
|
||||
<Folder
|
||||
class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
bind:value={directory}
|
||||
placeholder="C:\Movies or /media/movies"
|
||||
disabled={scanning}
|
||||
className="h-11 font-mono text-[13px] w-full pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
on:click={handleScan}
|
||||
disabled={scanning || !directory}
|
||||
size="lg"
|
||||
className="h-11 px-6 whitespace-nowrap w-full sm:w-auto"
|
||||
>
|
||||
<Scan class="h-4 w-4" />
|
||||
{scanning ? "Scanning..." : "Scan Directory"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if scanProgress.filesFound > 0}
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 text-[12px] text-text-secondary"
|
||||
>
|
||||
<span
|
||||
>Files found: <span class="text-white font-medium"
|
||||
>{scanProgress.filesFound}</span
|
||||
></span
|
||||
>
|
||||
<span class="text-text-tertiary">Scanning in progress...</span>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<span
|
||||
class="text-[13px] text-text-secondary whitespace-nowrap inline-flex items-center gap-2"
|
||||
>
|
||||
<Plug class="h-4 w-4 text-text-tertiary" />
|
||||
Metadata Source
|
||||
</span>
|
||||
|
||||
<Combobox
|
||||
items={metadataOptions}
|
||||
value={metadataProvider}
|
||||
disabled={scanning || activeMetadataOptions.length === 0}
|
||||
placeholder={activeMetadataOptions.length === 0
|
||||
? "Enable a provider"
|
||||
: "Select source"}
|
||||
className="w-full sm:min-w-[220px]"
|
||||
on:change={(event) => selectMetadataSource(event.detail.value)}
|
||||
>
|
||||
<Plug slot="icon" class="h-4 w-4 text-text-tertiary" />
|
||||
</Combobox>
|
||||
|
||||
<span class="text-[11px] text-text-tertiary sm:ml-2">
|
||||
{cleanOnly
|
||||
? "Clean-only mode skips metadata lookups."
|
||||
: activeMetadataOptions.length === 0
|
||||
? "Enable an integration in Settings to select a source."
|
||||
: "Choose which API to use for fetching plot summaries"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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">Run as "Clean-only scan"</div>
|
||||
<div class="text-[11px] text-text-tertiary">
|
||||
Remove watermark lines (YTS, YIFY, etc) without adding plots.
|
||||
</div>
|
||||
{#if scanProgress.startedAt}
|
||||
<div class="mt-2 text-[11px] text-text-tertiary">
|
||||
Started at {scanProgress.startedAt.toLocaleTimeString()}
|
||||
{#if scanProgress.estimatedFinishAt}
|
||||
· Estimated finish {scanProgress.estimatedFinishAt.toLocaleTimeString()}
|
||||
{/if}
|
||||
</div>
|
||||
<span class="relative inline-flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={cleanOnly}
|
||||
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>
|
||||
|
||||
{#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 scanning}
|
||||
<div
|
||||
class="px-6 py-5 bg-blue-500/5 border border-blue-500/20 rounded-xl"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 mb-3"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="w-4 h-4 border-2 border-blue-500/40 border-t-blue-500 rounded-full animate-spin"
|
||||
></div>
|
||||
<span class="text-[13px] font-medium text-blue-300"
|
||||
>{scanProgress.message}</span
|
||||
>
|
||||
</div>
|
||||
<Button
|
||||
on:click={cancelScan}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-red-500/60 text-red-400 hover:bg-red-500/10 w-full sm:w-auto"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if scanProgress.filesFound > 0}
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 text-[12px] text-text-secondary"
|
||||
>
|
||||
<span
|
||||
>Files found: <span class="text-white font-medium"
|
||||
>{scanProgress.filesFound}</span
|
||||
></span
|
||||
>
|
||||
<span class="text-text-tertiary">Scanning in progress...</span>
|
||||
</div>
|
||||
{#if scanProgress.startedAt}
|
||||
<div class="mt-2 text-[11px] text-text-tertiary">
|
||||
Started at {scanProgress.startedAt.toLocaleTimeString()}
|
||||
{#if scanProgress.estimatedFinishAt}
|
||||
· Estimated finish {scanProgress.estimatedFinishAt.toLocaleTimeString()}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div
|
||||
class="mt-3 h-1.5 bg-bg-secondary rounded-full overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-full bg-blue-500 rounded-full animate-pulse"
|
||||
style="width: 100%"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Progress bar -->
|
||||
<div
|
||||
class="mt-3 h-1.5 bg-bg-secondary rounded-full overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-full bg-blue-500 rounded-full animate-pulse"
|
||||
style="width: 100%"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Save Directory Prompt -->
|
||||
{#if showSaveDirectoryPrompt}
|
||||
<div class="bg-blue-500/5 border border-blue-500/20 rounded-2xl p-6">
|
||||
<div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Info class="w-5 h-5 text-blue-400" />
|
||||
<h3 class="text-[13px] font-medium text-blue-300">
|
||||
Save as Default Directory?
|
||||
</h3>
|
||||
</div>
|
||||
<p class="text-[11px] text-text-secondary">
|
||||
Would you like to make <span class="font-mono text-white"
|
||||
>{directory}</span
|
||||
> as your default scan directory?
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 w-full sm:w-auto">
|
||||
<Button
|
||||
on:click={dismissDirectoryPrompt}
|
||||
disabled={savingDirectory}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-text-secondary w-full sm:w-auto"
|
||||
{/if}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Save Directory Prompt -->
|
||||
{#if showSaveDirectoryPrompt}
|
||||
<div class="bg-blue-500/5 border border-blue-500/20 rounded-2xl p-6">
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<Info class="w-5 h-5 text-blue-400" />
|
||||
<h3 class="text-[13px] font-medium text-blue-300">
|
||||
Save as Default Directory?
|
||||
</h3>
|
||||
</div>
|
||||
<p class="text-[11px] text-text-secondary">
|
||||
Would you like to make <span class="font-mono text-white"
|
||||
>{directory}</span
|
||||
> as your default scan directory?
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 w-full sm:w-auto"
|
||||
>
|
||||
Not now
|
||||
</Button>
|
||||
<Button
|
||||
on:click={saveAsDefaultDirectory}
|
||||
disabled={savingDirectory}
|
||||
size="sm"
|
||||
className="bg-blue-500 text-white hover:bg-blue-600 w-full sm:w-auto"
|
||||
>
|
||||
{savingDirectory ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
<Button
|
||||
on:click={dismissDirectoryPrompt}
|
||||
disabled={savingDirectory}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-text-secondary w-full sm:w-auto"
|
||||
>
|
||||
Not now
|
||||
</Button>
|
||||
<Button
|
||||
on:click={saveAsDefaultDirectory}
|
||||
disabled={savingDirectory}
|
||||
size="sm"
|
||||
className="bg-blue-500 text-white hover:bg-blue-600 w-full sm:w-auto"
|
||||
>
|
||||
{savingDirectory ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Results Area -->
|
||||
{#if hasScanned}
|
||||
<div class="space-y-6">
|
||||
{#if files.length > 0}
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<h3 class="text-[13px] font-medium text-text-secondary">
|
||||
{files.length}
|
||||
{files.length !== 1 ? "files" : "file"} found
|
||||
</h3>
|
||||
<span class="text-[13px] text-text-tertiary">
|
||||
{selectedFilePaths.length} selected
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
on:click={clearResults}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-red-500 text-red-400 hover:bg-red-500/10 hover:text-red-300 w-full sm:w-auto"
|
||||
<!-- Results Area -->
|
||||
{#if hasScanned}
|
||||
<div class="space-y-6">
|
||||
{#if files.length > 0}
|
||||
<div
|
||||
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3"
|
||||
>
|
||||
Clear Results
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<ResultsList
|
||||
bind:this={resultsListRef}
|
||||
{files}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
disabled={!apiConfigured || processing}
|
||||
{metadataProvider}
|
||||
{metadataLanguage}
|
||||
activeIntegrations={{
|
||||
omdb: omdbEnabled,
|
||||
tmdb: tmdbEnabled,
|
||||
tvmaze: tvmazeEnabled,
|
||||
wikipedia: wikipediaEnabled,
|
||||
}}
|
||||
loading={processing}
|
||||
on:processSingle={handleProcessSingle}
|
||||
on:processBulk={handleProcessBulk}
|
||||
on:metadataSourceChange={handleMetadataSourceChange}
|
||||
/>
|
||||
{:else}
|
||||
<div class="border border-border rounded-2xl p-12 text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<FileText class="w-12 h-12 text-text-tertiary" />
|
||||
<div>
|
||||
<p class="text-[13px] text-text-secondary mb-1">
|
||||
No subtitle files found
|
||||
</p>
|
||||
<p class="text-[11px] text-text-tertiary">
|
||||
Try scanning a different directory
|
||||
</p>
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
<h3 class="text-[13px] font-medium text-text-secondary">
|
||||
{files.length}
|
||||
{files.length !== 1 ? "files" : "file"} found
|
||||
</h3>
|
||||
<span class="text-[13px] text-text-tertiary">
|
||||
{selectedFilePaths.length} selected
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
on:click={clearResults}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-text-secondary"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if !scanning && showTutorial}
|
||||
<!-- First-time tutorial -->
|
||||
<div class="rounded-2xl border border-border bg-gradient-to-br from-[#101010] via-[#0c0c0c] to-[#0b0b0b] p-6 sm:p-8">
|
||||
<div class="flex flex-col gap-6 lg:flex-row lg:items-start">
|
||||
<div class="flex-1 space-y-4">
|
||||
<div class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[10px] uppercase tracking-[0.2em] text-text-tertiary">
|
||||
First time setup
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-lg font-semibold text-white">
|
||||
Enrich your subtitle library in minutes
|
||||
</h3>
|
||||
<p class="text-[13px] text-text-secondary">
|
||||
Follow the steps below to connect your metadata source and scan a directory.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 px-4 py-3">
|
||||
<TypewriterQuote style={quoteStyle} />
|
||||
<p class="text-[11px] text-text-tertiary mt-1">
|
||||
Enter a directory path above to begin.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button
|
||||
on:click={() => onOpenSettings && onOpenSettings()}
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-white/90"
|
||||
>
|
||||
Open Settings
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => onOpenHistory && onOpenHistory()}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-white/20 text-text-secondary hover:bg-white/10"
|
||||
className="border-red-500 text-red-400 hover:bg-red-500/10 hover:text-red-300 w-full sm:w-auto"
|
||||
>
|
||||
View History
|
||||
Clear Results
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-1 lg:max-w-sm w-full">
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-9 w-9 rounded-full bg-white text-black flex items-center justify-center text-xs font-semibold">
|
||||
1
|
||||
</div>
|
||||
<ResultsList
|
||||
bind:this={resultsListRef}
|
||||
{files}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
disabled={(!apiConfigured && !cleanOnly) || processing}
|
||||
{metadataProvider}
|
||||
{metadataLanguage}
|
||||
cleanMode={cleanOnly}
|
||||
activeIntegrations={{
|
||||
omdb: omdbEnabled,
|
||||
tmdb: tmdbEnabled,
|
||||
tvmaze: tvmazeEnabled,
|
||||
wikipedia: wikipediaEnabled,
|
||||
}}
|
||||
loading={processing}
|
||||
on:processSingle={handleProcessSingle}
|
||||
on:processBulk={handleProcessBulk}
|
||||
on:metadataSourceChange={handleMetadataSourceChange}
|
||||
/>
|
||||
{:else}
|
||||
<div class="border border-border rounded-2xl p-12 text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<FileText class="w-12 h-12 text-text-tertiary" />
|
||||
<div>
|
||||
<p class="text-[13px] font-medium text-white">Connect metadata</p>
|
||||
<p class="text-[13px] text-text-secondary mb-1">
|
||||
No subtitle files found
|
||||
</p>
|
||||
<p class="text-[11px] text-text-tertiary">
|
||||
Add an API key or enable TVmaze in Settings.
|
||||
Try scanning a different directory
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
on:click={clearResults}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-text-secondary"
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if !scanning && showTutorial}
|
||||
<!-- First-time tutorial -->
|
||||
<div
|
||||
class="rounded-2xl border border-border bg-gradient-to-br from-[#101010] via-[#0c0c0c] to-[#0b0b0b] p-6 sm:p-8"
|
||||
>
|
||||
<div class="flex flex-col gap-6 lg:flex-row lg:items-start">
|
||||
<div class="flex-1 space-y-4">
|
||||
<div
|
||||
class="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[10px] uppercase tracking-[0.2em] text-text-tertiary"
|
||||
>
|
||||
First time setup
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-lg font-semibold text-white">
|
||||
Enrich your subtitle library in minutes
|
||||
</h3>
|
||||
<p class="text-[13px] text-text-secondary">
|
||||
Follow the steps below to connect your metadata source and scan
|
||||
a directory.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 px-4 py-3">
|
||||
<TypewriterQuote style={quoteStyle} />
|
||||
<p class="text-[11px] text-text-tertiary mt-1">
|
||||
Enter a directory path above to begin.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<Button
|
||||
on:click={() => onOpenSettings && onOpenSettings()}
|
||||
size="sm"
|
||||
className="bg-white text-black hover:bg-white/90"
|
||||
>
|
||||
Open Settings
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => onOpenHistory && onOpenHistory()}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-white/20 text-text-secondary hover:bg-white/10"
|
||||
>
|
||||
View History
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-9 w-9 rounded-full border border-white/20 text-white flex items-center justify-center text-xs font-semibold">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[13px] font-medium text-white">Choose a source</p>
|
||||
<p class="text-[11px] text-text-tertiary">
|
||||
Pick OMDb, TMDb, TVmaze, or both for the best match rate.
|
||||
</p>
|
||||
<div
|
||||
class="grid gap-3 sm:grid-cols-2 lg:grid-cols-1 lg:max-w-sm w-full"
|
||||
>
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="h-9 w-9 rounded-full bg-white text-black flex items-center justify-center text-xs font-semibold"
|
||||
>
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[13px] font-medium text-white">
|
||||
Connect metadata
|
||||
</p>
|
||||
<p class="text-[11px] text-text-tertiary">
|
||||
Add an API key or enable TVmaze in Settings.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-9 w-9 rounded-full border border-white/20 text-white flex items-center justify-center text-xs font-semibold">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[13px] font-medium text-white">Scan & enrich</p>
|
||||
<p class="text-[11px] text-text-tertiary">
|
||||
Scan a folder and apply summaries to selected subtitles.
|
||||
</p>
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="h-9 w-9 rounded-full border border-white/20 text-white flex items-center justify-center text-xs font-semibold"
|
||||
>
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[13px] font-medium text-white">
|
||||
Choose a source
|
||||
</p>
|
||||
<p class="text-[11px] text-text-tertiary">
|
||||
Pick OMDb, TMDb, TVmaze, or both for the best match rate.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<div class="text-[10px] uppercase tracking-[0.2em] text-text-tertiary mb-3">
|
||||
Progress
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="h-9 w-9 rounded-full border border-white/20 text-white flex items-center justify-center text-xs font-semibold"
|
||||
>
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[13px] font-medium text-white">
|
||||
Scan, clean, enrich
|
||||
</p>
|
||||
<p class="text-[11px] text-text-tertiary">
|
||||
Scan a folder to clean subtitles and add plot summaries.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2 text-[12px] text-text-secondary">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>API key connected</span>
|
||||
{#if apiConfigured}
|
||||
<span class="text-green-400">Done</span>
|
||||
{:else}
|
||||
<span class="text-text-tertiary">Pending</span>
|
||||
{/if}
|
||||
|
||||
<div class="rounded-xl border border-white/10 bg-white/5 p-4">
|
||||
<div
|
||||
class="text-[10px] uppercase tracking-[0.2em] text-text-tertiary mb-3"
|
||||
>
|
||||
Progress
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Metadata source selected</span>
|
||||
{#if metadataSelected}
|
||||
<span class="text-green-400">Done</span>
|
||||
{:else}
|
||||
<span class="text-text-tertiary">Pending</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>First scan completed</span>
|
||||
{#if hasScanned}
|
||||
<span class="text-green-400">Done</span>
|
||||
{:else}
|
||||
<span class="text-text-tertiary">Pending</span>
|
||||
{/if}
|
||||
<div class="space-y-2 text-[12px] text-text-secondary">
|
||||
<div class="flex items-center justify-between">
|
||||
<span>API key connected</span>
|
||||
{#if apiConfigured}
|
||||
<span class="text-green-400">Done</span>
|
||||
{:else}
|
||||
<span class="text-text-tertiary">Pending</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>Metadata source selected</span>
|
||||
{#if metadataSelected}
|
||||
<span class="text-green-400">Done</span>
|
||||
{:else}
|
||||
<span class="text-text-tertiary">Pending</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span>First scan completed</span>
|
||||
{#if hasScanned}
|
||||
<span class="text-green-400">Done</span>
|
||||
{:else}
|
||||
<span class="text-text-tertiary">Pending</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if !scanning}
|
||||
<!-- Waiting State -->
|
||||
<div class="border border-border rounded-2xl p-12 text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="relative">
|
||||
<Scan class="w-16 h-16 text-text-tertiary animate-pulse" />
|
||||
</div>
|
||||
<div>
|
||||
<TypewriterQuote style={quoteStyle} />
|
||||
<p class="text-[11px] text-text-tertiary">
|
||||
Enter a directory path above to begin
|
||||
</p>
|
||||
{:else if !scanning}
|
||||
<!-- Waiting State -->
|
||||
<div class="border border-border rounded-2xl p-12 text-center">
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="relative">
|
||||
<Scan class="w-16 h-16 text-text-tertiary animate-pulse" />
|
||||
</div>
|
||||
<div>
|
||||
<TypewriterQuote style={quoteStyle} />
|
||||
<p class="text-[11px] text-text-tertiary">
|
||||
Enter a directory path above to begin
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
let stripKeywords = settings.strip_keywords !== false;
|
||||
let cleanSubtitleContent = settings.clean_subtitle_content !== false;
|
||||
let forceRemoveKeywords = Array.isArray(settings.clean_subtitle_force_remove)
|
||||
? settings.clean_subtitle_force_remove.join(", ")
|
||||
: settings.clean_subtitle_force_remove || "YTS, OpenSubtitles";
|
||||
|
||||
// Example filenames to demonstrate the cleaning
|
||||
const filenameExamples = [
|
||||
@@ -53,6 +56,10 @@
|
||||
onSave({
|
||||
strip_keywords: stripKeywords,
|
||||
clean_subtitle_content: cleanSubtitleContent,
|
||||
clean_subtitle_force_remove: forceRemoveKeywords
|
||||
.split(/[\n,]+/)
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean),
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -165,6 +172,22 @@
|
||||
<!-- Collapsible Details -->
|
||||
{#if cleanSubtitleContent}
|
||||
<div class="space-y-4 pl-2 border-l-2 border-border ml-2">
|
||||
<div class="bg-bg-card border border-border rounded-xl p-4 space-y-3">
|
||||
<div class="text-[11px] text-text-tertiary uppercase tracking-wide">
|
||||
Force Remove Keywords
|
||||
</div>
|
||||
<textarea
|
||||
rows="3"
|
||||
bind:value={forceRemoveKeywords}
|
||||
class="w-full resize-none rounded-lg border border-white/10 bg-bg-primary/60 px-3 py-2 text-[12px] text-text-secondary placeholder:text-text-tertiary focus:border-accent focus:outline-none"
|
||||
placeholder="YTS, OpenSubtitles"
|
||||
></textarea>
|
||||
<p class="text-[11px] text-text-tertiary">
|
||||
Any subtitle block containing one of these strings (partial match)
|
||||
is removed entirely. Separate values with commas or new lines.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
|
||||
Reference in New Issue
Block a user