Imagefap Gallery Viewer Pro

Transforms ImageFap into a seamless, continuous LightGallery viewer and Masonry Grid with concurrent background fetching and smart batch downloading.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Imagefap Gallery Viewer Pro
// @namespace    captureitshawn
// @description  Transforms ImageFap into a seamless, continuous LightGallery viewer and Masonry Grid with concurrent background fetching and smart batch downloading.
// @version      3.0.6
// @author       Shawn Parker
// @license      MIT
// @match        https://www.imagefap.com/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/js/lightgallery.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/lg-thumbnail.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/lg-zoom.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/lg-autoplay.min.js
// @icon64       https://img.icons8.com/?size=100&id=GDM4AYUncYxx&format=png
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// ==/UserScript==

/*
=== CHANGELOG ===
v3.0.6
- Bugfix: Prevented Deduplicator from self-cannibalizing original layout elements on subsequent MutationObserver passes.

v3.0.5
- Bugfix: Fixed Javascript hoisting bug in DOM Deduplicator that caused "pfSeenIds is undefined" on initial page load.

v3.0.4
- Feature: Added DOM Deduplicator (Gatekeeper) using MutationObserver to prevent pagination drift and filter out duplicate images/galleries injected by Infinite Scroll plugins.

v3.0.3
- Bugfix: Replaced raw regex parsing with robust DOM extraction to guarantee first photo ID fetch.
- Bugfix: Added 'X-Requested-With' and 'Referer' headers to background fetcher to prevent server/Cloudflare blocking (Fixes offset 0 fetch errors).
- Bugfix: Added dynamic fallback to fetch from /pictures/ gallery page if photoId is unavailable.
- Bugfix: Prevented currentGalleryIndex from resetting to -1 during asynchronous LightGallery transitions, restoring seamless Next Gallery navigation.
- Added global 404 image replacement to inject a fallback icon when images are deleted or unresolvable.
- Reorganized CSS into cohesive thematic blocks for easier maintenance.
*/

const head_inject = `
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/sachinchoolur/lightgallery.js@master/dist/css/lightgallery.css">
<style>

/* Target the main structural blocks instead of everything */
body, html, div, table, tbody, tr, td, th, section, center, a, font, span {
    background-color: #1f1f28 !important;
    color: steelblue !important;
    border-color: #515166 !important;
}

/* Clean up link styling */
a, a font, a b, button {
    color: steelblue !important;
    text-decoration: salmon underline !important;
    transition: all 0.3s ease-in-out !important;
    text-underline-position: under !important;
}
input, select, textarea{
background: transparent !important;
color: steelblue !important;
text-decoration: none !important;
border: 1px dotted Salmon !important;
}

/* -----------------------------------Testing */
#main center table tbody td b {
  font-size: 16px !important;
  margin: 10px !important;
  display: inline-block;
  border-left: 3px solid salmon;
  padding-left: 10px !important;
  text-decoration: none !important;
  transition: transform ease 0.5s !important;
}
/* --------------------------------- Testing Ends */
.changecat a{
text-decoration: none !important;
}
.changecat{
text-align: center;
}
a:hover, a font:hover, a b:hover, button:hover, input:hover {
    color: salmon !important; /* Makes links pop when hovered */
    text-decoration: salmon underline !important;
    text-underline-position: under !important;
}

.darklink b u{
color: salmon !important;
}

td[align="center"] a img{
padding: 2px !important;
background: steelblue !important;
border: none !important;
border-radius: 10px !important;
box-shadow: 0 0px 7px steelblue !important;
}

#main table tr form table, .expp-container table, td[valign="top"] table tbody tr td[align="center"]{
border: 1px #515166 dotted !important;
}

/* Keep your element hider */
.tnaBarBlueWrap {
    display: none !important;
}


/* Fix LightGallery dynamic thumbnail wrap bug */
.lg-outer .lg-thumb {
    width: max-content !important;
    white-space: nowrap !important;
}
.lg-outer .lg-thumb-item {
    display: inline-block !important;
    float: none !important;
}

.lg-toolbar.group{
background: #1f1f28 !important;
}

/* LightGallery Blurred Transparent Backdrop */
.lg-backdrop {
    background-color: rgba(31, 31, 40, 0.75) !important; /* Semi-transparent #1f1f28 */
    backdrop-filter: blur(8px) !important;
    -webkit-backdrop-filter: blur(8px) !important;
}

/* Ensure LightGallery containers are transparent to show the blur */
.lg-outer, .lg-inner, .lg-item, .lg, .lg-outer center, .lg-outer div, .lg-outer .lg-toggle-thumb {
    background-color: transparent !important;
}

/* Dynamically resize image to avoid hiding behind toolbar and thumbnails */
.lg-outer .lg-img-wrap {
    box-sizing: border-box !important;
    transition: padding 0.3s ease-in-out !important;
}

/* Add padding for the top toolbar when UI is visible */
.lg-outer:not(.lg-hide-items) .lg-img-wrap {
    padding-top: 60px !important;
}

/* Add padding for the bottom thumbnails when they are open and UI is visible */
.lg-outer:not(.lg-hide-items).lg-thumb-open .lg-img-wrap {
    padding-bottom: 120px !important;
}

/* Ensure the image scales down smoothly within the padded box */
.lg-outer .lg-image {
    box-shadow: 0 0px 20px steelblue !important;
    border-radius: 10px !important;
    max-height: 100% !important;
    transition: max-height 0.3s ease-in-out, transform 0.3s ease-in-out !important;
}

.lg-outer .lg-toggle-thumb{
left: 20px !important;
}

.lg-thumb-item.active{
box-shadow: 0 0 10px 10px steelblue;
transition: 1s ease;
}

#overlay {
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
z-index: 2000;
background-color: #000;
opacity: 0.7;
-webkit-animation: slbOverlay 0.5s;
-moz-animation: slbOverlay 0.5s;
animation: slbOverlay 0.5s;
}
.lds-ellipsis {
display: inline-block;
position: relative;
width: 64px;
height: 64px;
}
.lds-ellipsis div {
position: absolute;
top: 27px;
width: 11px;
height: 11px;
border-radius: 50%;
background: #fff;
animation-timing-function: cubic-bezier(0, 1, 1, 0);
}
.lds-ellipsis div:nth-child(1) {
left: 6px;
animation: lds-ellipsis1 0.6s infinite;
}
.lds-ellipsis div:nth-child(2) {
left: 6px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(3) {
left: 26px;
animation: lds-ellipsis2 0.6s infinite;
}
.lds-ellipsis div:nth-child(4) {
left: 45px;
animation: lds-ellipsis3 0.6s infinite;
}
@keyframes lds-ellipsis1 {
0% {
transform: scale(0);
}
100% {
transform: scale(1);
}
}
@keyframes lds-ellipsis3 {
0% {
transform: scale(1);
}
100% {
transform: scale(0);
}
}
@keyframes lds-ellipsis2 {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(19px, 0);
}
}
#overlay {
display:none;
}
#overlay .lds-ellipsis {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#overlay-progress {
position: absolute;
top: calc(50% + 45px);
left: 50%;
transform: translate(-50%, 0);
color: #fff;
font-family: Arial, sans-serif;
font-size: 14px;
white-space: nowrap;
}

#pf-inline-title {
color: #999;
font-size: 16px;
margin-left: 12px;
margin-top: 12px;
text-transform: uppercase;
opacity: 1;
vertical-align: middle;
max-width: 50vw;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#pf-toast {
position: fixed;
bottom: 30px;
left: 50%;
transform: translate(-50%, 20px);
background: rgba(0,0,0,0.85);
color: #fff;
padding: 10px 20px;
border-radius: 6px;
font-family: Arial, sans-serif;
font-size: 14px;
z-index: 3000;
opacity: 0;
transition: opacity 0.4s, transform 0.4s;
}
#pf-toast.show {
opacity: 1;
transform: translate(-50%, 0);
}

/* ===================== [GRID MODAL STYLES] ===================== */
#pf-scroll-top {
    position: fixed;
    bottom: 24px;
    left: 24px;
    width: 44px;
    height: 44px;
    background: var(--pf-accent);
    color: #fff;
    border: none;
    border-radius: 50%;
    cursor: pointer;
    box-shadow: 0 4px 12px var(--pf-shadow);
    z-index: 3800;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.3s ease, transform 0.2s ease;
    transform: translateY(10px);
}
#pf-scroll-top.visible {
    opacity: 1;
    pointer-events: auto;
    transform: translateY(0);
}
#pf-scroll-top:hover {
    background: var(--pf-accent-hover);
    transform: translateY(-2px);
}

#pf-grid-modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background: var(--pf-modal-bg);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    z-index: 3500;
    box-sizing: border-box;
    flex-direction: column;
    color: #fff;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

#pf-grid-modal.open {
    display: flex !important;
}

#pf-grid-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 24px;
    background: var(--pf-header-bg);
    border-bottom: 1px solid var(--pf-border);
    flex-shrink: 0;
}

#pf-grid-title {
    font-size: 16px;
    font-weight: 600;
    letter-spacing: 0.5px;
    color: var(--pf-text-main);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 40vw;
}

#pf-grid-controls {
    display: flex;
    align-items: center;
    gap: 10px;
}

.pf-col-btn {
    background: var(--pf-bg-transparent);
    border: 1px solid var(--pf-border);
    color: var(--pf-text-muted);
    padding: 6px 14px;
    border-radius: 6px;
    font-size: 13px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.2s ease;
}

.pf-col-btn:hover {
    background: var(--pf-border-hover);
    color: #fff;
}

.pf-col-btn.active {
    background: var(--pf-accent);
    border-color: var(--pf-accent);
    color: #fff;
    box-shadow: 0 0 12px var(--pf-shadow-accent);
}

#pf-grid-close {
    background: transparent;
    border: none;
    color: var(--pf-text-muted);
    font-size: 26px;
    cursor: pointer;
    padding: 0 8px;
    line-height: 1;
    transition: color 0.2s ease;
}

#pf-grid-close:hover {
    color: #fff;
}

#pf-grid-body {
    flex: 1;
    overflow-y: auto;
    padding: 24px;
    box-sizing: border-box;
}

#pf-grid-masonry {
    display: flex;
    width: 100%;
    gap: 16px;
    align-items: flex-start;
}
.pf-masonry-column {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 16px;
    min-width: 0;
}

.pf-grid-card {
    background: var(--pf-card-bg);
    border-radius: 8px;
    overflow: hidden;
    cursor: pointer;
    border: 2px solid transparent;
    transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
    box-shadow: 0 4px 12px var(--pf-shadow);
}

.pf-grid-card:hover {
    transform: scale(1.025);
    border-color: var(--pf-accent);
    box-shadow: 0 8px 24px var(--pf-shadow-accent);
}

.pf-grid-card.active {
    border-color: var(--pf-accent);
    box-shadow: 0 0 20px var(--pf-shadow-accent);
}

.pf-grid-card img {
    display: block;
    width: 100%;
    height: auto;
    object-fit: cover;
    background: var(--pf-card-img-bg);
    min-height: 120px;
    transition: opacity 0.3s ease;
}

#pf-grid-btn {
    background: transparent;
    border: none;
    color: #999;
    cursor: pointer;
    float: right;
    width: 47px;
    height: 47px;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0;
    transition: color 0.2s ease, opacity 0.2s ease;
}

#pf-grid-btn:hover {
    color: #fff;
}

/* ===================== [GRID CHECKBOX & SELECTION STYLES] ===================== */
.pf-grid-card {
    position: relative;
}

.pf-grid-checkbox {
    position: absolute;
    top: 10px;
    right: 10px;
    width: 22px;
    height: 22px;
    cursor: pointer;
    z-index: 20;
    accent-color: var(--pf-accent);
    opacity: 0;
    transition: opacity 0.2s ease, transform 0.2s ease;
    transform: scale(0.95);
    box-shadow: 0 2px 8px rgba(0,0,0,0.6);
}

.pf-grid-card:hover .pf-grid-checkbox,
.pf-grid-card.selected .pf-grid-checkbox {
    opacity: 1;
    transform: scale(1);
}

.pf-grid-card.selected {
    border-color: var(--pf-accent) !important;
    box-shadow: 0 0 18px var(--pf-shadow-accent) !important;
}

.pf-grid-card.selected::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: var(--pf-accent-transparent);
    pointer-events: none;
    z-index: 5;
}

/* ===================== [FLOATING BATCH DOWNLOAD BAR] ===================== */
#pf-grid-download-bar {
    display: none;
    position: fixed;
    bottom: 24px;
    right: 24px;
    background: var(--pf-card-bg);
    border: 1px solid var(--pf-border);
    border-radius: 12px;
    padding: 14px 20px;
    z-index: 3800;
    box-shadow: 0 16px 36px var(--pf-shadow);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    color: var(--pf-text-main);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    min-width: 320px;
    box-sizing: border-box;
    transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

#pf-grid-download-bar.active {
    display: block !important;
    animation: pfSlideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes pfSlideUp {
    from { opacity: 0; transform: translateY(16px); }
    to { opacity: 1; transform: translateY(0); }
}

.pf-dl-bar-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
}

.pf-dl-count-text {
    font-size: 14px;
    font-weight: 600;
    color: var(--pf-text-main);
}

.pf-dl-bar-actions {
    display: flex;
    align-items: center;
    gap: 10px;
}

.pf-btn-dl-start {
    background: var(--pf-accent);
    color: #fff;
    border: none;
    padding: 7px 16px;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    box-shadow: 0 4px 12px var(--pf-shadow-accent);
    transition: background 0.2s ease;
}

.pf-btn-dl-start:hover {
    background: var(--pf-accent-hover);
}

.pf-btn-dl-start:disabled {
    background: var(--pf-disabled-bg);
    cursor: not-allowed;
    box-shadow: none;
}

.pf-btn-dl-clear {
    background: var(--pf-bg-transparent);
    color: var(--pf-text-muted);
    border: 1px solid var(--pf-border);
    padding: 7px 12px;
    border-radius: 8px;
    font-size: 12px;
    cursor: pointer;
    transition: background 0.2s ease;
}

.pf-btn-dl-clear:hover {
    background: var(--pf-border);
    color: #fff;
}

.pf-dl-progress-container {
    display: none;
    margin-top: 10px;
}

.pf-dl-progress-container.active {
    display: block !important;
}

.pf-dl-progress-track {
    width: 100%;
    height: 6px;
    background: var(--pf-border);
    border-radius: 3px;
    overflow: hidden;
    margin-bottom: 6px;
}

.pf-dl-progress-fill {
    width: 0%;
    height: 100%;
    background: linear-gradient(90deg, var(--pf-accent), #a855f7);
    transition: width 0.2s ease;
}

.pf-dl-progress-status {
    font-size: 12px;
    color: var(--pf-text-muted);
    display: flex;
    justify-content: space-between;
}

/* ===================== [SETTINGS MODAL STYLES] ===================== */
#pf-settings-modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background: var(--pf-modal-bg);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    z-index: 4000;
    align-items: center;
    justify-content: center;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    color: var(--pf-text-main);
}

#pf-settings-modal.open {
    display: flex !important;
}

.pf-settings-card {
    background: var(--pf-card-bg);
    border: 1px solid var(--pf-border);
    border-radius: 14px;
    width: 460px;
    max-width: 90vw;
    padding: 24px;
    box-shadow: 0 20px 40px var(--pf-shadow);
    box-sizing: border-box;
}

.pf-settings-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 20px;
    border-bottom: 1px solid var(--pf-border);
    padding-bottom: 12px;
}

.pf-settings-header h3 {
    margin: 0;
    font-size: 18px;
    font-weight: 600;
    color: var(--pf-text-main);
}

.pf-settings-close {
    background: transparent;
    border: none;
    color: var(--pf-text-muted);
    font-size: 24px;
    cursor: pointer;
    line-height: 1;
    transition: color 0.2s ease;
}

.pf-settings-close:hover {
    color: #fff;
}

.pf-settings-section {
    margin-bottom: 20px;
}

.pf-settings-section label.pf-radio-option {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px;
    background: var(--pf-bg-transparent);
    border: 1px solid var(--pf-bg-transparent);
    border-radius: 10px;
    margin-bottom: 12px;
    cursor: pointer;
    transition: all 0.2s ease;
}

.pf-settings-section label.pf-radio-option:hover {
    background: var(--pf-bg-transparent);
    border-color: var(--pf-accent-transparent);
}

.pf-settings-section label.pf-radio-option.selected {
    background: var(--pf-accent-transparent);
    border-color: var(--pf-accent);
}

.pf-radio-option input[type="radio"] {
    margin-top: 3px;
    accent-color: var(--pf-accent);
}

.pf-radio-text strong {
    display: block;
    font-size: 14px;
    color: var(--pf-text-main);
    margin-bottom: 4px;
}

.pf-radio-text span {
    display: block;
    font-size: 12px;
    color: var(--pf-text-muted);
    line-height: 1.4;
}

.pf-settings-footer {
    display: flex;
    justify-content: flex-end;
    gap: 12px;
    margin-top: 24px;
}

.pf-btn-save {
    background: var(--pf-accent);
    color: #fff;
    border: none;
    padding: 8px 20px;
    border-radius: 8px;
    font-weight: 600;
    font-size: 13px;
    cursor: pointer;
    box-shadow: 0 4px 12px var(--pf-shadow-accent);
    transition: background 0.2s ease;
}

.pf-btn-save:hover {
    background: var(--pf-accent-hover);
}

.pf-btn-cancel {
    background: var(--pf-bg-transparent);
    color: var(--pf-text-muted);
    border: 1px solid var(--pf-border);
    padding: 8px 16px;
    border-radius: 8px;
    font-size: 13px;
    cursor: pointer;
}

.pf-btn-cancel:hover {
    background: var(--pf-border);
    color: #fff;
}
</style>
`;

const body_inject = `
<div id="fap-gal" style=""></div>
<div id='overlay'>
  <div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
  <div id="overlay-progress">Loading...</div>
</div>
<div id="pf-grid-modal">
  <div id="pf-grid-header">
    <div id="pf-grid-title">Gallery Grid</div>
    <div id="pf-grid-controls">
      <button type="button" class="pf-col-btn" id="pf-btn-select-10">Select 10</button>
      <button type="button" class="pf-col-btn" id="pf-btn-select-all">Select All</button>
      <button type="button" class="pf-col-btn" data-cols="2">2 Cols</button>
      <button type="button" class="pf-col-btn" data-cols="3">3 Cols</button>
      <button type="button" class="pf-col-btn active" data-cols="4">4 Cols</button>
      <button type="button" id="pf-grid-close" title="Close Grid (Esc)">&times;</button>
    </div>
  </div>
  <div id="pf-grid-body">
    <div id="pf-grid-masonry" class="cols-4"></div>
  </div>
  <button type="button" id="pf-scroll-top" title="Scroll to Top">
    <svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"></line><polyline points="5 12 12 5 19 12"></polyline></svg>
  </button>
</div>
<div id="pf-grid-download-bar">
  <div class="pf-dl-bar-header">
    <span class="pf-dl-count-text" id="pf-dl-count-text">0 Selected</span>
    <div class="pf-dl-bar-actions">
      <button type="button" class="pf-btn-dl-start" id="pf-btn-dl-start">
        <svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
        Download
      </button>
      <button type="button" class="pf-btn-dl-clear" id="pf-btn-dl-clear">Clear</button>
    </div>
  </div>
  <div class="pf-dl-progress-container" id="pf-dl-progress-container">
    <div class="pf-dl-progress-track">
      <div class="pf-dl-progress-fill" id="pf-dl-progress-fill"></div>
    </div>
    <div class="pf-dl-progress-status">
      <span id="pf-dl-status-text">Preparing...</span>
      <span id="pf-dl-status-percent">0%</span>
    </div>
  </div>
</div>
<div id="pf-settings-modal">
  <div class="pf-settings-card">
    <div class="pf-settings-header">
      <h3>⚙️ Gallery Viewer Settings</h3>
      <button type="button" class="pf-settings-close" id="pf-settings-close">&times;</button>
    </div>
    <div class="pf-settings-section">
      <label class="pf-radio-option" id="pf-opt-thumb">
        <input type="radio" name="pf_trigger_mode" value="thumb_only">
        <div class="pf-radio-text">
          <strong>🖼️ Thumbnails Only (Default)</strong>
          <span>Opening Lightbox ONLY when clicking photo image thumbnails. Text title links and page navigation perform standard page navigation.</span>
        </div>
      </label>
      <label class="pf-radio-option" id="pf-opt-title">
        <input type="radio" name="pf_trigger_mode" value="thumb_title">
        <div class="pf-radio-text">
          <strong>🏷️ Thumbnails + Gallery Titles</strong>
          <span>Opening Lightbox when clicking photo thumbnails OR gallery title links across Favorites, Organizer, and Category pages.</span>
        </div>
      </label>
    </div>
    <div class="pf-settings-section" style="border-top: 1px solid rgba(255,255,255,0.1); padding-top: 16px;">
      <div style="display:flex; gap:16px; margin-bottom: 16px;">
        <div style="flex:1;">
          <label style="display:block; font-size:13px; color:#94a3b8; margin-bottom:6px;">Font Size</label>
          <select id="pf-opt-fontsize" style="width:100%; padding:8px; background:var(--pf-bg-transparent); border:1px solid var(--pf-border); color:var(--pf-text-main); border-radius:6px; outline:none;">
            <option value="12px">Small (12px)</option>
            <option value="14px">Normal (14px)</option>
            <option value="16px">Large (16px)</option>
            <option value="18px">Extra Large (18px)</option>
            <option value="20px">Huge (20px)</option>
          </select>
        </div>
        <div style="flex:1;">
          <label style="display:block; font-size:13px; color:#94a3b8; margin-bottom:6px;">Font Weight</label>
          <select id="pf-opt-fontweight" style="width:100%; padding:8px; background:var(--pf-bg-transparent); border:1px solid var(--pf-border); color:var(--pf-text-main); border-radius:6px; outline:none;">
            <option value="300">Thin (300)</option>
            <option value="400">Regular (400)</option>
            <option value="500">Medium (500)</option>
            <option value="600">Semi-Bold (600)</option>
            <option value="700">Bold (700)</option>
          </select>
        </div>
      </div>

    </div>
    <div class="pf-settings-footer">
      <button type="button" class="pf-btn-cancel" id="pf-settings-cancel">Cancel</button>
      <button type="button" class="pf-btn-save" id="pf-settings-save">Save Settings</button>
    </div>
  </div>
</div>
`;

const galRegex = /\/photo\/(\d+)/m;
const homeRegex = /image\.php\?id=(\d+)/m;

// ===================== [STATE] =====================
var preloadedImages = {};
var preloadQueue = [];
var isPreloading = false;
var activeFetchContext = null;
var resultCache = {};
var triggerMode = GM_getValue("triggerMode", "thumb_only");
var pfFontSize = GM_getValue("pfFontSize", "14px");
var pfFontWeight = GM_getValue("pfFontWeight", "400");
function cleanProfileDOM() {
    var profileTds = document.querySelectorAll('td.blk_profile');
    profileTds.forEach(function(td) {
        var topTable = td.querySelector('table[width="400"]');
        if (topTable && !td.getAttribute("data-cleaned")) {
            td.setAttribute("data-cleaned", "true");

            var newContainer = document.createElement("div");
            newContainer.className = "pf-profile-details";
            var currentRow = null;
            var childNodes = Array.from(td.childNodes);

            childNodes.forEach(function(node) {
                if (topTable.contains(node) || node === topTable) return;

                if (node.nodeName === "BR") {
                    if (currentRow && currentRow.childNodes.length > 0) {
                        newContainer.appendChild(currentRow);
                        currentRow = null;
                    }
                    return;
                }

                if (!currentRow) {
                    currentRow = document.createElement("div");
                    currentRow.className = "pf-profile-row";
                }

                if (node.nodeType === 3) {
                    var text = node.textContent.trim();
                    if (text !== "") {
                        var span = document.createElement("span");
                        span.className = "pf-profile-val";
                        span.textContent = text;
                        currentRow.appendChild(span);
                    }
                } else {
                    currentRow.appendChild(node.cloneNode(true));
                }
            });

            if (currentRow && currentRow.childNodes.length > 0) {
                newContainer.appendChild(currentRow);
            }

            td.innerHTML = "";
            td.appendChild(topTable);
            td.appendChild(newContainer);
        }
    });
}

function applySiteTheme() {
    var style = document.getElementById("pf-theme-styles");
    if (!style) {
        style = document.createElement("style");
        style.id = "pf-theme-styles";
        document.head.appendChild(style);
    }

    // Default Modal styling for Grid & Settings
    var css = `
        :root {
            --pf-font-size: ${pfFontSize};
            --pf-font-weight: ${pfFontWeight};

            --pf-modal-bg: rgba(46, 52, 64, 0.94);
            --pf-header-bg: rgba(59, 66, 82, 0.85);
            --pf-card-bg: #3b4252;
            --pf-card-img-bg: #2e3440;
            --pf-text-main: #eceff4;
            --pf-text-muted: #d8dee9;
            --pf-accent: #88c0d0;
            --pf-accent-hover: #81a1c1;
            --pf-disabled-bg: #434c5e;
            --pf-border: rgba(216, 222, 233, 0.15);
            --pf-border-hover: rgba(216, 222, 233, 0.25);
            --pf-bg-transparent: transparent;
            --pf-accent-transparent: rgba(136, 192, 208, 0.1);
            --pf-shadow: 0 8px 32px rgba(0,0,0,0.4);
            --pf-shadow-accent: 0 0 15px rgba(136,192,208,0.3);
        }
        #pf-grid-modal, #pf-settings-modal {
            font-size: var(--pf-font-size) !important;
            font-weight: var(--pf-font-weight) !important;
        }
    `;
    style.textContent = css;
}

const PAGE_REQUEST_DELAY_MS = 300;
const CACHE_TTL_MS = 45 * 60 * 1000; // 45 minutes
const MAX_RETRIES = 3;
var currentRunRetryCount = 0;

var galleryOrder = [];
var currentGalleryIndex = -1;
var currentGalleryTitle = "";
var atLastSlide = false;
var navigatingToNextGallery = false;
var isChangingGallery = false;
var isCurrentGalleryFullyLoaded = false;
var isGalleryShown = false;

// ===================== [URL HELPER] =====================
function toAbsoluteUrl(url) {
    if (!url) return "";
    if (url.startsWith("http://") || url.startsWith("https://")) return url;
    if (url.startsWith("//")) return window.location.protocol + url;
    if (url.startsWith("/")) return window.location.origin + url;
    return window.location.origin + "/" + url;
}
(function () {
    'use strict';

    document.head.insertAdjacentHTML("beforeend", head_inject);
    document.body.insertAdjacentHTML("beforeend", body_inject);
    applySiteTheme();
    cleanProfileDOM();
    rearrangeGalleryLayout();
    setupDownloadInterceptor();
    initGalleryListeners();
    buildGalleryOrder();
    setupMenuCommands();
    setupGridModalEvents();
    setupSettingsModalEvents();
    setupGridDownloadEvents();
    initDOMDeduplicator();

    try {
        var stored = GM_getValue("resultCache", null);
        if (stored) {
            resultCache = JSON.parse(stored);
        }
    } catch (e) {
        console.warn("Powerfap: failed to restore persisted cache", e);
        resultCache = {};
    }

    // ===================== [DOM DEDUPLICATOR (Pagination Drift Fix)] =====================
    // ===================== [DOM DEDUPLICATOR (Pagination Drift Fix)] =====================

    function initDOMDeduplicator() {
        var pfSeenIds = new Set();
        var pfDedupTimer = null;

        function deduplicateElements() {
            var duplicateCount = 0;

            // 1. Deduplicate Galleries
            var galRows = document.querySelectorAll('tr[id^="gid-"], table.blk_favorites tr');
            galRows.forEach(function(row) {
                if (row.getAttribute("data-pf-dedup") === "true") return;

                var galId = null;
                if (row.id && row.id.indexOf("gid-") === 0) {
                    galId = row.id.replace("gid-", "");
                } else {
                    var a = row.querySelector('a[href*="gid="], a[href*="/gallery/"], a[href*="/pictures/"]');
                    if (a) galId = extractGalId(null, a, a.getAttribute('href'));
                }
                if (galId) {
                    var uid = "G_" + galId;
                    if (pfSeenIds.has(uid)) {
                        row.remove();
                        duplicateCount++;
                    } else {
                        pfSeenIds.add(uid);
                        row.setAttribute("data-pf-dedup", "true");
                    }
                }
            });

            // 2. Deduplicate Photos
            var photoLinks = document.querySelectorAll('a[href*="/photo/"]');
            photoLinks.forEach(function(link) {
                if (link.getAttribute("data-pf-dedup") === "true") return;

                // Ignore the LightGallery viewer items because they are virtual duplicates of the grid
                if (link.closest('.lg-outer')) return;
                
                var photoId = extractPhotoId(link.getAttribute('href'));
                if (photoId) {
                    var uid = "P_" + photoId;
                    if (pfSeenIds.has(uid)) {
                        var container = link.closest('td[align="center"], .thumb-block') || link;
                        container.remove();
                        duplicateCount++;
                    } else {
                        pfSeenIds.add(uid);
                        link.setAttribute("data-pf-dedup", "true");
                    }
                }
            });

            if (duplicateCount > 0) {
                console.log("[Imagefap Pro] Deduplicator: Filtered out " + duplicateCount + " drifted/duplicate items from the layout.");
            }
        }

        // Run initially
        deduplicateElements();

        // Watch for infinite scroll or background DOM injections
        var observer = new MutationObserver(function(mutations) {
            var needsDedup = false;
            for (var i = 0; i < mutations.length; i++) {
                if (mutations[i].addedNodes.length > 0) {
                    needsDedup = true;
                    break;
                }
            }
            if (needsDedup) {
                clearTimeout(pfDedupTimer);
                pfDedupTimer = setTimeout(deduplicateElements, 50);
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    // ===================== [DOM REARRANGEMENT] =====================

    function rearrangeGalleryLayout() {
        var searchFormTable = document.querySelector('form[name="form"] > table > tbody');
        var displayingResultsCenter = document.querySelector('form[name="form"] + br + center') || document.querySelector('form[name="form"] + center');
        if (searchFormTable && displayingResultsCenter) {
            var newRow = document.createElement('tr');
            var newCell = document.createElement('td');
            newCell.colSpan = 2;
            newCell.style.textAlign = 'center';
            newCell.style.padding = '10px 0 0 0';
            newCell.appendChild(displayingResultsCenter);
            newRow.appendChild(newCell);
            searchFormTable.appendChild(newRow);
        }
    }

    // ===================== [CLICK HANDLERS (NATIVE DELEGATION)] =====================

    function isPaginationLink(a, href) {
        if (!a && !href) return false;
        var text = (a ? (a.textContent || "") : "").trim();

        if (text.match(/^(next|prev|previous|first|last|page|\d+|next page >|<< prev|next >|< prev)$/i) ||
            text === ">" || text === "<" || text === ">>" || text === "<<" || text === "Next »" || text === "« Prev") {
            return true;
        }

        if (href && (href.indexOf("page=") !== -1 || href.indexOf("search=") !== -1) && !href.match(/\/photo\/\d+/)) {
            if (text.match(/^\d+$/) || text.match(/next|prev/i)) return true;
        }

        if (a && a.closest && a.closest("#pagination, .pagination, td[align='right'] font, div.paging")) {
            return true;
        }

        return false;
    }

    function extractPhotoId(href) {
        if (!href) return undefined;
        var match = galRegex.exec(href) || homeRegex.exec(href);
        return match ? match[1] : undefined;
    }

    function extractGalId(el, a, href) {
        if (href) {
            var gidMatch = /[?&]gid=(\d+)/.exec(href) || /\/(gallery|pictures|organizer)\/(\d+)/.exec(href);
            if (gidMatch && gidMatch[gidMatch.length - 1] !== "0") {
                return gidMatch[gidMatch.length - 1];
            }
        }
        if (a) {
            var orgId = extractOrganizerGalId(a);
            if (orgId) return orgId;
        }
        if (el) {
            var currentTr = el.closest ? el.closest("tr") : null;
            while (currentTr) {
                var prevTr = currentTr.previousElementSibling;
                if (prevTr && prevTr.id && (prevTr.id.indexOf("gid-") === 0 || /^\d+$/.test(prevTr.id) || prevTr.id.length > 5)) {
                    var potentialId = prevTr.id.replace("gid-", "");
                    if (/^\d+$/.test(potentialId)) {
                        return potentialId;
                    }
                }
                currentTr = currentTr.parentElement ? currentTr.parentElement.closest("tr") : null;
            }
        }
        return undefined;
    }

    document.addEventListener("click", function (e) {
        // 1. Allow Ctrl / Cmd + Click to bypass Lightbox natively
        if (e.ctrlKey || e.metaKey) return;

        var target = e.target;
        if (!target) return;

        var galThumb = target.closest && target.closest("img.gal_thumb, img.gal_mini, img._lazy, img[src*='/images/']");
        var link = target.closest && target.closest('a[href*="image.php?id="], a[href*="/photo/"], a[href*="/gallery/"], a[href*="/pictures/"], a[href*="gid="]');

        if (!galThumb && !link) return;

        var a = link || (galThumb ? galThumb.closest("a") : null);
        var href = a ? (a.getAttribute("href") || "") : "";

        // 2. Never intercept pagination links
        if (isPaginationLink(a, href)) return;

        // 3. Enforce user-selected triggerMode
        if (triggerMode === "thumb_only") {
            var isImageClick = galThumb || (a && a.querySelector("img"));
            if (!isImageClick) return;
        } else if (triggerMode === "thumb_title") {
            var isImage = galThumb || (a && a.querySelector("img"));
            var isTitle = a && (
                a.classList.contains("blk_galleries") ||
                a.classList.contains("gal_title") ||
                a.classList.contains("blk_favorites") ||
                (a.parentElement && a.parentElement.tagName === "B") ||
                a.closest("tr[id^='gid-']") ||
                href.match(/\/(gallery|pictures|organizer)\/\d+/) ||
                href.match(/[?&]gid=\d+/)
            );
            if (!isImage && !isTitle) return;
        }

        var photoId = extractPhotoId(href);
        var galId = extractGalId(galThumb || target, a, href);

        if (!photoId && !galId) return;

        e.preventDefault();
        showOverlay();

        var expectedCount = 0;
        if (galId) {
            var row = document.getElementById(galId) || document.querySelector('tr[id="' + galId + '"]');
            if (row) {
                var tds = row.querySelectorAll('td');
                if (tds.length >= 2) {
                    var count = parseInt(tds[1].textContent.replace(/,/g, '').trim(), 10);
                    if (!isNaN(count) && count > 0) expectedCount = count;
                }
            }
        }

        var titleHint = extractPageTitleForElement(galThumb || target);
        preRun(photoId, galId, titleHint, expectedCount);
    }, true);
})();

// ===================== [TITLE EXTRACTION HELPER] =====================

function extractPageTitleForElement(el) {
    if (!el) return "";

    // 1. Check anchor or image attributes
    var a = el.closest ? el.closest("a") : null;
    if (a) {
        var aTitle = a.getAttribute("title");
        if (aTitle && aTitle.trim()) return aTitle.trim();
    }
    var img = (el.tagName === "IMG") ? el : (el.querySelector ? el.querySelector("img") : null);
    if (img) {
        var imgAlt = img.getAttribute("alt") || img.getAttribute("title");
        if (imgAlt && imgAlt.trim()) return imgAlt.trim();
    }

    // 2. Traverse up table/row for folder/gallery header title (showfavorites.php / organizer)
    var container = el.closest ? el.closest("td[colspan='5'], td.blk_favorites, table.blk_favorites, tr") : null;
    if (container) {
        var header = container.querySelector("a.blk_galleries, a.blk_favorites, a.gal_title, b, font[size='3']");
        if (header && header.textContent && header.textContent.trim()) {
            return header.textContent.trim();
        }
        if (container.previousElementSibling && container.previousElementSibling.textContent) {
            var prevText = container.previousElementSibling.textContent.trim();
            if (prevText) return prevText;
        }
    }

    // 3. Main page heading
    var pageH1 = document.querySelector("h1, font[size='4'] b, .gal_title");
    if (pageH1 && pageH1.textContent && pageH1.textContent.trim()) {
        return pageH1.textContent.trim();
    }

    // 4. Fallback: cleaned document title
    var docTitle = document.title || "";
    return docTitle.replace(/\s*-\s*ImageFap.*$/i, "").trim();
}

// ===================== [OVERLAY / PROGRESS UI] =====================

function showOverlay() {
    updateProgress(0);
    var el = document.getElementById("overlay");
    if (el) el.style.display = "block";
}

function hideOverlay() {
    var el = document.getElementById("overlay");
    if (el) el.style.display = "none";
}

function updateProgress(loaded, total) {
    var el = document.getElementById("overlay-progress");
    if (!el) return;
    if (typeof total === "number" && total > 0) {
        el.textContent = "Loaded " + loaded + "/" + total + " images...";
    } else {
        el.textContent = "Loaded " + loaded + " images...";
    }
}

// ===================== [MENU COMMANDS & SETTINGS MODAL] =====================

function setupMenuCommands() {
    if (typeof GM_registerMenuCommand === "undefined") return;

    GM_registerMenuCommand("⚙️ Click Settings", function () {
        openSettingsModal();
    });

    GM_registerMenuCommand("🗑️ Clear Cache", function () {
        clearCache();
    });
}

function openSettingsModal() {
    var modal = document.getElementById("pf-settings-modal");
    if (!modal) return;

    triggerMode = GM_getValue("triggerMode", "thumb_only");
    pfFontSize = GM_getValue("pfFontSize", "14px");
    pfFontWeight = GM_getValue("pfFontWeight", "400");
    pfSiteTheme = GM_getValue("pfSiteTheme", "default");

    var thumbRadio = modal.querySelector('input[value="thumb_only"]');
    var titleRadio = modal.querySelector('input[value="thumb_title"]');

    if (triggerMode === "thumb_title") {
        if (titleRadio) titleRadio.checked = true;
    } else {
        if (thumbRadio) thumbRadio.checked = true;
    }

    var fsSelect = document.getElementById("pf-opt-fontsize");
    if (fsSelect) fsSelect.value = pfFontSize;

    var fwSelect = document.getElementById("pf-opt-fontweight");
    if (fwSelect) fwSelect.value = pfFontWeight;

    updateRadioCardStyles();
    modal.classList.add("open");
}

function closeSettingsModal() {
    var modal = document.getElementById("pf-settings-modal");
    if (modal) {
        modal.classList.remove("open");
    }
}

function updateRadioCardStyles() {
    var modal = document.getElementById("pf-settings-modal");
    if (!modal) return;

    var optThumb = document.getElementById("pf-opt-thumb");
    var optTitle = document.getElementById("pf-opt-title");
    var thumbRadio = modal.querySelector('input[value="thumb_only"]');

    if (thumbRadio && thumbRadio.checked) {
        if (optThumb) optThumb.classList.add("selected");
        if (optTitle) optTitle.classList.remove("selected");
    } else {
        if (optThumb) optThumb.classList.remove("selected");
        if (optTitle) optTitle.classList.add("selected");
    }
}

function saveSettingsModal() {
    var modal = document.getElementById("pf-settings-modal");
    if (!modal) return;

    var selected = modal.querySelector('input[name="pf_trigger_mode"]:checked');
    if (selected) {
        triggerMode = selected.value;
        GM_setValue("triggerMode", triggerMode);
    }

    var fsSelect = document.getElementById("pf-opt-fontsize");
    if (fsSelect) {
        pfFontSize = fsSelect.value;
        GM_setValue("pfFontSize", pfFontSize);
    }

    var fwSelect = document.getElementById("pf-opt-fontweight");
    if (fwSelect) {
        pfFontWeight = fwSelect.value;
        GM_setValue("pfFontWeight", pfFontWeight);
    }


    applySiteTheme();
    showToast("Settings saved successfully!");
    closeSettingsModal();
}

function setupSettingsModalEvents() {
    var modal = document.getElementById("pf-settings-modal");
    if (!modal) return;

    var closeBtn = document.getElementById("pf-settings-close");
    var cancelBtn = document.getElementById("pf-settings-cancel");
    var saveBtn = document.getElementById("pf-settings-save");

    if (closeBtn) closeBtn.addEventListener("click", closeSettingsModal);
    if (cancelBtn) cancelBtn.addEventListener("click", closeSettingsModal);
    if (saveBtn) saveBtn.addEventListener("click", saveSettingsModal);

    var radios = modal.querySelectorAll('input[name="pf_trigger_mode"]');
    radios.forEach(function (radio) {
        radio.addEventListener("change", updateRadioCardStyles);
    });
}

function clearCache() {
    resultCache = {};
    preloadedImages = {};
    persistCache();
    showToast("Cache cleared successfully!");
    console.log("Powerfap: Cache cleared");
}

// ===================== [MASONRY GRID MODAL & QUICK JUMP] =====================

var currentGridCols = 4;
var isGridOpen = false;

function setupGridModalEvents() {
    var modal = document.getElementById("pf-grid-modal");
    if (!modal) return;

    var closeBtn = document.getElementById("pf-grid-close");
    if (closeBtn) {
        closeBtn.addEventListener("click", closeGridModal);
    }

    var colBtns = modal.querySelectorAll(".pf-col-btn");
    colBtns.forEach(function (btn) {
        btn.addEventListener("click", function () {
            var cols = parseInt(this.getAttribute("data-cols"), 10) || 4;
            setGridColumns(cols);
        });
    });

    var gridBody = document.getElementById("pf-grid-body");
    var scrollTopBtn = document.getElementById("pf-scroll-top");
    if (gridBody && scrollTopBtn) {
        gridBody.addEventListener("scroll", function() {
            if (gridBody.scrollTop > 300) {
                scrollTopBtn.classList.add("visible");
            } else {
                scrollTopBtn.classList.remove("visible");
            }
        });
        scrollTopBtn.addEventListener("click", function() {
            gridBody.scrollTo({ top: 0, behavior: 'smooth' });
        });
    }

    document.addEventListener("keydown", function (e) {
        var el = document.getElementById("fap-gal");
        var lgUid = el ? el.getAttribute('lg-uid') : null;
        var isViewerOpen = window.lgData && window.lgData[lgUid];

        if (!isViewerOpen) return;

        // Toggle Grid on 'g' / 'G' key
        if ((e.key === "g" || e.key === "G") && !e.ctrlKey && !e.altKey && !e.metaKey) {
            var activeEl = document.activeElement;
            if (activeEl && (activeEl.tagName === "INPUT" || activeEl.tagName === "TEXTAREA")) return;
            e.preventDefault();
            toggleGridModal();
            return;
        }

        // Close Grid on 'Escape' key
        if (e.key === "Escape" && isGridOpen) {
            e.preventDefault();
            e.stopPropagation();
            closeGridModal();
            return;
        }
    }, true);
}

function openGridModal() {
    var el = document.getElementById("fap-gal");
    var lgUid = el ? el.getAttribute('lg-uid') : null;
    if (!window.lgData || !window.lgData[lgUid]) return;

    var modal = document.getElementById("pf-grid-modal");
    if (modal) {
        modal.classList.add("open");
        isGridOpen = true;
    }

    setGridColumns(4); // Default to 4 columns every time opened
    renderGridMasonry();

    var titleEl = document.getElementById("pf-grid-title");
    if (titleEl && currentGalleryTitle) {
        titleEl.textContent = currentGalleryTitle;
    }
}

function closeGridModal() {
    var modal = document.getElementById("pf-grid-modal");
    if (modal) {
        modal.classList.remove("open");
        isGridOpen = false;
    }
}

function toggleGridModal() {
    if (isGridOpen) {
        closeGridModal();
    } else {
        openGridModal();
    }
}

function setGridColumns(cols) {
    var container = document.getElementById("pf-grid-masonry");
    var hasColumns = container && container.querySelectorAll(".pf-masonry-column").length === cols;
    var colsChanged = (currentGridCols !== cols) || !hasColumns;
    currentGridCols = cols;

    if (container && colsChanged) {
        container.innerHTML = "";
        for (var i = 0; i < cols; i++) {
            var colDiv = document.createElement("div");
            colDiv.className = "pf-masonry-column";
            container.appendChild(colDiv);
        }
        renderGridMasonry(true);
    }

    var colBtns = document.querySelectorAll("#pf-grid-modal .pf-col-btn");
    colBtns.forEach(function (btn) {
        var num = parseInt(btn.getAttribute("data-cols"), 10);
        if (num === cols) {
            btn.classList.add("active");
        } else {
            btn.classList.remove("active");
        }
    });
}

function renderGridMasonry(forceRebuild) {
    var container = document.getElementById("pf-grid-masonry");
    if (!container) return;

    var columns = container.querySelectorAll(".pf-masonry-column");
    if (columns.length === 0) {
        setGridColumns(currentGridCols);
        return;
    }

    var el = document.getElementById("fap-gal");
    var lgUid = el ? el.getAttribute('lg-uid') : null;
    if (!window.lgData || !window.lgData[lgUid]) return;

    var items = window.lgData[lgUid].items || [];
    var currentIndex = window.lgData[lgUid].index || 0;

    var existingCardCount = container.querySelectorAll(".pf-grid-card").length;

    if (forceRebuild || existingCardCount === 0 || existingCardCount > items.length) {
        columns.forEach(function(col) { col.innerHTML = ""; });
        existingCardCount = 0;
    }

    for (var index = existingCardCount; index < items.length; index++) {
        (function (i) {
            var item = items[i];
            var card = document.createElement("div");
            card.className = "pf-grid-card" + (i === currentIndex ? " active" : "") + (selectedGridIndices.has(i) ? " selected" : "");
            card.setAttribute("data-index", i);

            var chk = document.createElement("input");
            chk.type = "checkbox";
            chk.className = "pf-grid-checkbox";
            chk.checked = selectedGridIndices.has(i);

            chk.addEventListener("click", function (e) {
                e.stopPropagation();
            });

            chk.addEventListener("change", function (e) {
                e.stopPropagation();
                if (this.checked) {
                    selectedGridIndices.add(i);
                    card.classList.add("selected");
                } else {
                    selectedGridIndices.delete(i);
                    card.classList.remove("selected");
                }
                updateGridDownloadBar();
            });

            var img = document.createElement("img");
            img.src = item.src || item.thumb;
            img.loading = "lazy";
            img.alt = "";

            img.onerror = function () {
                if (item.thumb && this.src !== item.thumb) {
                    this.src = item.thumb;
                }
            };

            card.appendChild(chk);
            card.appendChild(img);

            card.addEventListener("click", function () {
                jumpToGridSlide(i);
            });

            // Round-robin distribution avoids race conditions with lazy loaded image heights and hidden modal states
            var targetCol = columns[i % columns.length];
            targetCol.appendChild(card);
        })(index);
    }

    var cards = container.querySelectorAll(".pf-grid-card");
    cards.forEach(function (c, idx) {
        if (idx === currentIndex) {
            c.classList.add("active");
        } else {
            c.classList.remove("active");
        }
    });

    if (existingCardCount === 0) {
        var gridBody = document.getElementById("pf-grid-body");
        if (gridBody) {
            gridBody.scrollTop = 0;
        }
    }
}

// ===================== [GRID BATCH DOWNLOAD ENGINE] =====================

var selectedGridIndices = new Set();
var lastSelect10Index = 0;
var isBatchDownloading = false;

function setupGridDownloadEvents() {
    var select10Btn = document.getElementById("pf-btn-select-10");
    var selectAllBtn = document.getElementById("pf-btn-select-all");
    var startDlBtn = document.getElementById("pf-btn-dl-start");
    var clearDlBtn = document.getElementById("pf-btn-dl-clear");

    if (select10Btn) select10Btn.addEventListener("click", handleSelect10);
    if (selectAllBtn) selectAllBtn.addEventListener("click", handleSelectAll);
    if (startDlBtn) startDlBtn.addEventListener("click", startGridBatchDownload);
    if (clearDlBtn) clearDlBtn.addEventListener("click", clearGridSelection);
}

function handleSelect10() {
    var el = document.getElementById("fap-gal");
    var lgUid = el ? el.getAttribute('lg-uid') : null;
    if (!window.lgData || !window.lgData[lgUid]) return;

    var totalItems = (window.lgData[lgUid].items || []).length;
    if (totalItems === 0) return;

    var start = lastSelect10Index;
    var end = Math.min(start + 10, totalItems);

    for (var i = start; i < end; i++) {
        selectedGridIndices.add(i);
    }

    lastSelect10Index = end;

    var select10Btn = document.getElementById("pf-btn-select-10");
    if (select10Btn) {
        if (lastSelect10Index >= totalItems) {
            select10Btn.textContent = "Select 10";
            lastSelect10Index = 0;
        } else {
            select10Btn.textContent = "Select Next 10";
        }
    }

    updateGridCardSelectionUI();
    updateGridDownloadBar();
}

function handleSelectAll() {
    var el = document.getElementById("fap-gal");
    var lgUid = el ? el.getAttribute('lg-uid') : null;
    if (!window.lgData || !window.lgData[lgUid]) return;

    var totalItems = (window.lgData[lgUid].items || []).length;
    if (totalItems === 0) return;

    var selectAllBtn = document.getElementById("pf-btn-select-all");

    if (selectedGridIndices.size >= totalItems) {
        clearGridSelection();
        if (selectAllBtn) selectAllBtn.textContent = "Select All";
    } else {
        selectedGridIndices.clear();
        for (var i = 0; i < totalItems; i++) {
            selectedGridIndices.add(i);
        }
        if (selectAllBtn) selectAllBtn.textContent = "Deselect All";
    }

    updateGridCardSelectionUI();
    updateGridDownloadBar();
}

function clearGridSelection() {
    selectedGridIndices.clear();
    lastSelect10Index = 0;

    var select10Btn = document.getElementById("pf-btn-select-10");
    if (select10Btn) select10Btn.textContent = "Select 10";

    var selectAllBtn = document.getElementById("pf-btn-select-all");
    if (selectAllBtn) selectAllBtn.textContent = "Select All";

    updateGridCardSelectionUI();
    updateGridDownloadBar();
}

function updateGridCardSelectionUI() {
    var container = document.getElementById("pf-grid-masonry");
    if (!container) return;

    var cards = container.querySelectorAll(".pf-grid-card");
    cards.forEach(function (card) {
        var idx = parseInt(card.getAttribute("data-index"), 10);
        var chk = card.querySelector(".pf-grid-checkbox");

        if (selectedGridIndices.has(idx)) {
            card.classList.add("selected");
            if (chk) chk.checked = true;
        } else {
            card.classList.remove("selected");
            if (chk) chk.checked = false;
        }
    });
}

function updateGridDownloadBar() {
    var bar = document.getElementById("pf-grid-download-bar");
    var countText = document.getElementById("pf-dl-count-text");
    var startBtn = document.getElementById("pf-btn-dl-start");
    var progressContainer = document.getElementById("pf-dl-progress-container");

    if (!bar) return;

    var count = selectedGridIndices.size;
    if (count > 0) {
        bar.classList.add("active");
        if (countText) countText.textContent = count + " Selected";
        if (startBtn) {
            startBtn.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg> Download (' + count + ')';
        }
    } else {
        if (!isBatchDownloading) {
            bar.classList.remove("active");
            if (progressContainer) progressContainer.classList.remove("active");
        }
    }
}

function extractCdnFilename(url) {
    if (!url) return "image.jpg";
    var clean = url.split("?")[0].split("#")[0];
    var parts = clean.split("/");
    var fname = parts[parts.length - 1];
    return fname && fname.match(/\.(jpg|jpeg|png|gif|webp)$/i) ? fname : "image_" + Date.now() + ".jpg";
}

function delay(ms) {
    return new Promise(function (resolve) {
        setTimeout(resolve, ms);
    });
}

async function startGridBatchDownload() {
    if (isBatchDownloading || selectedGridIndices.size === 0) return;

    var el = document.getElementById("fap-gal");
    var lgUid = el ? el.getAttribute('lg-uid') : null;
    if (!window.lgData || !window.lgData[lgUid]) return;

    var items = window.lgData[lgUid].items || [];
    var indicesToDownload = Array.from(selectedGridIndices).sort(function (a, b) { return a - b; });

    isBatchDownloading = true;

    var startBtn = document.getElementById("pf-btn-dl-start");
    var clearBtn = document.getElementById("pf-btn-dl-clear");
    var progressContainer = document.getElementById("pf-dl-progress-container");
    var fill = document.getElementById("pf-dl-progress-fill");
    var statusText = document.getElementById("pf-dl-status-text");
    var statusPercent = document.getElementById("pf-dl-status-percent");

    if (startBtn) startBtn.disabled = true;
    if (clearBtn) clearBtn.disabled = true;
    if (progressContainer) progressContainer.classList.add("active");

    var total = indicesToDownload.length;
    var completed = 0;

    for (var i = 0; i < total; i++) {
        var idx = indicesToDownload[i];
        var item = items[idx];
        if (!item || !item.src) continue;

        var fname = extractCdnFilename(item.src);
        var currentBatch = Math.floor(i / 30) + 1;
        var totalBatches = Math.ceil(total / 30);

        // Pause for 2.5s after every 30 downloads
        if (i > 0 && i % 30 === 0) {
            if (statusText) statusText.textContent = "Batch " + (currentBatch - 1) + " complete (" + i + "/" + total + "). Pausing 2.5s...";
            await delay(2500);
        }

        if (statusText) {
            statusText.textContent = "Downloading " + (i + 1) + "/" + total + " (Batch " + currentBatch + "/" + totalBatches + ")...";
        }

        await downloadSingleFile(item.src, fname);
        completed++;

        var percent = Math.round((completed / total) * 100);
        if (fill) fill.style.width = percent + "%";
        if (statusPercent) statusPercent.textContent = percent + "%";

        await delay(200);
    }

    if (statusText) statusText.textContent = "Downloaded all " + completed + " images!";
    showToast("Batch download complete: " + completed + " images");

    await delay(1500);

    isBatchDownloading = false;
    if (startBtn) startBtn.disabled = false;
    if (clearBtn) clearBtn.disabled = false;
    clearGridSelection();
}

function downloadSingleFile(url, filename) {
    return new Promise(function (resolve) {
        if (typeof GM_download !== "undefined") {
            GM_download({
                url: url,
                name: filename,
                onload: function () { resolve(); },
                onerror: function () {
                    downloadViaBlob(url, filename).then(resolve);
                },
                ontimeout: function () {
                    downloadViaBlob(url, filename).then(resolve);
                }
            });
        } else {
            downloadViaBlob(url, filename).then(resolve);
        }
    });
}

function downloadViaBlob(url, filename) {
    return new Promise(function (resolve) {
        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            responseType: "blob",
            onload: function (resp) {
                if (resp.status === 200 && resp.response) {
                    var blobUrl = URL.createObjectURL(resp.response);
                    var a = document.createElement("a");
                    a.href = blobUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    setTimeout(function () { URL.revokeObjectURL(blobUrl); }, 1000);
                }
                resolve();
            },
            onerror: function () { resolve(); }
        });
    });
}

function jumpToGridSlide(index) {
    closeGridModal();
    var el = document.getElementById("fap-gal");
    var lgUid = el ? el.getAttribute('lg-uid') : null;
    if (window.lgData && window.lgData[lgUid]) {
        window.lgData[lgUid].slide(index);
    }
}

function injectGridButton() {
    var toolbar = document.querySelector(".lg-toolbar");
    if (!toolbar || document.getElementById("pf-grid-btn")) return;

    var gridBtn = document.createElement("button");
    gridBtn.type = "button";
    gridBtn.id = "pf-grid-btn";
    gridBtn.title = "Masonry Grid View (G)";
    gridBtn.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.5"></rect><rect x="14" y="3" width="7" height="7" rx="1.5"></rect><rect x="14" y="14" width="7" height="7" rx="1.5"></rect><rect x="3" y="14" width="7" height="7" rx="1.5"></rect></svg>';

    gridBtn.addEventListener("click", function (e) {
        e.preventDefault();
        e.stopPropagation();
        toggleGridModal();
    });

    toolbar.appendChild(gridBtn);
}

function injectDownloadAllButton() {
    var lgToolbar = document.querySelector(".lg-toolbar");
    if (!lgToolbar || document.getElementById("pf-lg-download-all")) return;

    var dlAllBtn = document.createElement("button");
    dlAllBtn.id = "pf-lg-download-all";
    dlAllBtn.className = "lg-icon";
    dlAllBtn.style.cssText = "background: none; border: none; cursor: pointer; float: right; padding: 12px; margin-top: 1px; outline: none; transition: color 0.2s; color: #999;";
    dlAllBtn.innerHTML = '<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>';
    dlAllBtn.title = "Download All Images in Gallery";
    dlAllBtn.onmouseover = function() { this.style.color = "#eceff4"; };
    dlAllBtn.onmouseout = function() { this.style.color = "#999"; };
    dlAllBtn.addEventListener("click", function(e) {
        e.preventDefault();
        e.stopPropagation();
        if (isBatchDownloading) {
            showToast("A batch download is already in progress!");
            return;
        }
        var el = document.getElementById("fap-gal");
        var lgUid = el ? el.getAttribute('lg-uid') : null;
        if (!window.lgData || !window.lgData[lgUid]) return;

        var allItems = window.lgData[lgUid].items || [];
        selectedGridIndices.clear();
        for (var i = 0; i < allItems.length; i++) {
            selectedGridIndices.add(i);
        }
        updateGridDownloadBar();
        startGridBatchDownload();
        showToast("Queued " + allItems.length + " images for download!");
    });
    // Insert before the download icon if exists
    var lgDownload = document.getElementById("lg-download");
    if (lgDownload) {
        lgToolbar.insertBefore(dlAllBtn, lgDownload);
    } else {
        lgToolbar.appendChild(dlAllBtn);
    }
}


// ===================== [CACHE / PAGINATION] =====================
function getImgSrc(img) {
    if (!img) return "";
    var dataSrc = img.getAttribute("data-src") || img.getAttribute("data-original") || img.getAttribute("data-url") || img.getAttribute("original");
    if (dataSrc && dataSrc.indexOf("blank.gif") === -1 && dataSrc.indexOf("spacer.gif") === -1) {
        return toAbsoluteUrl(dataSrc);
    }
    var src = img.getAttribute("src") || "";
    if (src && src.indexOf("blank.gif") === -1 && src.indexOf("spacer.gif") === -1) {
        return toAbsoluteUrl(src);
    }
    return toAbsoluteUrl(dataSrc || src || "");
}

function isGalleryImage(item) {
    if (!item) return false;
    var src = item.src || "";
    var thumb = item.thumb || "";
    var checkUrl = (thumb || src).toLowerCase();

    if (!checkUrl) return false;

    // Reject site logos, badges, ads, icons, partners
    if (checkUrl.match(/(asacp|icra|rta|logo|banner|badge|avatar|icon|button|blank\.gif|spacer\.gif|advertisement|i_love_data|tnaflix|empflix|wankspider|moviefap|onsite_|00km|favicons)/i)) {
        return false;
    }

    // Accept if matches Imagefap gallery thumbnail/full patterns or photo CDNs
    if (checkUrl.match(/\/images\/(thumb|grid|mini|full)\//i) ||
        checkUrl.match(/(x\.imagefap\.com|cdn[a-z]?\.imagefap\.com|fap\.to|imagefap\.com)\/images\//i)) {
        return true;
    }

    // Accept valid photo URLs with standard image extension if src is not a badge
    if (src.match(/\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i) && !src.match(/(logo|banner|badge|icon|00km)/i)) {
        return true;
    }

    return false;
}

function sanitizeImageItems(items) {
    if (!Array.isArray(items)) return [];
    var valid = [];
    for (var i = 0; i < items.length; i++) {
        var src = items[i].src || "";
        var thumb = items[i].thumb || "";
        if (!src || src.indexOf("/photo/") !== -1 || !src.match(/\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i)) {
            if (thumb) {
                src = thumb.replace("/thumb/", "/full/").replace("/mini/", "/full/").replace("/grid/", "/full/");
            }
        }
        var item = { src: src, thumb: thumb };
        if (isGalleryImage(item)) {
            valid.push(item);
        }
    }
    return valid;
}

function preRun(photoId, galId, titleHint, expectedCount) {
    preloadedImages = {}; // Flush image preloader cache on new gallery load
    preloadQueue = [];
    isPreloading = false;
    navigatingToNextGallery = false;
    isCurrentGalleryFullyLoaded = false;
    isGalleryShown = false;
    buildGalleryOrder();

    var idx = findGalleryIndexById(galId);
    if (idx !== -1) {
        currentGalleryIndex = idx;
        currentGalleryTitle = galleryOrder[idx].title || titleHint || "";
    } else if (galId && galId !== "0") {
        galleryOrder.push({
            galId: String(galId),
            title: titleHint || ("Gallery " + galId),
            pageUrl: "/gallery/" + galId
        });
        currentGalleryIndex = galleryOrder.length - 1;
        currentGalleryTitle = titleHint || ("Gallery " + galId);
    } else {
        currentGalleryTitle = titleHint || extractPageTitleForElement(document.body);
    }

    atLastSlide = false;
    currentRunRetryCount = 0;

    var key = calculateCacheKey(photoId, galId);
    var cached = getCachedGallery(key);
    if (typeof cached !== "undefined") {
        var startIdx = 0;
        if (photoId) {
            for (var c = 0; c < cached.length; c++) {
                if ((cached[c].src && cached[c].src.indexOf(photoId) !== -1) ||
                    (cached[c].thumb && cached[c].thumb.indexOf(photoId) !== -1)) {
                    startIdx = c;
                    break;
                }
            }
        }
        showGallery(cached, startIdx);
        isGalleryShown = true;
        isCurrentGalleryFullyLoaded = true;
        return;
    }

    if (photoId && photoId !== "null" && photoId !== "undefined") {
        startGalleryFetch(photoId, galId, photoId, expectedCount);
    } else if (galId && galId !== "0" && galId !== "undefined") {
        showOverlay();
        fetchFirstPhotoId("/gallery/" + galId, function (firstPhotoId) {
            if (!firstPhotoId) {
                hideOverlay();
                alert("Could not load photos for gallery " + galId);
                return;
            }
            startGalleryFetch(firstPhotoId, galId, firstPhotoId, expectedCount);
        });
    } else {
        hideOverlay();
        alert("Cannot load gallery: missing gallery ID and photo ID.");
    }
}

function calculateCacheKey(photoId, galId) {
    if (typeof galId === "undefined" || galId === null || galId === "0") {
        return "photo_" + photoId;
    }
    return galId;
}

function getCachedGallery(key) {
    var entry = resultCache[key];
    if (typeof entry === "undefined") return undefined;

    if (Array.isArray(entry)) {
        delete resultCache[key];
        return undefined;
    }

    var age = Date.now() - entry.cachedAt;
    if (age > CACHE_TTL_MS) {
        delete resultCache[key];
        persistCache();
        return undefined;
    }

    var sanitized = sanitizeImageItems(entry.items);
    if (!sanitized || sanitized.length === 0) {
        delete resultCache[key];
        persistCache();
        return undefined;
    }
    return sanitized;
}

function setCachedGallery(key, items) {
    resultCache[key] = { items: items, cachedAt: Date.now() };
    persistCache();
}

function extractOrganizerGalId(anchor) {
    var td = anchor.closest("td[colspan='5']");
    if (td && td.parentElement) {
        var prevTr = td.parentElement.previousElementSibling;
        if (prevTr && prevTr.id) {
            var idMatch = /^gid-(\d+)$/.exec(prevTr.id);
            if (idMatch !== null && idMatch[1] !== "0") return idMatch[1];
        }
    }

    var href = anchor.getAttribute("href") || "";
    var hrefGidMatch = /[?&]gid=(\d+)/.exec(href);
    if (hrefGidMatch !== null && hrefGidMatch[1] !== "0") {
        return hrefGidMatch[1];
    }

    return undefined;
}

function startGalleryFetch(photoId, galId, targetPhotoId, expectedCount) {
    if (!photoId || photoId === "null" || photoId === "undefined") {
        hideOverlay();
        alert("Missing photo ID for fetch.");
        return;
    }

    activeFetchContext = {
        photoId: photoId,
        galId: galId,
        targetPhotoId: targetPhotoId,
        expectedCount: expectedCount || 0,
        offsetsQueue: [],
        pagesData: {},
        activeWorkers: 0,
        isComplete: false,
        retryCount: {},
        result: [],
        highestOffsetRequested: 0
    };

    if (expectedCount > 0) {
        var totalPages = Math.ceil(expectedCount / 24);
        for (var i = 0; i < totalPages; i++) {
            activeFetchContext.offsetsQueue.push(i * 24);
        }
        activeFetchContext.highestOffsetRequested = (totalPages - 1) * 24;
    } else {
        activeFetchContext.offsetsQueue.push(0);
        activeFetchContext.highestOffsetRequested = 0;
    }

    var workersToStart = expectedCount > 0 ? Math.min(3, Math.ceil(expectedCount / 24)) : 1;
    for (var w = 0; w < workersToStart; w++) {
        fetchNextBatch();
    }
}

function fetchNextBatch() {
    var ctx = activeFetchContext;
    if (!ctx) return;

    if (ctx.isComplete || ctx.offsetsQueue.length === 0) {
        if (ctx.activeWorkers === 0 && !ctx.isComplete) {
            ctx.isComplete = true;
            var key = calculateCacheKey(ctx.photoId, ctx.galId);
            setCachedGallery(key, ctx.result);
            isCurrentGalleryFullyLoaded = true;
            hideOverlay();
        }
        return;
    }

    var currentOffset = ctx.offsetsQueue.shift();
    ctx.activeWorkers++;
    ctx.retryCount[currentOffset] = ctx.retryCount[currentOffset] || 0;

    var url;
    if (ctx.photoId && ctx.photoId !== "undefined") {
        var gidQuery = (ctx.galId && ctx.galId !== "0" && ctx.galId !== "undefined") ? ("&gid=" + ctx.galId) : "";
        url = "https://www.imagefap.com/photo/" + ctx.photoId + "/?idx=" + currentOffset + gidQuery + "&partial=true";
    } else if (ctx.galId && ctx.galId !== "0" && ctx.galId !== "undefined") {
        var page = Math.floor(currentOffset / 24);
        url = "https://www.imagefap.com/pictures/" + ctx.galId + "/?page=" + page;
    } else {
        console.warn("Powerfap: Cannot fetch without photoId or galId");
        handleFetchError(currentOffset);
        return;
    }

    GM_xmlhttpRequest({
        method: "GET",
        url: url,
        headers: {
            "Referer": window.location.href,
            "X-Requested-With": "XMLHttpRequest"
        },
        onload: function (response) {
            ctx.activeWorkers--;
            if (ctx.isComplete) return;

            if (response.status !== 200) {
                handleFetchError(currentOffset);
                return;
            }

            var parser = new DOMParser();
            var doc = parser.parseFromString(response.responseText, "text/html");

            var pageTitle = doc.querySelector(".gal_title, font[size='4'] b, h1, .blk_galleries b, title");
            if (pageTitle && pageTitle.textContent) {
                var extractedTitle = pageTitle.textContent.replace(/\s*-\s*ImageFap.*$/i, "").trim();
                if (extractedTitle && (!currentGalleryTitle || currentGalleryTitle.indexOf("Gallery ") === 0)) {
                    currentGalleryTitle = extractedTitle;
                    updateInlineTitle();
                }
            }

            var newLinks = [];
            var rawItemsCount = 0;

            var items = doc.querySelectorAll("ul.thumbs li a, #_navi_cavi a, td[id] a[href*='/photo/'], #gallery a[href*='/photo/'], .gal_thumb, a[original]");
            if (items.length > 0) {
                rawItemsCount = items.length;
                items.forEach(function (a) {
                    var thumb = "";
                    var img = a.querySelector("img");
                    if (img) {
                        thumb = getImgSrc(img);
                    } else if (a.className === "gal_thumb") {
                        thumb = getImgSrc(a);
                    }
                    if (thumb && thumb.indexOf("/full/") !== -1) {
                        thumb = thumb.replace("/full/", "/thumb/");
                    }
                    if (!thumb) return;

                    var fullSrc = a.getAttribute("original") || a.getAttribute("data-original") || a.getAttribute("href") || "";
                    if (!fullSrc || fullSrc.indexOf("/photo/") !== -1 || !fullSrc.match(/\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i)) {
                        fullSrc = thumb.replace("/thumb/", "/full/").replace("/mini/", "/full/").replace("/grid/", "/full/");
                    } else {
                        fullSrc = toAbsoluteUrl(fullSrc);
                    }

                    var item = { src: fullSrc, thumb: thumb };
                    if (isGalleryImage(item)) {
                        newLinks.push(item);
                    }
                });
            } else {
                var imgs = doc.querySelectorAll("img");
                rawItemsCount = imgs.length;
                imgs.forEach(function (img) {
                    var thumb = getImgSrc(img);
                    if (thumb && thumb.indexOf("/full/") !== -1) {
                        thumb = thumb.replace("/full/", "/thumb/");
                    }
                    if (!thumb) return;
                    var fullSrc = thumb.replace("/thumb/", "/full/").replace("/mini/", "/full/").replace("/grid/", "/full/");
                    var item = { src: fullSrc, thumb: thumb };
                    if (isGalleryImage(item)) {
                        newLinks.push(item);
                    }
                });
            }

            if (rawItemsCount === 0 && ctx.retryCount[currentOffset] < MAX_RETRIES) {
                handleFetchError(currentOffset);
                return;
            }

            ctx.pagesData[currentOffset] = newLinks;

            if (ctx.expectedCount === 0 && rawItemsCount >= 24) {
                ctx.highestOffsetRequested += 24;
                ctx.offsetsQueue.push(ctx.highestOffsetRequested);
            } else if (ctx.expectedCount === 0 && rawItemsCount < 24) {
                ctx.offsetsQueue = [];
            }

            rebuildResultAndCheckDisplay();
            fetchNextBatch();
        },
        onerror: function () {
            ctx.activeWorkers--;
            if (ctx.isComplete) return;
            handleFetchError(currentOffset);
        }
    });
}

function rebuildResultAndCheckDisplay() {
    var ctx = activeFetchContext;
    var orderedOffsets = Object.keys(ctx.pagesData).map(Number).sort(function(a,b){return a-b;});

    var newResult = [];
    var expectedNextOffset = 0;

    for (var i = 0; i < orderedOffsets.length; i++) {
        var offset = orderedOffsets[i];
        if (offset !== expectedNextOffset) {
            break;
        }

        var links = ctx.pagesData[offset];
        links.forEach(function(item) {
             var isDup = false;
             for(var k=0; k<newResult.length; k++) {
                 if (newResult[k].thumb === item.thumb || newResult[k].src === item.src) {
                     isDup = true; break;
                 }
             }
             if (!isDup) newResult.push(item);
        });
        expectedNextOffset = offset + 24;
    }

    ctx.result = newResult;
    var progressText = ctx.result.length + (ctx.expectedCount ? (" / " + ctx.expectedCount) : "");
    updateProgress(progressText);

    if (!isGalleryShown && ctx.result.length > 0) {
        var startIndex = -1;
        if (ctx.targetPhotoId) {
            for (var k = 0; k < ctx.result.length; k++) {
                if ((ctx.result[k].src && ctx.result[k].src.indexOf(ctx.targetPhotoId) !== -1) ||
                    (ctx.result[k].thumb && ctx.result[k].thumb.indexOf(ctx.targetPhotoId) !== -1)) {
                    startIndex = k;
                    break;
                }
            }
        } else {
            startIndex = 0;
        }

        var isEnd = (ctx.offsetsQueue.length === 0 && ctx.activeWorkers === 0);

        if (startIndex !== -1 || isEnd) {
            showGallery(ctx.result, startIndex !== -1 ? startIndex : 0);
            isGalleryShown = true;
        }
    } else if (isGalleryShown && ctx.result.length > 0) {
        updateActiveGalleryItems(ctx.result);
    }
}

function handleFetchError(offset) {
    var ctx = activeFetchContext;
    ctx.retryCount[offset]++;
    if (ctx.retryCount[offset] <= MAX_RETRIES) {
        console.warn("Powerfap: fetch error at offset " + offset + ", retrying...");
        setTimeout(function() {
            ctx.offsetsQueue.push(offset);
            fetchNextBatch();
        }, 1500 * ctx.retryCount[offset]);
    } else {
        ctx.pagesData[offset] = [];
        rebuildResultAndCheckDisplay();
        fetchNextBatch();
    }
}

function updateActiveGalleryItems(items) {
    var el = document.getElementById("fap-gal");
    var lgUid = el ? el.getAttribute('lg-uid') : null;
    if (!window.lgData || !window.lgData[lgUid]) return;

    var instance = window.lgData[lgUid];
    
    if (instance.s) {
        instance.s.dynamicEl = items;
    }

    // Fix: Append missing DOM elements to LightGallery's inner container
    var lgInner = document.querySelector('.lg-inner');
    var currentCount = 0;
    var newItems = null;
    if (lgInner && instance.items) {
        currentCount = instance.items.length;
        for (var j = currentCount; j < items.length; j++) {
            var lgItem = document.createElement('div');
            lgItem.className = 'lg-item';
            lgInner.appendChild(lgItem);
        }
        newItems = lgInner.querySelectorAll('.lg-item');
    }

    var counterAll = document.getElementById("lg-counter-all");
    if (counterAll) {
        counterAll.textContent = items.length;
    }

    var nextBtn = document.querySelector('.lg-next');
    if (nextBtn && instance.index < items.length - 1) {
        nextBtn.classList.remove('lg-disabled');
    }

    // Dynamically update thumbnail strip for new background items
    var thumbContainer = document.querySelector(".lg-thumb");
    var currentThumbCount = 0;
    var newThumbs = null;
    if (thumbContainer) {
        currentThumbCount = thumbContainer.children.length;
        for (var i = currentThumbCount; i < items.length; i++) {
            var thumbDiv = document.createElement("div");
            thumbDiv.className = "lg-thumb-item";
            var img = document.createElement("img");
            img.src = items[i].thumb || items[i].src;
            thumbDiv.appendChild(img);
            (function (index) {
                thumbDiv.addEventListener("click", function () {
                    instance.slide(index);
                });
            })(i);
            thumbContainer.appendChild(thumbDiv);
        }
        newThumbs = thumbContainer.querySelectorAll('.lg-thumb-item');
    }

    // Patch LightGallery internal collections so plugins (like thumbnail) don't crash
    if (newItems || newThumbs) {
        var patchLGCollections = function(obj) {
            if (!obj) return;
            for (var key in obj) {
                if (Object.prototype.hasOwnProperty.call(obj, key)) {
                    var val = obj[key];
                    if (val && typeof val === 'object' && typeof val.length === 'number') {
                        // Skip if it's dynamicEl which was already updated
                        if (key === 'dynamicEl') continue;
                        
                        if (newItems && val.length === currentCount && newItems.length > currentCount) {
                            obj[key] = newItems;
                        } else if (newThumbs && val.length === currentThumbCount && newThumbs.length > currentThumbCount) {
                            obj[key] = newThumbs;
                        }
                    }
                }
            }
        };
        patchLGCollections(instance);
        if (instance.modules) {
            for (var modKey in instance.modules) {
                patchLGCollections(instance.modules[modKey]);
            }
        }
    }

    if (isGridOpen) {
        renderGridMasonry();
    }
    updateInlineTitle();
}

function persistCache() {
    try {
        GM_setValue("resultCache", JSON.stringify(resultCache));
    } catch (e) {
        console.warn("Powerfap: failed to persist cache", e);
    }
}

function queuePreloadImages(items, startIndex, count) {
    for (var i = startIndex; i < items.length && i < startIndex + count; i++) {
        var href = items[i].src;
        if (typeof href === "undefined" || href === null || preloadedImages[href] || preloadQueue.indexOf(href) !== -1) {
            continue;
        }
        if (href.indexOf("/photo/") !== -1 || !href.match(/\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i)) {
            continue;
        }
        preloadQueue.push(href);
    }
    processPreloadQueue();
}

function processPreloadQueue() {
    if (isPreloading || preloadQueue.length === 0) return;

    isPreloading = true;
    var href = preloadQueue.shift();

    var img = new Image();
    img.onload = img.onerror = function() {
        preloadedImages[href] = true;
        isPreloading = false;
        setTimeout(processPreloadQueue, 50);
    };
    img.src = href;
}

// ===================== [GALLERY DISPLAY] =====================

function showGallery(items, startIndex) {
    hideOverlay();

    if (!items || items.length === 0) {
        alert("No images found for this gallery.");
        console.warn("Powerfap: showGallery called with empty items", items);
        return;
    }

    var el = document.getElementById("fap-gal");

    lightGallery(el, {
        dynamic: true,
        dynamicEl: items,
        index: startIndex || 0,
        preload: 1,
        download: true,
        loop: false,
        mode: 'lg-fade',
        animateThumb: true,
        thumbWidth: 100,
        thumbHeight: '80px',
        thumbMargin: 5
    });
}

function initGalleryListeners() {
    var el = document.getElementById("fap-gal");

    el.addEventListener('onCloseAfter', function (e) {
        closeGridModal();
        var lgUid = this.getAttribute('lg-uid');
        if (window.lgData && window.lgData[lgUid]) {
            window.lgData[lgUid].destroy(true);
        }
        if (!isChangingGallery) {
            currentGalleryIndex = -1;
            currentGalleryTitle = "";
            atLastSlide = false;
        } else {
            isChangingGallery = false;
        }
    }, false);

    el.addEventListener('onAfterOpen', function (e) {
        updateInlineTitle();
        injectGridButton();
        injectDownloadAllButton();
    }, false);

    el.addEventListener('onAfterSlide', function (e) {
        var lgUid = this.getAttribute('lg-uid');
        if (!window.lgData || !window.lgData[lgUid]) return;
        var galleryItems = window.lgData[lgUid].items;
        var index = e.detail.index;

        atLastSlide = (index === galleryItems.length - 1);
        updateInlineTitle();

        // Queue next 15 images to load sequentially in background
        queuePreloadImages(galleryItems, index + 1, 15);
    }, false);

    function interceptNextGalleryKey(e) {
        if (e.key !== "ArrowRight") return;

        var el = document.getElementById("fap-gal");
        var lgUid = el ? el.getAttribute('lg-uid') : null;
        if (!window.lgData || !window.lgData[lgUid]) return;
        var instance = window.lgData[lgUid];

        if (instance.index < instance.items.length - 1) {
            if (atLastSlide) {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();
                if (e.type === 'keydown') {
                    instance.slide(instance.index + 1);
                }
            }
            return;
        }

        if (!isCurrentGalleryFullyLoaded) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            if (e.type === 'keydown') {
                showToast("Loading more images...");
            }
            return;
        }

        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        if (e.type !== 'keydown') return;
        if (navigatingToNextGallery) return;
        navigatingToNextGallery = true;
        goToNextGallery();
    }
    document.addEventListener('keydown', interceptNextGalleryKey, true);
    document.addEventListener('keyup', interceptNextGalleryKey, true);

    document.addEventListener('click', function (e) {
        var target = e.target.closest && e.target.closest('.lg-next');
        if (!target) return;

        var el = document.getElementById("fap-gal");
        var lgUid = el ? el.getAttribute('lg-uid') : null;
        if (!window.lgData || !window.lgData[lgUid]) return;
        var instance = window.lgData[lgUid];

        if (instance.index < instance.items.length - 1) {
            if (atLastSlide) {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();
                instance.slide(instance.index + 1);
            }
            return;
        }

        if (!isCurrentGalleryFullyLoaded) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            showToast("Loading more images...");
            return;
        }

        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();
        if (navigatingToNextGallery) return;
        navigatingToNextGallery = true;
        goToNextGallery();
    }, true);

    document.addEventListener('keydown', function (e) {
        if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;

        var outer = document.querySelector('.lg-outer');
        if (!outer || !outer.classList.contains('lg-hide-items')) return;

        setTimeout(function () {
            if (outer) outer.classList.add('lg-hide-items');
        }, 0);
    }, true);

    document.addEventListener('error', function(e) {
        if (e.target && e.target.tagName && e.target.tagName.toLowerCase() === 'img') {
            if (e.target.closest && (e.target.closest('.lg-outer') || e.target.closest('#pf-grid-modal'))) {
                var fallbackSrc = "https://img.icons8.com/?size=100&id=GDM4AYUncYxx&format=png";
                if (e.target.src !== fallbackSrc) {
                    e.target.src = fallbackSrc;
                }
            }
        }
    }, true);
}

// ===================== [MULTI-GALLERY NAVIGATION] =====================

function buildGalleryOrder() {
    galleryOrder = [];
    var seenGids = {};

    function addGallery(galId, title, pageUrl) {
        if (!galId || galId === "0" || seenGids[galId]) return;
        seenGids[galId] = true;
        galleryOrder.push({
            galId: String(galId),
            title: title || ("Gallery " + galId),
            pageUrl: pageUrl || ("/gallery/" + galId)
        });
    }

    // Single-pass DOM query for all gallery containers and links
    var elements = document.querySelectorAll("tr[id^='gid-'], a.gal_title, a[href*='gid='], a[href*='/gallery/']");
    elements.forEach(function (el) {
        if (el.tagName === "TR" && el.id && el.id.indexOf("gid-") === 0) {
            var rawId = el.id.replace("gid-", "");
            if (rawId === "0") return;
            var a = el.querySelector("a.blk_galleries") || el.querySelector("a[href*='gid=']") || el.querySelector("a");
            var title = a ? ((a.querySelector("b") ? a.querySelector("b").textContent : a.textContent) || "").trim() : "";
            var pageUrl = a ? (a.getAttribute("href") || "") : ("/gallery/" + rawId);
            addGallery(rawId, title, pageUrl);
        } else if (el.tagName === "A") {
            var href = el.getAttribute("href") || "";
            var gidMatch = /[?&]gid=(\d+)/.exec(href) || /\/gallery\/(\d+)/.exec(href);
            if (gidMatch) {
                var galId = gidMatch[1];
                var title = extractPageTitleForElement(el);
                addGallery(galId, title, href);
            }
        }
    });
}

function findGalleryIndexById(galId) {
    if (!galId || galId === "0") return -1;
    var targetGid = String(galId);
    for (var i = 0; i < galleryOrder.length; i++) {
        if (String(galleryOrder[i].galId) === targetGid) return i;
    }
    return -1;
}

function fetchFirstPhotoId(pageUrl, callback) {
    var absoluteUrl = pageUrl.indexOf("http") === 0
        ? pageUrl
        : "https://www.imagefap.com" + pageUrl;

    GM_xmlhttpRequest({
        method: "GET",
        url: absoluteUrl,
        onload: function (response) {
            var parser = new DOMParser();
            var doc = parser.parseFromString(response.responseText, "text/html");
            var items = doc.querySelectorAll("ul.thumbs li a, #_navi_cavi a, td[id] a[href*='/photo/'], #gallery a[href*='/photo/'], .gal_thumb");
            
            for (var i = 0; i < items.length; i++) {
                var href = items[i].getAttribute("href");
                var match = href ? galRegex.exec(href) : null;
                if (match) {
                    callback(match[1]);
                    return;
                }
            }
            
            console.warn("Powerfap: couldn't find a photo id on next gallery page", absoluteUrl);
            callback(null);
        },
        onerror: function () {
            console.warn("Powerfap: fetch error on next gallery page", absoluteUrl);
            callback(null);
        }
    });
}

function goToNextGallery() {
    if (currentGalleryIndex === -1 || currentGalleryIndex + 1 >= galleryOrder.length) {
        console.warn("Powerfap: no next gallery available in page order", currentGalleryIndex, galleryOrder.length);
        navigatingToNextGallery = false;
        return;
    }

    currentGalleryIndex++;
    var next = galleryOrder[currentGalleryIndex];
    isChangingGallery = true;

    var el = document.getElementById("fap-gal");
    var lgUid = el ? el.getAttribute('lg-uid') : null;
    if (window.lgData && window.lgData[lgUid]) {
        window.lgData[lgUid].destroy(true);
    }

    if (typeof getCachedGallery(next.galId) !== "undefined") {
        showToast("Now viewing: " + next.title);
        preRun(null, next.galId, next.title);
        return;
    }

    showOverlay();
    fetchFirstPhotoId(next.pageUrl, function (photoId) {
        if (photoId === null) {
            hideOverlay();
            alert("Couldn't load next gallery: " + next.title);
            isChangingGallery = false;
            return;
        }
        showToast("Now viewing: " + next.title);
        preRun(photoId, next.galId, next.title);
    });
}

// ===================== [TOAST / TITLE BAR UI] =====================

function updateInlineTitle() {
    var titleText = currentGalleryTitle;
    if (!titleText && currentGalleryIndex !== -1 && galleryOrder[currentGalleryIndex]) {
        titleText = galleryOrder[currentGalleryIndex].title;
    }
    if (!titleText) {
        titleText = extractPageTitleForElement(document.body);
    }
    if (!titleText) return;

    var el = document.getElementById("fap-gal");
    var lgUid = el ? el.getAttribute('lg-uid') : null;
    var itemCount = (window.lgData && window.lgData[lgUid] && window.lgData[lgUid].items) ? window.lgData[lgUid].items.length : 0;

    if (itemCount > 0) {
        titleText += " (" + itemCount + " images)";
    }

    var existing = document.getElementById("pf-inline-title");
    if (existing) {
        existing.textContent = titleText;
        return;
    }

    var counter = document.getElementById("lg-counter") || document.querySelector(".lg-toolbar");
    if (counter) {
        var titleEl = document.createElement("span");
        titleEl.id = "pf-inline-title";
        titleEl.textContent = titleText;
        if (counter.id === "lg-counter") {
            counter.parentNode.insertBefore(titleEl, counter.nextSibling);
        } else {
            counter.appendChild(titleEl);
        }
    }
}

function showToast(message) {
    var toast = document.getElementById("pf-toast");
    if (!toast) {
        toast = document.createElement("div");
        toast.id = "pf-toast";
        document.body.appendChild(toast);
    }
    toast.textContent = message;

    if (toast._hideTimer) clearTimeout(toast._hideTimer);

    setTimeout(function () { toast.classList.add("show"); }, 10);
    toast._hideTimer = setTimeout(function () {
        toast.classList.remove("show");
    }, 2500);
}

// ===================== [DOWNLOAD HANDLING] =====================

function setupDownloadInterceptor() {
    document.addEventListener('click', function (e) {
        var target = e.target.closest && e.target.closest('#lg-download');
        if (!target) return;

        e.preventDefault();
        e.stopPropagation();
        e.stopImmediatePropagation();

        var url = target.getAttribute('href');
        if (!url || url === 'javascript:void(0)' || url === '#' || url.indexOf("/photo/") !== -1) {
            var el = document.getElementById("fap-gal");
            var lgUid = el ? el.getAttribute('lg-uid') : null;
            if (window.lgData && window.lgData[lgUid]) {
                var instance = window.lgData[lgUid];
                if (instance.items && typeof instance.index === 'number' && instance.items[instance.index]) {
                    url = instance.items[instance.index].src;
                }
            }
        }

        if (!url || url === 'javascript:void(0)' || url === '#') {
            console.warn("Powerfap: could not determine image URL for download");
            return;
        }

        downloadImage(url);
    }, true);
}

function downloadImage(url) {
    var filename = url.split('/').pop().split('?')[0] || ("imagefap_" + Date.now() + ".jpg");
    downloadImageFallback(url, filename);
}

function downloadImageFallback(url, filename) {
    GM_xmlhttpRequest({
        method: "GET",
        url: url,
        responseType: "blob",
        onload: function (response) {
            var blobUrl = URL.createObjectURL(response.response);
            var a = document.createElement("a");
            a.href = blobUrl;
            a.download = filename;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(blobUrl);
        },
        onerror: function (err) {
            console.error("Powerfap: download fallback failed", err);
            alert("Could not download image.");
        }
    });
}

// Replace broken images with userscript icon
document.addEventListener('error', function(e) {
    var target = e.target;
    if (target.tagName && target.tagName.toLowerCase() === 'img') {
        // Prevent infinite loops if the fallback icon also fails to load
        var fallbackIcon = 'https://img.icons8.com/?size=100&id=GDM4AYUncYxx&format=png';
        if (target.src !== fallbackIcon) {
            target.src = fallbackIcon;
            target.title = 'Image not found';
        }
    }
}, true); // Use capture phase because error events don't bubble