Files

116 lines
3.8 KiB
JavaScript

'use strict';
const { app, BrowserWindow, clipboard, dialog, ipcMain, net, protocol, shell } = require('electron');
const fs = require('node:fs/promises');
const path = require('node:path');
const { pathToFileURL } = require('node:url');
const { buildCsv } = require('./static/export-utils');
const { APP_RENDERER_URL, resolveAppAssetPath } = require('./src/app-protocol');
const { KEY_PATTERN } = require('./src/discovery');
const { createScanSession } = require('./src/scan-session');
protocol.registerSchemesAsPrivileged([{
scheme: 'app',
privileges: { standard: true, secure: true, supportFetchAPI: true },
}]);
let mainWindow = null;
const scanSession = createScanSession();
function isTrustedSender(event) {
if (!mainWindow || mainWindow.isDestroyed() || !event || !event.senderFrame) return false;
return event.sender === mainWindow.webContents &&
event.senderFrame === mainWindow.webContents.mainFrame &&
event.senderFrame.url === APP_RENDERER_URL;
}
function registerIpc(channel, handler) {
ipcMain.handle(channel, async (event, ...args) => {
if (!isTrustedSender(event)) throw new Error('Forbidden IPC sender');
return handler(...args);
});
}
async function openCameraUrl(value) {
const url = scanSession.allowedCameraUrl(value);
if (!url) return;
await shell.openExternal(url);
}
function registerIpcHandlers() {
registerIpc('scan-cameras', scanSession.scanOnce);
registerIpc('copy-text', (value) => {
const text = String(value || '');
if (!KEY_PATTERN.test(text)) throw new Error('Invalid Alta Key.');
clipboard.writeText(text);
return { ok: true };
});
registerIpc('export-csv', async () => {
const csv = buildCsv(scanSession.getLatestCameras());
if (Buffer.byteLength(csv, 'utf8') > 1024 * 1024) throw new Error('CSV export is too large.');
const result = await dialog.showSaveDialog(mainWindow, {
title: 'Export discovered Alta cameras',
defaultPath: 'alta-camera-keys.csv',
filters: [{ name: 'CSV file', extensions: ['csv'] }],
});
if (result.canceled || !result.filePath) return { ok: false, canceled: true };
await fs.writeFile(result.filePath, csv, 'utf8');
return { ok: true, canceled: false };
});
}
function registerAppProtocol() {
const staticDir = path.join(__dirname, 'static');
protocol.handle('app', (request) => {
const assetPath = resolveAppAssetPath(request.url, staticDir);
if (!assetPath) return new Response('Not found', { status: 404 });
return net.fetch(pathToFileURL(assetPath).href);
});
}
function denyRendererPermissions(targetSession) {
targetSession.setPermissionCheckHandler(() => false);
targetSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
}
function createWindow() {
mainWindow = new BrowserWindow({
width: 1180,
height: 760,
minWidth: 820,
minHeight: 620,
backgroundColor: '#f5f6f8',
icon: path.join(__dirname, 'assets', 'icon.png'),
title: 'Alta Local Camera Discovery',
autoHideMenuBar: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
preload: path.join(__dirname, 'preload.js'),
},
});
denyRendererPermissions(mainWindow.webContents.session);
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
openCameraUrl(url).catch(() => {});
return { action: 'deny' };
});
mainWindow.webContents.on('will-navigate', (event) => event.preventDefault());
mainWindow.loadURL(APP_RENDERER_URL);
if (!app.isPackaged && process.argv.includes('--dev')) mainWindow.webContents.openDevTools();
}
app.whenReady().then(() => {
registerAppProtocol();
registerIpcHandlers();
createWindow();
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});