// ==UserScript== // @name Kbin Usability Pack // @namespace https://perry.dev // @license MIT // @version 0.1.0 // @description A collection of usability enhancements for Kbin // @author Daniel Pervan // @match https://kbin.social/* // @icon https://www.google.com/s2/favicons?sz=64&domain=kbin.social // @downloadURL none // ==/UserScript== (function () { 'use strict'; (() => { // src/Classes/User/User.js var User = class { username; avatar; constructor(username, avatar = null) { this.username = username; this.avatar = avatar; } }; var User_default = User; // style-helper:index.js function inject_style(text) { if (typeof document !== "undefined") { var style = document.createElement("style"); var node = document.createTextNode(text); style.appendChild(node); document.head.appendChild(style); } } // src/Classes/Article/Article.scss inject_style('#content article.entry aside.meta.entry__meta{line-height:3.5em}#content article.entry aside.meta.entry__meta img{margin-right:.25em;margin-left:.25em}#content article.entry .magazine-inline{white-space:nowrap}#content article.entry.no-image{grid-template-areas:"vote title" "vote shortDesc" "vote meta" "vote footer" "body body" "preview preview"}#content article.entry:not(.no-image){grid-template-areas:"vote image title" "vote image shortDesc" "vote image meta" "vote image footer" "body body body" "preview preview preview"!important;grid-template-columns:min-content min-content auto}#content article.entry.selected{border:var(--kbin-meta-border)}#content article.entry>figure{margin:0 1em 0 0;place-self:center}#content article.entry>figure img{max-width:100px;max-height:100px}#content article.entry aside.vote{place-content:center}#content article.entry button.show-preview,#content article.entry button.preview-button{border:1px solid var(--kbin-section-text-color);padding:.5em;background:var(--kbin-bg)}@media (max-width: 991.98px){#content article.entry:not(.no-image){grid-template-areas:"vote image title" "vote image shortDesc" "vote image meta" "footer footer footer" "body body body" "preview preview preview"!important}}@media (max-width: 689.98px){#content article.entry:not(.no-image){grid-template-areas:"image image" "vote title" "vote shortDesc" "vote meta" "vote footer" "body body" "preview preview"!important;grid-template-columns:min-content auto}#content article.entry.no-image{grid-template-areas:"vote title" "vote shortDesc" "vote meta" "vote footer" "body body" "preview preview"!important;grid-template-columns:min-content auto}#content article.entry>figure{place-self:center;width:100%;margin:0}#content article.entry>figure img{max-width:100%;width:100%;object-fit:cover}#content article.entry aside.vote{place-self:center}}.preview-outer{grid-area:preview}.preview-outer .preview-inner{position:relative;display:none;border-top:var(--kbin-meta-border);padding:1em}.preview-outer .preview-inner.show{display:block}.preview-outer .preview-inner .loading{position:relative;height:3em;z-index:1;display:flex;justify-content:center;align-items:center;animation:showPreviewLoading .25s ease-in-out}.preview-outer .preview-inner .article-preview-content{position:relative}.preview-outer .preview-inner .article-preview-content.loaded{animation:articlePreviewFadeIn .5s ease-in-out}.preview-outer .preview-inner .media-preview-content{position:relative}.preview-outer .preview-inner .media-preview-content img{max-width:100%;max-height:100%;min-width:100px;object-fit:contain}.preview-outer .preview-inner .media-preview-content img.animateMinResize{animation:animateMinResize .25s ease-out;transform-origin:left top}@keyframes animateMinResize{0%{opacity:1;transform:scale(1)}33%{opacity:.8;transform:scale(.95)}66%{opacity:1;transform:scale(1.01)}to{opacity:1;transform:scale(1)}}.preview-outer .preview-inner .media-preview-content.oembed-embed{position:relative;overflow:hidden;max-width:100%;height:100vh;margin:0 auto}.preview-outer .preview-inner .media-preview-content.oembed-embed iframe{position:absolute;top:0;left:0;overflow:hidden;width:100%;height:100%;border:none}.preview-outer .preview-inner .media-preview-content.youtube-embed{position:relative;overflow:hidden;max-width:100%;max-height:100vh;margin:0 auto;aspect-ratio:16/9}.preview-outer .preview-inner .media-preview-content.youtube-embed iframe{position:absolute;top:0;left:0;width:100%;height:100%;border:none}.preview-outer .preview-inner .media-preview-content.loaded{animation:articlePreviewFadeIn .5s ease-in-out}@keyframes showPreviewLoading{0%{opacity:0;transform:scale(.8)}50%{opacity:1;transform:scale(1.15)}80%{transform:scale(.9)}to{transform:scale(1)}}@keyframes articlePreviewFadeIn{0%{opacity:0;transform:translateY(-.5em)}to{opacity:1}}'); // src/Classes/Article/Article.js var Article = class _Article { feedElement = null; subject = null; author = null; date = null; articleUrl = null; mediaUrl = null; magazine = null; linkUrl = null; hasMedia = false; articlePreviewOpen = false; mediaPreviewOpen = false; TYPES = { ARTICLE: "article", LINK: "link", IMAGE: "image" }; #content; #articleLoaded; articlePageElement; enableArticlePreview = true; constructor() { } static fromFeedElement(element) { let article = new _Article(); article.feedElement = element; article.subject = element.querySelector("header h2").innerText; article.author = new User_default(element.querySelector(".meta .user-inline").innerText, element.querySelector(".meta .user-inline img")?.src); article.date = new Date(element.querySelector(".meta.entry__meta time")?.innerText); article.articleUrl = element.querySelector("header h2 a")?.href; article.thumbUrl = element.querySelector("figure a img")?.src; article.mediaUrl = element.querySelector("button.show-preview")?.dataset?.previewUrlParam; article.magazine = element.querySelector(".meta.entry__meta .magazine-inline")?.innerText; article.shortDescription = element.querySelector(".content.short-desc")?.innerText; const upvoteElement = element.querySelector("aside.vote .vote__up"); const downvoteElement = element.querySelector("aside.vote .vote__down"); article.upvotes = parseInt(upvoteElement?.querySelector("span")?.innerText) || 0; article.downvotes = parseInt(downvoteElement?.querySelector("span")?.innerText) || 0; if (article.feedElement.querySelector("li.meta-link")) { article.type = article.TYPES.ARTICLE; } else if (article.feedElement.querySelector("li>button.show-preview")) { article.type = article.TYPES.IMAGE; } else { article.type = article.TYPES.LINK; } return article; } upvote() { const element = this.feedElement ?? this.articlePageElement; if (!element) { return; } const voteElement = element.querySelector("aside.vote .vote__up button"); if (voteElement) { voteElement.click(); this.upvotes++; } } downvote() { const element = this.feedElement ?? this.articlePageElement; if (!element) { return; } const voteElement = element.querySelector("aside.vote .vote__down button"); if (voteElement) { voteElement.click(); this.upvotes--; } } boost() { const element = this.feedElement ?? this.articlePageElement; if (!element) { return; } const boostElement = element.querySelector("footer menu li form button"); if (boostElement && boostElement.dataset.action === "subject#favourite") { boostElement.click(); } } loadArticle() { if (this.#articleLoaded) { return Promise.resolve(this); } const fetchOptions = { method: "GET", headers: { "Accept": "text/html" } }; return fetch(this.articleUrl, fetchOptions).then((response) => { return response.text(); }).then((text) => { const parser = new DOMParser(); const htmlDocument = parser.parseFromString(text, "text/html"); const contentElement = htmlDocument.querySelector("article .entry__body .content"); this.#content = contentElement?.innerHTML ?? null; const linkElement = htmlDocument.querySelector("article header h1 a"); if (linkElement) { this.linkUrl = linkElement.href; } else { this.linkUrl = null; } this.#articleLoaded = true; return this; }); } static fromArticlePage(articleElement) { let article = new _Article(); article.subject = articleElement.querySelector("header h1")?.childNodes[0]?.textContent?.trim(); article.author = new User_default(articleElement.querySelector(".meta .user-inline").innerText, articleElement.querySelector(".meta .user-inline img")?.src); article.date = new Date(articleElement.querySelector(".meta.entry__meta time")?.innerText); article.linkUrl = articleElement.querySelector("header h1>a")?.href; article.thumbUrl = articleElement.querySelector("figure a img")?.src; article.mediaUrl = articleElement.querySelector("button.show-preview")?.dataset?.previewUrlParam; article.magazine = articleElement.querySelector(".meta.entry__meta .magazine-inline")?.innerText; article.#content = articleElement.querySelector(".entry__body .content")?.innerHTML ?? null; const upvoteElement = articleElement.querySelector("aside.vote .vote__up"); const downvoteElement = articleElement.querySelector("aside.vote .vote__down"); article.upvotes = parseInt(upvoteElement?.querySelector("span")?.innerText) || 0; article.downvotes = parseInt(downvoteElement?.querySelector("span")?.innerText) || 0; article.enableArticlePreview = false; article.#articleLoaded = true; article.articlePageElement = articleElement; if (articleElement.querySelector("li.meta-link")) { article.type = article.TYPES.ARTICLE; } else if (articleElement.querySelector("li>button.show-preview")) { article.type = article.TYPES.IMAGE; } else { article.type = article.TYPES.LINK; } return article; } getContent() { if (!this.shortDescription) { this.#content = null; return Promise.resolve(this.#content); } return this.loadArticle().then(() => { return this.#content; }); } hasContent() { return !!this.shortDescription || !!this.#content; } enrichFeedElement() { if (!this.feedElement) { return; } const footer = this.feedElement.querySelector("footer"); const footerMenu = footer.querySelector("menu"); const previewOuter = Object.assign(document.createElement("div"), { className: "preview-outer" }); this.feedElement.append(previewOuter); const thumbnail = this.feedElement.querySelector("figure a img"); if (thumbnail) { thumbnail.style.objectFit = null; } this.replaceMediaPreview(this.feedElement); if (this.hasContent()) { if (this.type === this.TYPES.ARTICLE) { footer.querySelector("li.meta-link i")?.remove(); } const previewButton = document.createElement("button"); previewButton.classList.add("show-article-preview", "preview-button"); previewButton.innerHTML = ''; previewButton.addEventListener("click", (event) => { event.preventDefault(); this.toggleArticlePreview(); }); let metaLinkElement = footer.querySelector("li.meta-link"); if (!metaLinkElement) { metaLinkElement = document.createElement("li"); metaLinkElement.classList.add("meta-link"); footerMenu.insertBefore(metaLinkElement, footerMenu.firstChild); } metaLinkElement.append(previewButton); let previewElement = this.feedElement.querySelector(".article-preview"); let previewContentElement; if (!previewElement) { previewElement = document.createElement("div"); previewElement.classList.add("article-preview", "preview-inner"); previewContentElement = document.createElement("div"); previewContentElement.classList.add("article-preview-content", "content"); previewElement.append(previewContentElement); previewOuter.append(previewElement); } } const commentsLinkElements = footer.querySelectorAll("menu li > a.stretched-link"); commentsLinkElements.forEach((commentsLinkElement) => { if (commentsLinkElement && commentsLinkElement.href.endsWith("#comments")) { const url = new URL(commentsLinkElement.href); commentsLinkElement.href = url.pathname; } }); } replaceMediaPreview(parent) { const footer = parent.querySelector("footer"); const mediaPreviewButton = footer.querySelector("button.show-preview"); const previewOuter = parent.querySelector(".preview-outer"); const footerMenu = footer.querySelector("menu"); if (mediaPreviewButton && mediaPreviewButton.dataset.action === "preview#show") { this.hasMedia = true; mediaPreviewButton.remove(); const newMediaPreviewButton = document.createElement("button"); newMediaPreviewButton.classList.add("show-media-preview", "preview-button"); newMediaPreviewButton.innerHTML = ''; newMediaPreviewButton.addEventListener("click", (event) => { event.preventDefault(); this.toggleMediaPreview(); }); const li = document.createElement("li"); li.append(newMediaPreviewButton); footer.querySelector("menu").insertBefore(li, footerMenu.firstChild); let previewElement = parent.querySelector(".media-preview"); if (!previewElement) { previewElement = document.createElement("div"); previewElement.classList.add("media-preview", "preview-inner"); const previewContentElement = document.createElement("div"); previewContentElement.classList.add("media-preview-content"); previewElement.append(previewContentElement); previewOuter.append(previewElement); } } } enrichArticlePage() { if (!this.articlePageElement) { return; } const previewOuter = Object.assign(document.createElement("div"), { className: "preview-outer" }); this.articlePageElement.append(previewOuter); this.replaceMediaPreview(this.articlePageElement); } showArticlePreview() { if (!this.feedElement || !this.type === this.TYPES.ARTICLE || !this.hasContent()) { return; } this.articlePreviewOpen = true; let previewElement = this.feedElement.querySelector(".article-preview"); let previewContentElement = previewElement.querySelector(".article-preview-content"); previewElement.classList.add("show"); previewContentElement.innerHTML = '