MangaDex Customizer

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.

  1. // ==UserScript==
  2. // @name MangaDex Customizer
  3. // @namespace https://github.com/rRoler/UserScripts
  4. // @version 1.0.2
  5. // @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.
  6. // @author Roler
  7. // @icon https://www.google.com/s2/favicons?sz=64&domain=mangadex.org
  8. // @match https://mangadex.org/*
  9. // @match https://canary.mangadex.dev/*
  10. // @match https://demo.komga.org/*
  11. // @supportURL https://github.com/rRoler/UserScripts/issues
  12. // @require https://cdnjs.cloudflare.com/ajax/libs/validator/13.12.0/validator.min.js#sha256-d2c75e3159ceac9c14dcc8a7aeb09ea30970de6c321c89070e5b0157842c5c88
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @run-at document-end
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. const userScriptId = `mdc-${crypto.randomUUID()}`;
  22.  
  23. const storage = {
  24. mangadex: {
  25. titles: {
  26. custom_sections: {
  27. id: 'mangadex_titles_custom_sections',
  28. defaultValue: 'array'
  29. },
  30. data: {
  31. id: 'mangadex_titles_data',
  32. defaultValue: 'object',
  33. custom_sections: {
  34. id: 'custom_sections',
  35. defaultValue: 'array'
  36. },
  37. alt_titles: {
  38. id: 'alt_titles',
  39. defaultValue: 'array'
  40. },
  41. main_title: {
  42. id: 'main_title',
  43. defaultValue: 'string'
  44. },
  45. main_cover: {
  46. id: 'main_cover',
  47. defaultValue: 'string'
  48. }
  49. },
  50. }
  51. }
  52. };
  53.  
  54. const createStorageDefaultValue = (type) => {
  55. switch (type) {
  56. case 'array':
  57. return [];
  58. case 'object':
  59. return {};
  60. default:
  61. return '';
  62. }
  63. };
  64. const getStorage = (section) => GM_getValue(section.id, createStorageDefaultValue(section.defaultValue));
  65. const setStorage = (section, value) => GM_setValue(section.id, value);
  66.  
  67. const isMd = /^mangadex\.org|canary\.mangadex\.dev$/.test(window.location.hostname);
  68. const mdTitleOptions = {
  69. altTitle: {
  70. add: mdAddAltTitleOptions
  71. },
  72. customSection: {
  73. add: mdAddCustomSectionOptions
  74. },
  75. volumeCover: {
  76. add: mdAddVolumeCoverOptions,
  77. tab: 'art',
  78. dynamic: true
  79. }
  80. };
  81.  
  82. const mdGetTitleStorage = (titleId, section) => {
  83. const storedData = getStorage(storage.mangadex.titles.data);
  84. return storedData[titleId] && storedData[titleId][section.id] || createStorageDefaultValue(section.defaultValue);
  85. }
  86. const mdSetTitleStorage = (titleId, section, value, del = false, append = false) => {
  87. const storedData = getStorage(storage.mangadex.titles.data);
  88. if (!storedData[titleId]) storedData[titleId] = {};
  89.  
  90. if (append) {
  91. if (!storedData[titleId][section.id]) storedData[titleId][section.id] = [];
  92. if (del) {
  93. const index = storedData[titleId][section.id].indexOf(value);
  94. if (index > -1) storedData[titleId][section.id].splice(index, 1);
  95. } else {
  96. storedData[titleId][section.id].push(value);
  97. }
  98. } else {
  99. if (del) delete storedData[titleId][section.id];
  100. else storedData[titleId][section.id] = value;
  101. }
  102.  
  103. try {
  104. if (storedData[titleId][section.id] && Object.keys(storedData[titleId][section.id]).length < 1)
  105. delete storedData[titleId][section.id];
  106. if (Object.keys(storedData[titleId]).length < 1)
  107. delete storedData[titleId];
  108. } catch (e) {}
  109.  
  110. setStorage(storage.mangadex.titles.data, storedData);
  111. }
  112.  
  113. const mdGetTitleId = (url = window.location.pathname) => {
  114. 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})/);
  115. return titleIdMatch && titleIdMatch[1];
  116. }
  117.  
  118. const mdGetCoverFileName = (url) => {
  119. 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]+)?/);
  120. return {
  121. fileName: fileNameMatch && fileNameMatch[1],
  122. size: fileNameMatch && fileNameMatch[2]
  123. }
  124. }
  125.  
  126. const mdGetAltTitlesSectionElement = (infoElement) => {
  127. const fullWidthSections = infoElement.querySelectorAll('.w-full');
  128. return Array.from(fullWidthSections).find(section => section.querySelector('.alt-title'));
  129. }
  130.  
  131. const mdGetInfoElement = (titleId) => {
  132. let infoElement = document.querySelector('.flex.flex-wrap.gap-x-4.gap-y-2');
  133. if (!infoElement) return;
  134. infoElement = window.getComputedStyle(infoElement).display === 'none' ? document.querySelector(`[id="${titleId}"]`) : infoElement;
  135. if (!infoElement) return;
  136. return infoElement;
  137. }
  138.  
  139. const komgaGetSeriesId = (url = window.location.pathname) => {
  140. const seriesIdMatch = url.match(/\/series\/([0-9A-Za-z]+)/);
  141. return seriesIdMatch && seriesIdMatch[1];
  142. }
  143.  
  144. let mdTitleOptionsLoaded = false;
  145. let komgaCurrentSeriesId;
  146. let scriptErrored = false;
  147. observeElement(async (mutations, observer) => {
  148. if (scriptErrored) {
  149. observer.disconnect();
  150. alert('The MangaDex Customizer userscript has encountered an error.\nPlease reload the page or disable the userscript if this error persists.');
  151. return;
  152. }
  153.  
  154. if (isMd && !window.location.pathname.includes('edit')) {
  155. if (!document.querySelector('.md-content')) return;
  156.  
  157. const titleId = mdGetTitleId();
  158.  
  159. if (titleId) {
  160. const currentTabMatch = window.location.search.match(/tab=([a-z]+)/);
  161. const currentTab = currentTabMatch && currentTabMatch[1] || 'chapters';
  162.  
  163. for (const optionId in mdTitleOptions) {
  164. const option = mdTitleOptions[optionId];
  165.  
  166. if (!option.tab || option.tab === currentTab) {
  167. if (option.dynamic || !option.loaded || option.loadedId !== titleId || option.loadedTab !== currentTab) {
  168. try {
  169. option.loaded = option.add(titleId);
  170. if (option.loaded) {
  171. option.loadedId = titleId;
  172. option.loadedTab = currentTab;
  173. mdTitleOptionsLoaded = true;
  174. }
  175. } catch (e) {
  176. console.error(e);
  177. scriptErrored = true;
  178. return;
  179. }
  180. }
  181. }
  182. }
  183. } else if (mdTitleOptionsLoaded) {
  184. for (const optionId in mdTitleOptions) {
  185. const option = mdTitleOptions[optionId];
  186.  
  187. option.loaded = false;
  188. option.loadedId = '';
  189. option.loadedTab = '';
  190. if (option.storage) delete option.storage;
  191. }
  192. mdTitleOptionsLoaded = false;
  193. }
  194.  
  195. try {
  196. mdReplaceTitles();
  197. mdReplaceVolumeCovers(titleId);
  198. } catch (e) {
  199. console.error(e);
  200. scriptErrored = true;
  201. }
  202. } else {
  203. if (!document.querySelector('.container')) return;
  204.  
  205. const seriesId = komgaGetSeriesId();
  206.  
  207. if (seriesId) {
  208. if (seriesId === komgaCurrentSeriesId) return;
  209.  
  210. try {
  211. if (komgaAutoMatch(seriesId)) komgaCurrentSeriesId = seriesId;
  212. } catch (e) {
  213. console.error(e);
  214. scriptErrored = true;
  215. }
  216. } else {
  217. komgaCurrentSeriesId = '';
  218. }
  219. }
  220. });
  221.  
  222. function mdAddCustomSectionOptions(titleId) {
  223. const infoElement = mdGetInfoElement(titleId);
  224. if (!infoElement) return false;
  225.  
  226. const infoSectionElement = infoElement.querySelector('.mb-2:not(.hidden)');
  227. if (!infoSectionElement) return false;
  228. const sectionInfoElement = infoSectionElement.querySelector('div.flex.flex-wrap');
  229. if (!sectionInfoElement) return false;
  230. const sectionInfoLinkElement = sectionInfoElement.querySelector('a');
  231. if (!sectionInfoLinkElement) return false;
  232. const altTitlesSectionElement = mdGetAltTitlesSectionElement(infoElement);
  233. if (!altTitlesSectionElement) return false;
  234.  
  235. const createSectionElement = (sectionData, required = false) => {
  236. const sectionIdAttribute = `${userScriptId}-section-id`;
  237. const sectionExists = !!document.querySelector(`[${sectionIdAttribute}="${sectionData.id}"]`);
  238. if (sectionExists) return;
  239.  
  240. const newInfoSectionElement = infoSectionElement.cloneNode(true);
  241. newInfoSectionElement.setAttribute(sectionIdAttribute, sectionData.id);
  242.  
  243. const newInfoNameElement = newInfoSectionElement.querySelector('div.font-bold');
  244. const newInfoElement = newInfoSectionElement.querySelector('div.flex.flex-wrap');
  245.  
  246. newInfoNameElement.textContent = sectionData.name + (required ? '' : ' ');
  247. newInfoElement.querySelectorAll('a').forEach(element => element.remove());
  248.  
  249. if (required) return newInfoSectionElement;
  250.  
  251. const newInfoRemoveElement = document.createElement('span');
  252. newInfoRemoveElement.textContent = '⨯';
  253. newInfoRemoveElement.classList.add('cursor-pointer');
  254. newInfoRemoveElement.addEventListener('click', () => {
  255. if (!confirm(`Are you sure you want to delete this section?\n\n${sectionData.name}`)) return;
  256.  
  257. const storedSections = getStorage(storage.mangadex.titles.custom_sections);
  258. const storedSectionIndex = storedSections.findIndex(section => section.id === sectionData.id);
  259.  
  260. if (storedSectionIndex > -1) {
  261. storedSections.splice(storedSectionIndex, 1);
  262. setStorage(storage.mangadex.titles.custom_sections, storedSections);
  263. }
  264.  
  265. newInfoSectionElement.remove();
  266. });
  267. newInfoNameElement.appendChild(newInfoRemoveElement);
  268.  
  269. return newInfoSectionElement;
  270. }
  271.  
  272. const createSectionButton = (sectionData, value, sectionInfoLinkElement) => {
  273. const newLink = sectionInfoLinkElement.cloneNode(true);
  274. const newLinkText = newLink.querySelector('span');
  275. newLink.href = '#';
  276. newLink.classList.add('gap-1');
  277. newLinkText.textContent = value;
  278.  
  279. const newLinkRemove = document.createElement('span');
  280. newLinkRemove.textContent = '⨯';
  281. newLinkRemove.addEventListener('click', (event) => {
  282. event.preventDefault();
  283. event.stopPropagation();
  284.  
  285. if (!confirm(`Are you sure you want to delete this ${sectionData.name}?\n\n${value}`)) return;
  286.  
  287. const storedTitleSections = mdGetTitleStorage(titleId, storage.mangadex.titles.data.custom_sections);
  288. const storedSectionIndex = storedTitleSections.findIndex(section => section.id === sectionData.id);
  289.  
  290. if (storedSectionIndex > -1) {
  291. const sectionValues = storedTitleSections[storedSectionIndex].values || [];
  292. const sectionValueIndex = sectionValues.findIndex(_value => _value === value);
  293. if (sectionValueIndex > -1) {
  294. sectionValues.splice(sectionValueIndex, 1);
  295. storedTitleSections[storedSectionIndex].values = sectionValues;
  296. }
  297. if (sectionValues.length < 1) {
  298. storedTitleSections.splice(storedSectionIndex, 1);
  299. }
  300. }
  301.  
  302. mdSetTitleStorage(titleId, storage.mangadex.titles.data.custom_sections, storedTitleSections);
  303. newLink.remove();
  304. });
  305. newLink.appendChild(newLinkRemove);
  306.  
  307. try {
  308. const valueMatch = value.match(/^\[([\s\w\-]+)]\((https?:\/\/.*)\)$/)
  309. const urlValue = valueMatch && valueMatch[2] ? valueMatch[2] : value
  310. if (!validator.isURL(urlValue)) throw new Error('Invalid URL');
  311. const url = new URL(urlValue);
  312. newLink.href = url.href;
  313. newLinkText.textContent = valueMatch && valueMatch[2] ? valueMatch[1] : url.hostname;
  314. return newLink;
  315. } catch (e) {}
  316.  
  317. newLink.addEventListener('click', (event) => {
  318. event.preventDefault();
  319. event.stopPropagation();
  320.  
  321. alert(value);
  322. });
  323.  
  324. return newLink;
  325. };
  326.  
  327. const createSectionLink = (sectionData, sectionElement) => {
  328. const newInfoElement = sectionElement.querySelector('div.flex.flex-wrap');
  329. const newInfoLinkElement = sectionInfoLinkElement.cloneNode(true);
  330. const newInfoLinkIconElement = newInfoLinkElement.querySelector('svg');
  331. const newInfoLinkTextElement = newInfoLinkElement.querySelector('span');
  332.  
  333. newInfoLinkElement.target = '_blank';
  334. newInfoLinkElement.rel = 'noopener noreferrer';
  335. if (newInfoLinkIconElement) newInfoLinkIconElement.remove();
  336.  
  337. const storedTitleSections = mdGetTitleStorage(titleId, storage.mangadex.titles.data.custom_sections);
  338. const storedSectionData = storedTitleSections.find(section => section.id === sectionData.id) || {};
  339. const storedSectionDataValues = storedSectionData.values || [];
  340.  
  341. storedSectionDataValues.forEach(value => {
  342. const newLink = createSectionButton(sectionData, value, newInfoLinkElement);
  343. if (!newLink) return;
  344. newInfoElement.appendChild(newLink);
  345. });
  346.  
  347. newInfoLinkTextElement.textContent = `+`;
  348. newInfoLinkElement.href = '#';
  349. newInfoLinkElement.addEventListener('click', (event) => {
  350. event.preventDefault();
  351. event.stopPropagation();
  352.  
  353. const storedSections = getStorage(storage.mangadex.titles.custom_sections);
  354. if (!storedSections.some(section => section.id === sectionData.id)) {
  355. storedSections.push(sectionData);
  356. setStorage(storage.mangadex.titles.custom_sections, storedSections);
  357. }
  358.  
  359. const value = prompt(`Enter new ${sectionData.name} value`);
  360. if (!value) return;
  361.  
  362. const newLink = createSectionButton(sectionData, value, newInfoLinkElement);
  363. if (!newLink) return;
  364.  
  365. const storedTitleSections = mdGetTitleStorage(titleId, storage.mangadex.titles.data.custom_sections);
  366. let storedSectionIndex = storedTitleSections.findIndex(section => section.id === sectionData.id);
  367. if (storedSectionIndex < 0) {
  368. storedTitleSections.push({ id: sectionData.id });
  369. storedSectionIndex = storedTitleSections.findIndex(section => section.id === sectionData.id);
  370. }
  371. const sectionValues = storedTitleSections[storedSectionIndex].values || [];
  372.  
  373. sectionValues.push(value);
  374. storedTitleSections[storedSectionIndex].values = sectionValues;
  375. mdSetTitleStorage(titleId, storage.mangadex.titles.data.custom_sections, storedTitleSections);
  376. newInfoElement.insertBefore(newLink, newInfoLinkElement);
  377. });
  378. newInfoElement.appendChild(newInfoLinkElement);
  379.  
  380. return newInfoElement;
  381. }
  382.  
  383. const createSection = (sectionData) => {
  384. const newSectionElement = createSectionElement(sectionData);
  385. if (!newSectionElement) return;
  386. const newSectionLinkElement = createSectionLink(sectionData, newSectionElement);
  387. newSectionElement.appendChild(newSectionLinkElement);
  388. infoElement.insertBefore(newSectionElement, altTitlesSectionElement);
  389. }
  390.  
  391. const addNewSectionElement = createSectionElement({ id: 'add_local_section', name: 'Custom Sections +' }, true);
  392. if (addNewSectionElement) {
  393. addNewSectionElement.querySelector('div.flex.flex-wrap').remove();
  394. const addNewSectionTextElement = addNewSectionElement.querySelector('div.font-bold');
  395. addNewSectionTextElement.classList.remove('mb-2');
  396. addNewSectionTextElement.classList.add('cursor-pointer');
  397. addNewSectionTextElement.style.setProperty('width', 'fit-content');
  398. addNewSectionElement.classList.remove('mb-2');
  399. addNewSectionElement.classList.add('w-full');
  400. addNewSectionTextElement.addEventListener('click', () => {
  401. const storedSections = getStorage(storage.mangadex.titles.custom_sections);
  402. const sectionName = prompt('Enter new section name');
  403. const trimmedSectionName = sectionName && sectionName.trim();
  404. if (!trimmedSectionName) return;
  405.  
  406. const sectionData = {
  407. id: trimmedSectionName.replace(/\s/g, '_').toLowerCase(),
  408. name: trimmedSectionName
  409. }
  410.  
  411. if (storedSections.some(section => section.id === sectionData.id)) return;
  412. storedSections.push(sectionData);
  413. setStorage(storage.mangadex.titles.custom_sections, storedSections);
  414.  
  415. createSection(sectionData);
  416. });
  417.  
  418. infoElement.insertBefore(addNewSectionElement, altTitlesSectionElement);
  419. }
  420.  
  421. const storedSections = getStorage(storage.mangadex.titles.custom_sections);
  422. storedSections.forEach(createSection);
  423.  
  424. return true;
  425. }
  426.  
  427. function mdAddAltTitleOptions(titleId) {
  428. const infoElement = mdGetInfoElement(titleId);
  429. if (!infoElement) return false;
  430.  
  431. if (!infoElement.querySelector('a')) return false;
  432.  
  433. let altTitlesSectionElement = mdGetAltTitlesSectionElement(infoElement);
  434. if (!altTitlesSectionElement) {
  435. altTitlesSectionElement = document.createElement('div');
  436. altTitlesSectionElement.classList.add('w-full');
  437. infoElement.appendChild(altTitlesSectionElement);
  438.  
  439. const altTitlesSectionTextElement = document.createElement('div');
  440. altTitlesSectionTextElement.classList.add('font-bold', 'mb-1');
  441. altTitlesSectionTextElement.textContent = 'Alternative Titles';
  442. altTitlesSectionElement.appendChild(altTitlesSectionTextElement);
  443.  
  444. const altTitleElement = document.createElement('div');
  445. altTitleElement.classList.add('mb-1', 'flex', 'gap-x-2', 'alt-title');
  446. altTitlesSectionElement.appendChild(altTitleElement);
  447. }
  448. const altTitlesSectionLoadedAttribute = `${userScriptId}-alt-title-section-loaded`;
  449. if (altTitlesSectionElement.hasAttribute(altTitlesSectionLoadedAttribute)) return true;
  450. altTitlesSectionElement.setAttribute(altTitlesSectionLoadedAttribute, 'true');
  451.  
  452. const altTitlesSectionTextElement = altTitlesSectionElement.querySelector('div.font-bold');
  453. const altTitlesElements = altTitlesSectionElement.querySelectorAll('.alt-title');
  454. const altTitleElement = altTitlesElements[0].cloneNode(true);
  455. if (!mdTitleOptions.altTitle.storage) mdTitleOptions.altTitle.storage = [];
  456.  
  457. const addAltTitleStar = altTitleElement => {
  458. const storedTitle = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_title);
  459. const altTitleTextElement = altTitleElement.querySelector('span');
  460. if (!altTitleTextElement) return;
  461. const setTitleObject = {
  462. selected: storedTitle === altTitleTextElement.textContent,
  463. element: altTitleElement,
  464. starElement: document.createElement('span'),
  465. value: altTitleTextElement.textContent
  466. }
  467.  
  468. setTitleObject.starElement.textContent = setTitleObject.selected ? '★' : '☆';
  469. setTitleObject.starElement.classList.add('cursor-pointer');
  470. if (setTitleObject.selected) mdReplaceTitles(titleId);
  471.  
  472. setTitleObject.starElement.addEventListener('click', () => {
  473. mdSetTitleStorage(titleId, storage.mangadex.titles.data.main_title, setTitleObject.value, setTitleObject.selected);
  474.  
  475. mdReplaceTitles(titleId, setTitleObject.selected);
  476.  
  477. setTitleObject.selected = !setTitleObject.selected;
  478. mdTitleOptions.altTitle.storage.forEach(_setTitleObject => {
  479. _setTitleObject.selected = _setTitleObject.value === setTitleObject.value && setTitleObject.selected;
  480. _setTitleObject.starElement.textContent = _setTitleObject.selected ? '★' : '☆';
  481. });
  482. });
  483.  
  484. mdTitleOptions.altTitle.storage.push(setTitleObject);
  485. altTitleElement.prepend(setTitleObject.starElement);
  486. };
  487.  
  488. const createAltTitle = (value) => {
  489. if (!altTitlesElements[0].querySelector('span')) altTitlesElements[0].remove();
  490. const newAltTitleElement = altTitleElement.cloneNode(true);
  491. const newAltTitleIconElement = newAltTitleElement.querySelector('div');
  492. let newAltTitleTextElement = newAltTitleElement.querySelector('span');
  493. if (!newAltTitleTextElement) {
  494. newAltTitleTextElement = document.createElement('span');
  495. newAltTitleElement.appendChild(newAltTitleTextElement);
  496. }
  497. const removeCustomAltTitleElement = document.createElement('span');
  498.  
  499. if (newAltTitleIconElement) newAltTitleIconElement.remove();
  500. newAltTitleTextElement.textContent = value;
  501. removeCustomAltTitleElement.textContent = '⨯';
  502. removeCustomAltTitleElement.classList.add('cursor-pointer');
  503. removeCustomAltTitleElement.addEventListener('click', () => {
  504. if (!confirm(`Are you sure you want to delete this title?\n\n${value}`)) return;
  505.  
  506. mdSetTitleStorage(titleId, storage.mangadex.titles.data.alt_titles, value, true, true);
  507.  
  508. const setTitleObjectIndex = mdTitleOptions.altTitle.storage.findIndex(setTitleObject => setTitleObject.value === value);
  509. if (setTitleObjectIndex > -1) mdTitleOptions.altTitle.storage.splice(setTitleObjectIndex, 1);
  510.  
  511. const storedAltTitles = mdGetTitleStorage(titleId, storage.mangadex.titles.data.alt_titles);
  512. const storedMainTitle = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_title);
  513. if (storedMainTitle === value && !storedAltTitles.some(altTitle => altTitle === value)) {
  514. mdSetTitleStorage(titleId, storage.mangadex.titles.data.main_title, value, true);
  515. mdReplaceTitles(titleId, true);
  516. }
  517.  
  518. newAltTitleElement.remove();
  519. });
  520. newAltTitleElement.appendChild(removeCustomAltTitleElement);
  521. addAltTitleStar(newAltTitleElement);
  522. altTitlesSectionElement.appendChild(newAltTitleElement);
  523. };
  524.  
  525. altTitlesElements.forEach(addAltTitleStar);
  526.  
  527. altTitlesSectionTextElement.textContent = `${altTitlesSectionTextElement.textContent} +`
  528. altTitlesSectionTextElement.classList.add('cursor-pointer');
  529. altTitlesSectionTextElement.style.setProperty('width', 'fit-content');
  530. altTitlesSectionTextElement.addEventListener('click', () => {
  531. const value = prompt('Enter new title');
  532. if (!value) return;
  533.  
  534. mdSetTitleStorage(titleId, storage.mangadex.titles.data.alt_titles, value, false, true);
  535.  
  536. createAltTitle(value);
  537. });
  538.  
  539. const storedAltTitles = mdGetTitleStorage(titleId, storage.mangadex.titles.data.alt_titles);
  540. if (storedAltTitles) storedAltTitles.forEach(createAltTitle);
  541. const storedTitle = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_title);
  542. if (storedTitle && !mdTitleOptions.altTitle.storage.some(setTitleObject => setTitleObject.selected)) {
  543. mdSetTitleStorage(titleId, storage.mangadex.titles.data.alt_titles, storedTitle, false, true);
  544. createAltTitle(storedTitle);
  545. }
  546.  
  547. return true;
  548. }
  549.  
  550. function mdReplaceTitles(titleId, useDefaultTitle) {
  551. if (titleId) {
  552. const titlePageTitleElement = document.querySelector('div.title > p');
  553. if (!titlePageTitleElement) return;
  554.  
  555. const defaultTitleAttribute = `${userScriptId}-default-title`;
  556. if (!titlePageTitleElement.hasAttribute(defaultTitleAttribute))
  557. titlePageTitleElement.setAttribute(defaultTitleAttribute, titlePageTitleElement.textContent);
  558.  
  559. const defaultTitle = useDefaultTitle && titlePageTitleElement.getAttribute(defaultTitleAttribute);
  560. const storedMainTitle = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_title);
  561. titlePageTitleElement.textContent = defaultTitle || storedMainTitle || 'undefined';
  562.  
  563. return;
  564. }
  565.  
  566. const titleLinkElements = document.querySelectorAll(
  567. 'a.title, a.chapter-feed__title, .dense-manga-container a, .swiper-slide a, .manga-draft-container a, a[class=""]'
  568. );
  569. titleLinkElements.forEach(titleLinkElement => {
  570. const titleReplacedAttribute = `${userScriptId}-title-replaced`;
  571. if (titleLinkElement.hasAttribute(titleReplacedAttribute)) return;
  572. titleLinkElement.setAttribute(titleReplacedAttribute, 'true');
  573.  
  574. let textElement = titleLinkElement;
  575. const hasTextNode = () => textElement && textElement.childNodes && Array.from(textElement.childNodes).some(text => text.data);
  576. if (!hasTextNode()) textElement = titleLinkElement.querySelector('span, h6');
  577. if (!hasTextNode() && titleLinkElement.parentElement)
  578. textElement = titleLinkElement.parentElement.querySelector('span, h2, div.font-bold');
  579. if (!hasTextNode()) return;
  580.  
  581. if (textElement.parentElement && textElement.parentElement.tagName === 'BUTTON') return;
  582.  
  583. const mdTitleId = mdGetTitleId(titleLinkElement.getAttribute('href'));
  584. if (!mdTitleId) return;
  585.  
  586. const storedMainTitle = mdGetTitleStorage(mdTitleId, storage.mangadex.titles.data.main_title);
  587. if (!storedMainTitle) return;
  588.  
  589. textElement.childNodes.forEach((text) => {
  590. if (text.data) text.data = storedMainTitle;
  591. });
  592. });
  593. }
  594.  
  595. function mdAddVolumeCoverOptions(titleId) {
  596. if (document.querySelector('div[role="alert"]')) return true;
  597. if (document.querySelectorAll(`a[href*="covers/${titleId}"]`).length < 2) return false;
  598. const volumeCoverLoadedAttribute = `${userScriptId}-volume-cover-loaded`;
  599. const volumeCoverLinkElements = document.querySelectorAll(`a[href*="covers/${titleId}"]:not([${volumeCoverLoadedAttribute}])`);
  600. if (!mdTitleOptions.volumeCover.storage) mdTitleOptions.volumeCover.storage = [];
  601.  
  602. volumeCoverLinkElements.forEach(volumeCoverLinkElement => {
  603. volumeCoverLinkElement.setAttribute(volumeCoverLoadedAttribute, 'true');
  604.  
  605. const volumeSubtitleElement = volumeCoverLinkElement.querySelector('.subtitle');
  606. if (!volumeSubtitleElement) return;
  607. volumeSubtitleElement.textContent = ` ${volumeSubtitleElement.textContent}`;
  608.  
  609. const volumeCoverLink = volumeCoverLinkElement.getAttribute('href');
  610. if (!volumeCoverLink) return;
  611. const volumeCoverFilename = mdGetCoverFileName(volumeCoverLink);
  612. if (!volumeCoverFilename.fileName) return;
  613.  
  614. const storedVolumeCover = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_cover);
  615. const setCoverObject = {
  616. selected: volumeCoverFilename.fileName === storedVolumeCover,
  617. element: volumeCoverLinkElement,
  618. starElement: document.createElement('span'),
  619. value: volumeCoverFilename.fileName
  620. }
  621.  
  622. setCoverObject.starElement.textContent = setCoverObject.selected ? '★' : '☆';
  623. setCoverObject.starElement.classList.add('cursor-pointer');
  624. setCoverObject.starElement.addEventListener('click', (event) => {
  625. event.preventDefault();
  626. event.stopPropagation();
  627.  
  628. mdSetTitleStorage(titleId, storage.mangadex.titles.data.main_cover, setCoverObject.value, setCoverObject.selected);
  629.  
  630. setCoverObject.selected = !setCoverObject.selected;
  631. mdTitleOptions.volumeCover.storage.forEach(_setCoverObject => {
  632. _setCoverObject.selected = _setCoverObject.value === setCoverObject.value && setCoverObject.selected;
  633. _setCoverObject.starElement.textContent = _setCoverObject.selected ? '★' : '☆';
  634. });
  635.  
  636. mdReplaceVolumeCovers(titleId, !setCoverObject.selected);
  637. });
  638. volumeSubtitleElement.prepend(setCoverObject.starElement);
  639.  
  640. mdTitleOptions.volumeCover.storage.push(setCoverObject);
  641. });
  642.  
  643. return true;
  644. }
  645.  
  646. function mdReplaceVolumeCovers(titleId, useDefault) {
  647. const coverLinkElement = document.querySelector(`.md-content > .manga a[href*="covers/${titleId}"]`);
  648. const replaceCoverUrl = (titleId, urlToReplace, storedCover) => {
  649. if (!titleId || !storedCover) return;
  650. const urlToReplaceFilename = mdGetCoverFileName(urlToReplace);
  651. if (!urlToReplaceFilename.size) urlToReplaceFilename.size = '';
  652. const newUrl = `https://mangadex.org/covers/${titleId}/${storedCover}${urlToReplaceFilename.size}`;
  653. if (newUrl !== urlToReplace) return newUrl;
  654. }
  655.  
  656. if (coverLinkElement) {
  657. const defaultCoverAttribute = `${userScriptId}-default-cover`;
  658. if (!coverLinkElement.hasAttribute(defaultCoverAttribute)) {
  659. const coverLinkFileName = mdGetCoverFileName(coverLinkElement.getAttribute('href'));
  660. if (coverLinkFileName.fileName) coverLinkElement.setAttribute(defaultCoverAttribute, coverLinkFileName.fileName);
  661. }
  662.  
  663. const storedCover = mdGetTitleStorage(titleId, storage.mangadex.titles.data.main_cover);
  664. const defaultCover = useDefault && coverLinkElement.getAttribute(defaultCoverAttribute);
  665. const newCover = defaultCover || storedCover;
  666.  
  667. if (newCover) {
  668. const newCoverLinkUrl = replaceCoverUrl(titleId, coverLinkElement.getAttribute('href'), newCover);
  669. if (newCoverLinkUrl) coverLinkElement.setAttribute('href', newCoverLinkUrl);
  670.  
  671. const coverImageElement = coverLinkElement.querySelector(`img[src*="covers/${titleId}"]`);
  672. if (coverImageElement) {
  673. const newCoverImageUrl = replaceCoverUrl(titleId, coverImageElement.getAttribute('src'), newCover);
  674. if (newCoverImageUrl) coverImageElement.setAttribute('src', newCoverImageUrl);
  675. }
  676.  
  677. const bannerImageElement = document.querySelector('.banner-image');
  678. if (bannerImageElement) {
  679. const newBannerImageUrl = replaceCoverUrl(titleId, bannerImageElement.style.getPropertyValue('background-image'), newCover);
  680. if (newBannerImageUrl) bannerImageElement.style.setProperty('background-image', `url("${newBannerImageUrl}")`);
  681. }
  682. }
  683. }
  684.  
  685. const coverLoadedAttribute = `${userScriptId}-cover-loaded`;
  686. const imageElements = document.querySelectorAll(`img:not([${coverLoadedAttribute}])`);
  687. imageElements.forEach(imageElement => {
  688. imageElement.setAttribute(coverLoadedAttribute, 'true');
  689. const imageUrl = imageElement.getAttribute('src');
  690. if (!imageUrl) return;
  691. const mdTitleId = mdGetTitleId(imageUrl);
  692. if (!mdTitleId || mdTitleId === titleId) return;
  693. const storedCover = mdGetTitleStorage(mdTitleId, storage.mangadex.titles.data.main_cover);
  694. const newCoverUrl = replaceCoverUrl(mdTitleId, imageUrl, storedCover);
  695. if (newCoverUrl) imageElement.setAttribute('src', newCoverUrl);
  696. });
  697. }
  698.  
  699. function komgaAutoMatch(seriesId) {
  700. if (!document.querySelector(`.v-image__image[style*="${seriesId}"]`)) return false;
  701.  
  702. const linkElements = document.querySelectorAll(`a.v-chip--link`);
  703. if (linkElements < 1) return false;
  704.  
  705. const sectionData = {
  706. id: 'local_links',
  707. name: 'Local Links'
  708. }
  709.  
  710. const storedSections = getStorage(storage.mangadex.titles.custom_sections);
  711. if (!storedSections.some(section => section.id === sectionData.id)) {
  712. storedSections.push(sectionData);
  713. setStorage(storage.mangadex.titles.custom_sections, storedSections);
  714. }
  715.  
  716. linkElements.forEach(link => {
  717. const mdTitleId = mdGetTitleId(link.href);
  718. if (!mdTitleId) return;
  719.  
  720. const storedTitleSections = mdGetTitleStorage(mdTitleId, storage.mangadex.titles.data.custom_sections);
  721. let storedSectionIndex = storedTitleSections.findIndex(section => section.id === sectionData.id);
  722. if (storedSectionIndex < 0) {
  723. storedTitleSections.push({ id: sectionData.id });
  724. storedSectionIndex = storedTitleSections.findIndex(section => section.id === sectionData.id);
  725. }
  726.  
  727. const sectionValues = storedTitleSections[storedSectionIndex].values || [];
  728. if (sectionValues.some(link => seriesId === komgaGetSeriesId(link))) return;
  729.  
  730. const sectionLink = `[Komga](${window.location.href.replace(/\?.*$/, '')})`;
  731. sectionValues.push(sectionLink);
  732. storedTitleSections[storedSectionIndex].values = sectionValues;
  733. mdSetTitleStorage(mdTitleId, storage.mangadex.titles.data.custom_sections, storedTitleSections);
  734. });
  735.  
  736. return true;
  737. }
  738.  
  739. function observeElement(onChange, element = document.body) {
  740. const observer = new MutationObserver(onChange);
  741.  
  742. onChange();
  743. observer.observe(element, {
  744. childList: true,
  745. subtree: true,
  746. });
  747. }
  748. })();