Skip to content

Commit 8c4d6cb

Browse files
committed
ci: Add GitHub Actions workflow for build and release
1 parent c5d10de commit 8c4d6cb

File tree

11 files changed

+573
-256
lines changed

11 files changed

+573
-256
lines changed

index.html

Lines changed: 79 additions & 75 deletions
Large diffs are not rendered by default.

js/camera-list.js

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// js/camera-list.js
2+
13
(function(window) {
24
window.AppModules = window.AppModules || {};
35

@@ -16,7 +18,7 @@
1618
}
1719

1820
async function deleteCamera(cameraId) {
19-
if (confirm('Вы уверены, что хотите удалить эту камеру?')) {
21+
if (confirm(App.t('confirm_delete_camera'))) {
2022
if (App.recordingStates[cameraId]) {
2123
await window.api.stopRecording(cameraId);
2224
}
@@ -88,12 +90,12 @@
8890

8991
const ungroupedCameras = App.cameras.filter(c => !c.groupId);
9092
if (ungroupedCameras.length > 0) {
91-
const ungroupedPseudoGroup = { id: null, name: 'Камеры без группы' };
93+
const ungroupedPseudoGroup = { id: null, name: App.t('ungrouped_cameras') };
9294
cameraListContainer.appendChild(createGroupHTML(ungroupedPseudoGroup, ungroupedCameras));
9395
}
9496

9597
if (cameraListContainer.innerHTML === '') {
96-
cameraListContainer.innerHTML = '<p style="padding: 10px; color: var(--text-secondary);">Камер и групп нет.</p>';
98+
cameraListContainer.innerHTML = `<p style="padding: 10px; color: var(--text-secondary);">${App.t('no_cameras_or_groups')}</p>`;
9799
}
98100

99101
pollCameraStatuses();
@@ -109,14 +111,43 @@
109111
function init() {
110112
openRecordingsBtn.addEventListener('click', () => window.api.openRecordingsFolder());
111113

114+
cameraListContainer.addEventListener('contextmenu', (e) => {
115+
const cameraItem = e.target.closest('.camera-item');
116+
if (cameraItem) {
117+
e.preventDefault();
118+
const cameraId = parseInt(cameraItem.dataset.cameraId, 10);
119+
const labels = {
120+
files: `🗂️ ${App.t('context_file_manager')}`,
121+
ssh: `💻 ${App.t('context_ssh')}`,
122+
settings: `⚙️ ${App.t('context_settings')}`,
123+
edit: `✏️ ${App.t('context_edit')}`,
124+
delete: `🗑️ ${App.t('context_delete')}`
125+
};
126+
window.api.showCameraContextMenu({ cameraId, labels });
127+
}
128+
});
129+
112130
window.api.onContextMenuCommand(({ command, cameraId }) => {
113131
const camera = App.cameras.find(c => c.id === cameraId);
114132
if (!camera) return;
133+
134+
// Создаем чистый объект для передачи через IPC
135+
const cameraData = {
136+
id: camera.id,
137+
name: camera.name,
138+
ip: camera.ip,
139+
port: camera.port,
140+
username: camera.username,
141+
password: camera.password,
142+
streamPath0: camera.streamPath0,
143+
streamPath1: camera.streamPath1
144+
};
145+
115146
switch(command) {
116-
case 'files': window.api.openFileManager(camera); break;
117-
case 'ssh': window.api.openSshTerminal(camera); break;
147+
case 'files': window.api.openFileManager(cameraData); break;
148+
case 'ssh': window.api.openSshTerminal(cameraData); break;
118149
case 'settings': App.modalHandler.openSettingsModal(cameraId); break;
119-
case 'edit': App.modalHandler.openAddModal(camera); break;
150+
case 'edit': App.modalHandler.openAddModal(cameraData); break;
120151
case 'delete': deleteCamera(cameraId); break;
121152
}
122153
});

js/grid-manager.js

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,19 @@
1313
let gridCellsState = Array(MAX_GRID_SIZE).fill(null);
1414
let fullscreenCellIndex = null;
1515

16+
function updatePlaceholdersLanguage() {
17+
const placeholderHTML = `<span><i class="material-icons placeholder-icon">add_photo_alternate</i><br>${App.t('drop_camera_here')}</span>`;
18+
19+
for (let i = 0; i < gridCellsState.length; i++) {
20+
if (gridCellsState[i] === null) {
21+
const cell = gridContainer.querySelector(`[data-cell-id='${i}']`);
22+
if (cell) {
23+
cell.innerHTML = placeholderHTML;
24+
}
25+
}
26+
}
27+
}
28+
1629
function getGridSize() {
1730
return { cols: gridCols, rows: gridRows };
1831
}
@@ -34,7 +47,7 @@
3447
btn.className = 'layout-btn';
3548
btn.dataset.layout = layout;
3649
btn.textContent = layout.split('x').reduce((a, b) => a * b, 1);
37-
btn.title = `Раскладка ${layout}`;
50+
btn.title = `Layout ${layout}`;
3851
btn.onclick = () => {
3952
const [cols, rows] = layout.split('x').map(Number);
4053
setGridLayout(cols, rows);
@@ -109,7 +122,7 @@
109122
cell.style.display = 'none';
110123
cell.ondblclick = () => toggleFullscreen(i);
111124

112-
cell.innerHTML = `<span><i class="material-icons placeholder-icon">add_photo_alternate</i><br>Перетащите камеру</span>`;
125+
cell.innerHTML = `<span><i class="material-icons placeholder-icon">add_photo_alternate</i><br>${App.t('drop_camera_here')}</span>`;
113126

114127
cell.addEventListener('dragover', (e) => { e.preventDefault(); cell.classList.add('drag-over'); });
115128
cell.addEventListener('dragleave', () => cell.classList.remove('drag-over'));
@@ -184,7 +197,7 @@
184197
sourceCell.classList.toggle('active', !!sourceState);
185198
targetCell.classList.toggle('active', !!targetState);
186199

187-
const placeholderHTML = `<span><i class="material-icons placeholder-icon">add_photo_alternate</i><br>Перетащите камеру</span>`;
200+
const placeholderHTML = `<span><i class="material-icons placeholder-icon">add_photo_alternate</i><br>${App.t('drop_camera_here')}</span>`;
188201
if (!sourceState) sourceCell.innerHTML = placeholderHTML;
189202
if (!targetState) targetCell.innerHTML = placeholderHTML;
190203

@@ -204,7 +217,7 @@
204217
const cellElement = document.querySelector(`[data-cell-id='${cellIndex}']`);
205218
if (!cellElement) return;
206219

207-
cellElement.innerHTML = `<span>Подключение...</span>`;
220+
cellElement.innerHTML = `<span>${App.t('connecting')}</span>`;
208221
cellElement.classList.add('active');
209222
cellElement.draggable = true;
210223
setupDragStartForCell(cellElement, cellIndex);
@@ -239,7 +252,6 @@
239252
cellElement.appendChild(nameDiv);
240253
cellElement.appendChild(statsDiv);
241254

242-
// ИЗМЕНЕНИЕ ЗДЕСЬ: добавлен onVideoDecode
243255
const player = new JSMpeg.Player(`ws://localhost:${result.wsPort}`, {
244256
canvas,
245257
autoplay: true,
@@ -276,7 +288,7 @@
276288

277289
gridCellsState[cellIndex].player = player;
278290
} else {
279-
cellElement.innerHTML = `<span>Ошибка: ${result.error || 'Неизвестная ошибка'}</span>`;
291+
cellElement.innerHTML = `<span>${App.t('error')}: ${result.error || App.t('unknown_error')}</span>`;
280292
cellElement.classList.remove('active');
281293
cellElement.draggable = false;
282294
gridCellsState[cellIndex] = null;
@@ -306,7 +318,7 @@
306318
if (clearCellUI) {
307319
const cellElement = document.querySelector(`[data-cell-id='${cellIndex}']`);
308320
if(cellElement) {
309-
cellElement.innerHTML = `<span><i class="material-icons placeholder-icon">add_photo_alternate</i><br>Перетащите камеру</span>`;
321+
cellElement.innerHTML = `<span><i class="material-icons placeholder-icon">add_photo_alternate</i><br>${App.t('drop_camera_here')}</span>`;
310322
cellElement.classList.remove('active');
311323
cellElement.draggable = false;
312324
}
@@ -323,7 +335,7 @@
323335

324336
await destroyPlayerInCell(cellIndex);
325337
const cellElement = document.querySelector(`[data-cell-id='${cellIndex}']`);
326-
if(cellElement) cellElement.innerHTML = '<span>Переключение потока...</span>';
338+
if(cellElement) cellElement.innerHTML = `<span>${App.t('switch_stream')}</span>`;
327339

328340
await startStreamInCell(cellIndex, cameraId, newStreamId);
329341

@@ -346,7 +358,7 @@
346358
const currentVolume = state.player ? state.player.volume : 0;
347359

348360
await destroyPlayerInCell(cellIndex);
349-
cell.innerHTML = '<span>Переключение...</span>';
361+
cell.innerHTML = `<span>${App.t('switch_fullscreen')}</span>`;
350362

351363
if (isCurrentlyFullscreen) {
352364
fullscreenCellIndex = null;
@@ -380,7 +392,7 @@
380392
const { camera, streamId } = gridCellsState[cellIndex];
381393
const cellElement = document.querySelector(`[data-cell-id='${cellIndex}']`);
382394
if (cellElement) {
383-
cellElement.innerHTML = `<span>Потеря связи.<br>Переподключение через 5с...</span>`;
395+
cellElement.innerHTML = `<span>${App.t('stream_died_reconnecting')}</span>`;
384396
cellElement.classList.remove('active');
385397
cellElement.draggable = false;
386398
}
@@ -442,7 +454,14 @@
442454
const state = gridCellsState[cellIndex];
443455
if (state && state.camera) {
444456
e.preventDefault();
445-
window.api.showCameraContextMenu(state.camera.id);
457+
const labels = {
458+
files: `🗂️ ${App.t('context_file_manager')}`,
459+
ssh: `💻 ${App.t('context_ssh')}`,
460+
settings: `⚙️ ${App.t('context_settings')}`,
461+
edit: `✏️ ${App.t('context_edit')}`,
462+
delete: `🗑️ ${App.t('context_delete')}`
463+
};
464+
window.api.showCameraContextMenu({ cameraId: state.camera.id, labels });
446465
}
447466
});
448467
window.addEventListener('keydown', (e) => {
@@ -462,7 +481,8 @@
462481
updateRecordingState,
463482
restartStreamsForCamera,
464483
updateCameraNameInGrid,
465-
removeStreamsForCamera
484+
removeStreamsForCamera,
485+
updatePlaceholdersLanguage
466486
}
467487
}
468488
})(window);

js/i18n.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// js/i18n.js
2+
(function(window) {
3+
'use strict';
4+
window.AppModules = window.AppModules || {};
5+
6+
AppModules.createI18n = function(App) {
7+
let translations = {};
8+
const supportedLangs = ['en', 'ru'];
9+
let currentLang = 'en';
10+
11+
// Определяем язык браузера, если в настройках ничего нет
12+
const getPreferredLanguage = () => {
13+
const lang = (navigator.language || navigator.userLanguage).split('-')[0];
14+
return supportedLangs.includes(lang) ? lang : 'en';
15+
};
16+
17+
// Загружаем файл локализации
18+
async function loadTranslations(lang) {
19+
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();
23+
currentLang = lang;
24+
document.documentElement.lang = lang;
25+
console.log(`Translations for '${lang}' loaded.`);
26+
return true;
27+
} catch (error) {
28+
console.error('Error loading translation file:', error);
29+
if (lang !== 'en') {
30+
return await loadTranslations('en'); // fallback to English
31+
}
32+
return false;
33+
}
34+
}
35+
36+
// Функция перевода
37+
function t(key, replacements = {}) {
38+
let translation = translations[key] || key;
39+
for (const placeholder in replacements) {
40+
translation = translation.replace(`{{${placeholder}}}`, replacements[placeholder]);
41+
}
42+
return translation;
43+
}
44+
45+
// Применяем переводы ко всем статическим элементам на странице
46+
function applyTranslationsToDOM() {
47+
// Текстовое содержимое
48+
document.querySelectorAll('[data-i18n-key]').forEach(element => {
49+
const key = element.getAttribute('data-i18n-key');
50+
element.textContent = t(key);
51+
});
52+
// Всплывающие подсказки
53+
document.querySelectorAll('[data-i18n-tooltip]').forEach(element => {
54+
const key = element.getAttribute('data-i18n-tooltip');
55+
element.title = t(key);
56+
});
57+
// Плейсхолдеры в инпутах
58+
document.querySelectorAll('[data-i18n-placeholder]').forEach(element => {
59+
const key = element.getAttribute('data-i18n-placeholder');
60+
element.placeholder = t(key);
61+
});
62+
}
63+
64+
// Функция для смены языка "на лету"
65+
async function setLanguage(lang) {
66+
if (!supportedLangs.includes(lang) || lang === currentLang) {
67+
return;
68+
}
69+
await loadTranslations(lang);
70+
applyTranslationsToDOM();
71+
72+
// Перерисовываем компоненты, где текст генерируется динамически
73+
App.cameraList.render();
74+
App.gridManager.updatePlaceholdersLanguage(); // Обновляем плейсхолдеры в сетке
75+
}
76+
77+
// Инициализация при старте приложения
78+
async function init() {
79+
// Сначала берем язык из настроек приложения, если он там есть
80+
const lang = App.appSettings.language || getPreferredLanguage();
81+
await loadTranslations(lang);
82+
applyTranslationsToDOM();
83+
App.t = t; // Делаем функцию перевода доступной глобально в App
84+
}
85+
86+
return {
87+
init,
88+
t,
89+
setLanguage // Экспортируем функцию смены языка
90+
};
91+
};
92+
93+
})(window);

0 commit comments

Comments
 (0)