getobsrv 0.11.0 → 0.13.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/README.md +34 -0
- package/out/cli/args.js +52 -5
- package/out/main/cli.js +43 -9
- package/out/main/index.js +792 -202
- package/out/main/{targetSource-nbfc-Fe6.js → targetSource-DuRYHUo8.js} +53 -1
- package/out/mcp/lib.js +14 -1
- package/out/mcp/server.js +88 -9
- package/out/preload/app.js +19 -16
- package/out/renderer/assets/{index-BlbBg6lm.js → index-BHt9Vm0J.js} +470 -120
- package/out/renderer/assets/{index-B61fbgqt.css → index-BRkFfzcI.css} +142 -4
- package/out/renderer/index.html +2 -2
- package/out/shared/calibration.js +35 -0
- package/out/shared/control.js +71 -1
- package/out/shared/ipcPayloads.js +44 -1
- package/out/shared/presets.js +16 -1
- package/out/shared/tabList.js +46 -0
- package/package.json +1 -1
- package/skills/obsrv-screens/SKILL.md +7 -0
|
@@ -12690,6 +12690,8 @@ const create = ((createState) => createImpl);
|
|
|
12690
12690
|
const MAX_VIEWPORT = 4096;
|
|
12691
12691
|
const SPLIT_MIN = 0.1;
|
|
12692
12692
|
const SPLIT_MAX = 0.9;
|
|
12693
|
+
const MAX_TABS_MIN = 2;
|
|
12694
|
+
const MAX_TABS_MAX = 32;
|
|
12693
12695
|
const DEFAULT_SETTINGS = {
|
|
12694
12696
|
hostDiagonalInches: 27,
|
|
12695
12697
|
hostNits: 500,
|
|
@@ -12697,8 +12699,10 @@ const DEFAULT_SETTINGS = {
|
|
|
12697
12699
|
updateCheck: true,
|
|
12698
12700
|
lastUpdateCheck: 0,
|
|
12699
12701
|
recordHistory: true,
|
|
12700
|
-
split: 0.5
|
|
12702
|
+
split: 0.5,
|
|
12703
|
+
maxTabs: 12
|
|
12701
12704
|
};
|
|
12705
|
+
const DEFAULT_ORIENTATION = "portrait";
|
|
12702
12706
|
const SCREEN_PRESETS = [
|
|
12703
12707
|
// Laptops — ordered largest to smallest panel, then the denser 1080p outlier.
|
|
12704
12708
|
{ id: "laptop-768", label: '1366×768 15.6"', width: 1366, height: 768, diagonalInches: 15.6, deviceScaleFactor: 1, group: "laptop" },
|
|
@@ -12732,6 +12736,13 @@ function findProfile(id) {
|
|
|
12732
12736
|
if (!p) throw new Error(`unknown profile: ${id}`);
|
|
12733
12737
|
return p;
|
|
12734
12738
|
}
|
|
12739
|
+
function applyOrientation(screen, orientation) {
|
|
12740
|
+
if (orientation !== "landscape") return screen;
|
|
12741
|
+
return { ...screen, width: screen.height, height: screen.width };
|
|
12742
|
+
}
|
|
12743
|
+
function screenShape(width, height) {
|
|
12744
|
+
return width > height ? "landscape" : "portrait";
|
|
12745
|
+
}
|
|
12735
12746
|
function ppi(width, height, diagonalInches) {
|
|
12736
12747
|
if (!(diagonalInches > 0)) throw new RangeError("diagonalInches must be > 0");
|
|
12737
12748
|
return Math.hypot(width, height) / diagonalInches;
|
|
@@ -12761,6 +12772,27 @@ function profileToParams(p, hostNits) {
|
|
|
12761
12772
|
dither: p.frc
|
|
12762
12773
|
};
|
|
12763
12774
|
}
|
|
12775
|
+
function closeTab(tabs, closeId, activeId) {
|
|
12776
|
+
const index = tabs.findIndex((t) => t.id === closeId);
|
|
12777
|
+
if (index === -1) return { tabs, activeId };
|
|
12778
|
+
const next = tabs.filter((t) => t.id !== closeId);
|
|
12779
|
+
if (next.length === 0) return { tabs: next, activeId: null };
|
|
12780
|
+
if (closeId !== activeId) return { tabs: next, activeId };
|
|
12781
|
+
const neighbour = next[Math.min(index, next.length - 1)];
|
|
12782
|
+
return { tabs: next, activeId: neighbour.id };
|
|
12783
|
+
}
|
|
12784
|
+
function canAddTab(count, max) {
|
|
12785
|
+
return count < max;
|
|
12786
|
+
}
|
|
12787
|
+
function tabTitle(url, pageTitle) {
|
|
12788
|
+
if (pageTitle.trim() !== "") return pageTitle;
|
|
12789
|
+
if (url.trim() === "" || url.trim() === "about:blank") return "New tab";
|
|
12790
|
+
try {
|
|
12791
|
+
return new URL(url).host || url;
|
|
12792
|
+
} catch {
|
|
12793
|
+
return url;
|
|
12794
|
+
}
|
|
12795
|
+
}
|
|
12764
12796
|
const CUSTOM_PRESET_ID = "custom";
|
|
12765
12797
|
const FALLBACK_SCALE = 2;
|
|
12766
12798
|
function sameError(a, b) {
|
|
@@ -12768,83 +12800,197 @@ function sameError(a, b) {
|
|
|
12768
12800
|
if (a === null || b === null) return false;
|
|
12769
12801
|
return a.code === b.code && a.url === b.url && a.description === b.description;
|
|
12770
12802
|
}
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12803
|
+
function blankTab() {
|
|
12804
|
+
return {
|
|
12805
|
+
mode: "url",
|
|
12806
|
+
url: "",
|
|
12807
|
+
title: "",
|
|
12808
|
+
lastUrl: "",
|
|
12809
|
+
presetId: "1080p-24",
|
|
12810
|
+
orientation: DEFAULT_ORIENTATION,
|
|
12811
|
+
custom: { width: 1920, height: 1080, diagonalInches: 24 },
|
|
12812
|
+
pixelExact: false,
|
|
12813
|
+
profileId: PANEL_PROFILES[0].id,
|
|
12814
|
+
profileOverride: null,
|
|
12815
|
+
targetLoading: false,
|
|
12816
|
+
error: null,
|
|
12817
|
+
image: null,
|
|
12818
|
+
// Fit, not 1:1: fit never enlarges past 1:1, so a render that already fits
|
|
12819
|
+
// its pane opens at true magnification anyway, while one that does not is
|
|
12820
|
+
// shown whole instead of as its top-left corner. Fit is interactive, so
|
|
12821
|
+
// this costs nothing — and the footer names the actual magnification
|
|
12822
|
+
// whenever fit is minifying.
|
|
12823
|
+
viewMode: "fit",
|
|
12824
|
+
fitScale: null,
|
|
12825
|
+
agentPan: null,
|
|
12826
|
+
agentHighlight: null
|
|
12827
|
+
};
|
|
12828
|
+
}
|
|
12829
|
+
let nextTabId = 0;
|
|
12830
|
+
function newTabId() {
|
|
12831
|
+
nextTabId += 1;
|
|
12832
|
+
return `local-${nextTabId}`;
|
|
12833
|
+
}
|
|
12834
|
+
const patchActiveWith = (f) => (s) => {
|
|
12835
|
+
const active = s.tabs[s.activeId];
|
|
12836
|
+
const patch = f(active);
|
|
12837
|
+
return patch === null ? {} : { tabs: { ...s.tabs, [s.activeId]: { ...active, ...patch } } };
|
|
12838
|
+
};
|
|
12839
|
+
const patchActive = (patch) => patchActiveWith(() => patch);
|
|
12840
|
+
const patchTabWith = (id, f) => (s) => {
|
|
12841
|
+
const t = s.tabs[id];
|
|
12842
|
+
if (!t) return {};
|
|
12843
|
+
const patch = f(t);
|
|
12844
|
+
return patch === null ? {} : { tabs: { ...s.tabs, [id]: { ...t, ...patch } } };
|
|
12845
|
+
};
|
|
12846
|
+
const FIRST_TAB = newTabId();
|
|
12847
|
+
const useStore = create()((set, get) => ({
|
|
12848
|
+
tabs: { [FIRST_TAB]: blankTab() },
|
|
12849
|
+
tabOrder: [FIRST_TAB],
|
|
12850
|
+
activeId: FIRST_TAB,
|
|
12780
12851
|
settings: { ...DEFAULT_SETTINGS },
|
|
12781
12852
|
// Zeroes until the first `getHostInfo`; `selectScale` falls back meanwhile.
|
|
12782
12853
|
host: { physicalWidth: 0, physicalHeight: 0, scaleFactor: 0 },
|
|
12783
|
-
targetLoading: false,
|
|
12784
|
-
error: null,
|
|
12785
12854
|
toast: null,
|
|
12786
12855
|
update: null,
|
|
12787
12856
|
history: [],
|
|
12788
|
-
image: null,
|
|
12789
12857
|
surround: "graphite",
|
|
12790
|
-
// Fit, not 1:1: fit never enlarges past 1:1, so a render that already fits
|
|
12791
|
-
// its pane opens at true magnification anyway, while one that does not is
|
|
12792
|
-
// shown whole instead of as its top-left corner. Fit is interactive, so
|
|
12793
|
-
// this costs nothing — and the footer names the actual magnification
|
|
12794
|
-
// whenever fit is minifying.
|
|
12795
|
-
viewMode: "fit",
|
|
12796
12858
|
panes: "both",
|
|
12797
|
-
fitScale: null,
|
|
12798
|
-
agentPan: null,
|
|
12799
|
-
agentHighlight: null,
|
|
12800
12859
|
// Does not clear `error`: a failed load navigates to Chromium's error page,
|
|
12801
12860
|
// so clearing here would wipe the toolbar badge the moment it appeared.
|
|
12802
12861
|
// Does clear the agent highlight: it marked pixels of the page that was
|
|
12803
12862
|
// showing, and a committed navigation (a reload included) replaces them.
|
|
12804
|
-
setUrl: (url) => set({ url, agentHighlight: null }),
|
|
12863
|
+
setUrl: (url) => set((s) => patchTabWith(s.activeId, () => ({ url, agentHighlight: null }))(s)),
|
|
12864
|
+
setTabUrl: (id, url) => set(patchTabWith(id, () => ({ url, agentHighlight: null }))),
|
|
12865
|
+
setTabTitle: (id, title) => set(patchTabWith(id, (t) => t.title === title ? null : { title })),
|
|
12866
|
+
// Both panes report the same failure; see `setError`.
|
|
12867
|
+
setTabError: (id, error) => set(patchTabWith(id, (t) => sameError(t.error, error) ? null : { error })),
|
|
12868
|
+
setTabLoading: (id, targetLoading) => set(patchTabWith(id, () => ({ targetLoading }))),
|
|
12805
12869
|
// A screen change re-rasters the target, so a highlight's target-pixel rect
|
|
12806
12870
|
// no longer marks what it marked; the same for the custom fields below.
|
|
12807
|
-
setPreset: (presetId) => set({ presetId, agentHighlight: null }),
|
|
12808
|
-
|
|
12809
|
-
|
|
12871
|
+
setPreset: (presetId) => set(patchActive({ presetId, agentHighlight: null })),
|
|
12872
|
+
// A rotation re-rasters the target exactly as a preset change does, so a
|
|
12873
|
+
// highlight's target-pixel rect no longer marks what it marked. Setting the
|
|
12874
|
+
// orientation already in force writes nothing, so a re-report from an agent
|
|
12875
|
+
// (or a click on the pressed half of the control) costs no re-render.
|
|
12876
|
+
setOrientation: (orientation) => set(patchActiveWith((t) => t.orientation === orientation ? null : { orientation, agentHighlight: null })),
|
|
12877
|
+
setCustom: (c) => set(patchActiveWith((t) => ({ custom: { ...t.custom, ...c }, presetId: CUSTOM_PRESET_ID, agentHighlight: null }))),
|
|
12878
|
+
setPixelExact: (pixelExact) => set(patchActive({ pixelExact })),
|
|
12810
12879
|
// Picking a profile drops any hand-tuned slider values.
|
|
12811
|
-
setProfile: (profileId) => set({ profileId, profileOverride: null }),
|
|
12812
|
-
setProfileOverride: (profileOverride) => set({ profileOverride }),
|
|
12880
|
+
setProfile: (profileId) => set(patchActive({ profileId, profileOverride: null })),
|
|
12881
|
+
setProfileOverride: (profileOverride) => set(patchActive({ profileOverride })),
|
|
12813
12882
|
setSettings: (settings) => set({ settings }),
|
|
12814
12883
|
setHost: (host) => set({ host }),
|
|
12815
|
-
setTargetLoading: (targetLoading) => set({ targetLoading }),
|
|
12884
|
+
setTargetLoading: (targetLoading) => set(patchActive({ targetLoading })),
|
|
12816
12885
|
// Both panes report the same `loadError` for one failed navigation; the
|
|
12817
12886
|
// duplicate must not replace the object and re-render everything twice.
|
|
12818
|
-
setError: (error) => set((
|
|
12887
|
+
setError: (error) => set(patchActiveWith((t) => sameError(t.error, error) ? null : { error })),
|
|
12819
12888
|
setUpdate: (update) => set({ update }),
|
|
12820
12889
|
setHistory: (history) => set({ history }),
|
|
12821
12890
|
setToast: (toast) => set({ toast }),
|
|
12822
|
-
setImage: (image) => set({ image }),
|
|
12891
|
+
setImage: (image) => set(patchActive({ image })),
|
|
12823
12892
|
setSurround: (surround) => set({ surround }),
|
|
12824
|
-
setViewMode: (viewMode) => set({ viewMode }),
|
|
12893
|
+
setViewMode: (viewMode) => set(patchActive({ viewMode })),
|
|
12825
12894
|
// No `agentHighlight: null` here, unlike setPreset: hiding a pane does not
|
|
12826
12895
|
// re-raster the target, so the highlight still marks the pixels it marked.
|
|
12827
12896
|
setPanes: (panes) => set({ panes }),
|
|
12828
|
-
setFitScale: (fitScale2) => set({ fitScale: fitScale2 }),
|
|
12829
|
-
requestAgentPan: (p) => set((
|
|
12830
|
-
clearAgentPan: () => set({ agentPan: null }),
|
|
12831
|
-
showAgentHighlight: (h) => set((
|
|
12832
|
-
clearAgentHighlight: (seq) => set((
|
|
12897
|
+
setFitScale: (fitScale2) => set(patchActive({ fitScale: fitScale2 })),
|
|
12898
|
+
requestAgentPan: (p) => set(patchActiveWith((t) => ({ agentPan: { ...p, seq: (t.agentPan?.seq ?? 0) + 1 } }))),
|
|
12899
|
+
clearAgentPan: () => set(patchActive({ agentPan: null })),
|
|
12900
|
+
showAgentHighlight: (h) => set(patchActiveWith((t) => ({ agentHighlight: { ...h, seq: (t.agentHighlight?.seq ?? 0) + 1 } }))),
|
|
12901
|
+
clearAgentHighlight: (seq) => set(patchActiveWith((t) => seq === void 0 || t.agentHighlight?.seq === seq ? { agentHighlight: null } : null)),
|
|
12833
12902
|
// Spec §7: leaving image mode restores the URL that was showing before.
|
|
12834
12903
|
// Either direction swaps what the target pane shows, so a highlight over
|
|
12835
12904
|
// the old content is dropped with it.
|
|
12836
12905
|
setMode: (mode) => set(
|
|
12837
|
-
(
|
|
12838
|
-
|
|
12906
|
+
patchActiveWith(
|
|
12907
|
+
(t) => mode === t.mode ? null : mode === "image" ? { mode, lastUrl: t.url, agentHighlight: null } : { mode, url: t.lastUrl, image: null, agentHighlight: null }
|
|
12908
|
+
)
|
|
12909
|
+
),
|
|
12910
|
+
syncTabs: (snap) => set((s) => {
|
|
12911
|
+
if (snap.tabs.length === 0) return {};
|
|
12912
|
+
const tabs = {};
|
|
12913
|
+
for (const info of snap.tabs) {
|
|
12914
|
+
const existing = s.tabs[info.id];
|
|
12915
|
+
tabs[info.id] = existing ? (
|
|
12916
|
+
// url and title are main's to know — it is what every tab's panes
|
|
12917
|
+
// report to — so the snapshot is authoritative for those two and
|
|
12918
|
+
// for nothing else.
|
|
12919
|
+
existing.url === info.url && existing.title === info.title ? existing : { ...existing, url: info.url, title: info.title }
|
|
12920
|
+
) : (
|
|
12921
|
+
// A tab the renderer has never seen. Its screen comes from the
|
|
12922
|
+
// snapshot rather than from `blankTab`'s defaults, because main
|
|
12923
|
+
// may have restored it from disk with a preset chosen in a
|
|
12924
|
+
// previous launch — and for a tab genuinely opened just now, the
|
|
12925
|
+
// session's own defaults are those same defaults.
|
|
12926
|
+
{
|
|
12927
|
+
...blankTab(),
|
|
12928
|
+
url: info.url,
|
|
12929
|
+
title: info.title,
|
|
12930
|
+
presetId: info.presetId,
|
|
12931
|
+
profileId: info.profileId,
|
|
12932
|
+
orientation: info.orientation
|
|
12933
|
+
}
|
|
12934
|
+
);
|
|
12935
|
+
}
|
|
12936
|
+
const tabOrder = snap.tabs.map((t) => t.id);
|
|
12937
|
+
return { tabs, tabOrder, activeId: tabs[snap.activeId] ? snap.activeId : tabOrder[0] };
|
|
12938
|
+
}),
|
|
12939
|
+
addTab: (id) => {
|
|
12940
|
+
const s = get();
|
|
12941
|
+
if (!canAddTab(s.tabOrder.length, s.settings.maxTabs)) return null;
|
|
12942
|
+
const next = id ?? newTabId();
|
|
12943
|
+
if (s.tabs[next]) {
|
|
12944
|
+
set({ activeId: next });
|
|
12945
|
+
return next;
|
|
12946
|
+
}
|
|
12947
|
+
set({ tabs: { ...s.tabs, [next]: blankTab() }, tabOrder: [...s.tabOrder, next], activeId: next });
|
|
12948
|
+
return next;
|
|
12949
|
+
},
|
|
12950
|
+
closeTab: (id) => set((s) => {
|
|
12951
|
+
if (!s.tabs[id]) return {};
|
|
12952
|
+
const result = closeTab(
|
|
12953
|
+
s.tabOrder.map((tabId) => ({ id: tabId })),
|
|
12954
|
+
id,
|
|
12955
|
+
s.activeId
|
|
12956
|
+
);
|
|
12957
|
+
if (result.activeId === null) {
|
|
12958
|
+
const fresh = newTabId();
|
|
12959
|
+
return { tabs: { [fresh]: blankTab() }, tabOrder: [fresh], activeId: fresh };
|
|
12960
|
+
}
|
|
12961
|
+
const tabs = { ...s.tabs };
|
|
12962
|
+
delete tabs[id];
|
|
12963
|
+
return { tabs, tabOrder: result.tabs.map((t) => t.id), activeId: result.activeId };
|
|
12964
|
+
}),
|
|
12965
|
+
activateTab: (id) => set((s) => s.tabs[id] ? { activeId: id } : {})
|
|
12839
12966
|
}));
|
|
12840
|
-
function
|
|
12841
|
-
|
|
12967
|
+
function selectTab(s) {
|
|
12968
|
+
return s.tabs[s.activeId];
|
|
12969
|
+
}
|
|
12970
|
+
function naturalScreen(s) {
|
|
12971
|
+
const tab = selectTab(s);
|
|
12972
|
+
const preset = SCREEN_PRESETS.find((p) => p.id === tab.presetId);
|
|
12842
12973
|
return preset ? {
|
|
12843
12974
|
width: preset.width,
|
|
12844
12975
|
height: preset.height,
|
|
12845
12976
|
diagonalInches: preset.diagonalInches,
|
|
12846
12977
|
deviceScaleFactor: preset.deviceScaleFactor
|
|
12847
|
-
} :
|
|
12978
|
+
} : tab.custom;
|
|
12979
|
+
}
|
|
12980
|
+
function selectScreen(s) {
|
|
12981
|
+
return applyOrientation(naturalScreen(s), selectTab(s).orientation);
|
|
12982
|
+
}
|
|
12983
|
+
function selectScreenShape(s) {
|
|
12984
|
+
const screen = selectScreen(s);
|
|
12985
|
+
return screenShape(screen.width, screen.height);
|
|
12986
|
+
}
|
|
12987
|
+
const ORIENTATIONS = ["portrait", "landscape"];
|
|
12988
|
+
function selectOrientationShapes(s) {
|
|
12989
|
+
const natural = naturalScreen(s);
|
|
12990
|
+
return ORIENTATIONS.map((value) => {
|
|
12991
|
+
const r = applyOrientation(natural, value);
|
|
12992
|
+
return screenShape(r.width, r.height);
|
|
12993
|
+
});
|
|
12848
12994
|
}
|
|
12849
12995
|
function selectDeviceScaleFactor(s) {
|
|
12850
12996
|
return selectScreen(s).deviceScaleFactor ?? 1;
|
|
@@ -12864,7 +13010,7 @@ function calibratedScale(s) {
|
|
|
12864
13010
|
diagonalInches: s.settings.hostDiagonalInches,
|
|
12865
13011
|
scaleFactor: s.host.scaleFactor
|
|
12866
13012
|
};
|
|
12867
|
-
const scale = computeScale(host, screen, s.pixelExact);
|
|
13013
|
+
const scale = computeScale(host, screen, selectTab(s).pixelExact);
|
|
12868
13014
|
return Number.isFinite(scale) && scale > 0 ? scale : null;
|
|
12869
13015
|
}
|
|
12870
13016
|
function selectScale(s) {
|
|
@@ -12877,13 +13023,15 @@ function selectHostNits(s) {
|
|
|
12877
13023
|
return s.settings.hostNits > 0 ? s.settings.hostNits : DEFAULT_SETTINGS.hostNits;
|
|
12878
13024
|
}
|
|
12879
13025
|
function selectProfile(s) {
|
|
12880
|
-
|
|
13026
|
+
const tab = selectTab(s);
|
|
13027
|
+
return tab.profileOverride ?? findProfile(tab.profileId);
|
|
12881
13028
|
}
|
|
12882
13029
|
function selectPanelParams(s) {
|
|
12883
13030
|
return profileToParams(selectProfile(s), selectHostNits(s));
|
|
12884
13031
|
}
|
|
12885
13032
|
function selectUrlBarText(s) {
|
|
12886
|
-
|
|
13033
|
+
const tab = selectTab(s);
|
|
13034
|
+
return tab.mode === "image" ? tab.image?.name ?? "" : tab.url;
|
|
12887
13035
|
}
|
|
12888
13036
|
const SCALES = [1, 2, 3];
|
|
12889
13037
|
const DEFAULT_SCALE = 2;
|
|
@@ -13060,12 +13208,13 @@ function TargetFooter() {
|
|
|
13060
13208
|
const params = useStore(useShallow(selectPanelParams));
|
|
13061
13209
|
const scale = useStore(selectScale);
|
|
13062
13210
|
const profile = useStore(selectProfile);
|
|
13063
|
-
const image = useStore((s) => s.image);
|
|
13064
|
-
const mode = useStore((s) => s.mode);
|
|
13065
|
-
const viewMode = useStore((s) => s.viewMode);
|
|
13066
|
-
const fitScale2 = useStore((s) => s.fitScale);
|
|
13211
|
+
const image = useStore((s) => selectTab(s).image);
|
|
13212
|
+
const mode = useStore((s) => selectTab(s).mode);
|
|
13213
|
+
const viewMode = useStore((s) => selectTab(s).viewMode);
|
|
13214
|
+
const fitScale2 = useStore((s) => selectTab(s).fitScale);
|
|
13067
13215
|
const dsf = useStore(selectDeviceScaleFactor);
|
|
13068
|
-
const
|
|
13216
|
+
const shape = useStore(selectScreenShape);
|
|
13217
|
+
const size = mode === "image" && image ? `${image.width}×${image.height}` : `${viewport.width}×${viewport.height}${dsf > 1 ? ` @${dsf}x` : ""} ${shape}`;
|
|
13069
13218
|
const depth = params.levels <= 63 ? "6-bit" : "8-bit";
|
|
13070
13219
|
const magnification = viewMode === "fit" && fitScale2 !== null ? [`fit ×${fitScale2.toFixed(2)}`, "not pixel-exact"] : [`×${scale.toFixed(2)}`];
|
|
13071
13220
|
return /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
@@ -13407,7 +13556,8 @@ function SettingsPanel() {
|
|
|
13407
13556
|
const settings = useStore(useShallow((s) => s.settings));
|
|
13408
13557
|
const update = useStore((s) => s.update);
|
|
13409
13558
|
const history = useStore((s) => s.history);
|
|
13410
|
-
const custom = useStore(useShallow((s) => s.custom));
|
|
13559
|
+
const custom = useStore(useShallow((s) => selectTab(s).custom));
|
|
13560
|
+
const orientation = useStore((s) => selectTab(s).orientation);
|
|
13411
13561
|
const viewport = useStore(useShallow(selectViewport));
|
|
13412
13562
|
const scale = useStore(selectScale);
|
|
13413
13563
|
const fallback = useStore(selectScaleIsFallback);
|
|
@@ -13480,6 +13630,15 @@ function SettingsPanel() {
|
|
|
13480
13630
|
] }),
|
|
13481
13631
|
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "Custom screen" }),
|
|
13482
13632
|
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "muted", children: "Editing these selects the Custom preset." }),
|
|
13633
|
+
orientation === "landscape" && /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "muted custom-rotated", children: [
|
|
13634
|
+
"Rotated, so these render transposed:",
|
|
13635
|
+
" ",
|
|
13636
|
+
(() => {
|
|
13637
|
+
const r = applyOrientation({ ...custom }, orientation);
|
|
13638
|
+
return `${r.width}×${r.height}`;
|
|
13639
|
+
})(),
|
|
13640
|
+
"."
|
|
13641
|
+
] }),
|
|
13483
13642
|
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
13484
13643
|
NumberField,
|
|
13485
13644
|
{
|
|
@@ -13564,6 +13723,24 @@ function SettingsPanel() {
|
|
|
13564
13723
|
] }),
|
|
13565
13724
|
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "check-now", onClick: () => void window.obsrv.checkUpdate(), children: "Check now" }),
|
|
13566
13725
|
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "muted", children: "One unauthenticated request to GitHub, at most once a day. No identifiers are sent." }),
|
|
13726
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "Tabs" }),
|
|
13727
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
13728
|
+
NumberField,
|
|
13729
|
+
{
|
|
13730
|
+
className: "max-tabs",
|
|
13731
|
+
label: "Maximum tabs",
|
|
13732
|
+
unit: "tabs",
|
|
13733
|
+
value: settings.maxTabs,
|
|
13734
|
+
min: MAX_TABS_MIN,
|
|
13735
|
+
step: 1,
|
|
13736
|
+
onCommit: (v) => commit({
|
|
13737
|
+
...useStore.getState().settings,
|
|
13738
|
+
maxTabs: Math.min(MAX_TABS_MAX, Math.max(MAX_TABS_MIN, Math.round(v)))
|
|
13739
|
+
}),
|
|
13740
|
+
onInvalid: setHostError
|
|
13741
|
+
}
|
|
13742
|
+
),
|
|
13743
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "muted", children: "Each tab is two Chromium renderers — a live pane and an offscreen render — plus the GPU and utility processes they pull in: twelve empty tabs measured 27 child processes and about 2.5 GB. A real page costs more. The cap is a memory decision, not a preference. Lowering it never closes a tab that is already open; it only stops new ones." }),
|
|
13567
13744
|
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "History" }),
|
|
13568
13745
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "control inline record-history-toggle", children: [
|
|
13569
13746
|
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
@@ -14036,10 +14213,11 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14036
14213
|
const params = useStore(useShallow(selectPanelParams));
|
|
14037
14214
|
const dsf = useStore(selectDeviceScaleFactor);
|
|
14038
14215
|
const requestedScale = useStore(selectScale);
|
|
14039
|
-
const mode = useStore((s) => s.mode);
|
|
14040
|
-
const viewMode = useStore((s) => s.viewMode);
|
|
14216
|
+
const mode = useStore((s) => selectTab(s).mode);
|
|
14217
|
+
const viewMode = useStore((s) => selectTab(s).viewMode);
|
|
14041
14218
|
const setViewMode = useStore((s) => s.setViewMode);
|
|
14042
14219
|
const setFitScale = useStore((s) => s.setFitScale);
|
|
14220
|
+
const activeId = useStore((s) => s.activeId);
|
|
14043
14221
|
const [stalled, setStalled] = reactExports.useState(false);
|
|
14044
14222
|
const stallTimer = reactExports.useRef(0);
|
|
14045
14223
|
const armedOnce = reactExports.useRef(false);
|
|
@@ -14071,7 +14249,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14071
14249
|
const smooth = fit || scale < 1;
|
|
14072
14250
|
reactExports.useEffect(() => {
|
|
14073
14251
|
setFitScale(fit ? scale : null);
|
|
14074
|
-
}, [fit, scale, setFitScale]);
|
|
14252
|
+
}, [fit, scale, activeId, setFitScale]);
|
|
14075
14253
|
reactExports.useEffect(() => () => setFitScale(null), [setFitScale]);
|
|
14076
14254
|
const draw = reactExports.useRef({ scale, params, smooth, dsf });
|
|
14077
14255
|
const imageRef = reactExports.useRef(imageFrame);
|
|
@@ -14110,7 +14288,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14110
14288
|
offFrame = window.obsrv.onFrame((m) => {
|
|
14111
14289
|
disarm();
|
|
14112
14290
|
if (!gl) return;
|
|
14113
|
-
if (useStore.getState().mode !== "url") return;
|
|
14291
|
+
if (selectTab(useStore.getState()).mode !== "url") return;
|
|
14114
14292
|
gl.resizeSource(m.frameWidth, m.frameHeight);
|
|
14115
14293
|
gl.uploadSlice(m.frame);
|
|
14116
14294
|
schedule();
|
|
@@ -14138,7 +14316,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14138
14316
|
const onWheel = (e) => {
|
|
14139
14317
|
if (e.altKey) {
|
|
14140
14318
|
e.preventDefault();
|
|
14141
|
-
if (useStore.getState().viewMode !== "1:1") return;
|
|
14319
|
+
if (selectTab(useStore.getState()).viewMode !== "1:1") return;
|
|
14142
14320
|
const body = canvas.closest(".pane-body");
|
|
14143
14321
|
if (body instanceof HTMLElement) {
|
|
14144
14322
|
body.scrollLeft += e.deltaX;
|
|
@@ -14146,7 +14324,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14146
14324
|
}
|
|
14147
14325
|
return;
|
|
14148
14326
|
}
|
|
14149
|
-
if (useStore.getState().mode !== "url") return;
|
|
14327
|
+
if (selectTab(useStore.getState()).mode !== "url") return;
|
|
14150
14328
|
e.preventDefault();
|
|
14151
14329
|
const r = canvas.getBoundingClientRect();
|
|
14152
14330
|
const cssPerTarget = draw.current.scale * draw.current.dsf / (window.devicePixelRatio || 1);
|
|
@@ -14155,7 +14333,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14155
14333
|
};
|
|
14156
14334
|
canvas.addEventListener("wheel", onWheel, { passive: false });
|
|
14157
14335
|
const onWindowUp = (e) => {
|
|
14158
|
-
if (useStore.getState().mode !== "url") return;
|
|
14336
|
+
if (selectTab(useStore.getState()).mode !== "url") return;
|
|
14159
14337
|
if (panRef.current || e.button === 1) return;
|
|
14160
14338
|
if (e.altKey && !forwardDrag.current) return;
|
|
14161
14339
|
if (e.target === canvas) return;
|
|
@@ -14178,8 +14356,10 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14178
14356
|
};
|
|
14179
14357
|
}, [onFatal, disarm]);
|
|
14180
14358
|
reactExports.useEffect(
|
|
14181
|
-
() => window.obsrv.onTargetNavigating(() => {
|
|
14182
|
-
|
|
14359
|
+
() => window.obsrv.onTargetNavigating(({ tabId }) => {
|
|
14360
|
+
const s = useStore.getState();
|
|
14361
|
+
if (tabId !== s.activeId) return;
|
|
14362
|
+
if (selectTab(s).mode === "url") arm();
|
|
14183
14363
|
}),
|
|
14184
14364
|
[arm]
|
|
14185
14365
|
);
|
|
@@ -14293,7 +14473,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14293
14473
|
);
|
|
14294
14474
|
setViewMode("1:1");
|
|
14295
14475
|
};
|
|
14296
|
-
const agentPan = useStore((s) => s.agentPan);
|
|
14476
|
+
const agentPan = useStore((s) => selectTab(s).agentPan);
|
|
14297
14477
|
const clearAgentPan = useStore((s) => s.clearAgentPan);
|
|
14298
14478
|
reactExports.useEffect(() => {
|
|
14299
14479
|
if (!agentPan) return;
|
|
@@ -14310,7 +14490,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14310
14490
|
body.scrollTop = jump.top;
|
|
14311
14491
|
}
|
|
14312
14492
|
}, [agentPan]);
|
|
14313
|
-
const agentHighlight = useStore((s) => s.agentHighlight);
|
|
14493
|
+
const agentHighlight = useStore((s) => selectTab(s).agentHighlight);
|
|
14314
14494
|
const clearAgentHighlight = useStore((s) => s.clearAgentHighlight);
|
|
14315
14495
|
reactExports.useEffect(() => {
|
|
14316
14496
|
if (!agentHighlight) return;
|
|
@@ -14415,6 +14595,23 @@ function matchHistory(entries, query, limit = HISTORY_SUGGESTIONS) {
|
|
|
14415
14595
|
const needle = query.trim().toLowerCase();
|
|
14416
14596
|
return entries.filter((e) => e.url.toLowerCase().includes(needle)).sort(byRank).slice(0, Math.max(0, limit));
|
|
14417
14597
|
}
|
|
14598
|
+
const AGENT_ACTIVITY_MS = 3e3;
|
|
14599
|
+
function useAgentActivity() {
|
|
14600
|
+
const [active, setActive] = reactExports.useState(false);
|
|
14601
|
+
reactExports.useEffect(() => {
|
|
14602
|
+
let timer;
|
|
14603
|
+
const off = window.obsrv.onAgentActivity(() => {
|
|
14604
|
+
setActive(true);
|
|
14605
|
+
clearTimeout(timer);
|
|
14606
|
+
timer = setTimeout(() => setActive(false), AGENT_ACTIVITY_MS);
|
|
14607
|
+
});
|
|
14608
|
+
return () => {
|
|
14609
|
+
clearTimeout(timer);
|
|
14610
|
+
off();
|
|
14611
|
+
};
|
|
14612
|
+
}, []);
|
|
14613
|
+
return active;
|
|
14614
|
+
}
|
|
14418
14615
|
/**
|
|
14419
14616
|
* @license lucide-react v1.34.0 - ISC
|
|
14420
14617
|
*
|
|
@@ -14542,42 +14739,73 @@ const createLucideIcon = (iconName, iconNode) => {
|
|
|
14542
14739
|
* This source code is licensed under the ISC license.
|
|
14543
14740
|
* See the LICENSE file in the root directory of this source tree.
|
|
14544
14741
|
*/
|
|
14545
|
-
const __iconNode$
|
|
14742
|
+
const __iconNode$a = [
|
|
14546
14743
|
["path", { d: "m12 19-7-7 7-7", key: "1l729n" }],
|
|
14547
14744
|
["path", { d: "M19 12H5", key: "x3x0zl" }]
|
|
14548
14745
|
];
|
|
14549
|
-
const ArrowLeft = createLucideIcon("arrow-left", __iconNode$
|
|
14746
|
+
const ArrowLeft = createLucideIcon("arrow-left", __iconNode$a);
|
|
14550
14747
|
/**
|
|
14551
14748
|
* @license lucide-react v1.34.0 - ISC
|
|
14552
14749
|
*
|
|
14553
14750
|
* This source code is licensed under the ISC license.
|
|
14554
14751
|
* See the LICENSE file in the root directory of this source tree.
|
|
14555
14752
|
*/
|
|
14556
|
-
const __iconNode$
|
|
14753
|
+
const __iconNode$9 = [
|
|
14557
14754
|
["path", { d: "M5 12h14", key: "1ays0h" }],
|
|
14558
14755
|
["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
|
|
14559
14756
|
];
|
|
14560
|
-
const ArrowRight = createLucideIcon("arrow-right", __iconNode$
|
|
14757
|
+
const ArrowRight = createLucideIcon("arrow-right", __iconNode$9);
|
|
14561
14758
|
/**
|
|
14562
14759
|
* @license lucide-react v1.34.0 - ISC
|
|
14563
14760
|
*
|
|
14564
14761
|
* This source code is licensed under the ISC license.
|
|
14565
14762
|
* See the LICENSE file in the root directory of this source tree.
|
|
14566
14763
|
*/
|
|
14567
|
-
const __iconNode$
|
|
14568
|
-
const ChevronDown = createLucideIcon("chevron-down", __iconNode$
|
|
14764
|
+
const __iconNode$8 = [["path", { d: "m6 9 6 6 6-6", key: "qrunsl" }]];
|
|
14765
|
+
const ChevronDown = createLucideIcon("chevron-down", __iconNode$8);
|
|
14569
14766
|
/**
|
|
14570
14767
|
* @license lucide-react v1.34.0 - ISC
|
|
14571
14768
|
*
|
|
14572
14769
|
* This source code is licensed under the ISC license.
|
|
14573
14770
|
* See the LICENSE file in the root directory of this source tree.
|
|
14574
14771
|
*/
|
|
14575
|
-
const __iconNode$
|
|
14772
|
+
const __iconNode$7 = [
|
|
14576
14773
|
["circle", { cx: "12", cy: "12", r: "1", key: "41hilf" }],
|
|
14577
14774
|
["circle", { cx: "12", cy: "5", r: "1", key: "gxeob9" }],
|
|
14578
14775
|
["circle", { cx: "12", cy: "19", r: "1", key: "lyex9k" }]
|
|
14579
14776
|
];
|
|
14580
|
-
const EllipsisVertical = createLucideIcon("ellipsis-vertical", __iconNode$
|
|
14777
|
+
const EllipsisVertical = createLucideIcon("ellipsis-vertical", __iconNode$7);
|
|
14778
|
+
/**
|
|
14779
|
+
* @license lucide-react v1.34.0 - ISC
|
|
14780
|
+
*
|
|
14781
|
+
* This source code is licensed under the ISC license.
|
|
14782
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
14783
|
+
*/
|
|
14784
|
+
const __iconNode$6 = [
|
|
14785
|
+
["path", { d: "M5 12h14", key: "1ays0h" }],
|
|
14786
|
+
["path", { d: "M12 5v14", key: "s699le" }]
|
|
14787
|
+
];
|
|
14788
|
+
const Plus = createLucideIcon("plus", __iconNode$6);
|
|
14789
|
+
/**
|
|
14790
|
+
* @license lucide-react v1.34.0 - ISC
|
|
14791
|
+
*
|
|
14792
|
+
* This source code is licensed under the ISC license.
|
|
14793
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
14794
|
+
*/
|
|
14795
|
+
const __iconNode$5 = [
|
|
14796
|
+
["rect", { width: "20", height: "12", x: "2", y: "6", rx: "2", key: "9lu3g6" }]
|
|
14797
|
+
];
|
|
14798
|
+
const RectangleHorizontal = createLucideIcon("rectangle-horizontal", __iconNode$5);
|
|
14799
|
+
/**
|
|
14800
|
+
* @license lucide-react v1.34.0 - ISC
|
|
14801
|
+
*
|
|
14802
|
+
* This source code is licensed under the ISC license.
|
|
14803
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
14804
|
+
*/
|
|
14805
|
+
const __iconNode$4 = [
|
|
14806
|
+
["rect", { width: "12", height: "20", x: "6", y: "2", rx: "2", key: "1oxtiu" }]
|
|
14807
|
+
];
|
|
14808
|
+
const RectangleVertical = createLucideIcon("rectangle-vertical", __iconNode$4);
|
|
14581
14809
|
/**
|
|
14582
14810
|
* @license lucide-react v1.34.0 - ISC
|
|
14583
14811
|
*
|
|
@@ -14643,7 +14871,13 @@ const ICONS = {
|
|
|
14643
14871
|
close: X,
|
|
14644
14872
|
sliders: SlidersHorizontal,
|
|
14645
14873
|
gear: Settings,
|
|
14646
|
-
chevron: ChevronDown
|
|
14874
|
+
chevron: ChevronDown,
|
|
14875
|
+
plus: Plus,
|
|
14876
|
+
// The rotate control's two shapes. A plain outline of the screen you get is
|
|
14877
|
+
// the whole affordance — no arrow, no device silhouette: the target may be a
|
|
14878
|
+
// monitor as readily as a phone.
|
|
14879
|
+
portrait: RectangleVertical,
|
|
14880
|
+
landscape: RectangleHorizontal
|
|
14647
14881
|
};
|
|
14648
14882
|
function Icon({ name, size = 16 }) {
|
|
14649
14883
|
const Glyph = ICONS[name];
|
|
@@ -14714,6 +14948,77 @@ function Segmented({
|
|
|
14714
14948
|
o.id
|
|
14715
14949
|
)) });
|
|
14716
14950
|
}
|
|
14951
|
+
function TabBar() {
|
|
14952
|
+
const tabOrder = useStore(useShallow((s) => s.tabOrder));
|
|
14953
|
+
const activeId = useStore((s) => s.activeId);
|
|
14954
|
+
const maxTabs = useStore((s) => s.settings.maxTabs);
|
|
14955
|
+
const driving = useStore((s) => s.settings.agentControl);
|
|
14956
|
+
const agentActive = useAgentActivity();
|
|
14957
|
+
const canAdd = canAddTab(tabOrder.length, maxTabs);
|
|
14958
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "chrome-row chrome-tabs", children: [
|
|
14959
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tabs", role: "tablist", "aria-label": "Open tabs", children: tabOrder.map((id) => /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14960
|
+
Tab,
|
|
14961
|
+
{
|
|
14962
|
+
id,
|
|
14963
|
+
active: id === activeId,
|
|
14964
|
+
driven: driving && id === activeId,
|
|
14965
|
+
busy: agentActive
|
|
14966
|
+
},
|
|
14967
|
+
id
|
|
14968
|
+
)) }),
|
|
14969
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14970
|
+
"button",
|
|
14971
|
+
{
|
|
14972
|
+
className: "tab-new",
|
|
14973
|
+
type: "button",
|
|
14974
|
+
"aria-label": "New tab",
|
|
14975
|
+
disabled: !canAdd,
|
|
14976
|
+
title: canAdd ? "New tab" : `${maxTabs} tabs is the limit — each one is two Chromium renderers, and twelve tabs cost about 2.5 GB. Raise it in Settings.`,
|
|
14977
|
+
onClick: () => {
|
|
14978
|
+
void window.obsrv.addTab();
|
|
14979
|
+
},
|
|
14980
|
+
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { name: "plus", size: 14 })
|
|
14981
|
+
}
|
|
14982
|
+
)
|
|
14983
|
+
] });
|
|
14984
|
+
}
|
|
14985
|
+
function Tab({
|
|
14986
|
+
id,
|
|
14987
|
+
active,
|
|
14988
|
+
driven,
|
|
14989
|
+
busy
|
|
14990
|
+
}) {
|
|
14991
|
+
const label = useStore((s) => {
|
|
14992
|
+
const t = s.tabs[id];
|
|
14993
|
+
return t ? tabTitle(t.url, t.title) : "";
|
|
14994
|
+
});
|
|
14995
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `tab${driven ? " driven" : ""}${driven && busy ? " busy" : ""}`, children: [
|
|
14996
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14997
|
+
"button",
|
|
14998
|
+
{
|
|
14999
|
+
className: "tab-label",
|
|
15000
|
+
type: "button",
|
|
15001
|
+
role: "tab",
|
|
15002
|
+
"aria-selected": active,
|
|
15003
|
+
title: driven ? `${label}
|
|
15004
|
+
Agent control is driving this tab` : label,
|
|
15005
|
+
onClick: () => window.obsrv.activateTab(id),
|
|
15006
|
+
children: label
|
|
15007
|
+
}
|
|
15008
|
+
),
|
|
15009
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
15010
|
+
"button",
|
|
15011
|
+
{
|
|
15012
|
+
className: "tab-close",
|
|
15013
|
+
type: "button",
|
|
15014
|
+
"aria-label": `Close ${label}`,
|
|
15015
|
+
title: "Close tab",
|
|
15016
|
+
onClick: () => window.obsrv.closeTab(id),
|
|
15017
|
+
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { name: "close", size: 12 })
|
|
15018
|
+
}
|
|
15019
|
+
)
|
|
15020
|
+
] });
|
|
15021
|
+
}
|
|
14717
15022
|
function Select({ className, value, label, ariaLabel, onChange, children }) {
|
|
14718
15023
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "select-shell", children: [
|
|
14719
15024
|
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "select-label", "aria-hidden": "true", children: label }),
|
|
@@ -14744,24 +15049,27 @@ const PANES = [
|
|
|
14744
15049
|
{ id: "target", label: "Target", title: "The target render alone, full width" }
|
|
14745
15050
|
];
|
|
14746
15051
|
function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
14747
|
-
const mode = useStore((s) => s.mode);
|
|
14748
|
-
const presetId = useStore((s) => s.presetId);
|
|
14749
|
-
const profileId = useStore((s) => s.profileId);
|
|
14750
|
-
const pixelExact = useStore((s) => s.pixelExact);
|
|
14751
|
-
const error = useStore((s) => s.error);
|
|
14752
|
-
const loading = useStore((s) => s.targetLoading);
|
|
15052
|
+
const mode = useStore((s) => selectTab(s).mode);
|
|
15053
|
+
const presetId = useStore((s) => selectTab(s).presetId);
|
|
15054
|
+
const profileId = useStore((s) => selectTab(s).profileId);
|
|
15055
|
+
const pixelExact = useStore((s) => selectTab(s).pixelExact);
|
|
15056
|
+
const error = useStore((s) => selectTab(s).error);
|
|
15057
|
+
const loading = useStore((s) => selectTab(s).targetLoading);
|
|
14753
15058
|
const barText = useStore(selectUrlBarText);
|
|
14754
15059
|
const viewport = useStore(useShallow(selectViewport));
|
|
14755
15060
|
const setUrl = useStore((s) => s.setUrl);
|
|
14756
15061
|
const setMode = useStore((s) => s.setMode);
|
|
14757
15062
|
const setPreset = useStore((s) => s.setPreset);
|
|
15063
|
+
const orientation = useStore((s) => selectTab(s).orientation);
|
|
15064
|
+
const orientationShapes = useStore(useShallow(selectOrientationShapes));
|
|
15065
|
+
const setOrientation = useStore((s) => s.setOrientation);
|
|
14758
15066
|
const setProfile = useStore((s) => s.setProfile);
|
|
14759
15067
|
const setPixelExact = useStore((s) => s.setPixelExact);
|
|
14760
15068
|
const setError = useStore((s) => s.setError);
|
|
14761
15069
|
const surround = useStore((s) => s.surround);
|
|
14762
15070
|
const update = useStore((s) => s.update);
|
|
14763
15071
|
const setSurround = useStore((s) => s.setSurround);
|
|
14764
|
-
const viewMode = useStore((s) => s.viewMode);
|
|
15072
|
+
const viewMode = useStore((s) => selectTab(s).viewMode);
|
|
14765
15073
|
const setViewMode = useStore((s) => s.setViewMode);
|
|
14766
15074
|
const panes = useStore((s) => s.panes);
|
|
14767
15075
|
const setPanes = useStore((s) => s.setPanes);
|
|
@@ -14779,19 +15087,7 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
14779
15087
|
setHighlight(-1);
|
|
14780
15088
|
};
|
|
14781
15089
|
const picked = highlight >= 0 && highlight < matches.length ? matches[highlight] : null;
|
|
14782
|
-
const
|
|
14783
|
-
reactExports.useEffect(() => {
|
|
14784
|
-
let timer;
|
|
14785
|
-
const off = window.obsrv.onAgentActivity(() => {
|
|
14786
|
-
setAgentActive(true);
|
|
14787
|
-
clearTimeout(timer);
|
|
14788
|
-
timer = setTimeout(() => setAgentActive(false), 3e3);
|
|
14789
|
-
});
|
|
14790
|
-
return () => {
|
|
14791
|
-
clearTimeout(timer);
|
|
14792
|
-
off();
|
|
14793
|
-
};
|
|
14794
|
-
}, []);
|
|
15090
|
+
const agentActive = useAgentActivity();
|
|
14795
15091
|
const toggleAgent = () => {
|
|
14796
15092
|
const current = useStore.getState().settings;
|
|
14797
15093
|
const next = { ...current, agentControl: !current.agentControl };
|
|
@@ -14858,6 +15154,7 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
14858
15154
|
const presetLabel = SCREEN_PRESETS.find((p) => p.id === presetId)?.label ?? "Custom";
|
|
14859
15155
|
const profileLabel = PANEL_PROFILES.find((p) => p.id === profileId)?.label ?? profileId;
|
|
14860
15156
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "chrome", children: [
|
|
15157
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(TabBar, {}),
|
|
14861
15158
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "chrome-row chrome-browse", children: [
|
|
14862
15159
|
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14863
15160
|
"button",
|
|
@@ -15038,6 +15335,23 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
15038
15335
|
]
|
|
15039
15336
|
}
|
|
15040
15337
|
),
|
|
15338
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "orientation-control", role: "group", "aria-label": "Screen orientation", children: ORIENTATIONS.map((value, i) => {
|
|
15339
|
+
const shape = orientationShapes[i] ?? value;
|
|
15340
|
+
const name = shape === "landscape" ? "Landscape" : "Portrait";
|
|
15341
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
15342
|
+
"button",
|
|
15343
|
+
{
|
|
15344
|
+
type: "button",
|
|
15345
|
+
className: `orient-${shape}`,
|
|
15346
|
+
title: `${name} — ${value === orientation ? "showing" : "rotate the screen"}`,
|
|
15347
|
+
"aria-label": name,
|
|
15348
|
+
"aria-pressed": orientation === value,
|
|
15349
|
+
onClick: () => setOrientation(value),
|
|
15350
|
+
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { name: shape })
|
|
15351
|
+
},
|
|
15352
|
+
value
|
|
15353
|
+
);
|
|
15354
|
+
}) }),
|
|
15041
15355
|
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
15042
15356
|
Segmented,
|
|
15043
15357
|
{
|
|
@@ -15089,29 +15403,35 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
15089
15403
|
function App() {
|
|
15090
15404
|
const [fatal, setFatal] = reactExports.useState(null);
|
|
15091
15405
|
const [drawer, setDrawer] = reactExports.useState("none");
|
|
15092
|
-
const [
|
|
15406
|
+
const [images, setImages] = reactExports.useState({});
|
|
15093
15407
|
const dropToken = reactExports.useRef(0);
|
|
15094
15408
|
const targetPaneRef = reactExports.useRef(null);
|
|
15095
15409
|
const [targetBounds, setTargetBounds] = reactExports.useState(null);
|
|
15096
15410
|
const [canvasBounds, setCanvasBounds] = reactExports.useState(null);
|
|
15411
|
+
const [tabsKnown, setTabsKnown] = reactExports.useState(false);
|
|
15097
15412
|
const toggle = (which) => () => setDrawer((d) => d === which ? "none" : which);
|
|
15098
15413
|
const setHost = useStore((s) => s.setHost);
|
|
15099
15414
|
const setSettings = useStore((s) => s.setSettings);
|
|
15100
|
-
const
|
|
15101
|
-
const
|
|
15102
|
-
const
|
|
15415
|
+
const setTabUrl = useStore((s) => s.setTabUrl);
|
|
15416
|
+
const setTabTitle = useStore((s) => s.setTabTitle);
|
|
15417
|
+
const setTabError = useStore((s) => s.setTabError);
|
|
15418
|
+
const setTabLoading = useStore((s) => s.setTabLoading);
|
|
15419
|
+
const syncTabs = useStore((s) => s.syncTabs);
|
|
15103
15420
|
const setUpdate = useStore((s) => s.setUpdate);
|
|
15104
15421
|
const setHistory = useStore((s) => s.setHistory);
|
|
15105
15422
|
const setImageMeta = useStore((s) => s.setImage);
|
|
15106
15423
|
const setMode = useStore((s) => s.setMode);
|
|
15107
15424
|
const setToast = useStore((s) => s.setToast);
|
|
15108
|
-
const mode = useStore((s) => s.mode);
|
|
15425
|
+
const mode = useStore((s) => selectTab(s).mode);
|
|
15109
15426
|
const surround = useStore((s) => s.surround);
|
|
15110
15427
|
const viewport = useStore(useShallow(selectViewport));
|
|
15111
15428
|
const deviceScaleFactor = useStore(selectDeviceScaleFactor);
|
|
15112
|
-
const presetId = useStore((s) => s.presetId);
|
|
15113
|
-
const profileId = useStore((s) => s.profileId);
|
|
15114
|
-
const
|
|
15429
|
+
const presetId = useStore((s) => selectTab(s).presetId);
|
|
15430
|
+
const profileId = useStore((s) => selectTab(s).profileId);
|
|
15431
|
+
const orientation = useStore((s) => selectTab(s).orientation);
|
|
15432
|
+
const viewMode = useStore((s) => selectTab(s).viewMode);
|
|
15433
|
+
const activeId = useStore((s) => s.activeId);
|
|
15434
|
+
const tabOrder = useStore((s) => s.tabOrder);
|
|
15115
15435
|
const panes = useStore((s) => s.panes);
|
|
15116
15436
|
const split = useStore((s) => s.settings.split);
|
|
15117
15437
|
reactExports.useEffect(() => {
|
|
@@ -15119,28 +15439,36 @@ function App() {
|
|
|
15119
15439
|
window.obsrv.getSettings().then(setSettings, (e) => console.warn("obsrv: getSettings failed", e));
|
|
15120
15440
|
window.obsrv.getUpdate().then(setUpdate, (e) => console.warn("obsrv: getUpdate failed", e));
|
|
15121
15441
|
window.obsrv.getHistory().then(setHistory, (e) => console.warn("obsrv: getHistory failed", e));
|
|
15442
|
+
window.obsrv.getTabs().then(syncTabs, (e) => console.warn("obsrv: getTabs failed", e)).finally(() => setTabsKnown(true));
|
|
15122
15443
|
const offs = [
|
|
15123
15444
|
window.obsrv.onHostChanged(setHost),
|
|
15445
|
+
// Every one of these names its tab, so a background tab keeps its own
|
|
15446
|
+
// strip entry current without rewriting the address bar of the tab in
|
|
15447
|
+
// front — which is what an unnamed report from a background tab did, and
|
|
15448
|
+
// why main used to gate them on the tab being in front at all.
|
|
15449
|
+
//
|
|
15124
15450
|
// A committed navigation — back, forward, reload, a link — supersedes
|
|
15125
|
-
//
|
|
15126
|
-
// error page without emitting `did-navigate`, so a failed
|
|
15127
|
-
// no URL at all (measured on Electron 43; a bad host, a
|
|
15128
|
-
// connection, a missing file and a Back onto an error page all
|
|
15129
|
-
// `did-fail-load` alone). The badge therefore lands last by having
|
|
15451
|
+
// that tab's last load failure, and only a committed one: Chromium
|
|
15452
|
+
// commits its error page without emitting `did-navigate`, so a failed
|
|
15453
|
+
// load reports no URL at all (measured on Electron 43; a bad host, a
|
|
15454
|
+
// refused connection, a missing file and a Back onto an error page all
|
|
15455
|
+
// fire `did-fail-load` alone). The badge therefore lands last by having
|
|
15130
15456
|
// nothing race it.
|
|
15131
|
-
window.obsrv.onUrlChanged((url) => {
|
|
15132
|
-
|
|
15133
|
-
|
|
15457
|
+
window.obsrv.onUrlChanged(({ tabId, url }) => {
|
|
15458
|
+
setTabError(tabId, null);
|
|
15459
|
+
setTabUrl(tabId, url);
|
|
15134
15460
|
}),
|
|
15135
|
-
window.obsrv.
|
|
15136
|
-
window.obsrv.
|
|
15461
|
+
window.obsrv.onTitleChanged(({ tabId, title }) => setTabTitle(tabId, title)),
|
|
15462
|
+
window.obsrv.onLoadError(({ tabId, error }) => setTabError(tabId, error)),
|
|
15463
|
+
window.obsrv.onTargetLoading(({ tabId, loading }) => setTabLoading(tabId, loading)),
|
|
15464
|
+
window.obsrv.onTabsChanged(syncTabs),
|
|
15137
15465
|
window.obsrv.onUpdateStatus(setUpdate),
|
|
15138
15466
|
window.obsrv.onHistoryChanged(setHistory)
|
|
15139
15467
|
];
|
|
15140
15468
|
return () => {
|
|
15141
15469
|
for (const off of offs) off();
|
|
15142
15470
|
};
|
|
15143
|
-
}, [setHost, setSettings,
|
|
15471
|
+
}, [setHost, setSettings, setTabUrl, setTabTitle, setTabError, setTabLoading, syncTabs, setUpdate, setHistory]);
|
|
15144
15472
|
reactExports.useEffect(() => {
|
|
15145
15473
|
void window.obsrv.setViewport(viewport.width, viewport.height, deviceScaleFactor);
|
|
15146
15474
|
}, [viewport.width, viewport.height, deviceScaleFactor]);
|
|
@@ -15180,13 +15508,15 @@ function App() {
|
|
|
15180
15508
|
};
|
|
15181
15509
|
}, []);
|
|
15182
15510
|
reactExports.useEffect(() => {
|
|
15183
|
-
|
|
15184
|
-
|
|
15511
|
+
if (!tabsKnown) return;
|
|
15512
|
+
window.obsrv.reportUiState({ tabId: activeId, presetId, profileId, orientation, viewMode, panes, mode, targetBounds, canvasBounds });
|
|
15513
|
+
}, [tabsKnown, activeId, presetId, profileId, orientation, viewMode, panes, mode, targetBounds, canvasBounds]);
|
|
15185
15514
|
reactExports.useEffect(() => {
|
|
15186
15515
|
return window.obsrv.onAgentApply((patch) => {
|
|
15187
15516
|
const s = useStore.getState();
|
|
15188
15517
|
if (patch.presetId !== void 0) s.setPreset(patch.presetId);
|
|
15189
15518
|
if (patch.profileId !== void 0) s.setProfile(patch.profileId);
|
|
15519
|
+
if (patch.orientation !== void 0) s.setOrientation(patch.orientation);
|
|
15190
15520
|
if (patch.viewMode !== void 0) s.setViewMode(patch.viewMode);
|
|
15191
15521
|
if (patch.panes !== void 0) s.setPanes(patch.panes);
|
|
15192
15522
|
if (patch.pixelExact !== void 0) s.setPixelExact(patch.pixelExact);
|
|
@@ -15197,21 +15527,24 @@ function App() {
|
|
|
15197
15527
|
reactExports.useEffect(() => {
|
|
15198
15528
|
document.documentElement.dataset.surround = surround;
|
|
15199
15529
|
}, [surround]);
|
|
15530
|
+
const image = images[activeId] ?? null;
|
|
15200
15531
|
const onImage = async (file, exportScale) => {
|
|
15201
15532
|
const token = ++dropToken.current;
|
|
15533
|
+
const tabId = activeId;
|
|
15202
15534
|
try {
|
|
15203
15535
|
const limits = {
|
|
15204
15536
|
...DEFAULT_IMAGE_LIMITS,
|
|
15205
15537
|
maxDimension: Math.min(DEFAULT_IMAGE_LIMITS.maxDimension, probeMaxTextureSize())
|
|
15206
15538
|
};
|
|
15207
15539
|
const loaded = await loadImage(file, exportScale, limits);
|
|
15208
|
-
if (token !== dropToken.current) {
|
|
15540
|
+
if (token !== dropToken.current || useStore.getState().activeId !== tabId) {
|
|
15209
15541
|
URL.revokeObjectURL(loaded.objectUrl);
|
|
15210
15542
|
return;
|
|
15211
15543
|
}
|
|
15212
|
-
|
|
15213
|
-
|
|
15214
|
-
|
|
15544
|
+
setImages((previous) => {
|
|
15545
|
+
const stale = previous[tabId];
|
|
15546
|
+
if (stale) URL.revokeObjectURL(stale.objectUrl);
|
|
15547
|
+
return { ...previous, [tabId]: loaded };
|
|
15215
15548
|
});
|
|
15216
15549
|
setImageMeta({
|
|
15217
15550
|
name: file.name,
|
|
@@ -15230,11 +15563,28 @@ function App() {
|
|
|
15230
15563
|
};
|
|
15231
15564
|
reactExports.useEffect(() => {
|
|
15232
15565
|
if (mode === "image") return;
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
return
|
|
15566
|
+
setImages((previous) => {
|
|
15567
|
+
const going = previous[activeId];
|
|
15568
|
+
if (!going) return previous;
|
|
15569
|
+
URL.revokeObjectURL(going.objectUrl);
|
|
15570
|
+
const next = { ...previous };
|
|
15571
|
+
delete next[activeId];
|
|
15572
|
+
return next;
|
|
15236
15573
|
});
|
|
15237
|
-
}, [mode]);
|
|
15574
|
+
}, [mode, activeId]);
|
|
15575
|
+
reactExports.useEffect(() => {
|
|
15576
|
+
setImages((previous) => {
|
|
15577
|
+
const open = new Set(tabOrder);
|
|
15578
|
+
const gone = Object.keys(previous).filter((id) => !open.has(id));
|
|
15579
|
+
if (gone.length === 0) return previous;
|
|
15580
|
+
const next = { ...previous };
|
|
15581
|
+
for (const id of gone) {
|
|
15582
|
+
URL.revokeObjectURL(next[id].objectUrl);
|
|
15583
|
+
delete next[id];
|
|
15584
|
+
}
|
|
15585
|
+
return next;
|
|
15586
|
+
});
|
|
15587
|
+
}, [tabOrder]);
|
|
15238
15588
|
const imageFrame = reactExports.useMemo(() => {
|
|
15239
15589
|
if (mode !== "image" || !image) return null;
|
|
15240
15590
|
return {
|