feat(frontend): add configurable app settings (file load limit, tag rule apply_to_existing)
- Add appSettings store (localStorage-backed) with two settings: fileLoadLimit (default 100) and tagRuleApplyToExisting (default false) - Settings page: new Behaviour section with numeric input for files per page (10–500) and an on/off toggle for retroactive tag rule application - files/+page.svelte: derive LIMIT from appSettings.fileLoadLimit so changes take effect immediately without reload - TagRuleEditor: pass apply_to_existing from appSettings when activating a rule via PATCH (only sent on activation, not deactivation) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8cfcd39ab6
commit
012c6f9c48
@ -2,6 +2,7 @@
|
|||||||
import { api, ApiError } from '$lib/api/client';
|
import { api, ApiError } from '$lib/api/client';
|
||||||
import type { Tag, TagOffsetPage, TagRule } from '$lib/api/types';
|
import type { Tag, TagOffsetPage, TagRule } from '$lib/api/types';
|
||||||
import TagBadge from './TagBadge.svelte';
|
import TagBadge from './TagBadge.svelte';
|
||||||
|
import { appSettings } from '$lib/stores/appSettings';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
tagId: string;
|
tagId: string;
|
||||||
@ -62,10 +63,11 @@
|
|||||||
busy = true;
|
busy = true;
|
||||||
error = '';
|
error = '';
|
||||||
const thenTagId = rule.then_tag_id!;
|
const thenTagId = rule.then_tag_id!;
|
||||||
|
const activating = !rule.is_active;
|
||||||
try {
|
try {
|
||||||
const updated = await api.patch<TagRule>(`/tags/${tagId}/rules/${thenTagId}`, {
|
const body: Record<string, unknown> = { is_active: activating };
|
||||||
is_active: !rule.is_active,
|
if (activating) body.apply_to_existing = $appSettings.tagRuleApplyToExisting;
|
||||||
});
|
const updated = await api.patch<TagRule>(`/tags/${tagId}/rules/${thenTagId}`, body);
|
||||||
onRulesChange(rules.map((r) => r.then_tag_id === thenTagId ? updated : r));
|
onRulesChange(rules.map((r) => r.then_tag_id === thenTagId ? updated : r));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof ApiError ? e.message : 'Failed to update rule';
|
error = e instanceof ApiError ? e.message : 'Failed to update rule';
|
||||||
|
|||||||
28
frontend/src/lib/stores/appSettings.ts
Normal file
28
frontend/src/lib/stores/appSettings.ts
Normal file
@ -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<AppSettings>(load());
|
||||||
|
|
||||||
|
appSettings.subscribe((v) => {
|
||||||
|
if (browser) localStorage.setItem('app-settings', JSON.stringify(v));
|
||||||
|
});
|
||||||
@ -15,6 +15,7 @@
|
|||||||
import BulkTagEditor from '$lib/components/file/BulkTagEditor.svelte';
|
import BulkTagEditor from '$lib/components/file/BulkTagEditor.svelte';
|
||||||
import { parseDslFilter } from '$lib/utils/dsl';
|
import { parseDslFilter } from '$lib/utils/dsl';
|
||||||
import type { File, FileCursorPage, Pool, PoolOffsetPage } from '$lib/api/types';
|
import type { File, FileCursorPage, Pool, PoolOffsetPage } from '$lib/api/types';
|
||||||
|
import { appSettings } from '$lib/stores/appSettings';
|
||||||
|
|
||||||
let uploader = $state<{ open: () => void } | undefined>();
|
let uploader = $state<{ open: () => void } | undefined>();
|
||||||
let confirmDeleteFiles = $state(false);
|
let confirmDeleteFiles = $state(false);
|
||||||
@ -65,7 +66,7 @@
|
|||||||
files = [file, ...files];
|
files = [file, ...files];
|
||||||
}
|
}
|
||||||
|
|
||||||
const LIMIT = 50;
|
let LIMIT = $derived($appSettings.fileLoadLimit);
|
||||||
|
|
||||||
const FILE_SORT_OPTIONS = [
|
const FILE_SORT_OPTIONS = [
|
||||||
{ value: 'created', label: 'Created' },
|
{ value: 'created', label: 'Created' },
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { api, ApiError } from '$lib/api/client';
|
import { api, ApiError } from '$lib/api/client';
|
||||||
import { authStore } from '$lib/stores/auth';
|
import { authStore } from '$lib/stores/auth';
|
||||||
import { themeStore, toggleTheme } from '$lib/stores/theme';
|
import { themeStore, toggleTheme } from '$lib/stores/theme';
|
||||||
|
import { appSettings } from '$lib/stores/appSettings';
|
||||||
import type { User, Session, SessionList } from '$lib/api/types';
|
import type { User, Session, SessionList } from '$lib/api/types';
|
||||||
|
|
||||||
// ---- Profile ----
|
// ---- Profile ----
|
||||||
@ -242,6 +243,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ====== App settings ====== -->
|
||||||
|
<section class="card">
|
||||||
|
<h2 class="section-title">Behaviour</h2>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label class="label" for="file-limit">Files per page</label>
|
||||||
|
<p class="hint-text">How many files to load in one batch when scrolling the file list.</p>
|
||||||
|
<input
|
||||||
|
id="file-limit"
|
||||||
|
class="input input-narrow"
|
||||||
|
type="number"
|
||||||
|
min="10"
|
||||||
|
max="500"
|
||||||
|
step="1"
|
||||||
|
value={$appSettings.fileLoadLimit}
|
||||||
|
oninput={(e) => {
|
||||||
|
const v = parseInt((e.currentTarget as HTMLInputElement).value, 10);
|
||||||
|
if (!isNaN(v) && v >= 10 && v <= 500)
|
||||||
|
appSettings.update((s) => ({ ...s, fileLoadLimit: v }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toggle-row">
|
||||||
|
<div>
|
||||||
|
<span class="toggle-label">Apply activated tag rules to existing files</span>
|
||||||
|
<p class="hint-text">When a tag rule is activated, automatically add the implied tag to all files that already have the source tag.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="toggle"
|
||||||
|
class:on={$appSettings.tagRuleApplyToExisting}
|
||||||
|
role="switch"
|
||||||
|
aria-checked={$appSettings.tagRuleApplyToExisting}
|
||||||
|
aria-label="Apply activated tag rules to existing files"
|
||||||
|
onclick={() => appSettings.update((s) => ({ ...s, tagRuleApplyToExisting: !s.tagRuleApplyToExisting }))}
|
||||||
|
>
|
||||||
|
<span class="thumb"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- ====== Sessions ====== -->
|
<!-- ====== Sessions ====== -->
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
@ -434,6 +476,43 @@
|
|||||||
color: var(--color-accent);
|
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 ---- */
|
/* ---- PWA ---- */
|
||||||
.hint-text {
|
.hint-text {
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user