diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 38e6405..fb6e47a 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -14,7 +14,7 @@ body: attributes: label: ProtonShift version description: "Check the app's About dialog or `electron/package.json`." - placeholder: "0.8.6" + placeholder: "0.8.7" validations: required: true diff --git a/assets/io.github.protonshift.metainfo.xml b/assets/io.github.protonshift.metainfo.xml index 8d31acb..dc14419 100644 --- a/assets/io.github.protonshift.metainfo.xml +++ b/assets/io.github.protonshift.metainfo.xml @@ -26,6 +26,7 @@ io.github.protonshift.desktop https://github.com/I4cTime/protonshift + diff --git a/electron/main.ts b/electron/main.ts index 3e6b12d..27a25d5 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, ipcMain, session } from "electron"; +import { app, BrowserWindow, ipcMain } from "electron"; import { ChildProcess, spawn } from "child_process"; import * as path from "path"; import * as http from "http"; @@ -6,6 +6,8 @@ import * as http from "http"; let mainWindow: BrowserWindow | null = null; let pythonProcess: ChildProcess | null = null; let apiPort: number | null = null; +let staticServer: http.Server | null = null; +let staticRendererPort: number | null = null; const isDev = !app.isPackaged; @@ -126,6 +128,102 @@ function getIconPath(): string { return path.join(process.resourcesPath, "assets", "256x256.png"); } +function mimeFor(filePath: string): string { + const ext = path.extname(filePath).toLowerCase(); + const map: Record = { + ".html": "text/html; charset=utf-8", + ".js": "application/javascript; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webp": "image/webp", + ".svg": "image/svg+xml", + ".ico": "image/x-icon", + ".woff": "font/woff", + ".woff2": "font/woff2", + ".txt": "text/plain; charset=utf-8", + ".map": "application/json; charset=utf-8", + }; + return map[ext] ?? "application/octet-stream"; +} + +/** Serves Next static export over http://127.0.0.1 — root-relative /_next/... URLs do not work with file:// */ +function startStaticRendererServer(rootDir: string): Promise { + const root = path.resolve(rootDir); + return new Promise((resolve, reject) => { + const server = http.createServer((req, res) => { + try { + const rawPath = req.url?.split("?")[0] ?? "/"; + let pathname: string; + try { + pathname = decodeURIComponent(rawPath); + } catch { + res.writeHead(400).end(); + return; + } + if (pathname.includes("..")) { + res.writeHead(403).end(); + return; + } + const rel = pathname.replace(/^\/+/, ""); + const rootResolved = path.resolve(root); + + const candidates: string[] = []; + if (rel === "" || rel === "/") { + candidates.push(path.join(rootResolved, "index.html")); + } else { + candidates.push( + path.join(rootResolved, rel), + path.join(rootResolved, `${rel}.html`), + path.join(rootResolved, rel, "index.html"), + ); + } + + let found: string | null = null; + for (const candidate of candidates) { + const normalized = path.resolve(candidate); + if (!normalized.startsWith(rootResolved + path.sep) && normalized !== rootResolved) { + continue; + } + if (fs.existsSync(normalized) && fs.statSync(normalized).isFile()) { + found = normalized; + break; + } + } + + if (!found) { + res.writeHead(404).end("Not found"); + return; + } + + const body = fs.readFileSync(found); + res.writeHead(200, { + "Content-Type": mimeFor(found), + "Content-Length": String(body.length), + "Cache-Control": "no-store", + }); + res.end(body); + } catch { + res.writeHead(500).end(); + } + }); + + server.on("error", reject); + server.listen(0, "127.0.0.1", () => { + const addr = server.address(); + staticServer = server; + if (addr && typeof addr === "object") { + staticRendererPort = addr.port; + resolve(addr.port); + } else { + reject(new Error("Static server failed to bind")); + } + }); + }); +} + function createWindow(): void { mainWindow = new BrowserWindow({ width: 1200, @@ -147,6 +245,8 @@ function createWindow(): void { if (isDev) { mainWindow.loadURL("http://localhost:3000"); mainWindow.webContents.openDevTools({ mode: "detach" }); + } else if (staticRendererPort) { + mainWindow.loadURL(`http://127.0.0.1:${staticRendererPort}/`); } else { mainWindow.loadFile(path.join(__dirname, "..", "renderer", "out", "index.html")); } @@ -202,6 +302,10 @@ app.on("ready", async () => { try { apiPort = await startPython(); await waitForHealth(apiPort); + if (!isDev) { + const outDir = path.join(__dirname, "..", "renderer", "out"); + await startStaticRendererServer(outDir); + } } catch (err) { console.error("Failed to start Python backend:", err); app.quit(); @@ -219,6 +323,11 @@ app.on("before-quit", () => { pythonProcess.kill("SIGTERM"); pythonProcess = null; } + if (staticServer) { + staticServer.close(); + staticServer = null; + staticRendererPort = null; + } }); app.on("activate", () => { diff --git a/electron/package.json b/electron/package.json index 182ae3a..448b24c 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "protonshift", - "version": "0.8.6", + "version": "0.8.7", "description": "Linux game configuration toolkit", "main": "dist/main.js", "scripts": { diff --git a/electron/renderer/package.json b/electron/renderer/package.json index 3eb1011..be42761 100644 --- a/electron/renderer/package.json +++ b/electron/renderer/package.json @@ -1,6 +1,6 @@ { "name": "protonshift-renderer", - "version": "0.8.6", + "version": "0.8.7", "private": true, "scripts": { "dev": "next dev --turbopack", diff --git a/pyproject.toml b/pyproject.toml index 0131dff..ee519fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "protonshift" -version = "0.8.6" +version = "0.8.7" description = "Linux game configuration toolkit: GPU, launch options, Proton, env vars" readme = "README.md" requires-python = ">=3.12" diff --git a/src/game_setup_hub/__init__.py b/src/game_setup_hub/__init__.py index e972e93..75fd889 100644 --- a/src/game_setup_hub/__init__.py +++ b/src/game_setup_hub/__init__.py @@ -1,3 +1,3 @@ """ProtonShift — game configuration for Pop!_OS, Ubuntu, and related distros.""" -__version__ = "0.8.6" +__version__ = "0.8.7" diff --git a/src/game_setup_hub/api.py b/src/game_setup_hub/api.py index 2979c17..7bceac4 100644 --- a/src/game_setup_hub/api.py +++ b/src/game_setup_hub/api.py @@ -324,7 +324,7 @@ class StatusResponse(BaseModel): # App setup # --------------------------------------------------------------------------- -app = FastAPI(title="ProtonShift API", version="0.8.6") +app = FastAPI(title="ProtonShift API", version="0.8.7") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) # Locks for serializing writes to shared resources diff --git a/src/game_setup_hub/app.py b/src/game_setup_hub/app.py index 4642910..b497aec 100644 --- a/src/game_setup_hub/app.py +++ b/src/game_setup_hub/app.py @@ -1032,7 +1032,7 @@ def on_about(_action, _param): about = Adw.AboutWindow( transient_for=win, application_name="ProtonShift", - version="0.8.6", + version="0.8.7", developer_name="ProtonShift", website="https://github.com/protonshift/protonshift", application_icon="io.github.protonshift",