// ==UserScript==
// @name Ultimate F95Zone
// @namespace https://github.com/balu100/Ultimate-F95Zone
// @version 1.7
// @license MIT
// @description Ultimate F95 - Removed const reassignment error, prefix display.
// @author balu100
// @match https://f95zone.to/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=f95zone.to
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// --- Constants and State Variables ---
let currentPage = 1;
let isLoading = false;
let noMorePages = false;
let currentFilters = '';
let infiniteScrollInitialized = false;
let itemsPerRow = 90;
let siteOptions = { newTab: "true", version: "small", searchHighlight: "true" };
let sitePrefixData = null; // To store latestUpdates.prefixes
let prefixCache = {}; // Cache for getPrefixDetails
const itemContainerSelector = 'div#latest-page_items-wrap_inner';
const itemSelector = '.resource-tile';
const originalPaginationSelector = '.sub-nav_paging';
const AJAX_ENDPOINT_URL = 'https://f95zone.to/sam/latest_alpha/latest_data.php';
// --- Helper Function Definitions ---
const htmlEscape = (str) => {
if (str === null || str === undefined) return '';
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
};
const highlightUnreadLinks = () => {
try {
document.querySelectorAll('a:not(.resource-item_row-hover_outer > a)').forEach(link => {
const text = link.textContent.trim().toLowerCase();
const buttonTextEl = link.querySelector('.button-text');
const buttonText = buttonTextEl ? buttonTextEl.textContent.trim().toLowerCase() : '';
if (link.href.includes('/unread?new=1') || text === 'jump to new' || buttonText === 'jump to new') {
link.classList.add('highlight-unread');
if (buttonTextEl) buttonTextEl.classList.add('highlight-unread');
}
});
} catch (e) { console.error("Error in highlightUnreadLinks:", e); }
};
const getCurrentPageFromUrl = () => {
const match = (window.location.hash || '').match(/page=(\d+)/);
return match && match[1] ? parseInt(match[1], 10) : 1;
};
const getCurrentFiltersFromUrl = () => {
const defaultFilters = { cat: 'games', sort: 'date', tagtype: 'and', date: 0 };
const parsedHashFilters = {}; const hash = window.location.hash;
if (hash && hash.length > 1 && hash !== '#/') {
let rhP = hash.substring(1); if (rhP.startsWith('/')) rhP = rhP.substring(1);
rhP.split('/').forEach(part => {
if (part.includes('=')) {
let [key, value] = part.split('='); value = decodeURIComponent(value);
if (key && value !== undefined && key !== 'page') parsedHashFilters[key] = value;
}});
}
const finalFilterState = { ...defaultFilters, ...parsedHashFilters };
const params = new URLSearchParams(); const arrayKeys = ['tags', 'notags', 'prefixes', 'noprefixes'];
for (const key in finalFilterState) {
if (finalFilterState.hasOwnProperty(key)) {
const value = finalFilterState[key];
if (arrayKeys.includes(key)) {
if (String(value).length > 0) String(value).split(',').filter(Boolean).forEach(sV => params.append(`${key}[]`, sV));
} else if (key==='cat'||key==='sort'||(key==='tagtype'&&value!=='and')||(key==='date'&&Number(value)!==0)||(['search','creator'].includes(key)&&String(value).length>0)) {
params.set(key, String(value));
}}}
return params.toString();
};
function getPrefixDetails(prefixId, category) {
const cacheKey = `${category}-${prefixId}`;
if (prefixCache[cacheKey]) return prefixCache[cacheKey];
if (!sitePrefixData || !sitePrefixData[category]) return false;
for (const group of sitePrefixData[category]) {
for (const prefix of group.prefixes) {
if (String(prefix.id) === String(prefixId)) { // Compare as strings after ensuring they are numbers
const details = { id: prefix.id, name: prefix.name, class: prefix.class, parentId: group.id };
prefixCache[cacheKey] = details; return details;
}
}
}
return false;
}
function createItemElement(itemData_g) {
const tile = document.createElement('div');
let tileCls = ['resource-tile', 'userscript-generated-tile'];
if(itemData_g.ignored)tileCls.push('resource-tile_ignored');
itemData_g.new ? tileCls.push('resource-tile_new') : tileCls.push('resource-tile_update');
const cat = (new URLSearchParams(currentFilters)).get('cat') || 'games';
if(cat) { tileCls.push(`${cat}-item`, `resource-tile_${cat}`); }
tile.className = tileCls.join(' ');
tile.dataset.threadId=String(itemData_g.thread_id||'0');
tile.dataset.tags=(itemData_g.tags||[]).join(',');
tile.dataset.images=(itemData_g.screens&&itemData_g.screens.length?itemData_g.screens:[itemData_g.cover||'']).join(',');
let tLink=`https://f95zone.to/threads/${htmlEscape(String(itemData_g.thread_id||'0'))}/`;
let title=htmlEscape(itemData_g.title||'No Title');
let titleAttr=htmlEscape(itemData_g.title||'No Title');
let creator=htmlEscape(itemData_g.creator||'Unknown');
let version=(itemData_g.version&&itemData_g.version!=="Unknown")?htmlEscape(itemData_g.version):"";
let cover=htmlEscape(itemData_g.cover||'');
let date=itemData_g.date||'N/A';
const dM=String(date).match(/^([0-9]+ )?([A-Za-z ]+)$/i);
let dH=dM?`<span class="tile-date_${htmlEscape(dM[2]).toLowerCase().replace(/ /g,"")}">${dM[1]?htmlEscape(dM[1].trim()):""}</span>`:htmlEscape(date);
let views=String(itemData_g.views||0);
if(itemData_g.views > 1E6)views=(itemData_g.views/1E6).toFixed(1)+"M"; else if(itemData_g.views > 1E3)views=Math.round(itemData_g.views/1E3)+"K";
let likes=htmlEscape(String(itemData_g.likes||0));
let rV=Number(itemData_g.rating)||0;
let rD=rV===0?"-":rV.toFixed(1);
let rW=20*rV;
const newTab=(siteOptions.newTab==="true")?' target="_blank"':'';
const smallVer=(siteOptions.version==="small");
let prefixesLeftHTML = ""; let prefixesRightHTML = "";
if (itemData_g.prefixes && sitePrefixData) {
itemData_g.prefixes.forEach(prefixId => {
const prefixInfo = getPrefixDetails(prefixId, cat);
if (prefixInfo) {
const prefixDiv = `<div class="${htmlEscape(prefixInfo.class)}">${htmlEscape(prefixInfo.name)}</div>`;
if (String(prefixInfo.parentId) === "4") prefixesRightHTML += prefixDiv; // Status prefixes
else prefixesLeftHTML += prefixDiv;
}
});
}
tile.innerHTML = `
<a href="${tLink}" class="resource-tile_link" rel="noopener"${newTab}>
<div class="resource-tile_thumb-wrap"><div class="resource-tile_thumb" style="background-image:url(${cover?"'"+cover+"'":'none'})">${itemData_g.watched?'<i class="far fa-eye watch-icon"></i>':''}</div></div>
<div class="resource-tile_body">
<div class="resource-tile_label-wrap">
<div class="resource-tile_label-wrap_left">${prefixesLeftHTML}</div>
<div class="resource-tile_label-wrap_right">${prefixesRightHTML}<div class="resource-tile_label-version">${(cat!=="assets"&&version&&smallVer)?version:""}</div></div>
</div>
<div class="resource-tile_info">
<header class="resource-tile_info-header"><div class="header_title-wrap"><h2 class="resource-tile_info-header_title">${title}</h2></div><div class="header_title-ver">${(cat!=="assets"&&version&&!smallVer)?version:""}</div><div class="resource-tile_dev fas fa-user">${(cat!=="assets")?` ${creator}`:""}</div></header>
<div class="resource-tile_info-meta"><div class="resource-tile_info-meta_time">${dH}</div><div class="resource-tile_info-meta_likes">${likes}</div><div class="resource-tile_info-meta_views">${views}</div>${(cat!=="assets"&&cat!=="comics")?`<div class="resource-tile_info-meta_rating">${rD}</div>`:""}<div class="resource-tile_rating"><span style="width:${rW}%"></span></div></div>
</div>
</div>
</a>`;
return tile;
}
async function loadMoreItems() {
if(isLoading||noMorePages)return;isLoading=true;
const bP=Number(currentPage);
if(isNaN(bP)){console.error("loadMoreItems: currentPage is NaN!",currentPage);isLoading=false;noMorePages=true;const elT=document.getElementById('infinite-scroll-trigger');if(elT)elT.classList.add('no-more');return;}
const nP=bP+1;
const lT=document.getElementById('infinite-scroll-trigger');if(lT)lT.classList.add('loading');
console.log(`loadMoreItems: Attempting page ${nP}. Filters: '${currentFilters}'`);
try{
const p=new URLSearchParams(currentFilters);
p.set('cmd','list');p.set('page',nP.toString());p.set('rows',itemsPerRow.toString());p.set('_',Date.now().toString());
const url=`${AJAX_ENDPOINT_URL}?${p.toString()}`;
// console.log(`loadMoreItems: Fetching URL: ${url}`);
const r=await fetch(url,{method:'GET',headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json, text/javascript, */*; q=0.01'}});
if(!r.ok){console.error("loadMoreItems: Fetch fail",r.status,url);noMorePages=true;return;}
const d=await r.json();
if(d&&d.status==='ok'&&d.msg&&d.msg.data){
const i=d.msg.data,c=document.querySelector(itemContainerSelector);
if(i.length&&c){
const f=document.createDocumentFragment(),aE=[];
i.forEach(iD=>{const el=createItemElement(iD);if(el){f.appendChild(el);aE.push(el);}});
c.appendChild(f);
if(typeof XF!=='undefined'&&XF.activate)aE.forEach(el=>XF.activate(el));
currentPage=nP;
highlightUnreadLinks();
if(d.msg.pagination&&d.msg.pagination.page>=d.msg.pagination.total)noMorePages=true;
}else{noMorePages=true;}
}else{noMorePages=true;console.error("loadMoreItems: AJAX response error",d);}
}catch(e){console.error('loadMoreItems: Error during AJAX/processing:',e);noMorePages=true;}
finally{isLoading=false;if(lT&&!noMorePages)lT.classList.remove('loading');else if(lT)lT.classList.add('no-more');}
}
function handleSiteStateChange(eventSource = "unknown") {
console.log(`handleSiteStateChange by: ${eventSource}. Hash: ${window.location.hash}`);
const newPg = getCurrentPageFromUrl(); const newFi = getCurrentFiltersFromUrl();
console.log(`StateChange Check: OldF('${currentFilters}') vs NewF('${newFi}')`);
console.log(`StateChange Check: OldP(${currentPage}) vs NewHashP(${newPg})`);
if (currentFilters !== newFi || (newPg === 1 && currentPage !== newPg )) {
console.log(`State change ACTION. Resetting.`);
currentPage = 1; currentFilters = newFi; noMorePages = false; isLoading = false;
const lT = document.getElementById('infinite-scroll-trigger'); if (lT) lT.className = '';
console.log(`State reset: curP=${currentPage}, curF='${currentFilters}'`);
}
}
function actualInitInfiniteScroll() {
if(infiniteScrollInitialized)return;const mA=document.querySelector(itemContainerSelector);
if(!mA||!mA.querySelector(itemSelector)){console.warn('actualInit:No initial items.');return;}
currentPage=getCurrentPageFromUrl();currentFilters=getCurrentFiltersFromUrl();
if(isNaN(Number(currentPage))){currentPage=1;}
infiniteScrollInitialized=true;console.log(`actualInit: p${currentPage}, f'${currentFilters}'`);
if(typeof latestUpdates!=='undefined'&&latestUpdates.options){
siteOptions=latestUpdates.options;itemsPerRow=parseInt(siteOptions.rows,10)||90;
if (window.latestUpdates && window.latestUpdates.prefixes) { // Also grab prefix data here
sitePrefixData = window.latestUpdates.prefixes;
console.log("Site prefix data captured in actualInit.");
}
}
const trg=document.createElement('div');trg.id='infinite-scroll-trigger';mA.insertAdjacentElement('afterend',trg);
new IntersectionObserver(e=>{if(e[0].isIntersecting&&!isLoading&&!noMorePages)loadMoreItems();},{threshold:0.01}).observe(trg);
window.addEventListener('hashchange', handleSiteStateChange, false);
new MutationObserver(ml=>{for(const m of ml){if(m.type==='childList'&&m.removedNodes.length>0){handleSiteStateChange("MutationObserver (content cleared)");break;}}}).observe(mA,{childList:true});
}
const style = document.createElement('style');
function adjustPageWidth() {
const pbi=document.querySelector('.p-body-inner');const pni=document.querySelector('.p-nav-inner');
const pc=document.querySelector('.pageContent');const nw=window.innerWidth*0.95;
if(pbi)pbi.style.maxWidth=`${nw}px`;if(pni)pni.style.maxWidth=`${nw}px`;
if(pc&&(pc.closest('.p-header-banner')||!pc.closest('footer')))pc.style.maxWidth=`${nw}px`;
};
style.innerHTML = `
.pageContent {max-width: ${(window.innerWidth*0.95)}px !important;max-height:360px!important;transition:none!important;top:110px!important;margin-left:auto!important;margin-right:auto!important;}
.p-body-inner,.p-nav-inner {max-width:${(window.innerWidth*0.95)}px !important;margin-left:auto!important;margin-right:auto!important;transition:none!important;box-sizing:border-box!important;}
.cover-hasImage {height:360px!important;transition:none!important;}
.p-sectionLinks,.uix_extendedFooter,.p-footer-inner,.view-thread.block--similarContents.block-container,.js-notices.notices--block.notices,${originalPaginationSelector}{display:none!important;}
.highlight-unread {color:cyan;font-weight:bold;text-shadow:1px 1px 2px black;}
.uix_contentWrapper {max-width:100%!important;padding-left:5px!important;padding-right:5px!important;box-sizing:border-box!important;}
.p-body-main--withSideNav {display:flex!important;flex-direction:row!important;max-width:100%!important;padding:0!important;box-sizing:border-box!important;}
main#latest-page_main-wrap {flex-grow:1!important;margin-left:0!important;margin-right:10px!important;min-width:0;box-sizing:border-box!important;}
aside#latest-page_filter-wrap {flex-shrink:0!important;width:280px!important;margin-left:0!important;margin-right:0!important;box-sizing:border-box!important;}
aside#latest-page_filter-wrap.filter-hidden,aside#latest-page_filter-wrap[style*="display:none"]{display:none!important;}
main#latest-page_main-wrap:has(+ aside#latest-page_filter-wrap.filter-hidden),main#latest-page_main-wrap:has(+ aside#latest-page_filter_wrap[style*="display:none"]){margin-right:0!important;}
div#latest-page_items-wrap {width:100%!important;margin-left:0!important;box-sizing:border-box!important;}
${itemContainerSelector}.resource-wrap-game.grid-normal {display:grid!important;grid-template-columns:repeat(auto-fit,minmax(250px,1fr))!important;gap:15px!important;padding:0!important;box-sizing:border-box!important;}
#infinite-scroll-trigger {padding:20px;text-align:center;font-size:1.2em;color:#777;border:1px solid transparent!important;min-height:40px!important;margin-top:10px!important;}
#infinite-scroll-trigger.loading::after {content:"Loading more items...";} #infinite-scroll-trigger.no-more::after {content:"No more items to load.";}
.userscript-generated-tile .resource-tile_thumb{background-size:cover;background-position:center;background-repeat:no-repeat;} .userscript-generated-tile .resource-tile_dev.fas.fa-user::before{margin-right:0.3em;}
`;
document.documentElement.appendChild(style);
function onDomReady() {
highlightUnreadLinks();
adjustPageWidth();
window.addEventListener('resize', adjustPageWidth);
// Capture sitePrefixData as early as possible if latestUpdates exists
if (typeof window.latestUpdates !== 'undefined' && window.latestUpdates.prefixes) {
sitePrefixData = window.latestUpdates.prefixes;
// console.log("Site prefix data captured on DOM ready.");
}
setTimeout(() => {
const mainContentArea = document.querySelector(itemContainerSelector);
if (mainContentArea && mainContentArea.querySelector(itemSelector)) {
actualInitInfiniteScroll();
} else { console.error("Initial items NOT found after 3s for init."); }
}, 3000);
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', onDomReady);
else onDomReady();
})();