Add i18n (RU/EN), dark/light themes, enhanced tracker/target forms (Phase 7a)
Some checks failed
Validate / Hassfest (push) Has been cancelled

Frontend enhancements:
- i18n: Full Russian and English translations (~170 keys each),
  language switcher in sidebar and login page, auto-detect from
  browser, persists to localStorage
- Themes: Light/dark mode with CSS custom properties, system
  preference detection, toggle in sidebar header, smooth transitions
- Dark theme: Full color palette (background, card, muted, border,
  success, warning, error variants)

Enhanced forms:
- Tracker creation: asset type filtering (images/videos), favorites
  only, include people/details toggles, sort by/order selects,
  max assets to show
- Target creation: Telegram media settings (collapsible) with
  max media, group size, chunk delay, max asset size, URL preview
  disable, large photos as documents
- Template creation: event_type selector (all/added/removed/renamed/deleted)

All pages use t() for translations, var(--color-*) for theme-safe
colors, and proper label-for-input associations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 15:44:32 +03:00
parent 1ad9b8af1d
commit 2aa9b8939d
14 changed files with 827 additions and 327 deletions

View File

@@ -3,13 +3,18 @@
import { onMount } from 'svelte';
import { api } from '$lib/api';
import { login } from '$lib/auth.svelte';
import { t, initLocale, getLocale, setLocale } from '$lib/i18n';
import { initTheme, getTheme, setTheme, type Theme } from '$lib/theme.svelte';
const theme = getTheme();
let username = $state('');
let password = $state('');
let error = $state('');
let submitting = $state(false);
onMount(async () => {
initLocale();
initTheme();
try {
const res = await api<{ needs_setup: boolean }>('/auth/needs-setup');
if (res.needs_setup) goto('/setup');
@@ -33,40 +38,37 @@
<div class="min-h-screen flex items-center justify-center bg-[var(--color-background)]">
<div class="w-full max-w-sm">
<div class="bg-[var(--color-card)] border border-[var(--color-border)] rounded-lg p-6 shadow-sm">
<h1 class="text-xl font-semibold text-center mb-1">Immich Watcher</h1>
<p class="text-sm text-[var(--color-muted-foreground)] text-center mb-6">Sign in to your account</p>
<div class="flex justify-end gap-1 mb-4">
<button onclick={() => setLocale(getLocale() === 'en' ? 'ru' : 'en')}
class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">
{getLocale().toUpperCase()}
</button>
<button onclick={() => { const o: Theme[] = ['light','dark','system']; setTheme(o[(o.indexOf(theme.current)+1)%3]); }}
class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">
{theme.resolved === 'dark' ? '🌙' : '☀️'}
</button>
</div>
<h1 class="text-xl font-semibold text-center mb-1">{t('app.name')}</h1>
<p class="text-sm text-[var(--color-muted-foreground)] text-center mb-6">{t('auth.signInTitle')}</p>
{#if error}
<div class="bg-red-50 text-red-700 text-sm rounded-md p-3 mb-4">{error}</div>
<div class="bg-[var(--color-error-bg)] text-[var(--color-error-fg)] text-sm rounded-md p-3 mb-4">{error}</div>
{/if}
<form onsubmit={handleSubmit} class="space-y-4">
<div>
<label for="username" class="block text-sm font-medium mb-1.5">Username</label>
<input
id="username"
type="text"
bind:value={username}
required
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--color-background)]"
/>
<label for="username" class="block text-sm font-medium mb-1.5">{t('auth.username')}</label>
<input id="username" type="text" bind:value={username} required
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--color-background)]" />
</div>
<div>
<label for="password" class="block text-sm font-medium mb-1.5">Password</label>
<input
id="password"
type="password"
bind:value={password}
required
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--color-background)]"
/>
<label for="password" class="block text-sm font-medium mb-1.5">{t('auth.password')}</label>
<input id="password" type="password" bind:value={password} required
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)] bg-[var(--color-background)]" />
</div>
<button
type="submit"
disabled={submitting}
class="w-full py-2 px-4 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 transition-opacity disabled:opacity-50"
>
{submitting ? 'Signing in...' : 'Sign in'}
<button type="submit" disabled={submitting}
class="w-full py-2 px-4 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 transition-opacity disabled:opacity-50">
{submitting ? t('auth.signingIn') : t('auth.signIn')}
</button>
</form>
</div>