Greasy Fork

来自缓存

Greasy Fork is available in English.

Bilibili UP Notes

A simple script to add notes to Bilibili UPs.

当前为 2026-02-06 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 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      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
// ==/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);