refactor: move arr header buttons into action bar, improve breadcrumbing

This commit is contained in:
Sam Chau
2026-01-16 20:35:29 +10:30
parent 2efb006482
commit 643ba8ce00
9 changed files with 212 additions and 232 deletions

View File

@@ -8,6 +8,9 @@
export let hasDropdown: boolean = false; export let hasDropdown: boolean = false;
export let dropdownPosition: 'left' | 'right' | 'middle' = 'left'; export let dropdownPosition: 'left' | 'right' | 'middle' = 'left';
export let disabled: boolean = false; export let disabled: boolean = false;
export let title: string = '';
export let type: 'button' | 'submit' = 'button';
export let variant: 'neutral' | 'danger' = 'neutral';
let isHovered = false; let isHovered = false;
let leaveTimer: ReturnType<typeof setTimeout> | null = null; let leaveTimer: ReturnType<typeof setTimeout> | null = null;
@@ -26,6 +29,11 @@
isHovered = false; isHovered = false;
}, 100); }, 100);
} }
const variantClasses = {
neutral: 'hover:bg-neutral-100 dark:hover:bg-neutral-700',
danger: 'hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400'
};
</script> </script>
<div <div
@@ -35,11 +43,11 @@
role="group" role="group"
> >
<button <button
{type}
{title}
class="flex items-center justify-center border border-neutral-200 bg-white transition-colors dark:border-neutral-700 dark:bg-neutral-800 {square class="flex items-center justify-center border border-neutral-200 bg-white transition-colors dark:border-neutral-700 dark:bg-neutral-800 {square
? 'h-10 w-10' ? 'h-10 w-10'
: 'h-10 px-4'} {disabled : 'h-10 px-4'} {disabled ? 'cursor-not-allowed opacity-50' : variantClasses[variant]}"
? 'cursor-not-allowed opacity-50'
: 'hover:bg-neutral-100 dark:hover:bg-neutral-700'}"
{disabled} {disabled}
on:click on:click
> >

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { ComponentType } from 'svelte'; import type { ComponentType } from 'svelte';
import { ArrowLeft } from 'lucide-svelte'; import { ArrowLeft, ChevronRight } from 'lucide-svelte';
interface Tab { interface Tab {
label: string; label: string;
@@ -14,8 +14,17 @@
href: string; href: string;
} }
interface Breadcrumb {
parent: {
label: string;
href: string;
};
current: string;
}
export let tabs: Tab[] = []; export let tabs: Tab[] = [];
export let backButton: BackButton | undefined = undefined; export let backButton: BackButton | undefined = undefined;
export let breadcrumb: Breadcrumb | undefined = undefined;
</script> </script>
<div class="border-b border-neutral-200 dark:border-neutral-800"> <div class="border-b border-neutral-200 dark:border-neutral-800">
@@ -40,7 +49,18 @@
<slot name="actions" /> <slot name="actions" />
</div> </div>
{#if backButton} {#if breadcrumb}
<div class="flex items-center gap-2 text-sm">
<a
href={breadcrumb.parent.href}
class="text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200 transition-colors"
>
{breadcrumb.parent.label}
</a>
<ChevronRight size={14} class="text-neutral-400 dark:text-neutral-600" />
<span class="font-medium text-neutral-900 dark:text-neutral-50">{breadcrumb.current}</span>
</div>
{:else if backButton}
<a <a
href={backButton.href} href={backButton.href}
class="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-neutral-600 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100" class="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-neutral-600 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"

View File

@@ -5,6 +5,7 @@
export let title: string; export let title: string;
export let variant: 'neutral' | 'danger' | 'accent' = 'neutral'; export let variant: 'neutral' | 'danger' | 'accent' = 'neutral';
export let size: 'sm' | 'md' = 'md'; export let size: 'sm' | 'md' = 'md';
export let type: 'button' | 'submit' = 'button';
const sizeClasses = { const sizeClasses = {
sm: 'h-6 w-6', sm: 'h-6 w-6',
@@ -27,7 +28,7 @@
</script> </script>
<button <button
type="button" {type}
on:click on:click
class="inline-flex items-center justify-center rounded border transition-colors {sizeClasses[size]} {variantClasses[variant]}" class="inline-flex items-center justify-center rounded border transition-colors {sizeClasses[size]} {variantClasses[variant]}"
{title} {title}

View File

@@ -0,0 +1,21 @@
import { error } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
import { arrInstancesQueries } from '$db/queries/arrInstances.ts';
export const load: LayoutServerLoad = ({ params }) => {
const id = parseInt(params.id || '', 10);
if (isNaN(id)) {
error(404, `Invalid instance ID: ${params.id}`);
}
const instance = arrInstancesQueries.getById(id);
if (!instance) {
error(404, `Instance not found: ${id}`);
}
return {
instance
};
};

View File

@@ -2,6 +2,9 @@
import Tabs from '$ui/navigation/tabs/Tabs.svelte'; import Tabs from '$ui/navigation/tabs/Tabs.svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { Library, RefreshCw, ArrowUpCircle, ScrollText } from 'lucide-svelte'; import { Library, RefreshCw, ArrowUpCircle, ScrollText } from 'lucide-svelte';
import type { LayoutData } from './$types';
export let data: LayoutData;
$: instanceId = $page.params.id; $: instanceId = $page.params.id;
$: currentPath = $page.url.pathname; $: currentPath = $page.url.pathname;
@@ -33,13 +36,16 @@
} }
]; ];
$: backButton = { $: breadcrumb = {
label: 'Back', parent: {
href: '/arr' label: 'Instances',
href: '/arr'
},
current: data.instance.name
}; };
</script> </script>
<div class="p-8"> <div class="p-8">
<Tabs {tabs} {backButton} /> <Tabs {tabs} {breadcrumb} />
<slot /> <slot />
</div> </div>

View File

@@ -1,26 +1,8 @@
import { error, redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import type { ServerLoad, Actions } from '@sveltejs/kit'; import type { Actions } from '@sveltejs/kit';
import { arrInstancesQueries } from '$db/queries/arrInstances.ts'; import { arrInstancesQueries } from '$db/queries/arrInstances.ts';
import { cache } from '$cache/cache.ts'; import { cache } from '$cache/cache.ts';
export const load: ServerLoad = async ({ params }) => {
const id = parseInt(params.id || '', 10);
if (isNaN(id)) {
error(404, `Invalid instance ID: ${params.id}`);
}
const instance = arrInstancesQueries.getById(id);
if (!instance) {
error(404, `Instance not found: ${id}`);
}
return {
instance
};
};
export const actions: Actions = { export const actions: Actions = {
delete: ({ params }) => { delete: ({ params }) => {
const id = parseInt(params.id || '', 10); const id = parseInt(params.id || '', 10);

View File

@@ -3,6 +3,7 @@
import { AlertTriangle, Film } from 'lucide-svelte'; import { AlertTriangle, Film } from 'lucide-svelte';
import { alertStore } from '$alerts/store'; import { alertStore } from '$alerts/store';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { goto, invalidateAll } from '$app/navigation';
import ExpandableTable from '$ui/table/ExpandableTable.svelte'; import ExpandableTable from '$ui/table/ExpandableTable.svelte';
import type { Column, SortState } from '$ui/table/types'; import type { Column, SortState } from '$ui/table/types';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -10,10 +11,10 @@
import { createSearchStore } from '$stores/search'; import { createSearchStore } from '$stores/search';
import { libraryCache } from '$stores/libraryCache'; import { libraryCache } from '$stores/libraryCache';
import LibraryHeader from './components/LibraryHeader.svelte';
import LibraryActionBar from './components/LibraryActionBar.svelte'; import LibraryActionBar from './components/LibraryActionBar.svelte';
import MovieRow from './components/MovieRow.svelte'; import MovieRow from './components/MovieRow.svelte';
import MovieRowSkeleton from './components/MovieRowSkeleton.svelte'; import MovieRowSkeleton from './components/MovieRowSkeleton.svelte';
import InfoModal from '$ui/modal/InfoModal.svelte';
export let data: PageData; export let data: PageData;
@@ -77,6 +78,32 @@
await fetchLibrary(true); await fetchLibrary(true);
} }
function handleOpen() {
const baseUrl = data.instance.url.replace(/\/$/, '');
window.open(baseUrl, '_blank', 'noopener,noreferrer');
}
function handleEdit() {
goto(`/arr/${data.instance.id}/edit`);
}
async function handleDelete() {
if (!confirm('Are you sure you want to delete this instance?')) return;
const response = await fetch(`/api/arr/${data.instance.id}`, { method: 'DELETE' });
if (response.ok) {
goto('/arr');
} else {
alertStore.add('error', 'Failed to delete instance');
}
}
let infoModalOpen = false;
function handleInfo() {
infoModalOpen = true;
}
let currentInstanceId: number | null = null; let currentInstanceId: number | null = null;
onMount(() => { onMount(() => {
@@ -187,11 +214,6 @@
}); });
} }
function handleChangeProfile(databaseName: string, profileName: string) {
const count = moviesWithFiles.length;
alertStore.add('success', `Changing ${count} movies to "${profileName}" from ${databaseName}`);
}
// ========================================================================== // ==========================================================================
// Data & Columns // Data & Columns
// ========================================================================== // ==========================================================================
@@ -277,32 +299,23 @@
</div> </div>
</div> </div>
{:else} {:else}
<LibraryHeader <LibraryActionBar
instance={data.instance} {searchStore}
{library} visibleColumns={new Set([...visibleColumns])}
{allMoviesWithFiles} toggleableColumns={TOGGLEABLE_COLUMNS}
{refreshing} {columnLabels}
{loading} {activeFilters}
uniqueQualities={loading ? [] : uniqueQualities}
uniqueProfiles={loading ? [] : uniqueProfiles}
onToggleColumn={toggleColumn}
onToggleFilter={toggleFilter}
onRefresh={handleRefresh} onRefresh={handleRefresh}
onOpen={handleOpen}
onEdit={handleEdit}
onDelete={handleDelete}
onInfo={handleInfo}
/> />
{#if !loading}
<LibraryActionBar
{searchStore}
visibleColumns={new Set([...visibleColumns])}
toggleableColumns={TOGGLEABLE_COLUMNS}
{columnLabels}
{activeFilters}
{uniqueQualities}
{uniqueProfiles}
{profilesByDatabase}
filteredCount={moviesWithFiles.length}
onToggleColumn={toggleColumn}
onToggleFilter={toggleFilter}
onChangeProfile={handleChangeProfile}
/>
{/if}
{#if allMoviesWithFiles.length === 0 && !loading} {#if allMoviesWithFiles.length === 0 && !loading}
<div class="rounded-lg border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900"> <div class="rounded-lg border border-neutral-200 bg-white p-6 dark:border-neutral-800 dark:bg-neutral-900">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
@@ -343,3 +356,9 @@
{/if} {/if}
{/if} {/if}
</div> </div>
<InfoModal bind:open={infoModalOpen} header="Library">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
Placeholder content. More information coming soon.
</p>
</InfoModal>

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Check, Columns, Filter, FolderSync, Database } from 'lucide-svelte'; import { Check, SlidersHorizontal, TableProperties, RefreshCw, ExternalLink, Pencil, Trash2, Info } from 'lucide-svelte';
import ActionsBar from '$ui/actions/ActionsBar.svelte'; import ActionsBar from '$ui/actions/ActionsBar.svelte';
import SearchAction from '$ui/actions/SearchAction.svelte'; import SearchAction from '$ui/actions/SearchAction.svelte';
import ActionButton from '$ui/actions/ActionButton.svelte'; import ActionButton from '$ui/actions/ActionButton.svelte';
@@ -17,12 +17,6 @@
label: string; label: string;
} }
interface DatabaseProfiles {
databaseId: number;
databaseName: string;
profiles: string[];
}
export let searchStore: SearchStore; export let searchStore: SearchStore;
export let visibleColumns: Set<string>; export let visibleColumns: Set<string>;
export let toggleableColumns: readonly string[]; export let toggleableColumns: readonly string[];
@@ -30,42 +24,24 @@
export let activeFilters: ActiveFilter[]; export let activeFilters: ActiveFilter[];
export let uniqueQualities: string[]; export let uniqueQualities: string[];
export let uniqueProfiles: string[]; export let uniqueProfiles: string[];
export let profilesByDatabase: DatabaseProfiles[];
export let filteredCount: number;
export let onToggleColumn: (key: string) => void; export let onToggleColumn: (key: string) => void;
export let onToggleFilter: (field: FilterField, operator: FilterOperator, value: string | number | boolean, label: string) => void; export let onToggleFilter: (field: FilterField, operator: FilterOperator, value: string | number | boolean, label: string) => void;
export let onChangeProfile: (databaseName: string, profileName: string) => void; export let onRefresh: () => void;
export let onOpen: () => void;
export let onEdit: () => void;
export let onDelete: () => void;
export let onInfo: () => void;
</script> </script>
<ActionsBar> <ActionsBar>
<SearchAction {searchStore} placeholder="Search movies..." /> <SearchAction {searchStore} placeholder="Search movies..." />
<ActionButton icon={Columns} hasDropdown={true} dropdownPosition="right"> <ActionButton icon={SlidersHorizontal} hasDropdown={true} dropdownPosition="right">
<svelte:fragment slot="dropdown" let:dropdownPosition let:open> <svelte:fragment slot="dropdown" let:dropdownPosition let:open>
<Dropdown position={dropdownPosition} {open} minWidth="10rem"> <Dropdown position={dropdownPosition} {open} minWidth="16rem">
<div class="py-1"> <div class="px-4 py-3 border-b border-neutral-100 dark:border-neutral-700">
{#each toggleableColumns as colKey} <p class="text-xs text-neutral-500 dark:text-neutral-400">Filter movies by quality or profile</p>
<button
type="button"
on:click={() => onToggleColumn(colKey)}
class="flex w-full items-center justify-between gap-3 px-4 py-2 text-sm transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-700 {visibleColumns.has(colKey) ? 'bg-neutral-50 dark:bg-neutral-700' : ''}"
>
<span class="text-neutral-700 dark:text-neutral-300">{columnLabels[colKey]}</span>
<IconCheckbox
checked={visibleColumns.has(colKey)}
icon={Check}
color="blue"
shape="circle"
/>
</button>
{/each}
</div> </div>
</Dropdown>
</svelte:fragment>
</ActionButton>
<ActionButton icon={Filter} hasDropdown={true} dropdownPosition="right">
<svelte:fragment slot="dropdown" let:dropdownPosition let:open>
<Dropdown position={dropdownPosition} {open} minWidth="14rem">
<div class="max-h-96 overflow-y-auto"> <div class="max-h-96 overflow-y-auto">
<!-- Quality Filter --> <!-- Quality Filter -->
<div class="border-b border-neutral-100 dark:border-neutral-700"> <div class="border-b border-neutral-100 dark:border-neutral-700">
@@ -114,39 +90,95 @@
</Dropdown> </Dropdown>
</svelte:fragment> </svelte:fragment>
</ActionButton> </ActionButton>
<ActionButton icon={FolderSync} hasDropdown={true} dropdownPosition="right" square={false}> <ActionButton icon={TableProperties} hasDropdown={true} dropdownPosition="right">
<span class="ml-2 text-sm text-neutral-700 dark:text-neutral-300">Change Profile</span>
<span class="ml-1 text-xs text-neutral-500 dark:text-neutral-400">({filteredCount})</span>
<svelte:fragment slot="dropdown" let:dropdownPosition let:open> <svelte:fragment slot="dropdown" let:dropdownPosition let:open>
<Dropdown position={dropdownPosition} {open} minWidth="16rem"> <Dropdown position={dropdownPosition} {open} minWidth="14rem">
<div class="max-h-80 overflow-y-auto py-1"> <div class="px-4 py-3 border-b border-neutral-100 dark:border-neutral-700">
{#if profilesByDatabase.length === 0} <p class="text-xs text-neutral-500 dark:text-neutral-400">Toggle visible table columns</p>
<div class="px-4 py-3 text-sm text-neutral-500 dark:text-neutral-400"> </div>
No databases configured <div class="py-1">
</div> {#each toggleableColumns as colKey}
{:else} <button
{#each profilesByDatabase as db} type="button"
<div class="border-b border-neutral-100 dark:border-neutral-700 last:border-b-0"> on:click={() => onToggleColumn(colKey)}
<div class="px-3 py-2 bg-neutral-50 dark:bg-neutral-900 flex items-center gap-2"> class="flex w-full items-center justify-between gap-3 px-4 py-2 text-sm transition-colors hover:bg-neutral-100 dark:hover:bg-neutral-700 {visibleColumns.has(colKey) ? 'bg-neutral-50 dark:bg-neutral-700' : ''}"
<Database size={12} class="text-neutral-400" /> >
<span class="text-xs font-medium text-neutral-500 dark:text-neutral-400 uppercase tracking-wider"> <span class="text-neutral-700 dark:text-neutral-300">{columnLabels[colKey]}</span>
{db.databaseName} <IconCheckbox
</span> checked={visibleColumns.has(colKey)}
</div> icon={Check}
{#each db.profiles as profile} color="blue"
<button shape="circle"
type="button" />
on:click={() => onChangeProfile(db.databaseName, profile)} </button>
class="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700" {/each}
>
{profile}
</button>
{/each}
</div>
{/each}
{/if}
</div> </div>
</Dropdown> </Dropdown>
</svelte:fragment> </svelte:fragment>
</ActionButton> </ActionButton>
<ActionButton icon={Info} hasDropdown={true} dropdownPosition="right">
<svelte:fragment slot="dropdown" let:dropdownPosition let:open>
<Dropdown position={dropdownPosition} {open} minWidth="10rem">
<button
type="button"
on:click={onInfo}
class="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700 rounded-lg"
>
About this page
</button>
</Dropdown>
</svelte:fragment>
</ActionButton>
<ActionButton icon={RefreshCw} hasDropdown={true} dropdownPosition="right">
<svelte:fragment slot="dropdown" let:dropdownPosition let:open>
<Dropdown position={dropdownPosition} {open} minWidth="10rem">
<button
type="button"
on:click={onRefresh}
class="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700 rounded-lg"
>
Refresh library
</button>
</Dropdown>
</svelte:fragment>
</ActionButton>
<ActionButton icon={ExternalLink} hasDropdown={true} dropdownPosition="right">
<svelte:fragment slot="dropdown" let:dropdownPosition let:open>
<Dropdown position={dropdownPosition} {open} minWidth="10rem">
<button
type="button"
on:click={onOpen}
class="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700 rounded-lg"
>
Open in Radarr
</button>
</Dropdown>
</svelte:fragment>
</ActionButton>
<ActionButton icon={Pencil} hasDropdown={true} dropdownPosition="right">
<svelte:fragment slot="dropdown" let:dropdownPosition let:open>
<Dropdown position={dropdownPosition} {open} minWidth="10rem">
<button
type="button"
on:click={onEdit}
class="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700 rounded-lg"
>
Edit instance
</button>
</Dropdown>
</svelte:fragment>
</ActionButton>
<ActionButton icon={Trash2} hasDropdown={true} dropdownPosition="right">
<svelte:fragment slot="dropdown" let:dropdownPosition let:open>
<Dropdown position={dropdownPosition} {open} minWidth="10rem">
<button
type="button"
on:click={onDelete}
class="w-full px-4 py-2 text-left text-sm text-neutral-700 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-700 rounded-lg"
>
Delete instance
</button>
</Dropdown>
</svelte:fragment>
</ActionButton>
</ActionsBar> </ActionsBar>

View File

@@ -1,109 +0,0 @@
<script lang="ts">
import { Film, ExternalLink, RefreshCw, HardDrive, CheckCircle, ArrowUpCircle, Pencil, Trash2 } from 'lucide-svelte';
import { enhance } from '$app/forms';
import type { RadarrLibraryItem } from '$utils/arr/types.ts';
export let instance: { id: number; name: string; type: string; url: string };
export let library: RadarrLibraryItem[];
export let allMoviesWithFiles: RadarrLibraryItem[];
export let refreshing = false;
export let loading = false;
export let onRefresh: () => void;
$: baseUrl = instance.url.replace(/\/$/, '');
</script>
<div class="flex items-center justify-between rounded-lg border border-neutral-200 bg-white px-4 py-3 dark:border-neutral-800 dark:bg-neutral-900">
<div class="flex flex-col gap-2">
<div class="flex items-center gap-3">
<h2 class="text-lg font-semibold text-neutral-900 dark:text-neutral-50">{instance.name}</h2>
<span class="inline-flex items-center rounded-full bg-orange-100 px-2 py-0.5 text-xs font-medium text-orange-800 dark:bg-orange-900/30 dark:text-orange-400 capitalize">
{instance.type}
</span>
<div class="hidden sm:flex items-center gap-2">
{#if loading}
<span class="inline-flex items-center gap-1 rounded-full bg-neutral-100 px-2 py-0.5 text-xs dark:bg-neutral-800">
<Film size={12} class="text-blue-500" />
<span class="h-3 w-12 rounded bg-neutral-200 dark:bg-neutral-700 animate-pulse"></span>
</span>
<span class="inline-flex items-center gap-1 rounded-full bg-neutral-100 px-2 py-0.5 text-xs dark:bg-neutral-800">
<HardDrive size={12} class="text-purple-500" />
<span class="h-3 w-14 rounded bg-neutral-200 dark:bg-neutral-700 animate-pulse"></span>
</span>
<span class="inline-flex items-center gap-1 rounded-full bg-neutral-100 px-2 py-0.5 text-xs dark:bg-neutral-800">
<CheckCircle size={12} class="text-green-500" />
<span class="h-3 w-16 rounded bg-neutral-200 dark:bg-neutral-700 animate-pulse"></span>
</span>
<span class="inline-flex items-center gap-1 rounded-full bg-neutral-100 px-2 py-0.5 text-xs dark:bg-neutral-800">
<ArrowUpCircle size={12} class="text-orange-500" />
<span class="h-3 w-16 rounded bg-neutral-200 dark:bg-neutral-700 animate-pulse"></span>
</span>
{:else}
<span class="inline-flex items-center gap-1 rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-medium text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300" title="Total movies in library">
<Film size={12} class="text-blue-500" />
{library.length} Total
</span>
<span class="inline-flex items-center gap-1 rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-medium text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300" title="Movies with files on disk">
<HardDrive size={12} class="text-purple-500" />
{allMoviesWithFiles.length} On Disk
</span>
<span class="inline-flex items-center gap-1 rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-medium text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300" title="Movies that have met the quality cutoff">
<CheckCircle size={12} class="text-green-500" />
{allMoviesWithFiles.filter((m) => m.cutoffMet).length} Cutoff Met
</span>
<span class="inline-flex items-center gap-1 rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-medium text-neutral-700 dark:bg-neutral-800 dark:text-neutral-300" title="Movies that can still be upgraded">
<ArrowUpCircle size={12} class="text-orange-500" />
{allMoviesWithFiles.filter((m) => !m.cutoffMet).length} Upgradeable
</span>
{/if}
</div>
</div>
<code class="text-xs font-mono text-neutral-500 dark:text-neutral-400">{instance.url}</code>
</div>
<div class="flex items-center gap-2">
<button
type="button"
disabled={refreshing}
on:click={onRefresh}
class="inline-flex items-center gap-2 rounded-lg border border-neutral-200 bg-neutral-50 px-3 py-1.5 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-100 disabled:opacity-50 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700"
>
<RefreshCw size={14} class={refreshing ? 'animate-spin' : ''} />
Refresh
</button>
<a
href={baseUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-2 rounded-lg border border-neutral-200 bg-neutral-50 px-3 py-1.5 text-sm font-medium text-neutral-700 transition-colors hover:bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700"
>
Open Radarr
<ExternalLink size={14} />
</a>
<a
href="/arr/{instance.id}/edit"
class="inline-flex items-center justify-center h-8 w-8 rounded-lg border border-neutral-200 bg-neutral-50 text-neutral-700 transition-colors hover:bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-300 dark:hover:bg-neutral-700"
title="Edit instance"
>
<Pencil size={14} />
</a>
<form
method="POST"
action="?/delete"
use:enhance={({ cancel }) => {
if (!confirm('Are you sure you want to delete this instance?')) {
cancel();
return;
}
return ({ update }) => update();
}}
>
<button
type="submit"
class="inline-flex items-center justify-center h-8 w-8 rounded-lg border border-red-200 bg-red-50 text-red-600 transition-colors hover:bg-red-100 dark:border-red-800 dark:bg-red-900/20 dark:text-red-400 dark:hover:bg-red-900/40"
title="Delete instance"
>
<Trash2 size={14} />
</button>
</form>
</div>
</div>