Adds search and settings buttons and modifies some UIs.
// ==UserScript==
// @name [kddit] Search Button & Settings Button & UI Modifications
// @match https://kddit.kalli.st/*
// @noframes
// @run-at document-body
// @inject-into content
// @grant GM.deleteValue
// @grant GM.deleteValues
// @grant GM.getValue
// @grant GM.getValues
// @grant GM.setValues
// @namespace Violentmonkey Scripts
// @author SedapnyaTidur
// @version 1.0.3
// @license MIT
// @revision 5/8/2026, 5:22:22 PM
// @description Adds search and settings buttons and modifies some UIs.
// ==/UserScript==
(async function() {
'use strict';
// https://gt.kalli.st/czar/kddit.kalli.st
// https://github.com/kzarist/kddit
const defaultSettings = {
autoplay_videos: false,
bottom_gap: '0px',
comment_scroll: true,
external_newtab: true,
font_scale: 1,
nsfw_contents: true,
preload_images: false,
preload_videos: false,
remember_search: false,
search_input: undefined,
video_quality: 1080,
comment_sort: 'confidence',
post_sort: 'hot',
user_sort: 'new',
search_sort: 'relevance',
author_search_sort: 'relevance',
flair_search_sort: 'relevance',
self_search_sort: 'relevance',
selftext_search_sort: 'relevance',
site_search_sort: 'relevance',
subreddit_search_sort: 'relevance',
title_search_sort: 'relevance',
url_search_sort: 'relevance'
};
const settings = await GM.getValues(defaultSettings);
const style = document.createElement('style');
style.textContent = `
:root {
--scale: ${settings.font_scale};
--bottom-gap: ${settings.bottom_gap};
--font: normal;
--author: #00AACC;
--border: #555555;
--deleted: #C55585;
--link: #4682B4;
--replies: #008B8B;
--text: #C0C0C0;
--user: #BDBB66;
--visited: #AA88DD;
--votes: #EEBBBB;
--blue: #6080A0;
--green: #5CCC85;
--orange: #906000;
--red: #AA4455;
}
:root, head, body { background: #000000; }
h1, h2, h3 { font-size: calc(22px * var(--scale)); }
a:active, a:hover { text-decoration: underline; }
.header { display: flex; flex-direction: row; align-items: center; margin: 5px 0px; padding: 10px; }
.header .main-link, .header .subreddit-link { white-space: nowrap; margin-right: 10px; }
.header .subreddit-link, .post-link, .comments > p:last-child { display: none; }
.header, .menu { border: none; }
.header a, .menu a, .button, .md p, p, b, pre code, li, .inner-post a, .votes, .post-info, .post-info .sub-link, .post-info a, .comment-info, .comment-info a, .flair, .nav a { font-family: var(--font); font-size: calc(16px * var(--scale)); color: var(--text); }
.container { max-width: none; margin: 0px; }
.content:has(:first-child[class="menu"]), .comments { display: flex; flex-direction: column; }
.content { padding: 0px 0px var(--bottom-gap) 0px; }
.menu { margin: 5px 0px; }
.menu a::first-letter { text-transform: capitalize; }
.menu a:active, .menu a:hover, .menu .focus, .button:active, .button:hover { background-color: var(--red); color: #000000; }
.post { display: flex; flex-direction: column; border: 1px solid var(--border); padding: 0px 10px; margin: 5px 0px; width: auto; }
.post:active, .post:hover { border: 1px solid var(--border); }
.votes { position: absolute; color: var(--votes); width: auto; margin: 10px; }
.inner-post, .comment { display: flex; flex-direction: column; align-items: start; margin: 0px; }
.inner-post { position: static; margin-left: calc(60px * var(--scale)); }
.inner-post a[href^="/r/"] { padding: 20px 0px; }
.inner-post a b, .comment a b { font-weight: 500; }
.inner-post a:visited b { color: var(--visited); }
.inner-post .post { width: 98%; }
.comment-info a[href^="/u/"] { color: var(--user); font-weight: 500; }
.post-info .sub-link, .comment-info .sub-link, .post-content a[href^="/r/"], .comment-content a[href^="/r/"] { color: var(--red); }
/* .author */
.post-info a[href^="/u/"], .comment .comment-info .author, .comment-content a[href^="/u/"] { color: var(--author); }
.post-info a[href^="/u/[deleted]"], .comment-info a[href^="/u/[deleted]"] { color: var(--deleted); }
/* .moderator */
.post-info a[href^="/u/AutoModerator"], .inner-post .post-info .moderator, .comment-info a[href^="/u/AutoModerator"], .comment .comment-info .moderator { color: var(--green); }
/* .link */
.post-info a[href^="/domain/"], .post-info a[target="_blank"], .post-content a:not([href^="/u/"], [href^="/r/"]), .comment-info .link, .comment-content a:not([href^="/u/"], [href^="/r/"]) { color: var(--link); font-weight: 400; }
.flair { height: auto; width: auto; padding: 5px 10px; margin: 5px 0px; }
.inner-post .flair { background: none; color: var(--green); border: 1px solid var(--green); }
.post-content, .post-content > *, .comment-content, ul:has(> .reply), .reply { display: flex; flex-direction: column; align-items: start; width: auto; margin: auto; }
.nav { margin-top: 0px; margin-bottom: 5px; }
.nav .button { border: 1px solid var(--border); margin: 10px; padding: 10px; }
.comment { border: 1px solid var(--border); margin: 5px 0px; padding: 10px; }
.comment hr { border: 1px solid var(--text); margin: 10px 0px; }
.comment > a { padding: 0px 0px 20px 0px; }
.comment-info { margin: 0px 0px 10px 0px; }
.comment-info .flair { background: var(--border); border: 1px solid var(--border); border-bottom-right-radius: calc(16px * var(--scale)); border-top-left-radius: calc(16px * var(--scale)); }
.comment-info span:not([class]) { color: var(--votes); margin: 0px 5px; }
.comment-info .link { font-size: calc(20px * var(--scale)); line-height 0px; margin: 0px 10px 0px 15px; }
.post-content:has(> .md), .comment ul, .comment-content, .md pre { margin: 0px; }
.reply, blockquote { padding: 0px 0px 0px 10px; }
.reply { border-left-style: solid; border-left-width: 2px; margin: 10px 0px 0px 0px; }
ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply,
ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply,
ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply,
ul > .reply > ul > .reply > ul > .reply > ul > .reply,
ul > .reply > ul > .reply { border-left-color: var(--orange); }
ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply,
ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply,
ul > .reply > ul > .reply > ul > .reply > ul > .reply > ul > .reply,
ul > .reply > ul > .reply > ul > .reply, ul > .reply { border-left-color: var(--blue); }
blockquote { border-left: 4px solid var(--red); margin: 10px 0px; }
pre code { max-width: 96vw; font-family: monospace; background: #303030; padding: 10px; margin: 0px; overflow: auto; scrollbar-width: none; }
.media { margin: auto; }
.media img, .media video { height: auto; min-height: 400px; max-height: 80vh; }
/* Long Post & Comment */
.long-post { max-height: 80vh; overflow: hidden; }
#expand-collapse { display: flex; flex-direction: column; font-weight: bold; padding: 20px 0px; align-items: center; width: auto; margin: auto; }
/* Sliding Images */
.css-slider-mask, .slide-gfx .media img { height: auto; min-height: 400px; max-height: 80vh; }
.slide { background: none; }
body .container .css-slider { display: flex; flex-direction: row; flex-grow: 1; align-items: center; justify-content: center; width: auto; height: auto; user-select: none; }
.css-slider li { display:flex; height: auto; margin: auto; }
.css-slider li:not(:last-child), .css-slider li[active="false"], li[active="true"] + #previous-slide, li:first-child[active="true"] ~ #next-slide { display: none; }
.css-slider li:last-child, .css-slider li[active="true"] { display: block; }
#previous-slide, #next-slide { display: flex; flex-direction: column; justify-content: center; position: absolute; height: 100%; color: var(--text); font-size: calc(32px * var(--scale)); text-shadow: -2px -2px 0 var(--border), 2px -2px 0 var(--border), -2px 2px 0 var(--border), 2px 2px 0 var(--border); }
#previous-slide:active, #previous-slide:hover, #next-slide:active, #next-slide:hover { color: var(--green); }
#previous-slide { left: 10px; padding: 0px 30px 0px 10px; }
#next-slide { right: 10px; padding: 0px 10px 0px 30px; }
/* Search Bar & Settings */
.custom-font { color: var(--text); font-family: var(--font); font-size: calc(16px * var(--scale)); }
.custom-highlight:active, .custom-highlight:hover { color: var(--red); }
.unselectable { user-select: none; }
#search-bar, #search-text, #search-label, #help-row { display: flex; flex-direction: row; align-items: center; width: auto; height: auto; margin: auto; }
#search-bar { max-width: 80%; padding: 0px 10px; border: 2px solid var(--border); }
#search-bar:has(> #search-text:focus) { border: 2px solid var(--red); }
#search-text { flex-grow: 1; outline: none; overflow: auto; padding: 10px 10px 10px 0px; scrollbar-width: none; }
#search-text, #search-help, #search-label, #search-button { background: none; border: none; }
#search-help { font-size: calc(20px * var(--scale)); font-weight: bold; margin-right: calc(5px * var(--scale)); }
#search-checkbox { accent-color: var(--red); margin: 0px calc(10px * var(--scale)); transform: scale(calc(1.4 * var(--scale)), calc(1.4 * var(--scale))); }
#search-label { font-weight: 500; padding: 10px; overflow: auto; white-space: nowrap; scrollbar-width: none; }
#search-button { font-size: calc(45px * var(--scale)); line-height: calc(40px * var(--scale)); font-weight: 500; }
#settings-button { font-size: calc(28px * var(--scale)); font-weight: bold; line-height: calc(40px * var(--scale)); margin-left: 10px; }
#settings-container, #help-container { position: absolute; top: calc(65px * var(--scale)); padding-bottom: var(--bottom-gap); z-index: 2; }
#settings-container { right: 10px; }
#settings-content, #help-content { background: #000000; border: 2px solid var(--border); }
#settings-header, #settings-save, #settings-reset, #help-header { flex-grow: 1; justify-content: center; font-weight: 700; }
#settings-body { background: var(--red); }
#settings-selection { overflow: wrap; justify-content: space-between; }
.settings-select { text-align-last: right; }
#settings-label + select { background: none; border: none; }
#settings-save:active, #settings-save:hover, #settings-reset:active, #settings-reset:hover { background: var(--red); color: #000000; }
#help-container { padding: 0px 10px var(--bottom-gap) 10px; align-self: center; }
#help-body, #help-body > * { display: flex; flex-direction: column; align-items: start; }
#help-body { background: #000000; }
#help-body > * { margin: 5px 0px; }
#help-row { flex-grow: 1; margin: 0px; padding: 0px 10px; white-space: pre-wrap; }
#help-title { font-style: italic; }
#help-title { color: var(--blue); min-width: calc(90px * var(--scale)); }
#help-description, #help-example { margin: 0px 0px 0px 10px; }
#help-example { color: var(--green); user-select: text; }
/* Comment Collapser */
.comment [hidden] { display: none; }
#collapser { display: inline-block; font-size: calc(18px * var(--scale)); margin: 0px 10px 0px 0px; transform: rotate(90deg); }
#collapser[collapsed="true"] { transform: rotate(0deg); }
/* See More Replies */
.see-more-replies { color: var(--replies); font-style: italic; font-weight: 500; margin: 20px 0px 10px 0px; }
.see-more-replies:active, .see-more-replies:hover { text-decoration: underline; }`;
document.head.appendChild(style);
// Helper function.
// Returns a substring or 0.
const substr = function(regex, str) {
const arr = regex.exec(str);
return arr && arr.length === 2 ? arr[1] : 0;
};
// TODOs:
// const disableAutoplayGifs = async function() {};
// const muteSubreddits = async function() {}; // hides disliked subreddits.
// const highlightSearchText = async function() {};
const hideLongPostsAndComments = async function() {
const onclick = async function(event) {
if (!event.target) return;
const target = event.target.parentElement.querySelector(':scope > .long-post');
if (target) {
target.classList.remove('long-post');
event.target.innerText = '⬆ COLLAPSE ⬆';
} else {
event.target.parentElement.querySelector(':scope > .md').classList.add('long-post');
event.target.innerText = '⬇ EXPAND ⬇';
}
};
const height = window.screen.height << 1;
document.body.querySelectorAll(':is(.post-content, .comment-content) > .md ').forEach(async element => {
if (element.getBoundingClientRect().height < height) return;
element.classList.add('long-post');
const button = document.createElement('div');
button.id = 'expand-collapse';
button.className = 'custom-font unselectable';
button.textContent = '⬇ EXPAND ⬇';
button.onclick = onclick;
element.parentElement.appendChild(button);
});
};
// https://support.reddithelp.com/hc/en-us/articles/19696541895316-Available-search-features
const addSearchBar = async function() {
let inputText, checkbox, sort, thread;
// author, flair, self, selftext, site, subreddit, title, url
if ((thread = substr(/^\/(r\/[^/]+)/, window.location.pathname))) {
sort = settings.subreddit_search_sort;
} else if ((thread = substr(/^\/(u\/[^/]+)/, window.location.pathname))) {
sort = settings.author_search_sort;
} else if ((thread = substr(/(?:=|\+|%20)((?:author|flair|self|selftext|site|subreddit|title|url)(?:%3A|:)[^+&]+)/, window.location.search))) {
thread = decodeURIComponent(thread);
sort = settings[thread.replace(/^([^:]+).+$/, '$1') + '_search_sort'];
} else {
thread = 'r/popular';
sort = settings.search_sort;
}
const onsearch = async function(event) {
if (event.type === 'keydown' && event.key !== 'Enter') return;
document.activeElement?.blur(); // Hide virtual keyboard.
let input = inputText.value.trim().replace(/\s/g, ' ');
if (!input) { inputText.value = ''; return; } // Delete previous whitespaces input.
let arr = [], ignoreCheckbox = false, index = 0, path = '/', rest = '', search = '';
input.replace(/(?:"[^"]*"|'[^']*')/g, match => {
if (match === '""' || match === "''") return '';
arr.push(match.replace(/^["']([ru]\/[a-zA-Z0-9_-]+)["']$/, '$1'));
return '\n';
}).match(/(?:\n|[^\n ]+)/g).forEach(field => {
if (!field) return;
if (field === '\n') {
search += (search ? ' ' : '') + arr[index++];
} else if (path === '/' && /^[ru]\/[a-zA-Z0-9_-]+$/.test(field)) {
path += field + '/'
} else if (/^u\/[a-zA-Z0-9_-]+$/.test(field)) {
search += (search ? ' ' : '') + 'author:' + field.replace(/^u\/(.+)$/, '$1');
} else {
search += (search ? ' ' : '') + field;
}
});
arr = undefined;
if (!search && path === '/') return;
GM.setValues({search_checked: checkbox.checked, search_input: (settings.remember_search ? input : undefined) });
if (!search) {
const url = window.location.origin + path.replace(/\/$/, '') +
(path.startsWith('/r') && settings.post_sort !== 'hot' ? '/' + settings.post_sort :
(path.startsWith('/u') && settings.user_sort !== 'new' ? '/' + settings.user_sort : ''));
window.location.href === url ? window.location.reload() : window.location.href = url;
return;
}
if (path.startsWith('/u')) {
search = path.replace(/^\/u\/([^/]+)\/$/, 'author:$1') + (search ? ' ' + search : '');
path = '/';
sort = settings.author_search_sort;
ignoreCheckbox = true;
} else if (path === '/' && !ignoreCheckbox && checkbox.checked) {
if (thread.startsWith('r/')) {
path += thread + '/';
} else if (thread.startsWith('u/')) {
search = thread.replace(/^u\/(.+)$/, 'author:$1') + (search ? ' ' + search : '');
} else {
search = thread + (search ? ' ' + search : '');
}
}
if (path !== '/') {
rest += '&restrict_sr=True';
sort = settings.subreddit_search_sort;
ignoreCheckbox = true;
}
rest += '&include_over_18=' + (settings.nsfw_contents ? true : false);
rest += ((ignoreCheckbox || checkbox.checked) ? (sort !== 'relevance' ? '&sort=' + sort : '') :
(settings.search_sort !== 'relevance' ? '&sort=' + settings.search_sort : ''));
const url = window.location.origin + path + 'search?' + new URLSearchParams({ q: search }) + rest;
window.location.href === url ? window.location.reload() : window.location.href = url;
};
const createHelp = function(title, description, examples) {
const container = document.createElement('div');
const row = document.createElement('div');
row.id = 'help-row';
const title_span = document.createElement('span');
title_span.id = 'help-title';
title_span.className = 'custom-font';
title_span.textContent = title;
const description_span = document.createElement('span');
description_span.id = 'help-description'
description_span.className = 'custom-font';
description_span.textContent = description;
row.appendChild(title_span);
row.appendChild(description_span);
container.appendChild(row);
examples.forEach(element => {
const row = document.createElement('div');
row.id = 'help-row';
const forexample_span = document.createElement('span');
forexample_span.id = 'help-forexample';
forexample_span.className = 'custom-font';
forexample_span.textContent = 'For example,';
const example_span = document.createElement('span');
example_span.id = 'help-example';
example_span.className = 'custom-font';
example_span.textContent = element;
row.appendChild(forexample_span);
row.appendChild(example_span);
container.appendChild(row);
});
return container;
};
const container = document.createElement('div');
container.id = 'search-bar';
inputText = document.createElement('input');
inputText.id = 'search-text';
inputText.className = 'custom-font';
inputText.type = 'text';
inputText.placeholder = 'Search';
inputText.title = 'Search something in kddit';
inputText.maxLength = 60;
inputText.value = settings.search_input ? settings.search_input.substr(0, 60) : '';
inputText.setSelectionRange(-1, -1);
inputText.onkeydown = onsearch;
let help_container;
const help = document.createElement('div');
help.id = 'search-help';
help.className = 'custom-font custom-highlight unselectable';
help.textContent = 'ⓘ';
help.onclick = async function(event) {
if (help_container && help_container.opened) {
help_container.opened = false;
help_container.remove();
} else if (help_container) {
help_container.opened = true;
document.body.appendChild(help_container);
} else {
help_container = document.createElement('div');
help_container.id = 'help-container';
help_container.className = 'unselectable';
help_container.opened = true;
const content = document.createElement('div');
content.id = 'help-content';
const header = document.createElement('div');
header.id = 'help-header';
header.className = 'header custom-font';
header.textContent = 'Search Features';
const body = document.createElement('div');
body.id = 'help-body';
body.appendChild(createHelp('r/subreddit', 'Go to, search in this subreddit, or search as text', ['r/cats', 'r/cats "hello kitty"', '"r/cats"', '"r/cats" "hello kitty"']));
body.appendChild(createHelp('u/user', 'Go to, search in this user who submitted the post, or search as text.', ['u/reddit', 'accounts u/reddit', "'u/reddit'"]));
body.appendChild(createHelp('author', 'The user who submitted the post.', ['author:reddit']));
body.appendChild(createHelp('flair', 'The text of the link flair on the post.', ['flair:cats']));
body.appendChild(createHelp('self', 'Filter by text post. Set to true to filter to only text posts, false otherwise.', ['self:true']));
body.appendChild(createHelp('selftext', 'The body of the post.', ['selftext:cat']));
body.appendChild(createHelp('site', 'The domain of the submitted URL.', ['site:example.com']));
body.appendChild(createHelp('subreddit', "The submission's subreddit.", ['subreddit:cats', 'subreddit:cats kitten']));
body.appendChild(createHelp('title', 'The submission title.', ['title:cat']));
body.appendChild(createHelp('url', "The submission's URL (the website's address).", ['url:cats']));
body.appendChild(createHelp('AND', 'Must contain these words in the results.', ['kitten AND cat', 'kitten AND cat AND dog', '"r/cats" AND kitten']));
body.appendChild(createHelp('OR', 'Must contain one of these words in the results.', ['kitten OR cat', '(kitten OR cat) NOT dog']));
body.appendChild(createHelp('NOT', 'Must not contain this word or these words in the results.', ['NOT dog', 'NOT cat NOT dog', 'r/cats kitten NOT cat', 'kitten NOT (cat OR dog)']));
content.appendChild(header);
content.appendChild(body);
help_container.appendChild(content);
document.body.appendChild(help_container);
}
};
checkbox = document.createElement('input');
checkbox.id = 'search-checkbox';
checkbox.type = 'checkbox';
checkbox.checked = await GM.getValue('search_checked', true);
const label = document.createElement('label');
label.id = 'search-label';
label.className = 'custom-font';
label.for = 'search-checkbox';
label.textContent = 'in ' + thread;
const button = document.createElement('div');
button.id = 'search-button';
button.className = 'custom-font custom-highlight unselectable';
button.textContent = '⌕';
button.onclick = onsearch;
container.appendChild(inputText);
container.appendChild(help);
container.appendChild(checkbox);
container.appendChild(label);
container.appendChild(button);
document.body.querySelector(':scope > .header')?.appendChild(container);
};
const addSettings = async function() {
const tempSettings = {};
const createMenu = function(title, tag, selectedValue, choices) {
if (/^(?:autoplay_videos|comment_scroll|external_newtab|nsfw_contents|preload_images|preload_videos|remember_search)$/.test(tag)) {
selectedValue = (selectedValue ? 'Yes' : 'No');
} else if (tag === 'font_scale') {
selectedValue = (selectedValue + '.0').replace(/^([0-9]+\.[0-9]+).*$/, '$1x');
} else if (tag === 'video_quality') {
selectedValue += 'p';
} else if (tag.endsWith('sort')) {
selectedValue = selectedValue[0].toUpperCase() + selectedValue.substr(1);
}
const container = document.createElement('div');
container.id = 'settings-selection';
container.className = 'menu';
const label = document.createElement('label');
label.id = 'settings-label';
label.className = 'custom-font';
label.for = 'settings-select-' + tag;
label.textContent = title;
const selection = document.createElement('select');
selection.id = 'settings-select-' + tag;
selection.className = 'custom-font settings-select';
selection.onchange = async function(event) {
const id = event.target.id.replace('settings-select-', '');
switch (id) {
case 'bottom_gap':
tempSettings.bottom_gap = event.target.value;
break;
case 'autoplay_videos':
case 'comment_scroll':
case 'external_newtab':
case 'nsfw_contents':
case 'preload_images':
case 'preload_videos':
case 'remember_search':
tempSettings[id] = (event.target.value === 'Yes' ? true : false);
break;
case 'font_scale':
tempSettings.font_scale = Number(event.target.value.replace('x', ''));
break;
case 'video_quality':
tempSettings.video_quality = Number(event.target.value.replace('p', ''));
break;
case 'comment_sort':
case 'post_sort':
case 'user_sort':
case 'search_sort':
case 'author_search_sort':
case 'flair_search_sort':
case 'self_search_sort':
case 'selftext_search_sort':
case 'site_search_sort':
case 'subreddit_search_sort':
case 'title_search_sort':
case 'url_search_sort':
tempSettings[id] = event.target.value.toLowerCase();
break;
}
};
choices.forEach(choice => {
const option = document.createElement('option');
if (choice === selectedValue) option.selected = true;
option.value = choice;
option.textContent = choice;
selection.appendChild(option);
});
container.appendChild(label);
container.appendChild(selection);
return container;
};
let container;
const button = document.createElement('div');
button.id = 'settings-button';
button.className = 'custom-font custom-highlight unselectable';
button.textContent = '⫶☰';
button.onclick = async function(event) {
if (container && container.opened) {
container.opened = false;
container.remove();
return;
} else if (container) {
container.opened = true;
document.body.appendChild(container);
} else {
container = document.createElement('div');
container.id = 'settings-container';
container.className = 'unselectable';
container.opened = true;
const content = document.createElement('div');
content.id = 'settings-content';
const header = document.createElement('div');
header.id = 'settings-header';
header.className = 'header custom-font';
header.textContent = 'Settings';
const body = document.createElement('div');
body.id = 'settings-body';
body.appendChild(createMenu('Autoplays Videos:', 'autoplay_videos', settings.autoplay_videos, ['Yes', 'No']));
body.appendChild(createMenu('Autoscrolls to Comments:', 'comment_scroll', settings.comment_scroll, ['Yes', 'No']));
body.appendChild(createMenu('Bottom Gap:', 'bottom_gap', settings.bottom_gap, ['0px', '20px', '40px', '60px', '80px', '100px']));
body.appendChild(createMenu('External Links In New Tab:', 'external_newtab', settings.external_newtab, ['Yes', 'No']));
body.appendChild(createMenu('Font Scaling:', 'font_scale', settings.font_scale, ['0.8x', '0.9x', '1.0x', '1.1x', '1.2x', '1.3x', '1.4x', '1.5x']));
body.appendChild(createMenu('NSFW Contents:', 'nsfw_contents', settings.nsfw_contents, ['Yes', 'No']));
body.appendChild(createMenu('Preloads Images:', 'preload_images', settings.preload_images, ['Yes', 'No']));
body.appendChild(createMenu('Preloads Videos:', 'preload_videos', settings.preload_videos, ['Yes', 'No']));
body.appendChild(createMenu('Remembers Search Input:', 'remember_search', settings.remember_search, ['Yes', 'No']));
body.appendChild(createMenu('Video Quality:', 'video_quality', settings.video_quality, ['220p', '270p', '360p', '480p', '720p', '1080p']));
body.appendChild(createMenu('Comments Sorting:', 'comment_sort', settings.comment_sort, ['Confidence', 'Top', 'New', 'Controversial', 'Old']));
body.appendChild(createMenu('Posts Sorting:', 'post_sort', settings.post_sort, ['Hot', 'New', 'Top', 'Rising', 'Controversial']));
body.appendChild(createMenu('User Sorting:', 'user_sort', settings.user_sort, ['Hot', 'New', 'Top', 'Controversial']));
body.appendChild(createMenu('Search Sorting:', 'search_sort', settings.search_sort, ['Relevance', 'Top', 'New', 'Comments']));
body.appendChild(createMenu('Search in User Sorting:', 'author_search_sort', settings.author_search_sort, ['Relevance', 'Top', 'New', 'Comments']));
body.appendChild(createMenu('Search in Flair Sorting:', 'flair_search_sort', settings.flair_search_sort, ['Relevance', 'Top', 'New', 'Comments']));
body.appendChild(createMenu('Search in Self Sorting:', 'self_search_sort', settings.self_search_sort, ['Relevance', 'Top', 'New', 'Comments']));
body.appendChild(createMenu('Search in SelfText Sorting:', 'selftext_search_sort', settings.selftext_search_sort, ['Relevance', 'Top', 'New', 'Comments']));
body.appendChild(createMenu('Search in Site Sorting:', 'site_search_sort', settings.site_search_sort, ['Relevance', 'Top', 'New', 'Comments']));
body.appendChild(createMenu('Search in Subreddit Sorting:', 'subreddit_search_sort', settings.subreddit_search_sort, ['Relevance', 'Top', 'New', 'Comments']));
body.appendChild(createMenu('Search in Title Sorting:', 'title_search_sort', settings.title_search_sort, ['Relevance', 'Top', 'New', 'Comments']));
body.appendChild(createMenu('Search in URL Sorting:', 'url_search_sort', settings.url_search_sort, ['Relevance', 'Top', 'New', 'Comments']));
const reset = document.createElement('div');
reset.id = 'settings-reset';
reset.className = 'header custom-font';
reset.textContent = 'Reset';
reset.onclick = async function(event) {
GM.deleteValues(['autoplay_videos', 'bottom_gap', 'comment_scroll', 'external_newtab', 'font_scale', 'nsfw_contents', 'preload_images', 'preload_videos',
'remember_search', 'search_checked', 'search_input', 'video_quality', 'comment_sort', 'post_sort', 'user_sort', 'search_sort', 'author_search_sort',
'flair_search_sort', 'self_search_sort', 'selftext_search_sort', 'site_search_sort', 'subreddit_search_sort', 'title_search_sort', 'url_search_sort']);
Object.assign(settings, defaultSettings);
container.remove();
container = undefined;
button.click();
};
const save = document.createElement('div');
save.id = 'settings-save';
save.className = 'header custom-font';
save.textContent = 'Save & Reload';
save.onclick = async function(event) {
container.remove();
container = undefined;
Object.assign(settings, tempSettings);
GM.setValues(tempSettings);
if (!settings.remember_search) GM.deleteValue('search_input');
const location = window.location;
if (/^\/r\/[^/]+\/comments\/./.test(location.pathname)) {
if (/[?&]sort=[^&]+/.test(location.search)) {
location.href = location.origin + location.pathname.replace(/\/+$/, '') + location.search.replace(/(.)sort=[^&]+/, `$1sort=${settings.comment_sort}`) + location.hash;
} else if (settings.comment_sort !== 'confidence') {
location.href = location.origin + location.pathname.replace(/\/+$/, '') + location.search + (location.search ? `&sort=${settings.comment_sort}` : `?sort=${settings.comment_sort}`) + location.hash;
} else {
location.reload();
}
} else if (/^\/r\/[^/]+(?:\/hot|\/new|\/top|\/rising|\/controversial)?$/.test(location.pathname)) {
location.href = location.origin + location.pathname.replace(/^(\/r\/[^/]+).*$/, '$1') + (settings.post_sort === 'hot' ? '' : `/${settings.post_sort}`) + location.search + location.hash;
} else if (/^\/u\/[^/]+(?:\/overview|\/comments|\/submitted)?/.test(location.pathname)) {
if (/[?&]sort=[^&]+/.test(location.search)) {
location.href = location.origin + location.pathname.replace(/\/+$/, '') + location.search.replace(/(.)sort=[^&]+/, `$1sort=${settings.user_sort}`) + location.hash;
} else if (settings.user_sort !== 'new') {
location.href = location.origin + location.pathname.replace(/\/+$/, '') + location.search + (location.search ? `&sort=${settings.user_sort}` : `?sort=${settings.user_sort}`) + location.hash;
} else {
location.reload();
}
} else if (!settings.nsfw_contents) {
if (!location.search) {
location.href = location.origin + location.pathname + '?include_over_18=false' + location.hash;
} else if (/include_over_18=[^&]+/.test(location.search)) {
location.href = location.origin + location.pathname + location.search.replace(/include_over_18=[^&]+/, 'include_over_18=false') + location.hash;
} else {
location.href = location.origin + location.pathname + location.search + '&include_over_18=false' + location.hash;
}
} else {
location.reload();
}
};
content.appendChild(header);
content.appendChild(body);
content.appendChild(reset);
content.appendChild(save);
container.appendChild(content);
document.body.appendChild(container);
}
};
document.body.querySelector(':scope > .header')?.appendChild(button);
};
const clickGoesToComments = async function() {
if (/^\/r\/[^/]+\/comments\/[^/]+\/(?!comment)/.test(window.location.pathname)) return;
window.addEventListener('click', async event => {
if (!event.target || !event.composed || event.target.id === 'previous-slide' || event.target.id === 'next-slide' ||
event.target.id === 'expand-collapse' || event.target.classList.contains('post')) return;
const anchor = event.composedPath().find(node => node.classList?.contains('inner-post'))?.querySelector(':scope > a[href^="/r/"]');
if (!anchor || !/^\/r\/[^/]+\/comments\/./.test(anchor.pathname)) return;
anchor.click();
//window.console.log('GO TO COMMENTS');
}, false);
};
// Highlighted at the wrong sort. It's "New" by default.
const highlightUserSortMenu = async function() {
if (!(/^\/u\/[^/]+(?:\/|\/overview|\/comments|\/submitted)?$/.test(window.location.pathname) && !substr(/[?&]sort=(hot|new|top|controversial)/, window.location.search))) return;
document.body.querySelectorAll(':scope > .container > .content > .menu > a').forEach(async anchor => {
if (!anchor.search) return;
if (anchor.search === '?sort=new') { anchor.classList.add('focus'); return; }
anchor.classList.remove('focus');
});
};
// Sometimes, can't return to the previous images from the last image.
// Sometimes, it just goes back and forth between next image and previous image.
// Always, flickering/flashing when other element is getting focus when you're on the next image.
const clickSlideImages = async function() {
const onclick = async function(event) {
let target = event.target.parentElement.firstElementChild;
do {
if (target.getAttribute('active') !== 'true') continue;
if (event.target.id === 'previous-slide' && target.nextElementSibling instanceof HTMLLIElement) {
target.setAttribute('active', false);
target.nextElementSibling.setAttribute('active', true);
break;
}
if (event.target.id === 'next-slide' && target.previousElementSibling instanceof HTMLLIElement) {
target.setAttribute('active', false);
target.previousElementSibling.setAttribute('active', true);
break;
}
break;
} while ((target = target.nextElementSibling));
};
document.body.querySelectorAll('ul.css-slider').forEach(async element => {
element.querySelectorAll(':scope > li.slide').forEach(async (element, index, array) => {
element.setAttribute('active', (index === array.length - 1 ? true : false));
element.classList.remove('slide');
element.removeAttribute('tabindex');
element.firstElementChild?.classList.remove('slide-outer');
});
const previous = document.createElement('div');
previous.id = 'previous-slide';
previous.className = 'unselectable';
previous.textContent = '❮❮';
previous.onclick = onclick;
const next = document.createElement('div');
next.id = 'next-slide';
next.className = 'unselectable';
next.textContent = '❯❯';
next.onclick = onclick;
element.appendChild(previous);
element.appendChild(next);
})
};
const uppercasePreviousNext = async function() {
if (/^\/r\/[^/]+\/comments\/./.test(window.location.pathname)) return;
document.body.querySelectorAll(':scope > .container > .content > .nav > a.button').forEach(async anchor => {
anchor.innerText = anchor.innerText.startsWith('<') ? 'Prev' : (anchor.innerText.endsWith('>') ? 'Next' : anchor.innerText);
});
};
const addCommentCollapsers = async function() {
if (!/^\/r\/[^/]+\/comments\/./.test(window.location.pathname)) return;
const onclick = async function(event) {
event.target?.parentElement?.parentElement?.querySelectorAll(':scope > :is(.comment-content, ul)').forEach(async element => {
event.target.setAttribute('collapsed', (element.hidden = !element.hidden));
});
};
document.body.querySelectorAll('.comment-info').forEach(async comment => {
const collapser = document.createElement('div');
collapser.id = 'collapser';
collapser.textContent = '➤';
collapser.onclick = onclick;
comment.insertBefore(collapser, comment.firstElementChild);
});
};
// Absolutely not perfect. There is no indicators at all.
const highlightAdminsBotsAndMods = async function() {
//if (!/^\/r\/[^/]+\/comments\/./.test(window.location.pathname)) return;
const regex = new RegExp('^\\/u\\/(?:[a-zA-Z0-9_-]+-ModTeam' +
'|AutoModerator|RemindMeBot|SaveVideo|WhyNotCollegeBoard|WorldNewsMods|MAGIC_EYE_BOT' +
'|flairassistant|kzreminderbot|pccmodbot|reddit|reputatorbot|sneakpeekbot|spotlight-app' +
'|sticky-comments|topredditbot|trendingtattler' +
')$');
document.body.querySelectorAll('a[href^="/u/"]').forEach(async anchor => {
if (regex.test(anchor.pathname)) anchor.classList.add('moderator');
});
};
const highlightOriginalPoster = async function() {
if (!/^\/r\/[^/]+\/comments\/./.test(window.location.pathname)) return;
const poster = document.body.querySelector(':scope > .container > .content > .post > .inner-post > .post-info > a[href^="/u/"]:not([href="/u/[deleted]"])');
if (!poster) return;
document.body.querySelectorAll(`.comment-info > a[href^="${poster.pathname}"]`).forEach(async anchor => anchor.classList.add('author'));
};
const hideAdminsBotsAndMods = async function() {
if (!/^\/r\/[^/]+\/comments\/./.test(window.location.pathname)) return;
document.body.querySelectorAll('.comment-info:has(a.moderator)').forEach(async comment => {
if (comment.firstElementChild.id !== 'collapser') return;
comment.firstElementChild.setAttribute('collapsed', true);
comment.parentElement?.querySelectorAll(':scope > :is(.comment-content, ul)').forEach(async element => {
element.hidden = true;
});
});
};
const replaceCommentLinkIcons = async function() {
if (!/^\/r\/[^/]+\/comments\/./.test(window.location.pathname)) return;
document.body.querySelectorAll(`.comment-info a[href^="${decodeURI(window.location.pathname).replace(/^(\/r\/[^/]+\/comments\/[^/]+\/).*$/, '$1')}"]`).forEach(async anchor => {
anchor.classList.add('link');
anchor.innerText = '➥';
});
};
const seeMoreReplies = async function() {
if (!/^\/r\/[^/]+\/comments\/./.test(window.location.pathname)) return;
const onclick = async function(event) {
if (!event.target) return;
const anchor = event.target.parentElement.parentElement.firstElementChild.querySelector('a[href^="/r/"]');
if (/^\/r\/[^/]+\/comments\/./.test(anchor)) return;
anchor.click();
};
document.body.querySelectorAll(':is(.comment, .reply) > ul > p:last-child').forEach(async element => {
if (element.innerText !== '...') return;
element.classList.add('see-more-replies');
element.innerText = 'See more replies to ' + element.parentElement.parentElement.firstElementChild.querySelector('a[href^="/u/"]').innerText;
element.onclick = onclick;
});
};
// navigate, reload, back_forward
const autoScrollsToComments = async function() {
if (!settings.comment_scroll || !/^\/r\/[^/]+\/comments\/./.test(window.location.pathname) ||
window.performance.getEntriesByType('navigation')[0].type === 'back_forward') return;
const comments = document.body.querySelector(':scope > .container > .content > .comments');
const rect = comments?.getBoundingClientRect();
if (!comments || window.scrollY >= rect.top) return;
window.scrollTo({ top: rect.top || 0, left: 0, behavior: 'instant' }) // smooth, instant, auto
};
const replaceDomainsPosition = async function() {
document.body.querySelectorAll('.inner-post').forEach(async element => {
const url = element.querySelector(':scope > a.post-link')?.href;
const anchor = element.querySelector(':scope > .post-info > a[href^="/domain/"]');
if (!url || !anchor) return;
anchor.href = url;
});
};
// search: confidence, top, new, controversial, old
const sortCommentLinks = async function() {
if (settings.comment_sort === 'confidence') return;
document.body.querySelectorAll('a[href^="/r/"]').forEach(async anchor => {
if (!/^\/r\/[^/]+\/comments\/./.test(anchor.pathname)) return;
anchor.href = anchor.pathname.replace(/\/+$/, '') + anchor.search + (anchor.search ? `&sort=${settings.comment_sort}` : `?sort=${settings.comment_sort}`) + anchor.hash;
});
};
// pathname: hot, new, top, rising, controversial
const sortPostLinks = async function() {
if (settings.post_sort === 'hot') return;
const anchor = document.body.querySelector(':scope > .header > a.main-link');
if (anchor) {
anchor.href = anchor.pathname + `${settings.post_sort}` + anchor.hash;
}
document.body.querySelectorAll('a[href^="/r/"]').forEach(async anchor => {
if (!/^\/r\/[^/]+\/?$/.test(anchor.pathname)) return;
anchor.href = anchor.pathname.replace(/\/+$/, '') + `/${settings.post_sort}` + anchor.search + anchor.hash;
});
};
// They're sorted by "hot" but it's actually "new" by default.
// pathname: overview, comments, submitted
// search: hot, new, top, controversial
const sortUserLinks = async function() {
if (settings.user_sort === 'new') return;
document.body.querySelectorAll('a[href^="/u/"]').forEach(async anchor => {
if (/[?&]sort=[^&]+/.test(anchor.search)) return;
anchor.href = anchor.pathname.replace(/\/+$/, '') + anchor.search + (anchor.search ? `&sort=${settings.user_sort}` : `?sort=${settings.user_sort}`) + anchor.hash;
});
};
// Enabled by default. Not yet supported by kddit to disable it.
const disableNSFW = async function() {
if (settings.nsfw_contents) return;
// Home.
const anchor = document.body.querySelector(':scope > .header > a.main-link');
if (anchor) {
anchor.href = anchor.pathname + '?include_over_18=false' + anchor.hash;
}
// Sort menu & Navigation.
document.body.querySelectorAll(':scope > .container > .content > :is(.menu, .nav) > a').forEach(async anchor => {
if (!anchor.search) {
anchor.href = anchor.pathname + '?include_over_18=false' + anchor.hash;
} else if (/include_over_18=[^&]+/.test(anchor.search)) {
anchor.href = anchor.pathname + anchor.search.replace(/include_over_18=[^&]+/, 'include_over_18=false') + anchor.hash;
} else {
anchor.href = anchor.pathname + anchor.search + '&include_over_18=false' + anchor.hash;
}
});
};
// Some links are not proxied.
const proxyExternalLinks = async function() {
document.body.querySelectorAll('.md a').forEach(async anchor => {
const domain = anchor.hostname.replace(/^(?:[^.]+\.)*([^.]+\.[^.]+)$/, '$1');
if (domain !== 'reddit.com' && domain !== 'removeddit.com') return;
anchor.href = anchor.pathname.replace(/(.)\/$/, '$1') + anchor.search + anchor.hash;
});
};
const externalLinksInNewTab = async function() {
if (!settings.external_newtab) return;
const hostname = window.location.hostname;
document.body.querySelectorAll('a').forEach(async anchor => {
if (anchor.hostname === hostname) return;
anchor.rel = 'noopener,noreferrer';
anchor.target = '_blank';
});
};
// Currently, kddit does not support m3u8. Just "in case" for the future.
// 220, 270, 360, 480, 720, 1080
// Videos are set to not preloaded by default.
const autoplayPreloadAndVideoResolution = async function() {
const autoplay = settings.autoplay_videos ? true : false;
const preload = settings.preload_videos ? 'auto' : 'none'; // none, metadata, auto
document.body.querySelectorAll('video').forEach(async video => {
if (video.src && !video.src.startsWith('blob:') && Number(substr(/_([0-9]+)\.(?:mp4|m3u8)/, video.src)) > settings.video_quality) {
const src = video.src;
video.src = '';
video.src = 'about:blank';
video.removeAttribute('src');
video.src = src.replace(/_[0-9]+\.(mp4|m3u8)/, `_${settings.video_quality}.$1`);
}
video.autoplay = autoplay;
video.preload = preload;
});
};
// Helper function.
// Retry once. Ignores the image after a 60-second times out if there is no errors.
const downloadImage = function(image, retries, timeout) {
image.onerror = image.onload = null;
image.removeAttribute('onerror');
let loading, timeoutId, unlock;
const abortController = new AbortController(), lock = new Promise(resolve => unlock = resolve), src = image.src;
const finished = async function() {
if (!loading) return;
window.clearTimeout(timeoutId);
abortController.abort();
image.fetchPriority = 'low'; // If it is not yet done.
image.loading = 'lazy';
unlock();
};
image.addEventListener('error', event => {
event.stopPropagation();
event.stopImmediatePropagation();
if (!loading) return;
if (retries <= 0) { finished(); return; }
image.src = '';
image.src = 'about:blank';
window.clearTimeout(timeoutId); // Errored within 60 seconds.
timeoutId = window.setTimeout(finished, timeout);
image.src = src;
--retries;
}, { capture: true, signal: abortController.signal });
image.addEventListener('load', finished, { capture: true, signal: abortController.signal });
image.src = '';
image.src = 'about:blank';
image.removeAttribute('src');
image.decoding = 'async'; // sync, async, auto
image.fetchPriority = 'high'; // low, high, auto
image.loading = 'eager'; // eager, lazy
loading = true;
timeoutId = window.setTimeout(finished, timeout); // 60 seconds.
image.src = src;
return lock;
};
// naturalWidth == source's width, naturalHeight == source's height.
// clientWidth == rendered width, clientHeight == rendered height.
const preloadImages = async function() {
if (!settings.preload_images) return;
for (const img of document.body.querySelectorAll('img')) {
if ((img.complete && img.naturalWidth && img.naturalHeight) || !img.src || img.src.startsWith('blob:')) continue;
await downloadImage(img, 1, 60000); // Loads one by one.
}
};
const redownloadBrokenImages = async function() {
if (settings.preload_images) return;
let loading, queue;
const onerror = async function() {
if (!this) return;
this.onerror = null;
(queue || (queue = [])).push(this);
if (loading) return; // Waits one to finish first.
loading = true;
do {
await downloadImage(queue[0], 1, 60000);
delete queue[0];
queue = queue.filter(image => !!image); // Trim.
} while (0 < queue.length);
loading = queue = undefined;
};
for (const img of document.body.querySelectorAll('img')) {
if ((img.complete && img.naturalWidth !== 0 && img.naturalHeight !== 0) || !img.src || img.src.startsWith('blob:')) continue;
if (!img.complete) {
img.onerror = onerror;
} else {
await downloadImage(img, 1, 60000);
}
}
};
const doAll = async function() {
hideLongPostsAndComments();
addSearchBar().then(addSettings);
clickGoesToComments();
highlightUserSortMenu();
clickSlideImages();
uppercasePreviousNext();
addCommentCollapsers();
highlightAdminsBotsAndMods().then(hideAdminsBotsAndMods);
highlightOriginalPoster();
replaceCommentLinkIcons();
seeMoreReplies();
autoScrollsToComments();
replaceDomainsPosition();
sortCommentLinks();
sortPostLinks();
sortUserLinks();
disableNSFW();
proxyExternalLinks().then(externalLinksInNewTab);
autoplayPreloadAndVideoResolution();
preloadImages();
redownloadBrokenImages();
};
if (document.readyState === 'loading') {
window.addEventListener('DOMContentLoaded', doAll, true);
} else {
doAll();
}
/*
// FOR DEBUGGING.
window.addEventListener('click', event => {
window.console.log(event.target);
if (!event.composed) return;
let str = '';
event.composedPath().forEach(element => {
str += (element.tagName ? '<' + element.tagName.toLowerCase()
+ (element.id ? ' id="' + element.id + '"' : '')
+ (element.className ? ' class="' + element.className + '"/>\n' : '/>\n')
: (element.constructor.name === 'HTMLDocument' ? 'document\n' : element.constructor.name.toLowerCase() + '\n'));
});
window.console.log(str);
}, false);
*/
})();