@swf2es/desktop 0.0.0-stage → 0.1.14
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 +202 -0
- package/README.md +132 -2
- package/dist/launch/electron.d.ts +14 -0
- package/dist/launch/electron.d.ts.map +1 -0
- package/dist/launch/electron.js +40 -0
- package/dist/launch/electron.js.map +1 -0
- package/dist/launch/launch.d.ts +3 -0
- package/dist/launch/launch.d.ts.map +1 -0
- package/dist/launch/launch.js +69 -0
- package/dist/launch/launch.js.map +1 -0
- package/dist/main/addresses.d.ts +7 -0
- package/dist/main/addresses.d.ts.map +1 -0
- package/dist/main/addresses.js +155 -0
- package/dist/main/addresses.js.map +1 -0
- package/dist/main/air.d.ts +71 -0
- package/dist/main/air.d.ts.map +1 -0
- package/dist/main/air.js +216 -0
- package/dist/main/air.js.map +1 -0
- package/dist/main/connections.d.ts +38 -0
- package/dist/main/connections.d.ts.map +1 -0
- package/dist/main/connections.js +103 -0
- package/dist/main/connections.js.map +1 -0
- package/dist/main/decompress.d.ts +51 -0
- package/dist/main/decompress.d.ts.map +1 -0
- package/dist/main/decompress.js +184 -0
- package/dist/main/decompress.js.map +1 -0
- package/dist/main/files.d.ts +9 -0
- package/dist/main/files.d.ts.map +1 -0
- package/dist/main/files.js +120 -0
- package/dist/main/files.js.map +1 -0
- package/dist/main/main.d.ts +2 -0
- package/dist/main/main.d.ts.map +1 -0
- package/dist/main/main.js +979 -0
- package/dist/main/main.js.map +1 -0
- package/dist/main/network.d.ts +182 -0
- package/dist/main/network.d.ts.map +1 -0
- package/dist/main/network.js +909 -0
- package/dist/main/network.js.map +1 -0
- package/dist/main/policy.d.ts +103 -0
- package/dist/main/policy.d.ts.map +1 -0
- package/dist/main/policy.js +481 -0
- package/dist/main/policy.js.map +1 -0
- package/dist/main/protocol.d.ts +14 -0
- package/dist/main/protocol.d.ts.map +1 -0
- package/dist/main/protocol.js +216 -0
- package/dist/main/protocol.js.map +1 -0
- package/dist/main/sandbox.d.ts +67 -0
- package/dist/main/sandbox.d.ts.map +1 -0
- package/dist/main/sandbox.js +182 -0
- package/dist/main/sandbox.js.map +1 -0
- package/dist/main/settings.d.ts +37 -0
- package/dist/main/settings.d.ts.map +1 -0
- package/dist/main/settings.js +88 -0
- package/dist/main/settings.js.map +1 -0
- package/dist/main/sockets.d.ts +10 -0
- package/dist/main/sockets.d.ts.map +1 -0
- package/dist/main/sockets.js +71 -0
- package/dist/main/sockets.js.map +1 -0
- package/dist/preload/preload.cjs +91 -0
- package/dist/preload/preload.cjs.map +1 -0
- package/dist/preload/preload.d.cts +2 -0
- package/dist/preload/preload.d.cts.map +1 -0
- package/dist/preload/prompt.cjs +12 -0
- package/dist/preload/prompt.cjs.map +1 -0
- package/dist/preload/prompt.d.cts +2 -0
- package/dist/preload/prompt.d.cts.map +1 -0
- package/dist/renderer/air.d.ts +14 -0
- package/dist/renderer/air.d.ts.map +1 -0
- package/dist/renderer/air.js +83 -0
- package/dist/renderer/air.js.map +1 -0
- package/dist/renderer/files.d.ts +4 -0
- package/dist/renderer/files.d.ts.map +1 -0
- package/dist/renderer/files.js +58 -0
- package/dist/renderer/files.js.map +1 -0
- package/dist/renderer/index.d.ts +7 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +126 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/renderer/network.d.ts +6 -0
- package/dist/renderer/network.d.ts.map +1 -0
- package/dist/renderer/network.js +39 -0
- package/dist/renderer/network.js.map +1 -0
- package/dist/renderer/open-url.d.ts +10 -0
- package/dist/renderer/open-url.d.ts.map +1 -0
- package/dist/renderer/open-url.js +38 -0
- package/dist/renderer/open-url.js.map +1 -0
- package/dist/renderer/sockets.d.ts +4 -0
- package/dist/renderer/sockets.d.ts.map +1 -0
- package/dist/renderer/sockets.js +36 -0
- package/dist/renderer/sockets.js.map +1 -0
- package/dist/renderer/stage3d.d.ts +3 -0
- package/dist/renderer/stage3d.d.ts.map +1 -0
- package/dist/renderer/stage3d.js +26 -0
- package/dist/renderer/stage3d.js.map +1 -0
- package/dist/shared/api.d.ts +205 -0
- package/dist/shared/api.d.ts.map +1 -0
- package/dist/shared/api.js +5 -0
- package/dist/shared/api.js.map +1 -0
- package/package.json +46 -3
- package/src/launch/electron.ts +44 -0
- package/src/launch/launch.ts +72 -0
- package/src/launch/tsconfig.json +14 -0
- package/src/main/addresses.ts +184 -0
- package/src/main/air.ts +275 -0
- package/src/main/connections.ts +125 -0
- package/src/main/decompress.ts +219 -0
- package/src/main/files.ts +147 -0
- package/src/main/main.ts +1146 -0
- package/src/main/network.ts +1205 -0
- package/src/main/policy.ts +630 -0
- package/src/main/protocol.ts +237 -0
- package/src/main/sandbox.ts +213 -0
- package/src/main/settings.ts +117 -0
- package/src/main/sockets.ts +81 -0
- package/src/main/tsconfig.json +15 -0
- package/src/preload/preload.cts +110 -0
- package/src/preload/prompt.cts +11 -0
- package/src/preload/tsconfig.json +12 -0
- package/src/renderer/air.ts +100 -0
- package/src/renderer/files.ts +65 -0
- package/src/renderer/index.ts +145 -0
- package/src/renderer/network.ts +56 -0
- package/src/renderer/open-url.ts +50 -0
- package/src/renderer/sockets.ts +42 -0
- package/src/renderer/stage3d.ts +27 -0
- package/src/renderer/tsconfig.json +17 -0
- package/src/shared/api.ts +204 -0
- package/src/shared/tsconfig.json +11 -0
- package/static/icon.png +0 -0
- package/static/index.html +22 -0
- package/static/open-url.html +21 -0
- package/static/style.css +96 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The page's one way to the shell, window.swf2esDesktop: a few messages,
|
|
2
|
+
// each checked again by the main process. A sandboxed preload is a
|
|
3
|
+
// CommonJS script that may require only Electron's renderer modules.
|
|
4
|
+
import type {
|
|
5
|
+
DesktopApi,
|
|
6
|
+
LibraryName,
|
|
7
|
+
NetworkResponse,
|
|
8
|
+
OpenedMovie,
|
|
9
|
+
SocketEvent,
|
|
10
|
+
} from "../shared/api.js";
|
|
11
|
+
|
|
12
|
+
const { contextBridge, ipcRenderer, webUtils } = require("electron") as typeof import("electron");
|
|
13
|
+
|
|
14
|
+
let nextSocket = 1;
|
|
15
|
+
let nextRequest = 1;
|
|
16
|
+
/** The requests waiting for their response, by number. */
|
|
17
|
+
const responses = new Map<number, (response: NetworkResponse) => void>();
|
|
18
|
+
ipcRenderer.on("net:response", (_event, id: number, response: NetworkResponse) =>
|
|
19
|
+
responses.get(id)?.(response),
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
const api: DesktopApi = {
|
|
23
|
+
files: {
|
|
24
|
+
browse: (filters, multiple) => ipcRenderer.invoke("files:browse", filters, multiple),
|
|
25
|
+
save: (name) => ipcRenderer.invoke("files:save", name),
|
|
26
|
+
read: (id) => ipcRenderer.invoke("files:read", id),
|
|
27
|
+
write: (id, bytes) => ipcRenderer.invoke("files:write", id, bytes),
|
|
28
|
+
release: (id) => ipcRenderer.send("files:release", id),
|
|
29
|
+
},
|
|
30
|
+
start: () => ipcRenderer.invoke("desktop:start"),
|
|
31
|
+
onOpen: (listener) => {
|
|
32
|
+
ipcRenderer.on("desktop:open", (_event, movie: OpenedMovie) => listener(movie));
|
|
33
|
+
},
|
|
34
|
+
onClose: (listener) => {
|
|
35
|
+
ipcRenderer.on("desktop:close", () => listener());
|
|
36
|
+
},
|
|
37
|
+
openDialog: () => ipcRenderer.send("desktop:open-dialog"),
|
|
38
|
+
openUrlDialog: () => ipcRenderer.send("desktop:open-url-dialog"),
|
|
39
|
+
// The page never sees the path, only the URL the shell gives back.
|
|
40
|
+
openDropped: (file) => ipcRenderer.send("desktop:open-path", webUtils.getPathForFile(file)),
|
|
41
|
+
chooseLibrary: (name: LibraryName) => ipcRenderer.send("desktop:choose-library", name),
|
|
42
|
+
sockets: {
|
|
43
|
+
connect: (host, port) => {
|
|
44
|
+
const id = nextSocket++;
|
|
45
|
+
ipcRenderer.send("socket:connect", id, String(host), Number(port));
|
|
46
|
+
return id;
|
|
47
|
+
},
|
|
48
|
+
send: (id, bytes) => ipcRenderer.send("socket:send", id, bytes),
|
|
49
|
+
close: (id) => ipcRenderer.send("socket:close", id),
|
|
50
|
+
onEvent: (listener) => {
|
|
51
|
+
ipcRenderer.on("socket:event", (_event, id: number, event: SocketEvent) =>
|
|
52
|
+
listener(id, event),
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
network: {
|
|
57
|
+
fetch: (request, cancel) => {
|
|
58
|
+
const id = nextRequest++;
|
|
59
|
+
cancel(() => ipcRenderer.send("net:abort", id));
|
|
60
|
+
// The response comes as a message of its own, before the answer that says it went.
|
|
61
|
+
return new Promise<NetworkResponse>((resolve, reject) => {
|
|
62
|
+
responses.set(id, (response) => {
|
|
63
|
+
responses.delete(id);
|
|
64
|
+
resolve(response);
|
|
65
|
+
});
|
|
66
|
+
ipcRenderer.invoke("net:fetch", id, request).then(
|
|
67
|
+
({ delivered }: { delivered: boolean }) => {
|
|
68
|
+
if (!delivered) {
|
|
69
|
+
responses.delete(id);
|
|
70
|
+
reject(new Error("swf2es: the request was refused"));
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
(error: unknown) => {
|
|
74
|
+
responses.delete(id);
|
|
75
|
+
reject(error);
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
loadPolicyFile: (url) => ipcRenderer.send("net:policy-file", String(url)),
|
|
81
|
+
},
|
|
82
|
+
// Synchronous where File and NativeWindow read: their properties answer at once.
|
|
83
|
+
air: {
|
|
84
|
+
stat: (path) => ipcRenderer.sendSync("air:stat", String(path)),
|
|
85
|
+
list: (path) => ipcRenderer.sendSync("air:list", String(path)),
|
|
86
|
+
read: (path) => ipcRenderer.invoke("air:read", String(path)),
|
|
87
|
+
readSync: (path) => ipcRenderer.sendSync("air:read-sync", String(path)),
|
|
88
|
+
getItem: (name) => ipcRenderer.sendSync("air:item-get", String(name)),
|
|
89
|
+
setItem: (name, bytes) => ipcRenderer.sendSync("air:item-set", String(name), bytes),
|
|
90
|
+
removeItem: (name) => ipcRenderer.sendSync("air:item-remove", String(name)),
|
|
91
|
+
resetItems: () => ipcRenderer.sendSync("air:items-reset"),
|
|
92
|
+
writeSync: (path, bytes, offset, length) =>
|
|
93
|
+
ipcRenderer.sendSync("air:write-sync", String(path), bytes, Number(offset), Number(length)),
|
|
94
|
+
createDirectory: (path) => ipcRenderer.sendSync("air:create-directory", String(path)),
|
|
95
|
+
remove: (path) => ipcRenderer.sendSync("air:remove", String(path)),
|
|
96
|
+
move: (from, to) => ipcRenderer.sendSync("air:move", String(from), String(to)),
|
|
97
|
+
window: () => ipcRenderer.sendSync("air:window"),
|
|
98
|
+
setBounds: (bounds) => ipcRenderer.send("air:set-bounds", bounds),
|
|
99
|
+
setTitle: (title) => ipcRenderer.send("air:set-title", String(title)),
|
|
100
|
+
minimize: () => ipcRenderer.send("air:window-do", "minimize"),
|
|
101
|
+
maximize: () => ipcRenderer.send("air:window-do", "maximize"),
|
|
102
|
+
restore: () => ipcRenderer.send("air:window-do", "restore"),
|
|
103
|
+
activate: () => ipcRenderer.send("air:window-do", "activate"),
|
|
104
|
+
setVisible: (visible) => ipcRenderer.send("air:window-do", visible ? "show" : "hide"),
|
|
105
|
+
close: () => ipcRenderer.send("air:window-do", "close"),
|
|
106
|
+
exit: (code) => ipcRenderer.send("air:exit", Number(code) | 0),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
contextBridge.exposeInMainWorld("swf2esDesktop", api);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The URL dialog's one way to the main process, window.swf2esPrompt: what
|
|
2
|
+
// to show first, and the user's answer. Apart from the player's page and
|
|
3
|
+
// its API, so that nothing a SWF runs can open a URL.
|
|
4
|
+
const { contextBridge, ipcRenderer } = require("electron") as typeof import("electron");
|
|
5
|
+
|
|
6
|
+
contextBridge.exposeInMainWorld("swf2esPrompt", {
|
|
7
|
+
initial: (): Promise<string> => ipcRenderer.invoke("prompt:initial"),
|
|
8
|
+
/** The URL to open, or null for none. */
|
|
9
|
+
answer: (url: string | null) =>
|
|
10
|
+
ipcRenderer.send("prompt:answer", url === null ? null : String(url)),
|
|
11
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": ".",
|
|
5
|
+
"outDir": "../../dist/preload",
|
|
6
|
+
"tsBuildInfoFile": "../../dist/preload/.tsbuildinfo",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"lib": ["ES2022", "DOM"]
|
|
9
|
+
},
|
|
10
|
+
"include": ["*.cts"],
|
|
11
|
+
"references": [{ "path": "../shared" }]
|
|
12
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// An AIR application the shell opened by its descriptor, as the player
|
|
2
|
+
// sees it: NativeApplication's launch and window, and the file system
|
|
3
|
+
// File reads and writes, all through the shell (window.swf2esDesktop.air),
|
|
4
|
+
// which answers only while that application plays. Its SWFs' app:, app-storage:
|
|
5
|
+
// and file: URLs are read from disk the same way (`airPath`).
|
|
6
|
+
import type { AirApplication, EncryptedStoreHost, FileSystemHost } from "@swf2es/player";
|
|
7
|
+
import type { AirLaunch, DesktopAir } from "../shared/api.js";
|
|
8
|
+
|
|
9
|
+
/** The player's view of `launch`: its application and window, and its file system. */
|
|
10
|
+
export function airHosts(
|
|
11
|
+
launch: AirLaunch,
|
|
12
|
+
air: DesktopAir,
|
|
13
|
+
): {
|
|
14
|
+
application: AirApplication;
|
|
15
|
+
fileSystem: FileSystemHost;
|
|
16
|
+
encryptedStore: EncryptedStoreHost;
|
|
17
|
+
} {
|
|
18
|
+
return {
|
|
19
|
+
application: {
|
|
20
|
+
descriptor: launch.descriptor,
|
|
21
|
+
id: launch.id,
|
|
22
|
+
arguments: launch.arguments,
|
|
23
|
+
currentDirectory: launch.currentDirectory,
|
|
24
|
+
window: {
|
|
25
|
+
bounds: () => air.window().bounds,
|
|
26
|
+
setBounds: (bounds) => air.setBounds(bounds),
|
|
27
|
+
title: () => air.window().title,
|
|
28
|
+
setTitle: (title) => air.setTitle(title),
|
|
29
|
+
displayState: () => air.window().displayState,
|
|
30
|
+
minimize: () => air.minimize(),
|
|
31
|
+
maximize: () => air.maximize(),
|
|
32
|
+
restore: () => air.restore(),
|
|
33
|
+
active: () => air.window().active,
|
|
34
|
+
activate: () => air.activate(),
|
|
35
|
+
visible: () => air.window().visible,
|
|
36
|
+
setVisible: (visible) => air.setVisible(visible),
|
|
37
|
+
close: () => air.close(),
|
|
38
|
+
stageOffset: () => air.window().contentOffset,
|
|
39
|
+
},
|
|
40
|
+
exit: (code) => air.exit(code),
|
|
41
|
+
},
|
|
42
|
+
fileSystem: {
|
|
43
|
+
separator: launch.separator,
|
|
44
|
+
directories: launch.directories,
|
|
45
|
+
stat: (path) => air.stat(path),
|
|
46
|
+
list: (path) => air.list(path),
|
|
47
|
+
read: (path) => air.readSync(path),
|
|
48
|
+
// A copy of the bytes alone: IPC would carry all of a view's buffer.
|
|
49
|
+
write: (path, bytes, offset, length) => air.writeSync(path, bytes.slice(), offset, length),
|
|
50
|
+
createDirectory: (path) => air.createDirectory(path),
|
|
51
|
+
remove: (path) => air.remove(path),
|
|
52
|
+
move: (from, to) => air.move(from, to),
|
|
53
|
+
},
|
|
54
|
+
encryptedStore: {
|
|
55
|
+
getItem: (name) => air.getItem(name),
|
|
56
|
+
// A copy of the bytes alone: IPC would carry all of a view's buffer.
|
|
57
|
+
setItem: (name, bytes) => air.setItem(name, bytes.slice()),
|
|
58
|
+
removeItem: (name) => air.removeItem(name),
|
|
59
|
+
reset: () => air.resetItems(),
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The native path an app:, app-storage: or file: URL names in `launch`;
|
|
66
|
+
* null for any other URL. The shell judges what it may read.
|
|
67
|
+
*/
|
|
68
|
+
export function airPath(launch: AirLaunch, url: string): string | null {
|
|
69
|
+
const m = /^(app|app-storage|file):(.*)$/i.exec(url);
|
|
70
|
+
if (!m) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const sep = launch.separator;
|
|
75
|
+
let rest: string;
|
|
76
|
+
try {
|
|
77
|
+
// A query or a fragment is no part of the file's name.
|
|
78
|
+
rest = decodeURIComponent(m[2].replace(/[?#].*$/, ""));
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
switch (m[1].toLowerCase()) {
|
|
84
|
+
case "app":
|
|
85
|
+
return join(launch.directories.application, rest, sep);
|
|
86
|
+
case "app-storage":
|
|
87
|
+
return join(launch.directories.applicationStorage, rest, sep);
|
|
88
|
+
default: {
|
|
89
|
+
// file:///path, or file:///C:/path on Windows.
|
|
90
|
+
const path = rest.replace(/^\/\/[^/]*/, "");
|
|
91
|
+
return sep === "\\" ? path.replace(/^\/([A-Za-z]:)/, "$1").replaceAll("/", "\\") : path;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** `rest`, a URL's path, under `base`, a directory: its segments joined by `sep`. */
|
|
97
|
+
function join(base: string, rest: string, sep: string): string {
|
|
98
|
+
const segments = rest.split("/").filter((s) => s !== "" && s !== ".");
|
|
99
|
+
return [base.replace(/[\\/]+$/, ""), ...segments].join(sep);
|
|
100
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { FileHost } from "@swf2es/player";
|
|
2
|
+
import type { DesktopFiles } from "../shared/api.js";
|
|
3
|
+
|
|
4
|
+
export function shellFiles(files: DesktopFiles): FileHost {
|
|
5
|
+
const collected = new FinalizationRegistry<number>((id) => files.release(id));
|
|
6
|
+
return {
|
|
7
|
+
async browse(filters, multiple, signal) {
|
|
8
|
+
const selected = await files.browse(filters, multiple);
|
|
9
|
+
if (signal.aborted) {
|
|
10
|
+
for (const file of selected) {
|
|
11
|
+
files.release(file.id);
|
|
12
|
+
}
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
signal.addEventListener(
|
|
17
|
+
"abort",
|
|
18
|
+
() => {
|
|
19
|
+
for (const file of selected) {
|
|
20
|
+
files.release(file.id);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{ once: true },
|
|
24
|
+
);
|
|
25
|
+
return selected.map((file) => {
|
|
26
|
+
const selectedFile: import("@swf2es/player").SelectedFile = {
|
|
27
|
+
...file,
|
|
28
|
+
async read(signal) {
|
|
29
|
+
signal.throwIfAborted();
|
|
30
|
+
const bytes = await files.read(file.id);
|
|
31
|
+
signal.throwIfAborted();
|
|
32
|
+
return new Uint8Array(bytes);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
collected.register(selectedFile, file.id);
|
|
36
|
+
return selectedFile;
|
|
37
|
+
});
|
|
38
|
+
},
|
|
39
|
+
async save(name, signal) {
|
|
40
|
+
const file = await files.save(name);
|
|
41
|
+
if (!file) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (signal.aborted) {
|
|
45
|
+
files.release(file.id);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const release = () => files.release(file.id);
|
|
50
|
+
signal.addEventListener("abort", release, { once: true });
|
|
51
|
+
return {
|
|
52
|
+
name: file.name,
|
|
53
|
+
async write(bytes, signal) {
|
|
54
|
+
try {
|
|
55
|
+
signal.throwIfAborted();
|
|
56
|
+
await files.write(file.id, bytes);
|
|
57
|
+
} finally {
|
|
58
|
+
signal.removeEventListener("abort", release);
|
|
59
|
+
release();
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// The desktop app's page: one <swf2es-player> filling the window, and what
|
|
2
|
+
// to do when nothing plays.
|
|
3
|
+
// It reaches the shell only through window.swf2esDesktop (shared/api.ts).
|
|
4
|
+
import { configure, type Swf2esPlayerElement } from "@swf2es/web";
|
|
5
|
+
import type { AirLaunch, DesktopApi, LibraryState, OpenedMovie } from "../shared/api.js";
|
|
6
|
+
import { airHosts, airPath } from "./air.js";
|
|
7
|
+
import { shellFiles } from "./files.js";
|
|
8
|
+
import { shellFetch } from "./network.js";
|
|
9
|
+
import { shellSocketHost } from "./sockets.js";
|
|
10
|
+
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
swf2esDesktop: DesktopApi;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const desktop = window.swf2esDesktop;
|
|
18
|
+
/** The AIR application playing, whose local URLs are read through the shell; null for a SWF. */
|
|
19
|
+
let airLaunch: AirLaunch | null = null;
|
|
20
|
+
|
|
21
|
+
configure({
|
|
22
|
+
cache: true,
|
|
23
|
+
fileHost: shellFiles(desktop.files),
|
|
24
|
+
sockets: shellSocketHost(desktop.sockets),
|
|
25
|
+
fetch: shellFetch(desktop.network, () => {
|
|
26
|
+
const launch = airLaunch;
|
|
27
|
+
return launch
|
|
28
|
+
? (url) => {
|
|
29
|
+
const path = airPath(launch, url);
|
|
30
|
+
return path === null ? null : desktop.air.read(path);
|
|
31
|
+
}
|
|
32
|
+
: null;
|
|
33
|
+
}),
|
|
34
|
+
loadPolicyFile: (url) => desktop.network.loadPolicyFile(url),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const byId = <T extends HTMLElement>(id: string) => document.getElementById(id) as T;
|
|
38
|
+
const player = byId<Swf2esPlayerElement>("player");
|
|
39
|
+
const welcome = byId<HTMLElement>("welcome");
|
|
40
|
+
const failure = byId<HTMLElement>("failure");
|
|
41
|
+
|
|
42
|
+
/** A playerglobal.abc the user chose, which the shell serves, in place of swf2es's own. */
|
|
43
|
+
function useLibraries(state: LibraryState): void {
|
|
44
|
+
configure({
|
|
45
|
+
libraries: {
|
|
46
|
+
playerglobal: state.playerglobal !== null ? "/libraries/playerglobal.abc" : undefined,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function showFailure(message: string | null): void {
|
|
52
|
+
failure.hidden = message === null;
|
|
53
|
+
failure.textContent = message ?? "";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let loadGeneration = 0;
|
|
57
|
+
|
|
58
|
+
function play(movie: OpenedMovie): void {
|
|
59
|
+
const generation = ++loadGeneration;
|
|
60
|
+
airLaunch = movie.air ?? null;
|
|
61
|
+
configure({
|
|
62
|
+
sandboxType: movie.sandbox,
|
|
63
|
+
air: movie.air ? airHosts(movie.air, desktop.air) : null,
|
|
64
|
+
});
|
|
65
|
+
showFailure(null);
|
|
66
|
+
welcome.hidden = true;
|
|
67
|
+
player.hidden = false;
|
|
68
|
+
player.load(movie.url).then(
|
|
69
|
+
() => {
|
|
70
|
+
if (generation === loadGeneration) {
|
|
71
|
+
player.focus();
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
(error: unknown) => {
|
|
75
|
+
if (generation !== loadGeneration) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
player.hidden = true;
|
|
80
|
+
welcome.hidden = false;
|
|
81
|
+
showFailure(`${movie.name} did not play: ${error instanceof Error ? error.message : error}`);
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function close(): void {
|
|
87
|
+
loadGeneration++;
|
|
88
|
+
player.destroy();
|
|
89
|
+
player.hidden = true;
|
|
90
|
+
welcome.hidden = false;
|
|
91
|
+
showFailure(null);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
byId("open").addEventListener("click", () => desktop.openDialog());
|
|
95
|
+
byId("open-url").addEventListener("click", () => desktop.openUrlDialog());
|
|
96
|
+
|
|
97
|
+
// A file dropped anywhere is offered to the shell; the page never navigates to it.
|
|
98
|
+
document.addEventListener("dragover", (event) => {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
if (event.dataTransfer) {
|
|
101
|
+
event.dataTransfer.dropEffect = "copy";
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
document.addEventListener("drop", (event) => {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
const file = event.dataTransfer?.files[0];
|
|
107
|
+
if (file) {
|
|
108
|
+
desktop.openDropped(file);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// An open or close during optional backend loading takes precedence over the startup snapshot.
|
|
113
|
+
let started = false;
|
|
114
|
+
let pending: OpenedMovie | null | undefined;
|
|
115
|
+
desktop.onOpen((movie) => {
|
|
116
|
+
if (started) {
|
|
117
|
+
play(movie);
|
|
118
|
+
} else {
|
|
119
|
+
pending = movie;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
desktop.onClose(() => {
|
|
123
|
+
if (started) {
|
|
124
|
+
close();
|
|
125
|
+
} else {
|
|
126
|
+
pending = null;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
const state = await desktop.start();
|
|
130
|
+
configure({ build: state.build ?? undefined });
|
|
131
|
+
if (state.stage3D) {
|
|
132
|
+
try {
|
|
133
|
+
const { createStage3D } = await import("./stage3d.js");
|
|
134
|
+
configure({ stage3D: createStage3D });
|
|
135
|
+
} catch (error) {
|
|
136
|
+
console.warn("swf2es: Stage3D backend could not be loaded; using default playback.", error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
useLibraries(state.libraries);
|
|
141
|
+
started = true;
|
|
142
|
+
const movie = pending === undefined ? state.movie : pending;
|
|
143
|
+
if (movie) {
|
|
144
|
+
play(movie);
|
|
145
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// What the SWFs load, through the shell: the player's fetch host. A
|
|
2
|
+
// swf2es://file URL, a local SWF's own files, the page fetches as before,
|
|
3
|
+
// its sandbox judged where the shell serves them; an http or https one
|
|
4
|
+
// goes to the main process, which sends it as Flash's sandboxes and policy
|
|
5
|
+
// files allow (main/network.ts), so the page itself never goes to the
|
|
6
|
+
// network. An AIR application's app:, app-storage: and file: URLs are read
|
|
7
|
+
// from disk through the shell while it plays. Nothing else is fetched.
|
|
8
|
+
import type { FetchRequest, FetchResult } from "@swf2es/player";
|
|
9
|
+
import type { DesktopNetwork } from "../shared/api.js";
|
|
10
|
+
|
|
11
|
+
export function shellFetch(
|
|
12
|
+
network: DesktopNetwork,
|
|
13
|
+
/** An AIR application's local URL read, null where `url` is none; null where none plays. */
|
|
14
|
+
airFile: () => ((url: string) => Promise<Uint8Array | null> | null) | null = () => null,
|
|
15
|
+
): (request: FetchRequest, signal: AbortSignal) => Promise<FetchResult> {
|
|
16
|
+
return async (request, signal) => {
|
|
17
|
+
const local = airFile()?.(request.url);
|
|
18
|
+
if (local) {
|
|
19
|
+
const bytes = await local;
|
|
20
|
+
signal.throwIfAborted();
|
|
21
|
+
// A file that is not there is a failed load, as a 404 is.
|
|
22
|
+
return { bytes, status: bytes ? 200 : 404, headers: [] };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const url = new URL(request.url);
|
|
26
|
+
// By its parts: a URL's origin is "null" for a scheme the URL standard does not know.
|
|
27
|
+
if (url.protocol === "swf2es:" && url.host === "file") {
|
|
28
|
+
const response = await fetch(url.href, { signal });
|
|
29
|
+
const headers: [string, string][] = [];
|
|
30
|
+
response.headers.forEach((value, name) => {
|
|
31
|
+
headers.push([name, value]);
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
bytes: response.ok ? new Uint8Array(await response.arrayBuffer()) : null,
|
|
35
|
+
status: response.status,
|
|
36
|
+
headers,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
41
|
+
throw new Error(`swf2es: no ${url.protocol} URL is loaded`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
signal.throwIfAborted();
|
|
45
|
+
return network.fetch(
|
|
46
|
+
{
|
|
47
|
+
url: url.href,
|
|
48
|
+
method: request.method,
|
|
49
|
+
headers: request.headers.map(([name, value]) => [name, value]),
|
|
50
|
+
body: request.body,
|
|
51
|
+
purpose: request.purpose ?? "data",
|
|
52
|
+
},
|
|
53
|
+
(abort) => signal.addEventListener("abort", abort, { once: true }),
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// The URL dialog (static/open-url.html): a URL to open a remote SWF by,
|
|
2
|
+
// told the main process through window.swf2esPrompt. Only http and https.
|
|
3
|
+
export {};
|
|
4
|
+
|
|
5
|
+
declare global {
|
|
6
|
+
interface Window {
|
|
7
|
+
swf2esPrompt: {
|
|
8
|
+
initial(): Promise<string>;
|
|
9
|
+
answer(url: string | null): void;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const shell = window.swf2esPrompt;
|
|
15
|
+
const form = document.getElementById("form") as HTMLFormElement;
|
|
16
|
+
const input = document.getElementById("url") as HTMLInputElement;
|
|
17
|
+
const problem = document.getElementById("problem") as HTMLElement;
|
|
18
|
+
|
|
19
|
+
function remote(text: string): boolean {
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(text.trim());
|
|
22
|
+
return (url.protocol === "http:" || url.protocol === "https:") && url.hostname !== "";
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
form.addEventListener("submit", (event) => {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
if (!remote(input.value)) {
|
|
31
|
+
problem.hidden = false;
|
|
32
|
+
input.focus();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
shell.answer(input.value.trim());
|
|
37
|
+
});
|
|
38
|
+
document.getElementById("cancel")?.addEventListener("click", () => shell.answer(null));
|
|
39
|
+
document.addEventListener("keydown", (event) => {
|
|
40
|
+
if (event.key === "Escape") {
|
|
41
|
+
shell.answer(null);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
input.addEventListener("input", () => {
|
|
45
|
+
problem.hidden = true;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
input.value = await shell.initial();
|
|
49
|
+
input.focus();
|
|
50
|
+
input.select();
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// flash.net.Socket over the shell's TCP connections: what the player asks
|
|
2
|
+
// of a SocketHost, carried to the main process and back.
|
|
3
|
+
import type { SocketEvents, SocketHost } from "@swf2es/player";
|
|
4
|
+
import type { DesktopSockets } from "../shared/api.js";
|
|
5
|
+
|
|
6
|
+
export function shellSocketHost(sockets: DesktopSockets): SocketHost {
|
|
7
|
+
// Until its close, which the shell sends after an error and after a close asked for.
|
|
8
|
+
const open = new Map<number, SocketEvents>();
|
|
9
|
+
sockets.onEvent((id, event) => {
|
|
10
|
+
const events = open.get(id);
|
|
11
|
+
if (!events) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
switch (event.type) {
|
|
16
|
+
case "open":
|
|
17
|
+
events.open(event);
|
|
18
|
+
break;
|
|
19
|
+
case "data":
|
|
20
|
+
events.data(event.bytes);
|
|
21
|
+
break;
|
|
22
|
+
case "error":
|
|
23
|
+
events.error(event.message);
|
|
24
|
+
break;
|
|
25
|
+
case "close":
|
|
26
|
+
open.delete(id);
|
|
27
|
+
events.close();
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
connect(host, port, events) {
|
|
34
|
+
const id = sockets.connect(host, port);
|
|
35
|
+
open.set(id, events);
|
|
36
|
+
return {
|
|
37
|
+
send: (bytes) => sockets.send(id, bytes),
|
|
38
|
+
close: () => sockets.close(id),
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { WebGLContext3D } from "@swf2es/context3d/webgl";
|
|
2
|
+
import type { Stage3DSurfaceFactory } from "@swf2es/web";
|
|
3
|
+
|
|
4
|
+
export const createStage3D: Stage3DSurfaceFactory = () => {
|
|
5
|
+
const canvas = document.createElement("canvas");
|
|
6
|
+
const gl = canvas.getContext("webgl2", {
|
|
7
|
+
alpha: false,
|
|
8
|
+
antialias: false,
|
|
9
|
+
depth: false,
|
|
10
|
+
stencil: false,
|
|
11
|
+
preserveDrawingBuffer: true,
|
|
12
|
+
});
|
|
13
|
+
if (!gl) {
|
|
14
|
+
throw new Error("WebGL2 is unavailable");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
return {
|
|
19
|
+
canvas,
|
|
20
|
+
context: new WebGLContext3D(gl),
|
|
21
|
+
destroy: () => gl.getExtension("WEBGL_lose_context")?.loseContext(),
|
|
22
|
+
};
|
|
23
|
+
} catch (error) {
|
|
24
|
+
gl.getExtension("WEBGL_lose_context")?.loseContext();
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": ".",
|
|
5
|
+
"outDir": "../../dist/renderer",
|
|
6
|
+
"tsBuildInfoFile": "../../dist/renderer/.tsbuildinfo",
|
|
7
|
+
"types": [],
|
|
8
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"]
|
|
9
|
+
},
|
|
10
|
+
"include": ["*.ts"],
|
|
11
|
+
"references": [
|
|
12
|
+
{ "path": "../shared" },
|
|
13
|
+
{ "path": "../../../../packages/context3d/tsconfig.webgl.json" },
|
|
14
|
+
{ "path": "../../../../packages/player" },
|
|
15
|
+
{ "path": "../../../../packages/web" }
|
|
16
|
+
]
|
|
17
|
+
}
|