v0.15.0 9006957

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