// ==UserScript== // @name B站一键备注 Rev // @namespace https://github.com/kaixinol/ // @version 1.0.1 // @author Kaesinol // @description B站UP主一键备注 | B站一键备注 Rev 的重写版本 // @license AGPL-3.0-or-later // @icon https://www.bilibili.com/favicon.ico // @website https://github.com/kaixinol/Bilibili-User-Memo // @match https://*.bilibili.com/* // @exclude https://*.hdslb.com/* // @require https://cdn.jsdelivr.net/npm/alpinejs@3.15.8/dist/cdn.min.js // @require https://cdn.jsdelivr.net/npm/query-selector-shadow-dom@1.0.1/dist/querySelectorShadowDom.js // @connect api.bilibili.com // @grant GM.xmlHttpRequest // @grant GM_addStyle // @grant GM_addValueChangeListener // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant unsafeWindow // @run-at document-body // @noframes // ==/UserScript== (function (Alpine, querySelectorShadowDom) { 'use strict'; const d=new Set;const importCSS = async e=>{d.has(e)||(d.add(e),(t=>{typeof GM_addStyle=="function"?GM_addStyle(t):(document.head||document.documentElement).appendChild(document.createElement("style")).append(t);})(e));}; importCSS("*{box-sizing:border-box}button,input{font-family:inherit}:root{--custom-font-size: 14px;--primary-color: #fb7299;--primary-alpha: rgba(251, 114, 153, .15);--link-color: #00aeec;--bg-main: #ffffff;--bg-sub: #f6f8fa;--text-main: #333333;--text-sub: #666666;--text-inverse: #ffffff;--border-main: #d1d1d1;--shadow-elevated: 0 4px 12px rgba(0, 0, 0, .15);--shadow-dark: 0 8px 24px rgba(0, 0, 0, .4);--overlay: rgba(0, 0, 0, .15);--glass: rgba(255, 255, 255, .35);--focus-color: #0366d6;--focus-ring: rgba(3, 102, 214, .2);--danger-color: #d73a49;--danger-alpha: rgba(215, 58, 73, .1)}.memo-container-dark-theme{--bg-main: #18181b;--bg-sub: #202024;--border-main: #2f2f33;--text-main: #e5e7eb;--text-sub: #a1a1aa;--link-color: #7dd3fc}"); importCSS(".panel-container{color-scheme:light;position:fixed;height:46vh;left:0;right:0;bottom:0;z-index:9999;background:var(--bg-main);border-top:2px solid var(--primary-color);box-shadow:0 -4px 12px #00000014;transition:transform .3s cubic-bezier(.4,0,.2,1)}.panel-container.closed{transform:translateY(100%)}.panel-container.open{transform:translateY(0)}.panel-inner{padding:12px 20px;position:relative;height:100%;display:flex;flex-direction:column;box-sizing:border-box}.panel-toggle-btn{position:absolute;top:-34px;left:20px;width:70px;height:34px;border-radius:8px 8px 0 0;cursor:context-menu;font-size:18px;color:var(--text-inverse);background:var(--primary-color);border:none;display:flex;align-items:center;justify-content:center;transition:opacity .3s ease,background .2s ease,top .2s ease;-webkit-user-select:none;user-select:none}.panel-toggle-btn:hover{opacity:1!important;background:var(--primary-color)}.panel-mode-section{display:flex;align-items:center;gap:18px;padding:8px 12px;font-size:16px;position:relative}.panel-right-actions{margin-left:auto;display:flex;align-items:center;gap:10px}.panel-custom-css-section{margin-top:8px;padding:8px 12px;border-radius:10px;background:var(--bg-sub);border:1px solid var(--border-main);display:none;flex-direction:column;gap:6px}.advanced-css-open .panel-custom-css-section{display:flex}.panel-custom-css-title{font-size:13px;font-weight:600;color:var(--text-main)}.panel-custom-css-input{width:100%;min-height:90px;resize:vertical;border-radius:8px;border:1px solid var(--border-main);background:var(--bg-main);color:var(--text-main);font-size:12px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;padding:8px 10px;outline:none;transition:border-color .15s ease,box-shadow .15s ease}.panel-custom-css-input:focus{border-color:var(--primary-color);box-shadow:0 0 0 2px var(--primary-alpha)}.panel-custom-css-status{display:none;font-size:12px;color:var(--text-inverse);background:var(--danger-color);border-radius:6px;padding:4px 8px;line-height:1.4}.panel-custom-css-status.is-visible{display:block}.panel-custom-css-hint{font-size:12px;color:var(--text-sub);line-height:1.4}.panel-custom-color-setting{display:flex;align-items:center;gap:8px;padding:4px 8px;border-radius:10px;background:var(--bg-sub);border:1px solid var(--primary-color);color:var(--text-main);font-size:13px;cursor:pointer;position:relative}.color-preview{width:20px;height:20px;border-radius:4px;border:1px solid var(--border-main);pointer-events:none;background:var(--custom-font-color, var(--primary-color))}.ghost-color-picker{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);opacity:0;width:0;height:0;overflow:hidden;z-index:-1;padding:0;margin:0;border:none}.panel-custom-color-setting label{cursor:pointer}.panel-custom-color-setting input[type=color]{width:24px;height:24px;border:1px solid var(--border-main);border-radius:6px;padding:0;background:var(--bg-main);cursor:pointer}.panel-custom-color-setting input[type=color]::-webkit-color-swatch-wrapper{padding:0}.panel-custom-color-setting input[type=color]::-webkit-color-swatch{border:none;border-radius:4px}.panel-theme-toggle{cursor:pointer;display:flex;align-items:center;justify-content:center;padding:6px;border-radius:8px;transition:all .2s ease;color:var(--text-sub)}.panel-theme-toggle:hover{color:var(--primary-color);transform:rotate(20deg);background:var(--overlay)}.memo-container-dark-theme .panel-theme-toggle:hover{background:var(--glass)}[x-cloak]{display:none!important}.panel-theme-toggle svg{width:20px;height:20px;stroke-width:2px}.panel-mode-label{font-size:15px;font-weight:600;color:var(--text-main)}.panel-mode-option{display:flex;align-items:center;gap:6px;font-size:14px;color:var(--text-sub);cursor:pointer}.panel-control-bar{display:flex;align-items:center;gap:12px;padding:12px 0}.panel-btn{padding:6px 16px;border:1px solid var(--primary-color);background:var(--bg-main);border-radius:20px;color:var(--primary-color);cursor:pointer;font-size:13px;font-weight:500;transition:all .2s}.panel-btn:hover,.panel-btn.btn-active{background:var(--primary-color);color:var(--text-inverse)}.panel-icon-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 10px;border:1px solid var(--primary-color);background:var(--bg-main);border-radius:18px;color:var(--primary-color);cursor:pointer;font-size:12px;transition:all .2s}.panel-icon-btn svg{width:16px;height:16px}.panel-icon-btn:hover{background:var(--primary-color);color:var(--text-inverse)}.panel-icon-text{font-size:12px}.panel-title{font-size:16px;font-weight:700;color:var(--text-main);flex-grow:1}.panel-search-box{border:1px solid var(--primary-color);padding:6px 10px;border-radius:18px;width:180px;display:flex;align-items:center;background:var(--bg-sub);transition:all .2s}.panel-search-box:focus-within{border-color:var(--primary-color);background:var(--bg-main);box-shadow:0 0 0 2px var(--primary-alpha)}.panel-search-input{border:none;outline:none;width:100%;font-size:14px;background:transparent;color:var(--text-main)}.panel-search-clear{color:var(--border-main);font-size:14px;cursor:pointer;border:none;background:none;padding:0 4px}.panel-search-clear:hover{color:var(--primary-color)}.panel-users-container{flex:1;overflow-y:auto;margin-top:10px;padding-right:8px}.panel-users-container::-webkit-scrollbar{width:6px}.panel-users-container::-webkit-scrollbar-thumb{background:var(--border-main);border-radius:10px}.panel-users-container::-webkit-scrollbar-thumb:hover{background:var(--border-main)}.memo-container-dark-theme .panel-container{background:var(--bg-main);border-top-color:var(--primary-color);box-shadow:0 -6px 16px #00000080;color-scheme:dark}.memo-container-dark-theme .panel-toggle-btn{background:var(--primary-color);color:var(--text-inverse)}.memo-container-dark-theme .panel-mode-label{color:var(--text-main)}.memo-container-dark-theme .panel-mode-option{color:var(--text-sub)}.memo-container-dark-theme .panel-btn{background:transparent;color:var(--primary-color);border-color:var(--primary-color)}.memo-container-dark-theme .panel-btn:hover{background:var(--primary-color);color:var(--text-main)}.memo-container-dark-theme .panel-icon-btn{background:transparent;color:var(--primary-color);border-color:var(--primary-color)}.memo-container-dark-theme .panel-icon-btn:hover{background:var(--primary-color);color:var(--text-main)}.memo-container-dark-theme .panel-search-box{background:var(--bg-sub);border:2px solid var(--primary-color)}.memo-container-dark-theme .panel-search-input{color:var(--text-main)}.memo-container-dark-theme .panel-search-clear{color:var(--text-sub)}.memo-container-dark-theme .panel-users-container::-webkit-scrollbar-thumb{background:var(--border-main)}.memo-container-dark-theme .panel-title{color:var(--text-inverse)}.panel-btn:disabled,.panel-btn.btn-disabled{background-color:var(--border-main);color:var(--text-sub);cursor:not-allowed;opacity:.8;transform:none}.panel-icon-btn:disabled{background-color:var(--border-main);color:var(--text-sub);cursor:not-allowed;opacity:.8;transform:none}.loading-spinner{display:inline-block;animation:spin 2s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}"), importCSS('.panel-users-list{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;width:100%}.panel-empty-state{grid-column:1 / -1;display:flex;align-items:center;justify-content:center;padding:24px 12px;color:var(--text-sub);font-size:14px}.user-box{display:flex;align-items:center;flex-wrap:nowrap;position:relative;width:100%;height:72px;padding:0 12px;background:var(--bg-main);border:1px solid var(--border-main);border-radius:8px;box-shadow:var(--shadow-elevated);box-sizing:border-box;transition:all .2s ease}.user-box:hover{border-color:var(--primary-color);background:var(--primary-alpha)}.user-box.is-selected{border-color:var(--primary-color);box-shadow:0 0 0 2px var(--primary-alpha)}.user-select{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-right:8px;flex-shrink:0;cursor:pointer}.user-select input{position:absolute;opacity:0;pointer-events:none}.user-select-mark{width:16px;height:16px;border-radius:4px;border:1px solid var(--border-main);background:var(--bg-main);box-shadow:var(--shadow-elevated)}.user-select input:checked+.user-select-mark{border-color:var(--primary-color);background:var(--primary-color);box-shadow:0 0 0 2px var(--primary-alpha)}.user-select input:checked+.user-select-mark:after{content:"";display:block;width:8px;height:4px;border-left:2px solid #fff;border-bottom:2px solid #fff;transform:translate(3px,4px) rotate(-45deg)}.user-avatar-wrapper{width:44px;height:44px;flex-shrink:0;border-radius:50%;overflow:hidden;margin-right:10px}.user-avatar{width:100%;height:100%;object-fit:cover}.user-info-section{width:85px;flex-shrink:0;display:flex;flex-direction:column;position:relative}.user-nickname-section{flex:1.5;min-width:0;padding:0 8px;display:flex;flex-direction:column}.user-nickname-link{color:var(--link-color);font-size:14px;font-weight:600;text-decoration:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.user-note-section{flex:1;min-width:0;display:flex;flex-direction:column;align-items:flex-start}.user-info-label{font-size:12px;color:var(--text-main);margin-bottom:2px}.user-id{font-family:monospace;font-size:12px;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block;transition:background .2s;cursor:pointer}.user-id.can-expand:hover{overflow:visible;position:absolute;width:fit-content;z-index:100;background-color:var(--bg-main);border:10px solid transparent;margin:-10px;padding:2px 8px;border-radius:4px;box-shadow:var(--shadow-elevated)}.user-note-input{border:none;outline:none;font-size:13px;width:100%;color:var(--custom-font-color, var(--primary-color));background:transparent;padding:0;cursor:pointer;border-bottom:2px dashed transparent;transition:all .2s}.user-note-input:hover{border-bottom-color:var(--primary-alpha)}.user-note-input.editing{cursor:text;background:var(--primary-alpha);border-bottom:2px solid var(--primary-color)}@media(max-width:1200px){.panel-users-list{grid-template-columns:repeat(2,1fr)}}@media(max-width:768px){.panel-users-list{grid-template-columns:1fr}.user-box{height:65px}}.memo-container-dark-theme .user-box{background:var(--bg-sub);border-color:var(--border-main);box-shadow:none}.memo-container-dark-theme .user-box:hover{background:var(--bg-sub);border-color:var(--primary-color)}.memo-container-dark-theme .user-nickname-link{color:var(--link-color)}.memo-container-dark-theme .user-info-label,.memo-container-dark-theme .user-id{color:var(--text-sub)}.memo-container-dark-theme .user-id.can-expand:hover{background-color:var(--bg-sub);color:var(--text-inverse);box-shadow:var(--shadow-dark)}.memo-container-dark-theme .user-note-input{color:var(--custom-font-color, var(--primary-color))}.memo-container-dark-theme .user-note-input.editing{background:var(--primary-alpha)}.memo-container-dark-theme .user-select-mark{background:var(--bg-sub);border-color:var(--text-sub);box-shadow:var(--shadow-dark)}.memo-container-dark-theme .user-select input:checked+.user-select-mark{border-color:var(--primary-color);background:var(--primary-color);box-shadow:0 0 0 2px var(--primary-alpha)}.delete-btn{height:16px;width:16px;position:absolute;top:5px;right:5px;color:var(--primary-color);cursor:pointer;z-index:10}.delete-btn:hover{stroke:var(--danger-color)}'); var _GM_getValue = (() => "undefined" != typeof GM_getValue ? GM_getValue : void 0)(), _GM_setValue = (() => "undefined" != typeof GM_setValue ? GM_setValue : void 0)(), _GM_xmlhttpRequest = (() => "undefined" != typeof GM_xmlhttpRequest ? GM_xmlhttpRequest : void 0)(); class ResImpl { constructor(body, init) { this.rawBody = body, this.init = init, this.body = body.stream(); const {headers: headers, statusCode: statusCode, statusText: statusText, finalUrl: finalUrl, redirected: redirected} = init; this.headers = headers, this.status = statusCode, this.statusText = statusText, this.url = finalUrl, this.type = "basic", this.redirected = redirected, this._bodyUsed = false; } get bodyUsed() { return this._bodyUsed; } get ok() { return this.status < 300; } arrayBuffer() { if (this.bodyUsed) throw new TypeError("Failed to execute 'arrayBuffer' on 'Response': body stream already read"); return this._bodyUsed = true, this.rawBody.arrayBuffer(); } blob() { if (this.bodyUsed) throw new TypeError("Failed to execute 'blob' on 'Response': body stream already read"); return this._bodyUsed = true, Promise.resolve(this.rawBody.slice(0, this.rawBody.size, this.rawBody.type)); } clone() { if (this.bodyUsed) throw new TypeError("Failed to execute 'clone' on 'Response': body stream already read"); return new ResImpl(this.rawBody, this.init); } formData() { if (this.bodyUsed) throw new TypeError("Failed to execute 'formData' on 'Response': body stream already read"); return this._bodyUsed = true, this.rawBody.text().then(decode); } async json() { if (this.bodyUsed) throw new TypeError("Failed to execute 'json' on 'Response': body stream already read"); return this._bodyUsed = true, JSON.parse(await this.rawBody.text()); } text() { if (this.bodyUsed) throw new TypeError("Failed to execute 'text' on 'Response': body stream already read"); return this._bodyUsed = true, this.rawBody.text(); } async bytes() { if (this.bodyUsed) throw new TypeError("Failed to execute 'bytes' on 'Response': body stream already read"); return this._bodyUsed = true, new Uint8Array(await this.rawBody.arrayBuffer()); } } function decode(body) { const form = new FormData; return body.trim().split("&").forEach(function(bytes) { if (bytes) { const split = bytes.split("="), name = split.shift()?.replace(/\+/g, " "), value = split.join("=").replace(/\+/g, " "); form.append(decodeURIComponent(name), decodeURIComponent(value)); } }), form; } async function GM_fetch(input, init) { const request = new Request(input, init); let data; return init?.body && (data = await request.text()), await function(request, init, data) { return new Promise((resolve, reject) => { if (request.signal && request.signal.aborted) return reject(new DOMException("Aborted", "AbortError")); GM.xmlHttpRequest({ url: request.url, method: gmXHRMethod(request.method.toUpperCase()), headers: Object.fromEntries(new Headers(init?.headers).entries()), data: data, responseType: "blob", onload(res) { try { resolve(function(req, res) { const headers = function(h) { const s = h.trim(); if (!s) return new Headers; const array2 = s.split("\r\n").map(value => { let s2 = value.split(":"); return [ s2[0].trim(), s2[1].trim() ]; }); return new Headers(array2); }(res.responseHeaders), body = "string" == typeof res.response ? new Blob([ res.response ], { type: headers.get("Content-Type") || "text/plain" }) : res.response; return new ResImpl(body, { statusCode: res.status, statusText: res.statusText, headers: headers, finalUrl: res.finalUrl, redirected: res.finalUrl === req.url }); }(request, res)); } catch (e) { reject(e); } }, onabort() { reject(new DOMException("Aborted", "AbortError")); }, ontimeout() { reject(new TypeError("Network request failed, timeout")); }, onerror(err) { reject(new TypeError("Failed to fetch: " + err.finalUrl)); } }); }); }(request, init, data); } const httpMethods = [ "GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "TRACE", "OPTIONS", "CONNECT" ]; function gmXHRMethod(method) { if (element = method, httpMethods.includes(element)) return method; var element; throw new Error(`unsupported http method ${method}`); } class Node { value; next; constructor(value) { this.value = value; } } class Queue { #head; #tail; #size; constructor() { this.clear(); } enqueue(value) { const node = new Node(value); this.#head ? (this.#tail.next = node, this.#tail = node) : (this.#head = node, this.#tail = node), this.#size++; } dequeue() { const current = this.#head; if (current) return this.#head = this.#head.next, this.#size--, this.#head || (this.#tail = void 0), current.value; } peek() { if (this.#head) return this.#head.value; } clear() { this.#head = void 0, this.#tail = void 0, this.#size = 0; } get size() { return this.#size; } * [Symbol.iterator]() { let current = this.#head; for (;current; ) yield current.value, current = current.next; } * drain() { for (;this.#head; ) yield this.dequeue(); } } function validateConcurrency(concurrency) { if (!Number.isInteger(concurrency) && concurrency !== Number.POSITIVE_INFINITY || !(concurrency > 0)) throw new TypeError("Expected `concurrency` to be a number from 1 and up"); } const limit = function(concurrency) { let rejectOnClear = false; if ("object" == typeof concurrency && ({concurrency: concurrency, rejectOnClear: rejectOnClear = false} = concurrency), validateConcurrency(concurrency), "boolean" != typeof rejectOnClear) throw new TypeError("Expected `rejectOnClear` to be a boolean"); const queue = new Queue; let activeCount = 0; const resumeNext = () => { activeCount < concurrency && queue.size > 0 && (activeCount++, queue.dequeue().run()); }, run = async (function_, resolve, arguments_) => { const result = (async () => function_(...arguments_))(); resolve(result); try { await result; } catch {} activeCount--, resumeNext(); }, generator = (function_, ...arguments_) => new Promise((resolve, reject) => { ((function_, resolve, reject, arguments_) => { const queueItem = { reject: reject }; new Promise(internalResolve => { queueItem.run = internalResolve, queue.enqueue(queueItem); }).then(run.bind(void 0, function_, resolve, arguments_)), activeCount < concurrency && resumeNext(); })(function_, resolve, reject, arguments_); }); return Object.defineProperties(generator, { activeCount: { get: () => activeCount }, pendingCount: { get: () => queue.size }, clearQueue: { value() { if (!rejectOnClear) return void queue.clear(); const abortError = AbortSignal.abort().reason; for (;queue.size > 0; ) queue.dequeue().reject(abortError); } }, concurrency: { get: () => concurrency, set(newConcurrency) { validateConcurrency(newConcurrency), concurrency = newConcurrency, queueMicrotask(() => { for (;activeCount < concurrency && queue.size > 0; ) resumeNext(); }); } }, map: { async value(iterable, function_) { const promises = Array.from(iterable, (value, index) => this(function_, value, index)); return Promise.all(promises); } } }), generator; }(2); "undefined" == typeof GM && (window.GM = { xmlHttpRequest: _GM_xmlhttpRequest }); const MIXIN_KEY_ENC_TAB = [ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52 ]; const cache = new Map, getUserInfo = function(fn) { return async (...args) => { const key = JSON.stringify(args), now = Date.now(); if (cache.has(key) && now - cache.get(key).time < 3e5) return cache.get(key).data; const result = await fn(...args); return cache.set(key, { data: result, time: now }), result; }; }((fn = async function(mid) { try { const {img_key: img_key, sub_key: sub_key} = await async function() { const now = Date.now(), cache2 = _GM_getValue("bili_wbi_keys", null); if (cache2 && now - cache2.timestamp < 36e5) return { img_key: cache2.img_key, sub_key: cache2.sub_key }; try { const res = await GM_fetch("https://api.bilibili.com/x/web-interface/nav", { headers: { Referer: "https://www.bilibili.com/" } }), json = await res.json(), {img_url: img_url, sub_url: sub_url} = json.data.wbi_img, keys = { img_key: img_url.slice(img_url.lastIndexOf("/") + 1, img_url.lastIndexOf(".")), sub_key: sub_url.slice(sub_url.lastIndexOf("/") + 1, sub_url.lastIndexOf(".")) }; return _GM_setValue("bili_wbi_keys", { ...keys, timestamp: now }), keys; } catch (err) { throw new Error("WBI key 初始化失败"); } }(), url = `https://api.bilibili.com/x/space/wbi/acc/info?${function(params, img_key, sub_key) { const mixin_key = (orig = img_key + sub_key, MIXIN_KEY_ENC_TAB.map(n => orig[n]).join("").slice(0, 32)); var orig; const curr_time = Math.round(Date.now() / 1e3), chr_filter = /[!'()*]/g, signedParams = { ...params, wts: curr_time }, query = Object.entries(signedParams).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => { const filteredValue = String(value).replace(chr_filter, ""); return `${encodeURIComponent(key)}=${encodeURIComponent(filteredValue)}`; }).join("&"); return `${query}&w_rid=${(message => { const buffer = (new TextEncoder).encode(message), n = buffer.length, words = new Uint32Array(1 + (n + 8 >> 6) << 4); for (let i = 0; i < n; i++) words[i >> 2] |= buffer[i] << i % 4 * 8; words[n >> 2] |= 128 << n % 4 * 8, words[words.length - 2] = 8 * n; let [a, b, c, d] = [ 1732584193, 4023233417, 2562383102, 271733878 ]; const K = Uint32Array.from({ length: 64 }, (_, i) => 4294967296 * Math.abs(Math.sin(i + 1)) >>> 0), S = [ 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21 ], rotl = (x, n2) => x << n2 | x >>> 32 - n2; for (let i = 0; i < words.length; i += 16) { let [A, B, C, D] = [ a, b, c, d ]; for (let j = 0; j < 64; j++) { let f, g; j < 16 ? (f = B & C | ~B & D, g = j) : j < 32 ? (f = D & B | ~D & C, g = (5 * j + 1) % 16) : j < 48 ? (f = B ^ C ^ D, g = (3 * j + 5) % 16) : (f = C ^ (B | ~D), g = 7 * j % 16); const temp = D; D = C, C = B, B = B + rotl(A + f + K[j] + words[i + g] | 0, S[j]) | 0, A = temp; } a = a + A | 0, b = b + B | 0, c = c + C | 0, d = d + D | 0; } const outBuf = new ArrayBuffer(16), view = new DataView(outBuf); return [ a, b, c, d ].forEach((val, i) => view.setUint32(4 * i, val, !0)), Array.from(new Uint8Array(outBuf)).map(b2 => b2.toString(16).padStart(2, "0")).join(""); })(query + mixin_key)}`; }({ mid: mid, token: "", platform: "web", web_location: 1550101 }, img_key, sub_key)}`, response = await GM_fetch(url, { headers: { Referer: "https://space.bilibili.com/" } }); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); const res = await response.json(); if (0 !== res.code) throw new Error(`Bilibili API error: ${res.message}`); return { nickname: res.data.name, avatar: res.data.face + "@96w_96h_1c_1s.avif" }; } catch (error) { throw error; } }, (...args) => limit(async () => (await new Promise(resolve => setTimeout(resolve, 300 + 500 * Math.random())), fn(...args))))); var fn, InjectionMode = (InjectionMode2 => (InjectionMode2[InjectionMode2.Static = 1] = "Static", InjectionMode2[InjectionMode2.Dynamic = 2] = "Dynamic", InjectionMode2[InjectionMode2.Polling = 3] = "Polling", InjectionMode2))(InjectionMode || {}), StyleScope = (StyleScope2 => (StyleScope2[StyleScope2.Minimal = 1] = "Minimal", StyleScope2[StyleScope2.Editable = 2] = "Editable", StyleScope2))(StyleScope || {}); const config = new Map([ [ /^https:\/\/www\.bilibili\.com\/(video|list)\/.*/, { name: "视频页面", injectMode: 1, styleScope: 2, aSelector: ".up-name" } ], [ /^https:\/\/www\.bilibili\.com\/(video|list)\/.*/, { name: "视频页面", injectMode: 1, styleScope: 1, aSelector: "a.staff-name" } ], [ /^https:\/\/www\.bilibili\.com\/(video|list)\/.*/, { name: "视频页面 - 推荐", injectMode: 2, styleScope: 1, aSelector: ".upname a", textSelector: "span.name", trigger: { watch: ".rcmd-tab", interval: 1e3 } } ], [ /^https:\/\/space\.bilibili\.com\/.*/, { name: "空间", injectMode: 1, styleScope: 2, aSelector: ".nickname", fontSize: "24px" } ], [ /^https:\/\/space\.bilibili\.com\/\d+\/favlist?fid=\d+&ftype=create/, { name: "空间收藏夹", injectMode: 2, styleScope: 1, aSelector: ".bili-video-card__author", trigger: { watch: ".favlist-main", interval: 1e3 } } ], [ /^https:\/\/[a-z0-9.]+\.bilibili\.com\/.*/, { name: "评论区", injectMode: 2, styleScope: 2, aSelector: "#user-name a", trigger: { watch: "#contents", interval: 1e3 } } ], [ /^https:\/\/message\.bilibili\.com\/.*/, { name: "私信", injectMode: 3, styleScope: 1, aSelector: 'div[data-id^="contact"]', textSelector: "div[class^='_SessionItem__Name']", trigger: { watch: "div[class^='_Sidebar_']", interval: 3e3 } } ], [ /^https:\/\/message\.bilibili\.com\/.*/, { name: "私信 - 当前", injectMode: 3, styleScope: 1, aSelector: 'div[class*="_SessionItemIsActive_"]', textSelector: "div[class^='_ContactName_']", trigger: { watch: "div.message-content", interval: 3e3 }, ignoreProcessed: true, useFallback: true } ] ]), sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); function formatDisplayName(user, fallbackName, displayMode) { const nickname = (user?.nickname || fallbackName || "").trim(), memo = (user?.memo || "").trim(); if (!memo) return nickname; switch (displayMode) { case 0: default: return nickname; case 1: return `${memo}(${nickname})`; case 2: return `${nickname}(${memo})`; case 3: return memo; } } function getUserAvatar(userID) { const sourceSrc = querySelectorShadowDom.querySelectorDeep(`#user-avatar[data-user-profile-id="${userID}"] bili-avatar source , div.avatar source`)?.getAttribute("srcset"); if (sourceSrc) return sourceSrc; const imgSrc = querySelectorShadowDom.querySelectorDeep(`up-avatar-wrap a[href*="${userID}"] img.bili-avatar-img`)?.getAttribute("data-src"); return imgSrc || "https://i0.hdslb.com/bfs/face/member/noface.jpg@96w_96h_1c_1s.avif"; } const userStore = new class { users=[]; displayMode=2; isSystemChanging=false; constructor() { this.refreshData(), this.listenToRemoteChanges(); } refreshData() { this.users = GM_getValue("biliUsers", []), this.displayMode = GM_getValue("displayMode", 2), this.users.length, this.displayMode; } listenToRemoteChanges() { GM_addValueChangeListener("biliUsers", (name, oldValue, newValue, remote) => { if (!this.isSystemChanging && remote) { this.isSystemChanging = true; try { this.users = newValue || [], this.syncFullStateToAlpine(), this.refreshAllDomNodes(); } catch (e) {} finally { this.isSystemChanging = false; } } }), GM_addValueChangeListener("displayMode", (name, oldValue, newValue, remote) => { remote && (this.displayMode = newValue ?? 2, this.refreshAllDomNodes()); }); } ensureUser(uid, originalName) { const existing = this.users.find(u => u.id === uid); if (existing) return existing; const newUser = { id: uid, nickname: originalName || uid, avatar: getUserAvatar(uid), memo: "" }; return this.users.push(newUser), newUser; } updateUserMemo(uid, newMemo, fallbackName = "") { this.isSystemChanging = true; try { let user, userIndex = this.users.findIndex(u => u.id === uid); -1 === userIndex ? (user = { id: uid, nickname: fallbackName || uid, avatar: getUserAvatar(uid), memo: newMemo }, this.users.push(user), userIndex = this.users.length - 1) : (user = this.users[userIndex], user.memo = newMemo), "" === newMemo.trim() && this.users.splice(userIndex, 1), GM_setValue("biliUsers", this.users), this.syncToAlpine(uid, newMemo, user), this.syncDomNodes(uid, newMemo, user, fallbackName); } catch (error) {} finally { setTimeout(() => { this.isSystemChanging = false; }, 200); } } syncToAlpine(uid, newMemo, user) { try { const store = Alpine.store("userList"); if (!store || !store.users) return; const storeIndex = store.users.findIndex(u => u.id === uid); "" === newMemo.trim() ? -1 !== storeIndex && store.users.splice(storeIndex, 1) : -1 !== storeIndex ? store.users[storeIndex].memo = newMemo : store.users.push({ ...user }); } catch {} } syncFullStateToAlpine() { try { const store = Alpine.store("userList"); store && store.users && (store.users = [ ...this.users ]); } catch {} } refreshAllDomNodes() { querySelectorShadowDom.querySelectorAllDeep(".bili-memo-tag[data-bili-uid], .editable-textarea[data-bili-uid]").forEach(tag => { const uid = tag.getAttribute("data-bili-uid"), originalName = tag.getAttribute("data-bili-original") || ""; if (!uid) return; const user = this.users.find(u => u.id === uid), memo = user ? user.memo : "", userObj = user || { id: uid, nickname: originalName, avatar: "", memo: "" }; this.syncDomNodes(uid, memo, userObj, originalName); }); } syncDomNodes(uid, newMemo, user, fallbackName) { querySelectorShadowDom.querySelectorAllDeep(`.bili-memo-tag[data-bili-uid="${uid}"], .editable-textarea[data-bili-uid="${uid}"]`).forEach(tag => { const originalName = tag.getAttribute("data-bili-original") || fallbackName; "" === newMemo.trim() ? (tag.textContent = originalName, tag.classList.contains("editable-textarea") || tag.classList.remove("bili-memo-tag")) : (tag.textContent = formatDisplayName(user, originalName, this.displayMode), tag.classList.contains("bili-memo-tag") || false !== tag.classList.contains("editable-textarea") || tag.classList.add("bili-memo-tag")); }); } }, GLOBAL_STYLE_SHEET = new CSSStyleSheet; GLOBAL_STYLE_SHEET.replaceSync(".editable-textarea{font-size:var(--custom-font-size, 13px);font-weight:600;min-width:2ch;color:var(--custom-font-color, black);margin-right:8px;padding:4px 6px;display:inline-block;white-space:nowrap;background:var(--glass);border:none;border-bottom:2px solid var(--overlay);outline:none;border-radius:6px 6px 0 0;text-shadow:0 1px 2px var(--overlay);transition:border-color .2s ease,background .2s ease,opacity .2s ease;opacity:.85}.editable-textarea:hover{background:var(--glass)}.editable-textarea:focus{border-bottom-color:var(--focus-color);background:var(--bg-main);opacity:1}.bili-memo-tag{cursor:pointer;display:inline;vertical-align:middle;color:var(--custom-font-color, black);font-size:var(--custom-font-size, 13px)}.bili-memo-input{background:var(--bg-main);border:1px solid var(--primary-color);font-size:var(--custom-font-size, 13px);padding:0 4px;margin-left:4px;border-radius:4px;outline:none;width:var(--memo-input-width, 60px);display:inline-block;height:calc(var(--custom-font-size, 14px) + 6px);line-height:calc(var(--custom-font-size, 14px) + 6px);vertical-align:middle}:host-context(.memo-container-dark-theme) .bili-memo-tag,.memo-container-dark-theme .bili-memo-tag{color:var(--custom-font-color, white)}:host-context(.memo-container-dark-theme) .editable-textarea,.memo-container-dark-theme .editable-textarea{background:var(--overlay);border-bottom-color:var(--glass);color:var(--custom-font-color, white)}:host-context(.memo-container-dark-theme) .editable-textarea:focus{background:var(--bg-sub);border-bottom-color:var(--primary-color)}:host-context(.memo-container-dark-theme) .bili-memo-input,.memo-container-dark-theme .bili-memo-input{background:var(--bg-main);border-color:var(--primary-color);color:var(--text-main)}"); const CUSTOM_MEMO_STYLE_SHEET = new CSSStyleSheet; function ensureMemoStyleSheets(root) { const sheets = root.adoptedStyleSheets, hasGlobal = sheets.includes(GLOBAL_STYLE_SHEET), hasCustom = sheets.includes(CUSTOM_MEMO_STYLE_SHEET); if (hasGlobal && hasCustom) return; const next = sheets.slice(); hasGlobal || next.push(GLOBAL_STYLE_SHEET), hasCustom || next.push(CUSTOM_MEMO_STYLE_SHEET), root.adoptedStyleSheets = next; } function ensureStylesForElement(target) { const root = target.getRootNode(); (root instanceof ShadowRoot || root instanceof Document) && ensureMemoStyleSheets(root); } function setCustomMemoCss(css) { const nextCss = css ?? ""; try { CUSTOM_MEMO_STYLE_SHEET.replaceSync(nextCss); } catch (error) { return { ok: false, error: error instanceof Error ? error.message : `未知错误: ${String(error)}`, ruleCount: CUSTOM_MEMO_STYLE_SHEET.cssRules.length }; } return function() { const targets = querySelectorShadowDom.querySelectorAllDeep(".bili-memo-tag, .editable-textarea, .bili-memo-input"); if (!targets || 0 === targets.length) return; const roots = new Set; targets.forEach(el => { const root = el.getRootNode(); (root instanceof ShadowRoot || root instanceof Document) && roots.add(root); }), roots.forEach(root => ensureMemoStyleSheets(root)); }(), { ok: true, ruleCount: CUSTOM_MEMO_STYLE_SHEET.cssRules.length }; } CUSTOM_MEMO_STYLE_SHEET.replaceSync(""); const wrapperCache = new WeakMap; function renderMinimal(element, text, user, meta) { return !!element && (ensureStylesForElement(element), element.textContent !== text && (element.textContent = text), updateElementState(element, user, meta), true); } function updateElementState(el, user, meta) { el.dataset.biliUid !== meta.uid && (el.dataset.biliUid = meta.uid), el.dataset.biliOriginal !== meta.originalName && (el.dataset.biliOriginal = meta.originalName), !el.classList.contains("bili-memo-tag") && user.memo && user.memo !== meta.originalName && el.classList.add("bili-memo-tag"); } class DynamicRuleWatcher { constructor(rule, onTrigger) { this.rule = rule, this.onTrigger = onTrigger; } observer=null; pollTimer=null; start() { this.tryAttachOrPoll(); } stop() { this.pollTimer && (clearInterval(this.pollTimer), this.pollTimer = null), this.observer && (this.observer.disconnect(), this.observer = null); } tryAttachOrPoll() { this.attach() || this.pollTimer || (this.pollTimer = window.setInterval(() => { this.attach() && (this.pollTimer && clearInterval(this.pollTimer), this.pollTimer = null, this.rule.name); }, 800)); } attach() { const watchTarget = querySelectorShadowDom.querySelectorDeep(this.rule.trigger.watch); if (!watchTarget) return false; const scope = watchTarget.shadowRoot || watchTarget; return this.observer = new MutationObserver(mutations => { mutations.some(m => m.addedNodes.length > 0) && this.onTrigger(this.rule, scope); }), this.observer.observe(scope, { childList: true, subtree: true }), this.onTrigger(this.rule, scope), true; } } class PollingRuleWatcher { constructor(rule, onTrigger) { this.rule = rule, this.onTrigger = onTrigger; } pollTimer=null; start() { this.rule.name, this.rule.trigger.interval, this.rule.trigger.watch, this.tick(), this.pollTimer = window.setInterval(() => this.tick(), this.rule.trigger.interval); } stop() { this.pollTimer && (clearInterval(this.pollTimer), this.pollTimer = null), this.rule.name; } tick() { const watchTarget = querySelectorShadowDom.querySelectorDeep(this.rule.trigger.watch); if (!watchTarget) return this.rule.name, void this.rule.trigger.watch; const scope = watchTarget.shadowRoot || watchTarget; this.rule.name, this.rule.trigger.watch, this.onTrigger(this.rule, scope); } } class PageInjector { domReady=false; lastUrl=""; activeWatchers= new Map; activePollingWatchers= new Map; ruleDebounceTimers= new Map; constructor() { userStore.refreshData(), this.startUrlMonitor(), this.onDomReady(async () => { await this.waitForBiliEnvironment(), await sleep(100), this.domReady = true, this.handleUrlChange(); }); } refreshData() { if (userStore.refreshData(), this.domReady) { this.refreshExistingDomNodes(); const activeRules = [ ...this.activeWatchers.keys(), ...this.activePollingWatchers.keys() ]; activeRules.length > 0 && this.scanSpecificRules(activeRules, document); } } startUrlMonitor() { this.lastUrl = unsafeWindow.location.href, window.setInterval(() => { const currentUrl = unsafeWindow.location.href; currentUrl !== this.lastUrl && (this.lastUrl = currentUrl, this.handleUrlChange()); }, 1e3); } handleUrlChange() { if (!this.domReady) return; const matchedRules = this.getMatchedRules(), staticRules = matchedRules.filter(r => r.injectMode === InjectionMode.Static), dynamicRules = matchedRules.filter(r => r.injectMode === InjectionMode.Dynamic), pollingRules = matchedRules.filter(r => r.injectMode === InjectionMode.Polling); staticRules.length > 0 && this.scanSpecificRules(staticRules, document), this.reconcileWatchers(dynamicRules), this.reconcilePollingWatchers(pollingRules); } reconcileWatchers(newRules) { for (const [rule, watcher] of this.activeWatchers) newRules.includes(rule) || (watcher.stop(), this.activeWatchers.delete(rule)); newRules.forEach(rule => { if (!this.activeWatchers.has(rule)) { const watcher = new DynamicRuleWatcher(rule, (r, scope) => { this.scheduleRuleScan(r, r.trigger.interval, scope); }); this.activeWatchers.set(rule, watcher), watcher.start(); } }); } reconcilePollingWatchers(newRules) { for (const [rule, watcher] of this.activePollingWatchers) newRules.includes(rule) || (watcher.stop(), this.activePollingWatchers.delete(rule)); newRules.forEach(rule => { if (!this.activePollingWatchers.has(rule)) { const watcher = new PollingRuleWatcher(rule, (r, scope) => { this.scanSpecificRules([ r ], scope); }); this.activePollingWatchers.set(rule, watcher), watcher.start(); } }); } scheduleRuleScan(rule, delay, scope) { const existing = this.ruleDebounceTimers.get(rule); existing && clearTimeout(existing); const timerId = window.setTimeout(() => { this.ruleDebounceTimers.delete(rule), this.scanSpecificRules([ rule ], scope); }, delay); this.ruleDebounceTimers.set(rule, timerId); } scanSpecificRules(rules, scope) { if (0 === rules.length) return; const queue = [ ...rules ], runChunk = deadline => { (async () => { for (;queue.length > 0 && deadline.timeRemaining() > 1; ) { const rule = queue.shift(); await this.scanAndInjectRule(rule, scope); } queue.length > 0 && this.requestIdle(runChunk); })(); }; this.requestIdle(runChunk); } requestIdle(cb) { const ric = window.requestIdleCallback || (fn => setTimeout(() => fn({ timeRemaining: () => 16 }), 16)); ric(cb, { timeout: 1e3 }); } async scanAndInjectRule(rule, scope) { let selector = `${rule.aSelector}`; if (rule.ignoreProcessed || (selector += ":not([data-bili-processed])"), rule.injectMode === InjectionMode.Static) { let elements2 = querySelectorShadowDom.querySelectorAllDeep(selector, scope); if (0 === elements2.length) for (let i = 0; i < 3 && (await sleep(300), elements2 = querySelectorShadowDom.querySelectorAllDeep(selector, scope), !(elements2.length > 0)); i++) ; return void (elements2.length > 0 && (elements2.length, elements2.forEach(element => { this.applyRuleToElement(element, rule); }))); } const elements = querySelectorShadowDom.querySelectorAllDeep(selector, scope); rule.injectMode === InjectionMode.Polling && elements.length > 0 && (rule.name, elements.length), elements.forEach(el => this.applyRuleToElement(el, rule)); } async applyRuleToElement(el, rule) { const uid = function(el) { const dataUid = el.getAttribute("data-id")?.split("_")?.[1] || el.getAttribute("data-user-profile-id"); if (dataUid) return dataUid; const initialState = unsafeWindow.__INITIAL_STATE__, href = el.getAttribute("href") || location.href; if (href) { const match = href.match(/space\.bilibili\.com\/(\d+)/); if (match) return match[1]; } return initialState?.detail?.basic?.uid || initialState?.detail?.modules?.find(m => m.module_author)?.module_author?.mid || null; }(el), originalName = function(el, rule) { if (rule.textSelector) { const target = el.querySelector(rule.textSelector); if (target?.textContent) return target.textContent.trim(); } return el.textContent?.trim() || ""; }(el, rule); if (!uid) return; const user = userStore.ensureUser(uid, originalName); await async function(el, user, rule, meta) { const displayText = formatDisplayName(user, meta.originalName, userStore.displayMode); switch (rule.styleScope) { case StyleScope.Minimal: if (!rule.textSelector) return renderMinimal(el, displayText, user, meta); if (!rule.useFallback) return renderMinimal(el.querySelector(rule.textSelector), displayText, user, meta); if (rule.trigger) return renderMinimal(document.querySelector(rule.trigger.watch).querySelector(rule.textSelector), displayText, user, meta); case StyleScope.Editable: return function(el, text, user, rule, meta) { let wrapper = wrapperCache.get(el); return !wrapper && el.nextElementSibling?.classList.contains("editable-textarea") && (wrapper = el.nextElementSibling, wrapperCache.set(el, wrapper)), wrapper || (wrapper = document.createElement("span"), wrapper.classList.add("editable-textarea"), wrapper.addEventListener("click", e => { e.stopPropagation(), e.preventDefault(), function(targetElement, user) { if (!user || targetElement.querySelector("input.bili-memo-input")) return; const originalName = targetElement.dataset.biliOriginal || targetElement.textContent || "", currentMemo = user.memo || originalName, input = document.createElement("input"); input.type = "text", input.value = currentMemo, input.className = "bili-memo-input", input.placeholder = "输入备注..."; const updateWidth = () => { const len = (input.value || "").replace(/[^\x00-\xff]/g, "xx").length; input.style.width = `${Math.max(8 * len + 20, 80)}px`; }; updateWidth(), input.addEventListener("input", updateWidth); const parent = targetElement.parentElement; if (!parent) return; targetElement.style.display = "none", parent.insertBefore(input, targetElement.nextSibling), input.focus(); const saveAndExit = shouldSave => { if (!input.isConnected) return; const newValue = input.value.trim(); if (input.remove(), targetElement.style.display = "", shouldSave && newValue !== currentMemo) { userStore.updateUserMemo(user.id, newValue, originalName); const newDisplayText = formatDisplayName({ ...user, memo: newValue }, originalName, userStore.displayMode); targetElement.textContent = newDisplayText, newValue ? targetElement.classList.add("bili-memo-tag") : targetElement.classList.remove("bili-memo-tag"); } }; input.addEventListener("keydown", e => { e.isComposing || ("Enter" === e.key ? (e.preventDefault(), saveAndExit(true)) : "Escape" === e.key && (e.preventDefault(), saveAndExit(false))); }), input.addEventListener("blur", () => saveAndExit(true)), input.addEventListener("click", e => e.stopPropagation()); }(wrapper, user); }), el.style.display = "none", el.insertAdjacentElement("afterend", wrapper), wrapperCache.set(el, wrapper)), wrapper.textContent !== text && (wrapper.textContent = text), rule.fontSize && wrapper.style.setProperty("--custom-font-size", rule.fontSize), updateElementState(wrapper, user, meta), ensureStylesForElement(wrapper), true; }(el, displayText, user, rule, meta); default: return rule.styleScope, false; } }(el, user, rule, { uid: uid, originalName: originalName }) && el.setAttribute("data-bili-processed", "true"); } getMatchedRules() { const currentUrl = unsafeWindow.location.href; return Array.from(config.entries()).filter(([pattern]) => pattern.test(currentUrl)).map(([_, rule]) => rule); } refreshExistingDomNodes() { querySelectorShadowDom.querySelectorAllDeep(".bili-memo-tag, .editable-textarea").forEach(tag => { const uid = tag.getAttribute("data-bili-uid"), originalName = tag.getAttribute("data-bili-original") || ""; if (!uid) return; const user = userStore.users.find(u => u.id === uid); user && user.memo ? (tag.textContent = formatDisplayName(user, originalName, userStore.displayMode), tag.classList.contains("bili-memo-tag") || tag.classList.contains("editable-textarea") || tag.classList.add("bili-memo-tag")) : (tag.textContent = originalName, tag.classList.remove("bili-memo-tag")); }); } onDomReady(callback) { "complete" !== document.readyState && "interactive" !== document.readyState ? window.addEventListener("DOMContentLoaded", () => callback(), { once: true }) : callback(); } async waitForBiliEnvironment() { return new Promise(resolve => { const check = () => { unsafeWindow.__VUE__ ? resolve() : setTimeout(check, 50); }; check(); }); } } let pageInjector = null; function refreshPageInjection() { pageInjector?.refreshData(); } // @__NO_SIDE_EFFECTS__ function getGlobalConfig(config$1) { return { lang: config$1?.lang ?? void 0, message: config$1?.message, abortEarly: config$1?.abortEarly ?? void 0, abortPipeEarly: config$1?.abortPipeEarly ?? void 0 }; } // @__NO_SIDE_EFFECTS__ function _stringify(input) { const type = typeof input; return "string" === type ? `"${input}"` : "number" === type || "bigint" === type || "boolean" === type ? `${input}` : "object" === type || "function" === type ? (input && Object.getPrototypeOf(input)?.constructor?.name) ?? "null" : type; } function _addIssue(context, label, dataset, config$1, other) { const input = other && "input" in other ? other.input : dataset.value, expected = other?.expected ?? context.expects ?? null, received = other?.received ?? _stringify(input), issue = { kind: context.kind, type: context.type, input: input, expected: expected, received: received, message: `Invalid ${label}: ${expected ? `Expected ${expected} but r` : "R"}eceived ${received}`, requirement: context.requirement, path: other?.path, issues: other?.issues, lang: config$1.lang, abortEarly: config$1.abortEarly, abortPipeEarly: config$1.abortPipeEarly }, isSchema = "schema" === context.kind, message$1 = other?.message ?? context.message ?? ( context.reference, void issue.lang) ?? (isSchema ? void issue.lang : null) ?? config$1.message ?? void issue.lang; void 0 !== message$1 && (issue.message = "function" == typeof message$1 ? message$1(issue) : message$1), isSchema && (dataset.typed = false), dataset.issues ? dataset.issues.push(issue) : dataset.issues = [ issue ]; } // @__NO_SIDE_EFFECTS__ function _getStandardProps(context) { return { version: 1, vendor: "valibot", validate: value$1 => context["~run"]({ value: value$1 }, getGlobalConfig()) }; } // @__NO_SIDE_EFFECTS__ function _isValidObjectKey(object$1, key) { return Object.hasOwn(object$1, key) && "__proto__" !== key && "prototype" !== key && "constructor" !== key; } // @__NO_SIDE_EFFECTS__ function _joinExpects(values$1, separator) { const list = [ ...new Set(values$1) ]; return list.length > 1 ? `(${list.join(` ${separator} `)})` : list[0] ?? "never"; } // @__NO_SIDE_EFFECTS__ function getFallback(schema, dataset, config$1) { return "function" == typeof schema.fallback ? schema.fallback(dataset, config$1) : schema.fallback; } // @__NO_SIDE_EFFECTS__ function getDefault(schema, dataset, config$1) { return "function" == typeof schema.default ? schema.default(dataset, config$1) : schema.default; } // @__NO_SIDE_EFFECTS__ function array(item, message$1) { return { kind: "schema", type: "array", reference: array, expects: "Array", async: false, item: item, message: message$1, get "~standard"() { return _getStandardProps(this); }, "~run"(dataset, config$1) { const input = dataset.value; if (Array.isArray(input)) { dataset.typed = true, dataset.value = []; for (let key = 0; key < input.length; key++) { const value$1 = input[key], itemDataset = this.item["~run"]({ value: value$1 }, config$1); if (itemDataset.issues) { const pathItem = { type: "array", origin: "value", input: input, key: key, value: value$1 }; for (const issue of itemDataset.issues) issue.path ? issue.path.unshift(pathItem) : issue.path = [ pathItem ], dataset.issues?.push(issue); if (dataset.issues || (dataset.issues = itemDataset.issues), config$1.abortEarly) { dataset.typed = false; break; } } itemDataset.typed || (dataset.typed = false), dataset.value.push(itemDataset.value); } } else _addIssue(this, "type", dataset, config$1); return dataset; } }; } // @__NO_SIDE_EFFECTS__ function object(entries$1, message$1) { return { kind: "schema", type: "object", reference: object, expects: "Object", async: false, entries: entries$1, message: message$1, get "~standard"() { return _getStandardProps(this); }, "~run"(dataset, config$1) { const input = dataset.value; if (input && "object" == typeof input) { dataset.typed = true, dataset.value = {}; for (const key in this.entries) { const valueSchema = this.entries[key]; if (key in input || ("exact_optional" === valueSchema.type || "optional" === valueSchema.type || "nullish" === valueSchema.type) && void 0 !== valueSchema.default) { const value$1 = key in input ? input[key] : getDefault(valueSchema), valueDataset = valueSchema["~run"]({ value: value$1 }, config$1); if (valueDataset.issues) { const pathItem = { type: "object", origin: "value", input: input, key: key, value: value$1 }; for (const issue of valueDataset.issues) issue.path ? issue.path.unshift(pathItem) : issue.path = [ pathItem ], dataset.issues?.push(issue); if (dataset.issues || (dataset.issues = valueDataset.issues), config$1.abortEarly) { dataset.typed = false; break; } } valueDataset.typed || (dataset.typed = false), dataset.value[key] = valueDataset.value; } else if (void 0 !== valueSchema.fallback) dataset.value[key] = getFallback(valueSchema); else if ("exact_optional" !== valueSchema.type && "optional" !== valueSchema.type && "nullish" !== valueSchema.type && (_addIssue(this, "key", dataset, config$1, { input: void 0, expected: `"${key}"`, path: [ { type: "object", origin: "key", input: input, key: key, value: input[key] } ] }), config$1.abortEarly)) break; } } else _addIssue(this, "type", dataset, config$1); return dataset; } }; } // @__NO_SIDE_EFFECTS__ function optional(wrapped, default_) { return { kind: "schema", type: "optional", reference: optional, expects: `(${wrapped.expects} | undefined)`, async: false, wrapped: wrapped, default: default_, get "~standard"() { return _getStandardProps(this); }, "~run"(dataset, config$1) { return void 0 === dataset.value && (void 0 !== this.default && (dataset.value = getDefault(this, dataset, config$1)), void 0 === dataset.value) ? (dataset.typed = true, dataset) : this.wrapped["~run"](dataset, config$1); } }; } // @__NO_SIDE_EFFECTS__ function record(key, value$1, message$1) { return { kind: "schema", type: "record", reference: record, expects: "Object", async: false, key: key, value: value$1, message: message$1, get "~standard"() { return _getStandardProps(this); }, "~run"(dataset, config$1) { const input = dataset.value; if (input && "object" == typeof input) { dataset.typed = true, dataset.value = {}; for (const entryKey in input) if ( _isValidObjectKey(input, entryKey)) { const entryValue = input[entryKey], keyDataset = this.key["~run"]({ value: entryKey }, config$1); if (keyDataset.issues) { const pathItem = { type: "object", origin: "key", input: input, key: entryKey, value: entryValue }; for (const issue of keyDataset.issues) issue.path = [ pathItem ], dataset.issues?.push(issue); if (dataset.issues || (dataset.issues = keyDataset.issues), config$1.abortEarly) { dataset.typed = false; break; } } const valueDataset = this.value["~run"]({ value: entryValue }, config$1); if (valueDataset.issues) { const pathItem = { type: "object", origin: "value", input: input, key: entryKey, value: entryValue }; for (const issue of valueDataset.issues) issue.path ? issue.path.unshift(pathItem) : issue.path = [ pathItem ], dataset.issues?.push(issue); if (dataset.issues || (dataset.issues = valueDataset.issues), config$1.abortEarly) { dataset.typed = false; break; } } keyDataset.typed && valueDataset.typed || (dataset.typed = false), keyDataset.typed && (dataset.value[keyDataset.value] = valueDataset.value); } } else _addIssue(this, "type", dataset, config$1); return dataset; } }; } // @__NO_SIDE_EFFECTS__ function string(message$1) { return { kind: "schema", type: "string", reference: string, expects: "string", async: false, message: message$1, get "~standard"() { return _getStandardProps(this); }, "~run"(dataset, config$1) { return "string" == typeof dataset.value ? dataset.typed = true : _addIssue(this, "type", dataset, config$1), dataset; } }; } // @__NO_SIDE_EFFECTS__ function _subIssues(datasets) { let issues; if (datasets) for (const dataset of datasets) issues ? issues.push(...dataset.issues) : issues = dataset.issues; return issues; } // @__NO_SIDE_EFFECTS__ function union(options, message$1) { return { kind: "schema", type: "union", reference: union, expects: _joinExpects(options.map(option => option.expects), "|"), async: false, options: options, message: message$1, get "~standard"() { return _getStandardProps(this); }, "~run"(dataset, config$1) { let validDataset, typedDatasets, untypedDatasets; for (const schema of this.options) { const optionDataset = schema["~run"]({ value: dataset.value }, config$1); if (optionDataset.typed) { if (!optionDataset.issues) { validDataset = optionDataset; break; } typedDatasets ? typedDatasets.push(optionDataset) : typedDatasets = [ optionDataset ]; } else untypedDatasets ? untypedDatasets.push(optionDataset) : untypedDatasets = [ optionDataset ]; } if (validDataset) return validDataset; if (typedDatasets) { if (1 === typedDatasets.length) return typedDatasets[0]; _addIssue(this, "type", dataset, config$1, { issues: _subIssues(typedDatasets) }), dataset.typed = true; } else { if (1 === untypedDatasets?.length) return untypedDatasets[0]; _addIssue(this, "type", dataset, config$1, { issues: _subIssues(untypedDatasets) }); } return dataset; } }; } // @__NO_SIDE_EFFECTS__ function safeParse(schema, input, config$1) { const dataset = schema["~run"]({ value: input }, getGlobalConfig(config$1)); return { typed: dataset.typed, success: !dataset.issues, output: dataset.value, issues: dataset.issues }; } const UID = string(), UserSchema = object({ id: UID, nickname: string(), avatar: optional( string()), memo: string() }), UserSchemaOld = object({ bid: UID, nickname: string(), memo: string(), avatar: optional( string()), info: string() }), CombinedSchema = union([ array(UserSchema), record(UID, UserSchemaOld) ]); function applyCustomFontColor(color) { color && document.documentElement.style.setProperty("--custom-font-color", color); } function lintCss(css) { const s = css.trim(); if (!s) return null; let q = null, esc = false, c = 0, br = 0, pr = 0, bk = 0; for (let i = 0; i < s.length; i++) { const ch = s[i], nx = s[i + 1]; if (c > 0) "*" === ch && "/" === nx && (c--, i++); else if (q) { if (esc) { esc = false; continue; } if ("\\" === ch) { esc = true; continue; } ch === q && (q = null); } else if ("/" !== ch || "*" !== nx) if ("'" !== ch && '"' !== ch) { if ("{" === ch ? br++ : "}" === ch ? br-- : "(" === ch ? pr++ : ")" === ch ? pr-- : "[" === ch ? bk++ : "]" === ch && bk--, br < 0) return "多余的 '}'"; if (pr < 0) return "多余的 ')'"; if (bk < 0) return "多余的 ']'"; } else q = ch; else c++, i++; } return c > 0 ? "注释未闭合" : q ? `字符串未闭合:${q}` : br > 0 ? "缺少 '}'" : pr > 0 ? "缺少 ')'" : bk > 0 ? "缺少 ']'" : null; } function detectCssParsingIssue(css, ruleCount) { if (!css.trim()) return null; if (0 !== (ruleCount || 0)) return null; const stripped = css.replace(/\/\*[\s\S]*?\*\//g, "").replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, ""); return /{/.test(stripped) ? "浏览器未解析出任何规则,可能语法错误被忽略" : null; } const themeManager = { isDark: _GM_getValue("isDark", false), init() { this.apply(this.isDark); }, toggle() { this.isDark = !this.isDark, _GM_setValue("isDark", this.isDark), this.apply(this.isDark); }, apply(dark) { document.querySelector("html")?.classList.toggle("memo-container-dark-theme", dark); } }; (async () => { pageInjector || (pageInjector = new PageInjector), function() { if (document.getElementById("bili-memo-container")) return; ((function() { if (Alpine.store("userList")) return; const store = { isOpen: false, users: (() => { const raw = _GM_getValue("biliUsers", []); if (!Array.isArray(raw)) return []; const userMap = new Map(raw.map(u => [ u.id, u ])); return Array.from(userMap.values()); })(), isDark: _GM_getValue("isDark", false), isRefreshing: false, refreshCurrent: 0, refreshTotal: 0, displayMode: _GM_getValue("displayMode", 2), searchQuery: "", isMultiSelect: false, selectedIds: [], get filteredUsers() { const query = this.searchQuery.trim().toLowerCase(); return query ? this.users.filter(user => { const id = String(user.id || ""), nickname = (user.nickname || "").toLowerCase(), memo = (user.memo || "").toLowerCase(); return id.includes(query) || nickname.includes(query) || memo.includes(query); }) : this.users; }, updateUser(id, updates) { const index = this.users.findIndex(user => user.id === id); if (-1 !== index) { const before = this.users[index], nextMemo = void 0 !== updates.memo ? updates.memo.trim() : before.memo, nextNickname = void 0 !== updates.nickname ? updates.nickname : before.nickname; if (nextMemo === before.memo && nextNickname === before.nickname && (void 0 === updates.id || updates.id === before.id)) return; if (void 0 !== updates.memo && "" === nextMemo) return this.users.splice(index, 1), this.saveUsers(), void refreshPageInjection(); this.users[index] = { ...before, ...updates, memo: nextMemo, nickname: nextNickname }, this.saveUsers(), void 0 === updates.memo && void 0 === updates.nickname && void 0 === updates.id || refreshPageInjection(); } }, removeUser(id) { const index = this.users.findIndex(user => user.id === id); -1 !== index && (this.users.splice(index, 1), this.saveUsers()); }, toggleMultiSelect() { this.isMultiSelect = !this.isMultiSelect, this.isMultiSelect || this.clearSelection(); }, toggleSelected(id) { this.selectedIds.includes(id) ? this.selectedIds = this.selectedIds.filter(item => item !== id) : this.selectedIds = [ ...this.selectedIds, id ]; }, isSelected(id) { return this.selectedIds.includes(id); }, clearSelection() { this.selectedIds = []; }, invertSelection(ids) { if (0 === ids.length) return; const current = new Set(this.selectedIds), next = new Set(current); ids.forEach(id => { next.has(id) ? next.delete(id) : next.add(id); }), this.selectedIds = Array.from(next); }, removeSelected() { if (0 === this.selectedIds.length) return; const targets = new Set(this.selectedIds); this.users = this.users.filter(user => !targets.has(user.id)), this.clearSelection(), this.saveUsers(); }, setDisplayMode(mode) { this.displayMode = mode, _GM_setValue("displayMode", mode), refreshPageInjection(); }, saveUsers() { _GM_setValue("biliUsers", this.users); }, async refreshData() { if (this.isRefreshing || 0 === this.users.length) return; this.isRefreshing = true, this.refreshCurrent = 0, this.refreshTotal = this.users.length; const tasks = this.users.map(async user => { try { const newData = await getUserInfo(String(user.id)); if (!newData.nickname) return; const target = this.users.find(u => u.id === user.id); target && (target.nickname = newData.nickname, target.avatar = newData.avatar); } catch (error) { user.id; } finally { this.refreshCurrent++; } }); await Promise.allSettled(tasks), this.saveUsers(), setTimeout(() => { this.isRefreshing = false; }, 1e3); }, exportData() { const exportData = this.users.map(user => ({ id: user.id, nickname: user.nickname, avatar: user.avatar || "", memo: user.memo || "" })), jsonContent = JSON.stringify(exportData, null, 2), blob = new Blob([ jsonContent ], { type: "application/json" }), url = URL.createObjectURL(blob), a = document.createElement("a"); a.href = url, a.download = `bili-user-notes-${ (new Date).toISOString().split("T")[0]}.json`, a.click(), URL.revokeObjectURL(url), alert(`导出成功!\n已导出 ${this.users.length} 个用户的数据`); }, async importData() { const input = document.createElement("input"); input.type = "file", input.accept = "application/json", input.onchange = async () => { const file = input.files?.[0]; if (file) try { const fileContent = await file.text(), parsedData = JSON.parse(fileContent), validation = function(data) { const result = safeParse(CombinedSchema, data); return result.success ? { ok: !0 } : { ok: !1, error: `格式不匹配: ${issues = result.issues, issues.slice(0, 2).map(({path: path, message: message}) => { const pathStr = path?.map(p => p.key).filter(Boolean).join(".") || ""; return pathStr ? `[${pathStr}] ${message}` : message; }).join("; ")}` }; var issues; }(parsedData); if (!validation.ok) return void alert(`导入失败:${validation.error}`); let importedUsers = []; if (Array.isArray(parsedData)) importedUsers = parsedData.map(user => ({ id: user.id || user.bid, nickname: user.nickname || "", avatar: user.avatar || "", memo: user.memo || "" })); else { if ("object" != typeof parsedData) return void alert("导入失败:不支持的数据格式"); importedUsers = Object.values(parsedData).map(user => ({ id: user.id || user.bid, nickname: user.nickname || "", avatar: user.avatar || "", memo: user.memo || "" })); } if (importedUsers = importedUsers.filter(user => user.id && user.nickname), 0 === importedUsers.length) return void alert("导入失败:没有有效的用户数据"); const existingIds = new Set(this.users.map(u => u.id)), newUsers = importedUsers.filter(user => !existingIds.has(user.id)), updatedUsers = importedUsers.filter(user => existingIds.has(user.id)); updatedUsers.forEach(importedUser => { const index = this.users.findIndex(u => u.id === importedUser.id); -1 !== index && this.users.splice(index, 1, importedUser); }), newUsers.length > 0 && this.users.splice(this.users.length, 0, ...newUsers), this.saveUsers(), refreshPageInjection(), alert(`导入成功!\n新增:${newUsers.length} 个用户\n更新:${updatedUsers.length} 个用户`); } catch (error) { alert("导入失败:JSON 格式错误或数据解析失败"); } }, input.click(); } }; Alpine.store("userList", store); }))(), Alpine.data("themeHandler", () => ({ isDark: themeManager.isDark, toggle() { themeManager.toggle(), this.isDark = themeManager.isDark; } })), Alpine.data("toggleBtn", () => ({ get isOpen() { return Alpine.store("userList").isOpen; }, set isOpen(val) { Alpine.store("userList").isOpen = val; }, openText: _GM_getValue("btn_open_text", "UvU"), closeText: _GM_getValue("btn_close_text", "UwU"), edit() { const isOp = this.isOpen, key = isOp ? "btn_open_text" : "btn_close_text", n = prompt("修改文字:", isOp ? this.openText : this.closeText); n?.trim() && (isOp ? this.openText = n.trim() : this.closeText = n.trim(), _GM_setValue(key, n.trim())); } })), themeManager.init(), Alpine.data("userList", () => Alpine.store("userList")); const finalHtml = '\n
\n \n
\n
昵称显示模式:
\n
\n \n
\n\n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
\n
\n
\n \n 导入\n \n \n 多选用户\n 退出多选\n \n \n \n \n \n \n \n \n \n 已选 \n \n
备注列表
\n \n ⚡一键刷新数据\n\n \n \n 正在刷新 (/)\n \n \n \n \n
\n
\n
自定义备注样式(CSS)
\n \n
\n
\n 这里支持完整 CSS 选择器。只影响备注相关元素时,建议用 .bili-memo-tag /\n .editable-textarea / .bili-memo-input\n
\n
\n
\n
\n \n\n \n 没有找到用户\n
\n
\n \n \n\n'.replace("${appName}", "备注管理").replace("${boxTemplate}", '\n \n
\n \n
\n \n \n
\n \n \n
\n
\n \n \n
\n \n \n \n \n \n \n \n\n'), container = document.createElement("div"); container.id = "bili-memo-container", container.innerHTML = finalHtml, document.body.appendChild(container); const initialColor = _GM_getValue("customFontColor", "") || document.documentElement.style.getPropertyValue("--custom-font-color"); applyCustomFontColor(initialColor); const colorInput = container.querySelector(".ghost-color-picker"); colorInput && (colorInput.value = initialColor, colorInput.addEventListener("input", () => { const nextColor = colorInput.value; applyCustomFontColor(nextColor), _GM_setValue("customFontColor", nextColor); })); const storedMemoCss = _GM_getValue("customMemoCss", ""), memoCssStatus = container.querySelector(".panel-custom-css-status"), setCssStatus = message => { if (memoCssStatus) { if (!message) return memoCssStatus.textContent = "", void memoCssStatus.classList.remove("is-visible"); memoCssStatus.textContent = message, memoCssStatus.classList.add("is-visible"); } }, initialApply = setCustomMemoCss(storedMemoCss), initialLint = lintCss(storedMemoCss), initialParseWarn = detectCssParsingIssue(storedMemoCss, initialApply.ruleCount); initialLint ? setCssStatus(`CSS 语法警告:${initialLint}`) : initialApply.ok ? setCssStatus(initialParseWarn ? `CSS 解析警告:${initialParseWarn}` : "") : setCssStatus(`CSS 语法错误:${initialApply.error || "无法解析"}`); const memoCssInput = container.querySelector(".panel-custom-css-input"), colorSetting = container.querySelector(".panel-custom-color-setting"); if (colorSetting && (colorSetting.addEventListener("click", () => { container.classList.remove("advanced-css-open"); }), colorSetting.addEventListener("contextmenu", event => { event.preventDefault(), container.classList.toggle("advanced-css-open"), container.classList.contains("advanced-css-open") && memoCssInput?.focus(); }), colorSetting.addEventListener("auxclick", event => { 1 === event.button && (document.documentElement.style.removeProperty("--custom-font-color"), _GM_setValue("customFontColor", ""), alert("已取消自定义字体颜色")); })), memoCssInput) { let cssTimer; memoCssInput.value = storedMemoCss; const applyNow = () => { const nextCss = memoCssInput.value || "", result = setCustomMemoCss(nextCss), lintResult = lintCss(nextCss), parseWarn = detectCssParsingIssue(nextCss, result.ruleCount); lintResult ? setCssStatus(`CSS 语法警告:${lintResult}`) : result.ok ? setCssStatus(parseWarn ? `CSS 解析警告:${parseWarn}` : "") : setCssStatus(`CSS 语法错误:${result.error || "无法解析"}`), _GM_setValue("customMemoCss", nextCss); }; memoCssInput.addEventListener("input", () => { cssTimer && window.clearTimeout(cssTimer), cssTimer = window.setTimeout(applyNow, 1e3); }), memoCssInput.addEventListener("blur", applyNow); } }(), unsafeWindow.Alpine = Alpine, Alpine.start(); })(); })(Alpine, querySelectorShadowDom);