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.
This commit is contained in:
2026-05-14 20:57:17 +12:00
parent 8eb5c1a3b2
commit 54216aa133
8 changed files with 156 additions and 69 deletions
+111 -7
View File
@@ -4,6 +4,9 @@
#include <algorithm> #include <algorithm>
#include <charconv> #include <charconv>
#include <cctype>
#include <filesystem>
#include <fstream>
#include <system_error> #include <system_error>
#include "browser/session_state.h" #include "browser/session_state.h"
@@ -17,6 +20,8 @@
namespace nebula::app { namespace nebula::app {
namespace { namespace {
constexpr size_t kMaxSiteHistoryEntries = 200;
std::wstring Utf8ToWide(const std::string& value) { std::wstring Utf8ToWide(const std::string& value) {
if (value.empty()) { if (value.empty()) {
return {}; return {};
@@ -30,6 +35,67 @@ std::wstring Utf8ToWide(const std::string& value) {
return result; 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<char>(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<std::string> LoadSiteHistory() {
std::vector<std::string> 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<std::string>& 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<std::string>& 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 ChildWindowInfo(HWND parent, const RECT& rect) {
CefWindowInfo info; CefWindowInfo info;
info.SetAsChild( info.SetAsChild(
@@ -141,7 +207,8 @@ NebulaController::NebulaController(HINSTANCE instance, std::string initial_url,
: instance_(instance), : instance_(instance),
initial_url_(std::move(initial_url)), initial_url_(std::move(initial_url)),
show_command_(show_command), show_command_(show_command),
tabs_(this) {} tabs_(this),
site_history_(LoadSiteHistory()) {}
NebulaController::~NebulaController() = default; NebulaController::~NebulaController() = default;
@@ -152,15 +219,11 @@ bool NebulaController::Create() {
void NebulaController::OnWindowCreated() { void NebulaController::OnWindowCreated() {
if (initial_url_.empty()) { 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 { } else {
tabs_.CreateInitialTab(initial_url_); tabs_.CreateInitialTab(initial_url_);
} }
PersistSession();
CreateChromeBrowser(); CreateChromeBrowser();
CreateContentBrowser(); CreateContentBrowser();
@@ -303,6 +366,12 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
CloseMenuPopup(); CloseMenuPopup();
} else if (command == "home") { } else if (command == "home") {
tabs_.LoadURL(nebula::ui::GetHomeUrl()); 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_) { } else if (command == "minimize" && window_) {
window_->Minimize(); window_->Minimize();
} else if (command == "maximize" && window_) { } else if (command == "maximize" && window_) {
@@ -315,10 +384,12 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
} }
void NebulaController::OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) { void NebulaController::OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) {
const std::string internal_url = nebula::ui::ToInternalUrl(url);
tabs_.UpdateURL(browser, tabs_.UpdateURL(browser,
nebula::ui::IsChromiumNewTabUrl(url) nebula::ui::IsChromiumNewTabUrl(url)
? nebula::ui::GetHomeUrl() ? nebula::ui::GetHomeUrl()
: nebula::ui::ToInternalUrl(url)); : internal_url);
RecordSiteHistory(internal_url);
PersistSession(); PersistSession();
} }
@@ -339,6 +410,12 @@ void NebulaController::OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browse
tabs_.UpdateLoadProgress(browser, progress); tabs_.UpdateLoadProgress(browser, progress);
} }
void NebulaController::OnContentLoadFinished(CefRefPtr<CefBrowser> browser, const std::string& url) {
if (nebula::ui::ToInternalUrl(url).starts_with(nebula::ui::GetSettingsUrl())) {
InjectSettingsHistory(browser);
}
}
void NebulaController::OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) { void NebulaController::OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) {
tabs_.UpdateFavicon(browser, 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); 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<CefBrowser> 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 { void NebulaController::PersistSession() const {
nebula::browser::SaveSessionState(tabs_.Tabs(), tabs_.ActiveTabIndex()); nebula::browser::SaveSessionState(tabs_.Tabs(), tabs_.ActiveTabIndex());
} }
+4
View File
@@ -33,6 +33,7 @@ public:
void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) override; void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) override;
void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) override; void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) override;
void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) override; void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) override;
void OnContentLoadFinished(CefRefPtr<CefBrowser> browser, const std::string& url) override;
void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) override; void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) override;
void OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) override; void OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) override;
void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) override; void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) override;
@@ -54,6 +55,8 @@ private:
void SetContentFullscreen(bool fullscreen); void SetContentFullscreen(bool fullscreen);
void ResizeBrowsers(); void ResizeBrowsers();
void SendChromeState(const nebula::browser::NebulaTab& tab); void SendChromeState(const nebula::browser::NebulaTab& tab);
void RecordSiteHistory(const std::string& url);
void InjectSettingsHistory(CefRefPtr<CefBrowser> browser);
void PersistSession() const; void PersistSession() const;
void MaybeFinishShutdown(); void MaybeFinishShutdown();
@@ -72,6 +75,7 @@ private:
CefRefPtr<nebula::cef::NebulaBrowserClient> content_client_; CefRefPtr<nebula::cef::NebulaBrowserClient> content_client_;
CefRefPtr<nebula::cef::NebulaBrowserClient> menu_popup_client_; CefRefPtr<nebula::cef::NebulaBrowserClient> menu_popup_client_;
std::unordered_set<std::string> insecure_warning_bypasses_; std::unordered_set<std::string> insecure_warning_bypasses_;
std::vector<std::string> site_history_;
}; };
} // namespace nebula::app } // namespace nebula::app
+18 -2
View File
@@ -17,6 +17,14 @@ bool IsInsecureInterstitialFrame(CefRefPtr<CefFrame> frame) {
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://insecure"); return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://insecure");
} }
bool IsSettingsFrame(CefRefPtr<CefFrame> frame) {
if (!frame) {
return false;
}
return nebula::ui::ToInternalUrl(frame->GetURL().ToString()).starts_with("nebula://settings");
}
std::vector<std::string> ToStringVector(const std::vector<CefString>& values) { std::vector<std::string> ToStringVector(const std::vector<CefString>& values) {
std::vector<std::string> result; std::vector<std::string> result;
result.reserve(values.size()); result.reserve(values.size());
@@ -51,10 +59,17 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser
CefRefPtr<CefListValue> args = message->GetArgumentList(); CefRefPtr<CefListValue> args = message->GetArgumentList();
const std::string command = args && args->GetSize() > 0 ? args->GetString(0).ToString() : ""; const std::string command = args && args->GetSize() > 0 ? args->GetString(0).ToString() : "";
const std::string payload = args && args->GetSize() > 1 ? args->GetString(1).ToString() : ""; const std::string payload = args && args->GetSize() > 1 ? args->GetString(1).ToString() : "";
if (role_ == BrowserRole::Content && if (role_ == BrowserRole::Content) {
(command != "navigate-insecure" || !IsInsecureInterstitialFrame(frame))) { 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; return false;
} }
}
if (delegate_ && !command.empty()) { if (delegate_ && !command.empty()) {
delegate_->OnChromeCommand(command, payload); delegate_->OnChromeCommand(command, payload);
@@ -204,6 +219,7 @@ void NebulaBrowserClient::OnLoadEnd(CefRefPtr<CefBrowser> browser,
} }
delegate_->OnContentLoadProgressChanged(browser, 1.0); delegate_->OnContentLoadProgressChanged(browser, 1.0);
delegate_->OnContentLoadFinished(browser, frame->GetURL().ToString());
} }
} }
+1
View File
@@ -29,6 +29,7 @@ public:
virtual void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) = 0; virtual void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) = 0;
virtual void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) = 0; virtual void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) = 0;
virtual void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) = 0; virtual void OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double progress) = 0;
virtual void OnContentLoadFinished(CefRefPtr<CefBrowser> browser, const std::string& url) = 0;
virtual void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) = 0; virtual void OnContentFaviconChanged(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) = 0;
virtual void OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) = 0; virtual void OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) = 0;
virtual void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) = 0; virtual void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) = 0;
+14 -2
View File
@@ -18,7 +18,7 @@ public:
UNREFERENCED_PARAMETER(object); UNREFERENCED_PARAMETER(object);
UNREFERENCED_PARAMETER(retval); UNREFERENCED_PARAMETER(retval);
if (name != "postMessage") { if (name != "postMessage" && name != "sendToHost" && name != "send") {
return false; return false;
} }
@@ -88,12 +88,24 @@ void NebulaApp::OnContextCreated(CefRefPtr<CefBrowser> browser,
UNREFERENCED_PARAMETER(frame); UNREFERENCED_PARAMETER(frame);
CefRefPtr<CefV8Value> global = context->GetGlobal(); CefRefPtr<CefV8Value> global = context->GetGlobal();
CefRefPtr<NativeBridgeHandler> handler = new NativeBridgeHandler();
CefRefPtr<CefV8Value> native = CefV8Value::CreateObject(nullptr, nullptr); CefRefPtr<CefV8Value> native = CefV8Value::CreateObject(nullptr, nullptr);
native->SetValue( native->SetValue(
"postMessage", "postMessage",
CefV8Value::CreateFunction("postMessage", new NativeBridgeHandler()), CefV8Value::CreateFunction("postMessage", handler),
V8_PROPERTY_ATTRIBUTE_NONE); V8_PROPERTY_ATTRIBUTE_NONE);
global->SetValue("nebulaNative", native, V8_PROPERTY_ATTRIBUTE_READONLY); global->SetValue("nebulaNative", native, V8_PROPERTY_ATTRIBUTE_READONLY);
CefRefPtr<CefV8Value> 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 } // namespace nebula::cef
-24
View File
@@ -338,20 +338,6 @@ function initNavigation() {
launchNebot.addEventListener('click', () => navigateTo('nebula://nebot')); 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 // Bookmarks actions
const addBookmarkBtn = document.getElementById('addBookmarkBtn'); const addBookmarkBtn = document.getElementById('addBookmarkBtn');
if (addBookmarkBtn) { if (addBookmarkBtn) {
@@ -1481,8 +1467,6 @@ async function loadHistory() {
const stored = localStorage.getItem('siteHistory'); const stored = localStorage.getItem('siteHistory');
state.history = stored ? JSON.parse(stored) : []; state.history = stored ? JSON.parse(stored) : [];
} }
renderHistory();
renderRecentSites();
} catch (err) { } catch (err) {
console.error('[BigPicture] Failed to load history:', err); console.error('[BigPicture] Failed to load history:', err);
state.history = []; state.history = [];
@@ -1505,8 +1489,6 @@ async function saveToHistory(url) {
if (history.length > 100) history = history.slice(0, 100); if (history.length > 100) history = history.slice(0, 100);
localStorage.setItem('siteHistory', JSON.stringify(history)); localStorage.setItem('siteHistory', JSON.stringify(history));
state.history = history; state.history = history;
renderHistory();
renderRecentSites();
} }
} catch (err) { } catch (err) {
console.error('[BigPicture] Failed to save history:', err); console.error('[BigPicture] Failed to save history:', err);
@@ -1522,8 +1504,6 @@ async function clearHistory() {
localStorage.removeItem('siteHistory'); localStorage.removeItem('siteHistory');
} }
state.history = []; state.history = [];
renderHistory();
renderRecentSites();
showToast('History cleared'); showToast('History cleared');
} catch (err) { } catch (err) {
console.error('[BigPicture] Failed to clear history:', err); console.error('[BigPicture] Failed to clear history:', err);
@@ -2484,8 +2464,6 @@ async function clearAllBrowsingData() {
// Also clear localStorage // Also clear localStorage
localStorage.removeItem('siteHistory'); localStorage.removeItem('siteHistory');
state.history = []; state.history = [];
renderHistory();
renderRecentSites();
showToast('All browsing data cleared'); showToast('All browsing data cleared');
playSelectSound(); playSelectSound();
@@ -2503,8 +2481,6 @@ async function clearBrowsingHistory() {
localStorage.removeItem('siteHistory'); localStorage.removeItem('siteHistory');
state.history = []; state.history = [];
renderHistory();
renderRecentSites();
showToast('Browsing history cleared'); showToast('Browsing history cleared');
playSelectSound(); playSelectSound();
-32
View File
@@ -72,10 +72,6 @@
<span class="material-symbols-outlined">bookmarks</span> <span class="material-symbols-outlined">bookmarks</span>
<span class="nav-label">Bookmarks</span> <span class="nav-label">Bookmarks</span>
</button> </button>
<button class="nav-item" data-section="history" data-focusable tabindex="0">
<span class="material-symbols-outlined">history</span>
<span class="nav-label">History</span>
</button>
<button class="nav-item" data-section="downloads" data-focusable tabindex="0"> <button class="nav-item" data-section="downloads" data-focusable tabindex="0">
<span class="material-symbols-outlined">download</span> <span class="material-symbols-outlined">download</span>
<span class="nav-label">Downloads</span> <span class="nav-label">Downloads</span>
@@ -123,13 +119,6 @@
</div> </div>
</div> </div>
<!-- Recent sites -->
<div class="recent-sites">
<h2 class="subsection-title">Continue Browsing</h2>
<div class="horizontal-scroll" id="recentSitesScroll">
<!-- Recent sites will be populated dynamically -->
</div>
</div>
</section> </section>
<!-- Browse section (for webview) --> <!-- Browse section (for webview) -->
@@ -158,27 +147,6 @@
</div> </div>
</section> </section>
<!-- History section -->
<section id="section-history" class="bp-section">
<div class="section-header">
<h1 class="section-title">History</h1>
<p class="section-subtitle">Recently visited sites</p>
</div>
<div class="section-actions">
<button class="action-btn" id="clearHistoryBtn" data-focusable tabindex="0">
<span class="material-symbols-outlined">delete_sweep</span>
<span>Clear History</span>
</button>
<button class="action-btn" id="refreshHistoryBtn" data-focusable tabindex="0">
<span class="material-symbols-outlined">refresh</span>
<span>Refresh</span>
</button>
</div>
<div class="list-container" id="historyList">
<!-- History will be populated dynamically -->
</div>
</section>
<!-- Downloads section --> <!-- Downloads section -->
<section id="section-downloads" class="bp-section"> <section id="section-downloads" class="bp-section">
<div class="section-header"> <div class="section-header">
+6
View File
@@ -534,6 +534,12 @@
async function clearSiteHistory() { async function clearSiteHistory() {
try { 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 // Clear from localStorage
localStorage.removeItem('siteHistory'); localStorage.removeItem('siteHistory');
console.log('[SETTINGS DEBUG] Cleared site history from localStorage'); console.log('[SETTINGS DEBUG] Cleared site history from localStorage');