Skip to content

Commit c723968

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

File tree

7 files changed

+283
-34
lines changed

7 files changed

+283
-34
lines changed

index.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +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>
375+
<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>
380+
</div>
381+
<!-- === КОНЕЦ НОВОГО БЛОКА === -->
373382
</div>
374383

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

js/modal-handler.js

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@
2222
const settingsToast = document.getElementById('settings-toast');
2323
const recordingsPathInput = document.getElementById('app-settings-recordings-path');
2424
const selectRecPathBtn = document.getElementById('select-rec-path-btn');
25-
const languageSelect = document.getElementById('app-settings-language'); // НОВЫЙ ЭЛЕМЕНТ
25+
const languageSelect = document.getElementById('app-settings-language');
26+
27+
// === НОВЫЕ ЭЛЕМЕНТЫ ДЛЯ ОБНОВЛЕНИЯ ===
28+
const checkForUpdatesBtn = document.getElementById('check-for-updates-btn');
29+
const updateStatusText = document.getElementById('update-status-text');
30+
2631
let toastTimeout;
27-
2832
let editingCameraId = null;
2933
let settingsCameraId = null;
3034
let initialSettings = null;
@@ -170,7 +174,7 @@
170174
}
171175

172176
recordingsPathInput.value = App.appSettings.recordingsPath || '';
173-
languageSelect.value = App.appSettings.language || 'en'; // Устанавливаем язык
177+
languageSelect.value = App.appSettings.language || 'en';
174178
restartMajesticBtn.style.display = isGeneralSettings ? 'none' : 'inline-flex';
175179

176180
openModal(settingsModal);
@@ -364,12 +368,10 @@
364368
saveSettingsBtn.disabled = true;
365369
saveSettingsBtn.textContent = App.t('saving_text');
366370

367-
// Сохраняем общие настройки приложения
368371
App.appSettings.recordingsPath = recordingsPathInput.value.trim();
369372
App.appSettings.language = languageSelect.value;
370373
await window.api.saveAppSettings(App.appSettings);
371374

372-
// Если это были только общие настройки, выходим
373375
if (settingsCameraId === null) {
374376
saveSettingsBtn.disabled = false;
375377
saveSettingsBtn.textContent = App.t('save');
@@ -391,7 +393,6 @@
391393
};
392394

393395
const settingsDataToSend = {
394-
// ... (все ключи настроек камеры остаются без изменений)
395396
'motionDetect.enabled': getFormValue('motionDetect.enabled'),
396397
'motionDetect.visualize': getFormValue('motionDetect.visualize'),
397398
'motionDetect.debug': getFormValue('motionDetect.debug'),
@@ -563,8 +564,7 @@
563564
addGroupModal.addEventListener('click', (e) => { if (e.target === addGroupModal) closeModal(addGroupModal); });
564565

565566
generalSettingsBtn.addEventListener('click', () => openSettingsModal(null));
566-
567-
// НОВЫЙ ОБРАБОТЧИК: Смена языка
567+
568568
languageSelect.addEventListener('change', async (e) => {
569569
await App.i18n.setLanguage(e.target.value);
570570
});
@@ -596,6 +596,46 @@
596596
});
597597
});
598598

599+
// === НОВАЯ ЛОГИКА ДЛЯ КНОПКИ И СТАТУСА ОБНОВЛЕНИЯ ===
600+
checkForUpdatesBtn.addEventListener('click', () => {
601+
updateStatusText.textContent = 'Проверка...';
602+
checkForUpdatesBtn.disabled = true;
603+
window.api.checkForUpdates();
604+
});
605+
606+
window.api.onUpdateStatus(({ status, message }) => {
607+
// Этот обработчик будет обновлять текст в модальном окне
608+
checkForUpdatesBtn.disabled = false;
609+
let version = message.includes('версия') ? message.split(' ').pop() : '';
610+
611+
switch (status) {
612+
case 'available':
613+
updateStatusText.textContent = `Доступна новая версия: ${version}`;
614+
updateStatusText.style.color = '#ffc107'; // Желтый
615+
break;
616+
case 'downloading':
617+
updateStatusText.textContent = message;
618+
updateStatusText.style.color = '#17a2b8'; // Голубой
619+
checkForUpdatesBtn.disabled = true; // Блокируем кнопку во время загрузки
620+
break;
621+
case 'downloaded':
622+
updateStatusText.textContent = `Обновление загружено! Перезапустите приложение.`;
623+
updateStatusText.style.color = '#28a745'; // Зеленый
624+
break;
625+
case 'error':
626+
updateStatusText.textContent = message;
627+
updateStatusText.style.color = '#dc3545'; // Красный
628+
break;
629+
case 'latest':
630+
updateStatusText.textContent = 'У вас установлена последняя версия.';
631+
updateStatusText.style.color = 'green';
632+
break;
633+
default:
634+
updateStatusText.textContent = 'Нажмите кнопку для проверки...';
635+
updateStatusText.style.color = 'inherit';
636+
}
637+
});
638+
599639
window.addEventListener('keydown', (e) => {
600640
if (e.key === 'Escape') {
601641
closeModal(addModal);

js/renderer.js

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
// js/renderer.js
1+
// --- renderer.js ---
2+
23
(function(window) {
34
'use strict';
45

@@ -87,38 +88,71 @@
8788

8889
// --- Основная функция инициализации приложения ---
8990
async function init() {
90-
// ИСПРАВЛЕННЫЙ ПОРЯДОК ИНИЦИАЛИЗАЦИИ
91-
92-
// 1. Сначала загружаем настройки приложения, чтобы знать сохраненный язык
9391
await loadAppSettings();
94-
95-
// 2. Теперь инициализируем локализацию, которая использует эти настройки
9692
await App.i18n.init();
9793

98-
// 3. Делаем важные функции доступными глобально внутри App
9994
App.saveConfiguration = saveConfiguration;
10095
App.toggleRecording = toggleRecording;
10196

102-
// 4. Загружаем основную конфигурацию
10397
await loadConfiguration();
10498

105-
// 5. Инициализируем все UI-модули
10699
App.modalHandler.init();
107100
App.cameraList.init();
108101
App.gridManager.init();
109102
App.archiveManager.init();
110103

111-
// 6. Первичная отрисовка интерфейса
112104
App.cameraList.render();
113105
await App.gridManager.render();
114106

115-
// 7. Запускаем периодические задачи
116107
setInterval(updateSystemStats, 3000);
117108
setInterval(() => App.cameraList.pollCameraStatuses(), 10000);
118-
updateSystemStats(); // Первый запуск сразу
109+
updateSystemStats();
119110
}
120111

121112
// Запускаем приложение
122113
init();
123114

115+
// === НОВЫЙ КОД: ОБРАБОТЧИК СТАТУСА ОБНОВЛЕНИЯ ===
116+
(function() {
117+
const updateStatusInfo = document.createElement('div');
118+
updateStatusInfo.style.marginLeft = '15px';
119+
updateStatusInfo.style.fontSize = '12px';
120+
updateStatusInfo.style.color = 'var(--text-secondary)';
121+
122+
const statusBar = document.getElementById('status-info').parentElement;
123+
if (statusBar) {
124+
statusBar.appendChild(updateStatusInfo);
125+
}
126+
127+
window.api.onUpdateStatus(({ status, message }) => {
128+
console.log(`Update status: ${status}, message: ${message}`);
129+
130+
switch (status) {
131+
case 'available':
132+
updateStatusInfo.innerHTML = `💡 <span style="text-decoration: underline; cursor: help;" title="${message}">Доступно обновление!</span>`;
133+
updateStatusInfo.style.color = '#ffc107';
134+
break;
135+
case 'downloading':
136+
updateStatusInfo.textContent = `⏳ ${message}`;
137+
updateStatusInfo.style.color = '#17a2b8';
138+
break;
139+
case 'downloaded':
140+
updateStatusInfo.innerHTML = `✅ <span style="text-decoration: underline; cursor: help;" title="${message}">Обновление загружено.</span>`;
141+
updateStatusInfo.style.color = '#28a745';
142+
break;
143+
case 'error':
144+
updateStatusInfo.textContent = `❌ ${message}`;
145+
updateStatusInfo.style.color = '#dc3545';
146+
break;
147+
case 'latest':
148+
updateStatusInfo.textContent = `👍 ${message}`;
149+
setTimeout(() => { if (updateStatusInfo.textContent.includes(message)) updateStatusInfo.textContent = ''; }, 5000);
150+
break;
151+
default:
152+
updateStatusInfo.textContent = '';
153+
break;
154+
}
155+
});
156+
})();
157+
124158
})(window);

main.js

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// main.js
1+
// --- main.js (полный код с изменениями) ---
22

33
const { app, BrowserWindow, ipcMain, Menu, clipboard, dialog, shell, protocol } = require('electron');
44
const path = require('path');
@@ -13,6 +13,7 @@ const WebSocket = require('ws');
1313
const crypto = require('crypto');
1414
const ffmpeg = require('@ffmpeg-installer/ffmpeg');
1515
const keytar = require('keytar');
16+
const { autoUpdater } = require('electron-updater');
1617

1718
// Отключаем sandbox для Linux, чтобы избежать проблем с AppImage
1819
if (process.platform === 'linux') {
@@ -124,7 +125,7 @@ async function startRecording(camera) {
124125
ffmpegProcess.on('close', (code) => {
125126
console.log(`[REC FFMPEG] Finished for "${camera.name}" with code ${code}.`);
126127
delete recordingManager[camera.id];
127-
if (mainWindow) {
128+
if (mainWindow && !mainWindow.isDestroyed()) {
128129
mainWindow.webContents.send('recording-state-change', {
129130
cameraId: camera.id,
130131
recording: false,
@@ -134,7 +135,7 @@ async function startRecording(camera) {
134135
}
135136
});
136137
console.log(`[REC] Starting for "${camera.name}" to ${outputPath}`);
137-
if (mainWindow) mainWindow.webContents.send('recording-state-change', { cameraId: camera.id, recording: true });
138+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('recording-state-change', { cameraId: camera.id, recording: true });
138139
return { success: true };
139140
}
140141

@@ -203,6 +204,13 @@ function createWindow() {
203204
webPreferences: { preload: path.join(__dirname, 'preload.js') }
204205
});
205206
mainWindow.loadFile('index.html');
207+
208+
mainWindow.once('ready-to-show', () => {
209+
if (app.isPackaged) {
210+
console.log('[Updater] App ready, checking for updates...');
211+
autoUpdater.checkForUpdates();
212+
}
213+
});
206214
}
207215

208216
function createFileManagerWindow(camera) {
@@ -387,7 +395,7 @@ ipcMain.handle('load-configuration', async () => {
387395
cameras: [],
388396
groups: [],
389397
layout: { cols: 2, rows: 2 },
390-
gridState: [null, null, null, null]
398+
gridState: Array(64).fill(null)
391399
};
392400
let config = defaultConfig;
393401
let needsResave = false;
@@ -408,6 +416,9 @@ ipcMain.handle('load-configuration', async () => {
408416
await fsPromises.access(configPath);
409417
const data = await fsPromises.readFile(configPath, 'utf-8');
410418
config = { ...defaultConfig, ...JSON.parse(data) };
419+
if (!config.gridState || config.gridState.length < 64) {
420+
config.gridState = Array(64).fill(null);
421+
}
411422
} catch (e) {
412423
const migratedConfig = await migrateOldFile();
413424
if (migratedConfig) {
@@ -779,6 +790,63 @@ ipcMain.handle('show-recording-in-folder', async (event, filename) => {
779790
}
780791
});
781792

793+
// НОВЫЙ ОБРАБОТЧИК ДЛЯ РУЧНОЙ ПРОВЕРКИ
794+
ipcMain.handle('check-for-updates', () => {
795+
if (!app.isPackaged) {
796+
console.log('[Updater] Skipping update check in dev mode.');
797+
if(mainWindow) {
798+
mainWindow.webContents.send('update-status', { status: 'error', message: 'Проверка обновлений доступна только в установленной версии.' });
799+
}
800+
return;
801+
}
802+
console.log('[Updater] Manual update check initiated.');
803+
autoUpdater.checkForUpdates();
804+
});
805+
806+
// ОБРАБОТЧИКИ СОБЫТИЙ AUTOUPDATER
807+
autoUpdater.on('update-available', (info) => {
808+
console.log('[Updater] Update available.', info);
809+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('update-status', { status: 'available', message: `Доступна версия ${info.version}` });
810+
});
811+
812+
autoUpdater.on('update-not-available', (info) => {
813+
console.log('[Updater] No new update available.');
814+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('update-status', { status: 'latest', message: 'У вас последняя версия.' });
815+
});
816+
817+
autoUpdater.on('error', (err) => {
818+
console.error('[Updater] Error:', err ? (err.stack || err) : 'unknown error');
819+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('update-status', { status: 'error', message: `Ошибка обновления: ${err.message}` });
820+
});
821+
822+
autoUpdater.on('download-progress', (progressObj) => {
823+
let log_message = "Download speed: " + progressObj.bytesPerSecond;
824+
log_message = log_message + ' - Downloaded ' + progressObj.percent.toFixed(2) + '%';
825+
log_message = log_message + ' (' + progressObj.transferred + "/" + progressObj.total + ')';
826+
console.log('[Updater] ' + log_message);
827+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('update-status', {
828+
status: 'downloading',
829+
message: `Загрузка... ${progressObj.percent.toFixed(0)}%`
830+
});
831+
});
832+
833+
autoUpdater.on('update-downloaded', (info) => {
834+
console.log('[Updater] Update downloaded.', info);
835+
if (mainWindow && !mainWindow.isDestroyed()) mainWindow.webContents.send('update-status', { status: 'downloaded', message: `Версия ${info.version} загружена. Перезапустите для установки.` });
836+
837+
dialog.showMessageBox(mainWindow, {
838+
type: 'info',
839+
title: 'Обновление готово',
840+
message: 'Новая версия загружена. Перезапустить приложение сейчас, чтобы установить обновление?',
841+
buttons: ['Перезапустить', 'Позже'],
842+
defaultId: 0
843+
}).then(({ response }) => {
844+
if (response === 0) {
845+
autoUpdater.quitAndInstall();
846+
}
847+
});
848+
});
849+
782850
app.whenReady().then(() => {
783851
protocol.registerFileProtocol('video-archive', async (request, callback) => {
784852
const settings = await getAppSettings();

0 commit comments

Comments
 (0)