Add portable Windows desktop downloads and release uploads
This commit is contained in:
BIN
desktop/assets/icon.ico
Normal file
BIN
desktop/assets/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
desktop/assets/icon.png
Normal file
BIN
desktop/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
175
desktop/main.cjs
Normal file
175
desktop/main.cjs
Normal file
@@ -0,0 +1,175 @@
|
||||
const { app, BrowserWindow, Menu, dialog, shell } = require("electron");
|
||||
const path = require("node:path");
|
||||
const { SITE_URL, isInternal, isExternal } = require("./navigation.cjs");
|
||||
let mainWindow;
|
||||
const smokeTest = process.argv.includes("--smoke-test");
|
||||
app.setAppUserModelId("app.minegeo.desktop");
|
||||
if (!app.requestSingleInstanceLock()) app.quit();
|
||||
else {
|
||||
app.on("second-instance", () => {
|
||||
if (!mainWindow) return;
|
||||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
app.whenReady().then(createWindow);
|
||||
}
|
||||
function openExternal(url) {
|
||||
if (isExternal(url)) shell.openExternal(url).catch(() => {});
|
||||
}
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1360,
|
||||
height: 900,
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
title: "MineGeo",
|
||||
backgroundColor: "#14201b",
|
||||
show: false,
|
||||
icon: path.join(__dirname, "assets/icon.png"),
|
||||
webPreferences: {
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true,
|
||||
sandbox: true,
|
||||
webSecurity: true,
|
||||
allowRunningInsecureContent: false,
|
||||
partition: "persist:minegeo",
|
||||
},
|
||||
});
|
||||
const contents = mainWindow.webContents;
|
||||
contents.session.setPermissionRequestHandler((_wc, _permission, callback) =>
|
||||
callback(false),
|
||||
);
|
||||
contents.session.setPermissionCheckHandler(() => false);
|
||||
contents.on("will-attach-webview", (event) => event.preventDefault());
|
||||
for (const eventName of ["will-navigate", "will-redirect"]) {
|
||||
contents.on(eventName, (event, url) => {
|
||||
if (!isInternal(url)) {
|
||||
event.preventDefault();
|
||||
openExternal(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
contents.setWindowOpenHandler(({ url }) => {
|
||||
if (isInternal(url)) contents.loadURL(url).catch(() => {});
|
||||
else openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
contents.on(
|
||||
"did-fail-load",
|
||||
(_event, code, _description, _url, isMainFrame) => {
|
||||
if (!isMainFrame || code === -3) return;
|
||||
if (smokeTest) {
|
||||
console.error("MineGeo smoke test: load failed", code);
|
||||
app.exit(1);
|
||||
return;
|
||||
}
|
||||
const html =
|
||||
'<!doctype html><html lang="en"><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src \'none\'; style-src \'unsafe-inline\'"><title>MineGeo · Connection interrupted</title><body style="background:#14201b;color:#f4f1e6;font:18px system-ui;padding:10vh 10vw"><h1>Let’s get you back to MineGeo.</h1><p>Check your internet connection, then choose Game → Retry connection (Ctrl+R).</p><p>Your account and saved games will be here when you reconnect.</p></body></html>';
|
||||
contents
|
||||
.loadURL("data:text/html;charset=utf-8," + encodeURIComponent(html))
|
||||
.catch(() => {});
|
||||
mainWindow.show();
|
||||
},
|
||||
);
|
||||
mainWindow.once("ready-to-show", () => mainWindow.show());
|
||||
contents.on("did-finish-load", () => {
|
||||
if (smokeTest && isInternal(contents.getURL())) {
|
||||
console.log(
|
||||
"MineGeo smoke test: production site loaded with sandbox enabled",
|
||||
);
|
||||
app.exit(0);
|
||||
}
|
||||
});
|
||||
contents.session.on("will-download", (_event, item) => {
|
||||
item.once("done", (_downloadEvent, state) => {
|
||||
if (state === "interrupted" && !mainWindow.isDestroyed())
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
type: "error",
|
||||
title: "Download interrupted",
|
||||
message: "Please check your connection and try downloading again.",
|
||||
});
|
||||
});
|
||||
});
|
||||
Menu.setApplicationMenu(
|
||||
Menu.buildFromTemplate([
|
||||
{
|
||||
label: "Game",
|
||||
submenu: [
|
||||
{
|
||||
label: "Home",
|
||||
accelerator: "Alt+Home",
|
||||
click: () => contents.loadURL(SITE_URL).catch(() => {}),
|
||||
},
|
||||
{
|
||||
label: "Back",
|
||||
accelerator: "Alt+Left",
|
||||
click: () => {
|
||||
if (contents.navigationHistory.canGoBack())
|
||||
contents.navigationHistory.goBack();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Retry connection",
|
||||
accelerator: "CmdOrCtrl+R",
|
||||
click: () => {
|
||||
if (isInternal(contents.getURL())) contents.reload();
|
||||
else contents.loadURL(SITE_URL).catch(() => {});
|
||||
},
|
||||
},
|
||||
{ type: "separator" },
|
||||
{ role: "quit" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
submenu: [
|
||||
{ role: "undo" },
|
||||
{ role: "redo" },
|
||||
{ type: "separator" },
|
||||
{ role: "cut" },
|
||||
{ role: "copy" },
|
||||
{ role: "paste" },
|
||||
{ role: "selectAll" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "View",
|
||||
submenu: [
|
||||
{ role: "resetZoom" },
|
||||
{ role: "zoomIn" },
|
||||
{ role: "zoomOut" },
|
||||
{ type: "separator" },
|
||||
{ role: "togglefullscreen" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Help",
|
||||
submenu: [
|
||||
{ label: "Open in browser", click: () => openExternal(SITE_URL) },
|
||||
{
|
||||
label: "About MineGeo",
|
||||
click: () =>
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
title: "MineGeo",
|
||||
message: `MineGeo Desktop ${app.getVersion()}`,
|
||||
detail:
|
||||
"Play at minegeo.vercel.app. Requires an internet connection.",
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
);
|
||||
contents.loadURL(SITE_URL).catch(() => {});
|
||||
if (smokeTest) setTimeout(() => app.exit(1), 60000).unref();
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") app.quit();
|
||||
});
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
17
desktop/navigation.cjs
Normal file
17
desktop/navigation.cjs
Normal file
@@ -0,0 +1,17 @@
|
||||
const SITE_URL = "https://minegeo.vercel.app";
|
||||
function isInternal(value) {
|
||||
try {
|
||||
return new URL(value).origin === SITE_URL;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function isExternal(value) {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === "https:" && !url.username && !url.password;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
module.exports = { SITE_URL, isInternal, isExternal };
|
||||
3600
desktop/package-lock.json
generated
Normal file
3600
desktop/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
39
desktop/package.json
Normal file
39
desktop/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "minegeo-desktop",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "MineGeo desktop client",
|
||||
"author": "MineGeo",
|
||||
"main": "main.cjs",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"build": "node prepare-icon.mjs && electron-builder --win portable --x64"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "44.7.0",
|
||||
"electron-builder": "26.15.3"
|
||||
},
|
||||
"build": {
|
||||
"appId": "app.minegeo.desktop",
|
||||
"productName": "MineGeo",
|
||||
"asar": true,
|
||||
"directories": {
|
||||
"output": "dist",
|
||||
"buildResources": "assets"
|
||||
},
|
||||
"files": [
|
||||
"main.cjs",
|
||||
"navigation.cjs",
|
||||
"assets/icon.png",
|
||||
"assets/icon.ico"
|
||||
],
|
||||
"win": {
|
||||
"target": "portable",
|
||||
"icon": "assets/icon.ico"
|
||||
},
|
||||
"portable": {
|
||||
"artifactName": "MineGeo-${version}-windows-x64.exe",
|
||||
"requestExecutionLevel": "user"
|
||||
}
|
||||
}
|
||||
}
|
||||
34
desktop/prepare-icon.mjs
Normal file
34
desktop/prepare-icon.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import sharp from "sharp";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
const assets = new URL("./assets/", import.meta.url);
|
||||
await mkdir(assets, { recursive: true });
|
||||
const source = fileURLToPath(new URL("../app/icon.png", import.meta.url));
|
||||
await sharp(source)
|
||||
.resize(512, 512, { kernel: "nearest" })
|
||||
.png()
|
||||
.toFile(fileURLToPath(new URL("icon.png", assets)));
|
||||
const sizes = [16, 24, 32, 48, 64, 128, 256];
|
||||
const images = await Promise.all(
|
||||
sizes.map((size) =>
|
||||
sharp(source).resize(size, size, { kernel: "nearest" }).png().toBuffer(),
|
||||
),
|
||||
);
|
||||
const header = Buffer.alloc(6 + 16 * sizes.length);
|
||||
header.writeUInt16LE(1, 2);
|
||||
header.writeUInt16LE(sizes.length, 4);
|
||||
let offset = header.length;
|
||||
sizes.forEach((size, index) => {
|
||||
const entry = 6 + index * 16;
|
||||
header[entry] = size === 256 ? 0 : size;
|
||||
header[entry + 1] = header[entry];
|
||||
header.writeUInt16LE(1, entry + 4);
|
||||
header.writeUInt16LE(32, entry + 6);
|
||||
header.writeUInt32LE(images[index].length, entry + 8);
|
||||
header.writeUInt32LE(offset, entry + 12);
|
||||
offset += images[index].length;
|
||||
});
|
||||
await writeFile(
|
||||
new URL("icon.ico", assets),
|
||||
Buffer.concat([header, ...images]),
|
||||
);
|
||||
65
desktop/publish.mjs
Normal file
65
desktop/publish.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
import { copyFile, mkdir, readFile, writeFile, stat } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import { fileURLToPath } from "node:url";
|
||||
const require = createRequire(import.meta.url);
|
||||
const asar = require("@electron/asar");
|
||||
const archive = fileURLToPath(
|
||||
new URL("./dist/win-unpacked/resources/app.asar", import.meta.url),
|
||||
);
|
||||
let privateTerms = [];
|
||||
try {
|
||||
const remote = execFileSync("git", ["remote", "get-url", "origin"], {
|
||||
cwd: fileURLToPath(new URL("..", import.meta.url)),
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
windowsHide: true,
|
||||
}).trim();
|
||||
const hostname = remote.includes("://")
|
||||
? new URL(remote).hostname
|
||||
: remote.split("@").pop().split(":")[0];
|
||||
privateTerms = [remote, hostname]
|
||||
.filter(Boolean)
|
||||
.map((term) => term.toLowerCase());
|
||||
} catch {
|
||||
/* Source snapshots need no Git metadata. */
|
||||
}
|
||||
for (const entry of asar.listPackage(archive)) {
|
||||
const name = entry.replace(/^[/\\]+/, "");
|
||||
if (!asar.statFile(archive, name).files) {
|
||||
const content = asar
|
||||
.extractFile(archive, name)
|
||||
.toString("utf8")
|
||||
.toLowerCase();
|
||||
if (privateTerms.some((term) => content.includes(term)))
|
||||
throw new Error(`Private Git reference in desktop archive: ${name}`);
|
||||
}
|
||||
}
|
||||
console.log("Desktop archive privacy scan passed.");
|
||||
const pkg = JSON.parse(
|
||||
await readFile(new URL("./package.json", import.meta.url), "utf8"),
|
||||
);
|
||||
const file = `MineGeo-${pkg.version}-windows-x64.exe`;
|
||||
const source = new URL(`./dist/${file}`, import.meta.url);
|
||||
const directory = new URL("../public/desktop/", import.meta.url);
|
||||
await mkdir(directory, { recursive: true });
|
||||
await copyFile(source, new URL(file, directory));
|
||||
const sha256 = createHash("sha256")
|
||||
.update(await readFile(source))
|
||||
.digest("hex");
|
||||
await writeFile(new URL(`${file}.sha256`, directory), `${sha256} ${file}\n`);
|
||||
await writeFile(
|
||||
new URL("../shared/desktop-release.json", import.meta.url),
|
||||
JSON.stringify(
|
||||
{
|
||||
version: pkg.version,
|
||||
file: `/desktop/${file}`,
|
||||
bytes: (await stat(source)).size,
|
||||
sha256,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
) + "\n",
|
||||
);
|
||||
console.log(`Published ${file} (${sha256})`);
|
||||
Reference in New Issue
Block a user