From 87f119ac54a77a55f024c87ced144bac9fb6128f Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 14:33:32 +0100 Subject: [PATCH 1/9] Update .gitignore to exclude VS Code config Added .vscode directory and tasks.json to .gitignore to prevent committing local Visual Studio Code configuration files. --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 8d52d1d..e3a8bdb 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ CMakeUserPresets.json /_CPack_Packages debug_log* debug_* +.vscode/tasks.json +/.vscode From d413189f3b1da3f788f8e92f6c3f62efcc1abb03 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 14:34:04 +0100 Subject: [PATCH 2/9] Add General Codebase Refactor summary documentation Introduces docs/General-Codebase-Refactor.md, detailing major refactoring changes including security fixes, privacy features, UI improvements, page caching, favicon system, build system updates, and a new settings system. Serves as a comprehensive reference for recent architectural and feature updates. --- docs/General-Codebase-Refactor.md | 285 ++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 docs/General-Codebase-Refactor.md diff --git a/docs/General-Codebase-Refactor.md b/docs/General-Codebase-Refactor.md new file mode 100644 index 0000000..6be6fe3 --- /dev/null +++ b/docs/General-Codebase-Refactor.md @@ -0,0 +1,285 @@ +# General Codebase Refactor + +**Branch:** `refactor/features-settings-codebase` +**Date:** December 2025 + +This document summarizes all changes and updates made during the comprehensive codebase refactoring session. + +--- + +## Table of Contents + +1. [Security & Code Quality Fixes](#security--code-quality-fixes) +2. [Privacy Features Implementation](#privacy-features-implementation) +3. [UI & Visual Improvements](#ui--visual-improvements) +4. [Page Caching System](#page-caching-system) +5. [Favicon System](#favicon-system) +6. [Build System Improvements](#build-system-improvements) +7. [Settings System](#settings-system) + +--- + +## Security & Code Quality Fixes + +### Buffer Overflow Prevention +- Added bounds checking for string operations throughout the codebase +- Replaced unsafe C-style string functions with safer alternatives +- Added null pointer checks before dereferencing + +### Memory Management +- Fixed potential memory leaks in tab management +- Ensured proper cleanup in destructors +- Added RAII patterns for resource management + +### Input Validation +- Added URL validation before navigation +- Sanitized user inputs in JavaScript callbacks +- Added proper error handling for file operations + +### Thread Safety +- Added mutex protection for shared data structures +- Fixed race conditions in download manager +- Ensured thread-safe access to settings + +--- + +## Privacy Features Implementation + +### Do Not Track (DNT) +- **Location:** `Tab::OnWindowObjectReady()` in `Tab.cpp` +- **Implementation:** JavaScript injection that sets `navigator.doNotTrack = '1'` +- **Behavior:** When enabled in settings, all pages receive the DNT signal +- **Compatibility:** Also sets `navigator.msDoNotTrack` for legacy browser compatibility + +### Third-Party Cookie Blocking +- **Location:** `Tab::OnWindowObjectReady()` in `Tab.cpp` +- **Implementation:** JavaScript injection that intercepts `document.cookie` setter +- **Behavior:** Blocks cookie setting from cross-origin iframes +- **Logging:** Blocked attempts are logged to the browser console + +### Web Security +- **Status:** Partial implementation +- **Current Support:** XHR/Fetch credential handling via polyfills +- **Note:** Full CORS enforcement would require Ultralight SDK API changes + +### New Public Accessors in `UI.h` +```cpp +bool do_not_track_enabled() const; +bool block_third_party_cookies_enabled() const; +bool web_security_enabled() const; +``` + +--- + +## UI & Visual Improvements + +### Dark Theme +- Added dark theme support with user preference detection +- Implemented exclusion list for sites where dark theme should not apply +- Added CSS injection for dark mode styling + +### Browser Internal Pages Styling +- Consistent styling across all internal pages: + - `about.html` + - `settings.html` + - `history.html` + - `downloads.html` + - `extensions.html` + - `new_tab_page.html` + +### Keyboard Shortcuts +- Comprehensive keyboard shortcut system +- Shortcuts stored in `assets/shortcuts.json` +- Support for common browser actions (navigation, tabs, zoom, etc.) + +--- + +## Page Caching System + +### Implementation +- Added caching for browser internal pages +- Pages are pre-loaded and cached for instant display +- Reduces perceived load time for settings, history, etc. + +### Cached Pages +- Settings page +- History page +- Downloads page +- Extensions page +- New tab page + +--- + +## Favicon System + +### Base64-Encoded SVG Favicons +All browser internal pages now have embedded favicons using base64-encoded SVGs: + +| Page | Icon | Color | +|------|------|-------| +| Settings | âš™ī¸ Gear | `#c2bce8` | +| History | 🕐 Clock | `#c2bce8` | +| Downloads | âŦ‡ī¸ Arrow | `#c2bce8` | +| Extensions | 🧩 Puzzle | `#c2bce8` | +| New Tab | 🏠 Home | `#c2bce8` | +| About | â„šī¸ Info | `#c2bce8` | +| Release Notes | 📋 Document | `#c2bce8` | + +### External Page Favicons +- **Implementation:** JavaScript injection to fetch favicons from external websites +- **Location:** `Tab::OnDOMReady()` +- **Fallback:** Uses Google's favicon service as fallback +- **Callback:** `Tab::OnFaviconFetched()` handles favicon updates +- **UI Integration:** `UI::UpdateTabFavicon()` applies favicons to tabs + +--- + +## Build System Improvements + +### Targeted Build Command +**Previous command:** +```bash +cmake --build build --config Release +``` +This built ALL targets including test executables. + +**New command:** +```bash +cmake --build build --config Release --target Ultralight-WebBrowser +``` +This builds ONLY the main browser executable. + +### Updated `.vscode/tasks.json` +```json +{ + "tasks": [ + { + "label": "Build Ultralight-WebBrowser (CMake)", + "command": "cmake --build build --config Release --target Ultralight-WebBrowser", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Build All (including tests)", + "command": "cmake --build build --config Release", + "group": "build" + } + ] +} +``` + +### Benefits +- Faster build times (skips test compilation) +- Cleaner output (only shows browser build progress) +- Easier to identify the output executable + +--- + +## Settings System + +### Settings Catalog +- Created `assets/settings_catalog.json` for structured settings definitions +- Each setting includes: + - Unique key + - Display name + - Description + - Category + - Type (boolean, string, number) + - Default value + +### Settings Categories +1. **Appearance** - Theme, visual options +2. **Privacy & Security** - Blocking, tracking, cookies +3. **Address Bar & Suggestions** - Autocomplete, favicons +4. **Downloads** - Save location, notifications +5. **Performance** - JavaScript, acceleration, scrolling +6. **Accessibility** - Motion, contrast, caret browsing +7. **Developer** - Inspector, overlays +8. **Networking** - User agent customization + +### BrowserSettings Struct +Located in `UI.h`, contains all runtime settings with defaults: +```cpp +struct BrowserSettings { + // Appearance + bool launch_dark_theme = false; + std::string dark_theme_excluded_sites; + + // Privacy & Security + bool enable_adblock = true; + bool enable_web_security = true; + bool block_third_party_cookies = false; + bool do_not_track = true; + + // Performance + bool enable_javascript = true; + bool hardware_acceleration = true; + bool smooth_scrolling = true; + + // ... and more +}; +``` + +--- + +## Files Modified + +### Core Source Files +- `src/Tab.cpp` - Privacy JS injection, favicon handling +- `src/Tab.h` - TabViewSettings struct +- `src/UI.cpp` - Settings application, favicon updates +- `src/UI.h` - Privacy accessors, BrowserSettings + +### Asset Files +- `assets/about.html` - Favicon, styling +- `assets/settings.html` - Favicon, styling +- `assets/history.html` - Favicon, styling +- `assets/downloads.html` - Favicon, styling +- `assets/extensions.html` - Favicon, styling +- `assets/new_tab_page.html` - Favicon, styling +- `assets/release_notes.html` - Favicon, styling +- `assets/settings_catalog.json` - Settings definitions +- `assets/shortcuts.json` - Keyboard shortcuts + +### Configuration Files +- `.vscode/tasks.json` - Build task improvements + +--- + +## Testing Recommendations + +1. **Privacy Features** + - Enable DNT in settings, visit [https://www.deviceinfo.me/](https://www.deviceinfo.me/) to verify + - Enable third-party cookie blocking, test with embedded iframes + +2. **Favicons** + - Open internal pages, verify favicons appear in tabs + - Navigate to external sites, verify favicon fetching + +3. **Build System** + - Run default build task, verify only browser is built + - Run "Build All" task, verify tests are included + +4. **Settings** + - Toggle each setting, verify changes apply correctly + - Restart browser, verify settings persist + +--- + +## Known Limitations + +1. **Web Security (`enable_web_security`)**: Full CORS enforcement requires Ultralight SDK API changes not available in ViewConfig +2. **Third-Party Cookies**: JavaScript-level blocking may not catch all cases; true network-level blocking requires SDK support +3. **DNT Header**: Only sets the JavaScript property; actual HTTP header requires network-level modification + +--- + +## Future Improvements + +- [ ] Implement network-level DNT header injection +- [ ] Add content security policy (CSP) support +- [ ] Implement full third-party cookie blocking at network level +- [ ] Add per-site settings overrides +- [ ] Implement sync settings across devices From f07e1ac2be5af30a0187e559274f2d6f5a66c31d Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 14:34:13 +0100 Subject: [PATCH 3/9] Inject DNT and third-party cookie blocking scripts Adds JavaScript injection to simulate the Do Not Track header and block third-party cookie access when enabled in user settings. This enhances user privacy by overriding navigator.doNotTrack and restricting cookie access in cross-origin iframes. --- src/Tab.cpp | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/src/Tab.cpp b/src/Tab.cpp index af2e989..5463dbb 100644 --- a/src/Tab.cpp +++ b/src/Tab.cpp @@ -460,6 +460,81 @@ void Tab::OnWindowObjectReady(View *caller, uint64_t frame_id, bool is_main_fram })(); )JS"; caller->EvaluateScript(String(cryptoPolyfill), nullptr); + + // Inject Do Not Track (DNT) header simulation if enabled in settings + // This overrides navigator.doNotTrack to report the user's preference + if (ui_->do_not_track_enabled()) + { + const char* dntScript = R"JS( +(function() { + 'use strict'; + // Set Do Not Track property to '1' (enabled) + // This tells websites the user prefers not to be tracked + try { + Object.defineProperty(Navigator.prototype, 'doNotTrack', { + value: '1', + writable: false, + configurable: false, + enumerable: true + }); + // Also set the older msDoNotTrack property for IE compatibility + if (typeof navigator.msDoNotTrack === 'undefined') { + Object.defineProperty(Navigator.prototype, 'msDoNotTrack', { + value: '1', + writable: false, + configurable: false, + enumerable: true + }); + } + console.log('[Ultralight] Do Not Track enabled'); + } catch(e) { + console.warn('[Ultralight] Failed to set DNT:', e); + } +})(); +)JS"; + caller->EvaluateScript(String(dntScript), nullptr); + } + + // Block third-party cookie access if enabled in settings + // This is a best-effort approach since true cookie blocking requires network-level control + if (ui_->block_third_party_cookies_enabled()) + { + const char* cookieBlockScript = R"JS( +(function() { + 'use strict'; + // Monitor and log third-party cookie attempts + var originalDescriptor = Object.getOwnPropertyDescriptor(Document.prototype, 'cookie'); + var topOrigin = window.top.location.origin; + + Object.defineProperty(document, 'cookie', { + get: function() { + return originalDescriptor.get.call(this); + }, + set: function(value) { + try { + // Check if this is a cross-origin iframe attempting to set cookies + if (window !== window.top) { + var currentOrigin = window.location.origin; + if (currentOrigin !== topOrigin) { + console.warn('[Ultralight] Blocked third-party cookie set from:', currentOrigin); + return; // Block the cookie + } + } + } catch(e) { + // Cross-origin frame access might throw, in which case this is third-party + console.warn('[Ultralight] Blocked third-party cookie set (cross-origin iframe)'); + return; + } + return originalDescriptor.set.call(this, value); + }, + configurable: true, + enumerable: true + }); + console.log('[Ultralight] Third-party cookie blocking enabled'); +})(); +)JS"; + caller->EvaluateScript(String(cookieBlockScript), nullptr); + } } } From 1d4e0ce4f045e293c48477b3006b287f652ceea3 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 14:34:33 +0100 Subject: [PATCH 4/9] Document privacy settings implementation in UI Expanded comments in UI::ApplySettings to clarify how privacy settings (do_not_track, block_third_party_cookies, enable_web_security) are implemented or supported. Added accessor methods in UI.h for these settings to facilitate JavaScript injection in tabs. --- src/UI.cpp | 9 +++++++-- src/UI.h | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/UI.cpp b/src/UI.cpp index eb97460..e899617 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -3323,9 +3323,14 @@ void UI::ApplySettings(bool initial, bool snapshot_is_baseline) adblock_enabled_cached_ = settings_.enable_adblock; clear_history_on_exit_ = settings_.clear_history_on_exit; - // Note: enable_javascript and hardware_acceleration are applied to NEW tabs via TabViewSettings + // Note: enable_javascript and hardware_acceleration are applied to NEW tabs via TabViewSettings. // Existing tabs keep their original settings since ViewConfig is immutable after creation. - // enable_web_security, block_third_party_cookies, do_not_track are not yet implemented. + // + // Privacy settings implementation: + // - do_not_track: Implemented via JavaScript injection (sets navigator.doNotTrack = '1') + // - block_third_party_cookies: Implemented via JavaScript injection (blocks cross-origin cookie access) + // - enable_web_security: Not directly supported by Ultralight ViewConfig. XHR/Fetch credentials + // are handled via the existing polyfills. Full CORS enforcement would require Ultralight API changes. // Address Bar & Suggestions suggestions_enabled_ = settings_.enable_suggestions; diff --git a/src/UI.h b/src/UI.h index 81776c5..fccef7a 100644 --- a/src/UI.h +++ b/src/UI.h @@ -214,6 +214,11 @@ class UI : public WindowListener, DownloadManager *download_manager() { return download_manager_.get(); } password::PasswordManager *password_manager() { return password_manager_.get(); } AdBlocker *network_blocker() { return adblock_; } + + // Privacy settings accessors for Tab's JavaScript injection + bool do_not_track_enabled() const { return settings_.do_not_track; } + bool block_third_party_cookies_enabled() const { return settings_.block_third_party_cookies; } + bool web_security_enabled() const { return settings_.enable_web_security; } protected: static std::filesystem::path SettingsDirectory(); From bdaa895827dd64f67c10989e78ea6662d58662fe Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 15:37:32 +0100 Subject: [PATCH 5/9] Add session restore and crash recovery features Implements session management for restoring tabs after crashes or normal shutdown. Adds settings for continuous session saving and restoring previous sessions on startup, with UI prompts and logic to avoid overwriting sessions before user choice. Updates tab creation, navigation, and close events to trigger session saves, and provides functions for session file handling and tab restoration. --- src/UI.cpp | 589 ++++++++++++++++++++++++++++++++++++++++++++++++++++- src/UI.h | 24 +++ 2 files changed, 610 insertions(+), 3 deletions(-) diff --git a/src/UI.cpp b/src/UI.cpp index e899617..06bdadf 100644 --- a/src/UI.cpp +++ b/src/UI.cpp @@ -58,7 +58,7 @@ namespace bool default_value; }; - constexpr std::array kFallbackSettingsCatalog = { + constexpr std::array kFallbackSettingsCatalog = { // Appearance SettingDescriptor{"launch_dark_theme", "Launch in dark theme", "Start Ultralight with dark chrome, toolbars, and tabs by default.", @@ -153,6 +153,14 @@ namespace "Automatically save changes to settings as soon as you toggle options.", "general", nullptr, false, &UI::BrowserSettings::auto_save_settings, true}, + // Session restore + SettingDescriptor{"restore_session_on_startup", "Restore previous session", + "Reopen tabs from your last browsing session when starting the browser.", + "general", nullptr, false, &UI::BrowserSettings::restore_session_on_startup, true}, + SettingDescriptor{"save_session_continuously", "Enable crash recovery", + "Continuously save session state so tabs can be restored after crashes or unexpected closures.", + "general", nullptr, false, &UI::BrowserSettings::save_session_continuously, true}, + // DRM subsystem SettingDescriptor{"enable_drm_webview", "Enable DRM WebView", "Automatically switch Widevine-protected sites to a native DRM-capable WebView.", @@ -501,6 +509,9 @@ UI::UI(RefPtr window) // Load history from disk LoadHistoryFromDisk(); + + // Load session data for crash recovery + LoadSessionFromDisk(); } // Compatibility overload: accepts optional ad/tracker blockers (ignored if not used) @@ -553,6 +564,9 @@ UI::UI(RefPtr window, AdBlocker *adblock, AdBlocker *tracker) // Load history from disk LoadHistoryFromDisk(); + // Load session data for crash recovery + LoadSessionFromDisk(); + // Initialize extension system InitializeExtensions(); @@ -851,6 +865,10 @@ void UI::HandleDrmNavigationState(uint64_t tab_id, bool can_back, bool can_forwa UI::~UI() { + // Save session one final time with clean_exit flag + // This preserves tabs for restoration while indicating it was a normal shutdown + SaveSessionToDiskWithCleanExit(); + // Persist or clear history on shutdown based on settings if (clear_history_on_exit_) { @@ -1403,6 +1421,9 @@ void UI::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const S global["OnPasswordSaveBarResponse"] = BindJSCallback(&UI::OnPasswordSaveBarResponse); // DRM prompt bar callback global["OnDrmPromptResponse"] = BindJSCallback(&UI::OnDrmPromptResponse); + // Session restore bar callbacks + global["OnRestoreSession"] = BindJSCallback(&UI::OnRestoreSession); + global["OnDismissSession"] = BindJSCallback(&UI::OnDismissSession); // Allow UI documents (including settings) to request a chrome overlay reload. global["OnReloadChromeUI"] = BindJSCallback(&UI::OnReloadChromeUI); // Allow UI documents to request reloading the active non-settings tab. @@ -1528,7 +1549,26 @@ void UI::OnDOMReady(View *caller, uint64_t frame_id, bool is_main_frame, const S RefPtr lock(view()->LockJSContext()); if (tabs_.empty()) { - CreateNewTab(); + // Check if we should restore a previous session + // Only show restore bar if there are meaningful (non-internal) tabs to restore + if (settings_.restore_session_on_startup && session_restore_pending_ && HasSavedSession() && GetMeaningfulSavedTabCount() > 0) + { + // IMPORTANT: Set this flag BEFORE creating the tab to prevent session saving + // from overwriting the saved session while the restore bar is visible + session_restore_bar_visible_ = true; + + // Create a blank tab first, then show restore bar + CreateNewTab(); + // Show the session restore bar to ask user + ShowSessionRestoreBar(); + } + else + { + // No meaningful session to restore, create a new tab + CreateNewTab(); + // Clear restore pending since there's nothing meaningful to restore + session_restore_pending_ = false; + } } else { @@ -1681,6 +1721,9 @@ void UI::OnRequestTabClose(const JSObject &obj, const JSArgs &args) RefPtr lock(view()->LockJSContext()); closeTab({id}); + + // Save session after tab close for crash recovery + SaveSessionToDisk(); } } @@ -2202,6 +2245,9 @@ void UI::CreateNewTab() addTab({id, "New Tab", GetFaviconURL(kStartPageURL), tabs_[id]->view()->is_loading()}); } UpdateDrmBadge(id, false); + + // Save session after new tab for crash recovery + SaveSessionToDisk(); } RefPtr UI::CreateNewTabForChildView(const String &url) @@ -2304,6 +2350,12 @@ void UI::UpdateTabNavigation(uint64_t id, bool is_loading, bool can_go_back, boo SetCanGoBack(can_go_back); SetCanGoForward(can_go_forward); } + + // Save session when navigation completes (not during loading to reduce disk I/O) + if (!is_loading) + { + SaveSessionToDisk(); + } } void UI::UpdateTabFavicon(uint64_t id, const String &favicon_data_url) @@ -4405,6 +4457,535 @@ void UI::SaveHistoryToDisk() out.close(); } +// ================================================================================ +// Session Management (Crash Recovery / Restore Tabs) +// ================================================================================ + +void UI::SaveSessionToDisk() +{ + // Save current session state to disk for crash recovery + // This is called whenever tabs change (new tab, close tab, navigation) + + if (!settings_.save_session_continuously) + return; + + // Don't overwrite saved session while restore bar is visible + // User hasn't made a choice yet, so preserve their previous session + if (session_restore_bar_visible_) + return; + + EnsureDataDirectoryExists(); + std::ofstream out("data/session.json", std::ios::out | std::ios::binary | std::ios::trunc); + if (!out.is_open()) + return; + + // Get current timestamp + auto now = std::chrono::system_clock::now(); + auto timestamp = std::chrono::duration_cast( + now.time_since_epoch()) + .count(); + + out << "{\n"; + out << " \"version\": 1,\n"; + out << " \"timestamp\": " << timestamp << ",\n"; + out << " \"clean_exit\": false,\n"; + out << " \"active_tab_id\": " << active_tab_id_ << ",\n"; + out << " \"tabs\": [\n"; + + bool first = true; + for (const auto &entry : tabs_) + { + if (!entry.second) + continue; + + auto view = entry.second->view(); + if (!view) + continue; + + auto url_ul = view->url(); + auto title_ul = view->title(); + std::string url = url_ul.utf8().data() ? url_ul.utf8().data() : ""; + std::string title = title_ul.utf8().data() ? title_ul.utf8().data() : ""; + + // Skip internal pages that shouldn't be restored + if (url.find("file:///ui.html") != std::string::npos || + url.find("file:///menu.html") != std::string::npos || + url.find("file:///contextmenu.html") != std::string::npos || + url.find("file:///suggestions.html") != std::string::npos || + url.find("file:///downloads-panel.html") != std::string::npos) + continue; + + // Skip empty URLs + if (url.empty() || url == "about:blank") + continue; + + if (!first) + out << ",\n"; + first = false; + + out << " {\"id\": " << entry.first + << ", \"url\": \"" << jsonEscape(url) + << "\", \"title\": \"" << jsonEscape(title) << "\"}"; + } + + out << "\n ],\n"; + + // Also save DRM tabs + out << " \"drm_tabs\": [\n"; + first = true; + for (const auto &entry : drm_tab_urls_) + { + auto title_it = drm_tab_titles_.find(entry.first); + std::string title = (title_it != drm_tab_titles_.end()) ? title_it->second : ""; + + if (entry.second.empty()) + continue; + + if (!first) + out << ",\n"; + first = false; + + out << " {\"id\": " << entry.first + << ", \"url\": \"" << jsonEscape(entry.second) + << "\", \"title\": \"" << jsonEscape(title) << "\"}"; + } + out << "\n ]\n"; + out << "}\n"; + out.close(); +} + +void UI::LoadSessionFromDisk() +{ + // Load session data from disk (does not restore tabs, just loads the data) + std::ifstream in("data/session.json"); + if (!in.is_open()) + { + session_restore_pending_ = false; + session_was_clean_exit_ = true; + return; + } + + std::stringstream buffer; + buffer << in.rdbuf(); + in.close(); + + std::string content = buffer.str(); + + // Parse clean_exit flag to determine if last session crashed + size_t clean_exit_pos = content.find("\"clean_exit\""); + if (clean_exit_pos != std::string::npos) + { + size_t colon_pos = content.find(":", clean_exit_pos); + if (colon_pos != std::string::npos) + { + std::string value = content.substr(colon_pos + 1, 10); + session_was_clean_exit_ = (value.find("true") != std::string::npos); + } + } + + // Mark for restore if we have session data (regardless of how last session ended) + // Chrome-like behavior: always restore previous session if enabled + if (content.find("\"tabs\"") != std::string::npos) + { + // Check if tabs array has content + size_t tabs_pos = content.find("\"tabs\""); + if (tabs_pos != std::string::npos) + { + size_t bracket_start = content.find("[", tabs_pos); + size_t bracket_end = content.find("]", bracket_start); + if (bracket_start != std::string::npos && bracket_end != std::string::npos) + { + std::string tabs_str = content.substr(bracket_start + 1, bracket_end - bracket_start - 1); + // Remove whitespace to check if empty + tabs_str.erase(std::remove_if(tabs_str.begin(), tabs_str.end(), ::isspace), tabs_str.end()); + if (!tabs_str.empty()) + { + session_restore_pending_ = true; + } + } + } + } +} + +bool UI::HasSavedSession() const +{ + std::ifstream in("data/session.json"); + if (!in.is_open()) + return false; + + // Quick check if file has any tab data + std::stringstream buffer; + buffer << in.rdbuf(); + std::string content = buffer.str(); + + // Check if there are any tabs saved + size_t tabs_pos = content.find("\"tabs\""); + if (tabs_pos == std::string::npos) + return false; + + // Check if tabs array is non-empty + size_t bracket_start = content.find("[", tabs_pos); + size_t bracket_end = content.find("]", bracket_start); + if (bracket_start == std::string::npos || bracket_end == std::string::npos) + return false; + + std::string tabs_content = content.substr(bracket_start + 1, bracket_end - bracket_start - 1); + // Remove whitespace + tabs_content.erase(std::remove_if(tabs_content.begin(), tabs_content.end(), ::isspace), tabs_content.end()); + + return !tabs_content.empty(); +} + +void UI::ClearSavedSession() +{ + // Clear the restore pending flag (used after session is restored) + session_restore_pending_ = false; +} + +void UI::SaveSessionToDiskWithCleanExit() +{ + // Save current session state with clean_exit=true + // Called during normal shutdown to preserve tabs for next startup + + EnsureDataDirectoryExists(); + std::ofstream out("data/session.json", std::ios::out | std::ios::binary | std::ios::trunc); + if (!out.is_open()) + return; + + auto now = std::chrono::system_clock::now(); + auto timestamp = std::chrono::duration_cast( + now.time_since_epoch()) + .count(); + + out << "{\n"; + out << " \"version\": 1,\n"; + out << " \"timestamp\": " << timestamp << ",\n"; + out << " \"clean_exit\": true,\n"; // Mark as clean exit + out << " \"active_tab_id\": " << active_tab_id_ << ",\n"; + out << " \"tabs\": [\n"; + + bool first = true; + for (const auto &entry : tabs_) + { + if (!entry.second) + continue; + + auto view = entry.second->view(); + if (!view) + continue; + + auto url_ul = view->url(); + auto title_ul = view->title(); + std::string url = url_ul.utf8().data() ? url_ul.utf8().data() : ""; + std::string title = title_ul.utf8().data() ? title_ul.utf8().data() : ""; + + // Skip internal UI pages + if (url.find("file:///ui.html") != std::string::npos || + url.find("file:///menu.html") != std::string::npos || + url.find("file:///contextmenu.html") != std::string::npos || + url.find("file:///suggestions.html") != std::string::npos || + url.find("file:///downloads-panel.html") != std::string::npos) + continue; + + if (url.empty() || url == "about:blank") + continue; + + if (!first) + out << ",\n"; + first = false; + + out << " {\"id\": " << entry.first + << ", \"url\": \"" << jsonEscape(url) + << "\", \"title\": \"" << jsonEscape(title) << "\"}"; + } + + out << "\n ],\n"; + out << " \"drm_tabs\": [\n"; + first = true; + for (const auto &entry : drm_tab_urls_) + { + auto title_it = drm_tab_titles_.find(entry.first); + std::string title = (title_it != drm_tab_titles_.end()) ? title_it->second : ""; + + if (entry.second.empty()) + continue; + + if (!first) + out << ",\n"; + first = false; + + out << " {\"id\": " << entry.first + << ", \"url\": \"" << jsonEscape(entry.second) + << "\", \"title\": \"" << jsonEscape(title) << "\"}"; + } + out << "\n ]\n"; + out << "}\n"; + out.close(); +} + +void UI::RestoreSavedSession() +{ + // Restore tabs from saved session + std::ifstream in("data/session.json"); + if (!in.is_open()) + return; + + std::stringstream buffer; + buffer << in.rdbuf(); + in.close(); + + std::string content = buffer.str(); + + // Parse tabs array - simple JSON parsing + size_t tabs_pos = content.find("\"tabs\""); + if (tabs_pos == std::string::npos) + return; + + size_t bracket_start = content.find("[", tabs_pos); + size_t bracket_end = content.find("]", bracket_start); + if (bracket_start == std::string::npos || bracket_end == std::string::npos) + return; + + std::string tabs_content = content.substr(bracket_start + 1, bracket_end - bracket_start - 1); + + // Parse each tab entry - collect ALL tabs including duplicates + size_t pos = 0; + std::vector urls_to_restore; + + while ((pos = tabs_content.find("{", pos)) != std::string::npos) + { + size_t end_obj = tabs_content.find("}", pos); + if (end_obj == std::string::npos) + break; + + std::string obj = tabs_content.substr(pos, end_obj - pos + 1); + + // Extract URL + size_t url_pos = obj.find("\"url\""); + std::string url; + if (url_pos != std::string::npos) + { + size_t url_start = obj.find("\"", url_pos + 5); + size_t url_end = obj.find("\"", url_start + 1); + if (url_start != std::string::npos && url_end != std::string::npos) + { + url = obj.substr(url_start + 1, url_end - url_start - 1); + } + } + + // Add ALL non-empty URLs (including duplicates) + if (!url.empty()) + { + urls_to_restore.push_back(url); + } + + pos = end_obj + 1; + } + + // If no tabs to restore, do nothing + if (urls_to_restore.empty()) + { + session_restore_pending_ = false; + return; + } + + // Strategy: Navigate the existing first tab to the first URL, + // then create new tabs for the remaining URLs. + // This avoids the complexity of closing tabs. + + bool first_url = true; + for (const auto &url : urls_to_restore) + { + if (first_url && !tabs_.empty()) + { + // Navigate the existing (start page) tab to the first restored URL + auto first_tab_it = tabs_.begin(); + if (first_tab_it->second && first_tab_it->second->view()) + { + first_tab_it->second->view()->LoadURL(String(url.c_str())); + } + first_url = false; + } + else + { + // Create new tabs for remaining URLs + CreateNewTabForChildView(String(url.c_str())); + } + } + + // Clear the pending restore flag + session_restore_pending_ = false; + + // Mark current session as active (not clean exit) since we're running + SaveSessionToDisk(); +} + +int UI::GetSavedSessionTabCount() const +{ + std::ifstream in("data/session.json"); + if (!in.is_open()) + return 0; + + std::stringstream buffer; + buffer << in.rdbuf(); + in.close(); + + std::string content = buffer.str(); + + // Count tabs in the array + size_t tabs_pos = content.find("\"tabs\""); + if (tabs_pos == std::string::npos) + return 0; + + size_t bracket_start = content.find("[", tabs_pos); + size_t bracket_end = content.find("]", bracket_start); + if (bracket_start == std::string::npos || bracket_end == std::string::npos) + return 0; + + std::string tabs_content = content.substr(bracket_start + 1, bracket_end - bracket_start - 1); + + // Count '{' characters to count objects + int count = 0; + for (char c : tabs_content) + { + if (c == '{') + count++; + } + + return count; +} + +bool UI::IsInternalBrowserPage(const std::string &url) const +{ + // List of internal/default browser pages that don't need to be restored + static const std::vector internal_pages = { + "file:///static-sties/google-static.html", + "file:///new_tab_page.html", + "file:///settings.html", + "file:///history.html", + "file:///downloads.html", + "file:///passwords.html", + "file:///extensions.html", + "file:///about.html", + "file:///release_notes.html", + "file:///ui.html", + "file:///menu.html", + "file:///contextmenu.html", + "file:///suggestions.html", + "file:///downloads-panel.html", + "about:blank" + }; + + for (const auto &page : internal_pages) + { + if (url.find(page) != std::string::npos || url == page) + return true; + } + + // Also check for any file:/// URL that's an internal asset + if (url.find("file:///") == 0) + { + // Check if it's a local static site or internal page + if (url.find("static-sties") != std::string::npos) + return true; + } + + return false; +} + +int UI::GetMeaningfulSavedTabCount() const +{ + // Count tabs that are NOT internal browser pages + std::ifstream in("data/session.json"); + if (!in.is_open()) + return 0; + + std::stringstream buffer; + buffer << in.rdbuf(); + in.close(); + + std::string content = buffer.str(); + + size_t tabs_pos = content.find("\"tabs\""); + if (tabs_pos == std::string::npos) + return 0; + + size_t bracket_start = content.find("[", tabs_pos); + size_t bracket_end = content.find("]", bracket_start); + if (bracket_start == std::string::npos || bracket_end == std::string::npos) + return 0; + + std::string tabs_content = content.substr(bracket_start + 1, bracket_end - bracket_start - 1); + + int meaningful_count = 0; + size_t pos = 0; + + while ((pos = tabs_content.find("{", pos)) != std::string::npos) + { + size_t end_obj = tabs_content.find("}", pos); + if (end_obj == std::string::npos) + break; + + std::string obj = tabs_content.substr(pos, end_obj - pos + 1); + + // Extract URL + size_t url_pos = obj.find("\"url\""); + if (url_pos != std::string::npos) + { + size_t url_start = obj.find("\"", url_pos + 5); + size_t url_end = obj.find("\"", url_start + 1); + if (url_start != std::string::npos && url_end != std::string::npos) + { + std::string url = obj.substr(url_start + 1, url_end - url_start - 1); + if (!IsInternalBrowserPage(url)) + { + meaningful_count++; + } + } + } + + pos = end_obj + 1; + } + + return meaningful_count; +} + +void UI::ShowSessionRestoreBar() +{ + // Mark that restore bar is visible to prevent session saving + session_restore_bar_visible_ = true; + + int tabCount = GetMeaningfulSavedTabCount(); + bool wasCrash = !session_was_clean_exit_; + + std::ostringstream js; + js << "(function(){ if(typeof showSessionRestoreBar === 'function') showSessionRestoreBar(" + << tabCount << ", " << (wasCrash ? "true" : "false") << "); })();"; + + view()->EvaluateScript(String(js.str().c_str()), nullptr); +} + +void UI::OnRestoreSession(const JSObject &obj, const JSArgs &args) +{ + // User clicked "Restore" - restore all saved tabs + // Clear the bar visibility flag first so we can save the restored session + session_restore_bar_visible_ = false; + RestoreSavedSession(); +} + +void UI::OnDismissSession(const JSObject &obj, const JSArgs &args) +{ + // User clicked "Start Fresh" or closed the bar + // Clear the bar visibility flag so we can save the new session + session_restore_bar_visible_ = false; + + // Clear the pending flag so we don't show the bar again + session_restore_pending_ = false; + + // Start a new session with the current tab + SaveSessionToDisk(); +} + std::vector UI::GetSuggestions(const std::string &input, int maxResults) { std::vector suggestions; @@ -5204,7 +5785,9 @@ bool UI::BrowserSettings::operator==(const BrowserSettings &other) const use_custom_user_agent == other.use_custom_user_agent && custom_user_agent == other.custom_user_agent && auto_save_settings == other.auto_save_settings && - enable_drm_webview == other.enable_drm_webview; + enable_drm_webview == other.enable_drm_webview && + restore_session_on_startup == other.restore_session_on_startup && + save_session_continuously == other.save_session_continuously; } std::filesystem::path UI::SettingsDirectory() diff --git a/src/UI.h b/src/UI.h index fccef7a..38e918d 100644 --- a/src/UI.h +++ b/src/UI.h @@ -102,6 +102,10 @@ class UI : public WindowListener, // DRM WebView subsystem toggle (disabled by default, user must opt-in) bool enable_drm_webview = false; + // Session restore settings + bool restore_session_on_startup = true; // Restore previous session tabs on startup + bool save_session_continuously = true; // Save session state continuously (crash recovery) + bool operator==(const BrowserSettings &other) const; bool operator!=(const BrowserSettings &other) const { return !(*this == other); } }; @@ -254,6 +258,20 @@ class UI : public WindowListener, String GetHistoryJSON(); void ClearHistory(); + // Session management (crash recovery / restore tabs) + void SaveSessionToDisk(); + void SaveSessionToDiskWithCleanExit(); + void LoadSessionFromDisk(); + bool HasSavedSession() const; + void ClearSavedSession(); + void RestoreSavedSession(); + void ShowSessionRestoreBar(); // Show restore prompt in UI + void OnRestoreSession(const JSObject &obj, const JSArgs &args); // User clicked restore + void OnDismissSession(const JSObject &obj, const JSArgs &args); // User clicked dismiss + int GetSavedSessionTabCount() const; // Get number of saved tabs + int GetMeaningfulSavedTabCount() const; // Get count of non-internal tabs + bool IsInternalBrowserPage(const std::string &url) const; // Check if URL is internal + // Downloads management helpers String GetDownloadsJSON(); void ClearCompletedDownloads(); @@ -372,6 +390,12 @@ class UI : public WindowListener, bool high_contrast_ui_enabled_ = false; bool vibrant_window_theme_enabled_ = false; bool smooth_scrolling_enabled_ = true; + + // Session restore state + bool session_restore_pending_ = false; // True if we should prompt to restore session + bool session_was_clean_exit_ = false; // True if last exit was clean (not crash/ALT+F4) + bool session_restore_bar_visible_ = false; // True while restore bar is shown (prevents session saving) + std::string session_file_path_; // Path to session.json JSFunction updateBack; JSFunction updateForward; From f972686bdbf982fda01378a17bd0c6a3c90cba49 Mon Sep 17 00:00:00 2001 From: ovsky Date: Fri, 5 Dec 2025 15:38:16 +0100 Subject: [PATCH 6/9] Add session restore bar UI and logic Introduces a session restore bar to the UI, including new HTML markup, CSS styles, and JavaScript functions for showing, hiding, and handling user actions. Updates .gitignore to exclude session data and .vscode directory. --- .gitignore | 2 + assets/ui.css | 106 +++++++++++++++++++++++++++++++++++++++++++++++++ assets/ui.html | 71 ++++++++++++++++++++++++++++++++- 3 files changed, 178 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index e3a8bdb..287822c 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ debug_log* debug_* .vscode/tasks.json /.vscode +data/session.json +.vscode/ diff --git a/assets/ui.css b/assets/ui.css index c9cc8b1..46f528f 100644 --- a/assets/ui.css +++ b/assets/ui.css @@ -300,4 +300,110 @@ body.compact-tabs .icon { body.compact-tabs #toggle-downloads .downloads-badge { top: 0; +} + +/* Session Restore Bar */ +#session-restore-bar { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + background: linear-gradient(180deg, #2d4a6f 0%, #1e3a5f 100%); + border-bottom: 1px solid #3d6a9f; + padding: 8px 16px; + z-index: 9999; + align-items: center; + justify-content: space-between; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + animation: slideDown 0.3s ease-out; +} + +@keyframes slideDown { + from { + transform: translateY(-100%); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +#session-restore-bar.visible { + display: flex; +} + +#session-restore-bar .restore-message { + display: flex; + align-items: center; + gap: 10px; + color: #e0e8f0; + font-size: 13px; +} + +#session-restore-bar .restore-icon { + width: 20px; + height: 20px; + fill: #7cb3e0; +} + +#session-restore-bar .restore-actions { + display: flex; + gap: 8px; +} + +#session-restore-bar .restore-btn { + padding: 6px 14px; + border-radius: 4px; + border: none; + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s, transform 0.1s; +} + +#session-restore-bar .restore-btn:hover { + transform: translateY(-1px); +} + +#session-restore-bar .restore-btn:active { + transform: translateY(0); +} + +#session-restore-bar .restore-btn.primary { + background: #4a9eff; + color: white; +} + +#session-restore-bar .restore-btn.primary:hover { + background: #5aadff; +} + +#session-restore-bar .restore-btn.secondary { + background: rgba(255, 255, 255, 0.1); + color: #c0d0e0; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +#session-restore-bar .restore-btn.secondary:hover { + background: rgba(255, 255, 255, 0.15); +} + +#session-restore-bar .dismiss-btn { + background: none; + border: none; + color: #8090a0; + cursor: pointer; + padding: 4px; + margin-left: 8px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; +} + +#session-restore-bar .dismiss-btn:hover { + background: rgba(255, 255, 255, 0.1); + color: #c0d0e0; } \ No newline at end of file diff --git a/assets/ui.html b/assets/ui.html index 34bd438..7f0e2a4 100644 --- a/assets/ui.html +++ b/assets/ui.html @@ -94,7 +94,26 @@ - + + +