Bilibili UP Notes

A simple script to add notes to Bilibili UPs.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Bilibili UP Notes
// @name:zh-CN   哔哩哔哩UP主备注
// @namespace    ckylin-script-bilibili-up-notes
// @version      v0.4
// @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.org/scripts/564901/1747775/CKUI.js
// ==/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',
            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'
        },
        profile: {
            sidebarBox: 'div.aside',
            dynamicSidebarBox: 'div.space-dynamic__right'
        }
    };
    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 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 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 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;
        }
    }

    // #endregion helpers
    
    // #region cores
    class UPNotesManager {
        static getAliasForUID(uid, fallback = null) {
            return GM_getValue(`upalias_${uid}`, fallback);
        }

        static setAliasForUID(uid, alias) {
            GM_setValue(`upalias_${uid}`, alias);
        }
        
        static deleteAliasForUID(uid) {
            GM_deleteValue(`upalias_${uid}`);
        }

        static getNotesForUID(uid, fallback = null) {
            return GM_getValue(`upnotes_${uid}`, fallback);
        }

        static setNotesForUID(uid, notes) {
            GM_setValue(`upnotes_${uid}`, notes);
        }
        
        static deleteNotesForUID(uid) {
            GM_deleteValue(`upnotes_${uid}`);
        }

        static callUIForEditing(uid, displayName="", closeCallback = null) {
            const currentAlias = this.getAliasForUID(uid) || '';
            const currentNotes = this.getNotesForUID(uid) || '';
            
            const form = Utils.ui.form()
                .input({ 
                    label: 'UP 别名', 
                    name: 'alias', 
                    placeholder: '请输入 UP 别名', 
                    value: currentAlias 
                })
                .textarea({ 
                    label: 'UP 备注', 
                    name: 'notes', 
                    placeholder: '请输入 UP 备注', 
                    value: currentNotes 
                })
                .button({ 
                    label: '保存', 
                    primary: true,
                    onClick: (values) => {
                        const newAlias = values.alias.trim();
                        const newNotes = values.notes.trim();
                        if (newAlias) {
                            this.setAliasForUID(uid, newAlias);
                        } else {
                            this.deleteAliasForUID(uid);
                        }
                        if (newNotes) {
                            this.setNotesForUID(uid, newNotes);
                        } else {
                            this.deleteNotesForUID(uid);
                        }
                        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
            });
            
            floatWindow.show();
            floatWindow.moveToMouse?.();
        }

        static callUIForRemoving(uid, displayName="") {
            Utils.ui.confirm(
                `确定要删除 ${displayName} (UID: ${uid}) 的 UP 备注吗?`,'确认删除 UP 备注'
            ).then(res => {
                if (res) {
                    this.deleteAliasForUID(uid);
                    this.deleteNotesForUID(uid);
                    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);
            }
            `);
    }

    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)`);
            let alias = UPNotesManager.getAliasForUID(uid) || '';
            let notes = UPNotesManager.getNotesForUID(uid) || '';

            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)');
            }

            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(".", ""));
                btn.textContent = '编辑备注';
                btn.style.cursor = 'pointer';
                btn.style.marginLeft = '8px';
                footerRootEl.appendChild(btn);
                btn.addEventListener('click', () => {
                    UPNotesManager.callUIForEditing(uid, username);
                });
            }
        } 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.wait(150); // wait for content load

            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)`);

            let alias = UPNotesManager.getAliasForUID(uid) || '';
            let notes = UPNotesManager.getNotesForUID(uid) || '';

            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)');
            }

            const footerRootEl = Utils.$child(shadowroot, selectors.cardModern.footerRoot);
            if (footerRootEl) {
                const btn = document.createElement('button');
                btn.classList.add(selectors.markup.idclass.replace(".", ""));
                btn.textContent = '编辑备注';
                btn.style.cursor = 'pointer';
                btn.style.marginLeft = '8px';
                footerRootEl.appendChild(btn);
                btn.addEventListener('click', () => {
                    UPNotesManager.callUIForEditing(uid, username);
                });
            }
            
        } 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)\n', cardElement.innerHTML);
            
            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)`);
            let alias = UPNotesManager.getAliasForUID(uid) || '';
            let notes = UPNotesManager.getNotesForUID(uid) || '';
            
            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)');
            }

            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 = '3px 6px';
                btn.style.borderRadius = '3px';
                footerRootEl.appendChild(btn);
                btn.addEventListener('click', () => {
                    UPNotesManager.callUIForEditing(uid, displayName);
                });
            }
        } 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(1000); // 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)`);
            let alias = UPNotesManager.getAliasForUID(uid) || '';
            let notes = UPNotesManager.getNotesForUID(uid) || '';

            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', () => {
                    UPNotesManager.callUIForEditing(uid, username, ()=>onUpInfoBoxShown());
                });
            }
            
        } finally {
            if (runtime.uptaskId === thisUpTaskId) runtime.uptaskId = null;
        }
    }

    // #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;
            }
            .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 alias = UPNotesManager.getAliasForUID(uid) || '';
        const notes = UPNotesManager.getNotesForUID(uid) || '';
        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);

        const editButton = document.createElement('button');
        editButton.classList.add('ckupnotes-profile-aside-card-button');
        editButton.textContent = '编辑备注';
        editButton.addEventListener('click', () => {
            UPNotesManager.callUIForEditing(uid, username, ()=>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 alias = UPNotesManager.getAliasForUID(uid) || '';
        const notes = UPNotesManager.getNotesForUID(uid) || '';
        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);

        const editButton = document.createElement('button');
        editButton.classList.add('ckupnotes-profile-aside-card-button');
        editButton.textContent = '编辑备注';
        editButton.addEventListener('click', () => {
            UPNotesManager.callUIForEditing(uid, username, ()=>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 init() {
        logger.log('Initializing Bilibili UP Notes script...');
        // 注册任意页面事件
        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);