Publish Untrace Desktop 1.6.1 with offline batch workflows

This commit is contained in:
Dario P. committed 2026-10-25 01:22:14 +02:00
1 parent 99b4b22352
commit 7ce81b0727
47 files changed
+5958 -1208

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
{
"name": "Untrace Desktop",
"appId": "local.untrace.desktop"
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

+54
View File
@@ -0,0 +1,54 @@
const { app, BrowserWindow, session, protocol, net, shell, dialog, ipcMain } = require('electron');
const path = require('node:path');
const fs = require('node:fs');
const { pathToFileURL } = require('node:url');
const { name: productName, appId } = require('./brand.json');
app.setName(productName);
app.setAppUserModelId(appId);
// Keep preferences alongside the portable executable, including on USB drives.
const home = process.env.PORTABLE_EXECUTABLE_DIR || path.dirname(app.getPath('exe'));
const data = process.env.UNTRACE_DATA_DIR || path.join(home, 'Untrace-data');
try {
fs.mkdirSync(data, { recursive: true });
app.setPath('userData', data);
app.setPath('sessionData', data);
} catch {
dialog.showErrorBox(`${productName} cannot start`, `Move ${productName} to a writable folder and try again.`);
app.exit(1);
}
protocol.registerSchemesAsPrivileged([{ scheme: 'untrace', privileges: { standard: true, secure: true, supportFetchAPI: true } }]);
const externalHosts = new Set(['github.com', 'www.buymeacoffee.com', 'artimic.com', 'realbrain.cc']);
function openLink(url) {
try {
const target = new URL(url);
if (target.protocol === 'https:' && externalHosts.has(target.hostname)) shell.openExternal(url);
} catch {}
}
app.whenReady().then(() => {
require('./output.cjs').registerOutput({ ipcMain, dialog, BrowserWindow });
const root = path.join(app.getAppPath(), 'dist');
protocol.handle('untrace', request => {
const url = new URL(request.url);
const file = path.resolve(root, '.' + decodeURIComponent(url.pathname === '/' ? '/index.html' : url.pathname));
if (url.host !== 'app' || !file.startsWith(root + path.sep)) return new Response('Forbidden', { status: 403 });
return net.fetch(pathToFileURL(file).href);
});
session.defaultSession.setPermissionRequestHandler((_contents, _permission, callback) => callback(false));
session.defaultSession.setPermissionCheckHandler(() => false);
session.defaultSession.webRequest.onBeforeRequest({ urls: ['http://*/*', 'https://*/*', 'ws://*/*', 'wss://*/*'] }, (_details, callback) => callback({ cancel: true }));
const win = new BrowserWindow({
width: 1200, height: 900, minWidth: 640, minHeight: 640,
title: productName, show: false, autoHideMenuBar: true,
webPreferences: { preload: path.join(__dirname, 'preload.cjs'), nodeIntegration: false, contextIsolation: true, sandbox: true, webSecurity: true },
});
win.removeMenu();
win.webContents.setWindowOpenHandler(({ url }) => { openLink(url); return { action: 'deny' }; });
win.webContents.on('will-navigate', (event, url) => {
const target = new URL(url);
if (target.protocol !== 'untrace:' || target.host !== 'app') { event.preventDefault(); openLink(url); }
});
win.once('ready-to-show', () => { if (!process.env.UNTRACE_TEST) win.show(); });
win.loadURL('untrace://app/');
});
app.on('window-all-closed', () => app.quit());
+57
View File
@@ -0,0 +1,57 @@
const fs = require('node:fs/promises');
const path = require('node:path');
const { randomUUID } = require('node:crypto');
// Exclusive creation protects originals, existing exports and case-insensitive collisions.
async function saveUnique(directory, filename, bytes) {
if (typeof filename !== 'string' || !/^[^<>:"/\\|?*\x00-\x1f]+\.(png|jpg)$/i.test(filename) || filename.length > 220 || /[. ]$/.test(filename) || /^(con|prn|aux|nul|com[1-9]|lpt[1-9])\./i.test(filename)) throw new Error('Invalid output filename.');
if (!(bytes instanceof Uint8Array) || !bytes.byteLength) throw new Error('No encoded image data.');
const extension = path.extname(filename), stem = filename.slice(0, -extension.length);
for (let index = 1; index <= 10000; index++) {
const name = `${stem}${index === 1 ? '' : '-' + index}${extension}`;
const target = path.join(directory, name);
let handle;
try { handle = await fs.open(target, 'wx'); }
catch (error) { if (error.code === 'EEXIST') continue; throw error; }
try {
await handle.writeFile(bytes);
await handle.close();
return { filename: name, path: target };
} catch (error) {
await handle.close().catch(() => {});
await fs.unlink(target).catch(() => {});
throw error;
}
}
throw new Error('Too many files with this name. Choose another output folder.');
}
function registerOutput({ ipcMain, dialog, BrowserWindow }) {
const folders = new Map();
function check(event) {
const url = new URL(event.senderFrame.url);
if (event.senderFrame !== event.sender.mainFrame || url.protocol !== 'untrace:' || url.host !== 'app') throw new Error('Untrusted sender.');
}
ipcMain.handle('output:choose', async event => {
check(event);
const selection = await dialog.showOpenDialog(BrowserWindow.fromWebContents(event.sender), { title: 'Choose output folder', properties: ['openDirectory', 'createDirectory'] });
if (selection.canceled || !selection.filePaths[0]) return null;
const directory = await fs.realpath(selection.filePaths[0]);
const token = randomUUID();
folders.set(event.sender.id, { token, directory });
return { token, path: directory };
});
ipcMain.handle('output:save', async (event, token, filename, bytes) => {
check(event);
const folder = folders.get(event.sender.id);
if (!folder || token !== folder.token) throw new Error('Choose an output folder first.');
try { return await saveUnique(folder.directory, filename, bytes); }
catch (error) {
if (['EACCES', 'EPERM', 'EROFS'].includes(error.code)) throw new Error('The output folder is not writable. Choose another folder.');
if (error.code === 'ENOSPC') throw new Error('The drive is full. Free space or choose another folder.');
if (error.code === 'ENOENT') throw new Error('The output folder is unavailable. Choose it again.');
throw error;
}
});
}
module.exports = { saveUnique, registerOutput };
+5
View File
@@ -0,0 +1,5 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('untraceDesktop', {
chooseOutputFolder: () => ipcRenderer.invoke('output:choose'),
saveOutput: (token, filename, bytes) => ipcRenderer.invoke('output:save', token, filename, bytes),
});