From 72d3407e723c0349569f8bbd9f20a0c25a37d27f Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 17:23:29 +0100 Subject: [PATCH 01/35] Add EscapeShellArg utility function Introduces EscapeShellArg to safely escape shell arguments by wrapping them in single quotes and handling embedded single quotes. Updates Utils.h to declare the new function. --- src/Utils.cpp | 21 +++++++++++++++++++++ src/Utils.h | 1 + 2 files changed, 22 insertions(+) diff --git a/src/Utils.cpp b/src/Utils.cpp index ae5e462..ce2ccb5 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -43,6 +43,27 @@ std::string EscapeJsStringLiteral(const std::string &input) return EscapeJsonString(input); } +std::string EscapeShellArg(const std::string &input) +{ + // Escape shell special characters for use in shell commands + // This wraps the argument in single quotes and escapes any embedded single quotes + std::string out = "'"; + for (char c : input) + { + if (c == '\'') + { + // End quote, escape the single quote, start quote again + out += "'\\''"; + } + else + { + out += c; + } + } + out += "'"; + return out; +} + std::string ToIso8601UTC(const std::chrono::system_clock::time_point &tp) { std::time_t raw = std::chrono::system_clock::to_time_t(tp); diff --git a/src/Utils.h b/src/Utils.h index 1e0526b..acde9d3 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -8,6 +8,7 @@ namespace util { std::string EscapeJsonString(const std::string &input); std::string EscapeJsStringLiteral(const std::string &input); +std::string EscapeShellArg(const std::string &input); std::string ToIso8601UTC(const std::chrono::system_clock::time_point &tp); std::string ToStdString(const ultralight::String &str); std::string Trim(const std::string &s); From 63ac6362435bfb9b18736d0aeee10cdd85565f0f Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 17:23:43 +0100 Subject: [PATCH 02/35] Remove unnecessary const_cast in password decryption Replaces usage of const_cast for calling Decrypt on encrypted passwords with direct calls to Decrypt. This improves code clarity and avoids unnecessary casting in several methods of PasswordManager. --- src/PasswordManager.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PasswordManager.cpp b/src/PasswordManager.cpp index 9c5e9f8..ace0c31 100644 --- a/src/PasswordManager.cpp +++ b/src/PasswordManager.cpp @@ -453,7 +453,7 @@ std::vector PasswordManager::GetCredentialsForOrigin(const std: if (cred.origin == normalized_origin && !cred.blacklisted) { SavedCredential decrypted = cred; if (!decrypted.encrypted_password.empty() && decrypted.password.empty()) { - decrypted.password = const_cast(this)->Decrypt(decrypted.encrypted_password); + decrypted.password = Decrypt(decrypted.encrypted_password); } result.push_back(decrypted); } @@ -479,7 +479,7 @@ std::vector PasswordManager::GetAllCredentials() const { for (const auto& cred : credentials_) { SavedCredential decrypted = cred; if (!decrypted.encrypted_password.empty() && decrypted.password.empty()) { - decrypted.password = const_cast(this)->Decrypt(decrypted.encrypted_password); + decrypted.password = Decrypt(decrypted.encrypted_password); } result.push_back(decrypted); } @@ -778,7 +778,7 @@ bool PasswordManager::ExportToCSV(const std::filesystem::path& filepath, const s std::string password = cred.password; if (password.empty() && !cred.encrypted_password.empty()) { - password = const_cast(this)->Decrypt(cred.encrypted_password); + password = Decrypt(cred.encrypted_password); } // Escape fields for CSV @@ -880,7 +880,7 @@ bool PasswordManager::ExportToJSON(const std::filesystem::path& filepath) const std::string password = cred.password; if (password.empty() && !cred.encrypted_password.empty()) { - password = const_cast(this)->Decrypt(cred.encrypted_password); + password = Decrypt(cred.encrypted_password); } file << " {\n" @@ -1073,7 +1073,7 @@ PasswordManager::Stats PasswordManager::GetStats() const { // Check for weak passwords std::string password = cred.password; if (password.empty() && !cred.encrypted_password.empty()) { - password = const_cast(this)->Decrypt(cred.encrypted_password); + password = Decrypt(cred.encrypted_password); } auto strength = CheckPasswordStrength(password); From fc9845d7a13bd6bf4f627a1bb818c6213fa1169d Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 17:23:49 +0100 Subject: [PATCH 03/35] Change DRM default to disabled in ResetToDefaults The DRMSettings::ResetToDefaults method now sets 'enabled_' to false by default, requiring users to opt-in. This change improves user control over DRM activation. --- src/drm/DRMSettings.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/drm/DRMSettings.cpp b/src/drm/DRMSettings.cpp index 4745e93..a0a8db9 100644 --- a/src/drm/DRMSettings.cpp +++ b/src/drm/DRMSettings.cpp @@ -174,7 +174,7 @@ namespace drm void DRMSettings::ResetToDefaults() { - enabled_ = true; + enabled_ = false; // Default to disabled - user must opt-in site_rules_.clear(); const std::string catalog_document = LoadCatalogDocument(*this); From 6e7bee2e4b87d6dd69fe2597ca90bbbab814407d Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 17:23:55 +0100 Subject: [PATCH 04/35] Use EscapeShellArg for shell command paths Replaces manual quoting with util::EscapeShellArg for file and folder paths in shell commands on Apple and Linux platforms. This improves security and correctness when opening or revealing downloads with potentially unsafe characters in paths. --- src/DownloadManager.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/DownloadManager.cpp b/src/DownloadManager.cpp index bdd7ff5..02919cd 100644 --- a/src/DownloadManager.cpp +++ b/src/DownloadManager.cpp @@ -370,10 +370,10 @@ bool DownloadManager::OpenDownload(DownloadId id) const auto result = ShellExecuteW(nullptr, L"open", wpath.c_str(), nullptr, nullptr, SW_SHOWNORMAL); return reinterpret_cast(result) > 32; #elif defined(__APPLE__) - std::string cmd = "open \"" + path_str + "\""; + std::string cmd = "open " + util::EscapeShellArg(path_str); return std::system(cmd.c_str()) == 0; #else - std::string cmd = "xdg-open \"" + path_str + "\""; + std::string cmd = "xdg-open " + util::EscapeShellArg(path_str); return std::system(cmd.c_str()) == 0; #endif } @@ -394,10 +394,10 @@ bool DownloadManager::RevealDownload(DownloadId id) const auto result = ShellExecuteW(nullptr, L"open", L"explorer.exe", params.c_str(), nullptr, SW_SHOWNORMAL); return reinterpret_cast(result) > 32; #elif defined(__APPLE__) - std::string cmd = "open -R \"" + rec->second.path.u8string() + "\""; + std::string cmd = "open -R " + util::EscapeShellArg(rec->second.path.u8string()); return std::system(cmd.c_str()) == 0; #else - std::string cmd = "xdg-open \"" + folder_str + "\""; + std::string cmd = "xdg-open " + util::EscapeShellArg(folder_str); return std::system(cmd.c_str()) == 0; #endif } From faec4d2735cf186483cd078aa4b6f2acca6f204d Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 18:00:08 +0100 Subject: [PATCH 05/35] Update UI.cpp --- src/UI.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/UI.cpp b/src/UI.cpp index 2647239..d86ee11 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -2079,10 +2079,10 @@ void UI::OnOpenExtensionsFolder(const JSObject &obj, const JSArgs &args) std::wstring wide_path(ext_dir.begin(), ext_dir.end()); ShellExecuteW(NULL, L"explore", wide_path.c_str(), NULL, NULL, SW_SHOWNORMAL); #elif defined(__APPLE__) - std::string cmd = "open \"" + ext_dir + "\""; + std::string cmd = "open " + util::EscapeShellArg(ext_dir); system(cmd.c_str()); #else - std::string cmd = "xdg-open \"" + ext_dir + "\""; + std::string cmd = "xdg-open " + util::EscapeShellArg(ext_dir); system(cmd.c_str()); #endif } @@ -5081,7 +5081,7 @@ void UI::OnDrmPromptResponse(const JSObject &obj, const JSArgs &args) ultralight::String action_ul = args[0].ToString(); ultralight::String url_ul = args[1].ToString(); - int tab_id_int = args[2].ToInteger(); + int64_t tab_id_int = args[2].ToInteger(); auto action_str = action_ul.utf8(); auto url_str = url_ul.utf8(); From 9711a9e51f1f468bc3b9f3f897f9e2d2896c4eb6 Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 18:05:28 +0100 Subject: [PATCH 06/35] Add file creation checks in CreateExtensionTemplate Added checks to ensure manifest.json and content.js are successfully created in CreateExtensionTemplate. If file creation fails, an error is logged and the function returns false to prevent further issues. --- src/ExtensionManager.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/ExtensionManager.cpp b/src/ExtensionManager.cpp index 0b5f5d1..e9ae57d 100644 --- a/src/ExtensionManager.cpp +++ b/src/ExtensionManager.cpp @@ -451,6 +451,10 @@ bool ExtensionManager::CreateExtensionTemplate(const std::string& name) { // Create manifest.json std::ofstream manifest(ext_dir / "manifest.json"); + if (!manifest.is_open()) { + std::cerr << "[Extensions] Failed to create manifest.json" << std::endl; + return false; + } manifest << "{\n"; manifest << " \"name\": \"" << name << "\",\n"; manifest << " \"version\": \"1.0.0\",\n"; @@ -463,6 +467,10 @@ bool ExtensionManager::CreateExtensionTemplate(const std::string& name) { // Create content.js template std::ofstream content(ext_dir / "content.js"); + if (!content.is_open()) { + std::cerr << "[Extensions] Failed to create content.js" << std::endl; + return false; + } content << "// " << name << " - Content Script\n"; content << "// This script runs on every page that matches your patterns.\n\n"; content << "console.log('[" << name << "] Extension loaded!');\n\n"; From d9c655cf45342f12ecc0dab33f974c094e6616fd Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 18:05:37 +0100 Subject: [PATCH 07/35] Fix file URL detection in OnDOMReady Replaces strncmp with std::string_view logic to more reliably skip internal file:/// URLs when injecting content scripts. --- src/Tab.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Tab.cpp b/src/Tab.cpp index bcf7bd6..d07429d 100644 --- a/src/Tab.cpp +++ b/src/Tab.cpp @@ -631,7 +631,8 @@ void Tab::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const auto url_str = url.utf8(); const char *c = url_str.data(); // Skip internal pages (file:/// URLs) - if (c && std::strncmp(c, "file://", 7) != 0) + std::string_view url_view(c ? c : ""); + if (c && (url_view.size() < 7 || url_view.substr(0, 7) != "file://")) { std::string script_code = extensions::ExtensionManager::Instance().GetContentScriptsForURL(c); if (!script_code.empty()) From f6cc8c98ee266a52662ce017fb0325fc67e575bb Mon Sep 17 00:00:00 2001 From: ovsky Date: Thu, 4 Dec 2025 18:05:48 +0100 Subject: [PATCH 08/35] Refactor URL checks and remove debug logging Replaces strncmp-based URL scheme checks with std::string_view for improved readability and safety. Removes temporary debug logging from OnAddressBarNavigate and adds a file open check when importing passwords. --- src/UI.cpp | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/UI.cpp b/src/UI.cpp index d86ee11..8a2fd21 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -1680,17 +1680,6 @@ void UI::OnRequestChangeURL(const JSObject &obj, const JSArgs &args) void UI::OnAddressBarNavigate(const JSObject &obj, const JSArgs &args) { - // DEBUG: Log that we got here - std::ofstream debug("debug_navigate.txt", std::ios::app); - debug << "OnAddressBarNavigate called with " << args.size() << " args\n"; - if (args.size() > 0) { - ultralight::String url = args[0]; - auto url_data = url.utf8(); - if (url_data.data()) - debug << "URL: " << url_data.data() << "\n"; - } - debug.close(); - if (args.size() == 1) { ultralight::String url = args[0]; @@ -2153,7 +2142,8 @@ void UI::UpdateTabTitle(uint64_t id, const ultralight::String &title) { auto url_u = tabs_[id]->view()->url().utf8(); const char *cur = url_u.data(); - if (cur && strncmp(cur, "file://", 7) == 0) + std::string_view cur_view(cur ? cur : ""); + if (cur && cur_view.size() >= 7 && cur_view.substr(0, 7) == "file://") { updateURL({title}); } @@ -2257,7 +2247,10 @@ String UI::GetFaviconURL(const String &page_url) if (!url) return String(""); - if (strncmp(url, "http://", 7) != 0 && strncmp(url, "https://", 8) != 0) + std::string_view url_view(url); + if (url_view.size() < 7 || + (url_view.substr(0, 7) != "http://" && + (url_view.size() < 8 || url_view.substr(0, 8) != "https://"))) return String(""); const char *scheme_sep = strstr(url, "://"); @@ -2297,7 +2290,10 @@ void UI::RecordHistory(const String &url, const String &title) return; // Only record http(s) - if (strncmp(c_url, "http://", 7) != 0 && strncmp(c_url, "https://", 8) != 0) + std::string_view url_view(c_url); + if (url_view.size() < 7 || + (url_view.substr(0, 7) != "http://" && + (url_view.size() < 8 || url_view.substr(0, 8) != "https://"))) return; // Basic cap to avoid unbounded growth later (we'll prune oldest when exceeding) @@ -4967,6 +4963,8 @@ void UI::OnImportPasswords(const JSObject &obj, const JSArgs &args) std::filesystem::path temp_path = SettingsDirectory() / ("temp_import." + format); { std::ofstream out(temp_path, std::ios::binary); + if (!out.is_open()) + return; out << content; } From 946ad5313a621ae28f27919fa0708343f3967f4f Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 09:36:44 +0100 Subject: [PATCH 09/35] Revamp downloads page with dark purple theme Updated the downloads page styles to use a dark purple theme consistent with Passwords & Settings. Improved button, card, and status styling, enhanced empty state messaging, and added custom scrollbar styles for a more modern UI. --- assets/downloads.html | 237 ++++++++++++++++++++++++++++++------------ 1 file changed, 171 insertions(+), 66 deletions(-) diff --git a/assets/downloads.html b/assets/downloads.html index 2a19670..0180c28 100644 --- a/assets/downloads.html +++ b/assets/downloads.html @@ -9,10 +9,27 @@ @@ -188,7 +290,10 @@

Downloads

-
No downloads yet.
+
@@ -237,7 +342,7 @@

Downloads

const meta = document.createElement('div'); meta.className = 'meta'; const status = document.createElement('span'); - status.className = 'status'; + status.className = 'status ' + (item.status || 'pending').toLowerCase().replace(/[^a-z-]/g, ''); status.textContent = item.status || 'pending'; meta.appendChild(status); From b71dd5a99e12843e0b274be8d87e5d3dee00fe0f Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 09:36:50 +0100 Subject: [PATCH 10/35] Update extensions page to dark purple theme Refactored CSS variables and styles in extensions.html to use a dark purple color scheme, matching the Passwords & Settings pages. Improved button, card, modal, and footer styling for better visual consistency and modern appearance. --- assets/extensions.html | 188 ++++++++++++++++++++++------------------- 1 file changed, 101 insertions(+), 87 deletions(-) diff --git a/assets/extensions.html b/assets/extensions.html index 0e9d215..a91b933 100644 --- a/assets/extensions.html +++ b/assets/extensions.html @@ -13,78 +13,87 @@ } :root { - color-scheme: light dark; - --bg-primary: #1e1e1e; - --bg-secondary: #252525; - --bg-tertiary: #2a2a2a; - --bg-elevated: #2d2d2d; - - --border-subtle: rgba(255, 255, 255, 0.06); - --border-soft: rgba(255, 255, 255, 0.08); - --border-medium: rgba(255, 255, 255, 0.12); - - --text-primary: #e4e4e7; - --text-secondary: #a1a1aa; - --text-tertiary: #71717a; - - --accent-primary: #8b7cf5; - --accent-soft: rgba(139, 124, 245, 0.15); - --accent-softer: rgba(139, 124, 245, 0.08); - - --success: #22c55e; - --success-soft: rgba(34, 197, 94, 0.15); - --danger: #ef4444; - --danger-soft: rgba(239, 68, 68, 0.15); - --warning: #f59e0b; - --warning-soft: rgba(245, 158, 11, 0.15); + /* Dark purple theme matching Passwords & Settings */ + --bg-primary: #1e1e2e; + --bg-secondary: #282839; + --bg-tertiary: #32324a; + --bg-elevated: #3d3d5c; + --bg-hover: #3d3d5c; + + --border-color: #404060; + --border-subtle: rgba(64, 64, 96, 0.5); + --border-soft: rgba(64, 64, 96, 0.7); + --border-medium: var(--border-color); + + --text-primary: #e4e4ef; + --text-secondary: #9999b3; + --text-tertiary: #71718a; + + --accent-color: #7c6aef; + --accent-hover: #9182f3; + --accent-primary: #7c6aef; + --accent-light: rgba(124, 106, 239, 0.15); + --accent-soft: rgba(124, 106, 239, 0.15); + --accent-softer: rgba(124, 106, 239, 0.08); + + --success: #6aef8a; + --success-soft: rgba(106, 239, 138, 0.15); + --danger: #ef6a6a; + --danger-color: #ef6a6a; + --danger-soft: rgba(239, 106, 106, 0.15); + --warning: #efcf6a; + --warning-color: #efcf6a; + --warning-soft: rgba(239, 207, 106, 0.15); + + --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); } body { - font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji"; - background: var(--bg-primary); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; + background: linear-gradient(135deg, var(--bg-primary) 0%, #252540 100%); color: var(--text-primary); - padding: 40px 20px; - line-height: 1.6; + padding: 24px; + line-height: 1.5; min-height: 100vh; } .container { max-width: 900px; margin: 0 auto; - background: var(--bg-secondary); - border: 1px solid var(--border-soft); - border-radius: 12px; - padding: 0; - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3); - overflow: hidden; } header { - padding: 20px 30px; - border-bottom: 1px solid var(--border-subtle); - background: var(--bg-tertiary); + display: flex; + flex-direction: column; + margin-bottom: 24px; + padding-bottom: 16px; + border-bottom: 1px solid var(--border-color); } h1 { - color: var(--text-primary); - margin: 0 0 6px 0; - font-size: 24px; - font-weight: 600; - letter-spacing: 0.01em; display: flex; align-items: center; gap: 12px; + font-size: 24px; + font-weight: 600; + margin-bottom: 4px; + color: var(--text-primary); } h1 .icon { - font-size: 28px; + font-size: 32px; + } + + h1 svg { + width: 32px; + height: 32px; + fill: var(--accent-color); } .subtitle { color: var(--text-secondary); + font-size: 14px; margin: 0; - font-size: 13px; - font-weight: 400; } .header-actions { @@ -123,7 +132,6 @@ /* Extensions container */ #extensions-container { - padding: 20px 30px 30px 30px; max-height: calc(100vh - 280px); overflow-y: auto; } @@ -154,18 +162,19 @@ /* Extension card */ .extension-card { - background: var(--bg-tertiary); - border: 1px solid var(--border-subtle); - border-radius: 10px; - margin-bottom: 12px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + margin-bottom: 16px; overflow: hidden; transition: all 0.2s; + box-shadow: var(--card-shadow); } .extension-card:hover { - border-color: var(--border-soft); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + border-color: var(--accent-color); + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(124, 106, 239, 0.2); } .extension-card.disabled { @@ -355,40 +364,39 @@ /* Buttons */ button { - padding: 10px 20px; - border: 1px solid rgba(255, 255, 255, 0.15); - border-radius: 7px; - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: all 0.2s; - font-family: inherit; display: inline-flex; align-items: center; gap: 6px; + padding: 8px 16px; + border: none; + border-radius: 6px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + font-family: inherit; } .btn-primary { - background: var(--accent-soft); - color: var(--accent-primary); - border-color: var(--accent-primary); + background: var(--accent-color); + color: white; } .btn-primary:hover:not(:disabled) { - background: var(--accent-primary); - color: #ffffff; + background: var(--accent-hover); transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(139, 124, 245, 0.3); + box-shadow: 0 4px 12px rgba(124, 106, 239, 0.4); } .btn-secondary { - background: transparent; - color: var(--text-secondary); + background: var(--bg-tertiary); + color: var(--text-primary); + border: 1px solid var(--border-color); } .btn-secondary:hover { - background: var(--bg-elevated); - border-color: var(--border-medium); + background: var(--bg-hover); + border-color: var(--accent-color); } .btn-small { @@ -461,7 +469,7 @@ .modal { background: var(--bg-secondary); - border: 1px solid var(--border-soft); + border: 1px solid var(--border-color); border-radius: 12px; max-width: 500px; width: 90%; @@ -469,6 +477,7 @@ overflow: hidden; transform: scale(0.95); transition: transform 0.2s; + box-shadow: var(--card-shadow); } .modal-overlay.visible .modal { @@ -477,10 +486,11 @@ .modal-header { padding: 20px; - border-bottom: 1px solid var(--border-subtle); + border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; + background: var(--bg-tertiary); } .modal-header h2 { @@ -523,12 +533,12 @@ .form-group input, .form-group textarea { width: 100%; - padding: 10px 12px; + padding: 12px 16px; background: var(--bg-tertiary); - border: 1px solid var(--border-soft); - border-radius: 6px; + border: 1px solid var(--border-color); + border-radius: 8px; color: var(--text-primary); - font-size: 13px; + font-size: 14px; font-family: inherit; } @@ -542,8 +552,8 @@ .form-group input:focus, .form-group textarea:focus { outline: none; - border-color: var(--accent-primary); - box-shadow: 0 0 0 3px var(--accent-softer); + border-color: var(--accent-color); + box-shadow: 0 0 0 3px rgba(124, 106, 239, 0.2); } .form-group .hint { @@ -554,19 +564,22 @@ .modal-footer { padding: 16px 20px; - border-top: 1px solid var(--border-subtle); + border-top: 1px solid var(--border-color); display: flex; justify-content: flex-end; gap: 10px; + background: var(--bg-tertiary); } /* Footer */ .footer-info { - padding: 16px 30px; - border-top: 1px solid var(--border-subtle); - background: var(--bg-tertiary); - font-size: 11px; - color: var(--text-tertiary); + margin-top: 24px; + padding: 16px; + border-top: 1px solid var(--border-color); + background: var(--bg-secondary); + border-radius: 8px; + font-size: 12px; + color: var(--text-secondary); display: flex; justify-content: space-between; align-items: center; @@ -574,9 +587,10 @@ .footer-info code { font-family: 'Consolas', 'Courier New', monospace; - background: var(--bg-secondary); - padding: 2px 6px; + background: var(--bg-tertiary); + padding: 4px 8px; border-radius: 4px; + color: var(--text-primary); } /* Loading state */ From 5f4b24c39f8304f9ffc76fdbc17f7d55d6998b6d Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 09:41:48 +0100 Subject: [PATCH 11/35] Update window to create custom extension Updated the .modal-body CSS to include a max-height and vertical scrolling. This ensures modal content remains accessible and scrollable when it exceeds the viewport. --- assets/extensions.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/assets/extensions.html b/assets/extensions.html index a91b933..8bb7857 100644 --- a/assets/extensions.html +++ b/assets/extensions.html @@ -514,6 +514,8 @@ .modal-body { padding: 20px; + max-height: calc(80vh - 160px); + overflow-y: auto; } .form-group { From 37fff9f746c51d3f7a37a1085cac2389de0857c3 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 09:44:26 +0100 Subject: [PATCH 12/35] Add Ctrl+P shortcut for Passwords menu item Updated the menu to display the Ctrl+P shortcut for the Passwords item and added the corresponding shortcut mapping in shortcuts.json. --- assets/menu.html | 2 +- assets/shortcuts.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/menu.html b/assets/menu.html index 2d5af00..b4cc7c9 100644 --- a/assets/menu.html +++ b/assets/menu.html @@ -69,7 +69,7 @@ - + diff --git a/assets/shortcuts.json b/assets/shortcuts.json index 44a1508..45a8d74 100644 --- a/assets/shortcuts.json +++ b/assets/shortcuts.json @@ -4,6 +4,7 @@ "Ctrl+H": "open-history", "Ctrl+J": "open-downloads", "Ctrl+Shift+E": "open-extensions", + "Ctrl+P": "open-passwords", "Ctrl+L": "focus-address", "Ctrl+,": "open-settings" } From 8df0f3db6bb280214665ebd6bb8823e3f4350b94 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 09:54:59 +0100 Subject: [PATCH 13/35] Update extensions shortcut to Ctrl+E Changed the keyboard shortcut for opening extensions from Ctrl+Shift+E to Ctrl+E in both the menu and shortcuts configuration for consistency and easier access. --- assets/menu.html | 2 +- assets/shortcuts.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/menu.html b/assets/menu.html index b4cc7c9..e63ec72 100644 --- a/assets/menu.html +++ b/assets/menu.html @@ -70,7 +70,7 @@ - + diff --git a/assets/shortcuts.json b/assets/shortcuts.json index 45a8d74..bde2b89 100644 --- a/assets/shortcuts.json +++ b/assets/shortcuts.json @@ -3,7 +3,7 @@ "Ctrl+W": "close-tab", "Ctrl+H": "open-history", "Ctrl+J": "open-downloads", - "Ctrl+Shift+E": "open-extensions", + "Ctrl+E": "open-extensions", "Ctrl+P": "open-passwords", "Ctrl+L": "focus-address", "Ctrl+,": "open-settings" From 6a3e3b8b179bd6d131518070bad096170566d016 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 09:57:06 +0100 Subject: [PATCH 14/35] Improve menu shortcut badge styling and layout Menu items now display keyboard shortcuts in separate, styled badges for better readability and visual consistency. Updated HTML structure and added CSS for the new shortcut-badge element. --- assets/menu.html | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/assets/menu.html b/assets/menu.html index e63ec72..68e184f 100644 --- a/assets/menu.html +++ b/assets/menu.html @@ -46,6 +46,17 @@ font-size: 12px; color: rgba(226, 225, 234, 0.7); } + + .menu-item .shortcut-badge { + margin-left: auto; + padding: 2px 8px; + font-size: 10px; + font-weight: 500; + color: rgba(226, 225, 234, 0.5); + background: rgba(255, 255, 255, 0.05); + border-radius: 4px; + font-family: 'Consolas', 'Courier New', monospace; + } @keyframes ul_fade_in { from { opacity: 0; } to { opacity: 1; } } @-webkit-keyframes ul_fade_in { from { opacity: 0; } to { opacity: 1; } } @@ -54,8 +65,14 @@ From 82e88c6d30e844e729d912a1e42d936eec18f9d1 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 10:03:27 +0100 Subject: [PATCH 15/35] Call OnRequestNewWindow for 'new-window' menu action Replaces the placeholder call to OnRequestNewTab with OnRequestNewWindow when the 'new-window' menu item is selected, enabling proper handling of new window requests. --- assets/menu.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/assets/menu.html b/assets/menu.html index 68e184f..563e642 100644 --- a/assets/menu.html +++ b/assets/menu.html @@ -159,8 +159,7 @@ if (window.OnRequestNewTab) OnRequestNewTab(); break; case 'new-window': - // Placeholder: open a new tab as "new window" is not implemented - if (window.OnRequestNewTab) OnRequestNewTab(); + if (window.OnRequestNewWindow) OnRequestNewWindow(); break; case 'toggle-dark-mode': if (window.OnToggleDarkMode) { From 5cb478c3756b52173160a6dbe8f76ba78db6695d Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 10:03:34 +0100 Subject: [PATCH 16/35] Add shortcut for opening new window Added 'Ctrl+N' to shortcuts.json to support opening a new window. --- assets/shortcuts.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/shortcuts.json b/assets/shortcuts.json index bde2b89..8427771 100644 --- a/assets/shortcuts.json +++ b/assets/shortcuts.json @@ -1,5 +1,6 @@ { "Ctrl+T": "new-tab", + "Ctrl+N": "new-window", "Ctrl+W": "close-tab", "Ctrl+H": "open-history", "Ctrl+J": "open-downloads", From ba95cbddfe431df53632843aa4ab4cc257838908 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 10:03:43 +0100 Subject: [PATCH 17/35] Add support for opening new windows and passwords tab Implemented 'new-window' and 'open-passwords' actions in RunShortcutAction. Added OnRequestNewWindow handler to launch a new application instance on both Windows and non-Windows platforms. Exposed OnRequestNewWindow to JavaScript. --- src/UI.cpp | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/UI.cpp b/src/UI.cpp index 8a2fd21..3229094 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -1015,6 +1015,11 @@ bool UI::RunShortcutAction(const std::string &action) CreateNewTab(); return true; } + if (action == "new-window") + { + OnRequestNewWindow({}, {}); + return true; + } if (action == "close-tab") { if (active_tab()) @@ -1063,6 +1068,17 @@ bool UI::RunShortcutAction(const std::string &action) } return false; } + if (action == "open-passwords") + { + // Open Passwords in a new tab + RefPtr child = CreateNewTabForChildView(String("file:///passwords.html")); + if (child) + { + child->LoadURL("file:///passwords.html"); + return true; + } + return false; + } if (action == "open-settings") { // Open Settings in a NEW tab (like Ctrl+H opens history) @@ -1369,6 +1385,7 @@ void UI::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const S } } global["OnRequestNewTab"] = BindJSCallback(&UI::OnRequestNewTab); + global["OnRequestNewWindow"] = BindJSCallback(&UI::OnRequestNewWindow); global["OnRequestTabClose"] = BindJSCallback(&UI::OnRequestTabClose); global["OnActiveTabChange"] = BindJSCallback(&UI::OnActiveTabChange); global["OnRequestChangeURL"] = BindJSCallback(&UI::OnRequestChangeURL); @@ -1549,6 +1566,32 @@ void UI::OnRequestNewTab(const JSObject &obj, const JSArgs &args) CreateNewTab(); } +void UI::OnRequestNewWindow(const JSObject &obj, const JSArgs &args) +{ +#if defined(_WIN32) + // Get the executable path + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + + // Launch new instance + STARTUPINFOW si = {sizeof(si)}; + PROCESS_INFORMATION pi; + if (CreateProcessW(exePath, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) + { + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + } +#else + // For non-Windows platforms, spawn a new process + std::string exePath = std::filesystem::read_symlink("/proc/self/exe").string(); + if (fork() == 0) + { + execl(exePath.c_str(), exePath.c_str(), nullptr); + exit(0); + } +#endif +} + void UI::OnRequestTabClose(const JSObject &obj, const JSArgs &args) { if (args.size() == 1) From 2de60b89ad61a57ce40d82af48ba849e4c3ced33 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 10:03:48 +0100 Subject: [PATCH 18/35] Add OnRequestNewWindow handler to UI class Introduces the OnRequestNewWindow method to handle new window requests in the UI class interface. --- src/UI.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/UI.h b/src/UI.h index b8ce585..fc3a975 100644 --- a/src/UI.h +++ b/src/UI.h @@ -125,6 +125,7 @@ class UI : public WindowListener, void OnStop(const JSObject &obj, const JSArgs &args); void OnToggleTools(const JSObject &obj, const JSArgs &args); void OnRequestNewTab(const JSObject &obj, const JSArgs &args); + void OnRequestNewWindow(const JSObject &obj, const JSArgs &args); void OnRequestTabClose(const JSObject &obj, const JSArgs &args); void OnActiveTabChange(const JSObject &obj, const JSArgs &args); void OnRequestChangeURL(const JSObject &obj, const JSArgs &args); From e2e93db2ad57ed26fde548f58a2ea6d6d9d979b1 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 10:10:35 +0100 Subject: [PATCH 19/35] Skip dark mode for internal browser pages Added a check to prevent dark mode styling from being applied to internal browser pages such as settings, passwords, extensions, and others, as these pages have their own dark styling. --- src/UI.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/UI.cpp b/src/UI.cpp index 3229094..2e9a123 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -3724,6 +3724,27 @@ void UI::ApplyDarkModeToView(RefPtr v) return; const char *js = R"JS((function(){ try{ + // Don't apply dark mode to browser internal pages (they have their own dark styling) + var url = window.location.href; + if(url.startsWith('file:///') && + (url.includes('settings.html') || + url.includes('passwords.html') || + url.includes('extensions.html') || + url.includes('downloads.html') || + url.includes('history.html') || + url.includes('ui.html') || + url.includes('menu.html') || + url.includes('contextmenu.html') || + url.includes('suggestions.html') || + url.includes('quick-inspector.html') || + url.includes('downloads-panel.html') || + url.includes('about.html') || + url.includes('new_tab_page.html') || + url.includes('release_notes.html') || + url.includes('static-sties/'))){ + return false; // Skip dark mode for these pages + } + var sid='__ul_auto_dark'; var prev=document.getElementById(sid); if(prev) prev.remove(); From 661d554962eeb85acd6e3934937c85d9c6fc8310 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 12:22:59 +0100 Subject: [PATCH 20/35] Add Dark Theme Excluded Sites settings section Introduces a new UI section in settings for specifying URL patterns where the dark theme should be disabled. Includes related styles, textarea input, help text, and JavaScript logic for hydrating, tracking, and saving the excluded sites list. --- assets/settings.html | 88 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/assets/settings.html b/assets/settings.html index 7fac84e..34cb9c9 100644 --- a/assets/settings.html +++ b/assets/settings.html @@ -443,6 +443,46 @@ cursor: default; } + /* Dark Theme Excluded Sites styles */ + .dark-sites-row { + margin: 32px 0 24px; + padding: 20px; + border-radius: 12px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + } + + .dark-sites-label { + font-size: 14px; + font-weight: 600; + margin-bottom: 12px; + color: var(--text-primary); + } + + .dark-sites-textarea { + width: 100%; + min-height: 150px; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid var(--border-color); + background: var(--bg-tertiary); + color: var(--text-primary); + font-size: 12px; + font-family: 'Consolas', 'Courier New', monospace; + resize: vertical; + } + + .dark-sites-textarea:focus { + outline: none; + border-color: var(--accent-color); + } + + .dark-sites-help { + font-size: 12px; + color: var(--text-tertiary); + margin-top: 8px; + } + /* Scrollbar styling */ ::-webkit-scrollbar { width: 8px; @@ -513,6 +553,12 @@

DRM WebView

Requesting DRM status...
+ +
+
Dark Theme Excluded Sites
+ +
Enter URL patterns for sites where dark theme should be disabled (one per line). Supports wildcards (*) and comments starting with #. Browser internal pages (settings, history, etc.) are automatically excluded.
+
Target User Agent String
@@ -619,6 +665,7 @@

DRM WebView

let currentSettings = {}, originalSettings = {}, isDirty = false; let targetUserAgent = '', originalTargetUserAgent = ''; + let darkThemeExcludedSites = '', originalDarkThemeExcludedSites = ''; const drmElements = {}; let drmStatusSnapshot = null; @@ -676,6 +723,14 @@

DRM WebView

targetUserAgent = ''; originalTargetUserAgent = ''; } + // Hydrate dark theme excluded sites from payload + if (typeof data.dark_theme_excluded_sites === 'string') { + darkThemeExcludedSites = data.dark_theme_excluded_sites; + originalDarkThemeExcludedSites = data.dark_theme_excluded_sites; + } else { + darkThemeExcludedSites = ''; + originalDarkThemeExcludedSites = ''; + } if (data.meta && data.meta.storage_path) { document.getElementById('storage-path').textContent = 'Storage: ' + data.meta.storage_path; } @@ -689,6 +744,7 @@

DRM WebView

} renderSettings(); hydrateUserAgentEditor(); + hydrateDarkThemeExcludedSitesEditor(); updateDirtyState(); loadDrmStatus(true); } @@ -782,6 +838,9 @@

DRM WebView

if (!isDirty && targetUserAgent !== originalTargetUserAgent) { isDirty = true; } + if (!isDirty && darkThemeExcludedSites !== originalDarkThemeExcludedSites) { + isDirty = true; + } document.getElementById('btn-save').disabled = !isDirty; } @@ -790,6 +849,8 @@

DRM WebView

if (typeof window.OnSaveSettings === 'function') { window.OnSaveSettings(); originalSettings = { ...currentSettings }; + originalTargetUserAgent = targetUserAgent; + originalDarkThemeExcludedSites = darkThemeExcludedSites; isDirty = false; document.getElementById('btn-save').disabled = true; log('Saved'); @@ -812,8 +873,16 @@

DRM WebView

targetUserAgent = ''; originalTargetUserAgent = ''; } + if (typeof data.dark_theme_excluded_sites === 'string') { + darkThemeExcludedSites = data.dark_theme_excluded_sites; + originalDarkThemeExcludedSites = data.dark_theme_excluded_sites; + } else { + darkThemeExcludedSites = ''; + originalDarkThemeExcludedSites = ''; + } renderSettings(); hydrateUserAgentEditor(); + hydrateDarkThemeExcludedSitesEditor(); updateDirtyState(); loadDrmStatus(true); log('Restored'); @@ -844,6 +913,25 @@

DRM WebView

} }; } + function hydrateDarkThemeExcludedSitesEditor() { + const sitesTextarea = document.getElementById('dark-sites-textarea'); + if (!sitesTextarea) return; + sitesTextarea.value = darkThemeExcludedSites || ''; + // Ensure we don't attach multiple listeners on re-render. + sitesTextarea.oninput = null; + sitesTextarea.oninput = () => { + const newVal = sitesTextarea.value || ''; + darkThemeExcludedSites = newVal; + if (typeof window.OnUpdateSetting === 'function') { + window.OnUpdateSetting('dark_theme_excluded_sites', newVal); + } + updateDirtyState(); + const autoSave = !!currentSettings.auto_save_settings; + if (autoSave && typeof window.OnSaveSettings === 'function') { + window.OnSaveSettings(); + } + }; + } // Search helpers function applySettingsSearch(term) { From 3796db596ae6e592d63b5881fd39fa2a92928aeb Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 12:23:24 +0100 Subject: [PATCH 21/35] Add new appearance, privacy, and developer settings Introduces several new settings to settings_catalog.json, including options for dark theme exclusions, vibrant window theme, compact tabs, accessibility features, privacy controls (JavaScript, web security, cookies, Do Not Track, custom user agent), download preferences, performance options (smooth scrolling, hardware acceleration, storage), and developer tools (remote inspector, performance overlay). These additions enhance user customization, privacy, accessibility, and developer support. --- assets/settings_catalog.json | 179 +++++++++++++++++++++++++++-------- 1 file changed, 142 insertions(+), 37 deletions(-) diff --git a/assets/settings_catalog.json b/assets/settings_catalog.json index 895218e..3e0c7be 100644 --- a/assets/settings_catalog.json +++ b/assets/settings_catalog.json @@ -7,6 +7,63 @@ "note": null, "default": false }, + { + "key": "dark_theme_excluded_sites", + "name": "Dark theme excluded sites", + "description": "List of URL patterns where dark theme should NOT be applied (one pattern per line). Browser internal pages are automatically excluded.", + "category": "appearance", + "note": "Supports wildcards: *.example.com, *://example.com/*", + "default": "", + "type": "text" + }, + { + "key": "vibrant_window_theme", + "name": "Vibrant window theme", + "description": "Apply a subtle color wash to the window frame for a livelier finish.", + "category": "appearance", + "note": null, + "default": false + }, + { + "key": "experimental_transparent_toolbar", + "name": "Transparent toolbar", + "description": "Blend the toolbar into page content with a translucent, glass-like surface.", + "category": "appearance", + "note": "Experimental", + "default": false + }, + { + "key": "experimental_compact_tabs", + "name": "Compact tabs", + "description": "Reduce tab height and spacing so more tabs stay visible without scrolling.", + "category": "appearance", + "note": "Experimental", + "default": false + }, + { + "key": "reduce_motion", + "name": "Reduce motion effects", + "description": "Limit animated transitions and parallax flourishes for a calmer experience.", + "category": "accessibility", + "note": null, + "default": false + }, + { + "key": "high_contrast_ui", + "name": "High contrast UI", + "description": "Boost contrast for overlays, menus, and dialogs to improve readability.", + "category": "accessibility", + "note": null, + "default": false + }, + { + "key": "enable_caret_browsing", + "name": "Enable caret browsing", + "description": "Navigate web pages using keyboard cursor like in a text editor.", + "category": "accessibility", + "note": null, + "default": false + }, { "key": "enable_adblock", "name": "Enable ad blocking", @@ -23,6 +80,54 @@ "note": null, "default": false }, + { + "key": "clear_history_on_exit", + "name": "Clear history on exit", + "description": "Remove browsing history when Ultralight closes and skip saving new visits.", + "category": "privacy", + "note": null, + "default": false + }, + { + "key": "enable_javascript", + "name": "Enable JavaScript", + "description": "Allow websites to run JavaScript code for interactive features and dynamic content.", + "category": "privacy", + "note": null, + "default": true + }, + { + "key": "enable_web_security", + "name": "Enable web security", + "description": "Enforce same-origin policy and other web security restrictions.", + "category": "privacy", + "note": null, + "default": true + }, + { + "key": "block_third_party_cookies", + "name": "Block third-party cookies", + "description": "Prevent websites from setting cookies that track you across different sites.", + "category": "privacy", + "note": null, + "default": false + }, + { + "key": "do_not_track", + "name": "Send Do Not Track header", + "description": "Request that websites not track your browsing activity.", + "category": "privacy", + "note": null, + "default": true + }, + { + "key": "use_custom_user_agent", + "name": "Use custom user agent", + "description": "When enabled, send a user agent string that you specify instead of the automatic Chromium-like default.", + "category": "privacy", + "note": null, + "default": false + }, { "key": "enable_suggestions", "name": "Show address bar suggestions", @@ -56,52 +161,44 @@ "default": true }, { - "key": "clear_history_on_exit", - "name": "Clear history on exit", - "description": "Remove browsing history when Ultralight closes and skip saving new visits.", - "category": "privacy", + "key": "ask_download_location", + "name": "Ask where to save downloads", + "description": "Show a file picker dialog for each download instead of using default location.", + "category": "downloads", "note": null, "default": false }, { - "key": "experimental_transparent_toolbar", - "name": "Transparent toolbar", - "description": "Blend the toolbar into page content with a translucent, glass-like surface.", - "category": "appearance", - "note": "Experimental", - "default": false - }, - { - "key": "experimental_compact_tabs", - "name": "Compact tabs", - "description": "Reduce tab height and spacing so more tabs stay visible without scrolling.", - "category": "appearance", - "note": "Experimental", - "default": false + "key": "smooth_scrolling", + "name": "Smooth scrolling", + "description": "Enable smooth animated scrolling for a more fluid browsing experience.", + "category": "performance", + "note": null, + "default": true }, { - "key": "reduce_motion", - "name": "Reduce motion effects", - "description": "Limit animated transitions and parallax flourishes for a calmer experience.", - "category": "accessibility", + "key": "hardware_acceleration", + "name": "Hardware acceleration", + "description": "Use GPU to accelerate graphics rendering for better performance.", + "category": "performance", "note": null, - "default": false + "default": true }, { - "key": "high_contrast_ui", - "name": "High contrast UI", - "description": "Boost contrast for overlays, menus, and dialogs to improve readability.", - "category": "accessibility", + "key": "enable_local_storage", + "name": "Enable local storage", + "description": "Allow websites to store data locally for offline functionality.", + "category": "performance", "note": null, - "default": false + "default": true }, { - "key": "vibrant_window_theme", - "name": "Vibrant window theme", - "description": "Apply a subtle color wash to the window frame for a livelier finish.", - "category": "appearance", + "key": "enable_database", + "name": "Enable database storage", + "description": "Allow websites to use IndexedDB and Web SQL for data storage.", + "category": "performance", "note": null, - "default": false + "default": true }, { "key": "enable_drm_webview", @@ -112,10 +209,18 @@ "default": true }, { - "key": "use_custom_user_agent", - "name": "Use custom user agent", - "description": "When enabled, send a user agent string that you specify instead of the automatic Chromium-like default.", - "category": "privacy", + "key": "enable_remote_inspector", + "name": "Enable remote inspector", + "description": "Allow remote debugging via Chrome DevTools Protocol.", + "category": "developer", + "note": null, + "default": false + }, + { + "key": "show_performance_overlay", + "name": "Show performance overlay", + "description": "Display FPS counter and rendering statistics on screen.", + "category": "developer", "note": null, "default": false } From ca6b30f342087410a7fa201ed1d1e89288e3c7c8 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 12:23:33 +0100 Subject: [PATCH 22/35] Add dark_theme_excluded_sites to settings load/save Introduces support for the dark_theme_excluded_sites setting in both loading and saving user settings, ensuring this value is persisted and restored alongside other settings. --- src/Settings.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Settings.cpp b/src/Settings.cpp index 9dbb0d8..eeb9a82 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -146,6 +146,7 @@ bool SettingsManager::LoadSettingsFromDisk(UI &ui) } // Parse string settings ui.settings_.custom_user_agent = ParseStringLenient(content, "custom_user_agent", ""); + ui.settings_.dark_theme_excluded_sites = ParseStringLenient(content, "dark_theme_excluded_sites", ""); ui.settings_storage_path_ = (migrated ? legacy_path.string() : primary_path.string()); } else @@ -180,6 +181,7 @@ bool SettingsManager::SaveSettingsToDisk(UI &ui) doc << "{\n"; doc << " \"values\": " << ui.BuildSettingsJSON() << ",\n"; doc << " \"custom_user_agent\": \"" << util::EscapeJsonString(ui.settings_.custom_user_agent) << "\",\n"; + doc << " \"dark_theme_excluded_sites\": \"" << util::EscapeJsonString(ui.settings_.dark_theme_excluded_sites) << "\",\n"; doc << " \"meta\": {\n"; doc << " \"updated_at\": \"" << util::ToIso8601UTC(std::chrono::system_clock::now()) << "\",\n"; doc << " \"dirty\": false,\n"; From 2095f971253cefdebc239d9aad7d0435b2296d6d Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 12:23:40 +0100 Subject: [PATCH 23/35] Add support for dark theme excluded sites Introduces a new setting, dark_theme_excluded_sites, allowing users to specify newline-separated URL patterns where dark mode should not be applied. Updates the settings payload, UI handling, and dark mode application logic to respect these exclusions. --- src/UI.cpp | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++- src/UI.h | 1 + 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/UI.cpp b/src/UI.cpp index 2e9a123..094f339 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -2933,6 +2933,22 @@ void UI::OnUpdateSetting(const JSObject &, const JSArgs &args) UpdateSettingsDirtyFlag(); return; } + + // Special-case: dark_theme_excluded_sites is a string value containing + // newline-separated URL patterns for sites where dark theme should be disabled. + if (key == "dark_theme_excluded_sites") + { + if (!args[1].IsString()) + return; + ultralight::String sites_ul = args[1].ToString(); + auto sites_str = sites_ul.utf8(); + std::string sites = sites_str.data() ? sites_str.data() : ""; + settings_.dark_theme_excluded_sites = sites; + UpdateSettingsDirtyFlag(); + ApplySettings(false, false); + UpdateSettingsDirtyFlag(); + return; + } bool value = false; if (args[1].IsBoolean()) { @@ -3331,6 +3347,8 @@ std::string UI::BuildSettingsPayload(bool snapshot_is_baseline) const // Settings page can always display the UA that will actually be used. // Also expose the raw custom_user_agent for the input field when use_custom_user_agent is enabled. ss << "\"target_user_agent\": \"" << util::EscapeJsonString(settings_.custom_user_agent.empty() ? active_user_agent_ : settings_.custom_user_agent) << "\","; + // Expose dark_theme_excluded_sites as a separate field for the text input in settings UI + ss << "\"dark_theme_excluded_sites\": \"" << util::EscapeJsonString(settings_.dark_theme_excluded_sites) << "\","; ss << "\"meta\": {"; ss << "\"updated_at\": \"" << util::ToIso8601UTC(std::chrono::system_clock::now()) << "\","; ss << "\"dirty\": " << (settings_dirty_ ? "true" : "false") << ","; @@ -3722,6 +3740,10 @@ void UI::ApplyDarkModeToView(RefPtr v) { if (!v) return; + + // Build excluded sites list from settings + std::string excluded_patterns = settings_.dark_theme_excluded_sites; + const char *js = R"JS((function(){ try{ // Don't apply dark mode to browser internal pages (they have their own dark styling) @@ -3745,6 +3767,22 @@ void UI::ApplyDarkModeToView(RefPtr v) return false; // Skip dark mode for these pages } + // Check user-defined excluded sites + var excludedPatterns = %s; + if(excludedPatterns && excludedPatterns.length > 0){ + for(var i=0; i v) return true; }catch(e){return false;} })())JS"; - v->EvaluateScript(js, nullptr); + + // Parse excluded patterns into JSON array + std::string patterns_json = "[]"; + if (!excluded_patterns.empty()) { + std::stringstream ss; + ss << "["; + bool first = true; + std::istringstream iss(excluded_patterns); + std::string line; + while (std::getline(iss, line)) { + line.erase(0, line.find_first_not_of(" \t\r\n")); + line.erase(line.find_last_not_of(" \t\r\n") + 1); + if (!line.empty() && line[0] != '#') { + if (!first) ss << ","; + ss << "\"" << line << "\""; + first = false; + } + } + ss << "]"; + patterns_json = ss.str(); + } + + char buffer[8192]; + snprintf(buffer, sizeof(buffer), js, patterns_json.c_str()); + v->EvaluateScript(buffer, nullptr); } void UI::RemoveDarkModeFromView(RefPtr v) @@ -4748,6 +4810,7 @@ void UI::OnNewDownloadStarted() bool UI::BrowserSettings::operator==(const BrowserSettings &other) const { return launch_dark_theme == other.launch_dark_theme && + dark_theme_excluded_sites == other.dark_theme_excluded_sites && vibrant_window_theme == other.vibrant_window_theme && experimental_transparent_toolbar == other.experimental_transparent_toolbar && experimental_compact_tabs == other.experimental_compact_tabs && diff --git a/src/UI.h b/src/UI.h index fc3a975..c19c393 100644 --- a/src/UI.h +++ b/src/UI.h @@ -49,6 +49,7 @@ class UI : public WindowListener, { // Appearance bool launch_dark_theme = false; + std::string dark_theme_excluded_sites; // URL patterns where dark theme should NOT be applied (newline-separated) bool vibrant_window_theme = false; bool experimental_transparent_toolbar = false; bool experimental_compact_tabs = false; From df32c204ef6464d4bdd7cbcec1118f6a7b277532 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 13:00:14 +0100 Subject: [PATCH 24/35] Add accessibility and appearance settings support Implements CSS injection methods for reduce motion, high contrast, smooth scrolling, and transparent toolbar features. Adds vibrant window theme support for Windows using DWM API. Updates ApplySettings to apply these features based on user settings, improving accessibility and UI customization. --- src/UI.cpp | 214 ++++++++++++++++++++++++++++++++++++++++++++++++++++- src/UI.h | 16 ++++ 2 files changed, 229 insertions(+), 1 deletion(-) diff --git a/src/UI.cpp b/src/UI.cpp index 094f339..66007fd 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -30,6 +30,8 @@ #define NOMINMAX 1 #endif #include // GetModuleFileNameW +#include // DwmSetWindowAttribute for window theming +#pragma comment(lib, "dwmapi.lib") #else #include // mkdir #include // getcwd @@ -3138,8 +3140,22 @@ void UI::ApplySettings(bool initial, bool snapshot_is_baseline) { // Appearance SetDarkModeEnabled(settings_.launch_dark_theme); + + // Vibrant window theme - changes title bar color + bool was_vibrant = vibrant_window_theme_enabled_; vibrant_window_theme_enabled_ = settings_.vibrant_window_theme; + if (was_vibrant != vibrant_window_theme_enabled_ || initial) + { + ApplyVibrantWindowTheme(vibrant_window_theme_enabled_); + } + + // Transparent toolbar - applies CSS to UI overlay + bool was_transparent = experimental_transparent_toolbar_enabled_; experimental_transparent_toolbar_enabled_ = settings_.experimental_transparent_toolbar; + if (was_transparent != experimental_transparent_toolbar_enabled_ || initial) + { + ApplyTransparentToolbar(experimental_transparent_toolbar_enabled_); + } // Handle compact tabs mode - adjust UI height and trigger resize bool was_compact = experimental_compact_tabs_enabled_; @@ -3191,11 +3207,48 @@ void UI::ApplySettings(bool initial, bool snapshot_is_baseline) // Performance // enable_javascript and hardware_acceleration are applied during Tab creation (see CreateNewTab) - // smooth_scrolling, local_storage, database - would require additional Ultralight session config + // Smooth scrolling - apply CSS to all tab views + bool was_smooth = smooth_scrolling_enabled_; + smooth_scrolling_enabled_ = settings_.smooth_scrolling; + if (was_smooth != smooth_scrolling_enabled_ || initial) + { + for (auto &entry : tabs_) + { + if (entry.second) + { + if (smooth_scrolling_enabled_) + ApplySmoothScrollingToView(entry.second->view()); + else + RemoveSmoothScrollingFromView(entry.second->view()); + } + } + } // Accessibility reduce_motion_enabled_ = settings_.reduce_motion; high_contrast_ui_enabled_ = settings_.high_contrast_ui; + + // Apply accessibility CSS to all views + auto apply_accessibility = [&](RefPtr v) + { + if (!v) + return; + if (reduce_motion_enabled_) + ApplyReduceMotionToView(v); + else + RemoveReduceMotionFromView(v); + if (high_contrast_ui_enabled_) + ApplyHighContrastToView(v); + else + RemoveHighContrastFromView(v); + }; + + apply_accessibility(view()); + for (auto &entry : tabs_) + { + if (entry.second) + apply_accessibility(entry.second->view()); + } // enable_caret_browsing would require page-level script injection // Developer @@ -3872,6 +3925,165 @@ void UI::RemoveDarkModeFromView(RefPtr v) v->EvaluateScript(js, nullptr); } +void UI::ApplyReduceMotionToView(RefPtr v) +{ + if (!v) + return; + const char *js = R"JS((function(){ + try{ + var sid='__ul_reduce_motion'; + if(document.getElementById(sid)) return false; + var css = '*, *::before, *::after { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; scroll-behavior: auto !important; }'; + var s=document.createElement('style'); + s.id=sid; + s.type='text/css'; + s.appendChild(document.createTextNode(css)); + (document.head||document.documentElement).appendChild(s); + return true; + }catch(e){return false;} + })())JS"; + v->EvaluateScript(js, nullptr); +} + +void UI::RemoveReduceMotionFromView(RefPtr v) +{ + if (!v) + return; + const char *js = R"JS((function(){ + try{ + var s=document.getElementById('__ul_reduce_motion'); if(s) s.remove(); + return true; + }catch(e){return false;} + })())JS"; + v->EvaluateScript(js, nullptr); +} + +void UI::ApplyHighContrastToView(RefPtr v) +{ + if (!v) + return; + const char *js = R"JS((function(){ + try{ + var sid='__ul_high_contrast'; + if(document.getElementById(sid)) return false; + var css = '* { border-color: currentColor !important; outline-color: currentColor !important; }\n'; + css += 'a, a:visited { text-decoration: underline !important; }\n'; + css += 'button, input, select, textarea { border: 2px solid currentColor !important; }\n'; + css += ':focus { outline: 3px solid #0066ff !important; outline-offset: 2px !important; }'; + var s=document.createElement('style'); + s.id=sid; + s.type='text/css'; + s.appendChild(document.createTextNode(css)); + (document.head||document.documentElement).appendChild(s); + return true; + }catch(e){return false;} + })())JS"; + v->EvaluateScript(js, nullptr); +} + +void UI::RemoveHighContrastFromView(RefPtr v) +{ + if (!v) + return; + const char *js = R"JS((function(){ + try{ + var s=document.getElementById('__ul_high_contrast'); if(s) s.remove(); + return true; + }catch(e){return false;} + })())JS"; + v->EvaluateScript(js, nullptr); +} + +void UI::ApplyVibrantWindowTheme(bool enabled) +{ +#if defined(_WIN32) + HWND hwnd = (HWND)window_->native_handle(); + if (hwnd) + { + // Use DWM attribute for caption color (DWMWA_CAPTION_COLOR = 35) + // Vibrant purple: brighter accent color, Dark: standard dark purple + COLORREF color = enabled ? RGB(120, 100, 200) : RGB(42, 33, 60); + DwmSetWindowAttribute(hwnd, 35, &color, sizeof(color)); + } +#endif + (void)enabled; // Suppress unused parameter warning on non-Windows +} + +void UI::ApplySmoothScrollingToView(RefPtr v) +{ + if (!v) + return; + const char *js = R"JS((function(){ + try{ + if(document.getElementById('__ul_smooth_scroll')) return true; + var s=document.createElement('style'); + s.id='__ul_smooth_scroll'; + s.textContent='html, body { scroll-behavior: smooth !important; } * { scroll-behavior: smooth !important; }'; + (document.head||document.documentElement).appendChild(s); + return true; + }catch(e){return false;} + })())JS"; + v->EvaluateScript(js, nullptr); +} + +void UI::RemoveSmoothScrollingFromView(RefPtr v) +{ + if (!v) + return; + const char *js = R"JS((function(){ + try{ + var s=document.getElementById('__ul_smooth_scroll'); if(s) s.remove(); + return true; + }catch(e){return false;} + })())JS"; + v->EvaluateScript(js, nullptr); +} + +void UI::ApplyTransparentToolbar(bool enabled) +{ + // Apply transparent/translucent effect to toolbar UI + if (!overlay_) + return; + + RefPtr ui_view = overlay_->view(); + if (!ui_view) + return; + + const char *js_enable = R"JS((function(){ + try{ + if(document.getElementById('__ul_transparent_toolbar')) return true; + var s=document.createElement('style'); + s.id='__ul_transparent_toolbar'; + s.textContent=` + .toolbar, .tab-bar, nav, header, .browser-toolbar { + background: rgba(30, 30, 46, 0.85) !important; + backdrop-filter: blur(10px) !important; + -webkit-backdrop-filter: blur(10px) !important; + } + .tab-content, .url-bar, .address-bar { + background: rgba(42, 33, 60, 0.9) !important; + } + `; + (document.head||document.documentElement).appendChild(s); + return true; + }catch(e){return false;} + })())JS"; + + const char *js_disable = R"JS((function(){ + try{ + var s=document.getElementById('__ul_transparent_toolbar'); if(s) s.remove(); + return true; + }catch(e){return false;} + })())JS"; + + ui_view->EvaluateScript(enabled ? js_enable : js_disable, nullptr); +} + +void UI::RemoveTransparentToolbar() +{ + ApplyTransparentToolbar(false); +} + // --- URL Suggestions Implementation --- void UI::LoadPopularSites() diff --git a/src/UI.h b/src/UI.h index c19c393..1c7b0b0 100644 --- a/src/UI.h +++ b/src/UI.h @@ -365,6 +365,7 @@ class UI : public WindowListener, bool reduce_motion_enabled_ = false; bool high_contrast_ui_enabled_ = false; bool vibrant_window_theme_enabled_ = false; + bool smooth_scrolling_enabled_ = true; JSFunction updateBack; JSFunction updateForward; @@ -413,6 +414,21 @@ class UI : public WindowListener, void ApplyDarkModeToView(RefPtr v); void RemoveDarkModeFromView(RefPtr v); + // Accessibility CSS injections + void ApplyReduceMotionToView(RefPtr v); + void RemoveReduceMotionFromView(RefPtr v); + void ApplyHighContrastToView(RefPtr v); + void RemoveHighContrastFromView(RefPtr v); + + // Performance CSS injections + void ApplySmoothScrollingToView(RefPtr v); + void RemoveSmoothScrollingFromView(RefPtr v); + + // Window appearance + void ApplyVibrantWindowTheme(bool enabled); + void ApplyTransparentToolbar(bool enabled); + void RemoveTransparentToolbar(); + // Cached user agent string currently applied to outgoing requests. std::string active_user_agent_; // Compute a Chromium-like user agent string approximating the host platform. From 4dcc2dadc60409c6c840cbca777aecd48ae46baa Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 13:00:24 +0100 Subject: [PATCH 25/35] Apply accessibility settings on DOM ready Adds logic to apply reduce motion, high contrast, and smooth scrolling CSS to the view when the DOM is ready, based on UI settings. Improves accessibility support for users with these preferences enabled. --- src/Tab.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Tab.cpp b/src/Tab.cpp index d07429d..cbe6425 100644 --- a/src/Tab.cpp +++ b/src/Tab.cpp @@ -948,6 +948,17 @@ void Tab::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const // Reuse UI helpers via the view ui_->ApplyDarkModeToView(caller); } + + // Accessibility: Apply reduce motion and high contrast CSS if enabled + if (ui_) + { + if (ui_->reduce_motion_enabled_) + ui_->ApplyReduceMotionToView(caller); + if (ui_->high_contrast_ui_enabled_) + ui_->ApplyHighContrastToView(caller); + if (ui_->smooth_scrolling_enabled_) + ui_->ApplySmoothScrollingToView(caller); + } } void Tab::OnQuickInspectorClose(const JSObject &obj, const JSArgs &args) From e920caf0176cf0ff5305419c0bf8cc4e25d219ba Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 13:11:29 +0100 Subject: [PATCH 26/35] Optimize dark mode exclusion for internal pages Moved browser internal page detection from JavaScript to a fast C++ check in ApplyDarkModeToView, reducing unnecessary JS execution for internal pages with their own dark styling. Added static IsBrowserInternalPage method to centralize internal page checks. --- src/UI.cpp | 58 ++++++++++++++++++++++++++++++++++++------------------ src/UI.h | 3 +++ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/UI.cpp b/src/UI.cpp index 66007fd..9e9d877 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -3789,36 +3789,56 @@ void UI::OnContextMenuAction(const JSObject &obj, const JSArgs &args) HideContextMenuOverlay(); } +bool UI::IsBrowserInternalPage(const std::string &url) +{ + // Fast check for browser internal pages - called from C++ to skip JS execution + if (url.find("file:///") != 0) + return false; + + // List of browser internal pages that have their own dark styling + static const char* internal_pages[] = { + "settings.html", + "passwords.html", + "extensions.html", + "downloads.html", + "history.html", + "ui.html", + "menu.html", + "contextmenu.html", + "suggestions.html", + "quick-inspector.html", + "downloads-panel.html", + "about.html", + "new_tab_page.html", + "release_notes.html", + "static-sties/" + }; + + for (const char* page : internal_pages) + { + if (url.find(page) != std::string::npos) + return true; + } + return false; +} + void UI::ApplyDarkModeToView(RefPtr v) { if (!v) return; + // Fast C++ check: skip dark mode injection entirely for browser internal pages + // This avoids expensive JS execution for pages that don't need it + auto url = v->url().utf8(); + if (url.data() && IsBrowserInternalPage(std::string(url.data()))) + return; + // Build excluded sites list from settings std::string excluded_patterns = settings_.dark_theme_excluded_sites; const char *js = R"JS((function(){ try{ - // Don't apply dark mode to browser internal pages (they have their own dark styling) var url = window.location.href; - if(url.startsWith('file:///') && - (url.includes('settings.html') || - url.includes('passwords.html') || - url.includes('extensions.html') || - url.includes('downloads.html') || - url.includes('history.html') || - url.includes('ui.html') || - url.includes('menu.html') || - url.includes('contextmenu.html') || - url.includes('suggestions.html') || - url.includes('quick-inspector.html') || - url.includes('downloads-panel.html') || - url.includes('about.html') || - url.includes('new_tab_page.html') || - url.includes('release_notes.html') || - url.includes('static-sties/'))){ - return false; // Skip dark mode for these pages - } // Check user-defined excluded sites var excludedPatterns = %s; diff --git a/src/UI.h b/src/UI.h index 1c7b0b0..d22cb5c 100644 --- a/src/UI.h +++ b/src/UI.h @@ -409,6 +409,9 @@ class UI : public WindowListener, bool suggestion_favicons_enabled_ = true; void LoadSuggestionsFaviconsFlag(); + // Check if URL is a browser internal page (settings, history, etc.) + static bool IsBrowserInternalPage(const std::string &url); + // Auto Dark Mode state bool dark_mode_enabled_ = false; void ApplyDarkModeToView(RefPtr v); From ed279412138479af4051b3411a3362a4abc9e5dd Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 13:11:36 +0100 Subject: [PATCH 27/35] Skip CSS injection for internal browser pages CSS injections for dark mode, reduce motion, and high contrast are now skipped for internal browser pages to improve their loading speed and avoid overriding their native styling. --- src/Tab.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Tab.cpp b/src/Tab.cpp index cbe6425..e19be31 100644 --- a/src/Tab.cpp +++ b/src/Tab.cpp @@ -942,16 +942,20 @@ void Tab::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const } } - // Auto Dark Mode: if enabled, inject dark styling in this document - if (ui_ && ui_->dark_mode_enabled_) + // Skip all CSS injections for browser internal pages - they have their own styling + // This significantly speeds up internal page loading + auto page_url = caller->url().utf8(); + bool is_internal = page_url.data() && UI::IsBrowserInternalPage(std::string(page_url.data())); + + if (!is_internal && ui_) { - // Reuse UI helpers via the view - ui_->ApplyDarkModeToView(caller); - } + // Auto Dark Mode: if enabled, inject dark styling in this document + if (ui_->dark_mode_enabled_) + { + ui_->ApplyDarkModeToView(caller); + } - // Accessibility: Apply reduce motion and high contrast CSS if enabled - if (ui_) - { + // Accessibility: Apply reduce motion and high contrast CSS if enabled if (ui_->reduce_motion_enabled_) ui_->ApplyReduceMotionToView(caller); if (ui_->high_contrast_ui_enabled_) From 762fa9b401455dc31a98d5779abde5706e54e941 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 13:22:04 +0100 Subject: [PATCH 28/35] Preload internal and start pages for instant tab creation Adds caching of start page and internal browser pages HTML to memory for instant loading, reducing file I/O delays and eliminating white flashes when opening new tabs. Refactors tab creation and shortcut actions to use cached HTML when available, streamlining new tab and internal page display. --- src/UI.cpp | 170 ++++++++++++++++++++++++++++++++++++----------------- src/UI.h | 8 +++ 2 files changed, 125 insertions(+), 53 deletions(-) diff --git a/src/UI.cpp b/src/UI.cpp index 9e9d877..64984e3 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -486,6 +486,11 @@ UI::UI(RefPtr window) // Apply runtime toggles (visual sync happens on DOMReady via SyncSettingsStateToUI) ApplySettings(true, true); + // Pre-load start page HTML for instant new tab creation + LoadCachedStartPage(); + // Pre-load internal browser pages for instant loading + LoadCachedInternalPages(); + // Load keyboard shortcuts mapping LoadShortcuts(); @@ -532,6 +537,11 @@ UI::UI(RefPtr window, AdBlocker *adblock, AdBlocker *tracker) // Apply runtime toggles (visual sync happens on DOMReady via SyncSettingsStateToUI) ApplySettings(true, true); + // Pre-load start page HTML for instant new tab creation + LoadCachedStartPage(); + // Pre-load internal browser pages for instant loading + LoadCachedInternalPages(); + // Load keyboard shortcuts mapping LoadShortcuts(); @@ -959,6 +969,70 @@ static void trim(std::string &s) s = s.substr(a, b - a + 1); } +void UI::LoadCachedStartPage() +{ + // Pre-load the start page HTML into memory for instant tab creation + // This eliminates file I/O delay when opening new tabs + std::ifstream in("assets/static-sties/google-static.html", std::ios::in | std::ios::binary); + if (!in.is_open()) + { + // Fallback to a minimal dark page if file not found + cached_start_page_html_ = R"(New Tab + + )"; + return; + } + std::ostringstream ss; + ss << in.rdbuf(); + cached_start_page_html_ = ss.str(); + in.close(); +} + +void UI::LoadCachedInternalPages() +{ + // Pre-load frequently used internal pages for instant loading + static const char* pages[] = { + "assets/settings.html", + "assets/history.html", + "assets/downloads.html", + "assets/passwords.html", + "assets/extensions.html", + "assets/about.html", + "assets/new_tab_page.html" + }; + + static const char* urls[] = { + "file:///settings.html", + "file:///history.html", + "file:///downloads.html", + "file:///passwords.html", + "file:///extensions.html", + "file:///about.html", + "file:///new_tab_page.html" + }; + + for (size_t i = 0; i < sizeof(pages) / sizeof(pages[0]); ++i) + { + std::ifstream in(pages[i], std::ios::in | std::ios::binary); + if (in.is_open()) + { + std::ostringstream ss; + ss << in.rdbuf(); + cached_internal_pages_[urls[i]] = ss.str(); + in.close(); + } + } +} + +const std::string& UI::GetCachedPageHTML(const std::string& url) const +{ + static const std::string empty; + auto it = cached_internal_pages_.find(url); + if (it != cached_internal_pages_.end()) + return it->second; + return empty; +} + void UI::LoadShortcuts() { // Defaults @@ -1034,13 +1108,8 @@ bool UI::RunShortcutAction(const std::string &action) if (action == "open-history") { // Open History in a NEW tab instead of replacing current - RefPtr child = CreateNewTabForChildView(String("file:///history.html")); - if (child) - { - child->LoadURL("file:///history.html"); - return true; - } - return false; + CreateNewTabForChildView(String("file:///history.html")); + return true; } if (action == "focus-address") { @@ -1062,35 +1131,20 @@ bool UI::RunShortcutAction(const std::string &action) if (action == "open-extensions") { // Open Extensions in a new tab - RefPtr child = CreateNewTabForChildView(String("file:///extensions.html")); - if (child) - { - child->LoadURL("file:///extensions.html"); - return true; - } - return false; + CreateNewTabForChildView(String("file:///extensions.html")); + return true; } if (action == "open-passwords") { // Open Passwords in a new tab - RefPtr child = CreateNewTabForChildView(String("file:///passwords.html")); - if (child) - { - child->LoadURL("file:///passwords.html"); - return true; - } - return false; + CreateNewTabForChildView(String("file:///passwords.html")); + return true; } if (action == "open-settings") { // Open Settings in a NEW tab (like Ctrl+H opens history) - RefPtr child = CreateNewTabForChildView(String("file:///settings.html")); - if (child) - { - child->LoadURL("file:///settings.html"); - return true; - } - return false; + CreateNewTabForChildView(String("file:///settings.html")); + return true; } return false; } @@ -1808,30 +1862,22 @@ void UI::OnAddressBarNavigate(const JSObject &obj, const JSArgs &args) void UI::OnOpenHistoryNewTab(const JSObject &obj, const JSArgs &args) { - RefPtr child = CreateNewTabForChildView(String("file:///history.html")); - if (child) - child->LoadURL("file:///history.html"); + CreateNewTabForChildView(String("file:///history.html")); } void UI::OnOpenDownloadsNewTab(const JSObject &obj, const JSArgs &args) { - RefPtr child = CreateNewTabForChildView(String("file:///downloads.html")); - if (child) - child->LoadURL("file:///downloads.html"); + CreateNewTabForChildView(String("file:///downloads.html")); } void UI::OnOpenPasswordsNewTab(const JSObject &obj, const JSArgs &args) { - RefPtr child = CreateNewTabForChildView(String("file:///passwords.html")); - if (child) - child->LoadURL("file:///passwords.html"); + CreateNewTabForChildView(String("file:///passwords.html")); } void UI::OnOpenExtensionsNewTab(const JSObject &obj, const JSArgs &args) { - RefPtr child = CreateNewTabForChildView(String("file:///extensions.html")); - if (child) - child->LoadURL("file:///extensions.html"); + CreateNewTabForChildView(String("file:///extensions.html")); } // ============================================================================ @@ -2138,13 +2184,22 @@ void UI::CreateNewTab() view_settings.hardware_acceleration = settings_.hardware_acceleration; tabs_[id] = std::make_unique(this, id, window->width(), (uint32_t)tab_height, 0, ui_height_, active_user_agent_, view_settings); - // Load local static start page - const char *kStartPage = "file:///static-sties/google-static.html"; - tabs_[id]->view()->LoadURL(kStartPage); + + // Use cached HTML for instant page display (no file I/O delay) + // This eliminates the white flash before page content loads + const char *kStartPageURL = "file:///static-sties/google-static.html"; + if (!cached_start_page_html_.empty()) + { + tabs_[id]->view()->LoadHTML(String(cached_start_page_html_.c_str()), String(kStartPageURL)); + } + else + { + tabs_[id]->view()->LoadURL(kStartPageURL); + } { RefPtr lock(view()->LockJSContext()); - addTab({id, "New Tab", GetFaviconURL(kStartPage), tabs_[id]->view()->is_loading()}); + addTab({id, "New Tab", GetFaviconURL(kStartPageURL), tabs_[id]->view()->is_loading()}); } UpdateDrmBadge(id, false); } @@ -2167,6 +2222,21 @@ RefPtr UI::CreateNewTabForChildView(const String &url) tabs_[id] = std::make_unique(this, id, window->width(), (uint32_t)tab_height, 0, ui_height_, active_user_agent_, view_settings); + // Try to use cached HTML for instant loading of internal pages + auto url_utf8 = url.utf8(); + std::string url_str(url_utf8.data() ? url_utf8.data() : ""); + const std::string& cached_html = GetCachedPageHTML(url_str); + if (!cached_html.empty()) + { + // Use cached HTML for instant display + tabs_[id]->view()->LoadHTML(String(cached_html.c_str()), url); + } + else + { + // Fall back to regular URL loading for non-cached pages + tabs_[id]->view()->LoadURL(url); + } + { RefPtr lock(view()->LockJSContext()); addTab({id, "", GetFaviconURL(url), tabs_[id]->view()->is_loading()}); @@ -2729,9 +2799,7 @@ void UI::OnToggleDarkMode(const JSObject &obj, const JSArgs &args) void UI::OnOpenSettingsPanel(const JSObject &, const JSArgs &) { HideMenuOverlay(); - RefPtr child = CreateNewTabForChildView(String("file:///settings.html")); - if (child) - child->LoadURL("file:///settings.html"); + CreateNewTabForChildView(String("file:///settings.html")); } void UI::OnCloseSettingsPanel(const JSObject &, const JSArgs &) @@ -3682,9 +3750,7 @@ void UI::OnContextMenuAction(const JSObject &obj, const JSArgs &args) if (action == "open_tab" && args.size() >= 2) { ultralight::String url = args[1]; - RefPtr child = CreateNewTabForChildView(url); - if (child) - child->LoadURL(url); + CreateNewTabForChildView(url); // Handles loading internally HideContextMenuOverlay(); return; } @@ -5002,9 +5068,7 @@ void UI::OnSuggestionPick(const JSObject &obj, const JSArgs &args) } if (open_new_tab) { - RefPtr child = CreateNewTabForChildView(s); - if (child) - child->LoadURL(s); + CreateNewTabForChildView(s); // Handles loading internally return; } if (!tabs_.empty()) diff --git a/src/UI.h b/src/UI.h index d22cb5c..1bf9706 100644 --- a/src/UI.h +++ b/src/UI.h @@ -432,6 +432,14 @@ class UI : public WindowListener, void ApplyTransparentToolbar(bool enabled); void RemoveTransparentToolbar(); + // Cached page HTML for instant loading (avoids file I/O delay) + std::string cached_start_page_html_; + std::unordered_map cached_internal_pages_; + void LoadCachedStartPage(); + void LoadCachedInternalPages(); + // Get cached HTML for a file:/// URL, or empty if not cached + const std::string& GetCachedPageHTML(const std::string& url) const; + // Cached user agent string currently applied to outgoing requests. std::string active_user_agent_; // Compute a Chromium-like user agent string approximating the host platform. From 981086c7b5c2cf2b177e9bed466958c1ddd5e661 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 13:33:57 +0100 Subject: [PATCH 29/35] Update UI.cpp --- src/UI.cpp | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/UI.cpp b/src/UI.cpp index 64984e3..4f7001c 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -2356,6 +2356,7 @@ void UI::SetCursor(ultralight::Cursor cursor) String UI::GetFaviconURL(const String &page_url) { // Best-effort: use origin + "/favicon.ico" for http/https URLs. + // For browser internal pages, return custom favicons. // Cache by origin so multiple tabs/pages from the same site reuse it. auto utf8 = page_url.utf8(); const char *url = utf8.data(); @@ -2363,6 +2364,52 @@ String UI::GetFaviconURL(const String &page_url) return String(""); std::string_view url_view(url); + + // Handle browser internal pages with custom favicons + if (url_view.find("file:///") == 0) + { + // Start page / Google static page - use browser logo + if (url_view.find("static-sties/") != std::string_view::npos || + url_view.find("google-static") != std::string_view::npos) + return String("file:///logo.png"); + + // Settings page - gear icon + if (url_view.find("settings.html") != std::string_view::npos) + return String("data:image/svg+xml,"); + + // History page - clock icon + if (url_view.find("history.html") != std::string_view::npos) + return String("data:image/svg+xml,"); + + // Downloads page - download icon + if (url_view.find("downloads.html") != std::string_view::npos || + url_view.find("downloads-panel.html") != std::string_view::npos) + return String("data:image/svg+xml,"); + + // Passwords page - key/lock icon + if (url_view.find("passwords.html") != std::string_view::npos) + return String("data:image/svg+xml,"); + + // Extensions page - puzzle piece icon + if (url_view.find("extensions.html") != std::string_view::npos) + return String("data:image/svg+xml,"); + + // About page - info icon + if (url_view.find("about.html") != std::string_view::npos) + return String("data:image/svg+xml,"); + + // New tab page - browser logo + if (url_view.find("new_tab_page.html") != std::string_view::npos) + return String("file:///logo.png"); + + // Release notes - document icon + if (url_view.find("release_notes.html") != std::string_view::npos) + return String("data:image/svg+xml,"); + + // Default for other file:// URLs - use browser logo + return String("file:///logo.png"); + } + if (url_view.size() < 7 || (url_view.substr(0, 7) != "http://" && (url_view.size() < 8 || url_view.substr(0, 8) != "https://"))) From b48b95bef7eb0491e69b19b8c3d4a030abc3d846 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 13:34:16 +0100 Subject: [PATCH 30/35] Update favicons for HTML asset pages Replaced or added custom SVG favicons with updated colors for downloads, extensions, history, new tab, passwords, release notes, and settings HTML pages to improve visual consistency and branding. --- assets/downloads.html | 2 +- assets/extensions.html | 1 + assets/history.html | 2 +- assets/new_tab_page.html | 1 + assets/passwords.html | 1 + assets/release_notes.html | 1 + assets/settings.html | 1 + 7 files changed, 7 insertions(+), 2 deletions(-) diff --git a/assets/downloads.html b/assets/downloads.html index 0180c28..de3bc7a 100644 --- a/assets/downloads.html +++ b/assets/downloads.html @@ -4,7 +4,7 @@ Downloads + href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%237c6aef'%3E%3Cpath d='M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z'/%3E%3C/svg%3E" />