feat: smart video size warnings + Jinja2 template autocomplete

Video size warnings:
- Add file_size field to ImmichAssetInfo from exifInfo.fileSizeInByte
- Expose per-target max_video_size (50 MB for Telegram, none for others)
- Compute has_oversized_videos and per-asset oversized flag in template context
- Default templates show warning only when videos actually exceed the limit
- Templates no longer hardcode Telegram-specific logic

Template autocomplete:
- New jinja-autocomplete.ts engine with contextual completions
- Top-level variables ({{ }}), asset/album fields (dot access in loops),
  Jinja2 filters (|), block tags ({% %}), and loop.* special vars
- JinjaEditor accepts optional variables prop via CodeMirror Compartment
- Wired into template-configs and command-template-configs pages

Also: fix template emoji (📷📎) and sync sample_context with new vars.
This commit is contained in:
2026-03-23 15:03:35 +03:00
parent 1ac6a17f6f
commit 39bac828fd
14 changed files with 365 additions and 11 deletions
+26 -3
View File
@@ -1,19 +1,24 @@
<script lang="ts">
import { onMount } from 'svelte';
import { EditorView, Decoration, placeholder as cmPlaceholder, type DecorationSet } from '@codemirror/view';
import { EditorState, StateField, StateEffect } from '@codemirror/state';
import { EditorView, Decoration, keymap, placeholder as cmPlaceholder, type DecorationSet } from '@codemirror/view';
import { EditorState, StateField, StateEffect, Compartment } from '@codemirror/state';
import { StreamLanguage } from '@codemirror/language';
import { oneDark } from '@codemirror/theme-one-dark';
import { acceptCompletion } from '@codemirror/autocomplete';
import { getTheme } from '$lib/theme.svelte';
import { jinjaAutocomplete, type SlotVariables } from '$lib/editor/jinja-autocomplete';
let { value = '', onchange, rows = 6, placeholder = '', errorLine = null } = $props<{
let { value = '', onchange, rows = 6, placeholder = '', errorLine = null, variables = undefined } = $props<{
value: string;
onchange: (val: string) => void;
rows?: number;
placeholder?: string;
errorLine?: number | null;
variables?: SlotVariables;
}>();
const autocompleteCompartment = new Compartment();
let container: HTMLDivElement;
let view: EditorView;
const theme = getTheme();
@@ -71,6 +76,8 @@
const extensions = [
jinjaLang,
errorLineField,
autocompleteCompartment.of(variables ? jinjaAutocomplete(variables) : []),
keymap.of([{ key: 'Tab', run: acceptCompletion }]),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
onchange(update.state.doc.toString());
@@ -86,6 +93,11 @@
'.ͼc': { color: '#e879f9' },
'.ͼd': { color: '#38bdf8' },
'.ͼ5': { color: '#6b7280' },
'.cm-tooltip-autocomplete': {
border: '1px solid var(--color-border)',
borderRadius: '0.375rem',
fontSize: '12px',
},
}),
];
if (isDark) extensions.push(oneDark);
@@ -116,6 +128,17 @@
}
});
// Update autocomplete when variables change
$effect(() => {
if (view) {
view.dispatch({
effects: autocompleteCompartment.reconfigure(
variables ? jinjaAutocomplete(variables) : [],
),
});
}
});
// Recreate editor when theme changes
let lastIsDark: boolean | undefined;
$effect(() => {