@telepath-computer/television-desktop 0.1.159 → 0.1.161
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/dist/connect-page.cjs +161 -1
- package/dist/electron.cjs +114 -14
- package/dist/webview-bridge-preload.cjs +43 -0
- package/package.json +2 -1
package/dist/connect-page.cjs
CHANGED
|
@@ -8,6 +8,138 @@
|
|
|
8
8
|
return ipcMatch?.[1]?.trim() || message;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// ../layout/src/constants.ts
|
|
12
|
+
var LAYOUT_FULL_WIDTH_UNITS = 4;
|
|
13
|
+
var LAYOUT_FULL_HEIGHT_UNITS = 6;
|
|
14
|
+
var DEFAULT_LAYOUT_UNIT = 128;
|
|
15
|
+
var CARD_GAP = 16;
|
|
16
|
+
var CARD_WIDTH = DEFAULT_LAYOUT_UNIT * LAYOUT_FULL_WIDTH_UNITS + CARD_GAP * (LAYOUT_FULL_WIDTH_UNITS - 1);
|
|
17
|
+
var CARD_HEIGHT = DEFAULT_LAYOUT_UNIT * LAYOUT_FULL_HEIGHT_UNITS + CARD_GAP * (LAYOUT_FULL_HEIGHT_UNITS - 1);
|
|
18
|
+
var LAYOUT_MOBILE_BREAKPOINT_PX = CARD_WIDTH + CARD_GAP * 2;
|
|
19
|
+
|
|
20
|
+
// ../../node_modules/@rupertsworld/event-target/dist/index.js
|
|
21
|
+
var RESERVED_EVENT_KEYS = /* @__PURE__ */ new Set([
|
|
22
|
+
"target",
|
|
23
|
+
"currentTarget",
|
|
24
|
+
"eventPhase",
|
|
25
|
+
"defaultPrevented",
|
|
26
|
+
"isTrusted",
|
|
27
|
+
"timeStamp",
|
|
28
|
+
"srcElement",
|
|
29
|
+
"returnValue",
|
|
30
|
+
"cancelBubble",
|
|
31
|
+
"NONE",
|
|
32
|
+
"CAPTURING_PHASE",
|
|
33
|
+
"AT_TARGET",
|
|
34
|
+
"BUBBLING_PHASE",
|
|
35
|
+
"composedPath",
|
|
36
|
+
"stopPropagation",
|
|
37
|
+
"stopImmediatePropagation",
|
|
38
|
+
"preventDefault",
|
|
39
|
+
"initEvent"
|
|
40
|
+
]);
|
|
41
|
+
function assertNoReservedPayloadKeys(props) {
|
|
42
|
+
for (const key of Object.keys(props)) {
|
|
43
|
+
if (RESERVED_EVENT_KEYS.has(key)) {
|
|
44
|
+
throw new Error(`Event payload key "${key}" is reserved; choose a different property name`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function assignPayload(target, props) {
|
|
49
|
+
assertNoReservedPayloadKeys(props);
|
|
50
|
+
for (const key of Object.keys(props)) {
|
|
51
|
+
Object.defineProperty(target, key, {
|
|
52
|
+
value: props[key],
|
|
53
|
+
writable: true,
|
|
54
|
+
enumerable: true,
|
|
55
|
+
configurable: true
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function defineEvent() {
|
|
60
|
+
class DefinedEvent extends Event {
|
|
61
|
+
constructor(type, init) {
|
|
62
|
+
const { type: initType, bubbles, cancelable, composed, ...payload } = init ?? {};
|
|
63
|
+
if (initType !== void 0) {
|
|
64
|
+
throw new Error(`Do not pass "type" in init; use the constructor argument instead`);
|
|
65
|
+
}
|
|
66
|
+
super(type, { bubbles, cancelable, composed });
|
|
67
|
+
assignPayload(this, payload);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return DefinedEvent;
|
|
71
|
+
}
|
|
72
|
+
var EventTarget = class extends globalThis.EventTarget {
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// ../shared/src/events.ts
|
|
76
|
+
var ChangeEvent = defineEvent();
|
|
77
|
+
var ServerEventMessageEvent = defineEvent();
|
|
78
|
+
var LayoutMutationEvent = defineEvent();
|
|
79
|
+
var LayoutScrollEvent = defineEvent();
|
|
80
|
+
var ArtifactCreatedEvent = defineEvent();
|
|
81
|
+
var ArtifactUpdatedEvent = defineEvent();
|
|
82
|
+
var ArtifactRemovedEvent = defineEvent();
|
|
83
|
+
var ArtifactContentChangedEvent = defineEvent();
|
|
84
|
+
var ScreenCreatedEvent = defineEvent();
|
|
85
|
+
var ScreenUpdatedEvent = defineEvent();
|
|
86
|
+
var ScreenRemovedEvent = defineEvent();
|
|
87
|
+
var ScreenChangedEvent = defineEvent();
|
|
88
|
+
var ArtifactFocusEvent = defineEvent();
|
|
89
|
+
var ThemeChangedEvent = defineEvent();
|
|
90
|
+
|
|
91
|
+
// ../shared/src/errors.ts
|
|
92
|
+
function defineError(name) {
|
|
93
|
+
class DefinedError extends Error {
|
|
94
|
+
constructor(message, fields) {
|
|
95
|
+
super(message);
|
|
96
|
+
this.name = name;
|
|
97
|
+
if (fields) Object.assign(this, fields);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return DefinedError;
|
|
101
|
+
}
|
|
102
|
+
var RequestError = defineError("RequestError");
|
|
103
|
+
var ValidationError = defineError("ValidationError");
|
|
104
|
+
var NotFoundError = defineError("NotFoundError");
|
|
105
|
+
var InvalidRequestError = defineError("InvalidRequestError");
|
|
106
|
+
var ConflictError = defineError("ConflictError");
|
|
107
|
+
|
|
108
|
+
// ../shared/src/connect-url.ts
|
|
109
|
+
var TOKEN_QUERY_PARAM = "token";
|
|
110
|
+
function parseConnectURL(input) {
|
|
111
|
+
const url = new URL(input);
|
|
112
|
+
const token = url.searchParams.get(TOKEN_QUERY_PARAM);
|
|
113
|
+
return { serverURL: url.origin, token };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/connect-url.ts
|
|
117
|
+
var ConnectURLError = class extends Error {
|
|
118
|
+
constructor(message = "Enter a valid http or https URL") {
|
|
119
|
+
super(message);
|
|
120
|
+
this.name = "ConnectURLError";
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
function parseDesktopConnectURL(input) {
|
|
124
|
+
const trimmed = input.trim();
|
|
125
|
+
if (!trimmed) throw new ConnectURLError();
|
|
126
|
+
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) && !/^https?:\/\//i.test(trimmed)) {
|
|
127
|
+
throw new ConnectURLError();
|
|
128
|
+
}
|
|
129
|
+
const withScheme = /^https?:\/\//i.test(trimmed) ? trimmed : `http://${trimmed}`;
|
|
130
|
+
let url;
|
|
131
|
+
try {
|
|
132
|
+
url = new URL(withScheme);
|
|
133
|
+
} catch {
|
|
134
|
+
throw new ConnectURLError();
|
|
135
|
+
}
|
|
136
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
137
|
+
throw new ConnectURLError();
|
|
138
|
+
}
|
|
139
|
+
const parsed = parseConnectURL(url.toString());
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
|
|
11
143
|
// src/connect-page.ts
|
|
12
144
|
var MIN_CONNECT_MS = 500;
|
|
13
145
|
function bindConnectPage(api, elements) {
|
|
@@ -48,10 +180,35 @@
|
|
|
48
180
|
setBusy(false);
|
|
49
181
|
}
|
|
50
182
|
}
|
|
183
|
+
function applyConnectURL(input) {
|
|
184
|
+
let parsed;
|
|
185
|
+
try {
|
|
186
|
+
parsed = parseDesktopConnectURL(input);
|
|
187
|
+
} catch {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
elements.serverInput.value = parsed.serverURL;
|
|
191
|
+
if (parsed.token !== null) {
|
|
192
|
+
elements.tokenInput.value = parsed.token;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const onPasteServerURL = (event) => {
|
|
196
|
+
const text = event.clipboardData?.getData("text");
|
|
197
|
+
if (!text) return;
|
|
198
|
+
try {
|
|
199
|
+
parseDesktopConnectURL(text);
|
|
200
|
+
} catch {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
applyConnectURL(text);
|
|
205
|
+
};
|
|
51
206
|
const onSubmit = (event) => {
|
|
52
207
|
event.preventDefault();
|
|
208
|
+
applyConnectURL(elements.serverInput.value);
|
|
53
209
|
void runConnect(elements.serverInput.value, elements.tokenInput.value);
|
|
54
210
|
};
|
|
211
|
+
elements.serverInput.addEventListener("paste", onPasteServerURL);
|
|
55
212
|
elements.form.addEventListener("submit", onSubmit);
|
|
56
213
|
void (async () => {
|
|
57
214
|
const intent = await api.getConnectScreenIntent();
|
|
@@ -64,7 +221,10 @@
|
|
|
64
221
|
await runConnect(saved.serverURL, saved.token);
|
|
65
222
|
}
|
|
66
223
|
})();
|
|
67
|
-
return () =>
|
|
224
|
+
return () => {
|
|
225
|
+
elements.serverInput.removeEventListener("paste", onPasteServerURL);
|
|
226
|
+
elements.form.removeEventListener("submit", onSubmit);
|
|
227
|
+
};
|
|
68
228
|
}
|
|
69
229
|
function initConnectPage(api) {
|
|
70
230
|
const form = document.getElementById("connect-form");
|
package/dist/electron.cjs
CHANGED
|
@@ -36,6 +36,111 @@ 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
|
+
// ../layout/src/constants.ts
|
|
40
|
+
var LAYOUT_FULL_WIDTH_UNITS = 4;
|
|
41
|
+
var LAYOUT_FULL_HEIGHT_UNITS = 6;
|
|
42
|
+
var DEFAULT_LAYOUT_UNIT = 128;
|
|
43
|
+
var CARD_GAP = 16;
|
|
44
|
+
var CARD_WIDTH = DEFAULT_LAYOUT_UNIT * LAYOUT_FULL_WIDTH_UNITS + CARD_GAP * (LAYOUT_FULL_WIDTH_UNITS - 1);
|
|
45
|
+
var CARD_HEIGHT = DEFAULT_LAYOUT_UNIT * LAYOUT_FULL_HEIGHT_UNITS + CARD_GAP * (LAYOUT_FULL_HEIGHT_UNITS - 1);
|
|
46
|
+
var LAYOUT_MOBILE_BREAKPOINT_PX = CARD_WIDTH + CARD_GAP * 2;
|
|
47
|
+
|
|
48
|
+
// ../../node_modules/@rupertsworld/event-target/dist/index.js
|
|
49
|
+
var RESERVED_EVENT_KEYS = /* @__PURE__ */ new Set([
|
|
50
|
+
"target",
|
|
51
|
+
"currentTarget",
|
|
52
|
+
"eventPhase",
|
|
53
|
+
"defaultPrevented",
|
|
54
|
+
"isTrusted",
|
|
55
|
+
"timeStamp",
|
|
56
|
+
"srcElement",
|
|
57
|
+
"returnValue",
|
|
58
|
+
"cancelBubble",
|
|
59
|
+
"NONE",
|
|
60
|
+
"CAPTURING_PHASE",
|
|
61
|
+
"AT_TARGET",
|
|
62
|
+
"BUBBLING_PHASE",
|
|
63
|
+
"composedPath",
|
|
64
|
+
"stopPropagation",
|
|
65
|
+
"stopImmediatePropagation",
|
|
66
|
+
"preventDefault",
|
|
67
|
+
"initEvent"
|
|
68
|
+
]);
|
|
69
|
+
function assertNoReservedPayloadKeys(props) {
|
|
70
|
+
for (const key of Object.keys(props)) {
|
|
71
|
+
if (RESERVED_EVENT_KEYS.has(key)) {
|
|
72
|
+
throw new Error(`Event payload key "${key}" is reserved; choose a different property name`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function assignPayload(target, props) {
|
|
77
|
+
assertNoReservedPayloadKeys(props);
|
|
78
|
+
for (const key of Object.keys(props)) {
|
|
79
|
+
Object.defineProperty(target, key, {
|
|
80
|
+
value: props[key],
|
|
81
|
+
writable: true,
|
|
82
|
+
enumerable: true,
|
|
83
|
+
configurable: true
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function defineEvent() {
|
|
88
|
+
class DefinedEvent extends Event {
|
|
89
|
+
constructor(type, init) {
|
|
90
|
+
const { type: initType, bubbles, cancelable, composed, ...payload } = init ?? {};
|
|
91
|
+
if (initType !== void 0) {
|
|
92
|
+
throw new Error(`Do not pass "type" in init; use the constructor argument instead`);
|
|
93
|
+
}
|
|
94
|
+
super(type, { bubbles, cancelable, composed });
|
|
95
|
+
assignPayload(this, payload);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return DefinedEvent;
|
|
99
|
+
}
|
|
100
|
+
var EventTarget = class extends globalThis.EventTarget {
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ../shared/src/events.ts
|
|
104
|
+
var ChangeEvent = defineEvent();
|
|
105
|
+
var ServerEventMessageEvent = defineEvent();
|
|
106
|
+
var LayoutMutationEvent = defineEvent();
|
|
107
|
+
var LayoutScrollEvent = defineEvent();
|
|
108
|
+
var ArtifactCreatedEvent = defineEvent();
|
|
109
|
+
var ArtifactUpdatedEvent = defineEvent();
|
|
110
|
+
var ArtifactRemovedEvent = defineEvent();
|
|
111
|
+
var ArtifactContentChangedEvent = defineEvent();
|
|
112
|
+
var ScreenCreatedEvent = defineEvent();
|
|
113
|
+
var ScreenUpdatedEvent = defineEvent();
|
|
114
|
+
var ScreenRemovedEvent = defineEvent();
|
|
115
|
+
var ScreenChangedEvent = defineEvent();
|
|
116
|
+
var ArtifactFocusEvent = defineEvent();
|
|
117
|
+
var ThemeChangedEvent = defineEvent();
|
|
118
|
+
|
|
119
|
+
// ../shared/src/errors.ts
|
|
120
|
+
function defineError(name) {
|
|
121
|
+
class DefinedError extends Error {
|
|
122
|
+
constructor(message, fields) {
|
|
123
|
+
super(message);
|
|
124
|
+
this.name = name;
|
|
125
|
+
if (fields) Object.assign(this, fields);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return DefinedError;
|
|
129
|
+
}
|
|
130
|
+
var RequestError = defineError("RequestError");
|
|
131
|
+
var ValidationError = defineError("ValidationError");
|
|
132
|
+
var NotFoundError = defineError("NotFoundError");
|
|
133
|
+
var InvalidRequestError = defineError("InvalidRequestError");
|
|
134
|
+
var ConflictError = defineError("ConflictError");
|
|
135
|
+
|
|
136
|
+
// ../shared/src/connect-url.ts
|
|
137
|
+
var TOKEN_QUERY_PARAM = "token";
|
|
138
|
+
function parseConnectURL(input) {
|
|
139
|
+
const url = new URL(input);
|
|
140
|
+
const token = url.searchParams.get(TOKEN_QUERY_PARAM);
|
|
141
|
+
return { serverURL: url.origin, token };
|
|
142
|
+
}
|
|
143
|
+
|
|
39
144
|
// src/connect-url.ts
|
|
40
145
|
var ConnectURLError = class extends Error {
|
|
41
146
|
constructor(message = "Enter a valid http or https URL") {
|
|
@@ -43,7 +148,7 @@ var ConnectURLError = class extends Error {
|
|
|
43
148
|
this.name = "ConnectURLError";
|
|
44
149
|
}
|
|
45
150
|
};
|
|
46
|
-
function
|
|
151
|
+
function parseDesktopConnectURL(input) {
|
|
47
152
|
const trimmed = input.trim();
|
|
48
153
|
if (!trimmed) throw new ConnectURLError();
|
|
49
154
|
if (/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) && !/^https?:\/\//i.test(trimmed)) {
|
|
@@ -59,7 +164,8 @@ function normalizeConnectURL(input) {
|
|
|
59
164
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
60
165
|
throw new ConnectURLError();
|
|
61
166
|
}
|
|
62
|
-
|
|
167
|
+
const parsed = parseConnectURL(url.toString());
|
|
168
|
+
return parsed;
|
|
63
169
|
}
|
|
64
170
|
function buildRemoteURL(serverURL, token) {
|
|
65
171
|
const url = new URL(serverURL);
|
|
@@ -135,8 +241,8 @@ function loadConnection() {
|
|
|
135
241
|
if (!(0, import_node_fs.existsSync)(file)) return null;
|
|
136
242
|
try {
|
|
137
243
|
const parsed = JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
|
|
138
|
-
if (typeof parsed.serverURL !== "string"
|
|
139
|
-
return { serverURL: parsed.serverURL, token: parsed.token };
|
|
244
|
+
if (typeof parsed.serverURL !== "string") return null;
|
|
245
|
+
return { serverURL: parsed.serverURL, token: typeof parsed.token === "string" ? parsed.token : "" };
|
|
140
246
|
} catch {
|
|
141
247
|
return null;
|
|
142
248
|
}
|
|
@@ -245,21 +351,14 @@ var App = class {
|
|
|
245
351
|
await this.window.loadFile(import_node_path2.default.join(__dirname, "connect.html"));
|
|
246
352
|
}
|
|
247
353
|
async tryConnect(raw) {
|
|
248
|
-
|
|
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;
|
|
354
|
+
let connection;
|
|
256
355
|
try {
|
|
257
|
-
|
|
356
|
+
const parsed = parseDesktopConnectURL(raw.serverURL);
|
|
357
|
+
connection = { serverURL: parsed.serverURL, token: parsed.token ?? raw.token ?? "" };
|
|
258
358
|
} catch (error) {
|
|
259
359
|
const message = error instanceof ConnectURLError ? error.message : new ConnectURLError().message;
|
|
260
360
|
return { ok: false, message };
|
|
261
361
|
}
|
|
262
|
-
const connection = { serverURL, token: submitted.token };
|
|
263
362
|
try {
|
|
264
363
|
const preflight = await preflightConnection({
|
|
265
364
|
serverURL: connection.serverURL,
|
|
@@ -268,6 +367,7 @@ var App = class {
|
|
|
268
367
|
if (!preflight.ok) {
|
|
269
368
|
return { ok: false, message: preflight.message };
|
|
270
369
|
}
|
|
370
|
+
saveConnection(connection);
|
|
271
371
|
await this.loadRemote(connection);
|
|
272
372
|
return { ok: true };
|
|
273
373
|
} catch (error) {
|
|
@@ -22,6 +22,49 @@ if (process.contextIsolated) {
|
|
|
22
22
|
} else {
|
|
23
23
|
window.__televisionContentBridge = contentBridge;
|
|
24
24
|
}
|
|
25
|
+
function startProxyContentPoll() {
|
|
26
|
+
const capturedURL = window.location.href;
|
|
27
|
+
const isTvArtifactURL = (url) => {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = new URL(url);
|
|
30
|
+
return /^\/artifact\/[0-9A-Za-z]{26}(\/|$)/.test(parsed.pathname);
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
if (!isTvArtifactURL(capturedURL) || typeof window.fetch !== "function") return;
|
|
36
|
+
const NORMAL_POLL_MS = 5e3;
|
|
37
|
+
const SLOW_POLL_MS = 15e3;
|
|
38
|
+
let baselineETag = null;
|
|
39
|
+
let delayMs = NORMAL_POLL_MS;
|
|
40
|
+
let stopped = false;
|
|
41
|
+
const poll = () => {
|
|
42
|
+
void (async () => {
|
|
43
|
+
try {
|
|
44
|
+
const response = await window.fetch(capturedURL, { method: "HEAD" });
|
|
45
|
+
if (!response.ok) throw new Error("Artifact poll failed");
|
|
46
|
+
const etag = response.headers.get("ETag");
|
|
47
|
+
if (!etag) throw new Error("Artifact poll missing ETag");
|
|
48
|
+
delayMs = NORMAL_POLL_MS;
|
|
49
|
+
if (baselineETag === null) {
|
|
50
|
+
baselineETag = etag;
|
|
51
|
+
} else if (etag !== baselineETag) {
|
|
52
|
+
baselineETag = etag;
|
|
53
|
+
import_electron.ipcRenderer.sendToHost(BRIDGE_CHANNEL, { type: "proxy-content-changed" });
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
delayMs = Math.min(SLOW_POLL_MS, delayMs * 2);
|
|
57
|
+
} finally {
|
|
58
|
+
if (!stopped) window.setTimeout(poll, delayMs);
|
|
59
|
+
}
|
|
60
|
+
})();
|
|
61
|
+
};
|
|
62
|
+
window.addEventListener("pagehide", () => {
|
|
63
|
+
stopped = true;
|
|
64
|
+
});
|
|
65
|
+
poll();
|
|
66
|
+
}
|
|
67
|
+
startProxyContentPoll();
|
|
25
68
|
function isTextEditingTarget(target) {
|
|
26
69
|
if (!(target instanceof Element)) {
|
|
27
70
|
return false;
|
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.161",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/electron.cjs",
|
|
7
7
|
"bin": {
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"type-check": "tsc -p tsconfig.json --noEmit"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@telepath-computer/television-shared": "*",
|
|
25
26
|
"electron": "35.7.5"
|
|
26
27
|
}
|
|
27
28
|
}
|