Transforms ImageFap into a seamless, continuous LightGallery viewer and Masonry Grid with concurrent background fetching and smart batch downloading.
// ==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)">×</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">×</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