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