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})();