Compare commits
7 Commits
dd6b3fa70d
...
CEF
| Author | SHA1 | Date | |
|---|---|---|---|
| 18bc607d93 | |||
| 54216aa133 | |||
| 8eb5c1a3b2 | |||
| 406d73c10f | |||
| 6fac7e320b | |||
| a32940a3f3 | |||
| 10180b7109 |
@@ -41,6 +41,7 @@ set(NEBULA_SOURCES
|
||||
app/main.cpp
|
||||
src/app/nebula_controller.cpp
|
||||
src/app/run.cpp
|
||||
src/browser/session_state.cpp
|
||||
src/browser/tab.cpp
|
||||
src/browser/tab_manager.cpp
|
||||
src/browser/url_utils.cpp
|
||||
|
||||
+227
-13
@@ -4,17 +4,24 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <system_error>
|
||||
|
||||
#include "browser/session_state.h"
|
||||
#include "browser/url_utils.h"
|
||||
#include "include/cef_app.h"
|
||||
#include "include/cef_browser.h"
|
||||
#include "include/cef_cookie.h"
|
||||
#include "include/wrapper/cef_helpers.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::app {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxSiteHistoryEntries = 200;
|
||||
|
||||
std::wstring Utf8ToWide(const std::string& value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
@@ -28,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<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 info;
|
||||
info.SetAsChild(
|
||||
@@ -37,9 +105,19 @@ CefWindowInfo ChildWindowInfo(HWND parent, const RECT& rect) {
|
||||
rect.top,
|
||||
rect.right - rect.left,
|
||||
rect.bottom - rect.top));
|
||||
// CEF defaults to the Chrome runtime style, which ignores the
|
||||
// SetAsChild hint and creates a top-level window per browser. Force the
|
||||
// Alloy runtime style so each browser embeds inside the Nebula HWND.
|
||||
info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
|
||||
return info;
|
||||
}
|
||||
|
||||
CefBrowserSettings BrowserSettings() {
|
||||
CefBrowserSettings settings;
|
||||
settings.webgl = STATE_ENABLED;
|
||||
return settings;
|
||||
}
|
||||
|
||||
int ParseTabId(const std::string& value) {
|
||||
int tab_id = 0;
|
||||
const auto result = std::from_chars(value.data(), value.data() + value.size(), tab_id);
|
||||
@@ -55,7 +133,7 @@ RECT MenuPopupRect(HWND hwnd, const nebula::window::BrowserLayout& layout) {
|
||||
GetClientRect(hwnd, &client);
|
||||
|
||||
const int width = ScaleForWindow(hwnd, 260);
|
||||
const int height = ScaleForWindow(hwnd, 218);
|
||||
const int height = ScaleForWindow(hwnd, 258);
|
||||
const int margin = ScaleForWindow(hwnd, 12);
|
||||
const int overlap = ScaleForWindow(hwnd, 2);
|
||||
|
||||
@@ -103,6 +181,10 @@ std::string WithCacheBuster(std::string url) {
|
||||
return url + separator + "nebula_cache_bust=" + std::to_string(GetTickCount64()) + fragment;
|
||||
}
|
||||
|
||||
std::string GetChromeDisplayUrl(const std::string& url) {
|
||||
return nebula::ui::IsInternalHomeUrl(url) ? std::string{} : url;
|
||||
}
|
||||
|
||||
void SetBrowserVisible(CefRefPtr<CefBrowser> browser, bool visible) {
|
||||
if (!browser) {
|
||||
return;
|
||||
@@ -125,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;
|
||||
|
||||
@@ -135,7 +218,13 @@ bool NebulaController::Create() {
|
||||
}
|
||||
|
||||
void NebulaController::OnWindowCreated() {
|
||||
tabs_.CreateInitialTab(initial_url_.empty() ? nebula::ui::GetHomeUrl() : initial_url_);
|
||||
if (initial_url_.empty()) {
|
||||
tabs_.CreateInitialTab(nebula::ui::GetHomeUrl());
|
||||
} else {
|
||||
tabs_.CreateInitialTab(initial_url_);
|
||||
}
|
||||
PersistSession();
|
||||
|
||||
CreateChromeBrowser();
|
||||
CreateContentBrowser();
|
||||
}
|
||||
@@ -147,11 +236,23 @@ void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layo
|
||||
|
||||
void NebulaController::OnWindowCloseRequested() {
|
||||
if (closing_) {
|
||||
// CEF re-sends WM_CLOSE to the top-level window after each Alloy
|
||||
// child browser finishes its JS unload + DoClose phase. Destroy the
|
||||
// Nebula window now so CEF can tear down the child browser HWNDs and
|
||||
// fire OnBeforeClose; MaybeFinishShutdown will then quit the loop.
|
||||
if (window_ && window_->hwnd()) {
|
||||
DestroyWindow(window_->hwnd());
|
||||
}
|
||||
MaybeFinishShutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
closing_ = true;
|
||||
PersistSession();
|
||||
if (auto cookie_manager = CefCookieManager::GetGlobalManager(nullptr)) {
|
||||
cookie_manager->FlushStore(nullptr);
|
||||
}
|
||||
|
||||
if (chrome_browser_) {
|
||||
chrome_browser_->GetHost()->CloseBrowser(false);
|
||||
}
|
||||
@@ -163,7 +264,6 @@ void NebulaController::OnWindowCloseRequested() {
|
||||
tab.browser->GetHost()->CloseBrowser(false);
|
||||
}
|
||||
}
|
||||
MaybeFinishShutdown();
|
||||
}
|
||||
|
||||
void NebulaController::OnActiveTabChanged(const nebula::browser::NebulaTab& tab) {
|
||||
@@ -201,6 +301,12 @@ void NebulaController::OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr
|
||||
menu_popup_browser_ = nullptr;
|
||||
menu_popup_client_ = nullptr;
|
||||
} else {
|
||||
if (content_fullscreen_) {
|
||||
const auto* active_tab = tabs_.ActiveTab();
|
||||
if (active_tab && active_tab->browser && active_tab->browser->IsSame(browser)) {
|
||||
SetContentFullscreen(false);
|
||||
}
|
||||
}
|
||||
tabs_.ClearBrowser(browser);
|
||||
}
|
||||
MaybeFinishShutdown();
|
||||
@@ -209,6 +315,12 @@ void NebulaController::OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr
|
||||
void NebulaController::OnChromeCommand(const std::string& command, const std::string& payload) {
|
||||
if (command == "navigate") {
|
||||
tabs_.LoadURL(payload);
|
||||
} else if (command == "navigate-insecure") {
|
||||
const std::string target = nebula::browser::NormalizeNavigationInput(payload);
|
||||
if (nebula::ui::IsHttpUrl(target)) {
|
||||
insecure_warning_bypasses_.insert(target);
|
||||
tabs_.LoadURL(target);
|
||||
}
|
||||
} else if (command == "new-tab") {
|
||||
CreateNewTab();
|
||||
} else if (command == "activate-tab") {
|
||||
@@ -233,6 +345,9 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
|
||||
} else if (command == "big-picture") {
|
||||
CloseMenuPopup();
|
||||
tabs_.LoadURL(nebula::ui::GetBigPictureUrl());
|
||||
} else if (command == "gpu-diagnostics") {
|
||||
CloseMenuPopup();
|
||||
tabs_.LoadURL(nebula::ui::GetGpuDiagnosticsUrl());
|
||||
} else if (command == "toggle-devtools") {
|
||||
ToggleDevTools();
|
||||
} else if (command == "zoom-out") {
|
||||
@@ -251,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_) {
|
||||
@@ -263,11 +384,18 @@ void NebulaController::OnChromeCommand(const std::string& command, const std::st
|
||||
}
|
||||
|
||||
void NebulaController::OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) {
|
||||
tabs_.UpdateURL(browser, nebula::ui::IsChromiumNewTabUrl(url) ? nebula::ui::GetHomeUrl() : url);
|
||||
const std::string internal_url = nebula::ui::ToInternalUrl(url);
|
||||
tabs_.UpdateURL(browser,
|
||||
nebula::ui::IsChromiumNewTabUrl(url)
|
||||
? nebula::ui::GetHomeUrl()
|
||||
: internal_url);
|
||||
RecordSiteHistory(internal_url);
|
||||
PersistSession();
|
||||
}
|
||||
|
||||
void NebulaController::OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) {
|
||||
tabs_.UpdateTitle(browser, title);
|
||||
PersistSession();
|
||||
const auto* active_tab = tabs_.ActiveTab();
|
||||
if (window_ && active_tab && active_tab->browser && active_tab->browser->IsSame(browser)) {
|
||||
window_->SetTitle(Utf8ToWide(title.empty() ? "Nebula Browser" : title + " - Nebula"));
|
||||
@@ -282,10 +410,25 @@ void NebulaController::OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browse
|
||||
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) {
|
||||
tabs_.UpdateFavicon(browser, urls);
|
||||
}
|
||||
|
||||
void NebulaController::OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) {
|
||||
const auto* active_tab = tabs_.ActiveTab();
|
||||
if (!active_tab || !active_tab->browser || !active_tab->browser->IsSame(browser)) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetContentFullscreen(fullscreen);
|
||||
}
|
||||
|
||||
void NebulaController::OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) {
|
||||
if (!tabs_.OwnsBrowser(browser)) {
|
||||
return;
|
||||
@@ -296,12 +439,27 @@ void NebulaController::OnPopupRequested(CefRefPtr<CefBrowser> browser, const std
|
||||
: target_url);
|
||||
}
|
||||
|
||||
bool NebulaController::ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) {
|
||||
if (!tabs_.OwnsBrowser(browser)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto bypass = insecure_warning_bypasses_.find(target_url);
|
||||
if (bypass == insecure_warning_bypasses_.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
insecure_warning_bypasses_.erase(bypass);
|
||||
return true;
|
||||
}
|
||||
|
||||
void NebulaController::CreateNewTab() {
|
||||
if (auto* tab = tabs_.ActiveTab()) {
|
||||
SetBrowserVisible(tab->browser, false);
|
||||
}
|
||||
|
||||
tabs_.CreateTab(nebula::ui::GetHomeUrl());
|
||||
PersistSession();
|
||||
CreateContentBrowser();
|
||||
}
|
||||
|
||||
@@ -315,6 +473,7 @@ void NebulaController::ActivateTab(int tab_id) {
|
||||
if (!tabs_.ActivateTab(tab_id)) {
|
||||
return;
|
||||
}
|
||||
PersistSession();
|
||||
|
||||
SetBrowserVisible(previous_browser, false);
|
||||
if (auto* active_tab = tabs_.ActiveTab()) {
|
||||
@@ -334,12 +493,14 @@ void NebulaController::CloseTab(int tab_id) {
|
||||
}();
|
||||
|
||||
CefRefPtr<CefBrowser> closing_browser = tabs_.CloseTab(tab_id);
|
||||
PersistSession();
|
||||
if (closing_browser) {
|
||||
closing_browser->GetHost()->CloseBrowser(false);
|
||||
}
|
||||
|
||||
if (!tabs_.ActiveTab()) {
|
||||
tabs_.CreateTab(nebula::ui::GetHomeUrl());
|
||||
PersistSession();
|
||||
CreateContentBrowser();
|
||||
return;
|
||||
}
|
||||
@@ -358,7 +519,7 @@ void NebulaController::CreateChromeBrowser() {
|
||||
}
|
||||
|
||||
const auto layout = window_->CurrentLayout();
|
||||
CefBrowserSettings browser_settings;
|
||||
CefBrowserSettings browser_settings = BrowserSettings();
|
||||
chrome_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::Chrome, this);
|
||||
CefWindowInfo window_info = ChildWindowInfo(window_->hwnd(), layout.chrome);
|
||||
CefBrowserHost::CreateBrowser(
|
||||
@@ -373,10 +534,11 @@ void NebulaController::CreateContentBrowser() {
|
||||
const auto* tab = tabs_.ActiveTab();
|
||||
const std::string url = tab && !tab->url.empty() ? tab->url : nebula::ui::GetHomeUrl();
|
||||
const auto layout = window_->CurrentLayout();
|
||||
CefBrowserSettings browser_settings;
|
||||
CefBrowserSettings browser_settings = BrowserSettings();
|
||||
content_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::Content, this);
|
||||
CefWindowInfo window_info = ChildWindowInfo(window_->hwnd(), layout.content);
|
||||
CefBrowserHost::CreateBrowser(window_info, content_client_, url, browser_settings, nullptr, nullptr);
|
||||
CefBrowserHost::CreateBrowser(
|
||||
window_info, content_client_, nebula::ui::ResolveInternalUrl(url), browser_settings, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void NebulaController::ToggleMenuPopup() {
|
||||
@@ -400,7 +562,7 @@ void NebulaController::CreateMenuPopupBrowser() {
|
||||
}
|
||||
|
||||
const auto layout = window_->CurrentLayout();
|
||||
CefBrowserSettings browser_settings;
|
||||
CefBrowserSettings browser_settings = BrowserSettings();
|
||||
menu_popup_client_ = new nebula::cef::NebulaBrowserClient(nebula::cef::BrowserRole::MenuPopup, this);
|
||||
CefWindowInfo window_info = ChildWindowInfo(window_->hwnd(), MenuPopupRect(window_->hwnd(), layout));
|
||||
CefBrowserHost::CreateBrowser(
|
||||
@@ -408,7 +570,7 @@ void NebulaController::CreateMenuPopupBrowser() {
|
||||
}
|
||||
|
||||
void NebulaController::PositionMenuPopup() {
|
||||
if (!window_ || !window_->hwnd() || !menu_popup_browser_) {
|
||||
if (content_fullscreen_ || !window_ || !window_->hwnd() || !menu_popup_browser_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -433,6 +595,7 @@ void NebulaController::ToggleDevTools() {
|
||||
|
||||
CefWindowInfo window_info;
|
||||
window_info.SetAsPopup(window_->hwnd(), "Nebula Developer Tools");
|
||||
window_info.runtime_style = CEF_RUNTIME_STYLE_ALLOY;
|
||||
CefBrowserSettings browser_settings;
|
||||
host->ShowDevTools(window_info, content_client_, browser_settings, CefPoint());
|
||||
}
|
||||
@@ -456,19 +619,38 @@ void NebulaController::FreshReload() {
|
||||
tabs_.LoadURL(WithCacheBuster(tab->url));
|
||||
}
|
||||
|
||||
void NebulaController::SetContentFullscreen(bool fullscreen) {
|
||||
if (content_fullscreen_ == fullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
content_fullscreen_ = fullscreen;
|
||||
if (fullscreen) {
|
||||
CloseMenuPopup();
|
||||
}
|
||||
|
||||
SetBrowserVisible(chrome_browser_, !fullscreen);
|
||||
if (window_) {
|
||||
window_->SetFullscreen(fullscreen);
|
||||
}
|
||||
ResizeBrowsers();
|
||||
}
|
||||
|
||||
void NebulaController::ResizeBrowsers() {
|
||||
if (!window_) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto layout = window_->CurrentLayout();
|
||||
const auto layout = window_->CurrentLayout(!content_fullscreen_);
|
||||
if (chrome_browser_) {
|
||||
window_->ResizeChild(chrome_browser_->GetHost()->GetWindowHandle(), layout.chrome);
|
||||
}
|
||||
if (const auto* tab = tabs_.ActiveTab(); tab && tab->browser) {
|
||||
window_->ResizeChild(tab->browser->GetHost()->GetWindowHandle(), layout.content);
|
||||
}
|
||||
PositionMenuPopup();
|
||||
if (!content_fullscreen_) {
|
||||
PositionMenuPopup();
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
|
||||
@@ -476,6 +658,7 @@ void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string display_url = GetChromeDisplayUrl(tab.url);
|
||||
std::string tabs_json = "[";
|
||||
const auto& tabs = tabs_.Tabs();
|
||||
for (size_t i = 0; i < tabs.size(); ++i) {
|
||||
@@ -495,7 +678,7 @@ void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
|
||||
const std::string script =
|
||||
"window.NebulaChrome && window.NebulaChrome.applyState({"
|
||||
"\"id\":" + std::to_string(tab.id) +
|
||||
",\"url\":\"" + nebula::browser::JsonEscape(tab.url) + "\""
|
||||
",\"url\":\"" + nebula::browser::JsonEscape(display_url) + "\""
|
||||
",\"title\":\"" + nebula::browser::JsonEscape(tab.title) + "\""
|
||||
",\"isLoading\":" + std::string(tab.is_loading ? "true" : "false") +
|
||||
",\"progress\":" + std::to_string(tab.load_progress) +
|
||||
@@ -508,6 +691,37 @@ 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<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 {
|
||||
nebula::browser::SaveSessionState(tabs_.Tabs(), tabs_.ActiveTabIndex());
|
||||
}
|
||||
|
||||
void NebulaController::MaybeFinishShutdown() {
|
||||
if (!closing_) {
|
||||
return;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "browser/tab_manager.h"
|
||||
@@ -32,8 +33,11 @@ public:
|
||||
void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) override;
|
||||
void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) 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 OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) override;
|
||||
void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) override;
|
||||
bool ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) override;
|
||||
|
||||
private:
|
||||
void CreateNewTab();
|
||||
@@ -48,8 +52,12 @@ private:
|
||||
void ToggleDevTools();
|
||||
void AdjustZoom(double delta);
|
||||
void FreshReload();
|
||||
void SetContentFullscreen(bool fullscreen);
|
||||
void ResizeBrowsers();
|
||||
void SendChromeState(const nebula::browser::NebulaTab& tab);
|
||||
void RecordSiteHistory(const std::string& url);
|
||||
void InjectSettingsHistory(CefRefPtr<CefBrowser> browser);
|
||||
void PersistSession() const;
|
||||
void MaybeFinishShutdown();
|
||||
|
||||
HINSTANCE instance_ = nullptr;
|
||||
@@ -57,6 +65,7 @@ private:
|
||||
int show_command_ = SW_SHOWDEFAULT;
|
||||
bool closing_ = false;
|
||||
bool chrome_ready_ = false;
|
||||
bool content_fullscreen_ = false;
|
||||
|
||||
std::unique_ptr<nebula::window::NebulaWindow> window_;
|
||||
nebula::browser::TabManager tabs_;
|
||||
@@ -65,6 +74,8 @@ private:
|
||||
CefRefPtr<nebula::cef::NebulaBrowserClient> chrome_client_;
|
||||
CefRefPtr<nebula::cef::NebulaBrowserClient> content_client_;
|
||||
CefRefPtr<nebula::cef::NebulaBrowserClient> menu_popup_client_;
|
||||
std::unordered_set<std::string> insecure_warning_bypasses_;
|
||||
std::vector<std::string> site_history_;
|
||||
};
|
||||
|
||||
} // namespace nebula::app
|
||||
|
||||
+40
-1
@@ -9,10 +9,30 @@
|
||||
namespace nebula::app {
|
||||
namespace {
|
||||
|
||||
constexpr wchar_t kMainInstanceMutexName[] = L"Local\\NebulaBrowserMainInstance";
|
||||
|
||||
void EnableDpiAwareness() {
|
||||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
}
|
||||
|
||||
class ScopedHandle {
|
||||
public:
|
||||
explicit ScopedHandle(HANDLE handle) : handle_(handle) {}
|
||||
~ScopedHandle() {
|
||||
if (handle_) {
|
||||
CloseHandle(handle_);
|
||||
}
|
||||
}
|
||||
|
||||
ScopedHandle(const ScopedHandle&) = delete;
|
||||
ScopedHandle& operator=(const ScopedHandle&) = delete;
|
||||
|
||||
bool valid() const { return handle_ != nullptr; }
|
||||
|
||||
private:
|
||||
HANDLE handle_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int RunNebula(HINSTANCE instance, int show_command) {
|
||||
@@ -26,8 +46,27 @@ int RunNebula(HINSTANCE instance, int show_command) {
|
||||
return subprocess_exit_code;
|
||||
}
|
||||
|
||||
ScopedHandle main_instance_mutex(CreateMutexW(nullptr, TRUE, kMainInstanceMutexName));
|
||||
if (main_instance_mutex.valid() && GetLastError() == ERROR_ALREADY_EXISTS) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
CefSettings settings;
|
||||
settings.no_sandbox = true;
|
||||
settings.persist_session_cookies = true;
|
||||
|
||||
// A persistent profile is required for the GPU shader cache and several
|
||||
// hardware acceleration features. Without these Chromium silently falls
|
||||
// back to software rendering, which causes choppy video and disables
|
||||
// WebGL/WebGL2 in the GPU diagnostics page.
|
||||
const std::wstring user_data_dir = nebula::ui::GetUserDataDirectory().wstring();
|
||||
const std::wstring cache_dir = nebula::ui::GetCacheDirectory().wstring();
|
||||
if (!user_data_dir.empty()) {
|
||||
CefString(&settings.root_cache_path).FromWString(user_data_dir);
|
||||
}
|
||||
if (!cache_dir.empty()) {
|
||||
CefString(&settings.cache_path).FromWString(cache_dir);
|
||||
}
|
||||
|
||||
if (!CefInitialize(main_args, settings, app, nullptr)) {
|
||||
return CefGetExitCode();
|
||||
@@ -37,7 +76,7 @@ int RunNebula(HINSTANCE instance, int show_command) {
|
||||
command_line->InitFromString(GetCommandLineW());
|
||||
|
||||
std::string initial_url = command_line->GetSwitchValue("url");
|
||||
if (nebula::ui::IsEmptyOrChromiumNewTabUrl(initial_url)) {
|
||||
if (!initial_url.empty() && nebula::ui::IsChromiumNewTabUrl(initial_url)) {
|
||||
initial_url = nebula::ui::GetHomeUrl();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "browser/session_state.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
|
||||
#include "browser/url_utils.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxRestoredTabs = 50;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
std::optional<size_t> ReadUnsignedValue(const std::string& json, std::string_view key) {
|
||||
const size_t key_pos = json.find(key);
|
||||
if (key_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t colon = json.find(':', key_pos + key.size());
|
||||
if (colon == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
++colon;
|
||||
while (colon < json.size() && std::isspace(static_cast<unsigned char>(json[colon]))) {
|
||||
++colon;
|
||||
}
|
||||
|
||||
size_t end = colon;
|
||||
while (end < json.size() && std::isdigit(static_cast<unsigned char>(json[end]))) {
|
||||
++end;
|
||||
}
|
||||
|
||||
size_t value = 0;
|
||||
const auto result = std::from_chars(json.data() + colon, json.data() + end, value);
|
||||
if (result.ec != std::errc{} || result.ptr != json.data() + end) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
std::optional<std::string> ReadStringValue(const std::string& object, std::string_view key) {
|
||||
const size_t key_pos = object.find(key);
|
||||
if (key_pos == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t colon = object.find(':', key_pos + key.size());
|
||||
if (colon == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t quote = object.find('"', colon + 1);
|
||||
if (quote == std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string value;
|
||||
for (size_t i = quote + 1; i < object.size(); ++i) {
|
||||
const char ch = object[i];
|
||||
if (ch == '"') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (ch != '\\') {
|
||||
value += ch;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (++i >= object.size()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
switch (object[i]) {
|
||||
case '"':
|
||||
case '\\':
|
||||
case '/':
|
||||
value += object[i];
|
||||
break;
|
||||
case 'b':
|
||||
value += '\b';
|
||||
break;
|
||||
case 'f':
|
||||
value += '\f';
|
||||
break;
|
||||
case 'n':
|
||||
value += '\n';
|
||||
break;
|
||||
case 'r':
|
||||
value += '\r';
|
||||
break;
|
||||
case 't':
|
||||
value += '\t';
|
||||
break;
|
||||
default:
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<PersistedTab> ReadTabs(const std::string& json) {
|
||||
std::vector<PersistedTab> tabs;
|
||||
const size_t tabs_pos = json.find("\"tabs\"");
|
||||
if (tabs_pos == std::string::npos) {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
const size_t array_start = json.find('[', tabs_pos);
|
||||
const size_t array_end = json.find(']', array_start == std::string::npos ? tabs_pos : array_start);
|
||||
if (array_start == std::string::npos || array_end == std::string::npos) {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
size_t cursor = array_start + 1;
|
||||
while (cursor < array_end && tabs.size() < kMaxRestoredTabs) {
|
||||
const size_t object_start = json.find('{', cursor);
|
||||
if (object_start == std::string::npos || object_start >= array_end) {
|
||||
break;
|
||||
}
|
||||
|
||||
const size_t object_end = json.find('}', object_start + 1);
|
||||
if (object_end == std::string::npos || object_end > array_end) {
|
||||
break;
|
||||
}
|
||||
|
||||
const std::string object = json.substr(object_start, object_end - object_start + 1);
|
||||
const auto url = ReadStringValue(object, "\"url\"");
|
||||
if (url && !url->empty()) {
|
||||
PersistedTab tab;
|
||||
tab.url = *url;
|
||||
if (const auto title = ReadStringValue(object, "\"title\""); title && !title->empty()) {
|
||||
tab.title = *title;
|
||||
}
|
||||
tabs.push_back(std::move(tab));
|
||||
}
|
||||
|
||||
cursor = object_end + 1;
|
||||
}
|
||||
|
||||
return tabs;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SessionState LoadSessionState() {
|
||||
SessionState state;
|
||||
const std::string json = ReadFile(nebula::ui::GetSessionStatePath());
|
||||
if (json.empty()) {
|
||||
return state;
|
||||
}
|
||||
|
||||
state.tabs = ReadTabs(json);
|
||||
if (const auto active_index = ReadUnsignedValue(json, "\"activeTabIndex\"")) {
|
||||
state.active_tab_index = *active_index;
|
||||
}
|
||||
|
||||
if (!state.tabs.empty()) {
|
||||
state.active_tab_index = std::min(state.active_tab_index, state.tabs.size() - 1);
|
||||
} else {
|
||||
state.active_tab_index = 0;
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
void SaveSessionState(const std::vector<NebulaTab>& tabs, size_t active_tab_index) {
|
||||
const auto path = nebula::ui::GetSessionStatePath();
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::ostringstream json;
|
||||
json << "{\n \"activeTabIndex\": " << active_tab_index << ",\n \"tabs\": [\n";
|
||||
|
||||
bool wrote_tab = false;
|
||||
for (const auto& tab : tabs) {
|
||||
if (tab.url.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (wrote_tab) {
|
||||
json << ",\n";
|
||||
}
|
||||
|
||||
json << " {\"url\": \"" << JsonEscape(tab.url)
|
||||
<< "\", \"title\": \"" << JsonEscape(tab.title) << "\"}";
|
||||
wrote_tab = true;
|
||||
}
|
||||
|
||||
json << "\n ]\n}\n";
|
||||
|
||||
std::filesystem::path temp_path = path;
|
||||
temp_path += L".tmp";
|
||||
{
|
||||
std::ofstream output(temp_path, std::ios::binary | std::ios::trunc);
|
||||
if (!output) {
|
||||
return;
|
||||
}
|
||||
output << json.str();
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
ec.clear();
|
||||
std::filesystem::rename(temp_path, path, ec);
|
||||
}
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "browser/tab.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
struct PersistedTab {
|
||||
std::string url;
|
||||
std::string title = "New Tab";
|
||||
};
|
||||
|
||||
struct SessionState {
|
||||
std::vector<PersistedTab> tabs;
|
||||
size_t active_tab_index = 0;
|
||||
};
|
||||
|
||||
SessionState LoadSessionState();
|
||||
void SaveSessionState(const std::vector<NebulaTab>& tabs, size_t active_tab_index);
|
||||
|
||||
} // namespace nebula::browser
|
||||
@@ -1,6 +1,9 @@
|
||||
#include "browser/tab_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "browser/url_utils.h"
|
||||
#include "ui/paths.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
@@ -27,6 +30,28 @@ NebulaTab& TabManager::CreateTab(std::string url) {
|
||||
return tabs_.back();
|
||||
}
|
||||
|
||||
void TabManager::RestoreTabs(const std::vector<PersistedTab>& tabs, size_t active_tab_index) {
|
||||
tabs_.clear();
|
||||
active_tab_id_ = 0;
|
||||
|
||||
for (const auto& restored_tab : tabs) {
|
||||
NebulaTab tab;
|
||||
tab.id = next_tab_id_++;
|
||||
tab.url = restored_tab.url.empty() ? nebula::ui::GetHomeUrl() : restored_tab.url;
|
||||
tab.title = restored_tab.title.empty() ? "New Tab" : restored_tab.title;
|
||||
tabs_.push_back(std::move(tab));
|
||||
}
|
||||
|
||||
if (tabs_.empty()) {
|
||||
CreateInitialTab(nebula::ui::GetHomeUrl());
|
||||
return;
|
||||
}
|
||||
|
||||
active_tab_index = std::min(active_tab_index, tabs_.size() - 1);
|
||||
active_tab_id_ = tabs_[active_tab_index].id;
|
||||
Notify();
|
||||
}
|
||||
|
||||
NebulaTab* TabManager::ActiveTab() {
|
||||
for (auto& tab : tabs_) {
|
||||
if (tab.id == active_tab_id_) {
|
||||
@@ -49,6 +74,15 @@ const std::vector<NebulaTab>& TabManager::Tabs() const {
|
||||
return tabs_;
|
||||
}
|
||||
|
||||
size_t TabManager::ActiveTabIndex() const {
|
||||
for (size_t i = 0; i < tabs_.size(); ++i) {
|
||||
if (tabs_[i].id == active_tab_id_) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool TabManager::ActivateTab(int tab_id) {
|
||||
if (!FindTab(tab_id)) {
|
||||
return false;
|
||||
@@ -134,7 +168,7 @@ void TabManager::LoadURL(const std::string& input) {
|
||||
|
||||
tab->url = target;
|
||||
tab->favicon_url.clear();
|
||||
tab->browser->GetMainFrame()->LoadURL(target);
|
||||
tab->browser->GetMainFrame()->LoadURL(nebula::ui::ResolveInternalUrl(target));
|
||||
Notify();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "browser/tab.h"
|
||||
#include "browser/session_state.h"
|
||||
|
||||
namespace nebula::browser {
|
||||
|
||||
@@ -19,9 +21,11 @@ public:
|
||||
|
||||
NebulaTab& CreateInitialTab(std::string initial_url);
|
||||
NebulaTab& CreateTab(std::string url);
|
||||
void RestoreTabs(const std::vector<PersistedTab>& tabs, size_t active_tab_index);
|
||||
NebulaTab* ActiveTab();
|
||||
const NebulaTab* ActiveTab() const;
|
||||
const std::vector<NebulaTab>& Tabs() const;
|
||||
size_t ActiveTabIndex() const;
|
||||
|
||||
bool ActivateTab(int tab_id);
|
||||
CefRefPtr<CefBrowser> CloseTab(int tab_id);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "browser/url_utils.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
@@ -19,13 +20,17 @@ std::string Trim(std::string value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
bool StartsWithScheme(const std::string& value) {
|
||||
bool StartsWithScheme(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
});
|
||||
return value.starts_with("http://") ||
|
||||
value.starts_with("https://") ||
|
||||
value.starts_with("file:") ||
|
||||
value.starts_with("data:") ||
|
||||
value.starts_with("blob:") ||
|
||||
value.starts_with("chrome:");
|
||||
value.starts_with("chrome:") ||
|
||||
value.starts_with("nebula://");
|
||||
}
|
||||
|
||||
bool LooksLikeHostName(const std::string& value) {
|
||||
|
||||
@@ -9,6 +9,22 @@ namespace {
|
||||
|
||||
constexpr char kChromeCommandMessage[] = "NebulaChromeCommand";
|
||||
|
||||
bool IsInsecureInterstitialFrame(CefRefPtr<CefFrame> frame) {
|
||||
if (!frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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> result;
|
||||
result.reserve(values.size());
|
||||
@@ -29,17 +45,32 @@ bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser
|
||||
CefRefPtr<CefProcessMessage> message) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(browser);
|
||||
UNREFERENCED_PARAMETER(frame);
|
||||
UNREFERENCED_PARAMETER(source_process);
|
||||
|
||||
if ((role_ != BrowserRole::Chrome && role_ != BrowserRole::MenuPopup) || !message ||
|
||||
message->GetName().ToString() != kChromeCommandMessage) {
|
||||
if (!message || message->GetName().ToString() != kChromeCommandMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (role_ != BrowserRole::Chrome && role_ != BrowserRole::MenuPopup &&
|
||||
role_ != BrowserRole::Content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CefRefPtr<CefListValue> 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) {
|
||||
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()) {
|
||||
delegate_->OnChromeCommand(command, payload);
|
||||
return true;
|
||||
@@ -73,6 +104,13 @@ void NebulaBrowserClient::OnFaviconURLChange(CefRefPtr<CefBrowser> browser,
|
||||
}
|
||||
}
|
||||
|
||||
void NebulaBrowserClient::OnFullscreenModeChange(CefRefPtr<CefBrowser> browser, bool fullscreen) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
if (role_ == BrowserRole::Content && delegate_) {
|
||||
delegate_->OnContentFullscreenChanged(browser, fullscreen);
|
||||
}
|
||||
}
|
||||
|
||||
bool NebulaBrowserClient::OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
|
||||
const CefKeyEvent& event,
|
||||
CefEventHandle os_event,
|
||||
@@ -172,10 +210,16 @@ void NebulaBrowserClient::OnLoadEnd(CefRefPtr<CefBrowser> browser,
|
||||
CefRefPtr<CefFrame> frame,
|
||||
int httpStatusCode) {
|
||||
CEF_REQUIRE_UI_THREAD();
|
||||
UNREFERENCED_PARAMETER(httpStatusCode);
|
||||
|
||||
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
|
||||
if (httpStatusCode == 404) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(
|
||||
nebula::ui::GetNotFoundUrl(frame->GetURL().ToString())));
|
||||
return;
|
||||
}
|
||||
|
||||
delegate_->OnContentLoadProgressChanged(browser, 1.0);
|
||||
delegate_->OnContentLoadFinished(browser, frame->GetURL().ToString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,10 +233,24 @@ bool NebulaBrowserClient::OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
|
||||
UNREFERENCED_PARAMETER(user_gesture);
|
||||
UNREFERENCED_PARAMETER(is_redirect);
|
||||
|
||||
if (role_ == BrowserRole::Content && frame && frame->IsMain() && request &&
|
||||
nebula::ui::IsChromiumNewTabUrl(request->GetURL().ToString())) {
|
||||
frame->LoadURL(nebula::ui::GetHomeUrl());
|
||||
return true;
|
||||
if (role_ == BrowserRole::Content && frame && frame->IsMain() && request) {
|
||||
const std::string url = request->GetURL().ToString();
|
||||
if (nebula::ui::IsChromiumNewTabUrl(url)) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(nebula::ui::GetHomeUrl()));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nebula::ui::IsNebulaInternalUrl(url)) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(url));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nebula::ui::IsHttpUrl(url) &&
|
||||
(!delegate_ || !delegate_->ShouldBypassInsecureWarning(browser, url))) {
|
||||
frame->LoadURL(nebula::ui::ResolveInternalUrl(
|
||||
nebula::ui::GetInsecureWarningUrl(url)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -29,8 +29,11 @@ public:
|
||||
virtual void OnContentTitleChanged(CefRefPtr<CefBrowser> browser, const std::string& title) = 0;
|
||||
virtual void OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) = 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 OnContentFullscreenChanged(CefRefPtr<CefBrowser> browser, bool fullscreen) = 0;
|
||||
virtual void OnPopupRequested(CefRefPtr<CefBrowser> browser, const std::string& target_url) = 0;
|
||||
virtual bool ShouldBypassInsecureWarning(CefRefPtr<CefBrowser> browser, const std::string& target_url) = 0;
|
||||
};
|
||||
|
||||
class NebulaBrowserClient final : public CefClient,
|
||||
@@ -62,6 +65,7 @@ public:
|
||||
const CefString& title) override;
|
||||
void OnFaviconURLChange(CefRefPtr<CefBrowser> browser,
|
||||
const std::vector<CefString>& icon_urls) override;
|
||||
void OnFullscreenModeChange(CefRefPtr<CefBrowser> browser, bool fullscreen) override;
|
||||
|
||||
bool OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
|
||||
const CefKeyEvent& event,
|
||||
|
||||
+34
-2
@@ -18,7 +18,7 @@ public:
|
||||
UNREFERENCED_PARAMETER(object);
|
||||
UNREFERENCED_PARAMETER(retval);
|
||||
|
||||
if (name != "postMessage") {
|
||||
if (name != "postMessage" && name != "sendToHost" && name != "send") {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,26 @@ void NebulaApp::OnBeforeCommandLineProcessing(const CefString& process_type,
|
||||
|
||||
// The bundled UI is loaded from file:// and uses ES modules.
|
||||
command_line->AppendSwitch("allow-file-access-from-files");
|
||||
|
||||
// CefSettings.no_sandbox disables the browser-level sandbox, but Chromium
|
||||
// still attempts to bring up a separate GPU sandbox inside the GPU process.
|
||||
// Without the host-side sandbox plumbing this fails with STATUS_BREAKPOINT
|
||||
// (-2147483645) immediately on startup, which is exactly what the GPU
|
||||
// diagnostics page was showing - the GPU process crashed three times and
|
||||
// Chromium then fell back to software rendering. Disabling the GPU sandbox
|
||||
// matches the rest of our no_sandbox configuration and lets the GPU
|
||||
// process initialize.
|
||||
command_line->AppendSwitch("no-sandbox");
|
||||
command_line->AppendSwitch("disable-gpu-sandbox");
|
||||
command_line->AppendSwitch("in-process-gpu");
|
||||
|
||||
// Avoid Chromium's conservative GPU blocklist, but let Chromium choose the
|
||||
// safest graphics backend for this machine. Forcing raster/zero-copy paths
|
||||
// can prevent WebGL shared contexts from initializing on some drivers.
|
||||
command_line->AppendSwitch("ignore-gpu-blocklist");
|
||||
command_line->AppendSwitch("enable-accelerated-video-decode");
|
||||
command_line->AppendSwitchWithValue("use-gl", "angle");
|
||||
command_line->AppendSwitchWithValue("use-angle", "d3d11");
|
||||
}
|
||||
|
||||
void NebulaApp::OnContextCreated(CefRefPtr<CefBrowser> browser,
|
||||
@@ -68,12 +88,24 @@ void NebulaApp::OnContextCreated(CefRefPtr<CefBrowser> browser,
|
||||
UNREFERENCED_PARAMETER(frame);
|
||||
|
||||
CefRefPtr<CefV8Value> global = context->GetGlobal();
|
||||
CefRefPtr<NativeBridgeHandler> handler = new NativeBridgeHandler();
|
||||
CefRefPtr<CefV8Value> 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<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
|
||||
|
||||
+192
-9
@@ -4,10 +4,31 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <string_view>
|
||||
|
||||
namespace nebula::ui {
|
||||
namespace {
|
||||
|
||||
constexpr std::string_view kNebulaScheme = "nebula://";
|
||||
constexpr std::wstring_view kInternalFallbackPage = L"404.html";
|
||||
|
||||
struct InternalPage {
|
||||
std::string_view slug;
|
||||
std::wstring_view file_name;
|
||||
};
|
||||
|
||||
constexpr InternalPage kInternalPages[] = {
|
||||
{"home", L"home.html"},
|
||||
{"settings", L"settings.html"},
|
||||
{"downloads", L"downloads.html"},
|
||||
{"bigpicture", L"bigpicture.html"},
|
||||
{"big-picture", L"bigpicture.html"},
|
||||
{"gpu-diagnostics", L"gpu-diagnostics.html"},
|
||||
{"setup", L"setup.html"},
|
||||
{"404", L"404.html"},
|
||||
{"insecure", L"insecure.html"},
|
||||
};
|
||||
|
||||
std::string WideToUtf8(const std::wstring& value) {
|
||||
if (value.empty()) {
|
||||
return {};
|
||||
@@ -31,6 +52,11 @@ std::string GetUrlWithoutDecoration(std::string url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
std::string GetUrlDecoration(const std::string& url) {
|
||||
const size_t split = url.find_first_of("?#");
|
||||
return split == std::string::npos ? std::string{} : url.substr(split);
|
||||
}
|
||||
|
||||
std::string ToLowerAscii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char ch) {
|
||||
return static_cast<char>(std::tolower(ch));
|
||||
@@ -38,6 +64,66 @@ std::string ToLowerAscii(std::string value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string PageFileUrl(std::wstring_view page_name) {
|
||||
const auto path = GetUiPagePath(std::wstring(page_name));
|
||||
return path.empty() ? std::string{} : FilePathToUrl(path);
|
||||
}
|
||||
|
||||
std::string PercentEncode(const std::string& value) {
|
||||
constexpr char kHex[] = "0123456789ABCDEF";
|
||||
std::string encoded;
|
||||
encoded.reserve(value.size());
|
||||
for (unsigned char ch : value) {
|
||||
if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') {
|
||||
encoded += static_cast<char>(ch);
|
||||
} else {
|
||||
encoded += '%';
|
||||
encoded += kHex[ch >> 4];
|
||||
encoded += kHex[ch & 0x0F];
|
||||
}
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
std::string InternalPageName(const std::string& url) {
|
||||
std::string target = ToLowerAscii(GetUrlWithoutDecoration(url));
|
||||
if (!target.starts_with(kNebulaScheme)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
target.erase(0, kNebulaScheme.size());
|
||||
while (!target.empty() && target.front() == '/') {
|
||||
target.erase(target.begin());
|
||||
}
|
||||
while (!target.empty() && target.back() == '/') {
|
||||
target.pop_back();
|
||||
}
|
||||
return target.empty() ? "home" : target;
|
||||
}
|
||||
|
||||
std::string InternalUrlForSlug(std::string_view slug) {
|
||||
return std::string(kNebulaScheme) + std::string(slug);
|
||||
}
|
||||
|
||||
const InternalPage* FindInternalPageBySlug(std::string_view slug) {
|
||||
for (const auto& page : kInternalPages) {
|
||||
if (page.slug == slug) {
|
||||
return &page;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const InternalPage* FindInternalPageByFileUrl(const std::string& url) {
|
||||
const std::string base_url = GetUrlWithoutDecoration(url);
|
||||
for (const auto& page : kInternalPages) {
|
||||
if (PageFileUrl(page.file_name) == base_url) {
|
||||
return &page;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
@@ -50,6 +136,49 @@ std::filesystem::path GetExecutableDirectory() {
|
||||
return std::filesystem::path(exe_path).parent_path();
|
||||
}
|
||||
|
||||
std::filesystem::path GetUserDataDirectory() {
|
||||
std::filesystem::path root;
|
||||
|
||||
wchar_t buffer[MAX_PATH] = {};
|
||||
// Prefer %LOCALAPPDATA% so the profile follows Chromium conventions and
|
||||
// survives executable relocation.
|
||||
const DWORD length =
|
||||
GetEnvironmentVariableW(L"LOCALAPPDATA", buffer, MAX_PATH);
|
||||
if (length > 0 && length < MAX_PATH) {
|
||||
root = std::filesystem::path(buffer);
|
||||
} else {
|
||||
// Fall back to a directory next to the executable so a portable
|
||||
// install still gets a writable profile.
|
||||
root = GetExecutableDirectory();
|
||||
}
|
||||
|
||||
if (root.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path user_data = root / L"Nebula" / L"User Data";
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(user_data, ec);
|
||||
return user_data;
|
||||
}
|
||||
|
||||
std::filesystem::path GetCacheDirectory() {
|
||||
auto user_data = GetUserDataDirectory();
|
||||
if (user_data.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path cache = user_data / L"Cache";
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(cache, ec);
|
||||
return cache;
|
||||
}
|
||||
|
||||
std::filesystem::path GetSessionStatePath() {
|
||||
auto user_data = GetUserDataDirectory();
|
||||
return user_data.empty() ? std::filesystem::path{} : user_data / L"session_state.json";
|
||||
}
|
||||
|
||||
std::filesystem::path GetUiPagePath(const std::wstring& page_name) {
|
||||
const auto exe_dir = GetExecutableDirectory();
|
||||
if (exe_dir.empty()) {
|
||||
@@ -77,31 +206,85 @@ std::string FilePathToUrl(std::filesystem::path path) {
|
||||
|
||||
std::string GetChromeUrl() {
|
||||
const auto path = GetUiPagePath(L"chrome.html");
|
||||
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
|
||||
const std::string fallback = PageFileUrl(L"home.html");
|
||||
return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path);
|
||||
}
|
||||
|
||||
std::string GetHomeUrl() {
|
||||
const auto path = GetUiPagePath(L"home.html");
|
||||
return path.empty() ? "https://www.google.com" : FilePathToUrl(path);
|
||||
return InternalUrlForSlug("home");
|
||||
}
|
||||
|
||||
std::string GetSettingsUrl() {
|
||||
const auto path = GetUiPagePath(L"settings.html");
|
||||
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
|
||||
return InternalUrlForSlug("settings");
|
||||
}
|
||||
|
||||
std::string GetDownloadsUrl() {
|
||||
return InternalUrlForSlug("downloads");
|
||||
}
|
||||
|
||||
std::string GetBigPictureUrl() {
|
||||
const auto path = GetUiPagePath(L"bigpicture.html");
|
||||
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
|
||||
return InternalUrlForSlug("bigpicture");
|
||||
}
|
||||
|
||||
std::string GetGpuDiagnosticsUrl() {
|
||||
return InternalUrlForSlug("gpu-diagnostics");
|
||||
}
|
||||
|
||||
std::string GetMenuPopupUrl() {
|
||||
const auto path = GetUiPagePath(L"menu-popup.html");
|
||||
return path.empty() ? GetHomeUrl() : FilePathToUrl(path);
|
||||
const std::string fallback = PageFileUrl(L"home.html");
|
||||
return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path);
|
||||
}
|
||||
|
||||
std::string GetInsecureWarningUrl(const std::string& target_url) {
|
||||
return InternalUrlForSlug("insecure") + "?target=" + PercentEncode(target_url);
|
||||
}
|
||||
|
||||
std::string GetNotFoundUrl(const std::string& target_url) {
|
||||
return InternalUrlForSlug("404") + "?url=" + PercentEncode(target_url);
|
||||
}
|
||||
|
||||
std::string ResolveInternalUrl(const std::string& url) {
|
||||
const std::string page_name = InternalPageName(url);
|
||||
if (page_name.empty()) {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (const auto* page = FindInternalPageBySlug(page_name)) {
|
||||
const std::string file_url = PageFileUrl(page->file_name);
|
||||
return file_url.empty() ? url : file_url + GetUrlDecoration(url);
|
||||
}
|
||||
|
||||
const std::string fallback_url = PageFileUrl(kInternalFallbackPage);
|
||||
return fallback_url.empty() ? url : fallback_url + "?url=" + PercentEncode(url);
|
||||
}
|
||||
|
||||
std::string ToInternalUrl(const std::string& url) {
|
||||
const std::string page_name = InternalPageName(url);
|
||||
if (!page_name.empty()) {
|
||||
if (const auto* page = FindInternalPageBySlug(page_name)) {
|
||||
return InternalUrlForSlug(page->slug) + GetUrlDecoration(url);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
if (const auto* page = FindInternalPageByFileUrl(url)) {
|
||||
return InternalUrlForSlug(page->slug) + GetUrlDecoration(url);
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
bool IsInternalHomeUrl(const std::string& url) {
|
||||
return GetUrlWithoutDecoration(url) == GetHomeUrl();
|
||||
return GetUrlWithoutDecoration(ToInternalUrl(url)) == GetHomeUrl();
|
||||
}
|
||||
|
||||
bool IsNebulaInternalUrl(const std::string& url) {
|
||||
return ToLowerAscii(url).starts_with(kNebulaScheme);
|
||||
}
|
||||
|
||||
bool IsHttpUrl(const std::string& url) {
|
||||
return ToLowerAscii(url).starts_with("http://");
|
||||
}
|
||||
|
||||
bool IsChromiumNewTabUrl(const std::string& url) {
|
||||
|
||||
@@ -6,15 +6,26 @@
|
||||
namespace nebula::ui {
|
||||
|
||||
std::filesystem::path GetExecutableDirectory();
|
||||
std::filesystem::path GetUserDataDirectory();
|
||||
std::filesystem::path GetCacheDirectory();
|
||||
std::filesystem::path GetSessionStatePath();
|
||||
std::filesystem::path GetUiPagePath(const std::wstring& page_name);
|
||||
std::string FilePathToUrl(std::filesystem::path path);
|
||||
std::string GetChromeUrl();
|
||||
std::string GetHomeUrl();
|
||||
std::string GetSettingsUrl();
|
||||
std::string GetDownloadsUrl();
|
||||
std::string GetBigPictureUrl();
|
||||
std::string GetGpuDiagnosticsUrl();
|
||||
std::string GetMenuPopupUrl();
|
||||
std::string GetInsecureWarningUrl(const std::string& target_url);
|
||||
std::string GetNotFoundUrl(const std::string& target_url);
|
||||
std::string ResolveInternalUrl(const std::string& url);
|
||||
std::string ToInternalUrl(const std::string& url);
|
||||
|
||||
bool IsInternalHomeUrl(const std::string& url);
|
||||
bool IsNebulaInternalUrl(const std::string& url);
|
||||
bool IsHttpUrl(const std::string& url);
|
||||
bool IsChromiumNewTabUrl(const std::string& url);
|
||||
bool IsEmptyOrChromiumNewTabUrl(const std::string& url);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ constexpr wchar_t kChildFrameHitTestParentProp[] = L"NebulaChildFrameHitTestPare
|
||||
constexpr int kTitleRowHeightDip = 42;
|
||||
constexpr int kWindowControlWidthDip = 46;
|
||||
constexpr int kWindowControlCount = 3;
|
||||
constexpr COLORREF kNoWindowBorderColor = 0xFFFFFFFE;
|
||||
|
||||
RECT GetWorkArea() {
|
||||
RECT work_area = {};
|
||||
@@ -22,6 +23,30 @@ RECT GetWorkArea() {
|
||||
return work_area;
|
||||
}
|
||||
|
||||
RECT GetMonitorWorkArea(HWND hwnd) {
|
||||
MONITORINFO monitor_info = {};
|
||||
monitor_info.cbSize = sizeof(monitor_info);
|
||||
|
||||
const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
if (monitor && GetMonitorInfoW(monitor, &monitor_info)) {
|
||||
return monitor_info.rcWork;
|
||||
}
|
||||
|
||||
return GetWorkArea();
|
||||
}
|
||||
|
||||
RECT GetMonitorArea(HWND hwnd) {
|
||||
MONITORINFO monitor_info = {};
|
||||
monitor_info.cbSize = sizeof(monitor_info);
|
||||
|
||||
const HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
|
||||
if (monitor && GetMonitorInfoW(monitor, &monitor_info)) {
|
||||
return monitor_info.rcMonitor;
|
||||
}
|
||||
|
||||
return GetWorkArea();
|
||||
}
|
||||
|
||||
bool IsResizeHit(LRESULT hit) {
|
||||
return hit == HTLEFT || hit == HTRIGHT || hit == HTTOP || hit == HTBOTTOM ||
|
||||
hit == HTTOPLEFT || hit == HTTOPRIGHT || hit == HTBOTTOMLEFT || hit == HTBOTTOMRIGHT;
|
||||
@@ -56,6 +81,28 @@ bool SetResizeCursor(LRESULT hit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ApplyWindowFrameStyle(HWND hwnd) {
|
||||
const BOOL dark_mode = TRUE;
|
||||
const DWM_WINDOW_CORNER_PREFERENCE corner_preference = DWMWCP_ROUND;
|
||||
DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_USE_IMMERSIVE_DARK_MODE,
|
||||
&dark_mode,
|
||||
sizeof(dark_mode));
|
||||
|
||||
DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_WINDOW_CORNER_PREFERENCE,
|
||||
&corner_preference,
|
||||
sizeof(corner_preference));
|
||||
|
||||
DwmSetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_BORDER_COLOR,
|
||||
&kNoWindowBorderColor,
|
||||
sizeof(kNoWindowBorderColor));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
NebulaWindow::NebulaWindow(WindowDelegate* delegate) : delegate_(delegate) {}
|
||||
@@ -92,8 +139,9 @@ bool NebulaWindow::Create(HINSTANCE instance, int show_command) {
|
||||
}
|
||||
|
||||
UpdateDpi();
|
||||
ApplyWindowFrameStyle(hwnd_);
|
||||
|
||||
const MARGINS margins = {1, 1, 1, 1};
|
||||
const MARGINS margins = {0, 0, 0, 0};
|
||||
DwmExtendFrameIntoClientArea(hwnd_, &margins);
|
||||
|
||||
ShowWindow(hwnd_, show_command);
|
||||
@@ -101,14 +149,16 @@ bool NebulaWindow::Create(HINSTANCE instance, int show_command) {
|
||||
return true;
|
||||
}
|
||||
|
||||
BrowserLayout NebulaWindow::CurrentLayout() const {
|
||||
BrowserLayout NebulaWindow::CurrentLayout(bool show_chrome) const {
|
||||
RECT client = {};
|
||||
if (hwnd_) {
|
||||
GetClientRect(hwnd_, &client);
|
||||
}
|
||||
|
||||
BrowserLayout layout;
|
||||
layout.chrome = {0, 0, client.right, std::min<LONG>(ScaleForDpi(chrome_height_dip_), client.bottom)};
|
||||
layout.chrome = show_chrome
|
||||
? RECT{0, 0, client.right, std::min<LONG>(ScaleForDpi(chrome_height_dip_), client.bottom)}
|
||||
: RECT{0, 0, 0, 0};
|
||||
layout.content = {0, layout.chrome.bottom, client.right, client.bottom};
|
||||
return layout;
|
||||
}
|
||||
@@ -136,13 +186,55 @@ void NebulaWindow::Minimize() {
|
||||
}
|
||||
|
||||
void NebulaWindow::ToggleMaximize() {
|
||||
if (!hwnd_) {
|
||||
if (!hwnd_ || fullscreen_) {
|
||||
return;
|
||||
}
|
||||
|
||||
ShowWindow(hwnd_, IsZoomed(hwnd_) ? SW_RESTORE : SW_MAXIMIZE);
|
||||
}
|
||||
|
||||
void NebulaWindow::SetFullscreen(bool fullscreen) {
|
||||
if (!hwnd_ || fullscreen_ == fullscreen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fullscreen) {
|
||||
restore_style_ = GetWindowLongPtrW(hwnd_, GWL_STYLE);
|
||||
restore_ex_style_ = GetWindowLongPtrW(hwnd_, GWL_EXSTYLE);
|
||||
restore_placement_.length = sizeof(restore_placement_);
|
||||
GetWindowPlacement(hwnd_, &restore_placement_);
|
||||
|
||||
fullscreen_ = true;
|
||||
const RECT monitor = GetMonitorArea(hwnd_);
|
||||
SetWindowLongPtrW(hwnd_, GWL_STYLE, restore_style_ & ~(WS_THICKFRAME | WS_MAXIMIZEBOX | WS_MINIMIZEBOX));
|
||||
SetWindowLongPtrW(hwnd_, GWL_EXSTYLE, restore_ex_style_);
|
||||
SetWindowPos(
|
||||
hwnd_,
|
||||
HWND_TOPMOST,
|
||||
monitor.left,
|
||||
monitor.top,
|
||||
monitor.right - monitor.left,
|
||||
monitor.bottom - monitor.top,
|
||||
SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
|
||||
} else {
|
||||
fullscreen_ = false;
|
||||
SetWindowLongPtrW(hwnd_, GWL_STYLE, restore_style_);
|
||||
SetWindowLongPtrW(hwnd_, GWL_EXSTYLE, restore_ex_style_);
|
||||
SetWindowPlacement(hwnd_, &restore_placement_);
|
||||
SetWindowPos(
|
||||
hwnd_,
|
||||
HWND_NOTOPMOST,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
|
||||
ApplyWindowFrameStyle(hwnd_);
|
||||
}
|
||||
|
||||
NotifyResize();
|
||||
}
|
||||
|
||||
void NebulaWindow::Close() {
|
||||
if (hwnd_) {
|
||||
SendMessageW(hwnd_, WM_CLOSE, 0, 0);
|
||||
@@ -265,6 +357,10 @@ LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) {
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_NCACTIVATE:
|
||||
ApplyWindowFrameStyle(hwnd_);
|
||||
return TRUE;
|
||||
|
||||
case WM_ERASEBKGND:
|
||||
return 1;
|
||||
|
||||
@@ -307,6 +403,18 @@ LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_GETMINMAXINFO: {
|
||||
const RECT work_area = GetMonitorWorkArea(hwnd_);
|
||||
const RECT monitor_area = GetMonitorArea(hwnd_);
|
||||
|
||||
auto* minmax = reinterpret_cast<MINMAXINFO*>(lparam);
|
||||
minmax->ptMaxPosition.x = work_area.left - monitor_area.left;
|
||||
minmax->ptMaxPosition.y = work_area.top - monitor_area.top;
|
||||
minmax->ptMaxSize.x = work_area.right - work_area.left;
|
||||
minmax->ptMaxSize.y = work_area.bottom - work_area.top;
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_CLOSE:
|
||||
if (delegate_) {
|
||||
delegate_->OnWindowCloseRequested();
|
||||
@@ -381,6 +489,10 @@ LRESULT NebulaWindow::HitTestPoint(POINT point) const {
|
||||
RECT window = {};
|
||||
GetWindowRect(hwnd_, &window);
|
||||
|
||||
if (fullscreen_ || IsZoomed(hwnd_)) {
|
||||
return HTCLIENT;
|
||||
}
|
||||
|
||||
const int resize_border = ScaleForDpi(resize_border_dip_);
|
||||
const bool left = point.x >= window.left && point.x < window.left + resize_border;
|
||||
const bool right = point.x < window.right && point.x >= window.right - resize_border;
|
||||
|
||||
@@ -26,11 +26,12 @@ public:
|
||||
|
||||
bool Create(HINSTANCE instance, int show_command);
|
||||
HWND hwnd() const { return hwnd_; }
|
||||
BrowserLayout CurrentLayout() const;
|
||||
BrowserLayout CurrentLayout(bool show_chrome = true) const;
|
||||
|
||||
void ResizeChild(HWND child, const RECT& rect) const;
|
||||
void Minimize();
|
||||
void ToggleMaximize();
|
||||
void SetFullscreen(bool fullscreen);
|
||||
void Close();
|
||||
void BeginDrag();
|
||||
void SetTitle(const std::wstring& title);
|
||||
@@ -53,6 +54,10 @@ private:
|
||||
WindowDelegate* delegate_ = nullptr;
|
||||
HINSTANCE instance_ = nullptr;
|
||||
HWND hwnd_ = nullptr;
|
||||
bool fullscreen_ = false;
|
||||
LONG_PTR restore_style_ = 0;
|
||||
LONG_PTR restore_ex_style_ = 0;
|
||||
WINDOWPLACEMENT restore_placement_ = {sizeof(WINDOWPLACEMENT)};
|
||||
UINT dpi_ = 96;
|
||||
int resize_border_dip_ = 8;
|
||||
int chrome_height_dip_ = 104;
|
||||
|
||||
@@ -78,26 +78,6 @@ button:disabled {
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
/* ── Brand ──────────────────────────────────────────────────── */
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
min-width: 104px;
|
||||
color: var(--accent-2);
|
||||
font-size: 0.73rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.brand-icon {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
/* ── Tabs ───────────────────────────────────────────────────── */
|
||||
|
||||
.tabs {
|
||||
|
||||
@@ -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();
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ const state = {
|
||||
function toNavigationUrl(input) {
|
||||
const value = (input || '').trim();
|
||||
if (!value) return null;
|
||||
if (/^(https?:|file:|data:|blob:|chrome:)/i.test(value)) return value;
|
||||
if (/^(https?:|file:|data:|blob:|chrome:|nebula:\/\/)/i.test(value)) return value;
|
||||
if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
|
||||
return `${SEARCH_URL}${encodeURIComponent(value)}`;
|
||||
}
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ function applySelectedSearchEngine(engine) {
|
||||
function normalizeNavigationUrl(input) {
|
||||
const value = (input || '').trim();
|
||||
if (!value) return null;
|
||||
if (/^(https?:|file:|data:|blob:)/i.test(value)) return value;
|
||||
if (/^(https?:|file:|data:|blob:|nebula:\/\/)/i.test(value)) return value;
|
||||
if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
|
||||
return `${searchEngines[selectedSearchEngine]}${encodeURIComponent(value)}`;
|
||||
}
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@
|
||||
const attemptedUrl = params.get('url');
|
||||
const box = document.getElementById('targetBox');
|
||||
if (attemptedUrl) {
|
||||
box.textContent = decodeURIComponent(attemptedUrl);
|
||||
box.textContent = attemptedUrl;
|
||||
} else {
|
||||
box.textContent = 'Unknown URL';
|
||||
}
|
||||
|
||||
@@ -72,10 +72,6 @@
|
||||
<span class="material-symbols-outlined">bookmarks</span>
|
||||
<span class="nav-label">Bookmarks</span>
|
||||
</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">
|
||||
<span class="material-symbols-outlined">download</span>
|
||||
<span class="nav-label">Downloads</span>
|
||||
@@ -123,13 +119,6 @@
|
||||
</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>
|
||||
|
||||
<!-- Browse section (for webview) -->
|
||||
@@ -158,27 +147,6 @@
|
||||
</div>
|
||||
</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 -->
|
||||
<section id="section-downloads" class="bp-section">
|
||||
<div class="section-header">
|
||||
|
||||
@@ -9,11 +9,6 @@
|
||||
<body>
|
||||
<div class="nebula-chrome" data-drag-region>
|
||||
<div class="title-row" data-drag-region>
|
||||
<div class="brand" data-drag-region>
|
||||
<img src="../assets/images/branding/Nebula-Icon.svg" alt="" class="brand-icon">
|
||||
<span>Nebula</span>
|
||||
</div>
|
||||
|
||||
<div class="tabs" role="tablist" aria-label="Nebula tabs">
|
||||
<button id="active-tab" class="tab active" type="button" role="tab" aria-selected="true">
|
||||
<span id="tab-favicon" class="tab-favicon"></span>
|
||||
|
||||
+935
-201
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,10 @@
|
||||
const box = document.getElementById('targetBox');
|
||||
if (target) box.textContent = target;
|
||||
function sendNavigate(url, opts){
|
||||
if (opts && opts.insecureBypass && window.nebulaNative && window.nebulaNative.postMessage){
|
||||
window.nebulaNative.postMessage('navigate-insecure', url);
|
||||
return;
|
||||
}
|
||||
if (window.electronAPI && window.electronAPI.sendToHost){
|
||||
window.electronAPI.sendToHost('navigate', url, opts||{});
|
||||
} else if (window.parent && window.parent !== window) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<div id="menu-popup" role="menu">
|
||||
<button data-cmd="open-settings" role="menuitem">Settings</button>
|
||||
<button data-cmd="big-picture" role="menuitem">🎮 Big Picture Mode</button>
|
||||
<button data-cmd="gpu-diagnostics" role="menuitem">GPU Diagnostics</button>
|
||||
<button data-cmd="toggle-devtools" role="menuitem">Toggle Developer Tools</button>
|
||||
<div class="zoom-controls" role="group" aria-label="Zoom controls">
|
||||
<button data-cmd="zoom-out" aria-label="Zoom out">-</button>
|
||||
|
||||
@@ -293,12 +293,12 @@
|
||||
|
||||
<div class="customization-group about-actions">
|
||||
<button id="copy-about-btn">Copy diagnostics</button>
|
||||
<a id="github-link" href="https://github.com/Bobbybear007/NebulaBrowser" class="github-btn" rel="noopener noreferrer">
|
||||
<a id="github-link" href="https://gitpub.zambazosmedia.group/#repo/nebula-project/NebulaBrowser" class="github-btn" rel="noopener noreferrer">
|
||||
<!-- GitHub mark (Octicons) MIT License -->
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false" role="img">
|
||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.01.08-2.11 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.91.08 2.11.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0 0 16 8c0-4.42-3.58-8-8-8z"/>
|
||||
</svg>
|
||||
<span>GitHub</span>
|
||||
<span>Gitpub</span>
|
||||
</a>
|
||||
<a id="help-link" href="https://nebula.zambazosmedia.group" class="help-btn" rel="noopener noreferrer">
|
||||
<!-- Help icon -->
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user