Skip to content

Commit d095fdf

Browse files
committed
ci: Add GitHub Actions workflow for build and release
1 parent c723968 commit d095fdf

File tree

11 files changed

+233
-612
lines changed

11 files changed

+233
-612
lines changed

index.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -370,15 +370,15 @@ <h3 data-i18n-key="settings_app_header"></h3>
370370
<button id="select-rec-path-btn" style="padding: 0 10px; min-width: 40px; height: 35px;"><i class="material-icons" style="font-size: 20px;">folder_open</i></button>
371371
</div>
372372
</div>
373-
<!-- === НОВЫЙ БЛОК ДЛЯ ОБНОВЛЕНИЙ === -->
374-
<h3>Обновление приложения</h3>
373+
<!-- === ИЗМЕНЕННЫЙ БЛОК === -->
374+
<h3 data-i18n-key="update_header"></h3>
375375
<div class="form-grid simple">
376-
<span>Текущий статус</span>
377-
<span id="update-status-text" style="font-style: italic;">Нажмите кнопку для проверки...</span>
378-
<span>Действие</span>
379-
<button id="check-for-updates-btn" style="width: 200px; justify-self: start;">Проверить обновления</button>
376+
<span data-i18n-key="update_status_label"></span>
377+
<span id="update-status-text" style="font-style: italic;" data-i18n-key="update_check_prompt"></span>
378+
<span data-i18n-key="update_action_label"></span>
379+
<button id="check-for-updates-btn" style="width: 200px; justify-self: start;" data-i18n-key="update_check_button"></button>
380380
</div>
381-
<!-- === КОНЕЦ НОВОГО БЛОКА === -->
381+
<!-- === КОНЕЦ ИЗМЕНЕННОГО БЛОКА === -->
382382
</div>
383383

384384
<div id="tab-system" class="tab-content">

js/camera-list.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@
117117
e.preventDefault();
118118
const cameraId = parseInt(cameraItem.dataset.cameraId, 10);
119119
const labels = {
120+
open_in_browser: `🌐 ${App.t('context_open_in_browser')}`,
120121
files: `🗂️ ${App.t('context_file_manager')}`,
121122
ssh: `💻 ${App.t('context_ssh')}`,
122123
settings: `⚙️ ${App.t('context_settings')}`,
@@ -144,6 +145,9 @@
144145
};
145146

146147
switch(command) {
148+
case 'open_in_browser':
149+
window.api.openInBrowser(camera.ip);
150+
break;
147151
case 'files': window.api.openFileManager(cameraData); break;
148152
case 'ssh': window.api.openSshTerminal(cameraData); break;
149153
case 'settings': App.modalHandler.openSettingsModal(cameraId); break;

js/grid-manager.js

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
const gridContainer = document.getElementById('grid-container');
88
const layoutControls = document.getElementById('layout-controls');
99
const MAX_GRID_SIZE = 64;
10+
let reconnectTimers = {};
1011

1112
let gridCols = 2;
1213
let gridRows = 2;
@@ -307,6 +308,11 @@
307308
}
308309

309310
async function stopStreamInCell(cellIndex, clearCellUI = true) {
311+
if (reconnectTimers[cellIndex]) {
312+
clearTimeout(reconnectTimers[cellIndex]);
313+
delete reconnectTimers[cellIndex];
314+
}
315+
310316
const state = gridCellsState[cellIndex];
311317
await destroyPlayerInCell(cellIndex);
312318
if (state) {
@@ -384,23 +390,50 @@
384390
}
385391
}
386392
}
387-
393+
388394
function handleStreamDeath(uniqueStreamIdentifier) {
389395
const cellIndex = gridCellsState.findIndex(s => s?.uniqueStreamIdentifier === uniqueStreamIdentifier);
390396
if (cellIndex === -1) return;
391-
397+
398+
if (reconnectTimers[cellIndex]) {
399+
clearTimeout(reconnectTimers[cellIndex]);
400+
}
401+
392402
const { camera, streamId } = gridCellsState[cellIndex];
393403
const cellElement = document.querySelector(`[data-cell-id='${cellIndex}']`);
394404
if (cellElement) {
395-
cellElement.innerHTML = `<span>${App.t('stream_died_reconnecting')}</span>`;
405+
cellElement.innerHTML = `
406+
<div style="text-align: center; padding: 10px;">
407+
<span>${App.t('stream_died_reconnecting')}</span>
408+
<button class="cancel-reconnect-btn" style="display: block; margin: 10px auto 0; padding: 5px 10px; background-color: #555; border: 1px solid #666; color: white; border-radius: 4px; cursor: pointer;">
409+
${App.t('cancel_reconnect')}
410+
</button>
411+
</div>
412+
`;
396413
cellElement.classList.remove('active');
397414
cellElement.draggable = false;
415+
416+
const cancelButton = cellElement.querySelector('.cancel-reconnect-btn');
417+
if (cancelButton) {
418+
cancelButton.onclick = (e) => {
419+
e.stopPropagation();
420+
console.log(`[Grid] Reconnect for cell ${cellIndex} cancelled by user.`);
421+
clearTimeout(reconnectTimers[cellIndex]);
422+
delete reconnectTimers[cellIndex];
423+
stopStreamInCell(cellIndex, true);
424+
};
425+
}
398426
}
399-
gridCellsState[cellIndex] = null;
427+
gridCellsState[cellIndex] = null;
400428
console.log(`[Grid] Stream ${uniqueStreamIdentifier} died. Reconnecting in 5 seconds.`);
401-
setTimeout(() => {
402-
if (!gridCellsState[cellIndex]) startStreamInCell(cellIndex, camera.id, streamId);
403-
else console.log(`[Grid] Reconnect cancelled for cell ${cellIndex}, it's already occupied.`);
429+
430+
reconnectTimers[cellIndex] = setTimeout(() => {
431+
delete reconnectTimers[cellIndex];
432+
if (!gridCellsState[cellIndex]) {
433+
startStreamInCell(cellIndex, camera.id, streamId);
434+
} else {
435+
console.log(`[Grid] Reconnect cancelled for cell ${cellIndex}, it's already occupied.`);
436+
}
404437
}, 5000);
405438
}
406439

@@ -455,6 +488,7 @@
455488
if (state && state.camera) {
456489
e.preventDefault();
457490
const labels = {
491+
open_in_browser: `🌐 ${App.t('context_open_in_browser')}`,
458492
files: `🗂️ ${App.t('context_file_manager')}`,
459493
ssh: `💻 ${App.t('context_ssh')}`,
460494
settings: `⚙️ ${App.t('context_settings')}`,
@@ -469,6 +503,7 @@
469503
toggleFullscreen(fullscreenCellIndex);
470504
}
471505
});
506+
window.addEventListener('language-changed', updatePlaceholdersLanguage);
472507
}
473508

474509
return {

js/i18n.js

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,32 @@
88
const supportedLangs = ['en', 'ru'];
99
let currentLang = 'en';
1010

11-
// Определяем язык браузера, если в настройках ничего нет
1211
const getPreferredLanguage = () => {
1312
const lang = (navigator.language || navigator.userLanguage).split('-')[0];
1413
return supportedLangs.includes(lang) ? lang : 'en';
1514
};
1615

17-
// Загружаем файл локализации
1816
async function loadTranslations(lang) {
1917
try {
20-
const response = await fetch(`./locales/${lang}.json`);
21-
if (!response.ok) throw new Error(`Failed to load ${lang}.json`);
22-
translations = await response.json();
18+
// ИЗМЕНЕНИЕ: Загрузка переводов через main процесс для надежности
19+
const loadedTranslations = await window.api.getTranslationFile(lang);
20+
if (!loadedTranslations) throw new Error(`Failed to load ${lang}.json`);
21+
22+
translations = loadedTranslations;
2323
currentLang = lang;
2424
document.documentElement.lang = lang;
2525
console.log(`Translations for '${lang}' loaded.`);
2626
return true;
2727
} catch (error) {
2828
console.error('Error loading translation file:', error);
2929
if (lang !== 'en') {
30+
console.log('Falling back to English.');
3031
return await loadTranslations('en'); // fallback to English
3132
}
3233
return false;
3334
}
3435
}
3536

36-
// Функция перевода
3737
function t(key, replacements = {}) {
3838
let translation = translations[key] || key;
3939
for (const placeholder in replacements) {
@@ -42,52 +42,46 @@
4242
return translation;
4343
}
4444

45-
// Применяем переводы ко всем статическим элементам на странице
4645
function applyTranslationsToDOM() {
47-
// Текстовое содержимое
4846
document.querySelectorAll('[data-i18n-key]').forEach(element => {
4947
const key = element.getAttribute('data-i18n-key');
5048
element.textContent = t(key);
5149
});
52-
// Всплывающие подсказки
5350
document.querySelectorAll('[data-i18n-tooltip]').forEach(element => {
5451
const key = element.getAttribute('data-i18n-tooltip');
5552
element.title = t(key);
5653
});
57-
// Плейсхолдеры в инпутах
5854
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
5955
const key = element.getAttribute('data-i18n-placeholder');
6056
element.placeholder = t(key);
6157
});
6258
}
6359

64-
// Функция для смены языка "на лету"
60+
// ИЗМЕНЕНИЕ: Функция теперь напрямую вызывает обновление DOM и событие
6561
async function setLanguage(lang) {
6662
if (!supportedLangs.includes(lang) || lang === currentLang) {
6763
return;
6864
}
69-
await loadTranslations(lang);
70-
applyTranslationsToDOM();
71-
72-
// Перерисовываем компоненты, где текст генерируется динамически
73-
App.cameraList.render();
74-
App.gridManager.updatePlaceholdersLanguage(); // Обновляем плейсхолдеры в сетке
65+
const success = await loadTranslations(lang);
66+
if (success) {
67+
applyTranslationsToDOM();
68+
// Генерируем событие, чтобы другие модули могли на него отреагировать
69+
window.dispatchEvent(new CustomEvent('language-changed'));
70+
}
7571
}
7672

77-
// Инициализация при старте приложения
7873
async function init() {
79-
// Сначала берем язык из настроек приложения, если он там есть
8074
const lang = App.appSettings.language || getPreferredLanguage();
8175
await loadTranslations(lang);
82-
applyTranslationsToDOM();
8376
App.t = t; // Делаем функцию перевода доступной глобально в App
77+
applyTranslationsToDOM(); // Первоначальный перевод
8478
}
8579

8680
return {
8781
init,
8882
t,
89-
setLanguage // Экспортируем функцию смены языка
83+
setLanguage,
84+
applyTranslationsToDOM // Экспортируем на всякий случай
9085
};
9186
};
92-
9387
})(window);

0 commit comments

Comments
 (0)