Refactor monolithic app.js into 8 modular files
Split 3803-line app.js into focused modules: - core.js: shared state, utilities, i18n, API commands, MDI icons - player.js: tabs, theme, accent, vinyl, visualizer, UI updates - websocket.js: connection, auth, reconnection - scripts.js: scripts CRUD, quick access, execution dialog - callbacks.js: callbacks CRUD - browser.js: media file browser, thumbnails, pagination, search - links.js: links CRUD, header links, display controls - main.js: DOMContentLoaded init orchestrator Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
414
media_server/static/js/links.js
Normal file
414
media_server/static/js/links.js
Normal file
@@ -0,0 +1,414 @@
|
||||
// ============================================================
|
||||
// Display Brightness & Power Control
|
||||
// ============================================================
|
||||
|
||||
let displayBrightnessTimers = {};
|
||||
const DISPLAY_THROTTLE_MS = 50;
|
||||
|
||||
async function loadDisplayMonitors() {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
if (!token) return;
|
||||
|
||||
const container = document.getElementById('displayMonitors');
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/display/monitors?refresh=true', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
container.innerHTML = `<div class="empty-state-illustration">
|
||||
<svg viewBox="0 0 64 64"><rect x="8" y="10" width="48" height="32" rx="3" fill="none" stroke="currentColor" stroke-width="2"/><line x1="32" y1="42" x2="32" y2="50" stroke="currentColor" stroke-width="2"/><line x1="22" y1="50" x2="42" y2="50" stroke="currentColor" stroke-width="2"/></svg>
|
||||
<p data-i18n="display.error">Failed to load monitors</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const monitors = await response.json();
|
||||
|
||||
if (monitors.length === 0) {
|
||||
container.innerHTML = `<div class="empty-state-illustration">
|
||||
<svg viewBox="0 0 64 64"><rect x="8" y="10" width="48" height="32" rx="3" fill="none" stroke="currentColor" stroke-width="2"/><line x1="32" y1="42" x2="32" y2="50" stroke="currentColor" stroke-width="2"/><line x1="22" y1="50" x2="42" y2="50" stroke="currentColor" stroke-width="2"/></svg>
|
||||
<p data-i18n="display.no_monitors">No monitors detected</p>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = '';
|
||||
monitors.forEach(monitor => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'display-monitor-card';
|
||||
card.id = `monitor-card-${monitor.id}`;
|
||||
|
||||
const brightnessValue = monitor.brightness !== null ? monitor.brightness : 0;
|
||||
const brightnessDisabled = monitor.brightness === null ? 'disabled' : '';
|
||||
|
||||
let powerBtn = '';
|
||||
if (monitor.power_supported) {
|
||||
powerBtn = `
|
||||
<button class="display-power-btn ${monitor.power_on ? 'on' : 'off'}" id="power-btn-${monitor.id}"
|
||||
onclick="toggleDisplayPower(${monitor.id}, '${monitor.name.replace(/'/g, "\\'")}')"
|
||||
title="${monitor.power_on ? t('display.power_off') : t('display.power_on')}">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42A6.92 6.92 0 0119 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.27 1.08-4.28 2.76-5.56L6.34 5.02A8.95 8.95 0 003 12c0 4.97 4.03 9 9 9s9-4.03 9-9a8.95 8.95 0 00-3.17-6.83z"/></svg>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
const details = [monitor.resolution, monitor.manufacturer].filter(Boolean).join(' \u00B7 ');
|
||||
const detailsHtml = details ? `<span class="display-monitor-details">${details}</span>` : '';
|
||||
const primaryBadge = monitor.is_primary ? `<span class="display-primary-badge">${t('display.primary')}</span>` : '';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="display-monitor-header">
|
||||
<svg class="display-monitor-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||
<path fill="currentColor" d="M20 3H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h6v2H8v2h8v-2h-2v-2h6c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12H4V5h16v10z"/>
|
||||
</svg>
|
||||
<div class="display-monitor-info">
|
||||
<span class="display-monitor-name">${monitor.name}${primaryBadge}</span>
|
||||
${detailsHtml}
|
||||
</div>
|
||||
${powerBtn}
|
||||
</div>
|
||||
<div class="display-brightness-control">
|
||||
<svg class="display-brightness-icon" viewBox="0 0 24 24" width="16" height="16">
|
||||
<path fill="currentColor" d="M20 8.69V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12 20 8.69zM12 18c-3.31 0-6-2.69-6-6s2.69-6 6-6 6 2.69 6 6-2.69 6-6 6zm0-10c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4z"/>
|
||||
</svg>
|
||||
<input type="range" class="display-brightness-slider" min="0" max="100" value="${brightnessValue}" ${brightnessDisabled}
|
||||
oninput="onDisplayBrightnessInput(${monitor.id}, this.value)"
|
||||
onchange="onDisplayBrightnessChange(${monitor.id}, this.value)">
|
||||
<span class="display-brightness-value" id="brightness-val-${monitor.id}">${brightnessValue}%</span>
|
||||
</div>`;
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to load display monitors:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function onDisplayBrightnessInput(monitorId, value) {
|
||||
const label = document.getElementById(`brightness-val-${monitorId}`);
|
||||
if (label) label.textContent = `${value}%`;
|
||||
|
||||
if (displayBrightnessTimers[monitorId]) clearTimeout(displayBrightnessTimers[monitorId]);
|
||||
displayBrightnessTimers[monitorId] = setTimeout(() => {
|
||||
sendDisplayBrightness(monitorId, parseInt(value));
|
||||
displayBrightnessTimers[monitorId] = null;
|
||||
}, DISPLAY_THROTTLE_MS);
|
||||
}
|
||||
|
||||
function onDisplayBrightnessChange(monitorId, value) {
|
||||
if (displayBrightnessTimers[monitorId]) {
|
||||
clearTimeout(displayBrightnessTimers[monitorId]);
|
||||
displayBrightnessTimers[monitorId] = null;
|
||||
}
|
||||
sendDisplayBrightness(monitorId, parseInt(value));
|
||||
}
|
||||
|
||||
async function sendDisplayBrightness(monitorId, brightness) {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
try {
|
||||
await fetch(`/api/display/brightness/${monitorId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ brightness })
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to set brightness:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleDisplayPower(monitorId, monitorName) {
|
||||
const btn = document.getElementById(`power-btn-${monitorId}`);
|
||||
const isOn = btn && btn.classList.contains('on');
|
||||
const newState = !isOn;
|
||||
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
try {
|
||||
const response = await fetch(`/api/display/power/${monitorId}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ on: newState })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
if (btn) {
|
||||
btn.classList.toggle('on', newState);
|
||||
btn.classList.toggle('off', !newState);
|
||||
btn.title = newState ? t('display.power_off') : t('display.power_on');
|
||||
}
|
||||
showToast(newState ? 'Monitor turned on' : 'Monitor turned off', 'success');
|
||||
} else {
|
||||
showToast('Failed to change monitor power', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to set display power:', e);
|
||||
showToast('Failed to change monitor power', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Header Quick Links
|
||||
// ============================================================
|
||||
|
||||
async function loadHeaderLinks() {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
if (!token) return;
|
||||
|
||||
const container = document.getElementById('headerLinks');
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/links/list', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) return;
|
||||
|
||||
const links = await response.json();
|
||||
container.innerHTML = '';
|
||||
|
||||
for (const link of links) {
|
||||
const a = document.createElement('a');
|
||||
a.href = link.url;
|
||||
a.target = '_blank';
|
||||
a.rel = 'noopener noreferrer';
|
||||
a.className = 'header-link';
|
||||
a.title = link.label || link.url;
|
||||
|
||||
const iconSvg = await fetchMdiIcon(link.icon || 'mdi:link');
|
||||
a.innerHTML = iconSvg;
|
||||
|
||||
container.appendChild(a);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load header links:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Links Management
|
||||
// ============================================================
|
||||
|
||||
let _loadLinksPromise = null;
|
||||
let linkFormDirty = false;
|
||||
|
||||
async function loadLinksTable() {
|
||||
if (_loadLinksPromise) return _loadLinksPromise;
|
||||
_loadLinksPromise = _loadLinksTableImpl();
|
||||
_loadLinksPromise.finally(() => { _loadLinksPromise = null; });
|
||||
return _loadLinksPromise;
|
||||
}
|
||||
|
||||
async function _loadLinksTableImpl() {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
const tbody = document.getElementById('linksTableBody');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/links/list', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch links');
|
||||
}
|
||||
|
||||
const linksList = await response.json();
|
||||
|
||||
if (linksList.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="empty-state"><div class="empty-state-illustration"><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><p>' + t('links.empty') + '</p></div></td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
tbody.innerHTML = linksList.map(link => `
|
||||
<tr>
|
||||
<td><span class="name-with-icon"><span class="table-icon" data-mdi-icon="${escapeHtml(link.icon || 'mdi:link')}"></span><code>${escapeHtml(link.name)}</code></span></td>
|
||||
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||
title="${escapeHtml(link.url)}">${escapeHtml(link.url)}</td>
|
||||
<td>${escapeHtml(link.label || '')}</td>
|
||||
<td>
|
||||
<div class="action-buttons">
|
||||
<button class="action-btn" data-action="edit" data-link-name="${escapeHtml(link.name)}" title="${t('links.button.edit')}">
|
||||
<svg viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
||||
</button>
|
||||
<button class="action-btn delete" data-action="delete" data-link-name="${escapeHtml(link.name)}" title="${t('links.button.delete')}">
|
||||
<svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
resolveMdiIcons(tbody);
|
||||
} catch (error) {
|
||||
console.error('Error loading links:', error);
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="empty-state" style="color: var(--error);">Failed to load links</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function showAddLinkDialog() {
|
||||
const dialog = document.getElementById('linkDialog');
|
||||
const form = document.getElementById('linkForm');
|
||||
const title = document.getElementById('linkDialogTitle');
|
||||
|
||||
form.reset();
|
||||
document.getElementById('linkOriginalName').value = '';
|
||||
document.getElementById('linkIsEdit').value = 'false';
|
||||
document.getElementById('linkName').disabled = false;
|
||||
document.getElementById('linkIconPreview').innerHTML = '';
|
||||
title.textContent = t('links.dialog.add');
|
||||
|
||||
linkFormDirty = false;
|
||||
|
||||
document.body.classList.add('dialog-open');
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
async function showEditLinkDialog(linkName) {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
const dialog = document.getElementById('linkDialog');
|
||||
const title = document.getElementById('linkDialogTitle');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/links/list', {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch link details');
|
||||
}
|
||||
|
||||
const linksList = await response.json();
|
||||
const link = linksList.find(l => l.name === linkName);
|
||||
|
||||
if (!link) {
|
||||
showToast(t('links.msg.not_found'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('linkOriginalName').value = linkName;
|
||||
document.getElementById('linkIsEdit').value = 'true';
|
||||
document.getElementById('linkName').value = linkName;
|
||||
document.getElementById('linkName').disabled = true;
|
||||
document.getElementById('linkUrl').value = link.url;
|
||||
document.getElementById('linkIcon').value = link.icon || '';
|
||||
document.getElementById('linkLabel').value = link.label || '';
|
||||
document.getElementById('linkDescription').value = link.description || '';
|
||||
|
||||
// Update icon preview
|
||||
const preview = document.getElementById('linkIconPreview');
|
||||
if (link.icon) {
|
||||
fetchMdiIcon(link.icon).then(svg => { preview.innerHTML = svg; });
|
||||
} else {
|
||||
preview.innerHTML = '';
|
||||
}
|
||||
|
||||
title.textContent = t('links.dialog.edit');
|
||||
|
||||
linkFormDirty = false;
|
||||
|
||||
document.body.classList.add('dialog-open');
|
||||
dialog.showModal();
|
||||
} catch (error) {
|
||||
console.error('Error loading link for edit:', error);
|
||||
showToast(t('links.msg.load_failed'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function closeLinkDialog() {
|
||||
if (linkFormDirty) {
|
||||
if (!await showConfirm(t('links.confirm.unsaved'))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const dialog = document.getElementById('linkDialog');
|
||||
linkFormDirty = false;
|
||||
dialog.close();
|
||||
document.body.classList.remove('dialog-open');
|
||||
}
|
||||
|
||||
async function saveLink(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const submitBtn = event.target.querySelector('button[type="submit"]');
|
||||
if (submitBtn) submitBtn.disabled = true;
|
||||
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
const isEdit = document.getElementById('linkIsEdit').value === 'true';
|
||||
const linkName = isEdit ?
|
||||
document.getElementById('linkOriginalName').value :
|
||||
document.getElementById('linkName').value;
|
||||
|
||||
const data = {
|
||||
url: document.getElementById('linkUrl').value,
|
||||
icon: document.getElementById('linkIcon').value || 'mdi:link',
|
||||
label: document.getElementById('linkLabel').value || '',
|
||||
description: document.getElementById('linkDescription').value || ''
|
||||
};
|
||||
|
||||
const endpoint = isEdit ?
|
||||
`/api/links/update/${linkName}` :
|
||||
`/api/links/create/${linkName}`;
|
||||
|
||||
const method = isEdit ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
method,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showToast(t(isEdit ? 'links.msg.updated' : 'links.msg.created'), 'success');
|
||||
linkFormDirty = false;
|
||||
closeLinkDialog();
|
||||
} else {
|
||||
showToast(result.detail || t(isEdit ? 'links.msg.update_failed' : 'links.msg.create_failed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving link:', error);
|
||||
showToast(t(isEdit ? 'links.msg.update_failed' : 'links.msg.create_failed'), 'error');
|
||||
} finally {
|
||||
if (submitBtn) submitBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteLinkConfirm(linkName) {
|
||||
if (!await showConfirm(t('links.confirm.delete').replace('{name}', linkName))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/links/delete/${linkName}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showToast(t('links.msg.deleted'), 'success');
|
||||
} else {
|
||||
showToast(result.detail || t('links.msg.delete_failed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting link:', error);
|
||||
showToast(t('links.msg.delete_failed'), 'error');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user