|
|
|
|
@ -2,11 +2,153 @@
@@ -2,11 +2,153 @@
|
|
|
|
|
|
|
|
|
|
const { app, BrowserWindow, ipcMain, shell, Menu } = require('electron') |
|
|
|
|
const fs = require('fs') |
|
|
|
|
const http = require('http') |
|
|
|
|
const path = require('path') |
|
|
|
|
|
|
|
|
|
/** True when running from source (`electron .`); false when packaged. */ |
|
|
|
|
const isDev = !app.isPackaged |
|
|
|
|
|
|
|
|
|
/** Stable loopback origin for packaged builds so YouTube embeds get a real `origin` (see YoutubeEmbeddedPlayer). */ |
|
|
|
|
const PACKAGED_STATIC_PORT_START = 45279 |
|
|
|
|
const PACKAGED_STATIC_PORT_TRIES = 40 |
|
|
|
|
|
|
|
|
|
const MIME = { |
|
|
|
|
'.html': 'text/html; charset=utf-8', |
|
|
|
|
'.js': 'application/javascript; charset=utf-8', |
|
|
|
|
'.mjs': 'application/javascript; charset=utf-8', |
|
|
|
|
'.css': 'text/css; charset=utf-8', |
|
|
|
|
'.json': 'application/json', |
|
|
|
|
'.png': 'image/png', |
|
|
|
|
'.jpg': 'image/jpeg', |
|
|
|
|
'.jpeg': 'image/jpeg', |
|
|
|
|
'.gif': 'image/gif', |
|
|
|
|
'.webp': 'image/webp', |
|
|
|
|
'.svg': 'image/svg+xml', |
|
|
|
|
'.ico': 'image/x-icon', |
|
|
|
|
'.woff': 'font/woff', |
|
|
|
|
'.woff2': 'font/woff2', |
|
|
|
|
'.webmanifest': 'application/manifest+json', |
|
|
|
|
'.txt': 'text/plain; charset=utf-8', |
|
|
|
|
'.wasm': 'application/wasm', |
|
|
|
|
'.map': 'application/json' |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let packagedStaticServer = null |
|
|
|
|
/** @type {string | null} */ |
|
|
|
|
let packagedStaticBaseUrl = null |
|
|
|
|
/** @type {Promise<string> | null} */ |
|
|
|
|
let packagedStaticStartPromise = null |
|
|
|
|
|
|
|
|
|
function resolveUnderDist(distDir, pathname) { |
|
|
|
|
let rel = pathname |
|
|
|
|
try { |
|
|
|
|
rel = decodeURIComponent(pathname) |
|
|
|
|
} catch { |
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
rel = rel.replace(/^\/+/, '') |
|
|
|
|
const candidate = path.resolve(distDir, rel) |
|
|
|
|
const root = path.resolve(distDir) |
|
|
|
|
const prefix = root.endsWith(path.sep) ? root : root + path.sep |
|
|
|
|
if (candidate !== root && !candidate.startsWith(prefix)) return null |
|
|
|
|
return candidate |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function createPackagedStaticHandler(distDir) { |
|
|
|
|
const indexPath = path.join(distDir, 'index.html') |
|
|
|
|
|
|
|
|
|
return function handleRequest(req, res) { |
|
|
|
|
if (req.method !== 'GET' && req.method !== 'HEAD') { |
|
|
|
|
res.writeHead(405).end() |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let pathname = '/' |
|
|
|
|
try { |
|
|
|
|
pathname = new URL(req.url || '/', 'http://127.0.0.1').pathname |
|
|
|
|
} catch { |
|
|
|
|
pathname = '/' |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const sendIndex = () => { |
|
|
|
|
fs.readFile(indexPath, (err, data) => { |
|
|
|
|
if (err) { |
|
|
|
|
res.writeHead(500).end('Missing index.html') |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Content-Length': data.length }) |
|
|
|
|
res.end(req.method === 'HEAD' ? undefined : data) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const filePath = resolveUnderDist(distDir, pathname) |
|
|
|
|
if (!filePath) { |
|
|
|
|
res.writeHead(403).end() |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fs.stat(filePath, (err, st) => { |
|
|
|
|
if (!err && st.isFile()) { |
|
|
|
|
const ext = path.extname(filePath).toLowerCase() |
|
|
|
|
const ct = MIME[ext] || 'application/octet-stream' |
|
|
|
|
res.writeHead(200, { 'Content-Type': ct, 'Content-Length': st.size }) |
|
|
|
|
if (req.method === 'HEAD') { |
|
|
|
|
res.end() |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
fs.createReadStream(filePath).pipe(res) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
sendIndex() |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function startPackagedStaticServer(distDir) { |
|
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
|
const handler = createPackagedStaticHandler(distDir) |
|
|
|
|
const server = http.createServer(handler) |
|
|
|
|
|
|
|
|
|
const listenOn = (port, attempt) => { |
|
|
|
|
if (attempt >= PACKAGED_STATIC_PORT_TRIES) { |
|
|
|
|
reject(new Error('No free port for packaged static server')) |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
const onErr = (err) => { |
|
|
|
|
server.removeListener('error', onErr) |
|
|
|
|
if (err && err.code === 'EADDRINUSE') { |
|
|
|
|
listenOn(port + 1, attempt + 1) |
|
|
|
|
} else { |
|
|
|
|
reject(err) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
server.on('error', onErr) |
|
|
|
|
server.listen(port, '127.0.0.1', () => { |
|
|
|
|
server.removeListener('error', onErr) |
|
|
|
|
packagedStaticServer = server |
|
|
|
|
const addr = server.address() |
|
|
|
|
const p = typeof addr === 'object' && addr ? addr.port : port |
|
|
|
|
packagedStaticBaseUrl = `http://127.0.0.1:${p}` |
|
|
|
|
resolve(packagedStaticBaseUrl) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
listenOn(PACKAGED_STATIC_PORT_START, 0) |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function ensurePackagedStaticBaseUrl() { |
|
|
|
|
if (packagedStaticBaseUrl) return Promise.resolve(packagedStaticBaseUrl) |
|
|
|
|
if (!packagedStaticStartPromise) { |
|
|
|
|
const distDir = path.join(__dirname, '..', 'dist') |
|
|
|
|
packagedStaticStartPromise = startPackagedStaticServer(distDir).catch((err) => { |
|
|
|
|
packagedStaticStartPromise = null |
|
|
|
|
throw err |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
return packagedStaticStartPromise |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function resolveWindowIcon() { |
|
|
|
|
const candidates = isDev |
|
|
|
|
? [path.join(__dirname, '..', 'public', 'favicon.png')] |
|
|
|
|
@ -30,7 +172,20 @@ function loadRenderer(win) {
@@ -30,7 +172,20 @@ function loadRenderer(win) {
|
|
|
|
|
} |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
void ensurePackagedStaticBaseUrl() |
|
|
|
|
.then((baseUrl) => { |
|
|
|
|
if (win.isDestroyed()) return |
|
|
|
|
const u = new URL('/', baseUrl) |
|
|
|
|
u.search = '' |
|
|
|
|
u.hash = '' |
|
|
|
|
void win.loadURL(u.href) |
|
|
|
|
}) |
|
|
|
|
.catch((err) => { |
|
|
|
|
console.error('Packaged static server failed, falling back to file://', err) |
|
|
|
|
if (!win.isDestroyed()) { |
|
|
|
|
void win.loadFile(path.join(__dirname, '..', 'dist', 'index.html')) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function createWindow() { |
|
|
|
|
@ -107,6 +262,19 @@ app.whenReady().then(() => {
@@ -107,6 +262,19 @@ app.whenReady().then(() => {
|
|
|
|
|
}) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
app.on('will-quit', () => { |
|
|
|
|
if (packagedStaticServer) { |
|
|
|
|
try { |
|
|
|
|
packagedStaticServer.close() |
|
|
|
|
} catch { |
|
|
|
|
// ignore |
|
|
|
|
} |
|
|
|
|
packagedStaticServer = null |
|
|
|
|
packagedStaticBaseUrl = null |
|
|
|
|
packagedStaticStartPromise = null |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
app.on('window-all-closed', () => { |
|
|
|
|
if (process.platform !== 'darwin') app.quit() |
|
|
|
|
}) |
|
|
|
|
|