Some checks failed
Validate / Hassfest (push) Has been cancelled
New pages: - /tracking-configs: Full CRUD with event tracking, asset display, periodic summary, scheduled assets, and memory mode sections. Collapsible sub-sections that show/hide based on enabled state. - /template-configs: Full CRUD with all 21 template slots organized into 5 fieldsets (event messages, asset formatting, date/location, scheduled messages, telegram). Preview support per slot. Updated pages: - Targets: added tracking_config_id + template_config_id selectors (dropdowns populated from configs). Configs are reusable. - Trackers: simplified to album selection + scan interval + targets. Added Test, Test Periodic, Test Memory buttons per tracker. - Nav: replaced Templates with Tracking + Templates config links Other fixes: - Language button: now triggers window.location.reload() to force all child pages to re-evaluate t() calls - Dark theme buttons: changed primary color to dark gray in dark mode - Removed old /templates page (replaced by /template-configs) - Added .gitignore for __pycache__ in server package Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
177 lines
8.0 KiB
Svelte
177 lines
8.0 KiB
Svelte
<script lang="ts">
|
||
import { onMount } from 'svelte';
|
||
import { api } from '$lib/api';
|
||
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 configs = $state<any[]>([]);
|
||
let loaded = $state(false);
|
||
let showForm = $state(false);
|
||
let editing = $state<number | null>(null);
|
||
let error = $state('');
|
||
let previewSlot = $state('message_assets_added');
|
||
let previewResult = $state('');
|
||
let previewId = $state<number | null>(null);
|
||
|
||
const defaultForm = () => ({
|
||
name: '',
|
||
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}".',
|
||
message_album_deleted: '🗑️ Album "{album_name}" was deleted.',
|
||
message_asset_image: '\n • 🖼️ {filename}',
|
||
message_asset_video: '\n • 🎬 {filename}',
|
||
message_assets_format: '\nAssets:{assets}',
|
||
message_assets_more: '\n • ...and {more_count} more',
|
||
message_people_format: ' People: {people}.',
|
||
date_format: '%d.%m.%Y, %H:%M UTC',
|
||
common_date_template: ' from {date}',
|
||
date_if_unique_template: ' ({date})',
|
||
location_format: '{city}, {country}',
|
||
common_location_template: ' in {location}',
|
||
location_if_unique_template: ' 📍 {location}',
|
||
favorite_indicator: '❤️',
|
||
periodic_summary_message: '📋 Tracked Albums Summary ({album_count} albums):{albums}',
|
||
periodic_album_template: '\n • {album_name}: {album_url}',
|
||
scheduled_assets_message: '📸 Here are some photos from album "{album_name}":{assets}',
|
||
memory_mode_message: '📅 On this day:{assets}',
|
||
video_warning: '\n\n⚠️ Note: Videos may not be sent due to Telegram\'s 50 MB file size limit.',
|
||
});
|
||
let form = $state(defaultForm());
|
||
|
||
const templateSlots = [
|
||
{ group: 'eventMessages', slots: [
|
||
{ key: 'message_assets_added', label: 'assetsAdded' },
|
||
{ key: 'message_assets_removed', label: 'assetsRemoved' },
|
||
{ key: 'message_album_renamed', label: 'albumRenamed' },
|
||
{ key: 'message_album_deleted', label: 'albumDeleted' },
|
||
]},
|
||
{ group: 'assetFormatting', slots: [
|
||
{ key: 'message_asset_image', label: 'imageTemplate' },
|
||
{ key: 'message_asset_video', label: 'videoTemplate' },
|
||
{ key: 'message_assets_format', label: 'assetsWrapper' },
|
||
{ key: 'message_assets_more', label: 'moreMessage' },
|
||
{ key: 'message_people_format', label: 'peopleFormat' },
|
||
]},
|
||
{ group: 'dateLocation', slots: [
|
||
{ key: 'date_format', label: 'dateFormat' },
|
||
{ key: 'common_date_template', label: 'commonDate' },
|
||
{ key: 'date_if_unique_template', label: 'uniqueDate' },
|
||
{ key: 'location_format', label: 'locationFormat' },
|
||
{ key: 'common_location_template', label: 'commonLocation' },
|
||
{ key: 'location_if_unique_template', label: 'uniqueLocation' },
|
||
{ key: 'favorite_indicator', label: 'favoriteIndicator' },
|
||
]},
|
||
{ group: 'scheduledMessages', slots: [
|
||
{ key: 'periodic_summary_message', label: 'periodicSummary' },
|
||
{ key: 'periodic_album_template', label: 'periodicAlbum' },
|
||
{ key: 'scheduled_assets_message', label: 'scheduledAssets' },
|
||
{ key: 'memory_mode_message', label: 'memoryMode' },
|
||
]},
|
||
{ group: 'telegramSettings', slots: [
|
||
{ key: 'video_warning', label: 'videoWarning' },
|
||
]},
|
||
];
|
||
|
||
onMount(load);
|
||
async function load() { try { configs = await api('/template-configs'); } catch {} finally { loaded = true; } }
|
||
|
||
function openNew() { form = defaultForm(); editing = null; showForm = true; }
|
||
function edit(c: any) { form = { ...defaultForm(), ...c }; editing = c.id; showForm = true; }
|
||
|
||
async function save(e: SubmitEvent) {
|
||
e.preventDefault(); error = '';
|
||
try {
|
||
if (editing) await api(`/template-configs/${editing}`, { method: 'PUT', body: JSON.stringify(form) });
|
||
else await api('/template-configs', { method: 'POST', body: JSON.stringify(form) });
|
||
showForm = false; editing = null; await load();
|
||
} catch (err: any) { error = err.message; }
|
||
}
|
||
|
||
async function preview(id: number, slot: string) {
|
||
previewId = id; previewSlot = slot;
|
||
try {
|
||
const res = await api(`/template-configs/${id}/preview?slot=${slot}`, { method: 'POST' });
|
||
previewResult = res.rendered;
|
||
} catch (err: any) { previewResult = `Error: ${err.message}`; }
|
||
}
|
||
|
||
async function remove(id: number) {
|
||
if (!confirm(t('common.delete') + '?')) return;
|
||
try { await api(`/template-configs/${id}`, { method: 'DELETE' }); await load(); } catch (err: any) { error = err.message; }
|
||
}
|
||
</script>
|
||
|
||
<PageHeader title={t('templateConfig.title')} description={t('templateConfig.description')}>
|
||
<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('common.cancel') : t('templateConfig.newConfig')}
|
||
</button>
|
||
</PageHeader>
|
||
|
||
{#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={save} class="space-y-5">
|
||
<div>
|
||
<label for="tpc-name" class="block text-sm font-medium mb-1">{t('templateConfig.name')}</label>
|
||
<input id="tpc-name" bind:value={form.name} required placeholder={t('templateConfig.namePlaceholder')}
|
||
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
||
</div>
|
||
|
||
{#each templateSlots as group}
|
||
<fieldset class="border border-[var(--color-border)] rounded-md p-3">
|
||
<legend class="text-sm font-medium px-1">{t(`templateConfig.${group.group}`)}</legend>
|
||
<div class="space-y-3 mt-2">
|
||
{#each group.slots as slot}
|
||
<div>
|
||
<label class="block text-xs text-[var(--color-muted-foreground)] mb-1">{t(`templateConfig.${slot.label}`)}</label>
|
||
<textarea bind:value={form[slot.key]} rows={slot.key.includes('message_asset_') || slot.key.includes('_template') || slot.key === 'favorite_indicator' || slot.key === 'date_format' || slot.key === 'location_format' ? 1 : 2}
|
||
class="w-full px-2 py-1 border border-[var(--color-border)] rounded text-sm bg-[var(--color-background)] font-mono"></textarea>
|
||
</div>
|
||
{/each}
|
||
</div>
|
||
</fieldset>
|
||
{/each}
|
||
|
||
<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('common.create')}
|
||
</button>
|
||
</form>
|
||
</Card>
|
||
{/if}
|
||
|
||
{#if configs.length === 0 && !showForm}
|
||
<Card><p class="text-sm text-[var(--color-muted-foreground)]">{t('templateConfig.noConfigs')}</p></Card>
|
||
{:else}
|
||
<div class="space-y-3">
|
||
{#each configs as config}
|
||
<Card>
|
||
<div class="flex items-start justify-between">
|
||
<div class="flex-1">
|
||
<p class="font-medium">{config.name}</p>
|
||
<pre class="text-xs text-[var(--color-muted-foreground)] mt-1 whitespace-pre-wrap font-mono bg-[var(--color-muted)] rounded p-2">{config.message_assets_added?.slice(0, 120)}...</pre>
|
||
{#if previewResult && previewId === config.id}
|
||
<div class="mt-2 p-2 bg-[var(--color-success-bg)] rounded text-sm">
|
||
<p class="text-xs font-medium mb-1">{previewSlot}:</p>
|
||
<pre class="whitespace-pre-wrap">{previewResult}</pre>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
<div class="flex items-center gap-3 ml-4">
|
||
<button onclick={() => preview(config.id, 'message_assets_added')} class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('templateConfig.preview')}</button>
|
||
<button onclick={() => edit(config)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('common.edit')}</button>
|
||
<button onclick={() => remove(config.id)} class="text-xs text-[var(--color-destructive)] hover:underline">{t('common.delete')}</button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
{/each}
|
||
</div>
|
||
{/if}
|
||
|
||
{/if}
|