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

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