Add MDI icon picker to all entity types
Some checks failed
Validate / Hassfest (push) Has been cancelled

- Install @mdi/js (~7000 Material Design Icons)
- IconPicker component: dropdown with search, popular icons grid,
  clear option. Stores icon name as string (e.g. "mdiCamera")
- MdiIcon component: renders SVG from icon name
- Backend: add `icon` field to ImmichServer, TelegramBot,
  TrackingConfig, TemplateConfig, NotificationTarget, AlbumTracker
- All 6 entity pages: icon picker next to name input in create/edit
  forms, icon displayed on entity cards

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 18:01:22 +03:00
parent af9bfb7b22
commit 5a0b0b78f6
11 changed files with 182 additions and 24 deletions

View File

@@ -5,6 +5,8 @@
import PageHeader from '$lib/components/PageHeader.svelte';
import Card from '$lib/components/Card.svelte';
import Loading from '$lib/components/Loading.svelte';
import IconPicker from '$lib/components/IconPicker.svelte';
import MdiIcon from '$lib/components/MdiIcon.svelte';
let loaded = $state(false);
let trackers = $state<any[]>([]);
@@ -15,7 +17,7 @@
let editing = $state<number | null>(null);
let albumFilter = $state('');
const defaultForm = () => ({
name: '', server_id: 0, album_ids: [] as string[],
name: '', icon: '', server_id: 0, album_ids: [] as string[],
target_ids: [] as number[], scan_interval: 60,
});
let form = $state(defaultForm());
@@ -30,7 +32,7 @@
function openNew() { form = defaultForm(); editing = null; showForm = true; albums = []; }
async function edit(trk: any) {
form = {
name: trk.name, server_id: trk.server_id, album_ids: [...trk.album_ids],
name: trk.name, icon: trk.icon || '', server_id: trk.server_id, album_ids: [...trk.album_ids],
target_ids: [...trk.target_ids], scan_interval: trk.scan_interval,
};
editing = trk.id; showForm = true;
@@ -74,7 +76,10 @@
<form onsubmit={save} class="space-y-4">
<div>
<label for="trk-name" class="block text-sm font-medium mb-1">{t('trackers.name')}</label>
<input id="trk-name" bind:value={form.name} required placeholder={t('trackers.namePlaceholder')} class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
<div class="flex gap-2">
<IconPicker value={form.icon} onselect={(v) => form.icon = v} />
<input id="trk-name" bind:value={form.name} required placeholder={t('trackers.namePlaceholder')} class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div>
</div>
<div>
<label for="trk-server" class="block text-sm font-medium mb-1">{t('trackers.server')}</label>
@@ -138,6 +143,7 @@
<div class="flex items-center justify-between">
<div>
<div class="flex items-center gap-2">
{#if tracker.icon}<MdiIcon name={tracker.icon} />{/if}
<p class="font-medium">{tracker.name}</p>
<span class="text-xs px-1.5 py-0.5 rounded {tracker.enabled ? 'bg-[var(--color-success-bg)] text-[var(--color-success-fg)]' : 'bg-[var(--color-muted)] text-[var(--color-muted-foreground)]'}">
{tracker.enabled ? t('trackers.active') : t('trackers.paused')}