Skip to content

Releases: lordmage/Manga-Dex-userscript

Release list

1.0.7

Choose a tag to compare

@lordmage lordmage released this 12 Dec 00:07
8f0422d

// ==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...

Read more