// ==UserScript==
// @name [Lemmy] Comment Line Styles
// @match *://*/*
// @exclude /^https:\/\/(?:(?:alex|old|photon|tess|voyager)\.lemmy\.ca|(?:a|m|old|photon)\.lemmy\.world)/
// @noframes
// @run-at document-idle
// @inject-into content
// @grant GM_addStyle
// @grant GM_getValues
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @namespace Violentmonkey Scripts
// @author SedapnyaTidur
// @version 1.0.0
// @license MIT
// @revision 8/18/2025, 8:00:27 PM
// @description Switch to several provided styles to customise the comment lines.
// ==/UserScript==
(function() {
'use strict';
if (!document.head.querySelector(':scope > meta[name="Description"][content="Lemmy"]')) return;
GM_addStyle(`
.custom-ml { margin-left: 0.5rem; }
.custom-mt { margin-top: 0.75rem; }
.custom-p { padding: 0.25rem 0.5rem 0.25rem 0.5rem; }
.custom-pb { padding-bottom: 0.5rem; }
.custom-pbrt { padding: 0.25rem 0.5rem 0.25rem 0px; }
.custom-plr { padding: 0px 0.5rem; }
.custom-pt { padding-top: 0.5rem; }`);
const Styles = [{
name: 'Original',
// Must have at least 1 style. That means the styles.length must be at least 1.
// Can have more than 7 for any styles. That means styles can be: styles.length > 7.
styles: [
'border-left: 2px solid rgba(172, 83, 83, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba(172, 157, 83, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba(113, 172, 83, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba( 83, 172, 128, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba( 83, 142, 172, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba( 98, 83, 172, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba(172, 83, 172, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;']
}, {
name: 'Bright Original',
styles: [
'border-left: 2px solid rgba(172, 83, 83, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba(172, 157, 83, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba(113, 172, 83, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba( 83, 172, 128, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba( 83, 142, 172, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba( 98, 83, 172, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;',
'border-left: 2px solid rgba(172, 83, 172, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.25rem !important;']
}, {
name: 'Gapped Original',
styles: [
'border-left: 2px solid rgba(172, 83, 83, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba(172, 157, 83, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba(113, 172, 83, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba( 83, 172, 128, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba( 83, 142, 172, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba( 98, 83, 172, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba(172, 83, 172, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;']
}, {
name: 'Bright & Gapped Original',
styles: [
'border-left: 2px solid rgba(172, 83, 83, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba(172, 157, 83, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba(113, 172, 83, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba( 83, 172, 128, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba( 83, 142, 172, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba( 98, 83, 172, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba(172, 83, 172, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;']
}, {
name: 'Blue & Yellow',
styles: [
'border-left: 2px solid rgba( 83, 142, 172, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba(172, 157, 83, 0.5) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;']
}, {
name: 'Bright Blue & Yellow',
styles: [
'border-left: 2px solid rgba( 83, 142, 172, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;',
'border-left: 2px solid rgba(172, 157, 83, 0.8) !important; color: rgb(200, 200, 200) !important; margin-left: 0.5rem !important;']
}, {
name: 'Stacked Darks',
styles: [
'background-color: rgb(34, 34, 34) !important; color: rgb(200, 200, 200) !important; border-color: rgb(94, 94, 94) !important; border-width: 2px 0px 0px 2px !important; border-style: solid !important; border-radius: 0.5rem !important; margin-left: 0.35rem !important;',
'background-color: rgb(54, 54, 54) !important; color: rgb(200, 200, 200) !important; border-color: rgb(94, 94, 94) !important; border-width: 2px 0px 0px 2px !important; border-style: solid !important; border-radius: 0.5rem !important; margin-left: 0.35rem !important;']
}];
const StyleTypes = [1, 2, 3, 4]; // Use emojis?
//const window = unsafeWindow;
const target = ':scope > div#root > div > main > div > div > div > div > :last-child:not([class])';
const waitTimeout = 10000; // 10 seconds.
let attrObserver, childObserver, searchInterval = 0, searchTimeout = 0;
let { type, style } = GM_getValues({ type: StyleTypes[0], style: Styles[0].name });
const getStyles = function(name) {
for (const object of Styles) {
if (object.name === name) return object.styles;
}
return Styles[0].styles;
};
const color4 = function(node, recurseCount = 0, styles = getStyles(style)) {
if (recurseCount === 0 && attrObserver) { attrObserver.disconnect(); attrObserver = undefined; }
node.removeAttribute('style');
node.classList.remove('border-top', 'ms-1');
const children = node.children;
const len = children.length;
for (let i = 0; i < len; ++i) {
const li = children[i];
if (recurseCount === 0) {
li.classList.add('custom-mt');
} else {
li.classList.add('custom-ml');
}
for (const element of li.children) {
const tagName = element.tagName.toLowerCase();
if (tagName === 'ul') {
color4(element, recurseCount + 1, styles);
} else if (tagName === 'article') {
element.classList.remove('border-top', 'mark', 'py-2');
element.classList.add('custom-plr');
element.style = styles[recurseCount % styles.length];
if (recurseCount === 0 && i == 0 && element.style.marginLeft) {
node.style.setProperty('margin-left', `-${element.style.marginLeft}`, 'important');
attrObserver = new MutationObserver(() => color4(node, 0, styles));
attrObserver.observe(li, { attributes: true });
}
const parent = element.firstChild;
parent.classList.remove('ms-2');
const secChild = parent.querySelector(':scope > :nth-child(2)');
secChild.querySelector(':scope > :first-child > :last-child')?.style.setProperty('margin-bottom', '0px', 'important');
const lastChild = parent.lastChild;
lastChild.classList.remove('mt-1');
if (lastChild.childElementCount === 0) {
secChild.classList.add('custom-pb');
lastChild.style.setProperty('display', 'none', 'important');
}
} else if (tagName === 'div' && element.classList.contains('details')) { //N more replies.
element.classList.remove('ms-1');
//element.classList.add('custom-ml');
element.style = styles[(recurseCount + 1) % styles.length];
}
}
}
};
// node must be a ul element (HTMLUListElement).
const color3 = function(node, recurseCount = 0, styles = getStyles(style)) {
// Disconnect if called multiple times by childObserver and disconnect for recursion called by attrObserver;
if (recurseCount === 0 && attrObserver) { attrObserver.disconnect(); attrObserver = undefined; }
node.classList.remove('border-top', 'ms-1'); // margin-left: 0.25rem
// Run in order because Lemmy may override our styles. forEach() is not an option.
for (const li of node.children) {
//const liChildCount = li.childElementCount;
for (const element of li.children) {
const tagName = element.tagName.toLowerCase();
if (tagName === 'ul') {
element.style = styles[recurseCount % styles.length];
color3(element, recurseCount + 1, styles);
} else if (tagName === 'article') {
element.classList.remove('border-top', 'mark', 'py-2'); // padding-top: 0.5rem; padding-bottom: 0.5rem
//element.classList.add('custom-pt');
const parent = element.firstChild;
parent.classList.remove('ms-2'); // margin-left: 0.5rem
if (recurseCount === 0 && !attrObserver) { // Styles may get overriden by Lemmy.
attrObserver = new MutationObserver(() => color3(node, 0, styles));
attrObserver.observe(parent, { attributes: true });
}
parent.firstChild.classList.add('custom-pbrt');
const secChild = parent.querySelector(':scope > :nth-child(2)');
secChild.classList.add('custom-p');
secChild.style = styles[recurseCount % styles.length];
secChild.querySelector(':scope > :first-child > :last-child')?.style.setProperty('margin-bottom', '0px', 'important');
const lastChild = parent.lastChild;
lastChild.classList.remove('mt-1'); // margin-top: 0.25rem
lastChild.style = styles[recurseCount % styles.length];
if (lastChild.childElementCount === 0) lastChild.style.setProperty('display', 'none', 'important');
} else if (tagName === 'div' && element.classList.contains('details')) { //N more replies.
element.classList.remove('ms-1');
element.style = styles[recurseCount % styles.length];
}
}
}
};
const color2 = function(node, recurseCount = 0, styles = getStyles(style)) {
if (recurseCount === 0 && attrObserver) { attrObserver.disconnect(); attrObserver = undefined; }
node.removeAttribute('style');
node.classList.remove('border-top', 'ms-1');
const children = node.children;
const len = children.length;
for (let i = 0; i < len; ++i) {
const li = children[i];
const liChildCount = li.childElementCount;
li.classList.add('custom-mt');
li.style = styles[recurseCount % styles.length];
if (recurseCount === 0 && i == 0 && li.style.marginLeft) {
node.style.setProperty('margin-left', `-${li.style.marginLeft}`, 'important');
attrObserver = new MutationObserver(() => color2(node, 0, styles));
attrObserver.observe(li, { attributes: true });
}
for (const element of li.children) {
const tagName = element.tagName.toLowerCase();
if (tagName === 'ul') {
color2(element, recurseCount + 1, styles);
} else if (tagName === 'article') {
element.classList.remove('border-top', 'mark', 'py-2');
element.classList.add('custom-plr');
const parent = element.firstChild;
parent.classList.remove('ms-2');
const secChild = parent.querySelector(':scope > :nth-child(2)');
secChild.querySelector(':scope > :first-child > :last-child')?.style.setProperty('margin-bottom', '0px', 'important');
const lastChild = parent.lastChild;
lastChild.classList.remove('mt-1');
if (lastChild.childElementCount === 0) {
if (liChildCount === 1) secChild.classList.add('custom-pb');
lastChild.style.setProperty('display', 'none', 'important');
}
} else if (tagName === 'div' && element.classList.contains('details')) { //N more replies.
element.classList.remove('ms-1');
element.classList.add('custom-mt');
element.style = styles[(recurseCount + 1) % styles.length];
}
}
}
};
const color1 = function(node, recurseCount = 0, styles = getStyles(style)) {
if (recurseCount === 0 && attrObserver) { attrObserver.disconnect(); attrObserver = undefined; }
node.classList.remove('border-top', 'ms-1');
if (recurseCount === 0) node.style.setProperty('margin-left', '-0.5rem', 'important');
for (const li of node.children) {
for (const element of li.children) {
const tagName = element.tagName.toLowerCase();
if (tagName === 'ul') {
element.style = styles[recurseCount % styles.length];
if (recurseCount === 0 && !attrObserver) {
attrObserver = new MutationObserver(() => color1(node, 0, styles));
attrObserver.observe(node, { attributes: true });
}
color1(element, recurseCount + 1, styles);
} else if (tagName === 'article') {
element.classList.remove('border-top', 'mark', 'py-2');
element.classList.add('custom-plr');
const parent = element.firstChild;
parent.classList.remove('ms-2');
const secChild = parent.querySelector(':scope > :nth-child(2)');
secChild.querySelector(':scope > :first-child > :last-child')?.style.setProperty('margin-bottom', '0px', 'important');
const lastChild = parent.lastChild;
lastChild.classList.remove('mt-1');
if (lastChild.childElementCount === 0) {
secChild.classList.add('custom-pb');
lastChild.style.setProperty('display', 'none', 'important');
}
} else if (tagName === 'div' && element.classList.contains('details')) { //N more replies.
element.classList.remove('ms-1');
element.style = styles[recurseCount % styles.length];
}
}
}
};
const color = function(node) {
switch(type) {
case StyleTypes[0]: color1(node);
break;
case StyleTypes[1]: color2(node);
break;
case StyleTypes[2]: color3(node);
break;
case StyleTypes[3]: color4(node);
break;
}
};
const reset = function() {
clearInterval(searchInterval);
clearTimeout(searchTimeout);
searchInterval = 0;
searchTimeout = 0;
if (childObserver) {
childObserver.disconnect();
childObserver = undefined;
}
if (attrObserver) {
attrObserver.disconnect();
attrObserver = undefined;
}
};
const run = function() {
searchTimeout = setTimeout(() => {
clearInterval(searchInterval);
}, waitTimeout);
searchInterval = setInterval(() => {
if (!document.body) return;
const targetParent = document.body.querySelector(target);
if (!targetParent) return;
clearInterval(searchInterval);
clearTimeout(searchTimeout);
if (targetParent.childElementCount === 0) return;
childObserver = new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLUListElement || node instanceof HTMLLIElement) {
color(targetParent.lastChild);
}
}
}
});
childObserver.observe(targetParent, { childList: true, subtree: true });
color(targetParent.lastChild);
}, 100);
};
new MutationObserver(mutations => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLLinkElement && node.rel === 'canonical') {
reset();
if (/^https:\/\/[^/]+\/(?:post|m\/[^/]+\/t)\/[0-9]/.test(node.href)) run();
return;
}
}
}
}).observe(document.head, { childList: true });
// First visit or reload the page.
if (/^\/(?:comment|post)\//.test(window.location.pathname)) run();
// Edit the style's name?
if (!Styles.some(object => object.name === style)) {
style = Styles[0].name;
GM_setValue('style', style);
}
const nextStyleType = function() {
type = StyleTypes[(StyleTypes.indexOf(type) + 1) % StyleTypes.length];
GM_setValue('type', type);
GM_registerMenuCommand(`Type:《${type}》`, nextStyleType, { id: '0', autoClose: false, title: "Click to change the comments' style." });
};
const nextStyle = function() {
for (let i=0; i<Styles.length; ++i) {
if (Styles[i].name === style) {
style = Styles[(i + 1) % Styles.length].name;
break;
}
}
GM_setValue('style', style);
GM_registerMenuCommand(`Style:《${style}》`, nextStyle, { id: '1', autoClose: false, title: "Click to change the comments' style." });
};
GM_registerMenuCommand(`Type:《${type}》`, nextStyleType, { id: '0', autoClose: false, title: "Click to change the comments' style." });
GM_registerMenuCommand(`Style:《${style}》`, nextStyle, { id: '1', autoClose: false, title: "Click to change the comments' style." });
})();