@pipelab/asset-electron 1.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +110 -0
- package/package.json +18 -0
- package/template/config.cjs +45 -0
- package/template/forge.config.cjs +70 -0
- package/template/package.json +62 -0
- package/template/pipelab-plugin.cjs +160 -0
- package/template/src/custom-main.js +1 -0
- package/template/src/handlers/dialog/folder.js +25 -0
- package/template/src/handlers/dialog/open.js +25 -0
- package/template/src/handlers/dialog/save.js +24 -0
- package/template/src/handlers/discord/set-activity.js +68 -0
- package/template/src/handlers/fs/copy.js +29 -0
- package/template/src/handlers/fs/delete.js +23 -0
- package/template/src/handlers/fs/exist.js +37 -0
- package/template/src/handlers/fs/file-size.js +24 -0
- package/template/src/handlers/fs/folder-create.js +25 -0
- package/template/src/handlers/fs/list.js +61 -0
- package/template/src/handlers/fs/move.js +30 -0
- package/template/src/handlers/fs/read-binary.js +24 -0
- package/template/src/handlers/fs/read.js +42 -0
- package/template/src/handlers/fs/write-base64.js +32 -0
- package/template/src/handlers/fs/write.js +30 -0
- package/template/src/handlers/general/engine.js +17 -0
- package/template/src/handlers/general/exit.js +20 -0
- package/template/src/handlers/general/infos.js +23 -0
- package/template/src/handlers/general/open-in-explorer.js +21 -0
- package/template/src/handlers/general/open.js +21 -0
- package/template/src/handlers/general/run.js +27 -0
- package/template/src/handlers/steam/deleteItem.js +26 -0
- package/template/src/handlers/steam/download.js +26 -0
- package/template/src/handlers/steam/downloadInfo.js +26 -0
- package/template/src/handlers/steam/downloadScore.js +28 -0
- package/template/src/handlers/steam/getItem.js +28 -0
- package/template/src/handlers/steam/getItems.js +28 -0
- package/template/src/handlers/steam/installInfo.js +26 -0
- package/template/src/handlers/steam/raw.js +42 -0
- package/template/src/handlers/steam/saveScreenshot.js +66 -0
- package/template/src/handlers/steam/state.js +26 -0
- package/template/src/handlers/steam/subscribe.js +26 -0
- package/template/src/handlers/steam/unsubscribe.js +26 -0
- package/template/src/handlers/steam/updateItem.js +26 -0
- package/template/src/handlers/steam/uploadScore.js +27 -0
- package/template/src/handlers/steam/utils.js +78 -0
- package/template/src/handlers/user/folder.js +71 -0
- package/template/src/handlers/window/maximize.js +20 -0
- package/template/src/handlers/window/minimize.js +20 -0
- package/template/src/handlers/window/request-attention.js +20 -0
- package/template/src/handlers/window/restore.js +20 -0
- package/template/src/handlers/window/set-always-on-top.js +20 -0
- package/template/src/handlers/window/set-fullscreen.js +26 -0
- package/template/src/handlers/window/set-height.js +21 -0
- package/template/src/handlers/window/set-ignore-mouse-events.js +36 -0
- package/template/src/handlers/window/set-maximum-size.js +20 -0
- package/template/src/handlers/window/set-minimum-size.js +20 -0
- package/template/src/handlers/window/set-resizable.js +20 -0
- package/template/src/handlers/window/set-title.js +20 -0
- package/template/src/handlers/window/set-width.js +21 -0
- package/template/src/handlers/window/set-x.js +21 -0
- package/template/src/handlers/window/set-y.js +21 -0
- package/template/src/handlers/window/show-dev-tools.js +24 -0
- package/template/src/handlers/window/unmaximize.js +20 -0
- package/template/src/index.js +958 -0
- package/template/src/preload.js +27 -0
- package/template/src/utils.js +18 -0
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { app, BrowserWindow, ipcMain, safeStorage, session, shell } from "electron";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { createReadStream } from "node:fs";
|
|
7
|
+
import { stat } from "node:fs/promises";
|
|
8
|
+
import mime from "mime-types";
|
|
9
|
+
import serve from "serve-handler";
|
|
10
|
+
import { createServer } from "http";
|
|
11
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
12
|
+
import "./custom-main.js";
|
|
13
|
+
import mri from "mri";
|
|
14
|
+
import config from "../config.cjs";
|
|
15
|
+
import steamworks from "@pipelab/steamworks.js";
|
|
16
|
+
import DiscordRPC from "discord-rpc";
|
|
17
|
+
|
|
18
|
+
// user
|
|
19
|
+
import userFolder from "./handlers/user/folder.js";
|
|
20
|
+
|
|
21
|
+
// fs
|
|
22
|
+
import fsWrite from "./handlers/fs/write.js";
|
|
23
|
+
import fsWriteBase64 from "./handlers/fs/write-base64.js";
|
|
24
|
+
import fsRead from "./handlers/fs/read.js";
|
|
25
|
+
import fsReadBinary from "./handlers/fs/read-binary.js";
|
|
26
|
+
import fsFolderCreate from "./handlers/fs/folder-create.js";
|
|
27
|
+
import fsList from "./handlers/fs/list.js";
|
|
28
|
+
import fsMove from "./handlers/fs/move.js";
|
|
29
|
+
import fsDelete from "./handlers/fs/delete.js";
|
|
30
|
+
import fsCopy from "./handlers/fs/copy.js";
|
|
31
|
+
import fsFileSize from "./handlers/fs/file-size.js";
|
|
32
|
+
import fsExist from "./handlers/fs/exist.js";
|
|
33
|
+
|
|
34
|
+
// window
|
|
35
|
+
import windowMaximize from "./handlers/window/maximize.js";
|
|
36
|
+
import windowMinimize from "./handlers/window/minimize.js";
|
|
37
|
+
import windowRequestAttention from "./handlers/window/request-attention.js";
|
|
38
|
+
import windowRestore from "./handlers/window/restore.js";
|
|
39
|
+
import windowSetAlwaysOnTop from "./handlers/window/set-always-on-top.js";
|
|
40
|
+
import windowSetHeight from "./handlers/window/set-height.js";
|
|
41
|
+
import windowSetWidth from "./handlers/window/set-width.js";
|
|
42
|
+
import windowSetMinimumSize from "./handlers/window/set-minimum-size.js";
|
|
43
|
+
import windowSetMaximumSize from "./handlers/window/set-maximum-size.js";
|
|
44
|
+
import windowSetResizable from "./handlers/window/set-resizable.js";
|
|
45
|
+
import windowSetTitle from "./handlers/window/set-title.js";
|
|
46
|
+
import windowSetX from "./handlers/window/set-x.js";
|
|
47
|
+
import windowSetY from "./handlers/window/set-y.js";
|
|
48
|
+
import windowShowDevTools from "./handlers/window/show-dev-tools.js";
|
|
49
|
+
import windowUnmaximize from "./handlers/window/unmaximize.js";
|
|
50
|
+
import windowSetFullscreen from "./handlers/window/set-fullscreen.js";
|
|
51
|
+
import windowSetIgnoreMouseEvents from "./handlers/window/set-ignore-mouse-events.js";
|
|
52
|
+
|
|
53
|
+
// dialog
|
|
54
|
+
import dialogFolder from "./handlers/dialog/folder.js";
|
|
55
|
+
import dialogOpen from "./handlers/dialog/open.js";
|
|
56
|
+
import dialogSave from "./handlers/dialog/save.js";
|
|
57
|
+
|
|
58
|
+
// general
|
|
59
|
+
import engine from "./handlers/general/engine.js";
|
|
60
|
+
import run from "./handlers/general/run.js";
|
|
61
|
+
import open from "./handlers/general/open.js";
|
|
62
|
+
import showInExplorer from "./handlers/general/open-in-explorer.js";
|
|
63
|
+
import exit from "./handlers/general/exit.js";
|
|
64
|
+
|
|
65
|
+
// steam raw
|
|
66
|
+
import steamRaw from "./handlers/steam/raw.js";
|
|
67
|
+
import steamUploadScore from "./handlers/steam/uploadScore.js";
|
|
68
|
+
import steamDownloadScore from "./handlers/steam/downloadScore.js";
|
|
69
|
+
import steamUpdateItem from "./handlers/steam/updateItem.js";
|
|
70
|
+
import steamGetItem from "./handlers/steam/getItem.js";
|
|
71
|
+
import steamGetItems from "./handlers/steam/getItems.js";
|
|
72
|
+
import steamSubscribe from "./handlers/steam/subscribe.js";
|
|
73
|
+
import steamUnsubscribe from "./handlers/steam/unsubscribe.js";
|
|
74
|
+
import steamState from "./handlers/steam/state.js";
|
|
75
|
+
import steamInstallInfo from "./handlers/steam/installInfo.js";
|
|
76
|
+
import steamDownloadInfo from "./handlers/steam/downloadInfo.js";
|
|
77
|
+
import steamDownload from "./handlers/steam/download.js";
|
|
78
|
+
import steamDeleteItem from "./handlers/steam/deleteItem.js";
|
|
79
|
+
import steamSaveScreenshot from "./handlers/steam/saveScreenshot.js";
|
|
80
|
+
|
|
81
|
+
// discord set activity
|
|
82
|
+
import discordSetActivity from "./handlers/discord/set-activity.js";
|
|
83
|
+
|
|
84
|
+
import { getAppName } from "./utils.js";
|
|
85
|
+
|
|
86
|
+
import infos from "./handlers/general/infos.js";
|
|
87
|
+
|
|
88
|
+
import semver from "semver";
|
|
89
|
+
import { protocol } from "electron/main";
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Assert switch is exhaustive
|
|
93
|
+
* @param {never} _x
|
|
94
|
+
* @returns {never}
|
|
95
|
+
*/
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
97
|
+
function assertUnreachable(_x) {
|
|
98
|
+
throw new Error("Didn't expect to get here");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
safeStorage.setUsePlainTextEncryption(true);
|
|
102
|
+
|
|
103
|
+
protocol.registerSchemesAsPrivileged([
|
|
104
|
+
{
|
|
105
|
+
scheme: "app",
|
|
106
|
+
privileges: {
|
|
107
|
+
standard: true, // Permet de résoudre les origines correctement (corrige l'erreur 'null')
|
|
108
|
+
secure: true, // Traité comme une origine sécurisée (nécessaire pour Service Workers/Web Workers)
|
|
109
|
+
supportFetchAPI: true, // Active l'API Fetch pour ce protocole
|
|
110
|
+
corsEnabled: true, // Autorise les requêtes CORS si nécessaire
|
|
111
|
+
allowServiceWorkers: true, // Souvent requis par Construct 3
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
function createErrorWindow(errorMessage) {
|
|
117
|
+
const errorWindow = new BrowserWindow({
|
|
118
|
+
width: 600,
|
|
119
|
+
height: 250,
|
|
120
|
+
title: "Game Startup Error",
|
|
121
|
+
center: true,
|
|
122
|
+
resizable: false,
|
|
123
|
+
webPreferences: {
|
|
124
|
+
preload: join(metaDirname, "preload.js"),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const htmlContent = `
|
|
129
|
+
<!DOCTYPE html>
|
|
130
|
+
<html>
|
|
131
|
+
<head>
|
|
132
|
+
<title>Error</title>
|
|
133
|
+
<style>
|
|
134
|
+
/* Basic reset and modern styling */
|
|
135
|
+
html, body {
|
|
136
|
+
margin: 0;
|
|
137
|
+
padding: 0;
|
|
138
|
+
height: 100%;
|
|
139
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
140
|
+
background-color: #2c2f33; /* Dark background */
|
|
141
|
+
color: #ffffff; /* White text */
|
|
142
|
+
overflow: hidden;
|
|
143
|
+
}
|
|
144
|
+
/* Flexbox container to center content */
|
|
145
|
+
body {
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
justify-content: center; /* Center horizontally */
|
|
149
|
+
align-items: center; /* Center vertically */
|
|
150
|
+
text-align: center;
|
|
151
|
+
padding: 1rem;
|
|
152
|
+
gap: 1.5rem;
|
|
153
|
+
}
|
|
154
|
+
h1 {
|
|
155
|
+
font-weight: 400; /* Lighter font weight */
|
|
156
|
+
font-size: 1.2rem;
|
|
157
|
+
line-height: 1.5;
|
|
158
|
+
margin: 0;
|
|
159
|
+
}
|
|
160
|
+
.button-container {
|
|
161
|
+
margin-top: 0.5rem;
|
|
162
|
+
}
|
|
163
|
+
button {
|
|
164
|
+
background-color: #404448; /* Slightly lighter dark background */
|
|
165
|
+
color: #ffffff;
|
|
166
|
+
border: 1px solid #55585c;
|
|
167
|
+
border-radius: 6px;
|
|
168
|
+
padding: 0.75rem 1.5rem;
|
|
169
|
+
font-family: inherit;
|
|
170
|
+
font-size: 0.9rem;
|
|
171
|
+
font-weight: 500;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
transition: background-color 0.2s ease, border-color 0.2s ease;
|
|
174
|
+
}
|
|
175
|
+
button:hover {
|
|
176
|
+
background-color: #4a4e52;
|
|
177
|
+
border-color: #60646a;
|
|
178
|
+
}
|
|
179
|
+
button:active {
|
|
180
|
+
background-color: #363a3e;
|
|
181
|
+
transform: translateY(1px);
|
|
182
|
+
}
|
|
183
|
+
button:focus {
|
|
184
|
+
outline: none;
|
|
185
|
+
box-shadow: 0 0 0 2px rgba(85, 88, 92, 0.5);
|
|
186
|
+
}
|
|
187
|
+
</style>
|
|
188
|
+
</head>
|
|
189
|
+
<body>
|
|
190
|
+
<h1>${errorMessage}</h1>
|
|
191
|
+
<div class="button-container">
|
|
192
|
+
<button onclick="exitApplication()">Close</button>
|
|
193
|
+
</div>
|
|
194
|
+
<script>
|
|
195
|
+
function exitApplication() {
|
|
196
|
+
window.electronAPI.exit()
|
|
197
|
+
}
|
|
198
|
+
</script>
|
|
199
|
+
</body>
|
|
200
|
+
</html>
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
// Encode the HTML and load it as a data URL
|
|
204
|
+
const encodedHtml = encodeURIComponent(htmlContent);
|
|
205
|
+
errorWindow.loadURL(`data:text/html,${encodedHtml}`);
|
|
206
|
+
return errorWindow;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
process.on("uncaughtException", async (error) => {
|
|
210
|
+
console.error("Uncaught Exception:", error, error.code);
|
|
211
|
+
|
|
212
|
+
if (error.code === "EADDRINUSE") {
|
|
213
|
+
const errorWindow = await createErrorWindow(
|
|
214
|
+
"Unable to start the game.<br>Is another instance already running?",
|
|
215
|
+
);
|
|
216
|
+
errorWindow.show();
|
|
217
|
+
errorWindow.on("closed", () => {
|
|
218
|
+
app.quit();
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
process.on("unhandledRejection", (reason) => {
|
|
223
|
+
console.error("Unhandled Promise Rejection:", reason);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
console.log("process.argv", process.argv);
|
|
227
|
+
const argv = process.argv;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @typedef {Object} Args
|
|
231
|
+
*
|
|
232
|
+
* @property {string} url
|
|
233
|
+
* @property {boolean} no-window
|
|
234
|
+
*/
|
|
235
|
+
|
|
236
|
+
/** @type {mri.Argv<Args>} */
|
|
237
|
+
const cliArgs = mri(argv, {
|
|
238
|
+
alias: {
|
|
239
|
+
u: "url",
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
/** @type {undefined | Object} */
|
|
244
|
+
let rpc;
|
|
245
|
+
if (config.enableDiscordSupport) {
|
|
246
|
+
DiscordRPC.register(config.discordAppId);
|
|
247
|
+
rpc = new DiscordRPC.Client({ transport: "ipc" });
|
|
248
|
+
console.log("rpc", rpc);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
//region commandLine Flags
|
|
252
|
+
if (config.enableInProcessGPU) {
|
|
253
|
+
app.commandLine.appendSwitch("in-process-gpu");
|
|
254
|
+
}
|
|
255
|
+
if (config.enableDisableRendererBackgrounding) {
|
|
256
|
+
app.commandLine.appendSwitch("disable-renderer-backgrounding");
|
|
257
|
+
}
|
|
258
|
+
if (config.forceHighPerformanceGpu) {
|
|
259
|
+
app.commandLine.appendSwitch("force-high-performance-gpu");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Fix steam remote play together
|
|
263
|
+
app.commandLine.appendSwitch("disable-features", "AudioServiceOutOfProcess");
|
|
264
|
+
|
|
265
|
+
app.commandLine.appendSwitch("force-high-performance-gpu");
|
|
266
|
+
app.commandLine.appendSwitch("disable-gpu-sandbox");
|
|
267
|
+
//endregion
|
|
268
|
+
|
|
269
|
+
const hasElectronVersion = config.electronVersion !== undefined && config.electronVersion !== "";
|
|
270
|
+
const isCJSOnly =
|
|
271
|
+
hasElectronVersion && semver.lt(semver.coerce(config.electronVersion) || "0.0.0", "28.0.0");
|
|
272
|
+
|
|
273
|
+
//region Steam
|
|
274
|
+
|
|
275
|
+
/** @type {Omit<import('@pipelab/steamworks.js').Client, "init" | "runCallbacks">} */
|
|
276
|
+
let client;
|
|
277
|
+
console.log("config.enableSteamSupport", config.enableSteamSupport);
|
|
278
|
+
if (config.enableSteamSupport) {
|
|
279
|
+
app.commandLine.appendSwitch("in-process-gpu");
|
|
280
|
+
//app.commandLine.appendSwitch('disable-direct-composition')
|
|
281
|
+
app.commandLine.appendSwitch("no-sandbox");
|
|
282
|
+
|
|
283
|
+
// const isNecessary = steamworks.restartAppIfNecessary(config.steamGameId)
|
|
284
|
+
// console.log('isNecessary', isNecessary)
|
|
285
|
+
|
|
286
|
+
// if (isNecessary) {
|
|
287
|
+
|
|
288
|
+
// }
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
client = steamworks.init(config.steamGameId);
|
|
292
|
+
console.log(client.localplayer.getName());
|
|
293
|
+
client.callback.register(client.callback.SteamCallback.GameOverlayActivated, (data) => {
|
|
294
|
+
/**
|
|
295
|
+
* @type {import('@pipelab/core').MakeInputOutput<import('@pipelab/core').FullscreenState, 'input'>}
|
|
296
|
+
*/
|
|
297
|
+
const order = {
|
|
298
|
+
url: "/steam/overlay-activated",
|
|
299
|
+
body: data,
|
|
300
|
+
};
|
|
301
|
+
broadcastMessage(JSON.stringify(order));
|
|
302
|
+
});
|
|
303
|
+
} catch (e) {
|
|
304
|
+
console.error("e", e);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
//endregion
|
|
308
|
+
|
|
309
|
+
const userData = app.getPath("userData");
|
|
310
|
+
const userDataDirname = dirname(userData);
|
|
311
|
+
const appNameFolder = getAppName(config);
|
|
312
|
+
const sessionDataPath = join(userDataDirname, `cache_${appNameFolder}`);
|
|
313
|
+
console.log("sessionDataPath", sessionDataPath);
|
|
314
|
+
app.setPath("userData", sessionDataPath);
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* @type {Set<import('ws').WebSocket>}
|
|
318
|
+
*/
|
|
319
|
+
const clients = new Set();
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* @type {import('http').Server | null}
|
|
323
|
+
*/
|
|
324
|
+
let httpServer = null;
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @type {import('ws').WebSocketServer | null}
|
|
328
|
+
*/
|
|
329
|
+
let wss = null;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @type {boolean}
|
|
333
|
+
*/
|
|
334
|
+
let isQuitting = false;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @type {Promise<void> | null}
|
|
338
|
+
*/
|
|
339
|
+
let cleanupPromise = null;
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @param {string} message
|
|
343
|
+
*/
|
|
344
|
+
const broadcastMessage = (message) => {
|
|
345
|
+
for (const client of clients) {
|
|
346
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
347
|
+
client.send(message);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
const metaDirname = isCJSOnly ? __dirname : import.meta.dirname;
|
|
353
|
+
const dir = app.isPackaged ? join(metaDirname, "./app") : "./src/app";
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @param {BrowserWindow} mainWindow
|
|
357
|
+
* @returns {Promise<number>}
|
|
358
|
+
*/
|
|
359
|
+
const createAppServer = (mainWindow, serveStatic = true) => {
|
|
360
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
361
|
+
return new Promise(async (resolve, reject) => {
|
|
362
|
+
httpServer = createServer();
|
|
363
|
+
const server = httpServer;
|
|
364
|
+
|
|
365
|
+
if (serveStatic) {
|
|
366
|
+
server.on("request", (req, res) => {
|
|
367
|
+
return serve(req, res, {
|
|
368
|
+
headers: [
|
|
369
|
+
{
|
|
370
|
+
source: "**/*",
|
|
371
|
+
headers: [
|
|
372
|
+
{
|
|
373
|
+
key: "Cache-Control",
|
|
374
|
+
value: "no-cache",
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
key: "Access-Control-Allow-Origin",
|
|
378
|
+
value: "*",
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
public: dir,
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
} else {
|
|
387
|
+
server.on("request", (req, res) => {
|
|
388
|
+
res.setHeader("Access-Control-Allow-Origin", "*"); // or 'https://preview.construct.net'
|
|
389
|
+
res.setHeader("Access-Control-Allow-Private-Network", "true");
|
|
390
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
391
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
392
|
+
if (req.method === "OPTIONS") {
|
|
393
|
+
res.writeHead(204);
|
|
394
|
+
res.end();
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
398
|
+
res.end("Pipelab Electron App Server\n");
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
wss = new WebSocketServer({ server });
|
|
404
|
+
wss.on("connection", function connection(ws) {
|
|
405
|
+
clients.add(ws);
|
|
406
|
+
|
|
407
|
+
ws.on("error", console.error);
|
|
408
|
+
|
|
409
|
+
ws.on("message", async (data) => {
|
|
410
|
+
//region Message handlers
|
|
411
|
+
/** @type {import('@pipelab/core').Message} */
|
|
412
|
+
const json = JSON.parse(data.toString());
|
|
413
|
+
console.log("received:", json);
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
switch (json.url) {
|
|
417
|
+
case "/paths":
|
|
418
|
+
await userFolder(json, ws, config);
|
|
419
|
+
break;
|
|
420
|
+
|
|
421
|
+
case "/fs/file/write":
|
|
422
|
+
await fsWrite(json, ws);
|
|
423
|
+
break;
|
|
424
|
+
|
|
425
|
+
case "/fs/file/write-base64":
|
|
426
|
+
await fsWriteBase64(json, ws);
|
|
427
|
+
break;
|
|
428
|
+
|
|
429
|
+
case "/fs/file/read":
|
|
430
|
+
await fsRead(json, ws);
|
|
431
|
+
break;
|
|
432
|
+
|
|
433
|
+
case "/fs/file/read/binary":
|
|
434
|
+
await fsReadBinary(json, ws);
|
|
435
|
+
break;
|
|
436
|
+
|
|
437
|
+
case "/fs/folder/create":
|
|
438
|
+
await fsFolderCreate(json, ws);
|
|
439
|
+
break;
|
|
440
|
+
|
|
441
|
+
case "/window/maximize":
|
|
442
|
+
await windowMaximize(json, ws, mainWindow);
|
|
443
|
+
break;
|
|
444
|
+
|
|
445
|
+
case "/window/minimize":
|
|
446
|
+
await windowMinimize(json, ws, mainWindow);
|
|
447
|
+
break;
|
|
448
|
+
case "/window/request-attention":
|
|
449
|
+
await windowRequestAttention(json, ws, mainWindow);
|
|
450
|
+
break;
|
|
451
|
+
case "/window/restore":
|
|
452
|
+
await windowRestore(json, ws, mainWindow);
|
|
453
|
+
break;
|
|
454
|
+
case "/dialog/folder":
|
|
455
|
+
await dialogFolder(json, ws);
|
|
456
|
+
break;
|
|
457
|
+
case "/dialog/open":
|
|
458
|
+
await dialogOpen(json, ws);
|
|
459
|
+
break;
|
|
460
|
+
case "/dialog/save":
|
|
461
|
+
await dialogSave(json, ws);
|
|
462
|
+
break;
|
|
463
|
+
case "/window/set-always-on-top":
|
|
464
|
+
await windowSetAlwaysOnTop(json, ws, mainWindow);
|
|
465
|
+
break;
|
|
466
|
+
case "/window/set-height":
|
|
467
|
+
await windowSetHeight(json, ws, mainWindow);
|
|
468
|
+
break;
|
|
469
|
+
case "/window/set-maximum-size":
|
|
470
|
+
await windowSetMaximumSize(json, ws, mainWindow);
|
|
471
|
+
break;
|
|
472
|
+
case "/window/set-minimum-size":
|
|
473
|
+
await windowSetMinimumSize(json, ws, mainWindow);
|
|
474
|
+
break;
|
|
475
|
+
case "/window/set-resizable":
|
|
476
|
+
await windowSetResizable(json, ws, mainWindow);
|
|
477
|
+
break;
|
|
478
|
+
case "/window/set-title":
|
|
479
|
+
await windowSetTitle(json, ws, mainWindow);
|
|
480
|
+
break;
|
|
481
|
+
case "/window/set-width":
|
|
482
|
+
await windowSetWidth(json, ws, mainWindow);
|
|
483
|
+
break;
|
|
484
|
+
case "/window/set-x":
|
|
485
|
+
await windowSetX(json, ws, mainWindow);
|
|
486
|
+
break;
|
|
487
|
+
case "/window/set-y":
|
|
488
|
+
await windowSetY(json, ws, mainWindow);
|
|
489
|
+
break;
|
|
490
|
+
case "/window/show-dev-tools":
|
|
491
|
+
await windowShowDevTools(json, ws, mainWindow);
|
|
492
|
+
break;
|
|
493
|
+
case "/window/unmaximize":
|
|
494
|
+
await windowUnmaximize(json, ws, mainWindow);
|
|
495
|
+
break;
|
|
496
|
+
case "/window/set-fullscreen":
|
|
497
|
+
await windowSetFullscreen(json, ws, mainWindow);
|
|
498
|
+
break;
|
|
499
|
+
case "/window/set-ignore-mouse-events":
|
|
500
|
+
await windowSetIgnoreMouseEvents(json, ws, mainWindow);
|
|
501
|
+
break;
|
|
502
|
+
case "/engine":
|
|
503
|
+
await engine(json, ws);
|
|
504
|
+
break;
|
|
505
|
+
case "/open":
|
|
506
|
+
await open(json, ws);
|
|
507
|
+
break;
|
|
508
|
+
case "/show-in-explorer":
|
|
509
|
+
await showInExplorer(json, ws);
|
|
510
|
+
break;
|
|
511
|
+
case "/run":
|
|
512
|
+
await run(json, ws);
|
|
513
|
+
break;
|
|
514
|
+
case "/fs/copy":
|
|
515
|
+
await fsCopy(json, ws);
|
|
516
|
+
break;
|
|
517
|
+
case "/fs/delete":
|
|
518
|
+
await fsDelete(json, ws);
|
|
519
|
+
break;
|
|
520
|
+
case "/fs/exist":
|
|
521
|
+
await fsExist(json, ws);
|
|
522
|
+
break;
|
|
523
|
+
case "/fs/list":
|
|
524
|
+
await fsList(json, ws);
|
|
525
|
+
break;
|
|
526
|
+
case "/fs/file/size":
|
|
527
|
+
await fsFileSize(json, ws);
|
|
528
|
+
break;
|
|
529
|
+
case "/fs/move":
|
|
530
|
+
await fsMove(json, ws);
|
|
531
|
+
break;
|
|
532
|
+
case "/steam/raw":
|
|
533
|
+
await steamRaw(json, ws, client);
|
|
534
|
+
break;
|
|
535
|
+
case "/steam/leaderboard/upload-score":
|
|
536
|
+
await steamUploadScore(json, ws, client);
|
|
537
|
+
break;
|
|
538
|
+
case "/steam/leaderboard/download-score":
|
|
539
|
+
await steamDownloadScore(json, ws, client);
|
|
540
|
+
break;
|
|
541
|
+
case "/steam/workshop/update-item":
|
|
542
|
+
await steamUpdateItem(json, ws, client);
|
|
543
|
+
break;
|
|
544
|
+
case "/steam/workshop/get-item":
|
|
545
|
+
await steamGetItem(json, ws, client);
|
|
546
|
+
break;
|
|
547
|
+
case "/steam/workshop/get-items":
|
|
548
|
+
await steamGetItems(json, ws, client);
|
|
549
|
+
break;
|
|
550
|
+
case "/steam/workshop/subscribe":
|
|
551
|
+
await steamSubscribe(json, ws, client);
|
|
552
|
+
break;
|
|
553
|
+
case "/steam/workshop/unsubscribe":
|
|
554
|
+
await steamUnsubscribe(json, ws, client);
|
|
555
|
+
break;
|
|
556
|
+
case "/steam/workshop/state":
|
|
557
|
+
await steamState(json, ws, client);
|
|
558
|
+
break;
|
|
559
|
+
case "/steam/workshop/install-info":
|
|
560
|
+
await steamInstallInfo(json, ws, client);
|
|
561
|
+
break;
|
|
562
|
+
case "/steam/workshop/download-info":
|
|
563
|
+
await steamDownloadInfo(json, ws, client);
|
|
564
|
+
break;
|
|
565
|
+
case "/steam/workshop/download":
|
|
566
|
+
await steamDownload(json, ws, client);
|
|
567
|
+
break;
|
|
568
|
+
case "/steam/workshop/delete-item":
|
|
569
|
+
await steamDeleteItem(json, ws, client);
|
|
570
|
+
break;
|
|
571
|
+
case "/steam/screenshots/save":
|
|
572
|
+
await steamSaveScreenshot(json, ws, client);
|
|
573
|
+
break;
|
|
574
|
+
case "/discord/set-activity":
|
|
575
|
+
await discordSetActivity(json, ws, mainWindow, rpc);
|
|
576
|
+
break;
|
|
577
|
+
case "/exit":
|
|
578
|
+
await exit(json, ws);
|
|
579
|
+
break;
|
|
580
|
+
case "/window/fullscreen-state":
|
|
581
|
+
// sent the other way around
|
|
582
|
+
break;
|
|
583
|
+
case "/infos":
|
|
584
|
+
await infos(json, ws);
|
|
585
|
+
break;
|
|
586
|
+
|
|
587
|
+
default:
|
|
588
|
+
console.log("unsupported", data);
|
|
589
|
+
assertUnreachable(json);
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
} catch (e) {
|
|
593
|
+
console.error("e", e);
|
|
594
|
+
ws.send(
|
|
595
|
+
JSON.stringify({
|
|
596
|
+
url: json.url,
|
|
597
|
+
correlationId: json.correlationId,
|
|
598
|
+
body: {
|
|
599
|
+
success: false,
|
|
600
|
+
error: e.message,
|
|
601
|
+
},
|
|
602
|
+
}),
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
ws.on("close", () => {
|
|
608
|
+
clients.delete(ws);
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
} catch (e) {
|
|
612
|
+
console.error("Unable to create websocket server", e);
|
|
613
|
+
return reject(e);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
server.listen(31753, "127.0.0.1", () => {
|
|
617
|
+
const adress = server.address();
|
|
618
|
+
if (adress && typeof adress !== "string") {
|
|
619
|
+
return resolve(adress.port);
|
|
620
|
+
}
|
|
621
|
+
return reject("Unable to bind server: adress is not an object");
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const createWindow = async () => {
|
|
627
|
+
const argUrl = cliArgs.url;
|
|
628
|
+
|
|
629
|
+
// If there is a window
|
|
630
|
+
const mainWindow = new BrowserWindow({
|
|
631
|
+
width: config.width,
|
|
632
|
+
height: config.height,
|
|
633
|
+
fullscreen: config.fullscreen,
|
|
634
|
+
fullscreenable: true,
|
|
635
|
+
frame: config.frame,
|
|
636
|
+
transparent: config.transparent,
|
|
637
|
+
alwaysOnTop: config.alwaysOnTop,
|
|
638
|
+
backgroundColor: config.backgroundColor,
|
|
639
|
+
icon: config.icon,
|
|
640
|
+
webPreferences: {
|
|
641
|
+
preload: join(metaDirname, "preload.js"),
|
|
642
|
+
},
|
|
643
|
+
show: true,
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
if (!mainWindow) {
|
|
647
|
+
throw new Error("Unable to create window");
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (!config.toolbar) {
|
|
651
|
+
mainWindow.setMenu(null);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (config.openDevtoolsOnStart) {
|
|
655
|
+
mainWindow.webContents.openDevTools();
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
if (config.clearServiceWorkerOnBoot) {
|
|
659
|
+
// It's better to export apps without service worker support
|
|
660
|
+
// only use when needed
|
|
661
|
+
try {
|
|
662
|
+
// Clear service workers to prevent old versions of the app
|
|
663
|
+
await session.defaultSession.clearStorageData({
|
|
664
|
+
storages: ["serviceworkers"],
|
|
665
|
+
});
|
|
666
|
+
} catch (e) {
|
|
667
|
+
console.error("Error clearing service workers", e);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// preview
|
|
672
|
+
if (argUrl) {
|
|
673
|
+
console.log("argUrl", argUrl);
|
|
674
|
+
await createAppServer(mainWindow, false);
|
|
675
|
+
|
|
676
|
+
await mainWindow?.loadURL(argUrl);
|
|
677
|
+
console.log("URL loaded");
|
|
678
|
+
}
|
|
679
|
+
// prod
|
|
680
|
+
else {
|
|
681
|
+
const isCustomProtocol = config.serverMode === "customProtocol";
|
|
682
|
+
const port = await createAppServer(mainWindow, isCustomProtocol === false);
|
|
683
|
+
if (isCustomProtocol) {
|
|
684
|
+
await mainWindow?.loadURL("app://game/index.html");
|
|
685
|
+
console.log("URL loaded (static)", "app://game/index.html");
|
|
686
|
+
} else {
|
|
687
|
+
const url = `http://127.0.0.1:${port}`;
|
|
688
|
+
await mainWindow?.loadURL(url);
|
|
689
|
+
console.log("URL loaded", url);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
mainWindow.on("enter-full-screen", () => {
|
|
694
|
+
/**
|
|
695
|
+
* @type {import('@pipelab/core').MakeInputOutput<import('@pipelab/core').FullscreenState, 'input'>}
|
|
696
|
+
*/
|
|
697
|
+
const order = {
|
|
698
|
+
url: "/window/fullscreen-state",
|
|
699
|
+
body: {
|
|
700
|
+
state: "fullscreen",
|
|
701
|
+
},
|
|
702
|
+
};
|
|
703
|
+
broadcastMessage(JSON.stringify(order));
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
mainWindow.on("leave-full-screen", () => {
|
|
707
|
+
/**
|
|
708
|
+
* @type {import('@pipelab/core').MakeInputOutput<import('@pipelab/core').FullscreenState, 'input'>}
|
|
709
|
+
*/
|
|
710
|
+
const order = {
|
|
711
|
+
url: "/window/fullscreen-state",
|
|
712
|
+
body: {
|
|
713
|
+
state: "normal",
|
|
714
|
+
},
|
|
715
|
+
};
|
|
716
|
+
broadcastMessage(JSON.stringify(order));
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
mainWindow.webContents?.setWindowOpenHandler(({ url }) => {
|
|
720
|
+
// Open the URL in the default browser
|
|
721
|
+
shell.openExternal(url);
|
|
722
|
+
return { action: "deny" }; // Prevent Electron from creating a new window
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
return mainWindow;
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Cleanup all resources before quitting
|
|
730
|
+
*/
|
|
731
|
+
const cleanup = async () => {
|
|
732
|
+
if (cleanupPromise) {
|
|
733
|
+
return cleanupPromise;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
cleanupPromise = (async () => {
|
|
737
|
+
console.log("Cleaning up resources...");
|
|
738
|
+
|
|
739
|
+
for (const client of clients) {
|
|
740
|
+
try {
|
|
741
|
+
client.close();
|
|
742
|
+
} catch (e) {
|
|
743
|
+
console.error("Error closing WebSocket client:", e);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
clients.clear();
|
|
747
|
+
|
|
748
|
+
if (wss) {
|
|
749
|
+
try {
|
|
750
|
+
wss.close();
|
|
751
|
+
} catch (e) {
|
|
752
|
+
console.error("Error closing WebSocket server:", e);
|
|
753
|
+
}
|
|
754
|
+
wss = null;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (rpc) {
|
|
758
|
+
try {
|
|
759
|
+
await rpc.clearActivity();
|
|
760
|
+
} catch (e) {
|
|
761
|
+
console.error("Error clearing Discord RPC activity:", e);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
try {
|
|
765
|
+
rpc.destroy();
|
|
766
|
+
} catch (e) {
|
|
767
|
+
console.error("Error destroying Discord RPC client:", e);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
rpc = undefined;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
try {
|
|
774
|
+
protocol.unhandle("app");
|
|
775
|
+
} catch (e) {
|
|
776
|
+
console.error("Error unregistering app protocol:", e);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (httpServer) {
|
|
780
|
+
const server = httpServer;
|
|
781
|
+
httpServer = null;
|
|
782
|
+
|
|
783
|
+
await new Promise((resolve) => {
|
|
784
|
+
let isSettled = false;
|
|
785
|
+
const done = () => {
|
|
786
|
+
if (!isSettled) {
|
|
787
|
+
isSettled = true;
|
|
788
|
+
resolve();
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
server.close(() => {
|
|
793
|
+
console.log("HTTP server closed");
|
|
794
|
+
done();
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
setTimeout(done, 500);
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
})().finally(() => {
|
|
801
|
+
cleanupPromise = null;
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
return cleanupPromise;
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
const registerHandlers = async () => {
|
|
808
|
+
ipcMain.on("exit", async (event, code) => {
|
|
809
|
+
console.log("exit", code);
|
|
810
|
+
isQuitting = true;
|
|
811
|
+
await cleanup();
|
|
812
|
+
app.exit(code);
|
|
813
|
+
});
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const rpcLogin = async () => {
|
|
817
|
+
return new Promise((resolve, reject) => {
|
|
818
|
+
rpc.on("ready", () => {
|
|
819
|
+
console.log("rpc ready");
|
|
820
|
+
return resolve(rpc);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
rpc.login({ clientId: config.discordAppId }).catch((e) => {
|
|
824
|
+
return reject(e);
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
};
|
|
828
|
+
|
|
829
|
+
function registerCustomProtocol() {
|
|
830
|
+
const GAME_DIR = path.join(metaDirname, "app");
|
|
831
|
+
|
|
832
|
+
protocol.handle("app", async (request) => {
|
|
833
|
+
let internalPath = request.url.replace("app://game/", "");
|
|
834
|
+
internalPath = internalPath.split("?")[0];
|
|
835
|
+
internalPath = decodeURIComponent(internalPath);
|
|
836
|
+
|
|
837
|
+
if (internalPath === "" || internalPath.endsWith("/")) {
|
|
838
|
+
internalPath = "index.html";
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const finalPath = path.join(GAME_DIR, internalPath);
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
const stats = await stat(finalPath);
|
|
845
|
+
const mimeType = mime.lookup(finalPath) || "application/octet-stream";
|
|
846
|
+
|
|
847
|
+
const headers = new Headers();
|
|
848
|
+
headers.set("Content-Type", mimeType);
|
|
849
|
+
headers.set("Access-Control-Allow-Origin", "*");
|
|
850
|
+
headers.set("Accept-Ranges", "bytes");
|
|
851
|
+
|
|
852
|
+
const rangeHeader = request.headers.get("Range");
|
|
853
|
+
|
|
854
|
+
if (rangeHeader) {
|
|
855
|
+
const parts = rangeHeader.replace(/bytes=/, "").split("-");
|
|
856
|
+
const start = parseInt(parts[0], 10);
|
|
857
|
+
const end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1;
|
|
858
|
+
const chunkSize = end - start + 1;
|
|
859
|
+
|
|
860
|
+
const stream = createReadStream(finalPath, { start, end });
|
|
861
|
+
|
|
862
|
+
headers.set("Content-Range", `bytes ${start}-${end}/${stats.size}`);
|
|
863
|
+
headers.set("Content-Length", String(chunkSize));
|
|
864
|
+
|
|
865
|
+
// @ts-expect-error streams are accepted
|
|
866
|
+
return new Response(stream, {
|
|
867
|
+
status: 206,
|
|
868
|
+
statusText: "Partial Content",
|
|
869
|
+
headers: headers,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
// no range / streaming
|
|
873
|
+
else {
|
|
874
|
+
headers.set("Content-Length", String(stats.size));
|
|
875
|
+
const stream = createReadStream(finalPath);
|
|
876
|
+
|
|
877
|
+
// @ts-expect-error streams are accepted
|
|
878
|
+
return new Response(stream, {
|
|
879
|
+
status: 200,
|
|
880
|
+
headers: headers,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
} catch (error) {
|
|
884
|
+
console.error(`[App Protocol] Error serving ${finalPath}:`, error);
|
|
885
|
+
return new Response("Not Found", { status: 404 });
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
app.whenReady().then(async () => {
|
|
891
|
+
await registerHandlers();
|
|
892
|
+
|
|
893
|
+
if (config.enableSteamSupport) {
|
|
894
|
+
console.log("Enabling steam overlay support");
|
|
895
|
+
try {
|
|
896
|
+
steamworks.electronEnableSteamOverlay();
|
|
897
|
+
} catch (e) {
|
|
898
|
+
console.error("e", e);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
registerCustomProtocol();
|
|
903
|
+
|
|
904
|
+
const mainWindow = await createWindow();
|
|
905
|
+
|
|
906
|
+
mainWindow.show();
|
|
907
|
+
|
|
908
|
+
if (config.enableDiscordSupport && rpc) {
|
|
909
|
+
try {
|
|
910
|
+
await rpcLogin();
|
|
911
|
+
} catch (e) {
|
|
912
|
+
console.error("e", e);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
app.on("activate", async () => {
|
|
917
|
+
if (BrowserWindow.getAllWindows().length === 0) {
|
|
918
|
+
await createWindow();
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
app.on("before-quit", (event) => {
|
|
924
|
+
if (!isQuitting) {
|
|
925
|
+
event.preventDefault();
|
|
926
|
+
isQuitting = true;
|
|
927
|
+
cleanup()
|
|
928
|
+
.catch((error) => {
|
|
929
|
+
console.error("Error while quitting application:", error);
|
|
930
|
+
})
|
|
931
|
+
.finally(() => {
|
|
932
|
+
app.quit();
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
app.on("will-quit", () => {
|
|
938
|
+
for (const client of clients) {
|
|
939
|
+
try {
|
|
940
|
+
client.terminate();
|
|
941
|
+
} catch (e) {}
|
|
942
|
+
}
|
|
943
|
+
clients.clear();
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
app.on("window-all-closed", () => {
|
|
947
|
+
// On macOS, apps typically stay open until explicitly quit
|
|
948
|
+
// But for games, we usually want to quit when the window is closed
|
|
949
|
+
if (process.platform !== "darwin" || isQuitting) {
|
|
950
|
+
app.quit();
|
|
951
|
+
} else {
|
|
952
|
+
// On macOS, trigger the quit process which will run cleanup
|
|
953
|
+
isQuitting = true;
|
|
954
|
+
cleanup().then(() => {
|
|
955
|
+
app.quit();
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
});
|