From 94ec382f203fbc82cdfff1884bebf66cc5041901 Mon Sep 17 00:00:00 2001 From: Silent Mode Date: Mon, 12 Oct 2026 01:41:17 +0200 Subject: [PATCH] =?UTF-8?q?Theseus=200.3.90:=20a=20desktop=20home=20page?= =?UTF-8?q?=20=E2=80=94=20two=20columns,=20brand=20icons,=20pick-and-move?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new-tab page is now a two-column desktop. The right column holds the curated Silent Mode X-family (Theseus.X, SilentMode.X, Sirius.X, Hephaestus.X, Prometheus.X, Hermes.X, Helios.X, Ariadne.X, Aegis.X) with real brand marks pulled from each product's own favicon or add-on manifest. The left column is yours — hello.bch, sia.coin, Pithos.sia, Bergamot.X by default, plus whatever you add. Icons pick up and move within and across columns in edit mode. Edit mode also carries per-column default-size pickers (S / M / L / XL), six background presets + a custom colour + an image upload, and + slots in each column. The Ariadne.X mark uses the real face/thread illustration in ancient-Greek-pottery colours (terracotta rings on cream). The Aegis.X tile matches the one the add-on itself ships. The on-disk format moves from a bare array of cards (v1) to a v3 object: { version:3, iconSize, quickSize, background, sections:{ sm, q } } On first read, a v1 array lands as sections.q and the Silent Mode column stays at the code default. The remote home-cards.json URL is still accepted as either shape; migrateCards() normalises before use. --- home-brand/ariadne-pottery.svg | 6 + home.html | 891 ++++++++++++++++++++++----------- main.js | 151 ++++-- package.json | 3 +- 4 files changed, 716 insertions(+), 335 deletions(-) create mode 100644 home-brand/ariadne-pottery.svg diff --git a/home-brand/ariadne-pottery.svg b/home-brand/ariadne-pottery.svg new file mode 100644 index 00000000..266a0888 --- /dev/null +++ b/home-brand/ariadne-pottery.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/home.html b/home.html index c7d713c6..8c1fd60d 100644 --- a/home.html +++ b/home.html @@ -1,292 +1,603 @@ - - + + Theseus - - - -
-
⛓️
-

Theseus Navigator

-

A browser that follows the thread. Names that live on the Bitcoin Cash chain.

-
- - -
-

Tip: type a .bch name in the address bar above to open a decentralized site.

-
- -
-
- Quick links -
- - -
-
-
-
- - - - - - - - + + + + +
+
+ +
+
+ + + + + + + + + + Theseus Navigator +
+ +
+
+ +
+
+ Family size +
+ + + + +
+
+
+ Your-links size +
+ + + +
+
+
+ Background +
+ + + + + + +
+ + +
+
+ + +
+
+ +
+
+
+ Your links + + +
+
+
+
+
+ Silent Mode + + +
+
+
+
+ + + + + + diff --git a/main.js b/main.js index 3e6d7666..27c14744 100644 --- a/main.js +++ b/main.js @@ -832,54 +832,112 @@ function emitUpdateAvailable() { sendAllChrome("update-available", payload); } -// ---- home page editable cards (userData/home-cards.json) ------------------ -// Rendered by home.html as the "quick links" grid on the new-tab page. User -// can add/edit/remove via the page's edit mode. First-run seed = the classic -// Silent Mode showcase (hello.bch, theseus.bch, silentmode.bch, etc.). -const DEFAULT_HOME_CARDS = [ - { title: "hello.bch", url: "https://hello.bch/", sub: "A small page on the blockchain itself.", badge: "on-chain" }, - { title: "siatest.bch", url: "https://siatest.bch/", sub: "A page with no server, backed by Sia.", badge: "Sia" }, - { title: "SilentMode.X", url: "https://silentmode.x/", sub: "Infrastructure development for a decentralized web.", badge: "Infrastructure" }, - { title: "Theseus.X", url: "https://theseus.x/", sub: "The Web Navigator — this browser's own address.", badge: "Navigator" }, - { title: "Sirius.X", url: "https://sirius.x/", sub: "Register and manage BCDN names.", badge: "Registrar" }, - { title: "Hephaestus.X", url: "https://hephaestus.x/", sub: "The forge — Silent Mode's code host.", badge: "Code host" }, - { title: "Prometheus.X", url: "https://prometheus.x/", sub: "Decentralized App Marketplace.", badge: "App store" }, - { title: "Helios.X", url: "https://helios.x/", sub: "Search engine for the decentralized web (in design).", badge: "Search" }, - { title: "Hermes.X", url: "https://hermes.x/", sub: "Messaging — end-to-end encrypted over Nostr.", badge: "Messaging" }, -]; -// User's local edits win over everything else — that's the whole point of -// the edit mode. Remote pull only feeds the "defaults" tier so brand copy -// changes reach installs without a browser release. +// ---- home page editable state (userData/home-cards.json) ------------------ +// Rendered by home.html as the two-column desktop on the new-tab page (left +// = user's own links, right = Silent Mode X-family). User can add / edit / +// remove / drag via the page's edit mode. First-run seed = the curated +// defaults below. +// +// Schema v3 is an object: +// { version:3, iconSize, quickSize, background:{...}, +// sections: { sm:[...], q:[...] } } +// A v1 bare array (the pre-0.3.90 schema) is still accepted on read and +// migrated into sections.q (user's own links) on first save. The remote URL +// still serves an array — see migrateCards() for how that lands into v3. +const DEFAULT_HOME_STATE = { + version: 3, + iconSize: "large", + quickSize: "medium", + background: { kind: "preset", value: "dark" }, + sections: { + // Right column — the curated Silent Mode X-family lineup. + sm: [ + { title: "Theseus.X", url: "https://theseus.x/", brand: "theseus" }, + { title: "SilentMode.X", url: "https://silentmode.x/", brand: "silentmode" }, + { title: "Sirius.X", url: "https://sirius.x/", brand: "sirius" }, + { title: "Hephaestus.X", url: "https://hephaestus.x/", brand: "hephaestus" }, + { title: "Prometheus.X", url: "https://prometheus.x/", brand: "prometheus" }, + { title: "Hermes.X", url: "https://hermes.x/", brand: "hermes" }, + { title: "Helios.X", url: "https://helios.x/", brand: "helios" }, + { title: "Ariadne.X", url: "https://ariadne.x/", brand: "ariadne" }, + { title: "Aegis.X", url: "https://aegis.x/", brand: "aegis" }, + ], + // Left column — user's own quick links. Seed includes the demos plus + // Pithos.sia + Bergamot.X (tools-you-use, not apex names). + q: [ + { title: "hello.bch", url: "https://hello.bch/", brand: "hellobch" }, + { title: "sia.coin", url: "https://sia.coin/", brand: "siacoin" }, + { title: "Pithos.sia", url: "https://pithos.sia/", brand: "pithos" }, + { title: "Bergamot.X", url: "https://bergamot.x/", brand: "bergamot" }, + ], + }, +}; const homeCardsFile = () => path.join(app.getPath("userData"), "home-cards.json"); const homeCardsRemoteCache = () => path.join(app.getPath("userData"), "home-cards-remote.json"); -// The canonical remote card list is served from silentmode.st (and mirrored -// on silentmode.bch via Sia). Editing that file updates every install on -// its next launch — no reinstall required. const HOME_CARDS_URL = "https://dl.silentmode.st/home-cards.json"; const HOME_CARDS_REFRESH_MS = 6 * 60 * 60 * 1000; // every 6h -function loadHomeCards() { +// Keep a stable v3-shaped clone of the defaults so a caller mutating what we +// hand them (edit-mode runs do that) never pollutes the module-level seed. +function cloneDefaults() { return JSON.parse(JSON.stringify(DEFAULT_HOME_STATE)); } +// Promote whatever shape we read on disk / off the wire into v3. Accepts: +// - a v3 object (returned as-is after shape repair); +// - a v1 bare array of {title,url,sub?,badge?} cards (lands in sections.q); +// - anything else -> null, and the caller falls back to defaults. +function migrateCards(raw) { + if (!raw) return null; + if (Array.isArray(raw)) { + const q = raw.filter((c) => c && typeof c.title === "string" && typeof c.url === "string") + .map((c) => ({ title: c.title, url: c.url })); + if (!q.length) return null; + const next = cloneDefaults(); + next.sections.q = q; // overlay the user's old picks onto the default SM column + return next; + } + if (typeof raw !== "object") return null; + if (!raw.sections || typeof raw.sections !== "object") return null; + const s = { + version: 3, + iconSize: raw.iconSize || "large", + quickSize: raw.quickSize || "medium", + background: (raw.background && typeof raw.background === "object") ? raw.background : { kind: "preset", value: "dark" }, + sections: { + sm: Array.isArray(raw.sections.sm) ? raw.sections.sm : [], + q: Array.isArray(raw.sections.q) ? raw.sections.q : [], + }, + }; + return s; +} +function loadHomeState() { // Priority: user's local edits > cached remote copy > code defaults. try { if (fs.existsSync(homeCardsFile())) { const v = JSON.parse(fs.readFileSync(homeCardsFile(), "utf8")); - if (Array.isArray(v) && v.length) return v; + const m = migrateCards(v); + if (m) return m; } - } catch (e) { console.error("home cards (user) load failed:", e.message); } + } catch (e) { console.error("home state (user) load failed:", e.message); } try { if (fs.existsSync(homeCardsRemoteCache())) { const v = JSON.parse(fs.readFileSync(homeCardsRemoteCache(), "utf8")); - if (Array.isArray(v) && v.length) return v; + const m = migrateCards(v); + if (m) return m; } - } catch (e) { console.error("home cards (remote-cache) load failed:", e.message); } - return DEFAULT_HOME_CARDS.slice(); + } catch (e) { console.error("home state (remote-cache) load failed:", e.message); } + return cloneDefaults(); } -function saveHomeCards(cards) { - try { fs.writeFileSync(homeCardsFile(), JSON.stringify(cards, null, 2)); } - catch (e) { console.error("home cards save failed:", e.message); } +function saveHomeState(state) { + const m = migrateCards(state); if (!m) return; + try { fs.writeFileSync(homeCardsFile(), JSON.stringify(m, null, 2)); } + catch (e) { console.error("home state save failed:", e.message); } } -// Fetch the canonical home-cards.json into the remote cache. Silent on any -// error (no network, 404, bad JSON, etc.) — the cache stays as-is and the -// user sees either their last cached set or the built-in defaults. +// Legacy aliases — some older callers (and the IPC handler names) still read +// as cards. New code should call loadHomeState / saveHomeState directly. +function loadHomeCards() { return loadHomeState(); } +function saveHomeCards(x) { return saveHomeState(x); } +// Fetch the canonical home-cards.json into the remote cache. The remote may +// serve either an array (v1) or a v3 object — migrateCards() handles both. +// Silent on any error — the cache stays as-is and the user keeps seeing +// either their last cached set or the built-in defaults. async function refreshRemoteHomeCards() { try { const controller = new AbortController(); @@ -887,20 +945,19 @@ async function refreshRemoteHomeCards() { const r = await fetch(HOME_CARDS_URL, { signal: controller.signal, cache: "no-store" }); clearTimeout(to); if (!r.ok) return; - const list = await r.json(); - if (!Array.isArray(list) || list.length === 0) return; - // Basic sanity: every entry must be an object with a string title + url. - const clean = list.filter((c) => c && typeof c.title === "string" && typeof c.url === "string"); - if (!clean.length) return; - fs.writeFileSync(homeCardsRemoteCache(), JSON.stringify(clean, null, 2)); + const raw = await r.json(); + const next = migrateCards(raw); + if (!next) return; + fs.writeFileSync(homeCardsRemoteCache(), JSON.stringify(next, null, 2)); // Only push into open home tabs if the user hasn't overridden — their - // edits stay put. + // edits stay put. The home page's onCards handler re-renders. if (!fs.existsSync(homeCardsFile())) { for (const t of tabs) { - try { t.view.webContents.send("home-cards", clean); } catch {} + try { t.view.webContents.send("home-cards", next); } catch {} } } - console.log(`[home-cards] refreshed from ${HOME_CARDS_URL}: ${clean.length} cards`); + const smN = next.sections.sm.length, qN = next.sections.q.length; + console.log(`[home-cards] refreshed from ${HOME_CARDS_URL}: ${smN + qN} total (sm=${smN}, q=${qN})`); } catch (e) { /* silent */ } } // Sender validation — only accept IPC from our own app pages. Compared against @@ -7571,8 +7628,14 @@ app.on("will-quit", () => { }); // Home page editable cards. Origin-gated to home.html — random pages that // snoop the preload can't act on the local file. -ipcMain.handle("home-cards-get", (e) => isHomePageSender(e.sender) ? loadHomeCards() : []); -ipcMain.handle("home-cards-set", (e, cards) => { if (!isHomePageSender(e.sender)) return false; if (!Array.isArray(cards)) return false; saveHomeCards(cards); return true; }); +ipcMain.handle("home-cards-get", (e) => isHomePageSender(e.sender) ? loadHomeState() : null); +// Accepts either the v3 state object or the legacy v1 array — migrateCards() +// normalises both into v3 before persisting (saveHomeState clones through it). +ipcMain.handle("home-cards-set", (e, state) => { + if (!isHomePageSender(e.sender)) return false; + if (!state || typeof state !== "object") return false; + saveHomeState(state); return true; +}); ipcMain.handle("home-cards-reset", (e) => { if (!isHomePageSender(e.sender)) return false; try { fs.unlinkSync(homeCardsFile()); } catch {} return true; }); 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(); }); diff --git a/package.json b/package.json index 78a7319c..2d96c209 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "theseus-navigator", - "version": "0.3.89", + "version": "0.3.90", "license": "MPL-2.0", "description": "Theseus Navigator — a browser that follows the thread. By Silent Mode, a Deviant project.", "author": "Silent Mode", @@ -109,6 +109,7 @@ "panel-header-preload.js", "panel-grip.html", "engine-icons/**/*", + "home-brand/**/*", "lib/**/*", "package.json", "node_modules/**/*",