v0.15.0 9006957

content_blocking.go RawHistoryFolder

1package webkit
2
3import (
4	"context"
5	"fmt"
6	"path/filepath"
7	"sync"
8
9	"github.com/bnema/dumber/internal/logging"
10	webkit "github.com/diamondburned/gotk4-webkitgtk/pkg/webkit/v6"
11	"github.com/diamondburned/gotk4/pkg/gio/v2"
12	"github.com/diamondburned/gotk4/pkg/glib/v2"
13)
14
15// ContentBlockingManager handles WebKit content filtering
16type ContentBlockingManager struct {
17	filterStore *webkit.UserContentFilterStore
18	storagePath string
19
20	// Cache compiled filter to avoid recompilation
21	mu             sync.RWMutex
22	compiledFilter *webkit.UserContentFilter
23	filterID       string
24}
25
26// NewContentBlockingManager creates a new content blocking manager
27func NewContentBlockingManager(storagePath string) (*ContentBlockingManager, error) {
28	// Create the filter store
29	filterStore := webkit.NewUserContentFilterStore(storagePath)
30	if filterStore == nil {
31		return nil, fmt.Errorf("failed to create UserContentFilterStore")
32	}
33
34	logging.Debug(fmt.Sprintf("[webkit] Created UserContentFilterStore at: %s", storagePath))
35
36	return &ContentBlockingManager{
37		filterStore: filterStore,
38		storagePath: storagePath,
39	}, nil
40}
41
42// CompileFilters compiles JSON filter rules and caches the result for reuse.
43// This should be called once during startup. Subsequent WebViews use ApplyCompiledFilter.
44func (cbm *ContentBlockingManager) CompileFilters(identifier string, jsonRules []byte, onComplete func(error)) {
45	if len(jsonRules) == 0 {
46		if onComplete != nil {
47			onComplete(fmt.Errorf("empty filter rules"))
48		}
49		return
50	}
51
52	logging.Debug(fmt.Sprintf("[webkit] Compiling content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules)))
53
54	// Store identifier for later use
55	cbm.mu.Lock()
56	cbm.filterID = identifier
57	cbm.mu.Unlock()
58
59	// Convert []byte to *glib.Bytes for WebKit API
60	gBytes := glib.NewBytesWithGo(jsonRules)
61
62	// Compile asynchronously - does not block main thread
63	ctx := context.Background()
64	cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
65		filter, err := cbm.filterStore.SaveFinish(result)
66		if err != nil {
67			logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
68			if onComplete != nil {
69				onComplete(fmt.Errorf("failed to compile filter: %w", err))
70			}
71			return
72		}
73
74		if filter == nil {
75			logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
76			if onComplete != nil {
77				onComplete(fmt.Errorf("filter compilation returned nil"))
78			}
79			return
80		}
81
82		// Cache the compiled filter
83		cbm.mu.Lock()
84		cbm.compiledFilter = filter
85		cbm.mu.Unlock()
86
87		logging.Debug(fmt.Sprintf("[webkit] Filter compilation complete: %s", identifier))
88		if onComplete != nil {
89			onComplete(nil)
90		}
91	})
92}
93
94// ApplyCompiledFilter applies the pre-compiled filter to a UserContentManager.
95// This is instant since the filter is already compiled.
96func (cbm *ContentBlockingManager) ApplyCompiledFilter(ucm *webkit.UserContentManager) error {
97	if ucm == nil {
98		return fmt.Errorf("UserContentManager is nil")
99	}
100
101	cbm.mu.RLock()
102	filter := cbm.compiledFilter
103	cbm.mu.RUnlock()
104
105	if filter == nil {
106		return fmt.Errorf("no compiled filter available")
107	}
108
109	ucm.AddFilter(filter)
110	return nil
111}
112
113// IsFilterCompiled returns true if filters have been compiled and are ready to apply.
114func (cbm *ContentBlockingManager) IsFilterCompiled() bool {
115	cbm.mu.RLock()
116	defer cbm.mu.RUnlock()
117	return cbm.compiledFilter != nil
118}
119
120// ApplyFiltersFromJSON compiles and applies WebKit JSON filter rules to a UserContentManager.
121// DEPRECATED: Use CompileFilters + ApplyCompiledFilter for better performance.
122// This method is kept for backward compatibility.
123func (cbm *ContentBlockingManager) ApplyFiltersFromJSON(ucm *webkit.UserContentManager, identifier string, jsonRules []byte) error {
124	if ucm == nil {
125		return fmt.Errorf("UserContentManager is nil")
126	}
127
128	// Check if already compiled
129	cbm.mu.RLock()
130	if cbm.compiledFilter != nil {
131		filter := cbm.compiledFilter
132		cbm.mu.RUnlock()
133		ucm.AddFilter(filter)
134		logging.Debug(fmt.Sprintf("[webkit] Applied cached content filter to UCM"))
135		return nil
136	}
137	cbm.mu.RUnlock()
138
139	if len(jsonRules) == 0 {
140		return fmt.Errorf("empty filter rules")
141	}
142
143	logging.Debug(fmt.Sprintf("[webkit] Applying content filters (identifier: %s, rules size: %d bytes)", identifier, len(jsonRules)))
144
145	// Convert []byte to *glib.Bytes for WebKit API
146	gBytes := glib.NewBytesWithGo(jsonRules)
147
148	ctx := context.Background()
149	done := make(chan error, 1)
150
151	cbm.filterStore.Save(ctx, identifier, gBytes, func(result gio.AsyncResulter) {
152		filter, err := cbm.filterStore.SaveFinish(result)
153		if err != nil {
154			logging.Error(fmt.Sprintf("[webkit] Failed to compile content filter: %v", err))
155			done <- fmt.Errorf("failed to compile filter: %w", err)
156			return
157		}
158
159		if filter == nil {
160			logging.Error(fmt.Sprintf("[webkit] Filter compilation returned nil"))
161			done <- fmt.Errorf("filter compilation returned nil")
162			return
163		}
164
165		// Cache for future use
166		cbm.mu.Lock()
167		cbm.compiledFilter = filter
168		cbm.filterID = identifier
169		cbm.mu.Unlock()
170
171		RunOnMainThread(func() {
172			ucm.AddFilter(filter)
173			logging.Debug(fmt.Sprintf("[webkit] Successfully added content filter: %s", identifier))
174			done <- nil
175		})
176	})
177
178	return <-done
179}
180
181// RemoveAllFilters removes all content filters from a UserContentManager
182func (cbm *ContentBlockingManager) RemoveAllFilters(ucm *webkit.UserContentManager) {
183	if ucm != nil {
184		ucm.RemoveAllFilters()
185		logging.Debug(fmt.Sprintf("[webkit] Removed all content filters"))
186	}
187}
188
189// GetFilterStore returns the underlying UserContentFilterStore
190func (cbm *ContentBlockingManager) GetFilterStore() *webkit.UserContentFilterStore {
191	return cbm.filterStore
192}
193
194// ApplyFiltersToWebView applies content blocking filters to a WebView
195func ApplyFiltersToWebView(view *WebView, filterJSON []byte) error {
196	if view == nil || view.view == nil {
197		return fmt.Errorf("invalid WebView")
198	}
199
200	// Get the UserContentManager
201	ucm := view.view.UserContentManager()
202	if ucm == nil {
203		return fmt.Errorf("UserContentManager is nil")
204	}
205
206	// Create content blocking manager
207	// Use a subdirectory of the cache dir for filter storage
208	dataDir := view.config.DataDir
209	if dataDir == "" {
210		return fmt.Errorf("WebView has no data directory configured")
211	}
212
213	filterStorePath := filepath.Join(dataDir, "content-filters")
214	cbm, err := NewContentBlockingManager(filterStorePath)
215	if err != nil {
216		return fmt.Errorf("failed to create content blocking manager: %w", err)
217	}
218
219	// Apply the filters
220	return cbm.ApplyFiltersFromJSON(ucm, "adblock-filters", filterJSON)
221}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close