321 lines
13 KiB
JavaScript
321 lines
13 KiB
JavaScript
'use strict';
|
|
|
|
const connectionStatus = document.getElementById('connectionStatus');
|
|
const deviceStatus = document.getElementById('deviceStatus');
|
|
const deviceList = document.getElementById('deviceList');
|
|
const statusIndicator = document.getElementById('statusIndicator');
|
|
const deviceSearch = document.getElementById('deviceSearch');
|
|
const disconnectBtn = document.getElementById('disconnectBtn');
|
|
const selectedDeviceId = document.getElementById('selectedDeviceId');
|
|
const altaUsername = document.getElementById('altaUsername');
|
|
const startProxyBtn = document.getElementById('startProxyBtn');
|
|
const stopProxyBtn = document.getElementById('stopProxyBtn');
|
|
const checkUpdateBtn = document.getElementById('checkUpdateBtn');
|
|
const updateNotice = document.getElementById('updateNotice');
|
|
const updateMessage = document.getElementById('updateMessage');
|
|
const openReleasesBtn = document.getElementById('openReleasesBtn');
|
|
const dismissUpdateBtn = document.getElementById('dismissUpdateBtn');
|
|
const pairingState = document.getElementById('pairingState');
|
|
const pairingSecret = document.getElementById('pairingSecret');
|
|
const pairingSecretRow = document.getElementById('pairingSecretRow');
|
|
const rotatePairingBtn = document.getElementById('rotatePairingBtn');
|
|
const revokePairingBtn = document.getElementById('revokePairingBtn');
|
|
|
|
let connection = { connected: false, origin: null, activeProxies: [] };
|
|
let selectedDevice = null;
|
|
let allDevices = [];
|
|
let allSites = {};
|
|
let collapsedSites = new Set();
|
|
let loadingDevices = false;
|
|
|
|
function showStatus(element, message, type) {
|
|
element.textContent = message;
|
|
element.className = `status-message ${type}`;
|
|
element.style.display = 'block';
|
|
}
|
|
|
|
function activeDeviceIds() {
|
|
return new Set((connection.activeProxies || []).map((proxy) => proxy.deviceId));
|
|
}
|
|
|
|
function renderConnectionState(state) {
|
|
connection = {
|
|
connected: state && state.connected === true,
|
|
origin: state && typeof state.origin === 'string' ? state.origin : null,
|
|
activeProxies: state && Array.isArray(state.activeProxies) ? state.activeProxies : [],
|
|
};
|
|
const dot = statusIndicator.querySelector('.status-dot');
|
|
const text = statusIndicator.querySelector('.status-text');
|
|
dot.className = `status-dot ${connection.connected ? 'online' : 'offline'}`;
|
|
text.textContent = connection.connected ? 'Connected' : 'Disconnected';
|
|
disconnectBtn.disabled = !connection.connected;
|
|
updateProxyButtons();
|
|
}
|
|
|
|
function updateProxyButtons() {
|
|
const id = selectedDevice && (selectedDevice.guid || selectedDevice.id);
|
|
const active = id ? activeDeviceIds().has(id.toLowerCase()) : false;
|
|
const hasUsername = altaUsername.value.trim().length > 0;
|
|
startProxyBtn.disabled = !connection.connected || !id || !hasUsername || active;
|
|
stopProxyBtn.disabled = !id || !active;
|
|
}
|
|
|
|
function clearDeviceList() {
|
|
deviceList.textContent = '';
|
|
const placeholder = document.createElement('p');
|
|
placeholder.className = 'placeholder-text';
|
|
placeholder.textContent = connection.connected ? 'Loading devices...' : 'Connect through the paired Chrome extension to load devices';
|
|
deviceList.appendChild(placeholder);
|
|
selectedDevice = null;
|
|
selectedDeviceId.value = '';
|
|
updateProxyButtons();
|
|
}
|
|
|
|
function deviceStatusFor(device) {
|
|
const raw = device && device.live && device.live.display_status !== undefined
|
|
? device.live.display_status
|
|
: device && device.status !== undefined
|
|
? device.status
|
|
: device && device.online;
|
|
const value = String(raw ?? '').toLowerCase();
|
|
return ['green', 'online', 'live', 'connected', 'true'].includes(value);
|
|
}
|
|
|
|
function selectDevice(device, item) {
|
|
const previous = deviceList.querySelector('.device-item.selected');
|
|
if (previous) previous.classList.remove('selected');
|
|
item.classList.add('selected');
|
|
selectedDevice = device;
|
|
const id = device.guid || device.id || '';
|
|
selectedDeviceId.value = id;
|
|
showStatus(connectionStatus, `Selected device: ${device.name || 'Unnamed Device'}`, 'info');
|
|
updateProxyButtons();
|
|
}
|
|
|
|
function createDeviceItem(device) {
|
|
const item = document.createElement('div');
|
|
const id = device.guid || device.id || '';
|
|
item.className = 'device-item';
|
|
item.dataset.deviceId = id;
|
|
if (activeDeviceIds().has(String(id).toLowerCase())) item.classList.add('proxy-active');
|
|
const name = document.createElement('div');
|
|
name.className = 'device-name';
|
|
name.textContent = device.name || 'Unnamed Device';
|
|
const dot = document.createElement('div');
|
|
dot.className = `device-status-dot ${deviceStatusFor(device) ? 'online' : 'offline'}`;
|
|
item.append(name, dot);
|
|
item.addEventListener('click', () => selectDevice(device, item));
|
|
return item;
|
|
}
|
|
|
|
function groupDevicesBySite(devices) {
|
|
const groups = new Map();
|
|
for (const device of devices) {
|
|
const siteId = device.server_group_id;
|
|
const siteName = siteId && allSites[siteId];
|
|
const key = siteName ? siteId : '__ungrouped';
|
|
if (!groups.has(key)) groups.set(key, { name: siteName || 'Ungrouped', devices: [] });
|
|
groups.get(key).devices.push(device);
|
|
}
|
|
return [...groups.entries()].sort((left, right) => left[1].name.localeCompare(right[1].name));
|
|
}
|
|
|
|
function displayDevices(devices) {
|
|
deviceList.textContent = '';
|
|
if (!devices.length) {
|
|
const empty = document.createElement('p');
|
|
empty.className = 'no-devices';
|
|
empty.textContent = deviceSearch.value.trim() ? 'No devices match your search' : 'No local cameras found';
|
|
deviceList.appendChild(empty);
|
|
return;
|
|
}
|
|
|
|
const grouped = Object.keys(allSites).length > 0;
|
|
if (!grouped) {
|
|
for (const device of devices) deviceList.appendChild(createDeviceItem(device));
|
|
return;
|
|
}
|
|
|
|
for (const [siteId, group] of groupDevicesBySite(devices)) {
|
|
const header = document.createElement('div');
|
|
const collapsed = collapsedSites.has(siteId);
|
|
header.className = `site-group-header${collapsed ? ' collapsed' : ''}`;
|
|
const arrow = document.createElement('span');
|
|
arrow.className = 'site-group-arrow';
|
|
arrow.textContent = collapsed ? '\u25B6' : '\u25BC';
|
|
const name = document.createElement('span');
|
|
name.className = 'site-group-name';
|
|
name.textContent = group.name;
|
|
const count = document.createElement('span');
|
|
count.className = 'site-group-count';
|
|
count.textContent = String(group.devices.length);
|
|
header.append(arrow, name, count);
|
|
header.addEventListener('click', () => {
|
|
if (collapsedSites.has(siteId)) collapsedSites.delete(siteId);
|
|
else collapsedSites.add(siteId);
|
|
filterDevices();
|
|
});
|
|
deviceList.appendChild(header);
|
|
if (!collapsed) for (const device of group.devices) deviceList.appendChild(createDeviceItem(device));
|
|
}
|
|
}
|
|
|
|
function filterDevices() {
|
|
const query = deviceSearch.value.toLowerCase().trim();
|
|
if (!query) {
|
|
displayDevices(allDevices);
|
|
return;
|
|
}
|
|
displayDevices(allDevices.filter((device) => {
|
|
const values = [
|
|
device.name, device.guid, device.id, device.type, device.model, device.ipAddress,
|
|
device.server_group_id && allSites[device.server_group_id],
|
|
];
|
|
return values.some((value) => String(value || '').toLowerCase().includes(query));
|
|
}));
|
|
}
|
|
|
|
async function loadDevices() {
|
|
if (!connection.connected || loadingDevices) return;
|
|
loadingDevices = true;
|
|
showStatus(deviceStatus, 'Fetching devices...', 'info');
|
|
try {
|
|
const [devicesResult, sitesResult] = await Promise.all([
|
|
window.electronAPI.getDevices(),
|
|
window.electronAPI.getDeviceSites(),
|
|
]);
|
|
allSites = {};
|
|
if (sitesResult.success && Array.isArray(sitesResult.sites)) {
|
|
for (const site of sitesResult.sites) if (site.id) allSites[site.id] = site.name || 'Unnamed Site';
|
|
}
|
|
if (!devicesResult.success) {
|
|
allDevices = [];
|
|
showStatus(deviceStatus, devicesResult.message || 'Failed to load devices', 'error');
|
|
clearDeviceList();
|
|
return;
|
|
}
|
|
allDevices = devicesResult.devices.filter((device) =>
|
|
!device.capabilities || device.capabilities.localStorage === undefined || device.capabilities.localStorage === false
|
|
);
|
|
showStatus(deviceStatus, `Found ${allDevices.length} local camera${allDevices.length === 1 ? '' : 's'}`, 'success');
|
|
filterDevices();
|
|
} catch {
|
|
showStatus(deviceStatus, 'Could not load devices.', 'error');
|
|
} finally {
|
|
loadingDevices = false;
|
|
}
|
|
}
|
|
|
|
const rendererController = window.AptRendererController.createRendererController({
|
|
disconnect: () => window.electronAPI.disconnect(),
|
|
renderConnectionState,
|
|
clearDisconnectedState: () => {
|
|
allDevices = [];
|
|
allSites = {};
|
|
collapsedSites.clear();
|
|
altaUsername.value = '';
|
|
clearDeviceList();
|
|
},
|
|
showConnectionStatus: (message, type) => showStatus(connectionStatus, message, type),
|
|
});
|
|
|
|
async function handleDisconnect() {
|
|
await rendererController.disconnect();
|
|
}
|
|
|
|
async function handleStartProxy() {
|
|
const id = selectedDevice && (selectedDevice.guid || selectedDevice.id);
|
|
if (!id) return;
|
|
startProxyBtn.disabled = true;
|
|
showStatus(connectionStatus, `Starting proxy for ${selectedDevice.name || 'selected device'}...`, 'info');
|
|
const result = await window.electronAPI.launchProxy(id, altaUsername.value);
|
|
if (result.success) {
|
|
connection = await window.electronAPI.getConnectionState();
|
|
renderConnectionState(connection);
|
|
filterDevices();
|
|
showStatus(connectionStatus, 'Camera proxy started.', 'success');
|
|
} else {
|
|
showStatus(connectionStatus, result.message || 'Failed to start camera proxy.', 'error');
|
|
updateProxyButtons();
|
|
}
|
|
}
|
|
|
|
async function handleStopProxy() {
|
|
const id = selectedDevice && (selectedDevice.guid || selectedDevice.id);
|
|
if (!id) return;
|
|
stopProxyBtn.disabled = true;
|
|
const result = await window.electronAPI.stopProxy(id);
|
|
connection = await window.electronAPI.getConnectionState();
|
|
renderConnectionState(connection);
|
|
filterDevices();
|
|
showStatus(connectionStatus, result.success ? 'Camera proxy stopped.' : result.message || 'Failed to stop camera proxy.', result.success ? 'success' : 'error');
|
|
}
|
|
|
|
async function checkForUpdates(showCurrent = true) {
|
|
checkUpdateBtn.disabled = true;
|
|
try {
|
|
const result = await window.electronAPI.checkForUpdates();
|
|
if (!result.success) {
|
|
if (showCurrent) showStatus(connectionStatus, result.message, 'error');
|
|
return;
|
|
}
|
|
if (result.status === 'update-available') {
|
|
updateMessage.textContent = `${result.releaseName || `Version ${result.latestVersion}`} is available. APT will not download or run updates; review it on GitPeji.`;
|
|
updateNotice.style.display = 'flex';
|
|
checkUpdateBtn.classList.add('update-available');
|
|
} else if (showCurrent) {
|
|
const message = result.status === 'no-release'
|
|
? 'No published release is available on GitPeji.'
|
|
: `You are on the latest version (v${result.currentVersion}).`;
|
|
showStatus(connectionStatus, message, 'success');
|
|
}
|
|
} finally {
|
|
checkUpdateBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function renderPairingStatus(result) {
|
|
pairingState.textContent = result.paired ? 'Paired' : 'Revoked / not paired';
|
|
pairingState.className = result.paired ? 'paired' : 'unpaired';
|
|
revokePairingBtn.disabled = !result.paired;
|
|
if (result.secret) {
|
|
pairingSecret.value = result.secret;
|
|
pairingSecretRow.style.display = 'block';
|
|
showStatus(connectionStatus, 'One-time bridge pairing secret generated. Paste it into the extension now; it will not be shown again.', 'info');
|
|
} else {
|
|
pairingSecret.value = '';
|
|
pairingSecretRow.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
async function initialize() {
|
|
renderConnectionState(await window.electronAPI.getConnectionState());
|
|
renderPairingStatus(await window.electronAPI.getPairingStatus());
|
|
if (connection.connected) await loadDevices();
|
|
window.electronAPI.onConnectionStateChanged(async (state) => {
|
|
const wasConnected = connection.connected;
|
|
const previousOrigin = connection.origin;
|
|
renderConnectionState(state);
|
|
if (state.connected && (!wasConnected || state.origin !== previousOrigin)) {
|
|
allDevices = [];
|
|
allSites = {};
|
|
clearDeviceList();
|
|
showStatus(connectionStatus, `Connected to ${state.origin}`, 'success');
|
|
await loadDevices();
|
|
}
|
|
});
|
|
setTimeout(() => checkForUpdates(false), 2000);
|
|
}
|
|
|
|
disconnectBtn.addEventListener('click', handleDisconnect);
|
|
startProxyBtn.addEventListener('click', handleStartProxy);
|
|
stopProxyBtn.addEventListener('click', handleStopProxy);
|
|
deviceSearch.addEventListener('input', filterDevices);
|
|
altaUsername.addEventListener('input', updateProxyButtons);
|
|
checkUpdateBtn.addEventListener('click', () => checkForUpdates(true));
|
|
openReleasesBtn.addEventListener('click', () => window.electronAPI.openFixedReleasesPage());
|
|
dismissUpdateBtn.addEventListener('click', () => { updateNotice.style.display = 'none'; });
|
|
rotatePairingBtn.addEventListener('click', async () => renderPairingStatus(await window.electronAPI.rotatePairing()));
|
|
revokePairingBtn.addEventListener('click', async () => renderPairingStatus(await window.electronAPI.revokePairing()));
|
|
document.addEventListener('DOMContentLoaded', () => { initialize().catch(() => showStatus(connectionStatus, 'Application initialization failed.', 'error')); });
|