Unified toolbox for DeviantArt: hover zoom, hide paid posts, ad blocking and more.
// ==UserScript==
// @name DeviantArt Toolbox
// @namespace https://gitlab.com/Kioraga/da-toolbox
// @version 1.4.1
// @description Unified toolbox for DeviantArt: hover zoom, hide paid posts, ad blocking and more.
// @author kioraga
// @match https://www.deviantart.com/*
// @icon https://gitlab.com/Kioraga/da-toolbox/-/raw/main/assets/da-icon.png
// @homepageURL https://gitlab.com/Kioraga/da-toolbox
// @supportURL https://gitlab.com/Kioraga/da-toolbox/-/issues
// @license MIT
// @grant none
// @noframes
// @run-at document-idle
// ==/UserScript==
(function () {
"use strict";
if (window !== window.top) return;
const PANEL_HTML = ` <!-- Drag handle (visible only on mobile) -->
<div class="hidden max-[600px]:flex justify-center pt-2 pb-0.5">
<div class="w-7 h-1 rounded-full bg-[var(--md-on-surface-variant)] opacity-40"></div>
</div>
<!-- Desktop header -->
<div class="flex items-center justify-between px-6 pt-4 pb-3 max-[600px]:hidden">
<span class="flex items-center gap-2 text-base font-medium text-[var(--md-on-surface)]">
<img src="https://gitlab.com/Kioraga/da-toolbox/-/raw/main/assets/da-icon.svg" width="20" height="20" alt="">
DA Toolbox
</span>
<button id="datb-close" title="Close"
class="w-10 h-10 rounded-full flex items-center justify-center text-[var(--md-on-surface-variant)] text-lg cursor-pointer bg-transparent border-none transition-colors duration-150 hover:bg-white/10 active:bg-white/20">
✕
</button>
</div>
<!-- Mobile header -->
<div class="hidden max-[600px]:flex items-center px-6 pt-4 pb-3">
<button class="datb-close w-10 h-10 rounded-full flex items-center justify-center cursor-pointer bg-transparent border border-[var(--md-outline-variant)] text-[var(--md-on-surface-variant)] transition-colors duration-150 hover:bg-white/10 active:bg-white/20" title="Back">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<span class="flex-1 text-center text-base font-medium text-[var(--md-on-surface)] flex items-center justify-center gap-2">
<img src="https://gitlab.com/Kioraga/da-toolbox/-/raw/main/assets/da-icon.svg" width="20" height="20" alt="">
DA Toolbox
</span>
<div class="w-10"></div>
</div>
<!-- Tab navigation -->
<nav class="flex gap-1 px-6 py-1 datb-tab-nav">
<div class="datb-tab-indicator"></div>
<button class="datb-tab datb-tab-active flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="hover">Hover</button>
<button class="datb-tab flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="zoom">Zoom</button>
<button class="datb-tab flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="filters">Filters</button>
<button class="datb-tab flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="layout">Layout</button>
<button class="datb-tab flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="adblock">Adblock</button>
<button class="datb-tab datb-debug-tab flex-1 px-3 py-2 text-sm font-medium rounded-full text-[var(--md-on-surface-variant)] bg-transparent border-none cursor-pointer" data-tab="debug">Debug</button>
</nav>
<!-- Sections -->
<div class="datb-sections px-6 pt-4 pb-5 overflow-y-auto overflow-x-hidden flex-1 min-h-0">
<!-- Hover tab -->
<div class="datb-section datb-section-active" id="datb-sec-hover">
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Enable</span>
<input type="checkbox" id="datb-zoom-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Enables the hover zoom effect on gallery posts</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Scale</span>
<div class="flex items-center gap-2.5">
<input type="range" id="datb-zoom-scale" min="1" max="2" step="0.05" value="1.4" class="w-20 accent-[var(--md-primary)] cursor-pointer">
<span class="datb-val min-w-[36px] text-right text-[var(--md-primary)] font-medium text-sm" id="datb-zoom-scale-val">1.4x</span>
</div>
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">How much the card scales up on hover</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Speed</span>
<select id="datb-zoom-speed" class="bg-[var(--md-surface-container-high)] text-[var(--md-on-surface)] border border-[var(--md-outline-variant)] rounded-lg px-2 py-1 text-xs cursor-pointer outline-none hover:border-[var(--md-outline)]">
<option value="fast">Fast</option>
<option value="normal">Normal</option>
<option value="slow">Slow</option>
</select>
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Transition duration for the zoom animation</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Shadow</span>
<input type="checkbox" id="datb-zoom-shadow" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Adds a drop shadow to the scaled card</div>
</div>
<!-- Zoom tab -->
<div class="datb-section" id="datb-sec-zoom">
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Enable</span>
<input type="checkbox" id="datb-zoom-z-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Enables zoom — hold Z to zoom, scroll while held to adjust size</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Full resolution</span>
<input type="checkbox" id="datb-zoom-fullres" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Try to load the image in full resolution</div>
</div>
<!-- Filters tab -->
<div class="datb-section" id="datb-sec-filters">
<div class="datb-subheader datb-subheader-first">Paid Posts</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Hide paid posts</span>
<input type="checkbox" id="datb-hp-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Hides subscribe/sale/buy & unlock gallery posts</div>
<div class="datb-subheader">Content</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Hide text posts</span>
<input type="checkbox" id="datb-htp-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Hides journals & status updates in the watch feed</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Hide feed carousels</span>
<input type="checkbox" id="datb-hc-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Hides carousels & "Recommended for You"</div>
<div class="datb-subheader">Loading</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Auto-load more results</span>
<input type="checkbox" id="datb-mr-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Automatically clicks "More Results" to load additional posts</div>
</div>
<!-- Layout tab -->
<div class="datb-section" id="datb-sec-layout">
<div class="datb-subheader datb-subheader-first">Scroll-to-top button</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Enable</span>
<input type="checkbox" id="datb-stt-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Shows a button to return to the top of the page</div>
<div class="datb-subheader">Image only</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Enable</span>
<input type="checkbox" id="datb-zoom-hide" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Hides UI overlays, only shows deviation images</div>
<div class="datb-mobile-grid-controls">
<div class="datb-subheader">Mobile Grid</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Grid layout</span>
<input type="checkbox" id="datb-mg-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Replaces DA's natural-scroll row layout with a compact CSS grid</div>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Columns</span>
<select id="datb-mg-columns" class="bg-[var(--md-surface-container-high)] text-[var(--md-on-surface)] border border-[var(--md-outline-variant)] rounded-lg px-2 py-1 text-xs cursor-pointer outline-none hover:border-[var(--md-outline)]">
<option value="0">Auto</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Number of columns in the grid (Auto fills available space)</div>
</div>
</div>
<!-- Adblock tab -->
<div class="datb-section" id="datb-sec-adblock">
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Enable</span>
<input type="checkbox" id="datb-ab-enabled" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<div class="-mt-0.5 mb-1 pb-1.5 text-[11px] text-[var(--md-on-surface-variant)] leading-[1.4]">Blocks banners, promos and upgrade offers</div>
</div>
<!-- Debug tab -->
<div class="datb-section datb-debug-section" id="datb-sec-debug">
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Enable</span>
<input type="checkbox" id="datb-debug" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
<label class="flex items-center justify-between py-2.5 min-h-[40px] text-[var(--md-on-surface)] cursor-pointer">
<span>Show hidden sections</span>
<input type="checkbox" id="datb-debug-showall" class="accent-[var(--md-primary)] scale-110 cursor-pointer">
</label>
</div>
</div>
`;
const PANEL_STYLES = `/* === CSS Custom Properties (MD3-inspired theme) === */
:root {
--md-primary: #2ecc71;
--md-on-primary: #003917;
--md-surface: #111318;
--md-surface-container: #1b1d22;
--md-surface-container-high: #26282d;
--md-on-surface: #e2e2e5;
--md-on-surface-variant: #c4c6ca;
--md-outline: #8e9098;
--md-outline-variant: #44474e;
}
/* === Tailwind-like utility classes === */
#datb-btn,
#datb-panel { font-family: 'Google Sans','Product Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; }
.flex { display: flex; }
.flex-col { flex-direction: column; }
.flex-1 { flex: 1 1 0%; }
.items-center { align-items: center; }
.justify-between { justify-content: space-between; }
.justify-center { justify-content: center; }
.gap-1 { gap: 0.25rem; }
.gap-2 { gap: 0.5rem; }
.gap-2\\.5 { gap: 0.625rem; }
.gap-3 { gap: 0.75rem; }
.hidden { display: none; }
.overflow-y-auto { overflow-y: auto; }
.overflow-x-hidden { overflow-x: hidden; }
.min-h-0 { min-height: 0; }
.w-fit { width: fit-content; }
.w-7 { width: 1.75rem; }
.w-10 { width: 2.5rem; }
.w-12 { width: 3rem; }
.w-20 { width: 5rem; }
.w-\\[1px\\] { width: 1px; }
.h-1 { height: 0.25rem; }
.h-10 { height: 2.5rem; }
.h-12 { height: 3rem; }
.min-w-\\[36px\\] { min-width: 36px; }
.min-w-\\[260px\\] { min-width: 260px; }
.min-h-\\[40px\\] { min-height: 40px; }
.p-0 { padding: 0; }
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
.py-2\\.5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
.pt-2 { padding-top: 0.5rem; }
.pt-4 { padding-top: 1rem; }
.pb-0\\.5 { padding-bottom: 0.125rem; }
.pb-1\\.5 { padding-bottom: 0.375rem; }
.pb-3 { padding-bottom: 0.75rem; }
.pb-5 { padding-bottom: 1.25rem; }
.-mt-0\\.5 { margin-top: -0.125rem; }
.mb-1 { margin-bottom: 0.25rem; }
.text-xs { font-size: 0.75rem; line-height: 1rem; }
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
.text-base { font-size: 1rem; line-height: 1.5rem; }
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
.text-center { text-align: center; }
.text-right { text-align: right; }
.font-medium { font-weight: 500; }
.font-semibold { font-weight: 600; }
.leading-\\[1\\.4\\] { line-height: 1.4; }
.uppercase { text-transform: uppercase; }
.tracking-\\[\\.05em\\] { letter-spacing: 0.05em; }
.text-\\[11px\\] { font-size: 11px; }
.cursor-pointer { cursor: pointer; }
.rounded-full { border-radius: 9999px; }
.rounded-2xl { border-radius: 1rem; }
.rounded-lg { border-radius: 0.5rem; }
.border { border-width: 1px; border-style: solid; }
.border-0 { border-width: 0; }
.border-none { border-style: none; }
.bg-transparent { background: transparent; }
.bg-white { background: #fff; }
.scale-110 { transform: scale(1.1); }
.opacity-40 { opacity: 0.4; }
.opacity-60 { opacity: 0.6; }
.outline-none { outline: none; }
.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1); }
.inline-flex { display: inline-flex; }
.flex-wrap { flex-wrap: wrap; }
.items-start { align-items: flex-start; }
.text-left { text-align: left; }
.whitespace-nowrap { white-space: nowrap; }
.transition-all { transition-property: all; transition-timing-function: cubic-bezier(.4,0,.2,1); transition-duration: .15s; }
.transition-colors { transition-property: color,background-color,border-color,text-decoration-color,fill,stroke; transition-timing-function: cubic-bezier(.4,0,.2,1); transition-duration: .15s; }
.duration-150 { transition-duration: .15s; }
.duration-200 { transition-duration: .2s; }
.ease-panel { transition-timing-function: cubic-bezier(.2,0,0,1); }
.fixed { position: fixed; }
.absolute { position: absolute; }
.relative { position: relative; }
.bottom-5 { bottom: 1.25rem; }
.right-5 { right: 1.25rem; }
.z-\\[2147483647\\] { z-index: 2147483647; }
.text-\\[var\\(--md-on-surface\\)\\] { color: var(--md-on-surface); }
.text-\\[var\\(--md-on-surface-variant\\)\\] { color: var(--md-on-surface-variant); }
.text-\\[var\\(--md-primary\\)\\] { color: var(--md-primary); }
.bg-\\[var\\(--md-surface-container-high\\)\\] { background: var(--md-surface-container-high); }
.bg-\\[var\\(--md-surface-container\\)\\] { background: var(--md-surface-container); }
.border-\\[var\\(--md-outline-variant\\)\\] { border-color: var(--md-outline-variant); }
.accent-\\[var\\(--md-primary\\)\\] { accent-color: var(--md-primary); }
.hover\\:bg-white\\/10:hover { background: rgba(255,255,255,.1); }
.hover\\:bg-white\\/20:hover { background: rgba(255,255,255,.2); }
.hover\\:border-\\[var\\(--md-outline\\)\\]:hover { border-color: var(--md-outline); }
.hover\\:scale-105:hover { transform: scale(1.05); }
.active\\:bg-white\\/20:active { background: rgba(255,255,255,.2); }
.active\\:scale-95:active { transform: scale(0.95); }
@media (max-width: 600px) {
.max-\\[600px\\]\\:hidden { display: none !important; }
.max-\\[600px\\]\\:flex { display: flex !important; }
.max-\\[600px\\]\\:justify-center { justify-content: center !important; }
.datb-tab[data-tab="hover"],
.datb-tab[data-tab="zoom"] { display: none !important; }
#datb-sec-hover,
#datb-sec-zoom { display: none !important; }
}
/* === FAB (floating action button) === */
#datb-btn { position: fixed !important; box-shadow: 0 4px 12px rgba(0,0,0,.4); }
#datb-btn:hover { box-shadow: 0 8px 25px rgba(0,0,0,.5); transform: scale(1.05); }
#datb-btn:active { transform: scale(0.95); }
#datb-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--md-primary), 0 4px 12px rgba(0,0,0,.4); }
#datb-btn.datb-fab-open { box-shadow: 0 0 0 3px var(--md-primary), 0 8px 25px rgba(0,0,0,.5) !important; }
#datb-btn img { display: block; width: 22px; height: 22px; }
/* === Scroll-to-top button === */
#datb-stt { position: fixed; z-index: 2147483646; bottom: 80px; right: 20px; width: 48px; height: 48px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; padding: 0; border: 0; background: var(--md-surface-container-high); color: #fff; opacity: 0; pointer-events: none; transition: opacity .25s, box-shadow .2s, transform .15s; box-shadow: 0 4px 12px rgba(0,0,0,.4); }
#datb-stt:hover { box-shadow: 0 8px 25px rgba(0,0,0,.5); transform: scale(1.05); }
#datb-stt:active { transform: scale(0.95); }
#datb-stt.datb-stt-visible { opacity: 1; pointer-events: auto; }
/* === Popover panel === */
#datb-panel { display: none !important; }
#datb-panel:popover-open { inset: auto; position: fixed; margin: 0; display: flex !important; height: 480px; }
/* === Tab navigation === */
.datb-tab-nav { position: relative; }
.datb-tab-indicator { position: absolute; top: 4px; bottom: 4px; border-radius: 9999px; background: var(--md-surface-container-high); z-index: 0; box-shadow: 0 1px 3px rgba(0,0,0,.3); pointer-events: none; transition: left .2s ease, width .2s ease; }
.datb-tab { position: relative; z-index: 1; }
.datb-section { display: none; }
.datb-section-active { display: block; }
.datb-section-empty { display: none !important; }
.datb-debug-tab,
.datb-debug-section { display: none !important; }
#datb-panel.datb-debug .datb-debug-tab { display: block !important; }
#datb-panel.datb-debug .datb-debug-section.datb-section-active { display: block !important; }
.datb-mobile-grid-controls { display: none; }
.datb-has-grid .datb-mobile-grid-controls { display: block; }
.datb-subheader { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--md-primary); padding-top: 16px; }
.datb-beta { font-size: 10px; font-weight: 700; background: var(--md-primary); color: var(--md-on-primary); padding: 1px 5px; border-radius: 3px; margin-left: 4px; vertical-align: middle; line-height: 1.4; }
.datb-subheader-first { padding-top: 0; }
.datb-touch-hidden { display: none !important; }
/* === Debug: force-show all hidden sections === */
#datb-panel.datb-debug-showall .datb-debug-tab,
#datb-panel.datb-debug-showall .datb-tab.datb-touch-hidden { display: revert !important; }
#datb-panel.datb-debug-showall .datb-debug-section.datb-section-active,
#datb-panel.datb-debug-showall .datb-section-active.datb-touch-hidden { display: revert !important; }
#datb-panel.datb-debug-showall .datb-mobile-grid-controls { display: revert !important; }
.datb-sections::-webkit-scrollbar { width: 4px; }
.datb-sections::-webkit-scrollbar-thumb { background: var(--md-outline-variant); border-radius: 2px; }
/* === Mobile responsive === */
@media (max-width: 600px) {
#datb-panel:popover-open {
top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
width: 100vw !important; max-width: none !important; height: 100dvh !important;
border-radius: 0 !important; max-height: none !important; border: none !important;
}
.datb-tab-nav {
overflow-x: auto;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
}
.datb-tab-nav::-webkit-scrollbar { display: none; }
.datb-tab { flex-shrink: 0; }
.datb-tab[data-tab="hover"],
.datb-tab[data-tab="zoom"] { display: none !important; }
#datb-sec-hover,
#datb-sec-zoom { display: none !important; }
/* debug-showall overrides mobile hide: show tabs always, sections only when active */
#datb-panel.datb-debug-showall .datb-tab[data-tab="hover"],
#datb-panel.datb-debug-showall .datb-tab[data-tab="zoom"],
#datb-panel.datb-debug-showall .datb-debug-tab { display: revert !important; }
#datb-panel.datb-debug-showall #datb-sec-hover.datb-section-active,
#datb-panel.datb-debug-showall #datb-sec-zoom.datb-section-active { display: revert !important; }
}
`;
// Namespace prefix and DOM selectors for DeviantArt gallery
const NS = "datb";
const STORAGE_KEY = NS + "-settings";
const ROW_SEL = '[data-testid="content_row"]';
const CARD_SEL = 'div[style*="inline-block"]';
const CAROUSEL_CARD_SEL = 'div.RyH8GD.qtdoVZ';
const BTN_ID = NS + "-btn";
const PANEL_ID = NS + "-panel";
const STYLE_ID = NS + "-styles";
const PANEL_STYLE_ID = NS + "-panel-styles";
const ACTIVE = NS + "-active";
// Duration (seconds) per speed setting
const SPEED_MAP = { fast: 0.1, normal: 0.25, slow: 0.4 };
// Default settings for all features
const DEFAULTS = {
zoomEnabled: true,
zoomScale: 1.4,
zoomSpeed: "normal",
zoomShadow: true,
zoomHideOverlays: false,
zoomFullRes: true,
zoomZEnabled: true,
hidePaidEnabled: true,
hideTextPosts: false,
hideCarousels: false,
adBlockEnabled: true,
scrollToTopEnabled: true,
mobileGridEnabled: false,
mobileGridColumns: 0,
moreResultsEnabled: true,
debug: false,
debugShowAll: false,
};
// Shared mutable state across all modules
let STATE = {};
let zHeld = false;
let wheelScale = null;
let zClone = null;
let zCloneCard = null;
let zSpinner = null;
let paidScanTimer = null;
// Conditional debug logger
function log(...args) {
if (STATE.debug) console.log("[" + NS + "]", ...args);
}
// Load settings from localStorage with migration from old script keys
function loadSettings() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
STATE = { ...DEFAULTS, ...JSON.parse(raw) };
if (STATE.hideRecommended !== undefined) {
STATE.hideCarousels = STATE.hideRecommended;
delete STATE.hideRecommended;
}
return;
}
} catch (e) {
log("settings parse error:", e);
}
// Migrate from da-hover-zoom settings (dahz-settings)
const oldHz = localStorage.getItem("dahz-settings");
if (oldHz) {
try {
const parsed = JSON.parse(oldHz);
STATE = {
...DEFAULTS,
zoomEnabled: parsed.enabled ?? true,
zoomScale: parsed.scale ?? 1.4,
zoomSpeed: parsed.speed ?? "normal",
zoomShadow: parsed.shadow ?? true,
zoomHideOverlays: parsed.hideOverlays ?? false,
zoomFullRes: parsed.fullRes ?? true,
adBlockEnabled: parsed.adBlock ?? true,
debug: parsed.debug ?? false,
};
saveSettings();
return;
} catch (e) {
log("migration error:", e);
}
}
STATE = { ...DEFAULTS };
}
// Persist current STATE to localStorage
function saveSettings() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(STATE));
}
// Toggle html.datb-image-only class based on zoomHideOverlays setting
function updateImageOnlyClass() {
document.documentElement.classList.toggle(NS + "-image-only", STATE.zoomHideOverlays);
}
// Apply multiple CSS properties with !important to an element
function setStyles(el, styles) {
for (const [k, v] of Object.entries(styles)) {
el.style.setProperty(k, v, "important");
}
}
const WRENCH_ICON = '<img src="https://gitlab.com/Kioraga/da-toolbox/-/raw/main/assets/wrench.svg" width="22" height="22" alt="">';
// Floating button dimensions and position from bottom-right
const BTN_BOTTOM = 20;
const BTN_SIZE = 48;
// Sync form inputs to STATE after panel HTML is inserted
function syncPanelState(panel) {
function setChecked(id) { var el = panel.querySelector("#" + id); if (el) el.checked = true; }
function setRange(id, val) { var el = panel.querySelector("#" + id); if (el) { el.value = val; } }
function setSelect(id, val) { var el = panel.querySelector("#" + id); if (el) el.value = val; }
if (STATE.zoomEnabled) setChecked(NS + "-zoom-enabled");
if (STATE.zoomShadow) setChecked(NS + "-zoom-shadow");
if (STATE.zoomFullRes) setChecked(NS + "-zoom-fullres");
if (STATE.zoomZEnabled) setChecked(NS + "-zoom-z-enabled");
if (STATE.hidePaidEnabled) setChecked(NS + "-hp-enabled");
if (STATE.hideTextPosts) setChecked(NS + "-htp-enabled");
if (STATE.hideCarousels) setChecked(NS + "-hc-enabled");
if (STATE.scrollToTopEnabled) setChecked(NS + "-stt-enabled");
if (STATE.zoomHideOverlays) setChecked(NS + "-zoom-hide");
if (STATE.mobileGridEnabled) setChecked(NS + "-mg-enabled");
if (STATE.moreResultsEnabled) setChecked(NS + "-mr-enabled");
if (STATE.adBlockEnabled) setChecked(NS + "-ab-enabled");
if (STATE.debug) setChecked(NS + "-debug");
if (STATE.debugShowAll) setChecked(NS + "-debug-showall");
setRange(NS + "-zoom-scale", STATE.zoomScale);
var scaleVal = panel.querySelector("#" + NS + "-zoom-scale-val");
if (scaleVal) scaleVal.textContent = STATE.zoomScale + "x";
setSelect(NS + "-zoom-speed", STATE.zoomSpeed);
setSelect(NS + "-mg-columns", String(STATE.mobileGridColumns));
}
// Build the settings panel and floating button if not already present
function buildPanel() {
if (document.getElementById(BTN_ID)) return togglePanel();
var btn = document.createElement("button");
btn.id = BTN_ID;
btn.title = "DA Toolbox";
btn.innerHTML = WRENCH_ICON;
btn.className = [
"fixed bottom-5 right-5 z-[2147483647]",
"w-12 h-12 rounded-full",
"flex items-center justify-center",
"cursor-pointer p-0 border-0",
"bg-[var(--md-surface-container-high)] text-white",
"transition-all duration-200 ease-panel",
].join(" ");
var panel = document.createElement("div");
panel.id = PANEL_ID;
panel.setAttribute("popover", "manual");
panel.className = [
"fixed",
"right-5",
"w-fit max-w-[calc(100vw-40px)] min-w-[260px]",
"rounded-2xl flex flex-col",
"bg-[var(--md-surface-container)] text-[var(--md-on-surface)]",
"text-sm",
].join(" ");
panel.style.bottom = (BTN_BOTTOM + BTN_SIZE + 8) + "px";
panel.innerHTML = PANEL_HTML;
syncPanelState(panel);
if (STATE.debugShowAll) panel.classList.add(NS + "-debug-showall");
btn.addEventListener("click", function (e) { togglePanel(e); });
document.documentElement.appendChild(btn);
document.documentElement.appendChild(panel);
// Close button (desktop id-based)
var closeBtn = document.getElementById(NS + "-close");
if (closeBtn) {
closeBtn.addEventListener("click", function (e) {
e.stopPropagation();
closePanel();
});
}
// Close button (mobile class-based)
panel.querySelectorAll("." + NS + "-close").forEach(function (btn) {
btn.addEventListener("click", (e) => {
e.stopPropagation();
closePanel();
});
});
// Tab switching
panel.querySelectorAll("." + NS + "-tab").forEach((tab) => {
tab.addEventListener("click", () => switchTab(tab.dataset.tab));
});
bindHoverControls(panel);
bindZoomTabControls(panel);
bindFilterControls(panel);
bindAdBlockControls(panel);
bindDebugControls(panel);
bindMobileGridControls(panel);
// Touch device detection: hide Hover and Zoom tabs
var isTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
if (isTouch) {
panel.querySelectorAll("." + NS + "-tab[data-tab='hover'], ." + NS + "-tab[data-tab='zoom']").forEach(function (t) {
t.classList.add(NS + "-touch-hidden");
});
document.getElementById(NS + "-sec-hover").classList.add(NS + "-touch-hidden");
document.getElementById(NS + "-sec-zoom").classList.add(NS + "-touch-hidden");
var firstVisible = panel.querySelector("." + NS + "-tab:not(." + NS + "-touch-hidden)");
if (firstVisible) switchTab(firstVisible.dataset.tab);
}
// Show mobile grid controls only when grid-row exists
updateMobileGridControlsVisibility();
hideEmptySections();
}
// Toggle panel open/close state
function togglePanel(e) {
const panel = document.getElementById(PANEL_ID);
if (!panel) return buildPanel();
if (panel.matches(":popover-open")) {
closePanel();
} else {
openPanel(panel, e && e.shiftKey);
}
}
function openPanel(panel, debugMode) {
panel.classList.toggle(NS + "-debug", !!debugMode);
repositionPanel(panel);
try {
panel.showPopover();
hideEmptySections();
// Auto-switch to Debug tab when opened with shift+click
if (debugMode) switchTab("debug");
var activeTab = panel.querySelector("." + NS + "-tab-active");
// If the active tab is now hidden (debug tab closed without debug), fall back
if (activeTab && activeTab.offsetHeight === 0) {
activeTab.classList.remove(NS + "-tab-active");
document.getElementById(NS + "-sec-" + activeTab.dataset.tab).classList.remove(NS + "-section-active");
activeTab = null;
}
if (activeTab) {
var indicator = panel.querySelector("." + NS + "-tab-indicator");
if (indicator) indicator.style.transition = "none";
updateTabIndicator(activeTab.dataset.tab);
requestAnimationFrame(function () {
if (indicator) indicator.style.transition = "";
});
} else {
// No visible tab active — switch to first visible tab
var firstVisible = Array.from(
panel.querySelectorAll("." + NS + "-tab")
).find(function(t) { return t.offsetHeight > 0; });
if (firstVisible) switchTab(firstVisible.dataset.tab);
}
var btn = document.getElementById(BTN_ID);
if (btn) btn.classList.add(NS + "-fab-open");
} catch (e) {
console.error("[" + NS + "] showPopover failed:", e);
}
}
function closePanel() {
var panel = document.getElementById(PANEL_ID);
if (panel && panel.matches(":popover-open")) {
panel.hidePopover();
var btn = document.getElementById(BTN_ID);
if (btn) btn.classList.remove(NS + "-fab-open");
}
}
// Keep panel within viewport bounds (CSS handles mobile full-screen + auto-width)
function repositionPanel(panel) {
if (window.innerWidth <= 600) return;
var vw = window.innerWidth;
var right = Math.min(20, vw - 260 - 20);
if (right < 20) right = 20;
panel.style.setProperty("right", right + "px", "important");
panel.style.setProperty("left", "auto", "important");
panel.style.setProperty("bottom", BTN_BOTTOM + BTN_SIZE + 8 + "px", "important");
panel.style.setProperty("position", "fixed", "important");
}
// Animate the tab indicator to the target tab's position
function updateTabIndicator(id) {
var nav = document.querySelector("." + NS + "-tab-nav");
if (!nav) return;
var indicator = nav.querySelector("." + NS + "-tab-indicator");
var target = nav.querySelector('[data-tab="' + id + '"]');
if (!indicator || !target) return;
indicator.style.left = target.offsetLeft + "px";
indicator.style.width = target.offsetWidth + "px";
}
// Switch the active tab and show its section
function switchTab(id) {
updateTabIndicator(id);
document.querySelectorAll("." + NS + "-tab").forEach((t) => {
t.classList.toggle(NS + "-tab-active", t.dataset.tab === id);
});
document.querySelectorAll("." + NS + "-section").forEach((s) => {
s.classList.toggle(NS + "-section-active", s.id === NS + "-sec-" + id);
});
hideEmptySections();
// Scroll the target tab into view if the nav overflows
var target = document.querySelector('.' + NS + '-tab-nav [data-tab="' + id + '"]');
if (target) target.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
}
function addPanelStyles() {
if (document.getElementById(PANEL_STYLE_ID)) return;
var s = document.createElement("style");
s.id = PANEL_STYLE_ID;
s.textContent = PANEL_STYLES;
document.head.appendChild(s);
}
// Wire up Hover tab controls to STATE and re-inject styles on change
function bindHoverControls(panel) {
panel.querySelector("#" + NS + "-zoom-enabled").addEventListener("change", (e) => {
STATE.zoomEnabled = e.target.checked;
saveSettings();
injectZoomStyles();
});
panel.querySelector("#" + NS + "-zoom-scale").addEventListener("input", (e) => {
const v = parseFloat(e.target.value);
STATE.zoomScale = v;
panel.querySelector("#" + NS + "-zoom-scale-val").textContent = v + "x";
saveSettings();
injectZoomStyles();
});
panel.querySelector("#" + NS + "-zoom-speed").addEventListener("change", (e) => {
STATE.zoomSpeed = e.target.value;
saveSettings();
injectZoomStyles();
});
panel.querySelector("#" + NS + "-zoom-shadow").addEventListener("change", (e) => {
STATE.zoomShadow = e.target.checked;
saveSettings();
injectZoomStyles();
});
panel.querySelector("#" + NS + "-zoom-hide").addEventListener("change", (e) => {
STATE.zoomHideOverlays = e.target.checked;
saveSettings();
updateImageOnlyClass();
if (typeof injectZoomStyles === "function") injectZoomStyles();
if (typeof rebuildOverlayGrid === "function") rebuildOverlayGrid();
});
}
// Wire up Zoom tab controls
function bindZoomTabControls(panel) {
panel.querySelector("#" + NS + "-zoom-fullres").addEventListener("change", (e) => {
STATE.zoomFullRes = e.target.checked;
saveSettings();
});
panel.querySelector("#" + NS + "-zoom-z-enabled").addEventListener("change", (e) => {
STATE.zoomZEnabled = e.target.checked;
saveSettings();
});
}
// Wire up Filters tab controls
function bindFilterControls(panel) {
panel.querySelector("#" + NS + "-hp-enabled").addEventListener("change", (e) => {
STATE.hidePaidEnabled = e.target.checked;
saveSettings();
if (!STATE.hidePaidEnabled) {
showAllPaid();
} else {
injectHidePaidStyles();
hidePaidScan();
}
updateHtmlClass();
if (typeof rebuildOverlayGrid === "function") rebuildOverlayGrid();
});
panel.querySelector("#" + NS + "-htp-enabled").addEventListener("change", (e) => {
STATE.hideTextPosts = e.target.checked;
saveSettings();
filterTextPosts();
});
panel.querySelector("#" + NS + "-hc-enabled").addEventListener("change", (e) => {
STATE.hideCarousels = e.target.checked;
saveSettings();
filterCarousels();
});
panel.querySelector("#" + NS + "-mr-enabled").addEventListener("change", (e) => {
STATE.moreResultsEnabled = e.target.checked;
saveSettings();
if (STATE.moreResultsEnabled) autoClickMoreResults();
});
panel.querySelector("#" + NS + "-stt-enabled").addEventListener("change", (e) => {
STATE.scrollToTopEnabled = e.target.checked;
saveSettings();
if (STATE.scrollToTopEnabled) {
initScrollBtn();
} else {
toggleScrollBtn(false);
removeScrollListener();
}
});
}
// Wire up Ad Block tab controls
function bindAdBlockControls(panel) {
panel.querySelector("#" + NS + "-ab-enabled").addEventListener("change", (e) => {
STATE.adBlockEnabled = e.target.checked;
saveSettings();
if (STATE.adBlockEnabled) {
blockAds();
}
});
}
// Wire up Debug tab controls
function bindDebugControls(panel) {
panel.querySelector("#" + NS + "-debug").addEventListener("change", (e) => {
STATE.debug = e.target.checked;
saveSettings();
log("debug mode", STATE.debug ? "enabled" : "disabled");
});
panel.querySelector("#" + NS + "-debug-showall").addEventListener("change", (e) => {
STATE.debugShowAll = e.target.checked;
saveSettings();
panel.classList.toggle(NS + "-debug-showall", STATE.debugShowAll);
// If the active tab is now hidden, fall back to the first visible tab
if (!STATE.debugShowAll) {
var active = panel.querySelector("." + NS + "-tab-active");
if (active && active.offsetHeight === 0) {
var firstVisible = Array.from(
panel.querySelectorAll("." + NS + "-tab")
).find(function(t) { return t.offsetHeight > 0; });
if (firstVisible) switchTab(firstVisible.dataset.tab);
}
}
});
}
// Wire up Mobile Grid controls
function bindMobileGridControls(panel) {
panel.querySelector("#" + NS + "-mg-enabled").addEventListener("change", function (e) {
STATE.mobileGridEnabled = e.target.checked;
saveSettings();
updateMobileGrid();
});
panel.querySelector("#" + NS + "-mg-columns").addEventListener("change", function (e) {
STATE.mobileGridColumns = parseInt(e.target.value, 10);
saveSettings();
updateOverlayColumns();
injectOverlayStyles();
});
}
// Show/hide mobile grid controls based on grid-row presence
function updateMobileGridControlsVisibility() {
var panel = document.getElementById(PANEL_ID);
if (!panel) return;
var hasGrid = !!document.querySelector('div[data-testid="grid-row"]');
panel.classList.toggle(NS + "-has-grid", hasGrid);
hideEmptySections();
}
// Hide sections where every child is hidden
function hideEmptySections() {
document.querySelectorAll("." + NS + "-section").forEach(function (sec) {
sec.classList.remove(NS + "-section-empty");
var style = getComputedStyle(sec);
if (style.display === "none") return;
var visible = Array.from(sec.children).some(function (el) {
return el.offsetHeight > 0 || el.getClientRects().length > 0;
});
if (!visible) sec.classList.add(NS + "-section-empty");
});
}
let _lastMgCard = null;
// Build CSS for card hover scale effect based on current settings
function cssScale() {
const dur = SPEED_MAP[STATE.zoomSpeed] || 0.25;
const scale = wheelScale ?? STATE.zoomScale;
const hide = STATE.zoomHideOverlays ? `
html.${NS}-image-only ${CARD_SEL} * {
visibility: hidden !important;
}
html.${NS}-image-only ${CARD_SEL} img {
visibility: visible !important;
}
html.${NS}-image-only ${CARD_SEL} img[alt*="avatar" i],
html.${NS}-image-only ${CARD_SEL} [class*="Avatar"] img,
html.${NS}-image-only ${CARD_SEL} [class*="avatar"] img {
visibility: hidden !important;
}
html.${NS}-image-only a.${NS}-mg-card * {
visibility: hidden !important;
}
html.${NS}-image-only a.${NS}-mg-card .${NS}-mg-img {
visibility: visible !important;
}
${CARD_SEL}.${ACTIVE} *,
a.${NS}-mg-card.${ACTIVE} * {
visibility: hidden !important;
}
${CARD_SEL}.${ACTIVE} img,
a.${NS}-mg-card.${ACTIVE} img {
visibility: visible !important;
}
` : "";
return `
${CARD_SEL} img,
a.${NS}-mg-card img {
max-width: revert !important;
}
${CARD_SEL},
a.${NS}-mg-card {
transition: transform ${dur}s cubic-bezier(0.25, 0.46, 0.45, 0.94),
box-shadow ${dur}s ease !important;
}
${CARD_SEL}.${ACTIVE},
a.${NS}-mg-card.${ACTIVE} {
will-change: transform;
transform: ${STATE.zoomEnabled ? `scale(${scale})` : "none"} !important;
z-index: 999999 !important;
position: relative !important;
pointer-events: auto !important;
background: #06070d !important;
${STATE.zoomShadow ? "box-shadow: 0 16px 48px rgba(0,0,0,0.5) !important;" : "box-shadow: none !important;"}
}
.${ACTIVE} { contain: none !important; }
${hide}
`;
}
// Inject or update zoom styles, plus spinner animation for full-res loading
function injectZoomStyles() {
let el = document.getElementById(STYLE_ID);
if (!el) {
el = document.createElement("style");
el.id = STYLE_ID;
document.head.appendChild(el);
}
el.textContent = cssScale() + `
${ROW_SEL} {
overflow: visible !important;
content-visibility: visible !important;
}
html.${NS}-zheld ${CARD_SEL}.${ACTIVE} {
transform: none !important;
box-shadow: none !important;
}
.${NS}-spinner {
position: fixed !important; z-index: 2147483647 !important;
top: 50% !important; left: 50% !important;
width: 36px !important; height: 36px !important;
margin: -18px 0 0 -18px !important;
border: 4px solid rgba(255,255,255,0.2) !important;
border-top-color: #2ecc71 !important;
border-radius: 50% !important;
animation: ${NS}-spin 0.7s linear infinite !important;
pointer-events: none !important;
}
@keyframes ${NS}-spin { to { transform: rotate(360deg); } }
`;
}
// On hover: tag card and apply active class with smart transform origin
function onHover(e) {
let card = e.target.closest(CARD_SEL);
if (!card) card = e.target.closest("a." + NS + "-mg-card");
if (card && card.classList.contains(NS + "-mg-card")) {
_lastMgCard = card;
}
if (!card || card.hasAttribute("data-" + NS)) return;
// Overlay cards are flat — no row walking needed
if (card.classList.contains(NS + "-mg-card")) {
card.setAttribute("data-" + NS, "1");
card.style.transformOrigin = getSmartOrigin(card);
card.classList.add(ACTIVE);
return;
}
const row = card.closest(ROW_SEL);
if (!row) return;
// Walk up through nested inline-blocks to find the outermost gallery card
while (card.parentElement && card.parentElement !== row && card.parentElement.closest(CARD_SEL)) {
card = card.parentElement.closest(CARD_SEL);
}
if (card.hasAttribute("data-" + NS)) return;
card.setAttribute("data-" + NS, "1");
card.style.transformOrigin = getSmartOrigin(card);
card.classList.add(ACTIVE);
row.classList.add(ACTIVE);
}
// On unhover: clean up classes, reset wheel scale, remove full-res clone
function onUnhover(e) {
const card = e.target.closest(CARD_SEL) || e.target.closest("a." + NS + "-mg-card");
if (!card) return;
if (e.relatedTarget && card.contains(e.relatedTarget)) return;
if (_lastMgCard === card) _lastMgCard = null;
card.removeAttribute("data-" + NS);
card.classList.remove(ACTIVE);
wheelScale = null;
removeClone();
if (!card.classList.contains(NS + "-mg-card")) {
const row = card.closest(ROW_SEL);
if (row && !row.querySelector("." + ACTIVE)) row.classList.remove(ACTIVE);
}
const dur = (SPEED_MAP[STATE.zoomSpeed] || 0.25) * 1000 + 50;
setTimeout(() => {
if (!card.classList.contains(ACTIVE)) card.style.transformOrigin = "";
}, dur);
}
// Determine transform origin so the card scales away from row/grid edges
function getSmartOrigin(card) {
if (card.classList.contains(NS + "-mg-card")) {
const p = card.parentNode;
if (!p) return "center center";
const cr = card.getBoundingClientRect();
const pr = p.getBoundingClientRect();
const isFirst = Math.abs(cr.left - pr.left) < 5;
const isLast = Math.abs(cr.right - pr.right) < 5;
const isTop = Math.abs(cr.top - pr.top) < 5;
const xOrigin = isFirst ? "left" : isLast ? "right" : "center";
const yOrigin = isTop ? "top" : "center";
return xOrigin + " " + yOrigin;
}
const row = card.closest(ROW_SEL);
if (!row) return "";
const cards = row.querySelectorAll(CARD_SEL);
const idx = Array.from(cards).indexOf(card);
const isFirst = idx === 0;
const isLast = idx === cards.length - 1;
const firstRow = document.querySelector(ROW_SEL);
const yOrigin = firstRow === row ? "top" : "center";
const xOrigin = isFirst ? "left" : isLast ? "right" : "center";
return xOrigin + " " + yOrigin;
}
// CSS classes for marking and hiding paid posts
const HIDDEN_CLASS = NS + "-hp-hidden";
const POST_CLASS = NS + "-hp-post";
// Inject CSS rules for hiding paid posts
function injectHidePaidStyles() {
if (document.getElementById(NS + "-hp-styles")) return;
const s = document.createElement("style");
s.id = NS + "-hp-styles";
s.textContent = [
"html:not(." + NS + "-hp-showing) " + CARD_SEL + ":has(img[src*=\"locked-small\"], img[src*=\"locked-large\"]) { display: none !important; }",
"html:not(." + NS + "-hp-showing) " + CARD_SEL + ":has([class*=\"unlock\"]) { display: none !important; }",
"html:not(." + NS + "-hp-showing) " + CAROUSEL_CARD_SEL + ":has(img[src*=\"locked-small\"], img[src*=\"locked-large\"]) { display: none !important; }",
"html:not(." + NS + "-hp-showing) " + CAROUSEL_CARD_SEL + ":has([class*=\"unlock\"]) { display: none !important; }",
"html:not(." + NS + "-hp-showing) section.NuU4Mu:has(img[src*=\"locked-small\"], img[src*=\"locked-large\"]) { display: none !important; }",
"html:not(." + NS + "-hp-showing) section.NuU4Mu:has([class*=\"unlock\"]) { display: none !important; }",
"." + POST_CLASS + "." + HIDDEN_CLASS + " { display: none !important; }",
].join("\n");
document.head.appendChild(s);
}
// Find the card container from any descendant, supporting both regular and carousel cards
function closestCard(el) {
return el.closest(CARD_SEL) || el.closest(CAROUSEL_CARD_SEL);
}
// Check if the button indicates a paid/subscription post
function isPaymentBtn(btn) {
if (btn.getAttribute("data-" + NS + "-hp") === "1") return false;
const overlay = btn.closest('[class*="wGXvLz"]');
if (!overlay) return false;
const text = btn.textContent.trim();
if (!/subscribe|sale|buy/i.test(text)) return false;
return true;
}
// Check if an element indicates an "Unlock Gallery" post
function isUnlockGallery(el) {
if (el.getAttribute("data-" + NS + "-hp") === "1") return false;
const overlay = el.closest('[class*="wGXvLz"]');
if (!overlay) return false;
if (!/unlock gallery/i.test(el.textContent)) return false;
return true;
}
// Mark a card as paid and hide it if filtering is active
function processPaidCard(card) {
if (card.classList.contains(POST_CLASS)) return;
card.classList.add(POST_CLASS);
if (STATE.hidePaidEnabled) {
card.classList.add(HIDDEN_CLASS);
}
}
// Remove paid-post markers from elements no longer in gallery rows
function hidePaidCleanup() {
document.querySelectorAll("." + POST_CLASS).forEach((el) => {
if (!el.closest(ROW_SEL)) {
el.classList.remove(POST_CLASS, HIDDEN_CLASS);
}
});
}
// Scan gallery rows for payment buttons and "Unlock Gallery" posts
function hidePaidScan() {
if (!STATE.hidePaidEnabled) return;
document.querySelectorAll(ROW_SEL + " button").forEach((btn) => {
if (!isPaymentBtn(btn)) return;
btn.setAttribute("data-" + NS + "-hp", "1");
const overlay = btn.closest('[class*="wGXvLz"]');
const card = closestCard(overlay);
if (card) processPaidCard(card);
});
document.querySelectorAll(ROW_SEL + " " + CARD_SEL + ", " + ROW_SEL + " " + CAROUSEL_CARD_SEL).forEach((card) => {
if (card.classList.contains(POST_CLASS)) return;
const unlockEl = card.querySelector('[class*="wGXvLz"] button, [class*="wGXvLz"] a, [class*="unlock"], [class*="gallery-locked"]');
if (unlockEl && isUnlockGallery(unlockEl)) {
unlockEl.setAttribute("data-" + NS + "-hp", "1");
processPaidCard(card);
}
});
// Scan for subscription tier cards (outside gallery rows)
document.querySelectorAll('a[aria-label*="subscription tier"]').forEach((link) => {
const card = closestCard(link);
if (card && !card.classList.contains(POST_CLASS)) {
processPaidCard(card);
}
});
// Scan for locked posts in mobile grid cards
document.querySelectorAll('div[data-testid="grid-row"] section.NuU4Mu').forEach((card) => {
if (card.classList.contains(POST_CLASS)) return;
if (card.querySelector('img[src*="locked"]')) {
processPaidCard(card);
}
});
}
// Reveal all previously hidden paid posts
function showAllPaid() {
document.querySelectorAll("." + POST_CLASS).forEach((p) => {
p.classList.remove(HIDDEN_CLASS);
});
}
// Schedule a paid-post scan on the next animation frame (throttled)
function hidePaidSchedule() {
if (paidScanTimer) return;
paidScanTimer = requestAnimationFrame(() => {
paidScanTimer = null;
hidePaidScan();
});
}
// Toggle the HTML-level class that hides paid posts via CSS
function updateHtmlClass() {
document.documentElement.classList.toggle(NS + "-hp-showing",
!STATE.hidePaidEnabled);
}
// CSS selectors for known DeviantArt ad, promo and upgrade elements
const AD_SELECTORS = [
".crYUnH", ".YSrWZY", ".DHdA8q", ".jqUgOy", ".p1Al36",
".Wd52TD", ".b4COXC", ".HPVs_J",
".qlAJA0", ".dgMz3k",
'img[alt="Banner"]', 'img[src*="offers-assets"]',
'iframe[src*="ads"]', '[title="DealerAdIframe"]',
'a[href*="core-membership"]',
'[class*="upgrade"]', '[class*="promo"]', '[class*="core-offer"]',
];
// Skip elements inside the header/nav or user menu — never block navigation UI
function inHeader(el) {
return !!el.closest(
'header, nav, [role="banner"], [class*="header"], [class*="nav-bar"], [class*="topbar"], #site-header-user-menu',
);
}
// Hard block known promo elements even inside the header (but not in the user menu)
function blockHeaderPromos() {
document.querySelectorAll('a[href*="core-membership"], [title="DealerAdIframe"]').forEach((el) => {
if (el.closest("#site-header-user-menu")) return;
el.style.setProperty("display", "none", "important");
const wrapper = el.closest('[class*="m4QFSe"], .ajOBFk');
if (wrapper) wrapper.style.setProperty("display", "none", "important");
});
}
// Hide all matched ad elements and their wrapper containers
function blockAds() {
if (!STATE.adBlockEnabled) return;
blockHeaderPromos();
document.querySelectorAll(AD_SELECTORS.join(",")).forEach((el) => {
if (inHeader(el)) return;
el.style.setProperty("display", "none", "important");
const wrapper = el.closest(
'a, [class*="banner"], [class*="ad"], [class*="promo"], [class*="offer"]',
);
if (wrapper) wrapper.style.setProperty("display", "none", "important");
});
// Also hide promotional text links
document.querySelectorAll('a, [class*="PAYVSS"], [class*="cfJSy4"]').forEach((a) => {
if (inHeader(a)) return;
if (/upgrade|get core|treat yourself|50% off|premium content/i.test(a.textContent)) {
a.style.setProperty("display", "none", "important");
}
});
}
// Hide the "Text Posts" carousel (journals & status updates) in the watch feed
function filterTextPosts() {
if (!STATE.hideTextPosts) return;
document.querySelectorAll('section.VsAf1f.UR2gWo, section.VsAf1f.bxadal, section.VsAf1f.QwWbAQ').forEach((el) => {
el.style.setProperty("display", "none", "important");
});
}
// Hide feed carousels and "Recommended for You" section
function filterCarousels() {
if (!STATE.hideCarousels) return;
document.querySelectorAll('div[data-testid="content_row"], div[data-testid="grid-row"]').forEach((row) => {
if (row.querySelector('h3') && /recommended for you|from deviants you watch|explore recent deviations|daily deviations/i.test(row.textContent)) {
row.style.setProperty("display", "none", "important");
}
});
}
// Scroll-to-top button
const STT_ID = NS + "-stt";
const STT_SHOW_OFFSET = 600;
const STT_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5m0 0l-7 7m7-7l7 7"/></svg>';
function buildScrollBtn() {
if (document.getElementById(STT_ID)) return;
const btn = document.createElement("button");
btn.id = STT_ID;
btn.title = "Scroll to top";
btn.innerHTML = STT_SVG;
btn.setAttribute("aria-label", "Scroll to top");
btn.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});
document.documentElement.appendChild(btn);
}
function toggleScrollBtn(show) {
const btn = document.getElementById(STT_ID);
if (!btn) return;
btn.classList.toggle(NS + "-stt-visible", show);
}
function onScroll() {
toggleScrollBtn(window.scrollY > STT_SHOW_OFFSET);
}
function removeScrollListener() {
window.removeEventListener("scroll", onScroll, { passive: true });
}
function initScrollBtn() {
buildScrollBtn();
window.addEventListener("scroll", onScroll, { passive: true });
if (window.scrollY > STT_SHOW_OFFSET) toggleScrollBtn(true);
}
const OVERLAY_ID = NS + "-mg-overlay";
const OVERLAY_STYLE_ID = NS + "-mg-overlay-styles";
const MG_ACTIVE_CLASS = NS + "-mg-active";
const _posts = [];
let _scrollSentinel = null;
// Return CSS grid-template-columns value from user setting
function getGridTemplate() {
var n = STATE.mobileGridColumns;
return n > 0 ? "repeat(" + n + ", 1fr)" : "repeat(auto-fill, minmax(160px, 1fr))";
}
// Inject or update overlay grid + card styles
function injectOverlayStyles() {
let el = document.getElementById(OVERLAY_STYLE_ID);
if (!el) {
el = document.createElement("style");
el.id = OVERLAY_STYLE_ID;
document.head.appendChild(el);
}
el.textContent = `
html.${MG_ACTIVE_CLASS} div[data-testid="grid-row"] {
display: none !important;
}
#${OVERLAY_ID} {
display: grid;
grid-template-columns: ${getGridTemplate()};
gap: 4px;
pointer-events: auto;
}
#${OVERLAY_ID} .${NS}-mg-card {
display: block;
aspect-ratio: 1 / 1;
overflow: hidden;
text-decoration: none;
position: relative;
}
#${OVERLAY_ID} .${NS}-mg-card .${NS}-mg-img {
display: block;
width: 100%; height: 100%;
object-fit: cover;
object-position: center;
}
#${OVERLAY_ID} .${NS}-mg-meta {
position: absolute; bottom: 0; left: 0; right: 0;
background: linear-gradient(transparent, rgba(0,0,0,0.85));
padding: 6px 8px; display: flex; align-items: center;
justify-content: space-between; pointer-events: none;
}
#${OVERLAY_ID} .${NS}-mg-left {
display: flex; align-items: center; gap: 6px; min-width: 0;
}
#${OVERLAY_ID} .${NS}-mg-right {
display: flex; align-items: center; gap: 3px; flex-shrink: 0;
}
#${OVERLAY_ID} .${NS}-mg-avatar {
width: 24px; height: 24px; border-radius: 50%;
object-fit: cover; flex-shrink: 0;
}
#${OVERLAY_ID} .${NS}-mg-username {
color: #fff; font-size: 11px; white-space: nowrap;
overflow: hidden; text-overflow: ellipsis;
}
#${OVERLAY_ID} .${NS}-mg-star {
color: #fff; width: 12px; height: 12px; flex-shrink: 0;
}
#${OVERLAY_ID} .${NS}-mg-likes {
color: #fff; font-size: 11px; opacity: 0.8; font-weight: 700;
}
`;
}
function removeOverlayStyles() {
const el = document.getElementById(OVERLAY_STYLE_ID);
if (el) el.remove();
}
// Extract metadata (img, link, avatar, username, likes) from a DA section
function extractPost(section) {
if (section.classList.contains(NS + "-hp-hidden")) return null;
if (section.style.display === "none") return null;
if (STATE.hidePaidEnabled && section.querySelector('img[src*="locked-small"], img[src*="locked-large"]')) return null;
if (STATE.hidePaidEnabled && section.querySelector('[class*="unlock"]')) return null;
var row = section.closest('[data-testid="grid-row"], [data-testid="content_row"]');
if (row && row.style.display === "none") return null;
var img = section.querySelector('img[property="contentUrl"]')
|| section.querySelector('img[src]:not([alt*="avatar" i])');
if (!img) return null;
var link = section.querySelector('a[href*="/art/"]') || section.closest('a');
var avatarEl = section.querySelector('img[alt*="avatar" i]');
var avatarSrc = avatarEl ? (avatarEl.src || avatarEl.getAttribute("src") || "") : "";
// Username from URL pathname — more reliable than data-username in grid rows
var username = "";
if (link) {
var up = link.pathname.split('/');
if (up.length >= 4) username = up[1];
}
var likes = "";
var favBtn = section.querySelector('button[aria-label*="Favourite" i]');
if (favBtn) {
var cs = favBtn.querySelector(':scope > span:last-child');
if (cs) likes = cs.textContent.trim();
}
return {
imgSrc: img.src || img.getAttribute("src") || "",
href: link ? link.href : "",
avatarSrc: avatarSrc,
username: username,
likes: likes,
};
}
// Build a grid-mode card element from extracted data
function createCard(data) {
var card = document.createElement("a");
card.className = NS + "-mg-card";
if (data.href) card.href = data.href;
card.target = "_blank";
card.rel = "noopener noreferrer";
var wrap = document.createElement("div");
wrap.style.width = "100%";
wrap.style.height = "100%";
wrap.style.position = "relative";
var img = document.createElement("img");
img.className = NS + "-mg-img";
img.src = data.imgSrc;
img.loading = "lazy";
wrap.appendChild(img);
// Metadata overlay — skipped when image-only mode is active
if (!STATE.zoomHideOverlays && (data.avatarSrc || data.username || data.likes)) {
var meta = document.createElement("div");
meta.className = NS + "-mg-meta";
var left = document.createElement("div");
left.className = NS + "-mg-left";
if (data.avatarSrc) {
var av = document.createElement("img");
av.className = NS + "-mg-avatar";
av.src = data.avatarSrc;
av.loading = "lazy";
left.appendChild(av);
}
if (data.username) {
var un = document.createElement("span");
un.className = NS + "-mg-username";
un.textContent = data.username;
left.appendChild(un);
}
meta.appendChild(left);
if (data.likes) {
var right = document.createElement("div");
right.className = NS + "-mg-right";
var lk = document.createElement("span");
lk.className = NS + "-mg-likes";
lk.textContent = data.likes;
right.appendChild(lk);
var star = document.createElementNS("http://www.w3.org/2000/svg", "svg");
star.setAttribute("class", NS + "-mg-star");
star.setAttribute("width", "12");
star.setAttribute("height", "12");
star.setAttribute("viewBox", "0 0 24 24");
star.setAttribute("fill", "none");
star.setAttribute("stroke", "currentColor");
star.setAttribute("stroke-width", "2.2");
star.setAttribute("stroke-linecap", "round");
star.setAttribute("stroke-linejoin", "round");
var path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z");
star.appendChild(path);
right.appendChild(star);
meta.appendChild(right);
}
wrap.appendChild(meta);
}
card.appendChild(wrap);
return card;
}
// Find the first grid-row parent container
function getWrapper() {
var rows = document.querySelectorAll('div[data-testid="grid-row"]');
return rows.length ? rows[0].parentNode : null;
}
// Collect all posts from DA grid rows, deduped by href
function seedPosts() {
_posts.length = 0;
document.querySelectorAll('div[data-testid="grid-row"] section').forEach(function (section) {
var data = extractPost(section);
if (data && !_posts.some(function (p) { return p.href === data.href; })) {
_posts.push(data);
}
});
}
// Scan for new posts appended by DA infinite scroll and insert them
function appendNewPosts() {
var added = 0;
document.querySelectorAll('div[data-testid="grid-row"]').forEach(function (row) {
row.querySelectorAll('section').forEach(function (section) {
var data = extractPost(section);
if (!data) return;
if (_posts.some(function (p) { return p.href === data.href; })) return;
_posts.push(data);
var overlay = document.getElementById(OVERLAY_ID);
if (overlay && _scrollSentinel) {
overlay.insertBefore(createCard(data), _scrollSentinel);
}
added++;
});
});
if (added) log("appended", added, "posts");
return added;
}
// Append all seeded cards to the overlay element
function populateOverlay(overlay) {
_posts.forEach(function (p) {
overlay.appendChild(createCard(p));
});
_scrollSentinel = document.createElement("div");
_scrollSentinel.style.height = "1px";
overlay.appendChild(_scrollSentinel);
}
// Create the overlay grid, hide original DA rows
function buildOverlayGrid() {
var wrapper = getWrapper();
if (!wrapper) return;
if (document.getElementById(OVERLAY_ID)) return;
document.documentElement.classList.add(MG_ACTIVE_CLASS);
var overlay = document.createElement("div");
overlay.id = OVERLAY_ID;
populateOverlay(overlay);
wrapper.insertBefore(overlay, wrapper.firstChild);
log("built overlay with", _posts.length, "posts");
// Re-sync STT after page reflow from grid replacement
if (typeof toggleScrollBtn === "function") {
toggleScrollBtn(window.scrollY > STT_SHOW_OFFSET);
}
// Re-create menu button if DA React removed it
if (typeof buildPanel === "function" && !document.getElementById(BTN_ID)) {
buildPanel();
}
}
// Remove overlay and restore original DA grid rows
function destroyOverlayGrid() {
var overlay = document.getElementById(OVERLAY_ID);
if (overlay) overlay.remove();
document.documentElement.classList.remove(MG_ACTIVE_CLASS);
_scrollSentinel = null;
}
// Toggle grid overlay on/off based on STATE.mobileGridEnabled
function updateMobileGrid() {
if (STATE.mobileGridEnabled) {
injectOverlayStyles();
if (document.querySelector('div[data-testid="grid-row"]')) {
seedPosts();
buildOverlayGrid();
}
} else {
destroyOverlayGrid();
removeOverlayStyles();
_posts.length = 0;
}
}
// Rebuild all cards in-place (used when toggles affect card content)
function rebuildOverlayGrid() {
if (!STATE.mobileGridEnabled) return;
var overlay = document.getElementById(OVERLAY_ID);
if (overlay) {
while (overlay.firstChild) overlay.removeChild(overlay.firstChild);
seedPosts();
populateOverlay(overlay);
} else {
destroyOverlayGrid();
seedPosts();
buildOverlayGrid();
}
}
// Update column count without rebuilding the full overlay
function updateOverlayColumns() {
var overlay = document.getElementById(OVERLAY_ID);
if (overlay) overlay.style.gridTemplateColumns = getGridTemplate();
}
// Handle DOM mutations: detect new/removed grid-rows, sync overlay
function onMobileGridMutation(mutations) {
if (!STATE.mobileGridEnabled) return;
var changed = Array.from(mutations).some(function (m) {
return m.type === "childList" && Array.from(m.addedNodes).concat(Array.from(m.removedNodes)).some(function (n) {
return n.nodeType === 1 && (n.matches && n.matches('[data-testid="grid-row"]') || n.querySelector && n.querySelector('[data-testid="grid-row"]'));
});
});
if (changed) {
if (document.getElementById(OVERLAY_ID)) {
appendNewPosts();
} else {
seedPosts();
buildOverlayGrid();
}
if (typeof updateMobileGridControlsVisibility === "function") updateMobileGridControlsVisibility();
}
}
const MR_SEL = '.EVgt_3 a';
function autoClickMoreResults() {
if (!STATE.moreResultsEnabled) return;
var links = document.querySelectorAll(MR_SEL);
for (var i = 0; i < links.length; i++) {
var a = links[i];
if (a.dataset.datbProcessed) continue;
if (a.textContent.includes("More Results") && a.offsetParent !== null) {
a.dataset.datbProcessed = "1";
a.click();
break;
}
}
}
// Parse max dimensions from a wixmp JWT token payload
function getMaxFromToken(url) {
const m = url.match(/\?token=([^&]+)/);
if (!m) return null;
try {
const parts = m[1].split(".");
const payload = atob(parts[1].replace(/-/g, "+").replace(/_/g, "/"));
const data = JSON.parse(payload);
if (data.obj && data.obj[0] && data.obj[0][0]) {
const dims = data.obj[0][0];
const maxW = parseInt(String(dims.width).replace(/[^0-9]/g, ""), 10);
const maxH = parseInt(String(dims.height).replace(/[^0-9]/g, ""), 10);
if (maxW && maxH) return { w: maxW, h: maxH };
}
} catch (e) {
log("token parse error:", e);
}
return null;
}
// Rewrite a wixmp image URL to request the highest available resolution
function getFullResUrl(src, rectW, rectH) {
log("getFullResUrl input:", src);
if (!src || !src.includes("wixmp.com")) {
log("not wixmp, returning as-is");
return src;
}
const max = getMaxFromToken(src);
const limitW = max ? max.w : Math.round(Math.max(rectW, 2000));
const limitH = max ? max.h : Math.round(Math.max(rectH, 2000));
let w, h;
if (max) {
w = limitW;
h = limitH;
} else {
const aspect = rectW / rectH;
if (aspect >= 1) {
w = limitW;
h = Math.round(limitW / aspect);
if (h > limitH) { h = limitH; w = Math.round(limitH * aspect); }
} else {
h = limitH;
w = Math.round(limitH * aspect);
if (w > limitW) { w = limitW; h = Math.round(limitW / aspect); }
}
}
const changed = src.replace(
/\/v1\/([^/]+)\/([^/]+)(?=\/[^/]+$)/,
(match, mode, params) => {
params = params.replace(/w_\d+/g, "w_" + w);
params = params.replace(/h_\d+/g, "h_" + h);
params = params.replace(/q_\d+/g, "q_100");
return "/v1/" + mode + "/" + params;
},
);
if (changed === src) {
log("regex did not match, returning as-is");
return src;
}
const cleaned = changed.replace("wixmp.com/intermediary/", "wixmp.com/");
log("getFullResUrl output:", cleaned);
return cleaned;
}
// Create a centered full-resolution clone of the hovered card's image
function createClone(card) {
if (!card) return;
if (zCloneCard === card) return;
const img = card.querySelector("img:not([alt*='avatar' i])");
if (!img) return;
removeClone();
const rect = img.getBoundingClientRect();
const nw = img.naturalWidth || img.width || rect.width;
const nh = img.naturalHeight || img.height || rect.height;
const clone = img.cloneNode(true);
clone.alt = "";
clone.removeAttribute("width");
clone.removeAttribute("height");
clone.style.cssText = [
"position: fixed !important",
"z-index: 2147483647 !important",
"top: 50% !important",
"left: 50% !important",
"translate: -50% -50% !important",
"pointer-events: none !important",
"contain: layout style !important",
"object-fit: contain !important",
"width: " + nw + "px !important",
"height: " + nh + "px !important",
"max-width: 95vw !important",
"max-height: 95vh !important",
].join(";");
const s = wheelScale ?? 2;
clone.style.transform = "scale(" + s + ")";
zClone = clone;
zCloneCard = card;
document.documentElement.appendChild(clone);
if (STATE.zoomFullRes) {
const fullUrl = getFullResUrl(
img.src || img.getAttribute("src"),
rect.width,
rect.height,
);
log("createClone img.src:", img.src, "fullUrl:", fullUrl);
if (fullUrl && fullUrl !== (img.src || img.getAttribute("src"))) {
zSpinner = document.createElement("div");
zSpinner.className = NS + "-spinner";
document.documentElement.appendChild(zSpinner);
const fullImg = new Image();
fullImg.onload = () => {
if (zClone === clone) {
clone.src = fullImg.src;
if (zSpinner) { zSpinner.remove(); zSpinner = null; }
}
};
fullImg.onerror = () => {
if (zSpinner) { zSpinner.remove(); zSpinner = null; }
};
fullImg.src = fullUrl;
}
}
}
function updateClone() {
if (!zClone) return;
const s = wheelScale ?? 2;
zClone.style.transform = "scale(" + s + ")";
}
function removeClone() {
if (zSpinner) { zSpinner.remove(); zSpinner = null; }
if (!zClone) return;
zClone.remove();
zClone = null;
zCloneCard = null;
}
function onWheel(e) {
if (!zHeld || !STATE.zoomZEnabled) return;
const card = document.querySelector("." + ACTIVE);
if (!card && !zClone) return;
e.preventDefault();
const delta = e.deltaY > 0 ? -0.2 : 0.2;
const base = zClone ? 2 : STATE.zoomScale;
const current = wheelScale ?? base;
const min = zClone ? 1 : base;
const max = 100;
const updated = Math.round(Math.min(max, Math.max(min, current + delta)) * 100) / 100;
if (updated === current) return;
wheelScale = updated;
if (zClone) {
updateClone();
} else {
injectZoomStyles();
}
}
function onKeyDown(e) {
if ((e.key === "z" || e.key === "Z") && STATE.zoomZEnabled) {
zHeld = true;
document.documentElement.classList.add(NS + "-zheld");
var card = document.querySelector(CARD_SEL + "." + ACTIVE);
if (!card) card = document.querySelector("a." + NS + "-mg-card." + ACTIVE);
if (!card) card = document.querySelector("a." + NS + "-mg-card:hover");
if (!card && _lastMgCard) card = _lastMgCard;
if (card) createClone(card);
}
}
function onKeyUp(e) {
if (e.key === "z" || e.key === "Z") {
zHeld = false;
wheelScale = null;
document.documentElement.classList.remove(NS + "-zheld");
removeClone();
}
}
// Debounce guard for MutationObserver callback
let _mutationRAF = null;
// Batch handler for DOM mutations: throttle via rAF, run all feature passes
function onMutation(mutations) {
if (_mutationRAF) return;
_mutationRAF = requestAnimationFrame(() => {
_mutationRAF = null;
blockAds();
if (STATE.hidePaidEnabled) hidePaidSchedule();
filterTextPosts();
filterCarousels();
if (STATE.mobileGridEnabled && typeof onMobileGridMutation === "function") {
onMobileGridMutation(mutations);
}
autoClickMoreResults();
});
}
// Entrypoint — called once when DOM is ready
function init() {
loadSettings();
addPanelStyles();
injectZoomStyles();
injectHidePaidStyles();
buildPanel();
blockAds();
filterTextPosts();
filterCarousels();
if (STATE.scrollToTopEnabled) initScrollBtn();
updateHtmlClass();
updateImageOnlyClass();
if (STATE.hidePaidEnabled) hidePaidScan();
updateMobileGrid();
autoClickMoreResults();
// Event listeners for zoom/hover and Z-zoom
document.addEventListener("mouseover", onHover, true);
document.addEventListener("mouseout", onUnhover, true);
document.addEventListener("keydown", onKeyDown);
document.addEventListener("keyup", onKeyUp);
document.addEventListener("wheel", onWheel, { passive: false, capture: true });
// Watch for DA's React-driven DOM changes
const observer = new MutationObserver(onMutation);
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style"] });
}
// Auto-boot: wait for DOMContentLoaded if page is still loading
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();