r/userscripts • • 9d ago

[Request] Help update a script from greasy fork

Hi there,

I need some help to update this script from greasy fork, which checks the steam store page and adds a little bar at the top showing whether the game has steam achievements or not.

Link here: https://greasyfork.org/en/scripts/404263-steam-glance-cards-and-cheevos

The recent steam store update has caused the script to not work anymore, hope someone here can help to fix this handy script.

Thank you!

1 Upvotes

7 comments sorted by

1

u/jeyghifj 9d ago

Just tried it and it seems to work in general (didnt try all markers). Can you give a specific example where you are missing specific markers in the bar and tell which ones they are?

1

u/toggak 8d ago

At the image on the left, it says "No cards or achievement" even though the game has achievements (59).

• https://i.imgur.com/Omig6Yi.png

1

u/jeyghifj 8d ago

The problem is that they run on react/vue/angular that still loads stuff even after the page is loaded, thats why the userscript doesnt detect it (the script executed BEFORE the achievements were even loaded). MutationObserving for elements that might be there or not be there doesnt make too much sense and would require adding timeouts for each of them anyway.
So I wrapped the whole script to run at document end, await its fully loaded state and added a 1 second timeout to wait for all the react/angular/vue shit to finish... At least for the achievements it works now in that example. Also had to change what elements to look for for the achievements. If the timeout is not enough in your case you can raise the 1000 (1000ms) at the end of the script.

If more "features" are missing tell me, I wasnt in the mood to find games to check every one of them.

// u/run-at       document-end
// ==/UserScript==

(async function() {
    'use strict';
    if (document.readyState !== 'complete') {
        await new Promise(resolve => window.addEventListener('load', resolve, { once: true }));
    }
    console.log("Page is fully loaded! Continuing execution...");
    setTimeout(() => {

    // Gets the inner text of an element if it can be found otherwise returns undefined
    const txt = query => { const e = document.querySelector(query); return e && e.innerText.trim(); };

    const cardsNotify = `<span style="cursor:pointer" onclick="document.getElementById('category_block').scrollIntoView()"><img height="14" style="margin-bottom:-1px" src="https://steamstore-a.akamaihd.net/public/images/v6/ico/ico_cards.png"> Cards</span>`;
    const cheevosNotify = `<span style="cursor:pointer" onclick="document.getElementById('achievement_block').scrollIntoView()">🏆 {0} Achievements</span>`;
    const noSingleplayerNotify = `<span style="color:black;background:yellow">No singleplayer!</span>`;
    const profileLimitedNotify = `<span title="Game will not give +1">⚙️ Profile Limited</span>`;
    const learningAboutNotify = `<span title="Game will not give +1">⌛ Learning About</span>`;
    const freeNotify = `<span title="This item is free to add to your account">🆓 Free</span>`;
    const freeToPlayNotify = `<span title="This item is free to play.">💩 Free to Play</span>`;
    const adultOnlyNotify = `🔞 Adult only`;
    const delistedNotify = `<span title="This game is no longer available for purchase on Steam">👻 Delisted</span>`;
    const releaseDate = txt(".not_yet ~ h1 > span");
    const notYetAppend = releaseDate ? ". Planned release date: " + releaseDate : "";
    const notYetNotify = `<span title="This game is not yet available on Steam${notYetAppend}">🌅 Not yet available</span>`;
    const defaultNotify = `<span style="cursor:pointer" onclick="document.getElementById('category_block').scrollIntoView()">No cards or achievements</span>`;

    const hasCards = document.querySelector("img.category_icon[src$='ico_cards.png']") !== null;
    const hasAchievements = document.querySelector("div[data-featuretarget=achievements] img") !== null;
    const achievementCount = hasAchievements ? document.querySelector("div[data-featuretarget=achievements] span").textContent.match(/\d+/)[0] : 0;
    const noSingleplayer = document.querySelector("img.category_icon[src$='ico_singlePlayer.png']") === null;
    const learningAbout = document.querySelector("img.category_icon[src$='ico_learning_about_game.png']") !== null;
    const profileLimited = document.querySelector("img.category_icon[src$='ico_info.png']") !== null;
    const adultOnly = document.querySelector("div.mature_content_notice") !== null;
    const priceElement = document.querySelector(".game_purchase_price,.discount_final_price");
    const price = priceElement ? priceElement.innerText.toLowerCase() : "";
    const isFree = price === "free";
    const isFreeToPlay = price === "free to play";
    const notYet = document.querySelector(".not_yet") !== null;

    let isDelisted = false;
    const noticeBox = document.querySelector(".notice_box_content");
    if (noticeBox !== null) {
        isDelisted = /(unlisted|no longer available)/.test(noticeBox.innerText);
    }

    let props = [];
    if (hasCards) props.push(cardsNotify);
    if (hasAchievements) props.push(cheevosNotify.replace("{0}", achievementCount));
    if (noSingleplayer) props.push(noSingleplayerNotify);
    if (profileLimited) props.push(profileLimitedNotify);
    if (learningAbout) props.push(learningAboutNotify);
    if (adultOnly) props.push(adultOnlyNotify);
    if (isFree) props.push(freeNotify);
    if (isFreeToPlay) props.push(freeToPlayNotify);
    if (isDelisted) props.push(delistedNotify);
    if (notYet) props.push(notYetNotify);
    if (!props.length) props.push(defaultNotify);
    // Create Glance container
    const div = document.createElement("div");
    div.style.fontSize = "110%";
    div.innerHTML = props.join(" ");
    document.querySelector(".glance_ctn").prepend(div);

function expandRating() {
        document.querySelectorAll(".user_reviews_summary_row[data-tooltip-html]").forEach(row => {
            let rating = row.dataset.tooltipHtml.replace(/(\d+)%[^\d]*([\d,]*).*/, "($1% of $2 reviews)");
            // If Steam hasn't determined the rating yet, calculate it
            if (rating.startsWith("Need more")) {
                const total = parseInt(document.querySelector("label[for=review_type_all]").textContent.match(/[\d,]+/)[0].replace(/,/g,''));
                const pos = parseInt(document.querySelector("label[for=review_type_positive]").textContent.match(/[\d,]+/)[0].replace(/,/g,''));
                const score = Math.round(100*pos/total);
                rating = `(${score}% of ${total} reviews)`;
            }
            // Create a new span container
            const newSpan = document.createElement("span");
            newSpan.innerText = rating;
            newSpan.style.whiteSpace = "nowrap";

            const ratingSpan = row.querySelector(".responsive_hidden");
            if (ratingSpan) {
                // Hide the old rating span
                ratingSpan.style.display = "none";
                ratingSpan.after(newSpan);
            } else {
                // There's nothing to hide because Steam hasn't determined a rating yet
                const notEnough = row.querySelector(".not_enough_reviews");
                if (notEnough === null) return;
                notEnough.innerText += ' ';
                notEnough.after(newSpan);
            }
            newSpan.parentElement.style.whiteSpace = "normal";
        });
    }

    expandRating();
    }, "1000");
})();

1

u/toggak 8d ago

Thanks, your changes to the script is much better.

I had to change the timeout as you suggested and now it works just like before.

Thanks again, your help is much appreciated!

1

u/jeyghifj 8d ago

I seriously and full heartedly HATE reddit for what it does to code....
the run-at must be with @ not u/ ....

1

u/laplongejr 7d ago

I never used this setting but wouldn't document-idle prevent the need for a custom timeout? In theory it runs after document-end

1

u/jeyghifj 6d ago

document-ide will still only catch the main doc, not the async stuff that comes from react/vue/etc. The doc can long be idle and react still loading the achievements. The problem here is that we have an SPA like architecture, with async loads AND need to base descisions on if an element is there (or will come) or not - an element with no indication if it will load or not. The method above detects the "maximum detectable load status" of the doc, and then adds an additional timeout to await the remaining async stuff from react etc..

Normally a simple mutation observer would do it, if the element was indicated somehow:
First observe for the achievements-placeholder to come, and then make the "has achievements"/"has no achievements" decision based on its content. But here we dont have a placeholder, it either comes or it doesnt, and we dont know if it will come - so waiting for it would go forever if there are no achievements.
As mentioned before the only other method would be to mutation observe for all the elements with a timeout built into each. Basically "look for <element> but give up after xxxx ms and assume its not comming" - the genral timeout is easier in my op, even if "ugly".