@next2d/builder 0.4.6 → 0.4.8
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/dist/electron-config.d.ts +10 -0
- package/dist/electron-config.js +28 -2
- package/docs/steam.md +68 -4
- package/package.json +2 -2
- package/templates/electron/index.js +148 -34
- package/templates/electron/preload.cjs +21 -0
|
@@ -5,6 +5,15 @@ export interface IElectronOverlayConfig {
|
|
|
5
5
|
anchor: "bottom";
|
|
6
6
|
clickThrough: boolean;
|
|
7
7
|
}
|
|
8
|
+
export type ElectronMiniAnchor = "bottom-right" | "bottom-left" | "top-right" | "top-left";
|
|
9
|
+
/** A small always-on-top window the game can switch to and back from at run time (idle/companion modes). */
|
|
10
|
+
export interface IElectronMiniConfig {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
anchor: ElectronMiniAnchor;
|
|
14
|
+
margin: number;
|
|
15
|
+
alwaysOnTop: boolean;
|
|
16
|
+
}
|
|
8
17
|
export interface IElectronConfig {
|
|
9
18
|
nativeBridge?: INativeBridgeConfig;
|
|
10
19
|
appId: string;
|
|
@@ -19,6 +28,7 @@ export interface IElectronConfig {
|
|
|
19
28
|
height: number;
|
|
20
29
|
fullscreen: boolean;
|
|
21
30
|
overlay?: IElectronOverlayConfig;
|
|
31
|
+
mini?: IElectronMiniConfig;
|
|
22
32
|
};
|
|
23
33
|
steam?: {
|
|
24
34
|
appId: string | null;
|
package/dist/electron-config.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { getTemplateDir } from "./utils.js";
|
|
4
4
|
import { resolveDescription } from "./description.js";
|
|
5
5
|
import { validateNativeBridge } from "./electron-native.js";
|
|
6
|
+
const MINI_ANCHORS = ["bottom-right", "bottom-left", "top-right", "top-left"];
|
|
6
7
|
export const validateSteamId = (value) => {
|
|
7
8
|
const id = String(value);
|
|
8
9
|
if (!/^[1-9][0-9]*$/.test(id) || !Number.isSafeInteger(Number(id)) || Number(id) > 4294967295) {
|
|
@@ -74,6 +75,32 @@ export const readElectronConfig = (root) => {
|
|
|
74
75
|
config.window.fullscreen = false;
|
|
75
76
|
}
|
|
76
77
|
}
|
|
78
|
+
const mini = config.window.mini;
|
|
79
|
+
if (mini !== undefined) {
|
|
80
|
+
if (!mini || typeof mini !== "object" || Array.isArray(mini)) {
|
|
81
|
+
throw new Error("electron.config.json window.mini must be an object.");
|
|
82
|
+
}
|
|
83
|
+
config.window.mini = { "width": 720, "height": 180, "anchor": "bottom-right", "margin": 16, "alwaysOnTop": true, ...mini };
|
|
84
|
+
const { width, height, anchor, margin, alwaysOnTop } = config.window.mini;
|
|
85
|
+
for (const dimension of [width, height]) {
|
|
86
|
+
if (!Number.isInteger(dimension) || dimension < 1 || dimension > 16384) {
|
|
87
|
+
throw new Error("electron.config.json window.mini.width/height must be integers between 1 and 16384.");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (!MINI_ANCHORS.includes(anchor)) {
|
|
91
|
+
throw new Error(`electron.config.json window.mini.anchor must be one of ${MINI_ANCHORS.map((a) => `"${a}"`).join(", ")}.`);
|
|
92
|
+
}
|
|
93
|
+
if (!Number.isInteger(margin) || margin < 0 || margin > 1000) {
|
|
94
|
+
throw new Error("electron.config.json window.mini.margin must be an integer between 0 and 1000.");
|
|
95
|
+
}
|
|
96
|
+
if (typeof alwaysOnTop !== "boolean") {
|
|
97
|
+
throw new Error("electron.config.json window.mini.alwaysOnTop must be a boolean.");
|
|
98
|
+
}
|
|
99
|
+
// The overlay already is the small always-on-top window, and it never takes keyboard focus.
|
|
100
|
+
if (config.window.overlay?.enabled) {
|
|
101
|
+
throw new Error("electron.config.json window.mini cannot be combined with window.overlay.enabled.");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
77
104
|
if (config.macos.notarize && !config.macos.sign) {
|
|
78
105
|
throw new Error("macos.notarize requires macos.sign.");
|
|
79
106
|
}
|
|
@@ -132,8 +159,7 @@ export const createElectronPackagerOptions = (root, config, os, resources) => {
|
|
|
132
159
|
...os === "macos" && (config.macos.localNetworkUsageDescription || config.window.overlay?.enabled) ? {
|
|
133
160
|
"extendInfo": {
|
|
134
161
|
...config.macos.localNetworkUsageDescription ? { "NSLocalNetworkUsageDescription": config.macos.localNetworkUsageDescription } : {},
|
|
135
|
-
// An overlay
|
|
136
|
-
// never shows one. The first instance shows its Dock icon itself (templates/electron/index.js).
|
|
162
|
+
// An overlay is an agent app: no Dock icon, also for a second launch that quits at once.
|
|
137
163
|
...config.window.overlay?.enabled ? { "LSUIElement": true } : {}
|
|
138
164
|
},
|
|
139
165
|
...config.macos.localNetworkUsageDescription ? {
|
package/docs/steam.md
CHANGED
|
@@ -105,12 +105,44 @@ await window.next2dWindow.quit();
|
|
|
105
105
|
const off = window.next2dWindow.onResume(() => { /* 状態の再送など */ });
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
重ね表示が有効なときは、アプリは**1つだけ**起動します(2
|
|
108
|
+
重ね表示が有効なときは、アプリは**1つだけ**起動します(2つ目の起動はすぐに終了します)。macOSではDockにアイコンを出しません(エージェントアプリ。終了はゲーム内のボタンで行います)。開発中に複数起動したいときは `--next2d-allow-multiple` を付けて起動します。
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
Windowsでスリープ(ノートPCのふたを閉じる等)から復帰すると、重ね表示の窓がマウスを受け取れなくなることがあるため、窓を作り直します(ゲームは自動保存を前提にしてください)。電源・ディスプレイ・GPUのイベントは `userData/diagnostics.log`(Windowsは `%APPDATA%\<appId>\diagnostics.log`)に記録されます。
|
|
111
111
|
|
|
112
112
|
Linuxの透明表示はコンポジタが必要です。
|
|
113
113
|
|
|
114
|
+
#### ミニウィンドウ(通常の窓 ⇄ 小窓)
|
|
115
|
+
|
|
116
|
+
ゲーム中に、通常の窓(全画面を含む)と、デスクトップの隅に置く最前面の小窓を切り替えられます。放置ゲームの「作業しながら遊ぶ」表示などに使います。`window.mini` 未指定なら従来どおりです。重ね表示(`window.overlay.enabled: true`)とは同時に使えません。
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
"window": {
|
|
120
|
+
"width": 1920, "height": 1080, "fullscreen": true,
|
|
121
|
+
"mini": { "width": 720, "height": 180, "anchor": "bottom-right", "margin": 16, "alwaysOnTop": true }
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| 項目 | 内容 |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `width` / `height` | 小窓の中身(タイトルバーを除く)の大きさ。既定 720×180 |
|
|
128
|
+
| `anchor` | 初めて小窓にしたときの位置。`"bottom-right"`(既定)/ `"bottom-left"` / `"top-right"` / `"top-left"`。窓のある画面の作業領域(タスクバー・Dockを除く)の隅 |
|
|
129
|
+
| `margin` | 隅からの間隔(px)。既定 `16` |
|
|
130
|
+
| `alwaysOnTop` | `true`(既定)で小窓を最前面に置く(macOSは全ワークスペース・全画面アプリ上にも表示) |
|
|
131
|
+
|
|
132
|
+
小窓は通常の窓のまま(タイトルバーあり。タイトルバーで動かせる)で、キー入力も受け取ります。リサイズ・最大化・全画面はできません。動かした位置は、アプリを終了するまで次の小窓で使います。通常の窓に戻すと、小窓にする前の位置・大きさ・最大化・全画面に戻ります。小窓の間は F11/Alt+Enter/Esc で全画面を切り替えません(Esc はゲームに届きます)。
|
|
133
|
+
|
|
134
|
+
rendererへ `window.next2dWindow` を公開します(アプリのメインフレームのみ)。
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
const { mode, width, height } = await window.next2dWindow.getMode(); // mode は "full" か "mini"
|
|
138
|
+
await window.next2dWindow.setMode("mini"); // 切り替えが終わると { mode, width, height } で解決
|
|
139
|
+
const off = window.next2dWindow.onModeChange(({ mode, width, height }) => { /* 表示を合わせ直す */ });
|
|
140
|
+
const { x, y, width: w, height: h, scaleFactor } = await window.next2dWindow.getBounds();
|
|
141
|
+
await window.next2dWindow.quit();
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`width` / `height` は窓の中身の大きさ(CSS ピクセル)です。Next2D のステージは縦横比を保ったまま窓に合わせて縮むため、1920×1080 のステージを 720×180 の小窓で表示すると、ステージの範囲は高さ 180・幅 320 になります(`options.fullScreen: true` なら、左右のステージの外も描かれます)。小窓用の表示は、この大きさに合わせてゲーム側で配置してください。
|
|
145
|
+
|
|
114
146
|
### STEP4:認証を用意する
|
|
115
147
|
|
|
116
148
|
#### macOSの署名・公証
|
|
@@ -327,12 +359,44 @@ await window.next2dWindow.quit();
|
|
|
327
359
|
const off = window.next2dWindow.onResume(() => { /* resend state, etc. */ });
|
|
328
360
|
```
|
|
329
361
|
|
|
330
|
-
With the overlay enabled the app runs **once**: a second launch quits at once
|
|
362
|
+
With the overlay enabled the app runs **once**: a second launch quits at once. On macOS it is an agent app with no Dock icon (quit from inside the game). Start with `--next2d-allow-multiple` to run several copies while developing.
|
|
331
363
|
|
|
332
|
-
|
|
364
|
+
On Windows, after sleep (e.g. closing a laptop lid) the overlay window can stop receiving the mouse, so a fresh window replaces it; the game should save automatically. Power, display and GPU events are logged to `userData/diagnostics.log` (Windows: `%APPDATA%\<appId>\diagnostics.log`).
|
|
333
365
|
|
|
334
366
|
Transparency on Linux requires a compositor.
|
|
335
367
|
|
|
368
|
+
#### Mini window (normal window ⇄ small window)
|
|
369
|
+
|
|
370
|
+
At run time the game can switch between its normal window (fullscreen included) and a small always-on-top window in a corner of the desktop, for "play while you work" idle modes and the like. Without `window.mini` the window behaves as before. It cannot be combined with the overlay (`window.overlay.enabled: true`).
|
|
371
|
+
|
|
372
|
+
```json
|
|
373
|
+
"window": {
|
|
374
|
+
"width": 1920, "height": 1080, "fullscreen": true,
|
|
375
|
+
"mini": { "width": 720, "height": 180, "anchor": "bottom-right", "margin": 16, "alwaysOnTop": true }
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
| Field | Behaviour |
|
|
380
|
+
|---|---|
|
|
381
|
+
| `width` / `height` | Content size of the small window (title bar excluded). Default 720×180. |
|
|
382
|
+
| `anchor` | Where the small window first appears: `"bottom-right"` (default), `"bottom-left"`, `"top-right"` or `"top-left"` corner of the work area (taskbar/Dock excluded) of the display the window is on. |
|
|
383
|
+
| `margin` | Distance from that corner in px. Default `16`. |
|
|
384
|
+
| `alwaysOnTop` | `true` (default) keeps the small window on top (on macOS also on every workspace and above fullscreen apps). |
|
|
385
|
+
|
|
386
|
+
The small window stays a normal window: it has a title bar (drag it to move the window) and takes keyboard input. It cannot be resized, maximized or made fullscreen. Where the player moves it is used for the next small window until the app quits. Switching back restores the position, size, maximized and fullscreen state from before. In the small window F11 / Alt+Enter / Escape do not toggle fullscreen (Escape reaches the game).
|
|
387
|
+
|
|
388
|
+
The renderer gets `window.next2dWindow` (app main frame only):
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
const { mode, width, height } = await window.next2dWindow.getMode(); // mode is "full" or "mini"
|
|
392
|
+
await window.next2dWindow.setMode("mini"); // resolves with { mode, width, height } once applied
|
|
393
|
+
const off = window.next2dWindow.onModeChange(({ mode, width, height }) => { /* lay the page out again */ });
|
|
394
|
+
const { x, y, width: w, height: h, scaleFactor } = await window.next2dWindow.getBounds();
|
|
395
|
+
await window.next2dWindow.quit();
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
`width` / `height` are the content size in CSS pixels. The Next2D stage keeps its aspect ratio when it is fitted into the window, so a 1920×1080 stage shown in a 720×180 window covers 320×180 (with `options.fullScreen: true` the area left and right of the stage is drawn too). Lay out the small-window view for that size in the game.
|
|
399
|
+
|
|
336
400
|
### STEP4: Prepare credentials
|
|
337
401
|
|
|
338
402
|
#### macOS signing and notarization
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@next2d/builder",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "Multi-platform builder for Next2d Framework, supporting export to various platforms such as macOS, Windows, iOS, Android and Web(HTML)",
|
|
5
5
|
"author": "Toshiyuki Ienaga <ienaga@next2d.app>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@typescript-eslint/parser": "^8.72.1",
|
|
48
48
|
"eslint": "^10.14.0",
|
|
49
49
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
50
|
-
"globals": "^17.
|
|
50
|
+
"globals": "^17.14.0",
|
|
51
51
|
"typescript": "^6.0.3",
|
|
52
52
|
"vite": "^8.3.5"
|
|
53
53
|
}
|
|
@@ -34,7 +34,92 @@ protocol.registerSchemesAsPrivileged([{
|
|
|
34
34
|
|
|
35
35
|
const startURL = "next2d://game/index.html";
|
|
36
36
|
const overlay = config.window.overlay?.enabled ? config.window.overlay : null;
|
|
37
|
+
// The game can switch between its normal window ("full") and a small always-on-top window ("mini") at run time.
|
|
38
|
+
const mini = !overlay && config.window.mini ? config.window.mini : null;
|
|
37
39
|
let mainWindow;
|
|
40
|
+
let windowMode = "full";
|
|
41
|
+
// How the window looked before it became small, to put it back exactly (position, size, maximized, fullscreen).
|
|
42
|
+
let fullState = null;
|
|
43
|
+
// Where the player last left the small window (it has a title bar and can be moved).
|
|
44
|
+
let miniPosition = null;
|
|
45
|
+
// Mode changes are applied one after another (leaving fullscreen is animated on macOS).
|
|
46
|
+
let modeQueue = Promise.resolve();
|
|
47
|
+
const alive = () => Boolean(mainWindow && !mainWindow.isDestroyed());
|
|
48
|
+
const modeInfo = () => {
|
|
49
|
+
const [width, height] = mainWindow.getContentSize();
|
|
50
|
+
return { mode: windowMode, width, height };
|
|
51
|
+
};
|
|
52
|
+
// Entering and leaving fullscreen is animated on macOS: wait for it, so the size the page is told is the real one.
|
|
53
|
+
const setFullScreenAndWait = (win, value) => new Promise((resolve) => {
|
|
54
|
+
if (win.isFullScreen() === value) {
|
|
55
|
+
resolve();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let timer;
|
|
59
|
+
const finish = () => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
resolve();
|
|
62
|
+
};
|
|
63
|
+
// Some window managers never report it; do not wait forever.
|
|
64
|
+
timer = setTimeout(finish, 1500);
|
|
65
|
+
win.once(value ? "enter-full-screen" : "leave-full-screen", finish);
|
|
66
|
+
win.setFullScreen(value);
|
|
67
|
+
});
|
|
68
|
+
const cornerOf = (win) => {
|
|
69
|
+
const { width, height } = win.getBounds();
|
|
70
|
+
const { workArea } = screen.getDisplayMatching(win.getBounds());
|
|
71
|
+
const right = mini.anchor.endsWith("right");
|
|
72
|
+
const bottom = mini.anchor.startsWith("bottom");
|
|
73
|
+
return {
|
|
74
|
+
x: right ? workArea.x + workArea.width - width - mini.margin : workArea.x + mini.margin,
|
|
75
|
+
y: bottom ? workArea.y + workArea.height - height - mini.margin : workArea.y + mini.margin
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
const toMini = async () => {
|
|
79
|
+
const win = mainWindow;
|
|
80
|
+
fullState = { bounds: win.getNormalBounds(), maximized: win.isMaximized(), fullScreen: win.isFullScreen() };
|
|
81
|
+
await setFullScreenAndWait(win, false);
|
|
82
|
+
if (win.isMaximized()) win.unmaximize();
|
|
83
|
+
// Resize first: some platforms ignore size changes of a window that is no longer resizable.
|
|
84
|
+
win.setContentSize(mini.width, mini.height);
|
|
85
|
+
win.setFullScreenable(false);
|
|
86
|
+
win.setMaximizable(false);
|
|
87
|
+
win.setResizable(false);
|
|
88
|
+
const { x, y } = miniPosition ?? cornerOf(win);
|
|
89
|
+
win.setPosition(x, y);
|
|
90
|
+
if (mini.alwaysOnTop) {
|
|
91
|
+
win.setAlwaysOnTop(true, "floating");
|
|
92
|
+
if (process.platform === "darwin") win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
93
|
+
}
|
|
94
|
+
windowMode = "mini";
|
|
95
|
+
};
|
|
96
|
+
const toFull = async () => {
|
|
97
|
+
const win = mainWindow;
|
|
98
|
+
const [x, y] = win.getPosition();
|
|
99
|
+
miniPosition = { x, y };
|
|
100
|
+
if (mini.alwaysOnTop) {
|
|
101
|
+
win.setAlwaysOnTop(false);
|
|
102
|
+
if (process.platform === "darwin") win.setVisibleOnAllWorkspaces(false);
|
|
103
|
+
}
|
|
104
|
+
win.setResizable(true);
|
|
105
|
+
win.setMaximizable(true);
|
|
106
|
+
win.setFullScreenable(true);
|
|
107
|
+
if (fullState) {
|
|
108
|
+
win.setBounds(fullState.bounds);
|
|
109
|
+
if (fullState.maximized) win.maximize();
|
|
110
|
+
if (fullState.fullScreen) await setFullScreenAndWait(win, true);
|
|
111
|
+
}
|
|
112
|
+
win.focus();
|
|
113
|
+
windowMode = "full";
|
|
114
|
+
};
|
|
115
|
+
const setWindowMode = (mode) => {
|
|
116
|
+
modeQueue = modeQueue.then(async () => {
|
|
117
|
+
if (!alive() || mode === windowMode) return;
|
|
118
|
+
await (mode === "mini" ? toMini() : toFull());
|
|
119
|
+
mainWindow.webContents.send("next2d:window:mode", modeInfo());
|
|
120
|
+
}).catch((error) => console.error("Window mode change failed:", error));
|
|
121
|
+
return modeQueue.then(() => (alive() ? modeInfo() : { mode: windowMode, width: 0, height: 0 }));
|
|
122
|
+
};
|
|
38
123
|
// The last click-through state asked for, so it can be applied again after sleep/lock.
|
|
39
124
|
let overlayIgnore = Boolean(config.window.overlay?.enabled && config.window.overlay.clickThrough);
|
|
40
125
|
// Bottom anchor: flush with the primary display's work area (above the taskbar/Dock), centred.
|
|
@@ -76,9 +161,6 @@ const diag = (message) => {
|
|
|
76
161
|
fs.appendFileSync(file, `${new Date().toISOString()} ${message}\n`);
|
|
77
162
|
} catch { /* diagnostics must never break the game */ }
|
|
78
163
|
};
|
|
79
|
-
// After sleep (closing a laptop lid) the GPU is often reset. The renderer then keeps showing its last frame but can
|
|
80
|
-
// no longer draw (the web page is not told), so the pet looks alive and menus never appear. Load the page again to
|
|
81
|
-
// get a fresh GPU device. The game saves automatically, so progress is kept.
|
|
82
164
|
// Counts of mouse events the page really receives (moves are forwarded while click-through; down/up only arrive
|
|
83
165
|
// while the window accepts clicks). Logged when they change, to tell "clicks never reach the page" from other causes.
|
|
84
166
|
const DIAG_SCRIPT = `(() => {
|
|
@@ -103,19 +185,37 @@ const snapshotRenderer = async () => {
|
|
|
103
185
|
diag(`renderer snapshot failed: ${error?.message ?? error}`);
|
|
104
186
|
}
|
|
105
187
|
};
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
188
|
+
// After sleep (closing a laptop lid) Windows can leave the overlay window itself broken: mouse moves are no longer
|
|
189
|
+
// forwarded to it, or clicks fall through even where the pet is drawn. Reloading the page does not help (the
|
|
190
|
+
// window is the same), so build a new window and close the old one. The game saves automatically, so progress is
|
|
191
|
+
// kept. The new window is created first so the app never has zero windows (which would quit it).
|
|
192
|
+
let recreating = false;
|
|
193
|
+
const recreateWindow = async () => {
|
|
194
|
+
if (recreating || !mainWindow || mainWindow.isDestroyed()) return;
|
|
195
|
+
recreating = true;
|
|
196
|
+
diag("recreate window (resume)");
|
|
197
|
+
const old = mainWindow;
|
|
109
198
|
overlayIgnore = Boolean(overlay?.clickThrough);
|
|
110
|
-
|
|
111
|
-
|
|
199
|
+
try {
|
|
200
|
+
await createWindow();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
diag(`recreate window failed: ${error?.message ?? error}`);
|
|
203
|
+
} finally {
|
|
204
|
+
recreating = false;
|
|
205
|
+
if (old !== mainWindow && !old.isDestroyed()) {
|
|
206
|
+
// Closing on purpose: do not treat the old page going away as a crash.
|
|
207
|
+
old.webContents.removeAllListeners("render-process-gone");
|
|
208
|
+
old.destroy();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
112
211
|
};
|
|
113
212
|
const createWindow = async () => {
|
|
114
213
|
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
|
115
214
|
// The preload reads these flags; each API is exposed only when its feature is configured.
|
|
116
215
|
const preloadFlags = [
|
|
117
216
|
...(config.nativeBridge ? ["--next2d-native-bridge"] : []),
|
|
118
|
-
...(overlay ? ["--next2d-overlay"] : [])
|
|
217
|
+
...(overlay ? ["--next2d-overlay"] : []),
|
|
218
|
+
...(mini ? ["--next2d-window-mode"] : [])
|
|
119
219
|
];
|
|
120
220
|
mainWindow = new BrowserWindow({
|
|
121
221
|
width: Math.min(config.window.width, width),
|
|
@@ -183,7 +283,8 @@ const createWindow = async () => {
|
|
|
183
283
|
}
|
|
184
284
|
});
|
|
185
285
|
mainWindow.webContents.on("before-input-event", (event, input) => {
|
|
186
|
-
|
|
286
|
+
// The small window never goes fullscreen; Escape belongs to the game there.
|
|
287
|
+
if (input.type !== "keyDown" || overlay || windowMode === "mini") {
|
|
187
288
|
return;
|
|
188
289
|
}
|
|
189
290
|
if (input.key === "F11" || (input.alt && input.key === "Enter")) {
|
|
@@ -205,9 +306,8 @@ const createWindow = async () => {
|
|
|
205
306
|
|
|
206
307
|
app.whenReady().then(async () => {
|
|
207
308
|
if (!hasLock) return;
|
|
208
|
-
// The overlay app is packaged as an agent app (LSUIElement)
|
|
209
|
-
// the
|
|
210
|
-
if (process.platform === "darwin" && overlay) app.dock?.show();
|
|
309
|
+
// The overlay app is packaged as an agent app (LSUIElement): it never shows a Dock icon (a desktop pet stays out
|
|
310
|
+
// of the Dock and the app switcher; the game offers its own "quit").
|
|
211
311
|
Menu.setApplicationMenu(process.platform === "darwin" ? Menu.buildFromTemplate([
|
|
212
312
|
{ role: "appMenu" }, { role: "editMenu" }, { role: "windowMenu" }
|
|
213
313
|
]) : null);
|
|
@@ -246,24 +346,42 @@ app.whenReady().then(async () => {
|
|
|
246
346
|
return nativeBridge.request(method, params);
|
|
247
347
|
});
|
|
248
348
|
}
|
|
349
|
+
const trusted = (event) => {
|
|
350
|
+
if (!isTrustedSender(event, mainWindow)) {
|
|
351
|
+
diag(`window API access denied (frame url: ${event?.senderFrame?.url})`);
|
|
352
|
+
throw new Error("Window API access denied");
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
if (overlay || mini) {
|
|
356
|
+
ipcMain.handle("next2d:window:get-bounds", (event) => {
|
|
357
|
+
trusted(event);
|
|
358
|
+
const bounds = mainWindow.getBounds();
|
|
359
|
+
return { ...bounds, scaleFactor: screen.getDisplayMatching(bounds).scaleFactor };
|
|
360
|
+
});
|
|
361
|
+
ipcMain.handle("next2d:window:quit", (event) => {
|
|
362
|
+
trusted(event);
|
|
363
|
+
// Reply before the window is torn down.
|
|
364
|
+
setImmediate(() => app.quit());
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (mini) {
|
|
368
|
+
ipcMain.handle("next2d:window:get-mode", (event) => {
|
|
369
|
+
trusted(event);
|
|
370
|
+
return modeInfo();
|
|
371
|
+
});
|
|
372
|
+
ipcMain.handle("next2d:window:set-mode", (event, mode) => {
|
|
373
|
+
trusted(event);
|
|
374
|
+
if (mode !== "full" && mode !== "mini") throw new TypeError("mode must be \"full\" or \"mini\"");
|
|
375
|
+
return setWindowMode(mode);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
249
378
|
if (overlay) {
|
|
250
|
-
const trusted = (event) => {
|
|
251
|
-
if (!isTrustedSender(event, mainWindow)) {
|
|
252
|
-
diag(`window API access denied (frame url: ${event?.senderFrame?.url})`);
|
|
253
|
-
throw new Error("Window API access denied");
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
379
|
ipcMain.handle("next2d:window:set-ignore-mouse-events", (event, ignore) => {
|
|
257
380
|
trusted(event);
|
|
258
381
|
if (typeof ignore !== "boolean") throw new TypeError("ignore must be a boolean");
|
|
259
382
|
overlayIgnore = ignore;
|
|
260
383
|
mainWindow.setIgnoreMouseEvents(ignore, ignore ? { forward: true } : undefined);
|
|
261
384
|
});
|
|
262
|
-
ipcMain.handle("next2d:window:get-bounds", (event) => {
|
|
263
|
-
trusted(event);
|
|
264
|
-
const bounds = mainWindow.getBounds();
|
|
265
|
-
return { ...bounds, scaleFactor: screen.getDisplayMatching(bounds).scaleFactor };
|
|
266
|
-
});
|
|
267
385
|
// After sleep or screen lock the OS can drop the mouse-forwarding hook and reset the window's
|
|
268
386
|
// z-order/bounds. Apply the overlay state again and tell the page so it can recover too.
|
|
269
387
|
const recoverOverlay = () => {
|
|
@@ -278,19 +396,15 @@ app.whenReady().then(async () => {
|
|
|
278
396
|
for (const name of ["resume", "unlock-screen"]) {
|
|
279
397
|
powerMonitor.on(name, () => {
|
|
280
398
|
diag(name);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
399
|
+
if (name === "resume" && process.platform === "win32") {
|
|
400
|
+
// Give the GPU/display time to come back, then start over with a fresh window.
|
|
401
|
+
setTimeout(() => { void recreateWindow(); }, 3000);
|
|
402
|
+
} else {
|
|
403
|
+
setTimeout(recoverOverlay, 1000);
|
|
404
|
+
}
|
|
286
405
|
});
|
|
287
406
|
}
|
|
288
407
|
app.on("child-process-gone", (_event, details) => diag(`child-process-gone ${details.type} ${details.reason}`));
|
|
289
|
-
ipcMain.handle("next2d:window:quit", (event) => {
|
|
290
|
-
trusted(event);
|
|
291
|
-
// Reply before the window is torn down.
|
|
292
|
-
setImmediate(() => app.quit());
|
|
293
|
-
});
|
|
294
408
|
}
|
|
295
409
|
await createWindow();
|
|
296
410
|
}).catch((error) => {
|
|
@@ -31,3 +31,24 @@ if (flags.has("--next2d-overlay")) {
|
|
|
31
31
|
}
|
|
32
32
|
});
|
|
33
33
|
}
|
|
34
|
+
|
|
35
|
+
// window.mini: switch between the normal window ("full") and the small always-on-top window ("mini").
|
|
36
|
+
// Never set together with the overlay (the config is rejected), so the two never expose next2dWindow twice.
|
|
37
|
+
if (flags.has("--next2d-window-mode")) {
|
|
38
|
+
contextBridge.exposeInMainWorld("next2dWindow", {
|
|
39
|
+
getMode: () => ipcRenderer.invoke("next2d:window:get-mode"),
|
|
40
|
+
setMode: async (mode) => {
|
|
41
|
+
if (mode !== "full" && mode !== "mini") throw new TypeError("Expected \"full\" or \"mini\"");
|
|
42
|
+
return ipcRenderer.invoke("next2d:window:set-mode", mode);
|
|
43
|
+
},
|
|
44
|
+
getBounds: () => ipcRenderer.invoke("next2d:window:get-bounds"),
|
|
45
|
+
quit: async () => { await ipcRenderer.invoke("next2d:window:quit"); },
|
|
46
|
+
// Fires after every change with { mode, width, height } (content size in CSS pixels).
|
|
47
|
+
onModeChange: (listener) => {
|
|
48
|
+
if (typeof listener !== "function") throw new TypeError("Expected mode listener");
|
|
49
|
+
const handler = (_event, info) => listener(info);
|
|
50
|
+
ipcRenderer.on("next2d:window:mode", handler);
|
|
51
|
+
return () => ipcRenderer.removeListener("next2d:window:mode", handler);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|