// ==UserScript== // @name Bilibili用户备注 // @namespace https://github.com/pxoxq // @version 0.2.1 // @description B站用户备注脚本| Bilibili用户备注 // @license AGPL-3.0-or-later // @author pxoxq // @match https://space.bilibili.com/** // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant GM_addElement // @grant GM_addStyle // @grant window.onurlchange // @require https://code.jquery.com/jquery-3.7.1.min.js // ==/UserScript== // ==========防抖函数============= function pxoDebounce(func, delay){ let timer = null function _debounce(...arg){ timer && clearTimeout(timer) timer = setTimeout(()=>{ func.apply(this, arg) timer = null }, delay) } return _debounce } class DateUtils{ static getCurrDateTimeStr(){ let date = new Date(); let year = date.getFullYear(); let month = date.getMonth() + 1; let day = date.getDate(); let hour = date.getHours(); let minutes = date.getMinutes(); let sec = date.getSeconds(); return `${year}${month}${day}${hour}${minutes}${sec}` } } /* ======================================= IndexedDB 开始 ======================================= */ class MyIndexedDB { request; db; dbName; dbVersion; store; constructor( dbName, dbVersion, store ) { this.dbName = dbName; this.dbVersion = dbVersion; this.store = store } static async create( dbName, dbVersion, store, ) { const obj = new MyIndexedDB( dbName, dbVersion, store ); obj.db = await obj.getConnection(); return obj; } async initDB() { return new Promise((resolve, rej) => { this.getConnection().then((res) => { this.db = res; resolve(this); }); }); } // 控制台打印错误 consoleError(msg) { console.log(`[myIndexedDB]: ${msg}`); } getConnection = async () => { return new Promise((resolve, rej) => { this.request = indexedDB.open(this.dbName, this.dbVersion); this.request.onerror = (e) => { console.error(`连接 ${this.dbName} [IndexedDB] 失败. version: [${this.dbVersion}]`, e); }; this.request.onupgradeneeded = async (event) => { const db = event.target.result; await this.createAndInitStore( db, this.store.conf.storeName, this.store.data, this.store.conf.uniqueIndex, this.store.conf.normalIndex ) resolve(db); }; this.request.onsuccess = (e) => { const db = e.target.result; resolve(db); }; }); }; async createAndInitStore( db = this.db, storeName = "", datas = [], uniqueIndex = [], normalIndex = [] ) { if(!storeName || !datas) return return new Promise((resolve, rej) => { // 自增id const store = db.createObjectStore(storeName, { keyPath: "id", autoIncrement: true, }); // 设置两类索引 uniqueIndex.forEach((item) => { store.createIndex(item, item, { unique: true }); }); normalIndex.forEach((item) => { store.createIndex(item, item, { unique: false }); }); // 初始填充数据 store.transaction.oncomplete = (e) => { const rwStore = this.getCustomRWstore(storeName, db); datas.forEach((item) => { rwStore.add(item); }); resolve(0) }; }); } // 获取所有数据 async getAllDatas() { return new Promise((resolve, rej) => { const rwStore = this.getCustomRWstore(); const req = rwStore.getAll(); req.onsuccess = (e) => { resolve(req?.result); }; }); } // 添加一条数据 async addOne(item) { return new Promise((resolve, rej) => { const rwStore = this.getCustomRWstore(); const req = rwStore.add(item); req.onsuccess = () => { resolve(true); }; req.onerror = () => { rej(false); }; }); } // 根据uid获取一条数据 async getOne(id = 0) { return new Promise((resolve, rej) => { const rwStore = this.getCustomRWstore(); const req = rwStore.get(id); req.onsuccess = () => { resolve(req.result); }; }); } // 查询一条数据, 字段column包含value子串 async queryOneLike(column, value) { return new Promise((resolve, rej) => { const rwStore = this.getCustomRWstore(); rwStore.openCursor().onsuccess = (event) => { const cursor = event.target.result; if (cursor) { const item = { ...cursor.value }; if (item[column] && item[column].indexOf(value) > -1) { item.id = cursor.key; resolve(item); } cursor.continue(); } else { resolve(false); } }; }); } // 查询一条数据, 字段column等于value async queryOneEq(column, value) { return new Promise((resolve, rej) => { const rwStore = this.getCustomRWstore(); rwStore.openCursor().onsuccess = (event) => { const cursor = event.target.result; if (cursor) { const item = { ...cursor.value }; if (item[column] == value) { // console.log(cursor); item.id = cursor.key; resolve(item); } cursor.continue(); } else { resolve(false); } }; }); } // 更新一条数据 async updateOne(item) { return new Promise((resolve, rej) => { const rwStore = this.getCustomRWstore(); const req = rwStore.put(item); req.onsuccess = () => { resolve(true); }; req.onerror = (e) => { console.log(req); console.log(e); rej(false); }; }); } // 删除一条数据 async delOne(id) { return new Promise((resolve, rej) => { const rwStore = this.getCustomRWstore(); const req = rwStore.delete(id); req.onsuccess = () => { resolve(true); }; req.onerror = (e) => { rej(false); }; }); } // 获取读写权限的存储桶 store。默认是this上挂的storename getCustomRWstore(storeName=this.store.conf.storeName, db = this.db) { return db.transaction(storeName, "readwrite").objectStore(storeName); } // 状态值为 done 时表示连接上了。db挂到了this上 requestState() { return this.request.readyState; } isReady() { return this.request.readyState == "done"; } // 关闭数据库链接 closeDB() { this.db && this.db.close(); } static setDBVersion(version){ localStorage.setItem("pxoxq-dbv", version) } static getDBVersion(){ const v = localStorage.getItem("pxoxq-dbv") return v } } /* ======================================= IndexedDB 结束 ======================================= */ /* ======================================= 配置数据库表 结束 ======================================= */ class ConfigDB{ static simplifyIdx=false static autoWideMode=false static playerHeight=700 static memoMode=0 static importMode=0 static Keys = { simplifyIdx: 'simplifyIdx', autoWideMode: 'autoWideMode', playerHeight: 'playerHeight', memoMode: 'memoMode', importMode: 'importMode', } static dbConfig = { DB_NAME: "bilibili_pxo", DB_V: MyIndexedDB.getDBVersion()??2, store: { conf:{ storeName: 'conf' } }, } static async connnectDB(func){ const myDb = await MyIndexedDB.create( this.dbConfig.DB_NAME, this.dbConfig.DB_V, this.dbConfig.store ) const result = await func(myDb) myDb.closeDB() return result } static async getConf(){ const res = await this.connnectDB(async db=>{ const rrr = db.getOne('bconf') return rrr; }) return res } static async updateConf(conf){ const res = await this.connnectDB(async db=>{ const rrr = await db.updateOne(conf) return rrr }) return res } static async updateOne(key, val){ const res = await this.connnectDB(async db=>{ const config = await this.getConf() config[key] = val const rrr = db.updateOne(config) return rrr }) return res } static async updateSimplifyIdx(val){ return await this.updateOne(this.Keys.simplifyIdx, val) } static async updateAutoWideMode(val){ return await this.updateOne(this.Keys.autoWideMode, val) } static async updatePlayerHeight(val){ return await this.updateOne(this.Keys.playerHeight, val) } static async updateMemoMode(val){ return await this.updateOne(this.Keys.memoMode, val) } static async updateImportMode(val){ return await this.updateOne(this.Keys.importMode, val) } } /* ======================================= 配置数据库表 结束 ======================================= */ /*========================================= 哔站昵称功能对IndexedDB 进行的封装 开始 ==========================================*/ class BilibiliMemoDB { static dbConfig = { DB_NAME: "bilibili_pxo", DB_V: MyIndexedDB.getDBVersion()??2, store: { conf:{ storeName: 'my_friends' } } }; static async connectDB(func) { const db = await MyIndexedDB.create( this.dbConfig.DB_NAME, this.dbConfig.DB_V, this.dbConfig.store, ); const result = await func(db); db.closeDB(); return result; } static async addOne(uid) { const res = await this.connectDB(async (db) => { const rrr = await db.addOne(uid); return rrr; }); return res; } static async getOne(uid) { const res = await this.connectDB(async (db) => { const rrr = await db.getOne(uid); return rrr; }); return res; } static async queryEq(column, value){ const res = await this.connectDB(async db =>{ const rrr = await db.queryOneEq(column, value) return rrr }) return res } static async queryLike(column, value){ const res = await this.connectDB(async db =>{ const rrr = await db.queryOneLike(column, value) return rrr }) return res } static async getOneByBid(bid){ const res = await this.queryEq('bid', bid) return res } static async getAll() { const res = await this.connectDB(async (db) => { const rrr = await db.getAllDatas(); return rrr; }); return res; } static async updateByIdAndMemo(id, memo){ const item = await this.getOne(id) item.nick_name = memo const res = await this.updateOne(item) return res } static async addOrUpdateMany(datas, ignore_mode=true){ for(const data of datas){ const _item = await this.getOneByBid(data.bid) if(_item){ if(!ignore_mode){ _item.nick_name = data.nick_name _item.bname = data.bname await this.updateOne(_item) } }else{ if(!data.bid) continue else{ const _itm = { bid: data.bid, bname: data.bname, nick_name: data.nick_name } await this.addOne(_itm) } } } return 1 } static async updateOne(item) { const res = await this.connectDB(async (db) => { const rrr = await db.updateOne(item); return rrr; }); return res; } static async delByBid(bid){ const _item = this.getOneByBid(bid) if(_item){ return await this.delOne(_item.id) } else{ return false } } static async delOne(id) { const res = await this.connectDB(async (db) => { const rrr = await db.delOne(id); return rrr; }); } } /*========================================= 哔站昵称功能对IndexedDB 进行的封装 结束 ==========================================*/ /* ======================================= 所有数据库表初始化 开始 ======================================= */ class DBInit{ static dbName = "bilibili_pxo" static dbV = '1' static storeList = [ { name: "B站备注表", conf:{ uniqueIndex: ["bid"], normalIndex: ["nick_name"], DB_NAME: "bilibili_pxo", storeName: "my_friends", }, data:[ { bid: "28563843", nick_name: "脚本作者", bname: "失名冲浪" }, ] }, { name: "配置项表", conf: { DB_NAME: "bilibili_pxo", storeName: "conf", }, data: [ { id: 'bconf', simplifyIdx: true, autoWideMode: false, playerHeight: 700, memoMode: 1, importMode: 0,} ] } ] static async initAllDB(){ for(let idx=0;idx { myDb.closeDB() }, 100); } } } /* ======================================= 所有数据库表初始化 结束 ======================================= */ /* ======================================= 菜单UI部分 结束 ======================================= */ class BMenu{ static menuStyle = ` @media (max-width: 1190px){ div#pxoxq-b-menu .pxoxq-menu-wrap{ display: block; overflow-y: scroll; scrollbar-width: thin; height: 340px; } #pxoxq-b-menu .pxoxq-menu-wrap::-webkit-scrollbar{ width: 5px; } #pxoxq-b-menu .pxoxq-menu-wrap::-webkit-scrollbar-thumb{ background-color: #FC6296; border-radius: 6px; } } /* 菜单最外层 */ #pxoxq-b-menu{ text-align: initial; font-size: 15px; z-index: 999; position: fixed; left: 0; right: 0; bottom: 0px; height: 340px; padding: 8px 10px; background-color: white; transition: all .24s linear; border-top: 1px solid #c3c3c3; } #pxoxq-b-menu.pxoxq-hide{ padding: unset; height: 0; } #pxoxq-b-menu button{ background-color: #FC6296; border: 1px solid white; color: white; font-size: 13px; padding: 1px 6px; border-radius: 5px; } #pxoxq-b-menu button:hover{ border: 1px solid #c5c5c5; } #pxoxq-b-menu button:active{ opacity: .7; } #pxoxq-b-menu .pxoxq-tag{ position: absolute; width: 24px; text-align: center; color: white; padding: 0px 6px; left: 2px; top: -21px; background-color: #FC6296; border-radius: 4px 4px 0 0; user-select: none; transition: all .3s linear; } #pxoxq-b-menu .pxoxq-tag:hover{ letter-spacing: 3px; } #pxoxq-b-menu .pxoxq-tag:active{ opacity: .5; } #pxoxq-b-menu .pxoxq-menu-wrap{ display: flex; } #pxoxq-b-menu .pxoxq-menu-col { height: 340px; min-height: 340px; overflow-y: scroll; scrollbar-width: thin; } #pxoxq-b-menu .pxoxq-menu-col::-webkit-scrollbar{ width: 5px; } #pxoxq-b-menu .pxoxq-menu-col::-webkit-scrollbar-thumb{ background-color: #FC6296; border-radius: 6px; } #pxoxq-b-menu .pxoxq-menu-wrap .pxoxq-setting-wrap{ flex-grow: 1; } #pxoxq-b-menu .setting-row:not(.import-row) { padding: 4px 0; display: flex; gap: 3px; } #pxoxq-b-menu .setting-row .pxoxq-label{ font-weight: 600; color: rgb(100, 100, 100); } #pxoxq-b-menu .pxoxq-setting-wrap .setting-box{ display: flex; gap: 22px; } #pxoxq-b-menu .setting-row .pxoxq-inline-label{ display: inline-block; margin-right: 20px; } #pxoxq-player-h{ width: 300px; } #pxoxq-b-menu .setting-row.memo-mode-row{ display: flex; padding-bottom: 10px; } #pxoxq-b-menu .setting-item-import{ display: flex; margin-bottom: 10px; } #pxoxq-b-menu .frd-import-btn{ margin-left: 40px; } /* 右边部分 */ #pxoxq-b-menu .pxoxq-menu-wrap .pxoxq-frd-wrap{ border-left: 1px solid #d1d1d1; padding-left: 10px; } #pxoxq-b-menu .pxoxq-right-header{ display: flex; padding-bottom: 6px; margin-bottom: 5px; border-bottom: 1px dotted #b2b2b2; } #pxoxq-b-menu .pxoxq-right-header .pxoxq-right-title{ font-size: 18px; flex-grow: 1; text-align: center; font-weight: 600; color: #4b4b4b; } /* 右边表格部分 */ #pxoxq-b-menu .pxoxq-frd-tab{ white-space: nowrap; height: 340px; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tbody{ height: 280px; overflow-y: scroll; scrollbar-width: thin; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tbody::-webkit-scrollbar{ width: 4px; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tbody::-webkit-scrollbar-thumb{ background-color: #FC6296; border-radius: 5px; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-thead{ font-weight: 600; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tr{ border-bottom: 1px solid #dadada; /* text-align: center; */ } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tr .pxoxq-cell{ display: inline-block; text-align: center; font-size: 14px; padding: 2px 3px; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-col-1{ width: 30px; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-col-2{ width: 120px; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-col-3{ width: 120px; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-col-4{ width: 180px; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-col-5{ width: 100px; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-memo-ipt{ outline: none; border: unset; text-align: center; padding: 2px 3px; } #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-memo-ipt.active{ border-bottom: 1px solid #ffb3e3; color:#FC6296; } ` static wrapId = 'pxoxq-b-menu' static saveDelay = 200 static importJson = "" static init(){ this.injectMemuHtml() this.injectStyle() } static injectMemuHtml(){ // 参数初始化 const wrap = $("#pxoxq-b-menu") ConfigDB.getConf().then(async _conf =>{ const friendTab = await this.genFriendTab() const leftMenu = `

备注模块设置

备注显示模式
导入数据
` if(wrap && wrap.length>0){ this.flushConfTab() this.flushFrdTab() }else{ const _html = `
:)
${leftMenu}
昵称数据
ID
BilibiliID
昵称
备注
操作
${friendTab}
` $('body').append(_html) this.addListener() } }) } static async genFriendTab(){ const friends = await BilibiliMemoDB.getAll() let _html = '' for(const friend of friends){ _html += `
${friend.id}
${friend.bid}
${friend.bname}
` } return _html } static flushFrdTab(){ this.genFriendTab().then(_tabHtml =>{ $("#pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tbody").html(_tabHtml) }) } static flushConfTab(){ ConfigDB.getConf().then(_conf=>{ const mmRadios = $(".pxoxq-memo-mode") for(const item of mmRadios){ if(item.value == _conf.memoMode){ item.checked = true }else{ item.checked = false } } const modeRadios = $(".pxoxq-import-mode") for(const item of modeRadios){ if(item.value == _conf.memoMode){ item.checked = true }else{ item.checked = false } } }) } static injectStyle(){ GM_addStyle(this.menuStyle) } static addListener(){ const wrapIdSelector = `#${this.wrapId}` // 面板展开、折叠 $("body").on("click", wrapIdSelector+" .pxoxq-tag", pxoDebounce(this.toggleMenuHandler, this.saveDelay)) // 备注模式选框 $("body").on("click", ".pxoxq-memo-mode", pxoDebounce(this.memoModeHandler, this.saveDelay)) // 导入数据模式 $("body").on("click", ".pxoxq-import-mode", pxoDebounce(this.importModeHandler, this.saveDelay)) // 导入数据 $("body").on("click", ".frd-import-btn", this.importFriendHandler) // 导出数据 $("body").on("click", ".pxoxq-export-frd-btn", pxoDebounce(this.exportFrdHandler, this.saveDelay*2)) // 双击比编辑 $("body").on("dblclick", "input.pxoxq-memo-ipt", (this.editMemoHandler)) // 编辑按钮编辑 $("body").on("click", ".pxoxq-memo-edit-btn", pxoDebounce(this.editMemoHandler, this.saveDelay)) // 保存昵称(更新 $("body").on("click", ".pxoxq-memo-save-btn", pxoDebounce(this.updateMemoHandler, this.saveDelay)) // 删除备注 $("body").on("click", ".pxoxq-memo-del-btn", pxoDebounce(this.delMemoHandler, this.saveDelay)) } // 折叠、打开面板 static toggleMenuHandler(){ $("#pxoxq-b-menu").toggleClass("pxoxq-hide") // 刷新面板数据 if(document.getElementById("pxoxq-b-menu").classList.value.indexOf('pxoxq-hide')<0){ BMenu.flushConfTab() BMenu.flushFrdTab() }else{ } } static delMemoHandler(){ const id = parseInt(this.dataset.id) const memo = $(".pxoxq-memo-ipt-"+id).val() if(confirm(`是否要删除备注【${memo}】?`)){ BilibiliMemoDB.delOne(id) $(".pxoxq-frd-tab .pxoxq-frd-"+id).remove() } } static updateMemoHandler(){ const id = this.dataset.id let editBtn = $(".pxoxq-memo-edit-btn-"+id) const memoInput = $(".pxoxq-memo-ipt-"+id) // 都需编辑按钮复原 $(editBtn).text("编辑") $(editBtn).removeClass("pxoxq-memo-save-btn") memoInput[0].readOnly = true $(memoInput).removeClass('active') const val = memoInput[0].value BilibiliMemoDB.updateByIdAndMemo(parseInt(id), val) } static editMemoHandler(){ const id = this.dataset.id let editBtn = $(".pxoxq-memo-edit-btn-"+id) const memoInput = $(".pxoxq-memo-ipt-"+id) if(!memoInput[0].readOnly){ return } $(editBtn).text("保存") $(editBtn).addClass("pxoxq-memo-save-btn") memoInput[0].readOnly = false $(memoInput).addClass('active') } // 导出数据 static exportFrdHandler(){ BilibiliMemoDB.getAll().then(_datas =>{ const json_str = JSON.stringify(_datas) const dataURI = "data:text/plain;charset=utf-8," + encodeURIComponent(json_str); const link = document.createElement("a") link.href = dataURI link.download = `${DateUtils.getCurrDateTimeStr()}.txt` link.click() }) } // 导入数据 static importFriendHandler(){ const textNode = $("#pxoxq-frd-json") const val = $(textNode).val() if(!/\S+/.test(val)) return ConfigDB.getConf().then(async _conf=>{ try{ const datas = JSON.parse(val) if(Array.isArray(datas)){ const ignore_mode = _conf.importMode == 1? false: true await BilibiliMemoDB.addOrUpdateMany(datas, ignore_mode) BMenu.flushFrdTab() alert("导入成功") }else{ throw Error("数据格式错误!") } }catch(e){ alert("导入失败:"+e) } }) } static importModeHandler(){ ConfigDB.updateImportMode(this.value) } static memoModeHandler(){ MemoGlobalConf.mode = this.value ConfigDB.updateMemoMode(this.value) } } /* ======================================= 菜单UI部分 结束 ======================================= */ /*............................................................................................ Memo部分 开始 ............................................................................................*/ /* ============================================= 一些配置参数 开始 =============================================*/ const memoClassPrefix = "pxo-memo"; const MemoGlobalConf = { maxRetries: 40, // 最多轮询查找n次,超过节点仍未出现则停止定时器 总时长:n x 100 ms mode: 1, // 【模式】 0:昵称替换成备注; 1:昵称(备注); 2:(备注)昵称 myFriends: [], // 好友信息列表 currListFlag: "", // 当前列表页面标志【换页后页面需要时间加载 activePage: "-99", currUrl: "", memoClassPrefix, fansInputBlurDelay: 280, // 输入框防抖延迟 fansInputBlurTimer: '', fansLoopTimer: '', memoStyle: ` .content .be-pager li{ z-index: 999; position: relative; } .pxo-frd{ color: #3fb9ffd4; font-weight:600; letter-spacing: 2px; border: 1px solid #ff88a973; border-radius: 6px; background: #ffa9c1a4; margin-top:-2px; padding: 2px 5px;} .h #h-name { background: #ffffffbd; padding: 5px 10px; border-radius: 6px; letter-spacing: 3px; line-height: 22px; font-size: 20px; box-shadow: 1px 1px 2px 2px #ffffff40; border: 1px solid #fff; color: #e87b99; overflow: hidden; transition:all .53s linear; } .h #h-name.hide{ width:0px; padding:0px; height:0px; border:none; } .h .homepage-memo-input{ border: none; outline:none; overflow:hidden; padding: 5px 6px; border-bottom:2px solid #ff0808; width: 230px; font-size: 17px; line-height: 22px; vertical-align: middle; background: #ffffffbd;; color: #f74979; font-weight:600; margin-right: 8px; transition:all .53s linear; border-radius: 5px 5px 0 0; } .h .homepage-memo-input.hide{ width: 0px; padding: 0; border:none; } .${memoClassPrefix}-setting-box{ display: inline-block; vertical-align:top; margin-top:-2px; line-height:20px; margin-left:18px; } .${memoClassPrefix}-setting-box div.btn{ padding:2px 5px; user-select:none; display:inline-block; overflow: hidden; letter-spacing:2px; background:#e87b99cc; border:none; border-radius:5px; color:white; margin:0 3px; transition:all .53s linear; } .${memoClassPrefix}-setting-box div.btn.hide{ height: 0px; width: 0px; opacity: 0.2; padding:0px; } .${memoClassPrefix}-setting-box div.btn:hover{ box-shadow: 1px 1px 2px 1px #80808024; outline: .5px solid #e87b99fc; } .${memoClassPrefix}-setting-box input{ border: none; outline:none; overflow:hidden; padding: 2px 3px; border-bottom:1px solid #c0c0c0; width: 190px; font-size: 16px; line-height: 18px; color: #ff739a; font-weight:600; vertical-align:top; transition:all .25s linear; } .${memoClassPrefix}-setting-box input.hide{ width:0px; padding:0px; } `, }; /* ============================================= 一些配置参数 结束 =============================================*/ /* ============================================= 定制日志输出 开始 =============================================*/ class MyLog { static prefix = "[BilibiliMemo]"; static genMsg(msg, type = "") { return `${this.prefix} ${type}: ${msg}`; } static error(msg) { console.error(this.genMsg(msg, "error")); } static warn(msg) { console.warn(this.genMsg(msg, "warn")); } static success(msg) { console.info(this.genMsg(msg, "success")); } static log(msg, ...arg){ console.log(this.genMsg(msg), ...arg) } } /* ============================================= 定制日志输出 结束 =============================================*/ /* ============================================= html 注入部分 开始 =============================================*/ class BilibiliMemoInjectoin { // 个人主页 替换 以及初始化 static async injectUserHome(bid) { const user = await this.getUserInfoByBid(bid); let uname = $("#h-name"); if (uname.length < 0 || uname.text() == 0) { let cnt = 0; let timer = setInterval(() => { uname = $("#h-name"); if (uname.length > 0) { clearInterval(timer); let nickName = $("#h-name").html(); if(user){ $("#h-name").html(this.getNameStr(nickName, user.nick_name)); $("#h-name").attr("data-id", user.id); } $("#h-name").attr("data-bid", bid); $("#h-name").attr("data-bname", nickName); // 添加备注模块 const inputNode = `` $('#h-name').after(inputNode) } cnt += 1; if (cnt > MemoGlobalConf.maxRetries) { MyLog.warn("加载失败"); clearInterval(timer); } }, 100); } else { let nickName = $("#h-name").html(); if(user){ $("#h-name").html(this.getNameStr(nickName, user.nick_name)); $("#h-name").attr("data-id", user.id); } $("#h-name").attr("data-bid", bid); $("#h-name").attr("data-bname", nickName); // 添加备注模块 const inputNode = `` $('#h-name').after(inputNode) } } // 个人主页 替换 更新 static injectOneHomePage(user){ if(user){ const nickName = $('.h #h-name').attr('data-bname') $("#h-name").html(this.getNameStr(nickName, user.nick_name)); $("#h-name").attr("data-id", user.id); } } // 个人关注、粉丝页替换 以及初始化 static injectFanList() { clearInterval(MemoGlobalConf.fansLoopTimer) let users = ""; let page = this.getActivePage(); let ifrom = window.location.href; // console.log('page!: ', page, MemoGlobalConf.activePage) // console.log('url!: ', ifrom, '\n', MemoGlobalConf.currUrl) if (page == MemoGlobalConf.activePage && ifrom == MemoGlobalConf.currUrl) { return false; } else { MemoGlobalConf.currUrl = ifrom; let cnt = 0; MemoGlobalConf.fansLoopTimer = setInterval(async () => { users = $(".relation-list > li > div.content > a"); if (users.length > 0 && users[0].href != MemoGlobalConf.currListFlag) { clearInterval(MemoGlobalConf.fansLoopTimer); MemoGlobalConf.activePage = page; MemoGlobalConf.currListFlag = users[0].href; for (let i = 0; i < users.length; i++) { let url = users[i].href; let uid = url.split("/")[3]; const cPrefix = MemoGlobalConf.memoClassPrefix; if($(`.${cPrefix}-setting-${uid}`).length < 1){ const user = await this.getUserInfoByBid(uid); let nickName = $(users[i].children).html(); $(users[i].children).attr("data-bname", nickName); $(users[i].children).attr("data-bid", uid); if (user) { $(users[i].children).html( this.getNameStr(nickName, user.nick_name) ); $(users[i].children).attr("data-id", user.id); $(users[i]).addClass("pxo-frd"); $(users[i]).addClass("pxo-frd-"+uid); } // 注入备注模块代码 const memoBlock = `
备注
确认
取消
清除备注
`; $(users[i]).after(memoBlock); } } } cnt += 1; if (cnt > MemoGlobalConf.maxRetries) { MyLog.warn("加载失败"); clearInterval(MemoGlobalConf.fansLoopTimer); } }, 120); } } // 个人关注、粉丝页替换 单个 static injectOneFans(user, userANode) { if (user && userANode) { const nickName = $(userANode.children).attr("data-bname"); $(userANode.children).html(this.getNameStr(nickName, user.nick_name)); $(userANode.children).attr("data-id", user.id); $(userANode).addClass("pxo-frd"); $(userANode).addClass("pxo-frd-"+user.bid); } } static replaceMemo(uri) { const uType = this.judgeUri(uri); switch (uType) { case "-1": MyLog.warn("Uri获取失败"); break; case "+1": //粉丝关注 BilibiliMemoInjectoin.injectFanList(); break; default: // 个人主页 BilibiliMemoInjectoin.injectUserHome(uType); } } static judgeUri(uri) { /* -1 uri为空 +x +1:粉丝、关注 | +* 后续 xxxx 纯数字,个人主页 */ if (!uri) return "-1"; const uri_parts = uri.split("/"); // 0-https 1-'' 2-host 3-bid 4-fans/query 5-fans/follow // 这是 space 域下的处理,之后可能扩展到其他更多页面模块 if (uri_parts[2] && "space.bilibili.com" == uri_parts[2]) { // 粉丝、关注列表 【归一类,处理方式一样】 if ( uri_parts.length > 4 && uri_parts[4] == "fans" && /(?=fans)|(?=follow)/.test(uri_parts[5]) ) { return "+1"; } // 个人主页 else { return uri_parts[3].split("?")[0]; } } return "-1"; } // 根据bid获取用户信息 直接从数据库取吧 static async getUserInfoByBid(bid) { const res = await BilibiliMemoDB.getOneByBid(bid) return res; } // 根据昵称、备注获取最终显示名 static getNameStr(nickName, remark) { if (nickName.indexOf("") > 0) { return nickName; } let res = ""; if(MemoGlobalConf.mode==1){ res = nickName + `(${remark})`; } else if(MemoGlobalConf.mode==2){ res = remark + `(${nickName})`; } else if(MemoGlobalConf.mode==3){ res = remark } console.log("ppppp: ", res) return res + ""; } // 获取当前活跃的页码 static getActivePage() { let pages = $(".be-pager .be-pager-item"); for (let i = 0; i < pages.length; i++) { let cls = $(pages[i]).attr("class"); if (cls.indexOf("active") > 0) { return pages[i].textContent; } } return ""; } // 注入css样式到头部 static injectCSS(css) { GM_addStyle(css); } } /* ============================================= html 注入部分 结束 =============================================*/ /* ============================================= 通用函数部分 开始 =============================================*/ class BMemoUtils{ // 关注、粉丝列表页 备注编辑模块 编辑模式 / 正常模式 static toggleMemoBox(bid, editMode=true){ if(editMode){ $(`.btn.op-btn-${bid}`).removeClass("hide"); $(`.${MemoGlobalConf.memoClassPrefix}-input-${bid}`).removeClass('hide') $(`.btn.bz-btn-${bid}`).addClass("hide"); }else{ $(`.btn.op-btn-${bid}`).addClass("hide"); $(`.${MemoGlobalConf.memoClassPrefix}-input-${bid}`).addClass('hide') $(`.btn.bz-btn-${bid}`).removeClass("hide"); } } // 个人主页 编辑模式 / 正常模式 static toggleUserHomeEditMode(editMode=true){ if(editMode){ $('.h #h-name').addClass('hide') $('.homepage-memo-input').removeClass('hide') }else{ $('.h #h-name').removeClass('hide') $('.homepage-memo-input').addClass('hide') } } // 个人空间主页 编辑模式初始化 static homePageEditModeHandler(bid){ this.toggleUserHomeEditMode() const inputNode = $('.homepage-memo-input')[0] const bName = $(inputNode).attr('data-bname') $(inputNode).focus() BilibiliMemoDB.getOneByBid(bid).then(user=>{ if(user){ $(inputNode).val(user.nick_name) }else{ $(inputNode).val(bName) } }) } // 个人空间主页 编辑确认 static homePageSetMemoHandler(bid){ const inputNode = $('.homepage-memo-input')[0] const bName = $(inputNode).attr('data-bname') const val = $(inputNode).val() const val_reg = /\S.*\S/ if(val && val_reg.test(val)){ const memo = val_reg.exec(val)[0] BilibiliMemoDB.getOneByBid(bid).then(async user =>{ if(user){ if(memo != user.nick_name){ user.nick_name = memo user.bname = bName await BilibiliMemoDB.updateOne(user) BilibiliMemoInjectoin.injectOneHomePage(user) } this.toggleUserHomeEditMode(false) }else{ if(memo != bName){ user = { bid, nick_name: memo, bname: bName } await BilibiliMemoDB.addOne(user) user = await BilibiliMemoDB.getOneByBid(bid) BilibiliMemoInjectoin.injectOneHomePage(user) } this.toggleUserHomeEditMode(false) } }) } } // 删除备注 static delMemoHandler(bid){ BilibiliMemoDB.getOneByBid(bid).then(async _item=>{ if(_item){ if(confirm(`是否删除备注【${_item.nick_name}】?`)){ await BilibiliMemoDB.delOne(_item.id) $("a.pxo-frd-"+bid).removeClass("pxo-frd") const nameSpan = $("a.pxo-frd-"+bid+" span.fans-name") $(nameSpan).text(nameSpan[0].dataset.bname) } } }) } // 粉丝、关注页 编辑模式初始化 static editModeHandler(bid){ const inputNode = $(`.${MemoGlobalConf.memoClassPrefix}-input-${bid}`)[0]; BilibiliMemoDB.getOneByBid(bid).then(user=>{ const val = $(inputNode).val() if(!/\S+/.test(val)){ if(user){ $(inputNode).val(user.nick_name) }else{ $(inputNode).val($(inputNode).attr('data-bname')) } } }) this.toggleMemoBox(bid) $(inputNode).focus() } // 粉丝、关注页编辑确认 static setMemoHandler(bid){ const inputNode = $(`.${MemoGlobalConf.memoClassPrefix}-input-${bid}`)[0]; const val = $(inputNode).val(); const val_reg = /\S.*\S/; const bName = $(inputNode).attr('data-bname') if (val_reg.test(val)) { const memo = val_reg.exec(val)[0]; const userANode = $(inputNode).parent().siblings('a')[0] BilibiliMemoInjectoin.getUserInfoByBid(bid).then(async (user)=>{ if (user) { if(user.nick_name != memo){ user.nick_name = memo; user.bname = bName await BilibiliMemoDB.updateOne(user); BilibiliMemoInjectoin.injectOneFans(user, userANode) } this.toggleMemoBox(bid, false) } else { if(memo != bName){ user = { bid, nick_name: memo, bname: bName }; await BilibiliMemoDB.addOne(user); user = await BilibiliMemoDB.getOneByBid(bid) BilibiliMemoInjectoin.injectOneFans(user, userANode) } this.toggleMemoBox(bid, false) } }) } } } /* ============================================= 通用函数部分 结束 =============================================*/ /*-----------------初始化 开始-----------------*/ async function BilibiliMemoInit() { // 注入样式 BilibiliMemoInjectoin.injectCSS(MemoGlobalConf.memoStyle); if (window.onurlchange === null) { window.addEventListener("urlchange", (info) => { uri = info?.url; BilibiliMemoInjectoin.replaceMemo(uri); }); } // 页码切换按钮 $(`#app`).on("click", ".content ul.be-pager li a", function () { setTimeout(()=>{ const currPage = BilibiliMemoInjectoin.getActivePage() BilibiliMemoInjectoin.replaceMemo(window.location.href); }, 100) }); // 页码切换按钮 $(`body`).on("click", ".content ul.be-pager", function () { const currPage = BilibiliMemoInjectoin.getActivePage() setTimeout(()=>{ const box = $(`.${MemoGlobalConf.memoClassPrefix}-setting-box`) if(box.length < 1){ MemoGlobalConf.activePage = '-99' MemoGlobalConf.currListFlag = 'xxx' BilibiliMemoInjectoin.replaceMemo(window.location.href); } }, 300) }); // 最常访问、最近关注 $(`body`).on("click", ".follow-main .follow-tabs > span", function () { MemoGlobalConf.activePage = $(this).text() BilibiliMemoInjectoin.replaceMemo(window.location.href); }); // 个人主页双击修改事件 $('body').on( 'dblclick', `.h #h-name`, function(event){ const bid = event.currentTarget.dataset.bid; BMemoUtils.homePageEditModeHandler(bid) } ) // 个人主页搜索框失去焦点事件 $('body').on( 'focusout', '.homepage-memo-input', function(event){ const bid = event.currentTarget.dataset.bid; BMemoUtils.homePageSetMemoHandler(bid) } ) // 粉丝、关注页 备注按钮点击事件: $("body").on( "click", `.${MemoGlobalConf.memoClassPrefix}-setting-box div.${MemoGlobalConf.memoClassPrefix}-btn-bz`, function (event) { const bid = event.currentTarget.dataset.bid; BMemoUtils.editModeHandler(bid) } ); // 删除备注按钮点击事件 $("body").on( "click", `.${MemoGlobalConf.memoClassPrefix}-setting-box div.${MemoGlobalConf.memoClassPrefix}-btn-del`, function (event) { const bid = event.currentTarget.dataset.bid; BMemoUtils.delMemoHandler(bid) } ) // 粉丝、关注页确认按钮点击事件 $("body").on( "click", `.${MemoGlobalConf.memoClassPrefix}-setting-box .${MemoGlobalConf.memoClassPrefix}-btn-cfm`, function (event) { clearTimeout(MemoGlobalConf.fansInputBlurTimer) const bid = event.currentTarget.dataset.bid; BMemoUtils.setMemoHandler(bid) } ); // 粉丝、关注页取消按钮点击事件 $("body").on( "click", `.${MemoGlobalConf.memoClassPrefix}-setting-box .${MemoGlobalConf.memoClassPrefix}-btn-cancel`, function (event) { clearTimeout(MemoGlobalConf.fansInputBlurTimer) const bid = event.currentTarget.dataset.bid; BMemoUtils.toggleMemoBox(bid, false) }) // 粉丝、关注页输入框市区焦点事件 $("body").on( "focusout", `.${MemoGlobalConf.memoClassPrefix}-setting-box input`, function (event) { clearTimeout(MemoGlobalConf.fansInputBlurTimer) MemoGlobalConf.fansInputBlurTimer = setTimeout(()=>{ const bid = event.currentTarget.dataset.bid; BMemoUtils.toggleMemoBox(bid, false) }, MemoGlobalConf.fansInputBlurDelay) }) } /*-----------------初始化 结束-----------------*/ /*........................................................................................................................................ Memo部分 结束 ........................................................................................................................................*/ async function flushConf(){ const _conf = await ConfigDB.getConf() MemoGlobalConf.mode = _conf.memoMode return true } /*+++++++++++++++++++++++++++++++++++++ 主程序初始化 开始 +++++++++++++++++++++++++++++++++++++*/ async function bilibiliCustomInit(){ if(!MyIndexedDB.getDBVersion()){ await DBInit.initAllDB() } // 从数据库获取数据,刷新配置参数 await flushConf() BMenu.init() if(MemoGlobalConf.mode==0) return const uri = window.location.href BilibiliMemoInit().then(r=>{ BilibiliMemoInjectoin.replaceMemo(uri) }) } /*+++++++++++++++++++++++++++++++++++++ 主程序初始化 结束 +++++++++++++++++++++++++++++++++++++*/ (function(){ bilibiliCustomInit().then(res=>{ console.log("init over") }) })();