Add a soprano recorder tab converter as a Linux AppImage.
The app turns a lead line into Baroque fingering tabs with rhythm kept, ships Twinkle and Ode to Joy, and reads photos locally with oemer when the bundled Python is present. Co-authored-by: catpersonisme <catpersonisme@gmail.com>
This commit is contained in:
55 files changed
+17536
No files matched your search
@@ -0,0 +1,9 @@
|
||||
// The Next standalone server ships its own node_modules as an extra resource.
|
||||
// Returning false tells electron-builder to leave the project node_modules out
|
||||
// of the AppImage. Those packages are not what the window runs.
|
||||
async function beforeBuild() {
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = beforeBuild;
|
||||
module.exports.beforeBuild = beforeBuild;
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,126 @@
|
||||
const { app, BrowserWindow, dialog } = require("electron");
|
||||
const { spawn } = require("node:child_process");
|
||||
const fs = require("node:fs");
|
||||
const http = require("node:http");
|
||||
const net = require("node:net");
|
||||
const path = require("node:path");
|
||||
|
||||
app.commandLine.appendSwitch("no-sandbox");
|
||||
app.commandLine.appendSwitch("disable-dev-shm-usage");
|
||||
|
||||
let server;
|
||||
|
||||
function freePort() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const probe = net.createServer();
|
||||
probe.listen(0, "127.0.0.1", () => {
|
||||
const address = probe.address();
|
||||
const port = address && typeof address === "object" ? address.port : 0;
|
||||
probe.close(() => resolve(port));
|
||||
});
|
||||
probe.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function waitFor(url, timeoutMs) {
|
||||
const started = Date.now();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tick = () => {
|
||||
const request = http.get(url, (response) => {
|
||||
response.resume();
|
||||
if (response.statusCode && response.statusCode < 500) resolve();
|
||||
else retry();
|
||||
});
|
||||
request.on("error", retry);
|
||||
};
|
||||
const retry = () => {
|
||||
if (Date.now() - started > timeoutMs) reject(new Error(`Timed out waiting for ${url}`));
|
||||
else setTimeout(tick, 250);
|
||||
};
|
||||
tick();
|
||||
});
|
||||
}
|
||||
|
||||
function stopServer() {
|
||||
if (server && !server.killed) server.kill("SIGTERM");
|
||||
}
|
||||
|
||||
async function open() {
|
||||
const resources = process.resourcesPath;
|
||||
const standalone = path.join(resources, "standalone");
|
||||
const python = path.join(resources, "python", "bin", "python3");
|
||||
const serverJs = path.join(standalone, "server.js");
|
||||
if (!fs.existsSync(serverJs) || !fs.existsSync(python)) {
|
||||
dialog.showErrorBox(
|
||||
"Recorder Tab Converter",
|
||||
"The bundled app or the local reader is missing from this AppImage.",
|
||||
);
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
const port = await freePort();
|
||||
const logPath = path.join(app.getPath("temp"), "recorder-tab-converter.log");
|
||||
const log = fs.createWriteStream(logPath, { flags: "a" });
|
||||
server = spawn(process.execPath, [serverJs], {
|
||||
cwd: standalone,
|
||||
env: {
|
||||
...process.env,
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
PORT: String(port),
|
||||
HOSTNAME: "127.0.0.1",
|
||||
OEMER_PYTHON: python,
|
||||
NODE_ENV: "production",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
server.stdout.pipe(log);
|
||||
server.stderr.pipe(log);
|
||||
server.on("exit", (code) => {
|
||||
log.write(`\nserver exit ${code}\n`);
|
||||
});
|
||||
|
||||
const url = `http://127.0.0.1:${port}/`;
|
||||
try {
|
||||
await waitFor(url, 45_000);
|
||||
} catch (error) {
|
||||
dialog.showErrorBox(
|
||||
"Recorder Tab Converter",
|
||||
`${error instanceof Error ? error.message : "The app server did not start."}\nLog: ${logPath}`,
|
||||
);
|
||||
stopServer();
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
const window = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 880,
|
||||
minWidth: 390,
|
||||
minHeight: 640,
|
||||
show: false,
|
||||
title: "Recorder Tab Converter",
|
||||
backgroundColor: "#f3ecdf",
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
// Renderer sandbox cannot create shared memory here, so the window
|
||||
// fails with ERR_FAILED. The page is the local UI and has no Node access.
|
||||
sandbox: false,
|
||||
},
|
||||
});
|
||||
window.once("ready-to-show", () => window.show());
|
||||
await window.loadURL(url);
|
||||
}
|
||||
|
||||
app.whenReady().then(open).catch((error) => {
|
||||
dialog.showErrorBox("Recorder Tab Converter", error instanceof Error ? error.message : String(error));
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on("before-quit", stopServer);
|
||||
app.on("window-all-closed", () => {
|
||||
stopServer();
|
||||
app.quit();
|
||||
});
|
||||
Reference in new issue
Block a user