diff --git a/frontend/src/lib/components/tag/TagRuleEditor.svelte b/frontend/src/lib/components/tag/TagRuleEditor.svelte index 6ef2c47..68aeee1 100644 --- a/frontend/src/lib/components/tag/TagRuleEditor.svelte +++ b/frontend/src/lib/components/tag/TagRuleEditor.svelte @@ -2,6 +2,7 @@ import { api, ApiError } from '$lib/api/client'; import type { Tag, TagOffsetPage, TagRule } from '$lib/api/types'; import TagBadge from './TagBadge.svelte'; + import { appSettings } from '$lib/stores/appSettings'; interface Props { tagId: string; @@ -62,10 +63,11 @@ busy = true; error = ''; const thenTagId = rule.then_tag_id!; + const activating = !rule.is_active; try { - const updated = await api.patch(`/tags/${tagId}/rules/${thenTagId}`, { - is_active: !rule.is_active, - }); + const body: Record = { is_active: activating }; + if (activating) body.apply_to_existing = $appSettings.tagRuleApplyToExisting; + const updated = await api.patch(`/tags/${tagId}/rules/${thenTagId}`, body); onRulesChange(rules.map((r) => r.then_tag_id === thenTagId ? updated : r)); } catch (e) { error = e instanceof ApiError ? e.message : 'Failed to update rule'; diff --git a/frontend/src/lib/stores/appSettings.ts b/frontend/src/lib/stores/appSettings.ts new file mode 100644 index 0000000..5decb7c --- /dev/null +++ b/frontend/src/lib/stores/appSettings.ts @@ -0,0 +1,28 @@ +import { writable } from 'svelte/store'; +import { browser } from '$app/environment'; + +export interface AppSettings { + fileLoadLimit: number; + tagRuleApplyToExisting: boolean; +} + +const DEFAULTS: AppSettings = { + fileLoadLimit: 100, + tagRuleApplyToExisting: false, +}; + +function load(): AppSettings { + if (!browser) return { ...DEFAULTS }; + try { + const stored = JSON.parse(localStorage.getItem('app-settings') ?? 'null'); + return stored ? { ...DEFAULTS, ...stored } : { ...DEFAULTS }; + } catch { + return { ...DEFAULTS }; + } +} + +export const appSettings = writable(load()); + +appSettings.subscribe((v) => { + if (browser) localStorage.setItem('app-settings', JSON.stringify(v)); +}); \ No newline at end of file diff --git a/frontend/src/routes/files/+page.svelte b/frontend/src/routes/files/+page.svelte index f410fec..f261f8a 100644 --- a/frontend/src/routes/files/+page.svelte +++ b/frontend/src/routes/files/+page.svelte @@ -15,6 +15,7 @@ import BulkTagEditor from '$lib/components/file/BulkTagEditor.svelte'; import { parseDslFilter } from '$lib/utils/dsl'; import type { File, FileCursorPage, Pool, PoolOffsetPage } from '$lib/api/types'; + import { appSettings } from '$lib/stores/appSettings'; let uploader = $state<{ open: () => void } | undefined>(); let confirmDeleteFiles = $state(false); @@ -65,7 +66,7 @@ files = [file, ...files]; } - const LIMIT = 50; + let LIMIT = $derived($appSettings.fileLoadLimit); const FILE_SORT_OPTIONS = [ { value: 'created', label: 'Created' }, diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index 6cad9d0..9120312 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -2,6 +2,7 @@ import { api, ApiError } from '$lib/api/client'; import { authStore } from '$lib/stores/auth'; import { themeStore, toggleTheme } from '$lib/stores/theme'; + import { appSettings } from '$lib/stores/appSettings'; import type { User, Session, SessionList } from '$lib/api/types'; // ---- Profile ---- @@ -242,6 +243,47 @@ + +
+

Behaviour

+ +
+ +

How many files to load in one batch when scrolling the file list.

+ { + const v = parseInt((e.currentTarget as HTMLInputElement).value, 10); + if (!isNaN(v) && v >= 10 && v <= 500) + appSettings.update((s) => ({ ...s, fileLoadLimit: v })); + }} + /> +
+ +
+
+ Apply activated tag rules to existing files +

When a tag rule is activated, automatically add the implied tag to all files that already have the source tag.

+
+ +
+
+

@@ -434,6 +476,43 @@ color: var(--color-accent); } + .input-narrow { + max-width: 100px; + } + + /* On/off toggle switch */ + .toggle { + flex-shrink: 0; + position: relative; + width: 42px; + height: 24px; + border-radius: 12px; + border: none; + background-color: color-mix(in srgb, var(--color-accent) 25%, var(--color-bg-primary)); + cursor: pointer; + padding: 0; + transition: background-color 0.15s; + } + + .toggle.on { + background-color: var(--color-accent); + } + + .toggle .thumb { + position: absolute; + top: 3px; + left: 3px; + width: 18px; + height: 18px; + border-radius: 50%; + background-color: #fff; + transition: transform 0.15s; + } + + .toggle.on .thumb { + transform: translateX(18px); + } + /* ---- PWA ---- */ .hint-text { font-size: 0.82rem;