Consolidate tabs, Quick Access links, mini player nav, link descriptions
- Merge Scripts/Callbacks/Links tabs into single Settings tab with collapsible sections - Rename Actions tab to Quick Access showing both scripts and configured links - Add prev/next buttons to mini (secondary) player - Add optional description field to links (backend + frontend) - Add CSS chevron indicators on collapsible settings sections - Persist section collapse/expand state in localStorage - Fix race condition in Quick Access rendering with generation counter - Order settings sections: Scripts, Links, Callbacks Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ class LinkConfig(BaseModel):
|
|||||||
url: str = Field(..., description="URL to open")
|
url: str = Field(..., description="URL to open")
|
||||||
icon: str = Field(default="mdi:link", description="MDI icon name (e.g., 'mdi:led-strip-variant')")
|
icon: str = Field(default="mdi:link", description="MDI icon name (e.g., 'mdi:led-strip-variant')")
|
||||||
label: str = Field(default="", description="Tooltip text")
|
label: str = Field(default="", description="Tooltip text")
|
||||||
|
description: str = Field(default="", description="Optional description")
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class LinkInfo(BaseModel):
|
|||||||
url: str
|
url: str
|
||||||
icon: str
|
icon: str
|
||||||
label: str
|
label: str
|
||||||
|
description: str
|
||||||
|
|
||||||
|
|
||||||
class LinkCreateRequest(BaseModel):
|
class LinkCreateRequest(BaseModel):
|
||||||
@@ -31,6 +32,7 @@ class LinkCreateRequest(BaseModel):
|
|||||||
url: str = Field(..., description="URL to open", min_length=1)
|
url: str = Field(..., description="URL to open", min_length=1)
|
||||||
icon: str = Field(default="mdi:link", description="MDI icon name (e.g., 'mdi:led-strip-variant')")
|
icon: str = Field(default="mdi:link", description="MDI icon name (e.g., 'mdi:led-strip-variant')")
|
||||||
label: str = Field(default="", description="Tooltip text")
|
label: str = Field(default="", description="Tooltip text")
|
||||||
|
description: str = Field(default="", description="Optional description")
|
||||||
|
|
||||||
|
|
||||||
def _validate_link_name(name: str) -> None:
|
def _validate_link_name(name: str) -> None:
|
||||||
@@ -67,6 +69,7 @@ async def list_links(_: str = Depends(verify_token)) -> list[LinkInfo]:
|
|||||||
url=config.url,
|
url=config.url,
|
||||||
icon=config.icon,
|
icon=config.icon,
|
||||||
label=config.label,
|
label=config.label,
|
||||||
|
description=config.description,
|
||||||
)
|
)
|
||||||
for name, config in settings.links.items()
|
for name, config in settings.links.items()
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
:root[data-theme="light"] .browser-container,
|
:root[data-theme="light"] .browser-container,
|
||||||
:root[data-theme="light"] .scripts-container,
|
:root[data-theme="light"] .scripts-container,
|
||||||
:root[data-theme="light"] .script-management,
|
:root[data-theme="light"] .script-management,
|
||||||
|
:root[data-theme="light"] .settings-section,
|
||||||
:root[data-theme="light"] .display-container {
|
:root[data-theme="light"] .display-container {
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
@@ -979,6 +980,91 @@ button:disabled {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Settings Container */
|
||||||
|
.settings-container.active {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section summary {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: background 0.2s;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section summary::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
border-right: 2px solid var(--text-muted);
|
||||||
|
border-bottom: 2px solid var(--text-muted);
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section[open] > summary::before {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section summary:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-content {
|
||||||
|
padding: 0 1.5rem 1.5rem;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-description {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Link card in Quick Access */
|
||||||
|
.link-card {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card:hover {
|
||||||
|
border-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mini player nav buttons */
|
||||||
|
.mini-nav-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-nav-btn svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Display Control Section */
|
/* Display Control Section */
|
||||||
.display-container {
|
.display-container {
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
|
|||||||
@@ -18,11 +18,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mini-controls">
|
<div class="mini-controls">
|
||||||
|
<button class="mini-control-btn mini-nav-btn" onclick="previousTrack()" data-i18n-title="player.previous" title="Previous">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/></svg>
|
||||||
|
</button>
|
||||||
<button class="mini-control-btn" onclick="togglePlayPause()" id="mini-btn-play-pause" title="Play/Pause">
|
<button class="mini-control-btn" onclick="togglePlayPause()" id="mini-btn-play-pause" title="Play/Pause">
|
||||||
<svg viewBox="0 0 24 24" id="mini-play-pause-icon">
|
<svg viewBox="0 0 24 24" id="mini-play-pause-icon">
|
||||||
<path d="M8 5v14l11-7z"/>
|
<path d="M8 5v14l11-7z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="mini-control-btn mini-nav-btn" onclick="nextTrack()" data-i18n-title="player.next" title="Next">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mini-progress-container">
|
<div class="mini-progress-container">
|
||||||
<div class="mini-time-display">
|
<div class="mini-time-display">
|
||||||
@@ -112,19 +118,11 @@
|
|||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" data-tab="quick-actions" onclick="switchTab('quick-actions')" role="tab" aria-selected="false" aria-controls="panel-quick-actions" tabindex="-1">
|
<button class="tab-btn" data-tab="quick-actions" onclick="switchTab('quick-actions')" role="tab" aria-selected="false" aria-controls="panel-quick-actions" tabindex="-1">
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7 2v11h3v9l7-12h-4l4-8z"/></svg>
|
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M7 2v11h3v9l7-12h-4l4-8z"/></svg>
|
||||||
<span data-i18n="tab.quick_actions">Actions</span>
|
<span data-i18n="tab.quick_access">Quick Access</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="tab-btn" data-tab="scripts" onclick="switchTab('scripts')" role="tab" aria-selected="false" aria-controls="panel-scripts" tabindex="-1">
|
<button class="tab-btn" data-tab="settings" onclick="switchTab('settings')" role="tab" aria-selected="false" aria-controls="panel-settings" tabindex="-1">
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
|
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.488.488 0 00-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 00-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
||||||
<span data-i18n="tab.scripts">Scripts</span>
|
<span data-i18n="tab.settings">Settings</span>
|
||||||
</button>
|
|
||||||
<button class="tab-btn" data-tab="callbacks" onclick="switchTab('callbacks')" role="tab" aria-selected="false" aria-controls="panel-callbacks" tabindex="-1">
|
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>
|
|
||||||
<span data-i18n="tab.callbacks">Callbacks</span>
|
|
||||||
</button>
|
|
||||||
<button class="tab-btn" data-tab="links" onclick="switchTab('links')" role="tab" aria-selected="false" aria-controls="panel-links" tabindex="-1">
|
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>
|
|
||||||
<span data-i18n="tab.links">Links</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -268,100 +266,106 @@
|
|||||||
<div class="scripts-grid" id="scripts-grid">
|
<div class="scripts-grid" id="scripts-grid">
|
||||||
<div class="scripts-empty empty-state-illustration">
|
<div class="scripts-empty empty-state-illustration">
|
||||||
<svg viewBox="0 0 64 64"><path d="M20 8l-8 48"/><path d="M44 8l8 48"/><path d="M10 24h44"/><path d="M8 40h44"/></svg>
|
<svg viewBox="0 0 64 64"><path d="M20 8l-8 48"/><path d="M44 8l8 48"/><path d="M10 24h44"/><path d="M8 40h44"/></svg>
|
||||||
<p data-i18n="scripts.no_scripts">No scripts configured</p>
|
<p data-i18n="quick_access.no_items">No quick actions or links configured</p>
|
||||||
</div>
|
|
||||||
<div class="script-btn add-card-grid" onclick="showAddScriptDialog()">
|
|
||||||
<span class="add-card-icon">+</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Script Management Section -->
|
<!-- Settings Section (Scripts, Callbacks, Links management) -->
|
||||||
<div class="script-management" data-tab-content="scripts" role="tabpanel" id="panel-scripts">
|
<div class="settings-container" data-tab-content="settings" role="tabpanel" id="panel-settings">
|
||||||
<table class="scripts-table">
|
<details class="settings-section" open>
|
||||||
<thead>
|
<summary data-i18n="settings.section.scripts">Scripts</summary>
|
||||||
<tr>
|
<div class="settings-section-content">
|
||||||
<th data-i18n="scripts.table.name">Name</th>
|
<table class="scripts-table">
|
||||||
<th data-i18n="scripts.table.label">Label</th>
|
<thead>
|
||||||
<th data-i18n="scripts.table.command">Command</th>
|
<tr>
|
||||||
<th data-i18n="scripts.table.timeout">Timeout</th>
|
<th data-i18n="scripts.table.name">Name</th>
|
||||||
<th data-i18n="scripts.table.actions">Actions</th>
|
<th data-i18n="scripts.table.label">Label</th>
|
||||||
</tr>
|
<th data-i18n="scripts.table.command">Command</th>
|
||||||
</thead>
|
<th data-i18n="scripts.table.timeout">Timeout</th>
|
||||||
<tbody id="scriptsTableBody">
|
<th data-i18n="scripts.table.actions">Actions</th>
|
||||||
<tr>
|
</tr>
|
||||||
<td colspan="5" class="empty-state">
|
</thead>
|
||||||
<div class="empty-state-illustration">
|
<tbody id="scriptsTableBody">
|
||||||
<svg viewBox="0 0 64 64"><rect x="8" y="4" width="48" height="56" rx="4"/><path d="M20 20h24M20 32h16M20 44h20"/></svg>
|
<tr>
|
||||||
<p data-i18n="scripts.empty">No scripts configured. Click "Add" to create one.</p>
|
<td colspan="5" class="empty-state">
|
||||||
</div>
|
<div class="empty-state-illustration">
|
||||||
</td>
|
<svg viewBox="0 0 64 64"><rect x="8" y="4" width="48" height="56" rx="4"/><path d="M20 20h24M20 32h16M20 44h20"/></svg>
|
||||||
</tr>
|
<p data-i18n="scripts.empty">No scripts configured. Click "Add" to create one.</p>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
</td>
|
||||||
<div class="add-card" onclick="showAddScriptDialog()">
|
</tr>
|
||||||
<span class="add-card-icon">+</span>
|
</tbody>
|
||||||
</div>
|
</table>
|
||||||
</div>
|
<div class="add-card" onclick="showAddScriptDialog()">
|
||||||
|
<span class="add-card-icon">+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<!-- Callback Management Section -->
|
<details class="settings-section" open>
|
||||||
<div class="script-management" data-tab-content="callbacks" role="tabpanel" id="panel-callbacks">
|
<summary data-i18n="settings.section.links">Links</summary>
|
||||||
<p style="color: var(--text-secondary); font-size: 0.875rem; margin-bottom: 1rem;" data-i18n="callbacks.description">
|
<div class="settings-section-content">
|
||||||
Callbacks are scripts triggered automatically by media control events (play, pause, stop, etc.)
|
<p class="settings-section-description" data-i18n="links.description">
|
||||||
</p>
|
Quick links displayed as icons in the header bar. Click an icon to open the URL in a new tab.
|
||||||
<table class="scripts-table">
|
</p>
|
||||||
<thead>
|
<table class="scripts-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th data-i18n="callbacks.table.event">Event</th>
|
<tr>
|
||||||
<th data-i18n="callbacks.table.command">Command</th>
|
<th data-i18n="links.table.name">Name</th>
|
||||||
<th data-i18n="callbacks.table.timeout">Timeout</th>
|
<th data-i18n="links.table.url">URL</th>
|
||||||
<th data-i18n="callbacks.table.actions">Actions</th>
|
<th data-i18n="links.table.label">Label</th>
|
||||||
</tr>
|
<th data-i18n="links.table.actions">Actions</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody id="callbacksTableBody">
|
</thead>
|
||||||
<tr>
|
<tbody id="linksTableBody">
|
||||||
<td colspan="4" class="empty-state">
|
<tr>
|
||||||
<div class="empty-state-illustration">
|
<td colspan="4" class="empty-state">
|
||||||
<svg viewBox="0 0 64 64"><circle cx="32" cy="32" r="24"/><path d="M32 20v12l8 8"/></svg>
|
<div class="empty-state-illustration">
|
||||||
<p>No callbacks configured. Click "Add" to create one.</p>
|
<svg viewBox="0 0 64 64"><path d="M26 20a10 10 0 010 14l-6 6a10 10 0 01-14-14l6-6a10 10 0 0114 0" fill="none" stroke="currentColor" stroke-width="2"/><path d="M38 44a10 10 0 010-14l6-6a10 10 0 0114 14l-6 6a10 10 0 01-14 0" fill="none" stroke="currentColor" stroke-width="2"/><path d="M24 40l16-16" stroke="currentColor" stroke-width="2"/></svg>
|
||||||
</div>
|
<p data-i18n="links.empty">No links configured. Click "Add" to create one.</p>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</tbody>
|
||||||
<div class="add-card" onclick="showAddCallbackDialog()">
|
</table>
|
||||||
<span class="add-card-icon">+</span>
|
<div class="add-card" onclick="showAddLinkDialog()">
|
||||||
</div>
|
<span class="add-card-icon">+</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<!-- Links Management Section -->
|
<details class="settings-section" open>
|
||||||
<div class="script-management" data-tab-content="links" role="tabpanel" id="panel-links">
|
<summary data-i18n="settings.section.callbacks">Callbacks</summary>
|
||||||
<p style="color: var(--text-secondary); font-size: 0.875rem; margin-bottom: 1rem;" data-i18n="links.description">
|
<div class="settings-section-content">
|
||||||
Quick links displayed as icons in the header bar. Click an icon to open the URL in a new tab.
|
<p class="settings-section-description" data-i18n="callbacks.description">
|
||||||
</p>
|
Callbacks are scripts triggered automatically by media control events (play, pause, stop, etc.)
|
||||||
<table class="scripts-table">
|
</p>
|
||||||
<thead>
|
<table class="scripts-table">
|
||||||
<tr>
|
<thead>
|
||||||
<th data-i18n="links.table.name">Name</th>
|
<tr>
|
||||||
<th data-i18n="links.table.url">URL</th>
|
<th data-i18n="callbacks.table.event">Event</th>
|
||||||
<th data-i18n="links.table.label">Label</th>
|
<th data-i18n="callbacks.table.command">Command</th>
|
||||||
<th data-i18n="links.table.actions">Actions</th>
|
<th data-i18n="callbacks.table.timeout">Timeout</th>
|
||||||
</tr>
|
<th data-i18n="callbacks.table.actions">Actions</th>
|
||||||
</thead>
|
</tr>
|
||||||
<tbody id="linksTableBody">
|
</thead>
|
||||||
<tr>
|
<tbody id="callbacksTableBody">
|
||||||
<td colspan="4" class="empty-state">
|
<tr>
|
||||||
<div class="empty-state-illustration">
|
<td colspan="4" class="empty-state">
|
||||||
<svg viewBox="0 0 64 64"><path d="M26 20a10 10 0 010 14l-6 6a10 10 0 01-14-14l6-6a10 10 0 0114 0" fill="none" stroke="currentColor" stroke-width="2"/><path d="M38 44a10 10 0 010-14l6-6a10 10 0 0114 14l-6 6a10 10 0 01-14 0" fill="none" stroke="currentColor" stroke-width="2"/><path d="M24 40l16-16" stroke="currentColor" stroke-width="2"/></svg>
|
<div class="empty-state-illustration">
|
||||||
<p data-i18n="links.empty">No links configured. Click "Add" to create one.</p>
|
<svg viewBox="0 0 64 64"><circle cx="32" cy="32" r="24"/><path d="M32 20v12l8 8"/></svg>
|
||||||
</div>
|
<p>No callbacks configured. Click "Add" to create one.</p>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</td>
|
||||||
</tbody>
|
</tr>
|
||||||
</table>
|
</tbody>
|
||||||
<div class="add-card" onclick="showAddLinkDialog()">
|
</table>
|
||||||
<span class="add-card-icon">+</span>
|
<div class="add-card" onclick="showAddCallbackDialog()">
|
||||||
</div>
|
<span class="add-card-icon">+</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Display Control Section -->
|
<!-- Display Control Section -->
|
||||||
@@ -508,6 +512,11 @@
|
|||||||
<span data-i18n="links.field.label">Label</span>
|
<span data-i18n="links.field.label">Label</span>
|
||||||
<input type="text" id="linkLabel" data-i18n-placeholder="links.placeholder.label" placeholder="Tooltip text">
|
<input type="text" id="linkLabel" data-i18n-placeholder="links.placeholder.label" placeholder="Tooltip text">
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<span data-i18n="links.field.description">Description</span>
|
||||||
|
<textarea id="linkDescription" data-i18n-placeholder="links.placeholder.description" placeholder="What does this link point to?"></textarea>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
<button type="button" class="btn-secondary" onclick="closeLinkDialog()" data-i18n="links.button.cancel">Cancel</button>
|
<button type="button" class="btn-secondary" onclick="closeLinkDialog()" data-i18n="links.button.cancel">Cancel</button>
|
||||||
|
|||||||
@@ -400,12 +400,13 @@
|
|||||||
document.getElementById('source').textContent = lastStatus.source || t('player.unknown_source');
|
document.getElementById('source').textContent = lastStatus.source || t('player.unknown_source');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload tables to get translated content
|
// Reload tables and quick access to get translated content
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
if (token) {
|
if (token) {
|
||||||
loadScriptsTable();
|
loadScriptsTable();
|
||||||
loadCallbacksTable();
|
loadCallbacksTable();
|
||||||
loadLinksTable();
|
loadLinksTable();
|
||||||
|
displayQuickAccess();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -564,8 +565,9 @@
|
|||||||
setupVolumeSlider('volume-slider');
|
setupVolumeSlider('volume-slider');
|
||||||
setupVolumeSlider('mini-volume-slider');
|
setupVolumeSlider('mini-volume-slider');
|
||||||
|
|
||||||
// Restore saved tab
|
// Restore saved tab (migrate old tab names)
|
||||||
const savedTab = localStorage.getItem('activeTab') || 'player';
|
let savedTab = localStorage.getItem('activeTab') || 'player';
|
||||||
|
if (['scripts', 'callbacks', 'links'].includes(savedTab)) savedTab = 'settings';
|
||||||
switchTab(savedTab);
|
switchTab(savedTab);
|
||||||
// Snap indicator to initial position without animation
|
// Snap indicator to initial position without animation
|
||||||
const initialActiveBtn = document.querySelector('.tab-btn.active');
|
const initialActiveBtn = document.querySelector('.tab-btn.active');
|
||||||
@@ -700,6 +702,17 @@
|
|||||||
setupIconPreview('scriptIcon', 'scriptIconPreview');
|
setupIconPreview('scriptIcon', 'scriptIconPreview');
|
||||||
setupIconPreview('linkIcon', 'linkIconPreview');
|
setupIconPreview('linkIcon', 'linkIconPreview');
|
||||||
|
|
||||||
|
// Settings sections: restore collapse state and persist on toggle
|
||||||
|
document.querySelectorAll('.settings-section').forEach(details => {
|
||||||
|
const key = `settings_section_${details.querySelector('summary')?.getAttribute('data-i18n') || ''}`;
|
||||||
|
const saved = localStorage.getItem(key);
|
||||||
|
if (saved === 'closed') details.removeAttribute('open');
|
||||||
|
else if (saved === 'open') details.setAttribute('open', '');
|
||||||
|
details.addEventListener('toggle', () => {
|
||||||
|
localStorage.setItem(key, details.open ? 'open' : 'closed');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Cleanup blob URLs on page unload
|
// Cleanup blob URLs on page unload
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
thumbnailCache.forEach(url => URL.revokeObjectURL(url));
|
thumbnailCache.forEach(url => URL.revokeObjectURL(url));
|
||||||
@@ -840,6 +853,7 @@
|
|||||||
console.log('Links changed, reloading...');
|
console.log('Links changed, reloading...');
|
||||||
loadHeaderLinks();
|
loadHeaderLinks();
|
||||||
loadLinksTable();
|
loadLinksTable();
|
||||||
|
displayQuickAccess();
|
||||||
} else if (msg.type === 'error') {
|
} else if (msg.type === 'error') {
|
||||||
console.error('WebSocket error:', msg.message);
|
console.error('WebSocket error:', msg.message);
|
||||||
}
|
}
|
||||||
@@ -1192,57 +1206,108 @@
|
|||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
scripts = await response.json();
|
scripts = await response.json();
|
||||||
displayScripts();
|
displayQuickAccess();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading scripts:', error);
|
console.error('Error loading scripts:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayScripts() {
|
let _quickAccessGen = 0;
|
||||||
const container = document.getElementById('panel-quick-actions');
|
async function displayQuickAccess() {
|
||||||
|
const gen = ++_quickAccessGen;
|
||||||
const grid = document.getElementById('scripts-grid');
|
const grid = document.getElementById('scripts-grid');
|
||||||
|
|
||||||
grid.innerHTML = '';
|
// Build everything into a fragment before touching the DOM
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const hasScripts = scripts.length > 0;
|
||||||
|
let hasLinks = false;
|
||||||
|
|
||||||
if (scripts.length === 0) {
|
// Render script buttons
|
||||||
grid.innerHTML = `<div class="scripts-empty empty-state-illustration"><svg viewBox="0 0 64 64"><path d="M20 8l-8 48"/><path d="M44 8l8 48"/><path d="M10 24h44"/><path d="M8 40h44"/></svg><p>${t('scripts.no_scripts')}</p></div>`;
|
scripts.forEach(script => {
|
||||||
} else {
|
const button = document.createElement('button');
|
||||||
scripts.forEach(script => {
|
button.className = 'script-btn';
|
||||||
const button = document.createElement('button');
|
button.onclick = () => executeScript(script.name, button);
|
||||||
button.className = 'script-btn';
|
|
||||||
button.onclick = () => executeScript(script.name, button);
|
|
||||||
|
|
||||||
if (script.icon) {
|
if (script.icon) {
|
||||||
const iconEl = document.createElement('div');
|
const iconEl = document.createElement('div');
|
||||||
iconEl.className = 'script-icon';
|
iconEl.className = 'script-icon';
|
||||||
iconEl.setAttribute('data-mdi-icon', script.icon);
|
iconEl.setAttribute('data-mdi-icon', script.icon);
|
||||||
button.appendChild(iconEl);
|
button.appendChild(iconEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'script-label';
|
||||||
|
label.textContent = script.label || script.name;
|
||||||
|
button.appendChild(label);
|
||||||
|
|
||||||
|
if (script.description) {
|
||||||
|
const description = document.createElement('div');
|
||||||
|
description.className = 'script-description';
|
||||||
|
description.textContent = script.description;
|
||||||
|
button.appendChild(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment.appendChild(button);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch link cards
|
||||||
|
try {
|
||||||
|
const token = localStorage.getItem('media_server_token');
|
||||||
|
if (token) {
|
||||||
|
const response = await fetch('/api/links/list', {
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
});
|
||||||
|
if (gen !== _quickAccessGen) return; // stale call, discard
|
||||||
|
if (response.ok) {
|
||||||
|
const links = await response.json();
|
||||||
|
hasLinks = links.length > 0;
|
||||||
|
links.forEach(link => {
|
||||||
|
const card = document.createElement('a');
|
||||||
|
card.className = 'script-btn link-card';
|
||||||
|
card.href = link.url;
|
||||||
|
card.target = '_blank';
|
||||||
|
card.rel = 'noopener noreferrer';
|
||||||
|
|
||||||
|
if (link.icon) {
|
||||||
|
const iconEl = document.createElement('div');
|
||||||
|
iconEl.className = 'script-icon';
|
||||||
|
iconEl.setAttribute('data-mdi-icon', link.icon);
|
||||||
|
card.appendChild(iconEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'script-label';
|
||||||
|
label.textContent = link.label || link.name;
|
||||||
|
card.appendChild(label);
|
||||||
|
|
||||||
|
if (link.description) {
|
||||||
|
const desc = document.createElement('div');
|
||||||
|
desc.className = 'script-description';
|
||||||
|
desc.textContent = link.description;
|
||||||
|
card.appendChild(desc);
|
||||||
|
}
|
||||||
|
|
||||||
|
fragment.appendChild(card);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const label = document.createElement('div');
|
} catch (e) {
|
||||||
label.className = 'script-label';
|
if (gen !== _quickAccessGen) return;
|
||||||
label.textContent = script.label || script.name;
|
console.warn('Failed to load links for quick access:', e);
|
||||||
|
|
||||||
button.appendChild(label);
|
|
||||||
|
|
||||||
if (script.description) {
|
|
||||||
const description = document.createElement('div');
|
|
||||||
description.className = 'script-description';
|
|
||||||
description.textContent = script.description;
|
|
||||||
button.appendChild(description);
|
|
||||||
}
|
|
||||||
|
|
||||||
grid.appendChild(button);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add "+" card at the end
|
// Show empty state if nothing
|
||||||
const addCard = document.createElement('div');
|
if (!hasScripts && !hasLinks) {
|
||||||
addCard.className = 'script-btn add-card-grid';
|
const empty = document.createElement('div');
|
||||||
addCard.onclick = () => showAddScriptDialog();
|
empty.className = 'scripts-empty empty-state-illustration';
|
||||||
addCard.innerHTML = '<span class="add-card-icon">+</span>';
|
empty.innerHTML = `<svg viewBox="0 0 64 64"><path d="M20 8l-8 48"/><path d="M44 8l8 48"/><path d="M10 24h44"/><path d="M8 40h44"/></svg><p>${t('quick_access.no_items')}</p>`;
|
||||||
grid.appendChild(addCard);
|
fragment.prepend(empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace grid content atomically
|
||||||
|
grid.innerHTML = '';
|
||||||
|
grid.appendChild(fragment);
|
||||||
|
|
||||||
// Resolve MDI icons
|
// Resolve MDI icons
|
||||||
resolveMdiIcons(grid);
|
resolveMdiIcons(grid);
|
||||||
@@ -3192,6 +3257,7 @@ async function showEditLinkDialog(linkName) {
|
|||||||
document.getElementById('linkUrl').value = link.url;
|
document.getElementById('linkUrl').value = link.url;
|
||||||
document.getElementById('linkIcon').value = link.icon || '';
|
document.getElementById('linkIcon').value = link.icon || '';
|
||||||
document.getElementById('linkLabel').value = link.label || '';
|
document.getElementById('linkLabel').value = link.label || '';
|
||||||
|
document.getElementById('linkDescription').value = link.description || '';
|
||||||
|
|
||||||
// Update icon preview
|
// Update icon preview
|
||||||
const preview = document.getElementById('linkIconPreview');
|
const preview = document.getElementById('linkIconPreview');
|
||||||
@@ -3241,7 +3307,8 @@ async function saveLink(event) {
|
|||||||
const data = {
|
const data = {
|
||||||
url: document.getElementById('linkUrl').value,
|
url: document.getElementById('linkUrl').value,
|
||||||
icon: document.getElementById('linkIcon').value || 'mdi:link',
|
icon: document.getElementById('linkIcon').value || 'mdi:link',
|
||||||
label: document.getElementById('linkLabel').value || ''
|
label: document.getElementById('linkLabel').value || '',
|
||||||
|
description: document.getElementById('linkDescription').value || ''
|
||||||
};
|
};
|
||||||
|
|
||||||
const endpoint = isEdit ?
|
const endpoint = isEdit ?
|
||||||
|
|||||||
@@ -116,11 +116,13 @@
|
|||||||
"callbacks.confirm.unsaved": "You have unsaved changes. Are you sure you want to discard them?",
|
"callbacks.confirm.unsaved": "You have unsaved changes. Are you sure you want to discard them?",
|
||||||
"tab.player": "Player",
|
"tab.player": "Player",
|
||||||
"tab.browser": "Browser",
|
"tab.browser": "Browser",
|
||||||
"tab.quick_actions": "Actions",
|
"tab.quick_access": "Quick Access",
|
||||||
"tab.scripts": "Scripts",
|
"tab.settings": "Settings",
|
||||||
"tab.callbacks": "Callbacks",
|
|
||||||
"tab.links": "Links",
|
|
||||||
"tab.display": "Display",
|
"tab.display": "Display",
|
||||||
|
"settings.section.scripts": "Scripts",
|
||||||
|
"settings.section.callbacks": "Callbacks",
|
||||||
|
"settings.section.links": "Links",
|
||||||
|
"quick_access.no_items": "No quick actions or links configured",
|
||||||
"display.loading": "Loading monitors...",
|
"display.loading": "Loading monitors...",
|
||||||
"display.error": "Failed to load monitors",
|
"display.error": "Failed to load monitors",
|
||||||
"display.no_monitors": "No monitors detected",
|
"display.no_monitors": "No monitors detected",
|
||||||
@@ -179,10 +181,12 @@
|
|||||||
"links.field.url": "URL *",
|
"links.field.url": "URL *",
|
||||||
"links.field.icon": "Icon (MDI)",
|
"links.field.icon": "Icon (MDI)",
|
||||||
"links.field.label": "Label",
|
"links.field.label": "Label",
|
||||||
|
"links.field.description": "Description",
|
||||||
"links.placeholder.name": "Only letters, numbers, and underscores allowed",
|
"links.placeholder.name": "Only letters, numbers, and underscores allowed",
|
||||||
"links.placeholder.url": "https://example.com",
|
"links.placeholder.url": "https://example.com",
|
||||||
"links.placeholder.icon": "mdi:link",
|
"links.placeholder.icon": "mdi:link",
|
||||||
"links.placeholder.label": "Tooltip text",
|
"links.placeholder.label": "Tooltip text",
|
||||||
|
"links.placeholder.description": "What does this link point to?",
|
||||||
"links.button.cancel": "Cancel",
|
"links.button.cancel": "Cancel",
|
||||||
"links.button.save": "Save",
|
"links.button.save": "Save",
|
||||||
"links.button.edit": "Edit",
|
"links.button.edit": "Edit",
|
||||||
|
|||||||
@@ -116,11 +116,13 @@
|
|||||||
"callbacks.confirm.unsaved": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?",
|
"callbacks.confirm.unsaved": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?",
|
||||||
"tab.player": "Плеер",
|
"tab.player": "Плеер",
|
||||||
"tab.browser": "Браузер",
|
"tab.browser": "Браузер",
|
||||||
"tab.quick_actions": "Действия",
|
"tab.quick_access": "Быстрый Доступ",
|
||||||
"tab.scripts": "Скрипты",
|
"tab.settings": "Настройки",
|
||||||
"tab.callbacks": "Колбэки",
|
|
||||||
"tab.links": "Ссылки",
|
|
||||||
"tab.display": "Дисплей",
|
"tab.display": "Дисплей",
|
||||||
|
"settings.section.scripts": "Скрипты",
|
||||||
|
"settings.section.callbacks": "Колбэки",
|
||||||
|
"settings.section.links": "Ссылки",
|
||||||
|
"quick_access.no_items": "Быстрые действия и ссылки не настроены",
|
||||||
"display.loading": "Загрузка мониторов...",
|
"display.loading": "Загрузка мониторов...",
|
||||||
"display.error": "Не удалось загрузить мониторы",
|
"display.error": "Не удалось загрузить мониторы",
|
||||||
"display.no_monitors": "Мониторы не обнаружены",
|
"display.no_monitors": "Мониторы не обнаружены",
|
||||||
@@ -179,10 +181,12 @@
|
|||||||
"links.field.url": "URL *",
|
"links.field.url": "URL *",
|
||||||
"links.field.icon": "Иконка (MDI)",
|
"links.field.icon": "Иконка (MDI)",
|
||||||
"links.field.label": "Метка",
|
"links.field.label": "Метка",
|
||||||
|
"links.field.description": "Описание",
|
||||||
"links.placeholder.name": "Только буквы, цифры и подчеркивания",
|
"links.placeholder.name": "Только буквы, цифры и подчеркивания",
|
||||||
"links.placeholder.url": "https://example.com",
|
"links.placeholder.url": "https://example.com",
|
||||||
"links.placeholder.icon": "mdi:link",
|
"links.placeholder.icon": "mdi:link",
|
||||||
"links.placeholder.label": "Текст подсказки",
|
"links.placeholder.label": "Текст подсказки",
|
||||||
|
"links.placeholder.description": "Куда ведет эта ссылка?",
|
||||||
"links.button.cancel": "Отмена",
|
"links.button.cancel": "Отмена",
|
||||||
"links.button.save": "Сохранить",
|
"links.button.save": "Сохранить",
|
||||||
"links.button.edit": "Редактировать",
|
"links.button.edit": "Редактировать",
|
||||||
|
|||||||
Reference in New Issue
Block a user