Rule 34 Media Gallery Viewer

Advanced gallery viewer for Rule 34 sites with fullscreen mode, keyboard navigation, and thumbnail grid

Size

20.8 KB

Version

1.0.1

Created

Dec 28, 2025

Updated

24 days ago

1// ==UserScript==
2// @name		Rule 34 Media Gallery Viewer
3// @description		Advanced gallery viewer for Rule 34 sites with fullscreen mode, keyboard navigation, and thumbnail grid
4// @version		1.0.1
5// @match		https://*.rule34.xxx/*
6// @match		https://*.rule34.us/*
7// @match		https://*.rule34.paheal.net/*
8// @match		https://rule34.xxx/*
9// @match		https://rule34.us/*
10// @match		https://rule34.paheal.net/*
11// @icon		https://cdn.search.brave.com/serp/v3/_app/immutable/assets/favicon.acxxetWH.ico
12// ==/UserScript==
13(function() {
14    'use strict';
15
16    // Gallery state
17    let galleryState = {
18        mediaItems: [],
19        currentIndex: 0,
20        isOpen: false,
21        isFullscreen: false,
22        showThumbnails: false
23    };
24
25    // Utility function for debouncing
26    function debounce(func, wait) {
27        let timeout;
28        return function executedFunction(...args) {
29            const later = () => {
30                clearTimeout(timeout);
31                func(...args);
32            };
33            clearTimeout(timeout);
34            timeout = setTimeout(later, wait);
35        };
36    }
37
38    // Add gallery styles
39    function addStyles() {
40        TM_addStyle(`
41            /* Gallery Container */
42            #r34-gallery-overlay {
43                position: fixed;
44                top: 0;
45                left: 0;
46                width: 100%;
47                height: 100%;
48                background: rgba(0, 0, 0, 0.95);
49                z-index: 999999;
50                display: none;
51                flex-direction: column;
52            }
53
54            #r34-gallery-overlay.active {
55                display: flex;
56            }
57
58            /* Gallery Header */
59            #r34-gallery-header {
60                display: flex;
61                justify-content: space-between;
62                align-items: center;
63                padding: 15px 20px;
64                background: rgba(0, 0, 0, 0.8);
65                color: white;
66                border-bottom: 1px solid rgba(255, 255, 255, 0.1);
67            }
68
69            #r34-gallery-counter {
70                font-size: 16px;
71                font-weight: 500;
72            }
73
74            #r34-gallery-controls {
75                display: flex;
76                gap: 15px;
77            }
78
79            .r34-gallery-btn {
80                background: rgba(255, 255, 255, 0.1);
81                border: 1px solid rgba(255, 255, 255, 0.2);
82                color: white;
83                padding: 8px 16px;
84                border-radius: 5px;
85                cursor: pointer;
86                font-size: 14px;
87                transition: all 0.2s;
88            }
89
90            .r34-gallery-btn:hover {
91                background: rgba(255, 255, 255, 0.2);
92                border-color: rgba(255, 255, 255, 0.4);
93            }
94
95            .r34-gallery-btn.active {
96                background: rgba(66, 133, 244, 0.8);
97                border-color: rgba(66, 133, 244, 1);
98            }
99
100            /* Main Viewer */
101            #r34-gallery-main {
102                flex: 1;
103                display: flex;
104                align-items: center;
105                justify-content: center;
106                position: relative;
107                overflow: hidden;
108            }
109
110            #r34-gallery-media-container {
111                max-width: 90%;
112                max-height: 90%;
113                display: flex;
114                align-items: center;
115                justify-content: center;
116            }
117
118            #r34-gallery-media-container img,
119            #r34-gallery-media-container video {
120                max-width: 100%;
121                max-height: 100%;
122                object-fit: contain;
123            }
124
125            /* Navigation Arrows */
126            .r34-gallery-nav {
127                position: absolute;
128                top: 50%;
129                transform: translateY(-50%);
130                background: rgba(0, 0, 0, 0.7);
131                color: white;
132                border: 2px solid rgba(255, 255, 255, 0.3);
133                width: 50px;
134                height: 50px;
135                border-radius: 50%;
136                cursor: pointer;
137                display: flex;
138                align-items: center;
139                justify-content: center;
140                font-size: 24px;
141                transition: all 0.2s;
142                z-index: 10;
143            }
144
145            .r34-gallery-nav:hover {
146                background: rgba(0, 0, 0, 0.9);
147                border-color: rgba(255, 255, 255, 0.6);
148                transform: translateY(-50%) scale(1.1);
149            }
150
151            .r34-gallery-nav.prev {
152                left: 20px;
153            }
154
155            .r34-gallery-nav.next {
156                right: 20px;
157            }
158
159            /* Thumbnail Grid */
160            #r34-gallery-thumbnails {
161                display: none;
162                grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
163                gap: 10px;
164                padding: 20px;
165                background: rgba(0, 0, 0, 0.9);
166                max-height: 300px;
167                overflow-y: auto;
168                border-top: 1px solid rgba(255, 255, 255, 0.1);
169            }
170
171            #r34-gallery-thumbnails.active {
172                display: grid;
173            }
174
175            .r34-thumbnail {
176                width: 100%;
177                height: 120px;
178                object-fit: cover;
179                cursor: pointer;
180                border: 2px solid transparent;
181                border-radius: 5px;
182                transition: all 0.2s;
183            }
184
185            .r34-thumbnail:hover {
186                border-color: rgba(66, 133, 244, 0.8);
187                transform: scale(1.05);
188            }
189
190            .r34-thumbnail.active {
191                border-color: rgba(66, 133, 244, 1);
192                box-shadow: 0 0 10px rgba(66, 133, 244, 0.5);
193            }
194
195            /* Loading Indicator */
196            .r34-loading {
197                color: white;
198                font-size: 18px;
199                text-align: center;
200            }
201
202            /* Open Gallery Button */
203            #r34-open-gallery-btn {
204                position: fixed;
205                bottom: 20px;
206                right: 20px;
207                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
208                color: white;
209                border: none;
210                padding: 15px 25px;
211                border-radius: 50px;
212                cursor: pointer;
213                font-size: 16px;
214                font-weight: 600;
215                box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
216                z-index: 999998;
217                transition: all 0.3s;
218                display: none;
219            }
220
221            #r34-open-gallery-btn:hover {
222                transform: translateY(-2px);
223                box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
224            }
225
226            #r34-open-gallery-btn.visible {
227                display: block;
228            }
229
230            /* Fullscreen mode */
231            #r34-gallery-overlay.fullscreen #r34-gallery-header {
232                display: none;
233            }
234
235            #r34-gallery-overlay.fullscreen #r34-gallery-thumbnails {
236                display: none;
237            }
238
239            #r34-gallery-overlay.fullscreen #r34-gallery-media-container {
240                max-width: 100%;
241                max-height: 100%;
242            }
243
244            /* Scrollbar styling */
245            #r34-gallery-thumbnails::-webkit-scrollbar {
246                width: 8px;
247            }
248
249            #r34-gallery-thumbnails::-webkit-scrollbar-track {
250                background: rgba(0, 0, 0, 0.3);
251            }
252
253            #r34-gallery-thumbnails::-webkit-scrollbar-thumb {
254                background: rgba(255, 255, 255, 0.3);
255                border-radius: 4px;
256            }
257
258            #r34-gallery-thumbnails::-webkit-scrollbar-thumb:hover {
259                background: rgba(255, 255, 255, 0.5);
260            }
261        `);
262    }
263
264    // Create gallery HTML structure
265    function createGalleryHTML() {
266        const galleryHTML = `
267            <div id="r34-gallery-overlay">
268                <div id="r34-gallery-header">
269                    <div id="r34-gallery-counter">0 / 0</div>
270                    <div id="r34-gallery-controls">
271                        <button class="r34-gallery-btn" id="r34-toggle-thumbnails">Thumbnails</button>
272                        <button class="r34-gallery-btn" id="r34-toggle-fullscreen">Fullscreen</button>
273                        <button class="r34-gallery-btn" id="r34-close-gallery">Close (Esc)</button>
274                    </div>
275                </div>
276                <div id="r34-gallery-main">
277                    <div class="r34-gallery-nav prev">‹</div>
278                    <div id="r34-gallery-media-container">
279                        <div class="r34-loading">Loading...</div>
280                    </div>
281                    <div class="r34-gallery-nav next">›</div>
282                </div>
283                <div id="r34-gallery-thumbnails"></div>
284            </div>
285            <button id="r34-open-gallery-btn">📷 Open Gallery</button>
286        `;
287
288        document.body.insertAdjacentHTML('beforeend', galleryHTML);
289        console.log('Gallery HTML created');
290    }
291
292    // Detect media items on the page
293    function detectMediaItems() {
294        const mediaItems = [];
295        
296        // Common selectors for Rule 34 sites
297        const imageSelectors = [
298            'img[src*="rule34"]',
299            'img[src*="//us.rule34"]',
300            'img[src*="//wimg.rule34"]',
301            'img.preview',
302            'img[id*="image"]',
303            'a[href*="images"] img',
304            '.thumb img',
305            '.content img',
306            'img[src*="thumbnail"]',
307            'img[src*="sample"]'
308        ];
309
310        const videoSelectors = [
311            'video',
312            'video[src*="rule34"]',
313            'source[src*=".mp4"]',
314            'source[src*=".webm"]'
315        ];
316
317        // Find images
318        imageSelectors.forEach(selector => {
319            try {
320                const images = document.querySelectorAll(selector);
321                images.forEach(img => {
322                    if (img.src && img.src.startsWith('http') && img.width > 50 && img.height > 50) {
323                        // Try to find full-size image link
324                        let fullSrc = img.src;
325                        const parent = img.closest('a');
326                        if (parent && parent.href && (parent.href.includes('image') || parent.href.includes('.jpg') || parent.href.includes('.png'))) {
327                            fullSrc = parent.href;
328                        }
329                        
330                        // Replace thumbnail URLs with full-size
331                        fullSrc = fullSrc.replace('/thumbnails/', '/images/');
332                        fullSrc = fullSrc.replace('/thumbnail_', '/');
333                        fullSrc = fullSrc.replace('thumbnail=', '');
334                        
335                        if (!mediaItems.some(item => item.src === fullSrc)) {
336                            mediaItems.push({
337                                type: 'image',
338                                src: fullSrc,
339                                thumbnail: img.src
340                            });
341                        }
342                    }
343                });
344            } catch (e) {
345                console.error('Error with selector:', selector, e);
346            }
347        });
348
349        // Find videos
350        videoSelectors.forEach(selector => {
351            try {
352                const videos = document.querySelectorAll(selector);
353                videos.forEach(video => {
354                    let src = video.src || (video.querySelector('source') && video.querySelector('source').src);
355                    if (src && src.startsWith('http')) {
356                        if (!mediaItems.some(item => item.src === src)) {
357                            mediaItems.push({
358                                type: 'video',
359                                src: src,
360                                thumbnail: video.poster || src
361                            });
362                        }
363                    }
364                });
365            } catch (e) {
366                console.error('Error with selector:', selector, e);
367            }
368        });
369
370        console.log(`Detected ${mediaItems.length} media items`);
371        return mediaItems;
372    }
373
374    // Update gallery counter
375    function updateCounter() {
376        const counter = document.getElementById('r34-gallery-counter');
377        if (counter) {
378            counter.textContent = `${galleryState.currentIndex + 1} / ${galleryState.mediaItems.length}`;
379        }
380    }
381
382    // Display current media
383    function displayMedia(index) {
384        const container = document.getElementById('r34-gallery-media-container');
385        if (!container || !galleryState.mediaItems[index]) return;
386
387        const media = galleryState.mediaItems[index];
388        galleryState.currentIndex = index;
389
390        container.innerHTML = '<div class="r34-loading">Loading...</div>';
391
392        if (media.type === 'image') {
393            const img = document.createElement('img');
394            img.src = media.src;
395            img.onload = () => {
396                container.innerHTML = '';
397                container.appendChild(img);
398            };
399            img.onerror = () => {
400                container.innerHTML = '<div class="r34-loading">Failed to load image</div>';
401            };
402        } else if (media.type === 'video') {
403            const video = document.createElement('video');
404            video.src = media.src;
405            video.controls = true;
406            video.autoplay = true;
407            video.loop = true;
408            video.onloadeddata = () => {
409                container.innerHTML = '';
410                container.appendChild(video);
411            };
412            video.onerror = () => {
413                container.innerHTML = '<div class="r34-loading">Failed to load video</div>';
414            };
415        }
416
417        updateCounter();
418        updateThumbnails();
419    }
420
421    // Navigate to next/previous media
422    function navigateGallery(direction) {
423        let newIndex = galleryState.currentIndex + direction;
424        
425        if (newIndex < 0) {
426            newIndex = galleryState.mediaItems.length - 1;
427        } else if (newIndex >= galleryState.mediaItems.length) {
428            newIndex = 0;
429        }
430
431        displayMedia(newIndex);
432    }
433
434    // Create thumbnail grid
435    function createThumbnails() {
436        const thumbnailContainer = document.getElementById('r34-gallery-thumbnails');
437        if (!thumbnailContainer) return;
438
439        thumbnailContainer.innerHTML = '';
440
441        galleryState.mediaItems.forEach((media, index) => {
442            const thumb = document.createElement('img');
443            thumb.src = media.thumbnail;
444            thumb.className = 'r34-thumbnail';
445            thumb.onclick = () => displayMedia(index);
446            thumbnailContainer.appendChild(thumb);
447        });
448
449        console.log('Thumbnails created');
450    }
451
452    // Update active thumbnail
453    function updateThumbnails() {
454        const thumbnails = document.querySelectorAll('.r34-thumbnail');
455        thumbnails.forEach((thumb, index) => {
456            if (index === galleryState.currentIndex) {
457                thumb.classList.add('active');
458            } else {
459                thumb.classList.remove('active');
460            }
461        });
462    }
463
464    // Toggle thumbnail view
465    function toggleThumbnails() {
466        galleryState.showThumbnails = !galleryState.showThumbnails;
467        const thumbnailContainer = document.getElementById('r34-gallery-thumbnails');
468        const toggleBtn = document.getElementById('r34-toggle-thumbnails');
469        
470        if (galleryState.showThumbnails) {
471            thumbnailContainer.classList.add('active');
472            toggleBtn.classList.add('active');
473        } else {
474            thumbnailContainer.classList.remove('active');
475            toggleBtn.classList.remove('active');
476        }
477    }
478
479    // Toggle fullscreen mode
480    function toggleFullscreen() {
481        galleryState.isFullscreen = !galleryState.isFullscreen;
482        const overlay = document.getElementById('r34-gallery-overlay');
483        const toggleBtn = document.getElementById('r34-toggle-fullscreen');
484        
485        if (galleryState.isFullscreen) {
486            overlay.classList.add('fullscreen');
487            toggleBtn.classList.add('active');
488        } else {
489            overlay.classList.remove('fullscreen');
490            toggleBtn.classList.remove('active');
491        }
492    }
493
494    // Open gallery
495    function openGallery() {
496        if (galleryState.mediaItems.length === 0) {
497            console.log('No media items found');
498            return;
499        }
500
501        galleryState.isOpen = true;
502        const overlay = document.getElementById('r34-gallery-overlay');
503        overlay.classList.add('active');
504        
505        displayMedia(0);
506        createThumbnails();
507        
508        console.log('Gallery opened');
509    }
510
511    // Close gallery
512    function closeGallery() {
513        galleryState.isOpen = false;
514        galleryState.isFullscreen = false;
515        galleryState.showThumbnails = false;
516        
517        const overlay = document.getElementById('r34-gallery-overlay');
518        overlay.classList.remove('active', 'fullscreen');
519        
520        // Stop any playing videos
521        const video = overlay.querySelector('video');
522        if (video) {
523            video.pause();
524        }
525        
526        console.log('Gallery closed');
527    }
528
529    // Setup event listeners
530    function setupEventListeners() {
531        // Navigation buttons
532        document.querySelector('.r34-gallery-nav.prev').addEventListener('click', () => navigateGallery(-1));
533        document.querySelector('.r34-gallery-nav.next').addEventListener('click', () => navigateGallery(1));
534
535        // Control buttons
536        document.getElementById('r34-close-gallery').addEventListener('click', closeGallery);
537        document.getElementById('r34-toggle-thumbnails').addEventListener('click', toggleThumbnails);
538        document.getElementById('r34-toggle-fullscreen').addEventListener('click', toggleFullscreen);
539        document.getElementById('r34-open-gallery-btn').addEventListener('click', openGallery);
540
541        // Keyboard shortcuts
542        document.addEventListener('keydown', (e) => {
543            if (!galleryState.isOpen) return;
544
545            switch(e.key) {
546                case 'Escape':
547                    closeGallery();
548                    break;
549                case 'ArrowLeft':
550                    navigateGallery(-1);
551                    break;
552                case 'ArrowRight':
553                    navigateGallery(1);
554                    break;
555                case 'f':
556                case 'F':
557                    toggleFullscreen();
558                    break;
559                case 't':
560                case 'T':
561                    toggleThumbnails();
562                    break;
563            }
564        });
565
566        // Close on overlay click (but not on media)
567        document.getElementById('r34-gallery-main').addEventListener('click', (e) => {
568            if (e.target.id === 'r34-gallery-main') {
569                closeGallery();
570            }
571        });
572
573        console.log('Event listeners setup complete');
574    }
575
576    // Initialize gallery
577    function init() {
578        console.log('Rule 34 Media Gallery Viewer initializing...');
579        
580        // Add styles
581        addStyles();
582        
583        // Wait for page to load
584        if (document.readyState === 'loading') {
585            document.addEventListener('DOMContentLoaded', () => {
586                setTimeout(initGallery, 1000);
587            });
588        } else {
589            setTimeout(initGallery, 1000);
590        }
591    }
592
593    function initGallery() {
594        // Create gallery UI
595        createGalleryHTML();
596        
597        // Detect media
598        galleryState.mediaItems = detectMediaItems();
599        
600        // Show open button if media found
601        if (galleryState.mediaItems.length > 0) {
602            const openBtn = document.getElementById('r34-open-gallery-btn');
603            openBtn.classList.add('visible');
604            openBtn.textContent = `📷 Open Gallery (${galleryState.mediaItems.length})`;
605        }
606        
607        // Setup event listeners
608        setupEventListeners();
609        
610        // Re-detect media on DOM changes
611        const observer = new MutationObserver(debounce(() => {
612            const newMedia = detectMediaItems();
613            if (newMedia.length !== galleryState.mediaItems.length) {
614                galleryState.mediaItems = newMedia;
615                const openBtn = document.getElementById('r34-open-gallery-btn');
616                if (galleryState.mediaItems.length > 0) {
617                    openBtn.classList.add('visible');
618                    openBtn.textContent = `📷 Open Gallery (${galleryState.mediaItems.length})`;
619                }
620                console.log(`Media updated: ${galleryState.mediaItems.length} items`);
621            }
622        }, 2000));
623        
624        observer.observe(document.body, {
625            childList: true,
626            subtree: true
627        });
628        
629        console.log('Gallery initialized successfully');
630    }
631
632    // Start the extension
633    init();
634})();