feat: asset-based image/video sources, notification sounds, UI improvements
Some checks failed
Lint & Test / test (push) Has been cancelled
Some checks failed
Lint & Test / test (push) Has been cancelled
- Replace URL-based image_source/url fields with image_asset_id/video_asset_id on StaticImagePictureSource and VideoCaptureSource (clean break, no migration) - Resolve asset IDs to file paths at runtime via AssetStore.get_file_path() - Add EntitySelect asset pickers for image/video in stream editor modal - Add notification sound configuration (global sound + per-app overrides) - Unify per-app color and sound overrides into single "Per-App Overrides" section - Persist notification history between server restarts - Add asset management system (upload, edit, delete, soft-delete) - Replace emoji buttons with SVG icons throughout UI - Various backend improvements: SQLite stores, auth, backup, MQTT, webhooks
This commit is contained in:
@@ -212,6 +212,8 @@
|
||||
{% include 'modals/test-value-source.html' %}
|
||||
{% include 'modals/sync-clock-editor.html' %}
|
||||
{% include 'modals/weather-source-editor.html' %}
|
||||
{% include 'modals/asset-upload.html' %}
|
||||
{% include 'modals/asset-editor.html' %}
|
||||
{% include 'modals/settings.html' %}
|
||||
|
||||
{% include 'partials/tutorial-overlay.html' %}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<div id="asset-editor-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="asset-editor-title">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="asset-editor-title" data-i18n="asset.edit">Edit Asset</h2>
|
||||
<button class="modal-close-btn" onclick="closeAssetEditorModal()" data-i18n-aria-label="aria.close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="asset-editor-id">
|
||||
<div id="asset-editor-error" class="modal-error" style="display:none"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="asset-editor-name" data-i18n="asset.name">Name:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="asset.name.hint">Display name for this asset.</small>
|
||||
<input type="text" id="asset-editor-name" required maxlength="100">
|
||||
<div id="asset-editor-tags-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="asset-editor-description" data-i18n="asset.description">Description:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="asset.description.hint">Optional description for this asset.</small>
|
||||
<input type="text" id="asset-editor-description" maxlength="500">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-icon btn-secondary" onclick="closeAssetEditorModal()" title="Cancel" data-i18n-title="settings.button.cancel" data-i18n-aria-label="aria.cancel">✕</button>
|
||||
<button class="btn btn-icon btn-primary" onclick="saveAssetMetadata()" title="Save" data-i18n-title="settings.button.save" data-i18n-aria-label="aria.save">✓</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
<div id="asset-upload-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="asset-upload-title">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="asset-upload-title" data-i18n="asset.upload">Upload Asset</h2>
|
||||
<button class="modal-close-btn" onclick="closeAssetUploadModal()" data-i18n-aria-label="aria.close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="asset-upload-error" class="modal-error" style="display:none"></div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="asset-upload-name" data-i18n="asset.name">Name:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="asset.name.hint">Optional display name. If blank, derived from filename.</small>
|
||||
<input type="text" id="asset-upload-name" maxlength="100" placeholder="">
|
||||
<div id="asset-upload-tags-container"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="asset-upload-description" data-i18n="asset.description">Description:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="asset.description.hint">Optional description for this asset.</small>
|
||||
<input type="text" id="asset-upload-description" maxlength="500">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="asset.file">File:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="asset.file.hint">Select a file to upload (sound, image, video, or other).</small>
|
||||
<input type="file" id="asset-upload-file" required hidden>
|
||||
<div id="asset-upload-dropzone" class="file-dropzone" tabindex="0" role="button"
|
||||
aria-label="Choose file or drag and drop">
|
||||
<div class="file-dropzone-icon">
|
||||
<svg class="icon" viewBox="0 0 24 24" style="width:32px;height:32px">
|
||||
<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/>
|
||||
<path d="M14 2v4a2 2 0 0 0 2 2h4"/>
|
||||
<path d="M12 12v6"/><path d="m15 15-3-3-3 3"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="file-dropzone-text">
|
||||
<span class="file-dropzone-label" data-i18n="asset.drop_or_browse">Drop file here or click to browse</span>
|
||||
</div>
|
||||
<div id="asset-upload-file-info" class="file-dropzone-info" style="display:none">
|
||||
<span id="asset-upload-file-name" class="file-dropzone-filename"></span>
|
||||
<span id="asset-upload-file-size" class="file-dropzone-filesize"></span>
|
||||
<button type="button" class="file-dropzone-remove" id="asset-upload-file-remove"
|
||||
title="Remove" data-i18n-title="common.remove">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-icon btn-secondary" onclick="closeAssetUploadModal()" title="Cancel" data-i18n-title="settings.button.cancel" data-i18n-aria-label="aria.cancel">✕</button>
|
||||
<button class="btn btn-icon btn-primary" onclick="uploadAsset()" title="Upload" data-i18n-title="asset.upload" data-i18n-aria-label="asset.upload">✓</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -458,23 +458,52 @@
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.filter_list.hint">One app name per line. Use Browse to pick from running processes.</small>
|
||||
<div class="condition-field" id="css-editor-notification-filter-picker-container">
|
||||
<div class="condition-apps-header">
|
||||
<button type="button" class="btn-browse-apps" data-i18n="automations.condition.application.browse">Browse</button>
|
||||
<button type="button" class="btn btn-icon btn-secondary btn-browse-apps" data-i18n-title="automations.condition.application.browse" title="Browse"><svg class="icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg></button>
|
||||
</div>
|
||||
<textarea id="css-editor-notification-filter-list" class="condition-apps" rows="3" data-i18n-placeholder="color_strip.notification.filter_list.placeholder" placeholder="Discord Slack Telegram"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details class="form-collapse">
|
||||
<summary data-i18n="color_strip.notification.app_colors">App Colors</summary>
|
||||
<summary data-i18n="color_strip.notification.sound">Sound</summary>
|
||||
<div class="form-collapse-body">
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="color_strip.notification.app_colors.label">Color Mappings:</label>
|
||||
<label for="css-editor-notification-sound" data-i18n="color_strip.notification.sound.asset">Sound Asset:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.app_colors.hint">Per-app color overrides. Each row maps an app name to a specific color.</small>
|
||||
<div id="notification-app-colors-list"></div>
|
||||
<button type="button" class="btn btn-secondary" onclick="notificationAddAppColor()" data-i18n="color_strip.notification.app_colors.add">+ Add Mapping</button>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.sound.asset.hint">Pick a sound asset to play when a notification fires. Leave empty for silent.</small>
|
||||
<select id="css-editor-notification-sound">
|
||||
<option value="" data-i18n="color_strip.notification.sound.none">None (silent)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="css-editor-notification-volume">
|
||||
<span data-i18n="color_strip.notification.sound.volume">Volume:</span>
|
||||
<span id="css-editor-notification-volume-val">100%</span>
|
||||
</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.sound.volume.hint">Global volume for notification sounds (0–100%).</small>
|
||||
<input type="range" id="css-editor-notification-volume" min="0" max="100" step="5" value="100"
|
||||
oninput="document.getElementById('css-editor-notification-volume-val').textContent = this.value + '%'">
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="form-collapse">
|
||||
<summary data-i18n="color_strip.notification.app_overrides">Per-App Overrides</summary>
|
||||
<div class="form-collapse-body">
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label data-i18n="color_strip.notification.app_overrides.label">App Overrides:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.app_overrides.hint">Per-app overrides for color and sound. Each row can set a custom color, sound asset, and volume for a specific app.</small>
|
||||
<div id="notification-app-overrides-list"></div>
|
||||
<button type="button" class="btn btn-secondary" onclick="notificationAddAppOverride()" data-i18n="color_strip.notification.app_overrides.add">+ Add Override</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<button class="modal-close-btn" onclick="closeNotificationHistory()" title="Close" data-i18n-aria-label="aria.close">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="form-hint" data-i18n="color_strip.notification.history.hint">Recent OS notifications captured by the listener (newest first). Up to 50 entries.</p>
|
||||
<p class="form-hint" style="margin-bottom:0.75rem" data-i18n="color_strip.notification.history.hint">Recent OS notifications captured by the listener (newest first). Up to 50 entries.</p>
|
||||
<div id="notification-history-status" style="display:none;color:var(--text-muted);font-size:0.85rem;margin-bottom:0.5rem"></div>
|
||||
<div id="notification-history-list" style="max-height:340px;overflow-y:auto;border:1px solid var(--border-color);border-radius:4px;padding:0.25rem 0"></div>
|
||||
</div>
|
||||
|
||||
@@ -78,28 +78,15 @@
|
||||
<!-- Static image fields -->
|
||||
<div id="stream-static-image-fields" style="display: none;">
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="stream-image-source" data-i18n="streams.image_source">Image Source:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="streams.image_source.hint">Enter a URL (http/https) or local file path to an image</small>
|
||||
<input type="text" id="stream-image-source" data-i18n-placeholder="streams.image_source.placeholder" placeholder="https://example.com/image.jpg or C:\path\to\image.png">
|
||||
<label for="stream-image-asset" data-i18n="streams.image_asset">Image Asset:</label>
|
||||
<select id="stream-image-asset"></select>
|
||||
</div>
|
||||
<div id="stream-image-preview-container" class="image-preview-container" style="display: none;">
|
||||
<img id="stream-image-preview" class="stream-image-preview" src="" alt="Preview">
|
||||
<div id="stream-image-info" class="stream-image-info"></div>
|
||||
</div>
|
||||
<div id="stream-image-validation-status" class="validation-status" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div id="stream-video-fields" style="display: none;">
|
||||
<div class="form-group">
|
||||
<div class="label-row">
|
||||
<label for="stream-video-url" data-i18n="picture_source.video.url">Video URL:</label>
|
||||
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||
</div>
|
||||
<small class="input-hint" style="display:none" data-i18n="picture_source.video.url.hint">Local file path, HTTP URL, or YouTube URL</small>
|
||||
<input type="text" id="stream-video-url" data-i18n-placeholder="picture_source.video.url.placeholder" placeholder="https://example.com/video.mp4">
|
||||
<label for="stream-video-asset" data-i18n="streams.video_asset">Video Asset:</label>
|
||||
<select id="stream-video-asset"></select>
|
||||
</div>
|
||||
<div class="form-group settings-toggle-group">
|
||||
<label data-i18n="picture_source.video.loop">Loop:</label>
|
||||
|
||||
Reference in New Issue
Block a user