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 @@
@@ -968,48 +982,48 @@
@@ -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": "У вас есть несохраненные изменения. Вы уверены, что хотите отменить их?"
}