Bangumi 年鉴

根据Bangumi的时光机数据生成年鉴

目前为 2025-03-05 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Bangumi 年鉴
  3. // @description 根据Bangumi的时光机数据生成年鉴
  4. // @namespace syaro.io
  5. // @version 1.3.13
  6. // @author 神戸小鳥 @vickscarlet
  7. // @license MIT
  8. // @icon https://bgm.tv/img/favicon.ico
  9. // @homepage https://github.com/bangumi/scripts/blob/master/vickscarlet/bangumi_report.user.js
  10. // @match *://bgm.tv/user/*
  11. // @match *://chii.in/user/*
  12. // @match *://bangumi.tv/user/*
  13. // ==/UserScript==
  14. (async () => {
  15. /**merge:js=common/dom.script.js**/
  16. class LoadScript { static #loaded = new Set(); static #pedding = new Map(); static async load(src) { if (this.#loaded.has(src)) return; const list = this.#pedding.get(src) ?? []; const pedding = new Promise((resolve) => list.push(resolve)); if (!this.#pedding.has(src)) { this.#pedding.set(src, list); const script = create('script', { src, type: 'text/javascript' }); script.onload = () => { this.#loaded.add(src); list.forEach((resolve) => resolve()); }; document.body.appendChild(script); } return pedding; } }
  17. /**merge**/
  18. /**merge:js=common/dom.style.js**/
  19. function addStyle(...styles) { const style = document.createElement('style'); style.append(document.createTextNode(styles.join('\n'))); document.head.appendChild(style); return style; }
  20. /**merge**/
  21. /**merge:js=common/dom.js**/
  22. function setEvents(element, events) { for (const [event, listener] of events) { element.addEventListener(event, listener); } return element; }
  23. function setProps(element, props) { if (!props || typeof props !== 'object') return element; const events = []; for (const [key, value] of Object.entries(props)) { if (typeof value === 'boolean') { element[key] = value; continue; } if (key === 'events') { if (Array.isArray(value)) { events.push(...value); } else { for (const event in value) { events.push([event, value[event]]); } } } else if (key === 'class') { addClass(element, value); } else if (key === 'style' && typeof value === 'object') { setStyle(element, value); } else if (key.startsWith('on')) { events.push([key.slice(2).toLowerCase(), value]); } else { element.setAttribute(key, value); } } setEvents(element, events); return element; }
  24. function addClass(element, value) { element.classList.add(...[value].flat()); return element; }
  25. function setStyle(element, styles) { for (let [k, v] of Object.entries(styles)) { if (v && typeof v === 'number' && !['zIndex', 'fontWeight'].includes(k)) v += 'px'; element.style[k] = v; } return element; }
  26. function create(name, props, ...childrens) { if (name == null) return null; if (name === 'svg') return createSVG(name, props, ...childrens); const element = name instanceof Element ? name : document.createElement(name); if (props === undefined) return element; if (Array.isArray(props) || props instanceof Node || typeof props !== 'object') return append(element, props, ...childrens); return append(setProps(element, props), ...childrens); }
  27. function append(element, ...childrens) { if (element.name === 'svg') return appendSVG(element, ...childrens); for (const child of childrens) { if (Array.isArray(child)) element.append(create(...child)); else if (child instanceof Node) element.appendChild(child); else element.append(document.createTextNode(child)); } return element; }
  28. function createSVG(name, props, ...childrens) { const element = document.createElementNS('http://www.w3.org/2000/svg', name); if (name === 'svg') element.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); if (props === undefined) return element; if (Array.isArray(props) || props instanceof Node || typeof props !== 'object') return append(element, props, ...childrens); return appendSVG(setProps(element, props), ...childrens); }
  29. function appendSVG(element, ...childrens) { for (const child of childrens) { if (Array.isArray(child)) element.append(createSVG(...child)); else if (child instanceof Node) element.appendChild(child); else element.append(document.createTextNode(child)); } return element; }
  30. function removeAllChildren(element) { while (element.firstChild) element.removeChild(element.firstChild); return element; }
  31. function createTextSVG(text, fontClass) { const testWidthElement = create('span', { class: fontClass, style: { fontSize: '10px', position: 'absolute', opacity: 0 }, }, text); append(document.body, testWidthElement); const w = testWidthElement.offsetWidth; testWidthElement.remove(); return createSVG('svg', { class: fontClass, fill: 'currentColor', viewBox: `0 0 ${w} 10` }, ['text', { 'font-size': 10 }, text,]); }
  32. async function newTab(href) { create('a', { href, target: '_blank' }).click(); }
  33. /**merge**/
  34. /**merge:js=common/util.js**/
  35. function callWhenDone(fn) { let done = true; return async () => { if (!done) return; done = false; await fn(); done = true; }; }
  36. function callNow(fn) { fn(); return fn; }
  37. function map(list, fn, ret = []) { let i = 0; for (const item of list) { const result = fn(item, i, list); ret.push(result); i++; } return ret; }
  38. /**merge**/
  39. /**merge:js=common/database.js**/
  40. class Cache { constructor({ hot, last }) { this.#hotLimit = hot ?? 0; this.#lastLimit = last ?? 0; this.#cacheLimit = this.#hotLimit + this.#lastLimit; } #hotLimit; #lastLimit; #cacheLimit; #hotList = []; #hot = new Set(); #last = new Set(); #pedding = new Set(); #cache = new Map(); #times = new Map(); #cHot(key) { if (!this.#hotLimit) return false; const counter = this.#times.get(key) || { key, cnt: 0 }; counter.cnt++; this.#times.set(key, counter); if (this.#hot.size == 0) { this.#hotList.push(counter); this.#hot.add(key); this.#pedding.delete(key); return true; } const i = this.#hotList.indexOf(counter); if (i == 0) return true; if (i > 0) { const up = this.#hotList[i - 1]; if (counter.cnt > up.cnt) this.#hotList.sort((a, b) => b.cnt - a.cnt); return true; } if (this.#hot.size < this.#hotLimit) { this.#hotList.push(counter); this.#hot.add(key); this.#pedding.delete(key); return true; } const min = this.#hotList.at(-1); if (counter.cnt <= min.cnt) return false; this.#hotList.pop(); this.#hot.delete(min.key); if (!this.#last.has(min.key)) this.#pedding.add(min.key); this.#hotList.push(counter); this.#hot.add(key); this.#pedding.delete(key); return true; } #cLast(key) { if (!this.#lastLimit) return false; this.#last.delete(key); this.#last.add(key); this.#pedding.delete(key); if (this.#last.size <= this.#lastLimit) return true; const out = this.#last.values().next().value; this.#last.delete(out); if (!this.#hot.has(out)) this.#pedding.add(out); return true; } async get(key, query) { const data = this.#cache.get(key) ?? (await query()); const inHot = this.#cHot(key); const inLast = this.#cLast(key); if (inHot || inLast) this.#cache.set(key, data); let i = this.#cache.size - this.#cacheLimit; if (!i) return data; for (const key of this.#pedding) { if (!i) return data; this.#cache.delete(key); this.#pedding.delete(key); i--; } return data; } update(key, value) { if (!this.#cache.has(key)) this.#cache.set(key, value); } clear() { this.#cache.clear(); } }
  41. class Collection { constructor(master, { collection, options, indexes, cache }) { this.#master = master; this.#collection = collection; this.#options = options; this.#indexes = indexes; if (cache && cache.enabled) { this.#cache = new Cache(cache); } } #master; #collection; #options; #indexes; #cache = null; get collection() { return this.#collection; } get options() { return this.#options; } get indexes() { return this.#indexes; } async transaction(handler, mode) { return this.#master.transaction(this.#collection, async (store) => { const request = await handler(store); return new Promise((resolve, reject) => { request.addEventListener('error', (e) => reject(e)); request.addEventListener('success', () => resolve(request.result)); }); }, mode); } #index(store, index = '') { if (!index) return store; return store.index(index); } async get(key, index) { const handler = () => this.transaction((store) => this.#index(store, index).get(key)); if (this.#cache && this.#options.keyPath && !index) return this.#cache.get(key, handler); return handler(); } async getAll(key, count, index) { return this.transaction((store) => this.#index(store, index).getAll(key, count)); } async getAllKeys(key, count, index) { return this.transaction((store) => this.#index(store, index).getAllKeys(key, count)); } async put(data) { if (this.#cache) { let key; if (Array.isArray(this.#options.keyPath)) { key = []; for (const path of this.#options.keyPath) { key.push(data[path]); } key = key.join('/'); } else { key = data[this.#options.keyPath]; } this.#cache.update(key, data); } return this.transaction((store) => store.put(data), 'readwrite').then((_) => true); } async delete(key) { return this.transaction((store) => store.delete(key), 'readwrite').then((_) => true); } async clear() { if (this.#cache) this.#cache.clear(); return this.transaction((store) => store.clear(), 'readwrite').then((_) => true); } }
  42. class Database { constructor({ dbName, version, collections, blocked }) { this.#dbName = dbName; this.#version = version; this.#blocked = blocked || { alert: false }; for (const options of collections) { this.#collections.set(options.collection, new Collection(this, options)); } } #dbName; #version; #collections = new Map(); #db; #blocked; async init() { this.#db = await new Promise((resolve, reject) => { const request = window.indexedDB.open(this.#dbName, this.#version); request.addEventListener('error', () => reject({ type: 'error', message: request.error })); request.addEventListener('blocked', () => { const message = this.#blocked?.message || 'indexedDB is blocked'; if (this.#blocked?.alert) alert(message); reject({ type: 'blocked', message }); }); request.addEventListener('success', () => resolve(request.result)); request.addEventListener('upgradeneeded', () => { for (const c of this.#collections.values()) { const { collection, options, indexes } = c; let store; if (!request.result.objectStoreNames.contains(collection)) store = request.result.createObjectStore(collection, options); else store = request.transaction.objectStore(collection); if (!indexes) continue; for (const { name, keyPath, unique } of indexes) { if (store.indexNames.contains(name)) continue; store.createIndex(name, keyPath, { unique }); } } }); }); return this; } async transaction(collection, handler, mode = 'readonly') { if (!this.#db) await this.init(); return new Promise(async (resolve, reject) => { const transaction = this.#db.transaction(collection, mode); const store = transaction.objectStore(collection); const result = await handler(store); transaction.addEventListener('error', (e) => reject(e)); transaction.addEventListener('complete', () => resolve(result)); }); } async get(collection, key, index) { return this.#collections.get(collection).get(key, index); } async getAll(collection, key, count, index) { return this.#collections.get(collection).getAll(key, count, index); } async getAllKeys(collection, key, count, index) { return this.#collections.get(collection).getAllKeys(key, count, index); } async put(collection, data) { return this.#collections.get(collection).put(data); } async delete(collection, key) { return this.#collections.get(collection).delete(key); } async clear(collection) { return this.#collections.get(collection).clear(); } async clearAll() { for (const c of this.#collections.values()) await c.clear(); return true; } }
  43. /**merge**/
  44. /**merge:js=common/event.js**/
  45. class Event { static #listeners = new Map(); static on(event, listener) { if (!this.#listeners.has(event)) this.#listeners.set(event, new Set()); this.#listeners.get(event).add(listener); } static emit(event, ...args) { if (!this.#listeners.has(event)) return; for (const listener of this.#listeners.get(event).values()) listener(...args); } static off(event, listener) { if (!this.#listeners.has(event)) return; this.#listeners.get(event).delete(listener); } }
  46. /**merge**/
  47. addStyle(/**merge:css=bangumi_report.user.css**/`.btn { user-select: none; cursor: pointer; }.btn.primary { background: #fc899488; }.btn.primary:hover { background: #fc8994; }.btn.danger { background: #fc222288; }.btn.danger:hover { background: #fc2222; }.btn.success { background: #22fc2288; }.btn.success:hover { background: #22fc22; }.btn.warning { background: #fcb12288; }.btn.warning:hover { background: #fcb122; }#kotori-report-canvas::-webkit-scrollbar, #kotori-report .scroll::-webkit-scrollbar { display: none; }#kotori-report-menu::before {position: absolute;content: "菜单";padding: 0 20px;top: -1px;right: -1px;left: -1px;height: 30px;line-height: 30px;background: #fc8994;backdrop-filter: blur(4px);border-radius: 10px 10px 0 0;}#kotori-report-menu {color: #fff;position: fixed;display: flex;flex-direction: column;top: 50%;left: 50%;transform: translate(-50%, -50%);padding: 20px;padding-top: 50px;background: #0d111788;backdrop-filter: blur(4px);border-radius: 10px;box-shadow: 2px 2px 10px #00000088;border: 1px solid #fc899422;min-width: 150px;> li:first-child { margin-top: 0; }> li {margin-top: 10px;> .btn-group {display: flex;gap: 10px;> .btn {width: 100%;padding: 10px 0;text-align: center;border-radius: 5px;transition: all 0.3s;font-size: 16px;font-weight: bold;}> .btn:hover {width: 100%;padding: 10px 0;text-align: center;border-radius: 5px;transition: all 0.3s;}}}> li:last-child {height: 20px;}fieldset {display: flex;gap: 5px;min-inline-size: min-content;margin-inline: 1px;border-width: 1px;border-style: groove;border-color: threedface;border-image: initial;padding-block: 0.35em 0.625em;padding-inline: 0.75em;> div {display: flex;gap: 2px;justify-content: center;}}}#kotori-report {color: #fff;position: fixed;top: 0;left: 0;right: 0;bottom: 0;> .close {position: absolute;top: 0;right: 0;left: 0;bottom: 0;background: rgba(0,0,0,0.3);backdrop-filter: blur(2px);}> .save {position: absolute;top: 10px;right: 10px;width: 40px;height: 40px;background: #fc8994;border-radius: 40px;border: 4px solid #fc8994;cursor: pointer;box-shadow: 2px 2px 10px #00000088;user-select: none;line-height: 40px;background-size: 40px;background-image: url(data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMzMwIDMzMCI+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE2NSwwQzc0LjAxOSwwLDAsNzQuMDE4LDAsMTY1YzAsOTAuOTgsNzQuMDE5LDE2NSwxNjUsMTY1czE2NS03NC4wMiwxNjUtMTY1QzMzMCw3NC4wMTgsMjU1Ljk4MSwwLDE2NSwweiBNMTY1LDMwMGMtNzQuNDM5LDAtMTM1LTYwLjU2MS0xMzUtMTM1UzkwLjU2MSwzMCwxNjUsMzBzMTM1LDYwLjU2MSwxMzUsMTM1UzIzOS40MzksMzAwLDE2NSwzMDB6Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTIxMS42NjcsMTI3LjEyMWwtMzEuNjY5LDMxLjY2NlY3NWMwLTguMjg1LTYuNzE2LTE1LTE1LTE1Yy04LjI4NCwwLTE1LDYuNzE1LTE1LDE1djgzLjc4N2wtMzEuNjY1LTMxLjY2NmMtNS44NTctNS44NTctMTUuMzU1LTUuODU3LTIxLjIxMywwYy01Ljg1OCw1Ljg1OS01Ljg1OCwxNS4zNTUsMCwyMS4yMTNsNTcuMjcxLDU3LjI3MWMyLjkyOSwyLjkzLDYuNzY4LDQuMzk1LDEwLjYwNiw0LjM5NWMzLjgzOCwwLDcuNjc4LTEuNDY1LDEwLjYwNy00LjM5M2w1Ny4yNzUtNTcuMjcxYzUuODU3LTUuODU3LDUuODU4LTE1LjM1NSwwLjAwMS0yMS4yMTVDMjI3LjAyMSwxMjEuMjY0LDIxNy41MjQsMTIxLjI2NCwyMTEuNjY3LDEyNy4xMjF6Ii8+PHBhdGggZmlsbD0iI2ZmZiIgZD0iTTE5NSwyNDBoLTYwYy04LjI4NCwwLTE1LDYuNzE1LTE1LDE1YzAsOC4yODMsNi43MTYsMTUsMTUsMTVoNjBjOC4yODQsMCwxNS02LjcxNywxNS0xNUMyMTAsMjQ2LjcxNSwyMDMuMjg0LDI0MCwxOTUsMjQweiIvPjwvc3ZnPg==);opacity: 0.8;z-index: 9999999999999;}> .scroll {position: absolute;top: 0;bottom: 0;left: 50%;transform: translateX(-50%);overflow: scroll;> .content {display: flex;flex-direction: column;gap: 5px;width: 1078px;margin: 0 auto;.banner {height: 110px;background: #fc899488;backdrop-filter: blur(2px);color: #fff;text-shadow: 0 0 5px #000;h1 {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);font-size: 36px;line-height: 36px;text-align: center;}.uid {position: absolute;top: 5px;left: 5px;font-size: 20px;}ul.bars {position: absolute;display: flex;flex-direction: column;justify-content: space-evenly;> li {position: relative;justify-content: center;> div:last-child {position: absolute;width: 60px;top: 50%;transform: translateY(-50%);height: 3px;transition: all 0.3s;> div {position: absolute;top: 0;height: 100%;background: #fff;}}}}ul.lb {align-items: flex-end;> li {> div:first-child {text-align: left;padding-left: 65px;}> div:last-child {left: 0;> div { right: 0; }}}}ul.rb {align-items: flex-start;> li {> div:first-child {text-align: right;padding-right: 65px;}> div:last-child {right: 0;> div { left: 0; }}}}ul.total-time {font-family: consolas, 'courier new', monospace, courier;bottom: 0;left: 0;> li > div:first-child {width: 150px;}}ul.includes {top: 0;right: 0;> li > div:first-child {width: 80px;}}}ul.year-cover {display: flex;flex-direction: column;gap: 5px;> li {position: relative;> h2 {position: relative;padding: 2px;text-align: center;background: #fc899488;backdrop-filter: blur(2px);color: #fff;font-weight: bold;text-shadow: 0 0 4px #000;> span {position: absolute;top: 50%;right: 10px;transform: translateY(-50%);font-size: 14px;color: #ffde20;}}}> li:before {content: "";display: block;position: absolute;top: 0;right: 0;bottom: 0;left: 0;border: 1px solid #fc8994;box-sizing: border-box;}}> .bar-group {display: flex;justify-content: space-between;align-items: flex-end;ul.bars {display: flex;flex-direction: column;gap: 2px;position: relative;width: calc(50% - 1px);> li {display: block;position: relative;width: 100%;height: 20px;background: #0008;margin: 0;line-height: 20px;backdrop-filter: blur(2px);> span {position: absolute;left: 5px;text-shadow: 0 0 2px #000;}> span:nth-child(2) {position: absolute;left: 50%;transform: translateX(-50%);}> div {display: inline-block;height: 100%;background: #fc8994aa;margin: 0;}}}}ul.covers[type="music"] > li { height: 150px; }ul.covers {line-height: 0;> li {display: inline-block;position: relative;width: 150px;height: 220px;margin: 2px;overflow: hidden;border-width: 1px;border-style: solid;border-color: #fc8994;box-sizing: border-box;img {max-height: 100%;position: absolute;top: 0;left: 50%;transform: translateX(-50%);}> span {width: 50px;height: 30px;position: absolute;top: 0;left: 0;line-height: 30px;text-align: center;font-size: 18px;background: #8c49548c;backdrop-filter: blur(2px);}.star {display: block;position: absolute;bottom: 3px;right: 3px;width: 20px;height: 20px;padding: 5px;background: none;> img {opacity: 0.85;}> span {position: absolute;top: 50%;left: 50%;color: #f4a;font-family: consolas, 'courier new', monospace, courier;font-size: 18px;font-weight: bold;text-shadow: 0 0 2px #fff;transform: translate(-50%, -50%);}}}}}}}#kotori-report-canvas {color: #fff;position: fixed;top: 0;left: 0;right: 0;bottom: 0;background: rgba(0,0,0,0.3);backdrop-filter: blur(2px);overflow: scroll;padding: 30px;scrollbar-width: none;-ms-overflow-style: none;> div {position: absolute;top: 0;right: 0;left: 0;bottom: 0;background: rgba(0,0,0,0.3);backdrop-filter: blur(2px);}> canvas {position: absolute;top: 0;left: 50%;transform: translateX(-50%);}}@media screen and (min-width: 616px) { #kotori-report .content { width: 616px !important; } }@media screen and (min-width: 830px) { #kotori-report .content { width: 770px !important; } }@media screen and (min-width: 924px) { #kotori-report .content { width: 924px !important; } }@media screen and (min-width: 1138px) { #kotori-report .content { width: 1078px !important; } }`/**merge**/)
  48.  
  49. const uid = /\/user\/(.+)?(\/.*)?/.exec(window.location.href)?.[1];
  50. const PRG = ['|', '/', '-', '\\'];
  51. const STAR_PATH = 'M60.556381,172.206 C60.1080307,172.639 59.9043306,173.263 60.0093306,173.875 L60.6865811,177.791 C60.8976313,179.01 59.9211306,180 58.8133798,180 C58.5214796,180 58.2201294,179.931 57.9282291,179.779 L54.3844766,177.93 C54.1072764,177.786 53.8038262,177.714 53.499326,177.714 C53.1958758,177.714 52.8924256,177.786 52.6152254,177.93 L49.0714729,179.779 C48.7795727,179.931 48.4782224,180 48.1863222,180 C47.0785715,180 46.1020708,179.01 46.3131209,177.791 L46.9903714,173.875 C47.0953715,173.263 46.8916713,172.639 46.443321,172.206 L43.575769,169.433 C42.4480682,168.342 43.0707186,166.441 44.6289197,166.216 L48.5916225,165.645 C49.211123,165.556 49.7466233,165.17 50.0227735,164.613 L51.7951748,161.051 C52.143775,160.35 52.8220755,160 53.499326,160 C54.1776265,160 54.855927,160.35 55.2045272,161.051 L56.9769285,164.613 C57.2530787,165.17 57.7885791,165.556 58.4080795,165.645 L62.3707823,166.216 C63.9289834,166.441 64.5516338,168.342 63.423933,169.433 L60.556381,172.206 Z';
  52. const STAR_SVG = `<svg fill="#ffde20" width="800px" height="800px" viewBox="43 159.5 21 21" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="${STAR_PATH}"></path></svg>`;
  53. const STAR_URL = URL.createObjectURL(new Blob([STAR_SVG], { type: 'image/svg+xml' }));
  54.  
  55. const Types = {
  56. anime: { sort: 1, value: 'anime', name: '动画', action: '看', unit: '部' },
  57. game: { sort: 2, value: 'game', name: '游戏', action: '玩', unit: '部' },
  58. music: { sort: 3, value: 'music', name: '音乐', action: '听', unit: '张' },
  59. book: { sort: 4, value: 'book', name: '图书', action: '读', unit: '本' },
  60. real: { sort: 5, value: 'real', name: '三次元', action: '看', unit: '部' },
  61. }
  62. const SubTypes = {
  63. collect: { sort: 1, value: 'collect', name: '$过', checked: true },
  64. do: { sort: 2, value: 'do', name: '在$', checked: false },
  65. dropped: { sort: 3, value: 'dropped', name: '抛弃', checked: false },
  66. on_hold: { sort: 4, value: 'on_hold', name: '搁置', checked: false },
  67. wish: { sort: 5, value: 'wish', name: '想$', checked: false },
  68. };
  69. const AnimeTypeTimes = {
  70. WEB: 23 * 60 + 40,
  71. TV: 23 * 60 + 40,
  72. OVA: 45 * 60,
  73. OAD: 45 * 60,
  74. 剧场版: 90 * 60,
  75. }
  76.  
  77. function formatSubType(subType, type) {
  78. const action = Types[type].action;
  79. return SubTypes[subType].name.replace('$', action);
  80. }
  81. const db = new Database({
  82. dbName: 'VReport',
  83. version: 6,
  84. collections: [
  85. { collection: 'pages', options: { keyPath: 'url' }, indexes: [{ name: 'url', keyPath: 'url', unique: true }] },
  86. { collection: 'times', options: { keyPath: 'id' }, indexes: [{ name: 'id', keyPath: 'id', unique: true }] }
  87. ]
  88. });
  89.  
  90. function easeOut(curtime, begin, end, duration) {
  91. let x = curtime / duration;
  92. let y = -x * x + 2 * x;
  93. return begin + (end - begin) * y;
  94. }
  95.  
  96. /**
  97. * @template T
  98. * @template U
  99. * @param {Iterable<T>} list 数据列表
  100. * @param {string|(item:T)=>U} group
  101. * @param {Map<U,T>} defaultMap
  102. * @returns {Map<U,T[]>}
  103. */
  104. function groupBy(list, group) {
  105. const groups = new Map();
  106. for (const item of list) {
  107. const key = typeof group == 'function' ? group(item) : item[group];
  108. if (groups.has(key)) groups.get(key).push(item);
  109. else groups.set(key, [item]);
  110. }
  111. return groups;
  112. }
  113.  
  114. function countMap(length) {
  115. return new Map(new Array(length).fill(0).map((_, i) => [i, 0]));
  116. }
  117.  
  118. /**
  119. * @template T
  120. * @template U
  121. * @param {Iterable<T>} list 数据列表
  122. * @param {string|(item:T)=>U} group
  123. * @param {Map<U,number>} defaultMap
  124. * @returns {Map<U,number>}
  125. */
  126. function groupCount(list, group, defaultMap) {
  127. const groups = defaultMap || new Map();
  128. for (const item of list) {
  129. const key = typeof group == 'function' ? group(item) : item[group];
  130. groups.set(key, (groups.get(key) || 0) + 1);
  131. }
  132. return groups;
  133. }
  134.  
  135. // LOAD DATA START
  136. /**
  137. * 获取页面Element
  138. * @param {string} url 网址
  139. */
  140. async function f(url) {
  141. Event.emit('process', { type: 'fetch', data: { url } });
  142. const html = await fetch(window.location.origin + '/' + url).then(res => res.text());
  143. if (html.includes('503 Service Temporarily Unavailable')) return null;
  144. const e = create('html');
  145. e.innerHTML = html.replace(/<img (.*)\/?>/g, '<span class="img" $1></span>');
  146. return e;
  147. };
  148.  
  149. /**
  150. * 获取页面数据
  151. * @typedef {{
  152. * id: string
  153. * name: string
  154. * title: string
  155. * jp_title: string
  156. * img: string
  157. * time: Date
  158. * year: number
  159. * month: number
  160. * star: number
  161. * tags: string[]
  162. * subType: string
  163. * }} Item
  164. * @typedef {{
  165. * url:string
  166. * list: Item[]
  167. * max: number
  168. * time: number
  169. * tags?: string[]
  170. * }} Result
  171. * @param {string} type 类型
  172. * @param {string} subType 子类
  173. * @param {number} p 页码
  174. * @param {number} expire 过期时间
  175. */
  176. async function fl(type, subType, p = 1, expire = 30) {
  177. Event.emit('process', { type: 'parse', data: { type, subType, p } });
  178. const url = `${type}/list/${uid}/${subType}?page=${p}`;
  179. /** @type {Result} */
  180. let data = await db.get('pages', url);
  181. if (data && data.time + expire * 60000 > Date.now()) return data;
  182.  
  183. const e = await f(url);
  184. /** @type {Item[]} */
  185. const list = Array
  186. .from(e.querySelectorAll('#browserItemList > li'))
  187. .map(li => {
  188. const data = { subType };
  189. data.id = li.querySelector('a').href.split('/').pop();
  190. const title = li.querySelector('h3');
  191. data.title = title.querySelector('a').innerText;
  192. data.jp_title = title.querySelector('small')?.innerText;
  193. data.img = li.querySelector('span.img')
  194. ?.getAttribute('src').replace('cover/c', 'cover/l')
  195. || '//bgm.tv/img/no_icon_subject.png';
  196. data.time = new Date(li.querySelector('span.tip_j').innerText);
  197. data.year = data.time.getFullYear();
  198. data.month = data.time.getMonth();
  199. data.star = parseInt(li.querySelector('span.starlight')?.className.match(/stars(\d{1,2})/)[1]) || 0;
  200. data.tags = li.querySelector('span.tip')?.textContent.trim().match(/标签:\s*(.*)/)?.[1].split(/\s+/) || [];
  201. return data;
  202. });
  203. const edge = e.querySelector('span.p_edge');
  204. let max;
  205. if (edge) {
  206. max = Number(edge.textContent.match(/\/\s*(\d+)\s*\)/)?.[1] || 1);
  207. } else {
  208. const ap = e.querySelectorAll('a.p');
  209. if (ap.length == 0) {
  210. max = 1;
  211. } else {
  212. let cursor = ap[ap.length - 1];
  213. if (cursor.innerText == '››')
  214. cursor = cursor.previousElementSibling;
  215. max = Number(cursor.textContent) || 1;
  216. }
  217. }
  218. const time = Date.now();
  219. data = { url, list, max, time };
  220. if (p == 1) {
  221. const tags = Array
  222. .from(e.querySelectorAll('#userTagList > li > a.l'))
  223. .map(l => l.childNodes[1].textContent);
  224. data.tags = tags;
  225. }
  226. await db.put('pages', data);
  227. return data;
  228. }
  229.  
  230. /**
  231. * 根据类型获取tag列表
  232. * @param {string} type 类型
  233. */
  234. async function ft(type) {
  235. Event.emit('process', { type: 'tags', data: { type } });
  236. const { tags } = await fl(type, 'collect')
  237. return tags
  238. }
  239.  
  240. // calc time
  241. function calcTime(s) {
  242. let m = /[时片]长:\s*(\d{2}):(\d{2}):(\d{2})/.exec(s);
  243. if (m) return parseInt(m[1]) * 3600 + parseInt(m[2]) * 60 + parseInt(m[3]);
  244. m = /[时片]长:\s*(\d{2}):(\d{2})/.exec(s);
  245. if (m) return parseInt(m[1]) * 60 + parseInt(m[2]);
  246. m = /[时片]长:\s*(\d+)\s*[m分]/.exec(s);
  247. if (m) return parseInt(m[1]) * 60;
  248. return 0;
  249. };
  250.  
  251. async function ftime(id) {
  252. let data = await db.get('times', id);
  253. if (data) {
  254. if (data.time) return { type: 1, time: data.time, a: true };
  255. else return { time: data.eps * AnimeTypeTimes[data.type] || 0, a: false }
  256. }
  257. const e = await f(`subject/${id}/ep`);
  258. const c = l => Array.from(l).reduce((a, e) => a + calcTime(e.innerText), 0)
  259. let time = c(e.querySelectorAll('ul.line_list > li > small.grey'));
  260. if (time) {
  261. data = { id, time };
  262. await db.put('times', data);
  263. return { time, a: true };
  264. }
  265. const se = await f(`subject/${id}`);
  266. time = c(se.querySelectorAll('ul#infobox > li'));
  267. if (time) {
  268. data = { id, time };
  269. await db.put('times', data);
  270. return { time, a: true };
  271. }
  272. const type = se.querySelector('h1.nameSingle > small')?.textContent;
  273. const eps = e.querySelectorAll('ul.line_list > li > h6').length;
  274.  
  275. data = { id, type, eps };
  276. await db.put('times', data);
  277. return { time: eps * AnimeTypeTimes[type] || 0, a: false }
  278. }
  279.  
  280. /**
  281. * @param {Iterable<Item>} list
  282. */
  283. async function totalTime(list) {
  284. const total = {
  285. total: { name: '总计', time: 0, count: 0 },
  286. normal: { name: '精确', time: 0, count: 0 },
  287. guess: { name: '推测', time: 0, count: 0 },
  288. unknown: { name: '未知', time: 0, count: 0 },
  289. };
  290. Event.emit('process', { type: 'totalTime', data: { total: list.length } });
  291. for (const { id } of list) {
  292. Event.emit('process', { type: 'totalTimeItem', data: { id, count: total.total.count + 1 } });
  293. const { time, a } = await ftime(id);
  294. if (a) {
  295. total.normal.count++;
  296. total.normal.time += time;
  297. } else if (time) {
  298. total.guess.count++;
  299. total.guess.time += time;
  300. } else {
  301. total.unknown.count++;
  302. }
  303. total.total.count++;
  304. total.total.time += time;
  305. }
  306. return total;
  307. }
  308.  
  309. /**
  310. * 二分搜索年份页面范围
  311. * 使用尽可能减少请求次数的算法
  312. * @param {string} type 类型
  313. * @param {string} subtype 子类
  314. * @param {string|number} year 年份
  315. */
  316. async function bsycs(type, subtype, year) {
  317. const { max } = await fl(type, subtype);
  318. let startL = 1;
  319. let startR = 1;
  320. let endL = max;
  321. let endR = max;
  322. let dL = false;
  323. let dR = false;
  324.  
  325. while (startL <= endL && startR <= endR) {
  326. const mid = startL < endL
  327. ? Math.max(Math.min(Math.floor((startL + endL) / 2), endL), startL)
  328. : Math.max(Math.min(Math.floor((startR + endR) / 2), endR), startR)
  329. Event.emit('process', { type: 'bsycs', data: { type, subtype, p: mid } });
  330. const { list } = await fl(type, subtype, mid);
  331. if (list.length == 0) return [1, 1];
  332. const first = list[0].year;
  333. const last = list[list.length - 1].year;
  334. if (first > year && last < year) return [mid, mid];
  335.  
  336. if (last > year) {
  337. if (!dL) startL = Math.min(mid + 1, endL);
  338. if (!dR) startR = Math.min(mid + 1, endR);
  339. } else if (first < year) {
  340. if (!dL) endL = Math.max(mid - 1, startL);
  341. if (!dR) endR = Math.max(mid - 1, startR);
  342. } else if (first == last) {
  343. if (!dL) endL = Math.max(mid - 1, startL);
  344. if (!dR) startR = Math.min(mid + 1, endR);
  345. } else if (first == year) {
  346. startR = endR = mid;
  347. if (!dL) endL = Math.min(mid + 1, endR);
  348. } else if (last == year) {
  349. startL = endL = mid;
  350. if (!dL) startR = Math.min(mid + 1, endR);
  351. }
  352. if (startL == endL) dL = true;
  353. if (startR == endR) dR = true;
  354. if (dL && dR) return [startL, startR];
  355. }
  356. }
  357.  
  358. /**
  359. * 获取指定类型的数据列表
  360. * @param {string} type 类型
  361. * @param {string} subtype 子类
  362. * @param {string | number} year 年份 0为全部
  363. */
  364. async function cbt(type, subtype, year = 0) {
  365. if (!year) return cbtAll(type, subtype);
  366. return cbtYear(type, subtype, year);
  367. };
  368.  
  369. /**
  370. * 获取指定类型与年份的数据列表
  371. * @param {string} type 类型
  372. * @param {string} subtype 子类
  373. * @param {string | number} year 年份
  374. */
  375. async function cbtYear(type, subtype, year) {
  376. const [start, end] = await bsycs(type, subtype, year);
  377. Event.emit('process', { type: 'collZone', data: { zone: [start, end] } });
  378. const ret = [];
  379. for (let i = start; i <= end; i++) {
  380. const { list } = await fl(type, subtype, i);
  381. ret.push(list);
  382. }
  383. return ret.flat();
  384. }
  385.  
  386. /**
  387. * 获取指定类型的数据列表
  388. * @param {string} type 类型
  389. * @param {string} subtype 子类
  390. */
  391. async function cbtAll(type, subtype) {
  392. const { list, max } = await fl(type, subtype, 1);
  393. Event.emit('process', { type: 'collZone', data: { zone: [1, max] } });
  394. const ret = [list];
  395. for (let i = 2; i <= max; i++) {
  396. const { list } = await fl(type, subtype, i);
  397. ret.push(list);
  398. }
  399. return ret.flat();
  400. }
  401.  
  402. /**
  403. * 根据参数获取数据列表
  404. * 根据 tag 过滤数据
  405. * 根据 time 进行排序
  406. * @param {{
  407. * type: string
  408. * subType: string
  409. * tag?: string
  410. * year?: string | number
  411. * }} param0
  412. */
  413. async function collects({ type, subTypes, tag, year }) {
  414. const ret = [];
  415. for (const subtype of subTypes) {
  416. Event.emit('process', { type: 'collSubtype', data: { subtype } });
  417. const list = await cbt(type, subtype, year);
  418. ret.push(list);
  419. }
  420. const fset = new Set();
  421. return ret.flat()
  422. .filter(({ id, year: y, tags }) => {
  423. if (year && year != y) return false;
  424. if (tag && !tags.includes(tag)) return false;
  425. if (fset.has(id)) return false;
  426. fset.add(id);
  427. return true;
  428. })
  429. .sort(({ time: a }, { time: b }) => b - a);
  430. }
  431. // LOAD DATA END
  432.  
  433. // SAVE IMAGE START
  434. /**
  435. * 元素转为 canvas
  436. * @param {Element} element 元素
  437. * @param {Function} done 完成回调
  438. * @returns {Promise<void>}
  439. */
  440. async function element2Canvas(element, done) {
  441. await LoadScript.load('https://html2canvas.hertzen.com/dist/html2canvas.min.js');
  442. const canvas = await html2canvas(element, { allowTaint: true, logging: false, backgroundColor: '#1c1c1c' })
  443. const close = create('div', { style: { height: canvas.style.height } });
  444. const main = create('div', { id: 'kotori-report-canvas' }, close, canvas);
  445. close.addEventListener('click', () => main.remove());
  446. document.body.appendChild(main);
  447. done();
  448. }
  449. // SAVE IMAGE END
  450.  
  451. // REPORT START
  452. function pad02(n) { return n.toString().padStart(2, '0') }
  453. function timeFormat(time, day = false) {
  454. const s = time % 60;
  455. const m = (time - s) / 60 % 60;
  456. if (!day) {
  457. const h = (time - s - m * 60) / 3600;
  458. return `${h}:${pad02(m)}:${pad02(s)}`;
  459. }
  460. const h = (time - s - m * 60) / 3600 % 24;
  461. const d = (time - s - m * 60 - h * 3600) / 86400;
  462. if (d) return `${d}天${pad02(h)}:${pad02(m)}:${pad02(s)}`;
  463. return `${h}:${pad02(m)}:${pad02(s)}`;
  464. };
  465.  
  466. function pw(v, m) {
  467. return { style: { width: v * 100 / m + '%' } }
  468. }
  469.  
  470. /**
  471. * 生成题头时间统计
  472. * @param {Awaited<ReturnType<typeof totalTime>>} param0
  473. * @returns {AppendParams}
  474. */
  475. function buildTotalTime({ total, normal, guess, unknown }) {
  476. const list = [total, normal, guess, unknown].sort((a, b) => b.time - a.time)
  477. const format = ({ name, count, time }) => `${timeFormat(time, true)} (${count})${name}`
  478. const buildItem = (item) => ['li', ['div', format(item)], ['div', ['div', pw(item.time, total.time)]]];
  479. return ['ul', { class: ['total-time', 'bars', 'rb'] }, ...list.map(buildItem)]
  480. }
  481. /**
  482. * 生成题头统计数据
  483. * @param {Iterable<[any,number]>} list
  484. * @param {string} type 类型
  485. * @returns {AppendParams}
  486. */
  487. function buildIncludes(list, type) {
  488. list = Array.from(list).map(([k, v]) => [formatSubType(k, type), v]);
  489. const total = list.reduce((sum, [_, v]) => sum + v, 0)
  490. list.unshift(['总计', total])
  491. list.sort((a, b) => b[1] - a[1])
  492. const format = (k, v) => k + ':' + (('' + v).padStart(5, ' ')) + Types[type].unit
  493. const buildItem = ([k, v]) => ['li', ['div', format(k, v)], ['div', ['div', pw(v, total)]]];
  494. return ['ul', { class: ['includes', 'bars', 'lb'] }, ...list.map(buildItem)];
  495. }
  496.  
  497. /**
  498. * 生成条形图
  499. * @param {Iterable<[number,string|number,number]>} list
  500. * @returns {AppendParams}
  501. */
  502. function buildBarList(list) {
  503. list = Array.from(list).sort(([, , a], [, , b]) => a - b);
  504. const m = Math.max(...list.map(([v]) => v));
  505. const buildItem = ([v, t]) => ['li', ['span', t], ['span', v], ['div', pw(v, m)]]
  506. return ['ul', { class: 'bars' }, ...list.map(buildItem)];
  507. }
  508.  
  509. /**
  510. * 生成封面列表
  511. * @param {Iterable<Item>} list
  512. * @param {string} type 类型
  513. * @returns {AppendParams}
  514. */
  515. function buildCoverList(list, type) {
  516. let last = -1;
  517. const covers = [];
  518. for (const { img, month, star } of list) {
  519. const childs = [['img', { src: img }]];
  520. if (month != last) {
  521. childs.push(['span', month + 1 + '月']);
  522. last = month;
  523. }
  524. if (star)
  525. childs.push(['div', { class: 'star' }, ['img', { src: STAR_URL }], ['span', star]]);
  526. covers.push(['li', ...childs]);
  527. }
  528. return ['ul', { class: 'covers', type }, ...covers]
  529. }
  530.  
  531. /**
  532. * 根据参数生成生涯总览
  533. * 根据 tag 过滤数据
  534. * @param {{
  535. * type: string
  536. * subType: string
  537. * tag?: string
  538. * }} param0
  539. */
  540. async function buildLifeTimeReport({ type, tag, subTypes, totalTime: ttt }) {
  541. const list = await collects({ type, subTypes, tag });
  542. const time = ttt ? await totalTime(list) : null;
  543.  
  544. const buildYearCover = ([year, l]) => ['li', ['h2', year + '年', ['span', l.length]], buildCoverList(l, type)];
  545. const banner = ['div', { class: 'banner' },
  546. ['h1', `Bangumi ${Types[type].name}生涯总览`],
  547. ['span', { class: 'uid' }, '@' + uid],
  548. buildIncludes(groupCount(list, 'subType').entries(), type)
  549. ];
  550. if (time) banner.push(buildTotalTime(time));
  551. const countList = buildBarList(groupCount(list, 'month', countMap(12)).entries().map(([k, v]) => [v, k + 1 + '月', k]));
  552. const starList = buildBarList(groupCount(list, 'star', countMap(11)).entries().map(([k, v]) => [v, k ? k + '星' : '未评分', k]));
  553. const barGroup = ['div', { class: 'bar-group' }, countList, starList];
  554. const yearCover = ['ul', { class: 'year-cover' }, ...groupBy(list, 'year').entries().map(buildYearCover)];
  555.  
  556. return create('div', { class: 'content' }, banner, barGroup, yearCover);
  557. }
  558.  
  559. /**
  560. * 根据参数生成年鉴
  561. * 根据 tag 过滤数据
  562. * @param {{
  563. * year: string | number
  564. * type: string
  565. * subType: string
  566. * tag?: string
  567. * }} param0
  568. */
  569. async function buildYearReport({ year, type, tag, subTypes, totalTime: ttt }) {
  570. const list = await collects({ type, subTypes, tag, year });
  571. const time = ttt ? await totalTime(list) : null;
  572.  
  573. const banner = ['div', { class: 'banner' },
  574. ['h1', `${year}年 Bangumi ${Types[type].name}年鉴`],
  575. ['span', { class: 'uid' }, '@' + uid],
  576. buildIncludes(groupCount(list, 'subType').entries(), type)
  577. ];
  578. if (time) banner.push(buildTotalTime(time));
  579. const countList = buildBarList(groupCount(list, 'month', countMap(12)).entries().map(([k, v]) => [v, k + 1 + '月', k]));
  580. const starList = buildBarList(groupCount(list, 'star', countMap(11)).entries().map(([k, v]) => [v, k ? k + '星' : '未评分', k]));
  581. const barGroup = ['div', { class: 'bar-group' }, countList, starList];
  582.  
  583. return create('div', { class: 'content' }, banner, barGroup, buildCoverList(list, type));
  584. }
  585.  
  586. /**
  587. * 根据参数生成报告
  588. * 根据 tag 过滤数据
  589. * isLifeTime 为 true 时生成生涯报告否则生成年鉴
  590. * @param {{
  591. * isLifeTime: false
  592. * year: string | number
  593. * type: string
  594. * subType: string
  595. * tag?: string
  596. * } | {
  597. * isLifeTime: true
  598. * type: string
  599. * subType: string
  600. * tag?: string
  601. * }} options
  602. * @returns {Promise<void>}
  603. */
  604. async function buildReport(options) {
  605. Event.emit('process', { type: 'start', data: options });
  606. const content = await (options.isLifeTime ? buildLifeTimeReport(options) : buildYearReport(options));
  607. Event.emit('process', { type: 'done' });
  608. const close = create('div', { class: 'close' });
  609. const scroll = create('div', { class: 'scroll' }, content);
  610. const save = create('div', { class: 'save' });
  611. const report = create('div', { id: 'kotori-report' }, close, scroll, save);
  612.  
  613. const saveFn = () => {
  614. save.onclick = null;
  615. element2Canvas(content, () => save.onclick = saveFn)
  616. };
  617. let ly = scroll.scrollTop || 0;
  618. let my = ly;
  619. let ey = ly;
  620. let interval = null;
  621. const scrollFn = (iey) => {
  622. ey = Math.max(Math.min(iey, scroll.scrollHeight - scroll.offsetHeight), 0);
  623. ly = my;
  624. if (interval) clearInterval(interval);
  625. let times = 1;
  626. interval = setInterval(() => {
  627. if (times > 50) {
  628. clearInterval(interval);
  629. interval = null;
  630. return;
  631. }
  632. my = easeOut(times, ly, ey, 50);
  633. scroll.scroll({ top: my })
  634. times++;
  635. }, 1)
  636. };
  637. const wheelFn = e => {
  638. e.preventDefault();
  639. scrollFn(ey + e.deltaY);
  640. }
  641. const keydownFn = e => {
  642. e.preventDefault();
  643. if (e.key == 'Escape') close.click();
  644. if (e.key == 'Home') scrollFn(0);
  645. if (e.key == 'End') scrollFn(scroll.scrollHeight - scroll.offsetHeight);
  646. if (e.key == 'ArrowUp') scrollFn(ey - 100);
  647. if (e.key == 'ArrowDown') scrollFn(ey + 100);
  648. if (e.key == 'PageUp') scrollFn(ey - scroll.offsetHeight);
  649. if (e.key == 'PageDown') scrollFn(ey + scroll.offsetHeight);
  650. };
  651. scroll.addEventListener('wheel', wheelFn);
  652. close.addEventListener('wheel', wheelFn)
  653. save.addEventListener('wheel', wheelFn)
  654. document.addEventListener('keydown', keydownFn);
  655. save.addEventListener('click', saveFn);
  656. close.addEventListener('click', () => {
  657. document.removeEventListener('keydown', keydownFn);
  658. report.remove()
  659. });
  660. document.body.appendChild(report);
  661. }
  662. // REPORT END
  663.  
  664. // MENU START
  665. /**
  666. * 生成菜单
  667. */
  668. function buildMenu() {
  669. const year = new Date().getFullYear();
  670.  
  671. const buildSubTypeCheck = ([_, { value, name, checked }]) => ['div', { value },
  672. ['input', { type: 'checkbox', id: 'yst_' + value, name, value, checked }],
  673. ['label', { for: 'yst_' + value }, name]
  674. ];
  675. const lifeTimeCheck = create('input', { type: 'checkbox', id: 'lftc' });
  676. const totalTimeCheck = create('input', { type: 'checkbox', id: 'tltc' });
  677. const yearSelect = create('select', ...new Array(year - 2007).fill(0).map((_, i) => ['option', { value: year - i }, year - i]));
  678. const typeSelect = create('select', ...Object.entries(Types).map(([_, { value, name }]) => ['option', { value }, name]));
  679. const tagSelect = create('select', ['option', { value: '' }, '不筛选']);
  680. const btnGo = create('div', { class: ['btn', 'primary'] }, '生成');
  681. const btnClr = create('div', { class: ['btn', 'warning'] }, '清理缓存');
  682. const btnGroup = ['div', { class: 'btn-group' }, btnGo, btnClr];
  683. const additionField = ['fieldset', ['legend', '附加选项'], ['div', lifeTimeCheck, ['label', { for: 'lftc' }, '生涯报告']], ['div', totalTimeCheck, ['label', { for: 'tltc' }, '看过时长(耗时)']]];
  684. const ytField = ['fieldset', ['legend', '选择年份与类型'], yearSelect, typeSelect];
  685. const tagField = ['fieldset', ['legend', '选择过滤标签'], tagSelect];
  686. const subtypeField = create('fieldset', ['legend', '选择包括的状态'], ...Object.entries(SubTypes).map(buildSubTypeCheck))
  687. const eventInfo = create('li')
  688. const menu = create('ul', { id: 'kotori-report-menu' }, ['li', additionField], ['li', ytField], ['li', tagField], ['li', subtypeField], ['li', btnGroup], eventInfo);
  689.  
  690. Event.on('process', (() => {
  691. let type;
  692. let zone = [0, 0];
  693. let subtype;
  694. let subtypes;
  695. let pz = false;
  696. let totalTimeCount = 0;
  697.  
  698. return ({ type: t, data }) => {
  699. switch (t) {
  700. case 'start':
  701. type = data.type;
  702. subtypes = data.subTypes;
  703. eventInfo.innerText = '';
  704. pz = false;
  705. break;
  706. case 'collSubtype':
  707. subtype = data.subtype;
  708. pz = false;
  709. break;
  710. case 'bsycs':
  711. eventInfo.innerText = `二分搜索[${formatSubType(subtype, type)}] (${data.p})`;
  712. break;
  713. case 'collZone':
  714. zone = data.zone;
  715. pz = true;
  716. break;
  717. case 'parse':
  718. if (!pz) return;
  719. eventInfo.innerText = `正在解析[${formatSubType(subtype, type)}] (`
  720. + (data.p - zone[0] + 1) + '/' + (zone[1] - zone[0] + 1) + ')('
  721. + (subtypes.indexOf(subtype) + 1) + '/' + subtypes.length + ')'
  722. break;
  723. case 'done':
  724. eventInfo.innerText = '';
  725. pz = false;
  726. break;
  727. case 'tags':
  728. eventInfo.innerText = `获取标签 [${Types[data.type].name}]`;
  729. break;
  730. case 'totalTime':
  731. totalTimeCount = data.total;
  732. break;
  733. case 'totalTimeItem':
  734. eventInfo.innerText = `获取条目时长 (${data.count}/${totalTimeCount}) (id: ${data.id})`;
  735. break;
  736. default:
  737. return;
  738. }
  739. }
  740. })());
  741.  
  742. lifeTimeCheck.addEventListener('change', () => {
  743. if (lifeTimeCheck.checked) yearSelect.disabled = true;
  744. else yearSelect.disabled = false;
  745. })
  746. typeSelect.addEventListener('change', callNow(async () => {
  747. const type = typeSelect.value;
  748. if (!type) return;
  749. totalTimeCheck.disabled = type !== 'anime';
  750. subtypeField.querySelectorAll('div').forEach(e => {
  751. const name = formatSubType(e.getAttribute('value'), type);
  752. e.querySelector('input').setAttribute('name', name);
  753. e.querySelector('label').innerText = name;
  754. });
  755. const tags = await ft(type);
  756. if (type != typeSelect.value) return;
  757. const last = tagSelect.value;
  758. removeAllChildren(tagSelect);
  759. tagSelect.append(create('option', { value: '' }, '不筛选'));
  760. append(tagSelect, ...tags.map(t => ['option', { value: t }, t]));
  761. if (tags.includes(last)) tagSelect.value = last;
  762. }));
  763. btnGo.addEventListener('click', callWhenDone(async () => {
  764. const type = typeSelect.value || 'anime';
  765. await buildReport({
  766. type,
  767. subTypes: Array.from(subtypeField.querySelectorAll('input:checked')).map(e => e.value),
  768. isLifeTime: lifeTimeCheck.checked,
  769. totalTime: type === 'anime' && totalTimeCheck.checked,
  770. year: parseInt(yearSelect.value) || year,
  771. tag: tagSelect.value,
  772. });
  773. menuToggle();
  774. }));
  775. btnClr.addEventListener('click', callWhenDone(async () => {
  776. let i = 0;
  777. const id = setInterval(() => btnClr.innerText = `清理缓存中[${PRG[i++ % 4]}]`, 50);
  778. await db.clear('pages');
  779. clearInterval(id);
  780. btnClr.innerText = '清理缓存';
  781. }))
  782.  
  783. document.body.appendChild(menu);
  784. return menu;
  785. }
  786.  
  787. let menu = null;
  788. /**
  789. * 切换菜单显隐
  790. */
  791. function menuToggle() {
  792. if (!menu) menu = buildMenu();
  793. menu.style.display = menu.style.display == 'block' ? 'none' : 'block';
  794. }
  795. // MENU END
  796. (async () => {
  797. await db.init();
  798. const btn = create('a', { class: 'chiiBtn', href: 'javascript:void(0)', title: '生成年鉴' }, ['span', '生成年鉴']);
  799. btn.addEventListener('click', menuToggle);
  800. document.querySelector('#headerProfile .actions').append(btn);
  801. })();
  802. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址