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:
@@ -0,0 +1,96 @@
|
||||
#include "app/first_run_state.h"
|
||||
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
#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<unsigned char>(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
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
namespace nebula::app {
|
||||
|
||||
bool ShouldShowFirstRunSetup();
|
||||
bool WriteFirstRunState(bool first_start);
|
||||
|
||||
} // namespace nebula::app
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <fstream>
|
||||
#include <system_error>
|
||||
|
||||
#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;
|
||||
|
||||
@@ -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<nebula::window::NebulaWindow> window_;
|
||||
|
||||
@@ -26,6 +26,14 @@ bool IsSettingsFrame(CefRefPtr<CefFrame> frame) {
|
||||
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://settings");
|
||||
}
|
||||
|
||||
bool IsSetupFrame(CefRefPtr<CefFrame> frame) {
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://setup");
|
||||
}
|
||||
|
||||
bool IsBigPictureFrame(CefRefPtr<CefFrame> frame) {
|
||||
if (!frame) {
|
||||
return false;
|
||||
@@ -105,10 +113,12 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> 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) {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user