Floating Subs List

Adds a floating list of subscribed magazines to the left of the page

// ==UserScript==
// @name         Floating Subs List
// @namespace    http://tampermonkey.net/
// @version      0.8
// @description  Adds a floating list of subscribed magazines to the left of the page
// @author       raltsm4k
// @match        *://kbin.social/*
// @match        *://fedia.io/*
// @match        *://karab.in/*
// @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      MIT
// ==/UserScript==

let user;
let is_populating = false;
let fetch_tries = 3;

(function() {
    'use strict';

    const a_login = document.querySelector("#header a.login");
    user = a_login.getAttribute("href");

    if (user !== "/login") {
        const div_head = document.querySelector('#header');
        const div_subs = document.createElement('div');
        const div_subs_s = document.createElement('section');
        const div_subs_c = document.createElement('div');
        const button_filter = Object.assign(document.createElement("a"), {
            className: "icon sort-button",
            id: "button-filter",
            title: "Filter by name",
        }).appendChild(Object.assign(document.createElement("i"), {
            className: "fa-solid fa-magnifying-glass"
        })).parentNode;
        const button_refresh = Object.assign(document.createElement("a"), {
            className: "icon sort-button",
            id: "button-refresh",
            title: "Refresh list",
        }).appendChild(Object.assign(document.createElement("i"), {
            className: "fa-solid fa-rotate-right"
        })).parentNode;
        const input_filter = Object.assign(document.createElement("input"), {
            id: "subs-filter",
            name: "subs-filter",
            type: "text",
            title: "Filter by name",
            style: "display:none;"
        });
        const button_collapse = Object.assign(document.createElement("button"), {
            id: "subs-sticky-collapse",
            title: "Collapse/expand",
            textContent: "<"
        });

        const ul_main = Object.assign(document.createElement("ul"), {
            id: "subs-sticky-main"
        });
        const ul_pins = Object.assign(document.createElement("ul"), {
            id: "subs-sticky-pins"
        });

        const preloader = Object.assign(document.createElement("div"), {
            className: "fa-3x",
            id: "subs-sticky-preloader"
        }).appendChild(Object.assign(document.createElement("i"), {
            className: "fa-solid fa-sync fa-spin"
        })).parentNode.appendChild(Object.assign(document.createElement("i"), {
            className: "fa-solid fa-circle-exclamation",
            style: "display:none;"
        })).parentNode;


        button_refresh.addEventListener('click', () => { localStorage.removeItem('cached_subs_' + user); populate(); });
        button_filter.addEventListener('click', () => {
            const search_box = $('#subs-filter');
            search_box.toggle();
            if (search_box.is(':visible')) {
                $('#button-filter').addClass('active');
                search_box.focus();
            } else {
                $('#button-filter').removeClass('active');
                search_box.val('');
                filter();
            }
        });
        input_filter.addEventListener('input', function() { filter(); });
        button_collapse.addEventListener('click', async () => { await localStorage.setItem('subs_collapsed', localStorage.getItem('subs_collapsed') !== 'true'); collapse(); });

        const sub_button = document.querySelector('.magazine__subscribe button[data-action="subs#send"]');
        if (sub_button !== null) sub_button.addEventListener('click', () => { localStorage.removeItem('cached_subs_' + user); });

        $('<style>').text(`#subs-sticky { display: none; }
                 @media only screen and (min-width: 1136px) {
                 #middle > .kbin-container {
                     margin: 0 auto 0 max(calc(200px + 1rem), calc(50vw - 720px + 1rem));
                 }
                 #middle > .kbin-container.collapsed-subs {
                     margin: 0 auto;
                 }
                 #subs-sticky {
                    display: block;
                    position: fixed;
                    top: 3.1625rem;
                    left: max(.5rem, calc(50vw - 680px - 200px - 2rem));
                }
                .fixed-navbar #subs-sticky {
                    position: absolute;
                }
                #subs-sticky.collapsed-subs {
                    left: 0;
                }
                #subs-sticky.collapsed-subs .section {
                    width: 0px;
                    padding: 0;
                    border: none;
                }
                #subs-sticky-collapse {
                    cursor: pointer;
                    height: min(200px, 20%);
                    width: .5rem;
                    position: absolute;
                    top: calc(50% - min(200px, 20%));
                    left: 100%;
                    border: var(--kbin-button-primary-border);
                    border-left: none;
                    background: var(--kbin-button-primary-bg);
                    padding: 0;
                    color: var(--kbin-text-muted-color);
                }
                #subs-sticky-collapse:hover {
                    background: var(--kbin-button-primary-hover-bg);
                }
                a.sort-button {
                    position: relative;
                    float: right;
                    padding-left: .5rem;
                }
                a.sort-button:hover {
                    cursor: pointer;
                }
                a.sort-button:not(.active) {
                    opacity: 50%;
                }
                a#button-filter {
                    margin-right: -.25rem;
                    padding-right: .25rem;
                    border-right: var(--kbin-meta-border);
                }
                #subs-sticky a {
                     white-space: nowrap;
                     text-overflow: ellipsis;
                     max-width: 100%;
                     overflow-x: hidden;
                     overflow-y: hidden;
                     display: inherit;
                }
                #subs-sticky h3 {
                     border-bottom: var(--kbin-sidebar-header-border);
                     color: var(--kbin-sidebar-header-text-color);
                     font-size: .8rem;
                     margin: 0 0 .5rem;
                     text-transform: uppercase;
                }
                #subs-sticky .section {
                     padding: .5rem .5rem 1rem;
                     margin: 0;
                     width: 200px;
                     max-height: calc(100vh - 6.325rem);
                     overflow-y: auto;
                }
                .fixed-navbar #subs-sticky .section {
                     max-height: calc(100vh - 3.5rem);
                }
                #subs-sticky .section a {
                    color: var(--kbin-meta-link-color);
                }
                #subs-sticky .section a:hover {
                    color: var(--kbin-meta-link-hover-color);
                }
                #subs-sticky ul {
                    list-style-type: none;
                    padding: 0;
                    margin: 0;
                }
                #subs-sticky ul li small {
                    display: none;
                }
                #subs-sticky ul div {
                    position: relative;
                }
                #subs-sticky ul div .pin-button {
                    position: absolute;
                    top: -2px;
                    left: -2px;
                    cursor: pointer;
                    opacity: 0%;
                    text-shadow: 2px 2px 5px black;
                    z-index: 20;
                }
                a.icon.pin-button i {
                    font-size: small;
                }
                #subs-sticky ul div:hover .pin-button {
                    opacity: 25%;
                }
                #subs-sticky-pins .pin-button {
                    color: #bf5ded!important;
                    opacity: 100%!important;
                }
                #subs-sticky ul div .pin-button:hover {
                    opacity: 100%;
                }
                #subs-sticky-pins .pin-button:hover {
                    color: #df7dff!important;
                }
                #subs-sticky-pins {
                    display: none;
                    border-bottom: var(--kbin-meta-border);
                    margin-bottom: .25rem!important;
                }
                #subs-sticky figure {
                    display: inline-block;
                    vertical-align: middle;
                    margin: 0 4px 4px 0;
                    height: 24px;
                    width: 24px;
                    float: left;
                }
                #subs-sticky figure, #subs-sticky img {
                    border-radius: 100%;
                }
                #subs-sticky span.subs-comp-name {
                    display: block;
                }
                #subs-sticky span.subs-comp-name-mag {
                    margin-top: -.25rem;
                }
                #subs-sticky span.subs-comp-name-instance {
                    font-size: xx-small;
                    margin-top: -.125rem;
                    opacity: 80%;
                    color: #bf5ded;
                }
                #subs-sticky a:hover span.subs-comp-name-instance {
                    opacity: 100%;
                }
                #subs-filter {
                    display: block;
                    padding: .25rem;
                    margin-bottom: .5rem;
                }
                #subs-sticky .section::-webkit-scrollbar {
                    width: 8px;
                }
                #subs-sticky .section::-webkit-scrollbar-track {
                    background: var(--kbin-bg-nth);
                    border-left: var(--kbin-section-border);
                }
                #subs-sticky .section::-webkit-scrollbar-thumb {
                    background: var(--kbin-section-bg);
                    border-left: var(--kbin-section-border);
                }
                #subs-sticky .section::-webkit-scrollbar-thumb:hover {
                    background: var(--kbin-primary-color);
                }
                #subs-sticky .dummy {
                    display: none;
                }
                #subs-sticky-preloader {
                    height: 100px;
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    opacity: 50%;
                }
                }`).appendTo(document.head);

        div_subs_c.className = 'container';
        div_subs_c.appendChild(ul_pins);
        div_subs_c.appendChild(ul_main);

        div_subs_s.className = 'section';
        div_subs_s.appendChild(button_refresh);
        div_subs_s.appendChild(button_filter);
        div_subs_s.appendChild(Object.assign(document.createElement('h3'), {
            textContent: "Subscribed"
        }));
        div_subs_s.appendChild(input_filter);
        div_subs_s.appendChild(preloader);
        div_subs_s.appendChild(div_subs_c);
        div_subs.appendChild(div_subs_s);
        div_subs.appendChild(button_collapse);

        div_subs.setAttribute('id', 'subs-sticky');
        div_head.appendChild(div_subs);

        // Stop the panel before it collides with the footer when scrolling
        // Not happy with this yet, uncomment if you want to use it
        /*if ($('.fixed-navbar').length == 0) {
            $(window).scroll(function(){
                $('#subs-sticky').css('top', Math.min(rem_to_px(3.1625) + $(window).scrollTop(), $(document).outerHeight() - $('#footer').outerHeight() - $('#subs-sticky').outerHeight() - rem_to_px(3.5)));
            });
        } else {
            $(window).scroll(function(){
                $('#subs-sticky').css('top', Math.min(rem_to_px(3.1625), $(document).outerHeight() - $('#footer').outerHeight() - $('#subs-sticky').outerHeight() - $(window).scrollTop() - rem_to_px(3.5)));
            });
        }*/

        populate();
    }
})();

function populate() {
    if (!is_populating) {
        is_populating = true;
        const cached = JSON.parse(localStorage.getItem('cached_subs_' + user));
        if (cached === null || Date.now() >= cached.expire) { regen(); }
        else { set_preloader(true); $('#subs-sticky-main').html(cached.html); $('#subs-sticky-pins').html(''); console.log("Fetched from cache"); post_populate(); }
    }
}

function post_populate() {
    sort();
    collapse();
    assign_pins();
    set_preloader(false);
    is_populating = false;
}

function regen() {
    set_preloader(true);
    $('#subs-sticky li').remove();
    fetch_next(1);
}

function fetch_next(p) {
    $.ajax({
        url: window.location.origin + user + "/subscriptions?p=" + p,
        success: function(data) {
            const dom = $($.parseHTML(data));
            const ul = dom.find('#content .magazines ul');
            if (!ul.length) {
                if (fetch_tries > 0) {
                    console.log("Failed to get page " + p + " of subs, retrying...");
                    fetch_tries--;
                    fetch_next(p);
                } else {
                    console.log("Failed to get page " + p + " of subs. Out of tries, stopping...");
                }
            } else {
                const els = ul.children('li');
                els.each(function() {
                    const div = $(this).find('div');
                    const a = $(this).find('a');
                    const fig = $(this).find('figure');
                    $(this).find('small').remove();

                    const a_parts = a.text().split('@');
                    if (a_parts.length === 2) {
                        a.html("<span class='subs-comp-name subs-comp-name-mag'>" + a_parts[0] + "</span><span class='subs-comp-name subs-comp-name-instance'>@" + a_parts[1] + "</span>");
                    }

                    if (fig.length > 0) {
                        fig.find('img').css({'height': '24px', 'width': '24px'});
                        a.prepend(fig);
                    } else {
                        a.prepend(`<figure style="background-color: rgba(128, 128, 128, 0.5); text-align: center;">
                              <i class="fa-solid fa-newspaper" style="opacity:50%;"></i></figure>`);
                    }
                    a.removeClass();
                    div.prepend(`<a class="icon pin-button" title="Pin magazine" aria-label="Pin magazine"><i class="fa-solid fa-thumbtack"></i></a>`);
                    $('#subs-sticky-main').append($(this));
                });
                fetch_tries = 3;

                const pg = dom.find('#content .pagination__item--next-page:not(.pagination__item--disabled)');
                if (pg.length) {
                    fetch_next(p+1);
                } else {
                    localStorage.setItem('cached_subs_' + user, JSON.stringify({html: $('#subs-sticky-main').html(), expire: Date.now() + 15 * 60 * 1000}));
                    post_populate();
                }
            }
        },
        error: function() {
            post_populate();
            set_preloader(true, true);
        }
    });
}

function sort() {
    const ul_pins = $('#subs-sticky-pins');
    ul_pins.hide();
    ul_pins.children('li').appendTo($('#subs-sticky-main'));
    ul_pins.children('li').remove();

    const elems = $('#subs-sticky-main').children('li').detach().sort(function (a, b) {
        return ($(a).text().trim().toLowerCase() < $(b).text().trim().toLowerCase() ? -1
                : $(a).text().trim().toLowerCase() > $(b).text().trim().toLowerCase() ? 1 : 0);
    });
    $('#subs-sticky-main').append(elems);

    // Keep pins from versions < 0.8
    const legacy_pins = localStorage.getItem('pinned_subs');
    if (legacy_pins !== null) {
        localStorage.setItem('pinned_subs_' + user, legacy_pins)
        localStorage.removeItem('pinned_subs');
    }

    let pins = localStorage.getItem('pinned_subs_' + user);
    if (pins !== null && (pins = JSON.parse(pins)).length > 0) {
        $('#subs-sticky-main > li').each(function() {
            if (pins.includes($(this).find('a:nth-child(2)').attr('href'))) {
                $(this).appendTo($('#subs-sticky-pins'));
            }
        });
        ul_pins.show();
    }
    filter();
}

function filter() {
    const filter = $('#subs-filter').val();
    const elems = $('#subs-sticky li');
    elems.each(function() {
        $(this).show();
        if ($(this).text().trim().toLowerCase().indexOf(filter.trim().toLowerCase()) == -1) $(this).hide();
    });
}

function assign_pins() {
    $('.pin-button').on('click', function() {
        const href = $(this).parent().children('a').eq(1).attr('href');
        let pins = localStorage.getItem('pinned_subs_' + user);
        if (pins !== null && (pins = JSON.parse(pins)).length > 0) {
            if (pins.includes(href)) {
                pins.splice(pins.indexOf(href), 1);
            } else {
                pins.push(href);
            }
        } else {
            pins = [href];
        }
        localStorage.setItem('pinned_subs_' + user, JSON.stringify(pins));
        sort();
    });
}

function collapse() {
    const is_collapsed = localStorage.getItem('subs_collapsed');
    if (is_collapsed === 'true') {
        $('#middle .kbin-container').addClass('collapsed-subs');
        $('#subs-sticky').addClass('collapsed-subs');
        $('#subs-sticky-collapse').text('>');
    } else {
        $('#middle .kbin-container').removeClass('collapsed-subs');
        $('#subs-sticky').removeClass('collapsed-subs');
        $('#subs-sticky-collapse').text('<');
    }
}

function set_preloader(vis, error = false) {
    const pre = $('#subs-sticky-preloader');
    const cont = $('#subs-sticky .container');
    const i_spin = pre.find(".fa-spin");
    const i_err = pre.find(".fa-circle-exclamation");

    if (vis) {
        pre.show();
        cont.hide();
    } else {
        pre.hide();
        cont.show();
    }

    if (error) {
        i_err.show();
        i_spin.hide();
    } else {
        i_err.hide();
        i_spin.show();
    }
}

function rem_to_px(rem) {
    return parseFloat(rem) * parseFloat(getComputedStyle(document.documentElement).fontSize);
}