diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c83769b..17c265f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "frontend", "version": "0.0.1", + "dependencies": { + "@mdi/js": "^7.4.47" + }, "devDependencies": { "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/adapter-static": "^3.0.10", @@ -521,6 +524,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mdi/js": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", + "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==" + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -2550,6 +2558,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@mdi/js": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/js/-/js-7.4.47.tgz", + "integrity": "sha512-KPnNOtm5i2pMabqZxpUz7iQf+mfrYZyKCZ8QNz85czgEt7cuHcGorWfdzUMWYA0SD+a6Hn4FmJ+YhzzzjkTZrQ==" + }, "@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5e3776e..1a27bf4 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,5 +27,8 @@ "tailwindcss": "^4.2.2", "typescript": "^5.9.3", "vite": "^7.3.1" + }, + "dependencies": { + "@mdi/js": "^7.4.47" } } diff --git a/frontend/src/lib/components/IconPicker.svelte b/frontend/src/lib/components/IconPicker.svelte new file mode 100644 index 0000000..442e6d9 --- /dev/null +++ b/frontend/src/lib/components/IconPicker.svelte @@ -0,0 +1,81 @@ + + +
+ + + {#if open} +
{ open = false; search = ''; }}>
+
+ +
+ + + {#each filtered() as iconName} + + {/each} +
+
+ {/if} +
diff --git a/frontend/src/lib/components/MdiIcon.svelte b/frontend/src/lib/components/MdiIcon.svelte new file mode 100644 index 0000000..31c4ecc --- /dev/null +++ b/frontend/src/lib/components/MdiIcon.svelte @@ -0,0 +1,13 @@ + + +{#if name && getPath(name)} + +{/if} diff --git a/frontend/src/routes/servers/+page.svelte b/frontend/src/routes/servers/+page.svelte index 9da4a94..d2d04c6 100644 --- a/frontend/src/routes/servers/+page.svelte +++ b/frontend/src/routes/servers/+page.svelte @@ -5,11 +5,13 @@ 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 servers = $state([]); let showForm = $state(false); let editing = $state(null); - let form = $state({ name: 'Immich', url: '', api_key: '' }); + let form = $state({ name: 'Immich', url: '', api_key: '', icon: '' }); let error = $state(''); let submitting = $state(false); let loaded = $state(false); @@ -27,11 +29,11 @@ } function openNew() { - form = { name: 'Immich', url: '', api_key: '' }; + form = { name: 'Immich', url: '', api_key: '', icon: '' }; editing = null; showForm = true; } function edit(s: any) { - form = { name: s.name, url: s.url, api_key: '' }; + form = { name: s.name, url: s.url, api_key: '', icon: s.icon || '' }; editing = s.id; showForm = true; } @@ -72,8 +74,13 @@ {#if error}
{error}
{/if}
- - +
+ +
+
+ form.icon = v} /> + +
@@ -100,6 +107,7 @@
+ {#if server.icon}{/if}

{server.name}

{server.url}

diff --git a/frontend/src/routes/targets/+page.svelte b/frontend/src/routes/targets/+page.svelte index f9a02b9..fc63f97 100644 --- a/frontend/src/routes/targets/+page.svelte +++ b/frontend/src/routes/targets/+page.svelte @@ -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 targets = $state([]); let trackingConfigs = $state([]); @@ -14,7 +16,7 @@ let showForm = $state(false); let editing = $state(null); let formType = $state<'telegram' | 'webhook'>('telegram'); - const defaultForm = () => ({ name: '', bot_id: 0, chat_id: '', bot_token: '', url: '', headers: '', + const defaultForm = () => ({ name: '', icon: '', bot_id: 0, chat_id: '', bot_token: '', url: '', headers: '', max_media_to_send: 50, max_media_per_group: 10, media_delay: 500, max_asset_size: 50, disable_url_preview: false, send_large_photos_as_documents: false, ai_captions: false, tracking_config_id: 0, template_config_id: 0 }); @@ -42,7 +44,7 @@ formType = tgt.type; const c = tgt.config || {}; form = { - name: tgt.name, bot_id: c.bot_id || 0, bot_token: '', chat_id: c.chat_id || '', url: c.url || '', headers: '', + name: tgt.name, icon: tgt.icon || '', bot_id: c.bot_id || 0, bot_token: '', chat_id: c.chat_id || '', url: c.url || '', headers: '', max_media_to_send: c.max_media_to_send ?? 50, max_media_per_group: c.max_media_per_group ?? 10, media_delay: c.media_delay ?? 500, max_asset_size: c.max_asset_size ?? 50, disable_url_preview: c.disable_url_preview ?? false, send_large_photos_as_documents: c.send_large_photos_as_documents ?? false, @@ -118,7 +120,10 @@
- +
+ form.icon = v} /> + +
{#if formType === 'telegram'} @@ -222,6 +227,7 @@
+ {#if target.icon}{/if}

{target.name}

{target.type}
diff --git a/frontend/src/routes/telegram-bots/+page.svelte b/frontend/src/routes/telegram-bots/+page.svelte index 0d668c5..cf9f4ae 100644 --- a/frontend/src/routes/telegram-bots/+page.svelte +++ b/frontend/src/routes/telegram-bots/+page.svelte @@ -5,11 +5,13 @@ 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 bots = $state([]); let loaded = $state(false); let showForm = $state(false); - let form = $state({ name: '', token: '' }); + let form = $state({ name: '', icon: '', token: '' }); let error = $state(''); let submitting = $state(false); @@ -25,7 +27,7 @@ e.preventDefault(); error = ''; submitting = true; try { await api('/telegram-bots', { method: 'POST', body: JSON.stringify(form) }); - form = { name: '', token: '' }; showForm = false; await load(); + form = { name: '', icon: '', token: '' }; showForm = false; await load(); } catch (err: any) { error = err.message; } submitting = false; } @@ -56,7 +58,7 @@ - @@ -70,8 +72,11 @@
- +
+ form.icon = v} /> + +
@@ -95,6 +100,7 @@
+ {#if bot.icon}{/if}

{bot.name}

{#if bot.bot_username} @{bot.bot_username} diff --git a/frontend/src/routes/template-configs/+page.svelte b/frontend/src/routes/template-configs/+page.svelte index 0a11d84..fb9bb6a 100644 --- a/frontend/src/routes/template-configs/+page.svelte +++ b/frontend/src/routes/template-configs/+page.svelte @@ -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 configs = $state([]); let loaded = $state(false); @@ -16,7 +18,7 @@ let previewId = $state(null); const defaultForm = () => ({ - name: '', + name: '', icon: '', message_assets_added: '๐Ÿ“ท {added_count} new photo(s) added to album "{album_name}"{common_date}{common_location}.{people}{assets}{video_warning}', message_assets_removed: '๐Ÿ—‘๏ธ {removed_count} photo(s) removed from album "{album_name}".', message_album_renamed: 'โœ๏ธ Album "{old_name}" renamed to "{new_name}".', @@ -119,8 +121,11 @@
- +
+ form.icon = v} /> + +
{#each templateSlots as group} @@ -153,7 +158,10 @@
-

{config.name}

+
+ {#if config.icon}{/if} +

{config.name}

+
{config.message_assets_added?.slice(0, 120)}...
{#if previewResult && previewId === config.id}
diff --git a/frontend/src/routes/trackers/+page.svelte b/frontend/src/routes/trackers/+page.svelte index 65e1b8a..ed86b8c 100644 --- a/frontend/src/routes/trackers/+page.svelte +++ b/frontend/src/routes/trackers/+page.svelte @@ -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([]); @@ -15,7 +17,7 @@ let editing = $state(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.icon = v} /> + +
@@ -138,6 +143,7 @@
+ {#if tracker.icon}{/if}

{tracker.name}

{tracker.enabled ? t('trackers.active') : t('trackers.paused')} diff --git a/frontend/src/routes/tracking-configs/+page.svelte b/frontend/src/routes/tracking-configs/+page.svelte index aeb6ff3..1f31826 100644 --- a/frontend/src/routes/tracking-configs/+page.svelte +++ b/frontend/src/routes/tracking-configs/+page.svelte @@ -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 configs = $state([]); let loaded = $state(false); @@ -13,7 +15,7 @@ let error = $state(''); const defaultForm = () => ({ - name: '', track_assets_added: true, track_assets_removed: false, + name: '', icon: '', track_assets_added: true, track_assets_removed: false, track_album_renamed: true, track_album_deleted: true, track_images: true, track_videos: true, notify_favorites_only: false, include_people: true, include_asset_details: false, @@ -66,8 +68,11 @@
- +
+ form.icon = v} /> + +
@@ -183,7 +188,10 @@
-

{config.name}

+
+ {#if config.icon}{/if} +

{config.name}

+

{[config.track_assets_added && 'added', config.track_assets_removed && 'removed', config.track_album_renamed && 'renamed', config.track_album_deleted && 'deleted'].filter(Boolean).join(', ')} {config.periodic_enabled ? ' ยท periodic' : ''} diff --git a/packages/server/src/immich_watcher_server/database/models.py b/packages/server/src/immich_watcher_server/database/models.py index fad54f4..5ca93ee 100644 --- a/packages/server/src/immich_watcher_server/database/models.py +++ b/packages/server/src/immich_watcher_server/database/models.py @@ -33,6 +33,7 @@ class ImmichServer(SQLModel, table=True): url: str api_key: str external_domain: str | None = None + icon: str = Field(default="") # MDI icon name (e.g. "mdiServer") created_at: datetime = Field(default_factory=_utcnow) @@ -45,6 +46,7 @@ class TelegramBot(SQLModel, table=True): user_id: int = Field(foreign_key="user.id") name: str # User-given display name token: str # Bot API token + icon: str = Field(default="") # MDI icon name bot_username: str = Field(default="") # @username from getMe bot_id: int = Field(default=0) # Numeric bot ID from getMe created_at: datetime = Field(default_factory=_utcnow) @@ -58,6 +60,7 @@ class TrackingConfig(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") name: str + icon: str = Field(default="") # Event-driven tracking track_assets_added: bool = Field(default=True) @@ -112,6 +115,7 @@ class TemplateConfig(SQLModel, table=True): id: int | None = Field(default=None, primary_key=True) user_id: int = Field(foreign_key="user.id") name: str # e.g. "Default EN", "Default RU" + icon: str = Field(default="") # Event messages message_assets_added: str = Field( @@ -172,6 +176,7 @@ class NotificationTarget(SQLModel, table=True): user_id: int = Field(foreign_key="user.id") type: str # "telegram" or "webhook" name: str + icon: str = Field(default="") config: dict[str, Any] = Field(default_factory=dict, sa_column=Column(JSON)) tracking_config_id: int | None = Field(default=None, foreign_key="tracking_config.id") template_config_id: int | None = Field(default=None, foreign_key="template_config.id") @@ -187,6 +192,7 @@ class AlbumTracker(SQLModel, table=True): user_id: int = Field(foreign_key="user.id") server_id: int = Field(foreign_key="immich_server.id") name: str + icon: str = Field(default="") album_ids: list[str] = Field(default_factory=list, sa_column=Column(JSON)) target_ids: list[int] = Field(default_factory=list, sa_column=Column(JSON)) scan_interval: int = Field(default=60)