Jinja2 syntax highlighting + description field + preview toggle
All checks were successful
Validate / Hassfest (push) Successful in 32s
All checks were successful
Validate / Hassfest (push) Successful in 32s
- Error line highlighting in JinjaEditor (red background on error line)
- Backend returns error_line from TemplateSyntaxError
- Localized syntax error messages with line number
- Renamed {{ }} button to "Variables" (localized)
- Localized all template variable descriptions (EN/RU)
- Added t() fallback parameter for graceful degradation
- Page transition animation (fade) to prevent content stacking
- Added syntaxError/line i18n keys
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,22 +1,44 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { EditorView, placeholder as cmPlaceholder } from '@codemirror/view';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
import { EditorView, Decoration, placeholder as cmPlaceholder, type DecorationSet } from '@codemirror/view';
|
||||
import { EditorState, StateField, StateEffect } from '@codemirror/state';
|
||||
import { StreamLanguage } from '@codemirror/language';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
import { getTheme } from '$lib/theme.svelte';
|
||||
|
||||
let { value = '', onchange, rows = 6, placeholder = '' } = $props<{
|
||||
let { value = '', onchange, rows = 6, placeholder = '', errorLine = null } = $props<{
|
||||
value: string;
|
||||
onchange: (val: string) => void;
|
||||
rows?: number;
|
||||
placeholder?: string;
|
||||
errorLine?: number | null;
|
||||
}>();
|
||||
|
||||
let container: HTMLDivElement;
|
||||
let view: EditorView;
|
||||
const theme = getTheme();
|
||||
|
||||
// Error line highlight effect and field
|
||||
const setErrorLine = StateEffect.define<number | null>();
|
||||
const errorLineField = StateField.define<DecorationSet>({
|
||||
create() { return Decoration.none; },
|
||||
update(decorations, tr) {
|
||||
for (const e of tr.effects) {
|
||||
if (e.is(setErrorLine)) {
|
||||
if (e.value === null) return Decoration.none;
|
||||
const lineNum = e.value;
|
||||
if (lineNum < 1 || lineNum > tr.state.doc.lines) return Decoration.none;
|
||||
const line = tr.state.doc.line(lineNum);
|
||||
return Decoration.set([
|
||||
Decoration.line({ class: 'cm-error-line' }).range(line.from),
|
||||
]);
|
||||
}
|
||||
}
|
||||
return decorations;
|
||||
},
|
||||
provide: f => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
// Simple Jinja2 stream parser for syntax highlighting
|
||||
const jinjaLang = StreamLanguage.define({
|
||||
token(stream) {
|
||||
@@ -52,6 +74,7 @@
|
||||
onMount(() => {
|
||||
const extensions = [
|
||||
jinjaLang,
|
||||
errorLineField,
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
onchange(update.state.doc.toString());
|
||||
@@ -63,6 +86,7 @@
|
||||
'.cm-content': { minHeight: `${rows * 1.5}em`, padding: '8px' },
|
||||
'.cm-editor': { borderRadius: '0.375rem', border: '1px solid var(--color-border)' },
|
||||
'.cm-focused': { outline: '2px solid var(--color-primary)', outlineOffset: '0px' },
|
||||
'.cm-error-line': { backgroundColor: 'rgba(239, 68, 68, 0.2)', outline: '1px solid rgba(239, 68, 68, 0.4)' },
|
||||
// Jinja2 syntax colors
|
||||
'.ͼc': { color: '#e879f9' }, // keyword ({% %}) - purple
|
||||
'.ͼd': { color: '#38bdf8' }, // variableName ({{ }}) - blue
|
||||
@@ -93,6 +117,12 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (view) {
|
||||
view.dispatch({ effects: setErrorLine.of(errorLine ?? null) });
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div bind:this={container}></div>
|
||||
|
||||
@@ -266,8 +266,48 @@
|
||||
"telegramSettings": "Telegram",
|
||||
"videoWarning": "Video warning",
|
||||
"preview": "Preview",
|
||||
"variables": "Variables",
|
||||
"assetFields": "Asset fields (in {% for asset in added_assets %})",
|
||||
"confirmDelete": "Delete this template config?"
|
||||
},
|
||||
"templateVars": {
|
||||
"message_assets_added": { "description": "Notification when new assets are added to an album" },
|
||||
"message_assets_removed": { "description": "Notification when assets are removed from an album" },
|
||||
"message_album_renamed": { "description": "Notification when an album is renamed" },
|
||||
"message_album_deleted": { "description": "Notification when an album is deleted" },
|
||||
"periodic_summary_message": { "description": "Periodic album summary with stats" },
|
||||
"scheduled_assets_message": { "description": "Scheduled asset picks from albums" },
|
||||
"memory_mode_message": { "description": "\"On This Day\" memories from past years" },
|
||||
"album_name": "Album name",
|
||||
"album_url": "Public share URL (if available)",
|
||||
"added_count": "Number of assets added",
|
||||
"removed_count": "Number of assets removed",
|
||||
"change_type": "Type of change",
|
||||
"people": "Detected people names (use {{ people | join(', ') }})",
|
||||
"added_assets": "List of added asset objects (use {% for asset in added_assets %})",
|
||||
"removed_assets": "List of removed asset IDs",
|
||||
"shared": "Whether album is shared (true/false)",
|
||||
"video_warning": "Video size warning text",
|
||||
"old_name": "Previous album name",
|
||||
"new_name": "New album name",
|
||||
"albums": "List of album objects (use {% for album in albums %})",
|
||||
"assets": "List of asset objects (use {% for asset in assets %})",
|
||||
"date": "Current date/time",
|
||||
"asset_filename": "Original filename",
|
||||
"asset_type": "IMAGE or VIDEO",
|
||||
"asset_created_at": "Creation date/time (ISO 8601)",
|
||||
"asset_owner": "Owner display name",
|
||||
"asset_description": "User or EXIF description",
|
||||
"asset_url": "Public viewer URL",
|
||||
"asset_download_url": "Direct download URL",
|
||||
"asset_photo_url": "Preview image URL (images only)",
|
||||
"asset_playback_url": "Video playback URL (videos only)",
|
||||
"asset_is_favorite": "Whether asset is favorited",
|
||||
"asset_rating": "Star rating (1-5 or null)",
|
||||
"asset_city": "City name",
|
||||
"asset_state": "State/region name",
|
||||
"asset_country": "Country name"
|
||||
},
|
||||
"hints": {
|
||||
"periodicSummary": "Sends a scheduled summary of all tracked albums at specified times. Great for daily/weekly digests.",
|
||||
"scheduledAssets": "Sends random or selected photos from tracked albums on a schedule. Like a daily photo pick.",
|
||||
@@ -318,6 +358,8 @@
|
||||
"newPassword": "New password",
|
||||
"passwordChanged": "Password changed successfully",
|
||||
"expand": "Expand",
|
||||
"collapse": "Collapse"
|
||||
"collapse": "Collapse",
|
||||
"syntaxError": "Syntax error",
|
||||
"line": "line"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +45,10 @@ export function initLocale() {
|
||||
* Falls back to English if key not found in current locale.
|
||||
* Reactive: re-evaluates when currentLocale changes.
|
||||
*/
|
||||
export function t(key: string): string {
|
||||
export function t(key: string, fallback?: string): string {
|
||||
return resolve(translations[currentLocale], key)
|
||||
?? resolve(translations.en, key)
|
||||
?? fallback
|
||||
?? key;
|
||||
}
|
||||
|
||||
|
||||
@@ -266,8 +266,48 @@
|
||||
"telegramSettings": "Telegram",
|
||||
"videoWarning": "Предупреждение о видео",
|
||||
"preview": "Предпросмотр",
|
||||
"variables": "Переменные",
|
||||
"assetFields": "Поля файла (в {% for asset in added_assets %})",
|
||||
"confirmDelete": "Удалить эту конфигурацию шаблона?"
|
||||
},
|
||||
"templateVars": {
|
||||
"message_assets_added": { "description": "Уведомление о добавлении файлов в альбом" },
|
||||
"message_assets_removed": { "description": "Уведомление об удалении файлов из альбома" },
|
||||
"message_album_renamed": { "description": "Уведомление о переименовании альбома" },
|
||||
"message_album_deleted": { "description": "Уведомление об удалении альбома" },
|
||||
"periodic_summary_message": { "description": "Периодическая сводка альбомов со статистикой" },
|
||||
"scheduled_assets_message": { "description": "Запланированная подборка фото из альбомов" },
|
||||
"memory_mode_message": { "description": "«В этот день» — фото из прошлых лет" },
|
||||
"album_name": "Название альбома",
|
||||
"album_url": "Публичная ссылка (если есть)",
|
||||
"added_count": "Количество добавленных файлов",
|
||||
"removed_count": "Количество удалённых файлов",
|
||||
"change_type": "Тип изменения",
|
||||
"people": "Обнаруженные люди ({{ people | join(', ') }})",
|
||||
"added_assets": "Список добавленных файлов ({% for asset in added_assets %})",
|
||||
"removed_assets": "Список ID удалённых файлов",
|
||||
"shared": "Общий альбом (true/false)",
|
||||
"video_warning": "Предупреждение о размере видео",
|
||||
"old_name": "Прежнее название альбома",
|
||||
"new_name": "Новое название альбома",
|
||||
"albums": "Список альбомов ({% for album in albums %})",
|
||||
"assets": "Список файлов ({% for asset in assets %})",
|
||||
"date": "Текущая дата/время",
|
||||
"asset_filename": "Имя файла",
|
||||
"asset_type": "IMAGE или VIDEO",
|
||||
"asset_created_at": "Дата создания (ISO 8601)",
|
||||
"asset_owner": "Имя владельца",
|
||||
"asset_description": "Описание (EXIF или пользовательское)",
|
||||
"asset_url": "Ссылка для просмотра",
|
||||
"asset_download_url": "Ссылка для скачивания",
|
||||
"asset_photo_url": "URL превью (только фото)",
|
||||
"asset_playback_url": "URL видео (только видео)",
|
||||
"asset_is_favorite": "В избранном",
|
||||
"asset_rating": "Рейтинг (1-5 или null)",
|
||||
"asset_city": "Город",
|
||||
"asset_state": "Регион",
|
||||
"asset_country": "Страна"
|
||||
},
|
||||
"hints": {
|
||||
"periodicSummary": "Отправляет плановую сводку по всем отслеживаемым альбомам в указанное время. Подходит для ежедневных/еженедельных дайджестов.",
|
||||
"scheduledAssets": "Отправляет случайные или выбранные фото из альбомов по расписанию. Как ежедневная подборка фото.",
|
||||
@@ -318,6 +358,8 @@
|
||||
"newPassword": "Новый пароль",
|
||||
"passwordChanged": "Пароль успешно изменён",
|
||||
"expand": "Развернуть",
|
||||
"collapse": "Свернуть"
|
||||
"collapse": "Свернуть",
|
||||
"syntaxError": "Ошибка синтаксиса",
|
||||
"line": "строка"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user