v0.12.0 d035193

injector.js RawHistoryFolder

1// Guard against duplicate injection in multiple frames
2if (typeof window.__DUMBER_COSMETIC_LOADED === 'undefined') {
3window.__DUMBER_COSMETIC_LOADED = true;
4
5const CosmeticFilter = (() => {
6    const hiddenElements = new WeakSet();
7    const selectors = {
8        generic: [],      // Apply everywhere
9        specific: [],     // Domain-specific
10        procedural: []    // Complex selectors with :has(), etc.
11    };
12
13    // Safe anti-breakage scriptlets that avoid WebKit internal interference
14    const antiBreakage = {
15        // Safely stub ad services without modifying global setTimeout
16        neutralizeAdPromises() {
17            try {
18                // Use WeakMap to avoid direct property modification
19                const stubServices = new WeakMap();
20
21                // Only stub if not already defined to avoid conflicts
22                if (typeof window.googletag === 'undefined') {
23                    window.googletag = {
24                        cmd: [],
25                        push: () => {},
26                        enableServices: () => {},
27                        display: () => {},
28                        defineSlot: () => ({ addService: () => {}, setTargeting: () => {} })
29                    };
30                }
31
32                // Stub Google Analytics safely
33                if (typeof window.gtag === 'undefined') {
34                    window.gtag = function() {};
35                }
36                if (typeof window.ga === 'undefined') {
37                    window.ga = function() {};
38                }
39            } catch (e) {
40                // Fail silently to avoid breaking pages
41                console.debug('[dumber] Anti-breakage initialization failed:', e);
42            }
43        },
44
45        // Clean up loading indicators after page load
46        cleanupLoadingIndicators() {
47            // Wait for page to be more stable before cleanup
48            const cleanup = () => {
49                try {
50                    // Only hide elements that are clearly loading indicators
51                    const loadingSelectors = [
52                        '.loading:not([role])', // Avoid hiding important loading states
53                        '.spinner:not([aria-live])',
54                        '.loader:not([aria-live])',
55                        '[class*="ad-loading"]:not([aria-live])'
56                    ];
57
58                    loadingSelectors.forEach(selector => {
59                        document.querySelectorAll(selector).forEach(el => {
60                            // Check if element is actually a loading indicator
61                            const rect = el.getBoundingClientRect();
62                            if (rect.width > 0 && rect.height > 0) {
63                                el.style.opacity = '0.3';
64                                el.style.transition = 'opacity 0.5s';
65                            }
66                        });
67                    });
68                } catch (e) {
69                    console.debug('[dumber] Loading cleanup failed:', e);
70                }
71            };
72
73            // Defer cleanup to avoid interfering with page initialization
74            if (document.readyState === 'complete') {
75                setTimeout(cleanup, 2000);
76            } else {
77                window.addEventListener('load', () => setTimeout(cleanup, 2000));
78            }
79        }
80    };
81
82    // High-performance element hiding
83    const hideElements = (elements) => {
84        const styleId = 'dumber-cosmetic-style';
85        let styleEl = document.getElementById(styleId);
86
87        if (!styleEl) {
88            styleEl = document.createElement('style');
89            styleEl.id = styleId;
90            styleEl.textContent = '';
91            (document.head || document.documentElement).appendChild(styleEl);
92        }
93
94        // Batch CSS injection for performance
95        const cssRules = elements.map(selector =>
96            `${selector} { display: none !important; }`
97        ).join('\n');
98
99        styleEl.textContent += cssRules;
100    };
101
102    // Handle complex procedural selectors
103    const applyProceduralFilters = () => {
104        selectors.procedural.forEach(rule => {
105            if (rule.includes(':has(')) {
106                // Handle :has() pseudo-selector
107                const match = rule.match(/(.+):has\((.+)\)/);
108                if (match) {
109                    const [_, parent, child] = match;
110                    document.querySelectorAll(parent).forEach(el => {
111                        if (el.querySelector(child)) {
112                            el.style.display = 'none';
113                            hiddenElements.add(el);
114                        }
115                    });
116                }
117            } else if (rule.includes(':not(')) {
118                // Handle :not() pseudo-selector
119                try {
120                    document.querySelectorAll(rule).forEach(el => {
121                        el.style.display = 'none';
122                        hiddenElements.add(el);
123                    });
124                } catch (e) {
125                    // Invalid selector, skip
126                }
127            }
128        });
129    };
130
131    // MutationObserver for lazy-loaded content
132    const observer = new MutationObserver((mutations) => {
133        // Debounce for performance
134        if (observer.timeout) {
135            clearTimeout(observer.timeout);
136        }
137
138        observer.timeout = setTimeout(() => {
139            // Check if new elements match our selectors
140            const allSelectors = [...selectors.generic, ...selectors.specific];
141
142            mutations.forEach(mutation => {
143                // Handle added nodes
144                mutation.addedNodes.forEach(node => {
145                    if (node.nodeType !== 1) return; // Only element nodes
146
147                    // Check the node itself
148                    allSelectors.forEach(selector => {
149                        if (node.matches && node.matches(selector)) {
150                            node.style.display = 'none';
151                            hiddenElements.add(node);
152                        }
153                    });
154
155                    // Check descendants
156                    if (node.querySelectorAll) {
157                        allSelectors.forEach(selector => {
158                            node.querySelectorAll(selector).forEach(el => {
159                                el.style.display = 'none';
160                                hiddenElements.add(el);
161                            });
162                        });
163                    }
164                });
165
166                // Handle attribute changes (for dynamic ads)
167                if (mutation.type === 'attributes') {
168                    const target = mutation.target;
169                    allSelectors.forEach(selector => {
170                        if (target.matches && target.matches(selector)) {
171                            target.style.display = 'none';
172                            hiddenElements.add(target);
173                        }
174                    });
175                }
176            });
177
178            // Reapply procedural filters
179            applyProceduralFilters();
180        }, 50); // 50ms debounce
181    });
182
183    // Initialize cosmetic filtering
184    const init = (rules) => {
185        // Initialize safe anti-breakage scriptlets
186        antiBreakage.neutralizeAdPromises();
187        antiBreakage.cleanupLoadingIndicators();
188
189        // Parse and categorize rules (with null safety)
190        if (rules && Array.isArray(rules)) {
191            rules.forEach(rule => {
192                if (rule.includes(':has(') || rule.includes(':not(')) {
193                    selectors.procedural.push(rule);
194                } else if (rule.domain === window.location.hostname) {
195                    selectors.specific.push(rule.selector);
196                } else {
197                    selectors.generic.push(rule.selector);
198                }
199            });
200        }
201
202        // Apply initial hiding
203        hideElements([...selectors.generic, ...selectors.specific]);
204        applyProceduralFilters();
205
206        // Start observing for changes
207        observer.observe(document.documentElement, {
208            childList: true,
209            subtree: true,
210            attributes: true,
211            attributeFilter: ['class', 'id', 'style']
212        });
213    };
214
215    // Performance optimization: batch rule updates
216    const updateRules = (newRules) => {
217        // Diff and apply only new rules
218        const newSelectors = newRules.filter(r =>
219            !selectors.generic.includes(r) &&
220            !selectors.specific.includes(r)
221        );
222
223        if (newSelectors.length > 0) {
224            hideElements(newSelectors);
225            selectors.generic.push(...newSelectors);
226        }
227    };
228
229    // Cleanup function for navigation
230    const cleanup = () => {
231        observer.disconnect();
232        hiddenElements.clear();
233        const styleEl = document.getElementById('dumber-cosmetic-style');
234        if (styleEl) styleEl.remove();
235    };
236
237    return { init, updateRules, cleanup };
238})();
239
240// Inject rules from native
241window.__dumber_cosmetic_init = (rules) => CosmeticFilter.init(rules);
242window.__dumber_cosmetic_update = (rules) => CosmeticFilter.updateRules(rules);
243window.__dumber_cosmetic_cleanup = () => CosmeticFilter.cleanup();
244
245} // End guard against duplicate injection

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