Theseus 0.3.63: lazy tab restore, Privacy language simplified, chip reloads the page
Session restore now paints the full strip from the saved titles + favicons and
loads only the ACTIVE tab's page; every other restored tab lives as a dormant
WebContentsView and navigates for the first time when the user clicks it. For
a 20-tab user that drops cold start from 20 renderer loads racing chrome.html
to one, so launch is roughly flat whatever the tab count — fixes the "not
responding" freeze on a session with many restored tabs. session.json is now
v3 ({v:3, tabs:[{url,title,favicon}], active}); v1/v2 session files still
parse (their tabs restore lazy without a cached title, which arrives on first
activation). Reload on a dormant tab materialises it.
Privacy › Anti-fingerprinting › Language is now two modes — Automatic (system
language) and Manual — matching the General › Website language row and the
URL-bar globe chip. The old Spoof-choose top-10 and Hide-en-US modes are gone
from the UI; legacy saved values auto-migrate to Automatic on first open. The
Manual list is the same 24 languages the General row uses, kept in one place
(WEB_LANG_LIST), so all three surfaces stay in sync.
Changing the language via the globe chip or either settings row now reloads
the active tab — the server picked the response body from Accept-Language on
the original request, so an already-rendered page can't adopt the new language
on its own. A reload is what a user clicking a one-click language switch
expects.
The Location row's country dropdown now stacks under the mode dropdown on its
own line when Manual is picked, so an open menu above it can't visually cover
it (the row's flex-row max-60% layout could wrap it where another dropdown's
overlay sat).
Also: the settings-update broadcast now reaches every open settings tab, not
only the chrome — so changing the chip updates both the General Website-
language row and the Privacy Anti-fingerprinting Language row live without a
Settings refresh.
This commit is contained in:
parent
838d686598
commit
3c35b2605b
3 changed files with 197 additions and 129 deletions
147
main.js
147
main.js
|
|
@ -1144,56 +1144,72 @@ function applyClientHintsSpoof() {
|
||||||
}
|
}
|
||||||
// ---- session restore + background throttling ----
|
// ---- session restore + background throttling ----
|
||||||
const sessionFile = () => path.join(app.getPath("userData"), "session.json");
|
const sessionFile = () => path.join(app.getPath("userData"), "session.json");
|
||||||
// v2 format: { v: 2, urls, active }. v1 was a bare array of URLs; reading one
|
// v3 format: { v: 3, tabs: [{url, title, favicon}], active }.
|
||||||
// maps to active = last tab, which is what the old restore loop ended up
|
// v2 was { v: 2, urls, active }; v1 was a bare array of URLs.
|
||||||
// showing (each createTab activated itself, so the rightmost tab won).
|
// Carrying title + favicon lets restoreTabs paint the full strip at launch
|
||||||
|
// without any tab having to load first — background tabs stay DORMANT (no
|
||||||
|
// loadURL, no renderer activity) and come to life only when activated.
|
||||||
let sessionSavedAtClose = false;
|
let sessionSavedAtClose = false;
|
||||||
function saveSession() {
|
function saveSession() {
|
||||||
if (sessionSavedAtClose && !winAlive()) return; // already captured when the window closed
|
if (sessionSavedAtClose && !winAlive()) return; // already captured when the window closed
|
||||||
try {
|
try {
|
||||||
const live = tabs.filter((t) => !t.settings && t.url);
|
const live = tabs.filter((t) => !t.settings && (t.url || t.pending?.url));
|
||||||
const active = Math.max(0, live.findIndex((t) => t.id === activeId));
|
const active = Math.max(0, live.findIndex((t) => t.id === activeId));
|
||||||
fs.writeFileSync(sessionFile(), JSON.stringify({ v: 2, urls: live.map((t) => t.url), active }));
|
const data = live.map((t) => {
|
||||||
|
// A still-dormant tab's URL + title + favicon live under `pending` —
|
||||||
|
// the user never activated it, so the view never paid a renderer cost.
|
||||||
|
// Save what we would have shown either way.
|
||||||
|
const url = t.pending?.url ?? t.url ?? "";
|
||||||
|
const title = t.pending?.title ?? t.title ?? "";
|
||||||
|
const favicon = t.pending?.favicon ?? t.favicon ?? null;
|
||||||
|
return { url, title, favicon };
|
||||||
|
});
|
||||||
|
fs.writeFileSync(sessionFile(), JSON.stringify({ v: 3, tabs: data, active }));
|
||||||
} catch (e) { console.error("session save failed:", e.message); }
|
} catch (e) { console.error("session save failed:", e.message); }
|
||||||
}
|
}
|
||||||
function loadSession() {
|
function loadSession() {
|
||||||
|
const empty = { tabs: [], active: 0 };
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(sessionFile())) return { urls: [], active: 0 };
|
if (!fs.existsSync(sessionFile())) return empty;
|
||||||
const raw = JSON.parse(fs.readFileSync(sessionFile(), "utf8"));
|
const raw = JSON.parse(fs.readFileSync(sessionFile(), "utf8"));
|
||||||
|
// v3: {v:3, tabs:[{url,title,favicon}], active}
|
||||||
|
if (raw && raw.v === 3 && Array.isArray(raw.tabs)) {
|
||||||
|
const t = raw.tabs.filter((x) => x && typeof x.url === "string" && x.url).map((x) => ({
|
||||||
|
url: x.url, title: typeof x.title === "string" ? x.title : "", favicon: typeof x.favicon === "string" ? x.favicon : null,
|
||||||
|
}));
|
||||||
|
const active = Math.min(Math.max(Number(raw.active) || 0, 0), Math.max(t.length - 1, 0));
|
||||||
|
return { tabs: t, active };
|
||||||
|
}
|
||||||
|
// v2 / v1: just URL lists. Title + favicon arrive once the user activates the tab.
|
||||||
const list = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.urls) ? raw.urls : []);
|
const list = Array.isArray(raw) ? raw : (raw && Array.isArray(raw.urls) ? raw.urls : []);
|
||||||
const urls = list.filter((u) => typeof u === "string" && u);
|
const urls = list.filter((u) => typeof u === "string" && u);
|
||||||
const last = Math.max(urls.length - 1, 0);
|
const last = Math.max(urls.length - 1, 0);
|
||||||
const active = Array.isArray(raw) ? last : Math.min(Math.max(Number(raw.active) || 0, 0), last);
|
const active = Array.isArray(raw) ? last : Math.min(Math.max(Number(raw.active) || 0, 0), last);
|
||||||
return { urls, active };
|
return { tabs: urls.map((url) => ({ url, title: "", favicon: null })), active };
|
||||||
} catch { return { urls: [], active: 0 }; }
|
} catch { return empty; }
|
||||||
}
|
}
|
||||||
// Session restore, staggered. Creating every saved tab in one synchronous
|
// Session restore, LAZY: only the active tab actually loads a page at startup.
|
||||||
// burst meant N renderer processes launching on top of chrome.html's first
|
// Every other restored tab is a dormant WebContentsView in the strip — title
|
||||||
// paint — on a slow or busy machine the window sat blank for seconds. The
|
// and favicon from the saved session — and only navigates when the user clicks
|
||||||
// active tab is created first (so the user's page is on screen right away)
|
// it (setActive consumes `pending`). This drops N renderer loads at startup
|
||||||
// and the rest arrive one per RESTORE_STAGGER_MS as background tabs, slotted
|
// to 1, so launch time is roughly flat whether the user has 2 or 25 tabs.
|
||||||
// into their saved strip positions.
|
|
||||||
const RESTORE_STAGGER_MS = 150;
|
|
||||||
function restoreTabs() {
|
function restoreTabs() {
|
||||||
const saved = settings.restoreSession ? loadSession() : { urls: [], active: 0 };
|
const saved = settings.restoreSession ? loadSession() : { tabs: [], active: 0 };
|
||||||
if (!saved.urls.length) { createTab(); return; }
|
if (!saved.tabs.length) { createTab(); return; }
|
||||||
createTab(saved.urls[saved.active]);
|
const activeMeta = saved.tabs[saved.active] || saved.tabs[0];
|
||||||
const rest = saved.urls.map((url, i) => ({ url, i })).filter((x) => x.i !== saved.active);
|
createTab(activeMeta.url);
|
||||||
const step = () => {
|
// Build the rest as lazy. Insert at their saved slot so the strip order
|
||||||
if (!rest.length || !win || win.isDestroyed()) return;
|
// matches the save.
|
||||||
const { url, i } = rest.shift();
|
for (let i = 0; i < saved.tabs.length; i++) {
|
||||||
|
if (i === saved.active) continue;
|
||||||
try {
|
try {
|
||||||
createTab(url, { background: true });
|
createTab(null, { background: true, pending: saved.tabs[i] });
|
||||||
// createTab appends; move the new tab to its saved slot. Lower indices
|
// createTab appends; move it into position.
|
||||||
// are created first, so `i` is the right position on both sides of
|
|
||||||
// the active tab.
|
|
||||||
const t = tabs.pop();
|
const t = tabs.pop();
|
||||||
tabs.splice(Math.min(i, tabs.length), 0, t);
|
tabs.splice(Math.min(i, tabs.length), 0, t);
|
||||||
emitTabs();
|
|
||||||
} catch (e) { console.warn("session restore: tab failed:", e?.message); }
|
} catch (e) { console.warn("session restore: tab failed:", e?.message); }
|
||||||
setTimeout(step, RESTORE_STAGGER_MS);
|
}
|
||||||
};
|
emitTabs();
|
||||||
setTimeout(step, RESTORE_STAGGER_MS);
|
|
||||||
}
|
}
|
||||||
// ---- deferred overlay loads ----
|
// ---- deferred overlay loads ----
|
||||||
// The floating overlays (site-info popover, engine picker, downloads,
|
// The floating overlays (site-info popover, engine picker, downloads,
|
||||||
|
|
@ -3116,6 +3132,8 @@ function setActive(id) {
|
||||||
const target = tabs.find((x) => x.id === id);
|
const target = tabs.find((x) => x.id === id);
|
||||||
if (target) target.view.setVisible(true);
|
if (target) target.view.setVisible(true);
|
||||||
for (const t of tabs) if (t.id !== id) t.view.setVisible(false);
|
for (const t of tabs) if (t.id !== id) t.view.setVisible(false);
|
||||||
|
// Dormant restored tabs come to life on first activation.
|
||||||
|
if (target && target.pending) materializePending(target);
|
||||||
const t = activeTab();
|
const t = activeTab();
|
||||||
// Track the last active tab that isn't an add-on-owned page so captureTab
|
// Track the last active tab that isn't an add-on-owned page so captureTab
|
||||||
// has a sensible fallback when the user re-triggers the dropdown from
|
// has a sensible fallback when the user re-triggers the dropdown from
|
||||||
|
|
@ -3606,7 +3624,17 @@ function createTab(initial, opts = {}) {
|
||||||
layout();
|
layout();
|
||||||
if (opts.background) { view.setVisible(false); emitTabs(); }
|
if (opts.background) { view.setVisible(false); emitTabs(); }
|
||||||
else setActive(id);
|
else setActive(id);
|
||||||
if (opts.settings) {
|
if (opts.pending) {
|
||||||
|
// Lazy / dormant: the tab lives in the strip with its saved metadata but
|
||||||
|
// nothing loads until setActive consumes `pending`. Only valid on
|
||||||
|
// background tabs — createTab's caller never asks for a foreground tab
|
||||||
|
// that is also dormant (restoreTabs enforces this).
|
||||||
|
tab.pending = { url: opts.pending.url, title: opts.pending.title || "", favicon: opts.pending.favicon || null };
|
||||||
|
tab.url = opts.pending.url;
|
||||||
|
if (opts.pending.title) tab.title = opts.pending.title;
|
||||||
|
if (opts.pending.favicon) tab.favicon = opts.pending.favicon;
|
||||||
|
emitTabs();
|
||||||
|
} else if (opts.settings) {
|
||||||
tab.prov = { host: "", kind: "home" };
|
tab.prov = { host: "", kind: "home" };
|
||||||
const settingsOpts = opts.settingsSection ? { hash: opts.settingsSection } : undefined;
|
const settingsOpts = opts.settingsSection ? { hash: opts.settingsSection } : undefined;
|
||||||
wc.loadFile("settings.html", settingsOpts);
|
wc.loadFile("settings.html", settingsOpts);
|
||||||
|
|
@ -3625,6 +3653,16 @@ function createTab(initial, opts = {}) {
|
||||||
else loadHome(id);
|
else loadHome(id);
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
// First activation of a dormant restored tab: navigate to its saved URL.
|
||||||
|
// Called from setActive and reload — both the "I'm looking at this tab" and
|
||||||
|
// "I want it to load" entry points consume `pending`.
|
||||||
|
function materializePending(tab) {
|
||||||
|
if (!tab || !tab.pending) return false;
|
||||||
|
const url = tab.pending.url;
|
||||||
|
tab.pending = null;
|
||||||
|
try { navigateTab(tab.id, url); } catch (e) { console.warn("materializePending failed:", e?.message); }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
function closeTab(id) {
|
function closeTab(id) {
|
||||||
const i = tabs.findIndex((t) => t.id === id);
|
const i = tabs.findIndex((t) => t.id === id);
|
||||||
if (i < 0) return;
|
if (i < 0) return;
|
||||||
|
|
@ -4097,16 +4135,27 @@ const WEBSITE_LANGUAGE_QUICK = [
|
||||||
{ tag: "zh-TW", label: "中文(繁體)" },
|
{ tag: "zh-TW", label: "中文(繁體)" },
|
||||||
];
|
];
|
||||||
function setWebsiteLanguage(mode, value) {
|
function setWebsiteLanguage(mode, value) {
|
||||||
|
const before = { mode: settings.languageMode, value: settings.languageValue };
|
||||||
if (mode === "show") {
|
if (mode === "show") {
|
||||||
settings.languageMode = "show";
|
settings.languageMode = "show";
|
||||||
} else if (mode === "manual" && value) {
|
} else if (mode === "manual" && value) {
|
||||||
settings.languageMode = "manual";
|
settings.languageMode = "manual";
|
||||||
settings.languageValue = String(value);
|
settings.languageValue = String(value);
|
||||||
} else return;
|
} else return;
|
||||||
|
const changed = before.mode !== settings.languageMode || before.value !== settings.languageValue;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
applyFingerprintAll();
|
applyFingerprintAll();
|
||||||
applyAcceptLanguage();
|
applyAcceptLanguage();
|
||||||
try { chrome?.webContents.send("settings-update", settings); } catch {}
|
broadcastSettings();
|
||||||
|
// Nothing short of a reload makes a loaded page re-render in a new language —
|
||||||
|
// the server picked the body from the Accept-Language on the ORIGINAL request.
|
||||||
|
// The chip is a one-click language switch, so the user expects to see it take
|
||||||
|
// effect on whatever they're looking at: reload the active tab (only).
|
||||||
|
if (changed) {
|
||||||
|
const t = activeTab();
|
||||||
|
const wc = t && !t.settings && !t.addonId && t.url ? t.view.webContents : null;
|
||||||
|
try { wc && wc.reload(); } catch {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Human-readable name for a BCP-47 tag — the plain language name, no regional
|
// Human-readable name for a BCP-47 tag — the plain language name, no regional
|
||||||
// qualifier ("en-US" → "English", not "American English"). The picker only
|
// qualifier ("en-US" → "English", not "American English"). The picker only
|
||||||
|
|
@ -5206,7 +5255,12 @@ ipcMain.handle("home-cards-reset", (e) => { if (!isHomePageSender(e.sender)) ret
|
||||||
ipcMain.handle("go-back", () => { const wc = activeTab()?.view.webContents; if (wc?.navigationHistory.canGoBack()) wc.navigationHistory.goBack(); });
|
ipcMain.handle("go-back", () => { const wc = activeTab()?.view.webContents; if (wc?.navigationHistory.canGoBack()) wc.navigationHistory.goBack(); });
|
||||||
ipcMain.handle("go-forward", () => { const wc = activeTab()?.view.webContents; if (wc?.navigationHistory.canGoForward()) wc.navigationHistory.goForward(); });
|
ipcMain.handle("go-forward", () => { const wc = activeTab()?.view.webContents; if (wc?.navigationHistory.canGoForward()) wc.navigationHistory.goForward(); });
|
||||||
ipcMain.handle("reload", (_e, hard) => {
|
ipcMain.handle("reload", (_e, hard) => {
|
||||||
const wc = activeTab()?.view.webContents;
|
const t = activeTab();
|
||||||
|
if (!t) return;
|
||||||
|
// A dormant restored tab has nothing to reload — treat Reload as "load it
|
||||||
|
// for the first time" rather than reloading an empty renderer.
|
||||||
|
if (t.pending) { materializePending(t); return; }
|
||||||
|
const wc = t.view.webContents;
|
||||||
if (!wc) return;
|
if (!wc) return;
|
||||||
try { hard ? wc.reloadIgnoringCache() : wc.reload(); } catch {}
|
try { hard ? wc.reloadIgnoringCache() : wc.reload(); } catch {}
|
||||||
});
|
});
|
||||||
|
|
@ -6192,12 +6246,31 @@ ipcMain.handle("settings-set", (_e, key, val) => {
|
||||||
if (key === "backgroundThrottle") applyThrottle();
|
if (key === "backgroundThrottle") applyThrottle();
|
||||||
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(); }
|
||||||
// Push updates chrome-side reactively so toolbar-size changes (urlBarSize,
|
// Language changes alter the Accept-Language the server sees on the NEXT
|
||||||
// searchBoxSize) take effect without a relaunch. Whole settings object
|
// request; an already-rendered page keeps the body it was first served.
|
||||||
// fits in one message and chrome only cares about a handful of fields.
|
// Reload the active tab so the user sees the switch take effect without a
|
||||||
try { chrome?.webContents.send("settings-update", settings); } catch {}
|
// manual F5. (Only for the language keys; timezone / location are JS-side
|
||||||
|
// overrides and don't need the server round-trip.)
|
||||||
|
if (key === "languageMode" || key === "languageValue") {
|
||||||
|
const t = activeTab();
|
||||||
|
const wc = t && !t.settings && !t.addonId && t.url ? t.view.webContents : null;
|
||||||
|
try { wc && wc.reload(); } catch {}
|
||||||
|
}
|
||||||
|
// Broadcast to every renderer that watches the live settings — the chrome
|
||||||
|
// (toolbar sizes, URL-bar language chip) and every open settings tab (so
|
||||||
|
// the General Website-language row and the Privacy Anti-fingerprinting
|
||||||
|
// Language row stay in sync with the chip and with each other).
|
||||||
|
broadcastSettings();
|
||||||
return settings;
|
return settings;
|
||||||
});
|
});
|
||||||
|
function broadcastSettings() {
|
||||||
|
try { chrome?.webContents.send("settings-update", settings); } catch {}
|
||||||
|
for (const t of tabs) {
|
||||||
|
if (t.settings && t.view?.webContents) {
|
||||||
|
try { t.view.webContents.send("settings-update", settings); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ipcMain.handle("set-chrome-height", (_e, h) => {
|
ipcMain.handle("set-chrome-height", (_e, h) => {
|
||||||
const next = Math.max(74, Math.min(260, Math.round(h) || 84));
|
const next = Math.max(74, Math.min(260, Math.round(h) || 84));
|
||||||
if (next !== CHROME_H) { CHROME_H = next; layout(); }
|
if (next !== CHROME_H) { CHROME_H = next; layout(); }
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "theseus-navigator",
|
"name": "theseus-navigator",
|
||||||
"version": "0.3.62",
|
"version": "0.3.63",
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
"description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.",
|
||||||
"author": "Silent Mode",
|
"author": "Silent Mode",
|
||||||
|
|
|
||||||
173
settings.html
173
settings.html
|
|
@ -271,7 +271,6 @@
|
||||||
<h1>General</h1>
|
<h1>General</h1>
|
||||||
<p class="lede">Changes apply immediately and are saved for next time.</p>
|
<p class="lede">Changes apply immediately and are saved for next time.</p>
|
||||||
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:0">Website language</h2>
|
<h2 class="sub" style="border-top:0;padding-top:0;margin-top:0">Website language</h2>
|
||||||
<p class="subd">The language sites serve you in — Wikipedia, YouTube, news sites, and anything else that has a localised version. Sent as your <code>Accept-Language</code> header and <code>navigator.language</code>. <b>Automatic</b> follows your operating system; pick a language to override it. The same setting lives under <b>Privacy → Anti-fingerprinting → Language</b> for advanced modes (Spoof, Hide).</p>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="txt"><div class="t">Preferred language</div><div class="d" id="webLangHint">Also switchable from the globe icon in the address bar.</div></div>
|
<div class="txt"><div class="t">Preferred language</div><div class="d" id="webLangHint">Also switchable from the globe icon in the address bar.</div></div>
|
||||||
<div class="ctl">
|
<div class="ctl">
|
||||||
|
|
@ -675,62 +674,25 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="txt"><div class="t">Language</div><div class="d">navigator.language and the Accept-Language header. <b>Spoof</b> reports a major world language; <b>Manual</b> lets you type any locale.</div></div>
|
<div class="txt"><div class="t">Language</div><div class="d">Same control as <b>General › Website language</b> and the globe chip in the URL bar.</div></div>
|
||||||
<div class="ctl">
|
<div class="ctl">
|
||||||
<select id="languageMode"><option value="show">Show real</option><option value="hide">Hide (en-US)</option><option value="spoof">Spoof (choose)</option><option value="manual">Manual…</option></select>
|
<select id="languageMode">
|
||||||
<select id="languageSpoof" hidden>
|
<option value="show">Automatic (system language)</option>
|
||||||
<option value="en-US">English</option>
|
<option value="manual">Manual…</option>
|
||||||
<option value="zh-CN">Chinese (中文)</option>
|
|
||||||
<option value="es-ES">Spanish (Español)</option>
|
|
||||||
<option value="hi-IN">Hindi (हिन्दी)</option>
|
|
||||||
<option value="ar">Arabic (العربية)</option>
|
|
||||||
<option value="pt-BR">Portuguese (Português)</option>
|
|
||||||
<option value="ru-RU">Russian (Русский)</option>
|
|
||||||
<option value="fr-FR">French (Français)</option>
|
|
||||||
<option value="de-DE">German (Deutsch)</option>
|
|
||||||
<option value="ja-JP">Japanese (日本語)</option>
|
|
||||||
</select>
|
|
||||||
<!-- Native <select> (same reason as timezone above). -->
|
|
||||||
<select id="languageValue" hidden>
|
|
||||||
<option value="en-US">English (US) — en-US</option>
|
|
||||||
<option value="en-GB">English (UK) — en-GB</option>
|
|
||||||
<option value="zh-CN">Chinese (Simplified) — zh-CN</option>
|
|
||||||
<option value="zh-TW">Chinese (Traditional) — zh-TW</option>
|
|
||||||
<option value="es-ES">Spanish (Spain) — es-ES</option>
|
|
||||||
<option value="es-MX">Spanish (Mexico) — es-MX</option>
|
|
||||||
<option value="pt-BR">Portuguese (Brazil) — pt-BR</option>
|
|
||||||
<option value="pt-PT">Portuguese (Portugal) — pt-PT</option>
|
|
||||||
<option value="fr-FR">French — fr-FR</option>
|
|
||||||
<option value="de-DE">German — de-DE</option>
|
|
||||||
<option value="it-IT">Italian — it-IT</option>
|
|
||||||
<option value="nl-NL">Dutch — nl-NL</option>
|
|
||||||
<option value="ru-RU">Russian — ru-RU</option>
|
|
||||||
<option value="ja-JP">Japanese — ja-JP</option>
|
|
||||||
<option value="ko-KR">Korean — ko-KR</option>
|
|
||||||
<option value="hi-IN">Hindi — hi-IN</option>
|
|
||||||
<option value="ar">Arabic — ar</option>
|
|
||||||
<option value="tr-TR">Turkish — tr-TR</option>
|
|
||||||
<option value="pl-PL">Polish — pl-PL</option>
|
|
||||||
<option value="uk-UA">Ukrainian — uk-UA</option>
|
|
||||||
<option value="sv-SE">Swedish — sv-SE</option>
|
|
||||||
<option value="fi-FI">Finnish — fi-FI</option>
|
|
||||||
<option value="el-GR">Greek — el-GR</option>
|
|
||||||
<option value="he-IL">Hebrew — he-IL</option>
|
|
||||||
<option value="vi-VN">Vietnamese — vi-VN</option>
|
|
||||||
<option value="th-TH">Thai — th-TH</option>
|
|
||||||
<option value="id-ID">Indonesian — id-ID</option>
|
|
||||||
<option value="__other__">Other…</option>
|
|
||||||
</select>
|
</select>
|
||||||
|
<!-- Populated in JS from WEB_LANG_QUICK (same list the General row + globe menu use). -->
|
||||||
|
<select id="languageValue" hidden></select>
|
||||||
<input id="languageValueOther" type="text" placeholder="BCP-47 tag, e.g. cs-CZ" hidden>
|
<input id="languageValueOther" type="text" placeholder="BCP-47 tag, e.g. cs-CZ" hidden>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="txt"><div class="t">Location</div><div class="d"><b>Hide</b> denies geolocation entirely; <b>Manual</b> reports the coordinates of a country you pick, so a page's <code>navigator.geolocation</code> answers with a plausible position instead of your real one.</div></div>
|
<div class="txt"><div class="t">Location</div><div class="d"><b>Hide</b> denies geolocation entirely; <b>Manual</b> reports the coordinates of a country you pick, so a page's <code>navigator.geolocation</code> answers with a plausible position instead of your real one.</div></div>
|
||||||
<div class="ctl">
|
<!-- Column layout so the country dropdown always lands on its own line
|
||||||
|
below the mode dropdown when Manual is picked — flex-row with a
|
||||||
|
60% max width was wrapping it where other open menus could cover it. -->
|
||||||
|
<div class="ctl" style="flex-direction:column;align-items:stretch">
|
||||||
<select id="locationMode"><option value="show">Show real</option><option value="hide">Hide (block)</option><option value="manual">Manual…</option></select>
|
<select id="locationMode"><option value="show">Show real</option><option value="hide">Hide (block)</option><option value="manual">Manual…</option></select>
|
||||||
<select id="locationCountry" hidden>
|
<select id="locationCountry" hidden><!-- Populated in JS from the COUNTRIES table --></select>
|
||||||
<!-- Populated in JS from the COUNTRIES table (kept in one place). -->
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="note">These reduce tracking and hide your IP, but a custom browser can still be fingerprinted. For maximum anonymity, use the Tor Browser.</div>
|
<div class="note">These reduce tracking and hide your IP, but a custom browser can still be fingerprinted. For maximum anonymity, use the Tor Browser.</div>
|
||||||
|
|
@ -829,6 +791,36 @@
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const C = window.cfg;
|
const C = window.cfg;
|
||||||
|
// Shared language picker list (same as WEBSITE_LANGUAGE_QUICK in main.js and
|
||||||
|
// the globe menu). Used by General › Website language AND Privacy › Anti-
|
||||||
|
// fingerprinting › Language — both edit the same languageMode/languageValue
|
||||||
|
// setting, so they share the same list.
|
||||||
|
const WEB_LANG_LIST = [
|
||||||
|
{ tag: "en-GB", label: "English" },
|
||||||
|
{ tag: "es-ES", label: "Español" },
|
||||||
|
{ tag: "fr-FR", label: "Français" },
|
||||||
|
{ tag: "pt-PT", label: "Português" },
|
||||||
|
{ tag: "ru-RU", label: "Русский" },
|
||||||
|
{ tag: "de-DE", label: "Deutsch" },
|
||||||
|
{ tag: "it-IT", label: "Italiano" },
|
||||||
|
{ tag: "tr-TR", label: "Türkçe" },
|
||||||
|
{ tag: "pl-PL", label: "Polski" },
|
||||||
|
{ tag: "nl-NL", label: "Nederlands" },
|
||||||
|
{ tag: "el-GR", label: "Ελληνικά" },
|
||||||
|
{ tag: "cs-CZ", label: "Čeština" },
|
||||||
|
{ tag: "sv-SE", label: "Svenska" },
|
||||||
|
{ tag: "fi-FI", label: "Suomi" },
|
||||||
|
{ tag: "zh-CN", label: "中文(简体)" },
|
||||||
|
{ tag: "hi-IN", label: "हिन्दी" },
|
||||||
|
{ tag: "ar", label: "العربية" },
|
||||||
|
{ tag: "id-ID", label: "Bahasa Indonesia" },
|
||||||
|
{ tag: "ja-JP", label: "日本語" },
|
||||||
|
{ tag: "vi-VN", label: "Tiếng Việt" },
|
||||||
|
{ tag: "ko-KR", label: "한국어" },
|
||||||
|
{ tag: "th-TH", label: "ไทย" },
|
||||||
|
{ tag: "he-IL", label: "עברית" },
|
||||||
|
{ tag: "zh-TW", label: "中文(繁體)" },
|
||||||
|
];
|
||||||
// sidebar navigation
|
// sidebar navigation
|
||||||
// "naming" is intentionally absent: the Registries section was folded
|
// "naming" is intentionally absent: the Registries section was folded
|
||||||
// into General in 0.3.21. Leaving the id in this list threw a
|
// into General in 0.3.21. Leaving the id in this list threw a
|
||||||
|
|
@ -981,38 +973,10 @@
|
||||||
// same languageMode/languageValue setting the Anti-fingerprinting Language
|
// same languageMode/languageValue setting the Anti-fingerprinting Language
|
||||||
// row edits. "Auto" = languageMode="show" (follow OS); anything else
|
// row edits. "Auto" = languageMode="show" (follow OS); anything else
|
||||||
// switches to languageMode="manual" and pins languageValue.
|
// switches to languageMode="manual" and pins languageValue.
|
||||||
// Same list the toolbar globe menu uses (kept in sync with
|
// The language list is defined once at the top of the script (WEB_LANG_LIST)
|
||||||
// WEBSITE_LANGUAGE_QUICK in main.js). Only the original of each language
|
// so the Privacy › Anti-fingerprinting row and the globe menu use the same
|
||||||
// — English UK, Spanish Spain, Portuguese Portugal. Ordered by global
|
// order and labels. Alias here for the General row's init block.
|
||||||
// speakers, European languages first.
|
const WEB_LANG_QUICK = WEB_LANG_LIST;
|
||||||
const WEB_LANG_QUICK = [
|
|
||||||
// European (ranked by global speakers)
|
|
||||||
{ tag: "en-GB", label: "English" },
|
|
||||||
{ tag: "es-ES", label: "Español" },
|
|
||||||
{ tag: "fr-FR", label: "Français" },
|
|
||||||
{ tag: "pt-PT", label: "Português" },
|
|
||||||
{ tag: "ru-RU", label: "Русский" },
|
|
||||||
{ tag: "de-DE", label: "Deutsch" },
|
|
||||||
{ tag: "it-IT", label: "Italiano" },
|
|
||||||
{ tag: "tr-TR", label: "Türkçe" },
|
|
||||||
{ tag: "pl-PL", label: "Polski" },
|
|
||||||
{ tag: "nl-NL", label: "Nederlands" },
|
|
||||||
{ tag: "el-GR", label: "Ελληνικά" },
|
|
||||||
{ tag: "cs-CZ", label: "Čeština" },
|
|
||||||
{ tag: "sv-SE", label: "Svenska" },
|
|
||||||
{ tag: "fi-FI", label: "Suomi" },
|
|
||||||
// Non-European (ranked by global speakers)
|
|
||||||
{ tag: "zh-CN", label: "中文(简体)" },
|
|
||||||
{ tag: "hi-IN", label: "हिन्दी" },
|
|
||||||
{ tag: "ar", label: "العربية" },
|
|
||||||
{ tag: "id-ID", label: "Bahasa Indonesia" },
|
|
||||||
{ tag: "ja-JP", label: "日本語" },
|
|
||||||
{ tag: "vi-VN", label: "Tiếng Việt" },
|
|
||||||
{ tag: "ko-KR", label: "한국어" },
|
|
||||||
{ tag: "th-TH", label: "ไทย" },
|
|
||||||
{ tag: "he-IL", label: "עברית" },
|
|
||||||
{ tag: "zh-TW", label: "中文(繁體)" },
|
|
||||||
];
|
|
||||||
// Plain language name for a locale tag — no regional qualifier so en-US
|
// Plain language name for a locale tag — no regional qualifier so en-US
|
||||||
// reads as "English", not "American English" (one picker row per language,
|
// reads as "English", not "American English" (one picker row per language,
|
||||||
// so the regional half is noise). Pass just the base to Intl.DisplayNames.
|
// so the regional half is noise). Pass just the base to Intl.DisplayNames.
|
||||||
|
|
@ -1329,25 +1293,56 @@
|
||||||
return sel;
|
return sel;
|
||||||
}
|
}
|
||||||
const tzV = selectWithOther("timezoneValue", "timezoneValueOther");
|
const tzV = selectWithOther("timezoneValue", "timezoneValueOther");
|
||||||
const lgV = selectWithOther("languageValue", "languageValueOther");
|
|
||||||
const tzOther = document.getElementById("timezoneValueOther");
|
const tzOther = document.getElementById("timezoneValueOther");
|
||||||
const lgOther = document.getElementById("languageValueOther");
|
|
||||||
// "Manual" mode reveals both the select AND the Other input (if Other was picked).
|
// "Manual" mode reveals both the select AND the Other input (if Other was picked).
|
||||||
const applyTzVis = (manual) => { tzV.hidden = !manual; tzOther.hidden = !manual || tzV.value !== "__other__"; };
|
const applyTzVis = (manual) => { tzV.hidden = !manual; tzOther.hidden = !manual || tzV.value !== "__other__"; };
|
||||||
bind("timezoneMode", null, applyTzVis);
|
bind("timezoneMode", null, applyTzVis);
|
||||||
// language: fixed dropdown when spoofing (top 10), broader dropdown when manual
|
// Language — same languageMode/languageValue setting that General ›
|
||||||
|
// Website language and the globe chip edit. Simplified to two modes:
|
||||||
|
// Automatic (follow OS) and Manual (pick a language). Legacy "hide" and
|
||||||
|
// "spoof" values migrate to Automatic on first open so the dropdown
|
||||||
|
// always has a valid pick. The shared WEB_LANG_LIST populates the
|
||||||
|
// language select so the picker here matches the General row's.
|
||||||
const lngMode = document.getElementById("languageMode");
|
const lngMode = document.getElementById("languageMode");
|
||||||
const lngSpoof = document.getElementById("languageSpoof");
|
const lgV = document.getElementById("languageValue");
|
||||||
lngSpoof.value = s.languageSpoof || "en-US";
|
const lgOther = document.getElementById("languageValueOther");
|
||||||
lngSpoof.addEventListener("change", () => C.set("languageSpoof", lngSpoof.value));
|
lgV.innerHTML = WEB_LANG_LIST.map((L) => `<option value="${L.tag}">${L.label}</option>`).join("")
|
||||||
|
+ `<option value="__other__">Other…</option>`;
|
||||||
|
// Migrate legacy modes to the new two-option set on load.
|
||||||
|
let initMode = s.languageMode || "show";
|
||||||
|
if (initMode === "hide" || initMode === "spoof") {
|
||||||
|
C.set("languageMode", "show");
|
||||||
|
initMode = "show";
|
||||||
|
}
|
||||||
|
const savedTag = s.languageValue || "en-GB";
|
||||||
|
const known = new Set(WEB_LANG_LIST.map((L) => L.tag));
|
||||||
|
if (known.has(savedTag)) { lgV.value = savedTag; lgOther.hidden = true; }
|
||||||
|
else { lgV.value = "__other__"; lgOther.value = savedTag; }
|
||||||
const applyLng = () => {
|
const applyLng = () => {
|
||||||
lngSpoof.hidden = lngMode.value !== "spoof";
|
|
||||||
lgV.hidden = lngMode.value !== "manual";
|
lgV.hidden = lngMode.value !== "manual";
|
||||||
lgOther.hidden = lngMode.value !== "manual" || lgV.value !== "__other__";
|
lgOther.hidden = lngMode.value !== "manual" || lgV.value !== "__other__";
|
||||||
};
|
};
|
||||||
lngMode.value = s.languageMode || "show";
|
lngMode.value = initMode;
|
||||||
applyLng();
|
applyLng();
|
||||||
lngMode.addEventListener("change", () => { C.set("languageMode", lngMode.value); applyLng(); });
|
lngMode.addEventListener("change", () => { C.set("languageMode", lngMode.value); applyLng(); });
|
||||||
|
lgV.addEventListener("change", () => {
|
||||||
|
if (lgV.value === "__other__") { applyLng(); setTimeout(() => lgOther.focus(), 0); return; }
|
||||||
|
C.set("languageValue", lgV.value);
|
||||||
|
applyLng();
|
||||||
|
});
|
||||||
|
lgOther.addEventListener("change", () => {
|
||||||
|
const v = String(lgOther.value).trim();
|
||||||
|
if (v) C.set("languageValue", v);
|
||||||
|
});
|
||||||
|
// React when the globe chip or the General row writes to the same setting.
|
||||||
|
if (C.onSettingsUpdate) C.onSettingsUpdate((next) => {
|
||||||
|
const m = (next.languageMode === "hide" || next.languageMode === "spoof") ? "show" : (next.languageMode || "show");
|
||||||
|
lngMode.value = m;
|
||||||
|
const t = next.languageValue || "en-GB";
|
||||||
|
if (known.has(t)) { lgV.value = t; lgOther.hidden = true; }
|
||||||
|
else { lgV.value = "__other__"; lgOther.value = t; }
|
||||||
|
applyLng();
|
||||||
|
});
|
||||||
// Location: three modes — Show real / Hide / Manual (pick a country).
|
// Location: three modes — Show real / Hide / Manual (pick a country).
|
||||||
// Manual writes the country's representative lat/lon into locationLat +
|
// Manual writes the country's representative lat/lon into locationLat +
|
||||||
// locationLon, which is what navigator.geolocation returns to pages via
|
// locationLon, which is what navigator.geolocation returns to pages via
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue