Skip to content

Commit 0e5f5bc

Browse files
committed
ci: Add GitHub Actions workflow for build and release
1 parent d1d31d8 commit 0e5f5bc

16 files changed

+3352
-1958
lines changed

index.html

Lines changed: 358 additions & 713 deletions
Large diffs are not rendered by default.

js/archive-manager.js

Lines changed: 343 additions & 88 deletions
Large diffs are not rendered by default.

js/camera-list.js

Lines changed: 145 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,167 +1,171 @@
1-
// js/camera-list.js
1+
// js/camera-list.js (Полная версия с изменениями для системы пользователей)
22

33
(function(window) {
44
window.AppModules = window.AppModules || {};
55

66
AppModules.createCameraList = function(App) {
7-
const cameraListContainer = document.getElementById('camera-list-container');
8-
const openRecordingsBtn = document.getElementById('open-recordings-btn');
7+
const stateManager = App.stateManager;
8+
const cameraListContainer = document.getElementById('camera-list-container');
9+
const openRecordingsBtn = document.getElementById('open-recordings-btn');
910

10-
async function pollCameraStatuses() {
11-
for (const camera of App.cameras) {
12-
const statusIcon = document.getElementById(`status-icon-${camera.id}`);
13-
if (statusIcon) {
14-
const pulse = await window.api.getCameraPulse(camera);
15-
statusIcon.classList.toggle('online', pulse.success);
11+
async function pollCameraStatuses() {
12+
for (const camera of stateManager.state.cameras) {
13+
const statusIcon = document.getElementById(`status-icon-${camera.id}`);
14+
if (statusIcon) {
15+
try {
16+
const pulse = await window.api.getCameraPulse(camera);
17+
statusIcon.classList.toggle('online', pulse.success);
18+
} catch (e) {
19+
statusIcon.classList.remove('online');
20+
}
21+
}
1622
}
1723
}
18-
}
1924

20-
async function deleteCamera(cameraId) {
21-
if (confirm(App.t('confirm_delete_camera'))) {
22-
if (App.recordingStates[cameraId]) {
23-
await window.api.stopRecording(cameraId);
25+
async function deleteCamera(cameraId) {
26+
if (confirm(App.i18n.t('confirm_delete_camera'))) {
27+
if (stateManager.state.recordingStates[cameraId]) {
28+
await window.api.stopRecording(cameraId);
29+
}
30+
stateManager.deleteCamera(cameraId);
2431
}
25-
App.gridManager.removeStreamsForCamera(cameraId);
26-
App.cameras = App.cameras.filter(c => c.id !== cameraId);
27-
await App.saveConfiguration();
28-
render();
2932
}
30-
}
3133

32-
function render() {
33-
cameraListContainer.innerHTML = '';
34-
35-
const createGroupHTML = (group, camerasInGroup) => {
36-
const groupContainer = document.createElement('div');
37-
groupContainer.className = 'group-container';
38-
39-
const groupHeader = document.createElement('div');
40-
groupHeader.className = 'group-header';
41-
groupHeader.innerHTML = `<i class="material-icons toggle-icon">arrow_drop_down</i><span class="group-name">${group.name}</span>`;
42-
43-
const groupCamerasList = document.createElement('div');
44-
groupCamerasList.className = 'group-cameras';
45-
46-
camerasInGroup.forEach(camera => {
47-
const cameraItem = document.createElement('div');
48-
cameraItem.className = 'camera-item';
49-
cameraItem.dataset.cameraId = camera.id;
50-
cameraItem.draggable = true;
51-
cameraItem.innerHTML = `<i class="status-icon" id="status-icon-${camera.id}"></i><span>${camera.name}</span><div class="rec-indicator"></div>`;
52-
if (App.recordingStates[camera.id]) {
53-
cameraItem.classList.add('recording');
54-
}
55-
cameraItem.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', camera.id); });
56-
groupCamerasList.appendChild(cameraItem);
57-
});
58-
59-
groupContainer.appendChild(groupHeader);
60-
groupContainer.appendChild(groupCamerasList);
61-
62-
groupHeader.addEventListener('click', () => {
63-
groupHeader.querySelector('.toggle-icon').classList.toggle('collapsed');
64-
groupCamerasList.classList.toggle('collapsed');
65-
});
66-
67-
if (group.id !== null) {
68-
groupHeader.addEventListener('dragover', (e) => { e.preventDefault(); groupHeader.style.backgroundColor = 'var(--accent-color)'; });
69-
groupHeader.addEventListener('dragleave', (e) => { groupHeader.style.backgroundColor = ''; });
70-
groupHeader.addEventListener('drop', (e) => {
71-
e.preventDefault();
72-
groupHeader.style.backgroundColor = '';
73-
const cameraId = parseInt(e.dataTransfer.getData('text/plain'), 10);
74-
const camera = App.cameras.find(c => c.id === cameraId);
75-
if (camera && camera.groupId !== group.id) {
76-
camera.groupId = group.id;
77-
App.saveConfiguration();
78-
render();
34+
function render() {
35+
cameraListContainer.innerHTML = '';
36+
const { cameras, groups, recordingStates } = stateManager.state;
37+
38+
const createGroupHTML = (group, camerasInGroup) => {
39+
const groupContainer = document.createElement('div');
40+
groupContainer.className = 'group-container';
41+
42+
const groupHeader = document.createElement('div');
43+
groupHeader.className = 'group-header';
44+
groupHeader.innerHTML = `<i class="material-icons toggle-icon">arrow_drop_down</i><span class="group-name">${group.name}</span>`;
45+
46+
const groupCamerasList = document.createElement('div');
47+
groupCamerasList.className = 'group-cameras';
48+
49+
camerasInGroup.forEach(camera => {
50+
const cameraItem = document.createElement('div');
51+
cameraItem.className = 'camera-item';
52+
cameraItem.dataset.cameraId = camera.id;
53+
// VVV ИЗМЕНЕНИЕ: Перетаскивание доступно только администратору VVV
54+
cameraItem.draggable = App.stateManager.state.currentUser?.role === 'admin';
55+
// ^^^ КОНЕЦ ИЗМЕНЕНИЯ ^^^
56+
cameraItem.innerHTML = `<i class="status-icon" id="status-icon-${camera.id}"></i><span>${camera.name}</span><div class="rec-indicator"></div>`;
57+
if (recordingStates[camera.id]) {
58+
cameraItem.classList.add('recording');
7959
}
60+
cameraItem.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', camera.id.toString()); });
61+
groupCamerasList.appendChild(cameraItem);
8062
});
81-
}
82-
83-
return groupContainer;
84-
};
63+
64+
groupContainer.appendChild(groupHeader);
65+
groupContainer.appendChild(groupCamerasList);
66+
67+
groupHeader.addEventListener('click', () => {
68+
groupHeader.querySelector('.toggle-icon').classList.toggle('collapsed');
69+
groupCamerasList.classList.toggle('collapsed');
70+
});
71+
72+
if (group.id !== null) {
73+
groupHeader.addEventListener('dragover', (e) => { e.preventDefault(); groupHeader.style.backgroundColor = 'var(--accent-color)'; });
74+
groupHeader.addEventListener('dragleave', (e) => { groupHeader.style.backgroundColor = ''; });
75+
groupHeader.addEventListener('drop', (e) => {
76+
e.preventDefault();
77+
groupHeader.style.backgroundColor = '';
78+
const cameraId = parseInt(e.dataTransfer.getData('text/plain'), 10);
79+
const camera = cameras.find(c => c.id === cameraId);
80+
if (camera && camera.groupId !== group.id) {
81+
stateManager.updateCamera({ ...camera, groupId: group.id });
82+
}
83+
});
84+
}
85+
86+
return groupContainer;
87+
};
8588

86-
App.groups.forEach(group => {
87-
const camerasInGroup = App.cameras.filter(c => c.groupId === group.id);
88-
cameraListContainer.appendChild(createGroupHTML(group, camerasInGroup));
89-
});
89+
groups.forEach(group => {
90+
const camerasInGroup = cameras.filter(c => c.groupId === group.id);
91+
cameraListContainer.appendChild(createGroupHTML(group, camerasInGroup));
92+
});
9093

91-
const ungroupedCameras = App.cameras.filter(c => !c.groupId);
92-
if (ungroupedCameras.length > 0) {
93-
const ungroupedPseudoGroup = { id: null, name: App.t('ungrouped_cameras') };
94-
cameraListContainer.appendChild(createGroupHTML(ungroupedPseudoGroup, ungroupedCameras));
95-
}
94+
const ungroupedCameras = cameras.filter(c => !c.groupId);
95+
if (ungroupedCameras.length > 0) {
96+
const ungroupedPseudoGroup = { id: null, name: App.i18n.t('ungrouped_cameras') };
97+
cameraListContainer.appendChild(createGroupHTML(ungroupedPseudoGroup, ungroupedCameras));
98+
}
9699

97-
if (cameraListContainer.innerHTML === '') {
98-
cameraListContainer.innerHTML = `<p style="padding: 10px; color: var(--text-secondary);">${App.t('no_cameras_or_groups')}</p>`;
99-
}
100+
if (cameraListContainer.innerHTML === '') {
101+
cameraListContainer.innerHTML = `<p style="padding: 10px; color: var(--text-secondary);">${App.i18n.t('no_cameras_or_groups')}</p>`;
102+
}
100103

101-
pollCameraStatuses();
102-
}
103-
104-
function updateRecordingState(cameraId, isRecording) {
105-
const cameraItem = cameraListContainer.querySelector(`.camera-item[data-camera-id='${cameraId}']`);
106-
if (cameraItem) {
107-
cameraItem.classList.toggle('recording', isRecording);
104+
pollCameraStatuses();
108105
}
109-
}
110106

111-
function init() {
112-
openRecordingsBtn.addEventListener('click', () => window.api.openRecordingsFolder());
113-
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-
open_in_browser: `🌐 ${App.t('context_open_in_browser')}`,
121-
files: `🗂️ ${App.t('context_file_manager')}`,
122-
ssh: `💻 ${App.t('context_ssh')}`,
123-
settings: `⚙️ ${App.t('context_settings')}`,
124-
edit: `✏️ ${App.t('context_edit')}`,
125-
delete: `🗑️ ${App.t('context_delete')}`
126-
};
127-
window.api.showCameraContextMenu({ cameraId, labels });
128-
}
129-
});
107+
function init() {
108+
openRecordingsBtn.addEventListener('click', () => window.api.openRecordingsFolder());
109+
110+
cameraListContainer.addEventListener('contextmenu', (e) => {
111+
// VVV ИЗМЕНЕНИЕ: Блокируем контекстное меню для всех, кроме админа VVV
112+
if (App.stateManager.state.currentUser?.role !== 'admin') {
113+
e.preventDefault();
114+
return;
115+
}
116+
// ^^^ КОНЕЦ ИЗМЕНЕНИЯ ^^^
117+
118+
const cameraItem = e.target.closest('.camera-item');
119+
if (cameraItem) {
120+
e.preventDefault();
121+
const cameraId = parseInt(cameraItem.dataset.cameraId, 10);
122+
const labels = {
123+
open_in_browser: `🌐 ${App.i18n.t('context_open_in_browser')}`,
124+
files: `🗂️ ${App.i18n.t('context_file_manager')}`,
125+
ssh: `💻 ${App.i18n.t('context_ssh')}`,
126+
archive: `🗄️ ${App.i18n.t('archive_title')}`,
127+
settings: `⚙️ ${App.i18n.t('context_settings')}`,
128+
edit: `✏️ ${App.i18n.t('context_edit')}`,
129+
delete: `🗑️ ${App.i18n.t('context_delete')}`
130+
};
131+
window.api.showCameraContextMenu({ cameraId, labels });
132+
}
133+
});
130134

131-
window.api.onContextMenuCommand(({ command, cameraId }) => {
132-
const camera = App.cameras.find(c => c.id === cameraId);
133-
if (!camera) return;
135+
window.api.onContextMenuCommand(({ command, cameraId }) => {
136+
const camera = stateManager.state.cameras.find(c => c.id === cameraId);
137+
if (!camera) return;
134138

135-
// Создаем чистый объект для передачи через IPC
136-
const cameraData = {
137-
id: camera.id,
138-
name: camera.name,
139-
ip: camera.ip,
140-
port: camera.port,
141-
username: camera.username,
142-
password: camera.password,
143-
streamPath0: camera.streamPath0,
144-
streamPath1: camera.streamPath1
145-
};
139+
const cameraDataForIPC = {
140+
id: camera.id,
141+
name: camera.name,
142+
ip: camera.ip,
143+
port: camera.port,
144+
username: camera.username,
145+
password: camera.password,
146+
streamPath0: camera.streamPath0,
147+
streamPath1: camera.streamPath1,
148+
groupId: camera.groupId
149+
};
146150

147-
switch(command) {
148-
case 'open_in_browser':
149-
window.api.openInBrowser(camera.ip);
150-
break;
151-
case 'files': window.api.openFileManager(cameraData); break;
152-
case 'ssh': window.api.openSshTerminal(cameraData); break;
153-
case 'settings': App.modalHandler.openSettingsModal(cameraId); break;
154-
case 'edit': App.modalHandler.openAddModal(cameraData); break;
155-
case 'delete': deleteCamera(cameraId); break;
156-
}
157-
});
158-
}
151+
switch(command) {
152+
case 'open_in_browser':
153+
window.api.openInBrowser(cameraDataForIPC.ip);
154+
break;
155+
case 'files': window.api.openFileManager(cameraDataForIPC); break;
156+
case 'ssh': window.api.openSshTerminal(cameraDataForIPC); break;
157+
case 'archive': App.archiveManager.openArchiveForCamera(camera); break;
158+
case 'settings': App.modalHandler.openSettingsModal(cameraId); break;
159+
case 'edit': App.modalHandler.openAddModal(cameraDataForIPC); break;
160+
case 'delete': deleteCamera(cameraId); break;
161+
}
162+
});
163+
}
159164

160-
return {
161-
init,
162-
render,
163-
pollCameraStatuses,
164-
updateRecordingState
165+
return {
166+
init,
167+
render,
168+
pollCameraStatuses
169+
}
165170
}
166-
}
167171
})(window);

0 commit comments

Comments
 (0)