1package webkit
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/logging"
9)
10
11// WebViewFactory creates WebView instances for the application.
12// It supports creating regular WebViews (optionally via pool) and
13// related WebViews that share session/cookies with a parent (for popups).
14type WebViewFactory struct {
15 wkCtx *WebKitContext
16 settings *SettingsManager
17 pool *WebViewPool
18 injector *ContentInjector
19 router *MessageRouter
20 filterApplier FilterApplier // Optional content filter applier
21}
22
23// NewWebViewFactory creates a new WebViewFactory.
24// The pool parameter is optional; if nil, WebViews are created directly.
25func NewWebViewFactory(
26 wkCtx *WebKitContext,
27 settings *SettingsManager,
28 pool *WebViewPool,
29 injector *ContentInjector,
30 router *MessageRouter,
31) *WebViewFactory {
32 return &WebViewFactory{
33 wkCtx: wkCtx,
34 settings: settings,
35 pool: pool,
36 injector: injector,
37 router: router,
38 }
39}
40
41// SetFilterApplier sets the content filter applier.
42// Filters will be applied to all newly created WebViews.
43func (f *WebViewFactory) SetFilterApplier(applier FilterApplier) {
44 f.filterApplier = applier
45 // Also propagate to the pool if present
46 if f.pool != nil {
47 f.pool.SetFilterApplier(applier)
48 }
49}
50
51// Create creates a new WebView instance.
52// If a pool is configured, it will try to acquire from the pool first.
53func (f *WebViewFactory) Create(ctx context.Context) (*WebView, error) {
54 log := logging.FromContext(ctx)
55
56 // Try pool first if available
57 if f.pool != nil {
58 wv, err := f.pool.Acquire(ctx)
59 if err == nil && wv != nil {
60 log.Debug().Uint64("id", uint64(wv.ID())).Msg("acquired webview from pool")
61 return wv, nil
62 }
63 log.Debug().Err(err).Msg("pool acquire failed, creating directly")
64 }
65
66 // Create directly
67 return f.createDirect(ctx)
68}
69
70// CreateRelated creates a WebView that shares session/cookies with the parent.
71// This is required for popup windows to maintain authentication state (OAuth).
72// Related WebViews bypass the pool since they must be linked to a specific parent.
73func (f *WebViewFactory) CreateRelated(ctx context.Context, parentID port.WebViewID) (*WebView, error) {
74 log := logging.FromContext(ctx)
75
76 // Look up parent WebView
77 parent := LookupWebView(parentID)
78 if parent == nil {
79 return nil, fmt.Errorf("parent webview %d not found", parentID)
80 }
81 if parent.IsDestroyed() {
82 return nil, fmt.Errorf("parent webview %d is destroyed", parentID)
83 }
84
85 // Create related WebView
86 wv, err := NewWebViewWithRelated(ctx, parent, f.settings)
87 if err != nil {
88 return nil, fmt.Errorf("create related webview: %w", err)
89 }
90
91 // Ensure visible
92 wv.inner.SetVisible(true)
93
94 // Attach frontend (scripts, message handler)
95 if err := wv.AttachFrontend(ctx, f.injector, f.router); err != nil {
96 log.Warn().Err(err).Uint64("id", uint64(wv.ID())).Msg("failed to attach frontend to related webview")
97 }
98
99 // Apply content filters if configured
100 if f.filterApplier != nil {
101 f.filterApplier.ApplyTo(ctx, wv.ucm)
102 }
103
104 log.Debug().
105 Uint64("id", uint64(wv.ID())).
106 Uint64("parent_id", uint64(parentID)).
107 Msg("created related webview for popup")
108
109 return wv, nil
110}
111
112// createDirect creates a WebView without using the pool.
113func (f *WebViewFactory) createDirect(ctx context.Context) (*WebView, error) {
114 log := logging.FromContext(ctx)
115
116 wv, err := NewWebView(ctx, f.wkCtx, f.settings)
117 if err != nil {
118 return nil, err
119 }
120
121 // Ensure visible
122 wv.inner.SetVisible(true)
123
124 // Attach frontend
125 if err := wv.AttachFrontend(ctx, f.injector, f.router); err != nil {
126 log.Warn().Err(err).Uint64("id", uint64(wv.ID())).Msg("failed to attach frontend to webview")
127 }
128
129 // Apply content filters if configured
130 if f.filterApplier != nil {
131 f.filterApplier.ApplyTo(ctx, wv.ucm)
132 }
133
134 log.Debug().Uint64("id", uint64(wv.ID())).Msg("created webview directly")
135 return wv, nil
136}
137
138// Release returns a WebView to the pool if available, otherwise destroys it.
139// Related WebViews (popups) should be destroyed directly, not released to pool.
140func (f *WebViewFactory) Release(ctx context.Context, wv *WebView) {
141 if wv == nil || wv.IsDestroyed() {
142 return
143 }
144
145 if f.pool != nil {
146 f.pool.Release(ctx, wv)
147 } else {
148 wv.Destroy()
149 }
150}