// ==UserScript== // @name MAL (MyAnimeList) German Dubs // @namespace https://github.com/Funami580/MAL-GerDubs // @version 0.9.48 // @description Labels German dubbed titles on MyAnimeList.net and adds dub only filtering // @author MAL Dubs // @supportURL https://github.com/Funami580/MAL-GerDubs/issues // @downloadURL https://github.com/Funami580/MAL-GerDubs/raw/main/mal-dubs.user.js // @updateURL https://github.com/Funami580/MAL-GerDubs/raw/main/mal-dubs.user.js // @match https://myanimelist.net/* // @iconURL https://raw.githubusercontent.com/Funami580/MAL-GerDubs/main/images/icon.png // @license GNU AGPLv3; https://www.gnu.org/licenses/agpl-3.0.html // @resource CSS https://raw.githubusercontent.com/Funami580/MAL-GerDubs/main/css/style.css // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_getResourceText // @connect githubusercontent.com // @connect github.com // @run-at document-end // @noframes // @icon data:image/svg+xml;charset=utf8, // ==/UserScript== const dubbedLinks = document.querySelectorAll("p.title-text>a,p.data a.title,.content-result .information>a:first-child,.list>.information>a:first-child,table.anime_detail_related_anime a[href^='/anime'],.content-result .title>a:first-child,td.data.title>a:first-child,.list td:nth-child(2)>a.hoverinfo_trigger,#content>table>tbody>tr>td>table>tbody>tr>td.borderClass>a[href*='myanimelist.net/anime/'],#content>div>div>table>tbody>tr>td>a[href*='/anime'],div[id^=raArea]+div>a:first-child,.news-container h2 a[href*='/anime/'],li.ranking-unit>div>h3>a,tr.ranking-list>td:nth-child(2)>div>div>h3>a,div.borderClass>a[href^='anime/'],#content>table>tbody>tr>td:nth-child(1)>a:nth-child(1),[id^='#revAreaItemTrigger'],.news-container a[href^='https://myanimelist.net/anime/'],.animeography>.title>a,.profile div.updates.anime>div.statistics-updates>div.data>a,.history_content_wrapper td:first-child>a,.forum-topic-message a[href^='https://myanimelist.net/anime/'],.forum-topic-message a[href^='/anime/'],.page-blog-detail a[href^='https://myanimelist.net/anime/'],.page-blog-detail a[href^='/anime/'],.pmessage-message-history a[href^='https://myanimelist.net/anime/'],.pmessage-message-history a[href^='/anime/'],a.js-people-title,.profile .anime>div>div.data>div.title>a,a[id^='sinfo'],[id^='#revAreaAnimeHover'],#dialog>tbody>tr>td>a,.company-favorites-ranking-table>tbody>tr>td.popularity>p>a,.company-favorites-ranking-table>tbody>tr>td.score>p>a,div.list.js-categories-seasonal>table>tbody>tr>td:nth-child(2)>div:nth-child(1)>a,.stacks #content>div.content-left>div.list-anime-list>div>div.head>div.title-text>h2>a,.footer-ranking li>a,#content > div:nth-child(3) > a[href^='/anime'],.blog_detail_content_wrapper a,div[id^=comment]>table>tbody>tr>td:nth-child(2)>a,.recommendations_h3>a,.reviews_h3>a,.friend_list_updates a.fw-b,.info>p>a.fw-b,#content>div.borderDark>table>tbody>tr>td>a,.js-history-updates-item .work-title>a:first-of-type"); const dubbedThumbs = 'div.auto-recommendations>div.items>a.item,div.recommendations div.items>a.item,div#widget-seasonal-video li.btn-anime>a.link,div#anime_recommendation li.btn-anime.auto>a.link,.js-seasonal-anime>.image>a:nth-child(1),#anime_favorites>.fav-slide-outer>ul>li>a'; const animeURLregex = /^(https?:\/\/myanimelist\.net)?\/?anime(\/|\.php\?id=)(\d+)\/?.*$/; const filterableURLregex = /.*\/(((anime\.php\?(?!id).+|topanime\.php.*))|anime\/(genre|producer|season)\/?.*)/; const filterable = filterableURLregex.test(document.location.href); const searchURLregex = /.*\/((anime\.php\??(?!id).*)|anime\/genre\/?.*)/; const searchPage = searchURLregex.test(document.location.href); const IDURL = 'https://raw.githubusercontent.com/Funami580/MAL-GerDubs/main/data/dubInfo.json'; let dubbedIDs = JSON.parse(localStorage.getItem('dubIDs')); let incompleteDubs = JSON.parse(localStorage.getItem('incompleteIDs')); GM_addStyle(GM_getResourceText('CSS')); function dubCache() { if (localStorage.getItem('dubCacheDate') === null) { localStorage.setItem('dubCacheDate', Date.now()); } if (parseInt(localStorage.getItem('dubCacheDate'), 10) + 600000 < Date.now()) { GM_xmlhttpRequest({ method: 'GET', url: IDURL, nocache: true, revalidate: true, onload(response) { const data = JSON.parse(response.responseText); localStorage.setItem('dubIDs', JSON.stringify(data.dubbed)); localStorage.setItem('incompleteIDs', JSON.stringify(data.incomplete)); localStorage.setItem('dubCacheDate', Date.now()); }, }); } } function labelDub(anime) { const animeElement = anime; if (animeURLregex.test(anime.href)) { const linkID = parseInt(anime.href.match(/(\/|\.php\?id=)(\d+)\/?/)[2], 10); if (dubbedIDs.includes(linkID)) { animeElement.title = 'Dubbed'; if (incompleteDubs.includes(linkID)) { animeElement.title = 'Incomplete Dub'; } } else { animeElement.title = 'Undubbed'; } } } function labelThumbnails() { document.querySelectorAll(dubbedThumbs).forEach((e) => { labelDub(e); e.classList.add('imagelink'); }); } function labelResults(selector) { const results = document.querySelectorAll(selector); results.forEach((e) => labelDub(e)); } function searchObserver(containerID, resultSelector) { const searchContainer = document.getElementById(containerID); new MutationObserver(() => labelResults(resultSelector)).observe(searchContainer, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'], }); } function animePages() { const thispage = animeURLregex.exec(document.location.href)[3]; const recommendations = document.querySelectorAll("div#anime_recommendation>div.anime-slide-outer>.anime-slide>li.btn-anime>a.link:not([href*='suggestion'])"); const recrgx = /^(https?:\/\/myanimelist\.net)?\/recommendations\/anime\/(\d+-\d+)\/?.*/; if (dubbedIDs.includes(parseInt(thispage, 10))) { const pagetitle = document.querySelectorAll('h1.title-name')[0]; pagetitle.title = 'Dubbed'; if (incompleteDubs.includes(parseInt(thispage, 10))) { pagetitle.title = 'Incomplete Dub'; } } recommendations.forEach((e) => { const recElement = e; const recID = parseInt(recrgx.exec(e.href)[2].replace(`${thispage}-`, '').replace(`-${thispage}`, ''), 10); if (dubbedIDs.includes(recID)) { recElement.title = 'Dubbed'; recElement.classList.add('imagelink'); if (incompleteDubs.includes(recID)) { recElement.title = 'Incomplete Dub'; } } }); } function seasonalCount() { const titlesArray = [].slice.call(document.querySelectorAll('.seasonal-anime')); const showingArray = titlesArray.filter((el) => getComputedStyle(el).display !== 'none'); const countDisplay = document.querySelector('.js-visible-anime-count'); countDisplay.textContent = `${showingArray.length}/${titlesArray.length}`; } function filterContainers() { const undubbed = document.querySelectorAll('[title="Undubbed"]'); undubbed.forEach((e) => { if (document.location.href.match(/.*\/topanime\.php.*/)) { e.parentNode.parentNode.parentNode.parentNode.parentNode.classList.add('noDub'); } else if (document.location.href.match(/.*anime\.php\?.*q=.*/)) { e.parentNode.parentNode.parentNode.classList.add('noDub'); } else if (document.location.href.match(/.*\/animelist\/.*/)) { e.parentNode.parentNode.parentNode.classList.add('noDub'); } else if (document.querySelector('div.list.js-categories-seasonal')) { e.parentNode.parentNode.parentNode.classList.add('noDub'); } else { e.parentNode.parentNode.classList.add('noDub'); } }); } function updateIds() { const numbers = document.querySelectorAll('#list-container>div.list-block>div>table>tbody.list-item>tr.list-table-data>td.data.number'); let currNumber = 1; numbers.forEach((e) => { if (e.clientWidth && e.clientHeight) { e.innerHTML = "" + currNumber; currNumber += 1; } }); } function searchFilter() { filterContainers(); const filterTarget = document.querySelector('.js-search-filter-block>div:last-of-type,div.horiznav-nav-seasonal>span.js-btn-show-sort:last-of-type,h2.top-rank-header2>span:last-of-type,.normal_header>div.view-style2:last-of-type,#show-stats-button'); const filterCheckbox = document.createElement('input'); const label = document.createElement('label'); if (filterTarget !== null) { filterCheckbox.type = 'checkbox'; filterCheckbox.id = 'undubbed-filter'; label.setAttribute('for', 'undubbed-filter'); label.className = 'fs11 fl-r btn-show-undubbed mr12 fw-n fn-grey2'; if (filterTarget.id === "show-stats-button") { label.className = label.className + " btn-show-undubbed-old"; label.appendChild(document.createTextNode('Dubs Only')); const color = window.getComputedStyle(filterTarget).color; label.setAttribute("style", "color: " + color); filterTarget.before(filterCheckbox); } else { label.innerHTML = ` Dubs Only `.trim(); filterTarget.after(filterCheckbox); } filterCheckbox.after(label); } let filter = true; let filterUndubbed = (localStorage.getItem('dubOnlySearch') === 'true'); if (filterUndubbed === null) { filterUndubbed = false; localStorage.setItem('dubOnlySearch', filterUndubbed); filterCheckbox.checked = filterUndubbed; filter = false; } if (filter) { filterCheckbox.checked = filterUndubbed; if (filterCheckbox.checked === true) { document.body.classList.add('filterOn'); } } if (document.location.href.match(/.*\/animelist\/.*/)) { const listContainer = document.getElementById('list-container'); if (listContainer) { new MutationObserver((mutations) => { const firstMutation = mutations[0]; if (firstMutation.addedNodes === undefined || firstMutation.addedNodes[0] === undefined) { return; } const mutatedNodeClassList = firstMutation.addedNodes[0].classList; if (mutatedNodeClassList === undefined) { return; } if (!mutatedNodeClassList.contains("list-table") && !mutatedNodeClassList.contains("list-item")) { return; } labelList(); filterContainers(); if (filterCheckbox.checked === true) { document.body.classList.add('filterOn'); } if (filterCheckbox.checked === false) { document.body.classList.remove('filterOn'); } updateIds(); }).observe(listContainer, { childList: true, subtree: true, }); } } filterCheckbox.addEventListener('change', () => { if (filterCheckbox.checked === true) { document.body.classList.add('filterOn'); } if (filterCheckbox.checked === false) { document.body.classList.remove('filterOn'); } if (document.location.href.match(/.*\/animelist\/.*/)) { updateIds(); } if (document.location.href.match(/.*\/season\/?.*/)) { seasonalCount(); } localStorage.setItem('dubOnlySearch', filterCheckbox.checked); }, false); } // hide function from jquery function isVisible(elem) { return !!elem && !!(elem.offsetWidth || elem.offsetHeight || elem.getClientRects().length); } function hideOnClickOutside(element) { const outsideClickListener = (event) => { if (!element.contains(event.target) && isVisible(element)) { element.classList.remove('on'); removeClickListener(); } }; const removeClickListener = () => document.removeEventListener('click', outsideClickListener); document.addEventListener('click', outsideClickListener); } // source (2018-03-11): https://github.com/jquery/jquery/blob/master/src/css/hiddenVisibleSelectors.js function toggleMenu() { const dropdown = document.getElementById('dubmenu'); if (dropdown.classList.contains('on')) { dropdown.classList.remove('on'); } else { dropdown.classList.add('on'); hideOnClickOutside(dropdown); } } function setTheme() { if (localStorage.getItem('classicTheme') === 'true') { document.body.classList.add('classic'); } } function toggleClassic() { if (localStorage.getItem('classicTheme') === 'true') { document.body.classList.remove('classic'); localStorage.setItem('classicTheme', false); } else { document.body.classList.add('classic'); localStorage.setItem('classicTheme', true); } } function placeHeaderMenu() { const menuContainer = document.createElement('div'); const borderDiv = document.createElement('div'); menuContainer.id = 'dubmenu'; menuContainer.classList.add('header-menu-unit', 'header-dub'); menuContainer.innerHTML += '
'; borderDiv.classList.add('border'); if (document.body.contains(document.querySelector('.header-profile'))) { document.querySelector('#header-menu>div.header-profile').before(menuContainer, borderDiv); } else if (document.body.contains(document.querySelector('.header-menu-login'))) { document.querySelector('#header-menu>div.header-menu-login').after(menuContainer); } document.getElementById('theme-toggle').addEventListener('click', toggleClassic, false); document.getElementById('menu-toggle').addEventListener('click', toggleMenu, false); } function labelList() { const listEntries = document.querySelectorAll('#list-container>div.list-block>div>table>tbody.list-item>tr.list-table-data>td.data.title>a.link,div#list_surround>table>tbody>tr>td>a.animetitle'); listEntries.forEach((e) => { if (!['Undubbed', 'Dubbed', 'Incomplete Dub'].includes(e.title)) { labelDub(e); } }); } function processList() { const listContainer = document.getElementById('list-container'); if (!listContainer) { labelList(); } } function processSite() { labelThumbnails(); dubbedLinks.forEach((e) => { labelDub(e); }); searchObserver('menu_right', '#top-search-bar>#topSearchResultList>div>div>a'); } function onComplete() { if (document.location.href.match(/.*\/animelist\/.*/)) { searchFilter(); processList(); } else { processSite(); if (filterable) { searchFilter(); } if (searchPage) { searchObserver('content', '#advancedsearch>div>#advancedSearchResultList>div>div>a') } if (animeURLregex.test(document.location.href)) { animePages(); } if (document.location.href.match(/https:\/\/myanimelist\.net\/addtolist\.php/)) { searchObserver('content', '.quickAdd-anime-result-unit>table>tbody>tr>td:nth-child(1)>a'); } placeHeaderMenu(); setTimeout(() => labelThumbnails(), 400); } setTheme(); } if (dubbedIDs === null || incompleteDubs === null) { GM_xmlhttpRequest({ method: 'GET', nocache: true, url: IDURL, onload(response) { const data = JSON.parse(response.responseText); dubbedIDs = data.dubbed; incompleteDubs = data.incomplete; localStorage.setItem('dubIDs', JSON.stringify(dubbedIDs)); localStorage.setItem('incompleteIDs', JSON.stringify(incompleteDubs)); onComplete(); localStorage.setItem('dubCacheDate', Date.now()); }, }); } else { onComplete(); dubCache(); }