Mastodon Timeline Counter

Indicates the number of remaining posts on the timeline.

目前为 2023-06-15 提交的版本。查看 最新版本

// ==UserScript==
// @name         Mastodon Timeline Counter
// @version      1.1
// @description  Indicates the number of remaining posts on the timeline.
// @namespace    http://tampermonkey.net/
// @author       Bene Laszlo
// @match        https://mastodon.social/@*
// @match        https://mastodon.online/@*
// @match        https://mas.to/@*
// @icon         https://mastodon.social/packs/media/icons/favicon-16x16-c58fdef40ced38d582d5b8eed9d15c5a.png
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    var list, listContainerEl,
        boxEl, counterEl, textEl,
        count, lastCount=0,
        subCount, placeholdersJustCreated,
        result, lastResult,
        total,
        pack, packSize,
        scrollCount=0,
        lastPos,
        firstContentFound,
        first = true,
        theme;

    preInit();

    // wait for timeline to load before anything can start
    function preInit() {
        var t = setInterval(function() {
            var coll = document.getElementsByClassName('item-list');
            if (coll.length) {
                listContainerEl = coll[0];
                clearTimeout(t);
                init();
            }
        }, 200);
    }

    function init() {
        // set the colors according to the theme
        for (const name of ['default','contrast','mastodon-light']) {
            if (document.body.classList.contains('theme-'+name)) {
                theme = name;
                break;
            }
        }

        // source for total number of posts;
        var totalEl = document.getElementsByClassName('account__header__extra__links')[0].firstElementChild;
        result = total = totalEl.getAttribute('title').replace(',', '');

        // the counter element
        var navPanel = document.getElementsByClassName('navigation-panel')[0];
        var boxX = navPanel.getBoundingClientRect().left;
        //// counter container
        boxEl = document.createElement("div");
        boxEl.style.position = 'fixed';
        boxEl.style.left = (boxX+12)+'px';
        boxEl.style.bottom = '10px';
        boxEl.style.display = 'flex';
        boxEl.style.gap = '4px';
        boxEl.style.alignItems = 'flex-end';
        //// counter
        counterEl = document.createElement("div");
        counterEl.style.fontSize = '40px';
        counterEl.style.lineHeight = '.9em';
        counterEl.style.fontWeight = '500';
        //// additional text
        textEl = document.createElement("div");
        textEl.innerText = 'total';
        var textColor;
        switch (theme) {
            case 'default': textColor='#606984'; break;
            case 'contrast': textColor='#c2cede'; break;
            case 'mastodon-light': textColor='#444b5d'; break;
        }
        textEl.style.color = textColor;
        boxEl.append(counterEl);
        boxEl.append(textEl);

        // featured hashtags should move aside
        for (var el of document.getElementsByClassName('getting-started__trends')) {
            el.style.position = 'relative';
            el.style.top = '-70px';
            el.style.borderBottom = '1px solid #393f4f';
        }

        putContent(total);

        document.body.appendChild(boxEl);

        document.addEventListener('scroll', handleScroll);
    }

    // scroll event, the main function
    function handleScroll() {
        if (window.scrollY <= lastPos) return; // scolling up or not scrolling further down

        // update the counterEl1 (at every 12th scroll);
        if (scrollCount == 0)
        {
            placeholdersJustCreated = false;
            list = listContainerEl.getElementsByTagName('article');
            count = list.length;
            if (count != lastCount)
            {
                placeholdersJustCreated = true;

                // get the newly loaded post placeholders
                packSize = count-lastCount;
                //console.log(count+" "+lastCount+' → '+packSize);
                pack = Array.prototype.slice.call(list, -packSize);
                //console.log(pack[0].getAttribute('aria-posinset')+' "'+pack[0].style.overflow+'"');

                lastCount = count;
            }

            // there's only info about how many post PLACEHOLDERS are loaded
            // need to know how many of them are fully loaded
            if (!placeholdersJustCreated)
            {
                firstContentFound = false;
                for (var i in pack)
                {
                    var article = pack[i];
                    var isEmpty = article.style.overflow && article.style.overflow=='hidden';
                    if (!firstContentFound && isEmpty) continue; // posts scrolled past, which are ALREADY re-unloaded

                    firstContentFound = true;
                    if (isEmpty) break; // first post that is STILL unloaded
                }

                subCount = firstContentFound ? i : 0;
            }
            else {
                subCount = 0;
            }

            var realCount = count-(packSize-subCount);
            // console.log(subCount+' :: '+realCount);
            // console.log(count%20+" "+realCount+"=="+(count-1));
            if (count%20!=0 && realCount==count-1) {end(result); return;}

            result = total-realCount;
            if (result!=total) result++; // post is digested when the NEXT post is loaded and its top is already visible
            lastResult = result;

            // on first post scroll, alter the additional text
            if (first && subCount>1) {
                textEl.innerText = 'more to go';
                first = false;
            }
        }

        scrollCount ++;
        if (scrollCount==12) scrollCount = 0;
        lastPos = window.scrollY;

        putContent(result);
    }

    function putContent(n) {
        // displaying result while fixing kerning of number 1
        var resultStr = '';
        const digits = Array.from(n.toString());
        for (const [j, c] of digits.entries()) {
            resultStr += (c!=1 || j==digits.length-1) ? c : '<span>'+c+'</span>';
        }
        counterEl.innerHTML = resultStr;
        for (var el of counterEl.getElementsByTagName('span')) { // inline styling of <span> doesn't take effect FSR
            el.style.position = 'relative';
            el.style.left = '1px';
        }
    }

    // zeroing the counter when the timeline has ended
    // this userscript relies on the timeline header about the number of posts, which is never exact
    function end(n) {
        document.removeEventListener('scroll', handleScroll);
        var t = setInterval(function() {
            n--;
            putContent(n);
            if (n==0) clearTimeout(t);
        }, 60);
    }
})();