From 302753cd3d05bee756cbc0d57dfda16ce2fbd5df Mon Sep 17 00:00:00 2001 From: Andrew Zambazos <62979495+Bobbybear007@users.noreply.github.com> Date: Wed, 20 May 2026 20:14:43 +1200 Subject: [PATCH] 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. --- CMakeLists.txt | 1 + src/app/first_run_state.cpp | 96 +++++++++++++++++++++++++++++++ src/app/first_run_state.h | 8 +++ src/app/nebula_controller.cpp | 60 +++++++++++++++++++- src/app/nebula_controller.h | 3 + src/cef/browser_client.cpp | 12 +++- src/ui/paths.cpp | 9 +++ src/ui/paths.h | 2 + ui/css/chrome.css | 72 +++++++++++++---------- ui/js/chrome.js | 104 +++++++++++++++++++++++++++++++++- ui/js/customization.js | 16 ++++-- ui/js/settings.js | 2 +- ui/js/setup.js | 89 ++++++++++++++++++++++------- ui/pages/setup.html | 1 + 14 files changed, 416 insertions(+), 59 deletions(-) create mode 100644 src/app/first_run_state.cpp create mode 100644 src/app/first_run_state.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b35acf8..3d0e2a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,6 +40,7 @@ SET_CEF_TARGET_OUT_DIR() # ------------------------------------------------------------ set(NEBULA_COMMON_SOURCES + src/app/first_run_state.cpp src/app/nebula_controller.cpp src/app/run.cpp src/browser/session_state.cpp diff --git a/src/app/first_run_state.cpp b/src/app/first_run_state.cpp new file mode 100644 index 0000000..5f8354a --- /dev/null +++ b/src/app/first_run_state.cpp @@ -0,0 +1,96 @@ +#include "app/first_run_state.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "ui/paths.h" + +namespace nebula::app { +namespace { + +std::string ReadFile(const std::filesystem::path& path) { + std::ifstream input(path, std::ios::binary); + if (!input) { + return {}; + } + + std::ostringstream buffer; + buffer << input.rdbuf(); + return buffer.str(); +} + +bool ReadFirstStartValue(const std::string& json, bool& first_start) { + constexpr std::string_view key = "\"first-start\""; + const size_t key_pos = json.find(key); + if (key_pos == std::string::npos) { + return false; + } + + size_t colon = json.find(':', key_pos + key.size()); + if (colon == std::string::npos) { + return false; + } + + ++colon; + while (colon < json.size() && std::isspace(static_cast(json[colon]))) { + ++colon; + } + + if (json.compare(colon, 4, "true") == 0) { + first_start = true; + return true; + } + if (json.compare(colon, 5, "false") == 0) { + first_start = false; + return true; + } + + return false; +} + +} // namespace + +bool ShouldShowFirstRunSetup() { + const auto path = nebula::ui::GetFirstRunStatePath(); + if (path.empty()) { + return true; + } + + const std::string json = ReadFile(path); + if (json.empty()) { + return true; + } + + bool first_start = true; + return ReadFirstStartValue(json, first_start) ? first_start : true; +} + +bool WriteFirstRunState(bool first_start) { + const auto path = nebula::ui::GetFirstRunStatePath(); + if (path.empty()) { + return false; + } + + std::filesystem::path temp_path = path; + temp_path += L".tmp"; + { + std::ofstream output(temp_path, std::ios::binary | std::ios::trunc); + if (!output) { + return false; + } + output << "{\n \"first-start\": " << (first_start ? "true" : "false") << "\n}\n"; + } + + std::error_code ec; + std::filesystem::remove(path, ec); + ec.clear(); + std::filesystem::rename(temp_path, path, ec); + return !ec; +} + +} // namespace nebula::app diff --git a/src/app/first_run_state.h b/src/app/first_run_state.h new file mode 100644 index 0000000..189eee0 --- /dev/null +++ b/src/app/first_run_state.h @@ -0,0 +1,8 @@ +#pragma once + +namespace nebula::app { + +bool ShouldShowFirstRunSetup(); +bool WriteFirstRunState(bool first_start); + +} // namespace nebula::app diff --git a/src/app/nebula_controller.cpp b/src/app/nebula_controller.cpp index ce5f958..29932a0 100644 --- a/src/app/nebula_controller.cpp +++ b/src/app/nebula_controller.cpp @@ -7,6 +7,7 @@ #include #include +#include "app/first_run_state.h" #include "browser/session_state.h" #include "browser/url_utils.h" #include "include/cef_app.h" @@ -194,14 +195,19 @@ void NebulaController::OnWindowCreated() { window_->SetFullscreen(true); } - if (initial_url_.empty()) { + first_run_setup_active_ = + !big_picture_mode_ && initial_url_.empty() && ShouldShowFirstRunSetup(); + + if (first_run_setup_active_) { + tabs_.CreateInitialTab(nebula::ui::GetSetupUrl()); + } else if (initial_url_.empty()) { tabs_.CreateInitialTab(nebula::ui::GetHomeUrl()); } else { tabs_.CreateInitialTab(initial_url_); } PersistSession(); - if (!big_picture_mode_) { + if (!big_picture_mode_ && !first_run_setup_active_) { CreateChromeBrowser(); } CreateContentBrowser(); @@ -378,6 +384,10 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st CloseMenuPopup(); } else if (command == "home") { tabs_.LoadURL(nebula::ui::GetHomeUrl()); + } else if (command == "theme-update") { + SendThemeToChromeSurfaces(payload); + } else if (command == "complete-first-run") { + CompleteFirstRunSetup(); } else if (command == "clear-site-history") { site_history_.clear(); SaveSiteHistory(site_history_); @@ -656,6 +666,10 @@ nebula::window::BrowserLayout NebulaController::CurrentBrowserLayout() const { return {}; } + if (first_run_setup_active_) { + return window_->CurrentLayout(false); + } + if (!big_picture_mode_) { return window_->CurrentLayout(!content_fullscreen_); } @@ -761,6 +775,29 @@ void NebulaController::SendMenuPopupZoom() { menu_popup_browser_->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetMenuPopupUrl(), 0); } +void NebulaController::SendThemeToChromeSurfaces(const std::string& theme_json) { + if (theme_json.empty()) { + return; + } + + const std::string escaped_theme = nebula::browser::JsonEscape(theme_json); + const std::string script = + "(function(){" + "try{" + "const theme=JSON.parse(\"" + escaped_theme + "\");" + "if(window.NebulaChrome&&window.NebulaChrome.applyTheme){window.NebulaChrome.applyTheme(theme);}" + "if(window.NebulaMenuPopup&&window.NebulaMenuPopup.applyTheme){window.NebulaMenuPopup.applyTheme(theme);}" + "}catch(e){console.warn('[Theme] Failed to apply chrome theme',e);}" + "})();"; + + if (chrome_browser_) { + chrome_browser_->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetChromeUrl(), 0); + } + if (menu_popup_browser_) { + menu_popup_browser_->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetMenuPopupUrl(), 0); + } +} + void NebulaController::ToggleDevTools() { auto* tab = tabs_.ActiveTab(); if (!tab || !tab->browser || !window_ || !window_->native_handle()) { @@ -924,6 +961,25 @@ void NebulaController::SetContentFullscreen(bool fullscreen) { ResizeBrowsers(); } +void NebulaController::CompleteFirstRunSetup() { + WriteFirstRunState(false); + + if (!first_run_setup_active_) { + tabs_.LoadURL(nebula::ui::GetHomeUrl()); + PersistSession(); + return; + } + + first_run_setup_active_ = false; + tabs_.LoadURL(nebula::ui::GetHomeUrl()); + PersistSession(); + + if (!big_picture_mode_ && !chrome_browser_) { + CreateChromeBrowser(); + } + ResizeBrowsers(); +} + void NebulaController::ResizeBrowsers() { if (!window_) { return; diff --git a/src/app/nebula_controller.h b/src/app/nebula_controller.h index 6a070d8..97a1825 100644 --- a/src/app/nebula_controller.h +++ b/src/app/nebula_controller.h @@ -58,6 +58,7 @@ private: void CreateMenuPopupBrowser(); void PositionMenuPopup(); void SendMenuPopupZoom(); + void SendThemeToChromeSurfaces(const std::string& theme_json); void ToggleDevTools(); void AdjustZoom(double delta); void FreshReload(); @@ -67,6 +68,7 @@ private: void SendBigPictureText(const std::string& payload); void SetBigPictureBrowseVisible(bool visible); void SetContentFullscreen(bool fullscreen); + void CompleteFirstRunSetup(); void ResizeBrowsers(); void SendChromeState(const nebula::browser::NebulaTab& tab); void SendBigPictureState(const nebula::browser::NebulaTab& tab); @@ -87,6 +89,7 @@ private: bool big_picture_mode_ = false; bool big_picture_browse_visible_ = false; bool content_fullscreen_ = false; + bool first_run_setup_active_ = false; bool menu_popup_visible_ = false; std::unique_ptr window_; diff --git a/src/cef/browser_client.cpp b/src/cef/browser_client.cpp index 70aff0b..40c6507 100644 --- a/src/cef/browser_client.cpp +++ b/src/cef/browser_client.cpp @@ -26,6 +26,14 @@ bool IsSettingsFrame(CefRefPtr frame) { return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://settings"); } +bool IsSetupFrame(CefRefPtr frame) { + if (!frame) { + return false; + } + + return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://setup"); +} + bool IsBigPictureFrame(CefRefPtr frame) { if (!frame) { return false; @@ -105,10 +113,12 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr browser command == "new-tab" || command == "clear-site-history" || command == "clear-search-history"); + const bool allowed_setup_command = + command == "complete-first-run" && IsSetupFrame(frame); const bool allowed_big_picture_command = IsBigPictureFrame(frame) && command == "exit-bigpicture"; if (!allowed_insecure_command && !allowed_settings_command && - !allowed_big_picture_command) { + !allowed_setup_command && !allowed_big_picture_command) { return false; } } else if (role_ == BrowserRole::BigPicture) { diff --git a/src/ui/paths.cpp b/src/ui/paths.cpp index 427c101..7afa8d3 100644 --- a/src/ui/paths.cpp +++ b/src/ui/paths.cpp @@ -147,6 +147,11 @@ std::filesystem::path GetSessionStatePath() { return user_data.empty() ? std::filesystem::path{} : user_data / "session_state.json"; } +std::filesystem::path GetFirstRunStatePath() { + auto user_data = GetUserDataDirectory(); + return user_data.empty() ? std::filesystem::path{} : user_data / "first_run_state.json"; +} + std::filesystem::path GetUiPagePath(const std::string& page_name) { const auto exe_dir = GetExecutableDirectory(); if (exe_dir.empty()) { @@ -182,6 +187,10 @@ std::string GetHomeUrl() { return InternalUrlForSlug("home"); } +std::string GetSetupUrl() { + return InternalUrlForSlug("setup"); +} + std::string GetSettingsUrl() { return InternalUrlForSlug("settings"); } diff --git a/src/ui/paths.h b/src/ui/paths.h index 3c10b76..371cafb 100644 --- a/src/ui/paths.h +++ b/src/ui/paths.h @@ -9,10 +9,12 @@ std::filesystem::path GetExecutableDirectory(); std::filesystem::path GetUserDataDirectory(); std::filesystem::path GetCacheDirectory(); std::filesystem::path GetSessionStatePath(); +std::filesystem::path GetFirstRunStatePath(); std::filesystem::path GetUiPagePath(const std::string& page_name); std::string FilePathToUrl(std::filesystem::path path); std::string GetChromeUrl(); std::string GetHomeUrl(); +std::string GetSetupUrl(); std::string GetSettingsUrl(); std::string GetDownloadsUrl(); std::string GetBigPictureUrl(); diff --git a/ui/css/chrome.css b/ui/css/chrome.css index c8cb692..361b6bb 100644 --- a/ui/css/chrome.css +++ b/ui/css/chrome.css @@ -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 { diff --git a/ui/js/chrome.js b/ui/js/chrome.js index 44eda0a..474f3d5 100644 --- a/ui/js/chrome.js +++ b/ui/js/chrome.js @@ -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); }); diff --git a/ui/js/customization.js b/ui/js/customization.js index 1576f28..91b9aa0 100644 --- a/ui/js/customization.js +++ b/ui/js/customization.js @@ -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 { diff --git a/ui/js/settings.js b/ui/js/settings.js index d6a2837..2040377 100644 --- a/ui/js/settings.js +++ b/ui/js/settings.js @@ -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)); } } }; diff --git a/ui/js/setup.js b/ui/js/setup.js index b41c7e9..d4542ed 100644 --- a/ui/js/setup.js +++ b/ui/js/setup.js @@ -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'; diff --git a/ui/pages/setup.html b/ui/pages/setup.html index ef90d74..459d916 100644 --- a/ui/pages/setup.html +++ b/ui/pages/setup.html @@ -129,6 +129,7 @@ +