stremio/apps/token-tray/src/main.ts
Jos Vooges | STH 15757d98b5 Add MC brand icon to Token Tray tray and Windows installer.
Previous builds used an empty placeholder and the default Electron icon.
2026-09-23 00:42:48 +02:00

303 lines
9.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
app,
BrowserWindow,
Menu,
Tray,
dialog,
nativeImage,
shell,
} from "electron";
import fs from "fs";
import path from "path";
import { configPath, loadConfig, saveConfig, type TrayConfig } from "./config";
import { pingAgent } from "./viaplay";
import { refreshAllSilent, refreshDueSilent } from "./jobs";
let tray: Tray | null = null;
let settingsWin: BrowserWindow | null = null;
let cfg: TrayConfig = loadConfig();
let pollTimer: ReturnType<typeof setInterval> | null = null;
let busy = false;
let lastPollAt: string | null = null;
let lastDueCount = 0;
const POLL_MS = 10 * 60 * 1000; // elke 10 min: vraag server wie bijna verloopt
const WITHIN_MINUTES = 90; // vernieuw als token < 90 min geldig is
function resolveAsset(...parts: string[]): string {
const candidates = [
path.join(__dirname, "..", "assets", ...parts),
path.join(app.getAppPath(), "assets", ...parts),
path.join(process.resourcesPath, "assets", ...parts),
];
for (const p of candidates) {
if (fs.existsSync(p)) return p;
}
return candidates[0];
}
function loadTrayImage(): Electron.NativeImage {
for (const name of ["tray-icon-32.png", "tray-icon-16.png", "icon.png"] as const) {
const p = resolveAsset(name);
if (!fs.existsSync(p)) continue;
let img = nativeImage.createFromPath(p);
if (img.isEmpty()) continue;
const { width } = img.getSize();
// Windows systeemvak: 16–32 px; groter schalen ziet er wazig/onzichtbaar uit
if (width > 32) img = img.resize({ width: 16, height: 16 });
return img;
}
return nativeImage.createEmpty();
}
function statusTooltip(): string {
const parts = ["Media Cluster Token Tray (auto)"];
if (busy) parts.push("Bezig met stille refresh…");
parts.push(`Poll: elke 10 min · drempel ${WITHIN_MINUTES} min`);
if (lastPollAt) {
parts.push(`Laatste check: ${new Date(lastPollAt).toLocaleString("nl-NL")}`);
}
if (lastDueCount > 0) parts.push(`Laatst due: ${lastDueCount} account(s)`);
if (cfg.lastPushSummary) parts.push(cfg.lastPushSummary);
if (cfg.lastError) parts.push(`Fout: ${cfg.lastError}`);
return parts.join("\n");
}
function rebuildMenu() {
if (!tray) return;
const menu = Menu.buildFromTemplate([
{
label: busy ? "Bezig…" : "Nu checken (server → due)",
enabled: !busy,
click: () => void runAuto(true),
},
{
label: "Alles vernieuwen (stil)",
enabled: !busy,
click: () => void runManualAll(true),
},
{
label: "Alles vernieuwen (zichtbaar)",
enabled: !busy,
click: () => void runManualAll(false),
},
{ type: "separator" },
{ label: "Verbinding testen", click: () => void testConnection() },
{ label: "Instellingen…", click: () => openSettings() },
{
label: "Configmap openen",
click: () => void shell.showItemInFolder(configPath()),
},
{ type: "separator" },
{ label: "Afsluiten", click: () => app.quit() },
]);
tray.setContextMenu(menu);
tray.setToolTip(statusTooltip());
}
function openSettings() {
if (settingsWin && !settingsWin.isDestroyed()) {
settingsWin.focus();
return;
}
settingsWin = new BrowserWindow({
width: 520,
height: 360,
title: "Token Tray — instellingen",
autoHideMenuBar: true,
webPreferences: { contextIsolation: true, nodeIntegration: false },
});
const html = `<!doctype html>
<html lang="nl"><head><meta charset="utf-8"/><title>Instellingen</title>
<style>
body{font-family:Segoe UI,system-ui,sans-serif;margin:1.25rem;background:#12141a;color:#e8eaed}
label{display:block;margin:0.75rem 0 0.25rem;font-size:0.85rem;opacity:.85}
input{width:100%;box-sizing:border-box;padding:0.5rem 0.6rem;border-radius:6px;border:1px solid #333;background:#1c1f28;color:#fff}
button{margin-top:1.25rem;padding:0.55rem 1rem;border:0;border-radius:6px;background:#3b82f6;color:#fff;font-weight:600;cursor:pointer}
p{font-size:0.85rem;opacity:.75;line-height:1.4}
</style></head><body>
<h2 style="margin-top:0">Token Tray (auto)</h2>
<p>Server houdt JWT-expiry bij. Deze PC pollt elke 10 min en logt stil in via Chrome als tokens bijna verlopen. Accounts + wachtwoorden staan in Admin → Scripts.</p>
<label>API URL</label>
<input id="apiUrl" value="${escapeHtml(cfg.apiUrl)}" />
<label>Tray key (X-Tray-Key)</label>
<input id="trayKey" value="${escapeHtml(cfg.trayKey)}" />
<button id="save">Opslaan</button>
<script>
document.getElementById('save').onclick = () => {
const payload = {
apiUrl: document.getElementById('apiUrl').value,
trayKey: document.getElementById('trayKey').value,
};
document.title = 'SAVE:' + encodeURIComponent(JSON.stringify(payload));
};
</script>
</body></html>`;
settingsWin.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(html)}`);
settingsWin.on("page-title-updated", (e, title) => {
if (!title.startsWith("SAVE:")) return;
e.preventDefault();
try {
const payload = JSON.parse(decodeURIComponent(title.slice(5))) as {
apiUrl: string;
trayKey: string;
};
cfg.apiUrl = payload.apiUrl.trim().replace(/\/$/, "") || cfg.apiUrl;
cfg.trayKey = payload.trayKey.trim();
saveConfig(cfg);
rebuildMenu();
startPolling();
void dialog.showMessageBox({
type: "info",
message: "Opgeslagen",
detail: "Auto-poll draait (elke 10 min, alleen bijna-verlopen tokens).",
});
settingsWin?.close();
} catch (err) {
void dialog.showErrorBox("Opslaan mislukt", String(err));
}
});
settingsWin.on("closed", () => {
settingsWin = null;
});
}
function escapeHtml(s: string): string {
return s
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
/** Automatische poll: alleen due accounts, geen dialoog bij succes. */
async function runAuto(showDialogIfManual: boolean) {
if (busy) return;
if (!cfg.trayKey.trim()) {
if (showDialogIfManual) openSettings();
return;
}
busy = true;
rebuildMenu();
try {
const { fetchRefreshDue } = await import("./jobs");
const due = await fetchRefreshDue(cfg, WITHIN_MINUTES);
lastPollAt = new Date().toISOString();
lastDueCount = due.length;
if (!due.length) {
cfg = loadConfig();
cfg.lastPushSummary = "Auto: niets te vernieuwen";
saveConfig(cfg);
if (showDialogIfManual) {
void dialog.showMessageBox({
type: "info",
message: "Niets te doen",
detail: `Geen tokens die binnen ${WITHIN_MINUTES} min verlopen.`,
});
}
return;
}
const summary = await refreshDueSilent(cfg, {
headless: true,
withinMinutes: WITHIN_MINUTES,
notifyOnFailOnly: !showDialogIfManual,
});
cfg = loadConfig();
if (showDialogIfManual) {
void dialog.showMessageBox({
type: summary.failed ? "warning" : "info",
message: "Auto-refresh klaar",
detail: `${summary.ok} ok · ${summary.failed} fout\n${summary.errors.slice(0, 3).join("\n")}`,
});
}
} catch (err) {
if (showDialogIfManual) {
void dialog.showErrorBox("Auto-check mislukt", String(err));
}
} finally {
busy = false;
rebuildMenu();
}
}
async function runManualAll(headless: boolean) {
if (busy) return;
if (!cfg.trayKey.trim()) {
openSettings();
return;
}
busy = true;
rebuildMenu();
try {
const summary = await refreshAllSilent(cfg, { headless, notify: true });
cfg = loadConfig();
void dialog.showMessageBox({
type: summary.failed ? "warning" : "info",
message: "Refresh klaar",
detail: [
`${summary.ok} ok · ${summary.failed} fout · ${summary.skipped} zonder wachtwoord`,
...summary.errors.slice(0, 5),
].join("\n"),
});
} catch (err) {
void dialog.showErrorBox("Refresh mislukt", String(err));
} finally {
busy = false;
rebuildMenu();
}
}
async function testConnection() {
if (!cfg.trayKey.trim()) {
openSettings();
return;
}
try {
const ok = await pingAgent(cfg);
void dialog.showMessageBox({
type: ok ? "info" : "warning",
message: ok ? "Verbinding OK" : "Geen geldige tray-key of API",
detail: cfg.apiUrl,
});
} catch (err) {
void dialog.showErrorBox("Verbinding mislukt", String(err));
}
}
function createTray() {
const img = loadTrayImage();
tray = new Tray(img);
tray.setToolTip("Media Cluster Token Tray");
rebuildMenu();
tray.on("double-click", () => void runAuto(true));
}
function startPolling() {
if (pollTimer) clearInterval(pollTimer);
// Direct één check, daarna elke 10 min
void runAuto(false);
pollTimer = setInterval(() => void runAuto(false), POLL_MS);
}
app.whenReady().then(() => {
try {
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: true,
});
} catch {
/* ignore — niet op alle platforms beschikbaar */
}
cfg = loadConfig();
createTray();
if (!cfg.trayKey) openSettings();
else startPolling();
});
app.on("window-all-closed", () => {
/* tray blijft */
});
app.on("before-quit", () => {
if (pollTimer) clearInterval(pollTimer);
});