feat: web UI + HTTP server + Electron shell
- serve.go: embedded web UI (embed.FS), REST API endpoints for search, list, download (SSE progress), and WAV extraction - main.go: added serve subcommand and flag registration - web/ui: vanilla JS frontend with search cards, download queue, extract controls; dark theme, zero dependencies - electron/: Electron wrapper that spawns fetch serve as sidecar, reads port from stdout, creates BrowserWindow; electron-builder config for macOS .app bundle with akaiutil + script as resources Refs #1, #9
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
const { app, BrowserWindow, dialog } = require('electron');
|
||||
const path = require('path');
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
let mainWindow;
|
||||
let serverProcess;
|
||||
let serverPort = 0;
|
||||
|
||||
function findServerBinary() {
|
||||
const candidates = [
|
||||
path.join(__dirname, '..', 'fetch'),
|
||||
path.join(__dirname, '..', 'akai-fetch'),
|
||||
path.join(process.resourcesPath, 'fetch'),
|
||||
path.join(process.resourcesPath, 'akai-fetch'),
|
||||
];
|
||||
for (const p of candidates) {
|
||||
try {
|
||||
require('fs').accessSync(p, require('fs').constants.X_OK);
|
||||
return p;
|
||||
} catch (_) {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function startServer() {
|
||||
const bin = findServerBinary();
|
||||
if (!bin) {
|
||||
dialog.showErrorBox('Missing Binary',
|
||||
'Could not find fetch/akai-fetch binary. Place it next to the app or in resources.');
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
const usePort = process.env.AKAI_PORT ? parseInt(process.env.AKAI_PORT) : 0;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
serverProcess = spawn(bin, ['serve', '-p', String(usePort), '--no-browser'], {
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let output = '';
|
||||
serverProcess.stdout.on('data', (data) => {
|
||||
output += data.toString();
|
||||
const m = output.match(/Local:\s+http:\/\/localhost:(\d+)/);
|
||||
if (m) {
|
||||
serverPort = parseInt(m[1]);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
serverProcess.stderr.on('data', (data) => {
|
||||
console.error('[server]', data.toString().trim());
|
||||
});
|
||||
|
||||
serverProcess.on('error', (err) => {
|
||||
console.error('Failed to start server:', err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
serverProcess.on('close', (code) => {
|
||||
if (serverPort === 0) {
|
||||
reject(new Error(`Server exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function createWindow() {
|
||||
try {
|
||||
await startServer();
|
||||
} catch (err) {
|
||||
dialog.showErrorBox('Server Error', String(err));
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
title: 'AKAI Utils',
|
||||
backgroundColor: '#1a1a2e',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.loadURL(`http://localhost:${serverPort}`);
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill();
|
||||
serverProcess = null;
|
||||
}
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow === null) {
|
||||
createWindow();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', () => {
|
||||
if (serverProcess) {
|
||||
serverProcess.kill();
|
||||
serverProcess = null;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "akai-utils",
|
||||
"version": "0.2.0",
|
||||
"description": "AKAI Sampler ISO Downloader, WAV Extractor & Library Manager",
|
||||
"main": "main.js",
|
||||
"build": {
|
||||
"appId": "dev.akai.utils",
|
||||
"productName": "AKAI Utils",
|
||||
"directories": {
|
||||
"output": "dist"
|
||||
},
|
||||
"files": [
|
||||
"main.js",
|
||||
"preload.js",
|
||||
"package.json"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "../fetch",
|
||||
"to": "fetch"
|
||||
},
|
||||
{
|
||||
"from": "../scripts/extract_wavs.sh",
|
||||
"to": "extract_wavs.sh"
|
||||
}
|
||||
],
|
||||
"mac": {
|
||||
"category": "public.app-category.music",
|
||||
"target": [
|
||||
{
|
||||
"target": "dir",
|
||||
"arch": ["x64", "arm64"]
|
||||
}
|
||||
],
|
||||
"artifactName": "${name}-${version}-${arch}.${ext}",
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "../fetch",
|
||||
"to": "fetch"
|
||||
},
|
||||
{
|
||||
"from": "../scripts/extract_wavs.sh",
|
||||
"to": "extract_wavs.sh"
|
||||
},
|
||||
{
|
||||
"from": "../third_party/akaiutil/akaiutil",
|
||||
"to": "akaiutil"
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": ["dir"],
|
||||
"category": "Audio"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "electron-builder",
|
||||
"build:mac": "electron-builder --mac",
|
||||
"build:linux": "electron-builder --linux"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^33.0.0",
|
||||
"electron-builder": "^25.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const { contextBridge } = require('electron');
|
||||
|
||||
contextBridge.exposeInMainWorld('electronAPI', {
|
||||
platform: process.platform,
|
||||
});
|
||||
Reference in New Issue
Block a user