NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name KissMAL // @namespace https://github.com/josefandersson/KissMAL // @version 1.99 // @description Adds links to kissanime/kissmanga on MAL. // @author Josef // @match *://myanimelist.net/animelist/* // @match *://myanimelist.net/anime/* // @match *://myanimelist.net/mangalist/* // @match *://myanimelist.net/manga/* // @require https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js // @require https://openuserjs.org/src/libs/DrDoof/RemoveDiacritics.js // @resource MainCSS https://raw.githubusercontent.com/josefandersson/KissMAL/master/resources/kissmal.css // @icon https://cdn.myanimelist.net/images/faviconv5.ico // @grant GM_getResourceText // @grant GM_setValue // @grant GM_getValue // @run-at document-end // @updateURL https://raw.githubusercontent.com/josefandersson/KissMAL/master/kissmal.meta.js // @downloadURL https://openuserjs.org/install/DrDoof/KissMAL.user.js // @license GPL-3.0-or-later; http://www.gnu.org/licenses/gpl-3.0.txt // @copyright 2016-2020, DrDoof (https://openuserjs.org/users/DrDoof) // ==/UserScript== /* An object that represents the current page. This object contains the methods for altering the page. */ class Page { constructor( url ) { this.url = url; this.isAnime = true; this.isList = true; this.isNewListDesign = false; // Find out what page we are on. Depending on the page we'll be using different methods of altering it. if (/(http[s]?:\/\/myanimelist.net\/animelist\/)/.test(url)) this.page = 'animelist'; else if (/(http[s]?:\/\/myanimelist.net\/anime\/)/.test(url)) this.page = 'anime'; else if (/(http[s]?:\/\/myanimelist.net\/mangalist\/)/.test(url)) this.page = 'mangalist'; else if (/(http[s]?:\/\/myanimelist.net\/manga\/)/.test(url)) this.page = 'manga'; // Check if we are viewing an anime or a manga page. if (this.page.indexOf('anime') < 0) this.isAnime = false; // Check if we are viewing a list page or info page. if (this.page.indexOf('list') < 0) this.isList = false; // If we are viewing a list page we have to check if it's the old or new design type. (As they also need different methods of altering.) else { if ($('#mal_cs_otherlinks').length <= 0) this.isNewListDesign = true; } } /* Add the links to the page using the appropiate method. */ makeLinks(dontMakeSettingsWindow) { if (page.isList) { this.makeLinksForListPage(); if (!dontMakeSettingsWindow) this.makeSettingsWindow(); } else { this.makeLinksForInfoPage(); } } createLinkElement(title, dub, displayText) { const link = document.createElement('a'); link.href = guessURL(title, dub, !this.isAnime); link.innerText = displayText; link.className = 'kissmal_link'; if (config.getValue('newTab')) link.target = '_blank'; return link; } /* Add the links for a information page. */ makeLinksForInfoPage() { // Get the title of the anime/manga. const title = document.querySelector('h1.h1 span').innerText; // All links will be put inside this DIV. const linkContainer = document.createElement('div'); linkContainer.className = 'kissmal_link_container'; // Create the link elements and append them to the link container const displayText = this.isAnime ? config.getValue('displayTextAnime') : config.getValue('displayTextManga'); if (config.getValue('generalLinkEnabled')) linkContainer.appendChild(this.createLinkElement(title, false, displayText)); if (this.isAnime && config.getValue('dubLinkEnabled')) linkContainer.appendChild(this.createLinkElement(title, true, config.getValue('displayTextDub'))); // Insert the link container underneath the anime/manga cover image. const parent = document.querySelector('#content > table > tbody > tr > td.borderClass > div > div'); parent.insertBefore(linkContainer, parent.children[1]); } /* Add the links for a list page. */ makeLinksForListPage() { const displayText = this.isAnime ? config.getValue('displayTextAnime') : config.getValue('displayTextManga'); // Depending on if the list is using the old or the new design we'll use different methods to add the links. if (this.isNewListDesign) { [...document.querySelectorAll('td.title')].forEach(element => { const childAfter = element.children[3]; const title = element.children[0].innerText; if (config.getValue('generalLinkEnabled')) element.insertBefore(this.createLinkElement(title, false, displayText), childAfter ); if (this.isAnime && config.getValue('dubLinkEnabled')) element.insertBefore(this.createLinkElement(title, true, config.getValue('displayTextDub')), childAfter); }); } else { [...document.querySelectorAll('.animetitle')].forEach(element => { const parent = element.parentNode; const title = element.children[0].innerText; if (config.getValue('generalLinkEnabled')) parent.appendChild(this.createLinkElement(title, false, displayText)); if (this.isAnime && config.getValue('dubLinkEnabled')) parent.appendChild(this.createLinkElement(title, true, config.getValue('displayTextDub'))); }); } } // Add settings window. makeSettingsWindow() { let html = '<h3 class="kissmal">KissMAL settings</h3>'; var setting, input; for (var settingName in Config.settings) { setting = Config.settings[settingName]; if (setting.type == 'textarea') input = `<textarea id="kmset_${settingName}"></textarea>`; else input = `<input type="${setting.type}" id="kmset_${settingName}">`; html += `<p> <label for="kmset_${settingName}">${setting.display_text}</label> ${input}<br> </p>`; } html += `<p> <input type="button" id="reset_settings" value="Reset"> <input type="button" id="save_settings" value="Save"> <input type="button" id="close_settings" value="Close"> </p>`; // Resets the fields in the settings popup to default settings. function resetSettings() { var setting; for (var settingName in Config.settings) { setting = Config.settings[settingName]; if (setting.type == 'checkbox') $(`#kmset_${settingName}`)[0].checked = setting.default; else $(`#kmset_${settingName}`).val(setting.default); } } // Saves the values in the fields in the settings popup to config. function saveSettings() { var setting, value; for (var settingName in Config.settings) { setting = Config.settings[settingName]; if (setting.type == 'checkbox') value = $(`#kmset_${settingName}`)[0].checked; else value = $(`#kmset_${settingName}`).val(); config.setValue( settingName, value ); } // Remake all the links with new settings. page.removeCreatedLinks(); page.makeLinks(true); } // Set fields in the settings popup to those set by the user in the config. function loadSettings() { var setting; for (var settingName in Config.settings) { setting = Config.settings[settingName]; if (setting.type == 'checkbox') { $(`#kmset_${settingName}`)[0].checked = config.getValue( settingName ); } else $(`#kmset_${settingName}`).val(config.getValue( settingName )); } } // This container is the settings popup itself. var container = $('<div></div>').attr('id', 'kissmal_settings_container').html(html).attr('hidden', true).appendTo($(document.body)); // Add event handlers for the settings checkboxes. $('#close_settings').click(function(e) { container.attr('hidden', true); }); $('#reset_settings').click(function(e) { resetSettings(); }); $('#save_settings').click(function(e) { saveSettings(); }); // Create the button that opens the settings window. var settings = $('<a></a>').attr('href', '#').html('Edit KissMAL settings'); // Add the button to the DOM. if (page.isNewListDesign) { var headerInfo = $('.header-info'); headerInfo.html(headerInfo.html() + ' - '); settings.appendTo(headerInfo); } else { $('#mal_cs_otherlinks').children('div').last().append(settings); } // Add a event handler for the settings opening button. $(settings).click(function(e) { if (container.attr('hidden')) { // Container is refering to the settings window container in the DOM that was created above container.attr('hidden', false); loadSettings(); } }); } // Remove all kissmal links. removeCreatedLinks() { $('.kissmal_link').each(function(index, element) { element.remove(); }); } } // Represents persistent user settings. class Config { // All available settings. static get settings() { return { "linkCss": { display_text:"Link style (css)", description: 'Change the styling of the kissMAL links using CSS.', type:'textarea', default:'font-size: 10px; opacity: 0.8; margin-left: 3px; margin-right: 2px;' }, "generalLinkEnabled": { display_text:'KissMAL link enabled', description:'Add a link to the normal version of the anime/manga.', type:'checkbox', default:true }, "dubLinkEnabled": { display_text:'Dubbed anime link enabled', description:'Add a link to the dubbed version of the anime.', type:'checkbox', default:true }, "newTab": { display_text:'Open kissMAL links in new tabs', description:'Open kissMAL links in new tabs.', type:'checkbox', default:true }, 'displayTextAnime': { display_text:'Anime link display text', description:'The display text of the anime kissMAL links.', type:'text', default:'KissAnime' }, 'displayTextManga': { display_text:'Manga link display text', description:'The display text of the manga KissMAL links.', type:'text', default:'KissManga' }, 'displayTextDub': { display_text:'Dub link display text', description:'The display text of the dubbed anime kissMAL links.', type:'text', default:'(dub)' }, }; } constructor() { // Get saved settings or set defaults. this.settingsData = {}; let val; for (const settingName in Config.settings) { if ((val = GM_getValue( settingName )) === undefined) val = Config.settings[settingName].default; this.setValue(settingName, val); } } // Set setting. (Saves to disk) setValue( key, value ) { this.settingsData[key] = value; GM_setValue(key, value); return value; } // Get setting. (Doesn't load from disk) getValue( key ) { let val = this.settingsData[key]; if (val === undefined) if ((val = GM_getValue( key )) === undefined) val = Config.settings[key].default; return val; } // Save settings to disk. saveSettings() { for (const settingName in this.settingsData) { GM_setValue(settingName, this.settingsData[settingName]); } } // Reset settings. (Saves to disk) resetSettings() { for (const settingName in this.settingsData) { setValue(settingName, Config.settings[settingName].default); } } } let config; let page; $(document).ready(() => { 'use strict'; // Load user settings. config = new Config(); // Add the styling for the settings popup and the kissmal links. const style = document.createElement('style'); style.innerHTML = GM_getResourceText('MainCSS') + '.kissmal_link {' + config.getValue('linkCss') + '}'; document.head.appendChild(style); // Parse the current page we're on. page = new Page(window.location.href); // Make the links. page.makeLinks(); }); /* The best we can do is to guess the url for the anime from the title.. ** This process should be pretty straight forward except for when the ** title of the anime contains special characters. */ function guessURL(title, dub, isManga) { if (title) { title = removeDiacritics(title); // Remove all diacritics title = title.replace(/[^\w\s\\/;:.\-,★☆]/g, ''); // Remove special characters title = title.replace(/[ \\/;:.,★☆]/g, '-'); // Remove whitespace title = title.replace(/-{2,}/g, '-'); // Remove dashes in a row title = title.replace(/\-$/g, ''); // Remove dash at the end of the string if (isManga) return `https://kissmanga.com/Manga/${title}${dub?'-dub':''}`; else return `https://kissanime.ru/Anime/${title}${dub?'-dub':''}`; } else { return false; } }