getobsrv 0.11.0 → 0.12.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/main/cli.js +1 -1
- package/out/main/index.js +741 -201
- package/out/main/{targetSource-nbfc-Fe6.js → targetSource-DlsHc-Ri.js} +43 -1
- package/out/mcp/server.js +21 -1
- package/out/preload/app.js +19 -16
- package/out/renderer/assets/{index-B61fbgqt.css → index-DvwDBeho.css} +107 -4
- package/out/renderer/assets/{index-BlbBg6lm.js → index-gYDbIgqJ.js} +380 -118
- package/out/renderer/index.html +2 -2
- package/out/shared/control.js +13 -1
- package/out/shared/ipcPayloads.js +29 -1
- package/out/shared/presets.js +6 -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,7 +12699,8 @@ 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
|
};
|
|
12702
12705
|
const SCREEN_PRESETS = [
|
|
12703
12706
|
// Laptops — ordered largest to smallest panel, then the denser 1080p outlier.
|
|
@@ -12761,6 +12764,27 @@ function profileToParams(p, hostNits) {
|
|
|
12761
12764
|
dither: p.frc
|
|
12762
12765
|
};
|
|
12763
12766
|
}
|
|
12767
|
+
function closeTab(tabs, closeId, activeId) {
|
|
12768
|
+
const index = tabs.findIndex((t) => t.id === closeId);
|
|
12769
|
+
if (index === -1) return { tabs, activeId };
|
|
12770
|
+
const next = tabs.filter((t) => t.id !== closeId);
|
|
12771
|
+
if (next.length === 0) return { tabs: next, activeId: null };
|
|
12772
|
+
if (closeId !== activeId) return { tabs: next, activeId };
|
|
12773
|
+
const neighbour = next[Math.min(index, next.length - 1)];
|
|
12774
|
+
return { tabs: next, activeId: neighbour.id };
|
|
12775
|
+
}
|
|
12776
|
+
function canAddTab(count, max) {
|
|
12777
|
+
return count < max;
|
|
12778
|
+
}
|
|
12779
|
+
function tabTitle(url, pageTitle) {
|
|
12780
|
+
if (pageTitle.trim() !== "") return pageTitle;
|
|
12781
|
+
if (url.trim() === "" || url.trim() === "about:blank") return "New tab";
|
|
12782
|
+
try {
|
|
12783
|
+
return new URL(url).host || url;
|
|
12784
|
+
} catch {
|
|
12785
|
+
return url;
|
|
12786
|
+
}
|
|
12787
|
+
}
|
|
12764
12788
|
const CUSTOM_PRESET_ID = "custom";
|
|
12765
12789
|
const FALLBACK_SCALE = 2;
|
|
12766
12790
|
function sameError(a, b) {
|
|
@@ -12768,83 +12792,175 @@ function sameError(a, b) {
|
|
|
12768
12792
|
if (a === null || b === null) return false;
|
|
12769
12793
|
return a.code === b.code && a.url === b.url && a.description === b.description;
|
|
12770
12794
|
}
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12795
|
+
function blankTab() {
|
|
12796
|
+
return {
|
|
12797
|
+
mode: "url",
|
|
12798
|
+
url: "",
|
|
12799
|
+
title: "",
|
|
12800
|
+
lastUrl: "",
|
|
12801
|
+
presetId: "1080p-24",
|
|
12802
|
+
custom: { width: 1920, height: 1080, diagonalInches: 24 },
|
|
12803
|
+
pixelExact: false,
|
|
12804
|
+
profileId: PANEL_PROFILES[0].id,
|
|
12805
|
+
profileOverride: null,
|
|
12806
|
+
targetLoading: false,
|
|
12807
|
+
error: null,
|
|
12808
|
+
image: null,
|
|
12809
|
+
// Fit, not 1:1: fit never enlarges past 1:1, so a render that already fits
|
|
12810
|
+
// its pane opens at true magnification anyway, while one that does not is
|
|
12811
|
+
// shown whole instead of as its top-left corner. Fit is interactive, so
|
|
12812
|
+
// this costs nothing — and the footer names the actual magnification
|
|
12813
|
+
// whenever fit is minifying.
|
|
12814
|
+
viewMode: "fit",
|
|
12815
|
+
fitScale: null,
|
|
12816
|
+
agentPan: null,
|
|
12817
|
+
agentHighlight: null
|
|
12818
|
+
};
|
|
12819
|
+
}
|
|
12820
|
+
let nextTabId = 0;
|
|
12821
|
+
function newTabId() {
|
|
12822
|
+
nextTabId += 1;
|
|
12823
|
+
return `local-${nextTabId}`;
|
|
12824
|
+
}
|
|
12825
|
+
const patchActiveWith = (f) => (s) => {
|
|
12826
|
+
const active = s.tabs[s.activeId];
|
|
12827
|
+
const patch = f(active);
|
|
12828
|
+
return patch === null ? {} : { tabs: { ...s.tabs, [s.activeId]: { ...active, ...patch } } };
|
|
12829
|
+
};
|
|
12830
|
+
const patchActive = (patch) => patchActiveWith(() => patch);
|
|
12831
|
+
const patchTabWith = (id, f) => (s) => {
|
|
12832
|
+
const t = s.tabs[id];
|
|
12833
|
+
if (!t) return {};
|
|
12834
|
+
const patch = f(t);
|
|
12835
|
+
return patch === null ? {} : { tabs: { ...s.tabs, [id]: { ...t, ...patch } } };
|
|
12836
|
+
};
|
|
12837
|
+
const FIRST_TAB = newTabId();
|
|
12838
|
+
const useStore = create()((set, get) => ({
|
|
12839
|
+
tabs: { [FIRST_TAB]: blankTab() },
|
|
12840
|
+
tabOrder: [FIRST_TAB],
|
|
12841
|
+
activeId: FIRST_TAB,
|
|
12780
12842
|
settings: { ...DEFAULT_SETTINGS },
|
|
12781
12843
|
// Zeroes until the first `getHostInfo`; `selectScale` falls back meanwhile.
|
|
12782
12844
|
host: { physicalWidth: 0, physicalHeight: 0, scaleFactor: 0 },
|
|
12783
|
-
targetLoading: false,
|
|
12784
|
-
error: null,
|
|
12785
12845
|
toast: null,
|
|
12786
12846
|
update: null,
|
|
12787
12847
|
history: [],
|
|
12788
|
-
image: null,
|
|
12789
12848
|
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
12849
|
panes: "both",
|
|
12797
|
-
fitScale: null,
|
|
12798
|
-
agentPan: null,
|
|
12799
|
-
agentHighlight: null,
|
|
12800
12850
|
// Does not clear `error`: a failed load navigates to Chromium's error page,
|
|
12801
12851
|
// so clearing here would wipe the toolbar badge the moment it appeared.
|
|
12802
12852
|
// Does clear the agent highlight: it marked pixels of the page that was
|
|
12803
12853
|
// showing, and a committed navigation (a reload included) replaces them.
|
|
12804
|
-
setUrl: (url) => set({ url, agentHighlight: null }),
|
|
12854
|
+
setUrl: (url) => set((s) => patchTabWith(s.activeId, () => ({ url, agentHighlight: null }))(s)),
|
|
12855
|
+
setTabUrl: (id, url) => set(patchTabWith(id, () => ({ url, agentHighlight: null }))),
|
|
12856
|
+
setTabTitle: (id, title) => set(patchTabWith(id, (t) => t.title === title ? null : { title })),
|
|
12857
|
+
// Both panes report the same failure; see `setError`.
|
|
12858
|
+
setTabError: (id, error) => set(patchTabWith(id, (t) => sameError(t.error, error) ? null : { error })),
|
|
12859
|
+
setTabLoading: (id, targetLoading) => set(patchTabWith(id, () => ({ targetLoading }))),
|
|
12805
12860
|
// A screen change re-rasters the target, so a highlight's target-pixel rect
|
|
12806
12861
|
// no longer marks what it marked; the same for the custom fields below.
|
|
12807
|
-
setPreset: (presetId) => set({ presetId, agentHighlight: null }),
|
|
12808
|
-
setCustom: (c) => set((
|
|
12809
|
-
setPixelExact: (pixelExact) => set({ pixelExact }),
|
|
12862
|
+
setPreset: (presetId) => set(patchActive({ presetId, agentHighlight: null })),
|
|
12863
|
+
setCustom: (c) => set(patchActiveWith((t) => ({ custom: { ...t.custom, ...c }, presetId: CUSTOM_PRESET_ID, agentHighlight: null }))),
|
|
12864
|
+
setPixelExact: (pixelExact) => set(patchActive({ pixelExact })),
|
|
12810
12865
|
// Picking a profile drops any hand-tuned slider values.
|
|
12811
|
-
setProfile: (profileId) => set({ profileId, profileOverride: null }),
|
|
12812
|
-
setProfileOverride: (profileOverride) => set({ profileOverride }),
|
|
12866
|
+
setProfile: (profileId) => set(patchActive({ profileId, profileOverride: null })),
|
|
12867
|
+
setProfileOverride: (profileOverride) => set(patchActive({ profileOverride })),
|
|
12813
12868
|
setSettings: (settings) => set({ settings }),
|
|
12814
12869
|
setHost: (host) => set({ host }),
|
|
12815
|
-
setTargetLoading: (targetLoading) => set({ targetLoading }),
|
|
12870
|
+
setTargetLoading: (targetLoading) => set(patchActive({ targetLoading })),
|
|
12816
12871
|
// Both panes report the same `loadError` for one failed navigation; the
|
|
12817
12872
|
// duplicate must not replace the object and re-render everything twice.
|
|
12818
|
-
setError: (error) => set((
|
|
12873
|
+
setError: (error) => set(patchActiveWith((t) => sameError(t.error, error) ? null : { error })),
|
|
12819
12874
|
setUpdate: (update) => set({ update }),
|
|
12820
12875
|
setHistory: (history) => set({ history }),
|
|
12821
12876
|
setToast: (toast) => set({ toast }),
|
|
12822
|
-
setImage: (image) => set({ image }),
|
|
12877
|
+
setImage: (image) => set(patchActive({ image })),
|
|
12823
12878
|
setSurround: (surround) => set({ surround }),
|
|
12824
|
-
setViewMode: (viewMode) => set({ viewMode }),
|
|
12879
|
+
setViewMode: (viewMode) => set(patchActive({ viewMode })),
|
|
12825
12880
|
// No `agentHighlight: null` here, unlike setPreset: hiding a pane does not
|
|
12826
12881
|
// re-raster the target, so the highlight still marks the pixels it marked.
|
|
12827
12882
|
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((
|
|
12883
|
+
setFitScale: (fitScale2) => set(patchActive({ fitScale: fitScale2 })),
|
|
12884
|
+
requestAgentPan: (p) => set(patchActiveWith((t) => ({ agentPan: { ...p, seq: (t.agentPan?.seq ?? 0) + 1 } }))),
|
|
12885
|
+
clearAgentPan: () => set(patchActive({ agentPan: null })),
|
|
12886
|
+
showAgentHighlight: (h) => set(patchActiveWith((t) => ({ agentHighlight: { ...h, seq: (t.agentHighlight?.seq ?? 0) + 1 } }))),
|
|
12887
|
+
clearAgentHighlight: (seq) => set(patchActiveWith((t) => seq === void 0 || t.agentHighlight?.seq === seq ? { agentHighlight: null } : null)),
|
|
12833
12888
|
// Spec §7: leaving image mode restores the URL that was showing before.
|
|
12834
12889
|
// Either direction swaps what the target pane shows, so a highlight over
|
|
12835
12890
|
// the old content is dropped with it.
|
|
12836
12891
|
setMode: (mode) => set(
|
|
12837
|
-
(
|
|
12838
|
-
|
|
12892
|
+
patchActiveWith(
|
|
12893
|
+
(t) => mode === t.mode ? null : mode === "image" ? { mode, lastUrl: t.url, agentHighlight: null } : { mode, url: t.lastUrl, image: null, agentHighlight: null }
|
|
12894
|
+
)
|
|
12895
|
+
),
|
|
12896
|
+
syncTabs: (snap) => set((s) => {
|
|
12897
|
+
if (snap.tabs.length === 0) return {};
|
|
12898
|
+
const tabs = {};
|
|
12899
|
+
for (const info of snap.tabs) {
|
|
12900
|
+
const existing = s.tabs[info.id];
|
|
12901
|
+
tabs[info.id] = existing ? (
|
|
12902
|
+
// url and title are main's to know — it is what every tab's panes
|
|
12903
|
+
// report to — so the snapshot is authoritative for those two and
|
|
12904
|
+
// for nothing else.
|
|
12905
|
+
existing.url === info.url && existing.title === info.title ? existing : { ...existing, url: info.url, title: info.title }
|
|
12906
|
+
) : (
|
|
12907
|
+
// A tab the renderer has never seen. Its screen comes from the
|
|
12908
|
+
// snapshot rather than from `blankTab`'s defaults, because main
|
|
12909
|
+
// may have restored it from disk with a preset chosen in a
|
|
12910
|
+
// previous launch — and for a tab genuinely opened just now, the
|
|
12911
|
+
// session's own defaults are those same defaults.
|
|
12912
|
+
{
|
|
12913
|
+
...blankTab(),
|
|
12914
|
+
url: info.url,
|
|
12915
|
+
title: info.title,
|
|
12916
|
+
presetId: info.presetId,
|
|
12917
|
+
profileId: info.profileId
|
|
12918
|
+
}
|
|
12919
|
+
);
|
|
12920
|
+
}
|
|
12921
|
+
const tabOrder = snap.tabs.map((t) => t.id);
|
|
12922
|
+
return { tabs, tabOrder, activeId: tabs[snap.activeId] ? snap.activeId : tabOrder[0] };
|
|
12923
|
+
}),
|
|
12924
|
+
addTab: (id) => {
|
|
12925
|
+
const s = get();
|
|
12926
|
+
if (!canAddTab(s.tabOrder.length, s.settings.maxTabs)) return null;
|
|
12927
|
+
const next = id ?? newTabId();
|
|
12928
|
+
if (s.tabs[next]) {
|
|
12929
|
+
set({ activeId: next });
|
|
12930
|
+
return next;
|
|
12931
|
+
}
|
|
12932
|
+
set({ tabs: { ...s.tabs, [next]: blankTab() }, tabOrder: [...s.tabOrder, next], activeId: next });
|
|
12933
|
+
return next;
|
|
12934
|
+
},
|
|
12935
|
+
closeTab: (id) => set((s) => {
|
|
12936
|
+
if (!s.tabs[id]) return {};
|
|
12937
|
+
const result = closeTab(
|
|
12938
|
+
s.tabOrder.map((tabId) => ({ id: tabId })),
|
|
12939
|
+
id,
|
|
12940
|
+
s.activeId
|
|
12941
|
+
);
|
|
12942
|
+
if (result.activeId === null) {
|
|
12943
|
+
const fresh = newTabId();
|
|
12944
|
+
return { tabs: { [fresh]: blankTab() }, tabOrder: [fresh], activeId: fresh };
|
|
12945
|
+
}
|
|
12946
|
+
const tabs = { ...s.tabs };
|
|
12947
|
+
delete tabs[id];
|
|
12948
|
+
return { tabs, tabOrder: result.tabs.map((t) => t.id), activeId: result.activeId };
|
|
12949
|
+
}),
|
|
12950
|
+
activateTab: (id) => set((s) => s.tabs[id] ? { activeId: id } : {})
|
|
12839
12951
|
}));
|
|
12952
|
+
function selectTab(s) {
|
|
12953
|
+
return s.tabs[s.activeId];
|
|
12954
|
+
}
|
|
12840
12955
|
function selectScreen(s) {
|
|
12841
|
-
const
|
|
12956
|
+
const tab = selectTab(s);
|
|
12957
|
+
const preset = SCREEN_PRESETS.find((p) => p.id === tab.presetId);
|
|
12842
12958
|
return preset ? {
|
|
12843
12959
|
width: preset.width,
|
|
12844
12960
|
height: preset.height,
|
|
12845
12961
|
diagonalInches: preset.diagonalInches,
|
|
12846
12962
|
deviceScaleFactor: preset.deviceScaleFactor
|
|
12847
|
-
} :
|
|
12963
|
+
} : tab.custom;
|
|
12848
12964
|
}
|
|
12849
12965
|
function selectDeviceScaleFactor(s) {
|
|
12850
12966
|
return selectScreen(s).deviceScaleFactor ?? 1;
|
|
@@ -12864,7 +12980,7 @@ function calibratedScale(s) {
|
|
|
12864
12980
|
diagonalInches: s.settings.hostDiagonalInches,
|
|
12865
12981
|
scaleFactor: s.host.scaleFactor
|
|
12866
12982
|
};
|
|
12867
|
-
const scale = computeScale(host, screen, s.pixelExact);
|
|
12983
|
+
const scale = computeScale(host, screen, selectTab(s).pixelExact);
|
|
12868
12984
|
return Number.isFinite(scale) && scale > 0 ? scale : null;
|
|
12869
12985
|
}
|
|
12870
12986
|
function selectScale(s) {
|
|
@@ -12877,13 +12993,15 @@ function selectHostNits(s) {
|
|
|
12877
12993
|
return s.settings.hostNits > 0 ? s.settings.hostNits : DEFAULT_SETTINGS.hostNits;
|
|
12878
12994
|
}
|
|
12879
12995
|
function selectProfile(s) {
|
|
12880
|
-
|
|
12996
|
+
const tab = selectTab(s);
|
|
12997
|
+
return tab.profileOverride ?? findProfile(tab.profileId);
|
|
12881
12998
|
}
|
|
12882
12999
|
function selectPanelParams(s) {
|
|
12883
13000
|
return profileToParams(selectProfile(s), selectHostNits(s));
|
|
12884
13001
|
}
|
|
12885
13002
|
function selectUrlBarText(s) {
|
|
12886
|
-
|
|
13003
|
+
const tab = selectTab(s);
|
|
13004
|
+
return tab.mode === "image" ? tab.image?.name ?? "" : tab.url;
|
|
12887
13005
|
}
|
|
12888
13006
|
const SCALES = [1, 2, 3];
|
|
12889
13007
|
const DEFAULT_SCALE = 2;
|
|
@@ -13060,10 +13178,10 @@ function TargetFooter() {
|
|
|
13060
13178
|
const params = useStore(useShallow(selectPanelParams));
|
|
13061
13179
|
const scale = useStore(selectScale);
|
|
13062
13180
|
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);
|
|
13181
|
+
const image = useStore((s) => selectTab(s).image);
|
|
13182
|
+
const mode = useStore((s) => selectTab(s).mode);
|
|
13183
|
+
const viewMode = useStore((s) => selectTab(s).viewMode);
|
|
13184
|
+
const fitScale2 = useStore((s) => selectTab(s).fitScale);
|
|
13067
13185
|
const dsf = useStore(selectDeviceScaleFactor);
|
|
13068
13186
|
const size = mode === "image" && image ? `${image.width}×${image.height}` : `${viewport.width}×${viewport.height}${dsf > 1 ? ` @${dsf}x` : ""}`;
|
|
13069
13187
|
const depth = params.levels <= 63 ? "6-bit" : "8-bit";
|
|
@@ -13407,7 +13525,7 @@ function SettingsPanel() {
|
|
|
13407
13525
|
const settings = useStore(useShallow((s) => s.settings));
|
|
13408
13526
|
const update = useStore((s) => s.update);
|
|
13409
13527
|
const history = useStore((s) => s.history);
|
|
13410
|
-
const custom = useStore(useShallow((s) => s.custom));
|
|
13528
|
+
const custom = useStore(useShallow((s) => selectTab(s).custom));
|
|
13411
13529
|
const viewport = useStore(useShallow(selectViewport));
|
|
13412
13530
|
const scale = useStore(selectScale);
|
|
13413
13531
|
const fallback = useStore(selectScaleIsFallback);
|
|
@@ -13564,6 +13682,24 @@ function SettingsPanel() {
|
|
|
13564
13682
|
] }),
|
|
13565
13683
|
/* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", className: "check-now", onClick: () => void window.obsrv.checkUpdate(), children: "Check now" }),
|
|
13566
13684
|
/* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "muted", children: "One unauthenticated request to GitHub, at most once a day. No identifiers are sent." }),
|
|
13685
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "Tabs" }),
|
|
13686
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
13687
|
+
NumberField,
|
|
13688
|
+
{
|
|
13689
|
+
className: "max-tabs",
|
|
13690
|
+
label: "Maximum tabs",
|
|
13691
|
+
unit: "tabs",
|
|
13692
|
+
value: settings.maxTabs,
|
|
13693
|
+
min: MAX_TABS_MIN,
|
|
13694
|
+
step: 1,
|
|
13695
|
+
onCommit: (v) => commit({
|
|
13696
|
+
...useStore.getState().settings,
|
|
13697
|
+
maxTabs: Math.min(MAX_TABS_MAX, Math.max(MAX_TABS_MIN, Math.round(v)))
|
|
13698
|
+
}),
|
|
13699
|
+
onInvalid: setHostError
|
|
13700
|
+
}
|
|
13701
|
+
),
|
|
13702
|
+
/* @__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
13703
|
/* @__PURE__ */ jsxRuntimeExports.jsx("h2", { children: "History" }),
|
|
13568
13704
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "control inline record-history-toggle", children: [
|
|
13569
13705
|
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
@@ -14036,10 +14172,11 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14036
14172
|
const params = useStore(useShallow(selectPanelParams));
|
|
14037
14173
|
const dsf = useStore(selectDeviceScaleFactor);
|
|
14038
14174
|
const requestedScale = useStore(selectScale);
|
|
14039
|
-
const mode = useStore((s) => s.mode);
|
|
14040
|
-
const viewMode = useStore((s) => s.viewMode);
|
|
14175
|
+
const mode = useStore((s) => selectTab(s).mode);
|
|
14176
|
+
const viewMode = useStore((s) => selectTab(s).viewMode);
|
|
14041
14177
|
const setViewMode = useStore((s) => s.setViewMode);
|
|
14042
14178
|
const setFitScale = useStore((s) => s.setFitScale);
|
|
14179
|
+
const activeId = useStore((s) => s.activeId);
|
|
14043
14180
|
const [stalled, setStalled] = reactExports.useState(false);
|
|
14044
14181
|
const stallTimer = reactExports.useRef(0);
|
|
14045
14182
|
const armedOnce = reactExports.useRef(false);
|
|
@@ -14071,7 +14208,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14071
14208
|
const smooth = fit || scale < 1;
|
|
14072
14209
|
reactExports.useEffect(() => {
|
|
14073
14210
|
setFitScale(fit ? scale : null);
|
|
14074
|
-
}, [fit, scale, setFitScale]);
|
|
14211
|
+
}, [fit, scale, activeId, setFitScale]);
|
|
14075
14212
|
reactExports.useEffect(() => () => setFitScale(null), [setFitScale]);
|
|
14076
14213
|
const draw = reactExports.useRef({ scale, params, smooth, dsf });
|
|
14077
14214
|
const imageRef = reactExports.useRef(imageFrame);
|
|
@@ -14110,7 +14247,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14110
14247
|
offFrame = window.obsrv.onFrame((m) => {
|
|
14111
14248
|
disarm();
|
|
14112
14249
|
if (!gl) return;
|
|
14113
|
-
if (useStore.getState().mode !== "url") return;
|
|
14250
|
+
if (selectTab(useStore.getState()).mode !== "url") return;
|
|
14114
14251
|
gl.resizeSource(m.frameWidth, m.frameHeight);
|
|
14115
14252
|
gl.uploadSlice(m.frame);
|
|
14116
14253
|
schedule();
|
|
@@ -14138,7 +14275,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14138
14275
|
const onWheel = (e) => {
|
|
14139
14276
|
if (e.altKey) {
|
|
14140
14277
|
e.preventDefault();
|
|
14141
|
-
if (useStore.getState().viewMode !== "1:1") return;
|
|
14278
|
+
if (selectTab(useStore.getState()).viewMode !== "1:1") return;
|
|
14142
14279
|
const body = canvas.closest(".pane-body");
|
|
14143
14280
|
if (body instanceof HTMLElement) {
|
|
14144
14281
|
body.scrollLeft += e.deltaX;
|
|
@@ -14146,7 +14283,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14146
14283
|
}
|
|
14147
14284
|
return;
|
|
14148
14285
|
}
|
|
14149
|
-
if (useStore.getState().mode !== "url") return;
|
|
14286
|
+
if (selectTab(useStore.getState()).mode !== "url") return;
|
|
14150
14287
|
e.preventDefault();
|
|
14151
14288
|
const r = canvas.getBoundingClientRect();
|
|
14152
14289
|
const cssPerTarget = draw.current.scale * draw.current.dsf / (window.devicePixelRatio || 1);
|
|
@@ -14155,7 +14292,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14155
14292
|
};
|
|
14156
14293
|
canvas.addEventListener("wheel", onWheel, { passive: false });
|
|
14157
14294
|
const onWindowUp = (e) => {
|
|
14158
|
-
if (useStore.getState().mode !== "url") return;
|
|
14295
|
+
if (selectTab(useStore.getState()).mode !== "url") return;
|
|
14159
14296
|
if (panRef.current || e.button === 1) return;
|
|
14160
14297
|
if (e.altKey && !forwardDrag.current) return;
|
|
14161
14298
|
if (e.target === canvas) return;
|
|
@@ -14178,8 +14315,10 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14178
14315
|
};
|
|
14179
14316
|
}, [onFatal, disarm]);
|
|
14180
14317
|
reactExports.useEffect(
|
|
14181
|
-
() => window.obsrv.onTargetNavigating(() => {
|
|
14182
|
-
|
|
14318
|
+
() => window.obsrv.onTargetNavigating(({ tabId }) => {
|
|
14319
|
+
const s = useStore.getState();
|
|
14320
|
+
if (tabId !== s.activeId) return;
|
|
14321
|
+
if (selectTab(s).mode === "url") arm();
|
|
14183
14322
|
}),
|
|
14184
14323
|
[arm]
|
|
14185
14324
|
);
|
|
@@ -14293,7 +14432,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14293
14432
|
);
|
|
14294
14433
|
setViewMode("1:1");
|
|
14295
14434
|
};
|
|
14296
|
-
const agentPan = useStore((s) => s.agentPan);
|
|
14435
|
+
const agentPan = useStore((s) => selectTab(s).agentPan);
|
|
14297
14436
|
const clearAgentPan = useStore((s) => s.clearAgentPan);
|
|
14298
14437
|
reactExports.useEffect(() => {
|
|
14299
14438
|
if (!agentPan) return;
|
|
@@ -14310,7 +14449,7 @@ function TargetCanvas({ onFatal, imageFrame }) {
|
|
|
14310
14449
|
body.scrollTop = jump.top;
|
|
14311
14450
|
}
|
|
14312
14451
|
}, [agentPan]);
|
|
14313
|
-
const agentHighlight = useStore((s) => s.agentHighlight);
|
|
14452
|
+
const agentHighlight = useStore((s) => selectTab(s).agentHighlight);
|
|
14314
14453
|
const clearAgentHighlight = useStore((s) => s.clearAgentHighlight);
|
|
14315
14454
|
reactExports.useEffect(() => {
|
|
14316
14455
|
if (!agentHighlight) return;
|
|
@@ -14415,6 +14554,23 @@ function matchHistory(entries, query, limit = HISTORY_SUGGESTIONS) {
|
|
|
14415
14554
|
const needle = query.trim().toLowerCase();
|
|
14416
14555
|
return entries.filter((e) => e.url.toLowerCase().includes(needle)).sort(byRank).slice(0, Math.max(0, limit));
|
|
14417
14556
|
}
|
|
14557
|
+
const AGENT_ACTIVITY_MS = 3e3;
|
|
14558
|
+
function useAgentActivity() {
|
|
14559
|
+
const [active, setActive] = reactExports.useState(false);
|
|
14560
|
+
reactExports.useEffect(() => {
|
|
14561
|
+
let timer;
|
|
14562
|
+
const off = window.obsrv.onAgentActivity(() => {
|
|
14563
|
+
setActive(true);
|
|
14564
|
+
clearTimeout(timer);
|
|
14565
|
+
timer = setTimeout(() => setActive(false), AGENT_ACTIVITY_MS);
|
|
14566
|
+
});
|
|
14567
|
+
return () => {
|
|
14568
|
+
clearTimeout(timer);
|
|
14569
|
+
off();
|
|
14570
|
+
};
|
|
14571
|
+
}, []);
|
|
14572
|
+
return active;
|
|
14573
|
+
}
|
|
14418
14574
|
/**
|
|
14419
14575
|
* @license lucide-react v1.34.0 - ISC
|
|
14420
14576
|
*
|
|
@@ -14542,42 +14698,53 @@ const createLucideIcon = (iconName, iconNode) => {
|
|
|
14542
14698
|
* This source code is licensed under the ISC license.
|
|
14543
14699
|
* See the LICENSE file in the root directory of this source tree.
|
|
14544
14700
|
*/
|
|
14545
|
-
const __iconNode$
|
|
14701
|
+
const __iconNode$8 = [
|
|
14546
14702
|
["path", { d: "m12 19-7-7 7-7", key: "1l729n" }],
|
|
14547
14703
|
["path", { d: "M19 12H5", key: "x3x0zl" }]
|
|
14548
14704
|
];
|
|
14549
|
-
const ArrowLeft = createLucideIcon("arrow-left", __iconNode$
|
|
14705
|
+
const ArrowLeft = createLucideIcon("arrow-left", __iconNode$8);
|
|
14550
14706
|
/**
|
|
14551
14707
|
* @license lucide-react v1.34.0 - ISC
|
|
14552
14708
|
*
|
|
14553
14709
|
* This source code is licensed under the ISC license.
|
|
14554
14710
|
* See the LICENSE file in the root directory of this source tree.
|
|
14555
14711
|
*/
|
|
14556
|
-
const __iconNode$
|
|
14712
|
+
const __iconNode$7 = [
|
|
14557
14713
|
["path", { d: "M5 12h14", key: "1ays0h" }],
|
|
14558
14714
|
["path", { d: "m12 5 7 7-7 7", key: "xquz4c" }]
|
|
14559
14715
|
];
|
|
14560
|
-
const ArrowRight = createLucideIcon("arrow-right", __iconNode$
|
|
14716
|
+
const ArrowRight = createLucideIcon("arrow-right", __iconNode$7);
|
|
14561
14717
|
/**
|
|
14562
14718
|
* @license lucide-react v1.34.0 - ISC
|
|
14563
14719
|
*
|
|
14564
14720
|
* This source code is licensed under the ISC license.
|
|
14565
14721
|
* See the LICENSE file in the root directory of this source tree.
|
|
14566
14722
|
*/
|
|
14567
|
-
const __iconNode$
|
|
14568
|
-
const ChevronDown = createLucideIcon("chevron-down", __iconNode$
|
|
14723
|
+
const __iconNode$6 = [["path", { d: "m6 9 6 6 6-6", key: "qrunsl" }]];
|
|
14724
|
+
const ChevronDown = createLucideIcon("chevron-down", __iconNode$6);
|
|
14569
14725
|
/**
|
|
14570
14726
|
* @license lucide-react v1.34.0 - ISC
|
|
14571
14727
|
*
|
|
14572
14728
|
* This source code is licensed under the ISC license.
|
|
14573
14729
|
* See the LICENSE file in the root directory of this source tree.
|
|
14574
14730
|
*/
|
|
14575
|
-
const __iconNode$
|
|
14731
|
+
const __iconNode$5 = [
|
|
14576
14732
|
["circle", { cx: "12", cy: "12", r: "1", key: "41hilf" }],
|
|
14577
14733
|
["circle", { cx: "12", cy: "5", r: "1", key: "gxeob9" }],
|
|
14578
14734
|
["circle", { cx: "12", cy: "19", r: "1", key: "lyex9k" }]
|
|
14579
14735
|
];
|
|
14580
|
-
const EllipsisVertical = createLucideIcon("ellipsis-vertical", __iconNode$
|
|
14736
|
+
const EllipsisVertical = createLucideIcon("ellipsis-vertical", __iconNode$5);
|
|
14737
|
+
/**
|
|
14738
|
+
* @license lucide-react v1.34.0 - ISC
|
|
14739
|
+
*
|
|
14740
|
+
* This source code is licensed under the ISC license.
|
|
14741
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
14742
|
+
*/
|
|
14743
|
+
const __iconNode$4 = [
|
|
14744
|
+
["path", { d: "M5 12h14", key: "1ays0h" }],
|
|
14745
|
+
["path", { d: "M12 5v14", key: "s699le" }]
|
|
14746
|
+
];
|
|
14747
|
+
const Plus = createLucideIcon("plus", __iconNode$4);
|
|
14581
14748
|
/**
|
|
14582
14749
|
* @license lucide-react v1.34.0 - ISC
|
|
14583
14750
|
*
|
|
@@ -14643,7 +14810,8 @@ const ICONS = {
|
|
|
14643
14810
|
close: X,
|
|
14644
14811
|
sliders: SlidersHorizontal,
|
|
14645
14812
|
gear: Settings,
|
|
14646
|
-
chevron: ChevronDown
|
|
14813
|
+
chevron: ChevronDown,
|
|
14814
|
+
plus: Plus
|
|
14647
14815
|
};
|
|
14648
14816
|
function Icon({ name, size = 16 }) {
|
|
14649
14817
|
const Glyph = ICONS[name];
|
|
@@ -14714,6 +14882,77 @@ function Segmented({
|
|
|
14714
14882
|
o.id
|
|
14715
14883
|
)) });
|
|
14716
14884
|
}
|
|
14885
|
+
function TabBar() {
|
|
14886
|
+
const tabOrder = useStore(useShallow((s) => s.tabOrder));
|
|
14887
|
+
const activeId = useStore((s) => s.activeId);
|
|
14888
|
+
const maxTabs = useStore((s) => s.settings.maxTabs);
|
|
14889
|
+
const driving = useStore((s) => s.settings.agentControl);
|
|
14890
|
+
const agentActive = useAgentActivity();
|
|
14891
|
+
const canAdd = canAddTab(tabOrder.length, maxTabs);
|
|
14892
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "chrome-row chrome-tabs", children: [
|
|
14893
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tabs", role: "tablist", "aria-label": "Open tabs", children: tabOrder.map((id) => /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14894
|
+
Tab,
|
|
14895
|
+
{
|
|
14896
|
+
id,
|
|
14897
|
+
active: id === activeId,
|
|
14898
|
+
driven: driving && id === activeId,
|
|
14899
|
+
busy: agentActive
|
|
14900
|
+
},
|
|
14901
|
+
id
|
|
14902
|
+
)) }),
|
|
14903
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14904
|
+
"button",
|
|
14905
|
+
{
|
|
14906
|
+
className: "tab-new",
|
|
14907
|
+
type: "button",
|
|
14908
|
+
"aria-label": "New tab",
|
|
14909
|
+
disabled: !canAdd,
|
|
14910
|
+
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.`,
|
|
14911
|
+
onClick: () => {
|
|
14912
|
+
void window.obsrv.addTab();
|
|
14913
|
+
},
|
|
14914
|
+
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { name: "plus", size: 14 })
|
|
14915
|
+
}
|
|
14916
|
+
)
|
|
14917
|
+
] });
|
|
14918
|
+
}
|
|
14919
|
+
function Tab({
|
|
14920
|
+
id,
|
|
14921
|
+
active,
|
|
14922
|
+
driven,
|
|
14923
|
+
busy
|
|
14924
|
+
}) {
|
|
14925
|
+
const label = useStore((s) => {
|
|
14926
|
+
const t = s.tabs[id];
|
|
14927
|
+
return t ? tabTitle(t.url, t.title) : "";
|
|
14928
|
+
});
|
|
14929
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `tab${driven ? " driven" : ""}${driven && busy ? " busy" : ""}`, children: [
|
|
14930
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14931
|
+
"button",
|
|
14932
|
+
{
|
|
14933
|
+
className: "tab-label",
|
|
14934
|
+
type: "button",
|
|
14935
|
+
role: "tab",
|
|
14936
|
+
"aria-selected": active,
|
|
14937
|
+
title: driven ? `${label}
|
|
14938
|
+
Agent control is driving this tab` : label,
|
|
14939
|
+
onClick: () => window.obsrv.activateTab(id),
|
|
14940
|
+
children: label
|
|
14941
|
+
}
|
|
14942
|
+
),
|
|
14943
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14944
|
+
"button",
|
|
14945
|
+
{
|
|
14946
|
+
className: "tab-close",
|
|
14947
|
+
type: "button",
|
|
14948
|
+
"aria-label": `Close ${label}`,
|
|
14949
|
+
title: "Close tab",
|
|
14950
|
+
onClick: () => window.obsrv.closeTab(id),
|
|
14951
|
+
children: /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { name: "close", size: 12 })
|
|
14952
|
+
}
|
|
14953
|
+
)
|
|
14954
|
+
] });
|
|
14955
|
+
}
|
|
14717
14956
|
function Select({ className, value, label, ariaLabel, onChange, children }) {
|
|
14718
14957
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "select-shell", children: [
|
|
14719
14958
|
/* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "select-label", "aria-hidden": "true", children: label }),
|
|
@@ -14744,12 +14983,12 @@ const PANES = [
|
|
|
14744
14983
|
{ id: "target", label: "Target", title: "The target render alone, full width" }
|
|
14745
14984
|
];
|
|
14746
14985
|
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);
|
|
14986
|
+
const mode = useStore((s) => selectTab(s).mode);
|
|
14987
|
+
const presetId = useStore((s) => selectTab(s).presetId);
|
|
14988
|
+
const profileId = useStore((s) => selectTab(s).profileId);
|
|
14989
|
+
const pixelExact = useStore((s) => selectTab(s).pixelExact);
|
|
14990
|
+
const error = useStore((s) => selectTab(s).error);
|
|
14991
|
+
const loading = useStore((s) => selectTab(s).targetLoading);
|
|
14753
14992
|
const barText = useStore(selectUrlBarText);
|
|
14754
14993
|
const viewport = useStore(useShallow(selectViewport));
|
|
14755
14994
|
const setUrl = useStore((s) => s.setUrl);
|
|
@@ -14761,7 +15000,7 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
14761
15000
|
const surround = useStore((s) => s.surround);
|
|
14762
15001
|
const update = useStore((s) => s.update);
|
|
14763
15002
|
const setSurround = useStore((s) => s.setSurround);
|
|
14764
|
-
const viewMode = useStore((s) => s.viewMode);
|
|
15003
|
+
const viewMode = useStore((s) => selectTab(s).viewMode);
|
|
14765
15004
|
const setViewMode = useStore((s) => s.setViewMode);
|
|
14766
15005
|
const panes = useStore((s) => s.panes);
|
|
14767
15006
|
const setPanes = useStore((s) => s.setPanes);
|
|
@@ -14779,19 +15018,7 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
14779
15018
|
setHighlight(-1);
|
|
14780
15019
|
};
|
|
14781
15020
|
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
|
-
}, []);
|
|
15021
|
+
const agentActive = useAgentActivity();
|
|
14795
15022
|
const toggleAgent = () => {
|
|
14796
15023
|
const current = useStore.getState().settings;
|
|
14797
15024
|
const next = { ...current, agentControl: !current.agentControl };
|
|
@@ -14858,6 +15085,7 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
14858
15085
|
const presetLabel = SCREEN_PRESETS.find((p) => p.id === presetId)?.label ?? "Custom";
|
|
14859
15086
|
const profileLabel = PANEL_PROFILES.find((p) => p.id === profileId)?.label ?? profileId;
|
|
14860
15087
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "chrome", children: [
|
|
15088
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(TabBar, {}),
|
|
14861
15089
|
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "chrome-row chrome-browse", children: [
|
|
14862
15090
|
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
14863
15091
|
"button",
|
|
@@ -15089,29 +15317,34 @@ function Toolbar({ drawer, onTogglePanel, onToggleSettings }) {
|
|
|
15089
15317
|
function App() {
|
|
15090
15318
|
const [fatal, setFatal] = reactExports.useState(null);
|
|
15091
15319
|
const [drawer, setDrawer] = reactExports.useState("none");
|
|
15092
|
-
const [
|
|
15320
|
+
const [images, setImages] = reactExports.useState({});
|
|
15093
15321
|
const dropToken = reactExports.useRef(0);
|
|
15094
15322
|
const targetPaneRef = reactExports.useRef(null);
|
|
15095
15323
|
const [targetBounds, setTargetBounds] = reactExports.useState(null);
|
|
15096
15324
|
const [canvasBounds, setCanvasBounds] = reactExports.useState(null);
|
|
15325
|
+
const [tabsKnown, setTabsKnown] = reactExports.useState(false);
|
|
15097
15326
|
const toggle = (which) => () => setDrawer((d) => d === which ? "none" : which);
|
|
15098
15327
|
const setHost = useStore((s) => s.setHost);
|
|
15099
15328
|
const setSettings = useStore((s) => s.setSettings);
|
|
15100
|
-
const
|
|
15101
|
-
const
|
|
15102
|
-
const
|
|
15329
|
+
const setTabUrl = useStore((s) => s.setTabUrl);
|
|
15330
|
+
const setTabTitle = useStore((s) => s.setTabTitle);
|
|
15331
|
+
const setTabError = useStore((s) => s.setTabError);
|
|
15332
|
+
const setTabLoading = useStore((s) => s.setTabLoading);
|
|
15333
|
+
const syncTabs = useStore((s) => s.syncTabs);
|
|
15103
15334
|
const setUpdate = useStore((s) => s.setUpdate);
|
|
15104
15335
|
const setHistory = useStore((s) => s.setHistory);
|
|
15105
15336
|
const setImageMeta = useStore((s) => s.setImage);
|
|
15106
15337
|
const setMode = useStore((s) => s.setMode);
|
|
15107
15338
|
const setToast = useStore((s) => s.setToast);
|
|
15108
|
-
const mode = useStore((s) => s.mode);
|
|
15339
|
+
const mode = useStore((s) => selectTab(s).mode);
|
|
15109
15340
|
const surround = useStore((s) => s.surround);
|
|
15110
15341
|
const viewport = useStore(useShallow(selectViewport));
|
|
15111
15342
|
const deviceScaleFactor = useStore(selectDeviceScaleFactor);
|
|
15112
|
-
const presetId = useStore((s) => s.presetId);
|
|
15113
|
-
const profileId = useStore((s) => s.profileId);
|
|
15114
|
-
const viewMode = useStore((s) => s.viewMode);
|
|
15343
|
+
const presetId = useStore((s) => selectTab(s).presetId);
|
|
15344
|
+
const profileId = useStore((s) => selectTab(s).profileId);
|
|
15345
|
+
const viewMode = useStore((s) => selectTab(s).viewMode);
|
|
15346
|
+
const activeId = useStore((s) => s.activeId);
|
|
15347
|
+
const tabOrder = useStore((s) => s.tabOrder);
|
|
15115
15348
|
const panes = useStore((s) => s.panes);
|
|
15116
15349
|
const split = useStore((s) => s.settings.split);
|
|
15117
15350
|
reactExports.useEffect(() => {
|
|
@@ -15119,28 +15352,36 @@ function App() {
|
|
|
15119
15352
|
window.obsrv.getSettings().then(setSettings, (e) => console.warn("obsrv: getSettings failed", e));
|
|
15120
15353
|
window.obsrv.getUpdate().then(setUpdate, (e) => console.warn("obsrv: getUpdate failed", e));
|
|
15121
15354
|
window.obsrv.getHistory().then(setHistory, (e) => console.warn("obsrv: getHistory failed", e));
|
|
15355
|
+
window.obsrv.getTabs().then(syncTabs, (e) => console.warn("obsrv: getTabs failed", e)).finally(() => setTabsKnown(true));
|
|
15122
15356
|
const offs = [
|
|
15123
15357
|
window.obsrv.onHostChanged(setHost),
|
|
15358
|
+
// Every one of these names its tab, so a background tab keeps its own
|
|
15359
|
+
// strip entry current without rewriting the address bar of the tab in
|
|
15360
|
+
// front — which is what an unnamed report from a background tab did, and
|
|
15361
|
+
// why main used to gate them on the tab being in front at all.
|
|
15362
|
+
//
|
|
15124
15363
|
// 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
|
|
15364
|
+
// that tab's last load failure, and only a committed one: Chromium
|
|
15365
|
+
// commits its error page without emitting `did-navigate`, so a failed
|
|
15366
|
+
// load reports no URL at all (measured on Electron 43; a bad host, a
|
|
15367
|
+
// refused connection, a missing file and a Back onto an error page all
|
|
15368
|
+
// fire `did-fail-load` alone). The badge therefore lands last by having
|
|
15130
15369
|
// nothing race it.
|
|
15131
|
-
window.obsrv.onUrlChanged((url) => {
|
|
15132
|
-
|
|
15133
|
-
|
|
15370
|
+
window.obsrv.onUrlChanged(({ tabId, url }) => {
|
|
15371
|
+
setTabError(tabId, null);
|
|
15372
|
+
setTabUrl(tabId, url);
|
|
15134
15373
|
}),
|
|
15135
|
-
window.obsrv.
|
|
15136
|
-
window.obsrv.
|
|
15374
|
+
window.obsrv.onTitleChanged(({ tabId, title }) => setTabTitle(tabId, title)),
|
|
15375
|
+
window.obsrv.onLoadError(({ tabId, error }) => setTabError(tabId, error)),
|
|
15376
|
+
window.obsrv.onTargetLoading(({ tabId, loading }) => setTabLoading(tabId, loading)),
|
|
15377
|
+
window.obsrv.onTabsChanged(syncTabs),
|
|
15137
15378
|
window.obsrv.onUpdateStatus(setUpdate),
|
|
15138
15379
|
window.obsrv.onHistoryChanged(setHistory)
|
|
15139
15380
|
];
|
|
15140
15381
|
return () => {
|
|
15141
15382
|
for (const off of offs) off();
|
|
15142
15383
|
};
|
|
15143
|
-
}, [setHost, setSettings,
|
|
15384
|
+
}, [setHost, setSettings, setTabUrl, setTabTitle, setTabError, setTabLoading, syncTabs, setUpdate, setHistory]);
|
|
15144
15385
|
reactExports.useEffect(() => {
|
|
15145
15386
|
void window.obsrv.setViewport(viewport.width, viewport.height, deviceScaleFactor);
|
|
15146
15387
|
}, [viewport.width, viewport.height, deviceScaleFactor]);
|
|
@@ -15180,8 +15421,9 @@ function App() {
|
|
|
15180
15421
|
};
|
|
15181
15422
|
}, []);
|
|
15182
15423
|
reactExports.useEffect(() => {
|
|
15183
|
-
|
|
15184
|
-
|
|
15424
|
+
if (!tabsKnown) return;
|
|
15425
|
+
window.obsrv.reportUiState({ tabId: activeId, presetId, profileId, viewMode, panes, mode, targetBounds, canvasBounds });
|
|
15426
|
+
}, [tabsKnown, activeId, presetId, profileId, viewMode, panes, mode, targetBounds, canvasBounds]);
|
|
15185
15427
|
reactExports.useEffect(() => {
|
|
15186
15428
|
return window.obsrv.onAgentApply((patch) => {
|
|
15187
15429
|
const s = useStore.getState();
|
|
@@ -15197,21 +15439,24 @@ function App() {
|
|
|
15197
15439
|
reactExports.useEffect(() => {
|
|
15198
15440
|
document.documentElement.dataset.surround = surround;
|
|
15199
15441
|
}, [surround]);
|
|
15442
|
+
const image = images[activeId] ?? null;
|
|
15200
15443
|
const onImage = async (file, exportScale) => {
|
|
15201
15444
|
const token = ++dropToken.current;
|
|
15445
|
+
const tabId = activeId;
|
|
15202
15446
|
try {
|
|
15203
15447
|
const limits = {
|
|
15204
15448
|
...DEFAULT_IMAGE_LIMITS,
|
|
15205
15449
|
maxDimension: Math.min(DEFAULT_IMAGE_LIMITS.maxDimension, probeMaxTextureSize())
|
|
15206
15450
|
};
|
|
15207
15451
|
const loaded = await loadImage(file, exportScale, limits);
|
|
15208
|
-
if (token !== dropToken.current) {
|
|
15452
|
+
if (token !== dropToken.current || useStore.getState().activeId !== tabId) {
|
|
15209
15453
|
URL.revokeObjectURL(loaded.objectUrl);
|
|
15210
15454
|
return;
|
|
15211
15455
|
}
|
|
15212
|
-
|
|
15213
|
-
|
|
15214
|
-
|
|
15456
|
+
setImages((previous) => {
|
|
15457
|
+
const stale = previous[tabId];
|
|
15458
|
+
if (stale) URL.revokeObjectURL(stale.objectUrl);
|
|
15459
|
+
return { ...previous, [tabId]: loaded };
|
|
15215
15460
|
});
|
|
15216
15461
|
setImageMeta({
|
|
15217
15462
|
name: file.name,
|
|
@@ -15230,11 +15475,28 @@ function App() {
|
|
|
15230
15475
|
};
|
|
15231
15476
|
reactExports.useEffect(() => {
|
|
15232
15477
|
if (mode === "image") return;
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
return
|
|
15478
|
+
setImages((previous) => {
|
|
15479
|
+
const going = previous[activeId];
|
|
15480
|
+
if (!going) return previous;
|
|
15481
|
+
URL.revokeObjectURL(going.objectUrl);
|
|
15482
|
+
const next = { ...previous };
|
|
15483
|
+
delete next[activeId];
|
|
15484
|
+
return next;
|
|
15236
15485
|
});
|
|
15237
|
-
}, [mode]);
|
|
15486
|
+
}, [mode, activeId]);
|
|
15487
|
+
reactExports.useEffect(() => {
|
|
15488
|
+
setImages((previous) => {
|
|
15489
|
+
const open = new Set(tabOrder);
|
|
15490
|
+
const gone = Object.keys(previous).filter((id) => !open.has(id));
|
|
15491
|
+
if (gone.length === 0) return previous;
|
|
15492
|
+
const next = { ...previous };
|
|
15493
|
+
for (const id of gone) {
|
|
15494
|
+
URL.revokeObjectURL(next[id].objectUrl);
|
|
15495
|
+
delete next[id];
|
|
15496
|
+
}
|
|
15497
|
+
return next;
|
|
15498
|
+
});
|
|
15499
|
+
}, [tabOrder]);
|
|
15238
15500
|
const imageFrame = reactExports.useMemo(() => {
|
|
15239
15501
|
if (mode !== "image" || !image) return null;
|
|
15240
15502
|
return {
|