Compare commits
9 Commits
207a849f06
...
CEF
| Author | SHA1 | Date | |
|---|---|---|---|
| 18bc607d93 | |||
| 54216aa133 | |||
| 8eb5c1a3b2 | |||
| 406d73c10f | |||
| 6fac7e320b | |||
| a32940a3f3 | |||
| 10180b7109 | |||
| dd6b3fa70d | |||
| a8786b4c1c |
@@ -39,6 +39,16 @@ add_subdirectory(
|
|||||||
|
|
||||||
set(NEBULA_SOURCES
|
set(NEBULA_SOURCES
|
||||||
app/main.cpp
|
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
|
||||||
|
src/cef/browser_client.cpp
|
||||||
|
src/cef/nebula_app.cpp
|
||||||
|
src/ui/paths.cpp
|
||||||
|
src/window/nebula_window.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(NebulaBrowser WIN32
|
add_executable(NebulaBrowser WIN32
|
||||||
@@ -54,6 +64,7 @@ if(MSVC)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_include_directories(NebulaBrowser PRIVATE
|
target_include_directories(NebulaBrowser PRIVATE
|
||||||
|
"${CMAKE_SOURCE_DIR}/src"
|
||||||
"${CEF_ROOT}"
|
"${CEF_ROOT}"
|
||||||
"${CEF_ROOT}/include"
|
"${CEF_ROOT}/include"
|
||||||
)
|
)
|
||||||
@@ -70,6 +81,7 @@ target_link_libraries(NebulaBrowser PRIVATE
|
|||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(NebulaBrowser PRIVATE
|
target_link_libraries(NebulaBrowser PRIVATE
|
||||||
"${CEF_ROOT}/Release/libcef.lib"
|
"${CEF_ROOT}/Release/libcef.lib"
|
||||||
|
dwmapi
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_definitions(NebulaBrowser PRIVATE
|
target_compile_definitions(NebulaBrowser PRIVATE
|
||||||
|
|||||||
+2
-330
@@ -1,333 +1,6 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include "app/run.h"
|
||||||
#include <cctype>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include "include/cef_app.h"
|
|
||||||
#include "include/cef_browser.h"
|
|
||||||
#include "include/cef_client.h"
|
|
||||||
#include "include/cef_command_line.h"
|
|
||||||
#include "include/cef_request.h"
|
|
||||||
#include "include/cef_request_handler.h"
|
|
||||||
#include "include/wrapper/cef_helpers.h"
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
std::string WideToUtf8(const std::wstring& value) {
|
|
||||||
if (value.empty()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const int size = WideCharToMultiByte(
|
|
||||||
CP_UTF8, 0, value.data(), static_cast<int>(value.size()),
|
|
||||||
nullptr, 0, nullptr, nullptr);
|
|
||||||
std::string result(size, '\0');
|
|
||||||
WideCharToMultiByte(
|
|
||||||
CP_UTF8, 0, value.data(), static_cast<int>(value.size()),
|
|
||||||
result.data(), size, nullptr, nullptr);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string FilePathToUrl(std::filesystem::path path) {
|
|
||||||
std::string value = WideToUtf8(path.wstring());
|
|
||||||
for (char& ch : value) {
|
|
||||||
if (ch == '\\') {
|
|
||||||
ch = '/';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string encoded;
|
|
||||||
encoded.reserve(value.size());
|
|
||||||
for (char ch : value) {
|
|
||||||
encoded += ch == ' ' ? "%20" : std::string(1, ch);
|
|
||||||
}
|
|
||||||
return "file:///" + encoded;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::filesystem::path GetHomePath() {
|
|
||||||
wchar_t exe_path[MAX_PATH] = {};
|
|
||||||
const DWORD length = GetModuleFileNameW(nullptr, exe_path, MAX_PATH);
|
|
||||||
if (length == 0 || length == MAX_PATH) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::filesystem::path(exe_path).parent_path() /
|
|
||||||
"ui" / "pages" / "home.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string GetHomeUrl() {
|
|
||||||
const auto home_path = GetHomePath();
|
|
||||||
if (home_path.empty()) {
|
|
||||||
return "https://www.google.com";
|
|
||||||
}
|
|
||||||
|
|
||||||
return FilePathToUrl(home_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string GetUrlWithoutDecoration(std::string url) {
|
|
||||||
const size_t split = url.find_first_of("?#");
|
|
||||||
if (split != std::string::npos) {
|
|
||||||
url.resize(split);
|
|
||||||
}
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 IsInternalHomeUrl(const std::string& url) {
|
|
||||||
return GetUrlWithoutDecoration(url) == GetHomeUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsChromiumNewTabUrl(const std::string& url) {
|
|
||||||
const std::string target = ToLowerAscii(GetUrlWithoutDecoration(url));
|
|
||||||
return target == "about:blank" ||
|
|
||||||
target == "chrome://newtab" ||
|
|
||||||
target == "chrome://newtab/" ||
|
|
||||||
target == "chrome://new-tab-page" ||
|
|
||||||
target == "chrome://new-tab-page/" ||
|
|
||||||
target == "chrome-search://local-ntp/local-ntp.html";
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsEmptyOrChromiumNewTabUrl(const std::string& url) {
|
|
||||||
return url.empty() || IsChromiumNewTabUrl(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
class NebulaClient final : public CefClient,
|
|
||||||
public CefDisplayHandler,
|
|
||||||
public CefKeyboardHandler,
|
|
||||||
public CefLifeSpanHandler,
|
|
||||||
public CefPermissionHandler,
|
|
||||||
public CefRequestHandler {
|
|
||||||
public:
|
|
||||||
CefRefPtr<CefDisplayHandler> GetDisplayHandler() override {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
CefRefPtr<CefKeyboardHandler> GetKeyboardHandler() override {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
CefRefPtr<CefPermissionHandler> GetPermissionHandler() override {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
CefRefPtr<CefRequestHandler> GetRequestHandler() override {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnAddressChange(CefRefPtr<CefBrowser> browser,
|
|
||||||
CefRefPtr<CefFrame> frame,
|
|
||||||
const CefString& url) override {
|
|
||||||
CEF_REQUIRE_UI_THREAD();
|
|
||||||
|
|
||||||
if (browser && frame && frame->IsMain() &&
|
|
||||||
IsChromiumNewTabUrl(url)) {
|
|
||||||
browser->GetMainFrame()->LoadURL(GetHomeUrl());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnTitleChange(CefRefPtr<CefBrowser> browser,
|
|
||||||
const CefString& title) override {
|
|
||||||
CEF_REQUIRE_UI_THREAD();
|
|
||||||
|
|
||||||
CefWindowHandle window = browser->GetHost()->GetWindowHandle();
|
|
||||||
if (window) {
|
|
||||||
SetWindowText(window, std::wstring(title).c_str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
|
|
||||||
const CefKeyEvent& event,
|
|
||||||
CefEventHandle os_event,
|
|
||||||
bool* is_keyboard_shortcut) override {
|
|
||||||
CEF_REQUIRE_UI_THREAD();
|
|
||||||
UNREFERENCED_PARAMETER(os_event);
|
|
||||||
|
|
||||||
if (event.type == KEYEVENT_RAWKEYDOWN &&
|
|
||||||
(event.modifiers & EVENTFLAG_CONTROL_DOWN) != 0 &&
|
|
||||||
event.windows_key_code == 'T') {
|
|
||||||
if (is_keyboard_shortcut) {
|
|
||||||
*is_keyboard_shortcut = true;
|
|
||||||
}
|
|
||||||
browser->GetMainFrame()->LoadURL(GetHomeUrl());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OnBeforePopup(CefRefPtr<CefBrowser> browser,
|
|
||||||
CefRefPtr<CefFrame> frame,
|
|
||||||
int popup_id,
|
|
||||||
const CefString& target_url,
|
|
||||||
const CefString& target_frame_name,
|
|
||||||
CefLifeSpanHandler::WindowOpenDisposition target_disposition,
|
|
||||||
bool user_gesture,
|
|
||||||
const CefPopupFeatures& popupFeatures,
|
|
||||||
CefWindowInfo& windowInfo,
|
|
||||||
CefRefPtr<CefClient>& client,
|
|
||||||
CefBrowserSettings& settings,
|
|
||||||
CefRefPtr<CefDictionaryValue>& extra_info,
|
|
||||||
bool* no_javascript_access) override {
|
|
||||||
CEF_REQUIRE_UI_THREAD();
|
|
||||||
UNREFERENCED_PARAMETER(frame);
|
|
||||||
UNREFERENCED_PARAMETER(popup_id);
|
|
||||||
UNREFERENCED_PARAMETER(target_frame_name);
|
|
||||||
UNREFERENCED_PARAMETER(user_gesture);
|
|
||||||
UNREFERENCED_PARAMETER(popupFeatures);
|
|
||||||
UNREFERENCED_PARAMETER(windowInfo);
|
|
||||||
UNREFERENCED_PARAMETER(settings);
|
|
||||||
UNREFERENCED_PARAMETER(extra_info);
|
|
||||||
UNREFERENCED_PARAMETER(no_javascript_access);
|
|
||||||
|
|
||||||
if (target_disposition == CEF_WOD_NEW_WINDOW &&
|
|
||||||
IsEmptyOrChromiumNewTabUrl(target_url)) {
|
|
||||||
client = this;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (IsChromiumNewTabUrl(target_url)) {
|
|
||||||
browser->GetMainFrame()->LoadURL(GetHomeUrl());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
|
|
||||||
CefRefPtr<CefFrame> frame,
|
|
||||||
CefRefPtr<CefRequest> request,
|
|
||||||
bool user_gesture,
|
|
||||||
bool is_redirect) override {
|
|
||||||
CEF_REQUIRE_UI_THREAD();
|
|
||||||
UNREFERENCED_PARAMETER(browser);
|
|
||||||
UNREFERENCED_PARAMETER(user_gesture);
|
|
||||||
UNREFERENCED_PARAMETER(is_redirect);
|
|
||||||
|
|
||||||
if (frame && frame->IsMain() && request &&
|
|
||||||
IsChromiumNewTabUrl(request->GetURL())) {
|
|
||||||
frame->LoadURL(GetHomeUrl());
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnAfterCreated(CefRefPtr<CefBrowser> browser) override {
|
|
||||||
CEF_REQUIRE_UI_THREAD();
|
|
||||||
++browser_count_;
|
|
||||||
|
|
||||||
if (browser_count_ > 1 && browser &&
|
|
||||||
IsEmptyOrChromiumNewTabUrl(browser->GetMainFrame()->GetURL())) {
|
|
||||||
browser->GetMainFrame()->LoadURL(GetHomeUrl());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override {
|
|
||||||
CEF_REQUIRE_UI_THREAD();
|
|
||||||
|
|
||||||
--browser_count_;
|
|
||||||
if (browser_count_ == 0) {
|
|
||||||
CefQuitMessageLoop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool OnShowPermissionPrompt(
|
|
||||||
CefRefPtr<CefBrowser> browser,
|
|
||||||
uint64_t prompt_id,
|
|
||||||
const CefString& requesting_origin,
|
|
||||||
uint32_t requested_permissions,
|
|
||||||
CefRefPtr<CefPermissionPromptCallback> callback) override {
|
|
||||||
CEF_REQUIRE_UI_THREAD();
|
|
||||||
UNREFERENCED_PARAMETER(prompt_id);
|
|
||||||
UNREFERENCED_PARAMETER(requesting_origin);
|
|
||||||
|
|
||||||
if ((requested_permissions & CEF_PERMISSION_TYPE_GEOLOCATION) != 0 &&
|
|
||||||
browser && callback &&
|
|
||||||
IsInternalHomeUrl(browser->GetMainFrame()->GetURL())) {
|
|
||||||
callback->Continue(CEF_PERMISSION_RESULT_ACCEPT);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
int browser_count_ = 0;
|
|
||||||
|
|
||||||
IMPLEMENT_REFCOUNTING(NebulaClient);
|
|
||||||
};
|
|
||||||
|
|
||||||
class NebulaApp final : public CefApp {
|
|
||||||
public:
|
|
||||||
void OnBeforeCommandLineProcessing(
|
|
||||||
const CefString& process_type,
|
|
||||||
CefRefPtr<CefCommandLine> command_line) override {
|
|
||||||
UNREFERENCED_PARAMETER(process_type);
|
|
||||||
|
|
||||||
// The bundled UI is loaded from file:// and uses ES modules.
|
|
||||||
command_line->AppendSwitch("allow-file-access-from-files");
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
|
||||||
IMPLEMENT_REFCOUNTING(NebulaApp);
|
|
||||||
};
|
|
||||||
|
|
||||||
int RunNebula(HINSTANCE instance) {
|
|
||||||
CefMainArgs main_args(instance);
|
|
||||||
CefRefPtr<NebulaApp> app(new NebulaApp);
|
|
||||||
|
|
||||||
const int subprocess_exit_code =
|
|
||||||
CefExecuteProcess(main_args, app, nullptr);
|
|
||||||
if (subprocess_exit_code >= 0) {
|
|
||||||
return subprocess_exit_code;
|
|
||||||
}
|
|
||||||
|
|
||||||
CefSettings settings;
|
|
||||||
settings.no_sandbox = true;
|
|
||||||
|
|
||||||
if (!CefInitialize(main_args, settings, app, nullptr)) {
|
|
||||||
return CefGetExitCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
|
|
||||||
command_line->InitFromString(GetCommandLineW());
|
|
||||||
|
|
||||||
std::string url = command_line->GetSwitchValue("url");
|
|
||||||
if (url.empty()) {
|
|
||||||
url = GetHomeUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
CefWindowInfo window_info;
|
|
||||||
window_info.SetAsPopup(nullptr, "Nebula Browser");
|
|
||||||
|
|
||||||
CefBrowserSettings browser_settings;
|
|
||||||
CefRefPtr<NebulaClient> client(new NebulaClient);
|
|
||||||
|
|
||||||
if (!CefBrowserHost::CreateBrowser(
|
|
||||||
window_info, client, url, browser_settings, nullptr, nullptr)) {
|
|
||||||
CefShutdown();
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
CefRunMessageLoop();
|
|
||||||
CefShutdown();
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
int APIENTRY wWinMain(HINSTANCE instance,
|
int APIENTRY wWinMain(HINSTANCE instance,
|
||||||
HINSTANCE previous_instance,
|
HINSTANCE previous_instance,
|
||||||
@@ -335,7 +8,6 @@ int APIENTRY wWinMain(HINSTANCE instance,
|
|||||||
int show_command) {
|
int show_command) {
|
||||||
UNREFERENCED_PARAMETER(previous_instance);
|
UNREFERENCED_PARAMETER(previous_instance);
|
||||||
UNREFERENCED_PARAMETER(command_line);
|
UNREFERENCED_PARAMETER(command_line);
|
||||||
UNREFERENCED_PARAMETER(show_command);
|
|
||||||
|
|
||||||
return RunNebula(instance);
|
return nebula::app::RunNebula(instance, show_command);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,740 @@
|
|||||||
|
#include "app/nebula_controller.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const int size = MultiByteToWideChar(
|
||||||
|
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0);
|
||||||
|
std::wstring result(size, L'\0');
|
||||||
|
MultiByteToWideChar(
|
||||||
|
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(), size);
|
||||||
|
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(
|
||||||
|
parent,
|
||||||
|
CefRect(
|
||||||
|
rect.left,
|
||||||
|
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);
|
||||||
|
return result.ec == std::errc{} && result.ptr == value.data() + value.size() ? tab_id : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int ScaleForWindow(HWND hwnd, int value) {
|
||||||
|
return MulDiv(value, static_cast<int>(GetDpiForWindow(hwnd)), 96);
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT MenuPopupRect(HWND hwnd, const nebula::window::BrowserLayout& layout) {
|
||||||
|
RECT client = {};
|
||||||
|
GetClientRect(hwnd, &client);
|
||||||
|
|
||||||
|
const int width = ScaleForWindow(hwnd, 260);
|
||||||
|
const int height = ScaleForWindow(hwnd, 258);
|
||||||
|
const int margin = ScaleForWindow(hwnd, 12);
|
||||||
|
const int overlap = ScaleForWindow(hwnd, 2);
|
||||||
|
|
||||||
|
const LONG x = std::max<LONG>(margin, client.right - width - margin);
|
||||||
|
const LONG y = std::max<LONG>(0, layout.chrome.bottom - overlap);
|
||||||
|
return {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
std::min<LONG>(client.right, x + width),
|
||||||
|
std::min<LONG>(client.bottom, y + height),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyRoundedWindowRegion(HWND hwnd, int corner_radius) {
|
||||||
|
RECT rect = {};
|
||||||
|
if (!hwnd || !GetClientRect(hwnd, &rect)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRGN region = CreateRoundRectRgn(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
std::max<LONG>(1, rect.right - rect.left) + 1,
|
||||||
|
std::max<LONG>(1, rect.bottom - rect.top) + 1,
|
||||||
|
corner_radius,
|
||||||
|
corner_radius);
|
||||||
|
if (region && !SetWindowRgn(hwnd, region, TRUE)) {
|
||||||
|
DeleteObject(region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string WithCacheBuster(std::string url) {
|
||||||
|
if (url.empty()) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t hash = url.find('#');
|
||||||
|
std::string fragment;
|
||||||
|
if (hash != std::string::npos) {
|
||||||
|
fragment = url.substr(hash);
|
||||||
|
url.resize(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char separator = url.find('?') == std::string::npos ? '?' : '&';
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HWND hwnd = browser->GetHost()->GetWindowHandle();
|
||||||
|
if (!hwnd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShowWindow(hwnd, visible ? SW_SHOW : SW_HIDE);
|
||||||
|
if (visible) {
|
||||||
|
SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
NebulaController::NebulaController(HINSTANCE instance, std::string initial_url, int show_command)
|
||||||
|
: instance_(instance),
|
||||||
|
initial_url_(std::move(initial_url)),
|
||||||
|
show_command_(show_command),
|
||||||
|
tabs_(this),
|
||||||
|
site_history_(LoadSiteHistory()) {}
|
||||||
|
|
||||||
|
NebulaController::~NebulaController() = default;
|
||||||
|
|
||||||
|
bool NebulaController::Create() {
|
||||||
|
window_ = std::make_unique<nebula::window::NebulaWindow>(this);
|
||||||
|
return window_->Create(instance_, show_command_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::OnWindowCreated() {
|
||||||
|
if (initial_url_.empty()) {
|
||||||
|
tabs_.CreateInitialTab(nebula::ui::GetHomeUrl());
|
||||||
|
} else {
|
||||||
|
tabs_.CreateInitialTab(initial_url_);
|
||||||
|
}
|
||||||
|
PersistSession();
|
||||||
|
|
||||||
|
CreateChromeBrowser();
|
||||||
|
CreateContentBrowser();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::OnWindowResized(const nebula::window::BrowserLayout& layout) {
|
||||||
|
UNREFERENCED_PARAMETER(layout);
|
||||||
|
ResizeBrowsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
if (menu_popup_browser_) {
|
||||||
|
menu_popup_browser_->GetHost()->CloseBrowser(false);
|
||||||
|
}
|
||||||
|
for (const auto& tab : tabs_.Tabs()) {
|
||||||
|
if (tab.browser) {
|
||||||
|
tab.browser->GetHost()->CloseBrowser(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::OnActiveTabChanged(const nebula::browser::NebulaTab& tab) {
|
||||||
|
if (chrome_ready_) {
|
||||||
|
SendChromeState(tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::OnBrowserCreated(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) {
|
||||||
|
if (window_ && browser) {
|
||||||
|
window_->EnableFrameHitTest(browser->GetHost()->GetWindowHandle());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role == nebula::cef::BrowserRole::Chrome) {
|
||||||
|
chrome_browser_ = browser;
|
||||||
|
chrome_ready_ = true;
|
||||||
|
if (const auto* tab = tabs_.ActiveTab()) {
|
||||||
|
SendChromeState(*tab);
|
||||||
|
}
|
||||||
|
} else if (role == nebula::cef::BrowserRole::MenuPopup) {
|
||||||
|
menu_popup_browser_ = browser;
|
||||||
|
PositionMenuPopup();
|
||||||
|
} else {
|
||||||
|
tabs_.SetActiveBrowser(browser);
|
||||||
|
}
|
||||||
|
|
||||||
|
ResizeBrowsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) {
|
||||||
|
if (role == nebula::cef::BrowserRole::Chrome) {
|
||||||
|
chrome_browser_ = nullptr;
|
||||||
|
chrome_ready_ = false;
|
||||||
|
} else if (role == nebula::cef::BrowserRole::MenuPopup) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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") {
|
||||||
|
ActivateTab(ParseTabId(payload));
|
||||||
|
} else if (command == "close-tab") {
|
||||||
|
CloseTab(ParseTabId(payload));
|
||||||
|
} else if (command == "back") {
|
||||||
|
tabs_.GoBack();
|
||||||
|
} else if (command == "forward") {
|
||||||
|
tabs_.GoForward();
|
||||||
|
} else if (command == "reload") {
|
||||||
|
tabs_.Reload();
|
||||||
|
} else if (command == "stop") {
|
||||||
|
tabs_.StopLoad();
|
||||||
|
} else if (command == "settings") {
|
||||||
|
tabs_.LoadURL(nebula::ui::GetSettingsUrl());
|
||||||
|
} else if (command == "menu-popup") {
|
||||||
|
ToggleMenuPopup();
|
||||||
|
} else if (command == "open-settings") {
|
||||||
|
CloseMenuPopup();
|
||||||
|
tabs_.LoadURL(nebula::ui::GetSettingsUrl());
|
||||||
|
} 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") {
|
||||||
|
AdjustZoom(-0.5);
|
||||||
|
} else if (command == "zoom-in") {
|
||||||
|
AdjustZoom(0.5);
|
||||||
|
} else if (command == "hard-reload") {
|
||||||
|
CloseMenuPopup();
|
||||||
|
if (auto* tab = tabs_.ActiveTab(); tab && tab->browser) {
|
||||||
|
tab->browser->ReloadIgnoreCache();
|
||||||
|
}
|
||||||
|
} else if (command == "fresh-reload") {
|
||||||
|
CloseMenuPopup();
|
||||||
|
FreshReload();
|
||||||
|
} else if (command == "close-menu-popup") {
|
||||||
|
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_) {
|
||||||
|
window_->ToggleMaximize();
|
||||||
|
} else if (command == "close" && window_) {
|
||||||
|
window_->Close();
|
||||||
|
} else if (command == "drag" && window_) {
|
||||||
|
window_->BeginDrag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::OnContentLoadingStateChanged(CefRefPtr<CefBrowser> browser, bool is_loading) {
|
||||||
|
tabs_.UpdateLoadingState(browser, is_loading);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::OnContentLoadProgressChanged(CefRefPtr<CefBrowser> browser, double 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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
tabs_.LoadURL(nebula::ui::IsEmptyOrChromiumNewTabUrl(target_url)
|
||||||
|
? nebula::ui::GetHomeUrl()
|
||||||
|
: 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::ActivateTab(int tab_id) {
|
||||||
|
auto* current_tab = tabs_.ActiveTab();
|
||||||
|
if (current_tab && current_tab->id == tab_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CefRefPtr<CefBrowser> previous_browser = current_tab ? current_tab->browser : nullptr;
|
||||||
|
if (!tabs_.ActivateTab(tab_id)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PersistSession();
|
||||||
|
|
||||||
|
SetBrowserVisible(previous_browser, false);
|
||||||
|
if (auto* active_tab = tabs_.ActiveTab()) {
|
||||||
|
if (active_tab->browser) {
|
||||||
|
SetBrowserVisible(active_tab->browser, true);
|
||||||
|
} else {
|
||||||
|
CreateContentBrowser();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ResizeBrowsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::CloseTab(int tab_id) {
|
||||||
|
const bool was_active = [this, tab_id] {
|
||||||
|
const auto* tab = tabs_.ActiveTab();
|
||||||
|
return tab && tab->id == 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (was_active) {
|
||||||
|
if (auto* active_tab = tabs_.ActiveTab()) {
|
||||||
|
SetBrowserVisible(active_tab->browser, true);
|
||||||
|
}
|
||||||
|
ResizeBrowsers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::CreateChromeBrowser() {
|
||||||
|
if (!window_ || !window_->hwnd()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto layout = window_->CurrentLayout();
|
||||||
|
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(
|
||||||
|
window_info, chrome_client_, nebula::ui::GetChromeUrl(), browser_settings, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::CreateContentBrowser() {
|
||||||
|
if (!window_ || !window_->hwnd()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = 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_, nebula::ui::ResolveInternalUrl(url), browser_settings, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::ToggleMenuPopup() {
|
||||||
|
if (menu_popup_browser_) {
|
||||||
|
CloseMenuPopup();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CreateMenuPopupBrowser();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::CloseMenuPopup() {
|
||||||
|
if (menu_popup_browser_) {
|
||||||
|
menu_popup_browser_->GetHost()->CloseBrowser(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::CreateMenuPopupBrowser() {
|
||||||
|
if (!window_ || !window_->hwnd()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto layout = window_->CurrentLayout();
|
||||||
|
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(
|
||||||
|
window_info, menu_popup_client_, nebula::ui::GetMenuPopupUrl(), browser_settings, nullptr, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::PositionMenuPopup() {
|
||||||
|
if (content_fullscreen_ || !window_ || !window_->hwnd() || !menu_popup_browser_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto rect = MenuPopupRect(window_->hwnd(), window_->CurrentLayout());
|
||||||
|
const HWND hwnd = menu_popup_browser_->GetHost()->GetWindowHandle();
|
||||||
|
window_->ResizeChild(hwnd, rect);
|
||||||
|
ApplyRoundedWindowRegion(hwnd, ScaleForWindow(window_->hwnd(), 28));
|
||||||
|
SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::ToggleDevTools() {
|
||||||
|
auto* tab = tabs_.ActiveTab();
|
||||||
|
if (!tab || !tab->browser || !window_ || !window_->hwnd()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CefRefPtr<CefBrowserHost> host = tab->browser->GetHost();
|
||||||
|
if (host->HasDevTools()) {
|
||||||
|
host->CloseDevTools();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::AdjustZoom(double delta) {
|
||||||
|
auto* tab = tabs_.ActiveTab();
|
||||||
|
if (!tab || !tab->browser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CefRefPtr<CefBrowserHost> host = tab->browser->GetHost();
|
||||||
|
host->SetZoomLevel(host->GetZoomLevel() + delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::FreshReload() {
|
||||||
|
auto* tab = tabs_.ActiveTab();
|
||||||
|
if (!tab || tab->url.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(!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);
|
||||||
|
}
|
||||||
|
if (!content_fullscreen_) {
|
||||||
|
PositionMenuPopup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaController::SendChromeState(const nebula::browser::NebulaTab& tab) {
|
||||||
|
if (!chrome_browser_) {
|
||||||
|
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) {
|
||||||
|
const auto& item = tabs[i];
|
||||||
|
if (i > 0) {
|
||||||
|
tabs_json += ",";
|
||||||
|
}
|
||||||
|
tabs_json +=
|
||||||
|
"{\"id\":" + std::to_string(item.id) +
|
||||||
|
",\"title\":\"" + nebula::browser::JsonEscape(item.title) + "\"" +
|
||||||
|
",\"isLoading\":" + std::string(item.is_loading ? "true" : "false") +
|
||||||
|
",\"favicon\":\"" + nebula::browser::JsonEscape(item.favicon_url) + "\"" +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
tabs_json += "]";
|
||||||
|
|
||||||
|
const std::string script =
|
||||||
|
"window.NebulaChrome && window.NebulaChrome.applyState({"
|
||||||
|
"\"id\":" + std::to_string(tab.id) +
|
||||||
|
",\"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) +
|
||||||
|
",\"canGoBack\":" + std::string(tab.CanGoBack() ? "true" : "false") +
|
||||||
|
",\"canGoForward\":" + std::string(tab.CanGoForward() ? "true" : "false") +
|
||||||
|
",\"favicon\":\"" + nebula::browser::JsonEscape(tab.favicon_url) + "\"" +
|
||||||
|
",\"tabs\":" + tabs_json +
|
||||||
|
"});";
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chrome_browser_ || menu_popup_browser_ || tabs_.HasOpenBrowsers()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window_ && window_->hwnd()) {
|
||||||
|
DestroyWindow(window_->hwnd());
|
||||||
|
}
|
||||||
|
CefQuitMessageLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace nebula::app
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "browser/tab_manager.h"
|
||||||
|
#include "cef/browser_client.h"
|
||||||
|
#include "window/nebula_window.h"
|
||||||
|
|
||||||
|
namespace nebula::app {
|
||||||
|
|
||||||
|
class NebulaController final : public nebula::window::WindowDelegate,
|
||||||
|
public nebula::browser::TabObserver,
|
||||||
|
public nebula::cef::BrowserClientDelegate {
|
||||||
|
public:
|
||||||
|
NebulaController(HINSTANCE instance, std::string initial_url, int show_command);
|
||||||
|
~NebulaController() override;
|
||||||
|
|
||||||
|
bool Create();
|
||||||
|
|
||||||
|
void OnWindowCreated() override;
|
||||||
|
void OnWindowResized(const nebula::window::BrowserLayout& layout) override;
|
||||||
|
void OnWindowCloseRequested() override;
|
||||||
|
|
||||||
|
void OnActiveTabChanged(const nebula::browser::NebulaTab& tab) override;
|
||||||
|
|
||||||
|
void OnBrowserCreated(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) override;
|
||||||
|
void OnBrowserClosing(nebula::cef::BrowserRole role, CefRefPtr<CefBrowser> browser) override;
|
||||||
|
void OnChromeCommand(const std::string& command, const std::string& payload) override;
|
||||||
|
void OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) override;
|
||||||
|
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();
|
||||||
|
void ActivateTab(int tab_id);
|
||||||
|
void CloseTab(int tab_id);
|
||||||
|
void CreateChromeBrowser();
|
||||||
|
void CreateContentBrowser();
|
||||||
|
void ToggleMenuPopup();
|
||||||
|
void CloseMenuPopup();
|
||||||
|
void CreateMenuPopupBrowser();
|
||||||
|
void PositionMenuPopup();
|
||||||
|
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;
|
||||||
|
std::string initial_url_;
|
||||||
|
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_;
|
||||||
|
CefRefPtr<CefBrowser> chrome_browser_;
|
||||||
|
CefRefPtr<CefBrowser> menu_popup_browser_;
|
||||||
|
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
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
#include "app/run.h"
|
||||||
|
|
||||||
|
#include "app/nebula_controller.h"
|
||||||
|
#include "cef/nebula_app.h"
|
||||||
|
#include "include/cef_app.h"
|
||||||
|
#include "include/cef_command_line.h"
|
||||||
|
#include "ui/paths.h"
|
||||||
|
|
||||||
|
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) {
|
||||||
|
EnableDpiAwareness();
|
||||||
|
|
||||||
|
CefMainArgs main_args(instance);
|
||||||
|
CefRefPtr<nebula::cef::NebulaApp> app(new nebula::cef::NebulaApp);
|
||||||
|
|
||||||
|
const int subprocess_exit_code = CefExecuteProcess(main_args, app, nullptr);
|
||||||
|
if (subprocess_exit_code >= 0) {
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
|
||||||
|
command_line->InitFromString(GetCommandLineW());
|
||||||
|
|
||||||
|
std::string initial_url = command_line->GetSwitchValue("url");
|
||||||
|
if (!initial_url.empty() && nebula::ui::IsChromiumNewTabUrl(initial_url)) {
|
||||||
|
initial_url = nebula::ui::GetHomeUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
NebulaController controller(instance, initial_url, show_command);
|
||||||
|
const bool created = controller.Create();
|
||||||
|
if (created) {
|
||||||
|
CefRunMessageLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
CefShutdown();
|
||||||
|
return created ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace nebula::app
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
namespace nebula::app {
|
||||||
|
|
||||||
|
int RunNebula(HINSTANCE instance, int show_command);
|
||||||
|
|
||||||
|
} // namespace nebula::app
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#include "browser/tab.h"
|
||||||
|
|
||||||
|
namespace nebula::browser {
|
||||||
|
|
||||||
|
bool NebulaTab::CanGoBack() const {
|
||||||
|
return browser && browser->CanGoBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NebulaTab::CanGoForward() const {
|
||||||
|
return browser && browser->CanGoForward();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace nebula::browser
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "include/cef_browser.h"
|
||||||
|
|
||||||
|
namespace nebula::browser {
|
||||||
|
|
||||||
|
struct NebulaTab {
|
||||||
|
int id = 1;
|
||||||
|
std::string url;
|
||||||
|
std::string title = "New Tab";
|
||||||
|
bool is_loading = false;
|
||||||
|
double load_progress = 0.0;
|
||||||
|
std::string favicon_url;
|
||||||
|
CefRefPtr<CefBrowser> browser;
|
||||||
|
|
||||||
|
bool CanGoBack() const;
|
||||||
|
bool CanGoForward() const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace nebula::browser
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
#include "browser/tab_manager.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "browser/url_utils.h"
|
||||||
|
#include "ui/paths.h"
|
||||||
|
|
||||||
|
namespace nebula::browser {
|
||||||
|
|
||||||
|
TabManager::TabManager(TabObserver* observer) : observer_(observer) {}
|
||||||
|
|
||||||
|
NebulaTab& TabManager::CreateInitialTab(std::string initial_url) {
|
||||||
|
tabs_.clear();
|
||||||
|
NebulaTab tab;
|
||||||
|
tab.id = next_tab_id_++;
|
||||||
|
tab.url = std::move(initial_url);
|
||||||
|
tabs_.push_back(std::move(tab));
|
||||||
|
active_tab_id_ = tabs_.front().id;
|
||||||
|
Notify();
|
||||||
|
return tabs_.front();
|
||||||
|
}
|
||||||
|
|
||||||
|
NebulaTab& TabManager::CreateTab(std::string url) {
|
||||||
|
NebulaTab tab;
|
||||||
|
tab.id = next_tab_id_++;
|
||||||
|
tab.url = std::move(url);
|
||||||
|
tabs_.push_back(std::move(tab));
|
||||||
|
active_tab_id_ = tabs_.back().id;
|
||||||
|
Notify();
|
||||||
|
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_) {
|
||||||
|
return &tab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NebulaTab* TabManager::ActiveTab() const {
|
||||||
|
for (const auto& tab : tabs_) {
|
||||||
|
if (tab.id == active_tab_id_) {
|
||||||
|
return &tab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
active_tab_id_ = tab_id;
|
||||||
|
Notify();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CefRefPtr<CefBrowser> TabManager::CloseTab(int tab_id) {
|
||||||
|
for (auto it = tabs_.begin(); it != tabs_.end(); ++it) {
|
||||||
|
if (it->id != tab_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CefRefPtr<CefBrowser> browser = it->browser;
|
||||||
|
const bool was_active = it->id == active_tab_id_;
|
||||||
|
const auto next_it = tabs_.erase(it);
|
||||||
|
|
||||||
|
if (tabs_.empty()) {
|
||||||
|
active_tab_id_ = 0;
|
||||||
|
} else if (was_active) {
|
||||||
|
active_tab_id_ = next_it != tabs_.end() ? next_it->id : tabs_.back().id;
|
||||||
|
}
|
||||||
|
|
||||||
|
Notify();
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::SetActiveBrowser(CefRefPtr<CefBrowser> browser) {
|
||||||
|
if (NebulaTab* tab = ActiveTab()) {
|
||||||
|
tab->browser = browser;
|
||||||
|
if (browser && tab->url.empty()) {
|
||||||
|
tab->url = browser->GetMainFrame()->GetURL();
|
||||||
|
}
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabManager::OwnsBrowser(CefRefPtr<CefBrowser> browser) const {
|
||||||
|
if (!browser) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& tab : tabs_) {
|
||||||
|
if (tab.browser && tab.browser->IsSame(browser)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TabManager::HasOpenBrowsers() const {
|
||||||
|
for (const auto& tab : tabs_) {
|
||||||
|
if (tab.browser) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::ClearBrowser(CefRefPtr<CefBrowser> browser) {
|
||||||
|
if (NebulaTab* tab = FindTab(browser)) {
|
||||||
|
tab->browser = nullptr;
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::LoadURL(const std::string& input) {
|
||||||
|
NebulaTab* tab = ActiveTab();
|
||||||
|
if (!tab || !tab->browser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string target = NormalizeNavigationInput(input);
|
||||||
|
if (target.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab->url = target;
|
||||||
|
tab->favicon_url.clear();
|
||||||
|
tab->browser->GetMainFrame()->LoadURL(nebula::ui::ResolveInternalUrl(target));
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::GoBack() {
|
||||||
|
NebulaTab* tab = ActiveTab();
|
||||||
|
if (tab && tab->browser && tab->browser->CanGoBack()) {
|
||||||
|
tab->browser->GoBack();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::GoForward() {
|
||||||
|
NebulaTab* tab = ActiveTab();
|
||||||
|
if (tab && tab->browser && tab->browser->CanGoForward()) {
|
||||||
|
tab->browser->GoForward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::Reload() {
|
||||||
|
NebulaTab* tab = ActiveTab();
|
||||||
|
if (tab && tab->browser) {
|
||||||
|
tab->browser->Reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::StopLoad() {
|
||||||
|
NebulaTab* tab = ActiveTab();
|
||||||
|
if (tab && tab->browser) {
|
||||||
|
tab->browser->StopLoad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::UpdateURL(CefRefPtr<CefBrowser> browser, std::string url) {
|
||||||
|
if (NebulaTab* tab = FindTab(browser)) {
|
||||||
|
tab->url = std::move(url);
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::UpdateTitle(CefRefPtr<CefBrowser> browser, std::string title) {
|
||||||
|
if (NebulaTab* tab = FindTab(browser)) {
|
||||||
|
tab->title = title.empty() ? "New Tab" : std::move(title);
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::UpdateLoadingState(CefRefPtr<CefBrowser> browser, bool is_loading) {
|
||||||
|
if (NebulaTab* tab = FindTab(browser)) {
|
||||||
|
tab->is_loading = is_loading;
|
||||||
|
if (is_loading) {
|
||||||
|
tab->favicon_url.clear();
|
||||||
|
}
|
||||||
|
if (!is_loading) {
|
||||||
|
tab->load_progress = 1.0;
|
||||||
|
}
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::UpdateLoadProgress(CefRefPtr<CefBrowser> browser, double progress) {
|
||||||
|
if (NebulaTab* tab = FindTab(browser)) {
|
||||||
|
tab->load_progress = progress;
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::UpdateFavicon(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls) {
|
||||||
|
if (NebulaTab* tab = FindTab(browser)) {
|
||||||
|
tab->favicon_url = urls.empty() ? std::string{} : urls.front();
|
||||||
|
Notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TabManager::Notify() {
|
||||||
|
const NebulaTab* tab = ActiveTab();
|
||||||
|
if (observer_ && tab) {
|
||||||
|
observer_->OnActiveTabChanged(*tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NebulaTab* TabManager::FindTab(int tab_id) {
|
||||||
|
for (auto& tab : tabs_) {
|
||||||
|
if (tab.id == tab_id) {
|
||||||
|
return &tab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
NebulaTab* TabManager::FindTab(CefRefPtr<CefBrowser> browser) {
|
||||||
|
if (!browser) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& tab : tabs_) {
|
||||||
|
if (tab.browser && tab.browser->IsSame(browser)) {
|
||||||
|
return &tab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace nebula::browser
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "browser/tab.h"
|
||||||
|
#include "browser/session_state.h"
|
||||||
|
|
||||||
|
namespace nebula::browser {
|
||||||
|
|
||||||
|
class TabObserver {
|
||||||
|
public:
|
||||||
|
virtual ~TabObserver() = default;
|
||||||
|
virtual void OnActiveTabChanged(const NebulaTab& tab) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TabManager {
|
||||||
|
public:
|
||||||
|
explicit TabManager(TabObserver* observer);
|
||||||
|
|
||||||
|
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);
|
||||||
|
void SetActiveBrowser(CefRefPtr<CefBrowser> browser);
|
||||||
|
bool OwnsBrowser(CefRefPtr<CefBrowser> browser) const;
|
||||||
|
void ClearBrowser(CefRefPtr<CefBrowser> browser);
|
||||||
|
bool HasOpenBrowsers() const;
|
||||||
|
|
||||||
|
void LoadURL(const std::string& input);
|
||||||
|
void GoBack();
|
||||||
|
void GoForward();
|
||||||
|
void Reload();
|
||||||
|
void StopLoad();
|
||||||
|
|
||||||
|
void UpdateURL(CefRefPtr<CefBrowser> browser, std::string url);
|
||||||
|
void UpdateTitle(CefRefPtr<CefBrowser> browser, std::string title);
|
||||||
|
void UpdateLoadingState(CefRefPtr<CefBrowser> browser, bool is_loading);
|
||||||
|
void UpdateLoadProgress(CefRefPtr<CefBrowser> browser, double progress);
|
||||||
|
void UpdateFavicon(CefRefPtr<CefBrowser> browser, const std::vector<std::string>& urls);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void Notify();
|
||||||
|
NebulaTab* FindTab(int tab_id);
|
||||||
|
NebulaTab* FindTab(CefRefPtr<CefBrowser> browser);
|
||||||
|
|
||||||
|
TabObserver* observer_ = nullptr;
|
||||||
|
std::vector<NebulaTab> tabs_;
|
||||||
|
int active_tab_id_ = 0;
|
||||||
|
int next_tab_id_ = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace nebula::browser
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#include "browser/url_utils.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
namespace nebula::browser {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr char kSearchUrl[] = "https://www.google.com/search?q=";
|
||||||
|
|
||||||
|
std::string Trim(std::string value) {
|
||||||
|
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.front()))) {
|
||||||
|
value.erase(value.begin());
|
||||||
|
}
|
||||||
|
while (!value.empty() && std::isspace(static_cast<unsigned char>(value.back()))) {
|
||||||
|
value.pop_back();
|
||||||
|
}
|
||||||
|
return 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("nebula://");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LooksLikeHostName(const std::string& value) {
|
||||||
|
return value.find('.') != std::string::npos &&
|
||||||
|
value.find_first_of(" \t\r\n") == std::string::npos;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string UrlEncodeSearch(const std::string& value) {
|
||||||
|
std::ostringstream encoded;
|
||||||
|
encoded << std::hex << std::uppercase;
|
||||||
|
|
||||||
|
for (unsigned char ch : value) {
|
||||||
|
if (std::isalnum(ch) || ch == '-' || ch == '_' || ch == '.' || ch == '~') {
|
||||||
|
encoded << static_cast<char>(ch);
|
||||||
|
} else if (ch == ' ') {
|
||||||
|
encoded << '+';
|
||||||
|
} else {
|
||||||
|
encoded << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return encoded.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::string NormalizeNavigationInput(const std::string& input) {
|
||||||
|
const std::string value = Trim(input);
|
||||||
|
if (value.empty()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StartsWithScheme(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (LooksLikeHostName(value)) {
|
||||||
|
return "https://" + value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::string(kSearchUrl) + UrlEncodeSearch(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string JsonEscape(const std::string& value) {
|
||||||
|
std::ostringstream escaped;
|
||||||
|
for (unsigned char ch : value) {
|
||||||
|
switch (ch) {
|
||||||
|
case '\\':
|
||||||
|
escaped << "\\\\";
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
escaped << "\\\"";
|
||||||
|
break;
|
||||||
|
case '\b':
|
||||||
|
escaped << "\\b";
|
||||||
|
break;
|
||||||
|
case '\f':
|
||||||
|
escaped << "\\f";
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
escaped << "\\n";
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
escaped << "\\r";
|
||||||
|
break;
|
||||||
|
case '\t':
|
||||||
|
escaped << "\\t";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (ch < 0x20) {
|
||||||
|
escaped << "\\u" << std::hex << std::uppercase << std::setw(4)
|
||||||
|
<< std::setfill('0') << static_cast<int>(ch);
|
||||||
|
} else {
|
||||||
|
escaped << static_cast<char>(ch);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return escaped.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace nebula::browser
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace nebula::browser {
|
||||||
|
|
||||||
|
std::string NormalizeNavigationInput(const std::string& input);
|
||||||
|
std::string JsonEscape(const std::string& value);
|
||||||
|
|
||||||
|
} // namespace nebula::browser
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
#include "cef/browser_client.h"
|
||||||
|
|
||||||
|
#include "include/cef_request.h"
|
||||||
|
#include "include/wrapper/cef_helpers.h"
|
||||||
|
#include "ui/paths.h"
|
||||||
|
|
||||||
|
namespace nebula::cef {
|
||||||
|
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());
|
||||||
|
for (const auto& value : values) {
|
||||||
|
result.push_back(value.ToString());
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
NebulaBrowserClient::NebulaBrowserClient(BrowserRole role, BrowserClientDelegate* delegate)
|
||||||
|
: role_(role), delegate_(delegate) {}
|
||||||
|
|
||||||
|
bool NebulaBrowserClient::OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
CefProcessId source_process,
|
||||||
|
CefRefPtr<CefProcessMessage> message) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
UNREFERENCED_PARAMETER(browser);
|
||||||
|
UNREFERENCED_PARAMETER(source_process);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaBrowserClient::OnAddressChange(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
const CefString& url) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
|
||||||
|
delegate_->OnContentAddressChanged(browser, url.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaBrowserClient::OnTitleChange(CefRefPtr<CefBrowser> browser,
|
||||||
|
const CefString& title) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
if (role_ == BrowserRole::Content && delegate_) {
|
||||||
|
delegate_->OnContentTitleChanged(browser, title.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaBrowserClient::OnFaviconURLChange(CefRefPtr<CefBrowser> browser,
|
||||||
|
const std::vector<CefString>& icon_urls) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
if (role_ == BrowserRole::Content && delegate_) {
|
||||||
|
delegate_->OnContentFaviconChanged(browser, ToStringVector(icon_urls));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
bool* is_keyboard_shortcut) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
UNREFERENCED_PARAMETER(os_event);
|
||||||
|
|
||||||
|
if (role_ == BrowserRole::Content &&
|
||||||
|
event.type == KEYEVENT_RAWKEYDOWN &&
|
||||||
|
(event.modifiers & EVENTFLAG_CONTROL_DOWN) != 0 &&
|
||||||
|
event.windows_key_code == 'T') {
|
||||||
|
if (is_keyboard_shortcut) {
|
||||||
|
*is_keyboard_shortcut = true;
|
||||||
|
}
|
||||||
|
if (delegate_) {
|
||||||
|
delegate_->OnPopupRequested(browser, nebula::ui::GetHomeUrl());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NebulaBrowserClient::OnBeforePopup(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
int popup_id,
|
||||||
|
const CefString& target_url,
|
||||||
|
const CefString& target_frame_name,
|
||||||
|
CefLifeSpanHandler::WindowOpenDisposition target_disposition,
|
||||||
|
bool user_gesture,
|
||||||
|
const CefPopupFeatures& popupFeatures,
|
||||||
|
CefWindowInfo& windowInfo,
|
||||||
|
CefRefPtr<CefClient>& client,
|
||||||
|
CefBrowserSettings& settings,
|
||||||
|
CefRefPtr<CefDictionaryValue>& extra_info,
|
||||||
|
bool* no_javascript_access) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
UNREFERENCED_PARAMETER(frame);
|
||||||
|
UNREFERENCED_PARAMETER(popup_id);
|
||||||
|
UNREFERENCED_PARAMETER(target_frame_name);
|
||||||
|
UNREFERENCED_PARAMETER(target_disposition);
|
||||||
|
UNREFERENCED_PARAMETER(user_gesture);
|
||||||
|
UNREFERENCED_PARAMETER(popupFeatures);
|
||||||
|
UNREFERENCED_PARAMETER(windowInfo);
|
||||||
|
UNREFERENCED_PARAMETER(client);
|
||||||
|
UNREFERENCED_PARAMETER(settings);
|
||||||
|
UNREFERENCED_PARAMETER(extra_info);
|
||||||
|
UNREFERENCED_PARAMETER(no_javascript_access);
|
||||||
|
|
||||||
|
if (role_ == BrowserRole::Content && delegate_) {
|
||||||
|
delegate_->OnPopupRequested(browser, target_url.ToString());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaBrowserClient::OnAfterCreated(CefRefPtr<CefBrowser> browser) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
if (delegate_) {
|
||||||
|
delegate_->OnBrowserCreated(role_, browser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaBrowserClient::OnBeforeClose(CefRefPtr<CefBrowser> browser) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
if (delegate_) {
|
||||||
|
delegate_->OnBrowserClosing(role_, browser);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaBrowserClient::OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
|
||||||
|
bool isLoading,
|
||||||
|
bool canGoBack,
|
||||||
|
bool canGoForward) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
UNREFERENCED_PARAMETER(canGoBack);
|
||||||
|
UNREFERENCED_PARAMETER(canGoForward);
|
||||||
|
|
||||||
|
if (role_ == BrowserRole::Content && delegate_) {
|
||||||
|
delegate_->OnContentLoadingStateChanged(browser, isLoading);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaBrowserClient::OnLoadStart(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
TransitionType transition_type) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
UNREFERENCED_PARAMETER(transition_type);
|
||||||
|
|
||||||
|
if (role_ == BrowserRole::Content && delegate_ && frame && frame->IsMain()) {
|
||||||
|
delegate_->OnContentLoadProgressChanged(browser, 0.12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaBrowserClient::OnLoadEnd(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
int httpStatusCode) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NebulaBrowserClient::OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
CefRefPtr<CefRequest> request,
|
||||||
|
bool user_gesture,
|
||||||
|
bool is_redirect) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
UNREFERENCED_PARAMETER(browser);
|
||||||
|
UNREFERENCED_PARAMETER(user_gesture);
|
||||||
|
UNREFERENCED_PARAMETER(is_redirect);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool NebulaBrowserClient::OnShowPermissionPrompt(
|
||||||
|
CefRefPtr<CefBrowser> browser,
|
||||||
|
uint64_t prompt_id,
|
||||||
|
const CefString& requesting_origin,
|
||||||
|
uint32_t requested_permissions,
|
||||||
|
CefRefPtr<CefPermissionPromptCallback> callback) {
|
||||||
|
CEF_REQUIRE_UI_THREAD();
|
||||||
|
UNREFERENCED_PARAMETER(prompt_id);
|
||||||
|
UNREFERENCED_PARAMETER(requesting_origin);
|
||||||
|
|
||||||
|
if (role_ == BrowserRole::Content &&
|
||||||
|
(requested_permissions & CEF_PERMISSION_TYPE_GEOLOCATION) != 0 &&
|
||||||
|
browser && callback &&
|
||||||
|
nebula::ui::IsInternalHomeUrl(browser->GetMainFrame()->GetURL().ToString())) {
|
||||||
|
callback->Continue(CEF_PERMISSION_RESULT_ACCEPT);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace nebula::cef
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "include/cef_client.h"
|
||||||
|
#include "include/cef_display_handler.h"
|
||||||
|
#include "include/cef_keyboard_handler.h"
|
||||||
|
#include "include/cef_life_span_handler.h"
|
||||||
|
#include "include/cef_load_handler.h"
|
||||||
|
#include "include/cef_permission_handler.h"
|
||||||
|
#include "include/cef_request_handler.h"
|
||||||
|
|
||||||
|
namespace nebula::cef {
|
||||||
|
|
||||||
|
enum class BrowserRole {
|
||||||
|
Chrome,
|
||||||
|
Content,
|
||||||
|
MenuPopup,
|
||||||
|
};
|
||||||
|
|
||||||
|
class BrowserClientDelegate {
|
||||||
|
public:
|
||||||
|
virtual ~BrowserClientDelegate() = default;
|
||||||
|
virtual void OnBrowserCreated(BrowserRole role, CefRefPtr<CefBrowser> browser) = 0;
|
||||||
|
virtual void OnBrowserClosing(BrowserRole role, CefRefPtr<CefBrowser> browser) = 0;
|
||||||
|
virtual void OnChromeCommand(const std::string& command, const std::string& payload) = 0;
|
||||||
|
virtual void OnContentAddressChanged(CefRefPtr<CefBrowser> browser, const std::string& url) = 0;
|
||||||
|
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,
|
||||||
|
public CefDisplayHandler,
|
||||||
|
public CefKeyboardHandler,
|
||||||
|
public CefLifeSpanHandler,
|
||||||
|
public CefLoadHandler,
|
||||||
|
public CefPermissionHandler,
|
||||||
|
public CefRequestHandler {
|
||||||
|
public:
|
||||||
|
NebulaBrowserClient(BrowserRole role, BrowserClientDelegate* delegate);
|
||||||
|
|
||||||
|
CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
|
||||||
|
CefRefPtr<CefKeyboardHandler> GetKeyboardHandler() override { return this; }
|
||||||
|
CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
|
||||||
|
CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
|
||||||
|
CefRefPtr<CefPermissionHandler> GetPermissionHandler() override { return this; }
|
||||||
|
CefRefPtr<CefRequestHandler> GetRequestHandler() override { return this; }
|
||||||
|
|
||||||
|
bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
CefProcessId source_process,
|
||||||
|
CefRefPtr<CefProcessMessage> message) override;
|
||||||
|
|
||||||
|
void OnAddressChange(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
const CefString& url) override;
|
||||||
|
void OnTitleChange(CefRefPtr<CefBrowser> browser,
|
||||||
|
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,
|
||||||
|
CefEventHandle os_event,
|
||||||
|
bool* is_keyboard_shortcut) override;
|
||||||
|
|
||||||
|
bool OnBeforePopup(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
int popup_id,
|
||||||
|
const CefString& target_url,
|
||||||
|
const CefString& target_frame_name,
|
||||||
|
CefLifeSpanHandler::WindowOpenDisposition target_disposition,
|
||||||
|
bool user_gesture,
|
||||||
|
const CefPopupFeatures& popupFeatures,
|
||||||
|
CefWindowInfo& windowInfo,
|
||||||
|
CefRefPtr<CefClient>& client,
|
||||||
|
CefBrowserSettings& settings,
|
||||||
|
CefRefPtr<CefDictionaryValue>& extra_info,
|
||||||
|
bool* no_javascript_access) override;
|
||||||
|
|
||||||
|
void OnAfterCreated(CefRefPtr<CefBrowser> browser) override;
|
||||||
|
void OnBeforeClose(CefRefPtr<CefBrowser> browser) override;
|
||||||
|
|
||||||
|
void OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
|
||||||
|
bool isLoading,
|
||||||
|
bool canGoBack,
|
||||||
|
bool canGoForward) override;
|
||||||
|
void OnLoadStart(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
TransitionType transition_type) override;
|
||||||
|
void OnLoadEnd(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
int httpStatusCode) override;
|
||||||
|
|
||||||
|
bool OnBeforeBrowse(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
CefRefPtr<CefRequest> request,
|
||||||
|
bool user_gesture,
|
||||||
|
bool is_redirect) override;
|
||||||
|
|
||||||
|
bool OnShowPermissionPrompt(CefRefPtr<CefBrowser> browser,
|
||||||
|
uint64_t prompt_id,
|
||||||
|
const CefString& requesting_origin,
|
||||||
|
uint32_t requested_permissions,
|
||||||
|
CefRefPtr<CefPermissionPromptCallback> callback) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
BrowserRole role_;
|
||||||
|
BrowserClientDelegate* delegate_ = nullptr;
|
||||||
|
|
||||||
|
IMPLEMENT_REFCOUNTING(NebulaBrowserClient);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace nebula::cef
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
#include "cef/nebula_app.h"
|
||||||
|
|
||||||
|
#include "include/cef_process_message.h"
|
||||||
|
#include "include/wrapper/cef_helpers.h"
|
||||||
|
|
||||||
|
namespace nebula::cef {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr char kChromeCommandMessage[] = "NebulaChromeCommand";
|
||||||
|
|
||||||
|
class NativeBridgeHandler final : public CefV8Handler {
|
||||||
|
public:
|
||||||
|
bool Execute(const CefString& name,
|
||||||
|
CefRefPtr<CefV8Value> object,
|
||||||
|
const CefV8ValueList& arguments,
|
||||||
|
CefRefPtr<CefV8Value>& retval,
|
||||||
|
CefString& exception) override {
|
||||||
|
UNREFERENCED_PARAMETER(object);
|
||||||
|
UNREFERENCED_PARAMETER(retval);
|
||||||
|
|
||||||
|
if (name != "postMessage" && name != "sendToHost" && name != "send") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arguments.empty() || !arguments[0]->IsString()) {
|
||||||
|
exception = "nebulaNative.postMessage requires a command string.";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CefRefPtr<CefV8Context> context = CefV8Context::GetCurrentContext();
|
||||||
|
CefRefPtr<CefBrowser> browser = context ? context->GetBrowser() : nullptr;
|
||||||
|
CefRefPtr<CefFrame> frame = context ? context->GetFrame() : nullptr;
|
||||||
|
if (!browser || !frame) {
|
||||||
|
exception = "No CEF frame is available for native messaging.";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CefRefPtr<CefProcessMessage> message = CefProcessMessage::Create(kChromeCommandMessage);
|
||||||
|
CefRefPtr<CefListValue> args = message->GetArgumentList();
|
||||||
|
args->SetString(0, arguments[0]->GetStringValue());
|
||||||
|
args->SetString(1, arguments.size() > 1 && arguments[1]->IsString()
|
||||||
|
? arguments[1]->GetStringValue()
|
||||||
|
: CefString());
|
||||||
|
|
||||||
|
frame->SendProcessMessage(PID_BROWSER, message);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
IMPLEMENT_REFCOUNTING(NativeBridgeHandler);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void NebulaApp::OnBeforeCommandLineProcessing(const CefString& process_type,
|
||||||
|
CefRefPtr<CefCommandLine> command_line) {
|
||||||
|
UNREFERENCED_PARAMETER(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,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
CefRefPtr<CefV8Context> context) {
|
||||||
|
CEF_REQUIRE_RENDERER_THREAD();
|
||||||
|
UNREFERENCED_PARAMETER(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", 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
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "include/cef_app.h"
|
||||||
|
#include "include/cef_render_process_handler.h"
|
||||||
|
#include "include/cef_v8.h"
|
||||||
|
|
||||||
|
namespace nebula::cef {
|
||||||
|
|
||||||
|
class NebulaApp final : public CefApp,
|
||||||
|
public CefRenderProcessHandler {
|
||||||
|
public:
|
||||||
|
CefRefPtr<CefRenderProcessHandler> GetRenderProcessHandler() override { return this; }
|
||||||
|
|
||||||
|
void OnBeforeCommandLineProcessing(const CefString& process_type,
|
||||||
|
CefRefPtr<CefCommandLine> command_line) override;
|
||||||
|
|
||||||
|
void OnContextCreated(CefRefPtr<CefBrowser> browser,
|
||||||
|
CefRefPtr<CefFrame> frame,
|
||||||
|
CefRefPtr<CefV8Context> context) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
IMPLEMENT_REFCOUNTING(NebulaApp);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace nebula::cef
|
||||||
@@ -0,0 +1,304 @@
|
|||||||
|
#include "ui/paths.h"
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#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 {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const int size = WideCharToMultiByte(
|
||||||
|
CP_UTF8, 0, value.data(), static_cast<int>(value.size()),
|
||||||
|
nullptr, 0, nullptr, nullptr);
|
||||||
|
std::string result(size, '\0');
|
||||||
|
WideCharToMultiByte(
|
||||||
|
CP_UTF8, 0, value.data(), static_cast<int>(value.size()),
|
||||||
|
result.data(), size, nullptr, nullptr);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetUrlWithoutDecoration(std::string url) {
|
||||||
|
const size_t split = url.find_first_of("?#");
|
||||||
|
if (split != std::string::npos) {
|
||||||
|
url.resize(split);
|
||||||
|
}
|
||||||
|
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));
|
||||||
|
});
|
||||||
|
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() {
|
||||||
|
wchar_t exe_path[MAX_PATH] = {};
|
||||||
|
const DWORD length = GetModuleFileNameW(nullptr, exe_path, MAX_PATH);
|
||||||
|
if (length == 0 || length == MAX_PATH) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return exe_dir / L"ui" / L"pages" / page_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string FilePathToUrl(std::filesystem::path path) {
|
||||||
|
std::string value = WideToUtf8(path.wstring());
|
||||||
|
for (char& ch : value) {
|
||||||
|
if (ch == '\\') {
|
||||||
|
ch = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string encoded;
|
||||||
|
encoded.reserve(value.size());
|
||||||
|
for (char ch : value) {
|
||||||
|
encoded += ch == ' ' ? "%20" : std::string(1, ch);
|
||||||
|
}
|
||||||
|
return "file:///" + encoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetChromeUrl() {
|
||||||
|
const auto path = GetUiPagePath(L"chrome.html");
|
||||||
|
const std::string fallback = PageFileUrl(L"home.html");
|
||||||
|
return path.empty() ? (fallback.empty() ? "https://www.google.com" : fallback) : FilePathToUrl(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetHomeUrl() {
|
||||||
|
return InternalUrlForSlug("home");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetSettingsUrl() {
|
||||||
|
return InternalUrlForSlug("settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetDownloadsUrl() {
|
||||||
|
return InternalUrlForSlug("downloads");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetBigPictureUrl() {
|
||||||
|
return InternalUrlForSlug("bigpicture");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetGpuDiagnosticsUrl() {
|
||||||
|
return InternalUrlForSlug("gpu-diagnostics");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string GetMenuPopupUrl() {
|
||||||
|
const auto path = GetUiPagePath(L"menu-popup.html");
|
||||||
|
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(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) {
|
||||||
|
const std::string target = ToLowerAscii(GetUrlWithoutDecoration(url));
|
||||||
|
return target == "about:blank" ||
|
||||||
|
target == "chrome://newtab" ||
|
||||||
|
target == "chrome://newtab/" ||
|
||||||
|
target == "chrome://new-tab-page" ||
|
||||||
|
target == "chrome://new-tab-page/" ||
|
||||||
|
target == "chrome-search://local-ntp/local-ntp.html";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsEmptyOrChromiumNewTabUrl(const std::string& url) {
|
||||||
|
return url.empty() || IsChromiumNewTabUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace nebula::ui
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
} // namespace nebula::ui
|
||||||
@@ -0,0 +1,538 @@
|
|||||||
|
#include "window/nebula_window.h"
|
||||||
|
|
||||||
|
#include <dwmapi.h>
|
||||||
|
#include <windowsx.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace nebula::window {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr wchar_t kWindowClassName[] = L"NebulaBrowserWindow";
|
||||||
|
constexpr wchar_t kWindowTitle[] = L"Nebula Browser";
|
||||||
|
constexpr wchar_t kChildFrameHitTestOldProcProp[] = L"NebulaChildFrameHitTestOldProc";
|
||||||
|
constexpr wchar_t kChildFrameHitTestParentProp[] = L"NebulaChildFrameHitTestParent";
|
||||||
|
constexpr int kTitleRowHeightDip = 42;
|
||||||
|
constexpr int kWindowControlWidthDip = 46;
|
||||||
|
constexpr int kWindowControlCount = 3;
|
||||||
|
constexpr COLORREF kNoWindowBorderColor = 0xFFFFFFFE;
|
||||||
|
|
||||||
|
RECT GetWorkArea() {
|
||||||
|
RECT work_area = {};
|
||||||
|
SystemParametersInfoW(SPI_GETWORKAREA, 0, &work_area, 0);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
HCURSOR CursorForResizeHit(LRESULT hit) {
|
||||||
|
switch (hit) {
|
||||||
|
case HTLEFT:
|
||||||
|
case HTRIGHT:
|
||||||
|
return LoadCursor(nullptr, IDC_SIZEWE);
|
||||||
|
case HTTOP:
|
||||||
|
case HTBOTTOM:
|
||||||
|
return LoadCursor(nullptr, IDC_SIZENS);
|
||||||
|
case HTTOPLEFT:
|
||||||
|
case HTBOTTOMRIGHT:
|
||||||
|
return LoadCursor(nullptr, IDC_SIZENWSE);
|
||||||
|
case HTTOPRIGHT:
|
||||||
|
case HTBOTTOMLEFT:
|
||||||
|
return LoadCursor(nullptr, IDC_SIZENESW);
|
||||||
|
default:
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SetResizeCursor(LRESULT hit) {
|
||||||
|
HCURSOR cursor = CursorForResizeHit(hit);
|
||||||
|
if (!cursor) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetCursor(cursor);
|
||||||
|
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) {}
|
||||||
|
|
||||||
|
NebulaWindow::~NebulaWindow() = default;
|
||||||
|
|
||||||
|
bool NebulaWindow::Create(HINSTANCE instance, int show_command) {
|
||||||
|
instance_ = instance;
|
||||||
|
RegisterClass(instance);
|
||||||
|
|
||||||
|
const RECT work_area = GetWorkArea();
|
||||||
|
dpi_ = GetDpiForSystem();
|
||||||
|
const int width = std::min<LONG>(ScaleForDpi(1400), work_area.right - work_area.left);
|
||||||
|
const int height = std::min<LONG>(ScaleForDpi(900), work_area.bottom - work_area.top);
|
||||||
|
const int x = work_area.left + ((work_area.right - work_area.left) - width) / 2;
|
||||||
|
const int y = work_area.top + ((work_area.bottom - work_area.top) - height) / 2;
|
||||||
|
|
||||||
|
hwnd_ = CreateWindowExW(
|
||||||
|
0,
|
||||||
|
kWindowClassName,
|
||||||
|
kWindowTitle,
|
||||||
|
WS_POPUP | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_CLIPCHILDREN,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
nullptr,
|
||||||
|
nullptr,
|
||||||
|
instance_,
|
||||||
|
this);
|
||||||
|
|
||||||
|
if (!hwnd_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateDpi();
|
||||||
|
ApplyWindowFrameStyle(hwnd_);
|
||||||
|
|
||||||
|
const MARGINS margins = {0, 0, 0, 0};
|
||||||
|
DwmExtendFrameIntoClientArea(hwnd_, &margins);
|
||||||
|
|
||||||
|
ShowWindow(hwnd_, show_command);
|
||||||
|
UpdateWindow(hwnd_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BrowserLayout NebulaWindow::CurrentLayout(bool show_chrome) const {
|
||||||
|
RECT client = {};
|
||||||
|
if (hwnd_) {
|
||||||
|
GetClientRect(hwnd_, &client);
|
||||||
|
}
|
||||||
|
|
||||||
|
BrowserLayout layout;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaWindow::ResizeChild(HWND child, const RECT& rect) const {
|
||||||
|
if (!child) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnableFrameHitTest(child);
|
||||||
|
SetWindowPos(
|
||||||
|
child,
|
||||||
|
nullptr,
|
||||||
|
rect.left,
|
||||||
|
rect.top,
|
||||||
|
std::max(0L, rect.right - rect.left),
|
||||||
|
std::max(0L, rect.bottom - rect.top),
|
||||||
|
SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOOWNERZORDER);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaWindow::Minimize() {
|
||||||
|
if (hwnd_) {
|
||||||
|
ShowWindow(hwnd_, SW_MINIMIZE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaWindow::ToggleMaximize() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaWindow::BeginDrag() {
|
||||||
|
if (!hwnd_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReleaseCapture();
|
||||||
|
SendMessageW(hwnd_, WM_NCLBUTTONDOWN, HTCAPTION, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaWindow::SetTitle(const std::wstring& title) {
|
||||||
|
if (hwnd_) {
|
||||||
|
SetWindowTextW(hwnd_, title.empty() ? kWindowTitle : title.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaWindow::EnableFrameHitTest(HWND child) const {
|
||||||
|
if (!hwnd_ || !child) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnableFrameHitTestForWindow(child);
|
||||||
|
EnumChildWindows(child, &NebulaWindow::EnableFrameHitTestForDescendant, reinterpret_cast<LPARAM>(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CALLBACK NebulaWindow::StaticWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
||||||
|
NebulaWindow* self = nullptr;
|
||||||
|
|
||||||
|
if (message == WM_NCCREATE) {
|
||||||
|
auto* create = reinterpret_cast<CREATESTRUCTW*>(lparam);
|
||||||
|
self = static_cast<NebulaWindow*>(create->lpCreateParams);
|
||||||
|
SetWindowLongPtrW(hwnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(self));
|
||||||
|
self->hwnd_ = hwnd;
|
||||||
|
} else {
|
||||||
|
self = reinterpret_cast<NebulaWindow*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||||
|
}
|
||||||
|
|
||||||
|
return self ? self->WndProc(message, wparam, lparam)
|
||||||
|
: DefWindowProcW(hwnd, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CALLBACK NebulaWindow::ChildFrameWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) {
|
||||||
|
auto old_proc = reinterpret_cast<WNDPROC>(GetPropW(hwnd, kChildFrameHitTestOldProcProp));
|
||||||
|
|
||||||
|
if (message == WM_NCHITTEST) {
|
||||||
|
const auto parent = reinterpret_cast<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
|
||||||
|
auto* self = parent ? reinterpret_cast<NebulaWindow*>(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr;
|
||||||
|
if (self) {
|
||||||
|
const LRESULT hit = self->HitTest(lparam);
|
||||||
|
if (IsResizeHit(hit)) {
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == WM_SETCURSOR) {
|
||||||
|
const auto parent = reinterpret_cast<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
|
||||||
|
auto* self = parent ? reinterpret_cast<NebulaWindow*>(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr;
|
||||||
|
POINT point = {};
|
||||||
|
if (self && GetCursorPos(&point) && SetResizeCursor(self->HitTestPoint(point))) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == WM_MOUSEMOVE || message == WM_NCMOUSEMOVE) {
|
||||||
|
const auto parent = reinterpret_cast<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
|
||||||
|
auto* self = parent ? reinterpret_cast<NebulaWindow*>(GetWindowLongPtrW(parent, GWLP_USERDATA)) : nullptr;
|
||||||
|
POINT point = {};
|
||||||
|
if (self && GetCursorPos(&point) && SetResizeCursor(self->HitTestPoint(point))) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == WM_NCLBUTTONDOWN && IsResizeHit(static_cast<LRESULT>(wparam))) {
|
||||||
|
const auto parent = reinterpret_cast<HWND>(GetPropW(hwnd, kChildFrameHitTestParentProp));
|
||||||
|
if (parent) {
|
||||||
|
ReleaseCapture();
|
||||||
|
SendMessageW(parent, WM_NCLBUTTONDOWN, wparam, lparam);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message == WM_NCDESTROY) {
|
||||||
|
RemovePropW(hwnd, kChildFrameHitTestParentProp);
|
||||||
|
RemovePropW(hwnd, kChildFrameHitTestOldProcProp);
|
||||||
|
if (old_proc) {
|
||||||
|
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(old_proc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return old_proc ? CallWindowProcW(old_proc, hwnd, message, wparam, lparam)
|
||||||
|
: DefWindowProcW(hwnd, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL CALLBACK NebulaWindow::EnableFrameHitTestForDescendant(HWND hwnd, LPARAM lparam) {
|
||||||
|
const auto* self = reinterpret_cast<const NebulaWindow*>(lparam);
|
||||||
|
if (self) {
|
||||||
|
self->EnableFrameHitTestForWindow(hwnd);
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT NebulaWindow::WndProc(UINT message, WPARAM wparam, LPARAM lparam) {
|
||||||
|
switch (message) {
|
||||||
|
case WM_CREATE:
|
||||||
|
UpdateDpi();
|
||||||
|
if (delegate_) {
|
||||||
|
delegate_->OnWindowCreated();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_NCCALCSIZE:
|
||||||
|
if (wparam == TRUE) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_NCACTIVATE:
|
||||||
|
ApplyWindowFrameStyle(hwnd_);
|
||||||
|
return TRUE;
|
||||||
|
|
||||||
|
case WM_ERASEBKGND:
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
case WM_NCHITTEST:
|
||||||
|
return HitTest(lparam);
|
||||||
|
|
||||||
|
case WM_SETCURSOR: {
|
||||||
|
POINT point = {};
|
||||||
|
if (GetCursorPos(&point) && SetResizeCursor(HitTestPoint(point))) {
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case WM_MOUSEMOVE:
|
||||||
|
case WM_NCMOUSEMOVE: {
|
||||||
|
POINT point = {};
|
||||||
|
if (GetCursorPos(&point) && SetResizeCursor(HitTestPoint(point))) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case WM_SIZE:
|
||||||
|
NotifyResize();
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
case WM_DPICHANGED: {
|
||||||
|
dpi_ = HIWORD(wparam);
|
||||||
|
const auto* suggested_rect = reinterpret_cast<RECT*>(lparam);
|
||||||
|
SetWindowPos(
|
||||||
|
hwnd_,
|
||||||
|
nullptr,
|
||||||
|
suggested_rect->left,
|
||||||
|
suggested_rect->top,
|
||||||
|
suggested_rect->right - suggested_rect->left,
|
||||||
|
suggested_rect->bottom - suggested_rect->top,
|
||||||
|
SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
|
NotifyResize();
|
||||||
|
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();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WM_DESTROY:
|
||||||
|
hwnd_ = nullptr;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DefWindowProcW(hwnd_, message, wparam, lparam);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaWindow::RegisterClass(HINSTANCE instance) {
|
||||||
|
WNDCLASSEXW window_class = {};
|
||||||
|
window_class.cbSize = sizeof(window_class);
|
||||||
|
window_class.lpfnWndProc = StaticWndProc;
|
||||||
|
window_class.hInstance = instance;
|
||||||
|
window_class.hCursor = LoadCursor(nullptr, IDC_ARROW);
|
||||||
|
window_class.lpszClassName = kWindowClassName;
|
||||||
|
|
||||||
|
RegisterClassExW(&window_class);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaWindow::NotifyResize() {
|
||||||
|
if (delegate_) {
|
||||||
|
delegate_->OnWindowResized(CurrentLayout());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaWindow::EnableFrameHitTestForWindow(HWND child) const {
|
||||||
|
if (!child || GetPropW(child, kChildFrameHitTestOldProcProp)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetPropW(child, kChildFrameHitTestParentProp, hwnd_);
|
||||||
|
const auto old_proc = reinterpret_cast<WNDPROC>(
|
||||||
|
SetWindowLongPtrW(child, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&NebulaWindow::ChildFrameWndProc)));
|
||||||
|
if (old_proc) {
|
||||||
|
SetPropW(child, kChildFrameHitTestOldProcProp, reinterpret_cast<HANDLE>(old_proc));
|
||||||
|
} else {
|
||||||
|
RemovePropW(child, kChildFrameHitTestParentProp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int NebulaWindow::ScaleForDpi(int value) const {
|
||||||
|
return MulDiv(value, static_cast<int>(dpi_), 96);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NebulaWindow::UpdateDpi() {
|
||||||
|
if (hwnd_) {
|
||||||
|
dpi_ = GetDpiForWindow(hwnd_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT NebulaWindow::HitTest(LPARAM lparam) const {
|
||||||
|
if (!hwnd_) {
|
||||||
|
return HTNOWHERE;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT point = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
|
||||||
|
return HitTestPoint(point);
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT NebulaWindow::HitTestPoint(POINT point) const {
|
||||||
|
if (!hwnd_) {
|
||||||
|
return HTNOWHERE;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
const bool top = point.y >= window.top && point.y < window.top + resize_border;
|
||||||
|
const bool bottom = point.y < window.bottom && point.y >= window.bottom - resize_border;
|
||||||
|
|
||||||
|
if (top && left) {
|
||||||
|
return HTTOPLEFT;
|
||||||
|
}
|
||||||
|
if (top && right) {
|
||||||
|
return HTTOPRIGHT;
|
||||||
|
}
|
||||||
|
if (bottom && left) {
|
||||||
|
return HTBOTTOMLEFT;
|
||||||
|
}
|
||||||
|
if (bottom && right) {
|
||||||
|
return HTBOTTOMRIGHT;
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
return HTLEFT;
|
||||||
|
}
|
||||||
|
if (right) {
|
||||||
|
return HTRIGHT;
|
||||||
|
}
|
||||||
|
if (top) {
|
||||||
|
return HTTOP;
|
||||||
|
}
|
||||||
|
if (bottom) {
|
||||||
|
return HTBOTTOM;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int controls_width = ScaleForDpi(kWindowControlWidthDip * kWindowControlCount);
|
||||||
|
const int controls_height = ScaleForDpi(kTitleRowHeightDip);
|
||||||
|
const bool window_controls = point.x >= window.right - controls_width && point.x < window.right &&
|
||||||
|
point.y >= window.top && point.y < window.top + controls_height;
|
||||||
|
if (window_controls) {
|
||||||
|
return HTCLIENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return HTCLIENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace nebula::window
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace nebula::window {
|
||||||
|
|
||||||
|
struct BrowserLayout {
|
||||||
|
RECT chrome = {};
|
||||||
|
RECT content = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
class WindowDelegate {
|
||||||
|
public:
|
||||||
|
virtual ~WindowDelegate() = default;
|
||||||
|
virtual void OnWindowCreated() = 0;
|
||||||
|
virtual void OnWindowResized(const BrowserLayout& layout) = 0;
|
||||||
|
virtual void OnWindowCloseRequested() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class NebulaWindow {
|
||||||
|
public:
|
||||||
|
explicit NebulaWindow(WindowDelegate* delegate);
|
||||||
|
~NebulaWindow();
|
||||||
|
|
||||||
|
bool Create(HINSTANCE instance, int show_command);
|
||||||
|
HWND hwnd() const { return hwnd_; }
|
||||||
|
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);
|
||||||
|
void EnableFrameHitTest(HWND child) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
static LRESULT CALLBACK StaticWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
|
||||||
|
static LRESULT CALLBACK ChildFrameWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
|
||||||
|
static BOOL CALLBACK EnableFrameHitTestForDescendant(HWND hwnd, LPARAM lparam);
|
||||||
|
LRESULT WndProc(UINT message, WPARAM wparam, LPARAM lparam);
|
||||||
|
|
||||||
|
void RegisterClass(HINSTANCE instance);
|
||||||
|
void NotifyResize();
|
||||||
|
void EnableFrameHitTestForWindow(HWND child) const;
|
||||||
|
LRESULT HitTest(LPARAM lparam) const;
|
||||||
|
LRESULT HitTestPoint(POINT point) const;
|
||||||
|
int ScaleForDpi(int value) const;
|
||||||
|
void UpdateDpi();
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace nebula::window
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #080a0f;
|
||||||
|
--surface: #0e1119;
|
||||||
|
--surface-raised: #141824;
|
||||||
|
--surface-hover: rgba(255, 255, 255, 0.06);
|
||||||
|
--text: #e8e8f0;
|
||||||
|
--muted: #7a7e90;
|
||||||
|
--accent: #7b2eff;
|
||||||
|
--accent-2: #00c6ff;
|
||||||
|
--outline: #1f2533;
|
||||||
|
--outline-soft: rgba(255, 255, 255, 0.06);
|
||||||
|
--danger: #e0445c;
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "InterVariable";
|
||||||
|
src: url("../assets/fonts/InterVariable.ttf") format("truetype");
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
font-family: "InterVariable", "Segoe UI", system-ui, sans-serif;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
input {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 0;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Chrome shell ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.nebula-chrome {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 42px 52px;
|
||||||
|
height: 100%;
|
||||||
|
border-bottom: 1px solid var(--outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Title row ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.title-row,
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
gap: 10px;
|
||||||
|
padding: 0 0 0 12px;
|
||||||
|
background: var(--bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Tabs ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 3px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
min-width: 0;
|
||||||
|
width: min(260px, 38vw);
|
||||||
|
height: 33px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 10px 10px 0 0;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-bottom: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
transition: background 120ms, color 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover:not(.active) {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: var(--surface-raised);
|
||||||
|
border-color: var(--outline);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-favicon,
|
||||||
|
.tab-loading {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-favicon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--accent);
|
||||||
|
opacity: 0.85;
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-favicon.has-favicon {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-favicon.empty {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-favicon img {
|
||||||
|
display: block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-loading {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
border: 2px solid rgba(0, 198, 255, 0.2);
|
||||||
|
border-top-color: var(--accent-2);
|
||||||
|
border-radius: 999px;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
opacity: 0;
|
||||||
|
transition: background 120ms, color 120ms, opacity 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover .tab-close,
|
||||||
|
.tab.active .tab-close,
|
||||||
|
.tab-close:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close:hover {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-add {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
transition: background 120ms, color 120ms, border-color 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-add:hover {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
border-color: var(--outline);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Window controls ────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.window-controls {
|
||||||
|
display: flex;
|
||||||
|
align-self: stretch;
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 46px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
transition: background 100ms, color 100ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls button:hover {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-controls .close:hover {
|
||||||
|
background: var(--danger);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Toolbar ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: var(--surface-raised);
|
||||||
|
border-top: 1px solid var(--outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Lucide icon sizing ─────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Lucide replaces <i data-lucide> with <svg>; enforce consistent size */
|
||||||
|
.icon-button svg,
|
||||||
|
.tab-close svg,
|
||||||
|
.tab-add svg,
|
||||||
|
.window-controls svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: block;
|
||||||
|
stroke-width: 1.75;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Icon buttons ───────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.icon-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background 120ms, color 120ms, border-color 120ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-button:hover:not(:disabled) {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
border-color: var(--outline);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Address bar ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.address-shell {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 160px;
|
||||||
|
height: 36px;
|
||||||
|
flex: 1;
|
||||||
|
margin: 0 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid var(--outline);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--surface);
|
||||||
|
transition: border-color 140ms, box-shadow 140ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-shell:focus-within {
|
||||||
|
border-color: rgba(123, 46, 255, 0.55);
|
||||||
|
box-shadow: 0 0 0 3px rgba(123, 46, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-shell input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 0;
|
||||||
|
outline: 0;
|
||||||
|
padding: 0 16px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 0.84rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-shell input::placeholder {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 2px;
|
||||||
|
width: 0%;
|
||||||
|
pointer-events: none;
|
||||||
|
background: var(--accent);
|
||||||
|
border-radius: 0 2px 2px 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
transition: width 120ms ease, opacity 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Utilities ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
const SEARCH_URL = 'https://www.google.com/search?q=';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
id: 1,
|
||||||
|
url: '',
|
||||||
|
title: 'New Tab',
|
||||||
|
isLoading: false,
|
||||||
|
progress: 0,
|
||||||
|
canGoBack: false,
|
||||||
|
canGoForward: false,
|
||||||
|
favicon: '',
|
||||||
|
tabs: []
|
||||||
|
};
|
||||||
|
|
||||||
|
function toNavigationUrl(input) {
|
||||||
|
const value = (input || '').trim();
|
||||||
|
if (!value) return null;
|
||||||
|
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)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function postCommand(command, payload = '') {
|
||||||
|
if (window.nebulaNative && typeof window.nebulaNative.postMessage === 'function') {
|
||||||
|
window.nebulaNative.postMessage(command, String(payload));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderFavicon(favicon, tab) {
|
||||||
|
const url = (tab.favicon || '').trim();
|
||||||
|
favicon.className = 'tab-favicon';
|
||||||
|
favicon.textContent = '';
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
|
favicon.classList.add('empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const image = document.createElement('img');
|
||||||
|
image.alt = '';
|
||||||
|
image.decoding = 'async';
|
||||||
|
image.draggable = false;
|
||||||
|
image.addEventListener('load', () => {
|
||||||
|
favicon.classList.add('has-favicon');
|
||||||
|
});
|
||||||
|
image.addEventListener('error', () => {
|
||||||
|
image.remove();
|
||||||
|
favicon.classList.remove('has-favicon');
|
||||||
|
favicon.classList.add('empty');
|
||||||
|
});
|
||||||
|
|
||||||
|
favicon.append(image);
|
||||||
|
image.src = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTabs() {
|
||||||
|
const tabsElement = document.querySelector('.tabs');
|
||||||
|
const addButton = tabsElement.querySelector('.tab-add');
|
||||||
|
const tabs = state.tabs.length
|
||||||
|
? state.tabs
|
||||||
|
: [{ id: state.id, title: state.title, isLoading: state.isLoading, favicon: state.favicon }];
|
||||||
|
|
||||||
|
tabsElement.querySelectorAll('.tab').forEach(tab => tab.remove());
|
||||||
|
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
const button = document.createElement('div');
|
||||||
|
const isActive = tab.id === state.id;
|
||||||
|
button.className = `tab${isActive ? ' active' : ''}`;
|
||||||
|
button.setAttribute('role', 'tab');
|
||||||
|
button.setAttribute('aria-selected', String(isActive));
|
||||||
|
button.tabIndex = 0;
|
||||||
|
button.dataset.tabId = String(tab.id);
|
||||||
|
|
||||||
|
const favicon = document.createElement('span');
|
||||||
|
renderFavicon(favicon, tab);
|
||||||
|
|
||||||
|
const title = document.createElement('span');
|
||||||
|
title.className = 'tab-title';
|
||||||
|
title.textContent = tab.title || 'New Tab';
|
||||||
|
|
||||||
|
const loading = document.createElement('span');
|
||||||
|
loading.className = 'tab-loading';
|
||||||
|
loading.hidden = !tab.isLoading;
|
||||||
|
|
||||||
|
const close = document.createElement('button');
|
||||||
|
close.className = 'tab-close';
|
||||||
|
close.type = 'button';
|
||||||
|
close.title = 'Close tab';
|
||||||
|
close.setAttribute('aria-label', `Close ${tab.title || 'New Tab'}`);
|
||||||
|
close.dataset.tabId = String(tab.id);
|
||||||
|
close.innerHTML = '<i data-lucide="x"></i>';
|
||||||
|
|
||||||
|
button.append(favicon, title, loading, close);
|
||||||
|
tabsElement.insertBefore(button, addButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.lucide) lucide.createIcons({ nodes: [tabsElement] });
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyState(nextState) {
|
||||||
|
Object.assign(state, nextState || {});
|
||||||
|
|
||||||
|
const title = state.title || 'New Tab';
|
||||||
|
const url = state.url || '';
|
||||||
|
const addressInput = document.getElementById('address-input');
|
||||||
|
const backButton = document.getElementById('back-button');
|
||||||
|
const forwardButton = document.getElementById('forward-button');
|
||||||
|
const reloadButton = document.getElementById('reload-button');
|
||||||
|
const progressBar = document.getElementById('progress-bar');
|
||||||
|
|
||||||
|
document.title = `${title} - Nebula`;
|
||||||
|
renderTabs();
|
||||||
|
backButton.disabled = !state.canGoBack;
|
||||||
|
forwardButton.disabled = !state.canGoForward;
|
||||||
|
const reloadIcon = state.isLoading ? 'x' : 'rotate-cw';
|
||||||
|
reloadButton.dataset.command = state.isLoading ? 'stop' : 'reload';
|
||||||
|
reloadButton.innerHTML = `<i data-lucide="${reloadIcon}"></i>`;
|
||||||
|
if (window.lucide) lucide.createIcons({ nodes: [reloadButton] });
|
||||||
|
|
||||||
|
if (document.activeElement !== addressInput) {
|
||||||
|
addressInput.value = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar.style.width = `${Math.max(0, Math.min(1, state.progress || 0)) * 100}%`;
|
||||||
|
progressBar.style.opacity = state.isLoading ? '1' : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireCommands() {
|
||||||
|
document.querySelector('.tabs').addEventListener('click', event => {
|
||||||
|
const close = event.target.closest('.tab-close[data-tab-id]');
|
||||||
|
if (close) {
|
||||||
|
postCommand('close-tab', close.dataset.tabId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tab = event.target.closest('.tab[data-tab-id]');
|
||||||
|
if (tab && !tab.classList.contains('active')) {
|
||||||
|
postCommand('activate-tab', tab.dataset.tabId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector('.tabs').addEventListener('auxclick', event => {
|
||||||
|
if (event.button !== 1) return;
|
||||||
|
|
||||||
|
const tab = event.target.closest('.tab[data-tab-id]');
|
||||||
|
if (tab) {
|
||||||
|
postCommand('close-tab', tab.dataset.tabId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-command]').forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
postCommand(button.dataset.command);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-drag-region]').forEach(region => {
|
||||||
|
region.addEventListener('pointerdown', event => {
|
||||||
|
const interactive = event.target.closest('button, input, .tab, .address-shell');
|
||||||
|
if (event.button === 0 && !interactive) {
|
||||||
|
postCommand('drag');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('address-form').addEventListener('submit', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const input = document.getElementById('address-input');
|
||||||
|
const target = toNavigationUrl(input.value);
|
||||||
|
if (target) {
|
||||||
|
postCommand('navigate', target);
|
||||||
|
input.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.NebulaChrome = { applyState, postCommand, toNavigationUrl };
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
wireCommands();
|
||||||
|
applyState(state);
|
||||||
|
});
|
||||||
+1
-1
@@ -88,7 +88,7 @@ function applySelectedSearchEngine(engine) {
|
|||||||
function normalizeNavigationUrl(input) {
|
function normalizeNavigationUrl(input) {
|
||||||
const value = (input || '').trim();
|
const value = (input || '').trim();
|
||||||
if (!value) return null;
|
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}`;
|
if (value.includes('.') && !/\s/.test(value)) return `https://${value}`;
|
||||||
return `${searchEngines[selectedSearchEngine]}${encodeURIComponent(value)}`;
|
return `${searchEngines[selectedSearchEngine]}${encodeURIComponent(value)}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+12
File diff suppressed because one or more lines are too long
+13
-2
@@ -17,6 +17,17 @@ function applyTheme(theme) {
|
|||||||
setCssVar('--url-bar-border', colors.urlBarBorder, '#3e4652');
|
setCssVar('--url-bar-border', colors.urlBarBorder, '#3e4652');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sendMenuCommand(cmd) {
|
||||||
|
if (window.electronAPI?.send) {
|
||||||
|
window.electronAPI.send('menu-popup-command', { cmd });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.nebulaNative?.postMessage) {
|
||||||
|
window.nebulaNative.postMessage(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshZoom() {
|
async function refreshZoom() {
|
||||||
if (!window.electronAPI?.invoke || !zoomPercentEl) return;
|
if (!window.electronAPI?.invoke || !zoomPercentEl) return;
|
||||||
try {
|
try {
|
||||||
@@ -34,7 +45,7 @@ window.addEventListener('click', (e) => {
|
|||||||
const btn = e.target.closest('button[data-cmd]');
|
const btn = e.target.closest('button[data-cmd]');
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
const cmd = btn.getAttribute('data-cmd');
|
const cmd = btn.getAttribute('data-cmd');
|
||||||
window.electronAPI?.send?.('menu-popup-command', { cmd });
|
sendMenuCommand(cmd);
|
||||||
if (cmd === 'zoom-in' || cmd === 'zoom-out') {
|
if (cmd === 'zoom-in' || cmd === 'zoom-out') {
|
||||||
setTimeout(refreshZoom, 50);
|
setTimeout(refreshZoom, 50);
|
||||||
}
|
}
|
||||||
@@ -42,7 +53,7 @@ window.addEventListener('click', (e) => {
|
|||||||
|
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
window.electronAPI?.send?.('menu-popup-command', { cmd: 'close' });
|
sendMenuCommand('close-menu-popup');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -61,7 +61,7 @@
|
|||||||
const attemptedUrl = params.get('url');
|
const attemptedUrl = params.get('url');
|
||||||
const box = document.getElementById('targetBox');
|
const box = document.getElementById('targetBox');
|
||||||
if (attemptedUrl) {
|
if (attemptedUrl) {
|
||||||
box.textContent = decodeURIComponent(attemptedUrl);
|
box.textContent = attemptedUrl;
|
||||||
} else {
|
} else {
|
||||||
box.textContent = 'Unknown URL';
|
box.textContent = 'Unknown URL';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Nebula Chrome</title>
|
||||||
|
<link rel="stylesheet" href="../css/chrome.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="nebula-chrome" data-drag-region>
|
||||||
|
<div class="title-row" data-drag-region>
|
||||||
|
<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>
|
||||||
|
<span id="tab-title" class="tab-title">New Tab</span>
|
||||||
|
<span id="tab-loading" class="tab-loading" hidden></span>
|
||||||
|
</button>
|
||||||
|
<button class="tab-add" type="button" data-command="new-tab" title="New tab" aria-label="New tab">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="window-controls" aria-label="Window controls">
|
||||||
|
<button type="button" data-command="minimize" aria-label="Minimize">
|
||||||
|
<i data-lucide="minus"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" data-command="maximize" aria-label="Maximize">
|
||||||
|
<i data-lucide="square"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" data-command="close" class="close" aria-label="Close">
|
||||||
|
<i data-lucide="x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<button id="back-button" class="icon-button" type="button" data-command="back" aria-label="Back" disabled>
|
||||||
|
<i data-lucide="chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
<button id="forward-button" class="icon-button" type="button" data-command="forward" aria-label="Forward" disabled>
|
||||||
|
<i data-lucide="chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
<button id="reload-button" class="icon-button" type="button" data-command="reload" aria-label="Reload">
|
||||||
|
<i data-lucide="rotate-cw"></i>
|
||||||
|
</button>
|
||||||
|
<button class="icon-button" type="button" data-command="home" aria-label="Home">
|
||||||
|
<i data-lucide="home"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<form id="address-form" class="address-shell" autocomplete="off">
|
||||||
|
<div id="progress-bar" class="progress-bar"></div>
|
||||||
|
<label class="sr-only" for="address-input">Search or enter address</label>
|
||||||
|
<input id="address-input" type="text" spellcheck="false" placeholder="Search or enter address">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<button class="icon-button" type="button" data-command="settings" aria-label="Settings">
|
||||||
|
<i data-lucide="settings"></i>
|
||||||
|
</button>
|
||||||
|
<button class="icon-button menu-button" type="button" data-command="menu-popup" aria-label="Open menu" title="Open menu">
|
||||||
|
<i data-lucide="menu"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../js/lucide.min.js"></script>
|
||||||
|
<script>lucide.createIcons();</script>
|
||||||
|
<script src="../js/chrome.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+934
-200
File diff suppressed because it is too large
Load Diff
@@ -62,6 +62,10 @@
|
|||||||
const box = document.getElementById('targetBox');
|
const box = document.getElementById('targetBox');
|
||||||
if (target) box.textContent = target;
|
if (target) box.textContent = target;
|
||||||
function sendNavigate(url, opts){
|
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){
|
if (window.electronAPI && window.electronAPI.sendToHost){
|
||||||
window.electronAPI.sendToHost('navigate', url, opts||{});
|
window.electronAPI.sendToHost('navigate', url, opts||{});
|
||||||
} else if (window.parent && window.parent !== window) {
|
} else if (window.parent && window.parent !== window) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
<div id="menu-popup" role="menu">
|
<div id="menu-popup" role="menu">
|
||||||
<button data-cmd="open-settings" role="menuitem">Settings</button>
|
<button data-cmd="open-settings" role="menuitem">Settings</button>
|
||||||
<button data-cmd="big-picture" role="menuitem">🎮 Big Picture Mode</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>
|
<button data-cmd="toggle-devtools" role="menuitem">Toggle Developer Tools</button>
|
||||||
<div class="zoom-controls" role="group" aria-label="Zoom controls">
|
<div class="zoom-controls" role="group" aria-label="Zoom controls">
|
||||||
<button data-cmd="zoom-out" aria-label="Zoom out">-</button>
|
<button data-cmd="zoom-out" aria-label="Zoom out">-</button>
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>Nebot</title>
|
|
||||||
<link rel="stylesheet" href="../plugins/nebot/page.css" onerror="this.remove()"/>
|
|
||||||
<style>
|
|
||||||
body { margin:0; font-family: system-ui,-apple-system,Segoe UI,Roboto,sans-serif; background:#12141c; color:#e6e8ef; }
|
|
||||||
.fallback { max-width:620px; margin:60px auto; padding:32px 36px; background:#1d222e; border:1px solid rgba(255,255,255,0.08); border-radius:18px; }
|
|
||||||
.fallback h1 { margin:0 0 12px; font-size:28px; background:linear-gradient(90deg,#a48bff,#6cb6ff); -webkit-background-clip:text; color:transparent; }
|
|
||||||
.fallback p { line-height:1.55; }
|
|
||||||
.tip { background:#232b38; padding:10px 14px; border-radius:10px; font-size:13px; margin-top:18px; border:1px solid rgba(255,255,255,0.08); }
|
|
||||||
.err { color:#ff6d7d; font-weight:600; }
|
|
||||||
#mount { min-height:400px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="mount"></div>
|
|
||||||
<script>
|
|
||||||
(async function(){
|
|
||||||
// Wait a tick so plugin preload (renderer-preload) runs and exposes window.ollamaChat
|
|
||||||
const mount = document.getElementById('mount');
|
|
||||||
function showFallback(reason){
|
|
||||||
mount.innerHTML = `<div class="fallback">`+
|
|
||||||
`<h1>Nebot</h1>`+
|
|
||||||
`<p>The Nebot plugin page could not load automatically.</p>`+
|
|
||||||
(reason?`<p class='err'>${reason}</p>`:'')+
|
|
||||||
`<div class="tip"><strong>How to fix</strong><br/>1. Ensure the Nebot plugin folder exists at plugins/nebot.<br/>2. Confirm plugin is enabled (manifest enabled: true).<br/>3. Restart the app so the plugin manager registers pages.</div>`+
|
|
||||||
`</div>`;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Try to fetch plugin page HTML directly
|
|
||||||
const res = await fetch('../plugins/nebot/page.html');
|
|
||||||
if(!res.ok){ showFallback('Missing page.html (status '+res.status+').'); return; }
|
|
||||||
const html = await res.text();
|
|
||||||
// Simple sandboxed injection
|
|
||||||
mount.innerHTML = html;
|
|
||||||
// The injected page expects its CSS & JS relative to itself; adjust asset paths
|
|
||||||
const fixLinks = mount.querySelectorAll('link[rel="stylesheet"], script[src]');
|
|
||||||
fixLinks.forEach(el=>{
|
|
||||||
const attr = el.tagName==='SCRIPT'?'src':'href';
|
|
||||||
if(el.getAttribute(attr) && !/plugins\/nebot\//.test(el.getAttribute(attr))){
|
|
||||||
el.setAttribute(attr,'../plugins/nebot/'+el.getAttribute(attr));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Inject JS if not already present
|
|
||||||
if(!mount.querySelector('script[data-nebot-page]')){
|
|
||||||
const s=document.createElement('script'); s.dataset.nebotPage='1';
|
|
||||||
// Pass the current URL hash to the page script for debug mode
|
|
||||||
s.src='../plugins/nebot/page.js' + window.location.hash;
|
|
||||||
mount.appendChild(s);
|
|
||||||
}
|
|
||||||
} catch(e){
|
|
||||||
showFallback(e.message||'Unknown error');
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -293,12 +293,12 @@
|
|||||||
|
|
||||||
<div class="customization-group about-actions">
|
<div class="customization-group about-actions">
|
||||||
<button id="copy-about-btn">Copy diagnostics</button>
|
<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 -->
|
<!-- GitHub mark (Octicons) MIT License -->
|
||||||
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false" role="img">
|
<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"/>
|
<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>
|
</svg>
|
||||||
<span>GitHub</span>
|
<span>Gitpub</span>
|
||||||
</a>
|
</a>
|
||||||
<a id="help-link" href="https://nebula.zambazosmedia.group" class="help-btn" rel="noopener noreferrer">
|
<a id="help-link" href="https://nebula.zambazosmedia.group" class="help-btn" rel="noopener noreferrer">
|
||||||
<!-- Help icon -->
|
<!-- Help icon -->
|
||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user