1package browser
2
3import (
4 "context"
5 "log"
6 "os"
7 "path/filepath"
8 "strings"
9 "time"
10
11 "github.com/bnema/dumber/internal/app/constants"
12 "github.com/bnema/dumber/internal/app/control"
13 "github.com/bnema/dumber/internal/app/environment"
14 "github.com/bnema/dumber/internal/config"
15 "github.com/bnema/dumber/internal/filtering"
16 "github.com/bnema/dumber/pkg/webkit"
17)
18
19// buildMemoryConfig converts config.WebkitMemoryConfig to webkit.MemoryConfig
20func buildMemoryConfig(cfg config.WebkitMemoryConfig) webkit.MemoryConfig {
21 mc := webkit.MemoryConfig{
22 MemoryLimitMB: cfg.MemoryLimitMB,
23 ConservativeThreshold: cfg.ConservativeThreshold,
24 StrictThreshold: cfg.StrictThreshold,
25 KillThreshold: cfg.KillThreshold,
26 PollIntervalSeconds: cfg.PollIntervalSeconds,
27 EnableGCInterval: cfg.EnableGCInterval,
28 ProcessRecycleThreshold: cfg.ProcessRecycleThreshold,
29 EnablePageCache: cfg.EnablePageCache,
30 EnableMemoryMonitoring: cfg.EnableMemoryMonitoring,
31 }
32
33 // Convert string cache model to webkit constant
34 switch strings.ToLower(cfg.CacheModel) {
35 case "document_viewer", "documentviewer", "doc":
36 mc.CacheModel = webkit.CacheModelDocumentViewer
37 case "primary_web_browser", "primary", "primarywebbrowser":
38 mc.CacheModel = webkit.CacheModelPrimaryWebBrowser
39 default:
40 mc.CacheModel = webkit.CacheModelWebBrowser
41 }
42
43 return mc
44}
45
46// createWebView creates and configures the WebView
47func (app *BrowserApp) createWebView() error {
48 log.Printf("Creating WebView (native backend expected: %v)", webkit.IsNativeAvailable())
49 dataDir, _ := config.GetDataDir()
50 stateDir, _ := config.GetStateDir()
51 webkitData := filepath.Join(dataDir, "webkit")
52 webkitCache := filepath.Join(stateDir, "webkit-cache")
53 _ = os.MkdirAll(webkitData, constants.DirPerm)
54 _ = os.MkdirAll(webkitCache, constants.DirPerm)
55
56 view, err := webkit.NewWebView(&webkit.Config{
57 Assets: app.assets,
58 InitialURL: "dumb://homepage",
59 ZoomDefault: 1.0,
60 EnableDeveloperExtras: true,
61 DataDir: webkitData,
62 CacheDir: webkitCache,
63 APIToken: app.config.APISecurity.Token,
64 DefaultSansFont: app.config.Appearance.SansFont,
65 DefaultSerifFont: app.config.Appearance.SerifFont,
66 DefaultMonospaceFont: app.config.Appearance.MonospaceFont,
67 DefaultFontSize: app.config.Appearance.DefaultFontSize,
68 Rendering: webkit.RenderingConfig{Mode: string(app.config.RenderingMode)},
69 VideoAcceleration: webkit.VideoAccelerationConfig{
70 EnableVAAPI: app.config.VideoAcceleration.EnableVAAPI,
71 AutoDetectGPU: app.config.VideoAcceleration.AutoDetectGPU,
72 VAAPIDriverName: app.config.VideoAcceleration.VAAPIDriverName,
73 EnableAllDrivers: app.config.VideoAcceleration.EnableAllDrivers,
74 LegacyVAAPI: app.config.VideoAcceleration.LegacyVAAPI,
75 },
76 Memory: buildMemoryConfig(app.config.WebkitMemory),
77 CodecPreferences: webkit.CodecPreferencesConfig{
78 PreferredCodecs: strings.Split(app.config.CodecPreferences.PreferredCodecs, ","),
79 BlockedCodecs: environment.BuildBlockedCodecsList(app.config.CodecPreferences),
80 ForceAV1: app.config.CodecPreferences.ForceAV1,
81 CustomUserAgent: app.config.CodecPreferences.CustomUserAgent,
82 DisableTwitchCodecControl: app.config.CodecPreferences.DisableTwitchCodecControl,
83 },
84 })
85
86 if err != nil {
87 return err
88 }
89
90 app.webView = view
91 app.setupWebViewIntegration()
92 app.setupWebViewHandlers()
93 app.setupControllers()
94 app.setupKeyboardShortcuts()
95
96 // Apply initial zoom and show window
97 app.zoomController.ApplyInitialZoom()
98
99 log.Printf("Showing WebView window…")
100 if err := view.Show(); err != nil {
101 log.Printf("Warning: failed to show WebView: %v", err)
102 } else if !webkit.IsNativeAvailable() {
103 log.Printf("Notice: running without webkit_cgo tag — no native window will be displayed.")
104 }
105
106 return nil
107}
108
109// setupWebViewIntegration connects the WebView to browser services
110func (app *BrowserApp) setupWebViewIntegration() {
111 app.browserService.AttachWebView(app.webView)
112
113 // GUI bundle is loaded via WebKit user scripts in enableUserContentManager
114 // No need to load separately in browser service
115
116 // Use native window as title updater
117 if win := app.webView.Window(); win != nil {
118 app.browserService.SetWindowTitleUpdater(win)
119 }
120}
121
122// setupWebViewHandlers configures WebView event handlers
123func (app *BrowserApp) setupWebViewHandlers() {
124 // Persist page titles to DB when they change
125 app.webView.RegisterTitleChangedHandler(func(title string) {
126 ctx := context.Background()
127 url := app.webView.GetCurrentURL()
128 if url != "" && title != "" {
129 if err := app.browserService.UpdatePageTitle(ctx, url, title); err != nil {
130 log.Printf("Warning: failed to update page title: %v", err)
131 }
132 }
133 })
134
135 // Set WebView reference for message handler and register script messages
136 app.messageHandler.SetWebView(app.webView)
137 app.webView.RegisterScriptMessageHandler(app.messageHandler.Handle)
138
139}
140
141// setupControllers initializes and configures controller objects
142func (app *BrowserApp) setupControllers() {
143 // Initialize controllers
144 app.zoomController = control.NewZoomController(app.browserService, app.webView)
145 app.navigationController = control.NewNavigationController(
146 app.parserService,
147 app.browserService,
148 app.webView,
149 app.zoomController,
150 )
151 app.clipboardController = control.NewClipboardController(app.webView)
152
153 // Register controller handlers
154 app.zoomController.RegisterHandlers()
155}
156
157// setupKeyboardShortcuts registers keyboard shortcuts
158func (app *BrowserApp) setupKeyboardShortcuts() {
159 app.shortcutHandler = NewShortcutHandler(app.webView, app.clipboardController)
160 app.shortcutHandler.RegisterShortcuts()
161}
162
163// setupContentBlocking initializes the content blocking system with proper timing
164func (app *BrowserApp) setupContentBlocking() error {
165 log.Printf("Initializing content blocking system...")
166
167 // Enable WebKit debug logging if requested
168 webkit.SetupWebKitDebugLogging(app.config)
169
170 defer func() {
171 if r := recover(); r != nil {
172 log.Printf("Content blocking setup panic recovered: %v", r)
173 }
174 }()
175
176 // Setup filter system
177 filterManager, err := filtering.SetupFilterSystem()
178 if err != nil {
179 log.Printf("Warning: Failed to setup filter system: %v", err)
180 return nil // Don't fail browser startup, continue without filters
181 }
182
183 // Store reference to filter manager
184 app.filterManager = filterManager
185
186 // Set up callback to re-apply network filters when they become ready
187 // This callback will be called from the async filter loading process
188 filterManager.SetFiltersReadyCallback(func() {
189 // Add small delay to avoid race conditions with WebView initialization
190 go func() {
191 time.Sleep(300 * time.Millisecond)
192 if app.webView != nil {
193 if err := app.webView.UpdateContentFilters(filterManager); err != nil {
194 log.Printf("Failed to apply filters after loading: %v", err)
195 } else {
196 log.Printf("Successfully applied filters after async loading")
197 }
198 }
199 }()
200 })
201
202 // Start async filter loading early (before WebView content blocking initialization)
203 // This allows filters to be ready when the WebView needs them
204 go func() {
205 if err := filtering.InitializeFiltersAsync(filterManager); err != nil {
206 log.Printf("Warning: failed to initialize filters asynchronously: %v", err)
207 }
208 }()
209
210 // Initialize content blocking in WebView with delay
211 // Wait for WebView to be fully loaded before setting up content blocking
212 go func() {
213 // Wait extra time for the first navigation to avoid preconnect interference
214 log.Printf("Waiting for WebView to complete initial load before enabling content blocking...")
215 time.Sleep(3000 * time.Millisecond) // 3 seconds for first load stability
216
217 if err := app.webView.InitializeContentBlocking(filterManager); err != nil {
218 log.Printf("Warning: Failed to initialize content blocking: %v", err)
219 // Continue without content blocking rather than failing
220 }
221
222 // Register navigation handler for domain-specific filtering and GUI injection
223 // Only register after content blocking is initialized
224 app.webView.RegisterURIChangedHandler(func(uri string) {
225 // Add small delay to avoid conflicts with page load
226 go func() {
227 time.Sleep(200 * time.Millisecond) // Slightly longer delay for stability
228
229 // Apply content filtering if available
230 if app.filterManager != nil {
231 app.webView.OnNavigate(uri, app.filterManager)
232 }
233
234 // Note: GUI bundle (controls and toast) is now injected as User Script
235 // in WebKit's enableUserContentManager, so it persists across all navigations
236 }()
237 })
238 }()
239
240 log.Printf("Content blocking system initialization started")
241 return nil
242}