@swf2es/desktop 0.1.20 → 0.1.22

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.
Files changed (54) hide show
  1. package/dist/main/air-bridge.d.ts +7 -0
  2. package/dist/main/air-bridge.d.ts.map +1 -0
  3. package/dist/main/air-bridge.js +166 -0
  4. package/dist/main/air-bridge.js.map +1 -0
  5. package/dist/main/dialogs.d.ts +18 -0
  6. package/dist/main/dialogs.d.ts.map +1 -0
  7. package/dist/main/dialogs.js +128 -0
  8. package/dist/main/dialogs.js.map +1 -0
  9. package/dist/main/main.js +28 -788
  10. package/dist/main/main.js.map +1 -1
  11. package/dist/main/menu.d.ts +2 -0
  12. package/dist/main/menu.d.ts.map +1 -0
  13. package/dist/main/menu.js +80 -0
  14. package/dist/main/menu.js.map +1 -0
  15. package/dist/main/network-rules.d.ts +35 -0
  16. package/dist/main/network-rules.d.ts.map +1 -0
  17. package/dist/main/network-rules.js +167 -0
  18. package/dist/main/network-rules.js.map +1 -0
  19. package/dist/main/network-types.d.ts +131 -0
  20. package/dist/main/network-types.d.ts.map +1 -0
  21. package/dist/main/network-types.js +21 -0
  22. package/dist/main/network-types.js.map +1 -0
  23. package/dist/main/network.d.ts +1 -113
  24. package/dist/main/network.d.ts.map +1 -1
  25. package/dist/main/network.js +8 -181
  26. package/dist/main/network.js.map +1 -1
  27. package/dist/main/open.d.ts +55 -0
  28. package/dist/main/open.d.ts.map +1 -0
  29. package/dist/main/open.js +256 -0
  30. package/dist/main/open.js.map +1 -0
  31. package/dist/main/permissions.d.ts +17 -0
  32. package/dist/main/permissions.d.ts.map +1 -0
  33. package/dist/main/permissions.js +113 -0
  34. package/dist/main/permissions.js.map +1 -0
  35. package/dist/main/state.d.ts +17 -0
  36. package/dist/main/state.d.ts.map +1 -0
  37. package/dist/main/state.js +30 -0
  38. package/dist/main/state.js.map +1 -0
  39. package/dist/main/switches.d.ts +25 -0
  40. package/dist/main/switches.d.ts.map +1 -0
  41. package/dist/main/switches.js +69 -0
  42. package/dist/main/switches.js.map +1 -0
  43. package/package.json +7 -7
  44. package/src/main/air-bridge.ts +185 -0
  45. package/src/main/dialogs.ts +147 -0
  46. package/src/main/main.ts +56 -937
  47. package/src/main/menu.ts +84 -0
  48. package/src/main/network-rules.ts +199 -0
  49. package/src/main/network-types.ts +150 -0
  50. package/src/main/network.ts +30 -337
  51. package/src/main/open.ts +302 -0
  52. package/src/main/permissions.ts +135 -0
  53. package/src/main/state.ts +44 -0
  54. package/src/main/switches.ts +87 -0
@@ -0,0 +1,185 @@
1
+ // An AIR application's reach through the main process (shared/api.ts
2
+ // DesktopAir): its file system, its EncryptedLocalStore, its window and
3
+ // the screens. air.ts does the work apart from Electron.
4
+ import { readFile, realpath } from "node:fs/promises";
5
+ import {
6
+ app,
7
+ type BrowserWindow,
8
+ type IpcMainEvent,
9
+ type IpcMainInvokeEvent,
10
+ ipcMain,
11
+ screen,
12
+ } from "electron";
13
+ import type { AirScreenState, AirWindowState } from "../shared/api.js";
14
+ import { createDirectoryOf, listOf, moveOf, readOf, removeOf, statOf, writeOf } from "./air.ts";
15
+ import { resolveCase } from "./case-paths.ts";
16
+ import { air, encryptedItems } from "./open.ts";
17
+ import { sandbox, trusted, window } from "./state.ts";
18
+
19
+ /** The window as an AIR application's NativeWindow reads it. */
20
+ function windowState(w: BrowserWindow): AirWindowState {
21
+ const bounds = w.getBounds();
22
+ const content = w.getContentBounds();
23
+ return {
24
+ bounds,
25
+ contentOffset: { x: content.x - bounds.x, y: content.y - bounds.y },
26
+ title: w.getTitle(),
27
+ displayState: w.isMinimized() ? "minimized" : w.isMaximized() ? "maximized" : "normal",
28
+ active: w.isFocused(),
29
+ visible: w.isVisible(),
30
+ };
31
+ }
32
+
33
+ /** The displays as an AIR application's Screen reads them: the work area is past the system's bars. */
34
+ function screenStates(): AirScreenState[] {
35
+ const primary = screen.getPrimaryDisplay().id;
36
+ return screen.getAllDisplays().map((display) => ({
37
+ bounds: display.bounds,
38
+ visibleBounds: display.workArea,
39
+ colorDepth: display.colorDepth,
40
+ main: display.id === primary,
41
+ }));
42
+ }
43
+
44
+ /**
45
+ * An AIR application's reach, shared/api.ts DesktopAir: the file system
46
+ * and the window, only while one the user opened plays and only for its
47
+ * page. The file system is read synchronously where File reads it so.
48
+ */
49
+ export function listenForAir(): void {
50
+ const playing = (event: IpcMainEvent | IpcMainInvokeEvent) =>
51
+ trusted(event) && sandbox.application;
52
+ ipcMain.on("air:stat", (event, path: unknown) => {
53
+ event.returnValue = playing(event) && typeof path === "string" ? statOf(path) : null;
54
+ });
55
+ ipcMain.on("air:list", (event, path: unknown) => {
56
+ event.returnValue = playing(event) && typeof path === "string" ? listOf(path) : null;
57
+ });
58
+ // FileStream's open, which reads a file at once, as stat does: only once the application plays.
59
+ ipcMain.on("air:read-sync", (event, path: unknown) => {
60
+ event.returnValue = playing(event) && typeof path === "string" ? readOf(path) : null;
61
+ });
62
+ // EncryptedLocalStore, at once as its methods are: only the application playing's.
63
+ const items = (event: IpcMainEvent) => (playing(event) ? encryptedItems : null);
64
+ ipcMain.on("air:item-get", (event, name: unknown) => {
65
+ event.returnValue = typeof name === "string" ? (items(event)?.get(name) ?? null) : null;
66
+ });
67
+ ipcMain.on("air:item-set", (event, name: unknown, bytes: unknown) => {
68
+ if (typeof name === "string" && bytes instanceof Uint8Array) {
69
+ items(event)?.set(name, bytes);
70
+ }
71
+
72
+ event.returnValue = null;
73
+ });
74
+ ipcMain.on("air:item-remove", (event, name: unknown) => {
75
+ if (typeof name === "string") {
76
+ items(event)?.remove(name);
77
+ }
78
+
79
+ event.returnValue = null;
80
+ });
81
+ ipcMain.on("air:items-reset", (event) => {
82
+ items(event)?.reset();
83
+ event.returnValue = null;
84
+ });
85
+ // FileStream's writes and File's changes, at once as adl's: only once the application plays.
86
+ ipcMain.on(
87
+ "air:write-sync",
88
+ (event, path: unknown, bytes: unknown, offset: unknown, length: unknown) => {
89
+ event.returnValue =
90
+ playing(event) &&
91
+ typeof path === "string" &&
92
+ bytes instanceof Uint8Array &&
93
+ Number.isSafeInteger(offset) &&
94
+ Number.isSafeInteger(length) &&
95
+ (offset as number) >= 0 &&
96
+ (length as number) >= 0 &&
97
+ writeOf(path, bytes, offset as number, length as number);
98
+ },
99
+ );
100
+ ipcMain.on("air:create-directory", (event, path: unknown) => {
101
+ event.returnValue = playing(event) && typeof path === "string" && createDirectoryOf(path);
102
+ });
103
+ ipcMain.on("air:remove", (event, path: unknown) => {
104
+ event.returnValue = playing(event) && typeof path === "string" && removeOf(path);
105
+ });
106
+ ipcMain.on("air:move", (event, from: unknown, to: unknown) => {
107
+ event.returnValue =
108
+ playing(event) && typeof from === "string" && typeof to === "string" && moveOf(from, to);
109
+ });
110
+ // A read is what starts the application's grant: the page reads its content SWF first.
111
+ ipcMain.handle("air:read", async (event, path: unknown) => {
112
+ if (!trusted(event) || typeof path !== "string" || !air) {
113
+ return null;
114
+ }
115
+
116
+ try {
117
+ const real = await realpath(resolveCase(path));
118
+ return sandbox.startApplication(real) ? new Uint8Array(await readFile(real)) : null;
119
+ } catch {
120
+ return null;
121
+ }
122
+ });
123
+ ipcMain.on("air:window", (event) => {
124
+ event.returnValue = playing(event) && window ? windowState(window) : null;
125
+ });
126
+ ipcMain.on("air:screens", (event) => {
127
+ event.returnValue = playing(event) ? screenStates() : [];
128
+ });
129
+ ipcMain.on("air:set-bounds", (event, bounds: unknown) => {
130
+ const b = bounds as { x?: unknown; y?: unknown; width?: unknown; height?: unknown } | null;
131
+ const n = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? Math.round(v) : null);
132
+ const [x, y, width, height] = [n(b?.x), n(b?.y), n(b?.width), n(b?.height)];
133
+ if (
134
+ playing(event) &&
135
+ window &&
136
+ x !== null &&
137
+ y !== null &&
138
+ width &&
139
+ height &&
140
+ width > 0 &&
141
+ height > 0
142
+ ) {
143
+ window.setBounds({ x, y, width, height });
144
+ }
145
+ });
146
+ ipcMain.on("air:set-title", (event, title: unknown) => {
147
+ if (playing(event) && window && typeof title === "string") {
148
+ window.setTitle(title.slice(0, 1024));
149
+ }
150
+ });
151
+ ipcMain.on("air:window-do", (event, what: unknown) => {
152
+ if (!playing(event) || !window) {
153
+ return;
154
+ }
155
+
156
+ switch (what) {
157
+ case "minimize":
158
+ window.minimize();
159
+ break;
160
+ case "maximize":
161
+ window.maximize();
162
+ break;
163
+ case "restore":
164
+ window.restore();
165
+ break;
166
+ case "activate":
167
+ window.focus();
168
+ break;
169
+ case "show":
170
+ window.show();
171
+ break;
172
+ case "hide":
173
+ window.hide();
174
+ break;
175
+ case "close":
176
+ window.close();
177
+ break;
178
+ }
179
+ });
180
+ ipcMain.on("air:exit", (event, code: unknown) => {
181
+ if (playing(event)) {
182
+ app.exit(typeof code === "number" ? code | 0 : 0);
183
+ }
184
+ });
185
+ }
@@ -0,0 +1,147 @@
1
+ // The app's dialogs: an error, File › Open, the library chooser, and the
2
+ // URL dialog, a window of its own.
3
+ import { basename } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { BrowserWindow, dialog, type IpcMainEvent, type IpcMainInvokeEvent } from "electron";
6
+ import type { LibraryName } from "../shared/api.js";
7
+ import { buildMenu } from "./menu.ts";
8
+ import { openChosen, readStart } from "./open.ts";
9
+ import { APP_ORIGIN } from "./protocol.ts";
10
+ import { settings, window } from "./state.ts";
11
+
12
+ const promptPreload = fileURLToPath(new URL("../preload/prompt.cjs", import.meta.url));
13
+ const PROMPT_URL = `${APP_ORIGIN}/open-url.html`;
14
+
15
+ /** The dialog that asks for a URL, while it is open. */
16
+ export let prompt: BrowserWindow | null = null;
17
+
18
+ export function refuse(message: string, detail: string): void {
19
+ if (window) {
20
+ void dialog.showMessageBox(window, { type: "error", message, detail });
21
+ }
22
+ }
23
+
24
+ export async function showOpenDialog(): Promise<void> {
25
+ if (!window) {
26
+ return;
27
+ }
28
+
29
+ const result = await dialog.showOpenDialog(window, {
30
+ title: "Open a SWF or an AIR application",
31
+ properties: ["openFile"],
32
+ filters: [
33
+ { name: "SWF and AIR application descriptors", extensions: ["swf", "xml"] },
34
+ { name: "All files", extensions: ["*"] },
35
+ ],
36
+ });
37
+ if (!result.canceled && result.filePaths[0]) {
38
+ await openChosen(result.filePaths[0]);
39
+ }
40
+ }
41
+
42
+ /** Ask for a library, check that it is ABC, keep its path, and start the page again with it. */
43
+ export async function chooseLibrary(name: LibraryName): Promise<void> {
44
+ if (!window) {
45
+ return;
46
+ }
47
+
48
+ const result = await dialog.showOpenDialog(window, {
49
+ title: `Choose ${name}.abc`,
50
+ properties: ["openFile"],
51
+ filters: [
52
+ { name: "ActionScript bytecode", extensions: ["abc"] },
53
+ { name: "All files", extensions: ["*"] },
54
+ ],
55
+ });
56
+ const path = result.filePaths[0];
57
+ if (result.canceled || !path) {
58
+ return;
59
+ }
60
+
61
+ // An ABC starts with its minor version, then its major one, 46.
62
+ const head = await readStart(path, 4);
63
+ if (head?.[2] !== 46 || head[3] !== 0) {
64
+ refuse(`${basename(path)} is not an ABC file.`, `swf2es needs ${name}.abc itself.`);
65
+ return;
66
+ }
67
+
68
+ if (!settings.setLibrary(name, path)) {
69
+ refuse("The settings could not be saved.", `swf2es uses ${path} until it quits.`);
70
+ }
71
+
72
+ buildMenu();
73
+
74
+ // The page fetched the libraries once; a fresh page fetches the new one.
75
+ window.webContents.reload();
76
+ }
77
+
78
+ /** Play on the library the app ships again, in place of one the user chose. */
79
+ export function resetLibrary(name: LibraryName): void {
80
+ if (!settings.clearLibrary(name)) {
81
+ refuse("The settings could not be saved.", `swf2es uses its own ${name} until it quits.`);
82
+ }
83
+
84
+ buildMenu();
85
+ window?.webContents.reload();
86
+ }
87
+
88
+ /**
89
+ * The dialog that asks for a SWF's URL: a window of its own, apart from the
90
+ * page, so that only the user, and never a SWF, opens one by its URL.
91
+ */
92
+ export function showUrlDialog(): void {
93
+ if (!window) {
94
+ return;
95
+ }
96
+
97
+ if (prompt) {
98
+ prompt.focus();
99
+ return;
100
+ }
101
+
102
+ const dialogWindow = new BrowserWindow({
103
+ parent: window,
104
+ modal: true,
105
+ width: 560,
106
+ height: 180,
107
+ resizable: false,
108
+ minimizable: false,
109
+ maximizable: false,
110
+ fullscreenable: false,
111
+ title: "Open URL",
112
+ show: false,
113
+ webPreferences: {
114
+ preload: promptPreload,
115
+ contextIsolation: true,
116
+ sandbox: true,
117
+ nodeIntegration: false,
118
+ webSecurity: true,
119
+ spellcheck: false,
120
+ },
121
+ });
122
+ prompt = dialogWindow;
123
+ dialogWindow.setMenu(null);
124
+ dialogWindow.on("page-title-updated", (event) => event.preventDefault());
125
+ dialogWindow.once("ready-to-show", () => dialogWindow.show());
126
+ dialogWindow.on("closed", () => {
127
+ if (prompt === dialogWindow) {
128
+ prompt = null;
129
+ }
130
+ });
131
+ const contents = dialogWindow.webContents;
132
+ contents.on("will-navigate", (event) => event.preventDefault());
133
+ contents.setWindowOpenHandler(() => ({ action: "deny" }));
134
+ void dialogWindow.loadURL(PROMPT_URL);
135
+ }
136
+
137
+ /** Whether a message comes from the URL dialog's own page. */
138
+ export function fromPrompt(event: IpcMainEvent | IpcMainInvokeEvent): boolean {
139
+ const frame = event.senderFrame;
140
+ return (
141
+ prompt !== null &&
142
+ event.sender === prompt.webContents &&
143
+ frame !== null &&
144
+ frame.parent === null &&
145
+ frame.url === PROMPT_URL
146
+ );
147
+ }