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');