diff --git a/main.js b/main.js index e0da04d..d374b82 100644 --- a/main.js +++ b/main.js @@ -83,7 +83,7 @@ const SEARCH_ENGINES = { }; // Engines enabled by default (shown in the toolbar dropdown). The rest are in the // catalog and can be turned on from Settings. Custom + detected engines are always on. -const DEFAULT_ENABLED = ["duckduckgo", "google", "brave", "bing", "startpage"]; +const DEFAULT_ENABLED = ["startpage", "duckduckgo", "google", "brave", "bing"]; // DuckDuckGo's icon service reliably returns a favicon for ANY domain from one // privacy-respecting host — far more robust than guessing /favicon.ico per site. const faviconUrl = (domain) => (domain ? `https://icons.duckduckgo.com/ip3/${domain}.ico` : null); @@ -203,7 +203,7 @@ const SETTINGS_DEFAULTS = { 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 - searchEngine: "duckduckgo",// default search engine (built-in id or a custom id) + 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) engineOrder: [], // user-defined display order of engine ids (empty = natural) @@ -1759,7 +1759,14 @@ function closeTab(id) { } function createWindow() { - win = new BrowserWindow({ width: 1220, height: 840, title: "Theseus Navigator", backgroundColor: "#0f1420" }); + win = new BrowserWindow({ + width: 1220, height: 840, title: "Theseus Navigator", backgroundColor: "#0f1420", + // Taskbar / titlebar icon. Packaged builds ship build/icon.ico as + // extraResource; dev reads the source file directly. + icon: app.isPackaged + ? path.join(process.resourcesPath, "icon.ico") + : path.join(__dirname, "build", "icon.ico"), + }); chrome = new WebContentsView({ webPreferences: { preload: path.join(__dirname, "preload.js") } }); win.contentView.addChildView(chrome); chrome.webContents.loadFile("chrome.html"); diff --git a/nsis/make-icons.mjs b/nsis/make-icons.mjs new file mode 100644 index 0000000..4d29618 --- /dev/null +++ b/nsis/make-icons.mjs @@ -0,0 +1,48 @@ +// One-shot script: render site-theseus-x/assets/favicon.svg to the icon +// files electron-builder wants — a 512x512 PNG (main icon) and a multi- +// resolution .ico (Windows taskbar / installer / shortcut icon). +// +// Not in the runtime app bundle; run manually when the brand changes: +// node nsis/make-icons.mjs +// Outputs build/icon.png and build/icon.ico (both gitignored regenerated). +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import sharp from "sharp"; +import pngToIco from "png-to-ico"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// SVG source lives in the theseus.x standalone site so the browser icon +// and the site brand stay in lockstep. +const SVG = path.join(__dirname, "..", "..", "site-theseus-x", "assets", "favicon.svg"); +// Icons drop into build/, which is gitignored (regenerated artifacts). +// electron-builder picks them up from there via package.json. +const OUT_DIR = path.join(__dirname, "..", "build"); +const PNG_512 = path.join(OUT_DIR, "icon.png"); +const ICO = path.join(OUT_DIR, "icon.ico"); +const TMP_DIR = path.join(OUT_DIR, ".icon-tmp"); +const ICO_SIZES = [16, 24, 32, 48, 64, 128, 256]; + +if (!fs.existsSync(SVG)) throw new Error("SVG not found at " + SVG); +console.log("source:", SVG); + +// Main 512x512 PNG for macOS / linux / electron-builder auto-derived Windows. +await sharp(SVG).resize(512, 512).png().toFile(PNG_512); +console.log("wrote", PNG_512); + +// Write individual sized PNGs to a temp dir, then feed their paths to +// png-to-ico so it preserves each as a distinct image inside the .ico. +fs.mkdirSync(TMP_DIR, { recursive: true }); +const paths = []; +for (const size of ICO_SIZES) { + const p = path.join(TMP_DIR, `icon-${size}.png`); + await sharp(SVG).resize(size, size).png().toFile(p); + paths.push(p); +} +const ico = await pngToIco(paths); +fs.writeFileSync(ICO, ico); +console.log("wrote", ICO, `(${ICO_SIZES.length} sizes)`); + +// Clean up the temp PNGs. +for (const p of paths) fs.unlinkSync(p); +fs.rmdirSync(TMP_DIR); diff --git a/package.json b/package.json index 66a59ed..4348e8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.2.2", + "version": "0.2.3", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "author": "Silent Mode", "main": "main.js", @@ -97,9 +97,14 @@ { "from": "bundled-addons", "to": "bundled-addons" + }, + { + "from": "build/icon.ico", + "to": "icon.ico" } ], "win": { + "icon": "build/icon.ico", "target": [ "nsis", "portable" @@ -110,7 +115,10 @@ "perMachine": false, "allowToChangeInstallationDirectory": true, "artifactName": "TheseusNavigator-Setup-${version}.${ext}", - "include": "nsis/installer.nsh" + "include": "nsis/installer.nsh", + "installerIcon": "build/icon.ico", + "uninstallerIcon": "build/icon.ico", + "installerHeaderIcon": "build/icon.ico" }, "portable": { "artifactName": "TheseusNavigator-${version}-portable.${ext}" diff --git a/sidebar-preload.js b/sidebar-preload.js index ede86cc..218b19e 100644 --- a/sidebar-preload.js +++ b/sidebar-preload.js @@ -15,12 +15,60 @@ contextBridge.exposeInMainWorld("silentmode", { onVisibility: (cb) => ipcRenderer.on("sidebar-visibility", (_e, visible) => cb(!!visible)), }); +// -- Panel picker strip ----------------------------------------------------- +// Injected at the top of every panel document so the user can switch between +// registered extension panels without a settings round-trip. Only appears +// when 2+ panels exist. Main pushes the panel list here via IPC on the +// initial sidebar-state event. +async function installPickerStrip() { + let state; + try { state = await ipcRenderer.invoke("sidebar-state"); } catch { return; } + const panels = (state && state.panels) || []; + if (panels.length < 2) return; + const strip = document.createElement("div"); + strip.setAttribute("aria-label", "Extension panels"); + strip.style.cssText = [ + "position:fixed", "top:0", "left:5px", "right:0", "height:32px", + "display:flex", "align-items:stretch", "gap:1px", + "background:rgba(11,14,20,.85)", "backdrop-filter:blur(8px)", + "border-bottom:1px solid rgba(255,255,255,.10)", + "z-index:2147483646", "user-select:none", + "font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif", + ].join(";"); + const active = state.active; + for (const p of panels) { + const tab = document.createElement("button"); + tab.type = "button"; + const isActive = p.panelId === active; + tab.style.cssText = [ + "flex:1", "min-width:0", "padding:0 10px", "border:0", + "background:" + (isActive ? "rgba(214,255,61,.10)" : "transparent"), + "color:" + (isActive ? "#d6ff3d" : "#8b98a9"), + "cursor:pointer", + "display:flex", "align-items:center", "gap:6px", "justify-content:center", + "overflow:hidden", "text-overflow:ellipsis", "white-space:nowrap", + "font:inherit", + ].join(";"); + tab.innerHTML = '' + (p.icon || "🧩") + '' + p.title.replace(/'; + tab.addEventListener("click", () => ipcRenderer.invoke("sidebar-open", p.panelId)); + strip.appendChild(tab); + } + document.body.appendChild(strip); + // Push the panel body down so the 32-px strip doesn't overlap the content. + // box-sizing:border-box keeps 100%-tall panels from overflowing when we + // add the padding. + const style = document.createElement("style"); + style.textContent = "html,body{box-sizing:border-box}body{padding-top:33px!important}"; + document.head.appendChild(style); +} + // Sidebar resize grip. Injected into every panel automatically so panel // authors don't have to reinvent it. A thin strip along the LEFT edge // (the boundary between the tab area and the sidebar) accepts mousedown // and streams drag deltas to main until mouseup. Main clamps the width // to [200, 800] and persists it in settings.sidebarWidth. window.addEventListener("DOMContentLoaded", () => { + installPickerStrip(); const grip = document.createElement("div"); grip.setAttribute("aria-label", "Resize sidebar"); grip.style.cssText = [