Repository navigation
Releases: lordmage/Manga-Dex-userscript
Release list
1.0.7
// ==UserScript==
// @name MangaDex++ Enhanced
// @namespace https://github.com/MangaDexPP/userscript
// @Version 1.0.7
// @description Enhanced QOL features for MangaDex with stable controls and filtering - Optimized
// @match https://mangadex.org/*
// @match http://mangadex.org/*
// @ICON https://icons.duckduckgo.com/ip2/www.mangadex.org.ico
// @grant none
// ==/UserScript==
//------------------------------------------------//
//------------------CONFIGURABLE------------------//
//------------------------------------------------//
//-------------------UNIVERSAL--------------------//
const POLLING_TIME = 100;
const API_REQUEST_INTERVAL = 1000;
//--------------------TRACKER---------------------//
const READ_BUTTON_COLOR = "#13ab493d";
const IGNORE_BUTTON_COLOR = "#ab13133d";
const UNMARKED_BUTTON_COLOR = "#4242cd3d";
const HIDE_ALL_READ_BUTTON_COLOR = "#ff80003d";
//-----------------HIDE ALL READ------------------//
const DOES_HIDE_ALL_READ = true;
//------------------BLOCK USERS-------------------//
const USER_LIST = [];
const GROUP_LIST = [];
const TAG_LIST = ["boys' love"]; // IMPORTANT: Use all lowercase
//------------------------------------------------//
//------------------DO NOT TOUCH------------------//
//------------------------------------------------//
let hideRead = false;
let hideIgnore = true;
let hideUnmarked = false;
let hideAllRead = true;
let forceRecheckNewEntry = false;
let queue = [];
const UUID_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
const CATEGORY_FEED = "/titles/feed";
const CATEGORY_FOLLOWS = "/titles/follows";
const CATEGORY_HISTORY = "/my/history";
const CATEGORY_ALL = "/titles";
const CATEGORY_RECENT = "/titles/recent";
const CATEGORY_LATEST = "/titles/latest";
const CATEGORY_AUTHOR = "/author/";
const CATEGORY_GROUP = "/group/";
const CATEGORY_TITLE = "/title/";
const CATEGORY_TAGS = "/tag/";
const FORMAT_NOT_FOUND = 0;
const FORMAT_LIST = 1;
const FORMAT_THUMBNAIL = 2;
const FORMAT_DETAIL = 3;
// ================ ENHANCED UTILITIES ================
function extractIdFromHref(href) {
if (!href) return null;
// First try UUID pattern
const m = href.match(UUID_RE);
if (m) return m[0];
// Try to extract from URL path
try {
// Parse URL
const url = new URL(href);
const pathParts = url.pathname.split('/');
// Look for 'title' in path and get next segment
const titleIndex = pathParts.indexOf('title');
if (titleIndex !== -1 && titleIndex + 1 < pathParts.length) {
const potentialId = pathParts[titleIndex + 1];
// Return if it's not empty
if (potentialId && potentialId.trim() !== '') {
return potentialId;
}
}
// Fallback: look for any non-empty path segment that's not a common word
const commonWords = ['title', 'chapter', 'manga', 'tag', 'group', 'user', 'settings', 'login', 'register'];
for (const part of pathParts) {
if (part && part.trim() !== '' && !commonWords.includes(part.toLowerCase())) {
return part;
}
}
} catch (e) {
// If URL parsing fails, fall back to original logic but skip domains
const parts = href.split("/");
for (const p of parts) {
// Skip common domains and protocol parts
if (p && p.length >= 1 && !p.includes('http') && !p.includes('www.') && !p.includes('.org') && !p.includes('.com')) {
return p;
}
}
}
return null;
}
function getFormat(pathname) {
if (pathname.startsWith(CATEGORY_TITLE)) {
return FORMAT_DETAIL;
}
if (pathname.startsWith(CATEGORY_GROUP)) {
return FORMAT_LIST;
}
if (pathname.startsWith(CATEGORY_AUTHOR)) {
return FORMAT_THUMBNAIL;
}
if (pathname.startsWith(CATEGORY_TAGS)) {
return FORMAT_THUMBNAIL;
}
switch (pathname) {
case CATEGORY_FEED:
return FORMAT_LIST;
case CATEGORY_FOLLOWS:
return FORMAT_THUMBNAIL;
case CATEGORY_HISTORY:
return FORMAT_LIST;
case CATEGORY_ALL:
return FORMAT_THUMBNAIL;
case CATEGORY_RECENT:
return FORMAT_THUMBNAIL;
case CATEGORY_LATEST:
return FORMAT_LIST;
default:
return FORMAT_NOT_FOUND;
}
}
// ================ PER-TITLE CONTROLS ================
function createControlsRow(entryID, format) {
const row = document.createElement("div");
row.className = "mangadexpp-controls";
row.style.display = "flex";
row.style.gap = "6px";
row.style.marginTop = "6px";
// Fix: Always use flex-start for left alignment in all formats
row.style.justifyContent = "flex-start";
row.addEventListener("click", function(e) {
e.preventDefault();
e.stopPropagation();
return false;
});
function mk(label, cls, cb) {
const b = document.createElement("input");
b.type = "button";
b.value = label;
b.className = cls;
b.setAttribute("entryid", entryID);
b.style.padding = "2px 6px";
b.style.borderRadius = "3px";
b.style.cursor = "pointer";
b.style.background = "transparent";
b.style.fontSize = "14px";
b.style.minWidth = "70px";
b.style.height = "28px";
b.style.lineHeight = "24px";
b.style.boxSizing = "border-box";
b.style.whiteSpace = "nowrap";
b.style.fontFamily = "inherit";
b.style.fontWeight = "500";
b.style.border = "1px solid rgba(255, 255, 255, 0.1)";
b.style.transition = "all 0.15s ease";
b.addEventListener("click", function(e) {
e.preventDefault();
e.stopPropagation();
cb(entryID);
applyFilters();
return false;
});
// Add hover effect
b.addEventListener("mouseenter", function() {
b.style.opacity = "0.9";
b.style.transform = "translateY(-1px)";
b.style.boxShadow = "0 2px 4px rgba(0,0,0,0.2)";
});
b.addEventListener("mouseleave", function() {
b.style.opacity = "1";
b.style.transform = "translateY(0)";
b.style.boxShadow = "none";
});
return b;
}
row.appendChild(mk("Read", "mangadexpp-read", function(id) { localStorage.setItem(id, "1"); }));
row.appendChild(mk("Ignore", "mangadexpp-ignore", function(id) { localStorage.setItem(id, "-1"); }));
row.appendChild(mk("Clear", "mangadexpp-clear", function(id) { localStorage.removeItem(id); }));
return row;
}
function insertControlsForMangaCard(card, format) {
try {
// Find the title link in the manga card
const titleLink = card.querySelector("a[href*='/title/']");
if (!titleLink) return;
const href = titleLink.getAttribute("href") || titleLink.href || "";
const id = extractIdFromHref(href);
if (!id) return;
// Check if controls already exist
if (card.querySelector(".mangadexpp-controls input[entryid='" + id + "']")) return;
// Find where to insert controls - look for status or body
const status = card.querySelector(".status");
const body = card.querySelector(".manga-card-body");
const controls = createControlsRow(id, format);
if (status) {
// Insert after status
status.parentNode.insertBefore(controls, status.nextSibling);
} else if (body) {
// Append to body
body.appendChild(controls);
} else {
// Append to card as fallback
card.appendChild(controls);
}
} catch (e) {
console.debug("Control insertion failed for manga card:", e);
}
}
function insertControlsForFeedContainer(container, format) {
try {
const titleLink = container.querySelector(".chapter-feed__title");
if (!titleLink) return;
const href = titleLink.getAttribute("href") || titleLink.href || "";
const id = extractIdFromHref(href);
if (!id) return;
if (container.querySelector(".mangadexpp-controls input[entryid='" + id + "']")) return;
const titleContainer = titleLink.parentElement;
const controls = createControlsRow(id, format);
// For feed format, insert after the title
if (titleContainer) {
titleContainer.appendChild(controls);
} else {
container.appendChild(controls);
}
} catch (e) {
console.debug("Control insertion failed for feed:", e);
}
}
function addControlsToAll(format) {
// Handle manga cards (thumbnail format)
if (format === FORMAT_THUMBNAIL) {
document.querySelectorAll(".manga-card, .md-card, .group.md-card").forEach(function(card) {
insertControlsForMangaCard(card, format);
});
}
// Handle feed containers (list format)
else if (format === FORMAT_LIST) {
document.querySelectorAll(".chapter-feed__container").forEach(function(container) {
insertControlsForFeedContainer(container, format);
});
}
}
// ================ FEED UNREAD DETECTION ================
function hasUnreadChaptersInFeedContainer(container) {
const list = container.querySelector(".chapter-feed__chapters-list");
if (!list) return null;
const unread = list.querySelector(".readMarker:not(.opacity-40)");
return !!unread;
}
function hideAllReadFeed() {
if (!DOES_HIDE_ALL_READ) return;
document.querySele...