Theseus: background tabs stop unless kept running; popups close on click-away
A tab you switch away from is frozen (page lifecycle "frozen": no JS, timers, network callbacks or media) one second later and thawed the moment it is shown again. Right-click a tab → "Keep running in background" exempts it (music, calls, dashboards); the strip marks it ▶. Dormant restored tabs and tabs waiting on a page dialog are never frozen. Settings › Performance › "Stop tabs in the background" (on by default) turns it off. Freezing uses the per-tab debugger applyFingerprint already keeps attached; Chromium only freezes hidden pages. The downloads, site-info and engine-picker popups close when focus moves elsewhere in the window or to another app; the toolbar click that caused that does not reopen them. Focus only moves into a popup while the window is active — focusing it from the background blurs the window and closed the popup it had just opened. Also fixes a bug in the lazy overlays: isLoading() is still true while did-finish-load is delivered, so a first show waited out the 4 s timeout before appearing. Finished loads are now recorded explicitly.
This commit is contained in:
parent
b531c89f82
commit
adcea19f8a
4 changed files with 108 additions and 4 deletions
|
|
@ -88,6 +88,7 @@
|
||||||
.tab .x { opacity: .5; cursor: pointer; padding: 0 2px; border-radius: 4px; }
|
.tab .x { opacity: .5; cursor: pointer; padding: 0 2px; border-radius: 4px; }
|
||||||
.tab .mute { font-size: 10px; opacity: .8; margin-right: 2px; }
|
.tab .mute { font-size: 10px; opacity: .8; margin-right: 2px; }
|
||||||
.tab .ask { font-size: 10px; margin-right: 2px; }
|
.tab .ask { font-size: 10px; margin-right: 2px; }
|
||||||
|
.tab .bg { font-size: 9px; opacity: .75; margin-right: 2px; }
|
||||||
/* Tab group visual: a colored dot before the title, plus a matching top
|
/* Tab group visual: a colored dot before the title, plus a matching top
|
||||||
accent stripe on the tab itself so a whole group reads as one cluster
|
accent stripe on the tab itself so a whole group reads as one cluster
|
||||||
even when tabs are next to each other. */
|
even when tabs are next to each other. */
|
||||||
|
|
@ -1579,7 +1580,7 @@
|
||||||
function makeTab(id) {
|
function makeTab(id) {
|
||||||
const el = document.createElement("div");
|
const el = document.createElement("div");
|
||||||
el.className = "tab"; el.dataset.id = String(id); el.dataset.key = "tab:" + id; el.draggable = true;
|
el.className = "tab"; el.dataset.id = String(id); el.dataset.key = "tab:" + id; el.draggable = true;
|
||||||
el.innerHTML = `<span class="ico"></span><span class="t"></span><span class="ask" title="This page is waiting for you" hidden>💬</span><span class="mute" title="Muted" hidden>🔇</span><span class="x" data-close="${id}">✕</span>`;
|
el.innerHTML = `<span class="ico"></span><span class="t"></span><span class="ask" title="This page is waiting for you" hidden>💬</span><span class="bg" title="Keeps running in the background" hidden>▶</span><span class="mute" title="Muted" hidden>🔇</span><span class="x" data-close="${id}">✕</span>`;
|
||||||
el.onclick = (e) => { if (e.target.dataset.close) T.closeTab(id); else T.switchTab(id); };
|
el.onclick = (e) => { if (e.target.dataset.close) T.closeTab(id); else T.switchTab(id); };
|
||||||
// Right-click → native OS menu popped from main.js (a DOM menu forced the
|
// Right-click → native OS menu popped from main.js (a DOM menu forced the
|
||||||
// chrome view taller and opened a gap under the toolbar).
|
// chrome view taller and opened a gap under the toolbar).
|
||||||
|
|
@ -1613,6 +1614,7 @@
|
||||||
if (label.textContent !== txt) label.textContent = txt;
|
if (label.textContent !== txt) label.textContent = txt;
|
||||||
el.querySelector(".mute").hidden = !t.muted;
|
el.querySelector(".mute").hidden = !t.muted;
|
||||||
el.querySelector(".ask").hidden = !t.asking;
|
el.querySelector(".ask").hidden = !t.asking;
|
||||||
|
el.querySelector(".bg").hidden = !t.keepRunning;
|
||||||
// Icon slot: spinner while loading, otherwise the favicon. The <img> is
|
// Icon slot: spinner while loading, otherwise the favicon. The <img> is
|
||||||
// only touched when the URL actually changes, so it never reloads.
|
// only touched when the URL actually changes, so it never reloads.
|
||||||
const ico = el.querySelector(".ico");
|
const ico = el.querySelector(".ico");
|
||||||
|
|
@ -1815,6 +1817,8 @@
|
||||||
const bmDisabled = !t.url;
|
const bmDisabled = !t.url;
|
||||||
m.appendChild(item("Add to Bookmarks", bmDisabled ? "off" : "", bmDisabled ? null : () => T.tabBookmark(t.id)));
|
m.appendChild(item("Add to Bookmarks", bmDisabled ? "off" : "", bmDisabled ? null : () => T.tabBookmark(t.id)));
|
||||||
m.appendChild(item(t.muted ? "Unmute" : "Mute", "", () => T.tabMute(t.id)));
|
m.appendChild(item(t.muted ? "Unmute" : "Mute", "", () => T.tabMute(t.id)));
|
||||||
|
// Background tabs stop (freeze) unless marked — for music, calls, dashboards.
|
||||||
|
m.appendChild(item(t.keepRunning ? "Stop when in background" : "Keep running in background", "", () => T.tabKeepRunning(t.id)));
|
||||||
const sep = document.createElement("div"); sep.className = "sep"; m.appendChild(sep);
|
const sep = document.createElement("div"); sep.className = "sep"; m.appendChild(sep);
|
||||||
m.appendChild(item("Close", "danger", () => T.closeTab(t.id)));
|
m.appendChild(item("Close", "danger", () => T.closeTab(t.id)));
|
||||||
document.body.appendChild(m);
|
document.body.appendChild(m);
|
||||||
|
|
|
||||||
99
main.js
99
main.js
|
|
@ -478,6 +478,7 @@ const SETTINGS_DEFAULTS = {
|
||||||
hideMediaDevices: true, // blank all enumerateDevices info (esp. speaker labels/ids) like Firefox
|
hideMediaDevices: true, // blank all enumerateDevices info (esp. speaker labels/ids) like Firefox
|
||||||
restoreSession: true, // reopen last session's tabs on launch
|
restoreSession: true, // reopen last session's tabs on launch
|
||||||
backgroundThrottle: true, // throttle inactive tabs / the window when unfocused
|
backgroundThrottle: true, // throttle inactive tabs / the window when unfocused
|
||||||
|
freezeBackgroundTabs: true, // a tab you switch away from stops (JS, timers, media) unless marked "Keep running"
|
||||||
// Storage retention — nothing persists by default. Auto-clear on quit
|
// Storage retention — nothing persists by default. Auto-clear on quit
|
||||||
// means a session leaves no trace on disk unless the user opts in per-type.
|
// means a session leaves no trace on disk unless the user opts in per-type.
|
||||||
clearCookiesOnQuit: true, // drop cookies + logins + saved-form data
|
clearCookiesOnQuit: true, // drop cookies + logins + saved-form data
|
||||||
|
|
@ -1344,9 +1345,16 @@ function loadOverlay(view) {
|
||||||
const i = overlayLoads.findIndex((o) => o.view === view);
|
const i = overlayLoads.findIndex((o) => o.view === view);
|
||||||
if (i < 0) return;
|
if (i < 0) return;
|
||||||
const { file } = overlayLoads.splice(i, 1)[0];
|
const { file } = overlayLoads.splice(i, 1)[0];
|
||||||
|
view.webContents.once("did-finish-load", () => overlayDone.add(view));
|
||||||
try { view.webContents.loadFile(file); } catch (e) { console.warn(`overlay load failed (${file}):`, e?.message); }
|
try { view.webContents.loadFile(file); } catch (e) { console.warn(`overlay load failed (${file}):`, e?.message); }
|
||||||
}
|
}
|
||||||
|
// Pages whose load has finished. Recorded from did-finish-load because
|
||||||
|
// isLoading() still reports true while that event is being delivered — a
|
||||||
|
// show deferred to it would then defer again and wait out overlayReady's
|
||||||
|
// timeout before appearing.
|
||||||
|
const overlayDone = new WeakSet();
|
||||||
function overlayLoaded(view) {
|
function overlayLoaded(view) {
|
||||||
|
if (overlayDone.has(view)) return true;
|
||||||
try { return !!view.webContents.getURL() && !view.webContents.isLoading(); } catch { return false; }
|
try { return !!view.webContents.getURL() && !view.webContents.isLoading(); } catch { return false; }
|
||||||
}
|
}
|
||||||
// The show functions send their data right after showing, which a page still
|
// The show functions send their data right after showing, which a page still
|
||||||
|
|
@ -1362,6 +1370,32 @@ function deferUntilOverlayLoaded(view, again) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
function cancelOverlayShow(view) { overlayWant.delete(view); }
|
function cancelOverlayShow(view) { overlayWant.delete(view); }
|
||||||
|
// Click-away for the toolbar popups (downloads, site info, engine picker):
|
||||||
|
// shown, they take focus; when focus moves anywhere else — a tab, the
|
||||||
|
// toolbar, another app — they close. The click on their own toolbar button
|
||||||
|
// is one of those focus moves, so a toggle right after a click-away close is
|
||||||
|
// that same click and must not reopen the popup.
|
||||||
|
const clickAwayClosedAt = new Map(); // view -> time it was closed by losing focus
|
||||||
|
const clickAwayPopups = []; // [{ isOpen, hide }] — also closed when the window loses focus
|
||||||
|
function closeOnClickAway(view, isOpen, hide) {
|
||||||
|
clickAwayPopups.push({ isOpen, hide });
|
||||||
|
view.webContents.on("blur", () => {
|
||||||
|
// Only a focus move inside an active window is a click elsewhere in
|
||||||
|
// Theseus. A popup shown while another app is in front gets focus and
|
||||||
|
// blur together, and must not close the moment it opens; switching to
|
||||||
|
// another app is handled by the window's own blur (closePopupsOnWindowBlur).
|
||||||
|
// The show functions only move focus into a popup while the window is
|
||||||
|
// active: focusing it from the background makes Windows touch the window,
|
||||||
|
// whose blur would close the popup it just opened.
|
||||||
|
if (!isOpen() || !win || win.isDestroyed() || !win.isFocused()) return;
|
||||||
|
clickAwayClosedAt.set(view, Date.now());
|
||||||
|
hide();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function closePopupsOnWindowBlur() {
|
||||||
|
for (const p of clickAwayPopups) { try { if (p.isOpen()) p.hide(); } catch {} }
|
||||||
|
}
|
||||||
|
const justClosedByClickAway = (view) => Date.now() - (clickAwayClosedAt.get(view) || 0) < 400;
|
||||||
// Idle prewarm, one at a time, of the overlays used in nearly every session.
|
// Idle prewarm, one at a time, of the overlays used in nearly every session.
|
||||||
let overlaysPrewarmed = false;
|
let overlaysPrewarmed = false;
|
||||||
async function prewarmOverlays() {
|
async function prewarmOverlays() {
|
||||||
|
|
@ -1380,7 +1414,7 @@ async function prewarmOverlays() {
|
||||||
function overlayReady(view, timeoutMs = 4000) {
|
function overlayReady(view, timeoutMs = 4000) {
|
||||||
loadOverlay(view);
|
loadOverlay(view);
|
||||||
const wc = view.webContents;
|
const wc = view.webContents;
|
||||||
try { if (wc.getURL() && !wc.isLoading()) return Promise.resolve(); } catch {}
|
if (overlayLoaded(view)) return Promise.resolve();
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const timer = setTimeout(done, timeoutMs);
|
const timer = setTimeout(done, timeoutMs);
|
||||||
function done() { clearTimeout(timer); wc.removeListener("did-finish-load", done); resolve(); }
|
function done() { clearTimeout(timer); wc.removeListener("did-finish-load", done); resolve(); }
|
||||||
|
|
@ -2870,6 +2904,7 @@ function showPopover(show) {
|
||||||
win.contentView.removeChildView(popover);
|
win.contentView.removeChildView(popover);
|
||||||
win.contentView.addChildView(popover);
|
win.contentView.addChildView(popover);
|
||||||
popover.setVisible(true); popVisible = true;
|
popover.setVisible(true); popVisible = true;
|
||||||
|
if (win.isFocused()) try { popover.webContents.focus(); } catch {} // see closeOnClickAway
|
||||||
popover.webContents.send("site-info", decorate(activeTab()?.prov) || { kind: "home" });
|
popover.webContents.send("site-info", decorate(activeTab()?.prov) || { kind: "home" });
|
||||||
} else { cancelOverlayShow(popover); popover.setVisible(false); popVisible = false; }
|
} else { cancelOverlayShow(popover); popover.setVisible(false); popVisible = false; }
|
||||||
}
|
}
|
||||||
|
|
@ -2887,6 +2922,7 @@ function showEnginePicker(show) {
|
||||||
win.contentView.removeChildView(enginePicker);
|
win.contentView.removeChildView(enginePicker);
|
||||||
win.contentView.addChildView(enginePicker);
|
win.contentView.addChildView(enginePicker);
|
||||||
enginePicker.setVisible(true); epVisible = true;
|
enginePicker.setVisible(true); epVisible = true;
|
||||||
|
if (win.isFocused()) try { enginePicker.webContents.focus(); } catch {} // see closeOnClickAway
|
||||||
enginePicker.webContents.send("engines", { engines: enabledEnginesList(), current: settings.searchEngine, detected: activeTab()?.detected || null });
|
enginePicker.webContents.send("engines", { engines: enabledEnginesList(), current: settings.searchEngine, detected: activeTab()?.detected || null });
|
||||||
} else { cancelOverlayShow(enginePicker); enginePicker.setVisible(false); epVisible = false; }
|
} else { cancelOverlayShow(enginePicker); enginePicker.setVisible(false); epVisible = false; }
|
||||||
}
|
}
|
||||||
|
|
@ -2904,6 +2940,7 @@ function showDownloads(show) {
|
||||||
win.contentView.removeChildView(downloadsPop);
|
win.contentView.removeChildView(downloadsPop);
|
||||||
win.contentView.addChildView(downloadsPop);
|
win.contentView.addChildView(downloadsPop);
|
||||||
downloadsPop.setVisible(true); dlVisible = true;
|
downloadsPop.setVisible(true); dlVisible = true;
|
||||||
|
if (win.isFocused()) try { downloadsPop.webContents.focus(); } catch {} // see closeOnClickAway
|
||||||
downloadsPop.webContents.send("downloads", downloadsPublic());
|
downloadsPop.webContents.send("downloads", downloadsPublic());
|
||||||
} else { cancelOverlayShow(downloadsPop); downloadsPop.setVisible(false); dlVisible = false; }
|
} else { cancelOverlayShow(downloadsPop); downloadsPop.setVisible(false); dlVisible = false; }
|
||||||
}
|
}
|
||||||
|
|
@ -3254,9 +3291,50 @@ function installDownloadTracker() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// ---- background tab freezing ----
|
||||||
|
// A tab the user switches away from is frozen (Chromium's page lifecycle
|
||||||
|
// "frozen": no JS, timers, network callbacks or media) after a short grace,
|
||||||
|
// and thawed the moment it is shown again. Exempt: tabs marked "Keep running
|
||||||
|
// in background" from the tab menu (music, calls, live dashboards), dormant
|
||||||
|
// restored tabs (nothing loaded), tabs waiting on a page dialog. Uses the
|
||||||
|
// per-tab debugger applyFingerprint already keeps attached. Chromium only
|
||||||
|
// freezes hidden pages, which background tabs are.
|
||||||
|
const FREEZE_GRACE_MS = 1000;
|
||||||
|
async function setTabFrozen(tab, frozen) {
|
||||||
|
if (!tab || !!tab.frozen === frozen) return;
|
||||||
|
const wc = tab.view?.webContents;
|
||||||
|
if (!wc || wc.isDestroyed()) return;
|
||||||
|
try {
|
||||||
|
if (!wc.debugger.isAttached()) wc.debugger.attach("1.3");
|
||||||
|
await wc.debugger.sendCommand("Page.setWebLifecycleState", { state: frozen ? "frozen" : "active" });
|
||||||
|
tab.frozen = frozen;
|
||||||
|
} catch (e) { console.warn(`[tabs] ${frozen ? "freeze" : "thaw"} failed:`, e?.message); }
|
||||||
|
}
|
||||||
|
function scheduleFreeze(tab) {
|
||||||
|
if (!tab) return;
|
||||||
|
clearTimeout(tab.freezeTimer);
|
||||||
|
if (!settings.freezeBackgroundTabs || tab.keepRunning || tab.pending || !tab.url || tab.id === activeId) return;
|
||||||
|
tab.freezeTimer = setTimeout(() => {
|
||||||
|
if (tab.id === activeId || !tabs.includes(tab) || tab.keepRunning || tab.pending || tabHasPendingDialog(tab.id)) return;
|
||||||
|
setTabFrozen(tab, true);
|
||||||
|
}, FREEZE_GRACE_MS);
|
||||||
|
}
|
||||||
|
function thawTab(tab) {
|
||||||
|
if (!tab) return;
|
||||||
|
clearTimeout(tab.freezeTimer);
|
||||||
|
if (tab.frozen) setTabFrozen(tab, false);
|
||||||
|
}
|
||||||
|
// The setting turned off (or on): thaw everything (or freeze the background).
|
||||||
|
function applyFreezeSetting() {
|
||||||
|
for (const t of tabs) { if (settings.freezeBackgroundTabs && t.id !== activeId) scheduleFreeze(t); else thawTab(t); }
|
||||||
|
}
|
||||||
function setActive(id) {
|
function setActive(id) {
|
||||||
const switching = id !== activeId;
|
const switching = id !== activeId;
|
||||||
|
const previous = switching ? tabs.find((x) => x.id === activeId) : null;
|
||||||
activeId = id;
|
activeId = id;
|
||||||
|
// The incoming tab runs again before it is shown; the one left behind stops.
|
||||||
|
thawTab(tabs.find((x) => x.id === id));
|
||||||
|
if (previous) scheduleFreeze(previous);
|
||||||
// A fullscreen video does not follow the user to another tab.
|
// A fullscreen video does not follow the user to another tab.
|
||||||
if (switching && fsTabId != null && fsTabId !== id) leaveHtmlFullscreen(tabs.find((t) => t.id === fsTabId), true);
|
if (switching && fsTabId != null && fsTabId !== id) leaveHtmlFullscreen(tabs.find((t) => t.id === fsTabId), true);
|
||||||
if (popVisible) showPopover(false); // don't carry a stale popover across tabs
|
if (popVisible) showPopover(false); // don't carry a stale popover across tabs
|
||||||
|
|
@ -3291,7 +3369,7 @@ function emitTabs() {
|
||||||
const t = activeTab();
|
const t = activeTab();
|
||||||
const wc = t?.view.webContents;
|
const wc = t?.view.webContents;
|
||||||
chrome?.webContents.send("tabs", {
|
chrome?.webContents.send("tabs", {
|
||||||
tabs: tabs.map((x) => ({ id: x.id, title: x.title || "New Tab", active: x.id === activeId, loading: !!x.loading, favicon: x.favicon || null, muted: !!x.muted, group: x.group || null, url: x.url || "", asking: tabHasPendingDialog(x.id) })),
|
tabs: tabs.map((x) => ({ id: x.id, title: x.title || "New Tab", active: x.id === activeId, loading: !!x.loading, favicon: x.favicon || null, muted: !!x.muted, group: x.group || null, url: x.url || "", asking: tabHasPendingDialog(x.id), keepRunning: !!x.keepRunning })),
|
||||||
collapsedGroups: [...tabGroupCollapsed],
|
collapsedGroups: [...tabGroupCollapsed],
|
||||||
url: t?.url || "",
|
url: t?.url || "",
|
||||||
loading: !!t?.loading,
|
loading: !!t?.loading,
|
||||||
|
|
@ -3917,6 +3995,11 @@ function createWindow() {
|
||||||
styleScrollbars(downloadsPop.webContents);
|
styleScrollbars(downloadsPop.webContents);
|
||||||
deferOverlayLoad(downloadsPop, "downloads.html");
|
deferOverlayLoad(downloadsPop, "downloads.html");
|
||||||
downloadsPop.setVisible(false);
|
downloadsPop.setVisible(false);
|
||||||
|
clickAwayPopups.length = 0;
|
||||||
|
closeOnClickAway(popover, () => popVisible, () => showPopover(false));
|
||||||
|
closeOnClickAway(enginePicker, () => epVisible, () => showEnginePicker(false));
|
||||||
|
closeOnClickAway(downloadsPop, () => dlVisible, () => showDownloads(false));
|
||||||
|
win.on("blur", closePopupsOnWindowBlur);
|
||||||
// Floating address-bar suggestions dropdown.
|
// Floating address-bar suggestions dropdown.
|
||||||
addressPicker = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "address-picker-preload.js") } });
|
addressPicker = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "address-picker-preload.js") } });
|
||||||
try { addressPicker.setBackgroundColor("#00000000"); } catch {}
|
try { addressPicker.setBackgroundColor("#00000000"); } catch {}
|
||||||
|
|
@ -4032,6 +4115,7 @@ function createWindow() {
|
||||||
|
|
||||||
async function navigateTab(id, input) {
|
async function navigateTab(id, input) {
|
||||||
const t = tabById(id); if (!t) return;
|
const t = tabById(id); if (!t) return;
|
||||||
|
thawTab(t);
|
||||||
// A pending tab the user navigates explicitly (URL bar, link follow,
|
// A pending tab the user navigates explicitly (URL bar, link follow,
|
||||||
// search) has outgrown its saved URL — otherwise a later reload or chip
|
// search) has outgrown its saved URL — otherwise a later reload or chip
|
||||||
// click would snap it back to that stale URL via materializePending.
|
// click would snap it back to that stale URL via materializePending.
|
||||||
|
|
@ -4216,6 +4300,13 @@ ipcMain.handle("tab-duplicate", (_e, id) => {
|
||||||
const target = t.url || "";
|
const target = t.url || "";
|
||||||
if (target) createTab(target); else createTab();
|
if (target) createTab(target); else createTab();
|
||||||
});
|
});
|
||||||
|
ipcMain.handle("tab-keep-running", (_e, id, on) => {
|
||||||
|
const t = tabById(id); if (!t) return false;
|
||||||
|
t.keepRunning = typeof on === "boolean" ? on : !t.keepRunning;
|
||||||
|
if (t.keepRunning) thawTab(t); else scheduleFreeze(t);
|
||||||
|
emitTabs();
|
||||||
|
return t.keepRunning;
|
||||||
|
});
|
||||||
ipcMain.handle("tab-mute", (_e, id, on) => {
|
ipcMain.handle("tab-mute", (_e, id, on) => {
|
||||||
const t = tabById(id); if (!t) return false;
|
const t = tabById(id); if (!t) return false;
|
||||||
const want = typeof on === "boolean" ? on : !t.muted;
|
const want = typeof on === "boolean" ? on : !t.muted;
|
||||||
|
|
@ -5570,6 +5661,7 @@ ipcMain.handle("settings-open-panel", (e, panelId) => {
|
||||||
});
|
});
|
||||||
ipcMain.handle("toggle-site-info", (_e, rect) => {
|
ipcMain.handle("toggle-site-info", (_e, rect) => {
|
||||||
if (popVisible) return showPopover(false);
|
if (popVisible) return showPopover(false);
|
||||||
|
if (justClosedByClickAway(popover)) return;
|
||||||
if (rect) popPos = { x: Math.round(rect.x), y: Math.round(rect.y) };
|
if (rect) popPos = { x: Math.round(rect.x), y: Math.round(rect.y) };
|
||||||
showPopover(true);
|
showPopover(true);
|
||||||
});
|
});
|
||||||
|
|
@ -6325,6 +6417,7 @@ ipcMain.handle("set-engine-order", (_e, ids) => {
|
||||||
// The custom dropdown overlay (real favicons).
|
// The custom dropdown overlay (real favicons).
|
||||||
ipcMain.handle("toggle-engine-picker", (_e, rect) => {
|
ipcMain.handle("toggle-engine-picker", (_e, rect) => {
|
||||||
if (epVisible) return showEnginePicker(false);
|
if (epVisible) return showEnginePicker(false);
|
||||||
|
if (justClosedByClickAway(enginePicker)) return;
|
||||||
if (rect) epPos = { x: Math.round(rect.x), y: Math.round(rect.y) };
|
if (rect) epPos = { x: Math.round(rect.x), y: Math.round(rect.y) };
|
||||||
showEnginePicker(true);
|
showEnginePicker(true);
|
||||||
});
|
});
|
||||||
|
|
@ -6347,6 +6440,7 @@ ipcMain.handle("picker-open-settings", () => {
|
||||||
ipcMain.handle("downloads-get", () => downloadsPublic());
|
ipcMain.handle("downloads-get", () => downloadsPublic());
|
||||||
ipcMain.handle("toggle-downloads", (_e, rect) => {
|
ipcMain.handle("toggle-downloads", (_e, rect) => {
|
||||||
if (dlVisible) return showDownloads(false);
|
if (dlVisible) return showDownloads(false);
|
||||||
|
if (justClosedByClickAway(downloadsPop)) return;
|
||||||
if (rect) dlPos = { x: Math.round(rect.x), y: Math.round(rect.y) };
|
if (rect) dlPos = { x: Math.round(rect.x), y: Math.round(rect.y) };
|
||||||
showDownloads(true);
|
showDownloads(true);
|
||||||
});
|
});
|
||||||
|
|
@ -6519,6 +6613,7 @@ ipcMain.handle("settings-set", (_e, key, val) => {
|
||||||
if (key === "searchEngine") emitEngines(); // the toolbar button and the picker show the default
|
if (key === "searchEngine") emitEngines(); // the toolbar button and the picker show the default
|
||||||
if (key === "theme") applyTheme();
|
if (key === "theme") applyTheme();
|
||||||
if (key === "backgroundThrottle") applyThrottle();
|
if (key === "backgroundThrottle") applyThrottle();
|
||||||
|
if (key === "freezeBackgroundTabs") applyFreezeSetting();
|
||||||
if (["timezoneMode", "timezoneValue", "languageMode", "languageSpoof", "languageValue",
|
if (["timezoneMode", "timezoneValue", "languageMode", "languageSpoof", "languageValue",
|
||||||
"locationMode", "locationRegion", "locationCountry", "locationLat", "locationLon", "hideMediaDevices"].includes(key)) { applyFingerprintAll(); applyAcceptLanguage(); }
|
"locationMode", "locationRegion", "locationCountry", "locationLat", "locationLon", "hideMediaDevices"].includes(key)) { applyFingerprintAll(); applyAcceptLanguage(); }
|
||||||
// Language changes alter the Accept-Language the server sees on the NEXT
|
// Language changes alter the Accept-Language the server sees on the NEXT
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ contextBridge.exposeInMainWorld("theseus", {
|
||||||
tabReload: (id) => ipcRenderer.invoke("tab-reload", id),
|
tabReload: (id) => ipcRenderer.invoke("tab-reload", id),
|
||||||
tabDuplicate: (id) => ipcRenderer.invoke("tab-duplicate", id),
|
tabDuplicate: (id) => ipcRenderer.invoke("tab-duplicate", id),
|
||||||
tabMute: (id, on) => ipcRenderer.invoke("tab-mute", id, on),
|
tabMute: (id, on) => ipcRenderer.invoke("tab-mute", id, on),
|
||||||
|
tabKeepRunning: (id, on) => ipcRenderer.invoke("tab-keep-running", id, on),
|
||||||
tabGroup: (id, color) => ipcRenderer.invoke("tab-group", id, color),
|
tabGroup: (id, color) => ipcRenderer.invoke("tab-group", id, color),
|
||||||
tabGroupToggle: (color) => ipcRenderer.invoke("tab-group-toggle", color),
|
tabGroupToggle: (color) => ipcRenderer.invoke("tab-group-toggle", color),
|
||||||
tabBookmark: (id) => ipcRenderer.invoke("tab-bookmark", id),
|
tabBookmark: (id) => ipcRenderer.invoke("tab-bookmark", id),
|
||||||
|
|
|
||||||
|
|
@ -590,6 +590,10 @@
|
||||||
<div class="txt"><div class="t">Throttle inactive tabs</div><div class="d">Background and inactive tabs use far less CPU. Recommended.</div></div>
|
<div class="txt"><div class="t">Throttle inactive tabs</div><div class="d">Background and inactive tabs use far less CPU. Recommended.</div></div>
|
||||||
<label class="sw"><input type="checkbox" id="backgroundThrottle"><span class="track"><span class="knob"></span></span></label>
|
<label class="sw"><input type="checkbox" id="backgroundThrottle"><span class="track"><span class="knob"></span></span></label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="txt"><div class="t">Stop tabs in the background</div><div class="d">A tab you switch away from stops running — scripts, timers, audio and video — until you come back to it. Right-click a tab and choose “Keep running in background” for music, calls or anything that should keep going.</div></div>
|
||||||
|
<label class="sw"><input type="checkbox" id="freezeBackgroundTabs"><span class="track"><span class="knob"></span></span></label>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<!-- PRIVACY -->
|
<!-- PRIVACY -->
|
||||||
<section id="privacy" hidden>
|
<section id="privacy" hidden>
|
||||||
|
|
@ -1018,7 +1022,7 @@
|
||||||
el("dohCustom").addEventListener("change", () => C.set("dohCustom", el("dohCustom").value.trim()));
|
el("dohCustom").addEventListener("change", () => C.set("dohCustom", el("dohCustom").value.trim()));
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
const TOGGLES = ["restoreSession", "backgroundThrottle", "blockCamera", "blockMicrophone", "hideMediaDevices", "gpc",
|
const TOGGLES = ["restoreSession", "backgroundThrottle", "freezeBackgroundTabs", "blockCamera", "blockMicrophone", "hideMediaDevices", "gpc",
|
||||||
"clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit",
|
"clearCookiesOnQuit", "clearCacheOnQuit", "clearStorageOnQuit", "clearHistoryOnQuit",
|
||||||
"quickLinksShow"];
|
"quickLinksShow"];
|
||||||
C.get().then((s) => {
|
C.get().then((s) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue