Fix UI issues: locale switching, dark theme, loading, edit support
Some checks failed
Validate / Hassfest (push) Has been cancelled
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user