Imagefap Gallery Viewer Pro

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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