// ==UserScript==
// @name Rule34 - Reskin
// @namespace ko-fi.com/awesome97076
// @version 1
// @description Rule34 - Dark Masonry Reskin
// @author Awesome
// @match https://rule34.xxx/*
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=rule34.xxx
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
"use strict";
const style = document.createElement("style");
style.innerHTML = `
:root {
/* Dark theme color palette */
--bg-color: #121212;
--bg-secondary: #1e1e1e;
--bg-tertiary: #2d2d2d;
--accent-color: #9c64a6;
--accent-secondary: #ae81ff;
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--text-muted: #707070;
--border-color: rgba(255, 255, 255, 0.1);
--success-color: #50fa7b;
--warning-color: #ffb86c;
--error-color: #ff5555;
/* Tag type colors - vibrant but fitting dark theme */
--tag-artist: #ff79c6;
--tag-character: #50fa7b;
--tag-copyright: #bd93f9;
--tag-metadata: #f1fa8c;
/* Layout - Responsive defaults */
--container-width: 100%;
--container-max-width: 100%;
--column-count: 3; /* Start with max columns for large screens */
--column-gap: 3px;
--content-padding: 8px;
--border-radius: 6px;
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
/* Thumbnails */
--thumbnail-padding: 0;
--thumbnail-border-width: 2px;
--thumbnail-border-radius: 8px;
--thumbnail-hover-scale: 1.08;
--thumbnail-transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
--video-border-color: #8e44ad;
/* Typography - Responsive base */
--font-primary: 'Inter', 'Segoe UI', Roboto, -apple-system, BlinkMacSystemFont, sans-serif;
--font-secondary: 'Poppins', 'Segoe UI', Roboto, sans-serif;
--font-code: 'Fira Code', 'Cascadia Code', Consolas, monospace;
--font-size-base: clamp(14px, 2.5vw, 16px); /* Responsive font size */
--line-height: 1.6;
/* UI Elements */
--button-padding: 8px 16px;
--input-padding: 8px 12px;
--nav-height: 60px;
}
/* Basic resets and body styling */
html, body {
margin: 0;
padding: 0;
font-family: var(--font-primary);
font-size: var(--font-size-base);
line-height: var(--line-height);
background-color: var(--bg-color);
color: var(--text-primary);
min-width: 320px; /* Minimum supported width */
overflow-x: hidden; /* Prevent horizontal scroll */
}
body, div, h1, h2, h3, h4, h5, h6, p, ul, li, dd, dt {
font-family: var(--font-primary);
}
/* Apply border-box sizing to all elements for consistent sizing */
*, *::before, *::after {
box-sizing: border-box;
}
/* Content container - Fully responsive */
div#content {
width: 100%;
max-width: 100%;
margin: 0 auto;
padding: var(--content-padding);
box-sizing: border-box;
}
/* Header area modernization - Mobile first */
div#header {
background: var(--bg-secondary);
padding: 0;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
position: sticky;
top: 0;
z-index: 1000;
margin-bottom: clamp(16px, 3vw, 24px);
}
div#header #site-title {
background-image: none;
padding: clamp(10px, 2vw, 15px) clamp(15px, 3vw, 20px);
}
div#header #site-title a {
color: var(--accent-color);
font-size: clamp(20px, 4vw, 24px);
font-weight: bold;
font-family: var(--font-secondary);
word-wrap: break-word;
}
.tag-count {
color: var(--text-secondary);
font-size: clamp(12px, 2vw, 14px);
}
/* Navigation styling - Responsive */
div#header ul#navbar,
div#header ul#subnavbar {
background: var(--bg-secondary);
display: flex;
flex-wrap: wrap;
align-items: center;
list-style: none;
padding: 0 clamp(10px, 2vw, 20px);
margin: 0;
border-bottom: 1px solid var(--border-color);
gap: 0;
overflow-x: auto; /* Allow horizontal scroll on very small screens */
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
}
div#header ul#navbar li,
div#header ul#subnavbar li {
margin: 0;
padding: 0;
flex-shrink: 0;
}
div#header ul#navbar li a,
div#header ul#subnavbar li a {
display: block;
padding: clamp(10px, 2vw, 15px);
color: var(--text-secondary);
transition: color 0.2s ease;
text-decoration: none;
white-space: nowrap;
font-size: clamp(13px, 2.5vw, 15px);
}
div#header ul#navbar li a:hover,
div#header ul#subnavbar li a:hover {
color: var(--accent-color);
}
div#header ul#navbar li.current-page,
div#header ul#subnavbar li.current-page {
background: transparent;
}
div#header ul#navbar li.current-page a,
div#header ul#subnavbar li.current-page a {
color: var(--accent-color);
font-weight: bold;
}
/* Links styling */
a:link, a:visited {
color: var(--accent-color);
text-decoration: none;
transition: color 0.2s, text-decoration 0.2s;
}
a:hover, a:active {
color: var(--accent-secondary);
text-decoration: underline;
}
/* Search form styling - Responsive */
div.tag-search {
background: var(--bg-tertiary);
padding: clamp(10px, 2vw, 15px);
border-radius: var(--border-radius);
margin-bottom: clamp(15px, 3vw, 20px);
width: 100%;
max-width: 100%;
}
div.tag-search input[type="text"] {
width: 100%;
padding: var(--input-padding);
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
color: var(--text-primary);
font-family: var(--font-primary);
font-size: clamp(14px, 2.5vw, 16px);
margin-bottom: 10px;
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
}
div.tag-search input[type="submit"] {
padding: var(--button-padding);
background: var(--accent-color);
color: white;
border: none;
border-radius: var(--border-radius);
cursor: pointer;
font-weight: bold;
font-size: clamp(14px, 2.5vw, 16px);
transition: background 0.2s;
width: 100%;
max-width: 200px;
}
div.tag-search input[type="submit"]:hover {
background: var(--accent-secondary);
}
/* Tag styling */
.tag-type-artist a, .tag-type-artist {
color: var(--tag-artist) !important;
}
.tag-type-character a, .tag-type-character {
color: var(--tag-character) !important;
}
.tag-type-copyright a, .tag-type-copyright {
color: var(--tag-copyright) !important;
}
.tag-type-metadata a, .tag-type-metadata {
color: var(--tag-metadata) !important;
}
/* Masonry layout for the image grid - Fully responsive */
div.image-list {
display: block !important;
column-count: var(--column-count);
column-gap: var(--column-gap);
width: 100%;
max-width: 100%;
margin: 0 auto;
padding: 0;
}
/* Thumbnail container */
.thumb {
width: 100% !important;
height: auto !important;
display: inline-block !important;
break-inside: avoid;
margin-bottom: var(--column-gap);
padding: var(--thumbnail-padding);
box-sizing: border-box;
page-break-inside: avoid;
}
/* Thumbnail links */
.thumb a {
display: block !important;
position: relative;
overflow: hidden;
border-radius: var(--thumbnail-border-radius);
background: var(--bg-tertiary);
box-shadow: var(--box-shadow);
transition: var(--thumbnail-transition);
max-height: 100%;
height: 100%;
width: auto;
}
.thumb a:hover {
transform: scale(var(--thumbnail-hover-scale));
transition: var(--thumbnail-transition);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.4);
z-index: 9999;
}
/* Images within thumbnails */
.thumb img, .thumb video {
width: 100% !important;
height: auto !important;
max-height: 100% !important;
max-width: 100% !important;
display: block !important;
transition: var(--thumbnail-transition);
object-fit: contain;
}
/* Special styling for video thumbnails */
.webm-thumb {
border: var(--thumbnail-border-width) solid var(--video-border-color) !important;
border-radius: var(--thumbnail-border-radius) !important;
box-sizing: border-box !important;
}
/* Hover effect for video thumbnails */
.thumb a:hover .webm-thumb {
opacity: 0.9;
}
/* Enhanced responsive breakpoints for any screen size */
/* Ultra-wide screens (4K+) */
@media (min-width: 2560px) {
:root {
--column-count: 6;
--column-gap: 20px;
--content-padding: 30px;
--font-size-base: 17px;
}
}
/* Very large screens */
@media (max-width: 2559px) and (min-width: 1920px) {
:root {
--column-count: 4;
--column-gap: 18px;
}
}
/* Large screens */
@media (max-width: 1919px) and (min-width: 1600px) {
:root {
--column-count: 4;
--column-gap: 16px;
}
}
/* Medium-large screens */
@media (max-width: 1599px) and (min-width: 1200px) {
:root {
--column-count: 3;
--column-gap: 14px;
}
}
/* Medium screens */
@media (max-width: 1199px) and (min-width: 992px) {
:root {
--column-count: 3;
--column-gap: 12px;
}
}
/* Small tablets */
@media (max-width: 991px) and (min-width: 768px) {
:root {
--column-count: 3;
--column-gap: 10px;
--content-padding: 15px;
}
}
/* Large phones / small tablets */
@media (max-width: 767px) and (min-width: 576px) {
:root {
--column-count: 2;
--column-gap: 8px;
--content-padding: 12px;
}
}
/* Mobile phones */
@media (max-width: 575px) {
:root {
--column-count: 1;
--column-gap: 10px;
--content-padding: 10px;
--nav-height: 50px;
}
/* Stack navigation vertically on very small screens */
div#header ul#navbar,
div#header ul#subnavbar {
justify-content: center;
}
/* Make buttons full width on mobile */
button, input[type="submit"] {
width: 100%;
}
}
/* Ultra-small screens (older phones) */
@media (max-width: 359px) {
:root {
--content-padding: 8px;
--column-gap: 8px;
--thumbnail-padding: 4px;
}
div#header ul#navbar li a,
div#header ul#subnavbar li a {
padding: 8px;
}
}
/* Paginator styling - Responsive */
div#paginator {
display: flex;
justify-content: center;
align-items: center;
margin: clamp(20px, 4vw, 30px) 0;
flex-wrap: wrap;
gap: 4px;
}
div#paginator a,
div#paginator b {
display: inline-block;
padding: clamp(6px, 1.5vw, 8px) clamp(10px, 2vw, 14px);
margin: 2px;
border-radius: var(--border-radius);
background: var(--bg-tertiary);
color: var(--text-secondary);
text-decoration: none;
transition: all 0.2s;
font-size: clamp(13px, 2.5vw, 15px);
}
div#paginator a:hover {
background: var(--accent-color);
color: white;
text-decoration: none;
}
div#paginator b {
background: var(--accent-color);
color: white;
font-weight: bold;
}
/* Manual page chooser styling - Responsive */
.manual-page-chooser {
display: inline-flex;
align-items: center;
margin: 10px 0;
width: 100%;
max-width: 200px;
}
.manual-page-chooser input[type="text"] {
width: 100%;
padding: clamp(6px, 1.5vw, 8px);
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius) 0 0 var(--border-radius);
color: var(--text-primary);
font-size: clamp(13px, 2.5vw, 15px);
}
.manual-page-chooser input[type="submit"] {
padding: clamp(6px, 1.5vw, 8px) clamp(10px, 2vw, 12px);
background: var(--accent-color);
color: white;
border: none;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
cursor: pointer;
font-size: clamp(13px, 2.5vw, 15px);
white-space: nowrap;
}
/* Tables styling - Responsive */
table {
width: 100%;
border-collapse: collapse;
margin-bottom: clamp(15px, 3vw, 20px);
background: var(--bg-secondary);
border-radius: var(--border-radius);
overflow: hidden;
display: block;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
table th {
background: var(--bg-tertiary);
color: var(--text-primary);
padding: clamp(8px, 1.5vw, 12px);
text-align: left;
font-weight: bold;
font-size: clamp(13px, 2.5vw, 15px);
white-space: nowrap;
}
table td {
padding: clamp(8px, 1.5vw, 12px);
border-top: 1px solid var(--border-color);
color: var(--text-secondary);
font-size: clamp(13px, 2.5vw, 15px);
}
table tr:hover td {
background: rgba(255, 255, 255, 0.03);
}
/* Form controls - Responsive */
input, textarea, select {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: var(--input-padding);
font-family: var(--font-primary);
font-size: clamp(14px, 2.5vw, 16px);
max-width: 100%;
}
/* Auto-expanding text input fields */
textarea, input[type="text"] {
word-wrap: break-word;
word-break: normal;
white-space: pre-wrap;
overflow-wrap: break-word;
min-height: 2.5em;
line-height: 1.5;
vertical-align: top;
resize: vertical;
width: 100%;
}
/* Convert single-line text inputs to auto-expanding */
input[type="text"] {
height: auto !important;
min-height: calc(1.5em + var(--input-padding) * 2);
}
/* Ensure inputs can expand on user input with JavaScript support */
textarea {
overflow: hidden;
transition: height 0.2s ease;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(156, 100, 166, 0.2);
}
button, input[type="submit"] {
background: var(--accent-color);
color: white;
border: none;
border-radius: var(--border-radius);
padding: var(--button-padding);
cursor: pointer;
font-weight: bold;
transition: background 0.2s;
font-size: clamp(14px, 2.5vw, 16px);
touch-action: manipulation; /* Improve touch responsiveness */
}
button:hover, input[type="submit"]:hover {
background: var(--accent-secondary);
}
/* Additional modernizations */
/* Notice styling - Responsive */
div.notice, div.status-notice {
background: var(--bg-secondary);
border-left: 4px solid var(--accent-color);
color: var(--text-primary);
padding: clamp(10px, 2vw, 15px);
margin: clamp(10px, 2vw, 15px) 0;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
font-size: clamp(13px, 2.5vw, 15px);
}
/* Quote blocks - Responsive */
div.quote {
background: var(--bg-tertiary);
border-left: 4px solid var(--accent-secondary);
padding: clamp(10px, 2vw, 15px);
margin: clamp(10px, 2vw, 15px) 0;
font-style: italic;
font-size: clamp(13px, 2.5vw, 15px);
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
/* Footer styling - Responsive */
div#footer {
margin-top: clamp(30px, 5vw, 40px);
padding: clamp(15px, 3vw, 20px) 0;
text-align: center;
color: var(--text-muted);
border-top: 1px solid var(--border-color);
font-size: clamp(12px, 2vw, 14px);
}
/* Hide or modernize ads */
[data-nosnippet], .gdprcontainer {
display: none !important;
}
/* Special effects for links in masonry grid */
.thumb a::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, rgba(0,0,0,0) 70%, rgba(0,0,0,0.8) 100%);
opacity: 0;
transition: opacity 0.3s ease;
z-index: 1;
border-radius: var(--thumbnail-border-radius);
}
/* Disable hover effects on touch devices */
@media (hover: hover) {
.thumb a:hover::before {
opacity: 1;
}
}
img {
border: none;
vertical-align: middle;
max-height: 90vh;
width: auto;
}
/* ===== SIDEBAR STYLES ===== */
div.sidebar {
background: var(--bg-secondary);
border-radius: 12px;
box-shadow: var(--box-shadow);
padding: 0;
margin-right: clamp(10px, 2vw, 20px);
margin-bottom: clamp(15px, 3vw, 25px);
max-width: 280px;
min-width: 260px;
border: 1px solid var(--border-color);
overflow: hidden;
transition: all 0.3s ease;
}
/* Hover effect for entire sidebar */
div.sidebar:hover {
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
border-color: rgba(156, 100, 166, 0.3);
}
/* Search section - Enhanced modern design */
div.tag-search {
background: linear-gradient(135deg, var(--bg-tertiary) 0%, rgba(45, 45, 45, 0.8) 100%);
padding: clamp(15px, 3vw, 20px);
margin-bottom: 0;
border-radius: 0;
border-bottom: 1px solid var(--border-color);
position: relative;
}
div.tag-search::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent-color), var(--accent-secondary));
opacity: 0.8;
}
div.tag-search h5 {
color: var(--text-primary);
font-family: var(--font-secondary);
font-weight: 600;
font-size: clamp(16px, 3vw, 18px);
margin: 0 0 12px 0;
display: flex;
align-items: center;
gap: 8px;
}
div.tag-search h5::before {
content: "🔍";
font-size: 16px;
opacity: 0.8;
}
/* Enhanced search input */
div.tag-search input[type="text"] {
width: 100%;
padding: 12px 16px;
background: var(--bg-color);
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
color: var(--text-primary);
font-family: var(--font-primary);
font-size: 14px;
margin-bottom: 12px;
transition: all 0.3s ease;
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
div.tag-search input[type="text"]:focus {
border-color: var(--accent-color);
box-shadow:
inset 0 2px 4px rgba(0, 0, 0, 0.1),
0 0 0 3px rgba(156, 100, 166, 0.15);
transform: translateY(-1px);
}
div.tag-search input[type="text"]::placeholder {
color: var(--text-muted);
}
/* Modern search button */
div.tag-search input[type="submit"] {
width: 100%;
padding: 12px 20px;
background: linear-gradient(135deg, var(--accent-color) 0%, var(--accent-secondary) 100%);
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
font-size: 14px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
div.tag-search input[type="submit"]:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(156, 100, 166, 0.4);
}
div.tag-search input[type="submit"]:active {
transform: translateY(0);
}
/* Ripple effect for search button */
div.tag-search input[type="submit"]::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transition: width 0.3s, height 0.3s, top 0.3s, left 0.3s;
transform: translate(-50%, -50%);
}
/* Search hint styling */
div.tag-search small {
color: var(--text-muted);
font-size: 12px;
display: block;
margin-top: 8px;
font-style: italic;
}
/* Display options section */
ul#displayOptions {
background: var(--bg-tertiary);
margin: 0;
padding: 15px 20px;
list-style: none;
border-bottom: 1px solid var(--border-color);
}
ul#displayOptions li {
margin: 0;
padding: 8px 0;
}
ul#displayOptions label {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--text-secondary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: color 0.2s ease;
}
ul#displayOptions label:hover {
color: var(--text-primary);
}
/* Modern toggle switch styling */
.switch {
position: relative;
display: inline-block;
width: 48px;
height: 24px;
margin-left: 10px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.1);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 24px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 2px;
background: var(--text-muted);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 50%;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
input:checked + .slider {
background: linear-gradient(135deg, var(--accent-color), var(--accent-secondary));
border-color: var(--accent-color);
}
input:checked + .slider:before {
transform: translateX(24px);
background: white;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.3);
}
.slider:hover {
box-shadow: 0 0 0 8px rgba(156, 100, 166, 0.1);
}
/* Tags section - Modern card design */
#tag-sidebar {
margin: 0;
padding: 0;
list-style: none;
max-height: 60vh;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--accent-color) var(--bg-tertiary);
}
/* Custom scrollbar for tag section */
#tag-sidebar::-webkit-scrollbar {
width: 6px;
}
#tag-sidebar::-webkit-scrollbar-track {
background: var(--bg-tertiary);
}
#tag-sidebar::-webkit-scrollbar-thumb {
background: var(--accent-color);
border-radius: 3px;
}
#tag-sidebar::-webkit-scrollbar-thumb:hover {
background: var(--accent-secondary);
}
/* Tag category headers with collapsible functionality */
#tag-sidebar h6 {
background: linear-gradient(135deg, var(--bg-tertiary) 0%, rgba(45, 45, 45, 0.8) 100%);
color: var(--text-primary);
margin: 0;
padding: 12px 20px;
font-family: var(--font-secondary);
font-weight: 600;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 1px;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 10;
cursor: pointer;
user-select: none;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: space-between;
}
#tag-sidebar h6:hover {
background: linear-gradient(135deg, rgba(156, 100, 166, 0.2) 0%, rgba(174, 129, 255, 0.15) 100%);
color: var(--accent-color);
}
/* Equilateral arrowhead indicator */
#tag-sidebar h6::after {
content: '';
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 8px solid currentColor;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
opacity: 0.7;
margin-left: 8px;
}
/* Collapsed state arrow */
#tag-sidebar h6.collapsed::after {
transform: rotate(-90deg);
}
/* Tag section container for collapsing */
.tag-section {
overflow: hidden;
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
max-height: 1000px; /* Large enough for any tag list */
opacity: 1;
}
.tag-section.collapsed {
max-height: 0;
opacity: 0;
}
/* Smooth animation for tag items when collapsing */
.tag-section.collapsing {
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
}
/* Tag list items - Updated selector to work with all tag items */
#tag-sidebar li {
padding: 0;
margin: 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
transition: background 0.2s ease;
}
/* Only apply hover to non-header items */
#tag-sidebar li:not(:has(h6)):hover,
#tag-sidebar li.tag-type-general:hover,
#tag-sidebar li.tag-type-artist:hover,
#tag-sidebar li.tag-type-character:hover,
#tag-sidebar li.tag-type-copyright:hover,
#tag-sidebar li.tag-type-metadata:hover {
background: rgba(255, 255, 255, 0.03);
}
#tag-sidebar li:last-child {
border-bottom: none;
}
/* Ensure header items don't get tag item styling */
#tag-sidebar li:has(h6) {
padding: 0;
border-bottom: none;
background: none;
}
/* Tag links and controls */
#tag-sidebar li a {
display: inline-block;
padding: 8px 12px;
color: var(--text-secondary);
text-decoration: none;
font-size: 13px;
transition: all 0.2s ease;
border-radius: 4px;
}
#tag-sidebar li a:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.05);
}
/* Tag type specific colors with modern styling - Updated for all tag types */
.tag-type-general a, .tag-type-general {
color: var(--text-secondary) !important;
}
.tag-type-artist a, .tag-type-artist {
color: var(--tag-artist) !important;
}
.tag-type-character a, .tag-type-character {
color: var(--tag-character) !important;
}
.tag-type-copyright a, .tag-type-copyright {
color: var(--tag-copyright) !important;
}
.tag-type-metadata a, .tag-type-metadata {
color: var(--tag-metadata) !important;
}
/* Tag control buttons (?, +, -) */
#tag-sidebar li a[href*="wiki"],
#tag-sidebar li a[onclick*="+"]:not([href*="tags="]),
#tag-sidebar li a[onclick*="-"]:not([href*="tags="]) {
width: 26px;
height: 26px;
padding: 0;
margin: 0 2px;
text-align: center;
line-height: 26px;
font-size: 13px;
font-weight: bold;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
color: var(--text-muted);
transition: all 0.2s ease;
}
#tag-sidebar li a[href*="wiki"]:hover {
background: var(--accent-color);
color: white;
}
#tag-sidebar li a[onclick*="+"]:not([href*="tags="]):hover {
background: var(--success-color);
color: white;
}
#tag-sidebar li a[onclick*="-"]:not([href*="tags="]):hover {
background: var(--error-color);
color: white;
}
/* Tag count styling */
.tag-count {
color: var(--text-muted);
font-size: 11px;
font-weight: 500;
background: rgba(255, 255, 255, 0.05);
padding: 2px 6px;
border-radius: 10px;
margin-left: auto;
display: inline-block;
min-width: 20px;
text-align: center;
}
/* Tag item layout - flexbox for better control */
#tag-sidebar li.tag-type-general,
#tag-sidebar li.tag-type-artist,
#tag-sidebar li.tag-type-character,
#tag-sidebar li.tag-type-copyright,
#tag-sidebar li.tag-type-metadata {
display: flex;
align-items: center;
padding: 6px 12px;
gap: 4px;
flex-wrap: wrap;
}
/* Ensure header items display normally */
#tag-sidebar li:has(h6) {
display: block;
}
/* Main tag link takes remaining space */
#tag-sidebar li a[href*="tags="]:not([onclick]) {
flex: 1;
padding: 4px 8px;
margin: 0;
word-break: break-word;
}
/* Hidden blacklist section styling */
#blacklisted-sidebar {
background: var(--bg-tertiary);
padding: 15px 20px;
border-bottom: 1px solid var(--border-color);
}
#blacklisted-sidebar h5 {
margin: 0;
color: var(--text-secondary);
font-size: 14px;
font-weight: 600;
}
#blacklisted-sidebar a {
color: var(--warning-color);
text-decoration: none;
}
#blacklisted-sidebar a:hover {
color: var(--error-color);
}
/* Sidebar footer - cute mascot area */
div.sidebar img[src*="r34chibi"] {
display: block;
margin: 15px auto;
opacity: 0.6;
transition: opacity 0.3s ease;
filter: brightness(1.2) contrast(1.1);
}
div.sidebar img[src*="r34chibi"]:hover {
opacity: 1;
}
/* Responsive sidebar adjustments */
@media (max-width: 1199px) {
div.sidebar {
max-width: 240px;
min-width: 220px;
}
}
@media (max-width: 575px) {
div.sidebar {
border-radius: 8px;
margin-bottom: 15px;
}
div.tag-search {
padding: 12px 15px;
}
#tag-sidebar {
max-height: 35vh;
}
#tag-sidebar li:not(:has(h6)) {
padding: 8px 15px;
}
}
/* Loading animation for tag section */
@keyframes tagLoad {
0% { opacity: 0; transform: translateY(10px); }
100% { opacity: 1; transform: translateY(0); }
}
#tag-sidebar li {
animation: tagLoad 0.3s ease forwards;
}
/* Add stagger effect for tag items */
#tag-sidebar li:nth-child(even) {
animation-delay: 0.05s;
}
#tag-sidebar li:nth-child(3n) {
animation-delay: 0.1s;
}
/* Dark mode specific enhancements */
@media (prefers-color-scheme: dark) {
div.sidebar {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
}
}
/* High contrast mode support */
@media (prefers-contrast: high) {
div.sidebar {
border: 2px solid var(--accent-color);
}
.tag-count {
background: var(--accent-color);
color: var(--bg-color);
}
}
`;
document.head.appendChild(style);
function setColumnCount(count) {
document.documentElement.style.setProperty('--column-count', count);
}
function addColumnControlPanel() {
// Avoid duplicate panel
if (document.getElementById('columnSlider')) return;
// Create the panel
const controlPanel = document.createElement('div');
controlPanel.innerHTML = `
<div style="margin-bottom: 18px; background: #1a1a1a; padding: 10px; border-radius: 8px; border: 1px solid #2a2a2a; display: flex; align-items: center; gap: 10px;">
<label style="color: #e0e0e0;">Columns:</label>
<input type="range" id="columnSlider" min="1" max="8" value="4" style="width: 100px;">
<span id="columnCount" style="color: #e0e0e0;">4</span>
</div>
`;
// Insert above .image-list
const imageList = document.querySelector('.image-list');
if (imageList && imageList.parentNode) {
imageList.parentNode.insertBefore(controlPanel, imageList);
} else {
// fallback: append to body if .image-list not found
document.body.appendChild(controlPanel);
}
const slider = document.getElementById('columnSlider');
const countDisplay = document.getElementById('columnCount');
slider.addEventListener('input', function() {
const count = this.value;
countDisplay.textContent = count;
setColumnCount(count);
localStorage.setItem('galleryColumns', count);
});
// Load saved preference
const savedColumns = localStorage.getItem('galleryColumns');
if (savedColumns) {
slider.value = savedColumns;
countDisplay.textContent = savedColumns;
setColumnCount(savedColumns);
}
}
const processedImages = new Set();
const apiCache = new Map();
function extractHashFromThumbnail(thumbnailUrl) {
// Extract hash from: https://wimg.rule34.xxx/thumbnails/5046/thumbnail_fffef5df32fbff452a0be69302ca8e8b.jpg?5747620
const match = thumbnailUrl.match(/thumbnail_([a-f0-9]+)/);
return match ? match[1] : null;
}
function extractDirectoryFromThumbnail(thumbnailUrl) {
// Extract directory from: https://wimg.rule34.xxx/thumbnails/5046/thumbnail_...
const match = thumbnailUrl.match(/thumbnails\/(\d+)\//);
return match ? match[1] : null;
}
function createJPGUrl(thumbnailUrl) {
const hash = extractHashFromThumbnail(thumbnailUrl);
const directory = extractDirectoryFromThumbnail(thumbnailUrl);
if (!hash || !directory) return null;
// Convert: thumbnails/5046/thumbnail_hash.jpg → samples/5046/sample_hash.jpg
return `https://rule34.xxx/images/${directory}/${hash}.jpg`;
}
function createJPEGUrl(thumbnailUrl) {
const hash = extractHashFromThumbnail(thumbnailUrl);
const directory = extractDirectoryFromThumbnail(thumbnailUrl);
if (!hash || !directory) return null;
// Convert: thumbnails/5046/thumbnail_hash.jpg → samples/5046/sample_hash.jpg
return `https://rule34.xxx/images/${directory}/${hash}.jpeg`;
}
function createPNGUrl(thumbnailUrl) {
const hash = extractHashFromThumbnail(thumbnailUrl);
const directory = extractDirectoryFromThumbnail(thumbnailUrl);
if (!hash || !directory) return null;
// Convert: thumbnails/5046/thumbnail_hash.jpg → samples/5046/sample_hash.jpg
return `https://rule34.xxx/images/${directory}/${hash}.gif`;
}
function getCurrentPageTags() {
// Extract tags from current URL: tags=rating%3Asafe+sort%3Ascore%3Adesc&pid=126
const urlParams = new URLSearchParams(window.location.search);
const tags = urlParams.get("tags") || "";
// Clean up the tags - decode and keep only the filter tags, remove sort
let cleanTags = decodeURIComponent(tags);
// Remove sorting parameters but keep filter tags
cleanTags = cleanTags.replace(/\s*sort:[^\s]+/g, "").trim();
console.log(`Current page tags: "${cleanTags}"`);
return cleanTags;
}
function extractIdFromThumbnail(thumbnailUrl) {
// Extract ID from: https://wimg.rule34.xxx/thumbnails/5046/thumbnail_fffef5df32fbff452a0be69302ca8e8b.jpg?5747620
const match = thumbnailUrl.match(/\?(\d+)$/);
return match ? match[1] : null;
}
async function fallbackToAPI(thumbnailUrl, img) {
const postId = extractIdFromThumbnail(thumbnailUrl);
if (!postId) {
console.log(`Could not extract post ID from ${thumbnailUrl}`);
return;
}
// Check if we already have this specific post cached
if (apiCache.has(postId)) {
const cachedPost = apiCache.get(postId);
replaceWithApiData(cachedPost, img);
return;
}
// Make API call for this specific post ID
const apiUrl = `https://api.rule34.xxx/index.php?page=dapi&s=post&q=index&json=1&limit=1000&id=${postId}`;
console.log(`Making API call for post ${postId}: ${apiUrl}`);
try {
const response = await fetch(apiUrl);
if (!response.ok) {
console.log(`API request failed: ${response.status}`);
return;
}
const data = await response.json();
console.log(`API returned data for post ${postId}:`, data);
// Handle response format - could be array or single object
let post = null;
if (Array.isArray(data) && data.length > 0) {
post = data[0];
} else if (data.id) {
post = data;
}
if (post) {
// Cache the post data
apiCache.set(postId, post);
// Replace the image
replaceWithApiData(post, img);
} else {
console.log(`No post data found for ID ${postId}`);
}
} catch (error) {
console.error(`API call failed for post ${postId}:`, error);
}
}
function replaceWithApiData(post, img) {
// Use sample_url if available, otherwise file_url
let betterUrl;
if (post.file_url.endsWith(".mp4") || post.file_url.endsWith(".webm")) {
betterUrl = post.sample_url;
} else {
betterUrl = post.file_url;
}
if (betterUrl) {
console.log(`Found API replacement for post ${post.id}: ${betterUrl}`);
// Test the image before replacing
const testImg = new Image();
testImg.onload = function () {
img.src = betterUrl;
img.dataset.replacedViaApi = "true";
img.dataset.replaced = "api";
console.log(`✅ Successfully replaced post ${post.id} via API`);
// Visual feedback - different color for API replacements
img.style.transition = "box-shadow 0.3s ease";
img.style.boxShadow = "0 0 8px #0099ff";
setTimeout(() => {
img.style.boxShadow = "";
}, 800);
};
testImg.onerror = function () {
console.log(
`❌ API replacement failed to load for post ${post.id}: ${betterUrl}`
);
};
testImg.src = betterUrl;
} else {
console.log(`No suitable URL found for post ${post.id} in API response`);
}
}
async function replaceThumbnailWithSample(img) {
const originalSrc = img.src;
// Skip if already processed
if (processedImages.has(originalSrc) || img.dataset.replaced) {
return;
}
// Only process thumbnail images
if (!originalSrc.includes("/thumbnails/")) {
return;
}
processedImages.add(originalSrc);
img.dataset.processing = "true";
console.log(`Processing thumbnail: ${originalSrc}`);
// Try all sample URL creators in order
const urlCreators = [createJPGUrl, createJPEGUrl, createPNGUrl];
let replaced = false;
for (const createUrl of urlCreators) {
const sampleUrl = createUrl(originalSrc);
if (!sampleUrl) continue;
console.log(`Trying sample URL: ${sampleUrl}`);
// eslint-disable-next-line no-await-in-loop
const success = await testImageUrl(sampleUrl);
if (success) {
img.src = sampleUrl;
img.dataset.replaced = "sample";
img.dataset.processing = "false";
console.log(`✅ Successfully replaced with sample: ${sampleUrl}`);
// Visual feedback
img.style.transition = "box-shadow 0.3s ease";
img.style.boxShadow = "0 0 8px #00ff00";
setTimeout(() => {
img.style.boxShadow = "";
}, 800);
replaced = true;
break;
}
}
if (!replaced) {
fallbackToAPI(originalSrc, img);
}
}
// Helper to test if an image URL loads
function testImageUrl(url) {
return new Promise((resolve) => {
const testImg = new Image();
testImg.onload = () => resolve(true);
testImg.onerror = () => resolve(false);
testImg.src = url;
});
}
function processAllThumbnails() {
const thumbnails = Array.from(
document.querySelectorAll('img[src*="/thumbnails/"]')
);
console.log(`Found ${thumbnails.length} thumbnail images to process`);
thumbnails.forEach((img, index) => {
// Add small delay to avoid overwhelming the browser
setTimeout(() => {
replaceThumbnailWithSample(img);
}, index * 50);
});
}
function processVisibleThumbnails() {
const thumbnails = Array.from(
document.querySelectorAll('img[src*="/thumbnails/"]')
).filter((img) => {
if (img.dataset.replaced || img.dataset.processing) return false;
const rect = img.getBoundingClientRect();
const windowHeight = window.innerHeight;
return rect.top < windowHeight + 300 && rect.bottom > -300;
});
if (thumbnails.length > 0) {
console.log(`Processing ${thumbnails.length} visible thumbnails`);
thumbnails.forEach(replaceThumbnailWithSample);
}
}
// Debug functions
function showStats() {
const total = document.querySelectorAll('img[src*="/thumbnails/"]').length;
const sampleReplaced = document.querySelectorAll(
'img[data-replaced="sample"]'
).length;
const apiReplaced = document.querySelectorAll(
'img[data-replaced="api"]'
).length;
const processing = document.querySelectorAll(
'img[data-processing="true"]'
).length;
console.log(
`Stats: ${sampleReplaced} sample, ${apiReplaced} API, ${processing} processing, ${total} total`
);
console.log(`API cache: ${apiCache.size} entries`);
return {
total,
sampleReplaced,
apiReplaced,
processing,
cached: apiCache.size,
};
}
function clearCacheAndReprocess() {
processedImages.clear();
apiCache.clear();
// Reset all images
document
.querySelectorAll("img[data-replaced], img[data-processing]")
.forEach((img) => {
delete img.dataset.replaced;
delete img.dataset.processing;
delete img.dataset.replacedViaApi;
});
console.log("Cache cleared, reprocessing...");
setTimeout(processAllThumbnails, 100);
}
// Make functions globally available
window.processAllThumbnails = processAllThumbnails;
window.showStats = showStats;
window.clearCacheAndReprocess = clearCacheAndReprocess;
// Initialize
function initialize() {
console.log("Rule34 Simple Reskin initialized");
// Process images after page loads
setTimeout(processAllThumbnails, 500);
document.head.appendChild(style);
addColumnControlPanel();
}
// Start when ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", initialize);
} else {
initialize();
}
// Process visible images on scroll
let scrollTimeout;
window.addEventListener("scroll", () => {
clearTimeout(scrollTimeout);
scrollTimeout = setTimeout(processVisibleThumbnails, 200);
});
// Process new images when they're dynamically added
const observer = new MutationObserver((mutations) => {
let hasNewThumbnails = false;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName === "IMG" && node.src.includes("/thumbnails/")) {
hasNewThumbnails = true;
} else if (
node.querySelector &&
node.querySelector('img[src*="/thumbnails/"]')
) {
hasNewThumbnails = true;
}
}
});
});
if (hasNewThumbnails) {
setTimeout(processVisibleThumbnails, 200);
}
});
// ===== COLLAPSIBLE SIDEBAR FUNCTIONALITY =====
function initializeCollapsibleSidebar() {
const tagSidebar = document.getElementById('tag-sidebar');
if (!tagSidebar) return;
// Get saved collapsed states from localStorage
const savedStates = JSON.parse(localStorage.getItem('sidebarCollapsedStates') || '{}');
// Find all h6 headers and wrap subsequent tag items
const headers = tagSidebar.querySelectorAll('h6');
headers.forEach((header, index) => {
const categoryName = header.textContent.toLowerCase().trim();
// Create wrapper for tag items in this category
const tagSection = document.createElement('div');
tagSection.className = 'tag-section';
tagSection.dataset.category = categoryName;
// Find all tag items until the next h6 or end of list
let currentElement = header.parentElement.nextElementSibling;
const tagItems = [];
while (currentElement && !currentElement.querySelector('h6')) {
tagItems.push(currentElement);
currentElement = currentElement.nextElementSibling;
}
// Move tag items into the wrapper
tagItems.forEach(item => {
tagSection.appendChild(item);
});
// Insert wrapper after header
header.parentElement.parentNode.insertBefore(tagSection, header.parentElement.nextElementSibling);
// Apply saved collapsed state
if (savedStates[categoryName]) {
header.classList.add('collapsed');
tagSection.classList.add('collapsed');
}
// Add click handler
header.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
toggleSection(header, tagSection, categoryName);
});
// Add keyboard support
header.addEventListener('keydown', function(e) {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleSection(header, tagSection, categoryName);
}
});
// Make headers focusable
header.setAttribute('tabindex', '0');
header.setAttribute('role', 'button');
header.setAttribute('aria-expanded', !savedStates[categoryName]);
header.setAttribute('aria-controls', `tag-section-${categoryName}`);
tagSection.setAttribute('id', `tag-section-${categoryName}`);
});
}
function toggleSection(header, section, categoryName) {
const isCollapsed = header.classList.contains('collapsed');
// Update visual state
header.classList.toggle('collapsed');
header.setAttribute('aria-expanded', isCollapsed);
if (isCollapsed) {
// Expanding
section.classList.remove('collapsed');
section.style.maxHeight = section.scrollHeight + 'px';
// Reset max-height after animation
setTimeout(() => {
if (!section.classList.contains('collapsed')) {
section.style.maxHeight = '1000px';
}
}, 400);
} else {
// Collapsing
section.style.maxHeight = section.scrollHeight + 'px';
section.classList.add('collapsing');
// Force reflow
section.offsetHeight;
section.style.maxHeight = '0px';
setTimeout(() => {
section.classList.add('collapsed');
section.classList.remove('collapsing');
}, 400);
}
// Save state to localStorage
saveCollapsedStates();
}
function saveCollapsedStates() {
const states = {};
const headers = document.querySelectorAll('#tag-sidebar h6');
headers.forEach(header => {
const categoryName = header.textContent.toLowerCase().trim();
states[categoryName] = header.classList.contains('collapsed');
});
localStorage.setItem('sidebarCollapsedStates', JSON.stringify(states));
}
// Utility functions for manual control
function expandAllSections() {
const headers = document.querySelectorAll('#tag-sidebar h6.collapsed');
headers.forEach(header => {
const categoryName = header.textContent.toLowerCase().trim();
const section = document.getElementById(`tag-section-${categoryName}`);
if (section) {
toggleSection(header, section, categoryName);
}
});
}
function collapseAllSections() {
const headers = document.querySelectorAll('#tag-sidebar h6:not(.collapsed)');
headers.forEach(header => {
const categoryName = header.textContent.toLowerCase().trim();
const section = document.getElementById(`tag-section-${categoryName}`);
if (section) {
toggleSection(header, section, categoryName);
}
});
}
// Reset all collapsed states
function resetSidebarStates() {
localStorage.removeItem('sidebarCollapsedStates');
location.reload();
}
// Make functions globally available for debugging
window.expandAllSections = expandAllSections;
window.collapseAllSections = collapseAllSections;
window.resetSidebarStates = resetSidebarStates;
// Initialize when DOM is ready
function initSidebarWhenReady() {
if (document.getElementById('tag-sidebar')) {
setTimeout(initializeCollapsibleSidebar, 100);
} else {
// Retry after a short delay if sidebar isn't ready yet
setTimeout(initSidebarWhenReady, 500);
}
}
// Call initialization
initSidebarWhenReady();
// Re-initialize if new content is loaded dynamically
const sidebarObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
const addedSidebar = Array.from(mutation.addedNodes).find(node =>
node.id === 'tag-sidebar' || (node.querySelector && node.querySelector('#tag-sidebar'))
);
if (addedSidebar) {
setTimeout(initializeCollapsibleSidebar, 100);
}
}
});
});
if (document.body) {
sidebarObserver.observe(document.body, {
childList: true,
subtree: true
});
}
})();