f95zone watched filter

f95zone filter only the watched threads.

As of 20.09.2024. See апошняя версія.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

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

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

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

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        f95zone watched filter
// @namespace   f95zone watched filter
// @description f95zone filter only the watched threads.
// @author       C. Gauthier
// @license     MIT
// @icon        https://www.google.com/s2/favicons?domain=f95zone.to
// @match       https://f95zone.to/sam/latest_alpha/
// @version     1.1.0
// @require     https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// ==/UserScript==

// ==/Changelog==
//removed the old buttons and remplaced them with new ones in the controls-block element

(function (web) {
    'use strict';
    $(function(b) {

        const prefixes = {"23":{"id":23,"name":"SiteRip","class":"label--lightGreen"},"19":{"id":19,"name":"Collection","class":"label--gray"},"13":{"id":13,"name":"VN","class":"label--red"},"14":{"id":14,"name":"Others","class":"label--lightGreen"},"5":{"id":5,"name":"RAGS","class":"label--orange"},"2":{"id":2,"name":"RPGM","class":"label--blue"},"47":{"id":47,"name":"WebGL","class":"pre-webgl"},"3":{"id":3,"name":"Unity","class":"pre-unity"},"4":{"id":4,"name":"HTML","class":"label--olive"},"1":{"id":1,"name":"QSP","class":"label--red"},"6":{"id":6,"name":"Java","class":"pre-java"},"7":{"id":7,"name":"Ren\u0026#039;Py","class":"pre-renpy"},"31":{"id":31,"name":"UnrealEngine","class":"label--royalBlue"},"30":{"id":30,"name":"WolfRPG","class":"label--green"},"8":{"id":8,"name":"Flash","class":"label--gray"},"12":{"id":12,"name":"ADRIFT","class":"label--blue"},"17":{"id":17,"name":"Tads","class":"label--blue"},"18":{"id":18,"name":"Completed","class":"label--blue"},"20":{"id":20,"name":"Onhold","class":"label--skyBlue"},"22":{"id":22,"name":"Abandoned","class":"label--orange"}};

        const css = "a#watched-filter.selected i {color: #c15858;} a#watched-filter:hover i {color: #c15858;} a#data-scraper:hover i {color: #c15858;}"/*{animation: icon-rotate 4s linear infinite;transform-origin: 50% 45%;}@keyframes icon-rotate {from { transform: rotate(0deg); }to { transform: rotate(-1turn); }}"*/;
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);


        var thread_data = GM_getValue('thread_data', []);
        var test = [];


        console.log(GM_getValue("thread_data", []));
        //GM_setValue("thread_data", []);


        // ==/ajaxSetup==
        b.ajaxSetup({
            method: "GET",
            cache: !1,
            dataType: "json",
            timeout: 1E4
        });

        // Save the original send method
        const originalSend = XMLHttpRequest.prototype.send;

        // Override the send method
        XMLHttpRequest.prototype.send = function(body) {
            // Store the reference to the current XHR object
            const xhr = this;

            // Attach an event listener to capture the response
            xhr.addEventListener('readystatechange', function() {
                if (xhr.readyState === XMLHttpRequest.DONE && xhr.responseURL.includes('latest_data.php?cmd=list') && GM_getValue('watchedFilter', false)) {

                    // Here you can modify the response before it's processed by the application
                    const filtered = filterThreadData(xhr.responseURL);
                    const threads = filtered[0];
                    const searchParams = new URLSearchParams(xhr.responseURL); const page = Number(searchParams.get("page"));
                    const pTotal = filtered[1];
                    const count = filtered[2];
                    let customData = JSON.stringify({ "status": "ok", "msg": { "data": threads, "pagination": { "page": page, "total": pTotal }, "count": count }}); // Your custom JSON data

                    // Override the responseText property to return your custom data
                    Object.defineProperty(xhr, 'responseText', { value: customData });
                }
            });

            // Call the original send method to continue with the request
            originalSend.apply(this, arguments);
        };

        function sortArrayByKey(arr, key, ascending = false) {
            return arr.sort((a, b) => {
                if (typeof a[key] === 'string') {
                    return ascending ? a[key].localeCompare(b[key]) : b[key].localeCompare(a[key]);
                } else {
                    return ascending ? a[key] - b[key] : b[key] - a[key];
                }
            });
        }

        function calculateWeightedRating(threads, m = 3.5, C = 100000) {
            // C is the minimum number of views required to be considered
            // m is the average rating across all threads

            m = threads.reduce((accumulator, currentValue) => accumulator + currentValue.rating, 0,)/threads.length; //~3.5

            return threads.map(thread => {
                const { rating, views } = thread;

                // Calculate the weighted rating using the Bayesian formula
                const weightedRating = (views*rating + m*C) / (views+C)

                return {
                    ...thread,
                    weightedRating: weightedRating.toFixed(2) // round to 2 decimal places
                };
            });
        }

        function filterThreadData(url){
            var tmp_thread_data = GM_getValue('thread_data', []);
            const searchParams = new URLSearchParams(url);

            //sort
            if(searchParams.has("sort")){
                let key = searchParams.get("sort"); let ascending = key == "title";
                switch(searchParams.get("sort")) {
                    case "date":
                        break;
                    case "rating":
                        tmp_thread_data = calculateWeightedRating(tmp_thread_data);
                        console.log(thread_data);
                        console.log(tmp_thread_data[0].title, "date :", tmp_thread_data[0].date, "weightedRating :", tmp_thread_data[0].weightedRating);
                        sortArrayByKey(tmp_thread_data, "weightedRating", ascending);
                        break;
                    default:
                        sortArrayByKey(tmp_thread_data, key, ascending);
                }
            }

            //date
            if(searchParams.has("date")){
                let key = Number(searchParams.get("date"));
                tmp_thread_data = tmp_thread_data.filter(thread => {
                    const timeDifference = new Date() - new Date(thread.date);
                    const daysDifference = timeDifference / (1000 * 60 * 60 * 24);
                    return daysDifference < key
                });
            }

            //tags (and/or)
            if(searchParams.has("tags[]")){
                let key = searchParams.getAll("tags[]").map(Number);
                tmp_thread_data = tmp_thread_data.filter(thread => {
                    if(searchParams.has("tagtype")){
                        // Return true if at least one of the tags in key is found in the thread's tags array
                        return key.some(tag => thread.tags.includes(tag));
                    }else{
                        // Check if every tag in keys is present in item.tags
                        return key.every(tag => thread.tags.includes(tag));
                    }
                });
            }

            //notags
            if(searchParams.has("notags[]")){
                let key = searchParams.getAll("notags[]").map(Number);
                tmp_thread_data = tmp_thread_data.filter(thread => {
                    // Return true if none of thetags in key are found in the thread's tags array
                    return !key.some(tag => thread.tags.includes(tag));
                });
            }

            //prefixes
            if(searchParams.has("prefixes[]")){
                let key = searchParams.getAll("prefixes[]").map(Number);
                tmp_thread_data = tmp_thread_data.filter(thread => {
                    // Return true if all of the prefixes in key are found in the thread's prefixes array
                    return key.every(tag => thread.prefixes.includes(tag));
                });
            }

            //noprefixes
            if(searchParams.has("noprefixes[]")){
                let key = searchParams.getAll("noprefixes[]").map(Number);
                tmp_thread_data = tmp_thread_data.filter(thread => {
                    // Return true if none of the prefixes in key are found in the thread's prefixes array
                    return !key.some(tag => thread.prefixes.includes(tag));
                });
            }

            //search
            if(searchParams.has("search")){
                let key = searchParams.get("search").toLowerCase();
                tmp_thread_data = tmp_thread_data.filter(thread => thread.title.toLowerCase().includes(key));
            }

            //creator
            if(searchParams.has("creator")){
                let key = searchParams.get("creator").toLowerCase();
                tmp_thread_data = tmp_thread_data.filter(thread => thread.creator.toLowerCase().includes(key));
            }

            //page and rows
            let page = [];
            let rows = 30; if(searchParams.has("rows")){rows = Number(searchParams.get("rows"));}
            if(searchParams.has("page")){
                let key = Number(searchParams.get("page"));
                for(let id = rows*(key-1); id < Math.min(rows*key, tmp_thread_data.length); id++){
                    page.push(tmp_thread_data[id]);
                }
            }

            //Date() to String ( Date() => "x mins / hrs / days / weeks / months / years" )
            page.forEach(thread => {
                let d = timeSince(Date.parse(thread.date));
                thread.date = String(d[0]) + " "+ d[1];
            });


            return [page, Math.ceil(tmp_thread_data.length/rows), tmp_thread_data.length];
        }



        function subtractTime(date, input) {
            const [amount, unit] = (() => {
                const parts = input.split(' ');
                if (parts.length === 2) {
                    return [parseInt(parts[0]), parts[1]];
                } else if (input.toLowerCase() === "yesterday") {
                    return [1, "days"];
                } else {
                    return [null, null];
                }
            })();

            if (amount === null || unit === null) {
                return date;
            }

            const newDate = new Date(date);

            switch (unit) {
                case 'min':
                case 'mins':
                    newDate.setMinutes(newDate.getMinutes() - amount);
                    break;
                case 'hr':
                case 'hrs':
                    newDate.setHours(newDate.getHours() - amount);
                    break;
                case 'days':
                    newDate.setDate(newDate.getDate() - amount);
                    break;
                case 'week':
                case 'weeks':
                    newDate.setDate(newDate.getDate() - (amount * 7));
                    break;
                case 'month':
                case 'months':
                    newDate.setMonth(newDate.getMonth() - amount);
                    break;
                case 'year':
                case 'years':
                    newDate.setFullYear(newDate.getFullYear() - amount);
                    break;
                default:
                    throw new Error('Invalid time unit');
            }

            return newDate;
        }

        function timeSince(date) {
            const now = new Date();
            const diffInMs = now - date;

            const minutes = Math.floor(diffInMs / (1000 * 60));
            const hours = Math.floor(diffInMs / (1000 * 60 * 60));
            const days = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
            const weeks = Math.floor(days / 7);
            const months = Math.floor(days / 30);
            const years = Math.floor(days / 365);

            if (minutes < 1) {
                return [0, "min"];
            } else if (minutes === 1) {
                return [1, "min"];
            } else if (minutes < 60) {
                return [minutes, "mins"];
            } else if (hours === 1) {
                return [1, "hr"];
            } else if (hours < 24) {
                return [hours, "hrs"];
            } else if (days === 1) {
                return ["", "yesterday"];
            } else if (days < 7) {
                return [days, "days"];
            } else if (weeks === 1) {
                return [1, "week"];
            } else if (weeks < 4) {
                return [weeks, "weeks"];
            } else if (months === 1) {
                return [1, "month"];
            } else if (months < 12) {
                return [months, "months"];
            } else if (years === 1) {
                return [1, "year"];
            } else {
                return [years, "years"];
            }
        }

        function splitString(input) {
            const parts = input.split(' ');

            if (parts.length === 2) {
                if(parts[1] == "hours"){return [parts[0], "hrs"];}
                return [parts[0], parts[1]];
            } else {
                return ["", parts[0]];
            }
        }

        function uniqByKeepFirst(data, key) {
            const seen = new Set();
            return data.filter(obj => {
                const keyValue = obj[key];
                if (seen.has(keyValue)) {
                    return false;
                } else {
                    seen.add(keyValue);
                    return true;
                }
            });
        }

        /*
        function AlternateFilter(){
            const element = document.querySelectorAll('.resource-tile');
            const data = GM_getValue("thread_data",[]);
            const parent = document.querySelector('.grid-selected');

            for(var i = 0; i < Math.max(data.length, 300); i++){
                const e = data[i];
                if(e.watched == true){
                    const newDiv = document.createElement("div");

                    var views = e.views;
                    if (999 < e.views && e.views <= 999999) {views = String(Math.floor(e.views/1000)); if(views.length==1){views+="."+String(Math.floor(e.views/100)-Number(views)*10);} views+="K";}
                    else if (e.views > 999999) {views = String(Math.floor(e.views/1000000)); if(views.length==1){views+="."+String(Math.floor(e.views/100000)-Number(views)*10);} views+="M";}

                    var ratingStars = Math.floor(e.rating/5*100); //the % of the 5 rating stars of the thread needing to be filled

                    var tags = e.tags[0]; for(var j0 = 1; j0 < e.tags.length; j0++){tags += "," + e.tags[j0];} //the list of tags of the thread

                    var images = e.screens[0]; for(var j1 = 1; j1 < e.screens.length; j1++){images += "," + e.screens[j1];} //the urls of preview pictures of the thread

                    var eDate = Date.parse(e.date); //the aproximate date since creation of the thread
                    var date_type = timeSince(eDate)[1];
                    var date = String(timeSince(eDate)[0]);


                    var p = e.prefixes;
                    var prefixes_left = ''; var prefixes_right = '';
                    p.forEach((id) => {
                        var temp = '<div class="'+prefixes[id]["class"]+'">'+prefixes[id]["name"]+'</div>';
                        if(id == 18 || id == 20 || id == 22){
                            prefixes_right += temp;
                        }else{
                            prefixes_left += temp;
                            }
                    });


                    // Set the class attribute
                    newDiv.className = 'resource-tile resource-tile_update border-gradient';

                    // Set the style attribute (even if it's empty here)
                    newDiv.style.cssText = '';

                    // Set the custom data attributes
                    newDiv.setAttribute('data-thread-id', e.thread_id);
                    newDiv.setAttribute('data-tags', tags);
                    newDiv.setAttribute('data-images', images);

                    // Set the other attributes
                    newDiv.setAttribute('isdisplay', e.watched);
                    newDiv.setAttribute('isignore', e.ignored);
                    newDiv.setAttribute('isOriginal', 'false');

                    newDiv.innerHTML = '<a href="https://f95zone.to/threads/'+e.thread_id+'/" class="resource-tile_link" rel="noopener" target="_blank" title=""><div class="resource-tile_thumb-wrap"><div class="resource-tile_thumb" style="background-image:url('+e.cover+')"></div></div><div class="resource-tile_body" style="position: relative; top: auto; width: 100%; left: auto; height: auto;"><div class="resource-tile_label-wrap"><div class="resource-tile_label-wrap_left">'+prefixes_left+'</div><div class="resource-tile_label-wrap_right">'+prefixes_right+'<div class="resource-tile_label-version">'+e.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" style="text-overflow: ellipsis; text-indent: 0px; transition: text-indent 0.2s linear;">'+e.title+'</h2></div><div class="header_title-ver">'+e.version+'</div><div class="resource-tile_dev fas fa-user">'+e.creator+'</div></header><div class="resource-tile_info-meta"><div class="resource-tile_info-meta_time"><span class="tile-date_'+date_type+'">'+date+'</span></div><div class="resource-tile_info-meta_likes">'+e.likes+'</div><div class="resource-tile_info-meta_views">'+views+'</div><div class="resource-tile_info-meta_rating">'+e.rating+'</div><div class="resource-tile_rating"><span style="width:'+ratingStars+'%"></span></div></div></div></div></a>'
                    parent.appendChild(newDiv);
                }

            };
        }
        */

        function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }

        async function getPagesData(page_number){

            //var page_number = GM_getValue('pages_to_filter', 1);
            console.log("data is loading, pages searched : " + page_number);
            //page_number = document.querySelector("#sub-nav_inner > div.sub-nav_paging > div").lastChild.getAttribute("data-page");// need to find something else to not use the "item per page" slider
            //page_number = 1;

            // Create the overlay div
            var overlay = document.createElement('div');

            // Apply styles to the overlay div
            overlay.style.position = 'fixed'; overlay.style.top = '0';overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; //style
            overlay.style.zIndex = '9999'; // ensure it's on top of all other elements

            // Create the message element
            var message = document.createElement('div');
            message.innerText = 'Please wait, loading...';
            message.style.color = 'white';message.style.fontSize = '24px';message.style.fontFamily = 'Arial, sans-serif';message.style.textAlign = 'center';message.style.padding = '20px';message.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';message.style.borderRadius = '10px';

            // Append the message to the overlay
            overlay.appendChild(message);

            // Add the overlay to the document body
            document.body.appendChild(overlay);

            for (let i = 1; i <= page_number; i++) {
                console.log("page "+i+" ...");
                message.innerText = 'Please wait, loading... \n' + (page_number-i+1) + ' pages left';
                await b.ajax({
                    url: "latest_data.php?cmd=list&cat=games&page="+i+"&sort=date&rows=90&_=1710376207895", //only works if the "item per page" slider is on 90 ¤¤
                    success: function(f) {
                        f.msg.data.forEach(function(l) {if(l.watched){

                            var newDate = subtractTime(new Date(), l.date); // change the l.date string as a Date object aproximating the creation date instead of a string of the time since creation
                            l.date = String(newDate);

                            test.push(l);
                        }});
                    }
                });
            }

            if (overlay) {
                overlay.parentNode.removeChild(overlay);
            }
            var concat = test.concat(thread_data);
            thread_data = uniqByKeepFirst(concat, "thread_id");
            GM_setValue("thread_data", thread_data);
            await sleep(500);
            console.log("your watched threads data :", GM_getValue("thread_data", "data not found"));
        }


        function createButtons(){
            // Select the controls-block where we want to add new elements
            var controlsBlock = document.querySelector(".controls-block");

            // Create the data scraper button
            var scraper = document.createElement('a');
            scraper.id = "data-scraper";
            scraper.setAttribute('data-tooltip', 'get filter data');

            var scraperIcon = document.createElement('i');
            scraper.appendChild(scraperIcon);
            scraperIcon.className = 'fas fa-download';

            // Create the watched thread filter button
            var filter = document.createElement('a');
            filter.id = "watched-filter";
            filter.setAttribute('href', '#/cat=games/page=1'); //to change ¤¤
            filter.setAttribute('rel', 'ajax');
            filter.setAttribute('data-tooltip', 'filter watched');
            if(GM_getValue('watchedFilter')){$(filter).addClass("selected");}

            var filterIcon = document.createElement('i');
            filter.appendChild(filterIcon);
            filterIcon.className = 'fa fa-eye';

            // Add listeners to the buttons

            const onClickScraper = event => {
                if(GM_getValue('watchedFilter', false)){
                    window.alert("can't get the data while the watched filter is checked"); //need to resolve that ¤¤
                }else{
                    let nbPages = window.prompt("Enter the number of pages you want to filter \n(with 90 items each page)", "1");
                    while(!Number.isInteger(parseInt(nbPages)) && !(nbPages == null)){nbPages = window.prompt("Please enter either a number or 'all'", "1");}
                    //if(nbPages == null){scraper.setAttribute('data-tooltip', "impossible");} //should inform user that this won't work ¤¤
                    console.log("number of page being filtered :", nbPages);
                    getPagesData(parseInt(nbPages));
                }
            };
            scraper.addEventListener("click", onClickScraper);

            const onClickWatched = event => {
                var e = $(event.currentTarget);
                e.hasClass("selected") ? (e.removeClass("selected")) : (e.addClass("selected"))
                GM_setValue('watchedFilter', e.hasClass("selected"));
            };
            filter.addEventListener("click", onClickWatched);

            // Insert the buttons elements into the controls block
            var autoRefreshControl = document.getElementById("controls_auto-refresh");
            controlsBlock.insertBefore(scraper, autoRefreshControl);
            controlsBlock.insertBefore(filter, autoRefreshControl);
        }

        async function init() {
            createButtons();

            let t = document.createElement('a');                                                       // this whole section is very bad, I'll change it next time ¤¤
            const sp2 = $('a[rel="noopener noreferrer"]:contains("sit amet")')[0];
            const parentDiv = $('a[rel="noopener noreferrer"]:contains("sit amet")')[0].parentNode;
            parentDiv.insertBefore(t, sp2);
            t.setAttribute('href', '#/cat=games/page=1');
            t.setAttribute('rel', 'ajax');
            console.log(t);

            await sleep(500);


            t.click();
        }

        init();
    });

})();