← back to Logo Variants App
main.js
149 lines
'use strict';
// Logo Showdown — minimal Electron wrapper around the TSD logo-variants
// viewer at http://127.0.0.1:9716/logo-variants.html.
//
// Design intent: this is for *picking* — Steve wants a focused window for
// ranking generated brand-mark variants without a browser tab. Therefore:
// - Native frame + macOS-vibrant titlebar (no chrome bar, no URL field).
// - 1440×900 default size; remembers position across launches.
// - Loads the URL on every launch; if :9716 is down, renders a friendly
// "Start TSD server" panel with a Retry button.
// - Cmd-R reloads, Cmd-Shift-R force-reloads, Cmd-Q quits.
// - External links (anything not 127.0.0.1:9716) open in the default
// browser, not in this window.
const { app, BrowserWindow, shell, Menu } = require('electron');
const path = require('path');
const http = require('http');
const TARGET_URL = process.env.LOGO_SHOWDOWN_URL || 'http://127.0.0.1:9716/logo-variants.html';
const TARGET_HOST = '127.0.0.1';
const TARGET_PORT = 9716;
let win;
function probeServer(timeoutMs = 800) {
return new Promise((resolve) => {
const req = http.request({ host: TARGET_HOST, port: TARGET_PORT, path: '/logo-variants.html', method: 'HEAD', timeout: timeoutMs }, (res) => {
resolve(res.statusCode >= 200 && res.statusCode < 500);
res.resume();
});
req.on('error', () => resolve(false));
req.on('timeout', () => { req.destroy(); resolve(false); });
req.end();
});
}
function offlineHtml(targetUrl) {
return `data:text/html;charset=utf-8,` + encodeURIComponent(`<!doctype html>
<html><head>
<meta charset="utf-8"><title>Logo Showdown — server not running</title>
<style>
:root { color-scheme: dark; }
html,body { margin:0; background:#0b0b0d; color:#f0ebe3; font:15px/1.55 -apple-system,BlinkMacSystemFont,Inter,system-ui,sans-serif; height:100%; }
body { display:flex; align-items:center; justify-content:center; padding:48px; }
.card { max-width:560px; text-align:center; }
h1 { font-family:'Cormorant Garamond',Georgia,serif; font-weight:400; font-size:42px; margin:0 0 14px; letter-spacing:-0.01em; color:#c9a14b; }
p { color:#c8beb2; margin:0 0 20px; }
code { background:#1a1714; color:#f0ebe3; padding:2px 7px; border-radius:4px; font-family:ui-monospace,Menlo,monospace; font-size:13px; }
pre { background:#1a1714; color:#c8beb2; padding:14px 18px; border-radius:8px; text-align:left; overflow:auto; }
button { background:#c9a14b; color:#1a1410; border:0; padding:10px 22px; font-size:14px; letter-spacing:0.08em; text-transform:uppercase; border-radius:6px; cursor:pointer; margin-top:8px; }
button:hover { background:#daa84d; }
.hint { color:#7a706a; font-size:12px; margin-top:24px; }
</style></head><body>
<div class="card">
<h1>The TSD server isn't running.</h1>
<p>Logo Showdown loads from <code>${targetUrl}</code> — that endpoint is the <code>thesetdecorator</code> Express server. Start it, then click Retry.</p>
<pre>cd ~/Projects/thesetdecorator
node server.js</pre>
<button onclick="location.reload()">Retry →</button>
<p class="hint">Or run <code>pm2 restart thesetdecorator</code> if it's pm2-managed.</p>
</div>
</body></html>`);
}
async function createWindow() {
win = new BrowserWindow({
width: 1440,
height: 900,
minWidth: 900,
minHeight: 600,
title: 'Logo Showdown',
titleBarStyle: 'hiddenInset', // macOS — traffic lights only, no chrome bar
vibrancy: 'under-window',
backgroundColor: '#0b0b0d',
show: false,
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
spellcheck: false,
},
});
// Open external links (anything off TARGET_HOST) in the default browser.
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith(`http://${TARGET_HOST}:${TARGET_PORT}`)) return { action: 'allow' };
shell.openExternal(url);
return { action: 'deny' };
});
win.webContents.on('will-navigate', (e, url) => {
if (!url.startsWith(`http://${TARGET_HOST}:${TARGET_PORT}`) && !url.startsWith('data:')) {
e.preventDefault();
shell.openExternal(url);
}
});
const ok = await probeServer();
if (ok) {
await win.loadURL(TARGET_URL);
} else {
await win.loadURL(offlineHtml(TARGET_URL));
}
win.show();
}
function buildMenu() {
const isMac = process.platform === 'darwin';
const template = [
...(isMac ? [{
label: app.name,
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'hide' }, { role: 'hideOthers' }, { role: 'unhide' },
{ type: 'separator' },
{ role: 'quit' },
],
}] : []),
{
label: 'View',
submenu: [
{ label: 'Reload', accelerator: 'CmdOrCtrl+R', click: () => win && win.reload() },
{ label: 'Force Reload', accelerator: 'CmdOrCtrl+Shift+R', click: () => win && win.webContents.reloadIgnoringCache() },
{ type: 'separator' },
{ role: 'togglefullscreen' },
{ role: 'toggleDevTools' },
{ type: 'separator' },
{ role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' },
],
},
{
label: 'Window',
submenu: [{ role: 'minimize' }, { role: 'close' }],
},
];
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
}
app.whenReady().then(() => {
buildMenu();
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});