From a3fb8917c3600c27c38acb1580fd03ef31fb7c38 Mon Sep 17 00:00:00 2001 From: Silent Mode Date: Thu, 1 Oct 2026 00:48:19 +0200 Subject: [PATCH] =?UTF-8?q?Theseus=200.3.61:=20Privacy=20tidied=20?= =?UTF-8?q?=E2=80=94=20country=20dropdown,=20VPN=20row=20honest,=20languag?= =?UTF-8?q?e=20names=20in=20the=20picker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Privacy › Location is three modes now: Show real, Hide, Manual. Manual reveals a 50-country dropdown whose pick becomes the coordinates navigator.geolocation returns to pages — country-capital granularity, no regions or free-form cities. Old profiles on the retired "Spoof (region)" auto-migrate to Manual + the region's representative country on first open, so nothing breaks. VPN row in Privacy stops opening the wrong add-on: the sidebar now no-ops on a specific panelId that isn't registered (used to silently substitute panels[0], which surfaced Aegis whenever the VPN add-on was disabled), and the row hides itself when vpn:main isn't in the sidebar panel list. Language picker (globe chip menu + Settings › General › Website language) drops the BCP-47 tag from every visible label — the tag surfaces only as the 2-letter chip in the URL bar once picked. "English" is the UK original; the US variant row is retired (same 2-letter chip, ~same text). Ukrainian dropped from the quick list too. "Automatic" reads as the OS language name (Intl.DisplayNames) instead of a raw en-US style tag. --- chrome.html | 8 +- main.js | 34 +++++-- package.json | 2 +- settings.html | 261 ++++++++++++++++++++++++++++---------------------- 4 files changed, 177 insertions(+), 128 deletions(-) diff --git a/chrome.html b/chrome.html index 151f8ac9..366df2f4 100644 --- a/chrome.html +++ b/chrome.html @@ -723,20 +723,24 @@ const langBtn = $("langBtn"), langCode = $("langCode"); let systemLocaleCached = "en-US"; function short(tag) { return String(tag || "").split("-")[0].toUpperCase().slice(0, 2) || "??"; } + function langName(tag) { + try { return new Intl.DisplayNames(["en"], { type: "language" }).of(tag) || tag; } + catch { return tag; } + } function renderLang(s) { if (!s) return; const auto = (s.languageMode || "show") === "show"; if (auto) { langBtn.classList.remove("on"); langCode.textContent = "AUTO"; - langBtn.title = `Website language: automatic (OS: ${systemLocaleCached}). Click to change.`; + langBtn.title = `Website language: automatic — ${langName(systemLocaleCached)}. Click to change.`; } else { const tag = (s.languageMode === "manual" && s.languageValue) ? s.languageValue : (s.languageMode === "spoof" && s.languageSpoof) ? s.languageSpoof : (s.languageMode === "hide") ? "en-US" : systemLocaleCached; langBtn.classList.add("on"); langCode.textContent = short(tag); - langBtn.title = `Websites see you in ${tag}. Click to change.`; + langBtn.title = `Websites see you in ${langName(tag)}. Click to change.`; } } langBtn.onclick = () => { diff --git a/main.js b/main.js index ffecc380..71022fff 100644 --- a/main.js +++ b/main.js @@ -463,8 +463,8 @@ const SETTINGS_DEFAULTS = { clearStorageOnQuit: true, // drop localStorage / IndexedDB / service workers / cache API // Anti-fingerprinting — each: show (real) | hide (neutral) | spoof (auto decoy) | manual (user value) timezoneMode: "show", timezoneValue: "Europe/Berlin", // IANA zone for manual - languageMode: "show", languageSpoof: "en-US", languageValue: "en-US", // spoof = top-10 pick, manual = free text - locationMode: "hide", locationRegion: "europe", locationLat: "40.7128", locationLon: "-74.0060", // spoof by region, or manual coords + languageMode: "show", languageSpoof: "en-US", languageValue: "en-GB", // spoof = top-10 pick, manual = free text (English = UK original; US variant retired from the picker) + locationMode: "hide", locationRegion: "europe", locationCountry: "DE", locationLat: "40.7128", locationLon: "-74.0060", // manual = pick a country (locationCountry drives lat/lon); legacy spoof/region still handled by effLocation() for old profiles searchEngine: "startpage",// default search engine (built-in id or a custom id) installedEngines: DEFAULT_ENABLED.slice(), // built-in engines added to the user's list (visible in Settings) enabledEngines: DEFAULT_ENABLED.slice(), // subset that's currently toggled on (shown in the toolbar dropdown) @@ -2853,7 +2853,15 @@ function setSidebar(show, panelId) { } if (show) { const wantId = panelId || sidebarActivePanelId || panels[0].panelId; - const panel = panels.find((p) => p.panelId === wantId) || panels[0]; + // A CALLER-supplied panelId that isn't registered used to silently fall + // back to panels[0], which surfaced the wrong add-on (Settings › Privacy + // › VPN opening Aegis whenever the VPN add-on was disabled). Fall back + // only when the id came from restore state; a specific request is a no-op. + let panel = panels.find((p) => p.panelId === wantId); + if (!panel) { + if (panelId) { console.warn(`setSidebar: no panel "${panelId}"; ignoring`); return; } + panel = panels[0]; + } if (sidebarActivePanelId !== panel.panelId) { sidebarActivePanelId = panel.panelId; try { sidebar.webContents.loadFile(panel.pageFile); } catch (e) { console.warn("sidebar loadFile failed:", e?.message); } @@ -4050,9 +4058,12 @@ ipcMain.handle("tab-context-menu-popup", (e, tabId, rect) => { // (languageMode="show", follow OS) or a specific BCP-47 tag // (languageMode="manual", languageValue=). One truth source, two entry // points. Applied via applyAcceptLanguage() + applyFingerprintAll() below. +// Labels are the language's own name in its own script — no BCP-47 tag in +// the label. The tag only surfaces as the 2-letter code on the URL-bar chip +// once picked. English is the UK original (en-GB); the US variant is 90%+ +// the same text and shared the "EN" chip code, so it isn't a separate row. const WEBSITE_LANGUAGE_QUICK = [ - { tag: "en-US", label: "English (US)" }, - { tag: "en-GB", label: "English (UK)" }, + { tag: "en-GB", label: "English" }, { tag: "es-ES", label: "Español" }, { tag: "pt-BR", label: "Português (Brasil)" }, { tag: "fr-FR", label: "Français" }, @@ -4060,7 +4071,6 @@ const WEBSITE_LANGUAGE_QUICK = [ { tag: "it-IT", label: "Italiano" }, { tag: "nl-NL", label: "Nederlands" }, { tag: "ru-RU", label: "Русский" }, - { tag: "uk-UA", label: "Українська" }, { tag: "pl-PL", label: "Polski" }, { tag: "tr-TR", label: "Türkçe" }, { tag: "ar", label: "العربية" }, @@ -4087,6 +4097,12 @@ function setWebsiteLanguage(mode, value) { applyAcceptLanguage(); try { chrome?.webContents.send("settings-update", settings); } catch {} } +// Human-readable name for a BCP-47 tag ("de" → "German") via V8 Intl. +// Falls back to the tag itself if the locale isn't recognised. +function languageNameFor(tag) { + try { return new Intl.DisplayNames(["en"], { type: "language" }).of(tag) || tag; } + catch { return tag; } +} ipcMain.handle("system-locale", () => app.getLocale() || "en-US"); ipcMain.handle("website-language-menu-popup", (e, rect) => { if (chrome && e.sender !== chrome.webContents) throw new Error("website-language-menu-popup: untrusted sender"); @@ -4094,11 +4110,11 @@ ipcMain.handle("website-language-menu-popup", (e, rect) => { const isAuto = settings.languageMode === "show"; const current = isAuto ? null : (settings.languageValue || "").toLowerCase(); const template = [ - { label: `Automatic (OS: ${osLoc})${isAuto ? " ✓" : ""}`, + { label: `Automatic (${languageNameFor(osLoc)})${isAuto ? " ✓" : ""}`, click: () => setWebsiteLanguage("show") }, { type: "separator" }, ...WEBSITE_LANGUAGE_QUICK.map((L) => ({ - label: `${L.label} — ${L.tag}${current === L.tag.toLowerCase() ? " ✓" : ""}`, + label: `${L.label}${current === L.tag.toLowerCase() ? " ✓" : ""}`, click: () => setWebsiteLanguage("manual", L.tag), })), { type: "separator" }, @@ -6099,7 +6115,7 @@ ipcMain.handle("settings-set", (_e, key, val) => { if (key === "theme") applyTheme(); if (key === "backgroundThrottle") applyThrottle(); if (["timezoneMode", "timezoneValue", "languageMode", "languageSpoof", "languageValue", - "locationMode", "locationRegion", "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, // searchBoxSize) take effect without a relaunch. Whole settings object // fits in one message and chrome only cares about a handful of fields. diff --git a/package.json b/package.json index 949daeed..7046928b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.3.60", + "version": "0.3.61", "license": "MPL-2.0", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "author": "Silent Mode", diff --git a/settings.html b/settings.html index e67d4e94..0e19ec4c 100644 --- a/settings.html +++ b/settings.html @@ -684,57 +684,12 @@
-
Location
Hide denies geolocation; Spoof reports a chosen region; Manual reports exact coordinates.
+
Location
Hide denies geolocation entirely; Manual reports the coordinates of a country you pick, so a page's navigator.geolocation answers with a plausible position instead of your real one.
- - + - - -
These reduce tracking and hide your IP, but a custom browser can still be fingerprinted. For maximum anonymity, use the Tor Browser.
@@ -935,7 +890,19 @@ } const refreshAll = () => refresh().then(refreshExtra, refreshExtra); el("torOn").addEventListener("change", () => { if (C.toggleTor) C.toggleTor().catch(() => {}); setTimeout(refreshExtra, 1200); setTimeout(refreshExtra, 4000); }); - el("vpnRow").addEventListener("click", () => C.openPanel && C.openPanel("vpn:main")); + // VPN row: only offer it when the VPN add-on is installed AND its panel + // registered. Otherwise clicking it used to open whichever add-on was + // first in the sidebar list (usually Aegis). Hide it in that case. + (async () => { + const vpnRow = el("vpnRow"); + try { + const list = await (C.listAddons ? C.listAddons() : Promise.resolve({})); + const panels = (list && list.sidebarPanels) || []; + const vpn = panels.find((p) => p.panelId === "vpn:main"); + if (!vpn) { vpnRow.hidden = true; return; } + vpnRow.addEventListener("click", () => C.openPanel && C.openPanel("vpn:main")); + } catch { vpnRow.hidden = true; } + })(); document.addEventListener("section", (e) => { if (String(e.detail).startsWith("privacy")) refreshAll(); }); refreshAll(); })(); @@ -973,13 +940,18 @@ // same languageMode/languageValue setting the Anti-fingerprinting Language // row edits. "Auto" = languageMode="show" (follow OS); anything else // switches to languageMode="manual" and pins languageValue. + // Same list the toolbar globe menu uses (kept in sync with + // WEBSITE_LANGUAGE_QUICK in main.js). Language name in its own script; + // the BCP-47 tag only surfaces as the 2-letter chip in the URL bar once + // picked, never in the picker. English is the UK original (en-GB) — + // the US variant is 90%+ the same text and shared "EN" anyway. const WEB_LANG_QUICK = [ - { tag: "en-US", label: "English (US)" }, { tag: "en-GB", label: "English (UK)" }, + { tag: "en-GB", label: "English" }, { tag: "es-ES", label: "Español (España)" }, { tag: "es-MX", label: "Español (México)" }, { tag: "pt-BR", label: "Português (Brasil)" }, { tag: "pt-PT", label: "Português (Portugal)" }, { tag: "fr-FR", label: "Français" }, { tag: "de-DE", label: "Deutsch" }, { tag: "it-IT", label: "Italiano" }, { tag: "nl-NL", label: "Nederlands" }, - { tag: "ru-RU", label: "Русский" }, { tag: "uk-UA", label: "Українська" }, + { tag: "ru-RU", label: "Русский" }, { tag: "pl-PL", label: "Polski" }, { tag: "cs-CZ", label: "Čeština" }, { tag: "sv-SE", label: "Svenska" }, { tag: "fi-FI", label: "Suomi" }, { tag: "tr-TR", label: "Türkçe" }, { tag: "el-GR", label: "Ελληνικά" }, @@ -989,6 +961,13 @@ { tag: "ko-KR", label: "한국어" }, { tag: "vi-VN", label: "Tiếng Việt" }, { tag: "th-TH", label: "ไทย" }, { tag: "id-ID", label: "Bahasa Indonesia" }, ]; + // "English" for a locale tag ("en-US" → "English", "de-DE" → "German") via + // browser Intl. Used for the "Automatic (…)" label so it reads as a + // language name rather than a raw tag. + const langNameFor = (tag) => { + try { return new Intl.DisplayNames(["en"], { type: "language" }).of(tag) || tag; } + catch { return tag; } + }; (async () => { const webLangPick = document.getElementById("webLangPick"); const webLangOther = document.getElementById("webLangOther"); @@ -997,8 +976,10 @@ let osLoc = "en-US"; try { osLoc = (C.systemLocale && await C.systemLocale()) || "en-US"; } catch {} // Build options: "Automatic" first, then the quick list, then "Other…". - const opts = [``, - ...WEB_LANG_QUICK.map((L) => ``), + // No BCP-47 tag in visible labels — the tag surfaces only as the 2-letter + // chip in the URL bar. + const opts = [``, + ...WEB_LANG_QUICK.map((L) => ``), ``]; webLangPick.innerHTML = opts.join(""); const paint = (cur) => { @@ -1006,15 +987,15 @@ if (mode !== "manual") { webLangPick.value = "__auto__"; webLangOther.hidden = true; hint.innerHTML = mode === "show" - ? "Following your operating system. Also switchable from the globe icon in the address bar." + ? `Following your operating system (${langNameFor(osLoc)}). Also switchable from the globe icon in the address bar.` : `Anti-fingerprinting is currently overriding this (mode: ${mode}). Change it under Privacy → Anti-fingerprinting.`; return; } - const tag = cur.languageValue || "en-US"; + const tag = cur.languageValue || "en-GB"; const known = new Set(WEB_LANG_QUICK.map((L) => L.tag)); if (known.has(tag)) { webLangPick.value = tag; webLangOther.hidden = true; } else { webLangPick.value = "__other__"; webLangOther.hidden = false; webLangOther.value = tag; } - hint.textContent = `Websites see you in ${tag}. Also switchable from the globe icon in the address bar.`; + hint.textContent = `Websites see you in ${langNameFor(tag)}. Also switchable from the globe icon in the address bar.`; }; paint(s); webLangPick.addEventListener("change", () => { @@ -1310,68 +1291,116 @@ lngMode.value = s.languageMode || "show"; applyLng(); lngMode.addEventListener("change", () => { C.set("languageMode", lngMode.value); applyLng(); }); - // location: region dropdown when spoofing, city dropdown when manual. - // Manual writes lat/lon into settings via the CITIES map below; the raw - // coord inputs were dropped from the UI (nobody types them by hand). - const locMode = document.getElementById("locationMode"); - const locRegion = document.getElementById("locationRegion"); - const locCity = document.getElementById("locationCity"); - locRegion.value = s.locationRegion || "europe"; - locRegion.addEventListener("change", () => C.set("locationRegion", locRegion.value)); - // Location manual reveals the city select; picking "Other…" reveals a - // lat/lon pair, whose values are pushed to locationLat/Lon settings. - const locOther = document.getElementById("locationOther"); - const locLatOther = document.getElementById("locationLatOther"); - const locLonOther = document.getElementById("locationLonOther"); - const applyLoc = () => { - locRegion.hidden = locMode.value !== "spoof"; - locCity.hidden = locMode.value !== "manual"; - locOther.hidden = locMode.value !== "manual" || locCity.value !== "__other__"; + // Location: three modes — Show real / Hide / Manual (pick a country). + // Manual writes the country's representative lat/lon into locationLat + + // locationLon, which is what navigator.geolocation returns to pages via + // the override in main.js. Legacy `spoof` (region) and free-form city + // picks are gone from the UI; effLocation() in main still handles a + // saved `spoof` mode so old profiles keep working until they change it. + // COUNTRIES: ISO-3166 alpha-2 → [ label, lat, lon ]. Coord is the + // country's capital (or biggest city) so pages that geocode the position + // land in the right country. Alphabetised for the dropdown. + const COUNTRIES = { + AR: ["Argentina", -34.6037, -58.3816], + AT: ["Austria", 48.2082, 16.3738], + AU: ["Australia", -33.8688, 151.2093], + BE: ["Belgium", 50.8503, 4.3517], + BR: ["Brazil", -23.5505, -46.6333], + CA: ["Canada", 43.6532, -79.3832], + CH: ["Switzerland", 47.3769, 8.5417], + CL: ["Chile", -33.4489, -70.6693], + CN: ["China", 31.2304, 121.4737], + CO: ["Colombia", 4.7110, -74.0721], + CZ: ["Czechia", 50.0755, 14.4378], + DE: ["Germany", 52.5200, 13.4050], + DK: ["Denmark", 55.6761, 12.5683], + EG: ["Egypt", 30.0444, 31.2357], + ES: ["Spain", 40.4168, -3.7038], + FI: ["Finland", 60.1699, 24.9384], + FR: ["France", 48.8566, 2.3522], + GB: ["United Kingdom", 51.5074, -0.1278], + GR: ["Greece", 37.9838, 23.7275], + HK: ["Hong Kong", 22.3193, 114.1694], + ID: ["Indonesia", -6.2088, 106.8456], + IE: ["Ireland", 53.3498, -6.2603], + IL: ["Israel", 32.0853, 34.7818], + IN: ["India", 19.0760, 72.8777], + IT: ["Italy", 41.9028, 12.4964], + JP: ["Japan", 35.6762, 139.6503], + KE: ["Kenya", -1.2921, 36.8219], + KR: ["South Korea", 37.5665, 126.9780], + MA: ["Morocco", 33.5731, -7.5898], + MX: ["Mexico", 19.4326, -99.1332], + MY: ["Malaysia", 3.1390, 101.6869], + NG: ["Nigeria", 6.5244, 3.3792], + NL: ["Netherlands", 52.3676, 4.9041], + NO: ["Norway", 59.9139, 10.7522], + NZ: ["New Zealand", -36.8485, 174.7633], + PE: ["Peru", -12.0464, -77.0428], + PH: ["Philippines", 14.5995, 120.9842], + PL: ["Poland", 52.2297, 21.0122], + PT: ["Portugal", 38.7223, -9.1393], + RU: ["Russia", 55.7558, 37.6173], + SA: ["Saudi Arabia", 24.7136, 46.6753], + SE: ["Sweden", 59.3293, 18.0686], + SG: ["Singapore", 1.3521, 103.8198], + TH: ["Thailand", 13.7563, 100.5018], + TR: ["Turkey", 41.0082, 28.9784], + TW: ["Taiwan", 25.0330, 121.5654], + UA: ["Ukraine", 50.4501, 30.5234], + US: ["United States", 40.7128, -74.0060], + VN: ["Vietnam", 10.8231, 106.6297], + ZA: ["South Africa", -26.2041, 28.0473], }; - locMode.value = s.locationMode || "show"; + // Legacy region → representative country (Spoof mode is retired but + // migrating old profiles keeps them roughly where they were). + const REGION_TO_COUNTRY = { + europe: "DE", asia: "JP", north_america: "US", south_america: "BR", + africa: "KE", middle_east: "AE", australia: "AU", + }; + const locMode = document.getElementById("locationMode"); + const locCountry = document.getElementById("locationCountry"); + // Build the country dropdown from the map above. + locCountry.innerHTML = Object.entries(COUNTRIES) + .sort((a, b) => a[1][0].localeCompare(b[1][0])) + .map(([code, [name]]) => ``) + .join(""); + // Restore prior selection: prefer an explicit locationCountry; otherwise + // recover the country whose coords match the saved lat/lon; else Germany. + const initialCountry = (() => { + if (s.locationCountry && COUNTRIES[s.locationCountry]) return s.locationCountry; + const la = Number(s.locationLat), lo = Number(s.locationLon); + for (const [code, [, x, y]] of Object.entries(COUNTRIES)) { + if (Math.abs(x - la) < 0.05 && Math.abs(y - lo) < 0.05) return code; + } + return "DE"; + })(); + locCountry.value = initialCountry; + // Migrate legacy spoof/region on this render pass so the mode dropdown + // has a valid value and the settings file is normalised on next save. + const initialMode = (() => { + if (s.locationMode === "spoof") { + const code = REGION_TO_COUNTRY[s.locationRegion] || "DE"; + const [, la, lo] = COUNTRIES[code]; + C.set("locationCountry", code); + C.set("locationLat", String(la)); + C.set("locationLon", String(lo)); + C.set("locationMode", "manual"); + locCountry.value = code; + return "manual"; + } + return s.locationMode || "show"; + })(); + const applyLoc = () => { locCountry.hidden = locMode.value !== "manual"; }; + locMode.value = initialMode; applyLoc(); locMode.addEventListener("change", () => { C.set("locationMode", locMode.value); applyLoc(); }); - locCity.addEventListener("change", applyLoc); - // Prefill Other lat/lon inputs from saved settings. - locLatOther.value = s.locationLat ?? ""; - locLonOther.value = s.locationLon ?? ""; - const saveOtherCoords = () => { - const la = String(locLatOther.value).trim(); - const lo = String(locLonOther.value).trim(); - if (la !== "") C.set("locationLat", la); - if (lo !== "") C.set("locationLon", lo); - C.set("locationCity", "__other__"); - }; - locLatOther.addEventListener("change", saveOtherCoords); - locLonOther.addEventListener("change", saveOtherCoords); - // If the previously-saved city was "__other__", set the select to it so the - // Other inputs stay visible on reload. - if (s.locationCity === "__other__") locCity.value = "__other__"; - // Picking a city writes its lat/lon to settings — that's what - // navigator.geolocation returns to pages once "manual" mode is active. - const CITIES = { - london:[51.5074,-0.1278], berlin:[52.52,13.405], paris:[48.8566,2.3522], - madrid:[40.4168,-3.7038], rome:[41.9028,12.4964], moscow:[55.7558,37.6173], - istanbul:[41.0082,28.9784], dubai:[25.2048,55.2708], mumbai:[19.076,72.8777], - singapore:[1.3521,103.8198], bangkok:[13.7563,100.5018], shanghai:[31.2304,121.4737], - tokyo:[35.6762,139.6503], seoul:[37.5665,126.978], sydney:[-33.8688,151.2093], - new_york:[40.7128,-74.006], los_angeles:[34.0522,-118.2437], chicago:[41.8781,-87.6298], - toronto:[43.6532,-79.3832], mexico_city:[19.4326,-99.1332], sao_paulo:[-23.5505,-46.6333], - buenos_aires:[-34.6037,-58.3816], cairo:[30.0444,31.2357], nairobi:[-1.2921,36.8219], - johannesburg:[-26.2041,28.0473], - }; - // Restore prior city selection when possible by matching stored lat/lon. - const restoreCity = () => { - const la = Number(s.locationLat), lo = Number(s.locationLon); - for (const [key, [x, y]] of Object.entries(CITIES)) - if (Math.abs(x - la) < 0.01 && Math.abs(y - lo) < 0.01) { locCity.value = key; return; } - }; - restoreCity(); - locCity.addEventListener("change", () => { - const c = CITIES[locCity.value]; if (!c) return; - C.set("locationLat", String(c[0])); - C.set("locationLon", String(c[1])); - C.set("locationCity", locCity.value); // save the pick so we can restore it later + locCountry.addEventListener("change", () => { + const code = locCountry.value; + const c = COUNTRIES[code]; if (!c) return; + C.set("locationCountry", code); + C.set("locationLat", String(c[1])); + C.set("locationLon", String(c[2])); }); // Storage: "Clear all now" wipes everything the toggles cover, without