@telepath-computer/television-desktop 0.1.112 → 0.1.152

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 CHANGED
@@ -14,8 +14,9 @@ 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 (URL +
18
- token, or a `--public` server with an empty 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
 
package/assets/icon.icns CHANGED
Binary file
package/assets/icon.png CHANGED
Binary file
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ (() => {
3
+ // src/connect-error.ts
4
+ function connectErrorMessage(error) {
5
+ if (!(error instanceof Error)) return "Failed to connect";
6
+ const message = error.message;
7
+ const ipcMatch = message.match(/^Error invoking remote method '[^']+': (?:(?:Error: )+)?([\s\S]+)$/);
8
+ return ipcMatch?.[1]?.trim() || message;
9
+ }
10
+
11
+ // src/connect-page.ts
12
+ var MIN_CONNECT_MS = 500;
13
+ function bindConnectPage(api, elements) {
14
+ let busy = false;
15
+ function setBusy(next) {
16
+ busy = next;
17
+ if (next && document.activeElement instanceof HTMLElement) {
18
+ document.activeElement.blur();
19
+ }
20
+ elements.submitButton.disabled = next;
21
+ elements.submitButton.dataset.connecting = next ? "true" : "false";
22
+ elements.submitLabel.textContent = next ? "Connecting\u2026" : "Connect";
23
+ elements.serverInput.disabled = next;
24
+ elements.tokenInput.disabled = next;
25
+ }
26
+ async function waitForMinConnect(started) {
27
+ const remaining = MIN_CONNECT_MS - (Date.now() - started);
28
+ if (remaining > 0) {
29
+ await new Promise((resolve) => setTimeout(resolve, remaining));
30
+ }
31
+ }
32
+ async function runConnect(serverURL, token) {
33
+ if (busy) return;
34
+ elements.errorEl.textContent = "";
35
+ if (!serverURL.trim()) return;
36
+ setBusy(true);
37
+ const started = Date.now();
38
+ try {
39
+ const result = await api.connect(serverURL, token);
40
+ if (!result.ok) {
41
+ await waitForMinConnect(started);
42
+ elements.errorEl.textContent = result.message;
43
+ setBusy(false);
44
+ }
45
+ } catch (err) {
46
+ await waitForMinConnect(started);
47
+ elements.errorEl.textContent = connectErrorMessage(err);
48
+ setBusy(false);
49
+ }
50
+ }
51
+ const onSubmit = (event) => {
52
+ event.preventDefault();
53
+ void runConnect(elements.serverInput.value, elements.tokenInput.value);
54
+ };
55
+ elements.form.addEventListener("submit", onSubmit);
56
+ void (async () => {
57
+ const intent = await api.getConnectScreenIntent();
58
+ const saved = await api.getConnection();
59
+ if (saved) {
60
+ elements.serverInput.value = saved.serverURL;
61
+ elements.tokenInput.value = saved.token;
62
+ }
63
+ if (intent === "bootstrap" && saved) {
64
+ await runConnect(saved.serverURL, saved.token);
65
+ }
66
+ })();
67
+ return () => elements.form.removeEventListener("submit", onSubmit);
68
+ }
69
+ function initConnectPage(api) {
70
+ const form = document.getElementById("connect-form");
71
+ const errorEl = document.getElementById("error");
72
+ const submitButton = document.getElementById("submit");
73
+ const serverInput = document.getElementById("serverURL");
74
+ const tokenInput = document.getElementById("token");
75
+ const submitLabel = submitButton?.querySelector(".button-label");
76
+ if (!(form instanceof HTMLFormElement)) throw new Error("connect form missing");
77
+ if (!(errorEl instanceof HTMLElement)) throw new Error("connect error element missing");
78
+ if (!(submitButton instanceof HTMLButtonElement)) throw new Error("connect submit button missing");
79
+ if (!(serverInput instanceof HTMLInputElement)) throw new Error("connect server input missing");
80
+ if (!(tokenInput instanceof HTMLInputElement)) throw new Error("connect token input missing");
81
+ if (!(submitLabel instanceof HTMLElement)) throw new Error("connect submit label missing");
82
+ bindConnectPage(api, { form, errorEl, submitButton, submitLabel, serverInput, tokenInput });
83
+ }
84
+ if (typeof window !== "undefined" && window.television) {
85
+ initConnectPage(window.television);
86
+ }
87
+ })();
@@ -2,8 +2,15 @@
2
2
 
3
3
  // src/connect-preload.ts
4
4
  var import_electron = require("electron");
5
+
6
+ // src/connect-screen.ts
7
+ var GET_CONNECT_SCREEN_INTENT_CHANNEL = "television:get-connect-screen-intent";
8
+
9
+ // src/connect-preload.ts
5
10
  if (window.location.protocol === "file:") {
6
11
  import_electron.contextBridge.exposeInMainWorld("television", {
7
- setConnection: (serverURL, token) => import_electron.ipcRenderer.invoke("television:set-connection", { serverURL, token })
12
+ getConnectScreenIntent: () => import_electron.ipcRenderer.invoke(GET_CONNECT_SCREEN_INTENT_CHANNEL),
13
+ getConnection: () => import_electron.ipcRenderer.invoke("television:get-connection"),
14
+ connect: (serverURL, token) => import_electron.ipcRenderer.invoke("television:connect", { serverURL, token })
8
15
  });
9
16
  }
package/dist/connect.html CHANGED
@@ -2,7 +2,7 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'unsafe-inline'; script-src 'unsafe-inline'" />
5
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'unsafe-inline'; script-src 'self' 'unsafe-inline'" />
6
6
  <title>Television</title>
7
7
  <style>
8
8
  :root {
@@ -61,25 +61,66 @@
61
61
  input:focus {
62
62
  border-color: #fff;
63
63
  }
64
+ input:disabled {
65
+ opacity: 0.6;
66
+ border-color: #2a2a2a;
67
+ }
64
68
  button {
65
69
  margin-top: 6px;
66
70
  background: #fff;
67
71
  color: #000;
68
72
  border: none;
69
- padding: 10px 12px;
73
+ padding: 0 16px;
70
74
  font-size: 14px;
71
75
  font-weight: 500;
72
76
  font-family: inherit;
73
77
  border-radius: 6px;
74
78
  cursor: default;
79
+ position: relative;
80
+ display: flex;
81
+ align-items: center;
82
+ justify-content: center;
83
+ height: 40px;
84
+ box-sizing: border-box;
75
85
  }
76
- button:disabled {
86
+ button:disabled:not([data-connecting="true"]) {
77
87
  opacity: 0.5;
78
88
  }
89
+ button[data-connecting="true"] {
90
+ opacity: 0.85;
91
+ }
92
+ .button-label {
93
+ line-height: 1;
94
+ }
95
+ .button-spinner {
96
+ position: absolute;
97
+ left: 16px;
98
+ top: 0;
99
+ bottom: 0;
100
+ width: 14px;
101
+ height: 14px;
102
+ margin-block: auto;
103
+ box-sizing: border-box;
104
+ border: 2px solid rgba(0, 0, 0, 0.2);
105
+ border-top-color: #000;
106
+ border-radius: 50%;
107
+ visibility: hidden;
108
+ animation: connect-spin 0.7s linear infinite;
109
+ }
110
+ button[data-connecting="true"] .button-spinner {
111
+ visibility: visible;
112
+ }
113
+ @keyframes connect-spin {
114
+ to {
115
+ transform: rotate(360deg);
116
+ }
117
+ }
79
118
  .error {
80
119
  color: #ff6b6b;
81
120
  font-size: 12px;
82
- min-height: 1em;
121
+ line-height: 18px;
122
+ height: 36px;
123
+ overflow: hidden;
83
124
  }
84
125
  </style>
85
126
  </head>
@@ -89,40 +130,19 @@
89
130
  <form id="connect-form">
90
131
  <label>
91
132
  Server URL
92
- <input id="serverURL" type="url" placeholder="https://example.com" required autofocus />
133
+ <input id="serverURL" type="text" placeholder="https://example.com" required autofocus />
93
134
  </label>
94
135
  <label>
95
- Token <span style="text-transform: none; letter-spacing: 0; color: #555;">(leave blank for --public servers)</span>
136
+ Token <span style="text-transform: none; letter-spacing: 0; color: #555;">(leave blank for no-auth servers)</span>
96
137
  <input id="token" type="password" />
97
138
  </label>
98
139
  <div class="error" id="error"></div>
99
- <button type="submit">Connect</button>
140
+ <button type="submit" id="submit">
141
+ <span class="button-spinner" aria-hidden="true"></span>
142
+ <span class="button-label">Connect</span>
143
+ </button>
100
144
  </form>
101
145
  </main>
102
- <script>
103
- const form = document.getElementById("connect-form");
104
- const errorEl = document.getElementById("error");
105
- const submitButton = form.querySelector("button");
106
- form.addEventListener("submit", async (event) => {
107
- event.preventDefault();
108
- errorEl.textContent = "";
109
- const serverURL = document.getElementById("serverURL").value.trim();
110
- const token = document.getElementById("token").value.trim();
111
- if (!serverURL) return;
112
- try {
113
- new URL(serverURL);
114
- } catch {
115
- errorEl.textContent = "Invalid URL";
116
- return;
117
- }
118
- submitButton.disabled = true;
119
- try {
120
- await window.television.setConnection(serverURL, token);
121
- } catch (err) {
122
- errorEl.textContent = err instanceof Error ? err.message : "Failed to connect";
123
- submitButton.disabled = false;
124
- }
125
- });
126
- </script>
146
+ <script src="connect-page.cjs"></script>
127
147
  </body>
128
148
  </html>
package/dist/electron.cjs CHANGED
@@ -36,6 +36,93 @@ module.exports = __toCommonJS(index_exports);
36
36
  var import_electron2 = require("electron");
37
37
  var import_node_path2 = __toESM(require("node:path"), 1);
38
38
 
39
+ // src/connect-url.ts
40
+ var ConnectURLError = class extends Error {
41
+ constructor(message = "Enter a valid http or https URL") {
42
+ super(message);
43
+ this.name = "ConnectURLError";
44
+ }
45
+ };
46
+ function normalizeConnectURL(input) {
47
+ const trimmed = input.trim();
48
+ if (!trimmed) throw new ConnectURLError();
49
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) && !/^https?:\/\//i.test(trimmed)) {
50
+ throw new ConnectURLError();
51
+ }
52
+ const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
53
+ let url;
54
+ try {
55
+ url = new URL(withScheme);
56
+ } catch {
57
+ throw new ConnectURLError();
58
+ }
59
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
60
+ throw new ConnectURLError();
61
+ }
62
+ return url.toString().replace(/\/$/, "");
63
+ }
64
+ function buildRemoteURL(serverURL, token) {
65
+ const url = new URL(serverURL);
66
+ url.searchParams.set("mode", "electron");
67
+ if (token) url.searchParams.set("token", token);
68
+ return url.toString();
69
+ }
70
+
71
+ // src/connect-preflight.ts
72
+ var HTTP_UNAUTHORIZED = 401;
73
+ var DEFAULT_PREFLIGHT_TIMEOUT_MS = 8e3;
74
+ var NOT_TV_SERVER_MESSAGE = "This URL doesn't seem to be a Television server \u2014 please check it.";
75
+ function unreachable(serverURL) {
76
+ return {
77
+ ok: false,
78
+ code: "unreachable",
79
+ message: `Couldn't reach ${serverURL} \u2014 is the server running?`
80
+ };
81
+ }
82
+ function notTVServer() {
83
+ return { ok: false, code: "not-tv-server", message: NOT_TV_SERVER_MESSAGE };
84
+ }
85
+ function isDisplayResponse(body) {
86
+ if (typeof body !== "object" || body === null) return false;
87
+ const candidate = body;
88
+ return (typeof candidate.activeScreenID === "string" || candidate.activeScreenID === null) && (typeof candidate.activeThemeName === "string" || candidate.activeThemeName === null) && typeof candidate.acpEnabled === "boolean";
89
+ }
90
+ async function preflightConnection(options) {
91
+ const fetchImpl = options.fetchImpl ?? fetch;
92
+ const probeURL = new URL(options.probePath ?? "/display", options.serverURL).toString();
93
+ const headers = {};
94
+ if (options.token) headers.Authorization = `Bearer ${options.token}`;
95
+ let response;
96
+ try {
97
+ response = await fetchImpl(probeURL, {
98
+ method: "GET",
99
+ headers,
100
+ signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_PREFLIGHT_TIMEOUT_MS)
101
+ });
102
+ } catch {
103
+ return unreachable(options.serverURL);
104
+ }
105
+ if (response.status === HTTP_UNAUTHORIZED) {
106
+ if (!options.token) {
107
+ return { ok: false, code: "auth-required", message: "This server requires a token" };
108
+ }
109
+ return { ok: false, code: "auth-rejected", message: "Token rejected" };
110
+ }
111
+ if (!response.ok) {
112
+ return notTVServer();
113
+ }
114
+ let body;
115
+ try {
116
+ body = await response.json();
117
+ } catch {
118
+ return notTVServer();
119
+ }
120
+ if (!isDisplayResponse(body)) {
121
+ return notTVServer();
122
+ }
123
+ return { ok: true };
124
+ }
125
+
39
126
  // src/connection-store.ts
40
127
  var import_electron = require("electron");
41
128
  var import_node_fs = require("node:fs");
@@ -55,39 +142,57 @@ function loadConnection() {
55
142
  }
56
143
  }
57
144
  function saveConnection(connection) {
58
- (0, import_node_fs.writeFileSync)(storePath(), JSON.stringify(connection, null, 2), "utf8");
145
+ const file = storePath();
146
+ const tmp = `${file}.tmp`;
147
+ (0, import_node_fs.writeFileSync)(tmp, JSON.stringify(connection, null, 2), "utf8");
148
+ (0, import_node_fs.renameSync)(tmp, file);
59
149
  }
60
150
 
151
+ // src/connect-screen.ts
152
+ var GET_CONNECT_SCREEN_INTENT_CHANNEL = "television:get-connect-screen-intent";
153
+
61
154
  // src/index.ts
62
155
  import_electron2.app.setName("Television");
63
156
  var WINDOW_WIDTH = 1400;
64
157
  var WINDOW_HEIGHT = 1e3;
65
158
  var TRAFFIC_LIGHT_POSITION = { x: 12, y: 12 };
66
- var SET_CONNECTION_CHANNEL = "television:set-connection";
159
+ var GET_CONNECTION_CHANNEL = "television:get-connection";
160
+ var CONNECT_CHANNEL = "television:connect";
161
+ var TEST_EXTERNAL_OPEN_CHANNEL = "television:test:external-open";
67
162
  var TEST_FIXTURE_FLAG = "--test-fixture";
68
- function buildRemoteURL({ serverURL, token }) {
69
- const url = new URL(serverURL);
70
- url.searchParams.set("mode", "electron");
71
- if (token) url.searchParams.set("token", token);
72
- return url.toString();
163
+ var ERR_ABORTED = -3;
164
+ if (process.env.TV_ELECTRON_CHROMIUM_LOGS !== "1") {
165
+ import_electron2.app.commandLine?.appendSwitch("log-level", "3");
73
166
  }
74
167
  function readTestFixtureURL(argv) {
75
168
  const idx = argv.indexOf(TEST_FIXTURE_FLAG);
76
169
  if (idx === -1 || idx === argv.length - 1) return null;
77
170
  return argv[idx + 1] ?? null;
78
171
  }
172
+ function isExternalOpenURL(url) {
173
+ return url.startsWith("http://") || url.startsWith("https://");
174
+ }
175
+ function recordExternalOpenForTest(url) {
176
+ const globalState = globalThis;
177
+ const log = globalState.__televisionExternalOpenLog ??= [];
178
+ log.push(url);
179
+ for (const window of import_electron2.BrowserWindow.getAllWindows()) {
180
+ window.webContents.send(TEST_EXTERNAL_OPEN_CHANNEL, url);
181
+ }
182
+ }
79
183
  var App = class {
80
184
  window = null;
185
+ connectScreenIntent = "manual";
81
186
  async start() {
82
187
  await import_electron2.app.whenReady();
83
188
  if (process.platform === "darwin" && import_electron2.app.dock) {
84
189
  import_electron2.app.dock.setIcon(import_node_path2.default.join(__dirname, "..", "assets", "icon.png"));
85
190
  }
86
191
  this.installMenu();
87
- import_electron2.ipcMain.handle(SET_CONNECTION_CHANNEL, async (_event, connection) => {
88
- saveConnection(connection);
89
- await this.loadRemote(connection);
90
- });
192
+ this.installWindowOpenHandler();
193
+ import_electron2.ipcMain.handle(GET_CONNECTION_CHANNEL, async () => loadConnection());
194
+ import_electron2.ipcMain.handle(GET_CONNECT_SCREEN_INTENT_CHANNEL, async () => this.connectScreenIntent);
195
+ import_electron2.ipcMain.handle(CONNECT_CHANNEL, async (_event, connection) => this.tryConnect(connection));
91
196
  await this.createWindow();
92
197
  import_electron2.app.on("window-all-closed", () => {
93
198
  if (process.platform !== "darwin") import_electron2.app.quit();
@@ -114,6 +219,13 @@ var App = class {
114
219
  });
115
220
  this.window.webContents.on("will-attach-webview", (_event, webPreferences) => {
116
221
  webPreferences.preload = import_node_path2.default.join(__dirname, "webview-bridge-preload.cjs");
222
+ webPreferences.contextIsolation = false;
223
+ });
224
+ this.window.webContents.on("did-fail-load", (_event, code, _description, validatedURL, isMainFrame) => {
225
+ if (!isMainFrame) return;
226
+ if (code === ERR_ABORTED) return;
227
+ if (validatedURL.startsWith("file:")) return;
228
+ void this.loadConnectScreen("manual");
117
229
  });
118
230
  this.window.once("ready-to-show", () => this.window?.show());
119
231
  this.window.on("closed", () => {
@@ -124,27 +236,69 @@ var App = class {
124
236
  await this.window.loadURL(testFixtureURL);
125
237
  return;
126
238
  }
127
- const connection = loadConnection();
128
- if (connection) {
129
- await this.loadRemote(connection);
130
- } else {
131
- await this.loadConnectScreen();
132
- }
239
+ await this.loadConnectScreen("bootstrap");
133
240
  }
134
- async loadConnectScreen() {
241
+ /** @param intent bootstrap = app open / retry saved connection; manual = user chose connect screen */
242
+ async loadConnectScreen(intent = "manual") {
135
243
  if (!this.window) return;
244
+ this.connectScreenIntent = intent;
136
245
  await this.window.loadFile(import_node_path2.default.join(__dirname, "connect.html"));
137
246
  }
247
+ async tryConnect(raw) {
248
+ const submitted = { serverURL: raw.serverURL, token: raw.token ?? "" };
249
+ try {
250
+ saveConnection(submitted);
251
+ } catch (error) {
252
+ const message = error instanceof Error ? error.message : "Failed to connect";
253
+ return { ok: false, message };
254
+ }
255
+ let serverURL;
256
+ try {
257
+ serverURL = normalizeConnectURL(raw.serverURL);
258
+ } catch (error) {
259
+ const message = error instanceof ConnectURLError ? error.message : new ConnectURLError().message;
260
+ return { ok: false, message };
261
+ }
262
+ const connection = { serverURL, token: submitted.token };
263
+ try {
264
+ const preflight = await preflightConnection({
265
+ serverURL: connection.serverURL,
266
+ token: connection.token
267
+ });
268
+ if (!preflight.ok) {
269
+ return { ok: false, message: preflight.message };
270
+ }
271
+ await this.loadRemote(connection);
272
+ return { ok: true };
273
+ } catch (error) {
274
+ const message = error instanceof Error ? error.message : "Failed to connect";
275
+ return { ok: false, message };
276
+ }
277
+ }
138
278
  async loadRemote(connection) {
139
279
  if (!this.window) return;
140
- await this.window.loadURL(buildRemoteURL(connection));
280
+ await this.window.loadURL(buildRemoteURL(connection.serverURL, connection.token));
281
+ }
282
+ installWindowOpenHandler() {
283
+ import_electron2.app.on("web-contents-created", (_event, contents) => {
284
+ contents.setWindowOpenHandler(({ url }) => {
285
+ if (isExternalOpenURL(url)) {
286
+ if (process.env.TV_TEST_MODE === "true") {
287
+ recordExternalOpenForTest(url);
288
+ } else {
289
+ void import_electron2.shell.openExternal(url);
290
+ }
291
+ }
292
+ return { action: "deny" };
293
+ });
294
+ });
141
295
  }
142
296
  installMenu() {
143
297
  const isMac = process.platform === "darwin";
144
298
  const connectItem = {
145
299
  label: "Connect to server\u2026",
146
300
  accelerator: "CmdOrCtrl+,",
147
- click: () => void this.loadConnectScreen()
301
+ click: () => void this.loadConnectScreen("manual")
148
302
  };
149
303
  const template = [
150
304
  ...isMac ? [
@@ -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 lastWheelClientX = 0;
7
- var lastWheelClientY = 0;
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
- lastWheelClientX = event.clientX;
12
- lastWheelClientY = event.clientY;
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: event.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.112",
4
+ "version": "0.1.152",
5
5
  "type": "module",
6
6
  "main": "dist/electron.cjs",
7
7
  "bin": {