B站备注

B站用户备注脚本| Bilibili用户备注

  1. // ==UserScript==
  2. // @name B站备注
  3. // @namespace https://github.com/pxoxq
  4. // @version 0.4.1
  5. // @description B站用户备注脚本| Bilibili用户备注
  6. // @license AGPL-3.0-or-later
  7. // @author pxoxq
  8. // @match https://*.bilibili.com/**
  9. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  10. // @grant GM_addElement
  11. // @grant GM_addStyle
  12. // @grant window.onurlchange
  13. // @require https://code.jquery.com/jquery-3.7.1.min.js
  14. // ==/UserScript==
  15.  
  16. // -----------ElementGetter----------------
  17. var elmGetter = function() {
  18. const win = window.unsafeWindow || document.defaultView || window;
  19. const doc = win.document;
  20. const listeners = new WeakMap();
  21. let mode = 'css';
  22. let $;
  23. const elProto = win.Element.prototype;
  24. const matches = elProto.matches ||
  25. elProto.matchesSelector ||
  26. elProto.webkitMatchesSelector ||
  27. elProto.mozMatchesSelector ||
  28. elProto.oMatchesSelector;
  29. const MutationObs = win.MutationObserver ||
  30. win.WebkitMutationObserver ||
  31. win.MozMutationObserver;
  32. function addObserver(target, callback) {
  33. const observer = new MutationObs(mutations => {
  34. for (const mutation of mutations) {
  35. if (mutation.type === 'attributes') {
  36. callback(mutation.target);
  37. if (observer.canceled) return;
  38. }
  39. for (const node of mutation.addedNodes) {
  40. if (node instanceof Element) callback(node);
  41. if (observer.canceled) return;
  42. }
  43. }
  44. });
  45. observer.canceled = false;
  46. observer.observe(target, {childList: true, subtree: true, attributes: true});
  47. return () => {
  48. observer.canceled = true;
  49. observer.disconnect();
  50. };
  51. }
  52. function addFilter(target, filter) {
  53. let listener = listeners.get(target);
  54. if (!listener) {
  55. listener = {
  56. filters: new Set(),
  57. remove: addObserver(target, el => listener.filters.forEach(f => f(el)))
  58. };
  59. listeners.set(target, listener);
  60. }
  61. listener.filters.add(filter);
  62. }
  63. function removeFilter(target, filter) {
  64. const listener = listeners.get(target);
  65. if (!listener) return;
  66. listener.filters.delete(filter);
  67. if (!listener.filters.size) {
  68. listener.remove();
  69. listeners.delete(target);
  70. }
  71. }
  72. function query(all, selector, parent, includeParent, curMode) {
  73. switch (curMode) {
  74. case 'css':
  75. const checkParent = includeParent && matches.call(parent, selector);
  76. if (all) {
  77. const queryAll = parent.querySelectorAll(selector);
  78. return checkParent ? [parent, ...queryAll] : [...queryAll];
  79. }
  80. return checkParent ? parent : parent.querySelector(selector);
  81. case 'jquery':
  82. let jNodes = $(includeParent ? parent : []);
  83. jNodes = jNodes.add([...parent.querySelectorAll('*')]).filter(selector);
  84. if (all) return $.map(jNodes, el => $(el));
  85. return jNodes.length ? $(jNodes.get(0)) : null;
  86. case 'xpath':
  87. const ownerDoc = parent.ownerDocument || parent;
  88. selector += '/self::*';
  89. if (all) {
  90. const xPathResult = ownerDoc.evaluate(selector, parent, null, 7, null);
  91. const result = [];
  92. for (let i = 0; i < xPathResult.snapshotLength; i++) {
  93. result.push(xPathResult.snapshotItem(i));
  94. }
  95. return result;
  96. }
  97. return ownerDoc.evaluate(selector, parent, null, 9, null).singleNodeValue;
  98. }
  99. }
  100. function isJquery(jq) {
  101. return jq && jq.fn && typeof jq.fn.jquery === 'string';
  102. }
  103. function getOne(selector, parent, timeout) {
  104. const curMode = mode;
  105. return new Promise(resolve => {
  106. const node = query(false, selector, parent, false, curMode);
  107. if (node) return resolve(node);
  108. let timer;
  109. const filter = el => {
  110. const node = query(false, selector, el, true, curMode);
  111. if (node) {
  112. removeFilter(parent, filter);
  113. timer && clearTimeout(timer);
  114. resolve(node);
  115. }
  116. };
  117. addFilter(parent, filter);
  118. if (timeout > 0) {
  119. timer = setTimeout(() => {
  120. removeFilter(parent, filter);
  121. resolve(null);
  122. }, timeout);
  123. }
  124. });
  125. }
  126. return {
  127. get currentSelector() {
  128. return mode;
  129. },
  130. get(selector, ...args) {
  131. let parent = typeof args[0] !== 'number' && args.shift() || doc;
  132. if (mode === 'jquery' && parent instanceof $) parent = parent.get(0);
  133. const timeout = args[0] || 0;
  134. if (Array.isArray(selector)) {
  135. return Promise.all(selector.map(s => getOne(s, parent, timeout)));
  136. }
  137. return getOne(selector, parent, timeout);
  138. },
  139. each(selector, ...args) {
  140. let parent = typeof args[0] !== 'function' && args.shift() || doc;
  141. if (mode === 'jquery' && parent instanceof $) parent = parent.get(0);
  142. const callback = args[0];
  143. const curMode = mode;
  144. const refs = new WeakSet();
  145. for (const node of query(true, selector, parent, false, curMode)) {
  146. refs.add(curMode === 'jquery' ? node.get(0) : node);
  147. if (callback(node, false) === false) return;
  148. }
  149. const filter = el => {
  150. for (const node of query(true, selector, el, true, curMode)) {
  151. const _el = curMode === 'jquery' ? node.get(0) : node;
  152. if (refs.has(_el)) break;
  153. refs.add(_el);
  154. if (callback(node, true) === false) {
  155. return removeFilter(parent, filter);
  156. }
  157. }
  158. };
  159. addFilter(parent, filter);
  160. },
  161. create(domString, ...args) {
  162. const returnList = typeof args[0] === 'boolean' && args.shift();
  163. const parent = args[0];
  164. const template = doc.createElement('template');
  165. template.innerHTML = domString;
  166. const node = template.content.firstElementChild;
  167. if (!node) return null;
  168. parent ? parent.appendChild(node) : node.remove();
  169. if (returnList) {
  170. const list = {};
  171. node.querySelectorAll('[id]').forEach(el => list[el.id] = el);
  172. list[0] = node;
  173. return list;
  174. }
  175. return node;
  176. },
  177. selector(desc) {
  178. switch (true) {
  179. case isJquery(desc):
  180. $ = desc;
  181. return mode = 'jquery';
  182. case !desc || typeof desc.toLowerCase !== 'function':
  183. return mode = 'css';
  184. case desc.toLowerCase() === 'jquery':
  185. for (const jq of [window.jQuery, window.$, win.jQuery, win.$]) {
  186. if (isJquery(jq)) {
  187. $ = jq;
  188. break;
  189. };
  190. }
  191. return mode = $ ? 'jquery' : 'css';
  192. case desc.toLowerCase() === 'xpath':
  193. return mode = 'xpath';
  194. default:
  195. return mode = 'css';
  196. }
  197. }
  198. };
  199. }();
  200.  
  201.  
  202. // ==========防抖函数=============
  203. function pxoDebounce(func, delay) {
  204. let timer = null;
  205. function _debounce(...arg) {
  206. timer && clearTimeout(timer);
  207. timer = setTimeout(() => {
  208. func.apply(this, arg);
  209. timer = null;
  210. }, delay);
  211. }
  212. return _debounce;
  213. }
  214.  
  215. class DateUtils {
  216. static getCurrDateTimeStr() {
  217. let date = new Date();
  218. let year = date.getFullYear();
  219. let month = date.getMonth() + 1;
  220. let day = date.getDate();
  221. let hour = date.getHours();
  222. let minutes = date.getMinutes();
  223. let sec = date.getSeconds();
  224. return `${year}${month}${day}${hour}${minutes}${sec}`;
  225. }
  226. }
  227.  
  228. /* =======================================
  229. IndexedDB 开始
  230. ======================================= */
  231. class MyIndexedDB {
  232. request;
  233. db;
  234. dbName;
  235. dbVersion;
  236. store;
  237.  
  238. constructor(dbName, dbVersion, store) {
  239. this.dbName = dbName;
  240. this.dbVersion = dbVersion;
  241. this.store = store;
  242. }
  243.  
  244. // 直接 await MyIndexedDB.create(xxxx) 获取实例对象
  245. static async create(dbName, dbVersion, store) {
  246. const obj = new MyIndexedDB(dbName, dbVersion, store);
  247. obj.db = await obj.getConnection();
  248. return obj;
  249. }
  250.  
  251. // 通过 new MyIndexedDB(xxx) 获取实例对象后,还需要 await initDB() 一下
  252. async initDB() {
  253. return new Promise((resolve, rej) => {
  254. this.getConnection().then((res) => {
  255. this.db = res;
  256. resolve(this);
  257. });
  258. });
  259. }
  260.  
  261. // 控制台打印错误
  262. consoleError(msg) {
  263. console.log(`[myIndexedDB]: ${msg}`);
  264. }
  265.  
  266. // 获取连接;直接挂到 this.db 上
  267. // 需要注意,第一次的话,会初始化好 db、 store。但是之后就不会初始化 store,需要判断获取
  268. getConnection = async () => {
  269. return new Promise((resolve, rej) => {
  270. // console.log("连接到数据库: "+`--${this.dbName}-- --${this.dbVersion}--`)
  271. // 打开数据库,没有则新建
  272. this.request = indexedDB.open(this.dbName, this.dbVersion);
  273. this.request.onerror = (e) => {
  274. console.error(
  275. `连接 ${this.dbName} [IndexedDB] 失败. version: [${this.dbVersion}]`,
  276. e
  277. );
  278. };
  279.  
  280. this.request.onupgradeneeded = async (event) => {
  281. const db = event.target.result;
  282. await this.createAndInitStore(
  283. db,
  284. this.store.conf.storeName,
  285. this.store.data,
  286. this.store.conf.uniqueIndex,
  287. this.store.conf.normalIndex
  288. );
  289. // await this.createAndInitStore(db);
  290. resolve(db);
  291. };
  292.  
  293. this.request.onsuccess = (e) => {
  294. const db = e.target.result;
  295. resolve(db);
  296. };
  297. });
  298. };
  299.  
  300. // 创建存储桶并初始化数据,默认是自增id
  301. async createAndInitStore(
  302. db = this.db,
  303. storeName = "",
  304. datas = [],
  305. uniqueIndex = [],
  306. normalIndex = []
  307. ) {
  308. if (!storeName || !datas) return;
  309. return new Promise((resolve, rej) => {
  310. // 自增id
  311. const store = db.createObjectStore(storeName, {
  312. keyPath: "id",
  313. autoIncrement: true,
  314. });
  315. // 设置两类索引
  316. uniqueIndex.forEach((item) => {
  317. store.createIndex(item, item, { unique: true });
  318. });
  319. normalIndex.forEach((item) => {
  320. store.createIndex(item, item, { unique: false });
  321. });
  322.  
  323. // 初始填充数据
  324. store.transaction.oncomplete = (e) => {
  325. const rwStore = this.getCustomRWstore(storeName, db);
  326. datas.forEach((item) => {
  327. rwStore.add(item);
  328. });
  329. resolve(0);
  330. };
  331. });
  332. }
  333.  
  334. // 获取所有数据
  335. async getAllDatas() {
  336. return new Promise((resolve, rej) => {
  337. const rwStore = this.getCustomRWstore();
  338. const req = rwStore.getAll();
  339. req.onsuccess = (e) => {
  340. resolve(req?.result);
  341. };
  342. });
  343. }
  344.  
  345. // 添加一条数据
  346. async addOne(item) {
  347. return new Promise((resolve, rej) => {
  348. const rwStore = this.getCustomRWstore();
  349. const req = rwStore.add(item);
  350. req.onsuccess = () => {
  351. resolve(true);
  352. };
  353. req.onerror = () => {
  354. rej(false);
  355. };
  356. });
  357. }
  358.  
  359. // 根据uid获取一条数据
  360. async getOne(id = 0) {
  361. return new Promise((resolve, rej) => {
  362. const rwStore = this.getCustomRWstore();
  363. const req = rwStore.get(id);
  364. req.onsuccess = () => {
  365. resolve(req.result);
  366. };
  367. });
  368. }
  369.  
  370. // 查询一条数据, 字段column包含value子串
  371. async queryOneLike(column, value) {
  372. return new Promise((resolve, rej) => {
  373. const rwStore = this.getCustomRWstore();
  374. rwStore.openCursor().onsuccess = (event) => {
  375. const cursor = event.target.result;
  376. if (cursor) {
  377. const item = { ...cursor.value };
  378. if (item[column] && item[column].indexOf(value) > -1) {
  379. item.id = cursor.key;
  380. resolve(item);
  381. }
  382. cursor.continue();
  383. } else {
  384. resolve(false);
  385. }
  386. };
  387. });
  388. }
  389.  
  390. // 查询一条数据, 字段column等于value
  391. async queryOneEq(column, value) {
  392. return new Promise((resolve, rej) => {
  393. const rwStore = this.getCustomRWstore();
  394. rwStore.openCursor().onsuccess = (event) => {
  395. const cursor = event.target.result;
  396. if (cursor) {
  397. const item = { ...cursor.value };
  398. if (item[column] == value) {
  399. item.id = cursor.key;
  400. resolve(item);
  401. }
  402. cursor.continue();
  403. } else {
  404. resolve(false);
  405. }
  406. };
  407. });
  408. }
  409.  
  410. // 更新一条数据
  411. async updateOne(item) {
  412. return new Promise((resolve, rej) => {
  413. const rwStore = this.getCustomRWstore();
  414. const req = rwStore.put(item);
  415. req.onsuccess = () => {
  416. resolve(true);
  417. };
  418. req.onerror = (e) => {
  419. console.log(req);
  420. console.log(e);
  421. rej(false);
  422. };
  423. });
  424. }
  425.  
  426. // 删除一条数据
  427. async delOne(id) {
  428. return new Promise((resolve, rej) => {
  429. const rwStore = this.getCustomRWstore();
  430. const req = rwStore.delete(id);
  431. req.onsuccess = () => {
  432. resolve(true);
  433. };
  434. req.onerror = (e) => {
  435. rej(false);
  436. };
  437. });
  438. }
  439.  
  440. // 获取读写权限的存储桶 store。默认是this上挂的storename
  441. getCustomRWstore(storeName = this.store.conf.storeName, db = this.db) {
  442. return db.transaction(storeName, "readwrite").objectStore(storeName);
  443. }
  444.  
  445. // 状态值为 done 时表示连接上了。db挂到了this上
  446. requestState() {
  447. return this.request.readyState;
  448. }
  449. isReady() {
  450. return this.request.readyState == "done";
  451. }
  452.  
  453. // 关闭数据库链接
  454. closeDB() {
  455. this.db && this.db.close();
  456. }
  457.  
  458. static setDBVersion(version) {
  459. localStorage.setItem("pxoxq-dbv", version);
  460. }
  461.  
  462. static getDBVersion() {
  463. const v = localStorage.getItem("pxoxq-dbv");
  464. return v;
  465. }
  466. }
  467. /* =======================================
  468. IndexedDB 结束
  469. ======================================= */
  470.  
  471. /* =======================================
  472. 配置数据库表 结束
  473. ======================================= */
  474. class ConfigDB {
  475. static simplifyIdx = false;
  476. static autoWideMode = false;
  477. static playerHeight = 700;
  478. static memoMode = 0;
  479. static importMode = 0;
  480.  
  481. static Keys = {
  482. simplifyIdx: "simplifyIdx",
  483. autoWideMode: "autoWideMode",
  484. playerHeight: "playerHeight",
  485. memoMode: "memoMode",
  486. importMode: "importMode",
  487. };
  488.  
  489. static dbConfig = {
  490. DB_NAME: "bilibili_pxo",
  491. DB_V: MyIndexedDB.getDBVersion() ?? 2,
  492. store: {
  493. conf: {
  494. storeName: "conf",
  495. },
  496. },
  497. };
  498.  
  499. static async connnectDB(func) {
  500. const myDb = await MyIndexedDB.create(
  501. this.dbConfig.DB_NAME,
  502. this.dbConfig.DB_V,
  503. this.dbConfig.store
  504. );
  505. const result = await func(myDb);
  506. myDb.closeDB();
  507. return result;
  508. }
  509.  
  510. static async getConf() {
  511. const res = await this.connnectDB(async (db) => {
  512. const rrr = db.getOne("bconf");
  513. return rrr;
  514. });
  515. return res;
  516. }
  517.  
  518. static async updateConf(conf) {
  519. const res = await this.connnectDB(async (db) => {
  520. const rrr = await db.updateOne(conf);
  521. return rrr;
  522. });
  523. return res;
  524. }
  525.  
  526. static async updateOne(key, val) {
  527. const res = await this.connnectDB(async (db) => {
  528. const config = await this.getConf();
  529. config[key] = val;
  530. const rrr = db.updateOne(config);
  531. return rrr;
  532. });
  533. return res;
  534. }
  535.  
  536. static async updateSimplifyIdx(val) {
  537. return await this.updateOne(this.Keys.simplifyIdx, val);
  538. }
  539.  
  540. static async updateAutoWideMode(val) {
  541. return await this.updateOne(this.Keys.autoWideMode, val);
  542. }
  543.  
  544. static async updatePlayerHeight(val) {
  545. return await this.updateOne(this.Keys.playerHeight, val);
  546. }
  547.  
  548. static async updateMemoMode(val) {
  549. return await this.updateOne(this.Keys.memoMode, val);
  550. }
  551.  
  552. static async updateImportMode(val) {
  553. return await this.updateOne(this.Keys.importMode, val);
  554. }
  555. }
  556. /* =======================================
  557. 配置数据库表 结束
  558. ======================================= */
  559.  
  560. /*=========================================
  561. 哔站昵称功能对IndexedDB 进行的封装 开始
  562. ==========================================*/
  563. class BilibiliMemoDB {
  564. static dbConfig = {
  565. DB_NAME: "bilibili_pxo",
  566. DB_V: MyIndexedDB.getDBVersion() ?? 2,
  567. store: {
  568. conf: {
  569. storeName: "my_friends",
  570. },
  571. },
  572. };
  573.  
  574. static async connectDB(func) {
  575. const db = await MyIndexedDB.create(
  576. this.dbConfig.DB_NAME,
  577. this.dbConfig.DB_V,
  578. this.dbConfig.store
  579. );
  580. const result = await func(db);
  581. db.closeDB();
  582. return result;
  583. }
  584.  
  585. static async addOne(uid) {
  586. const res = await this.connectDB(async (db) => {
  587. const rrr = await db.addOne(uid);
  588. return rrr;
  589. });
  590. return res;
  591. }
  592.  
  593. static async getOne(uid) {
  594. const res = await this.connectDB(async (db) => {
  595. const rrr = await db.getOne(uid);
  596. return rrr;
  597. });
  598. return res;
  599. }
  600.  
  601. static async queryEq(column, value) {
  602. const res = await this.connectDB(async (db) => {
  603. const rrr = await db.queryOneEq(column, value);
  604. return rrr;
  605. });
  606. return res;
  607. }
  608.  
  609. static async queryLike(column, value) {
  610. const res = await this.connectDB(async (db) => {
  611. const rrr = await db.queryOneLike(column, value);
  612. return rrr;
  613. });
  614. return res;
  615. }
  616.  
  617. static async getOneByBid(bid) {
  618. const res = await this.queryEq("bid", bid);
  619. return res;
  620. }
  621.  
  622. static async getAll() {
  623. const res = await this.connectDB(async (db) => {
  624. const rrr = await db.getAllDatas();
  625. return rrr;
  626. });
  627. return res;
  628. }
  629.  
  630. static async updateByIdAndMemo(id, memo) {
  631. const item = await this.getOne(id);
  632. item.nick_name = memo;
  633. const res = await this.updateOne(item);
  634. return res;
  635. }
  636.  
  637. static async addOrUpdateMany(datas, ignore_mode = true) {
  638. for (const data of datas) {
  639. const _item = await this.getOneByBid(data.bid);
  640. if (_item) {
  641. if (!ignore_mode) {
  642. _item.nick_name = data.nick_name;
  643. _item.bname = data.bname;
  644. await this.updateOne(_item);
  645. }
  646. } else {
  647. if (!data.bid) continue;
  648. else {
  649. const _itm = {
  650. bid: data.bid,
  651. bname: data.bname,
  652. nick_name: data.nick_name,
  653. };
  654. await this.addOne(_itm);
  655. }
  656. }
  657. }
  658. return 1;
  659. }
  660.  
  661. static async updateOne(item) {
  662. const res = await this.connectDB(async (db) => {
  663. const rrr = await db.updateOne(item);
  664. return rrr;
  665. });
  666. return res;
  667. }
  668.  
  669. static async delByBid(bid) {
  670. const _item = this.getOneByBid(bid);
  671. if (_item) {
  672. return await this.delOne(_item.id);
  673. } else {
  674. return false;
  675. }
  676. }
  677.  
  678. static async delOne(id) {
  679. const res = await this.connectDB(async (db) => {
  680. const rrr = await db.delOne(id);
  681. return rrr;
  682. });
  683. }
  684. }
  685. /*=========================================
  686. 哔站昵称功能对IndexedDB 进行的封装 结束
  687. ==========================================*/
  688.  
  689. /* =======================================
  690. 所有数据库表初始化 开始
  691. ======================================= */
  692. class DBInit {
  693. static dbName = "bilibili_pxo";
  694. static dbV = "1";
  695. static storeList = [
  696. {
  697. name: "B站备注表",
  698. conf: {
  699. uniqueIndex: ["bid"],
  700. normalIndex: ["nick_name"],
  701. DB_NAME: "bilibili_pxo",
  702. storeName: "my_friends",
  703. },
  704. data: [
  705. ],
  706. },
  707. {
  708. name: "配置项表",
  709. conf: {
  710. DB_NAME: "bilibili_pxo",
  711. storeName: "conf",
  712. },
  713. data: [
  714. {
  715. id: "bconf",
  716. simplifyIdx: false,
  717. autoWideMode: false,
  718. playerHeight: 700,
  719. memoMode: 0,
  720. importMode: 0,
  721. },
  722. ],
  723. },
  724. ];
  725.  
  726. static async initAllDB() {
  727. for (let idx = 0; idx < this.storeList.length; idx++) {
  728. const myDb = await MyIndexedDB.create(
  729. this.dbName,
  730. idx * 1 + 1,
  731. this.storeList[idx]
  732. );
  733. MyIndexedDB.setDBVersion(idx * 1 + 1);
  734. setTimeout(() => {
  735. myDb.closeDB();
  736. }, 100);
  737. }
  738. }
  739. }
  740. /* =======================================
  741. 所有数据库表初始化 结束
  742. ======================================= */
  743.  
  744. /* =======================================
  745. 菜单UI部分 结束
  746. ======================================= */
  747. class BMenu {
  748. static menuStyle = `
  749. @media (max-width: 1190px){
  750. div#pxoxq-b-menu .pxoxq-menu-wrap{
  751. display: block;
  752. overflow-y: scroll;
  753. scrollbar-width: thin;
  754. height: 340px;
  755. }
  756. #pxoxq-b-menu .pxoxq-menu-wrap::-webkit-scrollbar{
  757. width: 5px;
  758. }
  759. #pxoxq-b-menu .pxoxq-menu-wrap::-webkit-scrollbar-thumb{
  760. background-color: #FC6296;
  761. border-radius: 6px;
  762. }
  763. }
  764.  
  765. /* 菜单最外层 */
  766. #pxoxq-b-menu{
  767. text-align: initial;
  768. font-size: 15px;
  769. z-index: 999;
  770. position: fixed;
  771. left: 0;
  772. right: 0;
  773. bottom: 0px;
  774. height: 340px;
  775. padding: 8px 10px;
  776. background-color: white;
  777. transition: all .24s linear;
  778. border-top: 1px solid #c3c3c3;
  779. }
  780. #pxoxq-b-menu.pxoxq-hide{
  781. padding: unset;
  782. height: 0;
  783. }
  784. #pxoxq-b-menu button{
  785. background-color: #FC6296;
  786. border: 1px solid white;
  787. color: white;
  788. font-size: 13px;
  789. padding: 1px 6px;
  790. border-radius: 5px;
  791. }
  792. #pxoxq-b-menu button:hover{
  793. border: 1px solid #c5c5c5;
  794. }
  795. #pxoxq-b-menu button:active{
  796. opacity: .7;
  797. }
  798. #pxoxq-b-menu .pxoxq-tag{
  799. position: absolute;
  800. width: 24px;
  801. text-align: center;
  802. color: white;
  803. padding: 0px 6px;
  804. left: 2px;
  805. top: -21px;
  806. background-color: #FC6296;
  807. border-radius: 4px 4px 0 0;
  808. user-select: none;
  809. transition: all .3s linear;
  810. }
  811. #pxoxq-b-menu .pxoxq-tag:hover{
  812. letter-spacing: 3px;
  813. }
  814. #pxoxq-b-menu .pxoxq-tag:active{
  815. opacity: .5;
  816. }
  817. #pxoxq-b-menu .pxoxq-menu-wrap{
  818. display: flex;
  819. }
  820. #pxoxq-b-menu .pxoxq-menu-col {
  821. height: 340px;
  822. min-height: 340px;
  823. overflow-y: scroll;
  824. scrollbar-width: thin;
  825. }
  826. #pxoxq-b-menu .pxoxq-menu-col::-webkit-scrollbar{
  827. width: 5px;
  828. }
  829. #pxoxq-b-menu .pxoxq-menu-col::-webkit-scrollbar-thumb{
  830. background-color: #FC6296;
  831. border-radius: 6px;
  832. }
  833. #pxoxq-b-menu .pxoxq-menu-wrap .pxoxq-setting-wrap{
  834. flex-grow: 1;
  835. }
  836. #pxoxq-b-menu .setting-row:not(.import-row) {
  837. padding: 4px 0;
  838. display: flex;
  839. gap: 3px;
  840. }
  841. #pxoxq-b-menu .setting-row .pxoxq-label{
  842. font-weight: 600;
  843. color: rgb(100, 100, 100);
  844. }
  845. #pxoxq-b-menu .pxoxq-setting-wrap .setting-box{
  846. display: flex;
  847. gap: 22px;
  848. }
  849. #pxoxq-b-menu .setting-row .pxoxq-inline-label{
  850. display: inline-block;
  851. margin-right: 20px;
  852. }
  853. #pxoxq-player-h{
  854. width: 300px;
  855. }
  856. #pxoxq-b-menu .setting-row.memo-mode-row{
  857. display: flex;
  858. padding-bottom: 10px;
  859. }
  860. #pxoxq-b-menu .setting-item-import{
  861. display: flex;
  862. margin-bottom: 10px;
  863. }
  864. #pxoxq-b-menu .frd-import-btn{
  865. margin-left: 40px;
  866. }
  867. /* 右边部分 */
  868. #pxoxq-b-menu .pxoxq-menu-wrap .pxoxq-frd-wrap{
  869. border-left: 1px solid #d1d1d1;
  870. padding-left: 10px;
  871. }
  872. #pxoxq-b-menu .pxoxq-right-header{
  873. display: flex;
  874. padding-bottom: 6px;
  875. margin-bottom: 5px;
  876. border-bottom: 1px dotted #b2b2b2;
  877. }
  878. #pxoxq-b-menu .pxoxq-right-header .pxoxq-right-title{
  879. font-size: 18px;
  880. flex-grow: 1;
  881. text-align: center;
  882. font-weight: 600;
  883. color: #4b4b4b;
  884. }
  885. /* 右边表格部分 */
  886. #pxoxq-b-menu .pxoxq-frd-tab{
  887. white-space: nowrap;
  888. height: 340px;
  889. }
  890. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tbody{
  891. height: 280px;
  892. overflow-y: scroll;
  893. scrollbar-width: thin;
  894. }
  895. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tbody::-webkit-scrollbar{
  896. width: 4px;
  897. }
  898. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tbody::-webkit-scrollbar-thumb{
  899. background-color: #FC6296;
  900. border-radius: 5px;
  901. }
  902. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-thead{
  903. font-weight: 600;
  904. }
  905. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tr{
  906. border-bottom: 1px solid #dadada;
  907. /* text-align: center; */
  908. }
  909. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tr .pxoxq-cell{
  910. display: inline-block;
  911. text-align: center;
  912. font-size: 14px;
  913. padding: 2px 3px;
  914. }
  915. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-col-1{
  916. width: 30px;
  917. }
  918. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-col-2{
  919. width: 120px;
  920. }
  921. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-col-3{
  922. width: 120px;
  923. }
  924. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-col-4{
  925. width: 180px;
  926. }
  927. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-col-5{
  928. width: 100px;
  929. }
  930. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-memo-ipt{
  931. outline: none;
  932. border: unset;
  933. text-align: center;
  934. padding: 2px 3px;
  935. }
  936. #pxoxq-b-menu .pxoxq-frd-tab .pxoxq-memo-ipt.active{
  937. border-bottom: 1px solid #ffb3e3;
  938. color:#FC6296;
  939. }
  940. `;
  941.  
  942. static wrapId = "pxoxq-b-menu";
  943.  
  944. static saveDelay = 200;
  945.  
  946. static importJson = "";
  947.  
  948. static init() {
  949. this.injectMemuHtml();
  950. this.injectStyle();
  951. }
  952.  
  953. static injectMemuHtml() {
  954. // 参数初始化
  955. const wrap = $("#pxoxq-b-menu");
  956. ConfigDB.getConf().then(async (_conf) => {
  957. const friendTab = await this.genFriendTab();
  958. const leftMenu = `
  959. <h3>备注模块设置</h3>
  960. <div class="setting-row memo-mode-row">
  961. <div class="pxoxq-label pxoxq-inline-label">备注显示模式</div>
  962. <div class="pxoxq-radio-item">
  963. <input class="pxoxq-memo-mode" ${
  964. _conf.memoMode == 0 ? "checked" : ""
  965. } value="0" type="radio" name="memo-mode" id="nope">
  966. <label for="nope">关闭备注功能</label>
  967. </div>
  968. <div class="pxoxq-radio-item">
  969. <input class="pxoxq-memo-mode" ${
  970. _conf.memoMode == 1 ? "checked" : ""
  971. } value="1" type="radio" name="memo-mode" id="nick-first">
  972. <label for="nick-first">昵称(备注)</label>
  973. </div>
  974. <div class="pxoxq-radio-item">
  975. <input class="pxoxq-memo-mode" ${
  976. _conf.memoMode == 2 ? "checked" : ""
  977. } value="2" type="radio" name="memo-mode" id="memo-first">
  978. <label for="memo-first">备注(昵称)</label>
  979. </div>
  980. <div class="pxoxq-radio-item">
  981. <input class="pxoxq-memo-mode" ${
  982. _conf.memoMode == 3 ? "checked" : ""
  983. } value="3" type="radio" name="memo-mode" id="just-memo">
  984. <label for="just-memo">备注</label>
  985. </div>
  986. </div>
  987. <div class="setting-row import-row">
  988. <div class="pxoxq-setting-item setting-item-import">
  989. <div class="pxoxq-label pxoxq-inline-label">导入数据</div>
  990. <input class="pxoxq-import-mode" ${
  991. _conf.importMode == 0 ? "checked" : ""
  992. } id="ignore-same" value="0" type="radio" checked name="import-mode">
  993. <label for="ignore-same">跳过重复项</label>
  994. <input class="pxoxq-import-mode" ${
  995. _conf.importMode == 1 ? "checked" : ""
  996. } id="update-same" value="1" type="radio" name="import-mode">
  997. <label for="update-same">覆盖重复项</label>
  998. <button class="frd-import-btn" type="button">导入</button>
  999. </div>
  1000. <div class="pxoxq-setting-item">
  1001. <textarea placeholder="请输入数据..." name="pxoxq-frd-json" id="pxoxq-frd-json" cols="80" rows="10"></textarea>
  1002. </div>
  1003. </div>
  1004. `;
  1005. if (wrap && wrap.length > 0) {
  1006. this.flushConfTab();
  1007. this.flushFrdTab();
  1008. } else {
  1009. const _html = `
  1010. <div id="pxoxq-b-menu" class="pxoxq-hide">
  1011. <div class="pxoxq-tag">:)</div>
  1012. <div class="pxoxq-menu-wrap">
  1013. <div class="pxoxq-menu-col pxoxq-setting-wrap">
  1014. ${leftMenu}
  1015. </div>
  1016. <div class="pxoxq-frd-wrap">
  1017. <div class="pxoxq-right-header">
  1018. <div class="pxoxq-right-title">昵称数据</div>
  1019. <button class="pxoxq-export-frd-btn" type="button">导出当前数据</button>
  1020. </div>
  1021. <div class="pxoxq-tab-wrap">
  1022. <div class="pxoxq-frd-tab">
  1023. <div class="pxoxq-tr pxoxq-thead">
  1024. <div class="pxoxq-cell pxoxq-col-1">ID</div>
  1025. <div class="pxoxq-cell pxoxq-col-2">BilibiliID</div>
  1026. <div class="pxoxq-cell pxoxq-col-3">昵称</div>
  1027. <div class="pxoxq-cell pxoxq-col-4">备注</div>
  1028. <div class="pxoxq-cell pxoxq-col-5">操作</div>
  1029. </div>
  1030. <div class="pxoxq-tbody">
  1031. ${friendTab}
  1032. </div>
  1033. </div>
  1034. </div>
  1035. </div>
  1036. </div>
  1037. </div>
  1038. `;
  1039. $("body").append(_html);
  1040. this.addListener();
  1041. }
  1042. });
  1043. }
  1044.  
  1045. static async genFriendTab() {
  1046. const friends = await BilibiliMemoDB.getAll();
  1047. let _html = "";
  1048.  
  1049. for (const friend of friends) {
  1050. _html += `
  1051. <div class="pxoxq-tr pxoxq-frd-row pxoxq-frd-${friend.id}">
  1052. <div class="pxoxq-cell pxoxq-col-1">${friend.id}</div>
  1053. <div class="pxoxq-cell pxoxq-col-2" title="${friend.bid}">${friend.bid}</div>
  1054. <div class="pxoxq-cell pxoxq-col-3">${friend.bname}</div>
  1055. <div class="pxoxq-cell pxoxq-col-4">
  1056. <input class="pxoxq-memo-ipt pxoxq-memo-ipt-${friend.id}" data-id="${friend.id}" type="text" value="${friend.nick_name}" readonly>
  1057. </div>
  1058. <div class="pxoxq-cell pxoxq-col-5">
  1059. <button class="pxoxq-memo-edit-btn pxoxq-memo-edit-btn-${friend.id}" data-id="${friend.id}" type="button">编辑</button>
  1060. <button class="pxoxq-memo-del-btn" data-id="${friend.id}" type="button">删除</button>
  1061. </div>
  1062. </div>
  1063. `;
  1064. }
  1065. return _html;
  1066. }
  1067.  
  1068. static flushFrdTab() {
  1069. this.genFriendTab().then((_tabHtml) => {
  1070. $("#pxoxq-b-menu .pxoxq-frd-tab .pxoxq-tbody").html(_tabHtml);
  1071. });
  1072. }
  1073.  
  1074. static flushConfTab() {
  1075. ConfigDB.getConf().then((_conf) => {
  1076. const mmRadios = $(".pxoxq-memo-mode");
  1077. for (const item of mmRadios) {
  1078. if (item.value == _conf.memoMode) {
  1079. item.checked = true;
  1080. } else {
  1081. item.checked = false;
  1082. }
  1083. }
  1084. const modeRadios = $(".pxoxq-import-mode");
  1085. for (const item of modeRadios) {
  1086. if (item.value == _conf.memoMode) {
  1087. item.checked = true;
  1088. } else {
  1089. item.checked = false;
  1090. }
  1091. }
  1092. });
  1093. }
  1094.  
  1095. static injectStyle() {
  1096. GM_addStyle(this.menuStyle);
  1097. }
  1098.  
  1099. static addListener() {
  1100. const wrapIdSelector = `#${this.wrapId}`;
  1101.  
  1102. // 面板展开、折叠
  1103. $("body").on(
  1104. "click",
  1105. wrapIdSelector + " .pxoxq-tag",
  1106. pxoDebounce(this.toggleMenuHandler, this.saveDelay)
  1107. );
  1108.  
  1109. // 备注模式选框
  1110. $("body").on(
  1111. "click",
  1112. ".pxoxq-memo-mode",
  1113. pxoDebounce(this.memoModeHandler, this.saveDelay)
  1114. );
  1115.  
  1116. // 导入数据模式
  1117. $("body").on(
  1118. "click",
  1119. ".pxoxq-import-mode",
  1120. pxoDebounce(this.importModeHandler, this.saveDelay)
  1121. );
  1122.  
  1123. // 导入数据
  1124. $("body").on("click", ".frd-import-btn", this.importFriendHandler);
  1125.  
  1126. // 导出数据
  1127. $("body").on(
  1128. "click",
  1129. ".pxoxq-export-frd-btn",
  1130. pxoDebounce(this.exportFrdHandler, this.saveDelay * 2)
  1131. );
  1132.  
  1133. // 双击比编辑
  1134. $("body").on("dblclick", "input.pxoxq-memo-ipt", this.editMemoHandler);
  1135.  
  1136. // 编辑按钮编辑
  1137. $("body").on(
  1138. "click",
  1139. ".pxoxq-memo-edit-btn",
  1140. pxoDebounce(this.editMemoHandler, this.saveDelay)
  1141. );
  1142.  
  1143. // 保存昵称(更新
  1144. $("body").on(
  1145. "click",
  1146. ".pxoxq-memo-save-btn",
  1147. pxoDebounce(this.updateMemoHandler, this.saveDelay)
  1148. );
  1149.  
  1150. // 删除备注
  1151. $("body").on(
  1152. "click",
  1153. ".pxoxq-memo-del-btn",
  1154. pxoDebounce(this.delMemoHandler, this.saveDelay)
  1155. );
  1156. }
  1157.  
  1158. // 折叠、打开面板
  1159. static toggleMenuHandler() {
  1160. $("#pxoxq-b-menu").toggleClass("pxoxq-hide");
  1161. // 刷新面板数据
  1162. if (
  1163. document
  1164. .getElementById("pxoxq-b-menu")
  1165. .classList.value.indexOf("pxoxq-hide") < 0
  1166. ) {
  1167. BMenu.flushConfTab();
  1168. BMenu.flushFrdTab();
  1169. } else {
  1170. }
  1171. }
  1172.  
  1173. static delMemoHandler() {
  1174. const id = parseInt(this.dataset.id);
  1175. const memo = $(".pxoxq-memo-ipt-" + id).val();
  1176. if (confirm(`是否要删除备注【${memo}】?`)) {
  1177. BilibiliMemoDB.delOne(id);
  1178. $(".pxoxq-frd-tab .pxoxq-frd-" + id).remove();
  1179. }
  1180. }
  1181.  
  1182. static updateMemoHandler() {
  1183. const id = this.dataset.id;
  1184. let editBtn = $(".pxoxq-memo-edit-btn-" + id);
  1185. const memoInput = $(".pxoxq-memo-ipt-" + id);
  1186. // 都需编辑按钮复原
  1187. $(editBtn).text("编辑");
  1188. $(editBtn).removeClass("pxoxq-memo-save-btn");
  1189. memoInput[0].readOnly = true;
  1190. $(memoInput).removeClass("active");
  1191. const val = memoInput[0].value;
  1192. BilibiliMemoDB.updateByIdAndMemo(parseInt(id), val);
  1193. }
  1194.  
  1195. static editMemoHandler() {
  1196. const id = this.dataset.id;
  1197. // pxoxq-memo-ipt-2
  1198. let editBtn = $(".pxoxq-memo-edit-btn-" + id);
  1199. const memoInput = $(".pxoxq-memo-ipt-" + id);
  1200. if (!memoInput[0].readOnly) {
  1201. return;
  1202. }
  1203.  
  1204. // 都需要给编辑按钮变个东西
  1205. $(editBtn).text("保存");
  1206. $(editBtn).addClass("pxoxq-memo-save-btn");
  1207.  
  1208. memoInput[0].readOnly = false;
  1209. $(memoInput).addClass("active");
  1210. }
  1211.  
  1212. // 导出数据
  1213. static exportFrdHandler() {
  1214. BilibiliMemoDB.getAll().then((_datas) => {
  1215. const json_str = JSON.stringify(_datas);
  1216. const dataURI =
  1217. "data:text/plain;charset=utf-8," + encodeURIComponent(json_str);
  1218. const link = document.createElement("a");
  1219. link.href = dataURI;
  1220. link.download = `${DateUtils.getCurrDateTimeStr()}.txt`;
  1221. link.click();
  1222. });
  1223. }
  1224.  
  1225. // 导入数据
  1226. static importFriendHandler() {
  1227. const textNode = $("#pxoxq-frd-json");
  1228. const val = $(textNode).val();
  1229. if (!/\S+/.test(val)) return;
  1230. ConfigDB.getConf().then(async (_conf) => {
  1231. try {
  1232. const datas = JSON.parse(val);
  1233. if (Array.isArray(datas)) {
  1234. const ignore_mode = _conf.importMode == 1 ? false : true;
  1235. await BilibiliMemoDB.addOrUpdateMany(datas, ignore_mode);
  1236. BMenu.flushFrdTab();
  1237. alert("导入成功");
  1238. } else {
  1239. throw Error("数据格式错误!");
  1240. }
  1241. } catch (e) {
  1242. alert("导入失败:" + e);
  1243. }
  1244. });
  1245. }
  1246.  
  1247. static importModeHandler() {
  1248. ConfigDB.updateImportMode(this.value);
  1249. }
  1250.  
  1251. static memoModeHandler() {
  1252. MemoGlobalConf.mode = this.value;
  1253. ConfigDB.updateMemoMode(this.value);
  1254. }
  1255. }
  1256. /* =======================================
  1257. 菜单UI部分 结束
  1258. ======================================= */
  1259.  
  1260. /*............................................................................................
  1261. Memo部分 开始
  1262. ............................................................................................*/
  1263. /* =============================================
  1264. 一些配置参数 开始
  1265. =============================================*/
  1266. const memoClassPrefix = "pxo-memo";
  1267. const MemoGlobalConf = {
  1268. mode: 1, // 【模式】 0:昵称替换成备注; 1:昵称(备注); 2:(备注)昵称
  1269. myFriends: [], // 好友信息列表
  1270. memoClassPrefix,
  1271. fansInputBlurDelay: 280, // 输入框防抖延迟
  1272. fansInputBlurTimer: "",
  1273. fansLoopTimer: "",
  1274. memoStyle: `
  1275. .content .be-pager li{
  1276. z-index: 999;
  1277. position: relative;
  1278. }
  1279. .pxo-frd{
  1280. color: #3fb9ffd4;
  1281. font-weight:600;
  1282. letter-spacing: 2px;
  1283. border: 1px solid #ff88a973;
  1284. border-radius: 6px;
  1285. background: #ffa9c1a4;
  1286. margin-top:-2px;
  1287. padding: 2px 5px;}
  1288. .h #h-name {
  1289. background: #ffffffbd;
  1290. padding: 5px 10px;
  1291. border-radius: 6px;
  1292. letter-spacing: 3px;
  1293. line-height: 22px;
  1294. font-size: 20px;
  1295. box-shadow: 1px 1px 2px 2px #ffffff40;
  1296. border: 1px solid #fff;
  1297. color: #e87b99;
  1298. overflow: hidden;
  1299. transition:all .53s linear;
  1300. }
  1301. .h #h-name.hide{
  1302. width:0px;
  1303. padding:0px;
  1304. height:0px;
  1305. border:none;
  1306. }
  1307. .h .homepage-memo-input{
  1308. border: none;
  1309. outline:none;
  1310. overflow:hidden;
  1311. padding: 5px 6px;
  1312. border-bottom:2px solid #ff0808;
  1313. width: 230px;
  1314. font-size: 17px;
  1315. line-height: 22px;
  1316. vertical-align: middle;
  1317. background: #ffffffbd;;
  1318. color: #f74979;
  1319. font-weight:600;
  1320. margin-right: 8px;
  1321. transition:all .53s linear;
  1322. border-radius: 5px 5px 0 0;
  1323. }
  1324. .h .homepage-memo-input.hide{
  1325. width: 0px;
  1326. padding: 0;
  1327. border:none;
  1328.  
  1329. }
  1330. .${memoClassPrefix}-setting-box{
  1331. display: inline-block;
  1332. vertical-align:top;
  1333. margin-top:-2px;
  1334. line-height:20px;
  1335. margin-left:18px;
  1336. }
  1337. .${memoClassPrefix}-setting-box div.btn{
  1338. padding:2px 5px;
  1339. user-select:none;
  1340. display:inline-block;
  1341. overflow: hidden;
  1342. letter-spacing:2px;
  1343. background:#e87b99cc;
  1344. border:none;
  1345. border-radius:5px;
  1346. color:white;
  1347. margin:0 3px;
  1348. transition:all .53s linear;
  1349. }
  1350. .${memoClassPrefix}-setting-box div.btn.hide{
  1351. height: 0px;
  1352. width: 0px;
  1353. opacity: 0.2;
  1354. padding:0px;
  1355. }
  1356. .${memoClassPrefix}-setting-box div.btn:hover{
  1357. box-shadow: 1px 1px 2px 1px #80808024;
  1358. outline: .5px solid #e87b99fc;
  1359.  
  1360. }
  1361. .${memoClassPrefix}-setting-box input{
  1362. border: none;
  1363. outline:none;
  1364. overflow:hidden;
  1365. padding: 2px 3px;
  1366. border-bottom:1px solid #c0c0c0;
  1367. width: 190px;
  1368. font-size: 16px;
  1369. line-height: 18px;
  1370. color: #ff739a;
  1371. font-weight:600;
  1372. vertical-align:top;
  1373. transition:all .25s linear;
  1374. }
  1375. .${memoClassPrefix}-setting-box input.hide{
  1376. width:0px;
  1377. padding:0px;
  1378. }
  1379. `,
  1380. };
  1381. /* =============================================
  1382. 一些配置参数 结束
  1383. =============================================*/
  1384.  
  1385. /* =============================================
  1386. 定制日志输出 开始
  1387. =============================================*/
  1388. class MyLog {
  1389. static prefix = "[BilibiliMemo]";
  1390.  
  1391. static genMsg(msg, type = "") {
  1392. return `${this.prefix} ${type}: ${msg}`;
  1393. }
  1394.  
  1395. static error(msg) {
  1396. console.error(this.genMsg(msg, "error"));
  1397. }
  1398.  
  1399. static warn(msg) {
  1400. console.warn(this.genMsg(msg, "warn"));
  1401. }
  1402.  
  1403. static success(msg) {
  1404. console.info(this.genMsg(msg, "success"));
  1405. }
  1406. static log(msg, ...arg) {
  1407. console.log(this.genMsg(msg), ...arg);
  1408. }
  1409. }
  1410. /* =============================================
  1411. 定制日志输出 结束
  1412. =============================================*/
  1413.  
  1414. /* =============================================
  1415. html 注入部分 开始
  1416. =============================================*/
  1417. class BilibiliMemoInjectoin {
  1418. // 个人主页 替换 以及初始化
  1419. static async injectUserHome(bid) {
  1420. const user = await this.getUserInfoByBid(bid);
  1421. elmGetter.get('#h-name').then(uname => {
  1422. if(!uname) return
  1423. let nickName = uname.innerHTML;
  1424. if(user){
  1425. $(uname).html(this.getNameStr(nickName, user.nick_name));
  1426. $(uname).attr("data-id", user.id);
  1427. }
  1428. $(uname).attr("data-bid", bid);
  1429. $(uname).attr("data-bname", nickName);
  1430. // 添加备注模块
  1431. const inputNode = `<input data-bname="${nickName}" data-bid="${bid}" class='${MemoGlobalConf.memoClassPrefix}-input hide homepage-memo-input'/>`
  1432. $(uname).after(inputNode)
  1433. })
  1434. }
  1435. // 个人主页 替换 更新
  1436. static injectOneHomePage(user) {
  1437. if (user) {
  1438. const nickName = $(".h #h-name").attr("data-bname");
  1439. $("#h-name").html(this.getNameStr(nickName, user.nick_name));
  1440. $("#h-name").attr("data-id", user.id);
  1441. }
  1442. }
  1443.  
  1444. // 个人关注、粉丝页替换 以及初始化
  1445. static injectFanList() {
  1446. elmGetter.each(".relation-list > li > div.content > a", async (user) => {
  1447. try {
  1448. let url = user.href;
  1449. let uid = url.split("/")[3];
  1450. const cPrefix = MemoGlobalConf.memoClassPrefix;
  1451. if (!$(user.children).attr("data-bid")) {
  1452. const userInfo = await this.getUserInfoByBid(uid);
  1453. let nickName = $(user.children).html();
  1454. $(user.children).attr("data-bname", nickName);
  1455. $(user.children).attr("data-bid", uid);
  1456. if (userInfo) {
  1457. $(user.children).html(this.getNameStr(nickName, userInfo.nick_name));
  1458. $(user.children).attr("data-id", userInfo.id);
  1459. $(user).addClass("pxo-frd");
  1460. $(user).addClass("pxo-frd-" + uid);
  1461. }
  1462. // 注入备注模块代码
  1463. const memoBlock = `<div class='${cPrefix}-setting-${uid} ${cPrefix}-setting-box'>
  1464. <input data-bname="${nickName}" data-bid='${uid}' class='${cPrefix}-input-${uid} hide'/>
  1465. <div data-bid='${uid}' class='${cPrefix}-btn-bz btn bz-btn-${uid}'>备注</div>
  1466. <div data-bid='${uid}' class='${cPrefix}-btn-cfm op-btn-${uid} btn cfm-btn-${uid} hide'>确认</div>
  1467. <div data-bid='${uid}' class='${cPrefix}-btn-cancel op-btn-${uid} btn cancel-btn-${uid} hide'>取消</div>
  1468. <div data-bid='${uid}' class='${cPrefix}-btn-del op-btn-${uid} btn del-btn-${uid} hide'>清除备注</div>
  1469. </div>`;
  1470. $(user).after(memoBlock);
  1471. }
  1472. } catch (e) {
  1473. MyLog.error(e);
  1474. }
  1475. });
  1476. }
  1477.  
  1478. // 个人关注、粉丝页替换 单个
  1479. static injectOneFans(user, userANode) {
  1480. if (user && userANode) {
  1481. const nickName = $(userANode.children).attr("data-bname");
  1482. $(userANode.children).html(this.getNameStr(nickName, user.nick_name));
  1483. $(userANode.children).attr("data-id", user.id);
  1484. $(userANode).addClass("pxo-frd");
  1485. $(userANode).addClass("pxo-frd-" + user.bid);
  1486. }
  1487. }
  1488.  
  1489. static replaceMemo(uri) {
  1490. /*
  1491. uri 一共有几种形式:
  1492. https://space.bilibili.com/28563843/fans/follow
  1493. https://space.bilibili.com/28563843/fans/follow?tagid=-1
  1494. https://space.bilibili.com/28563843/fans/fans
  1495. https://space.bilibili.com/472118057/?spm_id_from=333.999.0.0
  1496. 1、换页是页内刷新,需要给页码搞个点击事件
  1497. 2、个人页形式跟其他不太一样
  1498. */
  1499. const uType = this.judgeUri(uri);
  1500. // MyLog.success(`类型是:[${uType}] ${uri}`);
  1501. switch (uType) {
  1502. case "-1":
  1503. MyLog.warn("Uri获取失败");
  1504. break;
  1505. case "+1": //粉丝关注
  1506. BilibiliMemoInjectoin.injectFanList();
  1507. break;
  1508. default: // 个人主页
  1509. BilibiliMemoInjectoin.injectUserHome(uType);
  1510. }
  1511. }
  1512.  
  1513. static judgeUri(uri) {
  1514. /*
  1515. -1 uri为空
  1516. +x +1:粉丝、关注 | +* 后续
  1517. xxxx 纯数字,个人主页
  1518. */
  1519. if (!uri) return "-1";
  1520.  
  1521. const uri_parts = uri.split("/"); // 0-https 1-'' 2-host 3-bid 4-fans/query 5-fans/follow
  1522. // 这是 space 域下的处理,之后可能扩展到其他更多页面模块
  1523. if (uri_parts[2] && "space.bilibili.com" == uri_parts[2]) {
  1524. // 粉丝、关注列表 【归一类,处理方式一样】
  1525. if (
  1526. uri_parts.length > 4 &&
  1527. uri_parts[4] == "fans" &&
  1528. /(?=fans)|(?=follow)/.test(uri_parts[5])
  1529. ) {
  1530. return "+1";
  1531. }
  1532. // 个人主页
  1533. else {
  1534. return uri_parts[3].split("?")[0];
  1535. }
  1536. }
  1537. return "-1";
  1538. }
  1539.  
  1540. // 根据bid获取用户信息 直接从数据库取吧
  1541. static async getUserInfoByBid(bid) {
  1542. const res = await BilibiliMemoDB.getOneByBid(bid);
  1543. return res;
  1544. }
  1545.  
  1546. // 根据昵称、备注获取最终显示名
  1547. static getNameStr(nickName, remark) {
  1548. // span 标签用于判断是否已经替换过
  1549. if (nickName.indexOf("<span>") > 0) {
  1550. return nickName;
  1551. }
  1552. let res = "";
  1553. if (MemoGlobalConf.mode == 1) {
  1554. res = remark;
  1555. } else if (MemoGlobalConf.mode == 2) {
  1556. res = nickName + `(${remark})`;
  1557. } else if (MemoGlobalConf.mode == 3) {
  1558. res = remark + `(${nickName})`;
  1559. }
  1560. return res + "<span>";
  1561. }
  1562.  
  1563. // 注入css样式到头部
  1564. static injectCSS(css) {
  1565. GM_addStyle(css);
  1566. }
  1567. }
  1568. /* =============================================
  1569. html 注入部分 结束
  1570. =============================================*/
  1571.  
  1572. /* =============================================
  1573. 通用函数部分 开始
  1574. =============================================*/
  1575. class BMemoUtils {
  1576. // 关注、粉丝列表页 备注编辑模块 编辑模式 / 正常模式
  1577. static toggleMemoBox(bid, editMode = true) {
  1578. if (editMode) {
  1579. $(`.btn.op-btn-${bid}`).removeClass("hide");
  1580. $(`.${MemoGlobalConf.memoClassPrefix}-input-${bid}`).removeClass("hide");
  1581. $(`.btn.bz-btn-${bid}`).addClass("hide");
  1582. } else {
  1583. $(`.btn.op-btn-${bid}`).addClass("hide");
  1584. $(`.${MemoGlobalConf.memoClassPrefix}-input-${bid}`).addClass("hide");
  1585. $(`.btn.bz-btn-${bid}`).removeClass("hide");
  1586. }
  1587. }
  1588.  
  1589. // 个人主页 编辑模式 / 正常模式
  1590. static toggleUserHomeEditMode(editMode = true) {
  1591. if (editMode) {
  1592. $(".h #h-name").addClass("hide");
  1593. $(".homepage-memo-input").removeClass("hide");
  1594. } else {
  1595. $(".h #h-name").removeClass("hide");
  1596. $(".homepage-memo-input").addClass("hide");
  1597. }
  1598. }
  1599.  
  1600. // 个人空间主页 编辑模式初始化
  1601. static homePageEditModeHandler(bid) {
  1602. this.toggleUserHomeEditMode();
  1603. const inputNode = $(".homepage-memo-input")[0];
  1604. const bName = $(inputNode).attr("data-bname");
  1605. $(inputNode).focus();
  1606. BilibiliMemoDB.getOneByBid(bid).then((user) => {
  1607. if (user) {
  1608. $(inputNode).val(user.nick_name);
  1609. } else {
  1610. $(inputNode).val(bName);
  1611. }
  1612. });
  1613. }
  1614.  
  1615. // 个人空间主页 编辑确认
  1616. static homePageSetMemoHandler(bid) {
  1617. const inputNode = $(".homepage-memo-input")[0];
  1618. const bName = $(inputNode).attr("data-bname");
  1619. const val = $(inputNode).val();
  1620. const val_reg = /\S.*\S/;
  1621. if (val && val_reg.test(val)) {
  1622. const memo = val_reg.exec(val)[0];
  1623. BilibiliMemoDB.getOneByBid(bid).then(async (user) => {
  1624. if (user) {
  1625. if (memo != user.nick_name) {
  1626. user.nick_name = memo;
  1627. user.bname = bName;
  1628. await BilibiliMemoDB.updateOne(user);
  1629. BilibiliMemoInjectoin.injectOneHomePage(user);
  1630. }
  1631. this.toggleUserHomeEditMode(false);
  1632. } else {
  1633. if (memo != bName) {
  1634. user = {
  1635. bid,
  1636. nick_name: memo,
  1637. bname: bName,
  1638. };
  1639. await BilibiliMemoDB.addOne(user);
  1640. user = await BilibiliMemoDB.getOneByBid(bid);
  1641. BilibiliMemoInjectoin.injectOneHomePage(user);
  1642. }
  1643. this.toggleUserHomeEditMode(false);
  1644. }
  1645. });
  1646. }
  1647. }
  1648.  
  1649. // 删除备注
  1650. static delMemoHandler(bid) {
  1651. BilibiliMemoDB.getOneByBid(bid).then(async (_item) => {
  1652. if (_item) {
  1653. if (confirm(`是否删除备注【${_item.nick_name}】?`)) {
  1654. await BilibiliMemoDB.delOne(_item.id);
  1655. $("a.pxo-frd-" + bid).removeClass("pxo-frd");
  1656. const nameSpan = $("a.pxo-frd-" + bid + " span.fans-name");
  1657. $(nameSpan).text(nameSpan[0].dataset.bname);
  1658. }
  1659. }
  1660. });
  1661. }
  1662.  
  1663. // 粉丝、关注页 编辑模式初始化
  1664. static editModeHandler(bid) {
  1665. const inputNode = $(`.${MemoGlobalConf.memoClassPrefix}-input-${bid}`)[0];
  1666. BilibiliMemoDB.getOneByBid(bid).then((user) => {
  1667. const val = $(inputNode).val();
  1668. if (!/\S+/.test(val)) {
  1669. if (user) {
  1670. $(inputNode).val(user.nick_name);
  1671. } else {
  1672. $(inputNode).val($(inputNode).attr("data-bname"));
  1673. }
  1674. }
  1675. });
  1676. this.toggleMemoBox(bid);
  1677. $(inputNode).focus();
  1678. }
  1679.  
  1680. // 粉丝、关注页编辑确认
  1681. static setMemoHandler(bid) {
  1682. const inputNode = $(`.${MemoGlobalConf.memoClassPrefix}-input-${bid}`)[0];
  1683. const val = $(inputNode).val();
  1684. const val_reg = /\S.*\S/;
  1685. const bName = $(inputNode).attr("data-bname");
  1686. if (val_reg.test(val)) {
  1687. const memo = val_reg.exec(val)[0];
  1688. const userANode = $(inputNode).parent().siblings("a")[0];
  1689. BilibiliMemoInjectoin.getUserInfoByBid(bid).then(async (user) => {
  1690. if (user) {
  1691. if (user.nick_name != memo) {
  1692. user.nick_name = memo;
  1693. user.bname = bName;
  1694. await BilibiliMemoDB.updateOne(user);
  1695. BilibiliMemoInjectoin.injectOneFans(user, userANode);
  1696. }
  1697. this.toggleMemoBox(bid, false);
  1698. } else {
  1699. if (memo != bName) {
  1700. user = {
  1701. bid,
  1702. nick_name: memo,
  1703. bname: bName,
  1704. };
  1705. await BilibiliMemoDB.addOne(user);
  1706. user = await BilibiliMemoDB.getOneByBid(bid);
  1707. BilibiliMemoInjectoin.injectOneFans(user, userANode);
  1708. }
  1709. this.toggleMemoBox(bid, false);
  1710. }
  1711. });
  1712. }
  1713. }
  1714. }
  1715. /* =============================================
  1716. 通用函数部分 结束
  1717. =============================================*/
  1718. /*-----------------初始化 开始-----------------*/
  1719. async function BilibiliMemoInit() {
  1720. // 注入样式
  1721. BilibiliMemoInjectoin.injectCSS(MemoGlobalConf.memoStyle);
  1722.  
  1723. // 个人主页双击修改事件
  1724. $('body').on(
  1725. 'dblclick',
  1726. `.h #h-name`,
  1727. function(event){
  1728. const bid = event.currentTarget.dataset.bid;
  1729. BMemoUtils.homePageEditModeHandler(bid)
  1730. }
  1731. )
  1732. // 个人主页搜索框失去焦点事件
  1733. $('body').on(
  1734. 'focusout',
  1735. '.homepage-memo-input',
  1736. function(event){
  1737. const bid = event.currentTarget.dataset.bid;
  1738. BMemoUtils.homePageSetMemoHandler(bid)
  1739. }
  1740. )
  1741. // 粉丝、关注页 备注按钮点击事件:
  1742. $("body").on(
  1743. "click",
  1744. `.${MemoGlobalConf.memoClassPrefix}-setting-box div.${MemoGlobalConf.memoClassPrefix}-btn-bz`,
  1745. function (event) {
  1746. const bid = event.currentTarget.dataset.bid;
  1747. BMemoUtils.editModeHandler(bid)
  1748. }
  1749. );
  1750. // 删除备注按钮点击事件
  1751. $("body").on(
  1752. "click",
  1753. `.${MemoGlobalConf.memoClassPrefix}-setting-box div.${MemoGlobalConf.memoClassPrefix}-btn-del`,
  1754. function (event) {
  1755. const bid = event.currentTarget.dataset.bid;
  1756. BMemoUtils.delMemoHandler(bid)
  1757. }
  1758. )
  1759. // 粉丝、关注页确认按钮点击事件
  1760. $("body").on(
  1761. "click",
  1762. `.${MemoGlobalConf.memoClassPrefix}-setting-box .${MemoGlobalConf.memoClassPrefix}-btn-cfm`,
  1763. function (event) {
  1764. clearTimeout(MemoGlobalConf.fansInputBlurTimer)
  1765. const bid = event.currentTarget.dataset.bid;
  1766. BMemoUtils.setMemoHandler(bid)
  1767. }
  1768. );
  1769. // 粉丝、关注页取消按钮点击事件
  1770. $("body").on(
  1771. "click",
  1772. `.${MemoGlobalConf.memoClassPrefix}-setting-box .${MemoGlobalConf.memoClassPrefix}-btn-cancel`,
  1773. function (event) {
  1774. clearTimeout(MemoGlobalConf.fansInputBlurTimer)
  1775. const bid = event.currentTarget.dataset.bid;
  1776. BMemoUtils.toggleMemoBox(bid, false)
  1777. })
  1778. // 粉丝、关注页输入框市区焦点事件
  1779. $("body").on(
  1780. "focusout",
  1781. `.${MemoGlobalConf.memoClassPrefix}-setting-box input`,
  1782. function (event) {
  1783. clearTimeout(MemoGlobalConf.fansInputBlurTimer)
  1784. MemoGlobalConf.fansInputBlurTimer = setTimeout(()=>{
  1785. const bid = event.currentTarget.dataset.bid;
  1786. BMemoUtils.toggleMemoBox(bid, false)
  1787. }, MemoGlobalConf.fansInputBlurDelay)
  1788. })
  1789. }
  1790. /*-----------------初始化 结束-----------------*/
  1791.  
  1792. /*........................................................................................................................................
  1793. Memo部分 结束
  1794. ........................................................................................................................................*/
  1795.  
  1796. async function flushConf() {
  1797. const _conf = await ConfigDB.getConf();
  1798. MemoGlobalConf.mode = _conf.memoMode;
  1799. return true;
  1800. }
  1801.  
  1802. /*+++++++++++++++++++++++++++++++++++++
  1803. 主程序初始化 开始
  1804. +++++++++++++++++++++++++++++++++++++*/
  1805. async function bilibiliCustomInit() {
  1806. if (!MyIndexedDB.getDBVersion()) {
  1807. await DBInit.initAllDB();
  1808. }
  1809. // 从数据库获取数据,刷新配置参数
  1810. await flushConf();
  1811. BMenu.init();
  1812. if (MemoGlobalConf.mode == 0) return;
  1813. const uri = window.location.href;
  1814. BilibiliMemoInit().then((r) => {
  1815. BilibiliMemoInjectoin.replaceMemo(uri);
  1816. });
  1817. }
  1818. /*+++++++++++++++++++++++++++++++++++++
  1819. 主程序初始化 结束
  1820. +++++++++++++++++++++++++++++++++++++*/
  1821.  
  1822.  
  1823. function toNewOne(){
  1824. const newScriptUrl = 'https://scriptcat.org/zh-CN/script-show-page/3059'
  1825. let timeDiff = 2 * 24 * 60 * 60 * 1e3
  1826. const never = localStorage.getItem('neverShow')
  1827. let neverEnd = localStorage.getItem('neverEndTime')
  1828. const curr = new Date().getTime()
  1829. neverEnd = Number(neverEnd)
  1830. if(never == 1 && neverEnd && curr - neverEnd < timeDiff){
  1831. return
  1832. }
  1833. const dog = document.createElement('dialog')
  1834. dog.style.cssText = `border:none;border-radius:8px;padding:18px;border: 5px solid #E16689;position:fixed;top: 20vh;margin: 0 auto;`
  1835. document.body.appendChild(dog)
  1836. const h = document.createElement('h2')
  1837. h.style.cssText = `color:#E16689;text-align:center;`
  1838. dog.appendChild(h)
  1839. h.innerText = 'B站备注 -- 全新版本来啦!!!!'
  1840. const content = `<div style="font-size: 18px;line-height: 40px;">新版本已完成适配,支持导入这个版本导出的数据(可以从这个版本导出数据,然后导入到新版本)。<br>
  1841. 迁移完数据后,可以卸载当前版本,只保留新版本。<br>
  1842. 新版本在这里安装:<a target="_blank" style="color:blue;outline:none;" href="${newScriptUrl}">${newScriptUrl}</a>
  1843. </div>`
  1844. dog.insertAdjacentHTML('beforeend', content)
  1845. const btnD = document.createElement('div')
  1846. dog.appendChild(btnD)
  1847. const cfm = document.createElement('button')
  1848. const neverShow = document.createElement('button')
  1849. btnD.appendChild(cfm)
  1850. btnD.appendChild(neverShow)
  1851. btnD.style.cssText = `text-align: right;`
  1852. cfm.innerText = '已知晓'
  1853. neverShow.innerText = '不再展示'
  1854. cfm.style.cssText = `margin-left: 20px;color:white;font-weight:600;font-size: 18px;border:2px solid pink;outline:none;background: #E16689;border-radius:4px;padding: 8px 10px;`
  1855. neverShow.style.cssText= cfm.style.cssText
  1856. dog.showModal()
  1857. cfm.addEventListener('click', function(){
  1858. dog.close()
  1859. })
  1860. neverShow.addEventListener('click', function(){
  1861. dog.close()
  1862. localStorage.setItem('neverShow', 1)
  1863. localStorage.setItem('neverEndTime', new Date().getTime())
  1864. })
  1865. }
  1866.  
  1867. (function () {
  1868. toNewOne()
  1869. bilibiliCustomInit().then((res) => {
  1870. console.log("init over");
  1871. });
  1872. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址