@telepath-computer/television-desktop 0.1.107 → 0.1.142
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 +8 -10
- package/assets/icon.icns +0 -0
- package/assets/icon.png +0 -0
- package/bin/tv-desktop.cjs +12 -1
- package/dist/connect.html +1 -1
- package/dist/electron.cjs +31 -0
- package/dist/webview-bridge-preload.cjs +62 -41
- package/package.json +5 -4
- package/scripts/{postinstall.cjs → rename-bundle.cjs} +7 -1
package/README.md
CHANGED
|
@@ -14,16 +14,14 @@ npm i -g @telepath-computer/television-desktop
|
|
|
14
14
|
tv-desktop
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
A connect window opens; point it at a running Television server
|
|
18
|
-
token
|
|
17
|
+
A connect window opens; point it at a running Television server. Enter the
|
|
18
|
+
server URL plus token when the server runs with `--auth`; leave the token blank
|
|
19
|
+
for no-auth servers.
|
|
19
20
|
|
|
20
21
|
## macOS notes
|
|
21
22
|
|
|
22
|
-
The first
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
If you install with `--ignore-scripts`, the rename is skipped — the app
|
|
28
|
-
still works but will be labelled "Electron" everywhere macOS reads the
|
|
29
|
-
bundle.
|
|
23
|
+
The first launch renames the bundled `Electron.app` to `Television.app`
|
|
24
|
+
(bundle directory + inner executable + `Info.plist` + bundle icon). This
|
|
25
|
+
is what gives the Dock and menu bar "Television" instead of "Electron".
|
|
26
|
+
First launch is a few seconds slower; every launch after takes the fast
|
|
27
|
+
path.
|
package/assets/icon.icns
CHANGED
|
Binary file
|
package/assets/icon.png
CHANGED
|
Binary file
|
package/bin/tv-desktop.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
const { spawn } = require("node:child_process");
|
|
2
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
3
3
|
const path = require("node:path");
|
|
4
4
|
const fs = require("node:fs");
|
|
5
5
|
|
|
@@ -11,6 +11,17 @@ function resolveExecutable() {
|
|
|
11
11
|
if (process.platform === "darwin") {
|
|
12
12
|
const renamed = path.join(distDir, "Television.app", "Contents", "MacOS", "Television");
|
|
13
13
|
if (fs.existsSync(renamed)) return renamed;
|
|
14
|
+
// npm runs our package's postinstall before electron's postinstall has
|
|
15
|
+
// downloaded the Electron.app, so we can't rely on a postinstall hook.
|
|
16
|
+
// Rename lazily on first launch (idempotent); subsequent launches take
|
|
17
|
+
// the fast path above.
|
|
18
|
+
const upstream = path.join(distDir, "Electron.app");
|
|
19
|
+
if (fs.existsSync(upstream)) {
|
|
20
|
+
spawnSync(process.execPath, [path.join(appDir, "scripts", "rename-bundle.cjs")], {
|
|
21
|
+
stdio: "inherit",
|
|
22
|
+
});
|
|
23
|
+
if (fs.existsSync(renamed)) return renamed;
|
|
24
|
+
}
|
|
14
25
|
}
|
|
15
26
|
return require("electron");
|
|
16
27
|
}
|
package/dist/connect.html
CHANGED
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
<input id="serverURL" type="url" placeholder="https://example.com" required autofocus />
|
|
93
93
|
</label>
|
|
94
94
|
<label>
|
|
95
|
-
Token <span style="text-transform: none; letter-spacing: 0; color: #555;">(leave blank for
|
|
95
|
+
Token <span style="text-transform: none; letter-spacing: 0; color: #555;">(leave blank for no-auth servers)</span>
|
|
96
96
|
<input id="token" type="password" />
|
|
97
97
|
</label>
|
|
98
98
|
<div class="error" id="error"></div>
|
package/dist/electron.cjs
CHANGED
|
@@ -64,7 +64,11 @@ var WINDOW_WIDTH = 1400;
|
|
|
64
64
|
var WINDOW_HEIGHT = 1e3;
|
|
65
65
|
var TRAFFIC_LIGHT_POSITION = { x: 12, y: 12 };
|
|
66
66
|
var SET_CONNECTION_CHANNEL = "television:set-connection";
|
|
67
|
+
var TEST_EXTERNAL_OPEN_CHANNEL = "television:test:external-open";
|
|
67
68
|
var TEST_FIXTURE_FLAG = "--test-fixture";
|
|
69
|
+
if (process.env.TV_ELECTRON_CHROMIUM_LOGS !== "1") {
|
|
70
|
+
import_electron2.app.commandLine?.appendSwitch("log-level", "3");
|
|
71
|
+
}
|
|
68
72
|
function buildRemoteURL({ serverURL, token }) {
|
|
69
73
|
const url = new URL(serverURL);
|
|
70
74
|
url.searchParams.set("mode", "electron");
|
|
@@ -76,6 +80,17 @@ function readTestFixtureURL(argv) {
|
|
|
76
80
|
if (idx === -1 || idx === argv.length - 1) return null;
|
|
77
81
|
return argv[idx + 1] ?? null;
|
|
78
82
|
}
|
|
83
|
+
function isExternalOpenURL(url) {
|
|
84
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
85
|
+
}
|
|
86
|
+
function recordExternalOpenForTest(url) {
|
|
87
|
+
const globalState = globalThis;
|
|
88
|
+
const log = globalState.__televisionExternalOpenLog ??= [];
|
|
89
|
+
log.push(url);
|
|
90
|
+
for (const window of import_electron2.BrowserWindow.getAllWindows()) {
|
|
91
|
+
window.webContents.send(TEST_EXTERNAL_OPEN_CHANNEL, url);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
79
94
|
var App = class {
|
|
80
95
|
window = null;
|
|
81
96
|
async start() {
|
|
@@ -84,6 +99,7 @@ var App = class {
|
|
|
84
99
|
import_electron2.app.dock.setIcon(import_node_path2.default.join(__dirname, "..", "assets", "icon.png"));
|
|
85
100
|
}
|
|
86
101
|
this.installMenu();
|
|
102
|
+
this.installWindowOpenHandler();
|
|
87
103
|
import_electron2.ipcMain.handle(SET_CONNECTION_CHANNEL, async (_event, connection) => {
|
|
88
104
|
saveConnection(connection);
|
|
89
105
|
await this.loadRemote(connection);
|
|
@@ -114,6 +130,7 @@ var App = class {
|
|
|
114
130
|
});
|
|
115
131
|
this.window.webContents.on("will-attach-webview", (_event, webPreferences) => {
|
|
116
132
|
webPreferences.preload = import_node_path2.default.join(__dirname, "webview-bridge-preload.cjs");
|
|
133
|
+
webPreferences.contextIsolation = false;
|
|
117
134
|
});
|
|
118
135
|
this.window.once("ready-to-show", () => this.window?.show());
|
|
119
136
|
this.window.on("closed", () => {
|
|
@@ -139,6 +156,20 @@ var App = class {
|
|
|
139
156
|
if (!this.window) return;
|
|
140
157
|
await this.window.loadURL(buildRemoteURL(connection));
|
|
141
158
|
}
|
|
159
|
+
installWindowOpenHandler() {
|
|
160
|
+
import_electron2.app.on("web-contents-created", (_event, contents) => {
|
|
161
|
+
contents.setWindowOpenHandler(({ url }) => {
|
|
162
|
+
if (isExternalOpenURL(url)) {
|
|
163
|
+
if (process.env.TV_TEST_MODE === "true") {
|
|
164
|
+
recordExternalOpenForTest(url);
|
|
165
|
+
} else {
|
|
166
|
+
void import_electron2.shell.openExternal(url);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { action: "deny" };
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
142
173
|
installMenu() {
|
|
143
174
|
const isMac = process.platform === "darwin";
|
|
144
175
|
const connectItem = {
|
|
@@ -3,55 +3,76 @@
|
|
|
3
3
|
// src/webview-bridge-preload.ts
|
|
4
4
|
var import_electron = require("electron");
|
|
5
5
|
var BRIDGE_CHANNEL = "television-artifact-bridge";
|
|
6
|
-
var
|
|
7
|
-
|
|
6
|
+
var contentBridge = {
|
|
7
|
+
postToHost(message) {
|
|
8
|
+
import_electron.ipcRenderer.sendToHost(BRIDGE_CHANNEL, message);
|
|
9
|
+
},
|
|
10
|
+
onHostMessage(callback) {
|
|
11
|
+
const listener = (_event, message) => {
|
|
12
|
+
callback(message);
|
|
13
|
+
};
|
|
14
|
+
import_electron.ipcRenderer.on(BRIDGE_CHANNEL, listener);
|
|
15
|
+
return () => {
|
|
16
|
+
import_electron.ipcRenderer.removeListener(BRIDGE_CHANNEL, listener);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
if (process.contextIsolated) {
|
|
21
|
+
import_electron.contextBridge.exposeInMainWorld("__televisionContentBridge", contentBridge);
|
|
22
|
+
} else {
|
|
23
|
+
window.__televisionContentBridge = contentBridge;
|
|
24
|
+
}
|
|
25
|
+
function isTextEditingTarget(target) {
|
|
26
|
+
if (!(target instanceof Element)) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
const editable = target.closest(
|
|
30
|
+
"input, textarea, select, [contenteditable], [role='textbox'], [role='searchbox'], [role='combobox'], [role='spinbutton'], [role='slider']"
|
|
31
|
+
);
|
|
32
|
+
if (!editable) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (editable.hasAttribute("contenteditable")) {
|
|
36
|
+
return editable.getAttribute("contenteditable")?.toLowerCase() !== "false";
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
window.addEventListener(
|
|
41
|
+
"keydown",
|
|
42
|
+
(event) => {
|
|
43
|
+
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (isTextEditingTarget(event.target)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
event.preventDefault();
|
|
53
|
+
import_electron.ipcRenderer.sendToHost(BRIDGE_CHANNEL, {
|
|
54
|
+
type: "filmstrip-key",
|
|
55
|
+
direction: event.key === "ArrowRight" ? 1 : -1
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
{ capture: true }
|
|
59
|
+
);
|
|
8
60
|
window.addEventListener(
|
|
9
61
|
"wheel",
|
|
10
62
|
(event) => {
|
|
11
|
-
|
|
12
|
-
|
|
63
|
+
const usingShiftFallback = event.shiftKey && event.deltaX === 0 && event.deltaY !== 0;
|
|
64
|
+
const horizontalDelta = usingShiftFallback ? event.deltaY : event.deltaX;
|
|
65
|
+
const horizontalMagnitude = Math.abs(horizontalDelta);
|
|
66
|
+
const verticalMagnitude = usingShiftFallback ? 0 : Math.abs(event.deltaY);
|
|
67
|
+
if (horizontalMagnitude <= verticalMagnitude) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
13
70
|
const payload = {
|
|
14
71
|
type: "wheel",
|
|
15
|
-
deltaX:
|
|
16
|
-
deltaY: event.deltaY,
|
|
72
|
+
deltaX: horizontalDelta,
|
|
17
73
|
shiftKey: event.shiftKey
|
|
18
74
|
};
|
|
19
75
|
import_electron.ipcRenderer.sendToHost(BRIDGE_CHANNEL, payload);
|
|
20
|
-
event.preventDefault();
|
|
21
76
|
},
|
|
22
77
|
{ passive: false }
|
|
23
78
|
);
|
|
24
|
-
import_electron.ipcRenderer.on(BRIDGE_CHANNEL, (_event, payload) => {
|
|
25
|
-
if (payload?.type !== "scroll") return;
|
|
26
|
-
const deltaX = payload.deltaX ?? 0;
|
|
27
|
-
const deltaY = payload.deltaY ?? 0;
|
|
28
|
-
const target = resolveScrollTarget(lastWheelClientX, lastWheelClientY, deltaX, deltaY);
|
|
29
|
-
target.scrollBy(deltaX, deltaY);
|
|
30
|
-
});
|
|
31
|
-
function resolveScrollTarget(x, y, deltaX, deltaY) {
|
|
32
|
-
const fallback = document.scrollingElement ?? document.documentElement;
|
|
33
|
-
if (typeof document.elementFromPoint !== "function") return fallback;
|
|
34
|
-
let node = document.elementFromPoint(x, y);
|
|
35
|
-
while (node && node !== document.documentElement) {
|
|
36
|
-
if (canScroll(node, deltaX, deltaY)) return node;
|
|
37
|
-
node = node.parentElement;
|
|
38
|
-
}
|
|
39
|
-
return fallback;
|
|
40
|
-
}
|
|
41
|
-
function canScroll(node, deltaX, deltaY) {
|
|
42
|
-
const style = getComputedStyle(node);
|
|
43
|
-
if (deltaY !== 0) {
|
|
44
|
-
if (isScrollableOverflow(style.overflowY) && node.scrollHeight > node.clientHeight) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (deltaX !== 0) {
|
|
49
|
-
if (isScrollableOverflow(style.overflowX) && node.scrollWidth > node.clientWidth) {
|
|
50
|
-
return true;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
function isScrollableOverflow(value) {
|
|
56
|
-
return value === "auto" || value === "scroll" || value === "overlay";
|
|
57
|
-
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telepath-computer/television-desktop",
|
|
3
3
|
"productName": "Television",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.142",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/electron.cjs",
|
|
7
7
|
"bin": {
|
|
@@ -11,16 +11,17 @@
|
|
|
11
11
|
"dist/**",
|
|
12
12
|
"bin/**",
|
|
13
13
|
"assets/**",
|
|
14
|
-
"scripts/
|
|
14
|
+
"scripts/rename-bundle.cjs"
|
|
15
15
|
],
|
|
16
16
|
"exports": {
|
|
17
17
|
".": "./src/index.ts"
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "node build.mjs",
|
|
21
|
-
"postinstall": "node scripts/postinstall.cjs",
|
|
22
21
|
"prepublishOnly": "node build.mjs",
|
|
23
|
-
"type-check": "tsc -p tsconfig.json --noEmit"
|
|
22
|
+
"type-check": "tsc -p tsconfig.json --noEmit",
|
|
23
|
+
"test": "vitest run --config vitest.config.ts",
|
|
24
|
+
"test:e2e": "node ../../scripts/run-electron-e2e.mjs"
|
|
24
25
|
},
|
|
25
26
|
"dependencies": {
|
|
26
27
|
"electron": "35.7.5"
|
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
// "Television" instead of "Electron". macOS reads the running-app name
|
|
5
5
|
// from the bundle's filesystem identity; just editing Info.plist isn't
|
|
6
6
|
// enough — the .app directory and Contents/MacOS/<exe> have to match.
|
|
7
|
-
// Scoped to this package's resolved electron install.
|
|
7
|
+
// Scoped to this package's resolved electron install. Idempotent; no-ops
|
|
8
|
+
// once the rename is complete.
|
|
9
|
+
//
|
|
10
|
+
// Invoked lazily on first launch by bin/tv-desktop.cjs. We can't use an
|
|
11
|
+
// npm `postinstall` hook because npm runs our package's postinstall
|
|
12
|
+
// before electron's postinstall has downloaded Electron.app — the rename
|
|
13
|
+
// has nothing to rename at install time.
|
|
8
14
|
|
|
9
15
|
const { execFileSync } = require("node:child_process");
|
|
10
16
|
const path = require("node:path");
|