Fix UI issues: locale switching, dark theme, loading, edit support
Some checks failed
Validate / Hassfest (push) Has been cancelled

- Fix i18n: remove $state rune (SSR incompatible in .ts files),
  use reactive localeVersion counter in layout to trigger re-render
  on locale change. Language switching now works immediately.
- Fix dark theme: add global CSS rules for input/select/textarea to
  use theme colors, override browser autofill in dark mode, set
  color-scheme for native controls (scrollbars, checkboxes)
- Collapsible sidebar: toggle button (▶/◀) with persistent state,
  icons-only mode when collapsed. Theme/language buttons moved to
  bottom above user info.
- Loading skeletons: all pages show animated pulse placeholders
  while data loads, eliminating content flicker on tab switch
- Edit support: Servers, Trackers, and Targets now have Edit buttons
  that open the form pre-filled with current values. Save calls PUT.
  Sensitive fields (API key, bot token) can be left empty to keep
  current value when editing.
- CLAUDE.md: add dev server restart rules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 16:15:17 +03:00
parent 42063b7bf6
commit fd1ad91fbe
11 changed files with 264 additions and 71 deletions

View File

@@ -4,30 +4,55 @@
import { t } from '$lib/i18n';
import PageHeader from '$lib/components/PageHeader.svelte';
import Card from '$lib/components/Card.svelte';
import Loading from '$lib/components/Loading.svelte';
let loaded = $state(false);
let trackers = $state<any[]>([]);
let servers = $state<any[]>([]);
let targets = $state<any[]>([]);
let albums = $state<any[]>([]);
let showForm = $state(false);
let form = $state({
let editing = $state<number | null>(null);
const defaultForm = () => ({
name: '', server_id: 0, album_ids: [] as string[], event_types: ['assets_added'],
target_ids: [] as number[], scan_interval: 60,
track_images: true, track_videos: true, notify_favorites_only: false,
include_people: true, include_asset_details: false,
max_assets_to_show: 5, assets_order_by: 'none', assets_order: 'descending',
});
let form = $state(defaultForm());
let error = $state('');
onMount(load);
async function load() {
try { [trackers, servers, targets] = await Promise.all([api('/trackers'), api('/servers'), api('/targets')]); } catch {}
try { [trackers, servers, targets] = await Promise.all([api('/trackers'), api('/servers'), api('/targets')]); } catch {} finally { loaded = true; }
}
async function loadAlbums() { if (!form.server_id) return; albums = await api(`/servers/${form.server_id}/albums`); }
async function create(e: SubmitEvent) {
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],
event_types: [...trk.event_types], target_ids: [...trk.target_ids], scan_interval: trk.scan_interval,
track_images: trk.track_images ?? true, track_videos: trk.track_videos ?? true,
notify_favorites_only: trk.notify_favorites_only ?? false, include_people: trk.include_people ?? true,
include_asset_details: trk.include_asset_details ?? false, max_assets_to_show: trk.max_assets_to_show ?? 5,
assets_order_by: trk.assets_order_by ?? 'none', assets_order: trk.assets_order ?? 'descending',
};
editing = trk.id; showForm = true;
if (form.server_id) await loadAlbums();
}
async function save(e: SubmitEvent) {
e.preventDefault(); error = '';
try { await api('/trackers', { method: 'POST', body: JSON.stringify(form) }); showForm = false; await load(); } catch (err: any) { error = err.message; }
try {
if (editing) {
await api(`/trackers/${editing}`, { method: 'PUT', body: JSON.stringify(form) });
} else {
await api('/trackers', { method: 'POST', body: JSON.stringify(form) });
}
showForm = false; editing = null; await load();
} catch (err: any) { error = err.message; }
}
async function toggle(tracker: any) {
await api(`/trackers/${tracker.id}`, { method: 'PUT', body: JSON.stringify({ enabled: !tracker.enabled }) }); await load();
@@ -41,16 +66,18 @@
</script>
<PageHeader title={t('trackers.title')} description={t('trackers.description')}>
<button onclick={() => { showForm = !showForm; form = { name: '', server_id: 0, album_ids: [], event_types: ['assets_added'], target_ids: [], scan_interval: 60, track_images: true, track_videos: true, notify_favorites_only: false, include_people: true, include_asset_details: false, max_assets_to_show: 5, assets_order_by: 'none', assets_order: 'descending' }; }}
<button onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
{showForm ? t('trackers.cancel') : t('trackers.newTracker')}
</button>
</PageHeader>
{#if showForm}
{#if !loaded}
<Loading />
{:else if showForm}
<Card class="mb-6">
{#if error}<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={create} class="space-y-4">
<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)]" />
@@ -143,12 +170,14 @@
</div>
{/if}
<button type="submit" class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">{t('trackers.createTracker')}</button>
<button type="submit" class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">{editing ? t('common.save') : t('trackers.createTracker')}</button>
</form>
</Card>
{/if}
{#if trackers.length === 0 && !showForm}
{#if !loaded}
<!-- skeleton shown above -->
{:else if trackers.length === 0 && !showForm}
<Card><p class="text-sm text-[var(--color-muted-foreground)]">{t('trackers.noTrackers')}</p></Card>
{:else}
<div class="space-y-3">
@@ -165,6 +194,7 @@
<p class="text-sm text-[var(--color-muted-foreground)]">{tracker.album_ids.length} {t('trackers.albums_count')} · {t('trackers.every')} {tracker.scan_interval}s · {tracker.event_types.join(', ')}</p>
</div>
<div class="flex items-center gap-3">
<button onclick={() => edit(tracker)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('common.edit')}</button>
<button onclick={() => toggle(tracker)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">
{tracker.enabled ? t('trackers.pause') : t('trackers.resume')}
</button>