getobsrv 0.10.0 → 0.11.0
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/out/main/cli.js +1 -1
- package/out/main/index.js +123 -13
- package/out/main/{targetSource-CbJwfugi.js → targetSource-nbfc-Fe6.js} +8 -1
- package/out/preload/app.js +8 -2
- package/out/renderer/assets/{index-CZUlXhEb.css → index-B61fbgqt.css} +177 -10
- package/out/renderer/assets/{index-CQVE5hF3.js → index-BlbBg6lm.js} +282 -46
- package/out/renderer/index.html +2 -2
- package/out/shared/history.js +90 -0
- package/out/shared/ipcPayloads.js +13 -2
- package/out/shared/presets.js +7 -1
- package/package.json +1 -1
package/out/main/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ const electron = require("electron");
|
|
|
3
3
|
const node_fs = require("node:fs");
|
|
4
4
|
const node_os = require("node:os");
|
|
5
5
|
const node_path = require("node:path");
|
|
6
|
-
const targetSource = require("./targetSource-
|
|
6
|
+
const targetSource = require("./targetSource-nbfc-Fe6.js");
|
|
7
7
|
function boxDownsample(src, factor) {
|
|
8
8
|
if (!Number.isInteger(factor) || factor < 1) throw new RangeError("factor must be an integer >= 1");
|
|
9
9
|
const width = Math.floor(src.width / factor);
|
package/out/main/index.js
CHANGED
|
@@ -4,7 +4,7 @@ const node_fs = require("node:fs");
|
|
|
4
4
|
const promises = require("node:fs/promises");
|
|
5
5
|
const node_path = require("node:path");
|
|
6
6
|
const node_crypto = require("node:crypto");
|
|
7
|
-
const targetSource = require("./targetSource-
|
|
7
|
+
const targetSource = require("./targetSource-nbfc-Fe6.js");
|
|
8
8
|
const node_http = require("node:http");
|
|
9
9
|
const node_url = require("node:url");
|
|
10
10
|
const IPC = {
|
|
@@ -41,7 +41,10 @@ const IPC = {
|
|
|
41
41
|
getUpdate: "obsrv:get-update",
|
|
42
42
|
checkUpdate: "obsrv:check-update",
|
|
43
43
|
openRelease: "obsrv:open-release",
|
|
44
|
-
updateStatus: "obsrv:update-status"
|
|
44
|
+
updateStatus: "obsrv:update-status",
|
|
45
|
+
getHistory: "obsrv:get-history",
|
|
46
|
+
clearHistory: "obsrv:clear-history",
|
|
47
|
+
historyChanged: "obsrv:history-changed"
|
|
45
48
|
};
|
|
46
49
|
function attachFrameBus(target, win) {
|
|
47
50
|
let ready = false;
|
|
@@ -79,9 +82,9 @@ const MAX_SCROLL_SELECTOR = 512;
|
|
|
79
82
|
const MAX_RECT = 16384;
|
|
80
83
|
const MAX_SCROLL_WARNINGS = 4;
|
|
81
84
|
const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
82
|
-
const isRecord$
|
|
85
|
+
const isRecord$2 = (v) => typeof v === "object" && v !== null;
|
|
83
86
|
function parseRect(raw) {
|
|
84
|
-
if (!isRecord$
|
|
87
|
+
if (!isRecord$2(raw)) return null;
|
|
85
88
|
const { x, y, width, height } = raw;
|
|
86
89
|
if (!isFiniteNumber(x) || !isFiniteNumber(y) || !isFiniteNumber(width) || !isFiniteNumber(height)) return null;
|
|
87
90
|
const r = { x: Math.round(x), y: Math.round(y), width: Math.round(width), height: Math.round(height) };
|
|
@@ -103,7 +106,7 @@ function parseModifiers(raw) {
|
|
|
103
106
|
return raw.filter((m) => typeof m === "string" && MODIFIERS.has(m));
|
|
104
107
|
}
|
|
105
108
|
function parseInputEvent(raw) {
|
|
106
|
-
if (!isRecord$
|
|
109
|
+
if (!isRecord$2(raw)) return null;
|
|
107
110
|
const modifiers = parseModifiers(raw.modifiers);
|
|
108
111
|
switch (raw.type) {
|
|
109
112
|
case "mouseDown":
|
|
@@ -136,7 +139,7 @@ function parseDeviceScaleFactor(raw) {
|
|
|
136
139
|
return raw;
|
|
137
140
|
}
|
|
138
141
|
function parseSettings(raw) {
|
|
139
|
-
if (!isRecord$
|
|
142
|
+
if (!isRecord$2(raw)) return null;
|
|
140
143
|
const { hostDiagonalInches, hostNits } = raw;
|
|
141
144
|
if (!isFiniteNumber(hostDiagonalInches) || hostDiagonalInches <= 0) return null;
|
|
142
145
|
if (!isFiniteNumber(hostNits) || hostNits <= 0) return null;
|
|
@@ -146,14 +149,18 @@ function parseSettings(raw) {
|
|
|
146
149
|
if (typeof updateCheck !== "boolean") return null;
|
|
147
150
|
const lastUpdateCheck = raw.lastUpdateCheck ?? 0;
|
|
148
151
|
if (!isFiniteNumber(lastUpdateCheck) || lastUpdateCheck < 0) return null;
|
|
149
|
-
|
|
152
|
+
const recordHistory = raw.recordHistory ?? true;
|
|
153
|
+
if (typeof recordHistory !== "boolean") return null;
|
|
154
|
+
const split = raw.split ?? targetSource.DEFAULT_SETTINGS.split;
|
|
155
|
+
if (!isFiniteNumber(split) || split < targetSource.SPLIT_MIN || split > targetSource.SPLIT_MAX) return null;
|
|
156
|
+
return { hostDiagonalInches, hostNits, agentControl, updateCheck, lastUpdateCheck, recordHistory, split };
|
|
150
157
|
}
|
|
151
158
|
function parseMode(raw) {
|
|
152
159
|
return raw === "url" || raw === "image" ? raw : null;
|
|
153
160
|
}
|
|
154
161
|
const MAX_UI_ID = 64;
|
|
155
162
|
function parseUiState(raw) {
|
|
156
|
-
if (!isRecord$
|
|
163
|
+
if (!isRecord$2(raw)) return null;
|
|
157
164
|
const { presetId, profileId, viewMode, mode } = raw;
|
|
158
165
|
if (typeof presetId !== "string" || presetId.length === 0 || presetId.length > MAX_UI_ID) return null;
|
|
159
166
|
if (typeof profileId !== "string" || profileId.length === 0 || profileId.length > MAX_UI_ID) return null;
|
|
@@ -172,7 +179,7 @@ function parseUiState(raw) {
|
|
|
172
179
|
};
|
|
173
180
|
}
|
|
174
181
|
function parseScrollPos(raw) {
|
|
175
|
-
if (!isRecord$
|
|
182
|
+
if (!isRecord$2(raw)) return null;
|
|
176
183
|
const { x, y } = raw;
|
|
177
184
|
if (!isFiniteNumber(x) || !isFiniteNumber(y) || x < 0 || y < 0) return null;
|
|
178
185
|
return { x, y };
|
|
@@ -189,7 +196,7 @@ function parseScrollRequest(raw) {
|
|
|
189
196
|
return { ...pos, selector: trimmed };
|
|
190
197
|
}
|
|
191
198
|
function parseScrollReport(raw) {
|
|
192
|
-
if (!isRecord$
|
|
199
|
+
if (!isRecord$2(raw)) return null;
|
|
193
200
|
const { id, x, y, scroller } = raw;
|
|
194
201
|
if (!isFiniteNumber(id)) return null;
|
|
195
202
|
if (!isFiniteNumber(x) || !isFiniteNumber(y)) return null;
|
|
@@ -222,7 +229,7 @@ const CONTROL_COMMANDS = [
|
|
|
222
229
|
function isControlCommand(v) {
|
|
223
230
|
return typeof v === "string" && CONTROL_COMMANDS.includes(v);
|
|
224
231
|
}
|
|
225
|
-
const isRecord = (v) => typeof v === "object" && v !== null;
|
|
232
|
+
const isRecord$1 = (v) => typeof v === "object" && v !== null;
|
|
226
233
|
function tokenEqual(expected, provided) {
|
|
227
234
|
if (typeof provided !== "string") return false;
|
|
228
235
|
const a = node_crypto.createHash("sha256").update(expected).digest();
|
|
@@ -258,7 +265,7 @@ function pixelExactApplyError(v) {
|
|
|
258
265
|
}
|
|
259
266
|
function parseClick(raw, viewport) {
|
|
260
267
|
const shape = "click payload must be { x, y, button? } with finite CSS-pixel coordinates";
|
|
261
|
-
if (!isRecord(raw)) return shape;
|
|
268
|
+
if (!isRecord$1(raw)) return shape;
|
|
262
269
|
const { x, y } = raw;
|
|
263
270
|
if (typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) return shape;
|
|
264
271
|
if (x < 0 || y < 0 || x >= viewport.width || y >= viewport.height) {
|
|
@@ -285,8 +292,71 @@ function parseHighlight(raw) {
|
|
|
285
292
|
durationMs: Math.min(Math.max(Math.round(d), HIGHLIGHT_DURATION_MIN_MS), HIGHLIGHT_DURATION_MAX_MS)
|
|
286
293
|
};
|
|
287
294
|
}
|
|
295
|
+
const HISTORY_MAX = 500;
|
|
296
|
+
const MAX_URL_LENGTH = 2048;
|
|
297
|
+
const isRecord = (v) => typeof v === "object" && v !== null;
|
|
298
|
+
const isCount = (v) => typeof v === "number" && Number.isInteger(v) && v >= 1;
|
|
299
|
+
const isStamp$1 = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
300
|
+
function isStorableUrl(v) {
|
|
301
|
+
if (typeof v !== "string" || v === "" || v.length > MAX_URL_LENGTH) return false;
|
|
302
|
+
try {
|
|
303
|
+
return targetSource.ALLOWED_URL_SCHEMES.includes(new URL(v).protocol);
|
|
304
|
+
} catch {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function byRank(a, b) {
|
|
309
|
+
if (a.lastVisit !== b.lastVisit) return b.lastVisit - a.lastVisit;
|
|
310
|
+
if (a.visits !== b.visits) return b.visits - a.visits;
|
|
311
|
+
return a.url < b.url ? -1 : a.url > b.url ? 1 : 0;
|
|
312
|
+
}
|
|
313
|
+
function recordVisit(entries, url, now) {
|
|
314
|
+
if (!isStorableUrl(url) || !isStamp$1(now)) return entries;
|
|
315
|
+
const previous = entries.find((e) => e.url === url);
|
|
316
|
+
const next = {
|
|
317
|
+
url,
|
|
318
|
+
visits: previous ? previous.visits + 1 : 1,
|
|
319
|
+
lastVisit: now
|
|
320
|
+
};
|
|
321
|
+
return [next, ...entries.filter((e) => e.url !== url)].sort(byRank).slice(0, HISTORY_MAX);
|
|
322
|
+
}
|
|
323
|
+
function loadHistory(file) {
|
|
324
|
+
try {
|
|
325
|
+
const raw = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
326
|
+
if (!Array.isArray(raw)) return [];
|
|
327
|
+
const seen = /* @__PURE__ */ new Set();
|
|
328
|
+
const out = [];
|
|
329
|
+
for (const item of raw) {
|
|
330
|
+
if (!isRecord(item)) continue;
|
|
331
|
+
if (!isStorableUrl(item.url) || seen.has(item.url)) continue;
|
|
332
|
+
seen.add(item.url);
|
|
333
|
+
out.push({
|
|
334
|
+
url: item.url,
|
|
335
|
+
// These two do fall back: a row whose counter got mangled is still a
|
|
336
|
+
// page that was visited, and losing its rank beats losing the row.
|
|
337
|
+
visits: isCount(item.visits) ? item.visits : 1,
|
|
338
|
+
lastVisit: isStamp$1(item.lastVisit) ? item.lastVisit : 0
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return out.sort(byRank).slice(0, HISTORY_MAX);
|
|
342
|
+
} catch {
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function saveHistory(file, entries) {
|
|
347
|
+
if (!Array.isArray(entries)) throw new RangeError("history must be an array");
|
|
348
|
+
if (entries.length > HISTORY_MAX) throw new RangeError(`history must hold at most ${HISTORY_MAX} entries`);
|
|
349
|
+
for (const e of entries) {
|
|
350
|
+
if (!isRecord(e) || !isStorableUrl(e.url)) throw new RangeError("history entries need a loadable url");
|
|
351
|
+
if (!isCount(e.visits)) throw new RangeError("visits must be an integer >= 1");
|
|
352
|
+
if (!isStamp$1(e.lastVisit)) throw new RangeError("lastVisit must be a finite epoch ms >= 0");
|
|
353
|
+
}
|
|
354
|
+
node_fs.mkdirSync(node_path.dirname(file), { recursive: true });
|
|
355
|
+
node_fs.writeFileSync(file, JSON.stringify(entries, null, 2));
|
|
356
|
+
}
|
|
288
357
|
const isPositive = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
289
358
|
const isStamp = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
|
|
359
|
+
const isRatio = (v) => typeof v === "number" && Number.isFinite(v) && v >= targetSource.SPLIT_MIN && v <= targetSource.SPLIT_MAX;
|
|
290
360
|
function loadSettings(file) {
|
|
291
361
|
try {
|
|
292
362
|
const raw = JSON.parse(node_fs.readFileSync(file, "utf8"));
|
|
@@ -299,7 +369,16 @@ function loadSettings(file) {
|
|
|
299
369
|
// The opposite default: only a literal false turns the update check off,
|
|
300
370
|
// so a file from before this feature keeps it on.
|
|
301
371
|
updateCheck: raw.updateCheck !== false,
|
|
302
|
-
lastUpdateCheck: isStamp(raw.lastUpdateCheck) ? raw.lastUpdateCheck : 0
|
|
372
|
+
lastUpdateCheck: isStamp(raw.lastUpdateCheck) ? raw.lastUpdateCheck : 0,
|
|
373
|
+
// Same default as `updateCheck`, for the same reason: a file written
|
|
374
|
+
// before this feature existed has no key, and history is on by default.
|
|
375
|
+
recordHistory: raw.recordHistory !== false,
|
|
376
|
+
// A ratio outside 0.1–0.9 is treated as absent, not rejected: unlike a
|
|
377
|
+
// nits or diagonal figure it is a layout preference, and a file that
|
|
378
|
+
// was hand-edited to 0.97 means "I want the target tiny", not "refuse
|
|
379
|
+
// to start". Falling back to an even split loses one preference; the
|
|
380
|
+
// panes stay usable, which is the point of the band.
|
|
381
|
+
split: isRatio(raw.split) ? raw.split : targetSource.DEFAULT_SETTINGS.split
|
|
303
382
|
};
|
|
304
383
|
} catch {
|
|
305
384
|
return { ...targetSource.DEFAULT_SETTINGS };
|
|
@@ -310,6 +389,8 @@ function saveSettings(file, s) {
|
|
|
310
389
|
if (typeof s.agentControl !== "boolean") throw new RangeError("agentControl must be a boolean");
|
|
311
390
|
if (typeof s.updateCheck !== "boolean") throw new RangeError("updateCheck must be a boolean");
|
|
312
391
|
if (!isStamp(s.lastUpdateCheck)) throw new RangeError("lastUpdateCheck must be a finite epoch ms >= 0");
|
|
392
|
+
if (typeof s.recordHistory !== "boolean") throw new RangeError("recordHistory must be a boolean");
|
|
393
|
+
if (!isRatio(s.split)) throw new RangeError(`split must be a finite ratio in ${targetSource.SPLIT_MIN}..${targetSource.SPLIT_MAX}`);
|
|
313
394
|
node_fs.mkdirSync(node_path.dirname(file), { recursive: true });
|
|
314
395
|
node_fs.writeFileSync(file, JSON.stringify(s, null, 2));
|
|
315
396
|
}
|
|
@@ -904,6 +985,35 @@ function registerIpc(ctx) {
|
|
|
904
985
|
await electron.shell.openExternal(releaseUrl);
|
|
905
986
|
return true;
|
|
906
987
|
});
|
|
988
|
+
const historyFile = node_path.join(electron.app.getPath("userData"), "history.json");
|
|
989
|
+
let history = loadHistory(historyFile);
|
|
990
|
+
const publishHistory = () => {
|
|
991
|
+
if (!win.isDestroyed()) win.webContents.send(IPC.historyChanged, history);
|
|
992
|
+
};
|
|
993
|
+
const commitHistory = (next) => {
|
|
994
|
+
try {
|
|
995
|
+
saveHistory(historyFile, next);
|
|
996
|
+
} catch (e) {
|
|
997
|
+
console.warn("obsrv: could not write history.json", e);
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
history = next;
|
|
1001
|
+
publishHistory();
|
|
1002
|
+
};
|
|
1003
|
+
native.webContents.on("did-navigate", (_e, url) => {
|
|
1004
|
+
if (!settings.recordHistory) return;
|
|
1005
|
+
const next = recordVisit(history, url, Date.now());
|
|
1006
|
+
if (next === history) return;
|
|
1007
|
+
commitHistory(next);
|
|
1008
|
+
});
|
|
1009
|
+
electron.ipcMain.handle(IPC.getHistory, (e) => {
|
|
1010
|
+
assertRenderer(e);
|
|
1011
|
+
return history;
|
|
1012
|
+
});
|
|
1013
|
+
electron.ipcMain.handle(IPC.clearHistory, (e) => {
|
|
1014
|
+
assertRenderer(e);
|
|
1015
|
+
commitHistory([]);
|
|
1016
|
+
});
|
|
907
1017
|
const bootCheckAllowed = process.env.OBSRV_TEST !== "1" || process.env.OBSRV_RELEASES_API !== void 0;
|
|
908
1018
|
if (bootCheckAllowed && settings.updateCheck && isCheckDue(settings.lastUpdateCheck, Date.now())) {
|
|
909
1019
|
void runUpdateCheck();
|
|
@@ -3,12 +3,16 @@ const electron = require("electron");
|
|
|
3
3
|
const node_events = require("node:events");
|
|
4
4
|
const node_path = require("node:path");
|
|
5
5
|
const MAX_VIEWPORT = 4096;
|
|
6
|
+
const SPLIT_MIN = 0.1;
|
|
7
|
+
const SPLIT_MAX = 0.9;
|
|
6
8
|
const DEFAULT_SETTINGS = {
|
|
7
9
|
hostDiagonalInches: 27,
|
|
8
10
|
hostNits: 500,
|
|
9
11
|
agentControl: false,
|
|
10
12
|
updateCheck: true,
|
|
11
|
-
lastUpdateCheck: 0
|
|
13
|
+
lastUpdateCheck: 0,
|
|
14
|
+
recordHistory: true,
|
|
15
|
+
split: 0.5
|
|
12
16
|
};
|
|
13
17
|
const SCREEN_PRESETS = [
|
|
14
18
|
// Laptops — ordered largest to smallest panel, then the denser 1080p outlier.
|
|
@@ -365,10 +369,13 @@ class TargetSource extends node_events.EventEmitter {
|
|
|
365
369
|
if (!this.win.isDestroyed()) this.win.destroy();
|
|
366
370
|
}
|
|
367
371
|
}
|
|
372
|
+
exports.ALLOWED_URL_SCHEMES = ALLOWED_URL_SCHEMES;
|
|
368
373
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
369
374
|
exports.IMAGE_EXTENSIONS = IMAGE_EXTENSIONS;
|
|
370
375
|
exports.PANEL_PROFILES = PANEL_PROFILES;
|
|
371
376
|
exports.SCREEN_PRESETS = SCREEN_PRESETS;
|
|
377
|
+
exports.SPLIT_MAX = SPLIT_MAX;
|
|
378
|
+
exports.SPLIT_MIN = SPLIT_MIN;
|
|
372
379
|
exports.TargetSource = TargetSource;
|
|
373
380
|
exports.classifyFileNavigation = classifyFileNavigation;
|
|
374
381
|
exports.findPreset = findPreset;
|
package/out/preload/app.js
CHANGED
|
@@ -31,7 +31,10 @@ const IPC = {
|
|
|
31
31
|
getUpdate: "obsrv:get-update",
|
|
32
32
|
checkUpdate: "obsrv:check-update",
|
|
33
33
|
openRelease: "obsrv:open-release",
|
|
34
|
-
updateStatus: "obsrv:update-status"
|
|
34
|
+
updateStatus: "obsrv:update-status",
|
|
35
|
+
getHistory: "obsrv:get-history",
|
|
36
|
+
clearHistory: "obsrv:clear-history",
|
|
37
|
+
historyChanged: "obsrv:history-changed"
|
|
35
38
|
};
|
|
36
39
|
function subscribe(channel, cb) {
|
|
37
40
|
const listener = (_e, v) => cb(v);
|
|
@@ -112,6 +115,9 @@ const api = {
|
|
|
112
115
|
getUpdate: () => electron.ipcRenderer.invoke(IPC.getUpdate),
|
|
113
116
|
checkUpdate: () => electron.ipcRenderer.invoke(IPC.checkUpdate),
|
|
114
117
|
openRelease: () => electron.ipcRenderer.invoke(IPC.openRelease),
|
|
115
|
-
onUpdateStatus: (cb) => subscribe(IPC.updateStatus, cb)
|
|
118
|
+
onUpdateStatus: (cb) => subscribe(IPC.updateStatus, cb),
|
|
119
|
+
getHistory: () => electron.ipcRenderer.invoke(IPC.getHistory),
|
|
120
|
+
clearHistory: () => electron.ipcRenderer.invoke(IPC.clearHistory),
|
|
121
|
+
onHistoryChanged: (cb) => subscribe(IPC.historyChanged, cb)
|
|
116
122
|
};
|
|
117
123
|
electron.contextBridge.exposeInMainWorld("obsrv", api);
|
|
@@ -129,7 +129,43 @@
|
|
|
129
129
|
:root[data-surround='grey50'] { --surround: #808080; }
|
|
130
130
|
|
|
131
131
|
html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color: var(--text-0); }
|
|
132
|
-
|
|
132
|
+
/* `--split` (the native pane's share), `--pane-min` and `--seam` live here
|
|
133
|
+
rather than on `.panes` because the chrome needs them too — see
|
|
134
|
+
`--native-right` below. `App` writes the first two inline; the fallbacks
|
|
135
|
+
keep the layout sane if either is ever missing. */
|
|
136
|
+
.app {
|
|
137
|
+
height: 100%;
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
--split: 0.5;
|
|
141
|
+
--pane-min: 240px;
|
|
142
|
+
--seam: 1px;
|
|
143
|
+
--drawer-w: 0px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* The drawer's whole box. Declared here so the panes row's width can be
|
|
147
|
+
derived from the window's without measuring either; `.drawer` sizes itself
|
|
148
|
+
from the same value, so the two cannot drift. */
|
|
149
|
+
.app:has(.drawer) { --drawer-w: 309px; }
|
|
150
|
+
|
|
151
|
+
/* Where the native pane's right edge falls, in window coordinates: the very
|
|
152
|
+
clamp `.panes > .pane:first-child` is laid out with, applied to the width
|
|
153
|
+
the row gets (the window, less whatever a drawer takes). A style rule, not
|
|
154
|
+
a measurement — the split is already a custom property, so nothing here
|
|
155
|
+
observes the layout it is describing.
|
|
156
|
+
Solo target has no native view, so the clamp collapses to zero. */
|
|
157
|
+
.app[data-panes='both'] {
|
|
158
|
+
--native-right: round(
|
|
159
|
+
down,
|
|
160
|
+
clamp(
|
|
161
|
+
var(--pane-min),
|
|
162
|
+
calc((100vw - var(--drawer-w) - var(--seam)) * var(--split)),
|
|
163
|
+
calc(100vw - var(--drawer-w) - var(--seam) - var(--pane-min))
|
|
164
|
+
),
|
|
165
|
+
1px
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
.app[data-panes='target'] { --native-right: 0px; }
|
|
133
169
|
.muted { color: var(--text-1); }
|
|
134
170
|
.warn { color: var(--warn); }
|
|
135
171
|
.chrome .warn { white-space: nowrap; }
|
|
@@ -148,6 +184,10 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
148
184
|
border-bottom: 1px solid var(--line);
|
|
149
185
|
}
|
|
150
186
|
.chrome-browse { height: 44px; background: var(--chrome-1); }
|
|
187
|
+
/* The containing block the URL-history list falls back to. Its padding box
|
|
188
|
+
starts at the window's left edge (the row's only border is at the bottom),
|
|
189
|
+
which is the frame `--native-right` is expressed in. */
|
|
190
|
+
.chrome-browse { position: relative; }
|
|
151
191
|
.chrome-screen { height: 38px; background: var(--chrome-0); }
|
|
152
192
|
.chrome-spacer { flex: 1 1 auto; }
|
|
153
193
|
|
|
@@ -168,7 +208,12 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
168
208
|
cursor: pointer;
|
|
169
209
|
}
|
|
170
210
|
|
|
171
|
-
|
|
211
|
+
/* Static in Both, so the history list below resolves against `.chrome-browse`
|
|
212
|
+
and can be placed in window coordinates. In solo target there is no native
|
|
213
|
+
view to dodge and the list belongs under the field, so the form becomes the
|
|
214
|
+
containing block instead. */
|
|
215
|
+
.url-form { position: static; flex: 1 1 auto; min-width: 160px; }
|
|
216
|
+
.app[data-panes='target'] .url-form { position: relative; }
|
|
172
217
|
.url-form input {
|
|
173
218
|
width: 100%;
|
|
174
219
|
height: 30px;
|
|
@@ -182,6 +227,61 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
182
227
|
}
|
|
183
228
|
.url-form input[readonly] { color: var(--text-1); }
|
|
184
229
|
|
|
230
|
+
/* Visited-URL type-ahead. Its left edge is clamped to the native pane's right
|
|
231
|
+
edge because that pane is an OS-level WebContentsView painting over
|
|
232
|
+
anything the renderer draws beneath it: a list anchored to the field, which
|
|
233
|
+
starts near x=120, would be entirely invisible. The cost is cosmetic — with
|
|
234
|
+
a wide native pane the list sits offset from the field it belongs to — and
|
|
235
|
+
it is the same constraint that pushed the overflow menu rightward.
|
|
236
|
+
`right` bounds it against the window so a very wide split cannot push it
|
|
237
|
+
off the edge; `max-width` keeps it from spanning a whole empty pane. */
|
|
238
|
+
.url-history {
|
|
239
|
+
position: absolute;
|
|
240
|
+
top: calc(100% + 4px);
|
|
241
|
+
left: var(--native-right, 0px);
|
|
242
|
+
right: 10px;
|
|
243
|
+
max-width: 560px;
|
|
244
|
+
z-index: 10;
|
|
245
|
+
margin: 0;
|
|
246
|
+
padding: 4px 0;
|
|
247
|
+
list-style: none;
|
|
248
|
+
font-size: 11.5px;
|
|
249
|
+
background: var(--chrome-2);
|
|
250
|
+
border: 1px solid var(--line);
|
|
251
|
+
border-radius: 6px;
|
|
252
|
+
}
|
|
253
|
+
/* Solo target: the list is the field's own dropdown, so it tracks its width. */
|
|
254
|
+
.app[data-panes='target'] .url-history { left: 0; right: 0; max-width: none; }
|
|
255
|
+
.url-history-row {
|
|
256
|
+
display: flex;
|
|
257
|
+
align-items: baseline;
|
|
258
|
+
gap: 10px;
|
|
259
|
+
height: 26px;
|
|
260
|
+
padding: 0 11px;
|
|
261
|
+
cursor: pointer;
|
|
262
|
+
}
|
|
263
|
+
/* The chrome's one selected idiom, shared with `.menu-row[aria-pressed]`. */
|
|
264
|
+
.url-history-row.active { background: var(--chrome-3); box-shadow: inset 2px 0 0 var(--text-0); }
|
|
265
|
+
/* Truncated at the *start*, not the end. What distinguishes one of these
|
|
266
|
+
addresses from another is the tail — the port, the path, the filename —
|
|
267
|
+
and right-truncating a list of localhost URLs or file paths yields six
|
|
268
|
+
identical rows, which is what the first screenshot of this list showed.
|
|
269
|
+
`direction: rtl` puts the ellipsis on the leading edge; the `bdi` around
|
|
270
|
+
the URL keeps the characters themselves in left-to-right order. */
|
|
271
|
+
.url-history-url {
|
|
272
|
+
flex: 1 1 auto;
|
|
273
|
+
min-width: 0;
|
|
274
|
+
overflow: hidden;
|
|
275
|
+
text-overflow: ellipsis;
|
|
276
|
+
white-space: nowrap;
|
|
277
|
+
direction: rtl;
|
|
278
|
+
/* The box is right-to-left only to move the ellipsis; a URL short enough to
|
|
279
|
+
fit must still start at the left edge like every other row. */
|
|
280
|
+
text-align: left;
|
|
281
|
+
font-family: var(--mono);
|
|
282
|
+
}
|
|
283
|
+
.url-history-age { flex: 0 0 auto; color: var(--text-1); font-size: 10px; }
|
|
284
|
+
|
|
185
285
|
.status-cluster { flex: 0 0 auto; display: flex; align-items: center; gap: 6px; font-size: 10px; }
|
|
186
286
|
.badge-error {
|
|
187
287
|
color: var(--error);
|
|
@@ -335,13 +435,71 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
335
435
|
|
|
336
436
|
.panes { flex: 1 1 auto; display: flex; min-height: 0; }
|
|
337
437
|
.pane {
|
|
338
|
-
flex: 1 1
|
|
438
|
+
flex: 1 1 0;
|
|
339
439
|
min-width: 0;
|
|
340
440
|
display: flex;
|
|
341
441
|
flex-direction: column;
|
|
342
442
|
background: var(--surround);
|
|
343
443
|
}
|
|
344
|
-
|
|
444
|
+
/* The native (or image) pane takes its share of everything the seam leaves;
|
|
445
|
+
the target pane grows into the rest, so the 1px divider comes out of the
|
|
446
|
+
row rather than out of one pane's half and skewing the ratio.
|
|
447
|
+
|
|
448
|
+
The `clamp()` is the *stored* ratio's floor, and the reason it lives in CSS
|
|
449
|
+
rather than in the drag: a ratio saved on a wide monitor and restored on a
|
|
450
|
+
narrow window is applied as close as 240px a side allows, without being
|
|
451
|
+
written back — grow the window and the preference returns on its own, with
|
|
452
|
+
no resize plumbing to keep in step. The drag enforces the same floor in JS,
|
|
453
|
+
where it also decides what to persist. */
|
|
454
|
+
.app[data-panes='both'] .panes > .pane:first-child {
|
|
455
|
+
flex-grow: 0;
|
|
456
|
+
flex-shrink: 0;
|
|
457
|
+
/* Rounded to whole pixels, and this is load-bearing. `TargetCanvas`
|
|
458
|
+
measures its pane with `clientWidth`, which *rounds*: a 1129.5px pane
|
|
459
|
+
reads as 1130, fit sizes the canvas to 1130, and the render then
|
|
460
|
+
overflows its own pane by half a pixel — which quietly turns off the
|
|
461
|
+
`safe center` centring and leaves the pane scrollable. Whole-pixel panes
|
|
462
|
+
keep the measurement honest. (The old `flex: 1 1 50%` hit the same thing
|
|
463
|
+
on an odd-width window; taking the seam out of the row would have made
|
|
464
|
+
it every window.) */
|
|
465
|
+
flex-basis: round(
|
|
466
|
+
down,
|
|
467
|
+
clamp(
|
|
468
|
+
var(--pane-min),
|
|
469
|
+
calc((100% - var(--seam)) * var(--split)),
|
|
470
|
+
calc(100% - var(--seam) - var(--pane-min))
|
|
471
|
+
),
|
|
472
|
+
1px
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/* The seam. A flex item exactly one pixel wide — the hairline the target pane
|
|
477
|
+
used to draw as its own `border-left` — with the grab area straddling it as
|
|
478
|
+
an overflowing pseudo-element, so the handle is 6px to the pointer and 1px
|
|
479
|
+
to the eye. The style spec forbids decorating anything that touches a pane,
|
|
480
|
+
and a resize handle drawn as furniture would be decoration. */
|
|
481
|
+
.pane-divider {
|
|
482
|
+
flex: 0 0 var(--seam);
|
|
483
|
+
/* Positioned *and* raised: `.target-wrap` next door is positioned too and
|
|
484
|
+
comes later in the DOM, so without a `z-index` the target pane's canvas
|
|
485
|
+
paints over the half of the grab area that overhangs it and swallows the
|
|
486
|
+
press. */
|
|
487
|
+
position: relative;
|
|
488
|
+
z-index: 1;
|
|
489
|
+
background: var(--line);
|
|
490
|
+
cursor: col-resize;
|
|
491
|
+
/* Or the drag scrolls the pane instead of moving the seam. */
|
|
492
|
+
touch-action: none;
|
|
493
|
+
}
|
|
494
|
+
.pane-divider::before {
|
|
495
|
+
content: '';
|
|
496
|
+
position: absolute;
|
|
497
|
+
inset: 0 -2.5px;
|
|
498
|
+
}
|
|
499
|
+
.pane-divider:hover, .pane-divider:focus-visible { background: var(--text-1); }
|
|
500
|
+
/* The hairline is the affordance; a ring around a 1px element would be a box
|
|
501
|
+
floating on the seam. Same reasoning as `.target-canvas:focus`. */
|
|
502
|
+
.pane-divider:focus-visible { outline: none; }
|
|
345
503
|
/* Square corners, no shadow, no blur: a rounded or blurred pane edge would
|
|
346
504
|
destroy the very corner pixels the user opened this app to inspect. */
|
|
347
505
|
.pane-body { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
|
@@ -353,7 +511,12 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
353
511
|
on the very pixels under inspection. Keyboard focus in the target pane is
|
|
354
512
|
shown on the pane's own hairlines instead, which touch no page pixel. */
|
|
355
513
|
.target-canvas:focus { outline: none; }
|
|
356
|
-
|
|
514
|
+
/* The seam moved out of `.target-pane`'s border and into its own element, so
|
|
515
|
+
the focus signal reaches it from the row both panes share — `:has` on the
|
|
516
|
+
ancestor, since the divider is the canvas's *earlier* sibling's sibling and
|
|
517
|
+
no combinator walks backwards. The footer rule is unchanged, and carries
|
|
518
|
+
the whole signal on its own in solo target, where there is no seam. */
|
|
519
|
+
.panes:has(.target-canvas:focus-visible) .pane-divider { background: var(--text-1); }
|
|
357
520
|
.target-pane:has(.target-canvas:focus-visible) .pane-footer { border-top-color: var(--text-1); }
|
|
358
521
|
|
|
359
522
|
/* Signature: each pane always states what it is showing, so a screenshot of
|
|
@@ -406,9 +569,6 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
406
569
|
.surround-control button[aria-pressed='true'] { box-shadow: inset 0 0 0 2px var(--text-0); }
|
|
407
570
|
.surround-swatch { display: block; width: 12px; height: 12px; margin: 0 auto; }
|
|
408
571
|
|
|
409
|
-
/* Nothing sits to the target pane's left in solo view, so the divider goes. */
|
|
410
|
-
.panes[data-panes='target'] .target-pane { border-left: 0; }
|
|
411
|
-
|
|
412
572
|
/* Numbers never jitter as a slider moves. */
|
|
413
573
|
.num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
414
574
|
|
|
@@ -431,7 +591,11 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
431
591
|
.body { flex: 1 1 auto; display: flex; min-height: 0; }
|
|
432
592
|
.panes { min-width: 0; }
|
|
433
593
|
.drawer {
|
|
434
|
-
|
|
594
|
+
/* Border-box against `--drawer-w`: 309 - 28 padding - 1 border = the 280px
|
|
595
|
+
of content this has always had, now stated once for both this and the
|
|
596
|
+
URL-history clamp. */
|
|
597
|
+
box-sizing: border-box;
|
|
598
|
+
flex: 0 0 var(--drawer-w);
|
|
435
599
|
overflow: auto;
|
|
436
600
|
padding: 12px 14px;
|
|
437
601
|
background: var(--chrome-1);
|
|
@@ -664,7 +828,8 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
664
828
|
cursor: pointer;
|
|
665
829
|
font: inherit;
|
|
666
830
|
}
|
|
667
|
-
.check-now
|
|
831
|
+
.check-now,
|
|
832
|
+
.clear-history {
|
|
668
833
|
width: 100%;
|
|
669
834
|
height: 24px;
|
|
670
835
|
margin-top: 8px;
|
|
@@ -674,3 +839,5 @@ html, body, #root { margin: 0; height: 100%; background: var(--chrome-0); color:
|
|
|
674
839
|
border-radius: 4px;
|
|
675
840
|
cursor: pointer;
|
|
676
841
|
}
|
|
842
|
+
/* Nothing to erase: the button says so rather than pretending to act. */
|
|
843
|
+
.clear-history:disabled { color: var(--text-1); cursor: default; }
|
|
@@ -12688,12 +12688,16 @@ const createImpl = (createState) => {
|
|
|
12688
12688
|
};
|
|
12689
12689
|
const create = ((createState) => createImpl);
|
|
12690
12690
|
const MAX_VIEWPORT = 4096;
|
|
12691
|
+
const SPLIT_MIN = 0.1;
|
|
12692
|
+
const SPLIT_MAX = 0.9;
|
|
12691
12693
|
const DEFAULT_SETTINGS = {
|
|
12692
12694
|
hostDiagonalInches: 27,
|
|
12693
12695
|
hostNits: 500,
|
|
12694
12696
|
agentControl: false,
|
|
12695
12697
|
updateCheck: true,
|
|
12696
|
-
lastUpdateCheck: 0
|
|
12698
|
+
lastUpdateCheck: 0,
|
|
12699
|
+
recordHistory: true,
|
|
12700
|
+
split: 0.5
|
|
12697
12701
|
};
|
|
12698
12702
|
const SCREEN_PRESETS = [
|
|
12699
12703
|
// Laptops — ordered largest to smallest panel, then the denser 1080p outlier.
|
|
@@ -12780,6 +12784,7 @@ const useStore = create()((set) => ({
|
|
|
12780
12784
|
error: null,
|
|
12781
12785
|
toast: null,
|
|
12782
12786
|
update: null,
|
|
12787
|
+
history: [],
|
|
12783
12788
|
image: null,
|
|
12784
12789
|
surround: "graphite",
|
|
12785
12790
|
// Fit, not 1:1: fit never enlarges past 1:1, so a render that already fits
|
|
@@ -12812,6 +12817,7 @@ const useStore = create()((set) => ({
|
|
|
12812
12817
|
// duplicate must not replace the object and re-render everything twice.
|
|
12813
12818
|
setError: (error) => set((s) => sameError(s.error, error) ? {} : { error }),
|
|
12814
12819
|
setUpdate: (update) => set({ update }),
|
|
12820
|
+
setHistory: (history) => set({ history }),
|
|
12815
12821
|
setToast: (toast) => set({ toast }),
|
|
12816
12822
|
setImage: (image) => set({ image }),
|
|
12817
12823
|
setSurround: (surround) => set({ surround }),
|
|
@@ -13115,6 +13121,98 @@ function NativeSlot() {
|
|
|
13115
13121
|
/* @__PURE__ */ jsxRuntimeExports.jsx(NativeFooter, { width: size.width, height: size.height })
|
|
13116
13122
|
] });
|
|
13117
13123
|
}
|
|
13124
|
+
const MIN_PANE_PX = 240;
|
|
13125
|
+
const KEY_STEP = 0.02;
|
|
13126
|
+
const KEY_STEP_COARSE = 0.1;
|
|
13127
|
+
function PaneDivider() {
|
|
13128
|
+
const split = useStore((s) => s.settings.split);
|
|
13129
|
+
const ref = reactExports.useRef(null);
|
|
13130
|
+
const drag = reactExports.useRef(null);
|
|
13131
|
+
const geometry = () => {
|
|
13132
|
+
const el = ref.current;
|
|
13133
|
+
const row = el?.parentElement;
|
|
13134
|
+
if (!el || !row) return { left: 0, usable: 1, min: 0.5, max: 0.5 };
|
|
13135
|
+
const r = row.getBoundingClientRect();
|
|
13136
|
+
const usable2 = Math.max(1, r.width - el.getBoundingClientRect().width);
|
|
13137
|
+
const floor = MIN_PANE_PX / usable2;
|
|
13138
|
+
return {
|
|
13139
|
+
left: r.left,
|
|
13140
|
+
usable: usable2,
|
|
13141
|
+
min: Math.min(Math.max(SPLIT_MIN, floor), 0.5),
|
|
13142
|
+
max: Math.max(Math.min(SPLIT_MAX, 1 - floor), 0.5)
|
|
13143
|
+
};
|
|
13144
|
+
};
|
|
13145
|
+
const clamp = (ratio) => {
|
|
13146
|
+
const g = geometry();
|
|
13147
|
+
return Math.min(Math.max(ratio, g.min), g.max);
|
|
13148
|
+
};
|
|
13149
|
+
const preview = (ratio) => {
|
|
13150
|
+
const s = useStore.getState();
|
|
13151
|
+
const next = clamp(ratio);
|
|
13152
|
+
if (next === s.settings.split) return;
|
|
13153
|
+
s.setSettings({ ...s.settings, split: next });
|
|
13154
|
+
};
|
|
13155
|
+
const persist = (before) => {
|
|
13156
|
+
const now = useStore.getState().settings;
|
|
13157
|
+
if (now.split === before.split) return;
|
|
13158
|
+
window.obsrv.setSettings(now).catch(() => useStore.getState().setSettings(before));
|
|
13159
|
+
};
|
|
13160
|
+
const commit = (ratio) => {
|
|
13161
|
+
const before = useStore.getState().settings;
|
|
13162
|
+
preview(ratio);
|
|
13163
|
+
persist(before);
|
|
13164
|
+
};
|
|
13165
|
+
const onPointerDown = (e) => {
|
|
13166
|
+
if (e.button !== 0) return;
|
|
13167
|
+
e.preventDefault();
|
|
13168
|
+
const el = e.currentTarget;
|
|
13169
|
+
el.setPointerCapture(e.pointerId);
|
|
13170
|
+
drag.current = {
|
|
13171
|
+
before: useStore.getState().settings,
|
|
13172
|
+
grab: e.clientX - el.getBoundingClientRect().left
|
|
13173
|
+
};
|
|
13174
|
+
};
|
|
13175
|
+
const onPointerMove = (e) => {
|
|
13176
|
+
const d = drag.current;
|
|
13177
|
+
if (!d) return;
|
|
13178
|
+
const g = geometry();
|
|
13179
|
+
preview((e.clientX - d.grab - g.left) / g.usable);
|
|
13180
|
+
};
|
|
13181
|
+
const onPointerUp = (e) => {
|
|
13182
|
+
const d = drag.current;
|
|
13183
|
+
if (!d) return;
|
|
13184
|
+
drag.current = null;
|
|
13185
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
13186
|
+
persist(d.before);
|
|
13187
|
+
};
|
|
13188
|
+
const onKeyDown = (e) => {
|
|
13189
|
+
const step = e.shiftKey ? KEY_STEP_COARSE : KEY_STEP;
|
|
13190
|
+
const delta = e.key === "ArrowLeft" ? -step : e.key === "ArrowRight" ? step : 0;
|
|
13191
|
+
if (!delta) return;
|
|
13192
|
+
e.preventDefault();
|
|
13193
|
+
commit(useStore.getState().settings.split + delta);
|
|
13194
|
+
};
|
|
13195
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
13196
|
+
"div",
|
|
13197
|
+
{
|
|
13198
|
+
ref,
|
|
13199
|
+
className: "pane-divider",
|
|
13200
|
+
role: "separator",
|
|
13201
|
+
"aria-orientation": "vertical",
|
|
13202
|
+
"aria-label": "Pane split",
|
|
13203
|
+
tabIndex: 0,
|
|
13204
|
+
"aria-valuenow": Math.round(split * 100),
|
|
13205
|
+
"aria-valuemin": Math.round(SPLIT_MIN * 100),
|
|
13206
|
+
"aria-valuemax": Math.round(SPLIT_MAX * 100),
|
|
13207
|
+
onPointerDown,
|
|
13208
|
+
onPointerMove,
|
|
13209
|
+
onPointerUp,
|
|
13210
|
+
onPointerCancel: onPointerUp,
|
|
13211
|
+
onDoubleClick: () => commit(0.5),
|
|
13212
|
+
onKeyDown
|
|
13213
|
+
}
|
|
13214
|
+
);
|
|
13215
|
+
}
|
|
13118
13216
|
const CONTRAST_MAX = 3e3;
|
|
13119
13217
|
const CUSTOM_PROFILE_ID = "custom";
|
|
13120
13218
|
function profileToControls(p, hostNits) {
|
|
@@ -13308,6 +13406,7 @@ function SettingsPanel() {
|
|
|
13308
13406
|
const host = useStore(useShallow((s) => s.host));
|
|
13309
13407
|
const settings = useStore(useShallow((s) => s.settings));
|
|
13310
13408
|
const update = useStore((s) => s.update);
|
|
13409
|
+
const history = useStore((s) => s.history);
|
|
13311
13410
|
const custom = useStore(useShallow((s) => s.custom));
|
|
13312
13411
|
const viewport = useStore(useShallow(selectViewport));
|
|
13313
13412
|
const scale = useStore(selectScale);
|
|
@@ -13464,7 +13563,34 @@ function SettingsPanel() {
|
|
|
13464
13563
|
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Check for updates automatically" })
|
|
13465
13564
|
] }),
|
|
13466
13565
|
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "check-now", onClick: () => void window.obsrv.checkUpdate(), children: "Check now" }),
|
|
13467
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "muted", children: "One unauthenticated request to GitHub, at most once a day. No identifiers are sent." })
|
|
13566
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "muted", children: "One unauthenticated request to GitHub, at most once a day. No identifiers are sent." }),
|
|
13567
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "History" }),
|
|
13568
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "control inline record-history-toggle", children: [
|
|
13569
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
13570
|
+
"input",
|
|
13571
|
+
{
|
|
13572
|
+
type: "checkbox",
|
|
13573
|
+
checked: settings.recordHistory,
|
|
13574
|
+
onChange: (e) => commit({ ...settings, recordHistory: e.target.checked })
|
|
13575
|
+
}
|
|
13576
|
+
),
|
|
13577
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Remember visited addresses" })
|
|
13578
|
+
] }),
|
|
13579
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
13580
|
+
"button",
|
|
13581
|
+
{
|
|
13582
|
+
type: "button",
|
|
13583
|
+
className: "clear-history",
|
|
13584
|
+
disabled: history.length === 0,
|
|
13585
|
+
onClick: () => void window.obsrv.clearHistory(),
|
|
13586
|
+
children: "Clear history"
|
|
13587
|
+
}
|
|
13588
|
+
),
|
|
13589
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "readout", children: [
|
|
13590
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "history-count num", children: history.length }),
|
|
13591
|
+
history.length === 1 ? " address remembered" : " addresses remembered"
|
|
13592
|
+
] }),
|
|
13593
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "muted", children: "Typed into the URL bar as suggestions and nowhere else. Turning this off stops recording and keeps what is stored; Clear erases it." })
|
|
13468
13594
|
] });
|
|
13469
13595
|
}
|
|
13470
13596
|
const VERT_SRC = `#version 300 es
|
|
@@ -14279,6 +14405,16 @@ function Toast() {
|
|
|
14279
14405
|
if (!toast) return null;
|
|
14280
14406
|
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "toast", role: "status", children: toast });
|
|
14281
14407
|
}
|
|
14408
|
+
const HISTORY_SUGGESTIONS = 6;
|
|
14409
|
+
function byRank(a, b) {
|
|
14410
|
+
if (a.lastVisit !== b.lastVisit) return b.lastVisit - a.lastVisit;
|
|
14411
|
+
if (a.visits !== b.visits) return b.visits - a.visits;
|
|
14412
|
+
return a.url < b.url ? -1 : a.url > b.url ? 1 : 0;
|
|
14413
|
+
}
|
|
14414
|
+
function matchHistory(entries, query, limit = HISTORY_SUGGESTIONS) {
|
|
14415
|
+
const needle = query.trim().toLowerCase();
|
|
14416
|
+
return entries.filter((e) => e.url.toLowerCase().includes(needle)).sort(byRank).slice(0, Math.max(0, limit));
|
|
14417
|
+
}
|
|
14282
14418
|
/**
|
|
14283
14419
|
* @license lucide-react v1.34.0 - ISC
|
|
14284
14420
|
*
|
|
@@ -14631,8 +14767,18 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
14631
14767
|
const setPanes = useStore((s) => s.setPanes);
|
|
14632
14768
|
const agentControl = useStore((s) => s.settings.agentControl);
|
|
14633
14769
|
const setSettings = useStore((s) => s.setSettings);
|
|
14770
|
+
const history = useStore((s) => s.history);
|
|
14634
14771
|
const inputRef = reactExports.useRef(null);
|
|
14635
14772
|
const [draft, setDraft] = reactExports.useState(barText);
|
|
14773
|
+
const [open, setOpen] = reactExports.useState(false);
|
|
14774
|
+
const [highlight, setHighlight] = reactExports.useState(-1);
|
|
14775
|
+
const matches = reactExports.useMemo(() => open ? matchHistory(history, draft) : [], [open, history, draft]);
|
|
14776
|
+
const listOpen = open && matches.length > 0;
|
|
14777
|
+
const closeList = () => {
|
|
14778
|
+
setOpen(false);
|
|
14779
|
+
setHighlight(-1);
|
|
14780
|
+
};
|
|
14781
|
+
const picked = highlight >= 0 && highlight < matches.length ? matches[highlight] : null;
|
|
14636
14782
|
const [agentActive, setAgentActive] = reactExports.useState(false);
|
|
14637
14783
|
reactExports.useEffect(() => {
|
|
14638
14784
|
let timer;
|
|
@@ -14665,14 +14811,50 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
14665
14811
|
});
|
|
14666
14812
|
}, []);
|
|
14667
14813
|
const readOnly = mode === "image";
|
|
14668
|
-
|
|
14669
|
-
|
|
14670
|
-
|
|
14814
|
+
reactExports.useEffect(() => {
|
|
14815
|
+
if (readOnly) closeList();
|
|
14816
|
+
}, [readOnly]);
|
|
14817
|
+
const go = async (url) => {
|
|
14818
|
+
closeList();
|
|
14671
14819
|
setError(null);
|
|
14672
|
-
const applied = await window.obsrv.navigate(
|
|
14820
|
+
const applied = await window.obsrv.navigate(url);
|
|
14673
14821
|
setUrl(applied);
|
|
14674
14822
|
setDraft(applied);
|
|
14675
14823
|
};
|
|
14824
|
+
const submit = (e) => {
|
|
14825
|
+
e.preventDefault();
|
|
14826
|
+
if (readOnly || draft.trim() === "") return;
|
|
14827
|
+
void go(draft);
|
|
14828
|
+
};
|
|
14829
|
+
const onKeyDown = (e) => {
|
|
14830
|
+
if (readOnly) return;
|
|
14831
|
+
if (e.key === "ArrowDown") {
|
|
14832
|
+
e.preventDefault();
|
|
14833
|
+
if (!open) {
|
|
14834
|
+
setOpen(true);
|
|
14835
|
+
setHighlight(0);
|
|
14836
|
+
return;
|
|
14837
|
+
}
|
|
14838
|
+
setHighlight((h) => h + 1 >= matches.length ? -1 : h + 1);
|
|
14839
|
+
return;
|
|
14840
|
+
}
|
|
14841
|
+
if (e.key === "ArrowUp" && listOpen) {
|
|
14842
|
+
e.preventDefault();
|
|
14843
|
+
setHighlight((h) => h <= -1 ? matches.length - 1 : h - 1);
|
|
14844
|
+
return;
|
|
14845
|
+
}
|
|
14846
|
+
if (e.key === "Enter" && picked) {
|
|
14847
|
+
e.preventDefault();
|
|
14848
|
+
void go(picked.url);
|
|
14849
|
+
return;
|
|
14850
|
+
}
|
|
14851
|
+
if (e.key === "Escape") {
|
|
14852
|
+
const dismissed = listOpen;
|
|
14853
|
+
closeList();
|
|
14854
|
+
if (dismissed) return;
|
|
14855
|
+
setDraft(barText);
|
|
14856
|
+
}
|
|
14857
|
+
};
|
|
14676
14858
|
const presetLabel = SCREEN_PRESETS.find((p) => p.id === presetId)?.label ?? "Custom";
|
|
14677
14859
|
const profileLabel = PANEL_PROFILES.find((p) => p.id === profileId)?.label ?? profileId;
|
|
14678
14860
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "chrome", children: [
|
|
@@ -14710,20 +14892,49 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
14710
14892
|
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { name: "reload" })
|
|
14711
14893
|
}
|
|
14712
14894
|
),
|
|
14713
|
-
/* @__PURE__ */ jsxRuntimeExports.
|
|
14714
|
-
|
|
14715
|
-
|
|
14716
|
-
|
|
14717
|
-
|
|
14718
|
-
|
|
14719
|
-
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
14895
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("form", { className: "url-form", onSubmit: submit, children: [
|
|
14896
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14897
|
+
"input",
|
|
14898
|
+
{
|
|
14899
|
+
ref: inputRef,
|
|
14900
|
+
value: draft,
|
|
14901
|
+
readOnly,
|
|
14902
|
+
spellCheck: false,
|
|
14903
|
+
placeholder: "Enter a URL, or drop a PNG",
|
|
14904
|
+
role: "combobox",
|
|
14905
|
+
"aria-expanded": listOpen,
|
|
14906
|
+
"aria-controls": "url-history",
|
|
14907
|
+
"aria-autocomplete": "list",
|
|
14908
|
+
"aria-activedescendant": picked ? `url-history-${highlight}` : void 0,
|
|
14909
|
+
onChange: (e) => {
|
|
14910
|
+
setDraft(e.target.value);
|
|
14911
|
+
setOpen(true);
|
|
14912
|
+
setHighlight(-1);
|
|
14913
|
+
},
|
|
14914
|
+
onKeyDown,
|
|
14915
|
+
onBlur: closeList
|
|
14724
14916
|
}
|
|
14725
|
-
|
|
14726
|
-
|
|
14917
|
+
),
|
|
14918
|
+
listOpen && /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { className: "url-history", id: "url-history", role: "listbox", "aria-label": "Visited addresses", children: matches.map((m, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
14919
|
+
"li",
|
|
14920
|
+
{
|
|
14921
|
+
id: `url-history-${i}`,
|
|
14922
|
+
className: `url-history-row${i === highlight ? " active" : ""}`,
|
|
14923
|
+
role: "option",
|
|
14924
|
+
"aria-selected": i === highlight,
|
|
14925
|
+
onMouseDown: (e) => {
|
|
14926
|
+
e.preventDefault();
|
|
14927
|
+
void go(m.url);
|
|
14928
|
+
},
|
|
14929
|
+
onMouseEnter: () => setHighlight(i),
|
|
14930
|
+
children: [
|
|
14931
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "url-history-url", children: /* @__PURE__ */ jsxRuntimeExports.jsx("bdi", { children: m.url }) }),
|
|
14932
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "url-history-age", children: formatAge(m.lastVisit, Date.now()) })
|
|
14933
|
+
]
|
|
14934
|
+
},
|
|
14935
|
+
m.url
|
|
14936
|
+
)) })
|
|
14937
|
+
] }),
|
|
14727
14938
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "status-cluster", children: [
|
|
14728
14939
|
mode === "image" && /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14729
14940
|
"button",
|
|
@@ -14890,6 +15101,7 @@ function App() {
|
|
|
14890
15101
|
const setError = useStore((s) => s.setError);
|
|
14891
15102
|
const setTargetLoading = useStore((s) => s.setTargetLoading);
|
|
14892
15103
|
const setUpdate = useStore((s) => s.setUpdate);
|
|
15104
|
+
const setHistory = useStore((s) => s.setHistory);
|
|
14893
15105
|
const setImageMeta = useStore((s) => s.setImage);
|
|
14894
15106
|
const setMode = useStore((s) => s.setMode);
|
|
14895
15107
|
const setToast = useStore((s) => s.setToast);
|
|
@@ -14901,27 +15113,34 @@ function App() {
|
|
|
14901
15113
|
const profileId = useStore((s) => s.profileId);
|
|
14902
15114
|
const viewMode = useStore((s) => s.viewMode);
|
|
14903
15115
|
const panes = useStore((s) => s.panes);
|
|
15116
|
+
const split = useStore((s) => s.settings.split);
|
|
14904
15117
|
reactExports.useEffect(() => {
|
|
14905
15118
|
window.obsrv.getHostInfo().then(setHost, (e) => console.warn("obsrv: getHostInfo failed", e));
|
|
14906
15119
|
window.obsrv.getSettings().then(setSettings, (e) => console.warn("obsrv: getSettings failed", e));
|
|
14907
15120
|
window.obsrv.getUpdate().then(setUpdate, (e) => console.warn("obsrv: getUpdate failed", e));
|
|
15121
|
+
window.obsrv.getHistory().then(setHistory, (e) => console.warn("obsrv: getHistory failed", e));
|
|
14908
15122
|
const offs = [
|
|
14909
15123
|
window.obsrv.onHostChanged(setHost),
|
|
14910
15124
|
// A committed navigation — back, forward, reload, a link — supersedes
|
|
14911
|
-
// the last load failure
|
|
14912
|
-
//
|
|
15125
|
+
// the last load failure, and only a committed one: Chromium commits its
|
|
15126
|
+
// error page without emitting `did-navigate`, so a failed load reports
|
|
15127
|
+
// no URL at all (measured on Electron 43; a bad host, a refused
|
|
15128
|
+
// connection, a missing file and a Back onto an error page all fire
|
|
15129
|
+
// `did-fail-load` alone). The badge therefore lands last by having
|
|
15130
|
+
// nothing race it.
|
|
14913
15131
|
window.obsrv.onUrlChanged((url) => {
|
|
14914
15132
|
setError(null);
|
|
14915
15133
|
setUrl(url);
|
|
14916
15134
|
}),
|
|
14917
15135
|
window.obsrv.onLoadError(setError),
|
|
14918
15136
|
window.obsrv.onTargetLoading(setTargetLoading),
|
|
14919
|
-
window.obsrv.onUpdateStatus(setUpdate)
|
|
15137
|
+
window.obsrv.onUpdateStatus(setUpdate),
|
|
15138
|
+
window.obsrv.onHistoryChanged(setHistory)
|
|
14920
15139
|
];
|
|
14921
15140
|
return () => {
|
|
14922
15141
|
for (const off of offs) off();
|
|
14923
15142
|
};
|
|
14924
|
-
}, [setHost, setSettings, setUrl, setError, setTargetLoading, setUpdate]);
|
|
15143
|
+
}, [setHost, setSettings, setUrl, setError, setTargetLoading, setUpdate, setHistory]);
|
|
14925
15144
|
reactExports.useEffect(() => {
|
|
14926
15145
|
void window.obsrv.setViewport(viewport.width, viewport.height, deviceScaleFactor);
|
|
14927
15146
|
}, [viewport.width, viewport.height, deviceScaleFactor]);
|
|
@@ -15031,29 +15250,46 @@ function App() {
|
|
|
15031
15250
|
};
|
|
15032
15251
|
}, [mode, image]);
|
|
15033
15252
|
if (fatal) return /* @__PURE__ */ jsxRuntimeExports.jsx(Fatal, { message: fatal });
|
|
15034
|
-
return
|
|
15035
|
-
|
|
15036
|
-
|
|
15037
|
-
|
|
15038
|
-
|
|
15039
|
-
|
|
15040
|
-
|
|
15041
|
-
|
|
15042
|
-
|
|
15043
|
-
|
|
15044
|
-
|
|
15045
|
-
|
|
15046
|
-
|
|
15047
|
-
|
|
15048
|
-
/* @__PURE__ */ jsxRuntimeExports.
|
|
15049
|
-
|
|
15050
|
-
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
|
|
15253
|
+
return (
|
|
15254
|
+
// The split and the panes mode are published here rather than on `.panes`
|
|
15255
|
+
// because the chrome needs them too: the URL bar's history dropdown is
|
|
15256
|
+
// clamped to the native pane's right edge, and a custom property set on
|
|
15257
|
+
// `.panes` reaches nothing above it. Everything below still inherits them.
|
|
15258
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
15259
|
+
"div",
|
|
15260
|
+
{
|
|
15261
|
+
className: "app",
|
|
15262
|
+
"data-panes": panes,
|
|
15263
|
+
style: { "--split": split, "--pane-min": `${MIN_PANE_PX}px` },
|
|
15264
|
+
children: [
|
|
15265
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Toolbar, { drawer, onTogglePanel: toggle("panel"), onToggleSettings: toggle("settings") }),
|
|
15266
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(DropZone, { onImage }),
|
|
15267
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "body", children: [
|
|
15268
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "panes", children: [
|
|
15269
|
+
panes === "both" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
15270
|
+
mode === "image" && image ? /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
15271
|
+
ImagePane,
|
|
15272
|
+
{
|
|
15273
|
+
src: image.objectUrl,
|
|
15274
|
+
width: image.natural.width,
|
|
15275
|
+
height: image.natural.height
|
|
15276
|
+
}
|
|
15277
|
+
) : /* @__PURE__ */ jsxRuntimeExports.jsx(NativeSlot, {}),
|
|
15278
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(PaneDivider, {})
|
|
15279
|
+
] }),
|
|
15280
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pane target-pane", ref: targetPaneRef, children: [
|
|
15281
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pane-body", children: /* @__PURE__ */ jsxRuntimeExports.jsx(TargetCanvas, { onFatal: setFatal, imageFrame }) }),
|
|
15282
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(TargetFooter, {})
|
|
15283
|
+
] })
|
|
15284
|
+
] }),
|
|
15285
|
+
drawer === "panel" && /* @__PURE__ */ jsxRuntimeExports.jsx("aside", { className: "drawer", children: /* @__PURE__ */ jsxRuntimeExports.jsx(PanelControls, {}) }),
|
|
15286
|
+
drawer === "settings" && /* @__PURE__ */ jsxRuntimeExports.jsx("aside", { className: "drawer", children: /* @__PURE__ */ jsxRuntimeExports.jsx(SettingsPanel, {}) })
|
|
15287
|
+
] }),
|
|
15288
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Toast, {})
|
|
15289
|
+
]
|
|
15290
|
+
}
|
|
15291
|
+
)
|
|
15292
|
+
);
|
|
15057
15293
|
}
|
|
15058
15294
|
function BrowserNotice() {
|
|
15059
15295
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "browser-notice", role: "note", children: [
|
package/out/renderer/index.html
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' blob: data:" />
|
|
6
6
|
<title>Obsrv</title>
|
|
7
|
-
<script type="module" crossorigin src="./assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
7
|
+
<script type="module" crossorigin src="./assets/index-BlbBg6lm.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="./assets/index-B61fbgqt.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<div id="root"></div>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isStamp = exports.isCount = exports.isRecord = exports.MAX_URL_LENGTH = exports.HISTORY_SUGGESTIONS = exports.HISTORY_MAX = void 0;
|
|
4
|
+
exports.isStorableUrl = isStorableUrl;
|
|
5
|
+
exports.byRank = byRank;
|
|
6
|
+
exports.recordVisit = recordVisit;
|
|
7
|
+
exports.matchHistory = matchHistory;
|
|
8
|
+
const url_1 = require("./url");
|
|
9
|
+
/**
|
|
10
|
+
* The file is read and written whole, so it needs a bound. 500 is far beyond
|
|
11
|
+
* what this app's usage produces — a handful of dev and staging addresses
|
|
12
|
+
* returned to over and over — and small enough to parse instantly.
|
|
13
|
+
*/
|
|
14
|
+
exports.HISTORY_MAX = 500;
|
|
15
|
+
/** Rows the URL bar offers. Past six, a longer list is a worse tool than a better query. */
|
|
16
|
+
exports.HISTORY_SUGGESTIONS = 6;
|
|
17
|
+
/**
|
|
18
|
+
* The other half of the bound: 500 entries of unbounded length is not a
|
|
19
|
+
* bounded file. Far longer than any address this tool is pointed at, and
|
|
20
|
+
* short enough that the whole file stays trivial to parse.
|
|
21
|
+
*/
|
|
22
|
+
exports.MAX_URL_LENGTH = 2048;
|
|
23
|
+
const isRecord = (v) => typeof v === 'object' && v !== null;
|
|
24
|
+
exports.isRecord = isRecord;
|
|
25
|
+
const isCount = (v) => typeof v === 'number' && Number.isInteger(v) && v >= 1;
|
|
26
|
+
exports.isCount = isCount;
|
|
27
|
+
const isStamp = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
28
|
+
exports.isStamp = isStamp;
|
|
29
|
+
/**
|
|
30
|
+
* Is this an address worth remembering? Only the schemes the app can load
|
|
31
|
+
* back (`shared/url.ts`), so `about:blank` — which the native pane commits on
|
|
32
|
+
* every launch — never becomes a suggestion, and a scheme the URL bar would
|
|
33
|
+
* refuse can never be offered by it.
|
|
34
|
+
*/
|
|
35
|
+
function isStorableUrl(v) {
|
|
36
|
+
if (typeof v !== 'string' || v === '' || v.length > exports.MAX_URL_LENGTH)
|
|
37
|
+
return false;
|
|
38
|
+
try {
|
|
39
|
+
return url_1.ALLOWED_URL_SCHEMES.includes(new URL(v).protocol);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Most recent first, visit count breaking ties — so the address used
|
|
47
|
+
* constantly stays at the top without a stale favourite outranking what was
|
|
48
|
+
* on screen ten minutes ago. URL breaks the remaining tie only to keep the
|
|
49
|
+
* order stable across reloads of the same file.
|
|
50
|
+
*/
|
|
51
|
+
function byRank(a, b) {
|
|
52
|
+
if (a.lastVisit !== b.lastVisit)
|
|
53
|
+
return b.lastVisit - a.lastVisit;
|
|
54
|
+
if (a.visits !== b.visits)
|
|
55
|
+
return b.visits - a.visits;
|
|
56
|
+
return a.url < b.url ? -1 : a.url > b.url ? 1 : 0;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The list after a committed navigation to `url`. Returns `entries`
|
|
60
|
+
* *unchanged* — the same reference — when the URL is not one we store, which
|
|
61
|
+
* is what lets the caller write the file only when something actually
|
|
62
|
+
* changed.
|
|
63
|
+
*
|
|
64
|
+
* Eviction is by rank, so the least recently visited entry is the one that
|
|
65
|
+
* goes; a busy day cannot push out the address visited an hour ago in favour
|
|
66
|
+
* of one visited last year.
|
|
67
|
+
*/
|
|
68
|
+
function recordVisit(entries, url, now) {
|
|
69
|
+
if (!isStorableUrl(url) || !(0, exports.isStamp)(now))
|
|
70
|
+
return entries;
|
|
71
|
+
const previous = entries.find(e => e.url === url);
|
|
72
|
+
const next = {
|
|
73
|
+
url,
|
|
74
|
+
visits: previous ? previous.visits + 1 : 1,
|
|
75
|
+
lastVisit: now,
|
|
76
|
+
};
|
|
77
|
+
return [next, ...entries.filter(e => e.url !== url)].sort(byRank).slice(0, exports.HISTORY_MAX);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The rows to offer for what has been typed: case-insensitive substring
|
|
81
|
+
* against the whole URL, in rank order. An empty query matches everything,
|
|
82
|
+
* so pressing Down in an empty field offers the most recent addresses.
|
|
83
|
+
*/
|
|
84
|
+
function matchHistory(entries, query, limit = exports.HISTORY_SUGGESTIONS) {
|
|
85
|
+
const needle = query.trim().toLowerCase();
|
|
86
|
+
return entries
|
|
87
|
+
.filter(e => e.url.toLowerCase().includes(needle))
|
|
88
|
+
.sort(byRank)
|
|
89
|
+
.slice(0, Math.max(0, limit));
|
|
90
|
+
}
|
|
@@ -10,6 +10,7 @@ exports.parseUiState = parseUiState;
|
|
|
10
10
|
exports.parseScrollPos = parseScrollPos;
|
|
11
11
|
exports.parseScrollRequest = parseScrollRequest;
|
|
12
12
|
exports.parseScrollReport = parseScrollReport;
|
|
13
|
+
const presets_1 = require("./presets");
|
|
13
14
|
const types_1 = require("./types");
|
|
14
15
|
/**
|
|
15
16
|
* Parsers for everything the renderer sends main over IPC. Each returns a
|
|
@@ -99,7 +100,7 @@ function parseDeviceScaleFactor(raw) {
|
|
|
99
100
|
return raw;
|
|
100
101
|
}
|
|
101
102
|
/**
|
|
102
|
-
* Copies exactly the
|
|
103
|
+
* Copies exactly the known keys; the numbers must be finite and
|
|
103
104
|
* positive. A missing `agentControl` means false and a missing `updateCheck`
|
|
104
105
|
* means true (the pre-feature wire shapes); any non-boolean value is refused,
|
|
105
106
|
* never coerced.
|
|
@@ -121,7 +122,17 @@ function parseSettings(raw) {
|
|
|
121
122
|
const lastUpdateCheck = raw.lastUpdateCheck ?? 0;
|
|
122
123
|
if (!isFiniteNumber(lastUpdateCheck) || lastUpdateCheck < 0)
|
|
123
124
|
return null;
|
|
124
|
-
|
|
125
|
+
const recordHistory = raw.recordHistory ?? true;
|
|
126
|
+
if (typeof recordHistory !== 'boolean')
|
|
127
|
+
return null;
|
|
128
|
+
// A missing `split` is the pre-feature wire shape and means the default.
|
|
129
|
+
// Out of band it is refused rather than clamped: `loadSettings` forgives a
|
|
130
|
+
// hand-edited file because it must, but the renderer clamps before it
|
|
131
|
+
// sends, so a bad ratio arriving here is a bug worth surfacing.
|
|
132
|
+
const split = raw.split ?? presets_1.DEFAULT_SETTINGS.split;
|
|
133
|
+
if (!isFiniteNumber(split) || split < presets_1.SPLIT_MIN || split > presets_1.SPLIT_MAX)
|
|
134
|
+
return null;
|
|
135
|
+
return { hostDiagonalInches, hostNits, agentControl, updateCheck, lastUpdateCheck, recordHistory, split };
|
|
125
136
|
}
|
|
126
137
|
function parseMode(raw) {
|
|
127
138
|
return raw === 'url' || raw === 'image' ? raw : null;
|
package/out/shared/presets.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PANEL_PROFILES = exports.SCREEN_PRESETS = exports.DEFAULT_SETTINGS = exports.MAX_VIEWPORT = void 0;
|
|
3
|
+
exports.PANEL_PROFILES = exports.SCREEN_PRESETS = exports.DEFAULT_SETTINGS = exports.SPLIT_MAX = exports.SPLIT_MIN = exports.MAX_VIEWPORT = void 0;
|
|
4
4
|
exports.findPreset = findPreset;
|
|
5
5
|
exports.findProfile = findProfile;
|
|
6
6
|
exports.MAX_VIEWPORT = 4096;
|
|
7
|
+
/** The loose sanity band for `Settings.split`; the clamp that matters is the
|
|
8
|
+
* drag's 240px-a-side floor, which only the renderer knows the pixels for. */
|
|
9
|
+
exports.SPLIT_MIN = 0.1;
|
|
10
|
+
exports.SPLIT_MAX = 0.9;
|
|
7
11
|
exports.DEFAULT_SETTINGS = {
|
|
8
12
|
hostDiagonalInches: 27,
|
|
9
13
|
hostNits: 500,
|
|
10
14
|
agentControl: false,
|
|
11
15
|
updateCheck: true,
|
|
12
16
|
lastUpdateCheck: 0,
|
|
17
|
+
recordHistory: true,
|
|
18
|
+
split: 0.5,
|
|
13
19
|
};
|
|
14
20
|
exports.SCREEN_PRESETS = [
|
|
15
21
|
// Laptops — ordered largest to smallest panel, then the denser 1080p outlier.
|