GreasyFork/SleazyFork Star Rating Display

Turns the visual 3-level green/yellow/red bar into a 0..5 star rating.

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 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            GreasyFork/SleazyFork Star Rating Display
// @name:de         GreasyFork/SleazyFork Sterne-Bewertungsanzeige
// @name:en         GreasyFork/SleazyFork Star Rating Display
// @name:fr         GreasyFork/SleazyFork Affichage der l'évaluation par étoiles
// @name:zh-CN      GreasyFork/SleazyFork 星级评分显示
//
// @description     Turns the visual 3-level green/yellow/red bar into a 0..5 star rating.
// @description:de  Verwandelt die visuelle 3-stufige grüne/gelbe/rote Leiste in eine 0..5-Sterne-Bewertung.
// @description:en  Turns the visual 3-level green/yellow/red bar into a 0..5 star rating.
// @description:fr  Transforme la barre visuelle verte/jaune/rouge à 3 niveaux en une évaluation de 0 à 5 étoiles.
// @description:zh-CN 将视觉上的绿/黄/紅 3 级进度条转换为 0..5 星级评分。
//
// @version         0.0.8
// @author          Wack.3gp (https://greatest.deepsurf.us/users/4792)
// @copyright       2026+, Wack.3gp
// @namespace       https://greatest.deepsurf.us/users/4792
// @license         CC BY-NC-SA-4.0; https://creativecommons.org/licenses/by-nc-sa/4.0/
// @icon            https://greatest.deepsurf.us/vite/assets/blacklogo16-DftkYuVe.png
//
// @match           https://greatest.deepsurf.us/*/scripts*
// @match           https://greatest.deepsurf.us/*/users/*
// @match           https://sleazyfork.org/*/scripts*
// @match           https://sleazyfork.org/*/users/*
//
// @grant           GM_xmlhttpRequest
// @grant           GM_info
// @grant           GM_notification
// @connect         sleazyfork.org
// @run-at          document-end
//
// @supportURL      https://greatest.deepsurf.us/scripts/576222/feedback
// @compatible      Chrome tested with Tampermonkey
// @contributionURL https://www.paypal.com/donate/?hosted_button_id=BYW9D395KJWZ2
// @contributionAmount €1.00
// ==/UserScript==

(function() {
    'use strict';

    const incrementVisibility = () => {
        let views = parseInt(localStorage.getItem('star_rating_views') || '0');
        views++;
        localStorage.setItem('star_rating_views', views);

        if (views === 2500) {
            const donationUrl = (typeof GM_info !== 'undefined') ? GM_info.script.header.match(/@contributionURL\s+(.+)/)[1] : "https://www.paypal.com/donate?hosted_button_id=BYW9D395KJWZ2";
            
            GM_notification({
                title: '☕ Support Wack.3gp',
                text: `You've viewed ${views} script ratings! Enjoying the star display? Click here to buy me a coffee.`,
                image: 'https://greatest.deepsurf.us/vite/assets/blacklogo96-CxYTSM_T.png',
                timeout: 10000,
                onclick: function() {
                    window.open(donationUrl, "_blank");
                }
            });

            localStorage.setItem('star_rating_views', '0'); 
        }
    };

    const addStyles = function() {
        if (document.getElementById('star-rating-style')) return;
        const style = document.createElement('style');
        style.id = 'star-rating-style';
        style.textContent = `
            .script-list-ratings { 
                display: inline-block !important; 
                min-width: 130px; 
                vertical-align: middle; 
            }
            .rating-link {
                text-decoration: none !important;
                color: inherit !important;
                display: inline-flex !important;
                cursor: pointer !important;
                border: none !important;
                position: relative;
                z-index: 100;
            }
            .rating-box { 
                display: inline-flex; 
                align-items: center; 
                gap: 6px; 
                font-family: sans-serif;
            }
            .rating-num { 
                font-weight: bold; 
                color: #333; 
                font-size: 13px; 
            }
            .star-outer { 
                position: relative; 
                display: inline-block; 
                font-size: 16px; 
                color: #ddd; 
                letter-spacing: 1px;
            }
            .star-outer::before { content: '★★★★★'; }
            .star-inner { 
                position: absolute; 
                top: 0; left: 0; 
                white-space: nowrap; 
                overflow: hidden; 
                color: #f39c12; 
            }
            .star-inner::before { content: '★★★★★'; }
            .rating-total { 
                color: #888; 
                font-size: 12px; 
            }
            .rating-link:hover .rating-num { color: #f39c12; }
            .rating-link:hover .star-outer { filter: brightness(1.1); }
        `;
        document.head.appendChild(style);
    };

    const checkLinkBridge = function(container) {
        if (!window.location.hostname.includes('sleazyfork.org')) return;

        const scriptLink = container.querySelector('.script-link');
        if (!scriptLink || scriptLink.dataset.bridgeChecked) return;
        
        scriptLink.dataset.bridgeChecked = "true";

        GM_xmlhttpRequest({
            method: "HEAD",
            url: scriptLink.href,
            onload: function(response) {
                if (response.status === 404) {
                    const allLinks = container.querySelectorAll('a');
                    allLinks.forEach(a => {
                        const href = a.getAttribute('href');
                        if (href) {
                            if (href.startsWith('/')) {
                                a.href = "https://greatest.deepsurf.us" + href;
                            } else if (href.includes('sleazyfork.org')) {
                                a.href = href.replace('sleazyfork.org', 'greatest.deepsurf.us');
                            }
                        }
                    });
                }
            }
        });
    };

    const transformRatings = function() {
        const ratingContainers = document.querySelectorAll('.script-list-ratings');
        
        ratingContainers.forEach(function(el) {
            const parentLi = el.closest('li');
            
            if (parentLi) {
                checkLinkBridge(parentLi);
            }

            if (el.querySelector('.rating-box')) return;

            const goodEl = el.querySelector('.good-rating-count');
            const okEl = el.querySelector('.ok-rating-count');
            const badEl = el.querySelector('.bad-rating-count');

            if (!goodEl && !okEl && !badEl) return;

            incrementVisibility();

            let feedbackUrl = "";
            
            if (parentLi) {
                const allLinks = parentLi.querySelectorAll('a');
                for (let i = 0; i < allLinks.length; i++) {
                    const a = allLinks[i];
                    const match = a.href.match(/\/scripts\/(\d+)/);
                    if (match) {
                        const baseUrl = a.href.split('/scripts/')[0];
                        feedbackUrl = baseUrl + '/scripts/' + match[1] + '/feedback';
                        break; 
                    }
                }
            }

            if (!feedbackUrl) {
                const urlMatch = window.location.pathname.match(/\/scripts\/(\d+)/);
                if (urlMatch) {
                    const langPart = window.location.origin + window.location.pathname.split('/scripts/')[0];
                    feedbackUrl = langPart + '/scripts/' + urlMatch[1] + '/feedback';
                }
            }

            const good = parseInt(goodEl ? goodEl.textContent : 0) || 0;
            const ok = parseInt(okEl ? okEl.textContent : 0) || 0;
            const bad = parseInt(badEl ? badEl.textContent : 0) || 0;
            const total = good + ok + bad;

            const avg = total > 0 ? ((good * 5) + (ok * 3) + (bad * 1)) / total : 0;
            const percent = (avg / 5) * 100;

            if (!feedbackUrl) feedbackUrl = "#";

            el.innerHTML = '';
            const link = document.createElement('a');
            link.href = feedbackUrl;
            link.className = 'rating-link';
            link.title = 'Feedback (Avg: ' + avg.toFixed(2) + ')';
            link.innerHTML = `
                <div class="rating-box">
                    <span class="rating-num">${avg.toFixed(2)}</span>
                    <div class="star-outer">
                        <div class="star-inner" style="width: ${percent}%"></div>
                    </div>
                    <span class="rating-total">(${total.toLocaleString()})</span>
                </div>
            `;
            el.appendChild(link);
        });
    };

    addStyles();
    transformRatings();

    const observer = new MutationObserver(transformRatings);
    observer.observe(document.body, { childList: true, subtree: true });
})();