Files
media-player-server/media_server/static/index.html
alexei.dolgolyov 4c13322936 Show bitrate in browser, remove type labels and Play All text
- Extract bitrate alongside duration in browse_directory via get_media_info
- Display bitrate in large card view metadata (duration · bitrate · size)
- Replace Audio/Video type badge with bitrate column in list view
- Remove Play All button text, keep icon only
- Add formatBitrate helper function

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 03:37:13 +03:00

472 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Media Server</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%231db954;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%231ed760;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='45' fill='url(%23grad)'/%3E%3Cpath fill='white' d='M35 25 L35 75 L75 50 Z'/%3E%3C/svg%3E">
<link rel="stylesheet" href="/static/css/styles.css">
</head>
<body class="loading-translations">
<!-- Mini Player (sticky) -->
<div class="mini-player hidden" id="mini-player">
<div class="mini-player-info">
<img id="mini-album-art" class="mini-album-art" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3Cpath fill='%236a6a6a' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E" alt="Album Art">
<div class="mini-track-details">
<div id="mini-track-title" class="mini-track-title">No media playing</div>
<div id="mini-artist" class="mini-artist"></div>
</div>
</div>
<div class="mini-controls">
<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">
<path d="M8 5v14l11-7z"/>
</svg>
</button>
</div>
<div class="mini-progress-container">
<div class="mini-time-display">
<span id="mini-current-time">0:00</span>
<span id="mini-total-time">0:00</span>
</div>
<div class="mini-progress-bar" id="mini-progress-bar">
<div class="mini-progress-fill" id="mini-progress-fill"></div>
</div>
</div>
<div class="mini-volume-container">
<button class="mini-control-btn" onclick="toggleMute()" id="mini-btn-mute" title="Mute">
<svg viewBox="0 0 24 24" id="mini-mute-icon">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
</svg>
</button>
<input type="range" id="mini-volume-slider" class="mini-volume-slider" min="0" max="100" value="50">
<div class="mini-volume-display" id="mini-volume-display">50%</div>
</div>
</div>
<!-- Auth Modal -->
<div id="auth-overlay" class="hidden">
<div class="auth-modal">
<h2 data-i18n="app.title">Media Server</h2>
<p data-i18n="auth.message">Enter your API token to connect to the media server.</p>
<input type="text" id="token-input" data-i18n-placeholder="auth.placeholder" placeholder="Enter API Token" autocomplete="off">
<button class="btn-connect" onclick="authenticate()" data-i18n="auth.connect">Connect</button>
<div class="help-text">
<p data-i18n="auth.help">To get your token, run:</p>
<code>media-server --show-token</code>
</div>
<div class="error-message" id="auth-error"></div>
</div>
</div>
<div class="container">
<header>
<div style="display: flex; align-items: center; gap: 0.5rem;">
<span class="status-dot" id="status-dot"></span>
<h1 data-i18n="app.title">Media Server</h1>
<span class="version-label" id="version-label"></span>
</div>
<div style="display: flex; align-items: center; gap: 1rem;">
<button class="theme-toggle" onclick="toggleTheme()" data-i18n-title="player.theme" title="Toggle theme" id="theme-toggle">
<svg id="theme-icon-sun" viewBox="0 0 24 24" style="display: none;">
<path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/>
</svg>
<svg id="theme-icon-moon" viewBox="0 0 24 24">
<path d="M9 2c-1.05 0-2.05.16-3 .46 4.06 1.27 7 5.06 7 9.54 0 4.48-2.94 8.27-7 9.54.95.3 1.95.46 3 .46 5.52 0 10-4.48 10-10S14.52 2 9 2z"/>
</svg>
</button>
<select id="locale-select" onchange="changeLocale()" title="Change language">
<option value="en">English</option>
<option value="ru">Русский</option>
</select>
<button class="clear-token-btn" onclick="clearToken()" data-i18n-title="auth.logout.title" data-i18n="auth.logout" title="Clear saved token">Logout</button>
</div>
</header>
<!-- Tab Bar -->
<div class="tab-bar" id="tabBar">
<button class="tab-btn active" data-tab="player" onclick="switchTab('player')">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
<span data-i18n="tab.player">Player</span>
</button>
<button class="tab-btn" data-tab="browser" onclick="switchTab('browser')">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>
<span data-i18n="tab.browser">Browser</span>
</button>
<button class="tab-btn" data-tab="quick-actions" onclick="switchTab('quick-actions')">
<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>
</button>
<button class="tab-btn" data-tab="scripts" onclick="switchTab('scripts')">
<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>
<span data-i18n="tab.scripts">Scripts</span>
</button>
<button class="tab-btn" data-tab="callbacks" onclick="switchTab('callbacks')">
<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>
</div>
<div class="player-container" data-tab-content="player">
<div class="album-art-container">
<img id="album-art" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 300 300'%3E%3Crect fill='%23282828' width='300' height='300'/%3E%3Cpath fill='%236a6a6a' d='M150 80c-38.66 0-70 31.34-70 70s31.34 70 70 70 70-31.34 70-70-31.34-70-70-70zm0 20c27.614 0 50 22.386 50 50s-22.386 50-50 50-50-22.386-50-50 22.386-50 50-50zm0 30a20 20 0 100 40 20 20 0 000-40z'/%3E%3C/svg%3E" alt="Album Art">
</div>
<div class="track-info">
<div id="track-title" data-i18n="player.no_media">No media playing</div>
<div id="artist"></div>
<div id="album"></div>
<div class="playback-state">
<svg class="state-icon" id="state-icon" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/>
</svg>
<span id="playback-state" data-i18n="state.idle">Idle</span>
</div>
</div>
<div class="progress-container">
<div class="time-display">
<span id="current-time">0:00</span>
<span id="total-time">0:00</span>
</div>
<div class="progress-bar" id="progress-bar" data-duration="0">
<div class="progress-fill" id="progress-fill"></div>
</div>
</div>
<div class="controls">
<button onclick="previousTrack()" data-i18n-title="player.previous" title="Previous" id="btn-previous">
<svg viewBox="0 0 24 24">
<path d="M6 6h2v12H6zm3.5 6l8.5 6V6z"/>
</svg>
</button>
<button class="primary" onclick="togglePlayPause()" data-i18n-title="player.play" title="Play/Pause" id="btn-play-pause">
<svg viewBox="0 0 24 24" id="play-pause-icon">
<path d="M8 5v14l11-7z"/>
</svg>
</button>
<button onclick="nextTrack()" data-i18n-title="player.next" title="Next" id="btn-next">
<svg viewBox="0 0 24 24">
<path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z"/>
</svg>
</button>
</div>
<div class="volume-container">
<button class="mute-btn" onclick="toggleMute()" data-i18n-title="player.mute" title="Mute" id="btn-mute">
<svg viewBox="0 0 24 24" id="mute-icon">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>
</svg>
</button>
<input type="range" id="volume-slider" min="0" max="100" value="50">
<div class="volume-display" id="volume-display">50%</div>
</div>
<div class="source-info">
<span data-i18n="player.source">Source:</span> <span id="source" data-i18n="player.unknown_source">Unknown</span>
</div>
</div>
<!-- Media Browser Section -->
<div class="browser-container" data-tab-content="browser" >
<h2 data-i18n="browser.title">Media Browser</h2>
<!-- Breadcrumb Navigation -->
<div class="breadcrumb" id="breadcrumb"></div>
<!-- Browser Toolbar -->
<div class="browser-toolbar" id="browserToolbar">
<div class="browser-toolbar-left">
<div class="view-toggle">
<button class="view-toggle-btn active" id="viewGridBtn" onclick="setViewMode('grid')" data-i18n-title="browser.view_grid" title="Grid view">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 3h8v8H3V3zm0 10h8v8H3v-8zm10-10h8v8h-8V3zm0 10h8v8h-8v-8z"/></svg>
</button>
<button class="view-toggle-btn" id="viewCompactBtn" onclick="setViewMode('compact')" data-i18n-title="browser.view_compact" title="Compact view">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M2 2h5v5H2V2zm0 8h5v5H2v-5zm0 8h5v5H2v-5zm7-16h5v5H9V2zm0 8h5v5H9v-5zm0 8h5v5H9v-5zm7-16h5v5h-5V2zm0 8h5v5h-5v-5zm0 8h5v5h-5v-5z"/></svg>
</button>
<button class="view-toggle-btn" id="viewListBtn" onclick="setViewMode('list')" data-i18n-title="browser.view_list" title="List view">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z"/></svg>
</button>
</div>
<button class="view-toggle-btn browser-refresh-btn" id="refreshBtn" onclick="refreshBrowser()" data-i18n-title="browser.refresh" title="Refresh">
<svg viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
</button>
<button class="browser-play-all-btn" id="playAllBtn" onclick="playAllFolder()" data-i18n-title="browser.play_all" title="Play All" style="display: none;">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M8 5v14l11-7z"/></svg>
</button>
</div>
<div class="browser-search-wrapper" id="browserSearchWrapper" style="display: none;">
<svg class="browser-search-icon" viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
<input type="text" id="browserSearchInput" class="browser-search-input" data-i18n-placeholder="browser.search" placeholder="Search..." oninput="onBrowserSearch()">
<button class="browser-search-clear" id="browserSearchClear" onclick="clearBrowserSearch()" style="display: none;">
<svg viewBox="0 0 24 24" width="14" height="14"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</div>
<div class="browser-toolbar-right">
<label class="items-per-page-label">
<span data-i18n="browser.items_per_page">Items per page:</span>
<select id="itemsPerPageSelect" onchange="onItemsPerPageChanged()">
<option value="25">25</option>
<option value="50">50</option>
<option value="100" selected>100</option>
<option value="200">200</option>
<option value="500">500</option>
</select>
</label>
</div>
</div>
<!-- File/Folder Grid -->
<div class="browser-grid" id="browserGrid">
<div class="browser-empty" data-i18n="browser.no_folder_selected">Select a folder to browse media files</div>
</div>
<!-- Pagination -->
<div class="pagination" id="browserPagination" style="display: none;">
<button id="prevPage" onclick="previousPage()" data-i18n="browser.previous">Previous</button>
<div class="pagination-center">
<span data-i18n="browser.page">Page</span>
<input type="number" id="pageInput" class="page-input" min="1" value="1" onchange="goToPage()">
<span id="pageTotal">/ 1</span>
</div>
<button id="nextPage" onclick="nextPage()" data-i18n="browser.next">Next</button>
</div>
</div>
<!-- Scripts Section (Quick Actions) -->
<div class="scripts-container" id="scripts-container" data-tab-content="quick-actions" >
<h2 data-i18n="scripts.quick_actions">Quick Actions</h2>
<div class="scripts-grid" id="scripts-grid">
<div class="scripts-empty" data-i18n="scripts.no_scripts">No scripts configured</div>
</div>
</div>
<!-- Script Management Section -->
<div class="script-management" data-tab-content="scripts" >
<div class="script-management-header">
<h2 data-i18n="scripts.management">Script Management</h2>
<button class="add-script-btn" onclick="showAddScriptDialog()" data-i18n="scripts.add">+ Add</button>
</div>
<table class="scripts-table">
<thead>
<tr>
<th data-i18n="scripts.table.name">Name</th>
<th data-i18n="scripts.table.label">Label</th>
<th data-i18n="scripts.table.command">Command</th>
<th data-i18n="scripts.table.timeout">Timeout</th>
<th data-i18n="scripts.table.actions">Actions</th>
</tr>
</thead>
<tbody id="scriptsTableBody">
<tr>
<td colspan="5" class="empty-state" data-i18n="scripts.empty">No scripts configured. Click "Add" to create one.</td>
</tr>
</tbody>
</table>
</div>
<!-- Callback Management Section -->
<div class="script-management" id="callbacksSection" data-tab-content="callbacks" >
<div class="script-management-header">
<h2 data-i18n="callbacks.management">Callback Management</h2>
<button class="add-script-btn" onclick="showAddCallbackDialog()" data-i18n="callbacks.add">+ Add</button>
</div>
<p style="color: var(--text-secondary); font-size: 0.875rem; margin-bottom: 1rem;" data-i18n="callbacks.description">
Callbacks are scripts triggered automatically by media control events (play, pause, stop, etc.)
</p>
<table class="scripts-table">
<thead>
<tr>
<th data-i18n="callbacks.table.event">Event</th>
<th data-i18n="callbacks.table.command">Command</th>
<th data-i18n="callbacks.table.timeout">Timeout</th>
<th data-i18n="callbacks.table.actions">Actions</th>
</tr>
</thead>
<tbody id="callbacksTableBody">
<tr>
<td colspan="4" class="empty-state">No callbacks configured. Click "Add Callback" to create one.</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Add/Edit Script Dialog -->
<dialog id="scriptDialog">
<div class="dialog-header">
<h3 id="dialogTitle" data-i18n="scripts.dialog.add">Add Script</h3>
</div>
<form id="scriptForm" onsubmit="saveScript(event)">
<div class="dialog-body">
<input type="hidden" id="scriptOriginalName">
<input type="hidden" id="scriptIsEdit">
<label>
<span data-i18n="scripts.field.name">Script Name *</span>
<input type="text" id="scriptName" required pattern="[a-zA-Z0-9_]+"
data-i18n-title="scripts.placeholder.name" title="Only letters, numbers, and underscores allowed" maxlength="64">
</label>
<label>
<span data-i18n="scripts.field.label">Label</span>
<input type="text" id="scriptLabel" data-i18n-placeholder="scripts.placeholder.label" placeholder="Human-readable name">
</label>
<label>
<span data-i18n="scripts.field.command">Command *</span>
<textarea id="scriptCommand" required rows="3" data-i18n-placeholder="scripts.placeholder.command" placeholder="e.g., shutdown /s /t 0"></textarea>
</label>
<label>
<span data-i18n="scripts.field.description">Description</span>
<textarea id="scriptDescription" data-i18n-placeholder="scripts.placeholder.description" placeholder="What does this script do?"></textarea>
</label>
<label>
<span data-i18n="scripts.field.icon">Icon (MDI)</span>
<input type="text" id="scriptIcon" data-i18n-placeholder="scripts.placeholder.icon" placeholder="e.g., mdi:power">
</label>
<label>
<span data-i18n="scripts.field.timeout">Timeout (seconds)</span>
<input type="number" id="scriptTimeout" value="30" min="1" max="300">
</label>
</div>
<div class="dialog-footer">
<button type="button" class="btn-secondary" onclick="closeScriptDialog()" data-i18n="scripts.button.cancel">Cancel</button>
<button type="submit" class="btn-primary" data-i18n="scripts.button.save">Save</button>
</div>
</form>
</dialog>
<!-- Add/Edit Callback Dialog -->
<dialog id="callbackDialog">
<div class="dialog-header">
<h3 id="callbackDialogTitle" data-i18n="callbacks.dialog.add">Add Callback</h3>
</div>
<form id="callbackForm" onsubmit="saveCallback(event)">
<div class="dialog-body">
<input type="hidden" id="callbackIsEdit">
<label>
<span data-i18n="callbacks.field.event">Event *</span>
<select id="callbackName" required>
<option value="" data-i18n="callbacks.placeholder.event">Select event...</option>
<option value="on_play" data-i18n="callbacks.event.on_play">on_play - After play succeeds</option>
<option value="on_pause" data-i18n="callbacks.event.on_pause">on_pause - After pause succeeds</option>
<option value="on_stop" data-i18n="callbacks.event.on_stop">on_stop - After stop succeeds</option>
<option value="on_next" data-i18n="callbacks.event.on_next">on_next - After next track succeeds</option>
<option value="on_previous" data-i18n="callbacks.event.on_previous">on_previous - After previous track succeeds</option>
<option value="on_volume" data-i18n="callbacks.event.on_volume">on_volume - After volume change</option>
<option value="on_mute" data-i18n="callbacks.event.on_mute">on_mute - After mute toggle</option>
<option value="on_seek" data-i18n="callbacks.event.on_seek">on_seek - After seek succeeds</option>
<option value="on_turn_on" data-i18n="callbacks.event.on_turn_on">on_turn_on - Callback-only action</option>
<option value="on_turn_off" data-i18n="callbacks.event.on_turn_off">on_turn_off - Callback-only action</option>
<option value="on_toggle" data-i18n="callbacks.event.on_toggle">on_toggle - Callback-only action</option>
</select>
</label>
<label>
<span data-i18n="callbacks.field.command">Command *</span>
<textarea id="callbackCommand" required rows="3" data-i18n-placeholder="callbacks.placeholder.command" placeholder="e.g., shutdown /s /t 0"></textarea>
</label>
<label>
<span data-i18n="callbacks.field.timeout">Timeout (seconds)</span>
<input type="number" id="callbackTimeout" value="30" min="1" max="300">
</label>
<label>
<span data-i18n="callbacks.field.workdir">Working Directory</span>
<input type="text" id="callbackWorkingDir" data-i18n-placeholder="callbacks.placeholder.workdir" placeholder="Optional">
</label>
</div>
<div class="dialog-footer">
<button type="button" class="btn-secondary" onclick="closeCallbackDialog()" data-i18n="callbacks.button.cancel">Cancel</button>
<button type="submit" class="btn-primary" data-i18n="callbacks.button.save">Save</button>
</div>
</form>
</dialog>
<!-- Execution Result Dialog -->
<dialog id="executionDialog">
<div class="dialog-header">
<h3 id="executionDialogTitle">Execution Result</h3>
</div>
<div class="dialog-body">
<div class="execution-status" id="executionStatus"></div>
<div class="result-section" id="outputSection" style="display: none;">
<h4>Output</h4>
<div class="execution-result">
<pre id="executionOutput"></pre>
</div>
</div>
<div class="result-section" id="errorSection" style="display: none;">
<h4>Error Output</h4>
<div class="execution-result">
<pre id="executionError"></pre>
</div>
</div>
</div>
<div class="dialog-footer">
<button type="button" class="btn-secondary" onclick="closeExecutionDialog()">Close</button>
</div>
</dialog>
<!-- Folder Management Dialog -->
<dialog id="folderDialog">
<div class="dialog-header">
<h3 id="folderDialogTitle" data-i18n="browser.folder_dialog.title_add">Add Media Folder</h3>
</div>
<form id="folderForm" onsubmit="saveFolder(event)">
<div class="dialog-body">
<input type="hidden" id="folderIsEdit">
<input type="hidden" id="folderOriginalId">
<label>
<span data-i18n="browser.folder_dialog.folder_id">Folder ID *</span>
<input type="text" id="folderId" required pattern="[a-zA-Z0-9_]+"
data-i18n-title="browser.folder_dialog.folder_id_help" title="Alphanumeric and underscore only" maxlength="32">
</label>
<label>
<span data-i18n="browser.folder_dialog.label">Label *</span>
<input type="text" id="folderLabel" required data-i18n-placeholder="browser.folder_dialog.label_help" placeholder="Display name">
</label>
<label>
<span data-i18n="browser.folder_dialog.path">Path *</span>
<input type="text" id="folderPath" required data-i18n-placeholder="browser.folder_dialog.path_help" placeholder="C:\Users\YourName\Music">
</label>
<label style="display: flex; align-items: center; gap: 0.5rem;">
<input type="checkbox" id="folderEnabled" checked style="width: auto; margin: 0;">
<span data-i18n="browser.folder_dialog.enabled">Enabled</span>
</label>
</div>
<div class="dialog-footer">
<button type="button" class="btn-secondary" onclick="closeFolderDialog()" data-i18n="browser.folder_dialog.cancel">Cancel</button>
<button type="submit" class="btn-primary" data-i18n="browser.folder_dialog.save">Save</button>
</div>
</form>
</dialog>
<!-- Toast Notification -->
<div class="toast" id="toast"></div>
<!-- Footer -->
<footer>
<div>
Created by <strong>Alexei Dolgolyov</strong>
<span class="separator"></span>
<a href="mailto:dolgolyov.alexei@gmail.com">dolgolyov.alexei@gmail.com</a>
<span class="separator"></span>
<a href="https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server" target="_blank" rel="noopener noreferrer">Source Code</a>
</div>
</footer>
<script src="/static/js/app.js"></script>
</body>
</html>