diff --git a/chrome.html b/chrome.html index 87fad454..151f8ac9 100644 --- a/chrome.html +++ b/chrome.html @@ -187,6 +187,20 @@ display: grid; place-items: center; line-height: 1; box-shadow: 0 0 0 2px var(--bg); } .dlbtn.done .dlbadge { background: #4fd1a5; } .dlbtn.err .dlbadge { background: #f6768a; } + /* Website-language chip inside the address bar (Firefox-style, next to the + bookmark star). Two-letter uppercase code shows the language sites see us + in — Accept-Language + navigator.language. Same setting the Anti- + fingerprinting Language row edits; both stay in sync via settings-update. */ + .langchip { flex: none; display: inline-flex; align-items: center; gap: 3px; + height: 24px; padding: 0 5px 0 4px; border: none; border-radius: 6px; + background: transparent; cursor: pointer; color: var(--dim); + font: 600 10.5px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; + letter-spacing: .04em; } + .langchip:hover { background: var(--line2); color: var(--ink); } + .langchip.on { color: var(--acid); } + .langchip svg { width: 15px; height: 15px; fill: none; stroke: currentColor; + stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; } + .langchip .lcode { min-width: 16px; text-align: center; } /* indeterminate loading bar under the toolbar — collapses when idle so it doesn't take a permanent 2px strip below the address bar */ .loadbar { height: 0; overflow: hidden; background: transparent; transition: height .12s linear; } @@ -286,7 +300,7 @@ its width and pushed the registry button and the star out past the bar's right edge. */ #url { flex: 1 1 0; min-width: 0; } - .secbadge, .pwchip, .star, .regbtn, .zoomchip { flex: none; } + .secbadge, .pwchip, .star, .regbtn, .zoomchip, .langchip { flex: none; } /* Zoom indicator — only shown when the active tab isn't at 100 %. Click resets (Ctrl+0); Ctrl +/- and Ctrl+wheel step the level. */ .zoomchip { border: none; background: transparent; color: var(--mut); cursor: pointer; padding: 2px 5px; @@ -490,6 +504,13 @@ makes it installable (Chrome's omnibox icon). Filled once the app is installed; click then opens it or removes it. --> + +
@@ -694,6 +715,41 @@ T.getDownloads && T.getDownloads().then(renderDownloads); T.onDownloads && T.onDownloads(renderDownloads); + // ---- website-language pill: shows the language sites see us in ---- + // "AUTO" (2-letter tone) when following the OS locale; a two-letter + // uppercase code (e.g. "DE", "PT") when a specific language is pinned. + // Same setting the Anti-fingerprinting Language row edits; both stay in + // sync via T.onSettingsUpdate below. + const langBtn = $("langBtn"), langCode = $("langCode"); + let systemLocaleCached = "en-US"; + function short(tag) { return String(tag || "").split("-")[0].toUpperCase().slice(0, 2) || "??"; } + 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.`; + } 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.onclick = () => { + const r = langBtn.getBoundingClientRect(); + T.websiteLanguageMenu && T.websiteLanguageMenu({ x: Math.round(r.left), y: Math.round(r.bottom + 4) }); + }; + // Locale is optional (older main.js won't expose it) — render on settings + // either way so the chip works even without it. + (T.systemLocale ? T.systemLocale() : Promise.resolve("en-US")) + .then((loc) => { systemLocaleCached = loc || "en-US"; return T.getSettings(); }) + .then(renderLang).catch(() => {}); + T.onSettingsUpdate && T.onSettingsUpdate(renderLang); + // ---- extension dock ---- // One toolbar button per registered addon sidebar-panel, plus a static // "coming soon" placeholder for Aegis (the built-in BCH wallet, in diff --git a/main.js b/main.js index ddc7f8d0..ffecc380 100644 --- a/main.js +++ b/main.js @@ -4045,6 +4045,72 @@ ipcMain.handle("tab-context-menu-popup", (e, tabId, rect) => { popup.popup({ window: win, x, y }); return true; }); +// Website-language quick switch — a native menu anchored under the toolbar +// pill. Same setting the Anti-fingerprinting Language row edits: "auto" +// (languageMode="show", follow OS) or a specific BCP-47 tag +// (languageMode="manual", languageValue=). One truth source, two entry +// points. Applied via applyAcceptLanguage() + applyFingerprintAll() below. +const WEBSITE_LANGUAGE_QUICK = [ + { tag: "en-US", label: "English (US)" }, + { tag: "en-GB", label: "English (UK)" }, + { tag: "es-ES", label: "Español" }, + { tag: "pt-BR", label: "Português (Brasil)" }, + { 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: "pl-PL", label: "Polski" }, + { tag: "tr-TR", label: "Türkçe" }, + { tag: "ar", label: "العربية" }, + { tag: "he-IL", label: "עברית" }, + { tag: "el-GR", label: "Ελληνικά" }, + { tag: "zh-CN", label: "中文(简体)" }, + { tag: "zh-TW", label: "中文(繁體)" }, + { tag: "ja-JP", label: "日本語" }, + { tag: "ko-KR", label: "한국어" }, + { tag: "hi-IN", label: "हिन्दी" }, + { tag: "vi-VN", label: "Tiếng Việt" }, + { tag: "th-TH", label: "ไทย" }, + { tag: "id-ID", label: "Bahasa Indonesia" }, +]; +function setWebsiteLanguage(mode, value) { + if (mode === "show") { + settings.languageMode = "show"; + } else if (mode === "manual" && value) { + settings.languageMode = "manual"; + settings.languageValue = String(value); + } else return; + saveSettings(); + applyFingerprintAll(); + applyAcceptLanguage(); + try { chrome?.webContents.send("settings-update", settings); } catch {} +} +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"); + const osLoc = app.getLocale() || "en-US"; + const isAuto = settings.languageMode === "show"; + const current = isAuto ? null : (settings.languageValue || "").toLowerCase(); + const template = [ + { label: `Automatic (OS: ${osLoc})${isAuto ? " ✓" : ""}`, + click: () => setWebsiteLanguage("show") }, + { type: "separator" }, + ...WEBSITE_LANGUAGE_QUICK.map((L) => ({ + label: `${L.label} — ${L.tag}${current === L.tag.toLowerCase() ? " ✓" : ""}`, + click: () => setWebsiteLanguage("manual", L.tag), + })), + { type: "separator" }, + { label: "More languages…", click: () => { try { openSettingsTab("general"); } catch {} } }, + ]; + const popup = Menu.buildFromTemplate(template); + const chromeBounds = chrome ? chrome.getBounds() : { x: 0, y: 0 }; + const x = Math.max(0, Math.round(chromeBounds.x + (rect?.x || 0))); + const y = Math.max(0, Math.round(chromeBounds.y + (rect?.y || 0))); + popup.popup({ window: win, x, y }); + return true; +}); ipcMain.handle("move-tab", (_e, id, targetId, place) => { const src = tabs.findIndex((t) => t.id === id); const dst = tabs.findIndex((t) => t.id === targetId); diff --git a/package.json b/package.json index 1097c9ba..949daeed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.3.59", + "version": "0.3.60", "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/preload.js b/preload.js index b1158c71..0efce62d 100644 --- a/preload.js +++ b/preload.js @@ -38,6 +38,8 @@ contextBridge.exposeInMainWorld("theseus", { getSettings: () => ipcRenderer.invoke("settings-get"), setSetting: (key, val) => ipcRenderer.invoke("settings-set", key, val), onSettingsUpdate: (cb) => ipcRenderer.on("settings-update", (_e, d) => cb(d)), + systemLocale: () => ipcRenderer.invoke("system-locale"), + websiteLanguageMenu: (rect) => ipcRenderer.invoke("website-language-menu-popup", rect), // Find-in-page. main.js fires 'find-open' on Ctrl+F; chrome renderer // owns the bar UI and drives findInPage / stopFindInPage via these // wrappers. Match count / active ordinal comes back through onFindResult. diff --git a/settings-preload.js b/settings-preload.js index d5ebc34d..ff7bb4a7 100644 --- a/settings-preload.js +++ b/settings-preload.js @@ -31,23 +31,24 @@ contextBridge.exposeInMainWorld("cfg", { collisionState: () => ipcRenderer.invoke("collision-state"), setCollisionPolicy: (p) => ipcRenderer.invoke("collision-set-policy", p), resetCollisions: () => ipcRenderer.invoke("collision-reset"), - ariadneState: () => ipcRenderer.invoke("ariadne-state"), - ariadneToggle: (on) => ipcRenderer.invoke("ariadne-toggle", !!on), - ariadneInstall: () => ipcRenderer.invoke("ariadne-install"), - ariadneUpdate: () => ipcRenderer.invoke("ariadne-update"), - ariadneUninstall: () => ipcRenderer.invoke("ariadne-uninstall"), - // Manual "Check for updates" — un-dismisses any existing chip and re- - // fetches the release manifest. Returns { updateAvailable, currentVersion }. - recheckUpdate: () => ipcRenderer.invoke("recheck-update"), - appVersion: () => ipcRenderer.invoke("app-version"), - restartApp: () => ipcRenderer.invoke("app-restart"), // Add-ons management (Settings > Add-ons tab). listAddons: () => ipcRenderer.invoke("addons-list"), setAddonEnabled: (id, enabled) => ipcRenderer.invoke("addons-set-enabled", id, !!enabled), revealAddon: (folder) => ipcRenderer.invoke("addons-reveal", folder), // Delete a non-bundled extension's folder (Settings › Extensions › ⋯ › Remove). removeAddon: (id) => ipcRenderer.invoke("addons-remove", id), - // Install a staged signed update now (Settings › Extensions › "Update to vX"); no id = every staged extension. + openAddonsDir: () => ipcRenderer.invoke("addons-open-dir"), + reloadAddons: () => ipcRenderer.invoke("addons-reload"), + // Add-on update flow. checkAddonUpdates hits the release manifest and + // stages any newer signed version; listStagedAddonUpdates reports what's + // waiting; applyStagedAddons promotes staged → active and reactivates the + // addon host so the new bytes load without a full Theseus restart. + // Community extensions from theseus.x/extensions: the catalog (with what + // is installed already) and a verified install/update of one entry. + communityCatalog: () => ipcRenderer.invoke("addons-community-catalog"), + installCommunity: (id) => ipcRenderer.invoke("addons-install-community", id), + checkAddonUpdates: () => ipcRenderer.invoke("addons-check-updates"), + listStagedAddonUpdates: () => ipcRenderer.invoke("addons-list-staged"), applyStagedAddons: (id) => ipcRenderer.invoke("addons-apply-staged", typeof id === "string" ? id : undefined), // Settings › Performance › Protections: talk to an add-on's own message // handlers (Shield, Cookie Pop-ups) and open its panel for the details. @@ -57,12 +58,23 @@ contextBridge.exposeInMainWorld("cfg", { reportSection: (slug) => ipcRenderer.invoke("settings-section", String(slug || "")), torState: () => ipcRenderer.invoke("tor-state"), toggleTor: () => ipcRenderer.invoke("toggle-tor"), - openAddonsDir: () => ipcRenderer.invoke("addons-open-dir"), - reloadAddons: () => ipcRenderer.invoke("addons-reload"), - // Community extensions from theseus.x/extensions: the catalog (with what - // is installed already) and a verified install/update of one entry. - communityCatalog: () => ipcRenderer.invoke("addons-community-catalog"), - installCommunity: (id) => ipcRenderer.invoke("addons-install-community", id), - checkAddonUpdates: () => ipcRenderer.invoke("addons-check-updates"), - listStagedAddonUpdates: () => ipcRenderer.invoke("addons-list-staged"), + // OS locale — used by the Website-language row to label "Automatic (OS: …)". + systemLocale: () => ipcRenderer.invoke("system-locale"), + // Live settings updates — the toolbar chip, this page's Website-language + // row, and the Anti-fingerprinting Language row all edit the same setting; + // any of them writing pushes a "settings-update" the others react to. + onSettingsUpdate: (cb) => ipcRenderer.on("settings-update", (_e, d) => cb(d)), + // Ariadne's Thread plug-in (system-wide resolver). The state getter returns + // { state:"running"|"stopped"|"not-installed", installedVersion, bundledVersion, + // canUpdate, hasUninstaller }; the mutators prompt UAC for admin. + ariadneState: () => ipcRenderer.invoke("ariadne-state"), + ariadneToggle: (on) => ipcRenderer.invoke("ariadne-toggle", !!on), + ariadneInstall: () => ipcRenderer.invoke("ariadne-install"), + ariadneUpdate: () => ipcRenderer.invoke("ariadne-update"), + ariadneUninstall: () => ipcRenderer.invoke("ariadne-uninstall"), + // Manual "Check for updates" — un-dismisses any existing chip and re- + // fetches the release manifest. Returns { updateAvailable, currentVersion }. + recheckUpdate: () => ipcRenderer.invoke("recheck-update"), + appVersion: () => ipcRenderer.invoke("app-version"), + restartApp: () => ipcRenderer.invoke("app-restart"), }); diff --git a/settings.html b/settings.html index 96259653..e67d4e94 100644 --- a/settings.html +++ b/settings.html @@ -42,6 +42,16 @@ .srow::before{content:"";width:9px;height:9px;border-radius:50%;background:var(--dim);flex:none} .srow.ok::before{background:#3ddc84}.srow.warn::before{background:#f5a524} .row.link{cursor:pointer}.row.link:hover{border-color:rgb(from var(--acid) r g b / .4)}.row .chev{color:var(--dim);font-size:20px;line-height:1} + /* Plug-in main-page row: the .txt area is a click target that navigates to + the plug-in's own settings sub-page (chevron on the title); .plug-ctl on + the right hosts the update button + on/off toggle, and stops the click + from bubbling so they don't navigate. */ + .plug-row .plug-open{cursor:pointer;border-radius:8px;padding:2px 6px;margin:-2px -6px;transition:background .12s} + .plug-row .plug-open:hover{background:rgba(255,255,255,.04)} + .plug-row .plug-open:focus-visible{outline:2px solid rgb(from var(--acid) r g b / .5);outline-offset:2px} + .plug-row .plug-open .chev{color:var(--dim);font-size:15px;line-height:1;margin-left:4px;opacity:.6} + .plug-row .plug-open:hover .chev{opacity:1;color:var(--acid-text)} + .plug-row .plug-ctl{display:flex;align-items:center;gap:8px;flex:none;flex-wrap:wrap;justify-content:flex-end;max-width:60%} .exlist{display:flex;flex-direction:column;gap:6px;margin-bottom:14px} .exrow{display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:9px 14px;font:13px ui-monospace,"Cascadia Code",Consolas,monospace} .exempty{color:var(--dim);font-size:13px;margin:0 0 14px} @@ -241,7 +251,16 @@

General

Changes apply immediately and are saved for next time.

-

Startup

+

Website language

+

The language sites serve you in — Wikipedia, YouTube, news sites, and anything else that has a localised version. Sent as your Accept-Language header and navigator.language. Automatic follows your operating system; pick a language to override it. The same setting lives under Privacy → Anti-fingerprinting → Language for advanced modes (Spoof, Hide).

+
+
Preferred language
Also switchable from the globe icon in the address bar.
+
+ + +
+
+

Startup

Open previous windows and tabs
Restore the tabs from your last session when Theseus starts.
@@ -309,52 +328,107 @@
+ + + + + + +