Improved Channel Select Menu for Kbin

Adds subscribed magazines and liked collections to the channel select menu.

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         Improved Channel Select Menu for Kbin
// @namespace    http://tampermonkey.net/
// @version      0.3.0
// @description  Adds subscribed magazines and liked collections to the channel select menu.
// @author       NeighborlyFedora
// @match        *://kbin.social/*
// @match        *://kbin.earth/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=kbin.social
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js
// @license      GPL-3.0-or-later
// ==/UserScript==

//Code partially based on Floating Subs List by raltsm4k (https://greasyfork.org/en/scripts/469121-floating-subs-list)



let user;
let cacheId;
let subs = [];
let clls = [];
let subsHtml = [];
let cllsHtml = [];
let settings = {
    cllsFirst: true,
    cacheEnabled: true,
    defaultIcons: true,
    fillOnChange: true
};
const SETTINGS_TEXT = {
    cllsFirst: "List liked collections before subscribed magazines.",
    cacheEnabled: "Cache menu items for faster loading.",
    defaultIcons: "Add placeholder icons to collections and iconless magazines.",
    fillOnChange: "Immediately reload menu when a magazine is subscribed to."
}
let settingsOpen = false;
let isFilling = false;
let fetchTries = 3;



main();
const observer = new MutationObserver( function(records){
    for(const record of records){
        if( Array.from(record.addedNodes).filter(node => node.nodeName == "BODY").length ){
            main();
        }
        if(settings["fillOnChange"]){
            const subButtons = Array.from(record.addedNodes).filter( node => node.classList !== undefined && node.classList.contains("magazine__subscribe") );
            if(subButtons.length){
                fill();
            };
        }
    }
});
observer.observe(document,{subtree: true, childList: true});



function main() {
    "use strict";

    user = document.querySelector("#header a.login").getAttribute("href");
    cacheId = "icsm_" + user;

    if(user === "/login") return;

    const channelList = document.querySelector("#header li:has(a[title='Select a channel']) .dropdown__menu");
    Object.assign(channelList, {
        id: "channel-list"
    });

    let clRefresh = document.querySelector("#cl-refresh")
    if(clRefresh === null){
        clRefresh = Object.assign(document.createElement("button"), {
            id: "cl-refresh",
            title: "Refresh list",
        });
        clRefresh.appendChild(Object.assign(document.createElement("i"), {
            className: "fa-solid fa-rotate"
        }));
    }
    
    channelList.prepend(clRefresh);
    clRefresh.addEventListener("click", fill);

    let clSettings = document.querySelector("#cl-settings")
    if(clSettings === null){
        clSettings = Object.assign(document.createElement("button"), {
            id: "cl-settings",
            title: "Settings",
        });
        clSettings.appendChild(Object.assign(document.createElement("i"), {
            className: "fa-solid fa-gear"
        }));
    }
    channelList.prepend(clSettings);

    clSettings.addEventListener("click", function(){
        if(settingsOpen){
            closeSettings();
        }else{
            openSettings();
        }
        settingsOpen = !settingsOpen;
    });

    $(document).on("keydown", function(event) {
        if(event.key == "Escape"){
            closeSettings();
        }
    });

    const icsmSettings = Object.assign(document.createElement("ul"), {
        id: "icsm-settings"
    });
    $(icsmSettings).hide();
    channelList.append(icsmSettings);

    icsmSettings.append(Object.assign(document.createElement("h3"), {
        textContent: "Settings"
    }));

    $("<style>").text(
        `
        #header #channel-list {
            scroll-behavior: auto;
            max-width: 60vw;
            width: 25rem;
            height: 25rem;
            overflow-x: hidden;
            overflow-y: scroll;
        }

        #header #channel-list h3 {
            border-bottom: var(--kbin-sidebar-header-border);
            color: var(--kbin-sidebar-header-text-color);
            font-size: .8rem;
            width: 95%;
            margin: 1rem 0 0 2.5%;
            text-transform: uppercase;
        }

        #cl-refresh {
            display: inline !important;
            position: absolute;
            background: none;
            border: 0;
            color: var(--kbin-meta-link-color);
            cursor: pointer;
            height: auto;
            width: auto;
            text-indent: 0;
            right: 2.5%;
            text-align: right;
            margin: 1rem 0 0 1rem;
            padding: 0 !important;
        }

        #cl-settings {
            display: inline !important;
            position: absolute;
            background: none;
            border: 0;
            color: var(--kbin-meta-link-color);
            cursor: pointer;
            height: auto;
            width: auto;
            text-indent: 0;
            right: calc(2.5% + 1.25rem);
            text-align: right;
            margin: 1rem 0 0 1rem;
            padding: 0 !important;
        }

        #cl-refresh:hover, #cl-settings:hover {
            color: var(--kbin-meta-link-hover-color);
        }

        #icsm-settings {
            padding: 0;
        }

        #icsm-settings li {
            padding: .35rem 1rem;
        }

        #icsm-settings input {
            min-width: 1.5rem;
            margin-right: .4rem;
        }

        #header #channel-list li {
            height: auto;
        }

        #header #channel-list #sub_item a:has(figure) {
            display: flex !important;
            justify-content: left;
        }

        #header #channel-list a {
            overflow-x: hidden;
            position: relative;
            padding: .35rem 1rem !important;
        }

        #header #channel-list #sub_item figure{
            float: left;
            margin-right: .25rem;
        }

        #header #channel-list #sub_item figure :is(img,i){
            width: 1.25rem;
            height: 1.25rem;
            vertical-align: middle;
        }

        #header #channel-list #sub_item figure i {
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
            top: .15rem;
        }

        #header #channel-list #sub_item figure i::before{
            vertical-align: middle;

        }

        #header #channel-list::-webkit-scrollbar {
            width: 8px;
        }

        .rounded-edges #header #channel-list::-webkit-scrollbar-track {
            border-top-right-radius: var(--kbin-rounded-edges-radius);
            border-bottom-right-radius: var(--kbin-rounded-edges-radius);
        }

        .rounded-edges #header #channel-list::-webkit-scrollbar-thumb  {
            border-radius: var(--kbin-rounded-edges-radius);
        }


        #header #channel-list::-webkit-scrollbar-track {
            background: var(--kbin-bg);
            border-left: var(--kbin-section-border);
        }


        #header #channel-list::-webkit-scrollbar-thumb {
            background: var(--kbin-meta-link-color);
            border-left: var(--kbin-section-border);
            transition-duration: 0.4s;
        }
        #header #channel-list::-webkit-scrollbar-thumb:hover {
            background: var(--kbin-meta-link-hover-color);
        }
        `
    ).appendTo(document.head);

    const settingsCache = JSON.parse(localStorage.getItem(cacheId + "_settings"));
    if(settingsCache !== null){
        Object.assign(settings, settingsCache);
    }
    for (const [key, val] of Object.entries(settings)) {
        if(SETTINGS_TEXT[key] === undefined) continue;
        const item = document.createElement("li");
        const checkbox = Object.assign(document.createElement("input"), {
            type: "checkbox",
            checked: val
        });
        item.append(checkbox);
        item.append(Object.assign(document.createElement("span"), {
            textContent: SETTINGS_TEXT[key]
        }));
        icsmSettings.append(item);
        checkbox.addEventListener("change", function(event){
            settings[key] = checkbox.checked;
            localStorage.setItem(cacheId + "_settings", JSON.stringify(settings));
        });
    }
    localStorage.setItem(cacheId + "_settings", JSON.stringify(settings));

    
    const cache = JSON.parse(localStorage.getItem(cacheId));
    if (!settings["cacheEnabled"] || cache === null || Date.now() >= cache.expire) {
        fill();
    } else if(!isFilling) {
        isFilling = true;
        empty();
        console.log("Fetching from cache....");
        cache.subs.forEach(function(cached_html){
           subs.push($(Object.assign(document.createElement("li"),{
               id: "sub_item"
           })).html(cached_html))
        });

        cache.clls.forEach(function(cached_html){
           clls.push($(Object.assign(document.createElement("li"),{
               id: "sub_item"
           })).html(cached_html))
        });
        complete();
    }

}

function fill(){
    if(isFilling) return;
    isFilling = true;
    $("#channel-list").children("li").show();
    $("#channel-list").children("h3").show();
    $("#icsm-settings").hide();
    localStorage.removeItem(cacheId);
    empty();
    $("#cl-refresh").find("i").addClass("fa-spin");
    fetchSubs(1);
}

function empty(){
    subs.forEach((sub) => { sub.remove();} );
    clls.forEach((cll) => { cll.remove();} );
    subs = [];
    subsHtml = [];
    clls = [];
    cllsHtml = [];
}

function fetchSubs(page){
    $.get( window.location.origin + "/settings/subscriptions/magazines?p=" + page, function(data) {
        const $dom = $($.parseHTML(data));
        const $subsList = $dom.find("#content .magazines ul");
        console.log("Fetching page " + page + " of subscriptions....");
        if (!$subsList.length) {
            if (fetchTries > 0) {
                console.log("Failed to fetch page " + page + " of subscriptions. Retrying....");
                fetchTries--;
                fetchSubs(page);
            } else {
                console.log("Failed to fetch page " + page + " of subscriptions. Out of attempts.");
                failSubs();
            }
        }else{
            fetchTries = 3;
            const $newSubs = $subsList.children("li");
            $newSubs.each(function() {

                $(this).prop("id", "sub_item");

                const $link = $(this).find("a");

                const $icon = $(this).find("figure");
                if($icon.length){
                    $link.prepend($icon);
                }

                $link.removeClass();
                if(window.location.href.includes("/m/"+$link.text())){
                   $link.addClass("active");
                }

                
                $(this).append($link);
                $(this).find("small").remove();
                $(this).find("div").remove();
                subs.push($(this));
                subsHtml.push($(this).html());

            });
            const $pg = $dom.find("#content .pagination__item--next-page.pagination__item--disabled");

            if ($pg.length) {
                fetchClls(1);
            } else {
                fetchSubs(page+1);
            }
        }
    }).fail(function() {
        console.log("Error occurred in reaching page " + page + " of subscriptions.");
        failSubs();
    });
}

function failSubs(){
    console.log("Skipping to collections.");
    fetchClls(1);
}

function fetchClls(page){
    $.get( window.location.origin + "/magazines/collections?p=" + page, function(data) {
        const $dom = $($.parseHTML(data));
        const $cllsList = $dom.find("#content .categories tbody");
        console.log("Fetching page " + page + " of collections....");
        if (!$cllsList.length) {
            if (fetchTries > 0) {
                console.log("Failed to fetch page " + page + " of collections. Retrying...");
                fetchTries--;
                fetchClls(page);
            } else {
                console.log("Failed to fetch page " + page + " of collections. Out of attempts.");
                failClls()
            }
        }else{
            fetchTries = 3;
            const $newClls = $cllsList.children("tr:has(button.active)");
            $newClls.each(function() {

                const $link = $(this).find("td:first-child a");

                $link.removeClass();
                const $li = $(document.createElement("li"));
                $li.prop("id", "sub_item");
                $li.append($link);
                clls.push($li);
                cllsHtml.push($li.html());

            });
            const $pg = $dom.find("#content .pagination__item--next-page.pagination__item--disabled");
            if ($pg.length) {
                if(settings["cacheEnabled"]){
                    cache();
                }
                complete();
            } else {
                fetchClls(page+1);
            }
        }
    }).fail(function() {
        console.log("Error occurred in reaching page " + page + " of collections.");
        failClls();
    });
}

function failClls(){
    console.log("Skipping to menu completion.");
    cache();
    complete();
}

function cache(){
    localStorage.setItem(cacheId, JSON.stringify({subs: subsHtml, clls: cllsHtml, expire: Date.now() + 15 * 60 * 1000}));
}


function complete(){

    console.log("Completing menu....");

    $("#cl-refresh").find("i").removeClass("fa-spin");

    $("#channel-list").children("h3").remove();

    $("#channel-list").prepend($(Object.assign(document.createElement("h3"), {
        id: "cl-header-feeds",
        textContent: "Feeds"
    })));

    $("#channel-list").prepend($("#cl-refresh"));
    $("#channel-list").prepend($("#cl-settings"));

    if(settings["cllsFirst"]){
        completeClls();
        completeSubs();
    }else{
        completeSubs();
        completeClls();
    }

    $("#channel-list").append($("#icsm-settings"));

    console.log("Done!");

    isFilling = false;
}


function completeSubs(){

    $("#channel-list").append($(Object.assign(document.createElement("h3"), {
        id: "cl-header-subs",
        textContent: "Subscribed Magazines"
    })));

    subs.sort(function(a, b){
        return a.find("a").text().trim().toLowerCase().localeCompare(b.find("a").text().trim().toLowerCase());
    });

    subs.forEach(function($sub) {
        $("#channel-list").append($sub);
        const $link = $sub.find("a")
        if(!$sub.find("figure img").length) {
            $sub.find("figure").remove();
            if(settings["defaultIcons"]){
                const icon = document.createElement("figure");
                icon.append(Object.assign(document.createElement("i"), {
                    className: "fa-solid fa-newspaper"
                }));
                $link.prepend($(icon));
            }
        }
        if( window.location.href.endsWith("/m/"+$link.text().trim()) || window.location.href.includes("/m/"+$link.text().trim()+"/") ){
            $link.addClass("active");
        }else{
            $link.removeClass("active");
        }
    })

}


function completeClls(){

    $("#channel-list").append($(Object.assign(document.createElement("h3"), {
        id: "cl-header-clls",
        textContent: "Liked Collections"
    })));

    clls.sort(function(a, b){
        return a.text().trim().toLowerCase().localeCompare(b.text().trim().toLowerCase());
    });

    clls.forEach(function($cll) {
        $("#channel-list").append($cll);
        const $link = $cll.find("a");
        $cll.find("figure").remove();
        if(settings["defaultIcons"]){
            const icon = document.createElement("figure");
            icon.append(Object.assign(document.createElement("i"), {
                className: "fa-solid fa-folder-open"
            }));
            $link.prepend($(icon));
        }
        if( window.location.href.includes("/c/"+$link.text().trim()) || window.location.href.includes("/c/"+$link.text().trim()+"/") ){
            $link.addClass("active");
        }else{
            $link.removeClass("active");
        }
    });
}


function openSettings(){
    if(isFilling) return;
    $("#channel-list").children("li").hide();
    $("#channel-list").children("h3").hide();
    $("#icsm-settings").show();
}


function closeSettings(){
    complete();
    $("#channel-list").children("li").show();
    $("#channel-list").children("h3").show();
    $("#icsm-settings").hide();
}