Vecorder

直播间内容记录 https://github.com/Xinrea/Vecorder

  1. // ==UserScript==
  2. // @name Vecorder
  3. // @namespace https://www.joi-club.cn/
  4. // @version 1.0.0
  5. // @description 直播间内容记录 https://github.com/Xinrea/Vecorder
  6. // @author Xinrea
  7. // @license MIT
  8. // @match https://live.bilibili.com/*
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_deleteValue
  12. // @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
  13. // @require https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.min.js
  14. // @run-at document-end
  15. // ==/UserScript==
  16.  
  17. // IndexedDB 存储管理
  18. class VecorderStorage {
  19. constructor() {
  20. this.dbName = "VecorderDB";
  21. this.dbVersion = 1;
  22. this.storeName = "vecorder_data";
  23. this.db = null;
  24. this.isInitialized = false;
  25. }
  26.  
  27. // 初始化数据库
  28. async init() {
  29. return new Promise((resolve, reject) => {
  30. const request = indexedDB.open(this.dbName, this.dbVersion);
  31.  
  32. request.onerror = () => {
  33. console.error("Vecorder: IndexedDB 打开失败:", request.error);
  34. reject(request.error);
  35. };
  36.  
  37. request.onsuccess = () => {
  38. this.db = request.result;
  39. this.isInitialized = true;
  40. console.log("Vecorder: IndexedDB 初始化成功");
  41. resolve();
  42. };
  43.  
  44. request.onupgradeneeded = (event) => {
  45. const db = event.target.result;
  46.  
  47. // 创建对象存储
  48. if (!db.objectStoreNames.contains(this.storeName)) {
  49. const store = db.createObjectStore(this.storeName, {
  50. keyPath: "key",
  51. });
  52. console.log("Vecorder: 创建 IndexedDB 存储");
  53. }
  54. };
  55. });
  56. }
  57.  
  58. // 获取数据
  59. async get(key, defaultValue = null) {
  60. if (!this.isInitialized) {
  61. await this.init();
  62. }
  63.  
  64. return new Promise((resolve, reject) => {
  65. const transaction = this.db.transaction([this.storeName], "readonly");
  66. const store = transaction.objectStore(this.storeName);
  67. const request = store.get(key);
  68.  
  69. request.onerror = () => {
  70. console.error("Vecorder: 获取数据失败:", request.error);
  71. reject(request.error);
  72. };
  73.  
  74. request.onsuccess = () => {
  75. const result = request.result;
  76. if (result) {
  77. resolve(result.value);
  78. } else {
  79. resolve(defaultValue);
  80. }
  81. };
  82. });
  83. }
  84.  
  85. // 设置数据
  86. async set(key, value) {
  87. if (!this.isInitialized) {
  88. await this.init();
  89. }
  90.  
  91. return new Promise((resolve, reject) => {
  92. const transaction = this.db.transaction([this.storeName], "readwrite");
  93. const store = transaction.objectStore(this.storeName);
  94. const request = store.put({ key, value });
  95.  
  96. request.onerror = () => {
  97. console.error("Vecorder: 设置数据失败:", request.error);
  98. reject(request.error);
  99. };
  100.  
  101. request.onsuccess = () => {
  102. console.log("Vecorder: 数据保存成功:", key);
  103. resolve();
  104. };
  105. });
  106. }
  107.  
  108. // 删除数据
  109. async delete(key) {
  110. if (!this.isInitialized) {
  111. await this.init();
  112. }
  113.  
  114. return new Promise((resolve, reject) => {
  115. const transaction = this.db.transaction([this.storeName], "readwrite");
  116. const store = transaction.objectStore(this.storeName);
  117. const request = store.delete(key);
  118.  
  119. request.onerror = () => {
  120. console.error("Vecorder: 删除数据失败:", request.error);
  121. reject(request.error);
  122. };
  123.  
  124. request.onsuccess = () => {
  125. console.log("Vecorder: 数据删除成功:", key);
  126. resolve();
  127. };
  128. });
  129. }
  130.  
  131. // 获取存储空间信息
  132. async getStorageInfo() {
  133. if (!this.isInitialized) {
  134. await this.init();
  135. }
  136.  
  137. return new Promise((resolve, reject) => {
  138. try {
  139. // 获取已用空间
  140. const transaction = this.db.transaction([this.storeName], "readonly");
  141. const store = transaction.objectStore(this.storeName);
  142. const request = store.getAll();
  143.  
  144. request.onerror = () => {
  145. console.error("Vecorder: 获取存储信息失败:", request.error);
  146. reject(request.error);
  147. };
  148.  
  149. request.onsuccess = () => {
  150. const data = request.result;
  151. let usedSize = 0;
  152.  
  153. // 计算已用空间
  154. data.forEach((item) => {
  155. usedSize += JSON.stringify(item).length;
  156. });
  157.  
  158. // 获取浏览器存储配额信息
  159. if ("storage" in navigator && "estimate" in navigator.storage) {
  160. navigator.storage
  161. .estimate()
  162. .then((estimate) => {
  163. const totalSpace = estimate.quota || 0;
  164. const availableSpace = estimate.usage || 0;
  165. const remainingSpace = totalSpace - availableSpace;
  166.  
  167. resolve({
  168. usedSize: usedSize,
  169. totalSpace: totalSpace,
  170. availableSpace: availableSpace,
  171. remainingSpace: remainingSpace,
  172. dataCount: data.length,
  173. });
  174. })
  175. .catch((error) => {
  176. console.error("Vecorder: 获取存储配额失败:", error);
  177. resolve({
  178. usedSize: usedSize,
  179. totalSpace: 0,
  180. availableSpace: 0,
  181. remainingSpace: 0,
  182. dataCount: data.length,
  183. });
  184. });
  185. } else {
  186. // 如果不支持 storage.estimate,只返回已用空间
  187. resolve({
  188. usedSize: usedSize,
  189. totalSpace: 0,
  190. availableSpace: 0,
  191. remainingSpace: 0,
  192. dataCount: data.length,
  193. });
  194. }
  195. };
  196. } catch (error) {
  197. console.error("Vecorder: 获取存储信息时出错:", error);
  198. reject(error);
  199. }
  200. });
  201. }
  202.  
  203. // 格式化字节大小
  204. formatBytes(bytes) {
  205. if (bytes === 0) return "0 B";
  206. const k = 1024;
  207. const sizes = ["B", "KB", "MB", "GB"];
  208. const i = Math.floor(Math.log(bytes) / Math.log(k));
  209. return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  210. }
  211.  
  212. // 检查是否有 GM 数据需要迁移
  213. async checkAndMigrateData() {
  214. try {
  215. // 检查是否有 GM 数据
  216. const gmData = GM_getValue(dbname, null);
  217. const gmOptions = GM_getValue("vop", null);
  218.  
  219. if (gmData || gmOptions) {
  220. console.log("Vecorder: 检测到 GM 数据,开始迁移到 IndexedDB");
  221.  
  222. // 迁移主数据
  223. if (gmData) {
  224. await this.set(dbname, gmData);
  225. GM_deleteValue(dbname);
  226. console.log("Vecorder: 主数据迁移完成");
  227. }
  228.  
  229. // 迁移选项数据
  230. if (gmOptions) {
  231. await this.set("vop", gmOptions);
  232. GM_deleteValue("vop");
  233. console.log("Vecorder: 选项数据迁移完成");
  234. }
  235.  
  236. console.log("Vecorder: 数据迁移完成,已清空 GM 存储");
  237. return true;
  238. }
  239.  
  240. return false;
  241. } catch (error) {
  242. console.error("Vecorder: 数据迁移失败:", error);
  243. return false;
  244. }
  245. }
  246. }
  247.  
  248. // 创建存储实例
  249. const vecorderStorage = new VecorderStorage();
  250.  
  251. // 全局更新存储信息函数
  252. async function updateStorageInfo() {
  253. try {
  254. if (!window.vecorderUseGMStorage) {
  255. const storageInfo = await vecorderStorage.getStorageInfo();
  256. let infoText = `已用: ${vecorderStorage.formatBytes(
  257. storageInfo.usedSize
  258. )} | 数据: ${storageInfo.dataCount}`;
  259.  
  260. if (storageInfo.totalSpace > 0) {
  261. infoText += ` | 剩余: ${vecorderStorage.formatBytes(
  262. storageInfo.remainingSpace
  263. )}`;
  264. }
  265.  
  266. $("#vecorder-storage-info").html(`
  267. <div class="storage-info-item">
  268. <span class="storage-value">${infoText}</span>
  269. </div>
  270. `);
  271. } else {
  272. $("#vecorder-storage-info").html(`
  273. <div class="storage-info-item">
  274. <span class="storage-value">GM 存储模式</span>
  275. </div>
  276. `);
  277. }
  278. } catch (error) {
  279. console.error("Vecorder: 更新存储信息失败:", error);
  280. $("#vecorder-storage-info").html(`
  281. <div class="storage-info-item">
  282. <span class="storage-value">存储信息获取失败</span>
  283. </div>
  284. `);
  285. }
  286. }
  287.  
  288. // 存储适配器,根据情况选择使用 IndexedDB 还是 GM 存储
  289. const storageAdapter = {
  290. async set(key, value) {
  291. if (window.vecorderUseGMStorage) {
  292. GM_setValue(key, value);
  293. return Promise.resolve();
  294. } else {
  295. return vecorderStorage.set(key, value);
  296. }
  297. },
  298.  
  299. async get(key, defaultValue = null) {
  300. if (window.vecorderUseGMStorage) {
  301. const value = GM_getValue(key, defaultValue);
  302. return Promise.resolve(value);
  303. } else {
  304. return vecorderStorage.get(key, defaultValue);
  305. }
  306. },
  307.  
  308. async delete(key) {
  309. if (window.vecorderUseGMStorage) {
  310. GM_deleteValue(key);
  311. return Promise.resolve();
  312. } else {
  313. return vecorderStorage.delete(key);
  314. }
  315. },
  316. };
  317.  
  318. // 立即执行的调试信息
  319. console.log("Vecorder: 脚本开始加载");
  320. console.log(
  321. "Vecorder: jQuery版本:",
  322. typeof $ !== "undefined" ? $.fn.jquery : "未加载"
  323. );
  324. console.log(
  325. "Vecorder: Moment版本:",
  326. typeof moment !== "undefined" ? moment.version : "未加载"
  327. );
  328. console.log("Vecorder: 当前页面:", window.location.href);
  329. vlog("脚本已加载,版本 0.70");
  330.  
  331. function vlog(msg) {
  332. console.log("[Vecorder]" + msg);
  333. }
  334.  
  335. function p(msg) {
  336. return {
  337. time: new Date().getTime(),
  338. content: msg,
  339. };
  340. }
  341.  
  342. // 根据当前地址获取直播间ID
  343. function getRoomID() {
  344. let roomid = window.location.pathname.substring(1);
  345. return roomid; //获取当前房间号
  346. }
  347.  
  348. var dbname = "vdb" + getRoomID();
  349.  
  350. // 初始化数据存储
  351. let db = [];
  352. let Option = { reltime: false, toffset: 0 };
  353.  
  354. // 异步初始化数据
  355. async function initializeData() {
  356. try {
  357. // 检查并迁移数据
  358. const migrated = await vecorderStorage.checkAndMigrateData();
  359.  
  360. // 从 IndexedDB 加载数据
  361. const dbData = await vecorderStorage.get(dbname, "[]");
  362. const optionsData = await vecorderStorage.get(
  363. "vop",
  364. '{"reltime":false,"toffset":0}'
  365. );
  366.  
  367. db = JSON.parse(dbData);
  368. Option = JSON.parse(optionsData);
  369.  
  370. console.log("Vecorder: 数据初始化完成");
  371. if (migrated) {
  372. console.log("Vecorder: 数据已从 GM 存储迁移到 IndexedDB");
  373. }
  374. } catch (error) {
  375. console.error("Vecorder: IndexedDB 初始化失败,回退到 GM 存储:", error);
  376. // 如果 IndexedDB 失败,回退到 GM 存储
  377. db = JSON.parse(GM_getValue(dbname, "[]"));
  378. Option = JSON.parse(GM_getValue("vop", '{"reltime":false,"toffset":0}'));
  379.  
  380. // 标记使用 GM 存储模式
  381. window.vecorderUseGMStorage = true;
  382. console.log("Vecorder: 已切换到 GM 存储模式");
  383. }
  384. }
  385.  
  386. // 数据初始化将在 DOM ready 时进行
  387.  
  388. function nindexOf(n) {
  389. for (let i in db) {
  390. if (!db[i].del && db[i].name == n) return i;
  391. }
  392. return -1;
  393. }
  394.  
  395. function tindexOf(id, t) {
  396. for (let i in db[id].lives) {
  397. if (!db[id].lives[i].del && db[id].lives[i].title == t) return i;
  398. }
  399. return -1;
  400. }
  401.  
  402. async function gc() {
  403. for (let i = db.length - 1; i >= 0; i--) {
  404. if (db[i].del) {
  405. db.splice(i, 1);
  406. continue;
  407. }
  408. for (let j = db[i].lives.length - 1; j >= 0; j--) {
  409. if (db[i].lives[j].del) {
  410. db[i].lives.splice(j, 1);
  411. continue;
  412. }
  413. }
  414. }
  415. await storageAdapter.set(dbname, JSON.stringify(db));
  416. }
  417.  
  418. async function addPoint(t, msg) {
  419. console.log("addPoint", t, msg);
  420. let ltime = t * 1000;
  421. if (ltime == 0) return;
  422. let [name, link, title] = getRoomInfo();
  423. console.log("CurrentRoom:", name, link, title);
  424. let id = nindexOf(name);
  425. if (id == -1) {
  426. db.push({
  427. name: name,
  428. link: link,
  429. del: false,
  430. lives: [
  431. {
  432. title: title,
  433. time: ltime,
  434. del: false,
  435. points: [p(msg)],
  436. },
  437. ],
  438. });
  439. } else {
  440. let lid = tindexOf(id, title);
  441. if (lid == -1) {
  442. db[id].lives.push({
  443. title: title,
  444. time: ltime,
  445. points: [p(msg)],
  446. });
  447. } else {
  448. db[id].lives[lid].points.push(p(msg));
  449. }
  450. }
  451. await storageAdapter.set(dbname, JSON.stringify(db));
  452. $(`#vecorder-list`).replaceWith(dbToListview());
  453. // 更新存储信息
  454. if (toggle) {
  455. updateStorageInfo();
  456. }
  457. }
  458.  
  459. function getMsg(body) {
  460. var vars = body.split("&");
  461. for (var i = 0; i < vars.length; i++) {
  462. var pair = vars[i].split("=");
  463. if (pair[0] == "msg") {
  464. return decodeURI(pair[1]);
  465. }
  466. }
  467. return false;
  468. }
  469.  
  470. function getRoomInfo() {
  471. let resp = $.ajax({
  472. url:
  473. "https://api.live.bilibili.com/xlive/web-room/v1/index/getH5InfoByRoom?room_id=" +
  474. getRoomID(),
  475. async: false,
  476. }).responseJSON.data;
  477. console.log("RoomInfo:", resp);
  478. return [
  479. resp.anchor_info.base_info.uname,
  480. "https://space.bilibili.com/" + resp.room_info.uid,
  481. resp.room_info.title,
  482. ];
  483. }
  484.  
  485. function tryAddPoint(msg) {
  486. // https://api.live.bilibili.com/room/v1/Room/room_init?id={roomID}
  487. console.log(msg, getRoomID());
  488. $.ajax({
  489. url:
  490. "https://api.live.bilibili.com/room/v1/Room/room_init?id=" + getRoomID(),
  491. async: true,
  492. success: function (resp) {
  493. let t = 0;
  494. if (resp.data.live_status != 1) t = 0;
  495. else t = resp.data.live_time;
  496. addPoint(t, msg).catch((error) => {
  497. console.error("Vecorder: 添加时间点失败:", error);
  498. });
  499. },
  500. });
  501. }
  502.  
  503. let toggle = false;
  504.  
  505. // 确保DOM加载完成后再执行
  506. $(document).ready(async function () {
  507. console.log("Vecorder: DOM已加载完成");
  508. vlog("开始初始化Vecorder功能");
  509.  
  510. // 等待数据初始化完成
  511. try {
  512. await initializeData();
  513. console.log("Vecorder: 数据初始化完成,开始检查页面元素");
  514. } catch (error) {
  515. console.error("Vecorder: 数据初始化失败:", error);
  516. }
  517.  
  518. // 延迟检查页面元素
  519. setTimeout(function () {
  520. console.log("Vecorder: 延迟检查页面元素");
  521. console.log(
  522. "Vecorder: control-panel-ctnr-box 存在:",
  523. $("#control-panel-ctnr-box").length > 0
  524. );
  525. console.log(
  526. "Vecorder: bottom-actions 存在:",
  527. $(".bottom-actions").length > 0
  528. );
  529. console.log(
  530. "Vecorder: bottom-actions 存在:",
  531. $(".bottom-actions").length > 0
  532. );
  533. }, 2000);
  534. });
  535.  
  536. // 尝试多个可能的选择器来插入时间点输入框
  537. function insertTimeInput() {
  538. const selectors = [
  539. "#control-panel-ctnr-box > div.bottom-actions.p-relative",
  540. "#control-panel-ctnr-box .bottom-actions",
  541. ".bottom-actions",
  542. "#control-panel-ctnr-box",
  543. ];
  544.  
  545. for (let selector of selectors) {
  546. if ($(selector).length > 0) {
  547. console.log(`Vecorder: 使用选择器 ${selector} 插入时间点输入框`);
  548.  
  549. // 创建时间点输入框容器
  550. const inputContainer = $(
  551. `<div id="vecorder-input-container">
  552. <div id="point-input">
  553. <textarea placeholder="输入内容并回车添加时间点"></textarea>
  554. </div>
  555. </div>`
  556. );
  557.  
  558. inputContainer
  559. .find("#point-input > textarea")
  560. .bind("keypress", function (event) {
  561. if (event.keyCode == "13") {
  562. window.event.returnValue = false;
  563. console.log("Enter detected");
  564. tryAddPoint($("#point-input > textarea").val());
  565. $("#point-input > textarea").val("");
  566. }
  567. });
  568.  
  569. // 尝试插入到不同的位置
  570. const targetSelectors = [
  571. "#control-panel-ctnr-box > div.bottom-actions.p-relative",
  572. ".bottom-actions",
  573. "#control-panel-ctnr-box",
  574. ];
  575.  
  576. for (let targetSelector of targetSelectors) {
  577. if ($(targetSelector).length > 0) {
  578. $(targetSelector).append(inputContainer);
  579. console.log(`Vecorder: 时间点输入框已插入到 ${targetSelector}`);
  580.  
  581. // 在同一个元素上插入记录按钮和面板
  582. insertRecordButton();
  583. return true; // 返回true表示成功插入,停止检查
  584. }
  585. }
  586. }
  587. }
  588. return false; // 如果没有找到合适的元素,返回false
  589. }
  590.  
  591. console.log("Vecorder: 开始等待聊天输入区域元素");
  592. waitForKeyElements(
  593. "#control-panel-ctnr-box > div.bottom-actions.p-relative",
  594. insertTimeInput,
  595. true // 只执行一次
  596. );
  597.  
  598. // 尝试多个可能的选择器来插入记录按钮和面板
  599. function insertRecordButton() {
  600. console.log("Vecorder: 开始插入记录按钮和面板");
  601.  
  602. // 直接使用当前找到的元素
  603. const n = $("#control-panel-ctnr-box > div.bottom-actions.p-relative");
  604. if (n.length > 0) {
  605. console.log(`Vecorder: 使用当前元素插入记录按钮和面板`);
  606.  
  607. // 尝试调整现有按钮的样式
  608. try {
  609. // 这里可以添加对现有按钮的调整,如果需要的话
  610. console.log("Vecorder: 找到目标元素,准备插入记录按钮");
  611. } catch (e) {
  612. console.log("Vecorder: 调整按钮样式时出错,继续执行", e);
  613. }
  614.  
  615. // 这里可以添加其他初始化代码,如果需要的话
  616.  
  617. // create panel
  618. let panel = $(
  619. '<div id="vPanel"><p class="vecorder-title">🍊 直播笔记</p></div>'
  620. );
  621.  
  622. // 添加存储空间信息区域
  623. let storageInfo = $(
  624. '<div id="vecorder-storage-info" class="vecorder-storage-info"></div>'
  625. );
  626. panel.append(storageInfo);
  627.  
  628. let contentList = dbToListview();
  629. panel.append(contentList);
  630.  
  631. // 创建底部操作区域
  632. let bottomActions = $('<div class="vecorder-bottom-actions"></div>');
  633.  
  634. // 创建设置按钮和折叠面板
  635. let settingsBtn = $(
  636. '<button class="vecorder-settings-btn" title="导出设置">⚙️</button>'
  637. );
  638. let settingsPanel =
  639. $(`<div class="vecorder-settings-panel" style="display: none;">
  640. <div class="timeop-item">
  641. <input type="checkbox" id="reltime" value="false"/>
  642. <label for="reltime">按相对时间导出</label>
  643. </div>
  644. <div class="timeop-item">
  645. <label for="toffset">时间偏移(秒):</label>
  646. <input type="number" id="toffset" value="${Option.toffset}"/>
  647. </div>
  648. </div>`);
  649.  
  650. settingsBtn.click(function () {
  651. console.log("Vecorder: 设置按钮被点击");
  652. settingsPanel.slideToggle(200);
  653. console.log("Vecorder: 设置面板可见性:", settingsPanel.is(":visible"));
  654. });
  655.  
  656. // 创建清空按钮
  657. let clearBtn = $(
  658. '<button class="vecorder-clear-btn" title="清空所有数据">🗑️</button>'
  659. );
  660. clearBtn.click(function () {
  661. if (confirm("确定要清空所有直播笔记吗?此操作不可恢复。")) {
  662. db = [];
  663. storageAdapter.delete(dbname).catch((error) => {
  664. console.error("Vecorder: 清空数据失败:", error);
  665. });
  666. // 重新生成列表并更新显示
  667. $(`#vecorder-list`).replaceWith(dbToListview());
  668. // 更新存储信息
  669. updateStorageInfo();
  670. }
  671. });
  672.  
  673. // 添加按钮到底部操作区域
  674. bottomActions.append(settingsBtn);
  675. bottomActions.append(clearBtn);
  676. panel.append(bottomActions);
  677.  
  678. // 将设置面板添加到主面板中
  679. panel.append(settingsPanel);
  680.  
  681. let closeBtn = $('<a class="vecorder-close-btn">&times;</a>');
  682. closeBtn.click(function () {
  683. console.log("Close clicked");
  684. $("#vPanel").hide();
  685. gc().catch((error) => {
  686. console.error("Vecorder: 保存数据失败:", error);
  687. });
  688. toggle = false;
  689. recordBtn.removeClass("vecorder-record-btn-active");
  690. // 更新存储信息
  691. updateStorageInfo();
  692. });
  693. panel.append(closeBtn);
  694.  
  695. // Setup recordButton
  696. let recordBtn = $('<div><span class="txt">记录</span></div>');
  697. recordBtn.addClass("vecorder-record-btn");
  698.  
  699. // 将面板插入到body中,确保正确的定位
  700. $("body").append(panel);
  701. $("#vPanel").hide();
  702. console.log("Vecorder: 面板已插入到body中");
  703.  
  704. recordBtn.hover(
  705. function () {
  706. if (!toggle) $(this).addClass("vecorder-record-btn-hover");
  707. },
  708. function () {
  709. if (!toggle) $(this).removeClass("vecorder-record-btn-hover");
  710. }
  711. );
  712.  
  713. recordBtn.click(function () {
  714. if (toggle) {
  715. $("#vPanel").hide();
  716. gc();
  717. toggle = false;
  718. $(this).removeClass("vecorder-record-btn-active");
  719. return;
  720. }
  721. console.log("Toggle panel");
  722. $("#vPanel").show();
  723. // 更新存储信息
  724. updateStorageInfo();
  725. // 确保面板在正确的位置显示
  726. if (Option.reltime) {
  727. $("#reltime").attr("checked", true);
  728. }
  729.  
  730. // 绑定设置面板的事件
  731. $("#reltime").change(function () {
  732. Option.reltime = $(this).prop("checked");
  733. storageAdapter.set("vop", JSON.stringify(Option)).catch((error) => {
  734. console.error("Vecorder: 保存选项失败:", error);
  735. });
  736. });
  737. $("#toffset").change(function () {
  738. Option.toffset = $(this).val();
  739. storageAdapter.set("vop", JSON.stringify(Option)).catch((error) => {
  740. console.error("Vecorder: 保存选项失败:", error);
  741. });
  742. });
  743. $(this).addClass("vecorder-record-btn-active");
  744. toggle = true;
  745. });
  746.  
  747. // 将记录按钮插入到输入容器中,而不是直接插入到控制面板
  748. $("#vecorder-input-container").append(recordBtn);
  749. console.log("Vecorder: 记录按钮已插入到输入容器中");
  750.  
  751. let styles = $(`<style type="text/css"></style>`);
  752. styles.text(`
  753. /* 主面板样式 */
  754. #vPanel {
  755. line-height: 1.4;
  756. font-size: 13px;
  757. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  758. display: block;
  759. box-sizing: border-box;
  760. background: #ffffff;
  761. border: 1px solid #e2e8f0;
  762. border-radius: 6px;
  763. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  764. padding: 12px;
  765. position: fixed;
  766. right: 20px;
  767. bottom: 120px;
  768. z-index: 99999;
  769. min-width: 320px;
  770. max-width: 420px;
  771. max-height: 75vh;
  772. overflow: hidden;
  773. display: flex;
  774. flex-direction: column;
  775. }
  776.  
  777.  
  778.  
  779. /* 列表容器样式 */
  780. .vecorder-list-container {
  781. max-height: 60vh;
  782. overflow-y: auto;
  783. padding: 0;
  784. margin: 0;
  785. }
  786.  
  787. /* 空状态样式 */
  788. .vecorder-empty-state {
  789. text-align: center;
  790. padding: 24px 16px;
  791. color: #6b7280;
  792. }
  793.  
  794. .vecorder-empty-icon {
  795. margin-bottom: 12px;
  796. opacity: 0.6;
  797. display: flex;
  798. justify-content: center;
  799. }
  800.  
  801. .vecorder-empty-icon svg {
  802. width: 36px;
  803. height: 36px;
  804. stroke: currentColor;
  805. stroke-width: 1.5;
  806. fill: none;
  807. }
  808.  
  809. .vecorder-empty-text {
  810. font-size: 14px;
  811. font-weight: 600;
  812. margin-bottom: 6px;
  813. color: #374151;
  814. }
  815.  
  816. .vecorder-empty-hint {
  817. font-size: 11px;
  818. color: #9ca3af;
  819. }
  820.  
  821. /* 主播分组样式 */
  822. .vecorder-anchor-group {
  823. margin-bottom: 12px;
  824. border: 1px solid #e5e7eb;
  825. border-radius: 4px;
  826. overflow: hidden;
  827. background: #ffffff;
  828. }
  829.  
  830. .vecorder-anchor-header {
  831. display: flex;
  832. justify-content: space-between;
  833. align-items: center;
  834. padding: 8px 12px;
  835. background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
  836. border-bottom: 1px solid #e5e7eb;
  837. }
  838.  
  839. .vecorder-anchor-name {
  840. font-size: 14px;
  841. font-weight: 600;
  842. color: #1f2937;
  843. }
  844.  
  845. .vecorder-anchor-count {
  846. font-size: 11px;
  847. color: #6b7280;
  848. background: rgba(35, 173, 229, 0.1);
  849. padding: 2px 8px;
  850. border-radius: 12px;
  851. }
  852.  
  853. /* 直播列表样式 */
  854. .vecorder-lives-list {
  855. max-height: 250px;
  856. overflow-y: auto;
  857. }
  858.  
  859. .vecorder-live-item {
  860. border-bottom: 1px solid #f3f4f6;
  861. transition: all 0.2s ease;
  862. }
  863.  
  864. .vecorder-live-item:last-child {
  865. border-bottom: none;
  866. }
  867.  
  868. .vecorder-live-item:hover {
  869. background: #f9fafb;
  870. }
  871.  
  872. .vecorder-live-header {
  873. display: flex;
  874. justify-content: space-between;
  875. align-items: center;
  876. padding: 8px 12px;
  877. min-height: 40px;
  878. }
  879.  
  880. .vecorder-live-info {
  881. display: flex;
  882. flex-direction: column;
  883. gap: 4px;
  884. flex: 1;
  885. min-width: 0;
  886. }
  887.  
  888. .vecorder-live-date {
  889. font-size: 10px;
  890. color: #6b7280;
  891. font-weight: 500;
  892. background: rgba(35, 173, 229, 0.1);
  893. padding: 1px 4px;
  894. border-radius: 3px;
  895. display: inline-block;
  896. width: fit-content;
  897. }
  898.  
  899. .vecorder-live-title {
  900. font-size: 12px;
  901. font-weight: 600;
  902. color: #1f2937;
  903. line-height: 1.3;
  904. word-break: break-word;
  905. white-space: normal;
  906. margin: 1px 0;
  907. }
  908.  
  909. .vecorder-live-points-count {
  910. font-size: 10px;
  911. color: #9ca3af;
  912. font-weight: 500;
  913. }
  914.  
  915. /* 操作按钮样式 */
  916. .vecorder-live-actions {
  917. display: flex;
  918. gap: 2px;
  919. margin-left: 8px;
  920. }
  921.  
  922. .vecorder-action-btn {
  923. width: 24px;
  924. height: 24px;
  925. border: none;
  926. border-radius: 4px;
  927. background: transparent;
  928. cursor: pointer;
  929. display: flex;
  930. align-items: center;
  931. justify-content: center;
  932. transition: all 0.2s ease;
  933. color: #6b7280;
  934. padding: 0;
  935. }
  936.  
  937. .vecorder-action-btn svg {
  938. width: 12px;
  939. height: 12px;
  940. stroke: currentColor;
  941. stroke-width: 2;
  942. fill: none;
  943. }
  944.  
  945. .vecorder-action-btn:hover {
  946. background: rgba(35, 173, 229, 0.1);
  947. color: #23ade5;
  948. transform: translateY(-1px);
  949. }
  950.  
  951. .vecorder-delete-btn:hover {
  952. background: rgba(239, 68, 68, 0.1);
  953. color: #ef4444;
  954. }
  955.  
  956. /* 滚动条样式 */
  957. .vecorder-lives-list::-webkit-scrollbar {
  958. width: 4px;
  959. }
  960.  
  961. .vecorder-lives-list::-webkit-scrollbar-track {
  962. background: transparent;
  963. }
  964.  
  965. .vecorder-lives-list::-webkit-scrollbar-thumb {
  966. background: rgba(0, 0, 0, 0.1);
  967. border-radius: 2px;
  968. }
  969.  
  970. .vecorder-lives-list::-webkit-scrollbar-thumb:hover {
  971. background: rgba(0, 0, 0, 0.2);
  972. }
  973.  
  974. /* 标题样式 */
  975. .vecorder-title {
  976. font-size: 16px;
  977. font-weight: 600;
  978. margin: 0 0 12px 0;
  979. color: #1f2937;
  980. text-align: center;
  981. }
  982.  
  983. /* 链接样式 */
  984. .vName {
  985. color: #23ade5;
  986. cursor: pointer;
  987. text-decoration: none;
  988. transition: all 0.2s ease;
  989. font-weight: 500;
  990. }
  991.  
  992. .vName:hover {
  993. color: #1a8bb8;
  994. text-decoration: underline;
  995. }
  996.  
  997. /* 按钮样式 */
  998. .vecorder-btn {
  999. font-family: inherit;
  1000. font-size: 11px;
  1001. font-weight: 500;
  1002. padding: 6px 12px;
  1003. border: none;
  1004. border-radius: 4px;
  1005. background: #23ade5;
  1006. color: white;
  1007. cursor: pointer;
  1008. margin-left: 6px;
  1009. display: inline-flex;
  1010. align-items: center;
  1011. justify-content: center;
  1012. min-height: 28px;
  1013. }
  1014.  
  1015. .vecorder-btn:hover {
  1016. background: #1a8bb8;
  1017. }
  1018.  
  1019. .vecorder-btn-danger {
  1020. background: #ef4444;
  1021. }
  1022.  
  1023. .vecorder-btn-danger:hover {
  1024. background: #dc2626;
  1025. }
  1026.  
  1027. .vecorder-btn-hover {
  1028. background: linear-gradient(135deg, #1a8bb8 0%, #147a9e 100%);
  1029. transform: translateY(-1px);
  1030. box-shadow: 0 4px 8px rgba(35, 173, 229, 0.4);
  1031. }
  1032.  
  1033. /* 记录按钮样式 */
  1034. .vecorder-record-btn {
  1035. font-family: inherit;
  1036. display: flex;
  1037. align-items: center;
  1038. justify-content: center;
  1039. position: relative;
  1040. box-sizing: border-box;
  1041. padding: 4px 10px;
  1042. cursor: pointer;
  1043. outline: none;
  1044. overflow: hidden;
  1045. background: linear-gradient(135deg, #23ade5 0%, #1a8bb8 100%);
  1046. color: #fff;
  1047. border-radius: 12px;
  1048. min-width: 40px;
  1049. height: 24px;
  1050. font-size: 11px;
  1051. font-weight: 500;
  1052. transition: all 0.2s ease;
  1053. box-shadow: 0 2px 4px rgba(35, 173, 229, 0.3);
  1054. border: none;
  1055. flex-shrink: 0;
  1056. }
  1057.  
  1058. .vecorder-record-btn:hover {
  1059. background: linear-gradient(135deg, #1a8bb8 0%, #147a9e 100%);
  1060. transform: translateY(-1px);
  1061. box-shadow: 0 4px 8px rgba(35, 173, 229, 0.4);
  1062. }
  1063.  
  1064. .vecorder-record-btn:active {
  1065. transform: translateY(0);
  1066. box-shadow: 0 2px 4px rgba(35, 173, 229, 0.3);
  1067. }
  1068.  
  1069. .vecorder-record-btn-hover {
  1070. background: linear-gradient(135deg, #1a8bb8 0%, #147a9e 100%);
  1071. transform: translateY(-1px);
  1072. box-shadow: 0 4px 8px rgba(35, 173, 229, 0.4);
  1073. }
  1074.  
  1075. .vecorder-record-btn-active {
  1076. background: linear-gradient(135deg, #0d749e 0%, #0a5a7a 100%);
  1077. box-shadow: 0 4px 8px rgba(13, 116, 158, 0.4);
  1078. }
  1079.  
  1080. /* 关闭按钮 */
  1081. .vecorder-close-btn {
  1082. position: absolute !important;
  1083. right: 12px !important;
  1084. top: 12px !important;
  1085. font-size: 18px !important;
  1086. color: #6b7280 !important;
  1087. cursor: pointer;
  1088. width: 24px;
  1089. height: 24px;
  1090. display: flex;
  1091. align-items: center;
  1092. justify-content: center;
  1093. border-radius: 50%;
  1094. text-decoration: none;
  1095. font-weight: 300;
  1096. }
  1097.  
  1098. .vecorder-close-btn:hover {
  1099. color: #ef4444 !important;
  1100. }
  1101.  
  1102. /* 分割线 */
  1103. .vecorder-divider {
  1104. border: 0;
  1105. height: 1px;
  1106. background: linear-gradient(90deg, transparent, #e5e7eb, transparent);
  1107. margin: 12px 0;
  1108. }
  1109.  
  1110. /* 存储信息区域样式 */
  1111. .vecorder-storage-info {
  1112. background: rgba(35, 173, 229, 0.05);
  1113. border: 1px solid rgba(35, 173, 229, 0.2);
  1114. border-radius: 4px;
  1115. padding: 6px 10px;
  1116. margin: 6px 0;
  1117. font-size: 10px;
  1118. }
  1119.  
  1120. .storage-info-item {
  1121. display: flex;
  1122. justify-content: center;
  1123. align-items: center;
  1124. margin: 0;
  1125. }
  1126.  
  1127. .storage-value {
  1128. color: #23ade5;
  1129. font-weight: 600;
  1130. font-family: 'Courier New', monospace;
  1131. text-align: center;
  1132. line-height: 1.2;
  1133. }
  1134.  
  1135. /* 底部操作区域 */
  1136. .vecorder-bottom-actions {
  1137. display: flex;
  1138. align-items: center;
  1139. justify-content: flex-end;
  1140. gap: 8px;
  1141. padding: 8px 0;
  1142. border-top: 1px solid #e5e7eb;
  1143. margin-top: 8px;
  1144. }
  1145.  
  1146. /* 设置按钮 */
  1147. .vecorder-settings-btn {
  1148. width: 28px;
  1149. height: 28px;
  1150. border: none;
  1151. border-radius: 4px;
  1152. background: transparent;
  1153. cursor: pointer;
  1154. display: flex;
  1155. align-items: center;
  1156. justify-content: center;
  1157. transition: all 0.2s ease;
  1158. color: #6b7280;
  1159. font-size: 14px;
  1160. padding: 0;
  1161. }
  1162.  
  1163. .vecorder-settings-btn:hover {
  1164. background: rgba(35, 173, 229, 0.1);
  1165. color: #23ade5;
  1166. transform: translateY(-1px);
  1167. }
  1168.  
  1169. /* 清空按钮 */
  1170. .vecorder-clear-btn {
  1171. width: 28px;
  1172. height: 28px;
  1173. border: none;
  1174. border-radius: 4px;
  1175. background: transparent;
  1176. cursor: pointer;
  1177. display: flex;
  1178. align-items: center;
  1179. justify-content: center;
  1180. transition: all 0.2s ease;
  1181. color: #6b7280;
  1182. font-size: 14px;
  1183. padding: 0;
  1184. }
  1185.  
  1186. .vecorder-clear-btn:hover {
  1187. background: rgba(239, 68, 68, 0.1);
  1188. color: #ef4444;
  1189. transform: translateY(-1px);
  1190. }
  1191.  
  1192. /* 设置面板 */
  1193. .vecorder-settings-panel {
  1194. background: rgba(35, 173, 229, 0.05);
  1195. border: 1px solid rgba(35, 173, 229, 0.2);
  1196. border-radius: 4px;
  1197. padding: 12px;
  1198. margin-top: 8px;
  1199. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
  1200. font-size: 12px;
  1201. overflow: hidden;
  1202. }
  1203.  
  1204. /* 时间选项区域 */
  1205. .timeop-item {
  1206. margin: 8px 0;
  1207. display: flex;
  1208. align-items: center;
  1209. gap: 8px;
  1210. }
  1211.  
  1212. .timeop-item label {
  1213. font-size: 12px;
  1214. color: #374151;
  1215. font-weight: 500;
  1216. flex: 1;
  1217. }
  1218.  
  1219. .vecorder-settings-panel input[type="number"] {
  1220. width: 80px;
  1221. padding: 6px 10px;
  1222. border: 1px solid #d1d5db;
  1223. border-radius: 6px;
  1224. font-size: 12px;
  1225. outline: none;
  1226. transition: all 0.2s ease;
  1227. background: rgba(255, 255, 255, 0.9);
  1228. }
  1229.  
  1230. .vecorder-settings-panel input[type="number"]:focus {
  1231. border-color: #23ade5;
  1232. box-shadow: 0 0 0 3px rgba(35, 173, 229, 0.1);
  1233. background: white;
  1234. }
  1235.  
  1236. .vecorder-settings-panel input[type="checkbox"] {
  1237. accent-color: #23ade5;
  1238. transform: scale(1.2);
  1239. margin: 0;
  1240. }
  1241.  
  1242. .vecorder-settings-panel .timeop-item {
  1243. margin: 6px 0;
  1244. }
  1245.  
  1246. .vecorder-settings-panel .timeop-item label {
  1247. font-size: 11px;
  1248. color: #374151;
  1249. font-weight: 500;
  1250. }
  1251.  
  1252. /* 聊天输入框样式 */
  1253. #control-panel-ctnr-box > div.chat-input-ctnr-new.p-relative > div.medal-section {
  1254. height: 30px;
  1255. line-height: 13px;
  1256. }
  1257.  
  1258. /* Vecorder输入容器 */
  1259. #vecorder-input-container {
  1260. display: flex;
  1261. align-items: center;
  1262. gap: 6px;
  1263. margin-top: 6px;
  1264. padding: 6px 10px;
  1265. background: rgba(255, 255, 255, 0.95);
  1266. border-radius: 6px;
  1267. border: 1px solid rgba(0, 0, 0, 0.08);
  1268. transition: all 0.2s ease;
  1269. }
  1270.  
  1271. #vecorder-input-container:focus-within {
  1272. border-color: #23ade5;
  1273. box-shadow: 0 0 0 3px rgba(35, 173, 229, 0.1);
  1274. }
  1275.  
  1276. /* 时间点输入框 */
  1277. #point-input {
  1278. flex: 1;
  1279. min-width: 0;
  1280. }
  1281.  
  1282. #point-input > textarea {
  1283. width: 100%;
  1284. border: 0;
  1285. outline: 0;
  1286. resize: none;
  1287. background: transparent;
  1288. color: #374151;
  1289. font-size: 12px;
  1290. height: 20px;
  1291. font-family: inherit;
  1292. line-height: 1.4;
  1293. }
  1294.  
  1295. #point-input > textarea::placeholder {
  1296. color: #9ca3af;
  1297. }
  1298.  
  1299. /* 响应式设计 */
  1300. @media (max-width: 768px) {
  1301. #vPanel {
  1302. right: 4px;
  1303. left: 4px;
  1304. bottom: 120px;
  1305. min-width: auto;
  1306. max-width: none;
  1307. max-height: 80vh;
  1308. }
  1309. .vecorder-lives-list {
  1310. max-height: 200px;
  1311. }
  1312. }
  1313.  
  1314. /* 滚动条样式 */
  1315. #vPanel::-webkit-scrollbar {
  1316. width: 6px;
  1317. }
  1318.  
  1319. #vPanel::-webkit-scrollbar-track {
  1320. background: rgba(0, 0, 0, 0.05);
  1321. border-radius: 3px;
  1322. }
  1323.  
  1324. #vPanel::-webkit-scrollbar-thumb {
  1325. background: rgba(35, 173, 229, 0.3);
  1326. border-radius: 3px;
  1327. }
  1328.  
  1329. #vPanel::-webkit-scrollbar-thumb:hover {
  1330. background: rgba(35, 173, 229, 0.5);
  1331. }
  1332.  
  1333. /* 移除聊天控制面板的高度限制 */
  1334. #chat-control-panel-vm {
  1335. max-height: none !important;
  1336. height: auto !important;
  1337. }
  1338.  
  1339. /* 优化bottom-actions区域的布局 */
  1340. .bottom-actions {
  1341. display: flex;
  1342. flex-direction: column;
  1343. gap: 8px;
  1344. position: relative;
  1345. }
  1346.  
  1347. /* 调整发送按钮位置,避免覆盖输入框 */
  1348. .bottom-actions .right-action {
  1349. position: static !important;
  1350. align-self: flex-end;
  1351. margin-top: 4px;
  1352. }
  1353.  
  1354. /* 确保Vecorder输入容器不被覆盖 */
  1355. #vecorder-input-container {
  1356. position: relative;
  1357. z-index: 1;
  1358. }
  1359. `);
  1360. $("head").prepend(styles);
  1361.  
  1362. return true; // 成功插入后退出函数
  1363. }
  1364.  
  1365. console.log("Vecorder: 未找到合适的控制面板元素");
  1366. return false;
  1367. }
  1368.  
  1369. // 移除单独的insertRecordButton调用,因为现在在insertTimeInput中调用
  1370.  
  1371. function dbToListview() {
  1372. let urlObject = window.URL || window.webkitURL || window;
  1373. let content = $(
  1374. `<div id="vecorder-list" class="vecorder-list-container"></div>`
  1375. );
  1376.  
  1377. // 如果没有数据,显示空状态
  1378. if (db.length === 0 || db.every((item) => item.del)) {
  1379. content.append(`
  1380. <div class="vecorder-empty-state">
  1381. <div class="vecorder-empty-icon">
  1382. <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
  1383. <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
  1384. <polyline points="14,2 14,8 20,8"/>
  1385. <line x1="16" y1="13" x2="8" y2="13"/>
  1386. <line x1="16" y1="17" x2="8" y2="17"/>
  1387. <polyline points="10,9 9,9 8,9"/>
  1388. </svg>
  1389. </div>
  1390. <div class="vecorder-empty-text">暂无直播笔记</div>
  1391. <div class="vecorder-empty-hint">在下方输入框添加时间点即可开始记录</div>
  1392. </div>
  1393. `);
  1394. return content;
  1395. }
  1396.  
  1397. // 创建主播分组列表
  1398. for (let i in db) {
  1399. if (db[i].del) continue;
  1400.  
  1401. // 检查该主播是否有未删除的直播
  1402. let hasValidLives = false;
  1403. for (let j in db[i].lives) {
  1404. if (!db[i].lives[j].del) {
  1405. hasValidLives = true;
  1406. break;
  1407. }
  1408. }
  1409. if (!hasValidLives) continue;
  1410.  
  1411. // 创建主播分组
  1412. let anchorGroup = $(`
  1413. <div class="vecorder-anchor-group">
  1414. <div class="vecorder-anchor-header">
  1415. <span class="vecorder-anchor-name">${db[i].name}</span>
  1416. <span class="vecorder-anchor-count">${
  1417. db[i].lives.filter((live) => !live.del).length
  1418. } 场直播</span>
  1419. </div>
  1420. <div class="vecorder-lives-list"></div>
  1421. </div>
  1422. `);
  1423.  
  1424. let livesList = anchorGroup.find(".vecorder-lives-list");
  1425.  
  1426. // 按时间倒序排列直播
  1427. let sortedLives = db[i].lives
  1428. .filter((live) => !live.del)
  1429. .sort((a, b) => b.time - a.time);
  1430.  
  1431. for (let live of sortedLives) {
  1432. let liveItem = $(`
  1433. <div class="vecorder-live-item">
  1434. <div class="vecorder-live-header">
  1435. <div class="vecorder-live-info">
  1436. <span class="vecorder-live-date">${moment(live.time).format(
  1437. "MM/DD HH:mm"
  1438. )}</span>
  1439. <span class="vecorder-live-title">${live.title}</span>
  1440. <span class="vecorder-live-points-count">${
  1441. live.points.length
  1442. } 个时间点</span>
  1443. </div>
  1444. <div class="vecorder-live-actions">
  1445. <button class="vecorder-action-btn vecorder-export-btn" title="导出笔记">
  1446. <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  1447. <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
  1448. <polyline points="7,10 12,15 17,10"/>
  1449. <line x1="12" y1="15" x2="12" y2="3"/>
  1450. </svg>
  1451. </button>
  1452. <button class="vecorder-action-btn vecorder-delete-btn" title="删除">
  1453. <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  1454. <polyline points="3,6 5,6 21,6"/>
  1455. <path d="M19,6v14a2,2 0 0,1 -2,2H7a2,2 0 0,1 -2,-2V6m3,0V4a2,2 0 0,1 2,-2h4a2,2 0 0,1 2,2v2"/>
  1456. <line x1="10" y1="11" x2="10" y2="17"/>
  1457. <line x1="14" y1="11" x2="14" y2="17"/>
  1458. </svg>
  1459. </button>
  1460. </div>
  1461. </div>
  1462. </div>
  1463. `);
  1464.  
  1465. // 绑定导出事件
  1466. liveItem.find(".vecorder-export-btn").click(function () {
  1467. exportRaw(
  1468. live,
  1469. db[i].name,
  1470. `[${db[i].name}][${live.title}][${moment(live.time).format(
  1471. "YYYY-MM-DD"
  1472. )}].txt`
  1473. );
  1474. });
  1475.  
  1476. // 绑定删除事件
  1477. liveItem.find(".vecorder-delete-btn").click(function () {
  1478. if (confirm(`确定要删除 "${live.title}" 的直播笔记吗?`)) {
  1479. if (db[i].lives.length == 1) {
  1480. db[i].del = true;
  1481. anchorGroup.remove();
  1482. } else {
  1483. live.del = true;
  1484. liveItem.remove();
  1485. }
  1486. storageAdapter.set(dbname, JSON.stringify(db)).catch((error) => {
  1487. console.error("Vecorder: 保存数据失败:", error);
  1488. });
  1489.  
  1490. // 更新存储信息
  1491. updateStorageInfo();
  1492.  
  1493. // 如果该主播组没有更多直播,移除整个组
  1494. if (anchorGroup.find(".vecorder-live-item").length === 0) {
  1495. anchorGroup.remove();
  1496. }
  1497.  
  1498. // 检查是否所有数据都被删除了,如果是则重新生成列表显示空状态
  1499. let hasValidData = false;
  1500. for (let j in db) {
  1501. if (!db[j].del) {
  1502. for (let k in db[j].lives) {
  1503. if (!db[j].lives[k].del) {
  1504. hasValidData = true;
  1505. break;
  1506. }
  1507. }
  1508. if (hasValidData) break;
  1509. }
  1510. }
  1511.  
  1512. if (!hasValidData) {
  1513. // 重新生成列表显示空状态
  1514. $(`#vecorder-list`).replaceWith(dbToListview());
  1515. }
  1516. }
  1517. });
  1518.  
  1519. livesList.append(liveItem);
  1520. }
  1521.  
  1522. content.append(anchorGroup);
  1523. }
  1524.  
  1525. return content;
  1526. }
  1527.  
  1528. function exportRaw(live, v, fname) {
  1529. var urlObject = window.URL || window.webkitURL || window;
  1530. var export_blob = new Blob([rawToString(live, v)]);
  1531. var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
  1532. save_link.href = urlObject.createObjectURL(export_blob);
  1533. save_link.download = fname;
  1534. save_link.click();
  1535. }
  1536.  
  1537. function rawToString(live, v) {
  1538. let r =
  1539. "# 由Vecorder自动生成,不妨关注下可爱的@轴伊Joi_Channel:https://space.bilibili.com/61639371/\n";
  1540. r += `# ${v} \n`;
  1541. r += `# ${live.title} - 直播开始时间:${moment(live.time).format(
  1542. "YYYY-MM-DD HH:mm:ss"
  1543. )}\n\n`;
  1544. for (let i in live.points) {
  1545. if (!Option.reltime)
  1546. r += `[${moment(live.points[i].time)
  1547. .add(Option.toffset, "seconds")
  1548. .format("HH:mm:ss")}] ${live.points[i].content}\n`;
  1549. else {
  1550. let seconds =
  1551. moment(live.points[i].time).diff(moment(live.time), "second") +
  1552. Number(Option.toffset);
  1553. let minutes = Math.floor(seconds / 60);
  1554. let hours = Math.floor(minutes / 60);
  1555. seconds = seconds % 60;
  1556. minutes = minutes % 60;
  1557. r += `[${f(hours)}:${f(minutes)}:${f(seconds)}] ${
  1558. live.points[i].content
  1559. }\n`;
  1560. }
  1561. }
  1562. return r;
  1563. }
  1564.  
  1565. function f(num) {
  1566. if (String(num).length > 2) return num;
  1567. return (Array(2).join(0) + num).slice(-2);
  1568. }
  1569.  
  1570. function waitForKeyElements(
  1571. selectorTxt /* Required: The jQuery selector string that
  1572. specifies the desired element(s).
  1573. */,
  1574. actionFunction /* Required: The code to run when elements are
  1575. found. It is passed a jNode to the matched
  1576. element.
  1577. */,
  1578. bWaitOnce /* Optional: If false, will continue to scan for
  1579. new elements even after the first match is
  1580. found.
  1581. */,
  1582. iframeSelector /* Optional: If set, identifies the iframe to
  1583. search.
  1584. */
  1585. ) {
  1586. console.log(`Vecorder: waitForKeyElements 检查选择器: ${selectorTxt}`);
  1587. var targetNodes, btargetsFound;
  1588.  
  1589. if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt);
  1590. else targetNodes = $(iframeSelector).contents().find(selectorTxt);
  1591.  
  1592. if (targetNodes && targetNodes.length > 0) {
  1593. console.log(`Vecorder: 找到 ${targetNodes.length} 个匹配元素`);
  1594. btargetsFound = true;
  1595. /*--- Found target node(s). Go through each and act if they
  1596. are new.
  1597. */
  1598. targetNodes.each(function () {
  1599. var jThis = $(this);
  1600. var alreadyFound = jThis.data("alreadyFound") || false;
  1601.  
  1602. if (!alreadyFound) {
  1603. console.log(`Vecorder: 执行动作函数`);
  1604. //--- Call the payload function.
  1605. var cancelFound = actionFunction(jThis);
  1606. if (cancelFound) {
  1607. console.log(`Vecorder: 动作函数返回true,标记为已处理并停止检查`);
  1608. jThis.data("alreadyFound", true);
  1609. btargetsFound = true; // 保持为true,这样会清除定时器
  1610. } else {
  1611. jThis.data("alreadyFound", true);
  1612. console.log(`Vecorder: 元素已标记为已处理`);
  1613. }
  1614. } else {
  1615. console.log(`Vecorder: 元素已经处理过,跳过`);
  1616. }
  1617. });
  1618. } else {
  1619. console.log(`Vecorder: 未找到匹配元素,继续等待`);
  1620. btargetsFound = false;
  1621. }
  1622.  
  1623. //--- Get the timer-control variable for this selector.
  1624. var controlObj = waitForKeyElements.controlObj || {};
  1625. var controlKey = selectorTxt.replace(/[^\w]/g, "_");
  1626. var timeControl = controlObj[controlKey];
  1627.  
  1628. //--- Now set or clear the timer as appropriate.
  1629. if (btargetsFound && bWaitOnce && timeControl) {
  1630. //--- The only condition where we need to clear the timer.
  1631. console.log(`Vecorder: 清除定时器,停止检查`);
  1632. clearInterval(timeControl);
  1633. delete controlObj[controlKey];
  1634. } else if (!btargetsFound) {
  1635. //--- Set a timer, if needed.
  1636. if (!timeControl) {
  1637. console.log(`Vecorder: 设置定时器,继续检查`);
  1638. timeControl = setInterval(function () {
  1639. waitForKeyElements(
  1640. selectorTxt,
  1641. actionFunction,
  1642. bWaitOnce,
  1643. iframeSelector
  1644. );
  1645. }, 300);
  1646. controlObj[controlKey] = timeControl;
  1647. }
  1648. }
  1649. waitForKeyElements.controlObj = controlObj;
  1650. }