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",