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 {