mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
refactor: move arr header buttons into action bar, improve breadcrumbing
This commit is contained in:
@@ -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
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
21
src/routes/arr/[id]/+layout.server.ts
Normal file
21
src/routes/arr/[id]/+layout.server.ts
Normal 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
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
Reference in New Issue
Block a user