Vecorder

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

// ==UserScript==
// @name         Vecorder
// @namespace    https://www.joi-club.cn/
// @version      1.0.0
// @description  直播间内容记录 https://github.com/Xinrea/Vecorder
// @author       Xinrea
// @license      MIT
// @match        https://live.bilibili.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/moment.min.js
// @run-at 		 document-end
// ==/UserScript==

// IndexedDB 存储管理
class VecorderStorage {
  constructor() {
    this.dbName = "VecorderDB";
    this.dbVersion = 1;
    this.storeName = "vecorder_data";
    this.db = null;
    this.isInitialized = false;
  }

  // 初始化数据库
  async init() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, this.dbVersion);

      request.onerror = () => {
        console.error("Vecorder: IndexedDB 打开失败:", request.error);
        reject(request.error);
      };

      request.onsuccess = () => {
        this.db = request.result;
        this.isInitialized = true;
        console.log("Vecorder: IndexedDB 初始化成功");
        resolve();
      };

      request.onupgradeneeded = (event) => {
        const db = event.target.result;

        // 创建对象存储
        if (!db.objectStoreNames.contains(this.storeName)) {
          const store = db.createObjectStore(this.storeName, {
            keyPath: "key",
          });
          console.log("Vecorder: 创建 IndexedDB 存储");
        }
      };
    });
  }

  // 获取数据
  async get(key, defaultValue = null) {
    if (!this.isInitialized) {
      await this.init();
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], "readonly");
      const store = transaction.objectStore(this.storeName);
      const request = store.get(key);

      request.onerror = () => {
        console.error("Vecorder: 获取数据失败:", request.error);
        reject(request.error);
      };

      request.onsuccess = () => {
        const result = request.result;
        if (result) {
          resolve(result.value);
        } else {
          resolve(defaultValue);
        }
      };
    });
  }

  // 设置数据
  async set(key, value) {
    if (!this.isInitialized) {
      await this.init();
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], "readwrite");
      const store = transaction.objectStore(this.storeName);
      const request = store.put({ key, value });

      request.onerror = () => {
        console.error("Vecorder: 设置数据失败:", request.error);
        reject(request.error);
      };

      request.onsuccess = () => {
        console.log("Vecorder: 数据保存成功:", key);
        resolve();
      };
    });
  }

  // 删除数据
  async delete(key) {
    if (!this.isInitialized) {
      await this.init();
    }

    return new Promise((resolve, reject) => {
      const transaction = this.db.transaction([this.storeName], "readwrite");
      const store = transaction.objectStore(this.storeName);
      const request = store.delete(key);

      request.onerror = () => {
        console.error("Vecorder: 删除数据失败:", request.error);
        reject(request.error);
      };

      request.onsuccess = () => {
        console.log("Vecorder: 数据删除成功:", key);
        resolve();
      };
    });
  }

  // 获取存储空间信息
  async getStorageInfo() {
    if (!this.isInitialized) {
      await this.init();
    }

    return new Promise((resolve, reject) => {
      try {
        // 获取已用空间
        const transaction = this.db.transaction([this.storeName], "readonly");
        const store = transaction.objectStore(this.storeName);
        const request = store.getAll();

        request.onerror = () => {
          console.error("Vecorder: 获取存储信息失败:", request.error);
          reject(request.error);
        };

        request.onsuccess = () => {
          const data = request.result;
          let usedSize = 0;

          // 计算已用空间
          data.forEach((item) => {
            usedSize += JSON.stringify(item).length;
          });

          // 获取浏览器存储配额信息
          if ("storage" in navigator && "estimate" in navigator.storage) {
            navigator.storage
              .estimate()
              .then((estimate) => {
                const totalSpace = estimate.quota || 0;
                const availableSpace = estimate.usage || 0;
                const remainingSpace = totalSpace - availableSpace;

                resolve({
                  usedSize: usedSize,
                  totalSpace: totalSpace,
                  availableSpace: availableSpace,
                  remainingSpace: remainingSpace,
                  dataCount: data.length,
                });
              })
              .catch((error) => {
                console.error("Vecorder: 获取存储配额失败:", error);
                resolve({
                  usedSize: usedSize,
                  totalSpace: 0,
                  availableSpace: 0,
                  remainingSpace: 0,
                  dataCount: data.length,
                });
              });
          } else {
            // 如果不支持 storage.estimate,只返回已用空间
            resolve({
              usedSize: usedSize,
              totalSpace: 0,
              availableSpace: 0,
              remainingSpace: 0,
              dataCount: data.length,
            });
          }
        };
      } catch (error) {
        console.error("Vecorder: 获取存储信息时出错:", error);
        reject(error);
      }
    });
  }

  // 格式化字节大小
  formatBytes(bytes) {
    if (bytes === 0) return "0 B";
    const k = 1024;
    const sizes = ["B", "KB", "MB", "GB"];
    const i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
  }

  // 检查是否有 GM 数据需要迁移
  async checkAndMigrateData() {
    try {
      // 检查是否有 GM 数据
      const gmData = GM_getValue(dbname, null);
      const gmOptions = GM_getValue("vop", null);

      if (gmData || gmOptions) {
        console.log("Vecorder: 检测到 GM 数据,开始迁移到 IndexedDB");

        // 迁移主数据
        if (gmData) {
          await this.set(dbname, gmData);
          GM_deleteValue(dbname);
          console.log("Vecorder: 主数据迁移完成");
        }

        // 迁移选项数据
        if (gmOptions) {
          await this.set("vop", gmOptions);
          GM_deleteValue("vop");
          console.log("Vecorder: 选项数据迁移完成");
        }

        console.log("Vecorder: 数据迁移完成,已清空 GM 存储");
        return true;
      }

      return false;
    } catch (error) {
      console.error("Vecorder: 数据迁移失败:", error);
      return false;
    }
  }
}

// 创建存储实例
const vecorderStorage = new VecorderStorage();

// 全局更新存储信息函数
async function updateStorageInfo() {
  try {
    if (!window.vecorderUseGMStorage) {
      const storageInfo = await vecorderStorage.getStorageInfo();
      let infoText = `已用: ${vecorderStorage.formatBytes(
        storageInfo.usedSize
      )} | 数据: ${storageInfo.dataCount}`;

      if (storageInfo.totalSpace > 0) {
        infoText += ` | 剩余: ${vecorderStorage.formatBytes(
          storageInfo.remainingSpace
        )}`;
      }

      $("#vecorder-storage-info").html(`
        <div class="storage-info-item">
          <span class="storage-value">${infoText}</span>
        </div>
      `);
    } else {
      $("#vecorder-storage-info").html(`
        <div class="storage-info-item">
          <span class="storage-value">GM 存储模式</span>
        </div>
      `);
    }
  } catch (error) {
    console.error("Vecorder: 更新存储信息失败:", error);
    $("#vecorder-storage-info").html(`
      <div class="storage-info-item">
        <span class="storage-value">存储信息获取失败</span>
      </div>
    `);
  }
}

// 存储适配器,根据情况选择使用 IndexedDB 还是 GM 存储
const storageAdapter = {
  async set(key, value) {
    if (window.vecorderUseGMStorage) {
      GM_setValue(key, value);
      return Promise.resolve();
    } else {
      return vecorderStorage.set(key, value);
    }
  },

  async get(key, defaultValue = null) {
    if (window.vecorderUseGMStorage) {
      const value = GM_getValue(key, defaultValue);
      return Promise.resolve(value);
    } else {
      return vecorderStorage.get(key, defaultValue);
    }
  },

  async delete(key) {
    if (window.vecorderUseGMStorage) {
      GM_deleteValue(key);
      return Promise.resolve();
    } else {
      return vecorderStorage.delete(key);
    }
  },
};

// 立即执行的调试信息
console.log("Vecorder: 脚本开始加载");
console.log(
  "Vecorder: jQuery版本:",
  typeof $ !== "undefined" ? $.fn.jquery : "未加载"
);
console.log(
  "Vecorder: Moment版本:",
  typeof moment !== "undefined" ? moment.version : "未加载"
);
console.log("Vecorder: 当前页面:", window.location.href);
vlog("脚本已加载,版本 0.70");

function vlog(msg) {
  console.log("[Vecorder]" + msg);
}

function p(msg) {
  return {
    time: new Date().getTime(),
    content: msg,
  };
}

// 根据当前地址获取直播间ID
function getRoomID() {
  let roomid = window.location.pathname.substring(1);
  return roomid; //获取当前房间号
}

var dbname = "vdb" + getRoomID();

// 初始化数据存储
let db = [];
let Option = { reltime: false, toffset: 0 };

// 异步初始化数据
async function initializeData() {
  try {
    // 检查并迁移数据
    const migrated = await vecorderStorage.checkAndMigrateData();

    // 从 IndexedDB 加载数据
    const dbData = await vecorderStorage.get(dbname, "[]");
    const optionsData = await vecorderStorage.get(
      "vop",
      '{"reltime":false,"toffset":0}'
    );

    db = JSON.parse(dbData);
    Option = JSON.parse(optionsData);

    console.log("Vecorder: 数据初始化完成");
    if (migrated) {
      console.log("Vecorder: 数据已从 GM 存储迁移到 IndexedDB");
    }
  } catch (error) {
    console.error("Vecorder: IndexedDB 初始化失败,回退到 GM 存储:", error);
    // 如果 IndexedDB 失败,回退到 GM 存储
    db = JSON.parse(GM_getValue(dbname, "[]"));
    Option = JSON.parse(GM_getValue("vop", '{"reltime":false,"toffset":0}'));

    // 标记使用 GM 存储模式
    window.vecorderUseGMStorage = true;
    console.log("Vecorder: 已切换到 GM 存储模式");
  }
}

// 数据初始化将在 DOM ready 时进行

function nindexOf(n) {
  for (let i in db) {
    if (!db[i].del && db[i].name == n) return i;
  }
  return -1;
}

function tindexOf(id, t) {
  for (let i in db[id].lives) {
    if (!db[id].lives[i].del && db[id].lives[i].title == t) return i;
  }
  return -1;
}

async function gc() {
  for (let i = db.length - 1; i >= 0; i--) {
    if (db[i].del) {
      db.splice(i, 1);
      continue;
    }
    for (let j = db[i].lives.length - 1; j >= 0; j--) {
      if (db[i].lives[j].del) {
        db[i].lives.splice(j, 1);
        continue;
      }
    }
  }
  await storageAdapter.set(dbname, JSON.stringify(db));
}

async function addPoint(t, msg) {
  console.log("addPoint", t, msg);
  let ltime = t * 1000;
  if (ltime == 0) return;
  let [name, link, title] = getRoomInfo();
  console.log("CurrentRoom:", name, link, title);
  let id = nindexOf(name);
  if (id == -1) {
    db.push({
      name: name,
      link: link,
      del: false,
      lives: [
        {
          title: title,
          time: ltime,
          del: false,
          points: [p(msg)],
        },
      ],
    });
  } else {
    let lid = tindexOf(id, title);
    if (lid == -1) {
      db[id].lives.push({
        title: title,
        time: ltime,
        points: [p(msg)],
      });
    } else {
      db[id].lives[lid].points.push(p(msg));
    }
  }
  await storageAdapter.set(dbname, JSON.stringify(db));
  $(`#vecorder-list`).replaceWith(dbToListview());
  // 更新存储信息
  if (toggle) {
    updateStorageInfo();
  }
}

function getMsg(body) {
  var vars = body.split("&");
  for (var i = 0; i < vars.length; i++) {
    var pair = vars[i].split("=");
    if (pair[0] == "msg") {
      return decodeURI(pair[1]);
    }
  }
  return false;
}

function getRoomInfo() {
  let resp = $.ajax({
    url:
      "https://api.live.bilibili.com/xlive/web-room/v1/index/getH5InfoByRoom?room_id=" +
      getRoomID(),
    async: false,
  }).responseJSON.data;
  console.log("RoomInfo:", resp);
  return [
    resp.anchor_info.base_info.uname,
    "https://space.bilibili.com/" + resp.room_info.uid,
    resp.room_info.title,
  ];
}

function tryAddPoint(msg) {
  // https://api.live.bilibili.com/room/v1/Room/room_init?id={roomID}
  console.log(msg, getRoomID());
  $.ajax({
    url:
      "https://api.live.bilibili.com/room/v1/Room/room_init?id=" + getRoomID(),
    async: true,
    success: function (resp) {
      let t = 0;
      if (resp.data.live_status != 1) t = 0;
      else t = resp.data.live_time;
      addPoint(t, msg).catch((error) => {
        console.error("Vecorder: 添加时间点失败:", error);
      });
    },
  });
}

let toggle = false;

// 确保DOM加载完成后再执行
$(document).ready(async function () {
  console.log("Vecorder: DOM已加载完成");
  vlog("开始初始化Vecorder功能");

  // 等待数据初始化完成
  try {
    await initializeData();
    console.log("Vecorder: 数据初始化完成,开始检查页面元素");
  } catch (error) {
    console.error("Vecorder: 数据初始化失败:", error);
  }

  // 延迟检查页面元素
  setTimeout(function () {
    console.log("Vecorder: 延迟检查页面元素");
    console.log(
      "Vecorder: control-panel-ctnr-box 存在:",
      $("#control-panel-ctnr-box").length > 0
    );
    console.log(
      "Vecorder: bottom-actions 存在:",
      $(".bottom-actions").length > 0
    );
    console.log(
      "Vecorder: bottom-actions 存在:",
      $(".bottom-actions").length > 0
    );
  }, 2000);
});

// 尝试多个可能的选择器来插入时间点输入框
function insertTimeInput() {
  const selectors = [
    "#control-panel-ctnr-box > div.bottom-actions.p-relative",
    "#control-panel-ctnr-box .bottom-actions",
    ".bottom-actions",
    "#control-panel-ctnr-box",
  ];

  for (let selector of selectors) {
    if ($(selector).length > 0) {
      console.log(`Vecorder: 使用选择器 ${selector} 插入时间点输入框`);

      // 创建时间点输入框容器
      const inputContainer = $(
        `<div id="vecorder-input-container">
          <div id="point-input">
            <textarea placeholder="输入内容并回车添加时间点"></textarea>
          </div>
        </div>`
      );

      inputContainer
        .find("#point-input > textarea")
        .bind("keypress", function (event) {
          if (event.keyCode == "13") {
            window.event.returnValue = false;
            console.log("Enter detected");
            tryAddPoint($("#point-input > textarea").val());
            $("#point-input > textarea").val("");
          }
        });

      // 尝试插入到不同的位置
      const targetSelectors = [
        "#control-panel-ctnr-box > div.bottom-actions.p-relative",
        ".bottom-actions",
        "#control-panel-ctnr-box",
      ];

      for (let targetSelector of targetSelectors) {
        if ($(targetSelector).length > 0) {
          $(targetSelector).append(inputContainer);
          console.log(`Vecorder: 时间点输入框已插入到 ${targetSelector}`);

          // 在同一个元素上插入记录按钮和面板
          insertRecordButton();
          return true; // 返回true表示成功插入,停止检查
        }
      }
    }
  }
  return false; // 如果没有找到合适的元素,返回false
}

console.log("Vecorder: 开始等待聊天输入区域元素");
waitForKeyElements(
  "#control-panel-ctnr-box > div.bottom-actions.p-relative",
  insertTimeInput,
  true // 只执行一次
);

// 尝试多个可能的选择器来插入记录按钮和面板
function insertRecordButton() {
  console.log("Vecorder: 开始插入记录按钮和面板");

  // 直接使用当前找到的元素
  const n = $("#control-panel-ctnr-box > div.bottom-actions.p-relative");
  if (n.length > 0) {
    console.log(`Vecorder: 使用当前元素插入记录按钮和面板`);

    // 尝试调整现有按钮的样式
    try {
      // 这里可以添加对现有按钮的调整,如果需要的话
      console.log("Vecorder: 找到目标元素,准备插入记录按钮");
    } catch (e) {
      console.log("Vecorder: 调整按钮样式时出错,继续执行", e);
    }

    // 这里可以添加其他初始化代码,如果需要的话

    // create panel
    let panel = $(
      '<div id="vPanel"><p class="vecorder-title">🍊 直播笔记</p></div>'
    );

    // 添加存储空间信息区域
    let storageInfo = $(
      '<div id="vecorder-storage-info" class="vecorder-storage-info"></div>'
    );
    panel.append(storageInfo);

    let contentList = dbToListview();
    panel.append(contentList);

    // 创建底部操作区域
    let bottomActions = $('<div class="vecorder-bottom-actions"></div>');

    // 创建设置按钮和折叠面板
    let settingsBtn = $(
      '<button class="vecorder-settings-btn" title="导出设置">⚙️</button>'
    );
    let settingsPanel =
      $(`<div class="vecorder-settings-panel" style="display: none;">
      <div class="timeop-item">
        <input type="checkbox" id="reltime" value="false"/>
        <label for="reltime">按相对时间导出</label>
      </div>
      <div class="timeop-item">
        <label for="toffset">时间偏移(秒):</label>
        <input type="number" id="toffset" value="${Option.toffset}"/>
      </div>
    </div>`);

    settingsBtn.click(function () {
      console.log("Vecorder: 设置按钮被点击");
      settingsPanel.slideToggle(200);
      console.log("Vecorder: 设置面板可见性:", settingsPanel.is(":visible"));
    });

    // 创建清空按钮
    let clearBtn = $(
      '<button class="vecorder-clear-btn" title="清空所有数据">🗑️</button>'
    );
    clearBtn.click(function () {
      if (confirm("确定要清空所有直播笔记吗?此操作不可恢复。")) {
        db = [];
        storageAdapter.delete(dbname).catch((error) => {
          console.error("Vecorder: 清空数据失败:", error);
        });
        // 重新生成列表并更新显示
        $(`#vecorder-list`).replaceWith(dbToListview());
        // 更新存储信息
        updateStorageInfo();
      }
    });

    // 添加按钮到底部操作区域
    bottomActions.append(settingsBtn);
    bottomActions.append(clearBtn);
    panel.append(bottomActions);

    // 将设置面板添加到主面板中
    panel.append(settingsPanel);

    let closeBtn = $('<a class="vecorder-close-btn">&times;</a>');
    closeBtn.click(function () {
      console.log("Close clicked");
      $("#vPanel").hide();
      gc().catch((error) => {
        console.error("Vecorder: 保存数据失败:", error);
      });
      toggle = false;
      recordBtn.removeClass("vecorder-record-btn-active");
      // 更新存储信息
      updateStorageInfo();
    });
    panel.append(closeBtn);

    // Setup recordButton
    let recordBtn = $('<div><span class="txt">记录</span></div>');
    recordBtn.addClass("vecorder-record-btn");

    // 将面板插入到body中,确保正确的定位
    $("body").append(panel);
    $("#vPanel").hide();
    console.log("Vecorder: 面板已插入到body中");

    recordBtn.hover(
      function () {
        if (!toggle) $(this).addClass("vecorder-record-btn-hover");
      },
      function () {
        if (!toggle) $(this).removeClass("vecorder-record-btn-hover");
      }
    );

    recordBtn.click(function () {
      if (toggle) {
        $("#vPanel").hide();
        gc();
        toggle = false;
        $(this).removeClass("vecorder-record-btn-active");
        return;
      }
      console.log("Toggle panel");
      $("#vPanel").show();
      // 更新存储信息
      updateStorageInfo();
      // 确保面板在正确的位置显示
      if (Option.reltime) {
        $("#reltime").attr("checked", true);
      }

      // 绑定设置面板的事件
      $("#reltime").change(function () {
        Option.reltime = $(this).prop("checked");
        storageAdapter.set("vop", JSON.stringify(Option)).catch((error) => {
          console.error("Vecorder: 保存选项失败:", error);
        });
      });
      $("#toffset").change(function () {
        Option.toffset = $(this).val();
        storageAdapter.set("vop", JSON.stringify(Option)).catch((error) => {
          console.error("Vecorder: 保存选项失败:", error);
        });
      });
      $(this).addClass("vecorder-record-btn-active");
      toggle = true;
    });

    // 将记录按钮插入到输入容器中,而不是直接插入到控制面板
    $("#vecorder-input-container").append(recordBtn);
    console.log("Vecorder: 记录按钮已插入到输入容器中");

    let styles = $(`<style type="text/css"></style>`);
    styles.text(`
            /* 主面板样式 */
            #vPanel {
                line-height: 1.4;
                font-size: 13px;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
                display: block;
                box-sizing: border-box;
                background: #ffffff;
                border: 1px solid #e2e8f0;
                border-radius: 6px;
                box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
                padding: 12px;
                position: fixed;
                right: 20px;
                bottom: 120px;
                z-index: 99999;
                min-width: 320px;
                max-width: 420px;
                max-height: 75vh;
                overflow: hidden;
                display: flex;
                flex-direction: column;
            }



            /* 列表容器样式 */
            .vecorder-list-container {
                max-height: 60vh;
                overflow-y: auto;
                padding: 0;
                margin: 0;
            }

            /* 空状态样式 */
            .vecorder-empty-state {
                text-align: center;
                padding: 24px 16px;
                color: #6b7280;
            }

            .vecorder-empty-icon {
                margin-bottom: 12px;
                opacity: 0.6;
                display: flex;
                justify-content: center;
            }

            .vecorder-empty-icon svg {
                width: 36px;
                height: 36px;
                stroke: currentColor;
                stroke-width: 1.5;
                fill: none;
            }

            .vecorder-empty-text {
                font-size: 14px;
                font-weight: 600;
                margin-bottom: 6px;
                color: #374151;
            }

            .vecorder-empty-hint {
                font-size: 11px;
                color: #9ca3af;
            }

            /* 主播分组样式 */
            .vecorder-anchor-group {
                margin-bottom: 12px;
                border: 1px solid #e5e7eb;
                border-radius: 4px;
                overflow: hidden;
                background: #ffffff;
            }

            .vecorder-anchor-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 8px 12px;
                background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
                border-bottom: 1px solid #e5e7eb;
            }

            .vecorder-anchor-name {
                font-size: 14px;
                font-weight: 600;
                color: #1f2937;
            }

            .vecorder-anchor-count {
                font-size: 11px;
                color: #6b7280;
                background: rgba(35, 173, 229, 0.1);
                padding: 2px 8px;
                border-radius: 12px;
            }

            /* 直播列表样式 */
            .vecorder-lives-list {
                max-height: 250px;
                overflow-y: auto;
            }

            .vecorder-live-item {
                border-bottom: 1px solid #f3f4f6;
                transition: all 0.2s ease;
            }

            .vecorder-live-item:last-child {
                border-bottom: none;
            }

            .vecorder-live-item:hover {
                background: #f9fafb;
            }

            .vecorder-live-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 8px 12px;
                min-height: 40px;
            }

            .vecorder-live-info {
                display: flex;
                flex-direction: column;
                gap: 4px;
                flex: 1;
                min-width: 0;
            }

            .vecorder-live-date {
                font-size: 10px;
                color: #6b7280;
                font-weight: 500;
                background: rgba(35, 173, 229, 0.1);
                padding: 1px 4px;
                border-radius: 3px;
                display: inline-block;
                width: fit-content;
            }

            .vecorder-live-title {
                font-size: 12px;
                font-weight: 600;
                color: #1f2937;
                line-height: 1.3;
                word-break: break-word;
                white-space: normal;
                margin: 1px 0;
            }

            .vecorder-live-points-count {
                font-size: 10px;
                color: #9ca3af;
                font-weight: 500;
            }

            /* 操作按钮样式 */
            .vecorder-live-actions {
                display: flex;
                gap: 2px;
                margin-left: 8px;
            }

            .vecorder-action-btn {
                width: 24px;
                height: 24px;
                border: none;
                border-radius: 4px;
                background: transparent;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                transition: all 0.2s ease;
                color: #6b7280;
                padding: 0;
            }

            .vecorder-action-btn svg {
                width: 12px;
                height: 12px;
                stroke: currentColor;
                stroke-width: 2;
                fill: none;
            }

            .vecorder-action-btn:hover {
                background: rgba(35, 173, 229, 0.1);
                color: #23ade5;
                transform: translateY(-1px);
            }

            .vecorder-delete-btn:hover {
                background: rgba(239, 68, 68, 0.1);
                color: #ef4444;
            }

            /* 滚动条样式 */
            .vecorder-lives-list::-webkit-scrollbar {
                width: 4px;
            }

            .vecorder-lives-list::-webkit-scrollbar-track {
                background: transparent;
            }

            .vecorder-lives-list::-webkit-scrollbar-thumb {
                background: rgba(0, 0, 0, 0.1);
                border-radius: 2px;
            }

            .vecorder-lives-list::-webkit-scrollbar-thumb:hover {
                background: rgba(0, 0, 0, 0.2);
            }

            /* 标题样式 */
            .vecorder-title {
                font-size: 16px;
                font-weight: 600;
                margin: 0 0 12px 0;
                color: #1f2937;
                text-align: center;
            }

            /* 链接样式 */
            .vName {
                color: #23ade5;
                cursor: pointer;
                text-decoration: none;
                transition: all 0.2s ease;
                font-weight: 500;
            }

            .vName:hover {
                color: #1a8bb8;
                text-decoration: underline;
            }

            /* 按钮样式 */
            .vecorder-btn {
                font-family: inherit;
                font-size: 11px;
                font-weight: 500;
                padding: 6px 12px;
                border: none;
                border-radius: 4px;
                background: #23ade5;
                color: white;
                cursor: pointer;
                margin-left: 6px;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                min-height: 28px;
            }

            .vecorder-btn:hover {
                background: #1a8bb8;
            }

            .vecorder-btn-danger {
                background: #ef4444;
            }

            .vecorder-btn-danger:hover {
                background: #dc2626;
            }

            .vecorder-btn-hover {
                background: linear-gradient(135deg, #1a8bb8 0%, #147a9e 100%);
                transform: translateY(-1px);
                box-shadow: 0 4px 8px rgba(35, 173, 229, 0.4);
            }

            /* 记录按钮样式 */
            .vecorder-record-btn {
                font-family: inherit;
                display: flex;
                align-items: center;
                justify-content: center;
                position: relative;
                box-sizing: border-box;
                padding: 4px 10px;
                cursor: pointer;
                outline: none;
                overflow: hidden;
                background: linear-gradient(135deg, #23ade5 0%, #1a8bb8 100%);
                color: #fff;
                border-radius: 12px;
                min-width: 40px;
                height: 24px;
                font-size: 11px;
                font-weight: 500;
                transition: all 0.2s ease;
                box-shadow: 0 2px 4px rgba(35, 173, 229, 0.3);
                border: none;
                flex-shrink: 0;
            }

            .vecorder-record-btn:hover {
                background: linear-gradient(135deg, #1a8bb8 0%, #147a9e 100%);
                transform: translateY(-1px);
                box-shadow: 0 4px 8px rgba(35, 173, 229, 0.4);
            }

            .vecorder-record-btn:active {
                transform: translateY(0);
                box-shadow: 0 2px 4px rgba(35, 173, 229, 0.3);
            }

            .vecorder-record-btn-hover {
                background: linear-gradient(135deg, #1a8bb8 0%, #147a9e 100%);
                transform: translateY(-1px);
                box-shadow: 0 4px 8px rgba(35, 173, 229, 0.4);
            }

            .vecorder-record-btn-active {
                background: linear-gradient(135deg, #0d749e 0%, #0a5a7a 100%);
                box-shadow: 0 4px 8px rgba(13, 116, 158, 0.4);
            }

            /* 关闭按钮 */
            .vecorder-close-btn {
                position: absolute !important;
                right: 12px !important;
                top: 12px !important;
                font-size: 18px !important;
                color: #6b7280 !important;
                cursor: pointer;
                width: 24px;
                height: 24px;
                display: flex;
                align-items: center;
                justify-content: center;
                border-radius: 50%;
                text-decoration: none;
                font-weight: 300;
            }

            .vecorder-close-btn:hover {
                color: #ef4444 !important;
            }

            /* 分割线 */
            .vecorder-divider {
                border: 0;
                height: 1px;
                background: linear-gradient(90deg, transparent, #e5e7eb, transparent);
                margin: 12px 0;
            }

            /* 存储信息区域样式 */
            .vecorder-storage-info {
                background: rgba(35, 173, 229, 0.05);
                border: 1px solid rgba(35, 173, 229, 0.2);
                border-radius: 4px;
                padding: 6px 10px;
                margin: 6px 0;
                font-size: 10px;
            }

            .storage-info-item {
                display: flex;
                justify-content: center;
                align-items: center;
                margin: 0;
            }

            .storage-value {
                color: #23ade5;
                font-weight: 600;
                font-family: 'Courier New', monospace;
                text-align: center;
                line-height: 1.2;
            }

            /* 底部操作区域 */
            .vecorder-bottom-actions {
                display: flex;
                align-items: center;
                justify-content: flex-end;
                gap: 8px;
                padding: 8px 0;
                border-top: 1px solid #e5e7eb;
                margin-top: 8px;
            }

            /* 设置按钮 */
            .vecorder-settings-btn {
                width: 28px;
                height: 28px;
                border: none;
                border-radius: 4px;
                background: transparent;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                transition: all 0.2s ease;
                color: #6b7280;
                font-size: 14px;
                padding: 0;
            }

            .vecorder-settings-btn:hover {
                background: rgba(35, 173, 229, 0.1);
                color: #23ade5;
                transform: translateY(-1px);
            }

            /* 清空按钮 */
            .vecorder-clear-btn {
                width: 28px;
                height: 28px;
                border: none;
                border-radius: 4px;
                background: transparent;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                transition: all 0.2s ease;
                color: #6b7280;
                font-size: 14px;
                padding: 0;
            }

            .vecorder-clear-btn:hover {
                background: rgba(239, 68, 68, 0.1);
                color: #ef4444;
                transform: translateY(-1px);
            }

            /* 设置面板 */
            .vecorder-settings-panel {
                background: rgba(35, 173, 229, 0.05);
                border: 1px solid rgba(35, 173, 229, 0.2);
                border-radius: 4px;
                padding: 12px;
                margin-top: 8px;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
                font-size: 12px;
                overflow: hidden;
            }

            /* 时间选项区域 */
            .timeop-item {
                margin: 8px 0;
                display: flex;
                align-items: center;
                gap: 8px;
            }

            .timeop-item label {
                font-size: 12px;
                color: #374151;
                font-weight: 500;
                flex: 1;
            }

            .vecorder-settings-panel input[type="number"] {
                width: 80px;
                padding: 6px 10px;
                border: 1px solid #d1d5db;
                border-radius: 6px;
                font-size: 12px;
                outline: none;
                transition: all 0.2s ease;
                background: rgba(255, 255, 255, 0.9);
            }

            .vecorder-settings-panel input[type="number"]:focus {
                border-color: #23ade5;
                box-shadow: 0 0 0 3px rgba(35, 173, 229, 0.1);
                background: white;
            }

            .vecorder-settings-panel input[type="checkbox"] {
                accent-color: #23ade5;
                transform: scale(1.2);
                margin: 0;
            }

            .vecorder-settings-panel .timeop-item {
                margin: 6px 0;
            }

            .vecorder-settings-panel .timeop-item label {
                font-size: 11px;
                color: #374151;
                font-weight: 500;
            }

            /* 聊天输入框样式 */
            #control-panel-ctnr-box > div.chat-input-ctnr-new.p-relative > div.medal-section {
                height: 30px;
                line-height: 13px;
            }

            /* Vecorder输入容器 */
            #vecorder-input-container {
                display: flex;
                align-items: center;
                gap: 6px;
                margin-top: 6px;
                padding: 6px 10px;
                background: rgba(255, 255, 255, 0.95);
                border-radius: 6px;
                border: 1px solid rgba(0, 0, 0, 0.08);
                transition: all 0.2s ease;
            }

            #vecorder-input-container:focus-within {
                border-color: #23ade5;
                box-shadow: 0 0 0 3px rgba(35, 173, 229, 0.1);
            }

            /* 时间点输入框 */
            #point-input {
                flex: 1;
                min-width: 0;
            }

            #point-input > textarea {
                width: 100%;
                border: 0;
                outline: 0;
                resize: none;
                background: transparent;
                color: #374151;
                font-size: 12px;
                height: 20px;
                font-family: inherit;
                line-height: 1.4;
            }

            #point-input > textarea::placeholder {
                color: #9ca3af;
            }

            /* 响应式设计 */
            @media (max-width: 768px) {
                #vPanel {
                    right: 4px;
                    left: 4px;
                    bottom: 120px;
                    min-width: auto;
                    max-width: none;
                    max-height: 80vh;
                }
                
                .vecorder-lives-list {
                    max-height: 200px;
                }
            }

            /* 滚动条样式 */
            #vPanel::-webkit-scrollbar {
                width: 6px;
            }

            #vPanel::-webkit-scrollbar-track {
                background: rgba(0, 0, 0, 0.05);
                border-radius: 3px;
            }

            #vPanel::-webkit-scrollbar-thumb {
                background: rgba(35, 173, 229, 0.3);
                border-radius: 3px;
            }

            #vPanel::-webkit-scrollbar-thumb:hover {
                background: rgba(35, 173, 229, 0.5);
            }

            /* 移除聊天控制面板的高度限制 */
            #chat-control-panel-vm {
                max-height: none !important;
                height: auto !important;
            }

            /* 优化bottom-actions区域的布局 */
            .bottom-actions {
                display: flex;
                flex-direction: column;
                gap: 8px;
                position: relative;
            }

            /* 调整发送按钮位置,避免覆盖输入框 */
            .bottom-actions .right-action {
                position: static !important;
                align-self: flex-end;
                margin-top: 4px;
            }

            /* 确保Vecorder输入容器不被覆盖 */
            #vecorder-input-container {
                position: relative;
                z-index: 1;
            }
        `);
    $("head").prepend(styles);

    return true; // 成功插入后退出函数
  }

  console.log("Vecorder: 未找到合适的控制面板元素");
  return false;
}

// 移除单独的insertRecordButton调用,因为现在在insertTimeInput中调用

function dbToListview() {
  let urlObject = window.URL || window.webkitURL || window;
  let content = $(
    `<div id="vecorder-list" class="vecorder-list-container"></div>`
  );

  // 如果没有数据,显示空状态
  if (db.length === 0 || db.every((item) => item.del)) {
    content.append(`
      <div class="vecorder-empty-state">
        <div class="vecorder-empty-icon">
          <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
            <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
            <polyline points="14,2 14,8 20,8"/>
            <line x1="16" y1="13" x2="8" y2="13"/>
            <line x1="16" y1="17" x2="8" y2="17"/>
            <polyline points="10,9 9,9 8,9"/>
          </svg>
        </div>
        <div class="vecorder-empty-text">暂无直播笔记</div>
        <div class="vecorder-empty-hint">在下方输入框添加时间点即可开始记录</div>
      </div>
    `);
    return content;
  }

  // 创建主播分组列表
  for (let i in db) {
    if (db[i].del) continue;

    // 检查该主播是否有未删除的直播
    let hasValidLives = false;
    for (let j in db[i].lives) {
      if (!db[i].lives[j].del) {
        hasValidLives = true;
        break;
      }
    }
    if (!hasValidLives) continue;

    // 创建主播分组
    let anchorGroup = $(`
      <div class="vecorder-anchor-group">
        <div class="vecorder-anchor-header">
          <span class="vecorder-anchor-name">${db[i].name}</span>
          <span class="vecorder-anchor-count">${
            db[i].lives.filter((live) => !live.del).length
          } 场直播</span>
        </div>
        <div class="vecorder-lives-list"></div>
      </div>
    `);

    let livesList = anchorGroup.find(".vecorder-lives-list");

    // 按时间倒序排列直播
    let sortedLives = db[i].lives
      .filter((live) => !live.del)
      .sort((a, b) => b.time - a.time);

    for (let live of sortedLives) {
      let liveItem = $(`
        <div class="vecorder-live-item">
          <div class="vecorder-live-header">
            <div class="vecorder-live-info">
              <span class="vecorder-live-date">${moment(live.time).format(
                "MM/DD HH:mm"
              )}</span>
              <span class="vecorder-live-title">${live.title}</span>
              <span class="vecorder-live-points-count">${
                live.points.length
              } 个时间点</span>
            </div>
            <div class="vecorder-live-actions">
              <button class="vecorder-action-btn vecorder-export-btn" title="导出笔记">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                  <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
                  <polyline points="7,10 12,15 17,10"/>
                  <line x1="12" y1="15" x2="12" y2="3"/>
                </svg>
              </button>
              <button class="vecorder-action-btn vecorder-delete-btn" title="删除">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                  <polyline points="3,6 5,6 21,6"/>
                  <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"/>
                  <line x1="10" y1="11" x2="10" y2="17"/>
                  <line x1="14" y1="11" x2="14" y2="17"/>
                </svg>
              </button>
            </div>
          </div>
        </div>
      `);

      // 绑定导出事件
      liveItem.find(".vecorder-export-btn").click(function () {
        exportRaw(
          live,
          db[i].name,
          `[${db[i].name}][${live.title}][${moment(live.time).format(
            "YYYY-MM-DD"
          )}].txt`
        );
      });

      // 绑定删除事件
      liveItem.find(".vecorder-delete-btn").click(function () {
        if (confirm(`确定要删除 "${live.title}" 的直播笔记吗?`)) {
          if (db[i].lives.length == 1) {
            db[i].del = true;
            anchorGroup.remove();
          } else {
            live.del = true;
            liveItem.remove();
          }
          storageAdapter.set(dbname, JSON.stringify(db)).catch((error) => {
            console.error("Vecorder: 保存数据失败:", error);
          });

          // 更新存储信息
          updateStorageInfo();

          // 如果该主播组没有更多直播,移除整个组
          if (anchorGroup.find(".vecorder-live-item").length === 0) {
            anchorGroup.remove();
          }

          // 检查是否所有数据都被删除了,如果是则重新生成列表显示空状态
          let hasValidData = false;
          for (let j in db) {
            if (!db[j].del) {
              for (let k in db[j].lives) {
                if (!db[j].lives[k].del) {
                  hasValidData = true;
                  break;
                }
              }
              if (hasValidData) break;
            }
          }

          if (!hasValidData) {
            // 重新生成列表显示空状态
            $(`#vecorder-list`).replaceWith(dbToListview());
          }
        }
      });

      livesList.append(liveItem);
    }

    content.append(anchorGroup);
  }

  return content;
}

function exportRaw(live, v, fname) {
  var urlObject = window.URL || window.webkitURL || window;
  var export_blob = new Blob([rawToString(live, v)]);
  var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a");
  save_link.href = urlObject.createObjectURL(export_blob);
  save_link.download = fname;
  save_link.click();
}

function rawToString(live, v) {
  let r =
    "# 由Vecorder自动生成,不妨关注下可爱的@轴伊Joi_Channel:https://space.bilibili.com/61639371/\n";
  r += `# ${v} \n`;
  r += `# ${live.title} - 直播开始时间:${moment(live.time).format(
    "YYYY-MM-DD HH:mm:ss"
  )}\n\n`;
  for (let i in live.points) {
    if (!Option.reltime)
      r += `[${moment(live.points[i].time)
        .add(Option.toffset, "seconds")
        .format("HH:mm:ss")}] ${live.points[i].content}\n`;
    else {
      let seconds =
        moment(live.points[i].time).diff(moment(live.time), "second") +
        Number(Option.toffset);
      let minutes = Math.floor(seconds / 60);
      let hours = Math.floor(minutes / 60);
      seconds = seconds % 60;
      minutes = minutes % 60;
      r += `[${f(hours)}:${f(minutes)}:${f(seconds)}] ${
        live.points[i].content
      }\n`;
    }
  }
  return r;
}

function f(num) {
  if (String(num).length > 2) return num;
  return (Array(2).join(0) + num).slice(-2);
}

function waitForKeyElements(
  selectorTxt /* Required: The jQuery selector string that
                        specifies the desired element(s).
                    */,
  actionFunction /* Required: The code to run when elements are
                        found. It is passed a jNode to the matched
                        element.
                    */,
  bWaitOnce /* Optional: If false, will continue to scan for
                        new elements even after the first match is
                        found.
                    */,
  iframeSelector /* Optional: If set, identifies the iframe to
                        search.
                    */
) {
  console.log(`Vecorder: waitForKeyElements 检查选择器: ${selectorTxt}`);
  var targetNodes, btargetsFound;

  if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt);
  else targetNodes = $(iframeSelector).contents().find(selectorTxt);

  if (targetNodes && targetNodes.length > 0) {
    console.log(`Vecorder: 找到 ${targetNodes.length} 个匹配元素`);
    btargetsFound = true;
    /*--- Found target node(s).  Go through each and act if they
            are new.
        */
    targetNodes.each(function () {
      var jThis = $(this);
      var alreadyFound = jThis.data("alreadyFound") || false;

      if (!alreadyFound) {
        console.log(`Vecorder: 执行动作函数`);
        //--- Call the payload function.
        var cancelFound = actionFunction(jThis);
        if (cancelFound) {
          console.log(`Vecorder: 动作函数返回true,标记为已处理并停止检查`);
          jThis.data("alreadyFound", true);
          btargetsFound = true; // 保持为true,这样会清除定时器
        } else {
          jThis.data("alreadyFound", true);
          console.log(`Vecorder: 元素已标记为已处理`);
        }
      } else {
        console.log(`Vecorder: 元素已经处理过,跳过`);
      }
    });
  } else {
    console.log(`Vecorder: 未找到匹配元素,继续等待`);
    btargetsFound = false;
  }

  //--- Get the timer-control variable for this selector.
  var controlObj = waitForKeyElements.controlObj || {};
  var controlKey = selectorTxt.replace(/[^\w]/g, "_");
  var timeControl = controlObj[controlKey];

  //--- Now set or clear the timer as appropriate.
  if (btargetsFound && bWaitOnce && timeControl) {
    //--- The only condition where we need to clear the timer.
    console.log(`Vecorder: 清除定时器,停止检查`);
    clearInterval(timeControl);
    delete controlObj[controlKey];
  } else if (!btargetsFound) {
    //--- Set a timer, if needed.
    if (!timeControl) {
      console.log(`Vecorder: 设置定时器,继续检查`);
      timeControl = setInterval(function () {
        waitForKeyElements(
          selectorTxt,
          actionFunction,
          bWaitOnce,
          iframeSelector
        );
      }, 300);
      controlObj[controlKey] = timeControl;
    }
  }
  waitForKeyElements.controlObj = controlObj;
}