StashDB Studio Filter

Hide scenes from specific studios on performer pages

Size

9.9 KB

Version

1.1.3

Created

Feb 7, 2026

Updated

about 1 month ago

1// ==UserScript==
2// @name		StashDB Studio Filter
3// @description		Hide scenes from specific studios on performer pages
4// @version		1.1.3
5// @match		https://*.stashdb.org/*
6// @grant		GM.getValue
7// @grant		GM.setValue
8// ==/UserScript==
9(function() {
10    'use strict';
11
12    console.log('StashDB Studio Filter: Extension loaded');
13
14    // Storage key for hidden studios
15    const STORAGE_KEY = 'stashdb_hidden_studios';
16
17    // Load hidden studios from storage
18    async function loadHiddenStudios() {
19        try {
20            const stored = await GM.getValue(STORAGE_KEY, '[]');
21            const hiddenStudios = JSON.parse(stored);
22            console.log('Loaded hidden studios:', hiddenStudios);
23            return hiddenStudios;
24        } catch (error) {
25            console.error('Error loading hidden studios:', error);
26            return [];
27        }
28    }
29
30    // Save hidden studios to storage
31    async function saveHiddenStudios(hiddenStudios) {
32        try {
33            await GM.setValue(STORAGE_KEY, JSON.stringify(hiddenStudios));
34            console.log('Saved hidden studios:', hiddenStudios);
35        } catch (error) {
36            console.error('Error saving hidden studios:', error);
37        }
38    }
39
40    // Toggle studio visibility
41    async function toggleStudio(studioName, studioUrl) {
42        const hiddenStudios = await loadHiddenStudios();
43        const studioData = { name: studioName, url: studioUrl };
44        const index = hiddenStudios.findIndex(s => s.url === studioUrl);
45
46        if (index > -1) {
47            // Studio is hidden, show it
48            hiddenStudios.splice(index, 1);
49            console.log('Showing studio:', studioName);
50        } else {
51            // Studio is visible, hide it
52            hiddenStudios.push(studioData);
53            console.log('Hiding studio:', studioName);
54        }
55
56        await saveHiddenStudios(hiddenStudios);
57        await applyFilters();
58    }
59
60    // Apply filters to hide/show scenes
61    async function applyFilters() {
62        const hiddenStudios = await loadHiddenStudios();
63        const hiddenUrls = hiddenStudios.map(s => s.url);
64
65        // Find all scene cards
66        const sceneCards = document.querySelectorAll('.SceneCard.card');
67        console.log(`Found ${sceneCards.length} scene cards`);
68
69        let hiddenCount = 0;
70        sceneCards.forEach(card => {
71            const studioLink = card.querySelector('a.SceneCard-studio-name[href^="/studios/"]');
72            if (studioLink) {
73                const studioUrl = studioLink.getAttribute('href');
74                const parentCol = card.closest('.col-3');
75                
76                if (hiddenUrls.includes(studioUrl)) {
77                    // Hide the scene
78                    if (parentCol) {
79                        parentCol.style.display = 'none';
80                    }
81                    hiddenCount++;
82                } else {
83                    // Show the scene
84                    if (parentCol) {
85                        parentCol.style.display = '';
86                    }
87                }
88            }
89        });
90
91        console.log(`Hidden ${hiddenCount} scenes from ${hiddenStudios.length} studios`);
92        
93        // Update button states
94        await updateButtonStates();
95    }
96
97    // Update button states based on current hidden studios
98    async function updateButtonStates() {
99        const hiddenStudios = await loadHiddenStudios();
100        const hiddenUrls = hiddenStudios.map(s => s.url);
101
102        const buttons = document.querySelectorAll('.studio-hide-btn');
103        buttons.forEach(button => {
104            const studioUrl = button.getAttribute('data-studio-url');
105            const isHidden = hiddenUrls.includes(studioUrl);
106            
107            if (isHidden) {
108                button.textContent = '👁️';
109                button.title = 'Show scenes from this studio';
110                button.style.backgroundColor = '#dc3545';
111            } else {
112                button.textContent = '🚫';
113                button.title = 'Hide scenes from this studio';
114                button.style.backgroundColor = '#6c757d';
115            }
116        });
117    }
118
119    // Add hide buttons next to studio names
120    function addHideButtons() {
121        const studioLinks = document.querySelectorAll('a.SceneCard-studio-name[href^="/studios/"]:not(.studio-filter-processed)');
122        console.log(`Adding hide buttons to ${studioLinks.length} studio links`);
123
124        studioLinks.forEach(link => {
125            link.classList.add('studio-filter-processed');
126            
127            const studioName = link.textContent.trim();
128            const studioUrl = link.getAttribute('href');
129
130            // Create hide button
131            const hideButton = document.createElement('button');
132            hideButton.className = 'studio-hide-btn';
133            hideButton.setAttribute('data-studio-name', studioName);
134            hideButton.setAttribute('data-studio-url', studioUrl);
135            hideButton.style.cssText = `
136                margin-left: 5px;
137                padding: 2px 6px;
138                font-size: 12px;
139                border: none;
140                border-radius: 3px;
141                cursor: pointer;
142                background-color: #6c757d;
143                color: white;
144                transition: all 0.2s;
145            `;
146            hideButton.textContent = '🚫';
147            hideButton.title = 'Hide scenes from this studio';
148
149            // Add hover effect
150            hideButton.addEventListener('mouseenter', () => {
151                hideButton.style.transform = 'scale(1.1)';
152            });
153            hideButton.addEventListener('mouseleave', () => {
154                hideButton.style.transform = 'scale(1)';
155            });
156
157            // Add click handler
158            hideButton.addEventListener('click', async (e) => {
159                e.preventDefault();
160                e.stopPropagation();
161                await toggleStudio(studioName, studioUrl);
162            });
163
164            // Insert button after the studio link
165            link.parentNode.insertBefore(hideButton, link.nextSibling);
166        });
167    }
168
169    // Debounce function to avoid excessive calls
170    function debounce(func, wait) {
171        let timeout;
172        return function executedFunction(...args) {
173            const later = () => {
174                clearTimeout(timeout);
175                func(...args);
176            };
177            clearTimeout(timeout);
178            timeout = setTimeout(later, wait);
179        };
180    }
181
182    // Initialize the extension
183    async function init() {
184        console.log('StashDB Studio Filter: Initializing...');
185        
186        // Wait a bit for the page to fully render
187        await new Promise(resolve => setTimeout(resolve, 1000));
188        
189        // Add buttons to existing studio links
190        addHideButtons();
191        
192        // Apply filters to hide scenes
193        await applyFilters();
194
195        // Watch for DOM changes to handle dynamically loaded content
196        const observer = new MutationObserver(debounce(async (mutations) => {
197            let shouldUpdate = false;
198            
199            for (const mutation of mutations) {
200                if (mutation.addedNodes.length > 0) {
201                    for (const node of mutation.addedNodes) {
202                        if (node.nodeType === 1) { // Element node
203                            if (node.classList && node.classList.contains('SceneCard')) {
204                                shouldUpdate = true;
205                                break;
206                            }
207                            if (node.querySelector && node.querySelector('.SceneCard')) {
208                                shouldUpdate = true;
209                                break;
210                            }
211                            // Check for row container that holds scene cards
212                            if (node.classList && node.classList.contains('row')) {
213                                shouldUpdate = true;
214                                break;
215                            }
216                            // Check for col-3 which contains scene cards
217                            if (node.classList && node.classList.contains('col-3')) {
218                                shouldUpdate = true;
219                                break;
220                            }
221                        }
222                    }
223                }
224                // Also check if nodes were removed (pagination change)
225                if (mutation.removedNodes.length > 0) {
226                    for (const node of mutation.removedNodes) {
227                        if (node.nodeType === 1) {
228                            if (node.classList && (node.classList.contains('SceneCard') || node.classList.contains('col-3'))) {
229                                shouldUpdate = true;
230                                break;
231                            }
232                        }
233                    }
234                }
235            }
236
237            if (shouldUpdate) {
238                console.log('DOM changed, updating buttons and filters');
239                addHideButtons();
240                await applyFilters();
241            }
242        }, 300));
243
244        // Start observing
245        observer.observe(document.body, {
246            childList: true,
247            subtree: true
248        });
249
250        // Watch for URL changes (for client-side navigation)
251        let lastUrl = location.href;
252        new MutationObserver(() => {
253            const currentUrl = location.href;
254            if (currentUrl !== lastUrl) {
255                lastUrl = currentUrl;
256                console.log('URL changed, updating buttons and filters');
257                setTimeout(async () => {
258                    addHideButtons();
259                    await applyFilters();
260                }, 500);
261            }
262        }).observe(document, { subtree: true, childList: true });
263
264        console.log('StashDB Studio Filter: Initialized successfully');
265    }
266
267    // Wait for page to be ready
268    if (document.readyState === 'loading') {
269        document.addEventListener('DOMContentLoaded', init);
270    } else {
271        init();
272    }
273})();