diff --git a/media_server/static/index.html b/media_server/static/index.html index 6cae0e1..bccb8cd 100644 --- a/media_server/static/index.html +++ b/media_server/static/index.html @@ -108,22 +108,30 @@ fill: var(--text-primary); } - #locale-toggle { - background: none; - border: 2px solid var(--text-secondary); + #locale-select { + background: var(--bg-tertiary); + border: 1px solid var(--border); color: var(--text-primary); border-radius: 6px; padding: 6px 12px; cursor: pointer; font-size: 14px; - font-weight: bold; + font-weight: 500; transition: all 0.3s ease; } - #locale-toggle:hover { + #locale-select:hover { border-color: var(--accent); - color: var(--accent); - transform: scale(1.05); + } + + #locale-select:focus { + outline: none; + border-color: var(--accent); + } + + #locale-select option { + background: var(--bg-secondary); + color: var(--text-primary); } .player-container { @@ -486,6 +494,7 @@ padding: 0; max-width: 500px; width: 90%; + margin: auto; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8); } @@ -550,13 +559,15 @@ } .dialog-footer button { - padding: 0.5rem 1rem; + padding: 0.625rem 1.5rem; border-radius: 6px; border: none; font-size: 0.875rem; font-weight: 600; cursor: pointer; transition: background 0.2s; + min-width: 100px; + white-space: nowrap; } .dialog-footer .btn-primary { @@ -790,7 +801,10 @@ - +
Disconnected @@ -920,7 +934,7 @@
-

Add Script

+

Add Script

@@ -928,39 +942,39 @@
@@ -968,48 +982,48 @@
-

Add Callback

+

Add Callback

@@ -1049,7 +1063,10 @@ // Locale management let currentLocale = 'en'; let translations = {}; - const supportedLocales = ['en', 'ru']; + const supportedLocales = { + 'en': 'English', + 'ru': 'Русский' + }; // Minimal inline fallback for critical UI elements const fallbackTranslations = { @@ -1096,7 +1113,7 @@ const langCode = browserLang.split('-')[0]; // 'en-US' -> 'en', 'ru-RU' -> 'ru' // Only return if we support it - return supportedLocales.includes(langCode) ? langCode : 'en'; + return supportedLocales[langCode] ? langCode : 'en'; } // Initialize locale @@ -1107,7 +1124,7 @@ // Set locale async function setLocale(locale) { - if (!supportedLocales.includes(locale)) { + if (!supportedLocales[locale]) { locale = 'en'; } @@ -1122,22 +1139,25 @@ // Update all text updateAllText(); - // Update locale toggle button (if visible) - updateLocaleToggle(); + // Update locale select dropdown (if visible) + updateLocaleSelect(); } - // Toggle between locales - async function toggleLocale() { - const newLocale = currentLocale === 'en' ? 'ru' : 'en'; - await setLocale(newLocale); + // Change locale from dropdown + function changeLocale() { + const select = document.getElementById('locale-select'); + const newLocale = select.value; + if (newLocale && newLocale !== currentLocale) { + localStorage.setItem('locale', newLocale); + setLocale(newLocale); + } } - // Update locale toggle button - function updateLocaleToggle() { - const localeButton = document.getElementById('locale-toggle'); - if (localeButton) { - localeButton.textContent = currentLocale === 'en' ? 'RU' : 'EN'; - localeButton.title = t('player.locale'); + // Update locale select dropdown + function updateLocaleSelect() { + const select = document.getElementById('locale-select'); + if (select) { + select.value = currentLocale; } } @@ -1192,6 +1212,10 @@ let scripts = []; let lastStatus = null; // Store last status for locale switching + // Dialog dirty state tracking + let scriptFormDirty = false; + let callbackFormDirty = false; + // Position interpolation let lastPositionUpdate = 0; let lastPositionValue = 0; @@ -1247,6 +1271,42 @@ authenticate(); } }); + + // Script form dirty state tracking + const scriptForm = document.getElementById('scriptForm'); + scriptForm.addEventListener('input', () => { + scriptFormDirty = true; + }); + scriptForm.addEventListener('change', () => { + scriptFormDirty = true; + }); + + // Callback form dirty state tracking + const callbackForm = document.getElementById('callbackForm'); + callbackForm.addEventListener('input', () => { + callbackFormDirty = true; + }); + callbackForm.addEventListener('change', () => { + callbackFormDirty = true; + }); + + // Script dialog backdrop click to close + const scriptDialog = document.getElementById('scriptDialog'); + scriptDialog.addEventListener('click', (e) => { + // Check if click is on the backdrop (not the dialog content) + if (e.target === scriptDialog) { + closeScriptDialog(); + } + }); + + // Callback dialog backdrop click to close + const callbackDialog = document.getElementById('callbackDialog'); + callbackDialog.addEventListener('click', (e) => { + // Check if click is on the backdrop (not the dialog content) + if (e.target === callbackDialog) { + closeCallbackDialog(); + } + }); }); function showAuthForm(errorMessage = '') { @@ -1707,7 +1767,10 @@ document.getElementById('scriptOriginalName').value = ''; document.getElementById('scriptIsEdit').value = 'false'; document.getElementById('scriptName').disabled = false; - title.textContent = 'Add Script'; + title.textContent = t('scripts.dialog.add'); + + // Reset dirty state + scriptFormDirty = false; dialog.showModal(); } @@ -1746,7 +1809,11 @@ document.getElementById('scriptIcon').value = script.icon || ''; document.getElementById('scriptTimeout').value = script.timeout || 30; - title.textContent = 'Edit Script'; + title.textContent = t('scripts.dialog.edit'); + + // Reset dirty state + scriptFormDirty = false; + dialog.showModal(); } catch (error) { console.error('Error loading script for edit:', error); @@ -1755,7 +1822,15 @@ } function closeScriptDialog() { + // Check if form has unsaved changes + if (scriptFormDirty) { + if (!confirm(t('scripts.confirm.unsaved'))) { + return; // User cancelled, don't close + } + } + const dialog = document.getElementById('scriptDialog'); + scriptFormDirty = false; // Reset dirty state dialog.close(); } @@ -1797,6 +1872,7 @@ if (response.ok && result.success) { showToast(`Script ${isEdit ? 'updated' : 'created'} successfully`, 'success'); + scriptFormDirty = false; // Reset dirty state before closing closeScriptDialog(); // Don't reload manually - WebSocket will trigger it } else { @@ -1886,7 +1962,10 @@ form.reset(); document.getElementById('callbackIsEdit').value = 'false'; document.getElementById('callbackName').disabled = false; - title.textContent = 'Add Callback'; + title.textContent = t('callbacks.dialog.add'); + + // Reset dirty state + callbackFormDirty = false; dialog.showModal(); } @@ -1922,7 +2001,11 @@ document.getElementById('callbackTimeout').value = callback.timeout; document.getElementById('callbackWorkingDir').value = callback.working_dir || ''; - title.textContent = 'Edit Callback'; + title.textContent = t('callbacks.dialog.edit'); + + // Reset dirty state + callbackFormDirty = false; + dialog.showModal(); } catch (error) { console.error('Error loading callback for edit:', error); @@ -1931,7 +2014,15 @@ } function closeCallbackDialog() { + // Check if form has unsaved changes + if (callbackFormDirty) { + if (!confirm(t('callbacks.confirm.unsaved'))) { + return; // User cancelled, don't close + } + } + const dialog = document.getElementById('callbackDialog'); + callbackFormDirty = false; // Reset dirty state dialog.close(); } @@ -1969,6 +2060,7 @@ if (response.ok && result.success) { showToast(`Callback ${isEdit ? 'updated' : 'created'} successfully`, 'success'); + callbackFormDirty = false; // Reset dirty state before closing closeCallbackDialog(); loadCallbacksTable(); } else { diff --git a/media_server/static/locales/en.json b/media_server/static/locales/en.json index 2f25018..1caf307 100644 --- a/media_server/static/locales/en.json +++ b/media_server/static/locales/en.json @@ -64,6 +64,7 @@ "scripts.msg.load_failed": "Failed to load script details", "scripts.msg.list_failed": "Failed to load scripts", "scripts.confirm.delete": "Are you sure you want to delete the script \"{name}\"?", + "scripts.confirm.unsaved": "You have unsaved changes. Are you sure you want to discard them?", "callbacks.management": "Callback Management", "callbacks.description": "Callbacks are scripts triggered automatically by media control events (play, pause, stop, etc.)", "callbacks.add": "Add", @@ -105,5 +106,6 @@ "callbacks.msg.not_found": "Callback not found", "callbacks.msg.load_failed": "Failed to load callback details", "callbacks.msg.list_failed": "Failed to load callbacks", - "callbacks.confirm.delete": "Are you sure you want to delete the callback \"{name}\"?" + "callbacks.confirm.delete": "Are you sure you want to delete the callback \"{name}\"?", + "callbacks.confirm.unsaved": "You have unsaved changes. Are you sure you want to discard them?" } diff --git a/media_server/static/locales/ru.json b/media_server/static/locales/ru.json index 061d395..75ea136 100644 --- a/media_server/static/locales/ru.json +++ b/media_server/static/locales/ru.json @@ -64,6 +64,7 @@ "scripts.msg.load_failed": "Не удалось загрузить данные скрипта", "scripts.msg.list_failed": "Не удалось загрузить скрипты", "scripts.confirm.delete": "Вы уверены, что хотите удалить скрипт \"{name}\"?", + "scripts.confirm.unsaved": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?", "callbacks.management": "Управление Обратными Вызовами", "callbacks.description": "Обратные вызовы - это скрипты, автоматически запускаемые при событиях управления медиа (воспроизведение, пауза, остановка и т.д.)", "callbacks.add": "Добавить", @@ -105,5 +106,6 @@ "callbacks.msg.not_found": "Обратный вызов не найден", "callbacks.msg.load_failed": "Не удалось загрузить данные обратного вызова", "callbacks.msg.list_failed": "Не удалось загрузить обратные вызовы", - "callbacks.confirm.delete": "Вы уверены, что хотите удалить обратный вызов \"{name}\"?" + "callbacks.confirm.delete": "Вы уверены, что хотите удалить обратный вызов \"{name}\"?", + "callbacks.confirm.unsaved": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?" }