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 0000000..cf3bf02 Binary files /dev/null and b/app/src/assets/newtab-wallpaper.jpg differ diff --git a/app/src/assets/newtab.html b/app/src/assets/newtab.html index 5ffb14f..9d65d9a 100644 --- a/app/src/assets/newtab.html +++ b/app/src/assets/newtab.html @@ -46,6 +46,7 @@ const MAX_TILES = 8; let pinned = []; let recent = []; + let showSites = true; function card(name, url) { const b = document.createElement('button'); @@ -77,6 +78,10 @@ const v = parseInt(t.accent.slice(1), 16); r.setProperty('--accent-text', (0.2126 * (v >> 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 {