v0.8.0 0fa6785

injector.js RawHistoryFolder

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

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