From 54216aa1338432a1020daed64a3a9cd7c3e420f0 Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Thu, 14 May 2026 20:57:17 +1200 Subject: [PATCH] Persist site history to disk and integrate with settings Add persistent site history storage and plumbing between the renderer settings UI and the native app. The app now loads/saves site_history.txt in the user data directory (max 200 entries, http/https-only, stored one URL per line) and records visited sites on navigation. Settings pages receive the history via injected JavaScript when the settings page finishes loading, and a "clear-site-history" message from the settings UI clears the on-disk history and updates the renderer. Other changes: allow settings-related process messages from content frames in the CEF client, introduce OnContentLoadFinished to trigger history injection, expose electronAPI.send/sendToHost (and reuse the native postMessage handler) in the V8 context, and remove the BigPicture in-app history UI/refresh/clear handlers (history is now managed by the native app). Also cleaned up includes and added helper utilities for JSON escaping, lowercasing, and file path handling. The initial tab restore logic was simplified to always create an initial tab (home or initial_url) and persist the session. --- src/app/nebula_controller.cpp | 120 +++++++++++++++++++++++++++++++--- src/app/nebula_controller.h | 4 ++ src/cef/browser_client.cpp | 22 ++++++- src/cef/browser_client.h | 1 + src/cef/nebula_app.cpp | 16 ++++- ui/js/bigpicture.js | 24 ------- ui/pages/bigpicture.html | 32 --------- ui/pages/settings.html | 6 ++ 8 files changed, 156 insertions(+), 69 deletions(-) diff --git a/src/app/nebula_controller.cpp b/src/app/nebula_controller.cpp index a339080..740d4c8 100644 --- a/src/app/nebula_controller.cpp +++ b/src/app/nebula_controller.cpp @@ -4,6 +4,9 @@ #include #include +#include +#include +#include #include #include "browser/session_state.h" @@ -17,6 +20,8 @@ namespace nebula::app { namespace { +constexpr size_t kMaxSiteHistoryEntries = 200; + std::wstring Utf8ToWide(const std::string& value) { if (value.empty()) { return {}; @@ -30,6 +35,67 @@ std::wstring Utf8ToWide(const std::string& value) { return result; } +std::filesystem::path GetSiteHistoryPath() { + const auto user_data = nebula::ui::GetUserDataDirectory(); + return user_data.empty() ? std::filesystem::path{} : user_data / L"site_history.txt"; +} + +std::string ToLowerAscii(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) { + return static_cast(std::tolower(ch)); + }); + return value; +} + +bool IsSiteHistoryUrl(const std::string& url) { + const std::string lower = ToLowerAscii(url); + return lower.starts_with("http://") || lower.starts_with("https://"); +} + +std::vector LoadSiteHistory() { + std::vector history; + std::ifstream input(GetSiteHistoryPath(), std::ios::binary); + if (!input) { + return history; + } + + std::string url; + while (std::getline(input, url) && history.size() < kMaxSiteHistoryEntries) { + if (IsSiteHistoryUrl(url)) { + history.push_back(url); + } + } + return history; +} + +void SaveSiteHistory(const std::vector& history) { + const auto path = GetSiteHistoryPath(); + if (path.empty()) { + return; + } + + std::ofstream output(path, std::ios::binary | std::ios::trunc); + if (!output) { + return; + } + + for (const auto& url : history) { + output << url << '\n'; + } +} + +std::string SiteHistoryJson(const std::vector& history) { + std::string json = "["; + for (size_t i = 0; i < history.size(); ++i) { + if (i > 0) { + json += ","; + } + json += "\"" + nebula::browser::JsonEscape(history[i]) + "\""; + } + json += "]"; + return json; +} + CefWindowInfo ChildWindowInfo(HWND parent, const RECT& rect) { CefWindowInfo info; info.SetAsChild( @@ -141,7 +207,8 @@ NebulaController::NebulaController(HINSTANCE instance, std::string initial_url, : instance_(instance), initial_url_(std::move(initial_url)), show_command_(show_command), - tabs_(this) {} + tabs_(this), + site_history_(LoadSiteHistory()) {} NebulaController::~NebulaController() = default; @@ -152,15 +219,11 @@ bool NebulaController::Create() { void NebulaController::OnWindowCreated() { if (initial_url_.empty()) { - const auto session = nebula::browser::LoadSessionState(); - if (!session.tabs.empty()) { - tabs_.RestoreTabs(session.tabs, session.active_tab_index); - } else { - tabs_.CreateInitialTab(nebula::ui::GetHomeUrl()); - } + tabs_.CreateInitialTab(nebula::ui::GetHomeUrl()); } else { tabs_.CreateInitialTab(initial_url_); } + PersistSession(); CreateChromeBrowser(); CreateContentBrowser(); @@ -303,6 +366,12 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st CloseMenuPopup(); } else if (command == "home") { tabs_.LoadURL(nebula::ui::GetHomeUrl()); + } else if (command == "clear-site-history") { + site_history_.clear(); + SaveSiteHistory(site_history_); + if (auto* tab = tabs_.ActiveTab(); tab && tab->browser) { + InjectSettingsHistory(tab->browser); + } } else if (command == "minimize" && window_) { window_->Minimize(); } else if (command == "maximize" && window_) { @@ -315,10 +384,12 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st } void NebulaController::OnContentAddressChanged(CefRefPtr browser, const std::string& url) { + const std::string internal_url = nebula::ui::ToInternalUrl(url); tabs_.UpdateURL(browser, nebula::ui::IsChromiumNewTabUrl(url) ? nebula::ui::GetHomeUrl() - : nebula::ui::ToInternalUrl(url)); + : internal_url); + RecordSiteHistory(internal_url); PersistSession(); } @@ -339,6 +410,12 @@ void NebulaController::OnContentLoadProgressChanged(CefRefPtr browse tabs_.UpdateLoadProgress(browser, progress); } +void NebulaController::OnContentLoadFinished(CefRefPtr browser, const std::string& url) { + if (nebula::ui::ToInternalUrl(url).starts_with(nebula::ui::GetSettingsUrl())) { + InjectSettingsHistory(browser); + } +} + void NebulaController::OnContentFaviconChanged(CefRefPtr browser, const std::vector& urls) { tabs_.UpdateFavicon(browser, urls); } @@ -614,6 +691,33 @@ void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) { chrome_browser_->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetChromeUrl(), 0); } +void NebulaController::RecordSiteHistory(const std::string& url) { + if (!IsSiteHistoryUrl(url)) { + return; + } + + site_history_.erase( + std::remove(site_history_.begin(), site_history_.end(), url), + site_history_.end()); + site_history_.insert(site_history_.begin(), url); + if (site_history_.size() > kMaxSiteHistoryEntries) { + site_history_.resize(kMaxSiteHistoryEntries); + } + SaveSiteHistory(site_history_); +} + +void NebulaController::InjectSettingsHistory(CefRefPtr browser) { + if (!browser) { + return; + } + + const std::string history_json = SiteHistoryJson(site_history_); + const std::string script = + "localStorage.setItem('siteHistory', \"" + nebula::browser::JsonEscape(history_json) + "\");" + "if (typeof loadHistories === 'function') { loadHistories(); }"; + browser->GetMainFrame()->ExecuteJavaScript(script, nebula::ui::GetSettingsUrl(), 0); +} + void NebulaController::PersistSession() const { nebula::browser::SaveSessionState(tabs_.Tabs(), tabs_.ActiveTabIndex()); } diff --git a/src/app/nebula_controller.h b/src/app/nebula_controller.h index e10f488..dd08f69 100644 --- a/src/app/nebula_controller.h +++ b/src/app/nebula_controller.h @@ -33,6 +33,7 @@ public: void OnContentTitleChanged(CefRefPtr browser, const std::string& title) override; void OnContentLoadingStateChanged(CefRefPtr browser, bool is_loading) override; void OnContentLoadProgressChanged(CefRefPtr browser, double progress) override; + void OnContentLoadFinished(CefRefPtr browser, const std::string& url) override; void OnContentFaviconChanged(CefRefPtr browser, const std::vector& urls) override; void OnContentFullscreenChanged(CefRefPtr browser, bool fullscreen) override; void OnPopupRequested(CefRefPtr browser, const std::string& target_url) override; @@ -54,6 +55,8 @@ private: void SetContentFullscreen(bool fullscreen); void ResizeBrowsers(); void SendChromeState(const nebula::browser::NebulaTab& tab); + void RecordSiteHistory(const std::string& url); + void InjectSettingsHistory(CefRefPtr browser); void PersistSession() const; void MaybeFinishShutdown(); @@ -72,6 +75,7 @@ private: CefRefPtr content_client_; CefRefPtr menu_popup_client_; std::unordered_set insecure_warning_bypasses_; + std::vector site_history_; }; } // namespace nebula::app diff --git a/src/cef/browser_client.cpp b/src/cef/browser_client.cpp index b3360dd..fdf1c74 100644 --- a/src/cef/browser_client.cpp +++ b/src/cef/browser_client.cpp @@ -17,6 +17,14 @@ bool IsInsecureInterstitialFrame(CefRefPtr frame) { return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://insecure"); } +bool IsSettingsFrame(CefRefPtr frame) { + if (!frame) { + return false; + } + + return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://settings"); +} + std::vector ToStringVector(const std::vector& values) { std::vector result; result.reserve(values.size()); @@ -51,9 +59,16 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr browser CefRefPtr args = message->GetArgumentList(); const std::string command = args && args->GetSize() > 0 ? args->GetString(0).ToString() : ""; const std::string payload = args && args->GetSize() > 1 ? args->GetString(1).ToString() : ""; - if (role_ == BrowserRole::Content && - (command != "navigate-insecure" || !IsInsecureInterstitialFrame(frame))) { - return false; + if (role_ == BrowserRole::Content) { + const bool allowed_insecure_command = + command == "navigate-insecure" && IsInsecureInterstitialFrame(frame); + const bool allowed_settings_command = + IsSettingsFrame(frame) && (command == "navigate" || + command == "clear-site-history" || + command == "clear-search-history"); + if (!allowed_insecure_command && !allowed_settings_command) { + return false; + } } if (delegate_ && !command.empty()) { @@ -204,6 +219,7 @@ void NebulaBrowserClient::OnLoadEnd(CefRefPtr browser, } delegate_->OnContentLoadProgressChanged(browser, 1.0); + delegate_->OnContentLoadFinished(browser, frame->GetURL().ToString()); } } diff --git a/src/cef/browser_client.h b/src/cef/browser_client.h index e401cd0..c7a1b2d 100644 --- a/src/cef/browser_client.h +++ b/src/cef/browser_client.h @@ -29,6 +29,7 @@ public: virtual void OnContentTitleChanged(CefRefPtr browser, const std::string& title) = 0; virtual void OnContentLoadingStateChanged(CefRefPtr browser, bool is_loading) = 0; virtual void OnContentLoadProgressChanged(CefRefPtr browser, double progress) = 0; + virtual void OnContentLoadFinished(CefRefPtr browser, const std::string& url) = 0; virtual void OnContentFaviconChanged(CefRefPtr browser, const std::vector& urls) = 0; virtual void OnContentFullscreenChanged(CefRefPtr browser, bool fullscreen) = 0; virtual void OnPopupRequested(CefRefPtr browser, const std::string& target_url) = 0; diff --git a/src/cef/nebula_app.cpp b/src/cef/nebula_app.cpp index 975d294..74ac0fd 100644 --- a/src/cef/nebula_app.cpp +++ b/src/cef/nebula_app.cpp @@ -18,7 +18,7 @@ public: UNREFERENCED_PARAMETER(object); UNREFERENCED_PARAMETER(retval); - if (name != "postMessage") { + if (name != "postMessage" && name != "sendToHost" && name != "send") { return false; } @@ -88,12 +88,24 @@ void NebulaApp::OnContextCreated(CefRefPtr browser, UNREFERENCED_PARAMETER(frame); CefRefPtr global = context->GetGlobal(); + CefRefPtr handler = new NativeBridgeHandler(); CefRefPtr native = CefV8Value::CreateObject(nullptr, nullptr); native->SetValue( "postMessage", - CefV8Value::CreateFunction("postMessage", new NativeBridgeHandler()), + CefV8Value::CreateFunction("postMessage", handler), V8_PROPERTY_ATTRIBUTE_NONE); global->SetValue("nebulaNative", native, V8_PROPERTY_ATTRIBUTE_READONLY); + + CefRefPtr electron_api = CefV8Value::CreateObject(nullptr, nullptr); + electron_api->SetValue( + "sendToHost", + CefV8Value::CreateFunction("sendToHost", handler), + V8_PROPERTY_ATTRIBUTE_NONE); + electron_api->SetValue( + "send", + CefV8Value::CreateFunction("send", handler), + V8_PROPERTY_ATTRIBUTE_NONE); + global->SetValue("electronAPI", electron_api, V8_PROPERTY_ATTRIBUTE_READONLY); } } // namespace nebula::cef diff --git a/ui/js/bigpicture.js b/ui/js/bigpicture.js index 87e2422..d2cdf8c 100644 --- a/ui/js/bigpicture.js +++ b/ui/js/bigpicture.js @@ -338,20 +338,6 @@ function initNavigation() { launchNebot.addEventListener('click', () => navigateTo('nebula://nebot')); } - // History section buttons - const clearHistoryBtn = document.getElementById('clearHistoryBtn'); - if (clearHistoryBtn) { - clearHistoryBtn.addEventListener('click', clearHistory); - } - - const refreshHistoryBtn = document.getElementById('refreshHistoryBtn'); - if (refreshHistoryBtn) { - refreshHistoryBtn.addEventListener('click', async () => { - await loadHistory(); - showToast('History refreshed'); - }); - } - // Bookmarks actions const addBookmarkBtn = document.getElementById('addBookmarkBtn'); if (addBookmarkBtn) { @@ -1481,8 +1467,6 @@ async function loadHistory() { const stored = localStorage.getItem('siteHistory'); state.history = stored ? JSON.parse(stored) : []; } - renderHistory(); - renderRecentSites(); } catch (err) { console.error('[BigPicture] Failed to load history:', err); state.history = []; @@ -1505,8 +1489,6 @@ async function saveToHistory(url) { if (history.length > 100) history = history.slice(0, 100); localStorage.setItem('siteHistory', JSON.stringify(history)); state.history = history; - renderHistory(); - renderRecentSites(); } } catch (err) { console.error('[BigPicture] Failed to save history:', err); @@ -1522,8 +1504,6 @@ async function clearHistory() { localStorage.removeItem('siteHistory'); } state.history = []; - renderHistory(); - renderRecentSites(); showToast('History cleared'); } catch (err) { console.error('[BigPicture] Failed to clear history:', err); @@ -2484,8 +2464,6 @@ async function clearAllBrowsingData() { // Also clear localStorage localStorage.removeItem('siteHistory'); state.history = []; - renderHistory(); - renderRecentSites(); showToast('All browsing data cleared'); playSelectSound(); @@ -2503,8 +2481,6 @@ async function clearBrowsingHistory() { localStorage.removeItem('siteHistory'); state.history = []; - renderHistory(); - renderRecentSites(); showToast('Browsing history cleared'); playSelectSound(); diff --git a/ui/pages/bigpicture.html b/ui/pages/bigpicture.html index 9f9d3a7..f8c21e4 100644 --- a/ui/pages/bigpicture.html +++ b/ui/pages/bigpicture.html @@ -72,10 +72,6 @@ bookmarks Bookmarks - - - -
- -
- -
diff --git a/ui/pages/settings.html b/ui/pages/settings.html index 02ad328..0c8c815 100644 --- a/ui/pages/settings.html +++ b/ui/pages/settings.html @@ -534,6 +534,12 @@ async function clearSiteHistory() { try { + if (window.electronAPI && typeof window.electronAPI.sendToHost === 'function') { + window.electronAPI.sendToHost('clear-site-history'); + } else if (window.nebulaNative && typeof window.nebulaNative.postMessage === 'function') { + window.nebulaNative.postMessage('clear-site-history'); + } + // Clear from localStorage localStorage.removeItem('siteHistory'); console.log('[SETTINGS DEBUG] Cleared site history from localStorage');