From 9f9b74fb6315c6015eeca4fdf5a1d3a49d9a0978 Mon Sep 17 00:00:00 2001 From: Filipe Moreira Date: Wed, 21 Oct 2026 14:40:21 -0300 Subject: [PATCH] feat(appearance): new tab backdrop, quick-links switch and four themes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Settings → Appearance gains: - "Fundo da nova aba": a wallpaper (default), the theme-color glow, or a solid color. The wallpaper, generated for the brand, keeps only its light and takes the theme accent as its color, so it matches every theme; light themes tint it further to keep text readable. - "Acesso rápido na nova aba": hides the quick-link tiles. - Four more theme presets: Midnight, Graphite, Rose and Sunset, each with its accent above 6.5:1 on its surface. Co-Authored-By: Claude Opus 5.5 --- app/src/assets/home.css | 18 ++++++++++++++++++ app/src/assets/newtab-wallpaper.jpg | Bin 0 -> 28509 bytes app/src/assets/newtab.html | 7 ++++++- app/src/assets/settings.html | 17 ++++++++++++++++- app/src/main/pageBridge.ts | 6 +++++- app/src/shared/i18n.ts | 12 ++++++++++++ app/src/shared/settings.ts | 15 +++++++++++++++ app/tests/e2e/shell.spec.ts | 16 +++++++++++++++- app/tests/unit/services.test.ts | 7 +++++++ 9 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 app/src/assets/newtab-wallpaper.jpg diff --git a/app/src/assets/home.css b/app/src/assets/home.css index 54990f5..dd5358b 100644 --- a/app/src/assets/home.css +++ b/app/src/assets/home.css @@ -11,6 +11,24 @@ body { font-family: "Inter", system-ui, sans-serif; } +/* Settings → Appearance picks the backdrop; the glow above is "Gradient". The wallpaper keeps only its light and takes + the theme's accent as its color (luminosity blend), then is tinted toward the theme background so text keeps its + contrast in every theme, light ones included. */ +body[data-bg="Solid"] { background: var(--bg); } +body[data-bg="Wallpaper"] { + background: + radial-gradient(ellipse at center, color-mix(in srgb, var(--bg) 78%, transparent), color-mix(in srgb, var(--bg) 45%, transparent) 80%), + url("newtab-wallpaper.jpg") center / cover no-repeat fixed, + var(--accent); + background-blend-mode: normal, luminosity, normal; +} +/* The wallpaper is a dark picture: a light theme washes it out further so dark text stays readable up to the edges. */ +body[data-bg="Wallpaper"][data-tone="light"] { + background-image: + radial-gradient(ellipse at center, color-mix(in srgb, var(--bg) 92%, transparent), color-mix(in srgb, var(--bg) 72%, transparent) 80%), + url("newtab-wallpaper.jpg"); +} + main { display: flex; flex-direction: column; diff --git a/app/src/assets/newtab-wallpaper.jpg b/app/src/assets/newtab-wallpaper.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cf3bf02dfcb5df9861aaa34b93b90d9b9bb40c5a GIT binary patch literal 28509 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 16 & 255) + 0.7152 * (v >> 8 & 255) + 0.0722 * (v & 255)) / 255 > 0.45 ? '#0A0F1A' : '#FFFFFF'); $('engine').textContent = `${msg.searchEngine.name}`; + document.body.dataset.bg = msg.newTabBackground || 'Wallpaper'; + const b = parseInt(t.background.slice(1), 16); + document.body.dataset.tone = (0.2126 * (b >> 16 & 255) + 0.7152 * (b >> 8 & 255) + 0.0722 * (b & 255)) / 255 > 0.5 ? 'light' : 'dark'; + showSites = msg.newTabSites !== false; pinned = [...msg.shortcuts, ...(msg.favorites || [])]; render(); } @@ -89,7 +94,7 @@ function render() { const seen = new Set(); const sites = [...pinned, ...recent].filter((s) => !seen.has(s.url) && seen.add(s.url)).slice(0, MAX_TILES); - $('sites').hidden = sites.length === 0; + $('sites').hidden = !showSites || sites.length === 0; $('sites').replaceChildren(...sites.map((s) => card(s.name, s.url))); } post('ready', {}); diff --git a/app/src/assets/settings.html b/app/src/assets/settings.html index 2eedacf..4b7902e 100644 --- a/app/src/assets/settings.html +++ b/app/src/assets/settings.html @@ -40,6 +40,18 @@

Aparência

Para TVs que cortam as bordas
+
+
+ +
+
+
Acesso rápido na nova abaSeus atalhos, favoritos e sites recentes
+ +
Alto contraste
@@ -299,10 +311,11 @@

Sobre e atualizações

post('ui-settings', { uiScale: Number($('uiScale').value), overscan: Number($('overscan').value), highContrast: $('highContrast').checked, language: $('language').value, crashReports: $('crashReports').checked, + newTabBackground: $('newTabBackground').value, newTabSites: $('newTabSites').checked, }); flash('uiStatus', i18n.t('settings.saved')); }; - for (const id of ['uiScale', 'overscan', 'highContrast', 'language', 'crashReports']) $(id).addEventListener('change', sendUi); + for (const id of ['uiScale', 'overscan', 'highContrast', 'language', 'crashReports', 'newTabBackground', 'newTabSites']) $(id).addEventListener('change', sendUi); function showUpdate(msg) { $('appUpdate').textContent = msg.appUpdate || ''; @@ -355,6 +368,8 @@

Sobre e atualizações

$('uiScale').value = String(msg.uiScale ?? 1); $('overscan').value = String(msg.overscan ?? 0); $('highContrast').checked = !!msg.highContrast; + $('newTabBackground').value = msg.newTabBackground || 'Wallpaper'; + $('newTabSites').checked = msg.newTabSites !== false; $('language').value = msg.language ?? 'pt-BR'; $('crashReports').checked = !!msg.crashReports; $('streamingUa').checked = !!msg.streamingDesktopUa; diff --git a/app/src/main/pageBridge.ts b/app/src/main/pageBridge.ts index dcfc9c8..096efdf 100644 --- a/app/src/main/pageBridge.ts +++ b/app/src/main/pageBridge.ts @@ -1,7 +1,7 @@ import { Channels } from "../shared/ipc"; import { Pages } from "../shared/pages"; import { - DEFAULT_SHORTCUTS, parseSearchEngine, parseShortcut, parseTheme, SEARCH_ENGINES, THEME_PRESETS, type AppSettings, + DEFAULT_SHORTCUTS, NEW_TAB_BACKGROUNDS, parseSearchEngine, parseShortcut, parseTheme, SEARCH_ENGINES, THEME_PRESETS, type AppSettings, withoutInputMode, withoutPermissions, } from "../shared/settings"; import { currentShortcutMod } from "./platform"; @@ -62,6 +62,8 @@ export class PageBridge { highContrast: s.highContrast, crashReports: s.crashReports, streamingDesktopUa: s.streamingDesktopUa, + newTabBackground: s.newTabBackground, + newTabSites: s.newTabSites, vibration: s.vibration, appVersion: this.deps.appVersion, appUpdate: this.deps.updateStatus(), @@ -145,6 +147,8 @@ export class PageBridge { if (typeof m.highContrast === "boolean") patch.highContrast = m.highContrast; if (m.language === "pt-BR" || m.language === "en") patch.language = m.language; if (typeof m.crashReports === "boolean") patch.crashReports = m.crashReports; + if ((NEW_TAB_BACKGROUNDS as readonly unknown[]).includes(m.newTabBackground)) patch.newTabBackground = m.newTabBackground as AppSettings["newTabBackground"]; + if (typeof m.newTabSites === "boolean") patch.newTabSites = m.newTabSites; store.update((s) => ({ ...s, ...patch })); this.shell.broadcast(); this.reply(tab, { type: "init", ...this.payloadFields() }); diff --git a/app/src/shared/i18n.ts b/app/src/shared/i18n.ts index 27002ef..02f721d 100644 --- a/app/src/shared/i18n.ts +++ b/app/src/shared/i18n.ts @@ -162,6 +162,12 @@ const en: Record = { "settings.overscan": "Screen margin (overscan)", "settings.overscanHint": "Keep content away from the edges of TVs that crop the picture", "settings.contrast": "High contrast", + "settings.ntBackground": "New tab background", + "settings.ntWallpaper": "Wallpaper", + "settings.ntGradient": "Theme color glow", + "settings.ntSolid": "Solid color", + "settings.ntSites": "Quick links on the new tab", + "settings.ntSitesHint": "Your shortcuts, favorites and recent sites", "settings.language": "Language", "settings.navigation": "Navigation", "settings.searchEngine": "Search engine", @@ -373,6 +379,12 @@ const ptBR: Record = { "settings.overscan": "Margem da tela (overscan)", "settings.overscanHint": "Mantém o conteúdo longe das bordas em TVs que cortam a imagem", "settings.contrast": "Alto contraste", + "settings.ntBackground": "Fundo da nova aba", + "settings.ntWallpaper": "Papel de parede", + "settings.ntGradient": "Brilho da cor do tema", + "settings.ntSolid": "Cor sólida", + "settings.ntSites": "Acesso rápido na nova aba", + "settings.ntSitesHint": "Seus atalhos, favoritos e sites recentes", "settings.language": "Idioma", "settings.navigation": "Navegação", "settings.searchEngine": "Mecanismo de busca", diff --git a/app/src/shared/settings.ts b/app/src/shared/settings.ts index 6226c33..049a11c 100644 --- a/app/src/shared/settings.ts +++ b/app/src/shared/settings.ts @@ -21,6 +21,10 @@ export const THEME_PRESETS: Record = { Ocean: { accent: "#4CC2FF", background: "#0B1220", surface: "#121C2E", text: "#EAF2FF" }, Ember: { accent: "#FF7A45", background: "#15100E", surface: "#1F1815", text: "#FFF1EA" }, Violet: { accent: "#B388FF", background: "#100D18", surface: "#191424", text: "#F2EDFF" }, + Midnight: { accent: "#7AA2FF", background: "#0A0E1A", surface: "#131A2B", text: "#E6ECFF" }, + Graphite: { accent: "#A9B8D0", background: "#141414", surface: "#232323", text: "#EDEDED" }, + Rose: { accent: "#FF6B9A", background: "#160E12", surface: "#21161B", text: "#FFEFF4" }, + Sunset: { accent: "#FFB224", background: "#15110A", surface: "#201A10", text: "#FFF6E5" }, Light: LIGHT_THEME, }; @@ -97,6 +101,10 @@ export function parseShortcut(value: unknown): Shortcut | undefined { return typeof s.name === "string" && s.name.trim() && isWebUrl(s.url) ? { name: s.name.trim(), url: s.url.trim() } : undefined; } +/** What the new tab draws behind the logo and the search: the bundled wallpaper, the accent glow, or the plain color. */ +export const NEW_TAB_BACKGROUNDS = ["Wallpaper", "Gradient", "Solid"] as const; +export type NewTabBackground = (typeof NEW_TAB_BACKGROUNDS)[number]; + export type StartupMode = "NewTab" | "Home" | "Restore" | "Custom"; export type NewTabMode = "NewTabPage" | "Home" | "Custom"; export type TrackingLevel = "Basic" | "Balanced" | "Strict"; @@ -142,6 +150,9 @@ export interface AppSettings { * sites whose rendition improves without the TV marker. Off until a row in docs/DRM-UAS.md shows a gain. */ streamingDesktopUa: boolean; + newTabBackground: NewTabBackground; + /** The row of quick-link tiles under the search on the new tab. */ + newTabSites: boolean; } export function defaultSettings(): AppSettings { @@ -172,6 +183,8 @@ export function defaultSettings(): AppSettings { vibration: false, sitePermissions: {}, streamingDesktopUa: false, + newTabBackground: "Wallpaper", + newTabSites: true, }; } @@ -260,6 +273,8 @@ export function parseSettings(input: unknown): AppSettings { vibration: bool(r.vibration, d.vibration), sitePermissions: permissions, streamingDesktopUa: bool(r.streamingDesktopUa, d.streamingDesktopUa), + newTabBackground: oneOf(NEW_TAB_BACKGROUNDS, r.newTabBackground, d.newTabBackground), + newTabSites: bool(r.newTabSites, d.newTabSites), }; } diff --git a/app/tests/e2e/shell.spec.ts b/app/tests/e2e/shell.spec.ts index 65d16f6..73aa669 100644 --- a/app/tests/e2e/shell.spec.ts +++ b/app/tests/e2e/shell.spec.ts @@ -1,5 +1,5 @@ import { _electron as electron, expect, test, type ElectronApplication } from "@playwright/test"; -import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -113,3 +113,17 @@ test("first run shows onboarding", async () => { rmSync(fresh, { recursive: true, force: true }); } }); + +test("settings change the new tab: backdrop and quick links", async () => { + const pageEval = (code: string) => app.evaluate(({}, c) => globalThis.joy.pageEval(c), code) as Promise; + await expect.poll(tabs, { timeout: 30_000 }).toHaveLength(1); + await command({ type: "navigate", url: "joychromium://settings" }); + await expect.poll(() => pageEval("document.getElementById('newTabBackground').value"), { timeout: 15_000 }).toBe("Wallpaper"); + await pageEval("(() => { const s = document.getElementById('newTabBackground'); s.value = 'Solid'; s.dispatchEvent(new Event('change')); })()"); + await pageEval("document.getElementById('newTabSites').click()"); + await expect.poll(() => JSON.parse(readFileSync(join(data, "settings.json"), "utf8")).newTabSites).toBe(false); + expect(JSON.parse(readFileSync(join(data, "settings.json"), "utf8")).newTabBackground).toBe("Solid"); + + await command({ type: "navigate", url: "joychromium://newtab" }); + await expect.poll(() => pageEval("document.body.dataset.bg"), { timeout: 15_000 }).toBe("Solid"); +}); diff --git a/app/tests/unit/services.test.ts b/app/tests/unit/services.test.ts index 990baea..ea2b744 100644 --- a/app/tests/unit/services.test.ts +++ b/app/tests/unit/services.test.ts @@ -166,6 +166,13 @@ describe("settings store", () => { expect(parseSettings({}).shortcuts).toEqual([]); }); + it("parses the new tab look and falls back on anything unknown", () => { + expect(parseSettings({}).newTabBackground).toBe("Wallpaper"); + expect(parseSettings({}).newTabSites).toBe(true); + expect(parseSettings({ newTabBackground: "Solid", newTabSites: false })).toMatchObject({ newTabBackground: "Solid", newTabSites: false }); + expect(parseSettings({ newTabBackground: "Video", newTabSites: "no" })).toMatchObject({ newTabBackground: "Wallpaper", newTabSites: true }); + }); + it("writes atomically and keeps the previous file as a backup", () => { const dir = temp(); try {