style(mm): card views for all media management pages

This commit is contained in:
Sam Chau
2026-01-28 23:44:41 +10:30
parent 1e75a4d6c1
commit 8eb57d276c
8 changed files with 256 additions and 18 deletions

View File

@@ -74,7 +74,7 @@
<h3 class="truncate text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{instance.name}
</h3>
<div class="mt-1 flex flex-wrap items-center gap-1">
<div class="mt-1 flex flex-col items-start gap-1">
{#if instance.enabled}
<Badge variant="success">Enabled</Badge>
{:else}
@@ -85,22 +85,22 @@
</div>
</div>
<!-- Action buttons - always visible, mobile-friendly -->
<div class="flex flex-shrink-0 flex-col gap-1">
<!-- Action buttons -->
<div class="flex flex-shrink-0 items-center gap-1">
<a
href={instance.url}
target="_blank"
rel="noopener noreferrer"
on:click={(e) => e.stopPropagation()}
class="flex items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 p-2.5 text-neutral-600 transition-colors hover:bg-neutral-100 active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:active:bg-neutral-600"
class="rounded-md p-1.5 text-neutral-400 transition-colors hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300"
>
<ExternalLink size={18} />
<ExternalLink size={16} />
</a>
<button
on:click={(e) => handleDeleteClick(e, instance)}
class="flex items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 p-2.5 text-neutral-600 transition-colors hover:border-red-200 hover:bg-red-50 hover:text-red-600 active:border-red-300 active:bg-red-100 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-400 dark:hover:border-red-800 dark:hover:bg-red-900/30 dark:hover:text-red-400 dark:active:bg-red-900/50"
class="rounded-md p-1.5 text-neutral-400 transition-colors hover:text-red-500 dark:text-neutral-500 dark:hover:text-red-400"
>
<Trash2 size={18} />
<Trash2 size={16} />
</button>
</div>
</div>

View File

@@ -113,21 +113,21 @@
</div>
<!-- Action buttons -->
<div class="flex flex-shrink-0 flex-col gap-1">
<div class="flex flex-shrink-0 items-center gap-1">
<a
href={database.repository_url}
target="_blank"
rel="noopener noreferrer"
on:click={(e) => e.stopPropagation()}
class="flex items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 p-2.5 text-neutral-600 transition-colors active:bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-300 dark:active:bg-neutral-700"
class="rounded-md p-1.5 text-neutral-400 transition-colors hover:text-neutral-600 dark:text-neutral-500 dark:hover:text-neutral-300"
>
<ExternalLink size={18} />
<ExternalLink size={16} />
</a>
<button
on:click={(e) => handleUnlinkClick(e, database)}
class="flex items-center justify-center rounded-lg border border-neutral-200 bg-neutral-50 p-2.5 text-neutral-600 transition-colors active:border-red-200 active:bg-red-100 active:text-red-600 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-300 dark:active:border-red-800 dark:active:bg-red-900 dark:active:text-red-400"
class="rounded-md p-1.5 text-neutral-400 transition-colors hover:text-red-500 dark:text-neutral-500 dark:hover:text-red-400"
>
<Unlink size={18} />
<Unlink size={16} />
</button>
</div>
</div>

View File

@@ -2,7 +2,9 @@
import ActionsBar from '$ui/actions/ActionsBar.svelte';
import ActionButton from '$ui/actions/ActionButton.svelte';
import SearchAction from '$ui/actions/SearchAction.svelte';
import ViewToggle from '$ui/actions/ViewToggle.svelte';
import TableView from './views/TableView.svelte';
import CardView from './views/CardView.svelte';
import { createDataPageStore } from '$lib/client/stores/dataPage';
import { goto } from '$app/navigation';
import { Plus } from 'lucide-svelte';
@@ -11,7 +13,7 @@
export let data: PageData;
// Initialize data page store
const { search, filtered, setItems } = createDataPageStore(data.mediaSettingsConfigs, {
const { search, view, filtered, setItems } = createDataPageStore(data.mediaSettingsConfigs, {
storageKey: 'mediaSettingsView',
searchKeys: ['name']
});
@@ -27,6 +29,7 @@
icon={Plus}
on:click={() => goto(`/media-management/${data.currentDatabase.id}/media-settings/new`)}
/>
<ViewToggle bind:value={$view} />
</ActionsBar>
<!-- Media Settings Content -->
@@ -45,7 +48,9 @@
>
<p class="text-neutral-600 dark:text-neutral-400">No media settings configs match your search</p>
</div>
{:else}
{:else if $view === 'table'}
<TableView configs={$filtered} databaseId={data.currentDatabase.id} />
{:else}
<CardView configs={$filtered} databaseId={data.currentDatabase.id} />
{/if}
</div>

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import { goto } from '$app/navigation';
import Badge from '$ui/badge/Badge.svelte';
import type { MediaSettingsListItem } from '$shared/pcd/display.ts';
import radarrLogo from '$lib/client/assets/Radarr.svg';
import sonarrLogo from '$lib/client/assets/Sonarr.svg';
export let configs: MediaSettingsListItem[];
export let databaseId: number;
const logos: Record<string, string> = {
radarr: radarrLogo,
sonarr: sonarrLogo
};
const propersRepacksConfig: Record<
string,
{ variant: 'neutral' | 'success' | 'warning'; label: string }
> = {
doNotPrefer: { variant: 'neutral', label: 'Do Not Prefer' },
preferAndUpgrade: { variant: 'success', label: 'Prefer & Upgrade' },
doNotUpgradeAutomatically: { variant: 'warning', label: 'No Auto Upgrade' }
};
let loadedImages: Set<string> = new Set();
function handleImageLoad(name: string) {
loadedImages.add(name);
loadedImages = loadedImages;
}
function handleCardClick(config: MediaSettingsListItem) {
goto(
`/media-management/${databaseId}/media-settings/${config.arr_type}/${encodeURIComponent(config.name)}`
);
}
</script>
<div class="grid grid-cols-1 gap-3">
{#each configs as config}
{@const prConfig = propersRepacksConfig[config.propers_repacks] || {
variant: 'neutral',
label: config.propers_repacks
}}
<div
on:click={() => handleCardClick(config)}
on:keydown={(e) => e.key === 'Enter' && handleCardClick(config)}
role="button"
tabindex="0"
class="group flex cursor-pointer items-center gap-4 rounded-lg border border-neutral-200 bg-white p-3 transition-all hover:border-neutral-300 hover:shadow-md active:bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-900 dark:hover:border-neutral-700 dark:active:bg-neutral-800"
>
<!-- Left: Logo + Name -->
<div class="flex min-w-0 flex-1 items-center gap-3">
<div class="relative h-10 w-10 flex-shrink-0">
{#if !loadedImages.has(config.name)}
<div
class="absolute inset-0 animate-pulse rounded-lg bg-neutral-200 dark:bg-neutral-700"
></div>
{/if}
<img
src={logos[config.arr_type]}
alt="{config.arr_type} logo"
class="h-10 w-10 rounded-lg {loadedImages.has(config.name)
? 'opacity-100'
: 'opacity-0'}"
on:load={() => handleImageLoad(config.name)}
/>
</div>
<div class="min-w-0">
<h3 class="truncate text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{config.name}
</h3>
<div class="mt-1 flex flex-wrap items-center gap-1">
<Badge variant={prConfig.variant}>{prConfig.label}</Badge>
{#if config.enable_media_info}
<Badge variant="success">Media Info</Badge>
{:else}
<Badge variant="neutral">No Media Info</Badge>
{/if}
</div>
</div>
</div>
</div>
{/each}
</div>

View File

@@ -2,7 +2,9 @@
import ActionsBar from '$ui/actions/ActionsBar.svelte';
import ActionButton from '$ui/actions/ActionButton.svelte';
import SearchAction from '$ui/actions/SearchAction.svelte';
import ViewToggle from '$ui/actions/ViewToggle.svelte';
import TableView from './views/TableView.svelte';
import CardView from './views/CardView.svelte';
import { createDataPageStore } from '$lib/client/stores/dataPage';
import { goto } from '$app/navigation';
import { Plus } from 'lucide-svelte';
@@ -11,7 +13,7 @@
export let data: PageData;
// Initialize data page store
const { search, filtered, setItems } = createDataPageStore(data.namingConfigs, {
const { search, view, filtered, setItems } = createDataPageStore(data.namingConfigs, {
storageKey: 'namingSettingsView',
searchKeys: ['name']
});
@@ -27,6 +29,7 @@
icon={Plus}
on:click={() => goto(`/media-management/${data.currentDatabase.id}/naming/new`)}
/>
<ViewToggle bind:value={$view} />
</ActionsBar>
<!-- Naming Configs Content -->
@@ -45,7 +48,9 @@
>
<p class="text-neutral-600 dark:text-neutral-400">No naming configs match your search</p>
</div>
{:else}
{:else if $view === 'table'}
<TableView configs={$filtered} databaseId={data.currentDatabase.id} />
{:else}
<CardView configs={$filtered} databaseId={data.currentDatabase.id} />
{/if}
</div>

View File

@@ -0,0 +1,71 @@
<script lang="ts">
import { goto } from '$app/navigation';
import Badge from '$ui/badge/Badge.svelte';
import type { NamingListItem } from '$shared/pcd/display.ts';
import radarrLogo from '$lib/client/assets/Radarr.svg';
import sonarrLogo from '$lib/client/assets/Sonarr.svg';
export let configs: NamingListItem[];
export let databaseId: number;
const logos: Record<string, string> = {
radarr: radarrLogo,
sonarr: sonarrLogo
};
let loadedImages: Set<string> = new Set();
function handleImageLoad(name: string) {
loadedImages.add(name);
loadedImages = loadedImages;
}
function handleCardClick(config: NamingListItem) {
goto(
`/media-management/${databaseId}/naming/${config.arr_type}/${encodeURIComponent(config.name)}`
);
}
</script>
<div class="grid grid-cols-1 gap-3">
{#each configs as config}
<div
on:click={() => handleCardClick(config)}
on:keydown={(e) => e.key === 'Enter' && handleCardClick(config)}
role="button"
tabindex="0"
class="group flex cursor-pointer items-center gap-4 rounded-lg border border-neutral-200 bg-white p-3 transition-all hover:border-neutral-300 hover:shadow-md active:bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-900 dark:hover:border-neutral-700 dark:active:bg-neutral-800"
>
<!-- Left: Logo + Name -->
<div class="flex min-w-0 flex-1 items-center gap-3">
<div class="relative h-10 w-10 flex-shrink-0">
{#if !loadedImages.has(config.name)}
<div
class="absolute inset-0 animate-pulse rounded-lg bg-neutral-200 dark:bg-neutral-700"
></div>
{/if}
<img
src={logos[config.arr_type]}
alt="{config.arr_type} logo"
class="h-10 w-10 rounded-lg {loadedImages.has(config.name)
? 'opacity-100'
: 'opacity-0'}"
on:load={() => handleImageLoad(config.name)}
/>
</div>
<div class="min-w-0">
<h3 class="truncate text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{config.name}
</h3>
<div class="mt-1">
{#if config.rename}
<Badge variant="success">Rename Enabled</Badge>
{:else}
<Badge variant="neutral">Rename Disabled</Badge>
{/if}
</div>
</div>
</div>
</div>
{/each}
</div>

View File

@@ -2,7 +2,9 @@
import ActionsBar from '$ui/actions/ActionsBar.svelte';
import ActionButton from '$ui/actions/ActionButton.svelte';
import SearchAction from '$ui/actions/SearchAction.svelte';
import ViewToggle from '$ui/actions/ViewToggle.svelte';
import TableView from './views/TableView.svelte';
import CardView from './views/CardView.svelte';
import { createDataPageStore } from '$lib/client/stores/dataPage';
import { goto } from '$app/navigation';
import { Plus } from 'lucide-svelte';
@@ -11,7 +13,7 @@
export let data: PageData;
// Initialize data page store
const { search, filtered, setItems } = createDataPageStore(data.qualityDefinitionsConfigs, {
const { search, view, filtered, setItems } = createDataPageStore(data.qualityDefinitionsConfigs, {
storageKey: 'qualityDefinitionsView',
searchKeys: ['name']
});
@@ -27,6 +29,7 @@
icon={Plus}
on:click={() => goto(`/media-management/${data.currentDatabase.id}/quality-definitions/new`)}
/>
<ViewToggle bind:value={$view} />
</ActionsBar>
<!-- Quality Definitions Content -->
@@ -45,7 +48,9 @@
>
<p class="text-neutral-600 dark:text-neutral-400">No quality definitions configs match your search</p>
</div>
{:else}
{:else if $view === 'table'}
<TableView configs={$filtered} databaseId={data.currentDatabase.id} />
{:else}
<CardView configs={$filtered} databaseId={data.currentDatabase.id} />
{/if}
</div>

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import { goto } from '$app/navigation';
import Badge from '$ui/badge/Badge.svelte';
import type { QualityDefinitionListItem } from '$shared/pcd/display.ts';
import radarrLogo from '$lib/client/assets/Radarr.svg';
import sonarrLogo from '$lib/client/assets/Sonarr.svg';
export let configs: QualityDefinitionListItem[];
export let databaseId: number;
const logos: Record<string, string> = {
radarr: radarrLogo,
sonarr: sonarrLogo
};
let loadedImages: Set<string> = new Set();
function handleImageLoad(name: string) {
loadedImages.add(name);
loadedImages = loadedImages;
}
function handleCardClick(config: QualityDefinitionListItem) {
goto(
`/media-management/${databaseId}/quality-definitions/${config.arr_type}/${encodeURIComponent(config.name)}`
);
}
</script>
<div class="grid grid-cols-1 gap-3">
{#each configs as config}
<div
on:click={() => handleCardClick(config)}
on:keydown={(e) => e.key === 'Enter' && handleCardClick(config)}
role="button"
tabindex="0"
class="group flex cursor-pointer items-center gap-4 rounded-lg border border-neutral-200 bg-white p-3 transition-all hover:border-neutral-300 hover:shadow-md active:bg-neutral-50 dark:border-neutral-800 dark:bg-neutral-900 dark:hover:border-neutral-700 dark:active:bg-neutral-800"
>
<!-- Left: Logo + Name -->
<div class="flex min-w-0 flex-1 items-center gap-3">
<div class="relative h-10 w-10 flex-shrink-0">
{#if !loadedImages.has(config.name)}
<div
class="absolute inset-0 animate-pulse rounded-lg bg-neutral-200 dark:bg-neutral-700"
></div>
{/if}
<img
src={logos[config.arr_type]}
alt="{config.arr_type} logo"
class="h-10 w-10 rounded-lg {loadedImages.has(config.name)
? 'opacity-100'
: 'opacity-0'}"
on:load={() => handleImageLoad(config.name)}
/>
</div>
<div class="min-w-0">
<h3 class="truncate text-sm font-semibold text-neutral-900 dark:text-neutral-100">
{config.name}
</h3>
<div class="mt-1">
<Badge variant="neutral">{config.quality_count} qualities</Badge>
</div>
</div>
</div>
</div>
{/each}
</div>