// ==UserScript==
// @name MangaDex Customizer
// @namespace https://github.com/rRoler/UserScripts
// @version 1.0.2
// @description Customize MangaDex title pages by adding custom alt titles, changing the main title and cover, and adding custom tags\links. All data is stored inside userscript storage.
// @author Roler
// @icon https://www.google.com/s2/favicons?sz=64&domain=mangadex.org
// @match https://mangadex.org/*
// @match https://canary.mangadex.dev/*
// @match https://demo.komga.org/*
// @supportURL https://github.com/rRoler/UserScripts/issues
// @require https://cdnjs.cloudflare.com/ajax/libs/validator/13.12.0/validator.min.js#sha256-d2c75e3159ceac9c14dcc8a7aeb09ea30970de6c321c89070e5b0157842c5c88
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
const userScriptId = `mdc-${crypto.randomUUID()}`;
const storage = {
mangadex: {
titles: {
custom_sections: {
id: 'mangadex_titles_custom_sections',
defaultValue: 'array'
},
data: {
id: 'mangadex_titles_data',
defaultValue: 'object',
custom_sections: {
id: 'custom_sections',
defaultValue: 'array'
},
alt_titles: {
id: 'alt_titles',
defaultValue: 'array'
},
main_title: {
id: 'main_title',
defaultValue: 'string'
},
main_cover: {
id: 'main_cover',
defaultValue: 'string'
}
},
}
}
};
const createStorageDefaultValue = (type) => {
switch (type) {
case 'array':
return [];
case 'object':
return {};
default:
return '';
}
};
const getStorage = (section) => GM_getValue(section.id, createStorageDefaultValue(section.defaultValue));
const setStorage = (section, value) => GM_setValue(section.id, value);
const isMd = /^mangadex\.org|canary\.mangadex\.dev$/.test(window.location.hostname);
const mdTitleOptions = {
altTitle: {
add: mdAddAltTitleOptions
},
customSection: {
add: mdAddCustomSectionOptions
},
volumeCover: {
add: mdAddVolumeCoverOptions,
tab: 'art',
dynamic: true
}
};
const mdGetTitleStorage = (titleId, section) => {
const storedData = getStorage(storage.mangadex.titles.data);
return storedData[titleId] && storedData[titleId][section.id] || createStorageDefaultValue(section.defaultValue);
}
const mdSetTitleStorage = (titleId, section, value, del = false, append = false) => {
const storedData = getStorage(storage.mangadex.titles.data);
if (!storedData[titleId]) storedData[titleId] = {};
if (append) {
if (!storedData[titleId][section.id]) storedData[titleId][section.id] = [];
if (del) {
const index = storedData[titleId][section.id].indexOf(value);
if (index > -1) storedData[titleId][section.id].splice(index, 1);
} else {
storedData[titleId][section.id].push(value);
}
} else {
if (del) delete storedData[titleId][section.id];
else storedData[titleId][section.id] = value;
}
try {
if (storedData[titleId][section.id] && Object.keys(storedData[titleId][section.id]).length < 1)
delete storedData[titleId][section.id];
if (Object.keys(storedData[titleId]).length < 1)
delete storedData[titleId];
} catch (e) {}
setStorage(storage.mangadex.titles.data, storedData);
}
const mdGetTitleId = (url = window.location.pathname) => {
const titleIdMatch = url.match(/\/(?:title|manga|covers)\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/);
return titleIdMatch && titleIdMatch[1];
}
const mdGetCoverFileName = (url) => {
const fileNameMatch = url.match(/\/covers\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.[A-Za-z]+)(\.[0-9]+\.[A-Za-z]+)?/);
return {
fileName: fileNameMatch && fileNameMatch[1],
size: fileNameMatch && fileNameMatch[2]
}
}
const mdGetAltTitlesSectionElement = (infoElement) => {
const fullWidthSections = infoElement.querySelectorAll('.w-full');
return Array.from(fullWidthSections).find(section => section.querySelector('.alt-title'));
}
const mdGetInfoElement = (titleId) => {
let infoElement = document.querySelector('.flex.flex-wrap.gap-x-4.gap-y-2');
if (!infoElement) return;
infoElement = window.getComputedStyle(infoElement).display === 'none' ? document.querySelector(`[id="${titleId}"]`) : infoElement;
if (!infoElement) return;
return infoElement;
}
const komgaGetSeriesId = (url = window.location.pathname) => {
const seriesIdMatch = url.match(/\/series\/([0-9A-Za-z]+)/);
return seriesIdMatch && seriesIdMatch[1];
}
let mdTitleOptionsLoaded = false;
let komgaCurrentSeriesId;
let scriptErrored = false;
observeElement(async (mutations, observer) => {
if (scriptErrored) {
observer.disconnect();
alert('The MangaDex Customizer userscript has encountered an error.\nPlease reload the page or disable the userscript if this error persists.');
return;
}
if (isMd && !window.location.pathname.includes('edit')) {
if (!document.querySelector('.md-content')) return;
const titleId = mdGetTitleId();
if (titleId) {
const currentTabMatch = window.location.search.match(/tab=([a-z]+)/);
const currentTab = currentTabMatch && currentTabMatch[1] || 'chapters';
for (const optionId in mdTitleOptions) {
const option = mdTitleOptions[optionId];
if (!option.tab || option.tab === currentTab) {
if (option.dynamic || !option.loaded || option.loadedId !== titleId || option.loadedTab !== currentTab) {
try {
option.loaded = option.add(titleId);
if (option.loaded) {
option.loadedId = titleId;
option.loadedTab = currentTab;
mdTitleOptionsLoaded = true;
}
} catch (e) {
console.error(e);
scriptErrored = true;
return;
}
}
}
}
} else if (mdTitleOptionsLoaded) {
for (const optionId in mdTitleOptions) {
const option = mdTitleOptions[optionId];
option.loaded = false;
option.loadedId = '';
option.loadedTab = '';
if (option.storage) delete option.storage;
}
mdTitleOptionsLoaded = false;
}
try {
mdReplaceTitles();
mdReplaceVolumeCovers(titleId);
} catch (e) {
console.error(e);
scriptErrored = true;
}
} else {
if (!document.querySelector('.container')) return;
const seriesId = komgaGetSeriesId();
if (seriesId) {
if (seriesId === komgaCurrentSeriesId) return;
try {
if (komgaAutoMatch(seriesId)) komgaCurrentSeriesId = seriesId;
} catch (e) {
console.error(e);
scriptErrored = true;
}
} else {
komgaCurrentSeriesId = '';
}
}
});
function mdAddCustomSectionOptions(titleId) {
const infoElement = mdGetInfoElement(titleId);
if (!infoElement) return false;
const infoSectionElement = infoElement.querySelector('.mb-2:not(.hidden)');
if (!infoSectionElement) return false;
const sectionInfoElement = infoSectionElement.querySelector('div.flex.flex-wrap');
if (!sectionInfoElement) return false;
const sectionInfoLinkElement = sectionInfoElement.querySelector('a');
if (!sectionInfoLinkElement) return false;
const altTitlesSectionElement = mdGetAltTitlesSectionElement(infoElement);
if (!altTitlesSectionElement) return false;
const createSectionElement = (sectionData, required = false) => {
const sectionIdAttribute = `${userScriptId}-section-id`;
const sectionExists = !!document.querySelector(`[${sectionIdAttribute}="${sectionData.id}"]`);
if (sectionExists) return;
const newInfoSectionElement = infoSectionElement.cloneNode(true);
newInfoSectionElement.setAttribute(sectionIdAttribute, sectionData.id);
const newInfoNameElement = newInfoSectionElement.querySelector('div.font-bold');
const newInfoElement = newInfoSectionElement.querySelector('div.flex.flex-wrap');
newInfoNameElement.textContent = sectionData.name + (required ? '' : ' ');
newInfoElement.querySelectorAll('a').forEach(element => element.remove());
if (required) return newInfoSectionElement;
const newInfoRemoveElement = document.createElement('span');
newInfoRemoveElement.textContent = '⨯';
newInfoRemoveElement.classList.add('cursor-pointer');
newInfoRemoveElement.addEventListener('click', () => {
if (!confirm(`Are you sure you want to delete this section?\n\n${sectionData.name}`)) return;
const storedSections = getStorage(storage.mangadex.titles.custom_sections);
const storedSectionIndex = storedSections.findIndex(section => section.id === sectionData.id);
if (storedSectionIndex > -1) {
storedSections.splice(storedSectionIndex, 1);
setStorage(storage.mangadex.titles.custom_sections, storedSections);
}
newInfoSectionElement.remove();
});
newInfoNameElement.appendChild(newInfoRemoveElement);
return newInfoSectionElement;
}
const createSectionButton = (sectionData, value, sectionInfoLinkElement) => {
const newLink = sectionInfoLinkElement.cloneNode(true);
const newLinkText = newLink.querySelector('span');
newLink.href = '#';
newLink.classList.add('gap-1');
newLinkText.textContent = value;
const newLinkRemove = document.createElement('span');
newLinkRemove.textContent = '⨯';
newLinkRemove.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
if (!confirm(`Are you sure you want to delete this ${sectionData.name}?\n\n${value}`)) return;
const storedTitleSections = mdGetTitleStorage(titleId, storage.mangadex.titles.data.custom_sections);
const storedSectionIndex = storedTitleSections.findIndex(section => section.id === sectionData.id);
if (storedSectionIndex > -1) {
const sectionValues = storedTitleSections[storedSectionIndex].values || [];
const sectionValueIndex = sectionValues.findIndex(_value => _value === value);
if (sectionValueIndex > -1) {
sectionValues.splice(sectionValueIndex, 1);
storedTitleSections[storedSectionIndex].values = sectionValues;
}
if (sectionValues.length < 1) {
storedTitleSections.splice(storedSectionIndex, 1);
}
}
mdSetTitleStorage(titleId, storage.mangadex.titles.data.custom_sections, storedTitleSections);
newLink.remove();
});
newLink.appendChild(newLinkRemove);
try {
const valueMatch = value.match(/^\[([\s\w\-]+)]\((https?:\/\/.*)\)$/)
const urlValue = valueMatch && valueMatch[2] ? valueMatch[2] : value
if (!validator.isURL(urlValue)) throw new Error('Invalid URL');
const url = new URL(urlValue);
newLink.href = url.href;
newLinkText.textContent = valueMatch && valueMatch[2] ? valueMatch[1] : url.hostname;
return newLink;
} catch (e) {}
newLink.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
alert(value);
});
return newLink;
};
const createSectionLink = (sectionData, sectionElement) => {
const newInfoElement = sectionElement.querySelector('div.flex.flex-wrap');
const newInfoLinkElement = sectionInfoLinkElement.cloneNode(true);
const newInfoLinkIconElement = newInfoLinkElement.querySelector('svg');
const newInfoLinkTextElement = newInfoLinkElement.querySelector('span');
newInfoLinkElement.target = '_blank';
newInfoLinkElement.rel = 'noopener noreferrer';
if (newInfoLinkIconElement) newInfoLinkIconElement.remove();
const storedTitleSections = mdGetTitleStorage(titleId, storage.mangadex.titles.data.custom_sections);
const storedSectionData = storedTitleSections.find(section => section.id === sectionData.id) || {};
const storedSectionDataValues = storedSectionData.values || [];
storedSectionDataValues.forEach(value => {
const newLink = createSectionButton(sectionData, value, newInfoLinkElement);
if (!newLink) return;
newInfoElement.appendChild(newLink);
});
newInfoLinkTextElement.textContent = `+`;
newInfoLinkElement.href = '#';
newInfoLinkElement.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const storedSections = getStorage(storage.mangadex.titles.custom_sections);
if (!storedSections.some(section => section.id === sectionData.id)) {
storedSections.push(sectionData);
setStorage(storage.mangadex.titles.custom_sections, storedSections);
}
const value = prompt(`Enter new ${sectionData.name} value`);
if (!value) return;
const newLink = createSectionButton(sectionData, value, newInfoLinkElement);
if (!newLink) return;
const storedTitleSections = mdGetTitleStorage(titleId, storage.mangadex.titles.data.custom_sections);
let storedSectionIndex = storedTitleSections.findIndex(section => section.id === sectionData.id);
if (storedSectionIndex < 0) {
storedTitleSections.push({ id: sectionData.id });
storedSectionIndex = storedTitleSections.findIndex(section => section.id === sectionData.id);
}
const sectionValues = storedTitleSections[storedSectionIndex].values || [];
sectionValues.push(value);
storedTitleSections[storedSectionIndex].values = sectionValues;
mdSetTitleStorage(titleId, storage.mangadex.titles.data.custom_sections, storedTitleSections);
newInfoElement.insertBefore(newLink, newInfoLinkElement);
});
newInfoElement.appendChild(newInfoLinkElement);
return newInfoElement;
}
const createSection = (sectionData) => {
const newSectionElement = createSectionElement(sectionData);
if (!newSectionElement) return;
const newSectionLinkElement = createSectionLink(sectionData, newSectionElement);
newSectionElement.appendChild(newSectionLinkElement);
infoElement.insertBefore(newSectionElement, altTitlesSectionElement);
}
const addNewSectionElement = createSectionElement({ id: 'add_local_section', name: 'Custom Sections +' }, true);
if (addNewSectionElement) {
addNewSectionElement.querySelector('div.flex.flex-wrap').remove();
const addNewSectionTextElement = addNewSectionElement.querySelector('div.font-bold');
addNewSectionTextElement.classList.remove('mb-2');
addNewSectionTextElement.classList.add('cursor-pointer');
addNewSectionTextElement.style.setProperty('width', 'fit-content');
addNewSectionElement.classList.remove('mb-2');
addNewSectionElement.classList.add('w-full');
addNewSectionTextElement.addEventListener('click', () => {
const storedSections = getStorage(storage.mangadex.titles.custom_sections);
const sectionName = prompt('Enter new section name');
const trimmedSectionName = sectionName && sectionName.trim();
if (!trimmedSectionName) return;
const sectionData = {
id: trimmedSectionName.replace(/\s/g, '_').toLowerCase(),
name: trimmedSectionName
}
if (storedSections.some(section => section.id === sectionData.id)) return;
storedSections.push(sectionData);
setStorage(storage.mangadex.titles.custom_sections, storedSections);
createSection(sectionData);
});
infoElement.insertBefore(addNewSectionElement, altTitlesSectionElement);
}
const storedSections = getStorage(storage.mangadex.titles.custom_sections);
storedSections.forEach(createSection);
return true;
}
function mdAddAltTitleOptions(titleId) {
const infoElement = mdGetInfoElement(titleId);
if (!infoElement) return false;
if (!infoElement.querySelector('a')) return false;
let altTitlesSectionElement = mdGetAltTitlesSectionElement(infoElement);
if (!altTitlesSectionElement) {
altTitlesSectionElement = document.createElement('div');
altTitlesSectionElement.classList.add('w-full');
infoElement.appendChild(altTitlesSectionElement);
const altTitlesSectionTextElement = document.createElement('div');
altTitlesSectionTextElement.classList.add('font-bold', 'mb-1');
altTitlesSectionTextElement.textContent = 'Alternative Titles';
altTitlesSectionElement.appendChild(altTitlesSectionTextElement);
const altTitleElement = document.createElement('div');
altTitleElement.classList.add('mb-1', 'flex', 'gap-x-2', 'alt-title');
altTitlesSectionElement.appendChild(altTitleElement);
}
const altTitlesSectionLoadedAttribute = `${userScriptId}-alt-title-section-loaded`;
if (altTitlesSectionElement.hasAttribute(altTitlesSectionLoadedAttribute)) return true;
altTitlesSectionElement.setAttribute(altTitlesSectionLoadedAttribute, 'true');
const altTitlesSectionTextElement = altTitlesSectionElement.querySelector('div.font-bold');
const altTitlesElements = altTitlesSectionElement.querySelectorAll('.alt-title');
const altTitleElement = altTitlesElements[0].cloneNode(true);
if (!mdTitleOptions.altTitle.storage) mdTitleOptions.altTitle.storage = [];
const addAltTitleStar = altTitleElement => {
const storedTitle = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_title);
const altTitleTextElement = altTitleElement.querySelector('span');
if (!altTitleTextElement) return;
const setTitleObject = {
selected: storedTitle === altTitleTextElement.textContent,
element: altTitleElement,
starElement: document.createElement('span'),
value: altTitleTextElement.textContent
}
setTitleObject.starElement.textContent = setTitleObject.selected ? '★' : '☆';
setTitleObject.starElement.classList.add('cursor-pointer');
if (setTitleObject.selected) mdReplaceTitles(titleId);
setTitleObject.starElement.addEventListener('click', () => {
mdSetTitleStorage(titleId, storage.mangadex.titles.data.main_title, setTitleObject.value, setTitleObject.selected);
mdReplaceTitles(titleId, setTitleObject.selected);
setTitleObject.selected = !setTitleObject.selected;
mdTitleOptions.altTitle.storage.forEach(_setTitleObject => {
_setTitleObject.selected = _setTitleObject.value === setTitleObject.value && setTitleObject.selected;
_setTitleObject.starElement.textContent = _setTitleObject.selected ? '★' : '☆';
});
});
mdTitleOptions.altTitle.storage.push(setTitleObject);
altTitleElement.prepend(setTitleObject.starElement);
};
const createAltTitle = (value) => {
if (!altTitlesElements[0].querySelector('span')) altTitlesElements[0].remove();
const newAltTitleElement = altTitleElement.cloneNode(true);
const newAltTitleIconElement = newAltTitleElement.querySelector('div');
let newAltTitleTextElement = newAltTitleElement.querySelector('span');
if (!newAltTitleTextElement) {
newAltTitleTextElement = document.createElement('span');
newAltTitleElement.appendChild(newAltTitleTextElement);
}
const removeCustomAltTitleElement = document.createElement('span');
if (newAltTitleIconElement) newAltTitleIconElement.remove();
newAltTitleTextElement.textContent = value;
removeCustomAltTitleElement.textContent = '⨯';
removeCustomAltTitleElement.classList.add('cursor-pointer');
removeCustomAltTitleElement.addEventListener('click', () => {
if (!confirm(`Are you sure you want to delete this title?\n\n${value}`)) return;
mdSetTitleStorage(titleId, storage.mangadex.titles.data.alt_titles, value, true, true);
const setTitleObjectIndex = mdTitleOptions.altTitle.storage.findIndex(setTitleObject => setTitleObject.value === value);
if (setTitleObjectIndex > -1) mdTitleOptions.altTitle.storage.splice(setTitleObjectIndex, 1);
const storedAltTitles = mdGetTitleStorage(titleId, storage.mangadex.titles.data.alt_titles);
const storedMainTitle = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_title);
if (storedMainTitle === value && !storedAltTitles.some(altTitle => altTitle === value)) {
mdSetTitleStorage(titleId, storage.mangadex.titles.data.main_title, value, true);
mdReplaceTitles(titleId, true);
}
newAltTitleElement.remove();
});
newAltTitleElement.appendChild(removeCustomAltTitleElement);
addAltTitleStar(newAltTitleElement);
altTitlesSectionElement.appendChild(newAltTitleElement);
};
altTitlesElements.forEach(addAltTitleStar);
altTitlesSectionTextElement.textContent = `${altTitlesSectionTextElement.textContent} +`
altTitlesSectionTextElement.classList.add('cursor-pointer');
altTitlesSectionTextElement.style.setProperty('width', 'fit-content');
altTitlesSectionTextElement.addEventListener('click', () => {
const value = prompt('Enter new title');
if (!value) return;
mdSetTitleStorage(titleId, storage.mangadex.titles.data.alt_titles, value, false, true);
createAltTitle(value);
});
const storedAltTitles = mdGetTitleStorage(titleId, storage.mangadex.titles.data.alt_titles);
if (storedAltTitles) storedAltTitles.forEach(createAltTitle);
const storedTitle = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_title);
if (storedTitle && !mdTitleOptions.altTitle.storage.some(setTitleObject => setTitleObject.selected)) {
mdSetTitleStorage(titleId, storage.mangadex.titles.data.alt_titles, storedTitle, false, true);
createAltTitle(storedTitle);
}
return true;
}
function mdReplaceTitles(titleId, useDefaultTitle) {
if (titleId) {
const titlePageTitleElement = document.querySelector('div.title > p');
if (!titlePageTitleElement) return;
const defaultTitleAttribute = `${userScriptId}-default-title`;
if (!titlePageTitleElement.hasAttribute(defaultTitleAttribute))
titlePageTitleElement.setAttribute(defaultTitleAttribute, titlePageTitleElement.textContent);
const defaultTitle = useDefaultTitle && titlePageTitleElement.getAttribute(defaultTitleAttribute);
const storedMainTitle = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_title);
titlePageTitleElement.textContent = defaultTitle || storedMainTitle || 'undefined';
return;
}
const titleLinkElements = document.querySelectorAll(
'a.title, a.chapter-feed__title, .dense-manga-container a, .swiper-slide a, .manga-draft-container a, a[class=""]'
);
titleLinkElements.forEach(titleLinkElement => {
const titleReplacedAttribute = `${userScriptId}-title-replaced`;
if (titleLinkElement.hasAttribute(titleReplacedAttribute)) return;
titleLinkElement.setAttribute(titleReplacedAttribute, 'true');
let textElement = titleLinkElement;
const hasTextNode = () => textElement && textElement.childNodes && Array.from(textElement.childNodes).some(text => text.data);
if (!hasTextNode()) textElement = titleLinkElement.querySelector('span, h6');
if (!hasTextNode() && titleLinkElement.parentElement)
textElement = titleLinkElement.parentElement.querySelector('span, h2, div.font-bold');
if (!hasTextNode()) return;
if (textElement.parentElement && textElement.parentElement.tagName === 'BUTTON') return;
const mdTitleId = mdGetTitleId(titleLinkElement.getAttribute('href'));
if (!mdTitleId) return;
const storedMainTitle = mdGetTitleStorage(mdTitleId, storage.mangadex.titles.data.main_title);
if (!storedMainTitle) return;
textElement.childNodes.forEach((text) => {
if (text.data) text.data = storedMainTitle;
});
});
}
function mdAddVolumeCoverOptions(titleId) {
if (document.querySelector('div[role="alert"]')) return true;
if (document.querySelectorAll(`a[href*="covers/${titleId}"]`).length < 2) return false;
const volumeCoverLoadedAttribute = `${userScriptId}-volume-cover-loaded`;
const volumeCoverLinkElements = document.querySelectorAll(`a[href*="covers/${titleId}"]:not([${volumeCoverLoadedAttribute}])`);
if (!mdTitleOptions.volumeCover.storage) mdTitleOptions.volumeCover.storage = [];
volumeCoverLinkElements.forEach(volumeCoverLinkElement => {
volumeCoverLinkElement.setAttribute(volumeCoverLoadedAttribute, 'true');
const volumeSubtitleElement = volumeCoverLinkElement.querySelector('.subtitle');
if (!volumeSubtitleElement) return;
volumeSubtitleElement.textContent = ` ${volumeSubtitleElement.textContent}`;
const volumeCoverLink = volumeCoverLinkElement.getAttribute('href');
if (!volumeCoverLink) return;
const volumeCoverFilename = mdGetCoverFileName(volumeCoverLink);
if (!volumeCoverFilename.fileName) return;
const storedVolumeCover = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_cover);
const setCoverObject = {
selected: volumeCoverFilename.fileName === storedVolumeCover,
element: volumeCoverLinkElement,
starElement: document.createElement('span'),
value: volumeCoverFilename.fileName
}
setCoverObject.starElement.textContent = setCoverObject.selected ? '★' : '☆';
setCoverObject.starElement.classList.add('cursor-pointer');
setCoverObject.starElement.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
mdSetTitleStorage(titleId, storage.mangadex.titles.data.main_cover, setCoverObject.value, setCoverObject.selected);
setCoverObject.selected = !setCoverObject.selected;
mdTitleOptions.volumeCover.storage.forEach(_setCoverObject => {
_setCoverObject.selected = _setCoverObject.value === setCoverObject.value && setCoverObject.selected;
_setCoverObject.starElement.textContent = _setCoverObject.selected ? '★' : '☆';
});
mdReplaceVolumeCovers(titleId, !setCoverObject.selected);
});
volumeSubtitleElement.prepend(setCoverObject.starElement);
mdTitleOptions.volumeCover.storage.push(setCoverObject);
});
return true;
}
function mdReplaceVolumeCovers(titleId, useDefault) {
const coverLinkElement = document.querySelector(`.md-content > .manga a[href*="covers/${titleId}"]`);
const replaceCoverUrl = (titleId, urlToReplace, storedCover) => {
if (!titleId || !storedCover) return;
const urlToReplaceFilename = mdGetCoverFileName(urlToReplace);
if (!urlToReplaceFilename.size) urlToReplaceFilename.size = '';
const newUrl = `https://mangadex.org/covers/${titleId}/${storedCover}${urlToReplaceFilename.size}`;
if (newUrl !== urlToReplace) return newUrl;
}
if (coverLinkElement) {
const defaultCoverAttribute = `${userScriptId}-default-cover`;
if (!coverLinkElement.hasAttribute(defaultCoverAttribute)) {
const coverLinkFileName = mdGetCoverFileName(coverLinkElement.getAttribute('href'));
if (coverLinkFileName.fileName) coverLinkElement.setAttribute(defaultCoverAttribute, coverLinkFileName.fileName);
}
const storedCover = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_cover);
const defaultCover = useDefault && coverLinkElement.getAttribute(defaultCoverAttribute);
const newCover = defaultCover || storedCover;
if (newCover) {
const newCoverLinkUrl = replaceCoverUrl(titleId, coverLinkElement.getAttribute('href'), newCover);
if (newCoverLinkUrl) coverLinkElement.setAttribute('href', newCoverLinkUrl);
const coverImageElement = coverLinkElement.querySelector(`img[src*="covers/${titleId}"]`);
if (coverImageElement) {
const newCoverImageUrl = replaceCoverUrl(titleId, coverImageElement.getAttribute('src'), newCover);
if (newCoverImageUrl) coverImageElement.setAttribute('src', newCoverImageUrl);
}
const bannerImageElement = document.querySelector('.banner-image');
if (bannerImageElement) {
const newBannerImageUrl = replaceCoverUrl(titleId, bannerImageElement.style.getPropertyValue('background-image'), newCover);
if (newBannerImageUrl) bannerImageElement.style.setProperty('background-image', `url("${newBannerImageUrl}")`);
}
}
}
const coverLoadedAttribute = `${userScriptId}-cover-loaded`;
const imageElements = document.querySelectorAll(`img:not([${coverLoadedAttribute}])`);
imageElements.forEach(imageElement => {
imageElement.setAttribute(coverLoadedAttribute, 'true');
const imageUrl = imageElement.getAttribute('src');
if (!imageUrl) return;
const mdTitleId = mdGetTitleId(imageUrl);
if (!mdTitleId || mdTitleId === titleId) return;
const storedCover = mdGetTitleStorage(mdTitleId, storage.mangadex.titles.data.main_cover);
const newCoverUrl = replaceCoverUrl(mdTitleId, imageUrl, storedCover);
if (newCoverUrl) imageElement.setAttribute('src', newCoverUrl);
});
}
function komgaAutoMatch(seriesId) {
if (!document.querySelector(`.v-image__image[style*="${seriesId}"]`)) return false;
const linkElements = document.querySelectorAll(`a.v-chip--link`);
if (linkElements < 1) return false;
const sectionData = {
id: 'local_links',
name: 'Local Links'
}
const storedSections = getStorage(storage.mangadex.titles.custom_sections);
if (!storedSections.some(section => section.id === sectionData.id)) {
storedSections.push(sectionData);
setStorage(storage.mangadex.titles.custom_sections, storedSections);
}
linkElements.forEach(link => {
const mdTitleId = mdGetTitleId(link.href);
if (!mdTitleId) return;
const storedTitleSections = mdGetTitleStorage(mdTitleId, storage.mangadex.titles.data.custom_sections);
let storedSectionIndex = storedTitleSections.findIndex(section => section.id === sectionData.id);
if (storedSectionIndex < 0) {
storedTitleSections.push({ id: sectionData.id });
storedSectionIndex = storedTitleSections.findIndex(section => section.id === sectionData.id);
}
const sectionValues = storedTitleSections[storedSectionIndex].values || [];
if (sectionValues.some(link => seriesId === komgaGetSeriesId(link))) return;
const sectionLink = `[Komga](${window.location.href.replace(/\?.*$/, '')})`;
sectionValues.push(sectionLink);
storedTitleSections[storedSectionIndex].values = sectionValues;
mdSetTitleStorage(mdTitleId, storage.mangadex.titles.data.custom_sections, storedTitleSections);
});
return true;
}
function observeElement(onChange, element = document.body) {
const observer = new MutationObserver(onChange);
onChange();
observer.observe(element, {
childList: true,
subtree: true,
});
}
})();