// ==UserScript==
// @name LynxChan Extended Minus Minus
// @namespace https://rentry.org/8chanMinusMinus
// @version 1.53
// @description LynxChan Extended with even more features
// @author SaddestPanda & Dandelion & /gfg/
// @license UNLICENSE
// @match *://8chan.moe/*/res/*
// @match *://8chan.se/*/res/*
// @match *://8chan.cc/*/res/*
// @grant GM.getValue
// @grant GM.setValue
// @grant GM.deleteValue
// @grant GM.registerMenuCommand
// @run-at document-idle
// @downloadURL none
// ==/UserScript==
(async function () {
"use strict";
const SETTINGS_DEFINITIONS = {
firstRun:{
default:true,
hidden:true,
desc:"You shouldn't be able to see this setting! (firstRun)"
},
showScrollbarMarkers:{
default:true,
desc:"Show your posts and replies on the scrollbar"
},
spoilerImageType:{
default:"off",
desc:"Override how the spoiler thumbnail looks",
type:"radio",
options:{
off:"Don't change the thumbnail.",
reveal:"Reveal spoilers. Previously spoilered images will have a red border around them indicating that they're spoilers.",
kachina:"Makes the spoiler image Kachina from Genshin Impact.",
thread:`Uses the first image of the first visible post on the current thread with the filename "ThreadSpoiler.jpg" (or .png or .webp)`,
threadAlt:`same as above with the filename "ThreadSpoilerAlt.jpg" (or .png or .webp)`
}
},
overrideBoardSpoilerImage: {
default:true,
parent:"spoilerImageType",
//Not implemented yet
//depends: function() {return settings.spoilerImageType != "off"},
desc:"Override spoiler thumbnail even if the board has a custom thumbnail set. (For example, /v/'s spoiler thumbnail is an image of a monitor with a ? inside it)"
},
revealSpoilerText:{
default:"off",
desc:"Reveal the spoiler text. Or make it into madoka runes.",
type:"radio",
options:{
off:"Don't reveal spoilers.",
on:"Spoilers will be always be shown by turning the text white.",
madoka:`Spoilers will turn into madoka runes. Please install MadokaRunes.ttf for it to show up properly.`
}
},
useExtraStylingFixes:{
default:true,
desc:"Apply some styling fixes (Mark your posts and replies, smaller thumbnails etc.)"
},
//I swear this used to be a built in option on 8chan
halfchanGreentexts:{
default:false,
desc:"Make the greentext brighter like 4chan"
},
showPostIndex:{
default:true,
desc:"Show the current index of a post on the thread. That is, the topmost post will start at 1 and count up from there."
},
showStubs:{
default:true,
desc:"Show post stubs when filtering"
},
preserveQuickReply:{
default:false,
desc:"Preserve the quick reply text when pressing the Esc key or X button"
}
//Would anyone ever want this off?
/*ctrlEnterPost:{
default:false,
desc:"Enable Ctrl+Enter to post your reply"
}*/
/*redirectToCatalog:{
default:false,
desc:"Redirect to catalog when clicking on the index."
}*/
}
const settingsNames = Object.keys(SETTINGS_DEFINITIONS);
const settingsValues = await Promise.all(settingsNames.map(key => GM.getValue(key, SETTINGS_DEFINITIONS[key]['default'])));
const settings = Object.fromEntries(settingsNames.map((key, index) => [key, settingsValues[index]]));
console.log("%cLynx Minus Minus Started with settings:", "color:rgb(0, 140, 255)", settings);
if (typeof api !== "undefined") {
console.log("The script is not sandboxed. Adding quick reply shortcut.")
function quickReplyShortcut(ev) {
if (ev.ctrlKey && ev.key == "r") {
ev.preventDefault(); qr.showQr(); document.getElementById('qrbody')?.focus();
};
}
document.addEventListener("keydown",quickReplyShortcut);
} else {
console.log("JS script is sandboxed and can't access page JS... (If you can read this, let me know what browser/extension does this. Or maybe the site just failed to load?)")
}
function addMyStyle(newID, newStyle) {
let myStyle = document.createElement("style");
//myStyle.type = 'text/css';
myStyle.id = newID;
myStyle.textContent = newStyle;
document.querySelector("head").appendChild(myStyle);
}
addMyStyle("lynx-extended-css", `
.marker-container {
position: fixed;
top: 16px;
right: 0;
width: 10px;
height: calc(100vh - 40px);
z-index: 11000;
pointer-events: none;
}
.marker {
position: absolute;
width: 100%;
height: 6px;
background: #0092ff;
cursor: pointer;
pointer-events: auto;
border-radius: 40% 0 0 40%;
z-index: 5;
}
.marker.alt {
background: #a8d8f8;
z-index: 2;
}
#lynxExtendedMenu {
position: fixed;
top: 15px;
left: 50%;
transform: TranslateX(-50%);
padding: 10px;
z-index: 10000;
font-family: Arial, sans-serif;
font-size: 14px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
background: #353535;
border: 1px solid #737373;
color: #ddd;
border-radius: 4px;
max-height:100%;
overflow-y: auto;
}
/*What the fuck is up with CSS */
/*.settings-content {
max-height: 90%;
}*/
.settings-footer {
height:70px;
}
@media screen and (max-width: 1000px) {
#lynxExtendedMenu{
right:0;
width:90%;
/*bottom:15px;*/
}
}
.lynxExtendedButton::before {
content: "\\e0da";
`);
// Register menu command
GM.registerMenuCommand("Show Options Menu", openMenu);
try {
createSettingsButton();
} catch (error) {
console.log("Error while creating settings button:", error);
}
//Open the settings menu on the first run
if (settings.firstRun) {
settings.firstRun = false;
await GM.setValue("firstRun", settings.firstRun);
openMenu();
}
function replyKeyboardShortcuts(ev) {
if (ev.ctrlKey) {
let combinations = {
"s":["[spoiler]","[/spoiler]"],
"b":["'''","'''"],
"u":["__","__"],
"i":["''","''"],
"d":["[doom]","[/doom]"],
"m":["[moe]","[/moe]"]
}
for (var key in combinations)
{
if (ev.key == key)
{
ev.preventDefault();
console.log("ctrl+"+key+" pressed in textbox")
const textBox = ev.target;
let newText = textBox.value;
const tags = combinations[key]
const selectionStart = textBox.selectionStart
const selectionEnd = textBox.selectionEnd
if (selectionStart == selectionEnd) { //If there is nothing selected, make empty tags and center the cursor between it
document.execCommand("insertText",false, tags[0] + tags[1]);
//Center the cursor between tags
textBox.selectionStart = textBox.selectionEnd = (textBox.selectionEnd - tags[1].length);
} else {
//Insert text and keep undo/redo support (Only replaces highlighted text)
document.execCommand("insertText",false, tags[0] + newText.slice(selectionStart, selectionEnd) + tags[1])
}
return;
}
}
//Ctrl+Enter to send reply
if (ev.key=="Enter") {
document.getElementById("qrbutton")?.click()
}
//I wanted to change this so it propagates upwards for any input but I decided it was kind of pointless.
//The quick reply field is focused... EXCEPT when you're pressing the button at the bottom. Why??? JUST ALWAYS FOCUS IT
} else if (ev.key == "Escape") {
//Because some script managers cannot access the JS of the page we have to do some funny stuff
//(So far violentmonkey isn't sandboxed)
document.getElementById("quick-reply").querySelector(".close-btn").click()
}
}
document.getElementById("qrbody").addEventListener("keydown", replyKeyboardShortcuts);
//I'm not sure who would ever want this on but I'm making it an option anyways
if (settings.preserveQuickReply===false) {
document.getElementById("quick-reply").querySelector(".close-btn").addEventListener("click", function(ev){
document.getElementById("qrbody").value = "";
})
}
function openMenu() {
const oldMenu = document.getElementById("lynxExtendedMenu");
if (oldMenu) {
oldMenu.remove();
return;
}
// Create options menu
const menu = document.createElement("div");
menu.id = "lynxExtendedMenu";
menu.innerHTML = `
LynxChan Extended-- Options
`;
//we use createElement() here instead of setting innerHTML so we can attach onclick to elements
//...In the future, at least. There aren't any onclicks added yet.
let settings_content = document.createElement("div");
settings_content.classList.add("settings-content");
Object.keys(SETTINGS_DEFINITIONS).forEach((name) => {
const setting = SETTINGS_DEFINITIONS[name];
if (setting.hidden) {
//pass
}
else if (setting.type == "radio") {
let html = `${setting.desc}