Add script execution to Web UI

- Add "Quick Actions" section to display configured scripts
- Load scripts from /api/scripts/list on connection
- Display scripts in responsive grid layout
- Execute scripts with single click via /api/scripts/execute
- Show toast notifications for execution feedback
- Visual feedback during script execution
- Auto-hide section if no scripts configured

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 03:28:54 +03:00
parent a0d138bb93
commit 5342cffac7

View File

@@ -263,6 +263,108 @@
border-top: 1px solid var(--border);
}
/* Scripts Section */
.scripts-container {
background: var(--bg-secondary);
border-radius: 12px;
padding: 2rem;
margin-top: 2rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
.scripts-container h2 {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-primary);
}
.scripts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 1rem;
}
.script-btn {
width: 100%;
height: auto;
min-height: 80px;
padding: 1rem;
border-radius: 8px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-primary);
cursor: pointer;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.script-btn:hover:not(:disabled) {
background: var(--accent);
border-color: var(--accent);
transform: translateY(-2px);
}
.script-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.script-btn .script-label {
font-weight: 600;
font-size: 0.875rem;
}
.script-btn .script-description {
font-size: 0.75rem;
color: var(--text-secondary);
text-align: center;
}
.script-btn.executing {
opacity: 0.6;
pointer-events: none;
}
.scripts-empty {
text-align: center;
color: var(--text-muted);
padding: 2rem;
font-size: 0.875rem;
}
.toast {
position: fixed;
bottom: 2rem;
right: 2rem;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
padding: 1rem 1.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
opacity: 0;
transform: translateY(20px);
transition: all 0.3s;
pointer-events: none;
z-index: 1000;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
.toast.success {
border-color: var(--accent);
}
.toast.error {
border-color: var(--error);
}
/* Auth Modal */
#auth-overlay {
position: fixed;
@@ -489,8 +591,19 @@
Source: <span id="source">Unknown</span>
</div>
</div>
<!-- Scripts Section -->
<div class="scripts-container" id="scripts-container" style="display: none;">
<h2>Quick Actions</h2>
<div class="scripts-grid" id="scripts-grid">
<div class="scripts-empty">No scripts configured</div>
</div>
</div>
</div>
<!-- Toast Notification -->
<div class="toast" id="toast"></div>
<script>
let ws = null;
let reconnectTimeout = null;
@@ -498,12 +611,14 @@
let currentDuration = 0;
let currentPosition = 0;
let isUserAdjustingVolume = false;
let scripts = [];
// Initialize on page load
window.addEventListener('DOMContentLoaded', () => {
const token = localStorage.getItem('media_server_token');
if (token) {
connectWebSocket(token);
loadScripts();
} else {
showAuthForm();
}
@@ -588,6 +703,7 @@
console.log('WebSocket connected');
updateConnectionStatus(true);
hideAuthForm();
loadScripts();
};
ws.onmessage = (event) => {
@@ -796,6 +912,102 @@
function seek(position) {
sendCommand('seek', { position: position });
}
// Scripts functionality
async function loadScripts() {
const token = localStorage.getItem('media_server_token');
try {
const response = await fetch('/api/scripts/list', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
scripts = await response.json();
displayScripts();
}
} catch (error) {
console.error('Error loading scripts:', error);
}
}
function displayScripts() {
const container = document.getElementById('scripts-container');
const grid = document.getElementById('scripts-grid');
if (scripts.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
grid.innerHTML = '';
scripts.forEach(script => {
const button = document.createElement('button');
button.className = 'script-btn';
button.onclick = () => executeScript(script.name, button);
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);
}
grid.appendChild(button);
});
}
async function executeScript(scriptName, buttonElement) {
const token = localStorage.getItem('media_server_token');
// Add executing state
buttonElement.classList.add('executing');
try {
const response = await fetch(`/api/scripts/execute/${scriptName}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ args: [] })
});
const result = await response.json();
if (response.ok && result.success) {
showToast(`${scriptName} executed successfully`, 'success');
} else {
showToast(`Failed to execute ${scriptName}`, 'error');
}
} catch (error) {
console.error(`Error executing script ${scriptName}:`, error);
showToast(`Error executing ${scriptName}`, 'error');
} finally {
// Remove executing state
buttonElement.classList.remove('executing');
}
}
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast ${type} show`;
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
</script>
</body>
</html>