feat: replace all select dropdowns with IconGridSelect, fix EN template seed
Shared grid-items.ts with reusable item definitions for: sort by/order, album mode, asset type, memory source, locale, response mode, event type filter, chat action, preview target type, provider type. Replaced selects on: - Dashboard: event type, provider, sort (compact mode — auto-width) - Tracking configs: sort by/order, album modes, asset types, memory source - Command configs: locale, response mode, provider type - Targets: chat action - Template configs: preview target type, provider type - Command template configs: provider type - Providers: type selector (read-only during edit) IconGridSelect: added compact prop for inline filter bars (auto-width, smaller padding, shows icon + label text). Backend: template seed now re-creates deleted system templates on startup using raw SQL to handle legacy NOT NULL columns. Added i18n: trackingConfig.providerType, trackingConfig.sortRandom Added provider_type badge to tracking config cards.
This commit is contained in:
@@ -14,12 +14,14 @@
|
|||||||
placeholder = 'Select...',
|
placeholder = 'Select...',
|
||||||
columns = 2,
|
columns = 2,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
compact = false,
|
||||||
}: {
|
}: {
|
||||||
items: GridItem[];
|
items: GridItem[];
|
||||||
value: string | number | null;
|
value: string | number | null;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
columns?: number;
|
columns?: number;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
compact?: boolean;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
let open = $state(false);
|
let open = $state(false);
|
||||||
@@ -58,16 +60,16 @@
|
|||||||
<svelte:window onkeydown={open ? handleKeydown : undefined} />
|
<svelte:window onkeydown={open ? handleKeydown : undefined} />
|
||||||
|
|
||||||
<button type="button" bind:this={triggerEl} onclick={toggle}
|
<button type="button" bind:this={triggerEl} onclick={toggle}
|
||||||
class="icon-grid-trigger"
|
class="icon-grid-trigger {compact ? 'icon-grid-compact' : ''}"
|
||||||
class:disabled
|
class:disabled
|
||||||
style="opacity: {disabled ? 0.5 : 1}; cursor: {disabled ? 'default' : 'pointer'};">
|
style="opacity: {disabled ? 0.5 : 1}; cursor: {disabled ? 'default' : 'pointer'};">
|
||||||
{#if selected}
|
{#if selected}
|
||||||
<span class="icon-grid-trigger-icon"><MdiIcon name={selected.icon} size={18} /></span>
|
<span class="icon-grid-trigger-icon"><MdiIcon name={selected.icon} size={compact ? 14 : 18} /></span>
|
||||||
<span class="icon-grid-trigger-label">{selected.label}</span>
|
<span class="icon-grid-trigger-label">{selected.label}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="icon-grid-trigger-label" style="color: var(--color-muted-foreground);">{placeholder}</span>
|
<span class="icon-grid-trigger-label" style="color: var(--color-muted-foreground);">{placeholder}</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="icon-grid-trigger-arrow"><MdiIcon name="mdiChevronDown" size={14} /></span>
|
<span class="icon-grid-trigger-arrow"><MdiIcon name="mdiChevronDown" size={compact ? 10 : 14} /></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
@@ -113,6 +115,15 @@
|
|||||||
.icon-grid-trigger:hover:not(.disabled) {
|
.icon-grid-trigger:hover:not(.disabled) {
|
||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
.icon-grid-compact {
|
||||||
|
width: auto;
|
||||||
|
padding: 0.3rem 0.5rem;
|
||||||
|
gap: 0.3rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
.icon-grid-compact .icon-grid-trigger-label {
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
.icon-grid-trigger-icon {
|
.icon-grid-trigger-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
/**
|
||||||
|
* Shared IconGridSelect item definitions used across multiple pages.
|
||||||
|
* Keeps grid item arrays DRY and consistent.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
|
import type { GridItem } from '$lib/components/IconGridSelect.svelte';
|
||||||
|
|
||||||
|
// --- Sort ---
|
||||||
|
|
||||||
|
export const sortByItems = (): GridItem[] => [
|
||||||
|
{ value: 'none', icon: 'mdiMinus', label: t('trackingConfig.sortNone') },
|
||||||
|
{ value: 'date', icon: 'mdiCalendar', label: t('trackingConfig.sortDate') },
|
||||||
|
{ value: 'rating', icon: 'mdiStar', label: t('trackingConfig.sortRating') },
|
||||||
|
{ value: 'name', icon: 'mdiSortAlphabeticalAscending', label: t('trackingConfig.sortName') },
|
||||||
|
{ value: 'random', icon: 'mdiDice3', label: t('trackingConfig.sortRandom') },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const sortOrderItems = (): GridItem[] => [
|
||||||
|
{ value: 'descending', icon: 'mdiSortDescending', label: t('trackingConfig.orderDesc') },
|
||||||
|
{ value: 'ascending', icon: 'mdiSortAscending', label: t('trackingConfig.orderAsc') },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Album mode ---
|
||||||
|
|
||||||
|
export const albumModeItems = (): GridItem[] => [
|
||||||
|
{ value: 'per_collection', icon: 'mdiViewGrid', label: t('trackingConfig.albumModePerAlbum') },
|
||||||
|
{ value: 'combined', icon: 'mdiSetMerge', label: t('trackingConfig.albumModeCombined') },
|
||||||
|
{ value: 'random', icon: 'mdiDice3', label: t('trackingConfig.albumModeRandom') },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Asset type ---
|
||||||
|
|
||||||
|
export const assetTypeItems = (): GridItem[] => [
|
||||||
|
{ value: 'all', icon: 'mdiSelectAll', label: t('trackingConfig.assetTypeAll') },
|
||||||
|
{ value: 'photo', icon: 'mdiImage', label: t('trackingConfig.assetTypePhoto') },
|
||||||
|
{ value: 'video', icon: 'mdiVideo', label: t('trackingConfig.assetTypeVideo') },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Memory source ---
|
||||||
|
|
||||||
|
export const memorySourceItems = (): GridItem[] => [
|
||||||
|
{ value: 'albums', icon: 'mdiImageMultiple', label: t('trackingConfig.memorySourceAlbums') },
|
||||||
|
{ value: 'native', icon: 'mdiMemory', label: t('trackingConfig.memorySourceNative') },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Locale ---
|
||||||
|
|
||||||
|
export const localeItems = (): GridItem[] => [
|
||||||
|
{ value: 'en', icon: 'mdiAlphabeticalVariant', label: 'English' },
|
||||||
|
{ value: 'ru', icon: 'mdiAlphabeticalVariant', label: 'Русский' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Response mode ---
|
||||||
|
|
||||||
|
export const responseModeItems = (tFn: typeof t): GridItem[] => [
|
||||||
|
{ value: 'media', icon: 'mdiImage', label: tFn('commandConfig.modeMedia') },
|
||||||
|
{ value: 'text', icon: 'mdiText', label: tFn('commandConfig.modeText') },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Event type filter (dashboard) ---
|
||||||
|
|
||||||
|
export const eventTypeFilterItems = (): GridItem[] => [
|
||||||
|
{ value: '', icon: 'mdiFilterOff', label: t('dashboard.allEvents') },
|
||||||
|
{ value: 'assets_added', icon: 'mdiImagePlus', label: t('dashboard.filterAssetsAdded') },
|
||||||
|
{ value: 'assets_removed', icon: 'mdiImageMinus', label: t('dashboard.filterAssetsRemoved') },
|
||||||
|
{ value: 'collection_renamed', icon: 'mdiRename', label: t('dashboard.filterRenamed') },
|
||||||
|
{ value: 'collection_deleted', icon: 'mdiDeleteAlert', label: t('dashboard.filterDeleted') },
|
||||||
|
{ value: 'sharing_changed', icon: 'mdiShareVariant', label: t('dashboard.filterSharingChanged') },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Sort filter (dashboard) ---
|
||||||
|
|
||||||
|
export const sortFilterItems = (): GridItem[] => [
|
||||||
|
{ value: 'newest', icon: 'mdiSortClockDescending', label: t('dashboard.newestFirst') },
|
||||||
|
{ value: 'oldest', icon: 'mdiSortClockAscending', label: t('dashboard.oldestFirst') },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Chat action (Telegram targets) ---
|
||||||
|
|
||||||
|
export const chatActionItems = (): GridItem[] => [
|
||||||
|
{ value: '', icon: 'mdiMinus', label: 'None' },
|
||||||
|
{ value: 'typing', icon: 'mdiKeyboard', label: 'Typing' },
|
||||||
|
{ value: 'upload_photo', icon: 'mdiImagePlus', label: 'Upload Photo' },
|
||||||
|
{ value: 'upload_video', icon: 'mdiVideoPlus', label: 'Upload Video' },
|
||||||
|
{ value: 'upload_document', icon: 'mdiFileUpload', label: 'Upload Doc' },
|
||||||
|
{ value: 'record_video', icon: 'mdiVideo', label: 'Record Video' },
|
||||||
|
{ value: 'record_voice', icon: 'mdiMicrophone', label: 'Record Voice' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Preview target type ---
|
||||||
|
|
||||||
|
export const previewTargetTypeItems = (): GridItem[] => [
|
||||||
|
{ value: 'telegram', icon: 'mdiSend', label: 'Telegram' },
|
||||||
|
{ value: 'webhook', icon: 'mdiWebhook', label: 'Webhook' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// --- Provider type ---
|
||||||
|
|
||||||
|
export const providerTypeItems = (): GridItem[] => [
|
||||||
|
{ value: 'immich', icon: 'mdiCamera', label: 'Immich' },
|
||||||
|
];
|
||||||
@@ -373,7 +373,9 @@
|
|||||||
"added": "added",
|
"added": "added",
|
||||||
"removed": "removed",
|
"removed": "removed",
|
||||||
"renamed": "renamed",
|
"renamed": "renamed",
|
||||||
"deleted": "deleted"
|
"deleted": "deleted",
|
||||||
|
"providerType": "Provider Type",
|
||||||
|
"sortRandom": "Random"
|
||||||
},
|
},
|
||||||
"templateConfig": {
|
"templateConfig": {
|
||||||
"title": "Template Configs",
|
"title": "Template Configs",
|
||||||
|
|||||||
@@ -373,7 +373,9 @@
|
|||||||
"added": "добавление",
|
"added": "добавление",
|
||||||
"removed": "удаление",
|
"removed": "удаление",
|
||||||
"renamed": "переименование",
|
"renamed": "переименование",
|
||||||
"deleted": "удалён"
|
"deleted": "удалён",
|
||||||
|
"providerType": "Тип провайдера",
|
||||||
|
"sortRandom": "Случайный"
|
||||||
},
|
},
|
||||||
"templateConfig": {
|
"templateConfig": {
|
||||||
"title": "Конфигурации шаблонов",
|
"title": "Конфигурации шаблонов",
|
||||||
|
|||||||
@@ -8,9 +8,16 @@
|
|||||||
import Loading from '$lib/components/Loading.svelte';
|
import Loading from '$lib/components/Loading.svelte';
|
||||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||||
import EventChart from '$lib/components/EventChart.svelte';
|
import EventChart from '$lib/components/EventChart.svelte';
|
||||||
|
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
||||||
|
import EntitySelect from '$lib/components/EntitySelect.svelte';
|
||||||
|
import { eventTypeFilterItems, sortFilterItems } from '$lib/grid-items';
|
||||||
|
|
||||||
let status = $state<any>(null);
|
let status = $state<any>(null);
|
||||||
let providers = $derived(providersCache.items);
|
let providers = $derived(providersCache.items);
|
||||||
|
const providerFilterItems = $derived([
|
||||||
|
{ value: '', label: t('dashboard.allProviders'), icon: 'mdiFilterOff' },
|
||||||
|
...providers.map(p => ({ value: p.id, label: p.name, icon: p.icon || 'mdiServer', desc: p.type })),
|
||||||
|
]);
|
||||||
let chartDays = $state<any[]>([]);
|
let chartDays = $state<any[]>([]);
|
||||||
let loaded = $state(false);
|
let loaded = $state(false);
|
||||||
let error = $state('');
|
let error = $state('');
|
||||||
@@ -88,6 +95,16 @@
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auto-apply when filter values change (via IconGridSelect bind:value)
|
||||||
|
let _prevFilterKey = '';
|
||||||
|
$effect(() => {
|
||||||
|
const key = `${filterEventType}|${filterProviderId}|${filterSort}`;
|
||||||
|
if (loaded && key !== _prevFilterKey && _prevFilterKey !== '') {
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
_prevFilterKey = key;
|
||||||
|
});
|
||||||
|
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
eventsOffset = 0;
|
eventsOffset = 0;
|
||||||
loadEvents();
|
loadEvents();
|
||||||
@@ -190,14 +207,6 @@
|
|||||||
collection_renamed: '#6366f1', collection_deleted: '#dc2626', sharing_changed: '#f59e0b',
|
collection_renamed: '#6366f1', collection_deleted: '#dc2626', sharing_changed: '#f59e0b',
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventTypeOptions = $derived([
|
|
||||||
{ value: '', label: t('dashboard.allEvents') },
|
|
||||||
{ value: 'assets_added', label: t('dashboard.filterAssetsAdded') },
|
|
||||||
{ value: 'assets_removed', label: t('dashboard.filterAssetsRemoved') },
|
|
||||||
{ value: 'collection_renamed', label: t('dashboard.filterRenamed') },
|
|
||||||
{ value: 'collection_deleted', label: t('dashboard.filterDeleted') },
|
|
||||||
{ value: 'sharing_changed', label: t('dashboard.filterSharingChanged') },
|
|
||||||
]);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageHeader title={t('dashboard.title')} description={t('dashboard.description')} />
|
<PageHeader title={t('dashboard.title')} description={t('dashboard.description')} />
|
||||||
@@ -248,24 +257,9 @@
|
|||||||
placeholder={t('dashboard.searchEvents')}
|
placeholder={t('dashboard.searchEvents')}
|
||||||
class="w-full px-3 py-1.5 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
class="w-full px-3 py-1.5 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
||||||
</div>
|
</div>
|
||||||
<select bind:value={filterEventType} onchange={applyFilters}
|
<IconGridSelect items={eventTypeFilterItems()} bind:value={filterEventType} columns={3} compact />
|
||||||
class="px-2 py-1.5 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
<IconGridSelect items={providerFilterItems} bind:value={filterProviderId} columns={2} compact />
|
||||||
{#each eventTypeOptions as opt}
|
<IconGridSelect items={sortFilterItems()} bind:value={filterSort} columns={2} compact />
|
||||||
<option value={opt.value}>{opt.label}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
<select bind:value={filterProviderId} onchange={applyFilters}
|
|
||||||
class="px-2 py-1.5 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
|
||||||
<option value="">{t('dashboard.allProviders')}</option>
|
|
||||||
{#each providers as p}
|
|
||||||
<option value={p.id}>{p.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
<select bind:value={filterSort} onchange={applyFilters}
|
|
||||||
class="px-2 py-1.5 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
|
||||||
<option value="newest">{t('dashboard.newestFirst')}</option>
|
|
||||||
<option value="oldest">{t('dashboard.oldestFirst')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart (now inside Events section, affected by filters) -->
|
<!-- Chart (now inside Events section, affected by filters) -->
|
||||||
|
|||||||
@@ -13,10 +13,7 @@
|
|||||||
import IconButton from '$lib/components/IconButton.svelte';
|
import IconButton from '$lib/components/IconButton.svelte';
|
||||||
import CrossLink from '$lib/components/CrossLink.svelte';
|
import CrossLink from '$lib/components/CrossLink.svelte';
|
||||||
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
||||||
|
import { providerTypeItems, localeItems, responseModeItems } from '$lib/grid-items';
|
||||||
const providerTypeItems = [
|
|
||||||
{ value: 'immich', icon: 'mdiCamera', label: 'Immich' },
|
|
||||||
];
|
|
||||||
import EntitySelect from '$lib/components/EntitySelect.svelte';
|
import EntitySelect from '$lib/components/EntitySelect.svelte';
|
||||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||||
import { highlightFromUrl } from '$lib/highlight';
|
import { highlightFromUrl } from '$lib/highlight';
|
||||||
@@ -169,7 +166,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1">{t('commandConfig.providerType')}</label>
|
<label class="block text-sm font-medium mb-1">{t('commandConfig.providerType')}</label>
|
||||||
{#if !editing}
|
{#if !editing}
|
||||||
<IconGridSelect items={providerTypeItems} bind:value={form.provider_type} columns={2} />
|
<IconGridSelect items={providerTypeItems()} bind:value={form.provider_type} columns={2} />
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-sm text-[var(--color-muted-foreground)]">{form.provider_type}</p>
|
<p class="text-sm text-[var(--color-muted-foreground)]">{form.provider_type}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -198,19 +195,11 @@
|
|||||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs mb-1">{t('commandConfig.locale')}</label>
|
<label class="block text-xs mb-1">{t('commandConfig.locale')}</label>
|
||||||
<select bind:value={form.locale}
|
<IconGridSelect items={localeItems()} bind:value={form.locale} columns={2} />
|
||||||
class="w-full px-2 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-background)]">
|
|
||||||
<option value="en">English</option>
|
|
||||||
<option value="ru">Русский</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs mb-1">{t('commandConfig.responseMode')}</label>
|
<label class="block text-xs mb-1">{t('commandConfig.responseMode')}</label>
|
||||||
<select bind:value={form.response_mode}
|
<IconGridSelect items={responseModeItems(t)} bind:value={form.response_mode} columns={2} />
|
||||||
class="w-full px-2 py-1.5 text-sm border border-[var(--color-border)] rounded-md bg-[var(--color-background)]">
|
|
||||||
<option value="media">{t('commandConfig.modeMedia')}</option>
|
|
||||||
<option value="text">{t('commandConfig.modeText')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs mb-1">{t('commandConfig.defaultCount')}</label>
|
<label class="block text-xs mb-1">{t('commandConfig.defaultCount')}</label>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||||
import IconButton from '$lib/components/IconButton.svelte';
|
import IconButton from '$lib/components/IconButton.svelte';
|
||||||
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
||||||
|
import { providerTypeItems as providerTypeItemsFn } from '$lib/grid-items';
|
||||||
import Modal from '$lib/components/Modal.svelte';
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
import JinjaEditor from '$lib/components/JinjaEditor.svelte';
|
import JinjaEditor from '$lib/components/JinjaEditor.svelte';
|
||||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||||
@@ -51,9 +52,6 @@
|
|||||||
// Provider capabilities
|
// Provider capabilities
|
||||||
let allCapabilities = $state<Record<string, any>>({});
|
let allCapabilities = $state<Record<string, any>>({});
|
||||||
let providerTypes = $derived(Object.keys(allCapabilities));
|
let providerTypes = $derived(Object.keys(allCapabilities));
|
||||||
const providerTypeItems = $derived(providerTypes.map(pt => ({
|
|
||||||
value: pt, icon: 'mdiCamera', label: allCapabilities[pt]?.display_name || pt,
|
|
||||||
})));
|
|
||||||
let commandSlots = $derived<SlotDef[]>(
|
let commandSlots = $derived<SlotDef[]>(
|
||||||
allCapabilities[form.provider_type]?.command_slots || []
|
allCapabilities[form.provider_type]?.command_slots || []
|
||||||
);
|
);
|
||||||
@@ -236,7 +234,7 @@
|
|||||||
{#if !editing}
|
{#if !editing}
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1">{t('templateConfig.providerType')}</label>
|
<label class="block text-sm font-medium mb-1">{t('templateConfig.providerType')}</label>
|
||||||
<IconGridSelect items={providerTypeItems} bind:value={form.provider_type} columns={2} />
|
<IconGridSelect items={providerTypeItemsFn()} bind:value={form.provider_type} columns={2} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
import EmptyState from '$lib/components/EmptyState.svelte';
|
import EmptyState from '$lib/components/EmptyState.svelte';
|
||||||
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
||||||
import IconButton from '$lib/components/IconButton.svelte';
|
import IconButton from '$lib/components/IconButton.svelte';
|
||||||
|
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
||||||
|
import { providerTypeItems } from '$lib/grid-items';
|
||||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||||
import { highlightFromUrl } from '$lib/highlight';
|
import { highlightFromUrl } from '$lib/highlight';
|
||||||
import type { ServiceProvider } from '$lib/types';
|
import type { ServiceProvider } from '$lib/types';
|
||||||
@@ -111,11 +113,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<form onsubmit={save} class="space-y-3">
|
<form onsubmit={save} class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label for="prv-type" class="block text-sm font-medium mb-1">{t('providers.type')}</label>
|
<label class="block text-sm font-medium mb-1">{t('providers.type')}</label>
|
||||||
<select id="prv-type" bind:value={form.type} disabled={!!editing}
|
{#if !editing}
|
||||||
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)] disabled:opacity-60">
|
<IconGridSelect items={providerTypeItems()} bind:value={form.type} columns={2} />
|
||||||
<option value="immich">Immich</option>
|
{:else}
|
||||||
</select>
|
<p class="text-sm text-[var(--color-muted-foreground)]">{form.type}</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="prv-name" class="block text-sm font-medium mb-1">{t('providers.name')}</label>
|
<label for="prv-name" class="block text-sm font-medium mb-1">{t('providers.name')}</label>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import CrossLink from '$lib/components/CrossLink.svelte';
|
import CrossLink from '$lib/components/CrossLink.svelte';
|
||||||
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
||||||
import EntitySelect from '$lib/components/EntitySelect.svelte';
|
import EntitySelect from '$lib/components/EntitySelect.svelte';
|
||||||
|
import { chatActionItems } from '$lib/grid-items';
|
||||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||||
import { highlightFromUrl } from '$lib/highlight';
|
import { highlightFromUrl } from '$lib/highlight';
|
||||||
import type { NotificationTarget, TelegramBot, TelegramChat, EmailBot, MatrixBot } from '$lib/types';
|
import type { NotificationTarget, TelegramBot, TelegramChat, EmailBot, MatrixBot } from '$lib/types';
|
||||||
@@ -308,17 +309,8 @@
|
|||||||
<input id="tgt-maxsize" type="number" bind:value={form.max_asset_size} min="1" max="50" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
<input id="tgt-maxsize" type="number" bind:value={form.max_asset_size} min="1" max="50" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<label for="tgt-chataction" class="block text-xs mb-1">{t('targets.chatAction')}</label>
|
<label class="block text-xs mb-1">{t('targets.chatAction')}</label>
|
||||||
<select id="tgt-chataction" bind:value={form.chat_action}
|
<IconGridSelect items={chatActionItems()} bind:value={form.chat_action} columns={4} />
|
||||||
class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
|
||||||
<option value="">{t('targets.chatActionNone')}</option>
|
|
||||||
<option value="typing">typing</option>
|
|
||||||
<option value="upload_photo">upload_photo</option>
|
|
||||||
<option value="upload_video">upload_video</option>
|
|
||||||
<option value="upload_document">upload_document</option>
|
|
||||||
<option value="record_video">record_video</option>
|
|
||||||
<option value="record_voice">record_voice</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<label class="flex items-center gap-2 text-sm col-span-2"><input type="checkbox" bind:checked={form.disable_url_preview} /> {t('targets.disableUrlPreview')}</label>
|
<label class="flex items-center gap-2 text-sm col-span-2"><input type="checkbox" bind:checked={form.disable_url_preview} /> {t('targets.disableUrlPreview')}</label>
|
||||||
<label class="flex items-center gap-2 text-sm col-span-2"><input type="checkbox" bind:checked={form.send_large_photos_as_documents} /> {t('targets.sendLargeAsDocuments')}</label>
|
<label class="flex items-center gap-2 text-sm col-span-2"><input type="checkbox" bind:checked={form.send_large_photos_as_documents} /> {t('targets.sendLargeAsDocuments')}</label>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
import Hint from '$lib/components/Hint.svelte';
|
import Hint from '$lib/components/Hint.svelte';
|
||||||
import IconButton from '$lib/components/IconButton.svelte';
|
import IconButton from '$lib/components/IconButton.svelte';
|
||||||
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
||||||
|
import { providerTypeItems, previewTargetTypeItems } from '$lib/grid-items';
|
||||||
import Modal from '$lib/components/Modal.svelte';
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
import JinjaEditor from '$lib/components/JinjaEditor.svelte';
|
import JinjaEditor from '$lib/components/JinjaEditor.svelte';
|
||||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||||
@@ -111,9 +112,6 @@
|
|||||||
// Provider capabilities: loaded dynamically
|
// Provider capabilities: loaded dynamically
|
||||||
let allCapabilities = $state<Record<string, any>>({});
|
let allCapabilities = $state<Record<string, any>>({});
|
||||||
let providerTypes = $derived(Object.keys(allCapabilities));
|
let providerTypes = $derived(Object.keys(allCapabilities));
|
||||||
const providerTypeItems = $derived(providerTypes.map(pt => ({
|
|
||||||
value: pt, icon: 'mdiCamera', label: allCapabilities[pt]?.display_name || pt,
|
|
||||||
})));
|
|
||||||
|
|
||||||
// Dynamic slot definitions based on selected provider_type
|
// Dynamic slot definitions based on selected provider_type
|
||||||
let notificationSlots = $derived<{name: string, description: string}[]>(
|
let notificationSlots = $derived<{name: string, description: string}[]>(
|
||||||
@@ -249,7 +247,7 @@
|
|||||||
{#if !editing}
|
{#if !editing}
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1">{t('templateConfig.providerType')}</label>
|
<label class="block text-sm font-medium mb-1">{t('templateConfig.providerType')}</label>
|
||||||
<IconGridSelect items={providerTypeItems} bind:value={form.provider_type} columns={2} />
|
<IconGridSelect items={providerTypeItems()} bind:value={form.provider_type} columns={2} />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
<div>
|
||||||
@@ -259,12 +257,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<label for="preview-target" class="text-sm font-medium">{t('templateConfig.previewAs')}:</label>
|
<label class="text-sm font-medium">{t('templateConfig.previewAs')}:</label>
|
||||||
<select id="preview-target" bind:value={previewTargetType} onchange={refreshAllPreviews}
|
<IconGridSelect items={previewTargetTypeItems()} bind:value={previewTargetType} columns={2} />
|
||||||
class="px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
|
||||||
<option value="telegram">Telegram</option>
|
|
||||||
<option value="webhook">Webhook</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each templateSlots as group}
|
{#each templateSlots as group}
|
||||||
|
|||||||
@@ -14,10 +14,7 @@
|
|||||||
import Hint from '$lib/components/Hint.svelte';
|
import Hint from '$lib/components/Hint.svelte';
|
||||||
import IconButton from '$lib/components/IconButton.svelte';
|
import IconButton from '$lib/components/IconButton.svelte';
|
||||||
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
|
||||||
|
import { providerTypeItems, sortByItems, sortOrderItems, albumModeItems, assetTypeItems, memorySourceItems } from '$lib/grid-items';
|
||||||
const providerTypeItems = [
|
|
||||||
{ value: 'immich', icon: 'mdiCamera', label: 'Immich' },
|
|
||||||
];
|
|
||||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||||
import { highlightFromUrl } from '$lib/highlight';
|
import { highlightFromUrl } from '$lib/highlight';
|
||||||
import type { TrackingConfig } from '$lib/types';
|
import type { TrackingConfig } from '$lib/types';
|
||||||
@@ -106,7 +103,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1">{t('trackingConfig.providerType')}</label>
|
<label class="block text-sm font-medium mb-1">{t('trackingConfig.providerType')}</label>
|
||||||
{#if !editing}
|
{#if !editing}
|
||||||
<IconGridSelect items={providerTypeItems} bind:value={form.provider_type} columns={2} />
|
<IconGridSelect items={providerTypeItems()} bind:value={form.provider_type} columns={2} />
|
||||||
{:else}
|
{:else}
|
||||||
<p class="text-sm text-[var(--color-muted-foreground)]">{form.provider_type}</p>
|
<p class="text-sm text-[var(--color-muted-foreground)]">{form.provider_type}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -133,16 +130,12 @@
|
|||||||
<input id="tc-max" type="number" bind:value={form.max_assets_to_show} min="0" max="50" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
<input id="tc-max" type="number" bind:value={form.max_assets_to_show} min="0" max="50" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="tc-sort" class="block text-xs mb-1">{t('trackingConfig.sortBy')}</label>
|
<label class="block text-xs mb-1">{t('trackingConfig.sortBy')}</label>
|
||||||
<select id="tc-sort" bind:value={form.assets_order_by} class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
<IconGridSelect items={sortByItems()} bind:value={form.assets_order_by} columns={2} />
|
||||||
<option value="none">{t('trackingConfig.sortNone')}</option><option value="date">{t('trackingConfig.sortDate')}</option><option value="rating">{t('trackingConfig.sortRating')}</option><option value="name">{t('trackingConfig.sortName')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="tc-order" class="block text-xs mb-1">{t('trackingConfig.sortOrder')}</label>
|
<label class="block text-xs mb-1">{t('trackingConfig.sortOrder')}</label>
|
||||||
<select id="tc-order" bind:value={form.assets_order} class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
<IconGridSelect items={sortOrderItems()} bind:value={form.assets_order} columns={2} />
|
||||||
<option value="descending">{t('trackingConfig.orderDesc')}</option><option value="ascending">{t('trackingConfig.orderAsc')}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
@@ -168,14 +161,10 @@
|
|||||||
<div class="grid grid-cols-3 gap-3 mt-3">
|
<div class="grid grid-cols-3 gap-3 mt-3">
|
||||||
<div><label class="block text-xs mb-1">{t('trackingConfig.times')}<Hint text={t('hints.times')} /></label><input bind:value={form.scheduled_times} placeholder="09:00" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
<div><label class="block text-xs mb-1">{t('trackingConfig.times')}<Hint text={t('hints.times')} /></label><input bind:value={form.scheduled_times} placeholder="09:00" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
||||||
<div><label class="block text-xs mb-1">{t('trackingConfig.albumMode')}<Hint text={t('hints.albumMode')} /></label>
|
<div><label class="block text-xs mb-1">{t('trackingConfig.albumMode')}<Hint text={t('hints.albumMode')} /></label>
|
||||||
<select bind:value={form.scheduled_collection_mode} class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
<IconGridSelect items={albumModeItems()} bind:value={form.scheduled_collection_mode} columns={3} /></div>
|
||||||
<option value="per_collection">{t('trackingConfig.albumModePerAlbum')}</option><option value="combined">{t('trackingConfig.albumModeCombined')}</option><option value="random">{t('trackingConfig.albumModeRandom')}</option>
|
|
||||||
</select></div>
|
|
||||||
<div><label class="block text-xs mb-1">{t('trackingConfig.maxAssets')}<Hint text={t('hints.maxAssets')} /></label><input type="number" bind:value={form.scheduled_limit} min="1" max="100" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
<div><label class="block text-xs mb-1">{t('trackingConfig.maxAssets')}<Hint text={t('hints.maxAssets')} /></label><input type="number" bind:value={form.scheduled_limit} min="1" max="100" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
||||||
<div><label class="block text-xs mb-1">{t('trackingConfig.assetType')}</label>
|
<div><label class="block text-xs mb-1">{t('trackingConfig.assetType')}</label>
|
||||||
<select bind:value={form.scheduled_asset_type} class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
<IconGridSelect items={assetTypeItems()} bind:value={form.scheduled_asset_type} columns={3} /></div>
|
||||||
<option value="all">{t('trackingConfig.assetTypeAll')}</option><option value="photo">{t('trackingConfig.assetTypePhoto')}</option><option value="video">{t('trackingConfig.assetTypeVideo')}</option>
|
|
||||||
</select></div>
|
|
||||||
<div><label class="block text-xs mb-1">{t('trackingConfig.minRating')}<Hint text={t('hints.minRating')} /></label><input type="number" bind:value={form.scheduled_min_rating} min="0" max="5" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
<div><label class="block text-xs mb-1">{t('trackingConfig.minRating')}<Hint text={t('hints.minRating')} /></label><input type="number" bind:value={form.scheduled_min_rating} min="0" max="5" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
||||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" bind:checked={form.scheduled_favorite_only} /> {t('trackingConfig.favoritesOnly')}<Hint text={t('hints.favoritesOnly')} /></label>
|
<label class="flex items-center gap-2 text-sm"><input type="checkbox" bind:checked={form.scheduled_favorite_only} /> {t('trackingConfig.favoritesOnly')}<Hint text={t('hints.favoritesOnly')} /></label>
|
||||||
</div>
|
</div>
|
||||||
@@ -189,19 +178,13 @@
|
|||||||
{#if form.memory_enabled}
|
{#if form.memory_enabled}
|
||||||
<div class="grid grid-cols-3 gap-3 mt-3">
|
<div class="grid grid-cols-3 gap-3 mt-3">
|
||||||
<div><label class="block text-xs mb-1">{t('trackingConfig.memorySource')}<Hint text={t('hints.memorySource')} /></label>
|
<div><label class="block text-xs mb-1">{t('trackingConfig.memorySource')}<Hint text={t('hints.memorySource')} /></label>
|
||||||
<select bind:value={form.memory_source} class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
<IconGridSelect items={memorySourceItems()} bind:value={form.memory_source} columns={2} /></div>
|
||||||
<option value="albums">{t('trackingConfig.memorySourceAlbums')}</option><option value="native">{t('trackingConfig.memorySourceNative')}</option>
|
|
||||||
</select></div>
|
|
||||||
<div><label class="block text-xs mb-1">{t('trackingConfig.times')}<Hint text={t('hints.times')} /></label><input bind:value={form.memory_times} placeholder="09:00" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
<div><label class="block text-xs mb-1">{t('trackingConfig.times')}<Hint text={t('hints.times')} /></label><input bind:value={form.memory_times} placeholder="09:00" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
||||||
<div><label class="block text-xs mb-1">{t('trackingConfig.albumMode')}<Hint text={t('hints.albumMode')} /></label>
|
<div><label class="block text-xs mb-1">{t('trackingConfig.albumMode')}<Hint text={t('hints.albumMode')} /></label>
|
||||||
<select bind:value={form.memory_collection_mode} class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
<IconGridSelect items={albumModeItems()} bind:value={form.memory_collection_mode} columns={3} /></div>
|
||||||
<option value="per_collection">{t('trackingConfig.albumModePerAlbum')}</option><option value="combined">{t('trackingConfig.albumModeCombined')}</option><option value="random">{t('trackingConfig.albumModeRandom')}</option>
|
|
||||||
</select></div>
|
|
||||||
<div><label class="block text-xs mb-1">{t('trackingConfig.maxAssets')}<Hint text={t('hints.maxAssets')} /></label><input type="number" bind:value={form.memory_limit} min="1" max="100" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
<div><label class="block text-xs mb-1">{t('trackingConfig.maxAssets')}<Hint text={t('hints.maxAssets')} /></label><input type="number" bind:value={form.memory_limit} min="1" max="100" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
||||||
<div><label class="block text-xs mb-1">{t('trackingConfig.assetType')}</label>
|
<div><label class="block text-xs mb-1">{t('trackingConfig.assetType')}</label>
|
||||||
<select bind:value={form.memory_asset_type} class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
|
<IconGridSelect items={assetTypeItems()} bind:value={form.memory_asset_type} columns={3} /></div>
|
||||||
<option value="all">{t('trackingConfig.assetTypeAll')}</option><option value="photo">{t('trackingConfig.assetTypePhoto')}</option><option value="video">{t('trackingConfig.assetTypeVideo')}</option>
|
|
||||||
</select></div>
|
|
||||||
<div><label class="block text-xs mb-1">{t('trackingConfig.minRating')}<Hint text={t('hints.minRating')} /></label><input type="number" bind:value={form.memory_min_rating} min="0" max="5" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
<div><label class="block text-xs mb-1">{t('trackingConfig.minRating')}<Hint text={t('hints.minRating')} /></label><input type="number" bind:value={form.memory_min_rating} min="0" max="5" class="w-full px-2 py-1 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" /></div>
|
||||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" bind:checked={form.memory_favorite_only} /> {t('trackingConfig.favoritesOnly')}<Hint text={t('hints.favoritesOnly')} /></label>
|
<label class="flex items-center gap-2 text-sm"><input type="checkbox" bind:checked={form.memory_favorite_only} /> {t('trackingConfig.favoritesOnly')}<Hint text={t('hints.favoritesOnly')} /></label>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,6 +212,7 @@
|
|||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
{#if config.icon}<MdiIcon name={config.icon} />{/if}
|
{#if config.icon}<MdiIcon name={config.icon} />{/if}
|
||||||
<p class="font-medium">{config.name}</p>
|
<p class="font-medium">{config.name}</p>
|
||||||
|
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)] font-mono">{config.provider_type}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-[var(--color-muted-foreground)]">
|
<p class="text-sm text-[var(--color-muted-foreground)]">
|
||||||
{[config.track_assets_added && t('trackingConfig.added'), config.track_assets_removed && t('trackingConfig.removed'), config.track_collection_renamed && t('trackingConfig.renamed'), config.track_collection_deleted && t('trackingConfig.deleted')].filter(Boolean).join(', ')}
|
{[config.track_assets_added && t('trackingConfig.added'), config.track_assets_removed && t('trackingConfig.removed'), config.track_collection_renamed && t('trackingConfig.renamed'), config.track_collection_deleted && t('trackingConfig.deleted')].filter(Boolean).join(', ')}
|
||||||
|
|||||||
Reference in New Issue
Block a user