Add first-run setup and theme synchronization

Introduce first-run setup flow and live chrome theme syncing.

- Add first_run_state.cpp/.h to read/write a first_run_state.json under user data and decide whether to show the setup UI.
- Wire first-run logic into NebulaController: track first_run_setup_active_, create initial setup tab, defer/bring up chrome browser accordingly, and add CompleteFirstRunSetup() to persist state and finish setup.
- Add SendThemeToChromeSurfaces() and handle "theme-update" and "complete-first-run" chrome commands; restrict setup completion to setup frame.
- Expose GetFirstRunStatePath() and GetSetupUrl() in UI path helpers and include the state file in the build list (CMakeLists.txt).
- Update chrome UI: new CSS variables and styles for tabs/url-bar; chrome.js can apply themes (applyTheme), persist/load theme, and listen for storage updates to apply theme changes live.
- Update customization.js, settings.js, and setup.js to normalize/persist themes, send theme updates to the native host (or fallback), and communicate completion via the native bridge when available; include customization.js in setup.html.

These changes allow the app to run an interactive first-run setup and keep the separate chrome UI in sync with user-selected themes.
This commit is contained in:
Andrew Zambazos
2026-05-20 20:14:43 +12:00
parent bbba5b2927
commit 302753cd3d
14 changed files with 416 additions and 59 deletions
+41 -31
View File
@@ -6,10 +6,20 @@
--text: #e8e8f0;
--muted: #7a7e90;
--accent: #7b2eff;
--primary: #7b2eff;
--accent-2: #00c6ff;
--outline: #1f2533;
--outline-soft: rgba(255, 255, 255, 0.06);
--danger: #e0445c;
--url-bar-bg: #1c2030;
--url-bar-text: #e0e0e0;
--url-bar-border: #3e4652;
--tab-bg: #161925;
--tab-text: #a4a7b3;
--tab-active: #1c2030;
--tab-active-text: #e0e0e0;
--tab-border: #2b3040;
--chrome-hover: color-mix(in srgb, var(--text) 10%, transparent);
color-scheme: dark;
}
@@ -102,21 +112,21 @@ button:disabled {
border: 1px solid transparent;
border-bottom: none;
background: transparent;
color: var(--muted);
color: var(--tab-text);
cursor: pointer;
font-size: 0.82rem;
transition: background 120ms, color 120ms;
}
.tab:hover:not(.active) {
background: var(--surface);
color: var(--text);
background: var(--tab-bg);
color: var(--tab-active-text);
}
.tab.active {
background: var(--surface-raised);
border-color: var(--outline);
color: var(--text);
background: var(--tab-active);
border-color: var(--tab-border);
color: var(--tab-active-text);
}
.tab-title {
@@ -139,7 +149,7 @@ button:disabled {
display: flex;
align-items: center;
justify-content: center;
background: var(--accent);
background: var(--primary);
opacity: 0.85;
border-radius: 999px;
overflow: hidden;
@@ -166,8 +176,8 @@ button:disabled {
.tab-loading {
width: 13px;
height: 13px;
border: 2px solid rgba(0, 198, 255, 0.2);
border-top-color: var(--accent-2);
border: 2px solid color-mix(in srgb, var(--accent) 20%, transparent);
border-top-color: var(--accent);
border-radius: 999px;
animation: spin 0.8s linear infinite;
}
@@ -181,7 +191,7 @@ button:disabled {
padding: 0;
border-radius: 6px;
background: transparent;
color: var(--muted);
color: var(--tab-text);
opacity: 0;
transition: background 120ms, color 120ms, opacity 120ms;
}
@@ -193,8 +203,8 @@ button:disabled {
}
.tab-close:hover {
background: var(--surface-hover);
color: var(--text);
background: var(--chrome-hover);
color: var(--tab-active-text);
}
.tab-add {
@@ -207,14 +217,14 @@ button:disabled {
border-radius: 8px;
border: 1px solid transparent;
background: transparent;
color: var(--muted);
color: var(--tab-text);
transition: background 120ms, color 120ms, border-color 120ms;
}
.tab-add:hover {
background: var(--surface-hover);
border-color: var(--outline);
color: var(--text);
background: var(--chrome-hover);
border-color: var(--tab-border);
color: var(--tab-active-text);
}
/* ── Window controls ────────────────────────────────────────── */
@@ -233,13 +243,13 @@ button:disabled {
justify-content: center;
width: 46px;
background: transparent;
color: var(--muted);
color: var(--tab-text);
transition: background 100ms, color 100ms;
}
.window-controls button:hover {
background: var(--surface-hover);
color: var(--text);
background: var(--chrome-hover);
color: var(--tab-active-text);
}
.window-controls .close:hover {
@@ -252,8 +262,8 @@ button:disabled {
.toolbar {
gap: 4px;
padding: 0 12px;
background: var(--surface-raised);
border-top: 1px solid var(--outline);
background: var(--tab-active);
border-top: 1px solid var(--tab-border);
overflow: visible;
}
@@ -283,15 +293,15 @@ button:disabled {
border-radius: 9px;
background: transparent;
border: 1px solid transparent;
color: var(--muted);
color: var(--tab-text);
flex-shrink: 0;
transition: background 120ms, color 120ms, border-color 120ms;
}
.icon-button:hover:not(:disabled) {
background: var(--surface-hover);
border-color: var(--outline);
color: var(--text);
background: var(--chrome-hover);
border-color: var(--tab-border);
color: var(--tab-active-text);
}
/* ── Address bar ────────────────────────────────────────────── */
@@ -305,15 +315,15 @@ button:disabled {
flex: 1;
margin: 0 4px;
overflow: hidden;
border: 1px solid var(--outline);
border: 1px solid var(--url-bar-border);
border-radius: 10px;
background: var(--surface);
background: var(--url-bar-bg);
transition: border-color 140ms, box-shadow 140ms;
}
.address-shell:focus-within {
border-color: rgba(123, 46, 255, 0.55);
box-shadow: 0 0 0 3px rgba(123, 46, 255, 0.12);
border-color: color-mix(in srgb, var(--primary) 70%, var(--url-bar-border));
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 18%, transparent);
}
.address-shell input {
@@ -323,12 +333,12 @@ button:disabled {
outline: 0;
padding: 0 16px;
background: transparent;
color: var(--text);
color: var(--url-bar-text);
font-size: 0.84rem;
}
.address-shell input::placeholder {
color: var(--muted);
color: color-mix(in srgb, var(--url-bar-text) 55%, transparent);
}
.progress-bar {
+103 -1
View File
@@ -1,5 +1,24 @@
const SEARCH_URL = 'https://www.google.com/search?q=';
const DEFAULT_THEME = {
colors: {
bg: '#080a0f',
darkBlue: '#0e1119',
darkPurple: '#141824',
primary: '#7b2eff',
accent: '#00c6ff',
text: '#e8e8f0',
urlBarBg: '#1c2030',
urlBarText: '#e0e0e0',
urlBarBorder: '#3e4652',
tabBg: '#161925',
tabText: '#a4a7b3',
tabActive: '#1c2030',
tabActiveText: '#e0e0e0',
tabBorder: '#2b3040'
}
};
const state = {
id: 1,
url: '',
@@ -12,6 +31,79 @@ const state = {
tabs: []
};
function hexToRgb(hex) {
if (!hex || typeof hex !== 'string') return null;
let normalized = hex.trim().replace(/^#/, '');
if (normalized.length === 3) {
normalized = normalized.split('').map(char => char + char).join('');
}
if (!/^[a-fA-F\d]{6}$/.test(normalized)) return null;
const value = parseInt(normalized, 16);
return {
r: (value >> 16) & 255,
g: (value >> 8) & 255,
b: value & 255
};
}
function isDarkColor(hex) {
const rgb = hexToRgb(hex);
if (!rgb) return true;
const luminance = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255;
return luminance < 0.5;
}
function setCssVar(name, value, fallback) {
document.documentElement.style.setProperty(name, value || fallback);
}
function normalizeTheme(theme) {
const colors = theme?.colors || {};
return {
...DEFAULT_THEME,
...(theme || {}),
colors: {
...DEFAULT_THEME.colors,
...colors
}
};
}
function applyTheme(theme) {
const normalized = normalizeTheme(theme);
const colors = normalized.colors;
setCssVar('--bg', colors.bg, DEFAULT_THEME.colors.bg);
setCssVar('--surface', colors.darkBlue, DEFAULT_THEME.colors.darkBlue);
setCssVar('--surface-raised', colors.darkPurple, DEFAULT_THEME.colors.darkPurple);
setCssVar('--text', colors.text, DEFAULT_THEME.colors.text);
setCssVar('--muted', colors.tabText, DEFAULT_THEME.colors.tabText);
setCssVar('--primary', colors.primary, DEFAULT_THEME.colors.primary);
setCssVar('--accent', colors.accent, DEFAULT_THEME.colors.accent);
setCssVar('--accent-2', colors.accent, DEFAULT_THEME.colors.accent);
setCssVar('--outline', colors.tabBorder, DEFAULT_THEME.colors.tabBorder);
setCssVar('--url-bar-bg', colors.urlBarBg, colors.darkBlue);
setCssVar('--url-bar-text', colors.urlBarText, colors.text);
setCssVar('--url-bar-border', colors.urlBarBorder, colors.primary);
setCssVar('--tab-bg', colors.tabBg, colors.darkBlue);
setCssVar('--tab-text', colors.tabText, colors.text);
setCssVar('--tab-active', colors.tabActive, colors.darkPurple);
setCssVar('--tab-active-text', colors.tabActiveText, colors.text);
setCssVar('--tab-border', colors.tabBorder, colors.darkBlue);
document.documentElement.style.colorScheme = isDarkColor(colors.bg) ? 'dark' : 'light';
}
function applySavedTheme() {
try {
const savedTheme = localStorage.getItem('currentTheme');
if (savedTheme) applyTheme(JSON.parse(savedTheme));
} catch (error) {
console.warn('[Chrome] Failed to apply saved theme:', error);
}
}
function toNavigationUrl(input) {
const value = (input || '').trim();
if (!value) return null;
@@ -175,9 +267,19 @@ function wireCommands() {
});
}
window.NebulaChrome = { applyState, postCommand, toNavigationUrl };
window.NebulaChrome = { applyState, applyTheme, postCommand, toNavigationUrl };
window.addEventListener('storage', event => {
if (event.key !== 'currentTheme' || !event.newValue) return;
try {
applyTheme(JSON.parse(event.newValue));
} catch (error) {
console.warn('[Chrome] Failed to apply updated theme:', error);
}
});
document.addEventListener('DOMContentLoaded', () => {
applySavedTheme();
wireCommands();
applyState(state);
});
+12 -4
View File
@@ -4,7 +4,7 @@
*/
class BrowserCustomizer {
constructor() {
constructor(options = {}) {
this.defaultTheme = {
name: 'Default',
colors: {
@@ -286,6 +286,10 @@ class BrowserCustomizer {
}
};
if (options.skipInit) {
return;
}
this.currentTheme = this.loadTheme();
this.activeThemeName = this.loadActiveThemeName();
this.init();
@@ -584,9 +588,13 @@ class BrowserCustomizer {
// This will be called to apply theme to home.html and other pages
this.saveTheme();
// Send theme update to host (for settings webview)
if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('theme-update', this.currentTheme);
const themePayload = JSON.stringify(this.currentTheme);
// Send theme update to host so the separate chrome browser can update live.
if (window.nebulaNative && typeof window.nebulaNative.postMessage === 'function') {
window.nebulaNative.postMessage('theme-update', themePayload);
} else if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('theme-update', themePayload);
}
// Fallback: send via postMessage (for iframe embedding)
try {
+1 -1
View File
@@ -65,7 +65,7 @@ function attachClearHandler(btn) {
} finally {
const currentTheme = window.browserCustomizer ? window.browserCustomizer.currentTheme : null;
if (currentTheme && window.electronAPI && typeof window.electronAPI.sendToHost === 'function') {
window.electronAPI.sendToHost('theme-update', currentTheme);
window.electronAPI.sendToHost('theme-update', JSON.stringify(currentTheme));
}
}
};
+70 -19
View File
@@ -12,17 +12,58 @@ const setupState = {
themes: []
};
function hasNebulaNativeBridge() {
return !!(window.nebulaNative && typeof window.nebulaNative.postMessage === 'function');
}
function getPresetThemes() {
if (typeof BrowserCustomizer === 'function') {
const customizer = new BrowserCustomizer({ skipInit: true });
return customizer.predefinedThemes || { default: customizer.defaultTheme };
}
return {
default: {
name: 'Default',
colors: {
bg: '#121418',
darkBlue: '#0B1C2B',
darkPurple: '#1B1035',
primary: '#7B2EFF',
accent: '#00C6FF',
text: '#E0E0E0',
urlBarBg: '#1C2030',
urlBarText: '#E0E0E0',
urlBarBorder: '#3E4652',
tabBg: '#161925',
tabText: '#A4A7B3',
tabActive: '#1C2030',
tabActiveText: '#E0E0E0',
tabBorder: '#2B3040'
},
layout: 'centered',
showLogo: true,
customTitle: 'Nebula Browser',
gradient: 'linear-gradient(145deg, #121418 0%, #1B1035 100%)'
}
};
}
function normalizeTheme(theme) {
const fallback = getPresetThemes().default;
return {
...fallback,
...(theme || {}),
colors: {
...fallback.colors,
...((theme && theme.colors) || {})
}
};
}
const nativeApi = window.api || {
async getAllThemes() {
return {
default: {
default: {
name: 'Default',
description: 'Classic Nebula theme',
colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF', text: '#E0E0E0' }
}
}
};
return { default: getPresetThemes() };
},
async isDefaultBrowser() {
return false;
@@ -31,10 +72,17 @@ const nativeApi = window.api || {
return { success: false, error: 'Default browser setup is handled by the native CEF app.' };
},
async applyTheme(themeId) {
const theme = getThemeById(themeId);
if (theme) {
localStorage.setItem('currentTheme', JSON.stringify(normalizeTheme(theme)));
}
localStorage.setItem('activeThemeName', themeId);
},
async completeFirstRun(data) {
localStorage.setItem('nebula-first-run-complete', JSON.stringify(data));
if (hasNebulaNativeBridge()) {
window.nebulaNative.postMessage('complete-first-run', JSON.stringify(data));
}
}
};
@@ -67,9 +115,7 @@ async function loadThemes() {
console.error('[Setup] Error loading themes:', error);
// Fallback to a default theme
setupState.themes = {
default: {
default: { name: 'Default', description: 'Classic Nebula theme', colors: { bg: '#121418', primary: '#7B2EFF', accent: '#00C6FF' } }
}
default: getPresetThemes()
};
renderThemeGrid(setupState.themes);
}
@@ -167,8 +213,9 @@ function hexToRgb(hex) {
* Apply theme to the setup page UI and persist selection
*/
function applyThemeToSetupPage(theme, themeId = null) {
if (!theme || !theme.colors) return;
const colors = theme.colors;
const completeTheme = normalizeTheme(theme);
if (!completeTheme || !completeTheme.colors) return;
const colors = completeTheme.colors;
const root = document.documentElement;
const setVar = (cssVar, value, fallback) => {
@@ -202,15 +249,15 @@ function applyThemeToSetupPage(theme, themeId = null) {
setVar('--warning-rgb', `${warningRgb.r}, ${warningRgb.g}, ${warningRgb.b}`);
}
if (theme.gradient) {
document.body.style.background = theme.gradient;
if (completeTheme.gradient) {
document.body.style.background = completeTheme.gradient;
} else if (colors.bg) {
document.body.style.background = colors.bg;
}
// Persist for main UI to pick up on first load
try {
localStorage.setItem('currentTheme', JSON.stringify(theme));
localStorage.setItem('currentTheme', JSON.stringify(completeTheme));
if (themeId) localStorage.setItem('activeThemeName', themeId);
} catch (err) {
console.warn('[Setup] Failed to persist theme:', err);
@@ -499,7 +546,9 @@ async function completeSetup() {
console.log('[Setup] First-time setup completed successfully');
window.location.href = 'home.html';
if (!hasNebulaNativeBridge()) {
window.location.href = 'home.html';
}
} catch (error) {
console.error('[Setup] Error completing setup:', error);
alert('There was an error saving your preferences. Please try again.');
@@ -522,7 +571,9 @@ async function skipSetup() {
console.log('[Setup] Setup skipped, using defaults');
window.location.href = 'home.html';
if (!hasNebulaNativeBridge()) {
window.location.href = 'home.html';
}
} catch (error) {
console.error('[Setup] Error skipping setup:', error);
window.location.href = 'home.html';
+1
View File
@@ -129,6 +129,7 @@
</div>
</div>
<script src="../js/customization.js"></script>
<script src="../js/setup.js"></script>
</body>
</html>