@swf2es/desktop 0.1.19 → 0.1.21
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/README.md +13 -8
- package/dist/main/air-bridge.d.ts +7 -0
- package/dist/main/air-bridge.d.ts.map +1 -0
- package/dist/main/air-bridge.js +166 -0
- package/dist/main/air-bridge.js.map +1 -0
- package/dist/main/dialogs.d.ts +18 -0
- package/dist/main/dialogs.d.ts.map +1 -0
- package/dist/main/dialogs.js +128 -0
- package/dist/main/dialogs.js.map +1 -0
- package/dist/main/main.js +24 -789
- package/dist/main/main.js.map +1 -1
- package/dist/main/menu.d.ts +2 -0
- package/dist/main/menu.d.ts.map +1 -0
- package/dist/main/menu.js +80 -0
- package/dist/main/menu.js.map +1 -0
- package/dist/main/network-rules.d.ts +35 -0
- package/dist/main/network-rules.d.ts.map +1 -0
- package/dist/main/network-rules.js +167 -0
- package/dist/main/network-rules.js.map +1 -0
- package/dist/main/network-types.d.ts +131 -0
- package/dist/main/network-types.d.ts.map +1 -0
- package/dist/main/network-types.js +21 -0
- package/dist/main/network-types.js.map +1 -0
- package/dist/main/network.d.ts +1 -113
- package/dist/main/network.d.ts.map +1 -1
- package/dist/main/network.js +8 -181
- package/dist/main/network.js.map +1 -1
- package/dist/main/open.d.ts +55 -0
- package/dist/main/open.d.ts.map +1 -0
- package/dist/main/open.js +256 -0
- package/dist/main/open.js.map +1 -0
- package/dist/main/permissions.d.ts +17 -0
- package/dist/main/permissions.d.ts.map +1 -0
- package/dist/main/permissions.js +113 -0
- package/dist/main/permissions.js.map +1 -0
- package/dist/main/settings.d.ts +8 -1
- package/dist/main/settings.d.ts.map +1 -1
- package/dist/main/settings.js +9 -4
- package/dist/main/settings.js.map +1 -1
- package/dist/main/state.d.ts +17 -0
- package/dist/main/state.d.ts.map +1 -0
- package/dist/main/state.js +30 -0
- package/dist/main/state.js.map +1 -0
- package/dist/main/switches.d.ts +25 -0
- package/dist/main/switches.d.ts.map +1 -0
- package/dist/main/switches.js +69 -0
- package/dist/main/switches.js.map +1 -0
- package/dist/renderer/index.js +11 -8
- package/dist/renderer/index.js.map +1 -1
- package/package.json +7 -7
- package/src/main/air-bridge.ts +185 -0
- package/src/main/dialogs.ts +147 -0
- package/src/main/main.ts +52 -938
- package/src/main/menu.ts +84 -0
- package/src/main/network-rules.ts +199 -0
- package/src/main/network-types.ts +150 -0
- package/src/main/network.ts +30 -337
- package/src/main/open.ts +302 -0
- package/src/main/permissions.ts +135 -0
- package/src/main/settings.ts +16 -5
- package/src/main/state.ts +44 -0
- package/src/main/switches.ts +87 -0
- package/src/renderer/index.ts +18 -7
package/dist/main/main.js
CHANGED
|
@@ -9,30 +9,33 @@
|
|
|
9
9
|
// --proxy=socks5://[user:pass@]host:port (or http://...) sends the SWF's network through a proxy.
|
|
10
10
|
//
|
|
11
11
|
// The second runs an AIR application by its descriptor, as adl does.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
//
|
|
13
|
+
// Its parts: state.ts, what they share; switches.ts, the command line's;
|
|
14
|
+
// open.ts, what plays and opening it; permissions.ts, sockets and
|
|
15
|
+
// WebSockets; dialogs.ts and menu.ts; air-bridge.ts, an AIR application's
|
|
16
|
+
// reach. Here: start-up, the window and its page's messages.
|
|
17
|
+
import { isAbsolute } from "node:path";
|
|
15
18
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
19
|
+
import { app, BrowserWindow, dialog, ipcMain, session, shell } from "electron";
|
|
20
|
+
import { airCommand } from "./air.js";
|
|
21
|
+
import { listenForAir } from "./air-bridge.js";
|
|
22
|
+
import { chooseLibrary, fromPrompt, prompt, showOpenDialog, showUrlDialog } from "./dialogs.js";
|
|
20
23
|
import { bridgeFiles } from "./files.js";
|
|
24
|
+
import { buildMenu } from "./menu.js";
|
|
21
25
|
import { failureLine, requestLine } from "./netlog.js";
|
|
22
|
-
import {
|
|
26
|
+
import { Refused } from "./network-types.js";
|
|
27
|
+
import { current, movieFromArguments, networkMovie, openAir, openChosen, openEntry, openUrl, remoteUrl, } from "./open.js";
|
|
28
|
+
import { permitSocket, permitWebSocket } from "./permissions.js";
|
|
23
29
|
import { APP_ORIGIN, registerScheme, serve } from "./protocol.js";
|
|
24
|
-
import {
|
|
25
|
-
import { Sandbox } from "./sandbox.js";
|
|
30
|
+
import { proxyArgument, sameProxy } from "./proxy.js";
|
|
26
31
|
import { SettingsFile } from "./settings.js";
|
|
27
32
|
import { bridgeSockets } from "./sockets.js";
|
|
28
|
-
import {
|
|
33
|
+
import { sandbox, setListening, setSettings, settings, setWindow, TITLE, trusted, window, } from "./state.js";
|
|
34
|
+
import { logSockets, netLog, network, proxied, proxy, trace, via, warn, webSocketOpen, } from "./switches.js";
|
|
29
35
|
import { bridgeWebSockets } from "./websockets.js";
|
|
30
36
|
const preload = fileURLToPath(new URL("../preload/preload.cjs", import.meta.url));
|
|
31
|
-
const promptPreload = fileURLToPath(new URL("../preload/prompt.cjs", import.meta.url));
|
|
32
37
|
// The window's and the taskbar's icon, the project's, where the platform takes one from the window.
|
|
33
38
|
const icon = fileURLToPath(new URL("../../static/icon.png", import.meta.url));
|
|
34
|
-
const PROMPT_URL = `${APP_ORIGIN}/open-url.html`;
|
|
35
|
-
const TITLE = "swf2es";
|
|
36
39
|
// The tests keep their settings apart from the user's.
|
|
37
40
|
const userData = process.env.SWF2ES_DESKTOP_USER_DATA;
|
|
38
41
|
if (userData) {
|
|
@@ -44,140 +47,8 @@ const build = /^[\w.+-]{1,40}$/.test(process.env.SWF2ES_DESKTOP_BUILD ?? "")
|
|
|
44
47
|
: null;
|
|
45
48
|
registerScheme();
|
|
46
49
|
app.enableSandbox();
|
|
47
|
-
/** --trace: what the page logs, a SWF's trace() among it, goes to the terminal. */
|
|
48
|
-
const trace = process.argv.includes("--trace");
|
|
49
|
-
// Each request and socket of the SWF's said on stderr: the page's DevTools show none of them.
|
|
50
|
-
// --log-sockets says what each socket sends and receives as well, and so the rest too.
|
|
51
|
-
const logSockets = process.argv.includes("--log-sockets");
|
|
52
|
-
const logNetwork = logSockets || process.argv.includes("--log-network");
|
|
53
|
-
const netLog = logNetwork ? (line) => process.stderr.write(`${line}\n`) : null;
|
|
54
|
-
const sandbox = new Sandbox();
|
|
55
|
-
/** The SWF's http and https requests, judged as Flash's sandboxes had them. */
|
|
56
|
-
/**
|
|
57
|
-
* --ignore-certificate-errors[=host,...]: HTTPS servers whose certificates go
|
|
58
|
-
* unchecked, for one's own with a certificate it signed itself. For this
|
|
59
|
-
* run alone, never kept, and said aloud: anyone on the way to such a host
|
|
60
|
-
* can read and change what the SWF loads from it.
|
|
61
|
-
*/
|
|
62
|
-
const unverified = unverifiedHosts(process.argv.slice(1));
|
|
63
|
-
if (unverified) {
|
|
64
|
-
process.stderr.write(unverified.all
|
|
65
|
-
? "swf2es: --ignore-certificate-errors: no HTTPS or WSS server's certificate is verified\n"
|
|
66
|
-
: `swf2es: --ignore-certificate-errors: certificates are not verified for ${unverified.hosts.join(", ") || "no host"}\n`);
|
|
67
|
-
}
|
|
68
|
-
const unverifiedHost = (host) => !!unverified && (unverified.all || unverified.hosts.includes(host.toLowerCase()));
|
|
69
|
-
/**
|
|
70
|
-
* --proxy=<url>: what the SWF's requests, sockets and WebSockets go
|
|
71
|
-
* through. For this run alone, never kept, and said aloud, without its
|
|
72
|
-
* credentials. One that does not parse stops the app rather than letting
|
|
73
|
-
* it run without.
|
|
74
|
-
*/
|
|
75
|
-
const proxied = proxyArgument(process.argv.slice(1));
|
|
76
|
-
if (proxied && "error" in proxied) {
|
|
77
|
-
process.stderr.write(`swf2es: ${proxied.error}\n`);
|
|
78
|
-
process.exit(2);
|
|
79
|
-
}
|
|
80
|
-
const proxy = proxied?.proxy ?? null;
|
|
81
|
-
if (proxy) {
|
|
82
|
-
process.stderr.write(`swf2es: --proxy: the SWF's requests, sockets and WebSockets go through ${describeProxy(proxy)}\n`);
|
|
83
|
-
}
|
|
84
|
-
/** What --log-network adds to a line of a connection that went through the proxy. */
|
|
85
|
-
const via = proxy ? " via proxy" : "";
|
|
86
|
-
const warn = (message) => process.stderr.write(`swf2es: ${message}\n`);
|
|
87
|
-
const network = new Network({ unverified: unverifiedHost, proxy });
|
|
88
|
-
// What opens an AIR application's WebSockets: the global's client, a wss: server's certificate
|
|
89
|
-
// unchecked where the switch names its host; or every one through the proxy.
|
|
90
|
-
const insecureWebSockets = unverified && !proxy ? unverifiedDispatcher() : null;
|
|
91
|
-
if (unverified && !proxy && !insecureWebSockets) {
|
|
92
|
-
process.stderr.write("swf2es: --ignore-certificate-errors: a WebSocket's certificate is verified all the same\n");
|
|
93
|
-
}
|
|
94
|
-
const proxiedWebSockets = proxy ? proxiedDispatcher(proxy, unverifiedHost, warn) : null;
|
|
95
|
-
// Never past the proxy: where none can be made, no WebSocket opens.
|
|
96
|
-
const webSocketOpen = proxy
|
|
97
|
-
? proxiedWebSockets
|
|
98
|
-
? dispatchedOpener(proxiedWebSockets)
|
|
99
|
-
: () => {
|
|
100
|
-
throw new Error("no WebSocket through the proxy");
|
|
101
|
-
}
|
|
102
|
-
: insecureWebSockets
|
|
103
|
-
? webSocketOpener(unverifiedHost, insecureWebSockets)
|
|
104
|
-
: undefined;
|
|
105
|
-
if (proxy && !proxiedWebSockets) {
|
|
106
|
-
process.stderr.write("swf2es: --proxy: this Electron's WebSocket cannot go through a proxy\n");
|
|
107
|
-
}
|
|
108
50
|
/** What the page's requests under way abort with, by the preload's number. */
|
|
109
51
|
const requests = new Map();
|
|
110
|
-
let settings;
|
|
111
|
-
/** What the network judges the movie playing as; null for none, or one without networking. */
|
|
112
|
-
let networkMovie = null;
|
|
113
|
-
let window = null;
|
|
114
|
-
/** The dialog that asks for a URL, while it is open. */
|
|
115
|
-
let prompt = null;
|
|
116
|
-
let current = null;
|
|
117
|
-
/** The servers the SWF playing was allowed, or denied, a socket to until it closes: "host:port". */
|
|
118
|
-
let allowedOnce = new Set();
|
|
119
|
-
let deniedOnce = new Set();
|
|
120
|
-
/** The prompts on screen, by "host:port": a second connection there waits on the first's answer. */
|
|
121
|
-
const asking = new Map();
|
|
122
|
-
/**
|
|
123
|
-
* The page has asked to start and listens for what opens: until then, start() hands it the movie.
|
|
124
|
-
*/
|
|
125
|
-
let listening = false;
|
|
126
|
-
/** The longest URL a SWF is opened by. */
|
|
127
|
-
const MAX_URL = 8192;
|
|
128
|
-
/** The AIR application playing, as its command line asked for it; null for none. */
|
|
129
|
-
let air = null;
|
|
130
|
-
/** The EncryptedLocalStore of the AIR application playing, if one is. */
|
|
131
|
-
let encryptedItems = null;
|
|
132
|
-
/**
|
|
133
|
-
* `text` as the URL of a remote SWF: http or https, with a host and no
|
|
134
|
-
* credentials, its fragment dropped; null if it is not one.
|
|
135
|
-
*/
|
|
136
|
-
function remoteUrl(text) {
|
|
137
|
-
if (text.length > MAX_URL) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
let url;
|
|
141
|
-
try {
|
|
142
|
-
url = new URL(text.trim());
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
if ((url.protocol !== "http:" && url.protocol !== "https:") || !url.hostname) {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
if (url.username || url.password) {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
url.hash = "";
|
|
154
|
-
return url;
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* The SWF named on a command line, from `cwd`: the last argument after the
|
|
158
|
-
* app's own that is an http or https URL or names a .swf file there is.
|
|
159
|
-
* Chromium's and Electron's switches may take values of their own
|
|
160
|
-
* (`--switch value`), so the first argument that is not a switch could be one.
|
|
161
|
-
*/
|
|
162
|
-
function movieFromArguments(argv, cwd = process.cwd()) {
|
|
163
|
-
const named = argv
|
|
164
|
-
.slice(process.defaultApp ? 2 : 1)
|
|
165
|
-
.filter((a) => !a.startsWith("-"))
|
|
166
|
-
.map((a) => remoteUrl(a)?.href ?? (/\.swf$/i.test(a) ? resolve(cwd, a) : null))
|
|
167
|
-
.filter((a) => a !== null);
|
|
168
|
-
return (named.reverse().find((a) => remoteUrl(a) || statSync(a, { throwIfNoEntry: false })?.isFile()) ??
|
|
169
|
-
null);
|
|
170
|
-
}
|
|
171
|
-
/** Whether a message comes from the window's own page, on swf2es://app/. */
|
|
172
|
-
function trusted(event) {
|
|
173
|
-
const frame = event.senderFrame;
|
|
174
|
-
return (window !== null &&
|
|
175
|
-
event.sender === window.webContents &&
|
|
176
|
-
frame !== null &&
|
|
177
|
-
frame.parent === null &&
|
|
178
|
-
// Node's URL gives a scheme it does not know no origin: compare the text.
|
|
179
|
-
frame.url.startsWith(`${APP_ORIGIN}/`));
|
|
180
|
-
}
|
|
181
52
|
/** How long after a click or a key a SWF may open a page: Chromium's transient activation. */
|
|
182
53
|
const GESTURE_MS = 5000;
|
|
183
54
|
/** The least time between two pages opened, whatever the gestures. */
|
|
@@ -219,432 +90,6 @@ function openExternal(url) {
|
|
|
219
90
|
openedAt = now;
|
|
220
91
|
void shell.openExternal(url);
|
|
221
92
|
}
|
|
222
|
-
/**
|
|
223
|
-
* How much of a SWF the main process reads to choose its sandbox: far more than FileAttributes
|
|
224
|
-
* takes compressed.
|
|
225
|
-
*/
|
|
226
|
-
const START = 64 * 1024;
|
|
227
|
-
const USE_NETWORK = 0x01;
|
|
228
|
-
/**
|
|
229
|
-
* The first `length` bytes of the file at `path`, or as many as it has, at least `least`; null
|
|
230
|
-
* otherwise.
|
|
231
|
-
*/
|
|
232
|
-
async function readStart(path, length, least = 8) {
|
|
233
|
-
try {
|
|
234
|
-
const file = await openFile(path);
|
|
235
|
-
try {
|
|
236
|
-
const bytes = new Uint8Array(length);
|
|
237
|
-
const { bytesRead } = await file.read(bytes, 0, length, 0);
|
|
238
|
-
return bytesRead >= Math.min(least, length) ? bytes.subarray(0, bytesRead) : null;
|
|
239
|
-
}
|
|
240
|
-
finally {
|
|
241
|
-
await file.close();
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
function refuse(message, detail) {
|
|
249
|
-
if (window) {
|
|
250
|
-
void dialog.showMessageBox(window, { type: "error", message, detail });
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
/** Open what the recent list, the command line or a reload names: a URL, a SWF's path or a descriptor's. */
|
|
254
|
-
function openEntry(entry) {
|
|
255
|
-
if (remoteUrl(entry)) {
|
|
256
|
-
return openUrl(entry);
|
|
257
|
-
}
|
|
258
|
-
// A descriptor reopened from the recent list or a reload, with the arguments it was run with.
|
|
259
|
-
if (/\.xml$/i.test(entry)) {
|
|
260
|
-
return openAir(air?.descriptor === entry ? air : { descriptor: entry, root: null, arguments: [] }, process.cwd());
|
|
261
|
-
}
|
|
262
|
-
return open(entry);
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Run the AIR application `command` names, as adl does: its content SWF in
|
|
266
|
-
* the window, at the descriptor's size, in AIR's application sandbox, with
|
|
267
|
-
* its launch (shared/api.ts AirLaunch), from `cwd`.
|
|
268
|
-
*/
|
|
269
|
-
async function openAir(command, cwd) {
|
|
270
|
-
const name = basename(command.descriptor);
|
|
271
|
-
let opened;
|
|
272
|
-
try {
|
|
273
|
-
opened = airLaunch(command, cwd, app.getPath("userData"), {
|
|
274
|
-
documents: app.getPath("documents"),
|
|
275
|
-
desktop: app.getPath("desktop"),
|
|
276
|
-
cache: app.getPath("temp"),
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
catch (error) {
|
|
280
|
-
refuse(`${name} could not be opened.`, String(error));
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
const head = await readStart(opened.content, START);
|
|
284
|
-
const signature = head ? String.fromCharCode(...head.subarray(0, 3)) : "";
|
|
285
|
-
if (!head || (signature !== "FWS" && signature !== "CWS" && signature !== "ZWS")) {
|
|
286
|
-
refuse(`${name}'s content is not a SWF.`, opened.content);
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
const content = await realpath(opened.content);
|
|
290
|
-
sandbox.playApplication(content);
|
|
291
|
-
air = command;
|
|
292
|
-
// Beside its storage directory, by its id, as AIR keeps it; encrypted by the system's keychain where it has one.
|
|
293
|
-
encryptedItems = new EncryptedItems(join(dirname(opened.launch.directories.applicationStorage), "EncryptedLocalStore"), safeStorage.isEncryptionAvailable()
|
|
294
|
-
? {
|
|
295
|
-
encrypt: (text) => new Uint8Array(safeStorage.encryptString(text)),
|
|
296
|
-
decrypt: (data) => safeStorage.decryptString(Buffer.from(data)),
|
|
297
|
-
}
|
|
298
|
-
: null);
|
|
299
|
-
if (window && opened.width && opened.height) {
|
|
300
|
-
window.setSize(opened.width, opened.height);
|
|
301
|
-
}
|
|
302
|
-
// Played from app:/, as AIR plays it: its relative URLs, and loaderInfo.url, are the application's.
|
|
303
|
-
const url = `app:/${opened.relative
|
|
304
|
-
.split("/")
|
|
305
|
-
.filter((s) => s !== "")
|
|
306
|
-
.map(encodeURIComponent)
|
|
307
|
-
.join("/")}`;
|
|
308
|
-
show(command.descriptor, { url, name: opened.title, sandbox: "application", air: opened.launch }, { kind: "application" });
|
|
309
|
-
}
|
|
310
|
-
/** Play `movie` in the window, in place of what plays: what the user opened, as `entry`. */
|
|
311
|
-
function show(entry, movie, onNetwork) {
|
|
312
|
-
current = { path: entry, movie };
|
|
313
|
-
playOnNetwork(onNetwork);
|
|
314
|
-
allowedOnce = new Set();
|
|
315
|
-
deniedOnce = new Set();
|
|
316
|
-
settings.opened(entry);
|
|
317
|
-
buildMenu();
|
|
318
|
-
if (window) {
|
|
319
|
-
// An AIR application's window is its own: its title alone, as adl shows it.
|
|
320
|
-
window.setTitle(movie.air ? movie.name : `${movie.name} — ${TITLE}`);
|
|
321
|
-
if (listening) {
|
|
322
|
-
window.webContents.send("desktop:open", movie);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Play the SWF at `text`, an http or https URL, in Flash's remote sandbox:
|
|
328
|
-
* no local file, and the network as its origin's (network.ts). The page
|
|
329
|
-
* fetches it through the main process like anything it loads.
|
|
330
|
-
*/
|
|
331
|
-
async function openUrl(text) {
|
|
332
|
-
const url = remoteUrl(text);
|
|
333
|
-
if (!url) {
|
|
334
|
-
refuse("Only an http or https URL opens.", text.slice(0, 200));
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
const last = url.pathname.split("/").pop() ?? "";
|
|
338
|
-
let name;
|
|
339
|
-
try {
|
|
340
|
-
name = decodeURIComponent(last) || url.host;
|
|
341
|
-
}
|
|
342
|
-
catch {
|
|
343
|
-
name = last;
|
|
344
|
-
}
|
|
345
|
-
air = null;
|
|
346
|
-
sandbox.playRemote(url.href);
|
|
347
|
-
show(url.href, { url: url.href, name, sandbox: "remote" }, { kind: "remote", url: url.href });
|
|
348
|
-
}
|
|
349
|
-
/**
|
|
350
|
-
* Open the file the user chose in the dialog, dropped on the window or
|
|
351
|
-
* opened with the app: an AIR application's descriptor (`.xml`) runs the
|
|
352
|
-
* application, anything else is played as a SWF.
|
|
353
|
-
*/
|
|
354
|
-
async function openChosen(path) {
|
|
355
|
-
if (!/\.xml$/i.test(path)) {
|
|
356
|
-
return open(path);
|
|
357
|
-
}
|
|
358
|
-
// An application has an installed program's reach, which a file chosen in
|
|
359
|
-
// passing should not get unasked: asked once, until it leaves the recent list.
|
|
360
|
-
if (window && !settings.settings.recent.includes(path)) {
|
|
361
|
-
const { response } = await dialog.showMessageBox(window, {
|
|
362
|
-
type: "question",
|
|
363
|
-
message: `Run ${basename(path)} as an AIR application?`,
|
|
364
|
-
detail: "An AIR application reads and writes your files and reaches the network unasked, as an installed program does. Run only one you trust.",
|
|
365
|
-
buttons: ["Run", "Cancel"],
|
|
366
|
-
defaultId: 1,
|
|
367
|
-
cancelId: 1,
|
|
368
|
-
});
|
|
369
|
-
if (response !== 0) {
|
|
370
|
-
return;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
// Run from its own directory, with no arguments, as one started by its icon is.
|
|
374
|
-
return openAir({ descriptor: path, root: null, arguments: [] }, dirname(path));
|
|
375
|
-
}
|
|
376
|
-
/** Play the SWF at `path` in the window, in place of what plays, in its sandbox. */
|
|
377
|
-
async function open(path) {
|
|
378
|
-
const name = basename(path);
|
|
379
|
-
// Its start alone: the main process never decompresses a SWF whole, whose
|
|
380
|
-
// header may name 4 GB. FileAttributes, the first tag, lies in it.
|
|
381
|
-
const head = await readStart(path, START);
|
|
382
|
-
const signature = head ? String.fromCharCode(...head.subarray(0, 3)) : "";
|
|
383
|
-
if (!head || (signature !== "FWS" && signature !== "CWS" && signature !== "ZWS")) {
|
|
384
|
-
refuse(`${name} is not a SWF.`, path);
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
const useNetwork = (fileAttributes(head) & USE_NETWORK) !== 0;
|
|
388
|
-
let url;
|
|
389
|
-
try {
|
|
390
|
-
air = null;
|
|
391
|
-
url = sandbox.play(path, useNetwork);
|
|
392
|
-
}
|
|
393
|
-
catch (error) {
|
|
394
|
-
// Gone, or unreadable, since it was read.
|
|
395
|
-
refuse(`${name} could not be opened.`, String(error));
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
app.addRecentDocument(path);
|
|
399
|
-
show(path, { url, name, sandbox: useNetwork ? "localWithNetwork" : "localWithFile" }, useNetwork ? { kind: "local" } : null);
|
|
400
|
-
}
|
|
401
|
-
/** Judge requests for `movie` from now on: the last one's are aborted. */
|
|
402
|
-
function playOnNetwork(movie) {
|
|
403
|
-
networkMovie = movie;
|
|
404
|
-
network.play(movie);
|
|
405
|
-
}
|
|
406
|
-
function closeMovie() {
|
|
407
|
-
current = null;
|
|
408
|
-
air = null;
|
|
409
|
-
sandbox.stop();
|
|
410
|
-
playOnNetwork(null);
|
|
411
|
-
buildMenu();
|
|
412
|
-
window?.setTitle(TITLE);
|
|
413
|
-
if (listening) {
|
|
414
|
-
window?.webContents.send("desktop:close");
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Whether the SWF playing may connect a socket to `host`:`port`, and where
|
|
419
|
-
* to: only with the network, and only as the user allows, once or always
|
|
420
|
-
* for that SWF, when it first asks for that server; and for a remote SWF,
|
|
421
|
-
* as that server's socket policy allows, at the address it came from.
|
|
422
|
-
* Denied where no one can be asked.
|
|
423
|
-
*/
|
|
424
|
-
async function permitSocket(host, port) {
|
|
425
|
-
// AIR's application sandbox connects where it likes, unasked, with no socket policy.
|
|
426
|
-
if (sandbox.application) {
|
|
427
|
-
return {};
|
|
428
|
-
}
|
|
429
|
-
const playing = current;
|
|
430
|
-
const swf = sandbox.swf;
|
|
431
|
-
const endpoint = `${host}:${port}`;
|
|
432
|
-
if (!playing || !swf || !sandbox.networkAllowed()) {
|
|
433
|
-
process.stderr.write(`swf2es: no socket to ${endpoint}: the SWF has no network\n`);
|
|
434
|
-
return null;
|
|
435
|
-
}
|
|
436
|
-
if (!(await allowedByUser(playing, swf, endpoint))) {
|
|
437
|
-
return null;
|
|
438
|
-
}
|
|
439
|
-
if (networkMovie?.kind !== "remote") {
|
|
440
|
-
return {};
|
|
441
|
-
}
|
|
442
|
-
try {
|
|
443
|
-
return { address: await network.socketAddress(host, port) };
|
|
444
|
-
}
|
|
445
|
-
catch (error) {
|
|
446
|
-
process.stderr.write(`swf2es: no socket to ${endpoint}: ${error.message}\n`);
|
|
447
|
-
return null;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
/** Whether the user allows the SWF playing a socket to `endpoint`, asked once a server. */
|
|
451
|
-
async function allowedByUser(playing, swf, endpoint) {
|
|
452
|
-
if (settings.socketAllowed(swf, endpoint) || allowedOnce.has(endpoint)) {
|
|
453
|
-
return true;
|
|
454
|
-
}
|
|
455
|
-
if (deniedOnce.has(endpoint)) {
|
|
456
|
-
return false;
|
|
457
|
-
}
|
|
458
|
-
let answer = asking.get(endpoint);
|
|
459
|
-
if (!answer) {
|
|
460
|
-
answer = askForSocket(playing.movie.name, swf, endpoint).finally(() => asking.delete(endpoint));
|
|
461
|
-
asking.set(endpoint, answer);
|
|
462
|
-
}
|
|
463
|
-
return (await answer) && current === playing;
|
|
464
|
-
}
|
|
465
|
-
async function askForSocket(name, swf, endpoint) {
|
|
466
|
-
if (!window) {
|
|
467
|
-
return false;
|
|
468
|
-
}
|
|
469
|
-
const playing = current;
|
|
470
|
-
let response;
|
|
471
|
-
try {
|
|
472
|
-
({ response } = await dialog.showMessageBox(window, {
|
|
473
|
-
type: "question",
|
|
474
|
-
message: `${name} asks to connect to ${endpoint}.`,
|
|
475
|
-
detail: "A socket lets the SWF send that server whatever it has. " +
|
|
476
|
-
"Allow it only for a server you trust.",
|
|
477
|
-
buttons: ["Allow Once", "Always Allow for This SWF", "Deny"],
|
|
478
|
-
defaultId: 2,
|
|
479
|
-
cancelId: 2,
|
|
480
|
-
noLink: true,
|
|
481
|
-
}));
|
|
482
|
-
}
|
|
483
|
-
catch {
|
|
484
|
-
return false;
|
|
485
|
-
}
|
|
486
|
-
if (current !== playing) {
|
|
487
|
-
return false;
|
|
488
|
-
}
|
|
489
|
-
switch (response) {
|
|
490
|
-
case 0:
|
|
491
|
-
allowedOnce.add(endpoint);
|
|
492
|
-
return true;
|
|
493
|
-
case 1:
|
|
494
|
-
if (!settings.allowSocket(swf, endpoint)) {
|
|
495
|
-
allowedOnce.add(endpoint);
|
|
496
|
-
}
|
|
497
|
-
return true;
|
|
498
|
-
default:
|
|
499
|
-
deniedOnce.add(endpoint);
|
|
500
|
-
return false;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* Whether what plays may open a WebSocket to `url`: an AIR application,
|
|
505
|
-
* unasked and anywhere, as AIR's application sandbox connects and as its
|
|
506
|
-
* sockets do here. air.net.WebSocket is AIR's alone, so nothing else has one.
|
|
507
|
-
*/
|
|
508
|
-
function permitWebSocket(url) {
|
|
509
|
-
if (sandbox.application && hasWebSocket() && (!proxy || proxiedWebSockets)) {
|
|
510
|
-
return true;
|
|
511
|
-
}
|
|
512
|
-
const why = !sandbox.application
|
|
513
|
-
? "only an AIR application has one"
|
|
514
|
-
: !hasWebSocket()
|
|
515
|
-
? "this Electron has no WebSocket client"
|
|
516
|
-
: "this Electron's WebSocket cannot go through the proxy";
|
|
517
|
-
process.stderr.write(`swf2es: no WebSocket to ${url.slice(0, 2048)}: ${why}\n`);
|
|
518
|
-
return false;
|
|
519
|
-
}
|
|
520
|
-
async function showOpenDialog() {
|
|
521
|
-
if (!window) {
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
const result = await dialog.showOpenDialog(window, {
|
|
525
|
-
title: "Open a SWF or an AIR application",
|
|
526
|
-
properties: ["openFile"],
|
|
527
|
-
filters: [
|
|
528
|
-
{ name: "SWF and AIR application descriptors", extensions: ["swf", "xml"] },
|
|
529
|
-
{ name: "All files", extensions: ["*"] },
|
|
530
|
-
],
|
|
531
|
-
});
|
|
532
|
-
if (!result.canceled && result.filePaths[0]) {
|
|
533
|
-
await openChosen(result.filePaths[0]);
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
/** Ask for a library, check that it is ABC, keep its path, and start the page again with it. */
|
|
537
|
-
async function chooseLibrary(name) {
|
|
538
|
-
if (!window) {
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
const result = await dialog.showOpenDialog(window, {
|
|
542
|
-
title: `Choose ${name}.abc`,
|
|
543
|
-
properties: ["openFile"],
|
|
544
|
-
filters: [
|
|
545
|
-
{ name: "ActionScript bytecode", extensions: ["abc"] },
|
|
546
|
-
{ name: "All files", extensions: ["*"] },
|
|
547
|
-
],
|
|
548
|
-
});
|
|
549
|
-
const path = result.filePaths[0];
|
|
550
|
-
if (result.canceled || !path) {
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
// An ABC starts with its minor version, then its major one, 46.
|
|
554
|
-
const head = await readStart(path, 4);
|
|
555
|
-
if (head?.[2] !== 46 || head[3] !== 0) {
|
|
556
|
-
refuse(`${basename(path)} is not an ABC file.`, `swf2es needs ${name}.abc itself.`);
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
if (!settings.setLibrary(name, path)) {
|
|
560
|
-
refuse("The settings could not be saved.", `swf2es uses ${path} until it quits.`);
|
|
561
|
-
}
|
|
562
|
-
buildMenu();
|
|
563
|
-
// The page fetched the libraries once; a fresh page fetches the new one.
|
|
564
|
-
window.webContents.reload();
|
|
565
|
-
}
|
|
566
|
-
/** Play on the library the app ships again, in place of one the user chose. */
|
|
567
|
-
function resetLibrary(name) {
|
|
568
|
-
if (!settings.clearLibrary(name)) {
|
|
569
|
-
refuse("The settings could not be saved.", `swf2es uses its own ${name} until it quits.`);
|
|
570
|
-
}
|
|
571
|
-
buildMenu();
|
|
572
|
-
window?.webContents.reload();
|
|
573
|
-
}
|
|
574
|
-
function buildMenu() {
|
|
575
|
-
const recent = settings.settings.recent;
|
|
576
|
-
const template = [
|
|
577
|
-
...(process.platform === "darwin" ? [{ role: "appMenu" }] : []),
|
|
578
|
-
{
|
|
579
|
-
label: "&File",
|
|
580
|
-
submenu: [
|
|
581
|
-
{ label: "&Open…", accelerator: "CmdOrCtrl+O", click: () => void showOpenDialog() },
|
|
582
|
-
{ label: "Open &URL…", accelerator: "CmdOrCtrl+L", click: showUrlDialog },
|
|
583
|
-
{
|
|
584
|
-
label: "Open &Recent",
|
|
585
|
-
submenu: [
|
|
586
|
-
...recent.map((entry) => ({ label: entry, click: () => void openEntry(entry) })),
|
|
587
|
-
...(recent.length > 0 ? [{ type: "separator" }] : []),
|
|
588
|
-
{
|
|
589
|
-
label: "Clear Recent",
|
|
590
|
-
enabled: recent.length > 0,
|
|
591
|
-
click: () => {
|
|
592
|
-
settings.clearRecent();
|
|
593
|
-
app.clearRecentDocuments();
|
|
594
|
-
buildMenu();
|
|
595
|
-
},
|
|
596
|
-
},
|
|
597
|
-
],
|
|
598
|
-
},
|
|
599
|
-
{
|
|
600
|
-
id: "close-movie",
|
|
601
|
-
label: "&Close Movie",
|
|
602
|
-
accelerator: "CmdOrCtrl+W",
|
|
603
|
-
enabled: !!current,
|
|
604
|
-
click: closeMovie,
|
|
605
|
-
},
|
|
606
|
-
{ type: "separator" },
|
|
607
|
-
{ label: "Choose playerglobal.abc…", click: () => void chooseLibrary("playerglobal") },
|
|
608
|
-
{
|
|
609
|
-
label: "Use the App's playerglobal",
|
|
610
|
-
enabled: !!settings.settings.playerglobal,
|
|
611
|
-
click: () => resetLibrary("playerglobal"),
|
|
612
|
-
},
|
|
613
|
-
{ type: "separator" },
|
|
614
|
-
{ role: "quit" },
|
|
615
|
-
],
|
|
616
|
-
},
|
|
617
|
-
{
|
|
618
|
-
label: "&View",
|
|
619
|
-
submenu: [
|
|
620
|
-
{
|
|
621
|
-
id: "reload-movie",
|
|
622
|
-
label: "&Reload Movie",
|
|
623
|
-
accelerator: "CmdOrCtrl+R",
|
|
624
|
-
enabled: !!current,
|
|
625
|
-
click: () => current && void openEntry(current.path),
|
|
626
|
-
},
|
|
627
|
-
{ role: "togglefullscreen", accelerator: "F11" },
|
|
628
|
-
{ type: "separator" },
|
|
629
|
-
{
|
|
630
|
-
id: "stage3d",
|
|
631
|
-
label: "Stage3D (Experimental)",
|
|
632
|
-
type: "checkbox",
|
|
633
|
-
checked: settings.settings.stage3D,
|
|
634
|
-
click: (item) => {
|
|
635
|
-
if (!settings.setStage3D(item.checked)) {
|
|
636
|
-
refuse("The settings could not be saved.", "The Stage3D choice holds until you quit.");
|
|
637
|
-
}
|
|
638
|
-
window?.webContents.reload();
|
|
639
|
-
},
|
|
640
|
-
},
|
|
641
|
-
{ type: "separator" },
|
|
642
|
-
{ role: "toggleDevTools" },
|
|
643
|
-
],
|
|
644
|
-
},
|
|
645
|
-
];
|
|
646
|
-
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
647
|
-
}
|
|
648
93
|
/**
|
|
649
94
|
* The window's page may go nowhere but swf2es://app/, and open nothing but the system's browser.
|
|
650
95
|
*/
|
|
@@ -664,7 +109,7 @@ function guard(contents) {
|
|
|
664
109
|
});
|
|
665
110
|
contents.on("did-start-navigation", (details) => {
|
|
666
111
|
if (details.isMainFrame && !details.isSameDocument) {
|
|
667
|
-
|
|
112
|
+
setListening(false);
|
|
668
113
|
// The page's requests go with it; the next page asks for the movie again.
|
|
669
114
|
network.play(networkMovie);
|
|
670
115
|
requests.clear();
|
|
@@ -677,63 +122,8 @@ function guard(contents) {
|
|
|
677
122
|
});
|
|
678
123
|
}
|
|
679
124
|
}
|
|
680
|
-
/**
|
|
681
|
-
* The dialog that asks for a SWF's URL: a window of its own, apart from the
|
|
682
|
-
* page, so that only the user, and never a SWF, opens one by its URL.
|
|
683
|
-
*/
|
|
684
|
-
function showUrlDialog() {
|
|
685
|
-
if (!window) {
|
|
686
|
-
return;
|
|
687
|
-
}
|
|
688
|
-
if (prompt) {
|
|
689
|
-
prompt.focus();
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
const dialogWindow = new BrowserWindow({
|
|
693
|
-
parent: window,
|
|
694
|
-
modal: true,
|
|
695
|
-
width: 560,
|
|
696
|
-
height: 180,
|
|
697
|
-
resizable: false,
|
|
698
|
-
minimizable: false,
|
|
699
|
-
maximizable: false,
|
|
700
|
-
fullscreenable: false,
|
|
701
|
-
title: "Open URL",
|
|
702
|
-
show: false,
|
|
703
|
-
webPreferences: {
|
|
704
|
-
preload: promptPreload,
|
|
705
|
-
contextIsolation: true,
|
|
706
|
-
sandbox: true,
|
|
707
|
-
nodeIntegration: false,
|
|
708
|
-
webSecurity: true,
|
|
709
|
-
spellcheck: false,
|
|
710
|
-
},
|
|
711
|
-
});
|
|
712
|
-
prompt = dialogWindow;
|
|
713
|
-
dialogWindow.setMenu(null);
|
|
714
|
-
dialogWindow.on("page-title-updated", (event) => event.preventDefault());
|
|
715
|
-
dialogWindow.once("ready-to-show", () => dialogWindow.show());
|
|
716
|
-
dialogWindow.on("closed", () => {
|
|
717
|
-
if (prompt === dialogWindow) {
|
|
718
|
-
prompt = null;
|
|
719
|
-
}
|
|
720
|
-
});
|
|
721
|
-
const contents = dialogWindow.webContents;
|
|
722
|
-
contents.on("will-navigate", (event) => event.preventDefault());
|
|
723
|
-
contents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
724
|
-
void dialogWindow.loadURL(PROMPT_URL);
|
|
725
|
-
}
|
|
726
|
-
/** Whether a message comes from the URL dialog's own page. */
|
|
727
|
-
function fromPrompt(event) {
|
|
728
|
-
const frame = event.senderFrame;
|
|
729
|
-
return (prompt !== null &&
|
|
730
|
-
event.sender === prompt.webContents &&
|
|
731
|
-
frame !== null &&
|
|
732
|
-
frame.parent === null &&
|
|
733
|
-
frame.url === PROMPT_URL);
|
|
734
|
-
}
|
|
735
125
|
function createWindow() {
|
|
736
|
-
window = new BrowserWindow({
|
|
126
|
+
const window = new BrowserWindow({
|
|
737
127
|
width: 1024,
|
|
738
128
|
height: 768,
|
|
739
129
|
title: TITLE,
|
|
@@ -747,10 +137,11 @@ function createWindow() {
|
|
|
747
137
|
spellcheck: false,
|
|
748
138
|
},
|
|
749
139
|
});
|
|
140
|
+
setWindow(window);
|
|
750
141
|
// The title is the movie's, set here, not the page's <title>.
|
|
751
142
|
window.on("page-title-updated", (event) => event.preventDefault());
|
|
752
143
|
window.on("closed", () => {
|
|
753
|
-
|
|
144
|
+
setWindow(null);
|
|
754
145
|
});
|
|
755
146
|
// The page hears of full screen however it came, a SWF's Stage.displayState or the menu's F11.
|
|
756
147
|
window.on("enter-full-screen", () => window?.webContents.send("desktop:full-screen", true));
|
|
@@ -770,9 +161,9 @@ function listen() {
|
|
|
770
161
|
if (!trusted(event)) {
|
|
771
162
|
throw new Error("not the app's page");
|
|
772
163
|
}
|
|
773
|
-
|
|
164
|
+
setListening(true);
|
|
774
165
|
return {
|
|
775
|
-
stage3D: settings.
|
|
166
|
+
stage3D: settings.stage3D,
|
|
776
167
|
libraries: settings.libraries(),
|
|
777
168
|
movie: current?.movie ?? null,
|
|
778
169
|
build,
|
|
@@ -850,162 +241,6 @@ function listen() {
|
|
|
850
241
|
});
|
|
851
242
|
listenForAir();
|
|
852
243
|
}
|
|
853
|
-
/** The window as an AIR application's NativeWindow reads it. */
|
|
854
|
-
function windowState(w) {
|
|
855
|
-
const bounds = w.getBounds();
|
|
856
|
-
const content = w.getContentBounds();
|
|
857
|
-
return {
|
|
858
|
-
bounds,
|
|
859
|
-
contentOffset: { x: content.x - bounds.x, y: content.y - bounds.y },
|
|
860
|
-
title: w.getTitle(),
|
|
861
|
-
displayState: w.isMinimized() ? "minimized" : w.isMaximized() ? "maximized" : "normal",
|
|
862
|
-
active: w.isFocused(),
|
|
863
|
-
visible: w.isVisible(),
|
|
864
|
-
};
|
|
865
|
-
}
|
|
866
|
-
/** The displays as an AIR application's Screen reads them: the work area is past the system's bars. */
|
|
867
|
-
function screenStates() {
|
|
868
|
-
const primary = screen.getPrimaryDisplay().id;
|
|
869
|
-
return screen.getAllDisplays().map((display) => ({
|
|
870
|
-
bounds: display.bounds,
|
|
871
|
-
visibleBounds: display.workArea,
|
|
872
|
-
colorDepth: display.colorDepth,
|
|
873
|
-
main: display.id === primary,
|
|
874
|
-
}));
|
|
875
|
-
}
|
|
876
|
-
/**
|
|
877
|
-
* An AIR application's reach, shared/api.ts DesktopAir: the file system
|
|
878
|
-
* and the window, only while one the user opened plays and only for its
|
|
879
|
-
* page. The file system is read synchronously where File reads it so.
|
|
880
|
-
*/
|
|
881
|
-
function listenForAir() {
|
|
882
|
-
const playing = (event) => trusted(event) && sandbox.application;
|
|
883
|
-
ipcMain.on("air:stat", (event, path) => {
|
|
884
|
-
event.returnValue = playing(event) && typeof path === "string" ? statOf(path) : null;
|
|
885
|
-
});
|
|
886
|
-
ipcMain.on("air:list", (event, path) => {
|
|
887
|
-
event.returnValue = playing(event) && typeof path === "string" ? listOf(path) : null;
|
|
888
|
-
});
|
|
889
|
-
// FileStream's open, which reads a file at once, as stat does: only once the application plays.
|
|
890
|
-
ipcMain.on("air:read-sync", (event, path) => {
|
|
891
|
-
event.returnValue = playing(event) && typeof path === "string" ? readOf(path) : null;
|
|
892
|
-
});
|
|
893
|
-
// EncryptedLocalStore, at once as its methods are: only the application playing's.
|
|
894
|
-
const items = (event) => (playing(event) ? encryptedItems : null);
|
|
895
|
-
ipcMain.on("air:item-get", (event, name) => {
|
|
896
|
-
event.returnValue = typeof name === "string" ? (items(event)?.get(name) ?? null) : null;
|
|
897
|
-
});
|
|
898
|
-
ipcMain.on("air:item-set", (event, name, bytes) => {
|
|
899
|
-
if (typeof name === "string" && bytes instanceof Uint8Array) {
|
|
900
|
-
items(event)?.set(name, bytes);
|
|
901
|
-
}
|
|
902
|
-
event.returnValue = null;
|
|
903
|
-
});
|
|
904
|
-
ipcMain.on("air:item-remove", (event, name) => {
|
|
905
|
-
if (typeof name === "string") {
|
|
906
|
-
items(event)?.remove(name);
|
|
907
|
-
}
|
|
908
|
-
event.returnValue = null;
|
|
909
|
-
});
|
|
910
|
-
ipcMain.on("air:items-reset", (event) => {
|
|
911
|
-
items(event)?.reset();
|
|
912
|
-
event.returnValue = null;
|
|
913
|
-
});
|
|
914
|
-
// FileStream's writes and File's changes, at once as adl's: only once the application plays.
|
|
915
|
-
ipcMain.on("air:write-sync", (event, path, bytes, offset, length) => {
|
|
916
|
-
event.returnValue =
|
|
917
|
-
playing(event) &&
|
|
918
|
-
typeof path === "string" &&
|
|
919
|
-
bytes instanceof Uint8Array &&
|
|
920
|
-
Number.isSafeInteger(offset) &&
|
|
921
|
-
Number.isSafeInteger(length) &&
|
|
922
|
-
offset >= 0 &&
|
|
923
|
-
length >= 0 &&
|
|
924
|
-
writeOf(path, bytes, offset, length);
|
|
925
|
-
});
|
|
926
|
-
ipcMain.on("air:create-directory", (event, path) => {
|
|
927
|
-
event.returnValue = playing(event) && typeof path === "string" && createDirectoryOf(path);
|
|
928
|
-
});
|
|
929
|
-
ipcMain.on("air:remove", (event, path) => {
|
|
930
|
-
event.returnValue = playing(event) && typeof path === "string" && removeOf(path);
|
|
931
|
-
});
|
|
932
|
-
ipcMain.on("air:move", (event, from, to) => {
|
|
933
|
-
event.returnValue =
|
|
934
|
-
playing(event) && typeof from === "string" && typeof to === "string" && moveOf(from, to);
|
|
935
|
-
});
|
|
936
|
-
// A read is what starts the application's grant: the page reads its content SWF first.
|
|
937
|
-
ipcMain.handle("air:read", async (event, path) => {
|
|
938
|
-
if (!trusted(event) || typeof path !== "string" || !air) {
|
|
939
|
-
return null;
|
|
940
|
-
}
|
|
941
|
-
try {
|
|
942
|
-
const real = await realpath(resolveCase(path));
|
|
943
|
-
return sandbox.startApplication(real) ? new Uint8Array(await readFile(real)) : null;
|
|
944
|
-
}
|
|
945
|
-
catch {
|
|
946
|
-
return null;
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
ipcMain.on("air:window", (event) => {
|
|
950
|
-
event.returnValue = playing(event) && window ? windowState(window) : null;
|
|
951
|
-
});
|
|
952
|
-
ipcMain.on("air:screens", (event) => {
|
|
953
|
-
event.returnValue = playing(event) ? screenStates() : [];
|
|
954
|
-
});
|
|
955
|
-
ipcMain.on("air:set-bounds", (event, bounds) => {
|
|
956
|
-
const b = bounds;
|
|
957
|
-
const n = (v) => (typeof v === "number" && Number.isFinite(v) ? Math.round(v) : null);
|
|
958
|
-
const [x, y, width, height] = [n(b?.x), n(b?.y), n(b?.width), n(b?.height)];
|
|
959
|
-
if (playing(event) &&
|
|
960
|
-
window &&
|
|
961
|
-
x !== null &&
|
|
962
|
-
y !== null &&
|
|
963
|
-
width &&
|
|
964
|
-
height &&
|
|
965
|
-
width > 0 &&
|
|
966
|
-
height > 0) {
|
|
967
|
-
window.setBounds({ x, y, width, height });
|
|
968
|
-
}
|
|
969
|
-
});
|
|
970
|
-
ipcMain.on("air:set-title", (event, title) => {
|
|
971
|
-
if (playing(event) && window && typeof title === "string") {
|
|
972
|
-
window.setTitle(title.slice(0, 1024));
|
|
973
|
-
}
|
|
974
|
-
});
|
|
975
|
-
ipcMain.on("air:window-do", (event, what) => {
|
|
976
|
-
if (!playing(event) || !window) {
|
|
977
|
-
return;
|
|
978
|
-
}
|
|
979
|
-
switch (what) {
|
|
980
|
-
case "minimize":
|
|
981
|
-
window.minimize();
|
|
982
|
-
break;
|
|
983
|
-
case "maximize":
|
|
984
|
-
window.maximize();
|
|
985
|
-
break;
|
|
986
|
-
case "restore":
|
|
987
|
-
window.restore();
|
|
988
|
-
break;
|
|
989
|
-
case "activate":
|
|
990
|
-
window.focus();
|
|
991
|
-
break;
|
|
992
|
-
case "show":
|
|
993
|
-
window.show();
|
|
994
|
-
break;
|
|
995
|
-
case "hide":
|
|
996
|
-
window.hide();
|
|
997
|
-
break;
|
|
998
|
-
case "close":
|
|
999
|
-
window.close();
|
|
1000
|
-
break;
|
|
1001
|
-
}
|
|
1002
|
-
});
|
|
1003
|
-
ipcMain.on("air:exit", (event, code) => {
|
|
1004
|
-
if (playing(event)) {
|
|
1005
|
-
app.exit(typeof code === "number" ? code | 0 : 0);
|
|
1006
|
-
}
|
|
1007
|
-
});
|
|
1008
|
-
}
|
|
1009
244
|
/**
|
|
1010
245
|
* A request of the page's: the SWF's own, as the movie opened by its URL,
|
|
1011
246
|
* or what the SWF playing loads, only with the network, as network.ts
|
|
@@ -1103,7 +338,7 @@ void app.whenReady().then(() => {
|
|
|
1103
338
|
if (!first) {
|
|
1104
339
|
return;
|
|
1105
340
|
}
|
|
1106
|
-
|
|
341
|
+
setSettings(new SettingsFile(app.getPath("userData")));
|
|
1107
342
|
// A playerglobal chosen once is kept across versions of the app: said at each start, as a
|
|
1108
343
|
// library older or newer than the app's own makes SWFs fail in ways nothing else explains.
|
|
1109
344
|
const chosen = settings.libraries().playerglobal;
|