// ==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();
}