// ==UserScript== // @name Bangumi 社区助手 preview // @version 0.0.8 // @namespace b38.dev // @description 社区助手预览版 // @author 神戸小鳥 @vickscarlet // @license MIT // @icon https://bgm.tv/img/favicon.ico // @homepage https://github.com/bangumi/scripts/blob/master/vickscarlet/bangumi_community.user.js // @match *://bgm.tv/* // @match *://chii.in/* // @match *://bangumi.tv/* // @run-at document-start // @downloadURL none // ==/UserScript== (async () => { /**merge:js=_common.dom.script.js**/ async function loadScript(src) { if (!this._loaded) this._loaded = new Set(); if (this._loaded.has(src)) return; return new Promise(resolve => { const script = create('script', { src, type: 'text/javascript' }); script.onload = () => { this._loaded.add(src); resolve(); }; document.body.appendChild(script); }) } /**merge**/ /**merge:js=_common.dom.style.js**/ function addStyle(...styles) { const style = document.createElement('style'); style.append(document.createTextNode(styles.join('\n'))); document.head.appendChild(style); return style; } /**merge**/ /**merge:js=_common.dom.js**/ function setEvents(element, events) { for (const [event, listener] of events) { element.addEventListener(event, listener); } return element; } 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; } function addClass(element, value) { element.classList.add(...[value].flat()); return element; } 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; } function create(name, props, ...childrens) { 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); } 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; } 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) } 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; } function removeAllChildren(element) { while (element.firstChild) element.removeChild(element.firstChild); return element; } 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]); } async function newTab(href) { create('a', { href, target: '_blank' }).click(); } /**merge**/ /**merge:js=_common.util.js**/ function callWhenDone(fn) { let done = true; return async () => { if (!done) return; done = false; await fn(); done = true; } } function callNow(fn) { fn(); return fn; } 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 } /**merge**/ /**merge:js=_common.database.js**/ class Collection { constructor(master, { collection, options, indexes }) { this.#master = master; this.#collection = collection; this.#options = options; this.#indexes = indexes; } #master; #collection; #options; #indexes; 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) } async get(key, index = '') { return this.transaction(store => (index ? store.index(index) : store).get(key)); } async put(data) { return this.transaction(store => store.put(data), 'readwrite').then(_ => true); } async clear() { return this.transaction(store => store.clear(), 'readwrite').then(_ => true); } } class Database { constructor({ dbName, version, collections }) { this.#dbName = dbName; this.#version = version; for (const options of collections) { this.#collections.set(options.collection, new Collection(this, options)); } } #dbName; #version; #collections = new Map(); #db; 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', () => reject({ type: 'blocked' })); 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') { 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 put(collection, data) { return this.#collections.get(collection).put(data); } async clear(collection) { return this.#collections.get(collection).clear(); } async clearAll() { for (const c of this.#collections.values()) await c.clear(); return true; } } /**merge**/ /**merge:js=_common.event.js**/ 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); } } /**merge**/ /**merge:js=_common.bangumi.js**/ function whoami() { const nid = window.parent.CHOBITS_UID ?? 0; const dockA = window.parent.document.querySelector('#dock li.first a'); if (dockA) { const id = dockA.href.split('/').pop(); return { id, nid }; } const bannerAvatar = window.parent.document.querySelector('.idBadgerNeue> .avatar'); if (bannerAvatar) { const id = bannerAvatar.href.split('/').pop(); return { id, nid }; } return null; } /**merge**/ addStyle( /**merge:css=bangumi_community.user.keyframes.css**/`@keyframes loading-spine {to{transform: rotate(.5turn)}}`/**merge**/, /**merge:css=bangumi_community.user.colors.light.css**/`html {--color-base: #ffffff;--color-base-2: #e8e8e8;--color-base-bg: #eaeffba0;--color-base-font: #282828;--color-gray-1: #e8e8e8;--color-gray-2: #cccccc;--color-gray-3: #aaaaaa;--color-gray-4: #969696;--color-gray-11: #cccccc;--color-bangumi-2: #AB515D;--color-bangumi-font: rgb(from var(--color-bangumi) calc(r - 50) calc(g - 50) calc(b - 50));--color-yellow-font: rgb(from var(--color-yellow) calc(r - 50) calc(g - 50) calc(b - 50));--color-purple-font: rgb(from var(--color-purple) calc(r - 50) calc(g - 50) calc(b - 50));--color-blue-font: rgb(from var(--color-blue) calc(r - 50) calc(g - 50) calc(b - 50));--color-green-font: rgb(from var(--color-green) calc(r - 50) calc(g - 50) calc(b - 50));--color-red-font: rgb(from var(--color-red) calc(r - 50) calc(g - 50) calc(b - 50));}`/**merge**/, /**merge:css=bangumi_community.user.colors.dark.css**/`html[data-theme='dark'] {--color-base: #000000;--color-base-2: #1f1f1f;--color-base-bg: #23262ba0;--color-base-font: #e8e8e8;--color-gray-1: #444444;--color-gray-2: #555555;--color-gray-3: #6a6a6a;--color-gray-4: #888888;--color-gray-11: #cccccc;--color-bangumi-2: #ffb6bd;--color-bangumi-font: rgb(from var(--color-bangumi) calc(r + 50) calc(g + 50) calc(b + 50));--color-yellow-font: rgb(from var(--color-yellow) calc(r + 50) calc(g + 50) calc(b + 50));--color-purple-font: rgb(from var(--color-purple) calc(r + 50) calc(g + 50) calc(b + 50));--color-blue-font: rgb(from var(--color-blue) calc(r + 50) calc(g + 50) calc(b + 50));--color-green-font: rgb(from var(--color-green) calc(r + 50) calc(g + 50) calc(b + 50));--color-red-font: rgb(from var(--color-red) calc(r + 50) calc(g + 50) calc(b + 50));}`/**merge**/, /**merge:css=bangumi_community.user.colors.css**/`html {--color-bangumi: #fd8a96;--color-white: #ffffff;--color-black: #000000;--color-yellow: #f9c74c;--color-purple: #a54cf9;--color-blue: #02a3fb;--color-green: #95eb89;--color-red: #f94144;--color-dock-sp: var(--color-gray-2);--color-switch-border: var(--color-gray-2);--color-switch-on: var(--color-green);--color-switch-off: var(--color-gray-4);--color-switch-bar-border: var(--color-white);--color-switch-bar-inner: var(--color-gray-11);--color-hover: var(--color-blue);--color-icon-btn-bg: rgb(from var(--color-bangumi) r g b / .25);--color-icon-btn-color: var(--color-white);--color-reply-sp: var(--color-gray-1);--color-reply-tips: var(--color-gray-3);--color-reply-normal: var(--color-bangumi);--color-reply-owner: var(--color-yellow);--color-reply-floor: var(--color-purple);--color-reply-friend: var(--color-green);--color-reply-self: var(--color-blue);--color-sicky-bg: rgb(from var(--color-base) r g b / .125);--color-sicky-border: rgb(from var(--color-bangumi) r g b / .25);--color-sicky-shadow: rgb(from var(--color-base) r g b / .05);--color-sicky-textarea: rgb(from var(--color-base) r g b / .8);--color-sicky-hover-bg: rgb(from var(--color-bangumi) r g b / .125);--color-sicky-hover-border: var(--color-bangumi);--color-sicky-hover-shadow: var(--color-bangumi);--color-primary: var(--color-bangumi);--color-secondary: var(--color-blue);--color-success: var(--color-green);--color-info: var(--color-blue);--color-important: var(--color-purple);--color-warning: var(--color-yellow);--color-danger: var(--color-red);}`/**merge**/, /**merge:css=bangumi_community.user.1.css**/`html, html[data-theme='dark'] {#dock {li {position: relative;height: 18px;display: flex;align-items: center;justify-content: center;}li:not(:last-child) {border-right: 1px solid var(--color-dock-sp);}}.columns {> #columnInSubjectB {> * { margin: 0; }display: flex;gap: 10px;flex-direction: column;position: sticky;top: 0;align-self: flex-start;max-height: 100vh;overflow-y: auto;}}*:has(>#comment_list) {.postTopic {border-bottom: none;.inner.tips {display: flex;height: 40px;align-items: center;gap: 8px;color: var(--color-reply-tips);}}.avatar:not(.tinyCover) {img,.avatarNeue {border-radius: 50% !important;}}.clearit:not(.message) {transition: all 0.3s ease;box-sizing: border-box;border-bottom: none !important;border-top: 1px dashed var(--color-reply-sp);.inner.tips {display: flex;height: 40px;align-items: center;gap: 8px;color: var(--color-reply-tips);}.sub_reply_collapse .inner.tips { height: auto; }--color-reply: var(--color-bangumi);}.clearit.friend { --color-reply: var(--color-green); }.clearit.owner { --color-reply: var(--color-yellow); }.clearit.floor { --color-reply: var(--color-purple); }.clearit.self { --color-reply: var(--color-blue); }.clearit.friend, .clearit.owner, .clearit.floor, .clearit.self {border-top: 1px solid var(--color-reply) !important;background: linear-gradient(rgb(from var(--color-reply) r g b / .125) 1px, #00000000 60px) !important;> .inner > :first-child > strong::before, > .inner > strong::before {padding: 1px 4px;margin-right: 4px;border-radius: 2px;background: rgb(from var(--color-bangumi) r g b /.5);}}.clearit:not(:has(.clearit:not(.message):hover), .message):hover {border-top: 1px solid var(--color-reply) !important;background: linear-gradient(rgb(from var(--color-reply) r g b / .125) 1px, #00000000 60px) !important;box-shadow: 0 0 4px rgb(from var(--color-reply) r g b / .5);}.clearit.self { > .inner > :first-child > strong::before, > .inner > strong::before { content: '自'; } }.clearit.friend { > .inner > :first-child > strong::before, > .inner > strong::before { content: '友'; } }.clearit.owner { > .inner > :first-child > strong::before, > .inner > strong::before { content: '楼'; } }.clearit.floor { > .inner > :first-child > strong::before, > .inner > strong::before { content: '层'; } }.clearit.friend.owner { > .inner > :first-child > strong::before, > .inner > strong::before { content: '友 楼'; } }.clearit.friend.floor { > .inner > :first-child > strong::before, > .inner > strong::before { content: '友 层'; } }.clearit.owner.floor { > .inner > :first-child > strong::before, > .inner > strong::before { content: '楼 层'; } }.clearit.self.owner { > .inner > :first-child > strong::before, > .inner > strong::before { content: '自 楼'; } }.clearit.self.floor { > .inner > :first-child > strong::before, > .inner > strong::before { content: '自 层'; } }.clearit.friend.owner.floor { > .inner > :first-child > strong::before, > .inner > strong::before { content: '友 楼 层'; } }.clearit.self.owner.floor { > .inner > :first-child > strong::before, > .inner > strong::before { content: '自 楼 层'; } }}#comment_list {box-sizing: border-box;.row:nth-child(odd), .row:nth-child(even) { background: transparent; }> .clearit:first-child { border-top: 1px solid transparent; }div.reply_collapse { padding: 5px 10px; }}@media (max-width: 640px) {.columns { > .column:last-child { align-self: auto !important; } }}}`/**merge**/, /**merge:css=bangumi_community.user.2.css**/`html, html[data-theme='dark'] {.svg-icon {display: flex !important;align-items: center !important;justify-content: center !important;cursor: pointer;span {visibility: hidden;position: absolute;top: 0;left: 50%;transform: translate(-50%, calc(-100% - 10px));padding: 2px 5px;border-radius: 5px;background: rgb(from var(--color-black) r g b / 0.6);white-space: nowrap;color: var(--color-white);}span::after {content: '';position: absolute !important;bottom: 0;left: 50%;border-top: 5px solid rgb(from var(--color-black) r g b / 0.6);border-right: 5px solid transparent;border-left: 5px solid transparent;backdrop-filter: blur(5px);transform: translate(-50%, 100%);}}.svg-icon:hover {span {visibility: visible;}}.switch {display: inline-block;position: relative;cursor: pointer;border-radius: 50px;height: 12px;width: 40px;border: 1px solid var(--color-switch-border);}.switch::before {content: '';display: block;position: absolute;pointer-events: none;height: 12px;width: 40px;top: 0px;border-radius: 24px;background-color: var(--color-switch-off);}.switch::after {content: '';display: block;position: absolute;pointer-events: none;top: 0;left: 0;height: 12px;width: 24px;border-radius: 24px;box-sizing: border-box;background-color: var(--color-switch-bar-inner);border: 5px solid var(--color-switch-bar-border);}.switch[switch="1"]::before {background-color: var(--color-switch-on);}.switch[switch="1"]::after {left: 16px;}.topic-box {#comment_list {.icon {color: var(--color-gray-11);}}.block {display: none;}.sicky-reply {background-color: var(--color-sicky-bg);border: 1px solid var(--color-sicky-border);box-shadow: 0px 0px 0px 2px var(--color-sicky-shadow);textarea {background-color: var(--color-sicky-textarea);}}.sicky-reply:has(:focus),.sicky-reply:hover {grid-template-rows: 1fr;background-color: var(--color-sicky-hover-bg);border: 1px solid var(--color-sicky-hover-border);box-shadow: 0 0 4px var(--color-sicky-hover-shadow);}#reply_wrapper {position: relative;padding: 5px;min-height: 50px;margin: 0;textarea.reply {width: 100% !important;}.switch {position: absolute;right: 10px;top: 10px;}.tip.rr + .switch {top: 35px;}}.sicky-reply {position: sticky;top: 0;z-index: 2;display: grid;height: auto;grid-template-rows: 0fr;border-radius: 4px;backdrop-filter: blur(5px);transition: all 0.3s ease;width: calc(100% - 1px);overflow: hidden;#slider {position: absolute;right: 5px;top: 13px;max-width: 100%;}}.svg-box {display: flex;justify-content: center;align-items: center;}}.vcomm {ul {white-space: nowrap;justify-content: center;align-items: center;}a {display: flex;align-items: center;gap: 0.5em;}}}`/**merge**/, /**merge:css=bangumi_community.user.3.css**/`html, html[data-theme='dark'] {.vc-serif {font-family: source-han-serif-sc, source-han-serif-japanese, 宋体, 新宋体;font-weight: 900;}#community-helper-user-panel {position: fixed !important;z-index: 9999;display: grid;place-items: center;top: 0;right: 0;bottom: 0;left: 0;> .close-mask {position: absolute;z-index: -1;display: grid;place-items: center;top: 0;right: 0;bottom: 0;left: 0;background: rgb(from var(--color-base) r g b / 0.5);cursor: pointer;backdrop-filter: blur(5px);}> .container {max-width: 1280px;max-height: 580px;width: calc(100% - 60px);height: calc(100vh - 60px);> fieldset {padding-top: 24px;legend {position: absolute;font-weight: bold;top: 5px;left: 5px;padding: 0;line-height: 12px;font-size: 12px;display: flex;align-items: center;gap: 0.5em;color: var(--color-bangumi-font);> svg {width: 14px;height: 14px;}}}> .tags-field {> ul {display: flex;flex-wrap: wrap;gap: 4px;overflow: auto;li {padding: 0 5px;border-radius: 50px;background: rgb(from var(--color-font) r g b / .25);border: 1px solid var(--color-font);box-sizing: border-box;white-space: pre;}}}display: grid;grid-template-columns: auto auto 1fr 2fr;grid-template-rows: 180px auto 3fr;gap: 5px 5px;grid-template-areas:"avatar note note bio""addition note note bio""usedname usedname tags bio";> * {--color-font: var(--color-bangumi-font);--color-from: var(--color-base-2);--color-to: var(--color-base-2);--color-alpha: 0.05;color: var(--color-font);padding: 10px;position: relative;border-radius: 4px;> .action {color: var(--color-base-font);position: absolute;top: 5px;right: 5px;cursor: pointer;}}> *::after,> *::before {content: '';position: absolute;border-radius: 4px;top: 0;left: 0;right: 0;bottom: 0;background-size: cover;z-index: -1;}> *::before { opacity: 0.2; }> .failed,> .loading::before,> *::after {opacity: 1;background: linear-gradient(150deg, rgb(from var(--color-from) r g b / var(--color-alpha)), rgb(from var(--color-to) r g b / var(--color-alpha)) 75%);box-shadow: 0 0 1px rgb(from var(--color-bangumi-2) r g b / .5);backdrop-filter: blur(10px);}> .loading::after,> .failed::before,> .failed::after {background: none !important;box-shadow: none !important;backdrop-filter: none !important;}> .loading::after {width: 50px;height: 50px;top: calc(50% - 25px);left: calc(50% - 25px);aspect-ratio: 1;border-radius: 50%;border: 8px solid;box-sizing: border-box;border-color: var(--color-bangumi) transparent;animation: loading-spine 1s infinite;}> .failed::before,> .failed::after {width: 50px;height: 8px;top: calc(50% - 25px);left: calc(50% - 4px);}> .failed::after { transform: rotate(45deg); }> .failed::before { transform: rotate(-45deg); }> .avatar {grid-area: avatar;--color-font: var(--color-bangumi);--color-from: var(--color-bangumi);--color-alpha: .25;min-width: 120px;max-width: 280px;display: flex;flex-direction: column;justify-content: center;align-items: center;gap: 5px;img {width: 100px;height: 100px;border-radius: 100px;object-fit: cover;}span {position: absolute;top: 5px;right: 0;transform: translate(100%) rotate(90deg);transform-origin: 0% 0%;}span::before {content: '@';}svg {width: 100%;height: 100%;text {transform: translate(50%, 0.18em);text-anchor: middle;dominant-baseline: hanging;}}}> .addition {grid-area: addition;--color-from: var(--color-yellow);--color-font: var(--color-yellow-font);display: grid;padding: 0;grid-template-columns: repeat(4, 1fr);grid-template-areas: "one two three four";> * {position: relative;display: grid;place-items: center;width: 100%;padding: 10px 0;}> .home { grid-area: one; }> .pm { grid-area: two; }> .friend { grid-area: three; }> .block { grid-area: four; }> *:not(.block)::after {position: absolute;content: '';width: 1px;height: calc(100% - 10px);top: 5px;right: -0.5px;background: rgb(from var(--color-from) r g b / var(--color-alpha));}}> .tags {min-width: 200px;grid-area: tags;--color-from: var(--color-blue);--color-font: var(--color-blue-font);}> .note {grid-area: note;min-width: 200px;--color-from: var(--color-green);--color-font: var(--color-green-font);}> .usedname {grid-area: usedname;--color-from: var(--color-purple);--color-font: var(--color-purple-font);max-width: 400px;min-width: 200px;> ul {max-height: 336px;}}> .bio {grid-area: bio;--color-from: var(--color-bangumi);--color-font: var(--color-base-font);max-width: 505px;min-width: 300px;max-height: 560px;height: calc(100vh - 80px);> div {height: 100%;overflow: auto;}}}}}`/**merge**/, ) const db = new Database({ dbName: 'VCommunity', version: 4, collections: [ { collection: 'values', options: { keyPath: 'id' }, indexes: [{ name: 'id', keyPath: 'id', unique: true }] }, { collection: 'friends', options: { keyPath: 'id' }, indexes: [{ name: 'id', keyPath: 'id', unique: true }] }, { collection: 'users', options: { keyPath: 'id' }, indexes: [{ name: 'id', keyPath: 'id', unique: true }] }, { collection: 'images', options: { keyPath: 'uri' }, indexes: [{ name: 'uri', keyPath: 'uri', unique: true }] } ] }); const menu = new class { constructor() { } #menu = create('ul', ['li', { onClick: () => this.#block() }, ['a', { href: 'javascript:void(0)' }, svg('block'), '屏蔽发言']], ['li', { onClick: () => this.#show() }, ['a', { href: 'javascript:void(0)' }, svg('detail'), '详细信息']] ); #panel = create('div', { id: 'community-helper-user-panel' }, ['div', { class: 'close-mask', onClick: () => this.#close() }], ); #style = addStyle() #bfbgi(src) { const randomClass = 'v-rand-' + Math.floor(Math.random() * 100000 + 100000).toString(16); this.#style.innerText = `.${randomClass}::before {background-image: url("${src}");}` return randomClass } #id; id(id) { this.#id = id; return this.#menu; } async #block() { const id = this.#id; if (!confirm('确定要屏蔽吗?')) return false; const data = await db.get('users', id) || { id }; data.block = true; await db.put('users', data); return true } async #unblock() { const id = this.#id; if (!confirm('确定要解除屏蔽吗?')) return false; const data = await db.get('users', id) || { id }; data.block = false; await db.put('users', data); return true } async #connect(nid, gh) { if (!confirm('真的要加好友吗?')) return false; const ret = await fetch(`/connect/${nid}?gh=${gh}`); return ret.ok } async #disconnect(nid, gh) { if (!confirm('真的要解除好友吗?')) return false; const ret = await fetch(`/disconnect/${nid}?gh=${gh}`); return ret.ok } async #loadUserData(id) { const data = await db.get('users', id) || { id, names: new Set() }; const { names, namesUpdate, namesTml } = data; if (namesUpdate < Date.now() - 3600_000) return data; const getUsedNames = async (end, tml, ret = [], page = 1) => { const res = await fetch(`/user/${id}/timeline?type=say&ajax=1&page=${page}`); const html = await res.text(); const names = Array.from(html.matchAll(/从 \(?.*?)\<\/strong\> 改名为/g), m => m.groups.from); const tmls = Array.from(html.matchAll(/\

(?\d{4}\-\d{1,2}\-\d{1,2})\<\/h4\>/g), m => m.groups.tml); if (!tml) tml = tmls[0]; ret.push(...names); if (tmls.includes(end) || !html.includes('>下一页 ››')) return { ret, tml }; return getUsedNames(end, tml, ret, page + 1); }; const { ret, tml } = await getUsedNames(namesTml); data.names = new Set(ret).union(names); data.names.delete(''); if (namesTml && names.size == data.names.size) return data; data.namesUpdate = Date.now(); data.namesTml = tml; await db.put('users', data); return data } async #loadHomepage(id) { const res = await fetch('/user/' + id); if (!res.ok) return null; const html = await res.text(); const element = document.createElement('html'); element.innerHTML = html.replace(/<(img|script|link)/g, ' newTab('/user/' + id) }, svg('home'), ['span', '主页']); append(addition, homeBtn); append(document.body, [this.#panel, ['div', { class: 'container' }, avatar, tags, note, usedname, bio, addition]]); const nicescroll = { cursorcolor: "rgb(from var(--color-bangumi) r g b / .5)", cursorwidth: "8px", cursorborder: "none" }; (async () => { // 加载 头像 用户名 简介 await loadScript('https://cdn.jsdelivr.net/npm/jquery.nicescroll@3.7/jquery.nicescroll.min.js'); const homepage = await this.#loadHomepage(id); if (!this.#isShow || id != this.#id) return; avatar.classList.remove('loading'); bio.classList.remove('loading'); if (!homepage) { avatar.classList.add('failed'); bio.classList.add('failed'); } const { name, src, friend, nid, gh } = homepage; bio.classList.add(this.#bfbgi(src)) append(avatar, ['img', { src }], createTextSVG(name, 'vc-serif'), ['span', id]); append(bio, homepage.bio); const pmBtn = create('div', { class: ['pm', 'svg-icon'], onClick: () => newTab('/pm/compose/' + nid + '.chii') }, svg('message'), ['span', '私信']); const connectBtn = create('div', { class: ['friend', 'svg-icon'] }, svg('connect'), ['span', '加好友']); const disconnectBtn = create('div', { class: ['friend', 'svg-icon'] }, svg('disconnect'), ['span', '解除好友']); connectBtn.addEventListener('click', async () => { if (await this.#connect(nid, gh)) connectBtn.replaceWith(disconnectBtn); }); disconnectBtn.addEventListener('click', async () => { if (await this.#disconnect(nid, gh)) disconnectBtn.replaceWith(connectBtn); }); append(addition, pmBtn, homeBtn, friend ? disconnectBtn : connectBtn); if (homepage.bio) $(homepage.bio).niceScroll(nicescroll); })(); (async () => { // 加载 曾用名 const user = await this.#loadUserData(id); if (!this.#isShow || id != this.#id) return; await loadScript('https://cdn.jsdelivr.net/npm/jquery.nicescroll@3.7/jquery.nicescroll.min.js'); usedname.classList.remove('loading'); tags.classList.remove('loading'); note.classList.remove('loading'); const editTags = create('div', { class: ['svg-icon', 'action'] }, svg('edit'), ['span', '编辑']); const editNote = create('div', { class: ['svg-icon', 'action'] }, svg('edit'), ['span', '编辑']); const usednameUl = this.#ul(user.names); const tagsUl = this.#ul(user.tags); append(usedname, usednameUl); append(tags, tagsUl, editTags); append(note, user.note ?? '', editNote); const blockedBtn = create('div', { class: ['block', 'svg-icon'] }, svg('block'), ['span', '解除屏蔽']); const unblockBtn = create('div', { class: ['block', 'svg-icon'] }, svg('notify'), ['span', '屏蔽']); blockedBtn.addEventListener('click', async () => { if (await this.#unblock()) blockedBtn.replaceWith(unblockBtn); }); unblockBtn.addEventListener('click', async () => { if (await this.#block()) unblockBtn.replaceWith(blockedBtn); }); append(addition, user.block ? blockedBtn : unblockBtn); $(usednameUl).niceScroll(nicescroll); $(tagsUl).niceScroll(nicescroll); })(); } #ul(list) { return create('ul', ...map(list ?? [], v => ['li', v])); } } function dockInject() { const dock = document.querySelector('#dock'); if (!dock) return; let n, o; o = dock.querySelector('#showrobot'); o.style.display = 'none'; n = create('a', { class: ['showrobot', 'svg-icon'], href: 'javascript:void(0)' }, svg('robot'), ['span', '春菜']); n.addEventListener('click', () => chiiLib.ukagaka.toggleDisplay()); o.parentElement.append(n); o = dock.querySelector('#toggleTheme'); o.style.display = 'none'; n = create('a', { class: ['toggleTheme', 'svg-icon'], href: 'javascript:void(0)' }, svg('light'), ['span', '开关灯']); n.addEventListener('click', () => chiiLib.ukagaka.toggleTheme()); o.parentElement.append(n); o.parentElement.classList.remove('last'); o = null; dock.querySelectorAll('li').forEach(e => { if (!o || o.children.length < e.children.length) o = e; }); o.querySelectorAll('a').forEach(a => { let icon; switch (a.innerText) { case '提醒': icon = 'notify'; break; case '短信': icon = 'message'; break; case '设置': icon = 'setting'; break; case '登出': icon = 'logout'; break; } if (icon) { const title = a.innerText removeAllChildren(a); a.classList.add('svg-icon'); append(a, svg(icon), ['span', title]); } o.parentElement.insertBefore(create('li', a), o); }) o.remove(); } async function parseHasCommentList() { const commentList = document.querySelector('#comment_list') if (!commentList) return; const e = commentList.parentElement; if (!e) return; e.classList.add('topic-box'); const first = e.querySelector(':scope>.clearit') const replyWrapper = e.querySelector('#reply_wrapper'); if (replyWrapper) { e.querySelector('#sliderContainer')?.style.setProperty('display', 'none', 'important'); const getSwitch = () => { const raw = localStorage.getItem('sickyReplySwitch') if (!raw) return 1; return Number(raw) || 0; } const swBtn = create('div', { class: 'switch', switch: Number(localStorage.getItem('sickyReplySwitch')) || 1 }); swBtn.addEventListener('click', callNow(sw => { const s = sw ? sw() : getSwitch(); swBtn.setAttribute('switch', s); const sicky = (() => { const q = e.querySelector('.sicky-reply') if (q) return q; const c = create('div', { class: 'sicky-reply' }); e.insertBefore(c, first || commentList); return c; })(); if (s) { sicky.style.visibility = 'visible'; sicky.append(replyWrapper); } else { sicky.style.visibility = 'hidden'; e.append(replyWrapper); } }).bind(this, () => { const s = (getSwitch() + 1) % 2; localStorage.setItem('sickyReplySwitch', s) return s; })); append(replyWrapper, swBtn); } const handlerClearit = async clearit => { const id = clearit.getAttribute('data-item-user') if (!id) return; const data = await db.get('users', id) || { id, names: new Set() }; const inner = clearit.querySelector('.inner'); const icon = create('a', { class: ['icon', 'svg-icon'], href: 'javascript:void(0)' }, svg('tag')); const action = create('div', { class: ['action', 'dropdown', 'vcomm'] }, icon); icon.addEventListener('mouseenter', () => append(action, menu.id(id))); const actionBox = clearit.querySelector('.post_actions'); actionBox.insertBefore(action, actionBox.lastElementChild); if (!data.names) data.names = new Set(); const currentName = inner.querySelector('strong > a').innerText; if (currentName && !data.names.has(currentName)) { data.names.add(currentName); await db.put('users', data); } if (data.block) { const btn = create('div', { class: 'svg-box' }, svg('expand')) const tip = create('span', { class: 'svg-box' }, svg('collapse'), '已折叠') const tips = create('div', { class: ['inner', 'tips'] }, tip, btn); btn.addEventListener('click', () => tips.replaceWith(inner)); inner.replaceWith(tips); } } if (first) handlerClearit(first); const owner = e.querySelector('.postTopic')?.getAttribute('data-item-user'); const self = whoami()?.id; const friends = await getFriends(); if (friends.has(owner)) first.classList.add('friend'); if (owner == self) first.classList.add('self'); for (const comment of Array.from(commentList.children)) { const floor = comment.getAttribute('data-item-user') if (friends.has(floor)) comment.classList.add('friend'); if (floor === owner) comment.classList.add('owner'); if (floor === self) comment.classList.add('self'); handlerClearit(comment) comment.querySelectorAll('.clearit').forEach(clearit => { const user = clearit.getAttribute('data-item-user'); if (friends.has(user)) clearit.classList.add('friend'); if (user === owner) clearit.classList.add('owner'); if (user === floor) clearit.classList.add('floor'); if (user === self) clearit.classList.add('self'); handlerClearit(clearit); }); } } async function getFriends() { const user = whoami(); if (!user) return new Set(); const id = user.id; const cache = await db.get('friends', id); if (cache && cache.timestamp > Date.now() - 3600_000) return cache.friends; const res = await fetch(`/user/${id}/friends`); if (!res.ok) console.warn(`Error fetching friends: ${res.status}`); const html = await res.text(); const element = document.createElement('html') element.innerHTML = html.replace(/<(img|script|link)/g, ' { if (document.readyState !== 'complete') return; await db.init().catch((reason) => { if (!reason) throw new Error('db init failed'); switch (reason.type) { case 'error': throw reason.message; case 'blocked': { alert('Bangumi 社区助手 preview 数据库有更新,请先关闭所有班固米标签页再刷新试试'); throw new Error('db init blocked'); } default: throw reason; } }); dockInject(); parseHasCommentList(); })) })();