Compare commits
5 Commits
v0.2.1-alp
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 85b886abf8 | |||
| a5e7a4e52f | |||
| 82ce2a7e2b | |||
| 2eeae4a7c1 | |||
| f4da47ca2b |
@@ -12,10 +12,13 @@ jobs:
|
|||||||
outputs:
|
outputs:
|
||||||
release_id: ${{ steps.create.outputs.release_id }}
|
release_id: ${{ steps.create.outputs.release_id }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Create Gitea release
|
- name: Create Gitea release
|
||||||
id: create
|
id: create
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
TAG="${{ gitea.ref_name }}"
|
TAG="${{ gitea.ref_name }}"
|
||||||
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||||
@@ -30,12 +33,31 @@ jobs:
|
|||||||
REPO=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
REPO=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||||
DOCKER_IMAGE="${SERVER_HOST}/${REPO}"
|
DOCKER_IMAGE="${SERVER_HOST}/${REPO}"
|
||||||
|
|
||||||
|
# Scan for RELEASE_NOTES.md (check repo root first, then recursively)
|
||||||
|
NOTES_FILE=$(find . -maxdepth 3 -name "RELEASE_NOTES.md" -type f | head -1)
|
||||||
|
if [ -n "$NOTES_FILE" ]; then
|
||||||
|
export RELEASE_NOTES=$(cat "$NOTES_FILE")
|
||||||
|
echo "Found release notes: $NOTES_FILE"
|
||||||
|
else
|
||||||
|
export RELEASE_NOTES=""
|
||||||
|
echo "No RELEASE_NOTES.md found"
|
||||||
|
fi
|
||||||
|
|
||||||
# Build release body via Python to avoid YAML escaping issues
|
# Build release body via Python to avoid YAML escaping issues
|
||||||
BODY_JSON=$(python3 -c "
|
BODY_JSON=$(python3 -c "
|
||||||
import json, sys
|
import json, sys, os, textwrap
|
||||||
|
|
||||||
tag = '$TAG'
|
tag = '$TAG'
|
||||||
image = '$DOCKER_IMAGE'
|
image = '$DOCKER_IMAGE'
|
||||||
body = f'''## Downloads
|
release_notes = os.environ.get('RELEASE_NOTES', '')
|
||||||
|
|
||||||
|
sections = []
|
||||||
|
|
||||||
|
if release_notes.strip():
|
||||||
|
sections.append(release_notes.strip())
|
||||||
|
|
||||||
|
sections.append(textwrap.dedent(f'''
|
||||||
|
## Downloads
|
||||||
|
|
||||||
| Platform | File | Description |
|
| Platform | File | Description |
|
||||||
|----------|------|-------------|
|
|----------|------|-------------|
|
||||||
@@ -58,9 +80,9 @@ jobs:
|
|||||||
1. Change the default API key in config/default_config.yaml
|
1. Change the default API key in config/default_config.yaml
|
||||||
2. Open http://localhost:8080 and discover your WLED devices
|
2. Open http://localhost:8080 and discover your WLED devices
|
||||||
3. See INSTALLATION.md for detailed configuration
|
3. See INSTALLATION.md for detailed configuration
|
||||||
'''
|
''').strip())
|
||||||
import textwrap
|
|
||||||
print(json.dumps(textwrap.dedent(body).strip()))
|
print(json.dumps('\n\n'.join(sections)))
|
||||||
")
|
")
|
||||||
|
|
||||||
RELEASE=$(curl -s -X POST "$BASE_URL/releases" \
|
RELEASE=$(curl -s -X POST "$BASE_URL/releases" \
|
||||||
@@ -126,7 +148,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Attach assets to release
|
- name: Attach assets to release
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
||||||
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||||
@@ -196,7 +218,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Attach tarball to release
|
- name: Attach tarball to release
|
||||||
env:
|
env:
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
||||||
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||||
@@ -248,7 +270,7 @@ jobs:
|
|||||||
id: docker-login
|
id: docker-login
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: |
|
run: |
|
||||||
echo "${{ secrets.GITEA_TOKEN }}" | docker login \
|
echo "${{ secrets.DEPLOY_TOKEN }}" | docker login \
|
||||||
"${{ steps.meta.outputs.server_host }}" \
|
"${{ steps.meta.outputs.server_host }}" \
|
||||||
-u "${{ gitea.actor }}" --password-stdin
|
-u "${{ gitea.actor }}" --password-stdin
|
||||||
|
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ Screen capture from inside a container requires X11 access. Uncomment `network_m
|
|||||||
Optional extras:
|
Optional extras:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install ".[camera]" # Webcam capture via OpenCV
|
|
||||||
pip install ".[perf]" # DXCam, BetterCam, WGC (Windows only)
|
pip install ".[perf]" # DXCam, BetterCam, WGC (Windows only)
|
||||||
pip install ".[notifications]" # OS notification capture
|
pip install ".[notifications]" # OS notification capture
|
||||||
|
pip install ".[tray]" # System tray icon (Windows only)
|
||||||
pip install ".[dev]" # pytest, black, ruff (development)
|
pip install ".[dev]" # pytest, black, ruff (development)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -155,13 +155,13 @@ Option A -- edit the config file:
|
|||||||
# server/config/default_config.yaml
|
# server/config/default_config.yaml
|
||||||
auth:
|
auth:
|
||||||
api_keys:
|
api_keys:
|
||||||
main: "your-secure-key-here" # replace the dev key
|
dev: "your-secure-key-here" # replace the dev key
|
||||||
```
|
```
|
||||||
|
|
||||||
Option B -- set an environment variable:
|
Option B -- set an environment variable:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export WLED_AUTH__API_KEYS__main="your-secure-key-here"
|
export WLED_AUTH__API_KEYS__dev="your-secure-key-here"
|
||||||
```
|
```
|
||||||
|
|
||||||
Generate a random key:
|
Generate a random key:
|
||||||
@@ -257,6 +257,7 @@ See [`server/.env.example`](server/.env.example) for every available variable wi
|
|||||||
| `WLED_SERVER__LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
| `WLED_SERVER__LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
|
||||||
| `WLED_SERVER__CORS_ORIGINS` | `["http://localhost:8080"]` | Allowed CORS origins (JSON array) |
|
| `WLED_SERVER__CORS_ORIGINS` | `["http://localhost:8080"]` | Allowed CORS origins (JSON array) |
|
||||||
| `WLED_AUTH__API_KEYS` | `{"dev":"development-key..."}` | API keys (JSON object) |
|
| `WLED_AUTH__API_KEYS` | `{"dev":"development-key..."}` | API keys (JSON object) |
|
||||||
|
| `WLED_STORAGE__DATABASE_FILE` | `data/ledgrab.db` | SQLite database path |
|
||||||
| `WLED_MQTT__ENABLED` | `false` | Enable MQTT for HA auto-discovery |
|
| `WLED_MQTT__ENABLED` | `false` | Enable MQTT for HA auto-discovery |
|
||||||
| `WLED_MQTT__BROKER_HOST` | `localhost` | MQTT broker address |
|
| `WLED_MQTT__BROKER_HOST` | `localhost` | MQTT broker address |
|
||||||
| `WLED_DEMO` | `false` | Enable demo mode (sandbox with virtual devices) |
|
| `WLED_DEMO` | `false` | Enable demo mode (sandbox with virtual devices) |
|
||||||
|
|||||||
15
RELEASE_NOTES.md
Normal file
15
RELEASE_NOTES.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
## v0.2.2 (2025-03-25)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- Add 4 built-in gradients, searchable gradient picker, cleaner modal titles ([a5e7a4e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/a5e7a4e))
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>All Commits</summary>
|
||||||
|
|
||||||
|
| Hash | Message | Author |
|
||||||
|
|------|---------|--------|
|
||||||
|
| [a5e7a4e](https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed/commit/a5e7a4e) | feat: add 4 built-in gradients, searchable gradient picker, cleaner modal titles | alexei.dolgolyov |
|
||||||
|
|
||||||
|
</details>
|
||||||
@@ -12,22 +12,9 @@
|
|||||||
# API keys are required. Format: JSON object {"label": "key"}.
|
# API keys are required. Format: JSON object {"label": "key"}.
|
||||||
# WLED_AUTH__API_KEYS={"dev": "development-key-change-in-production"}
|
# WLED_AUTH__API_KEYS={"dev": "development-key-change-in-production"}
|
||||||
|
|
||||||
# ── Storage paths ───────────────────────────────────────
|
# ── Storage ────────────────────────────────────────────
|
||||||
# All paths are relative to the server working directory.
|
# All data is stored in a single SQLite database.
|
||||||
# WLED_STORAGE__DEVICES_FILE=data/devices.json
|
# WLED_STORAGE__DATABASE_FILE=data/ledgrab.db
|
||||||
# WLED_STORAGE__TEMPLATES_FILE=data/capture_templates.json
|
|
||||||
# WLED_STORAGE__POSTPROCESSING_TEMPLATES_FILE=data/postprocessing_templates.json
|
|
||||||
# WLED_STORAGE__PICTURE_SOURCES_FILE=data/picture_sources.json
|
|
||||||
# WLED_STORAGE__OUTPUT_TARGETS_FILE=data/output_targets.json
|
|
||||||
# WLED_STORAGE__PATTERN_TEMPLATES_FILE=data/pattern_templates.json
|
|
||||||
# WLED_STORAGE__COLOR_STRIP_SOURCES_FILE=data/color_strip_sources.json
|
|
||||||
# WLED_STORAGE__AUDIO_SOURCES_FILE=data/audio_sources.json
|
|
||||||
# WLED_STORAGE__AUDIO_TEMPLATES_FILE=data/audio_templates.json
|
|
||||||
# WLED_STORAGE__VALUE_SOURCES_FILE=data/value_sources.json
|
|
||||||
# WLED_STORAGE__AUTOMATIONS_FILE=data/automations.json
|
|
||||||
# WLED_STORAGE__SCENE_PRESETS_FILE=data/scene_presets.json
|
|
||||||
# WLED_STORAGE__COLOR_STRIP_PROCESSING_TEMPLATES_FILE=data/color_strip_processing_templates.json
|
|
||||||
# WLED_STORAGE__SYNC_CLOCKS_FILE=data/sync_clocks.json
|
|
||||||
|
|
||||||
# ── MQTT (optional) ────────────────────────────────────
|
# ── MQTT (optional) ────────────────────────────────────
|
||||||
# WLED_MQTT__ENABLED=false
|
# WLED_MQTT__ENABLED=false
|
||||||
|
|||||||
19
server/package-lock.json
generated
19
server/package-lock.json
generated
@@ -10,7 +10,8 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"elkjs": "^0.11.1"
|
"elkjs": "^0.11.1",
|
||||||
|
"marked": "^17.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.27.4",
|
"esbuild": "^0.27.4",
|
||||||
@@ -495,6 +496,17 @@
|
|||||||
"@esbuild/win32-x64": "0.27.4"
|
"@esbuild/win32-x64": "0.27.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "17.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.5.tgz",
|
||||||
|
"integrity": "sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
@@ -744,6 +756,11 @@
|
|||||||
"@esbuild/win32-x64": "0.27.4"
|
"@esbuild/win32-x64": "0.27.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"marked": {
|
||||||
|
"version": "17.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.5.tgz",
|
||||||
|
"integrity": "sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg=="
|
||||||
|
},
|
||||||
"typescript": {
|
"typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chart.js": "^4.5.1",
|
"chart.js": "^4.5.1",
|
||||||
"elkjs": "^0.11.1"
|
"elkjs": "^0.11.1",
|
||||||
|
"marked": "^17.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -689,6 +689,25 @@ textarea:focus-visible {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-select-search {
|
||||||
|
width: calc(100% - 12px);
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 6px 6px 0;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.icon-select-search::placeholder {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.icon-select-search:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
.icon-select-grid {
|
.icon-select-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-auto-rows: 1fr;
|
grid-auto-rows: 1fr;
|
||||||
@@ -820,6 +839,7 @@ textarea:focus-visible {
|
|||||||
/* Override inline columns — use responsive auto-fill */
|
/* Override inline columns — use responsive auto-fill */
|
||||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)) !important;
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)) !important;
|
||||||
}
|
}
|
||||||
|
.icon-select-popup .icon-select-cell.disabled,
|
||||||
.type-picker-dialog .icon-select-cell.disabled {
|
.type-picker-dialog .icon-select-cell.disabled {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|||||||
@@ -448,6 +448,79 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Release notes content ─────────────────────────────────── */
|
||||||
|
|
||||||
|
.release-notes-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-color);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes-content h2,
|
||||||
|
.release-notes-content h3,
|
||||||
|
.release-notes-content h4 {
|
||||||
|
margin: 1.2em 0 0.4em;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes-content h2 { font-size: 1.2rem; }
|
||||||
|
.release-notes-content h3 { font-size: 1.05rem; }
|
||||||
|
.release-notes-content h4 { font-size: 0.95rem; }
|
||||||
|
|
||||||
|
.release-notes-content pre {
|
||||||
|
background: #0d0d0d;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes-content code {
|
||||||
|
background: var(--bg-tertiary, #2a2a2a);
|
||||||
|
padding: 0.15em 0.4em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.88em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes-content pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes-content a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes-content hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes-content table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes-content th,
|
||||||
|
.release-notes-content td {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.release-notes-content th {
|
||||||
|
background: var(--bg-tertiary, #2a2a2a);
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Log viewer base ───────────────────────────────────────── */
|
/* ── Log viewer base ───────────────────────────────────────── */
|
||||||
|
|
||||||
.log-viewer-output {
|
.log-viewer-output {
|
||||||
|
|||||||
@@ -203,6 +203,7 @@ import {
|
|||||||
loadUpdateStatus, initUpdateListener, checkForUpdates,
|
loadUpdateStatus, initUpdateListener, checkForUpdates,
|
||||||
loadUpdateSettings, saveUpdateSettings, dismissUpdate,
|
loadUpdateSettings, saveUpdateSettings, dismissUpdate,
|
||||||
initUpdateSettingsPanel, applyUpdate,
|
initUpdateSettingsPanel, applyUpdate,
|
||||||
|
openReleaseNotes, closeReleaseNotes,
|
||||||
} from './features/update.ts';
|
} from './features/update.ts';
|
||||||
|
|
||||||
// ─── Register all HTML onclick / onchange / onfocus globals ───
|
// ─── Register all HTML onclick / onchange / onfocus globals ───
|
||||||
@@ -572,6 +573,8 @@ Object.assign(window, {
|
|||||||
dismissUpdate,
|
dismissUpdate,
|
||||||
initUpdateSettingsPanel,
|
initUpdateSettingsPanel,
|
||||||
applyUpdate,
|
applyUpdate,
|
||||||
|
openReleaseNotes,
|
||||||
|
closeReleaseNotes,
|
||||||
|
|
||||||
// appearance
|
// appearance
|
||||||
applyStylePreset,
|
applyStylePreset,
|
||||||
|
|||||||
@@ -228,8 +228,9 @@ export async function loadServerInfo() {
|
|||||||
const wasOffline = _serverOnline === false;
|
const wasOffline = _serverOnline === false;
|
||||||
_setConnectionState(true);
|
_setConnectionState(true);
|
||||||
if (wasOffline) {
|
if (wasOffline) {
|
||||||
// Server came back — reload data
|
// Server came back — hard reload to ensure fresh data
|
||||||
window.dispatchEvent(new CustomEvent('server:reconnected'));
|
location.reload();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth mode detection
|
// Auth mode detection
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ export interface IconSelectOpts {
|
|||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
columns?: number;
|
columns?: number;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
searchable?: boolean;
|
||||||
|
searchPlaceholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class IconSelect {
|
export class IconSelect {
|
||||||
@@ -66,12 +68,15 @@ export class IconSelect {
|
|||||||
_onChange: ((value: string) => void) | undefined;
|
_onChange: ((value: string) => void) | undefined;
|
||||||
_columns: number;
|
_columns: number;
|
||||||
_placeholder: string;
|
_placeholder: string;
|
||||||
|
_searchable: boolean;
|
||||||
|
_searchPlaceholder: string;
|
||||||
_trigger: HTMLButtonElement;
|
_trigger: HTMLButtonElement;
|
||||||
_popup: HTMLDivElement;
|
_popup: HTMLDivElement;
|
||||||
|
_searchInput: HTMLInputElement | null = null;
|
||||||
_scrollHandler: (() => void) | null = null;
|
_scrollHandler: (() => void) | null = null;
|
||||||
_scrollTargets: (HTMLElement | Window)[] = [];
|
_scrollTargets: (HTMLElement | Window)[] = [];
|
||||||
|
|
||||||
constructor({ target, items, onChange, columns = 2, placeholder = '' }: IconSelectOpts) {
|
constructor({ target, items, onChange, columns = 2, placeholder = '', searchable = false, searchPlaceholder = 'Filter…' }: IconSelectOpts) {
|
||||||
_ensureGlobalListener();
|
_ensureGlobalListener();
|
||||||
|
|
||||||
this._select = target;
|
this._select = target;
|
||||||
@@ -79,6 +84,8 @@ export class IconSelect {
|
|||||||
this._onChange = onChange;
|
this._onChange = onChange;
|
||||||
this._columns = columns;
|
this._columns = columns;
|
||||||
this._placeholder = placeholder;
|
this._placeholder = placeholder;
|
||||||
|
this._searchable = searchable;
|
||||||
|
this._searchPlaceholder = searchPlaceholder;
|
||||||
|
|
||||||
// Hide the native select
|
// Hide the native select
|
||||||
this._select.style.display = 'none';
|
this._select.style.display = 'none';
|
||||||
@@ -100,6 +107,13 @@ export class IconSelect {
|
|||||||
this._popup.innerHTML = this._buildGrid();
|
this._popup.innerHTML = this._buildGrid();
|
||||||
document.body.appendChild(this._popup);
|
document.body.appendChild(this._popup);
|
||||||
|
|
||||||
|
this._bindPopupEvents();
|
||||||
|
|
||||||
|
// Sync to current select value
|
||||||
|
this._syncTrigger();
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindPopupEvents() {
|
||||||
// Bind item clicks
|
// Bind item clicks
|
||||||
this._popup.querySelectorAll('.icon-select-cell').forEach(cell => {
|
this._popup.querySelectorAll('.icon-select-cell').forEach(cell => {
|
||||||
cell.addEventListener('click', () => {
|
cell.addEventListener('click', () => {
|
||||||
@@ -109,20 +123,33 @@ export class IconSelect {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync to current select value
|
// Bind search input
|
||||||
this._syncTrigger();
|
this._searchInput = this._popup.querySelector('.icon-select-search') as HTMLInputElement | null;
|
||||||
|
if (this._searchInput) {
|
||||||
|
this._searchInput.addEventListener('input', () => {
|
||||||
|
const q = this._searchInput!.value.toLowerCase().trim();
|
||||||
|
this._popup.querySelectorAll('.icon-select-cell').forEach(cell => {
|
||||||
|
const el = cell as HTMLElement;
|
||||||
|
el.classList.toggle('disabled', !!q && !el.dataset.search!.includes(q));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildGrid() {
|
_buildGrid() {
|
||||||
const cells = this._items.map(item => {
|
const cells = this._items.map(item => {
|
||||||
return `<div class="icon-select-cell" data-value="${item.value}">
|
const search = (item.label + ' ' + (item.desc || '')).toLowerCase();
|
||||||
|
return `<div class="icon-select-cell" data-value="${item.value}" data-search="${search}">
|
||||||
<span class="icon-select-cell-icon">${item.icon}</span>
|
<span class="icon-select-cell-icon">${item.icon}</span>
|
||||||
<span class="icon-select-cell-label">${item.label}</span>
|
<span class="icon-select-cell-label">${item.label}</span>
|
||||||
${item.desc ? `<span class="icon-select-cell-desc">${item.desc}</span>` : ''}
|
${item.desc ? `<span class="icon-select-cell-desc">${item.desc}</span>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
return `<div class="icon-select-grid" style="grid-template-columns:repeat(${this._columns},1fr)">${cells}</div>`;
|
const searchHTML = this._searchable
|
||||||
|
? `<input class="icon-select-search" type="text" placeholder="${this._searchPlaceholder}" autocomplete="off">`
|
||||||
|
: '';
|
||||||
|
return searchHTML + `<div class="icon-select-grid" style="grid-template-columns:repeat(${this._columns},1fr)">${cells}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
_syncTrigger() {
|
_syncTrigger() {
|
||||||
@@ -184,6 +211,13 @@ export class IconSelect {
|
|||||||
this._positionPopup();
|
this._positionPopup();
|
||||||
this._popup.classList.add('open');
|
this._popup.classList.add('open');
|
||||||
this._addScrollListener();
|
this._addScrollListener();
|
||||||
|
if (this._searchInput) {
|
||||||
|
this._searchInput.value = '';
|
||||||
|
this._popup.querySelectorAll('.icon-select-cell').forEach(cell => {
|
||||||
|
(cell as HTMLElement).classList.remove('disabled');
|
||||||
|
});
|
||||||
|
requestAnimationFrame(() => desktopFocus(this._searchInput!));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,12 +267,7 @@ export class IconSelect {
|
|||||||
updateItems(items: IconSelectItem[]) {
|
updateItems(items: IconSelectItem[]) {
|
||||||
this._items = items;
|
this._items = items;
|
||||||
this._popup.innerHTML = this._buildGrid();
|
this._popup.innerHTML = this._buildGrid();
|
||||||
this._popup.querySelectorAll('.icon-select-cell').forEach(cell => {
|
this._bindPopupEvents();
|
||||||
cell.addEventListener('click', () => {
|
|
||||||
this.setValue((cell as HTMLElement).dataset.value!, true);
|
|
||||||
this._popup.classList.remove('open');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this._syncTrigger();
|
this._syncTrigger();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ function _ensureEffectPaletteIconSelect() {
|
|||||||
const items = _buildGradientEntityItems();
|
const items = _buildGradientEntityItems();
|
||||||
_syncSelectOptions(sel, items);
|
_syncSelectOptions(sel, items);
|
||||||
if (_effectPaletteIconSelect) { _effectPaletteIconSelect.updateItems(items); return; }
|
if (_effectPaletteIconSelect) { _effectPaletteIconSelect.updateItems(items); return; }
|
||||||
_effectPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2 });
|
_effectPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2, searchable: true, searchPlaceholder: t('palette.search') });
|
||||||
}
|
}
|
||||||
|
|
||||||
function _ensureGradientEasingIconSelect() {
|
function _ensureGradientEasingIconSelect() {
|
||||||
@@ -468,7 +468,7 @@ function _ensureAudioPaletteIconSelect() {
|
|||||||
const items = _buildGradientEntityItems();
|
const items = _buildGradientEntityItems();
|
||||||
_syncSelectOptions(sel, items);
|
_syncSelectOptions(sel, items);
|
||||||
if (_audioPaletteIconSelect) { _audioPaletteIconSelect.updateItems(items); return; }
|
if (_audioPaletteIconSelect) { _audioPaletteIconSelect.updateItems(items); return; }
|
||||||
_audioPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2 });
|
_audioPaletteIconSelect = new IconSelect({ target: sel, items, columns: 2, searchable: true, searchPlaceholder: t('palette.search') });
|
||||||
}
|
}
|
||||||
|
|
||||||
function _ensureAudioVizIconSelect() {
|
function _ensureAudioVizIconSelect() {
|
||||||
@@ -507,7 +507,7 @@ function _ensureGradientPresetIconSelect() {
|
|||||||
const items = _buildGradientEntityItems();
|
const items = _buildGradientEntityItems();
|
||||||
_syncSelectOptions(sel, items);
|
_syncSelectOptions(sel, items);
|
||||||
if (_gradientPresetIconSelect) { _gradientPresetIconSelect.updateItems(items); return; }
|
if (_gradientPresetIconSelect) { _gradientPresetIconSelect.updateItems(items); return; }
|
||||||
_gradientPresetIconSelect = new IconSelect({ target: sel, items, columns: 3 });
|
_gradientPresetIconSelect = new IconSelect({ target: sel, items, columns: 3, searchable: true, searchPlaceholder: t('palette.search') });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rebuild the gradient picker after entity changes. */
|
/** Rebuild the gradient picker after entity changes. */
|
||||||
@@ -1728,12 +1728,14 @@ export async function showCSSEditor(cssId: any = null, cloneData: any = null, pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
await _populateFromCSS(css);
|
await _populateFromCSS(css);
|
||||||
(document.getElementById('css-editor-title') as HTMLElement).innerHTML = `${ICON_FILM} ${t('color_strip.edit')}`;
|
const editIcon = getColorStripIcon(css.source_type);
|
||||||
|
(document.getElementById('css-editor-title') as HTMLElement).innerHTML = `${editIcon} ${t('color_strip.edit')} ${t(`color_strip.type.${css.source_type}`)}`;
|
||||||
} else if (cloneData) {
|
} else if (cloneData) {
|
||||||
(document.getElementById('css-editor-id') as HTMLInputElement).value = '';
|
(document.getElementById('css-editor-id') as HTMLInputElement).value = '';
|
||||||
(document.getElementById('css-editor-name') as HTMLInputElement).value = (cloneData.name || '') + ' (Copy)';
|
(document.getElementById('css-editor-name') as HTMLInputElement).value = (cloneData.name || '') + ' (Copy)';
|
||||||
await _populateFromCSS(cloneData);
|
await _populateFromCSS(cloneData);
|
||||||
(document.getElementById('css-editor-title') as HTMLElement).innerHTML = `${ICON_FILM} ${t('color_strip.add')}`;
|
const cloneIcon = getColorStripIcon(cloneData.source_type);
|
||||||
|
(document.getElementById('css-editor-title') as HTMLElement).innerHTML = `${cloneIcon} ${t('color_strip.add')} ${t(`color_strip.type.${cloneData.source_type}`)}`;
|
||||||
} else {
|
} else {
|
||||||
(document.getElementById('css-editor-id') as HTMLInputElement).value = '';
|
(document.getElementById('css-editor-id') as HTMLInputElement).value = '';
|
||||||
(document.getElementById('css-editor-name') as HTMLInputElement).value = '';
|
(document.getElementById('css-editor-name') as HTMLInputElement).value = '';
|
||||||
@@ -1748,7 +1750,7 @@ export async function showCSSEditor(cssId: any = null, cloneData: any = null, pr
|
|||||||
}
|
}
|
||||||
|
|
||||||
const typeIcon = getColorStripIcon(effectiveType);
|
const typeIcon = getColorStripIcon(effectiveType);
|
||||||
(document.getElementById('css-editor-title') as HTMLElement).innerHTML = `${typeIcon} ${t('color_strip.add')}: ${t(`color_strip.type.${effectiveType}`)}`;
|
(document.getElementById('css-editor-title') as HTMLElement).innerHTML = `${typeIcon} ${t('color_strip.add')} ${t(`color_strip.type.${effectiveType}`)}`;
|
||||||
_autoGenerateCSSName();
|
_autoGenerateCSSName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ interface UpdateStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _lastStatus: UpdateStatus | null = null;
|
let _lastStatus: UpdateStatus | null = null;
|
||||||
|
let _releaseNotesBody = '';
|
||||||
|
|
||||||
// ─── Version badge highlight ────────────────────────────────
|
// ─── Version badge highlight ────────────────────────────────
|
||||||
|
|
||||||
@@ -383,18 +384,33 @@ function _renderUpdatePanel(status: UpdateStatus): void {
|
|||||||
progressBar.parentElement!.style.display = show ? '' : 'none';
|
progressBar.parentElement!.style.display = show ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release notes preview
|
// Release notes button visibility
|
||||||
const notesEl = document.getElementById('update-release-notes');
|
const notesGroup = document.getElementById('update-release-notes-group');
|
||||||
if (notesEl) {
|
if (notesGroup) {
|
||||||
if (status.has_update && status.release && status.release.body) {
|
if (status.has_update && status.release && status.release.body) {
|
||||||
const truncated = status.release.body.length > 500
|
_releaseNotesBody = status.release.body;
|
||||||
? status.release.body.slice(0, 500) + '...'
|
notesGroup.style.display = '';
|
||||||
: status.release.body;
|
|
||||||
notesEl.textContent = truncated;
|
|
||||||
notesEl.parentElement!.style.display = '';
|
|
||||||
} else {
|
} else {
|
||||||
notesEl.textContent = '';
|
_releaseNotesBody = '';
|
||||||
notesEl.parentElement!.style.display = 'none';
|
notesGroup.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Release Notes Overlay ─────────────────────────────────
|
||||||
|
|
||||||
|
export function openReleaseNotes(): void {
|
||||||
|
const overlay = document.getElementById('release-notes-overlay');
|
||||||
|
const content = document.getElementById('release-notes-content');
|
||||||
|
if (overlay && content) {
|
||||||
|
import('marked').then(({ marked }) => {
|
||||||
|
content.innerHTML = marked.parse(_releaseNotesBody) as string;
|
||||||
|
overlay.style.display = 'flex';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeReleaseNotes(): void {
|
||||||
|
const overlay = document.getElementById('release-notes-overlay');
|
||||||
|
if (overlay) overlay.style.display = 'none';
|
||||||
|
}
|
||||||
|
|||||||
@@ -911,8 +911,8 @@
|
|||||||
"aria.next": "Next",
|
"aria.next": "Next",
|
||||||
"aria.hint": "Show hint",
|
"aria.hint": "Show hint",
|
||||||
"color_strip.select_type": "Select Color Strip Type",
|
"color_strip.select_type": "Select Color Strip Type",
|
||||||
"color_strip.add": "Add Color Strip Source",
|
"color_strip.add": "Add",
|
||||||
"color_strip.edit": "Edit Color Strip Source",
|
"color_strip.edit": "Edit",
|
||||||
"color_strip.name": "Name:",
|
"color_strip.name": "Name:",
|
||||||
"color_strip.name.placeholder": "Wall Strip",
|
"color_strip.name.placeholder": "Wall Strip",
|
||||||
"color_strip.picture_source": "Picture Source:",
|
"color_strip.picture_source": "Picture Source:",
|
||||||
@@ -1931,6 +1931,7 @@
|
|||||||
"update.last_check": "Last check",
|
"update.last_check": "Last check",
|
||||||
"update.never": "never",
|
"update.never": "never",
|
||||||
"update.release_notes": "Release Notes",
|
"update.release_notes": "Release Notes",
|
||||||
|
"update.view_release_notes": "View Release Notes",
|
||||||
"update.auto_check_label": "Auto-Check Settings",
|
"update.auto_check_label": "Auto-Check Settings",
|
||||||
"update.auto_check_hint": "Periodically check for new releases in the background.",
|
"update.auto_check_hint": "Periodically check for new releases in the background.",
|
||||||
"update.enable": "Enable auto-check",
|
"update.enable": "Enable auto-check",
|
||||||
|
|||||||
@@ -911,8 +911,8 @@
|
|||||||
"aria.next": "Вперёд",
|
"aria.next": "Вперёд",
|
||||||
"aria.hint": "Показать подсказку",
|
"aria.hint": "Показать подсказку",
|
||||||
"color_strip.select_type": "Выберите тип цветовой полосы",
|
"color_strip.select_type": "Выберите тип цветовой полосы",
|
||||||
"color_strip.add": "Добавить источник цветовой полосы",
|
"color_strip.add": "Добавить",
|
||||||
"color_strip.edit": "Редактировать источник цветовой полосы",
|
"color_strip.edit": "Редактировать",
|
||||||
"color_strip.name": "Название:",
|
"color_strip.name": "Название:",
|
||||||
"color_strip.name.placeholder": "Настенная полоса",
|
"color_strip.name.placeholder": "Настенная полоса",
|
||||||
"color_strip.picture_source": "Источник изображения:",
|
"color_strip.picture_source": "Источник изображения:",
|
||||||
@@ -1860,6 +1860,7 @@
|
|||||||
"update.last_check": "Последняя проверка",
|
"update.last_check": "Последняя проверка",
|
||||||
"update.never": "никогда",
|
"update.never": "никогда",
|
||||||
"update.release_notes": "Примечания к релизу",
|
"update.release_notes": "Примечания к релизу",
|
||||||
|
"update.view_release_notes": "Открыть примечания к релизу",
|
||||||
"update.auto_check_label": "Автоматическая проверка",
|
"update.auto_check_label": "Автоматическая проверка",
|
||||||
"update.auto_check_hint": "Периодически проверять наличие новых версий в фоновом режиме.",
|
"update.auto_check_hint": "Периодически проверять наличие новых версий в фоновом режиме.",
|
||||||
"update.enable": "Включить автопроверку",
|
"update.enable": "Включить автопроверку",
|
||||||
|
|||||||
@@ -911,8 +911,8 @@
|
|||||||
"aria.next": "下一个",
|
"aria.next": "下一个",
|
||||||
"aria.hint": "显示提示",
|
"aria.hint": "显示提示",
|
||||||
"color_strip.select_type": "选择色带类型",
|
"color_strip.select_type": "选择色带类型",
|
||||||
"color_strip.add": "添加色带源",
|
"color_strip.add": "添加",
|
||||||
"color_strip.edit": "编辑色带源",
|
"color_strip.edit": "编辑",
|
||||||
"color_strip.name": "名称:",
|
"color_strip.name": "名称:",
|
||||||
"color_strip.name.placeholder": "墙壁灯带",
|
"color_strip.name.placeholder": "墙壁灯带",
|
||||||
"color_strip.picture_source": "图片源:",
|
"color_strip.picture_source": "图片源:",
|
||||||
@@ -1858,6 +1858,7 @@
|
|||||||
"update.last_check": "上次检查",
|
"update.last_check": "上次检查",
|
||||||
"update.never": "从未",
|
"update.never": "从未",
|
||||||
"update.release_notes": "发布说明",
|
"update.release_notes": "发布说明",
|
||||||
|
"update.view_release_notes": "查看发布说明",
|
||||||
"update.auto_check_label": "自动检查设置",
|
"update.auto_check_label": "自动检查设置",
|
||||||
"update.auto_check_hint": "在后台定期检查新版本。",
|
"update.auto_check_hint": "在后台定期检查新版本。",
|
||||||
"update.enable": "启用自动检查",
|
"update.enable": "启用自动检查",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""Gradient storage with built-in seeding.
|
"""Gradient storage with built-in seeding.
|
||||||
|
|
||||||
Provides CRUD for gradient entities. On first run (empty/missing data),
|
Provides CRUD for gradient entities. On startup, seeds any missing
|
||||||
seeds 8 built-in gradients matching the legacy hardcoded palettes.
|
built-in gradients. Built-in gradients are read-only and cannot be
|
||||||
Built-in gradients are read-only and cannot be deleted or modified.
|
deleted or modified.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
@@ -36,6 +36,16 @@ _BUILTIN_DEFS = {
|
|||||||
(0.75, 255, 192, 64), (1.0, 255, 255, 192),
|
(0.75, 255, 192, 64), (1.0, 255, 255, 192),
|
||||||
],
|
],
|
||||||
"ice": [(0, 0, 0, 64), (0.33, 0, 64, 192), (0.66, 128, 192, 255), (1.0, 240, 248, 255)],
|
"ice": [(0, 0, 0, 64), (0.33, 0, 64, 192), (0.66, 128, 192, 255), (1.0, 240, 248, 255)],
|
||||||
|
"warm": [(0, 255, 255, 80), (0.33, 255, 160, 0), (0.67, 255, 60, 0), (1.0, 160, 0, 0)],
|
||||||
|
"cool": [(0, 0, 255, 200), (0.33, 0, 120, 255), (0.67, 60, 0, 255), (1.0, 120, 0, 180)],
|
||||||
|
"neon": [
|
||||||
|
(0, 255, 0, 200), (0.25, 0, 255, 255), (0.5, 0, 255, 50),
|
||||||
|
(0.75, 255, 255, 0), (1.0, 255, 0, 100),
|
||||||
|
],
|
||||||
|
"pastel": [
|
||||||
|
(0, 255, 180, 180), (0.2, 255, 220, 160), (0.4, 255, 255, 180),
|
||||||
|
(0.6, 180, 255, 200), (0.8, 180, 200, 255), (1.0, 220, 180, 255),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -50,14 +60,16 @@ class GradientStore(BaseSqliteStore[Gradient]):
|
|||||||
|
|
||||||
def __init__(self, db: Database):
|
def __init__(self, db: Database):
|
||||||
super().__init__(db, Gradient.from_dict)
|
super().__init__(db, Gradient.from_dict)
|
||||||
if not self._items:
|
self._seed_missing_builtins()
|
||||||
self._seed_builtins()
|
|
||||||
|
|
||||||
def _seed_builtins(self) -> None:
|
def _seed_missing_builtins(self) -> None:
|
||||||
"""Create the 8 built-in gradients on first run."""
|
"""Seed any built-in gradients not yet in the store."""
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
added = 0
|
||||||
for name, tuples in _BUILTIN_DEFS.items():
|
for name, tuples in _BUILTIN_DEFS.items():
|
||||||
gid = f"gr_builtin_{name}"
|
gid = f"gr_builtin_{name}"
|
||||||
|
if gid in self._items:
|
||||||
|
continue
|
||||||
gradient = Gradient(
|
gradient = Gradient(
|
||||||
id=gid,
|
id=gid,
|
||||||
name=name.capitalize(),
|
name=name.capitalize(),
|
||||||
@@ -69,7 +81,9 @@ class GradientStore(BaseSqliteStore[Gradient]):
|
|||||||
)
|
)
|
||||||
self._items[gid] = gradient
|
self._items[gid] = gradient
|
||||||
self._save_item(gid, gradient)
|
self._save_item(gid, gradient)
|
||||||
logger.info(f"Seeded {len(_BUILTIN_DEFS)} built-in gradients")
|
added += 1
|
||||||
|
if added:
|
||||||
|
logger.info(f"Seeded {added} new built-in gradients")
|
||||||
|
|
||||||
# Aliases
|
# Aliases
|
||||||
get_all_gradients = BaseSqliteStore.get_all
|
get_all_gradients = BaseSqliteStore.get_all
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div id="css-editor-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="css-editor-title">
|
<div id="css-editor-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="css-editor-title">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="css-editor-title"><svg class="icon" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 3v18"/><path d="M3 7.5h4"/><path d="M3 12h18"/><path d="M3 16.5h4"/><path d="M17 3v18"/><path d="M17 7.5h4"/><path d="M17 16.5h4"/></svg> <span data-i18n="color_strip.add">Add Color Strip Source</span></h2>
|
<h2 id="css-editor-title"><svg class="icon" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 3v18"/><path d="M3 7.5h4"/><path d="M3 12h18"/><path d="M3 16.5h4"/><path d="M17 3v18"/><path d="M17 7.5h4"/><path d="M17 16.5h4"/></svg> <span data-i18n="color_strip.add">Add</span></h2>
|
||||||
<button class="modal-close-btn" onclick="closeCSSEditorModal()" title="Close" data-i18n-aria-label="aria.close">✕</button>
|
<button class="modal-close-btn" onclick="closeCSSEditorModal()" title="Close" data-i18n-aria-label="aria.close">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
|||||||
@@ -239,12 +239,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Release notes preview -->
|
<!-- Release notes button -->
|
||||||
<div class="form-group" style="display:none">
|
<div class="form-group" id="update-release-notes-group" style="display:none">
|
||||||
<div class="label-row">
|
<button class="btn btn-secondary" onclick="openReleaseNotes()" style="width:100%" data-i18n="update.view_release_notes">View Release Notes</button>
|
||||||
<label data-i18n="update.release_notes">Release Notes</label>
|
|
||||||
</div>
|
|
||||||
<pre id="update-release-notes" style="max-height:200px;overflow-y:auto;font-size:0.82rem;white-space:pre-wrap;word-break:break-word;padding:0.5rem;background:var(--bg-secondary);border-radius:var(--radius-sm);border:1px solid var(--border-color);"></pre>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
@@ -309,3 +306,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<pre id="log-viewer-output" class="log-viewer-output"></pre>
|
<pre id="log-viewer-output" class="log-viewer-output"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Release Notes Overlay (full-screen, same pattern as log overlay) -->
|
||||||
|
<div id="release-notes-overlay" class="log-overlay" style="display:none;">
|
||||||
|
<button class="log-overlay-close" onclick="closeReleaseNotes()" title="Close" data-i18n-aria-label="aria.close">✕</button>
|
||||||
|
<div class="log-overlay-toolbar">
|
||||||
|
<h3 data-i18n="update.release_notes">Release Notes</h3>
|
||||||
|
</div>
|
||||||
|
<div id="release-notes-content" class="release-notes-content"></div>
|
||||||
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user