// ==UserScript== // @name Bangumi 社区助手 preview // @version 0.0.7 // @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.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 = 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; } /**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', event => reject(event.target.error)); request.addEventListener('success', event => resolve(event.target.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.colors.light.css**/`html {--color-base: #ffffff;--color-base-2: #e8e8e8;--color-base-bg: #eaeffba0;--color-gray-1: #e8e8e8;--color-gray-2: #cccccc;--color-gray-3: #aaaaaa;--color-gray-4: #969696;--color-gray-11: #cccccc;--color-bangumi-2: #AB515D;}`/**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-gray-1: #444444;--color-gray-2: #555555;--color-gray-3: #6a6a6a;--color-gray-4: #888888;--color-gray-11: #cccccc;--color-bangumi-2: #ffb6bd;}`/**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'] {.vc-btn {cursor: pointer;padding: 0.5em 1em;font-size: 18px;font-weight: 900;text-shadow: 0 0 4px rgb(from var(--color-black) r g b /.8);background: var(--color-primary);box-shadow: 0 0 4px rgb(from var(--color-black) r g b / .375);color: var(--color-white);border-radius: 4px;}.vc-btn.primary { background: var(--color-primary); }.vc-btn.secondary { background: var(--color-secondary); }.vc-btn.success { background: var(--color-success); }.vc-btn.info { background: var(--color-info); }.vc-btn.important { background: var(--color-important); }.vc-btn.warning { background: var(--color-warning); }.vc-btn.danger { background: var(--color-danger); }.svg-icon {display: flex !important;align-items: center !important;justify-content: center !important;span {visibility: hidden;position: absolute;top: 0;left: 50%;transform: translate(-50%, calc(-100% - 10px));padding: 2px 5px;border-radius: 5px;background: hexa(0, 0, 0, 0.6);white-space: nowrap;color: #fff;}span::after {content: '';position: absolute !important;bottom: 0;left: 50%;border-top: 5px solid hexa(0, 0, 0, 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;}}fieldset.tags {padding-left: 10px;legend {font-weight: bold;margin-left: -10px;display: flex;align-items: center;gap: 0.5em;}legend::after {content: ':';margin-left: -0.5em;}ul {display: flex;flex-wrap: wrap;gap: 4px;li {padding: 0 5px;border-radius: 50px;background: rgb(from var(--color-bangumi) r g b / .25);border: 1px solid var(--color-bangumi);box-sizing: border-box;}}}#community-helper {border-radius: 5px;display: flex;flex-direction: column;> .title {background: var(--color-bangumi);padding: 8px;color: rgb(from var(--color-base) r g b / .8);border-radius: 4px 4px 0 0;}> .user-info {padding: 10px;color: var(--color-bangumi-2);}}#community-helper:has(.user-info:empty) {visibility: hidden;}#robot_balloon {padding: 10px;.speech {ul {display: flex;flex-wrap: wrap;}}> .inner {padding: 0;max-height: 318px;background: none;overflow-y: scroll;scrollbar-width: none;::-webkit-scrollbar {display: none;}}#community-helper {padding: 0;box-shadow: none;> .title {display: none;}> .user-info {padding: 0;color: unset;}}}#robot_balloon::before {content: '';position: absolute;top: 0;left: 0;right: 0;bottom: 10px;background: url(/img/ukagaka/balloon_pink.png) no-repeat top left;background-size: 100% auto;z-index: -1;}.ukagaka_balloon_pink_bottom {position: absolute;height: 10px;left: 0;right: 0;bottom: 0;width: 100% !important;background-size: 100% auto;z-index: -1;}#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: 1024px;max-height: 580px;width: calc(100% - 60px);height: calc(100vh - 60px);display: grid;grid-template-columns: auto auto 1fr 2fr;grid-template-rows: auto auto 3fr;gap: 5px 5px;grid-template-areas:"avatar note note bio""addition note note bio""usedname usedname tags bio";> * {padding: 10px;position: relative;border-radius: 4px;}> *::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;}> *::after {background: rgb(from var(--color-base-2) r g b / .25);/* border: 1px solid rgb(from var(--color-bangumi) r g b /.5); */box-shadow: 0 0 1px rgb(from var(--color-bangumi-2) r g b / .5);backdrop-filter: blur(10px);}> .avatar::after {background: linear-gradient(150deg, rgb(from var(--color-bangumi) r g b / .25), rgb(from var(--color-base-2) r g b / .25) 75%);}> .avatar {max-height: 160px;min-width: 120px;max-width: 200px;grid-area: avatar;display: flex;flex-direction: column;justify-content: center;align-items: center;gap: 5px;color: var(--color-bangumi);img {width: 100px;height: 100px;border-radius: 100px;object-fit: cover;}svg {width: 100%;height: 50px;text {transform: translate(50%, 0.1em);text-anchor: middle;dominant-baseline: hanging;}}}> .addition {grid-area: addition;}> .tags {grid-area: tags;}> .note {grid-area: note;}> .usedname {grid-area: usedname;max-width: 400px;}> .bio {grid-area: bio;max-width: 480px;min-width: 300px;max-height: 560px;height: calc(100vh - 80px);> div {height: 100%;overflow: auto;}}}}#community-helper-user-panel.loading {> .container {display: none;}> .close-mask::before {content: '';display: block;width: 100px;height: 100px;aspect-ratio: 1;border-radius: 50%;border: 8px solid;box-sizing: border-box;border-color: var(--color-bangumi) transparent;animation: loading-spine 1s infinite;}}@media (max-width: 640px) {#robot_balloon > .inner {max-height: 125px;}}}@keyframes loading-spine {to{transform: rotate(.5turn)}}`/**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() }], // ['div', { class: 'container' }, // ['li', { onClick: () => this.#usedname() }, ['a', { href: 'javascript:void(0)' }, svg('clock'), '曾用名']], // ], ); #id; #isShow = false; #style; id(id) { this.#id = id; return this.#menu; } async #block() { const id = this.#id; if (!confirm('确定要屏蔽吗?')) return; const data = await db.get('users', id) || { id }; data.block = true; await db.put('users', data); } async #usedname() { const id = this.#id const names = await this.#getUsedNames(id); const data = await db.get('users', id) || { id }; data.names = data.names.union(new Set(names)); await db.put('users', data); } async #getUsedNames(id, 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); ret.push(...names); if (!html.includes('>下一页 ››')) return ret; return this.#getUsedNames(id, ret, page + 1); } #close() { this.#isShow = false; this.#panel.remove(); this.#panel.lastElementChild.remove(); } async #show() { this.#isShow = true; const id = this.#id; this.#panel.classList.add('loading'); append(document.body, this.#panel); const data = await db.get('users', id); if (!this.#isShow || id != this.#id) return; const friends = await getFriends(); if (!this.#isShow || id != this.#id) return; const res = await fetch('/user/' + id); if (!this.#isShow || id != this.#id) return; if (!res.ok) { append(this.#panel, ['div', { class: ['vc-btn', 'warning'], onClick: () => { this.#panel.lastElementChild.remove(); this.#show() } }, '重新加载']); this.#panel.classList.remove('loading'); return; } const html = await res.text(); if (!this.#isShow || id != this.#id) return; const element = document.createElement('html'); element.innerHTML = html.replace(/<(img|script|link)/g, ' ['li', name])] ] ); const tags = create('div', { class: 'tags' }); const note = create('div', { class: 'note' }); const addition = create('div', { class: 'addition' }); append(this.#panel, ['div', { class: 'container' }, avatar, tags, note, usedname, bio, addition]); this.#panel.classList.remove('loading'); } } function hoverUserListener() { const helper = create('div', { id: 'community-helper', class: 'borderNeue' }); const title = create('div', { class: 'title' }, 'Bangumi 社区助手'); const container = create('div', { class: 'user-info' }) append(helper, title, container); let last; const showUser = async (id, currentName) => { if (last === id) return; last = id; const data = await db.get('users', id); if (!data || last !== id) return; removeAllChildren(container); append(container, ['fieldset', { class: 'tags' }, ['legend', '用户名'], ['ul', ['li', currentName]]]); data.names.delete(currentName); if (data.names.size) { const used = ['ul', ...map(data.names, name => ['li', name])] append(container, ['fieldset', { class: 'tags' }, ['legend', '曾用名'], used]); } } let timeout; Event.on('hover', async ({ id, currentName }) => { clearTimeout(timeout); timeout = setTimeout(() => showUser(id, currentName), 50); }); Event.on('leave', () => { clearTimeout(timeout) }); window.addEventListener('resize', callNow(() => { const r = document.querySelector('#robot_balloon > .inner') const c = document.querySelector('.columns > .column:not(#columnSubjectHomeB,#columnHomeB):last-child') let inner; if (window.innerWidth < 640) inner = r; else inner = c || r; inner.append(helper); })) } 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('mark')); 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 (!data.names.has(currentName)) { data.names.add(currentName); await db.put('users', data); } clearit.addEventListener('mouseenter', e => { Event.emit('hover', { id, currentName }) e.stopPropagation(); }); clearit.addEventListener('mouseleave', () => Event.emit('leave', { id, currentName })); 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(); dockInject(); hoverUserListener(); parseHasCommentList(); })) })();