您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Displays tags cloud on profile pages.
// ==UserScript== // @name MyAnimeList (MAL) Tags Cloud // @namespace https://greasyfork.org/users/7517 // @description Displays tags cloud on profile pages. // @icon http://i.imgur.com/b7Fw8oH.png // @license Unlicense // @version 3.1.3 // @author akarin // @include /^https?:\/\/myanimelist\.net\/profile/ // @grant none // @noframes // ==/UserScript== (function () { 'use strict'; const STATUS = { ALL: 7, IN_PROGRESS: 1, COMPLETED: 2, ON_HOLD: 3, DROPPED: 4, PLAN_TO: 6 }; const OPTS = { CACHE_VERSION: '3.0.1', WIDTH: 900, HEIGHT: 700, FONT_MIN: 9, FONT_MAX: 70, FETCH_DELAY: 1000, FETCH_TIMEOUT: 30000 }; function fetchUrl (url, timeout, delay) { if (timeout === undefined) { timeout = OPTS.FETCH_TIMEOUT; } if (delay === undefined) { delay = 0; } let isTimeout = false; return new Promise((resolve, reject) => setTimeout(() => { const t = setTimeout(() => { isTimeout = true; reject(new Error('fetch timeout')); }, timeout); fetch(url) .then((response) => { clearTimeout(t); if (!isTimeout) { resolve(response); } }) .catch((err) => { if (!isTimeout) { reject(err); } }); }, delay)); } class Cache { constructor (name, username) { this.name = name; this.username = username; } encodeKey (key) { return this.name + '#' + OPTS.CACHE_VERSION + '#' + this.username + '#' + key; } loadValue (key, value) { try { return JSON.parse(localStorage.getItem(this.encodeKey(key))) || value; } catch (e) { console.log(e.name + ': ' + e.message); return value; } } saveValue (key, value) { localStorage.setItem(this.encodeKey(key), JSON.stringify(value)); } } class Fancybox { constructor () { this.body = document.createElement('div'); this.body.setAttribute('id', 'tc_fancybox_inner'); this.outer = document.createElement('div'); this.outer.setAttribute('id', 'tc_fancybox_outer'); this.outer.appendChild(this.body); this.wrapper = document.createElement('div'); this.wrapper.setAttribute('id', 'tc_fancybox_wrapper'); this.wrapper.onlick = () => this.hide(); this.hide(); } init (node) { node.parentNode.insertBefore(this.outer, node.nextSibling); node.parentNode.insertBefore(this.wrapper, node.nextSibling); this.wrapper.onclick = () => this.hide(); } show (callback) { if (this.body.hasChildNodes()) { Array.from(this.body.childNodes).forEach((node) => { node.style.display = 'none'; }); } if (callback === undefined || callback === true || callback()) { this.wrapper.style.display = ''; this.outer.style.display = ''; } else { this.hide(); } } hide () { this.outer.style.display = 'none'; this.wrapper.style.display = 'none'; } } class MalData { constructor (username, type, offset, timeout, delay) { this.username = username; this.type = type; this.offset = offset; this.timeout = timeout; this.delay = delay; this.data = {}; this.size = 0; this.running = false; } clear () { this.running = false; this.data = {}; this.size = 0; } load (offset) { return new Promise((resolve, reject) => { fetchUrl('/' + this.type + 'list/' + this.username + '/load.json?status=7&offset=' + offset, this.timeout, this.delay) .then((response) => response.json()) .then((data) => resolve(data)) .catch((err) => reject(err)); }); } async populate (callbacks, filter) { if (this.running) { return; } this.clear(); this.running = true; const hasFilter = Array.isArray(filter) && filter.length > 0; for (let offset = 0; ; offset = offset + this.offset) { let data; try { data = await this.load(offset); } catch (err) { this.clear(); if (callbacks.hasOwnProperty('onError')) { callbacks.onError(); } break; } if (!Array.isArray(data) || data.length === 0) { break; } for (const entry of data) { this.data[entry[this.type + '_id']] = hasFilter ? Object.keys(entry) .filter((key) => filter.includes(key)) .reduce((obj, key) => { obj[key] = entry[key]; return obj; }, {}) : entry; } this.size = this.size + data.length; if (callbacks.hasOwnProperty('onNext')) { callbacks.onNext(this.size); } } let data = Object.assign({}, this.data); this.clear(); if (callbacks.hasOwnProperty('onFinish')) { callbacks.onFinish(data); } } } class TagsCloud { constructor (username, fancybox) { this.username = username; this.fancybox = fancybox; this.cache = new Cache('mal_tags_cloud', this.username); this.content = {}; this.status = {}; this.tags = {}; this.loader = {}; this.onclick = {}; this.header = document.createElement('div'); this.header.classList.add('tc_title'); ['anime', 'manga'].forEach((type) => { let node = document.createElement('select'); node.setAttribute('id', 'tc_sel_' + type); this.header.appendChild(node); }); this.header.appendChild(document.createElement('span')); this.header.appendChild(document.createTextNode(' (')); ['anime', 'manga'].forEach((type) => { let node = document.createElement('a'); node.setAttribute('id', 'tc_upd_' + type); node.setAttribute('href', 'javascript:void(0);'); node.setAttribute('title', 'Update tags'); node.appendChild(document.createTextNode('update')); this.header.appendChild(node); }); this.header.appendChild(document.createTextNode(') ')); ['anime', 'manga'].forEach((type) => { let node = document.createElement('sup'); node.setAttribute('id', 'tc_sup_' + type); this.header.appendChild(node); }); this.body = document.createElement('div'); this.body.setAttribute('id', 'tags_cloud'); this.body.appendChild(this.header); } init (type, onStatusChange) { let node = document.createElement('div'); node.classList.add('tags_list'); let el = document.createElement('div'); el.classList.add('tags_not_found'); node.appendChild(el); this.body.appendChild(node); this.content[type] = node; this.status[type] = STATUS.ALL; this.tags[type] = this.cache.loadValue(type, {}); this.loader[type] = new MalData(this.username, type, 300, OPTS.FETCH_TIMEOUT, OPTS.FETCH_DELAY); this.onclick[type] = () => {}; node = this.header.querySelector('select#tc_sel_' + type); if (node) { while (node.hasChildNodes()) { node.removeChild(node.lastChild); } el = document.createElement('option'); el.value = STATUS.ALL; el.appendChild(document.createTextNode('All ' + type.replace(/^a/, 'A').replace(/^m/, 'M'))); node.appendChild(el); el = document.createElement('option'); el.value = STATUS.IN_PROGRESS; el.appendChild(document.createTextNode(type === 'anime' ? 'Watching' : 'Reading')); node.appendChild(el); el = document.createElement('option'); el.value = STATUS.COMPLETED; el.appendChild(document.createTextNode('Completed')); node.appendChild(el); el = document.createElement('option'); el.value = STATUS.ON_HOLD; el.appendChild(document.createTextNode('On-Hold')); node.appendChild(el); el = document.createElement('option'); el.value = STATUS.DROPPED; el.appendChild(document.createTextNode('Dropped')); node.appendChild(el); el = document.createElement('option'); el.value = STATUS.PLAN_TO; el.appendChild(document.createTextNode('Plan to ' + (type === 'anime' ? 'Watch' : 'Read'))); node.appendChild(el); node.onchange = () => { let select = this.header.querySelector('select#tc_sel_' + type); if (select) { onStatusChange(select.value); } } } node = this.header.querySelector('a#tc_upd_' + type); if (node) { node.onclick = () => this.onclick[type](); } } show (type, status) { status = parseInt(status); this.onclick[type] = () => { if (this.loader[type].running) { alert('Updating in process!'); return; } let node = this.header.querySelector('select#tc_sel_' + type); if (node) { node.setAttribute('disabled', 'disabled'); } node = this.content[type]; while (node.hasChildNodes()) { node.removeChild(node.lastChild); } let text = document.createTextNode('Loading...'); node = document.createElement('div'); node.classList.add('tags_not_found'); node.appendChild(text); this.content[type].appendChild(node); this.loader[type].populate({ onFinish: (data) => { let tags = {}; for (const entry of Object.values(data)) { if (typeof entry.tags === 'string' || entry.tags instanceof String) { for (let tag of entry.tags.split(',')) { tag = tag.trim(); if (tag.length === 0) { continue; } if (!tags.hasOwnProperty(tag)) { tags[tag] = {}; } let keyStatus = String(entry.status); tags[tag][keyStatus] = tags[tag].hasOwnProperty(keyStatus) ? tags[tag][keyStatus] + 1 : 1; let keyTotal = String(STATUS.ALL); tags[tag][keyTotal] = tags[tag].hasOwnProperty(keyTotal) ? tags[tag][keyTotal] + 1 : 1; } } } this.tags[type] = tags; this.cache.saveValue(type, tags); this.update(type, status); }, onNext: (count) => text.nodeValue = 'Loading... (' + count + ' entries)', onError: () => text.nodeValue = 'Loading... (failed)' }, [ 'status', 'tags' ]); }; if (this.status[type] !== status || this.content[type].querySelector('.tags_not_found')) { this.update(type, status); } this.status[type] = status; const invType = type === 'anime' ? 'manga' : 'anime'; this.content[invType].style.display = 'none'; this.header.querySelectorAll( 'select#tc_sel_' + invType + ', ' + 'a#tc_upd_' + invType + ', ' + 'sup#tc_sup_' + invType ).forEach((node) => { node.style.display = 'none'; }); this.content[type].style.display = ''; this.header.querySelectorAll( 'select#tc_sel_' + type + ', ' + 'a#tc_upd_' + type + ', ' + 'sup#tc_sup_' + type ).forEach((node) => { node.style.display = ''; }); let node = this.header.querySelector('span'); if (node) { while (node.hasChildNodes()) { node.removeChild(node.lastChild); } node.appendChild(document.createTextNode('Tags Cloud — ')); let el = document.createElement('a'); el.setAttribute('id', 'tc_link_' + type); el.setAttribute('href', 'javascript:void(0);'); el.setAttribute('title', 'Switch to ' + invType); el.appendChild(document.createTextNode(type.toLowerCase().replace(/^a/, 'A').replace(/^m/, 'M'))); el.onclick = () => this.fancybox.show(() => this.show(invType, this.status[invType])); node.appendChild(el); node.appendChild(document.createTextNode(' · ' + this.username)); } this.body.style.display = ''; return true; } update (type, status) { if (this.loader[type].running) { return; } let node = this.header.querySelector('select#tc_sel_' + type); if (node) { node.setAttribute('disabled', 'disabled'); } this.remap(type, status); if (node) { node.removeAttribute('disabled'); } } remap (type, status) { status = parseInt(status); const keyStatus = String(status); let node = this.content[type]; while (node.hasChildNodes()) { node.removeChild(node.lastChild); } let max = 0; let min = Number.MAX_VALUE; let tags = {}; for (const [tag, data] of Object.entries(this.tags[type])) { if (data.hasOwnProperty(keyStatus)) { tags[tag] = data[keyStatus]; max = Math.max(max, tags[tag]); min = Math.min(min, tags[tag]); } } let keys = Object.keys(tags) .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())); const len = keys.length; if (len === 0) { node = document.createElement('div'); node.classList.add('tags_not_found'); node.appendChild(document.createTextNode('Tags not found')); this.content[type].appendChild(node); node = this.header.querySelector('sup#tc_sup_' + type); if (node) { while (node.hasChildNodes()) { node.removeChild(node.lastChild); } } return; } node = this.header.querySelector('sup#tc_sup_' + type); if (node) { while (node.hasChildNodes()) { node.removeChild(node.lastChild); } if (len > 1) { node.appendChild(document.createTextNode('Total: ' + len)); } } const sizeDiff = max - min; const fontDiff = OPTS.FONT_MAX - OPTS.FONT_MIN; const tagLink = '/' + type + 'list/' + this.username + '?status=' + status + '&tag='; keys.forEach((tag, i) => { const count = tags[tag]; const fontSize = OPTS.FONT_MIN + (fontDiff * (count - min) / sizeDiff); let span = document.createElement('span'); span.classList.add('cloud_tag'); span.style.fontSize = Math.round(fontSize) + 'px'; span.style.margin = '0 ' + Math.round(fontSize / 4) + 'px'; let el = document.createElement('a'); el.setAttribute('href', tagLink + tag); el.setAttribute('target', '_blank'); el.appendChild(document.createTextNode(tag)); span.appendChild(el); if (count > 1) { el = document.createElement('sup'); el.appendChild(document.createTextNode(String(count))); span.appendChild(el); } node = this.content[type]; node.appendChild(span); if (i + 1 < len) { node.appendChild(document.createTextNode(', ')); } }); } } const username = document.querySelector('.user-profile .user-function .icon-user-function#comment') .getAttribute('href').match(/\/([^/]+)#lastcomment$/)[1].trim(); let fancybox = new Fancybox(); fancybox.init(document.querySelector('#contentWrapper')); let cloud = new TagsCloud(username, fancybox); fancybox.body.appendChild(cloud.body); ['anime', 'manga'].forEach((type) => { cloud.init(type, (status) => fancybox.show(() => cloud.show(type, status))); let el = document.querySelector('.profile .user-statistics .user-statistics-stats .updates.' + type + ' > h5'); if (el) { let history = el.querySelector('a[href*="/history/"]'); if (history) { history.textContent = history.textContent.replace(/^(Anime|Manga)\sHistory$/, 'History'); let node = document.createElement('a'); node.setAttribute('href', 'javascript:void(0);'); node.classList.add('floatRightHeader'); node.classList.add('ff-Verdana'); node.classList.add('mr4'); node.appendChild(document.createTextNode('Tags Cloud')); node.onclick = () => fancybox.show(() => cloud.show(type, cloud.status[type])); el.insertBefore(node, history.nextSibling); let span = document.createElement('span'); span.classList.add('floatRightHeader'); span.classList.add('ff-Verdana'); span.classList.add('mr4'); span.appendChild(document.createTextNode('-')); el.insertBefore(span, node); } } }); let style = document.createElement('style'); style.setAttribute('type', 'text/css'); style.appendChild(document.createTextNode( 'div#tc_fancybox_wrapper { position: fixed; width: 100%; height: 100%; top: 0; left: 0; background: rgba(102, 102, 102, 0.3); z-index: 99990; }' + 'div#tc_fancybox_inner { width: ' + OPTS.WIDTH + 'px !important; height: ' + OPTS.HEIGHT + 'px !important; overflow: hidden; }' + 'div#tc_fancybox_outer { position: absolute; display: block; width: auto; height: auto; padding: 10px; border-radius: 8px; top: 80px; left: 50%; margin-top: 0 !important; margin-left: ' + (-OPTS.WIDTH / 2) + 'px !important; background: #fff; box-shadow: 0 0 15px rgba(32, 32, 32, 0.4); z-index: 99991; }' + 'div#tags_cloud { width: 100%; height: 100%; text-align: center; padding-top: 45px; box-sizing: border-box; }' + 'div#tags_cloud sup { color: #90a0b0; font-weight: lighter; }' + 'div#tags_cloud .tc_title { position: absolute; top: 10px; left: 10px; width: ' + OPTS.WIDTH + 'px; font-size: 16px; font-weight: normal; text-align: center; margin: 0; border: 0; }' + 'div#tags_cloud .tc_title select { position: absolute; left: 0; top: 0; }' + 'div#tags_cloud .tc_title a[id^="tc_upd_"] { font-size: 12px; font-weight: normal; }' + 'div#tags_cloud .tc_title sup { position: absolute; right: 0; top: 0; }' + 'div#tags_cloud .tc_title:after { content: ""; display: block; position: relative; width: 100%; height: 8px; margin: 0.5em 0 0; padding: 0; border-top: 1px solid #ebebeb; background: center bottom no-repeat radial-gradient(#f6f6f6, #fff 70%); background-size: 100% 16px; }' + 'div.tags_list { width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; color: #666; font-size:' + OPTS.FONT_MIN + 'px; border: 1px solid #eee; box-sizing: border-box; }' + 'div.tags_list .cloud_tag { white-space: nowrap; }' + 'div.tags_list .tags_not_found { font-size: 12px; font-weight: normal; margin-top: 20px; }' )); document.querySelector('head').appendChild(style); }());