// ==UserScript== // @name Bilibili UP Notes // @name:zh-CN 哔哩哔哩UP主备注 // @namespace ckylin-script-bilibili-up-notes // @version 0.6.0 // @description A simple script to add notes to Bilibili UPs. // @description:zh-CN 一个可以给哔哩哔哩UP主添加备注的脚本。 // @author CKylinMC // @match https://*.bilibili.com/* // @grant unsafeWindow // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_addStyle // @license Apache-2.0 // @run-at document-end // @icon https://www.bilibili.com/favicon.ico // @require https://update.greasyfork.icu/scripts/564901/1749821/CKUI.js // @downloadURL none // ==/UserScript== (function (unsafeWindow, document) { // #region helpers if (typeof (GM_addStyle) === 'undefined') { unsafeWindow.GM_addStyle = function (css) { const style = document.createElement('style'); style.textContent = css; document.head.appendChild(style); } } const logger = { log(...args) { console.log('[BiliUPNotes]', ...args); }, error(...args) { console.error('[BiliUPNotes]', ...args); }, warn(...args) { console.warn('[BiliUPNotes]', ...args); }, } const pages = { isPlayPage() { return unsafeWindow.location.pathname.startsWith('/video/') || unsafeWindow.location.pathname.startsWith('/list/'); }, isProfilePage() { return unsafeWindow.location.hostname.startsWith('space.bilibili.com'); } } const runtime = { cardtaskId: null, uptaskId: null }; const selectors = { markup: { symbolclass: '.ckupnotes-symbol', idclass: '.ckupnotes-identifier' }, card: { root: 'div.bili-user-profile', avatar: 'picture.b-img__inner>img', avatarLink: 'a.bili-user-profile-view__avatar', infoRoot: 'div.bili-user-profile-view__info', userName: 'a.bili-user-profile-view__info__uname', bodyRoot: 'div.bili-user-profil1e__info__body', signBox: 'div.bili-user-profile-view__info__signature', footerRoot: 'div.bili-user-profile-view__info__footer', button: 'div.bili-user-profile-view__info__button' }, cardModern: { shadowRoot: 'bili-user-profile', readyDom: 'div#view', avatarLink: 'a#avatar', avatar: 'img#face', bodyBox: 'div#body', userNameBox: 'div#title', userName: 'a#name', bodyRoot: 'div#content', signBox: 'div#sign', footerRoot: 'div#action', }, userCard: { root: 'div.usercard-wrap', avatarLink: 'a.face', avatar: 'img.bili-avatar-img', bodyRoot: 'div.info', nameBox: 'div.user', userName: 'a.name', signBox: 'div.sign', footerRoot: 'div.btn-box' }, play: { upInfoBox: 'div.up-info-container', upAvatar: 'img.bili-avatar-img', upAvatarLink: 'a.up-avatar', upDetailBox: 'div.up-detail', upName: 'a.up-name', upDesc: 'div.up-description', upBtnBox: 'div.upinfo-btn-panel', upDetailTopBox: 'div.up-detail-top', subBtn: 'div.follow-btn', videoTitle: '.video-title' }, profile: { sidebarBox: 'div.aside', dynamicSidebarBox: 'div.space-dynamic__right', avatarImg: 'div.avatar div.b-avatar__layer__res>picture>img' } }; class Utils{ static _c(name) { return "ckupnotes-" + name; } static wait(ms = 0) { return new Promise(resolve => setTimeout(resolve, ms)); } static $(selector, root = document) { return root.querySelector(selector); } static $all(selector, root = document) { return Array.from(root.querySelectorAll(selector)); } static $child(parent, selector) { if (typeof parent === 'string') { return document.querySelector(parent+' '+selector); } return parent.querySelector(selector); } static $childAll(parent, selector) { if (typeof parent === 'string') { return Array.from(document.querySelectorAll(parent+' '+selector)); } return Array.from(parent.querySelectorAll(selector)); } static removeTailingSlash(str) { return str.replace(/\/+$/, ''); } static fixUrlProtocol(url) { if (url.startsWith('http://') || url.startsWith('https://')) { return url; } else if (url.startsWith('//')) { return unsafeWindow.location.protocol + url; } else if (url.startsWith('data:')) { return url; } else if (url.startsWith('/')) { return unsafeWindow.location.origin + url; } else { return unsafeWindow.location.origin + Utils.removeTailingSlash(unsafeWindow.location.pathname) + '/' + url; } } static waitForElementFirstAppearForever(selector, root = document) { return new Promise(resolve => { const element = root.querySelector(selector); if (element) { resolve(element); return; } const observer = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (!(node instanceof HTMLElement)) continue; const el = node.matches(selector) ? node : node.querySelector(selector); if (el) { resolve(el); observer.disconnect(); return; } } } }); observer.observe(root, { childList: true, subtree: true }); }); } static waitForElementFirstAppearForeverWithTimeout(selector, root = document, timeout = 5000) { return new Promise(resolve => { const element = root.querySelector(selector); if (element) { resolve(element); return; } let done = false; const observer = new MutationObserver(mutations => { if (done) return; for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (!(node instanceof HTMLElement)) continue; const el = node.matches(selector) ? node : node.querySelector(selector); if (el) { done = true; resolve(el); observer.disconnect(); return; } } } }); observer.observe(root, { childList: true, subtree: true }); if (timeout > 0) { setTimeout(() => { if (done) return; done = true; observer.disconnect(); resolve(null); }, timeout); } }); } static registerOnElementAttrChange(element, attr, callback) { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'attributes' && mutation.attributeName === attr) { callback(mutation); } }); }); observer.observe(element, { attributes: true }); return observer; } static registerOnElementContentChange(element, callback) { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.type === 'characterData') { callback(mutation); } }); }); observer.observe(element, { characterData: true, subtree: true }); return observer; } static registerOnceElementRemoved(element, callback, root = null) { if (!element) return null; if (!element.isConnected) { callback?.(element); return null; } const parent = root || element.parentNode || element.getRootNode?.(); if (!parent) { callback?.(element); return null; } let done = false; const observer = new MutationObserver(mutations => { if (done) return; if (!element.isConnected) { done = true; observer.disconnect(); callback?.(element); return; } }); observer.observe(parent, { childList: true }); return observer; } static formatDate(timestamp) { return (Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false, }).format(new Date(+timestamp))).replace(/\//g, '-').replace(',', ''); } static daysBefore(timestamp) { const target = new Date(+timestamp); const now = Date.now(); const diff = now - target.getTime(); return Math.floor(diff / (1000 * 60 * 60 * 24)); } static get ui() { return unsafeWindow.ckui; } static get currentUid() { if (pages.isProfilePage()) { const match = unsafeWindow.location.pathname.match(/\/space\.bilibili\.com\/(\d+)/); if (match) { return match[1]; } else { const uid = document.querySelector('.vui_icon.sic-fsp-uid_line.icon')?.nextSibling?.textContent || null; return uid; } } // on play page if(pages.isPlayPage()) { const upAvatarLink = Utils.$(selectors.play.upAvatarLink); if (upAvatarLink) { const link = upAvatarLink.getAttribute('href') || ''; const match2 = link.match(/\/space\.bilibili\.com\/(\d+)/); if (match2) { return match2[1]; } } } return null; } static get currentVID() { if (!pages.isPlayPage()) return null; // method referenced Bilibili Evolved if (unsafeWindow.aid || unsafeWindow.bvid) { return 'av'+unsafeWindow.aid || unsafeWindow.bvid; } const selector = '.av-link,.bv-link,.bvid-link'; const avEl = document.querySelector(selector); if (avEl) { const vid = avEl.innerText?.trim?.() || ''; if (vid.toLowerCase().startsWith('av') || vid.toLowerCase().startsWith('bv')) { return vid; } if (vid.match(/^\d+/)) { return 'av' + vid; } } return null; } } // #endregion helpers // #region store-v2 class GMStore { static _serialize(value) { return JSON.stringify({ v: value }); } static _deserialize(value) { if (value === null || typeof value === 'undefined') return null; if (typeof value !== 'string') return value; try { const parsed = JSON.parse(value); if (parsed && Object.prototype.hasOwnProperty.call(parsed, 'v')) { return parsed.v; } return parsed; } catch { return value; } } static get(key, fallback = null) { const raw = GM_getValue(key, null); if (raw === null || typeof raw === 'undefined') return fallback; const val = this._deserialize(raw); return (val === null || typeof val === 'undefined') ? fallback : val; } static set(key, value) { GM_setValue(key, this._serialize(value)); } static delete(key) { GM_deleteValue(key); } static has(key) { return GM_listValues().includes(key); } static list() { return GM_listValues(); } } class Store{ static datastore = GMStore; static settingsstore = GMStore; static setDataStore(storeName) { switch (storeName) { case 'GMStore': this.datastore = GMStore; break; default: throw new Error(`Unknown store: ${storeName}`); } } static set(key, value) { return this.datastore.set(key, value); } static get(key, fallback = null) { return this.datastore.get(key, fallback); } static delete(key) { return this.datastore.delete(key); } static has(key) { return this.datastore.has(key); } static list() { return this.datastore.list(); } static readSettings() { const settings = this.get('settings', {}); return settings; } static readSetting(key, fallback = null) { const settings = this.readSettings(); return (settings && Object.prototype.hasOwnProperty.call(settings, key)) ? settings[key] : fallback; } static setSettings(settings) { return this.set('settings', settings); } static setSetting(key, value) { const settings = this.readSettings() || {}; settings[key] = value; return this.setSettings(settings); } static deleteSetting(key) { const settings = this.readSettings(); if (settings && Object.prototype.hasOwnProperty.call(settings, key)) { delete settings[key]; return this.setSettings(settings); } } static _u(uid) { return (uid ? ((''+uid).trim?.() || uid) : null) } static hasUser(_uid) { const uid = this._u(_uid); if (!uid) return false; return this.has(`u:${uid}`); } static getUser(_uid, fallback = null) { const uid = this._u(_uid); if (!uid) return fallback; return this.get(`u:${uid}`, fallback); } static setUser(_uid, user) { const uid = this._u(_uid); if (!uid) return; return this.set(`u:${uid}`, user); } static delUser(_uid) { const uid = this._u(_uid); if (!uid) return; return this.delete(`u:${uid}`); } static listUsers() { return this.list().filter(key => key.startsWith('u:')).map(key => key.substring(2)); } } class User { uid = ""; uname = ""; uavatar = ""; alias = ""; notes = ""; tags = []; followInfo = null; externalInfo = null; extras = null; static LoadOrCreate(uid) { let user = Store.getUser(uid, null); if (user) { return User.fromJson(user); } else { user = new User(); user.uid = uid; user.save(); return user; } } static fromUID(uid) { const result = Store.getUser(uid, null); if (result) { return User.fromJson(result); } else { return null; } } static fromJson(jsonStr) { try { const obj = JSON.parse(jsonStr); const user = new User(); user.uid = obj.uid || ""; user.uname = obj.uname || ""; user.uavatar = obj.uavatar || ""; user.alias = obj.a || ""; user.notes = obj.n || ""; user.tags = obj.t || []; user.followInfo = obj.f || null; user.externalInfo = obj.s || null; user.extras = obj.e || null; return user; } catch { return null; } } toObj() { return { uid: this.uid, uname: this.uname, uavatar: this.uavatar, a: this.alias, n: this.notes, t: this.tags, f: this.followInfo, s: this.externalInfo, e: this.extras } } toJSON() { return JSON.stringify(this.toObj()); } toString() { return `[UP ${this.uid} - ${this.uname}${this.alias ? ` (${this.alias})` : ''}]`; } save() { return Store.setUser(this.uid, this.toJSON()); } remove() { return Store.delUser(this.uid); } getTags() { return this.tags || []; } setTags(tags) { this.tags = tags || []; } addTag(tag) { if (!this.tags) this.tags = []; if (!this.tags.includes(tag)) { this.tags.push(tag); } } removeTag(tag) { if (!this.tags) return; this.tags = this.tags.filter(t => t !== tag); } setFollowInfo({ timestamp, videoId, videoName, upName }) { this.followInfo = { t: timestamp, vi: videoId, vn: videoName, un: upName } } getFollowInfo() { if (!this.followInfo) return null; return { timestamp: this.followInfo.t, videoId: this.followInfo.vi, videoName: this.followInfo.vn, upName: this.followInfo.un } } removeFollowInfo() { this.followInfo = null; } setExternalInfo({ sourceName, sourceUrl, timestamp }) { this.externalInfo = { s: sourceName, u: sourceUrl, t: timestamp } } getExternalInfo() { if (!this.externalInfo) return null; return { sourceName: this.externalInfo.s, sourceUrl: this.externalInfo.u, timestamp: this.externalInfo.t } } setExtra(key, value) { if (!this.extras) this.extras = {}; this.extras[key] = value; } getExtra(key, fallback = null) { if (!this.extras) return fallback; return (Object.prototype.hasOwnProperty.call(this.extras, key)) ? this.extras[key] : fallback; } refresh() { // refresh data from store return User.fromUID(this.uid).then(user => { if (user) { this.uname = user.uname; this.uavatar = user.uavatar; this.alias = user.alias; this.notes = user.notes; this.tags = user.tags; this.followInfo = user.followInfo; this.externalInfo = user.externalInfo; this.extras = user.extras; } return this; }); } } function migrationCheckV2() { // move from UPNotesManager to UserBeans, check if needed // store is static const keys = Store.list(); let need = false; for (const key of keys) { if (key.startsWith('upalias_') || key.startsWith('upnotes_')) { need = true; break; } } return need; } function doMigrationV2() { const keys = Store.list(); for (const key of keys) { if (key.startsWith('upalias_')) { const uid = key.substring('upalias_'.length); const user = User.LoadOrCreate(uid); user.alias = Store.get(key, ''); user.save(); Store.delete(key); logger.log(`Migrated alias for UID ${uid}`); } else if (key.startsWith('upnotes_')) { const uid = key.substring('upnotes_'.length); const user = User.LoadOrCreate(uid); user.notes = Store.get(key, ''); user.save(); Store.delete(key); logger.log(`Migrated notes for UID ${uid}`); } } } // #endregion store-v2 // #region cores class UPNotesManager { static _u(uid) { return (uid ? ((''+uid).trim?.() || uid) : "not-a-uid") } static getAliasForUID(_uid, fallback = null) { const uid = UPNotesManager._u(_uid); const user = User.fromUID(uid); if (user) { return user.alias || fallback; } else return fallback; } static setAliasForUID(_uid, alias) { const uid = UPNotesManager._u(_uid); const user = User.LoadOrCreate(uid); user.alias = alias; user.save(); } static deleteAliasForUID(_uid) { const uid = UPNotesManager._u(_uid); const user = User.fromUID(uid); if (user) { user.alias = ""; user.save(); } } static getNotesForUID(_uid, fallback = null) { const uid = UPNotesManager._u(_uid); const user = User.fromUID(uid); if (user) { return user.notes || fallback; } else return fallback; } static setNotesForUID(_uid, notes) { const uid = UPNotesManager._u(_uid); const user = User.LoadOrCreate(uid); user.notes = notes; user.save(); } static deleteNotesForUID(_uid) { const uid = UPNotesManager._u(_uid); const user = User.fromUID(uid); if (user) { user.notes = ""; user.save(); } } static callUIForEditing(_uid, _displayName = "?", _avatarUrl = null, closeCallback = null) { const uid = UPNotesManager._u(_uid); const displayName = _displayName?.trim?.() || _displayName; const avatarUrl = _avatarUrl?.trim?.() || _avatarUrl; const user = User.LoadOrCreate(uid); user.uname = displayName || user.uname; user.uavatar = avatarUrl || user.uavatar; const form = Utils.ui.form() .input({ label: 'UP 别名', name: 'alias', placeholder: '请输入 UP 别名', value: user.alias }) .textarea({ label: 'UP 备注', name: 'notes', placeholder: '请输入 UP 备注', value: user.notes }) .tags({ label: '分类标签', name: 'tags', placeholder: '对 UP 进行标签归类', value: user.tags || [], maxTags: 10, validator(tag, tags) { if (tag.length < 1 || tag.length > 20) { return '标签长度应在 1-20 字符之间'; } return true; } }) .checkbox({ label: '勾选并保存以删除关注记录', name: 'deleteFollowInfo', value: false, }) .button({ label: '保存', primary: true, onClick: (values) => { const newAlias = values.alias.trim(); const newNotes = values.notes.trim(); const tags = values.tags || []; const deleteFollowInfo = values.deleteFollowInfo || false; if (deleteFollowInfo) { user.removeFollowInfo(); } user.alias = newAlias; user.notes = newNotes; user.setTags(tags); user.save(); Utils.ui.success('保存成功'); floatWindow.close(); if (closeCallback) { closeCallback(); } } }) .button({ label: '取消', onClick: () => { floatWindow.close(); } }); const floatWindow = Utils.ui.floatWindow({ title: `编辑备注 ${displayName} (UID: ${uid})`, content: form.render(), width: '450px', shadow: true, ...(avatarUrl ? { icon: Utils.fixUrlProtocol(avatarUrl), iconShape: 'circle', iconWidth: '24px', } : {}) }); floatWindow.show(); floatWindow.moveToMouse?.(); } static callUIForRemoving(_uid, _displayName = "", _avatarUrl = null) { const uid = UPNotesManager._u(_uid); const displayName = _displayName?.trim?.() || _displayName; const avatarUrl = _avatarUrl?.trim?.() || _avatarUrl; const user = User.fromUID(uid); if(!user) return Utils.ui.error('未找到该 UP 主的备注信息,无需删除。'); Utils.ui.confirm( `确定要删除 ${displayName} (UID: ${uid}) 的 UP 备注吗?`, '确认删除 UP 备注', null, avatarUrl ? { icon: Utils.fixUrlProtocol(avatarUrl), iconShape: 'circle', iconWidth: '24px', } : {} ).then(res => { if (res) { user.remove(); Utils.ui.success('删除成功'); } }); } } // #endregion cores // #region integrations class FoManPlugin_Provider{ static hasAlias(uid) { return UPNotesManager.getAliasForUID(uid, null) !== null; } static getAlias(uid, fallback = null) { return UPNotesManager.getAliasForUID(uid, fallback); } static setAlias(uid, alias) { UPNotesManager.setAliasForUID(uid, alias); } static removeAlias(uid) { UPNotesManager.deleteAliasForUID(uid); } } class FoManPlugin_Actions{ static async setFor(uid, displayName = null) { UPNotesManager.callUIForEditing(uid, displayName); } static async removeFor(uid, displayName = null) { UPNotesManager.callUIForRemoving(uid, displayName); } } // #endregion integrations // #region onAnyPage function injectCssOnAnyPage() { GM_addStyle(` .ckupnotes-usercard-btn{ border: 1px solid var(--text3); color: var(--text2); background-color: transparent; } .ckupnotes-usercard-btn:hover{ color: var(--brand_blue); border-color: var(--brand_blue); } .ckupnotes-tagrow{ margin-top: 4px; } .ckupnotes-tag{ display: inline-block; padding: 2px 6px; margin-right: 4px; background-color: var(--bg2); color: var(--text2); border-radius: 4px; font-size: 12px; } `); } function tagRowMaker(tags) { const row = document.createElement('div'); row.classList.add('ckupnotes-tagrow', selectors.markup.idclass.replace(".", "")); tags.forEach(tag => { const tagEl = document.createElement('div'); tagEl.classList.add('ckupnotes-tag'); tagEl.textContent = tag; row.appendChild(tagEl); }); return row; } function followInfoBlockMaker(user) { const followInfo = user.getFollowInfo(); if (!followInfo) return null; const block = document.createElement('div'); block.classList.add('ckupnotes-followinfo', selectors.markup.idclass.replace(".", "")); block.textContent = `关注于 `; const dateSpan = document.createElement('span'); dateSpan.innerText = Utils.formatDate(followInfo.timestamp); dateSpan.title = Utils.daysBefore(followInfo.timestamp) + '天前'; block.appendChild(dateSpan); const vidLink = document.createElement('a'); vidLink.href=`https://www.bilibili.com/video/${followInfo.videoId}`; vidLink.target = '_blank'; vidLink.textContent = `《${followInfo.videoName ||'未知'}》`; block.appendChild(vidLink); if(user.uname && followInfo.upName && user.uname !== followInfo.upName) { block.textContent += `(UP:${followInfo.upName})`; } return block; } function externalInfoBlockMaker(user) { const externalInfo = user.getExternalInfo(); if (!externalInfo) return null; const block = document.createElement('div'); block.classList.add('ckupnotes-externalinfo', selectors.markup.idclass.replace(".", "")); block.textContent = `信息来自 ${externalInfo.sourceName} 于 ${Utils.formatDate(externalInfo.timestamp)}`; if (externalInfo.sourceUrl) { const link = document.createElement('a'); link.href = Utils.fixUrlProtocol(externalInfo.sourceUrl); link.target = '_blank'; link.style.marginLeft = '8px'; link.textContent = '[查看来源]'; block.appendChild(link); } return block; } function registerOnAnyPage() { logger.log('Registering UP Card observer on any page...'); injectCssOnAnyPage(); Utils.waitForElementFirstAppearForever(selectors.card.root).then(onFirstCardShown); Utils.waitForElementFirstAppearForever(selectors.cardModern.shadowRoot).then(onFirstModernCardShown); Utils.waitForElementFirstAppearForever(selectors.userCard.root).then(onFirstUserCardShown); } function onFirstCardShown(cardElement) { logger.log('First UP Card note appeared.'); onCardShown(cardElement); Utils.registerOnElementAttrChange( cardElement, 'style', () => { if (!cardElement.style.display || cardElement.style.display !== 'none') { onCardShown(cardElement); } } ); } function onFirstModernCardShown(cardElement) { logger.log('First Modern UP Card note appeared.'); Utils.registerOnElementAttrChange(cardElement, 'style', () => { if (!cardElement.style.display || cardElement.style.display !== 'none') { onModernCardShown(); } }); } function onFirstUserCardShown(cardElement) { logger.log('First User Card note appeared.'); Utils.registerOnElementAttrChange(cardElement, 'style', () => { if (!cardElement.style.display || cardElement.style.display !== 'none') { onUserCardShown(); } }); } async function onCardShown() { const thisCardTaskId = (''+Date.now()) + Math.random(); try { runtime.cardtaskId = thisCardTaskId; const cardElement = Utils.$(selectors.card.root); const cardBody = Utils.$child(cardElement, selectors.card.bodyRoot); if (!cardBody) { return; } await Utils.wait(150); // 等待内容加载 const els = Utils.$childAll(cardElement, selectors.markup.idclass); els.forEach(element => { element.remove(); }); if(runtime.cardtaskId !== thisCardTaskId) { logger.log('A newer card task has started, aborting this one.(note)'); return; } const avatarLinkEl = Utils.$child(cardElement, selectors.card.avatarLink); const link = avatarLinkEl?.getAttribute('href') || ''; // value = `//space.bilibili.com/652239032/dynamic` // extract UID const match = link.match(/\/space\.bilibili\.com\/(\d+)/); if (!match) return logger.log('UID not found in avatar link, aborting.(note)'); const uid = match[1]; logger.log(`Extracted UID: ${uid} (note)`); const user = User.fromUID(uid) || {}; let alias = user.alias || ''; let notes = user.notes || ''; logger.log(`UP Card Shown - UID: ${uid}, Alias: ${alias}, Notes: ${notes}`); const userNameEl = Utils.$child(cardElement, selectors.card.userName); const username = userNameEl.textContent || ''; if (alias) { const span = document.createElement('span'); span.classList.add(selectors.markup.symbolclass.replace(".", ""), selectors.markup.idclass.replace(".", "")); span.textContent = ` (${alias})`; userNameEl.appendChild(span); } else { logger.log('No alias found.(note)'); } const bodyRootEl = Utils.$child(cardElement, selectors.card.bodyRoot); if (notes) { const notesEl = document.createElement('div'); notesEl.classList.add(selectors.card.signBox.replace("div.", ""), selectors.markup.idclass.replace(".", "")); notesEl.style.marginTop = '4px'; notesEl.style.fontStyle = 'italic'; notesEl.textContent = notes; bodyRootEl.appendChild(notesEl); logger.log('Notes added to UP Card.(note)'); } else { logger.log('No notes found.(note)'); } if (user.tags && user.tags.length > 0) { const tagRow = tagRowMaker(user.tags); bodyRootEl.appendChild(tagRow); logger.log('Tags added to UP Card.(note)'); } if (user.followInfo) { const followInfoBlock = followInfoBlockMaker(user); if (followInfoBlock) { bodyRootEl.appendChild(followInfoBlock); logger.log('Follow info added to UP Card.(note)'); } } if (user.externalInfo) { const externalInfoBlock = externalInfoBlockMaker(user); if (externalInfoBlock) { bodyRootEl.appendChild(externalInfoBlock); logger.log('External info added to UP Card.(note)'); } } const footerRootEl = Utils.$child(cardElement, selectors.card.footerRoot); if (footerRootEl) { const btn = document.createElement('div'); btn.classList.add(selectors.card.button.replace("div.", ""), selectors.markup.idclass.replace(".", ""), 'ckupnotes-usercard-btn'); btn.textContent = '编辑备注'; btn.style.cursor = 'pointer'; btn.style.marginLeft = '8px'; footerRootEl.appendChild(btn); btn.addEventListener('click', () => { const avatarEl = Utils.$child(cardElement, selectors.card.avatar); const avatarImgSrc = avatarEl?.getAttribute('src') || null; UPNotesManager.callUIForEditing(uid, username, avatarImgSrc); }); } } finally { if(runtime.cardtaskId === thisCardTaskId) runtime.cardtaskId = null; } } async function onModernCardShown() { const cardElement = Utils.$(selectors.cardModern.shadowRoot); if (!cardElement) return; const shadowroot = cardElement.shadowRoot; if (!shadowroot) return; const thisCardTaskId = ('' + Date.now()) + Math.random(); try { runtime.cardtaskId = thisCardTaskId; await Utils.waitForElementFirstAppearForever(selectors.cardModern.readyDom, shadowroot, 2000); if (runtime.cardtaskId !== thisCardTaskId) { logger.log('A newer card task has started, aborting this one.(modern)'); return; } const els = Utils.$childAll(shadowroot, selectors.markup.idclass); els.forEach(element => { element.remove(); }); const avatarLinkEl = Utils.$child(shadowroot, selectors.cardModern.avatarLink); const link = avatarLinkEl?.getAttribute('href') || ''; const match = link.match(/\/space\.bilibili\.com\/(\d+)/); if (!match) return logger.log('UID not found in avatar link, aborting.(modern)'); const uid = match[1]; logger.log(`Extracted UID: ${uid} (modern)`); const user = User.fromUID(uid) || {}; let alias = user.alias || ''; let notes = user.notes || ''; let followInfo = user.followInfo || null; let externalInfo = user.externalInfo || null; logger.log(`Modern UP Card Shown - UID: ${uid}, Alias: ${alias}, Notes: ${notes}`); const userNameEl = Utils.$child(shadowroot, selectors.cardModern.userName); const username = userNameEl?.textContent || ''; if (alias) { const span = document.createElement('span'); span.classList.add(selectors.markup.symbolclass.replace(".", ""), selectors.markup.idclass.replace(".", "")); span.textContent = ` (${alias})`; userNameEl.appendChild(span); } else { logger.log('No alias found.(modern)'); } const bodyRootEl = Utils.$child(shadowroot, selectors.cardModern.bodyRoot); if (notes) { const notesEl = document.createElement('div'); notesEl.classList.add(selectors.cardModern.signBox.replace("div.", ""), selectors.markup.idclass.replace(".", "")); notesEl.style.marginTop = '4px'; notesEl.style.fontStyle = 'italic'; notesEl.textContent = notes; bodyRootEl.appendChild(notesEl); logger.log('Notes added to Modern UP Card.(modern)'); } else { logger.log('No notes found.(modern)'); } if(user.tags && user.tags.length > 0) { const tagRow = tagRowMaker(user.tags); bodyRootEl.appendChild(tagRow); logger.log('Tags added to Modern UP Card.(modern)'); } if (followInfo) { const followInfoBlock = followInfoBlockMaker(user); if (followInfoBlock) { bodyRootEl.appendChild(followInfoBlock); logger.log('Follow info added to Modern UP Card.(modern)'); } } if (externalInfo) { const externalInfoBlock = externalInfoBlockMaker(user); if (externalInfoBlock) { bodyRootEl.appendChild(externalInfoBlock); logger.log('External info added to Modern UP Card.(modern)'); } } const footerRootEl = Utils.$child(shadowroot, selectors.cardModern.footerRoot); if (footerRootEl) { const btn = document.createElement('button'); btn.classList.add(selectors.markup.idclass.replace(".", ""), 'ckupnotes-usercard-btn'); btn.textContent = '编辑备注'; btn.style.cursor = 'pointer'; btn.style.marginLeft = '8px'; footerRootEl.appendChild(btn); btn.addEventListener('click', () => { const avatarEl = Utils.$child(shadowroot, selectors.cardModern.avatar); const avatarImgSrc = avatarEl?.getAttribute('src') || null; UPNotesManager.callUIForEditing(uid, username, avatarImgSrc); }); } // inject custom styles into shadowdom const styleEl = document.createElement('style'); styleEl.textContent = ` .ckupnotes-usercard-btn{ border: 1px solid var(--text3); color: var(--text2); background-color: transparent; } .ckupnotes-usercard-btn:hover{ color: var(--brand_blue); border-color: var(--brand_blue); } .ckupnotes-tagrow{ margin-top: 4px; } .ckupnotes-tag{ display: inline-block; padding: 2px 6px; margin-right: 4px; background-color: var(--bg2); color: var(--text2); border-radius: 4px; font-size: 12px; } `; styleEl.classList.add(selectors.markup.idclass.replace(".", "")); shadowroot.appendChild(styleEl); } finally { if (runtime.cardtaskId === thisCardTaskId) runtime.cardtaskId = null; } } async function onUserCardShown() { const cardElement = Utils.$(selectors.userCard.root); if (!cardElement) return; const thisCardTaskId = ('' + Date.now()) + Math.random(); try { runtime.cardtaskId = thisCardTaskId; await Utils.wait(300); // wait for content load if (runtime.cardtaskId !== thisCardTaskId) { logger.log('A newer card task has started, aborting this one.(usercard)'); return; } const els = Utils.$childAll(cardElement, selectors.markup.idclass); els.forEach(element => { element.remove(); }); logger.log('Processing User Card...(usercard)'); const userNameLink = Utils.$child(cardElement, selectors.userCard.userName); const link = userNameLink?.getAttribute('href') || ''; const match = link.match(/\/space\.bilibili\.com\/(\d+)/); if (!match) return logger.log('UID not found in avatar link, aborting.(usercard)'); const uid = match[1]; logger.log(`Extracted UID: ${uid} (usercard)`); const user = User.fromUID(uid) || {}; let alias = user.alias || ''; let notes = user.notes || ''; let followInfo = user.followInfo || null; let externalInfo = user.externalInfo || null; logger.log(`User Card Shown - UID: ${uid}, Alias: ${alias}, Notes: ${notes}`); const userNameEl = Utils.$child(cardElement, selectors.userCard.userName); const displayName = userNameEl?.textContent || ''; if (alias) { const span = document.createElement('span'); span.classList.add(selectors.markup.symbolclass.replace(".", ""), selectors.markup.idclass.replace(".", "")); span.textContent = ` (${alias})`; userNameEl.appendChild(span); } else { logger.log('No alias found.(usercard)'); } const bodyRootEl = Utils.$child(cardElement, selectors.userCard.bodyRoot); if (notes) { const notesEl = document.createElement('div'); notesEl.classList.add(selectors.userCard.signBox.replace("div.", ""), selectors.markup.idclass.replace(".", "")); notesEl.style.marginTop = '4px'; notesEl.style.fontStyle = 'italic'; notesEl.textContent = notes; bodyRootEl.appendChild(notesEl); logger.log('Notes added to User Card.(usercard)'); } else { logger.log('No notes found.(usercard)'); } if(user.tags && user.tags.length > 0) { const tagRow = tagRowMaker(user.tags); bodyRootEl.appendChild(tagRow); logger.log('Tags added to User Card.(usercard)'); } if (followInfo) { const followInfoBlock = followInfoBlockMaker(user); if (followInfoBlock) { bodyRootEl.appendChild(followInfoBlock); logger.log('Follow info added to User Card.(usercard)'); } } if (externalInfo) { const externalInfoBlock = externalInfoBlockMaker(user); if (externalInfoBlock) { bodyRootEl.appendChild(externalInfoBlock); logger.log('External info added to User Card.(usercard)'); } } const footerRootEl = Utils.$child(cardElement, selectors.userCard.footerRoot); if (footerRootEl) { const btn = document.createElement('div'); btn.classList.add('ckupnotes-usercard-btn', selectors.markup.idclass.replace(".", "")); btn.textContent = '备注'; btn.style.cursor = 'pointer'; btn.style.padding = '5px 6px'; btn.style.borderRadius = '4px'; btn.style.flex = '1'; btn.style.textAlign = 'center'; footerRootEl.appendChild(btn); btn.addEventListener('click', () => { const avatarEl = Utils.$child(cardElement, selectors.userCard.avatar); const avatarImgSrc = avatarEl?.getAttribute('src') || null; UPNotesManager.callUIForEditing(uid, displayName, avatarImgSrc); }); } } finally { if (runtime.cardtaskId === thisCardTaskId) runtime.cardtaskId = null; } } // #endregion onAnyPage // #region playpage function injectCssOnPlayPage() { GM_addStyle(` .ckupnotes-play-up-btn { margin-left: 2px; color: var(--text2); font-size: 13px; transition: color .3s; flex-shrink: 0; } .ckupnotes-play-up-btn:hover { color: var(--brand_blue); } `); } function registerOnPlayPage() { logger.log('Registering UP Info Box observer on play page...'); injectCssOnPlayPage(); Utils.waitForElementFirstAppearForever(selectors.play.upInfoBox).then(onFirstTimeUpInfoBoxShown); } function onFirstTimeUpInfoBoxShown() { logger.log('First UP Info Box appeared on play page.'); onUpInfoBoxShown(); Utils.registerOnElementContentChange( Utils.$(selectors.play.upInfoBox), () => { onUpInfoBoxShown(); } ); } async function onUpInfoBoxShown() { logger.log('UP Info Box shown on play page.'); const thisUpTaskId = ('' + Date.now()) + Math.random(); try { runtime.uptaskId = thisUpTaskId; await Utils.wait(500); // wait for content load if (runtime.uptaskId !== thisUpTaskId) { logger.log('A newer UP task has started, aborting this one.(play)'); return; } const upInfoBox = Utils.$(selectors.play.upInfoBox); const els = Utils.$all(selectors.markup.idclass, upInfoBox); els.forEach(element => { element.remove(); }); const upAvatarLinkEl = Utils.$(selectors.play.upAvatarLink, upInfoBox); const link = upAvatarLinkEl?.getAttribute('href') || ''; const match = link.match(/\/space\.bilibili\.com\/(\d+)/); if (!match) return logger.log('UID not found in avatar link, aborting.(play)'); const uid = match[1]; logger.log(`Extracted UID: ${uid} (play)`); const user = User.fromUID(uid) || {}; let alias = user.alias || ''; let notes = user.notes || ''; logger.log(`UP Info Box Shown - UID: ${uid}, Alias: ${alias}, Notes: ${notes}`); const upNameEl = Utils.$(selectors.play.upName, upInfoBox); const username = upNameEl.textContent || ''; if (alias) { const span = document.createElement('span'); span.classList.add(selectors.markup.symbolclass.replace(".", ""), selectors.markup.idclass.replace(".", "")); span.textContent = ` (${alias})`; upNameEl.appendChild(span); } else { logger.log('No alias found.(play)'); } const upDescEl = Utils.$(selectors.play.upDesc, upInfoBox); if (notes) { const notesEl = document.createElement('div'); notesEl.classList.add(selectors.markup.symbolclass.replace(".", ""), selectors.markup.idclass.replace(".", "")); notesEl.style.marginTop = '4px'; notesEl.style.fontStyle = 'italic'; notesEl.textContent = notes; upDescEl.appendChild(notesEl); logger.log('Notes added to UP Info Box.(play)'); } else { logger.log('No notes found.(play)'); } const upDetailTopBoxEl = Utils.$(selectors.play.upDetailTopBox, upInfoBox); if (upDetailTopBoxEl) { const btn = document.createElement('div'); btn.classList.add('ckupnotes-play-up-btn', selectors.markup.idclass.replace(".", "")); btn.textContent = '编辑备注'; btn.style.cursor = 'pointer'; btn.style.marginLeft = '8px'; upDetailTopBoxEl.appendChild(btn); btn.addEventListener('click', () => { const upAvatarImgEl = Utils.$(selectors.play.upAvatarImg, upInfoBox); const avatarImgSrc = upAvatarImgEl?.getAttribute('src') || null; UPNotesManager.callUIForEditing(uid, username, avatarImgSrc, ()=>onUpInfoBoxShown()); }); } const subButton = Utils.$(selectors.play.subBtn, upInfoBox); if (subButton) { logger.log('Registering follow/unfollow button listener on play page.'); subButton.removeEventListener('click', onSubBtn); subButton.addEventListener('click', onSubBtn); } else { logger.log('Follow/unfollow button not found, cannot register listener.(play)'); } if (!Utils.$(".ckupnote-upinfo-probe", upInfoBox)) { logger.log('Creating probe element for UP Info Box reset detection.(play)'); const probe = document.createElement('span'); probe.style.display = 'none'; probe.classList.add("ckupnote-upinfo-probe"); upInfoBox.appendChild(probe); if(!Utils.registerOnceElementRemoved(probe, () => { logger.log('Element reset, re-triggering up info box processing.(play)'); Utils.wait(500).then(() => onUpInfoBoxShown()); }, document.body)) { logger.log('Probe create failed: element already been removed.(play)'); } else logger.log('Probe created', probe); } else { logger.log('Probe element already exists, no need to create.(play)'); } } catch (e) { logger.error('Error occurred while processing UP Info Box on play page:', e); } finally { if (runtime.uptaskId === thisUpTaskId) runtime.uptaskId = null; } } async function onSubBtn(event) { logger.log('Follow/Unfollow button clicked on play page.'); await Utils.wait(500); try { const upInfoBox = Utils.$(selectors.play.upInfoBox); const upAvatarLinkEl = Utils.$(selectors.play.upAvatarLink, upInfoBox); const link = upAvatarLinkEl?.getAttribute('href') || ''; const match = link.match(/\/space\.bilibili\.com\/(\d+)/); if (!match) return logger.log('UID not found in avatar link, aborting.(play)'); const uid = match[1]; logger.log(`Extracted UID: ${uid} (play)`); const user = User.fromUID(uid) || {}; let notes = user.notes || ''; const upNameEl = Utils.$(selectors.play.upName, upInfoBox); let username = upNameEl.textContent || '?'; username = username?.trim?.() || username; user.uname = username; const vidNameEl = Utils.$(selectors.play.videoTitle); let vidName = vidNameEl?.textContent || '?'; vidName = vidName?.trim?.() || vidName; // const formatedDate = (Intl.DateTimeFormat('zh-CN', { // year: 'numeric', // month: '2-digit', // day: '2-digit', // hour: '2-digit', // minute: '2-digit', // hour12: false, // }).format(new Date())).replace(/\//g, '-').replace(',', ''); const subBtn = Utils.$(selectors.play.subBtn, upInfoBox); if (subBtn) { logger.log('Processing follow/unfollow action on play page.'); if (subBtn.classList.contains('following')) { // just followed // UPNotesManager.setNotesForUID(uid, // (notes ? notes + '\n' : '') + `[${formatedDate}] 在《${vidName}》关注了 "${username}"` // ); user.setFollowInfo({ timestamp: "" + (+new Date()), videoName: vidName, videoId: Utils.currentVID || '', upName: username, }); user.save(); Utils.ui?.success(`关注操作已记录到 ${username} 的备注`); } else if (subBtn.classList.contains('not-follow')) { // just unfollowed // not supported } else { logger.log('Follow button state unrecognized, no action taken.(play)'); } } } finally { } } // #endregion playpage // #region userprofilepage function injectCssOnUserProfilePage() { GM_addStyle(` .ckupnotes-profile-aside-card { background-color: var(--bg2); border-radius: 6px; width: 100%; padding: 20px 16px 24px; } .ckupnotes-profile-aside-card-line{ margin: 4px 0; } .ckupnotes-profile-aside-card-button{ width: 100%; margin-top: 12px; padding: 4px 0; border: 1px solid var(--text3); color: var(--text2); background-color: transparent; cursor: pointer; border-radius: 4px; } .ckupnotes-profile-aside-card-button:hover{ color: var(--brand_blue); border-color: var(--brand_blue); } `); } function registerOnUserProfilePage() { logger.log('Registering User Profile Page observer...'); injectCssOnUserProfilePage(); Utils.waitForElementFirstAppearForever(selectors.profile.sidebarBox).then(injectOnSidebarBox); Utils.waitForElementFirstAppearForever(selectors.profile.dynamicSidebarBox).then(injectOnDynamicSidebarBox); } async function injectOnSidebarBox(sidebarBox) { logger.log('User Profile Page sidebar box appeared.'); await Utils.wait(200); // wait for content load const uid = Utils.currentUid; if (!uid) { logger.warn('Cannot extract UID on profile page, aborting.'); return; } const user = User.fromUID(uid) || {}; const alias = user.alias || ''; const notes = user.notes || ''; const followInfo = user.followInfo || null; const externalInfo = user.externalInfo || null; const username = Utils.$('div.nickname')?.textContent || ''; const existingCard = Utils.$('.ckupnotes-profile-aside-card', sidebarBox); if (existingCard) { existingCard.remove(); } const card = document.createElement('div'); card.classList.add('ckupnotes-profile-aside-card'); const title = document.createElement('div'); title.textContent = 'UP 备注信息'; title.style.fontSize = '16px'; title.style.fontWeight = 'bold'; card.appendChild(title); const aliasLine = document.createElement('div'); aliasLine.classList.add('ckupnotes-profile-aside-card-line'); aliasLine.textContent = `别名: ${alias || '无'}`; card.appendChild(aliasLine); const notesLine = document.createElement('div'); notesLine.classList.add('ckupnotes-profile-aside-card-line'); notesLine.textContent = `备注: ${notes || '无'}`; card.appendChild(notesLine); if (user.tags && user.tags.length > 0) { const tagRow = tagRowMaker(user.tags); card.appendChild(tagRow); } if (followInfo) { const followInfoBlock = followInfoBlockMaker(user); if (followInfoBlock) { card.appendChild(followInfoBlock); } } if (externalInfo) { const externalInfoBlock = externalInfoBlockMaker(user); if (externalInfoBlock) { card.appendChild(externalInfoBlock); } } const editButton = document.createElement('button'); editButton.classList.add('ckupnotes-profile-aside-card-button'); editButton.textContent = '编辑备注'; editButton.addEventListener('click', () => { const avatarImgSrc = Utils.$(selectors.profile.avatarImg, sidebarBox)?.getAttribute('src') || ''; UPNotesManager.callUIForEditing(uid, username, avatarImgSrc, ()=>injectOnSidebarBox(sidebarBox)); }); card.appendChild(editButton); const wrap = document.createElement('div'); wrap.classList.add('home-aside-section'); wrap.appendChild(card); sidebarBox.prepend(wrap); } async function injectOnDynamicSidebarBox(sidebarBox) { logger.log('User Profile Page sidebar box appeared.'); await Utils.wait(200); // wait for content load const uid = Utils.currentUid; if (!uid) { logger.warn('Cannot extract UID on profile page, aborting.'); return; } const user = User.fromUID(uid) || {}; const alias = user.alias || ''; const notes = user.notes || ''; const followInfo = user.followInfo || null; const externalInfo = user.externalInfo || null; const username = Utils.$('div.nickname')?.textContent || ''; const existingCard = Utils.$('.ckupnotes-profile-aside-card', sidebarBox); if (existingCard) { existingCard.remove(); } const card = document.createElement('div'); card.classList.add('ckupnotes-profile-aside-card'); const title = document.createElement('div'); title.textContent = 'UP 备注信息'; title.style.fontSize = '16px'; title.style.fontWeight = 'bold'; card.appendChild(title); const aliasLine = document.createElement('div'); aliasLine.classList.add('ckupnotes-profile-aside-card-line'); aliasLine.textContent = `别名: ${alias || '无'}`; card.appendChild(aliasLine); const notesLine = document.createElement('div'); notesLine.classList.add('ckupnotes-profile-aside-card-line'); notesLine.textContent = `备注: ${notes || '无'}`; card.appendChild(notesLine); if (user.tags && user.tags.length > 0) { const tagRow = tagRowMaker(user.tags); card.appendChild(tagRow); } if (followInfo) { const followInfoBlock = followInfoBlockMaker(user); if (followInfoBlock) { card.appendChild(followInfoBlock); } } if (externalInfo) { const externalInfoBlock = externalInfoBlockMaker(user); if (externalInfoBlock) { card.appendChild(externalInfoBlock); } } const editButton = document.createElement('button'); editButton.classList.add('ckupnotes-profile-aside-card-button'); editButton.textContent = '编辑备注'; editButton.addEventListener('click', () => { const avatarImgSrc = Utils.$(selectors.profile.avatarImg, sidebarBox)?.getAttribute('src') || ''; UPNotesManager.callUIForEditing(uid, username, avatarImgSrc, ()=>injectOnDynamicSidebarBox(sidebarBox)); }); card.appendChild(editButton); const wrap = document.createElement('div'); wrap.classList.add('dynamic-aside-section'); wrap.appendChild(card); sidebarBox.prepend(wrap); } // #endregion userprofilepage // #region init function migrationCheckAndMigrate() { logger.log('Checking for old data to migrate...'); if (migrationCheckV2()) { logger.log('Old data detected, starting migration to new format (v2)...'); Utils.ui?.info('检测到旧版数据,正在进行数据迁移,请稍候...'); doMigrationV2(); Utils.ui?.success('迁移成功!'); } } function init() { logger.log('Initializing Bilibili UP Notes script...'); migrationCheckAndMigrate(); // 注册任意页面事件 registerOnAnyPage(); // 注册播放页面事件 if (pages.isPlayPage()) { registerOnPlayPage(); } // 注册个人主页事件 if (pages.isProfilePage()) { registerOnUserProfilePage(); } try { if(typeof(unsafeWindow.FoManPlugins) === 'undefined') { unsafeWindow.FoManPlugins = {}; } unsafeWindow.FoManPlugins.UpAlias = { provider: FoManPlugin_Provider, actions: FoManPlugin_Actions } }catch(e) { logger.error('Failed to register as FoMan plugin:', e); } Utils.ui?.trackMouseEvent?.(); logger.log('Bilibili UP Notes script initialized.'); } init(); // #endregion init }) (unsafeWindow,document);