[
  {
    "path": ".github/CODEOWNERS",
    "content": "* @algerkong\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.zh-CN.yml",
    "content": "name: 反馈 Bug\ndescription: 通过 github 模板进行 Bug 反馈。\ntitle: '描述问题的标题'\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # 欢迎你的参与\n        Issue 列表接受 bug 报告或是新功能请求。\n\n        在发布一个 Issue 前，请确保：\n        - 在Issue中搜索过你的问题。（你的问题可能已有人提出，也可能已在最新版本中被修正）\n        - 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在，不要在旧 Issue 下面留言，请建一个新的 issue。\n\n  - type: input\n    id: reproduce\n    attributes:\n      label: 重现链接\n      description: 请提供尽可能精简的 CodePen、CodeSandbox 或 GitHub 仓库的链接。请不要填无关链接，否则你的 Issue 将被关闭。\n      placeholder: 请填写\n\n  - type: textarea\n    id: reproduceSteps\n    attributes:\n      label: 重现步骤\n      description: 请清晰的描述重现该 Issue 的步骤，这能帮助我们快速定位问题。没有清晰重现步骤将不会被修复，标有 'need reproduction' 的 Issue 在 7 天内不提供相关步骤，将被关闭。\n      placeholder: 请填写\n\n  - type: textarea\n    id: expect\n    attributes:\n      label: 期望结果\n      placeholder: 请填写\n\n  - type: textarea\n    id: actual\n    attributes:\n      label: 实际结果\n      placeholder: 请填写\n\n  - type: input\n    id: frameworkVersion\n    attributes:\n      label: 框架版本\n      placeholder: Vue(3.3.0)\n\n  - type: input\n    id: browsersVersion\n    attributes:\n      label: 浏览器版本\n      placeholder: Chrome(8.213.231.123)\n\n  - type: input\n    id: systemVersion\n    attributes:\n      label: 系统版本\n      placeholder: MacOS(11.2.3)\n\n  - type: input\n    id: nodeVersion\n    attributes:\n      label: Node版本\n      placeholder: 请填写\n\n  - type: textarea\n    id: remarks\n    attributes:\n      label: 补充说明\n      description: 可以是遇到这个 bug 的业务场景、上下文等信息。\n      placeholder: 请填写\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: true\ncontact_links:\n  - name:\n    url:\n    about:\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-report.zh-CN.yml",
    "content": "name: 反馈新功能\ndescription: 通过 github 模板进行新功能反馈。\ntitle: '描述问题的标题'\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        # 欢迎你的参与\n\n        在发布一个 Issue 前，请确保：\n        - 在 Issue 中搜索过你的问题。（你的问题可能已有人提出，也可能已在最新版本中被修正）\n        - 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在，不要在旧 Issue 下面留言，请建一个新的 issue。\n\n  - type: textarea\n    id: functionContent\n    attributes:\n      label: 这个功能解决了什么问题\n      description: 请详尽说明这个需求的用例和场景。最重要的是：解释清楚是怎样的用户体验需求催生了这个功能上的需求。我们将考虑添加在现有 API 无法轻松实现的功能。新功能的用例也应当足够常见。\n      placeholder: 请填写\n    validations:\n      required: true\n\n  - type: textarea\n    id: functionalExpectations\n    attributes:\n      label: 你建议的方案是什么\n      placeholder: 请填写\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!--\n首先，感谢你的贡献！😄\nPR 在维护者审核通过后会合并，谢谢！\n-->\n\n### 🤔 这个 PR 的性质是？\n\n- [ ] 日常 bug 修复\n- [ ] 新特性提交\n- [ ] 文档改进\n- [ ] 演示代码改进\n- [ ] 组件样式/交互改进\n- [ ] CI/CD 改进\n- [ ] 重构\n- [ ] 代码风格优化\n- [ ] 测试用例\n- [ ] 分支合并\n- [ ] 其他\n\n### 🔗 相关 Issue\n\n<!--\n1. 描述相关需求的来源，如相关的 issue 讨论链接。\n-->\n\n### 💡 需求背景和解决方案\n\n<!--\n1. 要解决的具体问题。\n2. 列出最终的 API 实现和用法。\n3. 涉及UI/交互变动需要有截图或 GIF。\n-->\n\n### 📝 更新日志\n\n<!--\n从用户角度描述具体变化，以及可能的 breaking change 和其他风险。\n-->\n\n- fix(组件名称): 处理问题或特性描述 ...\n\n- [ ] 本条 PR 不需要纳入 Changelog\n\n### ☑️ 请求合并前的自查清单\n\n⚠️ 请自检并全部**勾选全部选项**。⚠️\n\n- [ ] 文档已补充或无须补充\n- [ ] 代码演示已提供或无须提供\n- [ ] TypeScript 定义已补充或无须补充\n- [ ] Changelog 已提供或无须提供\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# Basic dependabot.yml file with\n# minimum configuration for two package managers\n\nversion: 2\nupdates:\n  # Enable version updates for npm\n  - package-ecosystem: 'npm'\n    # Look for `package.json` and `lock` files in the `root` directory\n    directory: '/'\n    # Check the npm registry for updates every day (weekdays)\n    schedule:\n      interval: 'monthly'\n\n  # Enable version updates for Docker\n  - package-ecosystem: 'docker'\n    # Look for a `Dockerfile` in the `root` directory\n    directory: '/'\n    # Check for updates once a week\n    schedule:\n      interval: 'monthly'\n"
  },
  {
    "path": ".github/issue-shoot.md",
    "content": "## IssueShoot\n\n- 预估时长: {{ .duration }}\n- 期望完成时间: {{ .deadline }}\n- 开发难度: {{ .level }}\n- 参与人数: 1\n- 需求对接人: ivringpeng\n- 验收标准: 实现期望改造效果，提 PR 并通过验收无误\n- 备注: 最终激励以实际提交 `pull request` 并合并为准\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build and Release\n\non:\n  push:\n    tags:\n      - 'v*'\n\njobs:\n  release:\n    runs-on: ${{ matrix.os }}\n\n    strategy:\n      matrix:\n        os: [macos-latest, windows-latest, ubuntu-latest]\n\n    steps:\n      - name: Check out Git repository\n        uses: actions/checkout@v4\n\n      - name: Install Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 18\n\n      - name: Install Dependencies\n        run: npm install\n\n      # MacOS Build\n      - name: Build MacOS\n        if: matrix.os == 'macos-latest'\n        run: |\n          export ELECTRON_BUILDER_EXTRA_ARGS=\"--universal\"\n          npm run build:mac\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          CSC_IDENTITY_AUTO_DISCOVERY: false\n          DEBUG: electron-builder\n\n      # Windows Build\n      - name: Build Windows\n        if: matrix.os == 'windows-latest'\n        run: npm run build:win\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      # Linux Build\n      - name: Build Linux\n        if: matrix.os == 'ubuntu-latest'\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf\n          npm run build:linux\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n\n      # Get version from tag\n      - name: Get version from tag\n        id: get_version\n        run: echo \"VERSION=${GITHUB_REF#refs/tags/v}\" >> $GITHUB_ENV\n        shell: bash\n\n      # Read release notes\n      - name: Read release notes\n        id: release_notes\n        run: |\n          NOTES=$(awk \"/## \\[v${{ env.VERSION }}\\]/{p=1;print;next} /## \\[v/{p=0}p\" CHANGELOG.md)\n          echo \"NOTES<<EOF\" >> $GITHUB_ENV\n          echo \"$NOTES\" >> $GITHUB_ENV\n          echo \"EOF\" >> $GITHUB_ENV\n        shell: bash\n\n      # Upload artifacts\n      - name: Upload artifacts\n        uses: softprops/action-gh-release@v1\n        with:\n          files: |\n            dist/*.dmg\n            dist/*.exe\n            dist/*.deb\n            dist/*.rpm\n            dist/*.AppImage\n            dist/latest*.yml\n            dist/*.blockmap\n          body: ${{ env.NOTES }}\n          draft: false\n          prerelease: false\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/deploy.yml",
    "content": "name: Deploy Web\n\non:\n  push:\n    branches:\n      - main # 或者您的主分支名称\n  workflow_dispatch: # 允许手动触发\n\njobs:\n  build-and-deploy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v3\n        with:\n          node-version: '18'\n\n      - name: 创建环境变量文件\n        run: |\n          echo \"VITE_API=${{ secrets.VITE_API }}\" > .env.production.local\n          echo \"VITE_API_MUSIC=${{ secrets.VITE_API_MUSIC }}\" >> .env.production.local\n          # 添加其他需要的环境变量\n          cat .env.production.local # 查看创建的文件内容，调试用\n\n      - name: Install Dependencies\n        run: npm install\n\n      - name: Build\n        run: npm run build\n\n      - name: Deploy to Server\n        uses: appleboy/scp-action@master\n        with:\n          host: ${{ secrets.SERVER_HOST }}\n          username: ${{ secrets.SERVER_USERNAME }}\n          key: ${{ secrets.DEPLOY_KEY }}\n          source: 'out/renderer/*'\n          target: ${{ secrets.DEPLOY_PATH }}\n          strip_components: 2\n\n      - name: Execute Remote Commands\n        uses: appleboy/ssh-action@master\n        with:\n          host: ${{ secrets.SERVER_HOST }}\n          username: ${{ secrets.SERVER_USERNAME }}\n          key: ${{ secrets.DEPLOY_KEY }}\n          script: |\n            cd ${{ secrets.DEPLOY_PATH }}\n            echo \"部署完成于 $(date)\"\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\n.DS_Store\n\ndist\ndist-ssr\n*.local\ndist_electron\n.idea\n\n# lock\nyarn.lock\npnpm-lock.yaml\npackage-lock.json\ndist.zip\n\n.vscode\n\nbun.lockb\nbun.lock\n\n.env.*.local\n\nout\n\n.cursorrules\n\n.github/deploy_keys\n\nresources/android/**/*\nandroid/app/release\n\n.cursor\n.windsurf\n.agent\n\n\n.auto-imports.d.ts\n.components.d.ts\n\nsrc/renderer/auto-imports.d.ts\nsrc/renderer/components.d.ts"
  },
  {
    "path": ".husky/pre-commit",
    "content": "echo \"对已暂存文件运行 lint-staged...\"\nnpx lint-staged\n\necho \"运行类型检查...\"\nnpm run typecheck\n\necho \"所有检查通过，准备提交...\""
  },
  {
    "path": ".husky/pre-push",
    "content": "echo \"对已暂存文件运行 lint-staged...\"\nnpx lint-staged\n\necho \"运行类型检查...\"\nnpm run typecheck\n\necho \"所有检查通过，准备提交...\""
  },
  {
    "path": ".prettierignore",
    "content": "out\ndist\npnpm-lock.yaml\nLICENSE.md\ntsconfig.json\ntsconfig.*.json\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 更新日志\n\n## v5.0.0\n\n### ✨ 新功能\n\n- LX Music 音源脚本导入\n- 逐字歌词，支持全屏歌词和桌面歌词同步显示\n- 心动模式播放\n- 移动设备整体页面风格和效果优化\n- 移动端添加平板模式设置\n- 歌词页面样式控制优化 支持背景、宽度、字体粗细等个性化设置\n- 历史日推查看\n- 播放记录热力图\n- 历史记录支持本地和云端记录\n- 用户页面收藏专辑展示\n- 添加 GPU 硬件加速设置\n- 菜单展开状态保存 - 感谢 [harenchi](https://github.com/souvenp) 的贡献\n- 搜索建议 - 感谢 [harenchi](https://github.com/souvenp) 的贡献\n- 歌词繁体中文翻译模块，集成 OpenCC 引擎 - 感谢 [Leko](https://github.com/lekoOwO) 的贡献\n- 自定义 API源 支持 [自定义源文档](https://github.com/algerkong/AlgerMusicPlayer/blob/main/docs/custom-api-readme.md) - 感谢 [harenchi](https://github.com/souvenp) 的贡献\n\n### 🐛 Bug 修复\n\n- 修复随机播放顺序异常\n- 修复音源解析错误处理\n- 修复 Mini 播放栏主题颜色问题\n- 修复桌面歌词透明模式标题栏显示\n- 修复逐字歌词字间距\n- 修复远程控制设置无法保存\n- 修复下载无损格式返回 HiRes 音质 - 感谢 [harenchi](https://github.com/souvenp) 的贡献\n- 兼容 pnpm 包管理器 - 感谢 [Leko](https://github.com/lekoOwO) 的贡献\n\n### 🎨 优化\n\n- 音源解析缓存\n- 完善多语言国际化\n- 优化播放检测和错误处理\n- FLAC 元数据和封面图片处理 - 感谢 [harenchi](https://github.com/souvenp) 的贡献\n- 日推不感兴趣调用官方接口 - 感谢 [harenchi](https://github.com/souvenp) 的贡献\n- 代码提交流程优化，添加 lint-staged\n\n## 赞赏支持☕️\n\n[赞赏列表](https://donate.alger.fun/donate)\n\n<table>\n  <tr>\n    <th style=\"text-align:center\">微信赞赏</th>\n    <th style=\"width:100px\"></th>\n    <th style=\"text-align:center\">支付宝赞赏</th>\n  </tr>\n  <tr>\n    <td align=\"center\">\n      <img src=\"https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true\" alt=\"WeChat QRcode\" width=\"200\"><br>\n      <h6>☕️喝点咖啡继续干</h6>\n    </td>\n    <td></td>\n    <td align=\"center\">\n      <img src=\"https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true\" alt=\"Alipay QRcode\" width=\"200\"><br>\n      <h6>🍔来个汉堡</h6>\n    </td>\n  </tr>\n</table>\n"
  },
  {
    "path": "DEV.md",
    "content": "# Alger Music Player 开发文档\n\n## 项目结构\n\n### 技术栈\n\n- **前端框架**：Vue 3 + TypeScript\n- **UI 组件库**：naive-ui\n- **样式框架**：Tailwind CSS\n- **图标库**：remixicon\n- **状态管理**：Pinia\n- **工具库**：VueUse\n- **构建工具**：Vite, electron-vite\n- **打包工具**：electron-builder\n- **国际化**：vue-i18n\n- **HTTP 客户端**：axios\n- **本地存储**：electron-store localstorage\n- **网易云音乐 API**：netease-cloud-music-api\n- **音乐解锁**：@unblockneteasemusic/server\n\n### 项目结构\n\n```\nAlgerMusicPlayer/\n├── build/                  # 构建相关文件\n├── docs/                   # 项目文档\n├── node_modules/           # 依赖包\n├── out/                    # 构建输出目录\n├── resources/              # 资源文件\n├── src/                    # 源代码\n│   ├── i18n/               # 国际化配置\n│   │   ├── lang/           # 语言包\n│   │   ├── main.ts         # 主进程国际化入口\n│   │   └── renderer.ts     # 渲染进程国际化入口\n│   ├── main/               # Electron 主进程\n│   │   ├── modules/        # 主进程模块\n│   │   ├── index.ts        # 主进程入口\n│   │   ├── lyric.ts        # 歌词处理\n│   │   ├── server.ts       # 服务器\n│   │   ├── set.json        # 设置\n│   │   └── unblockMusic.ts # 音乐解锁\n│   ├── preload/            # 预加载脚本\n│   │   ├── index.ts        # 预加载脚本入口\n│   │   └── index.d.ts      # 预加载脚本类型声明\n│   └── renderer/           # Vue 渲染进程\n│       ├── api/            # API 请求\n│       ├── assets/         # 静态资源\n│       ├── components/     # 组件\n│       │   ├── common/     # 通用组件\n│       │   ├── home/       # 首页组件\n│       │   ├── lyric/      # 歌词组件\n│       │   ├── settings/   # 设置组件\n│       │   └── ...         # 其他组件\n│       ├── const/          # 常量定义\n│       ├── directive/      # 自定义指令\n│       ├── hooks/          # 自定义 Hooks\n│       ├── layout/         # 布局组件\n│       ├── router/         # 路由配置\n│       ├── services/       # 服务\n│       ├── store/          # Pinia 状态管理\n│       │   ├── modules/    # Pinia 模块\n│       │   └── index.ts    # Pinia 入口\n│       ├── type/           # 类型定义\n│       ├── types/          # 更多类型定义\n│       ├── utils/          # 工具函数\n│       ├── views/          # 页面视图\n│       ├── App.vue         # 根组件\n│       ├── index.css       # 全局样式\n│       ├── index.html      # HTML 模板\n│       ├── main.ts         # 渲染进程入口\n│       └── ...             # 其他文件\n├── .env.development        # 开发环境变量\n├── .env.development.local  # 本地开发环境变量\n├── .env.production.local   # 本地生产环境变量\n├── .eslintrc.cjs           # ESLint 配置\n├── .gitignore              # Git 忽略文件\n├── .prettierrc.yaml        # Prettier 配置\n├── electron-builder.yml    # electron-builder 配置\n├── electron.vite.config.ts # electron-vite 配置\n├── package.json            # 项目配置\n├── postcss.config.js       # PostCSS 配置\n├── tailwind.config.js      # Tailwind 配置\n├── tsconfig.json           # TypeScript 配置\n├── tsconfig.node.json      # 节点 TypeScript 配置\n└── tsconfig.web.json       # Web TypeScript 配置\n```\n\n### 主要组件说明\n\n#### 主进程 (src/main)\n\n主进程负责创建窗口、处理系统层面的交互以及与渲染进程的通信。\n\n- **index.ts**: 应用主入口，负责创建窗口和应用生命周期管理\n- **lyric.ts**: 歌词解析和处理\n- **unblockMusic.ts**: 网易云音乐解锁功能\n- **server.ts**: 本地服务器\n\n#### 预加载脚本 (src/preload)\n\n预加载脚本在渲染进程加载前执行，提供了渲染进程和主进程之间的桥接功能。\n\n#### 渲染进程 (src/renderer)\n\n渲染进程是基于 Vue 3 的前端应用，负责 UI 渲染和用户交互。\n\n- **components/**: 包含各种 UI 组件\n    - **common/**: 通用组件\n    - **home/**: 首页相关组件\n    - **lyric/**: 歌词显示组件\n    - **settings/**: 设置界面组件\n    - **MusicList.vue**: 音乐列表组件\n    - **MvPlayer.vue**: MV 播放器\n    - **EQControl.vue**: 均衡器控制\n    - **...**: 其他组件\n\n- **store/**: Pinia 状态管理\n    - **modules/**: 各功能模块的状态管理\n    - **index.ts**: 状态管理入口\n\n- **views/**: 页面视图组件\n\n- **router/**: 路由配置\n\n- **api/**: API 请求封装\n\n- **utils/**: 工具函数\n\n### 开发指南\n\n#### 命名约定\n\n- 目录使用 kebab-case (如: components/auth-wizard)\n- 组件文件名使用 PascalCase (如: AuthWizard.vue)\n- 可组合式函数使用 camelCase (如: useAuthState.ts)\n\n#### 代码风格\n\n- 使用 Composition API 和 `<script setup>` 语法\n- 使用 TypeScript 类型系统\n- 优先使用类型而非接口\n- 避免使用枚举，使用 const 对象代替\n- 使用 tailwind 实现响应式设计\n\n### 如何启动？\n\n安装依赖（最好使用node18+）：\n\n```\nnpm install\n```\n\n#### 桌面端开发\n\n启动桌面端开发：\n\n```\nnpm run dev\n```\n\n#### 网页端开发\n\n如果只启动网页端开发，需要自己部署服务 netease-cloud-music-api\n\n需要复制一份 `.env.development.local` 到 `src/renderer` 下\n\n```\n# .env.development.local\n\n# 你的接口地址 (必填)\nVITE_API = ***\n# 音乐破解接口地址\nVITE_API_MUSIC = ***\n```\n\n启动web端开发：\n\n```\nnpm run dev:web\n```\n\n### 打包\n\n打包桌面端：\n\n```\nnpm run build:win\n```\n\n打包后的文件在 /dist 下\n\n打包网页端：\n\n```\nnpm run build\n```\n\n打包后的文件在 /out/renderer 下\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2026 Alger\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<h2 align=\"center\">🎵 Alger Music Player</h2>\n<div align=\"center\">\n<div align=\"center\">\n  <a href=\"https://github.com/algerkong/AlgerMusicPlayer/stargazers\">\n    <img src=\"https://img.shields.io/github/stars/algerkong/AlgerMusicPlayer?style=for-the-badge&logo=github&label=Stars&logoColor=white&color=22c55e\" alt=\"GitHub stars\">\n  </a>\n  <a href=\"https://github.com/algerkong/AlgerMusicPlayer/releases\">\n    <img src=\"https://img.shields.io/github/v/release/algerkong/AlgerMusicPlayer?style=for-the-badge&logo=github&label=Release&logoColor=white&color=1a67af\" alt=\"GitHub release\">\n  </a>\n  <a href=\"https://pd.qq.com/s/cs056n33q?b=5\">\n    <img src=\"https://img.shields.io/badge/QQ频道-algermusic-blue?style=for-the-badge&color=yellow\" alt=\"加入频道\">\n  </a>\n  <a href=\"https://t.me/+9efsKRuvKBk2NWVl\">\n    <img src=\"https://img.shields.io/badge/AlgerMusic-blue?style=for-the-badge&logo=telegram&logoColor=white&label=Telegram\" alt=\"Telegram\">\n  </a>\n   <a href=\"https://donate.alger.fun/\">\n    <img src=\"https://img.shields.io/badge/%E9%A1%B9%E7%9B%AE%E6%8D%90%E8%B5%A0-blue?style=for-the-badge&logo=telegram&logoColor=pink&color=pink&label=%E8%B5%9E%E5%8A%A9\" alt=\"赞助\">\n  </a>\n</div>\n</div>\n<div align=\"center\">\n  <a href=\"https://hellogithub.com/repository/607b849c598d48e08fe38789d156ebdc\" target=\"_blank\"><img src=\"https://api.hellogithub.com/v1/widgets/recommend.svg?rid=607b849c598d48e08fe38789d156ebdc&claim_uid=ObuMXUfeHBmk9TI&theme=neutral\" alt=\"Featured｜HelloGitHub\" width=\"160\" height=\"32\" /></a>\n</div>\n\n[项目下安装以及常用问题文档](https://www.yuque.com/alger-pfg5q/ip4f1a/bmgmfmghnhgwghkm?singleDoc#)\n\n主要功能如下\n\n- 🎵 音乐推荐\n- 🔐 网易云账号登录与同步\n- 📝 功能\n  - 播放历史记录\n  - 歌曲收藏管理\n  - 歌单 MV 排行榜 每日推荐\n  - 自定义快捷键配置（全局或应用内）\n- 🎨 界面与交互\n  - 沉浸式歌词显示（点击左下角封面进入）\n  - 独立桌面歌词窗口\n  - 明暗主题切换\n  - 迷你模式\n  - 状态栏控制\n  - 多语言支持\n- 🎼 音乐功能\n  - 支持歌单、MV、专辑等完整音乐服务\n  - 音乐资源解析（基于 @unblockneteasemusic/server）\n  - EQ均衡器\n  - 定时播放 远程控制播放 倍速播放\n  - 高品质音乐\n  - 音乐文件下载\n  - 搜索 MV 音乐 专辑 歌单 bilibili\n  - 音乐单独选择音源解析\n- 🚀 技术特性\n  - 本地化服务，无需依赖在线API (基于 netease-cloud-music-api)\n  - 全平台适配（Desktop & Web & Mobile Web & Android<测试> & ios<后续>）\n\n## 项目简介\n\n一个第三方音乐播放器、本地服务、桌面歌词、音乐下载、最高音质\n\n## 预览地址\n\n[http://music.alger.fun/](http://music.alger.fun/)\n\n## 软件截图\n\n![首页白](./docs/image.png)\n![首页黑](./docs/image3.png)\n![歌词](./docs/image6.png)\n![桌面歌词](./docs/image2.png)\n![设置页面](./docs/image4.png)\n![音乐远程控制](./docs/image5.png)\n\n## 项目启动\n\n```bash\nnpm install\nnpm run dev\n```\n\n## 开发文档\n\n点击这里[开发文档](./DEV.md)\n\n## 赞赏☕️\n\n[赞赏列表](http://donate.alger.fun/)\n| 微信赞赏 | 支付宝赞赏 |\n| :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: |\n| <img src=\"https://github.com/algerkong/algerkong/blob/main/wechat.jpg?raw=true\" alt=\"WeChat QRcode\" width=200> <br><small>喝点咖啡继续干</small> | <img src=\"https://github.com/algerkong/algerkong/blob/main/alipay.jpg?raw=true\" alt=\"Wechat QRcode\" width=200> <br><small>来包辣条吧~</small> |\n\n## 项目统计\n\n[![Stargazers over time](https://starchart.cc/algerkong/AlgerMusicPlayer.svg?variant=adaptive)](https://starchart.cc/algerkong/AlgerMusicPlayer)\n![Alt](https://repobeats.axiom.co/api/embed/c4d01b3632e241c90cdec9508dfde86a7f54c9f5.svg 'Repobeats analytics image')\n\n## 欢迎提Issues\n\n## 声明\n\n本软件仅用于学习交流，禁止用于商业用途，否则后果自负。\n希望大家还是要多多支持官方正版，此软件仅用作开发教学。\n"
  },
  {
    "path": "android/.gitignore",
    "content": "# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore\n\n# Built application files\n*.apk\n*.aar\n*.ap_\n*.aab\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n#  Uncomment the following line in case you need and you don't have the release build type files in your app\n# release/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# IntelliJ\n*.iml\n.idea/workspace.xml\n.idea/tasks.xml\n.idea/gradle.xml\n.idea/assetWizardSettings.xml\n.idea/dictionaries\n.idea/libraries\n# Android Studio 3 in .gitignore file.\n.idea/caches\n.idea/modules.xml\n# Comment next line if keeping position of elements in Navigation Editor is relevant for you\n.idea/navEditor.xml\n\n# Keystore files\n# Uncomment the following lines if you do not want to check your keystore files in.\n#*.jks\n#*.keystore\n\n# External native build folder generated in Android Studio 2.2 and later\n.externalNativeBuild\n.cxx/\n\n# Google Services (e.g. APIs or Firebase)\n# google-services.json\n\n# Freeline\nfreeline.py\nfreeline/\nfreeline_project_description.json\n\n# fastlane\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots\nfastlane/test_output\nfastlane/readme.md\n\n# Version control\nvcs.xml\n\n# lint\nlint/intermediates/\nlint/generated/\nlint/outputs/\nlint/tmp/\n# lint/reports/\n\n# Android Profiling\n*.hprof\n\n# Cordova plugins for Capacitor\ncapacitor-cordova-android-plugins\n\n# Copied web assets\napp/src/main/assets/public\n\n# Generated Config files\napp/src/main/assets/capacitor.config.json\napp/src/main/assets/capacitor.plugins.json\napp/src/main/res/xml/config.xml\n"
  },
  {
    "path": "build/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n  <dict>\n    <key>com.apple.security.cs.allow-jit</key>\n    <true/>\n    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n    <true/>\n    <key>com.apple.security.cs.allow-dyld-environment-variables</key>\n    <true/>\n    <key>com.apple.security.network.client</key>\n    <true/>\n    <key>com.apple.security.network.server</key>\n    <true/>\n    <key>com.apple.security.files.user-selected.read-write</key>\n    <true/>\n    <key>com.apple.security.files.downloads.read-write</key>\n    <true/>\n    <key>com.apple.security.device.microphone</key>\n    <true/>\n  </dict>\n</plist>\n"
  },
  {
    "path": "build/installer.nsh",
    "content": "# 设置 Windows 7 兼容性\nManifestDPIAware true\nManifestSupportedOS all\n\n!macro customInit\n  # 检查系统版本\n  ${If} ${AtLeastWin7}\n    # Windows 7 或更高版本\n  ${Else}\n    MessageBox MB_OK|MB_ICONSTOP \"此应用程序需要 Windows 7 或更高版本。\"\n    Abort\n  ${EndIf}\n!macroend "
  },
  {
    "path": "dev-app-update.yml",
    "content": "provider: generic\nurl: https://example.com/auto-updates\nupdaterCacheDirName: electron-lan-file-updater\n"
  },
  {
    "path": "docs/custom-api-readme.md",
    "content": "## 🎵 自定义音源API配置\n\n现在支持通过导入一个简单的 JSON 配置文件来对接第三方的音乐解析 API。这将提供极大的灵活性，可以接入任意第三方音源。\n\n### 如何使用\n\n1.  前往 **设置 -> 播放设置 -> 音源设置**。\n2.  在 **自定义 API 设置** 区域，点击 **“导入 JSON 配置”** 按钮。\n3.  选择你已经编写好的 `xxx.json` 配置文件。\n4.  导入成功后，程序将优先使用你的自定义 API 进行解析。\n\n### JSON 配置文件格式说明\n\n导入的配置文件必须是一个合法的 JSON 文件，并包含以下字段：\n\n| 字段名            | 类型     | 是否必须 | 描述                                                                                                                        |\n| ----------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------------- |\n| `name`            | `string` | 是       | API 名称，将显示在应用的 UI 界面上。                                                                                        |\n| `apiUrl`          | `string` | 是       | API 的基础请求地址。                                                                                                        |\n| `method`          | `string` | 否       | HTTP 请求方法。可以是 `\"GET\"` 或 `\"POST\"`。**如果省略，默认为 \"GET\"**。                                                     |\n| `params`          | `object` | 是       | 请求时需要发送的参数。对于 `GET` 请求，它们会作为查询字符串；对于 `POST` 请求，它们会作为请求体。                           |\n| `qualityMapping`  | `object` | 否       | **音质映射表**。用于将应用内部的音质值（如 `\"lossless\"`）翻译成你的 API 需要的特定值。如果省略，则直接使用应用内部值。      |\n| `responseUrlPath` | `string` | 是       | **URL提取路径**。用于从 API 返回的 JSON 响应中找到最终可播放的音乐链接。支持点 `.` 和方括号 `[]` 语法来访问嵌套对象和数组。 |\n\n#### 占位符\n\n在 `params` 对象的值中，你可以使用以下占位符，程序在请求时会自动替换它们：\n\n- `{songId}`: 将被替换为当前歌曲的 ID。\n- `{quality}`: 将被替换为当前用户设置的音质字符串 (例如, `\"higher\"`, `\"lossless\"`)。\n\n#### 音质值列表\n\n应用内部使用的音质值如下，你可以在 `qualityMapping` 中使用它们作为**键**：\n`standard`, `higher`, `exhigh`, `lossless`, `hires`, `jyeffect`, `sky`, `dolby`, `jymaster`\n\n### 示例\n\n假设有一个 API 如下：\n`https://api.example.com/music?song_id=12345&bitrate=320000`\n它返回的 JSON 是：\n`{ \"code\": 200, \"data\": { \"play_url\": \"http://...\" } }`\n\n那么对应的 JSON 配置文件应该是：\n\n```json\n{\n  \"name\": \"Example API\",\n  \"apiUrl\": \"https://api.example.com/music\",\n  \"method\": \"GET\",\n  \"params\": {\n    \"song_id\": \"{songId}\",\n    \"bitrate\": \"{quality}\"\n  },\n  \"qualityMapping\": {\n    \"higher\": \"128000\",\n    \"exhigh\": \"320000\",\n    \"lossless\": \"999000\"\n  },\n  \"responseUrlPath\": \"data.play_url\"\n}\n```\n"
  },
  {
    "path": "electron.vite.config.ts",
    "content": "import vue from '@vitejs/plugin-vue';\nimport { defineConfig } from 'electron-vite';\nimport { resolve } from 'path';\nimport AutoImport from 'unplugin-auto-import/vite';\nimport { NaiveUiResolver } from 'unplugin-vue-components/resolvers';\nimport Components from 'unplugin-vue-components/vite';\nimport viteCompression from 'vite-plugin-compression';\nimport VueDevTools from 'vite-plugin-vue-devtools';\n\nexport default defineConfig({\n  main: {},\n  preload: {},\n  renderer: {\n    resolve: {\n      alias: {\n        '@': resolve('src/renderer'),\n        '@renderer': resolve('src/renderer'),\n        '@i18n': resolve('src/i18n')\n      }\n    },\n    plugins: [\n      vue(),\n      viteCompression(),\n      VueDevTools(),\n      AutoImport({\n        imports: [\n          'vue',\n          {\n            'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar']\n          }\n        ]\n      }),\n      Components({\n        resolvers: [NaiveUiResolver()]\n      })\n    ],\n    publicDir: resolve('resources'),\n    server: {\n      host: '0.0.0.0',\n      port: 2389\n    }\n  }\n});\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import js from '@eslint/js';\nimport typescript from '@typescript-eslint/eslint-plugin';\nimport typescriptParser from '@typescript-eslint/parser';\nimport vue from 'eslint-plugin-vue';\nimport vueParser from 'vue-eslint-parser';\nimport prettier from 'eslint-plugin-prettier';\nimport simpleImportSort from 'eslint-plugin-simple-import-sort';\nimport vueScopedCss from 'eslint-plugin-vue-scoped-css';\nimport globals from 'globals';\n\nexport default [\n  // 忽略文件配置\n  {\n    ignores: ['node_modules/**', 'dist/**', 'out/**', '.gitignore']\n  },\n\n  // 基础 JavaScript 配置\n  js.configs.recommended,\n\n  // JavaScript 文件配置\n  {\n    files: ['**/*.js', '**/*.cjs', '**/*.mjs'],\n    languageOptions: {\n      ecmaVersion: 'latest',\n      sourceType: 'module',\n      globals: {\n        ...globals.node,\n        ...globals.browser\n      }\n    },\n    rules: {\n      'no-console': 'off',\n      'no-undef': 'error'\n    }\n  },\n\n  // TypeScript 文件配置\n  {\n    files: ['**/*.ts', '**/*.tsx'],\n    languageOptions: {\n      parser: typescriptParser,\n      parserOptions: {\n        ecmaVersion: 'latest',\n        sourceType: 'module',\n        allowImportExportEverywhere: true,\n        ecmaFeatures: {\n          jsx: true\n        }\n      },\n      globals: {\n        ...globals.node,\n        ...globals.browser,\n        // Vue 3 特定全局变量\n        defineProps: 'readonly',\n        defineEmits: 'readonly',\n        // TypeScript 全局类型\n        NodeJS: 'readonly',\n        ScrollBehavior: 'readonly'\n      }\n    },\n    plugins: {\n      '@typescript-eslint': typescript,\n      'simple-import-sort': simpleImportSort\n    },\n    rules: {\n      '@typescript-eslint/no-explicit-any': 'off',\n      '@typescript-eslint/explicit-module-boundary-types': 'off',\n      '@typescript-eslint/no-unused-vars': [\n        'error',\n        // we are only using this rule to check for unused arguments since TS\n        // catches unused variables but not args.\n        { varsIgnorePattern: '.*', args: 'none' }\n      ],\n      '@typescript-eslint/no-use-before-define': 'off',\n      '@typescript-eslint/ban-ts-comment': 'off',\n      '@typescript-eslint/ban-types': 'off',\n      '@typescript-eslint/explicit-function-return-type': 'off',\n      'simple-import-sort/imports': 'error',\n      'simple-import-sort/exports': 'error',\n      'no-console': 'off',\n      'no-unused-vars': [\n        'error',\n        // we are only using this rule to check for unused arguments since TS\n        // catches unused variables but not args.\n        { varsIgnorePattern: '.*', args: 'none' }\n      ],\n      'no-use-before-define': 'off',\n      'max-classes-per-file': 'off',\n      'no-await-in-loop': 'off',\n      'dot-notation': 'off',\n      'constructor-super': 'off',\n      'getter-return': 'off',\n      'no-const-assign': 'off',\n      'no-dupe-args': 'off',\n      'no-dupe-class-members': 'off',\n      'no-dupe-keys': 'off',\n      'no-func-assign': 'off',\n      'no-import-assign': 'off',\n      'no-new-symbol': 'off',\n      'no-obj-calls': 'off',\n      'no-redeclare': 'off',\n      'no-setter-return': 'off',\n      'no-this-before-super': 'off',\n      'no-undef': 'off',\n      'no-unreachable': 'off',\n      'no-unsafe-negation': 'off',\n      'no-var': 'error',\n      'prefer-const': 'error',\n      'prefer-rest-params': 'error',\n      'prefer-spread': 'error',\n      'valid-typeof': 'off',\n      'consistent-return': 'off',\n      'no-promise-executor-return': 'off',\n      'prefer-promise-reject-errors': 'off'\n    }\n  },\n\n  // Vue 文件配置\n  {\n    files: ['**/*.vue'],\n    languageOptions: {\n      parser: vueParser,\n      parserOptions: {\n        parser: typescriptParser,\n        ecmaVersion: 'latest',\n        sourceType: 'module',\n        allowImportExportEverywhere: true\n      },\n      globals: {\n        ...globals.browser,\n        // Vue 3 特定全局变量\n        defineProps: 'readonly',\n        defineEmits: 'readonly',\n        // Vue 3 Composition API (如果使用了 unplugin-auto-import)\n        ref: 'readonly',\n        reactive: 'readonly',\n        computed: 'readonly',\n        watch: 'readonly',\n        watchEffect: 'readonly',\n        onMounted: 'readonly',\n        onUnmounted: 'readonly',\n        onBeforeUnmount: 'readonly',\n        nextTick: 'readonly',\n        inject: 'readonly',\n        provide: 'readonly',\n        // Naive UI (如果使用了 unplugin-auto-import)\n        useDialog: 'readonly',\n        useMessage: 'readonly',\n        // TypeScript 全局类型\n        NodeJS: 'readonly',\n        ScrollBehavior: 'readonly'\n      }\n    },\n    plugins: {\n      vue,\n      '@typescript-eslint': typescript,\n      prettier,\n      'simple-import-sort': simpleImportSort,\n      'vue-scoped-css': vueScopedCss\n    },\n    rules: {\n      // Vue 3 推荐规则\n      'vue/no-unused-vars': 'error',\n      'vue/no-unused-components': 'error',\n      'vue/no-multiple-template-root': 'off',\n      'vue/no-v-model-argument': 'off',\n      'vue/require-default-prop': 'off',\n      'vue/multi-word-component-names': 'off',\n      'vue/component-name-in-template-casing': ['error', 'kebab-case'],\n      'vue/no-reserved-props': 'off',\n      'vue/no-v-html': 'off',\n      'vue/first-attribute-linebreak': 'off',\n      'vue-scoped-css/enforce-style-type': [\n        'error',\n        {\n          allows: ['scoped']\n        }\n      ],\n      '@typescript-eslint/explicit-function-return-type': 'off',\n      '@typescript-eslint/no-explicit-any': 'off',\n      '@typescript-eslint/explicit-module-boundary-types': 'off',\n      'prettier/prettier': 'error',\n      'simple-import-sort/imports': 'error',\n      'simple-import-sort/exports': 'error'\n    }\n  },\n\n  // TypeScript 类型定义文件配置\n  {\n    files: ['**/*.d.ts'],\n    rules: {\n      'no-unused-vars': 'off',\n      '@typescript-eslint/no-unused-vars': 'off',\n      '@typescript-eslint/no-explicit-any': 'off',\n      '@typescript-eslint/no-empty-interface': 'off'\n    }\n  },\n\n  // JavaScript 第三方库文件配置\n  {\n    files: ['**/assets/**/*.js', '**/vendor/**/*.js', '**/lib/**/*.js'],\n    rules: {\n      'no-unused-vars': 'off',\n      'no-redeclare': 'off',\n      'no-self-assign': 'off',\n      'no-undef': 'off'\n    }\n  },\n\n  // 通用规则\n  {\n    files: ['**/*.js', '**/*.ts', '**/*.vue'],\n    rules: {\n      'no-console': 'off',\n      'no-underscore-dangle': 'off',\n      'no-nested-ternary': 'off',\n      'no-await-in-loop': 'off',\n      'no-continue': 'off',\n      'no-restricted-syntax': 'off',\n      'no-return-assign': 'off',\n      'no-unused-expressions': 'off',\n      'no-return-await': 'off',\n      'no-plusplus': 'off',\n      'no-param-reassign': 'off',\n      'no-shadow': 'off',\n      'guard-for-in': 'off',\n      'class-methods-use-this': 'off',\n      'no-case-declarations': 'off',\n      'no-unused-vars': [\n        'error',\n        // we are only using this rule to check for unused arguments since TS\n        // catches unused variables but not args.\n        { varsIgnorePattern: '.*', args: 'none' }\n      ]\n    }\n  }\n];\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"AlgerMusicPlayer\",\n  \"version\": \"5.0.0\",\n  \"description\": \"Alger Music Player\",\n  \"author\": \"Alger <algerkc@qq.com>\",\n  \"main\": \"./out/main/index.js\",\n  \"homepage\": \"https://github.com/algerkong/AlgerMusicPlayer\",\n  \"scripts\": {\n    \"prepare\": \"husky\",\n    \"format\": \"prettier --write ./src\",\n    \"lint\": \"eslint ./src --fix\",\n    \"typecheck:node\": \"tsc --noEmit -p tsconfig.node.json --composite false\",\n    \"typecheck:web\": \"vue-tsc --noEmit -p tsconfig.web.json --composite false\",\n    \"typecheck\": \"npm run typecheck:node && npm run typecheck:web\",\n    \"start\": \"electron-vite preview\",\n    \"dev\": \"electron-vite dev\",\n    \"dev:web\": \"vite dev\",\n    \"build\": \"electron-vite build\",\n    \"postinstall\": \"electron-builder install-app-deps\",\n    \"build:unpack\": \"npm run build && electron-builder --dir\",\n    \"build:win\": \"npm run build && electron-builder --win\",\n    \"build:mac\": \"npm run build && electron-builder --mac\",\n    \"build:linux\": \"npm run build && electron-builder --linux\"\n  },\n  \"lint-staged\": {\n    \"*.{ts,tsx,vue,js}\": [\n      \"eslint --fix\"\n    ],\n    \"*.{ts,tsx,vue,js,css,scss,md,json}\": [\n      \"prettier --write\"\n    ]\n  },\n  \"dependencies\": {\n    \"@electron-toolkit/preload\": \"^3.0.2\",\n    \"@electron-toolkit/utils\": \"^4.0.0\",\n    \"@unblockneteasemusic/server\": \"^0.27.10\",\n    \"cors\": \"^2.8.5\",\n    \"crypto-js\": \"^4.2.0\",\n    \"electron-store\": \"^8.2.0\",\n    \"electron-updater\": \"^6.6.2\",\n    \"electron-window-state\": \"^5.0.3\",\n    \"express\": \"^4.22.1\",\n    \"file-type\": \"^21.1.1\",\n    \"flac-tagger\": \"^1.0.7\",\n    \"font-list\": \"^1.6.0\",\n    \"form-data\": \"^4.0.5\",\n    \"husky\": \"^9.1.7\",\n    \"jsencrypt\": \"^3.5.4\",\n    \"music-metadata\": \"^11.10.3\",\n    \"netease-cloud-music-api-alger\": \"^4.26.1\",\n    \"node-fetch\": \"^2.7.0\",\n    \"node-id3\": \"^0.2.9\",\n    \"node-machine-id\": \"^1.1.12\",\n    \"pinia-plugin-persistedstate\": \"^4.7.1\",\n    \"sharp\": \"^0.34.5\",\n    \"vue-i18n\": \"^11.2.2\"\n  },\n  \"devDependencies\": {\n    \"@electron-toolkit/eslint-config\": \"^2.1.0\",\n    \"@electron-toolkit/eslint-config-ts\": \"^3.1.0\",\n    \"@electron-toolkit/tsconfig\": \"^1.0.1\",\n    \"@eslint/js\": \"^9.39.2\",\n    \"@rushstack/eslint-patch\": \"^1.15.0\",\n    \"@types/howler\": \"^2.2.12\",\n    \"@types/node\": \"^20.19.26\",\n    \"@types/node-fetch\": \"^2.6.13\",\n    \"@types/tinycolor2\": \"^1.4.6\",\n    \"@typescript-eslint/eslint-plugin\": \"^8.49.0\",\n    \"@typescript-eslint/parser\": \"^8.49.0\",\n    \"@vitejs/plugin-vue\": \"^5.2.4\",\n    \"@vue/compiler-sfc\": \"^3.5.25\",\n    \"@vue/eslint-config-prettier\": \"^10.2.0\",\n    \"@vue/eslint-config-typescript\": \"^14.6.0\",\n    \"@vue/runtime-core\": \"^3.5.25\",\n    \"@vueuse/core\": \"^11.3.0\",\n    \"@vueuse/electron\": \"^13.9.0\",\n    \"animate.css\": \"^4.1.1\",\n    \"autoprefixer\": \"^10.4.22\",\n    \"axios\": \"^1.13.2\",\n    \"cross-env\": \"^7.0.3\",\n    \"electron\": \"^39.2.7\",\n    \"electron-builder\": \"^26.0.12\",\n    \"electron-vite\": \"^5.0.0\",\n    \"eslint\": \"^9.39.2\",\n    \"eslint-config-prettier\": \"^10.1.8\",\n    \"eslint-plugin-import\": \"^2.32.0\",\n    \"eslint-plugin-prettier\": \"^5.5.4\",\n    \"eslint-plugin-simple-import-sort\": \"^12.1.1\",\n    \"eslint-plugin-vue\": \"^10.6.2\",\n    \"eslint-plugin-vue-scoped-css\": \"^2.12.0\",\n    \"globals\": \"^16.5.0\",\n    \"howler\": \"^2.2.4\",\n    \"lint-staged\": \"^15.5.2\",\n    \"lodash\": \"^4.17.21\",\n    \"marked\": \"^15.0.12\",\n    \"naive-ui\": \"^2.43.2\",\n    \"pinia\": \"^3.0.4\",\n    \"pinyin-match\": \"^1.2.10\",\n    \"postcss\": \"^8.5.6\",\n    \"prettier\": \"^3.7.4\",\n    \"remixicon\": \"^4.7.0\",\n    \"sass\": \"^1.96.0\",\n    \"tailwindcss\": \"^3.4.19\",\n    \"tinycolor2\": \"^1.6.0\",\n    \"tunajs\": \"^1.0.15\",\n    \"typescript\": \"^5.9.3\",\n    \"unplugin-auto-import\": \"^19.3.0\",\n    \"unplugin-vue-components\": \"^28.8.0\",\n    \"vite\": \"^6.4.1\",\n    \"vite-plugin-compression\": \"^0.5.1\",\n    \"vite-plugin-vue-devtools\": \"7.7.2\",\n    \"vue\": \"^3.5.25\",\n    \"vue-eslint-parser\": \"^10.2.0\",\n    \"vue-router\": \"^4.6.4\",\n    \"vue-tsc\": \"^2.2.12\"\n  },\n  \"build\": {\n    \"appId\": \"com.alger.music\",\n    \"productName\": \"AlgerMusicPlayer\",\n    \"publish\": [\n      {\n        \"provider\": \"github\",\n        \"owner\": \"algerkong\",\n        \"repo\": \"AlgerMusicPlayer\"\n      }\n    ],\n    \"extraResources\": [\n      {\n        \"from\": \"resources/html\",\n        \"to\": \"html\",\n        \"filter\": [\n          \"**/*\"\n        ]\n      }\n    ],\n    \"mac\": {\n      \"icon\": \"resources/icon.icns\",\n      \"target\": [\n        {\n          \"target\": \"dmg\",\n          \"arch\": [\n            \"universal\"\n          ]\n        }\n      ],\n      \"artifactName\": \"${productName}-${version}-mac-${arch}.${ext}\",\n      \"darkModeSupport\": true,\n      \"hardenedRuntime\": false,\n      \"gatekeeperAssess\": false,\n      \"entitlements\": \"build/entitlements.mac.plist\",\n      \"entitlementsInherit\": \"build/entitlements.mac.plist\",\n      \"extendInfo\": {\n        \"NSMicrophoneUsageDescription\": \"AlgerMusicPlayer needs access to the microphone for audio visualization.\",\n        \"NSCameraUsageDescription\": \"Application requests access to the device's camera.\",\n        \"NSDocumentsFolderUsageDescription\": \"Application requests access to the user's Documents folder.\",\n        \"NSDownloadsFolderUsageDescription\": \"Application requests access to the user's Downloads folder.\"\n      },\n      \"notarize\": false,\n      \"identity\": null,\n      \"type\": \"distribution\",\n      \"binaries\": [\n        \"Contents/MacOS/AlgerMusicPlayer\"\n      ]\n    },\n    \"win\": {\n      \"icon\": \"resources/icon.ico\",\n      \"target\": [\n        {\n          \"target\": \"nsis\",\n          \"arch\": [\n            \"x64\",\n            \"ia32\",\n            \"arm64\"\n          ]\n        }\n      ],\n      \"artifactName\": \"${productName}-${version}-win-${arch}.${ext}\",\n      \"requestedExecutionLevel\": \"asInvoker\"\n    },\n    \"linux\": {\n      \"icon\": \"resources/icon.png\",\n      \"target\": [\n        {\n          \"target\": \"AppImage\",\n          \"arch\": [\n            \"x64\",\n            \"arm64\"\n          ]\n        },\n        {\n          \"target\": \"deb\",\n          \"arch\": [\n            \"x64\",\n            \"arm64\"\n          ]\n        },\n        {\n          \"target\": \"rpm\",\n          \"arch\": [\n            \"x64\",\n            \"arm64\"\n          ]\n        }\n      ],\n      \"artifactName\": \"${productName}-${version}-linux-${arch}.${ext}\",\n      \"category\": \"Audio\",\n      \"maintainer\": \"Alger <algerkc@qq.com>\"\n    },\n    \"nsis\": {\n      \"oneClick\": false,\n      \"allowToChangeInstallationDirectory\": true,\n      \"installerIcon\": \"resources/icon.ico\",\n      \"uninstallerIcon\": \"resources/icon.ico\",\n      \"createDesktopShortcut\": true,\n      \"createStartMenuShortcut\": true,\n      \"shortcutName\": \"AlgerMusicPlayer\",\n      \"include\": \"build/installer.nsh\",\n      \"deleteAppDataOnUninstall\": true,\n      \"uninstallDisplayName\": \"AlgerMusicPlayer\"\n    }\n  },\n  \"pnpm\": {\n    \"onlyBuiltDependencies\": [\n      \"electron\",\n      \"esbuild\"\n    ]\n  }\n}\n"
  },
  {
    "path": "postcss.config.js",
    "content": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {}\n  }\n};\n"
  },
  {
    "path": "prettier.config.js",
    "content": "/**\n * @type {import('prettier').Config}\n */\nmodule.exports = {\n  singleQuote: true,\n  semi: true,\n  printWidth: 100,\n  trailingComma: 'none',\n  endOfLine: 'auto'\n};\n"
  },
  {
    "path": "resources/html/remote-control.html",
    "content": "<!doctype html>\n<html lang=\"zh-CN\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"\n    />\n    <title>AlgerMusicPlayer 远程控制</title>\n    <style>\n      :root {\n        --primary-color: #007aff;\n        --secondary-color: #5ac8fa;\n        --success-color: #4cd964;\n        --danger-color: #ff3b30;\n        --warning-color: #ff9500;\n        --light-gray: #f2f2f7;\n        --medium-gray: #e5e5ea;\n        --dark-gray: #8e8e93;\n        --text-color: #000000;\n        --text-secondary: #6c6c6c;\n        --background-color: #ffffff;\n      }\n\n      @media (prefers-color-scheme: dark) {\n        :root {\n          --primary-color: #0a84ff;\n          --secondary-color: #64d2ff;\n          --success-color: #30d158;\n          --danger-color: #ff453a;\n          --warning-color: #ff9f0a;\n          --light-gray: #1c1c1e;\n          --medium-gray: #2c2c2e;\n          --dark-gray: #8e8e93;\n          --text-color: #ffffff;\n          --text-secondary: #aeaeb2;\n          --background-color: #000000;\n        }\n      }\n\n      * {\n        margin: 0;\n        padding: 0;\n        box-sizing: border-box;\n        -webkit-tap-highlight-color: transparent;\n      }\n\n      body {\n        font-family:\n          -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', Arial, sans-serif;\n        line-height: 1.6;\n        color: var(--text-color);\n        background-color: var(--light-gray);\n        display: flex;\n        flex-direction: column;\n        min-height: 100vh;\n        padding: 0;\n        margin: 0;\n      }\n\n      .header {\n        position: sticky;\n        top: 0;\n        z-index: 100;\n        background-color: var(--background-color);\n        padding: 12px 16px;\n        text-align: center;\n        border-bottom: 1px solid var(--medium-gray);\n        backdrop-filter: blur(10px);\n        -webkit-backdrop-filter: blur(10px);\n      }\n\n      .header h1 {\n        font-size: 18px;\n        font-weight: 600;\n        margin: 0;\n      }\n\n      .container {\n        max-width: 540px;\n        margin: 0 auto;\n        padding: 16px;\n        width: 100%;\n        flex: 1;\n      }\n\n      .card {\n        background-color: var(--background-color);\n        border-radius: 12px;\n        padding: 16px;\n        margin-bottom: 16px;\n        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n      }\n\n      .song-info {\n        display: flex;\n        align-items: center;\n        padding-bottom: 16px;\n      }\n\n      .song-cover {\n        width: 72px;\n        height: 72px;\n        border-radius: 8px;\n        object-fit: cover;\n        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n        background-color: var(--medium-gray);\n      }\n\n      .song-details {\n        margin-left: 16px;\n        flex: 1;\n      }\n\n      .song-details h2 {\n        margin: 0;\n        font-size: 18px;\n        font-weight: 600;\n        color: var(--text-color);\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n\n      .song-details p {\n        margin: 4px 0 0;\n        font-size: 15px;\n        color: var(--text-secondary);\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n\n      .play-state {\n        font-size: 14px;\n        color: var(--primary-color);\n        margin-top: 4px;\n        display: flex;\n        align-items: center;\n      }\n\n      .play-state::before {\n        content: '';\n        display: inline-block;\n        width: 8px;\n        height: 8px;\n        border-radius: 50%;\n        margin-right: 6px;\n      }\n\n      .playing .play-state::before {\n        background-color: var(--success-color);\n      }\n\n      .paused .play-state::before {\n        background-color: var(--warning-color);\n      }\n\n      .controls {\n        display: grid;\n        grid-template-columns: repeat(3, 1fr);\n        gap: 12px;\n        margin-bottom: 16px;\n      }\n\n      .extra-controls {\n        display: grid;\n        grid-template-columns: repeat(2, 1fr);\n        gap: 12px;\n      }\n\n      .btn {\n        display: flex;\n        flex-direction: column;\n        align-items: center;\n        justify-content: center;\n        background-color: var(--background-color);\n        color: var(--primary-color);\n        border: 1px solid var(--medium-gray);\n        padding: 16px 0;\n        border-radius: 12px;\n        cursor: pointer;\n        font-size: 14px;\n        font-weight: 500;\n        transition: all 0.2s ease;\n        user-select: none;\n        position: relative;\n        overflow: hidden;\n      }\n\n      .btn:active {\n        transform: scale(0.97);\n        opacity: 0.7;\n      }\n\n      .btn::before {\n        content: '';\n        position: absolute;\n        top: 0;\n        left: 0;\n        right: 0;\n        bottom: 0;\n        background-color: var(--primary-color);\n        opacity: 0;\n        transition: opacity 0.2s ease;\n        z-index: -1;\n      }\n\n      .btn:active::before {\n        opacity: 0.1;\n      }\n\n      .btn svg {\n        margin-bottom: 8px;\n        width: 24px;\n        height: 24px;\n        fill: var(--primary-color);\n      }\n\n      .btn-play svg {\n        width: 28px;\n        height: 28px;\n        margin-bottom: 6px;\n      }\n\n      .status-bar {\n        text-align: center;\n        padding: 8px 16px;\n        font-size: 14px;\n        color: var(--text-secondary);\n        background-color: var(--background-color);\n        border-top: 1px solid var(--medium-gray);\n        position: sticky;\n        bottom: 0;\n      }\n\n      .status-message {\n        display: inline-block;\n        transition: opacity 0.3s ease;\n      }\n\n      .status-message.fade {\n        opacity: 0;\n      }\n\n      .refresh-button {\n        color: var(--primary-color);\n        background: none;\n        border: none;\n        font-size: 14px;\n        cursor: pointer;\n        padding: 0;\n        margin-left: 8px;\n      }\n\n      @media (max-width: 350px) {\n        .controls,\n        .extra-controls {\n          gap: 8px;\n        }\n\n        .btn {\n          padding: 12px 0;\n          font-size: 12px;\n        }\n\n        .btn svg {\n          width: 20px;\n          height: 20px;\n          margin-bottom: 6px;\n        }\n      }\n    </style>\n  </head>\n  <body>\n    <div class=\"header\">\n      <h1>AlgerMusicPlayer 远程控制</h1>\n    </div>\n\n    <div class=\"container\">\n      <div class=\"card\" id=\"songInfoCard\">\n        <div class=\"song-info\">\n          <img\n            id=\"songCover\"\n            class=\"song-cover\"\n            src=\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%238E8E93'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 14.5c-2.49 0-4.5-2.01-4.5-4.5S9.51 7.5 12 7.5s4.5 2.01 4.5 4.5-2.01 4.5-4.5 4.5zm0-5.5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1z'/%3E%3C/svg%3E\"\n            alt=\"封面\"\n          />\n          <div class=\"song-details\">\n            <h2 id=\"songTitle\">未在播放</h2>\n            <p id=\"songArtist\">--</p>\n            <div class=\"play-state\" id=\"playState\">未播放</div>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"card\">\n        <div class=\"controls\">\n          <button id=\"prevBtn\" class=\"btn\">\n            <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n              <path d=\"M6 6h2v12H6zm3.5 6l8.5 6V6z\" />\n            </svg>\n            上一首\n          </button>\n          <button id=\"playBtn\" class=\"btn btn-play\">\n            <svg id=\"playIcon\" viewBox=\"0 0 24 24\" fill=\"currentColor\">\n              <path d=\"M8 5v14l11-7z\" />\n            </svg>\n            播放/暂停\n          </button>\n          <button id=\"nextBtn\" class=\"btn\">\n            <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n              <path d=\"M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z\" />\n            </svg>\n            下一首\n          </button>\n        </div>\n\n        <div class=\"extra-controls\">\n          <button id=\"volumeDownBtn\" class=\"btn\">\n            <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n              <path\n                d=\"M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z\"\n              />\n            </svg>\n            音量-\n          </button>\n          <button id=\"volumeUpBtn\" class=\"btn\">\n            <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n              <path\n                d=\"M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z\"\n              />\n            </svg>\n            音量+\n          </button>\n        </div>\n      </div>\n\n      <div class=\"card\">\n        <div class=\"extra-controls\">\n          <button id=\"favoriteBtn\" class=\"btn\">\n            <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n              <path\n                d=\"M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z\"\n              />\n            </svg>\n            收藏\n          </button>\n          <button id=\"refreshBtn\" class=\"btn\">\n            <svg viewBox=\"0 0 24 24\" fill=\"currentColor\">\n              <path\n                d=\"M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z\"\n              />\n            </svg>\n            刷新\n          </button>\n        </div>\n      </div>\n    </div>\n\n    <div class=\"status-bar\">\n      <span id=\"status\" class=\"status-message\">准备就绪</span>\n    </div>\n\n    <script>\n      // 页面加载完成后执行\n      document.addEventListener('DOMContentLoaded', () => {\n        // 获取DOM元素\n        const songInfoCard = document.getElementById('songInfoCard');\n        const songTitle = document.getElementById('songTitle');\n        const songArtist = document.getElementById('songArtist');\n        const songCover = document.getElementById('songCover');\n        const playState = document.getElementById('playState');\n        const playBtn = document.getElementById('playBtn');\n        const playIcon = document.getElementById('playIcon');\n        const prevBtn = document.getElementById('prevBtn');\n        const nextBtn = document.getElementById('nextBtn');\n        const favoriteBtn = document.getElementById('favoriteBtn');\n        const volumeUpBtn = document.getElementById('volumeUpBtn');\n        const volumeDownBtn = document.getElementById('volumeDownBtn');\n        const refreshBtn = document.getElementById('refreshBtn');\n        const status = document.getElementById('status');\n\n        let isPlaying = false;\n\n        // 显示状态消息并淡出\n        function showStatus(message, autoClear = true) {\n          status.textContent = message;\n          status.classList.remove('fade');\n\n          if (autoClear) {\n            setTimeout(() => {\n              status.classList.add('fade');\n            }, 2000);\n          }\n        }\n\n        // 更新播放/暂停图标\n        function updatePlayIcon() {\n          if (isPlaying) {\n            playIcon.innerHTML = '<path d=\"M6 19h4V5H6v14zm8-14v14h4V5h-4z\"/>';\n          } else {\n            playIcon.innerHTML = '<path d=\"M8 5v14l11-7z\"/>';\n          }\n        }\n\n        // 更新状态的函数\n        async function updateStatus() {\n          try {\n            showStatus('获取播放状态...', false);\n\n            const response = await fetch('/api/status');\n            const data = await response.json();\n\n            // 更新播放状态\n            isPlaying = data.isPlaying;\n            updatePlayIcon();\n\n            // 更新UI\n            if (data.currentSong) {\n              songTitle.textContent = data.currentSong.name || '未知歌曲';\n\n              if (data.currentSong.ar && data.currentSong.ar.length) {\n                songArtist.textContent = data.currentSong.ar.map((a) => a.name).join(', ');\n              } else if (data.currentSong.artists && data.currentSong.artists.length) {\n                songArtist.textContent = data.currentSong.artists.map((a) => a.name).join(', ');\n              } else {\n                songArtist.textContent = '未知艺术家';\n              }\n\n              if (data.currentSong.picUrl) {\n                songCover.src = data.currentSong.picUrl;\n              }\n            } else {\n              songTitle.textContent = '未在播放';\n              songArtist.textContent = '--';\n              songCover.src =\n                \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%238E8E93'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 14.5c-2.49 0-4.5-2.01-4.5-4.5S9.51 7.5 12 7.5s4.5 2.01 4.5 4.5-2.01 4.5-4.5 4.5zm0-5.5c-.55 0-1 .45-1 1s.45 1 1 1 1-.45 1-1-.45-1-1-1z'/%3E%3C/svg%3E\";\n            }\n\n            // 更新播放状态\n            playState.textContent = isPlaying ? '正在播放' : '已暂停';\n            songInfoCard.className = isPlaying ? 'card playing' : 'card paused';\n\n            showStatus('已更新', true);\n          } catch (error) {\n            console.error('获取状态失败:', error);\n            showStatus('获取状态失败');\n          }\n        }\n\n        // 发送命令的函数\n        async function sendCommand(endpoint) {\n          try {\n            showStatus('发送命令中...', false);\n            const response = await fetch('/api/' + endpoint, { method: 'POST' });\n            const data = await response.json();\n\n            showStatus(data.message || '命令已发送');\n\n            // 稍等后更新状态\n            setTimeout(updateStatus, 500);\n          } catch (error) {\n            console.error('发送命令失败:', error);\n            showStatus('发送命令失败');\n          }\n        }\n\n        // 绑定按钮事件\n        playBtn.addEventListener('click', () => sendCommand('toggle-play'));\n        prevBtn.addEventListener('click', () => sendCommand('prev'));\n        nextBtn.addEventListener('click', () => sendCommand('next'));\n        favoriteBtn.addEventListener('click', () => sendCommand('toggle-favorite'));\n        volumeUpBtn.addEventListener('click', () => sendCommand('volume-up'));\n        volumeDownBtn.addEventListener('click', () => sendCommand('volume-down'));\n        refreshBtn.addEventListener('click', updateStatus);\n\n        // 初始加载状态\n        updateStatus();\n\n        // 每1秒更新一次状态\n        setInterval(updateStatus, 1000);\n\n        // 添加触摸反馈\n        const buttons = document.querySelectorAll('.btn');\n        buttons.forEach((btn) => {\n          btn.addEventListener('touchstart', function () {\n            this.style.transform = 'scale(0.97)';\n            this.style.opacity = '0.7';\n          });\n\n          btn.addEventListener('touchend', function () {\n            this.style.transform = 'scale(1)';\n            this.style.opacity = '1';\n          });\n        });\n\n        // 检测深色模式变化\n        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {\n          updateStatus();\n        });\n      });\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "resources/manifest.json",
    "content": "{\n  \"name\": \"Alger Music PWA\",\n  \"icons\": [\n    {\n      \"src\": \"./icon.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"256x256\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src/i18n/lang/en-US/artist.ts",
    "content": "export default {\n  hotSongs: 'Hot Songs',\n  albums: 'Albums',\n  description: 'Artist Introduction'\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/bilibili.ts",
    "content": "export default {\n  player: {\n    loading: 'Loading audio...',\n    retry: 'Retry',\n    playNow: 'Play Now',\n    loadingTitle: 'Loading...',\n    totalDuration: 'Total Duration: {duration}',\n    partsList: 'Parts List ({count} episodes)',\n    playStarted: 'Playback started',\n    switchingPart: 'Switching to part: {part}',\n    preloadingNext: 'Preloading next part: {part}',\n    playingCurrent: 'Playing current selected part: {name}',\n    num: 'M',\n    errors: {\n      invalidVideoId: 'Invalid video ID',\n      loadVideoDetailFailed: 'Failed to load video details',\n      loadPartInfoFailed: 'Unable to load video part information',\n      loadAudioUrlFailed: 'Failed to get audio playback URL',\n      videoDetailNotLoaded: 'Video details not loaded',\n      missingParams: 'Missing required parameters',\n      noAvailableAudioUrl: 'No available audio URL found',\n      loadPartAudioFailed: 'Failed to load part audio URL',\n      audioListEmpty: 'Audio list is empty, please retry',\n      currentPartNotFound: 'Current part audio not found',\n      audioUrlFailed: 'Failed to get audio URL',\n      playFailed: 'Playback failed, please retry',\n      getAudioUrlFailed: 'Failed to get audio URL, please retry',\n      audioNotFound: 'Corresponding audio not found, please retry',\n      preloadFailed: 'Failed to preload next part',\n      switchPartFailed: 'Failed to load audio URL when switching parts'\n    },\n    console: {\n      loadingDetail: 'Loading Bilibili video details',\n      detailData: 'Bilibili video detail data',\n      multipleParts: 'Video has multiple parts, total {count}',\n      noPartsData: 'Video has no parts or part data is empty',\n      loadingAudioSource: 'Loading audio source',\n      generatedAudioList: 'Generated audio list, total {count}',\n      getDashAudioUrl: 'Got dash audio URL',\n      getDurlAudioUrl: 'Got durl audio URL',\n      loadingPartAudio: 'Loading part audio URL: {part}, cid: {cid}',\n      loadPartAudioFailed: 'Failed to load part audio URL: {part}',\n      switchToPart: 'Switching to part: {part}',\n      audioNotFoundInList: 'Corresponding audio item not found',\n      preparingToPlay: 'Preparing to play current selected part: {name}',\n      preloadingNextPart: 'Preloading next part: {part}',\n      playingSelectedPart: 'Playing current selected part: {name}, audio URL: {url}',\n      preloadNextFailed: 'Failed to preload next part'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/common.ts",
    "content": "export default {\n  play: 'Play',\n  next: 'Next',\n  previous: 'Previous',\n  volume: 'Volume',\n  settings: 'Settings',\n  search: 'Search',\n  loading: 'Loading...',\n  loadingMore: 'Loading more...',\n  alipay: 'Alipay',\n  wechat: 'WeChat Pay',\n  on: 'On',\n  off: 'Off',\n  show: 'Show',\n  hide: 'Hide',\n  confirm: 'Confirm',\n  cancel: 'Cancel',\n  configure: 'Configure',\n  open: 'Open',\n  modify: 'Modify',\n  success: 'Operation Successful',\n  error: 'Operation Failed',\n  warning: 'Warning',\n  info: 'Info',\n  save: 'Save',\n  delete: 'Delete',\n  refresh: 'Refresh',\n  retry: 'Retry',\n  reset: 'Reset',\n  back: 'Back',\n  copySuccess: 'Copied to clipboard',\n  copyFailed: 'Copy failed',\n  validation: {\n    required: 'This field is required',\n    invalidInput: 'Invalid input',\n    selectRequired: 'Please select an option',\n    numberRange: 'Please enter a number between {min} and {max}',\n    ipAddress: 'Please enter a valid IP address',\n    portNumber: 'Please enter a valid port number (1-65535)'\n  },\n  viewMore: 'View More',\n  noMore: 'No more',\n  selectAll: 'Select All',\n  expand: 'Expand',\n  collapse: 'Collapse',\n  songCount: '{count} songs',\n  language: 'Language',\n  today: 'Today',\n  yesterday: 'Yesterday',\n  tray: {\n    show: 'Show',\n    quit: 'Quit',\n    playPause: 'Play/Pause',\n    prev: 'Previous',\n    next: 'Next',\n    pause: 'Pause',\n    play: 'Play',\n    favorite: 'Favorite'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/comp.ts",
    "content": "export default {\n  installApp: {\n    description: 'Install the application for a better experience',\n    noPrompt: 'Do not prompt again',\n    install: 'Install now',\n    cancel: 'Cancel',\n    download: 'Download',\n    downloadFailed: 'Download failed',\n    downloadComplete: 'Download complete',\n    downloadProblem: 'Download problem? Go to',\n    downloadProblemLinkText: 'Download the latest version'\n  },\n  playlistDrawer: {\n    title: 'Add to playlist',\n    createPlaylist: 'Create new playlist',\n    cancelCreate: 'Cancel create',\n    create: 'Create',\n    playlistName: 'Playlist name',\n    privatePlaylist: 'Private playlist',\n    publicPlaylist: 'Public playlist',\n    createSuccess: 'Playlist created successfully',\n    createFailed: 'Playlist creation failed',\n    addSuccess: 'Song added successfully',\n    addFailed: 'Song addition failed',\n    private: 'Private',\n    public: 'Public',\n    count: 'songs',\n    loginFirst: 'Please login first',\n    getPlaylistFailed: 'Get playlist failed',\n    inputPlaylistName: 'Please enter the playlist name'\n  },\n  update: {\n    title: 'New version found',\n    currentVersion: 'Current version',\n    cancel: 'Do not update',\n    prepareDownload: 'Preparing to download...',\n    downloading: 'Downloading...',\n    nowUpdate: 'Update now',\n    downloadFailed: 'Download failed, please try again or download manually',\n    startFailed: 'Start download failed, please try again or download manually',\n    noDownloadUrl:\n      'No suitable installation package found for the current system, please download manually',\n    installConfirmTitle: 'Install Update',\n    installConfirmContent: 'Do you want to close the application and install the update?',\n    manualInstallTip:\n      'If the installer does not open automatically after closing the application, please find the file in your download folder and open it manually.',\n    yesInstall: 'Install Now',\n    noThanks: 'Later',\n    fileLocation: 'File Location',\n    copy: 'Copy Path',\n    copySuccess: 'Path copied to clipboard',\n    copyFailed: 'Copy failed',\n    backgroundDownload: 'Background Download'\n  },\n  disclaimer: {\n    title: 'Terms of Use',\n    warning:\n      'This application is a development test version. Functions are not yet perfect, and there may be many problems and bugs. It is for learning and exchange only.',\n    item1:\n      'This application is for personal learning, research and technical exchange only. Please do not use it for any commercial purposes.',\n    item2:\n      'Please delete it within 24 hours after downloading. If you need to use it for a long time, please support the genuine music service.',\n    item3:\n      'By using this application, you understand and assume the relevant risks. The developer is not responsible for any loss.',\n    agree: 'I have read and agree',\n    disagree: 'Disagree and Exit'\n  },\n  donate: {\n    title: 'Support Developer',\n    subtitle: 'Your support is my motivation',\n    tip: 'Donation is completely voluntary. All functions can be used normally without donation. Thank you for your understanding and support!',\n    wechat: 'WeChat',\n    alipay: 'Alipay',\n    wechatQR: 'WeChat QR Code',\n    alipayQR: 'Alipay QR Code',\n    scanTip: 'Please use your phone to scan the QR code above to donate',\n    enterApp: 'Enter App',\n    noForce: 'No forced donation, click to enter'\n  },\n  coffee: {\n    title: 'Buy me a coffee',\n    alipay: 'Alipay',\n    wechat: 'Wechat',\n    alipayQR: 'Alipay QR code',\n    wechatQR: 'Wechat QR code',\n    coffeeDesc: 'A cup of coffee, a support',\n    coffeeDescLinkText: 'View more',\n    groupText: 'Wechat Public Account: AlgerMusic',\n    messages: {\n      copySuccess: 'Copied to clipboard'\n    },\n    donateList: 'Buy me a coffee'\n  },\n  playlistType: {\n    title: 'Playlist Category',\n    showAll: 'Show all',\n    hide: 'Hide some'\n  },\n  recommendAlbum: {\n    title: 'Latest Album'\n  },\n  recommendSinger: {\n    title: 'Daily Recommendation',\n    songlist: 'Daily Recommendation List'\n  },\n  recommendSonglist: {\n    title: 'Weekly Hot Music'\n  },\n  searchBar: {\n    login: 'Login',\n    toLogin: 'To Login',\n    logout: 'Logout',\n    set: 'Settings',\n    theme: 'Theme',\n    restart: 'Restart',\n    refresh: 'Refresh',\n    currentVersion: 'Current Version',\n    searchPlaceholder: 'Search for something...',\n    zoom: 'Zoom',\n    zoom100: 'Zoom 100%',\n    resetZoom: 'Reset Zoom',\n    zoomDefault: 'Default Zoom'\n  },\n  titleBar: {\n    closeTitle: 'Choose how to close',\n    minimizeToTray: 'Minimize to Tray',\n    exitApp: 'Exit App',\n    rememberChoice: 'Remember my choice',\n    closeApp: 'Close App'\n  },\n  userPlayList: {\n    title: \"{name}'s Playlist\"\n  },\n  musicList: {\n    searchSongs: 'Search Songs',\n    noSearchResults: 'No search results',\n    switchToNormal: 'Switch to normal layout',\n    switchToCompact: 'Switch to compact layout',\n    playAll: 'Play All',\n    collect: 'Collect',\n    collectSuccess: 'Collect Success',\n    cancelCollectSuccess: 'Cancel Collect Success',\n    cancelCollect: 'Cancel Collect',\n    addToPlaylist: 'Add to Playlist',\n    addToPlaylistSuccess: 'Add to Playlist Success',\n    operationFailed: 'Operation Failed',\n    songsAlreadyInPlaylist: 'Songs already in playlist',\n    historyRecommend: 'Daily History',\n    fetchDatesFailed: 'Failed to fetch dates',\n    fetchSongsFailed: 'Failed to fetch songs',\n    noSongs: 'No songs'\n  },\n  playlist: {\n    import: {\n      button: 'Import Playlist',\n      title: 'Import Playlist',\n      description: 'Import playlists via metadata, text, or links',\n      linkTab: 'Import by Link',\n      textTab: 'Import by Text',\n      localTab: 'Import by Metadata',\n      linkPlaceholder: 'Enter playlist links, one per line',\n      textPlaceholder: 'Enter song information in format: Song Name Artist Name',\n      localPlaceholder: 'Enter song metadata in JSON format',\n      linkTips: 'Supported link sources:',\n      linkTip1: 'Copy links after sharing playlists to WeChat/Weibo/QQ',\n      linkTip2: 'Directly copy playlist/profile links',\n      linkTip3: 'Directly copy article links',\n      textTips: 'Enter song information, one song per line',\n      textFormat: 'Format: Song Name Artist Name',\n      localTips: 'Add song metadata',\n      localFormat: 'Format example:',\n      songNamePlaceholder: 'Song Name',\n      artistNamePlaceholder: 'Artist Name',\n      albumNamePlaceholder: 'Album Name',\n      addSongButton: 'Add Song',\n      addLinkButton: 'Add Link',\n      importToStarPlaylist: 'Import to My Favorite Music',\n      playlistNamePlaceholder: 'Enter playlist name',\n      importButton: 'Start Import',\n      emptyLinkWarning: 'Please enter playlist links',\n      emptyTextWarning: 'Please enter song information',\n      emptyLocalWarning: 'Please enter song metadata',\n      invalidJsonFormat: 'Invalid JSON format',\n      importSuccess: 'Import task created successfully',\n      importFailed: 'Import failed',\n      importStatus: 'Import Status',\n      refresh: 'Refresh',\n      taskId: 'Task ID',\n      status: 'Status',\n      successCount: 'Success Count',\n      failReason: 'Failure Reason',\n      unknownError: 'Unknown error',\n      statusPending: 'Pending',\n      statusProcessing: 'Processing',\n      statusSuccess: 'Success',\n      statusFailed: 'Failed',\n      statusUnknown: 'Unknown',\n      taskList: 'Task List',\n      taskListTitle: 'Import Task List',\n      action: 'Action',\n      select: 'Select',\n      fetchTaskListFailed: 'Failed to fetch task list',\n      noTasks: 'No import tasks',\n      clearTasks: 'Clear Tasks',\n      clearTasksConfirmTitle: 'Confirm Clear',\n      clearTasksConfirmContent:\n        'Are you sure you want to clear all import task records? This action cannot be undone.',\n      confirm: 'Confirm',\n      cancel: 'Cancel',\n      clearTasksSuccess: 'Task list cleared',\n      clearTasksFailed: 'Failed to clear task list'\n    }\n  },\n  settings: 'Settings',\n  user: 'User',\n  toplist: 'Toplist',\n  history: 'History',\n  list: 'Playlist',\n  mv: 'MV',\n  home: 'Home',\n  search: 'Search'\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/donation.ts",
    "content": "export default {\n  description:\n    'Your donation will be used to support development and maintenance work, including but not limited to server maintenance, domain name renewal, etc.',\n  message: 'You can leave your email or github name when leaving a message.',\n  refresh: 'Refresh List',\n  toDonateList: 'Buy me a coffee',\n  title: 'Donation List',\n  noMessage: 'No Message'\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/download.ts",
    "content": "export default {\n  title: 'Download Manager',\n  localMusic: 'Local Music',\n  count: '{count} songs in total',\n  clearAll: 'Clear All',\n  settings: 'Settings',\n  tabs: {\n    downloading: 'Downloading',\n    downloaded: 'Downloaded'\n  },\n  empty: {\n    noTasks: 'No download tasks',\n    noDownloaded: 'No downloaded songs',\n    noDownloadedHint: 'Download your favorite songs to listen offline'\n  },\n  progress: {\n    total: 'Total Progress: {progress}%'\n  },\n  items: 'items',\n  status: {\n    downloading: 'Downloading',\n    completed: 'Completed',\n    failed: 'Failed',\n    unknown: 'Unknown'\n  },\n  artist: {\n    unknown: 'Unknown Artist'\n  },\n  delete: {\n    title: 'Delete Confirmation',\n    message: 'Are you sure you want to delete \"{filename}\"? This action cannot be undone.',\n    confirm: 'Delete',\n    cancel: 'Cancel',\n    success: 'Successfully deleted',\n    failed: 'Failed to delete',\n    fileNotFound: 'File not found or moved, removed from records',\n    recordRemoved: 'Failed to delete file, but removed from records'\n  },\n  clear: {\n    title: 'Clear Download Records',\n    message:\n      'Are you sure you want to clear all download records? This will not delete the actual music files, but will clear all records.',\n    confirm: 'Clear',\n    cancel: 'Cancel',\n    success: 'Download records cleared'\n  },\n  message: {\n    downloadComplete: '{filename} download completed',\n    downloadFailed: '{filename} download failed: {error}',\n    alreadyDownloading: '{filename} is already downloading'\n  },\n  loading: 'Loading...',\n  playStarted: 'Play started: {name}',\n  playFailed: 'Play failed: {name}',\n  path: {\n    copied: 'Path copied to clipboard',\n    copyFailed: 'Failed to copy path'\n  },\n  settingsPanel: {\n    title: 'Download Settings',\n    path: 'Download Location',\n    pathDesc: 'Set where your music files will be saved',\n    pathPlaceholder: 'Please select download path',\n    noPathSelected: 'Please select download path first',\n    select: 'Select Folder',\n    open: 'Open Folder',\n    fileFormat: 'Filename Format',\n    fileFormatDesc: 'Set how downloaded music files will be named',\n    customFormat: 'Custom Format',\n    separator: 'Separator',\n    separators: {\n      dash: 'Space-dash-space',\n      underscore: 'Underscore',\n      space: 'Space'\n    },\n    dragToArrange: 'Sort or use arrow buttons to arrange:',\n    formatVariables: 'Available variables',\n    preview: 'Preview:',\n    saveSuccess: 'Download settings saved',\n    presets: {\n      songArtist: 'Song - Artist',\n      artistSong: 'Artist - Song',\n      songOnly: 'Song only'\n    },\n    components: {\n      songName: 'Song name',\n      artistName: 'Artist name',\n      albumName: 'Album name'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/favorite.ts",
    "content": "export default {\n  title: 'Favorites',\n  count: 'Total {count}',\n  batchDownload: 'Batch Download',\n  selectAll: 'All',\n  download: 'Download ({count})',\n  cancel: 'Cancel',\n  emptyTip: 'No favorite songs yet',\n  viewMore: 'View More',\n  noMore: 'No more',\n  downloadSuccess: 'Download completed',\n  downloadFailed: 'Download failed',\n  downloading: 'Downloading, please wait...',\n  selectSongsFirst: 'Please select songs to download first',\n  descending: 'Descending',\n  ascending: 'Ascending'\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/history.ts",
    "content": "export default {\n  title: 'Play History',\n  heatmapTitle: 'Heatmap',\n  playCount: '{count}',\n  getHistoryFailed: 'Failed to get play history',\n  categoryTabs: {\n    songs: 'Songs',\n    playlists: 'Playlists',\n    albums: 'Albums'\n  },\n  tabs: {\n    all: 'All Records',\n    local: 'Local Records',\n    cloud: 'Cloud Records'\n  },\n  getCloudRecordFailed: 'Failed to get cloud records',\n  needLogin: 'Please login with cookie to view cloud records',\n  merging: 'Merging records...',\n  noDescription: 'No description',\n  noData: 'No records',\n  newKey: 'New translation',\n  heatmap: {\n    title: 'Play Heatmap',\n    loading: 'Loading data...',\n    unit: 'plays',\n    footerText: 'Hover to view details',\n    playCount: 'Played {count} times',\n    topSongs: 'Top songs of the day',\n    times: 'times',\n    totalPlays: 'Total Plays',\n    activeDays: 'Active Days',\n    noData: 'No play records',\n    colorTheme: 'Color Theme',\n    colors: {\n      green: 'Green',\n      blue: 'Blue',\n      orange: 'Orange',\n      purple: 'Purple',\n      red: 'Red'\n    },\n    mostPlayedSong: 'Most Played Song',\n    mostActiveDay: 'Most Active Day',\n    latestNightSong: 'Latest Night Song'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/login.ts",
    "content": "export default {\n  title: {\n    qr: 'QR Code Login',\n    phone: 'Phone Login',\n    cookie: 'Cookie Login',\n    uid: 'UID Login'\n  },\n  qrTip: 'Scan with NetEase Cloud Music APP',\n  phoneTip: 'Login with NetEase Cloud account',\n  tokenTip: 'Enter a valid NetEase Cloud Music Cookie to login',\n  uidTip: 'Enter User ID for quick login',\n  placeholder: {\n    phone: 'Phone Number',\n    password: 'Password',\n    cookie: 'Please enter NetEase Cloud Music Cookie (token)',\n    uid: 'Please enter User ID (UID)'\n  },\n  button: {\n    login: 'Login',\n    switchToQr: 'QR Code Login',\n    switchToPhone: 'Phone Login',\n    switchToToken: 'Use Cookie Login',\n    switchToUid: 'UID Login',\n    backToQr: 'Back to QR Code Login',\n    cookieLogin: 'Cookie Login',\n    autoGetCookie: 'Auto Get Cookie',\n    refresh: 'Click to Refresh',\n    refreshing: 'Refreshing...',\n    refreshQr: 'Refresh QR Code'\n  },\n  message: {\n    loginSuccess: 'Login successful',\n    loginFailed: 'Login failed',\n    tokenLoginSuccess: 'Cookie login successful',\n    uidLoginSuccess: 'UID login successful',\n    loadError: 'Error loading login information',\n    qrCheckError: 'Error checking QR code status',\n    tokenRequired: 'Please enter Cookie',\n    tokenInvalid: 'Invalid Cookie, please check and try again',\n    uidRequired: 'Please enter User ID',\n    uidInvalid: 'Invalid User ID or user does not exist',\n    uidLoginFailed: 'UID login failed, please check if User ID is correct',\n    phoneRequired: 'Please enter phone number',\n    passwordRequired: 'Please enter password',\n    phoneLoginFailed: 'Phone login failed, please check if phone number and password are correct',\n    autoGetCookieSuccess: 'Auto get Cookie successful',\n    autoGetCookieFailed: 'Auto get Cookie failed',\n    autoGetCookieTip:\n      'Will open NetEase Cloud Music login page, please complete login and close the window',\n    qrCheckFailed: 'Failed to check QR code status, please refresh and try again',\n    qrLoading: 'Loading QR code...',\n    qrExpired: 'QR code has expired, please click to refresh',\n    qrExpiredShort: 'QR code expired',\n    qrExpiredWarning: 'QR code has expired, please click to refresh for a new one',\n    qrScanned: 'QR code scanned, please confirm login on your phone',\n    qrScannedShort: 'Scanned',\n    qrScannedInfo: 'QR code scanned, please confirm login on your phone',\n    qrConfirmed: 'Login successful, redirecting...',\n    qrGenerating: 'Generating QR code...'\n  },\n  qrTitle: 'NetEase Cloud Music QR Code Login',\n  uidWarning:\n    'Note: UID login is only for viewing user public information and cannot access features that require login permissions.'\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/player.ts",
    "content": "export default {\n  nowPlaying: 'Now Playing',\n  playlist: 'Playlist',\n  lyrics: 'Lyrics',\n  previous: 'Previous',\n  play: 'Play',\n  pause: 'Pause',\n  next: 'Next',\n  volumeUp: 'Volume Up',\n  volumeDown: 'Volume Down',\n  mute: 'Mute',\n  unmute: 'Unmute',\n  songNum: 'Song Number: {num}',\n  addCorrection: 'Add {num} seconds',\n  subtractCorrection: 'Subtract {num} seconds',\n  playFailed: 'Play Failed, Play Next Song',\n  parseFailedPlayNext: 'Song parsing failed, playing next',\n  consecutiveFailsError:\n    'Playback error, possibly due to network issues or invalid source. Please switch playlist or try again later',\n  playMode: {\n    sequence: 'Sequence',\n    loop: 'Loop',\n    random: 'Random'\n  },\n  fullscreen: {\n    enter: 'Enter Fullscreen',\n    exit: 'Exit Fullscreen'\n  },\n  close: 'Close',\n  modeHint: {\n    single: 'Single',\n    list: 'Next'\n  },\n  lrc: {\n    noLrc: 'No lyrics, please enjoy',\n    noAutoScroll: 'This lyrics does not support auto-scroll'\n  },\n  reparse: {\n    title: 'Select Music Source',\n    desc: 'Click a source to directly reparse the current song. This source will be used next time this song plays.',\n    success: 'Reparse successful',\n    failed: 'Reparse failed',\n    warning: 'Please select a music source',\n    bilibiliNotSupported: 'Bilibili videos do not support reparsing',\n    processing: 'Processing...',\n    clear: 'Clear Custom Source',\n    customApiFailed: 'Custom API parsing failed, trying built-in sources...',\n    customApiError: 'Custom API request error, trying built-in sources...'\n  },\n  playBar: {\n    expand: 'Expand Lyrics',\n    collapse: 'Collapse Lyrics',\n    like: 'Like',\n    lyric: 'Lyric',\n    noSongPlaying: 'No song playing',\n    eq: 'Equalizer',\n    playList: 'Play List',\n    reparse: 'Reparse',\n    miniPlayBar: 'Mini Play Bar',\n    playMode: {\n      sequence: 'Sequence',\n      loop: 'Loop',\n      random: 'Random'\n    },\n    play: 'Play',\n    pause: 'Pause',\n    prev: 'Previous',\n    next: 'Next',\n    volume: 'Volume',\n    favorite: 'Favorite {name}',\n    unFavorite: 'Unfavorite {name}',\n    playbackSpeed: 'Playback Speed',\n    advancedControls: 'Advanced Controls',\n    intelligenceMode: {\n      title: 'Intelligence Mode',\n      needCookieLogin: 'Please login with Cookie method to use Intelligence Mode',\n      noFavoritePlaylist: 'Favorite playlist not found',\n      noLikedSongs: 'You have no liked songs yet',\n      loading: 'Loading Intelligence Mode',\n      success: 'Loaded {count} songs',\n      failed: 'Failed to get Intelligence Mode list',\n      error: 'Intelligence Mode error'\n    }\n  },\n  eq: {\n    title: 'Equalizer',\n    reset: 'Reset',\n    on: 'On',\n    off: 'Off',\n    bass: 'Bass',\n    midrange: 'Midrange',\n    treble: 'Treble',\n    presets: {\n      flat: 'Flat',\n      pop: 'Pop',\n      rock: 'Rock',\n      classical: 'Classical',\n      jazz: 'Jazz',\n      electronic: 'Electronic',\n      hiphop: 'Hip-Hop',\n      rb: 'R&B',\n      metal: 'Metal',\n      vocal: 'Vocal',\n      dance: 'Dance',\n      acoustic: 'Acoustic',\n      custom: 'Custom'\n    }\n  },\n  // Playback settings\n  settings: {\n    title: 'Playback Settings',\n    playbackSpeed: 'Playback Speed'\n  },\n  // Sleep timer related\n  sleepTimer: {\n    title: 'Sleep Timer',\n    cancel: 'Cancel Timer',\n    timeMode: 'By Time',\n    songsMode: 'By Songs',\n    playlistEnd: 'After Playlist',\n    afterPlaylist: 'After Playlist Ends',\n    activeUntilEnd: 'Active until end of playlist',\n    minutes: 'min',\n    hours: 'hr',\n    songs: 'songs',\n    set: 'Set',\n    timerSetSuccess: 'Timer set for {minutes} minutes',\n    songsSetSuccess: 'Timer set for {songs} songs',\n    playlistEndSetSuccess: 'Timer set to end after playlist',\n    timerCancelled: 'Sleep timer cancelled',\n    timerEnded: 'Sleep timer ended',\n    playbackStopped: 'Music playback stopped',\n    minutesRemaining: '{minutes} min remaining',\n    songsRemaining: '{count} songs remaining',\n    activeTime: 'Timer Active',\n    activeSongs: 'Counting Songs',\n    activeEnd: 'End After List'\n  },\n  playList: {\n    clearAll: 'Clear Playlist',\n    alreadyEmpty: 'Playlist is already empty',\n    cleared: 'Playlist cleared',\n    empty: 'Playlist is empty',\n    clearConfirmTitle: 'Clear Playlist',\n    clearConfirmContent:\n      'This will clear all songs in the playlist and stop the current playback. Continue?'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/search.ts",
    "content": "export default {\n  title: {\n    hotSearch: 'Hot Search',\n    searchList: 'Search Results',\n    searchHistory: 'Search History'\n  },\n  button: {\n    clear: 'Clear',\n    back: 'Back',\n    playAll: 'Play All'\n  },\n  loading: {\n    more: 'Loading...',\n    failed: 'Search failed',\n    searching: 'Searching...'\n  },\n  noMore: 'No more results',\n  error: {\n    searchFailed: 'Search failed'\n  },\n  search: {\n    single: 'Single',\n    album: 'Album',\n    playlist: 'Playlist',\n    mv: 'MV',\n    bilibili: 'Bilibili'\n  },\n  history: 'Search History',\n  hot: 'Hot Searches',\n  suggestions: 'Search Suggestions'\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/settings.ts",
    "content": "export default {\n  theme: 'Theme',\n  language: 'Language',\n  regard: 'About',\n  logout: 'Logout',\n  sections: {\n    basic: 'Basic Settings',\n    playback: 'Playback Settings',\n    application: 'Application Settings',\n    network: 'Network Settings',\n    system: 'System Management',\n    donation: 'Donation',\n    about: 'About'\n  },\n  basic: {\n    themeMode: 'Theme Mode',\n    themeModeDesc: 'Switch between light/dark theme',\n    autoTheme: 'Follow System',\n    manualTheme: 'Manual Switch',\n    language: 'Language Settings',\n    languageDesc: 'Change display language',\n    tokenManagement: 'Cookie Management',\n    tokenManagementDesc: 'Manage NetEase Cloud Music login Cookie',\n    tokenStatus: 'Current Cookie Status',\n    tokenSet: 'Set',\n    tokenNotSet: 'Not Set',\n    setToken: 'Set Cookie',\n    modifyToken: 'Modify Cookie',\n    clearToken: 'Clear Cookie',\n    font: 'Font Settings',\n    fontDesc: 'Select fonts, prioritize fonts in order',\n    fontScope: {\n      global: 'Global',\n      lyric: 'Lyrics Only'\n    },\n    animation: 'Animation Speed',\n    animationDesc: 'Enable/disable animations',\n    animationSpeed: {\n      slow: 'Very Slow',\n      normal: 'Normal',\n      fast: 'Very Fast'\n    },\n    fontPreview: {\n      title: 'Font Preview',\n      chinese: 'Chinese',\n      english: 'English',\n      japanese: 'Japanese',\n      korean: 'Korean',\n      chineseText: '静夜思 床前明月光 疑是地上霜',\n      englishText: 'The quick brown fox jumps over the lazy dog',\n      japaneseText: 'あいうえお かきくけこ さしすせそ',\n      koreanText: '가나다라마 바사아자차 카타파하'\n    },\n    gpuAcceleration: 'GPU Acceleration',\n    gpuAccelerationDesc:\n      'Enable or disable hardware acceleration, can improve rendering performance but may increase GPU load',\n    gpuAccelerationRestart:\n      'Changing GPU acceleration settings requires application restart to take effect',\n    gpuAccelerationChangeSuccess:\n      'GPU acceleration settings updated, restart application to take effect',\n    gpuAccelerationChangeError: 'Failed to update GPU acceleration settings',\n    tabletMode: 'Tablet Mode',\n    tabletModeDesc: 'Enabling tablet mode allows using PC-style interface on mobile devices'\n  },\n  playback: {\n    quality: 'Audio Quality',\n    qualityDesc: 'Select music playback quality (VIP)',\n    qualityOptions: {\n      standard: 'Standard',\n      higher: 'Higher',\n      exhigh: 'Extreme',\n      lossless: 'Lossless',\n      hires: 'Hi-Res',\n      jyeffect: 'HD Surround',\n      sky: 'Immersive',\n      dolby: 'Dolby Atmos',\n      jymaster: 'Master'\n    },\n    musicSources: 'Music Sources',\n    musicSourcesDesc: 'Select music sources for song resolution',\n    musicSourcesWarning: 'At least one music source must be selected',\n    musicUnblockEnable: 'Enable Music Unblocking',\n    musicUnblockEnableDesc: 'When enabled, attempts to resolve unplayable songs',\n    configureMusicSources: 'Configure Sources',\n    selectedMusicSources: 'Selected sources:',\n    noMusicSources: 'No sources selected',\n    gdmusicInfo:\n      'GD Music Station intelligently resolves music from multiple platforms automatically',\n    autoPlay: 'Auto Play',\n    autoPlayDesc: 'Auto resume playback when reopening the app',\n    showStatusBar: 'Show Status Bar',\n    showStatusBarContent:\n      'You can display the music control function in your mac status bar (effective after a restart)',\n    fallbackParser: 'Fallback Parser (GD Music)',\n    fallbackParserDesc:\n      'When \"GD Music\" is checked and regular sources fail, this service will be used.',\n    parserGD: 'GD Music (Built-in)',\n    parserCustom: 'Custom API',\n\n    // Source labels\n    sourceLabels: {\n      migu: 'Migu',\n      kugou: 'Kugou',\n      pyncmd: 'NetEase (Built-in)',\n      bilibili: 'Bilibili',\n      gdmusic: 'GD Music',\n      custom: 'Custom API'\n    },\n\n    customApi: {\n      sectionTitle: 'Custom API Settings',\n      importConfig: 'Import JSON Config',\n      currentSource: 'Current Source',\n      notImported: 'No custom source imported yet.',\n      importSuccess: 'Successfully imported source: {name}',\n      importFailed: 'Import failed: {message}',\n      enableHint: 'Import a JSON config file to enable',\n      status: {\n        imported: 'Custom Source Imported',\n        notImported: 'Not Imported'\n      }\n    },\n    lxMusic: {\n      tabs: {\n        sources: 'Source Selection',\n        lxMusic: 'LX Music',\n        customApi: 'Custom API'\n      },\n      scripts: {\n        title: 'Imported Scripts',\n        importLocal: 'Import Local',\n        importOnline: 'Import Online',\n        urlPlaceholder: 'Enter LX Music Script URL',\n        importBtn: 'Import',\n        empty: 'No imported LX Music scripts',\n        notConfigured: 'Not configured (Configure in LX Music Tab)',\n        importHint: 'Import compatible custom API plugins to extend sources',\n        noScriptWarning: 'Please import LX Music script first',\n        noSelectionWarning: 'Please select an LX Music source first',\n        notFound: 'Source not found',\n        switched: 'Switched to source: {name}',\n        deleted: 'Deleted source: {name}',\n        enterUrl: 'Please enter script URL',\n        invalidUrl: 'Invalid URL format',\n        invalidScript: 'Invalid LX Music script, globalThis.lx code not found',\n        nameRequired: 'Name cannot be empty',\n        renameSuccess: 'Rename successful'\n      }\n    }\n  },\n  application: {\n    closeAction: 'Close Action',\n    closeActionDesc: 'Choose action when closing window',\n    closeOptions: {\n      ask: 'Ask Every Time',\n      minimize: 'Minimize to Tray',\n      close: 'Exit Directly'\n    },\n    shortcut: 'Shortcut Settings',\n    shortcutDesc: 'Customize global shortcuts',\n    download: 'Download Management',\n    downloadDesc: 'Always show download list button',\n    unlimitedDownload: 'Unlimited Download',\n    unlimitedDownloadDesc: 'Enable unlimited download mode for music , default limit 300 songs',\n    downloadPath: 'Download Directory',\n    downloadPathDesc: 'Choose download location for music files',\n    remoteControl: 'Remote Control',\n    remoteControlDesc: 'Set remote control function'\n  },\n  network: {\n    apiPort: 'Music API Port',\n    apiPortDesc: 'Restart required after modification',\n    proxy: 'Proxy Settings',\n    proxyDesc: 'Enable proxy when unable to access music',\n    proxyHost: 'Proxy Host',\n    proxyHostPlaceholder: 'Enter proxy host',\n    proxyPort: 'Proxy Port',\n    proxyPortPlaceholder: 'Enter proxy port',\n    realIP: 'RealIP Settings',\n    realIPDesc: 'Use realIP parameter with mainland China IP to resolve access restrictions abroad',\n    messages: {\n      proxySuccess: 'Proxy settings saved, restart required to take effect',\n      proxyError: 'Please check your input',\n      realIPSuccess: 'RealIP settings saved',\n      realIPError: 'Please enter a valid IP address'\n    }\n  },\n  system: {\n    cache: 'Cache Management',\n    cacheDesc: 'Clear cache',\n    cacheClearTitle: 'Select cache types to clear:',\n    cacheTypes: {\n      history: {\n        label: 'Play History',\n        description: 'Clear played song records'\n      },\n      favorite: {\n        label: 'Favorites',\n        description: 'Clear local favorite songs (cloud favorites not affected)'\n      },\n      user: {\n        label: 'User Data',\n        description: 'Clear login info and user-related data'\n      },\n      settings: {\n        label: 'App Settings',\n        description: 'Clear all custom app settings'\n      },\n      downloads: {\n        label: 'Download History',\n        description: 'Clear download history (downloaded files not affected)'\n      },\n      resources: {\n        label: 'Music Resources',\n        description: 'Clear cached music files, lyrics and other resources'\n      },\n      lyrics: {\n        label: 'Lyrics Resources',\n        description: 'Clear cached lyrics resources'\n      }\n    },\n    restart: 'Restart',\n    restartDesc: 'Restart application',\n    messages: {\n      clearSuccess: 'Cache cleared successfully, some settings will take effect after restart'\n    }\n  },\n  about: {\n    version: 'Version',\n    checkUpdate: 'Check for Updates',\n    checking: 'Checking...',\n    latest: 'Already latest version',\n    hasUpdate: 'New version available',\n    gotoUpdate: 'Go to Update',\n    gotoGithub: 'Go to Github',\n    author: 'Author',\n    authorDesc: 'algerkong Give a star🌟',\n    messages: {\n      checkError: 'Failed to check for updates, please try again later'\n    }\n  },\n  validation: {\n    selectProxyProtocol: 'Please select proxy protocol',\n    proxyHost: 'Please enter proxy host',\n    portNumber: 'Please enter a valid port number (1-65535)'\n  },\n  lyricSettings: {\n    title: 'Lyric Settings',\n    tabs: {\n      display: 'Display',\n      interface: 'Interface',\n      typography: 'Typography',\n      background: 'Background',\n      mobile: 'Mobile'\n    },\n    pureMode: 'Pure Mode',\n    hideCover: 'Hide Cover',\n    centerDisplay: 'Center Display',\n    showTranslation: 'Show Translation',\n    hideLyrics: 'Hide Lyrics',\n    hidePlayBar: 'Hide Play Bar',\n    hideMiniPlayBar: 'Hide Mini Play Bar',\n    showMiniPlayBar: 'Show Mini Play Bar',\n    backgroundTheme: 'Background Theme',\n    themeOptions: {\n      default: 'Default',\n      light: 'Light',\n      dark: 'Dark'\n    },\n    fontSize: 'Font Size',\n    fontSizeMarks: {\n      small: 'Small',\n      medium: 'Medium',\n      large: 'Large'\n    },\n    fontWeight: 'Font Weight',\n    fontWeightMarks: {\n      thin: 'Thin',\n      normal: 'Normal',\n      bold: 'Bold'\n    },\n    letterSpacing: 'Letter Spacing',\n    letterSpacingMarks: {\n      compact: 'Compact',\n      default: 'Default',\n      loose: 'Loose'\n    },\n    lineHeight: 'Line Height',\n    lineHeightMarks: {\n      compact: 'Compact',\n      default: 'Default',\n      loose: 'Loose'\n    },\n    contentWidth: 'Content Width',\n    mobileLayout: 'Mobile Layout',\n    layoutOptions: {\n      default: 'Default',\n      ios: 'iOS Style',\n      android: 'Android Style'\n    },\n    mobileCoverStyle: 'Cover Style',\n    coverOptions: {\n      record: 'Record',\n      square: 'Square',\n      full: 'Full Screen'\n    },\n    lyricLines: 'Lyric Lines',\n    mobileUnavailable: 'This setting is only available on mobile devices',\n    // Background settings\n    background: {\n      useCustomBackground: 'Use Custom Background',\n      backgroundMode: 'Background Mode',\n      modeOptions: {\n        solid: 'Solid',\n        gradient: 'Gradient',\n        image: 'Image',\n        css: 'CSS'\n      },\n      solidColor: 'Select Color',\n      presetColors: 'Preset Colors',\n      customColor: 'Custom Color',\n      gradientEditor: 'Gradient Editor',\n      gradientColors: 'Gradient Colors',\n      gradientDirection: 'Gradient Direction',\n      directionOptions: {\n        toBottom: 'Top to Bottom',\n        toRight: 'Left to Right',\n        toBottomRight: 'Top Left to Bottom Right',\n        angle45: '45 Degrees',\n        toTop: 'Bottom to Top',\n        toLeft: 'Right to Left'\n      },\n      addColor: 'Add Color',\n      removeColor: 'Remove Color',\n      imageUpload: 'Upload Image',\n      imagePreview: 'Image Preview',\n      clearImage: 'Clear Image',\n      imageBlur: 'Blur',\n      imageBrightness: 'Brightness',\n      customCss: 'Custom CSS Style',\n      customCssPlaceholder: 'Enter CSS style, e.g.: background: linear-gradient(...)',\n      customCssHelp: 'Supports any CSS background property',\n      reset: 'Reset to Default',\n      fileSizeLimit: 'Image size limit: 20MB',\n      invalidImageFormat: 'Invalid image format',\n      imageTooLarge: 'Image too large, please select an image smaller than 20MB'\n    }\n  },\n  translationEngine: 'Lyric Translation Engine',\n  translationEngineOptions: {\n    none: 'Off',\n    opencc: 'OpenCC Traditionalize'\n  },\n  themeColor: {\n    title: 'Lyric Theme Color',\n    presetColors: 'Preset Colors',\n    customColor: 'Custom Color',\n    preview: 'Preview',\n    previewText: 'Lyric Effect',\n    colorNames: {\n      'spotify-green': 'Spotify Green',\n      'apple-blue': 'Apple Blue',\n      'youtube-red': 'YouTube Red',\n      orange: 'Vibrant Orange',\n      purple: 'Mystic Purple',\n      pink: 'Cherry Pink'\n    },\n    tooltips: {\n      openColorPicker: 'Open Color Picker',\n      closeColorPicker: 'Close Color Picker'\n    },\n    placeholder: '#1db954'\n  },\n  shortcutSettings: {\n    title: 'Shortcut Settings',\n    shortcut: 'Shortcut',\n    shortcutDesc: 'Customize global shortcuts',\n    shortcutConflict: 'Shortcut Conflict',\n    inputPlaceholder: 'Click to input shortcut',\n    resetShortcuts: 'Reset',\n    disableAll: 'Disable All',\n    enableAll: 'Enable All',\n    togglePlay: 'Play/Pause',\n    prevPlay: 'Previous',\n    nextPlay: 'Next',\n    volumeUp: 'Volume Up',\n    volumeDown: 'Volume Down',\n    toggleFavorite: 'Favorite/Unfavorite',\n    toggleWindow: 'Show/Hide Window',\n    scopeGlobal: 'Global',\n    scopeApp: 'App Only',\n    enabled: 'Enabled',\n    disabled: 'Disabled',\n    messages: {\n      resetSuccess: 'Shortcuts reset successfully, please save',\n      conflict: 'Shortcut conflict, please reset',\n      saveSuccess: 'Shortcuts saved successfully',\n      saveError: 'Failed to save shortcuts',\n      cancelEdit: 'Edit cancelled',\n      disableAll: 'All shortcuts disabled, please save to apply',\n      enableAll: 'All shortcuts enabled, please save to apply'\n    }\n  },\n  remoteControl: {\n    title: 'Remote Control',\n    enable: 'Enable Remote Control',\n    port: 'Port',\n    allowedIps: 'Allowed IPs',\n    addIp: 'Add IP',\n    emptyListHint: 'Empty list means allow all IPs',\n    saveSuccess: 'Remote control settings saved',\n    accessInfo: 'Remote control access address:'\n  },\n  cookie: {\n    title: 'Cookie Settings',\n    description: 'Please enter NetEase Cloud Music Cookie:',\n    placeholder: 'Please paste the complete Cookie...',\n    help: {\n      format: 'Cookie usually starts with \"MUSIC_U=\"',\n      source: 'Can be obtained from browser developer tools network requests',\n      storage: 'Cookie will be automatically saved to local storage after setting'\n    },\n    action: {\n      save: 'Save Cookie',\n      paste: 'Paste',\n      clear: 'Clear'\n    },\n    validation: {\n      required: 'Please enter Cookie',\n      format: 'Cookie format may be incorrect, please check if it contains MUSIC_U'\n    },\n    message: {\n      saveSuccess: 'Cookie saved successfully',\n      saveError: 'Failed to save Cookie',\n      pasteSuccess: 'Pasted successfully',\n      pasteError: 'Paste failed, please copy manually'\n    },\n    info: {\n      length: 'Current length: {length} characters'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/songItem.ts",
    "content": "export default {\n  menu: {\n    play: 'Play',\n    playNext: 'Play Next',\n    download: 'Download',\n    addToPlaylist: 'Add to Playlist',\n    favorite: 'Like',\n    unfavorite: 'Unlike',\n    removeFromPlaylist: 'Remove from Playlist',\n    dislike: 'Dislike',\n    undislike: 'Undislike'\n  },\n  message: {\n    downloading: 'Downloading, please wait...',\n    downloadFailed: 'Download failed',\n    downloadQueued: 'Added to download queue',\n    addedToNextPlay: 'Added to play next',\n    getUrlFailed: 'Failed to get music download URL, please check if logged in'\n  },\n  dialog: {\n    dislike: {\n      title: 'Dislike',\n      content: 'Are you sure you want to dislike this song?',\n      positiveText: 'Dislike',\n      negativeText: 'Cancel'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/en-US/user.ts",
    "content": "export default {\n  profile: {\n    followers: 'Followers',\n    following: 'Following',\n    level: 'Level'\n  },\n  playlist: {\n    created: 'Created Playlists',\n    mine: 'Mine',\n    trackCount: '{count} tracks',\n    playCount: 'Played {count} times'\n  },\n  tabs: {\n    created: 'Created',\n    favorite: 'Favorite',\n    album: 'Album'\n  },\n  ranking: {\n    title: 'Listening History',\n    playCount: '{count} times'\n  },\n  follow: {\n    title: 'Follow List',\n    viewPlaylist: 'View Playlist',\n    noFollowings: 'No Followings',\n    loadMore: 'Load More',\n    noSignature: 'This guy is lazy, nothing left',\n    userFollowsTitle: \"'s Followings\",\n    myFollowsTitle: 'My Followings'\n  },\n  follower: {\n    title: 'Follower List',\n    noFollowers: 'No Followers',\n    loadMore: 'Load More',\n    userFollowersTitle: \"'s Followers\",\n    myFollowersTitle: 'My Followers'\n  },\n  detail: {\n    playlists: 'Playlists',\n    records: 'Listening History',\n    noPlaylists: 'No Playlists',\n    noRecords: 'No Listening History',\n    artist: 'Artist',\n    noSignature: 'This guy is lazy, nothing left',\n    invalidUserId: 'Invalid User ID',\n    noRecordPermission: \"{name} doesn't let you see your listening history\"\n  },\n  message: {\n    loadFailed: 'Failed to load user page',\n    deleteSuccess: 'Successfully deleted',\n    deleteFailed: 'Failed to delete'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/artist.ts",
    "content": "export default {\r\n  hotSongs: '人気楽曲',\r\n  albums: 'アルバム',\r\n  description: 'アーティスト紹介'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/bilibili.ts",
    "content": "export default {\n  player: {\n    loading: 'オーディオ読み込み中...',\n    retry: '再試行',\n    playNow: '今すぐ再生',\n    loadingTitle: '読み込み中...',\n    totalDuration: '総再生時間: {duration}',\n    partsList: 'パートリスト ({count}話)',\n    playStarted: '再生を開始しました',\n    switchingPart: 'パートを切り替え中: {part}',\n    preloadingNext: '次のパートをプリロード中: {part}',\n    playingCurrent: '現在選択されたパートを再生中: {name}',\n    num: '万',\n    errors: {\n      invalidVideoId: '無効な動画ID',\n      loadVideoDetailFailed: '動画詳細の取得に失敗しました',\n      loadPartInfoFailed: '動画パート情報の読み込みができません',\n      loadAudioUrlFailed: 'オーディオ再生URLの取得に失敗しました',\n      videoDetailNotLoaded: '動画詳細が読み込まれていません',\n      missingParams: '必要なパラメータが不足しています',\n      noAvailableAudioUrl: '利用可能なオーディオURLが見つかりません',\n      loadPartAudioFailed: 'パートオーディオURLの読み込みに失敗しました',\n      audioListEmpty: 'オーディオリストが空です。再試行してください',\n      currentPartNotFound: '現在のパートのオーディオが見つかりません',\n      audioUrlFailed: 'オーディオURLの取得に失敗しました',\n      playFailed: '再生に失敗しました。再試行してください',\n      getAudioUrlFailed: 'オーディオURLの取得に失敗しました。再試行してください',\n      audioNotFound: '対応するオーディオが見つかりません。再試行してください',\n      preloadFailed: '次のパートのプリロードに失敗しました',\n      switchPartFailed: 'パート切り替え時のオーディオURL読み込みに失敗しました'\n    },\n    console: {\n      loadingDetail: 'Bilibiliビデオ詳細を読み込み中',\n      detailData: 'Bilibiliビデオ詳細データ',\n      multipleParts: 'ビデオに複数のパートがあります。合計{count}個',\n      noPartsData: 'ビデオにパートがないか、パートデータが空です',\n      loadingAudioSource: 'オーディオソースを読み込み中',\n      generatedAudioList: 'オーディオリストを生成しました。合計{count}個',\n      getDashAudioUrl: 'dashオーディオURLを取得しました',\n      getDurlAudioUrl: 'durlオーディオURLを取得しました',\n      loadingPartAudio: 'パートオーディオURLを読み込み中: {part}, cid: {cid}',\n      loadPartAudioFailed: 'パートオーディオURLの読み込みに失敗: {part}',\n      switchToPart: 'パートに切り替え中: {part}',\n      audioNotFoundInList: '対応するオーディオアイテムが見つかりません',\n      preparingToPlay: '現在選択されたパートの再生準備中: {name}',\n      preloadingNextPart: '次のパートをプリロード中: {part}',\n      playingSelectedPart: '現在選択されたパートを再生中: {name}、オーディオURL: {url}',\n      preloadNextFailed: '次のパートのプリロードに失敗しました'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/common.ts",
    "content": "export default {\r\n  play: '再生',\r\n  next: '次の曲',\r\n  previous: '前の曲',\r\n  volume: '音量',\r\n  settings: '設定',\r\n  search: '検索',\r\n  loading: '読み込み中...',\r\n  loadingMore: 'さらに読み込み中...',\r\n  alipay: 'Alipay',\r\n  wechat: 'WeChat Pay',\r\n  on: 'オン',\r\n  off: 'オフ',\r\n  show: '表示',\r\n  hide: '非表示',\r\n  confirm: '確認',\r\n  cancel: 'キャンセル',\r\n  configure: '設定',\r\n  open: '開く',\r\n  modify: '変更',\r\n  success: '操作成功',\r\n  error: '操作失敗',\r\n  warning: '警告',\r\n  info: 'お知らせ',\r\n  save: '保存',\r\n  delete: '削除',\r\n  refresh: '更新',\r\n  retry: '再試行',\r\n  reset: 'リセット',\r\n  back: '戻る',\r\n  copySuccess: 'クリップボードにコピーしました',\r\n  copyFailed: 'コピーに失敗しました',\r\n  validation: {\r\n    required: 'この項目は必須です',\r\n    invalidInput: '無効な入力です',\r\n    selectRequired: 'オプションを選択してください',\r\n    numberRange: '{min}から{max}の間の数値を入力してください'\r\n  },\r\n  viewMore: 'もっと見る',\r\n  noMore: 'これ以上ありません',\r\n  selectAll: '全選択',\r\n  expand: '展開',\r\n  collapse: '折りたたみ',\r\n  songCount: '{count}曲',\r\n  language: '言語',\r\n  today: '今日',\r\n  yesterday: '昨日',\r\n  tray: {\r\n    show: '表示',\r\n    quit: '終了',\r\n    playPause: '再生/一時停止',\r\n    prev: '前の曲',\r\n    next: '次の曲',\r\n    pause: '一時停止',\r\n    play: '再生',\r\n    favorite: 'お気に入り'\r\n  }\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/comp.ts",
    "content": "export default {\r\n  installApp: {\r\n    description: 'アプリをインストールして、より良い体験を',\r\n    noPrompt: '今後表示しない',\r\n    install: '今すぐインストール',\r\n    cancel: '後でインストール',\r\n    download: 'ダウンロード',\r\n    downloadFailed: 'ダウンロード失敗',\r\n    downloadComplete: 'ダウンロード完了',\r\n    downloadProblem: 'ダウンロードに問題がありますか？',\r\n    downloadProblemLinkText: '最新版をダウンロード'\r\n  },\r\n  playlistDrawer: {\r\n    title: 'プレイリストに追加',\r\n    createPlaylist: '新しいプレイリストを作成',\r\n    cancelCreate: '作成をキャンセル',\r\n    create: '作成',\r\n    playlistName: 'プレイリスト名',\r\n    privatePlaylist: 'プライベートプレイリスト',\r\n    publicPlaylist: 'パブリックプレイリスト',\r\n    createSuccess: 'プレイリストの作成に成功しました',\r\n    createFailed: 'プレイリストの作成に失敗しました',\r\n    addSuccess: '楽曲の追加に成功しました',\r\n    addFailed: '楽曲の追加に失敗しました',\r\n    private: 'プライベート',\r\n    public: 'パブリック',\r\n    count: '曲',\r\n    loginFirst: 'まずログインしてください',\r\n    getPlaylistFailed: 'プレイリストの取得に失敗しました',\r\n    inputPlaylistName: 'プレイリスト名を入力してください'\r\n  },\r\n  update: {\r\n    title: '新しいバージョンが見つかりました',\r\n    currentVersion: '現在のバージョン',\r\n    cancel: '後で更新',\r\n    prepareDownload: 'ダウンロード準備中...',\r\n    downloading: 'ダウンロード中...',\r\n    nowUpdate: '今すぐ更新',\r\n    downloadFailed: 'ダウンロードに失敗しました。再試行するか手動でダウンロードしてください',\r\n    startFailed: 'ダウンロードの開始に失敗しました。再試行するか手動でダウンロードしてください',\r\n    noDownloadUrl:\r\n      '現在のシステムに適したインストールパッケージが見つかりません。手動でダウンロードしてください',\r\n    installConfirmTitle: '更新をインストール',\r\n    installConfirmContent: 'アプリを閉じて更新をインストールしますか？',\r\n    manualInstallTip:\r\n      'アプリを閉じた後にインストーラーが正常に起動しない場合は、ダウンロードフォルダでファイルを見つけて手動で開いてください。',\r\n    yesInstall: '今すぐインストール',\r\n    noThanks: '後でインストール',\r\n    fileLocation: 'ファイルの場所',\r\n    copy: 'パスをコピー',\r\n    copySuccess: 'パスをクリップボードにコピーしました',\r\n    copyFailed: 'コピーに失敗しました',\r\n    backgroundDownload: 'バックグラウンドダウンロード'\r\n  },\r\n  disclaimer: {\r\n    title: '使用上の注意',\r\n    warning:\r\n      'このアプリは開発テスト版であり、機能が不完全で、多くの問題やバグが存在する可能性があります。学習と交流のみを目的としています。',\r\n    item1:\r\n      'このアプリは個人の学習、研究、技術交流のみを目的としています。商業目的で使用しないでください。',\r\n    item2:\r\n      'ダウンロード後24時間以内に削除してください。長期使用を希望される場合は、正規の音楽サービスをサポートしてください。',\r\n    item3:\r\n      'このアプリを使用することで、関連するリスクを理解し、負担するものとします。開発者は一切の損失に対して責任を負いません。',\r\n    agree: '以上の内容を読み、同意します',\r\n    disagree: '同意せずに終了'\r\n  },\r\n  donate: {\r\n    title: '開発者を支援',\r\n    subtitle: '皆様のサポートが私の原動力です',\r\n    tip: '寄付は完全に任意です。寄付しなくてもすべての機能を通常通り使用できます。ご理解とご支援に感謝します！',\r\n    wechat: 'WeChat',\r\n    alipay: 'Alipay',\r\n    wechatQR: 'WeChat 受取コード',\r\n    alipayQR: 'Alipay 受取コード',\r\n    scanTip: 'スマートフォンのアプリで上記のQRコードをスキャンして寄付してください',\r\n    enterApp: 'アプリに入る',\r\n    noForce: '寄付は強制ではありません。クリックして入れます'\r\n  },\r\n  coffee: {\r\n    title: 'コーヒーをおごる',\r\n    alipay: 'Alipay',\r\n    wechat: 'WeChat Pay',\r\n    alipayQR: 'Alipay QRコード',\r\n    wechatQR: 'WeChat QRコード',\r\n    coffeeDesc: '一杯のコーヒー、一つのサポート',\r\n    coffeeDescLinkText: 'もっと見る',\r\n    groupText: '微信公众号：AlgerMusic',\r\n    messages: {\r\n      copySuccess: 'クリップボードにコピーしました'\r\n    },\r\n    donateList: 'コーヒーをおごる'\r\n  },\r\n  playlistType: {\r\n    title: 'プレイリストカテゴリ',\r\n    showAll: 'すべて表示',\r\n    hide: '一部を非表示'\r\n  },\r\n  recommendAlbum: {\r\n    title: '最新アルバム'\r\n  },\r\n  recommendSinger: {\r\n    title: '毎日のおすすめ',\r\n    songlist: '毎日のおすすめリスト'\r\n  },\r\n  recommendSonglist: {\r\n    title: '今週の人気音楽'\r\n  },\r\n  searchBar: {\r\n    login: 'ログイン',\r\n    toLogin: 'ログインへ',\r\n    logout: 'ログアウト',\r\n    set: '設定',\r\n    theme: 'テーマ',\r\n    restart: '再起動',\r\n    refresh: '更新',\r\n    currentVersion: '現在のバージョン',\r\n    searchPlaceholder: '何かを検索してみましょう...',\r\n    zoom: 'ページズーム',\r\n    zoom100: '標準ズーム100%',\r\n    resetZoom: 'クリックしてズームをリセット',\r\n    zoomDefault: '標準ズーム'\r\n  },\r\n  titleBar: {\r\n    closeTitle: '閉じる方法を選択してください',\r\n    minimizeToTray: 'トレイに最小化',\r\n    exitApp: 'アプリを終了',\r\n    rememberChoice: '選択を記憶する',\r\n    closeApp: 'アプリを閉じる'\r\n  },\r\n  userPlayList: {\r\n    title: '{name}のよく聞く音楽'\r\n  },\r\n  musicList: {\r\n    searchSongs: '楽曲を検索',\r\n    noSearchResults: '関連する楽曲が見つかりませんでした',\r\n    switchToNormal: 'デフォルトレイアウトに切り替え',\r\n    switchToCompact: 'コンパクトレイアウトに切り替え',\r\n    playAll: 'すべて再生',\r\n    collect: 'お気に入り',\r\n    collectSuccess: 'お気に入りに追加しました',\r\n    cancelCollectSuccess: 'お気に入りから削除しました',\r\n    operationFailed: '操作に失敗しました',\r\n    cancelCollect: 'お気に入りから削除',\r\n    addToPlaylist: 'プレイリストに追加',\r\n    addToPlaylistSuccess: 'プレイリストに追加しました',\r\n    songsAlreadyInPlaylist: '楽曲は既にプレイリストに存在します',\r\n    historyRecommend: '履歴の日次推薦',\r\n    fetchDatesFailed: '日付リストの取得に失敗しました',\r\n    fetchSongsFailed: '楽曲リストの取得に失敗しました',\r\n    noSongs: '楽曲がありません'\r\n  },\r\n  playlist: {\r\n    import: {\r\n      button: 'プレイリストインポート',\r\n      title: 'プレイリストインポート',\r\n      description: 'メタデータ/テキスト/リンクの3つの方法でプレイリストをインポートできます',\r\n      linkTab: 'リンクインポート',\r\n      textTab: 'テキストインポート',\r\n      localTab: 'メタデータインポート',\r\n      linkPlaceholder: 'プレイリストのリンクを入力してください（1行に1つ）',\r\n      textPlaceholder: '楽曲情報を入力してください。形式：楽曲名 アーティスト名',\r\n      localPlaceholder: 'JSON形式の楽曲メタデータを入力してください',\r\n      linkTips: 'サポートされているリンクソース：',\r\n      linkTip1: 'プレイリストをWeChat/Weibo/QQでシェアした後、リンクをコピー',\r\n      linkTip2: 'プレイリスト/個人ページのリンクを直接コピー',\r\n      linkTip3: '記事のリンクを直接コピー',\r\n      textTips: '楽曲情報を入力してください（1行に1曲）',\r\n      textFormat: '形式：楽曲名 アーティスト名',\r\n      localTips: '楽曲メタデータを追加してください',\r\n      localFormat: '形式例：',\r\n      songNamePlaceholder: '楽曲名',\r\n      artistNamePlaceholder: 'アーティスト名',\r\n      albumNamePlaceholder: 'アルバム名',\r\n      addSongButton: '楽曲を追加',\r\n      addLinkButton: 'リンクを追加',\r\n      importToStarPlaylist: 'お気に入りの音楽にインポート',\r\n      playlistNamePlaceholder: 'プレイリスト名を入力してください',\r\n      importButton: 'インポート開始',\r\n      emptyLinkWarning: 'プレイリストのリンクを入力してください',\r\n      emptyTextWarning: '楽曲情報を入力してください',\r\n      emptyLocalWarning: '楽曲メタデータを入力してください',\r\n      invalidJsonFormat: 'JSON形式が正しくありません',\r\n      importSuccess: 'インポートタスクの作成に成功しました',\r\n      importFailed: 'インポートに失敗しました',\r\n      importStatus: 'インポート状況',\r\n      refresh: '更新',\r\n      taskId: 'タスクID',\r\n      status: 'ステータス',\r\n      successCount: '成功数',\r\n      failReason: '失敗理由',\r\n      unknownError: '不明なエラー',\r\n      statusPending: '処理待ち',\r\n      statusProcessing: '処理中',\r\n      statusSuccess: 'インポート成功',\r\n      statusFailed: 'インポート失敗',\r\n      statusUnknown: '不明なステータス',\r\n      taskList: 'タスクリスト',\r\n      taskListTitle: 'インポートタスクリスト',\r\n      action: '操作',\r\n      select: '選択',\r\n      fetchTaskListFailed: 'タスクリストの取得に失敗しました',\r\n      noTasks: 'インポートタスクがありません',\r\n      clearTasks: 'タスクをクリア',\r\n      clearTasksConfirmTitle: 'クリア確認',\r\n      clearTasksConfirmContent:\r\n        'すべてのインポートタスク記録をクリアしますか？この操作は元に戻せません。',\r\n      confirm: '確認',\r\n      cancel: 'キャンセル',\r\n      clearTasksSuccess: 'タスクリストをクリアしました',\r\n      clearTasksFailed: 'タスクリストのクリアに失敗しました'\r\n    }\r\n  },\r\n  settings: '設定',\r\n  user: 'ユーザー',\r\n  toplist: 'ランキング',\r\n  history: 'お気に入り履歴',\r\n  list: 'プレイリスト',\r\n  mv: 'MV',\r\n  home: 'ホーム',\r\n  search: '検索'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/donation.ts",
    "content": "export default {\r\n  description:\r\n    'あなたの寄付は開発・保守作業をサポートするために使用され、サーバー保守、ドメイン更新などが含まれます。',\r\n  message: 'メッセージを残す際は、メールアドレスやGitHubユーザー名を記載してください。',\r\n  refresh: 'リストを更新',\r\n  toDonateList: 'コーヒーをおごる',\r\n  noMessage: 'メッセージがありません',\r\n  title: '寄付リスト'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/download.ts",
    "content": "export default {\r\n  title: 'ダウンロード管理',\r\n  localMusic: 'ローカル音楽',\r\n  count: '合計{count}曲',\r\n  clearAll: '記録をクリア',\r\n  settings: '設定',\r\n  tabs: {\r\n    downloading: 'ダウンロード中',\r\n    downloaded: 'ダウンロード済み'\r\n  },\r\n  empty: {\r\n    noTasks: 'ダウンロードタスクがありません',\r\n    noDownloaded: 'ダウンロード済みの楽曲がありません'\r\n  },\r\n  progress: {\r\n    total: '全体の進行状況: {progress}%'\r\n  },\r\n  status: {\r\n    downloading: 'ダウンロード中',\r\n    completed: '完了',\r\n    failed: '失敗',\r\n    unknown: '不明'\r\n  },\r\n  artist: {\r\n    unknown: '不明なアーティスト'\r\n  },\r\n  delete: {\r\n    title: '削除確認',\r\n    message: '楽曲「{filename}」を削除しますか？この操作は元に戻せません。',\r\n    confirm: '削除確認',\r\n    cancel: 'キャンセル',\r\n    success: '削除成功',\r\n    failed: '削除失敗',\r\n    fileNotFound: 'ファイルが存在しないか移動されました。記録から削除しました',\r\n    recordRemoved: 'ファイルの削除に失敗しましたが、記録から削除しました'\r\n  },\r\n  clear: {\r\n    title: 'ダウンロード記録をクリア',\r\n    message:\r\n      'すべてのダウンロード記録をクリアしますか？この操作はダウンロード済みの音楽ファイルを削除しませんが、すべての記録をクリアします。',\r\n    confirm: 'クリア確認',\r\n    cancel: 'キャンセル',\r\n    success: 'ダウンロード記録をクリアしました'\r\n  },\r\n  message: {\r\n    downloadComplete: '{filename}のダウンロードが完了しました',\r\n    downloadFailed: '{filename}のダウンロードに失敗しました: {error}'\r\n  },\r\n  loading: '読み込み中...',\r\n  playStarted: '再生開始: {name}',\r\n  playFailed: '再生失敗: {name}',\r\n  path: {\r\n    copied: 'パスをクリップボードにコピーしました',\r\n    copyFailed: 'パスのコピーに失敗しました'\r\n  },\r\n  settingsPanel: {\r\n    title: 'ダウンロード設定',\r\n    path: 'ダウンロード場所',\r\n    pathDesc: '音楽ファイルのダウンロード保存場所を設定',\r\n    pathPlaceholder: 'ダウンロードパスを選択してください',\r\n    noPathSelected: 'まずダウンロードパスを選択してください',\r\n    select: 'フォルダを選択',\r\n    open: 'フォルダを開く',\r\n    fileFormat: 'ファイル名形式',\r\n    fileFormatDesc: '音楽ダウンロード時のファイル命名形式を設定',\r\n    customFormat: 'カスタム形式',\r\n    separator: '区切り文字',\r\n    separators: {\r\n      dash: 'スペース-スペース',\r\n      underscore: 'アンダースコア',\r\n      space: 'スペース'\r\n    },\r\n    dragToArrange: 'ドラッグで並び替えまたは矢印ボタンで順序を調整：',\r\n    formatVariables: '使用可能な変数',\r\n    preview: 'プレビュー効果：',\r\n    saveSuccess: 'ダウンロード設定を保存しました',\r\n    presets: {\r\n      songArtist: '楽曲名 - アーティスト名',\r\n      artistSong: 'アーティスト名 - 楽曲名',\r\n      songOnly: '楽曲名のみ'\r\n    },\r\n    components: {\r\n      songName: '楽曲名',\r\n      artistName: 'アーティスト名',\r\n      albumName: 'アルバム名'\r\n    }\r\n  }\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/favorite.ts",
    "content": "export default {\r\n  title: 'お気に入り',\r\n  count: '合計{count}曲',\r\n  batchDownload: '一括ダウンロード',\r\n  download: 'ダウンロード ({count})',\r\n  emptyTip: 'まだお気に入りの楽曲がありません',\r\n  downloadSuccess: 'ダウンロード完了',\r\n  downloadFailed: 'ダウンロード失敗',\r\n  downloading: 'ダウンロード中です。しばらくお待ちください...',\r\n  selectSongsFirst: 'まずダウンロードする楽曲を選択してください',\r\n  descending: '降順',\r\n  ascending: '昇順'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/history.ts",
    "content": "export default {\r\n  title: '再生履歴',\r\n  heatmapTitle: 'ヒートマップ',\r\n  playCount: '{count}',\r\n  getHistoryFailed: '履歴の取得に失敗しました',\r\n  tabs: {\r\n    all: 'すべての記録',\r\n    local: 'ローカル記録',\r\n    cloud: 'クラウド記録'\r\n  },\r\n  categoryTabs: {\r\n    songs: '楽曲',\r\n    playlists: 'プレイリスト',\r\n    albums: 'アルバム'\r\n  },\r\n  noDescription: '説明なし',\r\n  noData: '記録なし',\r\n  getCloudRecordFailed: 'クラウド記録の取得に失敗しました',\r\n  needLogin: 'cookieを使用してログインしてクラウド記録を表示できます',\r\n  merging: '記録を統合中...',\r\n  heatmap: {\r\n    title: '再生ヒートマップ',\r\n    loading: 'データを読み込み中...',\r\n    unit: '回再生',\r\n    footerText: 'ホバーして詳細を表示',\r\n    playCount: '{count} 回再生',\r\n    topSongs: 'その日の人気曲',\r\n    times: '回',\r\n    totalPlays: '総再生回数',\r\n    activeDays: 'アクティブ日数',\r\n    noData: '再生記録がありません',\r\n    colorTheme: 'カラーテーマ',\r\n    colors: {\r\n      green: 'グリーン',\r\n      blue: 'ブルー',\r\n      orange: 'オレンジ',\r\n      purple: 'パープル',\r\n      red: 'レッド'\r\n    },\r\n    mostPlayedSong: '最も再生された曲',\r\n    mostActiveDay: '最もアクティブな日',\r\n    latestNightSong: '深夜に再生した曲'\r\n  }\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/login.ts",
    "content": "export default {\r\n  title: {\r\n    qr: 'QRコードログイン',\r\n    phone: '電話番号ログイン',\r\n    cookie: 'Cookieログイン',\r\n    uid: 'UIDログイン'\r\n  },\r\n  qrTip: 'NetEase Cloudアプリでログイン',\r\n  phoneTip: 'NetEase Cloudアカウントでログイン',\r\n  tokenTip: '有効なNetEase Cloud MusicのCookieを入力してログイン',\r\n  uidTip: 'ユーザーIDを入力してクイックログイン',\r\n  placeholder: {\r\n    phone: '電話番号',\r\n    password: 'パスワード',\r\n    cookie: 'NetEase Cloud MusicのCookie（token）を入力してください',\r\n    uid: 'ユーザーID（UID）を入力してください'\r\n  },\r\n  button: {\r\n    login: 'ログイン',\r\n    switchToQr: 'QRコードログイン',\r\n    switchToPhone: '電話番号ログイン',\r\n    switchToToken: 'Cookieログインを使用',\r\n    switchToUid: 'UIDログイン',\r\n    backToQr: 'QRコードログインに戻る',\r\n    cookieLogin: 'Cookieログイン',\r\n    autoGetCookie: 'Cookie自動取得',\r\n    refresh: 'クリックしてリフレッシュ',\r\n    refreshing: 'リフレッシュ中...',\r\n    refreshQr: 'QRコードをリフレッシュ'\r\n  },\r\n  message: {\r\n    loginSuccess: 'ログイン成功',\r\n    tokenLoginSuccess: 'Cookieログイン成功',\r\n    uidLoginSuccess: 'UIDログイン成功',\r\n    loadError: 'ログイン情報の読み込み中にエラーが発生しました',\r\n    qrCheckError: 'QRコードの状態確認中にエラーが発生しました',\r\n    tokenRequired: 'Cookieを入力してください',\r\n    tokenInvalid: 'Cookieが無効です。確認して再試行してください',\r\n    uidRequired: 'ユーザーIDを入力してください',\r\n    uidInvalid: 'ユーザーIDが無効またはユーザーが存在しません',\r\n    uidLoginFailed: 'UIDログインに失敗しました。ユーザーIDが正しいか確認してください',\r\n    autoGetCookieSuccess: 'Cookie自動取得成功',\r\n    autoGetCookieFailed: 'Cookie自動取得失敗',\r\n    autoGetCookieTip:\r\n      'NetEase Cloud Musicのログインページを開きます。ログイン完了後、ウィンドウを閉じてください',\r\n    loginFailed: 'ログイン失敗',\r\n    phoneRequired: '電話番号を入力してください',\r\n    passwordRequired: 'パスワードを入力してください',\r\n    phoneLoginFailed:\r\n      '電話番号でのログインに失敗しました。電話番号とパスワードが正しいか確認してください',\r\n    qrCheckFailed: 'QRコードの状態確認に失敗しました。リフレッシュして再試行してください',\r\n    qrLoading: 'QRコードを読み込み中...',\r\n    qrExpired: 'QRコードの期限が切れました。クリックしてリフレッシュしてください',\r\n    qrExpiredShort: 'QRコード期限切れ',\r\n    qrExpiredWarning: 'QRコードの期限が切れました。クリックして新しいQRコードを取得してください',\r\n    qrScanned: 'QRコードがスキャンされました。スマートフォンでログインを確認してください',\r\n    qrScannedShort: 'スキャン済み',\r\n    qrScannedInfo: 'QRコードがスキャンされました。スマートフォンでログインを確認してください',\r\n    qrConfirmed: 'ログイン成功、リダイレクト中...',\r\n    qrGenerating: 'QRコードを生成中...'\r\n  },\r\n  qrTitle: 'NetEase Cloud Music QRコードログイン',\r\n  uidWarning:\r\n    '注意：UIDログインはユーザーの公開情報を表示するためのみ使用でき、ログイン権限が必要な機能にはアクセスできません。'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/player.ts",
    "content": "export default {\n  nowPlaying: '再生中',\n  playlist: 'プレイリスト',\n  lyrics: '歌詞',\n  previous: '前へ',\n  play: '再生',\n  pause: '一時停止',\n  next: '次へ',\n  volumeUp: '音量を上げる',\n  volumeDown: '音量を下げる',\n  mute: 'ミュート',\n  unmute: 'ミュート解除',\n  songNum: '楽曲総数：{num}',\n  addCorrection: '{num}秒早める',\n  subtractCorrection: '{num}秒遅らせる',\n  playFailed: '現在の楽曲の再生に失敗しました。次の曲を再生します',\n  parseFailedPlayNext: '楽曲の解析に失敗しました。次の曲を再生します',\n  consecutiveFailsError:\n    '再生エラーが発生しました。ネットワークの問題または無効な音源の可能性があります。プレイリストを切り替えるか、後でもう一度お試しください',\n  playMode: {\n    sequence: '順次再生',\n    loop: 'リピート再生',\n    random: 'ランダム再生'\n  },\n  fullscreen: {\n    enter: 'フルスクリーン',\n    exit: 'フルスクリーン終了'\n  },\n  close: '閉じる',\n  modeHint: {\n    single: 'リピート再生',\n    list: '自動で次の曲を再生'\n  },\n  lrc: {\n    noLrc: '歌詞がありません。お楽しみください',\n    noAutoScroll: '本歌詞は自動スクロールをサポートしていません'\n  },\n  reparse: {\n    title: '解析音源を選択',\n    desc: '音源をクリックして直接解析します。次回この楽曲を再生する際は選択した音源を使用します',\n    success: '再解析成功',\n    failed: '再解析失敗',\n    warning: '音源を選択してください',\n    bilibiliNotSupported: 'Bilibili動画は再解析をサポートしていません',\n    processing: '解析中...',\n    clear: 'カスタム音源をクリア',\n    customApiFailed: 'カスタムAPIの解析に失敗しました。内蔵音源を試しています...',\n    customApiError: 'カスタムAPIのリクエストでエラーが発生しました。内蔵音源を試しています...'\n  },\n  playBar: {\n    expand: '歌詞を展開',\n    collapse: '歌詞を折りたたみ',\n    like: 'いいね',\n    lyric: '歌詞',\n    noSongPlaying: '再生中の楽曲がありません',\n    eq: 'イコライザー',\n    playList: 'プレイリスト',\n    reparse: '再解析',\n    playMode: {\n      sequence: '順次再生',\n      loop: 'ループ再生',\n      random: 'ランダム再生'\n    },\n    play: '再生開始',\n    pause: '再生一時停止',\n    prev: '前の曲',\n    next: '次の曲',\n    volume: '音量',\n    favorite: '{name}をお気に入りに追加しました',\n    unFavorite: '{name}をお気に入りから削除しました',\n    miniPlayBar: 'ミニ再生バー',\n    playbackSpeed: '再生速度',\n    advancedControls: 'その他の設定',\n    intelligenceMode: {\n      title: 'インテリジェンスモード',\n      needCookieLogin: 'Cookie方式でログインしてからインテリジェンスモードを使用してください',\n      noFavoritePlaylist: '「お気に入りの音楽」プレイリストが見つかりません',\n      noLikedSongs: 'まだ「いいね」した楽曲がありません',\n      loading: 'インテリジェンスモードを読み込み中',\n      success: '{count} 曲を読み込みました',\n      failed: 'インテリジェンスモードのリスト取得に失敗しました',\n      error: 'インテリジェンスモードの再生でエラーが発生しました'\n    }\n  },\n  eq: {\n    title: 'イコライザー',\n    reset: 'リセット',\n    on: 'オン',\n    off: 'オフ',\n    bass: '低音',\n    midrange: '中音',\n    treble: '高音',\n    presets: {\n      flat: 'フラット',\n      pop: 'ポップ',\n      rock: 'ロック',\n      classical: 'クラシック',\n      jazz: 'ジャズ',\n      electronic: 'エレクトロニック',\n      hiphop: 'ヒップホップ',\n      rb: 'R&B',\n      metal: 'メタル',\n      vocal: 'ボーカル',\n      dance: 'ダンス',\n      acoustic: 'アコースティック',\n      custom: 'カスタム'\n    }\n  },\n  // プレイヤー設定\n  settings: {\n    title: '再生設定',\n    playbackSpeed: '再生速度'\n  },\n  // タイマー機能関連\n  sleepTimer: {\n    title: 'スリープタイマー',\n    cancel: 'タイマーをキャンセル',\n    timeMode: '時間で停止',\n    songsMode: '楽曲数で停止',\n    playlistEnd: 'プレイリスト終了後に停止',\n    afterPlaylist: 'プレイリスト終了後に停止',\n    activeUntilEnd: 'リスト終了まで再生',\n    minutes: '分',\n    hours: '時間',\n    songs: '曲',\n    set: '設定',\n    timerSetSuccess: '{minutes}分後に停止するよう設定しました',\n    songsSetSuccess: '{songs}曲再生後に停止するよう設定しました',\n    playlistEndSetSuccess: 'プレイリスト終了後に停止するよう設定しました',\n    timerCancelled: 'スリープタイマーをキャンセルしました',\n    timerEnded: 'スリープタイマーが作動しました',\n    playbackStopped: '音楽再生を停止しました',\n    minutesRemaining: '残り{minutes}分',\n    songsRemaining: '残り{count}曲'\n  },\n  playList: {\n    clearAll: 'プレイリストをクリア',\n    alreadyEmpty: 'プレイリストは既に空です',\n    cleared: 'プレイリストをクリアしました',\n    empty: 'プレイリストが空です',\n    clearConfirmTitle: 'プレイリストをクリア',\n    clearConfirmContent:\n      'これによりプレイリスト内のすべての楽曲がクリアされ、現在の再生が停止されます。続行しますか？'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/search.ts",
    "content": "export default {\r\n  title: {\r\n    hotSearch: '人気検索リスト',\r\n    searchList: '検索リスト',\r\n    searchHistory: '検索履歴'\r\n  },\r\n  button: {\r\n    clear: 'クリア',\r\n    back: '戻る',\r\n    playAll: 'リストを再生'\r\n  },\r\n  loading: {\r\n    more: '読み込み中...',\r\n    failed: '検索に失敗しました',\r\n    searching: '検索中...'\r\n  },\r\n  noMore: 'これ以上ありません',\r\n  error: {\r\n    searchFailed: '検索に失敗しました'\r\n  },\r\n  search: {\r\n    single: '楽曲',\r\n    album: 'アルバム',\r\n    playlist: 'プレイリスト',\r\n    mv: 'MV',\r\n    bilibili: 'Bilibili'\r\n  },\r\n  history: '検索履歴',\r\n  hot: '人気検索',\r\n  suggestions: '検索候補'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/settings.ts",
    "content": "export default {\n  theme: 'テーマ',\n  language: '言語',\n  regard: 'について',\n  logout: 'ログアウト',\n  sections: {\n    basic: '基本設定',\n    playback: '再生設定',\n    application: 'アプリケーション設定',\n    network: 'ネットワーク設定',\n    system: 'システム管理',\n    donation: '寄付サポート',\n    about: 'について'\n  },\n  basic: {\n    themeMode: 'テーマモード',\n    themeModeDesc: 'ライト/ダークテーマの切り替え',\n    autoTheme: 'システムに従う',\n    manualTheme: '手動切り替え',\n    language: '言語設定',\n    languageDesc: '表示言語を切り替え',\n    tokenManagement: 'Cookie管理',\n    tokenManagementDesc: 'NetEase Cloud MusicログインCookieを管理',\n    tokenStatus: '現在のCookieステータス',\n    tokenSet: '設定済み',\n    tokenNotSet: '未設定',\n    setToken: 'Cookieを設定',\n    modifyToken: 'Cookieを変更',\n    clearToken: 'Cookieをクリア',\n    font: 'フォント設定',\n    fontDesc: 'フォントを選択します。前に配置されたフォントが優先されます',\n    fontScope: {\n      global: 'グローバル',\n      lyric: '歌詞のみ'\n    },\n    animation: 'アニメーション速度',\n    animationDesc: 'アニメーションを有効にするかどうか',\n    animationSpeed: {\n      slow: '非常に遅い',\n      normal: '通常',\n      fast: '非常に速い'\n    },\n    fontPreview: {\n      title: 'フォントプレビュー',\n      chinese: '中国語',\n      english: 'English',\n      japanese: '日本語',\n      korean: '韓国語',\n      chineseText: '静夜思 床前明月光 疑是地上霜',\n      englishText: 'The quick brown fox jumps over the lazy dog',\n      japaneseText: 'あいうえお かきくけこ さしすせそ',\n      koreanText: '가나다라마 바사아자차 카타파하'\n    },\n    gpuAcceleration: 'GPUアクセラレーション',\n    gpuAccelerationDesc:\n      'ハードウェアアクセラレーションを有効または無効にします。レンダリングパフォーマンスを向上させますが、GPU負荷が増える可能性があります',\n    gpuAccelerationRestart: 'GPUアクセラレーション設定の変更はアプリの再起動後に有効になります',\n    gpuAccelerationChangeSuccess:\n      'GPUアクセラレーション設定を更新しました。アプリの再起動後に有効になります',\n    gpuAccelerationChangeError: 'GPUアクセラレーション設定の更新に失敗しました',\n    tabletMode: 'タブレットモード',\n    tabletModeDesc:\n      'タブレットモードを有効にすると、モバイルデバイスでPCスタイルのインターフェースを使用できます'\n  },\n  playback: {\n    quality: '音質設定',\n    qualityDesc: '音楽再生の音質を選択（NetEase Cloud VIP）',\n    qualityOptions: {\n      standard: '標準',\n      higher: '高音質',\n      exhigh: '超高音質',\n      lossless: 'ロスレス',\n      hires: 'Hi-Res',\n      jyeffect: 'HD サラウンド',\n      sky: 'イマーシブサラウンド',\n      dolby: 'Dolby Atmos',\n      jymaster: '超高解像度マスター'\n    },\n    musicSources: '音源設定',\n    musicSourcesDesc: '音楽解析に使用する音源プラットフォームを選択',\n    musicSourcesWarning: '少なくとも1つの音源プラットフォームを選択する必要があります',\n    musicUnblockEnable: '音楽解析を有効にする',\n    musicUnblockEnableDesc: '有効にすると、再生できない音楽の解析を試みます',\n    configureMusicSources: '音源を設定',\n    selectedMusicSources: '選択された音源：',\n    noMusicSources: '音源が選択されていません',\n    gdmusicInfo: 'GD音楽台は複数のプラットフォーム音源を自動解析し、最適な結果を自動選択できます',\n    autoPlay: '自動再生',\n    autoPlayDesc: 'アプリを再起動した際に自動的に再生を継続するかどうか',\n    showStatusBar: 'ステータスバーコントロール機能を表示するかどうか',\n    showStatusBarContent:\n      'Macのステータスバーに音楽コントロール機能を表示できます（再起動後に有効）',\n    fallbackParser: '代替解析サービス (GD音楽台)',\n    fallbackParserDesc:\n      '「GD音楽台」にチェックが入っていて、通常の音源で再生できない場合、このサービスが使用されます。',\n    parserGD: 'GD 音楽台 (内蔵)',\n    parserCustom: 'カスタム API',\n    sourceLabels: {\n      migu: 'Migu',\n      kugou: 'Kugou',\n      pyncmd: 'NetEase (内蔵)',\n      bilibili: 'Bilibili',\n      gdmusic: 'GD 音楽台',\n      custom: 'カスタム API'\n    },\n    customApi: {\n      sectionTitle: 'カスタム API 設定',\n      enableHint:\n        'カスタム API を有効にするには、まずカスタム API をインポートする必要があります。',\n      importConfig: 'JSON設定をインポート',\n      currentSource: '現在の音源',\n      notImported: 'カスタム音源はまだインポートされていません。',\n      importSuccess: '音源のインポートに成功しました: {name}',\n      importFailed: 'インポートに失敗しました: {message}',\n      status: {\n        imported: 'カスタム音源インポート済み',\n        notImported: '未インポート'\n      }\n    },\n    lxMusic: {\n      tabs: {\n        sources: '音源選択',\n        lxMusic: '落雪音源',\n        customApi: 'カスタムAPI'\n      },\n      scripts: {\n        title: 'インポート済みのスクリプト',\n        importLocal: 'ローカルインポート',\n        importOnline: 'オンラインインポート',\n        urlPlaceholder: '落雪音源スクリプトのURLを入力',\n        importBtn: 'インポート',\n        empty: 'インポート済みの落雪音源はありません',\n        notConfigured: '未設定（落雪音源タブで設定してください）',\n        importHint: '互換性のあるカスタムAPIプラグインをインポートして音源を拡張します',\n        noScriptWarning: '先に落雪音源スクリプトをインポートしてください',\n        noSelectionWarning: '先に落雪音源を選択してください',\n        notFound: '音源が存在しません',\n        switched: '音源を切り替えました: {name}',\n        deleted: '音源を削除しました: {name}',\n        enterUrl: 'スクリプトURLを入力してください',\n        invalidUrl: '無効なURL形式',\n        invalidScript: '無効な落雪音源スクリプトです（globalThis.lxが見つかりません）',\n        nameRequired: '名前を空にすることはできません',\n        renameSuccess: '名前を変更しました'\n      }\n    }\n  },\n  application: {\n    closeAction: '閉じる動作',\n    closeActionDesc: 'ウィンドウを閉じる際の動作を選択',\n    closeOptions: {\n      ask: '毎回確認',\n      minimize: 'トレイに最小化',\n      close: '直接終了'\n    },\n    shortcut: 'ショートカット設定',\n    shortcutDesc: 'グローバルショートカットをカスタマイズ',\n    download: 'ダウンロード管理',\n    downloadDesc: 'ダウンロードリストボタンを常に表示するかどうか',\n    unlimitedDownload: '無制限ダウンロード',\n    unlimitedDownloadDesc:\n      '有効にすると音楽を無制限でダウンロードします（ダウンロード失敗の可能性があります）。デフォルトは300曲制限',\n    downloadPath: 'ダウンロードディレクトリ',\n    downloadPathDesc: '音楽ファイルのダウンロード場所を選択',\n    remoteControl: 'リモートコントロール',\n    remoteControlDesc: 'リモートコントロール機能を設定'\n  },\n  network: {\n    apiPort: '音楽APIポート',\n    apiPortDesc: '変更後はアプリの再起動が必要です',\n    proxy: 'プロキシ設定',\n    proxyDesc: '音楽にアクセスできない場合はプロキシを有効にできます',\n    proxyHost: 'プロキシアドレス',\n    proxyHostPlaceholder: 'プロキシアドレスを入力してください',\n    proxyPort: 'プロキシポート',\n    proxyPortPlaceholder: 'プロキシポートを入力してください',\n    realIP: 'realIP設定',\n    realIPDesc:\n      '制限により、このプロジェクトは海外での使用が制限されます。realIPパラメータを使用して国内IPを渡すことで解決できます',\n    messages: {\n      proxySuccess: 'プロキシ設定を保存しました。アプリ再起動後に有効になります',\n      proxyError: '入力が正しいかどうか確認してください',\n      realIPSuccess: '実IPアドレス設定を保存しました',\n      realIPError: '有効なIPアドレスを入力してください'\n    }\n  },\n  system: {\n    cache: 'キャッシュ管理',\n    cacheDesc: 'キャッシュをクリア',\n    cacheClearTitle: 'クリアするキャッシュタイプを選択してください：',\n    cacheTypes: {\n      history: {\n        label: '再生履歴',\n        description: '再生した楽曲の記録をクリア'\n      },\n      favorite: {\n        label: 'お気に入り記録',\n        description: 'ローカルのお気に入り楽曲記録をクリア（クラウドのお気に入りには影響しません）'\n      },\n      user: {\n        label: 'ユーザーデータ',\n        description: 'ログイン情報とユーザー関連データをクリア'\n      },\n      settings: {\n        label: 'アプリ設定',\n        description: 'アプリのすべてのカスタム設定をクリア'\n      },\n      downloads: {\n        label: 'ダウンロード記録',\n        description: 'ダウンロード履歴をクリア（ダウンロード済みファイルは削除されません）'\n      },\n      resources: {\n        label: '音楽リソース',\n        description: '読み込み済みの音楽ファイル、歌詞などのリソースキャッシュをクリア'\n      },\n      lyrics: {\n        label: '歌詞リソース',\n        description: '読み込み済みの歌詞リソースキャッシュをクリア'\n      }\n    },\n    restart: '再起動',\n    restartDesc: 'アプリを再起動',\n    messages: {\n      clearSuccess: 'クリア成功。一部の設定は再起動後に有効になります'\n    }\n  },\n  about: {\n    version: 'バージョン',\n    checkUpdate: '更新を確認',\n    checking: '確認中...',\n    latest: '現在最新バージョンです',\n    hasUpdate: '新しいバージョンが見つかりました',\n    gotoUpdate: '更新へ',\n    gotoGithub: 'Githubへ',\n    author: '作者',\n    authorDesc: 'algerkong スターを付けてください🌟',\n    messages: {\n      checkError: '更新確認に失敗しました。後でもう一度お試しください'\n    }\n  },\n  validation: {\n    selectProxyProtocol: 'プロキシプロトコルを選択してください',\n    proxyHost: 'プロキシアドレスを入力してください',\n    portNumber: '有効なポート番号を入力してください（1-65535）'\n  },\n  lyricSettings: {\n    title: '歌詞設定',\n    tabs: {\n      display: '表示',\n      interface: 'インターフェース',\n      typography: 'テキスト',\n      background: '背景',\n      mobile: 'モバイル'\n    },\n    pureMode: 'ピュアモード',\n    hideCover: 'カバーを非表示',\n    centerDisplay: '中央表示',\n    showTranslation: '翻訳を表示',\n    hideLyrics: '歌詞を非表示',\n    hidePlayBar: '再生バーを非表示',\n    hideMiniPlayBar: 'ミニ再生バーを非表示',\n    showMiniPlayBar: 'ミニ再生バーを表示',\n    backgroundTheme: '背景テーマ',\n    themeOptions: {\n      default: 'デフォルト',\n      light: 'ライト',\n      dark: 'ダーク'\n    },\n    fontSize: 'フォントサイズ',\n    fontSizeMarks: {\n      small: '小',\n      medium: '中',\n      large: '大'\n    },\n    fontWeight: 'フォントの太さ',\n    fontWeightMarks: {\n      thin: '細い',\n      normal: '通常',\n      bold: '太い'\n    },\n    letterSpacing: '文字間隔',\n    letterSpacingMarks: {\n      compact: 'コンパクト',\n      default: 'デフォルト',\n      loose: 'ゆったり'\n    },\n    lineHeight: '行の高さ',\n    lineHeightMarks: {\n      compact: 'コンパクト',\n      default: 'デフォルト',\n      loose: 'ゆったり'\n    },\n    contentWidth: 'コンテンツ幅',\n    mobileLayout: 'モバイルレイアウト',\n    layoutOptions: {\n      default: 'デフォルト',\n      ios: 'iOSスタイル',\n      android: 'Androidスタイル'\n    },\n    mobileCoverStyle: 'カバースタイル',\n    coverOptions: {\n      record: 'レコード',\n      square: '正方形',\n      full: 'フルスクリーン'\n    },\n    lyricLines: '歌詞行数',\n    mobileUnavailable: 'この設定はモバイルでのみ利用可能です',\n    // 背景設定\n    background: {\n      useCustomBackground: 'カスタム背景を使用',\n      backgroundMode: '背景モード',\n      modeOptions: {\n        solid: '単色',\n        gradient: 'グラデーション',\n        image: '画像',\n        css: 'CSS'\n      },\n      solidColor: '色を選択',\n      presetColors: 'プリセットカラー',\n      customColor: 'カスタムカラー',\n      gradientEditor: 'グラデーションエディター',\n      gradientColors: 'グラデーションカラー',\n      gradientDirection: 'グラデーション方向',\n      directionOptions: {\n        toBottom: '上から下',\n        toRight: '左から右',\n        toBottomRight: '左上から右下',\n        angle45: '45度',\n        toTop: '下から上',\n        toLeft: '右から左'\n      },\n      addColor: '色を追加',\n      removeColor: '色を削除',\n      imageUpload: '画像をアップロード',\n      imagePreview: '画像プレビュー',\n      clearImage: '画像をクリア',\n      imageBlur: 'ぼかし',\n      imageBrightness: '明るさ',\n      customCss: 'カスタム CSS スタイル',\n      customCssPlaceholder: 'CSSスタイルを入力、例: background: linear-gradient(...)',\n      customCssHelp: '任意のCSS background プロパティをサポート',\n      reset: 'デフォルトにリセット',\n      fileSizeLimit: '画像サイズ制限: 20MB',\n      invalidImageFormat: '無効な画像形式',\n      imageTooLarge: '画像が大きすぎます。20MB未満の画像を選択してください'\n    }\n  },\n  translationEngine: '歌詞翻訳エンジン',\n  translationEngineOptions: {\n    none: 'オフ',\n    opencc: 'OpenCC 繁体字化'\n  },\n  themeColor: {\n    title: '歌詞テーマカラー',\n    presetColors: 'プリセットカラー',\n    customColor: 'カスタムカラー',\n    preview: 'プレビュー効果',\n    previewText: '歌詞効果',\n    colorNames: {\n      'spotify-green': 'Spotify グリーン',\n      'apple-blue': 'Apple ブルー',\n      'youtube-red': 'YouTube レッド',\n      orange: 'バイタルオレンジ',\n      purple: 'ミステリアスパープル',\n      pink: 'サクラピンク'\n    },\n    tooltips: {\n      openColorPicker: 'カラーパレットを開く',\n      closeColorPicker: 'カラーパレットを閉じる'\n    },\n    placeholder: '#1db954'\n  },\n  shortcutSettings: {\n    title: 'ショートカット設定',\n    shortcut: 'ショートカット',\n    shortcutDesc: 'ショートカットをカスタマイズ',\n    shortcutConflict: 'ショートカットの競合',\n    inputPlaceholder: 'クリックしてショートカットを入力',\n    resetShortcuts: 'デフォルトに戻す',\n    disableAll: 'すべて無効',\n    enableAll: 'すべて有効',\n    togglePlay: '再生/一時停止',\n    prevPlay: '前の曲',\n    nextPlay: '次の曲',\n    volumeUp: '音量を上げる',\n    volumeDown: '音量を下げる',\n    toggleFavorite: 'お気に入り/お気に入り解除',\n    toggleWindow: 'ウィンドウ表示/非表示',\n    scopeGlobal: 'グローバル',\n    scopeApp: 'アプリ内',\n    enabled: '有効',\n    disabled: '無効',\n    messages: {\n      resetSuccess: 'デフォルトのショートカットに戻しました。保存を忘れずに',\n      conflict: '競合するショートカットがあります。再設定してください',\n      saveSuccess: 'ショートカット設定を保存しました',\n      saveError: 'ショートカットの保存に失敗しました。再試行してください',\n      cancelEdit: '変更をキャンセルしました',\n      disableAll: 'すべてのショートカットを無効にしました。保存を忘れずに',\n      enableAll: 'すべてのショートカットを有効にしました。保存を忘れずに'\n    }\n  },\n  remoteControl: {\n    title: 'リモートコントロール',\n    enable: 'リモートコントロールを有効にする',\n    port: 'サービスポート',\n    allowedIps: '許可されたIPアドレス',\n    addIp: 'IPを追加',\n    emptyListHint: '空のリストはすべてのIPアクセスを許可することを意味します',\n    saveSuccess: 'リモートコントロール設定を保存しました',\n    accessInfo: 'リモートコントロールアクセスアドレス:'\n  },\n  cookie: {\n    title: 'Cookie設定',\n    description: 'NetEase Cloud MusicのCookieを入力してください：',\n    placeholder: '完全なCookieを貼り付けてください...',\n    help: {\n      format: 'Cookieは通常「MUSIC_U=」で始まります',\n      source: 'ブラウザの開発者ツールのネットワークリクエストから取得できます',\n      storage: 'Cookie設定後、自動的にローカルストレージに保存されます'\n    },\n    action: {\n      save: 'Cookieを保存',\n      paste: '貼り付け',\n      clear: 'クリア'\n    },\n    validation: {\n      required: 'Cookieを入力してください',\n      format: 'Cookie形式が正しくない可能性があります。MUSIC_Uが含まれているか確認してください'\n    },\n    message: {\n      saveSuccess: 'Cookieの保存に成功しました',\n      saveError: 'Cookieの保存に失敗しました',\n      pasteSuccess: '貼り付けに成功しました',\n      pasteError: '貼り付けに失敗しました。手動でコピーしてください'\n    },\n    info: {\n      length: '現在の長さ：{length} 文字'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/songItem.ts",
    "content": "export default {\r\n  menu: {\r\n    play: '再生',\r\n    playNext: '次に再生',\r\n    download: '楽曲をダウンロード',\r\n    addToPlaylist: 'プレイリストに追加',\r\n    favorite: 'いいね',\r\n    unfavorite: 'いいね解除',\r\n    removeFromPlaylist: 'プレイリストから削除',\r\n    dislike: '嫌い',\r\n    undislike: '嫌い解除'\r\n  },\r\n  message: {\r\n    downloading: 'ダウンロード中です。しばらくお待ちください...',\r\n    downloadFailed: 'ダウンロードに失敗しました',\r\n    downloadQueued: 'ダウンロードキューに追加しました',\r\n    addedToNextPlay: '次の再生に追加しました',\r\n    getUrlFailed: '音楽ダウンロードアドレスの取得に失敗しました。ログインしているか確認してください'\r\n  },\r\n  dialog: {\r\n    dislike: {\r\n      title: 'お知らせ！',\r\n      content: 'この楽曲を嫌いにしますか？再度アクセスすると毎日のおすすめから除外されます。',\r\n      positiveText: '嫌い',\r\n      negativeText: 'キャンセル'\r\n    }\r\n  }\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ja-JP/user.ts",
    "content": "export default {\r\n  profile: {\r\n    followers: 'フォロワー',\r\n    following: 'フォロー中',\r\n    level: 'レベル'\r\n  },\r\n  playlist: {\r\n    created: '作成したプレイリスト',\r\n    mine: '私が作成した',\r\n    trackCount: '{count}曲',\r\n    playCount: '{count}回再生'\r\n  },\r\n  tabs: {\r\n    created: '作成',\r\n    favorite: 'お気に入り',\r\n    album: 'アルバム'\r\n  },\r\n  ranking: {\r\n    title: '聴取ランキング',\r\n    playCount: '{count}回'\r\n  },\r\n  follow: {\r\n    title: 'フォローリスト',\r\n    viewPlaylist: 'プレイリストを見る',\r\n    noFollowings: 'フォローがありません',\r\n    loadMore: 'さらに読み込み',\r\n    noSignature: 'この人は怠け者で、何も残していません',\r\n    userFollowsTitle: 'のフォロー',\r\n    myFollowsTitle: '私のフォロー'\r\n  },\r\n  follower: {\r\n    title: 'フォロワーリスト',\r\n    noFollowers: 'フォロワーがいません',\r\n    loadMore: 'さらに読み込み',\r\n    userFollowersTitle: 'のフォロワー',\r\n    myFollowersTitle: '私のフォロワー'\r\n  },\r\n  detail: {\r\n    playlists: 'プレイリスト',\r\n    records: '聴取ランキング',\r\n    noPlaylists: 'プレイリストがありません',\r\n    noRecords: '聴取記録がありません',\r\n    artist: 'アーティスト',\r\n    noSignature: 'この人は怠け者で、何も残していません',\r\n    invalidUserId: '無効なユーザーID',\r\n    noRecordPermission: '{name}は聴取ランキングを見せてくれません'\r\n  },\r\n  message: {\r\n    loadFailed: 'ユーザーページの読み込みに失敗しました',\r\n    deleteSuccess: '削除成功',\r\n    deleteFailed: '削除失敗'\r\n  }\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/artist.ts",
    "content": "export default {\r\n  hotSongs: '인기 곡',\r\n  albums: '앨범',\r\n  description: '아티스트 소개'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/bilibili.ts",
    "content": "export default {\n  player: {\n    loading: '오디오 로딩 중...',\n    retry: '다시 시도',\n    playNow: '지금 재생',\n    loadingTitle: '로딩 중...',\n    totalDuration: '총 재생시간: {duration}',\n    partsList: '파트 목록 ({count}화)',\n    playStarted: '재생이 시작되었습니다',\n    switchingPart: '파트 전환 중: {part}',\n    preloadingNext: '다음 파트 미리 로딩 중: {part}',\n    playingCurrent: '현재 선택된 파트 재생 중: {name}',\n    num: '만',\n    errors: {\n      invalidVideoId: '유효하지 않은 비디오 ID',\n      loadVideoDetailFailed: '비디오 세부정보 로드 실패',\n      loadPartInfoFailed: '비디오 파트 정보를 로드할 수 없습니다',\n      loadAudioUrlFailed: '오디오 재생 URL 가져오기 실패',\n      videoDetailNotLoaded: '비디오 세부정보가 로드되지 않았습니다',\n      missingParams: '필수 매개변수가 누락되었습니다',\n      noAvailableAudioUrl: '사용 가능한 오디오 URL을 찾을 수 없습니다',\n      loadPartAudioFailed: '파트 오디오 URL 로드 실패',\n      audioListEmpty: '오디오 목록이 비어있습니다. 다시 시도해주세요',\n      currentPartNotFound: '현재 파트의 오디오를 찾을 수 없습니다',\n      audioUrlFailed: '오디오 URL 가져오기 실패',\n      playFailed: '재생 실패. 다시 시도해주세요',\n      getAudioUrlFailed: '오디오 URL 가져오기 실패. 다시 시도해주세요',\n      audioNotFound: '해당 오디오를 찾을 수 없습니다. 다시 시도해주세요',\n      preloadFailed: '다음 파트 미리 로딩 실패',\n      switchPartFailed: '파트 전환 시 오디오 URL 로드 실패'\n    },\n    console: {\n      loadingDetail: 'Bilibili 비디오 세부정보 로딩 중',\n      detailData: 'Bilibili 비디오 세부정보 데이터',\n      multipleParts: '비디오에 여러 파트가 있습니다. 총 {count}개',\n      noPartsData: '비디오에 파트가 없거나 파트 데이터가 비어있습니다',\n      loadingAudioSource: '오디오 소스 로딩 중',\n      generatedAudioList: '오디오 목록을 생성했습니다. 총 {count}개',\n      getDashAudioUrl: 'dash 오디오 URL을 가져왔습니다',\n      getDurlAudioUrl: 'durl 오디오 URL을 가져왔습니다',\n      loadingPartAudio: '파트 오디오 URL 로딩 중: {part}, cid: {cid}',\n      loadPartAudioFailed: '파트 오디오 URL 로드 실패: {part}',\n      switchToPart: '파트로 전환 중: {part}',\n      audioNotFoundInList: '해당 오디오 항목을 찾을 수 없습니다',\n      preparingToPlay: '현재 선택된 파트 재생 준비 중: {name}',\n      preloadingNextPart: '다음 파트 미리 로딩 중: {part}',\n      playingSelectedPart: '현재 선택된 파트 재생 중: {name}, 오디오 URL: {url}',\n      preloadNextFailed: '다음 파트 미리 로딩 실패'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/common.ts",
    "content": "export default {\r\n  play: '재생',\r\n  next: '다음 곡',\r\n  previous: '이전 곡',\r\n  volume: '볼륨',\r\n  settings: '설정',\r\n  search: '검색',\r\n  loading: '로딩 중...',\r\n  loadingMore: '더 불러오기...',\r\n  alipay: '알리페이',\r\n  wechat: '위챗 페이',\r\n  on: '켜기',\r\n  off: '끄기',\r\n  show: '표시',\r\n  hide: '숨기기',\r\n  confirm: '확인',\r\n  cancel: '취소',\r\n  configure: '구성',\r\n  open: '열기',\r\n  modify: '수정',\r\n  success: '작업 성공',\r\n  error: '작업 실패',\r\n  warning: '경고',\r\n  info: '알림',\r\n  save: '저장',\r\n  delete: '삭제',\r\n  refresh: '새로고침',\r\n  retry: '다시 시도',\r\n  reset: '재설정',\r\n  back: '뒤로',\r\n  copySuccess: '클립보드에 복사됨',\r\n  copyFailed: '복사 실패',\r\n  validation: {\r\n    required: '이 항목은 필수입니다',\r\n    invalidInput: '잘못된 입력',\r\n    selectRequired: '옵션을 선택해주세요',\r\n    numberRange: '{min}에서 {max} 사이의 숫자를 입력해주세요'\r\n  },\r\n  viewMore: '더 보기',\r\n  noMore: '더 이상 없음',\r\n  selectAll: '전체 선택',\r\n  expand: '펼치기',\r\n  collapse: '접기',\r\n  songCount: '{count}곡',\r\n  language: '언어',\r\n  today: '오늘',\r\n  yesterday: '어제',\r\n  tray: {\r\n    show: '표시',\r\n    quit: '종료',\r\n    playPause: '재생/일시정지',\r\n    prev: '이전 곡',\r\n    next: '다음 곡',\r\n    pause: '일시정지',\r\n    play: '재생',\r\n    favorite: '즐겨찾기'\r\n  }\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/comp.ts",
    "content": "export default {\r\n  installApp: {\r\n    description: '앱을 설치하여 더 나은 경험을 얻으세요',\r\n    noPrompt: '다시 묻지 않기',\r\n    install: '지금 설치',\r\n    cancel: '나중에 설치',\r\n    download: '다운로드',\r\n    downloadFailed: '다운로드 실패',\r\n    downloadComplete: '다운로드 완료',\r\n    downloadProblem: '다운로드에 문제가 있나요?',\r\n    downloadProblemLinkText: '최신 버전 다운로드'\r\n  },\r\n  playlistDrawer: {\r\n    title: '플레이리스트에 추가',\r\n    createPlaylist: '새 플레이리스트 만들기',\r\n    cancelCreate: '만들기 취소',\r\n    create: '만들기',\r\n    playlistName: '플레이리스트 이름',\r\n    privatePlaylist: '비공개 플레이리스트',\r\n    publicPlaylist: '공개 플레이리스트',\r\n    createSuccess: '플레이리스트 생성 성공',\r\n    createFailed: '플레이리스트 생성 실패',\r\n    addSuccess: '곡 추가 성공',\r\n    addFailed: '곡 추가 실패',\r\n    private: '비공개',\r\n    public: '공개',\r\n    count: '곡',\r\n    loginFirst: '먼저 로그인해주세요',\r\n    getPlaylistFailed: '플레이리스트 가져오기 실패',\r\n    inputPlaylistName: '플레이리스트 이름을 입력해주세요'\r\n  },\r\n  update: {\r\n    title: '새 버전 발견',\r\n    currentVersion: '현재 버전',\r\n    cancel: '나중에 업데이트',\r\n    prepareDownload: '다운로드 준비 중...',\r\n    downloading: '다운로드 중...',\r\n    nowUpdate: '지금 업데이트',\r\n    downloadFailed: '다운로드 실패, 다시 시도하거나 수동으로 다운로드해주세요',\r\n    startFailed: '다운로드 시작 실패, 다시 시도하거나 수동으로 다운로드해주세요',\r\n    noDownloadUrl: '현재 시스템에 적합한 설치 패키지를 찾을 수 없습니다. 수동으로 다운로드해주세요',\r\n    installConfirmTitle: '업데이트 설치',\r\n    installConfirmContent: '앱을 닫고 업데이트를 설치하시겠습니까?',\r\n    manualInstallTip:\r\n      '앱을 닫은 후 설치 프로그램이 정상적으로 나타나지 않으면 다운로드 폴더에서 파일을 찾아 수동으로 열어주세요.',\r\n    yesInstall: '지금 설치',\r\n    noThanks: '나중에 설치',\r\n    fileLocation: '파일 위치',\r\n    copy: '경로 복사',\r\n    copySuccess: '경로가 클립보드에 복사됨',\r\n    copyFailed: '복사 실패',\r\n    backgroundDownload: '백그라운드 다운로드'\r\n  },\r\n  disclaimer: {\r\n    title: '이용 안내',\r\n    warning:\r\n      '본 앱은 개발 테스트 버전으로 기능이 아직 미흡하며, 다수의 문제와 버그가 존재할 수 있습니다. 학습 및 교류 목적으로만 사용하십시오.',\r\n    item1:\r\n      '본 앱은 개인의 학습, 연구 및 기술 교류 목적으로만 사용되며, 상업적 용도로 사용하지 마십시오.',\r\n    item2:\r\n      '다운로드 후 24시간 이내에 삭제해 주십시오. 장기 사용을 원하시면 정품 음악 서비스를 이용해 주십시오.',\r\n    item3:\r\n      '본 앱을 사용함으로써 관련 위험을 이해하고 감수하는 것으로 간주합니다. 개발자는 어떠한 손실에 대해서도 책임을 지지 않습니다.',\r\n    agree: '숙지하였으며 이에 동의합니다',\r\n    disagree: '동의하지 않음 및 정지'\r\n  },\r\n  donate: {\r\n    title: '개발자 지원',\r\n    subtitle: '여러분의 지원이 저의 원동력입니다',\r\n    tip: '후원은 완전히 자율적입니다. 후원하지 않더라도 모든 기능을 정상적으로 사용할 수 있습니다. 이해와 지원에 감사드립니다!',\r\n    wechat: 'WeChat',\r\n    alipay: 'Alipay',\r\n    wechatQR: 'WeChat 결제 코드',\r\n    alipayQR: 'Alipay 결제 코드',\r\n    scanTip: '휴대전화로 위 QR 코드를 스캔하여 후원해 주세요',\r\n    enterApp: '앱 시작하기',\r\n    noForce: '후원은 강제가 아닙니다. 클릭하여 시작할 수 있습니다'\r\n  },\r\n  coffee: {\r\n    title: '커피 한 잔 사주세요',\r\n    alipay: '알리페이',\r\n    wechat: '위챗 페이',\r\n    alipayQR: '알리페이 결제 QR코드',\r\n    wechatQR: '위챗 결제 QR코드',\r\n    coffeeDesc: '커피 한 잔, 하나의 지원',\r\n    coffeeDescLinkText: '더 보기',\r\n    groupText: '微信公众号：AlgerMusic',\r\n    messages: {\r\n      copySuccess: '클립보드에 복사됨'\r\n    },\r\n    donateList: '커피 한 잔 사주세요'\r\n  },\r\n  playlistType: {\r\n    title: '플레이리스트 분류',\r\n    showAll: '모두 표시',\r\n    hide: '일부 숨기기'\r\n  },\r\n  recommendAlbum: {\r\n    title: '최신 앨범'\r\n  },\r\n  recommendSinger: {\r\n    title: '일일 추천',\r\n    songlist: '일일 추천 목록'\r\n  },\r\n  recommendSonglist: {\r\n    title: '이번 주 인기 음악'\r\n  },\r\n  searchBar: {\r\n    login: '로그인',\r\n    toLogin: '로그인하기',\r\n    logout: '로그아웃',\r\n    set: '설정',\r\n    theme: '테마',\r\n    restart: '재시작',\r\n    refresh: '새로고침',\r\n    currentVersion: '현재 버전',\r\n    searchPlaceholder: '검색해보세요...',\r\n    zoom: '페이지 확대/축소',\r\n    zoom100: '표준 확대/축소 100%',\r\n    resetZoom: '클릭하여 확대/축소 재설정',\r\n    zoomDefault: '표준 확대/축소'\r\n  },\r\n  titleBar: {\r\n    closeTitle: '닫기 방법을 선택해주세요',\r\n    minimizeToTray: '트레이로 최소화',\r\n    exitApp: '앱 종료',\r\n    rememberChoice: '선택 기억하기',\r\n    closeApp: '앱 닫기'\r\n  },\r\n  userPlayList: {\r\n    title: '{name}의 자주 듣는 음악'\r\n  },\r\n  musicList: {\r\n    searchSongs: '곡 검색',\r\n    noSearchResults: '관련 곡을 찾을 수 없습니다',\r\n    switchToNormal: '기본 레이아웃으로 전환',\r\n    switchToCompact: '컴팩트 레이아웃으로 전환',\r\n    playAll: '모두 재생',\r\n    collect: '수집',\r\n    collectSuccess: '수집 성공',\r\n    cancelCollectSuccess: '수집 취소 성공',\r\n    operationFailed: '작업 실패',\r\n    cancelCollect: '수집 취소',\r\n    addToPlaylist: '재생 목록에 추가',\r\n    addToPlaylistSuccess: '재생 목록에 추가 성공',\r\n    songsAlreadyInPlaylist: '곡이 이미 재생 목록에 있습니다',\r\n    historyRecommend: '일일 기록 권장',\r\n    fetchDatesFailed: '날짜를 가져오지 못했습니다',\r\n    fetchSongsFailed: '곡을 가져오지 못했습니다',\r\n    noSongs: '노래 없음'\r\n  },\r\n  playlist: {\r\n    import: {\r\n      button: '플레이리스트 가져오기',\r\n      title: '플레이리스트 가져오기',\r\n      description: '메타데이터/텍스트/링크 세 가지 방법으로 플레이리스트 가져오기 지원',\r\n      linkTab: '링크 가져오기',\r\n      textTab: '텍스트 가져오기',\r\n      localTab: '메타데이터 가져오기',\r\n      linkPlaceholder: '플레이리스트 링크를 입력하세요. 한 줄에 하나씩',\r\n      textPlaceholder: '곡 정보를 입력하세요. 형식: 곡명 가수명',\r\n      localPlaceholder: 'JSON 형식의 곡 메타데이터를 입력하세요',\r\n      linkTips: '지원되는 링크 소스:',\r\n      linkTip1: '플레이리스트를 위챗/웨이보/QQ로 공유한 후 링크 복사',\r\n      linkTip2: '플레이리스트/개인 홈페이지 링크 직접 복사',\r\n      linkTip3: '기사 링크 직접 복사',\r\n      textTips: '곡 정보를 입력하세요. 한 줄에 한 곡씩',\r\n      textFormat: '형식: 곡명 가수명',\r\n      localTips: '곡 메타데이터를 추가해주세요',\r\n      localFormat: '형식 예시:',\r\n      songNamePlaceholder: '곡명',\r\n      artistNamePlaceholder: '아티스트명',\r\n      albumNamePlaceholder: '앨범명',\r\n      addSongButton: '곡 추가',\r\n      addLinkButton: '링크 추가',\r\n      importToStarPlaylist: '내가 좋아하는 음악으로 가져오기',\r\n      playlistNamePlaceholder: '플레이리스트 이름을 입력하세요',\r\n      importButton: '가져오기 시작',\r\n      emptyLinkWarning: '플레이리스트 링크를 입력해주세요',\r\n      emptyTextWarning: '곡 정보를 입력해주세요',\r\n      emptyLocalWarning: '곡 메타데이터를 입력해주세요',\r\n      invalidJsonFormat: 'JSON 형식이 올바르지 않습니다',\r\n      importSuccess: '가져오기 작업 생성 성공',\r\n      importFailed: '가져오기 실패',\r\n      importStatus: '가져오기 상태',\r\n      refresh: '새로고침',\r\n      taskId: '작업 ID',\r\n      status: '상태',\r\n      successCount: '성공 수',\r\n      failReason: '실패 이유',\r\n      unknownError: '알 수 없는 오류',\r\n      statusPending: '처리 대기 중',\r\n      statusProcessing: '처리 중',\r\n      statusSuccess: '가져오기 성공',\r\n      statusFailed: '가져오기 실패',\r\n      statusUnknown: '알 수 없는 상태',\r\n      taskList: '작업 목록',\r\n      taskListTitle: '가져오기 작업 목록',\r\n      action: '작업',\r\n      select: '선택',\r\n      fetchTaskListFailed: '작업 목록 가져오기 실패',\r\n      noTasks: '가져오기 작업이 없습니다',\r\n      clearTasks: '작업 지우기',\r\n      clearTasksConfirmTitle: '지우기 확인',\r\n      clearTasksConfirmContent:\r\n        '모든 가져오기 작업 기록을 지우시겠습니까? 이 작업은 되돌릴 수 없습니다.',\r\n      confirm: '확인',\r\n      cancel: '취소',\r\n      clearTasksSuccess: '작업 목록이 지워졌습니다',\r\n      clearTasksFailed: '작업 목록 지우기 실패'\r\n    }\r\n  },\r\n  settings: '설정',\r\n  user: '사용자',\r\n  toplist: '순위',\r\n  history: '수집 기록',\r\n  list: '플레이리스트',\r\n  mv: 'MV',\r\n  home: '홈',\r\n  search: '검색'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/donation.ts",
    "content": "export default {\r\n  description:\r\n    '귀하의 기부는 서버 유지보수, 도메인 갱신 등을 포함한 개발 및 유지보수 작업을 지원하는 데 사용됩니다.',\r\n  message: '메시지를 남길 때 이메일이나 GitHub 이름을 남겨주세요.',\r\n  refresh: '목록 새로고침',\r\n  toDonateList: '커피 한 잔 사주세요',\r\n  noMessage: '메시지가 없습니다',\r\n  title: '기부 목록'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/download.ts",
    "content": "export default {\r\n  title: '다운로드 관리',\r\n  localMusic: '로컬 음악',\r\n  count: '총 {count}곡',\r\n  clearAll: '기록 지우기',\r\n  settings: '설정',\r\n  tabs: {\r\n    downloading: '다운로드 중',\r\n    downloaded: '다운로드 완료'\r\n  },\r\n  empty: {\r\n    noTasks: '다운로드 작업이 없습니다',\r\n    noDownloaded: '다운로드된 곡이 없습니다'\r\n  },\r\n  progress: {\r\n    total: '전체 진행률: {progress}%'\r\n  },\r\n  status: {\r\n    downloading: '다운로드 중',\r\n    completed: '완료',\r\n    failed: '실패',\r\n    unknown: '알 수 없음'\r\n  },\r\n  artist: {\r\n    unknown: '알 수 없는 가수'\r\n  },\r\n  delete: {\r\n    title: '삭제 확인',\r\n    message: '곡 \"{filename}\"을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.',\r\n    confirm: '삭제 확인',\r\n    cancel: '취소',\r\n    success: '삭제 성공',\r\n    failed: '삭제 실패',\r\n    fileNotFound: '파일이 존재하지 않거나 이동되었습니다. 기록에서 제거되었습니다',\r\n    recordRemoved: '파일 삭제 실패, 하지만 기록에서 제거되었습니다'\r\n  },\r\n  clear: {\r\n    title: '다운로드 기록 지우기',\r\n    message:\r\n      '모든 다운로드 기록을 지우시겠습니까? 이 작업은 다운로드된 음악 파일을 삭제하지 않지만 모든 기록을 지웁니다.',\r\n    confirm: '지우기 확인',\r\n    cancel: '취소',\r\n    success: '다운로드 기록이 지워졌습니다'\r\n  },\r\n  message: {\r\n    downloadComplete: '{filename} 다운로드 완료',\r\n    downloadFailed: '{filename} 다운로드 실패: {error}'\r\n  },\r\n  loading: '로딩 중...',\r\n  playStarted: '재생 시작: {name}',\r\n  playFailed: '재생 실패: {name}',\r\n  path: {\r\n    copied: '경로가 클립보드에 복사됨',\r\n    copyFailed: '경로 복사 실패'\r\n  },\r\n  settingsPanel: {\r\n    title: '다운로드 설정',\r\n    path: '다운로드 위치',\r\n    pathDesc: '음악 파일 다운로드 저장 위치 설정',\r\n    pathPlaceholder: '다운로드 경로를 선택해주세요',\r\n    noPathSelected: '먼저 다운로드 경로를 선택해주세요',\r\n    select: '폴더 선택',\r\n    open: '폴더 열기',\r\n    fileFormat: '파일명 형식',\r\n    fileFormatDesc: '음악 다운로드 시 파일 이름 형식 설정',\r\n    customFormat: '사용자 정의 형식',\r\n    separator: '구분자',\r\n    separators: {\r\n      dash: '공백-공백',\r\n      underscore: '밑줄',\r\n      space: '공백'\r\n    },\r\n    dragToArrange: '드래그하여 정렬하거나 화살표 버튼을 사용하여 순서 조정:',\r\n    formatVariables: '사용 가능한 변수',\r\n    preview: '미리보기 효과:',\r\n    saveSuccess: '다운로드 설정이 저장됨',\r\n    presets: {\r\n      songArtist: '곡명 - 가수명',\r\n      artistSong: '가수명 - 곡명',\r\n      songOnly: '곡명만'\r\n    },\r\n    components: {\r\n      songName: '곡명',\r\n      artistName: '가수명',\r\n      albumName: '앨범명'\r\n    }\r\n  }\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/favorite.ts",
    "content": "export default {\r\n  title: '내 수집',\r\n  count: '총 {count}곡',\r\n  batchDownload: '일괄 다운로드',\r\n  download: '다운로드 ({count})',\r\n  emptyTip: '아직 수집한 곡이 없습니다',\r\n  downloadSuccess: '다운로드 완료',\r\n  downloadFailed: '다운로드 실패',\r\n  downloading: '다운로드 중입니다. 잠시만 기다려주세요...',\r\n  selectSongsFirst: '먼저 다운로드할 곡을 선택해주세요',\r\n  descending: '내림차순',\r\n  ascending: '오름차순'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/history.ts",
    "content": "export default {\r\n  title: '재생 기록',\r\n  heatmapTitle: '히트맵',\r\n  playCount: '{count}',\r\n  getHistoryFailed: '기록 가져오기 실패',\r\n  tabs: {\r\n    all: '전체 기록',\r\n    local: '로컬 기록',\r\n    cloud: '클라우드 기록'\r\n  },\r\n  categoryTabs: {\r\n    songs: '곡',\r\n    playlists: '플레이리스트',\r\n    albums: '앨범'\r\n  },\r\n  noDescription: '설명 없음',\r\n  noData: '기록 없음',\r\n  getCloudRecordFailed: '클라우드 기록 가져오기 실패',\r\n  needLogin: 'cookie를 사용하여 로그인하여 클라우드 기록을 볼 수 있습니다',\r\n  merging: '기록 병합 중...',\r\n  heatmap: {\r\n    title: '재생 히트맵',\r\n    loading: '데이터 로딩 중...',\r\n    unit: '회 재생',\r\n    footerText: '마우스를 올려서 자세히 보기',\r\n    playCount: '{count}회 재생',\r\n    topSongs: '오늘의 인기곡',\r\n    times: '회',\r\n    totalPlays: '총 재생 횟수',\r\n    activeDays: '활동 일수',\r\n    noData: '재생 기록이 없습니다',\r\n    colorTheme: '색상 테마',\r\n    colors: {\r\n      green: '그린',\r\n      blue: '블루',\r\n      orange: '오렌지',\r\n      purple: '퍼플',\r\n      red: '레드'\r\n    },\r\n    mostPlayedSong: '가장 많이 재생한 노래',\r\n    mostActiveDay: '가장 활발한 날',\r\n    latestNightSong: '가장 늘게 재생한 노래'\r\n  }\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/login.ts",
    "content": "export default {\r\n  title: {\r\n    qr: 'QR코드 로그인',\r\n    phone: '휴대폰 번호 로그인',\r\n    cookie: 'Cookie 로그인',\r\n    uid: 'UID 로그인'\r\n  },\r\n  qrTip: '넷이즈 클라우드 뮤직 앱으로 QR코드를 스캔하여 로그인',\r\n  phoneTip: '넷이즈 클라우드 계정으로 로그인',\r\n  tokenTip: '유효한 넷이즈 클라우드 뮤직 Cookie을 입력하여 로그인',\r\n  uidTip: '사용자 ID를 입력하여 빠른 로그인',\r\n  placeholder: {\r\n    phone: '휴대폰 번호',\r\n    password: '비밀번호',\r\n    cookie: '넷이즈 클라우드 뮤직 Cookie(token)을 입력하세요',\r\n    uid: '사용자 ID(UID)를 입력하세요'\r\n  },\r\n  button: {\r\n    login: '로그인',\r\n    switchToQr: 'QR코드 로그인',\r\n    switchToPhone: '휴대폰 번호 로그인',\r\n    switchToToken: 'Cookie 로그인 사용',\r\n    switchToUid: 'UID 로그인',\r\n    backToQr: 'QR코드 로그인으로 돌아가기',\r\n    cookieLogin: 'Cookie 로그인',\r\n    autoGetCookie: 'Cookie 자동 가져오기',\r\n    refresh: '새로고침',\r\n    refreshing: '새로고침 중...',\r\n    refreshQr: 'QR코드 새로고침'\r\n  },\r\n  message: {\r\n    loginSuccess: '로그인 성공',\r\n    tokenLoginSuccess: 'Cookie 로그인 성공',\r\n    uidLoginSuccess: 'UID 로그인 성공',\r\n    loadError: '로그인 정보 로드 중 오류 발생',\r\n    qrCheckError: 'QR코드 상태 확인 중 오류 발생',\r\n    tokenRequired: 'Cookie을 입력하세요',\r\n    tokenInvalid: 'Cookie이 유효하지 않습니다. 확인 후 다시 시도하세요',\r\n    uidRequired: '사용자 ID를 입력하세요',\r\n    uidInvalid: '사용자 ID가 유효하지 않거나 사용자가 존재하지 않습니다',\r\n    uidLoginFailed: 'UID 로그인에 실패했습니다. 사용자 ID가 올바른지 확인하세요',\r\n    autoGetCookieSuccess: 'Cookie 자동 가져오기 성공',\r\n    autoGetCookieFailed: 'Cookie 자동 가져오기 실패',\r\n    autoGetCookieTip:\r\n      '넷이즈 클라우드 뮤직 로그인 페이지를 열겠습니다. 로그인 완료 후 창을 닫아주세요',\r\n    loginFailed: '로그인 실패',\r\n    phoneRequired: '휴대폰 번호를 입력하세요',\r\n    passwordRequired: '비밀번호를 입력하세요',\r\n    phoneLoginFailed: '휴대폰 번호 로그인 실패, 휴대폰 번호와 비밀번호가 올바른지 확인하세요',\r\n    qrCheckFailed: 'QR코드 상태 확인 실패, 새로고침하여 다시 시도하세요',\r\n    qrLoading: 'QR코드 로딩 중...',\r\n    qrExpired: 'QR코드가 만료되었습니다. 클릭하여 새로고침하세요',\r\n    qrExpiredShort: 'QR코드 만료됨',\r\n    qrExpiredWarning: 'QR코드가 만료되었습니다. 클릭하여 새로운 QR코드를 받으세요',\r\n    qrScanned: 'QR코드가 스캔되었습니다. 휴대폰에서 로그인을 확인하세요',\r\n    qrScannedShort: '스캔됨',\r\n    qrScannedInfo: 'QR코드가 스캔되었습니다. 휴대폰에서 로그인을 확인하세요',\r\n    qrConfirmed: '로그인 성공, 리다이렉트 중...',\r\n    qrGenerating: 'QR코드를 생성 중...'\r\n  },\r\n  qrTitle: '넷이즈 클라우드 뮤직 QR코드 로그인',\r\n  uidWarning:\r\n    '주의: UID 로그인은 사용자 공개 정보를 확인하는 데만 사용할 수 있으며, 로그인 권한이 필요한 기능에 액세스할 수 없습니다.'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/player.ts",
    "content": "export default {\n  nowPlaying: '현재 재생 중',\n  playlist: '재생 목록',\n  lyrics: '가사',\n  previous: '이전',\n  play: '재생',\n  pause: '일시정지',\n  next: '다음',\n  volumeUp: '볼륨 증가',\n  volumeDown: '볼륨 감소',\n  mute: '음소거',\n  unmute: '음소거 해제',\n  songNum: '총 곡 수: {num}',\n  addCorrection: '{num}초 앞당기기',\n  subtractCorrection: '{num}초 지연',\n  playFailed: '현재 곡 재생 실패, 다음 곡 재생',\n  parseFailedPlayNext: '곡 분석 실패, 다음 곡 재생',\n  consecutiveFailsError:\n    '재생 오류가 발생했습니다. 네트워크 문제 또는 유효하지 않은 음원일 수 있습니다. 재생 목록을 변경하거나 나중에 다시 시도하세요',\n  playMode: {\n    sequence: '순차 재생',\n    loop: '한 곡 반복',\n    random: '랜덤 재생'\n  },\n  fullscreen: {\n    enter: '전체화면',\n    exit: '전체화면 종료'\n  },\n  close: '닫기',\n  modeHint: {\n    single: '한 곡 반복',\n    list: '자동으로 다음 곡 재생'\n  },\n  lrc: {\n    noLrc: '가사가 없습니다. 음악을 감상해주세요',\n    noAutoScroll: '본 가사는 자동 스크롤을 지원하지 않습니다'\n  },\n  reparse: {\n    title: '음원 선택',\n    desc: '음원을 클릭하여 직접 분석하세요. 다음에 이 곡을 재생할 때 선택한 음원을 사용합니다',\n    success: '재분석 성공',\n    failed: '재분석 실패',\n    warning: '음원을 선택해주세요',\n    bilibiliNotSupported: 'B站 비디오는 재분석을 지원하지 않습니다',\n    processing: '분석 중...',\n    clear: '사용자 정의 음원 지우기',\n    customApiFailed: '사용자 정의 API 분석 실패, 기본 음원을 시도합니다...',\n    customApiError: '사용자 정의 API 요청 오류, 기본 음원을 시도합니다...'\n  },\n  playBar: {\n    expand: '가사 펼치기',\n    collapse: '가사 접기',\n    like: '좋아요',\n    lyric: '가사',\n    noSongPlaying: '재생 중인 곡이 없습니다',\n    eq: '이퀄라이저',\n    playList: '재생 목록',\n    reparse: '재분석',\n    playMode: {\n      sequence: '순차 재생',\n      loop: '반복 재생',\n      random: '랜덤 재생'\n    },\n    play: '재생 시작',\n    pause: '재생 일시정지',\n    prev: '이전 곡',\n    next: '다음 곡',\n    volume: '볼륨',\n    favorite: '{name} 즐겨찾기 추가됨',\n    unFavorite: '{name} 즐겨찾기 해제됨',\n    miniPlayBar: '미니 재생바',\n    playbackSpeed: '재생 속도',\n    advancedControls: '고급 설정',\n    intelligenceMode: {\n      title: '인텔리전스 모드',\n      needCookieLogin: '쿠키 방식으로 로그인한 후 인텔리전스 모드를 사용할 수 있습니다',\n      noFavoritePlaylist: '내가 좋아하는 음악 재생목록을 찾을 수 없습니다',\n      noLikedSongs: '아직 좋아한 노래가 없습니다',\n      loading: '인텔리전스 모드를 불러오는 중',\n      success: '총 {count}곡을 불러왔습니다',\n      failed: '인텔리전스 모드 목록을 가져오는 데 실패했습니다',\n      error: '인텔리전스 모드 재생 오류'\n    }\n  },\n  eq: {\n    title: '이퀄라이저',\n    reset: '재설정',\n    on: '켜기',\n    off: '끄기',\n    bass: '저음',\n    midrange: '중음',\n    treble: '고음',\n    presets: {\n      flat: '플랫',\n      pop: '팝',\n      rock: '록',\n      classical: '클래식',\n      jazz: '재즈',\n      electronic: '일렉트로닉',\n      hiphop: '힙합',\n      rb: 'R&B',\n      metal: '메탈',\n      vocal: '보컬',\n      dance: '댄스',\n      acoustic: '어쿠스틱',\n      custom: '사용자 정의'\n    }\n  },\n  // 플레이어 설정\n  settings: {\n    title: '재생 설정',\n    playbackSpeed: '재생 속도'\n  },\n  sleepTimer: {\n    title: '타이머 종료',\n    cancel: '타이머 취소',\n    timeMode: '시간으로 종료',\n    songsMode: '곡 수로 종료',\n    playlistEnd: '재생 목록 완료 후 종료',\n    afterPlaylist: '재생 목록 완료 후 종료',\n    activeUntilEnd: '목록 끝까지 재생',\n    minutes: '분',\n    hours: '시간',\n    songs: '곡',\n    set: '설정',\n    timerSetSuccess: '{minutes}분 후 종료로 설정됨',\n    songsSetSuccess: '{songs}곡 재생 후 종료로 설정됨',\n    playlistEndSetSuccess: '재생 목록 완료 후 종료로 설정됨',\n    timerCancelled: '타이머 종료 취소됨',\n    timerEnded: '타이머 종료 실행됨',\n    playbackStopped: '음악 재생이 중지됨',\n    minutesRemaining: '남은 시간 {minutes}분',\n    songsRemaining: '남은 곡 수 {count}곡'\n  },\n  playList: {\n    clearAll: '재생 목록 비우기',\n    alreadyEmpty: '재생 목록이 이미 비어있습니다',\n    cleared: '재생 목록이 비워졌습니다',\n    empty: '재생 목록이 비어있습니다',\n    clearConfirmTitle: '재생 목록 비우기',\n    clearConfirmContent: '재생 목록의 모든 곡을 삭제하고 현재 재생을 중지합니다. 계속하시겠습니까?'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/search.ts",
    "content": "export default {\r\n  title: {\r\n    hotSearch: '인기 검색',\r\n    searchList: '검색 목록',\r\n    searchHistory: '검색 기록'\r\n  },\r\n  button: {\r\n    clear: '지우기',\r\n    back: '뒤로',\r\n    playAll: '재생 목록'\r\n  },\r\n  loading: {\r\n    more: '로딩 중...',\r\n    failed: '검색 실패',\r\n    searching: '검색 중...'\r\n  },\r\n  noMore: '더 이상 없음',\r\n  error: {\r\n    searchFailed: '검색 실패'\r\n  },\r\n  search: {\r\n    single: '단일곡',\r\n    album: '앨범',\r\n    playlist: '플레이리스트',\r\n    mv: 'MV',\r\n    bilibili: 'B站'\r\n  },\r\n  history: '검색 기록',\r\n  hot: '인기 검색',\r\n  suggestions: '검색 제안'\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/settings.ts",
    "content": "export default {\n  theme: '테마',\n  language: '언어',\n  regard: '정보',\n  logout: '로그아웃',\n  sections: {\n    basic: '기본 설정',\n    playback: '재생 설정',\n    application: '애플리케이션 설정',\n    network: '네트워크 설정',\n    system: '시스템 관리',\n    donation: '후원 지원',\n    about: '정보'\n  },\n  basic: {\n    themeMode: '테마 모드',\n    themeModeDesc: '낮/밤 테마 전환',\n    autoTheme: '시스템 따라가기',\n    manualTheme: '수동 전환',\n    language: '언어 설정',\n    languageDesc: '표시 언어 전환',\n    tokenManagement: 'Cookie 관리',\n    tokenManagementDesc: '넷이즈 클라우드 뮤직 로그인 Cookie 관리',\n    tokenStatus: '현재 Cookie 상태',\n    tokenSet: '설정됨',\n    tokenNotSet: '설정되지 않음',\n    setToken: 'Cookie 설정',\n    modifyToken: 'Cookie 수정',\n    clearToken: 'Cookie 지우기',\n    font: '폰트 설정',\n    fontDesc: '폰트 선택, 앞에 있는 폰트를 우선 사용',\n    fontScope: {\n      global: '전역',\n      lyric: '가사만'\n    },\n    animation: '애니메이션 속도',\n    animationDesc: '애니메이션 활성화 여부',\n    animationSpeed: {\n      slow: '매우 느림',\n      normal: '보통',\n      fast: '매우 빠름'\n    },\n    fontPreview: {\n      title: '폰트 미리보기',\n      chinese: '中文',\n      english: 'English',\n      japanese: '日本語',\n      korean: '한국어',\n      chineseText: '静夜思 床前明月光 疑是地上霜',\n      englishText: 'The quick brown fox jumps over the lazy dog',\n      japaneseText: 'あいうえお かきくけこ さしすせそ',\n      koreanText: '가나다라마 바사아자차 카타파하'\n    },\n    gpuAcceleration: 'GPU 가속',\n    gpuAccelerationDesc:\n      'GPU 가속을 사용하면 애니메이션이 빠르게 재생되고 애니메이션이 느리게 재생되는 것보다 느릴 수 있습니다.',\n    gpuAccelerationRestart: 'GPU 가속 설정을 변경하면 애플리케이션을 다시 시작해야 합니다',\n    gpuAccelerationChangeSuccess:\n      'GPU 가속 설정이 업데이트되었습니다. 애플리케이션을 다시 시작하여 적용하십시오',\n    gpuAccelerationChangeError: 'GPU 가속 설정 업데이트에 실패했습니다',\n    tabletMode: '태블릿 모드',\n    tabletModeDesc:\n      '태블릿 모드를 사용하면 모바일 기기에서 PC 스타일의 인터페이스를 사용할 수 있습니다'\n  },\n  playback: {\n    quality: '음질 설정',\n    qualityDesc: '음악 재생 음질 선택 (넷이즈 클라우드 VIP)',\n    qualityOptions: {\n      standard: '표준',\n      higher: '높음',\n      exhigh: '매우 높음',\n      lossless: '무손실',\n      hires: 'Hi-Res',\n      jyeffect: 'HD 서라운드',\n      sky: '몰입형 서라운드',\n      dolby: '돌비 애트모스',\n      jymaster: '초고화질 마스터'\n    },\n    musicSources: '음원 설정',\n    musicSourcesDesc: '음악 해석에 사용할 음원 플랫폼 선택',\n    musicSourcesWarning: '최소 하나의 음원 플랫폼을 선택해야 합니다',\n    musicUnblockEnable: '음악 해석 활성화',\n    musicUnblockEnableDesc: '활성화하면 재생할 수 없는 음악을 해석하려고 시도합니다',\n    configureMusicSources: '음원 구성',\n    selectedMusicSources: '선택된 음원：',\n    noMusicSources: '음원이 선택되지 않음',\n    gdmusicInfo: 'GD 뮤직은 여러 플랫폼 음원을 자동으로 해석하고 최적의 결과를 자동 선택합니다',\n    autoPlay: '자동 재생',\n    autoPlayDesc: '앱을 다시 열 때 자동으로 재생을 계속할지 여부',\n    showStatusBar: '상태바 제어 기능 표시 여부',\n    showStatusBarContent: 'Mac 상태바에 음악 제어 기능을 표시할 수 있습니다 (재시작 후 적용)',\n    fallbackParser: '대체 분석 서비스 (GD Music)',\n    fallbackParserDesc:\n      '\"GD Music\"을 선택하고 일반 음원을 사용할 수 없을 때 이 서비스를 사용합니다.',\n    parserGD: 'GD Music (내장)',\n    parserCustom: '사용자 지정 API',\n\n    // 음원 라벨\n    sourceLabels: {\n      migu: 'Migu',\n      kugou: 'Kugou',\n      pyncmd: 'NetEase (내장)',\n      bilibili: 'Bilibili',\n      gdmusic: 'GD Music',\n      custom: '사용자 지정 API'\n    },\n\n    customApi: {\n      sectionTitle: '사용자 지정 API 설정',\n      importConfig: 'JSON 설정 가져오기',\n      currentSource: '현재 음원',\n      notImported: '아직 사용자 지정 음원을 가져오지 않았습니다.',\n      importSuccess: '음원 가져오기 성공: {name}',\n      importFailed: '가져오기 실패: {message}',\n      enableHint: '사용하려면 먼저 JSON 구성 파일을 가져오세요',\n      status: {\n        imported: '사용자 지정 음원 가져옴',\n        notImported: '가져오지 않음'\n      }\n    },\n    lxMusic: {\n      tabs: {\n        sources: '음원 선택',\n        lxMusic: '낙설 음원',\n        customApi: '사용자 정의 API'\n      },\n      scripts: {\n        title: '가져온 스크립트',\n        importLocal: '로컬 가져오기',\n        importOnline: '온라인 가져오기',\n        urlPlaceholder: '낙설 음원 스크립트 URL 입력',\n        importBtn: '가져오기',\n        empty: '가져온 낙설 음원이 없습니다',\n        notConfigured: '설정되지 않음 (낙설 음원 탭에서 설정하세요)',\n        importHint: '소스 확장을 위해 호환되는 사용자 정의 API 플러그인을 가져옵니다',\n        noScriptWarning: '먼저 낙설 음원 스크립트를 가져오세요',\n        noSelectionWarning: '먼저 낙설 음원 소스를 선택하세요',\n        notFound: '음원이 존재하지 않습니다',\n        switched: '음원으로 전환되었습니다: {name}',\n        deleted: '음원이 삭제되었습니다: {name}',\n        enterUrl: '스크립트 URL을 입력하세요',\n        invalidUrl: '유효하지 않은 URL 형식',\n        invalidScript: '유효하지 않은 낙설 음원 스크립트입니다 (globalThis.lx 코드를 찾을 수 없음)',\n        nameRequired: '이름은 비워둘 수 없습니다',\n        renameSuccess: '이름이 변경되었습니다'\n      }\n    }\n  },\n  application: {\n    closeAction: '닫기 동작',\n    closeActionDesc: '창을 닫을 때의 동작 선택',\n    closeOptions: {\n      ask: '매번 묻기',\n      minimize: '트레이로 최소화',\n      close: '직접 종료'\n    },\n    shortcut: '단축키 설정',\n    shortcutDesc: '전역 단축키 사용자 정의',\n    download: '다운로드 관리',\n    downloadDesc: '다운로드 목록 버튼을 항상 표시할지 여부',\n    unlimitedDownload: '무제한 다운로드',\n    unlimitedDownloadDesc:\n      '활성화하면 음악을 무제한으로 다운로드합니다 (다운로드 실패가 발생할 수 있음), 기본 제한 300곡',\n    downloadPath: '다운로드 디렉토리',\n    downloadPathDesc: '음악 파일의 다운로드 위치 선택',\n    remoteControl: '원격 제어',\n    remoteControlDesc: '원격 제어 기능 설정'\n  },\n  network: {\n    apiPort: '음악 API 포트',\n    apiPortDesc: '수정 후 앱을 재시작해야 합니다',\n    proxy: '프록시 설정',\n    proxyDesc: '음악에 액세스할 수 없을 때 프록시를 활성화할 수 있습니다',\n    proxyHost: '프록시 주소',\n    proxyHostPlaceholder: '프록시 주소를 입력하세요',\n    proxyPort: '프록시 포트',\n    proxyPortPlaceholder: '프록시 포트를 입력하세요',\n    realIP: 'realIP 설정',\n    realIPDesc:\n      '제한으로 인해 이 프로젝트는 해외에서 사용할 때 제한을 받을 수 있으며, realIP 매개변수를 사용하여 국내 IP를 전달하여 해결할 수 있습니다',\n    messages: {\n      proxySuccess: '프록시 설정이 저장되었습니다. 앱을 재시작한 후 적용됩니다',\n      proxyError: '입력이 올바른지 확인하세요',\n      realIPSuccess: '실제 IP 설정이 저장되었습니다',\n      realIPError: '유효한 IP 주소를 입력하세요'\n    }\n  },\n  system: {\n    cache: '캐시 관리',\n    cacheDesc: '캐시 지우기',\n    cacheClearTitle: '지울 캐시 유형을 선택하세요：',\n    cacheTypes: {\n      history: {\n        label: '재생 기록',\n        description: '재생한 곡 기록 지우기'\n      },\n      favorite: {\n        label: '즐겨찾기 기록',\n        description: '로컬 즐겨찾기 곡 기록 지우기 (클라우드 즐겨찾기에는 영향 없음)'\n      },\n      user: {\n        label: '사용자 데이터',\n        description: '로그인 정보 및 사용자 관련 데이터 지우기'\n      },\n      settings: {\n        label: '앱 설정',\n        description: '앱의 모든 사용자 정의 설정 지우기'\n      },\n      downloads: {\n        label: '다운로드 기록',\n        description: '다운로드 기록 지우기 (다운로드된 파일은 삭제되지 않음)'\n      },\n      resources: {\n        label: '음악 리소스',\n        description: '로드된 음악 파일, 가사 등 리소스 캐시 지우기'\n      },\n      lyrics: {\n        label: '가사 리소스',\n        description: '로드된 가사 리소스 캐시 지우기'\n      }\n    },\n    restart: '재시작',\n    restartDesc: '앱 재시작',\n    messages: {\n      clearSuccess: '지우기 성공, 일부 설정은 재시작 후 적용됩니다'\n    }\n  },\n  about: {\n    version: '버전',\n    checkUpdate: '업데이트 확인',\n    checking: '확인 중...',\n    latest: '현재 최신 버전입니다',\n    hasUpdate: '새 버전 발견',\n    gotoUpdate: '업데이트하러 가기',\n    gotoGithub: 'Github로 이동',\n    author: '작성자',\n    authorDesc: 'algerkong 별점🌟 부탁드려요',\n    messages: {\n      checkError: '업데이트 확인 실패, 나중에 다시 시도하세요'\n    }\n  },\n  validation: {\n    selectProxyProtocol: '프록시 프로토콜을 선택하세요',\n    proxyHost: '프록시 주소를 입력하세요',\n    portNumber: '유효한 포트 번호를 입력하세요 (1-65535)'\n  },\n  lyricSettings: {\n    title: '가사 설정',\n    tabs: {\n      display: '표시',\n      interface: '인터페이스',\n      typography: '텍스트',\n      background: '배경',\n      mobile: '모바일'\n    },\n    pureMode: '순수 모드',\n    hideCover: '커버 숨기기',\n    centerDisplay: '중앙 표시',\n    showTranslation: '번역 표시',\n    hideLyrics: '가사 숨기기',\n    hidePlayBar: '재생바 숨기기',\n    hideMiniPlayBar: '미니 재생바 숨기기',\n    showMiniPlayBar: '미니 재생바 표시',\n    backgroundTheme: '배경 테마',\n    themeOptions: {\n      default: '기본',\n      light: '밝음',\n      dark: '어둠'\n    },\n    fontSize: '폰트 크기',\n    fontSizeMarks: {\n      small: '작음',\n      medium: '중간',\n      large: '큼'\n    },\n    fontWeight: '글꼴 두께',\n    fontWeightMarks: {\n      thin: '가늘게',\n      normal: '보통',\n      bold: '굵게'\n    },\n    letterSpacing: '글자 간격',\n    letterSpacingMarks: {\n      compact: '좁음',\n      default: '기본',\n      loose: '넓음'\n    },\n    lineHeight: '줄 높이',\n    lineHeightMarks: {\n      compact: '좁음',\n      default: '기본',\n      loose: '넓음'\n    },\n    contentWidth: '콘텐츠 너비',\n    mobileLayout: '모바일 레이아웃',\n    layoutOptions: {\n      default: '기본',\n      ios: 'iOS 스타일',\n      android: '안드로이드 스타일'\n    },\n    mobileCoverStyle: '커버 스타일',\n    coverOptions: {\n      record: '레코드',\n      square: '정사각형',\n      full: '전체화면'\n    },\n    lyricLines: '가사 줄 수',\n    mobileUnavailable: '이 설정은 모바일에서만 사용 가능합니다',\n    // 배경 설정\n    background: {\n      useCustomBackground: '사용자 정의 배경 사용',\n      backgroundMode: '배경 모드',\n      modeOptions: {\n        solid: '단색',\n        gradient: '그라데이션',\n        image: '이미지',\n        css: 'CSS'\n      },\n      solidColor: '색상 선택',\n      presetColors: '프리셋 색상',\n      customColor: '사용자 정의 색상',\n      gradientEditor: '그라데이션 편집기',\n      gradientColors: '그라데이션 색상',\n      gradientDirection: '그라데이션 방향',\n      directionOptions: {\n        toBottom: '위에서 아래로',\n        toRight: '왼쪽에서 오른쪽으로',\n        toBottomRight: '왼쪽 위에서 오른쪽 아래로',\n        angle45: '45도',\n        toTop: '아래에서 위로',\n        toLeft: '오른쪽에서 왼쪽으로'\n      },\n      addColor: '색상 추가',\n      removeColor: '색상 제거',\n      imageUpload: '이미지 업로드',\n      imagePreview: '이미지 미리보기',\n      clearImage: '이미지 지우기',\n      imageBlur: '흐림',\n      imageBrightness: '밝기',\n      customCss: '사용자 정의 CSS 스타일',\n      customCssPlaceholder: 'CSS 스타일 입력, 예: background: linear-gradient(...)',\n      customCssHelp: '모든 CSS background 속성 지원',\n      reset: '기본값으로 재설정',\n      fileSizeLimit: '이미지 크기 제한: 20MB',\n      invalidImageFormat: '잘못된 이미지 형식',\n      imageTooLarge: '이미지가 너무 큽니다. 20MB 미만의 이미지를 선택하세요'\n    }\n  },\n  translationEngine: '가사 번역 엔진',\n  translationEngineOptions: {\n    none: '닫기',\n    opencc: 'OpenCC 중국어 번체'\n  },\n  themeColor: {\n    title: '가사 테마 색상',\n    presetColors: '미리 설정된 색상',\n    customColor: '사용자 정의 색상',\n    preview: '미리보기 효과',\n    previewText: '가사 효과',\n    colorNames: {\n      'spotify-green': 'Spotify 그린',\n      'apple-blue': '애플 블루',\n      'youtube-red': 'YouTube 레드',\n      orange: '활력 오렌지',\n      purple: '신비 퍼플',\n      pink: '벚꽃 핑크'\n    },\n    tooltips: {\n      openColorPicker: '색상 선택기 열기',\n      closeColorPicker: '색상 선택기 닫기'\n    },\n    placeholder: '#1db954'\n  },\n  shortcutSettings: {\n    title: '단축키 설정',\n    shortcut: '단축키',\n    shortcutDesc: '단축키 사용자 정의',\n    shortcutConflict: '단축키 충돌',\n    inputPlaceholder: '클릭하여 단축키 입력',\n    resetShortcuts: '기본값 복원',\n    disableAll: '모두 비활성화',\n    enableAll: '모두 활성화',\n    togglePlay: '재생/일시정지',\n    prevPlay: '이전 곡',\n    nextPlay: '다음 곡',\n    volumeUp: '볼륨 증가',\n    volumeDown: '볼륨 감소',\n    toggleFavorite: '즐겨찾기/즐겨찾기 취소',\n    toggleWindow: '창 표시/숨기기',\n    scopeGlobal: '전역',\n    scopeApp: '앱 내',\n    enabled: '활성화',\n    disabled: '비활성화',\n    messages: {\n      resetSuccess: '기본 단축키로 복원되었습니다. 저장을 잊지 마세요',\n      conflict: '충돌하는 단축키가 있습니다. 다시 설정하세요',\n      saveSuccess: '단축키 설정이 저장되었습니다',\n      saveError: '단축키 저장 실패, 다시 시도하세요',\n      cancelEdit: '수정이 취소되었습니다',\n      disableAll: '모든 단축키가 비활성화되었습니다. 저장을 잊지 마세요',\n      enableAll: '모든 단축키가 활성화되었습니다. 저장을 잊지 마세요'\n    }\n  },\n  remoteControl: {\n    title: '원격 제어',\n    enable: '원격 제어 활성화',\n    port: '서비스 포트',\n    allowedIps: '허용된 IP 주소',\n    addIp: 'IP 추가',\n    emptyListHint: '빈 목록은 모든 IP 액세스를 허용함을 의미합니다',\n    saveSuccess: '원격 제어 설정이 저장되었습니다',\n    accessInfo: '원격 제어 액세스 주소:'\n  },\n  cookie: {\n    title: 'Cookie 설정',\n    description: '넷이즈 클라우드 뮤직의 Cookie를 입력하세요:',\n    placeholder: '완전한 Cookie를 붙여넣으세요...',\n    help: {\n      format: 'Cookie는 일반적으로 \"MUSIC_U=\"로 시작합니다',\n      source: '브라우저 개발자 도구의 네트워크 요청에서 얻을 수 있습니다',\n      storage: 'Cookie 설정 후 자동으로 로컬 저장소에 저장됩니다'\n    },\n    action: {\n      save: 'Cookie 저장',\n      paste: '붙여넣기',\n      clear: '지우기'\n    },\n    validation: {\n      required: 'Cookie를 입력하세요',\n      format: 'Cookie 형식이 올바르지 않을 수 있습니다. MUSIC_U가 포함되어 있는지 확인하세요'\n    },\n    message: {\n      saveSuccess: 'Cookie 저장 성공',\n      saveError: 'Cookie 저장 실패',\n      pasteSuccess: '붙여넣기 성공',\n      pasteError: '붙여넣기 실패, 수동으로 복사하세요'\n    },\n    info: {\n      length: '현재 길이: {length} 문자'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/songItem.ts",
    "content": "export default {\r\n  menu: {\r\n    play: '재생',\r\n    playNext: '다음에 재생',\r\n    download: '곡 다운로드',\r\n    addToPlaylist: '플레이리스트에 추가',\r\n    favorite: '좋아요',\r\n    unfavorite: '좋아요 취소',\r\n    removeFromPlaylist: '플레이리스트에서 삭제',\r\n    dislike: '싫어요',\r\n    undislike: '싫어요 취소'\r\n  },\r\n  message: {\r\n    downloading: '다운로드 중입니다. 잠시 기다려주세요...',\r\n    downloadFailed: '다운로드 실패',\r\n    downloadQueued: '다운로드 대기열에 추가됨',\r\n    addedToNextPlay: '다음 재생에 추가됨',\r\n    getUrlFailed: '음악 다운로드 주소 가져오기 실패, 로그인 상태를 확인하세요'\r\n  },\r\n  dialog: {\r\n    dislike: {\r\n      title: '알림!',\r\n      content: '이 곡을 싫어한다고 확인하시겠습니까? 다시 들어가면 일일 추천에서 제외됩니다.',\r\n      positiveText: '싫어요',\r\n      negativeText: '취소'\r\n    }\r\n  }\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/ko-KR/user.ts",
    "content": "export default {\r\n  profile: {\r\n    followers: '팔로워',\r\n    following: '팔로잉',\r\n    level: '레벨'\r\n  },\r\n  playlist: {\r\n    created: '생성한 플레이리스트',\r\n    mine: '내가 만든',\r\n    trackCount: '{count}곡',\r\n    playCount: '{count}회 재생'\r\n  },\r\n  tabs: {\r\n    created: '생성',\r\n    favorite: '즐겨찾기',\r\n    album: '앨범'\r\n  },\r\n  ranking: {\r\n    title: '음악 청취 순위',\r\n    playCount: '{count}회'\r\n  },\r\n  follow: {\r\n    title: '팔로잉 목록',\r\n    viewPlaylist: '플레이리스트 보기',\r\n    noFollowings: '팔로잉이 없습니다',\r\n    loadMore: '더 보기',\r\n    noSignature: '이 사람은 게을러서 아무것도 남기지 않았습니다',\r\n    userFollowsTitle: '의 팔로잉',\r\n    myFollowsTitle: '내 팔로잉'\r\n  },\r\n  follower: {\r\n    title: '팔로워 목록',\r\n    noFollowers: '팔로워가 없습니다',\r\n    loadMore: '더 보기',\r\n    userFollowersTitle: '의 팔로워',\r\n    myFollowersTitle: '내 팔로워'\r\n  },\r\n  detail: {\r\n    playlists: '플레이리스트',\r\n    records: '음악 청취 순위',\r\n    noPlaylists: '플레이리스트가 없습니다',\r\n    noRecords: '음악 청취 기록이 없습니다',\r\n    artist: '아티스트',\r\n    noSignature: '이 사람은 게을러서 아무것도 남기지 않았습니다',\r\n    invalidUserId: '사용자 ID가 유효하지 않습니다',\r\n    noRecordPermission: '{name}님이 음악 청취 순위를 보지 못하게 했습니다'\r\n  },\r\n  message: {\r\n    loadFailed: '사용자 페이지 로드 실패',\r\n    deleteSuccess: '삭제 성공',\r\n    deleteFailed: '삭제 실패'\r\n  }\r\n};\r\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/artist.ts",
    "content": "export default {\n  hotSongs: '热门歌曲',\n  albums: '专辑',\n  description: '艺人介绍'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/bilibili.ts",
    "content": "export default {\n  player: {\n    loading: '听书加载中...',\n    retry: '重试',\n    playNow: '立即播放',\n    loadingTitle: '加载中...',\n    totalDuration: '总时长: {duration}',\n    partsList: '分P列表 (共{count}集)',\n    playStarted: '已开始播放',\n    switchingPart: '切换到分P: {part}',\n    preloadingNext: '预加载下一个分P: {part}',\n    playingCurrent: '播放当前选中的分P: {name}',\n    num: '万',\n    errors: {\n      invalidVideoId: '视频ID无效',\n      loadVideoDetailFailed: '获取视频详情失败',\n      loadPartInfoFailed: '无法加载视频分P信息',\n      loadAudioUrlFailed: '获取音频播放地址失败',\n      videoDetailNotLoaded: '视频详情未加载',\n      missingParams: '缺少必要参数',\n      noAvailableAudioUrl: '未找到可用的音频地址',\n      loadPartAudioFailed: '加载分P音频URL失败',\n      audioListEmpty: '音频列表为空，请重试',\n      currentPartNotFound: '未找到当前分P的音频',\n      audioUrlFailed: '获取音频URL失败',\n      playFailed: '播放失败，请重试',\n      getAudioUrlFailed: '获取音频地址失败，请重试',\n      audioNotFound: '未找到对应的音频，请重试',\n      preloadFailed: '预加载下一个分P失败',\n      switchPartFailed: '切换分P时加载音频URL失败'\n    },\n    console: {\n      loadingDetail: '加载B站视频详情',\n      detailData: 'B站视频详情数据',\n      multipleParts: '视频有多个分P，共{count}个',\n      noPartsData: '视频无分P或分P数据为空',\n      loadingAudioSource: '加载音频源',\n      generatedAudioList: '已生成音频列表，共{count}首',\n      getDashAudioUrl: '获取到dash音频URL',\n      getDurlAudioUrl: '获取到durl音频URL',\n      loadingPartAudio: '加载分P音频URL: {part}, cid: {cid}',\n      loadPartAudioFailed: '加载分P音频URL失败: {part}',\n      switchToPart: '切换到分P: {part}',\n      audioNotFoundInList: '未找到对应的音频项',\n      preparingToPlay: '准备播放当前选中的分P: {name}',\n      preloadingNextPart: '预加载下一个分P: {part}',\n      playingSelectedPart: '播放当前选中的分P: {name}，音频URL: {url}',\n      preloadNextFailed: '预加载下一个分P失败'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/common.ts",
    "content": "export default {\n  play: '播放',\n  next: '下一首',\n  previous: '上一首',\n  volume: '音量',\n  settings: '设置',\n  search: '搜索',\n  loading: '加载中...',\n  loadingMore: '加载更多...',\n  alipay: '支付宝',\n  wechat: '微信支付',\n  on: '开启',\n  off: '关闭',\n  show: '显示',\n  hide: '隐藏',\n  confirm: '确认',\n  cancel: '取消',\n  configure: '配置',\n  open: '打开',\n  modify: '修改',\n  success: '操作成功',\n  error: '操作失败',\n  warning: '警告',\n  info: '提示',\n  save: '保存',\n  delete: '删除',\n  refresh: '刷新',\n  retry: '重试',\n  reset: '重置',\n  back: '返回',\n  copySuccess: '已复制到剪贴板',\n  copyFailed: '复制失败',\n  validation: {\n    required: '此项是必填的',\n    invalidInput: '输入无效',\n    selectRequired: '请选择一个选项',\n    numberRange: '请输入 {min} 到 {max} 之间的数字'\n  },\n  viewMore: '查看更多',\n  noMore: '没有更多了',\n  selectAll: '全选',\n  expand: '展开',\n  collapse: '收起',\n  songCount: '{count}首',\n  language: '语言',\n  today: '今天',\n  yesterday: '昨天',\n  tray: {\n    show: '显示',\n    quit: '退出',\n    playPause: '播放/暂停',\n    prev: '上一首',\n    next: '下一首',\n    pause: '暂停',\n    play: '播放',\n    favorite: '收藏'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/comp.ts",
    "content": "export default {\n  installApp: {\n    description: '安装应用程序，获得更好的体验',\n    noPrompt: '不再提示',\n    install: '立即安装',\n    cancel: '暂不安装',\n    download: '下载',\n    downloadFailed: '下载失败',\n    downloadComplete: '下载完成',\n    downloadProblem: '下载遇到问题？去',\n    downloadProblemLinkText: '下载最新版本'\n  },\n  playlistDrawer: {\n    title: '添加到歌单',\n    createPlaylist: '创建新歌单',\n    cancelCreate: '取消创建',\n    create: '创建',\n    playlistName: '歌单名称',\n    privatePlaylist: '私密歌单',\n    publicPlaylist: '公开歌单',\n    createSuccess: '歌单创建成功',\n    createFailed: '歌单创建失败',\n    addSuccess: '歌曲添加成功',\n    addFailed: '歌曲添加失败',\n    private: '私密',\n    public: '公开',\n    count: '首歌曲',\n    loginFirst: '请先登录',\n    getPlaylistFailed: '获取歌单失败',\n    inputPlaylistName: '请输入歌单名称'\n  },\n  update: {\n    title: '发现新版本',\n    currentVersion: '当前版本',\n    cancel: '暂不更新',\n    prepareDownload: '准备下载...',\n    downloading: '下载中...',\n    nowUpdate: '立即更新',\n    downloadFailed: '下载失败，请重试或手动下载',\n    startFailed: '启动下载失败，请重试或手动下载',\n    noDownloadUrl: '未找到适合当前系统的安装包，请手动下载',\n    installConfirmTitle: '安装更新',\n    installConfirmContent: '是否关闭应用并安装更新？',\n    manualInstallTip: '如果关闭应用后没有正常弹出安装程序，请至下载文件夹查找文件并手动打开。',\n    yesInstall: '立即安装',\n    noThanks: '稍后安装',\n    fileLocation: '文件位置',\n    copy: '复制路径',\n    copySuccess: '路径已复制到剪贴板',\n    copyFailed: '复制失败',\n    backgroundDownload: '后台下载'\n  },\n  disclaimer: {\n    title: '使用须知',\n    warning: '本应用为开发测试版本，功能尚不完善，可能存在较多问题和 Bug，仅供学习交流使用。',\n    item1: '本应用仅供个人学习、研究和技术交流使用，请勿用于任何商业用途。',\n    item2: '请在下载后 24 小时内删除，如需长期使用请支持正版音乐服务。',\n    item3: '使用本应用即表示您理解并承担相关风险，开发者不对任何损失负责。',\n    agree: '我已阅读并同意',\n    disagree: '不同意并退出'\n  },\n  donate: {\n    title: '支持开发者',\n    subtitle: '您的支持是我前进的动力',\n    tip: '捐赠完全自愿，不捐赠也可以正常使用所有功能，感谢您的理解与支持！',\n    wechat: '微信',\n    alipay: '支付宝',\n    wechatQR: '微信收款码',\n    alipayQR: '支付宝收款码',\n    scanTip: '请使用手机扫描上方二维码进行捐赠',\n    enterApp: '进入应用',\n    noForce: '不强制捐赠，点击即可进入'\n  },\n  coffee: {\n    title: '请我喝咖啡',\n    alipay: '支付宝',\n    wechat: '微信支付',\n    alipayQR: '支付宝收款码',\n    wechatQR: '微信收款码',\n    coffeeDesc: '一杯咖啡，一份支持',\n    coffeeDescLinkText: '查看更多',\n    groupText: '微信公众号：AlgerMusic',\n    messages: {\n      copySuccess: '已复制到剪贴板'\n    },\n    donateList: '请我喝咖啡'\n  },\n  playlistType: {\n    title: '歌单分类',\n    showAll: '显示全部',\n    hide: '隐藏一些'\n  },\n  recommendAlbum: {\n    title: '最新专辑'\n  },\n  recommendSinger: {\n    title: '每日推荐',\n    songlist: '每日推荐列表'\n  },\n  recommendSonglist: {\n    title: '本周最热音乐'\n  },\n  searchBar: {\n    login: '登录',\n    toLogin: '去登录',\n    logout: '退出登录',\n    set: '设置',\n    theme: '主题',\n    restart: '重启',\n    refresh: '刷新',\n    currentVersion: '当前版本',\n    searchPlaceholder: '搜索点什么吧...',\n    zoom: '页面缩放',\n    zoom100: '标准缩放100%',\n    resetZoom: '点击重置缩放',\n    zoomDefault: '标准缩放'\n  },\n  titleBar: {\n    closeTitle: '请选择关闭方式',\n    minimizeToTray: '最小化到托盘',\n    exitApp: '退出应用',\n    rememberChoice: '记住我的选择',\n    closeApp: '关闭应用'\n  },\n  userPlayList: {\n    title: '{name}的常听'\n  },\n  musicList: {\n    searchSongs: '搜索歌曲',\n    noSearchResults: '没有找到相关歌曲',\n    switchToNormal: '切换到默认布局',\n    switchToCompact: '切换到紧凑布局',\n    playAll: '播放全部',\n    collect: '收藏',\n    collectSuccess: '收藏成功',\n    cancelCollectSuccess: '取消收藏成功',\n    operationFailed: '操作失败',\n    cancelCollect: '取消收藏',\n    addToPlaylist: '添加到播放列表',\n    addToPlaylistSuccess: '添加到播放列表成功',\n    songsAlreadyInPlaylist: '歌曲已存在于播放列表中',\n    historyRecommend: '历史日推',\n    fetchDatesFailed: '获取日期列表失败',\n    fetchSongsFailed: '获取歌曲列表失败',\n    noSongs: '暂无歌曲'\n  },\n  playlist: {\n    import: {\n      button: '歌单导入',\n      title: '歌单导入',\n      description: '支持通过元数据/文字/链接三种方式导入歌单',\n      linkTab: '链接导入',\n      textTab: '文字导入',\n      localTab: '元数据导入',\n      linkPlaceholder: '请输入歌单链接，每行一个',\n      textPlaceholder: '请输入歌曲信息，格式为：歌曲名 歌手名',\n      localPlaceholder: '请输入JSON格式的歌曲元数据',\n      linkTips: '支持的链接来源：',\n      linkTip1: '将歌单分享到微信/微博/QQ后复制链接',\n      linkTip2: '直接复制歌单/个人主页链接',\n      linkTip3: '直接复制文章链接',\n      textTips: '请输入歌曲信息，每行一首歌',\n      textFormat: '格式：歌曲名 歌手名',\n      localTips: '请添加歌曲元数据',\n      localFormat: '格式示例：',\n      songNamePlaceholder: '歌曲名称',\n      artistNamePlaceholder: '艺术家名称',\n      albumNamePlaceholder: '专辑名称',\n      addSongButton: '添加歌曲',\n      addLinkButton: '添加链接',\n      importToStarPlaylist: '导入到我喜欢的音乐',\n      playlistNamePlaceholder: '请输入歌单名称',\n      importButton: '开始导入',\n      emptyLinkWarning: '请输入歌单链接',\n      emptyTextWarning: '请输入歌曲信息',\n      emptyLocalWarning: '请输入歌曲元数据',\n      invalidJsonFormat: 'JSON格式不正确',\n      importSuccess: '导入任务创建成功',\n      importFailed: '导入失败',\n      importStatus: '导入状态',\n      refresh: '刷新',\n      taskId: '任务ID',\n      status: '状态',\n      successCount: '成功数量',\n      failReason: '失败原因',\n      unknownError: '未知错误',\n      statusPending: '等待处理',\n      statusProcessing: '处理中',\n      statusSuccess: '导入成功',\n      statusFailed: '导入失败',\n      statusUnknown: '未知状态',\n      taskList: '任务列表',\n      taskListTitle: '导入任务列表',\n      action: '操作',\n      select: '选择',\n      fetchTaskListFailed: '获取任务列表失败',\n      noTasks: '暂无导入任务',\n      clearTasks: '清除任务',\n      clearTasksConfirmTitle: '确认清除',\n      clearTasksConfirmContent: '确定要清除所有导入任务记录吗？此操作不可恢复。',\n      confirm: '确认',\n      cancel: '取消',\n      clearTasksSuccess: '任务列表已清除',\n      clearTasksFailed: '清除任务列表失败'\n    }\n  },\n  settings: '设置',\n  user: '用户',\n  toplist: '排行榜',\n  history: '收藏历史',\n  list: '歌单',\n  mv: 'MV',\n  home: '首页',\n  search: '搜索'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/donation.ts",
    "content": "export default {\n  description: '您的捐赠将用于支持开发和维护工作，包括但不限于服务器维护、域名续费等。',\n  message: '留言时可留下您的邮箱或 github名称。',\n  refresh: '刷新列表',\n  toDonateList: '请我喝咖啡',\n  noMessage: '暂无留言',\n  title: '捐赠列表'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/download.ts",
    "content": "export default {\n  title: '下载管理',\n  localMusic: '本地音乐',\n  count: '共 {count} 首歌曲',\n  clearAll: '清空记录',\n  settings: '设置',\n  tabs: {\n    downloading: '下载中',\n    downloaded: '已下载'\n  },\n  empty: {\n    noTasks: '暂无下载任务',\n    noDownloaded: '暂无已下载歌曲'\n  },\n  progress: {\n    total: '总进度: {progress}%'\n  },\n  status: {\n    downloading: '下载中',\n    completed: '已完成',\n    failed: '失败',\n    unknown: '未知'\n  },\n  artist: {\n    unknown: '未知歌手'\n  },\n  delete: {\n    title: '删除确认',\n    message: '确定要删除歌曲 \"{filename}\" 吗？此操作不可恢复。',\n    confirm: '确定删除',\n    cancel: '取消',\n    success: '删除成功',\n    failed: '删除失败',\n    fileNotFound: '文件不存在或已被移动，已从记录中移除',\n    recordRemoved: '文件删除失败，但已从记录中移除'\n  },\n  clear: {\n    title: '清空下载记录',\n    message: '确定要清空所有下载记录吗？此操作不会删除已下载的音乐文件，但将清空所有记录。',\n    confirm: '确定清空',\n    cancel: '取消',\n    success: '下载记录已清空'\n  },\n  message: {\n    downloadComplete: '{filename} 下载完成',\n    downloadFailed: '{filename} 下载失败: {error}'\n  },\n  loading: '加载中...',\n  playStarted: '开始播放: {name}',\n  playFailed: '播放失败: {name}',\n  path: {\n    copied: '路径已复制到剪贴板',\n    copyFailed: '复制路径失败'\n  },\n  settingsPanel: {\n    title: '下载设置',\n    path: '下载位置',\n    pathDesc: '设置音乐文件下载保存的位置',\n    pathPlaceholder: '请选择下载路径',\n    noPathSelected: '请先选择下载路径',\n    select: '选择文件夹',\n    open: '打开文件夹',\n    fileFormat: '文件名格式',\n    fileFormatDesc: '设置下载音乐时的文件命名格式',\n    customFormat: '自定义格式',\n    separator: '分隔符',\n    separators: {\n      dash: '空格-空格',\n      underscore: '下划线',\n      space: '空格'\n    },\n    dragToArrange: '拖动排序或使用箭头按钮调整顺序：',\n    formatVariables: '可用变量',\n    preview: '预览效果：',\n    saveSuccess: '下载设置已保存',\n    presets: {\n      songArtist: '歌曲名 - 歌手名',\n      artistSong: '歌手名 - 歌曲名',\n      songOnly: '仅歌曲名'\n    },\n    components: {\n      songName: '歌曲名',\n      artistName: '歌手名',\n      albumName: '专辑名'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/favorite.ts",
    "content": "export default {\n  title: '我的收藏',\n  count: '共 {count} 首',\n  batchDownload: '批量下载',\n  download: '下载 ({count})',\n  emptyTip: '还没有收藏歌曲',\n  downloadSuccess: '下载完成',\n  downloadFailed: '下载失败',\n  downloading: '正在下载中，请稍候...',\n  selectSongsFirst: '请先选择要下载的歌曲',\n  descending: '降',\n  ascending: '升'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/history.ts",
    "content": "export default {\n  title: '播放历史',\n  heatmapTitle: '热力图',\n  playCount: '{count}',\n  getHistoryFailed: '获取历史记录失败',\n  categoryTabs: {\n    songs: '歌曲',\n    playlists: '歌单',\n    albums: '专辑'\n  },\n  tabs: {\n    all: '全部记录',\n    local: '本地记录',\n    cloud: '云端记录'\n  },\n  getCloudRecordFailed: '获取云端记录失败',\n  needLogin: '请使用cookie登录以查看云端记录',\n  merging: '正在合并记录...',\n  noDescription: '暂无描述',\n  noData: '暂无记录',\n  heatmap: {\n    title: '播放热力图',\n    loading: '正在加载数据...',\n    unit: '次播放',\n    footerText: '鼠标悬停查看详细信息',\n    playCount: '播放 {count} 次',\n    topSongs: '当天热门歌曲',\n    times: '次',\n    totalPlays: '总播放次数',\n    activeDays: '活跃天数',\n    noData: '暂无播放记录',\n    colorTheme: '配色方案',\n    colors: {\n      green: '绿色',\n      blue: '蓝色',\n      orange: '橙色',\n      purple: '紫色',\n      red: '红色'\n    },\n    mostPlayedSong: '播放最多的歌曲',\n    mostActiveDay: '最活跃的一天',\n    latestNightSong: '最晚播放的歌曲'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/login.ts",
    "content": "export default {\n  title: {\n    qr: '扫码登录',\n    phone: '手机号登录',\n    cookie: 'Cookie登录',\n    uid: 'UID登录'\n  },\n  qrTip: '使用网易云APP扫码登录',\n  phoneTip: '使用网易云账号登录',\n  tokenTip: '输入有效的网易云音乐Cookie即可登录',\n  uidTip: '输入用户ID快速登录',\n  placeholder: {\n    phone: '手机号',\n    password: '密码',\n    cookie: '请输入网易云音乐Cookie（token）',\n    uid: '请输入用户ID（UID）'\n  },\n  button: {\n    login: '登录',\n    switchToQr: '扫码登录',\n    switchToPhone: '手机号登录',\n    switchToToken: '使用Cookie登录',\n    switchToUid: 'UID登录',\n    backToQr: '返回二维码登录',\n    cookieLogin: 'Cookie登录',\n    autoGetCookie: '自动获取Cookie',\n    refresh: '点击刷新',\n    refreshing: '刷新中...',\n    refreshQr: '刷新二维码'\n  },\n  message: {\n    loginSuccess: '登录成功',\n    loginFailed: '登录失败',\n    tokenLoginSuccess: 'Cookie登录成功',\n    uidLoginSuccess: 'UID登录成功',\n    loadError: '加载登录信息时出错',\n    qrCheckError: '检查二维码状态时出错',\n    tokenRequired: '请输入Cookie',\n    tokenInvalid: 'Cookie无效，请检查后重试',\n    uidRequired: '请输入用户ID',\n    uidInvalid: '用户ID无效或用户不存在',\n    uidLoginFailed: 'UID登录失败，请检查用户ID是否正确',\n    phoneRequired: '请输入手机号',\n    passwordRequired: '请输入密码',\n    phoneLoginFailed: '手机号登录失败，请检查手机号和密码是否正确',\n    autoGetCookieSuccess: '自动获取Cookie成功',\n    autoGetCookieFailed: '自动获取Cookie失败',\n    autoGetCookieTip: '将打开网易云音乐登录页面，请完成登录后关闭窗口',\n    qrCheckFailed: '检查二维码状态失败，请刷新重试',\n    qrLoading: '正在加载二维码...',\n    qrExpired: '二维码已过期，请点击刷新',\n    qrExpiredShort: '二维码已过期',\n    qrExpiredWarning: '二维码已过期，请点击刷新获取新的二维码',\n    qrScanned: '已扫码，请在手机上确认登录',\n    qrScannedShort: '已扫码',\n    qrScannedInfo: '已扫码，请在手机上确认登录',\n    qrConfirmed: '登录成功，正在跳转...',\n    qrGenerating: '正在生成二维码...'\n  },\n  qrTitle: '扫码登录网易云音乐',\n  uidWarning: '注意：UID登录仅用于查看用户公开信息，无法访问需要登录权限的功能'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/player.ts",
    "content": "export default {\n  nowPlaying: '正在播放',\n  playlist: '播放列表',\n  lyrics: '歌词',\n  previous: '上一个',\n  play: '播放',\n  pause: '暂停',\n  next: '下一个',\n  volumeUp: '音量增加',\n  volumeDown: '音量减少',\n  mute: '静音',\n  unmute: '取消静音',\n  songNum: '歌曲总数：{num}',\n  addCorrection: '提前 {num} 秒',\n  subtractCorrection: '延迟 {num} 秒',\n  playFailed: '当前歌曲播放失败，播放下一首',\n  parseFailedPlayNext: '歌曲解析失败，播放下一首',\n  consecutiveFailsError: '播放遇到错误，可能是网络波动或解析源失效，请切换播放列表或稍后重试',\n  playMode: {\n    sequence: '顺序播放',\n    loop: '单曲循环',\n    random: '随机播放'\n  },\n  fullscreen: {\n    enter: '全屏',\n    exit: '退出全屏'\n  },\n  close: '关闭',\n  modeHint: {\n    single: '单曲循环',\n    list: '自动播放下一个'\n  },\n  lrc: {\n    noLrc: '暂无歌词, 请欣赏',\n    noAutoScroll: '本歌词不支持自动滚动'\n  },\n  reparse: {\n    title: '选择解析音源',\n    desc: '点击音源直接进行解析，下次播放此歌曲时将使用所选音源',\n    success: '重新解析成功',\n    failed: '重新解析失败',\n    warning: '请选择一个音源',\n    bilibiliNotSupported: 'B站视频不支持重新解析',\n    processing: '解析中...',\n    clear: '清除自定义音源',\n    customApiFailed: '自定义API解析失败，正在尝试使用内置音源...',\n    customApiError: '自定义API请求出错，正在尝试使用内置音源...'\n  },\n  playBar: {\n    expand: '展开歌词',\n    collapse: '收起歌词',\n    like: '喜欢',\n    lyric: '歌词',\n    noSongPlaying: '没有正在播放的歌曲',\n    eq: '均衡器',\n    playList: '播放列表',\n    reparse: '重新解析',\n    playMode: {\n      sequence: '顺序播放',\n      loop: '循环播放',\n      random: '随机播放'\n    },\n    play: '开始播放',\n    pause: '暂停播放',\n    prev: '上一首',\n    next: '下一首',\n    volume: '音量',\n    favorite: '已收藏{name}',\n    unFavorite: '已取消收藏{name}',\n    miniPlayBar: '迷你播放栏',\n    playbackSpeed: '播放速度',\n    advancedControls: '更多设置',\n    intelligenceMode: {\n      title: '心动模式',\n      needCookieLogin: '请使用 Cookie 方式登录后使用心动模式',\n      noFavoritePlaylist: '未找到我喜欢的音乐歌单',\n      noLikedSongs: '您还没有喜欢的歌曲',\n      loading: '正在加载心动模式',\n      success: '已加载 {count} 首歌曲',\n      failed: '获取心动模式列表失败',\n      error: '心动模式播放出错'\n    }\n  },\n  eq: {\n    title: '均衡器',\n    reset: '重置',\n    on: '开启',\n    off: '关闭',\n    bass: '低音',\n    midrange: '中音',\n    treble: '高音',\n    presets: {\n      flat: '平坦',\n      pop: '流行',\n      rock: '摇滚',\n      classical: '古典',\n      jazz: '爵士',\n      electronic: '电子',\n      hiphop: '嘻哈',\n      rb: 'R&B',\n      metal: '金属',\n      vocal: '人声',\n      dance: '舞曲',\n      acoustic: '原声',\n      custom: '自定义'\n    }\n  },\n  // 播放器设置\n  settings: {\n    title: '播放设置',\n    playbackSpeed: '播放速度'\n  },\n  // 定时关闭功能相关\n  sleepTimer: {\n    title: '定时关闭',\n    cancel: '取消定时',\n    timeMode: '按时间关闭',\n    songsMode: '按歌曲数关闭',\n    playlistEnd: '播放完列表后关闭',\n    afterPlaylist: '播放完列表后关闭',\n    activeUntilEnd: '播放至列表结束',\n    minutes: '分钟',\n    hours: '小时',\n    songs: '首歌',\n    set: '设置',\n    timerSetSuccess: '已设置{minutes}分钟后关闭',\n    songsSetSuccess: '已设置播放{songs}首歌后关闭',\n    playlistEndSetSuccess: '已设置播放完列表后关闭',\n    timerCancelled: '已取消定时关闭',\n    timerEnded: '定时关闭已触发',\n    playbackStopped: '音乐播放已停止',\n    minutesRemaining: '剩余{minutes}分钟',\n    songsRemaining: '剩余{count}首歌'\n  },\n  playList: {\n    clearAll: '清空播放列表',\n    alreadyEmpty: '播放列表已经为空',\n    cleared: '已清空播放列表',\n    empty: '播放列表为空',\n    clearConfirmTitle: '清空播放列表',\n    clearConfirmContent: '这将清空所有播放列表中的歌曲并停止当前播放。是否继续？'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/search.ts",
    "content": "export default {\n  title: {\n    hotSearch: '热搜列表',\n    searchList: '搜索列表',\n    searchHistory: '搜索历史'\n  },\n  button: {\n    clear: '清空',\n    back: '返回',\n    playAll: '播放列表'\n  },\n  loading: {\n    more: '加载中...',\n    failed: '搜索失败',\n    searching: '搜索中...'\n  },\n  noMore: '没有更多了',\n  error: {\n    searchFailed: '搜索失败'\n  },\n  search: {\n    single: '单曲',\n    album: '专辑',\n    playlist: '歌单',\n    mv: 'MV',\n    bilibili: 'B站'\n  },\n  history: '搜索历史',\n  hot: '热门搜索',\n  suggestions: '搜索建议'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/settings.ts",
    "content": "export default {\n  theme: '主题',\n  language: '语言',\n  regard: '关于',\n  logout: '退出登录',\n  sections: {\n    basic: '基础设置',\n    playback: '播放设置',\n    application: '应用设置',\n    network: '网络设置',\n    system: '系统管理',\n    donation: '捐赠支持',\n    about: '关于'\n  },\n  basic: {\n    themeMode: '主题模式',\n    themeModeDesc: '切换日间/夜间主题',\n    autoTheme: '跟随系统',\n    manualTheme: '手动切换',\n    language: '语言设置',\n    languageDesc: '切换显示语言',\n    tokenManagement: 'Cookie管理',\n    tokenManagementDesc: '管理网易云音乐登录Cookie',\n    tokenStatus: '当前Cookie状态',\n    tokenSet: '已设置',\n    tokenNotSet: '未设置',\n    setToken: '设置Cookie',\n    modifyToken: '修改Cookie',\n    clearToken: '清除Cookie',\n    font: '字体设置',\n    fontDesc: '选择字体，优先使用排在前面的字体',\n    fontScope: {\n      global: '全局',\n      lyric: '仅歌词'\n    },\n    animation: '动画速度',\n    animationDesc: '是否开启动画',\n    animationSpeed: {\n      slow: '极慢',\n      normal: '正常',\n      fast: '极快'\n    },\n    fontPreview: {\n      title: '字体预览',\n      chinese: '中文',\n      english: 'English',\n      japanese: '日本語',\n      korean: '한국어',\n      chineseText: '静夜思 床前明月光 疑是地上霜',\n      englishText: 'The quick brown fox jumps over the lazy dog',\n      japaneseText: 'あいうえお かきくけこ さしすせそ',\n      koreanText: '가나다라마 바사아자차 카타파하'\n    },\n    gpuAcceleration: 'GPU加速',\n    gpuAccelerationDesc: '启用或禁用硬件加速，可以提高渲染性能但可能会增加GPU负载',\n    gpuAccelerationRestart: '更改GPU加速设置需要重启应用后生效',\n    gpuAccelerationChangeSuccess: 'GPU加速设置已更新，重启应用后生效',\n    gpuAccelerationChangeError: 'GPU加速设置更新失败',\n    tabletMode: '平板模式',\n    tabletModeDesc: '启用后将在移动设备上使用PC样式界面，适合平板等大屏设备'\n  },\n  playback: {\n    quality: '音质设置',\n    qualityDesc: '选择音乐播放音质（网易云VIP）',\n    qualityOptions: {\n      standard: '标准',\n      higher: '较高',\n      exhigh: '极高',\n      lossless: '无损',\n      hires: 'Hi-Res',\n      jyeffect: '高清环绕声',\n      sky: '沉浸环绕声',\n      dolby: '杜比全景声',\n      jymaster: '超清母带'\n    },\n    musicSources: '音源设置',\n    musicSourcesDesc: '选择音乐解析使用的音源平台',\n    musicSourcesWarning: '至少需要选择一个音源平台',\n    musicUnblockEnable: '启用音乐解析',\n    musicUnblockEnableDesc: '开启后将尝试解析无法播放的音乐',\n    configureMusicSources: '配置音源',\n    selectedMusicSources: '已选音源：',\n    noMusicSources: '未选择音源',\n    gdmusicInfo: 'GD音乐台可自动解析多个平台音源，自动选择最佳结果',\n    autoPlay: '自动播放',\n    autoPlayDesc: '重新打开应用时是否自动继续播放',\n    showStatusBar: '是否显示状态栏控制功能',\n    showStatusBarContent: '可以在您的mac状态栏显示音乐控制功能(重启后生效)',\n\n    fallbackParser: 'GD音乐台(music.gdstudio.xyz)设置',\n    fallbackParserDesc:\n      'GD音乐台将自动尝试多个音乐平台进行解析，无需额外配置。优先级高于其他解析方式，但是请求可能较慢。感谢（music.gdstudio.xyz）\\n',\n    parserGD: 'GD 音乐台 (内置)',\n    parserCustom: '自定义 API',\n\n    // 音源标签\n    sourceLabels: {\n      migu: '咪咕音乐',\n      kugou: '酷狗音乐',\n      pyncmd: '网易云（内置）',\n      bilibili: 'Bilibili',\n      gdmusic: 'GD音乐台',\n      custom: '自定义 API'\n    },\n\n    // 自定义API相关的提示\n    customApi: {\n      sectionTitle: '自定义 API 设置',\n      importConfig: '导入 JSON 配置',\n      currentSource: '当前音源',\n      notImported: '尚未导入自定义音源。',\n      importSuccess: '成功导入音源: {name}',\n      importFailed: '导入失败: {message}',\n      enableHint: '请先导入 JSON 配置文件才能启用',\n      status: {\n        imported: '已导入自定义音源',\n        notImported: '未导入'\n      }\n    },\n    lxMusic: {\n      tabs: {\n        sources: '音源选择',\n        lxMusic: '落雪音源',\n        customApi: '自定义API'\n      },\n      scripts: {\n        title: '已导入的音源脚本',\n        importLocal: '本地导入',\n        importOnline: '在线导入',\n        urlPlaceholder: '输入落雪音源脚本 URL',\n        importBtn: '导入',\n        empty: '暂无已导入的落雪音源',\n        notConfigured: '未配置 (请去落雪音源Tab配置)',\n        importHint: '导入兼容的自定义 API 插件以扩展音源',\n        noScriptWarning: '请先导入落雪音源脚本',\n        noSelectionWarning: '请先选择一个落雪音源',\n        notFound: '音源不存在',\n        switched: '已切换到音源: {name}',\n        deleted: '已删除音源: {name}',\n        enterUrl: '请输入脚本 URL',\n        invalidUrl: '无效的 URL 格式',\n        invalidScript: '无效的落雪音源脚本，未找到 globalThis.lx 相关代码',\n        nameRequired: '名称不能为空',\n        renameSuccess: '重命名成功'\n      }\n    }\n  },\n  application: {\n    closeAction: '关闭行为',\n    closeActionDesc: '选择关闭窗口时的行为',\n    closeOptions: {\n      ask: '每次询问',\n      minimize: '最小化到托盘',\n      close: '直接退出'\n    },\n    shortcut: '快捷键设置',\n    shortcutDesc: '自定义全局快捷键',\n    download: '下载管理',\n    downloadDesc: '是否始终显示下载列表按钮',\n    unlimitedDownload: '无限制下载',\n    unlimitedDownloadDesc: '开启后将无限制下载音乐（可能出现下载失败的情况）, 默认限制 300 首',\n    downloadPath: '下载目录',\n    downloadPathDesc: '选择音乐文件的下载位置',\n    remoteControl: '远程控制',\n    remoteControlDesc: '设置远程控制功能'\n  },\n  network: {\n    apiPort: '音乐API端口',\n    apiPortDesc: '修改后需要重启应用',\n    proxy: '代理设置',\n    proxyDesc: '无法访问音乐时可以开启代理',\n    proxyHost: '代理地址',\n    proxyHostPlaceholder: '请输入代理地址',\n    proxyPort: '代理端口',\n    proxyPortPlaceholder: '请输入代理端口',\n    realIP: 'realIP设置',\n    realIPDesc: '由于限制,此项目在国外使用会受到限制可使用realIP参数,传进国内IP解决',\n    messages: {\n      proxySuccess: '代理设置已保存，重启应用后生效',\n      proxyError: '请检查输入是否正确',\n      realIPSuccess: '真实IP设置已保存',\n      realIPError: '请输入有效的IP地址'\n    }\n  },\n  system: {\n    cache: '缓存管理',\n    cacheDesc: '清除缓存',\n    cacheClearTitle: '请选择要清除的缓存类型：',\n    cacheTypes: {\n      history: {\n        label: '播放历史',\n        description: '清除播放过的歌曲记录'\n      },\n      favorite: {\n        label: '收藏记录',\n        description: '清除本地收藏的歌曲记录(不会影响云端收藏)'\n      },\n      user: {\n        label: '用户数据',\n        description: '清除登录信息和用户相关数据'\n      },\n      settings: {\n        label: '应用设置',\n        description: '清除应用的所有自定义设置'\n      },\n      downloads: {\n        label: '下载记录',\n        description: '清除下载历史记录(不会删除已下载的文件)'\n      },\n      resources: {\n        label: '音乐资源',\n        description: '清除已加载的音乐文件、歌词等资源缓存'\n      },\n      lyrics: {\n        label: '歌词资源',\n        description: '清除已加载的歌词资源缓存'\n      }\n    },\n    restart: '重启',\n    restartDesc: '重启应用',\n    messages: {\n      clearSuccess: '清除成功，部分设置在重启后生效'\n    }\n  },\n  about: {\n    version: '版本',\n    checkUpdate: '检查更新',\n    checking: '检查中...',\n    latest: '当前已是最新版本',\n    hasUpdate: '发现新版本',\n    gotoUpdate: '前往更新',\n    gotoGithub: '前往 Github',\n    author: '作者',\n    authorDesc: 'algerkong 点个star🌟呗',\n    messages: {\n      checkError: '检查更新失败，请稍后重试'\n    }\n  },\n  validation: {\n    selectProxyProtocol: '请选择代理协议',\n    proxyHost: '请输入代理地址',\n    portNumber: '请输入有效的端口号(1-65535)'\n  },\n  lyricSettings: {\n    title: '歌词设置',\n    tabs: {\n      display: '显示',\n      interface: '界面',\n      typography: '文字',\n      background: '背景',\n      mobile: '移动端'\n    },\n    pureMode: '纯净模式',\n    hideCover: '隐藏封面',\n    centerDisplay: '居中显示',\n    showTranslation: '显示翻译',\n    hideLyrics: '隐藏歌词',\n    hidePlayBar: '隐藏播放栏',\n    hideMiniPlayBar: '隐藏迷你播放栏',\n    showMiniPlayBar: '显示迷你播放栏',\n    backgroundTheme: '背景主题',\n    themeOptions: {\n      default: '默认',\n      light: '亮色',\n      dark: '暗色'\n    },\n    fontSize: '字体大小',\n    fontSizeMarks: {\n      small: '小',\n      medium: '中',\n      large: '大'\n    },\n    fontWeight: '字体粗细',\n    fontWeightMarks: {\n      thin: '细',\n      normal: '常规',\n      bold: '粗'\n    },\n    letterSpacing: '字间距',\n    letterSpacingMarks: {\n      compact: '紧凑',\n      default: '默认',\n      loose: '宽松'\n    },\n    lineHeight: '行高',\n    lineHeightMarks: {\n      compact: '紧凑',\n      default: '默认',\n      loose: '宽松'\n    },\n    contentWidth: '内容区宽度',\n    mobileLayout: '移动端布局',\n    layoutOptions: {\n      default: '默认',\n      ios: 'iOS风格',\n      android: '安卓风格'\n    },\n    mobileCoverStyle: '封面样式',\n    coverOptions: {\n      record: '唱片',\n      square: '方形',\n      full: '全屏'\n    },\n    lyricLines: '歌词行数',\n    mobileUnavailable: '此设置仅在移动端可用',\n    // 背景设置\n    background: {\n      useCustomBackground: '使用自定义背景',\n      backgroundMode: '背景模式',\n      modeOptions: {\n        solid: '纯色',\n        gradient: '渐变',\n        image: '图片',\n        css: 'CSS'\n      },\n      solidColor: '选择颜色',\n      presetColors: '预设颜色',\n      customColor: '自定义颜色',\n      gradientEditor: '渐变编辑器',\n      gradientColors: '渐变颜色',\n      gradientDirection: '渐变方向',\n      directionOptions: {\n        toBottom: '上到下',\n        toRight: '左到右',\n        toBottomRight: '左上到右下',\n        angle45: '45度',\n        toTop: '下到上',\n        toLeft: '右到左'\n      },\n      addColor: '添加颜色',\n      removeColor: '移除颜色',\n      imageUpload: '上传图片',\n      imagePreview: '图片预览',\n      clearImage: '清除图片',\n      imageBlur: '模糊度',\n      imageBrightness: '明暗度',\n      customCss: '自定义 CSS 样式',\n      customCssPlaceholder: '输入 CSS 样式,如: background: linear-gradient(...)',\n      customCssHelp: '支持任意 CSS background 属性',\n      reset: '重置为默认',\n      fileSizeLimit: '图片大小限制: 20MB',\n      invalidImageFormat: '无效的图片格式',\n      imageTooLarge: '图片过大,请选择小于 20MB 的图片'\n    }\n  },\n  translationEngine: '歌詞翻譯引擎',\n  translationEngineOptions: {\n    none: '关闭',\n    opencc: 'OpenCC 繁化'\n  },\n  themeColor: {\n    title: '歌词主题色',\n    presetColors: '预设颜色',\n    customColor: '自定义颜色',\n    preview: '预览效果',\n    previewText: '歌词效果',\n    colorNames: {\n      'spotify-green': 'Spotify 绿',\n      'apple-blue': '苹果蓝',\n      'youtube-red': 'YouTube 红',\n      orange: '活力橙',\n      purple: '神秘紫',\n      pink: '樱花粉'\n    },\n    tooltips: {\n      openColorPicker: '打开色板',\n      closeColorPicker: '关闭色板'\n    },\n    placeholder: '#1db954'\n  },\n  shortcutSettings: {\n    title: '快捷键设置',\n    shortcut: '快捷键',\n    shortcutDesc: '自定义快捷键',\n    shortcutConflict: '快捷键冲突',\n    inputPlaceholder: '点击输入快捷键',\n    resetShortcuts: '恢复默认',\n    disableAll: '全部禁用',\n    enableAll: '全部启用',\n    togglePlay: '播放/暂停',\n    prevPlay: '上一首',\n    nextPlay: '下一首',\n    volumeUp: '音量增加',\n    volumeDown: '音量减少',\n    toggleFavorite: '收藏/取消收藏',\n    toggleWindow: '显示/隐藏窗口',\n    scopeGlobal: '全局',\n    scopeApp: '应用内',\n    enabled: '启用',\n    disabled: '禁用',\n    messages: {\n      resetSuccess: '已恢复默认快捷键，请记得保存',\n      conflict: '存在冲突的快捷键，请重新设置',\n      saveSuccess: '快捷键设置已保存',\n      saveError: '保存快捷键失败，请重试',\n      cancelEdit: '已取消修改',\n      disableAll: '已禁用所有快捷键，请记得保存',\n      enableAll: '已启用所有快捷键，请记得保存'\n    }\n  },\n  remoteControl: {\n    title: '远程控制',\n    enable: '启用远程控制',\n    port: '服务端口',\n    allowedIps: '允许的IP地址',\n    addIp: '添加IP',\n    emptyListHint: '空列表表示允许所有IP访问',\n    saveSuccess: '远程控制设置已保存',\n    accessInfo: '远程控制访问地址:'\n  },\n  cookie: {\n    title: 'Cookie设置',\n    description: '请输入网易云音乐的Cookie：',\n    placeholder: '请粘贴完整的Cookie...',\n    help: {\n      format: 'Cookie通常以 \"MUSIC_U=\" 开头',\n      source: '可以从浏览器开发者工具的网络请求中获取',\n      storage: 'Cookie设置后将自动保存到本地存储'\n    },\n    action: {\n      save: '保存Cookie',\n      paste: '粘贴',\n      clear: '清空'\n    },\n    validation: {\n      required: '请输入Cookie',\n      format: 'Cookie格式可能不正确，请检查是否包含MUSIC_U'\n    },\n    message: {\n      saveSuccess: 'Cookie保存成功',\n      saveError: 'Cookie保存失败',\n      pasteSuccess: '粘贴成功',\n      pasteError: '粘贴失败，请手动复制'\n    },\n    info: {\n      length: '当前长度：{length} 字符'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/songItem.ts",
    "content": "export default {\n  menu: {\n    play: '播放',\n    playNext: '下一首播放',\n    download: '下载歌曲',\n    addToPlaylist: '添加到歌单',\n    favorite: '喜欢',\n    unfavorite: '取消喜欢',\n    removeFromPlaylist: '从歌单中删除',\n    dislike: '不喜欢',\n    undislike: '取消不喜欢'\n  },\n  message: {\n    downloading: '正在下载中，请稍候...',\n    downloadFailed: '下载失败',\n    downloadQueued: '已加入下载队列',\n    addedToNextPlay: '已添加到下一首播放',\n    getUrlFailed: '获取音乐下载地址失败，请检查是否登录'\n  },\n  dialog: {\n    dislike: {\n      title: '提示！',\n      content: '确认不喜欢这首歌吗？再次进入将从每日推荐中排除。',\n      positiveText: '不喜欢',\n      negativeText: '取消'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-CN/user.ts",
    "content": "export default {\n  profile: {\n    followers: '粉丝',\n    following: '关注',\n    level: '等级'\n  },\n  playlist: {\n    created: '创建的歌单',\n    mine: '我创建的',\n    trackCount: '{count}首',\n    playCount: '播放{count}次'\n  },\n  tabs: {\n    created: '创建',\n    favorite: '收藏',\n    album: '专辑'\n  },\n  ranking: {\n    title: '听歌排行',\n    playCount: '{count}次'\n  },\n  follow: {\n    title: '关注列表',\n    viewPlaylist: '查看歌单',\n    noFollowings: '暂无关注',\n    loadMore: '加载更多',\n    noSignature: '这个家伙很懒，什么都没留下',\n    userFollowsTitle: '的关注',\n    myFollowsTitle: '我的关注'\n  },\n  follower: {\n    title: '粉丝列表',\n    noFollowers: '暂无粉丝',\n    loadMore: '加载更多',\n    userFollowersTitle: '的粉丝',\n    myFollowersTitle: '我的粉丝'\n  },\n  detail: {\n    playlists: '歌单',\n    records: '听歌排行',\n    noPlaylists: '暂无歌单',\n    noRecords: '暂无听歌记录',\n    artist: '歌手',\n    noSignature: '这个人很懒，什么都没留下',\n    invalidUserId: '用户ID无效',\n    noRecordPermission: '{name}不让你看听歌排行'\n  },\n  message: {\n    loadFailed: '加载用户页面失败',\n    deleteSuccess: '删除成功',\n    deleteFailed: '删除失败'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/artist.ts",
    "content": "export default {\n  hotSongs: '熱門歌曲',\n  albums: '專輯',\n  description: '藝人介紹'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/bilibili.ts",
    "content": "export default {\n  player: {\n    loading: '聽書載入中...',\n    retry: '重試',\n    playNow: '立即播放',\n    loadingTitle: '載入中...',\n    totalDuration: '總時長: {duration}',\n    partsList: '分P列表 (共{count}集)',\n    playStarted: '已開始播放',\n    switchingPart: '切換到分P: {part}',\n    preloadingNext: '預載入下一個分P: {part}',\n    playingCurrent: '播放當前選中的分P: {name}',\n    num: '萬',\n    errors: {\n      invalidVideoId: '影片ID無效',\n      loadVideoDetailFailed: '獲取影片詳情失敗',\n      loadPartInfoFailed: '無法載入影片分P資訊',\n      loadAudioUrlFailed: '獲取音訊播放地址失敗',\n      videoDetailNotLoaded: '影片詳情未載入',\n      missingParams: '缺少必要參數',\n      noAvailableAudioUrl: '未找到可用的音訊地址',\n      loadPartAudioFailed: '載入分P音訊URL失敗',\n      audioListEmpty: '音訊列表為空，請重試',\n      currentPartNotFound: '未找到當前分P的音訊',\n      audioUrlFailed: '獲取音訊URL失敗',\n      playFailed: '播放失敗，請重試',\n      getAudioUrlFailed: '獲取音訊地址失敗，請重試',\n      audioNotFound: '未找到對應的音訊，請重試',\n      preloadFailed: '預載入下一個分P失敗',\n      switchPartFailed: '切換分P時載入音訊URL失敗'\n    },\n    console: {\n      loadingDetail: '載入B站影片詳情',\n      detailData: 'B站影片詳情資料',\n      multipleParts: '影片有多個分P，共{count}個',\n      noPartsData: '影片無分P或分P資料為空',\n      loadingAudioSource: '載入音訊來源',\n      generatedAudioList: '已生成音訊列表，共{count}首',\n      getDashAudioUrl: '獲取到dash音訊URL',\n      getDurlAudioUrl: '獲取到durl音訊URL',\n      loadingPartAudio: '載入分P音訊URL: {part}, cid: {cid}',\n      loadPartAudioFailed: '載入分P音訊URL失敗: {part}',\n      switchToPart: '切換到分P: {part}',\n      audioNotFoundInList: '未找到對應的音訊項目',\n      preparingToPlay: '準備播放當前選中的分P: {name}',\n      preloadingNextPart: '預載入下一個分P: {part}',\n      playingSelectedPart: '播放當前選中的分P: {name}，音訊URL: {url}',\n      preloadNextFailed: '預載入下一個分P失敗'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/common.ts",
    "content": "export default {\n  play: '播放',\n  next: '下一首',\n  previous: '上一首',\n  volume: '音量',\n  settings: '設定',\n  search: '搜尋',\n  loading: '載入中...',\n  loadingMore: '載入更多...',\n  alipay: '支付寶',\n  wechat: '微信支付',\n  on: '開啟',\n  off: '關閉',\n  show: '顯示',\n  hide: '隱藏',\n  confirm: '確認',\n  cancel: '取消',\n  configure: '設定',\n  open: '開啟',\n  modify: '修改',\n  success: '操作成功',\n  error: '操作失敗',\n  warning: '警告',\n  info: '提示',\n  save: '儲存',\n  delete: '刪除',\n  refresh: '重新整理',\n  retry: '重試',\n  reset: '重設',\n  back: '返回',\n  copySuccess: '已複製到剪貼簿',\n  copyFailed: '複製失敗',\n  validation: {\n    required: '此項為必填',\n    invalidInput: '輸入無效',\n    selectRequired: '請選擇一個選項',\n    numberRange: '請輸入 {min} 到 {max} 之間的數字'\n  },\n  viewMore: '查看更多',\n  noMore: '沒有更多了',\n  selectAll: '全選',\n  expand: '展開',\n  collapse: '收合',\n  songCount: '{count}首',\n  language: '語言',\n  today: '今天',\n  yesterday: '昨天',\n  tray: {\n    show: '顯示',\n    quit: '退出',\n    playPause: '播放/暫停',\n    prev: '上一首',\n    next: '下一首',\n    pause: '暫停',\n    play: '播放',\n    favorite: '收藏'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/comp.ts",
    "content": "export default {\n  installApp: {\n    description: '安裝應用程式，獲得更好的體驗',\n    noPrompt: '不再提示',\n    install: '立即安裝',\n    cancel: '暫不安裝',\n    download: '下載',\n    downloadFailed: '下載失敗',\n    downloadComplete: '下載完成',\n    downloadProblem: '下載遇到問題？去',\n    downloadProblemLinkText: '下載最新版本'\n  },\n  playlistDrawer: {\n    title: '新增至播放清單',\n    createPlaylist: '建立新播放清單',\n    cancelCreate: '取消建立',\n    create: '建立',\n    playlistName: '播放清單名稱',\n    privatePlaylist: '私人播放清單',\n    publicPlaylist: '公開播放清單',\n    createSuccess: '播放清單建立成功',\n    createFailed: '播放清單建立失敗',\n    addSuccess: '歌曲新增成功',\n    addFailed: '歌曲新增失敗',\n    private: '私人',\n    public: '公開',\n    count: '首歌曲',\n    loginFirst: '請先登入',\n    getPlaylistFailed: '取得播放清單失敗',\n    inputPlaylistName: '請輸入播放清單名稱'\n  },\n  update: {\n    title: '發現新版本',\n    currentVersion: '目前版本',\n    cancel: '暫不更新',\n    prepareDownload: '準備下載...',\n    downloading: '下載中...',\n    nowUpdate: '立即更新',\n    downloadFailed: '下載失敗，請重試或手動下載',\n    startFailed: '啟動下載失敗，請重試或手動下載',\n    noDownloadUrl: '未找到適合目前系統的安裝包，請手動下載',\n    installConfirmTitle: '安裝更新',\n    installConfirmContent: '是否關閉應用程式並安裝更新？',\n    manualInstallTip: '如果關閉應用程式後沒有正常彈出安裝程式，請至下載資料夾尋找檔案並手動開啟。',\n    yesInstall: '立即安裝',\n    noThanks: '稍後安裝',\n    fileLocation: '檔案位置',\n    copy: '複製路徑',\n    copySuccess: '路徑已複製到剪貼簿',\n    copyFailed: '複製失敗',\n    backgroundDownload: '背景下載'\n  },\n  disclaimer: {\n    title: '使用說明',\n    warning: '本程式為開發測試版本，功能尚未完善，可能存在諸多問題及臭蟲，僅供學習交流使用。',\n    item1: '本程式僅供個人學習、研究及技術交流之目的，不得用於任何商業用途。',\n    item2: '請在下載後 24 小時內刪除，若對您有所幫助，請支持正版音樂。',\n    item3: '使用本程式即代表您已了解並同意相關風險，開發者對任何損失概不負責。',\n    agree: '我已了解並同意',\n    disagree: '不同意並退出'\n  },\n  donate: {\n    title: '支援開發者',\n    subtitle: '您的支援是我持續更新的動力',\n    tip: '捐贈完全採自願原則。即使不捐贈，您依然可以正常使用所有功能。感謝您的理解與支援！',\n    wechat: '微信支付',\n    alipay: '支付寶',\n    wechatQR: '微信收款碼',\n    alipayQR: '支付寶收款碼',\n    scanTip: '請使用手機 App 掃描 QR Code 進行捐贈',\n    enterApp: '進入程式',\n    noForce: '捐贈並非強制，您可以點擊按鈕直接進入'\n  },\n  coffee: {\n    title: '請我喝杯咖啡',\n    alipay: '支付寶',\n    wechat: '微信支付',\n    alipayQR: '支付寶收款碼',\n    wechatQR: '微信收款碼',\n    coffeeDesc: '一杯咖啡，一份支持',\n    coffeeDescLinkText: '查看更多',\n    groupText: '微信公众号：AlgerMusic',\n    messages: {\n      copySuccess: '已複製到剪貼簿'\n    },\n    donateList: '請我喝杯咖啡'\n  },\n  playlistType: {\n    title: '播放清單分類',\n    showAll: '顯示全部',\n    hide: '隱藏部分'\n  },\n  recommendAlbum: {\n    title: '最新專輯'\n  },\n  recommendSinger: {\n    title: '每日推薦',\n    songlist: '每日推薦清單'\n  },\n  recommendSonglist: {\n    title: '本週最熱音樂'\n  },\n  searchBar: {\n    login: '登入',\n    toLogin: '去登入',\n    logout: '登出',\n    set: '設定',\n    theme: '主題',\n    restart: '重新啟動',\n    refresh: '重新整理',\n    currentVersion: '目前版本',\n    searchPlaceholder: '搜尋點什麼吧...',\n    zoom: '頁面縮放',\n    zoom100: '標準縮放100%',\n    resetZoom: '點擊重設縮放',\n    zoomDefault: '標準縮放'\n  },\n  titleBar: {\n    closeTitle: '請選擇關閉方式',\n    minimizeToTray: '最小化到系統匣',\n    exitApp: '退出應用程式',\n    rememberChoice: '記住我的選擇',\n    closeApp: '關閉應用程式'\n  },\n  userPlayList: {\n    title: '{name}的常聽'\n  },\n  musicList: {\n    searchSongs: '搜尋歌曲',\n    noSearchResults: '沒有找到相關歌曲',\n    switchToNormal: '切換到預設版面',\n    switchToCompact: '切換到緊湊版面',\n    playAll: '播放全部',\n    collect: '收藏',\n    collectSuccess: '收藏成功',\n    cancelCollectSuccess: '取消收藏成功',\n    operationFailed: '操作失敗',\n    cancelCollect: '取消收藏',\n    addToPlaylist: '新增至播放清單',\n    addToPlaylistSuccess: '新增至播放清單成功',\n    songsAlreadyInPlaylist: '歌曲已存在於播放清單中',\n    historyRecommend: '歷史日推',\n    fetchDatesFailed: '獲取日期列表失敗',\n    fetchSongsFailed: '獲取歌曲列表失敗',\n    noSongs: '暫無歌曲'\n  },\n  playlist: {\n    import: {\n      button: '播放清單匯入',\n      title: '播放清單匯入',\n      description: '支援透過元資料/文字/連結三種方式匯入播放清單',\n      linkTab: '連結匯入',\n      textTab: '文字匯入',\n      localTab: '元資料匯入',\n      linkPlaceholder: '請輸入播放清單連結，每行一個',\n      textPlaceholder: '請輸入歌曲資訊，格式為：歌曲名 歌手名',\n      localPlaceholder: '請輸入JSON格式的歌曲元資料',\n      linkTips: '支援的連結來源：',\n      linkTip1: '將播放清單分享到微信/微博/QQ後複製連結',\n      linkTip2: '直接複製播放清單/個人主頁連結',\n      linkTip3: '直接複製文章連結',\n      textTips: '請輸入歌曲資訊，每行一首歌',\n      textFormat: '格式：歌曲名 歌手名',\n      localTips: '請新增歌曲元資料',\n      localFormat: '格式範例：',\n      songNamePlaceholder: '歌曲名稱',\n      artistNamePlaceholder: '藝人名稱',\n      albumNamePlaceholder: '專輯名稱',\n      addSongButton: '新增歌曲',\n      addLinkButton: '新增連結',\n      importToStarPlaylist: '匯入到我喜歡的音樂',\n      playlistNamePlaceholder: '請輸入播放清單名稱',\n      importButton: '開始匯入',\n      emptyLinkWarning: '請輸入播放清單連結',\n      emptyTextWarning: '請輸入歌曲資訊',\n      emptyLocalWarning: '請輸入歌曲元資料',\n      invalidJsonFormat: 'JSON格式不正確',\n      importSuccess: '匯入任務建立成功',\n      importFailed: '匯入失敗',\n      importStatus: '匯入狀態',\n      refresh: '重新整理',\n      taskId: '任務ID',\n      status: '狀態',\n      successCount: '成功數量',\n      failReason: '失敗原因',\n      unknownError: '未知錯誤',\n      statusPending: '等待處理',\n      statusProcessing: '處理中',\n      statusSuccess: '匯入成功',\n      statusFailed: '匯入失敗',\n      statusUnknown: '未知狀態',\n      taskList: '任務清單',\n      taskListTitle: '匯入任務清單',\n      action: '操作',\n      select: '選擇',\n      fetchTaskListFailed: '取得任務清單失敗',\n      noTasks: '暫無匯入任務',\n      clearTasks: '清除任務',\n      clearTasksConfirmTitle: '確認清除',\n      clearTasksConfirmContent: '確定要清除所有匯入任務記錄嗎？此操作不可恢復。',\n      confirm: '確認',\n      cancel: '取消',\n      clearTasksSuccess: '任務清單已清除',\n      clearTasksFailed: '清除任務清單失敗'\n    }\n  },\n  settings: '設定',\n  user: '使用者',\n  toplist: '排行榜',\n  history: '收藏歷史',\n  list: '播放清單',\n  mv: 'MV',\n  home: '首頁',\n  search: '搜尋'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/donation.ts",
    "content": "export default {\n  description: '您的捐贈將用於支持開發和維護工作，包括但不限於伺服器維護、域名續費等。',\n  message: '留言時可留下您的電子郵件或 github 名稱。',\n  refresh: '重新整理列表',\n  toDonateList: '請我喝杯咖啡',\n  noMessage: '暫無留言',\n  title: '捐贈列表'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/download.ts",
    "content": "export default {\n  title: '下載管理',\n  localMusic: '本機音樂',\n  count: '共 {count} 首歌曲',\n  clearAll: '清空記錄',\n  settings: '設定',\n  tabs: {\n    downloading: '下載中',\n    downloaded: '已下載'\n  },\n  empty: {\n    noTasks: '暫無下載任務',\n    noDownloaded: '暫無已下載歌曲'\n  },\n  progress: {\n    total: '總進度: {progress}%'\n  },\n  status: {\n    downloading: '下載中',\n    completed: '已完成',\n    failed: '失敗',\n    unknown: '未知'\n  },\n  artist: {\n    unknown: '未知歌手'\n  },\n  delete: {\n    title: '刪除確認',\n    message: '確定要刪除歌曲 \"{filename}\" 嗎？此操作不可恢復。',\n    confirm: '確定刪除',\n    cancel: '取消',\n    success: '刪除成功',\n    failed: '刪除失敗',\n    fileNotFound: '檔案不存在或已被移動，已從記錄中移除',\n    recordRemoved: '檔案刪除失敗，但已從記錄中移除'\n  },\n  clear: {\n    title: '清空下載記錄',\n    message: '確定要清空所有下載記錄嗎？此操作不會刪除已下載的音樂檔案，但將清空所有記錄。',\n    confirm: '確定清空',\n    cancel: '取消',\n    success: '下載記錄已清空'\n  },\n  message: {\n    downloadComplete: '{filename} 下載完成',\n    downloadFailed: '{filename} 下載失敗: {error}'\n  },\n  loading: '載入中...',\n  playStarted: '開始播放: {name}',\n  playFailed: '播放失敗: {name}',\n  path: {\n    copied: '路徑已複製到剪貼簿',\n    copyFailed: '複製路徑失敗'\n  },\n  settingsPanel: {\n    title: '下載設定',\n    path: '下載位置',\n    pathDesc: '設定音樂檔案下載儲存的位置',\n    pathPlaceholder: '請選擇下載路徑',\n    noPathSelected: '請先選擇下載路徑',\n    select: '選擇資料夾',\n    open: '開啟資料夾',\n    fileFormat: '檔名格式',\n    fileFormatDesc: '設定下載音樂時的檔案命名格式',\n    customFormat: '自訂格式',\n    separator: '分隔符號',\n    separators: {\n      dash: '空格-空格',\n      underscore: '底線',\n      space: '空格'\n    },\n    dragToArrange: '拖曳排序或使用箭頭按鈕調整順序：',\n    formatVariables: '可用變數',\n    preview: '預覽效果：',\n    saveSuccess: '下載設定已儲存',\n    presets: {\n      songArtist: '歌曲名 - 歌手名',\n      artistSong: '歌手名 - 歌曲名',\n      songOnly: '僅歌曲名'\n    },\n    components: {\n      songName: '歌曲名',\n      artistName: '歌手名',\n      albumName: '專輯名'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/favorite.ts",
    "content": "export default {\n  title: '我的收藏',\n  count: '共 {count} 首',\n  batchDownload: '批次下載',\n  download: '下載 ({count})',\n  emptyTip: '還沒有收藏歌曲',\n  downloadSuccess: '下載完成',\n  downloadFailed: '下載失敗',\n  downloading: '正在下載中，請稍候...',\n  selectSongsFirst: '請先選擇要下載的歌曲',\n  descending: '降',\n  ascending: '升'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/history.ts",
    "content": "export default {\n  title: '播放歷史',\n  heatmapTitle: '熱力圖',\n  playCount: '{count}',\n  getHistoryFailed: '取得歷史記錄失敗',\n  categoryTabs: {\n    songs: '歌曲',\n    playlists: '歌單',\n    albums: '專輯'\n  },\n  tabs: {\n    all: '全部記錄',\n    local: '本地記錄',\n    cloud: '雲端記錄'\n  },\n  getCloudRecordFailed: '取得雲端記錄失敗',\n  needLogin: '請使用cookie登入以查看雲端記錄',\n  merging: '正在合併記錄...',\n  noDescription: '暫無描述',\n  noData: '暫無記錄',\n  heatmap: {\n    title: '播放熱力圖',\n    loading: '正在載入數據...',\n    unit: '次播放',\n    footerText: '滑鼠懸停查看詳細信息',\n    playCount: '播放 {count} 次',\n    topSongs: '當天熱門歌曲',\n    times: '次',\n    totalPlays: '總播放次數',\n    activeDays: '活躍天數',\n    noData: '暫無播放記錄',\n    colorTheme: '配色方案',\n    colors: {\n      green: '綠色',\n      blue: '藍色',\n      orange: '橙色',\n      purple: '紫色',\n      red: '紅色'\n    },\n    mostPlayedSong: '播放最多的歌曲',\n    mostActiveDay: '最活躍的一天',\n    latestNightSong: '最晚播放的歌曲'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/login.ts",
    "content": "export default {\n  title: {\n    qr: '掃碼登入',\n    phone: '手機號登入',\n    cookie: 'Cookie登入',\n    uid: 'UID登入'\n  },\n  qrTip: '使用網易雲APP掃碼登入',\n  phoneTip: '使用網易雲帳號登入',\n  tokenTip: '輸入有效的網易雲音樂Cookie即可登入',\n  uidTip: '輸入使用者ID快速登入',\n  placeholder: {\n    phone: '手機號',\n    password: '密碼',\n    cookie: '請輸入網易雲音樂Cookie（token）',\n    uid: '請輸入使用者ID（UID）'\n  },\n  button: {\n    login: '登入',\n    switchToQr: '掃碼登入',\n    switchToPhone: '手機號登入',\n    switchToToken: '使用Cookie登入',\n    switchToUid: 'UID登入',\n    backToQr: '返回二維碼登入',\n    cookieLogin: 'Cookie登入',\n    autoGetCookie: '自動取得Cookie',\n    refresh: '點擊刷新',\n    refreshing: '刷新中...',\n    refreshQr: '刷新二維碼'\n  },\n  message: {\n    loginSuccess: '登入成功',\n    tokenLoginSuccess: 'Cookie登入成功',\n    uidLoginSuccess: 'UID登入成功',\n    loadError: '載入登入資訊時出錯',\n    qrCheckError: '檢查二維碼狀態時出錯',\n    tokenRequired: '請輸入Cookie',\n    tokenInvalid: 'Cookie無效，請檢查後重試',\n    uidRequired: '請輸入使用者ID',\n    uidInvalid: '使用者ID無效或使用者不存在',\n    uidLoginFailed: 'UID登入失敗，請檢查使用者ID是否正確',\n    autoGetCookieSuccess: '自動取得Cookie成功',\n    autoGetCookieFailed: '自動取得Cookie失敗',\n    autoGetCookieTip: '將開啟網易雲音樂登入頁面，請完成登入後關閉視窗',\n    loginFailed: '登入失敗',\n    phoneRequired: '請輸入手機號',\n    passwordRequired: '請輸入密碼',\n    phoneLoginFailed: '手機號登入失敗，請檢查手機號和密碼是否正確',\n    qrCheckFailed: '檢查二維碼狀態失敗，請刷新重試',\n    qrLoading: '正在載入二維碼...',\n    qrExpired: '二維碼已過期，請點擊刷新',\n    qrExpiredShort: '二維碼已過期',\n    qrExpiredWarning: '二維碼已過期，請點擊刷新獲取新的二維碼',\n    qrScanned: '已掃碼，請在手機上確認登入',\n    qrScannedShort: '已掃碼',\n    qrScannedInfo: '已扫码，请在手机上确认登录',\n    qrConfirmed: '登入成功，正在跳轉...',\n    qrGenerating: '正在生成二維碼...'\n  },\n  qrTitle: '掃碼登入網易雲音樂',\n  uidWarning: '注意：UID登入僅用於查看使用者公開資訊，無法訪問需要登入權限的功能'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/player.ts",
    "content": "export default {\n  nowPlaying: '正在播放',\n  playlist: '播放清單',\n  lyrics: '歌詞',\n  previous: '上一個',\n  play: '播放',\n  pause: '暫停',\n  next: '下一個',\n  volumeUp: '音量增加',\n  volumeDown: '音量減少',\n  mute: '靜音',\n  unmute: '取消靜音',\n  songNum: '歌曲總數：{num}',\n  addCorrection: '提前 {num} 秒',\n  subtractCorrection: '延遲 {num} 秒',\n  playFailed: '目前歌曲播放失敗，播放下一首',\n  parseFailedPlayNext: '歌曲解析失敗，播放下一首',\n  consecutiveFailsError: '播放遇到錯誤，可能是網路波動或解析源失效，請切換播放清單或稍後重試',\n  playMode: {\n    sequence: '順序播放',\n    loop: '單曲循環',\n    random: '隨機播放'\n  },\n  fullscreen: {\n    enter: '全螢幕',\n    exit: '退出全螢幕'\n  },\n  close: '關閉',\n  modeHint: {\n    single: '單曲循環',\n    list: '自動播放下一個'\n  },\n  lrc: {\n    noLrc: '暫無歌詞, 請欣賞',\n    noAutoScroll: '本歌詞不支持自動滾動'\n  },\n  reparse: {\n    title: '選擇解析音源',\n    desc: '點擊音源直接進行解析，下次播放此歌曲時將使用所選音源',\n    success: '重新解析成功',\n    failed: '重新解析失敗',\n    warning: '請選擇一個音源',\n    bilibiliNotSupported: 'B站影片不支援重新解析',\n    processing: '解析中...',\n    clear: '清除自訂音源',\n    customApiFailed: '自定義API解析失敗，正在嘗試使用內置音源...',\n    customApiError: '自定義API請求出錯，正在嘗試使用內置音源...'\n  },\n  playBar: {\n    expand: '展開歌詞',\n    collapse: '收合歌詞',\n    like: '喜歡',\n    lyric: '歌詞',\n    noSongPlaying: '沒有正在播放的歌曲',\n    eq: '等化器',\n    playList: '播放清單',\n    reparse: '重新解析',\n    playMode: {\n      sequence: '順序播放',\n      loop: '循環播放',\n      random: '隨機播放'\n    },\n    play: '開始播放',\n    pause: '暫停播放',\n    prev: '上一首',\n    next: '下一首',\n    volume: '音量',\n    favorite: '已收藏{name}',\n    unFavorite: '已取消收藏{name}',\n    miniPlayBar: '迷你播放列',\n    playbackSpeed: '播放速度',\n    advancedControls: '更多設定',\n    intelligenceMode: {\n      title: '心動模式',\n      needCookieLogin: '請使用 Cookie 方式登入後使用心動模式',\n      noFavoritePlaylist: '未找到我喜歡的音樂歌單',\n      noLikedSongs: '您還沒有喜歡的歌曲',\n      loading: '正在載入心動模式',\n      success: '已載入 {count} 首歌曲',\n      failed: '取得心動模式清單失敗',\n      error: '心動模式播放出錯'\n    }\n  },\n  eq: {\n    title: '等化器',\n    reset: '重設',\n    on: '開啟',\n    off: '關閉',\n    bass: '低音',\n    midrange: '中音',\n    treble: '高音',\n    presets: {\n      flat: '平坦',\n      pop: '流行',\n      rock: '搖滾',\n      classical: '古典',\n      jazz: '爵士',\n      electronic: '電子',\n      hiphop: '嘻哈',\n      rb: 'R&B',\n      metal: '金屬',\n      vocal: '人聲',\n      dance: '舞曲',\n      acoustic: '原聲',\n      custom: '自訂'\n    }\n  },\n  // 播放器設定\n  settings: {\n    title: '播放設定',\n    playbackSpeed: '播放速度'\n  },\n  // 定時關閉功能相關\n  sleepTimer: {\n    title: '定時關閉',\n    cancel: '取消定時',\n    timeMode: '按時間關閉',\n    songsMode: '按歌曲數關閉',\n    playlistEnd: '播放完清單後關閉',\n    afterPlaylist: '播放完清單後關閉',\n    activeUntilEnd: '播放至清單結束',\n    minutes: '分鐘',\n    hours: '小時',\n    songs: '首歌',\n    set: '設定',\n    timerSetSuccess: '已設定{minutes}分鐘後關閉',\n    songsSetSuccess: '已設定播放{songs}首歌後關閉',\n    playlistEndSetSuccess: '已設定播放完清單後關閉',\n    timerCancelled: '已取消定時關閉',\n    timerEnded: '定時關閉已觸發',\n    playbackStopped: '音樂播放已停止',\n    minutesRemaining: '剩餘{minutes}分鐘',\n    songsRemaining: '剩餘{count}首歌'\n  },\n  playList: {\n    clearAll: '清空播放清單',\n    alreadyEmpty: '播放清單已經為空',\n    cleared: '已清空播放清單',\n    empty: '播放清單為空',\n    clearConfirmTitle: '清空播放清單',\n    clearConfirmContent: '這將清空所有播放清單中的歌曲並停止目前播放。是否繼續？'\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/search.ts",
    "content": "export default {\n  title: {\n    hotSearch: '熱搜列表',\n    searchList: '搜尋列表',\n    searchHistory: '搜尋歷史'\n  },\n  button: {\n    clear: '清空',\n    back: '返回',\n    playAll: '播放列表'\n  },\n  loading: {\n    more: '載入中...',\n    failed: '搜尋失敗',\n    searching: '搜尋中...'\n  },\n  noMore: '沒有更多了',\n  error: {\n    searchFailed: '搜尋失敗'\n  },\n  search: {\n    single: '單曲',\n    album: '專輯',\n    playlist: '歌單',\n    mv: 'MV',\n    bilibili: 'B站'\n  },\n  history: '搜尋歷史',\n  hot: '熱門搜尋',\n  suggestions: '搜尋建議'\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/settings.ts",
    "content": "export default {\n  theme: '主題',\n  language: '語言',\n  regard: '關於',\n  logout: '登出',\n  sections: {\n    basic: '基礎設定',\n    playback: '播放設定',\n    application: '應用程式設定',\n    network: '網路設定',\n    system: '系統管理',\n    donation: '捐贈支持',\n    about: '關於'\n  },\n  basic: {\n    themeMode: '主題模式',\n    themeModeDesc: '切換日間/夜間主題',\n    autoTheme: '跟隨系統',\n    manualTheme: '手動切換',\n    language: '語言設定',\n    languageDesc: '切換顯示語言',\n    tokenManagement: 'Cookie管理',\n    tokenManagementDesc: '管理網易雲音樂登入Cookie',\n    tokenStatus: '目前Cookie狀態',\n    tokenSet: '已設定',\n    tokenNotSet: '未設定',\n    setToken: '設定Cookie',\n    setCookie: '設定Cookie',\n    modifyToken: '修改Cookie',\n    clearToken: '清除Cookie',\n    font: '字體設定',\n    fontDesc: '選擇字體，優先使用排在前面的字體',\n    fontScope: {\n      global: '全域',\n      lyric: '僅歌詞'\n    },\n    animation: '動畫速度',\n    animationDesc: '是否開起動畫',\n    animationSpeed: {\n      slow: '極慢',\n      normal: '正常',\n      fast: '極快'\n    },\n    fontPreview: {\n      title: '字體預覽',\n      chinese: '中文',\n      english: 'English',\n      japanese: '日本語',\n      korean: '한국어',\n      chineseText: '靜夜思 床前明月光 疑是地上霜',\n      englishText: 'The quick brown fox jumps over the lazy dog',\n      japaneseText: 'あいうえお かきくけこ さしすせそ',\n      koreanText: '가나다라마 바사아자차 카타파하'\n    },\n    gpuAcceleration: 'GPU加速',\n    gpuAccelerationDesc: '啟用或禁用硬體加速，可以提高渲染性能，但可能會增加GPU負載',\n    gpuAccelerationRestart: '更改GPU加速設定需要重啟應用後生效',\n    gpuAccelerationChangeSuccess: 'GPU加速設定已更新，重啟應用後生效',\n    gpuAccelerationChangeError: 'GPU加速設定更新失敗',\n    tabletMode: '平板模式',\n    tabletModeDesc: '啟用後將在移動設備上使用PC樣式界面，適合平板等大屏設備'\n  },\n  playback: {\n    quality: '音質設定',\n    qualityDesc: '選擇音樂播放音質（網易云VIP）',\n    qualityOptions: {\n      standard: '標準',\n      higher: '較高',\n      exhigh: '極高',\n      lossless: '無損',\n      hires: 'Hi-Res',\n      jyeffect: '高清環繞聲',\n      sky: '沉浸環繞聲',\n      dolby: '杜比全景聲',\n      jymaster: '超清母帶'\n    },\n    musicSources: '音源設定',\n    musicSourcesDesc: '選擇音樂解析使用的音源平台',\n    musicSourcesWarning: '至少需要選擇一個音源平台',\n    musicUnblockEnable: '啟用音樂解析',\n    musicUnblockEnableDesc: '開啟後將嘗試解析無法播放的音樂',\n    configureMusicSources: '設定音源',\n    selectedMusicSources: '已選音源：',\n    noMusicSources: '未選擇音源',\n    gdmusicInfo: 'GD音樂台可自動解析多個平台音源，自動選擇最佳結果',\n    autoPlay: '自動播放',\n    autoPlayDesc: '重新開啟應用程式時是否自動繼續播放',\n    showStatusBar: '是否顯示狀態列控制功能',\n    showStatusBarContent: '可以在您的mac狀態列顯示音樂控制功能(重啟後生效)',\n    fallbackParser: '備用解析服務 (GD音樂台)',\n    fallbackParserDesc: '當勾選「GD音樂台」且常規音源無法播放時，將使用此服務嘗試解析。',\n    parserGD: 'GD 音樂台 (內建)',\n    parserCustom: '自訂 API',\n\n    // 音源標籤\n    sourceLabels: {\n      migu: '咪咕音樂',\n      kugou: '酷狗音樂',\n      pyncmd: '網易雲（內建）',\n      bilibili: 'Bilibili',\n      gdmusic: 'GD音樂台',\n      custom: '自訂 API'\n    },\n\n    customApi: {\n      sectionTitle: '自訂 API 設定',\n      importConfig: '匯入 JSON 設定',\n      currentSource: '目前音源',\n      notImported: '尚未匯入自訂音源。',\n      importSuccess: '成功匯入音源：{name}',\n      importFailed: '匯入失敗：{message}',\n      enableHint: '請先匯入 JSON 設定檔才能啟用',\n      status: {\n        imported: '已匯入自訂音源',\n        notImported: '未匯入'\n      }\n    },\n    lxMusic: {\n      tabs: {\n        sources: '音源選擇',\n        lxMusic: '落雪音源',\n        customApi: '自訂API'\n      },\n      scripts: {\n        title: '已匯入的音源腳本',\n        importLocal: '本機匯入',\n        importOnline: '線上匯入',\n        urlPlaceholder: '輸入落雪音源腳本 URL',\n        importBtn: '匯入',\n        empty: '暫無已匯入的落雪音源',\n        notConfigured: '未設定 (請至落雪音源分頁設定)',\n        importHint: '匯入相容的自訂 API 外掛以擴充音源',\n        noScriptWarning: '請先匯入落雪音源腳本',\n        noSelectionWarning: '請先選擇一個落雪音源',\n        notFound: '音源不存在',\n        switched: '已切換到音源: {name}',\n        deleted: '已刪除音源: {name}',\n        enterUrl: '請輸入腳本 URL',\n        invalidUrl: '無效的 URL 格式',\n        invalidScript: '無效的落雪音源腳本，未找到 globalThis.lx 相關程式碼',\n        nameRequired: '名稱不能為空',\n        renameSuccess: '重新命名成功'\n      }\n    }\n  },\n  application: {\n    closeAction: '關閉行為',\n    closeActionDesc: '選擇關閉視窗時的行為',\n    closeOptions: {\n      ask: '每次詢問',\n      minimize: '最小化到系統匣',\n      close: '直接退出'\n    },\n    shortcut: '快捷鍵設定',\n    shortcutDesc: '自訂全域快捷鍵',\n    download: '下載管理',\n    downloadDesc: '是否始終顯示下載清單按鈕',\n    unlimitedDownload: '無限制下載',\n    unlimitedDownloadDesc: '開啟後將無限制下載音樂（可能出現下載失敗的情況）, 預設限制 300 首',\n    downloadPath: '下載目錄',\n    downloadPathDesc: '選擇音樂檔案的下載位置',\n    remoteControl: '遠端控制',\n    remoteControlDesc: '設定遠端控制功能'\n  },\n  network: {\n    apiPort: '音樂API連接埠',\n    apiPortDesc: '修改後需要重啟應用程式',\n    proxy: '代理設定',\n    proxyDesc: '無法存取音樂時可以開啟代理',\n    proxyHost: '代理位址',\n    proxyHostPlaceholder: '請輸入代理位址',\n    proxyPort: '代理連接埠',\n    proxyPortPlaceholder: '請輸入代理連接埠',\n    realIP: 'realIP設定',\n    realIPDesc: '由於限制,此項目在國外使用會受到限制可使用realIP參數,傳進國內IP解決',\n    messages: {\n      proxySuccess: '代理設定已儲存，重啟應用程式後生效',\n      proxyError: '請檢查輸入是否正確',\n      realIPSuccess: '真實IP設定已儲存',\n      realIPError: '請輸入有效的IP位址'\n    }\n  },\n  system: {\n    cache: '快取管理',\n    cacheDesc: '清除快取',\n    cacheClearTitle: '請選擇要清除的快取類型：',\n    cacheTypes: {\n      history: {\n        label: '播放歷史',\n        description: '清除播放過的歌曲記錄'\n      },\n      favorite: {\n        label: '收藏記錄',\n        description: '清除本機收藏的歌曲記錄(不會影響雲端收藏)'\n      },\n      user: {\n        label: '使用者資料',\n        description: '清除登入資訊和使用者相關資料'\n      },\n      settings: {\n        label: '應用程式設定',\n        description: '清除應用程式的所有自訂設定'\n      },\n      downloads: {\n        label: '下載記錄',\n        description: '清除下載歷史記錄(不會刪除已下載的檔案)'\n      },\n      resources: {\n        label: '音樂資源',\n        description: '清除已載入的音樂檔案、歌詞等資源快取'\n      },\n      lyrics: {\n        label: '歌詞資源',\n        description: '清除已載入的歌詞資源快取'\n      }\n    },\n    restart: '重新啟動',\n    restartDesc: '重新啟動應用程式',\n    messages: {\n      clearSuccess: '清除成功，部分設定在重啟後生效'\n    }\n  },\n  about: {\n    version: '版本',\n    checkUpdate: '檢查更新',\n    checking: '檢查中...',\n    latest: '目前已是最新版本',\n    hasUpdate: '發現新版本',\n    gotoUpdate: '前往更新',\n    gotoGithub: '前往 Github',\n    author: '作者',\n    authorDesc: 'algerkong 點個star🌟呗',\n    messages: {\n      checkError: '檢查更新失敗，請稍後重試'\n    }\n  },\n  validation: {\n    selectProxyProtocol: '請選擇代理協議',\n    proxyHost: '請輸入代理位址',\n    portNumber: '請輸入有效的連接埠號(1-65535)'\n  },\n  lyricSettings: {\n    title: '歌詞設定',\n    tabs: {\n      display: '顯示',\n      interface: '介面',\n      typography: '文字',\n      background: '背景',\n      mobile: '行動端'\n    },\n    pureMode: '純淨模式',\n    hideCover: '隱藏封面',\n    centerDisplay: '置中顯示',\n    showTranslation: '顯示翻譯',\n    hideLyrics: '隱藏歌詞',\n    hidePlayBar: '隱藏播放列',\n    hideMiniPlayBar: '隱藏迷你播放列',\n    showMiniPlayBar: '顯示迷你播放列',\n    backgroundTheme: '背景主題',\n    themeOptions: {\n      default: '預設',\n      light: '亮色',\n      dark: '暗色'\n    },\n    fontSize: '字體大小',\n    fontSizeMarks: {\n      small: '小',\n      medium: '中',\n      large: '大'\n    },\n    fontWeight: '字體粗細',\n    fontWeightMarks: {\n      thin: '細',\n      normal: '常規',\n      bold: '粗'\n    },\n    letterSpacing: '字間距',\n    letterSpacingMarks: {\n      compact: '緊湊',\n      default: '預設',\n      loose: '寬鬆'\n    },\n    lineHeight: '行高',\n    lineHeightMarks: {\n      compact: '緊湊',\n      default: '預設',\n      loose: '寬鬆'\n    },\n    contentWidth: '內容區寬度',\n    mobileLayout: '行動端佈局',\n    layoutOptions: {\n      default: '預設',\n      ios: 'iOS 風格',\n      android: 'Android 風格'\n    },\n    mobileCoverStyle: '封面風格',\n    coverOptions: {\n      record: '唱片',\n      square: '方形',\n      full: '全螢幕'\n    },\n    lyricLines: '歌詞行數',\n    mobileUnavailable: '此設定僅在行動端可用',\n    // 背景設定\n    background: {\n      useCustomBackground: '使用自訂背景',\n      backgroundMode: '背景模式',\n      modeOptions: {\n        solid: '純色',\n        gradient: '漸層',\n        image: '圖片',\n        css: 'CSS'\n      },\n      solidColor: '選擇顏色',\n      presetColors: '預設顏色',\n      customColor: '自訂顏色',\n      gradientEditor: '漸層編輯器',\n      gradientColors: '漸層顏色',\n      gradientDirection: '漸層方向',\n      directionOptions: {\n        toBottom: '上到下',\n        toRight: '左到右',\n        toBottomRight: '左上到右下',\n        angle45: '45度',\n        toTop: '下到上',\n        toLeft: '右到左'\n      },\n      addColor: '新增顏色',\n      removeColor: '移除顏色',\n      imageUpload: '上傳圖片',\n      imagePreview: '圖片預覽',\n      clearImage: '清除圖片',\n      imageBlur: '模糊度',\n      imageBrightness: '明暗度',\n      customCss: '自訂 CSS 樣式',\n      customCssPlaceholder: '輸入 CSS 樣式，如: background: linear-gradient(...)',\n      customCssHelp: '支援任意 CSS background 屬性',\n      reset: '重設為預設',\n      fileSizeLimit: '圖片大小限制: 20MB',\n      invalidImageFormat: '無效的圖片格式',\n      imageTooLarge: '圖片過大，請選擇小於 20MB 的圖片'\n    }\n  },\n  themeColor: {\n    title: '歌詞主題色',\n    presetColors: '預設顏色',\n    customColor: '自訂顏色',\n    preview: '預覽效果',\n    previewText: '歌詞效果',\n    colorNames: {\n      'spotify-green': 'Spotify 綠',\n      'apple-blue': '蘋果藍',\n      'youtube-red': 'YouTube 紅',\n      orange: '活力橙',\n      purple: '神秘紫',\n      pink: '櫻花粉'\n    },\n    tooltips: {\n      openColorPicker: '開啟色板',\n      closeColorPicker: '關閉色板'\n    },\n    placeholder: '#1db954'\n  },\n  translationEngine: '歌詞翻譯引擎',\n  translationEngineOptions: {\n    none: '關閉',\n    opencc: 'OpenCC 繁化'\n  },\n  shortcutSettings: {\n    title: '快捷鍵設定',\n    shortcut: '快捷鍵',\n    shortcutDesc: '自訂快捷鍵',\n    shortcutConflict: '快捷鍵衝突',\n    inputPlaceholder: '點擊輸入快捷鍵',\n    resetShortcuts: '恢復預設',\n    disableAll: '全部停用',\n    enableAll: '全部啟用',\n    togglePlay: '播放/暫停',\n    prevPlay: '上一首',\n    nextPlay: '下一首',\n    volumeUp: '增加音量',\n    volumeDown: '減少音量',\n    toggleFavorite: '收藏/取消收藏',\n    toggleWindow: '顯示/隱藏視窗',\n    scopeGlobal: '全域',\n    scopeApp: '應用程式內',\n    enabled: '已啟用',\n    disabled: '已停用',\n    messages: {\n      resetSuccess: '已恢復預設快捷鍵，請記得儲存',\n      conflict: '存在快捷鍵衝突，請重新設定',\n      saveSuccess: '快捷鍵設定已儲存',\n      saveError: '快捷鍵儲存失敗，請重試',\n      cancelEdit: '已取消修改',\n      disableAll: '已停用所有快捷鍵，請記得儲存',\n      enableAll: '已啟用所有快捷鍵，請記得儲存'\n    }\n  },\n  remoteControl: {\n    title: '遠端控制',\n    enable: '啟用遠端控制',\n    port: '服務連接埠',\n    allowedIps: '允許的 IP 位址',\n    addIp: '新增 IP',\n    emptyListHint: '空白清單表示允許所有 IP 存取',\n    saveSuccess: '遠端控制設定已儲存',\n    accessInfo: '遠端控制存取位址：'\n  },\n  cookie: {\n    title: 'Cookie設定',\n    description: '請輸入網易雲音樂的Cookie：',\n    placeholder: '請貼上完整的Cookie...',\n    help: {\n      format: 'Cookie通常以 \"MUSIC_U=\" 開頭',\n      source: '可以從瀏覽器開發者工具的網路請求中取得',\n      storage: 'Cookie設定後將自動儲存到本機儲存'\n    },\n    action: {\n      save: '儲存Cookie',\n      paste: '貼上',\n      clear: '清空'\n    },\n    validation: {\n      required: '請輸入Cookie',\n      format: 'Cookie格式可能不正確，請檢查是否包含MUSIC_U'\n    },\n    message: {\n      saveSuccess: 'Cookie儲存成功',\n      saveError: 'Cookie儲存失敗',\n      pasteSuccess: '貼上成功',\n      pasteError: '貼上失敗，請手動複製'\n    },\n    info: {\n      length: '目前長度：{length} 字元'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/songItem.ts",
    "content": "export default {\n  menu: {\n    play: '播放',\n    playNext: '下一首播放',\n    download: '下載歌曲',\n    addToPlaylist: '新增至播放清單',\n    favorite: '喜歡',\n    unfavorite: '取消喜歡',\n    removeFromPlaylist: '從播放清單中刪除',\n    dislike: '不喜歡',\n    undislike: '取消不喜歡'\n  },\n  message: {\n    downloading: '正在下載中，請稍候...',\n    downloadFailed: '下載失敗',\n    downloadQueued: '已加入下載佇列',\n    addedToNextPlay: '已新增至下一首播放',\n    getUrlFailed: '取得音樂下載位址失敗，請檢查是否登入'\n  },\n  dialog: {\n    dislike: {\n      title: '提示！',\n      content: '確認不喜歡這首歌嗎？再次進入將從每日推薦中排除。',\n      positiveText: '不喜歡',\n      negativeText: '取消'\n    }\n  }\n};\n"
  },
  {
    "path": "src/i18n/lang/zh-Hant/user.ts",
    "content": "export default {\n  profile: {\n    followers: '粉絲',\n    following: '關注',\n    level: '等級'\n  },\n  playlist: {\n    created: '建立的歌單',\n    mine: '我建立的',\n    trackCount: '{count}首',\n    playCount: '播放{count}次'\n  },\n  tabs: {\n    created: '建立',\n    favorite: '收藏',\n    album: '專輯'\n  },\n  ranking: {\n    title: '聽歌排行',\n    playCount: '{count}次'\n  },\n  follow: {\n    title: '關注列表',\n    viewPlaylist: '查看歌單',\n    noFollowings: '暫無關注',\n    loadMore: '載入更多',\n    noSignature: '這個傢伙很懶，什麼都沒留下',\n    userFollowsTitle: '的關注',\n    myFollowsTitle: '我的關注'\n  },\n  follower: {\n    title: '粉絲列表',\n    noFollowers: '暫無粉絲',\n    loadMore: '載入更多',\n    userFollowersTitle: '的粉絲',\n    myFollowersTitle: '我的粉絲'\n  },\n  detail: {\n    playlists: '歌單',\n    records: '聽歌排行',\n    noPlaylists: '暫無歌單',\n    noRecords: '暫無聽歌記錄',\n    artist: '歌手',\n    noSignature: '這個人很懶，什麼都沒留下',\n    invalidUserId: '使用者ID無效',\n    noRecordPermission: '{name}不讓你聽歌排行'\n  },\n  message: {\n    loadFailed: '載入使用者頁面失敗',\n    deleteSuccess: '刪除成功',\n    deleteFailed: '刪除失敗'\n  }\n};\n"
  },
  {
    "path": "src/i18n/languages.ts",
    "content": "// 语言配置文件 - 集中管理语言相关的配置\r\n\r\n// 语言显示名称映射\r\nexport const LANGUAGE_DISPLAY_NAMES: Record<string, string> = {\r\n  'zh-CN': '简体中文',\r\n  'zh-Hant': '繁體中文',\r\n  'en-US': 'English',\r\n  'ja-JP': '日本語',\r\n  'ko-KR': '한국어'\r\n};\r\n\r\n// 默认语言\r\nexport const DEFAULT_LANGUAGE = 'zh-CN';\r\n\r\n// 回退语言\r\nexport const FALLBACK_LANGUAGE = 'en-US';\r\n\r\n// 语言排序优先级（用于在UI中的显示顺序）\r\nexport const LANGUAGE_PRIORITY: Record<string, number> = {\r\n  'zh-CN': 1,\r\n  'zh-Hant': 2,\r\n  'en-US': 3,\r\n  'ja-JP': 4,\r\n  'ko-KR': 5\r\n};\r\n"
  },
  {
    "path": "src/i18n/main.ts",
    "content": "import { DEFAULT_LANGUAGE } from './languages';\nimport { buildLanguageMessages } from './utils';\n\n// 使用工具函数构建语言消息对象\nconst messages = buildLanguageMessages();\n\ntype Language = keyof typeof messages;\n\n// 为主进程提供一个简单的 i18n 实现\nconst mainI18n = {\n  global: {\n    currentLocale: DEFAULT_LANGUAGE as Language,\n    get locale() {\n      return this.currentLocale;\n    },\n    set locale(value: Language) {\n      this.currentLocale = value;\n    },\n    t(key: string) {\n      const keys = key.split('.');\n      let current: any = messages[this.currentLocale];\n      for (const k of keys) {\n        if (current[k] === undefined) {\n          // 如果找不到翻译，返回键名\n          return key;\n        }\n        current = current[k];\n      }\n      return current;\n    },\n    messages\n  }\n};\n\nexport type { Language };\nexport default mainI18n;\n"
  },
  {
    "path": "src/i18n/renderer.ts",
    "content": "import { createI18n } from 'vue-i18n';\n\nimport { DEFAULT_LANGUAGE, FALLBACK_LANGUAGE } from './languages';\nimport { buildLanguageMessages } from './utils';\n\n// 使用工具函数构建语言消息对象\nconst messages = buildLanguageMessages();\n\nconst i18n = createI18n({\n  legacy: false,\n  locale: DEFAULT_LANGUAGE,\n  fallbackLocale: FALLBACK_LANGUAGE,\n  messages,\n  globalInjection: true,\n  silentTranslationWarn: true,\n  silentFallbackWarn: true\n});\n\nexport default i18n;\n"
  },
  {
    "path": "src/i18n/utils.ts",
    "content": "// 自动导入所有语言的所有翻译文件\r\nconst allLangModules = import.meta.glob('./lang/**/*.ts', { eager: true });\r\n\r\n// 构建语言消息对象\r\nexport const buildLanguageMessages = () => {\r\n  const messages: Record<string, Record<string, any>> = {};\r\n\r\n  Object.entries(allLangModules).forEach(([path, module]) => {\r\n    // 解析路径，例如 './lang/zh-CN/common.ts' -> { lang: 'zh-CN', module: 'common' }\r\n    const match = path.match(/\\.\\/lang\\/([^/]+)\\/([^/]+)\\.ts$/);\r\n    if (match) {\r\n      const [, langCode, moduleName] = match;\r\n\r\n      // 跳过 index 文件\r\n      if (moduleName !== 'index') {\r\n        if (!messages[langCode]) {\r\n          messages[langCode] = {};\r\n        }\r\n        messages[langCode][moduleName] = (module as any).default;\r\n      }\r\n    }\r\n  });\r\n\r\n  return messages;\r\n};\r\n\r\n// 获取所有支持的语言\r\nexport const getSupportedLanguages = (): string[] => {\r\n  const messages = buildLanguageMessages();\r\n  return Object.keys(messages);\r\n};\r\n\r\nexport const isLanguageSupported = (lang: string): boolean => {\r\n  return getSupportedLanguages().includes(lang);\r\n};\r\n\r\nimport { LANGUAGE_DISPLAY_NAMES, LANGUAGE_PRIORITY } from './languages';\r\n\r\n// 获取语言显示名称的映射\r\nexport const getLanguageDisplayNames = (): Record<string, string> => {\r\n  return LANGUAGE_DISPLAY_NAMES;\r\n};\r\n\r\n// 生成语言选项数组，用于下拉选择等组件\r\nexport const getLanguageOptions = () => {\r\n  const supportedLanguages = getSupportedLanguages();\r\n  const displayNames = getLanguageDisplayNames();\r\n\r\n  // 按优先级排序\r\n  const sortedLanguages = supportedLanguages.sort((a, b) => {\r\n    const priorityA = LANGUAGE_PRIORITY[a] || 999;\r\n    const priorityB = LANGUAGE_PRIORITY[b] || 999;\r\n    return priorityA - priorityB;\r\n  });\r\n\r\n  return sortedLanguages.map((lang) => ({\r\n    label: displayNames[lang] || lang,\r\n    value: lang\r\n  }));\r\n};\r\n"
  },
  {
    "path": "src/main/index.ts",
    "content": "import { electronApp, optimizer } from '@electron-toolkit/utils';\nimport { app, ipcMain, nativeImage } from 'electron';\nimport { join } from 'path';\n\nimport type { Language } from '../i18n/main';\nimport i18n from '../i18n/main';\nimport { loadLyricWindow } from './lyric';\nimport { initializeConfig } from './modules/config';\nimport { initializeFileManager } from './modules/fileManager';\nimport { initializeFonts } from './modules/fonts';\nimport { initializeLoginWindow } from './modules/loginWindow';\nimport { initializeOtherApi } from './modules/otherApi';\nimport { initializeRemoteControl } from './modules/remoteControl';\nimport { initializeShortcuts, registerShortcuts } from './modules/shortcuts';\nimport { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray';\nimport { setupUpdateHandlers } from './modules/update';\nimport { createMainWindow, initializeWindowManager, setAppQuitting } from './modules/window';\nimport { initWindowSizeManager } from './modules/window-size';\nimport { startMusicApi } from './server';\nimport { initLxMusicHttp } from './modules/lxMusicHttp';\n\n// 导入所有图标\nconst iconPath = join(__dirname, '../../resources');\nconst icon = nativeImage.createFromPath(\n  process.platform === 'darwin' ? join(iconPath, 'icon.icns') : join(iconPath, 'icon.png')\n);\n\nlet mainWindow: Electron.BrowserWindow;\n\n// 初始化应用\nfunction initialize(configStore: any) {\n  // 使用已初始化的配置存储\n  const store = configStore;\n\n  // 设置初始语言\n  const savedLanguage = store.get('set.language') as Language;\n  if (savedLanguage) {\n    i18n.global.locale = savedLanguage;\n  }\n\n  // 初始化文件管理\n  initializeFileManager();\n  // 初始化其他 API （搜索建议等）\n  initializeOtherApi();\n  // 初始化窗口管理\n  initializeWindowManager();\n  // 初始化字体管理\n  initializeFonts();\n  // 初始化登录窗口\n  initializeLoginWindow();\n\n  // 创建主窗口\n  mainWindow = createMainWindow(icon);\n\n  // 初始化托盘\n  initializeTray(iconPath, mainWindow);\n\n  // 启动音乐API\n  startMusicApi();\n\n  // 初始化落雪音乐 HTTP 请求处理\n  initLxMusicHttp();\n\n  // 加载歌词窗口\n  loadLyricWindow(ipcMain, mainWindow);\n\n  // 初始化快捷键\n  initializeShortcuts(mainWindow);\n\n  // 初始化远程控制服务\n  initializeRemoteControl(mainWindow);\n\n  // 初始化更新处理程序\n  setupUpdateHandlers(mainWindow);\n}\n\n// 检查是否为第一个实例\nconst isSingleInstance = app.requestSingleInstanceLock();\n\nif (!isSingleInstance) {\n  app.quit();\n} else {\n  // 在应用准备就绪前初始化GPU加速设置\n  // 必须在 app.ready 之前调用 disableHardwareAcceleration\n  try {\n    // 初始化配置管理以获取GPU加速设置\n    const store = initializeConfig();\n    const enableGpuAcceleration = store.get('set.enableGpuAcceleration', true) as boolean;\n\n    if (!enableGpuAcceleration) {\n      console.log('GPU加速已禁用');\n      app.disableHardwareAcceleration();\n    } else {\n      console.log('GPU加速已启用');\n    }\n  } catch (error) {\n    console.error('GPU加速设置初始化失败:', error);\n    // 如果配置读取失败，默认启用GPU加速\n  }\n  // 当第二个实例启动时，将焦点转移到第一个实例的窗口\n  app.on('second-instance', () => {\n    if (mainWindow) {\n      if (mainWindow.isMinimized()) {\n        mainWindow.restore();\n      }\n      mainWindow.show();\n      mainWindow.focus();\n    }\n  });\n\n  // 应用程序准备就绪时的处理\n  app.whenReady().then(() => {\n    // 设置应用ID\n    electronApp.setAppUserModelId('com.alger.music');\n\n    // 监听窗口创建事件\n    app.on('browser-window-created', (_, window) => {\n      optimizer.watchWindowShortcuts(window);\n    });\n\n    // 初始化窗口大小管理器\n    initWindowSizeManager();\n\n    // 重新初始化配置管理以获取完整的配置存储\n    const store = initializeConfig();\n\n    // 初始化应用\n    initialize(store);\n\n    // macOS 激活应用时的处理\n    app.on('activate', () => {\n      if (mainWindow === null) initialize(store);\n    });\n  });\n\n  // 监听快捷键更新\n  ipcMain.on('update-shortcuts', () => {\n    registerShortcuts(mainWindow);\n  });\n\n  // 监听语言切换\n  ipcMain.on('change-language', (_, locale: Language) => {\n    // 更新主进程的语言设置\n    i18n.global.locale = locale;\n    // 更新托盘菜单\n    updateTrayMenu(mainWindow);\n    // 通知所有窗口语言已更改\n    mainWindow?.webContents.send('language-changed', locale);\n  });\n\n  // 监听播放状态变化\n  ipcMain.on('update-play-state', (_, playing: boolean) => {\n    updatePlayState(playing);\n  });\n\n  // 监听当前歌曲变化\n  ipcMain.on('update-current-song', (_, song: any) => {\n    updateCurrentSong(song);\n  });\n\n  // 所有窗口关闭时的处理\n  app.on('window-all-closed', () => {\n    if (process.platform !== 'darwin') {\n      app.quit();\n    }\n  });\n\n  // 应用即将退出时的处理\n  app.on('before-quit', () => {\n    // 设置退出标志\n    setAppQuitting(true);\n  });\n\n  // 重启应用\n  ipcMain.on('restart', () => {\n    app.relaunch();\n    app.exit(0);\n  });\n\n  // 获取系统架构信息\n  ipcMain.on('get-arch', (event) => {\n    event.returnValue = process.arch;\n  });\n}\n"
  },
  {
    "path": "src/main/lyric.ts",
    "content": "import { BrowserWindow, IpcMain, screen } from 'electron';\nimport Store from 'electron-store';\nimport path, { join } from 'path';\n\nconst store = new Store();\nlet lyricWindow: BrowserWindow | null = null;\n\n// 跟踪拖动状态\nlet isDragging = false;\n\n// 添加窗口大小变化防护\nlet originalSize = { width: 0, height: 0 };\n\nconst createWin = () => {\n  console.log('Creating lyric window');\n\n  // 获取保存的窗口位置\n  const windowBounds =\n    (store.get('lyricWindowBounds') as {\n      x?: number;\n      y?: number;\n      width?: number;\n      height?: number;\n      displayId?: number;\n    }) || {};\n\n  const { x, y, width, height, displayId } = windowBounds;\n\n  // 获取所有屏幕的信息\n  const displays = screen.getAllDisplays();\n  let isValidPosition = false;\n  let targetDisplay = displays[0]; // 默认使用主显示器\n\n  // 如果有显示器ID，尝试按ID匹配\n  if (displayId) {\n    const matchedDisplay = displays.find((d) => d.id === displayId);\n    if (matchedDisplay) {\n      targetDisplay = matchedDisplay;\n      console.log('Found matching display by ID:', displayId);\n    }\n  }\n\n  // 验证位置是否在任何显示器的范围内\n  if (x !== undefined && y !== undefined) {\n    for (const display of displays) {\n      const { bounds } = display;\n      if (\n        x >= bounds.x - 50 && // 允许一点偏移，避免卡在边缘\n        x < bounds.x + bounds.width + 50 &&\n        y >= bounds.y - 50 &&\n        y < bounds.y + bounds.height + 50\n      ) {\n        isValidPosition = true;\n        targetDisplay = display;\n        break;\n      }\n    }\n  }\n\n  // 确保宽高合理\n  const defaultWidth = 800;\n  const defaultHeight = 200;\n  const maxWidth = 1600; // 设置最大宽度限制\n  const maxHeight = 800; // 设置最大高度限制\n\n  const validWidth = width && width > 0 && width <= maxWidth ? width : defaultWidth;\n  const validHeight = height && height > 0 && height <= maxHeight ? height : defaultHeight;\n\n  // 确定窗口位置\n  let windowX = isValidPosition ? x : undefined;\n  let windowY = isValidPosition ? y : undefined;\n\n  // 如果位置无效，默认在当前显示器中居中\n  if (windowX === undefined || windowY === undefined) {\n    windowX = targetDisplay.bounds.x + (targetDisplay.bounds.width - validWidth) / 2;\n    windowY = targetDisplay.bounds.y + (targetDisplay.bounds.height - validHeight) / 2;\n  }\n\n  lyricWindow = new BrowserWindow({\n    width: validWidth,\n    height: validHeight,\n    x: windowX,\n    y: windowY,\n    frame: false,\n    show: false,\n    transparent: true,\n    opacity: 1,\n    hasShadow: false,\n    alwaysOnTop: true,\n    resizable: true,\n    roundedCorners: false,\n    titleBarStyle: 'hidden',\n    titleBarOverlay: false,\n    // 添加跨屏幕支持选项\n    webPreferences: {\n      preload: join(__dirname, '../preload/index.js'),\n      sandbox: false,\n      contextIsolation: true\n    },\n    backgroundColor: '#00000000'\n  });\n\n  // 监听窗口关闭事件\n  lyricWindow.on('closed', () => {\n    if (lyricWindow) {\n      lyricWindow.destroy();\n      lyricWindow = null;\n    }\n  });\n\n  // 监听窗口大小变化事件，保存新的尺寸\n  lyricWindow.on('resize', () => {\n    // 如果正在拖动，忽略大小调整事件\n    if (isDragging) return;\n\n    if (lyricWindow && !lyricWindow.isDestroyed()) {\n      const [width, height] = lyricWindow.getSize();\n      const [x, y] = lyricWindow.getPosition();\n\n      // 保存窗口位置和大小\n      store.set('lyricWindowBounds', { x, y, width, height });\n    }\n  });\n\n  lyricWindow.on('blur', () => lyricWindow && lyricWindow.setMaximizable(false));\n\n  return lyricWindow;\n};\n\nexport const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void => {\n  const showLyricWindow = () => {\n    if (lyricWindow && !lyricWindow.isDestroyed()) {\n      if (lyricWindow.isMinimized()) {\n        lyricWindow.restore();\n      }\n      lyricWindow.focus();\n      lyricWindow.show();\n      return true;\n    }\n    return false;\n  };\n\n  ipcMain.on('open-lyric', () => {\n    console.log('Received open-lyric request');\n\n    if (showLyricWindow()) {\n      return;\n    }\n\n    console.log('Creating new lyric window');\n    const win = createWin();\n\n    if (!win) {\n      console.error('Failed to create lyric window');\n      return;\n    }\n\n    if (process.env.NODE_ENV === 'development') {\n      win.webContents.openDevTools({ mode: 'detach' });\n      win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/#/lyric`);\n    } else {\n      const distPath = path.resolve(__dirname, '../renderer');\n      win.loadURL(`file://${distPath}/index.html#/lyric`);\n    }\n\n    win.setMinimumSize(600, 200);\n    win.setSkipTaskbar(true);\n\n    win.once('ready-to-show', () => {\n      console.log('Lyric window ready to show');\n      win.show();\n    });\n  });\n\n  ipcMain.on('send-lyric', (_, data) => {\n    if (lyricWindow && !lyricWindow.isDestroyed()) {\n      try {\n        lyricWindow.webContents.send('receive-lyric', data);\n      } catch (error) {\n        console.error('Error processing lyric data:', error);\n      }\n    }\n  });\n\n  ipcMain.on('top-lyric', (_, data) => {\n    if (lyricWindow && !lyricWindow.isDestroyed()) {\n      lyricWindow.setAlwaysOnTop(data);\n    }\n  });\n\n  ipcMain.on('close-lyric', () => {\n    if (lyricWindow && !lyricWindow.isDestroyed()) {\n      lyricWindow.webContents.send('lyric-window-close');\n      mainWin.webContents.send('lyric-control-back', 'close');\n      mainWin.webContents.send('lyric-window-closed');\n      lyricWindow.destroy();\n      lyricWindow = null;\n    }\n  });\n\n  // 处理鼠标事件\n  ipcMain.on('mouseenter-lyric', () => {\n    if (lyricWindow && !lyricWindow.isDestroyed()) {\n      lyricWindow.setIgnoreMouseEvents(true);\n    }\n  });\n\n  ipcMain.on('mouseleave-lyric', () => {\n    if (lyricWindow && !lyricWindow.isDestroyed()) {\n      lyricWindow.setIgnoreMouseEvents(false);\n    }\n  });\n\n  // 开始拖动时设置标志\n  ipcMain.on('lyric-drag-start', () => {\n    isDragging = true;\n    if (lyricWindow && !lyricWindow.isDestroyed()) {\n      // 记录原始窗口大小\n      const [width, height] = lyricWindow.getSize();\n      originalSize = { width, height };\n    }\n  });\n\n  // 结束拖动时清除标志\n  ipcMain.on('lyric-drag-end', () => {\n    isDragging = false;\n    if (lyricWindow && !lyricWindow.isDestroyed()) {\n      // 确保窗口大小恢复原样\n      lyricWindow.setSize(originalSize.width, originalSize.height);\n    }\n  });\n\n  // 处理拖动移动\n  ipcMain.on('lyric-drag-move', (_, { deltaX, deltaY }) => {\n    if (!lyricWindow || lyricWindow.isDestroyed() || !isDragging) return;\n\n    const [currentX, currentY] = lyricWindow.getPosition();\n\n    // 使用记录的原始大小，而不是当前大小\n    const windowWidth = originalSize.width;\n    const windowHeight = originalSize.height;\n\n    // 计算新位置\n    const newX = currentX + deltaX;\n    const newY = currentY + deltaY;\n\n    try {\n      // 获取当前鼠标所在的显示器\n      const mousePoint = screen.getCursorScreenPoint();\n      const currentDisplay = screen.getDisplayNearestPoint(mousePoint);\n\n      // 拖动期间使用setBounds确保大小不变，使用false避免动画卡顿\n      lyricWindow.setBounds(\n        {\n          x: newX,\n          y: newY,\n          width: windowWidth,\n          height: windowHeight\n        },\n        false\n      );\n\n      // 更新存储的位置\n      const windowBounds = {\n        x: newX,\n        y: newY,\n        width: windowWidth,\n        height: windowHeight,\n        displayId: currentDisplay.id // 记录当前显示器ID，有助于多屏幕处理\n      };\n      store.set('lyricWindowBounds', windowBounds);\n    } catch (error) {\n      console.error('Error during window drag:', error);\n      // 出错时尝试使用更简单的方法\n      lyricWindow.setPosition(newX, newY);\n    }\n  });\n\n  // 添加鼠标穿透事件处理\n  ipcMain.on('set-ignore-mouse', (_, shouldIgnore) => {\n    if (!lyricWindow || lyricWindow.isDestroyed()) return;\n\n    lyricWindow.setIgnoreMouseEvents(shouldIgnore, { forward: true });\n  });\n\n  // 添加播放控制处理\n  ipcMain.on('control-back', (_, command) => {\n    console.log('command', command);\n    if (mainWin && !mainWin.isDestroyed()) {\n      console.log('Sending control-back command:', command);\n      mainWin.webContents.send('lyric-control-back', command);\n    }\n  });\n};\n"
  },
  {
    "path": "src/main/modules/cache.ts",
    "content": "import { ipcMain } from 'electron';\nimport Store from 'electron-store';\n\ninterface LyricData {\n  id: number;\n  data: any;\n  timestamp: number;\n}\n\ninterface StoreSchema {\n  lyrics: Record<number, LyricData>;\n}\n\nclass CacheManager {\n  private store: Store<StoreSchema>;\n\n  constructor() {\n    this.store = new Store<StoreSchema>({\n      name: 'lyrics',\n      defaults: {\n        lyrics: {}\n      }\n    });\n  }\n\n  async cacheLyric(id: number, data: any) {\n    try {\n      const lyrics = this.store.get('lyrics');\n      lyrics[id] = {\n        id,\n        data,\n        timestamp: Date.now()\n      };\n      this.store.set('lyrics', lyrics);\n      return true;\n    } catch (error) {\n      console.error('Error caching lyric:', error);\n      return false;\n    }\n  }\n\n  async getCachedLyric(id: number) {\n    try {\n      const lyrics = this.store.get('lyrics');\n      const result = lyrics[id];\n\n      if (!result) return undefined;\n\n      // 检查缓存是否过期（24小时）\n      if (Date.now() - result.timestamp > 24 * 60 * 60 * 1000) {\n        delete lyrics[id];\n        this.store.set('lyrics', lyrics);\n        return undefined;\n      }\n\n      return result.data;\n    } catch (error) {\n      console.error('Error getting cached lyric:', error);\n      return undefined;\n    }\n  }\n\n  async clearLyricCache() {\n    try {\n      this.store.set('lyrics', {});\n      return true;\n    } catch (error) {\n      console.error('Error clearing lyric cache:', error);\n      return false;\n    }\n  }\n}\n\nexport const cacheManager = new CacheManager();\n\nexport function initializeCacheManager() {\n  // 添加歌词缓存相关的 IPC 处理\n  ipcMain.handle('cache-lyric', async (_, id: number, lyricData: any) => {\n    return await cacheManager.cacheLyric(id, lyricData);\n  });\n\n  ipcMain.handle('get-cached-lyric', async (_, id: number) => {\n    return await cacheManager.getCachedLyric(id);\n  });\n\n  ipcMain.handle('clear-lyric-cache', async () => {\n    return await cacheManager.clearLyricCache();\n  });\n}\n"
  },
  {
    "path": "src/main/modules/config.ts",
    "content": "import { app, ipcMain } from 'electron';\nimport Store from 'electron-store';\n\nimport set from '../set.json';\nimport { defaultShortcuts } from './shortcuts';\n\ntype SetConfig = {\n  isProxy: boolean;\n  proxyConfig: {\n    enable: boolean;\n    protocol: string;\n    host: string;\n    port: number;\n  };\n  enableRealIP: boolean;\n  realIP: string;\n  noAnimate: boolean;\n  animationSpeed: number;\n  author: string;\n  authorUrl: string;\n  musicApiPort: number;\n  closeAction: 'ask' | 'minimize' | 'close';\n  musicQuality: string;\n  fontFamily: string;\n  fontScope: 'global' | 'lyric';\n  language: string;\n  showTopAction: boolean;\n  enableGpuAcceleration: boolean;\n};\ninterface StoreType {\n  set: SetConfig;\n  shortcuts: typeof defaultShortcuts;\n}\n\nlet store: Store<StoreType>;\n\n/**\n * 初始化配置管理\n */\nexport function initializeConfig() {\n  store = new Store<StoreType>({\n    name: 'config',\n    defaults: {\n      set: set as SetConfig,\n      shortcuts: defaultShortcuts\n    }\n  });\n\n  store.get('set.downloadPath') || store.set('set.downloadPath', app.getPath('downloads'));\n\n  // 定义ipcRenderer监听事件\n  ipcMain.on('set-store-value', (_, key, value) => {\n    store.set(key, value);\n  });\n\n  ipcMain.on('get-store-value', (_, key) => {\n    const value = store.get(key);\n    _.returnValue = value || '';\n  });\n\n  // GPU加速设置更新处理\n  // 注意：GPU加速设置必须在应用启动时在app.ready之前设置才能生效\n  ipcMain.on('update-gpu-acceleration', (event, enabled: boolean) => {\n    try {\n      console.log('GPU加速设置更新:', enabled);\n      store.set('set.enableGpuAcceleration', enabled);\n\n      // GPU加速设置需要重启应用才能生效\n      event.sender.send('gpu-acceleration-updated', enabled);\n      console.log('GPU加速设置已保存，重启应用后生效');\n    } catch (error) {\n      console.error('GPU加速设置更新失败:', error);\n      const errorMessage = error instanceof Error ? error.message : String(error);\n      event.sender.send('gpu-acceleration-update-error', errorMessage);\n    }\n  });\n\n  return store;\n}\n\nexport function getStore() {\n  return store;\n}\n"
  },
  {
    "path": "src/main/modules/deviceInfo.ts",
    "content": "import { app } from 'electron';\nimport Store from 'electron-store';\nimport { machineIdSync } from 'node-machine-id';\nimport os from 'os';\n\nconst store = new Store();\n\n/**\n * 获取设备唯一标识符\n * 优先使用存储的ID，如果没有则获取机器ID并存储\n */\nexport function getDeviceId(): string {\n  let deviceId = store.get('deviceId') as string | undefined;\n\n  if (!deviceId) {\n    try {\n      // 使用node-machine-id获取设备唯一标识\n      deviceId = machineIdSync(true);\n    } catch (error) {\n      console.error('获取机器ID失败:', error);\n      // 如果获取失败，使用主机名和MAC地址组合作为备选方案\n      const networkInterfaces = os.networkInterfaces();\n      let macAddress = '';\n\n      // 尝试获取第一个非内部网络接口的MAC地址\n      Object.values(networkInterfaces).forEach((interfaces) => {\n        if (interfaces) {\n          interfaces.forEach((iface) => {\n            if (!iface.internal && !macAddress && iface.mac !== '00:00:00:00:00:00') {\n              macAddress = iface.mac;\n            }\n          });\n        }\n      });\n\n      deviceId = `${os.hostname()}-${macAddress}`.replace(/:/g, '');\n    }\n\n    // 存储设备ID\n    if (deviceId) {\n      store.set('deviceId', deviceId);\n    } else {\n      // 如果所有方法都失败，使用随机ID\n      deviceId = Math.random().toString(36).substring(2, 15);\n      store.set('deviceId', deviceId);\n    }\n  }\n\n  return deviceId;\n}\n\n/**\n * 获取系统信息\n */\nexport function getSystemInfo() {\n  return {\n    osType: os.type(),\n    osVersion: os.release(),\n    osArch: os.arch(),\n    platform: process.platform,\n    appVersion: app.getVersion()\n  };\n}\n"
  },
  {
    "path": "src/main/modules/fileManager.ts",
    "content": "import axios from 'axios';\nimport { app, dialog, ipcMain, Notification, protocol, shell } from 'electron';\nimport Store from 'electron-store';\nimport { fileTypeFromFile } from 'file-type';\nimport { FlacTagMap, writeFlacTags } from 'flac-tagger';\nimport * as fs from 'fs';\nimport * as http from 'http';\nimport * as https from 'https';\nimport * as mm from 'music-metadata';\nimport * as NodeID3 from 'node-id3';\nimport * as os from 'os';\nimport * as path from 'path';\nimport sharp from 'sharp';\n\nimport { getStore } from './config';\n\nconst MAX_CONCURRENT_DOWNLOADS = 3;\nconst downloadQueue: { url: string; filename: string; songInfo: any; type?: string }[] = [];\nlet activeDownloads = 0;\n\n// 创建一个store实例用于存储下载历史\nconst downloadStore = new Store({\n  name: 'downloads',\n  defaults: {\n    history: []\n  }\n});\n\n// 创建一个store实例用于存储音频缓存\nconst audioCacheStore = new Store({\n  name: 'audioCache',\n  defaults: {\n    cache: {}\n  }\n});\n\n// 保存已发送通知的文件，避免重复通知\nconst sentNotifications = new Map();\n\n/**\n * 初始化文件管理相关的IPC监听\n */\nexport function initializeFileManager() {\n  // 注册本地文件协议\n  protocol.registerFileProtocol('local', (request, callback) => {\n    try {\n      const url = request.url;\n      // local://C:/Users/xxx.mp3\n      let filePath = decodeURIComponent(url.replace('local:///', ''));\n\n      // 兼容 local:///C:/Users/xxx.mp3 这种情况\n      if (/^\\/[a-zA-Z]:\\//.test(filePath)) {\n        filePath = filePath.slice(1);\n      }\n\n      // 还原为系统路径格式\n      filePath = path.normalize(filePath);\n\n      // 检查文件是否存在\n      if (!fs.existsSync(filePath)) {\n        console.error('File not found:', filePath);\n        callback({ error: -6 }); // net::ERR_FILE_NOT_FOUND\n        return;\n      }\n\n      callback({ path: filePath });\n    } catch (error) {\n      console.error('Error handling local protocol:', error);\n      callback({ error: -2 }); // net::FAILED\n    }\n  });\n\n  // 检查文件是否存在\n  ipcMain.handle('check-file-exists', (_, filePath) => {\n    try {\n      return fs.existsSync(filePath);\n    } catch (error) {\n      console.error('Error checking if file exists:', error);\n      return false;\n    }\n  });\n\n  // 获取支持的音频格式列表\n  ipcMain.handle('get-supported-audio-formats', () => {\n    return {\n      formats: [\n        { ext: 'mp3', name: 'MP3' },\n        { ext: 'm4a', name: 'M4A/AAC' },\n        { ext: 'flac', name: 'FLAC' },\n        { ext: 'wav', name: 'WAV' },\n        { ext: 'ogg', name: 'OGG Vorbis' },\n        { ext: 'aac', name: 'AAC' }\n      ],\n      default: 'mp3'\n    };\n  });\n\n  // 通用的选择目录处理\n  ipcMain.handle('select-directory', async () => {\n    const result = await dialog.showOpenDialog({\n      properties: ['openDirectory'],\n      title: '选择目录'\n    });\n    return result;\n  });\n\n  // 通用的打开目录处理\n  ipcMain.on('open-directory', (_, filePath) => {\n    try {\n      // 验证文件路径\n      if (!filePath) {\n        console.error('无效的文件路径: 路径为空');\n        return;\n      }\n\n      // 统一处理路径分隔符\n      const normalizedPath = path.normalize(filePath);\n\n      if (fs.statSync(normalizedPath).isDirectory()) {\n        shell.openPath(normalizedPath);\n      } else {\n        shell.showItemInFolder(normalizedPath);\n      }\n    } catch (error) {\n      console.error('打开路径失败:', error);\n    }\n  });\n\n  // 获取默认下载路径\n  ipcMain.handle('get-downloads-path', () => {\n    return app.getPath('downloads');\n  });\n\n  // 获取存储的配置值\n  ipcMain.handle('get-store-value', (_, key) => {\n    const store = new Store();\n    return store.get(key);\n  });\n\n  // 设置存储的配置值\n  ipcMain.on('set-store-value', (_, key, value) => {\n    const store = new Store();\n    store.set(key, value);\n  });\n\n  // 下载音乐处理\n  ipcMain.on('download-music', handleDownloadRequest);\n\n  // 检查文件是否已下载\n  ipcMain.handle('check-music-downloaded', (_, filename: string) => {\n    const store = new Store();\n    const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads');\n    const filePath = path.join(downloadPath, `${filename}.mp3`);\n    return fs.existsSync(filePath);\n  });\n\n  // 删除已下载的音乐\n  ipcMain.handle('delete-downloaded-music', async (_, filePath: string) => {\n    try {\n      if (fs.existsSync(filePath)) {\n        // 先删除文件\n        try {\n          await fs.promises.unlink(filePath);\n        } catch (error) {\n          console.error('Error deleting file:', error);\n        }\n\n        // 删除对应的歌曲信息\n        const store = new Store();\n        const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;\n        delete songInfos[filePath];\n        store.set('downloadedSongs', songInfos);\n\n        return true;\n      }\n      return false;\n    } catch (error) {\n      console.error('Error deleting file:', error);\n      return false;\n    }\n  });\n\n  // 获取已下载音乐列表\n  ipcMain.handle('get-downloaded-music', async () => {\n    try {\n      const store = new Store();\n      const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;\n\n      // 异步处理文件存在性检查\n      const entriesArray = Object.entries(songInfos);\n      const validEntriesPromises = await Promise.all(\n        entriesArray.map(async ([path, info]) => {\n          try {\n            const exists = await fs.promises\n              .access(path)\n              .then(() => true)\n              .catch(() => false);\n            return exists ? info : null;\n          } catch (error) {\n            console.error('Error checking file existence:', error);\n            return null;\n          }\n        })\n      );\n\n      // 过滤有效的歌曲并排序\n      const validSongs = validEntriesPromises\n        .filter((song) => song !== null)\n        .sort((a, b) => (b.downloadTime || 0) - (a.downloadTime || 0));\n\n      // 更新存储，移除不存在的文件记录\n      const newSongInfos = validSongs.reduce((acc, song) => {\n        if (song && song.path) {\n          acc[song.path] = song;\n        }\n        return acc;\n      }, {});\n      store.set('downloadedSongs', newSongInfos);\n\n      return validSongs;\n    } catch (error) {\n      console.error('Error getting downloaded music:', error);\n      return [];\n    }\n  });\n\n  // 检查歌曲是否已下载并返回本地路径\n  ipcMain.handle('check-song-downloaded', (_, songId: number) => {\n    const store = new Store();\n    const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;\n\n    // 通过ID查找已下载的歌曲\n    for (const [path, info] of Object.entries(songInfos)) {\n      if (info.id === songId && fs.existsSync(path)) {\n        return {\n          isDownloaded: true,\n          localPath: `local://${path}`,\n          songInfo: info\n        };\n      }\n    }\n\n    return {\n      isDownloaded: false,\n      localPath: '',\n      songInfo: null\n    };\n  });\n\n  // 添加清除下载历史的处理函数\n  ipcMain.on('clear-downloads-history', () => {\n    downloadStore.set('history', []);\n  });\n\n  // 添加清除已下载音乐记录的处理函数\n  ipcMain.handle('clear-downloaded-music', () => {\n    const store = new Store();\n    store.set('downloadedSongs', {});\n    return true;\n  });\n\n  // 添加清除音频缓存的处理函数\n  ipcMain.on('clear-audio-cache', () => {\n    audioCacheStore.set('cache', {});\n    // 清除临时音频文件目录\n    const tempDir = path.join(app.getPath('userData'), 'AudioCache');\n    if (fs.existsSync(tempDir)) {\n      try {\n        fs.readdirSync(tempDir).forEach((file) => {\n          const filePath = path.join(tempDir, file);\n          if (file.endsWith('.mp3') || file.endsWith('.m4a')) {\n            fs.unlinkSync(filePath);\n          }\n        });\n      } catch (error) {\n        console.error('清除音频缓存文件失败:', error);\n      }\n    }\n  });\n\n  // 处理导入自定义API插件的请求\n  ipcMain.handle('import-custom-api-plugin', async () => {\n    const result = await dialog.showOpenDialog({\n      title: '选择自定义音源配置文件',\n      filters: [{ name: 'JSON Files', extensions: ['json'] }],\n      properties: ['openFile']\n    });\n\n    if (result.canceled || result.filePaths.length === 0) {\n      return null;\n    }\n\n    const filePath = result.filePaths[0];\n    try {\n      const fileContent = fs.readFileSync(filePath, 'utf-8');\n\n      // 基础验证，确保它是个合法的JSON并且包含关键字段\n      const pluginData = JSON.parse(fileContent);\n      if (!pluginData.name || !pluginData.apiUrl) {\n        throw new Error('无效的插件文件，缺少 name 或 apiUrl 字段。');\n      }\n\n      return {\n        name: pluginData.name,\n        content: fileContent // 返回完整的JSON字符串\n      };\n    } catch (error: any) {\n      console.error('读取或解析插件文件失败:', error);\n      // 向渲染进程抛出错误，以便UI可以显示提示\n      throw new Error(`文件读取或解析失败: ${error.message}`);\n    }\n  });\n\n  // 处理导入落雪音源脚本的请求\n  ipcMain.handle('import-lx-music-script', async () => {\n    const result = await dialog.showOpenDialog({\n      title: '选择落雪音源脚本文件',\n      filters: [{ name: 'JavaScript Files', extensions: ['js'] }],\n      properties: ['openFile']\n    });\n\n    if (result.canceled || result.filePaths.length === 0) {\n      return null;\n    }\n\n    const filePath = result.filePaths[0];\n    try {\n      const fileContent = fs.readFileSync(filePath, 'utf-8');\n\n      // 验证脚本格式：检查是否包含落雪音源特征\n      if (\n        !fileContent.includes('globalThis.lx') &&\n        !fileContent.includes('lx.on') &&\n        !fileContent.includes('EVENT_NAMES')\n      ) {\n        throw new Error('无效的落雪音源脚本，未找到 globalThis.lx 相关代码。');\n      }\n\n      // 检查是否包含必要的元信息注释\n      const hasMetaComment = fileContent.includes('@name');\n      if (!hasMetaComment) {\n        console.warn('警告: 脚本缺少 @name 元信息注释');\n      }\n\n      return {\n        name: path.basename(filePath, '.js'),\n        content: fileContent\n      };\n    } catch (error: any) {\n      console.error('读取落雪音源脚本失败:', error);\n      throw new Error(`脚本读取失败: ${error.message}`);\n    }\n  });\n}\n\n/**\n * 处理下载请求\n */\nfunction handleDownloadRequest(\n  event: Electron.IpcMainEvent,\n  {\n    url,\n    filename,\n    songInfo,\n    type\n  }: { url: string; filename: string; songInfo?: any; type?: string }\n) {\n  // 检查是否已经在队列中或正在下载\n  if (downloadQueue.some((item) => item.filename === filename)) {\n    event.reply('music-download-error', {\n      filename,\n      error: '该歌曲已在下载队列中'\n    });\n    return;\n  }\n\n  // 检查是否已下载\n  const store = new Store();\n  const songInfos = store.get('downloadedSongs', {}) as Record<string, any>;\n\n  // 检查是否已下载（通过ID）\n  const isDownloaded =\n    songInfo?.id && Object.values(songInfos).some((info: any) => info.id === songInfo.id);\n\n  if (isDownloaded) {\n    event.reply('music-download-error', {\n      filename,\n      error: '该歌曲已下载'\n    });\n    return;\n  }\n\n  // 添加到下载队列\n  downloadQueue.push({ url, filename, songInfo, type });\n  event.reply('music-download-queued', {\n    filename,\n    songInfo\n  });\n\n  // 尝试开始下载\n  processDownloadQueue(event);\n}\n\n/**\n * 处理下载队列\n */\nasync function processDownloadQueue(event: Electron.IpcMainEvent) {\n  if (activeDownloads >= MAX_CONCURRENT_DOWNLOADS || downloadQueue.length === 0) {\n    return;\n  }\n\n  const { url, filename, songInfo, type } = downloadQueue.shift()!;\n  activeDownloads++;\n\n  try {\n    await downloadMusic(event, { url, filename, songInfo, type });\n  } finally {\n    activeDownloads--;\n    processDownloadQueue(event);\n  }\n}\n\n/**\n * 清理文件名中的非法字符\n */\nfunction sanitizeFilename(filename: string): string {\n  // 替换 Windows 和 Unix 系统中的非法字符\n  return filename\n    .replace(/[<>:\"/\\\\|?*]/g, '_') // 替换特殊字符为下划线\n    .replace(/\\s+/g, ' ') // 将多个空格替换为单个空格\n    .trim(); // 移除首尾空格\n}\n\n/**\n * 下载音乐和歌词\n */\nasync function downloadMusic(\n  event: Electron.IpcMainEvent,\n  {\n    url,\n    filename,\n    songInfo,\n    type = 'mp3'\n  }: { url: string; filename: string; songInfo: any; type?: string }\n) {\n  let finalFilePath = '';\n  let writer: fs.WriteStream | null = null;\n  let tempFilePath = '';\n\n  try {\n    // 使用配置Store来获取设置\n    const configStore = getStore();\n    const downloadPath =\n      (configStore.get('set.downloadPath') as string) || app.getPath('downloads');\n    const apiPort = configStore.get('set.musicApiPort') || 30488;\n\n    // 获取文件名格式设置\n    const nameFormat =\n      (configStore.get('set.downloadNameFormat') as string) || '{songName} - {artistName}';\n\n    // 根据格式创建文件名\n    let formattedFilename = filename;\n    if (songInfo) {\n      // 准备替换变量\n      const artistName = songInfo.ar?.map((a: any) => a.name).join('、') || '未知艺术家';\n      const songName = songInfo.name || filename;\n      const albumName = songInfo.al?.name || '未知专辑';\n\n      // 应用自定义格式\n      formattedFilename = nameFormat\n        .replace(/\\{songName\\}/g, songName)\n        .replace(/\\{artistName\\}/g, artistName)\n        .replace(/\\{albumName\\}/g, albumName);\n    }\n\n    // 清理文件名中的非法字符\n    const sanitizedFilename = sanitizeFilename(formattedFilename);\n\n    // 创建临时文件路径 (在系统临时目录中创建)\n    const tempDir = path.join(os.tmpdir(), 'AlgerMusicPlayerTemp');\n\n    // 确保临时目录存在\n    if (!fs.existsSync(tempDir)) {\n      fs.mkdirSync(tempDir, { recursive: true });\n    }\n\n    tempFilePath = path.join(tempDir, `${Date.now()}_${sanitizedFilename}.tmp`);\n\n    // 先获取文件大小\n    const headResponse = await axios.head(url);\n    const totalSize = parseInt(headResponse.headers['content-length'] || '0', 10);\n\n    // 开始下载到临时文件\n    const response = await axios({\n      url,\n      method: 'GET',\n      responseType: 'stream',\n      timeout: 30000, // 30秒超时\n      httpAgent: new http.Agent({ keepAlive: true }),\n      httpsAgent: new https.Agent({ keepAlive: true })\n    });\n\n    writer = fs.createWriteStream(tempFilePath);\n    let downloadedSize = 0;\n\n    // 使用 data 事件来跟踪下载进度\n    response.data.on('data', (chunk: Buffer) => {\n      downloadedSize += chunk.length;\n      const progress = Math.round((downloadedSize / totalSize) * 100);\n      event.reply('music-download-progress', {\n        filename,\n        progress,\n        loaded: downloadedSize,\n        total: totalSize,\n        path: tempFilePath,\n        status: progress === 100 ? 'completed' : 'downloading',\n        songInfo: songInfo || {\n          name: filename,\n          ar: [{ name: '本地音乐' }],\n          picUrl: '/images/default_cover.png'\n        }\n      });\n    });\n\n    // 等待下载完成\n    await new Promise((resolve, reject) => {\n      writer!.on('finish', () => resolve(undefined));\n      writer!.on('error', (error) => reject(error));\n      response.data.pipe(writer!);\n    });\n\n    // 验证文件是否完整下载\n    const stats = fs.statSync(tempFilePath);\n    if (stats.size !== totalSize) {\n      throw new Error('文件下载不完整');\n    }\n\n    // 检测文件类型\n    let fileExtension = '';\n\n    try {\n      // 首先尝试使用file-type库检测\n      const fileType = await fileTypeFromFile(tempFilePath);\n      if (fileType && fileType.ext) {\n        fileExtension = `.${fileType.ext}`;\n        console.log(`文件类型检测结果: ${fileType.mime}, 扩展名: ${fileExtension}`);\n      } else {\n        // 如果file-type无法识别，尝试使用music-metadata\n        const metadata = await mm.parseFile(tempFilePath);\n        if (metadata && metadata.format) {\n          // 根据format.container或codec判断扩展名\n          const formatInfo = metadata.format;\n          const container = formatInfo.container || '';\n          const codec = formatInfo.codec || '';\n\n          // 音频格式映射表\n          const formatMap = {\n            mp3: ['MPEG', 'MP3', 'mp3'],\n            aac: ['AAC'],\n            flac: ['FLAC'],\n            ogg: ['Ogg', 'Vorbis'],\n            wav: ['WAV', 'PCM'],\n            m4a: ['M4A', 'MP4']\n          };\n\n          // 查找匹配的格式\n          const format = Object.entries(formatMap).find(([_, keywords]) =>\n            keywords.some((keyword) => container.includes(keyword) || codec.includes(keyword))\n          );\n\n          // 设置文件扩展名，如果没找到则默认为mp3\n          fileExtension = format ? `.${format[0]}` : '.mp3';\n\n          console.log(\n            `music-metadata检测结果: 容器:${container}, 编码:${codec}, 扩展名: ${fileExtension}`\n          );\n        } else {\n          // 两种方法都失败，使用传入的type或默认mp3\n          fileExtension = type ? `.${type}` : '.mp3';\n          console.log(`无法检测文件类型，使用默认扩展名: ${fileExtension}`);\n        }\n      }\n    } catch (err) {\n      console.error('检测文件类型失败:', err);\n      // 检测失败，使用传入的type或默认mp3\n      fileExtension = type ? `.${type}` : '.mp3';\n    }\n\n    // 使用检测到的文件扩展名创建最终文件路径\n    const filePath = path.join(downloadPath, `${sanitizedFilename}${fileExtension}`);\n\n    // 检查文件是否已存在，如果存在则添加序号\n    finalFilePath = filePath;\n    let counter = 1;\n    while (fs.existsSync(finalFilePath)) {\n      const ext = path.extname(filePath);\n      const nameWithoutExt = filePath.slice(0, -ext.length);\n      finalFilePath = `${nameWithoutExt} (${counter})${ext}`;\n      counter++;\n    }\n\n    // 将临时文件移动到最终位置\n    fs.copyFileSync(tempFilePath, finalFilePath);\n    fs.unlinkSync(tempFilePath); // 删除临时文件\n\n    // 下载歌词\n    let lyricData = null;\n    let lyricsContent = '';\n    try {\n      if (songInfo?.id) {\n        // 下载歌词，使用配置的端口\n        const lyricsResponse = await axios.get(\n          `http://localhost:${apiPort}/lyric?id=${songInfo.id}`\n        );\n        if (lyricsResponse.data && (lyricsResponse.data.lrc || lyricsResponse.data.tlyric)) {\n          lyricData = lyricsResponse.data;\n\n          // 处理歌词内容\n          if (lyricsResponse.data.lrc && lyricsResponse.data.lrc.lyric) {\n            lyricsContent = lyricsResponse.data.lrc.lyric;\n\n            // 如果有翻译歌词，合并到主歌词中\n            if (lyricsResponse.data.tlyric && lyricsResponse.data.tlyric.lyric) {\n              // 解析原歌词和翻译\n              const originalLyrics = parseLyrics(lyricsResponse.data.lrc.lyric);\n              const translatedLyrics = parseLyrics(lyricsResponse.data.tlyric.lyric);\n\n              // 合并歌词\n              const mergedLyrics = mergeLyrics(originalLyrics, translatedLyrics);\n              lyricsContent = mergedLyrics;\n            }\n          }\n\n          console.log('歌词已准备好，将写入元数据');\n        }\n      }\n    } catch (lyricError) {\n      console.error('下载歌词失败:', lyricError);\n      // 继续处理，不影响音乐下载\n    }\n\n    // 下载封面\n    let coverImageBuffer: Buffer | null = null;\n    try {\n      if (songInfo?.picUrl || songInfo?.al?.picUrl) {\n        const picUrl = songInfo.picUrl || songInfo.al?.picUrl;\n        if (picUrl && picUrl !== '/images/default_cover.png') {\n          const coverResponse = await axios({\n            url: picUrl.replace('http://', 'https://'),\n            method: 'GET',\n            responseType: 'arraybuffer',\n            timeout: 10000\n          });\n\n          const originalCoverBuffer = Buffer.from(coverResponse.data);\n          const TWO_MB = 2 * 1024 * 1024;\n          // 检查图片大小是否超过2MB\n          if (originalCoverBuffer.length > TWO_MB) {\n            const originalSizeMB = (originalCoverBuffer.length / (1024 * 1024)).toFixed(2);\n            console.log(`封面图大于2MB (${originalSizeMB} MB)，开始压缩...`);\n            try {\n              // 使用 sharp 进行压缩\n              coverImageBuffer = await sharp(originalCoverBuffer)\n                .resize({\n                  width: 1600,\n                  height: 1600,\n                  fit: 'inside',\n                  withoutEnlargement: true\n                })\n                .jpeg({\n                  quality: 80,\n                  mozjpeg: true\n                })\n                .toBuffer();\n\n              const compressedSizeMB = (coverImageBuffer.length / (1024 * 1024)).toFixed(2);\n              console.log(`封面图压缩完成，新大小: ${compressedSizeMB} MB`);\n            } catch (compressionError) {\n              console.error('封面图压缩失败，将使用原图:', compressionError);\n              coverImageBuffer = originalCoverBuffer; // 如果压缩失败，则回退使用原始图片\n            }\n          } else {\n            // 如果图片不大于2MB，直接使用原图\n            coverImageBuffer = originalCoverBuffer;\n          }\n\n          console.log('封面已准备好，将写入元数据');\n        }\n      }\n    } catch (coverError) {\n      console.error('下载封面失败:', coverError);\n      // 继续处理，不影响音乐下载\n    }\n\n    const fileFormat = fileExtension.toLowerCase();\n    const artistNames =\n      (songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('、') || '未知艺术家';\n\n    // 根据文件类型处理元数据\n    if (['.mp3'].includes(fileFormat)) {\n      // 对MP3文件使用NodeID3处理ID3标签\n      try {\n        // 在写入ID3标签前，先移除可能存在的旧标签\n        NodeID3.removeTags(finalFilePath);\n\n        const tags = {\n          title: songInfo?.name,\n          artist: artistNames,\n          TPE1: artistNames,\n          TPE2: artistNames,\n          album: songInfo?.al?.name || songInfo?.song?.album?.name || songInfo?.name || filename,\n          APIC: {\n            // 专辑封面\n            imageBuffer: coverImageBuffer,\n            type: {\n              id: 3,\n              name: 'front cover'\n            },\n            description: 'Album cover',\n            mime: 'image/jpeg'\n          },\n          USLT: {\n            // 歌词\n            language: 'chi',\n            description: 'Lyrics',\n            text: lyricsContent || ''\n          },\n          trackNumber: songInfo?.no || undefined,\n          year: songInfo?.publishTime\n            ? new Date(songInfo.publishTime).getFullYear().toString()\n            : undefined\n        };\n\n        const success = NodeID3.write(tags, finalFilePath);\n        if (!success) {\n          console.error('Failed to write ID3 tags');\n        } else {\n          console.log('ID3 tags written successfully');\n        }\n      } catch (err) {\n        console.error('Error writing ID3 tags:', err);\n      }\n    } else if (['.flac'].includes(fileFormat)) {\n      try {\n        const tagMap: FlacTagMap = {\n          TITLE: songInfo?.name,\n          ARTIST: artistNames,\n          ALBUM: songInfo?.al?.name || songInfo?.song?.album?.name || songInfo?.name || filename,\n          LYRICS: lyricsContent || '',\n          TRACKNUMBER: songInfo?.no ? String(songInfo.no) : undefined,\n          DATE: songInfo?.publishTime\n            ? new Date(songInfo.publishTime).getFullYear().toString()\n            : undefined\n        };\n\n        await writeFlacTags(\n          {\n            tagMap,\n            picture: coverImageBuffer\n              ? {\n                  buffer: coverImageBuffer,\n                  mime: 'image/jpeg'\n                }\n              : undefined\n          },\n          finalFilePath\n        );\n        console.log('FLAC tags written successfully');\n      } catch (err) {\n        console.error('Error writing FLAC tags:', err);\n      }\n    }\n\n    // 保存下载信息\n    try {\n      const songInfos = configStore.get('downloadedSongs', {}) as Record<string, any>;\n      const defaultInfo = {\n        name: filename,\n        ar: [{ name: '本地音乐' }],\n        picUrl: '/images/default_cover.png'\n      };\n\n      const newSongInfo = {\n        id: songInfo?.id || 0,\n        name: songInfo?.name || filename,\n        filename,\n        picUrl: songInfo?.picUrl || songInfo?.al?.picUrl || defaultInfo.picUrl,\n        ar: songInfo?.ar || defaultInfo.ar,\n        al: songInfo?.al || {\n          picUrl: songInfo?.picUrl || defaultInfo.picUrl,\n          name: songInfo?.name || filename\n        },\n        size: totalSize,\n        path: finalFilePath,\n        downloadTime: Date.now(),\n        type: fileExtension.substring(1), // 去掉前面的点号，只保留扩展名\n        lyric: lyricData\n      };\n\n      // 保存到下载记录\n      songInfos[finalFilePath] = newSongInfo;\n      configStore.set('downloadedSongs', songInfos);\n\n      // 添加到下载历史\n      const history = downloadStore.get('history', []) as any[];\n      history.unshift(newSongInfo);\n      downloadStore.set('history', history);\n\n      // 避免重复发送通知\n      const notificationId = `download-${finalFilePath}`;\n      if (!sentNotifications.has(notificationId)) {\n        sentNotifications.set(notificationId, true);\n\n        // 发送桌面通知\n        try {\n          const artistNames =\n            (songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('、') ||\n            '未知艺术家';\n          const notification = new Notification({\n            title: '下载完成',\n            body: `${songInfo?.name || filename} - ${artistNames}`,\n            silent: false\n          });\n\n          notification.on('click', () => {\n            shell.showItemInFolder(finalFilePath);\n          });\n\n          notification.show();\n\n          // 60秒后清理通知记录，释放内存\n          setTimeout(() => {\n            sentNotifications.delete(notificationId);\n          }, 60000);\n        } catch (notifyError) {\n          console.error('发送通知失败:', notifyError);\n        }\n      }\n\n      // 发送下载完成事件，确保只发送一次\n      event.reply('music-download-complete', {\n        success: true,\n        path: finalFilePath,\n        filename,\n        size: totalSize,\n        songInfo: newSongInfo\n      });\n    } catch (error) {\n      console.error('Error saving download info:', error);\n      throw new Error('保存下载信息失败');\n    }\n  } catch (error: any) {\n    console.error('Download error:', error);\n\n    // 清理未完成的下载\n    if (writer) {\n      writer.end();\n    }\n\n    // 清理临时文件\n    if (tempFilePath && fs.existsSync(tempFilePath)) {\n      try {\n        fs.unlinkSync(tempFilePath);\n      } catch (e) {\n        console.error('Failed to delete temporary file:', e);\n      }\n    }\n\n    // 清理未完成的最终文件\n    if (finalFilePath && fs.existsSync(finalFilePath)) {\n      try {\n        fs.unlinkSync(finalFilePath);\n      } catch (e) {\n        console.error('Failed to delete incomplete download:', e);\n      }\n    }\n\n    event.reply('music-download-complete', {\n      success: false,\n      error: error.message || '下载失败',\n      filename\n    });\n  }\n}\n\n// 辅助函数 - 解析歌词文本成时间戳和内容的映射\nfunction parseLyrics(lyricsText: string): Map<string, string> {\n  const lyricMap = new Map<string, string>();\n  const lines = lyricsText.split('\\n');\n\n  for (const line of lines) {\n    // 匹配时间标签，形如 [00:00.000]\n    const timeTagMatches = line.match(/\\[\\d{2}:\\d{2}(\\.\\d{1,3})?\\]/g);\n    if (!timeTagMatches) continue;\n\n    // 提取歌词内容（去除时间标签）\n    const content = line.replace(/\\[\\d{2}:\\d{2}(\\.\\d{1,3})?\\]/g, '').trim();\n    if (!content) continue;\n\n    // 将每个时间标签与歌词内容关联\n    for (const timeTag of timeTagMatches) {\n      lyricMap.set(timeTag, content);\n    }\n  }\n\n  return lyricMap;\n}\n\n// 辅助函数 - 合并原文歌词和翻译歌词\nfunction mergeLyrics(\n  originalLyrics: Map<string, string>,\n  translatedLyrics: Map<string, string>\n): string {\n  const mergedLines: string[] = [];\n\n  // 对每个时间戳，组合原始歌词和翻译\n  for (const [timeTag, originalContent] of originalLyrics.entries()) {\n    const translatedContent = translatedLyrics.get(timeTag);\n\n    // 添加原始歌词行\n    mergedLines.push(`${timeTag}${originalContent}`);\n\n    // 如果有翻译，添加翻译行（时间戳相同，这样可以和原歌词同步显示）\n    if (translatedContent) {\n      mergedLines.push(`${timeTag}${translatedContent}`);\n    }\n  }\n\n  // 按时间顺序排序\n  mergedLines.sort((a, b) => {\n    const timeA = a.match(/\\[\\d{2}:\\d{2}(\\.\\d{1,3})?\\]/)?.[0] || '';\n    const timeB = b.match(/\\[\\d{2}:\\d{2}(\\.\\d{1,3})?\\]/)?.[0] || '';\n    return timeA.localeCompare(timeB);\n  });\n\n  return mergedLines.join('\\n');\n}\n"
  },
  {
    "path": "src/main/modules/fonts.ts",
    "content": "import { ipcMain } from 'electron';\nimport { getFonts } from 'font-list';\n\n/**\n * 清理字体名称\n * @param fontName 原始字体名称\n * @returns 清理后的字体名称\n */\nfunction cleanFontName(fontName: string): string {\n  return fontName\n    .trim()\n    .replace(/^[\"']|[\"']$/g, '') // 移除首尾的引号\n    .replace(/\\s+/g, ' '); // 将多个空格替换为单个空格\n}\n\n/**\n * 获取系统字体列表\n */\nasync function getSystemFonts(): Promise<string[]> {\n  try {\n    // 使用 font-list 获取系统字体\n    const fonts = await getFonts();\n    // 清理字体名称并去重\n    const cleanedFonts = [...new Set(fonts.map(cleanFontName))];\n    // 添加系统默认字体并排序\n    return ['system-ui', ...cleanedFonts].sort();\n  } catch (error) {\n    console.error('获取系统字体失败:', error);\n    // 如果获取失败，至少返回系统默认字体\n    return ['system-ui'];\n  }\n}\n\n/**\n * 初始化字体管理模块\n */\nexport function initializeFonts() {\n  // 添加获取系统字体的 IPC 处理\n  ipcMain.handle('get-system-fonts', async () => {\n    return await getSystemFonts();\n  });\n}\n"
  },
  {
    "path": "src/main/modules/loginWindow.ts",
    "content": "import { BrowserWindow, ipcMain, session } from 'electron';\nimport { join } from 'path';\n\nimport i18n from '../../i18n/main';\n\nlet loginWindow: BrowserWindow | null = null;\n\nconst loginUrl = 'https://music.163.com/#/login/';\nconst loginTitle = i18n.global.t('login.qrTitle');\n\n/**\n * 打开登录窗口获取Cookie\n */\nconst openLoginWindow = async (mainWin: BrowserWindow) => {\n  let loginTimer: NodeJS.Timeout;\n\n  // 如果登录窗口已存在，则聚焦并返回\n  if (loginWindow && !loginWindow.isDestroyed()) {\n    loginWindow.focus();\n    return;\n  }\n\n  const loginSession = session.fromPartition('persist:login');\n\n  // 清除 Cookie\n  await loginSession.clearStorageData({\n    storages: ['cookies', 'localstorage']\n  });\n\n  loginWindow = new BrowserWindow({\n    parent: mainWin,\n    title: loginTitle,\n    width: 1280,\n    height: 800,\n    center: true,\n    autoHideMenuBar: true,\n    webPreferences: {\n      session: loginSession,\n      sandbox: false,\n      webSecurity: false,\n      preload: join(__dirname, '../../preload/index.js')\n    }\n  });\n\n  // 打开网易云登录页面\n  loginWindow.loadURL(loginUrl);\n\n  // 阻止新窗口创建\n  loginWindow.webContents.setWindowOpenHandler(() => {\n    return { action: 'deny' };\n  });\n\n  // 检查是否登录\n  const checkLogin = async () => {\n    try {\n      if (!loginWindow || loginWindow.isDestroyed()) {\n        if (loginTimer) clearInterval(loginTimer);\n        return;\n      }\n\n      const MUSIC_U = await loginSession.cookies.get({\n        name: 'MUSIC_U'\n      });\n\n      if (MUSIC_U && MUSIC_U?.length > 0) {\n        if (loginTimer) clearInterval(loginTimer);\n        const value = `MUSIC_U=${MUSIC_U[0].value};`;\n\n        mainWin?.webContents.send('send-cookies', value);\n\n        // 关闭登录窗口\n        loginWindow.destroy();\n        loginWindow = null;\n      }\n    } catch (error) {\n      console.error('检查登录状态失败:', error);\n    }\n  };\n\n  // 循环检查登录状态\n  loginWindow.webContents.once('did-finish-load', () => {\n    loginWindow?.show();\n    loginTimer = setInterval(checkLogin, 500);\n\n    loginWindow?.on('closed', () => {\n      if (loginTimer) clearInterval(loginTimer);\n      loginWindow = null;\n    });\n  });\n};\n\n/**\n * 初始化登录窗口相关的IPC监听\n */\nexport function initializeLoginWindow() {\n  ipcMain.on('open-login', (event) => {\n    const mainWin = BrowserWindow.fromWebContents(event.sender);\n    if (mainWin) {\n      openLoginWindow(mainWin);\n    }\n  });\n}\n\nexport default openLoginWindow;\n"
  },
  {
    "path": "src/main/modules/lxMusicHttp.ts",
    "content": "/**\n * 落雪音乐 HTTP 请求处理（主进程）\n * 绕过渲染进程的 CORS 限制\n */\n\nimport { ipcMain } from 'electron';\nimport fetch, { type RequestInit } from 'node-fetch';\n\ninterface LxHttpRequest {\n  url: string;\n  options: {\n    method?: string;\n    headers?: Record<string, string>;\n    body?: string;\n    form?: Record<string, string>;\n    formData?: Record<string, string>;\n    timeout?: number;\n  };\n  requestId: string;\n}\n\ninterface LxHttpResponse {\n  statusCode: number;\n  headers: Record<string, string | string[]>;\n  body: any;\n}\n\n// 取消控制器映射\nconst abortControllers = new Map<string, AbortController>();\n\n/**\n * 初始化 HTTP 请求处理\n */\nexport const initLxMusicHttp = () => {\n  // 处理 HTTP 请求\n  ipcMain.handle(\n    'lx-music-http-request',\n    async (_, request: LxHttpRequest): Promise<LxHttpResponse> => {\n      const { url, options, requestId } = request;\n      const controller = new AbortController();\n\n      // 保存取消控制器\n      abortControllers.set(requestId, controller);\n\n      try {\n        console.log(`[LxMusicHttp] 请求: ${options.method || 'GET'} ${url}`);\n\n        const fetchOptions: RequestInit = {\n          method: options.method || 'GET',\n          headers: {\n            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n            ...(options.headers || {})\n          },\n          signal: controller.signal\n        };\n\n        // 处理请求体\n        if (options.body) {\n          fetchOptions.body = options.body;\n        } else if (options.form) {\n          const formData = new URLSearchParams(options.form);\n          fetchOptions.body = formData.toString();\n          fetchOptions.headers = {\n            ...fetchOptions.headers,\n            'Content-Type': 'application/x-www-form-urlencoded'\n          };\n        } else if (options.formData) {\n          // node-fetch 的 FormData 需要特殊处理\n          const FormData = (await import('form-data')).default;\n          const formData = new FormData();\n          for (const [key, value] of Object.entries(options.formData)) {\n            formData.append(key, value);\n          }\n          fetchOptions.body = formData as any;\n          // FormData 会自动设置 Content-Type\n        }\n\n        // 设置超时\n        const timeout = options.timeout || 30000;\n        const timeoutId = setTimeout(() => {\n          console.warn(`[LxMusicHttp] 请求超时: ${url}`);\n          controller.abort();\n        }, timeout);\n\n        const response = await fetch(url, fetchOptions);\n        clearTimeout(timeoutId);\n\n        console.log(`[LxMusicHttp] 响应: ${response.status} ${url}`);\n\n        // 读取响应体\n        const rawBody = await response.text();\n\n        // 尝试解析 JSON\n        let parsedBody: any = rawBody;\n        const contentType = response.headers.get('content-type') || '';\n        if (\n          contentType.includes('application/json') ||\n          rawBody.startsWith('{') ||\n          rawBody.startsWith('[')\n        ) {\n          try {\n            parsedBody = JSON.parse(rawBody);\n          } catch {\n            // 解析失败则使用原始字符串\n          }\n        }\n\n        // 转换 headers 为普通对象\n        const headers: Record<string, string | string[]> = {};\n        response.headers.forEach((value, key) => {\n          headers[key] = value;\n        });\n\n        const result: LxHttpResponse = {\n          statusCode: response.status,\n          headers,\n          body: parsedBody\n        };\n\n        return result;\n      } catch (error: any) {\n        console.error(`[LxMusicHttp] 请求失败: ${url}`, error.message);\n        throw error;\n      } finally {\n        // 清理取消控制器\n        abortControllers.delete(requestId);\n      }\n    }\n  );\n\n  // 处理请求取消\n  ipcMain.handle('lx-music-http-cancel', (_, requestId: string) => {\n    const controller = abortControllers.get(requestId);\n    if (controller) {\n      console.log(`[LxMusicHttp] 取消请求: ${requestId}`);\n      controller.abort();\n      abortControllers.delete(requestId);\n    }\n  });\n\n  console.log('[LxMusicHttp] HTTP 请求处理已初始化');\n};\n\n/**\n * 清理所有正在进行的请求\n */\nexport const cleanupLxMusicHttp = () => {\n  for (const [requestId, controller] of abortControllers.entries()) {\n    console.log(`[LxMusicHttp] 清理请求: ${requestId}`);\n    controller.abort();\n  }\n  abortControllers.clear();\n};\n"
  },
  {
    "path": "src/main/modules/otherApi.ts",
    "content": "import axios from 'axios';\nimport { ipcMain } from 'electron';\n\n/**\n * 初始化其他杂项 API（如搜索建议等）\n */\nexport function initializeOtherApi() {\n  // 搜索建议（从酷狗获取）\n  ipcMain.handle('get-search-suggestions', async (_, keyword: string) => {\n    if (!keyword || !keyword.trim()) {\n      return [];\n    }\n    try {\n      console.log(`[Main Process Proxy] Forwarding suggestion request for: ${keyword}`);\n      const response = await axios.get('http://msearchcdn.kugou.com/new/app/i/search.php', {\n        params: {\n          cmd: 302,\n          keyword: keyword\n        },\n        timeout: 5000\n      });\n      return response.data;\n    } catch (error: any) {\n      console.error('[Main Process Proxy] Failed to fetch search suggestions:', error.message);\n      return [];\n    }\n  });\n}\n"
  },
  {
    "path": "src/main/modules/remoteControl.ts",
    "content": "import cors from 'cors';\nimport { ipcMain } from 'electron';\nimport express from 'express';\nimport fs from 'fs';\nimport os from 'os';\nimport path from 'path';\n\nimport { getStore } from './config';\n\n// 定义远程控制相关接口\nexport interface RemoteControlConfig {\n  enabled: boolean;\n  port: number;\n  allowedIps: string[];\n}\n\n// 默认配置\nexport const defaultRemoteControlConfig: RemoteControlConfig = {\n  enabled: false,\n  port: 31888,\n  allowedIps: []\n};\n\nlet app: express.Application | null = null;\nlet server: any = null;\nlet mainWindowRef: Electron.BrowserWindow | null = null;\nlet currentSong: any = null;\nlet isPlaying: boolean = false;\n\n// 获取本地IP地址\nfunction getLocalIpAddresses(): string[] {\n  const interfaces = os.networkInterfaces();\n  const addresses: string[] = [];\n\n  for (const key in interfaces) {\n    const iface = interfaces[key];\n    if (iface) {\n      for (const alias of iface) {\n        if (alias.family === 'IPv4' && !alias.internal) {\n          addresses.push(alias.address);\n        }\n      }\n    }\n  }\n\n  return addresses;\n}\n\n// 初始化远程控制服务\nexport function initializeRemoteControl(mainWindow: Electron.BrowserWindow) {\n  mainWindowRef = mainWindow;\n  const store = getStore() as any;\n  let config = store.get('remoteControl') as RemoteControlConfig;\n\n  // 如果配置不存在，使用默认配置\n  if (!config) {\n    config = defaultRemoteControlConfig;\n    store.set('remoteControl', config);\n  }\n\n  // 监听当前歌曲变化\n  ipcMain.on('update-current-song', (_, song: any) => {\n    currentSong = song;\n  });\n\n  // 监听播放状态变化\n  ipcMain.on('update-play-state', (_, playing: boolean) => {\n    isPlaying = playing;\n  });\n\n  // 监听远程控制配置变化\n  ipcMain.on('update-remote-control-config', (_, newConfig: RemoteControlConfig) => {\n    if (server) {\n      stopServer();\n    }\n\n    store.set('remoteControl', newConfig);\n\n    if (newConfig.enabled) {\n      startServer(newConfig);\n    }\n  });\n\n  // 获取远程控制配置\n  ipcMain.handle('get-remote-control-config', () => {\n    const config = store.get('remoteControl') as RemoteControlConfig;\n    return config || defaultRemoteControlConfig;\n  });\n\n  // 获取本地IP地址\n  ipcMain.handle('get-local-ip-addresses', () => {\n    return getLocalIpAddresses();\n  });\n\n  // 如果启用了远程控制，启动服务器\n  if (config.enabled) {\n    startServer(config);\n  }\n}\n\n// 启动远程控制服务器\nfunction startServer(config: RemoteControlConfig) {\n  if (!mainWindowRef) {\n    console.error('主窗口未初始化，无法启动远程控制服务');\n    return;\n  }\n\n  app = express();\n\n  // 跨域配置\n  app.use(cors());\n  app.use(express.json());\n\n  // IP 过滤中间件\n  app.use((req, res, next) => {\n    const clientIp = req.ip || req.socket.remoteAddress || '';\n    const cleanIp = clientIp.replace(/^::ffff:/, ''); // 移除IPv6前缀\n    console.log('config', config);\n    if (config.allowedIps.length === 0 || config.allowedIps.includes(cleanIp)) {\n      next();\n    } else {\n      res.status(403).json({ error: '未授权的IP地址' });\n    }\n  });\n\n  // 路由配置\n  setupRoutes(app);\n\n  // 启动服务器\n  try {\n    server = app.listen(config.port, () => {\n      console.log(`远程控制服务已启动，监听端口: ${config.port}`);\n    });\n  } catch (error) {\n    console.error('启动远程控制服务失败:', error);\n  }\n}\n\n// 停止远程控制服务器\nfunction stopServer() {\n  if (server) {\n    server.close();\n    server = null;\n    app = null;\n    console.log('远程控制服务已停止');\n  }\n}\n\n// 设置路由\nfunction setupRoutes(app: express.Application) {\n  // 获取当前播放状态\n  app.get('/api/status', (_, res) => {\n    res.json({\n      isPlaying,\n      currentSong\n    });\n  });\n\n  // 播放/暂停\n  app.post('/api/toggle-play', (_, res) => {\n    if (!mainWindowRef) {\n      return res.status(500).json({ error: '主窗口未初始化' });\n    }\n    mainWindowRef.webContents.send('global-shortcut', 'togglePlay');\n    res.json({ success: true, message: '已发送播放/暂停指令' });\n  });\n\n  // 上一首\n  app.post('/api/prev', (_, res) => {\n    if (!mainWindowRef) {\n      return res.status(500).json({ error: '主窗口未初始化' });\n    }\n    mainWindowRef.webContents.send('global-shortcut', 'prevPlay');\n    res.json({ success: true, message: '已发送上一首指令' });\n  });\n\n  // 下一首\n  app.post('/api/next', (_, res) => {\n    if (!mainWindowRef) {\n      return res.status(500).json({ error: '主窗口未初始化' });\n    }\n    mainWindowRef.webContents.send('global-shortcut', 'nextPlay');\n    res.json({ success: true, message: '已发送下一首指令' });\n  });\n\n  // 音量加\n  app.post('/api/volume-up', (_, res) => {\n    if (!mainWindowRef) {\n      return res.status(500).json({ error: '主窗口未初始化' });\n    }\n    mainWindowRef.webContents.send('global-shortcut', 'volumeUp');\n    res.json({ success: true, message: '已发送音量加指令' });\n  });\n\n  // 音量减\n  app.post('/api/volume-down', (_, res) => {\n    if (!mainWindowRef) {\n      return res.status(500).json({ error: '主窗口未初始化' });\n    }\n    mainWindowRef.webContents.send('global-shortcut', 'volumeDown');\n    res.json({ success: true, message: '已发送音量减指令' });\n  });\n\n  // 收藏/取消收藏\n  app.post('/api/toggle-favorite', (_, res) => {\n    if (!mainWindowRef) {\n      return res.status(500).json({ error: '主窗口未初始化' });\n    }\n    mainWindowRef.webContents.send('global-shortcut', 'toggleFavorite');\n    res.json({ success: true, message: '已发送收藏/取消收藏指令' });\n  });\n\n  // 提供远程控制界面HTML\n  app.get('/', (_, res) => {\n    try {\n      const resourcesPath = process.resourcesPath || '';\n      const isDev = process.env.NODE_ENV === 'development';\n      const htmlPath = path.join(process.cwd(), 'resources', 'html', 'remote-control.html');\n      const finalPath = isDev ? htmlPath : path.join(resourcesPath, 'html', 'remote-control.html');\n\n      if (fs.existsSync(finalPath)) {\n        res.sendFile(finalPath);\n      } else {\n        res.status(404).send('远程控制界面文件未找到');\n        console.error('远程控制界面文件不存在:', finalPath);\n      }\n    } catch (error) {\n      console.error('加载远程控制界面失败:', error);\n      res.status(500).send('加载远程控制界面失败');\n    }\n  });\n}\n"
  },
  {
    "path": "src/main/modules/shortcuts.ts",
    "content": "import { globalShortcut, ipcMain } from 'electron';\n\nimport { getStore } from './config';\n\n// 添加获取平台信息的 IPC 处理程序\nipcMain.on('get-platform', (event) => {\n  event.returnValue = process.platform;\n});\n\n// 定义快捷键配置接口\nexport interface ShortcutConfig {\n  key: string;\n  enabled: boolean;\n  scope: 'global' | 'app';\n}\n\nexport interface ShortcutsConfig {\n  [key: string]: ShortcutConfig;\n}\n\n// 定义默认快捷键\nexport const defaultShortcuts: ShortcutsConfig = {\n  togglePlay: { key: 'CommandOrControl+Alt+P', enabled: true, scope: 'global' },\n  prevPlay: { key: 'Alt+Left', enabled: true, scope: 'global' },\n  nextPlay: { key: 'Alt+Right', enabled: true, scope: 'global' },\n  volumeUp: { key: 'Alt+Up', enabled: true, scope: 'app' },\n  volumeDown: { key: 'Alt+Down', enabled: true, scope: 'app' },\n  toggleFavorite: { key: 'CommandOrControl+Alt+L', enabled: true, scope: 'app' },\n  toggleWindow: { key: 'CommandOrControl+Alt+Shift+M', enabled: true, scope: 'global' }\n};\n\nlet mainWindowRef: Electron.BrowserWindow | null = null;\n\n// 注册快捷键\nexport function registerShortcuts(\n  mainWindow: Electron.BrowserWindow,\n  shortcutsConfig?: ShortcutsConfig\n) {\n  mainWindowRef = mainWindow;\n  const store = getStore();\n  const shortcuts =\n    shortcutsConfig || (store.get('shortcuts') as ShortcutsConfig) || defaultShortcuts;\n\n  // 注销所有已注册的快捷键\n  globalShortcut.unregisterAll();\n\n  // 对旧格式数据进行兼容处理\n  if (shortcuts && typeof shortcuts.togglePlay === 'string') {\n    // 将 shortcuts 强制转换为 unknown，再转为 Record<string, string>\n    const oldShortcuts = { ...shortcuts } as unknown as Record<string, string>;\n    const newShortcuts: ShortcutsConfig = {};\n\n    Object.entries(oldShortcuts).forEach(([key, value]) => {\n      newShortcuts[key] = {\n        key: value,\n        enabled: true,\n        scope: ['volumeUp', 'volumeDown', 'toggleFavorite'].includes(key) ? 'app' : 'global'\n      };\n    });\n\n    store.set('shortcuts', newShortcuts);\n    registerShortcuts(mainWindow, newShortcuts);\n    return;\n  }\n\n  // 注册全局快捷键\n  Object.entries(shortcuts).forEach(([action, config]) => {\n    const { key, enabled, scope } = config as ShortcutConfig;\n\n    // 只注册启用且作用域为全局的快捷键\n    if (!enabled || scope !== 'global') return;\n\n    try {\n      switch (action) {\n        case 'toggleWindow':\n          globalShortcut.register(key, () => {\n            if (mainWindow.isVisible()) {\n              mainWindow.hide();\n            } else {\n              mainWindow.show();\n            }\n          });\n          break;\n        default:\n          globalShortcut.register(key, () => {\n            mainWindow.webContents.send('global-shortcut', action);\n          });\n          break;\n      }\n    } catch (error) {\n      console.error(`注册快捷键 ${key} 失败:`, error);\n    }\n  });\n\n  // 通知渲染进程更新应用内快捷键\n  mainWindow.webContents.send('update-app-shortcuts', shortcuts);\n}\n\n// 初始化快捷键\nexport function initializeShortcuts(mainWindow: Electron.BrowserWindow) {\n  mainWindowRef = mainWindow;\n  registerShortcuts(mainWindow);\n\n  // 监听禁用快捷键事件\n  ipcMain.on('disable-shortcuts', () => {\n    globalShortcut.unregisterAll();\n  });\n\n  // 监听启用快捷键事件\n  ipcMain.on('enable-shortcuts', () => {\n    if (mainWindowRef) {\n      registerShortcuts(mainWindowRef);\n    }\n  });\n\n  // 监听快捷键更新事件\n  ipcMain.on('update-shortcuts', (_, shortcutsConfig: ShortcutsConfig) => {\n    if (mainWindowRef) {\n      registerShortcuts(mainWindowRef, shortcutsConfig);\n    }\n  });\n}\n"
  },
  {
    "path": "src/main/modules/tray.ts",
    "content": "import {\n  app,\n  BrowserWindow,\n  Menu,\n  MenuItem,\n  MenuItemConstructorOptions,\n  nativeImage,\n  Tray\n} from 'electron';\nimport { join } from 'path';\n\nimport i18n from '../../i18n/main';\nimport { getLanguageOptions } from '../../i18n/utils';\nimport { getStore } from './config';\n\n// 歌曲信息接口定义\ninterface SongInfo {\n  name: string;\n  song: {\n    artists: Array<{ name: string; [key: string]: any }>;\n    [key: string]: any;\n  };\n  [key: string]: any;\n}\n\nlet tray: Tray | null = null;\n// 为macOS状态栏添加控制图标\nlet playPauseTray: Tray | null = null;\nlet prevTray: Tray | null = null;\nlet nextTray: Tray | null = null;\nlet songTitleTray: Tray | null = null;\n\nlet isPlaying = false;\nlet currentSong: SongInfo | null = null;\n\n// 使用自动导入的语言选项\nconst LANGUAGES = getLanguageOptions();\n\n// 更新播放状态\nexport function updatePlayState(playing: boolean) {\n  isPlaying = playing;\n  if (tray) {\n    updateTrayMenu(BrowserWindow.getAllWindows()[0]);\n  }\n  // 更新播放/暂停图标\n  updateStatusBarTray();\n}\n\n// 获取艺术家名称字符串\nfunction getArtistString(song: SongInfo | null): string {\n  if (!song || !song.song || !song.song.artists) return '';\n  return song.song.artists.map((item) => item.name).join(' / ');\n}\n\n// 获取歌曲完整标题（歌曲名 - 艺术家）\nfunction getSongTitle(song: SongInfo | null): string {\n  if (!song) return '未播放';\n  const artistStr = getArtistString(song);\n  return artistStr ? `${song.name} - ${artistStr}` : song.name;\n}\n\n// 截断歌曲标题，防止菜单中显示过长\nfunction getTruncatedSongTitle(song: SongInfo | null, maxLength: number = 14): string {\n  const fullTitle = getSongTitle(song);\n  if (fullTitle.length <= maxLength) return fullTitle;\n  return fullTitle.slice(0, maxLength) + '...';\n}\n\n// 更新当前播放的音乐信息\nexport function updateCurrentSong(song: SongInfo | null) {\n  currentSong = song;\n  if (tray) {\n    updateTrayMenu(BrowserWindow.getAllWindows()[0]);\n  }\n  // 更新状态栏歌曲信息\n  updateStatusBarTray();\n}\n\n// 确保 macOS 状态栏图标能正确显示\nfunction getProperIconSize() {\n  // macOS 状态栏通常高度为22像素\n  const height = 18;\n  const width = 18;\n  return { width, height };\n}\n\n// 更新macOS状态栏图标\nfunction updateStatusBarTray() {\n  if (process.platform !== 'darwin') return;\n\n  const iconSize = getProperIconSize();\n\n  // 更新歌曲标题显示\n  if (songTitleTray) {\n    if (currentSong) {\n      // 限制歌曲名显示长度，添加作者名\n      const songName = currentSong.name.slice(0, 10);\n      let title = songName;\n      const artistStr = getArtistString(currentSong);\n      // 如果有艺术家名称，添加到标题中\n      if (artistStr) {\n        title = `${songName} - ${artistStr.slice(0, 6)}${artistStr.length > 6 ? '..' : ''}`;\n      }\n\n      // 设置标题和提示\n      songTitleTray.setTitle(title, {\n        fontType: 'monospacedDigit' // 使用等宽字体以确保更好的可读性\n      });\n\n      // 完整信息放在tooltip中\n      const fullTitle = getSongTitle(currentSong);\n      songTitleTray.setToolTip(fullTitle);\n      console.log('更新状态栏歌曲显示:', title, '完整信息:', fullTitle);\n    } else {\n      songTitleTray.setTitle('未播放', {\n        fontType: 'monospacedDigit'\n      });\n      songTitleTray.setToolTip('未播放');\n      console.log('更新状态栏歌曲显示: 未播放');\n    }\n  }\n\n  // 更新播放/暂停图标\n  if (playPauseTray) {\n    // 使用PNG图标替代文本\n    const iconPath = join(\n      app.getAppPath(),\n      'resources/icons',\n      isPlaying ? 'pause.png' : 'play.png'\n    );\n    const icon = nativeImage.createFromPath(iconPath).resize(iconSize);\n    icon.setTemplateImage(true); // 设置为模板图片，适合macOS深色/浅色模式\n    playPauseTray.setImage(icon);\n    playPauseTray.setToolTip(\n      isPlaying ? i18n.global.t('common.tray.pause') : i18n.global.t('common.tray.play')\n    );\n  }\n}\n\n// 导出更新菜单的函数\nexport function updateTrayMenu(mainWindow: BrowserWindow) {\n  if (!tray) return;\n\n  // 如果是macOS，设置TouchBar\n  if (process.platform === 'darwin') {\n    // macOS 上使用直接的控制按钮\n    const menu = new Menu();\n\n    // 当前播放的音乐信息\n    if (currentSong) {\n      menu.append(\n        new MenuItem({\n          label: getTruncatedSongTitle(currentSong),\n          enabled: false,\n          type: 'normal'\n        })\n      );\n      menu.append(new MenuItem({ type: 'separator' }));\n    }\n\n    // 上一首、播放/暂停、下一首的菜单项\n    // 在macOS上临时使用文本菜单项替代图标，确保基本功能正常\n    menu.append(\n      new MenuItem({\n        label: i18n.global.t('common.tray.prev'),\n        type: 'normal',\n        click: () => {\n          mainWindow.webContents.send('global-shortcut', 'prevPlay');\n        }\n      })\n    );\n\n    menu.append(\n      new MenuItem({\n        label: i18n.global.t(isPlaying ? 'common.tray.pause' : 'common.tray.play'),\n        type: 'normal',\n        click: () => {\n          mainWindow.webContents.send('global-shortcut', 'togglePlay');\n        }\n      })\n    );\n\n    // 收藏\n    menu.append(\n      new MenuItem({\n        label: i18n.global.t('common.tray.favorite'),\n        type: 'normal',\n        click: () => {\n          console.log('[Tray] 发送收藏命令 - macOS菜单');\n          mainWindow.webContents.send('global-shortcut', 'toggleFavorite');\n        }\n      })\n    );\n\n    menu.append(\n      new MenuItem({\n        label: i18n.global.t('common.tray.next'),\n        type: 'normal',\n        click: () => {\n          mainWindow.webContents.send('global-shortcut', 'nextPlay');\n        }\n      })\n    );\n\n    // 分隔符\n    menu.append(new MenuItem({ type: 'separator' }));\n\n    // 显示主窗口\n    menu.append(\n      new MenuItem({\n        label: i18n.global.t('common.tray.show'),\n        type: 'normal',\n        click: () => {\n          mainWindow.show();\n        }\n      })\n    );\n\n    // 语言切换子菜单\n    const languageSubmenu = Menu.buildFromTemplate(\n      LANGUAGES.map(({ label, value }) => ({\n        label,\n        type: 'radio',\n        checked: i18n.global.locale === value,\n        click: () => {\n          i18n.global.locale = value;\n          updateTrayMenu(mainWindow);\n          mainWindow.webContents.send('language-changed', value);\n        }\n      }))\n    );\n\n    menu.append(\n      new MenuItem({\n        label: i18n.global.t('common.language'),\n        type: 'submenu',\n        submenu: languageSubmenu\n      })\n    );\n\n    // 退出按钮\n    menu.append(\n      new MenuItem({\n        label: i18n.global.t('common.tray.quit'),\n        type: 'normal',\n        click: () => {\n          app.quit();\n        }\n      })\n    );\n\n    tray.setContextMenu(menu);\n  } else {\n    // Windows 和 Linux 使用原来的菜单样式\n    const menuTemplate: MenuItemConstructorOptions[] = [\n      // 当前播放的音乐信息\n      ...((currentSong\n        ? [\n            {\n              label: getTruncatedSongTitle(currentSong),\n              enabled: false,\n              type: 'normal'\n            },\n            { type: 'separator' }\n          ]\n        : []) as MenuItemConstructorOptions[]),\n      {\n        label: i18n.global.t('common.tray.show'),\n        type: 'normal',\n        click: () => {\n          mainWindow.show();\n        }\n      },\n      {\n        label: i18n.global.t('common.tray.favorite'),\n        type: 'normal',\n        click: () => {\n          console.log('[Tray] 发送收藏命令 - Windows/Linux菜单');\n          mainWindow.webContents.send('global-shortcut', 'toggleFavorite');\n        }\n      },\n      { type: 'separator' },\n      {\n        label: i18n.global.t('common.tray.prev'),\n        type: 'normal',\n        click: () => {\n          mainWindow.webContents.send('global-shortcut', 'prevPlay');\n        }\n      },\n      {\n        label: i18n.global.t(isPlaying ? 'common.tray.pause' : 'common.tray.play'),\n        type: 'normal',\n        click: () => {\n          mainWindow.webContents.send('global-shortcut', 'togglePlay');\n        }\n      },\n      {\n        label: i18n.global.t('common.tray.next'),\n        type: 'normal',\n        click: () => {\n          mainWindow.webContents.send('global-shortcut', 'nextPlay');\n        }\n      },\n      { type: 'separator' },\n      {\n        label: i18n.global.t('common.language'),\n        type: 'submenu',\n        submenu: LANGUAGES.map(({ label, value }) => ({\n          label,\n          type: 'radio',\n          checked: i18n.global.locale === value,\n          click: () => {\n            i18n.global.locale = value;\n            updateTrayMenu(mainWindow);\n            mainWindow.webContents.send('language-changed', value);\n          }\n        }))\n      },\n      { type: 'separator' },\n      {\n        label: i18n.global.t('common.tray.quit'),\n        type: 'normal',\n        click: () => {\n          app.quit();\n        }\n      }\n    ];\n\n    const contextMenu = Menu.buildFromTemplate(menuTemplate);\n    tray.setContextMenu(contextMenu);\n  }\n}\n\n// 初始化状态栏Tray\nfunction initializeStatusBarTray(mainWindow: BrowserWindow) {\n  const store = getStore();\n  if (process.platform !== 'darwin' || !store.get('set.showTopAction')) return;\n\n  const iconSize = getProperIconSize();\n\n  // 创建下一首按钮（调整顺序，先创建下一首按钮）\n  const nextIcon = nativeImage\n    .createFromPath(join(app.getAppPath(), 'resources/icons', 'next.png'))\n    .resize(iconSize);\n  nextIcon.setTemplateImage(true); // 设置为模板图片，适合macOS深色/浅色模式\n  nextTray = new Tray(nextIcon);\n  nextTray.setToolTip(i18n.global.t('common.tray.next'));\n  nextTray.on('click', () => {\n    mainWindow.webContents.send('global-shortcut', 'nextPlay');\n  });\n\n  // 创建播放/暂停按钮\n  const playPauseIcon = nativeImage\n    .createFromPath(join(app.getAppPath(), 'resources/icons', isPlaying ? 'pause.png' : 'play.png'))\n    .resize(iconSize);\n  playPauseIcon.setTemplateImage(true); // 设置为模板图片，适合macOS深色/浅色模式\n  playPauseTray = new Tray(playPauseIcon);\n  playPauseTray.setToolTip(\n    isPlaying ? i18n.global.t('common.tray.pause') : i18n.global.t('common.tray.play')\n  );\n  playPauseTray.on('click', () => {\n    mainWindow.webContents.send('global-shortcut', 'togglePlay');\n  });\n\n  // 创建上一首按钮（调整顺序，最后创建上一首按钮）\n  const prevIcon = nativeImage\n    .createFromPath(join(app.getAppPath(), 'resources/icons', 'prev.png'))\n    .resize(iconSize);\n  prevIcon.setTemplateImage(true); // 设置为模板图片，适合macOS深色/浅色模式\n  prevTray = new Tray(prevIcon);\n  prevTray.setToolTip(i18n.global.t('common.tray.prev'));\n  prevTray.on('click', () => {\n    mainWindow.webContents.send('global-shortcut', 'prevPlay');\n  });\n\n  // 创建歌曲信息显示 - 需要使用特殊处理\n  const titleIcon = nativeImage\n    .createFromPath(join(app.getAppPath(), 'resources/icons', 'note.png'))\n    .resize({ width: 16, height: 16 });\n  titleIcon.setTemplateImage(true);\n  songTitleTray = new Tray(titleIcon);\n\n  // 初始化显示文本\n  const initialText = getSongTitle(currentSong);\n\n  // 在macOS上，特别设置title来显示文本，确保它能正确显示\n  songTitleTray.setTitle(initialText, {\n    fontType: 'monospacedDigit' // 使用等宽字体以确保更好的可读性\n  });\n\n  songTitleTray.setToolTip(initialText);\n  songTitleTray.on('click', () => {\n    mainWindow.show();\n  });\n\n  // 强制更新一次所有图标\n  updateStatusBarTray();\n\n  // 打印调试信息\n  console.log('状态栏初始化完成，歌曲显示标题:', initialText);\n}\n\n/**\n * 初始化系统托盘\n */\nexport function initializeTray(iconPath: string, mainWindow: BrowserWindow) {\n  // 根据平台选择合适的图标\n  const iconSize = process.platform === 'darwin' ? 18 : 16;\n  const iconFile = process.platform === 'darwin' ? 'icon_16x16.png' : 'icon_16x16.png';\n\n  const trayIcon = nativeImage\n    .createFromPath(join(iconPath, iconFile))\n    .resize({ width: iconSize, height: iconSize });\n\n  tray = new Tray(trayIcon);\n\n  // 设置托盘图标的提示文字\n  tray.setToolTip('Alger Music Player');\n\n  // 初始化菜单\n  updateTrayMenu(mainWindow);\n\n  // 初始化状态栏控制按钮 (macOS)\n  initializeStatusBarTray(mainWindow);\n\n  // 在 macOS 上，点击图标时显示菜单\n  if (process.platform === 'darwin') {\n    tray.on('click', () => {\n      if (tray) {\n        tray.popUpContextMenu();\n      }\n    });\n  } else {\n    // 在其他平台上，点击图标时切换窗口显示状态\n    tray.on('click', () => {\n      if (mainWindow.isVisible()) {\n        mainWindow.hide();\n      } else {\n        mainWindow.show();\n      }\n    });\n  }\n\n  return tray;\n}\n"
  },
  {
    "path": "src/main/modules/update.ts",
    "content": "import axios from 'axios';\nimport { spawn } from 'child_process';\nimport { app, BrowserWindow, ipcMain } from 'electron';\nimport * as fs from 'fs';\nimport * as path from 'path';\n\nexport function setupUpdateHandlers(_mainWindow: BrowserWindow) {\n  ipcMain.on('start-download', async (event, url: string) => {\n    try {\n      const response = await axios({\n        url,\n        method: 'GET',\n        responseType: 'stream',\n        onDownloadProgress: (progressEvent: { loaded: number; total?: number }) => {\n          if (!progressEvent.total) return;\n          const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);\n          const downloaded = (progressEvent.loaded / 1024 / 1024).toFixed(2);\n          const total = (progressEvent.total / 1024 / 1024).toFixed(2);\n          event.sender.send('download-progress', percent, `已下载 ${downloaded}MB / ${total}MB`);\n        }\n      });\n\n      const fileName = url.split('/').pop() || 'update.exe';\n      const downloadPath = path.join(app.getPath('downloads'), fileName);\n\n      // 创建写入流\n      const writer = fs.createWriteStream(downloadPath);\n\n      // 将响应流写入文件\n      response.data.pipe(writer);\n\n      // 处理写入完成\n      writer.on('finish', () => {\n        event.sender.send('download-complete', true, downloadPath);\n      });\n\n      // 处理写入错误\n      writer.on('error', (error) => {\n        console.error('Write file error:', error);\n        event.sender.send('download-complete', false, '');\n      });\n    } catch (error) {\n      console.error('Download failed:', error);\n      event.sender.send('download-complete', false, '');\n    }\n  });\n\n  ipcMain.on('install-update', (_event, filePath: string) => {\n    if (!fs.existsSync(filePath)) {\n      console.error('Installation file not found:', filePath);\n      return;\n    }\n\n    const { platform } = process;\n\n    // 先启动安装程序，再退出应用\n    try {\n      if (platform === 'win32') {\n        // 使用spawn替代exec，并使用detached选项确保子进程独立运行\n        const child = spawn(filePath, [], {\n          detached: true,\n          stdio: 'ignore'\n        });\n        child.unref();\n      } else if (platform === 'darwin') {\n        // 挂载 DMG 文件\n        const child = spawn('open', [filePath], {\n          detached: true,\n          stdio: 'ignore'\n        });\n        child.unref();\n      } else if (platform === 'linux') {\n        const ext = path.extname(filePath);\n        if (ext === '.AppImage') {\n          // 先添加执行权限\n          fs.chmodSync(filePath, '755');\n          const child = spawn(filePath, [], {\n            detached: true,\n            stdio: 'ignore'\n          });\n          child.unref();\n        } else if (ext === '.deb') {\n          const child = spawn('pkexec', ['dpkg', '-i', filePath], {\n            detached: true,\n            stdio: 'ignore'\n          });\n          child.unref();\n        }\n      }\n\n      // 给安装程序一点时间启动\n      setTimeout(() => {\n        app.quit();\n      }, 500);\n    } catch (error) {\n      console.error('启动安装程序失败:', error);\n      // 尽管出错，仍然尝试退出应用\n      app.quit();\n    }\n  });\n}\n"
  },
  {
    "path": "src/main/modules/window-size.ts",
    "content": "import { app, BrowserWindow, ipcMain, screen } from 'electron';\nimport Store from 'electron-store';\n\nconst store = new Store();\n\n// 默认窗口尺寸\nexport const DEFAULT_MAIN_WIDTH = 1200;\nexport const DEFAULT_MAIN_HEIGHT = 780;\nexport const DEFAULT_MINI_WIDTH = 340;\nexport const DEFAULT_MINI_HEIGHT = 64;\nexport const DEFAULT_MINI_EXPANDED_HEIGHT = 400;\n\n// 用于存储窗口状态的键名\nexport const WINDOW_STATE_KEY = 'windowState';\n\n// 最小窗口尺寸\nlet MIN_WIDTH = Math.round(DEFAULT_MAIN_WIDTH * 0.5);\nlet MIN_HEIGHT = Math.round(DEFAULT_MAIN_HEIGHT * 0.5);\n\n// 标记IPC处理程序是否已注册\nlet ipcHandlersRegistered = false;\n\n/**\n * 窗口状态类型定义\n */\nexport interface WindowState {\n  width: number;\n  height: number;\n  x?: number;\n  y?: number;\n  isMaximized: boolean;\n}\n\n/**\n * 窗口大小管理器\n * 负责保存、恢复和维护窗口大小状态\n */\nclass WindowSizeManager {\n  private store: Store;\n  private mainWindow: BrowserWindow | null = null;\n  private savedState: WindowState | null = null;\n  private isInitialized: boolean = false;\n\n  constructor() {\n    this.store = store;\n    // 初始化时不做与screen相关的操作，等app ready后再初始化\n  }\n\n  /**\n   * 初始化窗口大小管理器\n   * 必须在app ready后调用\n   */\n  initialize(): void {\n    if (!app.isReady()) {\n      console.warn('WindowSizeManager.initialize() 必须在 app ready 之后调用！');\n      return;\n    }\n\n    if (this.isInitialized) {\n      return;\n    }\n\n    this.initMinimumWindowSize();\n    this.setupIPCHandlers();\n    this.isInitialized = true;\n    console.log('窗口大小管理器初始化完成');\n  }\n\n  /**\n   * 设置主窗口引用\n   */\n  setMainWindow(win: BrowserWindow): void {\n    if (!this.isInitialized) {\n      this.initialize();\n    }\n\n    this.mainWindow = win;\n\n    // 读取保存的状态\n    this.savedState = this.getWindowState();\n\n    // 监听重要事件\n    this.setupEventListeners(win);\n\n    // 立即保存初始状态\n    this.saveWindowState(win);\n  }\n\n  /**\n   * 初始化最小窗口尺寸\n   */\n  private initMinimumWindowSize(): void {\n    if (!app.isReady()) {\n      console.warn('不能在 app ready 之前访问 screen 模块');\n      return;\n    }\n\n    try {\n      const { width: workAreaWidth, height: workAreaHeight } = screen.getPrimaryDisplay().workArea;\n\n      // 根据工作区大小设置合理的最小尺寸\n      MIN_WIDTH = Math.min(Math.round(DEFAULT_MAIN_WIDTH * 0.5), Math.round(workAreaWidth * 0.3));\n      MIN_HEIGHT = Math.min(\n        Math.round(DEFAULT_MAIN_HEIGHT * 0.5),\n        Math.round(workAreaHeight * 0.3)\n      );\n\n      console.log(`设置最小窗口尺寸: ${MIN_WIDTH}x${MIN_HEIGHT}`);\n    } catch (error) {\n      console.error('初始化最小窗口尺寸失败:', error);\n      // 使用默认值\n      MIN_WIDTH = Math.round(DEFAULT_MAIN_WIDTH * 0.5);\n      MIN_HEIGHT = Math.round(DEFAULT_MAIN_HEIGHT * 0.5);\n    }\n  }\n\n  /**\n   * 设置事件监听器\n   */\n  private setupEventListeners(win: BrowserWindow): void {\n    // 监听窗口大小调整事件\n    win.on('resize', () => {\n      if (!win.isDestroyed() && !win.isMinimized()) {\n        this.saveWindowState(win);\n      }\n    });\n\n    // 监听窗口移动事件\n    win.on('move', () => {\n      if (!win.isDestroyed() && !win.isMinimized()) {\n        this.saveWindowState(win);\n      }\n    });\n\n    // 监听窗口最大化事件\n    win.on('maximize', () => {\n      if (!win.isDestroyed()) {\n        this.saveWindowState(win);\n      }\n    });\n\n    // 监听窗口从最大化恢复事件\n    win.on('unmaximize', () => {\n      if (!win.isDestroyed()) {\n        this.saveWindowState(win);\n      }\n    });\n\n    // 监听窗口关闭事件，确保保存最终状态\n    win.on('close', () => {\n      if (!win.isDestroyed()) {\n        this.saveWindowState(win);\n      }\n    });\n\n    // 在页面加载完成后确保窗口大小正确\n    win.webContents.on('did-finish-load', () => {\n      this.enforceCorrectSize(win);\n    });\n\n    // 在窗口准备好显示时确保尺寸正确\n    win.on('ready-to-show', () => {\n      this.enforceCorrectSize(win);\n    });\n  }\n\n  /**\n   * 强制应用正确的窗口大小\n   */\n  private enforceCorrectSize(win: BrowserWindow): void {\n    if (!this.savedState || win.isMaximized() || win.isMinimized() || win.isDestroyed()) {\n      return;\n    }\n\n    const [currentWidth, currentHeight] = win.getSize();\n\n    if (\n      Math.abs(currentWidth - this.savedState.width) > 2 ||\n      Math.abs(currentHeight - this.savedState.height) > 2\n    ) {\n      console.log(\n        `强制调整窗口大小: 当前=${currentWidth}x${currentHeight}, 目标=${this.savedState.width}x${this.savedState.height}`\n      );\n\n      // 临时禁用minimum size限制\n      const [minWidth, minHeight] = win.getMinimumSize();\n      win.setMinimumSize(1, 1);\n\n      // 强制设置正确大小\n      win.setSize(this.savedState.width, this.savedState.height, false);\n\n      // 恢复原始minimum size\n      win.setMinimumSize(minWidth, minHeight);\n\n      // 验证尺寸设置是否成功\n      const [newWidth, newHeight] = win.getSize();\n      console.log(`调整后窗口大小: ${newWidth}x${newHeight}`);\n\n      // 如果调整后的大小仍然与目标不一致，尝试再次调整\n      if (\n        Math.abs(newWidth - this.savedState.width) > 1 ||\n        Math.abs(newHeight - this.savedState.height) > 1\n      ) {\n        console.log(`窗口大小调整后仍不一致，将再次尝试调整`);\n        setTimeout(() => {\n          if (!win.isDestroyed() && !win.isMaximized() && !win.isMinimized()) {\n            win.setSize(this.savedState!.width, this.savedState!.height, false);\n          }\n        }, 50);\n      }\n\n      // // 开始尺寸强制执行\n      // this.startSizeEnforcement(win);\n    }\n  }\n\n  /**\n   * 开启尺寸强制执行定时器\n   */\n  // private startSizeEnforcement(win: BrowserWindow): void {\n  //   // 清除之前的定时器\n  //   if (this.enforceTimer) {\n  //     clearInterval(this.enforceTimer);\n  //     this.enforceTimer = null;\n  //   }\n\n  //   this.enforceCount = 0;\n\n  //   // 创建新的定时器，每50ms检查一次窗口大小\n  //   this.enforceTimer = setInterval(() => {\n  //     if (this.enforceCount >= this.MAX_ENFORCE_COUNT ||\n  //         !this.savedState ||\n  //         win.isDestroyed() ||\n  //         win.isMaximized() ||\n  //         win.isMinimized()) {\n  //       // 达到最大检查次数或不需要检查，清除定时器\n  //       if (this.enforceTimer) {\n  //         clearInterval(this.enforceTimer);\n  //         this.enforceTimer = null;\n  //       }\n  //       return;\n  //     }\n\n  //     const [currentWidth, currentHeight] = win.getSize();\n\n  //     if (Math.abs(currentWidth - this.savedState.width) > 2 ||\n  //         Math.abs(currentHeight - this.savedState.height) > 2) {\n  //       console.log(`[定时检查] 强制调整窗口大小: 当前=${currentWidth}x${currentHeight}, 目标=${this.savedState.width}x${this.savedState.height}`);\n\n  //       // 临时禁用minimum size限制\n  //       const [minWidth, minHeight] = win.getMinimumSize();\n  //       win.setMinimumSize(1, 1);\n\n  //       // 强制设置正确大小\n  //       win.setSize(this.savedState.width, this.savedState.height, false);\n\n  //       // 恢复原始minimum size\n  //       win.setMinimumSize(minWidth, minHeight);\n\n  //       // 验证尺寸设置是否成功\n  //       const [newWidth, newHeight] = win.getSize();\n  //       if (Math.abs(newWidth - this.savedState.width) <= 1 &&\n  //           Math.abs(newHeight - this.savedState.height) <= 1) {\n  //         console.log(`窗口大小已成功调整为目标尺寸: ${newWidth}x${newHeight}`);\n  //       }\n  //     }\n\n  //     this.enforceCount++;\n  //   }, 50);\n  // }\n\n  /**\n   * 获取窗口创建选项\n   */\n  getWindowOptions(): Electron.BrowserWindowConstructorOptions {\n    // 确保初始化\n    if (!this.isInitialized && app.isReady()) {\n      this.initialize();\n    }\n\n    // 读取保存的状态\n    const savedState = this.getWindowState();\n\n    // 准备选项\n    const options: Electron.BrowserWindowConstructorOptions = {\n      width: savedState?.width || DEFAULT_MAIN_WIDTH,\n      height: savedState?.height || DEFAULT_MAIN_HEIGHT,\n      minWidth: MIN_WIDTH,\n      minHeight: MIN_HEIGHT,\n      show: false,\n      frame: false,\n      webPreferences: {\n        nodeIntegration: false,\n        contextIsolation: true\n      }\n    };\n\n    // 如果有保存的位置，且位置有效，则使用该位置\n    if (savedState?.x !== undefined && savedState?.y !== undefined && app.isReady()) {\n      if (this.isPositionVisible(savedState.x, savedState.y)) {\n        options.x = savedState.x;\n        options.y = savedState.y;\n      }\n    }\n\n    console.log(\n      `窗口创建选项: 大小=${options.width}x${options.height}, 位置=(${options.x}, ${options.y})`\n    );\n\n    return options;\n  }\n\n  /**\n   * 应用窗口初始状态\n   * 在窗口创建后调用\n   */\n  applyInitialState(win: BrowserWindow): void {\n    const savedState = this.getWindowState();\n\n    if (!savedState) {\n      win.center();\n      return;\n    }\n\n    // 如果需要最大化，直接最大化\n    if (savedState.isMaximized) {\n      console.log('应用已保存的最大化状态');\n      win.maximize();\n    }\n    // 如果位置无效，则居中显示\n    else if (\n      !app.isReady() ||\n      savedState.x === undefined ||\n      savedState.y === undefined ||\n      !this.isPositionVisible(savedState.x, savedState.y)\n    ) {\n      console.log('保存的位置无效，窗口居中显示');\n      win.center();\n    }\n  }\n\n  /**\n   * 保存窗口状态\n   */\n  saveWindowState(win: BrowserWindow): WindowState {\n    // 如果窗口已销毁，则返回之前的状态或默认状态\n    console.log('win.isDestroyed()', win.isDestroyed());\n    if (win.isDestroyed()) {\n      return (\n        this.savedState || {\n          width: DEFAULT_MAIN_WIDTH,\n          height: DEFAULT_MAIN_HEIGHT,\n          isMaximized: false\n        }\n      );\n    }\n\n    // 检查是否是mini模式窗口（根据窗口大小判断）\n    const [currentWidth, currentHeight] = win.getSize();\n    const isMiniMode = currentWidth === DEFAULT_MINI_WIDTH && currentHeight === DEFAULT_MINI_HEIGHT;\n\n    const isMaximized = win.isMaximized();\n    let state: WindowState;\n\n    if (isMaximized) {\n      // 如果窗口处于最大化状态，保存最大化标志\n      // 由于 Electron 的限制，最大化状态下 getBounds() 可能不准确\n      // 所以我们尽量保留之前保存的非最大化时的大小\n      const currentBounds = win.getBounds();\n      const previousSize =\n        this.savedState && !this.savedState.isMaximized\n          ? { width: this.savedState.width, height: this.savedState.height }\n          : { width: currentBounds.width, height: currentBounds.height };\n\n      state = {\n        width: previousSize.width,\n        height: previousSize.height,\n        x: currentBounds.x,\n        y: currentBounds.y,\n        isMaximized: true\n      };\n      console.log('state IsMaximized', state);\n    } else if (win.isMinimized()) {\n      // 最小化状态下不保存窗口大小，因为可能不准确\n      console.log('state IsMinimized', this.savedState);\n      return (\n        this.savedState || {\n          width: DEFAULT_MAIN_WIDTH,\n          height: DEFAULT_MAIN_HEIGHT,\n          isMaximized: false\n        }\n      );\n    } else {\n      // 正常状态下保存当前大小和位置\n      const [width, height] = win.getSize();\n      const [x, y] = win.getPosition();\n\n      state = {\n        width,\n        height,\n        x,\n        y,\n        isMaximized: false\n      };\n      console.log('state IsNormal', state);\n    }\n\n    // 如果是mini模式，不保存到持久化存储，只返回状态用于内存中的恢复\n    if (isMiniMode) {\n      console.log('检测到mini模式窗口，不保存到持久化存储');\n      return state;\n    }\n\n    // 保存状态到存储\n    this.store.set(WINDOW_STATE_KEY, state);\n    console.log(`已保存窗口状态: ${JSON.stringify(state)}`);\n\n    // 更新内部状态\n    this.savedState = state;\n    console.log('state', state);\n\n    return state;\n  }\n\n  /**\n   * 获取保存的窗口状态\n   */\n  getWindowState(): WindowState | null {\n    const state = this.store.get(WINDOW_STATE_KEY) as WindowState | undefined;\n\n    if (!state) {\n      console.log('未找到保存的窗口状态，将使用默认值');\n      return null;\n    }\n\n    // 验证尺寸，确保不小于最小值\n    const validatedState: WindowState = {\n      width: Math.max(MIN_WIDTH, state.width || DEFAULT_MAIN_WIDTH),\n      height: Math.max(MIN_HEIGHT, state.height || DEFAULT_MAIN_HEIGHT),\n      x: state.x,\n      y: state.y,\n      isMaximized: !!state.isMaximized\n    };\n\n    console.log(`读取保存的窗口状态: ${JSON.stringify(validatedState)}`);\n\n    return validatedState;\n  }\n\n  /**\n   * 检查位置是否在可见屏幕范围内\n   */\n  isPositionVisible(x: number, y: number): boolean {\n    if (!app.isReady()) {\n      return false;\n    }\n\n    try {\n      const displays = screen.getAllDisplays();\n\n      for (const display of displays) {\n        const { x: screenX, y: screenY, width, height } = display.workArea;\n        if (x >= screenX && x < screenX + width && y >= screenY && y < screenY + height) {\n          return true;\n        }\n      }\n    } catch (error) {\n      console.error('检查位置可见性失败:', error);\n      return false;\n    }\n\n    return false;\n  }\n\n  /**\n   * 计算适合当前缩放比的缩放因子\n   */\n  calculateContentZoomFactor(): number {\n    // 只有在 app 准备好后才能使用screen\n    if (!app.isReady()) {\n      return 1;\n    }\n\n    try {\n      // 获取系统的缩放因子\n      const { scaleFactor } = screen.getPrimaryDisplay();\n\n      // 缩放因子默认为1\n      let zoomFactor = 1;\n\n      // 只在高DPI情况下调整\n      if (scaleFactor > 1) {\n        // 自定义逻辑来根据不同的缩放比例进行调整\n        if (scaleFactor >= 2.5) {\n          // 极高缩放比，例如4K屏幕用200%+缩放\n          zoomFactor = 0.7;\n        } else if (scaleFactor >= 2) {\n          // 高缩放比，例如200%\n          zoomFactor = 0.8;\n        } else if (scaleFactor >= 1.5) {\n          // 中等缩放比，例如150%\n          zoomFactor = 0.85;\n        } else if (scaleFactor > 1.25) {\n          // 略高缩放比，例如125%-149%\n          zoomFactor = 0.9;\n        } else {\n          // 低缩放比，不做调整\n          zoomFactor = 1;\n        }\n      }\n\n      // 获取用户的自定义缩放设置（如果有）\n      const userZoomFactor = this.store.get('set.contentZoomFactor') as number | undefined;\n      if (userZoomFactor) {\n        zoomFactor = userZoomFactor;\n      }\n\n      return zoomFactor;\n    } catch (error) {\n      console.error('计算内容缩放因子失败:', error);\n      return 1;\n    }\n  }\n\n  /**\n   * 应用页面内容缩放\n   */\n  applyContentZoom(win: BrowserWindow): void {\n    const zoomFactor = this.calculateContentZoomFactor();\n    win.webContents.setZoomFactor(zoomFactor);\n\n    if (app.isReady()) {\n      try {\n        console.log(\n          `应用页面缩放因子: ${zoomFactor}, 系统缩放比: ${screen.getPrimaryDisplay().scaleFactor}`\n        );\n      } catch (error) {\n        console.error('获取系统缩放比失败:', error);\n      }\n    } else {\n      console.log(`应用页面缩放因子: ${zoomFactor}`);\n    }\n  }\n\n  /**\n   * 初始化IPC消息处理程序\n   */\n  setupIPCHandlers(): void {\n    // 防止重复注册IPC处理程序\n    if (ipcHandlersRegistered) {\n      console.log('IPC处理程序已注册，跳过重复注册');\n      return;\n    }\n\n    console.log('注册窗口大小相关的IPC处理程序');\n\n    // 标记为已注册\n    ipcHandlersRegistered = true;\n\n    // 安全地移除已存在的处理程序（如果有）\n    const removeHandlerSafely = (channel: string) => {\n      try {\n        ipcMain.removeHandler(channel);\n      } catch (error) {\n        console.warn(`移除IPC处理程序 ${channel} 时出错:`, error);\n      }\n    };\n\n    // 为需要使用handle方法的通道先移除已有处理程序\n    removeHandlerSafely('get-content-zoom');\n    removeHandlerSafely('get-system-scale-factor');\n\n    // 注册新的处理程序\n    ipcMain.on('set-content-zoom', (event, zoomFactor) => {\n      const win = BrowserWindow.fromWebContents(event.sender);\n      if (win && !win.isDestroyed()) {\n        win.webContents.setZoomFactor(zoomFactor);\n        this.store.set('set.contentZoomFactor', zoomFactor);\n      }\n    });\n\n    ipcMain.handle('get-content-zoom', (event) => {\n      const win = BrowserWindow.fromWebContents(event.sender);\n      if (win && !win.isDestroyed()) {\n        return win.webContents.getZoomFactor();\n      }\n      return 1;\n    });\n\n    ipcMain.handle('get-system-scale-factor', () => {\n      if (!app.isReady()) {\n        return 1;\n      }\n\n      try {\n        return screen.getPrimaryDisplay().scaleFactor;\n      } catch (error) {\n        console.error('获取系统缩放因子失败:', error);\n        return 1;\n      }\n    });\n\n    ipcMain.on('reset-content-zoom', (event) => {\n      const win = BrowserWindow.fromWebContents(event.sender);\n      if (win && !win.isDestroyed()) {\n        this.store.delete('set.contentZoomFactor');\n        this.applyContentZoom(win);\n      }\n    });\n\n    ipcMain.on('resize-window', (event, width, height) => {\n      const win = BrowserWindow.fromWebContents(event.sender);\n      if (win && !win.isDestroyed()) {\n        console.log(`接收到调整窗口大小请求: ${width}x${height}`);\n\n        // 确保尺寸不小于最小值\n        const adjustedWidth = Math.max(width, MIN_WIDTH);\n        const adjustedHeight = Math.max(height, MIN_HEIGHT);\n\n        // 设置窗口的大小\n        win.setSize(adjustedWidth, adjustedHeight);\n        console.log(`窗口大小已调整为: ${adjustedWidth}x${adjustedHeight}`);\n\n        // 保存窗口状态\n        this.saveWindowState(win);\n      }\n    });\n\n    ipcMain.on('resize-mini-window', (event, showPlaylist) => {\n      const win = BrowserWindow.fromWebContents(event.sender);\n      if (win && !win.isDestroyed()) {\n        if (showPlaylist) {\n          console.log(`扩大迷你窗口至 ${DEFAULT_MINI_WIDTH} x ${DEFAULT_MINI_EXPANDED_HEIGHT}`);\n          win.setMinimumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT);\n          win.setMaximumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_EXPANDED_HEIGHT);\n          win.setSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_EXPANDED_HEIGHT, false);\n        } else {\n          console.log(`缩小迷你窗口至 ${DEFAULT_MINI_WIDTH} x ${DEFAULT_MINI_HEIGHT}`);\n          win.setMaximumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT);\n          win.setMinimumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT);\n          win.setSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT, false);\n        }\n      }\n    });\n\n    // 只在app ready后设置显示器变化监听\n    if (app.isReady()) {\n      // 监听显示器变化事件\n      screen.on('display-metrics-changed', (_event, _display, changedMetrics) => {\n        if (this.mainWindow && !this.mainWindow.isDestroyed()) {\n          // 当缩放因子变化时，重新应用页面缩放\n          if (changedMetrics.includes('scaleFactor')) {\n            this.applyContentZoom(this.mainWindow);\n          }\n\n          // 重新初始化最小尺寸\n          this.initMinimumWindowSize();\n        }\n      });\n    }\n\n    // 监听 store 中的缩放设置变化\n    this.store.onDidChange('set.contentZoomFactor', () => {\n      if (this.mainWindow && !this.mainWindow.isDestroyed()) {\n        this.applyContentZoom(this.mainWindow);\n      }\n    });\n  }\n}\n\n// 创建窗口大小管理器实例\nconst windowSizeManager = new WindowSizeManager();\n\n// 导出初始化函数\nexport const initWindowSizeManager = (): void => {\n  // 等待app ready后再初始化\n  if (app.isReady()) {\n    windowSizeManager.initialize();\n  } else {\n    app.on('ready', () => {\n      windowSizeManager.initialize();\n    });\n  }\n};\n\n// 导出实例方法\nexport const getWindowOptions = (): Electron.BrowserWindowConstructorOptions => {\n  return windowSizeManager.getWindowOptions();\n};\n\nexport const applyInitialState = (win: BrowserWindow): void => {\n  windowSizeManager.applyInitialState(win);\n};\n\nexport const saveWindowState = (win: BrowserWindow): WindowState => {\n  return windowSizeManager.saveWindowState(win);\n};\n\nexport const getWindowState = (): WindowState | null => {\n  return windowSizeManager.getWindowState();\n};\n\nexport const applyContentZoom = (win: BrowserWindow): void => {\n  windowSizeManager.applyContentZoom(win);\n};\n\nexport const initWindowSizeHandlers = (mainWindow: BrowserWindow | null): void => {\n  // 确保app ready后再初始化\n  if (!app.isReady()) {\n    app.on('ready', () => {\n      if (mainWindow) {\n        windowSizeManager.setMainWindow(mainWindow);\n      }\n    });\n  } else {\n    if (mainWindow) {\n      windowSizeManager.setMainWindow(mainWindow);\n    }\n  }\n};\n\nexport const calculateMinimumWindowSize = (): { minWidth: number; minHeight: number } => {\n  return { minWidth: MIN_WIDTH, minHeight: MIN_HEIGHT };\n};\n"
  },
  {
    "path": "src/main/modules/window.ts",
    "content": "import { is } from '@electron-toolkit/utils';\nimport {\n  app,\n  BrowserWindow,\n  globalShortcut,\n  ipcMain,\n  nativeImage,\n  screen,\n  session,\n  shell\n} from 'electron';\nimport Store from 'electron-store';\nimport { join } from 'path';\n\nimport {\n  applyContentZoom,\n  applyInitialState,\n  DEFAULT_MAIN_HEIGHT,\n  DEFAULT_MAIN_WIDTH,\n  DEFAULT_MINI_HEIGHT,\n  DEFAULT_MINI_WIDTH,\n  getWindowOptions,\n  getWindowState,\n  initWindowSizeHandlers,\n  saveWindowState,\n  WindowState\n} from './window-size';\n\nconst store = new Store();\n\n// 保存主窗口引用，以便在 activate 事件中使用\nlet mainWindowInstance: BrowserWindow | null = null;\nlet isPlaying = false;\nlet isAppQuitting = false;\n// 保存迷你模式前的窗口状态\nlet preMiniModeState: WindowState = {\n  width: DEFAULT_MAIN_WIDTH,\n  height: DEFAULT_MAIN_HEIGHT,\n  x: undefined,\n  y: undefined,\n  isMaximized: false\n};\n\n/**\n * 设置应用退出状态\n */\nexport function setAppQuitting(quitting: boolean) {\n  isAppQuitting = quitting;\n}\n\n/**\n * 初始化代理设置\n */\nfunction initializeProxy() {\n  const defaultConfig = {\n    enable: false,\n    protocol: 'http',\n    host: '127.0.0.1',\n    port: 7890\n  };\n\n  const proxyConfig = store.get('set.proxyConfig', defaultConfig) as {\n    enable: boolean;\n    protocol: string;\n    host: string;\n    port: number;\n  };\n\n  if (proxyConfig?.enable) {\n    const proxyRules = `${proxyConfig.protocol}://${proxyConfig.host}:${proxyConfig.port}`;\n    session.defaultSession.setProxy({ proxyRules });\n  } else {\n    session.defaultSession.setProxy({ proxyRules: '' });\n  }\n}\n\nfunction setThumbarButtons(window: BrowserWindow) {\n  window.setThumbarButtons([\n    {\n      tooltip: 'prev',\n      icon: nativeImage.createFromPath(join(app.getAppPath(), 'resources/icons', 'prev.png')),\n      click() {\n        window.webContents.send('global-shortcut', 'prevPlay');\n      }\n    },\n\n    {\n      tooltip: isPlaying ? 'pause' : 'play',\n      icon: nativeImage.createFromPath(\n        join(app.getAppPath(), 'resources/icons', isPlaying ? 'pause.png' : 'play.png')\n      ),\n      click() {\n        window.webContents.send('global-shortcut', 'togglePlay');\n      }\n    },\n\n    {\n      tooltip: 'next',\n      icon: nativeImage.createFromPath(join(app.getAppPath(), 'resources/icons', 'next.png')),\n      click() {\n        window.webContents.send('global-shortcut', 'nextPlay');\n      }\n    }\n  ]);\n}\n\n/**\n * 初始化窗口管理相关的IPC监听\n */\nexport function initializeWindowManager() {\n  // 初始化代理设置\n  initializeProxy();\n\n  ipcMain.on('minimize-window', (event) => {\n    const win = BrowserWindow.fromWebContents(event.sender);\n    if (win) {\n      win.minimize();\n    }\n  });\n\n  ipcMain.on('maximize-window', (event) => {\n    const win = BrowserWindow.fromWebContents(event.sender);\n    if (win) {\n      if (win.isMaximized()) {\n        win.unmaximize();\n      } else {\n        win.maximize();\n      }\n      // 状态保存在事件监听器中处理\n    }\n  });\n\n  ipcMain.on('close-window', (event) => {\n    const win = BrowserWindow.fromWebContents(event.sender);\n    if (win) {\n      // 在 macOS 上，关闭窗口不应该退出应用，而是隐藏窗口\n      if (process.platform === 'darwin') {\n        win.hide();\n      } else {\n        win.destroy();\n        app.quit();\n      }\n    }\n  });\n\n  // 强制退出应用（用于免责声明拒绝等场景）\n  ipcMain.on('quit-app', () => {\n    setAppQuitting(true);\n    app.quit();\n  });\n\n  ipcMain.on('mini-tray', (event) => {\n    const win = BrowserWindow.fromWebContents(event.sender);\n    if (win) {\n      win.hide();\n    }\n  });\n\n  ipcMain.on('mini-window', (event) => {\n    const win = BrowserWindow.fromWebContents(event.sender);\n    if (win) {\n      // 保存当前窗口状态，以便之后恢复\n      preMiniModeState = saveWindowState(win);\n      console.log('保存正常模式状态用于恢复:', JSON.stringify(preMiniModeState));\n\n      // 获取屏幕工作区尺寸\n      const display = screen.getDisplayMatching(win.getBounds());\n      const { width: screenWidth, x: screenX } = display.workArea;\n\n      // 设置迷你窗口的大小和位置\n      win.unmaximize();\n      win.setMinimumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT);\n      win.setMaximumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT);\n      win.setSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT, false); // 禁用动画\n      // 将迷你窗口放在工作区的右上角，留出一些边距\n      win.setPosition(\n        screenX + screenWidth - DEFAULT_MINI_WIDTH - 20,\n        display.workArea.y + 20,\n        false\n      );\n      win.setAlwaysOnTop(true);\n      win.setSkipTaskbar(false);\n      win.setResizable(false);\n\n      // 导航到迷你模式路由\n      win.webContents.send('navigate', '/mini');\n\n      // 发送事件到渲染进程，通知切换到迷你模式\n      win.webContents.send('mini-mode', true);\n\n      // 迷你窗口使用默认的缩放比\n      win.webContents.setZoomFactor(1);\n    }\n  });\n\n  // 恢复窗口\n  ipcMain.on('restore-window', (event) => {\n    const win = BrowserWindow.fromWebContents(event.sender);\n    if (win) {\n      // 恢复窗口的大小调整功能\n      win.setResizable(true);\n      win.setMaximumSize(0, 0); // 取消最大尺寸限制\n\n      console.log('从迷你模式恢复，使用保存的状态:', JSON.stringify(preMiniModeState));\n\n      // 设置适当的最小尺寸\n      win.setMinimumSize(\n        Math.max(DEFAULT_MAIN_WIDTH * 0.5, 600),\n        Math.max(DEFAULT_MAIN_HEIGHT * 0.5, 400)\n      );\n\n      // 恢复窗口状态\n      win.setAlwaysOnTop(false);\n      win.setSkipTaskbar(false);\n\n      // 导航回主页面\n      win.webContents.send('navigate', '/');\n\n      // 发送事件到渲染进程，通知退出迷你模式\n      win.webContents.send('mini-mode', false);\n\n      // 应用保存的状态\n      setTimeout(() => {\n        // 如果有保存的位置，则应用\n        if (preMiniModeState.x !== undefined && preMiniModeState.y !== undefined) {\n          win.setPosition(preMiniModeState.x, preMiniModeState.y, false);\n        } else {\n          win.center();\n        }\n\n        // 使用存储的迷你模式前的状态\n        if (preMiniModeState.isMaximized) {\n          win.maximize();\n        } else {\n          // 设置正确的窗口大小\n          win.setSize(preMiniModeState.width, preMiniModeState.height, false);\n        }\n\n        // 应用页面缩放\n        applyContentZoom(win);\n\n        // 确保窗口大小被正确应用\n        setTimeout(() => {\n          if (!win.isDestroyed() && !win.isMaximized() && !win.isMinimized()) {\n            // 再次验证窗口大小\n            const [width, height] = win.getSize();\n            if (\n              Math.abs(width - preMiniModeState.width) > 2 ||\n              Math.abs(height - preMiniModeState.height) > 2\n            ) {\n              console.log(\n                `恢复后窗口大小不一致，再次调整: 当前=${width}x${height}, 目标=${preMiniModeState.width}x${preMiniModeState.height}`\n              );\n              win.setSize(preMiniModeState.width, preMiniModeState.height, false);\n            }\n          }\n        }, 150);\n      }, 50);\n    }\n  });\n\n  ipcMain.on('update-play-state', (_, playing: boolean) => {\n    isPlaying = playing;\n    if (mainWindowInstance) {\n      setThumbarButtons(mainWindowInstance);\n    }\n  });\n\n  // 监听代理设置变化\n  store.onDidChange('set.proxyConfig', () => {\n    initializeProxy();\n  });\n\n  // 初始化窗口大小和缩放相关的IPC处理程序\n  initWindowSizeHandlers(mainWindowInstance);\n  // 监听 macOS 下点击 Dock 图标的事件\n  app.on('activate', () => {\n    // 当应用被激活时，检查主窗口是否存在\n    if (mainWindowInstance && !mainWindowInstance.isDestroyed()) {\n      // 如果窗口存在但被隐藏，则显示窗口\n      if (!mainWindowInstance.isVisible()) {\n        mainWindowInstance.show();\n      }\n    }\n  });\n}\n\n/**\n * 创建主窗口\n */\nexport function createMainWindow(icon: Electron.NativeImage): BrowserWindow {\n  console.log('开始创建主窗口...');\n\n  // 获取窗口创建选项\n  const options = getWindowOptions();\n\n  // 添加图标和预加载脚本\n  options.icon = icon;\n  options.webPreferences = {\n    preload: join(__dirname, '../preload/index.js'),\n    sandbox: false,\n    contextIsolation: true,\n    webSecurity: false\n  };\n\n  console.log(\n    `创建窗口，使用选项: ${JSON.stringify({\n      width: options.width,\n      height: options.height,\n      x: options.x,\n      y: options.y,\n      minWidth: options.minWidth,\n      minHeight: options.minHeight\n    })}`\n  );\n\n  // 创建窗口\n  const mainWindow = new BrowserWindow(options);\n\n  // 移除菜单\n  mainWindow.removeMenu();\n\n  // 应用初始状态 (例如最大化状态)\n  applyInitialState(mainWindow);\n\n  // 更新 preMiniModeState，以便迷你模式可以正确恢复\n  const savedState = getWindowState();\n  if (savedState) {\n    preMiniModeState = { ...savedState };\n  }\n\n  mainWindow.on('show', () => {\n    setThumbarButtons(mainWindow);\n  });\n\n  // 处理窗口关闭事件\n  mainWindow.on('close', (event) => {\n    // 在 macOS 上，阻止默认的关闭行为，改为隐藏窗口\n    if (process.platform === 'darwin') {\n      // 检查是否是应用正在退出\n      if (!isAppQuitting) {\n        event.preventDefault();\n        mainWindow.hide();\n        return;\n      }\n    }\n    // 在其他平台上，或者应用正在退出时，允许正常关闭\n  });\n\n  mainWindow.on('ready-to-show', () => {\n    const [width, height] = mainWindow.getSize();\n    console.log(`窗口显示前的大小: ${width}x${height}`);\n\n    // 强制确保窗口使用正确的大小\n    if (savedState && !savedState.isMaximized) {\n      mainWindow.setSize(savedState.width, savedState.height, false);\n    }\n\n    // 显示窗口\n    mainWindow.show();\n    // 应用页面内容缩放\n    applyContentZoom(mainWindow);\n\n    // 再次检查窗口大小是否正确应用\n    setTimeout(() => {\n      if (!mainWindow.isDestroyed() && !mainWindow.isMaximized()) {\n        const [currentWidth, currentHeight] = mainWindow.getSize();\n        if (savedState && !savedState.isMaximized) {\n          if (\n            Math.abs(currentWidth - savedState.width) > 2 ||\n            Math.abs(currentHeight - savedState.height) > 2\n          ) {\n            console.log(\n              `窗口大小不匹配，再次调整: 当前=${currentWidth}x${currentHeight}, 目标=${savedState.width}x${savedState.height}`\n            );\n            mainWindow.setSize(savedState.width, savedState.height, false);\n          }\n        }\n      }\n    }, 100);\n  });\n\n  mainWindow.webContents.setWindowOpenHandler((details) => {\n    shell.openExternal(details.url);\n    return { action: 'deny' };\n  });\n\n  // HMR for renderer base on electron-vite cli.\n  // Load the remote URL for development or the local html file for production.\n  if (is.dev && process.env.ELECTRON_RENDERER_URL) {\n    mainWindow.webContents.openDevTools({ mode: 'detach' });\n    mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);\n\n    // 注册快捷键 打开开发者工具\n    globalShortcut.register('CommandOrControl+Shift+I', () => {\n      mainWindow.webContents.openDevTools({ mode: 'detach' });\n    });\n  } else {\n    mainWindow.loadFile(join(__dirname, '../renderer/index.html'));\n  }\n\n  initWindowSizeHandlers(mainWindow);\n\n  // 保存主窗口引用\n  mainWindowInstance = mainWindow;\n\n  return mainWindow;\n}\n"
  },
  {
    "path": "src/main/server.ts",
    "content": "import { ipcMain } from 'electron';\nimport Store from 'electron-store';\nimport fs from 'fs';\nimport server from 'netease-cloud-music-api-alger/server';\nimport os from 'os';\nimport path from 'path';\n\nimport { type Platform, unblockMusic } from './unblockMusic';\n\nconst store = new Store();\nif (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {\n  fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');\n}\n\n// 设置音乐解析的处理程序\nipcMain.handle('unblock-music', async (_event, id, songData, enabledSources) => {\n  try {\n    const result = await unblockMusic(id, songData, 1, enabledSources as Platform[]);\n    return result;\n  } catch (error) {\n    console.error('音乐解析失败:', error);\n    return { error: (error as Error).message || '未知错误' };\n  }\n});\n\n/**\n * 检查端口是否可用\n */\nfunction checkPortAvailable(port: number): Promise<boolean> {\n  return new Promise((resolve) => {\n    const net = require('net');\n    const tester = net\n      .createServer()\n      .once('error', () => {\n        resolve(false);\n      })\n      .once('listening', () => {\n        tester.close(() => resolve(true));\n      })\n      .listen(port);\n  });\n}\n\nasync function startMusicApi(): Promise<void> {\n  console.log('MUSIC API STARTING...');\n\n  const settings = store.get('set') as any;\n  let port = settings?.musicApiPort || 30488;\n  const maxRetries = 10;\n\n  // 检查端口是否可用，如果不可用则尝试下一个端口\n  for (let i = 0; i < maxRetries; i++) {\n    const isAvailable = await checkPortAvailable(port);\n    if (isAvailable) {\n      break;\n    }\n    console.log(`端口 ${port} 被占用，尝试切换到端口 ${port + 1}`);\n    port++;\n  }\n\n  // 如果端口发生变化，保存新端口到配置\n  const originalPort = settings?.musicApiPort || 30488;\n  if (port !== originalPort) {\n    console.log(`端口从 ${originalPort} 切换到 ${port}`);\n    store.set('set', { ...settings, musicApiPort: port });\n  }\n\n  try {\n    await server.serveNcmApi({\n      port\n    });\n    console.log(`MUSIC API STARTED on port ${port}`);\n  } catch (error) {\n    console.error(`MUSIC API 启动失败:`, error);\n    throw error;\n  }\n}\n\nexport { startMusicApi };\n"
  },
  {
    "path": "src/main/set.json",
    "content": "{\n  \"isProxy\": false,\n  \"proxyConfig\": {\n    \"enable\": false,\n    \"protocol\": \"http\",\n    \"host\": \"127.0.0.1\",\n    \"port\": 7890\n  },\n  \"enableRealIP\": false,\n  \"realIP\": \"\",\n  \"noAnimate\": false,\n  \"animationSpeed\": 1,\n  \"author\": \"Alger\",\n  \"authorUrl\": \"https://github.com/algerkong\",\n  \"musicApiPort\": 30488,\n  \"closeAction\": \"ask\",\n  \"musicQuality\": \"higher\",\n  \"lyricTranslationEngine\": \"none\",\n  \"fontFamily\": \"system-ui\",\n  \"fontScope\": \"global\",\n  \"autoPlay\": false,\n  \"downloadPath\": \"\",\n  \"language\": \"zh-CN\",\n  \"alwaysShowDownloadButton\": false,\n  \"unlimitedDownload\": false,\n  \"enableMusicUnblock\": true,\n  \"enabledMusicSources\": [\"migu\", \"kugou\", \"pyncmd\"],\n  \"showTopAction\": false,\n  \"contentZoomFactor\": 1,\n  \"autoTheme\": false,\n  \"manualTheme\": \"light\",\n  \"isMenuExpanded\": false,\n  \"customApiPlugin\": \"\",\n  \"customApiPluginName\": \"\",\n  \"lxMusicScripts\": [],\n  \"activeLxMusicApiId\": null,\n  \"enableGpuAcceleration\": true\n}\n"
  },
  {
    "path": "src/main/unblockMusic.ts",
    "content": "import match from '@unblockneteasemusic/server';\n\ntype Platform = 'qq' | 'migu' | 'kugou' | 'kuwo' | 'pyncmd' | 'joox' | 'bilibili';\n\ninterface SongData {\n  name: string;\n  artists: Array<{ name: string }>;\n  album?: { name: string };\n  ar?: Array<{ name: string }>;\n  al?: { name: string };\n}\n\ninterface ResponseData {\n  url: string;\n  br: number;\n  size: number;\n  md5?: string;\n  platform?: Platform;\n  gain?: number;\n}\n\ninterface UnblockResult {\n  data: {\n    data: ResponseData;\n    params: {\n      id: number;\n      type: 'song';\n    };\n  };\n}\n\n// 所有可用平台\nexport const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'kuwo', 'pyncmd', 'bilibili'];\n\n/**\n * 确保对象数据结构完整，处理null或undefined的情况\n * @param data 需要处理的数据对象\n */\nfunction ensureDataStructure(data: any): any {\n  // 如果数据本身为空，则返回一个基本结构\n  if (!data) {\n    return {\n      name: '',\n      artists: [],\n      album: { name: '' }\n    };\n  }\n\n  // 确保name字段存在\n  if (data.name === undefined || data.name === null) {\n    data.name = '';\n  }\n\n  // 确保artists字段存在且为数组\n  if (!data.artists || !Array.isArray(data.artists)) {\n    data.artists = data.ar && Array.isArray(data.ar) ? data.ar : [];\n  }\n\n  // 确保artists中的每个元素都有name属性\n  if (data.artists.length > 0) {\n    data.artists = data.artists.map((artist) => {\n      return artist ? { name: artist.name || '' } : { name: '' };\n    });\n  }\n\n  // 确保album对象存在并有name属性\n  if (!data.album || typeof data.album !== 'object') {\n    data.album = data.al && typeof data.al === 'object' ? data.al : { name: '' };\n  }\n\n  if (!data.album.name) {\n    data.album.name = '';\n  }\n\n  return data;\n}\n\n/**\n * 音乐解析函数\n * @param id 歌曲ID\n * @param songData 歌曲信息\n * @param retryCount 重试次数\n * @param enabledPlatforms 启用的平台列表，默认为所有平台\n * @returns Promise<UnblockResult>\n */\nconst unblockMusic = async (\n  id: number | string,\n  songData: SongData,\n  retryCount = 1,\n  enabledPlatforms?: Platform[]\n): Promise<UnblockResult> => {\n  // 过滤 enabledPlatforms，确保只包含 ALL_PLATFORMS 中存在的平台\n  const filteredPlatforms = enabledPlatforms\n    ? enabledPlatforms.filter((platform) => ALL_PLATFORMS.includes(platform))\n    : ALL_PLATFORMS;\n\n  // 处理歌曲数据，确保数据结构完整\n  const processedSongData = ensureDataStructure(songData);\n\n  const retry = async (attempt: number): Promise<UnblockResult> => {\n    try {\n      const data = await match(parseInt(String(id), 10), filteredPlatforms, processedSongData);\n      const result: UnblockResult = {\n        data: {\n          data,\n          params: {\n            id: parseInt(String(id), 10),\n            type: 'song'\n          }\n        }\n      };\n      return result;\n    } catch (err) {\n      if (attempt < retryCount) {\n        // 延迟重试，每次重试增加延迟时间\n        await new Promise((resolve) => setTimeout(resolve, 100 * attempt));\n        return retry(attempt + 1);\n      }\n\n      // 所有重试都失败后，抛出详细错误\n      throw new Error(\n        `音乐解析失败 (ID: ${id}): ${err instanceof Error ? err.message : '未知错误'}`\n      );\n    }\n  };\n\n  return retry(1);\n};\n\nexport { type Platform, type ResponseData, type SongData, unblockMusic, type UnblockResult };\n"
  },
  {
    "path": "src/preload/index.d.ts",
    "content": "import { ElectronAPI } from '@electron-toolkit/preload';\n\ninterface API {\n  minimize: () => void;\n  maximize: () => void;\n  close: () => void;\n  quitApp: () => void;\n  dragStart: (data: any) => void;\n  miniTray: () => void;\n  miniWindow: () => void;\n  restore: () => void;\n  restart: () => void;\n  resizeWindow: (width: number, height: number) => void;\n  resizeMiniWindow: (showPlaylist: boolean) => void;\n  openLyric: () => void;\n  sendLyric: (data: any) => void;\n  sendSong: (data: any) => void;\n  unblockMusic: (id: number, data: any, enabledSources?: string[]) => Promise<any>;\n  onLyricWindowClosed: (callback: () => void) => void;\n  startDownload: (url: string) => void;\n  onDownloadProgress: (callback: (progress: number, status: string) => void) => void;\n  onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => void;\n  onLanguageChanged: (callback: (locale: string) => void) => void;\n  removeDownloadListeners: () => void;\n  importCustomApiPlugin: () => Promise<{ name: string; content: string } | null>;\n  importLxMusicScript: () => Promise<{ name: string; content: string } | null>;\n  invoke: (channel: string, ...args: any[]) => Promise<any>;\n  getSearchSuggestions: (keyword: string) => Promise<any>;\n  lxMusicHttpRequest: (request: { url: string; options: any; requestId: string }) => Promise<any>;\n  lxMusicHttpCancel: (requestId: string) => Promise<void>;\n}\n\n// 自定义IPC渲染进程通信接口\ninterface IpcRenderer {\n  send: (channel: string, ...args: any[]) => void;\n  invoke: (channel: string, ...args: any[]) => Promise<any>;\n  on: (channel: string, listener: (...args: any[]) => void) => () => void;\n  removeAllListeners: (channel: string) => void;\n}\n\ndeclare global {\n  interface Window {\n    electron: ElectronAPI;\n    api: API;\n    ipcRenderer: IpcRenderer;\n    $message: any;\n  }\n}\n"
  },
  {
    "path": "src/preload/index.ts",
    "content": "import { electronAPI } from '@electron-toolkit/preload';\nimport { contextBridge, ipcRenderer } from 'electron';\n\n// Custom APIs for renderer\nconst api = {\n  minimize: () => ipcRenderer.send('minimize-window'),\n  maximize: () => ipcRenderer.send('maximize-window'),\n  close: () => ipcRenderer.send('close-window'),\n  quitApp: () => ipcRenderer.send('quit-app'),\n  dragStart: (data) => ipcRenderer.send('drag-start', data),\n  miniTray: () => ipcRenderer.send('mini-tray'),\n  miniWindow: () => ipcRenderer.send('mini-window'),\n  restore: () => ipcRenderer.send('restore-window'),\n  restart: () => ipcRenderer.send('restart'),\n  resizeWindow: (width, height) => ipcRenderer.send('resize-window', width, height),\n  resizeMiniWindow: (showPlaylist) => ipcRenderer.send('resize-mini-window', showPlaylist),\n  openLyric: () => ipcRenderer.send('open-lyric'),\n  sendLyric: (data) => ipcRenderer.send('send-lyric', data),\n  sendSong: (data) => ipcRenderer.send('update-current-song', data),\n  unblockMusic: (id, data, enabledSources) =>\n    ipcRenderer.invoke('unblock-music', id, data, enabledSources),\n  importCustomApiPlugin: () => ipcRenderer.invoke('import-custom-api-plugin'),\n  importLxMusicScript: () => ipcRenderer.invoke('import-lx-music-script'),\n  // 歌词窗口关闭事件\n  onLyricWindowClosed: (callback: () => void) => {\n    ipcRenderer.on('lyric-window-closed', () => callback());\n  },\n  // 更新相关\n  startDownload: (url: string) => ipcRenderer.send('start-download', url),\n  onDownloadProgress: (callback: (progress: number, status: string) => void) => {\n    ipcRenderer.on('download-progress', (_event, progress, status) => callback(progress, status));\n  },\n  onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => {\n    ipcRenderer.on('download-complete', (_event, success, filePath) => callback(success, filePath));\n  },\n  // 语言相关\n  onLanguageChanged: (callback: (locale: string) => void) => {\n    ipcRenderer.on('language-changed', (_event, locale) => {\n      callback(locale);\n    });\n  },\n  removeDownloadListeners: () => {\n    ipcRenderer.removeAllListeners('download-progress');\n    ipcRenderer.removeAllListeners('download-complete');\n  },\n  // 歌词缓存相关\n  invoke: (channel: string, ...args: any[]) => {\n    const validChannels = [\n      'get-lyrics',\n      'clear-lyrics-cache',\n      'get-system-fonts',\n      'get-cached-lyric',\n      'cache-lyric',\n      'clear-lyric-cache'\n    ];\n    if (validChannels.includes(channel)) {\n      return ipcRenderer.invoke(channel, ...args);\n    }\n    return Promise.reject(new Error(`未授权的 IPC 通道: ${channel}`));\n  },\n  // 搜索建议\n  getSearchSuggestions: (keyword: string) => ipcRenderer.invoke('get-search-suggestions', keyword),\n\n  // 落雪音乐 HTTP 请求（绕过 CORS）\n  lxMusicHttpRequest: (request: { url: string; options: any; requestId: string }) =>\n    ipcRenderer.invoke('lx-music-http-request', request),\n\n  lxMusicHttpCancel: (requestId: string) => ipcRenderer.invoke('lx-music-http-cancel', requestId)\n};\n\n// 创建带类型的ipcRenderer对象，暴露给渲染进程\nconst ipc = {\n  // 发送消息到主进程（无返回值）\n  send: (channel: string, ...args: any[]) => {\n    ipcRenderer.send(channel, ...args);\n  },\n  // 调用主进程方法（有返回值）\n  invoke: (channel: string, ...args: any[]) => {\n    return ipcRenderer.invoke(channel, ...args);\n  },\n  // 监听主进程消息\n  on: (channel: string, listener: (...args: any[]) => void) => {\n    ipcRenderer.on(channel, (_, ...args) => listener(...args));\n    return () => {\n      ipcRenderer.removeListener(channel, listener);\n    };\n  },\n  // 移除所有监听器\n  removeAllListeners: (channel: string) => {\n    ipcRenderer.removeAllListeners(channel);\n  }\n};\n\n// Use `contextBridge` APIs to expose Electron APIs to\n// renderer only if context isolation is enabled, otherwise\n// just add to the DOM global.\nif (process.contextIsolated) {\n  try {\n    contextBridge.exposeInMainWorld('electron', electronAPI);\n    contextBridge.exposeInMainWorld('api', api);\n    contextBridge.exposeInMainWorld('ipcRenderer', ipc);\n  } catch (error) {\n    console.error(error);\n  }\n} else {\n  // @ts-ignore (define in dts)\n  window.electron = electronAPI;\n  // @ts-ignore (define in dts)\n  window.api = api;\n  // @ts-ignore (define in dts)\n  window.ipcRenderer = ipc;\n}\n"
  },
  {
    "path": "src/renderer/App.vue",
    "content": "<template>\n  <div class=\"app-container\" :class=\"{ mobile: isMobile, noElectron: !isElectron }\">\n    <n-config-provider :theme=\"theme === 'dark' ? darkTheme : lightTheme\">\n      <n-dialog-provider>\n        <n-message-provider>\n          <router-view></router-view>\n          <traffic-warning-drawer v-if=\"!isElectron\"></traffic-warning-drawer>\n          <disclaimer-modal></disclaimer-modal>\n        </n-message-provider>\n      </n-dialog-provider>\n    </n-config-provider>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { cloneDeep } from 'lodash';\nimport { darkTheme, lightTheme } from 'naive-ui';\nimport { computed, nextTick, onMounted, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\n\nimport DisclaimerModal from '@/components/common/DisclaimerModal.vue';\nimport TrafficWarningDrawer from '@/components/TrafficWarningDrawer.vue';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { useSettingsStore } from '@/store/modules/settings';\nimport { useUserStore } from '@/store/modules/user';\nimport { isElectron, isLyricWindow } from '@/utils';\nimport { checkLoginStatus } from '@/utils/auth';\n\nimport { initAudioListeners, initMusicHook } from './hooks/MusicHook';\nimport { audioService } from './services/audioService';\nimport { initLxMusicRunner } from './services/LxMusicSourceRunner';\nimport { isMobile } from './utils';\nimport { useAppShortcuts } from './utils/appShortcuts';\n\nconst { locale } = useI18n();\nconst settingsStore = useSettingsStore();\nconst playerStore = usePlayerStore();\nconst userStore = useUserStore();\nconst router = useRouter();\n\n// 监听语言变化\nwatch(\n  () => settingsStore.setData.language,\n  (newLanguage) => {\n    if (newLanguage && newLanguage !== locale.value) {\n      locale.value = newLanguage;\n    }\n  },\n  { immediate: true }\n);\n\nconst theme = computed(() => {\n  return settingsStore.theme;\n});\n\n// 监听字体变化并应用\nwatch(\n  () => [settingsStore.setData.fontFamily, settingsStore.setData.fontScope],\n  ([newFont, fontScope]) => {\n    const appElement = document.body;\n    if (newFont && fontScope === 'global') {\n      appElement.style.fontFamily = newFont;\n    } else {\n      appElement.style.fontFamily = '';\n    }\n  }\n);\n\nconst handleSetLanguage = (value: string) => {\n  console.log('应用语言变更:', value);\n  if (value) {\n    locale.value = value;\n  }\n};\n\nif (!isLyricWindow.value) {\n  settingsStore.initializeSettings();\n  settingsStore.initializeTheme();\n  settingsStore.initializeSystemFonts();\n\n  // 初始化登录状态 - 从 localStorage 恢复用户信息和登录类型\n  const loginInfo = checkLoginStatus();\n  if (loginInfo.isLoggedIn) {\n    if (loginInfo.user && !userStore.user) {\n      userStore.setUser(loginInfo.user);\n    }\n    if (loginInfo.loginType && !userStore.loginType) {\n      userStore.setLoginType(loginInfo.loginType);\n    }\n  }\n}\n\nhandleSetLanguage(settingsStore.setData.language);\n\n// 监听迷你模式状态\nif (isElectron) {\n  window.api.onLanguageChanged(handleSetLanguage);\n  window.electron.ipcRenderer.on('mini-mode', (_, value) => {\n    settingsStore.setMiniMode(value);\n    if (value) {\n      // 存储当前路由\n      localStorage.setItem('currentRoute', router.currentRoute.value.path);\n      router.push('/mini');\n    } else {\n      // 恢复当前路由\n      const currentRoute = localStorage.getItem('currentRoute');\n      if (currentRoute) {\n        router.push(currentRoute);\n        localStorage.removeItem('currentRoute');\n      } else {\n        router.push('/');\n      }\n    }\n  });\n}\n\n// 使用应用内快捷键\nuseAppShortcuts();\n\nonMounted(async () => {\n  playerStore.setIsPlay(false);\n  if (isLyricWindow.value) {\n    return;\n  }\n  // 初始化 MusicHook，注入 playerStore\n  initMusicHook(playerStore);\n  // 初始化播放状态\n  await playerStore.initializePlayState();\n\n  // 初始化落雪音源（如果有激活的音源）\n  const activeLxApiId = settingsStore.setData?.activeLxMusicApiId;\n  if (activeLxApiId) {\n    const lxMusicScripts = settingsStore.setData?.lxMusicScripts || [];\n    const activeScript = lxMusicScripts.find((script: any) => script.id === activeLxApiId);\n    if (activeScript && activeScript.script) {\n      try {\n        console.log('[App] 初始化激活的落雪音源:', activeScript.name);\n        await initLxMusicRunner(activeScript.script);\n      } catch (error) {\n        console.error('[App] 初始化落雪音源失败:', error);\n      }\n    }\n  }\n\n  // 如果有正在播放的音乐，则初始化音频监听器\n  if (playerStore.playMusic && playerStore.playMusic.id) {\n    // 使用 nextTick 确保 DOM 更新后再初始化\n    await nextTick();\n    initAudioListeners();\n    if (isElectron) {\n      window.api.sendSong(cloneDeep(playerStore.playMusic));\n    }\n  }\n\n  audioService.releaseOperationLock();\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.app-container {\n  @apply h-full w-full;\n  user-select: none;\n}\n\n.mobile {\n  .text-base {\n    font-size: 14px !important;\n  }\n}\n\n.html:has(.mobile) {\n  font-size: 14px;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/api/artist.ts",
    "content": "import request from '@/utils/request';\n\n// 获取歌手详情\nexport const getArtistDetail = (id) => {\n  return request.get('/artist/detail', { params: { id } });\n};\n\n// 获取歌手热门歌曲\nexport const getArtistTopSongs = (params) => {\n  return request.get('/artist/songs', {\n    params: {\n      ...params,\n      order: 'hot'\n    }\n  });\n};\n\n// 获取歌手专辑\nexport const getArtistAlbums = (params) => {\n  return request.get('/artist/album', { params });\n};\n"
  },
  {
    "path": "src/renderer/api/bilibili.ts",
    "content": "import type { IBilibiliPage, IBilibiliPlayUrl, IBilibiliVideoDetail } from '@/types/bilibili';\nimport type { SongResult } from '@/types/music';\nimport { getSetData, isElectron } from '@/utils';\nimport request from '@/utils/request';\n\ninterface ISearchParams {\n  keyword: string;\n  page?: number;\n  pagesize?: number;\n  search_type?: string;\n}\n\n/**\n * 搜索B站视频（带自动重试）\n * 最多重试10次，每次间隔100ms\n * @param params 搜索参数\n */\nexport const searchBilibili = async (params: ISearchParams): Promise<any> => {\n  console.log('调用B站搜索API，参数:', params);\n  const maxRetries = 10;\n  const delayMs = 100;\n  const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));\n\n  let lastError: unknown = null;\n  for (let attempt = 1; attempt <= maxRetries; attempt++) {\n    try {\n      const response = await request.get('/bilibili/search', { params });\n      console.log('B站搜索API响应:', response);\n      const hasTitle = Boolean(response?.data?.data?.result?.length);\n      if (response?.status === 200 && hasTitle) {\n        return response;\n      }\n\n      lastError = new Error(\n        `搜索结果不符合成功条件(缺少 data.title ) (attempt ${attempt}/${maxRetries})`\n      );\n      console.warn('B站搜索API响应不符合要求，将重试。调试信息：', {\n        status: response?.status,\n        hasData: Boolean(response?.data),\n        hasInnerData: Boolean(response?.data?.data),\n        title: response?.data?.data?.title\n      });\n    } catch (error) {\n      lastError = error;\n      console.warn(`B站搜索API错误[第${attempt}次]，将重试:`, error);\n    }\n\n    if (attempt === maxRetries) {\n      console.error('B站搜索API重试达到上限，仍然失败');\n      if (lastError instanceof Error) throw lastError;\n      throw new Error('B站搜索失败且达到最大重试次数');\n    }\n\n    await delay(delayMs);\n  }\n  // 理论上不会到达这里，添加以满足TS控制流分析\n  throw new Error('B站搜索在重试后未返回有效结果');\n};\n\ninterface IBilibiliResponse<T> {\n  code: number;\n  message: string;\n  ttl: number;\n  data: T;\n}\n\n/**\n * 获取B站视频详情\n * @param bvid B站视频BV号\n * @returns 视频详情响应\n */\nexport const getBilibiliVideoDetail = (\n  bvid: string\n): Promise<IBilibiliResponse<IBilibiliVideoDetail>> => {\n  console.log('调用B站视频详情API，bvid:', bvid);\n  return new Promise((resolve, reject) => {\n    request\n      .get('/bilibili/video/detail', {\n        params: { bvid }\n      })\n      .then((response) => {\n        console.log('B站视频详情API响应:', response.status);\n\n        // 检查响应状态和数据格式\n        if (response.status === 200 && response.data && response.data.data) {\n          console.log('B站视频详情API成功，标题:', response.data.data.title);\n          resolve(response.data);\n        } else {\n          console.error('B站视频详情API响应格式不正确:', response.data);\n          reject(new Error('获取视频详情响应格式不正确'));\n        }\n      })\n      .catch((error) => {\n        console.error('B站视频详情API错误:', error);\n        reject(error);\n      });\n  });\n};\n\n/**\n * 获取B站视频播放地址\n * @param bvid B站视频BV号\n * @param cid 视频分P的id\n * @param qn 视频质量，默认为0\n * @param fnval 视频格式标志，默认为80\n * @param fnver 视频格式版本，默认为0\n * @param fourk 是否允许4K视频，默认为1\n * @returns 视频播放地址响应\n */\nexport const getBilibiliPlayUrl = (\n  bvid: string,\n  cid: number,\n  qn: number = 0,\n  fnval: number = 80,\n  fnver: number = 0,\n  fourk: number = 1\n): Promise<IBilibiliResponse<IBilibiliPlayUrl>> => {\n  console.log('调用B站视频播放地址API，bvid:', bvid, 'cid:', cid);\n  return new Promise((resolve, reject) => {\n    request\n      .get('/bilibili/playurl', {\n        params: {\n          bvid,\n          cid,\n          qn,\n          fnval,\n          fnver,\n          fourk\n        }\n      })\n      .then((response) => {\n        console.log('B站视频播放地址API响应:', response.status);\n\n        // 检查响应状态和数据格式\n        if (response.status === 200 && response.data && response.data.data) {\n          if (response.data.data.dash?.audio?.length > 0) {\n            console.log(\n              'B站视频播放地址API成功，获取到',\n              response.data.data.dash.audio.length,\n              '个音频地址'\n            );\n          } else if (response.data.data.durl?.length > 0) {\n            console.log(\n              'B站视频播放地址API成功，获取到',\n              response.data.data.durl.length,\n              '个播放地址'\n            );\n          }\n          resolve(response.data);\n        } else {\n          console.error('B站视频播放地址API响应格式不正确:', response.data);\n          reject(new Error('获取视频播放地址响应格式不正确'));\n        }\n      })\n      .catch((error) => {\n        console.error('B站视频播放地址API错误:', error);\n        reject(error);\n      });\n  });\n};\n\nexport const getBilibiliProxyUrl = (url: string) => {\n  const setData = getSetData();\n  const baseURL = isElectron\n    ? `http://127.0.0.1:${setData?.musicApiPort}`\n    : import.meta.env.VITE_API;\n  const AUrl = url.startsWith('http') ? url : `https:${url}`;\n  return `${baseURL}/bilibili/stream-proxy?url=${encodeURIComponent(AUrl)}`;\n};\n\nexport const getBilibiliAudioUrl = async (bvid: string, cid: number): Promise<string> => {\n  console.log('获取B站音频URL', { bvid, cid });\n  try {\n    const res = await getBilibiliPlayUrl(bvid, cid);\n    const playUrlData = res.data;\n    let url = '';\n\n    if (playUrlData.dash && playUrlData.dash.audio && playUrlData.dash.audio.length > 0) {\n      url = playUrlData.dash.audio[playUrlData.dash.audio.length - 1].baseUrl;\n    } else if (playUrlData.durl && playUrlData.durl.length > 0) {\n      url = playUrlData.durl[0].url;\n    } else {\n      throw new Error('未找到可用的音频地址');\n    }\n\n    return getBilibiliProxyUrl(url);\n  } catch (error) {\n    console.error('获取B站音频URL失败:', error);\n    throw error;\n  }\n};\n\n// 根据音乐名称搜索并直接返回音频URL\nexport const searchAndGetBilibiliAudioUrl = async (keyword: string): Promise<string> => {\n  try {\n    // 搜索B站视频，取第一页第一个结果\n    const res = await searchBilibili({ keyword, page: 1, pagesize: 1 });\n    if (!res) {\n      throw new Error('B站搜索返回为空');\n    }\n    const result = res.data?.data?.result;\n    if (!result || result.length === 0) {\n      throw new Error('未找到相关B站视频');\n    }\n    const first = result[0];\n    const bvid = first.bvid;\n    // 需要获取视频详情以获得cid\n    const detailRes = await getBilibiliVideoDetail(bvid);\n    const pages = detailRes.data.pages;\n    if (!pages || pages.length === 0) {\n      throw new Error('未找到视频分P信息');\n    }\n    const cid = pages[0].cid;\n    // 获取音频URL\n    return await getBilibiliAudioUrl(bvid, cid);\n  } catch (error) {\n    console.error('根据名称搜索B站音频URL失败:', error);\n    throw error;\n  }\n};\n\n/**\n * 解析B站ID格式\n * @param biliId B站ID，可能是字符串格式（bvid--pid--cid）\n * @returns 解析后的对象 {bvid, pid, cid} 或 null\n */\nexport const parseBilibiliId = (\n  biliId: string | number\n): { bvid: string; pid: string; cid: number } | null => {\n  const strBiliId = String(biliId);\n\n  if (strBiliId.includes('--')) {\n    const [bvid, pid, cid] = strBiliId.split('--');\n    if (!bvid || !pid || !cid) {\n      console.warn(`B站ID格式错误: ${strBiliId}, 正确格式应为 bvid--pid--cid`);\n      return null;\n    }\n    return { bvid, pid, cid: Number(cid) };\n  }\n\n  return null;\n};\n\n/**\n * 创建默认的Artist对象\n * @param name 艺术家名称\n * @param id 艺术家ID\n * @returns Artist对象\n */\nconst createDefaultArtist = (name: string, id: number = 0) => ({\n  name,\n  id,\n  picId: 0,\n  img1v1Id: 0,\n  briefDesc: '',\n  img1v1Url: '',\n  albumSize: 0,\n  alias: [],\n  trans: '',\n  musicSize: 0,\n  topicPerson: 0,\n  picUrl: ''\n});\n\n/**\n * 创建默认的Album对象\n * @param name 专辑名称\n * @param picUrl 专辑图片URL\n * @param artistName 艺术家名称\n * @param artistId 艺术家ID\n * @returns Album对象\n */\nconst createDefaultAlbum = (\n  name: string,\n  picUrl: string,\n  artistName: string,\n  artistId: number = 0\n) => ({\n  name,\n  picUrl,\n  id: 0,\n  type: '',\n  size: 0,\n  picId: 0,\n  blurPicUrl: '',\n  companyId: 0,\n  pic: 0,\n  publishTime: 0,\n  description: '',\n  tags: '',\n  company: '',\n  briefDesc: '',\n  artist: createDefaultArtist(artistName, artistId),\n  songs: [],\n  alias: [],\n  status: 0,\n  copyrightId: 0,\n  commentThreadId: '',\n  artists: [],\n  subType: '',\n  transName: null,\n  onSale: false,\n  mark: 0,\n  picId_str: ''\n});\n\n/**\n * 创建基础的B站SongResult对象\n * @param config 配置对象\n * @returns SongResult对象\n */\nconst createBaseBilibiliSong = (config: {\n  id: string | number;\n  name: string;\n  picUrl: string;\n  artistName: string;\n  artistId?: number;\n  albumName: string;\n  bilibiliData?: { bvid: string; cid: number };\n  playMusicUrl?: string;\n  duration?: number;\n}): SongResult => {\n  const {\n    id,\n    name,\n    picUrl,\n    artistName,\n    artistId = 0,\n    albumName,\n    bilibiliData,\n    playMusicUrl,\n    duration\n  } = config;\n\n  const baseResult: SongResult = {\n    id,\n    name,\n    picUrl,\n    ar: [createDefaultArtist(artistName, artistId)],\n    al: createDefaultAlbum(albumName, picUrl, artistName, artistId),\n    count: 0,\n    source: 'bilibili' as const\n  };\n\n  if (bilibiliData) {\n    baseResult.bilibiliData = bilibiliData;\n  }\n\n  if (playMusicUrl) {\n    baseResult.playMusicUrl = playMusicUrl;\n  }\n\n  if (duration !== undefined) {\n    baseResult.duration = duration;\n  }\n\n  return baseResult as SongResult;\n};\n\n/**\n * 从B站视频详情和分P信息创建SongResult对象\n * @param videoDetail B站视频详情\n * @param page 分P信息\n * @param bvid B站视频ID\n * @returns SongResult对象\n */\nexport const createSongFromBilibiliVideo = (\n  videoDetail: IBilibiliVideoDetail,\n  page: IBilibiliPage,\n  bvid: string\n): SongResult => {\n  const pageName = page.part || '';\n  const title = `${pageName} - ${videoDetail.title}`;\n  const songId = `${bvid}--${page.page}--${page.cid}`;\n  const picUrl = getBilibiliProxyUrl(videoDetail.pic);\n\n  return createBaseBilibiliSong({\n    id: songId,\n    name: title,\n    picUrl,\n    artistName: videoDetail.owner.name,\n    artistId: videoDetail.owner.mid,\n    albumName: videoDetail.title,\n    bilibiliData: {\n      bvid,\n      cid: page.cid\n    }\n  });\n};\n\n/**\n * 创建简化的SongResult对象（用于搜索结果直接播放）\n * @param item 搜索结果项\n * @param audioUrl 音频URL\n * @returns SongResult对象\n */\nexport const createSimpleBilibiliSong = (item: any, audioUrl: string): SongResult => {\n  const duration = typeof item.duration === 'string' ? 0 : item.duration * 1000; // 转换为毫秒\n\n  return createBaseBilibiliSong({\n    id: item.id,\n    name: item.title,\n    picUrl: item.pic,\n    artistName: item.author,\n    albumName: item.title,\n    playMusicUrl: audioUrl,\n    duration\n  });\n};\n\n/**\n * 批量处理B站视频，从ID列表获取SongResult列表\n * @param bilibiliIds B站ID列表\n * @returns SongResult列表\n */\nexport const processBilibiliVideos = async (\n  bilibiliIds: (string | number)[]\n): Promise<SongResult[]> => {\n  const bilibiliSongs: SongResult[] = [];\n\n  for (const biliId of bilibiliIds) {\n    const parsedId = parseBilibiliId(biliId);\n    if (!parsedId) continue;\n\n    try {\n      const res = await getBilibiliVideoDetail(parsedId.bvid);\n      const videoDetail = res.data;\n\n      // 找到对应的分P\n      const page = videoDetail.pages.find((p) => p.cid === parsedId.cid);\n      if (!page) {\n        console.warn(`未找到对应的分P: cid=${parsedId.cid}`);\n        continue;\n      }\n\n      const songData = createSongFromBilibiliVideo(videoDetail, page, parsedId.bvid);\n      bilibiliSongs.push(songData);\n    } catch (error) {\n      console.error(`获取B站视频详情失败 (${biliId}):`, error);\n    }\n  }\n\n  return bilibiliSongs;\n};\n"
  },
  {
    "path": "src/renderer/api/donation.ts",
    "content": "import axios from 'axios';\n\nexport interface Donor {\n  id: number;\n  name: string;\n  amount: number;\n  date: string;\n  message?: string;\n  avatar?: string;\n  badge: string;\n  badgeColor: string;\n}\n\n/**\n * 获取捐赠列表\n */\nexport const getDonationList = async (): Promise<Donor[]> => {\n  const { data } = await axios.get('http://donate.alger.fun/api/donations');\n  return data;\n};\n"
  },
  {
    "path": "src/renderer/api/gdmusic.ts",
    "content": "import axios from 'axios';\n\nimport type { MusicSourceType } from '@/types/music';\n\n/**\n * GD音乐台解析服务\n */\nexport interface GDMusicResponse {\n  url: string;\n  br: number;\n  size: number;\n  md5: string;\n  platform: string;\n  gain: number;\n}\n\nexport interface ParsedMusicResult {\n  data: {\n    data: GDMusicResponse;\n    params: {\n      id: number;\n      type: string;\n    };\n  };\n}\n\n/**\n * 从GD音乐台解析音乐URL\n * @param id 音乐ID\n * @param data 音乐数据，包含名称和艺术家信息\n * @param quality 音质设置\n * @param timeout 超时时间(毫秒)，默认15000ms\n * @returns 解析后的音乐URL及相关信息\n */\nexport const parseFromGDMusic = async (\n  id: number,\n  data: any,\n  quality: string = '999',\n  timeout: number = 15000\n): Promise<ParsedMusicResult | null> => {\n  // 创建一个超时Promise\n  const timeoutPromise = new Promise<null>((_, reject) => {\n    setTimeout(() => {\n      reject(new Error('GD音乐台解析超时'));\n    }, timeout);\n  });\n\n  try {\n    // 使用Promise.race竞争主解析流程和超时\n    return await Promise.race([\n      (async () => {\n        // 处理不同数据结构\n        if (!data) {\n          console.error('GD音乐台解析：歌曲数据为空');\n          throw new Error('歌曲数据为空');\n        }\n\n        const songName = data.name || '';\n        let artistNames = '';\n\n        // 处理不同的艺术家字段结构\n        if (data.artists && Array.isArray(data.artists)) {\n          artistNames = data.artists.map((artist) => artist.name).join(' ');\n        } else if (data.ar && Array.isArray(data.ar)) {\n          artistNames = data.ar.map((artist) => artist.name).join(' ');\n        } else if (data.artist) {\n          artistNames = typeof data.artist === 'string' ? data.artist : '';\n        }\n\n        const searchQuery = `${songName} ${artistNames}`.trim();\n\n        if (!searchQuery || searchQuery.length < 2) {\n          console.error('GD音乐台解析：搜索查询过短', { name: songName, artists: artistNames });\n          throw new Error('搜索查询过短');\n        }\n\n        // 所有可用的音乐源 netease、joox、tidal\n        const allSources = ['joox', 'tidal', 'netease'] as MusicSourceType[];\n\n        console.log('GD音乐台开始搜索:', searchQuery);\n\n        // 依次尝试所有音源\n        for (const source of allSources) {\n          try {\n            const result = await searchAndGetUrl(source, searchQuery, quality);\n            if (result) {\n              console.log(`GD音乐台成功通过 ${result.source} 解析音乐!`);\n              // 返回符合原API格式的数据\n              return {\n                data: {\n                  data: {\n                    url: result.url.replace(/\\\\/g, ''),\n                    br: parseInt(result.br, 10) * 1000 || 320000,\n                    size: result.size || 0,\n                    md5: '',\n                    platform: 'gdmusic',\n                    gain: 0\n                  },\n                  params: {\n                    id: parseInt(String(id), 10),\n                    type: 'song'\n                  }\n                }\n              };\n            }\n          } catch (error) {\n            console.error(`GD音乐台 ${source} 音源解析失败:`, error);\n            // 该音源失败，继续尝试下一个音源\n            continue;\n          }\n        }\n\n        console.log('GD音乐台所有音源均解析失败');\n        return null;\n      })(),\n      timeoutPromise\n    ]);\n  } catch (error: any) {\n    if (error.message === 'GD音乐台解析超时') {\n      console.error('GD音乐台解析超时(15秒):', error);\n    } else {\n      console.error('GD音乐台解析完全失败:', error);\n    }\n    return null;\n  }\n};\n\ninterface GDMusicUrlResult {\n  url: string;\n  br: string;\n  size: number;\n  source: string;\n}\n\nconst baseUrl = 'https://music-api.gdstudio.xyz/api.php';\n\n/**\n * 在指定音源搜索歌曲并获取URL\n * @param source 音源\n * @param searchQuery 搜索关键词\n * @param quality 音质\n * @returns 音乐URL结果\n */\nasync function searchAndGetUrl(\n  source: MusicSourceType,\n  searchQuery: string,\n  quality: string\n): Promise<GDMusicUrlResult | null> {\n  // 1. 搜索歌曲\n  const searchUrl = `${baseUrl}?types=search&source=${source}&name=${encodeURIComponent(searchQuery)}&count=1&pages=1`;\n  console.log(`GD音乐台尝试音源 ${source} 搜索:`, searchUrl);\n\n  const searchResponse = await axios.get(searchUrl, { timeout: 5000 });\n\n  if (searchResponse.data && Array.isArray(searchResponse.data) && searchResponse.data.length > 0) {\n    const firstResult = searchResponse.data[0];\n    if (!firstResult || !firstResult.id) {\n      console.log(`GD音乐台 ${source} 搜索结果无效`);\n      return null;\n    }\n\n    const trackId = firstResult.id;\n    const trackSource = firstResult.source || source;\n\n    // 2. 获取歌曲URL\n    const songUrl = `${baseUrl}?types=url&source=${trackSource}&id=${trackId}&br=${quality}`;\n    console.log(`GD音乐台尝试获取 ${trackSource} 歌曲URL:`, songUrl);\n\n    const songResponse = await axios.get(songUrl, { timeout: 5000 });\n\n    if (songResponse.data && songResponse.data.url) {\n      return {\n        url: songResponse.data.url,\n        br: songResponse.data.br,\n        size: songResponse.data.size || 0,\n        source: trackSource\n      };\n    } else {\n      console.log(`GD音乐台 ${trackSource} 未返回有效URL`);\n      return null;\n    }\n  } else {\n    console.log(`GD音乐台 ${source} 搜索结果为空`);\n    return null;\n  }\n}\n"
  },
  {
    "path": "src/renderer/api/home.ts",
    "content": "import { IData } from '@/types';\nimport { IAlbumNew } from '@/types/album';\nimport { IDayRecommend } from '@/types/day_recommend';\nimport { IRecommendMusic } from '@/types/music';\nimport { IPlayListSort } from '@/types/playlist';\nimport { IHotSearch, ISearchKeyword } from '@/types/search';\nimport { IHotSinger } from '@/types/singer';\nimport request from '@/utils/request';\n\ninterface IHotSingerParams {\n  offset: number;\n  limit: number;\n}\n\ninterface IRecommendMusicParams {\n  limit: number;\n}\n\n// 获取热门歌手\nexport const getHotSinger = (params: IHotSingerParams) => {\n  return request.get<IHotSinger>('/top/artists', { params });\n};\n\n// 获取搜索推荐词\nexport const getSearchKeyword = () => {\n  return request.get<ISearchKeyword>('/search/default');\n};\n\n// 获取热门搜索\nexport const getHotSearch = () => {\n  return request.get<IHotSearch>('/search/hot/detail');\n};\n\n// 获取歌单分类\nexport const getPlaylistCategory = () => {\n  return request.get<IPlayListSort>('/playlist/catlist');\n};\n\n// 获取推荐音乐\nexport const getRecommendMusic = (params: IRecommendMusicParams) => {\n  return request.get<IRecommendMusic>('/personalized/newsong', { params });\n};\n\n// 获取每日推荐\nexport const getDayRecommend = () => {\n  return request.get<IData<IData<IDayRecommend>>>('/recommend/songs');\n};\n\n// 获取最新专辑推荐\nexport const getNewAlbum = () => {\n  return request.get<IAlbumNew>('/album/newest');\n};\n"
  },
  {
    "path": "src/renderer/api/list.ts",
    "content": "import { IList } from '@/types/list';\nimport type { IListDetail } from '@/types/listDetail';\nimport request from '@/utils/request';\n\ninterface IListByTagParams {\n  tag: string;\n  before: number;\n  limit: number;\n}\n\ninterface IListByCatParams {\n  cat: string;\n  offset: number;\n  limit: number;\n}\n\n// 根据tag 获取歌单列表\nexport function getListByTag(params: IListByTagParams) {\n  return request.get<IList>('/top/playlist/highquality', { params });\n}\n\n// 根据cat 获取歌单列表\nexport function getListByCat(params: IListByCatParams) {\n  return request.get('/top/playlist', {\n    params\n  });\n}\n\n// 获取推荐歌单\nexport function getRecommendList(limit: number = 30) {\n  return request.get('/personalized', { params: { limit } });\n}\n\n// 获取歌单详情\nexport function getListDetail(id: number | string) {\n  return request.get<IListDetail>('/playlist/detail', { params: { id } });\n}\n\n// 获取专辑内容\nexport function getAlbum(id: number | string) {\n  return request.get('/album', { params: { id } });\n}\n\n// 获取排行榜列表\nexport function getToplist() {\n  return request.get('/toplist');\n}\n"
  },
  {
    "path": "src/renderer/api/login.ts",
    "content": "import request from '@/utils/request';\n\n// 创建二维码key\n//  /login/qr/key\nexport function getQrKey() {\n  return request.get('/login/qr/key');\n}\n\n// 创建二维码\n// /login/qr/create\nexport function createQr(key: any) {\n  return request.get('/login/qr/create', { params: { key, qrimg: true } });\n}\n\n// 获取二维码状态\n//  /login/qr/check\nexport function checkQr(key: any) {\n  return request.get('/login/qr/check', { params: { key, noCookie: true } });\n}\n\n// 获取登录状态\n// /login/status\nexport function getLoginStatus() {\n  return request.get('/login/status');\n}\n\n// 获取用户信息\n// /user/account\nexport function getUserDetail() {\n  return request.get('/user/account');\n}\n\n// 退出登录\n// /logout\nexport function logout() {\n  return request.get('/logout');\n}\n\n// 手机号登录\n// /login/cellphone\nexport function loginByCellphone(phone: string, password: string) {\n  return request.post('/login/cellphone', {\n    phone,\n    password\n  });\n}\n\n// UID登录 - 通过用户ID获取用户信息\n// /user/detail\nexport function loginByUid(uid: string | number) {\n  return request.get('/user/detail', {\n    params: { uid }\n  });\n}\n"
  },
  {
    "path": "src/renderer/api/lxMusicStrategy.ts",
    "content": "/**\n * 落雪音乐 (LX Music) 音源解析策略\n *\n * 实现 MusicSourceStrategy 接口，作为落雪音源的解析入口\n */\n\nimport { getLxMusicRunner, initLxMusicRunner } from '@/services/LxMusicSourceRunner';\nimport { useSettingsStore } from '@/store';\nimport type { LxMusicInfo, LxQuality, LxSourceKey } from '@/types/lxMusic';\nimport { LX_SOURCE_NAMES, QUALITY_TO_LX } from '@/types/lxMusic';\nimport type { SongResult } from '@/types/music';\n\nimport type { MusicParseResult } from './musicParser';\nimport { CacheManager } from './musicParser';\n\n/**\n * 解析可能是 API 端点的 URL，获取真实音频 URL\n * 一些音源脚本返回的是 API 端点，需要额外请求才能获取真实音频 URL\n */\nconst resolveAudioUrl = async (url: string): Promise<string> => {\n  try {\n    // 检查是否看起来像 API 端点（包含 /api/ 且有查询参数）\n    const isApiEndpoint = url.includes('/api/') || (url.includes('?') && url.includes('type=url'));\n\n    if (!isApiEndpoint) {\n      // 看起来像直接的音频 URL，直接返回\n      return url;\n    }\n\n    console.log('[LxMusicStrategy] 检测到 API 端点，尝试解析真实 URL:', url);\n\n    // 尝试获取真实 URL\n    const response = await fetch(url, {\n      method: 'HEAD',\n      redirect: 'manual' // 不自动跟随重定向\n    });\n\n    // 检查是否是重定向\n    if (response.status >= 300 && response.status < 400) {\n      const location = response.headers.get('Location');\n      if (location) {\n        console.log('[LxMusicStrategy] API 返回重定向 URL:', location);\n        return location;\n      }\n    }\n\n    // 如果 HEAD 请求没有重定向，尝试 GET 请求\n    const getResponse = await fetch(url, {\n      redirect: 'follow'\n    });\n\n    // 检查 Content-Type\n    const contentType = getResponse.headers.get('Content-Type') || '';\n\n    // 如果是音频类型，返回最终 URL\n    if (contentType.includes('audio/') || contentType.includes('application/octet-stream')) {\n      console.log('[LxMusicStrategy] 解析到音频 URL:', getResponse.url);\n      return getResponse.url;\n    }\n\n    // 如果是 JSON，尝试解析\n    if (contentType.includes('application/json') || contentType.includes('text/json')) {\n      const json = await getResponse.json();\n      console.log('[LxMusicStrategy] API 返回 JSON:', json);\n\n      // 尝试从 JSON 中提取 URL（常见字段）\n      const audioUrl = json.url || json.data?.url || json.audio_url || json.link || json.src;\n      if (audioUrl && typeof audioUrl === 'string') {\n        console.log('[LxMusicStrategy] 从 JSON 中提取音频 URL:', audioUrl);\n        return audioUrl;\n      }\n    }\n\n    // 如果都不是，返回原始 URL（可能直接可用）\n    console.warn('[LxMusicStrategy] 无法解析 API 端点，返回原始 URL');\n    return url;\n  } catch (error) {\n    console.error('[LxMusicStrategy] URL 解析失败:', error);\n    // 解析失败时返回原始 URL\n    return url;\n  }\n};\n\n/**\n * 将 SongResult 转换为 LxMusicInfo 格式\n */\nconst convertToLxMusicInfo = (songResult: SongResult): LxMusicInfo => {\n  const artistName =\n    songResult.ar && songResult.ar.length > 0\n      ? songResult.ar.map((a) => a.name).join('、')\n      : songResult.artists && songResult.artists.length > 0\n        ? songResult.artists.map((a) => a.name).join('、')\n        : '';\n\n  const albumName = songResult.al?.name || (songResult.album as any)?.name || '';\n\n  const albumId = songResult.al?.id || (songResult.album as any)?.id || '';\n\n  // 计算时长（秒转分钟:秒格式）\n  const duration = songResult.dt || songResult.duration || 0;\n  const minutes = Math.floor(duration / 60000);\n  const seconds = Math.floor((duration % 60000) / 1000);\n  const interval = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;\n\n  return {\n    songmid: songResult.id,\n    name: songResult.name,\n    singer: artistName,\n    album: albumName,\n    albumId,\n    source: 'wy', // 默认使用网易云作为源，因为我们的数据来自网易云\n    interval,\n    img: songResult.picUrl || songResult.al?.picUrl || ''\n  };\n};\n\n/**\n * 获取最佳匹配的落雪音源\n * 因为我们的数据来自网易云，优先尝试 wy 音源\n */\nconst getBestMatchingSource = (\n  availableSources: LxSourceKey[],\n  _songSource?: string\n): LxSourceKey | null => {\n  // 优先级顺序：网易云 > 酷我 > 咪咕 > 酷狗 > QQ音乐\n  const priority: LxSourceKey[] = ['wy', 'kw', 'mg', 'kg', 'tx'];\n\n  for (const source of priority) {\n    if (availableSources.includes(source)) {\n      return source;\n    }\n  }\n\n  return availableSources[0] || null;\n};\n\n/**\n * 落雪音乐解析策略\n */\nexport class LxMusicStrategy {\n  name = 'lxMusic';\n  priority = 0; // 最高优先级\n\n  /**\n   * 检查是否可以处理\n   */\n  canHandle(sources: string[], settingsStore?: any): boolean {\n    // 检查是否启用了落雪音源\n    if (!sources.includes('lxMusic')) {\n      return false;\n    }\n\n    // 检查是否有激活的音源\n    const activeLxApiId = settingsStore?.setData?.activeLxMusicApiId;\n    if (!activeLxApiId) {\n      return false;\n    }\n\n    // 检查音源列表中是否存在该 ID\n    const lxMusicScripts = settingsStore?.setData?.lxMusicScripts || [];\n    const activeScript = lxMusicScripts.find((script: any) => script.id === activeLxApiId);\n\n    return Boolean(activeScript && activeScript.script);\n  }\n\n  /**\n   * 解析音乐 URL\n   */\n  async parse(\n    id: number,\n    data: SongResult,\n    quality?: string,\n    _sources?: string[]\n  ): Promise<MusicParseResult | null> {\n    // 检查失败缓存\n    if (CacheManager.isInFailedCache(id, this.name)) {\n      return null;\n    }\n\n    try {\n      const settingsStore = useSettingsStore();\n\n      // 获取激活的音源 ID\n      const activeLxApiId = settingsStore.setData?.activeLxMusicApiId;\n      if (!activeLxApiId) {\n        console.log('[LxMusicStrategy] 未选择激活的落雪音源');\n        return null;\n      }\n\n      // 从音源列表中获取激活的脚本\n      const lxMusicScripts = settingsStore.setData?.lxMusicScripts || [];\n      const activeScript = lxMusicScripts.find((script: any) => script.id === activeLxApiId);\n\n      if (!activeScript || !activeScript.script) {\n        console.log('[LxMusicStrategy] 未找到激活的落雪音源脚本');\n        return null;\n      }\n\n      console.log(`[LxMusicStrategy] 使用激活的音源: ${activeScript.name} (ID: ${activeScript.id})`);\n\n      // 获取或初始化执行器\n      let runner = getLxMusicRunner();\n      if (!runner || !runner.isInitialized()) {\n        console.log('[LxMusicStrategy] 初始化落雪音源执行器...');\n        runner = await initLxMusicRunner(activeScript.script);\n      }\n\n      // 获取可用音源\n      const sources = runner.getSources();\n      const availableSourceKeys = Object.keys(sources) as LxSourceKey[];\n\n      if (availableSourceKeys.length === 0) {\n        console.log('[LxMusicStrategy] 没有可用的落雪音源');\n        CacheManager.addFailedCache(id, this.name);\n        return null;\n      }\n\n      // 选择最佳音源\n      const bestSource = getBestMatchingSource(availableSourceKeys);\n      if (!bestSource) {\n        console.log('[LxMusicStrategy] 无法找到匹配的音源');\n        CacheManager.addFailedCache(id, this.name);\n        return null;\n      }\n\n      console.log(`[LxMusicStrategy] 使用音源: ${LX_SOURCE_NAMES[bestSource]} (${bestSource})`);\n\n      // 转换歌曲信息\n      const lxMusicInfo = convertToLxMusicInfo(data);\n\n      // 转换音质\n      const lxQuality: LxQuality = QUALITY_TO_LX[quality || 'higher'] || '320k';\n\n      // 获取音乐 URL\n      const rawUrl = await runner.getMusicUrl(bestSource, lxMusicInfo, lxQuality);\n\n      if (!rawUrl) {\n        console.log('[LxMusicStrategy] 获取 URL 失败');\n        CacheManager.addFailedCache(id, this.name);\n        return null;\n      }\n\n      console.log('[LxMusicStrategy] 脚本返回 URL:', rawUrl.substring(0, 80) + '...');\n\n      // 解析可能是 API 端点的 URL\n      const resolvedUrl = await resolveAudioUrl(rawUrl);\n\n      if (!resolvedUrl) {\n        console.log('[LxMusicStrategy] URL 解析失败');\n        CacheManager.addFailedCache(id, this.name);\n        return null;\n      }\n\n      console.log('[LxMusicStrategy] 最终音频 URL:', resolvedUrl.substring(0, 80) + '...');\n\n      return {\n        data: {\n          code: 200,\n          message: 'success',\n          data: {\n            url: resolvedUrl,\n            source: `lx-${bestSource}`,\n            quality: lxQuality\n          }\n        }\n      };\n    } catch (error) {\n      console.error('[LxMusicStrategy] 解析失败:', error);\n      CacheManager.addFailedCache(id, this.name);\n      return null;\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/api/music.ts",
    "content": "import { musicDB } from '@/hooks/MusicHook';\nimport { useSettingsStore, useUserStore } from '@/store';\nimport type { ILyric } from '@/types/lyric';\nimport type { SongResult } from '@/types/music';\nimport request from '@/utils/request';\n\nimport { MusicParser, type MusicParseResult } from './musicParser';\n\nconst { addData, getData, deleteData } = musicDB;\n\n// 获取音乐音质详情\nexport const getMusicQualityDetail = (id: number) => {\n  return request.get('/song/music/detail', { params: { id } });\n};\n\n// 根据音乐Id获取音乐播放URl\nexport const getMusicUrl = async (id: number, isDownloaded: boolean = false) => {\n  const userStore = useUserStore();\n  const settingStore = useSettingsStore();\n  // 判断是否登录\n  try {\n    if (userStore.user && isDownloaded && userStore.user.vipType !== 0) {\n      const url = '/song/download/url/v1';\n      const res = await request.get(url, {\n        params: {\n          id,\n          level: settingStore.setData.musicQuality || 'higher',\n          encodeType: settingStore.setData.musicQuality == 'lossless' ? 'aac' : 'flac',\n          // level为lossless时，encodeType=flac时网易云会返回hires音质，encodeType=aac时网易云会返回lossless音质\n          cookie: `${localStorage.getItem('token')} os=pc;`\n        }\n      });\n\n      if (res.data.data.url) {\n        return { data: { data: [{ ...res.data.data }] } };\n      }\n    }\n  } catch (error) {\n    console.error('error', error);\n  }\n\n  return await request.get('/song/url/v1', {\n    params: {\n      id,\n      level: settingStore.setData.musicQuality || 'higher',\n      encodeType: settingStore.setData.musicQuality == 'lossless' ? 'aac' : 'flac'\n    }\n  });\n};\n\n// 获取歌曲详情\nexport const getMusicDetail = (ids: Array<number>) => {\n  return request.get('/song/detail', { params: { ids: ids.join(',') } });\n};\n\n// 根据音乐Id获取音乐歌词\nexport const getMusicLrc = async (id: number) => {\n  const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000; // 10天的毫秒数\n\n  try {\n    // 尝试获取缓存的歌词\n    const cachedLyric = await getData('music_lyric', id);\n    if (cachedLyric?.createTime && Date.now() - cachedLyric.createTime < TEN_DAYS_MS) {\n      return { ...cachedLyric };\n    }\n\n    // 获取新的歌词数据\n    const res = await request.get<ILyric>('/lyric/new', { params: { id } });\n\n    // 只有在成功获取新数据后才删除旧缓存并添加新缓存\n    if (res?.data) {\n      if (cachedLyric) {\n        await deleteData('music_lyric', id);\n      }\n      addData('music_lyric', { id, data: res.data, createTime: Date.now() });\n    }\n\n    return res;\n  } catch (error) {\n    console.error('获取歌词失败:', error);\n    throw error; // 向上抛出错误，让调用者处理\n  }\n};\n\n/**\n * 获取解析后的音乐URL\n * @param id 歌曲ID\n * @param data 歌曲数据\n * @returns 解析结果\n */\nexport const getParsingMusicUrl = async (\n  id: number,\n  data: SongResult\n): Promise<MusicParseResult> => {\n  return await MusicParser.parseMusic(id, data);\n};\n\n// 收藏歌曲\nexport const likeSong = (id: number, like: boolean = true) => {\n  return request.get('/like', { params: { id, like } });\n};\n\n// 将每日推荐中的歌曲标记为不感兴趣，并获取一首新歌\nexport const dislikeRecommendedSong = (id: number | string) => {\n  return request.get('/recommend/songs/dislike', {\n    params: { id }\n  });\n};\n// 获取用户喜欢的音乐列表\nexport const getLikedList = (uid: number) => {\n  return request.get('/likelist', {\n    params: { uid, noLogin: true }\n  });\n};\n\n// 创建歌单\nexport const createPlaylist = (params: { name: string; privacy: number }) => {\n  return request.post('/playlist/create', params);\n};\n\n// 添加或删除歌单歌曲\nexport const updatePlaylistTracks = (params: {\n  op: 'add' | 'del';\n  pid: number;\n  tracks: string;\n}) => {\n  return request.post('/playlist/tracks', params);\n};\n\n/**\n * 根据类型获取列表数据\n * @param type 列表类型 album/playlist\n * @param id 列表ID\n */\nexport function getMusicListByType(type: string, id: string) {\n  if (type === 'album') {\n    return getAlbumDetail(id);\n  } else if (type === 'playlist') {\n    return getPlaylistDetail(id);\n  }\n  return Promise.reject(new Error('未知列表类型'));\n}\n\n/**\n * 获取专辑详情\n * @param id 专辑ID\n */\nexport function getAlbumDetail(id: string) {\n  return request({\n    url: '/album',\n    method: 'get',\n    params: {\n      id\n    }\n  });\n}\n\n/**\n * 获取歌单详情\n * @param id 歌单ID\n */\nexport function getPlaylistDetail(id: string) {\n  return request({\n    url: '/playlist/detail',\n    method: 'get',\n    params: {\n      id\n    }\n  });\n}\n\nexport function subscribePlaylist(params: { t: number; id: number }) {\n  return request({\n    url: '/playlist/subscribe',\n    method: 'post',\n    params\n  });\n}\n\n/**\n * 收藏/取消收藏专辑\n * @param params t: 1 收藏, 2 取消收藏; id: 专辑id\n */\nexport function subscribeAlbum(params: { t: number; id: number }) {\n  return request({\n    url: '/album/sub',\n    method: 'post',\n    params\n  });\n}\n\n/**\n * 获取历史日推可用日期列表\n */\nexport function getHistoryRecommendDates() {\n  return request({\n    url: '/history/recommend/songs',\n    method: 'get'\n  });\n}\n\n/**\n * 获取历史日推详情数据\n * @param date 日期，格式：YYYY-MM-DD\n */\nexport function getHistoryRecommendSongs(date: string) {\n  return request({\n    url: '/history/recommend/songs/detail',\n    method: 'get',\n    params: { date }\n  });\n}\n\n/**\n * 心动模式/智能播放\n * @param params id: 歌曲id, pid: 歌单id, sid: 要开始播放的歌曲id(可选)\n */\nexport function getIntelligenceList(params: { id: number; pid: number; sid?: number }) {\n  return request({\n    url: '/playmode/intelligence/list',\n    method: 'get',\n    params\n  });\n}\n"
  },
  {
    "path": "src/renderer/api/musicParser.ts",
    "content": "import { cloneDeep } from 'lodash';\n\nimport { musicDB } from '@/hooks/MusicHook';\nimport { SongSourceConfigManager } from '@/services/SongSourceConfigManager';\nimport { useSettingsStore } from '@/store';\nimport type { SongResult } from '@/types/music';\nimport { isElectron } from '@/utils';\nimport requestMusic from '@/utils/request_music';\n\nimport { searchAndGetBilibiliAudioUrl } from './bilibili';\nimport type { ParsedMusicResult } from './gdmusic';\nimport { parseFromGDMusic } from './gdmusic';\nimport { LxMusicStrategy } from './lxMusicStrategy';\nimport { parseFromCustomApi } from './parseFromCustomApi';\n\nconst { saveData, getData, deleteData } = musicDB;\n\n/**\n * 音乐解析结果接口\n */\nexport interface MusicParseResult {\n  data: {\n    code: number;\n    message: string;\n    data?: {\n      url: string;\n      [key: string]: any;\n    };\n  };\n}\n\n/**\n * 缓存配置\n */\nconst CACHE_CONFIG = {\n  // 音乐URL缓存时间：30分钟\n  MUSIC_URL_CACHE_TIME: 30 * 60 * 1000,\n  // 失败缓存时间：1分钟（减少到 1 分钟以便更快恢复）\n  FAILED_CACHE_TIME: 1 * 60 * 1000,\n  // 重试配置\n  MAX_RETRY_COUNT: 2,\n  RETRY_DELAY: 1000\n};\n\n/**\n * 内存失败缓存（替代 IndexedDB，更轻量且应用重启后自动失效）\n */\nconst failedCacheMap = new Map<string, number>();\n\n/**\n * 缓存管理器\n */\nexport class CacheManager {\n  /**\n   * 获取缓存的音乐URL\n   */\n  static async getCachedMusicUrl(\n    id: number,\n    musicSources?: string[]\n  ): Promise<MusicParseResult | null> {\n    try {\n      const cached = await getData('music_url_cache', id);\n      if (\n        cached?.createTime &&\n        Date.now() - cached.createTime < CACHE_CONFIG.MUSIC_URL_CACHE_TIME\n      ) {\n        // 检查缓存的音源配置是否与当前配置一致\n        const cachedSources = cached.musicSources || [];\n        const currentSources = musicSources || [];\n\n        // 如果音源配置不一致，清除缓存\n        if (JSON.stringify(cachedSources.sort()) !== JSON.stringify(currentSources.sort())) {\n          console.log(`音源配置已变更，清除歌曲 ${id} 的缓存`);\n          await deleteData('music_url_cache', id);\n          return null;\n        }\n\n        console.log(`使用缓存的音乐URL: ${id}`);\n        return cached.data;\n      }\n      // 清理过期缓存\n      if (cached) {\n        await deleteData('music_url_cache', id);\n      }\n    } catch (error) {\n      console.warn('获取缓存失败:', error);\n    }\n    return null;\n  }\n\n  /**\n   * 缓存音乐URL\n   */\n  static async setCachedMusicUrl(\n    id: number,\n    result: MusicParseResult,\n    musicSources?: string[]\n  ): Promise<void> {\n    try {\n      // 深度克隆数据，确保可以被 IndexedDB 存储\n      await saveData('music_url_cache', {\n        id,\n        data: cloneDeep(result),\n        musicSources: cloneDeep(musicSources || []),\n        createTime: Date.now()\n      });\n      console.log(`缓存音乐URL成功: ${id}`);\n    } catch (error) {\n      console.error('缓存音乐URL失败:', error);\n    }\n  }\n\n  /**\n   * 检查是否在失败缓存期内（使用内存缓存）\n   */\n  static isInFailedCache(id: number, strategyName: string): boolean {\n    const cacheKey = `${id}_${strategyName}`;\n    const cachedTime = failedCacheMap.get(cacheKey);\n    if (cachedTime && Date.now() - cachedTime < CACHE_CONFIG.FAILED_CACHE_TIME) {\n      console.log(`策略 ${strategyName} 在失败缓存期内，跳过`);\n      return true;\n    }\n    // 清理过期缓存\n    if (cachedTime) {\n      failedCacheMap.delete(cacheKey);\n    }\n    return false;\n  }\n\n  /**\n   * 添加失败缓存（使用内存缓存）\n   */\n  static addFailedCache(id: number, strategyName: string): void {\n    const cacheKey = `${id}_${strategyName}`;\n    failedCacheMap.set(cacheKey, Date.now());\n    console.log(\n      `添加失败缓存成功: ${strategyName} (缓存时间: ${CACHE_CONFIG.FAILED_CACHE_TIME / 1000}秒)`\n    );\n  }\n\n  /**\n   * 清除指定歌曲的失败缓存\n   */\n  static clearFailedCache(id: number): void {\n    const keysToDelete: string[] = [];\n    failedCacheMap.forEach((_, key) => {\n      if (key.startsWith(`${id}_`)) {\n        keysToDelete.push(key);\n      }\n    });\n    keysToDelete.forEach((key) => failedCacheMap.delete(key));\n    if (keysToDelete.length > 0) {\n      console.log(`清除歌曲 ${id} 的失败缓存: ${keysToDelete.length} 项`);\n    }\n  }\n\n  /**\n   * 清除指定歌曲的所有缓存\n   */\n  static async clearMusicCache(id: number): Promise<void> {\n    try {\n      // 清除URL缓存\n      await deleteData('music_url_cache', id);\n      console.log(`清除歌曲 ${id} 的URL缓存`);\n\n      // 清除失败缓存 - 需要遍历所有策略\n      const strategies = ['custom', 'bilibili', 'gdmusic', 'unblockMusic'];\n      for (const strategy of strategies) {\n        const cacheKey = `${id}_${strategy}`;\n        try {\n          await deleteData('music_failed_cache', cacheKey);\n        } catch {\n          // 忽略删除不存在缓存的错误\n        }\n      }\n      console.log(`清除歌曲 ${id} 的失败缓存`);\n    } catch (error) {\n      console.error('清除缓存失败:', error);\n    }\n  }\n}\n\n/**\n * 重试工具\n */\nclass RetryHelper {\n  /**\n   * 带重试的异步执行\n   */\n  static async withRetry<T>(\n    fn: () => Promise<T>,\n    maxRetries = CACHE_CONFIG.MAX_RETRY_COUNT,\n    delay = CACHE_CONFIG.RETRY_DELAY\n  ): Promise<T> {\n    let lastError: Error;\n\n    for (let i = 0; i <= maxRetries; i++) {\n      try {\n        return await fn();\n      } catch (error) {\n        lastError = error as Error;\n        if (i < maxRetries) {\n          console.log(`重试第 ${i + 1} 次，延迟 ${delay}ms`);\n          await new Promise((resolve) => setTimeout(resolve, delay));\n          delay *= 2; // 指数退避\n        }\n      }\n    }\n\n    throw lastError!;\n  }\n}\n\n/**\n * 从Bilibili获取音频URL\n * @param data 歌曲数据\n * @returns 解析结果\n */\nconst getBilibiliAudio = async (data: SongResult) => {\n  const songName = data?.name || '';\n  const artistName =\n    Array.isArray(data?.ar) && data.ar.length > 0 && data.ar[0]?.name ? data.ar[0].name : '';\n  const albumName = data?.al && typeof data.al === 'object' && data.al?.name ? data.al.name : '';\n\n  const searchQuery = [songName, artistName, albumName].filter(Boolean).join(' ').trim();\n  console.log('开始搜索bilibili音频:', searchQuery);\n\n  const url = await searchAndGetBilibiliAudioUrl(searchQuery);\n  return {\n    data: {\n      code: 200,\n      message: 'success',\n      data: { url }\n    }\n  };\n};\n\n/**\n * 从GD音乐台获取音频URL\n * @param id 歌曲ID\n * @param data 歌曲数据\n * @returns 解析结果，失败时返回null\n */\nconst getGDMusicAudio = async (id: number, data: SongResult): Promise<ParsedMusicResult | null> => {\n  try {\n    const gdResult = await parseFromGDMusic(id, data, '999');\n    if (gdResult) {\n      return gdResult;\n    }\n  } catch (error) {\n    console.error('GD音乐台解析失败:', error);\n  }\n  return null;\n};\n\n/**\n * 使用unblockMusic解析音频URL\n * @param id 歌曲ID\n * @param data 歌曲数据\n * @param sources 音源列表\n * @returns 解析结果\n */\nconst getUnblockMusicAudio = (id: number, data: SongResult, sources: any[]) => {\n  const filteredSources = sources.filter((source) => source !== 'gdmusic');\n  console.log(`使用unblockMusic解析，音源:`, filteredSources);\n  return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources));\n};\n\n/**\n * 统一的解析结果适配器\n */\nconst adaptParseResult = (result: any): MusicParseResult | null => {\n  if (!result) return null;\n\n  // 如果已经是标准格式\n  if (result.data?.code !== undefined && result.data?.message !== undefined) {\n    return result;\n  }\n\n  // 适配GD音乐台的返回格式\n  if (result.data?.data?.url) {\n    return {\n      data: {\n        code: 200,\n        message: 'success',\n        data: {\n          url: result.data.data.url,\n          ...result.data.data\n        }\n      }\n    };\n  }\n\n  // 适配其他格式\n  if (result.url) {\n    return {\n      data: {\n        code: 200,\n        message: 'success',\n        data: {\n          url: result.url,\n          ...result\n        }\n      }\n    };\n  }\n\n  return null;\n};\n\n/**\n * 音源解析策略接口\n */\ninterface MusicSourceStrategy {\n  name: string;\n  priority: number;\n  canHandle: (sources: string[], settingsStore?: any) => boolean;\n  parse: (\n    id: number,\n    data: SongResult,\n    quality?: string,\n    sources?: string[]\n  ) => Promise<MusicParseResult | null>;\n}\n\n/**\n * 自定义API解析策略\n */\nclass CustomApiStrategy implements MusicSourceStrategy {\n  name = 'custom';\n  priority = 1;\n\n  canHandle(sources: string[], settingsStore?: any): boolean {\n    return sources.includes('custom') && Boolean(settingsStore?.setData?.customApiPlugin);\n  }\n\n  async parse(id: number, data: SongResult, quality = 'higher'): Promise<MusicParseResult | null> {\n    // 检查失败缓存\n    if (CacheManager.isInFailedCache(id, this.name)) {\n      return null;\n    }\n\n    try {\n      console.log('尝试使用自定义API解析...');\n      const result = await RetryHelper.withRetry(async () => {\n        return await parseFromCustomApi(id, data, quality);\n      });\n\n      const adaptedResult = adaptParseResult(result);\n      if (adaptedResult?.data?.data?.url) {\n        console.log('自定义API解析成功');\n        return adaptedResult;\n      }\n\n      // 解析失败，添加失败缓存\n      CacheManager.addFailedCache(id, this.name);\n      return null;\n    } catch (error) {\n      console.error('自定义API解析失败:', error);\n      CacheManager.addFailedCache(id, this.name);\n      return null;\n    }\n  }\n}\n\n/**\n * Bilibili解析策略\n */\nclass BilibiliStrategy implements MusicSourceStrategy {\n  name = 'bilibili';\n  priority = 2;\n\n  canHandle(sources: string[]): boolean {\n    return sources.includes('bilibili');\n  }\n\n  async parse(id: number, data: SongResult): Promise<MusicParseResult | null> {\n    // 检查失败缓存\n    if (CacheManager.isInFailedCache(id, this.name)) {\n      return null;\n    }\n\n    try {\n      console.log('尝试使用Bilibili解析...');\n      const result = await RetryHelper.withRetry(async () => {\n        return await getBilibiliAudio(data);\n      });\n\n      const adaptedResult = adaptParseResult(result);\n      if (adaptedResult?.data?.data?.url) {\n        console.log('Bilibili解析成功');\n        return adaptedResult;\n      }\n\n      // 解析失败，添加失败缓存\n      CacheManager.addFailedCache(id, this.name);\n      return null;\n    } catch (error) {\n      console.error('Bilibili解析失败:', error);\n      CacheManager.addFailedCache(id, this.name);\n      return null;\n    }\n  }\n}\n\n/**\n * GD音乐台解析策略\n */\nclass GDMusicStrategy implements MusicSourceStrategy {\n  name = 'gdmusic';\n  priority = 3;\n\n  canHandle(sources: string[]): boolean {\n    return sources.includes('gdmusic');\n  }\n\n  async parse(id: number, data: SongResult): Promise<MusicParseResult | null> {\n    // 检查失败缓存\n    if (CacheManager.isInFailedCache(id, this.name)) {\n      return null;\n    }\n\n    try {\n      console.log('尝试使用GD音乐台解析...');\n      const result = await RetryHelper.withRetry(async () => {\n        return await getGDMusicAudio(id, data);\n      });\n\n      const adaptedResult = adaptParseResult(result);\n      if (adaptedResult?.data?.data?.url) {\n        console.log('GD音乐台解析成功');\n        return adaptedResult;\n      }\n\n      // 解析失败，添加失败缓存\n      CacheManager.addFailedCache(id, this.name);\n      return null;\n    } catch (error) {\n      console.error('GD音乐台解析失败:', error);\n      CacheManager.addFailedCache(id, this.name);\n      return null;\n    }\n  }\n}\n\n/**\n * UnblockMusic解析策略\n */\nclass UnblockMusicStrategy implements MusicSourceStrategy {\n  name = 'unblockMusic';\n  priority = 4;\n\n  canHandle(sources: string[]): boolean {\n    const unblockSources = sources.filter(\n      (source) => !['custom', 'bilibili', 'gdmusic'].includes(source)\n    );\n    return unblockSources.length > 0;\n  }\n\n  async parse(\n    id: number,\n    data: SongResult,\n    _quality?: string,\n    sources?: string[]\n  ): Promise<MusicParseResult | null> {\n    // 检查失败缓存\n    if (CacheManager.isInFailedCache(id, this.name)) {\n      return null;\n    }\n\n    try {\n      const unblockSources = (sources || []).filter(\n        (source) => !['custom', 'bilibili', 'gdmusic'].includes(source)\n      );\n      console.log('尝试使用UnblockMusic解析:', unblockSources);\n\n      const result = await RetryHelper.withRetry(async () => {\n        return await getUnblockMusicAudio(id, data, unblockSources);\n      });\n\n      const adaptedResult = adaptParseResult(result);\n      if (adaptedResult?.data?.data?.url) {\n        console.log('UnblockMusic解析成功');\n        return adaptedResult;\n      }\n\n      // 解析失败，添加失败缓存\n      CacheManager.addFailedCache(id, this.name);\n      return null;\n    } catch (error) {\n      console.error('UnblockMusic解析失败:', error);\n      CacheManager.addFailedCache(id, this.name);\n      return null;\n    }\n  }\n}\n\n/**\n * 音源策略工厂\n */\nclass MusicSourceStrategyFactory {\n  private static strategies: MusicSourceStrategy[] = [\n    new LxMusicStrategy(),\n    new CustomApiStrategy(),\n    new BilibiliStrategy(),\n    new GDMusicStrategy(),\n    new UnblockMusicStrategy()\n  ];\n\n  /**\n   * 获取可用的解析策略\n   * @param sources 音源列表\n   * @param settingsStore 设置存储\n   * @returns 排序后的可用策略列表\n   */\n  static getAvailableStrategies(sources: string[], settingsStore?: any): MusicSourceStrategy[] {\n    return this.strategies\n      .filter((strategy) => strategy.canHandle(sources, settingsStore))\n      .sort((a, b) => a.priority - b.priority);\n  }\n}\n\n/**\n * 获取音源配置\n * @param id 歌曲ID\n * @param settingsStore 设置存储\n * @returns 音源列表和音质设置\n */\nconst getMusicConfig = (id: number, settingsStore?: any) => {\n  let musicSources: string[] = [];\n  let quality = 'higher';\n\n  try {\n    // 尝试获取歌曲自定义音源（使用 SongSourceConfigManager）\n    const songConfig = SongSourceConfigManager.getConfig(id);\n    if (songConfig && songConfig.sources.length > 0) {\n      musicSources = songConfig.sources;\n      console.log(`使用歌曲 ${id} 自定义音源:`, musicSources);\n    }\n\n    // 如果没有自定义音源，使用全局设置\n    if (musicSources.length === 0) {\n      musicSources = settingsStore?.setData?.enabledMusicSources || [];\n      console.log('使用全局音源设置:', musicSources);\n    }\n\n    quality = settingsStore?.setData?.musicQuality || 'higher';\n  } catch (error) {\n    console.error('读取音源配置失败，使用默认配置:', error);\n    musicSources = [];\n    quality = 'higher';\n  }\n\n  return { musicSources, quality };\n};\n\n/**\n * 音乐解析器主类\n */\nexport class MusicParser {\n  /**\n   * 解析音乐URL\n   * @param id 歌曲ID\n   * @param data 歌曲数据\n   * @returns 解析结果\n   */\n  static async parseMusic(id: number, data: SongResult): Promise<MusicParseResult> {\n    const startTime = performance.now();\n\n    try {\n      // 非Electron环境直接使用API请求\n      if (!isElectron) {\n        console.log('非Electron环境，使用API请求');\n        return await requestMusic.get<any>('/music', { params: { id } });\n      }\n\n      // 获取设置存储\n      let settingsStore: any;\n      try {\n        settingsStore = useSettingsStore();\n      } catch (error) {\n        console.error('无法获取设置存储，使用后备方案:', error);\n        return await requestMusic.get<any>('/music', { params: { id } });\n      }\n\n      // 获取音源配置\n      const { musicSources, quality } = getMusicConfig(id, settingsStore);\n\n      // 检查缓存（传入音源配置用于验证缓存有效性）\n      console.log(`检查歌曲 ${id} 的缓存...`);\n      const cachedResult = await CacheManager.getCachedMusicUrl(id, musicSources);\n      if (cachedResult) {\n        const endTime = performance.now();\n        console.log(`✅ 命中缓存，歌曲 ${id}，耗时: ${(endTime - startTime).toFixed(2)}ms`);\n        return cachedResult;\n      }\n      console.log(`❌ 未命中缓存，歌曲 ${id}，开始解析...`);\n\n      // 检查音乐解析功能是否启用\n      if (!settingsStore?.setData?.enableMusicUnblock) {\n        console.log('音乐解析功能已禁用');\n        return {\n          data: {\n            code: 404,\n            message: '音乐解析功能已禁用',\n            data: undefined\n          }\n        };\n      }\n\n      if (musicSources.length === 0) {\n        console.warn('没有配置可用的音源，使用后备方案');\n        return await requestMusic.get<any>('/music', { params: { id } });\n      }\n\n      // 获取可用的解析策略\n      const availableStrategies = MusicSourceStrategyFactory.getAvailableStrategies(\n        musicSources,\n        settingsStore\n      );\n\n      if (availableStrategies.length === 0) {\n        console.warn('没有可用的解析策略，使用后备方案');\n        return await requestMusic.get<any>('/music', { params: { id } });\n      }\n\n      console.log(\n        `开始解析歌曲 ${id}，可用策略:`,\n        availableStrategies.map((s) => s.name)\n      );\n\n      // 按优先级依次尝试解析策略\n      for (const strategy of availableStrategies) {\n        try {\n          const result = await strategy.parse(id, data, quality, musicSources);\n          if (result?.data?.data?.url) {\n            const endTime = performance.now();\n            console.log(\n              `解析成功，使用策略: ${strategy.name}，耗时: ${(endTime - startTime).toFixed(2)}ms`\n            );\n\n            // 缓存成功结果（包含音源配置）\n            await CacheManager.setCachedMusicUrl(id, result, musicSources);\n\n            return result;\n          }\n          console.log(`策略 ${strategy.name} 解析失败，继续尝试下一个策略`);\n        } catch (error) {\n          console.error(`策略 ${strategy.name} 解析异常:`, error);\n          // 继续尝试下一个策略\n        }\n      }\n\n      console.warn('所有解析策略都失败了，使用后备方案');\n    } catch (error) {\n      console.error('MusicParser.parseMusic 执行异常，使用后备方案:', error);\n    }\n\n    // 后备方案：使用API请求\n    try {\n      console.log('使用后备方案：API请求');\n      const result = await requestMusic.get<any>('/music', { params: { id } });\n\n      // 如果后备方案成功，也进行缓存\n      if (result?.data?.data?.url) {\n        console.log('后备方案成功，缓存结果');\n        await CacheManager.setCachedMusicUrl(id, result, []);\n      }\n\n      return result;\n    } catch (apiError) {\n      console.error('API请求也失败了:', apiError);\n      const endTime = performance.now();\n      console.log(`总耗时: ${(endTime - startTime).toFixed(2)}ms`);\n      return {\n        data: {\n          code: 500,\n          message: '所有解析方式都失败了',\n          data: undefined\n        }\n      };\n    }\n  }\n}\n"
  },
  {
    "path": "src/renderer/api/mv.ts",
    "content": "import { IData } from '@/types';\nimport { IMvUrlData } from '@/types/mv';\nimport request from '@/utils/request';\n\ninterface MvParams {\n  limit?: number;\n  offset?: number;\n  area?: string;\n}\n\n// 获取 mv 排行\nexport const getTopMv = (params: MvParams) => {\n  return request({\n    url: '/mv/all',\n    method: 'get',\n    params\n  });\n};\n\n// 获取所有mv\nexport const getAllMv = (params: MvParams) => {\n  return request({\n    url: '/mv/all',\n    method: 'get',\n    params\n  });\n};\n\n// 获取 mv 数据\nexport const getMvDetail = (mvid: string) => {\n  return request.get('/mv/detail', {\n    params: {\n      mvid\n    }\n  });\n};\n\n// 获取 mv 地址\nexport const getMvUrl = (id: Number) => {\n  return request.get<IData<IMvUrlData>>('/mv/url', {\n    params: {\n      id\n    }\n  });\n};\n"
  },
  {
    "path": "src/renderer/api/parseFromCustomApi.ts",
    "content": "import axios from 'axios';\nimport { get } from 'lodash';\n\nimport { useSettingsStore } from '@/store';\n\nimport type { ParsedMusicResult } from './gdmusic';\n\n/**\n * 定义自定义API JSON插件的结构\n */\ninterface CustomApiPlugin {\n  name: string;\n  apiUrl: string;\n  method?: 'GET' | 'POST';\n  params: Record<string, string>;\n  qualityMapping?: Record<string, string>;\n  responseUrlPath: string;\n}\n\n/**\n * 从用户导入的自定义API JSON配置中解析音乐URL\n */\nexport const parseFromCustomApi = async (\n  id: number,\n  _songData: any,\n  quality: string = 'higher',\n  timeout: number = 10000\n): Promise<ParsedMusicResult | null> => {\n  const settingsStore = useSettingsStore();\n  const pluginString = settingsStore.setData.customApiPlugin;\n\n  if (!pluginString) {\n    return null;\n  }\n\n  let plugin: CustomApiPlugin;\n  try {\n    plugin = JSON.parse(pluginString);\n    if (!plugin.apiUrl || !plugin.params || !plugin.responseUrlPath) {\n      console.error('自定义API：JSON配置文件格式不正确。');\n      return null;\n    }\n  } catch (error) {\n    console.error('自定义API：解析JSON配置文件失败。', error);\n    return null;\n  }\n\n  console.log(`自定义API：正在使用插件 [${plugin.name}] 进行解析...`);\n\n  try {\n    // 1. 准备请求参数，替换占位符\n    const finalParams: Record<string, string> = {};\n    for (const [key, value] of Object.entries(plugin.params)) {\n      if (value === '{songId}') {\n        finalParams[key] = String(id);\n      } else if (value === '{quality}') {\n        // 使用 qualityMapping (如果存在) 进行音质翻译，否则直接使用原quality\n        finalParams[key] = plugin.qualityMapping?.[quality] ?? quality;\n      } else {\n        // 固定值参数\n        finalParams[key] = value;\n      }\n    }\n\n    // 2. 判断请求方法，默认为GET\n    const method = plugin.method?.toUpperCase() === 'POST' ? 'POST' : 'GET';\n    let response;\n\n    // 3. 根据方法发送不同的请求\n    if (method === 'POST') {\n      console.log('自定义API：发送 POST 请求到:', plugin.apiUrl, '参数:', finalParams);\n      response = await axios.post(plugin.apiUrl, finalParams, { timeout });\n    } else {\n      // 默认为 GET\n      const finalUrl = `${plugin.apiUrl}?${new URLSearchParams(finalParams).toString()}`;\n      console.log('自定义API：发送 GET 请求到:', finalUrl);\n      response = await axios.get(finalUrl, { timeout });\n    }\n\n    // 4. 使用 lodash.get 安全地从响应数据中提取URL\n    const musicUrl = get(response.data, plugin.responseUrlPath);\n\n    if (musicUrl && typeof musicUrl === 'string') {\n      console.log('自定义API：成功获取URL！');\n      // 5. 组装成应用所需的标准格式并返回\n      return {\n        data: {\n          data: {\n            url: musicUrl,\n            br: parseInt(quality) * 1000,\n            size: 0,\n            md5: '',\n            platform: plugin.name.toLowerCase().replace(/\\s/g, ''),\n            gain: 0\n          },\n          params: { id, type: 'song' }\n        }\n      };\n    } else {\n      console.error('自定义API：根据路径未能从响应中找到URL:', plugin.responseUrlPath);\n      return null;\n    }\n  } catch (error) {\n    console.error(`自定义API [${plugin.name}] 执行失败:`, error);\n    return null;\n  }\n};\n"
  },
  {
    "path": "src/renderer/api/playlist.ts",
    "content": "import request from '@/utils/request';\n\n/**\n * 歌单导入 - 元数据/文字/链接导入\n * @param params 导入参数\n */\nexport function importPlaylist(params: {\n  local?: string;\n  text?: string;\n  link?: string;\n  importStarPlaylist?: boolean;\n  playlistName?: string;\n}) {\n  return request.post('/playlist/import/name/task/create', params);\n}\n\n/**\n * 歌单导入 - 任务状态\n * @param id 任务ID\n */\nexport function getImportTaskStatus(id: string | number) {\n  return request({\n    url: '/playlist/import/task/status',\n    method: 'get',\n    params: { id }\n  });\n}\n"
  },
  {
    "path": "src/renderer/api/search.ts",
    "content": "import { isElectron } from '@/utils';\nimport request from '@/utils/request';\n\ninterface IParams {\n  keywords: string;\n  type: number;\n  limit?: number;\n  offset?: number;\n}\n// 搜索内容\nexport const getSearch = (params: IParams) => {\n  return request.get<any>('/cloudsearch', {\n    params\n  });\n};\n\n/**\n * 搜索建议接口返回的数据结构\n */\ninterface Suggestion {\n  keyword: string;\n}\n\ninterface KugouSuggestionResponse {\n  data: Suggestion[];\n}\n\n// 网易云搜索建议返回的数据结构（部分字段）\ninterface NeteaseSuggestResult {\n  result?: {\n    songs?: Array<{ name: string }>;\n    artists?: Array<{ name: string }>;\n    albums?: Array<{ name: string }>;\n  };\n  code?: number;\n}\n\n/**\n * 从酷狗获取搜索建议\n * @param keyword 搜索关键词\n */\nexport const getSearchSuggestions = async (keyword: string) => {\n  console.log('[API] getSearchSuggestions: 开始执行');\n\n  if (!keyword || !keyword.trim()) {\n    return Promise.resolve([]);\n  }\n\n  console.log(`[API] getSearchSuggestions: 准备请求，关键词: \"${keyword}\"`);\n\n  try {\n    let responseData: KugouSuggestionResponse;\n    if (isElectron) {\n      console.log('[API] Running in Electron, using IPC proxy.');\n      responseData = await window.api.getSearchSuggestions(keyword);\n    } else {\n      // 非 Electron 环境下，使用网易云接口\n      const res = await request.get<NeteaseSuggestResult>('/search/suggest', {\n        params: { keywords: keyword }\n      });\n\n      const result = res?.data?.result || {};\n      const names: string[] = [];\n      if (Array.isArray(result.songs)) names.push(...result.songs.map((s) => s.name));\n      if (Array.isArray(result.artists)) names.push(...result.artists.map((a) => a.name));\n      if (Array.isArray(result.albums)) names.push(...result.albums.map((al) => al.name));\n\n      // 去重并截取前10个\n      const unique = Array.from(new Set(names)).slice(0, 10);\n      console.log('[API] getSearchSuggestions: 网易云建议解析成功:', unique);\n      return unique;\n    }\n\n    if (responseData && Array.isArray(responseData.data)) {\n      const suggestions = responseData.data.map((item) => item.keyword).slice(0, 10);\n      console.log('[API] getSearchSuggestions: 成功解析建议:', suggestions);\n      return suggestions;\n    }\n\n    console.warn('[API] getSearchSuggestions: 响应数据格式不正确，返回空数组。');\n    return [];\n  } catch (error) {\n    console.error('[API] getSearchSuggestions: 请求失败，错误信息:', error);\n    return [];\n  }\n};\n"
  },
  {
    "path": "src/renderer/api/user.ts",
    "content": "import type { IUserDetail, IUserFollow } from '@/types/user';\nimport request from '@/utils/request';\n\n// /user/detail\nexport function getUserDetail(uid: number) {\n  return request.get('/user/detail', { params: { uid } });\n}\n\n// /user/playlist\nexport function getUserPlaylist(uid: number, limit: number = 30, offset: number = 0) {\n  return request.get('/user/playlist', { params: { uid, limit, offset } });\n}\n\n// 播放历史\n// /user/record?uid=32953014&type=1\nexport function getUserRecord(uid: number, type: number = 0) {\n  return request.get('/user/record', {\n    params: { uid, type },\n    noRetry: true\n  } as any);\n}\n\n// 最近播放-歌曲\n// /record/recent/song\nexport function getRecentSongs(limit: number = 100) {\n  return request.get('/record/recent/song', {\n    params: { limit },\n    noRetry: true\n  } as any);\n}\n\n// 最近播放-歌单\n// /record/recent/playlist\nexport function getRecentPlaylists(limit: number = 100) {\n  return request.get('/record/recent/playlist', {\n    params: { limit },\n    noRetry: true\n  } as any);\n}\n\n// 最近播放-专辑\n// /record/recent/album\nexport function getRecentAlbums(limit: number = 100) {\n  return request.get('/record/recent/album', {\n    params: { limit },\n    noRetry: true\n  } as any);\n}\n\n// 获取用户关注列表\n// /user/follows?uid=32953014\nexport function getUserFollows(uid: number, limit: number = 30, offset: number = 0) {\n  return request.get('/user/follows', { params: { uid, limit, offset } });\n}\n\n// 获取用户粉丝列表\nexport function getUserFollowers(uid: number, limit: number = 30, offset: number = 0) {\n  return request.post('/user/followeds', { uid, limit, offset });\n}\n\n// 获取用户账号信息\nexport const getUserAccount = () => {\n  return request<any>({\n    url: '/user/account',\n    method: 'get'\n  });\n};\n\n// 获取用户详情\nexport const getUserDetailInfo = (params: { uid: string | number }) => {\n  return request<IUserDetail>({\n    url: '/user/detail',\n    method: 'get',\n    params\n  });\n};\n\n// 获取用户关注列表\nexport const getUserFollowsInfo = (params: {\n  uid: string | number;\n  limit?: number;\n  offset?: number;\n}) => {\n  return request<{\n    follow: IUserFollow[];\n    more: boolean;\n  }>({\n    url: '/user/follows',\n    method: 'get',\n    params\n  });\n};\n\n// 获取用户歌单\nexport const getUserPlaylists = (params: { uid: string | number }) => {\n  return request({\n    url: '/user/playlist',\n    method: 'get',\n    params\n  });\n};\n\n// 获取已收藏专辑列表\nexport const getUserAlbumSublist = (params?: { limit?: number; offset?: number }) => {\n  return request({\n    url: '/album/sublist',\n    method: 'get',\n    params: {\n      limit: params?.limit || 25,\n      offset: params?.offset || 0\n    }\n  });\n};\n"
  },
  {
    "path": "src/renderer/assets/css/base.css",
    "content": "body {\n  /* background-color: #000; */\n  overflow: hidden;\n}\n\n.n-popover:has(.music-play) {\n  border-radius: 1.5rem !important;\n}\n.n-popover {\n  border-radius: 0.5rem !important;\n  overflow: hidden !important;\n}\n.n-popover:has(.transparent-popover) {\n  background-color: transparent !important;\n  padding: 0 !important;\n}\n\n.settings-slider .n-slider-mark {\n  font-size: 10px !important;\n}\n"
  },
  {
    "path": "src/renderer/assets/icon/iconfont.css",
    "content": "@font-face {\n  font-family: 'iconfont'; /* Project id 2685283 */\n  src:\n    url('iconfont.woff2?t=1703643214551') format('woff2'),\n    url('iconfont.woff?t=1703643214551') format('woff'),\n    url('iconfont.ttf?t=1703643214551') format('truetype');\n}\n\n.iconfont {\n  font-family: 'iconfont' !important;\n  font-size: 16px;\n  font-style: normal;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.icon-list:before {\n  content: '\\e603';\n}\n\n.icon-maxsize:before {\n  content: '\\e692';\n}\n\n.icon-close:before {\n  content: '\\e616';\n}\n\n.icon-minisize:before {\n  content: '\\e602';\n}\n\n.icon-shuaxin:before {\n  content: '\\e627';\n}\n\n.icon-icon_error:before {\n  content: '\\e615';\n}\n\n.icon-a-3User:before {\n  content: '\\e601';\n}\n\n.icon-Chat:before {\n  content: '\\e605';\n}\n\n.icon-Category:before {\n  content: '\\e606';\n}\n\n.icon-Document:before {\n  content: '\\e607';\n}\n\n.icon-Heart:before {\n  content: '\\e608';\n}\n\n.icon-Hide:before {\n  content: '\\e609';\n}\n\n.icon-Home:before {\n  content: '\\e60a';\n}\n\n.icon-a-Image2:before {\n  content: '\\e60b';\n}\n\n.icon-Profile:before {\n  content: '\\e60c';\n}\n\n.icon-Search:before {\n  content: '\\e60d';\n}\n\n.icon-Paper:before {\n  content: '\\e60e';\n}\n\n.icon-Play:before {\n  content: '\\e60f';\n}\n\n.icon-Setting:before {\n  content: '\\e610';\n}\n\n.icon-a-TicketStar:before {\n  content: '\\e611';\n}\n\n.icon-a-VolumeOff:before {\n  content: '\\e612';\n}\n\n.icon-a-VolumeUp:before {\n  content: '\\e613';\n}\n\n.icon-a-VolumeDown:before {\n  content: '\\e614';\n}\n\n.icon-stop:before {\n  content: '\\e600';\n}\n\n.icon-next:before {\n  content: '\\e6a9';\n}\n\n.icon-prev:before {\n  content: '\\e6ac';\n}\n\n.icon-play:before {\n  content: '\\e6aa';\n}\n\n.icon-xiasanjiaoxing:before {\n  content: '\\e642';\n}\n\n.icon-videofill:before {\n  content: '\\e7c7';\n}\n\n.icon-favorfill:before {\n  content: '\\e64b';\n}\n\n.icon-favor:before {\n  content: '\\e64c';\n}\n\n.icon-loading:before {\n  content: '\\e64f';\n}\n\n.icon-search:before {\n  content: '\\e65c';\n}\n\n.icon-likefill:before {\n  content: '\\e668';\n}\n\n.icon-like:before {\n  content: '\\e669';\n}\n\n.icon-notificationfill:before {\n  content: '\\e66a';\n}\n\n.icon-notification:before {\n  content: '\\e66b';\n}\n\n.icon-evaluate:before {\n  content: '\\e672';\n}\n\n.icon-homefill:before {\n  content: '\\e6bb';\n}\n\n.icon-link:before {\n  content: '\\e6bf';\n}\n\n.icon-roundaddfill:before {\n  content: '\\e6d8';\n}\n\n.icon-roundadd:before {\n  content: '\\e6d9';\n}\n\n.icon-add:before {\n  content: '\\e6da';\n}\n\n.icon-appreciatefill:before {\n  content: '\\e6e3';\n}\n\n.icon-forwardfill:before {\n  content: '\\e6ea';\n}\n\n.icon-voicefill:before {\n  content: '\\e6f0';\n}\n\n.icon-wefill:before {\n  content: '\\e6f4';\n}\n\n.icon-keyboard:before {\n  content: '\\e71b';\n}\n\n.icon-picfill:before {\n  content: '\\e72c';\n}\n\n.icon-markfill:before {\n  content: '\\e730';\n}\n\n.icon-presentfill:before {\n  content: '\\e732';\n}\n\n.icon-peoplefill:before {\n  content: '\\e735';\n}\n\n.icon-read:before {\n  content: '\\e742';\n}\n\n.icon-backwardfill:before {\n  content: '\\e74d';\n}\n\n.icon-playfill:before {\n  content: '\\e74f';\n}\n\n.icon-all:before {\n  content: '\\e755';\n}\n\n.icon-hotfill:before {\n  content: '\\e757';\n}\n\n.icon-recordfill:before {\n  content: '\\e7a4';\n}\n\n.icon-full:before {\n  content: '\\e7bc';\n}\n\n.icon-favor_fill_light:before {\n  content: '\\e7ec';\n}\n\n.icon-round_favor_fill:before {\n  content: '\\e80a';\n}\n\n.icon-round_location_fill:before {\n  content: '\\e80b';\n}\n\n.icon-round_like_fill:before {\n  content: '\\e80c';\n}\n\n.icon-round_people_fill:before {\n  content: '\\e80d';\n}\n\n.icon-round_skin_fill:before {\n  content: '\\e80e';\n}\n\n.icon-broadcast_fill:before {\n  content: '\\e81d';\n}\n\n.icon-card_fill:before {\n  content: '\\e81f';\n}\n"
  },
  {
    "path": "src/renderer/assets/icon/iconfont.js",
    "content": "((window._iconfont_svg_string_2685283 =\n  '<svg><symbol id=\"icon-list\" viewBox=\"0 0 1024 1024\"><path d=\"M897.427256 249.003144 126.57172 249.003144c-34.673707 0-62.73174-28.088732-62.73174-62.73174s28.058033-62.73174 62.73174-62.73174l770.855536 0c34.673707 0 62.73174 28.088732 62.73174 62.73174S932.101987 249.003144 897.427256 249.003144z\" fill=\"#231815\" ></path><path d=\"M897.427256 574.73174 126.57172 574.73174c-34.673707 0-62.73174-28.088732-62.73174-62.73174s28.058033-62.73174 62.73174-62.73174l770.855536 0c34.673707 0 62.73174 28.088732 62.73174 62.73174S932.101987 574.73174 897.427256 574.73174z\" fill=\"#231815\" ></path><path d=\"M897.427256 900.460336 126.57172 900.460336c-34.673707 0-62.73174-28.088732-62.73174-62.73174 0-34.643008 28.058033-62.73174 62.73174-62.73174l770.855536 0c34.673707 0 62.73174 28.088732 62.73174 62.73174C960.16002 872.371604 932.101987 900.460336 897.427256 900.460336z\" fill=\"#231815\" ></path></symbol><symbol id=\"icon-maxsize\" viewBox=\"0 0 1024 1024\"><path d=\"M812.2 65H351.6c-78.3 0-142.5 61.1-147.7 138.1-77 5.1-138.1 69.4-138.1 147.7v460.6c0 81.6 66.4 148 148 148h460.6c78.3 0 142.5-61.1 147.7-138.1 77-5.1 138.1-69.4 138.1-147.7V213c0-81.6-66.4-148-148-148z m-45.8 746.3c0 50.7-41.3 92-92 92H213.8c-50.7 0-92-41.3-92-92V350.7c0-50.7 41.3-92 92-92h460.6c50.7 0 92 41.3 92 92v460.6z m137.8-137.7c0 47.3-35.8 86.3-81.8 91.4V350.7c0-81.6-66.4-148-148-148H260.2c5.1-45.9 44.2-81.8 91.4-81.8h460.6c50.7 0 92 41.3 92 92v460.7z\" fill=\"#2c2c2c\" ></path></symbol><symbol id=\"icon-close\" viewBox=\"0 0 1024 1024\"><path d=\"M590.553125 512l168.328125-168.328125c24.69375-24.69375 27.225-62.55 5.625-84.15s-59.484375-19.06875-84.15 5.625l-168.328125 168.328125-168.328125-168.328125c-24.69375-24.69375-62.55-27.225-84.15-5.596875s-19.06875 59.484375 5.625 84.15l168.328125 168.328125-168.328125 168.328125c-24.69375 24.69375-27.225 62.55-5.596875 84.15s59.484375 19.06875 84.15-5.625l168.328125-168.328125 168.328125 168.328125c24.69375 24.69375 62.55 27.225 84.15 5.625s19.06875-59.484375-5.625-84.15l-168.328125-168.328125z\" fill=\"#333333\" ></path></symbol><symbol id=\"icon-minisize\" viewBox=\"0 0 1024 1024\"><path d=\"M919 467.9c0 34.6-28.3 62.9-62.9 62.9H159.2c-34.6 0-62.9-28.3-62.9-62.9 0-34.6 28.3-62.9 62.9-62.9h696.9c34.6 0 62.9 28.4 62.9 62.9z\" fill=\"#2c2c2c\" ></path></symbol><symbol id=\"icon-shuaxin\" viewBox=\"0 0 1024 1024\"><path d=\"M968.604 366.465H762.475c-26.297 0-47.567-21.306-47.567-47.57 0-26.26 21.271-47.567 47.567-47.567h82.748C770.58 165.856 647.837 96.912 508.776 96.912c-227.685 0-412.26 184.576-412.26 412.257 0 227.685 184.576 412.26 412.26 412.26 227.685 0 412.26-184.577 412.26-412.26 0-26.26 21.27-47.567 47.566-47.567 26.263 0 47.57 21.307 47.57 47.567 0 280.226-227.191 507.397-507.396 507.397-280.21 0-507.398-227.17-507.398-507.397 0-280.224 227.189-507.395 507.398-507.395 170.062 0 320.27 83.846 412.259 212.306v-85.457c0-26.263 21.27-47.568 47.566-47.568 26.263 0 47.57 21.305 47.57 47.568v190.273c0.004 26.263-21.307 47.57-47.567 47.57z\"  ></path></symbol><symbol id=\"icon-icon_error\" viewBox=\"0 0 1024 1024\"><path d=\"M42.666667 512C42.666667 252.8 252.8 42.666667 512 42.666667s469.333333 210.133333 469.333333 469.333333-210.133333 469.333333-469.333333 469.333333S42.666667 771.2 42.666667 512z m469.333333-384a384 384 0 1 0 0 768 384 384 0 0 0 0-768z\"  ></path><path d=\"M662.869333 361.130667a42.666667 42.666667 0 0 1 0 60.373333l-241.365333 241.365333a42.666667 42.666667 0 0 1-60.330667-60.373333l241.322667-241.365333a42.666667 42.666667 0 0 1 60.373333 0z\"  ></path><path d=\"M361.173333 361.130667a42.666667 42.666667 0 0 1 60.330667 0l241.365333 241.365333a42.666667 42.666667 0 0 1-60.373333 60.373333L361.173333 421.504a42.666667 42.666667 0 0 1 0-60.373333z\"  ></path></symbol><symbol id=\"icon-a-3User\" viewBox=\"0 0 1024 1024\"><path d=\"M689.408 351.274667a179.498667 179.498667 0 0 1-179.584 180.650666 179.498667 179.498667 0 0 1-179.626667-180.650666A179.456 179.456 0 0 1 509.866667 170.666667a179.456 179.456 0 0 1 179.626666 180.608zM509.866667 853.333333c-146.389333 0-271.36-23.210667-271.36-116.053333 0-92.885333 124.16-116.906667 271.36-116.906667 146.389333 0 271.402667 23.210667 271.402666 116.053334S657.024 853.333333 509.824 853.333333z m256.341333-498.773333a245.973333 245.973333 0 0 1-41.984 138.24 6.741333 6.741333 0 0 0 4.565333 10.453333 145.066667 145.066667 0 0 0 20.608 1.962667c70.101333 1.877333 133.034667-43.52 150.4-111.829333 25.770667-101.504-49.834667-192.597333-146.133333-192.597334-10.453333 0-20.48 1.109333-30.208 3.114667-1.322667 0.298667-2.730667 0.896-3.498667 2.133333-0.938667 1.450667-0.256 3.413333 0.682667 4.693334a247.765333 247.765333 0 0 1 45.568 143.786666z m116.096 221.994667c47.104 9.258667 78.08 28.16 90.922667 55.637333a82.048 82.048 0 0 1 0 71.253333c-19.626667 42.581333-82.944 56.277333-107.52 59.818667-5.12 0.768-9.173333-3.669333-8.661334-8.789333 12.586667-118.058667-87.381333-174.08-113.237333-186.922667-1.109333-0.597333-1.365333-1.450667-1.28-2.005333 0.128-0.384 0.554667-0.981333 1.408-1.109334 55.978667-1.024 116.138667 6.656 138.368 12.117334zM274.688 505.173333c6.997333-0.170667 13.866667-0.810667 20.608-1.962666a6.741333 6.741333 0 0 0 4.522667-10.453334 245.973333 245.973333 0 0 1-41.984-138.24c0-53.333333 16.64-103.082667 45.568-143.872 0.938667-1.28 1.578667-3.2 0.682666-4.693333-0.725333-1.152-2.176-1.792-3.498666-2.133333a150.186667 150.186667 0 0 0-30.293334-3.072c-96.213333 0-171.818667 91.093333-146.048 192.597333 17.365333 68.266667 80.298667 113.664 150.4 111.829333z m6.784 60.330667c0.128 0.554667-0.128 1.408-1.194667 2.005333-25.898667 12.885333-125.866667 68.864-113.322666 186.88 0.554667 5.162667-3.498667 9.557333-8.576 8.832-24.618667-3.541333-87.893333-17.237333-107.52-59.818666a81.792 81.792 0 0 1 0-71.253334c12.8-27.477333 43.776-46.378667 90.88-55.68 22.272-5.418667 82.346667-13.098667 138.410666-12.074666 0.853333 0.128 1.28 0.725333 1.28 1.109333z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-Chat\" viewBox=\"0 0 1024 1024\"><path d=\"M85.333333 512.64C85.333333 287.872 264.96 85.333333 512.853333 85.333333 755.2 85.333333 938.666667 284.032 938.666667 511.36 938.666667 775.04 723.626667 938.666667 512 938.666667c-69.973333 0-147.626667-18.773333-209.92-55.552-21.76-13.226667-40.106667-23.04-63.573333-15.36l-86.186667 25.6c-21.76 6.826667-41.386667-10.24-34.986667-33.28l28.586667-95.744c4.693333-13.226667 3.84-27.349333-2.986667-38.485334C106.24 658.346667 85.333333 584.405333 85.333333 512.64z m371.2 0c0 30.336 24.32 54.698667 54.613334 55.125333 30.293333 0 54.613333-24.746667 54.613333-54.698666 0-30.336-24.32-54.698667-54.613333-54.698667-29.866667-0.426667-54.613333 24.362667-54.613334 54.272z m196.693334 0.426667c0 29.909333 24.32 54.698667 54.613333 54.698666 30.293333 0 54.613333-24.746667 54.613333-54.698666 0-30.336-24.32-54.698667-54.613333-54.698667-30.293333 0-54.613333 24.362667-54.613333 54.698667z m-338.773334 54.698666c-29.866667 0-54.613333-24.746667-54.613333-54.698666 0-30.336 24.32-54.698667 54.613333-54.698667 30.293333 0 54.613333 24.362667 54.613334 54.698667a55.04 55.04 0 0 1-54.613334 54.698666z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-Category\" viewBox=\"0 0 1024 1024\"><path d=\"M193.706667 85.333333h144.213333c60.16 0 108.373333 49.066667 108.373333 109.269334V340.053333c0 60.586667-48.213333 109.226667-108.373333 109.226667H193.706667C133.973333 449.28 85.333333 400.64 85.333333 340.053333V194.602667C85.333333 134.4 133.973333 85.333333 193.706667 85.333333z m0 489.386667h144.213333c60.16 0 108.373333 48.64 108.373333 109.226667v145.493333c0 60.16-48.213333 109.226667-108.373333 109.226667H193.706667C133.973333 938.666667 85.333333 889.6 85.333333 829.44v-145.493333c0-60.586667 48.64-109.226667 108.373334-109.226667zM830.293333 85.333333h-144.213333c-60.16 0-108.373333 49.066667-108.373333 109.269334V340.053333c0 60.586667 48.213333 109.226667 108.373333 109.226667h144.213333c59.733333 0 108.373333-48.64 108.373334-109.226667V194.602667C938.666667 134.4 890.026667 85.333333 830.293333 85.333333z m-144.213333 489.386667h144.213333c59.733333 0 108.373333 48.64 108.373334 109.226667v145.493333c0 60.16-48.64 109.226667-108.373334 109.226667h-144.213333c-60.16 0-108.373333-49.066667-108.373333-109.226667v-145.493333c0-60.586667 48.213333-109.226667 108.373333-109.226667z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-Document\" viewBox=\"0 0 1024 1024\"><path d=\"M333.226667 85.333333h357.589333C822.613333 85.333333 896 161.28 896 291.413333v440.746667c0 132.266667-73.386667 206.506667-205.184 206.506667H333.226667C203.52 938.666667 128 864.426667 128 732.16V291.413333C128 161.28 203.52 85.333333 333.226667 85.333333z m11.52 198.826667v-0.426667h127.530666c18.389333 0 33.322667 14.933333 33.322667 33.237334 0 18.816-14.933333 33.749333-33.322667 33.749333H344.746667a33.28 33.28 0 0 1 0-66.56z m0 259.413333h334.506666a33.322667 33.322667 0 0 0 0-66.602666H344.746667a33.322667 33.322667 0 0 0 0 66.602666z m0 194.986667h334.506666c17.024-1.706667 29.866667-16.256 29.866667-33.28 0-17.493333-12.842667-32-29.866667-33.706667H344.746667a33.92 33.92 0 0 0-32 51.626667c6.826667 10.666667 19.2 17.066667 32 15.36z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-Heart\" viewBox=\"0 0 1024 1024\"><path d=\"M676.266667 106.666667c26.88 0 53.76 3.84 79.36 12.373333 157.44 51.2 214.186667 224 166.826666 375.04a543.061333 543.061333 0 0 1-128.426666 205.226667 1640.789333 1640.789333 0 0 1-270.08 211.626666l-10.666667 6.4-11.093333-6.826666a1625.344 1625.344 0 0 1-271.786667-211.626667 551.808 551.808 0 0 1-128.426667-204.8c-48.213333-151.04 8.533333-323.84 167.68-375.893333 12.373333-4.266667 25.173333-7.253333 37.973334-8.96h5.12c11.946667-1.706667 23.893333-2.56 35.84-2.56h4.693333c26.88 0.853333 52.906667 5.546667 78.08 14.08h2.56c1.706667 0.853333 2.986667 1.706667 3.84 2.56 9.386667 2.986667 18.346667 6.4 26.88 11.093333l16.213333 7.253333c3.925333 2.133333 8.32 5.333333 12.117334 8.106667 2.389333 1.706667 4.565333 3.285333 6.229333 4.266667l2.133333 1.28c3.626667 2.133333 7.466667 4.352 10.666667 6.826666a267.221333 267.221333 0 0 1 164.266667-55.466666z m113.493333 307.2c17.493333-0.426667 32.426667-14.506667 33.706667-32.426667v-5.12a140.8 140.8 0 0 0-90.026667-134.826667 34.133333 34.133333 0 0 0-43.093333 21.333334c-5.973333 17.92 3.413333 37.546667 21.333333 43.946666 27.306667 10.24 45.653333 37.12 45.653333 66.986667v1.28a36.693333 36.693333 0 0 0 8.106667 26.453333c5.973333 7.253333 14.933333 11.52 24.32 12.373334z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-Hide\" viewBox=\"0 0 1024 1024\"><path d=\"M418.261333 650.794667c26.666667 18.048 59.136 28.970667 93.696 28.970666 91.562667 0 166.101333-75.178667 166.101334-167.594666 0-34.858667-10.837333-67.626667-28.714667-94.506667l-45.397333 45.781333c7.509333 14.250667 11.648 31.061333 11.648 48.725334 0 57.514667-46.634667 104.576-103.68 104.576-17.493333 0-34.133333-4.181333-48.256-11.733334l-45.397334 45.781334zM786.346667 279.466667c60.373333 55.466667 111.573333 130.176 149.888 220.074666a32 32 0 0 1 0 24.789334c-89.130667 209.194667-247.722667 334.378667-424.234667 334.378666h-0.426667c-80.341333 0-157.354667-26.453333-225.237333-74.794666l-80.768 81.493333a30.72 30.72 0 0 1-22.058667 9.258667 30.08 30.08 0 0 1-22.058666-9.258667 31.402667 31.402667 0 0 1-3.754667-39.466667l1.28-1.706666 615.68-621.226667c0.853333-0.853333 1.706667-1.706667 2.133333-2.56 0.810667-0.853333 1.621333-1.664 2.048-2.474667l39.125334-39.509333a31.189333 31.189333 0 0 1 44.16 0 31.146667 31.146667 0 0 1 0 44.544L786.304 279.466667z m-440.746667 232.874666c0 10.922667 1.237333 21.845333 2.901333 31.914667l-154.026666 155.392c-41.642667-48.725333-77.866667-107.52-106.581334-175.146667a32 32 0 0 1 0-24.746666c89.088-209.237333 247.68-333.994667 423.808-333.994667h0.426667c59.52 0 117.333333 14.293333 170.666667 41.173333l-139.093334 140.288a192.64 192.64 0 0 0-31.573333-2.944c-92.032 0-166.570667 75.221333-166.570667 168.021334z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-Home\" viewBox=\"0 0 1024 1024\"><path d=\"M389.76 886.314667v-130.432c0-33.28 27.178667-60.330667 60.714667-60.330667h122.666666c16.085333 0 31.573333 6.4 42.922667 17.664 11.392 11.306667 17.792 26.666667 17.792 42.666667v130.432c-0.085333 13.866667 5.376 27.178667 15.189333 36.992 9.813333 9.813333 23.210667 15.36 37.12 15.36h83.712a147.626667 147.626667 0 0 0 104.234667-42.666667 145.493333 145.493333 0 0 0 43.221333-103.338667V420.992c0-31.36-13.994667-61.056-38.186666-81.152l-284.629334-225.706667a132.138667 132.138667 0 0 0-168.490666 3.072L147.925333 339.84A105.557333 105.557333 0 0 0 106.666667 420.992v371.285333C106.666667 873.130667 172.672 938.666667 254.122667 938.666667h81.749333c29.013333 0 52.522667-23.210667 52.736-51.968l1.152-0.384z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-a-Image2\" viewBox=\"0 0 1024 1024\"><path d=\"M325.845333 144.853333c-109.952 0-180.992 75.392-180.992 191.701334v350.464c0 32.64 6.058667 61.781333 16.426667 87.04l22.954667-27.989334c24.917333-30.293333 61.141333-74.496 61.44-74.752 29.482667-33.706667 84.906667-83.968 157.653333-53.546666 15.957333 6.613333 30.122667 15.701333 43.178667 24.021333l3.754666 2.389333c24.448 16.341333 38.826667 24.021333 53.76 22.741334 6.186667-0.853333 11.989333-2.688 17.493334-6.101334 20.778667-12.8 74.624-89.045333 90.709333-111.872l4.437333-6.272c46.506667-60.586667 118.186667-76.8 177.92-40.96 8.021333 4.778667 65.493333 44.928 84.522667 61.056V336.554667c0-116.309333-71.04-191.701333-181.333333-191.701334H325.802667zM697.728 85.333333C841.813333 85.333333 938.666667 186.112 938.666667 336.554667v350.464c0 3.84-0.384 7.338667-0.768 10.88a129.877333 129.877333 0 0 0-0.853334 12.245333c-0.085333 2.048-0.170667 4.138667-0.341333 6.186667-0.085333 0.853333-0.213333 1.621333-0.426667 2.389333a23.637333 23.637333 0 0 0-0.384 2.346667 290.346667 290.346667 0 0 1-6.613333 38.613333c-0.682667 3.114667-1.536 6.101333-2.346667 9.130667l-0.213333 0.64c-3.413333 12.032-7.424 23.509333-12.245333 34.474666l-2.602667 5.461334a230.272 230.272 0 0 1-19.114667 33.408c-1.237333 1.706667-2.517333 3.328-3.84 4.949333l-2.517333 3.2a216.448 216.448 0 0 1-21.888 24.832c-1.578667 1.536-3.328 2.944-5.034667 4.309333l-3.242666 2.645334a215.466667 215.466667 0 0 1-25.898667 19.669333c-2.005333 1.28-4.138667 2.346667-6.272 3.413333-1.408 0.682667-2.773333 1.365333-4.138667 2.133334a226.816 226.816 0 0 1-29.312 14.165333c-2.474667 0.981333-5.12 1.664-7.808 2.346667a106.154667 106.154667 0 0 0-5.632 1.536l-2.730666 0.853333a218.88 218.88 0 0 1-28.501334 7.466667c-5.76 1.024-11.904 1.408-18.005333 1.792a397.312 397.312 0 0 0-7.978667 0.554666c-2.816 0.213333-5.546667 0.554667-8.362666 0.896a110.677333 110.677333 0 0 1-13.824 1.109334H325.845333c-16.042667 0-31.36-1.621333-46.165333-4.053334l-1.578667-0.256c-57.728-9.941333-105.642667-37.802667-139.221333-79.786666-0.213333 0-0.298667-0.170667-0.426667-0.426667a1.962667 1.962667 0 0 0-0.341333-0.426667C104.405333 811.221333 85.333333 754.090667 85.333333 687.018667V336.554667C85.333333 186.112 182.186667 85.333333 325.845333 85.333333h371.882667zM469.333333 363.306667C469.333333 421.12 420.949333 469.333333 362.88 469.333333a107.52 107.52 0 0 1-104.362667-85.589333A102.784 102.784 0 0 1 256 361.344 105.472 105.472 0 0 1 361.770667 256c29.738667 0 56.746667 12.501333 76.032 32.469333 19.370667 19.285333 31.530667 45.866667 31.530666 74.837334z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-Profile\" viewBox=\"0 0 1024 1024\"><path d=\"M737.877333 311.082667A225.024 225.024 0 0 1 512 536.874667a225.066667 225.066667 0 0 1-225.877333-225.792A225.024 225.024 0 0 1 512 85.333333a224.981333 224.981333 0 0 1 225.877333 225.749334zM512 938.666667c-185.088 0-341.333333-30.08-341.333333-146.133334 0-116.096 157.226667-145.109333 341.333333-145.109333 185.130667 0 341.333333 30.08 341.333333 146.133333C853.333333 909.653333 696.106667 938.666667 512 938.666667z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-Search\" viewBox=\"0 0 1024 1024\"><path d=\"M85.333333 455.253333C85.333333 250.965333 249.173333 85.333333 451.328 85.333333c97.066667 0 190.122667 38.954667 258.773333 108.373334a371.925333 371.925333 0 0 1 107.178667 261.546666c0 204.288-163.84 369.92-365.952 369.92C249.173333 825.173333 85.333333 659.541333 85.333333 455.253333z m725.888 297.984l109.013334 87.978667h1.877333c22.058667 22.314667 22.058667 58.453333 0 80.725333a56.064 56.064 0 0 1-79.829333 0l-90.453334-103.68a46.165333 46.165333 0 0 1 0-65.024 42.069333 42.069333 0 0 1 59.392 0z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-Paper\" viewBox=\"0 0 1024 1024\"><path d=\"M380.842667 699.306667h229.802666c17.322667 0 31.701333-14.506667 31.701334-32s-14.378667-31.573333-31.701334-31.573334H380.842667a31.701333 31.701333 0 0 0-31.701334 31.573334c0 17.493333 14.378667 32 31.701334 32z m142.762666-276.906667H380.842667a32.085333 32.085333 0 0 0-31.701334 32c0 17.493333 14.378667 31.573333 31.701334 31.573333h142.762666c17.322667 0 31.701333-14.08 31.701334-31.573333s-14.378667-32-31.701334-32z m301.482667-37.290667c9.941333-0.128 20.736-0.256 30.549333-0.256 10.581333 0 19.029333 8.533333 19.029334 19.2v343.04c0 105.813333-84.906667 191.573333-189.653334 191.573334H348.714667C238.933333 938.666667 149.333333 848.64 149.333333 737.706667V277.76C149.333333 171.946667 234.666667 85.333333 339.84 85.333333h225.578667c11.008 0 19.456 8.96 19.456 19.626667v137.386667c0 78.08 63.786667 142.08 141.098666 142.506666 18.048 0 33.962667 0.128 47.914667 0.256 10.837333 0.085333 20.48 0.170667 28.970667 0.170667 5.973333 0 13.781333-0.085333 22.229333-0.170667z m11.648-62.293333c-34.730667 0.128-75.648 0-105.088-0.298667-46.72 0-85.205333-38.869333-85.205333-86.058666V123.989333c0-18.389333 22.101333-27.52 34.730666-14.250666l85.546667 89.856 84.48 88.789333a20.352 20.352 0 0 1-14.464 34.432z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-Play\" viewBox=\"0 0 1024 1024\"><path d=\"M85.333333 512.256C85.333333 276.736 276.821333 85.333333 512 85.333333s426.666667 191.402667 426.666667 426.922667C938.666667 747.264 747.178667 938.666667 512 938.666667S85.333333 747.264 85.333333 512.256z m583.168 43.178667c4.522667-4.522667 10.282667-11.52 11.52-13.184 6.613333-8.618667 9.898667-19.328 9.898667-29.994667 0-11.989333-3.712-23.125333-10.709333-32.170667a158.805333 158.805333 0 0 1-3.157334-3.413333c-2.730667-2.944-6.698667-7.253333-10.453333-10.965333-33.749333-36.266667-121.898667-95.530667-168.021333-113.621334-6.997333-2.858667-24.704-9.045333-34.176-9.472-9.045333 0-17.706667 2.048-25.941334 6.186667a53.376 53.376 0 0 0-23.04 25.514667c-2.901333 7.381333-7.424 29.610667-7.424 30.037333-4.565333 24.32-6.997333 63.786667-6.997333 107.434667 0 41.642667 2.432 79.445333 6.144 104.149333 0.128 0.085333 0.469333 1.877333 1.024 4.608 1.706667 8.362667 5.12 25.728 8.874667 32.853333 9.045333 17.28 26.752 27.989333 45.696 27.989334h1.664c12.373333-0.426667 38.314667-11.093333 38.314666-11.52 43.648-18.133333 129.706667-74.496 164.309334-111.957334l2.474666-2.474666z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-Setting\" viewBox=\"0 0 1024 1024\"><path d=\"M870.485333 579.413333c15.274667 8.106667 27.050667 20.906667 35.328 33.706667 16.128 26.453333 14.848 58.88-0.853333 87.466667l-30.549333 51.2a90.88 90.88 0 0 1-77.184 44.373333c-15.232 0-32.256-4.266667-46.208-12.8-11.349333-7.253333-24.405333-9.813333-38.4-9.813333-43.136 0-79.36 35.413333-80.64 77.653333 0 49.066667-40.106667 87.466667-90.24 87.466667h-59.306667c-50.602667 0-90.709333-38.4-90.709333-87.466667-0.853333-42.24-37.077333-77.653333-80.213334-77.653333-14.421333 0-27.477333 2.56-38.4 9.813333-13.952 8.533333-31.36 12.8-46.208 12.8-31.402667 0-61.44-17.066667-77.610666-44.373333l-30.08-51.2c-16.128-27.733333-17.024-61.013333-0.853334-87.466667 6.954667-12.8 20.053333-25.6 34.858667-33.706667 12.202667-5.973333 20.053333-15.786667 27.477333-27.306666 21.76-36.693333 8.704-84.906667-28.330666-106.666667a87.210667 87.210667 0 0 1-32.298667-120.746667L149.333333 274.346667a90.624 90.624 0 0 1 122.965334-32.426667c37.973333 20.48 87.210667 6.826667 109.44-29.44 6.997333-11.946667 10.922667-24.746667 10.026666-37.546667-0.853333-16.64 3.925333-32.426667 12.202667-45.226666A93.653333 93.653333 0 0 1 481.152 85.333333h61.44c32.256 0 61.482667 17.92 77.653333 44.373334 7.808 12.8 13.056 28.586667 11.733334 45.226666-0.853333 12.8 3.072 25.6 10.026666 37.546667 22.229333 36.266667 71.509333 49.92 109.866667 29.44a90.112 90.112 0 0 1 122.538667 32.426667l29.226666 50.346666c25.301333 42.24 11.349333 96.426667-32.256 120.746667-37.077333 21.76-50.176 69.973333-27.904 106.666667 6.954667 11.52 14.805333 21.333333 27.008 27.306666zM388.693333 512.426667c0 66.986667 55.381333 120.32 123.818667 120.32s122.538667-53.333333 122.538667-120.32c0-66.986667-54.101333-120.746667-122.538667-120.746667-68.437333 0-123.818667 53.76-123.818667 120.746667z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-a-TicketStar\" viewBox=\"0 0 1024 1024\"><path d=\"M843.136 512.042667c0 34.730667 28.501333 62.933333 63.530667 62.933333 17.664 0 32 14.208 32 31.701333v114.218667C938.666667 817.450667 859.392 896 761.941333 896H262.101333C164.650667 896 85.333333 817.450667 85.333333 720.896v-114.218667c0-17.493333 14.336-31.701333 32-31.701333 35.072 0 63.573333-28.245333 63.573334-62.933333 0-33.834667-27.349333-59.306667-63.573334-59.306667a32.085333 32.085333 0 0 1-22.613333-9.258667 31.573333 31.573333 0 0 1-9.386667-22.4l0.085334-117.930666C85.418667 206.592 164.693333 128 262.144 128h499.712c97.450667 0 176.768 78.592 176.768 175.146667L938.666667 417.365333a31.616 31.616 0 0 1-9.344 22.442667 32.128 32.128 0 0 1-22.656 9.301333c-35.029333 0-63.530667 28.245333-63.530667 62.933334z m-235.050667 27.605333l50.304-48.512a31.146667 31.146667 0 1 0-17.450666-53.333333l-69.504-10.069334-31.104-62.378666a31.445333 31.445333 0 0 0-28.245334-17.493334H512a31.573333 31.573333 0 0 0-28.288 17.450667l-31.104 62.421333-69.376 10.026667a31.36 31.36 0 0 0-25.6 21.248 30.890667 30.890667 0 0 0 7.978667 32.128l50.304 48.512-11.861334 68.608a31.018667 31.018667 0 0 0 12.586667 30.677333 31.658667 31.658667 0 0 0 33.152 2.304L512 608.853333l62.08 32.298667a31.274667 31.274667 0 0 0 33.28-2.261333 30.848 30.848 0 0 0 12.629333-30.634667l-11.904-68.608z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-a-VolumeOff\" viewBox=\"0 0 1024 1024\"><path d=\"M871.253333 139.52a39.509333 39.509333 0 1 1 55.850667 55.936L195.370667 927.146667a40.405333 40.405333 0 0 1-27.904 11.562666 41.088 41.088 0 0 1-27.776-11.434666 39.68 39.68 0 0 1-0.170667-56.021334l120.618667-120.618666h-1.237334c-60.117333 0-103.893333-42.453333-112-108.373334-9.088-65.92-7.253333-178.858667 0-238.933333 8.533333-62.293333 54.613333-105.173333 112-105.173333h78.08l149.077334-121.941334c18.133333-15.36 50.176-29.866667 75.008-30.250666 45.141333 0 86.698667 31.573333 101.589333 82.176 5.888 21.248 8.192 42.410667 9.984 62.805333l3.584 28.842667c0.597333 4.437333 1.109333 8.704 1.578667 13.226666L871.253333 139.52z m-236.373333 446.336c6.144-5.973333 19.754667-10.24 25.898667-8.661333 16.597333 4.224 19.84 27.989333 19.584 46.72-0.768 54.272-2.56 92.032-5.461334 115.370666l-2.048 19.242667v0.341333c-1.962667 19.370667-3.968 39.381333-9.685333 60.757334-15.061333 50.517333-55.381333 83.285333-101.248 83.285333l-4.522667-0.042667c-25.344 0-52.778667-15.189333-68.394666-28.416l-55.466667-42.922666c-21.12-15.701333-14.890667-40.704-3.072-55.210667 8.874667-10.794667 115.072-108.288 170.88-159.488 18.901333-17.365333 32-29.44 33.578667-30.976z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-a-VolumeUp\" viewBox=\"0 0 1024 1024\"><path d=\"M569.898667 275.2c-2.133333-20.736-4.394667-42.24-9.898667-63.744C545.066667 160.085333 503.509333 128 459.008 128c-24.832-0.085333-56.234667 15.189333-74.069333 30.72l-147.626667 123.605333h-77.226667c-56.874667 0-102.570667 43.818667-111.232 107.093334-7.338667 60.757333-9.130667 175.36 0 242.218666 7.936 66.858667 51.626667 110.037333 111.232 110.037334h77.226667l150.485333 125.44c15.445333 13.44 42.666667 28.842667 67.754667 28.842666l4.48 0.042667c45.354667 0 85.333333-33.28 100.266667-84.48 5.674667-21.717333 7.68-42.026667 9.557333-61.653333l0.042667-0.341334 2.005333-19.584c7.68-63.445333 7.68-372.864 0-435.84l-2.005333-18.858666z m172.8 1.92a38.698667 38.698667 0 0 0-54.570667-10.112 40.832 40.832 0 0 0-9.728 55.808c34.218667 50.432 53.034667 117.589333 53.034667 189.184 0 71.552-18.816 138.752-53.034667 189.184a40.789333 40.789333 0 0 0 9.813333 55.808 38.613333 38.613333 0 0 0 54.485334-10.112c43.178667-63.658667 67.029333-147.072 67.029333-234.88s-23.850667-171.221333-67.029333-234.88zM823.04 137.386667a38.613333 38.613333 0 0 1 54.485333 10.069333C944.469333 246.058667 981.333333 375.552 981.333333 512c0 136.533333-36.864 265.984-103.808 364.544a38.485333 38.485333 0 0 1-54.442666 10.069333 40.832 40.832 0 0 1-9.813334-55.808c57.856-85.290667 89.770667-198.528 89.770667-318.805333 0-120.234667-31.914667-233.472-89.813333-318.762667a40.874667 40.874667 0 0 1 9.813333-55.808z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-a-VolumeDown\" viewBox=\"0 0 1024 1024\"><path d=\"M645.973333 211.456c5.504 21.504 7.722667 43.008 9.898667 63.786667l2.005333 18.858666c7.68 62.976 7.68 372.394667 0 435.84l-2.005333 19.584-0.042667 0.341334c-1.92 19.626667-3.925333 39.936-9.557333 61.610666-15.018667 51.285333-55.04 84.522667-100.437333 84.522667l-4.48-0.042667c-25.088 0-52.352-15.36-67.84-28.8l-150.613334-125.482666H245.546667c-59.733333 0-103.424-43.178667-111.36-110.037334-9.130667-66.816-7.338667-181.461333 0-242.218666 8.661333-63.274667 54.442667-107.093333 111.36-107.093334h77.354666l147.797334-123.648c17.792-15.488 49.237333-30.762667 74.112-30.677333 44.586667 0 86.144 32.085333 101.12 83.456z m128.341334 55.552a38.826667 38.826667 0 0 1 54.528 10.112C872.106667 340.821333 896 424.234667 896 512s-23.893333 171.178667-67.157333 234.88a39.04 39.04 0 0 1-32.170667 17.237333 38.485333 38.485333 0 0 1-22.357333-7.125333 40.746667 40.746667 0 0 1-9.813334-55.808c34.261333-50.474667 53.12-117.674667 53.12-189.184 0-71.552-18.858667-138.666667-53.12-189.184a40.746667 40.746667 0 0 1 9.813334-55.808z\" fill=\"#200E32\" ></path></symbol><symbol id=\"icon-stop\" viewBox=\"0 0 1024 1024\"><path d=\"M688.64 168.96c-40.96 0-74.24 35.84-74.24 74.24v552.96c0 40.96 35.84 74.24 74.24 74.24s74.24-35.84 74.24-74.24V243.2c0-40.96-35.84-74.24-74.24-74.24z m-353.28 0c-40.96 0-74.24 35.84-74.24 74.24v552.96c0 40.96 35.84 74.24 74.24 74.24s74.24-35.84 74.24-74.24V243.2c2.56-40.96-33.28-74.24-74.24-74.24z\" fill=\"#2c2c2c\" ></path></symbol><symbol id=\"icon-next\" viewBox=\"0 0 1024 1024\"><path d=\"M686.08 448L339.2 207.36c-34.56-24.32-83.2 1.28-83.2 43.52v481.28c0 42.24 47.36 67.84 83.2 43.52l346.88-240.64c29.44-21.76 29.44-66.56 0-87.04z\" fill=\"#333333\" ></path><path d=\"M746.24 785.92c-21.76 0-38.4-16.64-38.4-38.4v-512c0-21.76 16.64-38.4 38.4-38.4s38.4 16.64 38.4 38.4v512c0 20.48-16.64 38.4-38.4 38.4z\" fill=\"#333333\" ></path></symbol><symbol id=\"icon-prev\" viewBox=\"0 0 1024 1024\"><path d=\"M381.44 577.28l346.88 240.64c34.56 24.32 83.2-1.28 83.2-43.52V293.12c0-42.24-47.36-67.84-83.2-43.52L381.44 490.24c-30.72 20.48-30.72 65.28 0 87.04z\" fill=\"#333333\" ></path><path d=\"M320 828.16c-21.76 0-38.4-16.64-38.4-38.4v-512c0-21.76 16.64-38.4 38.4-38.4s38.4 16.64 38.4 38.4v512c0 20.48-16.64 38.4-38.4 38.4z\" fill=\"#333333\" ></path></symbol><symbol id=\"icon-play\" viewBox=\"0 0 1024 1024\"><path d=\"M943.36 472.32L433.92 119.04c-49.92-34.56-117.76 1.28-117.76 61.44v706.56c0 60.16 67.84 96 117.76 61.44l509.44-353.28c42.24-29.44 42.24-93.44 0-122.88z\" fill=\"#333333\" ></path></symbol><symbol id=\"icon-xiasanjiaoxing\" viewBox=\"0 0 1024 1024\"><path d=\"M325.456896 862.27968\"  ></path><path d=\"M882.05824 862.27968\"  ></path><path d=\"M236.027904 877.161472\"  ></path><path d=\"M960.132096 877.161472\"  ></path><path d=\"M63.683584 788.737024\"  ></path><path d=\"M958.46912 788.737024\"  ></path><path d=\"M64.77824 858.791936\"  ></path><path d=\"M163.396608 289.168384c-40.577024 0-66.526208 54.183936-35.44064 85.25824L477.217792 723.704832c20.031488 20.031488 49.82272 20.031488 69.853184 0l349.274112-349.278208c30.30528-30.294016 6.677504-85.25824-34.927616-85.25824L163.396608 289.168384z\"  ></path><path d=\"M959.522816 858.791936\"  ></path></symbol><symbol id=\"icon-videofill\" viewBox=\"0 0 1024 1024\"><path d=\"M512 64C265.6 64 64 265.6 64 512s201.6 448 448 448 448-201.6 448-448S758.4 64 512 64zM691.2 544l-256 156.8C428.8 704 422.4 704 416 704c-6.4 0-9.6 0-16-3.2C390.4 694.4 384 684.8 384 672L384 352c0-12.8 6.4-22.4 16-28.8 9.6-6.4 22.4-6.4 32 0l256 166.4c9.6 6.4 16 16 16 28.8C704 528 700.8 540.8 691.2 544z\"  ></path></symbol><symbol id=\"icon-favorfill\" viewBox=\"0 0 1024 1024\"><path d=\"M957.216 404.32c-3.808-11.36-13.632-19.68-25.504-21.504l-270.336-41.728-120.8-258.624C535.328 71.232 524.032 64.032 511.648 64c0 0-0.032 0-0.064 0-12.384 0-23.648 7.136-28.928 18.336l-121.856 258.016-270.72 40.8c-11.872 1.792-21.728 10.048-25.568 21.408-3.84 11.36-0.992 23.936 7.36 32.512l196.448 202.08L221.44 921.952c-1.984 12.096 3.104 24.256 13.12 31.328 9.984 7.072 23.168 7.808 33.888 1.92l241.824-133.024 241.312 133.856C756.416 958.656 761.76 960 767.104 960c0.256 0 0.48 0 0.64 0 17.696 0 32-14.304 32-32 0-3.968-0.704-7.776-2.016-11.296l-44.896-278.688 196.928-201.248C958.08 428.224 960.992 415.68 957.216 404.32z\"  ></path></symbol><symbol id=\"icon-favor\" viewBox=\"0 0 1024 1024\"><path d=\"M767.104 959.936c-5.344 0-10.688-1.344-15.52-4.032l-241.312-133.856-241.824 133.024c-10.72 5.92-23.904 5.152-33.888-1.92-10.016-7.072-15.104-19.264-13.12-31.328l46.88-284.736-196.448-202.08c-8.256-8.512-11.168-20.928-7.456-32.192 3.68-11.296 13.312-19.616 25.024-21.632l155.072-26.592c17.632-2.944 33.984 8.736 36.96 26.144 2.976 17.408-8.704 33.952-26.144 36.96l-95.168 16.32 165.344 170.08c7.072 7.296 10.272 17.504 8.64 27.488l-38.816 235.68 199.616-109.824c9.632-5.312 21.344-5.312 30.944 0.064l199.168 110.464-38.016-235.776c-1.632-10.016 1.632-20.224 8.704-27.456l164.672-168.256-225.664-34.816c-10.56-1.632-19.584-8.416-24.128-18.08l-99.2-212.384-100.064 211.84c-7.552 16-26.624 22.816-42.624 15.264-15.968-7.552-22.816-26.624-15.264-42.624l129.152-273.44c5.312-11.2 16.576-18.336 28.928-18.336 0 0 0.032 0 0.064 0 12.416 0.032 23.68 7.232 28.928 18.464l120.8 258.624 270.336 41.728c11.872 1.824 21.696 10.144 25.504 21.504 3.776 11.36 0.864 23.936-7.488 32.48l-196.928 201.216 45.92 284.864c1.952 12.096-3.2 24.256-13.216 31.296C780 958.016 773.568 959.936 767.104 959.936z\"  ></path></symbol><symbol id=\"icon-loading\" viewBox=\"0 0 1024 1024\"><path d=\"M923.424 447.744\"  ></path><path d=\"M512.064 963.296c-96.16 0-189.344-30.816-267.68-89.472-95.744-71.712-157.856-176.48-174.848-294.912-16.992-118.464 13.152-236.448 84.864-332.224 148.096-197.76 429.44-238.08 627.136-90.08 82.88 62.08 142.016 151.584 166.56 252 4.192 17.184-6.336 34.496-23.488 38.688-17.152 4.064-34.496-6.304-38.688-23.488-20.992-86.048-71.68-162.752-142.752-215.968C573.792 80.96 332.608 115.552 205.632 285.056c-61.472 82.08-87.296 183.2-72.704 284.736 14.56 101.536 67.808 191.296 149.888 252.736 169.536 127.04 410.688 92.384 537.6-77.12 33.216-44.384 56-94.112 67.648-147.84 3.776-17.28 20.896-28.256 38.048-24.512 17.28 3.744 28.256 20.8 24.512 38.048-13.664 62.784-40.224 120.832-78.976 172.672-71.712 95.744-176.48 157.888-294.976 174.848C555.072 961.76 533.504 963.296 512.064 963.296z\"  ></path></symbol><symbol id=\"icon-search\" viewBox=\"0 0 1024 1024\"><path d=\"M953.504 908.256l-152.608-163.296c61.856-74.496 95.872-167.36 95.872-265.12 0-229.344-186.624-415.968-416.032-415.968-229.344 0-415.968 186.592-415.968 415.968s186.624 415.968 416 415.968c60.096-0.032 118.048-12.576 172.224-37.248 16.096-7.328 23.2-26.304 15.872-42.368-7.328-16.128-26.4-23.264-42.368-15.872-45.856 20.864-94.88 31.456-145.76 31.488-194.08 0-351.968-157.888-351.968-351.968 0-194.048 157.888-351.968 351.968-351.968 194.112 0 352.032 157.888 352.032 351.968 0 91.36-34.848 177.92-98.08 243.744-12.256 12.736-11.84 32.992 0.864 45.248 0.96 0.928 2.208 1.28 3.296 2.08 0.864 1.28 1.312 2.752 2.4 3.904l165.504 177.088c6.272 6.752 14.816 10.144 23.36 10.144 7.84 0 15.68-2.848 21.856-8.64C964.864 941.408 965.568 921.152 953.504 908.256z\"  ></path></symbol><symbol id=\"icon-likefill\" viewBox=\"0 0 1024 1024\"><path d=\"M736 128c-65.952 0-128.576 25.024-176.384 70.464-4.576 4.32-28.672 28.736-47.328 47.68L464.96 199.04C417.12 153.216 354.272 128 288 128c-141.152 0-256 114.848-256 256 0 82.432 41.184 144.288 76.48 182.496l316.896 320.128C450.464 911.68 478.304 928 512 928c33.696 0 61.568-16.32 86.752-41.504l316.736-320 2.208-2.464C955.904 516.384 992 471.392 992 384 992 242.848 877.152 128 736 128z\"  ></path></symbol><symbol id=\"icon-like\" viewBox=\"0 0 1024 1024\"><path d=\"M512 928c-28.928 0-57.92-12.672-86.624-41.376L106.272 564C68.064 516.352 32 471.328 32 384c0-141.152 114.848-256 256-256 53.088 0 104 16.096 147.296 46.592 14.432 10.176 17.92 30.144 7.712 44.608-10.176 14.432-30.08 17.92-44.608 7.712C366.016 204.064 327.808 192 288 192c-105.888 0-192 86.112-192 192 0 61.408 20.288 90.112 59.168 138.688l315.584 318.816C486.72 857.472 499.616 863.808 512 864c12.704 0.192 24.928-6.176 41.376-22.624l316.672-319.904C896.064 493.28 928 445.696 928 384c0-105.888-86.112-192-192-192-48.064 0-94.08 17.856-129.536 50.272l-134.08 134.112c-12.512 12.512-32.736 12.512-45.248 0s-12.512-32.736 0-45.248l135.104-135.136C610.56 151.808 671.904 128 736 128c141.152 0 256 114.848 256 256 0 82.368-41.152 144.288-75.68 181.696l-317.568 320.8C569.952 915.328 540.96 928 512 928z\"  ></path></symbol><symbol id=\"icon-notificationfill\" viewBox=\"0 0 1024 1024\"><path d=\"M526.432 924.064c-20.96 0-44.16-12.576-68.96-37.344L274.752 704 192 704c-52.928 0-96-43.072-96-96l0-192c0-52.928 43.072-96 96-96l82.752 0 182.624-182.624c24.576-24.576 47.744-37.024 68.864-37.024C549.184 100.352 576 116 576 160l0 704C576 908.352 549.28 924.064 526.432 924.064z\"  ></path><path d=\"M687.584 730.368c-6.464 0-12.992-1.952-18.656-6.016-14.336-10.304-17.632-30.304-7.328-44.672l12.672-17.344C707.392 617.44 736 578.624 736 512c0-69.024-25.344-102.528-57.44-144.928-5.664-7.456-11.328-15.008-16.928-22.784-10.304-14.336-7.04-34.336 7.328-44.672 14.368-10.368 34.336-7.04 44.672 7.328 5.248 7.328 10.656 14.464 15.968 21.504C764.224 374.208 800 421.504 800 512c0 87.648-39.392 141.12-74.144 188.32l-12.224 16.736C707.36 725.76 697.568 730.368 687.584 730.368z\"  ></path><path d=\"M796.448 839.008c-7.488 0-15.04-2.624-21.088-7.936-13.28-11.648-14.624-31.872-2.976-45.152C836.608 712.672 896 628.864 896 512c0-116.864-59.392-200.704-123.616-273.888-11.648-13.312-10.304-33.504 2.976-45.184 13.216-11.648 33.44-10.336 45.152 2.944C889.472 274.56 960 373.6 960 512s-70.528 237.472-139.488 316.096C814.144 835.328 805.312 839.008 796.448 839.008z\"  ></path></symbol><symbol id=\"icon-notification\" viewBox=\"0 0 1024 1024\"><path d=\"M526.432 924.064c-20.96 0-44.16-12.576-68.96-37.344L274.752 704 192 704c-52.928 0-96-43.072-96-96l0-192c0-52.928 43.072-96 96-96l82.752 0 182.624-182.624c24.576-24.576 47.744-37.024 68.864-37.024C549.184 100.352 576 116 576 160l0 704C576 908.352 549.28 924.064 526.432 924.064zM192 384c-17.632 0-32 14.368-32 32l0 192c0 17.664 14.368 32 32 32l96 0c8.48 0 16.64 3.36 22.624 9.376l192.064 192.096c3.392 3.36 6.496 6.208 9.312 8.576L512 174.016c-2.784 2.336-5.952 5.184-9.376 8.608l-192 192C304.64 380.64 296.48 384 288 384L192 384z\"  ></path><path d=\"M687.584 730.368c-6.464 0-12.992-1.952-18.656-6.016-14.336-10.304-17.632-30.304-7.328-44.672l12.672-17.344C707.392 617.44 736 578.624 736 512c0-69.024-25.344-102.528-57.44-144.928-5.664-7.456-11.328-15.008-16.928-22.784-10.304-14.336-7.04-34.336 7.328-44.672 14.368-10.368 34.336-7.04 44.672 7.328 5.248 7.328 10.656 14.464 15.968 21.504C764.224 374.208 800 421.504 800 512c0 87.648-39.392 141.12-74.144 188.32l-12.224 16.736C707.36 725.76 697.568 730.368 687.584 730.368z\"  ></path><path d=\"M796.448 839.008c-7.488 0-15.04-2.624-21.088-7.936-13.28-11.648-14.624-31.872-2.976-45.152C836.608 712.672 896 628.864 896 512c0-116.864-59.392-200.704-123.616-273.888-11.648-13.312-10.304-33.504 2.976-45.184 13.216-11.648 33.44-10.336 45.152 2.944C889.472 274.56 960 373.6 960 512s-70.528 237.472-139.488 316.096C814.144 835.328 805.312 839.008 796.448 839.008z\"  ></path></symbol><symbol id=\"icon-evaluate\" viewBox=\"0 0 1024 1024\"><path d=\"M405.536 932.128c-8.8 0-17.568-3.584-23.904-10.688-83.232-93.216-217.856-94.112-220.64-94.112-0.096 0-0.096 0-0.192 0-17.568 0-31.904-14.176-32.032-31.744-0.128-17.664 14.016-32.064 31.648-32.256 6.656-0.352 165.664-0.192 268.96 115.488 11.776 13.184 10.624 33.408-2.56 45.184C420.736 929.44 413.12 932.128 405.536 932.128z\"  ></path><path d=\"M620.128 932.064c-7.776 0-15.584-2.816-21.76-8.544-12.96-12-13.76-32.256-1.728-45.216 106.976-115.52 262.752-115.392 268.896-115.136 17.696 0.192 31.872 14.656 31.68 32.32-0.192 17.568-14.464 31.68-32 31.68-0.096 0-0.16 0-0.256 0L864.96 827.168c-2.656 0-134.56 0.864-221.376 94.624C637.312 928.64 628.704 932.064 620.128 932.064z\"  ></path><path d=\"M563.712 769.312l-101.408 0c-3.232 0-6.368-0.48-9.344-1.408-108.256-13.888-200.256-76.704-259.552-177.44C124.576 473.472 108.8 321.888 154.208 213.248c4.896-11.68 16.224-19.392 28.896-19.648l9.632-0.096c60.96 0 117.888 11.52 169.696 34.304 37.44-68.448 80.192-120.384 127.328-154.688 2.496-2.176 5.344-4 8.512-5.344l0 0c9.888-4.288 21.376-3.36 30.432 2.528 1.568 1.024 3.04 2.176 4.384 3.456 47.264 34.688 88.832 85.856 126.464 155.744 52.832-23.872 111.072-35.968 173.632-35.968l9.792 0.096c12.672 0.256 24 7.968 28.896 19.648 45.408 108.672 29.664 260.256-39.232 377.248-59.296 100.672-151.264 163.52-259.488 177.44C570.112 768.8 566.976 769.312 563.712 769.312zM468 705.312l90.016 0c0.672-0.128 1.376-0.224 2.08-0.32 115.712-12.864 182.08-87.072 217.376-147.008 54.048-91.872 70.432-211.936 42.528-300.32-58.24 1.888-111.616 16.16-158.816 42.528-7.712 4.288-16.832 5.184-25.184 2.624-8.384-2.624-15.328-8.608-19.2-16.512-32.448-66.368-67.232-115.2-105.824-148.48-38.624 33.216-74.176 82.4-105.92 146.592-3.872 7.808-10.72 13.696-19.04 16.32-8.32 2.656-17.28 1.76-24.96-2.4C314.848 273.216 262.72 259.552 206.048 257.696c-27.904 88.384-11.52 208.448 42.528 300.288 35.296 59.936 101.632 134.112 217.312 147.008C466.592 705.088 467.296 705.184 468 705.312z\"  ></path><path d=\"M512.768 960c-17.664 0-32-14.304-32-32l0-190.688c0-17.696 14.336-32 32-32s32 14.304 32 32L544.768 928C544.736 945.696 530.432 960 512.768 960z\"  ></path></symbol><symbol id=\"icon-homefill\" viewBox=\"0 0 1024 1024\"><path d=\"M947.2 422.4L572.8 115.2c-32-25.6-86.4-25.6-118.4 0L76.8 425.6c-12.8 6.4-16 22.4-9.6 35.2 3.2 12.8 16 19.2 28.8 19.2h32v364.8C128 892.8 163.2 928 211.2 928H416c19.2 0 32-12.8 32-32v-147.2c0-22.4 35.2-44.8 64-44.8 28.8 0 67.2 22.4 67.2 44.8V896c0 19.2 12.8 32 32 32h208c48 0 80-32 80-83.2V480h32c12.8 0 25.6-9.6 28.8-22.4 3.2-12.8 0-25.6-12.8-35.2z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-link\" viewBox=\"0 0 1024 1024\"><path d=\"M545.856 640c-0.416 0-0.8 0-1.216 0-17.216 0-33.12-13.12-44.832-25.952l-36.992-43.392c-11.904-13.056-10.944-34.688 2.112-46.56 13.088-11.936 33.312-11.648 45.216 1.408l36.992 40.256 173.472-179.136-49.888-54.144-72 74.144c-12.48 12.544-32.704 12.672-45.248 0.192-12.576-12.416-12.672-32.704-0.256-45.248l73.44-74.176c12.128-12.256 28.992-18.336 45.408-18.752 17.184 0.16 33.184 7.168 45.056 19.68l49.888 52.512c23.872 25.12 23.52 65.728-0.736 90.464l-176 184.448C578.272 627.872 562.592 640 545.856 640z\"  ></path><path d=\"M351.968 754.592c-16.32 0-32.64-6.048-45.088-18.208l-50.432-49.216c-12.224-11.936-19.04-27.936-19.2-45.056-0.128-17.152 6.4-33.28 18.464-45.44l179.04-187.104C446.784 397.44 462.848 384 479.968 384c0.032 0 0.096 0 0.16 0 17.088 0 33.12 13.344 45.152 25.376l41.312 44.672c12.512 12.512 12.512 34.432 0 46.912-12.512 12.512-32.736 13.344-45.248 0.832l-41.312-40.896-178.848 180.608 50.4 48.96 74.944-73.344c12.608-12.416 32.8-12.32 45.28 0.288 12.416 12.576 12.32 32.832-0.224 45.248l-74.176 73.408C384.896 748.384 368.416 754.592 351.968 754.592z\"  ></path><path d=\"M512 960C264.96 960 64 759.04 64 512S264.96 64 512 64s448 200.96 448 448S759.04 960 512 960zM512 128C300.256 128 128 300.256 128 512c0 211.744 172.256 384 384 384 211.744 0 384-172.256 384-384C896 300.256 723.744 128 512 128z\"  ></path></symbol><symbol id=\"icon-roundaddfill\" viewBox=\"0 0 1024 1024\"><path d=\"M828.704 196.576c-84.608-84.192-197.056-130.56-316.704-130.56s-232.128 46.368-316.736 130.56C110.624 280.8 64 392.832 64 512c0 119.2 46.624 231.2 131.232 315.424 84.608 84.192 197.088 130.56 316.736 130.56s232.128-46.368 316.704-130.56c84.672-84.256 131.296-196.288 131.264-315.456C959.968 392.8 913.376 280.8 828.704 196.576zM736 544l-192 0 0 192c0 17.696-14.336 32-32 32s-32-14.304-32-32l0-192L288 544c-17.664 0-32-14.336-32-32s14.336-32 32-32l192 0L480 288c0-17.664 14.336-32 32-32s32 14.336 32 32l0 192 192 0c17.696 0 32 14.336 32 32S753.696 544 736 544z\"  ></path></symbol><symbol id=\"icon-roundadd\" viewBox=\"0 0 1024 1024\"><path d=\"M512 958.016c-119.648 0-232.128-46.368-316.736-130.56C110.624 743.2 64 631.2 64 512c0-119.168 46.624-231.2 131.232-315.424 84.608-84.192 197.088-130.56 316.736-130.56s232.128 46.368 316.704 130.56c84.672 84.224 131.264 196.256 131.264 315.392 0.032 119.2-46.592 231.232-131.264 315.456C744.128 911.616 631.648 958.016 512 958.016zM512 129.984c-102.624 0-199.072 39.744-271.584 111.936C167.936 314.048 128 409.984 128 512c0 102.016 39.904 197.952 112.384 270.048 72.512 72.192 168.96 111.936 271.584 111.936 102.592 0 199.072-39.744 271.584-111.936 72.48-72.16 112.416-168.064 112.384-270.08 0-102.016-39.904-197.92-112.384-270.016C711.072 169.76 614.592 129.984 512 129.984z\"  ></path><path d=\"M736 480l-192 0L544 288c0-17.664-14.336-32-32-32s-32 14.336-32 32l0 192L288 480c-17.664 0-32 14.336-32 32s14.336 32 32 32l192 0 0 192c0 17.696 14.336 32 32 32s32-14.304 32-32l0-192 192 0c17.696 0 32-14.336 32-32S753.696 480 736 480z\"  ></path></symbol><symbol id=\"icon-add\" viewBox=\"0 0 1024 1024\"><path d=\"M800 480l-256 0L544 224c0-17.664-14.336-32-32-32s-32 14.336-32 32l0 256L224 480c-17.664 0-32 14.336-32 32s14.336 32 32 32l256 0 0 256c0 17.696 14.336 32 32 32s32-14.304 32-32l0-256 256 0c17.696 0 32-14.336 32-32S817.696 480 800 480z\"  ></path></symbol><symbol id=\"icon-appreciatefill\" viewBox=\"0 0 1024 1024\"><path d=\"M873.6 416h-188.8c12.8-44.8 28.8-115.2 19.2-188.8-6.4-60.8-41.6-105.6-92.8-124.8-38.4-12.8-76.8-6.4-99.2 16-25.6 25.6-38.4 76.8-51.2 128-9.6 35.2-16 70.4-28.8 89.6-32 54.4-102.4 76.8-115.2 80H224c-19.2 0-32 12.8-32 32v448c0 19.2 12.8 32 32 32h547.2C896 928 960 537.6 960 515.2c0-57.6-44.8-99.2-86.4-99.2zM96 416c-19.2 0-32 12.8-32 32v448c0 19.2 12.8 32 32 32s32-12.8 32-32V448c0-19.2-12.8-32-32-32z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-forwardfill\" viewBox=\"0 0 1024 1024\"><path d=\"M951.52 493.952l-384-419.424c-8.96-9.696-22.944-12.928-35.2-8.096C520.064 71.232 512 83.072 512 96.224l0 224.896c-149.28 9.44-272.768 85.664-358.592 221.824-68 107.936-88.064 214.688-88.896 219.2-2.464 13.408 3.904 26.912 15.776 33.6 11.872 6.72 26.72 5.152 36.96-3.904C118.848 790.368 274.24 655.264 512 643.84l0 283.904c0 13.152 8.032 24.96 20.288 29.792 3.808 1.504 7.776 2.208 11.712 2.208 8.704 0 17.248-3.552 23.392-10.176l384-412.096C962.816 525.248 962.88 506.272 951.52 493.952z\"  ></path></symbol><symbol id=\"icon-voicefill\" viewBox=\"0 0 1024 1024\"><path d=\"M512 705.728c105.888 0 192-86.112 192-192L704 257.952c0-105.888-86.112-192-192-192s-192 86.112-192 192l0 255.776C320 619.584 406.112 705.728 512 705.728z\"  ></path><path d=\"M864 479.776 864 352c0-17.664-14.304-32-32-32s-32 14.336-32 32l0 127.776c0 160.16-129.184 290.464-288 290.464-158.784 0-288-130.304-288-290.464L224 352c0-17.664-14.336-32-32-32s-32 14.336-32 32l0 127.776c0 184.608 140.864 336.48 320 352.832L480 896 288 896c-17.664 0-32 14.304-32 32s14.336 32 32 32l448 0c17.696 0 32-14.304 32-32s-14.304-32-32-32l-192 0 0-63.36C723.136 816.256 864 664.384 864 479.776z\"  ></path></symbol><symbol id=\"icon-wefill\" viewBox=\"0 0 1024 1024\"><path d=\"M768 732.8c-6.4 0-12.8-3.2-19.2-6.4-12.8-9.6-16-32-6.4-44.8 38.4-48 57.6-105.6 57.6-166.4 0-64-25.6-128-70.4-176-12.8-12.8-9.6-32 3.2-44.8 12.8-12.8 32-9.6 44.8 3.2 54.4 60.8 86.4 140.8 86.4 220.8 0 73.6-25.6 147.2-73.6 204.8-3.2 6.4-12.8 9.6-22.4 9.6z m-492.8 0c-9.6 0-16-3.2-22.4-9.6C195.2 665.6 160 588.8 160 508.8S192 352 252.8 294.4c12.8-12.8 32-12.8 44.8 0 12.8 12.8 12.8 32 0 44.8C252.8 387.2 224 448 224 508.8s25.6 121.6 73.6 169.6c12.8 12.8 12.8 32 0 44.8-6.4 6.4-12.8 9.6-22.4 9.6z\" fill=\"#666666\" ></path><path d=\"M224 896c-6.4 0-16-3.2-22.4-6.4l-48-48C76.8 748.8 32 633.6 32 512c0-121.6 44.8-240 124.8-329.6 16-16 28.8-32 44.8-44.8 16-9.6 35.2-9.6 48 3.2 12.8 12.8 9.6 32-3.2 44.8-16 12.8-28.8 25.6-41.6 41.6C134.4 304 96 406.4 96 512c0 105.6 38.4 204.8 105.6 284.8 12.8 16 25.6 28.8 41.6 41.6 12.8 12.8 16 32 3.2 44.8-3.2 6.4-12.8 12.8-22.4 12.8z m576 0c-9.6 0-16-3.2-22.4-9.6-12.8-12.8-12.8-32 0-44.8 28.8-28.8 54.4-54.4 64-67.2C899.2 697.6 928 608 928 512c0-102.4-35.2-201.6-99.2-278.4-16-19.2-32-35.2-51.2-51.2-12.8-12.8-16-32-3.2-44.8 12.8-12.8 32-16 44.8-3.2 22.4 19.2 41.6 38.4 57.6 57.6 73.6 89.6 115.2 201.6 115.2 320 0 108.8-35.2 211.2-99.2 297.6-16 22.4-51.2 57.6-70.4 76.8-6.4 6.4-12.8 9.6-22.4 9.6zM672 406.4c-19.2-16-44.8-25.6-70.4-22.4-25.6 3.2-48 16-64 35.2l-25.6 32-25.6-32c-32-41.6-92.8-48-134.4-12.8-19.2 16-32 41.6-32 67.2-3.2 25.6 6.4 51.2 22.4 73.6l96 118.4c19.2 22.4 44.8 35.2 70.4 35.2h3.2c28.8 3.2 57.6-12.8 76.8-35.2l96-118.4c32-41.6 25.6-105.6-12.8-140.8z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-keyboard\" viewBox=\"0 0 1024 1024\"><path d=\"M736 352c-17.696 0-32 14.336-32 32l0 96-96 0c-17.696 0-32 14.336-32 32s14.304 32 32 32l128 0c17.696 0 32-14.336 32-32l0-128C768 366.336 753.696 352 736 352z\"  ></path><path d=\"M736 672 288 672c-17.664 0-32-14.304-32-32s14.336-32 32-32l448 0c17.696 0 32 14.304 32 32S753.696 672 736 672z\"  ></path><path d=\"M480 544l-32 0c-17.664 0-32-14.336-32-32s14.336-32 32-32l32 0c17.664 0 32 14.336 32 32S497.664 544 480 544z\"  ></path><path d=\"M320 544 288 544c-17.664 0-32-14.336-32-32s14.336-32 32-32l32 0c17.664 0 32 14.336 32 32S337.664 544 320 544z\"  ></path><path d=\"M480 416l-32 0c-17.664 0-32-14.336-32-32s14.336-32 32-32l32 0c17.664 0 32 14.336 32 32S497.664 416 480 416z\"  ></path><path d=\"M640 416l-32 0c-17.696 0-32-14.336-32-32s14.304-32 32-32l32 0c17.696 0 32 14.336 32 32S657.696 416 640 416z\"  ></path><path d=\"M320 416 288 416c-17.664 0-32-14.336-32-32s14.336-32 32-32l32 0c17.664 0 32 14.336 32 32S337.664 416 320 416z\"  ></path><path d=\"M512.128 957.344c-247.04 0-448-200.96-448-448s200.96-448 448-448 448 200.96 448 448S759.168 957.344 512.128 957.344zM512.128 125.344c-211.744 0-384 172.256-384 384 0 211.744 172.256 384 384 384 211.744 0 384-172.256 384-384C896.128 297.6 723.872 125.344 512.128 125.344z\"  ></path></symbol><symbol id=\"icon-picfill\" viewBox=\"0 0 1024 1024\"><path d=\"M842.688 128 181.312 128C116.64 128 64 180.64 64 245.312l0 533.376C64 843.36 116.64 896 181.312 896l661.376 0C907.36 896 960 843.36 960 778.688L960 245.312C960 180.64 907.36 128 842.688 128zM288 288c35.36 0 64 28.64 64 64s-28.64 64-64 64c-35.328 0-64-28.64-64-64S252.672 288 288 288zM832 736c0 17.696-14.304 31.488-32 31.488L225.92 768c-0.128 0-0.224 0-0.352 0-10.08 0-19.616-4.288-25.664-12.384-6.112-8.192-7.936-18.56-4.896-28.352 2.304-7.488 58.272-183.552 180.064-183.552 38.08 0.896 67.424 9.824 95.776 18.336 35.712 10.72 70.528 19.936 109.664 13.76 20.448-3.296 28.896-23.808 43.328-69.952 19.04-60.8 47.808-152.736 174.656-152.736 17.536 0 31.776 14.08 32 31.616L832 511.616 832 736z\"  ></path></symbol><symbol id=\"icon-markfill\" viewBox=\"0 0 1024 1024\"><path d=\"M512 64C264.96 64 64 236.256 64 448c0 182.976 149.344 339.168 357.056 375.744l64.896 90.848C491.968 923.008 501.664 928 512 928s20.032-4.992 26.048-13.408l64.896-90.88C810.656 787.168 960 631.008 960 448 960 236.256 759.04 64 512 64zM288.032 512c-35.296 0-64-28.704-64-64s28.704-64 64-64 64 28.704 64 64S323.328 512 288.032 512zM512.032 512c-35.296 0-64-28.704-64-64s28.704-64 64-64c35.296 0 64 28.704 64 64S547.328 512 512.032 512zM736.032 512c-35.296 0-64-28.704-64-64s28.704-64 64-64 64 28.704 64 64S771.328 512 736.032 512z\"  ></path></symbol><symbol id=\"icon-presentfill\" viewBox=\"0 0 1024 1024\"><path d=\"M928 288h-147.2l54.4-51.2c19.2-19.2 28.8-44.8 28.8-73.6 0-25.6-9.6-51.2-28.8-70.4-19.2-19.2-44.8-28.8-70.4-28.8-25.6 0-51.2 9.6-70.4 28.8l-105.6 105.6c-12.8 12.8-12.8 32 0 44.8 12.8 12.8 32 12.8 44.8 0l105.6-105.6c6.4-6.4 16-9.6 25.6-9.6 9.6 0 19.2 3.2 25.6 9.6 6.4 9.6 9.6 16 9.6 25.6s-3.2 19.2-9.6 25.6L691.2 288h-166.4l-195.2-195.2C310.4 73.6 284.8 64 259.2 64c-25.6 0-51.2 9.6-70.4 28.8S160 137.6 160 163.2c0 28.8 9.6 54.4 28.8 73.6L243.2 288H96c-19.2 0-32 12.8-32 32v163.2c0 19.2 12.8 32 32 32v313.6C96 883.2 137.6 928 188.8 928h646.4c51.2 0 96-44.8 96-102.4V512c16 0 32-16 32-32v-160c-3.2-19.2-16-32-35.2-32zM233.6 188.8c-6.4-6.4-9.6-16-9.6-25.6 0-9.6 3.2-19.2 9.6-25.6s16-9.6 25.6-9.6c9.6 0 19.2 3.2 25.6 9.6L435.2 288h-102.4L233.6 188.8z m633.6 326.4H544V832c0 19.2-12.8 32-32 32s-32-12.8-32-32v-316.8H163.2c-19.2 0-32-12.8-32-32s12.8-32 32-32H480V384c0-19.2 12.8-32 32-32s32 12.8 32 32v67.2h323.2c19.2 0 32 12.8 32 32s-12.8 32-32 32z\" fill=\"#60646D\" ></path></symbol><symbol id=\"icon-peoplefill\" viewBox=\"0 0 1024 1024\"><path d=\"M649.6 633.6c86.4-48 147.2-144 147.2-249.6 0-160-128-288-288-288s-288 128-288 288c0 108.8 57.6 201.6 147.2 249.6-121.6 48-214.4 153.6-240 288-3.2 9.6 0 19.2 6.4 25.6 3.2 9.6 12.8 12.8 22.4 12.8h704c9.6 0 19.2-3.2 25.6-12.8 6.4-6.4 9.6-16 6.4-25.6-25.6-134.4-121.6-240-243.2-288z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-read\" viewBox=\"0 0 1024 1024\"><path d=\"M448 928c-1.184 0-2.368-0.064-3.52-0.192l-286.432-31.84C106.016 894.944 64 852.288 64 800L64 192c0-52.928 43.072-96 96-96 1.184 0 2.368 0.064 3.52 0.192L449.952 128C501.984 129.056 544 171.712 544 224l0 608C544 884.928 500.928 928 448 928zM158.528 160.032C141.568 160.8 128 174.848 128 192l0 608c0 17.664 14.368 32 32 32 1.184 0 2.368 0.064 3.52 0.192l285.952 31.776C466.432 863.2 480 849.152 480 832L480 224c0-17.632-14.368-32-32-32-1.184 0-2.368-0.064-3.52-0.192L158.528 160.032z\"  ></path><path d=\"M609.184 924.32c-16.096 0-29.984-12.096-31.776-28.48-1.952-17.568 10.72-33.376 28.288-35.328l254.784-28.32C861.632 832.064 862.816 832 864 832c17.664 0 32-14.336 32-32L896 192c0-17.152-13.568-31.2-30.528-31.968L612.48 188.128c-17.248 2.112-33.376-10.656-35.328-28.256-1.952-17.568 10.72-33.376 28.288-35.328l255.072-28.352C861.632 96.064 862.816 96 864 96c52.928 0 96 43.072 96 96l0 608c0 52.288-41.984 94.944-94.048 95.968l-253.184 28.16C611.552 924.256 610.336 924.32 609.184 924.32z\"  ></path></symbol><symbol id=\"icon-backwardfill\" viewBox=\"0 0 1024 1024\"><path d=\"M814.976 222.304c-10.432-5.504-23.168-4.832-32.96 1.824L576 364.288 576 250.592c0-11.872-6.56-22.72-17.024-28.288-10.432-5.504-23.168-4.832-32.96 1.824l-384 261.248C133.248 491.328 128 501.248 128 511.84s5.248 20.512 14.016 26.464l384 261.376c5.408 3.68 11.68 5.568 17.984 5.568 5.12 0 10.272-1.216 14.976-3.712C569.44 796 576 785.088 576 773.248l0-113.792 206.016 140.224c5.408 3.68 11.68 5.568 17.984 5.568 5.12 0 10.272-1.216 14.976-3.712C825.44 796 832 785.088 832 773.248L832 250.592C832 238.72 825.44 227.872 814.976 222.304z\"  ></path></symbol><symbol id=\"icon-playfill\" viewBox=\"0 0 1024 1024\"><path d=\"M817.088 484.96l-512-323.744C295.232 154.976 282.752 154.592 272.576 160.224 262.336 165.856 256 176.608 256 188.256l0 647.328c0 11.648 6.336 22.4 16.576 28.032 4.8 2.656 10.112 3.968 15.424 3.968 5.952 0 11.904-1.664 17.088-4.928l512-323.616C826.368 533.184 832 522.976 832 512 832 501.024 826.368 490.816 817.088 484.96z\"  ></path></symbol><symbol id=\"icon-all\" viewBox=\"0 0 1024 1024\"><path d=\"M384 896c-17.664 0-32-14.304-32-32L352 160c0-17.664 14.336-32 32-32s32 14.336 32 32l0 704C416 881.696 401.664 896 384 896z\"  ></path><path d=\"M641.056 896.128c-17.696 0-32-14.304-32-32l0-704c0-17.664 14.304-32 32-32s32 14.336 32 32l0 704C673.056 881.824 658.752 896.128 641.056 896.128z\"  ></path><path d=\"M864 736c-17.696 0-32-14.304-32-32L832 320c0-17.664 14.304-32 32-32s32 14.336 32 32l0 384C896 721.696 881.696 736 864 736z\"  ></path><path d=\"M160 736c-17.664 0-32-14.304-32-32L128 320c0-17.664 14.336-32 32-32s32 14.336 32 32l0 384C192 721.696 177.664 736 160 736z\"  ></path></symbol><symbol id=\"icon-hotfill\" viewBox=\"0 0 1024 1024\"><path d=\"M726.4 201.6c-12.8-9.6-28.8-6.4-38.4 0-9.6 9.6-16 25.6-9.6 38.4 6.4 12.8 9.6 28.8 12.8 44.8-86.4-201.6-230.4-246.4-236.8-249.6-9.6-3.2-22.4 0-28.8 6.4-9.6 6.4-12.8 19.2-9.6 28.8 12.8 86.4-25.6 188.8-115.2 310.4-6.4-25.6-16-51.2-32-80-9.6-9.6-22.4-16-35.2-12.8-16 3.2-25.6 12.8-25.6 28.8-3.2 48-25.6 92.8-51.2 140.8-22.4 41.6-44.8 86.4-54.4 134.4-32 150.4 99.2 329.6 233.6 380.8 9.6 3.2 19.2 6.4 32 9.6-25.6-19.2-41.6-51.2-48-96-25.6-195.2 185.6-246.4 195.2-425.6 153.6 105.6 224 336 137.6 505.6 3.2 0 6.4-3.2 9.6-3.2 0 0 3.2 0 3.2-3.2 163.2-89.6 252.8-208 259.2-345.6 16-211.2-163.2-390.4-198.4-412.8z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-recordfill\" viewBox=\"0 0 1024 1024\"><path d=\"M943.488 260c-10.176-5.632-22.592-5.312-32.48 0.864L800 330.368 800 288c0-52.928-43.072-96-96-96L160 192C107.072 192 64 235.072 64 288l0 448c0 52.928 43.072 96 96 96l544 0c52.928 0 96-43.072 96-96l0-38.88 111.648 66.368C916.672 766.496 922.336 768 928 768c5.472 0 10.912-1.408 15.808-4.192C953.824 758.112 960 747.488 960 736L960 288C960 276.352 953.696 265.632 943.488 260zM256 448c-35.296 0-64-28.704-64-64s28.704-64 64-64 64 28.704 64 64S291.296 448 256 448z\"  ></path></symbol><symbol id=\"icon-full\" viewBox=\"0 0 1024 1024\"><path d=\"M639.328 416c8.032 0 16.096-3.008 22.304-9.056l202.624-197.184-0.8 143.808c-0.096 17.696 14.144 32.096 31.808 32.192 0.064 0 0.128 0 0.192 0 17.6 0 31.904-14.208 32-31.808l1.248-222.208c0-0.672-0.352-1.248-0.384-1.92 0.032-0.512 0.288-0.896 0.288-1.408 0.032-17.664-14.272-32-31.968-32.032L671.552 96l-0.032 0c-17.664 0-31.968 14.304-32 31.968C639.488 145.632 653.824 160 671.488 160l151.872 0.224-206.368 200.8c-12.672 12.32-12.928 32.608-0.64 45.248C622.656 412.736 630.976 416 639.328 416z\"  ></path><path d=\"M896.032 639.552 896.032 639.552c-17.696 0-32 14.304-32.032 31.968l-0.224 151.872-200.832-206.4c-12.32-12.64-32.576-12.96-45.248-0.64-12.672 12.352-12.928 32.608-0.64 45.248l197.184 202.624-143.808-0.8c-0.064 0-0.128 0-0.192 0-17.6 0-31.904 14.208-32 31.808-0.096 17.696 14.144 32.096 31.808 32.192l222.24 1.248c0.064 0 0.128 0 0.192 0 0.64 0 1.12-0.32 1.76-0.352 0.512 0.032 0.896 0.288 1.408 0.288l0.032 0c17.664 0 31.968-14.304 32-31.968L928 671.584C928.032 653.952 913.728 639.584 896.032 639.552z\"  ></path><path d=\"M209.76 159.744l143.808 0.8c0.064 0 0.128 0 0.192 0 17.6 0 31.904-14.208 32-31.808 0.096-17.696-14.144-32.096-31.808-32.192L131.68 95.328c-0.064 0-0.128 0-0.192 0-0.672 0-1.248 0.352-1.888 0.384-0.448 0-0.8-0.256-1.248-0.256 0 0-0.032 0-0.032 0-17.664 0-31.968 14.304-32 31.968L96 352.448c-0.032 17.664 14.272 32 31.968 32.032 0 0 0.032 0 0.032 0 17.664 0 31.968-14.304 32-31.968l0.224-151.936 200.832 206.4c6.272 6.464 14.624 9.696 22.944 9.696 8.032 0 16.096-3.008 22.304-9.056 12.672-12.32 12.96-32.608 0.64-45.248L209.76 159.744z\"  ></path><path d=\"M362.368 617.056l-202.624 197.184 0.8-143.808c0.096-17.696-14.144-32.096-31.808-32.192-0.064 0-0.128 0-0.192 0-17.6 0-31.904 14.208-32 31.808l-1.248 222.24c0 0.704 0.352 1.312 0.384 2.016 0 0.448-0.256 0.832-0.256 1.312-0.032 17.664 14.272 32 31.968 32.032L352.448 928c0 0 0.032 0 0.032 0 17.664 0 31.968-14.304 32-31.968s-14.272-32-31.968-32.032l-151.936-0.224 206.4-200.832c12.672-12.352 12.96-32.608 0.64-45.248S375.008 604.704 362.368 617.056z\"  ></path></symbol><symbol id=\"icon-favor_fill_light\" viewBox=\"0 0 1024 1024\"><path d=\"M915.2 413.866667c-4.266667-14.933333-19.2-25.6-34.133333-29.866667l-228.266667-34.133333-100.266667-215.466667c-6.4-14.933333-21.333333-25.6-38.4-25.6s-32 8.533333-38.4 23.466667l-106.666666 215.466666L142.933333 384c-14.933333 2.133333-27.733333 12.8-34.133333 27.733333-4.266667 14.933333-2.133333 32 10.666667 42.666667l166.4 172.8-38.4 238.933333c-2.133333 17.066667 4.266667 32 17.066666 42.666667 12.8 8.533333 32 10.666667 44.8 2.133333l202.666667-110.933333 202.666667 113.066667c6.4 4.266667 12.8 6.4 21.333333 6.4s17.066667-2.133333 23.466667-8.533334c12.8-8.533333 19.2-25.6 17.066666-40.533333l-38.4-241.066667 166.4-170.666666c10.666667-12.8 14.933333-29.866667 10.666667-44.8z\"  ></path></symbol><symbol id=\"icon-round_favor_fill\" viewBox=\"0 0 1024 1024\"><path d=\"M515.2 67.2c-246.4 0-448 201.6-448 448s201.6 448 448 448 448-201.6 448-448-201.6-448-448-448z m214.4 425.6l-86.4 86.4 19.2 121.6c3.2 12.8-3.2 25.6-12.8 32-6.4 3.2-12.8 6.4-19.2 6.4-6.4 0-9.6 0-16-3.2L512 678.4 409.6 736c-9.6 6.4-22.4 6.4-35.2-3.2-9.6-6.4-16-19.2-12.8-32l19.2-121.6-83.2-86.4c-9.6-9.6-12.8-22.4-6.4-32 3.2-12.8 12.8-19.2 25.6-22.4l115.2-16 51.2-112c6.4-9.6 16-19.2 28.8-19.2s22.4 6.4 28.8 19.2l51.2 112 115.2 19.2c12.8 3.2 22.4 9.6 25.6 22.4 6.4 6.4 6.4 19.2-3.2 28.8z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-round_location_fill\" viewBox=\"0 0 1024 1024\"><path d=\"M515.2 67.2c-246.4 0-448 201.6-448 448s201.6 448 448 448 448-201.6 448-448c0-249.6-201.6-448-448-448z m73.6 620.8l-60.8 70.4c-3.2 3.2-9.6 6.4-12.8 6.4-6.4 0-9.6-3.2-12.8-6.4-6.4-9.6-188.8-198.4-188.8-304 0-112 89.6-201.6 201.6-201.6s201.6 89.6 201.6 201.6c-3.2 54.4-44.8 131.2-128 233.6z\" fill=\"#666666\" ></path><path d=\"M512 454.4m-92.8 0a92.8 92.8 0 1 0 185.6 0 92.8 92.8 0 1 0-185.6 0Z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-round_like_fill\" viewBox=\"0 0 1024 1024\"><path d=\"M515.2 67.2c-246.4 0-448 201.6-448 448s201.6 448 448 448 448-201.6 448-448-201.6-448-448-448zM704 563.2l-150.4 153.6c-12.8 12.8-25.6 19.2-41.6 19.2s-28.8-6.4-41.6-19.2l-147.2-153.6C307.2 544 288 515.2 288 476.8c0-67.2 54.4-121.6 118.4-121.6 32 0 60.8 12.8 83.2 35.2l22.4 22.4c9.6-9.6 19.2-22.4 22.4-22.4 22.4-22.4 51.2-35.2 83.2-35.2 67.2 0 118.4 54.4 118.4 121.6 3.2 41.6-12.8 64-32 86.4z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-round_people_fill\" viewBox=\"0 0 1024 1024\"><path d=\"M515.2 64c-246.4 0-448 198.4-448 444.8S268.8 953.6 512 953.6c246.4 0 444.8-201.6 444.8-444.8S758.4 64 515.2 64z m185.6 656c-3.2 6.4-9.6 6.4-12.8 6.4h-352c-6.4 0-9.6-3.2-12.8-6.4-3.2-3.2-6.4-9.6-3.2-12.8 12.8-67.2 60.8-121.6 121.6-144-44.8-25.6-73.6-70.4-73.6-124.8 0-80 64-144 144-144s144 64 144 144c0 54.4-32 102.4-73.6 124.8 60.8 25.6 108.8 76.8 121.6 144 3.2 3.2 0 9.6-3.2 12.8z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-round_skin_fill\" viewBox=\"0 0 1024 1024\"><path d=\"M515.2 67.2c-246.4 0-448 201.6-448 448s201.6 448 448 448 448-201.6 448-448-198.4-448-448-448z m233.6 396.8c-16 16-32 28.8-48 44.8-6.4 6.4-12.8 16-22.4 16-12.8 3.2-22.4-6.4-28.8-12.8-3.2-3.2-3.2-6.4-6.4-6.4v208c0 6.4-3.2 9.6-6.4 12.8-9.6 6.4-25.6 6.4-41.6 6.4h-160c-12.8 0-22.4 0-32-3.2-3.2-3.2-6.4-6.4-9.6-12.8v-211.2c-6.4 6.4-12.8 12.8-22.4 16-6.4 3.2-16 0-22.4-3.2l-9.6-9.6c-6.4-6.4-12.8-12.8-22.4-19.2l-9.6-9.6-12.8-12.8c-3.2-6.4-9.6-9.6-12.8-16 0-3.2-3.2-6.4-3.2-12.8 3.2-9.6 9.6-16 16-22.4l12.8-12.8 67.2-64c3.2-3.2 6.4-9.6 12.8-12.8 6.4-6.4 9.6-16 22.4-16h41.6c3.2 3.2 6.4 3.2 9.6 6.4l9.6 9.6c3.2 6.4 9.6 9.6 16 12.8 6.4 3.2 16 6.4 22.4 6.4h22.4c16-3.2 25.6-6.4 35.2-16 3.2-3.2 6.4-9.6 9.6-12.8 3.2-3.2 3.2-3.2 6.4-3.2s3.2 0 6.4-3.2h12.8c9.6 0 28.8-3.2 35.2 3.2 6.4 3.2 9.6 6.4 12.8 12.8l16 16c19.2 19.2 38.4 35.2 57.6 54.4l19.2 19.2c3.2 6.4 9.6 9.6 12.8 16v3.2c3.2 16 0 22.4-6.4 28.8z\" fill=\"#666666\" ></path></symbol><symbol id=\"icon-broadcast_fill\" viewBox=\"0 0 1024 1024\"><path d=\"M704 374.4V198.4C704 128 675.2 128 643.2 128c-9.6 0-112 83.2-144 112l-112 89.6H179.2c-64 0-115.2 51.2-115.2 112v144c0 60.8 51.2 112 115.2 112h19.2v176c0 32 25.6 57.6 57.6 57.6s57.6-25.6 57.6-57.6v-176h73.6l112 92.8c41.6 38.4 134.4 108.8 144 108.8 28.8 0 60.8 0 60.8-70.4v-195.2c38.4-28.8 64-73.6 64-128 0-54.4-25.6-99.2-64-131.2z\"  ></path><path d=\"M819.2 262.4c-12.8-9.6-35.2-9.6-44.8 6.4-9.6 12.8-9.6 35.2 6.4 44.8 76.8 60.8 115.2 124.8 115.2 198.4 0 73.6-38.4 140.8-115.2 198.4-12.8 9.6-16 32-6.4 44.8 6.4 9.6 16 12.8 25.6 12.8 6.4 0 12.8-3.2 19.2-6.4 92.8-70.4 140.8-156.8 140.8-249.6 0-92.8-48-179.2-140.8-249.6z\"  ></path></symbol><symbol id=\"icon-card_fill\" viewBox=\"0 0 1024 1024\"><path d=\"M891.733333 170.666667H132.266667C93.866667 170.666667 64 200.533333 64 238.933333V341.333333h896v-102.4C960 200.533333 930.133333 170.666667 891.733333 170.666667zM891.733333 874.666667c38.4 0 68.266667-29.866667 68.266667-68.266667V426.666667H64v379.733333C64 844.8 93.866667 874.666667 132.266667 874.666667h759.466666zM192 554.666667h253.866667c19.2 0 32 12.8 32 32s-12.8 32-32 32H187.733333c-17.066667-2.133333-27.733333-14.933333-27.733333-32 0-19.2 12.8-32 32-32z m0 128h128c19.2 0 32 12.8 32 32S339.2 746.666667 320 746.666667H187.733333c-17.066667-2.133333-27.733333-14.933333-27.733333-32 0-19.2 12.8-32 32-32z\"  ></path></symbol></svg>'),\n  (function (a) {\n    var c = (c = document.getElementsByTagName('script'))[c.length - 1];\n    const l = c.getAttribute('data-injectcss');\n    var c = c.getAttribute('data-disable-injectsvg');\n    if (!c) {\n      let o;\n      let i;\n      var t;\n      var h;\n      var s;\n      const d = function (c, l) {\n        l.parentNode.insertBefore(c, l);\n      };\n      if (l && !a.__iconfont__svg__cssinject__) {\n        a.__iconfont__svg__cssinject__ = !0;\n        try {\n          document.write(\n            '<style>.svgfont {display: inline-block;width: 1em;height: 1em;fill: currentColor;vertical-align: -0.1em;font-size:16px;}</style>'\n          );\n        } catch (c) {\n          console && console.log(c);\n        }\n      }\n      ((o = function () {\n        let c;\n        let l = document.createElement('div');\n        ((l.innerHTML = a._iconfont_svg_string_2685283),\n          (l = l.getElementsByTagName('svg')[0]) &&\n            (l.setAttribute('aria-hidden', 'true'),\n            (l.style.position = 'absolute'),\n            (l.style.width = 0),\n            (l.style.height = 0),\n            (l.style.overflow = 'hidden'),\n            (l = l),\n            (c = document.body).firstChild ? d(l, c.firstChild) : c.appendChild(l)));\n      }),\n        document.addEventListener\n          ? ~['complete', 'loaded', 'interactive'].indexOf(document.readyState)\n            ? setTimeout(o, 0)\n            : ((i = function () {\n                (document.removeEventListener('DOMContentLoaded', i, !1), o());\n              }),\n              document.addEventListener('DOMContentLoaded', i, !1))\n          : document.attachEvent &&\n            ((t = o),\n            (h = a.document),\n            (s = !1),\n            e(),\n            (h.onreadystatechange = function () {\n              h.readyState == 'complete' && ((h.onreadystatechange = null), p());\n            })));\n    }\n    function p() {\n      s || ((s = !0), t());\n    }\n    function e() {\n      try {\n        h.documentElement.doScroll('left');\n      } catch {\n        return void setTimeout(e, 50);\n      }\n      p();\n    }\n  })(window));\n"
  },
  {
    "path": "src/renderer/assets/icon/iconfont.json",
    "content": "{\n  \"id\": \"2685283\",\n  \"name\": \"music\",\n  \"font_family\": \"iconfont\",\n  \"css_prefix_text\": \"icon-\",\n  \"description\": \"\",\n  \"glyphs\": [\n    {\n      \"icon_id\": \"1111849\",\n      \"name\": \"list\",\n      \"font_class\": \"list\",\n      \"unicode\": \"e603\",\n      \"unicode_decimal\": 58883\n    },\n    {\n      \"icon_id\": \"1306794\",\n      \"name\": \"maxsize\",\n      \"font_class\": \"maxsize\",\n      \"unicode\": \"e692\",\n      \"unicode_decimal\": 59026\n    },\n    {\n      \"icon_id\": \"4437591\",\n      \"name\": \"close\",\n      \"font_class\": \"close\",\n      \"unicode\": \"e616\",\n      \"unicode_decimal\": 58902\n    },\n    {\n      \"icon_id\": \"5383753\",\n      \"name\": \"minisize\",\n      \"font_class\": \"minisize\",\n      \"unicode\": \"e602\",\n      \"unicode_decimal\": 58882\n    },\n    {\n      \"icon_id\": \"13075017\",\n      \"name\": \"刷新\",\n      \"font_class\": \"shuaxin\",\n      \"unicode\": \"e627\",\n      \"unicode_decimal\": 58919\n    },\n    {\n      \"icon_id\": \"24457556\",\n      \"name\": \"icon_error\",\n      \"font_class\": \"icon_error\",\n      \"unicode\": \"e615\",\n      \"unicode_decimal\": 58901\n    },\n    {\n      \"icon_id\": \"24492642\",\n      \"name\": \"3 User\",\n      \"font_class\": \"a-3User\",\n      \"unicode\": \"e601\",\n      \"unicode_decimal\": 58881\n    },\n    {\n      \"icon_id\": \"24492643\",\n      \"name\": \"Chat\",\n      \"font_class\": \"Chat\",\n      \"unicode\": \"e605\",\n      \"unicode_decimal\": 58885\n    },\n    {\n      \"icon_id\": \"24492646\",\n      \"name\": \"Category\",\n      \"font_class\": \"Category\",\n      \"unicode\": \"e606\",\n      \"unicode_decimal\": 58886\n    },\n    {\n      \"icon_id\": \"24492661\",\n      \"name\": \"Document\",\n      \"font_class\": \"Document\",\n      \"unicode\": \"e607\",\n      \"unicode_decimal\": 58887\n    },\n    {\n      \"icon_id\": \"24492662\",\n      \"name\": \"Heart\",\n      \"font_class\": \"Heart\",\n      \"unicode\": \"e608\",\n      \"unicode_decimal\": 58888\n    },\n    {\n      \"icon_id\": \"24492665\",\n      \"name\": \"Hide\",\n      \"font_class\": \"Hide\",\n      \"unicode\": \"e609\",\n      \"unicode_decimal\": 58889\n    },\n    {\n      \"icon_id\": \"24492667\",\n      \"name\": \"Home\",\n      \"font_class\": \"Home\",\n      \"unicode\": \"e60a\",\n      \"unicode_decimal\": 58890\n    },\n    {\n      \"icon_id\": \"24492678\",\n      \"name\": \"Image 2\",\n      \"font_class\": \"a-Image2\",\n      \"unicode\": \"e60b\",\n      \"unicode_decimal\": 58891\n    },\n    {\n      \"icon_id\": \"24492684\",\n      \"name\": \"Profile\",\n      \"font_class\": \"Profile\",\n      \"unicode\": \"e60c\",\n      \"unicode_decimal\": 58892\n    },\n    {\n      \"icon_id\": \"24492685\",\n      \"name\": \"Search\",\n      \"font_class\": \"Search\",\n      \"unicode\": \"e60d\",\n      \"unicode_decimal\": 58893\n    },\n    {\n      \"icon_id\": \"24492687\",\n      \"name\": \"Paper\",\n      \"font_class\": \"Paper\",\n      \"unicode\": \"e60e\",\n      \"unicode_decimal\": 58894\n    },\n    {\n      \"icon_id\": \"24492690\",\n      \"name\": \"Play\",\n      \"font_class\": \"Play\",\n      \"unicode\": \"e60f\",\n      \"unicode_decimal\": 58895\n    },\n    {\n      \"icon_id\": \"24492698\",\n      \"name\": \"Setting\",\n      \"font_class\": \"Setting\",\n      \"unicode\": \"e610\",\n      \"unicode_decimal\": 58896\n    },\n    {\n      \"icon_id\": \"24492708\",\n      \"name\": \"Ticket Star\",\n      \"font_class\": \"a-TicketStar\",\n      \"unicode\": \"e611\",\n      \"unicode_decimal\": 58897\n    },\n    {\n      \"icon_id\": \"24492712\",\n      \"name\": \"Volume Off\",\n      \"font_class\": \"a-VolumeOff\",\n      \"unicode\": \"e612\",\n      \"unicode_decimal\": 58898\n    },\n    {\n      \"icon_id\": \"24492713\",\n      \"name\": \"Volume Up\",\n      \"font_class\": \"a-VolumeUp\",\n      \"unicode\": \"e613\",\n      \"unicode_decimal\": 58899\n    },\n    {\n      \"icon_id\": \"24492714\",\n      \"name\": \"Volume Down\",\n      \"font_class\": \"a-VolumeDown\",\n      \"unicode\": \"e614\",\n      \"unicode_decimal\": 58900\n    },\n    {\n      \"icon_id\": \"18875422\",\n      \"name\": \"暂停 停止  灰色\",\n      \"font_class\": \"stop\",\n      \"unicode\": \"e600\",\n      \"unicode_decimal\": 58880\n    },\n    {\n      \"icon_id\": \"15262786\",\n      \"name\": \"1_music82\",\n      \"font_class\": \"next\",\n      \"unicode\": \"e6a9\",\n      \"unicode_decimal\": 59049\n    },\n    {\n      \"icon_id\": \"15262807\",\n      \"name\": \"1_music83\",\n      \"font_class\": \"prev\",\n      \"unicode\": \"e6ac\",\n      \"unicode_decimal\": 59052\n    },\n    {\n      \"icon_id\": \"15262830\",\n      \"name\": \"1_music81\",\n      \"font_class\": \"play\",\n      \"unicode\": \"e6aa\",\n      \"unicode_decimal\": 59050\n    },\n    {\n      \"icon_id\": \"15367\",\n      \"name\": \"下三角形\",\n      \"font_class\": \"xiasanjiaoxing\",\n      \"unicode\": \"e642\",\n      \"unicode_decimal\": 58946\n    },\n    {\n      \"icon_id\": \"1096518\",\n      \"name\": \"video_fill\",\n      \"font_class\": \"videofill\",\n      \"unicode\": \"e7c7\",\n      \"unicode_decimal\": 59335\n    },\n    {\n      \"icon_id\": \"29930\",\n      \"name\": \"favor_fill\",\n      \"font_class\": \"favorfill\",\n      \"unicode\": \"e64b\",\n      \"unicode_decimal\": 58955\n    },\n    {\n      \"icon_id\": \"29931\",\n      \"name\": \"favor\",\n      \"font_class\": \"favor\",\n      \"unicode\": \"e64c\",\n      \"unicode_decimal\": 58956\n    },\n    {\n      \"icon_id\": \"29934\",\n      \"name\": \"loading\",\n      \"font_class\": \"loading\",\n      \"unicode\": \"e64f\",\n      \"unicode_decimal\": 58959\n    },\n    {\n      \"icon_id\": \"29947\",\n      \"name\": \"search\",\n      \"font_class\": \"search\",\n      \"unicode\": \"e65c\",\n      \"unicode_decimal\": 58972\n    },\n    {\n      \"icon_id\": \"30417\",\n      \"name\": \"like_fill\",\n      \"font_class\": \"likefill\",\n      \"unicode\": \"e668\",\n      \"unicode_decimal\": 58984\n    },\n    {\n      \"icon_id\": \"30418\",\n      \"name\": \"like\",\n      \"font_class\": \"like\",\n      \"unicode\": \"e669\",\n      \"unicode_decimal\": 58985\n    },\n    {\n      \"icon_id\": \"30419\",\n      \"name\": \"notification_fill\",\n      \"font_class\": \"notificationfill\",\n      \"unicode\": \"e66a\",\n      \"unicode_decimal\": 58986\n    },\n    {\n      \"icon_id\": \"30420\",\n      \"name\": \"notification\",\n      \"font_class\": \"notification\",\n      \"unicode\": \"e66b\",\n      \"unicode_decimal\": 58987\n    },\n    {\n      \"icon_id\": \"30434\",\n      \"name\": \"evaluate\",\n      \"font_class\": \"evaluate\",\n      \"unicode\": \"e672\",\n      \"unicode_decimal\": 58994\n    },\n    {\n      \"icon_id\": \"33519\",\n      \"name\": \"home_fill\",\n      \"font_class\": \"homefill\",\n      \"unicode\": \"e6bb\",\n      \"unicode_decimal\": 59067\n    },\n    {\n      \"icon_id\": \"34922\",\n      \"name\": \"link\",\n      \"font_class\": \"link\",\n      \"unicode\": \"e6bf\",\n      \"unicode_decimal\": 59071\n    },\n    {\n      \"icon_id\": \"38744\",\n      \"name\": \"round_add_fill\",\n      \"font_class\": \"roundaddfill\",\n      \"unicode\": \"e6d8\",\n      \"unicode_decimal\": 59096\n    },\n    {\n      \"icon_id\": \"38746\",\n      \"name\": \"round_add\",\n      \"font_class\": \"roundadd\",\n      \"unicode\": \"e6d9\",\n      \"unicode_decimal\": 59097\n    },\n    {\n      \"icon_id\": \"38747\",\n      \"name\": \"add\",\n      \"font_class\": \"add\",\n      \"unicode\": \"e6da\",\n      \"unicode_decimal\": 59098\n    },\n    {\n      \"icon_id\": \"43903\",\n      \"name\": \"appreciate_fill\",\n      \"font_class\": \"appreciatefill\",\n      \"unicode\": \"e6e3\",\n      \"unicode_decimal\": 59107\n    },\n    {\n      \"icon_id\": \"52506\",\n      \"name\": \"forward_fill\",\n      \"font_class\": \"forwardfill\",\n      \"unicode\": \"e6ea\",\n      \"unicode_decimal\": 59114\n    },\n    {\n      \"icon_id\": \"55448\",\n      \"name\": \"voice_fill\",\n      \"font_class\": \"voicefill\",\n      \"unicode\": \"e6f0\",\n      \"unicode_decimal\": 59120\n    },\n    {\n      \"icon_id\": \"61146\",\n      \"name\": \"we_fill\",\n      \"font_class\": \"wefill\",\n      \"unicode\": \"e6f4\",\n      \"unicode_decimal\": 59124\n    },\n    {\n      \"icon_id\": \"90847\",\n      \"name\": \"keyboard\",\n      \"font_class\": \"keyboard\",\n      \"unicode\": \"e71b\",\n      \"unicode_decimal\": 59163\n    },\n    {\n      \"icon_id\": \"127305\",\n      \"name\": \"pic_fill\",\n      \"font_class\": \"picfill\",\n      \"unicode\": \"e72c\",\n      \"unicode_decimal\": 59180\n    },\n    {\n      \"icon_id\": \"143738\",\n      \"name\": \"mark_fill\",\n      \"font_class\": \"markfill\",\n      \"unicode\": \"e730\",\n      \"unicode_decimal\": 59184\n    },\n    {\n      \"icon_id\": \"143740\",\n      \"name\": \"present_fill\",\n      \"font_class\": \"presentfill\",\n      \"unicode\": \"e732\",\n      \"unicode_decimal\": 59186\n    },\n    {\n      \"icon_id\": \"158873\",\n      \"name\": \"people_fill\",\n      \"font_class\": \"peoplefill\",\n      \"unicode\": \"e735\",\n      \"unicode_decimal\": 59189\n    },\n    {\n      \"icon_id\": \"176313\",\n      \"name\": \"read\",\n      \"font_class\": \"read\",\n      \"unicode\": \"e742\",\n      \"unicode_decimal\": 59202\n    },\n    {\n      \"icon_id\": \"212324\",\n      \"name\": \"backward_fill\",\n      \"font_class\": \"backwardfill\",\n      \"unicode\": \"e74d\",\n      \"unicode_decimal\": 59213\n    },\n    {\n      \"icon_id\": \"212328\",\n      \"name\": \"play_fill\",\n      \"font_class\": \"playfill\",\n      \"unicode\": \"e74f\",\n      \"unicode_decimal\": 59215\n    },\n    {\n      \"icon_id\": \"240126\",\n      \"name\": \"all\",\n      \"font_class\": \"all\",\n      \"unicode\": \"e755\",\n      \"unicode_decimal\": 59221\n    },\n    {\n      \"icon_id\": \"240128\",\n      \"name\": \"hot_fill\",\n      \"font_class\": \"hotfill\",\n      \"unicode\": \"e757\",\n      \"unicode_decimal\": 59223\n    },\n    {\n      \"icon_id\": \"747747\",\n      \"name\": \"record_fill\",\n      \"font_class\": \"recordfill\",\n      \"unicode\": \"e7a4\",\n      \"unicode_decimal\": 59300\n    },\n    {\n      \"icon_id\": \"1005712\",\n      \"name\": \"full\",\n      \"font_class\": \"full\",\n      \"unicode\": \"e7bc\",\n      \"unicode_decimal\": 59324\n    },\n    {\n      \"icon_id\": \"1512759\",\n      \"name\": \"favor_fill_light\",\n      \"font_class\": \"favor_fill_light\",\n      \"unicode\": \"e7ec\",\n      \"unicode_decimal\": 59372\n    },\n    {\n      \"icon_id\": \"4110741\",\n      \"name\": \"round_favor_fill\",\n      \"font_class\": \"round_favor_fill\",\n      \"unicode\": \"e80a\",\n      \"unicode_decimal\": 59402\n    },\n    {\n      \"icon_id\": \"4110743\",\n      \"name\": \"round_location_fill\",\n      \"font_class\": \"round_location_fill\",\n      \"unicode\": \"e80b\",\n      \"unicode_decimal\": 59403\n    },\n    {\n      \"icon_id\": \"4110745\",\n      \"name\": \"round_like_fill\",\n      \"font_class\": \"round_like_fill\",\n      \"unicode\": \"e80c\",\n      \"unicode_decimal\": 59404\n    },\n    {\n      \"icon_id\": \"4110746\",\n      \"name\": \"round_people_fill\",\n      \"font_class\": \"round_people_fill\",\n      \"unicode\": \"e80d\",\n      \"unicode_decimal\": 59405\n    },\n    {\n      \"icon_id\": \"4110750\",\n      \"name\": \"round_skin_fill\",\n      \"font_class\": \"round_skin_fill\",\n      \"unicode\": \"e80e\",\n      \"unicode_decimal\": 59406\n    },\n    {\n      \"icon_id\": \"11778953\",\n      \"name\": \"broadcast_fill\",\n      \"font_class\": \"broadcast_fill\",\n      \"unicode\": \"e81d\",\n      \"unicode_decimal\": 59421\n    },\n    {\n      \"icon_id\": \"12625085\",\n      \"name\": \"card_fill\",\n      \"font_class\": \"card_fill\",\n      \"unicode\": \"e81f\",\n      \"unicode_decimal\": 59423\n    }\n  ]\n}\n"
  },
  {
    "path": "src/renderer/components/Coffee.vue",
    "content": "<template>\n  <div class=\"relative inline-block\">\n    <n-popover trigger=\"hover\" placement=\"top\" :show-arrow=\"true\" :raw=\"true\" :delay=\"100\">\n      <template #trigger>\n        <slot>\n          <n-button\n            quaternary\n            class=\"inline-flex items-center gap-2 px-4 py-2 transition-all duration-300 hover:-translate-y-0.5\"\n          >\n            {{ t('comp.coffee.title') }}\n          </n-button>\n        </slot>\n      </template>\n\n      <div class=\"p-6 rounded-lg shadow-lg bg-light dark:bg-gray-800\">\n        <div class=\"flex gap-10\">\n          <div class=\"flex flex-col items-center gap-2\">\n            <n-image\n              :src=\"alipayQR\"\n              :alt=\"t('comp.coffee.alipayQR')\"\n              class=\"w-32 h-32 rounded-lg cursor-none\"\n              preview-disabled\n            />\n            <span class=\"text-sm text-gray-700 dark:text-gray-200\">{{\n              t('comp.coffee.alipay')\n            }}</span>\n          </div>\n          <div class=\"flex flex-col items-center gap-2\">\n            <n-image\n              :src=\"wechatQR\"\n              :alt=\"t('comp.coffee.wechatQR')\"\n              class=\"w-32 h-32 rounded-lg cursor-none\"\n              preview-disabled\n            />\n            <span class=\"text-sm text-gray-700 dark:text-gray-200\">{{\n              t('comp.coffee.wechat')\n            }}</span>\n          </div>\n        </div>\n\n        <div class=\"mt-4\">\n          <p\n            class=\"text-sm text-gray-700 dark:text-gray-200 text-center cursor-pointer hover:text-green-500\"\n            @click=\"copyText\"\n          >\n            {{ t('comp.coffee.groupText') }}\n          </p>\n        </div>\n        <div class=\"mt-4\">\n          <!-- 赞赏列表地址 -->\n          <p\n            class=\"text-sm text-green-600 dark:text-gray-200 text-center cursor-pointer hover:text-green-500\"\n            @click=\"toDonateList\"\n          >\n            {{ t('comp.coffee.donateList') }}\n          </p>\n        </div>\n      </div>\n    </n-popover>\n  </div>\n</template>\n\n<script setup>\nimport { NButton, NImage, NPopover, useMessage } from 'naive-ui';\nimport { useI18n } from 'vue-i18n';\n\nimport alipay from '@/assets/alipay.png';\nimport wechat from '@/assets/wechat.png';\n\nconst { t } = useI18n();\n\nconst message = useMessage();\nconst copyText = () => {\n  navigator.clipboard.writeText('AlgerMusic');\n  message.success(t('common.copySuccess'));\n};\n\nconst toDonateList = () => {\n  window.open('http://donate.alger.fun/download', '_blank');\n};\n\ndefineProps({\n  alipayQR: {\n    type: String,\n    default: alipay\n  },\n  wechatQR: {\n    type: String,\n    default: wechat\n  }\n});\n</script>\n"
  },
  {
    "path": "src/renderer/components/EQControl.vue",
    "content": "<template>\n  <div class=\"eq-control\">\n    <div class=\"eq-header\">\n      <h3>\n        {{ t('player.eq.title') }}\n        <n-tag type=\"warning\" size=\"small\" round v-if=\"!isElectron\">\n          桌面版可用，网页端不支持\n        </n-tag>\n      </h3>\n      <div class=\"eq-controls\">\n        <n-switch v-model:value=\"isEnabled\" @update:value=\"toggleEQ\">\n          <template #checked>{{ t('player.eq.on') }}</template>\n          <template #unchecked>{{ t('player.eq.off') }}</template>\n        </n-switch>\n      </div>\n    </div>\n\n    <div class=\"eq-presets\">\n      <n-scrollbar x-scrollable>\n        <n-space :size=\"6\" :wrap=\"false\">\n          <n-tag\n            v-for=\"preset in presetOptions\"\n            :key=\"preset.value\"\n            :type=\"currentPreset === preset.value ? 'success' : 'default'\"\n            :bordered=\"false\"\n            size=\"medium\"\n            round\n            clickable\n            @click=\"applyPreset(preset.value)\"\n          >\n            {{ preset.label }}\n          </n-tag>\n        </n-space>\n      </n-scrollbar>\n    </div>\n\n    <div class=\"eq-sliders\">\n      <div v-for=\"freq in frequencies\" :key=\"freq\" class=\"eq-slider\">\n        <div class=\"freq-label\">{{ formatFreq(freq) }}</div>\n        <n-slider\n          v-model:value=\"eqValues[freq.toString()]\"\n          :min=\"-12\"\n          :max=\"12\"\n          :step=\"0.1\"\n          vertical\n          :disabled=\"!isEnabled\"\n          @update:value=\"updateEQ(freq.toString(), $event)\"\n        />\n        <div class=\"gain-value\">{{ eqValues[freq.toString()] }}dB</div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { audioService } from '@/services/audioService';\nimport { isElectron } from '@/utils';\n\nconst { t } = useI18n();\n\nconst frequencies = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];\nconst eqValues = ref<{ [key: string]: number }>({});\nconst isEnabled = ref(audioService.isEQEnabled());\nconst currentPreset = ref(audioService.getCurrentPreset() || 'flat');\n\n// 预设配置\nconst presets = {\n  flat: {\n    label: t('player.eq.presets.flat'),\n    values: Object.fromEntries(frequencies.map((f) => [f, 0]))\n  },\n  pop: {\n    label: t('player.eq.presets.pop'),\n    values: {\n      31: -1.5,\n      62: 3.5,\n      125: 5.5,\n      250: 3.5,\n      500: -0.5,\n      1000: -1.5,\n      2000: 1.5,\n      4000: 2.5,\n      8000: 2.5,\n      16000: 2.5\n    }\n  },\n  rock: {\n    label: t('player.eq.presets.rock'),\n    values: {\n      31: 4.5,\n      62: 3.5,\n      125: 2,\n      250: 0.5,\n      500: -0.5,\n      1000: -1,\n      2000: 0.5,\n      4000: 2,\n      8000: 2.5,\n      16000: 3.5\n    }\n  },\n  classical: {\n    label: t('player.eq.presets.classical'),\n    values: {\n      31: 3.5,\n      62: 3,\n      125: 2.5,\n      250: 1.5,\n      500: -0.5,\n      1000: -1.5,\n      2000: -1.5,\n      4000: 0.5,\n      8000: 2,\n      16000: 3\n    }\n  },\n  jazz: {\n    label: t('player.eq.presets.jazz'),\n    values: {\n      31: 3,\n      62: 2,\n      125: 1.5,\n      250: 2,\n      500: -1,\n      1000: -1.5,\n      2000: -0.5,\n      4000: 1,\n      8000: 2.5,\n      16000: 3\n    }\n  },\n  hiphop: {\n    label: t('player.eq.presets.hiphop'),\n    values: {\n      31: 5,\n      62: 4.5,\n      125: 3,\n      250: 1.5,\n      500: -0.5,\n      1000: -1,\n      2000: 0.5,\n      4000: 1.5,\n      8000: 2,\n      16000: 2.5\n    }\n  },\n  vocal: {\n    label: t('player.eq.presets.vocal'),\n    values: {\n      31: -2,\n      62: -1.5,\n      125: -1,\n      250: 0.5,\n      500: 2,\n      1000: 3.5,\n      2000: 3,\n      4000: 1.5,\n      8000: 0.5,\n      16000: 0\n    }\n  },\n  dance: {\n    label: t('player.eq.presets.dance'),\n    values: {\n      31: 4,\n      62: 3.5,\n      125: 2.5,\n      250: 1,\n      500: 0,\n      1000: -0.5,\n      2000: 1.5,\n      4000: 2.5,\n      8000: 3,\n      16000: 2.5\n    }\n  },\n  acoustic: {\n    label: t('player.eq.presets.acoustic'),\n    values: {\n      31: 2,\n      62: 1.5,\n      125: 1,\n      250: 1.5,\n      500: 2,\n      1000: 1.5,\n      2000: 2,\n      4000: 2.5,\n      8000: 2,\n      16000: 1.5\n    }\n  }\n};\n\nconst presetOptions = Object.entries(presets).map(([value, preset]) => ({\n  label: preset.label,\n  value\n}));\n\nconst toggleEQ = (enabled: boolean) => {\n  audioService.setEQEnabled(enabled);\n};\n\nconst applyPreset = (presetName: string) => {\n  currentPreset.value = presetName;\n  audioService.setCurrentPreset(presetName);\n  const preset = presets[presetName as keyof typeof presets];\n  if (preset) {\n    Object.entries(preset.values).forEach(([freq, gain]) => {\n      updateEQ(freq, gain);\n    });\n  }\n};\n\nonMounted(() => {\n  // 恢复 EQ 设置\n  const settings = audioService.getAllEQSettings();\n  eqValues.value = settings;\n\n  // 如果有保存的预设，应用该预设\n  const savedPreset = audioService.getCurrentPreset();\n  if (savedPreset && presets[savedPreset as keyof typeof presets]) {\n    currentPreset.value = savedPreset;\n  }\n});\n\nconst updateEQ = (frequency: string, gain: number) => {\n  audioService.setEQFrequencyGain(frequency, gain);\n  eqValues.value = {\n    ...eqValues.value,\n    [frequency]: gain\n  };\n\n  // 检查当前值是否与任何预设匹配\n  const currentValues = eqValues.value;\n  let matchedPreset: string | null = null;\n\n  // 检查是否与任何预设完全匹配\n  Object.entries(presets).forEach(([presetName, preset]) => {\n    const isMatch = Object.entries(preset.values).every(\n      ([freq, value]) => Math.abs(currentValues[freq] - value) < 0.1\n    );\n    if (isMatch) {\n      matchedPreset = presetName;\n    }\n  });\n\n  // 更新当前预设状态\n  if (matchedPreset !== null) {\n    currentPreset.value = matchedPreset;\n    audioService.setCurrentPreset(matchedPreset);\n  } else if (currentPreset.value !== 'custom') {\n    // 如果与任何预设都不匹配，将状态设置为自定义\n    currentPreset.value = 'custom';\n    audioService.setCurrentPreset('custom');\n  }\n};\n\nconst formatFreq = (freq: number) => {\n  if (freq >= 1000) {\n    return `${freq / 1000}kHz`;\n  }\n  return `${freq}Hz`;\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.eq-control {\n  @apply p-6 rounded-lg;\n  @apply bg-light dark:bg-dark;\n  width: 100%;\n  max-width: 700px;\n\n  .eq-header {\n    @apply flex justify-between items-center mb-4;\n\n    h3 {\n      @apply text-xl font-semibold;\n      @apply text-gray-800 dark:text-gray-200;\n    }\n  }\n\n  .eq-presets {\n    @apply mb-2 relative;\n    height: 40px;\n\n    :deep(.n-scrollbar) {\n      @apply -mx-2 px-2;\n    }\n\n    :deep(.n-tag) {\n      @apply cursor-pointer transition-all duration-200;\n      text-align: center;\n\n      &:hover {\n        transform: translateY(-2px);\n      }\n    }\n\n    :deep(.n-space) {\n      flex-wrap: nowrap;\n      padding: 4px 0;\n    }\n  }\n\n  .eq-sliders {\n    @apply flex justify-between items-end;\n    @apply bg-gray-50 dark:bg-gray-800 gap-1;\n    @apply rounded-lg p-2;\n    height: 300px;\n\n    .eq-slider {\n      @apply flex flex-col items-center;\n      width: 45px;\n      height: 100%;\n\n      .n-slider {\n        flex: 1;\n        margin: 12px 0;\n        min-height: 180px;\n      }\n\n      .freq-label {\n        @apply text-xs font-medium text-center;\n        @apply text-gray-600 dark:text-gray-400;\n        white-space: nowrap;\n        margin: 8px 0;\n        height: 20px;\n      }\n\n      .gain-value {\n        @apply text-xs font-medium text-center;\n        @apply text-gray-600 dark:text-gray-400;\n        white-space: nowrap;\n        margin: 4px 0;\n        height: 16px;\n      }\n    }\n  }\n}\n\n:deep(.n-slider) {\n  --n-rail-height: 4px;\n  --n-rail-color: theme('colors.gray.200');\n  --n-rail-color-hover: theme('colors.gray.300');\n  --n-fill-color: theme('colors.green.500');\n  --n-fill-color-hover: theme('colors.green.600');\n  --n-handle-color: theme('colors.green.500');\n  --n-handle-box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n\n  .n-slider-handle {\n    @apply transition-all duration-200;\n    &:hover {\n      transform: scale(1.2);\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/LanguageSwitcher.vue",
    "content": "<script setup lang=\"ts\">\nimport { getLanguageOptions } from '@i18n/utils';\nimport { computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { useSettingsStore } from '@/store/modules/settings';\n\nconst settingsStore = useSettingsStore();\nconst { locale } = useI18n();\n\n// 使用自动导入的语言选项\nconst languages = getLanguageOptions();\n\nconsole.log('locale', locale);\n// 使用计算属性来获取当前语言\nconst currentLanguage = computed({\n  get: () => locale.value,\n  set: (value) => {\n    settingsStore.setLanguage(value);\n  }\n});\n</script>\n\n<template>\n  <n-select v-model:value=\"currentLanguage\" :options=\"languages\" size=\"small\" />\n</template>\n"
  },
  {
    "path": "src/renderer/components/MusicList.vue",
    "content": "<template>\n  <n-drawer\n    :show=\"show\"\n    :height=\"isMobile ? '100%' : '80%'\"\n    placement=\"bottom\"\n    block-scroll\n    mask-closable\n    :style=\"{ backgroundColor: 'transparent' }\"\n    :to=\"`#layout-main`\"\n    :z-index=\"zIndex\"\n    @mask-click=\"close\"\n  >\n    <div class=\"music-page\">\n      <div class=\"music-header h-12 flex items-center justify-between\">\n        <n-ellipsis :line-clamp=\"1\" class=\"flex-shrink-0 mr-3\">\n          <div class=\"music-title\">\n            {{ name }}\n          </div>\n        </n-ellipsis>\n\n        <!-- 搜索框 -->\n        <div class=\"flex-grow flex-1 flex items-center justify-end\">\n          <div class=\"search-container\">\n            <n-input\n              v-model:value=\"searchKeyword\"\n              :placeholder=\"t('comp.musicList.searchSongs')\"\n              clearable\n              round\n              size=\"small\"\n            >\n              <template #prefix>\n                <i class=\"icon iconfont ri-search-line text-sm\"></i>\n              </template>\n            </n-input>\n          </div>\n        </div>\n        <div class=\"music-close flex-shrink-0 ml-3\">\n          <i class=\"icon iconfont ri-close-line\" @click=\"close\"></i>\n        </div>\n      </div>\n\n      <div class=\"music-content\">\n        <!-- 左侧歌单信息 -->\n        <div class=\"music-info\">\n          <div class=\"music-cover\">\n            <n-image\n              :src=\"getCoverImgUrl\"\n              class=\"cover-img\"\n              preview-disabled\n              :class=\"setAnimationClass('animate__fadeIn')\"\n              object-fit=\"cover\"\n            />\n          </div>\n          <div v-if=\"listInfo?.creator\" class=\"creator-info\">\n            <n-avatar round :size=\"24\" :src=\"getImgUrl(listInfo.creator.avatarUrl, '50y50')\" />\n            <span class=\"creator-name\">{{ listInfo.creator.nickname }}</span>\n          </div>\n          <div v-if=\"total\" class=\"music-total\">{{ t('player.songNum', { num: total }) }}</div>\n\n          <n-scrollbar style=\"max-height: 200px\">\n            <div v-if=\"listInfo?.description\" class=\"music-desc\">\n              {{ listInfo.description }}\n            </div>\n          </n-scrollbar>\n        </div>\n\n        <!-- 右侧歌曲列表 -->\n        <div class=\"music-list-container\">\n          <div class=\"music-list\">\n            <n-spin :show=\"loadingList || loading\">\n              <div class=\"music-list-content\">\n                <div v-if=\"filteredSongs.length === 0 && searchKeyword\" class=\"no-result\">\n                  {{ t('comp.musicList.noSearchResults') }}\n                </div>\n\n                <!-- 虚拟列表，设置正确的固定高度 -->\n                <n-virtual-list\n                  ref=\"songListRef\"\n                  class=\"song-virtual-list\"\n                  style=\"height: calc(70vh - 60px)\"\n                  :items=\"filteredSongs\"\n                  :item-size=\"70\"\n                  item-resizable\n                  key-field=\"id\"\n                  @scroll=\"handleVirtualScroll\"\n                >\n                  <template #default=\"{ item }\">\n                    <div class=\"double-item\">\n                      <song-item\n                        :item=\"formatSong(item)\"\n                        :can-remove=\"canRemove\"\n                        @play=\"handlePlay\"\n                        @remove-song=\"(id) => emit('remove-song', id)\"\n                      />\n                    </div>\n                  </template>\n                </n-virtual-list>\n              </div>\n            </n-spin>\n          </div>\n          <play-bottom />\n        </div>\n      </div>\n    </div>\n  </n-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport PinyinMatch from 'pinyin-match';\nimport { computed, onUnmounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { getMusicDetail } from '@/api/music';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { SongResult } from '@/types/music';\nimport { getImgUrl, isMobile, setAnimationClass } from '@/utils';\n\nimport PlayBottom from './common/PlayBottom.vue';\n\nconst { t } = useI18n();\nconst playerStore = usePlayerStore();\nconst props = withDefaults(\n  defineProps<{\n    show: boolean;\n    name: string;\n    zIndex?: number;\n    songList: any[];\n    loading?: boolean;\n    listInfo?: {\n      trackIds: { id: number }[];\n      [key: string]: any;\n    };\n    cover?: boolean;\n    canRemove?: boolean;\n  }>(),\n  {\n    loading: false,\n    cover: true,\n    zIndex: 9996,\n    canRemove: false\n  }\n);\n\nconst emit = defineEmits(['update:show', 'update:loading', 'remove-song']);\n\nconst page = ref(0);\nconst pageSize = 40;\nconst isLoadingMore = ref(false);\nconst displayedSongs = ref<SongResult[]>([]);\nconst loadingList = ref(false);\nconst loadedIds = ref(new Set<number>()); // 用于追踪已加载的歌曲ID\nconst isPlaylistLoading = ref(false); // 标记是否正在加载播放列表\nconst completePlaylist = ref<SongResult[]>([]); // 存储完整的播放列表\nconst hasMore = ref(true); // 标记是否还有更多数据可加载\nconst searchKeyword = ref(''); // 搜索关键词\nconst isFullPlaylistLoaded = ref(false); // 标记完整播放列表是否已加载完成\n\n// 计算总数\nconst total = computed(() => {\n  if (props.listInfo?.trackIds) {\n    return props.listInfo.trackIds.length;\n  }\n  return props.songList.length;\n});\n\nconst getCoverImgUrl = computed(() => {\n  if (props.listInfo?.coverImgUrl) {\n    return props.listInfo.coverImgUrl;\n  }\n\n  const song = props.songList[0];\n  if (song?.picUrl) {\n    return song.picUrl;\n  }\n  if (song?.al?.picUrl) {\n    return song.al.picUrl;\n  }\n  if (song?.album?.picUrl) {\n    return song.album.picUrl;\n  }\n  return '';\n});\n\n// 过滤歌曲列表\nconst filteredSongs = computed(() => {\n  if (!searchKeyword.value) {\n    return displayedSongs.value;\n  }\n\n  const keyword = searchKeyword.value.toLowerCase().trim();\n  return displayedSongs.value.filter((song) => {\n    const songName = song.name?.toLowerCase() || '';\n    const albumName = song.al?.name?.toLowerCase() || '';\n    const artists = song.ar || song.artists || [];\n\n    // 原始文本匹配\n    const nameMatch = songName.includes(keyword);\n    const albumMatch = albumName.includes(keyword);\n    const artistsMatch = artists.some((artist: any) => {\n      return artist.name?.toLowerCase().includes(keyword);\n    });\n\n    // 拼音匹配\n    const namePinyinMatch = song.name && PinyinMatch.match(song.name, keyword);\n    const albumPinyinMatch = song.al?.name && PinyinMatch.match(song.al.name, keyword);\n    const artistsPinyinMatch = artists.some((artist: any) => {\n      return artist.name && PinyinMatch.match(artist.name, keyword);\n    });\n\n    return (\n      nameMatch ||\n      albumMatch ||\n      artistsMatch ||\n      namePinyinMatch ||\n      albumPinyinMatch ||\n      artistsPinyinMatch\n    );\n  });\n});\n\n// 格式化歌曲数据\nconst formatSong = (item: any) => {\n  if (!item) {\n    return null;\n  }\n  return {\n    ...item,\n    picUrl: item.al?.picUrl || item.picUrl,\n    song: {\n      artists: item.ar || item.artists,\n      name: item.al?.name || item.name,\n      id: item.al?.id || item.id\n    }\n  };\n};\n\n/**\n * 加载歌曲数据的核心函数\n * @param ids 要加载的歌曲ID数组\n * @param appendToList 是否将加载的歌曲追加到现有列表\n * @param updateComplete 是否更新完整播放列表\n */\nconst loadSongs = async (ids: number[], appendToList = true, updateComplete = false) => {\n  if (ids.length === 0) return [];\n\n  try {\n    console.log(`请求歌曲详情，ID数量: ${ids.length}`);\n    const { data } = await getMusicDetail(ids);\n\n    if (data?.songs) {\n      console.log(`API返回歌曲数量: ${data.songs.length}`);\n\n      // 直接使用API返回的所有歌曲，不再过滤已加载的歌曲\n      // 因为当需要完整加载列表时，我们希望获取所有歌曲，即使ID可能重复\n      const { songs } = data;\n\n      // 只在非更新完整列表时执行过滤\n      let newSongs = songs;\n      if (!updateComplete) {\n        // 在普通加载模式下继续过滤已加载的歌曲，避免重复\n        newSongs = songs.filter((song: any) => !loadedIds.value.has(song.id));\n        console.log(`过滤已加载ID后剩余歌曲数量: ${newSongs.length}`);\n      }\n\n      // 更新已加载ID集合\n      songs.forEach((song: any) => {\n        loadedIds.value.add(song.id);\n      });\n\n      // 追加到显示列表 - 仅当appendToList=true时添加到displayedSongs\n      if (appendToList) {\n        displayedSongs.value.push(...newSongs);\n      }\n\n      // 更新完整播放列表 - 仅当updateComplete=true时添加到completePlaylist\n      if (updateComplete) {\n        completePlaylist.value.push(...songs);\n        console.log(`已添加到完整播放列表，当前完整列表长度: ${completePlaylist.value.length}`);\n      }\n\n      return updateComplete ? songs : newSongs;\n    }\n    console.log('API返回无歌曲数据');\n    return [];\n  } catch (error) {\n    console.error('加载歌曲失败:', error);\n  }\n\n  return [];\n};\n\n// 加载完整播放列表\nconst loadFullPlaylist = async () => {\n  if (isPlaylistLoading.value || isFullPlaylistLoaded.value) return;\n\n  isPlaylistLoading.value = true;\n  // 记录开始时间\n  const startTime = Date.now();\n  console.log(`开始加载完整播放列表，当前显示列表长度: ${displayedSongs.value.length}`);\n\n  try {\n    // 如果没有trackIds，直接使用当前歌曲列表并标记为已完成\n    if (!props.listInfo?.trackIds) {\n      isFullPlaylistLoaded.value = true;\n      console.log('无trackIds信息，使用当前列表作为完整列表');\n      return;\n    }\n\n    // 获取所有trackIds\n    const allIds = props.listInfo.trackIds.map((item) => item.id);\n    console.log(`歌单共有歌曲ID: ${allIds.length}首`);\n\n    // 重置completePlaylist和当前显示歌曲ID集合，保证不会重复添加歌曲\n    completePlaylist.value = [];\n\n    // 使用Set记录所有已加载的歌曲ID\n    const loadedSongIds = new Set<number>();\n\n    // 将当前显示列表中的歌曲和ID添加到集合中\n    displayedSongs.value.forEach((song) => {\n      loadedSongIds.add(song.id as number);\n      // 将已有歌曲添加到completePlaylist\n      completePlaylist.value.push(song);\n    });\n\n    console.log(\n      `已有显示歌曲: ${displayedSongs.value.length}首，已有ID数量: ${loadedSongIds.size}`\n    );\n\n    // 过滤出尚未加载的歌曲ID\n    const unloadedIds = allIds.filter((id) => !loadedSongIds.has(id));\n    console.log(`还需要加载的歌曲ID数量: ${unloadedIds.length}`);\n\n    if (unloadedIds.length === 0) {\n      console.log('所有歌曲已加载，无需再次加载');\n      isFullPlaylistLoaded.value = true;\n      hasMore.value = false;\n      return;\n    }\n\n    // 分批加载所有未加载的歌曲\n    const batchSize = 500; // 每批加载的歌曲数量\n\n    for (let i = 0; i < unloadedIds.length; i += batchSize) {\n      const batchIds = unloadedIds.slice(i, i + batchSize);\n      if (batchIds.length === 0) continue;\n\n      console.log(`请求第${Math.floor(i / batchSize) + 1}批歌曲，数量: ${batchIds.length}`);\n      // 关键修改: 设置appendToList为false，避免loadSongs直接添加到displayedSongs\n      const loadedBatch = await loadSongs(batchIds, false, false);\n\n      // 添加新加载的歌曲到displayedSongs\n      if (loadedBatch.length > 0) {\n        // 过滤掉已有的歌曲，确保不会重复添加\n        const newSongs = loadedBatch.filter((song) => !loadedSongIds.has(song.id as number));\n\n        // 更新已加载ID集合\n        newSongs.forEach((song) => {\n          loadedSongIds.add(song.id as number);\n        });\n\n        console.log(`新增${newSongs.length}首歌曲到显示列表`);\n\n        // 更新显示列表和完整播放列表\n        if (newSongs.length > 0) {\n          // 添加到显示列表\n          displayedSongs.value = [...displayedSongs.value, ...newSongs];\n\n          // 添加到完整播放列表\n          completePlaylist.value.push(...newSongs);\n\n          // 如果当前正在播放的列表与这个列表匹配，实时更新播放列表\n          const currentPlaylist = playerStore.playList;\n          if (currentPlaylist.length > 0 && currentPlaylist[0].id === displayedSongs.value[0]?.id) {\n            console.log('实时更新当前播放列表');\n            playerStore.setPlayList(displayedSongs.value.map(formatSong));\n          }\n        }\n      }\n\n      // 添加小延迟避免请求过于密集\n      if (i + batchSize < unloadedIds.length) {\n        await new Promise<void>((resolve) => {\n          setTimeout(() => resolve(), 100);\n        });\n      }\n    }\n\n    // 加载完成，更新状态\n    isFullPlaylistLoaded.value = true;\n    hasMore.value = false;\n\n    // 计算加载耗时\n    const endTime = Date.now();\n    const timeUsed = Math.round(((endTime - startTime) / 1000) * 100) / 100;\n\n    console.log(\n      `完整播放列表加载完成，共加载${displayedSongs.value.length}首歌曲，耗时${timeUsed}秒`\n    );\n    console.log(`歌单应有${allIds.length}首歌，实际加载${displayedSongs.value.length}首`);\n\n    // 检查加载的歌曲数量是否与预期相符\n    if (displayedSongs.value.length !== allIds.length) {\n      console.warn(\n        `警告: 加载的歌曲数量(${displayedSongs.value.length})与歌单应有数量(${allIds.length})不符`\n      );\n\n      // 如果数量不符，可能是API未返回所有歌曲，打印缺失的歌曲ID\n      if (displayedSongs.value.length < allIds.length) {\n        const loadedIds = new Set(displayedSongs.value.map((song) => song.id));\n        const missingIds = allIds.filter((id) => !loadedIds.has(id));\n        console.warn(`缺失的歌曲ID: ${missingIds.join(', ')}`);\n      }\n    }\n  } catch (error) {\n    console.error('加载完整播放列表失败:', error);\n  } finally {\n    isPlaylistLoading.value = false;\n  }\n};\n\n// 处理播放\nconst handlePlay = async () => {\n  // 当搜索状态下播放时，只播放过滤后的歌曲\n  if (searchKeyword.value) {\n    playerStore.setPlayList(filteredSongs.value.map(formatSong));\n    return;\n  }\n\n  // 如果完整播放列表已加载完成\n  if (isFullPlaylistLoaded.value && completePlaylist.value.length > 0) {\n    playerStore.setPlayList(completePlaylist.value.map(formatSong));\n    return;\n  }\n\n  // 如果完整播放列表未加载完成，先使用当前已加载的歌曲开始播放\n  playerStore.setPlayList(displayedSongs.value.map(formatSong));\n\n  // 如果完整播放列表正在加载中，不需要重新触发加载\n  if (isPlaylistLoading.value) {\n    return;\n  }\n\n  // 在后台继续加载完整播放列表（如果未加载完成）\n  if (!isFullPlaylistLoaded.value) {\n    console.log('播放时继续在后台加载完整列表');\n    loadFullPlaylist();\n  }\n};\n\nconst close = () => {\n  emit('update:show', false);\n};\n\n// 加载更多歌曲\nconst loadMoreSongs = async () => {\n  if (isFullPlaylistLoaded.value) {\n    hasMore.value = false;\n    return;\n  }\n\n  if (searchKeyword.value) {\n    return;\n  }\n\n  if (isLoadingMore.value || displayedSongs.value.length >= total.value) {\n    hasMore.value = false;\n    return;\n  }\n\n  isLoadingMore.value = true;\n\n  try {\n    const start = displayedSongs.value.length;\n    const end = Math.min(start + pageSize, total.value);\n\n    if (props.listInfo?.trackIds) {\n      const trackIdsToLoad = props.listInfo.trackIds\n        .slice(start, end)\n        .map((item) => item.id)\n        .filter((id) => !loadedIds.value.has(id));\n\n      if (trackIdsToLoad.length > 0) {\n        await loadSongs(trackIdsToLoad, true, false);\n      }\n    } else if (start < props.songList.length) {\n      const newSongs = props.songList.slice(start, end);\n      newSongs.forEach((song) => {\n        if (!loadedIds.value.has(song.id)) {\n          loadedIds.value.add(song.id);\n          displayedSongs.value.push(song);\n        }\n      });\n    }\n\n    hasMore.value = displayedSongs.value.length < total.value;\n  } catch (error) {\n    console.error('加载更多歌曲失败:', error);\n  } finally {\n    isLoadingMore.value = false;\n    loadingList.value = false;\n  }\n};\n\n// 处理虚拟列表滚动事件\nconst handleVirtualScroll = (e: any) => {\n  if (!e || !e.target) return;\n\n  const { scrollTop, scrollHeight, clientHeight } = e.target;\n  const threshold = 200;\n\n  if (\n    scrollHeight - scrollTop - clientHeight < threshold &&\n    !isLoadingMore.value &&\n    hasMore.value &&\n    !searchKeyword.value // 搜索状态下不触发加载更多\n  ) {\n    loadMoreSongs();\n  }\n};\n\n// 重置列表状态\nconst resetListState = () => {\n  page.value = 0;\n  loadedIds.value.clear();\n  displayedSongs.value = [];\n  completePlaylist.value = [];\n  hasMore.value = true;\n  loadingList.value = false;\n  searchKeyword.value = ''; // 重置搜索关键词\n  isFullPlaylistLoaded.value = false; // 重置完整播放列表状态\n};\n\n// 初始化歌曲列表\nconst initSongList = (songs: any[]) => {\n  if (songs.length > 0) {\n    displayedSongs.value = [...songs];\n    songs.forEach((song) => loadedIds.value.add(song.id));\n    page.value = Math.ceil(songs.length / pageSize);\n  }\n\n  // 检查是否还有更多数据可加载\n  hasMore.value = displayedSongs.value.length < total.value;\n};\n\nwatch(\n  () => props.listInfo,\n  (newListInfo) => {\n    if (newListInfo?.trackIds) {\n      loadFullPlaylist();\n    }\n  },\n  { deep: true }\n);\n// 修改 songList 监听器\nwatch(\n  () => props.songList,\n  (newSongs) => {\n    // 重置所有状态\n    resetListState();\n\n    // 初始化歌曲列表\n    initSongList(newSongs);\n\n    // 如果还有更多歌曲需要加载，且差距较小，立即加载\n    if (hasMore.value && props.listInfo?.trackIds) {\n      setTimeout(() => {\n        loadMoreSongs();\n      }, 300);\n    }\n  },\n  { immediate: true }\n);\n\n// 监听搜索关键词变化\nwatch(searchKeyword, () => {\n  // 当搜索关键词为空时，考虑加载更多歌曲\n  if (!searchKeyword.value && hasMore.value && displayedSongs.value.length < total.value) {\n    loadMoreSongs();\n  }\n});\n\n// 组件卸载时清理状态\nonUnmounted(() => {\n  isPlaylistLoading.value = false;\n});\n</script>\n\n<style scoped lang=\"scss\">\n.music {\n  &-title {\n    @apply text-xl font-bold text-gray-900 dark:text-white;\n  }\n\n  &-total {\n    @apply text-sm font-normal text-gray-500 dark:text-gray-400;\n  }\n\n  &-page {\n    @apply px-8 w-full h-full bg-light dark:bg-black bg-opacity-75 dark:bg-opacity-75 rounded-t-2xl;\n    backdrop-filter: blur(20px);\n  }\n\n  &-close {\n    @apply cursor-pointer text-gray-500 dark:text-white hover:text-gray-900 dark:hover:text-gray-300 flex gap-2 items-center transition;\n    .icon {\n      @apply text-3xl;\n    }\n  }\n\n  &-content {\n    @apply flex h-[calc(100%-60px)];\n  }\n\n  &-info {\n    @apply w-[25%] flex-shrink-0 pr-8 flex flex-col;\n\n    .music-cover {\n      @apply w-full aspect-square rounded-2xl overflow-hidden mb-4 min-h-[250px];\n      .cover-img {\n        @apply w-full h-full object-cover;\n      }\n    }\n\n    .creator-info {\n      @apply flex items-center mb-4;\n      .creator-name {\n        @apply ml-2 text-gray-700 dark:text-gray-300;\n      }\n    }\n\n    .music-desc {\n      @apply text-sm text-gray-600 dark:text-gray-400 leading-relaxed pr-4;\n    }\n  }\n\n  &-list {\n    @apply flex-grow min-h-0;\n    &-container {\n      @apply flex-grow min-h-0 flex flex-col relative;\n    }\n\n    &-content {\n      @apply min-h-[calc(80vh-60px)];\n    }\n  }\n}\n\n.search-container {\n  @apply max-w-md;\n\n  :deep(.n-input) {\n    @apply bg-light-200 dark:bg-dark-200;\n  }\n\n  .icon {\n    @apply text-gray-500 dark:text-gray-400;\n  }\n}\n\n.no-result {\n  @apply text-center py-8 text-gray-500 dark:text-gray-400;\n}\n\n/* 虚拟列表样式 */\n.song-virtual-list {\n  :deep(.n-virtual-list__scroll) {\n    scrollbar-width: thin;\n    &::-webkit-scrollbar {\n      width: 4px;\n    }\n    &::-webkit-scrollbar-thumb {\n      @apply bg-gray-400 dark:bg-gray-600 rounded;\n    }\n  }\n}\n\n.mobile {\n  .music-page {\n    @apply px-4;\n  }\n\n  .music-content {\n    @apply flex-col;\n    width: 100vw !important;\n  }\n\n  .music-info {\n    @apply w-full pr-0 mb-2 flex flex-row;\n\n    .music-cover {\n      @apply w-[100px] h-[100px] rounded-lg overflow-hidden mb-4;\n    }\n    .music-detail {\n      @apply flex-1 ml-4;\n    }\n  }\n\n  .music-title {\n    @apply text-base;\n  }\n\n  .search-container {\n    @apply max-w-[50%];\n  }\n\n  .song-virtual-list {\n    height: calc(80vh - 120px) !important;\n  }\n}\n\n.loading-more {\n  @apply text-center py-4 text-gray-500 dark:text-gray-400;\n}\n\n.double-item {\n  @apply mb-2 bg-light-100 bg-opacity-20 dark:bg-dark-100 dark:bg-opacity-20 rounded-3xl;\n}\n\n.mobile {\n  .music-info {\n    @apply hidden;\n  }\n  .music-list-content {\n    @apply pb-[100px];\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/MvPlayer.vue",
    "content": "<template>\n  <n-drawer :show=\"show\" height=\"100%\" placement=\"bottom\" :z-index=\"999999999\" :to=\"`#layout-main`\">\n    <div class=\"mv-detail\">\n      <div\n        ref=\"videoContainerRef\"\n        class=\"video-container\"\n        :class=\"{ 'cursor-hidden': !showCursor }\"\n      >\n        <video\n          ref=\"videoRef\"\n          :src=\"mvUrl\"\n          class=\"video-player\"\n          @ended=\"handleEnded\"\n          @timeupdate=\"handleTimeUpdate\"\n          @loadedmetadata=\"handleLoadedMetadata\"\n          @play=\"isPlaying = true\"\n          @pause=\"isPlaying = false\"\n          @click=\"togglePlay\"\n        ></video>\n\n        <div v-if=\"autoPlayBlocked\" class=\"play-hint\" @click=\"togglePlay\">\n          <n-button quaternary circle size=\"large\">\n            <template #icon>\n              <n-icon size=\"48\">\n                <i class=\"ri-play-circle-line\"></i>\n              </n-icon>\n            </template>\n          </n-button>\n        </div>\n\n        <div class=\"custom-controls\" :class=\"{ 'controls-hidden': !showControls }\">\n          <div class=\"progress-bar custom-slider\">\n            <n-slider\n              v-model:value=\"progress\"\n              :min=\"0\"\n              :max=\"100\"\n              :tooltip=\"false\"\n              :step=\"0.1\"\n              @update:value=\"handleProgressChange\"\n            >\n            </n-slider>\n          </div>\n\n          <div class=\"controls-main\">\n            <div class=\"left-controls\">\n              <n-tooltip v-if=\"!props.noList\" placement=\"top\">\n                <template #trigger>\n                  <n-button quaternary circle @click=\"handlePrev\">\n                    <template #icon>\n                      <n-icon size=\"24\">\n                        <n-spin v-if=\"prevLoading\" size=\"small\" />\n                        <i v-else class=\"ri-skip-back-line\"></i>\n                      </n-icon>\n                    </template>\n                  </n-button>\n                </template>\n                {{ t('player.previous') }}\n              </n-tooltip>\n\n              <n-tooltip placement=\"top\">\n                <template #trigger>\n                  <n-button quaternary circle @click=\"togglePlay\">\n                    <template #icon>\n                      <n-icon size=\"24\">\n                        <n-spin v-if=\"playLoading\" size=\"small\" />\n                        <i v-else :class=\"isPlaying ? 'ri-pause-line' : 'ri-play-line'\"></i>\n                      </n-icon>\n                    </template>\n                  </n-button>\n                </template>\n                {{ isPlaying ? t('player.pause') : t('player.play') }}\n              </n-tooltip>\n\n              <n-tooltip v-if=\"!props.noList\" placement=\"top\">\n                <template #trigger>\n                  <n-button quaternary circle @click=\"handleNext\">\n                    <template #icon>\n                      <n-icon size=\"24\">\n                        <n-spin v-if=\"nextLoading\" size=\"small\" />\n                        <i v-else class=\"ri-skip-forward-line\"></i>\n                      </n-icon>\n                    </template>\n                  </n-button>\n                </template>\n                {{ t('player.next') }}\n              </n-tooltip>\n\n              <div class=\"time-display\">\n                {{ formatTime(currentTime) }} / {{ formatTime(duration) }}\n              </div>\n            </div>\n\n            <div class=\"right-controls\">\n              <div v-if=\"!isMobile\" class=\"volume-control custom-slider\">\n                <n-tooltip placement=\"top\">\n                  <template #trigger>\n                    <n-button quaternary circle @click=\"toggleMute\">\n                      <template #icon>\n                        <n-icon size=\"24\">\n                          <i\n                            :class=\"volume === 0 ? 'ri-volume-mute-line' : 'ri-volume-up-line'\"\n                          ></i>\n                        </n-icon>\n                      </template>\n                    </n-button>\n                  </template>\n                  {{ volume === 0 ? t('player.unmute') : t('player.mute') }}\n                </n-tooltip>\n                <n-slider\n                  v-model:value=\"volume\"\n                  :min=\"0\"\n                  :max=\"100\"\n                  :tooltip=\"false\"\n                  class=\"volume-slider\"\n                />\n              </div>\n\n              <n-tooltip v-if=\"!props.noList\" placement=\"top\">\n                <template #trigger>\n                  <n-button quaternary circle class=\"play-mode-btn\" @click=\"togglePlayMode\">\n                    <template #icon>\n                      <n-icon size=\"24\">\n                        <i\n                          :class=\"\n                            playMode === 'single' ? 'ri-repeat-one-line' : 'ri-play-list-line'\n                          \"\n                        ></i>\n                      </n-icon>\n                    </template>\n                  </n-button>\n                </template>\n                {{\n                  playMode === 'single' ? t('player.modeHint.single') : t('player.modeHint.list')\n                }}\n              </n-tooltip>\n\n              <n-tooltip placement=\"top\">\n                <template #trigger>\n                  <n-button quaternary circle @click=\"toggleFullscreen\">\n                    <template #icon>\n                      <n-icon size=\"24\">\n                        <i\n                          :class=\"isFullscreen ? 'ri-fullscreen-exit-line' : 'ri-fullscreen-line'\"\n                        ></i>\n                      </n-icon>\n                    </template>\n                  </n-button>\n                </template>\n                {{ isFullscreen ? t('player.fullscreen.exit') : t('player.fullscreen.enter') }}\n              </n-tooltip>\n\n              <n-tooltip placement=\"top\">\n                <template #trigger>\n                  <n-button quaternary circle @click=\"handleClose\">\n                    <template #icon>\n                      <n-icon size=\"24\">\n                        <i class=\"ri-close-line\"></i>\n                      </n-icon>\n                    </template>\n                  </n-button>\n                </template>\n                {{ t('player.close') }}\n              </n-tooltip>\n            </div>\n          </div>\n        </div>\n\n        <!-- 添加模式切换提示 -->\n        <transition name=\"fade\">\n          <div v-if=\"showModeHint\" class=\"mode-hint\">\n            <n-icon size=\"48\" class=\"mode-icon\">\n              <i :class=\"playMode === 'single' ? 'ri-repeat-one-line' : 'ri-play-list-line'\"></i>\n            </n-icon>\n            <div class=\"mode-text\">\n              {{ playMode === 'single' ? t('player.modeHint.single') : t('player.modeHint.list') }}\n            </div>\n          </div>\n        </transition>\n      </div>\n\n      <div class=\"mv-detail-title\" :class=\"{ 'title-hidden': !showControls }\">\n        <div class=\"title\">\n          <n-ellipsis>{{ currentMv?.name }}</n-ellipsis>\n        </div>\n      </div>\n    </div>\n  </n-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport { NButton, NIcon, NSlider, NTooltip } from 'naive-ui';\nimport { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { getMvUrl } from '@/api/mv';\nimport { IMvItem } from '@/types/mv';\n\nconst { t } = useI18n();\ntype PlayMode = 'single' | 'auto';\nconst PLAY_MODE = {\n  Single: 'single' as PlayMode,\n  Auto: 'auto' as PlayMode\n} as const;\n\nconst props = withDefaults(\n  defineProps<{\n    show: boolean;\n    currentMv?: IMvItem;\n    noList?: boolean;\n  }>(),\n  {\n    show: false,\n    currentMv: undefined,\n    noList: false\n  }\n);\n\nconst emit = defineEmits<{\n  (e: 'update:show', value: boolean): void;\n  (e: 'next', loading: (value: boolean) => void): void;\n  (e: 'prev', loading: (value: boolean) => void): void;\n}>();\n\nconst mvUrl = ref<string>();\nconst playMode = ref<PlayMode>(PLAY_MODE.Auto);\n\nconst videoRef = ref<HTMLVideoElement>();\nconst isPlaying = ref(false);\nconst currentTime = ref(0);\nconst duration = ref(0);\nconst progress = ref(0);\nconst bufferedProgress = ref(0);\nconst volume = ref(100);\nconst showControls = ref(true);\nlet controlsTimer: NodeJS.Timeout | null = null;\n\nconst formatTime = (seconds: number) => {\n  const minutes = Math.floor(seconds / 60);\n  const remainingSeconds = Math.floor(seconds % 60);\n  return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;\n};\n\nconst togglePlay = () => {\n  if (!videoRef.value) return;\n  if (isPlaying.value) {\n    videoRef.value.pause();\n  } else {\n    videoRef.value.play();\n  }\n  resetCursorTimer();\n};\n\nconst toggleMute = () => {\n  if (!videoRef.value) return;\n  if (volume.value === 0) {\n    volume.value = 100;\n  } else {\n    volume.value = 0;\n  }\n};\n\nwatch(volume, (newVolume) => {\n  if (videoRef.value) {\n    videoRef.value.volume = newVolume / 100;\n  }\n});\n\nconst handleProgressChange = (value: number) => {\n  if (!videoRef.value || !duration.value) return;\n  const newTime = (value / 100) * duration.value;\n  videoRef.value.currentTime = newTime;\n};\n\nconst handleTimeUpdate = () => {\n  if (!videoRef.value) return;\n  currentTime.value = videoRef.value.currentTime;\n  if (!isDragging.value) {\n    progress.value = (currentTime.value / duration.value) * 100;\n  }\n\n  if (videoRef.value.buffered.length > 0) {\n    bufferedProgress.value = (videoRef.value.buffered.end(0) / duration.value) * 100;\n  }\n};\n\nconst handleLoadedMetadata = () => {\n  if (!videoRef.value) return;\n  duration.value = videoRef.value.duration;\n};\n\nconst resetControlsTimer = () => {\n  if (controlsTimer) {\n    clearTimeout(controlsTimer);\n  }\n  showControls.value = true;\n  controlsTimer = setTimeout(() => {\n    if (isPlaying.value) {\n      showControls.value = false;\n    }\n  }, 3000);\n};\n\nconst handleMouseMove = () => {\n  resetControlsTimer();\n  resetCursorTimer();\n};\n\nonMounted(() => {\n  document.addEventListener('mousemove', handleMouseMove);\n});\n\nonUnmounted(() => {\n  document.removeEventListener('mousemove', handleMouseMove);\n  if (controlsTimer) {\n    clearTimeout(controlsTimer);\n  }\n  if (cursorTimer) {\n    clearTimeout(cursorTimer);\n  }\n});\n\n// 监听 currentMv 的变化\nwatch(\n  () => props.currentMv,\n  async (newMv) => {\n    if (newMv) {\n      await loadMvUrl(newMv);\n    }\n  }\n);\n\nconst autoPlayBlocked = ref(false);\n\nconst playLoading = ref(false);\n\nconst loadMvUrl = async (mv: IMvItem) => {\n  playLoading.value = true;\n  autoPlayBlocked.value = false;\n  try {\n    const res = await getMvUrl(mv.id);\n    mvUrl.value = res.data.data.url;\n    await nextTick();\n    if (videoRef.value) {\n      try {\n        await videoRef.value.play();\n      } catch (error) {\n        console.warn('自动播放失败，可能需要用户交互:', error);\n        autoPlayBlocked.value = true;\n      }\n    }\n  } catch (error) {\n    console.error('加载MV地址失败:', error);\n  } finally {\n    playLoading.value = false;\n  }\n};\n\nconst handleClose = () => {\n  emit('update:show', false);\n};\n\nconst handleEnded = () => {\n  if (playMode.value === PLAY_MODE.Single) {\n    // 单曲循环模式，重新加载当前MV\n    if (props.currentMv) {\n      loadMvUrl(props.currentMv);\n    }\n  } else {\n    // 自动播放模式，触发下一个\n    emit('next', (value: boolean) => {\n      nextLoading.value = value;\n    });\n  }\n};\n\nconst togglePlayMode = () => {\n  playMode.value = playMode.value === PLAY_MODE.Auto ? PLAY_MODE.Single : PLAY_MODE.Auto;\n  showModeHint.value = true;\n  setTimeout(() => {\n    showModeHint.value = false;\n  }, 1500);\n};\n\nconst isDragging = ref(false);\n\n// 添加全屏相关的状态和方法\nconst videoContainerRef = ref<HTMLElement>();\nconst isFullscreen = ref(false);\n\n// 检查是否支持全屏API\nconst checkFullscreenAPI = () => {\n  const doc = document as any;\n  return {\n    requestFullscreen:\n      videoContainerRef.value?.requestFullscreen ||\n      (videoContainerRef.value as any)?.webkitRequestFullscreen ||\n      (videoContainerRef.value as any)?.mozRequestFullScreen ||\n      (videoContainerRef.value as any)?.msRequestFullscreen,\n    exitFullscreen:\n      doc.exitFullscreen ||\n      doc.webkitExitFullscreen ||\n      doc.mozCancelFullScreen ||\n      doc.msExitFullscreen,\n    fullscreenElement:\n      doc.fullscreenElement ||\n      doc.webkitFullscreenElement ||\n      doc.mozFullScreenElement ||\n      doc.msFullscreenElement,\n    fullscreenEnabled:\n      doc.fullscreenEnabled ||\n      doc.webkitFullscreenEnabled ||\n      doc.mozFullScreenEnabled ||\n      doc.msFullscreenEnabled\n  };\n};\n\n// 修改切换全屏状态的方法\nconst toggleFullscreen = async () => {\n  const api = checkFullscreenAPI();\n\n  if (!api.fullscreenEnabled) {\n    console.warn('全屏API不可用');\n    return;\n  }\n\n  try {\n    if (!api.fullscreenElement) {\n      await videoContainerRef.value?.requestFullscreen();\n      isFullscreen.value = true;\n    } else {\n      await document.exitFullscreen();\n      isFullscreen.value = false;\n    }\n  } catch (error) {\n    console.error('切换全屏失败:', error);\n  }\n};\n\n// 监听全屏状态变化\nconst handleFullscreenChange = () => {\n  const api = checkFullscreenAPI();\n  isFullscreen.value = !!api.fullscreenElement;\n};\n\n// 在组件挂载时添加全屏变化监听\nonMounted(() => {\n  document.addEventListener('fullscreenchange', handleFullscreenChange);\n  document.addEventListener('webkitfullscreenchange', handleFullscreenChange);\n  document.addEventListener('mozfullscreenchange', handleFullscreenChange);\n  document.addEventListener('MSFullscreenChange', handleFullscreenChange);\n});\n\n// 在组件卸载时移除监听\nonUnmounted(() => {\n  document.removeEventListener('fullscreenchange', handleFullscreenChange);\n  document.removeEventListener('webkitfullscreenchange', handleFullscreenChange);\n  document.removeEventListener('mozfullscreenchange', handleFullscreenChange);\n  document.removeEventListener('MSFullscreenChange', handleFullscreenChange);\n});\n\n// 添加键盘快捷键支持\nconst handleKeyPress = (e: KeyboardEvent) => {\n  if (e.key === 'f' || e.key === 'F') {\n    toggleFullscreen();\n  }\n};\n\nonMounted(() => {\n  // 添加到现有的 onMounted 中\n  document.addEventListener('keydown', handleKeyPress);\n});\n\nonUnmounted(() => {\n  // 添加到现有的 onUnmounted 中\n  document.removeEventListener('keydown', handleKeyPress);\n});\n\n// 添加提示状态\nconst showModeHint = ref(false);\n\n// 添加加载状态\nconst prevLoading = ref(false);\nconst nextLoading = ref(false);\n\n// 添加处理函数\nconst handlePrev = () => {\n  prevLoading.value = true;\n  emit('prev', (value: boolean) => {\n    prevLoading.value = value;\n  });\n};\n\nconst handleNext = () => {\n  nextLoading.value = true;\n  emit('next', (value: boolean) => {\n    nextLoading.value = value;\n  });\n};\n\n// 添加鼠标显示状态\nconst showCursor = ref(true);\nlet cursorTimer: NodeJS.Timeout | null = null;\n\n// 添加重置鼠标计时器的函数\nconst resetCursorTimer = () => {\n  if (cursorTimer) {\n    clearTimeout(cursorTimer);\n  }\n  showCursor.value = true;\n  if (isPlaying.value && !showControls.value) {\n    cursorTimer = setTimeout(() => {\n      showCursor.value = false;\n    }, 3000);\n  }\n};\n\n// 监听播放状态变化\nwatch(isPlaying, (newValue) => {\n  if (!newValue) {\n    showCursor.value = true;\n    if (cursorTimer) {\n      clearTimeout(cursorTimer);\n    }\n  } else {\n    resetCursorTimer();\n  }\n});\n\n// 添加控制栏状态监听\nwatch(showControls, (newValue) => {\n  if (newValue) {\n    showCursor.value = true;\n    if (cursorTimer) {\n      clearTimeout(cursorTimer);\n    }\n  } else {\n    resetCursorTimer();\n  }\n});\n\nconst isMobile = computed(() => false); // TODO: 从 settings store 获取\n</script>\n\n<style scoped lang=\"scss\">\n.mv-detail {\n  @apply h-full bg-light dark:bg-black;\n\n  &-title {\n    @apply fixed top-0 left-0 right-0 p-4 z-10 transition-opacity duration-300;\n    background: linear-gradient(to bottom, rgba(0, 0, 0, 0.7), transparent);\n\n    .title {\n      @apply text-white text-lg font-bold;\n    }\n  }\n}\n\n.video-container {\n  @apply h-full w-full relative;\n\n  .video-player {\n    @apply h-full w-full object-contain bg-black;\n  }\n\n  .play-hint {\n    @apply absolute inset-0 flex items-center justify-center bg-black bg-opacity-50;\n    .n-button {\n      @apply text-white hover:text-green-500 transition-colors;\n    }\n  }\n\n  .custom-controls {\n    @apply absolute bottom-0 left-0 right-0 p-4 transition-opacity duration-300;\n    background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);\n\n    .controls-main {\n      @apply flex justify-between items-center;\n\n      .left-controls,\n      .right-controls {\n        @apply flex items-center gap-2;\n\n        .n-button {\n          @apply text-white hover:text-green-500 transition-colors;\n        }\n\n        .time-display {\n          @apply text-white text-sm ml-4;\n        }\n      }\n    }\n  }\n}\n\n.mode-hint {\n  @apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 flex flex-col items-center;\n\n  .mode-icon {\n    @apply text-white mb-2;\n  }\n\n  .mode-text {\n    @apply text-white text-sm;\n  }\n}\n\n.custom-slider {\n  :deep(.n-slider) {\n    --n-rail-height: 4px;\n    --n-rail-color: theme('colors.gray.200');\n    --n-rail-color-dark: theme('colors.gray.700');\n    --n-fill-color: theme('colors.green.500');\n    --n-handle-size: 12px;\n    --n-handle-color: theme('colors.green.500');\n\n    &.n-slider--vertical {\n      height: 100%;\n\n      .n-slider-rail {\n        width: 4px;\n      }\n\n      &:hover {\n        .n-slider-rail {\n          width: 6px;\n        }\n\n        .n-slider-handle {\n          width: 14px;\n          height: 14px;\n        }\n      }\n    }\n\n    .n-slider-rail {\n      @apply overflow-hidden transition-all duration-200;\n      @apply bg-gray-500 dark:bg-dark-300 bg-opacity-10 !important;\n    }\n\n    .n-slider-handle {\n      @apply transition-all duration-200;\n      opacity: 0;\n    }\n\n    &:hover .n-slider-handle {\n      opacity: 1;\n    }\n  }\n}\n\n.progress-bar {\n  @apply mb-4;\n\n  .progress-rail {\n    @apply relative w-full h-1 bg-gray-600;\n\n    .progress-buffer {\n      @apply absolute top-0 left-0 h-full bg-gray-400;\n    }\n  }\n}\n\n.volume-control {\n  @apply flex items-center gap-2;\n\n  .volume-slider {\n    width: 100px;\n  }\n}\n\n.controls-hidden {\n  opacity: 0;\n  pointer-events: none;\n}\n\n.cursor-hidden {\n  cursor: none;\n}\n\n.title-hidden {\n  opacity: 0;\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/ShortcutToast.vue",
    "content": "<template>\n  <transition name=\"shortcut-toast\">\n    <div v-if=\"visible\" class=\"shortcut-toast\" :class=\"`shortcut-toast-${position}`\">\n      <div class=\"shortcut-toast-content\">\n        <div v-if=\"showIcon\" class=\"shortcut-toast-icon\">\n          <i :class=\"icon\"></i>\n        </div>\n        <div class=\"shortcut-toast-text\">{{ text }}</div>\n      </div>\n    </div>\n  </transition>\n</template>\n\n<script lang=\"ts\" setup>\nimport { onBeforeUnmount, ref } from 'vue';\n\ndefineProps({\n  position: {\n    type: String,\n    default: 'center',\n    validator: (val: string) => ['top', 'center', 'bottom'].includes(val)\n  },\n  showIcon: {\n    type: Boolean,\n    default: true\n  }\n});\n\nconst visible = ref(false);\nconst text = ref('');\nconst icon = ref('');\nlet timer: NodeJS.Timeout | null = null;\n\nconst show = (message: string, iconName = '') => {\n  if (timer) {\n    clearTimeout(timer);\n  }\n\n  text.value = message;\n  icon.value = iconName;\n  visible.value = true;\n\n  timer = setTimeout(() => {\n    visible.value = false;\n    // 在动画结束后触发销毁事件\n    setTimeout(() => {\n      emit('destroy');\n    }, 300);\n  }, 1500);\n};\n\n// 清理定时器\nonBeforeUnmount(() => {\n  if (timer) {\n    clearTimeout(timer);\n  }\n});\n\nconst emit = defineEmits(['destroy']);\n\n// 暴露方法给父组件\ndefineExpose({\n  show\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.shortcut-toast {\n  @apply fixed left-1/2 z-[9999];\n  @apply flex items-center justify-center;\n\n  // 位置变体\n  &-center {\n    @apply top-1/2 -translate-y-1/2;\n    .shortcut-toast-content {\n      @apply p-2;\n    }\n  }\n\n  &-top {\n    @apply top-20;\n  }\n\n  &-bottom {\n    @apply bottom-40;\n  }\n\n  // 水平居中\n  @apply -translate-x-1/2;\n\n  &-content {\n    @apply flex flex-col items-center gap-2 p-4 rounded-lg;\n    @apply bg-light-200 bg-opacity-70 dark:bg-dark-200 dark:bg-opacity-90;\n    @apply text-dark-100 dark:text-light-100;\n    @apply shadow-lg backdrop-blur-sm;\n    min-width: 120px;\n  }\n\n  &-icon {\n    @apply text-3xl;\n  }\n\n  &-text {\n    @apply text-sm font-medium text-center;\n  }\n}\n\n.shortcut-toast-enter-active,\n.shortcut-toast-leave-active {\n  @apply transition-all duration-300;\n}\n\n.shortcut-toast-enter-from,\n.shortcut-toast-leave-to {\n  @apply opacity-0 scale-90;\n}\n\n.shortcut-toast-enter-to,\n.shortcut-toast-leave-from {\n  @apply opacity-100 scale-100;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/TrafficWarningDrawer.vue",
    "content": "<template>\n  <n-drawer\n    v-model:show=\"showDrawer\"\n    :width=\"isMobile ? '100%' : '800px'\"\n    :height=\"isMobile ? '100%' : '100%'\"\n    :placement=\"isMobile ? 'bottom' : 'right'\"\n    @after-leave=\"handleDrawerClose\"\n    :z-index=\"999999999\"\n    :mask-closable=\"false\"\n  >\n    <n-drawer-content\n      title=\"欢迎使用 AlgerMusicPlayer\"\n      closable\n      :native-scrollbar=\"false\"\n      class=\"mac-style-drawer\"\n    >\n      <div class=\"drawer-container\">\n        <div class=\"warning-content\">\n          <div class=\"warning-message\">\n            <h3>获取完整体验</h3>\n            <p class=\"platform-support\">\n              <span> <i class=\"ri-window-line mr-1\"></i>Windows 10+ </span>\n              <span> <i class=\"ri-apple-line mr-1\"></i>macOS </span>\n              <span> <i class=\"ri-ubuntu-line mr-1\"></i>Linux </span>\n              <span> <i class=\"ri-android-line mr-1\"></i>Android </span>\n            </p>\n            <p class=\"description\">\n              下载桌面应用以获得最佳音乐体验，包含完整功能与更高音质。\n              目前无iOS版本，请使用安卓应用或网页版。\n            </p>\n          </div>\n\n          <div class=\"action-links\">\n            <a\n              href=\"https://mp.weixin.qq.com/s/9pr1XQB36gShM_-TG2LBdg\"\n              target=\"_blank\"\n              class=\"doc-link\"\n            >\n              <i class=\"ri-file-text-line mr-1\"></i> 查看使用文档\n            </a>\n            <a href=\"http://donate.alger.fun/download\" target=\"_blank\" class=\"download-link\">\n              <i class=\"ri-download-2-line mr-1\"></i> 立即下载\n            </a>\n          </div>\n\n          <div class=\"qrcode-section\">\n            <img class=\"qrcode\" src=\"@/assets/gzh.png\" alt=\"公众号\" />\n            <p>关注公众号获取最新版本与更新信息</p>\n          </div>\n\n          <div class=\"support-section\">\n            <h4>支持项目</h4>\n            <p class=\"support-desc\">您的支持是我们持续改进的动力</p>\n            <div class=\"payment-options\">\n              <div class=\"payment-option\">\n                <div class=\"payment-icon wechat\">\n                  <img src=\"@/assets/wechat.png\" alt=\"微信支付\" />\n                </div>\n                <span>微信支付</span>\n              </div>\n              <div class=\"payment-option\">\n                <div class=\"payment-icon alipay\">\n                  <img src=\"@/assets/alipay.png\" alt=\"支付宝\" />\n                </div>\n                <span>支付宝</span>\n              </div>\n            </div>\n          </div>\n\n          <div class=\"drawer-actions\">\n            <n-button secondary class=\"action-button\" @click=\"markAsDonated\">已支持</n-button>\n            <n-button type=\"primary\" class=\"action-button primary\" @click=\"remindLater\"\n              >稍后提醒</n-button\n            >\n          </div>\n        </div>\n      </div>\n    </n-drawer-content>\n  </n-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, ref } from 'vue';\n\nimport { isMobile } from '@/utils';\n\n// 控制抽屉显示状态\nconst showDrawer = ref(false);\n\n// 处理抽屉关闭后的操作\nconst handleDrawerClose = () => {\n  // 抽屉关闭后的逻辑\n};\n\n// 一天后提醒\nconst remindLater = () => {\n  const now = new Date();\n  localStorage.setItem('trafficDonated4RemindLater', now.toISOString());\n  showDrawer.value = false;\n};\n\n// 标记为已捐赠（永久不再提示）\nconst markAsDonated = () => {\n  localStorage.setItem('trafficDonated4Never', '1');\n  showDrawer.value = false;\n};\n// 组件挂载时检查是否需要显示\nonMounted(() => {\n  // 优先判断是否永久不再提示\n  if (localStorage.getItem('trafficDonated4Never')) return;\n\n  // 判断一天后提醒\n  const remindLaterTime = localStorage.getItem('trafficDonated4RemindLater');\n  if (remindLaterTime) {\n    const lastRemind = new Date(remindLaterTime);\n    const now = new Date();\n    const hoursDiff = (now.getTime() - lastRemind.getTime()) / (1000 * 60 * 60);\n    if (hoursDiff < 24) return;\n  }\n\n  // 延迟20秒显示\n  setTimeout(() => {\n    showDrawer.value = true;\n  }, 20000);\n});\n</script>\n\n<style scoped lang=\"scss\">\n.traffic-warning-trigger {\n  display: inline-block;\n\n  .mac-style-button {\n    background-color: rgba(0, 0, 0, 0.05);\n    color: #333;\n    transition: all 0.2s ease;\n\n    &:hover {\n      background-color: rgba(0, 0, 0, 0.1);\n    }\n  }\n}\n\n.mac-style-drawer {\n  border-radius: 10px 0 0 10px;\n  overflow: hidden;\n  position: relative;\n}\n\n.drawer-container {\n  padding: 20px;\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n}\n\n.warning-content {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 24px;\n}\n\n.app-icon {\n  width: 100px;\n  height: 100px;\n  margin-bottom: 12px;\n\n  img {\n    width: 100%;\n    height: 100%;\n    object-fit: contain;\n    border-radius: 14px;\n  }\n}\n\n.warning-message {\n  text-align: center;\n  max-width: 520px;\n\n  h3 {\n    font-size: 28px;\n    font-weight: 600;\n    margin-bottom: 18px;\n    color: #333;\n  }\n\n  .platform-support {\n    display: flex;\n    justify-content: center;\n    gap: 20px;\n    flex-wrap: wrap;\n    margin-bottom: 16px;\n\n    span {\n      display: flex;\n      align-items: center;\n      font-size: 16px;\n      color: #444;\n    }\n  }\n\n  .description {\n    font-size: 16px;\n    line-height: 1.6;\n    color: #444;\n    margin: 0 auto;\n  }\n}\n\n.action-links {\n  display: flex;\n  gap: 20px;\n  justify-content: center;\n  flex-wrap: wrap;\n  margin: 6px 0;\n\n  a {\n    display: inline-flex;\n    align-items: center;\n    padding: 10px 20px;\n    border-radius: 8px;\n    font-size: 16px;\n    text-decoration: none;\n    transition: all 0.2s ease;\n\n    &.doc-link {\n      color: #555;\n      background-color: rgba(0, 0, 0, 0.05);\n\n      &:hover {\n        background-color: rgba(0, 0, 0, 0.1);\n      }\n    }\n\n    &.download-link {\n      color: #fff;\n      background-color: #007aff;\n\n      &:hover {\n        background-color: #0062cc;\n      }\n    }\n  }\n}\n\n.qrcode-section {\n  text-align: center;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 10px;\n\n  .qrcode {\n    width: 180px;\n    height: 180px;\n    border-radius: 10px;\n    padding: 10px;\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);\n    background: white;\n  }\n\n  p {\n    margin-top: 14px;\n    font-size: 15px;\n    color: #0062cc;\n  }\n}\n\n.support-section {\n  width: 100%;\n  text-align: center;\n\n  h4 {\n    font-size: 22px;\n    font-weight: 600;\n    color: #333;\n    margin-bottom: 8px;\n  }\n\n  .support-desc {\n    font-size: 15px;\n    color: #555;\n    margin-bottom: 20px;\n  }\n}\n\n.payment-options {\n  display: flex;\n  justify-content: center;\n  gap: 100px;\n  flex-wrap: wrap;\n  padding-bottom: 100px;\n}\n\n.payment-option {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 10px;\n\n  .payment-icon {\n    width: 220px;\n    height: 220px;\n    border-radius: 12px;\n    overflow: hidden;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);\n\n    img {\n      width: 100%;\n      height: 100%;\n      object-fit: cover;\n    }\n  }\n\n  span {\n    font-size: 15px;\n    color: #444;\n  }\n}\n\n.drawer-actions {\n  display: flex;\n  justify-content: center;\n  gap: 16px;\n  margin-top: 30px;\n  width: 100%;\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  padding: 10px;\n  background-color: #fff;\n  z-index: 999999999;\n\n  .action-button {\n    min-width: 110px;\n    border-radius: 8px;\n    font-size: 16px;\n    padding: 8px 16px;\n\n    &.primary {\n      background-color: #007aff;\n      color: white;\n\n      &:hover {\n        background-color: #0062cc;\n      }\n    }\n  }\n}\n\n@media (max-width: 768px) {\n  .warning-message {\n    h3 {\n      font-size: 20px;\n    }\n\n    .platform-support {\n      gap: 12px;\n    }\n\n    .description {\n      font-size: 13px;\n    }\n  }\n\n  .app-icon {\n    width: 64px;\n    height: 64px;\n  }\n\n  .qrcode-section {\n    .qrcode {\n      width: 140px;\n      height: 140px;\n    }\n  }\n\n  .payment-option {\n    .payment-icon {\n      width: 190px;\n      height: 190px;\n    }\n  }\n\n  .drawer-actions {\n    flex-wrap: wrap;\n    position: fixed;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    padding: 10px;\n    background-color: #fff;\n    z-index: 999999999;\n\n    .action-button {\n      flex: 1 0 auto;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/AlbumItem.vue",
    "content": "<template>\n  <div class=\"album-item\" @click=\"handleClick\">\n    <n-image\n      :src=\"getImgUrl(item.picUrl || '', '100y100')\"\n      class=\"album-item-img\"\n      lazy\n      preview-disabled\n    />\n    <div class=\"album-item-info\">\n      <div class=\"album-item-name\">\n        <n-ellipsis :line-clamp=\"1\">{{ item.name }}</n-ellipsis>\n      </div>\n      <div class=\"album-item-desc\">\n        {{ getDescription() }}\n      </div>\n    </div>\n    <div v-if=\"showCount && item.count\" class=\"album-item-count\">\n      {{ item.count }}\n    </div>\n    <div v-if=\"showDelete\" class=\"album-item-delete\" @click.stop=\"handleDelete\">\n      <i class=\"iconfont icon-close\"></i>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n';\n\nimport type { AlbumHistoryItem } from '@/hooks/AlbumHistoryHook';\nimport { getImgUrl } from '@/utils';\n\ninterface Props {\n  item: AlbumHistoryItem;\n  showCount?: boolean;\n  showDelete?: boolean;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  showCount: false,\n  showDelete: false\n});\n\nconst emit = defineEmits<{\n  click: [item: AlbumHistoryItem];\n  delete: [item: AlbumHistoryItem];\n}>();\n\nconst { t } = useI18n();\n\nconst getDescription = () => {\n  const parts: string[] = [];\n\n  if (props.item.artist?.name) {\n    parts.push(props.item.artist.name);\n  }\n\n  if (props.item.size !== undefined) {\n    parts.push(t('user.album.songCount', { count: props.item.size }));\n  }\n\n  return parts.join(' · ') || t('history.noDescription');\n};\n\nconst handleClick = () => {\n  emit('click', props.item);\n};\n\nconst handleDelete = () => {\n  emit('delete', props.item);\n};\n</script>\n\n<style scoped lang=\"scss\">\n.album-item {\n  @apply flex items-center px-2 py-2 rounded-xl cursor-pointer;\n  @apply transition-all duration-200;\n  @apply bg-light-100 dark:bg-dark-100;\n  @apply hover:bg-light-200 dark:hover:bg-dark-200;\n  @apply mb-2;\n\n  &-img {\n    @apply flex items-center justify-center rounded-xl;\n    @apply w-[60px] h-[60px] flex-shrink-0;\n    @apply bg-light-300 dark:bg-dark-300;\n  }\n\n  &-info {\n    @apply ml-3 flex-1 min-w-0;\n  }\n\n  &-name {\n    @apply text-gray-900 dark:text-white text-base mb-1;\n  }\n\n  &-desc {\n    @apply text-sm text-gray-500 dark:text-gray-400;\n  }\n\n  &-count {\n    @apply px-4 text-lg text-center min-w-[60px];\n    @apply text-gray-600 dark:text-gray-400;\n  }\n\n  &-delete {\n    @apply cursor-pointer rounded-full border-2 w-8 h-8 flex justify-center items-center;\n    @apply border-gray-400 dark:border-gray-600;\n    @apply text-gray-600 dark:text-gray-400;\n    @apply hover:border-red-500 hover:text-red-500;\n    @apply transition-all;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/ArtistDrawer.vue",
    "content": "<template>\n  <n-drawer\n    v-model:show=\"modelValue\"\n    :width=\"800\"\n    placement=\"right\"\n    :mask-closable=\"true\"\n    :z-index=\"9997\"\n  >\n    <div v-loading=\"loading\" class=\"artist-drawer\">\n      <div class=\"close-btn\">\n        <i class=\"ri-close-line\" @click=\"modelValue = false\"></i>\n      </div>\n\n      <!-- 歌手信息头部 -->\n      <div class=\"artist-header\">\n        <div class=\"artist-cover\">\n          <n-image\n            :src=\"getImgUrl(artistInfo?.avatar, '300y300')\"\n            class=\"w-48 h-48 rounded-2xl object-cover\"\n            preview-disabled\n          />\n        </div>\n        <div class=\"artist-info\">\n          <h1 class=\"artist-name\">{{ artistInfo?.name }}</h1>\n          <div v-if=\"artistInfo?.alias?.length\" class=\"artist-alias\">\n            {{ artistInfo.alias.join(' / ') }}\n          </div>\n          <div v-if=\"artistInfo?.briefDesc\" class=\"artist-desc\">\n            {{ artistInfo.briefDesc }}\n          </div>\n        </div>\n      </div>\n\n      <!-- 标签页切换 -->\n      <n-tabs v-model:value=\"activeTab\" class=\"flex-1\" type=\"line\" animated>\n        <n-tab-pane name=\"songs\" :tab=\"t('artist.hotSongs')\">\n          <div ref=\"songListRef\" class=\"songs-list\">\n            <n-scrollbar style=\"max-height: 61vh\" :size=\"5\" @scroll=\"handleSongScroll\">\n              <div class=\"song-list-content\">\n                <song-item\n                  v-for=\"song in songs\"\n                  :key=\"song.id\"\n                  :item=\"song\"\n                  :list=\"true\"\n                  @play=\"handlePlay\"\n                />\n                <div v-if=\"songLoading\" class=\"loading-more\">{{ t('common.loading') }}</div>\n              </div>\n              <play-bottom />\n            </n-scrollbar>\n          </div>\n        </n-tab-pane>\n\n        <n-tab-pane name=\"albums\" :tab=\"t('artist.albums')\">\n          <div ref=\"albumListRef\" class=\"albums-list\">\n            <n-scrollbar style=\"max-height: 61vh\" :size=\"5\" @scroll=\"handleAlbumScroll\">\n              <div class=\"albums-grid\">\n                <search-item\n                  v-for=\"album in albums\"\n                  :key=\"album.id\"\n                  shape=\"square\"\n                  :z-index=\"9998\"\n                  :item=\"{\n                    id: album.id,\n                    picUrl: album.picUrl,\n                    name: album.name,\n                    desc: formatPublishTime(album.publishTime),\n                    size: album.size,\n                    type: '专辑'\n                  }\"\n                />\n                <div v-if=\"albumLoading\" class=\"loading-more\">{{ t('common.loading') }}</div>\n              </div>\n              <play-bottom />\n            </n-scrollbar>\n          </div>\n        </n-tab-pane>\n\n        <n-tab-pane name=\"about\" :tab=\"t('artist.description')\">\n          <div class=\"artist-description\">\n            <n-scrollbar style=\"max-height: 60vh\">\n              <div class=\"description-content\" v-html=\"artistInfo?.briefDesc\"></div>\n            </n-scrollbar>\n          </div>\n        </n-tab-pane>\n      </n-tabs>\n    </div>\n  </n-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport { useDateFormat } from '@vueuse/core';\nimport { computed, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { getArtistAlbums, getArtistDetail, getArtistTopSongs } from '@/api/artist';\nimport { getMusicDetail } from '@/api/music';\nimport SearchItem from '@/components/common/SearchItem.vue';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { usePlayerStore, useSettingsStore } from '@/store';\nimport { IArtist } from '@/types/artist';\nimport { getImgUrl } from '@/utils';\n\nimport PlayBottom from './PlayBottom.vue';\n\nconst { t } = useI18n();\n\nconst settingsStore = useSettingsStore();\nconst playerStore = usePlayerStore();\n\nconst currentArtistId = computed({\n  get: () => settingsStore.currentArtistId,\n  set: (val) => settingsStore.setCurrentArtistId(val as number)\n});\n\nconst modelValue = defineModel<boolean>('show', { required: true });\n\nconst activeTab = ref('songs');\n\n// 歌手信息\nconst artistInfo = ref<IArtist>();\nconst songs = ref<any[]>([]);\nconst albums = ref<any[]>([]);\n\n// 加载状态\nconst songLoading = ref(false);\nconst albumLoading = ref(false);\n\n// 分页参数\nconst songPage = ref({\n  page: 1,\n  pageSize: 30,\n  hasMore: true\n});\n\nconst albumPage = ref({\n  page: 1,\n  pageSize: 30,\n  hasMore: true\n});\n\nwatch(modelValue, (newVal) => {\n  settingsStore.setShowArtistDrawer(newVal);\n});\nconst loading = ref(false);\n// 加载歌手信息\n\nconst previousArtistId = ref<number>();\nconst loadArtistInfo = async (id: number) => {\n  // if (currentArtistId.value === id) return;\n  if (previousArtistId.value === id) return;\n  activeTab.value = 'songs';\n  loading.value = true;\n  previousArtistId.value = id;\n  try {\n    const info = await getArtistDetail(id);\n    if (info.data?.data?.artist) {\n      artistInfo.value = info.data.data.artist;\n    }\n    // 重置分页并加载初始数据\n    resetPagination();\n    await Promise.all([loadSongs(), loadAlbums()]);\n  } catch (error) {\n    console.error('加载歌手信息失败:', error);\n  } finally {\n    loading.value = false;\n  }\n};\n\n// 重置分页\nconst resetPagination = () => {\n  songPage.value = {\n    page: 1,\n    pageSize: 30,\n    hasMore: true\n  };\n  albumPage.value = {\n    page: 1,\n    pageSize: 30,\n    hasMore: true\n  };\n  songs.value = [];\n  albums.value = [];\n};\n\n// 加载歌曲\nconst loadSongs = async () => {\n  if (!currentArtistId.value || !songPage.value.hasMore || songLoading.value) return;\n\n  try {\n    songLoading.value = true;\n    const { page, pageSize } = songPage.value;\n    const res = await getArtistTopSongs({\n      id: currentArtistId.value,\n      limit: pageSize,\n      offset: (page - 1) * pageSize\n    });\n\n    const ids = res.data.songs.map((item) => item.id);\n    const songsDetail = await getMusicDetail(ids);\n\n    if (songsDetail.data?.songs) {\n      const newSongs = songsDetail.data.songs.map((item) => {\n        return {\n          ...item,\n          picUrl: item.al.picUrl,\n          song: {\n            artists: item.ar,\n            name: item.name,\n            id: item.id\n          }\n        };\n      });\n      songs.value = page === 1 ? newSongs : [...songs.value, ...newSongs];\n      songPage.value.hasMore = newSongs.length === pageSize;\n      songPage.value.page++;\n    }\n  } catch (error) {\n    console.error('加载歌曲失败:', error);\n  } finally {\n    songLoading.value = false;\n  }\n};\n\n// 加载专辑\nconst loadAlbums = async () => {\n  if (!currentArtistId.value || !albumPage.value.hasMore || albumLoading.value) return;\n\n  try {\n    albumLoading.value = true;\n    const { page, pageSize } = albumPage.value;\n    const res = await getArtistAlbums({\n      id: currentArtistId.value,\n      limit: pageSize,\n      offset: (page - 1) * pageSize\n    });\n\n    if (res.data?.hotAlbums) {\n      const newAlbums = res.data.hotAlbums;\n      albums.value = page === 1 ? newAlbums : [...albums.value, ...newAlbums];\n      albumPage.value.hasMore = newAlbums.length === pageSize;\n      albumPage.value.page++;\n    }\n  } catch (error) {\n    console.error('加载专辑失败:', error);\n  } finally {\n    albumLoading.value = false;\n  }\n};\n\n// 处理滚动加载\nconst handleSongScroll = (e: { target: any }) => {\n  const { scrollTop, scrollHeight, clientHeight } = e.target;\n  if (scrollHeight - scrollTop - clientHeight < 50) {\n    loadSongs();\n  }\n};\n\nconst handleAlbumScroll = (e: { target: any }) => {\n  const { scrollTop, scrollHeight, clientHeight } = e.target;\n  if (scrollHeight - scrollTop - clientHeight < 50) {\n    loadAlbums();\n  }\n};\n\n// 格式化发布时间\nconst formatPublishTime = (time: number) => {\n  return useDateFormat(time, 'YYYY-MM-DD').value;\n};\n\nconst handlePlay = () => {\n  playerStore.setPlayList(\n    songs.value.map((item) => ({\n      ...item,\n      picUrl: item.al.picUrl\n    }))\n  );\n};\n\n// 暴露方法给父组件\ndefineExpose({\n  loadArtistInfo\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.artist-drawer {\n  @apply h-full bg-light dark:bg-dark px-6 overflow-hidden flex flex-col;\n\n  .close-btn {\n    @apply absolute top-4 right-4 text-gray-500 dark:text-gray-400 hover:text-green-500 text-2xl cursor-pointer p-2;\n  }\n\n  .artist-header {\n    @apply flex gap-6 pt-6;\n\n    .artist-info {\n      @apply flex-1;\n\n      .artist-name {\n        @apply text-4xl font-bold mb-2;\n      }\n\n      .artist-alias {\n        @apply text-gray-500 dark:text-gray-400 mb-2;\n      }\n\n      .artist-desc {\n        @apply text-sm text-gray-600 dark:text-gray-300 line-clamp-3;\n      }\n    }\n  }\n\n  .albums-grid {\n    @apply grid gap-4 grid-cols-5;\n  }\n\n  .loading-more {\n    @apply text-center py-4 text-gray-500 dark:text-gray-400;\n  }\n\n  .artist-description {\n    .description-content {\n      @apply text-sm leading-relaxed whitespace-pre-wrap;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/BilibiliItem.vue",
    "content": "<template>\n  <div class=\"bilibili-item\" @click=\"handleClick\">\n    <div class=\"bilibili-item-img\">\n      <n-image class=\"w-full h-full\" :src=\"item.pic\" lazy preview-disabled />\n      <div class=\"play\">\n        <i class=\"ri-play-fill text-4xl\"></i>\n      </div>\n      <div class=\"duration\">{{ formatDuration(item.duration) }}</div>\n    </div>\n    <div class=\"bilibili-item-info\">\n      <p class=\"bilibili-item-title\" v-html=\"item.title\"></p>\n      <p class=\"bilibili-item-author\"><i class=\"ri-user-line mr-1\"></i>{{ item.author }}</p>\n      <div class=\"bilibili-item-stats\">\n        <span><i class=\"ri-play-line mr-1\"></i>{{ formatNumber(item.view) }}</span>\n        <span><i class=\"ri-chat-1-line mr-1\"></i>{{ formatNumber(item.danmaku) }}</span>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n';\n\nimport type { IBilibiliSearchResult } from '@/types/bilibili';\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n  item: IBilibiliSearchResult;\n}>();\n\nconst emit = defineEmits<{\n  (e: 'play', item: IBilibiliSearchResult): void;\n}>();\n\nconst handleClick = () => {\n  emit('play', props.item);\n};\n\n/**\n * 格式化数字显示\n */\nconst formatNumber = (num?: number) => {\n  if (!num) return '0';\n  if (num >= 10000) {\n    return `${(num / 10000).toFixed(1)}${t('bilibili.player.num')}`;\n  }\n  return num.toString();\n};\n\n/**\n * 格式化视频时长\n */\nconst formatDuration = (duration?: number | string) => {\n  if (!duration) return '00:00:00';\n\n  // 处理字符串格式 (例如 \"4352:29\")\n  if (typeof duration === 'string') {\n    // 检查是否是合法的格式\n    if (/^\\d+:\\d+$/.test(duration)) {\n      // 分解分钟和秒数\n      const [minutes, seconds] = duration.split(':').map(Number);\n\n      // 转换为时:分:秒格式\n      const hours = Math.floor(minutes / 60);\n      const remainingMinutes = minutes % 60;\n\n      return `${hours.toString().padStart(2, '0')}:${remainingMinutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;\n    }\n    return '00:00:00';\n  }\n\n  // 数字处理逻辑 (秒数转为\"时:分:秒\"格式)\n  const hours = Math.floor(duration / 3600);\n  const minutes = Math.floor((duration % 3600) / 60);\n  const seconds = duration % 60;\n\n  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;\n};\n</script>\n\n<style scoped lang=\"scss\">\n.bilibili-item {\n  @apply rounded-lg flex items-start hover:bg-light-200 dark:hover:bg-dark-200 p-3 transition cursor-pointer border-none;\n\n  &-img {\n    @apply w-40 rounded-lg overflow-hidden relative mr-4;\n    aspect-ratio: 16/9;\n\n    &:hover {\n      .play {\n        @apply opacity-80;\n      }\n    }\n\n    .play {\n      @apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity text-white;\n    }\n\n    .duration {\n      @apply absolute bottom-1 right-1 text-xs text-white px-1 py-0.5 rounded-sm bg-black/60 backdrop-blur-sm;\n    }\n  }\n\n  &-info {\n    @apply flex-1 overflow-hidden;\n  }\n\n  &-title {\n    @apply text-gray-800 dark:text-gray-200 text-sm font-medium mb-1 line-clamp-2 leading-tight;\n  }\n\n  &-author {\n    @apply text-gray-500 dark:text-gray-400 text-xs flex items-center mb-1;\n  }\n\n  &-stats {\n    @apply flex items-center text-xs text-gray-500 dark:text-gray-400 gap-3;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/DisclaimerModal.vue",
    "content": "<template>\n  <Teleport to=\"body\">\n    <Transition name=\"disclaimer-modal\">\n      <!-- 免责声明页面 -->\n      <div\n        v-if=\"showDisclaimer\"\n        class=\"fixed inset-0 z-[999999] flex items-center justify-center bg-black/60 backdrop-blur-md\"\n      >\n        <div\n          class=\"w-full max-w-md mx-4 bg-white dark:bg-gray-900 rounded-3xl overflow-hidden shadow-2xl\"\n        >\n          <!-- 顶部渐变装饰 -->\n          <div class=\"h-2 bg-gradient-to-r from-amber-400 via-orange-500 to-red-500\"></div>\n          <!-- 标题 -->\n          <h2 class=\"text-2xl font-bold text-center text-gray-900 dark:text-white px-6 mt-10\">\n            {{ t('comp.disclaimer.title') }}\n          </h2>\n\n          <!-- 内容区域 -->\n          <div class=\"px-6 py-6\">\n            <div class=\"space-y-4 text-sm text-gray-600 dark:text-gray-300 leading-relaxed\">\n              <!-- 警告框 -->\n              <div\n                class=\"p-4 rounded-2xl bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800\"\n              >\n                <div class=\"flex items-start gap-3\">\n                  <i class=\"ri-alert-line text-amber-500 text-xl flex-shrink-0 mt-0.5\"></i>\n                  <p class=\"text-amber-700 dark:text-amber-300\">\n                    {{ t('comp.disclaimer.warning') }}\n                  </p>\n                </div>\n              </div>\n\n              <!-- 免责条款列表 -->\n              <div class=\"space-y-3\">\n                <div class=\"flex items-start gap-3\">\n                  <div\n                    class=\"w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center flex-shrink-0\"\n                  >\n                    <i class=\"ri-book-2-line text-blue-500 text-sm\"></i>\n                  </div>\n                  <p>{{ t('comp.disclaimer.item1') }}</p>\n                </div>\n\n                <div class=\"flex items-start gap-3\">\n                  <div\n                    class=\"w-6 h-6 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center flex-shrink-0\"\n                  >\n                    <i class=\"ri-time-line text-green-500 text-sm\"></i>\n                  </div>\n                  <p>{{ t('comp.disclaimer.item2') }}</p>\n                </div>\n\n                <div class=\"flex items-start gap-3\">\n                  <div\n                    class=\"w-6 h-6 rounded-full bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center flex-shrink-0\"\n                  >\n                    <i class=\"ri-shield-check-line text-purple-500 text-sm\"></i>\n                  </div>\n                  <p>{{ t('comp.disclaimer.item3') }}</p>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <!-- 操作按钮 -->\n          <div class=\"px-6 pb-8 space-y-3\">\n            <button\n              @click=\"handleAgree\"\n              class=\"w-full py-4 rounded-2xl text-base font-medium text-white bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 active:scale-[0.98] transition-all duration-200 shadow-lg shadow-green-500/25\"\n            >\n              <span class=\"flex items-center justify-center gap-2\">\n                <i class=\"ri-check-line text-lg\"></i>\n                {{ t('comp.disclaimer.agree') }}\n              </span>\n            </button>\n\n            <button\n              @click=\"handleDisagree\"\n              class=\"w-full py-3 rounded-2xl text-sm font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors\"\n            >\n              {{ t('comp.disclaimer.disagree') }}\n            </button>\n          </div>\n        </div>\n      </div>\n    </Transition>\n\n    <!-- 捐赠页面 -->\n    <Transition name=\"donate-modal\">\n      <div\n        v-if=\"showDonate\"\n        class=\"fixed inset-0 z-[999999] flex items-center justify-center bg-black/60 backdrop-blur-md\"\n      >\n        <div\n          class=\"w-full max-w-md mx-4 bg-white dark:bg-gray-900 rounded-3xl overflow-hidden shadow-2xl\"\n        >\n          <!-- 顶部渐变装饰 -->\n          <div class=\"h-2 bg-gradient-to-r from-pink-400 via-rose-500 to-red-500\"></div>\n\n          <!-- 图标区域 -->\n          <div class=\"flex justify-center pt-8 pb-4\">\n            <div\n              class=\"w-20 h-20 rounded-2xl bg-gradient-to-br from-pink-400 to-rose-500 flex items-center justify-center shadow-lg\"\n            >\n              <i class=\"ri-heart-3-fill text-4xl text-white\"></i>\n            </div>\n          </div>\n\n          <!-- 标题 -->\n          <h2 class=\"text-2xl font-bold text-center text-gray-900 dark:text-white px-6\">\n            {{ t('comp.donate.title') }}\n          </h2>\n\n          <p class=\"text-sm text-gray-500 dark:text-gray-400 text-center mt-2 px-6\">\n            {{ t('comp.donate.subtitle') }}\n          </p>\n\n          <!-- 内容区域 -->\n          <div class=\"px-6 py-6\">\n            <!-- 提示信息 -->\n            <div\n              class=\"p-4 rounded-2xl bg-rose-50 dark:bg-rose-900/20 border border-rose-200 dark:border-rose-800 mb-6\"\n            >\n              <div class=\"flex items-start gap-3\">\n                <i class=\"ri-gift-line text-rose-500 text-xl flex-shrink-0 mt-0.5\"></i>\n                <p class=\"text-rose-700 dark:text-rose-300 text-sm\">\n                  {{ t('comp.donate.tip') }}\n                </p>\n              </div>\n            </div>\n\n            <!-- 捐赠方式 -->\n            <div class=\"grid grid-cols-2 gap-4\">\n              <button\n                @click=\"openDonateLink('wechat')\"\n                class=\"flex flex-col items-center gap-2 p-4 rounded-2xl bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 hover:bg-green-100 dark:hover:bg-green-900/30 transition-colors\"\n              >\n                <div class=\"w-12 h-12 rounded-xl bg-green-500 flex items-center justify-center\">\n                  <i class=\"ri-wechat-fill text-2xl text-white\"></i>\n                </div>\n                <span class=\"text-sm font-medium text-green-700 dark:text-green-300\">{{\n                  t('comp.donate.wechat')\n                }}</span>\n              </button>\n\n              <button\n                @click=\"openDonateLink('alipay')\"\n                class=\"flex flex-col items-center gap-2 p-4 rounded-2xl bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 hover:bg-blue-100 dark:hover:bg-blue-900/30 transition-colors\"\n              >\n                <div class=\"w-12 h-12 rounded-xl bg-blue-500 flex items-center justify-center\">\n                  <i class=\"ri-alipay-fill text-2xl text-white\"></i>\n                </div>\n                <span class=\"text-sm font-medium text-blue-700 dark:text-blue-300\">{{\n                  t('comp.donate.alipay')\n                }}</span>\n              </button>\n            </div>\n          </div>\n\n          <!-- 进入应用按钮 -->\n          <div class=\"px-6 pb-8\">\n            <button\n              @click=\"handleEnterApp\"\n              class=\"w-full py-4 rounded-2xl text-base font-medium text-white bg-gradient-to-r from-gray-700 to-gray-900 dark:from-gray-600 dark:to-gray-800 hover:from-gray-800 hover:to-gray-950 active:scale-[0.98] transition-all duration-200 shadow-lg\"\n            >\n              <span class=\"flex items-center justify-center gap-2\">\n                <i class=\"ri-arrow-right-line text-lg\"></i>\n                {{ t('comp.donate.enterApp') }}\n              </span>\n            </button>\n\n            <p class=\"text-xs text-gray-400 dark:text-gray-500 text-center mt-3\">\n              {{ t('comp.donate.noForce') }}\n            </p>\n          </div>\n        </div>\n      </div>\n    </Transition>\n\n    <!-- 收款码弹窗 -->\n    <Transition name=\"qrcode-modal\">\n      <div\n        v-if=\"showQRCode\"\n        class=\"fixed inset-0 z-[9999999] flex items-center justify-center bg-black/70 backdrop-blur-md\"\n        @click.self=\"closeQRCode\"\n      >\n        <div\n          class=\"w-full max-w-sm mx-4 bg-white dark:bg-gray-900 rounded-3xl overflow-hidden shadow-2xl\"\n        >\n          <!-- 顶部渐变装饰 -->\n          <div\n            class=\"h-2\"\n            :class=\"\n              qrcodeType === 'wechat'\n                ? 'bg-gradient-to-r from-green-400 to-green-600'\n                : 'bg-gradient-to-r from-blue-400 to-blue-600'\n            \"\n          ></div>\n\n          <!-- 标题 -->\n          <div class=\"flex items-center justify-between px-6 py-4\">\n            <h3 class=\"text-lg font-bold text-gray-900 dark:text-white\">\n              {{ qrcodeType === 'wechat' ? t('comp.donate.wechatQR') : t('comp.donate.alipayQR') }}\n            </h3>\n            <button\n              @click=\"closeQRCode\"\n              class=\"w-8 h-8 rounded-full flex items-center justify-center text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors\"\n            >\n              <i class=\"ri-close-line text-xl\"></i>\n            </button>\n          </div>\n\n          <!-- 二维码图片 -->\n          <div class=\"px-6 pb-6\">\n            <div class=\"bg-white p-4 rounded-2xl\">\n              <img\n                :src=\"qrcodeType === 'wechat' ? wechatQRCode : alipayQRCode\"\n                :alt=\"qrcodeType === 'wechat' ? 'WeChat QR Code' : 'Alipay QR Code'\"\n                class=\"w-full rounded-xl\"\n              />\n            </div>\n            <p class=\"text-sm text-gray-500 dark:text-gray-400 text-center mt-4\">\n              {{ t('comp.donate.scanTip') }}\n            </p>\n          </div>\n        </div>\n      </div>\n    </Transition>\n  </Teleport>\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\n// 导入收款码图片\nimport alipayQRCode from '@/assets/alipay.png';\nimport wechatQRCode from '@/assets/wechat.png';\nimport { isElectron, isLyricWindow } from '@/utils';\n\nconst { t } = useI18n();\n\n// 缓存键\nconst DISCLAIMER_AGREED_KEY = 'disclaimer_agreed_timestamp';\n\nconst showDisclaimer = ref(false);\nconst showDonate = ref(false);\nconst showQRCode = ref(false);\nconst qrcodeType = ref<'wechat' | 'alipay'>('wechat');\nconst isTransitioning = ref(false); // 防止用户点击过快\n\n// 检查是否需要显示免责声明\nconst shouldShowDisclaimer = () => {\n  return !localStorage.getItem(DISCLAIMER_AGREED_KEY);\n};\n\n// 处理同意\nconst handleAgree = () => {\n  if (isTransitioning.value) return;\n  isTransitioning.value = true;\n\n  showDisclaimer.value = false;\n  setTimeout(() => {\n    showDonate.value = true;\n    isTransitioning.value = false;\n  }, 300);\n};\n\n// 处理不同意 - 退出应用\nconst handleDisagree = () => {\n  if (isTransitioning.value) return;\n  isTransitioning.value = true;\n\n  if (isElectron) {\n    // Electron 环境下强制退出应用\n    window.api?.quitApp?.();\n  } else {\n    // Web 环境下尝试关闭窗口\n    window.close();\n  }\n  isTransitioning.value = false;\n};\n\n// 打开捐赠链接\nconst openDonateLink = (type: 'wechat' | 'alipay') => {\n  if (isTransitioning.value) return;\n\n  qrcodeType.value = type;\n  showQRCode.value = true;\n};\n\n// 关闭二维码弹窗\nconst closeQRCode = () => {\n  showQRCode.value = false;\n};\n\n// 进入应用\nconst handleEnterApp = () => {\n  if (isTransitioning.value) return;\n  isTransitioning.value = true;\n\n  // 记录同意时间\n  localStorage.setItem(DISCLAIMER_AGREED_KEY, Date.now().toString());\n  showDonate.value = false;\n\n  setTimeout(() => {\n    isTransitioning.value = false;\n  }, 300);\n};\n\nonMounted(() => {\n  // 歌词窗口不显示免责声明\n  if (isLyricWindow.value) return;\n\n  // 检查是否需要显示免责声明\n  if (shouldShowDisclaimer()) {\n    showDisclaimer.value = true;\n  }\n});\n</script>\n\n<style scoped>\n/* 免责声明弹窗动画 */\n.disclaimer-modal-enter-active,\n.disclaimer-modal-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.disclaimer-modal-enter-from,\n.disclaimer-modal-leave-to {\n  opacity: 0;\n}\n\n/* 捐赠弹窗动画 */\n.donate-modal-enter-active,\n.donate-modal-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.donate-modal-enter-from,\n.donate-modal-leave-to {\n  opacity: 0;\n}\n\n/* 二维码弹窗动画 */\n.qrcode-modal-enter-active,\n.qrcode-modal-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.qrcode-modal-enter-from,\n.qrcode-modal-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/DonationList.vue",
    "content": "<template>\n  <div class=\"donation-container\">\n    <div class=\"qrcode-container\">\n      <div class=\"description\">\n        <p>{{ t('donation.description') }}</p>\n        <p>{{ t('donation.message') }}</p>\n        <n-button type=\"primary\" @click=\"toDonateList\">\n          <template #icon>\n            <i class=\"ri-cup-line\"></i>\n          </template>\n          {{ t('donation.toDonateList') }}\n        </n-button>\n      </div>\n      <div class=\"qrcode-grid\">\n        <div class=\"qrcode-item\">\n          <n-image :src=\"alipay\" :alt=\"t('common.alipay')\" class=\"qrcode-image\" preview-disabled />\n          <span class=\"qrcode-label\">{{ t('common.alipay') }}</span>\n        </div>\n\n        <div class=\"qrcode-item\">\n          <n-image :src=\"wechat\" :alt=\"t('common.wechat')\" class=\"qrcode-image\" preview-disabled />\n          <span class=\"qrcode-label\">{{ t('common.wechat') }}</span>\n        </div>\n      </div>\n    </div>\n\n    <div class=\"header-container\">\n      <h3 class=\"section-title\">{{ t('donation.title') }}</h3>\n      <n-button secondary round size=\"small\" :loading=\"isLoading\" @click=\"fetchDonors\">\n        <template #icon>\n          <i class=\"ri-refresh-line\"></i>\n        </template>\n        {{ t('donation.refresh') }}\n      </n-button>\n    </div>\n\n    <div class=\"donation-grid\" :class=\"{ 'grid-expanded': isExpanded }\">\n      <div\n        v-for=\"donor in displayDonors\"\n        :key=\"donor.id\"\n        class=\"donation-card\"\n        :class=\"{ 'no-message': !donor.message }\"\n      >\n        <div class=\"card-content\">\n          <div class=\"donor-avatar\">\n            <n-avatar :src=\"donor.avatar\" :fallback-src=\"defaultAvatar\" round class=\"avatar-img\" />\n          </div>\n          <div class=\"donor-info\">\n            <div class=\"donor-meta\">\n              <div class=\"donor-name\">{{ donor.name }}</div>\n              <!-- <div class=\"price-tag\">￥{{ donor.amount }}</div> -->\n            </div>\n            <div class=\"donation-date\">{{ donor.date }}</div>\n          </div>\n        </div>\n\n        <!-- 有留言的情况 -->\n        <n-popover\n          v-if=\"donor.message\"\n          trigger=\"hover\"\n          placement=\"bottom\"\n          :show-arrow=\"true\"\n          :width=\"240\"\n        >\n          <template #trigger>\n            <div class=\"donation-message\">\n              <i class=\"ri-double-quotes-l quote-icon\"></i>\n              <span class=\"message-text\">{{ donor.message }}</span>\n              <i class=\"ri-double-quotes-r quote-icon\"></i>\n            </div>\n          </template>\n          <div class=\"message-popover\">\n            <i class=\"ri-double-quotes-l quote-icon\"></i>\n            <span>{{ donor.message }}</span>\n            <i class=\"ri-double-quotes-r quote-icon\"></i>\n          </div>\n        </n-popover>\n\n        <!-- 没有留言的情况显示占位符 -->\n        <div v-else class=\"donation-message-placeholder\">\n          <i class=\"ri-emotion-line\"></i>\n          <span>{{ t('donation.noMessage') }}</span>\n        </div>\n      </div>\n    </div>\n\n    <div v-if=\"donors.length > 8\" class=\"expand-button\">\n      <n-button text @click=\"toggleExpand\">\n        <template #icon>\n          <i :class=\"isExpanded ? 'ri-arrow-up-s-line' : 'ri-arrow-down-s-line'\"></i>\n        </template>\n        {{ isExpanded ? t('common.collapse') : t('common.expand') }}\n      </n-button>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onActivated, onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport type { Donor } from '@/api/donation';\nimport { getDonationList } from '@/api/donation';\nimport alipay from '@/assets/alipay.png';\nimport wechat from '@/assets/wechat.png';\n\nconst { t } = useI18n();\n\n// 默认头像\nconst defaultAvatar = 'https://avatars.githubusercontent.com/u/0?v=4';\n\nconst donors = ref<Donor[]>([]);\nconst isLoading = ref(false);\n\nconst fetchDonors = async () => {\n  isLoading.value = true;\n  try {\n    const data = await getDonationList();\n    donors.value = data.map((donor, index) => ({\n      ...donor,\n      avatar: `https://api.dicebear.com/7.x/micah/svg?seed=${index}`\n    }));\n  } catch (error) {\n    console.error('Failed to fetch donors:', error);\n  } finally {\n    isLoading.value = false;\n  }\n};\n\nonMounted(() => {\n  fetchDonors();\n});\n\nonActivated(() => {\n  fetchDonors();\n});\n\nconst isExpanded = ref(false);\n\nconst displayDonors = computed(() => {\n  if (isExpanded.value) {\n    return donors.value;\n  }\n  return donors.value.slice(0, 8);\n});\n\nconst toggleExpand = () => {\n  isExpanded.value = !isExpanded.value;\n};\n\nconst toDonateList = () => {\n  window.open('http://donate.alger.fun/download', '_blank');\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.donation-container {\n  @apply w-full overflow-hidden flex flex-col gap-4;\n}\n\n.header-container {\n  @apply flex justify-between items-center px-4 py-2;\n\n  .section-title {\n    @apply text-lg font-medium text-gray-700 dark:text-gray-200;\n  }\n}\n\n.donation-grid {\n  @apply grid gap-3 transition-all duration-300 overflow-hidden;\n  grid-template-columns: repeat(2, 1fr);\n  max-height: 320px;\n\n  @media (min-width: 768px) {\n    grid-template-columns: repeat(3, 1fr);\n  }\n\n  @media (min-width: 1024px) {\n    grid-template-columns: repeat(4, 1fr);\n  }\n\n  &.grid-expanded {\n    @apply max-h-none;\n  }\n}\n\n.donation-card {\n  @apply rounded-lg p-2.5 transition-all duration-200 hover:shadow-md;\n  @apply bg-light-100 dark:bg-gray-800/5 backdrop-blur-sm;\n  @apply border border-gray-200 dark:border-gray-700/10;\n  @apply flex flex-col;\n  min-height: 100px;\n\n  .card-content {\n    @apply flex items-start gap-2 mb-2;\n  }\n}\n\n.donor-avatar {\n  @apply relative flex-shrink-0;\n\n  .avatar-img {\n    @apply border border-gray-200 dark:border-gray-700/10 shadow-sm;\n    @apply w-9 h-9;\n  }\n}\n\n.donor-info {\n  @apply flex-1 min-w-0 flex flex-col justify-center;\n\n  .donor-meta {\n    @apply flex justify-between items-center mb-0.5;\n\n    .donor-name {\n      @apply text-sm font-medium truncate flex-1 mr-1;\n    }\n\n    .price-tag {\n      @apply text-xs text-gray-400/80 dark:text-gray-500/80 whitespace-nowrap;\n    }\n  }\n\n  .donation-date {\n    @apply text-xs text-gray-400/60 dark:text-gray-500/60;\n  }\n}\n\n.donation-message {\n  @apply text-xs text-gray-500 dark:text-gray-400 italic mt-1 px-2 py-1.5;\n  @apply bg-gray-100/10 dark:bg-dark-300 rounded;\n  @apply flex items-start;\n  @apply cursor-pointer transition-all duration-200;\n\n  .quote-icon {\n    @apply text-gray-300 dark:text-gray-600 flex-shrink-0 opacity-60;\n\n    &:first-child {\n      @apply mr-1 self-start;\n    }\n\n    &:last-child {\n      @apply ml-1 self-end;\n    }\n  }\n\n  .message-text {\n    @apply flex-1 line-clamp-2;\n  }\n\n  &:hover {\n    @apply bg-gray-100/40 dark:bg-dark-200;\n  }\n}\n\n.donation-message-placeholder {\n  @apply text-xs text-gray-400 dark:text-gray-500 mt-1 px-2 py-1.5;\n  @apply bg-gray-100/5 dark:bg-dark-300 rounded;\n  @apply flex items-center justify-center gap-1 italic;\n  @apply border border-transparent;\n\n  i {\n    @apply text-gray-300 dark:text-gray-600;\n  }\n}\n\n.message-popover {\n  @apply text-sm text-gray-700 dark:text-gray-200 italic p-2;\n  @apply flex items-start;\n\n  .quote-icon {\n    @apply text-gray-400 dark:text-gray-500 flex-shrink-0;\n\n    &:first-child {\n      @apply mr-1.5 self-start;\n    }\n\n    &:last-child {\n      @apply ml-1.5 self-end;\n    }\n  }\n}\n\n.expand-button {\n  @apply flex justify-center items-center py-2;\n\n  :deep(.n-button) {\n    @apply transition-all duration-200 hover:-translate-y-0.5;\n  }\n}\n\n.qrcode-container {\n  @apply p-5 rounded-lg shadow-sm bg-light-100 dark:bg-gray-800/5 backdrop-blur-sm border border-gray-200 dark:border-gray-700/10;\n\n  .description {\n    @apply text-center text-sm text-gray-600 dark:text-gray-300 mb-4;\n\n    p {\n      @apply mb-2;\n    }\n  }\n\n  .qrcode-grid {\n    @apply flex justify-between items-center gap-4 flex-wrap;\n\n    .qrcode-item {\n      @apply flex flex-col items-center gap-2;\n\n      .qrcode-image {\n        @apply w-36 h-36 rounded-lg border border-gray-200 dark:border-gray-700/10 shadow-sm transition-transform duration-200 hover:scale-105;\n      }\n\n      .qrcode-label {\n        @apply text-sm text-gray-600 dark:text-gray-300;\n      }\n    }\n\n    .donate-button {\n      @apply flex flex-col items-center justify-center;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/DownloadDrawer.vue",
    "content": "<template>\n  <div class=\"download-drawer-trigger\">\n    <n-badge :value=\"downloadingCount\" :max=\"99\" :show=\"downloadingCount > 0\">\n      <n-button circle @click=\"navigateToDownloads\">\n        <template #icon>\n          <i class=\"iconfont ri-download-cloud-2-line\"></i>\n        </template>\n      </n-button>\n    </n-badge>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, ref } from 'vue';\nimport { useRouter } from 'vue-router';\n\nconst router = useRouter();\nconst downloadList = ref<any[]>([]);\n\n// 计算下载中的任务数量\nconst downloadingCount = computed(() => {\n  return downloadList.value.filter((item) => item.status === 'downloading').length;\n});\n\n// 导航到下载页面\nconst navigateToDownloads = () => {\n  router.push('/downloads');\n};\n\n// 监听下载进度\nonMounted(() => {\n  // 监听下载进度\n  window.electron.ipcRenderer.on('music-download-progress', (_, data) => {\n    const existingItem = downloadList.value.find((item) => item.filename === data.filename);\n\n    // 如果进度为100%，将状态设置为已完成\n    if (data.progress === 100) {\n      data.status = 'completed';\n    }\n\n    if (existingItem) {\n      Object.assign(existingItem, {\n        ...data,\n        songInfo: data.songInfo || existingItem.songInfo\n      });\n\n      // 如果下载完成，从列表中移除\n      if (data.status === 'completed') {\n        downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);\n      }\n    } else {\n      downloadList.value.push({\n        ...data,\n        songInfo: data.songInfo\n      });\n    }\n  });\n\n  // 监听下载完成\n  window.electron.ipcRenderer.on('music-download-complete', async (_, data) => {\n    if (data.success) {\n      downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);\n    } else {\n      const existingItem = downloadList.value.find((item) => item.filename === data.filename);\n      if (existingItem) {\n        Object.assign(existingItem, {\n          status: 'error',\n          error: data.error,\n          progress: 0\n        });\n        setTimeout(() => {\n          downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);\n        }, 3000);\n      }\n    }\n  });\n\n  // 监听下载队列\n  window.electron.ipcRenderer.on('music-download-queued', (_, data) => {\n    const existingItem = downloadList.value.find((item) => item.filename === data.filename);\n    if (!existingItem) {\n      downloadList.value.push({\n        filename: data.filename,\n        progress: 0,\n        loaded: 0,\n        total: 0,\n        path: '',\n        status: 'downloading',\n        songInfo: data.songInfo\n      });\n    }\n  });\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.download-drawer-trigger {\n  @apply fixed left-6 bottom-24 z-[999];\n\n  .n-button {\n    @apply bg-white/80 dark:bg-gray-800/80 shadow-lg backdrop-blur-sm;\n    @apply hover:bg-light dark:hover:bg-dark-200;\n    @apply text-gray-600 dark:text-gray-300;\n    @apply transition-all duration-300;\n    @apply w-10 h-10;\n\n    .iconfont {\n      @apply text-xl;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/InstallAppModal.vue",
    "content": "<template>\n  <n-modal\n    v-model:show=\"showModal\"\n    preset=\"dialog\"\n    :show-icon=\"false\"\n    :mask-closable=\"true\"\n    class=\"install-app-modal\"\n  >\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <div class=\"app-icon\">\n          <img src=\"@/assets/logo.png\" alt=\"App Icon\" />\n        </div>\n        <div class=\"app-info\">\n          <h2 class=\"app-name\">Alger Music Player {{ config.version }}</h2>\n          <p class=\"app-desc mb-2\">{{ t('comp.installApp.description') }}</p>\n          <n-checkbox v-model:checked=\"noPrompt\">{{ t('comp.installApp.noPrompt') }}</n-checkbox>\n        </div>\n      </div>\n      <div class=\"modal-actions\">\n        <n-button class=\"cancel-btn\" @click=\"closeModal\">{{\n          t('comp.installApp.cancel')\n        }}</n-button>\n        <n-button type=\"primary\" class=\"install-btn\" @click=\"handleInstall\">{{\n          t('comp.installApp.install')\n        }}</n-button>\n      </div>\n      <div class=\"modal-desc mt-4 text-center\">\n        <p class=\"text-xs text-gray-400\">\n          {{ t('comp.installApp.downloadProblem') }}\n          <a\n            class=\"text-green-500\"\n            target=\"_blank\"\n            href=\"https://github.com/algerkong/AlgerMusicPlayer/releases\"\n            >GitHub</a\n          >\n          {{ t('comp.installApp.downloadProblemLinkText') }}\n        </p>\n      </div>\n    </div>\n  </n-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { isElectron } from '@/utils';\n\nimport config from '../../../../package.json';\n\nconst { t } = useI18n();\n\nconst showModal = ref(false);\nconst noPrompt = ref(false);\n\nconst closeModal = () => {\n  showModal.value = false;\n  if (noPrompt.value) {\n    localStorage.setItem('installPromptDismissed', 'true');\n  }\n};\n\nonMounted(async () => {\n  // 如果是 electron 环境，不显示安装提示\n  if (isElectron) {\n    return;\n  }\n\n  // 检查是否已经点击过\"暂不安装\"\n  const isDismissed = localStorage.getItem('installPromptDismissed') === 'true';\n  if (isDismissed) {\n    return;\n  }\n  showModal.value = true;\n});\n\nconst handleInstall = async (): Promise<void> => {\n  window.open('http://donate.alger.fun/download', '_blank');\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.install-app-modal {\n  :deep(.n-modal) {\n    @apply max-w-sm;\n  }\n  .modal-content {\n    @apply p-4 pb-0;\n    .modal-header {\n      @apply flex items-center mb-6;\n      .app-icon {\n        @apply w-20 h-20 mr-4 rounded-2xl overflow-hidden;\n        img {\n          @apply w-full h-full object-cover;\n        }\n      }\n      .app-info {\n        @apply flex-1;\n        .app-name {\n          @apply text-xl font-bold mb-1;\n        }\n        .app-desc {\n          @apply text-sm text-gray-400;\n        }\n      }\n    }\n    .modal-actions {\n      @apply flex gap-3 mt-4;\n      .n-button {\n        @apply flex-1;\n      }\n      .cancel-btn {\n        @apply bg-gray-800 text-gray-300 border-none;\n        &:hover {\n          @apply bg-gray-700;\n        }\n      }\n      .install-btn {\n        @apply bg-green-600 border-none;\n        &:hover {\n          @apply bg-green-500;\n        }\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/MobileUpdateModal.vue",
    "content": "<template>\n  <Teleport to=\"body\">\n    <Transition name=\"update-modal\">\n      <div\n        v-if=\"showModal\"\n        class=\"fixed inset-0 z-[999999] flex items-end justify-center bg-black/50 backdrop-blur-sm\"\n      >\n        <!-- 弹窗内容 -->\n        <div\n          class=\"w-full max-w-lg bg-white dark:bg-gray-900 rounded-t-3xl overflow-hidden animate-slide-up\"\n        >\n          <!-- 顶部装饰条 -->\n          <div class=\"h-1 bg-gradient-to-r from-green-400 via-green-500 to-emerald-600\"></div>\n\n          <!-- 关闭条 -->\n          <div class=\"flex justify-center pt-3 pb-2\">\n            <div class=\"w-10 h-1 rounded-full bg-gray-300 dark:bg-gray-700\"></div>\n          </div>\n\n          <!-- 头部信息 -->\n          <div class=\"px-6 pb-5\">\n            <div class=\"flex items-center gap-4\">\n              <!-- 应用图标 -->\n              <div\n                class=\"w-20 h-20 rounded-2xl overflow-hidden shadow-lg flex-shrink-0 ring-2 ring-green-500/20\"\n              >\n                <img src=\"@/assets/logo.png\" alt=\"App Icon\" class=\"w-full h-full object-cover\" />\n              </div>\n\n              <!-- 版本信息 -->\n              <div class=\"flex-1 min-w-0\">\n                <div class=\"flex items-center gap-2 mb-2\">\n                  <span\n                    class=\"px-3 py-1 text-xs font-medium text-white bg-gradient-to-r from-green-500 to-emerald-600 rounded-full\"\n                  >\n                    {{ t('comp.update.newVersion') }}\n                  </span>\n                </div>\n                <h2 class=\"text-2xl font-bold text-gray-900 dark:text-white truncate\">\n                  v{{ updateInfo.latestVersion }}\n                </h2>\n                <p class=\"text-sm text-gray-500 dark:text-gray-400 mt-1\">\n                  {{ t('comp.update.currentVersion') }}: v{{ updateInfo.currentVersion }}\n                </p>\n              </div>\n            </div>\n          </div>\n\n          <!-- 更新内容 -->\n          <div\n            class=\"mx-6 mb-6 max-h-80 overflow-y-auto rounded-2xl bg-gray-50 dark:bg-gray-800/50\"\n          >\n            <div\n              class=\"p-5 text-sm text-gray-600 dark:text-gray-300 leading-relaxed\"\n              v-html=\"parsedReleaseNotes\"\n            ></div>\n          </div>\n\n          <!-- 操作按钮 -->\n          <div\n            class=\"px-6 pb-8 flex gap-3\"\n            :style=\"{ paddingBottom: `calc(32px + var(--safe-area-inset-bottom, 0px))` }\"\n          >\n            <button\n              @click=\"handleLater\"\n              class=\"flex-1 py-4 px-4 rounded-2xl text-base font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 active:scale-[0.98] transition-all duration-200\"\n            >\n              {{ t('comp.update.later') }}\n            </button>\n            <button\n              @click=\"handleUpdate\"\n              class=\"flex-1 py-4 px-4 rounded-2xl text-base font-medium text-white bg-gradient-to-r from-green-500 to-emerald-600 hover:from-green-600 hover:to-emerald-700 active:scale-[0.98] transition-all duration-200 shadow-lg shadow-green-500/25\"\n            >\n              <span class=\"flex items-center justify-center gap-2\">\n                <i class=\"ri-download-2-line text-lg\"></i>\n                {{ t('comp.update.updateNow') }}\n              </span>\n            </button>\n          </div>\n        </div>\n      </div>\n    </Transition>\n  </Teleport>\n</template>\n\n<script setup lang=\"ts\">\nimport { marked } from 'marked';\nimport { computed, onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { checkUpdate, getProxyNodes, UpdateResult } from '@/utils/update';\n\nimport config from '../../../../package.json';\n\nconst { t } = useI18n();\n\n// 缓存键：记录用户点击\"稍后提醒\"的时间\nconst REMIND_LATER_KEY = 'update_remind_later_timestamp';\n\n// 配置 marked\nmarked.setOptions({\n  breaks: true,\n  gfm: true\n});\n\nconst showModal = ref(false);\n\nconst updateInfo = ref<UpdateResult>({\n  hasUpdate: false,\n  latestVersion: '',\n  currentVersion: config.version,\n  releaseInfo: null\n});\n\n// 解析 Markdown\nconst parsedReleaseNotes = computed(() => {\n  if (!updateInfo.value.releaseInfo?.body) return '';\n  try {\n    return marked.parse(updateInfo.value.releaseInfo.body);\n  } catch (error) {\n    console.error('Error parsing markdown:', error);\n    return updateInfo.value.releaseInfo.body;\n  }\n});\n\n// 检查是否应该显示更新提醒\nconst shouldShowReminder = (): boolean => {\n  const remindLaterTime = localStorage.getItem(REMIND_LATER_KEY);\n  if (!remindLaterTime) return true;\n\n  const savedTime = parseInt(remindLaterTime, 10);\n  const now = Date.now();\n  const oneDayInMs = 24 * 60 * 60 * 1000; // 24小时\n\n  // 如果距离上次点击\"稍后提醒\"超过24小时，则显示\n  return now - savedTime >= oneDayInMs;\n};\n\n// 处理\"稍后提醒\"\nconst handleLater = () => {\n  // 记录当前时间\n  localStorage.setItem(REMIND_LATER_KEY, Date.now().toString());\n  showModal.value = false;\n};\n\nconst closeModal = () => {\n  showModal.value = false;\n};\n\nconst checkForUpdates = async () => {\n  // 检查是否应该显示提醒\n  if (!shouldShowReminder()) {\n    console.log('更新提醒被延迟，等待24小时后再提醒');\n    return;\n  }\n\n  try {\n    const result = await checkUpdate(config.version);\n    if (result && result.hasUpdate) {\n      updateInfo.value = result;\n      showModal.value = true;\n    }\n  } catch (error) {\n    console.error('检查更新失败:', error);\n  }\n};\n\nconst handleUpdate = async () => {\n  const version = updateInfo.value.latestVersion;\n\n  // Android APK 下载地址\n  const downloadUrl = `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}.apk`;\n\n  try {\n    // 获取代理节点\n    const proxyHosts = await getProxyNodes();\n    const proxyDownloadUrl = `${proxyHosts[0]}/${downloadUrl}`;\n\n    // 清除\"稍后提醒\"记录（用户选择更新后，下次应该正常提醒）\n    localStorage.removeItem(REMIND_LATER_KEY);\n\n    // 使用系统浏览器打开下载链接\n    window.open(proxyDownloadUrl, '_blank');\n\n    // 关闭弹窗\n    closeModal();\n  } catch (error) {\n    console.error('打开下载链接失败:', error);\n    // 回退到直接打开 GitHub Releases\n    const releaseUrl =\n      updateInfo.value.releaseInfo?.html_url ||\n      'https://github.com/algerkong/AlgerMusicPlayer/releases/latest';\n    window.open(releaseUrl, '_blank');\n    closeModal();\n  }\n};\n\nonMounted(() => {\n  // 延迟检查更新，确保应用完全加载\n  setTimeout(() => {\n    checkForUpdates();\n  }, 2000);\n});\n</script>\n\n<style scoped>\n/* 动画 */\n.update-modal-enter-active,\n.update-modal-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.update-modal-enter-from,\n.update-modal-leave-to {\n  opacity: 0;\n}\n\n.update-modal-enter-active .animate-slide-up,\n.update-modal-leave-active .animate-slide-up {\n  transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);\n}\n\n.update-modal-enter-from .animate-slide-up {\n  transform: translateY(100%);\n}\n\n.update-modal-leave-to .animate-slide-up {\n  transform: translateY(100%);\n}\n\n/* 更新内容样式 */\n:deep(h1) {\n  font-size: 1.25rem;\n  font-weight: 700;\n  margin-bottom: 0.75rem;\n}\n\n:deep(h2) {\n  font-size: 1.125rem;\n  font-weight: 600;\n  margin-bottom: 0.5rem;\n}\n\n:deep(h3) {\n  font-size: 1rem;\n  font-weight: 600;\n  margin-bottom: 0.5rem;\n}\n\n:deep(ul) {\n  list-style-type: disc;\n  padding-left: 1.25rem;\n  margin-bottom: 0.75rem;\n}\n\n:deep(ol) {\n  list-style-type: decimal;\n  padding-left: 1.25rem;\n  margin-bottom: 0.75rem;\n}\n\n:deep(li) {\n  margin-bottom: 0.25rem;\n}\n\n:deep(p) {\n  margin-bottom: 0.5rem;\n}\n\n:deep(code) {\n  padding: 0.125rem 0.375rem;\n  border-radius: 0.25rem;\n  background-color: rgba(0, 0, 0, 0.05);\n  font-size: 0.875rem;\n}\n\n:deep(a) {\n  color: #22c55e;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/MusicListNavigator.ts",
    "content": "import { Router } from 'vue-router';\n\nimport { useMusicStore } from '@/store/modules/music';\n\n/**\n * 导航到音乐列表页面的通用方法\n * @param router Vue路由实例\n * @param options 导航选项\n */\nexport function navigateToMusicList(\n  router: Router,\n  options: {\n    id?: string | number;\n    type?: 'album' | 'playlist' | 'dailyRecommend' | string;\n    name: string;\n    songList: any[];\n    listInfo?: any;\n    canRemove?: boolean;\n  }\n) {\n  const musicStore = useMusicStore();\n  const { id, type, name, songList, listInfo, canRemove = false } = options;\n\n  // 如果是每日推荐，不需要设置 musicStore，直接从 recommendStore 获取\n  if (type !== 'dailyRecommend') {\n    musicStore.setCurrentMusicList(songList, name, listInfo, canRemove);\n  } else {\n    // 确保 musicStore 的数据被清空，避免显示旧的列表\n    musicStore.clearCurrentMusicList();\n  }\n\n  // 路由跳转\n  if (id) {\n    router.push({\n      name: 'musicList',\n      params: { id },\n      query: { type }\n    });\n  } else {\n    router.push({\n      name: 'musicList',\n      query: { type: 'dailyRecommend' }\n    });\n  }\n}\n"
  },
  {
    "path": "src/renderer/components/common/PlayBottom.vue",
    "content": "<template>\n  <div v-if=\"isPlay && !isMobile\" class=\"bottom\" :style=\"{ height }\"></div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue';\n\nimport { usePlayerStore } from '@/store/modules/player';\nimport { isMobile } from '@/utils';\n\nconst playerStore = usePlayerStore();\nconst isPlay = computed(() => playerStore.playMusicUrl);\n\ndefineProps({\n  height: {\n    type: String,\n    default: undefined\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.bottom {\n  @apply h-28;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/PlayListsItem.vue",
    "content": ""
  },
  {
    "path": "src/renderer/components/common/PlaylistDrawer.vue",
    "content": "<template>\n  <n-drawer\n    :show=\"modelValue\"\n    :width=\"400\"\n    placement=\"right\"\n    @update:show=\"$emit('update:modelValue', $event)\"\n    :unstable-show-mask=\"false\"\n    :show-mask=\"false\"\n  >\n    <n-drawer-content :title=\"t('comp.playlistDrawer.title')\" class=\"mac-style-drawer\">\n      <n-scrollbar class=\"h-full\">\n        <div class=\"playlist-drawer\">\n          <!-- 创建新歌单按钮和表单 -->\n          <div class=\"create-playlist-section\">\n            <div\n              class=\"create-playlist-button\"\n              :class=\"{ 'is-expanded': isCreating }\"\n              @click=\"toggleCreateForm\"\n            >\n              <div class=\"create-playlist-icon\">\n                <i class=\"iconfont\" :class=\"isCreating ? 'ri-close-line' : 'ri-add-line'\"></i>\n              </div>\n              <div class=\"create-playlist-text\">\n                {{\n                  isCreating\n                    ? t('comp.playlistDrawer.cancelCreate')\n                    : t('comp.playlistDrawer.createPlaylist')\n                }}\n              </div>\n            </div>\n\n            <!-- 创建歌单表单 -->\n            <div class=\"create-playlist-form\" :class=\"{ 'is-visible': isCreating }\">\n              <n-input\n                v-model:value=\"formValue.name\"\n                :placeholder=\"t('comp.playlistDrawer.playlistName')\"\n                maxlength=\"40\"\n                class=\"mac-style-input\"\n                :status=\"inputError ? 'error' : undefined\"\n              >\n                <template #prefix>\n                  <i class=\"iconfont ri-music-2-line\"></i>\n                </template>\n              </n-input>\n              <div class=\"privacy-switch\">\n                <div class=\"privacy-label\">\n                  <i\n                    class=\"iconfont\"\n                    :class=\"formValue.privacy ? 'ri-lock-line' : 'ri-earth-line'\"\n                  ></i>\n                  <span>{{\n                    formValue.privacy\n                      ? t('comp.playlistDrawer.privatePlaylist')\n                      : t('comp.playlistDrawer.publicPlaylist')\n                  }}</span>\n                </div>\n                <n-switch v-model:value=\"formValue.privacy\" class=\"mac-style-switch\">\n                  <template #checked>{{ t('comp.playlistDrawer.private') }}</template>\n                  <template #unchecked>{{ t('comp.playlistDrawer.public') }}</template>\n                </n-switch>\n              </div>\n              <div class=\"form-actions\">\n                <n-button\n                  type=\"primary\"\n                  quaternary\n                  class=\"mac-style-button\"\n                  :loading=\"creating\"\n                  :disabled=\"!formValue.name\"\n                  @click=\"handleCreatePlaylist\"\n                >\n                  {{ t('comp.playlistDrawer.create') }}\n                </n-button>\n              </div>\n            </div>\n          </div>\n\n          <!-- 歌单列表 -->\n          <div class=\"playlist-list\">\n            <div\n              v-for=\"playlist in playlists\"\n              :key=\"playlist.id\"\n              class=\"playlist-item\"\n              @click=\"handleAddToPlaylist(playlist)\"\n            >\n              <n-image\n                :src=\"getImgUrl(playlist.coverImgUrl || playlist.picUrl, '100y100')\"\n                class=\"playlist-item-img\"\n                preview-disabled\n                :img-props=\"{\n                  crossorigin: 'anonymous'\n                }\"\n              />\n              <div class=\"playlist-item-info\">\n                <div class=\"playlist-item-name\">{{ playlist.name }}</div>\n                <div class=\"playlist-item-count\">\n                  {{ playlist.trackCount }}\n                  {{ t('comp.playlistDrawer.count') }}\n                </div>\n              </div>\n              <div class=\"playlist-item-action\">\n                <i class=\"iconfont ri-add-line\"></i>\n              </div>\n            </div>\n          </div>\n        </div>\n      </n-scrollbar>\n    </n-drawer-content>\n  </n-drawer>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useMessage } from 'naive-ui';\nimport { computed, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { createPlaylist, updatePlaylistTracks } from '@/api/music';\nimport { getUserPlaylist } from '@/api/user';\nimport { useUserStore } from '@/store';\nimport { getImgUrl } from '@/utils';\nimport { getLoginErrorMessage, hasPermission } from '@/utils/auth';\n\nconst store = useUserStore();\nconst { t } = useI18n();\nconst props = defineProps<{\n  modelValue: boolean;\n  songId?: number;\n}>();\n\nconst emit = defineEmits(['update:modelValue']);\n\nconst message = useMessage();\nconst playlists = ref<any[]>([]);\nconst creating = ref(false);\nconst isCreating = ref(false);\n\nconst formValue = ref({\n  name: '',\n  privacy: false\n});\n\nconst inputError = computed(() => {\n  return isCreating.value && !formValue.value.name;\n});\n\nconst toggleCreateForm = () => {\n  if (creating.value) return;\n  isCreating.value = !isCreating.value;\n  if (!isCreating.value) {\n    formValue.value.name = '';\n    formValue.value.privacy = false;\n  }\n};\n\n// 获取用户歌单\nconst fetchUserPlaylists = async () => {\n  try {\n    const { user } = store;\n    if (!user?.userId) {\n      message.error(t('comp.playlistDrawer.loginFirst'));\n      emit('update:modelValue', false);\n      return;\n    }\n\n    // 检查是否有真实登录权限\n    if (!hasPermission(true)) {\n      message.error(getLoginErrorMessage(true));\n      emit('update:modelValue', false);\n      return;\n    }\n\n    const res = await getUserPlaylist(user.userId, 999);\n    if (res.data?.playlist) {\n      playlists.value = res.data.playlist.filter((item: any) => item.userId === user.userId);\n    }\n  } catch (error) {\n    console.error('获取歌单失败:', error);\n    message.error(t('comp.playlistDrawer.getPlaylistFailed'));\n  }\n};\n\n// 添加到歌单\nconst handleAddToPlaylist = async (playlist: any) => {\n  if (!props.songId) return;\n\n  // 检查是否有真实登录权限\n  if (!hasPermission(true)) {\n    message.error(getLoginErrorMessage(true));\n    return;\n  }\n\n  try {\n    const res = await updatePlaylistTracks({\n      op: 'add',\n      pid: playlist.id,\n      tracks: props.songId.toString()\n    });\n    console.log('res.data', res.data);\n\n    if (res.status === 200) {\n      message.success(t('comp.playlistDrawer.addSuccess'));\n      emit('update:modelValue', false);\n    } else {\n      throw new Error(res.data?.msg || t('comp.playlistDrawer.addFailed'));\n    }\n  } catch (error: any) {\n    console.error('添加到歌单失败:', error);\n    message.error(error.message || t('comp.playlistDrawer.addFailed'));\n  }\n};\n\n// 创建歌单\nconst handleCreatePlaylist = async () => {\n  if (!formValue.value.name) {\n    message.error(t('comp.playlistDrawer.inputPlaylistName'));\n    return;\n  }\n\n  // 检查是否有真实登录权限\n  if (!hasPermission(true)) {\n    message.error(getLoginErrorMessage(true));\n    return;\n  }\n\n  try {\n    creating.value = true;\n\n    const res = await createPlaylist({\n      name: formValue.value.name,\n      privacy: formValue.value.privacy ? 10 : 0\n    });\n\n    if (res.data?.id) {\n      message.success(t('comp.playlistDrawer.createSuccess'));\n      isCreating.value = false;\n      formValue.value.name = '';\n      formValue.value.privacy = false;\n      await fetchUserPlaylists();\n    }\n  } catch (error) {\n    console.error('创建歌单失败:', error);\n    message.error(t('comp.playlistDrawer.createFailed'));\n  } finally {\n    creating.value = false;\n  }\n};\n\n// 监听显示状态变化\nwatch(\n  () => props.modelValue,\n  (newVal) => {\n    if (newVal) {\n      fetchUserPlaylists();\n    }\n  }\n);\n</script>\n\n<style lang=\"scss\" scoped>\n.mac-style-drawer {\n  @apply h-full;\n\n  :deep(.n-drawer-header__main) {\n    @apply text-base font-medium;\n  }\n\n  :deep(.n-drawer-content) {\n    @apply h-full;\n  }\n\n  :deep(.n-drawer-content-wrapper) {\n    @apply h-full;\n  }\n\n  :deep(.n-scrollbar-rail) {\n    @apply right-0.5;\n  }\n}\n\n.playlist-drawer {\n  @apply flex flex-col gap-6 py-6;\n}\n\n.create-playlist-section {\n  @apply flex flex-col;\n}\n\n.create-playlist-button {\n  @apply flex items-center gap-4 p-3 rounded-xl cursor-pointer transition-all duration-200\n         bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700;\n\n  &.is-expanded {\n    @apply bg-gray-100 dark:bg-gray-700;\n\n    .create-playlist-icon {\n      transform: rotate(45deg);\n    }\n  }\n\n  &-icon {\n    @apply w-10 h-10 rounded-xl bg-green-500 flex items-center justify-center text-white\n           transition-all duration-300;\n\n    .iconfont {\n      @apply text-xl transition-transform duration-300;\n    }\n  }\n\n  &-text {\n    @apply text-sm font-medium transition-colors duration-300;\n  }\n}\n\n.create-playlist-form {\n  @apply max-h-0 overflow-hidden transition-all duration-300 ease-in-out opacity-0;\n\n  &.is-visible {\n    @apply max-h-[200px] mt-4 opacity-100;\n  }\n\n  .mac-style-input {\n    @apply rounded-lg;\n    :deep(.n-input-wrapper) {\n      @apply bg-gray-50 dark:bg-gray-800 border-0;\n    }\n    :deep(.n-input__input) {\n      @apply text-sm;\n    }\n    :deep(.n-input__prefix) {\n      @apply text-gray-400;\n    }\n  }\n\n  .form-actions {\n    @apply mt-4;\n    .mac-style-button {\n      @apply w-full rounded-lg text-sm py-2 bg-green-500 hover:bg-green-600 text-white;\n    }\n  }\n}\n\n.privacy-switch {\n  @apply flex items-center justify-between mt-4 px-2;\n\n  .privacy-label {\n    @apply flex items-center gap-2;\n\n    .iconfont {\n      @apply text-base text-gray-500 dark:text-gray-400;\n    }\n    span {\n      @apply text-sm;\n    }\n  }\n\n  :deep(.n-switch) {\n    @apply h-5 min-w-[40px];\n  }\n}\n\n.playlist-list {\n  @apply flex flex-col gap-2 pb-40;\n}\n\n.playlist-item {\n  @apply flex items-center gap-3 p-2 rounded-xl cursor-pointer transition-all duration-200\n         hover:bg-gray-50 dark:hover:bg-gray-800;\n\n  &-img {\n    @apply w-10 h-10 rounded-xl;\n  }\n\n  &-info {\n    @apply flex-1 min-w-0;\n  }\n\n  &-name {\n    @apply text-sm font-medium truncate;\n  }\n\n  &-count {\n    @apply text-xs text-gray-500 dark:text-gray-400;\n  }\n\n  &-action {\n    @apply w-8 h-8 rounded-lg flex items-center justify-center\n           text-gray-400 hover:text-green-500 transition-colors duration-200;\n\n    .iconfont {\n      @apply text-xl;\n    }\n  }\n}\n\n:deep(.n-drawer-body-content-wrapper) {\n  padding-bottom: 0 !important;\n  padding-top: 0 !important;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/PlaylistItem.vue",
    "content": "<template>\n  <div class=\"playlist-item\" @click=\"handleClick\">\n    <n-image\n      :src=\"getImgUrl(item.coverImgUrl || item.picUrl || '', '100y100')\"\n      class=\"playlist-item-img\"\n      lazy\n      preview-disabled\n    />\n    <div class=\"playlist-item-info\">\n      <div class=\"playlist-item-name\">\n        <n-ellipsis :line-clamp=\"1\">{{ item.name }}</n-ellipsis>\n      </div>\n      <div class=\"playlist-item-desc\">\n        {{ getDescription() }}\n      </div>\n    </div>\n    <div v-if=\"showCount && item.count\" class=\"playlist-item-count\">\n      {{ item.count }}\n    </div>\n    <div v-if=\"showDelete\" class=\"playlist-item-delete\" @click.stop=\"handleDelete\">\n      <i class=\"iconfont icon-close\"></i>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n';\n\nimport type { PlaylistHistoryItem } from '@/hooks/PlaylistHistoryHook';\nimport { getImgUrl } from '@/utils';\n\ninterface Props {\n  item: PlaylistHistoryItem;\n  showCount?: boolean;\n  showDelete?: boolean;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  showCount: false,\n  showDelete: false\n});\n\nconst emit = defineEmits<{\n  click: [item: PlaylistHistoryItem];\n  delete: [item: PlaylistHistoryItem];\n}>();\n\nconst { t } = useI18n();\n\nconst getDescription = () => {\n  const parts: string[] = [];\n\n  if (props.item.trackCount !== undefined) {\n    parts.push(t('user.playlist.trackCount', { count: props.item.trackCount }));\n  }\n\n  if (props.item.creator?.nickname) {\n    parts.push(props.item.creator.nickname);\n  }\n\n  return parts.join(' · ') || t('history.noDescription');\n};\n\nconst handleClick = () => {\n  emit('click', props.item);\n};\n\nconst handleDelete = () => {\n  emit('delete', props.item);\n};\n</script>\n\n<style scoped lang=\"scss\">\n.playlist-item {\n  @apply flex items-center px-2 py-2 rounded-xl cursor-pointer;\n  @apply transition-all duration-200;\n  @apply bg-light-100 dark:bg-dark-100;\n  @apply hover:bg-light-200 dark:hover:bg-dark-200;\n  @apply mb-2;\n\n  &-img {\n    @apply flex items-center justify-center rounded-xl;\n    @apply w-[60px] h-[60px] flex-shrink-0;\n    @apply bg-light-300 dark:bg-dark-300;\n  }\n\n  &-info {\n    @apply ml-3 flex-1 min-w-0;\n  }\n\n  &-name {\n    @apply text-gray-900 dark:text-white text-base mb-1;\n  }\n\n  &-desc {\n    @apply text-sm text-gray-500 dark:text-gray-400;\n  }\n\n  &-count {\n    @apply px-4 text-lg text-center min-w-[60px];\n    @apply text-gray-600 dark:text-gray-400;\n  }\n\n  &-delete {\n    @apply cursor-pointer rounded-full border-2 w-8 h-8 flex justify-center items-center;\n    @apply border-gray-400 dark:border-gray-600;\n    @apply text-gray-600 dark:text-gray-400;\n    @apply hover:border-red-500 hover:text-red-500;\n    @apply transition-all;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/ResponsiveModal.vue",
    "content": "<template>\n  <Teleport to=\"body\">\n    <Transition name=\"fade\">\n      <div\n        v-if=\"show\"\n        class=\"fixed inset-0 z-[1000] flex items-center justify-center md:items-center items-end\"\n        @click=\"handleMaskClick\"\n      >\n        <!-- Overlay -->\n        <div class=\"absolute inset-0 bg-black/40 backdrop-blur-sm transition-opacity\"></div>\n\n        <!-- Content -->\n        <Transition :name=\"isMobile ? 'slide-up' : 'scale-fade'\">\n          <div\n            v-if=\"show\"\n            class=\"relative z-10 w-full bg-white dark:bg-[#1c1c1e] shadow-2xl overflow-hidden flex flex-col max-h-[85vh]\"\n            :class=\"[\n              isMobile\n                ? 'rounded-t-[20px] pb-safe'\n                : 'md:max-w-[720px] md:rounded-2xl'\n            ]\"\n            @click.stop\n          >\n            <!-- Header -->\n            <div\n              class=\"flex items-center justify-between px-4 py-3 border-b border-gray-100 dark:border-white/5 shrink-0\"\n            >\n              <h3 class=\"text-[15px] font-semibold text-gray-900 dark:text-white truncate\">\n                {{ title }}\n              </h3>\n              <button\n                class=\"p-1 -mr-1 rounded-full text-gray-400 hover:bg-gray-100 dark:hover:bg-white/10 transition-colors\"\n                @click=\"close\"\n              >\n                <i class=\"ri-close-line text-lg\"></i>\n              </button>\n            </div>\n\n            <!-- Body -->\n            <div class=\"flex-1 overflow-y-auto overscroll-contain px-4 py-3\">\n              <slot></slot>\n            </div>\n\n            <!-- Footer -->\n            <div\n              v-if=\"$slots.footer\"\n              class=\"px-4 py-3 border-t border-gray-100 dark:border-white/5 shrink-0 bg-gray-50/50 dark:bg-white/5 backdrop-blur-xl\"\n            >\n              <slot name=\"footer\"></slot>\n            </div>\n          </div>\n        </Transition>\n      </div>\n    </Transition>\n  </Teleport>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted, ref, watch } from 'vue';\n\nconst props = defineProps<{\n  modelValue: boolean;\n  title?: string;\n}>();\n\nconst emit = defineEmits<{\n  (e: 'update:modelValue', value: boolean): void;\n  (e: 'close'): void;\n}>();\n\nconst show = computed({\n  get: () => props.modelValue,\n  set: (val) => emit('update:modelValue', val)\n});\n\nconst isMobile = ref(false);\n\nconst checkMobile = () => {\n  isMobile.value = window.innerWidth < 768;\n};\n\nconst close = () => {\n  show.value = false;\n  emit('close');\n};\n\nconst handleMaskClick = () => {\n  close();\n};\n\nonMounted(() => {\n  checkMobile();\n  window.addEventListener('resize', checkMobile);\n});\n\nonUnmounted(() => {\n  window.removeEventListener('resize', checkMobile);\n});\n\n// Prevent body scroll when modal is open\nwatch(show, (val) => {\n  if (val) {\n    document.body.style.overflow = 'hidden';\n  } else {\n    document.body.style.overflow = '';\n  }\n});\n</script>\n\n<style scoped>\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n\n/* PC Scale Fade Transition */\n.scale-fade-enter-active,\n.scale-fade-leave-active {\n  transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);\n}\n\n.scale-fade-enter-from,\n.scale-fade-leave-to {\n  opacity: 0;\n  transform: scale(0.95);\n}\n\n/* Mobile Slide Up Transition */\n.slide-up-enter-active,\n.slide-up-leave-active {\n  transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);\n}\n\n.slide-up-enter-from,\n.slide-up-leave-to {\n  transform: translateY(100%);\n}\n\n.pb-safe {\n  padding-bottom: env(safe-area-inset-bottom);\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/SearchItem.vue",
    "content": "<template>\n  <div class=\"search-item\" :class=\"[shape, item.type]\" @click=\"handleClick\">\n    <div class=\"search-item-img\">\n      <n-image\n        class=\"w-full h-full\"\n        :src=\"getImgUrl(item.picUrl, item.type === 'mv' ? '320y180' : '200y200')\"\n        lazy\n        preview-disabled\n      />\n      <div v-if=\"item.type === 'mv'\" class=\"play\">\n        <i class=\"iconfont icon icon-play\"></i>\n      </div>\n    </div>\n    <div class=\"search-item-info\">\n      <p class=\"search-item-name\">{{ item.name }}</p>\n      <p class=\"search-item-artist\">{{ item.desc }}</p>\n    </div>\n\n    <div v-if=\"item.type === '专辑'\" class=\"search-item-size\">\n      <i class=\"ri-music-2-line\"></i>\n      <span>{{ item.size }}</span>\n    </div>\n\n    <mv-player\n      v-if=\"item.type === 'mv'\"\n      v-model:show=\"showPop\"\n      :current-mv=\"getCurrentMv()\"\n      no-list\n    />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useRouter } from 'vue-router';\n\nimport { getAlbum, getListDetail } from '@/api/list';\nimport MvPlayer from '@/components/MvPlayer.vue';\nimport { useMusicStore } from '@/store/modules/music';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { IMvItem } from '@/types/mv';\nimport { getImgUrl } from '@/utils';\n\nconst props = withDefaults(\n  defineProps<{\n    shape?: 'square' | 'rectangle';\n    zIndex?: number;\n    item: {\n      picUrl: string;\n      name: string;\n      desc: string;\n      type: string;\n      [key: string]: any;\n    };\n  }>(),\n  {\n    shape: 'rectangle'\n  }\n);\n\nconst songList = ref<any[]>([]);\n\nconst showPop = ref(false);\nconst listInfo = ref<any>(null);\n\nconst playerStore = usePlayerStore();\nconst router = useRouter();\nconst musicStore = useMusicStore();\n\nconst getCurrentMv = () => {\n  return {\n    id: props.item.id,\n    name: props.item.name\n  } as unknown as IMvItem;\n};\n\nconst handleClick = async () => {\n  listInfo.value = null;\n  if (props.item.type === '专辑') {\n    const res = await getAlbum(props.item.id);\n    songList.value = res.data.songs.map((song: any) => {\n      song.al.picUrl = song.al.picUrl || props.item.picUrl;\n      return song;\n    });\n    listInfo.value = {\n      ...res.data.album,\n      creator: {\n        avatarUrl: res.data.album.artist.img1v1Url,\n        nickname: `${res.data.album.artist.name} - ${res.data.album.company}`\n      },\n      description: res.data.album.description\n    };\n\n    // 保存数据到store\n    musicStore.setCurrentMusicList(songList.value, props.item.name, listInfo.value, false);\n\n    // 使用路由跳转\n    router.push({\n      name: 'musicList',\n      params: { id: props.item.id },\n      query: { type: 'album' }\n    });\n  } else if (props.item.type === 'playlist') {\n    const res = await getListDetail(props.item.id);\n    songList.value = res.data.playlist.tracks;\n    listInfo.value = res.data.playlist;\n\n    // 保存数据到store\n    musicStore.setCurrentMusicList(songList.value, props.item.name, listInfo.value, false);\n\n    // 使用路由跳转\n    router.push({\n      name: 'musicList',\n      params: { id: props.item.id },\n      query: { type: 'playlist' }\n    });\n  } else if (props.item.type === 'mv') {\n    handleShowMv();\n  }\n};\n\nconst handleShowMv = async () => {\n  playerStore.handlePause();\n  showPop.value = true;\n};\n</script>\n\n<style scoped lang=\"scss\">\n.search-item {\n  @apply rounded-lg p-0 flex items-center hover:bg-transparent transition cursor-pointer border-none;\n\n  &.square {\n    @apply flex-col relative;\n\n    .search-item-img {\n      @apply w-full aspect-square mb-2 mr-0 rounded-lg overflow-hidden hover:shadow-xl transition-all duration-300 shadow-sm shadow-black/20 dark:shadow-white/20;\n      img {\n        @apply object-cover w-full h-full transition-transform duration-500;\n      }\n    }\n\n    .search-item-info {\n      @apply w-full text-left px-0;\n\n      .search-item-name {\n        @apply truncate mb-1 font-medium text-base text-gray-800 dark:text-gray-200;\n      }\n\n      .search-item-artist {\n        @apply truncate text-sm text-gray-500 dark:text-gray-400;\n      }\n    }\n\n    .search-item-size {\n      @apply absolute top-2 right-2 text-xs text-white px-2 py-1 rounded-full bg-black/30 backdrop-blur-sm;\n      i {\n        @apply text-xs;\n      }\n    }\n  }\n\n  &.rectangle {\n    @apply hover:bg-light-200 dark:hover:bg-dark-200 p-3;\n    .search-item-img {\n      @apply w-12 h-12 mr-4 rounded-lg overflow-hidden;\n    }\n  }\n\n  .search-item-info {\n    @apply flex-1 overflow-hidden;\n    &-name {\n      @apply text-white text-sm text-center;\n    }\n    &-artist {\n      @apply text-gray-400 text-xs text-center;\n    }\n  }\n}\n\n.search-item.mv {\n  &:hover {\n    .play {\n      @apply opacity-60;\n    }\n  }\n  .search-item-img {\n    width: 160px !important;\n    height: 90px !important;\n    @apply rounded-lg relative;\n  }\n  .play {\n    @apply absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity;\n    .icon {\n      @apply text-white text-5xl;\n    }\n  }\n}\n\n.search-item-size {\n  @apply flex items-center gap-2 text-gray-400;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/SongItem.vue",
    "content": "<template>\n  <component\n    :is=\"renderComponent\"\n    :item=\"item\"\n    :favorite=\"favorite\"\n    :selectable=\"selectable\"\n    :selected=\"selected\"\n    :can-remove=\"canRemove\"\n    :is-next=\"isNext\"\n    :index=\"index\"\n    @play=\"(...args) => $emit('play', ...args)\"\n    @select=\"(...args) => $emit('select', ...args)\"\n    @remove-song=\"(...args) => $emit('remove-song', ...args)\"\n  />\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed } from 'vue';\n\nimport type { SongResult } from '@/types/music';\n\nimport CompactSongItem from './songItemCom/CompactSongItem.vue';\nimport ListSongItem from './songItemCom/ListSongItem.vue';\nimport MiniSongItem from './songItemCom/MiniSongItem.vue';\nimport StandardSongItem from './songItemCom/StandardSongItem.vue';\n\nconst props = withDefaults(\n  defineProps<{\n    item: SongResult;\n    mini?: boolean;\n    list?: boolean;\n    compact?: boolean;\n    favorite?: boolean;\n    selectable?: boolean;\n    selected?: boolean;\n    canRemove?: boolean;\n    isNext?: boolean;\n    index?: number;\n  }>(),\n  {\n    mini: false,\n    list: false,\n    compact: false,\n    favorite: true,\n    selectable: false,\n    selected: false,\n    canRemove: false,\n    isNext: false,\n    index: undefined\n  }\n);\n\ndefineEmits(['play', 'select', 'remove-song']);\n\n// 根据属性决定渲染哪个组件\nconst renderComponent = computed(() => {\n  if (props.mini) return MiniSongItem;\n  if (props.list) return ListSongItem;\n  if (props.compact) return CompactSongItem;\n  return StandardSongItem;\n});\n</script>\n"
  },
  {
    "path": "src/renderer/components/common/UpdateModal.vue",
    "content": "<template>\n  <n-modal\n    v-model:show=\"showModal\"\n    preset=\"dialog\"\n    :show-icon=\"false\"\n    :mask-closable=\"!downloading\"\n    :closable=\"!downloading\"\n    class=\"update-app-modal\"\n    style=\"width: 800px; max-width: 90vw\"\n  >\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <div class=\"app-icon\">\n          <img src=\"@/assets/logo.png\" alt=\"App Icon\" />\n        </div>\n        <div class=\"app-info\">\n          <h2 class=\"app-name\">{{ t('comp.update.title') }} {{ updateInfo.latestVersion }}</h2>\n          <p class=\"app-desc mb-2\">\n            {{ t('comp.update.currentVersion') }} {{ updateInfo.currentVersion }}\n          </p>\n        </div>\n      </div>\n      <div class=\"update-info\">\n        <n-scrollbar style=\"max-height: 300px\">\n          <div class=\"update-body\" v-html=\"parsedReleaseNotes\"></div>\n        </n-scrollbar>\n      </div>\n      <div v-if=\"downloading\" class=\"download-status mt-6\">\n        <div class=\"flex items-center justify-between mb-2\">\n          <span class=\"text-sm text-gray-500\">{{ downloadStatus }}</span>\n          <span class=\"text-sm font-medium\">{{ downloadProgress }}%</span>\n        </div>\n        <div class=\"progress-bar-wrapper\">\n          <div class=\"progress-bar\" :style=\"{ width: `${downloadProgress}%` }\"></div>\n        </div>\n      </div>\n      <div class=\"modal-actions\" :class=\"{ 'mt-6': !downloading }\">\n        <n-button class=\"cancel-btn\" :disabled=\"downloading\" @click=\"closeModal\">\n          {{ t('comp.update.cancel') }}\n        </n-button>\n        <n-button\n          v-if=\"!downloading\"\n          type=\"primary\"\n          class=\"update-btn\"\n          :disabled=\"downloading\"\n          @click=\"handleUpdate\"\n        >\n          {{ downloadBtnText }}\n        </n-button>\n        <!-- 后台下载 -->\n        <n-button v-else class=\"update-btn\" type=\"primary\" @click=\"closeModal\">\n          {{ t('comp.update.backgroundDownload') }}\n        </n-button>\n      </div>\n      <div v-if=\"!downloading\" class=\"modal-desc mt-4 text-center\">\n        <p class=\"text-xs text-gray-400\">\n          {{ t('comp.installApp.downloadProblem') }}\n          <a\n            class=\"text-green-500\"\n            target=\"_blank\"\n            href=\"https://github.com/algerkong/AlgerMusicPlayer/releases\"\n            >GitHub</a\n          >\n          {{ t('comp.installApp.downloadProblemLinkText') }}\n        </p>\n      </div>\n    </div>\n  </n-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { marked } from 'marked';\nimport { computed, h, onMounted, onUnmounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { useSettingsStore } from '@/store/modules/settings';\nimport { checkUpdate, getProxyNodes, UpdateResult } from '@/utils/update';\n\nimport config from '../../../../package.json';\n\nconst { t } = useI18n();\nconst dialog = useDialog();\nconst message = useMessage();\n\n// 配置 marked\nmarked.setOptions({\n  breaks: true, // 支持 GitHub 风格的换行\n  gfm: true // 启用 GitHub 风格的 Markdown\n});\n\nconst settingsStore = useSettingsStore();\n\nconst showModal = computed({\n  get: () => settingsStore.showUpdateModal,\n  set: (val) => settingsStore.setShowUpdateModal(val)\n});\n\nconst updateInfo = ref<UpdateResult>({\n  hasUpdate: false,\n  latestVersion: '',\n  currentVersion: config.version,\n  releaseInfo: null\n});\n\n// 解析 Markdown\nconst parsedReleaseNotes = computed(() => {\n  if (!updateInfo.value.releaseInfo?.body) return '';\n  try {\n    return marked.parse(updateInfo.value.releaseInfo.body);\n  } catch (error) {\n    console.error('Error parsing markdown:', error);\n    return updateInfo.value.releaseInfo.body;\n  }\n});\n\nconst closeModal = () => {\n  showModal.value = false;\n};\n\nconst checkForUpdates = async () => {\n  try {\n    const result = await checkUpdate(config.version);\n    if (result) {\n      updateInfo.value = result;\n      showModal.value = true;\n    }\n  } catch (error) {\n    console.error('检查更新失败:', error);\n  }\n};\n\nconst downloading = ref(false);\nconst downloadProgress = ref(0);\nconst downloadStatus = ref(t('comp.update.prepareDownload'));\nconst downloadBtnText = computed(() => {\n  if (downloading.value) return t('comp.update.downloading');\n  return t('comp.update.nowUpdate');\n});\n\n// 下载完成后的文件路径\nconst downloadedFilePath = ref('');\n// 防止对话框重复弹出\nconst isDialogShown = ref(false);\n\n// 处理下载状态更新\nconst handleDownloadProgress = (_event: any, progress: number, status: string) => {\n  downloadProgress.value = progress;\n  downloadStatus.value = status;\n};\n\n// 处理下载完成\nconst handleDownloadComplete = (_event: any, success: boolean, filePath: string) => {\n  downloading.value = false;\n  closeModal();\n\n  if (success && !isDialogShown.value) {\n    downloadedFilePath.value = filePath;\n    isDialogShown.value = true;\n\n    // 复制文件路径到剪贴板\n    const copyFilePath = () => {\n      navigator.clipboard\n        .writeText(filePath)\n        .then(() => {\n          message.success(t('comp.update.copySuccess'));\n        })\n        .catch(() => {\n          message.error(t('comp.update.copyFailed'));\n        });\n    };\n\n    // 使用naive-ui的对话框询问用户是否安装\n    const dialogRef = dialog.create({\n      title: t('comp.update.installConfirmTitle'),\n      content: () =>\n        h('div', { class: 'update-dialog-content' }, [\n          h('p', { class: 'content-text' }, t('comp.update.installConfirmContent')),\n          h('div', { class: 'divider' }),\n          h('p', { class: 'manual-tip' }, t('comp.update.manualInstallTip')),\n          h('div', { class: 'file-path-container' }, [\n            h('div', { class: 'file-path-box' }, [\n              h('p', { class: 'file-path-label' }, t('comp.update.fileLocation')),\n              h('div', { class: 'file-path-value' }, filePath)\n            ]),\n            h(\n              'div',\n              {\n                class: 'copy-btn',\n                onClick: copyFilePath\n              },\n              [h('i', { class: 'ri-file-copy-line' }), h('span', t('comp.update.copy'))]\n            )\n          ])\n        ]),\n      positiveText: t('comp.update.yesInstall'),\n      negativeText: t('comp.update.noThanks'),\n      onPositiveClick: () => {\n        window.electron.ipcRenderer.send('install-update', filePath);\n      },\n      onNegativeClick: () => {\n        closeModal();\n        // 关闭当前窗口\n        dialogRef.destroy();\n      },\n      onClose: () => {\n        isDialogShown.value = false;\n      }\n    });\n  } else if (!success) {\n    message.error(t('comp.update.downloadFailed'));\n  }\n};\n\n// 监听下载事件\nonMounted(() => {\n  checkForUpdates();\n  // 确保事件监听器只注册一次\n  window.electron.ipcRenderer.removeListener('download-progress', handleDownloadProgress);\n  window.electron.ipcRenderer.removeListener('download-complete', handleDownloadComplete);\n\n  window.electron.ipcRenderer.on('download-progress', handleDownloadProgress);\n  window.electron.ipcRenderer.on('download-complete', handleDownloadComplete);\n});\n\n// 清理事件监听\nonUnmounted(() => {\n  window.electron.ipcRenderer.removeListener('download-progress', handleDownloadProgress);\n  window.electron.ipcRenderer.removeListener('download-complete', handleDownloadComplete);\n  isDialogShown.value = false;\n});\n\nconst handleUpdate = async () => {\n  const assets = updateInfo.value.releaseInfo?.assets || [];\n  const { platform } = window.electron.process;\n  const arch = window.electron.ipcRenderer.sendSync('get-arch');\n  const version = updateInfo.value.latestVersion;\n  const downUrls = {\n    win32: {\n      all: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-win.exe`,\n      x64: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-win-x64.exe`,\n      ia32: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-win-ia32.exe`\n    },\n    darwin: {\n      x64: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-x64.dmg`,\n      arm64: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-arm64.dmg`\n    },\n    linux: {\n      AppImage: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-linux-x64.AppImage`,\n      deb: `https://github.com/algerkong/AlgerMusicPlayer/releases/download/v${version}/AlgerMusicPlayer-${version}-linux-x64.deb`\n    }\n  };\n\n  let downloadUrl = '';\n\n  // 根据平台和架构选择对应的安装包\n  if (platform === 'darwin') {\n    // macOS - 根据芯片架构选择对应的 DMG\n    const macArch = arch === 'arm64' ? 'arm64' : 'x64';\n    const macAsset = assets.find(\n      (asset) => asset.name.includes('mac') && asset.name.includes(macArch)\n    );\n    downloadUrl = macAsset?.browser_download_url || downUrls.darwin[macArch] || '';\n  } else if (platform === 'win32') {\n    // Windows\n    const winAsset = assets.find(\n      (asset) =>\n        asset.name.includes('win') &&\n        (arch === 'x64' ? asset.name.includes('x64') : asset.name.includes('ia32'))\n    );\n    downloadUrl =\n      winAsset?.browser_download_url || downUrls.win32[arch] || downUrls.win32.all || '';\n  } else if (platform === 'linux') {\n    // Linux\n    const linuxAsset = assets.find(\n      (asset) =>\n        (asset.name.endsWith('.AppImage') || asset.name.endsWith('.deb')) &&\n        asset.name.includes('x64')\n    );\n    downloadUrl = linuxAsset?.browser_download_url || downUrls.linux[arch] || '';\n  }\n\n  if (downloadUrl) {\n    try {\n      downloading.value = true;\n      downloadStatus.value = t('comp.update.prepareDownload');\n      isDialogShown.value = false;\n\n      // 获取代理节点列表\n      const proxyHosts = await getProxyNodes();\n      const proxyDownloadUrl = `${proxyHosts[0]}/${downloadUrl}`;\n\n      // 发送所有可能的下载地址到主进程\n      window.electron.ipcRenderer.send('start-download', proxyDownloadUrl);\n    } catch (error) {\n      downloading.value = false;\n      message.error(t('comp.update.startFailed'));\n      console.error('下载失败:', error);\n    }\n  } else {\n    message.error(t('comp.update.noDownloadUrl'));\n    window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');\n  }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.update-app-modal {\n  :deep(.n-modal) {\n    @apply max-w-4xl;\n  }\n  .modal-content {\n    @apply p-6 pb-4;\n    .modal-header {\n      @apply flex items-center mb-6;\n      .app-icon {\n        @apply w-24 h-24 mr-6 rounded-2xl overflow-hidden;\n        img {\n          @apply w-full h-full object-cover;\n        }\n      }\n      .app-info {\n        @apply flex-1;\n        .app-name {\n          @apply text-2xl font-bold mb-2;\n        }\n        .app-desc {\n          @apply text-base text-gray-400;\n        }\n      }\n    }\n    .update-info {\n      @apply mb-6 rounded-lg bg-gray-50 dark:bg-gray-800;\n      .update-title {\n        @apply text-base font-medium p-4 pb-2;\n      }\n      .update-body {\n        @apply p-4 pt-2 text-gray-600 dark:text-gray-300 rounded-lg overflow-hidden;\n\n        :deep(h1) {\n          @apply text-xl font-bold mb-3;\n        }\n        :deep(h2) {\n          @apply text-lg font-bold mb-3;\n        }\n        :deep(h3) {\n          @apply text-base font-bold mb-2;\n        }\n        :deep(p) {\n          @apply mb-3 leading-relaxed;\n        }\n        :deep(ul) {\n          @apply list-disc list-inside mb-3;\n        }\n        :deep(ol) {\n          @apply list-decimal list-inside mb-3;\n        }\n        :deep(li) {\n          @apply mb-2 leading-relaxed;\n        }\n        :deep(code) {\n          @apply px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200;\n        }\n        :deep(pre) {\n          @apply p-3 rounded bg-gray-100 dark:bg-gray-700 overflow-x-auto mb-3;\n          code {\n            @apply bg-transparent p-0;\n          }\n        }\n        :deep(blockquote) {\n          @apply pl-4 border-l-4 border-gray-200 dark:border-gray-600 mb-3;\n        }\n        :deep(a) {\n          @apply text-green-500 hover:text-green-600 dark:hover:text-green-400;\n        }\n        :deep(hr) {\n          @apply my-4 border-gray-200 dark:border-gray-600;\n        }\n        :deep(table) {\n          @apply w-full mb-3;\n          th,\n          td {\n            @apply px-3 py-2 border border-gray-200 dark:border-gray-600;\n          }\n          th {\n            @apply bg-gray-100 dark:bg-gray-700;\n          }\n        }\n      }\n    }\n    .download-status {\n      @apply p-2;\n      .progress-bar-wrapper {\n        @apply w-full h-2 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden;\n        .progress-bar {\n          @apply h-full bg-green-500 rounded-full transition-all duration-300 ease-out;\n          box-shadow: 0 0 10px rgba(34, 197, 94, 0.5);\n        }\n      }\n    }\n    .modal-actions {\n      @apply flex gap-4;\n      .n-button {\n        @apply flex-1 text-base py-2;\n      }\n      .cancel-btn {\n        @apply bg-gray-800 text-gray-300 border-none;\n        &:hover {\n          @apply bg-gray-700;\n        }\n        &:disabled {\n          @apply opacity-50 cursor-not-allowed;\n        }\n      }\n      .update-btn {\n        @apply bg-green-600 border-none;\n        &:hover {\n          @apply bg-green-500;\n        }\n        &:disabled {\n          @apply opacity-50 cursor-not-allowed;\n        }\n      }\n    }\n  }\n}\n</style>\n\n<style lang=\"scss\" scoped>\n/* 对话框内容样式 */\n.update-dialog-content {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n\n  .content-text {\n    font-size: 16px;\n    font-weight: 500;\n  }\n\n  .divider {\n    width: 100%;\n    height: 1px;\n    background-color: #e5e7eb;\n    margin: 4px 0;\n  }\n\n  .manual-tip {\n    font-size: 14px;\n    color: #6b7280;\n  }\n\n  .file-path-container {\n    display: flex;\n    align-items: center;\n    gap: 12px;\n    margin-top: 8px;\n\n    .file-path-box {\n      flex: 1;\n\n      .file-path-label {\n        font-size: 12px;\n        color: #6b7280;\n        margin-bottom: 4px;\n      }\n\n      .file-path-value {\n        padding: 8px;\n        border-radius: 4px;\n        background-color: #f3f4f6;\n        font-size: 12px;\n        font-family: monospace;\n        color: #1f2937;\n        word-break: break-all;\n      }\n    }\n\n    .copy-btn {\n      display: flex;\n      align-items: center;\n      gap: 4px;\n      padding: 8px 12px;\n      border-radius: 4px;\n      background-color: #e5e7eb;\n      color: #4b5563;\n      font-size: 12px;\n      cursor: pointer;\n      transition: background-color 0.2s;\n\n      &:hover {\n        background-color: #d1d5db;\n      }\n\n      i {\n        font-size: 14px;\n      }\n    }\n  }\n}\n\n/* 深色模式样式 */\n.dark .update-dialog-content {\n  .divider {\n    background-color: #374151;\n  }\n\n  .manual-tip {\n    color: #9ca3af;\n  }\n\n  .file-path-container {\n    .file-path-box {\n      .file-path-label {\n        color: #9ca3af;\n      }\n\n      .file-path-value {\n        background-color: #1f2937;\n        color: #d1d5db;\n      }\n    }\n\n    .copy-btn {\n      background-color: #374151;\n      color: #d1d5db;\n\n      &:hover {\n        background-color: #4b5563;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/songItemCom/BaseSongItem.vue",
    "content": "<template>\n  <div\n    class=\"song-item\"\n    @contextmenu.prevent=\"handleContextMenu\"\n    @mouseenter=\"handleMouseEnter\"\n    @mouseleave=\"handleMouseLeave\"\n    @dblclick.stop=\"playMusicEvent(item)\"\n  >\n    <slot name=\"index\"></slot>\n    <slot name=\"select\" v-if=\"selectable\"></slot>\n    <slot name=\"image\"></slot>\n    <slot name=\"content\"></slot>\n    <slot name=\"operating\"></slot>\n\n    <song-item-dropdown\n      v-if=\"isElectron\"\n      :item=\"item\"\n      :show=\"showDropdown\"\n      :x=\"dropdownX\"\n      :y=\"dropdownY\"\n      :is-favorite=\"isFavorite\"\n      :is-dislike=\"isDislike\"\n      :can-remove=\"canRemove\"\n      @update:show=\"showDropdown = $event\"\n      @play=\"playMusicEvent(item)\"\n      @play-next=\"handlePlayNext\"\n      @download=\"downloadMusic(item)\"\n      @toggle-favorite=\"toggleFavorite\"\n      @toggle-dislike=\"toggleDislike\"\n      @remove=\"$emit('remove-song', $event)\"\n    />\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useSongItem } from '@/hooks/useSongItem';\nimport type { SongResult } from '@/types/music';\nimport { isElectron } from '@/utils';\n\nimport SongItemDropdown from './SongItemDropdown.vue';\n\nconst props = defineProps<{\n  item: SongResult;\n  selectable?: boolean;\n  selected?: boolean;\n  canRemove?: boolean;\n  isNext?: boolean;\n  index?: number;\n}>();\n\nconst emits = defineEmits(['play', 'select', 'remove-song']);\n\n// 使用公共逻辑\nconst {\n  playLoading,\n  isPlaying,\n  isFavorite,\n  isDislike,\n  artists,\n  showDropdown,\n  dropdownX,\n  dropdownY,\n  isHovering,\n  handleImageLoad,\n  playMusicEvent,\n  toggleFavorite,\n  toggleDislike,\n  handlePlayNext,\n  handleContextMenu,\n  handleMenuClick,\n  handleArtistClick,\n  handleMouseEnter,\n  handleMouseLeave,\n  downloadMusic\n} = useSongItem(props);\n\n// 处理图片加载\nconst imageLoad = async (event: Event) => {\n  const target = event.target as HTMLImageElement;\n  if (!target) return;\n  await handleImageLoad(target);\n};\n\n// 切换选择状态\nconst toggleSelect = () => {\n  emits('select', props.item.id, !props.selected);\n};\n\n// 把图片处理、艺术家处理等公共方法暴露给子组件\ndefineExpose({\n  imageLoad,\n  toggleSelect,\n  handleArtistClick,\n  handleMenuClick,\n  playMusicEvent,\n  toggleFavorite,\n  handlePlayNext,\n  playLoading,\n  isPlaying,\n  isFavorite,\n  isDislike,\n  artists,\n  isHovering\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.song-item {\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  @apply rounded-3xl p-3 flex items-center transition bg-transparent dark:text-white text-gray-900;\n}\n\n.text-ellipsis {\n  width: 100%;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/songItemCom/CompactSongItem.vue",
    "content": "<template>\n  <base-song-item\n    :item=\"item\"\n    :selectable=\"selectable\"\n    :selected=\"selected\"\n    :can-remove=\"canRemove\"\n    :is-next=\"isNext\"\n    :index=\"index\"\n    @play=\"(...args) => $emit('play', ...args)\"\n    @select=\"(...args) => $emit('select', ...args)\"\n    @remove-song=\"(...args) => $emit('remove-song', ...args)\"\n    class=\"compact-song-item\"\n    ref=\"baseItem\"\n  >\n    <!-- 索引插槽 -->\n    <template #index>\n      <div\n        v-if=\"index !== undefined\"\n        class=\"song-item-index\"\n        :class=\"{ 'text-green-500': isPlaying }\"\n      >\n        {{ index + 1 }}\n      </div>\n    </template>\n\n    <!-- 选择框插槽 -->\n    <template #select>\n      <div v-if=\"baseItem && selectable\" class=\"song-item-select\" @click.stop=\"onToggleSelect\">\n        <n-checkbox :checked=\"selected\" />\n      </div>\n    </template>\n\n    <!-- 内容插槽 -->\n    <template #content>\n      <div class=\"song-item-content-compact\">\n        <div class=\"song-item-content-compact-wrapper\">\n          <div class=\"song-item-content-compact-title\">\n            <n-ellipsis\n              class=\"text-ellipsis\"\n              line-clamp=\"1\"\n              :class=\"{ 'text-green-500': isPlaying }\"\n            >\n              {{ item.name }}\n            </n-ellipsis>\n          </div>\n          <div class=\"song-item-content-compact-artist\">\n            <n-ellipsis line-clamp=\"1\">\n              <template v-for=\"(artist, index) in artists\" :key=\"index\">\n                <span\n                  class=\"cursor-pointer hover:text-green-500\"\n                  @click.stop=\"onArtistClick(artist.id)\"\n                  >{{ artist.name }}</span\n                >\n                <span v-if=\"index < artists.length - 1\"> / </span>\n              </template>\n            </n-ellipsis>\n          </div>\n        </div>\n        <div class=\"song-item-content-compact-album\">\n          <n-ellipsis line-clamp=\"1\">{{ item.al?.name || '-' }}</n-ellipsis>\n        </div>\n        <div class=\"song-item-content-compact-duration\">\n          {{ formatDuration(getDuration(item)) }}\n        </div>\n      </div>\n    </template>\n\n    <!-- 操作插槽 -->\n    <template #operating>\n      <div class=\"song-item-operating-compact\">\n        <div\n          v-if=\"favorite\"\n          class=\"song-item-operating-like\"\n          :class=\"{ 'opacity-0': !isHovering && !isFavorite }\"\n        >\n          <i\n            class=\"iconfont icon-likefill\"\n            :class=\"{ 'like-active': isFavorite }\"\n            @click.stop=\"onToggleFavorite\"\n          ></i>\n        </div>\n        <div\n          class=\"song-item-operating-play animate__animated\"\n          :class=\"{\n            'bg-green-600': isPlaying,\n            animate__flipInY: playLoading,\n            'opacity-0': !isHovering && !isPlaying\n          }\"\n          @click=\"onPlayMusic\"\n        >\n          <i v-if=\"isPlaying && play\" class=\"iconfont icon-stop\"></i>\n          <i v-else class=\"iconfont icon-playfill\"></i>\n        </div>\n        <div\n          class=\"song-item-operating-menu\"\n          @click.stop=\"onMenuClick\"\n          :class=\"{ 'opacity-0': !isHovering && !isPlaying }\"\n        >\n          <i class=\"iconfont ri-more-fill\"></i>\n        </div>\n      </div>\n    </template>\n  </base-song-item>\n</template>\n\n<script lang=\"ts\" setup>\nimport { NCheckbox, NEllipsis } from 'naive-ui';\nimport { computed, ref } from 'vue';\n\nimport { usePlayerStore } from '@/store';\nimport type { SongResult } from '@/types/music';\n\nimport BaseSongItem from './BaseSongItem.vue';\n\nconst playerStore = usePlayerStore();\n\nconst props = withDefaults(\n  defineProps<{\n    item: SongResult;\n    favorite?: boolean;\n    selectable?: boolean;\n    selected?: boolean;\n    canRemove?: boolean;\n    isNext?: boolean;\n    index?: number;\n  }>(),\n  {\n    favorite: true,\n    selectable: false,\n    selected: false,\n    canRemove: false,\n    isNext: false,\n    index: undefined\n  }\n);\n\nconst emit = defineEmits(['play', 'select', 'remove-song']);\nconst baseItem = ref<InstanceType<typeof BaseSongItem>>();\n\n// 从基础组件获取响应式状态\nconst play = computed(() => playerStore.isPlay);\nconst isPlaying = computed(() => baseItem.value?.isPlaying || false);\nconst playLoading = computed(() => baseItem.value?.playLoading || false);\nconst isFavorite = computed(() => baseItem.value?.isFavorite || false);\nconst isHovering = computed(() => baseItem.value?.isHovering || false);\nconst artists = computed(() => baseItem.value?.artists || []);\n\n// 包装方法，避免直接访问可能为undefined的ref\nconst onToggleSelect = () => {\n  baseItem.value?.toggleSelect();\n};\nconst onArtistClick = (id: number) => baseItem.value?.handleArtistClick(id);\nconst onToggleFavorite = (event: Event) => {\n  baseItem.value?.toggleFavorite(event);\n  // 可选：emit 收藏事件\n};\nconst onPlayMusic = () => {\n  baseItem.value?.playMusicEvent(props.item);\n  emit('play', props.item);\n};\nconst onMenuClick = (event: MouseEvent) => baseItem.value?.handleMenuClick(event);\n\n// 从useSongItem.ts导入格式化时长和获取时长方法\nconst getDuration = (item: SongResult): number => {\n  if (item.duration) return item.duration;\n  if (typeof item.dt === 'number') return item.dt;\n  return 0;\n};\n\nconst formatDuration = (ms: number): string => {\n  if (!ms) return '--:--';\n  const totalSeconds = Math.floor(ms / 1000);\n  const minutes = Math.floor(totalSeconds / 60);\n  const seconds = totalSeconds % 60;\n  return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.compact-song-item {\n  @apply rounded-lg p-2 h-12 mb-1 border-b dark:border-gray-800 border-gray-100;\n\n  &:hover {\n    @apply bg-gray-50 dark:bg-gray-700;\n\n    .opacity-0 {\n      opacity: 1;\n    }\n  }\n\n  .song-item-index {\n    @apply w-8 text-center text-gray-500 dark:text-gray-400 text-sm;\n  }\n\n  .song-item-select {\n    @apply mr-3 cursor-pointer;\n  }\n\n  .song-item-content-compact {\n    @apply flex-1 flex items-center gap-2;\n\n    &-wrapper {\n      @apply flex-[2] flex items-center gap-2 min-w-0;\n    }\n\n    &-title {\n      @apply flex-[2.5] min-w-0 text-sm cursor-pointer text-gray-900 dark:text-white;\n    }\n\n    &-artist {\n      @apply flex-[1.5] min-w-0 text-sm text-gray-500 dark:text-gray-400;\n    }\n\n    &-album {\n      @apply flex-[1.5] min-w-0 text-sm text-gray-500 dark:text-gray-400;\n    }\n\n    &-duration {\n      @apply w-14 flex-shrink-0 text-sm text-gray-500 dark:text-gray-400 justify-end;\n    }\n  }\n\n  .song-item-operating-compact {\n    @apply border-none bg-transparent gap-2 flex items-center;\n\n    .song-item-operating-like,\n    .song-item-operating-play,\n    .song-item-operating-menu {\n      @apply transition-opacity duration-200;\n    }\n\n    .song-item-operating-play {\n      @apply w-7 h-7 flex items-center justify-center cursor-pointer rounded-full bg-gray-300 dark:bg-gray-800 border dark:border-gray-700 border-gray-200 text-gray-900 dark:text-white;\n\n      &:hover,\n      &.bg-green-600 {\n        @apply bg-green-500 border-green-500 text-white;\n      }\n\n      .iconfont {\n        @apply text-base;\n      }\n    }\n\n    .song-item-operating-like {\n      @apply mr-1 ml-0 cursor-pointer;\n\n      .iconfont {\n        @apply text-base transition text-gray-500 dark:text-gray-400 hover:text-red-500;\n      }\n      .like-active {\n        @apply text-red-500 dark:text-red-500;\n      }\n    }\n\n    .song-item-operating-menu {\n      @apply cursor-pointer flex items-center justify-center px-2;\n\n      .iconfont {\n        @apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-green-500;\n      }\n    }\n\n    .opacity-0 {\n      opacity: 0;\n    }\n  }\n}\n\n// 全局应用\n:deep(.text-ellipsis) {\n  width: 100%;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/songItemCom/ListSongItem.vue",
    "content": "<template>\n  <base-song-item\n    :item=\"item\"\n    :selectable=\"selectable\"\n    :selected=\"selected\"\n    :can-remove=\"canRemove\"\n    :is-next=\"isNext\"\n    :index=\"index\"\n    @play=\"(...args) => $emit('play', ...args)\"\n    @select=\"(...args) => $emit('select', ...args)\"\n    @remove-song=\"(...args) => $emit('remove-song', ...args)\"\n    class=\"list-song-item\"\n    ref=\"baseItem\"\n  >\n    <!-- 选择框插槽 -->\n    <template #select>\n      <div v-if=\"baseItem && selectable\" class=\"song-item-select\" @click.stop=\"onToggleSelect\">\n        <n-checkbox :checked=\"selected\" />\n      </div>\n    </template>\n\n    <!-- 图片插槽 -->\n    <template #image>\n      <n-image\n        v-if=\"item.picUrl\"\n        :src=\"getImgUrl(item.picUrl, '100y100')\"\n        class=\"song-item-img\"\n        preview-disabled\n        :img-props=\"{\n          crossorigin: 'anonymous'\n        }\"\n        @load=\"onImageLoad\"\n      />\n    </template>\n\n    <!-- 内容插槽 -->\n    <template #content>\n      <div class=\"song-item-content\">\n        <div class=\"song-item-content-wrapper\">\n          <n-ellipsis\n            class=\"song-item-content-title text-ellipsis\"\n            line-clamp=\"1\"\n            :class=\"{ 'text-green-500': isPlaying }\"\n          >\n            {{ item.name }}\n          </n-ellipsis>\n          <div class=\"song-item-content-divider\">-</div>\n          <n-ellipsis class=\"song-item-content-name text-ellipsis\" line-clamp=\"1\">\n            <template v-for=\"(artist, index) in artists\" :key=\"index\">\n              <span\n                class=\"cursor-pointer hover:text-green-500\"\n                @click.stop=\"onArtistClick(artist.id)\"\n                >{{ artist.name }}</span\n              >\n              <span v-if=\"index < artists.length - 1\"> / </span>\n            </template>\n          </n-ellipsis>\n        </div>\n      </div>\n    </template>\n\n    <!-- 操作插槽 -->\n    <template #operating>\n      <div class=\"song-item-operating song-item-operating-list\">\n        <div v-if=\"favorite\" class=\"song-item-operating-like\">\n          <i\n            class=\"iconfont icon-likefill\"\n            :class=\"{ 'like-active': isFavorite }\"\n            @click.stop=\"onToggleFavorite\"\n          ></i>\n        </div>\n        <div\n          class=\"song-item-operating-play bg-gray-300 dark:bg-gray-800 animate__animated\"\n          :class=\"{ 'bg-green-600': isPlaying, animate__flipInY: playLoading }\"\n          @click=\"onPlayMusic\"\n        >\n          <i v-if=\"isPlaying && play\" class=\"iconfont icon-stop\"></i>\n          <i v-else class=\"iconfont icon-playfill\"></i>\n        </div>\n      </div>\n    </template>\n  </base-song-item>\n</template>\n\n<script lang=\"ts\" setup>\nimport { NCheckbox, NEllipsis, NImage } from 'naive-ui';\nimport { computed, ref } from 'vue';\n\nimport { usePlayerStore } from '@/store';\nimport type { SongResult } from '@/types/music';\nimport { getImgUrl } from '@/utils';\n\nimport BaseSongItem from './BaseSongItem.vue';\n\nconst playerStore = usePlayerStore();\n\nconst props = withDefaults(\n  defineProps<{\n    item: SongResult;\n    favorite?: boolean;\n    selectable?: boolean;\n    selected?: boolean;\n    canRemove?: boolean;\n    isNext?: boolean;\n    index?: number;\n  }>(),\n  {\n    favorite: true,\n    selectable: false,\n    selected: false,\n    canRemove: false,\n    isNext: false,\n    index: undefined\n  }\n);\n\nconst emit = defineEmits(['play', 'select', 'remove-song']);\nconst baseItem = ref<InstanceType<typeof BaseSongItem>>();\n\n// 从基础组件获取响应式状态\nconst play = computed(() => playerStore.isPlay);\nconst isPlaying = computed(() => baseItem.value?.isPlaying || false);\nconst playLoading = computed(() => baseItem.value?.playLoading || false);\nconst isFavorite = computed(() => baseItem.value?.isFavorite || false);\nconst artists = computed(() => baseItem.value?.artists || []);\n\n// 包装方法，避免直接访问可能为undefined的ref\nconst onToggleSelect = () => {\n  baseItem.value?.toggleSelect();\n  emit('select', props.item.id, !props.selected);\n};\nconst onImageLoad = (event: Event) => baseItem.value?.imageLoad(event);\nconst onArtistClick = (id: number) => baseItem.value?.handleArtistClick(id);\nconst onToggleFavorite = (event: Event) => {\n  baseItem.value?.toggleFavorite(event);\n  // 可选：emit 收藏事件\n};\nconst onPlayMusic = () => {\n  baseItem.value?.playMusicEvent(props.item);\n  emit('play', props.item);\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.list-song-item {\n  @apply p-2 rounded-lg mb-2 border dark:border-gray-800 border-gray-200;\n\n  &:hover {\n    @apply bg-gray-50 dark:bg-gray-800;\n  }\n\n  .song-item-img {\n    @apply w-10 h-10 rounded-lg mr-3;\n  }\n\n  .song-item-content {\n    @apply flex items-center flex-1;\n\n    &-wrapper {\n      @apply flex items-center flex-1 text-sm;\n    }\n\n    &-title {\n      @apply flex-shrink-0 max-w-[45%] text-gray-900 dark:text-white;\n    }\n\n    &-divider {\n      @apply mx-2 text-gray-500 dark:text-gray-400;\n    }\n\n    &-name {\n      @apply flex-1 min-w-0 text-gray-500 dark:text-gray-400;\n    }\n  }\n\n  .song-item-operating-list {\n    @apply flex items-center gap-2;\n\n    &-like {\n      @apply cursor-pointer hover:scale-110 transition-transform;\n\n      .iconfont {\n        @apply text-base text-gray-500 dark:text-gray-400 hover:text-red-500;\n      }\n    }\n\n    &-play {\n      @apply w-7 h-7 cursor-pointer hover:scale-110 transition-transform;\n\n      .iconfont {\n        @apply text-base;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/songItemCom/MiniSongItem.vue",
    "content": "<template>\n  <base-song-item\n    :item=\"item\"\n    :selectable=\"selectable\"\n    :selected=\"selected\"\n    :can-remove=\"canRemove\"\n    :is-next=\"isNext\"\n    :index=\"index\"\n    @play=\"(...args) => $emit('play', ...args)\"\n    @select=\"(...args) => $emit('select', ...args)\"\n    @remove-song=\"(...args) => $emit('remove-song', ...args)\"\n    class=\"mini-song-item\"\n    ref=\"baseItem\"\n  >\n    <!-- 选择框插槽 -->\n    <template #select>\n      <div v-if=\"baseItem && selectable\" class=\"song-item-select\" @click.stop=\"onToggleSelect\">\n        <n-checkbox :checked=\"selected\" />\n      </div>\n    </template>\n\n    <!-- 图片插槽 -->\n    <template #image>\n      <n-image\n        v-if=\"item.picUrl\"\n        :src=\"getImgUrl(item.picUrl, '100y100')\"\n        class=\"song-item-img\"\n        preview-disabled\n        :img-props=\"{\n          crossorigin: 'anonymous'\n        }\"\n        @load=\"onImageLoad\"\n      />\n    </template>\n\n    <!-- 内容插槽 -->\n    <template #content>\n      <div class=\"song-item-content\">\n        <div class=\"song-item-content-title\">\n          <n-ellipsis class=\"text-ellipsis\" line-clamp=\"1\" :class=\"{ 'text-green-500': isPlaying }\">\n            {{ item.name }}\n          </n-ellipsis>\n        </div>\n        <div class=\"song-item-content-name\">\n          <n-ellipsis class=\"text-ellipsis\" line-clamp=\"1\">\n            <template v-for=\"(artist, index) in artists\" :key=\"index\">\n              <span\n                class=\"cursor-pointer hover:text-green-500\"\n                @click.stop=\"onArtistClick(artist.id)\"\n                >{{ artist.name }}</span\n              >\n              <span v-if=\"index < artists.length - 1\"> / </span>\n            </template>\n          </n-ellipsis>\n        </div>\n      </div>\n    </template>\n\n    <!-- 操作插槽 -->\n    <template #operating>\n      <div class=\"song-item-operating\">\n        <div v-if=\"favorite\" class=\"song-item-operating-like\">\n          <i\n            class=\"iconfont icon-likefill\"\n            :class=\"{ 'like-active': isFavorite }\"\n            @click.stop=\"onToggleFavorite\"\n          ></i>\n        </div>\n        <div\n          class=\"song-item-operating-play bg-gray-300 dark:bg-gray-800 animate__animated\"\n          :class=\"{ 'bg-green-600': isPlaying, animate__flipInY: playLoading }\"\n          @click=\"onPlayMusic\"\n        >\n          <i v-if=\"isPlaying && play\" class=\"iconfont icon-stop\"></i>\n          <i v-else class=\"iconfont icon-playfill\"></i>\n        </div>\n      </div>\n    </template>\n  </base-song-item>\n</template>\n\n<script lang=\"ts\" setup>\nimport { NCheckbox, NEllipsis, NImage } from 'naive-ui';\nimport { computed, ref } from 'vue';\n\nimport { usePlayerStore } from '@/store';\nimport type { SongResult } from '@/types/music';\nimport { getImgUrl } from '@/utils';\n\nimport BaseSongItem from './BaseSongItem.vue';\n\nconst playerStore = usePlayerStore();\n\nconst props = withDefaults(\n  defineProps<{\n    item: SongResult;\n    favorite?: boolean;\n    selectable?: boolean;\n    selected?: boolean;\n    canRemove?: boolean;\n    isNext?: boolean;\n    index?: number;\n  }>(),\n  {\n    favorite: true,\n    selectable: false,\n    selected: false,\n    canRemove: false,\n    isNext: false,\n    index: undefined\n  }\n);\n\nconst emit = defineEmits(['play', 'select', 'remove-song']);\nconst baseItem = ref<InstanceType<typeof BaseSongItem>>();\n\n// 从基础组件获取响应式状态\nconst play = computed(() => playerStore.isPlay);\nconst isPlaying = computed(() => baseItem.value?.isPlaying || false);\nconst playLoading = computed(() => baseItem.value?.playLoading || false);\nconst isFavorite = computed(() => baseItem.value?.isFavorite || false);\nconst artists = computed(() => baseItem.value?.artists || []);\n\n// 包装方法，避免直接访问可能为undefined的ref\nconst onToggleSelect = () => {\n  baseItem.value?.toggleSelect();\n  emit('select', props.item.id, !props.selected);\n};\nconst onImageLoad = (event: Event) => baseItem.value?.imageLoad(event);\nconst onArtistClick = (id: number) => baseItem.value?.handleArtistClick(id);\nconst onToggleFavorite = (event: Event) => {\n  baseItem.value?.toggleFavorite(event);\n  // 可选：emit 收藏事件\n};\nconst onPlayMusic = () => {\n  baseItem.value?.playMusicEvent(props.item);\n  emit('play', props.item);\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.mini-song-item {\n  @apply p-2 rounded-2xl;\n\n  &:hover {\n    @apply bg-light-100 dark:bg-dark-100;\n  }\n\n  .song-item-img {\n    @apply w-10 h-10 mr-2 rounded-xl;\n  }\n\n  .song-item-content {\n    @apply flex-1;\n\n    &-title {\n      @apply text-sm text-gray-900 dark:text-white;\n    }\n\n    &-name {\n      @apply text-xs text-gray-500 dark:text-gray-400;\n    }\n  }\n\n  .song-item-operating {\n    @apply flex items-center rounded-full ml-4 pl-2 border dark:border-gray-700 border-gray-200 bg-light dark:bg-black;\n\n    .iconfont {\n      @apply text-base;\n    }\n\n    &-like {\n      @apply mr-1 ml-1 cursor-pointer;\n\n      .icon-likefill {\n        @apply text-base transition text-gray-500 dark:text-gray-400 hover:text-red-500;\n      }\n\n      .like-active {\n        @apply text-red-500 dark:text-red-500;\n      }\n    }\n\n    &-play {\n      @apply cursor-pointer rounded-full w-8 h-8 flex justify-center items-center transition\n             border dark:border-gray-700 border-gray-200 text-gray-900 dark:text-white;\n\n      &:hover,\n      &.bg-green-600 {\n        @apply bg-green-500 border-green-500 text-white;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/songItemCom/SongItemDropdown.vue",
    "content": "<template>\n  <n-dropdown\n    v-if=\"isElectron\"\n    :show=\"show\"\n    :x=\"x\"\n    :y=\"y\"\n    :options=\"dropdownOptions\"\n    :z-index=\"99999999\"\n    placement=\"bottom-start\"\n    @clickoutside=\"$emit('update:show', false)\"\n    @select=\"handleSelect\"\n    class=\"rounded-xl\"\n  />\n</template>\n\n<script lang=\"ts\" setup>\nimport type { MenuOption } from 'naive-ui';\nimport { NDropdown, NEllipsis, NImage } from 'naive-ui';\nimport { computed, h, inject } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport type { SongResult } from '@/types/music';\nimport { getImgUrl, isElectron } from '@/utils';\nimport { hasPermission } from '@/utils/auth';\n\nconst { t } = useI18n();\n\nconst props = defineProps<{\n  item: SongResult;\n  show: boolean;\n  x: number;\n  y: number;\n  isFavorite: boolean;\n  isDislike: boolean;\n  canRemove?: boolean;\n}>();\n\nconst emits = defineEmits([\n  'update:show',\n  'select',\n  'play',\n  'play-next',\n  'download',\n  'add-to-playlist',\n  'toggle-favorite',\n  'toggle-dislike',\n  'remove'\n]);\n\nconst openPlaylistDrawer = inject<(songId: number | string) => void>('openPlaylistDrawer');\n\n// 渲染歌曲预览\nconst renderSongPreview = () => {\n  return h(\n    'div',\n    {\n      class: 'flex items-center gap-3 px-2 dark:border-gray-800 dark:text-white'\n    },\n    [\n      h(NImage, {\n        src: getImgUrl(props.item.picUrl || props.item.al?.picUrl, '100y100'),\n        class: 'w-10 h-10 rounded-lg flex-shrink-0',\n        previewDisabled: true,\n        imgProps: {\n          crossorigin: 'anonymous'\n        }\n      }),\n      h(\n        'div',\n        {\n          class: 'flex-1 min-w-0 py-1 overflow-hidden'\n        },\n        [\n          h(\n            'div',\n            {\n              class: 'mb-1 overflow-hidden'\n            },\n            [\n              h(\n                NEllipsis,\n                {\n                  lineClamp: 1,\n                  depth: 1,\n                  class: 'text-sm font-medium w-full',\n                  style: 'max-width: 150px; min-width: 120px;'\n                },\n                {\n                  default: () => props.item.name\n                }\n              )\n            ]\n          ),\n          h(\n            'div',\n            {\n              class: 'text-xs text-gray-500 dark:text-gray-400 overflow-hidden'\n            },\n            [\n              h(\n                NEllipsis,\n                {\n                  lineClamp: 1,\n                  style: 'max-width: 150px;'\n                },\n                {\n                  default: () => {\n                    const artistNames = (props.item.ar || props.item.song?.artists)\n                      ?.map((a) => a.name)\n                      .join(' / ');\n                    return artistNames || '未知艺术家';\n                  }\n                }\n              )\n            ]\n          )\n        ]\n      )\n    ]\n  );\n};\n\n// 下拉菜单选项\nconst dropdownOptions = computed<MenuOption[]>(() => {\n  const hasRealAuth = hasPermission(true);\n\n  const options: MenuOption[] = [\n    {\n      key: 'header',\n      type: 'render',\n      render: renderSongPreview\n    },\n    {\n      key: 'divider1',\n      type: 'divider'\n    },\n    {\n      label: t('songItem.menu.play'),\n      key: 'play',\n      icon: () => h('i', { class: 'iconfont ri-play-circle-line' })\n    },\n    {\n      label: t('songItem.menu.playNext'),\n      key: 'playNext',\n      icon: () => h('i', { class: 'iconfont ri-play-list-2-line' })\n    },\n    {\n      type: 'divider',\n      key: 'd1'\n    },\n    {\n      label: t('songItem.menu.download'),\n      key: 'download',\n      icon: () => h('i', { class: 'iconfont ri-download-line' })\n    },\n    {\n      label: t('songItem.menu.addToPlaylist'),\n      key: 'addToPlaylist',\n      icon: () => h('i', { class: 'iconfont ri-folder-add-line' }),\n      disabled: !hasRealAuth\n    },\n    {\n      label: props.isFavorite ? t('songItem.menu.unfavorite') : t('songItem.menu.favorite'),\n      key: 'favorite',\n      icon: () =>\n        h('i', {\n          class: `iconfont ${props.isFavorite ? 'ri-heart-fill text-red-500' : 'ri-heart-line'}`\n        })\n      // 收藏功能不禁用，UID登录时可以本地收藏/取消收藏\n    },\n    {\n      label: props.isDislike ? t('songItem.menu.undislike') : t('songItem.menu.dislike'),\n      key: 'dislike',\n      icon: () =>\n        h('i', {\n          class: `iconfont ${props.isDislike ? 'ri-dislike-fill text-green-500' : 'ri-dislike-line'}`\n        })\n    }\n  ];\n\n  if (props.canRemove) {\n    options.push(\n      {\n        type: 'divider',\n        key: 'd2'\n      },\n      {\n        label: t('songItem.menu.removeFromPlaylist'),\n        key: 'remove',\n        icon: () => h('i', { class: 'iconfont ri-delete-bin-line' })\n      }\n    );\n  }\n\n  return options;\n});\n\n// 处理选择\nconst handleSelect = (key: string | number) => {\n  emits('update:show', false);\n\n  switch (key) {\n    case 'download':\n      emits('download');\n      break;\n    case 'playNext':\n      emits('play-next');\n      break;\n    case 'addToPlaylist':\n      openPlaylistDrawer?.(props.item.id);\n      break;\n    case 'favorite':\n      emits('toggle-favorite');\n      break;\n    case 'play':\n      emits('play');\n      break;\n    case 'remove':\n      emits('remove', props.item.id);\n      break;\n    case 'dislike':\n      emits('toggle-dislike');\n      break;\n    default:\n      break;\n  }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n:deep(.n-dropdown-menu) {\n  @apply min-w-[240px] overflow-hidden rounded-lg border dark:border-gray-800;\n\n  .n-dropdown-option {\n    @apply h-9 text-sm;\n\n    &:hover {\n      @apply bg-gray-100 dark:bg-gray-800;\n    }\n\n    .n-dropdown-option-body {\n      @apply h-full;\n\n      .n-dropdown-option-body__prefix {\n        @apply w-8 flex justify-center items-center;\n\n        .iconfont {\n          @apply text-base;\n        }\n      }\n    }\n  }\n\n  .n-dropdown-divider {\n    @apply my-1;\n  }\n}\n\n:deep(.n-dropdown-option-body--render) {\n  @apply p-0;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/common/songItemCom/StandardSongItem.vue",
    "content": "<template>\n  <base-song-item\n    :item=\"item\"\n    :selectable=\"selectable\"\n    :selected=\"selected\"\n    :can-remove=\"canRemove\"\n    :is-next=\"isNext\"\n    :index=\"index\"\n    @play=\"(...args) => $emit('play', ...args)\"\n    @select=\"(...args) => $emit('select', ...args)\"\n    @remove-song=\"(...args) => $emit('remove-song', ...args)\"\n    class=\"standard-song-item\"\n    ref=\"baseItem\"\n  >\n    <!-- 选择框插槽 -->\n    <template #select>\n      <div v-if=\"baseItem && selectable\" class=\"song-item-select\" @click.stop=\"onToggleSelect\">\n        <n-checkbox :checked=\"selected\" />\n      </div>\n    </template>\n\n    <!-- 图片插槽 -->\n    <template #image>\n      <n-image\n        v-if=\"item.picUrl\"\n        :src=\"getImgUrl(item.picUrl, '100y100')\"\n        class=\"song-item-img\"\n        preview-disabled\n        :img-props=\"{\n          crossorigin: 'anonymous'\n        }\"\n        @load=\"onImageLoad\"\n      />\n    </template>\n\n    <!-- 内容插槽 -->\n    <template #content>\n      <div class=\"song-item-content\">\n        <div class=\"song-item-content-title\">\n          <n-ellipsis\n            class=\"text-ellipsis\"\n            line-clamp=\"1\"\n            :class=\"{ 'text-green-500': isPlaying }\"\n            >{{ item.name }}</n-ellipsis\n          >\n        </div>\n        <div class=\"song-item-content-name\">\n          <n-ellipsis class=\"text-ellipsis\" line-clamp=\"1\">\n            <template v-for=\"(artist, index) in artists\" :key=\"index\">\n              <span\n                class=\"cursor-pointer hover:text-green-500\"\n                @click.stop=\"onArtistClick(artist.id)\"\n                >{{ artist.name }}</span\n              >\n              <span v-if=\"index < artists.length - 1\"> / </span>\n            </template>\n          </n-ellipsis>\n        </div>\n      </div>\n    </template>\n\n    <!-- 操作插槽 -->\n    <template #operating>\n      <div class=\"song-item-operating\">\n        <div v-if=\"favorite\" class=\"song-item-operating-like\">\n          <i\n            class=\"iconfont icon-likefill\"\n            :class=\"{ 'like-active': isFavorite }\"\n            @click.stop=\"onToggleFavorite\"\n          ></i>\n        </div>\n        <n-tooltip v-if=\"isNext\" trigger=\"hover\" :z-index=\"9999999\" :delay=\"400\">\n          <template #trigger>\n            <div class=\"song-item-operating-next\" @click.stop=\"onPlayNext\">\n              <i class=\"iconfont ri-skip-forward-fill\"></i>\n            </div>\n          </template>\n          {{ t('songItem.menu.playNext') }}\n        </n-tooltip>\n        <div\n          class=\"song-item-operating-play bg-gray-300 dark:bg-gray-800 animate__animated\"\n          :class=\"{ 'bg-green-600': isPlaying, animate__flipInY: playLoading }\"\n          @click=\"onPlayMusic\"\n        >\n          <i v-if=\"isPlaying && play\" class=\"iconfont icon-stop\"></i>\n          <i v-else class=\"iconfont icon-playfill\"></i>\n        </div>\n      </div>\n    </template>\n  </base-song-item>\n</template>\n\n<script lang=\"ts\" setup>\nimport { NCheckbox, NEllipsis, NImage, NTooltip } from 'naive-ui';\nimport { computed, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { usePlayerStore } from '@/store';\nimport type { SongResult } from '@/types/music';\nimport { getImgUrl } from '@/utils';\n\nimport BaseSongItem from './BaseSongItem.vue';\n\nconst { t } = useI18n();\nconst playerStore = usePlayerStore();\n\nconst props = withDefaults(\n  defineProps<{\n    item: SongResult;\n    favorite?: boolean;\n    selectable?: boolean;\n    selected?: boolean;\n    canRemove?: boolean;\n    isNext?: boolean;\n    index?: number;\n  }>(),\n  {\n    favorite: true,\n    selectable: false,\n    selected: false,\n    canRemove: false,\n    isNext: false,\n    index: undefined\n  }\n);\n\nconst emit = defineEmits(['play', 'select', 'remove-song']);\nconst baseItem = ref<InstanceType<typeof BaseSongItem>>();\n\n// 从playerStore和baseItem获取响应式状态\nconst play = computed(() => playerStore.isPlay);\nconst isPlaying = computed(() => baseItem.value?.isPlaying || false);\nconst playLoading = computed(() => baseItem.value?.playLoading || false);\nconst isFavorite = computed(() => baseItem.value?.isFavorite || false);\nconst artists = computed(() => baseItem.value?.artists || []);\n\n// 包装方法，避免直接访问可能为undefined的ref\nconst onToggleSelect = () => {\n  baseItem.value?.toggleSelect();\n};\nconst onImageLoad = (event: Event) => baseItem.value?.imageLoad(event);\nconst onArtistClick = (id: number) => baseItem.value?.handleArtistClick(id);\nconst onToggleFavorite = (event: Event) => {\n  baseItem.value?.toggleFavorite(event);\n};\nconst onPlayMusic = () => {\n  baseItem.value?.playMusicEvent(props.item);\n  emit('play', props.item);\n};\nconst onPlayNext = () => {\n  baseItem.value?.handlePlayNext();\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.standard-song-item {\n  &:hover {\n    @apply bg-light-100 dark:bg-dark-100;\n  }\n\n  .song-item-img {\n    @apply w-12 h-12 rounded-2xl mr-4;\n  }\n\n  .song-item-content {\n    @apply flex-1;\n\n    &-title {\n      @apply text-base text-gray-900 dark:text-white;\n    }\n\n    &-name {\n      @apply text-xs text-gray-500 dark:text-gray-400;\n    }\n  }\n\n  .song-item-operating {\n    @apply flex items-center rounded-full ml-4 border dark:border-gray-700 border-gray-200 bg-light dark:bg-black;\n\n    .iconfont {\n      @apply text-xl;\n    }\n\n    .icon-likefill {\n      @apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-red-500;\n    }\n\n    &-like {\n      @apply mr-2 cursor-pointer ml-4 transition-all;\n    }\n\n    &-next {\n      @apply mr-2 cursor-pointer transition-all;\n\n      .iconfont {\n        @apply text-xl transition text-gray-500 dark:text-gray-400 hover:text-green-500;\n      }\n    }\n\n    .like-active {\n      @apply text-red-500 dark:text-red-500;\n    }\n\n    &-play {\n      @apply cursor-pointer rounded-full w-10 h-10 flex justify-center items-center transition\n             border dark:border-gray-700 border-gray-200 text-gray-900 dark:text-white;\n\n      &:hover,\n      &.bg-green-600 {\n        @apply bg-green-500 border-green-500 text-white;\n      }\n    }\n  }\n\n  .song-item-select {\n    @apply mr-3 cursor-pointer;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/cover/Cover3D.vue",
    "content": "<template>\n  <div\n    ref=\"coverContainer\"\n    class=\"cover-3d-container relative cursor-pointer\"\n    @mousemove=\"handleMouseMove\"\n    @mouseleave=\"handleMouseLeave\"\n    @mouseenter=\"handleMouseEnter\"\n  >\n    <div ref=\"coverImage\" class=\"cover-wrapper\" :style=\"coverTransformStyle\">\n      <n-image :src=\"src\" class=\"cover-image\" lazy preview-disabled :object-fit=\"objectFit\" />\n      <div class=\"cover-shine\" :style=\"shineStyle\"></div>\n    </div>\n    <div v-if=\"loading\" class=\"loading-overlay\">\n      <i class=\"ri-loader-4-line loading-icon\"></i>\n    </div>\n    <slot />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref } from 'vue';\n\ninterface Props {\n  src: string;\n  loading?: boolean;\n  maxTilt?: number;\n  scale?: number;\n  shineIntensity?: number;\n  objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | 'none';\n  disabled?: boolean;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  loading: false,\n  maxTilt: 12,\n  scale: 1.03,\n  shineIntensity: 0.25,\n  objectFit: 'cover',\n  disabled: false\n});\n\n// 3D视差效果相关\nconst coverContainer = ref<HTMLElement | null>(null);\nconst coverImage = ref<HTMLElement | null>(null);\nconst mouseX = ref(0.5);\nconst mouseY = ref(0.5);\nconst isHovering = ref(false);\nconst rafId = ref<number | null>(null);\n\n// 3D视差效果计算\nconst coverTransformStyle = computed(() => {\n  if (!isHovering.value || props.disabled) {\n    return {\n      transform: 'perspective(1000px) rotateX(0deg) rotateY(0deg) scale(1)',\n      transition: 'transform 0.4s cubic-bezier(0.4, 0, 0.2, 1)'\n    };\n  }\n\n  const tiltX = Math.round((mouseY.value - 0.5) * props.maxTilt * 100) / 100;\n  const tiltY = Math.round((mouseX.value - 0.5) * -props.maxTilt * 100) / 100;\n\n  return {\n    transform: `perspective(1000px) rotateX(${tiltX}deg) rotateY(${tiltY}deg) scale(${props.scale})`,\n    transition: 'none'\n  };\n});\n\n// 光泽效果计算\nconst shineStyle = computed(() => {\n  if (!isHovering.value || props.disabled) {\n    return {\n      opacity: 0,\n      background: 'transparent',\n      transition: 'opacity 0.3s ease-out'\n    };\n  }\n\n  const shineX = Math.round(mouseX.value * 100);\n  const shineY = Math.round(mouseY.value * 100);\n\n  return {\n    opacity: props.shineIntensity,\n    background: `radial-gradient(200px circle at ${shineX}% ${shineY}%, rgba(255,255,255,0.3), transparent 50%)`,\n    transition: 'none'\n  };\n});\n\n// 使用 requestAnimationFrame 优化鼠标事件\nconst updateMousePosition = (x: number, y: number) => {\n  if (rafId.value) {\n    cancelAnimationFrame(rafId.value);\n  }\n\n  rafId.value = requestAnimationFrame(() => {\n    // 只在位置有显著变化时更新，减少不必要的重绘\n    const deltaX = Math.abs(mouseX.value - x);\n    const deltaY = Math.abs(mouseY.value - y);\n\n    if (deltaX > 0.01 || deltaY > 0.01) {\n      mouseX.value = x;\n      mouseY.value = y;\n    }\n  });\n};\n\n// 3D视差效果的鼠标事件处理\nconst handleMouseMove = (event: MouseEvent) => {\n  if (!coverContainer.value || !isHovering.value || props.disabled) return;\n\n  const rect = coverContainer.value.getBoundingClientRect();\n  const x = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width));\n  const y = Math.max(0, Math.min(1, (event.clientY - rect.top) / rect.height));\n\n  updateMousePosition(x, y);\n};\n\nconst handleMouseEnter = () => {\n  if (!props.disabled) {\n    isHovering.value = true;\n  }\n};\n\nconst handleMouseLeave = () => {\n  isHovering.value = false;\n  if (rafId.value) {\n    cancelAnimationFrame(rafId.value);\n    rafId.value = null;\n  }\n  // 平滑回到中心位置\n  updateMousePosition(0.5, 0.5);\n};\n\n// 清理资源\nonBeforeUnmount(() => {\n  if (rafId.value) {\n    cancelAnimationFrame(rafId.value);\n  }\n});\n</script>\n\n<style scoped lang=\"scss\">\n.cover-3d-container {\n  @apply w-full h-full;\n}\n\n/* 3D视差效果样式 */\n.cover-wrapper {\n  @apply relative w-full h-full rounded-xl overflow-hidden;\n  transform-style: preserve-3d;\n  will-change: transform;\n  backface-visibility: hidden;\n  transform: translateZ(0); /* 强制硬件加速 */\n}\n\n.cover-image {\n  @apply w-full h-full;\n  border-radius: inherit;\n  transform: translateZ(0); /* 强制硬件加速 */\n}\n\n.cover-shine {\n  @apply absolute inset-0 pointer-events-none rounded-xl;\n  mix-blend-mode: overlay;\n  z-index: 1;\n  will-change: background, opacity;\n  backface-visibility: hidden;\n}\n\n/* 为封面容器添加阴影效果 */\n.cover-3d-container:hover .cover-wrapper {\n  filter: drop-shadow(0 15px 30px rgba(0, 0, 0, 0.25));\n}\n\n@keyframes spin {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n.loading-overlay {\n  @apply absolute inset-0 flex items-center justify-center rounded-xl;\n  background-color: rgba(0, 0, 0, 0.5);\n  z-index: 2;\n}\n\n.loading-icon {\n  font-size: 48px;\n  color: white;\n  animation: spin 1s linear infinite;\n}\n\n/* 移动端禁用3D效果 */\n@media (max-width: 768px) {\n  .cover-wrapper {\n    transform: none !important;\n  }\n\n  .cover-shine {\n    display: none;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/home/PlaylistType.vue",
    "content": "<template>\n  <!-- 歌单分类列表 -->\n  <div class=\"play-list-type\">\n    <div class=\"title\" :class=\"setAnimationClass('animate__fadeInLeft')\">\n      {{ t('comp.playlistType.title') }}\n    </div>\n    <div>\n      <template v-for=\"(item, index) in playlistCategory?.sub\" :key=\"item.name\">\n        <span\n          v-show=\"isShowAllPlaylistCategory || index <= 19 || isHiding\"\n          class=\"play-list-type-item\"\n          :class=\"\n            setAnimationClass(\n              index <= 19\n                ? 'animate__bounceIn'\n                : !isShowAllPlaylistCategory\n                  ? 'animate__backOutLeft'\n                  : 'animate__bounceIn'\n            ) +\n            ' ' +\n            'type-item-' +\n            index\n          \"\n          :style=\"getAnimationDelay(index)\"\n          @click=\"handleClickPlaylistType(item.name)\"\n          >{{ item.name }}</span\n        >\n      </template>\n      <div\n        class=\"play-list-type-showall\"\n        :class=\"setAnimationClass('animate__bounceIn')\"\n        :style=\"\n          setAnimationDelay(\n            !isShowAllPlaylistCategory ? 25 : playlistCategory?.sub.length || 100 + 30\n          )\n        \"\n        @click=\"handleToggleShowAllPlaylistCategory\"\n      >\n        {{\n          !isShowAllPlaylistCategory ? t('comp.playlistType.showAll') : t('comp.playlistType.hide')\n        }}\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\n\nimport { getPlaylistCategory } from '@/api/home';\nimport type { IPlayListSort } from '@/types/playlist';\nimport { setAnimationClass, setAnimationDelay } from '@/utils';\n\nconst { t } = useI18n();\n// 歌单分类\nconst playlistCategory = ref<IPlayListSort>();\n// 是否显示全部歌单分类\nconst isShowAllPlaylistCategory = ref<boolean>(false);\nconst DELAY_TIME = 40;\nconst getAnimationDelay = computed(() => {\n  return (index: number) => {\n    if (index <= 19) {\n      return setAnimationDelay(index, DELAY_TIME);\n    }\n    if (!isShowAllPlaylistCategory.value) {\n      const nowIndex = (playlistCategory.value?.sub.length || 0) - index;\n      return setAnimationDelay(nowIndex, DELAY_TIME);\n    }\n    return setAnimationDelay(index - 19, DELAY_TIME);\n  };\n});\n\nwatch(isShowAllPlaylistCategory, (newVal) => {\n  if (!newVal) {\n    const elements = playlistCategory.value?.sub.map((_, index) =>\n      document.querySelector(`.type-item-${index}`)\n    ) as HTMLElement[];\n    elements\n      .slice(20)\n      .reverse()\n      .forEach((element, index) => {\n        if (element) {\n          setTimeout(\n            () => {\n              (element as HTMLElement).style.position = 'absolute';\n            },\n            index * DELAY_TIME + 400\n          );\n        }\n      });\n\n    setTimeout(\n      () => {\n        isHiding.value = false;\n        document.querySelectorAll('.play-list-type-item').forEach((element) => {\n          if (element) {\n            console.log('element', element);\n            (element as HTMLElement).style.position = 'none';\n          }\n        });\n      },\n      (playlistCategory.value?.sub.length || 0 - 19) * DELAY_TIME\n    );\n  } else {\n    document.querySelectorAll('.play-list-type-item').forEach((element) => {\n      if (element) {\n        (element as HTMLElement).style.position = 'none';\n      }\n    });\n  }\n});\n\n// 加载歌单分类\nconst loadPlaylistCategory = async () => {\n  const { data } = await getPlaylistCategory();\n  playlistCategory.value = data;\n};\n\nconst router = useRouter();\nconst handleClickPlaylistType = (type: string) => {\n  router.push({\n    path: '/list',\n    query: {\n      type\n    }\n  });\n};\n\nconst isHiding = ref<boolean>(false);\nconst handleToggleShowAllPlaylistCategory = () => {\n  isShowAllPlaylistCategory.value = !isShowAllPlaylistCategory.value;\n  if (!isShowAllPlaylistCategory.value) {\n    isHiding.value = true;\n  }\n};\n// 页面初始化\nonMounted(() => {\n  loadPlaylistCategory();\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.title {\n  @apply text-lg font-bold mb-4 text-gray-900 dark:text-white;\n}\n.play-list-type {\n  width: 250px;\n  @apply mr-4;\n  &-item,\n  &-showall {\n    @apply bg-light dark:bg-black text-gray-900 dark:text-white;\n    @apply py-2 px-3 mr-3 mb-3 inline-block border border-gray-200 dark:border-gray-700 rounded-xl cursor-pointer hover:bg-green-600 hover:text-white transition;\n  }\n  &-showall {\n    @apply block text-center;\n  }\n}\n\n.mobile {\n  .play-list-type {\n    @apply mx-0 w-full;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/home/RecommendAlbum.vue",
    "content": "<template>\n  <div class=\"recommend-album\">\n    <div class=\"title\" :class=\"setAnimationClass('animate__fadeInRight')\">\n      {{ t('comp.recommendAlbum.title') }}\n    </div>\n    <div class=\"recommend-album-list\">\n      <template v-for=\"(item, index) in albumData?.albums\" :key=\"item.id\">\n        <div\n          v-if=\"index < 6\"\n          class=\"recommend-album-list-item\"\n          :class=\"setAnimationClass('animate__backInUp')\"\n          :style=\"setAnimationDelay(index, 100)\"\n          @click=\"handleClick(item)\"\n        >\n          <n-image\n            class=\"recommend-album-list-item-img\"\n            :src=\"getImgUrl(item.blurPicUrl, '200y200')\"\n            lazy\n            preview-disabled\n          />\n          <div class=\"recommend-album-list-item-content\">{{ item.name }}</div>\n        </div>\n      </template>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\n\nimport { getNewAlbum } from '@/api/home';\nimport { getAlbum } from '@/api/list';\nimport { navigateToMusicList } from '@/components/common/MusicListNavigator';\nimport type { IAlbumNew } from '@/types/album';\nimport { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';\n\nconst { t } = useI18n();\nconst albumData = ref<IAlbumNew>();\nconst loadAlbumList = async () => {\n  const { data } = await getNewAlbum();\n  albumData.value = data;\n};\n\nconst router = useRouter();\n\nconst handleClick = async (item: any) => {\n  openAlbum(item);\n};\n\nconst openAlbum = async (album: any) => {\n  if (!album) return;\n\n  try {\n    const res = await getAlbum(album.id);\n    const { songs, album: albumInfo } = res.data;\n\n    const formattedSongs = songs.map((song: any) => {\n      song.al.picUrl = song.al.picUrl || albumInfo.picUrl;\n      song.picUrl = song.al.picUrl || albumInfo.picUrl || song.picUrl;\n      return song;\n    });\n\n    navigateToMusicList(router, {\n      id: album.id,\n      type: 'album',\n      name: album.name,\n      songList: formattedSongs,\n      listInfo: {\n        ...albumInfo,\n        creator: {\n          avatarUrl: albumInfo.artist.img1v1Url,\n          nickname: `${albumInfo.artist.name} - ${albumInfo.company}`\n        },\n        description: albumInfo.description\n      }\n    });\n  } catch (error) {\n    console.error('获取专辑详情失败:', error);\n  }\n};\n\nonMounted(() => {\n  loadAlbumList();\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.recommend-album {\n  @apply flex-1 mx-5;\n  .title {\n    @apply text-lg font-bold mb-4 text-gray-900 dark:text-white;\n  }\n\n  .recommend-album-list {\n    @apply grid grid-cols-2 grid-rows-3 gap-2;\n    &-item {\n      @apply rounded-xl overflow-hidden relative;\n      &-img {\n        @apply rounded-xl transition w-full h-full;\n      }\n      &:hover img {\n        filter: brightness(50%);\n      }\n      &-content {\n        @apply w-full h-full opacity-0 transition absolute z-10 top-0 left-0 p-4 text-xl text-white bg-opacity-60 bg-black dark:bg-opacity-60 dark:bg-black;\n      }\n      &-content:hover {\n        opacity: 1;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/home/RecommendSonglist.vue",
    "content": "<template>\n  <div class=\"recommend-music\">\n    <div class=\"title\" :class=\"setAnimationClass('animate__fadeInLeft')\">\n      {{ t('comp.recommendSonglist.title') }}\n    </div>\n    <div\n      v-show=\"recommendMusic?.result\"\n      v-loading=\"loading\"\n      class=\"recommend-music-list\"\n      :class=\"setAnimationClass('animate__bounceInUp')\"\n    >\n      <!-- 推荐音乐列表 -->\n      <template v-for=\"(item, index) in recommendMusic?.result\" :key=\"item.id\">\n        <div\n          :class=\"setAnimationClass('animate__bounceInUp')\"\n          :style=\"setAnimationDelay(index, 100)\"\n        >\n          <song-item :item=\"item\" @play=\"handlePlay\" />\n        </div>\n      </template>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useI18n } from 'vue-i18n';\n\nimport { getRecommendMusic } from '@/api/home';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { usePlayerStore } from '@/store/modules/player';\nimport type { IRecommendMusic } from '@/types/music';\nimport { setAnimationClass, setAnimationDelay } from '@/utils';\n\nconst { t } = useI18n();\nconst playerStore = usePlayerStore();\n// 推荐歌曲\nconst recommendMusic = ref<IRecommendMusic>();\nconst loading = ref(false);\n\n// 加载推荐歌曲\nconst loadRecommendMusic = async () => {\n  loading.value = true;\n  const { data } = await getRecommendMusic({ limit: 10 });\n  recommendMusic.value = data;\n  loading.value = false;\n};\n\n// 页面初始化\nonMounted(() => {\n  loadRecommendMusic();\n});\n\nconst handlePlay = () => {\n  if (recommendMusic.value?.result) {\n    playerStore.setPlayList(recommendMusic.value.result);\n  }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.title {\n  @apply text-lg font-bold mb-4 text-gray-900 dark:text-white;\n}\n.recommend-music {\n  @apply flex-auto;\n  .text-ellipsis {\n    width: 100%;\n  }\n  &-list {\n    @apply rounded-3xl p-2 w-full border border-gray-200 dark:border-gray-700 bg-light dark:bg-black;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/home/TopBanner.vue",
    "content": "<template>\n  <div class=\"recommend-singer\">\n    <div class=\"recommend-singer-list\">\n      <n-carousel\n        v-if=\"hotSingerData?.artists.length\"\n        slides-per-view=\"auto\"\n        :show-dots=\"false\"\n        :space-between=\"20\"\n        draggable\n        show-arrow\n        :autoplay=\"false\"\n      >\n        <n-carousel-item\n          :class=\"setAnimationClass('animate__backInRight')\"\n          :style=\"getCarouselItemStyle(0, 100, 6)\"\n        >\n          <div v-if=\"dayRecommendData\" class=\"recommend-singer-item relative\">\n            <div\n              :style=\"\n                setBackgroundImg(getImgUrl(dayRecommendData?.dailySongs[0].al.picUrl, '500y500'))\n              \"\n              class=\"recommend-singer-item-bg\"\n            ></div>\n            <div\n              class=\"recommend-singer-item-count p-2 text-base text-gray-200 z-10 cursor-pointer\"\n              @click=\"showDayRecommend\"\n            >\n              <div class=\"font-bold text-lg\">\n                {{ t('comp.recommendSinger.title') }}\n              </div>\n\n              <div class=\"mt-2\">\n                <p v-for=\"item in getDisplayDaySongs.slice(0, 5)\" :key=\"item.id\" class=\"text-el\">\n                  {{ item.name }}\n                  <br />\n                </p>\n              </div>\n            </div>\n          </div>\n        </n-carousel-item>\n\n        <n-carousel-item\n          v-if=\"userStore.user && userPlaylist.length\"\n          :class=\"setAnimationClass('animate__backInRight')\"\n          :style=\"getCarouselItemStyleForPlaylist(userPlaylist.length)\"\n        >\n          <div class=\"user-play\">\n            <div class=\"user-play-title mb-3\">\n              {{ t('comp.userPlayList.title', { name: userStore.user?.nickname }) }}\n            </div>\n            <div class=\"user-play-list\" :class=\"getPlaylistGridClass(userPlaylist.length)\">\n              <div\n                v-for=\"item in userPlaylist\"\n                :key=\"item.id\"\n                class=\"user-play-item\"\n                @click=\"openPlaylist(item)\"\n              >\n                <div class=\"user-play-item-img\">\n                  <img :src=\"getImgUrl(item.coverImgUrl, '200y200')\" alt=\"\" />\n                  <div class=\"user-play-item-title\">\n                    <div class=\"user-play-item-title-name\">{{ item.name }}</div>\n\n                    <div class=\"user-play-item-list\">\n                      <div\n                        v-for=\"song in item.tracks\"\n                        :key=\"song.id\"\n                        class=\"user-play-item-list-name\"\n                      >\n                        {{ song.name }}\n                      </div>\n                    </div>\n                  </div>\n                  <div class=\"user-play-item-count\">\n                    <div class=\"user-play-item-count-tag\">\n                      {{ t('common.songCount', { count: item.trackCount }) }}\n                    </div>\n                  </div>\n                  <div class=\"user-play-item-direct-play\" @click.stop=\"handlePlayPlaylist(item.id)\">\n                    <i class=\"iconfont icon-playfill text-xl text-white\"></i>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </n-carousel-item>\n        <n-carousel-item\n          v-for=\"(item, index) in hotSingerData?.artists\"\n          :key=\"item.id\"\n          :class=\"setAnimationClass('animate__backInRight')\"\n          :style=\"getCarouselItemStyle(index + 1, 100, 6)\"\n        >\n          <div\n            class=\"recommend-singer-item relative\"\n            :class=\"setAnimationClass('animate__backInRight')\"\n            :style=\"setAnimationDelay(index + 2, 100)\"\n            @click=\"handleArtistClick(item.id)\"\n          >\n            <div\n              :style=\"\n                setBackgroundImg(getImgUrl(item.picUrl || item.avatar || item.cover, '500y500'))\n              \"\n              class=\"recommend-singer-item-bg\"\n            ></div>\n            <div class=\"recommend-singer-item-count p-2 text-base text-gray-200 z-10\">\n              {{ t('common.songCount', { count: item.musicSize }) }}\n            </div>\n            <div class=\"recommend-singer-item-info z-10\">\n              <div class=\"recommend-singer-item-info-name text-el text-right line-clamp-1\">\n                {{ item.name }}\n              </div>\n            </div>\n            <!-- 播放按钮(hover时显示) -->\n            <div\n              class=\"recommend-singer-item-play-overlay\"\n              @click.stop=\"handleArtistClick(item.id)\"\n            >\n              <div class=\"recommend-singer-item-play-btn\">\n                <i class=\"iconfont icon-playfill text-4xl\"></i>\n              </div>\n            </div>\n          </div>\n        </n-carousel-item>\n      </n-carousel>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, onMounted, ref, watchEffect } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\n\nimport { getHotSinger } from '@/api/home';\nimport { getListDetail } from '@/api/list';\nimport { getMusicDetail } from '@/api/music';\nimport { getUserPlaylist } from '@/api/user';\nimport { navigateToMusicList } from '@/components/common/MusicListNavigator';\nimport { useArtist } from '@/hooks/useArtist';\nimport { usePlayerStore, useRecommendStore, useUserStore } from '@/store';\nimport { Playlist } from '@/types/list';\nimport type { IListDetail } from '@/types/listDetail';\nimport { SongResult } from '@/types/music';\nimport type { IHotSinger } from '@/types/singer';\nimport {\n  getImgUrl,\n  isMobile,\n  setAnimationClass,\n  setAnimationDelay,\n  setBackgroundImg\n} from '@/utils';\n\nconst userStore = useUserStore();\nconst playerStore = usePlayerStore();\nconst recommendStore = useRecommendStore();\nconst router = useRouter();\n\nconst { t } = useI18n();\n\n// 歌手信息\nconst hotSingerData = ref<IHotSinger>();\nconst dayRecommendData = computed(() => {\n  if (recommendStore.dailyRecommendSongs.length > 0) {\n    return {\n      dailySongs: recommendStore.dailyRecommendSongs\n    };\n  }\n  return null;\n});\nconst userPlaylist = ref<Playlist[]>([]);\n\n// 为歌单弹窗添加的状态\nconst playlistLoading = ref(false);\nconst playlistItem = ref<Playlist | null>(null);\nconst playlistDetail = ref<IListDetail | null>(null);\n\nconst { navigateToArtist } = useArtist();\n\n/**\n * 获取轮播项的样式\n * @param index 项目索引（用于动画延迟）\n * @param delayStep 动画延迟的步长（毫秒）\n * @param totalItems 总共分成几等分（默认为5）\n * @param maxWidth 最大宽度（可选，单位为px）\n * @returns 样式字符串\n */\nconst getCarouselItemStyle = (\n  index: number,\n  delayStep: number,\n  totalItems: number,\n  maxWidth?: number\n) => {\n  if (isMobile.value) {\n    return 'width: 30%;';\n  }\n  const animationDelay = setAnimationDelay(index, delayStep);\n  const width = `calc((100% / ${totalItems}) - 16px)`;\n  const maxWidthStyle = maxWidth ? `max-width: ${maxWidth}px;` : '';\n\n  return `${animationDelay}; width: ${width}; ${maxWidthStyle}`;\n};\n\n/**\n * 根据歌单数量获取轮播项的样式\n * @param playlistCount 歌单数量\n * @returns 样式字符串\n */\nconst getCarouselItemStyleForPlaylist = (playlistCount: number) => {\n  if (isMobile.value) {\n    return 'width: 100%;';\n  }\n  const animationDelay = setAnimationDelay(1, 100);\n  let width = '';\n  let maxWidth = '';\n\n  switch (playlistCount) {\n    case 1:\n      width = 'calc(100% / 4 - 16px)';\n      maxWidth = 'max-width: 180px;';\n      break;\n    case 2:\n      width = 'calc(100% / 3 - 16px)';\n      maxWidth = 'max-width: 380px;';\n      break;\n    case 3:\n      width = 'calc(100% / 2 - 16px)';\n      maxWidth = 'max-width: 520px;';\n      break;\n    default:\n      width = 'calc(100% / 1 - 16px)';\n      maxWidth = 'max-width: 656px;';\n  }\n\n  return `${animationDelay}; width: ${width}; ${maxWidth}`;\n};\n\nonMounted(async () => {\n  loadNonUserData();\n});\n\nconst loadDayRecommendData = async () => {\n  await recommendStore.fetchDailyRecommendSongs();\n};\n\n// 加载不需要登录的数据\nconst loadNonUserData = async () => {\n  try {\n    // 获取每日推荐（仅在用户未登录时加载，已登录用户会通过watchEffect触发loadDayRecommendData）\n    if (!userStore.user) {\n      await loadDayRecommendData();\n    }\n\n    // 获取热门歌手\n    const { data: singerData } = await getHotSinger({ offset: 0, limit: 5 });\n    hotSingerData.value = singerData;\n  } catch (error) {\n    console.error('加载热门歌手数据失败:', error);\n  }\n};\n\n// 加载需要登录的数据\nconst loadUserData = async () => {\n  try {\n    if (userStore.user) {\n      const { data: playlistData } = await getUserPlaylist(userStore.user?.userId);\n      // 确保最多只显示4个歌单，并按播放次数排序\n      userPlaylist.value = (playlistData.playlist as Playlist[])\n        .sort((a, b) => b.playCount - a.playCount)\n        .slice(0, 4);\n    }\n  } catch (error) {\n    console.error('加载用户数据失败:', error);\n  }\n};\n\nconst handleArtistClick = (id: number) => {\n  navigateToArtist(id);\n};\nconst getDisplayDaySongs = computed(() => {\n  if (!dayRecommendData.value) {\n    return [];\n  }\n  return dayRecommendData.value.dailySongs.filter(\n    (song) => !playerStore.dislikeList.includes(song.id)\n  );\n});\n\nconst showDayRecommend = () => {\n  if (!dayRecommendData.value?.dailySongs) return;\n\n  navigateToMusicList(router, {\n    type: 'dailyRecommend',\n    name: t('comp.recommendSinger.songlist'),\n    songList: getDisplayDaySongs.value,\n    canRemove: false\n  });\n};\n\nconst openPlaylist = (item: any) => {\n  playlistItem.value = item;\n  playlistLoading.value = true;\n\n  getListDetail(item.id).then((res) => {\n    playlistDetail.value = res.data;\n    playlistLoading.value = false;\n\n    navigateToMusicList(router, {\n      id: item.id,\n      type: 'playlist',\n      name: item.name,\n      songList: res.data.playlist.tracks || [],\n      listInfo: res.data.playlist,\n      canRemove: false\n    });\n  });\n};\n\n// 添加直接播放歌单的方法\nconst handlePlayPlaylist = async (id: number) => {\n  try {\n    // 先显示加载状态\n    playlistLoading.value = true;\n\n    // 获取歌单详情\n    const { data } = await getListDetail(id);\n\n    if (data?.playlist) {\n      // 先使用已有的tracks开始播放（这些是已经在歌单详情中返回的前几首歌曲）\n      if (data.playlist.tracks?.length > 0) {\n        // 格式化歌曲列表\n        const initialSongs = data.playlist.tracks.map((track) => ({\n          ...track,\n          source: 'netease',\n          picUrl: track.al.picUrl\n        })) as unknown as SongResult[];\n\n        // 设置播放列表\n        playerStore.setPlayList(initialSongs);\n\n        // 开始播放第一首\n        await playerStore.setPlay(initialSongs[0]);\n\n        // 如果有trackIds，异步加载完整歌单\n        if (data.playlist.trackIds?.length > initialSongs.length) {\n          loadFullPlaylist(data.playlist.trackIds, initialSongs);\n        }\n      }\n    }\n\n    // 关闭加载状态\n    playlistLoading.value = false;\n  } catch (error) {\n    console.error('播放歌单失败:', error);\n    playlistLoading.value = false;\n  }\n};\n\n// 异步加载完整歌单\nconst loadFullPlaylist = async (trackIds: { id: number }[], initialSongs: SongResult[]) => {\n  try {\n    // 获取已加载歌曲的ID集合，避免重复加载\n    const loadedIds = new Set(initialSongs.map((song) => song.id));\n\n    // 筛选出未加载的ID\n    const unloadedTrackIds = trackIds\n      .filter((item) => !loadedIds.has(item.id as number))\n      .map((item) => item.id);\n\n    if (unloadedTrackIds.length === 0) return;\n\n    // 分批获取歌曲详情，每批最多获取500首\n    const batchSize = 500;\n    const allSongs = [...initialSongs];\n\n    for (let i = 0; i < unloadedTrackIds.length; i += batchSize) {\n      const batchIds = unloadedTrackIds.slice(i, i + batchSize);\n      if (batchIds.length > 0) {\n        try {\n          const { data: songsData } = await getMusicDetail(batchIds);\n          if (songsData?.songs?.length) {\n            const formattedSongs = songsData.songs.map((item) => ({\n              ...item,\n              source: 'netease',\n              picUrl: item.al.picUrl\n            })) as unknown as SongResult[];\n\n            allSongs.push(...formattedSongs);\n          }\n        } catch (error) {\n          console.error('获取批次歌曲详情失败:', error);\n        }\n      }\n    }\n\n    // 更新完整的播放列表但保持当前播放的歌曲不变\n    if (allSongs.length > initialSongs.length) {\n      console.log('更新播放列表，总歌曲数:', allSongs.length);\n      playerStore.setPlayList(allSongs);\n    }\n  } catch (error) {\n    console.error('加载完整歌单失败:', error);\n  }\n};\n\n// 监听登录状态\nwatchEffect(() => {\n  if (userStore.user) {\n    loadUserData();\n    loadDayRecommendData();\n  }\n});\n\nconst getPlaylistGridClass = (length: number) => {\n  switch (length) {\n    case 1:\n      return 'one-column';\n    case 2:\n      return 'two-columns';\n    case 3:\n      return 'three-columns';\n    default:\n      return 'four-columns';\n  }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.recommend-singer {\n  &-list {\n    @apply flex;\n    height: 220px;\n    margin-right: 20px;\n  }\n  &-item {\n    @apply flex-1 h-full rounded-3xl p-5 flex flex-col justify-between overflow-hidden relative;\n    cursor: pointer;\n    transition: transform 0.3s ease;\n\n    &:hover {\n      transform: translateY(-5px);\n    }\n\n    &-bg {\n      @apply bg-gray-900 dark:bg-gray-800 bg-no-repeat bg-cover bg-center rounded-3xl absolute w-full h-full top-0 left-0 z-0;\n      filter: brightness(60%);\n    }\n\n    &-info {\n      @apply flex flex-col p-2;\n      &-name {\n        @apply text-gray-100 dark:text-gray-100;\n      }\n    }\n\n    &-count {\n      @apply text-gray-100 dark:text-gray-100;\n    }\n\n    &-play {\n      &-overlay {\n        @apply absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-black/20 z-20 opacity-0 transition-all duration-300 flex items-center justify-center;\n        backdrop-filter: blur(1px);\n\n        .recommend-singer-item:hover & {\n          opacity: 1;\n        }\n      }\n\n      &-btn {\n        @apply w-20 h-20 bg-transparent flex justify-center items-center text-white;\n        transform: translateY(50px) scale(0.8);\n        transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);\n\n        .recommend-singer-item:hover & {\n          transform: translateY(0) scale(1);\n        }\n      }\n    }\n  }\n}\n\n.user-play {\n  @apply bg-light-300 dark:bg-dark-300 rounded-3xl px-4 py-3 h-full;\n  backdrop-filter: blur(20px);\n  &-title {\n    @apply text-gray-900 dark:text-gray-100 font-bold text-lg line-clamp-1;\n  }\n  &-list {\n    @apply grid gap-3 h-full;\n    &.one-column {\n      grid-template-columns: repeat(1, minmax(0, 1fr));\n      .user-play-item {\n        max-width: 100%;\n      }\n    }\n    &.two-columns {\n      grid-template-columns: repeat(2, minmax(0, 1fr));\n      .user-play-item {\n        max-width: 100%;\n      }\n    }\n    &.three-columns {\n      grid-template-columns: repeat(3, minmax(0, 1fr));\n      .user-play-item {\n        max-width: 100%;\n      }\n    }\n    &.four-columns {\n      grid-template-columns: repeat(4, minmax(0, 1fr));\n      .user-play-item {\n        max-width: 100%;\n      }\n    }\n  }\n  &-item {\n    @apply rounded-2xl overflow-hidden flex flex-col;\n    height: 176px;\n\n    &-img {\n      @apply relative cursor-pointer transition-all duration-300;\n      height: 0;\n      width: 100%;\n      padding-bottom: 100%; /* 确保宽高比为1:1，即正方形 */\n      border-radius: 12px;\n      overflow: hidden;\n      &:hover {\n        transform: translateY(-5px);\n        box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);\n      }\n\n      img {\n        @apply absolute inset-0 w-full h-full object-cover;\n      }\n    }\n    &-title {\n      @apply absolute top-0 left-0 right-0 p-2 bg-gradient-to-b from-black/70 to-transparent z-10;\n      &-name {\n        @apply text-white font-medium text-sm line-clamp-3;\n      }\n    }\n    &-count {\n      @apply absolute bottom-2 left-2 z-10;\n      &-tag {\n        @apply px-2 py-0.5 text-xs text-white bg-black/50 backdrop-blur-sm rounded-full;\n      }\n    }\n    &-direct-play {\n      @apply absolute bottom-2 right-2 z-20 w-10 h-10 rounded-full bg-green-600 hover:bg-green-700 flex items-center justify-center cursor-pointer transform scale-90 hover:scale-100 transition-all;\n      &:hover {\n        @apply shadow-lg;\n      }\n    }\n    &-play-btn {\n      @apply flex items-center justify-center;\n      transform: scale(0.8);\n      transition: transform 0.3s ease;\n\n      .user-play-item:hover & {\n        transform: scale(1);\n      }\n    }\n  }\n}\n.mobile {\n  .recommend-singer {\n    &-list {\n      height: 180px;\n      @apply ml-4;\n    }\n    &-item {\n      @apply p-2 rounded-xl;\n      &-bg {\n        @apply rounded-xl;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/login/CookieLogin.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useMessage } from 'naive-ui';\nimport { onBeforeUnmount, onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { getUserDetail } from '@/api/login';\nimport { isElectron, setAnimationClass } from '@/utils';\n\ndefineOptions({\n  name: 'CookieLogin'\n});\n\n// Emits\ninterface Emits {\n  (e: 'loginSuccess', userProfile: any, loginType: string): void;\n  (e: 'loginError', error: string): void;\n}\n\nconst emit = defineEmits<Emits>();\n\nconst { t } = useI18n();\nconst message = useMessage();\n\nconst token = ref('');\n\n// Token登录\nconst loginByToken = async () => {\n  if (!token.value.trim()) {\n    const errorMsg = t('login.message.tokenRequired');\n    message.error(errorMsg);\n    emit('loginError', errorMsg);\n    return;\n  }\n\n  try {\n    // 直接设置token到localStorage\n    localStorage.setItem('token', token.value.trim());\n\n    // 获取用户信息验证token有效性\n    const user = await getUserDetail();\n    if (user.data && user.data.profile) {\n      const successMsg = t('login.message.tokenLoginSuccess');\n      message.success(successMsg);\n      emit('loginSuccess', user.data.profile, 'cookie');\n    } else {\n      // token无效，清除localStorage\n      localStorage.removeItem('token');\n      const errorMsg = t('login.message.tokenInvalid');\n      message.error(errorMsg);\n      emit('loginError', errorMsg);\n    }\n  } catch (error) {\n    // token无效，清除localStorage\n    localStorage.removeItem('token');\n    const errorMsg = t('login.message.tokenInvalid');\n    message.error(errorMsg);\n    emit('loginError', errorMsg);\n    console.error('Token登录失败:', error);\n  }\n};\n\n// 自动获取Cookie\nconst autoGetCookie = () => {\n  if (!isElectron) {\n    message.error('此功能仅在桌面版中可用');\n    return;\n  }\n\n  message.info(t('login.message.autoGetCookieTip'));\n  window.electron.ipcRenderer.send('open-login');\n};\n\n// 监听Cookie接收\nconst handleCookieReceived = async (_event: any, cookieValue: string) => {\n  try {\n    // 设置Cookie到localStorage\n    localStorage.setItem('token', cookieValue);\n\n    // 验证Cookie有效性\n    const user = await getUserDetail();\n    if (user.data && user.data.profile) {\n      const successMsg = t('login.message.autoGetCookieSuccess');\n      message.success(successMsg);\n      emit('loginSuccess', user.data.profile, 'cookie');\n    } else {\n      localStorage.removeItem('token');\n      const errorMsg = t('login.message.autoGetCookieFailed');\n      message.error(errorMsg);\n      emit('loginError', errorMsg);\n    }\n  } catch (error) {\n    localStorage.removeItem('token');\n    const errorMsg = t('login.message.autoGetCookieFailed');\n    message.error(errorMsg);\n    emit('loginError', errorMsg);\n    console.error('自动获取Cookie失败:', error);\n  }\n};\n\n// 在组件挂载时添加监听器\nonMounted(() => {\n  if (isElectron) {\n    window.electron.ipcRenderer.on('send-cookies', handleCookieReceived);\n  }\n});\n\n// 在组件卸载时移除监听器\nonBeforeUnmount(() => {\n  if (isElectron) {\n    window.electron.ipcRenderer.removeAllListeners('send-cookies');\n  }\n});\n</script>\n\n<template>\n  <div class=\"cookie-login\" :class=\"setAnimationClass('animate__fadeInUp')\">\n    <div class=\"login-title\">{{ t('login.title.cookie') }}</div>\n    <div class=\"phone-page\">\n      <textarea\n        v-model=\"token\"\n        class=\"token-input\"\n        :placeholder=\"t('login.placeholder.cookie')\"\n        rows=\"4\"\n      />\n    </div>\n    <div class=\"text\">{{ t('login.tokenTip') }}</div>\n    <n-button class=\"btn-login\" @click=\"loginByToken()\">{{\n      t('login.button.cookieLogin')\n    }}</n-button>\n    <n-button v-if=\"isElectron\" class=\"btn-auto-cookie\" @click=\"autoGetCookie()\" type=\"info\">\n      {{ t('login.button.autoGetCookie') }}\n    </n-button>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.cookie-login {\n  animation-duration: 0.5s;\n  @apply flex flex-col items-center;\n}\n\n.login-title {\n  @apply text-2xl font-bold mb-6 text-white;\n}\n\n.text {\n  @apply mt-4 text-white text-xs;\n}\n\n.phone-page {\n  @apply bg-light dark:bg-gray-800 bg-opacity-90 dark:bg-opacity-90;\n  width: 250px;\n  @apply rounded-2xl overflow-hidden;\n  padding: 0;\n  border: none;\n}\n\n.token-input {\n  @apply w-full outline-none resize-none;\n  @apply text-gray-900 dark:text-white bg-transparent;\n  @apply placeholder-gray-500 dark:placeholder-gray-400;\n  font-family: monospace;\n  font-size: 12px;\n  line-height: 1.4;\n  min-height: 100px;\n  padding: 16px;\n  margin: 0;\n  border: none;\n  border-radius: inherit;\n  box-sizing: border-box;\n\n  &:focus {\n    @apply outline-none;\n    box-shadow: none;\n    border: none;\n  }\n\n  &::placeholder {\n    @apply text-gray-400 dark:text-gray-500;\n  }\n\n  /* 移除浏览器默认样式 */\n  &::-webkit-scrollbar {\n    width: 4px;\n  }\n\n  &::-webkit-scrollbar-track {\n    background: transparent;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background: rgba(156, 163, 175, 0.3);\n    border-radius: 2px;\n  }\n\n  &::-webkit-scrollbar-thumb:hover {\n    background: rgba(156, 163, 175, 0.5);\n  }\n}\n\n.btn-login {\n  width: 250px;\n  height: 40px;\n  @apply mt-10 text-white rounded-xl;\n  @apply bg-green-600 hover:bg-green-700 transition-colors;\n}\n\n.btn-auto-cookie {\n  width: 250px;\n  height: 40px;\n  @apply mt-4 text-white rounded-xl;\n  @apply bg-blue-600 hover:bg-blue-700 transition-colors;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/login/QrLogin.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useMessage } from 'naive-ui';\nimport { onMounted } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { checkQr, createQr, getQrKey, getUserDetail } from '@/api/login';\nimport { setAnimationClass } from '@/utils';\n\ndefineOptions({\n  name: 'QrLogin'\n});\n\n// Emits\ninterface Emits {\n  (e: 'loginSuccess', userProfile: any, loginType: string): void;\n  (e: 'loginError', error: string): void;\n}\n\nconst emit = defineEmits<Emits>();\n\nconst { t } = useI18n();\nconst message = useMessage();\n\nconst qrUrl = ref<string>();\nconst timerRef = ref(null);\nconst qrStatus = ref<'loading' | 'active' | 'expired' | 'scanned' | 'confirmed'>('loading');\nconst isRefreshing = ref(false);\n\nconst loadLogin = async () => {\n  try {\n    isRefreshing.value = true;\n    qrStatus.value = 'loading';\n\n    // 清理之前的定时器\n    if (timerRef.value) {\n      clearInterval(timerRef.value);\n      timerRef.value = null;\n    }\n\n    const qrKey = await getQrKey();\n    const key = qrKey.data.data.unikey;\n    const { data } = await createQr(key);\n    qrUrl.value = data.data.qrimg;\n    qrStatus.value = 'active';\n\n    const timer = timerIsQr(key);\n    timerRef.value = timer as any;\n  } catch (error) {\n    console.error(t('login.message.loadError'), error);\n    qrStatus.value = 'expired';\n    const errorMsg = t('login.message.loadError');\n    message.error(errorMsg);\n    emit('loginError', errorMsg);\n  } finally {\n    isRefreshing.value = false;\n  }\n};\n\nconst timerIsQr = (key: string) => {\n  const timer = setInterval(async () => {\n    try {\n      const { data } = await checkQr(key);\n\n      // 二维码过期或不存在\n      if (data.code === 800) {\n        qrStatus.value = 'expired';\n        clearInterval(timer);\n        timerRef.value = null;\n        message.warning(t('login.message.qrExpiredWarning'));\n        return;\n      }\n\n      // 等待扫码\n      if (data.code === 801) {\n        qrStatus.value = 'active';\n        return;\n      }\n\n      // 已扫码，等待确认\n      if (data.code === 802) {\n        qrStatus.value = 'scanned';\n        message.info(t('login.message.qrScannedInfo'));\n        return;\n      }\n\n      // 登录成功\n      if (data.code === 803) {\n        qrStatus.value = 'confirmed';\n        localStorage.setItem('token', data.cookie);\n        const user = await getUserDetail();\n        const successMsg = t('login.message.loginSuccess');\n        message.success(successMsg);\n        emit('loginSuccess', user.data.profile, 'qr');\n\n        clearInterval(timer);\n        timerRef.value = null;\n      }\n    } catch (error) {\n      console.error(t('login.message.qrCheckError'), error);\n      qrStatus.value = 'expired';\n      clearInterval(timer);\n      timerRef.value = null;\n      const errorMsg = t('login.message.qrCheckFailed');\n      message.error(errorMsg);\n      emit('loginError', errorMsg);\n    }\n  }, 3000);\n\n  return timer;\n};\n\n// 手动刷新二维码\nconst refreshQr = () => {\n  loadLogin();\n};\n\n// 获取状态显示文本\nconst getStatusText = () => {\n  switch (qrStatus.value) {\n    case 'loading':\n      return t('login.message.qrLoading');\n    case 'active':\n      return t('login.qrTip');\n    case 'expired':\n      return t('login.message.qrExpired');\n    case 'scanned':\n      return t('login.message.qrScanned');\n    case 'confirmed':\n      return t('login.message.qrConfirmed');\n    default:\n      return t('login.qrTip');\n  }\n};\n\nonMounted(() => {\n  loadLogin();\n});\n\nonUnmounted(() => {\n  if (timerRef.value) {\n    clearInterval(timerRef.value);\n    timerRef.value = null;\n  }\n});\n</script>\n\n<template>\n  <div class=\"qr-login\" :class=\"setAnimationClass('animate__fadeInUp')\">\n    <div class=\"login-title\">{{ t('login.title.qr') }}</div>\n\n    <!-- 二维码容器 -->\n    <div class=\"qr-container\">\n      <!-- 加载状态 -->\n      <div v-if=\"qrStatus === 'loading'\" class=\"qr-loading\">\n        <n-spin size=\"large\" />\n        <div class=\"loading-text\">{{ t('login.message.qrGenerating') }}</div>\n      </div>\n\n      <!-- 二维码图片 -->\n      <div v-else class=\"qr-image-wrapper\" :class=\"{ expired: qrStatus === 'expired' }\">\n        <img class=\"qr-img\" :src=\"qrUrl\" />\n\n        <!-- 过期遮罩 -->\n        <div v-if=\"qrStatus === 'expired'\" class=\"expired-overlay\">\n          <div class=\"expired-text\">{{ t('login.message.qrExpiredShort') }}</div>\n          <n-button class=\"refresh-btn\" type=\"primary\" @click=\"refreshQr\" :loading=\"isRefreshing\">\n            {{ isRefreshing ? t('login.button.refreshing') : t('login.button.refresh') }}\n          </n-button>\n        </div>\n\n        <!-- 已扫码遮罩 -->\n        <div v-if=\"qrStatus === 'scanned'\" class=\"scanned-overlay\">\n          <div class=\"scanned-icon\">✓</div>\n          <div class=\"scanned-text\">{{ t('login.message.qrScannedShort') }}</div>\n        </div>\n      </div>\n    </div>\n\n    <!-- 状态文本 -->\n    <div class=\"text\" :class=\"{ expired: qrStatus === 'expired', scanned: qrStatus === 'scanned' }\">\n      {{ getStatusText() }}\n    </div>\n\n    <!-- 手动刷新按钮 -->\n    <div v-if=\"qrStatus === 'active'\" class=\"refresh-area\">\n      <n-button text class=\"manual-refresh\" @click=\"refreshQr\" :loading=\"isRefreshing\">\n        {{ t('login.button.refreshQr') }}\n      </n-button>\n    </div>\n  </div>\n</template>\n\n<style lang=\"scss\" scoped>\n.qr-login {\n  animation-duration: 0.5s;\n}\n\n.login-title {\n  @apply text-2xl font-bold mb-6 text-white;\n}\n\n.qr-container {\n  @apply relative;\n  width: 200px;\n  height: 200px;\n  @apply mx-auto;\n}\n\n.qr-loading {\n  @apply flex flex-col items-center justify-center h-full;\n\n  .loading-text {\n    @apply mt-4 text-white text-sm;\n  }\n}\n\n.qr-image-wrapper {\n  @apply relative w-full h-full;\n\n  &.expired {\n    .qr-img {\n      @apply opacity-30;\n    }\n  }\n}\n\n.qr-img {\n  @apply w-full h-full rounded-2xl transition-all duration-300;\n  object-fit: cover;\n}\n\n.expired-overlay {\n  @apply absolute inset-0 flex flex-col items-center justify-center;\n  @apply bg-black bg-opacity-50 rounded-2xl;\n\n  .expired-text {\n    @apply text-white text-sm mb-3;\n  }\n\n  .refresh-btn {\n    @apply text-sm;\n  }\n}\n\n.scanned-overlay {\n  @apply absolute inset-0 flex flex-col items-center justify-center;\n  @apply bg-green-500 bg-opacity-80 rounded-2xl;\n\n  .scanned-icon {\n    @apply text-white text-4xl font-bold mb-2;\n  }\n\n  .scanned-text {\n    @apply text-white text-sm;\n  }\n}\n\n.text {\n  @apply mt-4 text-white text-xs transition-colors duration-300;\n\n  &.expired {\n    @apply text-orange-400;\n  }\n\n  &.scanned {\n    @apply text-green-400;\n  }\n}\n\n.refresh-area {\n  @apply mt-3;\n\n  .manual-refresh {\n    @apply text-gray-300 hover:text-white text-xs;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/login/UidLogin.vue",
    "content": "<template>\n  <div class=\"uid-login\">\n    <div class=\"login-title\">{{ t('login.title.uid') }}</div>\n    <div class=\"uid-page\">\n      <input\n        v-model=\"uid\"\n        class=\"uid-input\"\n        type=\"text\"\n        :placeholder=\"t('login.placeholder.uid')\"\n        @keyup.enter=\"handleLogin\"\n      />\n    </div>\n    <div class=\"text\">{{ t('login.uidTip') }}</div>\n    <div class=\"warning-text\">\n      {{ t('login.uidWarning') }}\n    </div>\n    <n-button class=\"btn-login\" :loading=\"loading\" @click=\"handleLogin\">\n      {{ t('login.button.login') }}\n    </n-button>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useMessage } from 'naive-ui';\nimport { useI18n } from 'vue-i18n';\n\nimport { loginByUid } from '@/api/login';\n\ndefineOptions({\n  name: 'UidLogin'\n});\n\n// Props\ninterface Props {\n  disabled?: boolean;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  disabled: false\n});\n\n// Emits\ninterface Emits {\n  (e: 'loginSuccess', userProfile: any, loginType: string): void;\n  (e: 'loginError', error: string): void;\n}\n\nconst emit = defineEmits<Emits>();\n\nconst { t } = useI18n();\nconst message = useMessage();\n\n// 状态\nconst uid = ref('');\nconst loading = ref(false);\n\n// UID登录处理\nconst handleLogin = async () => {\n  if (props.disabled || loading.value) return;\n\n  if (!uid.value.trim()) {\n    const errorMsg = t('login.message.uidRequired');\n    message.error(errorMsg);\n    emit('loginError', errorMsg);\n    return;\n  }\n\n  try {\n    loading.value = true;\n    const { data } = await loginByUid(uid.value);\n\n    if (data && data.profile) {\n      const successMsg = t('login.message.uidLoginSuccess');\n      message.success(successMsg);\n      emit('loginSuccess', data.profile, 'uid');\n    } else {\n      const errorMsg = t('login.message.uidInvalid');\n      message.error(errorMsg);\n      emit('loginError', errorMsg);\n    }\n  } catch (error: any) {\n    console.error('UID登录失败:', error);\n    let errorMsg = t('login.message.uidLoginFailed');\n\n    if (error.response?.status === 404 || error.response?.data?.code === 404) {\n      errorMsg = t('login.message.uidInvalid');\n    }\n\n    message.error(errorMsg);\n    emit('loginError', errorMsg);\n  } finally {\n    loading.value = false;\n  }\n};\n\n// 重置表单\nconst reset = () => {\n  uid.value = '';\n  loading.value = false;\n};\n\n// 暴露方法给父组件\ndefineExpose({\n  reset\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.uid-login {\n  animation-duration: 0.5s;\n  width: 250px;\n\n  .login-title {\n    @apply text-2xl font-bold mb-6 text-white;\n  }\n\n  .text {\n    @apply mt-4 text-white text-xs;\n  }\n\n  .warning-text {\n    @apply mt-2 text-orange-400 text-xs text-center max-w-xs;\n    line-height: 1.4;\n  }\n\n  .uid-page {\n    @apply bg-light dark:bg-gray-800 bg-opacity-90 dark:bg-opacity-90;\n    @apply rounded-2xl overflow-hidden;\n  }\n\n  .uid-input {\n    height: 40px;\n    @apply w-full px-4 outline-none;\n    @apply text-gray-900 dark:text-white bg-transparent;\n    @apply border-b border-gray-200 dark:border-gray-700;\n    @apply placeholder-gray-500 dark:placeholder-gray-400;\n\n    &:focus {\n      @apply border-green-500;\n    }\n  }\n\n  .btn-login {\n    width: 250px;\n    height: 40px;\n    @apply mt-10 text-white rounded-xl;\n    @apply bg-green-600 hover:bg-green-700 transition-colors;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/lyric/LyricCorrectionControl.vue",
    "content": "<script setup lang=\"ts\">\nimport { defineEmits, defineProps } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps<{\n  correctionTime: number;\n}>();\nconst emit = defineEmits<{\n  (e: 'adjust', delta: number): void;\n}>();\n\nconst { t } = useI18n();\n</script>\n\n<template>\n  <div class=\"lyric-correction\">\n    <n-tooltip placement=\"right\">\n      <template #trigger>\n        <div\n          class=\"lyric-correction-btn\"\n          @click=\"emit('adjust', -0.2)\"\n          :title=\"t('player.subtractCorrection', { num: 0.2 })\"\n        >\n          <i class=\"ri-subtract-line text-base\"></i>\n        </div>\n      </template>\n      <span>{{ t('player.subtractCorrection', { num: 0.2 }) }}</span>\n    </n-tooltip>\n    <span\n      class=\"text-xs py-0.5 px-1 rounded bg-white/70 dark:bg-neutral-800/70 shadow font-mono tracking-wider text-gray-700 dark:text-gray-200 bg-opacity-40 backdrop-blur-2xl\"\n    >\n      {{ props.correctionTime > 0 ? '+' : '' }}{{ props.correctionTime.toFixed(1) }}s\n    </span>\n    <n-tooltip placement=\"right\">\n      <template #trigger>\n        <div\n          class=\"lyric-correction-btn\"\n          @click=\"emit('adjust', 0.2)\"\n          :title=\"t('player.addCorrection', { num: 0.2 })\"\n        >\n          <i class=\"ri-add-line text-base\"></i>\n        </div>\n      </template>\n      <span>{{ t('player.addCorrection', { num: 0.2 }) }}</span>\n    </n-tooltip>\n  </div>\n</template>\n\n<style scoped lang=\"scss\">\n.lyric-correction {\n  @apply absolute right-0 bottom-4 flex flex-col items-center space-y-1 z-50 select-none transition-opacity duration-200 opacity-0 pointer-events-none;\n}\n\n.lyric-correction-btn {\n  @apply w-7 h-7 flex items-center justify-center rounded-lg bg-white dark:bg-neutral-800 border border-white/20 dark:border-neutral-700/40 shadow-md backdrop-blur-2xl cursor-pointer transition-all duration-150 text-gray-700 dark:text-gray-200 hover:bg-green-500/80 hover:text-white hover:border-green-400/60 active:scale-95 bg-opacity-40 dark:hover:bg-green-500/80 dark:hover:text-white dark:hover:border-green-400/60 dark:hover:bg-opacity-40;\n}\n\n.mobile {\n  .lyric-correction {\n    @apply opacity-100;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/lyric/LyricSettings.vue",
    "content": "<template>\n  <div\n    class=\"w-80 rounded-2xl bg-black/30 backdrop-blur-3xl border border-white/10 shadow-2xl overflow-hidden\"\n  >\n    <!-- 标题栏 -->\n    <div class=\"px-6 py-4 border-b border-white/5\">\n      <h2 class=\"text-lg font-semibold tracking-tight text-white/90\">\n        {{ t('settings.lyricSettings.title') }}\n      </h2>\n    </div>\n\n    <!-- 标签页导航 -->\n    <div class=\"px-4 pt-3 pb-2\">\n      <div class=\"flex gap-1 p-1 bg-black/20 rounded-xl\">\n        <button\n          v-for=\"tab in tabs\"\n          :key=\"tab.key\"\n          @click=\"activeTab = tab.key\"\n          :class=\"[\n            'flex-1 px-4 py-2 text-sm font-medium rounded-lg transition-all duration-200',\n            activeTab === tab.key\n              ? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/30'\n              : 'hover:bg-white/5'\n          ]\"\n          :style=\"activeTab !== tab.key ? 'color: rgba(255, 255, 255, 0.7);' : ''\"\n        >\n          {{ tab.label }}\n        </button>\n      </div>\n    </div>\n\n    <!-- 内容区域 -->\n    <div\n      class=\"px-3 pb-3 max-h-[450px] overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent\"\n    >\n      <!-- 显示设置 -->\n      <div v-show=\"activeTab === 'display'\" class=\"space-y-2 pt-2\">\n        <div class=\"setting-item\">\n          <span>{{ t('settings.lyricSettings.pureMode') }}</span>\n          <input type=\"checkbox\" v-model=\"config.pureModeEnabled\" class=\"toggle-switch\" />\n        </div>\n        <div class=\"setting-item\">\n          <span>{{ t('settings.lyricSettings.hideCover') }}</span>\n          <input type=\"checkbox\" v-model=\"config.hideCover\" class=\"toggle-switch\" />\n        </div>\n        <div class=\"setting-item\">\n          <span>{{ t('settings.lyricSettings.centerDisplay') }}</span>\n          <input type=\"checkbox\" v-model=\"config.centerLyrics\" class=\"toggle-switch\" />\n        </div>\n        <div class=\"setting-item\">\n          <span>{{ t('settings.lyricSettings.showTranslation') }}</span>\n          <input type=\"checkbox\" v-model=\"config.showTranslation\" class=\"toggle-switch\" />\n        </div>\n        <div class=\"setting-item\">\n          <span>{{ t('settings.lyricSettings.hideLyrics') }}</span>\n          <input type=\"checkbox\" v-model=\"config.hideLyrics\" class=\"toggle-switch\" />\n        </div>\n      </div>\n\n      <!-- 界面设置 -->\n      <div v-show=\"activeTab === 'interface'\" class=\"space-y-4 pt-3\">\n        <div class=\"setting-item\">\n          <span>{{ t('settings.lyricSettings.showMiniPlayBar') }}</span>\n          <input type=\"checkbox\" v-model=\"showMiniPlayBar\" class=\"toggle-switch\" />\n        </div>\n\n        <div class=\"slider-group\">\n          <label class=\"slider-label\">{{ t('settings.lyricSettings.contentWidth') }}</label>\n          <input\n            type=\"range\"\n            v-model.number=\"config.contentWidth\"\n            min=\"50\"\n            max=\"100\"\n            step=\"5\"\n            class=\"slider-emerald\"\n          />\n          <div class=\"slider-marks\">\n            <span>50%</span>\n            <span>75%</span>\n            <span>100%</span>\n          </div>\n        </div>\n      </div>\n\n      <!-- 文字设置 -->\n      <div v-show=\"activeTab === 'typography'\" class=\"space-y-4 pt-3\">\n        <div class=\"slider-group\">\n          <label class=\"slider-label\">{{ t('settings.lyricSettings.fontSize') }}</label>\n          <input\n            type=\"range\"\n            v-model.number=\"config.fontSize\"\n            min=\"12\"\n            max=\"32\"\n            step=\"1\"\n            class=\"slider-emerald\"\n          />\n          <div class=\"slider-marks\">\n            <span>{{ t('settings.lyricSettings.fontSizeMarks.small') }}</span>\n            <span>{{ t('settings.lyricSettings.fontSizeMarks.medium') }}</span>\n            <span>{{ t('settings.lyricSettings.fontSizeMarks.large') }}</span>\n          </div>\n        </div>\n\n        <div class=\"slider-group\">\n          <label class=\"slider-label\">{{ t('settings.lyricSettings.letterSpacing') }}</label>\n          <input\n            type=\"range\"\n            v-model.number=\"config.letterSpacing\"\n            min=\"-2\"\n            max=\"10\"\n            step=\"0.2\"\n            class=\"slider-emerald\"\n          />\n          <div class=\"slider-marks\">\n            <span>{{ t('settings.lyricSettings.letterSpacingMarks.compact') }}</span>\n            <span>{{ t('settings.lyricSettings.letterSpacingMarks.default') }}</span>\n            <span>{{ t('settings.lyricSettings.letterSpacingMarks.loose') }}</span>\n          </div>\n        </div>\n\n        <div class=\"slider-group\">\n          <label class=\"slider-label\">{{ t('settings.lyricSettings.fontWeight') }}</label>\n          <input\n            type=\"range\"\n            v-model.number=\"config.fontWeight\"\n            min=\"100\"\n            max=\"900\"\n            step=\"100\"\n            class=\"slider-emerald\"\n          />\n          <div class=\"slider-marks\">\n            <span>{{ t('settings.lyricSettings.fontWeightMarks.thin') }}</span>\n            <span>{{ t('settings.lyricSettings.fontWeightMarks.normal') }}</span>\n            <span>{{ t('settings.lyricSettings.fontWeightMarks.bold') }}</span>\n          </div>\n        </div>\n\n        <div class=\"slider-group\">\n          <label class=\"slider-label\">{{ t('settings.lyricSettings.lineHeight') }}</label>\n          <input\n            type=\"range\"\n            v-model.number=\"config.lineHeight\"\n            min=\"1\"\n            max=\"3\"\n            step=\"0.1\"\n            class=\"slider-emerald\"\n          />\n          <div class=\"slider-marks\">\n            <span>{{ t('settings.lyricSettings.lineHeightMarks.compact') }}</span>\n            <span>{{ t('settings.lyricSettings.lineHeightMarks.default') }}</span>\n            <span>{{ t('settings.lyricSettings.lineHeightMarks.loose') }}</span>\n          </div>\n        </div>\n      </div>\n\n      <!-- 背景设置 -->\n      <div v-show=\"activeTab === 'background'\" class=\"space-y-4 pt-3\">\n        <div class=\"setting-item\">\n          <span>{{ t('settings.lyricSettings.background.useCustomBackground') }}</span>\n          <input type=\"checkbox\" v-model=\"config.useCustomBackground\" class=\"toggle-switch\" />\n        </div>\n\n        <!-- 主题选择 -->\n        <div v-if=\"!config.useCustomBackground\" class=\"radio-group\">\n          <label class=\"radio-label\">{{ t('settings.lyricSettings.backgroundTheme') }}</label>\n          <div class=\"space-y-2\">\n            <label class=\"radio-item\">\n              <input type=\"radio\" v-model=\"config.theme\" value=\"default\" class=\"radio-input\" />\n              <span>{{ t('settings.lyricSettings.themeOptions.default') }}</span>\n            </label>\n            <label class=\"radio-item\">\n              <input type=\"radio\" v-model=\"config.theme\" value=\"light\" class=\"radio-input\" />\n              <span>{{ t('settings.lyricSettings.themeOptions.light') }}</span>\n            </label>\n            <label class=\"radio-item\">\n              <input type=\"radio\" v-model=\"config.theme\" value=\"dark\" class=\"radio-input\" />\n              <span>{{ t('settings.lyricSettings.themeOptions.dark') }}</span>\n            </label>\n          </div>\n        </div>\n\n        <!-- 背景模式选择 -->\n        <div v-if=\"config.useCustomBackground\" class=\"radio-group\">\n          <label class=\"radio-label\">{{\n            t('settings.lyricSettings.background.backgroundMode')\n          }}</label>\n          <div class=\"grid grid-cols-2 gap-2\">\n            <label class=\"radio-item-compact\">\n              <input\n                type=\"radio\"\n                v-model=\"config.backgroundMode\"\n                value=\"solid\"\n                class=\"radio-input\"\n              />\n              <span>{{ t('settings.lyricSettings.background.modeOptions.solid') }}</span>\n            </label>\n            <label class=\"radio-item-compact\">\n              <input\n                type=\"radio\"\n                v-model=\"config.backgroundMode\"\n                value=\"gradient\"\n                class=\"radio-input\"\n              />\n              <span>{{ t('settings.lyricSettings.background.modeOptions.gradient') }}</span>\n            </label>\n            <label class=\"radio-item-compact\">\n              <input\n                type=\"radio\"\n                v-model=\"config.backgroundMode\"\n                value=\"image\"\n                class=\"radio-input\"\n              />\n              <span>{{ t('settings.lyricSettings.background.modeOptions.image') }}</span>\n            </label>\n            <label class=\"radio-item-compact\">\n              <input type=\"radio\" v-model=\"config.backgroundMode\" value=\"css\" class=\"radio-input\" />\n              <span>{{ t('settings.lyricSettings.background.modeOptions.css') }}</span>\n            </label>\n          </div>\n        </div>\n\n        <!-- 纯色模式 -->\n        <div\n          v-if=\"config.useCustomBackground && config.backgroundMode === 'solid'\"\n          class=\"color-picker-group\"\n        >\n          <label class=\"color-picker-label\">{{\n            t('settings.lyricSettings.background.solidColor')\n          }}</label>\n          <input type=\"color\" v-model=\"config.solidColor\" class=\"color-picker\" />\n        </div>\n\n        <!-- 渐变模式 -->\n        <div\n          v-if=\"config.useCustomBackground && config.backgroundMode === 'gradient'\"\n          class=\"space-y-3\"\n        >\n          <label class=\"color-picker-label\">{{\n            t('settings.lyricSettings.background.gradientEditor')\n          }}</label>\n          <div class=\"flex flex-wrap gap-2\">\n            <div v-for=\"(_, index) in config.gradientColors.colors\" :key=\"index\" class=\"relative\">\n              <input\n                type=\"color\"\n                v-model=\"config.gradientColors.colors[index]\"\n                class=\"color-picker-small\"\n              />\n              <button\n                v-if=\"config.gradientColors.colors.length > 2\"\n                @click=\"removeGradientColor(index)\"\n                class=\"absolute -top-1 -right-1 w-5 h-5 flex items-center justify-center rounded-full bg-red-500 text-white text-xs hover:bg-red-600 transition-colors\"\n              >\n                <i class=\"ri-close-line\"></i>\n              </button>\n            </div>\n          </div>\n\n          <button\n            v-if=\"config.gradientColors.colors.length < 5\"\n            @click=\"addGradientColor\"\n            class=\"w-full py-2 px-4 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 transition-colors text-sm font-medium flex items-center justify-center gap-2 text-white/90\"\n          >\n            <i class=\"ri-add-line\"></i>\n            {{ t('settings.lyricSettings.background.addColor') }}\n          </button>\n\n          <div class=\"select-group\">\n            <label class=\"select-label\">{{\n              t('settings.lyricSettings.background.gradientDirection')\n            }}</label>\n            <select v-model=\"config.gradientColors.direction\" class=\"select-input\">\n              <option v-for=\"opt in gradientDirectionOptions\" :key=\"opt.value\" :value=\"opt.value\">\n                {{ opt.label }}\n              </option>\n            </select>\n          </div>\n        </div>\n\n        <!-- 图片模式 -->\n        <div\n          v-if=\"config.useCustomBackground && config.backgroundMode === 'image'\"\n          class=\"space-y-3\"\n        >\n          <label class=\"color-picker-label\">{{\n            t('settings.lyricSettings.background.imageUpload')\n          }}</label>\n          <input\n            type=\"file\"\n            accept=\"image/*\"\n            @change=\"handleImageChange\"\n            class=\"hidden\"\n            ref=\"fileInput\"\n          />\n          <button\n            @click=\"fileInput?.click()\"\n            class=\"w-full py-2 px-4 rounded-lg bg-emerald-500/20 hover:bg-emerald-500/30 transition-colors text-sm font-medium flex items-center justify-center gap-2 text-white/90\"\n          >\n            <i class=\"ri-image-add-line\"></i>\n            {{ t('settings.lyricSettings.background.imageUpload') }}\n          </button>\n\n          <div v-if=\"config.backgroundImage\" class=\"space-y-3\">\n            <div class=\"relative rounded-lg overflow-hidden border border-white/10\">\n              <img\n                :src=\"config.backgroundImage\"\n                class=\"w-full max-h-40 object-cover\"\n                alt=\"Preview\"\n              />\n              <button\n                @click=\"clearBackgroundImage\"\n                class=\"absolute top-2 right-2 p-2 rounded-lg bg-red-500/80 text-white hover:bg-red-500 transition-colors\"\n              >\n                <i class=\"ri-delete-bin-line\"></i>\n              </button>\n            </div>\n\n            <div class=\"slider-group\">\n              <label class=\"slider-label\">{{\n                t('settings.lyricSettings.background.imageBlur')\n              }}</label>\n              <input\n                type=\"range\"\n                v-model.number=\"config.imageBlur\"\n                min=\"0\"\n                max=\"20\"\n                step=\"1\"\n                class=\"slider-emerald\"\n              />\n              <div class=\"slider-marks\">\n                <span>0</span>\n                <span>10</span>\n                <span>20px</span>\n              </div>\n            </div>\n\n            <div class=\"slider-group\">\n              <label class=\"slider-label\">{{\n                t('settings.lyricSettings.background.imageBrightness')\n              }}</label>\n              <input\n                type=\"range\"\n                v-model.number=\"config.imageBrightness\"\n                min=\"0\"\n                max=\"200\"\n                step=\"5\"\n                class=\"slider-emerald\"\n              />\n              <div class=\"slider-marks\">\n                <span>暗</span>\n                <span>正常</span>\n                <span>亮</span>\n              </div>\n            </div>\n          </div>\n\n          <p class=\"text-xs text-white/50\">\n            {{ t('settings.lyricSettings.background.fileSizeLimit') }}\n          </p>\n        </div>\n\n        <!-- CSS 模式 -->\n        <div v-if=\"config.useCustomBackground && config.backgroundMode === 'css'\" class=\"space-y-2\">\n          <label class=\"color-picker-label\">{{\n            t('settings.lyricSettings.background.customCss')\n          }}</label>\n          <textarea\n            v-model=\"config.customCss\"\n            :placeholder=\"t('settings.lyricSettings.background.customCssPlaceholder')\"\n            rows=\"4\"\n            class=\"w-full px-3 py-2 bg-black/20 border border-white/10 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500/50 font-mono text-white/90\"\n          ></textarea>\n          <p class=\"text-xs text-white/50\">\n            {{ t('settings.lyricSettings.background.customCssHelp') }}\n          </p>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { DEFAULT_LYRIC_CONFIG, LyricConfig } from '@/types/lyric';\n\nconst { t } = useI18n();\nconst config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });\nconst emit = defineEmits(['themeChange']);\nconst message = window.$message;\nconst activeTab = ref('display');\nconst fileInput = ref<HTMLInputElement>();\n\nconst tabs = computed(() => [\n  { key: 'display', label: t('settings.lyricSettings.tabs.display') },\n  { key: 'interface', label: t('settings.lyricSettings.tabs.interface') },\n  { key: 'typography', label: t('settings.lyricSettings.tabs.typography') },\n  { key: 'background', label: t('settings.lyricSettings.tabs.background') }\n]);\n\nconst showMiniPlayBar = computed({\n  get: () => !config.value.hideMiniPlayBar,\n  set: (value: boolean) => {\n    config.value.hideMiniPlayBar = !value;\n    config.value.hidePlayBar = value;\n  }\n});\n\nconst gradientDirectionOptions = computed(() => [\n  { label: t('settings.lyricSettings.background.directionOptions.toBottom'), value: 'to bottom' },\n  { label: t('settings.lyricSettings.background.directionOptions.toTop'), value: 'to top' },\n  { label: t('settings.lyricSettings.background.directionOptions.toRight'), value: 'to right' },\n  { label: t('settings.lyricSettings.background.directionOptions.toLeft'), value: 'to left' },\n  {\n    label: t('settings.lyricSettings.background.directionOptions.toBottomRight'),\n    value: 'to bottom right'\n  },\n  { label: t('settings.lyricSettings.background.directionOptions.angle45'), value: '45deg' }\n]);\n\nconst addGradientColor = () => {\n  if (config.value.gradientColors.colors.length < 5) {\n    config.value.gradientColors.colors.push('#666666');\n  }\n};\n\nconst removeGradientColor = (index: number) => {\n  if (config.value.gradientColors.colors.length > 2) {\n    config.value.gradientColors.colors.splice(index, 1);\n  }\n};\n\nconst handleImageChange = (event: Event) => {\n  const target = event.target as HTMLInputElement;\n  const file = target.files?.[0];\n  if (!file) return;\n\n  if (!file.type.startsWith('image/')) {\n    message?.error(t('settings.lyricSettings.background.invalidImageFormat'));\n    return;\n  }\n\n  if (file.size > 20 * 1024 * 1024) {\n    message?.error(t('settings.lyricSettings.background.imageTooLarge'));\n    return;\n  }\n\n  const reader = new FileReader();\n  reader.onload = (e) => {\n    config.value.backgroundImage = e.target?.result as string;\n  };\n  reader.readAsDataURL(file);\n};\n\nconst clearBackgroundImage = () => {\n  config.value.backgroundImage = undefined;\n  if (fileInput.value) {\n    fileInput.value.value = '';\n  }\n};\n\nwatch(\n  () => config.value,\n  (newConfig) => {\n    localStorage.setItem('music-full-config', JSON.stringify(newConfig));\n    updateCSSVariables(newConfig);\n  },\n  { deep: true }\n);\n\nwatch(\n  () => config.value.theme,\n  (newTheme) => {\n    emit('themeChange', newTheme);\n  }\n);\n\nconst updateCSSVariables = (config: LyricConfig) => {\n  document.documentElement.style.setProperty('--lyric-font-size', `${config.fontSize}px`);\n  document.documentElement.style.setProperty('--lyric-letter-spacing', `${config.letterSpacing}px`);\n  document.documentElement.style.setProperty(\n    '--lyric-font-weight',\n    config.fontWeight?.toString() || '400'\n  );\n  document.documentElement.style.setProperty('--lyric-line-height', config.lineHeight.toString());\n};\n\nonMounted(() => {\n  const savedConfig = localStorage.getItem('music-full-config');\n  if (savedConfig) {\n    config.value = { ...config.value, ...JSON.parse(savedConfig) };\n    updateCSSVariables(config.value);\n  }\n});\n\ndefineExpose({\n  config\n});\n</script>\n\n<style scoped>\n/* 设置项 */\n.setting-item {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  padding: 8px 12px;\n  background: rgba(255, 255, 255, 0.05);\n  border: 1px solid rgba(255, 255, 255, 0.08);\n  border-radius: 12px;\n  transition: all 0.2s;\n  font-size: 13px;\n  color: rgba(255, 255, 255, 0.9);\n}\n\n.setting-item:hover {\n  background: rgba(255, 255, 255, 0.06);\n}\n\n/* 切换开关 */\n.toggle-switch {\n  appearance: none;\n  width: 44px;\n  height: 24px;\n  background: rgba(255, 255, 255, 0.1);\n  border-radius: 12px;\n  position: relative;\n  cursor: pointer;\n  transition: all 0.3s;\n}\n\n.toggle-switch::before {\n  content: '';\n  position: absolute;\n  width: 20px;\n  height: 20px;\n  background: white;\n  border-radius: 50%;\n  left: 2px;\n  top: 2px;\n  transition: all 0.3s;\n}\n\n.toggle-switch:checked {\n  background: #10b981;\n}\n\n.toggle-switch:checked::before {\n  left: 22px;\n}\n\n/* 滑块组 */\n.slider-group {\n  padding: 10px 12px;\n  background: rgba(255, 255, 255, 0.05);\n  border: 1px solid rgba(255, 255, 255, 0.08);\n  border-radius: 12px;\n}\n\n.slider-label {\n  display: block;\n  font-size: 11px;\n  font-weight: 600;\n  text-transform: uppercase;\n  letter-spacing: 0.05em;\n  color: rgba(255, 255, 255, 0.8);\n  opacity: 0.8;\n  margin-bottom: 8px;\n}\n\n.slider-emerald {\n  width: 100%;\n  height: 4px;\n  background: rgba(255, 255, 255, 0.1);\n  border-radius: 2px;\n  outline: none;\n  appearance: none;\n}\n\n.slider-emerald::-webkit-slider-thumb {\n  appearance: none;\n  width: 16px;\n  height: 16px;\n  background: #10b981;\n  border-radius: 50%;\n  cursor: pointer;\n  box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);\n}\n\n.slider-emerald::-moz-range-thumb {\n  width: 16px;\n  height: 16px;\n  background: #10b981;\n  border-radius: 50%;\n  cursor: pointer;\n  border: none;\n  box-shadow: 0 2px 8px rgba(16, 185, 129, 0.4);\n}\n\n.slider-marks {\n  display: flex;\n  justify-content: space-between;\n  margin-top: 8px;\n  font-size: 11px;\n  color: rgba(255, 255, 255, 0.8);\n  opacity: 0.5;\n}\n\n/* 单选框组 */\n.radio-group {\n  padding: 16px;\n  background: rgba(255, 255, 255, 0.03);\n  border: 1px solid rgba(255, 255, 255, 0.05);\n  border-radius: 12px;\n}\n\n.radio-label {\n  display: block;\n  font-size: 12px;\n  font-weight: 600;\n  text-transform: uppercase;\n  letter-spacing: 0.05em;\n  color: rgba(255, 255, 255, 0.8);\n  opacity: 0.7;\n  margin-bottom: 12px;\n}\n\n.radio-item {\n  display: flex;\n  align-items: center;\n  padding: 8px 12px;\n  border-radius: 8px;\n  cursor: pointer;\n  transition: all 0.2s;\n  font-size: 14px;\n  color: rgba(255, 255, 255, 0.8);\n}\n\n.radio-item:hover {\n  background: rgba(255, 255, 255, 0.05);\n}\n\n/* 紧凑版单选项（用于横向布局） */\n.radio-item-compact {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  padding: 10px 8px;\n  border-radius: 8px;\n  cursor: pointer;\n  transition: all 0.2s;\n  font-size: 13px;\n  color: rgba(255, 255, 255, 0.8);\n  background: rgba(255, 255, 255, 0.03);\n  border: 1px solid rgba(255, 255, 255, 0.05);\n}\n\n.radio-item-compact:hover {\n  background: rgba(255, 255, 255, 0.08);\n  border-color: rgba(255, 255, 255, 0.1);\n}\n\n.radio-input {\n  appearance: none;\n  width: 18px;\n  height: 18px;\n  border: 2px solid var(--text-color-primary);\n  opacity: 0.4;\n  border-radius: 50%;\n  margin-right: 12px;\n  position: relative;\n  cursor: pointer;\n  flex-shrink: 0;\n}\n\n.radio-input:checked {\n  border-color: #10b981;\n  opacity: 1;\n}\n\n.radio-input:checked::before {\n  content: '';\n  position: absolute;\n  width: 10px;\n  height: 10px;\n  background: #10b981;\n  border-radius: 50%;\n  left: 2px;\n  top: 2px;\n}\n\n/* 颜色选择器 */\n.color-picker-group {\n  padding: 16px;\n  background: rgba(255, 255, 255, 0.03);\n  border: 1px solid rgba(255, 255, 255, 0.05);\n  border-radius: 12px;\n}\n\n.color-picker-label {\n  display: block;\n  font-size: 12px;\n  font-weight: 600;\n  text-transform: uppercase;\n  letter-spacing: 0.05em;\n  color: rgba(255, 255, 255, 0.8);\n  opacity: 0.7;\n  margin-bottom: 12px;\n}\n\n.color-picker {\n  width: 100%;\n  height: 48px;\n  border: none;\n  border-radius: 8px;\n  cursor: pointer;\n  background: transparent;\n}\n\n.color-picker::-webkit-color-swatch-wrapper {\n  padding: 0;\n}\n\n.color-picker::-webkit-color-swatch {\n  border: 1px solid rgba(255, 255, 255, 0.1);\n  border-radius: 8px;\n}\n\n/* 小尺寸颜色选择器（用于渐变） */\n.color-picker-small {\n  width: 56px;\n  height: 56px;\n  border: none;\n  border-radius: 12px;\n  cursor: pointer;\n  background: transparent;\n}\n\n.color-picker-small::-webkit-color-swatch-wrapper {\n  padding: 0;\n}\n\n.color-picker-small::-webkit-color-swatch {\n  border: 2px solid rgba(255, 255, 255, 0.15);\n  border-radius: 12px;\n}\n\n/* 下拉选择 */\n.select-group {\n  padding: 16px;\n  background: rgba(255, 255, 255, 0.03);\n  border: 1px solid rgba(255, 255, 255, 0.05);\n  border-radius: 12px;\n}\n\n.select-label {\n  display: block;\n  font-size: 12px;\n  font-weight: 600;\n  text-transform: uppercase;\n  letter-spacing: 0.05em;\n  color: rgba(255, 255, 255, 0.8);\n  opacity: 0.7;\n  margin-bottom: 12px;\n}\n\n.select-input {\n  width: 100%;\n  padding: 10px 12px;\n  background: rgba(0, 0, 0, 0.2);\n  border: 1px solid rgba(255, 255, 255, 0.1);\n  border-radius: 8px;\n  color: rgba(255, 255, 255, 0.8);\n  font-size: 14px;\n  cursor: pointer;\n  outline: none;\n}\n\n.select-input:focus {\n  border-color: #10b981;\n  box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);\n}\n\n/* 滚动条 */\n.scrollbar-thin::-webkit-scrollbar {\n  width: 6px;\n}\n\n.scrollbar-thin::-webkit-scrollbar-track {\n  background: transparent;\n}\n\n.scrollbar-thin::-webkit-scrollbar-thumb {\n  background: rgba(255, 255, 255, 0.2);\n  border-radius: 3px;\n}\n\n.scrollbar-thin::-webkit-scrollbar-thumb:hover {\n  background: rgba(255, 255, 255, 0.3);\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/lyric/MusicFull.vue",
    "content": "<template>\n  <n-drawer\n    v-model:show=\"isVisible\"\n    height=\"100%\"\n    placement=\"bottom\"\n    :style=\"drawerBaseStyle\"\n    :to=\"`#layout-main`\"\n    :z-index=\"9998\"\n  >\n    <!-- 背景层（用于图片模糊和明暗效果） -->\n    <div\n      v-if=\"\n        config.useCustomBackground && config.backgroundMode === 'image' && config.backgroundImage\n      \"\n      class=\"background-layer\"\n      :style=\"backgroundImageStyle\"\n    ></div>\n    <div id=\"drawer-target\" :class=\"[config.theme]\" class=\"relative z-10\">\n      <!-- 左侧关闭按钮 -->\n      <div\n        class=\"control-left absolute top-8 left-8 z-[9999]\"\n        :class=\"{ 'pure-mode': config.pureModeEnabled }\"\n      >\n        <div class=\"control-btn\" @click=\"closeMusicFull\">\n          <i class=\"ri-arrow-down-s-line\"></i>\n        </div>\n      </div>\n\n      <!-- 右侧功能按钮组 -->\n      <div\n        class=\"control-right absolute top-8 right-8 z-[9999]\"\n        :class=\"{ 'pure-mode': config.pureModeEnabled }\"\n      >\n        <n-popover trigger=\"click\" placement=\"bottom\" raw>\n          <template #trigger>\n            <div class=\"control-btn\">\n              <i class=\"ri-settings-3-line\"></i>\n            </div>\n          </template>\n          <lyric-settings ref=\"lyricSettingsRef\" />\n        </n-popover>\n\n        <div class=\"control-btn\" @click=\"toggleFullScreen\">\n          <i :class=\"isFullScreen ? 'ri-fullscreen-exit-line' : 'ri-fullscreen-line'\"></i>\n        </div>\n      </div>\n\n      <div class=\"content-wrapper\" :style=\"{ width: `${config.contentWidth}%` }\">\n        <!-- 左侧：封面区域 -->\n        <div\n          v-if=\"!config.hideCover\"\n          class=\"left-side\"\n          :class=\"{ 'only-cover': config.hideLyrics }\"\n        >\n          <div class=\"img-container\">\n            <cover3-d\n              ref=\"PicImgRef\"\n              :src=\"getImgUrl(playMusic?.picUrl, '500y500')\"\n              :loading=\"playMusic?.playLoading\"\n              :max-tilt=\"12\"\n              :scale=\"1.03\"\n              :shine-intensity=\"0.25\"\n            />\n          </div>\n          <div class=\"music-info\">\n            <div class=\"music-content-name\" v-html=\"playMusic.name\"></div>\n            <div class=\"music-content-singer\">\n              <n-ellipsis\n                class=\"text-ellipsis\"\n                line-clamp=\"2\"\n                :tooltip=\"{\n                  contentStyle: { maxWidth: '600px' },\n                  zIndex: 99999\n                }\"\n              >\n                <span\n                  v-for=\"(item, index) in artistList\"\n                  :key=\"index\"\n                  class=\"cursor-pointer hover:text-green-500\"\n                  @click=\"handleArtistClick(item.id)\"\n                >\n                  {{ item.name }}\n                  {{ index < artistList.length - 1 ? ' / ' : '' }}\n                </span>\n              </n-ellipsis>\n            </div>\n            <simple-play-bar\n              v-if=\"!config.hideMiniPlayBar\"\n              class=\"mt-4\"\n              :pure-mode-enabled=\"config.pureModeEnabled\"\n              :isDark=\"textColors.theme === 'dark'\"\n            />\n          </div>\n        </div>\n\n        <!-- 右侧：歌词区域 -->\n        <div\n          class=\"right-side\"\n          :class=\"{\n            center: config.centerLyrics,\n            hide: config.hideLyrics,\n            'full-width': config.hideCover\n          }\"\n        >\n          <n-layout\n            ref=\"lrcSider\"\n            class=\"music-lrc\"\n            :native-scrollbar=\"false\"\n            @mouseover=\"mouseOverLayout\"\n            @mouseleave=\"mouseLeaveLayout\"\n          >\n            <!-- 歌曲信息 -->\n            <div class=\"music-lrc-container\">\n              <div\n                v-if=\"config.hideCover\"\n                class=\"music-info-header\"\n                :style=\"{ textAlign: config.centerLyrics ? 'center' : 'left' }\"\n              >\n                <div class=\"music-info-name\" v-html=\"playMusic.name\"></div>\n                <div class=\"music-info-singer\">\n                  <span\n                    v-for=\"(item, index) in artistList\"\n                    :key=\"index\"\n                    class=\"cursor-pointer hover:text-green-500\"\n                    @click=\"handleArtistClick(item.id)\"\n                  >\n                    {{ item.name }}\n                    {{ index < artistList.length - 1 ? ' / ' : '' }}\n                  </span>\n                </div>\n              </div>\n              <!-- 无时间戳歌词提示 -->\n              <div v-if=\"!supportAutoScroll\" class=\"music-lrc-text no-scroll-tip\">\n                <span>{{ t('player.lrc.noAutoScroll') }}</span>\n              </div>\n              <div\n                v-for=\"(item, index) in lrcArray\"\n                :id=\"`music-lrc-text-${index}`\"\n                :key=\"index\"\n                class=\"music-lrc-text\"\n                :class=\"{\n                  'now-text': index === nowIndex,\n                  'hover-text': item.text && item.startTime !== -1\n                }\"\n                @click=\"item.startTime !== -1 ? setAudioTime(index) : null\"\n              >\n                <!-- 逐字歌词显示 -->\n                <div\n                  v-if=\"item.hasWordByWord && item.words && item.words.length > 0\"\n                  class=\"word-by-word-lyric\"\n                >\n                  <template v-for=\"(word, wordIndex) in item.words\" :key=\"wordIndex\">\n                    <span class=\"lyric-word\" :style=\"getWordStyle(index, wordIndex, word)\">\n                      {{ word.text }} </span\n                    ><span class=\"lyric-word\" v-if=\"word.space\">&nbsp;</span></template\n                  >\n                </div>\n                <!-- 普通歌词显示 -->\n                <span v-else :style=\"getLrcStyle(index)\">{{ item.text }}</span>\n                <div v-show=\"config.showTranslation\" class=\"music-lrc-text-tr\">\n                  {{ item.trText }}\n                </div>\n              </div>\n\n              <!-- 无歌词 -->\n              <div v-if=\"!lrcArray.length\" class=\"music-lrc-text\">\n                <span>{{ t('player.lrc.noLrc') }}</span>\n              </div>\n            </div>\n            <!-- 歌词右下角矫正按钮组件 -->\n            <lyric-correction-control\n              v-if=\"!isMobile\"\n              :correction-time=\"correctionTime\"\n              @adjust=\"adjustCorrectionTime\"\n            />\n          </n-layout>\n        </div>\n      </div>\n    </div>\n  </n-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport { useDebounceFn } from '@vueuse/core';\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport Cover3D from '@/components/cover/Cover3D.vue';\nimport LyricCorrectionControl from '@/components/lyric/LyricCorrectionControl.vue';\nimport LyricSettings from '@/components/lyric/LyricSettings.vue';\nimport SimplePlayBar from '@/components/player/SimplePlayBar.vue';\nimport {\n  adjustCorrectionTime,\n  artistList,\n  correctionTime,\n  lrcArray,\n  nowIndex,\n  nowTime,\n  playMusic,\n  setAudioTime,\n  textColors,\n  useLyricProgress\n} from '@/hooks/MusicHook';\nimport { useArtist } from '@/hooks/useArtist';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { useSettingsStore } from '@/store/modules/settings';\nimport { DEFAULT_LYRIC_CONFIG, LyricConfig } from '@/types/lyric';\nimport { getImgUrl, isMobile } from '@/utils';\nimport { animateGradient, getHoverBackgroundColor, getTextColors } from '@/utils/linearColor';\n\nconst { t } = useI18n();\n// 定义 refs\nconst lrcSider = ref<any>(null);\nconst isMouse = ref(false);\nconst currentBackground = ref('');\nconst animationFrame = ref<number | null>(null);\nconst isDark = ref(false);\n\n// 计算自定义背景样式\nconst customBackgroundStyle = computed(() => {\n  if (!config.value.useCustomBackground) {\n    return null;\n  }\n\n  switch (config.value.backgroundMode) {\n    case 'solid':\n      return config.value.solidColor;\n    case 'gradient': {\n      const { colors, direction } = config.value.gradientColors;\n      return `linear-gradient(${direction}, ${colors.join(', ')})`;\n    }\n    case 'image':\n      if (!config.value.backgroundImage) return null;\n      // 构建完整的背景样式，包括滤镜效果\n      return config.value.backgroundImage;\n    case 'css':\n      return config.value.customCss || null;\n    default:\n      return null;\n  }\n});\n\n// drawer 基础样式（非图片模式）\nconst drawerBaseStyle = computed(() => {\n  // 图片模式时不设置背景，使用单独的背景层\n  if (config.value.useCustomBackground && config.value.backgroundMode === 'image') {\n    return { background: 'transparent' };\n  }\n  // 其他模式正常设置背景\n  if (config.value.useCustomBackground && customBackgroundStyle.value) {\n    return { background: customBackgroundStyle.value };\n  }\n  return { background: currentBackground.value || props.background };\n});\n\n// 背景图片层样式（只在图片模式下使用）\nconst backgroundImageStyle = computed(() => {\n  const blur = config.value.imageBlur || 0;\n  const brightness = config.value.imageBrightness || 100;\n  return {\n    backgroundImage: `url(${config.value.backgroundImage})`,\n    filter: `blur(${blur}px) brightness(${brightness}%)`\n  };\n});\nconst showStickyHeader = ref(false);\nconst lyricSettingsRef = ref<InstanceType<typeof LyricSettings>>();\nconst isSongChanging = ref(false);\nconst isFullScreen = ref(false);\n\nconst config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });\n\nwatch(\n  () => lyricSettingsRef.value?.config,\n  (newConfig) => {\n    if (newConfig) {\n      config.value = newConfig;\n    }\n  },\n  { deep: true, immediate: true }\n);\n\n// 监听本地配置变化，保存到 localStorage\nwatch(\n  () => config.value,\n  (newConfig) => {\n    localStorage.setItem('music-full-config', JSON.stringify(newConfig));\n    if (lyricSettingsRef.value) {\n      lyricSettingsRef.value.config = newConfig;\n    }\n  },\n  { deep: true }\n);\n\nconst supportAutoScroll = computed(() => {\n  return lrcArray.value.length > 0 && lrcArray.value[0].startTime !== -1;\n});\n\nconst props = defineProps({\n  modelValue: {\n    type: Boolean,\n    default: false\n  },\n  background: {\n    type: String,\n    default: ''\n  }\n});\n\nconst themeMusic = {\n  light: 'linear-gradient(to bottom, #ffffff, #f5f5f5)',\n  dark: 'linear-gradient(to bottom, #1a1a1a, #000000)'\n};\n\nconst emit = defineEmits(['update:modelValue']);\n\nconst isVisible = computed({\n  get: () => props.modelValue,\n  set: (value) => emit('update:modelValue', value)\n});\n\n// 歌词滚动方法\nconst lrcScroll = (behavior: ScrollBehavior = 'smooth', forceTop: boolean = false) => {\n  if (!isVisible.value || !lrcSider.value || !supportAutoScroll.value) return;\n\n  if (forceTop) {\n    lrcSider.value.scrollTo({\n      top: 0,\n      behavior\n    });\n    return;\n  }\n\n  if (isMouse.value) return;\n\n  const nowEl = document.querySelector(`#music-lrc-text-${nowIndex.value}`) as HTMLElement;\n  if (nowEl) {\n    const containerHeight = lrcSider.value.$el.clientHeight;\n    const elementTop = nowEl.offsetTop;\n    const scrollTop = elementTop - containerHeight / 2 + nowEl.clientHeight / 2;\n\n    lrcSider.value.scrollTo({\n      top: scrollTop,\n      behavior\n    });\n  }\n};\n\nconst debouncedLrcScroll = useDebounceFn(lrcScroll, 200);\n\nconst mouseOverLayout = () => {\n  if (isMobile.value) {\n    return;\n  }\n  isMouse.value = true;\n};\n\nconst mouseLeaveLayout = () => {\n  if (isMobile.value) {\n    return;\n  }\n  setTimeout(() => {\n    isMouse.value = false;\n    lrcScroll();\n  }, 2000);\n};\n\nwatch(nowIndex, () => {\n  // 歌曲切换时不自动滚动\n  if (isSongChanging.value) return;\n  debouncedLrcScroll();\n});\n\nwatch(\n  () => isVisible.value,\n  () => {\n    if (isVisible.value) {\n      nextTick(() => {\n        lrcScroll('instant');\n      });\n    }\n  }\n);\n\nconst setTextColors = (background: string) => {\n  if (!background) {\n    textColors.value = getTextColors();\n    document.documentElement.style.setProperty('--hover-bg-color', getHoverBackgroundColor(false));\n    document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);\n    document.documentElement.style.setProperty('--text-color-active', textColors.value.active);\n    return;\n  }\n\n  // 更新文字颜色\n  textColors.value = getTextColors(background);\n  isDark.value = textColors.value.active === '#000000';\n\n  document.documentElement.style.setProperty(\n    '--hover-bg-color',\n    getHoverBackgroundColor(isDark.value)\n  );\n  document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);\n  document.documentElement.style.setProperty('--text-color-active', textColors.value.active);\n\n  // 处理背景颜色动画\n  if (currentBackground.value) {\n    if (animationFrame.value) {\n      cancelAnimationFrame(animationFrame.value);\n    }\n    const result = animateGradient(currentBackground.value, background, (gradient) => {\n      currentBackground.value = gradient;\n    });\n    if (typeof result === 'number') {\n      animationFrame.value = result;\n    }\n  } else {\n    currentBackground.value = background;\n  }\n};\n\nconst targetBackground = computed(() => {\n  if (config.value.useCustomBackground && customBackgroundStyle.value) {\n    if (typeof customBackgroundStyle.value === 'string') {\n      return customBackgroundStyle.value;\n    }\n  }\n  if (config.value.theme !== 'default') {\n    return themeMusic[config.value.theme] || props.background;\n  }\n  return props.background;\n});\n\n// 监听目标背景变化并更新文字颜色\nwatch(\n  targetBackground,\n  (newBg) => {\n    if (newBg) {\n      setTextColors(newBg);\n    }\n  },\n  { immediate: true }\n);\n\nconst { getLrcStyle: originalLrcStyle } = useLyricProgress();\n\nconst getLrcStyle = (index: number) => {\n  const colors = textColors.value || getTextColors();\n  const originalStyle = originalLrcStyle(index);\n\n  if (index === nowIndex.value) {\n    // 当前播放的歌词\n    if (originalStyle.backgroundImage) {\n      // 有渐变进度时，使用渐变效果\n      return {\n        ...originalStyle,\n        backgroundImage: originalStyle.backgroundImage\n          .replace(/#ffffff/g, colors.active)\n          .replace(/#ffffff8a/g, `${colors.primary}`),\n        backgroundClip: 'text',\n        WebkitBackgroundClip: 'text',\n        color: 'transparent'\n      };\n    } else {\n      return {\n        color: colors.primary\n      };\n    }\n  }\n\n  // 非当前播放的歌词，使用普通颜色\n  return {\n    color: colors.primary\n  };\n};\n\n// 逐字歌词样式函数\nconst getWordStyle = (lineIndex: number, _wordIndex: number, word: any) => {\n  const colors = textColors.value || getTextColors();\n  // 如果不是当前行，返回普通样式\n  if (lineIndex !== nowIndex.value) {\n    return {\n      color: colors.primary,\n      transition: 'color 0.3s ease',\n      // 重置背景相关属性\n      backgroundImage: 'none',\n      WebkitTextFillColor: 'initial'\n    };\n  }\n\n  // 当前行的逐字效果，应用歌词矫正时间\n  const currentTime = (nowTime.value + correctionTime.value) * 1000; // 转换为毫秒，确保与word时间单位一致\n\n  // 直接使用绝对时间比较\n  const wordStartTime = word.startTime; // 单词开始的绝对时间（毫秒）\n  const wordEndTime = word.startTime + word.duration;\n\n  if (currentTime >= wordStartTime && currentTime < wordEndTime) {\n    // 当前正在播放的单词 - 使用渐变进度效果\n    const progress = Math.min((currentTime - wordStartTime) / word.duration, 1);\n    const progressPercent = Math.round(progress * 100);\n\n    return {\n      backgroundImage: `linear-gradient(to right, ${colors.active} 0%, ${colors.active} ${progressPercent}%, ${colors.primary} ${progressPercent}%, ${colors.primary} 100%)`,\n      backgroundClip: 'text',\n      WebkitBackgroundClip: 'text',\n      WebkitTextFillColor: 'transparent',\n      textShadow: `0 0 8px ${colors.active}40`,\n      transition: 'all 0.1s ease'\n    };\n  } else if (currentTime >= wordEndTime) {\n    // 已经播放过的单词 - 纯色显示\n    return {\n      color: colors.active,\n      WebkitTextFillColor: 'initial',\n      transition: 'none'\n    };\n  } else {\n    // 还未播放的单词 - 普通状态\n    return {\n      color: colors.primary,\n      WebkitTextFillColor: 'initial',\n      transition: 'none'\n    };\n  }\n};\n\n// 组件卸载时清理动画\nonBeforeUnmount(() => {\n  if (animationFrame.value) {\n    cancelAnimationFrame(animationFrame.value);\n  }\n});\n\nconst settingsStore = useSettingsStore();\n\nconst { navigateToArtist } = useArtist();\n\nconst handleArtistClick = (id: number) => {\n  isVisible.value = false;\n  navigateToArtist(id);\n};\n\nconst setData = computed(() => settingsStore.setData);\n\n// 监听字体变化并更新 CSS 变量\nwatch(\n  () => [setData.value.fontFamily, setData.value.fontScope],\n  ([newFont, fontScope]) => {\n    const defaultFonts =\n      'system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif';\n\n    // 如果不是歌词模式或全局模式，使用默认字体\n    if (fontScope !== 'lyric' && fontScope !== 'global') {\n      document.documentElement.style.setProperty('--current-font-family', defaultFonts);\n      return;\n    }\n\n    if (newFont === 'system-ui') {\n      document.documentElement.style.setProperty('--current-font-family', defaultFonts);\n    } else {\n      // 处理多个字体，确保每个字体名都被正确引用\n      const fontList = newFont.split(',').map((font) => {\n        const trimmedFont = font.trim();\n        // 如果字体名包含空格或特殊字符，添加引号（如果还没有引号的话）\n        return /[\\s'\"()]/.test(trimmedFont) && !/^['\"].*['\"]$/.test(trimmedFont)\n          ? `\"${trimmedFont}\"`\n          : trimmedFont;\n      });\n\n      // 将选择的字体和默认字体组合\n      document.documentElement.style.setProperty(\n        '--current-font-family',\n        `${fontList.join(', ')}, ${defaultFonts}`\n      );\n    }\n  },\n  { immediate: true }\n);\n\n// 监听滚动事件\nconst handleScroll = () => {\n  if (!lrcSider.value || !config.value.hideCover) return;\n  const { scrollTop } = lrcSider.value.$el;\n  showStickyHeader.value = scrollTop > 100;\n};\n\nconst playerStore = usePlayerStore();\n\nconst closeMusicFull = () => {\n  // 退出全屏模式\n  if (isFullScreen.value && document.fullscreenElement) {\n    document.exitFullscreen();\n  }\n  isVisible.value = false;\n  playerStore.setMusicFull(false);\n};\n\n// 全屏切换方法\nconst toggleFullScreen = async () => {\n  try {\n    if (!document.fullscreenElement) {\n      // 进入全屏\n      await document.documentElement.requestFullscreen();\n      isFullScreen.value = true;\n    } else {\n      // 退出全屏\n      await document.exitFullscreen();\n      isFullScreen.value = false;\n    }\n  } catch (error) {\n    console.error('全屏切换失败:', error);\n  }\n};\n\n// 监听全屏状态变化\nconst handleFullScreenChange = () => {\n  isFullScreen.value = !!document.fullscreenElement;\n};\n\n// 添加滚动监听和全屏状态监听\nonMounted(() => {\n  if (lrcSider.value?.$el) {\n    lrcSider.value.$el.addEventListener('scroll', handleScroll);\n  }\n  document.addEventListener('fullscreenchange', handleFullScreenChange);\n});\n\n// 移除滚动监听和全屏状态监听\nonBeforeUnmount(() => {\n  if (animationFrame.value) {\n    cancelAnimationFrame(animationFrame.value);\n  }\n  if (lrcSider.value?.$el) {\n    lrcSider.value.$el.removeEventListener('scroll', handleScroll);\n  }\n  document.removeEventListener('fullscreenchange', handleFullScreenChange);\n  // 退出全屏模式\n  if (document.fullscreenElement) {\n    document.exitFullscreen();\n  }\n});\n\n// 监听字体大小变化\nwatch(\n  () => config.value.fontSize,\n  (newSize) => {\n    document.documentElement.style.setProperty('--lyric-font-size', `${newSize}px`);\n  }\n);\n\n// 监听字体粗细变化\nwatch(\n  () => config.value.fontWeight,\n  (newWeight) => {\n    document.documentElement.style.setProperty('--lyric-font-weight', newWeight.toString());\n  }\n);\n\n// 添加文字间距监听\nwatch(\n  () => config.value.letterSpacing,\n  (newSpacing) => {\n    document.documentElement.style.setProperty('--lyric-letter-spacing', `${newSpacing}px`);\n  }\n);\n\n// 添加行高监听\nwatch(\n  () => config.value.lineHeight,\n  (newLineHeight) => {\n    document.documentElement.style.setProperty('--lyric-line-height', newLineHeight.toString());\n  }\n);\n\n// 加载保存的配置\nonMounted(() => {\n  const savedConfig = localStorage.getItem('music-full-config');\n  if (savedConfig) {\n    config.value = { ...config.value, ...JSON.parse(savedConfig) };\n  }\n  if (lrcSider.value?.$el) {\n    lrcSider.value.$el.addEventListener('scroll', handleScroll);\n  }\n});\n\n// 添加对 playMusic.id 的监听，歌曲切换时滚动到顶部\nwatch(\n  () => playMusic.value.id,\n  (newId, oldId) => {\n    // 只在歌曲真正切换时滚动到顶部\n    if (newId !== oldId && newId) {\n      isSongChanging.value = true;\n      // 延迟滚动，确保 nowIndex 已重置\n      setTimeout(() => {\n        lrcScroll('instant', true);\n        // 延迟恢复自动滚动，等待歌词数据更新\n        setTimeout(() => {\n          isSongChanging.value = false;\n        }, 300);\n      }, 100);\n    }\n  }\n);\n\ndefineExpose({\n  lrcScroll,\n  config\n});\n</script>\n\n<style scoped lang=\"scss\">\n@keyframes round {\n  0% {\n    transform: rotate(0deg);\n  }\n\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n.background-layer {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-size: cover;\n  background-position: center;\n  background-repeat: no-repeat;\n  z-index: 0;\n}\n\n.drawer-back {\n  @apply absolute bg-cover bg-center;\n  z-index: -1;\n  width: 200%;\n  height: 200%;\n  top: -50%;\n  left: -50%;\n}\n\n.drawer-back.paused {\n  animation-play-state: paused;\n}\n\n#drawer-target {\n  @apply top-0 left-0 absolute overflow-hidden rounded w-full h-full;\n  animation-duration: 300ms;\n\n  .content-wrapper {\n    @apply grid items-center mx-auto h-full;\n    grid-template-columns: minmax(300px, 40%) 1fr;\n    gap: 4rem;\n    max-width: 1600px;\n    padding: 2rem;\n    transition: width 0.3s ease;\n\n    @media (max-width: 1024px) {\n      grid-template-columns: 1fr;\n      grid-template-rows: auto 1fr;\n      gap: 2rem;\n    }\n  }\n\n  .left-side {\n    @apply flex flex-col items-center justify-center h-full;\n    transition: all 0.3s ease;\n\n    &.only-cover {\n      @apply col-span-2;\n\n      .img-container {\n        @apply w-[60vh] aspect-square;\n      }\n\n      .music-info {\n        @apply max-w-[800px];\n      }\n    }\n\n    .img-container {\n      @apply relative w-[45vh] mb-8 aspect-square;\n      max-width: 100%;\n    }\n\n    .music-info {\n      @apply w-full text-center max-w-[400px];\n\n      .music-content-name {\n        @apply text-3xl font-bold mb-2 line-clamp-2;\n        color: var(--text-color-active);\n      }\n\n      .music-content-singer {\n        @apply text-lg opacity-80;\n        color: var(--text-color-primary);\n      }\n    }\n  }\n\n  .right-side {\n    @apply flex flex-col justify-center h-full relative overflow-hidden;\n\n    &.full-width {\n      @apply col-span-2;\n    }\n\n    &.center {\n      .music-lrc {\n        @apply w-full mx-auto text-center;\n      }\n\n      .music-lrc-text {\n        @apply text-center;\n        transform-origin: center center;\n      }\n\n      .word-by-word-lyric {\n        @apply justify-center;\n      }\n    }\n\n    &.hide {\n      @apply hidden;\n    }\n\n    .music-lrc {\n      @apply w-full h-full bg-transparent;\n      mask-image: linear-gradient(\n        to bottom,\n        transparent 0%,\n        black 15%,\n        black 85%,\n        transparent 100%\n      );\n      -webkit-mask-image: linear-gradient(\n        to bottom,\n        transparent 0%,\n        black 15%,\n        black 85%,\n        transparent 100%\n      );\n\n      .music-info-header {\n        @apply mb-8;\n\n        .music-info-name {\n          @apply text-4xl font-bold mb-2 line-clamp-2;\n          color: var(--text-color-active);\n        }\n\n        .music-info-singer {\n          @apply text-xl opacity-80;\n          color: var(--text-color-primary);\n        }\n      }\n    }\n\n    .music-lrc-container {\n      padding: 50vh 0;\n      min-height: 100%;\n    }\n\n    .music-lrc-text {\n      @apply text-2xl cursor-pointer font-bold px-4 py-3;\n      font-family: var(--current-font-family);\n      font-weight: var(--lyric-font-weight, bold) !important;\n      transition: all 0.3s ease;\n      background-color: transparent;\n      font-size: var(--lyric-font-size, 22px) !important;\n      letter-spacing: var(--lyric-letter-spacing, 0) !important;\n      line-height: var(--lyric-line-height, 2) !important;\n      opacity: 0.6;\n      transform-origin: left center;\n\n      &.now-text {\n        opacity: 1;\n        transform: scale(1.05);\n      }\n\n      &.no-scroll-tip {\n        @apply text-base opacity-60 cursor-default py-2;\n        color: var(--text-color-primary);\n        font-weight: normal;\n\n        span {\n          padding-right: 0;\n        }\n\n        &:hover {\n          background-color: transparent;\n        }\n      }\n\n      span {\n        background-clip: text !important;\n        -webkit-background-clip: text !important;\n        padding-right: 30px;\n      }\n\n      &-tr {\n        @apply font-normal;\n        opacity: 0.7;\n        color: var(--text-color-primary);\n      }\n\n      // 逐字歌词样式\n      .word-by-word-lyric {\n        @apply flex flex-wrap;\n\n        .lyric-word {\n          @apply inline-block;\n          padding-right: 0;\n          font-weight: inherit;\n          font-size: inherit;\n          letter-spacing: inherit;\n          line-height: inherit;\n          cursor: inherit;\n          position: relative;\n\n          &:hover {\n            background-color: rgba(255, 255, 255, 0.1);\n          }\n        }\n      }\n    }\n\n    .hover-text {\n      &:hover {\n        @apply font-bold opacity-100 rounded-xl;\n        background-color: var(--hover-bg-color);\n\n        span {\n          color: var(--text-color-active) !important;\n        }\n      }\n    }\n  }\n}\n\n.mobile {\n  #drawer-target {\n    @apply p-4 pt-8;\n\n    .content-wrapper {\n      @apply flex-col justify-start p-0;\n    }\n\n    .music-img {\n      display: none;\n    }\n\n    .music-lrc {\n      height: calc(100vh - 260px) !important;\n      width: 100vw;\n\n      span {\n        padding-right: 0px !important;\n      }\n\n      .hover-text {\n        &:hover {\n          background-color: transparent;\n        }\n      }\n\n      .music-lrc-text {\n        @apply text-xl text-center;\n      }\n    }\n\n    .music-content {\n      @apply h-[calc(100vh-120px)];\n      width: 100vw !important;\n    }\n  }\n}\n\n.music-drawer {\n  transition: none; // 移除之前的过渡效果，现在使用 JS 动画\n}\n\n// 添加全局字体样式\n// 字体设置已移至上方或不再需要单独的 drawer-target 块\n:root {\n  --current-font-family:\n    system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,\n    sans-serif;\n}\n\n.close-btn {\n  opacity: 0.3;\n  transition: opacity 0.3s ease;\n\n  &:hover {\n    opacity: 1;\n  }\n}\n\n.control-left,\n.control-right {\n  &.pure-mode {\n    @apply pointer-events-auto;\n\n    .control-btn {\n      @apply opacity-0 transition-all duration-300;\n      pointer-events: none;\n    }\n\n    &:hover .control-btn {\n      @apply opacity-100;\n      pointer-events: auto;\n    }\n  }\n\n  &:not(.pure-mode) .control-btn {\n    pointer-events: auto;\n  }\n}\n\n.control-right {\n  @apply flex items-center gap-2;\n}\n\n.control-btn {\n  @apply w-9 h-9 flex items-center justify-center rounded cursor-pointer transition-all duration-300;\n  background: rgba(142, 142, 142, 0.192);\n  backdrop-filter: blur(12px);\n\n  i {\n    @apply text-xl;\n    color: var(--text-color-active);\n  }\n\n  &:hover {\n    background: rgba(126, 121, 121, 0.2);\n\n    i {\n      opacity: 1;\n    }\n  }\n}\n\n.lyric-correction {\n  .music-lrc:hover & {\n    opacity: 1 !important;\n    pointer-events: auto !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/lyric/MusicFullMobile.vue",
    "content": "<template>\n  <n-drawer\n    v-model:show=\"isVisible\"\n    height=\"100%\"\n    placement=\"bottom\"\n    :style=\"{ background: playerStore.playMusic.primaryColor || background }\"\n    :to=\"`#layout-main`\"\n    :z-index=\"9998\"\n  >\n    <div\n      id=\"mobile-drawer-target\"\n      :class=\"[\n        config.theme,\n        `cover-style-${config.mobileCoverStyle}`,\n        { 'is-landscape': isLandscape },\n        { 'is-dark': isDark }\n      ]\"\n    >\n      <!-- 顶部控制按钮 -->\n      <div v-if=\"playMusic?.playLoading\" class=\"loading-overlay\">\n        <i class=\"ri-loader-4-line loading-icon\"></i>\n      </div>\n      <div\n        class=\"control-btn absolute left-5\"\n        :class=\"{ 'pure-mode': config.pureModeEnabled }\"\n        @click=\"closeMusicFull\"\n      >\n        <i class=\"ri-arrow-down-s-line\"></i>\n      </div>\n\n      <!-- 右上角设置按钮 -->\n      <div\n        class=\"control-btn absolute right-5 flex items-center gap-2\"\n        :class=\"[\n          { 'pure-mode': config.pureModeEnabled },\n          hasSleepTimerActive ? '!w-auto !px-2' : ''\n        ]\"\n      >\n        <!-- 定时器倒计时显示 -->\n        <div\n          v-if=\"hasSleepTimerActive\"\n          class=\"flex items-center gap-1 px-2 py-1 rounded-full bg-black/30 backdrop-blur-sm text-xs text-white/90\"\n          @click=\"showPlayerSettings = true\"\n        >\n          <i class=\"ri-timer-line text-green-400\"></i>\n          <span class=\"font-medium tabular-nums\">{{ sleepTimerDisplayText }}</span>\n        </div>\n        <div @click=\"showPlayerSettings = true\">\n          <i class=\"ri-more-2-fill\"></i>\n        </div>\n      </div>\n\n      <!-- 播放设置弹窗 -->\n      <mobile-player-settings v-model:visible=\"showPlayerSettings\" />\n\n      <!-- 全屏歌词页面 - 竖屏模式下 -->\n      <transition name=\"fade\">\n        <div v-if=\"showFullLyrics && !isLandscape\" class=\"fullscreen-lyrics\" :class=\"config.theme\">\n          <div class=\"fullscreen-header\">\n            <div class=\"song-title\" v-html=\"playMusic.name\"></div>\n            <div class=\"artist-name\">\n              <span v-for=\"(item, index) in artistList\" :key=\"index\">\n                {{ item.name }}{{ index < artistList.length - 1 ? ' / ' : '' }}\n              </span>\n            </div>\n          </div>\n\n          <div\n            ref=\"lyricsScrollerRef\"\n            class=\"lyrics-scroller\"\n            @touchstart=\"handleTouchStart\"\n            @touchmove=\"handleTouchMove\"\n            @touchend=\"handleTouchEnd\"\n            @scroll=\"handleScroll\"\n          >\n            <div class=\"lyrics-padding-top\"></div>\n            <!-- 无时间戳歌词提示 -->\n            <div v-if=\"!supportAutoScroll\" class=\"lyric-line no-scroll-tip\">\n              <span>{{ t('player.lrc.noAutoScroll') }}</span>\n            </div>\n            <div\n              v-for=\"(item, index) in lrcArray\"\n              :key=\"index\"\n              :id=\"`lyric-line-${index}`\"\n              class=\"lyric-line\"\n              :class=\"{\n                'now-text': index === nowIndex,\n                'hover-text': item.text && item.startTime !== -1\n              }\"\n              @click=\"item.startTime !== -1 ? setAudioTime(index) : null\"\n            >\n              <!-- 逐字歌词显示 -->\n              <div\n                v-if=\"item.hasWordByWord && item.words && item.words.length > 0\"\n                class=\"word-by-word-lyric\"\n              >\n                <template v-for=\"(word, wordIndex) in item.words\" :key=\"wordIndex\">\n                  <span class=\"lyric-word\" :style=\"getWordStyle(index, wordIndex, word)\">\n                    {{ word.text }} </span\n                  ><span class=\"lyric-word\" v-if=\"word.space\">&nbsp;</span></template\n                >\n              </div>\n              <!-- 普通歌词显示 -->\n              <span v-else :style=\"getLrcStyle(index)\">{{ item.text }}</span>\n              <div v-if=\"config.showTranslation && item.trText\" class=\"translation\">\n                {{ item.trText }}\n              </div>\n            </div>\n            <div class=\"lyrics-padding-bottom\"></div>\n          </div>\n        </div>\n      </transition>\n\n      <!-- 主要内容区域 - 竖屏模式下的普通布局 -->\n      <transition name=\"fade\">\n        <div v-if=\"!showFullLyrics && !isLandscape\" class=\"ios-layout-container\">\n          <!-- 封面区域 -->\n          <div\n            class=\"cover-container\"\n            :class=\"{\n              'record-style': config.mobileCoverStyle === 'record',\n              'square-style': config.mobileCoverStyle === 'square',\n              'full-style': config.mobileCoverStyle === 'full',\n              paused: !play\n            }\"\n            @click=\"cycleCoverStyle\"\n          >\n            <div class=\"img-wrapper\">\n              <n-image\n                ref=\"PicImgRef\"\n                :src=\"getImgUrl(playMusic?.picUrl, '500y500')\"\n                lazy\n                preview-disabled\n                class=\"cover-image\"\n                :class=\"{ 'full-blend': config.mobileCoverStyle === 'full' }\"\n              />\n            </div>\n          </div>\n\n          <div class=\"px-2 flex-1 flex flex-col justify-around w-[85%]\">\n            <!-- 歌曲信息 -->\n            <div class=\"song-info\">\n              <div class=\"song-title-container\">\n                <h1 class=\"song-title\" v-html=\"playMusic.name\"></h1>\n              </div>\n              <p class=\"song-artist\">\n                <span\n                  v-for=\"(item, index) in artistList\"\n                  :key=\"index\"\n                  class=\"artist-name\"\n                  @click=\"handleArtistClick(item.id)\"\n                >\n                  {{ item.name }}\n                  {{ index < artistList.length - 1 ? ' / ' : '' }}\n                </span>\n              </p>\n              <div class=\"favorite-icon\" @click=\"toggleFavorite\">\n                <i class=\"ri-heart-3-fill\" :class=\"{ favorite: isFavorite }\"></i>\n              </div>\n            </div>\n\n            <!-- 歌词区域 -->\n            <div class=\"lyrics-container\" v-if=\"!config.hideLyrics\" @click=\"showFullLyricScreen\">\n              <div v-if=\"lrcArray.length > 0\" class=\"lyrics-wrapper\">\n                <div v-for=\"(line, idx) in visibleLyrics\" :key=\"idx\" class=\"lyric-line\">\n                  <!-- 逐字歌词显示 -->\n                  <div\n                    v-if=\"line.hasWordByWord && line.words && line.words.length > 0\"\n                    class=\"word-by-word-lyric\"\n                  >\n                    <template v-for=\"(word, wordIndex) in line.words\" :key=\"wordIndex\">\n                      <span\n                        class=\"lyric-word\"\n                        :style=\"getWordStyle(line.originalIndex, wordIndex, word)\"\n                      >\n                        {{ word.text }}</span\n                      ><span v-if=\"word.space\">&nbsp;</span></template\n                    >\n                  </div>\n                  <!-- 普通歌词显示 -->\n                  <span v-else>{{ line.text }}</span>\n                </div>\n              </div>\n              <div v-else class=\"no-lyrics\">\n                {{ t('player.lrc.noLrc') }}\n              </div>\n            </div>\n          </div>\n        </div>\n      </transition>\n\n      <!-- 横屏模式布局 -->\n      <div v-if=\"isLandscape\" class=\"landscape-layout\">\n        <!-- 左侧封面和进度条 -->\n        <div class=\"landscape-left-section\">\n          <div\n            class=\"landscape-cover-container cover-container\"\n            :class=\"{\n              'record-style': config.mobileCoverStyle === 'record',\n              'square-style': config.mobileCoverStyle === 'square',\n              'full-style': config.mobileCoverStyle === 'full',\n              paused: !play\n            }\"\n            @click=\"cycleCoverStyle\"\n          >\n            <div class=\"img-wrapper\">\n              <n-image\n                :src=\"getImgUrl(playMusic?.picUrl, '500y500')\"\n                lazy\n                preview-disabled\n                class=\"cover-image\"\n                :class=\"{ 'full-blend': config.mobileCoverStyle === 'full' }\"\n              />\n            </div>\n          </div>\n\n          <!-- 左侧进度条 -->\n          <div class=\"landscape-progress-container\">\n            <div class=\"time-info\">\n              <span class=\"current-time\">{{ secondToMinute(nowTime) }}</span>\n              <span class=\"total-time\">{{ secondToMinute(allTime) }}</span>\n            </div>\n            <div\n              class=\"apple-style-progress\"\n              @click=\"handleProgressBarClick\"\n              @mousedown=\"handleMouseDown\"\n            >\n              <div class=\"progress-track\">\n                <div\n                  class=\"progress-fill\"\n                  :style=\"{ width: `${(nowTime / Math.max(1, allTime)) * 100}%` }\"\n                ></div>\n                <div\n                  class=\"progress-thumb\"\n                  :class=\"{ active: isThumbDragging || isMouseDragging }\"\n                  :style=\"{ left: `${(nowTime / Math.max(1, allTime)) * 100}%` }\"\n                  @touchstart=\"handleThumbTouchStart\"\n                  @touchmove=\"handleThumbTouchMove\"\n                  @touchend=\"handleThumbTouchEnd\"\n                  @mousedown=\"handleMouseDown\"\n                ></div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- 右侧歌词区域 -->\n        <div class=\"landscape-lyrics-section\">\n          <!-- 歌曲信息放置在顶部 -->\n          <div class=\"landscape-song-info\">\n            <div class=\"flex flex-col flex-1\">\n              <h1 class=\"song-title\" v-html=\"playMusic.name\"></h1>\n              <p class=\"song-artist\">\n                <span\n                  v-for=\"(item, index) in artistList\"\n                  :key=\"index\"\n                  class=\"artist-name\"\n                  @click=\"handleArtistClick(item.id)\"\n                >\n                  {{ item.name }}{{ index < artistList.length - 1 ? ' / ' : '' }}\n                </span>\n              </p>\n            </div>\n            <div class=\"favorite-icon landscape\" @click=\"toggleFavorite\">\n              <i class=\"ri-heart-3-fill\" :class=\"{ favorite: isFavorite }\"></i>\n            </div>\n          </div>\n\n          <!-- 歌词滚动区域 -->\n          <div\n            ref=\"landscapeLyricsRef\"\n            class=\"landscape-lyrics-scroller\"\n            @touchstart=\"handleTouchStart\"\n            @touchmove=\"handleTouchMove\"\n            @touchend=\"handleTouchEnd\"\n            @scroll=\"handleScroll\"\n          >\n            <div class=\"lyrics-padding-top\"></div>\n            <!-- 无时间戳歌词提示 -->\n            <div v-if=\"!supportAutoScroll\" class=\"lyric-line no-scroll-tip\">\n              <span>{{ t('player.lrc.noAutoScroll') }}</span>\n            </div>\n            <div\n              v-for=\"(item, index) in lrcArray\"\n              :key=\"index\"\n              :id=\"`landscape-lyric-line-${index}`\"\n              class=\"lyric-line\"\n              :class=\"{\n                'now-text': index === nowIndex,\n                'hover-text': item.text && item.startTime !== -1\n              }\"\n              @click=\"item.startTime !== -1 ? setAudioTime(index) : null\"\n            >\n              <!-- 逐字歌词显示 -->\n              <div\n                v-if=\"item.hasWordByWord && item.words && item.words.length > 0\"\n                class=\"word-by-word-lyric\"\n              >\n                <template v-for=\"(word, wordIndex) in item.words\" :key=\"wordIndex\">\n                  <span class=\"lyric-word\" :style=\"getWordStyle(index, wordIndex, word)\">\n                    {{ word.text }} </span\n                  ><span class=\"lyric-word\" v-if=\"word.space\">&nbsp;</span></template\n                >\n              </div>\n              <!-- 普通歌词显示 -->\n              <span v-else :style=\"getLrcStyle(index)\">{{ item.text }}</span>\n              <div v-if=\"config.showTranslation && item.trText\" class=\"translation\">\n                {{ item.trText }}\n              </div>\n            </div>\n            <div class=\"lyrics-padding-bottom\"></div>\n          </div>\n\n          <!-- 右下角控制按钮 -->\n          <div class=\"landscape-main-controls\">\n            <div class=\"main-button prev\" @click=\"prevSong\">\n              <i class=\"ri-skip-back-fill\"></i>\n            </div>\n            <div class=\"main-button play-pause\" @click=\"togglePlay\">\n              <i :class=\"playIcon\"></i>\n            </div>\n            <div class=\"main-button next\" @click=\"nextSong\">\n              <i class=\"ri-skip-forward-fill\"></i>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- 竖屏模式的控制区域 -->\n      <div\n        v-if=\"!isLandscape\"\n        class=\"unified-controls\"\n        :class=\"{ 'fullscreen-mode': showFullLyrics }\"\n      >\n        <!-- 进度条 (苹果风格) -->\n        <div class=\"progress-container\">\n          <div class=\"time-info\">\n            <span class=\"current-time\">{{ secondToMinute(nowTime) }}</span>\n            <span class=\"total-time\">{{ secondToMinute(allTime) }}</span>\n          </div>\n          <div\n            class=\"apple-style-progress\"\n            @click=\"handleProgressBarClick\"\n            @mousedown=\"handleMouseDown\"\n          >\n            <div class=\"progress-track\">\n              <div\n                class=\"progress-fill\"\n                :style=\"{ width: `${(nowTime / Math.max(1, allTime)) * 100}%` }\"\n              ></div>\n              <div\n                class=\"progress-thumb\"\n                :class=\"{ active: isThumbDragging || isMouseDragging }\"\n                :style=\"{ left: `${(nowTime / Math.max(1, allTime)) * 100}%` }\"\n                @touchstart=\"handleThumbTouchStart\"\n                @touchmove=\"handleThumbTouchMove\"\n                @touchend=\"handleThumbTouchEnd\"\n                @mousedown=\"handleMouseDown\"\n              ></div>\n            </div>\n          </div>\n        </div>\n\n        <!-- 控制按钮 -->\n        <div class=\"control-buttons\">\n          <!-- 返回按钮，仅在全屏歌词模式下显示 -->\n          <div v-if=\"showFullLyrics\" class=\"back-button\" @click.stop=\"closeFullLyrics\">\n            <i class=\"ri-arrow-down-s-line\"></i>\n          </div>\n          <div class=\"side-button\" @click=\"togglePlayMode\">\n            <i :class=\"[playModeIcon, { 'intelligence-active': playMode === 3 }]\"></i>\n          </div>\n          <div class=\"main-button prev\" @click=\"prevSong\">\n            <i class=\"ri-skip-back-fill\"></i>\n          </div>\n          <div class=\"main-button play-pause\" @click=\"togglePlay\">\n            <i :class=\"playIcon\"></i>\n          </div>\n          <div class=\"main-button next\" @click=\"nextSong\">\n            <i class=\"ri-skip-forward-fill\"></i>\n          </div>\n          <div class=\"side-button\" @click=\"showPlaylist\">\n            <i class=\"iconfont icon-list\"></i>\n          </div>\n        </div>\n      </div>\n    </div>\n  </n-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport { useWindowSize } from '@vueuse/core';\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport MobilePlayerSettings from '@/components/player/MobilePlayerSettings.vue';\nimport {\n  allTime,\n  artistList,\n  correctionTime,\n  lrcArray,\n  nowIndex,\n  nowTime,\n  playMusic,\n  setAudioTime,\n  sound,\n  textColors,\n  useLyricProgress\n} from '@/hooks/MusicHook';\nimport { useArtist } from '@/hooks/useArtist';\nimport { usePlayMode } from '@/hooks/usePlayMode';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { DEFAULT_LYRIC_CONFIG, LyricConfig } from '@/types/lyric';\nimport { getImgUrl, secondToMinute } from '@/utils';\nimport { animateGradient, getHoverBackgroundColor, getTextColors } from '@/utils/linearColor';\nimport { showBottomToast } from '@/utils/shortcutToast';\n\nconst { t } = useI18n();\nconst playerStore = usePlayerStore();\n\n// 播放控制相关\nconst play = computed(() => playerStore.isPlay);\nconst playIcon = computed(() => (play.value ? 'ri-pause-fill' : 'ri-play-fill'));\n\n// 播放设置弹窗\nconst showPlayerSettings = ref(false);\n\n// 定时器相关\nconst sleepTimerRefresh = ref(0);\nlet sleepTimerInterval: ReturnType<typeof setInterval> | null = null;\n\nconst hasSleepTimerActive = computed(() => playerStore.hasSleepTimerActive);\n\nconst sleepTimerDisplayText = computed(() => {\n  void sleepTimerRefresh.value; // 触发响应式更新\n\n  const timer = playerStore.sleepTimer;\n  if (timer.type === 'time' && timer.endTime) {\n    const remaining = Math.max(0, timer.endTime - Date.now());\n    const totalSeconds = Math.floor(remaining / 1000);\n    const minutes = Math.floor(totalSeconds / 60);\n    const seconds = totalSeconds % 60;\n    return `${minutes}:${seconds.toString().padStart(2, '0')}`;\n  }\n  if (timer.type === 'songs' && timer.remainingSongs) {\n    return `${timer.remainingSongs}首`;\n  }\n  if (timer.type === 'end') {\n    return '列表结束';\n  }\n  return '';\n});\n\n// 启动/停止定时器刷新\nwatch(\n  hasSleepTimerActive,\n  (active) => {\n    if (active && playerStore.sleepTimer.type === 'time') {\n      if (!sleepTimerInterval) {\n        sleepTimerInterval = setInterval(() => {\n          sleepTimerRefresh.value = Date.now();\n        }, 1000);\n      }\n    } else {\n      if (sleepTimerInterval) {\n        clearInterval(sleepTimerInterval);\n        sleepTimerInterval = null;\n      }\n    }\n  },\n  { immediate: true }\n);\n\n// 播放模式\nconst { playMode, playModeIcon, playModeText, togglePlayMode: togglePlayModeBase } = usePlayMode();\n// 打开播放列表\nconst showPlaylist = () => {\n  playerStore.setPlayListDrawerVisible(true);\n};\n\n// 喜欢歌曲\nconst isFavorite = computed(() => {\n  return playerStore.favoriteList.includes(playMusic.value.id as number);\n});\n\nconst toggleFavorite = () => {\n  if (isFavorite.value) {\n    playerStore.removeFromFavorite(playMusic.value.id as number);\n  } else {\n    playerStore.addToFavorite(playMusic.value.id as number);\n  }\n};\n\n// 歌词全屏控制\nconst showFullLyrics = ref(false);\nconst isAutoScrollEnabled = ref(true);\nconst lyricsScrollerRef = ref<HTMLElement | null>(null);\nconst isTouchScrolling = ref(false);\nconst touchStartY = ref(0);\nconst lastScrollTop = ref(0);\nconst autoScrollTimer = ref<number | null>(null);\nconst isSongChanging = ref(false);\n\n// 横屏检测相关\nconst { width, height } = useWindowSize();\nconst isLandscape = computed(() => width.value > height.value);\nconst landscapeLyricsRef = ref<HTMLElement | null>(null);\n\n// 监听横屏变化\nwatch(isLandscape, (newVal) => {\n  if (newVal) {\n    // 横屏模式下，确保歌词容器可见并滚动到当前歌词\n    nextTick(() => {\n      setTimeout(() => {\n        scrollToCurrentLyric(true, landscapeLyricsRef.value);\n      }, 300);\n    });\n  }\n});\n\n// 显示全屏歌词\n// 显示全屏歌词\nconst showFullLyricScreen = () => {\n  showFullLyrics.value = true;\n\n  // 使用多次延迟尝试滚动，确保能够滚动到当前歌词\n  nextTick(() => {\n    scrollToCurrentLyric(true);\n\n    setTimeout(() => {\n      scrollToCurrentLyric(true);\n    }, 200);\n\n    setTimeout(() => {\n      scrollToCurrentLyric(true);\n    }, 500);\n  });\n};\n\nconst supportAutoScroll = computed(() => {\n  return lrcArray.value.length > 0 && lrcArray.value[0].startTime !== -1;\n});\n\n// 关闭全屏歌词\nconst closeFullLyrics = () => {\n  showFullLyrics.value = false;\n  if (autoScrollTimer.value) {\n    clearTimeout(autoScrollTimer.value);\n    autoScrollTimer.value = null;\n  }\n};\n\n// 滚动到当前歌词，添加错误处理和日志\nconst scrollToCurrentLyric = (immediate = false, customScrollerRef?: HTMLElement | null) => {\n  try {\n    const scrollerRef = customScrollerRef || lyricsScrollerRef.value;\n    if (!scrollerRef) {\n      console.log('歌词容器引用不存在');\n      return;\n    }\n\n    if (!supportAutoScroll.value) {\n      console.log('歌词不支持自动滚动');\n      return;\n    }\n\n    // 如果用户正在手动滚动，不打断他们的操作\n    if (isTouchScrolling.value && !immediate) {\n      return;\n    }\n\n    const prefix = customScrollerRef ? 'landscape-' : '';\n    const activeEl = document.getElementById(`${prefix}lyric-line-${nowIndex.value}`);\n    if (!activeEl) {\n      console.log(`找不到当前歌词元素: ${prefix}lyric-line-${nowIndex.value}`);\n      return;\n    }\n\n    const containerRect = scrollerRef.getBoundingClientRect();\n    const lineRect = activeEl.getBoundingClientRect();\n\n    // 优化滚动位置计算，确保当前歌词在视图中央\n    const scrollTop =\n      scrollerRef.scrollTop +\n      (lineRect.top - containerRect.top) -\n      containerRect.height / 2 +\n      lineRect.height / 2;\n\n    console.log(`滚动到歌词 #${nowIndex.value}, 位置: ${scrollTop}px`);\n\n    scrollerRef.scrollTo({\n      top: scrollTop,\n      behavior: immediate ? 'auto' : 'smooth'\n    });\n  } catch (err) {\n    console.error('滚动歌词出错:', err);\n  }\n};\n\n// 监听歌词变化，自动滚动\nwatch(nowIndex, (newIndex, oldIndex) => {\n  console.log(`歌词索引变化: ${oldIndex} -> ${newIndex}`);\n\n  // 歌曲切换时不自动滚动\n  if (isSongChanging.value) return;\n\n  // 在竖屏全屏歌词模式下滚动\n  if (showFullLyrics.value) {\n    nextTick(() => {\n      scrollToCurrentLyric(false);\n    });\n  }\n  // 在横屏模式下滚动\n  else if (isLandscape.value) {\n    nextTick(() => {\n      scrollToCurrentLyric(false, landscapeLyricsRef.value);\n    });\n  }\n});\n\n// 当显示状态变化时，触发滚动\nwatch(showFullLyrics, (newVal) => {\n  if (newVal) {\n    nextTick(() => {\n      setTimeout(() => {\n        scrollToCurrentLyric(true);\n      }, 300);\n    });\n  }\n});\n\n// 监听音乐播放时间变化，触发歌词滚动更新\nwatch(nowTime, () => {\n  // 只有当系统不是由于用户手动拖动进度条而更新时间时才触发滚动\n  if (!isThumbDragging.value && !isTouchScrolling.value) {\n    // 在竖屏全屏歌词模式下滚动\n    if (showFullLyrics.value) {\n      scrollToCurrentLyric(false);\n    }\n    // 在横屏模式下滚动\n    else if (isLandscape.value) {\n      scrollToCurrentLyric(false, landscapeLyricsRef.value);\n    }\n  }\n});\n\n// 处理滚动事件\nconst handleScroll = () => {\n  if (!isTouchScrolling.value) return;\n\n  // 用户手动滚动时，临时停止自动滚动\n  isAutoScrollEnabled.value = false;\n\n  // 清除之前的计时器\n  if (autoScrollTimer.value) {\n    clearTimeout(autoScrollTimer.value);\n  }\n\n  // 设置新的计时器，3秒后恢复自动滚动\n  autoScrollTimer.value = window.setTimeout(() => {\n    isAutoScrollEnabled.value = true;\n    isTouchScrolling.value = false;\n\n    // 滚动到当前歌词\n    if (showFullLyrics.value) {\n      scrollToCurrentLyric(false);\n    } else if (isLandscape.value) {\n      scrollToCurrentLyric(false, landscapeLyricsRef.value);\n    }\n  }, 3000);\n};\n\n// 触摸相关事件\nconst handleTouchStart = (e: TouchEvent) => {\n  touchStartY.value = e.touches[0].clientY;\n\n  // 根据当前模式获取正确的滚动容器\n  const scrollerRef = showFullLyrics.value\n    ? lyricsScrollerRef.value\n    : isLandscape.value\n      ? landscapeLyricsRef.value\n      : lyricsScrollerRef.value;\n\n  lastScrollTop.value = scrollerRef?.scrollTop || 0;\n  isTouchScrolling.value = true;\n\n  // 用户开始触摸时，暂时停止自动滚动\n  isAutoScrollEnabled.value = false;\n\n  // 清除之前可能存在的计时器\n  if (autoScrollTimer.value) {\n    clearTimeout(autoScrollTimer.value);\n    autoScrollTimer.value = null;\n  }\n};\n\nconst handleTouchMove = () => {\n  if (!isTouchScrolling.value) return;\n  // 实际的滚动处理由浏览器默认行为完成\n};\n\nconst handleTouchEnd = () => {\n  // 设置计时器，3秒后恢复自动滚动\n  if (autoScrollTimer.value) {\n    clearTimeout(autoScrollTimer.value);\n  }\n\n  autoScrollTimer.value = window.setTimeout(() => {\n    isAutoScrollEnabled.value = true;\n    isTouchScrolling.value = false;\n\n    // 恢复自动滚动到当前歌词\n    if (showFullLyrics.value) {\n      scrollToCurrentLyric(true);\n    } else if (isLandscape.value) {\n      scrollToCurrentLyric(true, landscapeLyricsRef.value);\n    }\n  }, 3000);\n};\n\n// 封面样式循环切换\nconst cycleCoverStyle = () => {\n  const styles = ['record', 'square', 'full'];\n  const currentIdx = styles.indexOf(config.value.mobileCoverStyle);\n  const nextIdx = (currentIdx + 1) % styles.length;\n  config.value.mobileCoverStyle = styles[nextIdx] as 'record' | 'square' | 'full';\n\n  // 添加动画反馈\n  const container = document.querySelector('.cover-container');\n  if (container) {\n    container.classList.add('style-changing');\n    setTimeout(() => {\n      container.classList.remove('style-changing');\n    }, 500);\n  }\n};\n\n// 进度条相关\nconst isThumbDragging = ref(false);\nconst progressContainerWidth = ref(0);\n\n// 鼠标拖动进度条相关变量\nconst isMouseDragging = ref(false);\n\n// 处理进度条点击\nconst handleProgressBarClick = (e: MouseEvent) => {\n  if (!sound.value) return;\n\n  e.stopPropagation(); // 阻止事件冒泡\n  const progressBar = e.currentTarget as HTMLElement;\n  const rect = progressBar.getBoundingClientRect();\n  const offsetX = e.clientX - rect.left;\n  progressContainerWidth.value = rect.width;\n\n  const percentage = offsetX / rect.width;\n  const newTime = Math.max(0, Math.min(percentage * allTime.value, allTime.value));\n\n  console.log(`进度条点击: ${percentage.toFixed(2)}, 新时间: ${newTime.toFixed(2)}`);\n\n  sound.value.seek(newTime);\n  nowTime.value = newTime;\n};\n\n// 鼠标按下事件\nconst handleMouseDown = (e: MouseEvent) => {\n  if (e.button !== 0) return; // 只处理左键点击\n\n  e.preventDefault();\n  e.stopPropagation();\n  isMouseDragging.value = true;\n\n  // 立即更新位置\n  const progressBar = (e.currentTarget as HTMLElement).closest(\n    '.apple-style-progress'\n  ) as HTMLElement;\n  if (progressBar) {\n    const rect = progressBar.getBoundingClientRect();\n    const offsetX = e.clientX - rect.left;\n    const percentage = Math.max(0, Math.min(1, offsetX / rect.width));\n    const newTime = percentage * allTime.value;\n\n    nowTime.value = newTime;\n    console.log(`鼠标按下，位置: ${percentage.toFixed(2)}, 时间: ${newTime.toFixed(2)}秒`);\n  }\n\n  // 添加全局鼠标事件监听\n  document.addEventListener('mousemove', handleMouseMove);\n  document.addEventListener('mouseup', handleMouseUp);\n};\n\n// 鼠标移动事件\nconst handleMouseMove = (e: MouseEvent) => {\n  if (!isMouseDragging.value || !sound.value) return;\n\n  e.preventDefault();\n\n  // 查找当前视图中的进度条元素\n  const progressBar = isLandscape.value\n    ? document.querySelector('.landscape-left-section .apple-style-progress')\n    : document.querySelector('.unified-controls .apple-style-progress');\n\n  if (!progressBar) return;\n\n  const rect = (progressBar as HTMLElement).getBoundingClientRect();\n  const offsetX = e.clientX - rect.left;\n  const percentage = Math.max(0, Math.min(1, offsetX / rect.width));\n  const newTime = percentage * allTime.value;\n\n  nowTime.value = newTime;\n  console.log(`鼠标移动，位置: ${percentage.toFixed(2)}, 时间: ${newTime.toFixed(2)}秒`);\n};\n\n// 鼠标释放事件\nconst handleMouseUp = (e: MouseEvent) => {\n  if (!isMouseDragging.value || !sound.value) return;\n\n  e.preventDefault();\n\n  // 释放时跳转到指定位置\n  sound.value.seek(nowTime.value);\n  console.log(`鼠标释放，跳转到: ${nowTime.value.toFixed(2)}秒`);\n\n  isMouseDragging.value = false;\n\n  // 移除全局事件监听\n  document.removeEventListener('mousemove', handleMouseMove);\n  document.removeEventListener('mouseup', handleMouseUp);\n};\n\n// 处理滑块拖动\nconst handleThumbTouchStart = (e: TouchEvent) => {\n  e.preventDefault(); // 阻止默认行为\n  e.stopPropagation(); // 阻止事件冒泡\n  isThumbDragging.value = true;\n\n  // 获取进度条宽度\n  const target = e.currentTarget as HTMLElement;\n  const progressBar = target.parentElement?.parentElement as HTMLElement;\n  if (progressBar) {\n    progressContainerWidth.value = progressBar.getBoundingClientRect().width;\n    console.log(`进度条宽度: ${progressContainerWidth.value}px`);\n  }\n};\n\nconst handleThumbTouchMove = (e: TouchEvent) => {\n  if (!isThumbDragging.value || !sound.value) return;\n\n  e.preventDefault(); // 阻止默认行为\n\n  const touch = e.touches[0];\n  const target = e.currentTarget as HTMLElement;\n  const progressBar = target.parentElement?.parentElement as HTMLElement;\n  const rect = progressBar.getBoundingClientRect();\n  const offsetX = touch.clientX - rect.left;\n\n  // 计算百分比并限制在0-1之间\n  const percentage = Math.max(0, Math.min(1, offsetX / rect.width));\n  const newTime = percentage * allTime.value;\n\n  // 实时更新UI，但不频繁seek\n  nowTime.value = newTime;\n\n  console.log(`thumb拖动: ${percentage.toFixed(2)}, 时间: ${newTime.toFixed(2)}`);\n};\n\nconst handleThumbTouchEnd = (e: TouchEvent) => {\n  if (!isThumbDragging.value || !sound.value) return;\n\n  e.preventDefault(); // 阻止默认行为\n  e.stopPropagation(); // 阻止事件冒泡\n\n  // 拖动结束时执行seek操作\n  console.log(`拖动结束，跳转到: ${nowTime.value.toFixed(2)}秒`);\n  sound.value.seek(nowTime.value);\n  isThumbDragging.value = false;\n};\n\n// 背景相关\nconst currentBackground = ref('');\nconst animationFrame = ref<number | null>(null);\nconst isDark = ref(false);\nconst config = ref<LyricConfig>({ ...DEFAULT_LYRIC_CONFIG });\n\n// 可见歌词计算\nconst visibleLyrics = computed(() => {\n  const centerIndex = nowIndex.value;\n  const numLines = 3;\n  const halfLines = Math.floor(numLines / 2);\n\n  let startIdx = centerIndex - halfLines;\n  let endIdx = centerIndex + halfLines;\n\n  // 处理奇偶数行数的情况\n  if (numLines % 2 === 0) {\n    endIdx -= 1;\n  }\n\n  // 处理边界情况\n  if (startIdx < 0) {\n    startIdx = 0;\n    endIdx = Math.min(numLines - 1, lrcArray.value.length - 1);\n  }\n\n  if (endIdx >= lrcArray.value.length) {\n    endIdx = lrcArray.value.length - 1;\n    startIdx = Math.max(0, endIdx - numLines + 1);\n  }\n\n  // 返回带有原始索引的歌词数组\n  return lrcArray.value.slice(startIdx, endIdx + 1).map((item, idx) => ({\n    ...item,\n    originalIndex: startIdx + idx\n  }));\n});\n\nconst props = defineProps({\n  modelValue: {\n    type: Boolean,\n    default: false\n  },\n  background: {\n    type: String,\n    default: ''\n  }\n});\n\nconst themeMusic = {\n  light: 'linear-gradient(to bottom, #ffffff, #f5f5f5)',\n  dark: 'linear-gradient(to bottom, #1a1a1a, #000000)'\n};\n\nconst emit = defineEmits(['update:modelValue']);\n\nconst isVisible = computed({\n  get: () => props.modelValue,\n  set: (value) => emit('update:modelValue', value)\n});\n\n// 设置文字颜色\nconst setTextColors = (background: string) => {\n  if (!background) {\n    textColors.value = getTextColors();\n    document.documentElement.style.setProperty('--hover-bg-color', getHoverBackgroundColor(false));\n    document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);\n    document.documentElement.style.setProperty('--text-color-active', textColors.value.active);\n    document.documentElement.style.setProperty('--bg-color', 'rgba(25, 25, 25, 1)');\n    return;\n  }\n\n  // 更新文字颜色\n  textColors.value = getTextColors(background);\n  isDark.value = textColors.value.active === '#000000';\n\n  document.documentElement.style.setProperty(\n    '--hover-bg-color',\n    getHoverBackgroundColor(isDark.value)\n  );\n  document.documentElement.style.setProperty('--text-color-primary', textColors.value.primary);\n  document.documentElement.style.setProperty('--text-color-active', textColors.value.active);\n\n  // 解析背景颜色用于封面融合\n  let bgColor = playerStore.playMusic.primaryColor || 'rgba(25, 25, 25, 1)';\n\n  document.documentElement.style.setProperty('--bg-color', bgColor);\n\n  // 处理背景颜色动画\n  if (currentBackground.value) {\n    if (animationFrame.value) {\n      cancelAnimationFrame(animationFrame.value);\n    }\n    const result = animateGradient(currentBackground.value, background, (gradient) => {\n      currentBackground.value = gradient;\n    });\n    if (typeof result === 'number') {\n      animationFrame.value = result;\n    }\n  } else {\n    currentBackground.value = background;\n  }\n};\n\nconst targetBackground = computed(() => {\n  if (config.value.theme !== 'default') {\n    return themeMusic[config.value.theme] || props.background;\n  }\n  return props.background;\n});\n\n// 监听目标背景变化并更新文字颜色\nwatch(\n  targetBackground,\n  (newBg) => {\n    if (newBg) {\n      setTextColors(newBg);\n    }\n  },\n  { immediate: true }\n);\n\n// 组件卸载时清理动画\nonBeforeUnmount(() => {\n  if (animationFrame.value) {\n    cancelAnimationFrame(animationFrame.value);\n  }\n  if (autoScrollTimer.value) {\n    clearTimeout(autoScrollTimer.value);\n  }\n\n  // 清理鼠标事件监听\n  document.removeEventListener('mousemove', handleMouseMove);\n  document.removeEventListener('mouseup', handleMouseUp);\n});\n\nconst { navigateToArtist } = useArtist();\n\nconst handleArtistClick = (id: number) => {\n  isVisible.value = false;\n  navigateToArtist(id);\n};\n\n// 播放控制功能\nconst togglePlay = () => {\n  try {\n    playerStore.setPlay(playMusic.value);\n  } catch (error) {\n    console.error('播放出错:', error);\n  }\n};\n\nconst nextSong = () => {\n  playerStore.nextPlay();\n};\n\nconst prevSong = () => {\n  playerStore.prevPlay();\n};\n\nconst togglePlayMode = () => {\n  togglePlayModeBase();\n  showBottomToast(playModeText.value);\n};\n\nconst closeMusicFull = () => {\n  isVisible.value = false;\n  playerStore.setMusicFull(false);\n};\n\n// 添加对 playMusic.id 的监听，歌曲切换时滚动到顶部\nwatch(\n  () => playMusic.value.id,\n  (newId, oldId) => {\n    // 只在歌曲真正切换时滚动到顶部\n    if (newId !== oldId && newId) {\n      isSongChanging.value = true;\n      // 延迟滚动，确保 nowIndex 已重置\n      setTimeout(() => {\n        // 在全屏歌词模式下滚动到顶部\n        if (showFullLyrics.value && lyricsScrollerRef.value) {\n          lyricsScrollerRef.value.scrollTo({\n            top: 0,\n            behavior: 'smooth'\n          });\n        }\n        // 在横屏模式下滚动到顶部\n        else if (isLandscape.value && landscapeLyricsRef.value) {\n          landscapeLyricsRef.value.scrollTo({\n            top: 0,\n            behavior: 'smooth'\n          });\n        }\n        // 延迟恢复自动滚动，等待歌词数据更新\n        setTimeout(() => {\n          isSongChanging.value = false;\n        }, 300);\n      }, 100);\n    }\n  }\n);\n\n// 加载保存的配置\nonMounted(() => {\n  const savedConfig = localStorage.getItem('music-full-config');\n  if (savedConfig) {\n    config.value = { ...config.value, ...JSON.parse(savedConfig) };\n  }\n\n  // 初始化自动滚动状态\n  isAutoScrollEnabled.value = true;\n  isTouchScrolling.value = false;\n\n  // 等待DOM元素渲染完成后初始化歌词滚动\n  nextTick(() => {\n    if (isVisible.value) {\n      // 在横屏模式下\n      if (isLandscape.value) {\n        setTimeout(() => {\n          scrollToCurrentLyric(true, landscapeLyricsRef.value);\n        }, 500);\n      }\n      // 在全屏歌词模式下\n      else if (showFullLyrics.value) {\n        setTimeout(() => {\n          scrollToCurrentLyric(true);\n        }, 500);\n      }\n    }\n  });\n});\n\n// 当显示状态变化时，更新封面与背景融合效果\nwatch(isVisible, (newVal) => {\n  if (newVal) {\n    // 播放器显示时，重新设置背景颜色\n    if (targetBackground.value) {\n      setTextColors(targetBackground.value);\n    }\n  } else {\n    showFullLyrics.value = false;\n    if (autoScrollTimer.value) {\n      clearTimeout(autoScrollTimer.value);\n      autoScrollTimer.value = null;\n    }\n  }\n});\n\n// 添加getLrcStyle函数\nconst { getLrcStyle: originalLrcStyle } = useLyricProgress();\n\n// 修改 getLrcStyle 函数\nconst getLrcStyle = (index: number) => {\n  const colors = textColors.value || getTextColors;\n  const originalStyle = originalLrcStyle(index);\n\n  if (index === nowIndex.value) {\n    // 当前播放的歌词，使用渐变效果\n    return {\n      ...originalStyle,\n      backgroundImage: originalStyle.backgroundImage\n        ?.replace(/#ffffff/g, colors.active)\n        .replace(/#ffffff8a/g, `${colors.primary}`),\n      backgroundClip: 'text',\n      WebkitBackgroundClip: 'text',\n      color: 'transparent'\n    };\n  }\n\n  // 非当前播放的歌词，使用普通颜色\n  return {\n    color: colors.primary\n  };\n};\n\n// 逐字歌词样式函数\nconst getWordStyle = (lineIndex: number, _wordIndex: number, word: any) => {\n  const colors = textColors.value || getTextColors();\n  // 如果不是当前行，返回普通样式\n  if (lineIndex !== nowIndex.value) {\n    return {\n      color: colors.primary,\n      transition: 'color 0.3s ease',\n      // 重置背景相关属性\n      backgroundImage: 'none',\n      WebkitTextFillColor: 'initial'\n    };\n  }\n\n  // 当前行的逐字效果，应用歌词矫正时间\n  const currentTime = (nowTime.value + correctionTime.value) * 1000; // 转换为毫秒，确保与word时间单位一致\n\n  // 直接使用绝对时间比较\n  const wordStartTime = word.startTime; // 单词开始的绝对时间（毫秒）\n  const wordEndTime = word.startTime + word.duration;\n\n  if (currentTime >= wordStartTime && currentTime < wordEndTime) {\n    // 当前正在播放的单词 - 使用渐变进度效果\n    const progress = Math.min((currentTime - wordStartTime) / word.duration, 1);\n    const progressPercent = Math.round(progress * 100);\n\n    return {\n      backgroundImage: `linear-gradient(to right, ${colors.active} 0%, ${colors.active} ${progressPercent}%, ${colors.primary} ${progressPercent}%, ${colors.primary} 100%)`,\n      backgroundClip: 'text',\n      WebkitBackgroundClip: 'text',\n      WebkitTextFillColor: 'transparent',\n      textShadow: `0 0 8px ${colors.active}40`,\n      transition: 'all 0.1s ease'\n    };\n  } else if (currentTime >= wordEndTime) {\n    // 已经播放过的单词 - 纯色显示\n    return {\n      color: colors.active,\n      WebkitTextFillColor: 'initial',\n      transition: 'none'\n    };\n  } else {\n    // 还未播放的单词 - 普通状态\n    return {\n      color: colors.primary,\n      WebkitTextFillColor: 'initial',\n      transition: 'none'\n    };\n  }\n};\n</script>\n\n<style scoped lang=\"scss\">\n#mobile-drawer-target {\n  @apply top-0 left-0 absolute overflow-hidden flex flex-col w-full h-full;\n  animation-duration: 300ms;\n\n  // 通用控制按钮样式\n  .main-button {\n    @apply flex items-center justify-center cursor-pointer transition-all duration-200 rounded-full;\n\n    i {\n      @apply text-2xl;\n      color: var(--text-color-active);\n    }\n\n    &.play-pause {\n      i {\n        @apply text-4xl;\n      }\n    }\n\n    &:hover {\n      transform: scale(1.05);\n    }\n\n    &:active {\n      transform: scale(0.95);\n    }\n  }\n\n  // 通用进度条样式\n  .apple-style-progress {\n    @apply relative flex items-center cursor-pointer;\n    touch-action: none; // 确保触摸事件正常工作\n\n    .progress-track {\n      @apply relative w-full h-2 bg-white bg-opacity-20 rounded-full;\n\n      .progress-fill {\n        @apply absolute top-0 left-0 h-full bg-white rounded-full;\n        box-shadow: 0 0 8px rgba(255, 255, 255, 0.5);\n        z-index: 1;\n        transition: width 0.1s linear;\n      }\n\n      .progress-thumb {\n        @apply absolute top-1/2 -translate-y-1/2 -translate-x-1/2 rounded-full bg-white;\n        box-shadow: 0 0 8px rgba(255, 255, 255, 0.6);\n        z-index: 2;\n        transition: transform 0.15s ease-out;\n\n        &.active {\n          transform: translate(-50%, -50%) scale(1.3);\n          box-shadow: 0 0 12px rgba(255, 255, 255, 0.9);\n        }\n\n        &:active {\n          transform: translate(-50%, -50%) scale(1.3);\n        }\n      }\n    }\n  }\n\n  // 通用唱片样式\n  .record-style-common {\n    @apply rounded-full overflow-hidden relative;\n    aspect-ratio: 1/1;\n\n    &::before {\n      content: '';\n      @apply absolute top-0 left-0 w-full h-full rounded-full z-10;\n      background: radial-gradient(\n        circle at center,\n        transparent 38%,\n        rgba(0, 0, 0, 0.15) 38%,\n        rgba(0, 0, 0, 0.15) 39%,\n        rgba(255, 255, 255, 0.1) 39%,\n        rgba(255, 255, 255, 0.1) 39.5%,\n        rgba(0, 0, 0, 0.08) 39.5%,\n        rgba(0, 0, 0, 0.08) 40.5%,\n        rgba(0, 0, 0, 0.2) 40.5%,\n        rgba(0, 0, 0, 0.2) 41.5%,\n        rgba(0, 0, 0, 0.6) 41.5%,\n        rgba(0, 0, 0, 0.6) 100%\n      );\n      pointer-events: none;\n      animation: spin 20s linear infinite;\n      animation-play-state: running;\n    }\n\n    &::after {\n      content: '';\n      @apply absolute w-6 h-6 rounded-full bg-gray-900 z-20;\n      top: 50%;\n      left: 50%;\n      transform: translate(-50%, -50%);\n      box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.4);\n    }\n\n    &.paused {\n      &::before,\n      &::after {\n        animation-play-state: paused;\n      }\n    }\n\n    .img-wrapper {\n      @apply rounded-full overflow-hidden border-solid border-black z-0;\n      position: absolute;\n      top: 50%;\n      left: 50%;\n      transform: translate(-50%, -50%);\n\n      &::after {\n        content: '';\n        @apply absolute top-0 left-0 w-full h-full rounded-full z-[2];\n        background: linear-gradient(\n          135deg,\n          rgba(255, 255, 255, 0.05) 0%,\n          rgba(255, 255, 255, 0) 50%,\n          rgba(0, 0, 0, 0.05) 100%\n        );\n        pointer-events: none;\n      }\n    }\n\n    .cover-image {\n      @apply w-full h-full rounded-full border-[2px] border-gray-900;\n      animation: spin 20s linear infinite;\n      animation-play-state: running;\n    }\n\n    &.paused .cover-image {\n      animation-play-state: paused;\n    }\n  }\n\n  // 通用时间显示样式\n  .time-info {\n    @apply flex justify-between items-center mb-2;\n\n    .current-time,\n    .total-time {\n      @apply text-sm;\n      color: var(--text-color-primary);\n      opacity: 0.8;\n    }\n  }\n\n  // 通用收藏按钮样式\n  .favorite-icon {\n    @apply cursor-pointer transition-all duration-200;\n\n    i {\n      @apply text-xl;\n      color: var(--text-color-primary);\n\n      &.favorite {\n        @apply text-red-500 !important;\n      }\n    }\n\n    &:hover {\n      transform: scale(1.1);\n    }\n\n    &:active {\n      transform: scale(0.9);\n    }\n\n    &.landscape {\n      i {\n        @apply text-3xl;\n      }\n    }\n  }\n\n  // 通用歌曲信息样式\n  .song-info-common {\n    @apply z-[9995];\n\n    .song-title {\n      @apply font-bold line-clamp-1;\n      color: var(--text-color-active);\n    }\n\n    .song-artist {\n      @apply font-medium line-clamp-1;\n      color: var(--text-color-primary);\n      opacity: 0.9;\n\n      .artist-name {\n        @apply cursor-pointer;\n\n        &:hover {\n          @apply underline;\n        }\n      }\n    }\n  }\n\n  // 横屏模式布局\n  &.is-landscape {\n    .landscape-layout {\n      @apply flex flex-row w-full h-full overflow-hidden px-8 gap-4;\n\n      // 左侧区域\n      .landscape-left-section {\n        @apply h-full flex flex-col items-center justify-center pt-6 pb-6 px-3 relative;\n        width: 35%;\n        min-width: 320px;\n        max-width: 480px;\n\n        // 封面\n        .landscape-cover-container {\n          @apply flex-shrink-0 mx-auto mb-4 z-[9995];\n          width: 85%;\n          max-width: 260px;\n          min-width: 180px;\n\n          &.record-style {\n            @extend .record-style-common;\n\n            .img-wrapper {\n              @apply border-[20px];\n              width: 90%;\n              height: 90%;\n            }\n          }\n        }\n\n        // 左侧进度条\n        .landscape-progress-container {\n          @apply mt-0 mb-2 px-2 w-full max-w-md;\n\n          .apple-style-progress {\n            height: 48px; // 增加高度使更容易点击\n\n            .progress-thumb {\n              @apply w-5 h-5;\n            }\n          }\n        }\n      }\n\n      // 右侧区域\n      .landscape-lyrics-section {\n        @apply h-full flex-1 flex flex-col relative;\n\n        // 歌曲信息\n        .landscape-song-info {\n          @apply flex justify-between items-center pt-5 z-[9995] px-4;\n          @extend .song-info-common;\n\n          .song-title {\n            @apply text-2xl mb-1;\n          }\n\n          .song-artist {\n            @apply text-base;\n          }\n        }\n\n        // 歌词滚动区域\n        .landscape-lyrics-scroller {\n          @apply h-full w-full overflow-y-auto pt-24 pb-24;\n          scroll-behavior: smooth;\n          -webkit-overflow-scrolling: touch;\n          mask-image: linear-gradient(\n            to bottom,\n            transparent 5%,\n            black 15%,\n            black 85%,\n            transparent 95%\n          );\n          -webkit-mask-image: linear-gradient(\n            to bottom,\n            transparent 5%,\n            black 15%,\n            black 85%,\n            transparent 95%\n          );\n        }\n\n        // 控制按钮\n        .landscape-main-controls {\n          @apply fixed bottom-6 right-6 flex items-center z-[10000];\n\n          .main-button {\n            @apply mx-2;\n            width: 54px;\n            height: 54px;\n            background-color: rgba(255, 255, 255, 0.15);\n            backdrop-filter: blur(8px);\n            border-radius: 50%;\n\n            &.play-pause {\n              width: 70px;\n              height: 70px;\n              background-color: rgba(255, 255, 255, 0.25);\n            }\n          }\n        }\n      }\n    }\n  }\n\n  // 竖屏模式布局\n  &:not(.is-landscape) {\n    .ios-layout-container {\n      @apply flex flex-col items-center justify-between w-full h-full pt-10;\n      padding-bottom: 180px; // 为控制区域留出空间\n\n      // 封面样式\n      .cover-container {\n        @apply relative mb-6 transition-all duration-500 border-gray-900 z-[9995];\n\n        &.style-changing {\n          animation: styleChange 0.5s ease;\n        }\n\n        &.record-style {\n          @extend .record-style-common;\n          @apply w-72 h-72;\n\n          .img-wrapper {\n            @apply border-[40px];\n            width: 90%;\n            height: 90%;\n          }\n        }\n      }\n\n      // 歌曲信息\n      .song-info {\n        @apply flex flex-col items-center mb-5 w-full z-[9995];\n        @extend .song-info-common;\n\n        .song-title-container {\n          @apply w-full text-center;\n\n          .song-title {\n            @apply text-2xl inline-block;\n          }\n        }\n\n        .song-artist {\n          @apply text-base mb-2;\n        }\n\n        .ri-heart-3-fill {\n          @apply text-2xl;\n        }\n      }\n    }\n\n    // 统一控制区域\n    .unified-controls {\n      @apply fixed bottom-0 left-0 right-0 px-6 pt-6 pb-6;\n      background: linear-gradient(to top, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);\n      height: 230px;\n      pointer-events: auto;\n      z-index: 10000 !important;\n\n      .progress-container {\n        @apply w-full mb-6;\n\n        .apple-style-progress {\n          height: 40px;\n\n          .progress-thumb {\n            @apply w-4 h-4;\n          }\n        }\n      }\n\n      .control-buttons {\n        @apply flex items-center justify-between w-full px-4;\n\n        .side-button {\n          @apply w-10 h-10 flex items-center justify-center cursor-pointer transition-all duration-200;\n\n          i {\n            @apply text-2xl;\n            color: var(--text-color-primary);\n\n            &.intelligence-active {\n              @apply text-green-500;\n            }\n          }\n\n          &:hover {\n            i {\n              color: var(--text-color-active);\n            }\n          }\n        }\n\n        .main-button {\n          @apply w-14 h-14;\n\n          i {\n            @apply text-3xl;\n          }\n\n          &.play-pause {\n            @apply w-16 h-16 bg-white/15 rounded-full backdrop-blur-sm;\n\n            i {\n              @apply text-4xl;\n            }\n          }\n\n          &:hover:not(.play-pause) {\n            i {\n              color: var(--text-color-active);\n            }\n          }\n\n          &.play-pause:hover {\n            @apply bg-white/30;\n          }\n        }\n      }\n    }\n  }\n}\n\n// 旋转动画\n@keyframes spin {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n// 加载动画\n.loading-overlay {\n  @apply absolute top-0 left-0 w-full h-full flex items-center justify-center;\n  background-color: rgba(0, 0, 0, 0.5);\n  z-index: 9999999999;\n\n  .loading-icon {\n    font-size: 36px;\n    color: white;\n    animation: spin 1s linear infinite;\n  }\n}\n\n// 根据封面样式调整容器布局\n#mobile-drawer-target.cover-style-record {\n  .ios-layout-container .cover-container {\n    @apply mt-4;\n  }\n}\n\n#mobile-drawer-target.cover-style-full {\n  .ios-layout-container {\n    @apply pt-0;\n  }\n}\n\n// 过渡动画\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n\n@keyframes styleChange {\n  0% {\n    opacity: 0.7;\n    transform: scale(0.95);\n  }\n  50% {\n    opacity: 0.9;\n    transform: scale(1.03);\n  }\n  100% {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n@keyframes clickPulse {\n  0% {\n    opacity: 0.5;\n    transform: scale(1);\n  }\n  50% {\n    opacity: 1;\n    transform: scale(1.1);\n  }\n  100% {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n@keyframes pulse {\n  0% {\n    opacity: 0.9;\n  }\n  50% {\n    opacity: 1;\n  }\n  100% {\n    opacity: 0.9;\n  }\n}\n\n.favorite-icon {\n  @apply cursor-pointer transition-all duration-200;\n\n  i {\n    @apply text-xl;\n    color: var(--text-color-primary);\n\n    &.favorite {\n      @apply text-red-500 !important;\n    }\n  }\n\n  &:hover {\n    transform: scale(1.1);\n  }\n\n  &:active {\n    transform: scale(0.9);\n  }\n\n  &.landscape {\n    @apply mt-2;\n    i {\n      @apply text-2xl;\n    }\n  }\n}\n\n// 歌曲标题容器样式\n.song-title-container {\n  @apply w-full flex items-center justify-center relative;\n\n  .song-title {\n    @apply text-center text-2xl font-bold max-w-[80%] truncate;\n    color: var(--text-color-active);\n  }\n}\n\n// 通用歌词样式\n.lyric-line {\n  @apply cursor-pointer transition-all duration-300 font-medium;\n  font-weight: 500;\n  letter-spacing: var(--lyric-letter-spacing, 0);\n  line-height: var(--lyric-line-height, 1.6);\n  color: var(--text-color-primary);\n  opacity: 0.8;\n\n  &.no-scroll-tip {\n    @apply text-base opacity-60 cursor-default py-2;\n    color: var(--text-color-primary);\n    font-weight: normal;\n\n    span {\n      padding-right: 0;\n    }\n  }\n\n  span {\n    background-clip: text !important;\n    -webkit-background-clip: text !important;\n  }\n\n  &.now-text {\n    @apply font-medium py-4;\n    color: var(--text-color-active);\n    opacity: 1;\n  }\n\n  &.clicked {\n    animation: clickPulse 0.3s ease-in-out;\n  }\n\n  .translation {\n    @apply font-normal opacity-70 mt-1 text-base;\n  }\n\n  // 逐字歌词样式\n  .word-by-word-lyric {\n    @apply flex flex-wrap justify-center;\n\n    .lyric-word {\n      @apply inline-block;\n      font-weight: inherit;\n      font-size: inherit;\n      letter-spacing: inherit;\n      line-height: inherit;\n      cursor: inherit;\n      position: relative;\n      padding-right: 0 !important;\n\n      &:hover {\n        background-color: rgba(255, 255, 255, 0.1);\n      }\n    }\n  }\n}\n\n// 全屏歌词相关样式\n.fullscreen-lyrics {\n  @apply flex flex-col w-full h-full relative;\n\n  &.light {\n    background: linear-gradient(to bottom, #ffffff, #f5f5f5);\n  }\n\n  &.dark {\n    background: linear-gradient(to bottom, #1a1a1a, #000000);\n  }\n\n  .fullscreen-header {\n    @apply pt-16 pb-4 px-6 flex flex-col items-center fixed top-0 left-0 w-full z-10;\n    background: linear-gradient(to bottom, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0) 100%);\n    pointer-events: auto;\n\n    .song-title {\n      @apply text-xl font-semibold text-center mb-1 max-w-full line-clamp-1;\n      color: var(--text-color-active);\n    }\n\n    .artist-name {\n      @apply text-sm text-opacity-80 text-center;\n      color: var(--text-color-primary);\n    }\n  }\n\n  .lyrics-scroller {\n    @apply flex-1 overflow-y-auto px-4;\n    scroll-behavior: smooth;\n    -webkit-overflow-scrolling: touch;\n    mask-image: linear-gradient(to bottom, transparent 0%, black 10%, black 90%, transparent 100%);\n    -webkit-mask-image: linear-gradient(\n      to bottom,\n      transparent 0%,\n      black 10%,\n      black 90%,\n      transparent 100%\n    );\n    padding-top: 100px;\n    padding-bottom: 200px;\n    margin-bottom: 180px;\n    margin-top: 90px;\n\n    .lyrics-padding-top {\n      height: 70px;\n      min-height: 70px;\n    }\n\n    .lyrics-padding-bottom {\n      height: 150px;\n      min-height: 150px;\n    }\n\n    .lyric-line {\n      @apply px-6 py-4 text-center;\n      font-size: var(--lyric-font-size, 22px);\n\n      span {\n        padding-right: 10px;\n      }\n    }\n\n    .now-text {\n      @apply text-2xl;\n    }\n  }\n}\n\n// 必要的控制按钮样式\n.control-btn {\n  @apply w-9 h-9 flex items-center justify-center rounded cursor-pointer transition-all duration-300 z-[9999];\n  background: rgba(142, 142, 142, 0.192);\n  backdrop-filter: blur(12px);\n  top: calc(var(--safe-area-inset-top, 0) + 20px);\n\n  i {\n    @apply text-xl;\n    color: var(--text-color-active);\n  }\n\n  &.pure-mode {\n    background: transparent;\n    backdrop-filter: none;\n\n    &:not(:hover) {\n      i {\n        opacity: 0;\n      }\n    }\n  }\n\n  &:hover {\n    background: rgba(126, 121, 121, 0.2);\n    i {\n      opacity: 1;\n    }\n  }\n}\n\n#mobile-drawer-target {\n  // 横屏模式下的歌词样式\n  &.is-landscape {\n    .landscape-lyrics-section {\n      .landscape-lyrics-scroller {\n        .lyrics-padding-top {\n          height: 30px;\n          min-height: 30px;\n        }\n\n        .lyrics-padding-bottom {\n          height: 100px;\n          min-height: 100px;\n        }\n\n        .lyric-line {\n          @apply px-4 py-3 text-left;\n          font-size: 26px;\n        }\n\n        .now-text {\n          @apply text-3xl;\n        }\n      }\n    }\n\n    .word-by-word-lyric {\n      @apply justify-start;\n    }\n  }\n\n  .unified-controls {\n    &.fullscreen-mode {\n      background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%);\n    }\n\n    .back-button {\n      @apply absolute top-4 left-1/2 -translate-x-1/2 w-10 h-10 flex items-center justify-center bg-black bg-opacity-30 rounded-2xl;\n\n      i {\n        @apply text-4xl;\n        color: var(--text-color-primary);\n      }\n    }\n  }\n\n  .ios-layout-container {\n    .lyrics-container {\n      @apply w-full flex-grow flex flex-col items-center justify-center mb-6 overflow-hidden cursor-pointer;\n\n      .lyrics-wrapper {\n        @apply w-full flex flex-col items-center justify-center;\n\n        .lyric-line {\n          @apply text-center py-1 transition-all duration-300 opacity-70;\n\n          &:nth-child(2) {\n            @apply text-lg font-medium opacity-100;\n            color: var(--text-color-active);\n          }\n\n          .translation {\n            @apply text-sm opacity-60 mt-1;\n          }\n        }\n      }\n\n      .lyric-word {\n        @apply px-[2px];\n      }\n\n      .no-lyrics {\n        @apply text-center text-base opacity-60;\n        color: var(--text-color-primary);\n      }\n    }\n  }\n}\n\n.cover-container {\n  // 方形封面样式\n  &.square-style {\n    @apply w-[85%] shadow-2xl shadow-black/50 rounded-xl overflow-hidden mt-8 aspect-square;\n\n    .cover-image {\n      @apply w-full h-full;\n      transition: transform 0.3s ease-out;\n\n      &:active {\n        transform: scale(0.95);\n      }\n    }\n  }\n\n  // 全屏封面样式\n  &.full-style {\n    @apply w-full max-h-[50vh] relative overflow-hidden;\n\n    &::after {\n      content: '';\n      position: absolute;\n      bottom: 0;\n      left: 0;\n      right: 0;\n      height: 40%;\n      background: linear-gradient(\n        transparent,\n        var(--bg-color, rgba(25, 25, 25, 1)) 70%,\n        var(--bg-color, rgba(25, 25, 25, 1))\n      );\n      z-index: 1;\n      pointer-events: none;\n    }\n\n    .cover-image {\n      @apply w-full h-auto shadow-lg;\n\n      &.full-blend {\n        mix-blend-mode: luminosity;\n      }\n    }\n  }\n}\n\n.is-dark {\n  .square-style {\n    @apply shadow-2xl shadow-black/50;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/lyric/MusicFullWrapper.vue",
    "content": "<template>\n  <component :is=\"componentToUse\" v-bind=\"$attrs\" ref=\"musicFullRef\" />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue';\n\nimport MusicFull from '@/components/lyric/MusicFull.vue';\nimport MusicFullMobile from '@/components/lyric/MusicFullMobile.vue';\nimport { isMobile } from '@/utils';\n\n// 根据当前设备类型选择需要显示的组件\nconst componentToUse = computed(() => {\n  return isMobile.value ? MusicFullMobile : MusicFull;\n});\n\nconst musicFullRef = ref<InstanceType<typeof MusicFull>>();\n\ndefineExpose({\n  musicFullRef\n});\n</script>\n"
  },
  {
    "path": "src/renderer/components/lyric/ThemeColorPanel.vue",
    "content": "<template>\r\n  <div\r\n    v-show=\"visible\"\r\n    class=\"theme-color-panel\"\r\n    :class=\"{ visible: visible, hidden: !visible }\"\r\n    @click.stop\r\n  >\r\n    <div class=\"panel-header\">\r\n      <span class=\"panel-title\">{{ t('settings.themeColor.title') }}</span>\r\n      <div class=\"close-button\" @click=\"handleClose\">\r\n        <i class=\"ri-close-line\"></i>\r\n      </div>\r\n    </div>\r\n\r\n    <div class=\"panel-content\">\r\n      <!-- 横向紧凑布局 -->\r\n      <div class=\"compact-layout\">\r\n        <!-- 预设颜色 -->\r\n        <div class=\"preset-section\">\r\n          <div class=\"section-label\">{{ t('settings.themeColor.presetColors') }}</div>\r\n          <div class=\"preset-colors\">\r\n            <div\r\n              v-for=\"color in presetColors\"\r\n              :key=\"color.id\"\r\n              class=\"color-dot\"\r\n              :class=\"{ active: isColorActive(color) }\"\r\n              :style=\"{ backgroundColor: getColorValue(color) }\"\r\n              :title=\"getColorName(color)\"\r\n              @click=\"handlePresetColorSelect(color)\"\r\n            >\r\n              <i v-if=\"isColorActive(color)\" class=\"ri-check-line\"></i>\r\n            </div>\r\n          </div>\r\n        </div>\r\n\r\n        <!-- 分隔线 -->\r\n        <div class=\"divider\"></div>\r\n\r\n        <!-- 自定义颜色 -->\r\n        <div class=\"custom-section\">\r\n          <div class=\"section-label\">{{ t('settings.themeColor.customColor') }}</div>\r\n          <div class=\"custom-controls\">\r\n            <div\r\n              class=\"color-preview\"\r\n              :style=\"{ backgroundColor: currentColor }\"\r\n              @click=\"showColorPicker = !showColorPicker\"\r\n              :title=\"\r\n                showColorPicker\r\n                  ? t('settings.themeColor.tooltips.closeColorPicker')\r\n                  : t('settings.themeColor.tooltips.openColorPicker')\r\n              \"\r\n            >\r\n              <i class=\"ri-palette-line\"></i>\r\n            </div>\r\n            <input\r\n              v-model=\"colorInput\"\r\n              type=\"text\"\r\n              class=\"color-input\"\r\n              :placeholder=\"t('settings.themeColor.placeholder')\"\r\n              @input=\"handleColorInput\"\r\n              @keyup.enter=\"handleColorInputConfirm\"\r\n            />\r\n          </div>\r\n        </div>\r\n\r\n        <!-- 分隔线 -->\r\n        <div class=\"divider\"></div>\r\n\r\n        <!-- 效果预览 -->\r\n        <div class=\"preview-section\">\r\n          <div class=\"section-label\">{{ t('settings.themeColor.preview') }}</div>\r\n          <div class=\"preview-text\" :style=\"getPreviewStyle()\">\r\n            {{ t('settings.themeColor.previewText') }}\r\n          </div>\r\n        </div>\r\n      </div>\r\n\r\n      <!-- 颜色选择器（展开时显示） -->\r\n      <div v-if=\"showColorPicker\" class=\"color-picker-dropdown\">\r\n        <n-color-picker\r\n          v-model:value=\"pickerColor\"\r\n          :show-alpha=\"false\"\r\n          :modes=\"['hex']\"\r\n          size=\"small\"\r\n          @update:value=\"handlePickerColorChange\"\r\n        />\r\n      </div>\r\n    </div>\r\n  </div>\r\n</template>\r\n\r\n<script setup lang=\"ts\">\r\nimport { NColorPicker } from 'naive-ui';\r\nimport { ref, watch } from 'vue';\r\nimport { useI18n } from 'vue-i18n';\r\n\r\nimport {\r\n  getLyricThemeColors,\r\n  getPresetColorValue,\r\n  type LyricThemeColor,\r\n  optimizeColorForTheme,\r\n  validateColor\r\n} from '@/utils/linearColor';\r\n\r\ninterface Props {\r\n  visible: boolean;\r\n  currentColor: string;\r\n  theme: 'light' | 'dark';\r\n}\r\n\r\ninterface Emits {\r\n  (e: 'colorChange', _color: string): void;\r\n  (e: 'close'): void;\r\n}\r\n\r\nconst props = withDefaults(defineProps<Props>(), {\r\n  visible: false,\r\n  currentColor: '#1db954',\r\n  theme: 'dark'\r\n});\r\n\r\nconst emit = defineEmits<Emits>();\r\n\r\nconst { t } = useI18n();\r\n\r\n// 响应式数据\r\nconst presetColors = ref<LyricThemeColor[]>(getLyricThemeColors());\r\nconst showColorPicker = ref(false);\r\nconst colorInput = ref('');\r\nconst pickerColor = ref(props.currentColor);\r\n\r\n// 计算属性\r\nconst getColorValue = (color: LyricThemeColor): string => {\r\n  return getPresetColorValue(color.id, props.theme);\r\n};\r\n\r\nconst isColorActive = (color: LyricThemeColor): boolean => {\r\n  const colorValue = getColorValue(color);\r\n  return colorValue === props.currentColor;\r\n};\r\n\r\nconst getColorName = (color: LyricThemeColor): string => {\r\n  return t(`settings.themeColor.colorNames.${color.id}`) || color.name;\r\n};\r\n\r\nconst getPreviewStyle = () => {\r\n  const progress = 60; // 模拟60%的播放进度\r\n  return {\r\n    background: `linear-gradient(to right, ${props.currentColor} ${progress}%, var(--text-color) ${progress}%)`,\r\n    WebkitBackgroundClip: 'text',\r\n    WebkitTextFillColor: 'transparent',\r\n    fontSize: '18px',\r\n    fontWeight: '600'\r\n  };\r\n};\r\n\r\n// 事件处理\r\nconst handleClose = () => {\r\n  showColorPicker.value = false;\r\n  emit('close');\r\n};\r\n\r\nconst handlePresetColorSelect = (color: LyricThemeColor) => {\r\n  const colorValue = getColorValue(color);\r\n  const optimizedColor = optimizeColorForTheme(colorValue, props.theme);\r\n  emit('colorChange', optimizedColor);\r\n\r\n  // 更新输入框和选择器\r\n  colorInput.value = optimizedColor;\r\n  pickerColor.value = optimizedColor;\r\n};\r\n\r\nconst handleColorInput = () => {\r\n  if (validateColor(colorInput.value)) {\r\n    try {\r\n      const optimizedColor = optimizeColorForTheme(colorInput.value, props.theme);\r\n      pickerColor.value = optimizedColor;\r\n      emit('colorChange', optimizedColor);\r\n    } catch (error) {\r\n      console.error('Failed to optimize color:', error);\r\n      // 恢复到当前有效颜色\r\n      colorInput.value = props.currentColor;\r\n      pickerColor.value = props.currentColor;\r\n    }\r\n  }\r\n};\r\n\r\nconst handleColorInputConfirm = () => {\r\n  if (validateColor(colorInput.value)) {\r\n    try {\r\n      const optimizedColor = optimizeColorForTheme(colorInput.value, props.theme);\r\n      emit('colorChange', optimizedColor);\r\n    } catch (error) {\r\n      console.error('Failed to optimize color:', error);\r\n      // 恢复到当前有效颜色\r\n      colorInput.value = props.currentColor;\r\n      pickerColor.value = props.currentColor;\r\n    }\r\n  } else {\r\n    console.warn('Invalid color input:', colorInput.value);\r\n    // 恢复到当前有效颜色\r\n    colorInput.value = props.currentColor;\r\n    pickerColor.value = props.currentColor;\r\n  }\r\n};\r\n\r\nconst handlePickerColorChange = (color: string) => {\r\n  if (validateColor(color)) {\r\n    try {\r\n      const optimizedColor = optimizeColorForTheme(color, props.theme);\r\n      colorInput.value = optimizedColor;\r\n      emit('colorChange', optimizedColor);\r\n    } catch (error) {\r\n      console.error('Failed to optimize picker color:', error);\r\n      // 恢复到当前有效颜色\r\n      colorInput.value = props.currentColor;\r\n      pickerColor.value = props.currentColor;\r\n    }\r\n  } else {\r\n    console.warn('Invalid picker color:', color);\r\n  }\r\n};\r\n\r\n// 监听属性变化\r\nwatch(\r\n  () => props.currentColor,\r\n  (newColor) => {\r\n    colorInput.value = newColor;\r\n    pickerColor.value = newColor;\r\n  },\r\n  { immediate: true }\r\n);\r\n\r\nwatch(\r\n  () => props.visible,\r\n  (visible) => {\r\n    if (!visible) {\r\n      showColorPicker.value = false;\r\n    }\r\n  }\r\n);\r\n</script>\r\n\r\n<style scoped lang=\"scss\">\r\n.theme-color-panel {\r\n  position: absolute;\r\n  top: 50px;\r\n  left: 50%;\r\n  transform: translateX(-50%);\r\n  width: auto;\r\n  min-width: 480px;\r\n  max-width: calc(100vw - 40px);\r\n  background: var(--control-bg);\r\n  backdrop-filter: blur(10px);\r\n  border-radius: 8px;\r\n  padding: 12px;\r\n  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);\r\n  z-index: 1000;\r\n  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n\r\n  &.hidden {\r\n    opacity: 0;\r\n    visibility: hidden;\r\n    transform: translateX(-50%) translateY(-10px) scale(0.95);\r\n    pointer-events: none;\r\n  }\r\n\r\n  &.visible {\r\n    opacity: 1;\r\n    visibility: visible;\r\n    transform: translateX(-50%) translateY(0) scale(1);\r\n    pointer-events: auto;\r\n  }\r\n\r\n  // 小屏幕适配\r\n  @media (max-width: 520px) {\r\n    min-width: calc(100vw - 40px);\r\n    left: 20px;\r\n    transform: none;\r\n\r\n    &.hidden {\r\n      transform: translateY(-10px) scale(0.95);\r\n    }\r\n\r\n    &.visible {\r\n      transform: translateY(0) scale(1);\r\n    }\r\n  }\r\n}\r\n\r\n.panel-header {\r\n  display: flex;\r\n  justify-content: space-between;\r\n  align-items: center;\r\n  margin-bottom: 12px;\r\n\r\n  .panel-title {\r\n    font-size: 13px;\r\n    font-weight: 600;\r\n    color: var(--text-color);\r\n    opacity: 0.9;\r\n  }\r\n\r\n  .close-button {\r\n    width: 24px;\r\n    height: 24px;\r\n    display: flex;\r\n    align-items: center;\r\n    justify-content: center;\r\n    cursor: pointer;\r\n    border-radius: 6px;\r\n    color: var(--text-color);\r\n    transition: all 0.2s ease;\r\n\r\n    &:hover {\r\n      background: rgba(255, 255, 255, 0.15);\r\n      color: #ff6b6b;\r\n    }\r\n\r\n    i {\r\n      font-size: 14px;\r\n    }\r\n  }\r\n}\r\n\r\n.panel-content {\r\n  .compact-layout {\r\n    display: flex;\r\n    align-items: center;\r\n    gap: 16px;\r\n  }\r\n\r\n  .section-label {\r\n    font-size: 11px;\r\n    font-weight: 500;\r\n    color: var(--text-color);\r\n    opacity: 0.7;\r\n    margin-bottom: 6px;\r\n    text-align: center;\r\n  }\r\n\r\n  .divider {\r\n    width: 1px;\r\n    height: 40px;\r\n    background: rgba(255, 255, 255, 0.1);\r\n  }\r\n}\r\n\r\n// 预设颜色区域\r\n.preset-section {\r\n  .preset-colors {\r\n    display: flex;\r\n    gap: 6px;\r\n\r\n    .color-dot {\r\n      width: 24px;\r\n      height: 24px;\r\n      border-radius: 50%;\r\n      cursor: pointer;\r\n      border: 2px solid transparent;\r\n      transition: all 0.2s ease;\r\n      display: flex;\r\n      align-items: center;\r\n      justify-content: center;\r\n\r\n      &:hover {\r\n        transform: scale(1.1);\r\n        border-color: rgba(255, 255, 255, 0.3);\r\n      }\r\n\r\n      &.active {\r\n        border-color: var(--text-color);\r\n        box-shadow: 0 0 0 2px var(--control-bg);\r\n      }\r\n\r\n      i {\r\n        color: white;\r\n        font-size: 10px;\r\n        text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\n// 自定义颜色区域\r\n.custom-section {\r\n  .custom-controls {\r\n    display: flex;\r\n    gap: 8px;\r\n    align-items: center;\r\n\r\n    .color-preview {\r\n      width: 24px;\r\n      height: 24px;\r\n      border-radius: 4px;\r\n      cursor: pointer;\r\n      border: 1px solid rgba(255, 255, 255, 0.2);\r\n      display: flex;\r\n      align-items: center;\r\n      justify-content: center;\r\n      transition: all 0.2s ease;\r\n\r\n      &:hover {\r\n        border-color: rgba(255, 255, 255, 0.4);\r\n        transform: scale(1.05);\r\n      }\r\n\r\n      i {\r\n        color: white;\r\n        font-size: 12px;\r\n        text-shadow: 0 0 4px rgba(0, 0, 0, 0.8);\r\n      }\r\n    }\r\n\r\n    .color-input {\r\n      width: 80px;\r\n      height: 24px;\r\n      background: rgba(255, 255, 255, 0.08);\r\n      border: 1px solid rgba(255, 255, 255, 0.2);\r\n      border-radius: 4px;\r\n      padding: 0 6px;\r\n      color: var(--text-color);\r\n      font-size: 11px;\r\n      font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;\r\n      outline: none;\r\n      transition: all 0.2s ease;\r\n\r\n      &:focus {\r\n        border-color: var(--highlight-color, rgba(255, 255, 255, 0.4));\r\n        background: rgba(255, 255, 255, 0.12);\r\n      }\r\n\r\n      &::placeholder {\r\n        color: rgba(255, 255, 255, 0.4);\r\n      }\r\n    }\r\n  }\r\n}\r\n\r\n// 预览区域\r\n.preview-section {\r\n  .preview-text {\r\n    font-size: 14px;\r\n    font-weight: 600;\r\n    line-height: 1.2;\r\n    white-space: nowrap;\r\n    transition: all 0.2s ease;\r\n\r\n    &:hover {\r\n      transform: scale(1.02);\r\n    }\r\n  }\r\n}\r\n\r\n// 颜色选择器下拉\r\n.color-picker-dropdown {\r\n  margin-top: 8px;\r\n  padding: 8px;\r\n  background: rgba(0, 0, 0, 0.2);\r\n  border-radius: 6px;\r\n  border: 1px solid rgba(255, 255, 255, 0.1);\r\n\r\n  :deep(.n-color-picker) {\r\n    width: 100%;\r\n  }\r\n}\r\n\r\n// 小屏幕适配\r\n@media (max-width: 520px) {\r\n  .compact-layout {\r\n    flex-direction: column;\r\n    gap: 12px;\r\n\r\n    .divider {\r\n      width: 100%;\r\n      height: 1px;\r\n    }\r\n  }\r\n\r\n  .preset-section .preset-colors {\r\n    justify-content: center;\r\n  }\r\n}\r\n</style>\r\n"
  },
  {
    "path": "src/renderer/components/player/AdvancedControlsPopover.vue",
    "content": "<template>\n  <n-dropdown\n    :show=\"showDropdown\"\n    :options=\"dropdownOptions\"\n    trigger=\"hover\"\n    :z-index=\"9999999\"\n    @select=\"handleSelect\"\n    placement=\"top\"\n    @update:show=\"(show) => (showDropdown = show)\"\n  >\n    <n-tooltip trigger=\"hover\" :z-index=\"9999999\">\n      <template #trigger>\n        <div class=\"advanced-controls-btn\">\n          <i class=\"iconfont ri-settings-3-line\"></i>\n\n          <!-- 激活状态的小标记 -->\n          <div v-if=\"hasActiveSettings\" class=\"active-indicator\">\n            <span v-if=\"hasActiveSleepTimer\" class=\"timer-badge\">\n              <i class=\"ri-time-line\"></i>\n            </span>\n          </div>\n        </div>\n      </template>\n      {{ t('player.playBar.advancedControls') }}\n    </n-tooltip>\n  </n-dropdown>\n\n  <!-- EQ 均衡器弹窗 -->\n  <n-modal\n    v-model:show=\"showEQModal\"\n    :mask-closable=\"true\"\n    :unstable-show-mask=\"false\"\n    :z-index=\"9999999\"\n  >\n    <div class=\"eq-modal-content\">\n      <div class=\"modal-close\" @click=\"showEQModal = false\">\n        <i class=\"ri-close-line\"></i>\n      </div>\n      <eq-control />\n    </div>\n  </n-modal>\n\n  <!-- 定时关闭弹窗 -->\n  <n-modal\n    v-model:show=\"playerStore.showSleepTimer\"\n    :mask-closable=\"true\"\n    :unstable-show-mask=\"false\"\n    :z-index=\"9999999\"\n  >\n    <div class=\"timer-modal-content\">\n      <div class=\"modal-close\" @click=\"playerStore.showSleepTimer = false\">\n        <i class=\"ri-close-line\"></i>\n      </div>\n      <sleep-timer />\n    </div>\n  </n-modal>\n\n  <!-- 播放速度设置弹窗 -->\n  <n-modal\n    v-model:show=\"showSpeedModal\"\n    :mask-closable=\"true\"\n    :unstable-show-mask=\"false\"\n    :z-index=\"9999999\"\n  >\n    <div class=\"speed-modal-content\">\n      <div class=\"modal-close\" @click=\"showSpeedModal = false\">\n        <i class=\"ri-close-line\"></i>\n      </div>\n      <h3>{{ t('player.playBar.playbackSpeed') }} ({{ playbackRate }}x)</h3>\n      <div class=\"speed-controls\">\n        <div class=\"speed-options\">\n          <div\n            v-for=\"option in playbackRateOptions\"\n            :key=\"option.key\"\n            class=\"speed-option\"\n            :class=\"{ active: playbackRate === option.key }\"\n            @click=\"selectSpeed(option.key)\"\n          >\n            {{ option.label }}\n          </div>\n        </div>\n        <div class=\"speed-slider\">\n          <n-slider\n            :value=\"playbackRate\"\n            :min=\"0.25\"\n            :max=\"2.0\"\n            :step=\"0.01\"\n            @update:value=\"selectSpeed\"\n          />\n        </div>\n      </div>\n    </div>\n  </n-modal>\n</template>\n\n<script lang=\"ts\" setup>\nimport { DropdownOption, NSlider } from 'naive-ui';\nimport { computed, h, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport EqControl from '@/components/EQControl.vue';\nimport SleepTimer from '@/components/player/SleepTimer.vue';\nimport { usePlayerStore } from '@/store/modules/player';\n\nconst { t } = useI18n();\nconst playerStore = usePlayerStore();\n\n// 下拉菜单状态\nconst showDropdown = ref(false);\nconst showEQModal = ref(false);\nconst showSpeedModal = ref(false);\nconst isEQVisible = ref(false);\n\n// 监听弹窗状态，确保互斥\nwatch(showEQModal, (newValue) => {\n  if (newValue) {\n    // 如果EQ弹窗打开，关闭其他弹窗\n    playerStore.showSleepTimer = false;\n    showSpeedModal.value = false;\n  }\n});\n\nwatch(\n  () => playerStore.showSleepTimer,\n  (newValue) => {\n    if (newValue) {\n      // 如果睡眠定时器弹窗打开，关闭其他弹窗\n      showEQModal.value = false;\n      showSpeedModal.value = false;\n    }\n  }\n);\n\nwatch(showSpeedModal, (newValue) => {\n  if (newValue) {\n    // 如果播放速度弹窗打开，关闭其他弹窗\n    showEQModal.value = false;\n    playerStore.showSleepTimer = false;\n  }\n});\n\n// 播放速度状态\nconst playbackRate = computed(() => playerStore.playbackRate);\n\n// 播放速度选项\nconst playbackRateOptions = [\n  { label: '0.5x', key: 0.5 },\n  { label: '0.75x', key: 0.75 },\n  { label: '1.0x', key: 1.0 },\n  { label: '1.25x', key: 1.25 },\n  { label: '1.5x', key: 1.5 },\n  { label: '2.0x', key: 2.0 }\n];\n\n// 是否有激活的睡眠定时器\nconst hasActiveSleepTimer = computed(() => playerStore.hasSleepTimerActive);\n\n// 检查是否有任何高级设置是激活状态\nconst hasActiveSettings = computed(() => {\n  return playbackRate.value !== 1.0 || hasActiveSleepTimer.value || isEQVisible.value;\n});\n\n// 下拉菜单选项\nconst dropdownOptions = computed<DropdownOption[]>(() => [\n  {\n    label: t('player.playBar.eq'),\n    key: 'eq',\n    icon: () => h('i', { class: 'ri-equalizer-line' })\n  },\n  {\n    label: t('player.sleepTimer.title'),\n    key: 'timer',\n    icon: () => h('i', { class: 'ri-timer-line' }),\n    // 如果有激活的定时器，添加标记\n    suffix: () => (hasActiveSleepTimer.value ? h('span', { class: 'active-option-mark' }) : null)\n  },\n  {\n    label: t('player.playBar.playbackSpeed') + `(${playbackRate.value}x)`,\n    key: 'speed',\n    icon: () => h('i', { class: 'ri-speed-line' }),\n    // 如果播放速度不是1.0，添加标记\n    suffix: () =>\n      playbackRate.value !== 1.0\n        ? h('span', { class: 'active-option-mark' }, `${playbackRate.value}x`)\n        : null\n  }\n]);\n\n// 处理菜单选择\nconst handleSelect = (key: string) => {\n  // 先关闭所有弹窗\n  showEQModal.value = false;\n  playerStore.showSleepTimer = false;\n  showSpeedModal.value = false;\n\n  // 然后仅打开所选弹窗\n  switch (key) {\n    case 'eq':\n      showEQModal.value = true;\n      break;\n    case 'timer':\n      playerStore.showSleepTimer = true;\n      break;\n    case 'speed':\n      showSpeedModal.value = true;\n      break;\n  }\n};\n\n// 选择播放速度\nconst selectSpeed = (speed: number) => {\n  playerStore.setPlaybackRate(speed);\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.sleep-timer-countdown {\n  @apply fixed top-0 left-1/2 transform -translate-x-1/2 py-1 px-3 rounded-b-lg bg-green-500 text-white text-sm flex items-center;\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);\n  z-index: 9998;\n  min-width: 80px;\n  text-align: center;\n  animation: fadeInDown 0.3s ease-out;\n\n  @keyframes fadeInDown {\n    from {\n      transform: translate(-50%, -100%);\n      opacity: 0;\n    }\n    to {\n      transform: translate(-50%, 0);\n      opacity: 1;\n    }\n  }\n\n  span {\n    font-variant-numeric: tabular-nums;\n    letter-spacing: 0.5px;\n    font-weight: 500;\n  }\n}\n\n.advanced-controls-btn {\n  @apply cursor-pointer mx-3 relative;\n\n  .iconfont {\n    @apply text-2xl transition;\n    @apply hover:text-green-500;\n  }\n\n  .active-indicator {\n    @apply absolute -top-1 -right-1 flex;\n\n    .timer-badge,\n    .speed-badge {\n      @apply flex items-center justify-center text-xs bg-green-500 text-white rounded-full;\n      height: 16px;\n      min-width: 16px;\n      padding: 0 3px;\n      font-weight: 600;\n      font-size: 10px;\n\n      i {\n        font-size: 10px;\n      }\n    }\n\n    .timer-badge + .speed-badge {\n      @apply -ml-2 z-10;\n    }\n  }\n}\n\n.eq-modal-content,\n.timer-modal-content,\n.speed-modal-content {\n  @apply p-6 rounded-3xl bg-light-100 dark:bg-dark-100 bg-opacity-80 filter backdrop-blur-sm;\n  max-width: 600px;\n  margin: 0 auto;\n}\n\n.eq-modal-content {\n  @apply p-10 max-w-[800px];\n}\n\n.speed-modal-content {\n  h3 {\n    @apply text-lg font-medium mb-4 text-center;\n  }\n\n  .speed-controls {\n    @apply my-8 mx-4;\n  }\n  .speed-options {\n    @apply flex flex-wrap justify-center gap-4;\n  }\n  .speed-slider {\n    @apply mt-4;\n  }\n  .speed-option {\n    @apply py-2 px-4 rounded-full cursor-pointer transition-all;\n    @apply bg-gray-100 dark:bg-gray-800;\n    @apply hover:bg-green-100 dark:hover:bg-green-900;\n  }\n  .speed-option.active {\n    @apply bg-green-500 text-white;\n  }\n}\n\n.active-option-mark {\n  @apply ml-2 text-xs bg-green-500 text-white py-0.5 px-1.5 rounded-full;\n  font-weight: 500;\n}\n\n.modal-close {\n  @apply absolute top-4 right-4 cursor-pointer hover:text-green-500;\n  i {\n    @apply text-2xl;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/player/MiniPlayBar.vue",
    "content": "<template>\n  <div\n    class=\"mini-play-bar\"\n    :class=\"{ 'pure-mode': pureModeEnabled, 'mini-mode': settingsStore.isMiniMode }\"\n  >\n    <div class=\"mini-bar-container\">\n      <!-- 专辑封面 -->\n      <div class=\"album-cover\" @click=\"setMusicFull\">\n        <n-image\n          :src=\"getImgUrl(playMusic?.picUrl, '100y100')\"\n          fallback-src=\"/placeholder.png\"\n          class=\"cover-img\"\n          preview-disabled\n        />\n      </div>\n\n      <!-- 歌曲信息 -->\n      <div class=\"song-info\" @click=\"setMusicFull\">\n        <div class=\"song-title\" v-html=\"playMusic?.name || '未播放'\"></div>\n        <div class=\"song-artist\">\n          <span\n            v-for=\"(artists, artistsindex) in artistList\"\n            :key=\"artistsindex\"\n            class=\"cursor-pointer hover:text-green-500\"\n            @click.stop=\"handleArtistClick(artists.id)\"\n          >\n            {{ artists.name }}{{ artistsindex < artistList.length - 1 ? ' / ' : '' }}\n          </span>\n        </div>\n      </div>\n\n      <!-- 控制按钮区域 -->\n      <div class=\"control-buttons\">\n        <div class=\"control-button previous\" @click=\"handlePrev\">\n          <i class=\"iconfont icon-prev\"></i>\n        </div>\n        <div class=\"control-button play\" @click=\"playMusicEvent\">\n          <i class=\"iconfont\" :class=\"play ? 'icon-stop' : 'icon-play'\"></i>\n        </div>\n        <div class=\"control-button next\" @click=\"handleNext\">\n          <i class=\"iconfont icon-next\"></i>\n        </div>\n      </div>\n\n      <!-- 右侧功能按钮 -->\n      <div class=\"function-buttons\">\n        <div class=\"function-button\">\n          <i\n            class=\"iconfont icon-likefill\"\n            :class=\"{ 'like-active': isFavorite }\"\n            @click=\"toggleFavorite\"\n          ></i>\n        </div>\n\n        <n-popover\n          v-if=\"component\"\n          trigger=\"hover\"\n          :z-index=\"99999999\"\n          placement=\"top\"\n          :show-arrow=\"false\"\n        >\n          <template #trigger>\n            <div class=\"function-button\" @click=\"mute\" @wheel.prevent=\"handleVolumeWheel\">\n              <i class=\"iconfont\" :class=\"getVolumeIcon\"></i>\n            </div>\n          </template>\n          <div class=\"volume-slider-wrapper transparent-popover\">\n            <n-slider\n              v-model:value=\"volumeSlider\"\n              :step=\"0.01\"\n              :tooltip=\"false\"\n              vertical\n              @wheel.prevent=\"handleVolumeWheel\"\n            ></n-slider>\n          </div>\n        </n-popover>\n\n        <!-- 播放列表按钮 -->\n        <div v-if=\"!component\" class=\"function-button\" @click=\"togglePlaylist\">\n          <i class=\"iconfont icon-list\"></i>\n        </div>\n      </div>\n\n      <!-- 关闭按钮 -->\n      <div v-if=\"!component\" class=\"close-button\" @click=\"handleClose\">\n        <i class=\"iconfont ri-close-line\"></i>\n      </div>\n    </div>\n\n    <!-- 进度条 -->\n    <div\n      class=\"progress-bar\"\n      @click=\"handleProgressClick\"\n      @mousemove=\"handleProgressHover\"\n      @mouseleave=\"handleProgressLeave\"\n    >\n      <div class=\"progress-track\"></div>\n      <div class=\"progress-fill\" :style=\"{ width: `${(nowTime / allTime) * 100}%` }\"></div>\n    </div>\n\n    <!-- 播放列表 - 单独放在外层，不再使用 popover -->\n    <div\n      v-if=\"!component\"\n      v-show=\"isPlaylistOpen\"\n      class=\"playlist-container\"\n      :class=\"{ 'mini-mode-list': settingsStore.isMiniMode }\"\n    >\n      <n-scrollbar ref=\"palyListRef\" class=\"playlist-scrollbar\">\n        <div class=\"playlist-items\">\n          <div v-for=\"item in playList\" :key=\"item.id\" class=\"music-play-list-content\">\n            <div class=\"flex items-center justify-between\">\n              <song-item :key=\"item.id\" class=\"flex-1\" :item=\"item\" mini></song-item>\n              <div class=\"delete-btn\" @click.stop=\"handleDeleteSong(item)\">\n                <i\n                  class=\"iconfont ri-delete-bin-line text-gray-400 hover:text-red-500 transition-colors\"\n                ></i>\n              </div>\n            </div>\n          </div>\n        </div>\n      </n-scrollbar>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, provide, ref, useTemplateRef } from 'vue';\n\nimport SongItem from '@/components/common/SongItem.vue';\nimport { allTime, artistList, nowTime, playMusic } from '@/hooks/MusicHook';\nimport { useArtist } from '@/hooks/useArtist';\nimport { audioService } from '@/services/audioService';\nimport { isBilibiliIdMatch, usePlayerStore, useSettingsStore } from '@/store';\nimport type { SongResult } from '@/types/music';\nimport { getImgUrl } from '@/utils';\n\nconst playerStore = usePlayerStore();\nconst settingsStore = useSettingsStore();\nconst { navigateToArtist } = useArtist();\n\nwithDefaults(\n  defineProps<{\n    pureModeEnabled?: boolean;\n    component?: boolean;\n  }>(),\n  {\n    component: false\n  }\n);\n\n// 处理关闭按钮点击\nconst handleClose = () => {\n  if (settingsStore.isMiniMode) {\n    window.api.restore();\n  }\n};\n\n// 是否播放\nconst play = computed(() => playerStore.play as boolean);\n// 播放列表\nconst playList = computed(() => playerStore.playList as SongResult[]);\n\n// 音量控制\nconst audioVolume = ref(\n  localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1\n);\n\nconst volumeSlider = computed({\n  get: () => audioVolume.value * 100,\n  set: (value) => {\n    localStorage.setItem('volume', (value / 100).toString());\n    audioService.setVolume(value / 100);\n    audioVolume.value = value / 100;\n  }\n});\n\n// 音量图标\nconst getVolumeIcon = computed(() => {\n  if (audioVolume.value === 0) return 'ri-volume-mute-line';\n  if (audioVolume.value <= 0.5) return 'ri-volume-down-line';\n  return 'ri-volume-up-line';\n});\n\n// 静音\nconst mute = () => {\n  if (volumeSlider.value === 0) {\n    volumeSlider.value = 30;\n  } else {\n    volumeSlider.value = 0;\n  }\n};\n\n// 鼠标滚轮调整音量\nconst handleVolumeWheel = (e: WheelEvent) => {\n  // 向上滚动增加音量，向下滚动减少音量\n  const delta = e.deltaY < 0 ? 5 : -5;\n  const newValue = Math.min(Math.max(volumeSlider.value + delta, 0), 100);\n  volumeSlider.value = newValue;\n};\n\n// 收藏相关\nconst isFavorite = computed(() => {\n  // 对于B站视频，使用ID匹配函数\n  if (playMusic.value.source === 'bilibili' && playMusic.value.bilibiliData?.bvid) {\n    return playerStore.favoriteList.some((id) => isBilibiliIdMatch(id, playMusic.value.id));\n  }\n\n  // 非B站视频直接比较ID\n  return playerStore.favoriteList.includes(playMusic.value.id);\n});\n\nconst toggleFavorite = async (e: Event) => {\n  e.stopPropagation();\n\n  // 处理B站视频的收藏ID\n  let favoriteId = playMusic.value.id;\n  if (playMusic.value.source === 'bilibili' && playMusic.value.bilibiliData?.bvid) {\n    // 如果当前播放的是B站视频，且已有ID不包含--格式，则需要构造完整ID\n    if (!String(favoriteId).includes('--')) {\n      favoriteId = `${playMusic.value.bilibiliData.bvid}--${playMusic.value.song?.ar?.[0]?.id || 0}--${playMusic.value.bilibiliData.cid}`;\n    }\n  }\n\n  if (isFavorite.value) {\n    playerStore.removeFromFavorite(favoriteId);\n  } else {\n    playerStore.addToFavorite(favoriteId);\n  }\n};\n\n// 播放列表相关\nconst palyListRef = useTemplateRef('palyListRef') as any;\nconst isPlaylistOpen = ref(false);\n\n// 提供 openPlaylistDrawer 给子组件\nprovide('openPlaylistDrawer', (songId: number) => {\n  console.log('打开歌单抽屉', songId);\n  // 由于在迷你模式不处理这个功能，所以只记录日志\n});\n\n// 切换播放列表显示/隐藏\nconst togglePlaylist = () => {\n  isPlaylistOpen.value = !isPlaylistOpen.value;\n  console.log('切换播放列表状态', isPlaylistOpen.value);\n\n  // 调整窗口大小\n  if (settingsStore.isMiniMode) {\n    try {\n      if (isPlaylistOpen.value) {\n        // 打开播放列表时调整DOM\n        document.body.style.height = 'auto';\n        document.body.style.overflow = 'visible';\n\n        // 使用新的专用 API 调整窗口大小\n        if (window.api && typeof window.api.resizeMiniWindow === 'function') {\n          window.api.resizeMiniWindow(true);\n        }\n      } else {\n        // 关闭播放列表时强制调整DOM\n        document.body.style.height = '64px';\n        document.body.style.overflow = 'hidden';\n\n        // 使用新的专用 API 调整窗口大小\n        if (window.api && typeof window.api.resizeMiniWindow === 'function') {\n          window.api.resizeMiniWindow(false);\n        }\n      }\n    } catch (error) {\n      console.error('调整窗口大小失败:', error);\n    }\n  }\n\n  // 如果打开列表，滚动到当前播放歌曲\n  if (isPlaylistOpen.value) {\n    scrollToPlayList();\n  }\n};\n\nconst scrollToPlayList = () => {\n  setTimeout(() => {\n    const currentIndex = playerStore.playListIndex;\n    const itemHeight = 69; // 每个列表项的高度\n    palyListRef.value?.scrollTo({\n      top: currentIndex * itemHeight,\n      behavior: 'smooth'\n    });\n  }, 50);\n};\n\nconst handleDeleteSong = (song: SongResult) => {\n  if (song.id === playMusic.value.id) {\n    playerStore.nextPlay();\n  }\n  playerStore.removeFromPlayList(song.id as number);\n};\n\n// 艺术家点击\nconst handleArtistClick = (id: number) => {\n  navigateToArtist(id);\n};\n\n// 进度条相关\nconst handleProgressClick = (e: MouseEvent) => {\n  const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();\n  const percent = (e.clientX - rect.left) / rect.width;\n  audioService.seek(allTime.value * percent);\n  nowTime.value = allTime.value * percent;\n};\n\nconst hoverTime = ref(0);\nconst isHovering = ref(false);\n\nconst handleProgressHover = (e: MouseEvent) => {\n  const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();\n  const percent = (e.clientX - rect.left) / rect.width;\n  hoverTime.value = allTime.value * percent;\n  isHovering.value = true;\n};\n\nconst handleProgressLeave = () => {\n  isHovering.value = false;\n};\n\n// 播放控制\nconst handlePrev = () => playerStore.prevPlay();\nconst handleNext = () => playerStore.nextPlay();\n\nconst playMusicEvent = async () => {\n  try {\n    playerStore.setPlay(playerStore.playMusic);\n  } catch (error) {\n    console.error('播放出错:', error);\n    playerStore.nextPlay();\n  }\n};\n\n// 切换到完整播放器\nconst setMusicFull = () => {\n  playerStore.setMusicFull(true);\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.mini-play-bar {\n  @apply w-full flex flex-col bg-light-200 dark:bg-dark-200 shadow-md bg-opacity-60 backdrop-blur dark:bg-opacity-60;\n  height: 64px;\n  border-radius: 8px;\n  position: relative;\n\n  &.mini-mode {\n    @apply shadow-lg;\n    -webkit-app-region: drag;\n\n    .mini-bar-container {\n      @apply px-2;\n    }\n\n    .song-info {\n      width: 120px;\n\n      .song-title {\n        @apply text-xs font-medium;\n      }\n\n      .song-artist {\n        @apply text-xs opacity-50;\n      }\n    }\n\n    .function-buttons {\n      -webkit-app-region: no-drag;\n      @apply space-x-1 ml-1;\n\n      .function-button {\n        width: 28px;\n        height: 28px;\n\n        .iconfont {\n          @apply text-base;\n        }\n      }\n    }\n\n    .control-buttons {\n      @apply mx-1 space-x-0.5;\n      -webkit-app-region: no-drag;\n      .control-button {\n        width: 28px;\n        height: 28px;\n\n        .iconfont {\n          @apply text-base;\n        }\n      }\n    }\n\n    .close-button {\n      -webkit-app-region: no-drag;\n      width: 28px;\n      height: 28px;\n    }\n\n    .album-cover {\n      @apply flex-shrink-0 mr-2;\n      width: 36px;\n      height: 36px;\n      -webkit-app-region: no-drag;\n    }\n\n    .progress-bar {\n      height: 2px !important;\n\n      &:hover {\n        height: 3px !important;\n\n        .progress-track,\n        .progress-fill {\n          height: 3px !important;\n        }\n      }\n    }\n  }\n}\n\n.mini-bar-container {\n  @apply flex items-center px-3 h-full relative;\n}\n\n.album-cover {\n  @apply flex-shrink-0 mr-3 cursor-pointer;\n  width: 40px;\n  height: 40px;\n\n  .cover-img {\n    @apply w-full h-full rounded-md object-cover pointer-events-none;\n  }\n}\n\n.song-info {\n  @apply flex flex-col justify-center min-w-0 flex-shrink mr-4 cursor-pointer;\n  width: 200px;\n\n  .song-title {\n    @apply text-sm font-medium truncate;\n    color: var(--text-color-1, #000);\n  }\n\n  .song-artist {\n    @apply text-xs truncate mt-0.5 opacity-60;\n    color: var(--text-color-2, #666);\n  }\n}\n\n.control-buttons {\n  @apply flex items-center space-x-1 mx-4;\n}\n\n.control-button {\n  @apply flex items-center justify-center rounded-full transition-all duration-200 border-0 bg-transparent cursor-pointer text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200;\n  width: 32px;\n  height: 32px;\n\n  &:hover {\n    @apply bg-gray-100 dark:bg-dark-300;\n  }\n\n  &.play {\n    @apply bg-primary text-white;\n    &:hover {\n      @apply bg-green-800;\n    }\n  }\n\n  .iconfont {\n    @apply text-lg;\n  }\n}\n\n.function-buttons {\n  @apply flex items-center ml-auto space-x-2;\n}\n\n.function-button {\n  @apply flex items-center justify-center rounded-full transition-all duration-200 border-0 bg-transparent cursor-pointer text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200;\n  width: 32px;\n  height: 32px;\n\n  &:hover {\n    @apply bg-gray-100 dark:bg-dark-300;\n    color: var(--text-color-1, #000);\n  }\n\n  .iconfont {\n    @apply text-lg;\n  }\n}\n\n.close-button {\n  @apply flex items-center justify-center rounded-full transition-all duration-200 border-0 bg-transparent cursor-pointer ml-2;\n  width: 32px;\n  height: 32px;\n  color: var(--text-color-2, #666);\n\n  &:hover {\n    @apply bg-gray-100 dark:bg-dark-300;\n    color: var(--text-color-1, #000);\n  }\n}\n\n.progress-bar {\n  @apply relative w-full cursor-pointer;\n  height: 2px;\n\n  &:hover {\n    height: 4px;\n\n    .progress-track,\n    .progress-fill {\n      height: 4px;\n    }\n  }\n}\n\n.progress-track {\n  @apply absolute inset-x-0 bottom-0 transition-all duration-200;\n  height: 2px;\n  background: rgba(0, 0, 0, 0.1);\n\n  .dark & {\n    background: rgba(255, 255, 255, 0.15);\n  }\n}\n\n.progress-fill {\n  @apply absolute bottom-0 left-0 transition-all duration-200;\n  height: 2px;\n  background: var(--primary-color, #18a058);\n}\n\n.like-active {\n  @apply text-red-500 hover:text-red-600 !important;\n}\n\n.volume-slider-wrapper {\n  @apply p-2 py-4 rounded-xl bg-white dark:bg-dark-100 shadow-lg bg-opacity-90 backdrop-blur;\n  height: 160px;\n\n  :deep(.n-slider) {\n    --n-rail-height: 4px;\n    --n-rail-color: theme('colors.gray.200');\n    --n-rail-color-dark: theme('colors.gray.700');\n    --n-fill-color: theme('colors.green.500');\n    --n-handle-size: 12px;\n    --n-handle-color: theme('colors.green.500');\n\n    &.n-slider--vertical {\n      height: 100%;\n\n      .n-slider-rail {\n        width: 4px;\n      }\n\n      &:hover {\n        .n-slider-rail {\n          width: 6px;\n        }\n\n        .n-slider-handle {\n          width: 14px;\n          height: 14px;\n        }\n      }\n    }\n\n    .n-slider-rail {\n      @apply overflow-hidden transition-all duration-200;\n      @apply bg-gray-500 dark:bg-dark-300 bg-opacity-10 !important;\n    }\n\n    .n-slider-handle {\n      @apply transition-all duration-200;\n      opacity: 0;\n    }\n\n    &:hover {\n      .n-slider-handle {\n        opacity: 1;\n      }\n    }\n  }\n}\n\n// 播放列表样式\n.playlist-container {\n  @apply fixed left-0 right-0 bg-white dark:bg-dark-100 overflow-hidden;\n  top: 64px;\n  height: 330px;\n  max-height: 330px;\n\n  &.mini-mode-list {\n    width: 340px;\n    @apply bg-opacity-90 dark:bg-opacity-90;\n  }\n}\n\n// 播放列表内容样式\n.music-play-list-content {\n  @apply px-2 py-1;\n\n  .delete-btn {\n    @apply p-2 rounded-full transition-colors duration-200 cursor-pointer;\n    @apply hover:bg-red-50 dark:hover:bg-red-900/20;\n\n    .iconfont {\n      @apply text-lg;\n    }\n  }\n}\n\n// 过渡动画\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n\n.playlist-scrollbar {\n  height: 100%;\n}\n\n.playlist-items {\n  padding: 4px 0;\n}\n\n.dark {\n  .song-info {\n    .song-title {\n      color: var(--text-color-1, #fff);\n    }\n\n    .song-artist {\n      color: var(--text-color-2, #fff);\n    }\n  }\n}\n\n:deep(.n-popover) {\n  background-color: transparent !important;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/player/MobilePlayBar.vue",
    "content": "<template>\n  <div\n    ref=\"playBarRef\"\n    class=\"mobile-play-bar\"\n    :class=\"[\n      setAnimationClass('animate__fadeInUp'),\n      playerStore.musicFull ? 'play-bar-expanded' : 'play-bar-mini',\n      shouldShowMobileMenu ? 'is-menu-show' : 'is-menu-hide'\n    ]\"\n    :style=\"{\n      color: playerStore.musicFull\n        ? textColors.theme === 'dark'\n          ? '#ffffff'\n          : '#ffffff'\n        : settingsStore.theme === 'dark'\n          ? '#ffffff'\n          : '#000000'\n    }\"\n  >\n    <!-- Mini模式 - 在musicFullVisible为false时显示 -->\n    <div v-if=\"!playerStore.musicFull\" class=\"mobile-mini-controls\">\n      <!-- 歌曲信息 -->\n      <div class=\"mini-song-info\" @click=\"setMusicFull\">\n        <n-image\n          :src=\"getImgUrl(playMusic?.picUrl, '100y100')\"\n          class=\"mini-song-cover\"\n          lazy\n          preview-disabled\n        />\n        <div class=\"mini-song-text\">\n          <n-ellipsis line-clamp=\"1\">\n            <span class=\"mini-song-title\">{{ playMusic.name }}</span>\n            <span class=\"mx-2 text-gray-500 dark:text-gray-400\">-</span>\n            <span\n              class=\"mini-song-artist\"\n              v-for=\"(artists, artistsindex) in artistList\"\n              :key=\"artistsindex\"\n            >\n              {{ artists.name }}{{ artistsindex < artistList.length - 1 ? ' / ' : '' }}\n            </span>\n          </n-ellipsis>\n        </div>\n      </div>\n\n      <div class=\"mini-playback-controls\">\n        <div class=\"mini-control-btn play\" @click=\"playMusicEvent\">\n          <i class=\"iconfont icon\" :class=\"play ? 'icon-stop' : 'icon-play'\"></i>\n        </div>\n        <i class=\"iconfont icon-list mini-list-icon\" @click=\"openPlayListDrawer\"></i>\n      </div>\n    </div>\n\n    <!-- 全屏播放器 -->\n    <music-full-wrapper\n      ref=\"MusicFullRef\"\n      v-model=\"playerStore.musicFull\"\n      :background=\"background\"\n    />\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useSwipe } from '@vueuse/core';\nimport type { Ref } from 'vue';\nimport { computed, inject, onMounted, ref, watch } from 'vue';\n\nimport MusicFullWrapper from '@/components/lyric/MusicFullWrapper.vue';\nimport { artistList, playMusic, textColors } from '@/hooks/MusicHook';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { useSettingsStore } from '@/store/modules/settings';\nimport { getImgUrl, setAnimationClass } from '@/utils';\n\nconst shouldShowMobileMenu = inject('shouldShowMobileMenu') as Ref<boolean>;\n\nconst playerStore = usePlayerStore();\nconst settingsStore = useSettingsStore();\n\n// 是否播放\nconst play = computed(() => playerStore.isPlay);\n// 背景颜色\nconst background = ref('#000');\n\n// 播放控制\nfunction handleNext() {\n  playerStore.nextPlay();\n}\n\nfunction handlePrev() {\n  playerStore.prevPlay();\n}\n\n// 全屏播放器\nconst MusicFullRef = ref<any>(null);\n\n// 设置musicFull\nconst setMusicFull = () => {\n  playerStore.setMusicFull(!playerStore.musicFull);\n  if (playerStore.musicFull) {\n    settingsStore.showArtistDrawer = false;\n  }\n};\n\nwatch(\n  () => playerStore.musicFull,\n  (_newVal) => {\n    // 状态栏样式更新已在 Web 环境下禁用\n  }\n);\n\n// 打开播放列表抽屉\nconst openPlayListDrawer = () => {\n  playerStore.setPlayListDrawerVisible(true);\n};\n\n// 播放暂停按钮事件\nconst playMusicEvent = async () => {\n  try {\n    playerStore.setPlay(playMusic.value);\n  } catch (error) {\n    console.error('播放出错:', error);\n    playerStore.nextPlay();\n  }\n};\n\n// 滑动切歌\nconst playBarRef = ref<HTMLElement | null>(null);\nonMounted(() => {\n  if (playBarRef.value) {\n    const { direction } = useSwipe(playBarRef, {\n      onSwipeEnd: () => {\n        if (direction.value === 'left') handleNext();\n        if (direction.value === 'right') handlePrev();\n      },\n      threshold: 30\n    });\n  }\n});\n\nwatch(\n  () => playerStore.playMusic,\n  async () => {\n    background.value = playMusic.value.backgroundColor as string;\n  },\n  { immediate: true, deep: true }\n);\n</script>\n\n<style lang=\"scss\" scoped>\n.mobile-play-bar {\n  @apply fixed bottom-[76px] left-0 w-full flex flex-col;\n  z-index: 10000;\n  animation-duration: 0.3s !important;\n  transition: all 0.3s ease;\n\n  &.is-menu-show {\n    bottom: calc(var(--safe-area-inset-bottom, 0) + 66px);\n  }\n  &.is-menu-hide {\n    bottom: calc(var(--safe-area-inset-bottom, 0) + 10px);\n  }\n\n  &.play-bar-expanded {\n    @apply bg-transparent;\n    height: auto; /* 自动适应内容高度 */\n    max-height: 230px; /* 限制最大高度 */\n    background: linear-gradient(\n      to bottom,\n      rgba(0, 0, 0, 0) 0%,\n      rgba(0, 0, 0, 0.5) 20%,\n      rgba(0, 0, 0, 0.8) 80%,\n      rgba(0, 0, 0, 0.9) 100%\n    );\n  }\n\n  &.play-bar-mini {\n    @apply h-14 py-0;\n  }\n\n  // 进度条\n  .music-progress-bar {\n    @apply flex items-center justify-between px-4 py-2 relative z-10;\n\n    .current-time,\n    .total-time {\n      @apply text-xs text-white opacity-80;\n    }\n\n    .progress-wrapper {\n      @apply flex-1 mx-3 flex flex-col items-center;\n\n      .progress-slider {\n        @apply w-full;\n\n        :deep(.n-slider) {\n          --n-rail-height: 3px;\n          --n-rail-color: rgba(255, 255, 255, 0.15);\n          --n-rail-color-dark: rgba(255, 255, 255, 0.15);\n          --n-fill-color: #1ed760; /* Spotify绿色，可调整为其他绿色 */\n          --n-handle-size: 0px; /* 隐藏滑块 */\n          --n-handle-color: #1ed760;\n\n          &:hover {\n            --n-handle-size: 10px; /* 鼠标悬停时显示滑块 */\n          }\n\n          .n-slider-rail {\n            @apply rounded-full !important; /* 圆角进度条 */\n          }\n\n          .n-slider-fill {\n            @apply rounded-full !important;\n            box-shadow: 0 0 4px rgba(30, 215, 96, 0.5); /* 发光效果 */\n          }\n\n          .n-slider-handle {\n            @apply transition-all duration-200;\n            opacity: 0;\n            box-shadow: 0 0 4px rgba(255, 255, 255, 0.7);\n          }\n\n          &:hover .n-slider-handle,\n          &:active .n-slider-handle {\n            opacity: 1;\n          }\n        }\n      }\n\n      .quality-label {\n        @apply text-xs text-white opacity-70 mt-1;\n      }\n    }\n  }\n\n  // 主控制区\n  .player-controls {\n    @apply flex items-center justify-between px-8 py-3 relative z-10 pb-8;\n\n    .control-btn {\n      @apply flex items-center justify-center cursor-pointer transition;\n\n      i {\n        @apply text-white transition-all;\n      }\n\n      &.like i {\n        @apply text-2xl;\n      }\n\n      &.prev i,\n      &.next i {\n        @apply text-3xl;\n      }\n\n      &.play-pause {\n        @apply w-12 h-12 rounded-full flex items-center justify-center;\n        background: rgba(255, 255, 255, 0.2);\n\n        i {\n          @apply text-4xl;\n        }\n      }\n\n      &.list i {\n        @apply text-2xl;\n      }\n\n      .like-active {\n        @apply text-red-500;\n      }\n    }\n  }\n\n  // Mini模式样式\n  .mobile-mini-controls {\n    @apply flex items-center justify-between pr-4 mx-3 h-12 rounded-full bg-light-100 dark:bg-dark-100 shadow-lg;\n\n    .mini-song-info {\n      @apply flex items-center flex-1 min-w-0 cursor-pointer;\n\n      .mini-song-cover {\n        @apply w-12 h-12 rounded-full border-8 border-dark-300 dark:border-light-300;\n      }\n\n      .mini-song-text {\n        @apply ml-3 min-w-0 flex-1 flex items-center;\n\n        .mini-song-title {\n          @apply text-sm font-medium;\n        }\n\n        .mini-song-artist {\n          @apply text-xs text-gray-500 dark:text-gray-400;\n        }\n      }\n    }\n\n    .mini-playback-controls {\n      @apply flex items-center;\n\n      .mini-control-btn {\n        @apply flex items-center justify-center cursor-pointer transition;\n\n        &.play {\n          @apply w-9 h-9 rounded-full flex items-center justify-center mr-2;\n          @apply bg-gray-100 dark:bg-gray-800;\n\n          .iconfont {\n            @apply text-xl text-green-500 transition hover:text-green-600;\n          }\n        }\n      }\n\n      .mini-list-icon {\n        @apply text-xl p-1 transition cursor-pointer;\n        @apply hover:text-green-500;\n      }\n    }\n  }\n}\n\n.mobile-play-list-container {\n  height: 60vh;\n  width: 90vw;\n  max-width: 400px;\n  @apply relative rounded-t-2xl overflow-hidden;\n\n  .mobile-play-list-back {\n    backdrop-filter: blur(20px);\n    @apply absolute top-0 left-0 w-full h-full;\n    @apply bg-light dark:bg-black bg-opacity-90;\n  }\n\n  .mobile-play-list-item {\n    @apply px-3 py-1;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/player/MobilePlayerSettings.vue",
    "content": "<template>\n  <Teleport to=\"body\">\n    <Transition name=\"settings-drawer\">\n      <div\n        v-if=\"visible\"\n        class=\"fixed inset-0 z-[99999] flex items-end justify-center\"\n        @click.self=\"close\"\n      >\n        <!-- 遮罩层 -->\n        <div class=\"absolute inset-0 bg-black/50\" @click=\"close\"></div>\n\n        <!-- 弹窗内容 - 磨砂玻璃效果 -->\n        <div\n          class=\"relative w-full max-w-lg bg-gray-900/70 backdrop-blur-2xl rounded-t-3xl overflow-hidden max-h-[85vh] flex flex-col border-t border-white/10 shadow-2xl\"\n        >\n          <!-- 顶部拖拽条 -->\n          <div class=\"flex justify-center pt-3 pb-2 flex-shrink-0\">\n            <div class=\"w-10 h-1 rounded-full bg-white/30\"></div>\n          </div>\n\n          <!-- 标题栏 -->\n          <div class=\"flex items-center justify-between px-5 pb-4 flex-shrink-0\">\n            <h2 class=\"text-lg font-semibold text-white\">\n              {{ t('player.settings.title') }}\n            </h2>\n            <button\n              @click=\"close\"\n              class=\"w-8 h-8 rounded-full flex items-center justify-center text-white/60 hover:bg-white/10\"\n            >\n              <i class=\"ri-close-line text-xl\"></i>\n            </button>\n          </div>\n\n          <!-- 内容区域 -->\n          <div\n            class=\"flex-1 overflow-y-auto px-5 pb-6\"\n            :style=\"{ paddingBottom: `calc(24px + var(--safe-area-inset-bottom, 0px))` }\"\n          >\n            <!-- 播放速度 -->\n            <div class=\"mb-6\">\n              <div class=\"flex items-center justify-between mb-3\">\n                <span class=\"text-sm font-medium text-white/80\">\n                  {{ t('player.settings.playbackSpeed') }}\n                </span>\n                <span class=\"text-sm text-green-400 font-medium\">{{ playbackRate }}x</span>\n              </div>\n              <div class=\"flex flex-wrap gap-2\">\n                <button\n                  v-for=\"option in speedOptions\"\n                  :key=\"option\"\n                  @click=\"setSpeed(option)\"\n                  class=\"px-4 py-2 rounded-full text-sm font-medium transition-colors\"\n                  :class=\"\n                    playbackRate === option\n                      ? 'bg-green-500 text-white'\n                      : 'bg-white/10 text-white/70 hover:bg-white/15'\n                  \"\n                >\n                  {{ option }}x\n                </button>\n              </div>\n            </div>\n\n            <!-- 分隔线 -->\n            <div class=\"h-px bg-white/10 my-5\"></div>\n\n            <!-- 定时关闭 -->\n            <div>\n              <div class=\"flex items-center justify-between mb-3\">\n                <span class=\"text-sm font-medium text-white/80\">\n                  {{ t('player.sleepTimer.title') }}\n                </span>\n                <span v-if=\"hasTimerActive\" class=\"text-sm text-green-400 font-medium\">\n                  {{ timerStatusText }}\n                </span>\n              </div>\n\n              <!-- 已激活状态 -->\n              <div v-if=\"hasTimerActive\" class=\"space-y-3\">\n                <div class=\"p-4 rounded-2xl bg-green-500/15 border border-green-500/30\">\n                  <div class=\"flex items-center justify-between\">\n                    <div class=\"flex items-center gap-3\">\n                      <i class=\"ri-timer-line text-green-400 text-xl\"></i>\n                      <span class=\"text-green-400\">\n                        {{ timerDisplayText }}\n                      </span>\n                    </div>\n                    <button\n                      @click=\"cancelTimer\"\n                      class=\"px-3 py-1 rounded-full text-sm bg-red-500/20 text-red-400 hover:bg-red-500/30\"\n                    >\n                      {{ t('player.sleepTimer.cancel') }}\n                    </button>\n                  </div>\n                </div>\n              </div>\n\n              <!-- 未激活状态 - 设置选项 -->\n              <div v-else class=\"space-y-4\">\n                <!-- 按时间 -->\n                <div>\n                  <p class=\"text-xs text-white/50 mb-2\">\n                    {{ t('player.sleepTimer.timeMode') }}\n                  </p>\n                  <div class=\"flex flex-wrap gap-2\">\n                    <button\n                      v-for=\"minutes in [15, 30, 60, 90]\"\n                      :key=\"minutes\"\n                      @click=\"setTimeTimer(minutes)\"\n                      class=\"px-4 py-2 rounded-full text-sm font-medium bg-white/10 text-white/70 hover:bg-white/15\"\n                    >\n                      {{ minutes }}{{ t('player.sleepTimer.minutes') }}\n                    </button>\n                  </div>\n                  <!-- 自定义时间 -->\n                  <div class=\"flex items-center gap-2 mt-3\">\n                    <div class=\"flex items-center flex-1 bg-white/10 rounded-full overflow-hidden\">\n                      <button\n                        @click=\"decreaseMinutes\"\n                        class=\"w-10 h-10 flex items-center justify-center text-white/70 hover:bg-white/10 active:bg-white/20\"\n                      >\n                        <i class=\"ri-subtract-line text-lg\"></i>\n                      </button>\n                      <input\n                        v-model=\"customMinutes\"\n                        type=\"text\"\n                        inputmode=\"numeric\"\n                        pattern=\"[0-9]*\"\n                        placeholder=\"分钟\"\n                        class=\"flex-1 px-2 py-2 text-sm text-center bg-transparent text-white/80 border-0 outline-none placeholder-white/40\"\n                        @input=\"handleMinutesInput\"\n                      />\n                      <button\n                        @click=\"increaseMinutes\"\n                        class=\"w-10 h-10 flex items-center justify-center text-white/70 hover:bg-white/10 active:bg-white/20\"\n                      >\n                        <i class=\"ri-add-line text-lg\"></i>\n                      </button>\n                    </div>\n                    <button\n                      @click=\"setCustomTimeTimer\"\n                      :disabled=\"!customMinutes || Number(customMinutes) < 1\"\n                      class=\"px-4 py-2 rounded-full text-sm font-medium bg-green-500 text-white disabled:opacity-50 disabled:cursor-not-allowed\"\n                    >\n                      {{ t('player.sleepTimer.set') }}\n                    </button>\n                  </div>\n                </div>\n\n                <!-- 按歌曲数 -->\n                <div>\n                  <p class=\"text-xs text-white/50 mb-2\">\n                    {{ t('player.sleepTimer.songsMode') }}\n                  </p>\n                  <div class=\"flex flex-wrap gap-2\">\n                    <button\n                      v-for=\"songs in [1, 3, 5, 10]\"\n                      :key=\"songs\"\n                      @click=\"setSongsTimer(songs)\"\n                      class=\"px-4 py-2 rounded-full text-sm font-medium bg-white/10 text-white/70 hover:bg-white/15\"\n                    >\n                      {{ songs }}{{ t('player.sleepTimer.songs') }}\n                    </button>\n                  </div>\n                </div>\n\n                <!-- 播放列表结束 -->\n                <button\n                  @click=\"setPlaylistEndTimer\"\n                  class=\"w-full py-3 rounded-2xl text-sm font-medium bg-white/10 text-white/70 hover:bg-white/15\"\n                >\n                  {{ t('player.sleepTimer.playlistEnd') }}\n                </button>\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </Transition>\n  </Teleport>\n</template>\n\n<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia';\nimport { computed, onMounted, onUnmounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { usePlayerStore } from '@/store/modules/player';\n\nconst { t } = useI18n();\nconst playerStore = usePlayerStore();\nconst { sleepTimer, playbackRate } = storeToRefs(playerStore);\n\n// Props & Emits\ndefineProps<{\n  visible: boolean;\n}>();\n\nconst emit = defineEmits<{\n  (e: 'update:visible', value: boolean): void;\n}>();\n\n// 播放速度选项\nconst speedOptions = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];\n\n// 自定义时间\nconst customMinutes = ref<number | string>(30);\n\n// 定时器相关\nconst refreshTrigger = ref(0);\nlet timerInterval: number | null = null;\n\nconst hasTimerActive = computed(() => playerStore.hasSleepTimerActive);\n\nconst timerStatusText = computed(() => {\n  if (sleepTimer.value.type === 'time') return t('player.sleepTimer.activeTime');\n  if (sleepTimer.value.type === 'songs') return t('player.sleepTimer.activeSongs');\n  if (sleepTimer.value.type === 'end') return t('player.sleepTimer.activeEnd');\n  return '';\n});\n\nconst timerDisplayText = computed(() => {\n  void refreshTrigger.value;\n\n  if (sleepTimer.value.type === 'time' && sleepTimer.value.endTime) {\n    const remaining = Math.max(0, sleepTimer.value.endTime - Date.now());\n    const totalSeconds = Math.floor(remaining / 1000);\n    const hours = Math.floor(totalSeconds / 3600);\n    const minutes = Math.floor((totalSeconds % 3600) / 60);\n    const seconds = Math.floor(totalSeconds % 60);\n    if (hours > 0) {\n      return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;\n    }\n    return `${minutes}:${seconds.toString().padStart(2, '0')}`;\n  }\n\n  if (sleepTimer.value.type === 'songs') {\n    return t('player.sleepTimer.songsRemaining', { count: sleepTimer.value.remainingSongs || 0 });\n  }\n\n  if (sleepTimer.value.type === 'end') {\n    return t('player.sleepTimer.afterPlaylist');\n  }\n\n  return '';\n});\n\n// 方法\nconst close = () => {\n  emit('update:visible', false);\n};\n\nconst setSpeed = (speed: number) => {\n  playerStore.setPlaybackRate(speed);\n};\n\nconst setTimeTimer = (minutes: number) => {\n  playerStore.setSleepTimerByTime(minutes);\n};\n\nconst setCustomTimeTimer = () => {\n  const minutes =\n    typeof customMinutes.value === 'number'\n      ? customMinutes.value\n      : parseInt(String(customMinutes.value) || '0', 10);\n  if (minutes >= 1) {\n    playerStore.setSleepTimerByTime(minutes);\n    customMinutes.value = 30;\n  }\n};\n\nconst increaseMinutes = () => {\n  const current = Number(customMinutes.value) || 0;\n  customMinutes.value = Math.min(300, current + 1);\n};\n\nconst decreaseMinutes = () => {\n  const current = Number(customMinutes.value) || 0;\n  customMinutes.value = Math.max(1, current - 1);\n};\n\nconst handleMinutesInput = (e: Event) => {\n  const input = e.target as HTMLInputElement;\n  const value = input.value.replace(/[^0-9]/g, '');\n  if (value) {\n    customMinutes.value = Math.min(300, Math.max(1, parseInt(value, 10)));\n  } else {\n    customMinutes.value = '';\n  }\n};\n\nconst setSongsTimer = (songs: number) => {\n  playerStore.setSleepTimerBySongs(songs);\n};\n\nconst setPlaylistEndTimer = () => {\n  playerStore.setSleepTimerAtPlaylistEnd();\n};\n\nconst cancelTimer = () => {\n  playerStore.clearSleepTimer();\n};\n\n// 定时刷新倒计时\nconst startTimerUpdate = () => {\n  if (timerInterval) return;\n  timerInterval = window.setInterval(() => {\n    refreshTrigger.value = Date.now();\n  }, 500);\n};\n\nconst stopTimerUpdate = () => {\n  if (timerInterval) {\n    clearInterval(timerInterval);\n    timerInterval = null;\n  }\n};\n\nwatch(\n  () => [hasTimerActive.value, sleepTimer.value.type],\n  ([active, type]) => {\n    if (active && type === 'time') {\n      startTimerUpdate();\n    } else {\n      stopTimerUpdate();\n    }\n  },\n  { immediate: true }\n);\n\nonMounted(() => {\n  if (hasTimerActive.value && sleepTimer.value.type === 'time') {\n    startTimerUpdate();\n  }\n});\n\nonUnmounted(() => {\n  stopTimerUpdate();\n});\n</script>\n\n<style scoped>\n/* 弹窗动画 */\n.settings-drawer-enter-active,\n.settings-drawer-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.settings-drawer-enter-active > div:last-child,\n.settings-drawer-leave-active > div:last-child {\n  transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);\n}\n\n.settings-drawer-enter-from,\n.settings-drawer-leave-to {\n  opacity: 0;\n}\n\n.settings-drawer-enter-from > div:last-child,\n.settings-drawer-leave-to > div:last-child {\n  transform: translateY(100%);\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/player/PlayBar.vue",
    "content": "<template>\n  <div\n    class=\"music-play-bar\"\n    :class=\"[\n      setAnimationClass('animate__bounceInUp'),\n      musicFullVisible ? 'play-bar-opcity' : '',\n      musicFullVisible && MusicFullRef?.musicFullRef?.config?.hidePlayBar\n        ? 'animate__animated animate__slideOutDown'\n        : ''\n    ]\"\n    :style=\"{\n      color: musicFullVisible\n        ? textColors.theme === 'dark'\n          ? '#000000'\n          : '#ffffff'\n        : settingsStore.theme === 'dark'\n          ? '#ffffff'\n          : '#000000'\n    }\"\n  >\n    <div class=\"music-time custom-slider\">\n      <n-slider\n        v-model:value=\"timeSlider\"\n        :step=\"1\"\n        :max=\"allTime\"\n        :min=\"0\"\n        :format-tooltip=\"formatTooltip\"\n        :show-tooltip=\"showSliderTooltip\"\n        @mouseenter=\"showSliderTooltip = true\"\n        @mouseleave=\"showSliderTooltip = false\"\n        @dragstart=\"handleSliderDragStart\"\n        @dragend=\"handleSliderDragEnd\"\n      ></n-slider>\n    </div>\n    <div class=\"play-bar-img-wrapper\" @click=\"setMusicFull\">\n      <n-image\n        :src=\"getImgUrl(playMusic?.picUrl, '100y100')\"\n        class=\"play-bar-img\"\n        lazy\n        preview-disabled\n      />\n      <div v-if=\"playMusic?.playLoading\" class=\"loading-overlay\">\n        <i class=\"ri-loader-4-line loading-icon\"></i>\n      </div>\n      <div class=\"hover-arrow\">\n        <div class=\"hover-content\">\n          <!-- <i class=\"ri-arrow-up-s-line text-3xl\" :class=\"{ 'ri-arrow-down-s-line': musicFullVisible }\"></i> -->\n          <i\n            class=\"text-3xl\"\n            :class=\"musicFullVisible ? 'ri-arrow-down-s-line' : 'ri-arrow-up-s-line'\"\n          ></i>\n          <span class=\"hover-text\">{{\n            musicFullVisible ? t('player.playBar.collapse') : t('player.playBar.expand')\n          }}</span>\n        </div>\n      </div>\n    </div>\n    <div class=\"music-content\">\n      <div class=\"music-content-title flex items-center\">\n        <n-ellipsis class=\"text-ellipsis\" line-clamp=\"1\">\n          <p v-html=\"playMusic?.name || ''\"></p>\n        </n-ellipsis>\n        <span v-if=\"playbackRate !== 1.0\" class=\"playback-rate-badge\"> {{ playbackRate }}x </span>\n      </div>\n      <div class=\"music-content-name\">\n        <n-ellipsis\n          class=\"text-ellipsis\"\n          line-clamp=\"1\"\n          :tooltip=\"{\n            contentStyle: { maxWidth: '600px' },\n            zIndex: 99999\n          }\"\n        >\n          <span\n            v-for=\"(artists, artistsindex) in artistList\"\n            :key=\"artistsindex\"\n            class=\"cursor-pointer hover:text-green-500\"\n            @click=\"handleArtistClick(artists.id)\"\n          >\n            {{ artists.name }}{{ artistsindex < artistList.length - 1 ? ' / ' : '' }}\n          </span>\n        </n-ellipsis>\n      </div>\n    </div>\n    <div class=\"music-buttons\">\n      <div class=\"music-buttons-prev\" @click=\"handlePrev\">\n        <i class=\"iconfont icon-prev\"></i>\n      </div>\n      <div class=\"music-buttons-play\" @click=\"playMusicEvent\">\n        <i class=\"iconfont icon\" :class=\"play ? 'icon-stop' : 'icon-play'\"></i>\n      </div>\n      <div class=\"music-buttons-next\" @click=\"handleNext\">\n        <i class=\"iconfont icon-next\"></i>\n      </div>\n    </div>\n    <div class=\"audio-button\">\n      <div class=\"audio-volume custom-slider\" @wheel.prevent=\"handleVolumeWheel\">\n        <div class=\"volume-icon\" @click=\"mute\">\n          <i class=\"iconfont\" :class=\"getVolumeIcon\"></i>\n        </div>\n        <div class=\"volume-slider\">\n          <div class=\"volume-percentage\">{{ Math.round(volumeSlider) }}%</div>\n          <n-slider v-model:value=\"volumeSlider\" :step=\"0.01\" :tooltip=\"false\" vertical></n-slider>\n        </div>\n      </div>\n      <n-tooltip v-if=\"!isMobile\" trigger=\"hover\" :z-index=\"9999999\">\n        <template #trigger>\n          <i\n            class=\"iconfont\"\n            :class=\"[playModeIcon, { 'intelligence-active': playMode === 3 }]\"\n            @click=\"togglePlayMode\"\n          ></i>\n        </template>\n        {{ playModeText }}\n      </n-tooltip>\n      <n-tooltip v-if=\"!isMobile\" trigger=\"hover\" :z-index=\"9999999\">\n        <template #trigger>\n          <i\n            class=\"iconfont\"\n            :class=\"{\n              'like-active': isFavorite,\n              'ri-heart-3-fill': isFavorite,\n              'ri-heart-3-line': !isFavorite\n            }\"\n            @click=\"toggleFavorite\"\n          ></i>\n        </template>\n        {{ t('player.playBar.like') }}\n      </n-tooltip>\n      <n-tooltip v-if=\"isElectron\" class=\"music-lyric\" trigger=\"hover\" :z-index=\"9999999\">\n        <template #trigger>\n          <i\n            class=\"iconfont ri-netease-cloud-music-line\"\n            :class=\"{ 'text-green-500': isLyricWindowOpen, 'disabled-icon': !playMusic?.id }\"\n            @click=\"playMusic?.id && openLyricWindow()\"\n          ></i>\n        </template>\n        {{ playMusic?.id ? t('player.playBar.lyric') : t('player.playBar.noSongPlaying') }}\n      </n-tooltip>\n      <n-tooltip v-if=\"playMusic?.id && isElectron\" trigger=\"hover\" :z-index=\"9999999\">\n        <template #trigger>\n          <reparse-popover v-if=\"playMusic?.id\" />\n        </template>\n        {{ t('player.playBar.reparse') }}\n      </n-tooltip>\n\n      <!-- 高级控制菜单按钮（整合了 EQ、定时关闭、播放速度） -->\n      <advanced-controls-popover />\n\n      <n-tooltip trigger=\"hover\" :z-index=\"9999999\">\n        <template #trigger>\n          <i\n            class=\"iconfont icon-list text-2xl hover:text-green-500 transition-colors cursor-pointer\"\n            @click=\"openPlayListDrawer\"\n          ></i>\n        </template>\n        {{ t('player.playBar.playList') }}\n      </n-tooltip>\n    </div>\n    <!-- 全屏播放器 -->\n    <music-full-wrapper ref=\"MusicFullRef\" v-model=\"musicFullVisible\" :background=\"background\" />\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useThrottleFn } from '@vueuse/core';\nimport { useMessage } from 'naive-ui';\nimport { storeToRefs } from 'pinia';\nimport { computed, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport MusicFullWrapper from '@/components/lyric/MusicFullWrapper.vue';\nimport AdvancedControlsPopover from '@/components/player/AdvancedControlsPopover.vue';\nimport ReparsePopover from '@/components/player/ReparsePopover.vue';\nimport {\n  allTime,\n  artistList,\n  isLyricWindowOpen,\n  nowTime,\n  openLyric,\n  playMusic,\n  textColors\n} from '@/hooks/MusicHook';\nimport { useArtist } from '@/hooks/useArtist';\nimport { usePlayMode } from '@/hooks/usePlayMode';\nimport { audioService } from '@/services/audioService';\nimport { isBilibiliIdMatch, usePlayerStore } from '@/store/modules/player';\nimport { useSettingsStore } from '@/store/modules/settings';\nimport { getImgUrl, isElectron, isMobile, secondToMinute, setAnimationClass } from '@/utils';\n\nconst playerStore = usePlayerStore();\nconst settingsStore = useSettingsStore();\nconst { t } = useI18n();\nconst message = useMessage();\n// 是否播放\nconst play = computed(() => playerStore.isPlay);\n// 背景颜色\nconst background = ref('#000');\n\nwatch(\n  () => playerStore.playMusic,\n  async () => {\n    if (playMusic && playMusic.value && playMusic.value.backgroundColor) {\n      background.value = playMusic.value.backgroundColor as string;\n    }\n  },\n  { immediate: true, deep: true }\n);\n\n// 节流版本的 seek 函数\nconst throttledSeek = useThrottleFn((value: number) => {\n  audioService.seek(value);\n  nowTime.value = value;\n}, 50); // 50ms 的节流延迟\n\n// 拖动时的临时值，避免频繁更新 nowTime 触发重渲染\nconst dragValue = ref(0);\n\n// 为滑块拖动添加状态跟踪\nconst isDragging = ref(false);\n\n// 修改 timeSlider 计算属性\nconst timeSlider = computed({\n  get: () => (isDragging.value ? dragValue.value : nowTime.value),\n  set: (value) => {\n    if (isDragging.value) {\n      // 拖动中只更新临时值，不触发 nowTime 更新和 seek 操作\n      dragValue.value = value;\n      return;\n    }\n\n    // 点击操作 (非拖动)，可以直接 seek\n    throttledSeek(value);\n  }\n});\n\n// 添加滑块拖动开始和结束事件处理\nconst handleSliderDragStart = () => {\n  isDragging.value = true;\n  // 初始化拖动值为当前时间\n  dragValue.value = nowTime.value;\n};\n\nconst handleSliderDragEnd = () => {\n  isDragging.value = false;\n\n  // 直接应用最终的拖动值\n  audioService.seek(dragValue.value);\n  nowTime.value = dragValue.value;\n};\n\n// 格式化提示文本，根据拖动状态显示不同的时间\nconst formatTooltip = (value: number) => {\n  return `${secondToMinute(value)} / ${secondToMinute(allTime.value)}`;\n};\n\n// 音量条 - 使用 playerStore 的统一音量管理\nconst getVolumeIcon = computed(() => {\n  // 0 静音 ri-volume-mute-line 0.5 ri-volume-down-line 1 ri-volume-up-line\n  if (playerStore.volume === 0) {\n    return 'ri-volume-mute-line';\n  }\n  if (playerStore.volume <= 0.5) {\n    return 'ri-volume-down-line';\n  }\n  return 'ri-volume-up-line';\n});\n\nconst volumeSlider = computed({\n  get: () => playerStore.volume * 100,\n  set: (value) => {\n    playerStore.setVolume(value / 100);\n  }\n});\n\n// 静音\nconst mute = () => {\n  if (volumeSlider.value === 0) {\n    volumeSlider.value = 30;\n  } else {\n    volumeSlider.value = 0;\n  }\n};\n\n// 鼠标滚轮调整音量\nconst handleVolumeWheel = (e: WheelEvent) => {\n  // 向上滚动增加音量，向下滚动减少音量\n  const delta = e.deltaY < 0 ? 5 : -5;\n  const newValue = Math.min(Math.max(volumeSlider.value + delta, 0), 100);\n  volumeSlider.value = newValue;\n};\n\n// 播放模式\nconst { playMode, playModeIcon, playModeText, togglePlayMode } = usePlayMode();\n\n// 播放速度控制\nconst { playbackRate } = storeToRefs(playerStore);\n\nfunction handleNext() {\n  playerStore.nextPlay();\n}\n\nfunction handlePrev() {\n  playerStore.prevPlay();\n}\n\nconst MusicFullRef = ref<any>(null);\nconst showSliderTooltip = ref(false);\n\n// 播放暂停按钮事件\nconst playMusicEvent = async () => {\n  try {\n    const result = await playerStore.setPlay({ ...playMusic.value });\n    if (result) {\n      playerStore.setPlayMusic(true);\n    }\n  } catch (error) {\n    console.error('重新获取播放链接失败:', error);\n    message.error(t('player.playFailed'));\n  }\n};\n\nconst musicFullVisible = computed({\n  get: () => playerStore.musicFull,\n  set: (value) => {\n    playerStore.setMusicFull(value);\n  }\n});\n\n// 设置musicFull\nconst setMusicFull = () => {\n  musicFullVisible.value = !musicFullVisible.value;\n  playerStore.setMusicFull(musicFullVisible.value);\n  if (musicFullVisible.value) {\n    settingsStore.showArtistDrawer = false;\n  }\n};\n\nconst isFavorite = computed(() => {\n  if (!playMusic || !playMusic.value) return false;\n  // 对于B站视频，使用ID匹配函数\n  if (playMusic.value.source === 'bilibili' && playMusic.value.bilibiliData?.bvid) {\n    return playerStore.favoriteList.some((id) => isBilibiliIdMatch(id, playMusic.value.id));\n  }\n\n  // 非B站视频直接比较ID\n  return playerStore.favoriteList.includes(playMusic.value.id);\n});\n\nconst toggleFavorite = async (e: Event) => {\n  console.log('playMusic.value', playMusic.value);\n  e.stopPropagation();\n\n  // 处理B站视频的收藏ID\n  let favoriteId = playMusic.value.id;\n  if (playMusic.value.source === 'bilibili' && playMusic.value.bilibiliData?.bvid) {\n    // 如果当前播放的是B站视频，且已有ID不包含--格式，则需要构造完整ID\n    if (!String(favoriteId).includes('--')) {\n      favoriteId = `${playMusic.value.bilibiliData.bvid}--${playMusic.value.song?.ar?.[0]?.id || 0}--${playMusic.value.bilibiliData.cid}`;\n    }\n  }\n\n  if (isFavorite.value) {\n    playerStore.removeFromFavorite(favoriteId);\n  } else {\n    playerStore.addToFavorite(favoriteId);\n  }\n};\n\nconst openLyricWindow = () => {\n  openLyric();\n};\n\nconst { navigateToArtist } = useArtist();\n\nconst handleArtistClick = (id: number) => {\n  musicFullVisible.value = false;\n  navigateToArtist(id);\n};\n\n// 打开播放列表抽屉\nconst openPlayListDrawer = () => {\n  playerStore.setPlayListDrawerVisible(true);\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.text-ellipsis {\n  width: 100%;\n}\n\n.music-play-bar {\n  @apply h-20 w-full absolute bottom-0 left-0 flex items-center box-border px-6 py-2 pt-3;\n  @apply bg-light dark:bg-dark shadow-2xl shadow-gray-300;\n  z-index: 9999;\n  animation-duration: 0.5s !important;\n\n  &.play-bar-opcity {\n    @apply bg-transparent !important;\n    box-shadow: 0 0 20px 5px #0000001d;\n  }\n\n  &.animate__slideOutDown {\n    animation-duration: 0.3s !important;\n    pointer-events: none;\n  }\n\n  .music-content {\n    width: 200px;\n    @apply ml-4;\n\n    &-title {\n      @apply text-base;\n    }\n\n    &-name {\n      @apply text-xs mt-1 opacity-80;\n    }\n  }\n}\n\n.play-bar-img {\n  @apply w-14 h-14 rounded-2xl;\n}\n\n.music-buttons {\n  @apply mx-6 flex-1 flex justify-center;\n\n  .iconfont {\n    @apply text-2xl transition;\n    @apply hover:text-green-500;\n  }\n\n  .icon {\n    @apply text-3xl;\n    @apply hover:text-green-500;\n  }\n\n  @apply flex items-center;\n\n  > div {\n    @apply cursor-pointer;\n  }\n\n  &-play {\n    @apply flex justify-center items-center w-20 h-12 rounded-full mx-4 transition text-gray-500;\n    @apply bg-gray-100 bg-opacity-60 dark:bg-gray-800 dark:bg-opacity-60 hover:bg-gray-200;\n  }\n}\n\n.audio-volume {\n  @apply flex items-center relative;\n  &:hover {\n    .volume-slider {\n      @apply opacity-100 visible;\n    }\n  }\n  .volume-icon {\n    @apply cursor-pointer;\n  }\n\n  .iconfont {\n    @apply text-2xl transition;\n    @apply hover:text-green-500;\n  }\n\n  .volume-slider {\n    @apply absolute opacity-0 invisible transition-all duration-300 bottom-[30px] left-1/2 -translate-x-1/2 h-[180px] px-2 py-4 rounded-xl;\n    @apply bg-light dark:bg-dark-200;\n    @apply border border-gray-200 dark:border-gray-700;\n\n    .volume-percentage {\n      @apply absolute -top-6 left-1/2 -translate-x-1/2 text-xs font-medium bg-light dark:bg-dark-200 px-2 py-1 rounded-md;\n      @apply border border-gray-200 dark:border-gray-700;\n      @apply text-gray-800 dark:text-white;\n      white-space: nowrap;\n    }\n  }\n}\n\n.audio-button {\n  @apply flex items-center;\n\n  .iconfont {\n    @apply text-2xl transition cursor-pointer mx-3;\n    @apply hover:text-green-500;\n  }\n}\n\n.music-play {\n  &-list {\n    height: 50vh;\n    width: 300px;\n    @apply relative rounded-3xl overflow-hidden py-2;\n    &-back {\n      backdrop-filter: blur(20px);\n      @apply absolute top-0 left-0 w-full h-full;\n      @apply bg-light dark:bg-black bg-opacity-75;\n    }\n    &-content {\n      @apply mx-2;\n    }\n  }\n}\n\n.mobile {\n  .music-play-bar {\n    @apply px-4 bottom-[56px] transition-all duration-300;\n  }\n  .music-time {\n    display: none;\n  }\n  .ri-netease-cloud-music-line {\n    display: none;\n  }\n  .audio-volume {\n    display: none;\n  }\n  .audio-button {\n    @apply mx-0;\n  }\n  .music-buttons {\n    @apply m-0;\n    &-prev,\n    &-next {\n      display: none;\n    }\n    &-play {\n      @apply m-0;\n    }\n  }\n  .music-content {\n    flex: 1;\n  }\n}\n\n// 自定义滑块样式\n.custom-slider {\n  :deep(.n-slider) {\n    --n-rail-height: 4px;\n    --n-rail-color: theme('colors.gray.200');\n    --n-rail-color-dark: theme('colors.gray.700');\n    --n-fill-color: theme('colors.green.500');\n    --n-handle-size: 12px;\n    --n-handle-color: theme('colors.green.500');\n\n    &.n-slider--vertical {\n      height: 100%;\n\n      .n-slider-rail {\n        width: 4px;\n      }\n\n      &:hover {\n        .n-slider-rail {\n          width: 6px;\n        }\n\n        .n-slider-handle {\n          width: 14px;\n          height: 14px;\n        }\n      }\n    }\n\n    .n-slider-rail {\n      @apply overflow-hidden transition-all duration-200;\n      @apply bg-gray-500 dark:bg-dark-300 bg-opacity-10 !important;\n    }\n\n    .n-slider-handle {\n      @apply transition-all duration-200;\n      opacity: 0;\n    }\n\n    &:hover {\n      .n-slider-handle {\n        opacity: 1;\n      }\n    }\n\n    // 确保悬停时提示样式正确\n    .n-slider-tooltip {\n      @apply bg-dark-200 text-white text-xs py-1 px-2 rounded;\n      z-index: 999999;\n    }\n  }\n}\n\n.play-bar-img-wrapper {\n  @apply relative cursor-pointer w-14 h-14;\n\n  .hover-arrow {\n    @apply absolute inset-0 flex items-center justify-center opacity-0 transition-opacity duration-300 rounded-2xl;\n    background: rgba(0, 0, 0, 0.5);\n\n    .hover-content {\n      @apply flex flex-col items-center justify-center;\n\n      i {\n        @apply text-white mb-0.5;\n      }\n\n      .hover-text {\n        @apply text-white text-xs scale-90;\n      }\n    }\n  }\n\n  &:hover {\n    .hover-arrow {\n      @apply opacity-100;\n    }\n  }\n}\n\n.tooltip-content {\n  @apply text-sm py-1 px-2;\n}\n\n.play-bar-img {\n  @apply w-14 h-14 rounded-2xl;\n}\n\n.like-active {\n  @apply text-red-500 hover:text-red-600 !important;\n}\n\n.intelligence-active {\n  @apply text-green-500 hover:text-green-600 !important;\n}\n\n.disabled-icon {\n  @apply opacity-50 cursor-not-allowed !important;\n  &:hover {\n    @apply text-inherit !important;\n  }\n}\n\n.icon-loop,\n.icon-single-loop {\n  font-size: 1.5rem;\n}\n\n.music-time .n-slider {\n  position: absolute;\n  top: 0;\n  left: 0;\n  padding: 0;\n  border-radius: 0;\n}\n\n.music-eq {\n  @apply p-4 rounded-3xl;\n  backdrop-filter: blur(20px);\n  @apply bg-light dark:bg-black bg-opacity-75;\n}\n\n.music-play-list-content {\n  @apply mx-2;\n\n  .delete-btn {\n    @apply p-2 rounded-full transition-colors duration-200 cursor-pointer;\n    @apply hover:bg-red-50 dark:hover:bg-red-900/20;\n\n    .iconfont {\n      @apply text-lg;\n    }\n  }\n}\n\n@keyframes spin {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n.loading-overlay {\n  @apply absolute inset-0 flex items-center justify-center rounded-2xl;\n  background-color: rgba(0, 0, 0, 0.5);\n  z-index: 2;\n}\n\n.loading-icon {\n  font-size: 24px;\n  color: white;\n  animation: spin 1s linear infinite;\n}\n\n.play-speed {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n  padding: 0 8px;\n}\n\n.speed-button {\n  font-size: 14px;\n  color: var(--text-color);\n  padding: 4px 8px;\n  border-radius: 4px;\n  background: var(--hover-color);\n}\n\n.speed-button:hover {\n  background: var(--hover-color-dark);\n}\n\n.playback-rate-badge {\n  @apply ml-2 px-1.5 h-4 flex items-center text-xs rounded bg-green-500 bg-opacity-15 text-green-600 dark:text-green-400;\n  font-weight: 500;\n  vertical-align: 1px;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/player/PlayingListDrawer.vue",
    "content": "<template>\n  <!-- 透明遮罩层，点击任意位置关闭 -->\n  <div v-if=\"internalVisible\" class=\"fixed-overlay\" @click=\"closePanel\"></div>\n\n  <!-- 使用animate.css进行动画效果 -->\n  <div\n    v-if=\"internalVisible\"\n    class=\"playlist-panel\"\n    :class=\"[\n      'animate__animated',\n      closing\n        ? isMobile\n          ? 'animate__slideOutDown'\n          : 'animate__slideOutRight'\n        : isMobile\n          ? 'animate__slideInUp'\n          : 'animate__slideInRight'\n    ]\"\n  >\n    <div class=\"playlist-panel-header\">\n      <div class=\"title\">{{ t('player.playBar.playList') }}</div>\n      <div class=\"header-actions\">\n        <n-tooltip trigger=\"hover\">\n          <template #trigger>\n            <div class=\"action-btn\" @click=\"handleClearPlaylist\">\n              <i class=\"iconfont ri-delete-bin-line\"></i>\n            </div>\n          </template>\n          {{ t('player.playList.clearAll') }}\n        </n-tooltip>\n        <div class=\"close-btn\" @click=\"closePanel\">\n          <i class=\"iconfont ri-close-line\"></i>\n        </div>\n      </div>\n    </div>\n    <div class=\"playlist-panel-content\">\n      <div v-if=\"playList.length === 0\" class=\"empty-playlist\">\n        <i class=\"iconfont ri-music-2-line\"></i>\n        <p>{{ t('player.playList.empty') }}</p>\n      </div>\n      <n-virtual-list v-else ref=\"playListRef\" :item-size=\"62\" item-resizable :items=\"playList\">\n        <template #default=\"{ item }\">\n          <div class=\"music-play-list-content\">\n            <div class=\"flex items-center justify-between\">\n              <song-item :key=\"item.id\" class=\"flex-1\" :item=\"item\" mini></song-item>\n              <div class=\"delete-btn\" @click.stop=\"handleDeleteSong(item)\">\n                <i\n                  class=\"iconfont ri-delete-bin-line text-gray-400 hover:text-red-500 transition-colors\"\n                ></i>\n              </div>\n            </div>\n          </div>\n        </template>\n      </n-virtual-list>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useDialog, useMessage } from 'naive-ui';\nimport { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport SongItem from '@/components/common/SongItem.vue';\nimport { usePlayerStore } from '@/store/modules/player';\nimport type { SongResult } from '@/types/music';\nimport { isMobile } from '@/utils';\n\nconst { t } = useI18n();\nconst message = useMessage();\nconst dialog = useDialog();\nconst playerStore = usePlayerStore();\n\n// 内部状态控制组件的可见性\nconst internalVisible = ref(false);\nconst closing = ref(false);\n\n// 当前是否显示播放列表面板\nconst show = computed({\n  get: () => playerStore.playListDrawerVisible,\n  set: (value) => {\n    playerStore.setPlayListDrawerVisible(value);\n  }\n});\n\n// 监听外部可见性变化\nwatch(\n  show,\n  (newValue) => {\n    if (newValue) {\n      // 打开面板\n      internalVisible.value = true;\n      closing.value = false;\n      // 在下一个渲染周期后滚动到当前歌曲\n      nextTick(() => {\n        scrollToCurrentSong();\n      });\n    } else {\n      // 如果已经是关闭状态，不需要处理\n      if (!internalVisible.value) return;\n\n      // 开始关闭动画\n      closing.value = true;\n      // 等待动画完成后再隐藏组件\n      setTimeout(() => {\n        internalVisible.value = false;\n      }, 400); // 动画持续时间\n    }\n  },\n  { immediate: true }\n);\n\n// 播放列表\nconst playList = computed(() => playerStore.playList as SongResult[]);\n\n// 播放列表引用\nconst playListRef = ref<any>(null);\n\n// 关闭面板\nconst closePanel = () => {\n  show.value = false;\n};\n\n// 清空播放列表\nconst handleClearPlaylist = () => {\n  if (playList.value.length === 0) {\n    message.info(t('player.playList.alreadyEmpty'));\n    return;\n  }\n\n  if (isMobile.value) {\n    closePanel();\n  }\n\n  dialog.warning({\n    title: t('player.playList.clearConfirmTitle'),\n    content: t('player.playList.clearConfirmContent'),\n    positiveText: t('common.confirm'),\n    negativeText: t('common.cancel'),\n    style: { zIndex: 999999999 }, // 确保对话框显示在遮罩之上\n    onPositiveClick: () => {\n      // 清空播放列表\n      playerStore.clearPlayAll();\n      message.success(t('player.playList.cleared'));\n    }\n  });\n};\n\n// 处理键盘事件\nconst handleKeyDown = (event: KeyboardEvent) => {\n  if (event.key === 'Escape' && internalVisible.value) {\n    closePanel();\n  }\n};\n\n// 添加和移除键盘事件监听\nonMounted(() => {\n  window.addEventListener('keydown', handleKeyDown);\n});\n\nonUnmounted(() => {\n  window.removeEventListener('keydown', handleKeyDown);\n});\n\n// 滚动到当前播放歌曲\nconst scrollToCurrentSong = () => {\n  // 延长等待时间，确保列表已渲染完成\n  setTimeout(() => {\n    if (playListRef.value && playList.value.length > 0) {\n      const index = playerStore.playListIndex;\n      console.log('滚动到歌曲索引:', index);\n      playListRef.value.scrollTo({\n        top: (index > 3 ? index - 3 : 0) * 62\n      });\n    }\n  }, 100);\n};\n\n// 删除歌曲\nconst handleDeleteSong = (song: SongResult) => {\n  playerStore.removeFromPlayList(song.id as number);\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.fixed-overlay {\n  @apply fixed inset-0 z-[999999];\n  pointer-events: auto; // 允许点击关闭\n  cursor: default;\n}\n\n.playlist-panel {\n  @apply fixed right-0 z-[9999999] rounded-l-xl overflow-hidden;\n  width: 350px;\n  height: 70vh;\n  top: 15vh; // 距离顶部15%\n  animation-duration: 0.4s !important; // 动画持续时间\n\n  @apply bg-light dark:bg-dark shadow-2xl dark:border dark:border-gray-700;\n\n  &-header {\n    @apply flex items-center justify-between px-4 py-2 border-b border-gray-100 dark:border-gray-900;\n    backdrop-filter: blur(10px);\n    background-color: rgba(255, 255, 255, 0.7);\n\n    .dark & {\n      background-color: rgba(18, 18, 18, 0.7);\n    }\n\n    .title {\n      @apply text-base font-medium text-gray-800 dark:text-gray-200;\n    }\n\n    .header-actions {\n      @apply flex items-center;\n    }\n\n    .action-btn,\n    .close-btn {\n      @apply w-8 h-8 flex items-center justify-center rounded-full cursor-pointer mx-1 text-gray-800 dark:text-gray-200;\n      @apply hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors;\n\n      .iconfont {\n        @apply text-xl;\n      }\n    }\n\n    .action-btn {\n      @apply text-gray-500 dark:text-gray-400;\n      &:hover {\n        @apply text-red-500 dark:text-red-400;\n      }\n    }\n  }\n\n  &-content {\n    @apply h-[calc(70vh-60px)] overflow-hidden;\n  }\n}\n\n.empty-playlist {\n  @apply flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500;\n\n  .iconfont {\n    @apply text-5xl mb-4;\n  }\n\n  p {\n    @apply text-sm;\n  }\n}\n\n.music-play-list-content {\n  @apply pr-2 hover:bg-light-100 dark:hover:bg-dark-100;\n  &:hover {\n    .delete-btn {\n      @apply visible;\n    }\n  }\n  .delete-btn {\n    @apply pr-2 cursor-pointer invisible;\n    .iconfont {\n      @apply text-lg;\n    }\n  }\n}\n\n// 移动端适配\n@media (max-width: 768px) {\n  .playlist-panel {\n    position: fixed;\n    width: 100%;\n    height: 80vh;\n    top: auto;\n    bottom: 0; // 移动端底部留出导航栏高度\n    border-radius: 30px 30px 0 0;\n    border-left: none;\n    border-top: 1px solid theme('colors.gray.200');\n    box-shadow: 0 -5px 20px rgba(0, 0, 0, 0.1);\n\n    &-header {\n      @apply text-center relative px-4;\n\n      &::before {\n        content: '';\n        position: absolute;\n        top: -15px;\n        left: 50%;\n        transform: translateX(-50%);\n        width: 40px;\n        height: 5px;\n        border-radius: 5px;\n        background-color: rgba(150, 150, 150, 0.3);\n      }\n    }\n\n    &-content {\n      height: calc(80vh - 60px);\n      @apply px-4;\n      .delete-btn {\n        @apply visible;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/player/ReparsePopover.vue",
    "content": "<template>\n  <n-popover\n    trigger=\"click\"\n    :z-index=\"99999999\"\n    placement=\"top\"\n    content-class=\"music-source-popover\"\n    raw\n    :show-arrow=\"false\"\n    :delay=\"200\"\n  >\n    <template #trigger>\n      <n-tooltip trigger=\"hover\" :z-index=\"9999999\">\n        <template #trigger>\n          <i\n            class=\"iconfont ri-refresh-line\"\n            :class=\"{ 'text-green-500': isReparse, 'animate-spin': isReparsing }\"\n          ></i>\n        </template>\n        {{ t('player.playBar.reparse') }}\n      </n-tooltip>\n    </template>\n    <div class=\"reparse-popover bg-light-100 dark:bg-dark-100 p-4 rounded-xl max-w-60\">\n      <div class=\"text-base font-medium mb-2\">{{ t('player.reparse.title') }}</div>\n      <div class=\"text-sm opacity-70 mb-3\">{{ t('player.reparse.desc') }}</div>\n      <div class=\"mb-3\">\n        <div class=\"flex flex-col space-y-2\">\n          <div\n            v-for=\"source in musicSourceOptions\"\n            :key=\"source.value\"\n            class=\"source-button flex items-center p-2 rounded-lg cursor-pointer transition-all duration-200 bg-light-200 dark:bg-dark-200 hover:bg-light-300 dark:hover:bg-dark-300\"\n            :class=\"{\n              'bg-green-50 dark:bg-green-900/20 text-green-500': isCurrentSource(source.value),\n              'opacity-50 cursor-not-allowed': isReparsing || playMusic.source === 'bilibili'\n            }\"\n            @click=\"directReparseMusic(source.value)\"\n          >\n            <div class=\"flex items-center justify-center w-6 h-6 mr-3 text-lg\">\n              <i :class=\"getSourceIcon(source.value)\"></i>\n            </div>\n            <div class=\"flex-1 text-sm whitespace-nowrap overflow-hidden text-ellipsis\">\n              {{ source.label }}\n            </div>\n            <div\n              v-if=\"isReparsing && currentReparsingSource === source.value\"\n              class=\"w-5 h-5 flex items-center justify-center\"\n            >\n              <i class=\"ri-loader-4-line animate-spin\"></i>\n            </div>\n            <div\n              v-else-if=\"isCurrentSource(source.value)\"\n              class=\"w-5 h-5 flex items-center justify-center\"\n            >\n              <i class=\"ri-check-line\"></i>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div v-if=\"playMusic.source === 'bilibili'\" class=\"text-red-500 text-sm\">\n        {{ t('player.reparse.bilibiliNotSupported') }}\n      </div>\n      <!-- 清除自定义音源 -->\n      <div\n        class=\"text-red-500 text-sm flex items-center bg-light-200 dark:bg-dark-200 rounded-lg p-2 cursor-pointer\"\n        @click=\"clearCustomSource\"\n      >\n        <div class=\"flex items-center justify-center w-6 h-6 mr-3 text-lg\">\n          <i class=\"ri-close-circle-line\"></i>\n        </div>\n        <div>\n          {{ t('player.reparse.clear') }}\n        </div>\n      </div>\n    </div>\n  </n-popover>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useMessage } from 'naive-ui';\nimport { computed, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { CacheManager } from '@/api/musicParser';\nimport { playMusic } from '@/hooks/MusicHook';\nimport { SongSourceConfigManager } from '@/services/SongSourceConfigManager';\nimport { usePlayerStore } from '@/store/modules/player';\nimport type { Platform } from '@/types/music';\n\nconst playerStore = usePlayerStore();\nconst { t } = useI18n();\nconst message = useMessage();\n\n// 音源重新解析状态\nconst isReparsing = ref(false);\nconst currentReparsingSource = ref<Platform | null>(null);\n\n// 实际存储选中音源的值\nconst selectedSourcesValue = ref<Platform[]>([]);\n\nconst isReparse = computed(() => selectedSourcesValue.value.length > 0);\n\n// 可选音源列表\nconst musicSourceOptions = ref([\n  { label: 'MiGu', value: 'migu' as Platform },\n  { label: 'KuGou', value: 'kugou' as Platform },\n  { label: 'pyncmd', value: 'pyncmd' as Platform },\n  { label: 'Bilibili', value: 'bilibili' as Platform },\n  { label: 'GdMuisc', value: 'gdmusic' as Platform }\n]);\n\n// 检查音源是否被选中\nconst isCurrentSource = (source: Platform) => {\n  return selectedSourcesValue.value.includes(source);\n};\n\n// 获取音源图标\nconst getSourceIcon = (source: Platform) => {\n  const iconMap: Record<Platform, string> = {\n    migu: 'ri-music-2-fill',\n    kugou: 'ri-music-fill',\n    qq: 'ri-qq-fill',\n    joox: 'ri-disc-fill',\n    pyncmd: 'ri-netease-cloud-music-fill',\n    bilibili: 'ri-bilibili-fill',\n    gdmusic: 'ri-google-fill',\n    kuwo: 'ri-music-fill',\n    lxMusic: 'ri-leaf-fill'\n  };\n\n  return iconMap[source] || 'ri-music-2-fill';\n};\n\n// 初始化选中的音源\nconst initSelectedSources = () => {\n  const songId = playMusic.value.id;\n  const config = SongSourceConfigManager.getConfig(songId);\n\n  if (config) {\n    selectedSourcesValue.value = config.sources;\n  } else {\n    selectedSourcesValue.value = [];\n  }\n};\n\n// 清除自定义音源\nconst clearCustomSource = () => {\n  SongSourceConfigManager.clearConfig(playMusic.value.id);\n  selectedSourcesValue.value = [];\n};\n\n// 直接重新解析当前歌曲\nconst directReparseMusic = async (source: Platform) => {\n  if (isReparsing.value || playMusic.value.source === 'bilibili') {\n    return;\n  }\n\n  try {\n    isReparsing.value = true;\n    currentReparsingSource.value = source;\n\n    const songId = Number(playMusic.value.id);\n\n    await CacheManager.clearMusicCache(songId);\n\n    // 更新选中的音源值为当前点击的音源\n    selectedSourcesValue.value = [source];\n\n    // 使用 SongSourceConfigManager 保存配置（手动选择）\n    SongSourceConfigManager.setConfig(songId, [source], 'manual');\n\n    const success = await playerStore.reparseCurrentSong(source, false);\n\n    if (success) {\n      message.success(t('player.reparse.success'));\n    } else {\n      message.error(t('player.reparse.failed'));\n    }\n  } catch (error) {\n    console.error('解析失败:', error);\n    message.error(t('player.reparse.failed'));\n  } finally {\n    isReparsing.value = false;\n    currentReparsingSource.value = null;\n  }\n};\n\n// 监听歌曲ID变化，初始化音源设置\nwatch(\n  () => playMusic.value.id,\n  () => {\n    if (playMusic.value.id) {\n      initSelectedSources();\n    }\n  },\n  { immediate: true }\n);\n</script>\n\n<style lang=\"scss\" scoped>\n.music-source-popover {\n  @apply w-64 rounded-xl overflow-hidden;\n}\n\n@keyframes spin {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n.animate-spin {\n  animation: spin 1s linear infinite;\n}\n\n.source-button {\n  &:hover:not(.opacity-50) {\n    @apply transform -translate-y-0.5 shadow-sm;\n  }\n}\n\n.iconfont {\n  @apply text-2xl mx-3;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/player/SimplePlayBar.vue",
    "content": "<template>\n  <div class=\"play-bar\" :class=\"{ 'dark-theme': isDarkMode }\" ref=\"playBarRef\">\n    <div class=\"container\">\n      <!-- 顶部进度条和时间 -->\n      <div class=\"top-section\">\n        <!-- 进度条 -->\n        <div\n          class=\"progress-bar\"\n          :class=\"{ 'is-dragging': isDragging }\"\n          @mousedown=\"handleProgressMouseDown\"\n          @click.stop=\"handleProgressClick\"\n        >\n          <div class=\"progress-track\"></div>\n          <div class=\"progress-fill\" :style=\"{ width: `${progressPercentage}%` }\"></div>\n        </div>\n\n        <!-- 时间显示 -->\n        <div class=\"time-display\">\n          <span class=\"current-time\">{{ formatTime(displayTime) }}</span>\n          <span class=\"total-time\">{{ formatTime(allTime) }}</span>\n        </div>\n      </div>\n\n      <!-- 主控制区域 -->\n      <div class=\"controls-section\">\n        <div class=\"left-controls\">\n          <button class=\"control-btn small-btn\" @click=\"togglePlayMode\">\n            <i\n              class=\"iconfont\"\n              :class=\"[playModeIcon, { 'intelligence-active': playMode === 3 }]\"\n            ></i>\n          </button>\n        </div>\n\n        <div class=\"center-controls\">\n          <!-- 上一首 -->\n          <button class=\"control-btn\" @click=\"handlePrev\">\n            <i class=\"iconfont icon-prev\"></i>\n          </button>\n\n          <!-- 播放/暂停 -->\n          <button class=\"control-btn play-btn\" @click=\"playMusicEvent\">\n            <i class=\"iconfont\" :class=\"play ? 'icon-stop' : 'icon-play'\"></i>\n          </button>\n\n          <!-- 下一首 -->\n          <button class=\"control-btn\" @click=\"handleNext\">\n            <i class=\"iconfont icon-next\"></i>\n          </button>\n        </div>\n\n        <div class=\"right-controls\">\n          <!-- 播放列表按钮 -->\n          <button class=\"control-btn small-btn\" @click=\"openPlayListDrawer\">\n            <i class=\"iconfont icon-list\"></i>\n          </button>\n        </div>\n      </div>\n\n      <!-- 底部控制区域 -->\n      <div class=\"bottom-section\">\n        <div class=\"spacer\"></div>\n        <!-- 音量控制 -->\n        <div class=\"volume-control\">\n          <i class=\"iconfont\" :class=\"getVolumeIcon\" @click=\"mute\"></i>\n          <div class=\"volume-slider\">\n            <n-slider\n              v-model:value=\"volumeSlider\"\n              :step=\"1\"\n              :tooltip=\"false\"\n              @wheel.prevent=\"handleVolumeWheel\"\n            ></n-slider>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, ref, watch } from 'vue';\n\nimport { allTime, nowTime, playMusic } from '@/hooks/MusicHook';\nimport { usePlayMode } from '@/hooks/usePlayMode';\nimport { audioService } from '@/services/audioService';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { secondToMinute } from '@/utils';\n\nconst props = withDefaults(\n  defineProps<{\n    isDark: boolean;\n  }>(),\n  {\n    isDark: false\n  }\n);\n\nconst playerStore = usePlayerStore();\nconst playBarRef = ref<HTMLElement | null>(null);\n\n// 播放状态\nconst play = computed(() => playerStore.isPlay);\n\n// 播放模式\nconst { playMode, playModeIcon, togglePlayMode } = usePlayMode();\n\n// 音量控制\nconst audioVolume = ref(\n  localStorage.getItem('volume') ? parseFloat(localStorage.getItem('volume') as string) : 1\n);\n\nconst volumeSlider = computed({\n  get: () => audioVolume.value * 100,\n  set: (value) => {\n    localStorage.setItem('volume', (value / 100).toString());\n    audioService.setVolume(value / 100);\n    audioVolume.value = value / 100;\n  }\n});\n\n// 音量图标\nconst getVolumeIcon = computed(() => {\n  if (audioVolume.value === 0) return 'ri-volume-mute-line';\n  if (audioVolume.value <= 0.5) return 'ri-volume-down-line';\n  return 'ri-volume-up-line';\n});\n\n// 静音切换\nconst mute = () => {\n  if (volumeSlider.value === 0) {\n    volumeSlider.value = 30;\n  } else {\n    volumeSlider.value = 0;\n  }\n};\n\n// 鼠标滚轮调整音量\nconst handleVolumeWheel = (e: WheelEvent) => {\n  const delta = e.deltaY < 0 ? 5 : -5;\n  const newValue = Math.min(Math.max(volumeSlider.value + delta, 0), 100);\n  volumeSlider.value = newValue;\n};\n\n// 播放控制\nconst handlePrev = () => playerStore.prevPlay();\nconst handleNext = () => playerStore.nextPlay();\n\nconst playMusicEvent = async () => {\n  try {\n    await playerStore.setPlay({ ...playMusic.value });\n  } catch (error) {\n    console.error('播放出错:', error);\n    playerStore.nextPlay();\n  }\n};\n\n// 进度条控制\nconst isDragging = ref(false);\nconst dragProgress = ref(0); // 拖拽时的预览进度 (0-100)\n\n// 计算当前显示的进度百分比\nconst progressPercentage = computed(() => {\n  if (isDragging.value) {\n    return dragProgress.value;\n  }\n  if (allTime.value === 0) return 0;\n  return (nowTime.value / allTime.value) * 100;\n});\n\n// 计算显示的时间\nconst displayTime = computed(() => {\n  if (isDragging.value) {\n    return (dragProgress.value / 100) * allTime.value;\n  }\n  return nowTime.value;\n});\n\n// 计算进度百分比的辅助函数\nconst calculateProgress = (clientX: number, element: HTMLElement): number => {\n  const rect = element.getBoundingClientRect();\n  const percent = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));\n  return percent * 100;\n};\n\n// 更新音频进度\nconst seekToProgress = (percentage: number) => {\n  const targetTime = (percentage / 100) * allTime.value;\n  audioService.seek(targetTime);\n  // 不立即更新 nowTime,让音频服务的回调来更新,避免不同步\n};\n\n// 鼠标按下开始拖拽\nconst handleProgressMouseDown = (e: MouseEvent) => {\n  if (e.button !== 0) return; // 只响应左键\n\n  const target = e.currentTarget as HTMLElement;\n  isDragging.value = true;\n  dragProgress.value = calculateProgress(e.clientX, target);\n\n  // 添加全局鼠标移动和释放监听\n  const handleMouseMove = (moveEvent: MouseEvent) => {\n    if (isDragging.value) {\n      dragProgress.value = calculateProgress(moveEvent.clientX, target);\n    }\n  };\n\n  const handleMouseUp = () => {\n    if (isDragging.value) {\n      // 拖拽结束,执行跳转\n      seekToProgress(dragProgress.value);\n      isDragging.value = false;\n    }\n    // 移除事件监听\n    document.removeEventListener('mousemove', handleMouseMove);\n    document.removeEventListener('mouseup', handleMouseUp);\n  };\n\n  document.addEventListener('mousemove', handleMouseMove);\n  document.addEventListener('mouseup', handleMouseUp);\n\n  // 防止文本选择\n  e.preventDefault();\n};\n\n// 点击进度条跳转\nconst handleProgressClick = (e: MouseEvent) => {\n  // 如果正在拖拽,不处理点击事件\n  if (isDragging.value) return;\n\n  const target = e.currentTarget as HTMLElement;\n  const percentage = calculateProgress(e.clientX, target);\n  seekToProgress(percentage);\n};\n\n// 格式化时间\nconst formatTime = (seconds: number) => {\n  return secondToMinute(seconds);\n};\n\n// 打开播放列表抽屉\nconst openPlayListDrawer = () => {\n  playerStore.setPlayListDrawerVisible(true);\n};\n\n// 深色模式\nconst isDarkMode = computed(() => props.isDark);\n\n// 主题颜色应用函数\nconst applyThemeColor = (colorValue: string) => {\n  if (!colorValue || !playBarRef.value) return;\n\n  console.log('应用主题色:', colorValue);\n  const playBarElement = playBarRef.value;\n\n  // 解析RGB值\n  const rgbMatch = colorValue.match(/rgb\\((\\d+),\\s*(\\d+),\\s*(\\d+)\\)/);\n\n  if (rgbMatch) {\n    const [_, r, g, b] = rgbMatch.map(Number);\n\n    // 计算颜色亮度 (0-255)\n    // 使用加权平均值公式: 0.299*R + 0.587*G + 0.114*B\n    const brightness = Math.round(0.299 * r + 0.587 * g + 0.114 * b);\n\n    console.log(`主题色亮度: ${brightness}/255`);\n\n    // 设置主色\n    playBarElement.style.setProperty('--fill-color', colorValue);\n\n    // 亮度自适应处理\n    if (brightness > 200) {\n      // 非常亮的颜色\n      // 深化主色以增加对比度\n      const darkenedColor = `rgb(${Math.max(0, r - 60)}, ${Math.max(0, g - 60)}, ${Math.max(0, b - 60)})`;\n      playBarElement.style.setProperty('--fill-color-alt', darkenedColor);\n      playBarElement.style.setProperty('--fill-color-transparent', `rgba(${r}, ${g}, ${b}, 0.5)`); // 提高透明度\n      playBarElement.style.setProperty('--text-on-fill', '#000000'); // 亮色背景上用黑色文字\n      playBarElement.style.setProperty('--high-contrast-color', '#000000'); // 高对比度颜色\n      playBarElement.classList.add('light-theme-color');\n      playBarElement.classList.remove('dark-theme-color');\n    } else if (brightness < 50) {\n      // 非常暗的颜色\n      // 提亮主色以增加可见性\n      const lightenedColor = `rgb(${Math.min(255, r + 60)}, ${Math.min(255, g + 60)}, ${Math.min(255, b + 60)})`;\n      playBarElement.style.setProperty('--fill-color-alt', lightenedColor);\n      playBarElement.style.setProperty('--fill-color-transparent', `rgba(${r}, ${g}, ${b}, 0.7)`); // 提高透明度\n      playBarElement.style.setProperty('--text-on-fill', '#ffffff'); // 暗色背景上用白色文字\n      playBarElement.style.setProperty('--high-contrast-color', '#ffffff'); // 高对比度颜色\n      playBarElement.classList.add('dark-theme-color');\n      playBarElement.classList.remove('light-theme-color');\n    } else {\n      // 计算辅助色和高亮色\n      // 普通亮度颜色，正常处理\n      playBarElement.style.setProperty('--fill-color-alt', colorValue); // 保持一致\n      playBarElement.style.setProperty('--fill-color-transparent', `rgba(${r}, ${g}, ${b}, 0.25)`);\n      // 根据亮度决定文本颜色\n      const textColor = brightness > 125 ? '#000000' : '#ffffff';\n      playBarElement.style.setProperty('--text-on-fill', textColor);\n      playBarElement.style.setProperty('--high-contrast-color', textColor);\n      playBarElement.classList.remove('light-theme-color');\n      playBarElement.classList.remove('dark-theme-color');\n    }\n\n    // 设置亮色（用于高亮效果）\n    const lightenedColor = `rgb(${Math.min(255, r + 40)}, ${Math.min(255, g + 40)}, ${Math.min(255, b + 40)})`;\n    playBarElement.style.setProperty('--fill-color-light', lightenedColor);\n  } else {\n    // 无法解析RGB值时的默认设置\n    playBarElement.style.setProperty('--fill-color', colorValue);\n    playBarElement.style.setProperty('--fill-color-transparent', `${colorValue}40`);\n    playBarElement.style.setProperty('--fill-color-light', `${colorValue}80`);\n    playBarElement.style.setProperty('--fill-color-alt', colorValue);\n    playBarElement.style.setProperty('--text-on-fill', '#ffffff');\n    playBarElement.style.setProperty('--high-contrast-color', '#ffffff');\n  }\n};\n\n// 监听主题色变化\nwatch(\n  () => playerStore.playMusic.primaryColor,\n  (newVal) => {\n    if (newVal) {\n      applyThemeColor(newVal);\n    }\n  }\n);\n\nonMounted(() => {\n  if (playerStore.playMusic?.primaryColor) {\n    setTimeout(() => {\n      applyThemeColor(playerStore.playMusic.primaryColor as string);\n    }, 50);\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.play-bar {\n  @apply w-full;\n  border-radius: 12px;\n  transition: all 0.3s ease;\n\n  /* 默认变量 */\n  --text-on-fill: #ffffff;\n  --high-contrast-color: #ffffff;\n\n  &.dark-theme {\n    --text-color: #333333;\n    --muted-color: rgba(0, 0, 0, 0.6);\n    --track-color: rgba(0, 0, 0, 0.2);\n    --track-color-hover: rgba(0, 0, 0, 0.4);\n    --fill-color: #1ed760;\n    --fill-color-alt: #1ed760;\n    --fill-color-transparent: rgba(30, 215, 96, 0.25);\n    --fill-color-light: rgba(30, 215, 96, 0.5);\n    --button-bg: rgba(0, 0, 0, 0.1);\n    --button-hover: rgba(0, 0, 0, 0.2);\n  }\n\n  &:not(.dark-theme) {\n    --text-color: #f1f1f1;\n    --muted-color: rgba(255, 255, 255, 0.6);\n    --track-color: rgba(255, 255, 255, 0.1);\n    --track-color-hover: rgba(255, 255, 255, 0.2);\n    --fill-color: #73e49a;\n    --fill-color-alt: #73e49a;\n    --fill-color-transparent: rgba(115, 228, 154, 0.25);\n    --fill-color-light: rgba(115, 228, 154, 0.5);\n    --button-bg: rgba(255, 255, 255, 0.05);\n    --button-hover: rgba(255, 255, 255, 0.1);\n  }\n\n  /* 极亮主题色适配 */\n  &.light-theme-color {\n    .progress-fill {\n      box-shadow:\n        0 0 8px var(--fill-color-transparent),\n        inset 0 0 0 1px rgba(0, 0, 0, 0.1);\n    }\n\n    .control-btn.play-btn {\n      box-shadow:\n        0 3px 8px var(--fill-color-transparent),\n        0 1px 2px rgba(0, 0, 0, 0.3);\n      color: var(--text-on-fill);\n    }\n\n    .volume-control .iconfont:hover {\n      color: var(--fill-color-alt);\n    }\n  }\n\n  /* 极暗主题色适配 */\n  &.dark-theme-color {\n    .progress-fill {\n      box-shadow:\n        0 0 10px var(--fill-color-transparent),\n        inset 0 0 0 1px rgba(255, 255, 255, 0.2);\n    }\n\n    .control-btn.play-btn {\n      box-shadow:\n        0 3px 12px var(--fill-color-transparent),\n        0 0 0 1px rgba(255, 255, 255, 0.2);\n\n      .iconfont {\n        text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);\n      }\n    }\n\n    .volume-control .iconfont:hover {\n      color: var(--fill-color-light);\n    }\n  }\n}\n\n.container {\n  @apply flex flex-col;\n}\n\n.top-section {\n  @apply mb-3;\n\n  .progress-bar {\n    @apply relative cursor-pointer h-2 mb-2 w-full;\n    user-select: none;\n\n    .progress-track {\n      @apply absolute inset-0 rounded-full transition-all duration-150;\n      background-color: var(--track-color);\n    }\n\n    .progress-fill {\n      @apply absolute top-0 left-0 h-full rounded-full transition-all duration-150;\n      background: linear-gradient(90deg, var(--fill-color), var(--fill-color-light));\n      box-shadow: 0 0 8px var(--fill-color-transparent);\n    }\n\n    &:hover {\n      .progress-track {\n        background-color: var(--track-color-hover);\n      }\n\n      .progress-fill {\n        box-shadow: 0 0 12px var(--fill-color-transparent);\n      }\n    }\n  }\n\n  .time-display {\n    @apply flex justify-between text-base;\n    color: var(--muted-color);\n\n    .time-separator {\n      @apply mx-1;\n    }\n\n    .current-time {\n      opacity: 0.8;\n      transition: opacity 0.3s ease;\n\n      &:hover {\n        opacity: 1;\n      }\n    }\n  }\n}\n\n.controls-section {\n  @apply flex items-center justify-between mb-4;\n\n  .left-controls,\n  .right-controls {\n    @apply flex items-center;\n  }\n\n  .center-controls {\n    @apply flex items-center justify-center space-x-6;\n  }\n}\n\n.bottom-section {\n  @apply flex items-center justify-between mt-2;\n}\n\n.control-btn {\n  @apply flex items-center justify-center rounded-full outline-none border-0 transition-all duration-200;\n  color: var(--text-color);\n  background: transparent;\n  width: 32px;\n  height: 32px;\n  cursor: pointer;\n\n  &:hover {\n    background-color: var(--button-bg);\n    transform: scale(1.05);\n  }\n\n  &:active {\n    background-color: var(--button-hover);\n    transform: scale(0.95);\n  }\n\n  &.play-btn {\n    background: linear-gradient(145deg, var(--fill-color), var(--fill-color-alt));\n    color: var(--text-on-fill);\n    width: 46px;\n    height: 46px;\n    box-shadow: 0 3px 8px var(--fill-color-transparent);\n\n    &:hover {\n      box-shadow: 0 4px 12px var(--fill-color-transparent);\n    }\n\n    .iconfont {\n      font-size: 1.25rem;\n    }\n  }\n\n  &.small-btn {\n    @apply text-2xl;\n    width: 28px;\n    height: 28px;\n  }\n\n  .iconfont {\n    @apply text-2xl;\n  }\n}\n\n.volume-control {\n  @apply flex items-center space-x-2;\n  color: var(--text-color);\n\n  .iconfont {\n    @apply cursor-pointer text-base;\n    transition:\n      transform 0.2s ease,\n      color 0.2s ease;\n\n    &:hover {\n      transform: scale(1.1);\n      color: var(--fill-color);\n    }\n  }\n\n  .volume-slider {\n    @apply w-24;\n\n    :deep(.n-slider) {\n      --n-rail-height: 3px;\n      --n-fill-color: var(--fill-color);\n      --n-rail-color: var(--track-color);\n      --n-handle-size: 12px;\n\n      .n-slider-rail {\n        @apply rounded-full;\n      }\n\n      .n-slider-rail__fill {\n        background: linear-gradient(90deg, var(--fill-color), var(--fill-color-light));\n        box-shadow: 0 0 6px var(--fill-color-transparent);\n      }\n\n      .n-slider-handle {\n        @apply opacity-0 transition-opacity duration-200;\n        background: white;\n        box-shadow:\n          0 0 6px var(--fill-color-transparent),\n          0 0 0 1px var(--high-contrast-color);\n        border: 2px solid var(--fill-color);\n      }\n\n      &:hover .n-slider-handle {\n        @apply opacity-100;\n      }\n    }\n  }\n}\n\n.spacer {\n  flex: 1;\n}\n\n.like-active {\n  color: var(--fill-color);\n  text-shadow: 0 0 8px var(--fill-color-transparent);\n}\n\n.intelligence-active {\n  @apply text-green-500;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/player/SleepTimer.vue",
    "content": "<template>\n  <div class=\"sleep-timer-content\">\n    <h3 class=\"timer-title\">{{ t('player.sleepTimer.title') }}</h3>\n\n    <div v-if=\"hasTimerActive\" class=\"sleep-timer-active\">\n      <div class=\"timer-status\">\n        <template v-if=\"timerType === 'time'\">\n          <div class=\"timer-value countdown-timer\">{{ formattedRemainingTime }}</div>\n        </template>\n        <template v-else-if=\"timerType === 'songs'\">\n          <div class=\"timer-value\">{{ remainingSongs }}</div>\n          <div class=\"timer-label\">\n            {{ t('player.sleepTimer.songsRemaining', { count: remainingSongs }) }}\n          </div>\n        </template>\n        <template v-else-if=\"timerType === 'end'\">\n          <div class=\"timer-value\">{{ t('player.sleepTimer.activeUntilEnd') }}</div>\n          <div class=\"timer-label\">{{ t('player.sleepTimer.afterPlaylist') }}</div>\n        </template>\n      </div>\n\n      <n-button type=\"error\" class=\"cancel-timer-btn\" @click=\"handleCancelTimer\" round>\n        {{ t('player.sleepTimer.cancel') }}\n      </n-button>\n    </div>\n\n    <div v-else class=\"sleep-timer-options\">\n      <!-- 按时间定时 -->\n      <div class=\"option-section\">\n        <h4 class=\"option-title\">{{ t('player.sleepTimer.timeMode') }}</h4>\n        <div class=\"time-options\">\n          <n-button\n            v-for=\"minutes in [15, 30, 60, 90]\"\n            :key=\"minutes\"\n            size=\"small\"\n            class=\"time-option-btn\"\n            @click=\"handleSetTimeTimer(minutes)\"\n            round\n          >\n            {{ minutes }}{{ t('player.sleepTimer.minutes') }}\n          </n-button>\n          <div class=\"custom-time\">\n            <n-input-number\n              v-model:value=\"customMinutes\"\n              :min=\"1\"\n              :max=\"300\"\n              size=\"small\"\n              class=\"custom-time-input\"\n              round\n            />\n            <n-button\n              size=\"small\"\n              type=\"primary\"\n              class=\"custom-time-btn\"\n              :disabled=\"!customMinutes\"\n              @click=\"handleSetTimeTimer(customMinutes)\"\n              round\n            >\n              {{ t('player.sleepTimer.set') }}\n            </n-button>\n          </div>\n        </div>\n      </div>\n\n      <!-- 按歌曲数定时 -->\n      <div class=\"option-section\">\n        <h4 class=\"option-title\">{{ t('player.sleepTimer.songsMode') }}</h4>\n        <div class=\"songs-options\">\n          <n-button\n            v-for=\"songs in [1, 3, 5, 10]\"\n            :key=\"songs\"\n            size=\"small\"\n            class=\"songs-option-btn\"\n            @click=\"handleSetSongsTimer(songs)\"\n            round\n          >\n            {{ songs }}{{ t('player.sleepTimer.songs') }}\n          </n-button>\n          <div class=\"custom-songs\">\n            <n-input-number\n              v-model:value=\"customSongs\"\n              :min=\"1\"\n              :max=\"50\"\n              size=\"small\"\n              class=\"custom-songs-input\"\n              round\n            />\n            <n-button\n              size=\"small\"\n              type=\"primary\"\n              class=\"custom-songs-btn\"\n              :disabled=\"!customSongs\"\n              @click=\"handleSetSongsTimer(customSongs)\"\n              round\n            >\n              {{ t('player.sleepTimer.set') }}\n            </n-button>\n          </div>\n        </div>\n      </div>\n\n      <!-- 播放完列表后关闭 -->\n      <div class=\"option-section playlist-end-section\">\n        <n-button block class=\"playlist-end-btn\" @click=\"handleSetPlaylistEndTimer\" round>\n          {{ t('player.sleepTimer.playlistEnd') }}\n        </n-button>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { storeToRefs } from 'pinia';\nimport { computed, onMounted, onUnmounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { usePlayerStore } from '@/store/modules/player';\n\nconst { t } = useI18n();\nconst playerStore = usePlayerStore();\n\n// 从store获取所有相关状态\nconst { sleepTimer } = storeToRefs(playerStore);\n\n// 本地状态，用于UI展示\nconst customMinutes = ref(30);\nconst customSongs = ref(5);\n// 添加一个刷新触发变量，用于强制更新倒计时\nconst refreshTrigger = ref(0);\n\n// 计算属性，判断定时器状态\nconst hasTimerActive = computed(() => {\n  return playerStore.hasSleepTimerActive;\n});\n\nconst timerType = computed(() => {\n  return sleepTimer.value.type;\n});\n\n// 剩余歌曲数\nconst remainingSongs = computed(() => {\n  return playerStore.sleepTimerRemainingSongs;\n});\n\n// 处理设置时间定时器\nfunction handleSetTimeTimer(minutes: number) {\n  playerStore.setSleepTimerByTime(minutes);\n}\n\n// 处理设置歌曲数定时器\nfunction handleSetSongsTimer(songs: number) {\n  playerStore.setSleepTimerBySongs(songs);\n}\n\n// 处理设置播放列表结束定时器\nfunction handleSetPlaylistEndTimer() {\n  playerStore.setSleepTimerAtPlaylistEnd();\n}\n\n// 处理取消定时器\nfunction handleCancelTimer() {\n  playerStore.clearSleepTimer();\n}\n\n// 格式化剩余时间为 HH:MM:SS\nconst formattedRemainingTime = computed(() => {\n  // 依赖刷新触发器强制更新\n  void refreshTrigger.value;\n\n  if (timerType.value !== 'time' || !sleepTimer.value.endTime) {\n    return '00:00:00';\n  }\n\n  const remaining = Math.max(0, sleepTimer.value.endTime - Date.now());\n  const totalSeconds = Math.floor(remaining / 1000);\n\n  const hours = Math.floor(totalSeconds / 3600);\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\n  const seconds = Math.floor(totalSeconds % 60);\n\n  const formattedHours = hours.toString().padStart(2, '0');\n  const formattedMinutes = minutes.toString().padStart(2, '0');\n  const formattedSeconds = seconds.toString().padStart(2, '0');\n\n  return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;\n});\n\n// 监听剩余时间变化\nlet timerInterval: number | null = null;\n\nonMounted(() => {\n  // 如果当前有定时器，开始更新UI\n  if (hasTimerActive.value && timerType.value === 'time') {\n    startTimerUpdate();\n  }\n\n  // 监听定时器状态变化\n  watch(\n    () => [hasTimerActive.value, timerType.value],\n    ([newHasTimer, newType]) => {\n      if (newHasTimer && newType === 'time') {\n        startTimerUpdate();\n      } else {\n        stopTimerUpdate();\n      }\n    }\n  );\n});\n\n// 启动定时器更新UI\nfunction startTimerUpdate() {\n  stopTimerUpdate(); // 先停止之前的计时器\n\n  // 每秒更新UI\n  timerInterval = window.setInterval(() => {\n    // 更新刷新触发器，强制重新计算\n    refreshTrigger.value = Date.now();\n  }, 500) as unknown as number;\n}\n\n// 停止定时器更新UI\nfunction stopTimerUpdate() {\n  if (timerInterval) {\n    clearInterval(timerInterval);\n    timerInterval = null;\n  }\n}\n\nonUnmounted(() => {\n  stopTimerUpdate();\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.sleep-timer-content {\n  @apply w-full p-4;\n\n  .timer-title {\n    @apply text-lg font-medium mb-4 text-center;\n  }\n\n  // 激活状态显示\n  .sleep-timer-active {\n    @apply flex flex-col items-center;\n\n    // 定时状态卡片\n    .timer-status {\n      @apply flex flex-col items-center justify-center p-8 mb-5 w-full rounded-2xl dark:bg-gray-800 dark:bg-opacity-40 dark:shadow-gray-900/20;\n      background-color: rgba(255, 255, 255, 0.5);\n      box-shadow:\n        0 1px 3px rgba(0, 0, 0, 0.05),\n        0 0 0 1px rgba(255, 255, 255, 0.1);\n      transition: all 0.3s ease;\n\n      // 定时值显示\n      .timer-value {\n        @apply text-4xl font-semibold mb-2 text-green-500;\n\n        &.countdown-timer {\n          font-variant-numeric: tabular-nums;\n          letter-spacing: 2px;\n        }\n      }\n\n      // 标签文本\n      .timer-label {\n        @apply text-base text-gray-600 dark:text-gray-300;\n      }\n    }\n\n    // 取消按钮\n    .cancel-timer-btn {\n      @apply w-full py-3 text-base rounded-full transition-all duration-200;\n\n      &:hover {\n        @apply transform scale-105 shadow-md;\n      }\n\n      &:active {\n        @apply transform scale-95;\n      }\n    }\n  }\n\n  // 定时器选项区域\n  .sleep-timer-options {\n    @apply flex flex-col;\n\n    // 选项部分\n    .option-section {\n      @apply mb-7;\n\n      // 选项标题\n      .option-title {\n        @apply text-base font-medium mb-4 text-gray-700 dark:text-gray-200;\n        letter-spacing: 0.3px;\n      }\n\n      // 时间/歌曲选项容器\n      .time-options,\n      .songs-options {\n        @apply flex flex-wrap gap-2;\n\n        // 选项按钮共享样式\n        .time-option-btn,\n        .songs-option-btn {\n          @apply px-4 py-2 rounded-full text-gray-800 dark:text-gray-200 transition-all duration-200;\n          background-color: rgba(255, 255, 255, 0.5);\n          @apply dark:bg-gray-800 dark:bg-opacity-40 hover:bg-white dark:hover:bg-gray-700;\n          box-shadow:\n            0 1px 2px rgba(0, 0, 0, 0.05),\n            0 0 0 1px rgba(255, 255, 255, 0.1);\n          @apply dark:shadow-gray-900/20;\n\n          &:hover {\n            @apply transform scale-105 shadow-md;\n          }\n\n          &:active {\n            @apply transform scale-95;\n          }\n        }\n\n        // 自定义输入区域\n        .custom-time,\n        .custom-songs {\n          @apply flex items-center space-x-2 mt-4 w-full;\n\n          // 输入框\n          .custom-time-input,\n          .custom-songs-input {\n            @apply flex-1;\n          }\n\n          // 设置按钮\n          .custom-time-btn,\n          .custom-songs-btn {\n            @apply py-2 px-4 rounded-full transition-all duration-200;\n          }\n        }\n      }\n    }\n\n    // 播放列表结束选项\n    .playlist-end-section {\n      @apply mt-2;\n\n      .playlist-end-btn {\n        @apply py-3 text-base rounded-full transition-all duration-200;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/player/SleepTimerTop.vue",
    "content": "<template>\n  <div>\n    <!-- 定时关闭倒计时显示区域 -->\n    <div v-if=\"hasActiveSleepTimer\" class=\"sleep-timer-countdown\" @click=\"handleShowTimer\">\n      <i class=\"iconfont ri-time-line mr-1\"></i>\n      <span>{{ formattedRemainingTime }}</span>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia';\nimport { useI18n } from 'vue-i18n';\n\nimport { usePlayerStore } from '@/store/modules/player';\n\nconst { t } = useI18n();\n// 定时器状态\nconst playerStore = usePlayerStore();\nconst { sleepTimer } = storeToRefs(playerStore);\nconst hasActiveSleepTimer = computed(() => playerStore.hasSleepTimerActive);\nconst refreshTrigger = ref(0);\n\n// 检查定时器是否已结束\nconst checkTimerExpired = () => {\n  if (sleepTimer.value.type === 'time' && sleepTimer.value.endTime) {\n    const now = Date.now();\n    if (now >= sleepTimer.value.endTime) {\n      playerStore.clearSleepTimer();\n    }\n  }\n};\n\n// 在组件挂载时检查定时器状态\nonMounted(() => {\n  checkTimerExpired();\n});\n\n// 倒计时显示\nconst formattedRemainingTime = computed(() => {\n  // 依赖刷新触发器强制更新\n  void refreshTrigger.value;\n\n  if (sleepTimer.value.type !== 'time' || !sleepTimer.value.endTime) {\n    if (sleepTimer.value.type === 'songs' && sleepTimer.value.remainingSongs) {\n      return t('player.sleepTimer.songsRemaining', { count: sleepTimer.value.remainingSongs });\n    }\n    if (sleepTimer.value.type === 'end') {\n      return t('player.sleepTimer.activeUntilEnd');\n    }\n    return '';\n  }\n\n  const remaining = Math.max(0, sleepTimer.value.endTime - Date.now());\n  const totalSeconds = Math.floor(remaining / 1000);\n\n  const hours = Math.floor(totalSeconds / 3600);\n  const minutes = Math.floor((totalSeconds % 3600) / 60);\n  const seconds = Math.floor(totalSeconds % 60);\n\n  if (hours > 0) {\n    return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;\n  } else {\n    return `${minutes}:${seconds.toString().padStart(2, '0')}`;\n  }\n});\n\n// 监听剩余时间变化\nlet timerUpdateInterval: number | null = null;\n\nwatch(\n  () => hasActiveSleepTimer.value,\n  (newHasTimer) => {\n    if (newHasTimer && sleepTimer.value.type === 'time') {\n      startTimerUpdate();\n    } else if (!newHasTimer) {\n      stopTimerUpdate();\n    }\n  },\n  { immediate: true }\n);\n\n// 启动定时器更新UI\nfunction startTimerUpdate() {\n  stopTimerUpdate(); // 先停止之前的计时器\n\n  // 每秒更新UI\n  timerUpdateInterval = window.setInterval(() => {\n    // 更新刷新触发器，强制重新计算\n    refreshTrigger.value = Date.now();\n  }, 1000) as unknown as number;\n}\n\n// 停止定时器更新UI\nfunction stopTimerUpdate() {\n  if (timerUpdateInterval) {\n    clearInterval(timerUpdateInterval);\n    timerUpdateInterval = null;\n  }\n}\n\nconst handleShowTimer = () => {\n  playerStore.showSleepTimer = !playerStore.showSleepTimer;\n};\n\n// 播放器卸载时清除定时器\nonUnmounted(() => {\n  stopTimerUpdate();\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.sleep-timer-countdown {\n  @apply fixed top-[28px] left-1/2 transform -translate-x-1/2 -translate-y-full py-1 px-3 rounded-b-lg bg-green-500 text-white text-sm flex items-center hover:scale-110 transition-all cursor-pointer;\n  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);\n  z-index: 9998;\n  min-width: 80px;\n  text-align: center;\n  animation: fadeInDown 0.3s ease-out;\n  -webkit-app-region: no-drag;\n\n  @keyframes fadeInDown {\n    from {\n      transform: translate(-50%, -150%);\n      opacity: 0;\n    }\n    to {\n      transform: translate(-50%, -100%);\n      opacity: 1;\n    }\n  }\n\n  span {\n    font-variant-numeric: tabular-nums;\n    letter-spacing: 0.5px;\n    font-weight: 500;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/settings/ClearCacheSettings.vue",
    "content": "<template>\n  <n-modal\n    v-model:show=\"visible\"\n    preset=\"dialog\"\n    :title=\"t('settings.system.cache')\"\n    :positive-text=\"t('common.confirm')\"\n    :negative-text=\"t('common.cancel')\"\n    @positive-click=\"handleConfirm\"\n    @negative-click=\"handleCancel\"\n  >\n    <n-space vertical>\n      <p>{{ t('settings.system.cacheClearTitle') }}</p>\n      <n-checkbox-group v-model:value=\"selectedTypes\">\n        <n-space vertical>\n          <n-checkbox\n            v-for=\"option in clearCacheOptions\"\n            :key=\"option.key\"\n            :value=\"option.key\"\n            :label=\"option.label\"\n          >\n            <template #default>\n              <div>\n                <div>{{ t(`settings.system.cacheTypes.${option.key}.label`) }}</div>\n                <div class=\"text-gray-400 text-sm\">\n                  {{ t(`settings.system.cacheTypes.${option.key}.description`) }}\n                </div>\n              </div>\n            </template>\n          </n-checkbox>\n        </n-space>\n      </n-checkbox-group>\n    </n-space>\n  </n-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { defineEmits, defineProps, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  show: {\n    type: Boolean,\n    default: false\n  }\n});\n\nconst emit = defineEmits(['update:show', 'confirm']);\n\nconst { t } = useI18n();\nconst visible = ref(props.show);\nconst selectedTypes = ref<string[]>([]);\n\nconst clearCacheOptions = ref([\n  {\n    label: t('settings.system.cacheTypes.history.label'),\n    key: 'history',\n    description: t('settings.system.cacheTypes.history.description')\n  },\n  {\n    label: t('settings.system.cacheTypes.favorite.label'),\n    key: 'favorite',\n    description: t('settings.system.cacheTypes.favorite.description')\n  },\n  {\n    label: t('settings.system.cacheTypes.user.label'),\n    key: 'user',\n    description: t('settings.system.cacheTypes.user.description')\n  },\n  {\n    label: t('settings.system.cacheTypes.settings.label'),\n    key: 'settings',\n    description: t('settings.system.cacheTypes.settings.description')\n  },\n  {\n    label: t('settings.system.cacheTypes.downloads.label'),\n    key: 'downloads',\n    description: t('settings.system.cacheTypes.downloads.description')\n  },\n  {\n    label: t('settings.system.cacheTypes.resources.label'),\n    key: 'resources',\n    description: t('settings.system.cacheTypes.resources.description')\n  },\n  {\n    label: t('settings.system.cacheTypes.lyrics.label'),\n    key: 'lyrics',\n    description: t('settings.system.cacheTypes.lyrics.description')\n  }\n]);\n\n// 同步外部show属性变化\nwatch(\n  () => props.show,\n  (newVal) => {\n    visible.value = newVal;\n  }\n);\n\n// 同步内部visible变化\nwatch(\n  () => visible.value,\n  (newVal) => {\n    emit('update:show', newVal);\n  }\n);\n\nconst handleConfirm = () => {\n  emit('confirm', selectedTypes.value);\n  selectedTypes.value = [];\n};\n\nconst handleCancel = () => {\n  selectedTypes.value = [];\n  visible.value = false;\n};\n</script>\n"
  },
  {
    "path": "src/renderer/components/settings/CookieSettingsModal.vue",
    "content": "<script lang=\"ts\" setup>\nimport { useMessage } from 'naive-ui';\nimport { ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\ndefineOptions({\n  name: 'CookieSettingsModal'\n});\n\ninterface Props {\n  show: boolean;\n  initialValue?: string;\n}\n\ninterface Emits {\n  (e: 'update:show', value: boolean): void;\n  (e: 'save', value: string): void;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  initialValue: ''\n});\n\nconst emit = defineEmits<Emits>();\n\nconst { t } = useI18n();\nconst message = useMessage();\n\nconst tokenInput = ref('');\nconst isLoading = ref(false);\n\n// 监听显示状态变化，重置输入框\nwatch(\n  () => props.show,\n  (newShow) => {\n    if (newShow) {\n      tokenInput.value = props.initialValue;\n    }\n  }\n);\n\n// 监听初始值变化\nwatch(\n  () => props.initialValue,\n  (newValue) => {\n    if (props.show) {\n      tokenInput.value = newValue;\n    }\n  }\n);\n\n// 关闭弹窗\nconst handleClose = () => {\n  emit('update:show', false);\n};\n\n// 保存Cookie\nconst handleSave = async () => {\n  const trimmedToken = tokenInput.value.trim();\n\n  if (!trimmedToken) {\n    message.error(t('settings.cookie.validation.required'));\n    return;\n  }\n\n  // 简单验证Cookie格式\n  if (!trimmedToken.includes('MUSIC_U=')) {\n    message.warning(t('settings.cookie.validation.format'));\n  }\n\n  try {\n    isLoading.value = true;\n    emit('save', trimmedToken);\n    message.success(t('settings.cookie.message.saveSuccess'));\n    handleClose();\n  } catch (error) {\n    console.error('保存Cookie失败:', error);\n    message.error(t('settings.cookie.message.saveError'));\n  } finally {\n    isLoading.value = false;\n  }\n};\n\n// 清空输入框\nconst handleClear = () => {\n  tokenInput.value = '';\n};\n\n// 从剪贴板粘贴\nconst handlePaste = async () => {\n  try {\n    const text = await navigator.clipboard.readText();\n    if (text) {\n      tokenInput.value = text;\n      message.success(t('settings.cookie.message.pasteSuccess'));\n    }\n  } catch (error) {\n    console.error('粘贴失败:', error);\n    message.error(t('settings.cookie.message.pasteError'));\n  }\n};\n</script>\n\n<template>\n  <n-modal\n    :show=\"show\"\n    preset=\"dialog\"\n    :title=\"t('settings.cookie.title')\"\n    @update:show=\"emit('update:show', $event)\"\n  >\n    <template #header>\n      <div class=\"flex items-center gap-2\">\n        <i class=\"ri-key-line\"></i>\n        <span>{{ t('settings.cookie.title') }}</span>\n      </div>\n    </template>\n\n    <div class=\"space-y-4\">\n      <div>\n        <div class=\"text-sm text-gray-600 dark:text-gray-400 mb-2\">\n          {{ t('settings.cookie.description') }}\n        </div>\n\n        <div class=\"relative\">\n          <n-input\n            v-model:value=\"tokenInput\"\n            type=\"textarea\"\n            :placeholder=\"t('settings.cookie.placeholder')\"\n            :rows=\"6\"\n            :autosize=\"{ minRows: 4, maxRows: 8 }\"\n            style=\"font-family: monospace; font-size: 12px\"\n            class=\"cookie-input\"\n          />\n\n          <!-- 工具按钮 -->\n          <div class=\"absolute top-2 right-2 flex gap-1\">\n            <n-button\n              size=\"tiny\"\n              quaternary\n              @click=\"handlePaste\"\n              :title=\"t('settings.cookie.action.paste')\"\n            >\n              <i class=\"ri-clipboard-line\"></i>\n            </n-button>\n            <n-button\n              size=\"tiny\"\n              quaternary\n              @click=\"handleClear\"\n              :title=\"t('settings.cookie.action.clear')\"\n            >\n              <i class=\"ri-delete-bin-line\"></i>\n            </n-button>\n          </div>\n        </div>\n      </div>\n\n      <!-- 帮助信息 -->\n      <div class=\"text-xs text-gray-500 space-y-1\">\n        <p>• {{ t('settings.cookie.help.format') }}</p>\n        <p>• {{ t('settings.cookie.help.source') }}</p>\n        <p>• {{ t('settings.cookie.help.storage') }}</p>\n      </div>\n\n      <!-- Cookie长度提示 -->\n      <div v-if=\"tokenInput\" class=\"text-xs text-gray-400\">\n        {{ t('settings.cookie.info.length', { length: tokenInput.length }) }}\n      </div>\n    </div>\n\n    <template #action>\n      <div class=\"flex gap-2\">\n        <n-button @click=\"handleClose\">\n          {{ t('common.cancel') }}\n        </n-button>\n        <n-button\n          type=\"primary\"\n          @click=\"handleSave\"\n          :disabled=\"!tokenInput.trim()\"\n          :loading=\"isLoading\"\n        >\n          {{ t('settings.cookie.action.save') }}\n        </n-button>\n      </div>\n    </template>\n  </n-modal>\n</template>\n\n<style lang=\"scss\" scoped>\n.cookie-input {\n  :deep(.n-input__textarea) {\n    font-family:\n      'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Fira Mono', 'Droid Sans Mono', 'Consolas',\n      monospace;\n    line-height: 1.4;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/settings/MusicSourceSettings.vue",
    "content": "<template>\n  <ResponsiveModal\n    v-model=\"visible\"\n    :title=\"t('settings.playback.musicSources')\"\n    @close=\"handleCancel\"\n  >\n    <div class=\"flex flex-col h-full\">\n      <!-- Tabs Header -->\n      <div class=\"flex p-0.5 mb-3 bg-gray-100 dark:bg-white/5 rounded-lg shrink-0\">\n        <button\n          v-for=\"tab in tabs\"\n          :key=\"tab.key\"\n          class=\"flex-1 py-1 text-xs font-medium rounded-md transition-all duration-200\"\n          :class=\"[\n            activeTab === tab.key\n              ? 'bg-white dark:bg-white/10 text-gray-900 dark:text-white shadow-sm'\n              : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'\n          ]\"\n          @click=\"activeTab = tab.key\"\n        >\n          {{ tab.label }}\n        </button>\n      </div>\n\n      <!-- Tab Content -->\n      <div class=\"h-[400px] relative shrink-0\">\n        <Transition name=\"fade\" mode=\"out-in\">\n          <div :key=\"activeTab\" class=\"h-full overflow-y-auto overscroll-contain\">\n            <!-- Sources Tab -->\n            <div v-if=\"activeTab === 'sources'\" class=\"space-y-3 pb-2\">\n              <p class=\"text-xs text-gray-500 dark:text-gray-400 px-1\">\n                {{ t('settings.playback.musicSourcesDesc') }}\n              </p>\n\n              <div class=\"grid grid-cols-2 md:grid-cols-3 gap-2\">\n                <!-- Standard Sources -->\n                <div\n                  v-for=\"source in MUSIC_SOURCES\"\n                  :key=\"source.key\"\n                  class=\"group relative flex items-center p-2.5 rounded-xl border transition-all duration-200 cursor-pointer\"\n                  :class=\"[\n                    isSourceSelected(source.key)\n                      ? 'bg-emerald-50/50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20'\n                      : 'bg-white dark:bg-white/5 border-gray-100 dark:border-white/5 hover:bg-gray-50 dark:hover:bg-white/10'\n                  ]\"\n                  @click=\"toggleSource(source.key)\"\n                >\n                  <div\n                    class=\"flex items-center justify-center w-8 h-8 rounded-full mr-2.5 transition-colors shrink-0\"\n                    :style=\"{\n                      backgroundColor: isSourceSelected(source.key) ? source.color : 'transparent',\n                      color: isSourceSelected(source.key) ? '#fff' : source.color\n                    }\"\n                    :class=\"{ 'bg-gray-100 dark:bg-white/10': !isSourceSelected(source.key) }\"\n                  >\n                    <i class=\"ri-music-2-fill text-base\"></i>\n                  </div>\n                  \n                  <div class=\"flex-1 min-w-0\">\n                    <div class=\"flex items-center justify-between\">\n                      <span class=\"font-semibold text-gray-900 dark:text-white text-sm truncate\">{{ source.key }}</span>\n                      <div\n                        class=\"w-4 h-4 rounded-full border flex items-center justify-center transition-colors shrink-0 ml-1\"\n                        :class=\"[\n                          isSourceSelected(source.key)\n                            ? 'bg-emerald-500 border-emerald-500'\n                            : 'border-gray-300 dark:border-gray-600'\n                        ]\"\n                      >\n                        <i v-if=\"isSourceSelected(source.key)\" class=\"ri-check-line text-white text-xs scale-75\"></i>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n\n                <!-- LX Music Source -->\n                <div\n                  class=\"group relative flex items-center p-2.5 rounded-xl border transition-all duration-200 cursor-pointer\"\n                  :class=\"[\n                    isSourceSelected('lxMusic')\n                      ? 'bg-emerald-50/50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20'\n                      : 'bg-white dark:bg-white/5 border-gray-100 dark:border-white/5 hover:bg-gray-50 dark:hover:bg-white/10',\n                    { 'opacity-60 cursor-not-allowed': !activeLxApiId || lxMusicApis.length === 0 }\n                  ]\"\n                  @click=\"toggleSource('lxMusic')\"\n                >\n                  <div\n                    class=\"flex items-center justify-center w-8 h-8 rounded-full mr-2.5 transition-colors shrink-0\"\n                    :class=\"[\n                      isSourceSelected('lxMusic')\n                        ? 'bg-emerald-500 text-white'\n                        : 'bg-gray-100 dark:bg-white/10 text-emerald-500'\n                    ]\"\n                  >\n                    <i class=\"ri-netease-cloud-music-fill text-base\"></i>\n                  </div>\n                  \n                  <div class=\"flex-1 min-w-0\">\n                    <div class=\"flex items-center justify-between\">\n                      <span class=\"font-semibold text-gray-900 dark:text-white text-sm truncate\">落雪音源</span>\n                      <div\n                        class=\"w-4 h-4 rounded-full border flex items-center justify-center transition-colors shrink-0 ml-1\"\n                        :class=\"[\n                          isSourceSelected('lxMusic')\n                            ? 'bg-emerald-500 border-emerald-500'\n                            : 'border-gray-300 dark:border-gray-600'\n                        ]\"\n                      >\n                        <i v-if=\"isSourceSelected('lxMusic')\" class=\"ri-check-line text-white text-xs scale-75\"></i>\n                      </div>\n                    </div>\n                    <p class=\"text-[10px] text-gray-500 mt-0.5 truncate\">\n                      {{ activeLxApiId && lxMusicScriptInfo ? lxMusicScriptInfo.name : t('settings.playback.lxMusic.scripts.notConfigured') }}\n                    </p>\n                  </div>\n                </div>\n\n                <!-- Custom API Source -->\n                <div\n                  class=\"group relative flex items-center p-2.5 rounded-xl border transition-all duration-200 cursor-pointer\"\n                  :class=\"[\n                    isSourceSelected('custom')\n                      ? 'bg-emerald-50/50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20'\n                      : 'bg-white dark:bg-white/5 border-gray-100 dark:border-white/5 hover:bg-gray-50 dark:hover:bg-white/10',\n                    { 'opacity-60 cursor-not-allowed': !settingsStore.setData.customApiPlugin }\n                  ]\"\n                  @click=\"toggleSource('custom')\"\n                >\n                  <div\n                    class=\"flex items-center justify-center w-8 h-8 rounded-full mr-2.5 transition-colors shrink-0\"\n                    :class=\"[\n                      isSourceSelected('custom')\n                        ? 'bg-violet-500 text-white'\n                        : 'bg-gray-100 dark:bg-white/10 text-violet-500'\n                    ]\"\n                  >\n                    <i class=\"ri-plug-fill text-base\"></i>\n                  </div>\n                  \n                  <div class=\"flex-1 min-w-0\">\n                    <div class=\"flex items-center justify-between\">\n                      <span class=\"font-semibold text-gray-900 dark:text-white text-sm truncate\">{{ t('settings.playback.sourceLabels.custom') }}</span>\n                      <div\n                        class=\"w-4 h-4 rounded-full border flex items-center justify-center transition-colors shrink-0 ml-1\"\n                        :class=\"[\n                          isSourceSelected('custom')\n                            ? 'bg-emerald-500 border-emerald-500'\n                            : 'border-gray-300 dark:border-gray-600'\n                        ]\"\n                      >\n                        <i v-if=\"isSourceSelected('custom')\" class=\"ri-check-line text-white text-xs scale-75\"></i>\n                      </div>\n                    </div>\n                    <p class=\"text-[10px] text-gray-500 mt-0.5 truncate\">\n                      {{ settingsStore.setData.customApiPlugin ? t('settings.playback.customApi.status.imported') : t('settings.playback.customApi.status.notImported') }}\n                    </p>\n                  </div>\n                </div>\n              </div>\n            </div>\n\n            <!-- LX Music Management Tab -->\n            <div v-else-if=\"activeTab === 'lxMusic'\" class=\"space-y-3 pb-2\">\n              <div class=\"flex justify-between items-center mb-1\">\n                <h3 class=\"text-xs font-medium text-gray-500 dark:text-gray-400\">{{ t('settings.playback.lxMusic.scripts.title') }}</h3>\n                <button\n                  @click=\"importLxMusicScript\"\n                  class=\"flex items-center gap-1 px-2.5 py-1 bg-emerald-500 hover:bg-emerald-600 text-white text-xs font-medium rounded-lg transition-colors\"\n                >\n                  <i class=\"ri-upload-line\"></i>\n                  {{ t('settings.playback.lxMusic.scripts.importLocal') }}\n                </button>\n              </div>\n\n              <!-- Script List -->\n              <div v-if=\"lxMusicApis.length > 0\" class=\"grid grid-cols-1 md:grid-cols-3 gap-2\">\n                <div\n                  v-for=\"api in lxMusicApis\"\n                  :key=\"api.id\"\n                  class=\"flex items-center p-2.5 rounded-xl border transition-all duration-200\"\n                  :class=\"[\n                    activeLxApiId === api.id\n                      ? 'bg-emerald-50/50 dark:bg-emerald-500/10 border-emerald-200 dark:border-emerald-500/20'\n                      : 'bg-white dark:bg-white/5 border-gray-100 dark:border-white/5'\n                  ]\"\n                >\n                  <div class=\"relative flex items-center justify-center w-4 h-4 mr-3\">\n                    <input\n                      type=\"radio\"\n                      :checked=\"activeLxApiId === api.id\"\n                      class=\"peer appearance-none w-4 h-4 rounded-full border border-gray-300 dark:border-gray-600 checked:border-emerald-500 checked:bg-emerald-500 transition-colors cursor-pointer\"\n                      @change=\"setActiveLxApi(api.id)\"\n                    />\n                    <i class=\"ri-check-line absolute text-white text-[10px] pointer-events-none opacity-0 peer-checked:opacity-100 transition-opacity\"></i>\n                  </div>\n\n                  <div class=\"flex-1 min-w-0 mr-2\">\n                    <div class=\"flex items-center gap-2\">\n                      <span v-if=\"editingScriptId !== api.id\" class=\"font-medium text-sm text-gray-900 dark:text-white truncate\">\n                        {{ api.name }}\n                      </span>\n                      <input\n                        v-else\n                        v-model=\"editingName\"\n                        ref=\"renameInputRef\"\n                        class=\"w-full px-2 py-0.5 text-sm bg-white dark:bg-black/20 border border-emerald-500 rounded focus:outline-none\"\n                        @blur=\"saveScriptName(api.id)\"\n                        @keyup.enter=\"saveScriptName(api.id)\"\n                      />\n                      \n                      <button\n                        v-if=\"editingScriptId !== api.id\"\n                        class=\"text-gray-400 hover:text-emerald-500 transition-colors\"\n                        @click=\"startRenaming(api)\"\n                      >\n                        <i class=\"ri-edit-line text-sm\"></i>\n                      </button>\n                    </div>\n                    <div class=\"flex items-center gap-2 mt-0.5\">\n                      <span v-if=\"api.info.version\" class=\"text-[10px] text-gray-500 bg-gray-100 dark:bg-white/10 px-1.5 py-0.5 rounded\">\n                        v{{ api.info.version }}\n                      </span>\n                    </div>\n                  </div>\n\n                  <button\n                    class=\"p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-lg transition-colors\"\n                    @click=\"removeLxApi(api.id)\"\n                  >\n                    <i class=\"ri-delete-bin-line text-sm\"></i>\n                  </button>\n                </div>\n              </div>\n              \n              <div v-else class=\"py-6 text-center text-xs text-gray-400 bg-gray-50 dark:bg-white/5 rounded-xl border border-dashed border-gray-200 dark:border-white/10\">\n                <p>{{ t('settings.playback.lxMusic.scripts.empty') }}</p>\n              </div>\n\n              <!-- URL Import -->\n              <div class=\"mt-4 pt-4 border-t border-gray-100 dark:border-white/5\">\n                <h4 class=\"text-xs font-medium mb-2 text-gray-900 dark:text-white\">{{ t('settings.playback.lxMusic.scripts.importOnline') }}</h4>\n                <div class=\"flex gap-2\">\n                  <input\n                    v-model=\"lxScriptUrl\"\n                    :placeholder=\"t('settings.playback.lxMusic.scripts.urlPlaceholder')\"\n                    class=\"flex-1 px-3 py-1.5 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-xs focus:outline-none focus:border-emerald-500 transition-colors\"\n                    :disabled=\"isImportingFromUrl\"\n                  />\n                  <button\n                    @click=\"importLxMusicScriptFromUrl\"\n                    class=\"px-3 py-1.5 bg-emerald-500 hover:bg-emerald-600 disabled:opacity-50 disabled:cursor-not-allowed text-white text-xs font-medium rounded-xl transition-colors flex items-center gap-1\"\n                    :disabled=\"!lxScriptUrl.trim() || isImportingFromUrl\"\n                  >\n                    <i v-if=\"isImportingFromUrl\" class=\"ri-loader-4-line animate-spin\"></i>\n                    <i v-else class=\"ri-download-line\"></i>\n                    {{ t('settings.playback.lxMusic.scripts.importBtn') }}\n                  </button>\n                </div>\n              </div>\n            </div>\n\n            <!-- Custom API Tab -->\n            <div v-else-if=\"activeTab === 'customApi'\" class=\"flex flex-col items-center justify-center py-6 text-center h-full\">\n              <div class=\"w-12 h-12 bg-violet-100 dark:bg-violet-500/20 text-violet-500 rounded-xl flex items-center justify-center mb-3\">\n                <i class=\"ri-plug-fill text-2xl\"></i>\n              </div>\n              \n              <h3 class=\"text-base font-semibold text-gray-900 dark:text-white mb-1\">\n                {{ t('settings.playback.customApi.sectionTitle') }}\n              </h3>\n              <p class=\"text-gray-500 dark:text-gray-400 text-xs mb-4 max-w-xs mx-auto\">\n                {{ t('settings.playback.lxMusic.scripts.importHint') }}\n              </p>\n\n              <button\n                @click=\"importPlugin\"\n                class=\"px-5 py-2 bg-violet-500 hover:bg-violet-600 text-white text-sm font-medium rounded-xl transition-colors flex items-center gap-2 shadow-lg shadow-violet-500/20\"\n              >\n                <i class=\"ri-upload-line\"></i>\n                {{ t('settings.playback.customApi.importConfig') }}\n              </button>\n\n              <div v-if=\"settingsStore.setData.customApiPluginName\" class=\"mt-4 flex items-center gap-2 px-3 py-1.5 bg-green-50 dark:bg-green-500/10 text-green-600 dark:text-green-400 rounded-lg text-xs\">\n                <i class=\"ri-check-circle-fill\"></i>\n                <span>{{ t('settings.playback.customApi.currentSource') }}: <b>{{ settingsStore.setData.customApiPluginName }}</b></span>\n              </div>\n              \n              <div v-else class=\"mt-4 text-xs text-gray-400\">\n                {{ t('settings.playback.customApi.notImported') }}\n              </div>\n            </div>\n          </div>\n        </Transition>\n      </div>\n    </div>\n\n    <!-- Footer Actions -->\n    <template #footer>\n      <div class=\"flex justify-end gap-2\">\n        <button\n          class=\"px-4 py-2 text-xs font-medium text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-white/10 rounded-lg transition-colors\"\n          @click=\"handleCancel\"\n        >\n          {{ t('common.cancel') }}\n        </button>\n        <button\n          class=\"px-4 py-2 text-xs font-medium text-white bg-emerald-500 hover:bg-emerald-600 rounded-lg shadow-lg shadow-emerald-500/20 transition-all active:scale-95\"\n          @click=\"handleConfirm\"\n        >\n          {{ t('common.confirm') }}\n        </button>\n      </div>\n    </template>\n  </ResponsiveModal>\n</template>\n\n<script setup lang=\"ts\">\nimport { useMessage } from 'naive-ui';\nimport { computed, nextTick, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport ResponsiveModal from '@/components/common/ResponsiveModal.vue';\nimport {\n  initLxMusicRunner,\n  parseScriptInfo,\n  setLxMusicRunner\n} from '@/services/LxMusicSourceRunner';\nimport { useSettingsStore } from '@/store';\nimport type { LxMusicScriptConfig, LxScriptInfo, LxSourceKey } from '@/types/lxMusic';\nimport { type Platform } from '@/types/music';\n\n// ==================== 类型定义 ====================\ntype ExtendedPlatform = Platform | 'custom' | 'lxMusic';\n\ninterface MusicSourceConfig {\n  key: string;\n  description?: string;\n  color: string;\n  disabled?: boolean;\n}\n\n// ==================== 音源配置 ====================\nconst MUSIC_SOURCES: MusicSourceConfig[] = [\n  { key: 'migu', color: '#ff6600' },\n  { key: 'kugou', color: '#2979ff' },\n  { key: 'kuwo', color: '#ff8c00' },\n  { key: 'pyncmd', color: '#ec4141' },\n  { key: 'bilibili', color: '#00a1d6' }\n];\n\n// ==================== Props & Emits ====================\nconst props = defineProps({\n  show: {\n    type: Boolean,\n    default: false\n  },\n  sources: {\n    type: Array as () => ExtendedPlatform[],\n    default: () => ['migu', 'kugou', 'kuwo', 'pyncmd', 'bilibili'] as ExtendedPlatform[]\n  }\n});\n\nconst emit = defineEmits(['update:show', 'update:sources']);\n\n// ==================== 状态管理 ====================\nconst { t } = useI18n();\nconst settingsStore = useSettingsStore();\nconst message = useMessage();\nconst visible = ref(props.show);\nconst selectedSources = ref<ExtendedPlatform[]>([...props.sources]);\nconst activeTab = ref('sources');\n\nconst tabs = computed(() => [\n  { key: 'sources', label: t('settings.playback.lxMusic.tabs.sources') },\n  { key: 'lxMusic', label: t('settings.playback.lxMusic.tabs.lxMusic') },\n  { key: 'customApi', label: t('settings.playback.lxMusic.tabs.customApi') }\n]);\n\n// 落雪音源列表（从 store 中的脚本解析）\nconst lxMusicApis = computed<LxMusicScriptConfig[]>(() => {\n  const scripts = settingsStore.setData.lxMusicScripts || [];\n  return scripts;\n});\n\n// 当前激活的音源 ID\nconst activeLxApiId = computed<string | null>({\n  get: () => settingsStore.setData.activeLxMusicApiId || null,\n  set: (id: string | null) => {\n    settingsStore.setSetData({ activeLxMusicApiId: id });\n  }\n});\n\n// 落雪音源脚本信息（保持向后兼容）\nconst lxMusicScriptInfo = computed<LxScriptInfo | null>(() => {\n  const activeId = activeLxApiId.value;\n  if (!activeId) {\n    return null;\n  }\n  const activeApi = lxMusicApis.value.find((api: LxMusicScriptConfig) => api.id === activeId);\n  return activeApi?.info || null;\n});\n\n// URL 导入相关状态\nconst lxScriptUrl = ref('');\nconst isImportingFromUrl = ref(false);\n\n// 重命名相关状态\nconst editingScriptId = ref<string | null>(null);\nconst editingName = ref('');\nconst renameInputRef = ref<HTMLInputElement | null>(null);\n\n// ==================== 计算属性 ====================\nconst isSourceSelected = (sourceKey: string): boolean => {\n  return selectedSources.value.includes(sourceKey as ExtendedPlatform);\n};\n\n// ==================== 方法 ====================\n/**\n * 切换音源选择状态\n */\nconst toggleSource = (sourceKey: string) => {\n  // 检查是否是自定义API且未导入\n  if (sourceKey === 'custom' && !settingsStore.setData.customApiPlugin) {\n    message.warning(t('settings.playback.customApi.enableHint'));\n    activeTab.value = 'customApi';\n    return;\n  }\n\n  // 检查是否是落雪音源且未配置\n  if (sourceKey === 'lxMusic') {\n    if (lxMusicApis.value.length === 0) {\n      message.warning(t('settings.playback.lxMusic.scripts.noScriptWarning'));\n      activeTab.value = 'lxMusic';\n      return;\n    }\n    if (!activeLxApiId.value) {\n      message.warning(t('settings.playback.lxMusic.scripts.noSelectionWarning'));\n      activeTab.value = 'lxMusic';\n      return;\n    }\n  }\n\n  const index = selectedSources.value.indexOf(sourceKey as ExtendedPlatform);\n  if (index > -1) {\n    // 至少保留一个音源\n    if (selectedSources.value.length <= 1) {\n      message.warning(t('settings.playback.musicSourcesMinWarning'));\n      return;\n    }\n    selectedSources.value.splice(index, 1);\n  } else {\n    selectedSources.value.push(sourceKey as ExtendedPlatform);\n  }\n};\n\n/**\n * 导入自定义API插件\n */\nconst importPlugin = async () => {\n  try {\n    const result = await window.api.importCustomApiPlugin();\n    if (result && result.name && result.content) {\n      settingsStore.setCustomApiPlugin(result);\n      message.success(t('settings.playback.customApi.importSuccess', { name: result.name }));\n\n      // 导入成功后自动勾选\n      if (!selectedSources.value.includes('custom')) {\n        selectedSources.value.push('custom');\n      }\n    }\n  } catch (error: any) {\n    message.error(t('settings.playback.customApi.importFailed', { message: error.message }));\n  }\n};\n\n/**\n * 导入落雪音源脚本\n */\nconst importLxMusicScript = async () => {\n  try {\n    const result = await window.api.importLxMusicScript();\n    if (result && result.content) {\n      await addLxMusicScript(result.content);\n    }\n  } catch (error: any) {\n    console.error('导入落雪音源脚本失败:', error);\n    message.error(`${t('common.error')}：${error.message}`);\n  }\n};\n\n/**\n * 添加落雪音源脚本到列表\n */\nconst addLxMusicScript = async (scriptContent: string) => {\n  // 解析脚本信息\n  const scriptInfo = parseScriptInfo(scriptContent);\n\n  // 尝试初始化执行器以验证脚本\n  try {\n    const runner = await initLxMusicRunner(scriptContent);\n    const sources = runner.getSources();\n    const sourceKeys = Object.keys(sources) as LxSourceKey[];\n\n    // 生成唯一 ID\n    const id = `lx_api_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n\n    // 创建新的脚本配置\n    const newApiConfig: LxMusicScriptConfig = {\n      id,\n      name: scriptInfo.name,\n      script: scriptContent,\n      info: scriptInfo,\n      sources: sourceKeys,\n      enabled: true,\n      createdAt: Date.now()\n    };\n\n    // 添加到列表\n    const scripts = [...(settingsStore.setData.lxMusicScripts || []), newApiConfig];\n\n    settingsStore.setSetData({\n      lxMusicScripts: scripts,\n      activeLxMusicApiId: id // 自动激活新添加的音源\n    });\n\n    message.success(`${t('common.success')}：${scriptInfo.name}`);\n\n    // 导入成功后自动勾选\n    if (!selectedSources.value.includes('lxMusic')) {\n      selectedSources.value.push('lxMusic');\n    }\n  } catch (initError: any) {\n    console.error('[MusicSourceSettings] 落雪音源脚本初始化失败:', initError);\n    message.error(`${t('common.error')}：${initError.message}`);\n  }\n};\n\n/**\n * 设置激活的落雪音源\n */\nconst setActiveLxApi = async (apiId: string) => {\n  const api = lxMusicApis.value.find((a: LxMusicScriptConfig) => a.id === apiId);\n  if (!api) {\n    message.error(t('settings.playback.lxMusic.scripts.notFound'));\n    return;\n  }\n\n  try {\n    // 清除旧的 runner\n    setLxMusicRunner(null);\n\n    // 初始化新选中的脚本\n    await initLxMusicRunner(api.script);\n\n    // 更新激活的音源 ID\n    activeLxApiId.value = apiId;\n\n    // 确保 lxMusic 在已选音源中\n    if (!selectedSources.value.includes('lxMusic')) {\n      selectedSources.value.push('lxMusic');\n    }\n\n    message.success(t('settings.playback.lxMusic.scripts.switched', { name: api.name }));\n  } catch (error: any) {\n    console.error('[MusicSourceSettings] 切换落雪音源失败:', error);\n    message.error(`${t('common.error')}：${error.message}`);\n  }\n};\n\n/**\n * 删除落雪音源\n */\nconst removeLxApi = (apiId: string) => {\n  const scripts = [...(settingsStore.setData.lxMusicScripts || [])];\n  const index = scripts.findIndex((s) => s.id === apiId);\n\n  if (index === -1) return;\n\n  const removedScript = scripts[index];\n  scripts.splice(index, 1);\n\n  // 更新 store\n  settingsStore.setSetData({\n    lxMusicScripts: scripts\n  });\n\n  // 如果删除的是当前激活的音源\n  if (activeLxApiId.value === apiId) {\n    // 自动选择下一个可用音源，或者清空\n    if (scripts.length > 0) {\n      setActiveLxApi(scripts[0].id);\n    } else {\n      setLxMusicRunner(null);\n      settingsStore.setSetData({ activeLxMusicApiId: null });\n      // 从已选音源中移除 lxMusic\n      const srcIndex = selectedSources.value.indexOf('lxMusic');\n      if (srcIndex > -1) {\n        selectedSources.value.splice(srcIndex, 1);\n      }\n    }\n  }\n\n  message.success(t('settings.playback.lxMusic.scripts.deleted', { name: removedScript.name }));\n};\n\n/**\n * 从 URL 导入落雪音源脚本\n */\nconst importLxMusicScriptFromUrl = async () => {\n  const url = lxScriptUrl.value.trim();\n  if (!url) {\n    message.warning(t('settings.playback.lxMusic.scripts.enterUrl'));\n    return;\n  }\n\n  // 验证 URL 格式\n  try {\n    new URL(url);\n  } catch {\n    message.error(t('settings.playback.lxMusic.scripts.invalidUrl'));\n    return;\n  }\n\n  isImportingFromUrl.value = true;\n\n  try {\n    // 下载脚本内容\n    const response = await fetch(url);\n    if (!response.ok) {\n      throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n    }\n\n    const content = await response.text();\n\n    // 验证脚本格式 - 检查是否包含 lx-music 脚本的特征\n    // 1. 检查是否有头部注释块（包含 @name、@version 等）\n    const hasHeaderComment = /^\\/\\*+[\\s\\S]*?@name[\\s\\S]*?\\*\\//.test(content);\n    // 2. 检查是否使用 lx API（lx.on 或 lx.send）\n    const hasLxApi = content.includes('lx.on(') || content.includes('lx.send(');\n\n    if (!hasHeaderComment && !hasLxApi) {\n      throw new Error(t('settings.playback.lxMusic.scripts.invalidScript'));\n    }\n\n    // 使用统一的添加方法\n    await addLxMusicScript(content);\n\n    // 清空 URL 输入框\n    lxScriptUrl.value = '';\n  } catch (error: any) {\n    console.error('从 URL 导入落雪音源脚本失败:', error);\n    message.error(`${t('settings.playback.lxMusic.scripts.importOnline')} ${t('common.error')}：${error.message}`);\n  } finally {\n    isImportingFromUrl.value = false;\n  }\n};\n\n/**\n * 开始重命名\n */\nconst startRenaming = (api: LxMusicScriptConfig) => {\n  editingScriptId.value = api.id;\n  editingName.value = api.name;\n  nextTick(() => {\n    renameInputRef.value?.focus();\n  });\n};\n\n/**\n * 保存脚本名称\n */\nconst saveScriptName = (apiId: string) => {\n  if (!editingName.value.trim()) {\n    message.warning(t('settings.playback.lxMusic.scripts.nameRequired'));\n    return;\n  }\n\n  const scripts = [...(settingsStore.setData.lxMusicScripts || [])];\n  const index = scripts.findIndex((s) => s.id === apiId);\n\n  if (index > -1) {\n    scripts[index] = {\n      ...scripts[index],\n      name: editingName.value.trim()\n    };\n\n    settingsStore.setSetData({\n      lxMusicScripts: scripts\n    });\n\n    message.success(t('settings.playback.lxMusic.scripts.renameSuccess'));\n  }\n\n  editingScriptId.value = null;\n  editingName.value = '';\n};\n\n/**\n * 确认选择\n */\nconst handleConfirm = () => {\n  const defaultPlatforms: ExtendedPlatform[] = ['migu', 'kugou', 'kuwo', 'pyncmd', 'bilibili'];\n  const valuesToEmit =\n    selectedSources.value.length > 0 ? [...new Set(selectedSources.value)] : defaultPlatforms;\n  emit('update:sources', valuesToEmit);\n  visible.value = false;\n};\n\n/**\n * 取消选择\n */\nconst handleCancel = () => {\n  selectedSources.value = [...props.sources];\n  visible.value = false;\n};\n\n// ==================== 监听器 ====================\n// 监听自定义插件内容变化\nwatch(\n  () => settingsStore.setData.customApiPlugin,\n  (newPluginContent: any) => {\n    if (!newPluginContent) {\n      const index = selectedSources.value.indexOf('custom');\n      if (index > -1) {\n        selectedSources.value.splice(index, 1);\n      }\n    }\n  }\n);\n\n// 监听落雪音源列表变化\nwatch(\n  [() => lxMusicApis.value.length, () => activeLxApiId.value],\n  ([apiCount, activeId]) => {\n    // 如果没有音源或没有激活的音源，自动从已选音源中移除 lxMusic\n    if (apiCount === 0 || !activeId) {\n      const index = selectedSources.value.indexOf('lxMusic');\n      if (index > -1) {\n        selectedSources.value.splice(index, 1);\n      }\n    }\n  },\n  { deep: true }\n);\n\n// 同步外部show属性变化\nwatch(\n  () => props.show,\n  (newVal: boolean) => {\n    visible.value = newVal;\n  }\n);\n\n// 同步内部visible变化\nwatch(\n  () => visible.value,\n  (newVal: boolean) => {\n    emit('update:show', newVal);\n  }\n);\n\n// 同步外部sources属性变化\nwatch(\n  () => props.sources,\n  (newVal: ExtendedPlatform[]) => {\n    selectedSources.value = [...newVal];\n  },\n  { deep: true }\n);\n</script>\n\n<style scoped>\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.2s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/settings/ProxySettings.vue",
    "content": "<template>\n  <n-modal\n    v-model:show=\"visible\"\n    preset=\"dialog\"\n    :title=\"t('settings.network.proxy')\"\n    :positive-text=\"t('common.confirm')\"\n    :negative-text=\"t('common.cancel')\"\n    :show-icon=\"false\"\n    @positive-click=\"handleProxyConfirm\"\n    @negative-click=\"handleCancel\"\n  >\n    <n-form\n      ref=\"formRef\"\n      :model=\"proxyForm\"\n      :rules=\"proxyRules\"\n      label-placement=\"left\"\n      label-width=\"80\"\n      require-mark-placement=\"right-hanging\"\n    >\n      <n-form-item :label=\"t('settings.network.proxy')\" path=\"protocol\">\n        <n-select\n          v-model:value=\"proxyForm.protocol\"\n          :options=\"[\n            { label: 'HTTP', value: 'http' },\n            { label: 'HTTPS', value: 'https' },\n            { label: 'SOCKS5', value: 'socks5' }\n          ]\"\n        />\n      </n-form-item>\n      <n-form-item :label=\"t('settings.network.proxyHost')\" path=\"host\">\n        <n-input\n          v-model:value=\"proxyForm.host\"\n          :placeholder=\"t('settings.network.proxyHostPlaceholder')\"\n        />\n      </n-form-item>\n      <n-form-item :label=\"t('settings.network.proxyPort')\" path=\"port\">\n        <n-input-number\n          v-model:value=\"proxyForm.port\"\n          :placeholder=\"t('settings.network.proxyPortPlaceholder')\"\n          :min=\"1\"\n          :max=\"65535\"\n        />\n      </n-form-item>\n    </n-form>\n  </n-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport type { FormRules } from 'naive-ui';\nimport { useMessage } from 'naive-ui';\nimport { defineEmits, defineProps, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst props = defineProps({\n  show: {\n    type: Boolean,\n    default: false\n  },\n  config: {\n    type: Object,\n    default: () => ({\n      protocol: 'http',\n      host: '127.0.0.1',\n      port: 7890\n    })\n  }\n});\n\nconst emit = defineEmits(['update:show', 'confirm']);\n\nconst { t } = useI18n();\nconst message = useMessage();\nconst formRef = ref();\n\nconst visible = ref(props.show);\nconst proxyForm = ref({\n  protocol: props.config.protocol || 'http',\n  host: props.config.host || '127.0.0.1',\n  port: props.config.port || 7890\n});\n\nconst proxyRules: FormRules = {\n  protocol: {\n    required: true,\n    message: t('settings.validation.selectProxyProtocol'),\n    trigger: ['blur', 'change']\n  },\n  host: {\n    required: true,\n    message: t('settings.validation.proxyHost'),\n    trigger: ['blur', 'change'],\n    validator: (_rule, value) => {\n      if (!value) return false;\n      // 简单的IP或域名验证\n      const ipRegex =\n        /^(\\d{1,3}\\.){3}\\d{1,3}$|^localhost$|^[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$/;\n      return ipRegex.test(value);\n    }\n  },\n  port: {\n    required: true,\n    message: t('settings.validation.portNumber'),\n    trigger: ['blur', 'change'],\n    validator: (_rule, value) => {\n      return value >= 1 && value <= 65535;\n    }\n  }\n};\n\n// 同步外部show属性变化\nwatch(\n  () => props.show,\n  (newVal) => {\n    visible.value = newVal;\n  }\n);\n\n// 同步内部visible变化\nwatch(\n  () => visible.value,\n  (newVal) => {\n    emit('update:show', newVal);\n  }\n);\n\n// 同步外部config变化\nwatch(\n  () => props.config,\n  (newVal) => {\n    proxyForm.value = {\n      protocol: newVal.protocol || 'http',\n      host: newVal.host || '127.0.0.1',\n      port: newVal.port || 7890\n    };\n  },\n  { deep: true }\n);\n\nconst handleProxyConfirm = async () => {\n  try {\n    await formRef.value?.validate();\n    emit('confirm', { ...proxyForm.value });\n    visible.value = false;\n    message.success(t('settings.network.messages.proxySuccess'));\n  } catch (err) {\n    console.error('代理设置验证失败:', err);\n    message.error(t('settings.network.messages.proxyError'));\n  }\n};\n\nconst handleCancel = () => {\n  visible.value = false;\n};\n</script>\n"
  },
  {
    "path": "src/renderer/components/settings/ServerSetting.vue",
    "content": "<template>\n  <n-modal\n    v-model:show=\"visible\"\n    preset=\"card\"\n    :title=\"t('settings.remoteControl.title')\"\n    class=\"remote-control-modal\"\n    style=\"max-width: 650px; width: 100%\"\n  >\n    <n-scrollbar>\n      <div class=\"remote-control-setting\">\n        <n-form label-placement=\"left\" label-width=\"auto\" :style=\"{ maxWidth: '640px' }\">\n          <n-form-item :label=\"t('settings.remoteControl.enable')\">\n            <n-switch v-model:value=\"remoteControlConfig.enabled\" />\n          </n-form-item>\n\n          <n-form-item :label=\"t('settings.remoteControl.port')\">\n            <n-input-number\n              v-model:value=\"remoteControlConfig.port\"\n              :min=\"1024\"\n              :max=\"65535\"\n              :disabled=\"!remoteControlConfig.enabled\"\n            />\n          </n-form-item>\n\n          <n-form-item :label=\"t('settings.remoteControl.allowedIps')\">\n            <div class=\"allowed-ips-container\">\n              <div\n                v-for=\"(_, index) in remoteControlConfig.allowedIps\"\n                :key=\"index\"\n                class=\"ip-item\"\n              >\n                <n-input\n                  v-model:value=\"remoteControlConfig.allowedIps[index]\"\n                  :disabled=\"!remoteControlConfig.enabled\"\n                />\n                <n-button\n                  quaternary\n                  circle\n                  type=\"error\"\n                  :disabled=\"!remoteControlConfig.enabled\"\n                  @click=\"removeIp(index)\"\n                >\n                  <template #icon>\n                    <n-icon><i class=\"ri-delete-bin-line\"></i></n-icon>\n                  </template>\n                </n-button>\n              </div>\n              <n-button\n                secondary\n                size=\"small\"\n                :disabled=\"!remoteControlConfig.enabled\"\n                @click=\"addIp\"\n              >\n                <template #icon>\n                  <n-icon><i class=\"ri-add-line\"></i></n-icon>\n                </template>\n                {{ t('settings.remoteControl.addIp') }}\n              </n-button>\n              <n-text depth=\"3\" size=\"small\" class=\"allow-all-hint\">\n                {{ t('settings.remoteControl.emptyListHint') }}\n              </n-text>\n            </div>\n          </n-form-item>\n\n          <n-form-item>\n            <n-space>\n              <n-button type=\"primary\" @click=\"saveConfig\">\n                {{ t('common.save') }}\n              </n-button>\n              <n-button @click=\"resetConfig\">\n                {{ t('common.reset') }}\n              </n-button>\n            </n-space>\n          </n-form-item>\n\n          <n-collapse-transition :show=\"remoteControlConfig.enabled\">\n            <div class=\"remote-info\">\n              <n-alert type=\"info\">\n                <template #icon>\n                  <n-icon><i class=\"ri-information-line\"></i></n-icon>\n                </template>\n                <p>{{ t('settings.remoteControl.accessInfo') }}</p>\n                <div class=\"access-url\">\n                  <n-tag type=\"success\"> http://localhost:{{ remoteControlConfig.port }}/ </n-tag>\n                </div>\n                <div v-if=\"localIpAddresses.length\" class=\"local-ips\">\n                  <div v-for=\"ip in localIpAddresses\" :key=\"ip\" class=\"ip-address\">\n                    <n-tag type=\"info\"> http://{{ ip }}:{{ remoteControlConfig.port }}/ </n-tag>\n                  </div>\n                </div>\n              </n-alert>\n            </div>\n          </n-collapse-transition>\n        </n-form>\n      </div>\n    </n-scrollbar>\n  </n-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { cloneDeep } from 'lodash';\nimport { useMessage } from 'naive-ui';\nimport { onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nconst { t } = useI18n();\nconst message = useMessage();\n\n// 控制弹窗显示的属性\nconst visible = defineModel('visible', { default: false });\n\n// 默认配置\nconst defaultConfig: {\n  enabled: boolean;\n  port: number;\n  allowedIps: string[];\n} = {\n  enabled: false,\n  port: 31888,\n  allowedIps: []\n};\n\n// 远程控制配置\nconst remoteControlConfig = ref({ ...defaultConfig });\n\n// 本地IP地址列表\nconst localIpAddresses = ref<string[]>([]);\n\n// 获取本地IP地址\nconst getLocalIpAddresses = () => {\n  if (window.electron) {\n    window.electron.ipcRenderer.invoke('get-local-ip-addresses').then((ips: string[]) => {\n      localIpAddresses.value = ips;\n    });\n  }\n};\n\n// 添加IP地址\nconst addIp = () => {\n  remoteControlConfig.value.allowedIps.push('');\n};\n\n// 删除IP地址\nconst removeIp = (index: number) => {\n  remoteControlConfig.value.allowedIps.splice(index, 1);\n};\n\n// 保存配置\nconst saveConfig = () => {\n  // 过滤空IP\n  remoteControlConfig.value.allowedIps = remoteControlConfig.value.allowedIps.filter(\n    (ip) => ip.trim() !== ''\n  );\n\n  if (window.electron) {\n    window.electron.ipcRenderer.send(\n      'update-remote-control-config',\n      cloneDeep(remoteControlConfig.value)\n    );\n    message.success(t('settings.remoteControl.saveSuccess'));\n  }\n};\n\n// 重置配置\nconst resetConfig = () => {\n  if (window.electron) {\n    window.electron.ipcRenderer.invoke('get-remote-control-config').then((config) => {\n      if (config) {\n        remoteControlConfig.value = config;\n      } else {\n        remoteControlConfig.value = { ...defaultConfig };\n      }\n    });\n  }\n};\n\n// 组件挂载时，获取当前配置\nonMounted(async () => {\n  if (window.electron) {\n    try {\n      const config = await window.electron.ipcRenderer.invoke('get-remote-control-config');\n      if (config) {\n        remoteControlConfig.value = config;\n      }\n      // 获取本地IP地址\n      getLocalIpAddresses();\n    } catch (error) {\n      console.error('获取远程控制配置失败:', error);\n    }\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.remote-control-setting {\n  padding: 0 20px;\n}\n\n.allowed-ips-container {\n  display: flex;\n  flex-direction: column;\n  gap: 10px;\n  width: 100%;\n\n  .ip-item {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n  }\n\n  .allow-all-hint {\n    margin-top: 5px;\n  }\n}\n\n.remote-info {\n  margin-top: 16px;\n\n  .access-url {\n    margin-top: 10px;\n  }\n\n  .local-ips {\n    margin-top: 10px;\n    display: flex;\n    flex-direction: column;\n    gap: 5px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/components/settings/ShortcutSettings.vue",
    "content": "<template>\n  <n-modal\n    v-model:show=\"visible\"\n    preset=\"dialog\"\n    :title=\"t('settings.shortcutSettings.title')\"\n    :show-icon=\"false\"\n    style=\"width: 600px\"\n    @after-leave=\"handleAfterLeave\"\n  >\n    <div class=\"shortcut-settings\">\n      <div class=\"shortcut-card\">\n        <div class=\"shortcut-content\">\n          <n-scrollbar>\n            <n-space vertical>\n              <div v-for=\"(shortcut, key) in tempShortcuts\" :key=\"key\" class=\"shortcut-item\">\n                <div class=\"shortcut-info\">\n                  <span class=\"shortcut-label\">{{ getShortcutLabel(key) }}</span>\n                </div>\n                <div class=\"shortcut-controls\">\n                  <div class=\"shortcut-input\">\n                    <n-input\n                      :value=\"formatShortcut(shortcut.key)\"\n                      :status=\"duplicateKeys[key] ? 'error' : undefined\"\n                      :placeholder=\"t('settings.shortcutSettings.inputPlaceholder')\"\n                      :disabled=\"!shortcut.enabled\"\n                      readonly\n                      @keydown=\"(e) => handleKeyDown(e, key)\"\n                      @focus=\"() => startRecording(key)\"\n                      @blur=\"stopRecording\"\n                    />\n                    <n-tooltip v-if=\"duplicateKeys[key]\" trigger=\"hover\">\n                      <template #trigger>\n                        <n-icon class=\"error-icon\" size=\"18\">\n                          <i class=\"ri-error-warning-line\"></i>\n                        </n-icon>\n                      </template>\n                      {{ t('settings.shortcutSettings.shortcutConflict') }}\n                    </n-tooltip>\n                  </div>\n                  <div class=\"shortcut-options\">\n                    <n-tooltip trigger=\"hover\">\n                      <template #trigger>\n                        <n-switch v-model:value=\"shortcut.enabled\" size=\"small\" />\n                      </template>\n                      {{\n                        shortcut.enabled\n                          ? t('settings.shortcutSettings.enabled')\n                          : t('settings.shortcutSettings.disabled')\n                      }}\n                    </n-tooltip>\n                    <n-tooltip v-if=\"shortcut.enabled\" trigger=\"hover\">\n                      <template #trigger>\n                        <n-select\n                          v-model:value=\"shortcut.scope\"\n                          :options=\"scopeOptions\"\n                          size=\"small\"\n                          style=\"width: 100px\"\n                        />\n                      </template>\n                      {{\n                        shortcut.scope === 'global'\n                          ? t('settings.shortcutSettings.scopeGlobal')\n                          : t('settings.shortcutSettings.scopeApp')\n                      }}\n                    </n-tooltip>\n                  </div>\n                </div>\n              </div>\n            </n-space>\n          </n-scrollbar>\n        </div>\n\n        <div class=\"shortcut-footer\">\n          <n-space justify=\"end\">\n            <n-button size=\"small\" @click=\"handleCancel\">{{ t('common.cancel') }}</n-button>\n            <n-button size=\"small\" @click=\"resetShortcuts\">{{\n              t('settings.shortcutSettings.resetShortcuts')\n            }}</n-button>\n            <n-button size=\"small\" type=\"warning\" @click=\"disableAllShortcuts\">{{\n              t('settings.shortcutSettings.disableAll')\n            }}</n-button>\n            <n-button size=\"small\" type=\"success\" @click=\"enableAllShortcuts\">{{\n              t('settings.shortcutSettings.enableAll')\n            }}</n-button>\n            <n-button type=\"primary\" size=\"small\" :disabled=\"hasConflict\" @click=\"handleSave\">\n              {{ t('common.save') }}\n            </n-button>\n          </n-space>\n        </div>\n      </div>\n    </div>\n  </n-modal>\n</template>\n\n<script lang=\"ts\" setup>\nimport { cloneDeep } from 'lodash';\nimport { useMessage } from 'naive-ui';\nimport { computed, onMounted, onUnmounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { isElectron } from '@/utils';\n\nconst { t } = useI18n();\n\ninterface ShortcutConfig {\n  key: string;\n  enabled: boolean;\n  scope: 'global' | 'app';\n}\n\ninterface Shortcuts {\n  togglePlay: ShortcutConfig;\n  prevPlay: ShortcutConfig;\n  nextPlay: ShortcutConfig;\n  volumeUp: ShortcutConfig;\n  volumeDown: ShortcutConfig;\n  toggleFavorite: ShortcutConfig;\n  toggleWindow: ShortcutConfig;\n}\n\nconst defaultShortcuts: Shortcuts = {\n  togglePlay: { key: 'CommandOrControl+Alt+P', enabled: true, scope: 'global' },\n  prevPlay: { key: 'Alt+Left', enabled: true, scope: 'global' },\n  nextPlay: { key: 'Alt+Right', enabled: true, scope: 'global' },\n  volumeUp: { key: 'Alt+Up', enabled: true, scope: 'app' },\n  volumeDown: { key: 'Alt+Down', enabled: true, scope: 'app' },\n  toggleFavorite: { key: 'CommandOrControl+Alt+L', enabled: true, scope: 'app' },\n  toggleWindow: { key: 'CommandOrControl+Alt+Shift+M', enabled: true, scope: 'global' }\n};\n\nconst scopeOptions = [\n  { label: t('settings.shortcutSettings.scopeGlobal'), value: 'global' },\n  { label: t('settings.shortcutSettings.scopeApp'), value: 'app' }\n];\n\nconst shortcuts = ref<Shortcuts>(\n  isElectron\n    ? window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts') || defaultShortcuts\n    : { ...defaultShortcuts }\n);\n\n// 临时存储编辑中的快捷键\nconst tempShortcuts = ref<Shortcuts>(cloneDeep(shortcuts.value));\n\n// 监听快捷键更新\nif (isElectron) {\n  window.electron.ipcRenderer.on('shortcuts-updated', () => {\n    const newShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts');\n    if (newShortcuts) {\n      shortcuts.value = newShortcuts;\n      tempShortcuts.value = cloneDeep(newShortcuts);\n    }\n  });\n}\n\n// 组件挂载时禁用快捷键\nonMounted(() => {\n  if (isElectron) {\n    // 禁用全局快捷键\n    window.electron.ipcRenderer.send('disable-shortcuts');\n\n    const storedShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts');\n    console.log('storedShortcuts', storedShortcuts);\n    if (storedShortcuts) {\n      shortcuts.value = storedShortcuts;\n      tempShortcuts.value = cloneDeep(storedShortcuts);\n    } else {\n      shortcuts.value = { ...defaultShortcuts };\n      tempShortcuts.value = cloneDeep(defaultShortcuts);\n      window.electron.ipcRenderer.send('set-store-value', 'shortcuts', defaultShortcuts);\n    }\n\n    // 转换旧格式的快捷键数据到新格式\n    if (storedShortcuts && typeof storedShortcuts.togglePlay === 'string') {\n      const convertedShortcuts = {} as Shortcuts;\n      Object.entries(storedShortcuts).forEach(([key, value]) => {\n        convertedShortcuts[key as keyof Shortcuts] = {\n          key: value as string,\n          enabled: true,\n          scope: ['volumeUp', 'volumeDown', 'toggleFavorite'].includes(key) ? 'app' : 'global'\n        };\n      });\n      shortcuts.value = convertedShortcuts;\n      tempShortcuts.value = cloneDeep(convertedShortcuts);\n      window.electron.ipcRenderer.send('set-store-value', 'shortcuts', convertedShortcuts);\n    }\n  }\n});\n\nconst shortcutLabels: Record<keyof Shortcuts, string> = {\n  togglePlay: t('settings.shortcutSettings.togglePlay'),\n  prevPlay: t('settings.shortcutSettings.prevPlay'),\n  nextPlay: t('settings.shortcutSettings.nextPlay'),\n  volumeUp: t('settings.shortcutSettings.volumeUp'),\n  volumeDown: t('settings.shortcutSettings.volumeDown'),\n  toggleFavorite: t('settings.shortcutSettings.toggleFavorite'),\n  toggleWindow: t('settings.shortcutSettings.toggleWindow')\n};\n\nconst getShortcutLabel = (key: keyof Shortcuts) => shortcutLabels[key];\n\nconst isRecording = ref(false);\nconst currentKey = ref<keyof Shortcuts | ''>('');\nconst message = useMessage();\n\n// 检查快捷键冲突\nconst duplicateKeys = computed(() => {\n  const result: Record<string, boolean> = {};\n  const usedShortcuts = new Map<string, string>();\n\n  Object.entries(tempShortcuts.value).forEach(([key, shortcut]) => {\n    // 只检查启用的快捷键\n    if (!shortcut.enabled) return;\n\n    const conflictKey = usedShortcuts.get(shortcut.key);\n    if (conflictKey) {\n      // 只有相同作用域的快捷键才会被认为冲突\n      const conflictScope = tempShortcuts.value[conflictKey as keyof Shortcuts].scope;\n      if (shortcut.scope === conflictScope) {\n        result[key] = true;\n      }\n    } else {\n      usedShortcuts.set(shortcut.key, key);\n    }\n  });\n\n  return result;\n});\n\n// 是否存在冲突\nconst hasConflict = computed(() => Object.keys(duplicateKeys.value).length > 0);\n\nconst startRecording = (key: keyof Shortcuts) => {\n  if (!tempShortcuts.value[key].enabled) return;\n\n  isRecording.value = true;\n  currentKey.value = key;\n  // 禁用全局快捷键\n  if (isElectron) {\n    window.electron.ipcRenderer.send('disable-shortcuts');\n  }\n};\n\nconst stopRecording = () => {\n  isRecording.value = false;\n  currentKey.value = '';\n  // 重新启用全局快捷键\n  if (isElectron) {\n    window.electron.ipcRenderer.send('enable-shortcuts');\n  }\n};\n\nconst handleKeyDown = (e: KeyboardEvent, key: keyof Shortcuts) => {\n  if (!isRecording.value || currentKey.value !== key) return;\n\n  e.preventDefault();\n  e.stopPropagation();\n\n  const modifiers: string[] = [];\n\n  // 统一使用 CommandOrControl\n  if (e.ctrlKey || e.metaKey) {\n    modifiers.push('CommandOrControl');\n  }\n  if (e.altKey) modifiers.push('Alt');\n  if (e.shiftKey) modifiers.push('Shift');\n\n  let keyName = e.key;\n\n  // 特殊按键处理\n  switch (e.key) {\n    case 'ArrowLeft':\n      keyName = 'Left';\n      break;\n    case 'ArrowRight':\n      keyName = 'Right';\n      break;\n    case 'ArrowUp':\n      keyName = 'Up';\n      break;\n    case 'ArrowDown':\n      keyName = 'Down';\n      break;\n    case 'Control':\n    case 'Alt':\n    case 'Shift':\n    case 'Meta':\n    case 'Command':\n      return; // 忽略单独的修饰键\n    default:\n      keyName = e.key.length === 1 ? e.key.toUpperCase() : e.key;\n  }\n\n  if (!['Control', 'Alt', 'Shift', 'Meta', 'Command'].includes(keyName)) {\n    tempShortcuts.value[key].key = [...modifiers, keyName].join('+');\n  }\n};\n\nconst resetShortcuts = () => {\n  tempShortcuts.value = cloneDeep(defaultShortcuts);\n  message.success(t('settings.shortcutSettings.messages.resetSuccess'));\n};\n\nconst saveShortcuts = () => {\n  if (hasConflict.value) {\n    message.error(t('settings.shortcutSettings.messages.conflict'));\n    return;\n  }\n\n  // 创建一个新的 Shortcuts 对象\n  const shortcutsToSave = cloneDeep(tempShortcuts.value);\n\n  shortcuts.value = shortcutsToSave;\n\n  if (isElectron) {\n    try {\n      // 先保存到 store\n      window.electron.ipcRenderer.send('set-store-value', 'shortcuts', shortcutsToSave);\n      // 然后更新快捷键\n      window.electron.ipcRenderer.send('update-shortcuts', shortcutsToSave);\n      message.success(t('settings.shortcutSettings.messages.saveSuccess'));\n    } catch (error) {\n      console.error('保存快捷键失败:', error);\n      message.error(t('settings.shortcutSettings.messages.saveError'));\n    }\n  }\n};\n\nconst cancelEdit = () => {\n  tempShortcuts.value = cloneDeep(shortcuts.value);\n  message.info(t('settings.shortcutSettings.messages.cancelEdit'));\n  emit('update:show', false);\n};\n\n// 组件卸载时确保快捷键被重新启用\nonUnmounted(() => {\n  if (isElectron) {\n    window.electron.ipcRenderer.send('enable-shortcuts');\n  }\n});\n\n// 格式化快捷键显示\nconst formatShortcut = (shortcut: string) => {\n  const isMac = isElectron\n    ? window.electron.ipcRenderer.sendSync('get-platform') === 'darwin'\n    : false;\n  return shortcut\n    .replace(/CommandOrControl/g, isMac ? '⌘' : 'Ctrl')\n    .replace(/\\+/g, ' + ')\n    .replace(/Meta/g, isMac ? '⌘' : 'Win')\n    .replace(/Control/g, isMac ? '⌃' : 'Ctrl')\n    .replace(/Alt/g, isMac ? '⌥' : 'Alt')\n    .replace(/Shift/g, isMac ? '⇧' : 'Shift')\n    .replace(/ArrowUp/g, '↑')\n    .replace(/ArrowDown/g, '↓')\n    .replace(/ArrowLeft/g, '←')\n    .replace(/ArrowRight/g, '→');\n};\n\nconst visible = ref(false);\nconst emit = defineEmits(['update:show', 'change']);\n\n// 接收外部的 show 属性\nconst props = defineProps<{\n  show?: boolean;\n}>();\n\n// 监听 show 属性变化\nwatch(\n  () => props.show,\n  (newVal) => {\n    visible.value = newVal;\n  }\n);\n\n// 监听内部 visible 变化\nwatch(visible, (newVal) => {\n  emit('update:show', newVal);\n});\n\n// 处理弹窗关闭后的事件\nconst handleAfterLeave = () => {\n  // 重置临时数据\n  tempShortcuts.value = cloneDeep(shortcuts.value);\n};\n\n// 处理取消按钮点击\nconst handleCancel = () => {\n  visible.value = false;\n  cancelEdit();\n};\n\n// 处理保存按钮点击\nconst handleSave = () => {\n  saveShortcuts();\n  visible.value = false;\n  emit('change', shortcuts.value);\n};\n\n// 全部禁用快捷键\nconst disableAllShortcuts = () => {\n  Object.keys(tempShortcuts.value).forEach((key) => {\n    tempShortcuts.value[key as keyof Shortcuts].enabled = false;\n  });\n  message.info(t('settings.shortcutSettings.messages.disableAll'));\n};\n\n// 全部启用快捷键\nconst enableAllShortcuts = () => {\n  Object.keys(tempShortcuts.value).forEach((key) => {\n    tempShortcuts.value[key as keyof Shortcuts].enabled = true;\n  });\n  message.info(t('settings.shortcutSettings.messages.enableAll'));\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.shortcut-settings {\n  height: 500px;\n\n  .shortcut-card {\n    @apply flex flex-col h-full;\n\n    .shortcut-footer {\n      @apply p-4 border-t border-gray-100 dark:border-gray-800;\n    }\n\n    .shortcut-content {\n      @apply flex-1 overflow-hidden;\n\n      :deep(.n-scrollbar) {\n        @apply h-full;\n\n        .n-scrollbar-content {\n          @apply p-4;\n        }\n      }\n    }\n  }\n\n  .shortcut-item {\n    @apply flex items-center justify-between p-3 rounded-lg transition-all mb-3;\n    @apply bg-gray-50 dark:bg-gray-800;\n\n    &:last-child {\n      margin-bottom: 0;\n    }\n\n    .shortcut-info {\n      @apply flex flex-col min-w-[150px];\n\n      .shortcut-label {\n        @apply text-base font-medium;\n      }\n    }\n\n    .shortcut-controls {\n      @apply flex items-center gap-3 flex-1;\n\n      .shortcut-input {\n        @apply flex items-center gap-2 flex-1;\n\n        :deep(.n-input) {\n          .n-input__input-el {\n            @apply text-center font-mono;\n          }\n        }\n\n        .error-icon {\n          @apply text-red-500;\n        }\n      }\n\n      .shortcut-options {\n        @apply flex items-center gap-2;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/const/bar-const.ts",
    "content": "export const USER_SET_OPTIONS = [\n  // {\n  //   label: '打卡',\n  //   key: 'card',\n  // },\n  // {\n  //   label: '听歌升级',\n  //   key: 'card_music',\n  // },\n  // {\n  //   label: '歌曲次数',\n  //   key: 'listen',\n  // },\n  {\n    label: '退出登录',\n    key: 'logout'\n  },\n  {\n    label: '设置',\n    key: 'set'\n  }\n];\n\nexport const SEARCH_TYPES = [\n  {\n    label: 'search.search.single', // 单曲\n    key: 1\n  },\n  {\n    label: 'search.search.album', // 专辑\n    key: 10\n  },\n  {\n    label: 'search.search.playlist', // 歌单\n    key: 1000\n  },\n  {\n    label: 'search.search.mv', // MV\n    key: 1004\n  },\n  {\n    label: 'search.search.bilibili', // B站\n    key: 2000\n  }\n];\n\nexport const SEARCH_TYPE = {\n  MUSIC: 1, // 单曲\n  ALBUM: 10, // 专辑\n  ARTIST: 100, // 歌手\n  PLAYLIST: 1000, // 歌单\n  MV: 1004, // MV\n  BILIBILI: 2000 // B站视频\n} as const;\n"
  },
  {
    "path": "src/renderer/directive/index.ts",
    "content": "import { vLoading } from './loading/index';\n\nconst directives = {\n  loading: vLoading\n};\n\nexport default directives;\n"
  },
  {
    "path": "src/renderer/directive/loading/index.ts",
    "content": "import { createVNode, render, VNode } from 'vue';\n\nimport Loading from './index.vue';\n\nconst vnode: VNode = createVNode(Loading) as VNode;\n\nexport const vLoading = {\n  // 在绑定元素的父组件 及他自己的所有子节点都挂载完成后调用\n  mounted: (el: HTMLElement) => {\n    render(vnode, el);\n  },\n  // 在绑定元素的父组件 及他自己的所有子节点都更新后调用\n  updated: (el: HTMLElement, binding: any) => {\n    if (binding.value) {\n      vnode?.component?.exposed?.show();\n    } else {\n      vnode?.component?.exposed?.hide();\n    }\n    // 动态添加删除自定义class: loading-parent\n    formatterClass(el, binding);\n  },\n  // 绑定元素的父组件卸载后调用\n  unmounted: () => {\n    vnode?.component?.exposed?.hide();\n  }\n};\n\nfunction formatterClass(el: HTMLElement, binding: any) {\n  const classStr = el.getAttribute('class');\n  const tagetClass: number = classStr?.indexOf('loading-parent') as number;\n  if (binding.value) {\n    if (tagetClass === -1) {\n      el.setAttribute('class', `${classStr} loading-parent`);\n    }\n  } else if (tagetClass > -1) {\n    const classArray: Array<string> = classStr?.split('') as string[];\n    classArray.splice(tagetClass - 1, tagetClass + 15);\n    el.setAttribute('class', classArray?.join(''));\n  }\n}\n"
  },
  {
    "path": "src/renderer/directive/loading/index.vue",
    "content": "<!--  -->\n<template>\n  <div v-if=\"isShow\" class=\"loading-box\">\n    <div class=\"mask\"></div>\n    <div class=\"loading-content-box\">\n      <n-spin size=\"small\" />\n      <div :style=\"{ color: textColor }\" class=\"tip\">{{ tip }}</div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { NSpin } from 'naive-ui';\nimport { ref } from 'vue';\n\ndefineProps({\n  tip: {\n    type: String,\n    default() {\n      return '加载中...';\n    }\n  },\n  maskBackground: {\n    type: String,\n    default() {\n      return 'rgba(0, 0, 0, 0.05)';\n    }\n  },\n  loadingColor: {\n    type: String,\n    default() {\n      return 'rgba(255, 255, 255, 1)';\n    }\n  },\n  textColor: {\n    type: String,\n    default() {\n      return 'rgba(255, 255, 255, 1)';\n    }\n  }\n});\n\nconst isShow = ref(false);\nconst show = () => {\n  isShow.value = true;\n};\nconst hide = () => {\n  isShow.value = false;\n};\ndefineExpose({\n  show,\n  hide,\n  isShow\n});\n</script>\n<style lang=\"scss\" scoped>\n.loading-box {\n  position: absolute;\n  left: 0;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  z-index: 9999;\n  .n-spin {\n    // color: #ccc;\n  }\n  .mask {\n    width: 100%;\n    height: 100%;\n    @apply bg-light-100 bg-opacity-50 dark:bg-dark-100 dark:bg-opacity-50;\n  }\n  .loading-content-box {\n    position: absolute;\n    left: 0;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    width: 100%;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n  }\n  .tip {\n    font-size: 14px;\n    margin-top: 8px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/hooks/AlbumHistoryHook.ts",
    "content": "import { useLocalStorage } from '@vueuse/core';\nimport { ref, watch } from 'vue';\n\n// 专辑历史记录类型\nexport interface AlbumHistoryItem {\n  id: number;\n  name: string;\n  picUrl?: string;\n  size?: number; // 歌曲数量\n  artist?: {\n    name: string;\n    id: number;\n  };\n  count?: number; // 播放次数\n  lastPlayTime?: number; // 最后播放时间\n}\n\nexport const useAlbumHistory = () => {\n  const albumHistory = useLocalStorage<AlbumHistoryItem[]>('albumHistory', []);\n\n  const addAlbum = (album: AlbumHistoryItem) => {\n    const index = albumHistory.value.findIndex((item) => item.id === album.id);\n    const now = Date.now();\n\n    if (index !== -1) {\n      // 如果已存在，更新播放次数和时间，并移到最前面\n      albumHistory.value[index].count = (albumHistory.value[index].count || 0) + 1;\n      albumHistory.value[index].lastPlayTime = now;\n      albumHistory.value.unshift(albumHistory.value.splice(index, 1)[0]);\n    } else {\n      // 如果不存在，添加新记录\n      albumHistory.value.unshift({\n        ...album,\n        count: 1,\n        lastPlayTime: now\n      });\n    }\n  };\n\n  const delAlbum = (album: AlbumHistoryItem) => {\n    const index = albumHistory.value.findIndex((item) => item.id === album.id);\n    if (index !== -1) {\n      albumHistory.value.splice(index, 1);\n    }\n  };\n\n  const albumList = ref(albumHistory.value);\n\n  watch(\n    () => albumHistory.value,\n    () => {\n      albumList.value = albumHistory.value;\n    },\n    { deep: true }\n  );\n\n  return {\n    albumHistory,\n    albumList,\n    addAlbum,\n    delAlbum\n  };\n};\n"
  },
  {
    "path": "src/renderer/hooks/IndexDBHook.ts",
    "content": "import { ref } from 'vue';\n\n// 定义表配置的泛型接口\nexport interface StoreConfig<T extends string> {\n  name: T;\n  keyPath?: string;\n}\n\n// 创建一个使用 IndexedDB 的组合函数\nconst useIndexedDB = async <T extends string, S extends Record<T, Record<string, any>>>(\n  dbName: string,\n  stores: StoreConfig<T>[],\n  version: number = 1\n) => {\n  const db = ref<IDBDatabase | null>(null);\n\n  // 打开数据库并创建表\n  const initDB = () => {\n    return new Promise<void>((resolve, reject) => {\n      const request = indexedDB.open(dbName, version);\n\n      request.onupgradeneeded = (event: any) => {\n        const db = event.target.result;\n        stores.forEach((store) => {\n          if (!db.objectStoreNames.contains(store.name)) {\n            db.createObjectStore(store.name, {\n              keyPath: store.keyPath || 'id',\n              autoIncrement: true\n            });\n          }\n        });\n      };\n\n      request.onsuccess = (event: any) => {\n        db.value = event.target.result;\n        resolve();\n      };\n\n      request.onerror = (event: any) => {\n        reject(event.target.error);\n      };\n    });\n  };\n\n  await initDB();\n\n  // 通用新增数据\n  const addData = <K extends T>(storeName: K, value: S[K]) => {\n    return new Promise<void>((resolve, reject) => {\n      if (!db.value) return reject('数据库未初始化');\n      const tx = db.value.transaction(storeName, 'readwrite');\n      const store = tx.objectStore(storeName);\n\n      const request = store.add(value);\n\n      request.onsuccess = () => {\n        console.log('成功');\n        resolve();\n      };\n\n      request.onerror = (event) => {\n        console.error('新增失败:', (event.target as IDBRequest).error);\n        reject((event.target as IDBRequest).error);\n      };\n    });\n  };\n\n  // 通用保存数据（新增或更新）\n  const saveData = <K extends T>(storeName: K, value: S[K]) => {\n    return new Promise<void>((resolve, reject) => {\n      if (!db.value) return reject('数据库未初始化');\n      const tx = db.value.transaction(storeName, 'readwrite');\n      const store = tx.objectStore(storeName);\n      const request = store.put(value);\n\n      request.onsuccess = () => {\n        console.log('成功');\n        resolve();\n      };\n\n      request.onerror = (event) => {\n        reject((event.target as IDBRequest).error);\n      };\n    });\n  };\n\n  // 通用获取数据\n  const getData = <K extends T>(storeName: K, key: string | number) => {\n    return new Promise<S[K]>((resolve, reject) => {\n      if (!db.value) return reject('数据库未初始化');\n      const tx = db.value.transaction(storeName, 'readonly');\n      const store = tx.objectStore(storeName);\n      const request = store.get(key);\n\n      request.onsuccess = (event) => {\n        if (event.target) {\n          resolve((event.target as IDBRequest).result);\n        } else {\n          reject('事件目标为空');\n        }\n      };\n\n      request.onerror = (event) => {\n        reject((event.target as IDBRequest).error);\n      };\n    });\n  };\n\n  // 删除数据\n  const deleteData = <K extends T>(storeName: K, key: string | number) => {\n    return new Promise<void>((resolve, reject) => {\n      if (!db.value) return reject('数据库未初始化');\n      const tx = db.value.transaction(storeName, 'readwrite');\n      const store = tx.objectStore(storeName);\n      const request = store.delete(key);\n\n      request.onsuccess = () => {\n        console.log('删除成功');\n        resolve();\n      };\n\n      request.onerror = (event) => {\n        reject((event.target as IDBRequest).error);\n      };\n    });\n  };\n\n  // 查询所有数据\n  const getAllData = <K extends T>(storeName: K) => {\n    return new Promise<S[K][]>((resolve, reject) => {\n      if (!db.value) return reject('数据库未初始化');\n      const tx = db.value.transaction(storeName, 'readonly');\n      const store = tx.objectStore(storeName);\n      const request = store.getAll();\n\n      request.onsuccess = (event) => {\n        if (event.target) {\n          resolve((event.target as IDBRequest).result);\n        } else {\n          reject('事件目标为空');\n        }\n      };\n\n      request.onerror = (event) => {\n        reject((event.target as IDBRequest).error);\n      };\n    });\n  };\n\n  // 分页查询数据\n  const getDataWithPagination = <K extends T>(storeName: K, page: number, pageSize: number) => {\n    return new Promise<S[K][]>((resolve, reject) => {\n      if (!db.value) return reject('数据库未初始化');\n      const tx = db.value.transaction(storeName, 'readonly');\n      const store = tx.objectStore(storeName);\n      const request = store.openCursor();\n      const results: S[K][] = [];\n      let index = 0;\n      const skip = (page - 1) * pageSize;\n\n      request.onsuccess = (event: any) => {\n        const cursor = event.target.result;\n        if (!cursor) {\n          resolve(results);\n          return;\n        }\n\n        if (index >= skip && results.length < pageSize) {\n          results.push(cursor.value);\n        }\n\n        index++;\n        cursor.continue();\n      };\n\n      request.onerror = (event: any) => {\n        reject(event.target.error);\n      };\n    });\n  };\n\n  return {\n    initDB,\n    addData,\n    saveData,\n    getData,\n    deleteData,\n    getAllData,\n    getDataWithPagination\n  };\n};\n\nexport default useIndexedDB;\n"
  },
  {
    "path": "src/renderer/hooks/MusicHistoryHook.ts",
    "content": "import { useLocalStorage } from '@vueuse/core';\n\nimport type { SongResult } from '@/types/music';\n\nexport const useMusicHistory = () => {\n  const musicHistory = useLocalStorage<SongResult[]>('musicHistory', []);\n\n  const addMusic = (music: SongResult) => {\n    const index = musicHistory.value.findIndex((item) => item.id === music.id);\n    if (index !== -1) {\n      musicHistory.value[index].count = (musicHistory.value[index].count || 0) + 1;\n      musicHistory.value.unshift(musicHistory.value.splice(index, 1)[0]);\n    } else {\n      musicHistory.value.unshift({ ...music, count: 1 });\n    }\n  };\n\n  const delMusic = (music: SongResult) => {\n    const index = musicHistory.value.findIndex((item) => item.id === music.id);\n    if (index !== -1) {\n      musicHistory.value.splice(index, 1);\n    }\n  };\n  const musicList = ref(musicHistory.value);\n  watch(\n    () => musicHistory.value,\n    () => {\n      musicList.value = musicHistory.value;\n    }\n  );\n\n  return {\n    musicHistory,\n    musicList,\n    addMusic,\n    delMusic\n  };\n};\n"
  },
  {
    "path": "src/renderer/hooks/MusicHook.ts",
    "content": "import { cloneDeep } from 'lodash';\nimport { createDiscreteApi } from 'naive-ui';\nimport { computed, type ComputedRef, nextTick, onUnmounted, ref, watch } from 'vue';\n\nimport useIndexedDB from '@/hooks/IndexDBHook';\nimport { audioService } from '@/services/audioService';\nimport type { usePlayerStore } from '@/store';\nimport type { Artist, ILyricText, SongResult } from '@/types/music';\nimport { isElectron } from '@/utils';\nimport { getTextColors } from '@/utils/linearColor';\nimport { parseLyrics } from '@/utils/yrcParser';\n\nconst windowData = window as any;\n\n// 全局 playerStore 引用，通过 initMusicHook 函数注入\nlet playerStore: ReturnType<typeof usePlayerStore> | null = null;\n\n// 初始化函数，接受 store 实例\nexport const initMusicHook = (store: ReturnType<typeof usePlayerStore>) => {\n  playerStore = store;\n\n  // 创建 computed 属性\n  playMusic = computed(() => getPlayerStore().playMusic as SongResult);\n  artistList = computed(\n    () => (getPlayerStore().playMusic.ar || getPlayerStore().playMusic?.song?.artists) as Artist[]\n  );\n\n  // 在 store 注入后初始化需要 store 的功能\n  setupKeyboardListeners();\n  initProgressAnimation();\n  setupMusicWatchers();\n  setupCorrectionTimeWatcher();\n  setupPlayStateWatcher();\n};\n\n// 获取 playerStore 的辅助函数\nconst getPlayerStore = () => {\n  if (!playerStore) {\n    throw new Error('MusicHook not initialized. Call initMusicHook first.');\n  }\n  return playerStore;\n};\nexport const lrcArray = ref<ILyricText[]>([]); // 歌词数组\nexport const lrcTimeArray = ref<number[]>([]); // 歌词时间数组\nexport const nowTime = ref(0); // 当前播放时间\nexport const allTime = ref(0); // 总播放时间\nexport const nowIndex = ref(0); // 当前播放歌词\nexport const currentLrcProgress = ref(0); // 来存储当前歌词的进度\nexport const sound = ref<Howl | null>(audioService.getCurrentSound());\nexport const isLyricWindowOpen = ref(false); // 新增状态\nexport const textColors = ref<any>(getTextColors());\n\n// 这些 computed 属性需要在初始化后创建\nexport let playMusic: ComputedRef<SongResult>;\nexport let artistList: ComputedRef<Artist[]>;\n\nexport const musicDB = await useIndexedDB(\n  'musicDB',\n  [\n    { name: 'music', keyPath: 'id' },\n    { name: 'music_lyric', keyPath: 'id' },\n    { name: 'api_cache', keyPath: 'id' },\n    { name: 'music_url_cache', keyPath: 'id' },\n    { name: 'music_failed_cache', keyPath: 'id' }\n  ],\n  3\n);\n\n// 键盘事件处理器，在初始化后设置\nconst setupKeyboardListeners = () => {\n  document.onkeyup = (e) => {\n    // 检查事件目标是否是输入框元素\n    const target = e.target as HTMLElement;\n    if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') {\n      return;\n    }\n\n    const store = getPlayerStore();\n    switch (e.code) {\n      case 'Space':\n        if (store.play) {\n          store.setPlayMusic(false);\n          audioService.getCurrentSound()?.pause();\n        } else {\n          store.setPlayMusic(true);\n          audioService.getCurrentSound()?.play();\n        }\n        break;\n      default:\n    }\n  };\n};\n\nconst { message } = createDiscreteApi(['message']);\n\n// 全局变量\nlet progressAnimationInitialized = false;\nlet globalAnimationFrameId: number | null = null;\nconst lastSavedTime = ref(0);\n\n// 全局停止函数\nconst stopProgressAnimation = () => {\n  if (globalAnimationFrameId) {\n    cancelAnimationFrame(globalAnimationFrameId);\n    globalAnimationFrameId = null;\n  }\n};\n\n// 全局更新函数\nconst updateProgress = () => {\n  if (!getPlayerStore().play) {\n    stopProgressAnimation();\n    return;\n  }\n\n  const currentSound = sound.value;\n  if (!currentSound) {\n    console.log('进度更新：无效的 sound 对象');\n    // 不是立即返回，而是设置定时器稍后再次尝试\n    globalAnimationFrameId = setTimeout(() => {\n      requestAnimationFrame(updateProgress);\n    }, 100) as unknown as number;\n    return;\n  }\n\n  if (typeof currentSound.seek !== 'function') {\n    console.log('进度更新：无效的 seek 函数');\n    // 不是立即返回，而是设置定时器稍后再次尝试\n    globalAnimationFrameId = setTimeout(() => {\n      requestAnimationFrame(updateProgress);\n    }, 100) as unknown as number;\n    return;\n  }\n\n  try {\n    const { start, end } = currentLrcTiming.value;\n    if (typeof start !== 'number' || typeof end !== 'number' || start === end) {\n      globalAnimationFrameId = requestAnimationFrame(updateProgress);\n      return;\n    }\n\n    let currentTime;\n    try {\n      // 获取当前播放位置\n      currentTime = currentSound.seek() as number;\n\n      // 减少更新频率，避免频繁更新UI\n      const timeDiff = Math.abs(currentTime - nowTime.value);\n      if (timeDiff > 0.2 || Math.floor(currentTime) !== Math.floor(nowTime.value)) {\n        nowTime.value = currentTime;\n      }\n\n      // 保存当前播放进度到 localStorage (每秒保存一次，避免频繁写入)\n      if (\n        Math.floor(currentTime) % 2 === 0 &&\n        Math.floor(currentTime) !== Math.floor(lastSavedTime.value)\n      ) {\n        lastSavedTime.value = currentTime;\n        if (getPlayerStore().playMusic && getPlayerStore().playMusic.id) {\n          localStorage.setItem(\n            'playProgress',\n            JSON.stringify({\n              songId: getPlayerStore().playMusic.id,\n              progress: currentTime\n            })\n          );\n        }\n      }\n    } catch (seekError) {\n      console.error('调用 seek() 方法出错:', seekError);\n      globalAnimationFrameId = requestAnimationFrame(updateProgress);\n      return;\n    }\n\n    if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) {\n      console.error('无效的当前时间:', currentTime);\n      globalAnimationFrameId = requestAnimationFrame(updateProgress);\n      return;\n    }\n\n    const elapsed = currentTime - start;\n    const duration = end - start;\n    const progress = (elapsed / duration) * 100;\n\n    // 确保进度在 0-100 之间\n    currentLrcProgress.value = Math.min(Math.max(progress, 0), 100);\n  } catch (error) {\n    console.error('更新进度出错:', error);\n  }\n\n  // 继续下一帧更新，但降低更新频率为60帧中更新10帧\n  globalAnimationFrameId = setTimeout(() => {\n    requestAnimationFrame(updateProgress);\n  }, 100) as unknown as number;\n};\n\n// 全局启动函数\nconst startProgressAnimation = () => {\n  stopProgressAnimation(); // 先停止之前的动画\n  updateProgress();\n};\n\n// 全局初始化函数\nconst initProgressAnimation = () => {\n  if (progressAnimationInitialized) return;\n\n  console.log('初始化进度动画');\n  progressAnimationInitialized = true;\n\n  // 监听播放状态变化，这里使用防抖，避免频繁触发\n  let debounceTimer: any = null;\n\n  watch(\n    () => getPlayerStore().play,\n    (newIsPlaying) => {\n      console.log('播放状态变化:', newIsPlaying);\n\n      // 清除之前的定时器\n      if (debounceTimer) {\n        clearTimeout(debounceTimer);\n      }\n\n      // 使用防抖，延迟 100ms 再执行\n      debounceTimer = setTimeout(() => {\n        if (newIsPlaying) {\n          // 确保 sound 对象有效时才启动进度更新\n          if (sound.value) {\n            console.log('sound 对象已存在，立即启动进度更新');\n            startProgressAnimation();\n          } else {\n            console.log('等待 sound 对象初始化...');\n            // 定时检查 sound 对象是否已初始化\n            const checkInterval = setInterval(() => {\n              if (sound.value) {\n                clearInterval(checkInterval);\n                console.log('sound 对象已初始化，开始进度更新');\n                startProgressAnimation();\n              }\n            }, 100);\n            // 设置超时，防止无限等待\n            setTimeout(() => {\n              clearInterval(checkInterval);\n              console.log('等待 sound 对象超时，已停止等待');\n            }, 5000);\n          }\n        } else {\n          stopProgressAnimation();\n        }\n      }, 100);\n    }\n  );\n\n  // 监听当前歌词索引变化\n  watch(nowIndex, () => {\n    currentLrcProgress.value = 0;\n    if (getPlayerStore().play) {\n      startProgressAnimation();\n    }\n  });\n\n  // 监听音频对象变化\n  watch(sound, (newSound) => {\n    console.log('sound 对象变化:', !!newSound);\n    if (newSound && getPlayerStore().play) {\n      startProgressAnimation();\n    }\n  });\n};\n\n/**\n * 解析歌词字符串并转换为ILyricText格式\n * @param lyricsStr 歌词字符串\n * @returns 解析后的歌词数据\n */\nconst parseLyricsString = async (\n  lyricsStr: string\n): Promise<{ lrcArray: ILyricText[]; lrcTimeArray: number[]; hasWordByWord: boolean }> => {\n  if (!lyricsStr || typeof lyricsStr !== 'string') {\n    return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false };\n  }\n\n  try {\n    const parseResult = parseLyrics(lyricsStr);\n    console.log('parseResult', parseResult);\n\n    if (!parseResult.success) {\n      console.error('歌词解析失败:', parseResult.error.message);\n      return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false };\n    }\n\n    const { lyrics } = parseResult.data;\n    const lrcArray: ILyricText[] = [];\n    const lrcTimeArray: number[] = [];\n    let hasWordByWord = false;\n\n    for (const line of lyrics) {\n      // 检查是否有逐字歌词\n      const hasWords = line.words && line.words.length > 0;\n      if (hasWords) {\n        hasWordByWord = true;\n      }\n\n      lrcArray.push({\n        text: line.fullText,\n        trText: '', // 翻译文本稍后处理\n        words: hasWords\n          ? line.words.map((word) => ({\n              ...word\n            }))\n          : undefined,\n        hasWordByWord: hasWords,\n        startTime: line.startTime,\n        duration: line.duration\n      });\n\n      lrcTimeArray.push(line.startTime);\n    }\n    return { lrcArray, lrcTimeArray, hasWordByWord };\n  } catch (error) {\n    console.error('解析歌词时发生错误:', error);\n    return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false };\n  }\n};\n\n// 设置音乐相关的监听器\nconst setupMusicWatchers = () => {\n  const store = getPlayerStore();\n\n  // 监听 playerStore.playMusic 的变化以更新歌词数据\n  watch(\n    () => store.playMusic.id,\n    async (newId, oldId) => {\n      // 如果没有歌曲ID，清空歌词\n      if (!newId) {\n        lrcArray.value = [];\n        lrcTimeArray.value = [];\n        nowIndex.value = 0;\n        return;\n      }\n\n      // 避免相同ID的重复执行(但允许初始化时执行)\n      if (newId === oldId && lrcArray.value.length > 0) return;\n\n      // 歌曲切换时重置歌词索引\n      if (newId !== oldId) {\n        nowIndex.value = 0;\n      }\n\n      await nextTick(async () => {\n        console.log('歌曲切换，更新歌词数据');\n\n        // 检查是否有原始歌词字符串需要解析\n        const lyricData = playMusic.value.lyric;\n        if (lyricData && typeof lyricData === 'string') {\n          // 如果歌词是字符串格式，使用新的解析器\n          const {\n            lrcArray: parsedLrcArray,\n            lrcTimeArray: parsedTimeArray,\n            hasWordByWord\n          } = await parseLyricsString(lyricData);\n          lrcArray.value = parsedLrcArray;\n          lrcTimeArray.value = parsedTimeArray;\n\n          // 更新歌曲的歌词数据结构\n          if (playMusic.value.lyric && typeof playMusic.value.lyric === 'object') {\n            playMusic.value.lyric.hasWordByWord = hasWordByWord;\n          }\n        } else {\n          // 使用现有的歌词数据结构\n          const rawLrc = lyricData?.lrcArray || [];\n          lrcTimeArray.value = lyricData?.lrcTimeArray || [];\n\n          try {\n            const { translateLyrics } = await import('@/services/lyricTranslation');\n            lrcArray.value = await translateLyrics(rawLrc as any);\n          } catch (e) {\n            console.error('翻译歌词失败，使用原始歌词：', e);\n            lrcArray.value = rawLrc as any;\n          }\n        }\n        // 当歌词数据更新时，如果歌词窗口打开，则发送数据\n        if (isElectron && isLyricWindowOpen.value) {\n          console.log('歌词窗口已打开，同步最新歌词数据');\n          // 不管歌词数组是否为空，都发送最新数据\n          sendLyricToWin();\n\n          // 再次延迟发送，确保歌词窗口已完全加载\n          setTimeout(() => {\n            sendLyricToWin();\n          }, 500);\n        }\n      });\n    },\n    { immediate: true }\n  );\n};\n\nconst setupAudioListeners = () => {\n  let interval: any = null;\n\n  const clearInterval = () => {\n    if (interval) {\n      window.clearInterval(interval);\n      interval = null;\n    }\n  };\n\n  // 清理所有事件监听器\n  audioService.clearAllListeners();\n\n  // 监听seek开始事件，立即更新UI\n  audioService.on('seek_start', (time) => {\n    // 直接更新显示位置，不检查拖动状态\n    nowTime.value = time;\n  });\n\n  // 监听seek完成事件\n  audioService.on('seek', () => {\n    try {\n      const currentSound = sound.value;\n      if (currentSound) {\n        // 立即更新显示时间，不进行任何检查\n        const currentTime = currentSound.seek() as number;\n        if (typeof currentTime === 'number' && !Number.isNaN(currentTime)) {\n          nowTime.value = currentTime;\n\n          // 检查是否需要更新歌词\n          const newIndex = getLrcIndex(nowTime.value);\n          if (newIndex !== nowIndex.value) {\n            nowIndex.value = newIndex;\n            if (isElectron && isLyricWindowOpen.value) {\n              sendLyricToWin();\n            }\n          }\n        }\n      }\n    } catch (error) {\n      console.error('处理seek事件出错:', error);\n    }\n  });\n\n  // 立即更新一次时间和进度（解决初始化时进度条不显示问题）\n  const updateCurrentTimeAndDuration = () => {\n    const currentSound = audioService.getCurrentSound();\n    if (currentSound) {\n      try {\n        // 更新当前时间和总时长\n        const currentTime = currentSound.seek() as number;\n        if (typeof currentTime === 'number' && !Number.isNaN(currentTime)) {\n          nowTime.value = currentTime;\n          allTime.value = currentSound.duration() as number;\n        }\n      } catch (error) {\n        console.error('初始化时间和进度失败:', error);\n      }\n    }\n  };\n\n  // 立即执行一次更新\n  updateCurrentTimeAndDuration();\n\n  // 监听播放\n  audioService.on('play', () => {\n    getPlayerStore().setPlayMusic(true);\n    if (isElectron) {\n      window.api.sendSong(cloneDeep(getPlayerStore().playMusic));\n    }\n    clearInterval();\n    interval = window.setInterval(() => {\n      try {\n        const currentSound = sound.value;\n        if (!currentSound) {\n          console.error('Invalid sound object: sound is null or undefined');\n          clearInterval();\n          return;\n        }\n\n        // 确保 seek 方法存在且可调用\n        if (typeof currentSound.seek !== 'function') {\n          console.error('Invalid sound object: seek function not available');\n          clearInterval();\n          return;\n        }\n\n        const currentTime = currentSound.seek() as number;\n        if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) {\n          console.error('Invalid current time:', currentTime);\n          clearInterval();\n          return;\n        }\n\n        nowTime.value = currentTime;\n        allTime.value = currentSound.duration() as number;\n        const newIndex = getLrcIndex(nowTime.value);\n        if (newIndex !== nowIndex.value) {\n          nowIndex.value = newIndex;\n          // 注意：我们不在这里设置 currentLrcProgress 为 0\n          // 因为这会与全局进度更新冲突\n          if (isElectron && isLyricWindowOpen.value) {\n            sendLyricToWin();\n          }\n        }\n        if (isElectron && isLyricWindowOpen.value) {\n          sendLyricToWin();\n        }\n      } catch (error) {\n        console.error('Error in interval:', error);\n        clearInterval();\n      }\n    }, 50);\n  });\n\n  // 监听暂停\n  audioService.on('pause', () => {\n    console.log('音频暂停事件触发');\n    getPlayerStore().setPlayMusic(false);\n    clearInterval();\n    if (isElectron && isLyricWindowOpen.value) {\n      sendLyricToWin();\n    }\n  });\n\n  const replayMusic = async () => {\n    try {\n      // 如果当前有音频实例，先停止并销毁\n      if (sound.value) {\n        sound.value.stop();\n        sound.value.unload();\n        sound.value = null;\n      }\n\n      // 重新播放当前歌曲\n      if (getPlayerStore().playMusicUrl && playMusic.value) {\n        const newSound = await audioService.play(getPlayerStore().playMusicUrl, playMusic.value);\n        sound.value = newSound as Howl;\n        setupAudioListeners();\n      } else {\n        console.error('No music URL or playMusic data available');\n        getPlayerStore().nextPlay();\n      }\n    } catch (error) {\n      console.error('Error replaying song:', error);\n      getPlayerStore().nextPlay();\n    }\n  };\n\n  // 监听结束\n  audioService.on('end', () => {\n    console.log('音频播放结束事件触发');\n    clearInterval();\n\n    if (getPlayerStore().playMode === 1) {\n      // 单曲循环模式\n      if (sound.value) {\n        replayMusic();\n      }\n    } else {\n      // 顺序播放、列表循环、随机播放模式都使用统一的nextPlay方法\n      getPlayerStore().nextPlay();\n    }\n  });\n\n  audioService.on('previoustrack', () => {\n    getPlayerStore().prevPlay();\n  });\n\n  audioService.on('nexttrack', () => {\n    getPlayerStore().nextPlay();\n  });\n\n  return clearInterval;\n};\n\nexport const play = () => {\n  const currentSound = audioService.getCurrentSound();\n  if (currentSound) {\n    currentSound.play();\n    // 在播放时也进行状态检测，防止URL已过期导致无声\n    getPlayerStore().checkPlaybackState(getPlayerStore().playMusic);\n  }\n};\n\nexport const pause = () => {\n  const currentSound = audioService.getCurrentSound();\n  if (currentSound) {\n    try {\n      // 保存当前播放进度\n      const currentTime = currentSound.seek() as number;\n      if (getPlayerStore().playMusic && getPlayerStore().playMusic.id) {\n        localStorage.setItem(\n          'playProgress',\n          JSON.stringify({\n            songId: getPlayerStore().playMusic.id,\n            progress: currentTime\n          })\n        );\n      }\n\n      audioService.pause();\n    } catch (error) {\n      console.error('暂停播放出错:', error);\n    }\n  }\n};\n\n// 歌词矫正时间映射（每首歌独立）\nconst CORRECTION_KEY = 'lyric-correction-map';\nconst correctionTimeMap = ref<Record<string, number>>({});\n\n// 初始化 correctionTimeMap\nconst loadCorrectionMap = () => {\n  try {\n    const raw = localStorage.getItem(CORRECTION_KEY);\n    correctionTimeMap.value = raw ? JSON.parse(raw) : {};\n  } catch {\n    correctionTimeMap.value = {};\n  }\n};\nconst saveCorrectionMap = () => {\n  localStorage.setItem(CORRECTION_KEY, JSON.stringify(correctionTimeMap.value));\n};\n\nloadCorrectionMap();\n\n// 歌词矫正时间，当前歌曲\nexport const correctionTime = ref(0);\n\n// 设置歌词矫正时间的监听器\nconst setupCorrectionTimeWatcher = () => {\n  // 切歌时自动读取矫正时间\n  watch(\n    () => playMusic.value?.id,\n    (id) => {\n      if (!id) return;\n      correctionTime.value = correctionTimeMap.value[id] ?? 0;\n    },\n    { immediate: true }\n  );\n};\n\n/**\n * 调整歌词矫正时间（每首歌独立）\n * @param delta 增加/减少的秒数（正为加，负为减）\n */\nexport const adjustCorrectionTime = (delta: number) => {\n  const id = playMusic.value?.id;\n  if (!id) return;\n  const newVal = Math.max(-10, Math.min(10, (correctionTime.value ?? 0) + delta));\n  correctionTime.value = newVal;\n  correctionTimeMap.value[id] = newVal;\n  saveCorrectionMap();\n};\n\n// 获取当前播放歌词\nexport const isCurrentLrc = (index: number, time: number): boolean => {\n  const currentTime = lrcTimeArray.value[index];\n\n  // 如果是最后一句歌词，只需要判断时间是否大于等于当前句的开始时间\n  if (index === lrcTimeArray.value.length - 1) {\n    const correctedTime = time + correctionTime.value;\n    return correctedTime >= currentTime;\n  }\n\n  // 非最后一句歌词，需要判断时间在当前句和下一句之间\n  const nextTime = lrcTimeArray.value[index + 1];\n  const correctedTime = time + correctionTime.value;\n  return correctedTime >= currentTime && correctedTime < nextTime;\n};\n\n// 获取当前播放歌词INDEX\nexport const getLrcIndex = (time: number): number => {\n  const correctedTime = time + correctionTime.value;\n\n  // 如果歌词数组为空，返回当前索引\n  if (lrcTimeArray.value.length === 0) {\n    return nowIndex.value;\n  }\n\n  // 处理最后一句歌词的情况\n  const lastIndex = lrcTimeArray.value.length - 1;\n  if (correctedTime >= lrcTimeArray.value[lastIndex]) {\n    nowIndex.value = lastIndex;\n    return lastIndex;\n  }\n\n  // 查找当前时间对应的歌词索引\n  for (let i = 0; i < lrcTimeArray.value.length - 1; i++) {\n    const currentTime = lrcTimeArray.value[i];\n    const nextTime = lrcTimeArray.value[i + 1];\n\n    if (correctedTime >= currentTime && correctedTime < nextTime) {\n      nowIndex.value = i;\n      return i;\n    }\n  }\n\n  return nowIndex.value;\n};\n\n// 获取当前播放歌词进度\nconst currentLrcTiming = computed(() => {\n  const start = lrcTimeArray.value[nowIndex.value] || 0;\n  const end = lrcTimeArray.value[nowIndex.value + 1] || start + 1;\n  return { start, end };\n});\n\n// 获取歌词样式\nexport const getLrcStyle = (index: number) => {\n  const currentTime = nowTime.value + correctionTime.value;\n  const start = lrcTimeArray.value[index];\n  const end = lrcTimeArray.value[index + 1] ?? start + 1;\n\n  if (currentTime >= start && currentTime < end) {\n    // 当前句，显示进度\n    const progress = ((currentTime - start) / (end - start)) * 100;\n    return {\n      backgroundImage: `linear-gradient(to right, #ffffff ${progress}%, #ffffff8a ${progress}%)`,\n      backgroundClip: 'text',\n      WebkitBackgroundClip: 'text',\n      color: 'transparent',\n      transition: 'background-image 0.1s linear'\n    };\n  }\n  // 其它句\n  return {};\n};\n\n// 播放进度\nexport const useLyricProgress = () => {\n  // 如果已经在全局更新进度，立即返回\n  return {\n    getLrcStyle\n  };\n};\n\n// 设置当前播放时间\nexport const setAudioTime = (index: number) => {\n  const currentSound = sound.value;\n  if (!currentSound) return;\n\n  currentSound.seek(lrcTimeArray.value[index]);\n  currentSound.play();\n};\n\n// 获取当前播放的歌词\nexport const getCurrentLrc = () => {\n  const index = getLrcIndex(nowTime.value);\n  return {\n    currentLrc: lrcArray.value[index],\n    nextLrc: lrcArray.value[index + 1]\n  };\n};\n\n// 获取一句歌词播放时间几秒到几秒\nexport const getLrcTimeRange = (index: number) => ({\n  currentTime: lrcTimeArray.value[index],\n  nextTime: lrcTimeArray.value[index + 1]\n});\n\n// 监听歌词数组变化，当切换歌曲时重新初始化歌词窗口\nwatch(\n  () => lrcArray.value,\n  (newLrcArray) => {\n    if (newLrcArray.length > 0 && isElectron && isLyricWindowOpen.value) {\n      sendLyricToWin();\n    }\n  }\n);\n\n// 发送歌词更新数据\nexport const sendLyricToWin = () => {\n  if (!isElectron || !isLyricWindowOpen.value) {\n    return;\n  }\n\n  // 检查是否有播放的歌曲\n  if (!playMusic.value || !playMusic.value.id) {\n    return;\n  }\n\n  try {\n    // 记录歌词发送状态\n    if (lrcArray.value && lrcArray.value.length > 0) {\n      const nowIndex = getLrcIndex(nowTime.value);\n      // 构建完整的歌词更新数据\n      const updateData = {\n        type: 'full',\n        nowIndex,\n        nowTime: nowTime.value,\n        startCurrentTime: lrcTimeArray.value[nowIndex] || 0,\n        nextTime: lrcTimeArray.value[nowIndex + 1] || 0,\n        isPlay: getPlayerStore().play,\n        lrcArray: lrcArray.value,\n        lrcTimeArray: lrcTimeArray.value,\n        allTime: allTime.value,\n        playMusic: playMusic.value\n      };\n\n      // 发送数据到歌词窗口\n      window.api.sendLyric(JSON.stringify(updateData));\n    } else {\n      console.log('No lyric data available, sending empty lyric message');\n\n      // 发送没有歌词的提示\n      const emptyLyricData = {\n        type: 'empty',\n        nowIndex: 0,\n        nowTime: nowTime.value,\n        startCurrentTime: 0,\n        nextTime: 0,\n        isPlay: getPlayerStore().play,\n        lrcArray: [{ text: '当前歌曲暂无歌词', trText: '' }],\n        lrcTimeArray: [0],\n        allTime: allTime.value,\n        playMusic: playMusic.value\n      };\n      window.api.sendLyric(JSON.stringify(emptyLyricData));\n    }\n  } catch (error) {\n    console.error('Error sending lyric update:', error);\n  }\n};\n\n// 歌词同步定时器\nlet lyricSyncInterval: any = null;\n\n// 开始歌词同步\nconst startLyricSync = () => {\n  // 清除已有的定时器\n  if (lyricSyncInterval) {\n    clearInterval(lyricSyncInterval);\n  }\n\n  // 每秒同步一次歌词数据\n  lyricSyncInterval = setInterval(() => {\n    if (isElectron && isLyricWindowOpen.value && getPlayerStore().play && playMusic.value?.id) {\n      // 发送当前播放进度的更新\n      try {\n        const updateData = {\n          type: 'update',\n          nowIndex: getLrcIndex(nowTime.value),\n          nowTime: nowTime.value,\n          isPlay: getPlayerStore().play\n        };\n        window.api.sendLyric(JSON.stringify(updateData));\n      } catch (error) {\n        console.error('发送歌词进度更新失败:', error);\n      }\n    }\n  }, 1000);\n};\n\n// 停止歌词同步\nconst stopLyricSync = () => {\n  if (lyricSyncInterval) {\n    clearInterval(lyricSyncInterval);\n    lyricSyncInterval = null;\n  }\n};\n\n// 修改openLyric函数，添加定时同步\nexport const openLyric = () => {\n  if (!isElectron) return;\n\n  // 检查是否有播放中的歌曲\n  if (!playMusic.value || !playMusic.value.id) {\n    console.log('没有正在播放的歌曲，无法打开歌词窗口');\n    return;\n  }\n\n  console.log('Opening lyric window with current song:', playMusic.value?.name);\n\n  isLyricWindowOpen.value = !isLyricWindowOpen.value;\n  if (isLyricWindowOpen.value) {\n    // 立即打开窗口\n    window.api.openLyric();\n\n    // 确保有歌词数据，如果没有，则使用默认的\"无歌词\"提示\n    if (!lrcArray.value || lrcArray.value.length === 0) {\n      // 如果当前播放的歌曲有ID但没有歌词，则尝试加载歌词\n      console.log('尝试加载歌词数据...');\n      // 发送默认的\"无歌词\"数据\n      const emptyLyricData = {\n        type: 'empty',\n        nowIndex: 0,\n        nowTime: nowTime.value,\n        startCurrentTime: 0,\n        nextTime: 0,\n        isPlay: getPlayerStore().play,\n        lrcArray: [{ text: '加载歌词中...', trText: '' }],\n        lrcTimeArray: [0],\n        allTime: allTime.value,\n        playMusic: playMusic.value\n      };\n      window.api.sendLyric(JSON.stringify(emptyLyricData));\n    } else {\n      // 发送完整歌词数据\n      sendLyricToWin();\n    }\n\n    // 设置定时器，确保500ms后再次发送数据，以防窗口加载延迟\n    setTimeout(() => {\n      sendLyricToWin();\n    }, 500);\n\n    // 启动歌词同步\n    startLyricSync();\n  } else {\n    closeLyric();\n    // 停止歌词同步\n    stopLyricSync();\n  }\n};\n\n// 修改closeLyric函数，确保停止定时同步\nexport const closeLyric = () => {\n  if (!isElectron) return;\n  isLyricWindowOpen.value = false; // 确保状态更新\n  windowData.electron.ipcRenderer.send('close-lyric');\n\n  // 停止歌词同步\n  stopLyricSync();\n};\n\n// 设置播放状态监听器\nconst setupPlayStateWatcher = () => {\n  // 在组件挂载时设置对播放状态的监听\n  watch(\n    () => getPlayerStore().play,\n    (isPlaying) => {\n      // 如果歌词窗口打开，根据播放状态控制同步\n      if (isElectron && isLyricWindowOpen.value) {\n        if (isPlaying) {\n          startLyricSync();\n        } else {\n          // 如果暂停播放，发送一次暂停状态的更新\n          const pauseData = {\n            type: 'update',\n            isPlay: false\n          };\n          window.api.sendLyric(JSON.stringify(pauseData));\n        }\n      }\n    }\n  );\n};\n\n// 在组件卸载时清理资源\nonUnmounted(() => {\n  stopLyricSync();\n});\n\n// 导出歌词解析函数供外部使用\nexport { parseLyricsString };\n\n// 添加播放控制命令监听\nif (isElectron) {\n  windowData.electron.ipcRenderer.on('lyric-control-back', (_, command: string) => {\n    switch (command) {\n      case 'playpause':\n        if (getPlayerStore().play) {\n          getPlayerStore().setPlayMusic(false);\n          audioService.getCurrentSound()?.pause();\n        } else {\n          getPlayerStore().setPlayMusic(true);\n\n          audioService.getCurrentSound()?.play();\n        }\n        break;\n      case 'prev':\n        getPlayerStore().prevPlay();\n        break;\n      case 'next':\n        getPlayerStore().nextPlay();\n        break;\n      case 'close':\n        isLyricWindowOpen.value = false; // 确保状态更新\n        break;\n      default:\n        console.log('Unknown command:', command);\n        break;\n    }\n  });\n}\n\n// 在组件挂载时设置监听器\nexport const initAudioListeners = async () => {\n  try {\n    // 确保有正在播放的音乐\n    if (!getPlayerStore().playMusic || !getPlayerStore().playMusic.id) {\n      console.log('没有正在播放的音乐，跳过音频监听器初始化');\n      return;\n    }\n\n    // 确保有音频实例\n    const initialSound = audioService.getCurrentSound();\n    if (!initialSound) {\n      console.log('没有音频实例，等待音频加载...');\n      // 等待音频加载完成\n      await new Promise<void>((resolve) => {\n        const checkInterval = setInterval(() => {\n          const sound = audioService.getCurrentSound();\n          if (sound) {\n            clearInterval(checkInterval);\n            resolve();\n          }\n        }, 100);\n\n        // 设置超时\n        setTimeout(() => {\n          clearInterval(checkInterval);\n          console.log('等待音频加载超时');\n          resolve();\n        }, 5000);\n      });\n    }\n\n    // 初始化音频监听器\n    setupAudioListeners();\n\n    // 监听歌词窗口关闭事件\n    if (isElectron) {\n      window.api.onLyricWindowClosed(() => {\n        isLyricWindowOpen.value = false;\n      });\n    }\n\n    // 获取最新的音频实例\n    const finalSound = audioService.getCurrentSound();\n    if (finalSound) {\n      // 更新全局 sound 引用\n      sound.value = finalSound;\n    } else {\n      console.warn('无法获取音频实例，跳过进度更新初始化');\n    }\n  } catch (error) {\n    console.error('初始化音频监听器失败:', error);\n  }\n};\n\n// 监听URL过期事件，自动重新获取URL并恢复播放\naudioService.on('url_expired', async (expiredTrack) => {\n  if (!expiredTrack) return;\n\n  console.log('检测到URL过期事件，准备重新获取URL', expiredTrack.name);\n\n  try {\n    // 使用 handlePlayMusic 重新播放，它会自动处理 URL 获取和状态跟踪\n    // 我们将 isFirstPlay 设为 true 以强制获取新 URL\n    const trackToPlay = {\n      ...expiredTrack,\n      isFirstPlay: true,\n      playMusicUrl: undefined\n    };\n\n    await getPlayerStore().handlePlayMusic(trackToPlay, getPlayerStore().play);\n\n    message.success('已自动恢复播放');\n  } catch (error) {\n    console.error('处理URL过期事件失败:', error);\n    message.error('恢复播放失败，请手动点击播放');\n  }\n});\n\n// 添加音频就绪事件监听器\nwindow.addEventListener('audio-ready', ((event: CustomEvent) => {\n  try {\n    const { sound: newSound } = event.detail;\n    if (newSound) {\n      // 更新本地 sound 引用\n      sound.value = newSound as Howl;\n\n      // 设置音频监听器\n      setupAudioListeners();\n\n      // 获取当前播放位置并更新显示\n      const currentPosition = newSound.seek() as number;\n      if (typeof currentPosition === 'number' && !Number.isNaN(currentPosition)) {\n        nowTime.value = currentPosition;\n      }\n\n      console.log('音频就绪，已设置监听器并更新进度显示');\n    }\n  } catch (error) {\n    console.error('处理音频就绪事件出错:', error);\n  }\n}) as EventListener);\n"
  },
  {
    "path": "src/renderer/hooks/PlaylistHistoryHook.ts",
    "content": "import { useLocalStorage } from '@vueuse/core';\nimport { ref, watch } from 'vue';\n\n// 歌单历史记录类型\nexport interface PlaylistHistoryItem {\n  id: number;\n  name: string;\n  coverImgUrl?: string;\n  picUrl?: string; // 兼容字段\n  trackCount?: number;\n  playCount?: number;\n  creator?: {\n    nickname: string;\n    userId: number;\n  };\n  count?: number; // 播放次数\n  lastPlayTime?: number; // 最后播放时间\n}\n\nexport const usePlaylistHistory = () => {\n  const playlistHistory = useLocalStorage<PlaylistHistoryItem[]>('playlistHistory', []);\n\n  const addPlaylist = (playlist: PlaylistHistoryItem) => {\n    const index = playlistHistory.value.findIndex((item) => item.id === playlist.id);\n    const now = Date.now();\n\n    if (index !== -1) {\n      // 如果已存在，更新播放次数和时间，并移到最前面\n      playlistHistory.value[index].count = (playlistHistory.value[index].count || 0) + 1;\n      playlistHistory.value[index].lastPlayTime = now;\n      playlistHistory.value.unshift(playlistHistory.value.splice(index, 1)[0]);\n    } else {\n      // 如果不存在，添加新记录\n      playlistHistory.value.unshift({\n        ...playlist,\n        count: 1,\n        lastPlayTime: now\n      });\n    }\n  };\n\n  const delPlaylist = (playlist: PlaylistHistoryItem) => {\n    const index = playlistHistory.value.findIndex((item) => item.id === playlist.id);\n    if (index !== -1) {\n      playlistHistory.value.splice(index, 1);\n    }\n  };\n\n  const playlistList = ref(playlistHistory.value);\n\n  watch(\n    () => playlistHistory.value,\n    () => {\n      playlistList.value = playlistHistory.value;\n    },\n    { deep: true }\n  );\n\n  return {\n    playlistHistory,\n    playlistList,\n    addPlaylist,\n    delPlaylist\n  };\n};\n"
  },
  {
    "path": "src/renderer/hooks/useArtist.ts",
    "content": "import { useRouter } from 'vue-router';\n\nexport const useArtist = () => {\n  const router = useRouter();\n\n  /**\n   * 跳转到歌手详情页\n   * @param id 歌手ID\n   */\n  const navigateToArtist = (id: number) => {\n    router.push(`/artist/detail/${id}`);\n  };\n\n  return {\n    navigateToArtist\n  };\n};\n"
  },
  {
    "path": "src/renderer/hooks/useDownload.ts",
    "content": "import { cloneDeep } from 'lodash';\nimport { useMessage } from 'naive-ui';\nimport { ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { getSongUrl } from '@/store/modules/player';\nimport type { SongResult } from '@/types/music';\nimport { isElectron } from '@/utils';\n\nconst ipcRenderer = isElectron ? window.electron.ipcRenderer : null;\n\n// 全局下载管理（闭包模式）\nconst createDownloadManager = () => {\n  // 正在下载的文件集合\n  const activeDownloads = new Set<string>();\n\n  // 已经发送了通知的文件集合（避免重复通知）\n  const notifiedDownloads = new Set<string>();\n\n  // 事件监听器是否已初始化\n  let isInitialized = false;\n\n  // 监听器引用（用于清理）\n  let completeListener: ((event: any, data: any) => void) | null = null;\n  let errorListener: ((event: any, data: any) => void) | null = null;\n\n  return {\n    // 添加下载\n    addDownload: (filename: string) => {\n      activeDownloads.add(filename);\n    },\n\n    // 移除下载\n    removeDownload: (filename: string) => {\n      activeDownloads.delete(filename);\n      // 延迟清理通知记录\n      setTimeout(() => {\n        notifiedDownloads.delete(filename);\n      }, 5000);\n    },\n\n    // 标记文件已通知\n    markNotified: (filename: string) => {\n      notifiedDownloads.add(filename);\n    },\n\n    // 检查文件是否已通知\n    isNotified: (filename: string) => {\n      return notifiedDownloads.has(filename);\n    },\n\n    // 清理所有下载\n    clearDownloads: () => {\n      activeDownloads.clear();\n      notifiedDownloads.clear();\n    },\n\n    // 初始化事件监听器\n    initEventListeners: (message: any, t: any) => {\n      if (isInitialized) return;\n\n      // 移除可能存在的旧监听器\n      if (completeListener) {\n        ipcRenderer?.removeListener('music-download-complete', completeListener);\n      }\n\n      if (errorListener) {\n        ipcRenderer?.removeListener('music-download-error', errorListener);\n      }\n\n      // 创建新的监听器\n      completeListener = (_event, data) => {\n        if (!data.filename || !activeDownloads.has(data.filename)) return;\n\n        // 如果该文件已经通知过，则跳过\n        if (notifiedDownloads.has(data.filename)) return;\n\n        // 标记为已通知\n        notifiedDownloads.add(data.filename);\n\n        // 从活动下载移除\n        activeDownloads.delete(data.filename);\n      };\n\n      errorListener = (_event, data) => {\n        if (!data.filename || !activeDownloads.has(data.filename)) return;\n\n        // 如果该文件已经通知过，则跳过\n        if (notifiedDownloads.has(data.filename)) return;\n\n        // 标记为已通知\n        notifiedDownloads.add(data.filename);\n\n        // 显示失败通知\n        message.error(\n          t('songItem.message.downloadFailed', {\n            filename: data.filename,\n            error: data.error || '未知错误'\n          })\n        );\n\n        // 从活动下载移除\n        activeDownloads.delete(data.filename);\n      };\n\n      // 添加监听器\n      ipcRenderer?.on('music-download-complete', completeListener);\n      ipcRenderer?.on('music-download-error', errorListener);\n\n      isInitialized = true;\n    },\n\n    // 清理事件监听器\n    cleanupEventListeners: () => {\n      if (!isInitialized) return;\n\n      if (completeListener) {\n        ipcRenderer?.removeListener('music-download-complete', completeListener);\n        completeListener = null;\n      }\n\n      if (errorListener) {\n        ipcRenderer?.removeListener('music-download-error', errorListener);\n        errorListener = null;\n      }\n\n      isInitialized = false;\n    },\n\n    // 获取活跃下载数量\n    getActiveDownloadCount: () => {\n      return activeDownloads.size;\n    },\n\n    // 检查是否有特定文件正在下载\n    hasDownload: (filename: string) => {\n      return activeDownloads.has(filename);\n    }\n  };\n};\n\n// 创建单例下载管理器\nconst downloadManager = createDownloadManager();\n\nexport const useDownload = () => {\n  const { t } = useI18n();\n  const message = useMessage();\n  const isDownloading = ref(false);\n\n  // 初始化事件监听器\n  downloadManager.initEventListeners(message, t);\n\n  /**\n   * 下载单首音乐\n   * @param song 歌曲信息\n   * @returns Promise<void>\n   */\n  const downloadMusic = async (song: SongResult) => {\n    if (isDownloading.value) {\n      message.warning(t('songItem.message.downloading'));\n      return;\n    }\n\n    try {\n      isDownloading.value = true;\n\n      const musicUrl = (await getSongUrl(song.id as number, cloneDeep(song), true)) as any;\n      if (!musicUrl) {\n        throw new Error(t('songItem.message.getUrlFailed'));\n      }\n\n      // 构建文件名\n      const artistNames = (song.ar || song.song?.artists)?.map((a) => a.name).join(',');\n      const filename = `${song.name} - ${artistNames}`;\n\n      // 检查是否已在下载\n      if (downloadManager.hasDownload(filename)) {\n        isDownloading.value = false;\n        return;\n      }\n\n      // 添加到活动下载集合\n      downloadManager.addDownload(filename);\n\n      const songData = cloneDeep(song);\n      songData.ar = songData.ar || songData.song?.artists;\n\n      // 发送下载请求\n      ipcRenderer?.send('download-music', {\n        url: typeof musicUrl === 'string' ? musicUrl : musicUrl.url,\n        filename,\n        songInfo: {\n          ...songData,\n          downloadTime: Date.now()\n        },\n        type: musicUrl.type\n      });\n\n      message.success(t('songItem.message.downloadQueued'));\n\n      // 简化的监听逻辑，基本通知由全局监听器处理\n      setTimeout(() => {\n        isDownloading.value = false;\n      }, 2000);\n    } catch (error: any) {\n      console.error('Download error:', error);\n      isDownloading.value = false;\n      message.error(error.message || t('songItem.message.downloadFailed'));\n    }\n  };\n\n  /**\n   * 批量下载音乐\n   * @param songs 歌曲列表\n   * @returns Promise<void>\n   */\n  const batchDownloadMusic = async (songs: SongResult[]) => {\n    if (isDownloading.value) {\n      message.warning(t('favorite.downloading'));\n      return;\n    }\n\n    if (songs.length === 0) {\n      message.warning(t('favorite.selectSongsFirst'));\n      return;\n    }\n\n    try {\n      isDownloading.value = true;\n      message.success(t('favorite.downloading'));\n\n      let successCount = 0;\n      let failCount = 0;\n      const totalCount = songs.length;\n\n      // 下载进度追踪\n      const trackProgress = () => {\n        if (successCount + failCount === totalCount) {\n          isDownloading.value = false;\n          message.success(t('favorite.downloadSuccess'));\n        }\n      };\n\n      // 并行获取所有歌曲的下载链接\n      const downloadUrls = await Promise.all(\n        songs.map(async (song) => {\n          try {\n            const data = (await getSongUrl(song.id, song, true)) as any;\n            return { song, ...data };\n          } catch (error) {\n            console.error(`获取歌曲 ${song.name} 下载链接失败:`, error);\n            failCount++;\n            return { song, url: null };\n          }\n        })\n      );\n\n      // 开始下载有效的链接\n      downloadUrls.forEach(({ song, url, type }) => {\n        if (!url) {\n          failCount++;\n          trackProgress();\n          return;\n        }\n\n        const songData = cloneDeep(song);\n        const filename = `${song.name} - ${(song.ar || song.song?.artists)?.map((a) => a.name).join(',')}`;\n\n        // 检查是否已在下载\n        if (downloadManager.hasDownload(filename)) {\n          failCount++;\n          trackProgress();\n          return;\n        }\n\n        // 添加到活动下载集合\n        downloadManager.addDownload(filename);\n\n        const songInfo = {\n          ...songData,\n          ar: songData.ar || songData.song?.artists,\n          downloadTime: Date.now()\n        };\n\n        ipcRenderer?.send('download-music', {\n          url,\n          filename,\n          songInfo,\n          type\n        });\n\n        successCount++;\n      });\n\n      // 所有下载开始后，检查进度\n      trackProgress();\n    } catch (error) {\n      console.error('下载失败:', error);\n      isDownloading.value = false;\n      message.destroyAll();\n      message.error(t('favorite.downloadFailed'));\n    }\n  };\n\n  return {\n    isDownloading,\n    downloadMusic,\n    batchDownloadMusic\n  };\n};\n"
  },
  {
    "path": "src/renderer/hooks/usePlayMode.ts",
    "content": "import { computed } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { usePlayerStore } from '@/store/modules/player';\n\n/**\n * 播放模式相关的 Hook\n * 提供播放模式的图标、文本和切换功能\n */\nexport function usePlayMode() {\n  const { t } = useI18n();\n  const playerStore = usePlayerStore();\n\n  // 当前播放模式\n  const playMode = computed(() => playerStore.playMode);\n\n  // 播放模式图标\n  const playModeIcon = computed(() => {\n    switch (playMode.value) {\n      case 0:\n        return 'ri-repeat-2-line';\n      case 1:\n        return 'ri-repeat-one-line';\n      case 2:\n        return 'ri-shuffle-line';\n      case 3:\n        return 'ri-heart-pulse-line';\n      default:\n        return 'ri-repeat-2-line';\n    }\n  });\n\n  // 播放模式文本\n  const playModeText = computed(() => {\n    switch (playMode.value) {\n      case 0:\n        return t('player.playBar.playMode.sequence');\n      case 1:\n        return t('player.playBar.playMode.loop');\n      case 2:\n        return t('player.playBar.playMode.random');\n      case 3:\n        return t('player.playBar.intelligenceMode.title');\n      default:\n        return t('player.playBar.playMode.sequence');\n    }\n  });\n\n  // 切换播放模式\n  const togglePlayMode = () => {\n    playerStore.togglePlayMode();\n  };\n\n  return {\n    playMode,\n    playModeIcon,\n    playModeText,\n    togglePlayMode\n  };\n}\n"
  },
  {
    "path": "src/renderer/hooks/usePlayerHooks.ts",
    "content": "import { cloneDeep } from 'lodash';\nimport { createDiscreteApi } from 'naive-ui';\n\nimport i18n from '@/../i18n/renderer';\nimport { getBilibiliAudioUrl } from '@/api/bilibili';\nimport { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';\nimport { playbackRequestManager } from '@/services/playbackRequestManager';\nimport { SongSourceConfigManager } from '@/services/SongSourceConfigManager';\nimport type { ILyric, ILyricText, IWordData, SongResult } from '@/types/music';\nimport { getImgUrl } from '@/utils';\nimport { getImageLinearBackground } from '@/utils/linearColor';\nimport { parseLyrics as parseYrcLyrics } from '@/utils/yrcParser';\n\nconst { message } = createDiscreteApi(['message']);\n\n/**\n * 获取歌曲播放URL（独立函数）\n */\nexport const getSongUrl = async (\n  id: string | number,\n  songData: SongResult,\n  isDownloaded: boolean = false,\n  requestId?: string\n) => {\n  const numericId = typeof id === 'string' ? parseInt(id, 10) : id;\n\n  // 动态导入 settingsStore\n  const { useSettingsStore } = await import('@/store/modules/settings');\n  const settingsStore = useSettingsStore();\n\n  try {\n    // 在开始处理前验证请求\n    if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n      console.log(`[getSongUrl] 请求已失效: ${requestId}`);\n      throw new Error('Request cancelled');\n    }\n\n    if (songData.playMusicUrl) {\n      return songData.playMusicUrl;\n    }\n\n    if (songData.source === 'bilibili' && songData.bilibiliData) {\n      console.log('加载B站音频URL');\n      if (!songData.playMusicUrl && songData.bilibiliData.bvid && songData.bilibiliData.cid) {\n        try {\n          songData.playMusicUrl = await getBilibiliAudioUrl(\n            songData.bilibiliData.bvid,\n            songData.bilibiliData.cid\n          );\n          // 验证请求\n          if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n            console.log(`[getSongUrl] 获取B站URL后请求已失效: ${requestId}`);\n            throw new Error('Request cancelled');\n          }\n          return songData.playMusicUrl;\n        } catch (error) {\n          console.error('重启后获取B站音频URL失败:', error);\n          return '';\n        }\n      }\n      return songData.playMusicUrl || '';\n    }\n\n    // ==================== 自定义API最优先 ====================\n    const globalSources = settingsStore.setData.enabledMusicSources || [];\n    const useCustomApiGlobally = globalSources.includes('custom');\n\n    const songConfig = SongSourceConfigManager.getConfig(id);\n    const useCustomApiForSong = songConfig?.sources.includes('custom' as any) ?? false;\n\n    // 如果全局或歌曲专属设置中启用了自定义API，则最优先尝试\n    if ((useCustomApiGlobally || useCustomApiForSong) && settingsStore.setData.customApiPlugin) {\n      console.log(`优先级 1: 尝试使用自定义API解析歌曲 ${id}...`);\n      try {\n        const { parseFromCustomApi } = await import('@/api/parseFromCustomApi');\n        const customResult = await parseFromCustomApi(\n          numericId,\n          cloneDeep(songData),\n          settingsStore.setData.musicQuality || 'higher'\n        );\n\n        // 验证请求\n        if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n          console.log(`[getSongUrl] 自定义API解析后请求已失效: ${requestId}`);\n          throw new Error('Request cancelled');\n        }\n\n        if (\n          customResult &&\n          customResult.data &&\n          customResult.data.data &&\n          customResult.data.data.url\n        ) {\n          console.log('自定义API解析成功！');\n          if (isDownloaded) return customResult.data.data as any;\n          return customResult.data.data.url;\n        } else {\n          console.log('自定义API解析失败，将使用默认降级流程...');\n          message.warning(i18n.global.t('player.reparse.customApiFailed'));\n        }\n      } catch (error) {\n        console.error('调用自定义API时发生错误:', error);\n        if ((error as Error).message === 'Request cancelled') {\n          throw error;\n        }\n        message.error(i18n.global.t('player.reparse.customApiError'));\n      }\n    }\n\n    // 如果有自定义音源设置，直接使用getParsingMusicUrl获取URL\n    if (songConfig && songData.source !== 'bilibili') {\n      try {\n        console.log(`使用自定义音源解析歌曲 ID: ${id}`);\n        const res = await getParsingMusicUrl(numericId, cloneDeep(songData));\n        console.log('res', res);\n\n        // 验证请求\n        if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n          console.log(`[getSongUrl] 自定义音源解析后请求已失效: ${requestId}`);\n          throw new Error('Request cancelled');\n        }\n\n        if (res && res.data && res.data.data && res.data.data.url) {\n          return res.data.data.url;\n        }\n        console.warn('自定义音源解析失败，使用默认音源');\n      } catch (error) {\n        console.error('error', error);\n        if ((error as Error).message === 'Request cancelled') {\n          throw error;\n        }\n        console.error('自定义音源解析出错:', error);\n      }\n    }\n\n    // 正常获取URL流程\n    const { data } = await getMusicUrl(numericId, isDownloaded);\n\n    // 验证请求\n    if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n      console.log(`[getSongUrl] 获取官方URL后请求已失效: ${requestId}`);\n      throw new Error('Request cancelled');\n    }\n\n    if (data && data.data && data.data[0]) {\n      const songDetail = data.data[0];\n      const hasNoUrl = !songDetail.url;\n      const isTrial = !!songDetail.freeTrialInfo;\n\n      if (hasNoUrl || isTrial) {\n        console.log(`官方URL无效 (无URL: ${hasNoUrl}, 试听: ${isTrial})，进入内置备用解析...`);\n        const res = await getParsingMusicUrl(numericId, cloneDeep(songData));\n        // 验证请求\n        if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n          console.log(`[getSongUrl] 备用解析后请求已失效: ${requestId}`);\n          throw new Error('Request cancelled');\n        }\n        if (isDownloaded) return res?.data?.data as any;\n        return res?.data?.data?.url || null;\n      }\n\n      console.log('官方API解析成功！');\n      if (isDownloaded) return songDetail as any;\n      return songDetail.url;\n    }\n\n    console.log('官方API返回数据结构异常，进入内置备用解析...');\n    const res = await getParsingMusicUrl(numericId, cloneDeep(songData));\n    // 验证请求\n    if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n      console.log(`[getSongUrl] 备用解析后请求已失效: ${requestId}`);\n      throw new Error('Request cancelled');\n    }\n    if (isDownloaded) return res?.data?.data as any;\n    return res?.data?.data?.url || null;\n  } catch (error) {\n    if ((error as Error).message === 'Request cancelled') {\n      throw error;\n    }\n    console.error('官方API请求失败，进入内置备用解析流程:', error);\n    const res = await getParsingMusicUrl(numericId, cloneDeep(songData));\n    if (isDownloaded) return res?.data?.data as any;\n    return res?.data?.data?.url || null;\n  }\n};\n\n/**\n * useSongUrl hook（兼容旧代码）\n */\nexport const useSongUrl = () => {\n  return { getSongUrl };\n};\n\n/**\n * 使用新的yrcParser解析歌词（独立函数）\n */\nconst parseLyrics = (lyricsString: string): { lyrics: ILyricText[]; times: number[] } => {\n  if (!lyricsString || typeof lyricsString !== 'string') {\n    return { lyrics: [], times: [] };\n  }\n\n  try {\n    const parseResult = parseYrcLyrics(lyricsString);\n\n    if (!parseResult.success) {\n      console.error('歌词解析失败:', parseResult.error.message);\n      return { lyrics: [], times: [] };\n    }\n\n    const { lyrics: parsedLyrics } = parseResult.data;\n    const lyrics: ILyricText[] = [];\n    const times: number[] = [];\n\n    for (const line of parsedLyrics) {\n      // 检查是否有逐字歌词\n      const hasWords = line.words && line.words.length > 0;\n\n      lyrics.push({\n        text: line.fullText,\n        trText: '', // 翻译文本稍后处理\n        words: hasWords ? (line.words as IWordData[]) : undefined,\n        hasWordByWord: hasWords,\n        startTime: line.startTime,\n        duration: line.duration\n      });\n\n      // 时间数组使用秒为单位（与原有逻辑保持一致）\n      times.push(line.startTime / 1000);\n    }\n\n    return { lyrics, times };\n  } catch (error) {\n    console.error('解析歌词时发生错误:', error);\n    return { lyrics: [], times: [] };\n  }\n};\n\n/**\n * 加载歌词（独立函数）\n */\nexport const loadLrc = async (id: string | number): Promise<ILyric> => {\n  if (typeof id === 'string' && id.includes('--')) {\n    console.log('B站音频，无需加载歌词');\n    return {\n      lrcTimeArray: [],\n      lrcArray: [],\n      hasWordByWord: false\n    };\n  }\n\n  try {\n    const numericId = typeof id === 'string' ? parseInt(id, 10) : id;\n    const { data } = await getMusicLrc(numericId);\n    const { lyrics, times } = parseLyrics(data?.yrc?.lyric || data?.lrc?.lyric);\n\n    // 检查是否有逐字歌词\n    let hasWordByWord = false;\n    for (const lyric of lyrics) {\n      if (lyric.hasWordByWord) {\n        hasWordByWord = true;\n        break;\n      }\n    }\n\n    if (data.tlyric && data.tlyric.lyric) {\n      const { lyrics: tLyrics } = parseLyrics(data.tlyric.lyric);\n\n      // 按索引顺序一一对应翻译歌词\n      if (tLyrics.length === lyrics.length) {\n        // 数量相同，直接按索引对应\n        lyrics.forEach((item, index) => {\n          item.trText = item.text && tLyrics[index] ? tLyrics[index].text : '';\n        });\n      } else {\n        // 数量不同，构建时间戳映射并尝试匹配\n        const tLyricMap = new Map<number, string>();\n        tLyrics.forEach((lyric) => {\n          if (lyric.text && lyric.startTime !== undefined) {\n            const timeInSeconds = lyric.startTime / 1000;\n            tLyricMap.set(timeInSeconds, lyric.text);\n          }\n        });\n\n        // 为每句歌词查找最接近的翻译\n        lyrics.forEach((item, index) => {\n          if (!item.text) {\n            item.trText = '';\n            return;\n          }\n\n          const currentTime = times[index];\n          let closestTime = -1;\n          let minDiff = 2.0; // 最大允许差异2秒\n\n          // 查找最接近的时间戳\n          for (const [tTime] of tLyricMap.entries()) {\n            const diff = Math.abs(tTime - currentTime);\n            if (diff < minDiff) {\n              minDiff = diff;\n              closestTime = tTime;\n            }\n          }\n\n          item.trText = closestTime !== -1 ? tLyricMap.get(closestTime) || '' : '';\n        });\n      }\n    } else {\n      // 没有翻译歌词，清空 trText\n      lyrics.forEach((item) => {\n        item.trText = '';\n      });\n    }\n\n    return {\n      lrcTimeArray: times,\n      lrcArray: lyrics,\n      hasWordByWord\n    };\n  } catch (err) {\n    console.error('Error loading lyrics:', err);\n    return {\n      lrcTimeArray: [],\n      lrcArray: [],\n      hasWordByWord: false\n    };\n  }\n};\n\n/**\n * useLyrics hook（兼容旧代码）\n */\nexport const useLyrics = () => {\n  return { loadLrc, parseLyrics };\n};\n\n/**\n * 获取歌曲详情\n */\nexport const useSongDetail = () => {\n  const { getSongUrl } = useSongUrl();\n\n  const getSongDetail = async (playMusic: SongResult, requestId?: string) => {\n    // 验证请求\n    if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n      console.log(`[getSongDetail] 请求已失效: ${requestId}`);\n      throw new Error('Request cancelled');\n    }\n\n    if (playMusic.source === 'bilibili') {\n      try {\n        if (!playMusic.playMusicUrl && playMusic.bilibiliData) {\n          playMusic.playMusicUrl = await getBilibiliAudioUrl(\n            playMusic.bilibiliData.bvid,\n            playMusic.bilibiliData.cid\n          );\n        }\n\n        // 验证请求\n        if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n          console.log(`[getSongDetail] B站URL获取后请求已失效: ${requestId}`);\n          throw new Error('Request cancelled');\n        }\n\n        playMusic.playLoading = false;\n        return { ...playMusic } as SongResult;\n      } catch (error) {\n        console.error('获取B站音频详情失败:', error);\n        playMusic.playLoading = false;\n        throw error;\n      }\n    }\n\n    if (playMusic.expiredAt && playMusic.expiredAt < Date.now()) {\n      console.info(`歌曲已过期，重新获取: ${playMusic.name}`);\n      playMusic.playMusicUrl = undefined;\n    }\n\n    try {\n      const playMusicUrl =\n        playMusic.playMusicUrl || (await getSongUrl(playMusic.id, playMusic, false, requestId));\n\n      // 验证请求\n      if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n        console.log(`[getSongDetail] URL获取后请求已失效: ${requestId}`);\n        throw new Error('Request cancelled');\n      }\n\n      playMusic.createdAt = Date.now();\n      // 半小时后过期\n      playMusic.expiredAt = playMusic.createdAt + 1800000;\n      const { backgroundColor, primaryColor } =\n        playMusic.backgroundColor && playMusic.primaryColor\n          ? playMusic\n          : await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30'));\n\n      // 验证请求\n      if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n        console.log(`[getSongDetail] 背景色获取后请求已失效: ${requestId}`);\n        throw new Error('Request cancelled');\n      }\n\n      playMusic.playLoading = false;\n      return { ...playMusic, playMusicUrl, backgroundColor, primaryColor } as SongResult;\n    } catch (error) {\n      if ((error as Error).message === 'Request cancelled') {\n        throw error;\n      }\n      console.error('获取音频URL失败:', error);\n      playMusic.playLoading = false;\n      throw error;\n    }\n  };\n\n  return { getSongDetail };\n};\n"
  },
  {
    "path": "src/renderer/hooks/useSongItem.ts",
    "content": "import { useMessage } from 'naive-ui';\nimport { computed, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { usePlayerStore, useRecommendStore } from '@/store';\nimport type { SongResult } from '@/types/music';\nimport { getImgUrl } from '@/utils';\nimport { getImageBackground } from '@/utils/linearColor';\n\nimport { dislikeRecommendedSong } from '../api/music';\nimport { useArtist } from './useArtist';\nimport { useDownload } from './useDownload';\n\nexport function useSongItem(props: { item: SongResult; canRemove?: boolean }) {\n  const { t } = useI18n();\n  const playerStore = usePlayerStore();\n  const recommendStore = useRecommendStore();\n  const message = useMessage();\n  const { downloadMusic } = useDownload();\n  const { navigateToArtist } = useArtist();\n\n  // 状态变量\n  const showDropdown = ref(false);\n  const dropdownX = ref(0);\n  const dropdownY = ref(0);\n  const isHovering = ref(false);\n\n  // 计算属性\n  const play = computed(() => playerStore.isPlay);\n  const playMusic = computed(() => playerStore.playMusic);\n  const playLoading = computed(\n    () => playMusic.value.id === props.item.id && playMusic.value.playLoading\n  );\n  const isPlaying = computed(() => playMusic.value.id === props.item.id);\n\n  // 收藏与不喜欢状态\n  const isFavorite = computed(() => {\n    const numericId =\n      typeof props.item.id === 'string' ? parseInt(props.item.id, 10) : props.item.id;\n    return playerStore.favoriteList.includes(numericId);\n  });\n\n  const isDislike = computed(() => {\n    const numericId =\n      typeof props.item.id === 'string' ? parseInt(props.item.id, 10) : props.item.id;\n    return playerStore.dislikeList.includes(numericId);\n  });\n\n  // 获取艺术家列表\n  const artists = computed(() => {\n    return (props.item.ar || props.item.song?.artists)?.slice(0, 4) || [];\n  });\n\n  // 处理图片加载\n  const handleImageLoad = async (imageElement: HTMLImageElement) => {\n    if (!imageElement) return;\n\n    const { backgroundColor, primaryColor } = await getImageBackground(imageElement);\n    props.item.backgroundColor = backgroundColor;\n    props.item.primaryColor = primaryColor;\n  };\n\n  // 播放音乐\n  const playMusicEvent = async (item: SongResult) => {\n    try {\n      const result = await playerStore.setPlay(item);\n      if (!result) {\n        throw new Error('播放失败');\n      }\n      return true;\n    } catch (error) {\n      console.error('播放出错:', error);\n      return false;\n    }\n  };\n\n  // 切换收藏状态\n  const toggleFavorite = async (e: Event) => {\n    e && e.stopPropagation();\n    const numericId =\n      typeof props.item.id === 'string' ? parseInt(props.item.id, 10) : props.item.id;\n\n    if (isFavorite.value) {\n      playerStore.removeFromFavorite(numericId);\n    } else {\n      playerStore.addToFavorite(numericId);\n    }\n  };\n\n  // 判断当前歌曲是否为每日推荐歌曲\n  const isDailyRecommendSong = computed(() => {\n    return recommendStore.dailyRecommendSongs.some((song) => song.id === props.item.id);\n  });\n\n  // 切换不喜欢状态\n  const toggleDislike = async (e: Event) => {\n    e && e.stopPropagation();\n\n    if (isDislike.value) {\n      playerStore.removeFromDislikeList(props.item.id);\n      return;\n    }\n\n    playerStore.addToDislikeList(props.item.id);\n\n    // 只有当前歌曲是每日推荐歌曲时才调用接口\n    if (!isDailyRecommendSong.value) {\n      return;\n    }\n    try {\n      console.log('发送不感兴趣请求，歌曲ID:', props.item.id);\n      const numericId = typeof props.item.id === 'string' ? parseInt(props.item.id) : props.item.id;\n      const response = await dislikeRecommendedSong(numericId);\n      if (response.data.data) {\n        console.log(response);\n        const newSongData = response.data.data;\n        const newSong: SongResult = {\n          ...newSongData,\n          name: newSongData.name,\n          id: newSongData.id,\n          picUrl: newSongData.al?.picUrl || newSongData.album?.picUrl,\n          ar: newSongData.ar || newSongData.artists,\n          al: newSongData.al || newSongData.album,\n          song: {\n            ...newSongData.song,\n            id: newSongData.id,\n            name: newSongData.name,\n            artists: newSongData.ar || newSongData.artists,\n            album: newSongData.al || newSongData.album\n          },\n          source: 'netease',\n          count: 0\n        };\n        recommendStore.replaceSongInDailyRecommend(props.item.id, newSong);\n      } else {\n        console.warn('标记不感兴趣API成功，但未返回新歌曲。', response.data);\n      }\n    } catch (error) {\n      console.error('发送不感兴趣请求时出错:', error);\n    }\n  };\n\n  // 添加到下一首播放\n  const handlePlayNext = () => {\n    playerStore.addToNextPlay(props.item);\n    message.success(t('songItem.message.addedToNextPlay'));\n  };\n\n  // 获取歌曲时长\n  const getDuration = (item: SongResult): number => {\n    if (item.duration) return item.duration;\n    if (typeof item.dt === 'number') return item.dt;\n    return 0;\n  };\n\n  // 格式化时长\n  const formatDuration = (ms: number): string => {\n    if (!ms) return '--:--';\n    const totalSeconds = Math.floor(ms / 1000);\n    const minutes = Math.floor(totalSeconds / 60);\n    const seconds = totalSeconds % 60;\n    return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;\n  };\n\n  // 处理右键菜单\n  const handleContextMenu = (e: MouseEvent) => {\n    e.preventDefault();\n    showDropdown.value = true;\n    dropdownX.value = e.clientX;\n    dropdownY.value = e.clientY;\n  };\n\n  // 处理菜单点击\n  const handleMenuClick = (e: MouseEvent) => {\n    e.preventDefault();\n    showDropdown.value = true;\n    dropdownX.value = e.clientX;\n    dropdownY.value = e.clientY;\n  };\n\n  // 处理艺术家点击\n  const handleArtistClick = (id: number) => {\n    navigateToArtist(id);\n  };\n\n  // 鼠标悬停处理\n  const handleMouseEnter = () => {\n    isHovering.value = true;\n  };\n\n  const handleMouseLeave = () => {\n    isHovering.value = false;\n  };\n\n  return {\n    t,\n    play,\n    playMusic,\n    playLoading,\n    isPlaying,\n    isFavorite,\n    isDislike,\n    artists,\n    showDropdown,\n    dropdownX,\n    dropdownY,\n    isHovering,\n    playerStore,\n    message,\n    getImgUrl,\n    handleImageLoad,\n    playMusicEvent,\n    toggleFavorite,\n    toggleDislike,\n    handlePlayNext,\n    getDuration,\n    formatDuration,\n    handleContextMenu,\n    handleMenuClick,\n    handleArtistClick,\n    handleMouseEnter,\n    handleMouseLeave,\n    downloadMusic\n  };\n}\n"
  },
  {
    "path": "src/renderer/hooks/useZoom.ts",
    "content": "import { ref } from 'vue';\n\n/**\n * 页面缩放功能的组合式API\n * 提供页面缩放相关的状态和方法\n */\nexport function useZoom() {\n  // 缩放相关常量\n  const MIN_ZOOM = 0.5;\n  const MAX_ZOOM = 1.5;\n  const ZOOM_STEP = 0.05; // 5%的步长\n\n  // 当前缩放因子\n  const zoomFactor = ref(1);\n\n  // 初始化获取当前缩放比例\n  const initZoomFactor = async () => {\n    try {\n      const currentZoom = await window.ipcRenderer.invoke('get-content-zoom');\n      zoomFactor.value = currentZoom;\n    } catch (error) {\n      console.error('获取缩放比例失败:', error);\n    }\n  };\n\n  // 增加缩放比例，保证100%为节点\n  const increaseZoom = () => {\n    let newZoom;\n\n    // 如果当前缩放低于100%并且增加后会超过100%，则直接设为100%\n    if (zoomFactor.value < 1.0 && zoomFactor.value + ZOOM_STEP > 1.0) {\n      newZoom = 1.0; // 精确设置为100%\n    } else {\n      newZoom = Math.min(MAX_ZOOM, Math.round((zoomFactor.value + ZOOM_STEP) * 20) / 20);\n    }\n\n    setZoomFactor(newZoom);\n  };\n\n  // 减少缩放比例，保证100%为节点\n  const decreaseZoom = () => {\n    let newZoom;\n\n    // 如果当前缩放大于100%并且减少后会低于100%，则直接设为100%\n    if (zoomFactor.value > 1.0 && zoomFactor.value - ZOOM_STEP < 1.0) {\n      newZoom = 1.0; // 精确设置为100%\n    } else {\n      newZoom = Math.max(MIN_ZOOM, Math.round((zoomFactor.value - ZOOM_STEP) * 20) / 20);\n    }\n\n    setZoomFactor(newZoom);\n  };\n\n  // 重置缩放比例到系统建议值\n  const resetZoom = async () => {\n    try {\n      setZoomFactor(1);\n    } catch (error) {\n      console.error('重置缩放比例失败:', error);\n    }\n  };\n\n  // 设置为100%标准缩放\n  const setZoom100 = () => {\n    setZoomFactor(1.0);\n  };\n\n  // 设置缩放比例\n  const setZoomFactor = (zoom: number) => {\n    window.ipcRenderer.send('set-content-zoom', zoom);\n    zoomFactor.value = zoom;\n  };\n\n  // 检查是否为100%缩放\n  const isZoom100 = () => {\n    return Math.abs(zoomFactor.value - 1.0) < 0.001;\n  };\n\n  return {\n    zoomFactor,\n    initZoomFactor,\n    increaseZoom,\n    decreaseZoom,\n    resetZoom,\n    setZoom100,\n    setZoomFactor,\n    isZoom100,\n    MIN_ZOOM,\n    MAX_ZOOM,\n    ZOOM_STEP\n  };\n}\n"
  },
  {
    "path": "src/renderer/index.css",
    "content": "/* ./src/index.css */\n\n/*! @import */\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n.n-image img {\n  background-color: #111111;\n  width: 100%;\n}\n\n.n-slider-handle-indicator--top {\n  @apply bg-transparent text-2xl px-2 py-1 shadow-none mb-0 text-white bg-dark-300 dark:bg-gray-800 bg-opacity-80 rounded-lg  !important;\n  mix-blend-mode: difference !important;\n}\n\n.v-binder-follower-container:has(.n-slider-handle-indicator--top) {\n  z-index: 999999999 !important;\n}\n\n.text-el {\n  @apply overflow-ellipsis overflow-hidden whitespace-nowrap;\n}\n\n.theme-dark {\n  --bg-color: #000;\n  --text-color: #fff;\n  --bg-color-100: #161616;\n  --bg-color-200: #2d2d2d;\n  --bg-color-300: #3d3d3d;\n  --text-color: #f8f9fa;\n  --text-color-100: #e9ecef;\n  --text-color-200: #dee2e6;\n  --text-color-300: #dde0e3;\n  --primary-color: #22c55e;\n}\n\n.theme-light {\n  --bg-color: #fff;\n  --text-color: #000;\n  --bg-color-100: #f8f9fa;\n  --bg-color-200: #e9ecef;\n  --bg-color-300: #dee2e6;\n  --text-color: #000;\n  --text-color-100: #161616;\n  --text-color-200: #2d2d2d;\n  --text-color-300: #3d3d3d;\n  --primary-color: #22c55e;\n}\n\n.theme-gray {\n  --bg-color: #f8f9fa;\n  --text-color: #000;\n  --bg-color-100: #e9ecef;\n  --bg-color-200: #dee2e6;\n  --bg-color-300: #dde0e3;\n  --text-color: #000;\n  --text-color-100: #161616;\n  --text-color-200: #2d2d2d;\n  --text-color-300: #3d3d3d;\n  --primary-color: #22c55e;\n}\n\n:root {\n  --text-color: #000000dd;\n  --safe-area-inset-top: 0px;\n  --safe-area-inset-right: 0px;\n  --safe-area-inset-bottom: 10px;\n  --safe-area-inset-left: 0px;\n}\n\n:root[class='dark'] {\n  --text-color: #ffffffdd;\n}\n"
  },
  {
    "path": "src/renderer/index.html",
    "content": "<!doctype html>\n<html lang=\"zh-CN\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" href=\"/favicon.ico\" />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"\n    />\n\n    <!-- SEO 元数据 -->\n    <title>AlgerMusicPlayer ｜ algerkong</title>\n    <meta\n      name=\"description\"\n      content=\"AlgerMusicPlayer 音乐播放器，支持在线播放、歌词显示、音乐下载等功能。提供海量音乐资源，让您随时随地享受音乐。\"\n    />\n    <meta\n      name=\"keywords\"\n      content=\"AlgerMusic, AlgerMusicPlayer, 音乐播放器, 在线音乐, 歌词显示, 音乐下载, AlgerKong, algerkong\"\n    />\n\n    <!-- 作者信息 -->\n    <meta name=\"author\" content=\"algerkong\" />\n    <meta name=\"author-url\" content=\"https://github.com/algerkong\" />\n\n    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\" />\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\" />\n\n    <!-- 资源预加载 -->\n    <link rel=\"preload\" href=\"./assets/icon/iconfont.css\" as=\"style\" />\n    <link rel=\"preload\" href=\"./assets/css/base.css\" as=\"style\" />\n\n    <!-- 样式表 -->\n    <link rel=\"stylesheet\" href=\"./assets/icon/iconfont.css\" />\n    <link rel=\"stylesheet\" href=\"./assets/css/base.css\" />\n\n    <!-- 百度统计 -->\n    <script>\n      var _hmt = _hmt || [];\n      (function () {\n        var hm = document.createElement('script');\n        hm.src = 'https://hm.baidu.com/hm.js?75a7ee3d3875dfdd2fe9d134883ddcbd';\n        var s = document.getElementsByTagName('script')[0];\n        s.parentNode.insertBefore(hm, s);\n      })();\n    </script>\n    <script>\n      var _hmt = _hmt || [];\n      (function () {\n        var hm = document.createElement('script');\n        hm.src = 'https://hm.baidu.com/hm.js?27b3850e627d266b20b38cce19af18f7';\n        var s = document.getElementsByTagName('script')[0];\n        s.parentNode.insertBefore(hm, s);\n      })();\n    </script>\n    <!-- 动画配置 -->\n    <style>\n      :root {\n        --animate-delay: 0.5s;\n      }\n    </style>\n  </head>\n\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"./main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "src/renderer/layout/AppLayout.vue",
    "content": "<template>\n  <!-- 移动端使用专用布局（平板模式下使用 PC 布局） -->\n  <mobile-layout v-if=\"isPhone && !settingsStore.setData?.tabletMode\" :is-phone=\"isPhone\" />\n\n  <!-- PC 端 / 浏览器移动端 / 平板模式 保持原有布局 -->\n  <div v-else class=\"layout-page\" :class=\"{ mobile: settingsStore.isMobile }\">\n    <div id=\"layout-main\" class=\"layout-main\">\n      <title-bar />\n      <div class=\"layout-main-page\">\n        <!-- 侧边菜单栏 -->\n        <app-menu v-if=\"!settingsStore.isMobile\" class=\"menu\" :menus=\"menuStore.menus\" />\n        <div class=\"main\">\n          <!-- 搜索栏 -->\n          <search-bar class=\"search-bar\" />\n          <!-- 主页面路由 -->\n          <div\n            class=\"main-content\"\n            :native-scrollbar=\"false\"\n            :class=\"{ 'mobile-content': !shouldShowMobileMenu }\"\n          >\n            <router-view\n              v-slot=\"{ Component }\"\n              class=\"main-page\"\n              :class=\"route.meta.noScroll && !settingsStore.isMobile ? 'pr-3' : ''\"\n            >\n              <keep-alive :include=\"keepAliveInclude\">\n                <component :is=\"Component\" />\n              </keep-alive>\n            </router-view>\n          </div>\n          <play-bottom />\n          <!-- 移动端底部菜单（浏览器模拟移动端时使用） -->\n          <app-menu v-if=\"shouldShowMobileMenu\" class=\"menu mobile-menu\" :menus=\"menuStore.menus\" />\n        </div>\n      </div>\n      <!-- 底部音乐播放 -->\n      <template v-if=\"!settingsStore.isMiniMode\">\n        <play-bar\n          v-if=\"!settingsStore.isMobile\"\n          v-show=\"isPlay\"\n          :style=\"playerStore.musicFull ? 'bottom: 0;' : ''\"\n        />\n        <mobile-play-bar\n          v-else\n          v-show=\"isPlay\"\n          :style=\"settingsStore.isMobile && playerStore.musicFull ? 'bottom: 0;' : ''\"\n        />\n      </template>\n    </div>\n    <update-modal v-if=\"isElectron\" />\n    <playlist-drawer v-model=\"showPlaylistDrawer\" :song-id=\"currentSongId\" />\n    <sleep-timer-top v-if=\"!settingsStore.isMobile\" />\n    <!-- 下载管理抽屉 -->\n    <download-drawer\n      v-if=\"\n        isElectron &&\n        (settingsStore.setData?.alwaysShowDownloadButton ||\n          settingsStore.showDownloadDrawer ||\n          settingsStore.setData?.hasDownloadingTasks)\n      \"\n    />\n    <!-- 播放列表抽屉 -->\n    <playing-list-drawer />\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { computed, defineAsyncComponent, onMounted, provide, ref } from 'vue';\nimport { useRoute } from 'vue-router';\n\nimport DownloadDrawer from '@/components/common/DownloadDrawer.vue';\nimport PlayBottom from '@/components/common/PlayBottom.vue';\nimport UpdateModal from '@/components/common/UpdateModal.vue';\nimport SleepTimerTop from '@/components/player/SleepTimerTop.vue';\nimport homeRouter from '@/router/home';\nimport otherRouter from '@/router/other';\nimport { useMenuStore } from '@/store/modules/menu';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { useSettingsStore } from '@/store/modules/settings';\nimport { isElectron } from '@/utils';\n\n// 移动端专用布局\nimport MobileLayout from './MobileLayout.vue';\n\nconst keepAliveInclude = computed(() => {\n  const allRoutes = [...homeRouter, ...otherRouter];\n\n  return allRoutes\n    .filter((item) => {\n      return item.meta?.keepAlive;\n    })\n    .map((item) => {\n      return typeof item.name === 'string'\n        ? item.name.charAt(0).toUpperCase() + item.name.slice(1)\n        : '';\n    })\n    .filter(Boolean);\n});\n\nconst AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));\nconst PlayBar = defineAsyncComponent(() => import('@/components/player/PlayBar.vue'));\nconst MobilePlayBar = defineAsyncComponent(() => import('@/components/player/MobilePlayBar.vue'));\nconst SearchBar = defineAsyncComponent(() => import('./components/SearchBar.vue'));\nconst TitleBar = defineAsyncComponent(() => import('./components/TitleBar.vue'));\nconst PlayingListDrawer = defineAsyncComponent(\n  () => import('@/components/player/PlayingListDrawer.vue')\n);\nconst PlaylistDrawer = defineAsyncComponent(() => import('@/components/common/PlaylistDrawer.vue'));\n\nconst playerStore = usePlayerStore();\nconst settingsStore = useSettingsStore();\nconst menuStore = useMenuStore();\n\nconst isPlay = computed(() => playerStore.playMusic && playerStore.playMusic.id);\nconst route = useRoute();\n\n// 判断当前路由是否应该在移动端显示AppMenu\nconst shouldShowMobileMenu = computed(() => {\n  // 过滤出在menus中定义的路径\n  const menuPaths = menuStore.menus.map((item: any) => item.path);\n  // 检查当前路由路径是否在menus中\n  return menuPaths.includes(route.path) && settingsStore.isMobile && !playerStore.musicFull;\n});\n\nprovide('shouldShowMobileMenu', shouldShowMobileMenu);\n\n// 使用 settingsStore.isMobile 进行移动端检测而不是 Capacitor 设备检测\nconst isPhone = computed(() => settingsStore.isMobile);\n\nonMounted(() => {\n  settingsStore.initializeSettings();\n  settingsStore.initializeTheme();\n});\n\nconst showPlaylistDrawer = ref(false);\nconst currentSongId = ref<number | undefined>();\n\n// 提供一个方法来打开歌单抽屉\nconst openPlaylistDrawer = (songId: number, isOpen: boolean = true) => {\n  currentSongId.value = songId;\n  showPlaylistDrawer.value = isOpen;\n  playerStore.setMusicFull(false);\n  playerStore.setPlayListDrawerVisible(!isOpen);\n};\n\n// 将方法提供给全局\nprovide('openPlaylistDrawer', openPlaylistDrawer);\n</script>\n\n<style lang=\"scss\" scoped>\n.layout-page {\n  @apply w-screen h-screen overflow-hidden bg-light dark:bg-black;\n}\n\n.layout-main {\n  @apply w-full h-full relative text-gray-900 dark:text-white;\n}\n\n.layout-main-page {\n  @apply flex h-full;\n}\n\n.menu {\n  @apply h-full;\n}\n\n.main {\n  @apply overflow-hidden flex-1 flex flex-col;\n}\n\n.main-content {\n  @apply flex-1 overflow-hidden;\n}\n\n.main-page {\n  @apply h-full;\n}\n\n.mobile {\n  .main-content {\n    height: calc(100vh - 130px);\n    overflow: auto;\n    display: block;\n    flex: none;\n    position: relative;\n  }\n\n  .mobile-content {\n    height: calc(100vh - 75px);\n    position: relative;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/layout/MiniLayout.vue",
    "content": "<!-- 迷你模式布局 -->\n<template>\n  <div class=\"mini-layout\">\n    <mini-play-bar />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport MiniPlayBar from '@/components/player/MiniPlayBar.vue';\n</script>\n\n<style lang=\"scss\" scoped>\n.mini-layout {\n  @apply w-full h-full bg-transparent;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/layout/MobileLayout.vue",
    "content": "<template>\n  <div id=\"layout-main\" class=\"mobile-layout mobile\" :class=\"{ 'has-safe-area': isPhone }\">\n    <!-- 顶部头部 -->\n    <mobile-header />\n\n    <!-- 主内容区域 -->\n    <div\n      class=\"mobile-content\"\n      :class=\"{ 'has-bottom-menu': shouldShowBottomMenu, 'has-player': isPlay }\"\n    >\n      <router-view v-slot=\"{ Component }\" class=\"mobile-page\">\n        <keep-alive :include=\"keepAliveInclude\">\n          <component :is=\"Component\" />\n        </keep-alive>\n      </router-view>\n    </div>\n\n    <!-- 底部播放条 -->\n    <mobile-play-bar v-if=\"isPlay\" />\n\n    <!-- 底部导航菜单 -->\n    <div v-if=\"shouldShowBottomMenu\" class=\"mobile-bottom-menu\">\n      <app-menu class=\"mobile-menu\" :menus=\"menuStore.menus\" />\n    </div>\n    <!-- 其他弹窗/抽屉 -->\n    <playlist-drawer v-model=\"showPlaylistDrawer\" :song-id=\"currentSongId\" />\n    <playing-list-drawer />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, defineAsyncComponent, provide, ref } from 'vue';\nimport { useRoute } from 'vue-router';\n\nimport homeRouter from '@/router/home';\nimport otherRouter from '@/router/other';\nimport { useMenuStore } from '@/store/modules/menu';\nimport { usePlayerStore } from '@/store/modules/player';\n\nimport MobileHeader from './components/MobileHeader.vue';\n\nconst AppMenu = defineAsyncComponent(() => import('./components/AppMenu.vue'));\nconst MobilePlayBar = defineAsyncComponent(() => import('@/components/player/MobilePlayBar.vue'));\nconst PlayingListDrawer = defineAsyncComponent(\n  () => import('@/components/player/PlayingListDrawer.vue')\n);\nconst PlaylistDrawer = defineAsyncComponent(() => import('@/components/common/PlaylistDrawer.vue'));\n\nconst props = defineProps<{\n  isPhone: boolean;\n}>();\n\nconst route = useRoute();\nconst playerStore = usePlayerStore();\nconst menuStore = useMenuStore();\n\n// 提供是否有安全区域\nprovide('hasSafeArea', props.isPhone);\n\n// 是否有播放的歌曲\nconst isPlay = computed(() => playerStore.playMusic && playerStore.playMusic.id);\n\n// 是否显示底部菜单\nconst shouldShowBottomMenu = computed(() => {\n  const menuPaths = menuStore.menus.map((item: any) => item.path);\n  return menuPaths.includes(route.path) && !playerStore.musicFull;\n});\n\n// 提供给 MobilePlayBar 使用，用于调整播放栏位置\nprovide('shouldShowMobileMenu', shouldShowBottomMenu);\n\n// Keep-alive 配置\nconst keepAliveInclude = computed(() => {\n  const allRoutes = [...homeRouter, ...otherRouter];\n  return allRoutes\n    .filter((item) => item.meta?.keepAlive)\n    .map((item) =>\n      typeof item.name === 'string' ? item.name.charAt(0).toUpperCase() + item.name.slice(1) : ''\n    )\n    .filter(Boolean);\n});\n\n// 歌单抽屉\nconst showPlaylistDrawer = ref(false);\nconst currentSongId = ref<number | undefined>();\n\n// 提供打开歌单抽屉的方法\nconst openPlaylistDrawer = (songId: number, isOpen: boolean = true) => {\n  currentSongId.value = songId;\n  showPlaylistDrawer.value = isOpen;\n  playerStore.setMusicFull(false);\n  playerStore.setPlayListDrawerVisible(!isOpen);\n};\n\nprovide('openPlaylistDrawer', openPlaylistDrawer);\n</script>\n\n<style lang=\"scss\" scoped>\n.mobile-layout {\n  @apply w-screen h-screen flex flex-col;\n  @apply bg-light dark:bg-black;\n  @apply overflow-hidden;\n  position: relative;\n}\n\n.mobile-content {\n  @apply flex-1 overflow-auto;\n\n  // // 只有底部菜单\n  // &.has-bottom-menu:not(.has-player) {\n  //   padding-bottom: calc(60px + var(--safe-area-inset-bottom, 0px));\n  // }\n\n  // // 只有播放栏\n  // &.has-player:not(.has-bottom-menu) {\n  //   padding-bottom: calc(70px + var(--safe-area-inset-bottom, 0px));\n  // }\n}\n\n.mobile-page {\n  @apply h-full;\n}\n\n// 底部菜单固定在底部\n.mobile-bottom-menu {\n  @apply bg-light dark:bg-black;\n  @apply border-t border-gray-200 dark:border-gray-800;\n}\n\n.mobile-menu {\n  @apply w-full;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/layout/components/AppMenu.vue",
    "content": "<template>\n  <div>\n    <!-- menu -->\n    <div class=\"app-menu\" :class=\"{ 'app-menu-expanded': settingsStore.setData.isMenuExpanded }\">\n      <div class=\"app-menu-header\">\n        <div class=\"app-menu-logo\" @click=\"toggleMenu\">\n          <img :src=\"icon\" class=\"w-9 h-9\" alt=\"logo\" />\n        </div>\n      </div>\n      <div class=\"app-menu-list\">\n        <div v-for=\"(item, index) in menus\" :key=\"item.path\" class=\"app-menu-item\">\n          <n-tooltip\n            :delay=\"200\"\n            :disabled=\"settingsStore.setData.isMenuExpanded || isMobile\"\n            placement=\"bottom\"\n          >\n            <template #trigger>\n              <router-link class=\"app-menu-item-link\" :to=\"item.path\">\n                <i\n                  class=\"iconfont app-menu-item-icon\"\n                  :style=\"iconStyle(index)\"\n                  :class=\"item.meta.icon\"\n                ></i>\n                <span\n                  v-if=\"settingsStore.setData.isMenuExpanded\"\n                  class=\"app-menu-item-text ml-3\"\n                  :class=\"isChecked(index) ? 'text-green-500' : ''\"\n                  >{{ t(item.meta.title) }}</span\n                >\n              </router-link>\n            </template>\n            <div v-if=\"!settingsStore.setData.isMenuExpanded\">{{ t(item.meta.title) }}</div>\n          </n-tooltip>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute } from 'vue-router';\n\nimport icon from '@/assets/icon.png';\nimport { useSettingsStore } from '@/store';\nimport { isMobile } from '@/utils';\n\nconst props = defineProps({\n  size: {\n    type: String,\n    default: '26px'\n  },\n  color: {\n    type: String,\n    default: '#aaa'\n  },\n  selectColor: {\n    type: String,\n    default: '#10B981'\n  },\n  menus: {\n    type: Array as any,\n    default: () => []\n  }\n});\n\nconst route = useRoute();\nconst path = ref(route.path);\nconst settingsStore = useSettingsStore();\nwatch(\n  () => route.path,\n  async (newParams) => {\n    path.value = newParams;\n  }\n);\n\nconst { t } = useI18n();\n\nconst isChecked = (index: number) => {\n  return path.value === props.menus[index].path;\n};\n\nconst iconStyle = (index: number) => {\n  const style = {\n    fontSize: props.size,\n    color: isChecked(index) ? props.selectColor : props.color\n  };\n  return style;\n};\n\nconst toggleMenu = () => {\n  settingsStore.setSetData({\n    isMenuExpanded: !settingsStore.setData.isMenuExpanded\n  });\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.app-menu {\n  @apply flex-col items-center justify-center transition-all duration-300 w-[100px] px-1;\n}\n\n.app-menu-list {\n  max-height: calc(100vh - 120px); /* 为header预留空间，防止菜单项被遮挡 */\n  overflow-y: auto;\n  overflow-x: hidden;\n  /* 自定义滚动条样式 - 默认隐藏，悬停时显示 */\n  scrollbar-width: thin;\n  scrollbar-color: transparent transparent;\n  padding-bottom: 20px;\n  transition: scrollbar-color 0.3s ease;\n\n  &::-webkit-scrollbar {\n    width: 4px;\n  }\n\n  &::-webkit-scrollbar-track {\n    background: transparent;\n  }\n\n  &::-webkit-scrollbar-thumb {\n    background-color: transparent;\n    border-radius: 2px;\n    transition: background-color 0.3s ease;\n  }\n\n  /* 悬停时显示滚动条 */\n  &:hover {\n    scrollbar-color: rgba(156, 163, 175, 0.5) transparent;\n\n    &::-webkit-scrollbar-thumb {\n      background-color: rgba(156, 163, 175, 0.5);\n\n      &:hover {\n        background-color: rgba(156, 163, 175, 0.7);\n      }\n    }\n  }\n}\n\n.app-menu-expanded {\n  @apply w-[160px];\n\n  .app-menu-item {\n    @apply hover:bg-gray-100 dark:hover:bg-gray-800 rounded mr-4;\n  }\n}\n\n.app-menu-item-link,\n.app-menu-header {\n  @apply flex items-center w-[200px] overflow-hidden ml-2 px-5;\n}\n\n.app-menu-header {\n  @apply ml-1;\n}\n\n.app-menu-item-link {\n  @apply mb-6 mt-6;\n}\n\n.app-menu-item-icon {\n  @apply transition-all duration-200 text-gray-500 dark:text-gray-400;\n\n  &:hover {\n    @apply text-green-500 scale-105 !important;\n  }\n}\n\n.mobile {\n  .app-menu {\n    max-width: 100%;\n    width: 100vw;\n    position: relative;\n    bottom: 0;\n    left: 0;\n    z-index: 99;\n    @apply bg-light dark:bg-black border-t border-gray-200 dark:border-gray-700;\n    z-index: 99999;\n    @apply bg-light dark:bg-black border-none border-gray-200 dark:border-gray-700;\n\n    &-header {\n      display: none;\n    }\n\n    &-list {\n      @apply flex justify-between px-4;\n      max-height: none !important; /* 移动端不限制高度 */\n      overflow: visible !important; /* 移动端不需要滚动 */\n    }\n\n    &-item {\n      &-link {\n        @apply my-2 w-auto px-2;\n        width: auto !important;\n        margin-top: 8px;\n        margin-bottom: 8px;\n      }\n    }\n\n    &-expanded {\n      @apply w-full;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/layout/components/MobileHeader.vue",
    "content": "<template>\n  <div class=\"mobile-header\" :class=\"{ 'safe-area-top': hasSafeArea }\">\n    <!-- 左侧区域 -->\n    <div class=\"header-left\">\n      <div v-if=\"showBack\" class=\"header-btn\" @click=\"goBack\">\n        <i class=\"ri-arrow-left-s-line\"></i>\n      </div>\n      <div v-else class=\"header-logo\">\n        <span class=\"logo-text\">Alger</span>\n      </div>\n    </div>\n\n    <!-- 中间标题 -->\n    <div class=\"header-title\">\n      <span v-if=\"title\">{{ t(title) }}</span>\n    </div>\n\n    <!-- 右侧区域 -->\n    <div class=\"header-right\">\n      <div class=\"header-btn\" @click=\"openSearch\">\n        <i class=\"ri-search-line\"></i>\n      </div>\n      <div class=\"header-btn\" @click=\"openSettings\">\n        <i class=\"ri-settings-3-line\"></i>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, inject } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\n\nconst route = useRoute();\nconst router = useRouter();\nconst { t } = useI18n();\n\n// 注入是否有安全区域\nconst hasSafeArea = inject('hasSafeArea', false);\n\n// 是否显示返回按钮\nconst showBack = computed(() => {\n  return route.meta.back === true;\n});\n\n// 页面标题\nconst title = computed(() => {\n  return (route.meta.title as string) || '';\n});\n\n// 返回上一页\nconst goBack = () => {\n  router.back();\n};\n\n// 打开搜索\nconst openSearch = () => {\n  router.push('/mobile-search');\n};\n\nconst openSettings = () => {\n  router.push('/set');\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.mobile-header {\n  @apply flex items-center justify-between px-4 py-3;\n  @apply bg-light dark:bg-black;\n  @apply border-b border-gray-100 dark:border-gray-800;\n  min-height: 56px;\n\n  &.safe-area-top {\n    padding-top: calc(var(--safe-area-inset-top, 0px) + 16px);\n  }\n}\n\n.header-left {\n  @apply flex items-center;\n  min-width: 80px;\n}\n\n.header-logo {\n  @apply flex items-center;\n\n  .logo-text {\n    @apply text-lg font-bold text-green-500;\n  }\n}\n\n.header-title {\n  @apply flex-1 text-center;\n\n  span {\n    @apply text-base font-medium text-gray-900 dark:text-white;\n  }\n}\n\n.header-right {\n  @apply flex items-center gap-2;\n  min-width: 80px;\n  justify-content: flex-end;\n}\n\n.header-btn {\n  @apply flex items-center justify-center;\n  @apply w-10 h-10 rounded-full;\n  @apply text-xl text-gray-600 dark:text-gray-300;\n  @apply active:bg-gray-100 dark:active:bg-gray-800;\n  @apply transition-colors duration-150;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/layout/components/SearchBar.vue",
    "content": "<template>\n  <div class=\"search-box flex search-bar\">\n    <div v-if=\"showBackButton\" class=\"back-button\" @click=\"goBack\">\n      <i class=\"ri-arrow-left-line\"></i>\n    </div>\n    <div class=\"search-box-input flex-1 relative\">\n      <n-popover\n        trigger=\"manual\"\n        placement=\"bottom-start\"\n        :show=\"showSuggestions\"\n        :show-arrow=\"false\"\n        style=\"width: 100%; margin-top: 4px\"\n        content-style=\"padding: 0; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);\"\n        raw\n      >\n        <template #trigger>\n          <n-input\n            v-model:value=\"searchValue\"\n            size=\"medium\"\n            round\n            :placeholder=\"hotSearchKeyword\"\n            class=\"border dark:border-gray-600 border-gray-200\"\n            @input=\"handleInput\"\n            @keydown=\"handleKeydown\"\n            @focus=\"handleFocus\"\n            @blur=\"handleBlur\"\n          >\n            <template #prefix>\n              <i class=\"iconfont icon-search\"></i>\n            </template>\n            <template #suffix>\n              <n-dropdown trigger=\"hover\" :options=\"searchTypeOptions\" @select=\"selectSearchType\">\n                <div class=\"w-20 px-3 flex justify-between items-center\">\n                  <div>\n                    {{\n                      searchTypeOptions.find((item) => item.key === searchStore.searchType)?.label\n                    }}\n                  </div>\n                  <i class=\"iconfont icon-xiasanjiaoxing\"></i>\n                </div>\n              </n-dropdown>\n            </template>\n          </n-input>\n        </template>\n        <div class=\"search-suggestions-panel\">\n          <n-scrollbar style=\"max-height: 300px\">\n            <div v-if=\"suggestionsLoading\" class=\"suggestion-item loading\">\n              <n-spin size=\"small\" />\n            </div>\n            <div\n              v-for=\"(suggestion, index) in suggestions\"\n              :key=\"index\"\n              class=\"suggestion-item\"\n              :class=\"{ highlighted: index === highlightedIndex }\"\n              @mousedown.prevent=\"selectSuggestion(suggestion)\"\n              @mouseenter=\"highlightedIndex = index\"\n            >\n              <i class=\"ri-search-line suggestion-icon\"></i>\n              <span>{{ suggestion }}</span>\n            </div>\n          </n-scrollbar>\n        </div>\n      </n-popover>\n    </div>\n    <n-popover trigger=\"hover\" placement=\"bottom\" :show-arrow=\"false\" raw>\n      <template #trigger>\n        <div class=\"user-box\">\n          <n-avatar\n            v-if=\"userStore.user\"\n            class=\"cursor-pointer\"\n            circle\n            size=\"medium\"\n            :src=\"getImgUrl(userStore.user.avatarUrl)\"\n            @click=\"selectItem('user')\"\n          />\n          <div v-else class=\"mx-2 rounded-full cursor-pointer text-sm\" @click=\"toLogin\">\n            {{ t('comp.searchBar.login') }}\n          </div>\n        </div>\n      </template>\n      <div class=\"user-popover\">\n        <div v-if=\"userStore.user\" class=\"user-header\" @click=\"selectItem('user')\">\n          <n-avatar circle size=\"small\" :src=\"getImgUrl(userStore.user?.avatarUrl)\" />\n          <div>\n            <p class=\"username\">{{ userStore.user?.nickname || 'Theodore' }}</p>\n            <p></p>\n          </div>\n        </div>\n        <div class=\"menu-items\">\n          <div v-if=\"!userStore.user\" class=\"menu-item\" @click=\"toLogin\">\n            <i class=\"iconfont ri-login-box-line\"></i>\n            <span>{{ t('comp.searchBar.toLogin') }}</span>\n          </div>\n          <div v-if=\"userStore.user\" class=\"menu-item\" @click=\"selectItem('logout')\">\n            <i class=\"iconfont ri-logout-box-r-line\"></i>\n            <span>{{ t('comp.searchBar.logout') }}</span>\n          </div>\n          <!-- 切换主题 -->\n          <div class=\"menu-item\" @click=\"selectItem('set')\">\n            <i class=\"iconfont ri-settings-3-line\"></i>\n            <span>{{ t('comp.searchBar.set') }}</span>\n          </div>\n          <div class=\"menu-item\" v-if=\"isElectron\">\n            <i class=\"iconfont ri-zoom-in-line\"></i>\n            <span>{{ t('comp.searchBar.zoom') }}</span>\n            <div class=\"zoom-controls ml-auto\">\n              <n-button quaternary circle size=\"tiny\" @click=\"decreaseZoom\">\n                <i class=\"ri-subtract-line\"></i>\n              </n-button>\n              <n-tooltip trigger=\"hover\">\n                <template #trigger>\n                  <span class=\"zoom-value\" :class=\"{ 'zoom-100': isZoom100() }\" @click=\"resetZoom\"\n                    >{{ Math.round(zoomFactor * 100) }}%</span\n                  >\n                </template>\n                {{ isZoom100() ? t('comp.searchBar.zoom100') : t('comp.searchBar.resetZoom') }}\n              </n-tooltip>\n              <n-button quaternary circle size=\"tiny\" @click=\"increaseZoom\">\n                <i class=\"ri-add-line\"></i>\n              </n-button>\n            </div>\n          </div>\n          <div class=\"menu-item\">\n            <i class=\"iconfont\" :class=\"isDark ? 'ri-moon-line' : 'ri-sun-line'\"></i>\n            <span>{{ t('comp.searchBar.theme') }}</span>\n            <n-switch v-model:value=\"isDark\" class=\"ml-auto\">\n              <template #checked>\n                <i class=\"ri-moon-line\"></i>\n              </template>\n              <template #unchecked>\n                <i class=\"ri-sun-line\"></i>\n              </template>\n            </n-switch>\n          </div>\n          <div class=\"menu-item\" @click=\"restartApp\">\n            <i class=\"iconfont ri-restart-line\"></i>\n            <span>{{ t('comp.searchBar.restart') }}</span>\n          </div>\n          <div class=\"menu-item\" @click=\"selectItem('refresh')\">\n            <i class=\"iconfont ri-refresh-line\"></i>\n            <span>{{ t('comp.searchBar.refresh') }}</span>\n          </div>\n          <div class=\"menu-item\" @click=\"toGithubRelease\">\n            <i class=\"iconfont ri-github-fill\"></i>\n            <span>{{ t('comp.searchBar.currentVersion') }}</span>\n            <div class=\"version-info\">\n              <span class=\"version-number\">{{ updateInfo.currentVersion }}</span>\n              <n-tag v-if=\"updateInfo.hasUpdate\" type=\"success\" size=\"small\" class=\"ml-1\">\n                New {{ updateInfo.latestVersion }}\n              </n-tag>\n            </div>\n          </div>\n        </div>\n      </div>\n    </n-popover>\n\n    <coffee :alipay-q-r=\"alipay\" :wechat-q-r=\"wechat\">\n      <div class=\"github\" @click=\"toGithub\">\n        <i class=\"ri-github-fill\"></i>\n      </div>\n    </coffee>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useDebounceFn } from '@vueuse/core';\nimport { computed, onMounted, ref, watch, watchEffect } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\n\nimport { getSearchKeyword } from '@/api/home';\nimport { getUserDetail } from '@/api/login';\nimport { getSearchSuggestions } from '@/api/search';\nimport alipay from '@/assets/alipay.png';\nimport wechat from '@/assets/wechat.png';\nimport Coffee from '@/components/Coffee.vue';\nimport { SEARCH_TYPE, SEARCH_TYPES, USER_SET_OPTIONS } from '@/const/bar-const';\nimport { useZoom } from '@/hooks/useZoom';\nimport { useSearchStore } from '@/store/modules/search';\nimport { useSettingsStore } from '@/store/modules/settings';\nimport { useUserStore } from '@/store/modules/user';\nimport { getImgUrl, isElectron } from '@/utils';\nimport { checkUpdate, UpdateResult } from '@/utils/update';\n\nimport config from '../../../../package.json';\n\nconst router = useRouter();\nconst searchStore = useSearchStore();\nconst settingsStore = useSettingsStore();\nconst userStore = useUserStore();\nconst userSetOptions = ref(USER_SET_OPTIONS);\nconst { t, locale } = useI18n();\n\n// 使用缩放hook\nconst { zoomFactor, initZoomFactor, increaseZoom, decreaseZoom, resetZoom, isZoom100 } = useZoom();\n\n// 显示返回按钮\nconst showBackButton = computed(() => {\n  return router.currentRoute.value.meta.back === true;\n});\n\n// 返回上一页\nconst goBack = () => {\n  router.back();\n};\n\n// 推荐热搜词\nconst hotSearchKeyword = ref(t('comp.searchBar.searchPlaceholder'));\nconst hotSearchValue = ref('');\nconst loadHotSearchKeyword = async () => {\n  const { data } = await getSearchKeyword();\n  hotSearchKeyword.value = data.data.showKeyword;\n  hotSearchValue.value = data.data.realkeyword;\n};\n\nconst loadPage = async () => {\n  const token = localStorage.getItem('token');\n  if (!token) return;\n  const { data } = await getUserDetail();\n  userStore.user =\n    data.profile || userStore.user || JSON.parse(localStorage.getItem('user') || '{}');\n  localStorage.setItem('user', JSON.stringify(userStore.user));\n};\n\nloadPage();\n\nwatchEffect(() => {\n  if (userStore.user) {\n    userSetOptions.value = USER_SET_OPTIONS;\n  } else {\n    userSetOptions.value = USER_SET_OPTIONS.filter((item) => item.key !== 'logout');\n  }\n});\n\nconst restartApp = () => {\n  window.electron.ipcRenderer.send('restart');\n};\n\nconst toLogin = () => {\n  router.push('/user');\n};\n\n// 页面初始化\nonMounted(() => {\n  loadHotSearchKeyword();\n  loadPage();\n  checkForUpdates();\n  isElectron && initZoomFactor();\n});\n\nconst isDark = computed({\n  get: () => settingsStore.theme === 'dark',\n  set: () => settingsStore.toggleTheme()\n});\n\n// 搜索词\nconst searchValue = ref('');\n\n// 使用 watch 代替 watchEffect 监听搜索值变化，确保深度监听\nwatch(\n  () => searchStore.searchValue,\n  (newValue) => {\n    if (newValue) {\n      searchValue.value = newValue;\n    }\n  },\n  { immediate: true }\n);\n\nconst search = () => {\n  const { value } = searchValue;\n  if (value === '') {\n    searchValue.value = hotSearchValue.value;\n    return;\n  }\n\n  if (router.currentRoute.value.path === '/search') {\n    searchStore.searchValue = value;\n    return;\n  }\n\n  router.push({\n    path: '/search',\n    query: {\n      keyword: value,\n      type: searchStore.searchType\n    }\n  });\n\n  console.log(`[UI] 执行搜索，关键词: \"${searchValue.value}\"`); // <--- 日志 K\n  showSuggestions.value = false; // 搜索后强制隐藏\n};\n\nconst selectSearchType = (key: number) => {\n  searchStore.searchType = key;\n  if (searchValue.value) {\n    if (router.currentRoute.value.path === '/search') {\n      search();\n    } else {\n      router.push({\n        path: '/search',\n        query: {\n          keyword: searchValue.value,\n          type: key\n        }\n      });\n    }\n  }\n};\n\nconst rawSearchTypes = ref(SEARCH_TYPES);\nconst searchTypeOptions = computed(() => {\n  locale.value;\n  return rawSearchTypes.value\n    .filter((type) => isElectron || type.key !== SEARCH_TYPE.BILIBILI)\n    .map((type) => ({\n      label: t(type.label),\n      key: type.key\n    }));\n});\n\nconst selectItem = async (key: string) => {\n  // switch 判断\n  switch (key) {\n    case 'logout':\n      userStore.handleLogout();\n      break;\n    case 'login':\n      router.push('/login');\n      break;\n    case 'set':\n      router.push('/set');\n      break;\n    case 'user':\n      router.push('/user');\n      break;\n    case 'refresh':\n      window.location.reload();\n      break;\n    default:\n  }\n};\n\nconst toGithub = () => {\n  window.open('http://donate.alger.fun/download', '_blank');\n};\n\nconst updateInfo = ref<UpdateResult>({\n  hasUpdate: false,\n  latestVersion: '',\n  currentVersion: config.version,\n  releaseInfo: null\n});\n\nconst checkForUpdates = async () => {\n  try {\n    const result = await checkUpdate(config.version);\n    if (result) {\n      updateInfo.value = result;\n    }\n  } catch (error) {\n    console.error('检查更新失败:', error);\n  }\n};\n\nconst toGithubRelease = () => {\n  window.location.href = 'https://donate.alger.fun/download';\n};\n\nconst suggestions = ref<string[]>([]);\nconst showSuggestions = ref(false);\nconst suggestionsLoading = ref(false);\nconst highlightedIndex = ref(-1); // -1 表示没有高亮项\n// 使用防抖函数来避免频繁请求API\nconst debouncedGetSuggestions = useDebounceFn(async (keyword: string) => {\n  if (!keyword.trim()) {\n    suggestions.value = [];\n    showSuggestions.value = false;\n    return;\n  }\n  suggestionsLoading.value = true;\n  suggestions.value = await getSearchSuggestions(keyword);\n  suggestionsLoading.value = false;\n  // 只有当有建议时才显示面板\n  showSuggestions.value = suggestions.value.length > 0;\n  highlightedIndex.value = -1;\n}, 300); // 300ms延迟\n\nconst handleInput = (value: string) => {\n  debouncedGetSuggestions(value);\n};\nconst handleFocus = () => {\n  if (searchValue.value && suggestions.value.length > 0) {\n    showSuggestions.value = true;\n  }\n};\n\nconst handleBlur = () => {\n  setTimeout(() => {\n    showSuggestions.value = false;\n  }, 150);\n};\n\nconst selectSuggestion = (suggestion: string) => {\n  searchValue.value = suggestion;\n  showSuggestions.value = false;\n  search();\n};\nconst handleKeydown = (event: KeyboardEvent) => {\n  // 如果建议列表不显示，则不处理上下键\n  if (!showSuggestions.value || suggestions.value.length === 0) {\n    // 如果是回车键，则正常执行搜索\n    if (event.key === 'Enter') {\n      search();\n    }\n    return;\n  }\n\n  switch (event.key) {\n    case 'ArrowDown':\n      event.preventDefault(); // 阻止光标移动到末尾\n      highlightedIndex.value = (highlightedIndex.value + 1) % suggestions.value.length;\n      break;\n    case 'ArrowUp':\n      event.preventDefault(); // 阻止光标移动到开头\n      highlightedIndex.value =\n        (highlightedIndex.value - 1 + suggestions.value.length) % suggestions.value.length;\n      break;\n    case 'Enter':\n      event.preventDefault(); // 阻止表单默认提交行为\n      if (highlightedIndex.value !== -1) {\n        // 如果有高亮项，就选择它\n        selectSuggestion(suggestions.value[highlightedIndex.value]);\n      } else {\n        // 否则，执行默认搜索\n        search();\n      }\n      break;\n    case 'Escape':\n      showSuggestions.value = false; // 按 Esc 隐藏建议\n      break;\n  }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.back-button {\n  @apply mr-2 flex items-center justify-center text-xl cursor-pointer;\n  @apply w-9 h-9 rounded-full;\n  @apply bg-light-100 dark:bg-dark-100 text-gray-900 dark:text-white;\n  @apply border dark:border-gray-600 border-gray-200;\n  @apply hover:bg-light-200 dark:hover:bg-dark-200;\n  @apply transition-all duration-200;\n}\n\n.user-box {\n  @apply ml-4 flex text-lg justify-center items-center rounded-full transition-colors duration-200;\n  @apply border dark:border-gray-600 border-gray-200 hover:border-gray-400 dark:hover:border-gray-400;\n  @apply bg-light dark:bg-gray-800;\n}\n\n.search-box {\n  @apply pb-4 pr-4;\n}\n\n.search-box-input {\n  @apply relative;\n\n  :deep(.n-input) {\n    @apply bg-gray-50 dark:bg-black;\n\n    .n-input__input-el {\n      @apply text-gray-900 dark:text-white;\n    }\n\n    .n-input__prefix {\n      @apply text-gray-500 dark:text-gray-400;\n    }\n  }\n}\n\n.mobile {\n  .search-box {\n    @apply pl-4;\n  }\n}\n\n.github {\n  @apply cursor-pointer text-gray-900 dark:text-gray-100 hover:text-gray-600 dark:hover:text-gray-400 text-xl ml-4 rounded-full flex justify-center items-center px-2 h-full;\n  @apply border dark:border-gray-600 border-gray-200 bg-light dark:bg-black;\n}\n\n.user-popover {\n  @apply min-w-[220px] p-0 rounded-xl overflow-hidden;\n  @apply bg-light dark:bg-black;\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n\n  .user-header {\n    @apply flex items-center gap-2 p-3 cursor-pointer;\n    @apply border-b dark:border-gray-700 border-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700;\n\n    .username {\n      @apply text-sm font-medium text-gray-900 dark:text-gray-200;\n    }\n  }\n\n  .menu-items {\n    @apply py-1;\n\n    .menu-item {\n      @apply flex items-center px-3 py-1 text-sm cursor-pointer;\n      @apply text-gray-700 dark:text-gray-300;\n      transition: background-color 0.2s;\n\n      &:hover {\n        @apply bg-gray-100 dark:bg-gray-700;\n      }\n\n      i {\n        @apply mr-1 text-lg text-gray-500 dark:text-gray-400;\n      }\n\n      .version-info {\n        @apply ml-auto flex items-center;\n\n        .version-number {\n          @apply text-xs px-2 py-0.5 rounded;\n          @apply bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300;\n        }\n      }\n\n      // 缩放控制样式\n      .zoom-controls {\n        @apply flex items-center gap-1;\n\n        .zoom-value {\n          @apply text-xs px-2 py-0.5 rounded cursor-pointer;\n          @apply bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300;\n          @apply hover:bg-gray-200 dark:hover:bg-gray-600;\n          transition: all 0.2s ease;\n\n          &.zoom-100 {\n            @apply bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 font-bold;\n            @apply hover:bg-green-200 dark:hover:bg-green-800;\n          }\n        }\n      }\n    }\n  }\n}\n\n.search-suggestions-panel {\n  @apply bg-light dark:bg-dark-100 rounded-lg overflow-hidden;\n  .suggestion-item {\n    @apply flex items-center px-4 py-2 cursor-pointer;\n    @apply text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800;\n    &.highlighted {\n      @apply bg-gray-100 dark:bg-gray-800;\n    }\n    &.loading {\n      @apply justify-center;\n    }\n\n    .suggestion-icon {\n      @apply mr-2 text-gray-400;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/layout/components/TitleBar.vue",
    "content": "<template>\n  <div id=\"title-bar\" @mousedown=\"drag\">\n    <div id=\"title\">Alger Music</div>\n    <div id=\"buttons\">\n      <n-button\n        v-if=\"!isElectron\"\n        type=\"primary\"\n        size=\"small\"\n        text\n        title=\"下载应用\"\n        @click=\"openDownloadPage\"\n      >\n        <i class=\"ri-download-line\"></i>\n        下载桌面版\n      </n-button>\n      <template v-if=\"isElectron\">\n        <div class=\"button\" @click=\"miniWindow\">\n          <i class=\"iconfont ri-picture-in-picture-line\"></i>\n        </div>\n        <div class=\"button\" @click=\"minimize\">\n          <i class=\"iconfont icon-minisize\"></i>\n        </div>\n        <div class=\"button\" @click=\"handleClose\">\n          <i class=\"iconfont icon-close\"></i>\n        </div>\n      </template>\n    </div>\n  </div>\n\n  <n-modal\n    v-model:show=\"showCloseModal\"\n    preset=\"dialog\"\n    :title=\"t('comp.titleBar.closeApp')\"\n    :style=\"{ width: '400px' }\"\n    :mask-closable=\"true\"\n  >\n    <div class=\"close-dialog-content\">\n      <p>{{ t('comp.titleBar.closeTitle') }}</p>\n      <div class=\"remember-choice\">\n        <n-checkbox v-model:checked=\"rememberChoice\">\n          {{ t('comp.titleBar.rememberChoice') }}\n        </n-checkbox>\n      </div>\n    </div>\n    <template #action>\n      <div class=\"dialog-footer\">\n        <n-button type=\"primary\" @click=\"handleAction('minimize')\">\n          {{ t('comp.titleBar.minimizeToTray') }}\n        </n-button>\n        <n-button @click=\"handleAction('close')\">\n          {{ t('comp.titleBar.exitApp') }}\n        </n-button>\n      </div>\n    </template>\n  </n-modal>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { useSettingsStore } from '@/store/modules/settings';\nimport { isElectron } from '@/utils';\n\nconst { t } = useI18n();\n\nconst settingsStore = useSettingsStore();\nconst showCloseModal = ref(false);\nconst rememberChoice = ref(false);\n\nconst openDownloadPage = () => {\n  if (!isElectron) {\n    window.open('http://donate.alger.fun/download', '_blank');\n  }\n};\n\nconst minimize = () => {\n  if (!isElectron) {\n    return;\n  }\n  window.api.minimize();\n};\n\nconst miniWindow = () => {\n  if (!isElectron) return;\n  window.api.miniWindow();\n};\n\nconst handleAction = (action: 'minimize' | 'close') => {\n  if (rememberChoice.value) {\n    settingsStore.setSetData({\n      ...settingsStore.setData,\n      closeAction: action\n    });\n  }\n\n  if (action === 'minimize') {\n    window.api.miniTray();\n  } else {\n    window.api.close();\n  }\n  showCloseModal.value = false;\n};\n\nconst handleClose = () => {\n  const { closeAction } = settingsStore.setData;\n\n  if (closeAction === 'minimize') {\n    window.api.miniTray();\n  } else if (closeAction === 'close') {\n    window.api.close();\n  } else {\n    showCloseModal.value = true;\n  }\n};\n\nconst drag = (event: MouseEvent) => {\n  if (!isElectron) {\n    return;\n  }\n  window.api.dragStart(event as unknown as string);\n};\n</script>\n\n<style scoped lang=\"scss\">\n#title-bar {\n  -webkit-app-region: drag;\n  @apply flex justify-between px-6 py-2 select-none relative;\n  @apply text-dark dark:text-white;\n  z-index: 3000;\n}\n\n#buttons {\n  @apply flex gap-4;\n  -webkit-app-region: no-drag;\n}\n\n.button {\n  @apply text-gray-600 dark:text-gray-400 hover:text-green-500;\n}\n\n.close-dialog-content {\n  @apply flex flex-col gap-4;\n}\n\n.dialog-footer {\n  @apply flex gap-4 justify-end;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/layout/components/index.ts",
    "content": "import AppMenu from './AppMenu.vue';\nimport PlayBar from './PlayBar.vue';\nimport SearchBar from './SearchBar.vue';\n\nexport { AppMenu, PlayBar, SearchBar };\n"
  },
  {
    "path": "src/renderer/main.ts",
    "content": "import './index.css';\nimport 'animate.css';\nimport 'remixicon/fonts/remixicon.css';\n\nimport { createApp } from 'vue';\n\nimport i18n from '@/../i18n/renderer';\nimport router from '@/router';\nimport pinia from '@/store';\n\nimport App from './App.vue';\nimport directives from './directive';\nimport { initAppShortcuts } from './utils/appShortcuts';\n\nconst app = createApp(App);\n\nObject.keys(directives).forEach((key: string) => {\n  app.directive(key, directives[key as keyof typeof directives]);\n});\n\napp.use(pinia);\napp.use(router);\napp.use(i18n as any);\napp.mount('#app');\n\n// 初始化应用内快捷键\ninitAppShortcuts();\n"
  },
  {
    "path": "src/renderer/router/home.ts",
    "content": "const layoutRouter = [\n  {\n    path: '/',\n    name: 'home',\n    meta: {\n      title: 'comp.home',\n      icon: 'icon-Home',\n      keepAlive: true,\n      isMobile: true\n    },\n    component: () => import('@/views/home/index.vue')\n  },\n  {\n    path: '/search',\n    name: 'search',\n    meta: {\n      title: 'comp.search',\n      noScroll: true,\n      icon: 'icon-Search',\n      keepAlive: true\n    },\n    component: () => import('@/views/search/index.vue')\n  },\n  {\n    path: '/list',\n    name: 'list',\n    meta: {\n      title: 'comp.list',\n      icon: 'icon-Paper',\n      keepAlive: true,\n      isMobile: true\n    },\n    component: () => import('@/views/list/index.vue')\n  },\n  {\n    path: '/toplist',\n    name: 'toplist',\n    meta: {\n      title: 'comp.toplist',\n      icon: 'ri-bar-chart-grouped-fill',\n      keepAlive: true,\n      isMobile: true\n    },\n    component: () => import('@/views/toplist/index.vue')\n  },\n  {\n    path: '/mv',\n    name: 'mv',\n    meta: {\n      title: 'comp.mv',\n      icon: 'icon-recordfill',\n      keepAlive: true,\n      isMobile: false\n    },\n    component: () => import('@/views/mv/index.vue')\n  },\n  {\n    path: '/history',\n    name: 'history',\n    component: () => import('@/views/historyAndFavorite/index.vue'),\n    meta: {\n      title: 'comp.history',\n      icon: 'icon-a-TicketStar',\n      keepAlive: true,\n      isMobile: true\n    }\n  },\n  {\n    path: '/user',\n    name: 'user',\n    meta: {\n      title: 'comp.user',\n      icon: 'icon-Profile',\n      keepAlive: true,\n      noScroll: true,\n      isMobile: true\n    },\n    component: () => import('@/views/user/index.vue')\n  },\n  {\n    path: '/set',\n    name: 'set',\n    meta: {\n      title: 'comp.settings',\n      icon: 'ri-settings-3-fill',\n      keepAlive: true,\n      noScroll: true,\n      back: true\n    },\n    component: () => import('@/views/set/index.vue')\n  }\n];\nexport default layoutRouter;\n"
  },
  {
    "path": "src/renderer/router/index.ts",
    "content": "import { createRouter, createWebHashHistory } from 'vue-router';\n\nimport AppLayout from '@/layout/AppLayout.vue';\nimport MiniLayout from '@/layout/MiniLayout.vue';\nimport homeRouter from '@/router/home';\nimport otherRouter from '@/router/other';\nimport { useSettingsStore } from '@/store/modules/settings';\n\nimport { useUserStore } from '../store/modules/user';\n\nfunction getUserId(): string | null {\n  const userStore = useUserStore();\n  return userStore.user?.userId?.toString() || null;\n}\n\n// 由于 Vue Router 守卫在创建前不能直接使用组合式 API\n// 我们创建一个辅助函数来获取 store 实例\nlet _settingsStore: ReturnType<typeof useSettingsStore> | null = null;\nconst getSettingsStore = () => {\n  if (!_settingsStore) {\n    _settingsStore = useSettingsStore();\n  }\n  return _settingsStore;\n};\n\nconst loginRouter = {\n  path: '/login',\n  name: 'login',\n  meta: {\n    keepAlive: true,\n    title: '登录',\n    icon: 'icon-Home',\n    back: true\n  },\n  component: () => import('@/views/login/index.vue')\n};\n\nconst routes = [\n  {\n    path: '/',\n    component: AppLayout,\n    children: [...homeRouter, loginRouter, ...otherRouter]\n  },\n  {\n    path: '/lyric',\n    component: () => import('@/views/lyric/index.vue')\n  },\n  {\n    path: '/mini',\n    component: MiniLayout\n  }\n];\n\nconst router = createRouter({\n  routes,\n  history: createWebHashHistory()\n});\n\n// 添加全局前置守卫\nrouter.beforeEach((to, _, next) => {\n  const settingsStore = getSettingsStore();\n\n  // 如果是迷你模式\n  if (settingsStore.isMiniMode) {\n    // 只允许访问 /mini 路由\n    if (to.path === '/mini') {\n      next();\n    } else {\n      next(false); // 阻止导航\n    }\n  } else if (to.path === '/mini') {\n    // 如果不是迷你模式但想访问 /mini 路由，重定向到首页\n    next('/');\n  } else {\n    // 其他情况正常导航\n    next();\n  }\n});\n\n// 添加全局后置钩子，记录页面访问\nrouter.afterEach((to) => {\n  const pageName = to.name?.toString() || to.path;\n  // 使用setTimeout避免阻塞路由导航\n  setTimeout(() => {\n    const userId = getUserId();\n    console.log('pageName', pageName, userId);\n  }, 100);\n});\n\nexport default router;\n"
  },
  {
    "path": "src/renderer/router/other.ts",
    "content": "const otherRouter = [\n  {\n    path: '/user/follows',\n    name: 'userFollows',\n    meta: {\n      title: '关注列表',\n      keepAlive: false,\n      showInMenu: false,\n      back: true\n    },\n    component: () => import('@/views/user/follows.vue')\n  },\n  {\n    path: '/user/followers',\n    name: 'userFollowers',\n    meta: {\n      title: '粉丝列表',\n      keepAlive: false,\n      showInMenu: false,\n      back: true\n    },\n    component: () => import('@/views/user/followers.vue')\n  },\n  {\n    path: '/downloads',\n    name: 'downloads',\n    meta: {\n      title: '下载管理',\n      keepAlive: true,\n      showInMenu: true,\n      back: true,\n      icon: 'ri-download-cloud-2-line'\n    },\n    component: () => import('@/views/download/DownloadPage.vue')\n  },\n  {\n    path: '/user/detail/:uid',\n    name: 'userDetail',\n    meta: {\n      title: '用户详情',\n      keepAlive: false,\n      showInMenu: false,\n      back: true\n    },\n    component: () => import('@/views/user/detail.vue')\n  },\n  {\n    path: '/artist/detail/:id',\n    name: 'artistDetail',\n    meta: {\n      title: '歌手详情',\n      keepAlive: true,\n      showInMenu: false,\n      back: true\n    },\n    component: () => import('@/views/artist/detail.vue')\n  },\n  {\n    path: '/bilibili/:bvid',\n    name: 'bilibiliPlayer',\n    meta: {\n      title: 'B站听书',\n      keepAlive: true,\n      showInMenu: false,\n      back: true\n    },\n    component: () => import('@/views/bilibili/BilibiliPlayer.vue')\n  },\n  {\n    path: '/music-list/:id?',\n    name: 'musicList',\n    meta: {\n      title: '音乐列表',\n      keepAlive: false,\n      showInMenu: false,\n      back: true\n    },\n    component: () => import('@/views/music/MusicListPage.vue')\n  },\n  {\n    path: '/playlist/import',\n    name: 'playlistImport',\n    meta: {\n      title: '歌单导入',\n      keepAlive: true,\n      back: true\n    },\n    component: () => import('@/views/playlist/ImportPlaylist.vue')\n  },\n  {\n    path: '/heatmap',\n    name: 'heatmap',\n    meta: {\n      title: '播放热力图',\n      keepAlive: true,\n      showInMenu: false,\n      back: true\n    },\n    component: () => import('@/views/heatmap/index.vue')\n  },\n  {\n    path: '/history-recommend',\n    name: 'historyRecommend',\n    meta: {\n      title: '历史日推',\n      keepAlive: true,\n      showInMenu: false,\n      back: true\n    },\n    component: () => import('@/views/music/HistoryRecommend.vue')\n  },\n  {\n    path: '/mobile-search',\n    name: 'mobileSearch',\n    meta: {\n      title: '搜索',\n      keepAlive: false,\n      showInMenu: false,\n      back: true\n    },\n    component: () => import('@/views/mobile-search/index.vue')\n  },\n  {\n    path: '/mobile-search-result',\n    name: 'mobileSearchResult',\n    meta: {\n      title: '搜索结果',\n      keepAlive: false,\n      showInMenu: false,\n      back: true\n    },\n    component: () => import('@/views/mobile-search-result/index.vue')\n  }\n];\nexport default otherRouter;\n"
  },
  {
    "path": "src/renderer/services/LxMusicSourceRunner.ts",
    "content": "/**\n * 落雪音乐 (LX Music) 音源脚本执行器\n *\n * 核心职责：\n * 1. 解析脚本元信息\n * 2. 在隔离环境中执行用户脚本\n * 3. 模拟 globalThis.lx API\n * 4. 处理初始化和音乐解析请求\n */\n\nimport type {\n  LxInitedData,\n  LxLyricResult,\n  LxMusicInfo,\n  LxQuality,\n  LxScriptInfo,\n  LxSourceConfig,\n  LxSourceKey\n} from '@/types/lxMusic';\nimport * as lxCrypto from '@/utils/lxCrypto';\n\n/**\n * 解析脚本头部注释中的元信息\n */\nexport const parseScriptInfo = (script: string): LxScriptInfo => {\n  const info: LxScriptInfo = {\n    name: '未知音源',\n    rawScript: script\n  };\n\n  // 尝试匹配不同格式的头部注释块\n  // 支持 /** ... */ 和 /* ... */ 格式\n  const headerMatch = script.match(/^\\/\\*+[\\s\\S]*?\\*\\//);\n  if (!headerMatch) {\n    console.warn('[parseScriptInfo] 未找到脚本头部注释块');\n    return info;\n  }\n\n  const header = headerMatch[0];\n  console.log('[parseScriptInfo] 解析脚本头部:', header.substring(0, 200));\n\n  // 解析各个字段（支持 * 前缀和无前缀两种格式）\n  const nameMatch = header.match(/@name\\s+(.+?)(?:\\r?\\n|\\*\\/)/);\n  if (nameMatch) {\n    info.name = nameMatch[1].trim().replace(/^\\*\\s*/, '');\n    console.log('[parseScriptInfo] 解析到名称:', info.name);\n  } else {\n    console.warn('[parseScriptInfo] 未找到 @name 标签');\n  }\n\n  const descMatch = header.match(/@description\\s+(.+?)(?:\\r?\\n|\\*\\/)/);\n  if (descMatch) {\n    info.description = descMatch[1].trim().replace(/^\\*\\s*/, '');\n  }\n\n  const versionMatch = header.match(/@version\\s+(.+?)(?:\\r?\\n|\\*\\/)/);\n  if (versionMatch) {\n    info.version = versionMatch[1].trim().replace(/^\\*\\s*/, '');\n    console.log('[parseScriptInfo] 解析到版本:', info.version);\n  }\n\n  const authorMatch = header.match(/@author\\s+(.+?)(?:\\r?\\n|\\*\\/)/);\n  if (authorMatch) {\n    info.author = authorMatch[1].trim().replace(/^\\*\\s*/, '');\n  }\n\n  const homepageMatch = header.match(/@homepage\\s+(.+?)(?:\\r?\\n|\\*\\/)/);\n  if (homepageMatch) {\n    info.homepage = homepageMatch[1].trim().replace(/^\\*\\s*/, '');\n  }\n\n  return info;\n};\n\n/**\n * 落雪音源脚本执行器\n * 使用 Worker 或 iframe 隔离执行用户脚本\n */\nexport class LxMusicSourceRunner {\n  private script: string;\n  private scriptInfo: LxScriptInfo;\n  private sources: Partial<Record<LxSourceKey, LxSourceConfig>> = {};\n  private requestHandler: ((data: any) => Promise<any>) | null = null;\n  private initialized = false;\n  private initPromise: Promise<LxInitedData> | null = null;\n  // 临时存储最后一次 HTTP 请求返回的音乐 URL（用于脚本返回 undefined 时的后备）\n  private lastMusicUrl: string | null = null;\n\n  constructor(script: string) {\n    this.script = script;\n    this.scriptInfo = parseScriptInfo(script);\n  }\n\n  /**\n   * 获取脚本信息\n   */\n  getScriptInfo(): LxScriptInfo {\n    return this.scriptInfo;\n  }\n\n  /**\n   * 获取支持的音源列表\n   */\n  getSources(): Partial<Record<LxSourceKey, LxSourceConfig>> {\n    return this.sources;\n  }\n\n  /**\n   * 初始化执行器\n   */\n  async initialize(): Promise<LxInitedData> {\n    if (this.initPromise) return this.initPromise;\n\n    this.initPromise = new Promise<LxInitedData>((resolve, reject) => {\n      const timeout = setTimeout(() => {\n        reject(new Error('脚本初始化超时'));\n      }, 10000);\n\n      try {\n        // 创建沙盒环境并执行脚本\n        this.executeSandboxed(\n          (initedData) => {\n            clearTimeout(timeout);\n            this.sources = initedData.sources;\n            this.initialized = true;\n            console.log('[LxMusicRunner] 初始化成功:', initedData.sources);\n            resolve(initedData);\n          },\n          (error) => {\n            clearTimeout(timeout);\n            reject(error);\n          }\n        );\n      } catch (error) {\n        clearTimeout(timeout);\n        reject(error);\n      }\n    });\n\n    return this.initPromise;\n  }\n\n  /**\n   * 在沙盒中执行脚本\n   */\n  private executeSandboxed(\n    onInited: (data: LxInitedData) => void,\n    onError: (error: Error) => void\n  ): void {\n    // 构建沙盒执行环境\n    const sandbox = this.createSandbox(onInited, onError);\n\n    try {\n      // 使用 Function 构造器在受限环境中执行\n      // 注意：不能使用 const/let 声明 globalThis，因为它是保留标识符\n      const sandboxedScript = `\n        (function() {\n          ${sandbox.apiSetup}\n          ${this.script}\n        }).call(this);\n      `;\n\n      // 创建执行上下文\n      const context = sandbox.context;\n      const executor = new Function(sandboxedScript);\n\n      // 在隔离上下文中执行，context 将作为 this\n      executor.call(context);\n    } catch (error) {\n      onError(error as Error);\n    }\n  }\n\n  /**\n   * 创建沙盒环境\n   */\n  private createSandbox(\n    onInited: (data: LxInitedData) => void,\n    _onError: (error: Error) => void\n  ): { apiSetup: string; context: any } {\n    const self = this;\n\n    // 创建 globalThis.lx 对象\n    // 版本号使用落雪音乐最新版本以通过脚本版本检测\n    const context = {\n      lx: {\n        version: '2.8.0',\n        env: 'desktop',\n        appInfo: {\n          version: '2.8.0',\n          versionNum: 208,\n          locale: 'zh-cn'\n        },\n        currentScriptInfo: this.scriptInfo,\n        EVENT_NAMES: {\n          inited: 'inited',\n          request: 'request',\n          updateAlert: 'updateAlert'\n        },\n        on: (eventName: string, handler: (data: any) => Promise<any>) => {\n          if (eventName === 'request') {\n            self.requestHandler = handler;\n          }\n        },\n        send: (eventName: string, data: any) => {\n          if (eventName === 'inited') {\n            onInited(data as LxInitedData);\n          } else if (eventName === 'updateAlert') {\n            console.log('[LxMusicRunner] 更新提醒:', data);\n          }\n        },\n        request: (\n          url: string,\n          options: any,\n          callback: (err: Error | null, resp: any, body: any) => void\n        ) => {\n          return self.handleHttpRequest(url, options, callback);\n        },\n        utils: {\n          buffer: {\n            from: (data: any, _encoding?: string) => {\n              if (typeof data === 'string') {\n                return new TextEncoder().encode(data);\n              }\n              return new Uint8Array(data);\n            },\n            bufToString: (buffer: Uint8Array, encoding?: string) => {\n              return new TextDecoder(encoding || 'utf-8').decode(buffer);\n            }\n          },\n          crypto: {\n            md5: lxCrypto.md5,\n            sha1: lxCrypto.sha1,\n            sha256: lxCrypto.sha256,\n            randomBytes: lxCrypto.randomBytes,\n            aesEncrypt: lxCrypto.aesEncrypt,\n            aesDecrypt: lxCrypto.aesDecrypt,\n            rsaEncrypt: lxCrypto.rsaEncrypt,\n            rsaDecrypt: lxCrypto.rsaDecrypt,\n            base64Encode: lxCrypto.base64Encode,\n            base64Decode: lxCrypto.base64Decode\n          },\n          zlib: {\n            inflate: async (buffer: ArrayBuffer) => {\n              try {\n                const ds = new DecompressionStream('deflate');\n                const writer = ds.writable.getWriter();\n                writer.write(buffer);\n                writer.close();\n                const reader = ds.readable.getReader();\n                const chunks: Uint8Array[] = [];\n                let done = false;\n                while (!done) {\n                  const result = await reader.read();\n                  done = result.done;\n                  if (result.value) chunks.push(result.value);\n                }\n                const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);\n                const result = new Uint8Array(totalLength);\n                let offset = 0;\n                for (const chunk of chunks) {\n                  result.set(chunk, offset);\n                  offset += chunk.length;\n                }\n                return result.buffer;\n              } catch {\n                return buffer;\n              }\n            },\n            deflate: async (buffer: ArrayBuffer) => {\n              try {\n                const cs = new CompressionStream('deflate');\n                const writer = cs.writable.getWriter();\n                writer.write(buffer);\n                writer.close();\n                const reader = cs.readable.getReader();\n                const chunks: Uint8Array[] = [];\n                let done = false;\n                while (!done) {\n                  const result = await reader.read();\n                  done = result.done;\n                  if (result.value) chunks.push(result.value);\n                }\n                const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);\n                const result = new Uint8Array(totalLength);\n                let offset = 0;\n                for (const chunk of chunks) {\n                  result.set(chunk, offset);\n                  offset += chunk.length;\n                }\n                return result.buffer;\n              } catch {\n                return buffer;\n              }\n            }\n          }\n        }\n      },\n      console: {\n        log: (...args: any[]) => console.log('[LxScript]', ...args),\n        error: (...args: any[]) => console.error('[LxScript]', ...args),\n        warn: (...args: any[]) => console.warn('[LxScript]', ...args),\n        info: (...args: any[]) => console.info('[LxScript]', ...args)\n      },\n      setTimeout,\n      setInterval,\n      clearTimeout,\n      clearInterval,\n      Promise,\n      JSON,\n      Object,\n      Array,\n      String,\n      Number,\n      Boolean,\n      Date,\n      Math,\n      RegExp,\n      Error,\n      Map,\n      Set,\n      WeakMap,\n      WeakSet,\n      Symbol,\n      Proxy,\n      Reflect,\n      encodeURIComponent,\n      decodeURIComponent,\n      encodeURI,\n      decodeURI,\n      atob,\n      btoa,\n      TextEncoder,\n      TextDecoder,\n      Uint8Array,\n      ArrayBuffer,\n      crypto\n    };\n\n    // 只设置 lx 和 globalThis，不解构变量避免与脚本内部声明冲突\n    const apiSetup = `\n      var lx = this.lx;\n      var globalThis = this;\n    `;\n\n    return { apiSetup, context };\n  }\n\n  /**\n   * 处理 HTTP 请求（优先使用主进程，绕过 CORS 限制）\n   */\n  private handleHttpRequest(\n    url: string,\n    options: any,\n    callback: (err: Error | null, resp: any, body: any) => void\n  ): () => void {\n    console.log(`[LxMusicRunner] HTTP 请求: ${options.method || 'GET'} ${url}`);\n\n    const timeout = options.timeout || 30000;\n    const requestId = `lx_http_${Date.now()}_${Math.random().toString(36).substring(7)}`;\n\n    // 尝试使用主进程 HTTP 请求（如果可用）\n    const hasMainProcessHttp = typeof window.api?.lxMusicHttpRequest === 'function';\n\n    if (hasMainProcessHttp) {\n      // 使用主进程 HTTP 请求（绕过 CORS）\n      console.log(`[LxMusicRunner] 使用主进程 HTTP 请求`);\n\n      window.api\n        .lxMusicHttpRequest({\n          url,\n          options: {\n            ...options,\n            timeout\n          },\n          requestId\n        })\n        .then((response: any) => {\n          console.log(`[LxMusicRunner] HTTP 响应: ${response.statusCode} ${url}`);\n\n          // 如果响应中包含 URL，缓存下来以备后用\n          if (response.body && response.body.url && typeof response.body.url === 'string') {\n            this.lastMusicUrl = response.body.url;\n          }\n\n          callback(null, response, response.body);\n        })\n        .catch((error: Error) => {\n          console.error(`[LxMusicRunner] HTTP 请求失败: ${url}`, error.message);\n          callback(error, null, null);\n        });\n\n      // 返回取消函数\n      return () => {\n        void window.api?.lxMusicHttpCancel?.(requestId);\n      };\n    } else {\n      // 回退到渲染进程 fetch（可能受 CORS 限制）\n      console.log(`[LxMusicRunner] 主进程 HTTP 不可用，使用渲染进程 fetch`);\n\n      const controller = new AbortController();\n\n      const fetchOptions: RequestInit = {\n        method: options.method || 'GET',\n        headers: {\n          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n          ...options.headers\n        },\n        signal: controller.signal,\n        mode: 'cors',\n        credentials: 'omit'\n      };\n\n      if (options.body) {\n        fetchOptions.body = options.body;\n      } else if (options.form) {\n        fetchOptions.body = new URLSearchParams(options.form);\n        fetchOptions.headers = {\n          ...fetchOptions.headers,\n          'Content-Type': 'application/x-www-form-urlencoded'\n        };\n      } else if (options.formData) {\n        const formData = new FormData();\n        for (const [key, value] of Object.entries(options.formData)) {\n          formData.append(key, value as string);\n        }\n        fetchOptions.body = formData;\n      }\n\n      const timeoutId = setTimeout(() => {\n        console.warn(`[LxMusicRunner] HTTP 请求超时: ${url}`);\n        controller.abort();\n      }, timeout);\n\n      fetch(url, fetchOptions)\n        .then(async (response) => {\n          clearTimeout(timeoutId);\n          console.log(`[LxMusicRunner] HTTP 响应: ${response.status} ${url}`);\n\n          const rawBody = await response.text();\n\n          // 尝试解析 JSON\n          let parsedBody: any = rawBody;\n          const contentType = response.headers.get('content-type') || '';\n          if (\n            contentType.includes('application/json') ||\n            rawBody.startsWith('{') ||\n            rawBody.startsWith('[')\n          ) {\n            try {\n              parsedBody = JSON.parse(rawBody);\n              if (parsedBody && parsedBody.url && typeof parsedBody.url === 'string') {\n                this.lastMusicUrl = parsedBody.url;\n              }\n            } catch {\n              // 解析失败则使用原始字符串\n            }\n          }\n\n          callback(\n            null,\n            {\n              statusCode: response.status,\n              headers: Object.fromEntries(response.headers.entries()),\n              body: parsedBody\n            },\n            parsedBody\n          );\n        })\n        .catch((error) => {\n          clearTimeout(timeoutId);\n          console.error(`[LxMusicRunner] HTTP 请求失败: ${url}`, error.message);\n          callback(error, null, null);\n        });\n\n      // 返回取消函数\n      return () => controller.abort();\n    }\n  }\n\n  /**\n   * 获取音乐 URL\n   */\n  async getMusicUrl(\n    source: LxSourceKey,\n    musicInfo: LxMusicInfo,\n    quality: LxQuality\n  ): Promise<string> {\n    if (!this.initialized) {\n      await this.initialize();\n    }\n\n    if (!this.requestHandler) {\n      throw new Error('脚本未注册请求处理器');\n    }\n\n    const sourceConfig = this.sources[source];\n    if (!sourceConfig) {\n      throw new Error(`脚本不支持音源: ${source}`);\n    }\n\n    if (!sourceConfig.actions.includes('musicUrl')) {\n      throw new Error(`音源 ${source} 不支持获取音乐 URL`);\n    }\n\n    // 选择最佳音质\n    let targetQuality = quality;\n    if (!sourceConfig.qualitys.includes(quality)) {\n      // 按优先级选择可用音质\n      const qualityPriority: LxQuality[] = ['flac24bit', 'flac', '320k', '128k'];\n      for (const q of qualityPriority) {\n        if (sourceConfig.qualitys.includes(q)) {\n          targetQuality = q;\n          break;\n        }\n      }\n    }\n\n    console.log(`[LxMusicRunner] 请求音乐 URL: 音源=${source}, 音质=${targetQuality}`);\n\n    try {\n      const result = await this.requestHandler({\n        source,\n        action: 'musicUrl',\n        info: {\n          type: targetQuality,\n          musicInfo\n        }\n      });\n\n      console.log(`[LxMusicRunner] 脚本返回结果:`, result, typeof result);\n\n      // 脚本可能返回对象或字符串\n      let url: string | undefined;\n      if (typeof result === 'string') {\n        url = result;\n      } else if (result && typeof result === 'object') {\n        // 某些脚本可能返回 { url: '...' } 格式\n        url = result.url || result.data || result;\n      }\n\n      if (typeof url !== 'string' || !url) {\n        // 如果脚本返回 undefined，尝试使用缓存的 URL\n        if (this.lastMusicUrl) {\n          console.log('[LxMusicRunner] 脚本返回 undefined，使用缓存的 URL');\n          url = this.lastMusicUrl;\n          this.lastMusicUrl = null; // 清除缓存\n        } else {\n          console.error('[LxMusicRunner] 无效的返回值:', result);\n          throw new Error(result?.message || result?.msg || '获取音乐 URL 失败');\n        }\n      }\n\n      console.log('[LxMusicRunner] 获取到 URL:', url.substring(0, 80) + '...');\n      return url;\n    } catch (error) {\n      console.error('[LxMusicRunner] 获取音乐 URL 失败:', error);\n      throw error;\n    }\n  }\n\n  /**\n   * 获取歌词\n   */\n  async getLyric(source: LxSourceKey, musicInfo: LxMusicInfo): Promise<LxLyricResult | null> {\n    if (!this.initialized) {\n      await this.initialize();\n    }\n\n    if (!this.requestHandler) {\n      return null;\n    }\n\n    const sourceConfig = this.sources[source];\n    if (!sourceConfig || !sourceConfig.actions.includes('lyric')) {\n      return null;\n    }\n\n    try {\n      const result = await this.requestHandler({\n        source,\n        action: 'lyric',\n        info: {\n          type: null,\n          musicInfo\n        }\n      });\n\n      return result as LxLyricResult;\n    } catch (error) {\n      console.error('[LxMusicRunner] 获取歌词失败:', error);\n      return null;\n    }\n  }\n\n  /**\n   * 获取封面图\n   */\n  async getPic(source: LxSourceKey, musicInfo: LxMusicInfo): Promise<string | null> {\n    if (!this.initialized) {\n      await this.initialize();\n    }\n\n    if (!this.requestHandler) {\n      return null;\n    }\n\n    const sourceConfig = this.sources[source];\n    if (!sourceConfig || !sourceConfig.actions.includes('pic')) {\n      return null;\n    }\n\n    try {\n      const url = await this.requestHandler({\n        source,\n        action: 'pic',\n        info: {\n          type: null,\n          musicInfo\n        }\n      });\n\n      return typeof url === 'string' ? url : null;\n    } catch (error) {\n      console.error('[LxMusicRunner] 获取封面失败:', error);\n      return null;\n    }\n  }\n\n  /**\n   * 检查是否已初始化\n   */\n  isInitialized(): boolean {\n    return this.initialized;\n  }\n}\n\n// 全局单例\nlet runnerInstance: LxMusicSourceRunner | null = null;\n\n/**\n * 获取落雪音源执行器实例\n */\nexport const getLxMusicRunner = (): LxMusicSourceRunner | null => {\n  return runnerInstance;\n};\n\n/**\n * 设置落雪音源执行器实例\n */\nexport const setLxMusicRunner = (runner: LxMusicSourceRunner | null): void => {\n  runnerInstance = runner;\n};\n\n/**\n * 初始化落雪音源执行器（从脚本内容）\n */\nexport const initLxMusicRunner = async (script: string): Promise<LxMusicSourceRunner> => {\n  // 销毁旧实例\n  runnerInstance = null;\n\n  // 创建新实例\n  const runner = new LxMusicSourceRunner(script);\n  await runner.initialize();\n\n  runnerInstance = runner;\n  return runner;\n};\n"
  },
  {
    "path": "src/renderer/services/SongSourceConfigManager.ts",
    "content": "/**\n * 歌曲音源配置管理器\n *\n * 职责：\n * 1. 统一管理每首歌曲的自定义音源配置\n * 2. 提供清晰的读取/写入/清除 API\n * 3. 区分\"手动\"和\"自动\"设置的音源\n * 4. 管理已尝试的音源列表（按歌曲隔离）\n */\n\nimport type { Platform } from '@/types/music';\n\n// 歌曲音源配置类型\nexport type SongSourceConfig = {\n  sources: Platform[];\n  type: 'manual' | 'auto';\n  updatedAt: number;\n};\n\n// 内存中缓存已尝试的音源（按歌曲隔离）\nconst triedSourcesMap = new Map<string, Set<string>>();\nconst triedSourceDiffsMap = new Map<string, Map<string, number>>();\n\n// localStorage key 前缀\nconst STORAGE_KEY_PREFIX = 'song_source_';\nconst STORAGE_TYPE_KEY_PREFIX = 'song_source_type_';\n\n/**\n * 歌曲音源配置管理器\n */\nexport class SongSourceConfigManager {\n  /**\n   * 获取歌曲的自定义音源配置\n   */\n  static getConfig(songId: number | string): SongSourceConfig | null {\n    const id = String(songId);\n    const sourcesStr = localStorage.getItem(`${STORAGE_KEY_PREFIX}${id}`);\n    const typeStr = localStorage.getItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`);\n\n    if (!sourcesStr) {\n      return null;\n    }\n\n    try {\n      const sources = JSON.parse(sourcesStr) as Platform[];\n      if (!Array.isArray(sources) || sources.length === 0) {\n        return null;\n      }\n\n      return {\n        sources,\n        type: typeStr === 'auto' ? 'auto' : 'manual',\n        updatedAt: Date.now()\n      };\n    } catch (error) {\n      console.error(`[SongSourceConfigManager] 解析歌曲 ${id} 配置失败:`, error);\n      return null;\n    }\n  }\n\n  /**\n   * 设置歌曲的自定义音源配置\n   */\n  static setConfig(\n    songId: number | string,\n    sources: Platform[],\n    type: 'manual' | 'auto' = 'manual'\n  ): void {\n    const id = String(songId);\n\n    if (!sources || sources.length === 0) {\n      this.clearConfig(songId);\n      return;\n    }\n\n    try {\n      localStorage.setItem(`${STORAGE_KEY_PREFIX}${id}`, JSON.stringify(sources));\n      localStorage.setItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`, type);\n      console.log(`[SongSourceConfigManager] 设置歌曲 ${id} 音源: ${sources.join(', ')} (${type})`);\n    } catch (error) {\n      console.error(`[SongSourceConfigManager] 保存歌曲 ${id} 配置失败:`, error);\n    }\n  }\n\n  /**\n   * 清除歌曲的自定义配置\n   */\n  static clearConfig(songId: number | string): void {\n    const id = String(songId);\n    localStorage.removeItem(`${STORAGE_KEY_PREFIX}${id}`);\n    localStorage.removeItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`);\n    // 同时清除内存中的已尝试音源\n    this.clearTriedSources(songId);\n    console.log(`[SongSourceConfigManager] 清除歌曲 ${id} 配置`);\n  }\n\n  /**\n   * 检查歌曲是否有自定义配置\n   */\n  static hasConfig(songId: number | string): boolean {\n    return this.getConfig(songId) !== null;\n  }\n\n  /**\n   * 检查配置类型是否为手动设置\n   */\n  static isManualConfig(songId: number | string): boolean {\n    const config = this.getConfig(songId);\n    return config?.type === 'manual';\n  }\n\n  // ==================== 已尝试音源管理 ====================\n\n  /**\n   * 获取歌曲已尝试的音源列表\n   */\n  static getTriedSources(songId: number | string): Set<string> {\n    const id = String(songId);\n    if (!triedSourcesMap.has(id)) {\n      triedSourcesMap.set(id, new Set());\n    }\n    return triedSourcesMap.get(id)!;\n  }\n\n  /**\n   * 添加已尝试的音源\n   */\n  static addTriedSource(songId: number | string, source: string): void {\n    const id = String(songId);\n    const tried = this.getTriedSources(id);\n    tried.add(source);\n    console.log(`[SongSourceConfigManager] 歌曲 ${id} 添加已尝试音源: ${source}`);\n  }\n\n  /**\n   * 清除歌曲的已尝试音源\n   */\n  static clearTriedSources(songId: number | string): void {\n    const id = String(songId);\n    triedSourcesMap.delete(id);\n    triedSourceDiffsMap.delete(id);\n    console.log(`[SongSourceConfigManager] 清除歌曲 ${id} 已尝试音源`);\n  }\n\n  /**\n   * 获取歌曲已尝试音源的时长差异\n   */\n  static getTriedSourceDiffs(songId: number | string): Map<string, number> {\n    const id = String(songId);\n    if (!triedSourceDiffsMap.has(id)) {\n      triedSourceDiffsMap.set(id, new Map());\n    }\n    return triedSourceDiffsMap.get(id)!;\n  }\n\n  /**\n   * 设置音源的时长差异\n   */\n  static setTriedSourceDiff(songId: number | string, source: string, diff: number): void {\n    const id = String(songId);\n    const diffs = this.getTriedSourceDiffs(id);\n    diffs.set(source, diff);\n  }\n\n  /**\n   * 查找最佳匹配的音源（时长差异最小）\n   */\n  static findBestMatchingSource(songId: number | string): { source: string; diff: number } | null {\n    const diffs = this.getTriedSourceDiffs(songId);\n    if (diffs.size === 0) {\n      return null;\n    }\n\n    let bestSource = '';\n    let minDiff = Infinity;\n\n    for (const [source, diff] of diffs.entries()) {\n      if (diff < minDiff) {\n        minDiff = diff;\n        bestSource = source;\n      }\n    }\n\n    return bestSource ? { source: bestSource, diff: minDiff } : null;\n  }\n\n  /**\n   * 清理所有内存缓存（用于测试或重置）\n   */\n  static clearAllMemoryCache(): void {\n    triedSourcesMap.clear();\n    triedSourceDiffsMap.clear();\n    console.log('[SongSourceConfigManager] 清除所有内存缓存');\n  }\n}\n\n// 导出单例实例方便使用\nexport const songSourceConfig = SongSourceConfigManager;\n"
  },
  {
    "path": "src/renderer/services/audioService.ts",
    "content": "import { Howl, Howler } from 'howler';\n\nimport type { SongResult } from '@/types/music';\nimport { isElectron } from '@/utils'; // 导入isElectron常量\n\nclass AudioService {\n  private currentSound: Howl | null = null;\n  private pendingSound: Howl | null = null;\n\n  private currentTrack: SongResult | null = null;\n\n  private context: AudioContext | null = null;\n\n  private filters: BiquadFilterNode[] = [];\n\n  private source: MediaElementAudioSourceNode | null = null;\n\n  private gainNode: GainNode | null = null;\n\n  private bypass = false;\n\n  private playbackRate = 1.0; // 添加播放速度属性\n\n  // 预设的 EQ 频段\n  private readonly frequencies = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];\n\n  // 默认的 EQ 设置\n  private defaultEQSettings: { [key: string]: number } = {\n    '31': 0,\n    '62': 0,\n    '125': 0,\n    '250': 0,\n    '500': 0,\n    '1000': 0,\n    '2000': 0,\n    '4000': 0,\n    '8000': 0,\n    '16000': 0\n  };\n\n  private retryCount = 0;\n\n  private seekLock = false;\n\n  private seekDebounceTimer: NodeJS.Timeout | null = null;\n\n  // 添加操作锁防止并发操作\n  private operationLock = false;\n  private operationLockTimer: NodeJS.Timeout | null = null;\n  private operationLockTimeout = 5000; // 5秒超时\n  private operationLockStartTime: number = 0;\n  private operationLockId: string = '';\n\n  constructor() {\n    if ('mediaSession' in navigator) {\n      this.initMediaSession();\n    }\n    // 从本地存储加载 EQ 开关状态\n    const bypassState = localStorage.getItem('eqBypass');\n    this.bypass = bypassState ? JSON.parse(bypassState) : false;\n\n    // 页面加载时立即强制重置操作锁\n    this.forceResetOperationLock();\n\n    // 添加页面卸载事件，确保离开页面时清除锁\n    window.addEventListener('beforeunload', () => {\n      this.forceResetOperationLock();\n    });\n  }\n\n  private initMediaSession() {\n    navigator.mediaSession.setActionHandler('play', () => {\n      this.currentSound?.play();\n    });\n\n    navigator.mediaSession.setActionHandler('pause', () => {\n      this.currentSound?.pause();\n    });\n\n    navigator.mediaSession.setActionHandler('stop', () => {\n      this.stop();\n    });\n\n    navigator.mediaSession.setActionHandler('seekto', (event) => {\n      if (event.seekTime && this.currentSound) {\n        // this.currentSound.seek(event.seekTime);\n        this.seek(event.seekTime);\n      }\n    });\n\n    navigator.mediaSession.setActionHandler('seekbackward', (event) => {\n      if (this.currentSound) {\n        const currentTime = this.currentSound.seek() as number;\n        this.seek(currentTime - (event.seekOffset || 10));\n      }\n    });\n\n    navigator.mediaSession.setActionHandler('seekforward', (event) => {\n      if (this.currentSound) {\n        const currentTime = this.currentSound.seek() as number;\n        this.seek(currentTime + (event.seekOffset || 10));\n      }\n    });\n\n    navigator.mediaSession.setActionHandler('previoustrack', () => {\n      // 这里需要通过回调通知外部\n      this.emit('previoustrack');\n    });\n\n    navigator.mediaSession.setActionHandler('nexttrack', () => {\n      // 这里需要通过回调通知外部\n      this.emit('nexttrack');\n    });\n  }\n\n  private updateMediaSessionMetadata(track: SongResult) {\n    try {\n      if (!('mediaSession' in navigator)) return;\n\n      const artists = track.ar\n        ? track.ar.map((a) => a.name)\n        : track.song.artists?.map((a) => a.name);\n      const album = track.al ? track.al.name : track.song.album.name;\n      const artwork = ['96', '128', '192', '256', '384', '512'].map((size) => ({\n        src: `${track.picUrl}?param=${size}y${size}`,\n        type: 'image/jpg',\n        sizes: `${size}x${size}`\n      }));\n      const metadata = {\n        title: track.name || '',\n        artist: artists ? artists.join(',') : '',\n        album: album || '',\n        artwork\n      };\n\n      navigator.mediaSession.metadata = new window.MediaMetadata(metadata);\n    } catch (error) {\n      console.error('更新媒体会话元数据时出错:', error);\n    }\n  }\n\n  private updateMediaSessionState(isPlaying: boolean) {\n    if (!('mediaSession' in navigator)) return;\n\n    navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';\n    this.updateMediaSessionPositionState();\n  }\n\n  private updateMediaSessionPositionState() {\n    try {\n      if (!this.currentSound || !('mediaSession' in navigator)) return;\n      if ('setPositionState' in navigator.mediaSession) {\n        navigator.mediaSession.setPositionState({\n          duration: this.currentSound.duration(),\n          playbackRate: this.playbackRate,\n          position: this.currentSound.seek() as number\n        });\n      }\n    } catch (error) {\n      console.error('更新媒体会话位置状态时出错:', error);\n    }\n  }\n\n  // 事件处理相关\n  private callbacks: { [key: string]: Function[] } = {};\n\n  private emit(event: string, ...args: any[]) {\n    const eventCallbacks = this.callbacks[event];\n    if (eventCallbacks) {\n      eventCallbacks.forEach((callback) => callback(...args));\n    }\n  }\n\n  on(event: string, callback: Function) {\n    if (!this.callbacks[event]) {\n      this.callbacks[event] = [];\n    }\n    this.callbacks[event].push(callback);\n  }\n\n  off(event: string, callback: Function) {\n    const eventCallbacks = this.callbacks[event];\n    if (eventCallbacks) {\n      this.callbacks[event] = eventCallbacks.filter((cb) => cb !== callback);\n    }\n  }\n\n  // EQ 相关方法\n  public isEQEnabled(): boolean {\n    return !this.bypass;\n  }\n\n  public setEQEnabled(enabled: boolean) {\n    this.bypass = !enabled;\n    localStorage.setItem('eqBypass', JSON.stringify(this.bypass));\n\n    if (this.source && this.gainNode && this.context) {\n      this.applyBypassState();\n    }\n  }\n\n  public setEQFrequencyGain(frequency: string, gain: number) {\n    const filterIndex = this.frequencies.findIndex((f) => f.toString() === frequency);\n    if (filterIndex !== -1 && this.filters[filterIndex]) {\n      this.filters[filterIndex].gain.setValueAtTime(gain, this.context?.currentTime || 0);\n      this.saveEQSettings(frequency, gain);\n    }\n  }\n\n  public resetEQ() {\n    this.filters.forEach((filter) => {\n      filter.gain.setValueAtTime(0, this.context?.currentTime || 0);\n    });\n    localStorage.removeItem('eqSettings');\n  }\n\n  public getAllEQSettings(): { [key: string]: number } {\n    return this.loadEQSettings();\n  }\n\n  private saveEQSettings(frequency: string, gain: number) {\n    const settings = this.loadEQSettings();\n    settings[frequency] = gain;\n    localStorage.setItem('eqSettings', JSON.stringify(settings));\n  }\n\n  private loadEQSettings(): { [key: string]: number } {\n    const savedSettings = localStorage.getItem('eqSettings');\n    return savedSettings ? JSON.parse(savedSettings) : { ...this.defaultEQSettings };\n  }\n\n  private async disposeEQ(keepContext = false) {\n    try {\n      // 清理音频节点连接\n      if (this.source) {\n        this.source.disconnect();\n        this.source = null;\n      }\n\n      // 清理滤波器\n      this.filters.forEach((filter) => {\n        try {\n          filter.disconnect();\n        } catch (e) {\n          console.warn('清理滤波器时出错:', e);\n        }\n      });\n      this.filters = [];\n\n      // 清理增益节点\n      if (this.gainNode) {\n        this.gainNode.disconnect();\n        this.gainNode = null;\n      }\n\n      // 如果不需要保持上下文，则关闭它\n      if (!keepContext && this.context) {\n        try {\n          await this.context.close();\n          this.context = null;\n        } catch (e) {\n          console.warn('关闭音频上下文时出错:', e);\n        }\n      }\n    } catch (error) {\n      console.error('清理EQ资源时出错:', error);\n    }\n  }\n\n  private async setupEQ(sound: Howl) {\n    try {\n      if (!isElectron) {\n        console.log('Web环境中跳过EQ设置，避免CORS问题');\n        this.bypass = true;\n        return;\n      }\n      const howl = sound as any;\n\n      const audioNode = howl._sounds?.[0]?._node;\n\n      if (!audioNode || !(audioNode instanceof HTMLMediaElement)) {\n        if (this.retryCount < 3) {\n          console.warn('等待音频节点初始化，重试次数:', this.retryCount + 1);\n          await new Promise((resolve) => setTimeout(resolve, 100));\n          this.retryCount++;\n          return await this.setupEQ(sound);\n        }\n        throw new Error('无法获取音频节点，请重试');\n      }\n\n      this.retryCount = 0;\n\n      // 确保使用 Howler 的音频上下文\n      this.context = Howler.ctx as AudioContext;\n\n      if (!this.context || this.context.state === 'closed') {\n        Howler.ctx = new AudioContext();\n        this.context = Howler.ctx;\n        Howler.masterGain = this.context.createGain();\n        Howler.masterGain.connect(this.context.destination);\n      }\n\n      if (this.context.state === 'suspended') {\n        await this.context.resume();\n      }\n\n      // 清理现有连接\n      await this.disposeEQ(true);\n\n      try {\n        // 检查节点是否已经有源\n        const existingSource = (audioNode as any).source as MediaElementAudioSourceNode;\n        if (existingSource?.context === this.context) {\n          console.log('复用现有音频源节点');\n          this.source = existingSource;\n        } else {\n          // 创建新的源节点\n          console.log('创建新的音频源节点');\n          this.source = this.context.createMediaElementSource(audioNode);\n          (audioNode as any).source = this.source;\n        }\n      } catch (e) {\n        console.error('创建音频源节点失败:', e);\n        throw e;\n      }\n\n      // 创建增益节点\n      this.gainNode = this.context.createGain();\n\n      // 创建滤波器\n      this.filters = this.frequencies.map((freq) => {\n        const filter = this.context!.createBiquadFilter();\n        filter.type = 'peaking';\n        filter.frequency.value = freq;\n        filter.Q.value = 1;\n        filter.gain.value = this.loadEQSettings()[freq.toString()] || 0;\n        return filter;\n      });\n\n      // 应用EQ状态\n      this.applyBypassState();\n\n      // 从 localStorage 应用音量到增益节点\n      const savedVolume = localStorage.getItem('volume');\n      if (savedVolume) {\n        this.applyVolume(parseFloat(savedVolume));\n      } else {\n        this.applyVolume(1);\n      }\n\n      console.log('EQ initialization successful');\n    } catch (error) {\n      console.error('EQ initialization failed:', error);\n      await this.disposeEQ();\n      throw error;\n    }\n  }\n\n  private applyBypassState() {\n    if (!this.source || !this.gainNode || !this.context) return;\n\n    try {\n      // 断开所有现有连接\n      this.source.disconnect();\n      this.filters.forEach((filter) => filter.disconnect());\n      this.gainNode.disconnect();\n\n      if (this.bypass) {\n        // EQ被禁用时，直接连接到输出\n        this.source.connect(this.gainNode);\n        this.gainNode.connect(this.context.destination);\n      } else {\n        // EQ启用时，通过滤波器链连接\n        this.source.connect(this.filters[0]);\n        this.filters.forEach((filter, index) => {\n          if (index < this.filters.length - 1) {\n            filter.connect(this.filters[index + 1]);\n          }\n        });\n        this.filters[this.filters.length - 1].connect(this.gainNode);\n        this.gainNode.connect(this.context.destination);\n      }\n    } catch (error) {\n      console.error('应用EQ状态时出错:', error);\n    }\n  }\n\n  // 设置操作锁，带超时自动释放\n  private setOperationLock(): boolean {\n    // 生成唯一的锁ID\n    const lockId = Date.now().toString() + Math.random().toString(36).substring(2, 9);\n\n    // 如果锁已经存在，检查是否超时\n    if (this.operationLock) {\n      const currentTime = Date.now();\n      const lockDuration = currentTime - this.operationLockStartTime;\n\n      // 如果锁持续时间超过2秒，直接强制重置\n      if (lockDuration > 2000) {\n        console.warn(`操作锁已激活 ${lockDuration}ms，超过安全阈值，强制重置`);\n        this.forceResetOperationLock();\n      } else {\n        console.log(`操作锁激活中，持续时间 ${lockDuration}ms`);\n        return false;\n      }\n    }\n\n    this.operationLock = true;\n    this.operationLockStartTime = Date.now();\n    this.operationLockId = lockId;\n\n    // 将锁信息存储到 localStorage（仅用于调试，实际不依赖此值）\n    try {\n      localStorage.setItem(\n        'audioOperationLock',\n        JSON.stringify({\n          id: this.operationLockId,\n          startTime: this.operationLockStartTime\n        })\n      );\n    } catch (error) {\n      console.error('存储操作锁信息失败:', error);\n    }\n\n    // 清除之前的定时器\n    if (this.operationLockTimer) {\n      clearTimeout(this.operationLockTimer);\n    }\n\n    // 设置超时自动释放锁\n    this.operationLockTimer = setTimeout(() => {\n      console.warn('操作锁超时自动释放');\n      this.releaseOperationLock();\n    }, this.operationLockTimeout);\n\n    return true;\n  }\n\n  // 释放操作锁\n  public releaseOperationLock(): void {\n    this.operationLock = false;\n    this.operationLockStartTime = 0;\n\n    // 从 localStorage 中移除锁信息\n    try {\n      localStorage.removeItem('audioOperationLock');\n    } catch (error) {\n      console.error('清除存储的操作锁信息失败:', error);\n    }\n\n    if (this.operationLockTimer) {\n      clearTimeout(this.operationLockTimer);\n      this.operationLockTimer = null;\n    }\n  }\n\n  // 强制重置操作锁，用于特殊情况\n  public forceResetOperationLock(): void {\n    console.log('强制重置操作锁');\n    this.operationLock = false;\n    this.operationLockStartTime = 0;\n    this.operationLockId = '';\n\n    if (this.operationLockTimer) {\n      clearTimeout(this.operationLockTimer);\n      this.operationLockTimer = null;\n    }\n\n    // 清除存储的锁\n    localStorage.removeItem('audioOperationLock');\n  }\n\n  // 播放控制相关\n  public play(\n    url: string,\n    track: SongResult,\n    isPlay: boolean = true,\n    seekTime: number = 0,\n    existingSound?: Howl\n  ): Promise<Howl> {\n    // 每次调用play方法时，尝试强制重置锁（注意：仅在页面刷新后的第一次播放时应用）\n    if (!this.currentSound) {\n      console.log('首次播放请求，强制重置操作锁');\n      this.forceResetOperationLock();\n    }\n\n    // 如果有操作锁，且不是同一个 track 的操作，则等待\n    if (this.operationLock) {\n      console.log('audioService: 操作锁激活中，等待...');\n      return Promise.reject(new Error('操作锁激活中'));\n    }\n\n    if (!this.setOperationLock()) {\n      console.log('audioService: 获取操作锁失败');\n      return Promise.reject(new Error('操作锁激活中'));\n    }\n\n    // 如果操作锁已激活，但持续时间超过安全阈值，强制重置\n    if (this.operationLock) {\n      const currentTime = Date.now();\n      const lockDuration = currentTime - this.operationLockStartTime;\n\n      if (lockDuration > 2000) {\n        console.warn(`操作锁已激活 ${lockDuration}ms，超过安全阈值，强制重置`);\n        this.forceResetOperationLock();\n      }\n    }\n\n    // 获取锁\n    if (!this.setOperationLock()) {\n      console.log('audioService: 操作锁激活，强制执行当前播放请求');\n\n      // 如果只是要继续播放当前音频，直接执行\n      if (this.currentSound && !url && !track) {\n        if (this.seekLock && this.seekDebounceTimer) {\n          clearTimeout(this.seekDebounceTimer);\n          this.seekLock = false;\n        }\n        this.currentSound.play();\n        return Promise.resolve(this.currentSound);\n      }\n\n      // 强制释放锁并继续执行\n      this.forceResetOperationLock();\n\n      // 这里不再返回错误，而是继续执行播放逻辑\n    }\n\n    // 如果没有提供新的 URL 和 track，且当前有音频实例，则继续播放\n    if (this.currentSound && !url && !track) {\n      // 如果有进行中的seek操作，等待其完成\n      if (this.seekLock && this.seekDebounceTimer) {\n        clearTimeout(this.seekDebounceTimer);\n        this.seekLock = false;\n      }\n      this.currentSound.play();\n      this.releaseOperationLock();\n      return Promise.resolve(this.currentSound);\n    }\n\n    // 如果没有提供必要的参数，返回错误\n    if (!url || !track) {\n      this.releaseOperationLock();\n      return Promise.reject(new Error('缺少必要参数: url和track'));\n    }\n\n    // 检查是否是同一首歌曲的无缝切换（Hot-Swap）\n    const isHotSwap =\n      this.currentTrack && track && this.currentTrack.id === track.id && this.currentSound;\n\n    if (isHotSwap) {\n      console.log('audioService: 检测到同一首歌曲的源切换，启用无缝切换模式');\n    }\n\n    return new Promise<Howl>((resolve, reject) => {\n      let retryCount = 0;\n      const maxRetries = 1;\n\n      // 如果有正在加载的 pendingSound，先清理掉\n      if (this.pendingSound) {\n        console.log('audioService: 清理正在加载的 pendingSound');\n        this.pendingSound.unload();\n        this.pendingSound = null;\n      }\n\n      const tryPlay = async () => {\n        try {\n          console.log('audioService: 开始创建音频对象');\n\n          // 确保 Howler 上下文已初始化\n          if (!Howler.ctx) {\n            console.log('audioService: 初始化 Howler 上下文');\n            Howler.ctx = new (window.AudioContext || (window as any).webkitAudioContext)();\n          }\n\n          // 确保使用同一个音频上下文\n          if (Howler.ctx.state === 'closed') {\n            console.log('audioService: 重新创建音频上下文');\n            Howler.ctx = new (window.AudioContext || (window as any).webkitAudioContext)();\n            this.context = Howler.ctx;\n            Howler.masterGain = this.context.createGain();\n            Howler.masterGain.connect(this.context.destination);\n          }\n\n          // 恢复上下文状态\n          if (Howler.ctx.state === 'suspended') {\n            console.log('audioService: 恢复暂停的音频上下文');\n            await Howler.ctx.resume();\n          }\n\n          // 非热切换模式下，先停止并清理现有的音频实例\n          if (!isHotSwap && this.currentSound) {\n            console.log('audioService: 停止并清理现有的音频实例');\n            // 确保任何进行中的seek操作被取消\n            if (this.seekLock && this.seekDebounceTimer) {\n              clearTimeout(this.seekDebounceTimer);\n              this.seekLock = false;\n            }\n            this.currentSound.stop();\n            this.currentSound.unload();\n            this.currentSound = null;\n          }\n\n          // 清理 EQ 但保持上下文 (热切换时暂时不清理，等切换完成后再处理)\n          if (!isHotSwap) {\n            console.log('audioService: 清理 EQ');\n            await this.disposeEQ(true);\n          }\n\n          // 如果不是热切换，立即更新 currentTrack\n          if (!isHotSwap) {\n            this.currentTrack = track;\n          }\n\n          // 如果不是热切换，立即更新 currentTrack\n          if (!isHotSwap) {\n            this.currentTrack = track;\n          }\n\n          let newSound: Howl;\n\n          if (existingSound) {\n            console.log('audioService: 使用预加载的 Howl 对象');\n            newSound = existingSound;\n            // 确保 volume 和 rate 正确\n            newSound.volume(1); // 内部 volume 设为 1，由 Howler.masterGain 控制实际音量\n            newSound.rate(this.playbackRate);\n\n            // 重新绑定事件监听器，因为 PreloadService 可能没有绑定这些\n            // 注意：Howler 允许重复绑定，但最好先清理（如果无法清理，就直接绑定，Howler 是 EventEmitter）\n            // 这里我们假设 existingSound 是干净的或者我们只绑定我们需要关心的\n          } else {\n            console.log('audioService: 创建新的 Howl 对象');\n            newSound = new Howl({\n              src: [url],\n              html5: true,\n              autoplay: false,\n              volume: 1, // 禁用 Howler.js 音量控制\n              rate: this.playbackRate,\n              format: ['mp3', 'aac']\n            });\n          }\n\n          // 统一设置事件处理\n          const setupEvents = () => {\n            newSound.off('loaderror');\n            newSound.off('playerror');\n            newSound.off('load');\n\n            newSound.on('loaderror', (_, error) => {\n              console.error('Audio load error:', error);\n              if (retryCount < maxRetries && !existingSound) {\n                // 预加载的音频通常已经 loaded，不应重试\n                retryCount++;\n                console.log(`Retrying playback (${retryCount}/${maxRetries})...`);\n                setTimeout(tryPlay, 1000 * retryCount);\n              } else {\n                this.emit('url_expired', track);\n                this.releaseOperationLock();\n                if (isHotSwap) this.pendingSound = null;\n                reject(new Error('音频加载失败，请尝试切换其他歌曲'));\n              }\n            });\n\n            newSound.on('playerror', (_, error) => {\n              console.error('Audio play error:', error);\n              if (retryCount < maxRetries) {\n                retryCount++;\n                console.log(`Retrying playback (${retryCount}/${maxRetries})...`);\n                setTimeout(tryPlay, 1000 * retryCount);\n              } else {\n                this.emit('url_expired', track);\n                this.releaseOperationLock();\n                if (isHotSwap) this.pendingSound = null;\n                reject(new Error('音频播放失败，请尝试切换其他歌曲'));\n              }\n            });\n\n            const onLoaded = async () => {\n              try {\n                // 如果是热切换，现在执行切换逻辑\n                if (isHotSwap) {\n                  console.log('audioService: 执行无缝切换');\n\n                  // 1. 获取当前播放进度或使用指定的 seekTime\n                  let targetPos = 0;\n                  if (seekTime > 0) {\n                    // 如果有指定的 seekTime（如恢复播放进度），优先使用\n                    targetPos = seekTime;\n                    console.log(`audioService: 使用指定的 seekTime: ${seekTime}s`);\n                  } else if (this.currentSound) {\n                    // 否则同步当前进度\n                    targetPos = this.currentSound.seek() as number;\n                  }\n\n                  // 2. 同步新音频进度\n                  newSound.seek(targetPos);\n\n                  // 3. 初始化新音频的 EQ\n                  await this.disposeEQ(true);\n                  await this.setupEQ(newSound);\n\n                  // 4. 播放新音频\n                  if (isPlay) {\n                    newSound.play();\n                  }\n\n                  // 5. 停止旧音频\n                  if (this.currentSound) {\n                    this.currentSound.stop();\n                    this.currentSound.unload();\n                  }\n\n                  // 6. 更新引用\n                  this.currentSound = newSound;\n                  this.currentTrack = track;\n                  this.pendingSound = null;\n\n                  console.log(`audioService: 无缝切换完成，进度同步至 ${targetPos}s`);\n                } else {\n                  // 普通加载逻辑\n                  await this.setupEQ(newSound);\n                  this.currentSound = newSound;\n                }\n\n                // 重新应用已保存的音量\n                const savedVolume = localStorage.getItem('volume');\n                if (savedVolume) {\n                  this.applyVolume(parseFloat(savedVolume));\n                }\n\n                if (this.currentSound) {\n                  try {\n                    if (!isHotSwap && seekTime > 0) {\n                      this.currentSound.seek(seekTime);\n                    }\n\n                    console.log('audioService: 音频加载成功，设置 EQ');\n                    this.updateMediaSessionMetadata(track);\n                    this.updateMediaSessionPositionState();\n                    this.emit('load');\n\n                    if (!isHotSwap) {\n                      console.log('audioService: 音频完全初始化，isPlay =', isPlay);\n                      if (isPlay) {\n                        console.log('audioService: 开始播放');\n                        this.currentSound.play();\n                      }\n                    }\n\n                    resolve(this.currentSound);\n                  } catch (error) {\n                    console.error('Audio initialization failed:', error);\n                    reject(error);\n                  }\n                }\n              } catch (error) {\n                console.error('Audio initialization failed:', error);\n                reject(error);\n              }\n            };\n\n            if (newSound.state() === 'loaded') {\n              onLoaded();\n            } else {\n              newSound.once('load', onLoaded);\n            }\n          };\n\n          setupEvents();\n\n          if (isHotSwap) {\n            this.pendingSound = newSound;\n          } else {\n            this.currentSound = newSound;\n          }\n\n          // 设置音频事件监听 (play, pause, end, seek)\n          // ... (保持原有的事件监听逻辑不变，但需要确保绑定到 newSound)\n          const soundInstance = newSound;\n          if (soundInstance) {\n            // 清除旧的监听器以防重复\n            soundInstance.off('play');\n            soundInstance.off('pause');\n            soundInstance.off('end');\n            soundInstance.off('seek');\n\n            soundInstance.on('play', () => {\n              if (this.currentSound === soundInstance) {\n                this.updateMediaSessionState(true);\n                this.emit('play');\n              }\n            });\n\n            soundInstance.on('pause', () => {\n              if (this.currentSound === soundInstance) {\n                this.updateMediaSessionState(false);\n                this.emit('pause');\n              }\n            });\n\n            soundInstance.on('end', () => {\n              if (this.currentSound === soundInstance) {\n                this.emit('end');\n              }\n            });\n\n            soundInstance.on('seek', () => {\n              if (this.currentSound === soundInstance) {\n                this.updateMediaSessionPositionState();\n                this.emit('seek');\n              }\n            });\n          }\n        } catch (error) {\n          console.error('Error creating audio instance:', error);\n          this.releaseOperationLock();\n          reject(error);\n        }\n      };\n\n      tryPlay();\n    }).finally(() => {\n      // 无论成功或失败都解除操作锁\n      this.releaseOperationLock();\n    });\n  }\n\n  getCurrentSound() {\n    return this.currentSound;\n  }\n\n  getCurrentTrack() {\n    return this.currentTrack;\n  }\n\n  stop() {\n    // 强制重置操作锁并继续执行\n    this.forceResetOperationLock();\n\n    try {\n      if (this.currentSound) {\n        try {\n          // 确保任何进行中的seek操作被取消\n          if (this.seekLock && this.seekDebounceTimer) {\n            clearTimeout(this.seekDebounceTimer);\n            this.seekLock = false;\n          }\n          this.currentSound.stop();\n          this.currentSound.unload();\n        } catch (error) {\n          console.error('停止音频失败:', error);\n        }\n        this.currentSound = null;\n      }\n\n      this.currentTrack = null;\n      if ('mediaSession' in navigator) {\n        navigator.mediaSession.playbackState = 'none';\n      }\n      this.disposeEQ();\n    } catch (error) {\n      console.error('停止音频时发生错误:', error);\n    }\n  }\n\n  setVolume(volume: number) {\n    this.applyVolume(volume);\n  }\n\n  seek(time: number) {\n    // 直接强制重置操作锁\n    this.forceResetOperationLock();\n\n    if (this.currentSound) {\n      try {\n        // 直接执行seek操作\n        this.currentSound.seek(time);\n        // 触发seek事件\n        this.updateMediaSessionPositionState();\n        this.emit('seek', time);\n      } catch (error) {\n        console.error('Seek操作失败:', error);\n      }\n    }\n  }\n\n  pause() {\n    this.forceResetOperationLock();\n\n    if (this.currentSound) {\n      try {\n        // 确保任何进行中的seek操作被取消\n        if (this.seekLock && this.seekDebounceTimer) {\n          clearTimeout(this.seekDebounceTimer);\n          this.seekLock = false;\n        }\n        this.currentSound.pause();\n      } catch (error) {\n        console.error('暂停音频失败:', error);\n      }\n    }\n  }\n\n  clearAllListeners() {\n    this.callbacks = {};\n  }\n\n  public getCurrentPreset(): string | null {\n    return localStorage.getItem('currentPreset');\n  }\n\n  public setCurrentPreset(preset: string): void {\n    localStorage.setItem('currentPreset', preset);\n  }\n\n  public setPlaybackRate(rate: number) {\n    if (!this.currentSound) return;\n    this.playbackRate = rate;\n\n    // Howler 的 rate() 在 html5 模式下不生效\n    this.currentSound.rate(rate);\n\n    // 取出底层 HTMLAudioElement，改原生 playbackRate\n    const sounds = (this.currentSound as any)._sounds as any[];\n    sounds.forEach(({ _node }) => {\n      if (_node instanceof HTMLAudioElement) {\n        _node.playbackRate = rate;\n      }\n    });\n\n    // 同步给 Media Session UI\n    if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) {\n      navigator.mediaSession.setPositionState({\n        duration: this.currentSound.duration(),\n        playbackRate: rate,\n        position: this.currentSound.seek() as number\n      });\n    }\n  }\n\n  public getPlaybackRate(): number {\n    return this.playbackRate;\n  }\n\n  // 新的音量调节方法\n  private applyVolume(volume: number) {\n    // 确保值在0到1之间\n    const normalizedVolume = Math.max(0, Math.min(1, volume));\n\n    // 使用线性缩放音量\n    const linearVolume = normalizedVolume;\n\n    // 将音量应用到所有相关节点\n    if (this.gainNode) {\n      // 立即设置音量\n      this.gainNode.gain.cancelScheduledValues(this.context!.currentTime);\n      this.gainNode.gain.setValueAtTime(linearVolume, this.context!.currentTime);\n    } else {\n      this.currentSound?.volume(linearVolume);\n    }\n\n    // 保存值\n    localStorage.setItem('volume', linearVolume.toString());\n\n    console.log('Volume applied (linear):', linearVolume);\n  }\n\n  // 添加方法检查当前音频是否在加载状态\n  isLoading(): boolean {\n    if (!this.currentSound) return false;\n\n    // 检查Howl对象的内部状态\n    // 如果状态为1表示已经加载但未完成，状态为2表示正在加载\n    const state = (this.currentSound as any)._state;\n    // 如果操作锁激活也认为是加载状态\n    return this.operationLock || state === 'loading' || state === 1;\n  }\n\n  // 检查音频是否真正在播放\n  isActuallyPlaying(): boolean {\n    if (!this.currentSound) return false;\n\n    try {\n      // 综合判断:\n      // 1. Howler API是否报告正在播放\n      // 2. 是否不在加载状态\n      // 3. 确保音频上下文状态正常\n      const isPlaying = this.currentSound.playing();\n      const isLoading = this.isLoading();\n      const contextRunning = Howler.ctx && Howler.ctx.state === 'running';\n\n      // 只有在三个条件都满足时才认为是真正在播放\n      return isPlaying && !isLoading && contextRunning;\n    } catch (error) {\n      console.error('检查播放状态出错:', error);\n      return false;\n    }\n  }\n}\n\nexport const audioService = new AudioService();\n"
  },
  {
    "path": "src/renderer/services/eqService.ts",
    "content": "import { Howl, Howler } from 'howler';\nimport Tuna from 'tunajs';\n\n// 类型定义扩展\ninterface HowlSound {\n  _sounds: Array<{\n    _node: HTMLMediaElement & {\n      destination?: MediaElementAudioSourceNode;\n    };\n  }>;\n}\n\nexport interface EQSettings {\n  [key: string]: number;\n}\n\nexport class EQService {\n  private context: AudioContext | null = null;\n\n  private tuna: any = null;\n\n  private equalizer: any = null;\n\n  private source: MediaElementAudioSourceNode | null = null;\n\n  private gainNode: GainNode | null = null;\n\n  private bypass = false;\n\n  // 预设频率\n  private readonly frequencies = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000];\n\n  // 默认EQ设置\n  private defaultEQSettings: EQSettings = Object.fromEntries(\n    this.frequencies.map((f) => [f.toString(), 0])\n  );\n\n  constructor() {\n    this.loadSavedSettings();\n    this.bypass = localStorage.getItem('eqBypass') === 'true';\n    this.initializeUserGestureHandler();\n  }\n\n  // 初始化用户手势处理\n  private initializeUserGestureHandler() {\n    const handler = async () => {\n      if (this.context?.state === 'suspended') {\n        await this.context.resume();\n      }\n      document.removeEventListener('click', handler);\n    };\n    document.addEventListener('click', handler);\n  }\n\n  // 初始化音频上下文\n  public async setupAudioContext(howl: Howl) {\n    try {\n      // 使用Howler的现有上下文\n      this.context = (Howler.ctx as AudioContext) || new AudioContext();\n\n      // 初始化Howler的音频系统（如果需要）\n      if (!Howler.ctx) {\n        Howler.ctx = this.context;\n        Howler.masterGain = this.context.createGain();\n        Howler.masterGain.connect(this.context.destination);\n      }\n\n      // 确保上下文处于运行状态\n      if (this.context.state === 'suspended') {\n        await this.context.resume();\n      }\n\n      const sound = (howl as unknown as HowlSound)._sounds[0];\n      if (!sound?._node) throw new Error('无法获取音频节点');\n\n      // 清理现有资源\n      await this.dispose();\n\n      // 创建新的处理链\n      this.tuna = new Tuna(this.context);\n\n      // 创建/复用源节点\n      if (!sound._node.destination) {\n        this.source = this.context.createMediaElementSource(sound._node);\n        sound._node.destination = this.source;\n      } else {\n        this.source = sound._node.destination;\n      }\n\n      // 创建效果节点\n      this.gainNode = this.context.createGain();\n      this.equalizer = new this.tuna.Equalizer({\n        frequencies: this.frequencies,\n        gains: this.frequencies.map((f) => this.getSavedGain(f.toString())),\n        bypass: this.bypass\n      });\n\n      // 连接节点链\n      this.source!.connect(this.equalizer.input).connect(this.gainNode).connect(Howler.masterGain);\n\n      // 恢复音量设置\n      const volume = localStorage.getItem('volume');\n      this.gainNode.gain.value = volume ? parseFloat(volume) : 1;\n    } catch (error) {\n      console.error('音频上下文初始化失败:', error);\n      await this.dispose();\n      throw error;\n    }\n  }\n\n  // EQ功能开关\n  public setEnabled(enabled: boolean) {\n    this.bypass = !enabled;\n    localStorage.setItem('eqBypass', JSON.stringify(this.bypass));\n    if (this.equalizer) this.equalizer.bypass = this.bypass;\n  }\n\n  public isEnabled(): boolean {\n    return !this.bypass;\n  }\n\n  // 调整频率增益\n  public setFrequencyGain(frequency: string, gain: number) {\n    const index = this.frequencies.findIndex((f) => f.toString() === frequency);\n    if (index !== -1 && this.equalizer) {\n      this.equalizer.setGain(index, gain);\n      this.saveSettings(frequency, gain);\n    }\n  }\n\n  // 重置EQ设置\n  public resetEQ() {\n    this.frequencies.forEach((f) => {\n      this.setFrequencyGain(f.toString(), 0);\n    });\n    localStorage.removeItem('eqSettings');\n  }\n\n  // 获取当前设置\n  public getAllSettings(): EQSettings {\n    return this.loadSavedSettings();\n  }\n\n  // 保存/加载设置\n  private saveSettings(frequency: string, gain: number) {\n    const settings = this.loadSavedSettings();\n    settings[frequency] = gain;\n    localStorage.setItem('eqSettings', JSON.stringify(settings));\n  }\n\n  private loadSavedSettings(): EQSettings {\n    const saved = localStorage.getItem('eqSettings');\n    return saved ? JSON.parse(saved) : { ...this.defaultEQSettings };\n  }\n\n  private getSavedGain(frequency: string): number {\n    return this.loadSavedSettings()[frequency] || 0;\n  }\n\n  // 清理资源\n  public async dispose() {\n    try {\n      [this.source, this.equalizer, this.gainNode].forEach((node) => {\n        if (node) {\n          node.disconnect();\n          // 特殊清理Tuna节点\n          if (node instanceof Tuna.Equalizer) node.destroy();\n        }\n      });\n\n      if (this.context && this.context !== Howler.ctx) {\n        await this.context.close();\n      }\n\n      this.context = null;\n      this.tuna = null;\n      this.source = null;\n      this.equalizer = null;\n      this.gainNode = null;\n    } catch (error) {\n      console.error('资源清理失败:', error);\n    }\n  }\n}\n\nexport const eqService = new EQService();\n"
  },
  {
    "path": "src/renderer/services/lyricTranslation.ts",
    "content": "import { useSettingsStore } from '@/store/modules/settings';\nimport type { ILyricText } from '@/types/music';\n\n/**\n * Translate lyric lines according to selected engine.\n * Supports runtime-loading `opencc-rust` from CDN and caches the converter.\n */\nexport async function translateLyrics(lines: ILyricText[] | undefined) {\n  if (!lines || lines.length === 0) return lines || [];\n\n  const settingsStore = useSettingsStore();\n  const engine = settingsStore.setData?.lyricTranslationEngine || 'none';\n\n  switch (engine) {\n    case 'opencc': {\n      const mod: any = await import('./translation-engines/opencc');\n      const engineMod = await mod.ensureOpenccConverter();\n      return engineMod.translateLines(lines);\n    }\n    default: {\n      return lines.map((l) => ({ ...l, trText: l.trText || '' }));\n    }\n  }\n}\n\nexport default {\n  translateLyrics\n};\n"
  },
  {
    "path": "src/renderer/services/playbackRequestManager.ts",
    "content": "/**\n * 播放请求管理器\n * 负责管理播放请求的队列、取消、状态跟踪，防止竞态条件\n */\n\nimport type { SongResult } from '@/types/music';\n\n/**\n * 请求状态枚举\n */\nexport enum RequestStatus {\n  PENDING = 'pending',\n  ACTIVE = 'active',\n  COMPLETED = 'completed',\n  CANCELLED = 'cancelled',\n  FAILED = 'failed'\n}\n\n/**\n * 播放请求接口\n */\nexport interface PlaybackRequest {\n  id: string;\n  song: SongResult;\n  status: RequestStatus;\n  timestamp: number;\n  abortController?: AbortController;\n}\n\n/**\n * 播放请求管理器类\n */\nclass PlaybackRequestManager {\n  private currentRequestId: string | null = null;\n  private requestMap: Map<string, PlaybackRequest> = new Map();\n  private requestCounter = 0;\n\n  /**\n   * 生成唯一的请求ID\n   */\n  private generateRequestId(): string {\n    return `playback_${Date.now()}_${++this.requestCounter}`;\n  }\n\n  /**\n   * 创建新的播放请求\n   * @param song 要播放的歌曲\n   * @returns 新请求的ID\n   */\n  createRequest(song: SongResult): string {\n    // 取消所有之前的请求\n    this.cancelAllRequests();\n\n    const requestId = this.generateRequestId();\n    const abortController = new AbortController();\n\n    const request: PlaybackRequest = {\n      id: requestId,\n      song,\n      status: RequestStatus.PENDING,\n      timestamp: Date.now(),\n      abortController\n    };\n\n    this.requestMap.set(requestId, request);\n    this.currentRequestId = requestId;\n\n    console.log(`[PlaybackRequestManager] 创建新请求: ${requestId}, 歌曲: ${song.name}`);\n\n    return requestId;\n  }\n\n  /**\n   * 激活请求（标记为正在处理）\n   * @param requestId 请求ID\n   */\n  activateRequest(requestId: string): boolean {\n    const request = this.requestMap.get(requestId);\n    if (!request) {\n      console.warn(`[PlaybackRequestManager] 请求不存在: ${requestId}`);\n      return false;\n    }\n\n    if (request.status === RequestStatus.CANCELLED) {\n      console.warn(`[PlaybackRequestManager] 请求已被取消: ${requestId}`);\n      return false;\n    }\n\n    request.status = RequestStatus.ACTIVE;\n    console.log(`[PlaybackRequestManager] 激活请求: ${requestId}`);\n    return true;\n  }\n\n  /**\n   * 完成请求\n   * @param requestId 请求ID\n   */\n  completeRequest(requestId: string): void {\n    const request = this.requestMap.get(requestId);\n    if (!request) {\n      return;\n    }\n\n    request.status = RequestStatus.COMPLETED;\n    console.log(`[PlaybackRequestManager] 完成请求: ${requestId}`);\n\n    // 清理旧请求（保留最近3个）\n    this.cleanupOldRequests();\n  }\n\n  /**\n   * 标记请求失败\n   * @param requestId 请求ID\n   */\n  failRequest(requestId: string): void {\n    const request = this.requestMap.get(requestId);\n    if (!request) {\n      return;\n    }\n\n    request.status = RequestStatus.FAILED;\n    console.log(`[PlaybackRequestManager] 请求失败: ${requestId}`);\n  }\n\n  /**\n   * 取消指定请求\n   * @param requestId 请求ID\n   */\n  cancelRequest(requestId: string): void {\n    const request = this.requestMap.get(requestId);\n    if (!request) {\n      return;\n    }\n\n    if (request.status === RequestStatus.CANCELLED) {\n      return;\n    }\n\n    // 取消AbortController\n    if (request.abortController && !request.abortController.signal.aborted) {\n      request.abortController.abort();\n    }\n\n    request.status = RequestStatus.CANCELLED;\n    console.log(`[PlaybackRequestManager] 取消请求: ${requestId}, 歌曲: ${request.song.name}`);\n\n    // 如果是当前请求，清除当前请求ID\n    if (this.currentRequestId === requestId) {\n      this.currentRequestId = null;\n    }\n  }\n\n  /**\n   * 取消所有请求\n   */\n  cancelAllRequests(): void {\n    console.log(`[PlaybackRequestManager] 取消所有请求，当前请求数: ${this.requestMap.size}`);\n\n    this.requestMap.forEach((request) => {\n      if (\n        request.status !== RequestStatus.COMPLETED &&\n        request.status !== RequestStatus.CANCELLED\n      ) {\n        this.cancelRequest(request.id);\n      }\n    });\n  }\n\n  /**\n   * 检查请求是否仍然有效（是当前活动请求）\n   * @param requestId 请求ID\n   * @returns 是否有效\n   */\n  isRequestValid(requestId: string): boolean {\n    // 检查是否是当前请求\n    if (this.currentRequestId !== requestId) {\n      console.warn(\n        `[PlaybackRequestManager] 请求已过期: ${requestId}, 当前请求: ${this.currentRequestId}`\n      );\n      return false;\n    }\n\n    const request = this.requestMap.get(requestId);\n    if (!request) {\n      console.warn(`[PlaybackRequestManager] 请求不存在: ${requestId}`);\n      return false;\n    }\n\n    // 检查请求状态\n    if (request.status === RequestStatus.CANCELLED) {\n      console.warn(`[PlaybackRequestManager] 请求已被取消: ${requestId}`);\n      return false;\n    }\n\n    return true;\n  }\n\n  /**\n   * 检查请求是否应该中止（用于 AbortController）\n   * @param requestId 请求ID\n   * @returns AbortSignal 或 undefined\n   */\n  getAbortSignal(requestId: string): AbortSignal | undefined {\n    const request = this.requestMap.get(requestId);\n    return request?.abortController?.signal;\n  }\n\n  /**\n   * 获取当前请求ID\n   */\n  getCurrentRequestId(): string | null {\n    return this.currentRequestId;\n  }\n\n  /**\n   * 获取请求信息\n   * @param requestId 请求ID\n   */\n  getRequest(requestId: string): PlaybackRequest | undefined {\n    return this.requestMap.get(requestId);\n  }\n\n  /**\n   * 清理旧请求（保留最近3个）\n   */\n  private cleanupOldRequests(): void {\n    if (this.requestMap.size <= 3) {\n      return;\n    }\n\n    // 按时间戳排序，保留最新的3个\n    const sortedRequests = Array.from(this.requestMap.values()).sort(\n      (a, b) => b.timestamp - a.timestamp\n    );\n\n    const toKeep = new Set(sortedRequests.slice(0, 3).map((r) => r.id));\n    const toDelete: string[] = [];\n\n    this.requestMap.forEach((_, id) => {\n      if (!toKeep.has(id)) {\n        toDelete.push(id);\n      }\n    });\n\n    toDelete.forEach((id) => {\n      this.requestMap.delete(id);\n    });\n\n    if (toDelete.length > 0) {\n      console.log(`[PlaybackRequestManager] 清理了 ${toDelete.length} 个旧请求`);\n    }\n  }\n\n  /**\n   * 重置管理器（用于调试或特殊情况）\n   */\n  reset(): void {\n    console.log('[PlaybackRequestManager] 重置管理器');\n    this.cancelAllRequests();\n    this.requestMap.clear();\n    this.currentRequestId = null;\n    this.requestCounter = 0;\n  }\n\n  /**\n   * 获取调试信息\n   */\n  getDebugInfo(): {\n    currentRequestId: string | null;\n    totalRequests: number;\n    requestsByStatus: Record<string, number>;\n  } {\n    const requestsByStatus: Record<string, number> = {\n      [RequestStatus.PENDING]: 0,\n      [RequestStatus.ACTIVE]: 0,\n      [RequestStatus.COMPLETED]: 0,\n      [RequestStatus.CANCELLED]: 0,\n      [RequestStatus.FAILED]: 0\n    };\n\n    this.requestMap.forEach((request) => {\n      requestsByStatus[request.status]++;\n    });\n\n    return {\n      currentRequestId: this.currentRequestId,\n      totalRequests: this.requestMap.size,\n      requestsByStatus\n    };\n  }\n}\n\n// 导出单例实例\nexport const playbackRequestManager = new PlaybackRequestManager();\n"
  },
  {
    "path": "src/renderer/services/preloadService.ts",
    "content": "import { Howl } from 'howler';\n\nimport type { SongResult } from '@/types/music';\n\nclass PreloadService {\n  private loadingPromises: Map<string | number, Promise<Howl>> = new Map();\n  private preloadedSounds: Map<string | number, Howl> = new Map();\n\n  /**\n   * 加载并验证音频\n   * 如果已经在加载中，返回现有的 Promise\n   * 如果已经加载完成，返回缓存的 Howl 实例\n   */\n  public async load(song: SongResult): Promise<Howl> {\n    if (!song || !song.id) {\n      throw new Error('无效的歌曲对象');\n    }\n\n    // 1. 检查是否有正在进行的加载\n    if (this.loadingPromises.has(song.id)) {\n      console.log(`[PreloadService] 歌曲 ${song.name} 正在加载中，复用现有请求`);\n      return this.loadingPromises.get(song.id)!;\n    }\n\n    // 2. 检查是否有已完成的缓存\n    if (this.preloadedSounds.has(song.id)) {\n      const sound = this.preloadedSounds.get(song.id)!;\n      if (sound.state() === 'loaded') {\n        console.log(`[PreloadService] 歌曲 ${song.name} 已预加载完成，直接使用`);\n        return sound;\n      } else {\n        // 如果缓存的音频状态不正常，清理并重新加载\n        this.preloadedSounds.delete(song.id);\n      }\n    }\n\n    // 3. 开始新的加载过程\n    const loadPromise = this._performLoad(song);\n    this.loadingPromises.set(song.id, loadPromise);\n\n    try {\n      const sound = await loadPromise;\n      this.preloadedSounds.set(song.id, sound);\n      return sound;\n    } finally {\n      this.loadingPromises.delete(song.id);\n    }\n  }\n\n  /**\n   * 执行实际的加载和验证逻辑\n   */\n  private async _performLoad(song: SongResult): Promise<Howl> {\n    console.log(`[PreloadService] 开始加载歌曲: ${song.name}`);\n\n    if (!song.playMusicUrl) {\n      throw new Error('歌曲没有 URL');\n    }\n\n    // 创建初始音频实例\n    const sound = await this._createSound(song.playMusicUrl);\n\n    // 检查时长\n    const duration = sound.duration();\n    const expectedDuration = (song.dt || 0) / 1000;\n\n    // 时长差异只记录警告，不自动触发重新解析\n    // 用户可以通过 ReparsePopover 手动选择正确的音源\n    if (\n      expectedDuration > 0 &&\n      Math.abs(duration - expectedDuration) > 5 &&\n      song.source !== 'bilibili'\n    ) {\n      console.warn(\n        `[PreloadService] 时长差异警告：实际 ${duration.toFixed(1)}s, 预期 ${expectedDuration.toFixed(1)}s (${song.name})`\n      );\n    }\n\n    return sound;\n  }\n\n  private _createSound(url: string): Promise<Howl> {\n    return new Promise((resolve, reject) => {\n      const sound = new Howl({\n        src: [url],\n        html5: true,\n        preload: true,\n        autoplay: false,\n        onload: () => resolve(sound),\n        onloaderror: (_, err) => reject(err)\n      });\n    });\n  }\n\n  /**\n   * 取消特定歌曲的预加载（如果可能）\n   * 注意：Promise 无法真正取消，但我们可以清理结果\n   */\n  public cancel(songId: string | number) {\n    if (this.preloadedSounds.has(songId)) {\n      const sound = this.preloadedSounds.get(songId)!;\n      sound.unload();\n      this.preloadedSounds.delete(songId);\n    }\n    // loadingPromises 中的任务会继续执行，但因为 preloadedSounds 中没有记录，\n    // 下次请求时会重新加载（或者我们可以让 _performLoad 检查一个取消标记，但这增加了复杂性）\n  }\n\n  /**\n   * 获取已预加载的音频实例（如果存在）\n   */\n  public getPreloadedSound(songId: string | number): Howl | undefined {\n    return this.preloadedSounds.get(songId);\n  }\n\n  /**\n   * 消耗（使用）已预加载的音频\n   * 从缓存中移除但不 unload（由调用方管理生命周期）\n   * @returns 预加载的 Howl 实例，如果没有则返回 undefined\n   */\n  public consume(songId: string | number): Howl | undefined {\n    const sound = this.preloadedSounds.get(songId);\n    if (sound) {\n      this.preloadedSounds.delete(songId);\n      console.log(`[PreloadService] 消耗预加载的歌曲: ${songId}`);\n      return sound;\n    }\n    return undefined;\n  }\n\n  /**\n   * 清理所有预加载资源\n   */\n  public clearAll() {\n    this.preloadedSounds.forEach((sound) => sound.unload());\n    this.preloadedSounds.clear();\n    this.loadingPromises.clear();\n  }\n}\n\nexport const preloadService = new PreloadService();\n"
  },
  {
    "path": "src/renderer/services/translation-engines/index.ts",
    "content": "// Re-export available translation engines from this folder.\n// Add more engines here as separate files and re-export them.\nexport * from './opencc';\n"
  },
  {
    "path": "src/renderer/services/translation-engines/opencc.ts",
    "content": "import type { ILyricText } from '@/types/music';\n\nlet _inited = false;\nlet _converter: any = null;\n\nexport async function init(): Promise<void> {\n  if (_inited) return;\n  const mod: any = await import('https://cdn.jsdelivr.net/npm/opencc-rust/dist/opencc-rust.mjs');\n  if (!mod?.initOpenccRust || !mod?.getConverter) {\n    throw new Error('opencc-rust module missing expected exports');\n  }\n  await mod.initOpenccRust();\n  _converter = mod.getConverter();\n  _inited = true;\n}\n\nexport async function convert(text: string): Promise<string> {\n  await init();\n  if (!_converter) return text;\n  return _converter.convert(text);\n}\n\nexport async function convertLines(lines: string[]) {\n  await init();\n  if (!_converter) return lines.slice();\n\n  const cjkRe = /[\\u4e00-\\u9fff]/;\n\n  const convertOne = async (s: string) => {\n    const src = (s || '').trim();\n    if (!src || !cjkRe.test(src)) return '';\n    try {\n      return await _converter.convert(src);\n    } catch (e) {\n      console.warn('opencc convertLines item failed:', e);\n      return '';\n    }\n  };\n\n  const results = await Promise.all(lines.map((s) => convertOne(s)));\n  return results;\n}\n\nexport async function translateLines(lines: ILyricText[]) {\n  if (!lines || lines.length === 0) return lines.slice();\n  const srcInfo = lines.map((l) => {\n    const hasTr = !!(l.trText && l.trText.trim() !== '');\n    return {\n      src: hasTr ? (l.trText || '').trim() : (l.text || '').trim(),\n      targetIsTr: hasTr\n    };\n  });\n\n  const srcLines = srcInfo.map((s) => s.src);\n  const converted = await convertLines(srcLines);\n\n  return lines.map((l, i) => {\n    const { targetIsTr } = srcInfo[i];\n    const conv = converted[i] || '';\n    if (targetIsTr) {\n      const tr = conv || l.trText || l.text || '';\n      return { ...l, trText: tr };\n    }\n    const txt = conv || l.text || '';\n    return { ...l, text: txt };\n  });\n}\n\nexport default {\n  init,\n  convert,\n  convertLines,\n  translateLines\n};\n\n// Ensure the engine is initialized and return public API.\nlet _ensurePromise: Promise<any> | null = null;\nexport async function ensureOpenccConverter() {\n  if (_ensurePromise) return _ensurePromise;\n  _ensurePromise = (async () => {\n    try {\n      await init();\n    } catch (e) {\n      console.warn('opencc ensureOpenccConverter init failed:', e);\n    }\n    return {\n      init,\n      convert,\n      convertLines,\n      translateLines\n    };\n  })();\n  return _ensurePromise;\n}\n"
  },
  {
    "path": "src/renderer/shims-vue.d.ts",
    "content": "declare module '*.vue' {\n  import { DefineComponent } from 'vue';\n\n  const component: DefineComponent<{}, {}, any>;\n  export default component;\n}\n"
  },
  {
    "path": "src/renderer/store/index.ts",
    "content": "import { createPinia } from 'pinia';\nimport piniaPluginPersistedstate from 'pinia-plugin-persistedstate';\nimport { markRaw } from 'vue';\n\nimport router from '@/router';\n\n// 创建 pinia 实例\nconst pinia = createPinia();\n\npinia.use(piniaPluginPersistedstate);\n\n// 添加路由到 Pinia\npinia.use(({ store }) => {\n  store.router = markRaw(router);\n});\n\n// 导出所有 store\nexport * from './modules/lyric';\nexport * from './modules/menu';\nexport * from './modules/music';\nexport * from './modules/player';\nexport * from './modules/recommend';\nexport * from './modules/search';\nexport * from './modules/settings';\nexport * from './modules/user';\n\nexport default pinia;\n"
  },
  {
    "path": "src/renderer/store/modules/favorite.ts",
    "content": "import { defineStore } from 'pinia';\nimport { ref } from 'vue';\n\nimport { getLikedList, likeSong } from '@/api/music';\nimport { hasPermission } from '@/utils/auth';\nimport { getLocalStorageItem, isBilibiliIdMatch, setLocalStorageItem } from '@/utils/playerUtils';\n\n/**\n * 收藏管理 Store\n * 负责：收藏列表、不喜欢列表的管理\n */\nexport const useFavoriteStore = defineStore('favorite', () => {\n  // ==================== 状态 ====================\n  const favoriteList = ref<Array<number | string>>(getLocalStorageItem('favoriteList', []));\n  const dislikeList = ref<Array<number | string>>(getLocalStorageItem('dislikeList', []));\n\n  // ==================== Actions ====================\n\n  /**\n   * 添加到收藏列表\n   */\n  const addToFavorite = async (id: number | string) => {\n    // 检查是否已存在\n    const isAlreadyInList = favoriteList.value.some((existingId) =>\n      typeof id === 'string' && id.includes('--')\n        ? isBilibiliIdMatch(existingId, id)\n        : existingId === id\n    );\n\n    if (!isAlreadyInList) {\n      favoriteList.value.push(id);\n      setLocalStorageItem('favoriteList', favoriteList.value);\n\n      // 只有在有真实登录权限时才调用API\n      if (typeof id === 'number') {\n        const { useUserStore } = await import('./user');\n        const userStore = useUserStore();\n\n        if (userStore.user && hasPermission(true)) {\n          try {\n            await likeSong(id, true);\n          } catch (error) {\n            console.error('收藏歌曲API调用失败:', error);\n          }\n        }\n      }\n    }\n  };\n\n  /**\n   * 从收藏列表移除\n   */\n  const removeFromFavorite = async (id: number | string) => {\n    // 对于B站视频，需要根据bvid和cid来匹配\n    if (typeof id === 'string' && id.includes('--')) {\n      favoriteList.value = favoriteList.value.filter(\n        (existingId) => !isBilibiliIdMatch(existingId, id)\n      );\n    } else {\n      favoriteList.value = favoriteList.value.filter((existingId) => existingId !== id);\n\n      // 只有在有真实登录权限时才调用API\n      if (typeof id === 'number') {\n        const { useUserStore } = await import('./user');\n        const userStore = useUserStore();\n\n        if (userStore.user && hasPermission(true)) {\n          try {\n            await likeSong(id, false);\n          } catch (error) {\n            console.error('取消收藏歌曲API调用失败:', error);\n          }\n        }\n      }\n    }\n    setLocalStorageItem('favoriteList', favoriteList.value);\n  };\n\n  /**\n   * 添加到不喜欢列表\n   */\n  const addToDislikeList = (id: number | string) => {\n    if (!dislikeList.value.includes(id)) {\n      dislikeList.value.push(id);\n      setLocalStorageItem('dislikeList', dislikeList.value);\n    }\n  };\n\n  /**\n   * 从不喜欢列表移除\n   */\n  const removeFromDislikeList = (id: number | string) => {\n    dislikeList.value = dislikeList.value.filter((existingId) => existingId !== id);\n    setLocalStorageItem('dislikeList', dislikeList.value);\n  };\n\n  /**\n   * 初始化收藏列表（从服务器同步）\n   */\n  const initializeFavoriteList = async () => {\n    const { useUserStore } = await import('./user');\n    const userStore = useUserStore();\n    const localFavoriteList = localStorage.getItem('favoriteList');\n    const localList: number[] = localFavoriteList ? JSON.parse(localFavoriteList) : [];\n\n    if (userStore.user && userStore.user.userId) {\n      try {\n        const res = await getLikedList(userStore.user.userId);\n        if (res.data?.ids) {\n          const serverList = res.data.ids.reverse();\n          const mergedList = Array.from(new Set([...localList, ...serverList]));\n          favoriteList.value = mergedList;\n        } else {\n          favoriteList.value = localList;\n        }\n      } catch (error) {\n        console.error('获取服务器收藏列表失败，使用本地数据:', error);\n        favoriteList.value = localList;\n      }\n    } else {\n      favoriteList.value = localList;\n    }\n\n    setLocalStorageItem('favoriteList', favoriteList.value);\n  };\n\n  /**\n   * 检查歌曲是否已收藏\n   */\n  const isFavorite = (id: number | string): boolean => {\n    return favoriteList.value.some((existingId) =>\n      typeof id === 'string' && id.includes('--')\n        ? isBilibiliIdMatch(existingId, id)\n        : existingId === id\n    );\n  };\n\n  /**\n   * 检查歌曲是否在不喜欢列表中\n   */\n  const isDisliked = (id: number | string): boolean => {\n    return dislikeList.value.includes(id);\n  };\n\n  return {\n    // 状态\n    favoriteList,\n    dislikeList,\n\n    // Actions\n    addToFavorite,\n    removeFromFavorite,\n    addToDislikeList,\n    removeFromDislikeList,\n    initializeFavoriteList,\n    isFavorite,\n    isDisliked\n  };\n});\n"
  },
  {
    "path": "src/renderer/store/modules/intelligenceMode.ts",
    "content": "import { createDiscreteApi } from 'naive-ui';\nimport { defineStore } from 'pinia';\nimport { ref } from 'vue';\n\nimport i18n from '@/../i18n/renderer';\nimport { getLikedList } from '@/api/music';\nimport type { Platform } from '@/types/music';\nimport { getLocalStorageItem, setLocalStorageItem } from '@/utils/playerUtils';\n\nconst { message } = createDiscreteApi(['message']);\n\n/**\n * 心动模式管理 Store\n * 负责：心动模式的播放和状态管理\n */\nexport const useIntelligenceModeStore = defineStore('intelligenceMode', () => {\n  // ==================== 状态 ====================\n  const isIntelligenceMode = ref(getLocalStorageItem('isIntelligenceMode', false));\n  const intelligenceModeInfo = ref<{\n    playlistId: number;\n    seedSongId: number;\n  } | null>(getLocalStorageItem('intelligenceModeInfo', null));\n\n  // ==================== Actions ====================\n\n  /**\n   * 播放心动模式\n   */\n  const playIntelligenceMode = async () => {\n    const { useUserStore } = await import('./user');\n    const { usePlayerCoreStore } = await import('./playerCore');\n    const { usePlaylistStore } = await import('./playlist');\n\n    const userStore = useUserStore();\n    const playerCore = usePlayerCoreStore();\n    const playlistStore = usePlaylistStore();\n    const { t } = i18n.global;\n\n    // 检查是否使用cookie登录\n    if (!userStore.user || userStore.loginType !== 'cookie') {\n      message.warning(t('player.playBar.intelligenceMode.needCookieLogin'));\n      return;\n    }\n\n    try {\n      // 获取用户歌单列表\n      if (userStore.playList.length === 0) {\n        await userStore.initializePlaylist();\n      }\n\n      // 找到\"我喜欢的音乐\"歌单\n      const favoritePlaylist = userStore.playList.find(\n        (pl: any) => pl.userId === userStore.user?.userId && pl.specialType === 5\n      );\n\n      if (!favoritePlaylist) {\n        message.warning(t('player.playBar.intelligenceMode.noFavoritePlaylist'));\n        return;\n      }\n\n      // 获取喜欢的歌曲列表\n      const likedListRes = await getLikedList(userStore.user.userId);\n      const likedIds = likedListRes.data?.ids || [];\n\n      if (likedIds.length === 0) {\n        message.warning(t('player.playBar.intelligenceMode.noLikedSongs'));\n        return;\n      }\n\n      // 随机选择一首歌曲\n      const randomSongId = likedIds[Math.floor(Math.random() * likedIds.length)];\n\n      // 调用心动模式API\n      const { getIntelligenceList } = await import('@/api/music');\n      const res = await getIntelligenceList({\n        id: randomSongId,\n        pid: favoritePlaylist.id\n      });\n\n      if (res.data?.data && res.data.data.length > 0) {\n        const intelligenceSongs = res.data.data.map((item: any) => ({\n          id: item.id,\n          name: item.songInfo.name,\n          picUrl: item.songInfo.al?.picUrl,\n          source: 'netease' as Platform,\n          song: item.songInfo,\n          ...item.songInfo,\n          playLoading: false\n        }));\n\n        // 设置心动模式状态\n        isIntelligenceMode.value = true;\n        intelligenceModeInfo.value = {\n          playlistId: favoritePlaylist.id,\n          seedSongId: randomSongId\n        };\n        playlistStore.playMode = 3; // 设置播放模式为心动模式\n\n        setLocalStorageItem('isIntelligenceMode', true);\n        setLocalStorageItem('intelligenceModeInfo', intelligenceModeInfo.value);\n        setLocalStorageItem('playMode', playlistStore.playMode);\n\n        // 替换播放列表并开始播放\n        playlistStore.setPlayList(intelligenceSongs, false, true);\n        await playerCore.handlePlayMusic(intelligenceSongs[0], true);\n      } else {\n        message.error(t('player.playBar.intelligenceMode.failed'));\n      }\n    } catch (error) {\n      console.error('心动模式播放失败:', error);\n      message.error(t('player.playBar.intelligenceMode.error'));\n    }\n  };\n\n  /**\n   * 清除心动模式状态\n   */\n  const clearIntelligenceMode = () => {\n    isIntelligenceMode.value = false;\n    intelligenceModeInfo.value = null;\n    setLocalStorageItem('isIntelligenceMode', false);\n    localStorage.removeItem('intelligenceModeInfo');\n  };\n\n  return {\n    // 状态\n    isIntelligenceMode,\n    intelligenceModeInfo,\n\n    // Actions\n    playIntelligenceMode,\n    clearIntelligenceMode\n  };\n});\n"
  },
  {
    "path": "src/renderer/store/modules/lyric.ts",
    "content": "import { defineStore } from 'pinia';\nimport { ref } from 'vue';\n\nexport const useLyricStore = defineStore('lyric', () => {\n  const lyric = ref({});\n\n  const setLyric = (newLyric: any) => {\n    lyric.value = newLyric;\n  };\n\n  return {\n    lyric,\n    setLyric\n  };\n});\n"
  },
  {
    "path": "src/renderer/store/modules/menu.ts",
    "content": "import { defineStore } from 'pinia';\nimport { ref } from 'vue';\n\nimport homeRouter from '@/router/home';\n\nexport const useMenuStore = defineStore('menu', () => {\n  const menus = ref(homeRouter);\n\n  const setMenus = (newMenus: any[]) => {\n    menus.value = newMenus;\n  };\n\n  return {\n    menus,\n    setMenus\n  };\n});\n"
  },
  {
    "path": "src/renderer/store/modules/music.ts",
    "content": "import { defineStore } from 'pinia';\n\ninterface MusicState {\n  currentMusicList: any[] | null;\n  currentMusicListName: string;\n  currentListInfo: any | null;\n  canRemoveSong: boolean;\n}\n\nexport const useMusicStore = defineStore('music', {\n  state: (): MusicState => ({\n    currentMusicList: null,\n    currentMusicListName: '',\n    currentListInfo: null,\n    canRemoveSong: false\n  }),\n\n  actions: {\n    // 设置当前音乐列表\n    setCurrentMusicList(list: any[], name: string, listInfo: any = null, canRemove = false) {\n      this.currentMusicList = list;\n      this.currentMusicListName = name;\n      this.currentListInfo = listInfo;\n      this.canRemoveSong = canRemove;\n    },\n\n    // 清除当前音乐列表\n    clearCurrentMusicList() {\n      this.currentMusicList = null;\n      this.currentMusicListName = '';\n      this.currentListInfo = null;\n      this.canRemoveSong = false;\n    },\n\n    // 从列表中移除一首歌曲\n    removeSongFromList(id: number) {\n      if (!this.currentMusicList) return;\n\n      const index = this.currentMusicList.findIndex((song) => song.id === id);\n      if (index !== -1) {\n        this.currentMusicList.splice(index, 1);\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "src/renderer/store/modules/player.ts",
    "content": "/**\n * - usePlayerCoreStore: 核心播放控制（播放/暂停、音量、速度）\n * - usePlaylistStore: 播放列表管理（列表、索引、模式、上下一首）\n * - useFavoriteStore: 收藏管理（收藏列表、不喜欢列表）\n * - useSleepTimerStore: 定时关闭（时间/歌曲数/列表结束）\n * - useIntelligenceModeStore: 心动模式\n */\n\nimport { defineStore, storeToRefs } from 'pinia';\nimport { computed } from 'vue';\n\n// 导入所有拆分的子 stores\nimport { useFavoriteStore } from './favorite';\nimport { useIntelligenceModeStore } from './intelligenceMode';\nimport { usePlayerCoreStore } from './playerCore';\nimport { usePlaylistStore } from './playlist';\nimport { type SleepTimerInfo, SleepTimerType, useSleepTimerStore } from './sleepTimer';\n\nexport { type SleepTimerInfo, SleepTimerType };\nexport { getSongUrl, loadLrc, useLyrics, useSongDetail, useSongUrl } from '@/hooks/usePlayerHooks';\nexport { isBilibiliIdMatch } from '@/utils/playerUtils';\n\n/**\n * 聚合 Player Store\n */\nexport const usePlayerStore = defineStore('player', () => {\n  // 获取所有子 stores\n  const playerCore = usePlayerCoreStore();\n  const playlist = usePlaylistStore();\n  const favorite = useFavoriteStore();\n  const sleepTimer = useSleepTimerStore();\n  const intelligenceMode = useIntelligenceModeStore();\n\n  // 使用 storeToRefs 获取响应式引用\n  const { play, isPlay, playMusic, playMusicUrl, musicFull, playbackRate, volume, userPlayIntent } =\n    storeToRefs(playerCore);\n\n  const { playList, playListIndex, playMode, originalPlayList, playListDrawerVisible } =\n    storeToRefs(playlist);\n\n  const { favoriteList, dislikeList } = storeToRefs(favorite);\n\n  const { sleepTimer: sleepTimerState, showSleepTimer } = storeToRefs(sleepTimer);\n\n  const { isIntelligenceMode, intelligenceModeInfo } = storeToRefs(intelligenceMode);\n\n  // ==================== Computed ====================\n  const currentSong = computed(() => playerCore.currentSong);\n  const isPlaying = computed(() => playerCore.isPlaying);\n  const currentPlayList = computed(() => playlist.currentPlayList);\n  const currentPlayListIndex = computed(() => playlist.currentPlayListIndex);\n\n  // 定时器相关 computed\n  const currentSleepTimer = computed(() => sleepTimer.currentSleepTimer);\n  const hasSleepTimerActive = computed(() => sleepTimer.hasSleepTimerActive);\n  const sleepTimerRemainingTime = computed(() => sleepTimer.sleepTimerRemainingTime);\n  const sleepTimerRemainingSongs = computed(() => sleepTimer.sleepTimerRemainingSongs);\n\n  // ==================== 初始化方法 ====================\n  /**\n   * 初始化播放状态（从 localStorage 恢复）\n   */\n  const initializePlayState = async () => {\n    await playerCore.initializePlayState();\n    await playlist.initializePlaylist();\n  };\n\n  /**\n   * 初始化收藏列表（从服务器同步）\n   */\n  const initializeFavoriteList = async () => {\n    await favorite.initializeFavoriteList();\n  };\n\n  // ==================== 返回所有状态和方法 ====================\n  return {\n    // ========== 核心播放控制 (PlayerCore) ==========\n    play,\n    isPlay,\n    playMusic,\n    playMusicUrl,\n    musicFull,\n    playbackRate,\n    volume,\n    userPlayIntent,\n\n    // PlayerCore - Computed\n    currentSong,\n    isPlaying,\n\n    // PlayerCore - Actions\n    setIsPlay: playerCore.setIsPlay,\n    setMusicFull: playerCore.setMusicFull,\n    setPlayMusic: playerCore.setPlayMusic,\n    setPlaybackRate: playerCore.setPlaybackRate,\n    setVolume: playerCore.setVolume,\n    getVolume: playerCore.getVolume,\n    increaseVolume: playerCore.increaseVolume,\n    decreaseVolume: playerCore.decreaseVolume,\n    handlePlayMusic: playerCore.handlePlayMusic,\n    playAudio: playerCore.playAudio,\n    handlePause: playerCore.handlePause,\n    checkPlaybackState: playerCore.checkPlaybackState,\n    reparseCurrentSong: playerCore.reparseCurrentSong,\n\n    // ========== 播放列表管理 (Playlist) ==========\n    playList,\n    playListIndex,\n    playMode,\n    originalPlayList,\n    playListDrawerVisible,\n\n    // Playlist - Computed\n    currentPlayList,\n    currentPlayListIndex,\n\n    // Playlist - Actions\n    setPlayList: playlist.setPlayList,\n    addToNextPlay: playlist.addToNextPlay,\n    removeFromPlayList: playlist.removeFromPlayList,\n    clearPlayAll: playlist.clearPlayAll,\n    togglePlayMode: playlist.togglePlayMode,\n    shufflePlayList: playlist.shufflePlayList,\n    restoreOriginalOrder: playlist.restoreOriginalOrder,\n    preloadNextSongs: playlist.preloadNextSongs,\n    nextPlay: playlist.nextPlay,\n    prevPlay: playlist.prevPlay,\n    setPlayListDrawerVisible: playlist.setPlayListDrawerVisible,\n    setPlay: playlist.setPlay,\n\n    // ========== 收藏管理 (Favorite) ==========\n    favoriteList,\n    dislikeList,\n\n    // Favorite - Actions\n    addToFavorite: favorite.addToFavorite,\n    removeFromFavorite: favorite.removeFromFavorite,\n    addToDislikeList: favorite.addToDislikeList,\n    removeFromDislikeList: favorite.removeFromDislikeList,\n\n    // ========== 定时关闭 (SleepTimer) ==========\n    sleepTimer: sleepTimerState,\n    showSleepTimer,\n\n    // SleepTimer - Computed\n    currentSleepTimer,\n    hasSleepTimerActive,\n    sleepTimerRemainingTime,\n    sleepTimerRemainingSongs,\n\n    // SleepTimer - Actions\n    setSleepTimerByTime: sleepTimer.setSleepTimerByTime,\n    setSleepTimerBySongs: sleepTimer.setSleepTimerBySongs,\n    setSleepTimerAtPlaylistEnd: sleepTimer.setSleepTimerAtPlaylistEnd,\n    clearSleepTimer: sleepTimer.clearSleepTimer,\n\n    // ========== 心动模式 (IntelligenceMode) ==========\n    isIntelligenceMode,\n    intelligenceModeInfo,\n\n    // IntelligenceMode - Actions\n    playIntelligenceMode: intelligenceMode.playIntelligenceMode,\n\n    // ========== 初始化方法 ==========\n    initializePlayState,\n    initializeFavoriteList\n  };\n});\n"
  },
  {
    "path": "src/renderer/store/modules/playerCore.ts",
    "content": "import { cloneDeep } from 'lodash';\nimport { createDiscreteApi } from 'naive-ui';\nimport { defineStore } from 'pinia';\nimport { computed, ref } from 'vue';\n\nimport i18n from '@/../i18n/renderer';\nimport { getBilibiliAudioUrl } from '@/api/bilibili';\nimport { getParsingMusicUrl } from '@/api/music';\nimport { useMusicHistory } from '@/hooks/MusicHistoryHook';\nimport { useLyrics, useSongDetail } from '@/hooks/usePlayerHooks';\nimport { audioService } from '@/services/audioService';\nimport { playbackRequestManager } from '@/services/playbackRequestManager';\nimport { preloadService } from '@/services/preloadService';\nimport { SongSourceConfigManager } from '@/services/SongSourceConfigManager';\nimport type { Platform, SongResult } from '@/types/music';\nimport { getImgUrl } from '@/utils';\nimport { getImageLinearBackground } from '@/utils/linearColor';\n\nconst musicHistory = useMusicHistory();\nconst { message } = createDiscreteApi(['message']);\n\n/**\n * 核心播放控制 Store\n * 负责：播放/暂停、当前歌曲、音频URL、音量、播放速度、全屏状态\n */\nexport const usePlayerCoreStore = defineStore(\n  'playerCore',\n  () => {\n    // ==================== 状态 ====================\n    const play = ref(false);\n    const isPlay = ref(false);\n    const playMusic = ref<SongResult>({} as SongResult);\n    const playMusicUrl = ref('');\n    const musicFull = ref(false);\n    const playbackRate = ref(1.0);\n    const volume = ref(1);\n    const userPlayIntent = ref(false); // 用户是否想要播放\n\n    let checkPlayTime: NodeJS.Timeout | null = null;\n\n    // ==================== Computed ====================\n    const currentSong = computed(() => playMusic.value);\n    const isPlaying = computed(() => isPlay.value);\n\n    // ==================== Actions ====================\n\n    /**\n     * 设置播放状态\n     */\n    const setIsPlay = (value: boolean) => {\n      isPlay.value = value;\n      play.value = value;\n      window.electron?.ipcRenderer.send('update-play-state', value);\n    };\n\n    /**\n     * 设置全屏状态\n     */\n    const setMusicFull = (value: boolean) => {\n      musicFull.value = value;\n    };\n\n    /**\n     * 设置播放速度\n     */\n    const setPlaybackRate = (rate: number) => {\n      playbackRate.value = rate;\n      audioService.setPlaybackRate(rate);\n    };\n\n    /**\n     * 设置音量\n     */\n    const setVolume = (newVolume: number) => {\n      const normalizedVolume = Math.max(0, Math.min(1, newVolume));\n      volume.value = normalizedVolume;\n      audioService.setVolume(normalizedVolume);\n    };\n\n    /**\n     * 获取音量\n     */\n    const getVolume = () => volume.value;\n\n    /**\n     * 增加音量\n     */\n    const increaseVolume = (step: number = 0.1) => {\n      const newVolume = Math.min(1, volume.value + step);\n      setVolume(newVolume);\n      return newVolume;\n    };\n\n    /**\n     * 减少音量\n     */\n    const decreaseVolume = (step: number = 0.1) => {\n      const newVolume = Math.max(0, volume.value - step);\n      setVolume(newVolume);\n      return newVolume;\n    };\n\n    /**\n     * 播放状态检测\n     */\n    const checkPlaybackState = (song: SongResult, requestId?: string, timeout: number = 4000) => {\n      if (checkPlayTime) {\n        clearTimeout(checkPlayTime);\n      }\n      const sound = audioService.getCurrentSound();\n      if (!sound) return;\n\n      // 如果没有提供 requestId，创建一个临时标识\n      const actualRequestId = requestId || `check_${Date.now()}`;\n\n      const onPlayHandler = () => {\n        console.log(`[${actualRequestId}] 播放事件触发，歌曲成功开始播放`);\n        audioService.off('play', onPlayHandler);\n        audioService.off('playerror', onPlayErrorHandler);\n      };\n\n      const onPlayErrorHandler = async () => {\n        console.log('播放错误事件触发，检查是否需要重新获取URL');\n        audioService.off('play', onPlayHandler);\n        audioService.off('playerror', onPlayErrorHandler);\n\n        // 如果有 requestId，验证其有效性\n        if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n          console.log('请求已过期，跳过重试');\n          return;\n        }\n\n        if (userPlayIntent.value && play.value) {\n          console.log('播放失败，尝试刷新URL并重新播放');\n          playMusic.value.playMusicUrl = undefined;\n          const refreshedSong = { ...song, isFirstPlay: true };\n          await handlePlayMusic(refreshedSong, true);\n        }\n      };\n\n      audioService.on('play', onPlayHandler);\n      audioService.on('playerror', onPlayErrorHandler);\n\n      checkPlayTime = setTimeout(() => {\n        // 如果有 requestId，验证其有效性\n        if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n          console.log('请求已过期，跳过超时重试');\n          audioService.off('play', onPlayHandler);\n          audioService.off('playerror', onPlayErrorHandler);\n          return;\n        }\n\n        if (!audioService.isActuallyPlaying() && userPlayIntent.value && play.value) {\n          console.log(`${timeout}ms后歌曲未真正播放且用户仍希望播放，尝试重新获取URL`);\n          audioService.off('play', onPlayHandler);\n          audioService.off('playerror', onPlayErrorHandler);\n\n          playMusic.value.playMusicUrl = undefined;\n          (async () => {\n            const refreshedSong = { ...song, isFirstPlay: true };\n            await handlePlayMusic(refreshedSong, true);\n          })();\n        }\n      }, timeout);\n    };\n\n    /**\n     * 核心播放处理函数\n     */\n    const handlePlayMusic = async (music: SongResult, isPlay: boolean = true) => {\n      // 如果是新歌曲，重置已尝试的音源（使用 SongSourceConfigManager 按歌曲隔离）\n      if (music.id !== playMusic.value.id) {\n        SongSourceConfigManager.clearTriedSources(music.id);\n      }\n\n      // 创建新的播放请求并取消之前的所有请求\n      const requestId = playbackRequestManager.createRequest(music);\n      console.log(`[handlePlayMusic] 开始处理歌曲: ${music.name}, 请求ID: ${requestId}`);\n\n      const currentSound = audioService.getCurrentSound();\n      if (currentSound) {\n        console.log('主动停止并卸载当前音频实例');\n        currentSound.stop();\n        currentSound.unload();\n      }\n\n      // 验证请求是否仍然有效\n      if (!playbackRequestManager.isRequestValid(requestId)) {\n        console.log(`[handlePlayMusic] 请求已失效: ${requestId}`);\n        return false;\n      }\n\n      // 激活请求\n      if (!playbackRequestManager.activateRequest(requestId)) {\n        console.log(`[handlePlayMusic] 无法激活请求: ${requestId}`);\n        return false;\n      }\n\n      const originalMusic = { ...music };\n      const { loadLrc } = useLyrics();\n      const { getSongDetail } = useSongDetail();\n\n      // 并行加载歌词和背景色\n      const [lyrics, { backgroundColor, primaryColor }] = await Promise.all([\n        (async () => {\n          if (music.lyric && music.lyric.lrcTimeArray.length > 0) {\n            return music.lyric;\n          }\n          return await loadLrc(music.id);\n        })(),\n        (async () => {\n          if (music.backgroundColor && music.primaryColor) {\n            return { backgroundColor: music.backgroundColor, primaryColor: music.primaryColor };\n          }\n          return await getImageLinearBackground(getImgUrl(music?.picUrl, '30y30'));\n        })()\n      ]);\n\n      // 在更新状态前再次验证请求\n      if (!playbackRequestManager.isRequestValid(requestId)) {\n        console.log(`[handlePlayMusic] 加载歌词/背景色后请求已失效: ${requestId}`);\n        return false;\n      }\n\n      // 设置歌词和背景色\n      music.lyric = lyrics;\n      music.backgroundColor = backgroundColor;\n      music.primaryColor = primaryColor;\n      music.playLoading = true;\n\n      // 更新 playMusic\n      playMusic.value = music;\n      play.value = isPlay;\n\n      // 更新标题\n      let title = music.name;\n      if (music.source === 'netease' && music?.song?.artists) {\n        title += ` - ${music.song.artists.reduce(\n          (prev: string, curr: any) => `${prev}${curr.name}/`,\n          ''\n        )}`;\n      } else if (music.source === 'bilibili' && music?.song?.ar?.[0]) {\n        title += ` - ${music.song.ar[0].name}`;\n      }\n      document.title = 'AlgerMusic - ' + title;\n\n      try {\n        // 添加到历史记录\n        musicHistory.addMusic(music);\n\n        // 获取歌曲详情\n        const updatedPlayMusic = await getSongDetail(originalMusic, requestId);\n\n        // 在获取详情后再次验证请求\n        if (!playbackRequestManager.isRequestValid(requestId)) {\n          console.log(`[handlePlayMusic] 获取歌曲详情后请求已失效: ${requestId}`);\n          playbackRequestManager.failRequest(requestId);\n          return false;\n        }\n\n        updatedPlayMusic.lyric = lyrics;\n\n        playMusic.value = updatedPlayMusic;\n        playMusicUrl.value = updatedPlayMusic.playMusicUrl as string;\n        music.playMusicUrl = updatedPlayMusic.playMusicUrl as string;\n\n        // 在拆分后补充：触发预加载下一首/下下首（与 playlist store 保持一致）\n        try {\n          const { usePlaylistStore } = await import('./playlist');\n          const playlistStore = usePlaylistStore();\n          // 基于当前歌曲在播放列表中的位置来预加载\n          const list = playlistStore.playList;\n          if (Array.isArray(list) && list.length > 0) {\n            const idx = list.findIndex(\n              (item: SongResult) =>\n                item.id === updatedPlayMusic.id && item.source === updatedPlayMusic.source\n            );\n            if (idx !== -1) {\n              setTimeout(() => {\n                playlistStore.preloadNextSongs(idx);\n              }, 3000);\n            }\n          }\n        } catch (e) {\n          console.warn('预加载触发失败（可能是依赖未加载或循环依赖），已忽略:', e);\n        }\n\n        let playInProgress = false;\n\n        try {\n          if (playInProgress) {\n            console.warn('播放操作正在进行中，避免重复调用');\n            return true;\n          }\n\n          playInProgress = true;\n          const result = await playAudio(requestId);\n          playInProgress = false;\n\n          if (result) {\n            playbackRequestManager.completeRequest(requestId);\n            return true;\n          } else {\n            playbackRequestManager.failRequest(requestId);\n            return false;\n          }\n        } catch (error) {\n          console.error('自动播放音频失败:', error);\n          playInProgress = false;\n          playbackRequestManager.failRequest(requestId);\n          return false;\n        }\n      } catch (error) {\n        console.error('处理播放音乐失败:', error);\n        message.error(i18n.global.t('player.playFailed'));\n        if (playMusic.value) {\n          playMusic.value.playLoading = false;\n        }\n        playbackRequestManager.failRequest(requestId);\n\n        return false;\n      }\n    };\n\n    /**\n     * 播放音频\n     */\n    const playAudio = async (requestId?: string) => {\n      if (!playMusicUrl.value || !playMusic.value) return null;\n\n      // 如果提供了 requestId，验证请求是否仍然有效\n      if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n        console.log(`[playAudio] 请求已失效: ${requestId}`);\n        return null;\n      }\n\n      try {\n        const shouldPlay = play.value;\n        console.log('播放音频，当前播放状态:', shouldPlay ? '播放' : '暂停');\n\n        // 检查保存的进度\n        let initialPosition = 0;\n        const savedProgress = JSON.parse(localStorage.getItem('playProgress') || '{}');\n        console.log(\n          '[playAudio] 读取保存的进度:',\n          savedProgress,\n          '当前歌曲ID:',\n          playMusic.value.id\n        );\n        if (savedProgress.songId === playMusic.value.id) {\n          initialPosition = savedProgress.progress;\n          console.log('[playAudio] 恢复播放进度:', initialPosition);\n        }\n\n        // B站视频URL检查\n        if (\n          playMusic.value.source === 'bilibili' &&\n          (!playMusicUrl.value || playMusicUrl.value === 'undefined')\n        ) {\n          console.log('B站视频URL无效，尝试重新获取');\n\n          if (playMusic.value.bilibiliData) {\n            try {\n              const proxyUrl = await getBilibiliAudioUrl(\n                playMusic.value.bilibiliData.bvid,\n                playMusic.value.bilibiliData.cid\n              );\n\n              // 再次验证请求\n              if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n                console.log(`[playAudio] 获取B站URL后请求已失效: ${requestId}`);\n                return null;\n              }\n\n              (playMusic.value as any).playMusicUrl = proxyUrl;\n              playMusicUrl.value = proxyUrl;\n            } catch (error) {\n              console.error('获取B站音频URL失败:', error);\n              message.error(i18n.global.t('player.playFailed'));\n              return null;\n            }\n          }\n        }\n\n        // 使用 PreloadService 获取音频\n        // 优先使用已预加载的 sound（通过 consume 获取并从缓存中移除）\n        // 如果没有预加载，则进行加载\n        let sound: Howl;\n        try {\n          // 先尝试消耗预加载的 sound\n          const preloadedSound = preloadService.consume(playMusic.value.id);\n          if (preloadedSound && preloadedSound.state() === 'loaded') {\n            console.log(`[playAudio] 使用预加载的音频: ${playMusic.value.name}`);\n            sound = preloadedSound;\n          } else {\n            // 没有预加载或预加载状态不正常，需要加载\n            console.log(`[playAudio] 没有预加载，开始加载: ${playMusic.value.name}`);\n            sound = await preloadService.load(playMusic.value);\n          }\n        } catch (error) {\n          console.error('PreloadService 加载失败:', error);\n          // 如果 PreloadService 失败，尝试直接播放作为回退\n          // 但通常 PreloadService 失败意味着 URL 问题\n          throw error;\n        }\n\n        // 播放新音频，传入已加载的 sound 实例\n        const newSound = await audioService.play(\n          playMusicUrl.value,\n          playMusic.value,\n          shouldPlay,\n          initialPosition || 0,\n          sound\n        );\n\n        // 播放后再次验证请求\n        if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n          console.log(`[playAudio] 播放后请求已失效: ${requestId}`);\n          newSound.stop();\n          newSound.unload();\n          return null;\n        }\n\n        // 添加播放状态检测\n        if (shouldPlay && requestId) {\n          checkPlaybackState(playMusic.value, requestId);\n        }\n\n        // 发布音频就绪事件\n        window.dispatchEvent(\n          new CustomEvent('audio-ready', { detail: { sound: newSound, shouldPlay } })\n        );\n\n        // 时长检查已在 preloadService.ts 中完成\n\n        return newSound;\n      } catch (error) {\n        console.error('播放音频失败:', error);\n        setPlayMusic(false);\n\n        const errorMsg = error instanceof Error ? error.message : String(error);\n\n        // 操作锁错误处理\n        if (errorMsg.includes('操作锁激活')) {\n          console.log('由于操作锁正在使用，将在1000ms后重试');\n\n          try {\n            audioService.forceResetOperationLock();\n            console.log('已强制重置操作锁');\n          } catch (e) {\n            console.error('重置操作锁失败:', e);\n          }\n\n          setTimeout(() => {\n            // 验证请求是否仍然有效再重试\n            if (requestId && !playbackRequestManager.isRequestValid(requestId)) {\n              console.log('重试时请求已失效，跳过重试');\n              return;\n            }\n            if (userPlayIntent.value && play.value) {\n              playAudio(requestId).catch((e) => {\n                console.error('重试播放失败:', e);\n              });\n            }\n          }, 1000);\n        } else {\n          console.warn('播放音频失败（非操作锁错误），由调用方处理重试');\n        }\n\n        message.error(i18n.global.t('player.playFailed'));\n        return null;\n      }\n    };\n\n    /**\n     * 暂停播放\n     */\n    const handlePause = async () => {\n      try {\n        const currentSound = audioService.getCurrentSound();\n        if (currentSound) {\n          currentSound.pause();\n        }\n        setPlayMusic(false);\n        userPlayIntent.value = false;\n      } catch (error) {\n        console.error('暂停播放失败:', error);\n      }\n    };\n\n    /**\n     * 设置播放/暂停\n     */\n    const setPlayMusic = async (value: boolean | SongResult) => {\n      if (typeof value === 'boolean') {\n        setIsPlay(value);\n        userPlayIntent.value = value;\n      } else {\n        await handlePlayMusic(value);\n        play.value = true;\n        isPlay.value = true;\n        userPlayIntent.value = true;\n      }\n    };\n\n    /**\n     * 使用指定音源重新解析当前歌曲\n     */\n    const reparseCurrentSong = async (sourcePlatform: Platform, isAuto: boolean = false) => {\n      try {\n        const currentSong = playMusic.value;\n        if (!currentSong || !currentSong.id) {\n          console.warn('没有有效的播放对象');\n          return false;\n        }\n\n        if (currentSong.source === 'bilibili') {\n          console.warn('B站视频不支持重新解析');\n          return false;\n        }\n\n        // 使用 SongSourceConfigManager 保存配置\n        SongSourceConfigManager.setConfig(\n          currentSong.id,\n          [sourcePlatform],\n          isAuto ? 'auto' : 'manual'\n        );\n\n        const currentSound = audioService.getCurrentSound();\n        if (currentSound) {\n          currentSound.pause();\n        }\n\n        const numericId =\n          typeof currentSong.id === 'string' ? parseInt(currentSong.id, 10) : currentSong.id;\n\n        console.log(`使用音源 ${sourcePlatform} 重新解析歌曲 ${numericId}`);\n\n        const songData = cloneDeep(currentSong);\n        const res = await getParsingMusicUrl(numericId, songData);\n\n        if (res && res.data && res.data.data && res.data.data.url) {\n          const newUrl = res.data.data.url;\n          console.log(`解析成功，获取新URL: ${newUrl.substring(0, 50)}...`);\n\n          const updatedMusic = {\n            ...currentSong,\n            playMusicUrl: newUrl,\n            expiredAt: Date.now() + 1800000\n          };\n\n          await handlePlayMusic(updatedMusic, true);\n\n          // 更新播放列表中的歌曲信息\n          const { usePlaylistStore } = await import('./playlist');\n          const playlistStore = usePlaylistStore();\n          playlistStore.updateSong(updatedMusic);\n\n          return true;\n        } else {\n          console.warn(`使用音源 ${sourcePlatform} 解析失败`);\n          return false;\n        }\n      } catch (error) {\n        console.error('重新解析失败:', error);\n        return false;\n      }\n    };\n\n    /**\n     * 初始化播放状态\n     */\n    const initializePlayState = async () => {\n      const { useSettingsStore } = await import('./settings');\n      const settingStore = useSettingsStore();\n\n      if (playMusic.value && Object.keys(playMusic.value).length > 0) {\n        try {\n          console.log('恢复上次播放的音乐:', playMusic.value.name);\n          const isPlaying = settingStore.setData.autoPlay;\n\n          if (playMusic.value.source === 'bilibili' && playMusic.value.bilibiliData) {\n            console.log('恢复B站视频播放', playMusic.value.bilibiliData);\n            playMusic.value.playMusicUrl = undefined;\n          }\n\n          await handlePlayMusic(\n            { ...playMusic.value, isFirstPlay: true, playMusicUrl: undefined },\n            isPlaying\n          );\n        } catch (error) {\n          console.error('重新获取音乐链接失败:', error);\n          play.value = false;\n          isPlay.value = false;\n          playMusic.value = {} as SongResult;\n          playMusicUrl.value = '';\n        }\n      }\n\n      setTimeout(() => {\n        audioService.setPlaybackRate(playbackRate.value);\n      }, 2000);\n    };\n\n    return {\n      // 状态\n      play,\n      isPlay,\n      playMusic,\n      playMusicUrl,\n      musicFull,\n      playbackRate,\n      volume,\n      userPlayIntent,\n\n      // Computed\n      currentSong,\n      isPlaying,\n\n      // Actions\n      setIsPlay,\n      setMusicFull,\n      setPlayMusic,\n      setPlaybackRate,\n      setVolume,\n      getVolume,\n      increaseVolume,\n      decreaseVolume,\n      handlePlayMusic,\n      playAudio,\n      handlePause,\n      checkPlaybackState,\n      reparseCurrentSong,\n      initializePlayState\n    };\n  },\n  {\n    persist: {\n      key: 'player-core-store',\n      storage: localStorage,\n      pick: ['playMusic', 'playMusicUrl', 'playbackRate', 'volume', 'isPlay']\n    }\n  }\n);\n"
  },
  {
    "path": "src/renderer/store/modules/playlist.ts",
    "content": "import { useThrottleFn } from '@vueuse/core';\nimport { createDiscreteApi } from 'naive-ui';\nimport { defineStore, storeToRefs } from 'pinia';\nimport { computed, ref, shallowRef } from 'vue';\n\nimport i18n from '@/../i18n/renderer';\nimport { useSongDetail } from '@/hooks/usePlayerHooks';\nimport { preloadService } from '@/services/preloadService';\nimport type { SongResult } from '@/types/music';\nimport { getImgUrl } from '@/utils';\nimport { performShuffle, preloadCoverImage } from '@/utils/playerUtils';\n\nimport { useIntelligenceModeStore } from './intelligenceMode';\nimport { usePlayerCoreStore } from './playerCore';\nimport { useSleepTimerStore } from './sleepTimer';\n\nconst { message } = createDiscreteApi(['message']);\n\n/**\n * 播放列表管理 Store\n * 负责：播放列表、索引、播放模式、预加载、上/下一首\n */\nexport const usePlaylistStore = defineStore(\n  'playlist',\n  () => {\n    // ==================== 状态 ====================\n    // 状态将由 pinia-plugin-persistedstate 自动从 localStorage 恢复\n    const playList = shallowRef<SongResult[]>([]);\n    const playListIndex = ref(0);\n    const playMode = ref(0);\n    const originalPlayList = shallowRef<SongResult[]>([]);\n    const playListDrawerVisible = ref(false);\n\n    // 连续失败计数器（用于防止无限循环）\n    const consecutiveFailCount = ref(0);\n    const MAX_CONSECUTIVE_FAILS = 5; // 最大连续失败次数\n    const SINGLE_TRACK_MAX_RETRIES = 3; // 单曲最大重试次数\n\n    // ==================== Computed ====================\n    const currentPlayList = computed(() => playList.value);\n    const currentPlayListIndex = computed(() => playListIndex.value);\n\n    // ==================== Actions ====================\n\n    /**\n     * 获取歌曲详情并预加载\n     */\n    const fetchSongs = async (startIndex: number, endIndex: number) => {\n      try {\n        const songs = playList.value.slice(\n          Math.max(0, startIndex),\n          Math.min(endIndex, playList.value.length)\n        );\n        const { getSongDetail } = useSongDetail();\n\n        const detailedSongs = await Promise.all(\n          songs.map(async (song: SongResult) => {\n            try {\n              if (!song.playMusicUrl || (song.source === 'netease' && !song.backgroundColor)) {\n                return await getSongDetail(song);\n              }\n              return song;\n            } catch (error) {\n              console.error('获取歌曲详情失败:', error);\n              return song;\n            }\n          })\n        );\n\n        const nextSong = detailedSongs[0];\n        if (nextSong && !(nextSong.lyric && nextSong.lyric.lrcTimeArray.length > 0)) {\n          try {\n            const { useLyrics } = await import('@/hooks/usePlayerHooks');\n            const { loadLrc } = useLyrics();\n            nextSong.lyric = await loadLrc(nextSong.id);\n          } catch (error) {\n            console.error('加载歌词失败:', error);\n          }\n        }\n\n        detailedSongs.forEach((song, index) => {\n          if (song && startIndex + index < playList.value.length) {\n            playList.value[startIndex + index] = song;\n          }\n        });\n\n        // 预加载下一首歌曲的音频和封面\n        if (nextSong) {\n          if (nextSong.playMusicUrl) {\n            preloadService.load(nextSong);\n          }\n          if (nextSong.picUrl) {\n            preloadCoverImage(nextSong.picUrl, getImgUrl);\n          }\n        }\n      } catch (error) {\n        console.error('获取歌曲列表失败:', error);\n      }\n    };\n\n    /**\n     * 智能预加载下一首歌曲\n     */\n    const preloadNextSongs = (currentIndex: number) => {\n      if (playList.value.length <= 1) return;\n\n      let nextIndex: number;\n\n      if (playMode.value === 0) {\n        // 顺序播放模式\n        if (currentIndex >= playList.value.length - 1) {\n          return;\n        }\n        nextIndex = currentIndex + 1;\n      } else {\n        // 循环播放和随机播放模式\n        nextIndex = (currentIndex + 1) % playList.value.length;\n      }\n\n      const endIndex = Math.min(nextIndex + 2, playList.value.length);\n\n      if (nextIndex < playList.value.length) {\n        fetchSongs(nextIndex, endIndex);\n\n        // 循环模式且接近列表末尾，预加载列表开头\n        if (\n          (playMode.value === 1 || playMode.value === 2) &&\n          nextIndex + 1 >= playList.value.length &&\n          playList.value.length > 2\n        ) {\n          setTimeout(() => {\n            fetchSongs(0, 1);\n          }, 1000);\n        }\n      }\n    };\n\n    /**\n     * 应用随机播放\n     */\n    const shufflePlayList = () => {\n      console.log('[PlaylistStore] shufflePlayList called');\n      if (playList.value.length === 0) return;\n\n      // 保存原始列表\n      if (originalPlayList.value.length === 0) {\n        console.log('[PlaylistStore] Saving original list, length:', playList.value.length);\n        originalPlayList.value = [...playList.value];\n      }\n\n      const currentSong = playList.value[playListIndex.value];\n      console.log('[PlaylistStore] Current song before shuffle:', currentSong?.name);\n\n      // 执行洗牌\n      const shuffled = performShuffle([...playList.value], currentSong);\n      // 确保触发 shallowRef 的响应式\n      playList.value = [...shuffled];\n      playListIndex.value = 0;\n\n      console.log('[PlaylistStore] List shuffled, new length:', playList.value.length);\n      console.log('[PlaylistStore] New first song:', playList.value[0]?.name);\n    };\n\n    /**\n     * 恢复原始播放列表顺序\n     */\n    const restoreOriginalOrder = () => {\n      console.log('[PlaylistStore] restoreOriginalOrder called');\n      if (originalPlayList.value.length === 0) return;\n\n      const currentSong = playList.value[playListIndex.value];\n      console.log('[PlaylistStore] Current song before restore:', currentSong?.name);\n\n      playList.value = [...originalPlayList.value];\n      originalPlayList.value = [];\n\n      // 找到当前歌曲在原始列表中的索引\n      if (currentSong) {\n        const index = playList.value.findIndex((s) => s.id === currentSong.id);\n        if (index !== -1) {\n          playListIndex.value = index;\n        }\n      }\n      console.log('[PlaylistStore] Original order restored, new index:', playListIndex.value);\n    };\n\n    /**\n     * 设置播放列表\n     */\n    const setPlayList = (\n      list: SongResult[],\n      keepIndex: boolean = false,\n      fromIntelligenceMode: boolean = false\n    ) => {\n      // 如果不是从心动模式调用，清除心动模式状态\n      if (!fromIntelligenceMode) {\n        const intelligenceStore = useIntelligenceModeStore();\n        if (intelligenceStore.isIntelligenceMode) {\n          intelligenceStore.clearIntelligenceMode();\n        }\n      }\n\n      if (list.length === 0) {\n        playList.value = [];\n        playListIndex.value = 0;\n        originalPlayList.value = [];\n        return;\n      }\n\n      const playerCore = usePlayerCoreStore();\n      const { playMusic } = storeToRefs(playerCore);\n\n      // 根据当前播放模式处理新的播放列表\n      if (playMode.value === 2) {\n        // 随机模式\n        console.log('随机模式下设置新播放列表，保存原始顺序并洗牌');\n\n        originalPlayList.value = [...list];\n\n        const currentSong = playMusic.value;\n        const shuffledList = performShuffle(list, currentSong);\n\n        if (currentSong && currentSong.id) {\n          const currentSongIndex = shuffledList.findIndex((song) => song.id === currentSong.id);\n          playListIndex.value =\n            currentSongIndex !== -1 ? 0 : keepIndex ? Math.max(0, playListIndex.value) : 0;\n        } else {\n          playListIndex.value = keepIndex ? Math.max(0, playListIndex.value) : 0;\n        }\n\n        playList.value = shuffledList;\n      } else {\n        console.log('顺序/循环模式下设置新播放列表');\n        if (originalPlayList.value.length > 0) {\n          originalPlayList.value = [];\n        }\n\n        if (!keepIndex) {\n          const foundIndex = list.findIndex((item) => item.id === playMusic.value.id);\n          playListIndex.value = foundIndex !== -1 ? foundIndex : 0;\n        }\n\n        playList.value = list;\n      }\n      // pinia-plugin-persistedstate 会自动保存状态\n    };\n\n    /**\n     * 添加到下一首播放\n     */\n    const addToNextPlay = (song: SongResult) => {\n      const list = [...playList.value];\n      const currentIndex = playListIndex.value;\n\n      // 如果歌曲已在播放列表中，先移除\n      const existingIndex = list.findIndex((item) => item.id === song.id);\n      if (existingIndex !== -1) {\n        list.splice(existingIndex, 1);\n        if (existingIndex <= currentIndex) {\n          playListIndex.value = Math.max(0, playListIndex.value - 1);\n        }\n      }\n\n      // 插入到当前播放歌曲的下一个位置\n      const insertIndex = playListIndex.value + 1;\n      list.splice(insertIndex, 0, song);\n\n      setPlayList(list, true);\n    };\n\n    /**\n     * 从播放列表移除歌曲\n     */\n    const removeFromPlayList = (id: number | string) => {\n      const index = playList.value.findIndex((item) => item.id === id);\n      if (index === -1) return;\n\n      const playerCore = usePlayerCoreStore();\n      const { playMusic } = storeToRefs(playerCore);\n\n      // 如果删除的是当前播放的歌曲，先切换到下一首\n      if (id === playMusic.value.id) {\n        nextPlay();\n      }\n\n      const newPlayList = [...playList.value];\n      newPlayList.splice(index, 1);\n      setPlayList(newPlayList);\n    };\n\n    /**\n     * 清空播放列表\n     */\n    const clearPlayAll = async () => {\n      const { audioService } = await import('@/services/audioService');\n      const playerCore = usePlayerCoreStore();\n\n      audioService.pause();\n      setTimeout(() => {\n        playerCore.playMusic = {} as SongResult;\n        playerCore.playMusicUrl = '';\n        playList.value = [];\n        playListIndex.value = 0;\n        originalPlayList.value = [];\n        // 只清除 playerCore 的 localStorage（这些由 playerCore store 管理）\n        localStorage.removeItem('currentPlayMusic');\n        localStorage.removeItem('currentPlayMusicUrl');\n        // playlist 状态由 pinia-plugin-persistedstate 自动管理\n      }, 500);\n    };\n\n    /**\n     * 切换播放模式\n     */\n    const togglePlayMode = async () => {\n      const { useUserStore } = await import('./user');\n      const userStore = useUserStore();\n      const wasRandom = playMode.value === 2;\n      const wasIntelligence = playMode.value === 3;\n\n      let newMode = (playMode.value + 1) % 4;\n\n      // 如果要切换到心动模式，但用户未使用cookie登录，则跳过\n      if (newMode === 3 && (!userStore.user || userStore.loginType !== 'cookie')) {\n        console.log('跳过心动模式：需要cookie登录');\n        newMode = 0;\n      }\n\n      const isRandom = newMode === 2;\n      const isIntelligence = newMode === 3;\n\n      console.log(`[PlaylistStore] togglePlayMode: ${playMode.value} -> ${newMode}`);\n      playMode.value = newMode;\n\n      // 切换到随机模式时洗牌\n      if (isRandom && !wasRandom && playList.value.length > 0) {\n        shufflePlayList();\n        console.log('切换到随机模式，洗牌播放列表');\n      }\n\n      // 从随机模式切换出去时恢复原始顺序\n      if (!isRandom && wasRandom) {\n        restoreOriginalOrder();\n        console.log('切换出随机模式，恢复原始顺序');\n      }\n\n      // 切换到心动模式\n      if (isIntelligence && !wasIntelligence) {\n        console.log('切换到心动模式');\n        const intelligenceStore = useIntelligenceModeStore();\n        await intelligenceStore.playIntelligenceMode();\n      }\n\n      // 从心动模式切换出去\n      if (!isIntelligence && wasIntelligence) {\n        console.log('退出心动模式');\n        const intelligenceStore = useIntelligenceModeStore();\n        intelligenceStore.clearIntelligenceMode();\n      }\n    };\n\n    /**\n     * 下一首\n     * @param singleTrackRetryCount 单曲重试次数（同一首歌的重试）\n     */\n    const _nextPlay = async (singleTrackRetryCount: number = 0) => {\n      try {\n        if (playList.value.length === 0) {\n          return;\n        }\n\n        const playerCore = usePlayerCoreStore();\n        const sleepTimerStore = useSleepTimerStore();\n\n        // 检查是否超过最大连续失败次数\n        if (consecutiveFailCount.value >= MAX_CONSECUTIVE_FAILS) {\n          console.error(`[nextPlay] 连续${MAX_CONSECUTIVE_FAILS}首歌曲播放失败，停止播放`);\n          message.warning(i18n.global.t('player.consecutiveFailsError'));\n          consecutiveFailCount.value = 0; // 重置计数器\n          playerCore.setIsPlay(false);\n          return;\n        }\n\n        // 检查是否是播放列表的最后一首且设置了播放列表结束定时\n        if (\n          playMode.value === 0 &&\n          playListIndex.value === playList.value.length - 1 &&\n          sleepTimerStore.sleepTimer.type === 'end'\n        ) {\n          sleepTimerStore.stopPlayback();\n          return;\n        }\n\n        const currentIndex = playListIndex.value;\n        const nowPlayListIndex = (playListIndex.value + 1) % playList.value.length;\n        const nextSong = { ...playList.value[nowPlayListIndex] };\n\n        console.log(\n          `[nextPlay] 尝试播放: ${nextSong.name}, 索引: ${currentIndex} -> ${nowPlayListIndex}, 单曲重试: ${singleTrackRetryCount}/${SINGLE_TRACK_MAX_RETRIES}, 连续失败: ${consecutiveFailCount.value}/${MAX_CONSECUTIVE_FAILS}`\n        );\n        console.log(\n          '[nextPlay] Current mode:',\n          playMode.value,\n          'Playlist length:',\n          playList.value.length\n        );\n\n        // 先尝试播放歌曲\n        const success = await playerCore.handlePlayMusic(nextSong, true);\n\n        if (success) {\n          // 播放成功，重置所有计数器并更新索引\n          consecutiveFailCount.value = 0;\n          playListIndex.value = nowPlayListIndex;\n          console.log(`[nextPlay] 播放成功，索引已更新为: ${nowPlayListIndex}`);\n          console.log(\n            '[nextPlay] New current song in list:',\n            playList.value[playListIndex.value]?.name\n          );\n          sleepTimerStore.handleSongChange();\n        } else {\n          console.error(`[nextPlay] 播放失败: ${nextSong.name}`);\n\n          // 单曲重试逻辑\n          if (singleTrackRetryCount < SINGLE_TRACK_MAX_RETRIES) {\n            console.log(\n              `[nextPlay] 单曲重试 ${singleTrackRetryCount + 1}/${SINGLE_TRACK_MAX_RETRIES}`\n            );\n            // 不更新索引，重试同一首歌\n            setTimeout(() => {\n              _nextPlay(singleTrackRetryCount + 1);\n            }, 1000);\n          } else {\n            // 单曲重试次数用尽，递增连续失败计数，尝试下一首\n            consecutiveFailCount.value++;\n            console.log(\n              `[nextPlay] 单曲重试用尽，连续失败计数: ${consecutiveFailCount.value}/${MAX_CONSECUTIVE_FAILS}`\n            );\n\n            if (playList.value.length > 1) {\n              // 更新索引到失败的歌曲位置，这样下次递归调用会继续往下\n              playListIndex.value = nowPlayListIndex;\n              message.warning(i18n.global.t('player.parseFailedPlayNext'));\n\n              // 延迟后尝试下一首（重置单曲重试计数）\n              setTimeout(() => {\n                _nextPlay(0);\n              }, 500);\n            } else {\n              // 只有一首歌且失败\n              message.error(i18n.global.t('player.playFailed'));\n              playerCore.setIsPlay(false);\n            }\n          }\n        }\n      } catch (error) {\n        console.error('切换下一首出错:', error);\n      }\n    };\n\n    const nextPlay = useThrottleFn(_nextPlay, 500);\n\n    /**\n     * 上一首\n     */\n    const _prevPlay = async () => {\n      try {\n        if (playList.value.length === 0) {\n          return;\n        }\n\n        const playerCore = usePlayerCoreStore();\n        const currentIndex = playListIndex.value;\n        const nowPlayListIndex =\n          (playListIndex.value - 1 + playList.value.length) % playList.value.length;\n\n        const prevSong = { ...playList.value[nowPlayListIndex] };\n\n        console.log(\n          `[prevPlay] 尝试播放上一首: ${prevSong.name}, 索引: ${currentIndex} -> ${nowPlayListIndex}`\n        );\n\n        let success = false;\n        let retryCount = 0;\n        const maxRetries = 2;\n\n        // 先尝试播放歌曲，成功后再更新索引\n        while (!success && retryCount < maxRetries) {\n          success = await playerCore.handlePlayMusic(prevSong);\n\n          if (!success) {\n            retryCount++;\n            console.error(`播放上一首失败，尝试 ${retryCount}/${maxRetries}`);\n\n            if (retryCount >= maxRetries) {\n              console.error('多次尝试播放失败，将从播放列表中移除此歌曲');\n              const newPlayList = [...playList.value];\n              newPlayList.splice(nowPlayListIndex, 1);\n\n              if (newPlayList.length > 0) {\n                const keepCurrentIndexPosition = true;\n                setPlayList(newPlayList, keepCurrentIndexPosition);\n\n                if (newPlayList.length === 1) {\n                  playListIndex.value = 0;\n                } else {\n                  const newPrevIndex =\n                    (playListIndex.value - 1 + newPlayList.length) % newPlayList.length;\n                  playListIndex.value = newPrevIndex;\n                }\n\n                setTimeout(() => {\n                  prevPlay();\n                }, 300);\n                return;\n              } else {\n                console.error('播放列表为空，停止尝试');\n                break;\n              }\n            }\n          }\n        }\n\n        if (success) {\n          // 播放成功，更新索引\n          playListIndex.value = nowPlayListIndex;\n          console.log(`[prevPlay] 播放成功，索引已更新为: ${nowPlayListIndex}`);\n        } else {\n          console.error(`[prevPlay] 播放上一首失败，保持当前索引: ${currentIndex}`);\n          playerCore.setIsPlay(false);\n          message.error(i18n.global.t('player.playFailed'));\n        }\n      } catch (error) {\n        console.error('切换上一首出错:', error);\n      }\n    };\n\n    const prevPlay = useThrottleFn(_prevPlay, 500);\n\n    /**\n     * 设置播放列表抽屉显示状态\n     */\n    const setPlayListDrawerVisible = (value: boolean) => {\n      playListDrawerVisible.value = value;\n    };\n\n    /**\n     * 设置播放（兼容旧API）\n     */\n    const setPlay = async (song: SongResult) => {\n      try {\n        const playerCore = usePlayerCoreStore();\n\n        // 检查URL是否已过期\n        if (song.expiredAt && song.expiredAt < Date.now()) {\n          console.info(`歌曲URL已过期，重新获取: ${song.name}`);\n          song.playMusicUrl = undefined;\n          song.expiredAt = undefined;\n        }\n\n        // 如果是当前正在播放的音乐，则切换播放/暂停状态\n        if (\n          playerCore.playMusic.id === song.id &&\n          playerCore.playMusic.playMusicUrl === song.playMusicUrl &&\n          !song.isFirstPlay\n        ) {\n          if (playerCore.play) {\n            playerCore.setPlayMusic(false);\n            const { audioService } = await import('@/services/audioService');\n            audioService.getCurrentSound()?.pause();\n            playerCore.userPlayIntent = false;\n          } else {\n            playerCore.setPlayMusic(true);\n            playerCore.userPlayIntent = true;\n            const { audioService } = await import('@/services/audioService');\n            const sound = audioService.getCurrentSound();\n            if (sound) {\n              sound.play();\n              // 在恢复播放时也进行状态检测，防止URL已过期导致无声\n              playerCore.checkPlaybackState(playerCore.playMusic);\n            }\n          }\n          return;\n        }\n\n        if (song.isFirstPlay) {\n          song.isFirstPlay = false;\n        }\n\n        // 查找歌曲在播放列表中的索引\n        const songIndex = playList.value.findIndex(\n          (item: SongResult) => item.id === song.id && item.source === song.source\n        );\n\n        // 更新播放索引\n        if (songIndex !== -1 && songIndex !== playListIndex.value) {\n          console.log('歌曲索引不匹配，更新为:', songIndex);\n          playListIndex.value = songIndex;\n        }\n\n        const success = await playerCore.handlePlayMusic(song);\n\n        // playerCore 的状态由其自己的 store 管理\n\n        if (success) {\n          playerCore.isPlay = true;\n\n          // 预加载下一首歌曲\n          if (songIndex !== -1) {\n            setTimeout(() => {\n              preloadNextSongs(playListIndex.value);\n            }, 3000);\n          }\n        }\n        return success;\n      } catch (error) {\n        console.error('设置播放失败:', error);\n        return false;\n      }\n    };\n\n    /**\n     * 初始化播放列表\n     * 注意：状态已由 pinia-plugin-persistedstate 自动恢复\n     * 这里只需要处理特殊逻辑（如随机模式的恢复）\n     */\n    const initializePlaylist = async () => {\n      // 重启后恢复随机播放状态\n      if (playMode.value === 2 && playList.value.length > 0) {\n        if (originalPlayList.value.length === 0) {\n          console.log('重启后恢复随机播放模式，重新洗牌播放列表');\n          shufflePlayList();\n        } else {\n          console.log('重启后恢复随机播放模式，播放列表已是洗牌状态');\n        }\n      }\n    };\n\n    return {\n      // 状态\n      playList,\n      playListIndex,\n      playMode,\n      originalPlayList,\n      playListDrawerVisible,\n\n      // Computed\n      currentPlayList,\n      currentPlayListIndex,\n\n      // Actions\n      setPlayList,\n      addToNextPlay,\n      removeFromPlayList,\n      clearPlayAll,\n      togglePlayMode,\n      shufflePlayList,\n      restoreOriginalOrder,\n      preloadNextSongs,\n      nextPlay: nextPlay as unknown as typeof _nextPlay,\n      prevPlay: prevPlay as unknown as typeof _prevPlay,\n      setPlayListDrawerVisible,\n      setPlay,\n      initializePlaylist,\n      fetchSongs,\n      updateSong: (song: SongResult) => {\n        const index = playList.value.findIndex(\n          (item) => item.id === song.id && item.source === song.source\n        );\n        if (index !== -1) {\n          playList.value[index] = song;\n          // 触发响应式更新\n          playList.value = [...playList.value];\n        }\n      }\n    };\n  },\n  {\n    // 配置 pinia-plugin-persistedstate\n    persist: {\n      key: 'playlist-store',\n      storage: localStorage,\n      // 持久化所有状态，除了 playListDrawerVisible（UI 状态不需要持久化）\n      pick: ['playList', 'playListIndex', 'playMode', 'originalPlayList']\n    }\n  }\n);\n"
  },
  {
    "path": "src/renderer/store/modules/recommend.ts",
    "content": "import { defineStore } from 'pinia';\nimport { ref } from 'vue';\n\nimport { getDayRecommend } from '@/api/home';\nimport type { IDayRecommend } from '@/types/day_recommend';\nimport type { SongResult } from '@/types/music';\n\nexport const useRecommendStore = defineStore('recommend', () => {\n  const dailyRecommendSongs = ref<SongResult[]>([]);\n\n  const fetchDailyRecommendSongs = async () => {\n    try {\n      const { data } = await getDayRecommend();\n      const recommendData = data.data as unknown as IDayRecommend;\n\n      if (recommendData && Array.isArray(recommendData.dailySongs)) {\n        dailyRecommendSongs.value = recommendData.dailySongs as any;\n        console.log(`[Recommend Store] 已加载 ${recommendData.dailySongs.length} 首每日推荐歌曲。`);\n      } else {\n        dailyRecommendSongs.value = [];\n      }\n    } catch (error) {\n      console.error('[Recommend Store] 获取每日推荐失败:', error);\n      dailyRecommendSongs.value = [];\n    }\n  };\n\n  const replaceSongInDailyRecommend = (oldSongId: number | string, newSong: SongResult) => {\n    const index = dailyRecommendSongs.value.findIndex((song) => song.id === oldSongId);\n    if (index !== -1) {\n      dailyRecommendSongs.value.splice(index, 1, newSong as any);\n      console.log(`[Recommend Store] 已将歌曲 ${oldSongId} 替换为 ${newSong.name}`);\n    } else {\n      console.warn(`[Recommend Store] 未在日推列表中找到要替换的歌曲ID: ${oldSongId}`);\n    }\n  };\n\n  return {\n    dailyRecommendSongs,\n    fetchDailyRecommendSongs,\n    replaceSongInDailyRecommend\n  };\n});\n"
  },
  {
    "path": "src/renderer/store/modules/search.ts",
    "content": "import { defineStore } from 'pinia';\nimport { ref } from 'vue';\n\nexport const useSearchStore = defineStore('search', () => {\n  const searchValue = ref('');\n  const searchType = ref(1);\n\n  const setSearchValue = (value: string) => {\n    searchValue.value = value;\n  };\n\n  const setSearchType = (type: number) => {\n    searchType.value = type;\n  };\n\n  return {\n    searchValue,\n    searchType,\n    setSearchValue,\n    setSearchType\n  };\n});\n"
  },
  {
    "path": "src/renderer/store/modules/settings.ts",
    "content": "import { cloneDeep, isArray, mergeWith } from 'lodash';\nimport { defineStore } from 'pinia';\nimport { ref, watch } from 'vue';\n\nimport setDataDefault from '@/../main/set.json';\nimport homeRouter from '@/router/home';\nimport { useMenuStore } from '@/store/modules/menu';\nimport { isElectron } from '@/utils';\nimport {\n  applyTheme,\n  getCurrentTheme,\n  getSystemTheme,\n  ThemeType,\n  watchSystemTheme\n} from '@/utils/theme';\n\nexport const useSettingsStore = defineStore('settings', () => {\n  const theme = ref<ThemeType>(getCurrentTheme());\n  const isMobile = ref(false);\n  const isMiniMode = ref(false);\n  const showUpdateModal = ref(false);\n  const showArtistDrawer = ref(false);\n  const currentArtistId = ref<number | null>(null);\n  const systemFonts = ref<{ label: string; value: string }[]>([\n    { label: '系统默认', value: 'system-ui' }\n  ]);\n  const showDownloadDrawer = ref(false);\n\n  // 系统主题监听器清理函数\n  let systemThemeCleanup: (() => void) | null = null;\n\n  // 先声明 setData ref 但不初始化\n  const setData = ref<any>({});\n\n  // 先定义 setSetData 函数\n  const setSetData = (data: any) => {\n    // 合并现有设置和新设置\n    const mergedData = {\n      ...setData.value,\n      ...data\n    };\n\n    if (isElectron) {\n      window.electron.ipcRenderer.send('set-store-value', 'set', cloneDeep(mergedData));\n    } else {\n      localStorage.setItem('appSettings', JSON.stringify(cloneDeep(mergedData)));\n    }\n    setData.value = cloneDeep(mergedData);\n  };\n\n  // 初始化时先从存储中读取设置\n  const getInitialSettings = () => {\n    // 从存储中获取保存的设置\n    const savedSettings = isElectron\n      ? window.electron.ipcRenderer.sendSync('get-store-value', 'set')\n      : JSON.parse(localStorage.getItem('appSettings') || '{}');\n\n    // 自定义合并策略：如果是数组，直接使用源数组（覆盖默认值）\n    const customizer = (_objValue: any, srcValue: any) => {\n      if (isArray(srcValue)) {\n        return srcValue;\n      }\n      return undefined;\n    };\n\n    // 合并默认设置和保存的设置\n    const mergedSettings = mergeWith({}, setDataDefault, savedSettings, customizer);\n\n    // 更新设置并返回\n    setSetData(mergedSettings);\n    return mergedSettings;\n  };\n\n  // 初始化 setData\n  setData.value = getInitialSettings();\n\n  /**\n   * 保存导入的自定义API插件\n   * @param plugin 包含name和content的对象\n   */\n  const setCustomApiPlugin = (plugin: { name: string; content: string }) => {\n    setSetData({\n      customApiPlugin: plugin.content,\n      customApiPluginName: plugin.name\n    });\n  };\n\n  const toggleTheme = () => {\n    if (setData.value.autoTheme) {\n      // 如果是自动模式，切换到手动模式并设置相反的主题\n      const newTheme = theme.value === 'dark' ? 'light' : 'dark';\n      setSetData({\n        autoTheme: false,\n        manualTheme: newTheme\n      });\n      theme.value = newTheme;\n      applyTheme(newTheme);\n      // 停止监听系统主题\n      if (systemThemeCleanup) {\n        systemThemeCleanup();\n        systemThemeCleanup = null;\n      }\n    } else {\n      // 手动模式下正常切换\n      const newTheme = theme.value === 'dark' ? 'light' : 'dark';\n      theme.value = newTheme;\n      setSetData({ manualTheme: newTheme });\n      applyTheme(newTheme);\n    }\n  };\n\n  const setAutoTheme = (auto: boolean) => {\n    setSetData({ autoTheme: auto });\n\n    if (auto) {\n      // 启用自动模式\n      const systemTheme = getSystemTheme();\n      theme.value = systemTheme;\n      applyTheme(systemTheme);\n\n      // 开始监听系统主题变化\n      systemThemeCleanup = watchSystemTheme((newTheme) => {\n        if (setData.value.autoTheme) {\n          theme.value = newTheme;\n          applyTheme(newTheme);\n        }\n      });\n    } else {\n      // 切换到手动模式\n      const manualTheme = setData.value.manualTheme || 'light';\n      theme.value = manualTheme;\n      applyTheme(manualTheme);\n\n      // 停止监听系统主题\n      if (systemThemeCleanup) {\n        systemThemeCleanup();\n        systemThemeCleanup = null;\n      }\n    }\n  };\n\n  const setMiniMode = (value: boolean) => {\n    isMiniMode.value = value;\n  };\n\n  const setShowUpdateModal = (value: boolean) => {\n    showUpdateModal.value = value;\n  };\n\n  const setShowArtistDrawer = (show: boolean) => {\n    showArtistDrawer.value = show;\n    if (!show) {\n      currentArtistId.value = null;\n    }\n  };\n\n  const setCurrentArtistId = (id: number) => {\n    currentArtistId.value = id;\n  };\n\n  const setSystemFonts = (fonts: string[]) => {\n    systemFonts.value = [\n      { label: '系统默认', value: 'system-ui' },\n      ...fonts.map((font) => ({\n        label: font,\n        value: font\n      }))\n    ];\n  };\n\n  const setShowDownloadDrawer = (show: boolean) => {\n    showDownloadDrawer.value = show;\n  };\n\n  const setLanguage = (language: string) => {\n    setSetData({ language });\n    if (isElectron) {\n      window.electron.ipcRenderer.send('change-language', language);\n    }\n  };\n\n  const initializeSettings = () => {\n    // const savedSettings = getInitialSettings();\n    // setData.value = savedSettings;\n  };\n\n  const initializeTheme = () => {\n    // 根据设置初始化主题\n    if (setData.value.autoTheme) {\n      setAutoTheme(true);\n    } else {\n      const manualTheme = setData.value.manualTheme || getCurrentTheme();\n      theme.value = manualTheme;\n      applyTheme(manualTheme);\n    }\n  };\n\n  const initializeSystemFonts = async () => {\n    if (!isElectron) return;\n    if (systemFonts.value.length > 1) return;\n\n    try {\n      const fonts = await window.api.invoke('get-system-fonts');\n      setSystemFonts(fonts);\n    } catch (error) {\n      console.error('获取系统字体失败:', error);\n    }\n  };\n\n  // 计算移动端状态的函数\n  const calculateMobileStatus = () => {\n    const userAgentFlag = navigator.userAgent.match(\n      /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i\n    );\n    const isMobileWidth = window.innerWidth < 500;\n    const isMobileDevice = !!userAgentFlag || isMobileWidth;\n    const tabletMode = setData.value?.tabletMode;\n\n    return isMobileDevice && !tabletMode;\n  };\n\n  // 更新移动端状态和DOM类\n  const updateMobileStatus = () => {\n    const menuStore = useMenuStore();\n    const shouldUseMobileStyle = calculateMobileStatus();\n\n    // 更新store状态\n    if (shouldUseMobileStyle) {\n      menuStore.setMenus(homeRouter.filter((item) => item.meta.isMobile));\n    } else {\n      menuStore.setMenus(homeRouter);\n    }\n\n    // 更新DOM类\n    if (shouldUseMobileStyle) {\n      document.documentElement.classList.add('mobile');\n      document.documentElement.classList.remove('pc');\n    } else {\n      document.documentElement.classList.add('pc');\n      document.documentElement.classList.remove('mobile');\n    }\n\n    isMobile.value = shouldUseMobileStyle;\n  };\n\n  // 监听平板模式变化\n  watch(\n    () => setData.value?.tabletMode,\n    () => {\n      updateMobileStatus();\n    },\n    { immediate: true }\n  );\n\n  // 监听窗口大小变化\n  if (typeof window !== 'undefined') {\n    window.addEventListener('resize', updateMobileStatus);\n  }\n\n  return {\n    setData,\n    theme,\n    isMobile,\n    isMiniMode,\n    showUpdateModal,\n    showArtistDrawer,\n    currentArtistId,\n    systemFonts,\n    showDownloadDrawer,\n    setSetData,\n    toggleTheme,\n    setAutoTheme,\n    setMiniMode,\n    setShowUpdateModal,\n    setShowArtistDrawer,\n    setCurrentArtistId,\n    setSystemFonts,\n    setShowDownloadDrawer,\n    setLanguage,\n    initializeSettings,\n    initializeTheme,\n    initializeSystemFonts,\n    setCustomApiPlugin\n  };\n});\n"
  },
  {
    "path": "src/renderer/store/modules/sleepTimer.ts",
    "content": "import { defineStore } from 'pinia';\nimport { computed, ref } from 'vue';\n\nimport i18n from '@/../i18n/renderer';\nimport { getLocalStorageItem, setLocalStorageItem } from '@/utils/playerUtils';\n\n// 定时关闭类型\nexport enum SleepTimerType {\n  NONE = 'none',\n  TIME = 'time',\n  SONGS = 'songs',\n  PLAYLIST_END = 'end'\n}\n\n// 定时关闭信息\nexport interface SleepTimerInfo {\n  type: SleepTimerType;\n  value: number;\n  endTime?: number;\n  startSongIndex?: number;\n  remainingSongs?: number;\n}\n\n/**\n * 定时关闭管理 Store\n * 负责：定时关闭功能\n */\nexport const useSleepTimerStore = defineStore('sleepTimer', () => {\n  // ==================== 状态 ====================\n  const sleepTimer = ref<SleepTimerInfo>(\n    getLocalStorageItem('sleepTimer', {\n      type: SleepTimerType.NONE,\n      value: 0\n    })\n  );\n  const showSleepTimer = ref(false);\n  const timerInterval = ref<number | null>(null);\n\n  // ==================== Computed ====================\n  const currentSleepTimer = computed(() => sleepTimer.value);\n  const hasSleepTimerActive = computed(() => sleepTimer.value.type !== SleepTimerType.NONE);\n\n  const sleepTimerRemainingTime = computed(() => {\n    if (sleepTimer.value.type === SleepTimerType.TIME && sleepTimer.value.endTime) {\n      const remaining = Math.max(0, sleepTimer.value.endTime - Date.now());\n      return Math.ceil(remaining / 60000);\n    }\n    return 0;\n  });\n\n  const sleepTimerRemainingSongs = computed(() => {\n    if (sleepTimer.value.type === SleepTimerType.SONGS) {\n      return sleepTimer.value.remainingSongs || 0;\n    }\n    return 0;\n  });\n\n  // ==================== Actions ====================\n\n  /**\n   * 按时间设置定时关闭\n   */\n  const setSleepTimerByTime = (minutes: number) => {\n    clearSleepTimer();\n\n    if (minutes <= 0) {\n      return false;\n    }\n\n    const endTime = Date.now() + minutes * 60 * 1000;\n\n    sleepTimer.value = {\n      type: SleepTimerType.TIME,\n      value: minutes,\n      endTime\n    };\n\n    setLocalStorageItem('sleepTimer', sleepTimer.value);\n\n    timerInterval.value = window.setInterval(() => {\n      checkSleepTimer();\n    }, 1000) as unknown as number;\n\n    console.log(`设置定时关闭: ${minutes}分钟后`);\n    return true;\n  };\n\n  /**\n   * 按歌曲数设置定时关闭\n   */\n  const setSleepTimerBySongs = async (songs: number) => {\n    clearSleepTimer();\n\n    if (songs <= 0) {\n      return false;\n    }\n\n    const { usePlaylistStore } = await import('./playlist');\n    const playlistStore = usePlaylistStore();\n\n    sleepTimer.value = {\n      type: SleepTimerType.SONGS,\n      value: songs,\n      startSongIndex: playlistStore.playListIndex,\n      remainingSongs: songs\n    };\n\n    setLocalStorageItem('sleepTimer', sleepTimer.value);\n\n    console.log(`设置定时关闭: 再播放${songs}首歌后`);\n    return true;\n  };\n\n  /**\n   * 播放列表结束时关闭\n   */\n  const setSleepTimerAtPlaylistEnd = () => {\n    clearSleepTimer();\n\n    sleepTimer.value = {\n      type: SleepTimerType.PLAYLIST_END,\n      value: 0\n    };\n\n    setLocalStorageItem('sleepTimer', sleepTimer.value);\n\n    console.log('设置定时关闭: 播放列表结束时');\n    return true;\n  };\n\n  /**\n   * 取消定时关闭\n   */\n  const clearSleepTimer = () => {\n    if (timerInterval.value) {\n      window.clearInterval(timerInterval.value);\n      timerInterval.value = null;\n    }\n\n    sleepTimer.value = {\n      type: SleepTimerType.NONE,\n      value: 0\n    };\n\n    setLocalStorageItem('sleepTimer', sleepTimer.value);\n\n    console.log('取消定时关闭');\n    return true;\n  };\n\n  /**\n   * 检查定时关闭是否应该触发\n   */\n  const checkSleepTimer = () => {\n    if (sleepTimer.value.type === SleepTimerType.NONE) {\n      return;\n    }\n\n    if (sleepTimer.value.type === SleepTimerType.TIME && sleepTimer.value.endTime) {\n      if (Date.now() >= sleepTimer.value.endTime) {\n        stopPlayback();\n      }\n    }\n  };\n\n  /**\n   * 停止播放并清除定时器\n   */\n  const stopPlayback = async () => {\n    console.log('定时器触发：停止播放');\n\n    const { usePlayerCoreStore } = await import('./playerCore');\n    const playerCore = usePlayerCoreStore();\n    const { audioService } = await import('@/services/audioService');\n\n    if (playerCore.isPlaying) {\n      playerCore.setIsPlay(false);\n      audioService.pause();\n    }\n\n    // 发送通知\n    if (window.electron?.ipcRenderer) {\n      window.electron.ipcRenderer.send('show-notification', {\n        title: i18n.global.t('player.sleepTimer.timerEnded'),\n        body: i18n.global.t('player.sleepTimer.playbackStopped')\n      });\n    }\n\n    clearSleepTimer();\n  };\n\n  /**\n   * 监听歌曲变化，处理按歌曲数定时和播放列表结束定时\n   */\n  const handleSongChange = async () => {\n    console.log('歌曲已切换，检查定时器状态:', sleepTimer.value);\n\n    // 处理按歌曲数定时\n    if (\n      sleepTimer.value.type === SleepTimerType.SONGS &&\n      sleepTimer.value.remainingSongs !== undefined\n    ) {\n      sleepTimer.value.remainingSongs--;\n      console.log(`剩余歌曲数: ${sleepTimer.value.remainingSongs}`);\n\n      setLocalStorageItem('sleepTimer', sleepTimer.value);\n\n      if (sleepTimer.value.remainingSongs <= 0) {\n        console.log('已播放完设定的歌曲数，停止播放');\n        stopPlayback();\n        setTimeout(() => {\n          stopPlayback();\n        }, 1000);\n      }\n    }\n\n    // 处理播放列表结束定时\n    if (sleepTimer.value.type === SleepTimerType.PLAYLIST_END) {\n      const { usePlaylistStore } = await import('./playlist');\n      const playlistStore = usePlaylistStore();\n\n      const isLastSong = playlistStore.playListIndex === playlistStore.playList.length - 1;\n\n      if (isLastSong && playlistStore.playMode !== 1) {\n        console.log('已到达播放列表末尾，将在当前歌曲结束后停止播放');\n        sleepTimer.value = {\n          type: SleepTimerType.SONGS,\n          value: 1,\n          remainingSongs: 1\n        };\n        setLocalStorageItem('sleepTimer', sleepTimer.value);\n      }\n    }\n  };\n\n  /**\n   * 设置定时器弹窗显示状态\n   */\n  const setShowSleepTimer = (value: boolean) => {\n    showSleepTimer.value = value;\n  };\n\n  return {\n    // 状态\n    sleepTimer,\n    showSleepTimer,\n\n    // Computed\n    currentSleepTimer,\n    hasSleepTimerActive,\n    sleepTimerRemainingTime,\n    sleepTimerRemainingSongs,\n\n    // Actions\n    setSleepTimerByTime,\n    setSleepTimerBySongs,\n    setSleepTimerAtPlaylistEnd,\n    clearSleepTimer,\n    checkSleepTimer,\n    stopPlayback,\n    handleSongChange,\n    setShowSleepTimer\n  };\n});\n"
  },
  {
    "path": "src/renderer/store/modules/user.ts",
    "content": "import { defineStore } from 'pinia';\nimport { ref } from 'vue';\n\nimport { logout } from '@/api/login';\nimport { getLikedList } from '@/api/music';\nimport { getUserAlbumSublist, getUserPlaylist } from '@/api/user';\nimport { clearLoginStatus } from '@/utils/auth';\n\ninterface UserData {\n  userId: number;\n  [key: string]: any;\n}\n\nfunction getLocalStorageItem<T>(key: string, defaultValue: T): T {\n  try {\n    const item = localStorage.getItem(key);\n    return item ? JSON.parse(item) : defaultValue;\n  } catch {\n    return defaultValue;\n  }\n}\n\nexport const useUserStore = defineStore('user', () => {\n  // 状态\n  const user = ref<UserData | null>(getLocalStorageItem('user', null));\n  const loginType = ref<'token' | 'cookie' | 'qr' | 'uid' | null>(\n    getLocalStorageItem('loginType', null)\n  );\n  const searchValue = ref('');\n  const searchType = ref(1);\n  // 收藏的专辑 ID 列表\n  const collectedAlbumIds = ref<Set<number>>(new Set());\n  // 用户的歌单列表\n  const playList = ref<any[]>([]);\n  // 用户的专辑列表\n  const albumList = ref<any[]>([]);\n\n  // 方法\n  const setUser = (userData: UserData) => {\n    user.value = userData;\n    localStorage.setItem('user', JSON.stringify(userData));\n  };\n\n  const setLoginType = (type: typeof loginType.value) => {\n    loginType.value = type;\n    if (type) {\n      localStorage.setItem('loginType', type);\n    } else {\n      localStorage.removeItem('loginType');\n    }\n  };\n\n  const handleLogout = async () => {\n    try {\n      await logout();\n      user.value = null;\n      loginType.value = null;\n      collectedAlbumIds.value.clear();\n      playList.value = [];\n      albumList.value = [];\n      clearLoginStatus();\n      // 刷新\n      window.location.reload();\n    } catch (error) {\n      console.error('登出失败:', error);\n      // 即使API调用失败，也要清除本地状态\n      user.value = null;\n      loginType.value = null;\n      collectedAlbumIds.value.clear();\n      playList.value = [];\n      albumList.value = [];\n      clearLoginStatus();\n      window.location.reload();\n    }\n  };\n\n  const setSearchValue = (value: string) => {\n    searchValue.value = value;\n  };\n\n  const setSearchType = (type: number) => {\n    searchType.value = type;\n  };\n\n  // 初始化歌单列表\n  const initializePlaylist = async () => {\n    if (!user.value) {\n      playList.value = [];\n      return;\n    }\n\n    try {\n      const { data } = await getUserPlaylist(user.value.userId, 1000, 0);\n      playList.value = data?.playlist || [];\n      console.log(`已加载 ${playList.value.length} 个歌单`);\n    } catch (error) {\n      console.error('获取歌单列表失败:', error);\n      playList.value = [];\n    }\n  };\n\n  // 初始化专辑列表\n  const initializeAlbumList = async () => {\n    if (!user.value || !localStorage.getItem('token')) {\n      albumList.value = [];\n      return;\n    }\n\n    try {\n      const { data } = await getUserAlbumSublist({ limit: 1000, offset: 0 });\n      albumList.value = data?.data || [];\n      console.log(`已加载 ${albumList.value.length} 个收藏专辑`);\n    } catch (error) {\n      console.error('获取专辑列表失败:', error);\n      albumList.value = [];\n    }\n  };\n\n  // 初始化收藏的专辑ID列表\n  const initializeCollectedAlbums = async () => {\n    if (!user.value || !localStorage.getItem('token')) {\n      collectedAlbumIds.value.clear();\n      return;\n    }\n\n    try {\n      const { data } = await getUserAlbumSublist({ limit: 1000, offset: 0 });\n      const albumIds = (data?.data || []).map((album: any) => album.id);\n      collectedAlbumIds.value = new Set(albumIds);\n      console.log(`已加载 ${albumIds.length} 个收藏专辑ID`);\n    } catch (error) {\n      console.error('获取收藏专辑列表失败:', error);\n      collectedAlbumIds.value.clear();\n    }\n  };\n\n  // 添加收藏专辑\n  const addCollectedAlbum = (albumId: number) => {\n    collectedAlbumIds.value.add(albumId);\n  };\n\n  // 移除收藏专辑\n  const removeCollectedAlbum = (albumId: number) => {\n    collectedAlbumIds.value.delete(albumId);\n  };\n\n  // 检查专辑是否已收藏\n  const isAlbumCollected = (albumId: number) => {\n    return collectedAlbumIds.value.has(albumId);\n  };\n\n  // 判断用户是否为VIP\n  const isVip = computed(() => {\n    if (!user.value) return false;\n    // vipType: 0 非VIP, 11 VIP\n    return user.value.vipType && user.value.vipType !== 0;\n  });\n\n  // 初始化\n  const initializeUser = async () => {\n    const savedUser = getLocalStorageItem<UserData | null>('user', null);\n    if (savedUser) {\n      user.value = savedUser;\n      // 如果用户已登录，获取收藏列表\n      if (localStorage.getItem('token')) {\n        try {\n          // 并行加载歌单、专辑和收藏ID列表\n          await Promise.all([\n            initializePlaylist(),\n            initializeAlbumList(),\n            initializeCollectedAlbums()\n          ]);\n\n          const { data } = await getLikedList(savedUser.userId);\n          return data?.ids || [];\n        } catch (error) {\n          console.error('获取收藏列表失败:', error);\n          return [];\n        }\n      }\n    }\n    return [];\n  };\n\n  return {\n    // 状态\n    user,\n    loginType,\n    searchValue,\n    searchType,\n    collectedAlbumIds,\n    playList,\n    albumList,\n    isVip,\n\n    // 方法\n    setUser,\n    setLoginType,\n    handleLogout,\n    setSearchValue,\n    setSearchType,\n    initializeUser,\n    initializePlaylist,\n    initializeAlbumList,\n    initializeCollectedAlbums,\n    addCollectedAlbum,\n    removeCollectedAlbum,\n    isAlbumCollected\n  };\n});\n"
  },
  {
    "path": "src/renderer/types/album.ts",
    "content": "export interface IAlbumNew {\n  code: number;\n  albums: Album[];\n}\n\nexport interface Album {\n  name: string;\n  id: number;\n  type: string;\n  size: number;\n  picId: number;\n  blurPicUrl: string;\n  companyId: number;\n  pic: number;\n  picUrl: string;\n  publishTime: number;\n  description: string;\n  tags: string;\n  company: string;\n  briefDesc: string;\n  artist: Artist;\n  songs?: any;\n  alias: string[];\n  status: number;\n  copyrightId: number;\n  commentThreadId: string;\n  artists: Artist2[];\n  paid: boolean;\n  onSale: boolean;\n  picId_str: string;\n}\n\ninterface Artist2 {\n  name: string;\n  id: number;\n  picId: number;\n  img1v1Id: number;\n  briefDesc: string;\n  picUrl: string;\n  img1v1Url: string;\n  albumSize: number;\n  alias: any[];\n  trans: string;\n  musicSize: number;\n  topicPerson: number;\n  img1v1Id_str: string;\n}\n\ninterface Artist {\n  name: string;\n  id: number;\n  picId: number;\n  img1v1Id: number;\n  briefDesc: string;\n  picUrl: string;\n  img1v1Url: string;\n  albumSize: number;\n  alias: string[];\n  trans: string;\n  musicSize: number;\n  topicPerson: number;\n  picId_str?: string;\n  img1v1Id_str: string;\n  transNames?: string[];\n}\n"
  },
  {
    "path": "src/renderer/types/artist.ts",
    "content": "export interface IArtistDetail {\n  videoCount: number;\n  vipRights: VipRights;\n  identify: Identify;\n  artist: IArtist;\n  blacklist: boolean;\n  preferShow: number;\n  showPriMsg: boolean;\n  secondaryExpertIdentiy: SecondaryExpertIdentiy[];\n  eventCount: number;\n  user: User;\n}\n\ninterface User {\n  backgroundUrl: string;\n  birthday: number;\n  detailDescription: string;\n  authenticated: boolean;\n  gender: number;\n  city: number;\n  signature: null;\n  description: string;\n  remarkName: null;\n  shortUserName: string;\n  accountStatus: number;\n  locationStatus: number;\n  avatarImgId: number;\n  defaultAvatar: boolean;\n  province: number;\n  nickname: string;\n  expertTags: null;\n  djStatus: number;\n  avatarUrl: string;\n  accountType: number;\n  authStatus: number;\n  vipType: number;\n  userName: string;\n  followed: boolean;\n  userId: number;\n  lastLoginIP: string;\n  lastLoginTime: number;\n  authenticationTypes: number;\n  mutual: boolean;\n  createTime: number;\n  anchor: boolean;\n  authority: number;\n  backgroundImgId: number;\n  userType: number;\n  experts: null;\n  avatarDetail: AvatarDetail;\n}\n\ninterface AvatarDetail {\n  userType: number;\n  identityLevel: number;\n  identityIconUrl: string;\n}\n\ninterface SecondaryExpertIdentiy {\n  expertIdentiyId: number;\n  expertIdentiyName: string;\n  expertIdentiyCount: number;\n}\n\nexport interface IArtist {\n  id: number;\n  cover: string;\n  avatar: string;\n  name: string;\n  transNames: any[];\n  alias: any[];\n  identities: any[];\n  identifyTag: string[];\n  briefDesc: string;\n  rank: Rank;\n  albumSize: number;\n  musicSize: number;\n  mvSize: number;\n}\n\ninterface Rank {\n  rank: number;\n  type: number;\n}\n\ninterface Identify {\n  imageUrl: string;\n  imageDesc: string;\n  actionUrl: string;\n}\n\ninterface VipRights {\n  rightsInfoDetailDtoList: RightsInfoDetailDtoList[];\n  oldProtocol: boolean;\n  redVipAnnualCount: number;\n  redVipLevel: number;\n  now: number;\n}\n\ninterface RightsInfoDetailDtoList {\n  vipCode: number;\n  expireTime: number;\n  iconUrl: null;\n  dynamicIconUrl: null;\n  vipLevel: number;\n  signIap: boolean;\n  signDeduct: boolean;\n  signIapDeduct: boolean;\n  sign: boolean;\n}\n"
  },
  {
    "path": "src/renderer/types/bilibili.ts",
    "content": "export interface IBilibiliSearchResult {\n  id: number;\n  bvid: string;\n  title: string;\n  pic: string;\n  duration: number | string;\n  pubdate: number;\n  ctime: number;\n  author: string;\n  view: number;\n  danmaku: number;\n  owner: {\n    mid: number;\n    name: string;\n    face: string;\n  };\n  stat: {\n    view: number;\n    danmaku: number;\n    reply: number;\n    favorite: number;\n    coin: number;\n    share: number;\n    like: number;\n  };\n}\n\nexport interface IBilibiliVideoDetail {\n  aid: number;\n  bvid: string;\n  title: string;\n  pic: string;\n  desc: string;\n  duration: number;\n  pubdate: number;\n  ctime: number;\n  owner: {\n    mid: number;\n    name: string;\n    face: string;\n  };\n  stat: {\n    view: number;\n    danmaku: number;\n    reply: number;\n    favorite: number;\n    coin: number;\n    share: number;\n    like: number;\n  };\n  pages: IBilibiliPage[];\n}\n\nexport interface IBilibiliPage {\n  cid: number;\n  page: number;\n  part: string;\n  duration: number;\n  dimension: {\n    width: number;\n    height: number;\n    rotate: number;\n  };\n}\n\nexport interface IBilibiliPlayUrl {\n  durl?: {\n    order: number;\n    length: number;\n    size: number;\n    ahead: string;\n    vhead: string;\n    url: string;\n    backup_url: string[];\n  }[];\n  dash?: {\n    duration: number;\n    minBufferTime: number;\n    min_buffer_time: number;\n    video: IBilibiliDashItem[];\n    audio: IBilibiliDashItem[];\n  };\n  support_formats: {\n    quality: number;\n    format: string;\n    new_description: string;\n    display_desc: string;\n  }[];\n  accept_quality: number[];\n  accept_description: string[];\n  quality: number;\n  format: string;\n  timelength: number;\n  high_format: string;\n}\n\nexport interface IBilibiliDashItem {\n  id: number;\n  baseUrl: string;\n  base_url: string;\n  backupUrl: string[];\n  backup_url: string[];\n  bandwidth: number;\n  mimeType: string;\n  mime_type: string;\n  codecs: string;\n  width?: number;\n  height?: number;\n  frameRate?: string;\n  frame_rate?: string;\n  startWithSap?: number;\n  start_with_sap?: number;\n  codecid: number;\n}\n"
  },
  {
    "path": "src/renderer/types/day_recommend.ts",
    "content": "export interface IDayRecommend {\n  dailySongs: DailySong[];\n  orderSongs: any[];\n  recommendReasons: RecommendReason[];\n  mvResourceInfos: null;\n}\n\ninterface RecommendReason {\n  songId: number;\n  reason: string;\n  reasonId: string;\n  targetUrl: null;\n}\n\ninterface DailySong {\n  name: string;\n  id: number;\n  pst: number;\n  t: number;\n  ar: Ar[];\n  alia: string[];\n  pop: number;\n  st: number;\n  rt: null | string;\n  fee: number;\n  v: number;\n  crbt: null;\n  cf: string;\n  al: Al;\n  dt: number;\n  h: H;\n  m: H;\n  l: H;\n  sq: H | null;\n  hr: H | null;\n  a: null;\n  cd: string;\n  no: number;\n  rtUrl: null;\n  ftype: number;\n  rtUrls: any[];\n  djId: number;\n  copyright: number;\n  s_id: number;\n  mark: number;\n  originCoverType: number;\n  originSongSimpleData: OriginSongSimpleDatum | null;\n  tagPicList: null;\n  resourceState: boolean;\n  version: number;\n  songJumpInfo: null;\n  entertainmentTags: null;\n  single: number;\n  noCopyrightRcmd: null;\n  rtype: number;\n  rurl: null;\n  mst: number;\n  cp: number;\n  mv: number;\n  publishTime: number;\n  reason: null | string;\n  videoInfo: VideoInfo;\n  recommendReason: null | string;\n  privilege: Privilege;\n  alg: string;\n  tns?: string[];\n  s_ctrp?: string;\n}\n\ninterface Privilege {\n  id: number;\n  fee: number;\n  payed: number;\n  realPayed: number;\n  st: number;\n  pl: number;\n  dl: number;\n  sp: number;\n  cp: number;\n  subp: number;\n  cs: boolean;\n  maxbr: number;\n  fl: number;\n  pc: null;\n  toast: boolean;\n  flag: number;\n  paidBigBang: boolean;\n  preSell: boolean;\n  playMaxbr: number;\n  downloadMaxbr: number;\n  maxBrLevel: string;\n  playMaxBrLevel: string;\n  downloadMaxBrLevel: string;\n  plLevel: string;\n  dlLevel: string;\n  flLevel: string;\n  rscl: null;\n  freeTrialPrivilege: FreeTrialPrivilege;\n  rightSource: number;\n  chargeInfoList: ChargeInfoList[];\n}\n\ninterface ChargeInfoList {\n  rate: number;\n  chargeUrl: null;\n  chargeMessage: null;\n  chargeType: number;\n}\n\ninterface FreeTrialPrivilege {\n  resConsumable: boolean;\n  userConsumable: boolean;\n  listenType: number;\n  cannotListenReason: number;\n  playReason: null;\n}\n\ninterface VideoInfo {\n  moreThanOne: boolean;\n  video: Video | null;\n}\n\ninterface Video {\n  vid: string;\n  type: number;\n  title: string;\n  playTime: number;\n  coverUrl: string;\n  publishTime: number;\n  artists: null;\n  alias: null;\n}\n\ninterface OriginSongSimpleDatum {\n  songId: number;\n  name: string;\n  artists: Artist[];\n  albumMeta: Artist;\n}\n\ninterface Artist {\n  id: number;\n  name: string;\n}\n\ninterface H {\n  br: number;\n  fid: number;\n  size: number;\n  vd: number;\n  sr: number;\n}\n\ninterface Al {\n  id: number;\n  name: string;\n  picUrl: string;\n  tns: string[];\n  pic_str?: string;\n  pic: number;\n}\n\ninterface Ar {\n  id: number;\n  name: string;\n  tns: any[];\n  alias: any[];\n}\n"
  },
  {
    "path": "src/renderer/types/electron.d.ts",
    "content": "export interface IElectronAPI {\n  minimize: () => void;\n  maximize: () => void;\n  close: () => void;\n  dragStart: (_data: string) => void;\n  miniTray: () => void;\n  restart: () => void;\n  openLyric: () => void;\n  sendLyric: (_data: string) => void;\n  unblockMusic: (_id: number) => Promise<string>;\n  importCustomApiPlugin: () => Promise<{ name: string; content: string } | null>;\n  importLxMusicScript: () => Promise<{ name: string; content: string } | null>;\n  onLanguageChanged: (_callback: (_locale: string) => void) => void;\n  store: {\n    get: (_key: string) => Promise<any>;\n    set: (_key: string, _value: any) => Promise<boolean>;\n    delete: (_key: string) => Promise<boolean>;\n  };\n}\n\ndeclare global {\n  interface Window {\n    api: IElectronAPI;\n  }\n}\n"
  },
  {
    "path": "src/renderer/types/index.ts",
    "content": "export interface IData<T> {\n  code: number;\n  data: T;\n  result: T;\n}\n"
  },
  {
    "path": "src/renderer/types/list.ts",
    "content": "export interface IList {\n  playlists: Playlist[];\n  code: number;\n  more: boolean;\n  lasttime: number;\n  total: number;\n}\n\nexport interface Playlist {\n  name: string;\n  id: number;\n  trackNumberUpdateTime: number;\n  status: number;\n  userId: number;\n  createTime: number;\n  updateTime: number;\n  subscribedCount: number;\n  trackCount: number;\n  cloudTrackCount: number;\n  coverImgUrl: string;\n  coverImgId: number;\n  description: string;\n  tags: string[];\n  playCount: number;\n  trackUpdateTime: number;\n  specialType: number;\n  totalDuration: number;\n  creator: Creator;\n  tracks?: any;\n  subscribers: Subscriber[];\n  subscribed: boolean;\n  commentThreadId: string;\n  newImported: boolean;\n  adType: number;\n  highQuality: boolean;\n  privacy: number;\n  ordered: boolean;\n  anonimous: boolean;\n  coverStatus: number;\n  recommendInfo?: any;\n  shareCount: number;\n  coverImgId_str?: string;\n  commentCount: number;\n  copywriter: string;\n  tag: string;\n}\n\ninterface Subscriber {\n  defaultAvatar: boolean;\n  province: number;\n  authStatus: number;\n  followed: boolean;\n  avatarUrl: string;\n  accountStatus: number;\n  gender: number;\n  city: number;\n  birthday: number;\n  userId: number;\n  userType: number;\n  nickname: string;\n  signature: string;\n  description: string;\n  detailDescription: string;\n  avatarImgId: number;\n  backgroundImgId: number;\n  backgroundUrl: string;\n  authority: number;\n  mutual: boolean;\n  expertTags?: any;\n  experts?: any;\n  djStatus: number;\n  vipType: number;\n  remarkName?: any;\n  authenticationTypes: number;\n  avatarDetail?: any;\n  avatarImgIdStr: string;\n  backgroundImgIdStr: string;\n  anchor: boolean;\n  avatarImgId_str?: string;\n}\n\ninterface Creator {\n  defaultAvatar: boolean;\n  province: number;\n  authStatus: number;\n  followed: boolean;\n  avatarUrl: string;\n  accountStatus: number;\n  gender: number;\n  city: number;\n  birthday: number;\n  userId: number;\n  userType: number;\n  nickname: string;\n  signature: string;\n  description: string;\n  detailDescription: string;\n  avatarImgId: number;\n  backgroundImgId: number;\n  backgroundUrl: string;\n  authority: number;\n  mutual: boolean;\n  expertTags?: string[];\n  experts?: Expert;\n  djStatus: number;\n  vipType: number;\n  remarkName?: any;\n  authenticationTypes: number;\n  avatarDetail?: AvatarDetail;\n  avatarImgIdStr: string;\n  backgroundImgIdStr: string;\n  anchor: boolean;\n  avatarImgId_str?: string;\n}\n\ninterface AvatarDetail {\n  userType: number;\n  identityLevel: number;\n  identityIconUrl: string;\n}\n\ninterface Expert {\n  '2': string;\n  '1'?: string;\n}\n\n// 推荐歌单\nexport interface IRecommendList {\n  hasTaste: boolean;\n  code: number;\n  category: number;\n  result: IRecommendItem[];\n}\n\nexport interface IRecommendItem {\n  id: number;\n  type: number;\n  name: string;\n  copywriter: string;\n  picUrl: string;\n  canDislike: boolean;\n  trackNumberUpdateTime: number;\n  playCount: number;\n  trackCount: number;\n  highQuality: boolean;\n  alg: string;\n}\n"
  },
  {
    "path": "src/renderer/types/listDetail.ts",
    "content": "export interface IListDetail {\n  code: number;\n  relatedVideos?: any;\n  playlist: Playlist;\n  urls?: any;\n  privileges: Privilege[];\n  sharedPrivilege?: any;\n  resEntrance?: any;\n}\n\ninterface Privilege {\n  id: number;\n  fee: number;\n  payed: number;\n  realPayed: number;\n  st: number;\n  pl: number;\n  dl: number;\n  sp: number;\n  cp: number;\n  subp: number;\n  cs: boolean;\n  maxbr: number;\n  fl: number;\n  pc?: any;\n  toast: boolean;\n  flag: number;\n  paidBigBang: boolean;\n  preSell: boolean;\n  playMaxbr: number;\n  downloadMaxbr: number;\n  rscl?: any;\n  freeTrialPrivilege: FreeTrialPrivilege;\n  chargeInfoList: ChargeInfoList[];\n}\n\ninterface ChargeInfoList {\n  rate: number;\n  chargeUrl?: any;\n  chargeMessage?: any;\n  chargeType: number;\n}\n\ninterface FreeTrialPrivilege {\n  resConsumable: boolean;\n  userConsumable: boolean;\n}\n\nexport interface Playlist {\n  id: number;\n  name: string;\n  coverImgId: number;\n  coverImgUrl: string;\n  coverImgId_str: string;\n  adType: number;\n  userId: number;\n  createTime: number;\n  status: number;\n  opRecommend: boolean;\n  highQuality: boolean;\n  newImported: boolean;\n  updateTime: number;\n  trackCount: number;\n  specialType: number;\n  privacy: number;\n  trackUpdateTime: number;\n  commentThreadId: string;\n  playCount: number;\n  trackNumberUpdateTime: number;\n  subscribedCount: number;\n  cloudTrackCount: number;\n  ordered: boolean;\n  description: string;\n  tags: string[];\n  updateFrequency?: any;\n  backgroundCoverId: number;\n  backgroundCoverUrl?: any;\n  titleImage: number;\n  titleImageUrl?: any;\n  englishTitle?: any;\n  officialPlaylistType?: any;\n  subscribers: Subscriber[];\n  subscribed: boolean;\n  creator: Subscriber;\n  tracks: Track[];\n  videoIds?: any;\n  videos?: any;\n  trackIds: TrackId[];\n  shareCount: number;\n  commentCount: number;\n  remixVideo?: any;\n  sharedUsers?: any;\n  historySharedUsers?: any;\n}\n\ninterface TrackId {\n  id: number;\n  v: number;\n  t: number;\n  at: number;\n  alg?: any;\n  uid: number;\n  rcmdReason: string;\n}\n\ninterface Track {\n  name: string;\n  id: number;\n  pst: number;\n  t: number;\n  ar: Ar[];\n  alia: string[];\n  pop: number;\n  st: number;\n  rt?: string;\n  fee: number;\n  v: number;\n  crbt?: any;\n  cf: string;\n  al: Al;\n  dt: number;\n  h: H;\n  m: H;\n  l?: H;\n  a?: any;\n  cd: string;\n  no: number;\n  rtUrl?: any;\n  ftype: number;\n  rtUrls: any[];\n  djId: number;\n  copyright: number;\n  s_id: number;\n  mark: number;\n  originCoverType: number;\n  originSongSimpleData?: any;\n  single: number;\n  noCopyrightRcmd?: any;\n  mst: number;\n  cp: number;\n  mv: number;\n  rtype: number;\n  rurl?: any;\n  publishTime: number;\n  tns?: string[];\n}\n\ninterface H {\n  br: number;\n  fid: number;\n  size: number;\n  vd: number;\n}\n\ninterface Al {\n  id: number;\n  name: string;\n  picUrl: string;\n  tns: any[];\n  pic_str?: string;\n  pic: number;\n}\n\ninterface Ar {\n  id: number;\n  name: string;\n  tns: any[];\n  alias: any[];\n}\n\ninterface Subscriber {\n  defaultAvatar: boolean;\n  province: number;\n  authStatus: number;\n  followed: boolean;\n  avatarUrl: string;\n  accountStatus: number;\n  gender: number;\n  city: number;\n  birthday: number;\n  userId: number;\n  userType: number;\n  nickname: string;\n  signature: string;\n  description: string;\n  detailDescription: string;\n  avatarImgId: number;\n  backgroundImgId: number;\n  backgroundUrl: string;\n  authority: number;\n  mutual: boolean;\n  expertTags?: any;\n  experts?: any;\n  djStatus: number;\n  vipType: number;\n  remarkName?: any;\n  authenticationTypes: number;\n  avatarDetail?: any;\n  backgroundImgIdStr: string;\n  anchor: boolean;\n  avatarImgIdStr: string;\n  avatarImgId_str: string;\n}\n"
  },
  {
    "path": "src/renderer/types/lxMusic.ts",
    "content": "/**\n * 落雪音乐 (LX Music) 自定义源类型定义\n *\n * 参考文档: https://lxmusic.toside.cn/desktop/custom-source\n */\n\n/**\n * 脚本元信息（从注释头解析）\n */\nexport type LxScriptInfo = {\n  name: string;\n  description?: string;\n  version?: string;\n  author?: string;\n  homepage?: string;\n  rawScript: string;\n};\n\n/**\n * 支持的音质类型\n */\nexport type LxQuality = '128k' | '320k' | 'flac' | 'flac24bit';\n\n/**\n * 支持的音源 key\n * - kw: 酷我\n * - kg: 酷狗\n * - tx: QQ音乐\n * - wy: 网易云\n * - mg: 咪咕\n * - local: 本地音乐\n */\nexport type LxSourceKey = 'kw' | 'kg' | 'tx' | 'wy' | 'mg' | 'local';\n\n/**\n * 音源配置\n */\nexport type LxSourceConfig = {\n  name: string;\n  type: 'music';\n  actions: ('musicUrl' | 'lyric' | 'pic')[];\n  qualitys: LxQuality[];\n};\n\n/**\n * 初始化事件数据\n */\nexport type LxInitedData = {\n  openDevTools?: boolean;\n  sources: Partial<Record<LxSourceKey, LxSourceConfig>>;\n};\n\n/**\n * 请求事件数据\n */\nexport type LxRequestData = {\n  source: LxSourceKey;\n  action: 'musicUrl' | 'lyric' | 'pic';\n  info: {\n    type: LxQuality | null;\n    musicInfo: LxMusicInfo;\n  };\n};\n\n/**\n * 落雪音乐信息格式\n * 需要从 SongResult 转换而来\n */\nexport type LxMusicInfo = {\n  songmid: string | number;\n  name: string;\n  singer: string;\n  album?: string;\n  albumId?: string | number;\n  source?: string;\n  interval?: string;\n  img?: string;\n  types?: { type: LxQuality; size?: string }[];\n};\n\n/**\n * 歌词返回格式\n */\nexport type LxLyricResult = {\n  lyric: string;\n  tlyric?: string | null;\n  rlyric?: string | null;\n  lxlyric?: string | null;\n};\n\n/**\n * 存储在 settings 中的单个落雪音源配置\n */\nexport type LxMusicScriptConfig = {\n  id: string; // 唯一标识\n  name: string; // 用户自定义名称，可编辑\n  script: string; // 脚本内容\n  info: LxScriptInfo; // 解析的脚本元信息\n  sources: LxSourceKey[];\n  enabled: boolean; // 是否启用\n  createdAt: number; // 创建时间戳\n};\n\n/**\n * 存储在 settings 中的落雪音源列表\n */\nexport type LxMusicApiList = {\n  apis: LxMusicScriptConfig[];\n  activeId: string | null; // 当前激活的音源 ID\n};\n\n/**\n * globalThis.lx API 的事件名称\n */\nexport const LX_EVENT_NAMES = {\n  inited: 'inited',\n  request: 'request',\n  updateAlert: 'updateAlert'\n} as const;\n\n/**\n * 落雪音源 key 到平台名称的映射\n */\nexport const LX_SOURCE_NAMES: Record<LxSourceKey, string> = {\n  kw: '酷我',\n  kg: '酷狗',\n  tx: 'QQ音乐',\n  wy: '网易云',\n  mg: '咪咕',\n  local: '本地'\n};\n\n/**\n * 本项目音质到落雪音质的映射\n */\nexport const QUALITY_TO_LX: Record<string, LxQuality> = {\n  standard: '128k',\n  higher: '320k',\n  exhigh: '320k',\n  lossless: 'flac',\n  hires: 'flac24bit',\n  jyeffect: 'flac',\n  sky: 'flac',\n  dolby: 'flac',\n  jymaster: 'flac24bit'\n};\n"
  },
  {
    "path": "src/renderer/types/lyric.ts",
    "content": "export interface LyricConfig {\n  hideCover: boolean;\n  centerLyrics: boolean;\n  fontSize: number;\n  letterSpacing: number;\n  fontWeight: number;\n  lineHeight: number;\n  showTranslation: boolean;\n  theme: 'default' | 'light' | 'dark';\n  hidePlayBar: boolean;\n  translationEngine?: 'none' | 'opencc';\n  pureModeEnabled: boolean;\n  hideMiniPlayBar: boolean;\n  hideLyrics: boolean;\n  contentWidth: number; // 内容区域宽度百分比\n  // 移动端配置\n  mobileLayout: 'default' | 'ios' | 'android';\n  mobileCoverStyle: 'record' | 'square' | 'full';\n  mobileShowLyricLines: number;\n  // 背景自定义功能\n  useCustomBackground: boolean; // 是否使用自定义背景\n  backgroundMode: 'solid' | 'gradient' | 'image' | 'css'; // 背景模式\n  solidColor: string; // 纯色背景颜色值\n  gradientColors: {\n    colors: string[]; // 渐变颜色数组\n    direction: string; // 渐变方向\n  };\n  backgroundImage?: string; // 图片背景 (Base64 或 URL)\n  imageBlur: number; // 图片模糊度 (0-20px)\n  imageBrightness: number; // 图片明暗度 (0-200%, 100为正常)\n  customCss?: string; // 自定义 CSS 样式\n}\n\nexport const DEFAULT_LYRIC_CONFIG: LyricConfig = {\n  hideCover: false,\n  centerLyrics: false,\n  fontSize: 22,\n  letterSpacing: 0,\n  fontWeight: 500,\n  lineHeight: 2,\n  showTranslation: true,\n  theme: 'default',\n  hidePlayBar: true,\n  hideMiniPlayBar: false,\n  pureModeEnabled: false,\n  hideLyrics: false,\n  contentWidth: 75, // 默认100%宽度\n  // 移动端默认配置\n  mobileLayout: 'ios',\n  mobileCoverStyle: 'full',\n  mobileShowLyricLines: 3,\n  // 翻译引擎: 'none' or 'opencc'\n  translationEngine: 'none',\n  // 背景自定义功能默认值\n  useCustomBackground: false,\n  backgroundMode: 'solid',\n  solidColor: '#1a1a1a',\n  gradientColors: {\n    colors: ['#1a1a1a', '#000000'],\n    direction: 'to bottom'\n  },\n  backgroundImage: undefined,\n  imageBlur: 0,\n  imageBrightness: 100,\n  customCss: undefined\n};\n\nexport interface ILyric {\n  sgc: boolean;\n  sfy: boolean;\n  qfy: boolean;\n  lrc: Lrc;\n  klyric: Lrc;\n  tlyric: Lrc;\n  code: number;\n}\n\ninterface Lrc {\n  version: number;\n  lyric: string;\n}\n"
  },
  {
    "path": "src/renderer/types/music.ts",
    "content": "// 音乐平台类型\nexport type Platform =\n  | 'qq'\n  | 'migu'\n  | 'kugou'\n  | 'kuwo'\n  | 'pyncmd'\n  | 'joox'\n  | 'bilibili'\n  | 'gdmusic'\n  | 'lxMusic';\n\n// 默认平台列表\nexport const DEFAULT_PLATFORMS: Platform[] = [\n  'lxMusic',\n  'migu',\n  'kugou',\n  'kuwo',\n  'pyncmd',\n  'bilibili'\n];\n\nexport interface IRecommendMusic {\n  code: number;\n  category: number;\n  result: SongResult[];\n}\n// 逐字歌词单词数据\nexport interface IWordData {\n  text: string;\n  startTime: number;\n  duration: number;\n  space?: boolean;\n}\n\nexport interface ILyricText {\n  text: string;\n  trText: string;\n  words?: IWordData[];\n  hasWordByWord?: boolean;\n  startTime?: number;\n  duration?: number;\n}\n\nexport interface ILyric {\n  lrcTimeArray: number[];\n  lrcArray: ILyricText[];\n  // 新增字段标识是否包含逐字歌词\n  hasWordByWord?: boolean;\n}\n\nexport interface SongResult {\n  id: string | number;\n  name: string;\n  picUrl: string;\n  playCount?: number;\n  song?: any;\n  copywriter?: string;\n  type?: number;\n  canDislike?: boolean;\n  program?: any;\n  alg?: string;\n  ar: Artist[];\n  artists?: Artist[];\n  al: Album;\n  album?: Album;\n  count: number;\n  playMusicUrl?: string;\n  playLoading?: boolean;\n  lyric?: ILyric;\n  backgroundColor?: string;\n  primaryColor?: string;\n  bilibiliData?: {\n    bvid: string;\n    cid: number;\n  };\n  source?: 'netease' | 'bilibili';\n  // 过期时间\n  expiredAt?: number;\n  // 获取时间\n  createdAt?: number;\n  // 时长\n  duration?: number;\n  dt?: number;\n  isFirstPlay?: boolean;\n}\n\nexport interface Song {\n  name: string;\n  id: number;\n  position: number;\n  alias: string[];\n  status: number;\n  fee: number;\n  copyrightId: number;\n  disc: string;\n  no: number;\n  artists: Artist[];\n  album: Album;\n  starred: boolean;\n  popularity: number;\n  score: number;\n  starredNum: number;\n  duration: number;\n  playedNum: number;\n  dayPlays: number;\n  hearTime: number;\n  ringtone: string;\n  crbt?: any;\n  audition?: any;\n  copyFrom: string;\n  commentThreadId: string;\n  rtUrl?: any;\n  ftype: number;\n  rtUrls: any[];\n  copyright: number;\n  transName?: any;\n  sign?: any;\n  mark: number;\n  originCoverType: number;\n  originSongSimpleData?: any;\n  single: number;\n  noCopyrightRcmd?: any;\n  rtype: number;\n  rurl?: any;\n  mvid: number;\n  bMusic: BMusic;\n  mp3Url?: any;\n  hMusic: BMusic;\n  mMusic: BMusic;\n  lMusic: BMusic;\n  exclusive: boolean;\n  privilege: Privilege;\n  count?: number;\n  playLoading?: boolean;\n  picUrl?: string;\n  ar: Artist[];\n}\n\ninterface Privilege {\n  id: number;\n  fee: number;\n  payed: number;\n  st: number;\n  pl: number;\n  dl: number;\n  sp: number;\n  cp: number;\n  subp: number;\n  cs: boolean;\n  maxbr: number;\n  fl: number;\n  toast: boolean;\n  flag: number;\n  preSell: boolean;\n  playMaxbr: number;\n  downloadMaxbr: number;\n  rscl?: any;\n  freeTrialPrivilege: FreeTrialPrivilege;\n  chargeInfoList: ChargeInfoList[];\n}\n\ninterface ChargeInfoList {\n  rate: number;\n  chargeUrl?: any;\n  chargeMessage?: any;\n  chargeType: number;\n}\n\ninterface FreeTrialPrivilege {\n  resConsumable: boolean;\n  userConsumable: boolean;\n}\n\ninterface BMusic {\n  name?: any;\n  id: number;\n  size: number;\n  extension: string;\n  sr: number;\n  dfsId: number;\n  bitrate: number;\n  playTime: number;\n  volumeDelta: number;\n}\n\ninterface Album {\n  name: string;\n  id: number;\n  type: string;\n  size: number;\n  picId: number;\n  blurPicUrl: string;\n  companyId: number;\n  pic: number;\n  picUrl: string;\n  publishTime: number;\n  description: string;\n  tags: string;\n  company: string;\n  briefDesc: string;\n  artist: Artist;\n  songs: any[];\n  alias: string[];\n  status: number;\n  copyrightId: number;\n  commentThreadId: string;\n  artists: Artist[];\n  subType: string;\n  transName?: any;\n  onSale: boolean;\n  mark: number;\n  picId_str: string;\n}\n\nexport interface Artist {\n  name: string;\n  id: number;\n  picId: number;\n  img1v1Id: number;\n  briefDesc: string;\n  picUrl: string;\n  img1v1Url: string;\n  albumSize: number;\n  alias: any[];\n  trans: string;\n  musicSize: number;\n  topicPerson: number;\n}\n\nexport interface IPlayMusicUrl {\n  data: Datum[];\n  code: number;\n}\n\ninterface Datum {\n  id: number;\n  url: string;\n  br: number;\n  size: number;\n  md5: string;\n  code: number;\n  expi: number;\n  type: string;\n  gain: number;\n  fee: number;\n  uf?: any;\n  payed: number;\n  flag: number;\n  canExtend: boolean;\n  freeTrialInfo?: any;\n  level: string;\n  encodeType: string;\n  freeTrialPrivilege: FreeTrialPrivilege;\n  freeTimeTrialPrivilege: FreeTimeTrialPrivilege;\n  urlSource: number;\n}\n\ninterface FreeTimeTrialPrivilege {\n  resConsumable: boolean;\n  userConsumable: boolean;\n  type: number;\n  remainTime: number;\n}\n\ninterface FreeTrialPrivilege {\n  resConsumable: boolean;\n  userConsumable: boolean;\n}\n\nexport interface IArtists {\n  id: number;\n  name: string;\n  picUrl: string | null;\n  alias: string[];\n  albumSize: number;\n  picId: number;\n  fansGroup: null;\n  img1v1Url: string;\n  img1v1: number;\n  trans: null;\n}\n\n// 音乐源类型定义\nexport type MusicSourceType =\n  | 'tencent'\n  | 'kugou'\n  | 'migu'\n  | 'netease'\n  | 'joox'\n  | 'ytmusic'\n  | 'spotify'\n  | 'qobuz'\n  | 'deezer'\n  | 'gdmusic';\n\n// 更多音乐相关的类型可以在这里定义\n"
  },
  {
    "path": "src/renderer/types/mv.ts",
    "content": "export interface IMvItem {\n  id: number;\n  cover: string;\n  name: string;\n  playCount: number;\n  briefDesc?: any;\n  desc?: any;\n  artistName: string;\n  artistId: number;\n  duration: number;\n  mark: number;\n  mv: IMvData;\n  lastRank: number;\n  score: number;\n  subed: boolean;\n  artists: Artist[];\n  transNames?: string[];\n  alias?: string[];\n}\n\nexport interface IMvData {\n  authId: number;\n  status: number;\n  id: number;\n  title: string;\n  subTitle: string;\n  appTitle: string;\n  aliaName: string;\n  transName: string;\n  pic4v3: number;\n  pic16v9: number;\n  caption: number;\n  captionLanguage: string;\n  style?: any;\n  mottos: string;\n  oneword?: any;\n  appword: string;\n  stars?: any;\n  desc: string;\n  area: string;\n  type: string;\n  subType: string;\n  neteaseonly: number;\n  upban: number;\n  topWeeks: string;\n  publishTime: string;\n  online: number;\n  score: number;\n  plays: number;\n  monthplays: number;\n  weekplays: number;\n  dayplays: number;\n  fee: number;\n  artists: Artist[];\n  videos: Video[];\n}\n\ninterface Video {\n  tagSign: TagSign;\n  tag: string;\n  url: string;\n  duration: number;\n  size: number;\n  width: number;\n  height: number;\n  container: string;\n  md5: string;\n  check: boolean;\n}\n\ninterface TagSign {\n  br: number;\n  type: string;\n  tagSign: string;\n  resolution: number;\n  mvtype: string;\n}\n\ninterface Artist {\n  id: number;\n  name: string;\n}\n\n// {\n//   \"id\": 14686812,\n//   \"url\": \"http://vodkgeyttp8.vod.126.net/cloudmusic/e18b/core/aa57/6f56150a35613ef77fc70b253bea4977.mp4?wsSecret=84a301277e05143de1dd912d2a4dbb0d&wsTime=1703668700\",\n//   \"r\": 1080,\n//   \"size\": 215391070,\n//   \"md5\": \"\",\n//   \"code\": 200,\n//   \"expi\": 3600,\n//   \"fee\": 0,\n//   \"mvFee\": 0,\n//   \"st\": 0,\n//   \"promotionVo\": null,\n//   \"msg\": \"\"\n// }\n\nexport interface IMvUrlData {\n  id: number;\n  url: string;\n  r: number;\n  size: number;\n  md5: string;\n  code: number;\n  expi: number;\n  fee: number;\n  mvFee: number;\n  st: number;\n  promotionVo: null | any;\n  msg: string;\n}\n"
  },
  {
    "path": "src/renderer/types/opencc-rust.d.ts",
    "content": "declare module 'https://cdn.jsdelivr.net/npm/opencc-rust/dist/opencc-rust.mjs' {\n  export function initOpenccRust(): Promise<void>;\n  export function getConverter(): {\n    convert: (text: string) => Promise<string>;\n  };\n}\n\n// Allow wildcard import if different CDN URL is used\ndeclare module 'opencc-rust' {\n  export function initOpenccRust(): Promise<void>;\n  export function getConverter(): {\n    convert: (text: string) => Promise<string>;\n  };\n}\n\ndeclare module './translation-engines/opencc' {\n  export function init(): Promise<void>;\n  export function convert(text: string): Promise<string>;\n}\n"
  },
  {
    "path": "src/renderer/types/playlist.ts",
    "content": "export interface IPlayListSort {\n  code: number;\n  all: SortAll;\n  sub: SortAll[];\n  categories: SortCategories;\n}\n\ninterface SortCategories {\n  '0': string;\n  '1': string;\n  '2': string;\n  '3': string;\n  '4': string;\n}\n\ninterface SortAll {\n  name: string;\n  resourceCount?: number;\n  imgId?: number;\n  imgUrl?: any;\n  type?: number;\n  category?: number;\n  resourceType?: number;\n  hot?: boolean;\n  activity?: boolean;\n}\n"
  },
  {
    "path": "src/renderer/types/search.ts",
    "content": "export interface ISearchKeyword {\n  code: number;\n  message?: any;\n  data: SearchKeywordData;\n}\n\ninterface SearchKeywordData {\n  showKeyword: string;\n  realkeyword: string;\n  searchType: number;\n  action: number;\n  alg: string;\n  gap: number;\n  source?: any;\n  bizQueryInfo: string;\n}\n\nexport interface IHotSearch {\n  code: number;\n  data: Datum[];\n  message: string;\n}\n\ninterface Datum {\n  searchWord: string;\n  score: number;\n  content: string;\n  source: number;\n  iconType: number;\n  iconUrl?: string;\n  url: string;\n  alg: string;\n}\n\nexport interface ISearchDetail {\n  result: Result;\n  code: number;\n}\n\ninterface Result {\n  song: Song2;\n  code: number;\n  mlog: Mlog2;\n  playList: PlayList2;\n  artist: Artist3;\n  album: Album3;\n  video: Video2;\n  sim_query: Simquery2;\n  djRadio: DjRadio2;\n  rec_type?: any;\n  talk: Talk2;\n  rec_query: null[];\n  user: User2;\n  order: string[];\n}\n\ninterface User2 {\n  moreText: string;\n  more: boolean;\n  users: User[];\n  resourceIds: number[];\n}\n\ninterface User {\n  defaultAvatar: boolean;\n  province: number;\n  authStatus: number;\n  followed: boolean;\n  avatarUrl: string;\n  accountStatus: number;\n  gender: number;\n  city: number;\n  birthday: number;\n  userId: number;\n  userType: number;\n  nickname: string;\n  signature: string;\n  description: string;\n  detailDescription: string;\n  avatarImgId: number;\n  backgroundImgId: number;\n  backgroundUrl: string;\n  authority: number;\n  mutual: boolean;\n  expertTags?: any;\n  experts?: any;\n  djStatus: number;\n  vipType: number;\n  remarkName?: any;\n  authenticationTypes: number;\n  avatarDetail?: any;\n  anchor: boolean;\n  avatarImgIdStr: string;\n  backgroundImgIdStr: string;\n  avatarImgId_str: string;\n  alg: string;\n}\n\ninterface Talk2 {\n  more: boolean;\n  talks: Talk[];\n  resourceIds: number[];\n}\n\ninterface Talk {\n  talkId: number;\n  shareUrl: string;\n  talkName: string;\n  shareCover: ShareCover;\n  showCover: ShareCover;\n  talkDes: string;\n  follows: number;\n  participations: number;\n  showParticipations: number;\n  status: number;\n  time?: any;\n  hasTag: boolean;\n  alg: string;\n  mlogCount: number;\n  commentCount: number;\n}\n\ninterface ShareCover {\n  picKey: string;\n  nosKey: string;\n  width: number;\n  height: number;\n  url: string;\n}\n\ninterface DjRadio2 {\n  moreText: string;\n  djRadios: DjRadio[];\n  more: boolean;\n  resourceIds: number[];\n}\n\ninterface DjRadio {\n  id: number;\n  dj: Dj;\n  name: string;\n  picUrl: string;\n  desc: string;\n  subCount: number;\n  programCount: number;\n  createTime: number;\n  categoryId: number;\n  category: string;\n  radioFeeType: number;\n  feeScope: number;\n  buyed: boolean;\n  videos?: any;\n  finished: boolean;\n  underShelf: boolean;\n  purchaseCount: number;\n  price: number;\n  originalPrice: number;\n  discountPrice?: any;\n  lastProgramCreateTime: number;\n  lastProgramName?: any;\n  lastProgramId: number;\n  picId: number;\n  rcmdText?: string;\n  hightQuality: boolean;\n  whiteList: boolean;\n  liveInfo?: any;\n  playCount: number;\n  icon?: any;\n  composeVideo: boolean;\n  shareCount: number;\n  likedCount: number;\n  alg: string;\n  commentCount: number;\n}\n\ninterface Dj {\n  defaultAvatar: boolean;\n  province: number;\n  authStatus: number;\n  followed: boolean;\n  avatarUrl: string;\n  accountStatus: number;\n  gender: number;\n  city: number;\n  birthday: number;\n  userId: number;\n  userType: number;\n  nickname: string;\n  signature: string;\n  description: string;\n  detailDescription: string;\n  avatarImgId: number;\n  backgroundImgId: number;\n  backgroundUrl: string;\n  authority: number;\n  mutual: boolean;\n  expertTags?: any;\n  experts?: any;\n  djStatus: number;\n  vipType: number;\n  remarkName?: any;\n  authenticationTypes: number;\n  avatarDetail?: any;\n  anchor: boolean;\n  avatarImgIdStr: string;\n  backgroundImgIdStr: string;\n  avatarImgId_str: string;\n}\n\ninterface Simquery2 {\n  sim_querys: Simquery[];\n  more: boolean;\n}\n\ninterface Simquery {\n  keyword: string;\n  alg: string;\n}\n\ninterface Video2 {\n  moreText: string;\n  more: boolean;\n  videos: Video[];\n  resourceIds: number[];\n}\n\ninterface Video {\n  coverUrl: string;\n  title: string;\n  durationms: number;\n  playTime: number;\n  type: number;\n  creator: Creator2[];\n  aliaName?: any;\n  transName?: any;\n  vid: string;\n  markTypes?: number[];\n  alg: string;\n}\n\ninterface Creator2 {\n  userId: number;\n  userName: string;\n}\n\ninterface Album3 {\n  moreText: string;\n  albums: Album2[];\n  more: boolean;\n  resourceIds: number[];\n}\n\ninterface Album2 {\n  name: string;\n  id: number;\n  type: string;\n  size: number;\n  picId: number;\n  blurPicUrl: string;\n  companyId: number;\n  pic: number;\n  picUrl: string;\n  publishTime: number;\n  description: string;\n  tags: string;\n  company?: string;\n  briefDesc: string;\n  artist: Artist4;\n  songs?: any;\n  alias: string[];\n  status: number;\n  copyrightId: number;\n  commentThreadId: string;\n  artists: Artist5[];\n  paid: boolean;\n  onSale: boolean;\n  picId_str: string;\n  alg: string;\n}\n\ninterface Artist5 {\n  name: string;\n  id: number;\n  picId: number;\n  img1v1Id: number;\n  briefDesc: string;\n  picUrl: string;\n  img1v1Url: string;\n  albumSize: number;\n  alias: any[];\n  trans: string;\n  musicSize: number;\n  topicPerson: number;\n  img1v1Id_str: string;\n}\n\ninterface Artist4 {\n  name: string;\n  id: number;\n  picId: number;\n  img1v1Id: number;\n  briefDesc: string;\n  picUrl: string;\n  img1v1Url: string;\n  albumSize: number;\n  alias: string[];\n  trans: string;\n  musicSize: number;\n  topicPerson: number;\n  picId_str: string;\n  img1v1Id_str: string;\n  alia: string[];\n}\n\ninterface Artist3 {\n  moreText: string;\n  artists: Artist2[];\n  more: boolean;\n  resourceIds: number[];\n}\n\ninterface Artist2 {\n  id: number;\n  name: string;\n  picUrl: string;\n  alias: string[];\n  albumSize: number;\n  picId: number;\n  img1v1Url: string;\n  img1v1: number;\n  mvSize: number;\n  followed: boolean;\n  alg: string;\n  alia?: string[];\n  trans?: any;\n  accountId?: number;\n}\n\ninterface PlayList2 {\n  moreText: string;\n  more: boolean;\n  playLists: PlayList[];\n  resourceIds: number[];\n}\n\ninterface PlayList {\n  id: number;\n  name: string;\n  coverImgUrl: string;\n  creator: Creator;\n  subscribed: boolean;\n  trackCount: number;\n  userId: number;\n  playCount: number;\n  bookCount: number;\n  specialType: number;\n  officialTags: string[];\n  description: string;\n  highQuality: boolean;\n  track: Track;\n  alg: string;\n}\n\ninterface Track {\n  name: string;\n  id: number;\n  position: number;\n  alias: any[];\n  status: number;\n  fee: number;\n  copyrightId: number;\n  disc: string;\n  no: number;\n  artists: Artist[];\n  album: Album;\n  starred: boolean;\n  popularity: number;\n  score: number;\n  starredNum: number;\n  duration: number;\n  playedNum: number;\n  dayPlays: number;\n  hearTime: number;\n  ringtone?: string;\n  crbt?: any;\n  audition?: any;\n  copyFrom: string;\n  commentThreadId: string;\n  rtUrl?: any;\n  ftype: number;\n  rtUrls: any[];\n  copyright: number;\n  mvid: number;\n  rtype: number;\n  rurl?: any;\n  hMusic: HMusic;\n  mMusic: HMusic;\n  lMusic: HMusic;\n  bMusic: HMusic;\n  mp3Url?: any;\n  transNames?: string[];\n}\n\ninterface HMusic {\n  name?: any;\n  id: number;\n  size: number;\n  extension: string;\n  sr: number;\n  dfsId: number;\n  bitrate: number;\n  playTime: number;\n  volumeDelta: number;\n}\n\ninterface Album {\n  name: string;\n  id: number;\n  type: string;\n  size: number;\n  picId: number;\n  blurPicUrl: string;\n  companyId: number;\n  pic: number;\n  picUrl: string;\n  publishTime: number;\n  description: string;\n  tags: string;\n  company?: string;\n  briefDesc: string;\n  artist: Artist;\n  songs: any[];\n  alias: any[];\n  status: number;\n  copyrightId: number;\n  commentThreadId: string;\n  artists: Artist[];\n  picId_str?: string;\n}\n\ninterface Artist {\n  name: string;\n  id: number;\n  picId: number;\n  img1v1Id: number;\n  briefDesc: string;\n  picUrl: string;\n  img1v1Url: string;\n  albumSize: number;\n  alias: any[];\n  trans: string;\n  musicSize: number;\n}\n\ninterface Creator {\n  nickname: string;\n  userId: number;\n  userType: number;\n  avatarUrl: string;\n  authStatus: number;\n  expertTags?: any;\n  experts?: any;\n}\n\ninterface Mlog2 {\n  moreText: string;\n  more: boolean;\n  mlogs: Mlog[];\n  resourceIds: any[];\n}\n\ninterface Mlog {\n  id: string;\n  type: number;\n  mlogBaseDataType: number;\n  position?: any;\n  resource: Resource;\n  alg: string;\n  reason?: any;\n  matchField: number;\n  matchFieldContent: string;\n  sameCity: boolean;\n}\n\ninterface Resource {\n  mlogBaseData: MlogBaseData;\n  mlogExtVO: MlogExtVO;\n  userProfile: UserProfile;\n  status: number;\n  shareUrl: string;\n}\n\ninterface UserProfile {\n  userId: number;\n  nickname: string;\n  avatarUrl: string;\n  followed: boolean;\n  userType: number;\n  isAnchor: boolean;\n}\n\ninterface MlogExtVO {\n  likedCount: number;\n  commentCount: number;\n  playCount: number;\n  song?: any;\n  canCollect?: any;\n  artistName?: any;\n  rcmdInfo?: any;\n  strongPushMark?: any;\n  strongPushIcon?: any;\n  specialTag?: any;\n  channelTag: string;\n  artists: any[];\n}\n\ninterface MlogBaseData {\n  id: string;\n  type: number;\n  text: string;\n  interveneText?: string;\n  pubTime: number;\n  coverUrl: string;\n  coverHeight: number;\n  coverWidth: number;\n  coverColor: number;\n  coverPicKey: string;\n  coverDynamicUrl?: any;\n  audio?: any;\n  threadId: string;\n  duration: number;\n}\n\ninterface Song2 {\n  moreText: string;\n  songs: Song[];\n  more: boolean;\n  ksongInfos: KsongInfos;\n  resourceIds: number[];\n}\n\ninterface KsongInfos {\n  '347230': _347230;\n}\n\ninterface _347230 {\n  androidDownloadUrl: string;\n  accompanyId: string;\n  deeplinkUrl: string;\n}\n\ninterface Song {\n  name: string;\n  id: number;\n  pst: number;\n  t: number;\n  ar: Ar[];\n  alia: any[];\n  pop: number;\n  st: number;\n  rt: string;\n  fee: number;\n  v: number;\n  crbt?: any;\n  cf: string;\n  al: Al;\n  dt: number;\n  h: H;\n  m: H;\n  l: H;\n  a?: any;\n  cd: string;\n  no: number;\n  rtUrl?: any;\n  ftype: number;\n  rtUrls: any[];\n  djId: number;\n  copyright: number;\n  s_id: number;\n  mark: number;\n  originCoverType: number;\n  originSongSimpleData?: any;\n  resourceState: boolean;\n  version: number;\n  single: number;\n  noCopyrightRcmd?: any;\n  rtype: number;\n  rurl?: any;\n  mst: number;\n  cp: number;\n  mv: number;\n  publishTime: number;\n  showRecommend: boolean;\n  recommendText: string;\n  tns?: string[];\n  officialTags: any[];\n  privilege: Privilege;\n  alg: string;\n  specialTags: any[];\n}\n\ninterface Privilege {\n  id: number;\n  fee: number;\n  payed: number;\n  st: number;\n  pl: number;\n  dl: number;\n  sp: number;\n  cp: number;\n  subp: number;\n  cs: boolean;\n  maxbr: number;\n  fl: number;\n  toast: boolean;\n  flag: number;\n  preSell: boolean;\n  playMaxbr: number;\n  downloadMaxbr: number;\n  rscl?: any;\n  freeTrialPrivilege: FreeTrialPrivilege;\n  chargeInfoList: ChargeInfoList[];\n}\n\ninterface ChargeInfoList {\n  rate: number;\n  chargeUrl?: any;\n  chargeMessage?: any;\n  chargeType: number;\n}\n\ninterface FreeTrialPrivilege {\n  resConsumable: boolean;\n  userConsumable: boolean;\n}\n\ninterface H {\n  br: number;\n  fid: number;\n  size: number;\n  vd: number;\n}\n\ninterface Al {\n  id: number;\n  name: string;\n  picUrl: string;\n  tns: any[];\n  pic_str?: string;\n  pic: number;\n}\n\ninterface Ar {\n  id: number;\n  name: string;\n  tns: any[];\n  alias: string[];\n  alia?: string[];\n}\n"
  },
  {
    "path": "src/renderer/types/singer.ts",
    "content": "export interface IHotSinger {\n  code: number;\n  more: boolean;\n  artists: Artist[];\n}\n\nexport interface Artist {\n  name: string;\n  id: number;\n  picId: number;\n  img1v1Id: number;\n  briefDesc: string;\n  picUrl: string;\n  img1v1Url: string;\n  albumSize: number;\n  alias: string[];\n  trans: string;\n  musicSize: number;\n  topicPerson: number;\n  showPrivateMsg?: any;\n  isSubed?: any;\n  accountId?: number;\n  picId_str?: string;\n  img1v1Id_str: string;\n  transNames?: string[];\n  followed: boolean;\n  mvSize?: any;\n  publishTime?: any;\n  identifyTag?: any;\n  alg?: any;\n  fansCount?: any;\n  cover?: string;\n  avatar?: string;\n}\n"
  },
  {
    "path": "src/renderer/types/user.ts",
    "content": "export interface IUserDetail {\n  level: number;\n  listenSongs: number;\n  userPoint: UserPoint;\n  mobileSign: boolean;\n  pcSign: boolean;\n  profile: Profile;\n  peopleCanSeeMyPlayRecord: boolean;\n  bindings: Binding[];\n  adValid: boolean;\n  code: number;\n  createTime: number;\n  createDays: number;\n  profileVillageInfo: ProfileVillageInfo;\n}\n\nexport interface IUserFollow {\n  followed: boolean;\n  follows: boolean;\n  nickname: string;\n  avatarUrl: string;\n  userId: number;\n  gender: number;\n  signature: string;\n  backgroundUrl: string;\n  vipType: number;\n  userType: number;\n  accountType: number;\n}\n\ninterface ProfileVillageInfo {\n  title: string;\n  imageUrl?: any;\n  targetUrl: string;\n}\n\ninterface Binding {\n  userId: number;\n  url: string;\n  expiresIn: number;\n  refreshTime: number;\n  bindingTime: number;\n  tokenJsonStr?: any;\n  expired: boolean;\n  id: number;\n  type: number;\n}\n\ninterface Profile {\n  avatarDetail?: any;\n  userId: number;\n  avatarImgIdStr: string;\n  backgroundImgIdStr: string;\n  description: string;\n  vipType: number;\n  userType: number;\n  createTime: number;\n  nickname: string;\n  avatarUrl: string;\n  experts: any;\n  expertTags?: any;\n  djStatus: number;\n  accountStatus: number;\n  birthday: number;\n  gender: number;\n  province: number;\n  city: number;\n  defaultAvatar: boolean;\n  avatarImgId: number;\n  backgroundImgId: number;\n  backgroundUrl: string;\n  mutual: boolean;\n  followed: boolean;\n  remarkName?: any;\n  authStatus: number;\n  detailDescription: string;\n  signature: string;\n  authority: number;\n  followeds: number;\n  follows: number;\n  blacklist: boolean;\n  eventCount: number;\n  allSubscribedCount: number;\n  playlistBeSubscribedCount: number;\n  avatarImgId_str: string;\n  followTime?: any;\n  followMe: boolean;\n  artistIdentity: any[];\n  cCount: number;\n  sDJPCount: number;\n  playlistCount: number;\n  sCount: number;\n  newFollows: number;\n}\n\ninterface UserPoint {\n  userId: number;\n  balance: number;\n  updateTime: number;\n  version: number;\n  status: number;\n  blockBalance: number;\n}\n"
  },
  {
    "path": "src/renderer/utils/appShortcuts.ts",
    "content": "import { onMounted, onUnmounted } from 'vue';\n\nimport i18n from '@/../i18n/renderer';\nimport { audioService } from '@/services/audioService';\nimport { usePlayerStore, useSettingsStore } from '@/store';\n\nimport { isElectron } from '.';\nimport { showShortcutToast } from './shortcutToast';\n\n// 添加一个简单的防抖机制\nlet actionTimeout: NodeJS.Timeout | null = null;\nconst ACTION_DELAY = 300; // 毫秒\n\n// 添加一个操作锁，记录最后一次操作的时间和动作\nlet lastActionInfo = {\n  action: '',\n  timestamp: 0\n};\n\ninterface ShortcutConfig {\n  key: string;\n  enabled: boolean;\n  scope: 'global' | 'app';\n}\n\ninterface ShortcutsConfig {\n  [key: string]: ShortcutConfig;\n}\n\nconst { t } = i18n.global;\n\n// 全局存储快捷键配置\nlet appShortcuts: ShortcutsConfig = {};\n\n/**\n * 处理快捷键动作\n * @param action 快捷键动作\n */\nexport async function handleShortcutAction(action: string) {\n  const now = Date.now();\n\n  // 如果存在未完成的动作，则忽略当前请求\n  if (actionTimeout) {\n    console.log('[AppShortcuts] 忽略快速连续的动作请求:', action);\n    return;\n  }\n\n  // 检查是否是同一个动作的重复触发（300ms内）\n  if (lastActionInfo.action === action && now - lastActionInfo.timestamp < ACTION_DELAY) {\n    console.log(\n      `[AppShortcuts] 忽略重复的 ${action} 动作，距上次仅 ${now - lastActionInfo.timestamp}ms`\n    );\n    return;\n  }\n\n  // 更新最后一次操作信息\n  lastActionInfo = {\n    action,\n    timestamp: now\n  };\n\n  // 设置防抖锁\n  actionTimeout = setTimeout(() => {\n    actionTimeout = null;\n  }, ACTION_DELAY);\n\n  console.log(`[AppShortcuts] 执行动作: ${action}, 时间戳: ${now}`);\n\n  const playerStore = usePlayerStore();\n  const settingsStore = useSettingsStore();\n\n  const showToast = (message: string, iconName: string) => {\n    if (settingsStore.isMiniMode) {\n      return;\n    }\n    showShortcutToast(message, iconName);\n  };\n\n  try {\n    switch (action) {\n      case 'togglePlay':\n        if (playerStore.play) {\n          await audioService.pause();\n          showToast(t('player.playBar.pause'), 'ri-pause-circle-line');\n        } else {\n          await audioService.getCurrentSound()?.play();\n          showToast(t('player.playBar.play'), 'ri-play-circle-line');\n        }\n        break;\n      case 'prevPlay':\n        await playerStore.prevPlay();\n        showToast(t('player.playBar.prev'), 'ri-skip-back-line');\n        break;\n      case 'nextPlay':\n        await playerStore.nextPlay();\n        showToast(t('player.playBar.next'), 'ri-skip-forward-line');\n        break;\n      case 'volumeUp':\n        if (playerStore.getVolume() < 1) {\n          const newVolume = playerStore.increaseVolume(0.1);\n          showToast(\n            `${t('player.playBar.volume')}${Math.round(newVolume * 100)}%`,\n            'ri-volume-up-line'\n          );\n        }\n        break;\n      case 'volumeDown':\n        if (playerStore.getVolume() > 0) {\n          const newVolume = playerStore.decreaseVolume(0.1);\n          showToast(\n            `${t('player.playBar.volume')}${Math.round(newVolume * 100)}%`,\n            'ri-volume-down-line'\n          );\n        }\n        break;\n      case 'toggleFavorite': {\n        const isFavorite = playerStore.favoriteList.includes(Number(playerStore.playMusic.id));\n        const numericId = Number(playerStore.playMusic.id);\n        console.log(`[AppShortcuts] toggleFavorite 当前状态: ${isFavorite}, ID: ${numericId}`);\n        if (isFavorite) {\n          playerStore.removeFromFavorite(numericId);\n          console.log(`[AppShortcuts] 已从收藏中移除: ${numericId}`);\n        } else {\n          playerStore.addToFavorite(numericId);\n          console.log(`[AppShortcuts] 已添加到收藏: ${numericId}`);\n        }\n        showToast(\n          isFavorite\n            ? t('player.playBar.unFavorite', { name: playerStore.playMusic.name })\n            : t('player.playBar.favorite', { name: playerStore.playMusic.name }),\n          isFavorite ? 'ri-heart-line' : 'ri-heart-fill'\n        );\n        break;\n      }\n      default:\n        console.log('未知的快捷键动作:', action);\n        break;\n    }\n  } catch (error) {\n    console.error(`执行快捷键动作 ${action} 时出错:`, error);\n  } finally {\n    // 确保在出错时也能清除超时\n    clearTimeout(actionTimeout);\n    actionTimeout = null;\n    console.log(\n      `[AppShortcuts] 动作完成: ${action}, 时间戳: ${Date.now()}, 耗时: ${Date.now() - now}ms`\n    );\n  }\n}\n\n/**\n * 检查按键是否匹配快捷键\n * @param e KeyboardEvent\n * @param shortcutKey 快捷键字符串\n * @returns 是否匹配\n */\nfunction matchShortcut(e: KeyboardEvent, shortcutKey: string): boolean {\n  const keys = shortcutKey.split('+');\n  const pressedKey = e.key.length === 1 ? e.key.toUpperCase() : e.key;\n\n  // 检查修饰键\n  const hasCommandOrControl = keys.includes('CommandOrControl');\n  const hasAlt = keys.includes('Alt');\n  const hasShift = keys.includes('Shift');\n\n  // 检查主键\n  let mainKey = keys.find((k) => !['CommandOrControl', 'Alt', 'Shift'].includes(k));\n  if (!mainKey) return false;\n\n  // 处理特殊键\n  if (mainKey === 'Left' && pressedKey === 'ArrowLeft') mainKey = 'ArrowLeft';\n  if (mainKey === 'Right' && pressedKey === 'ArrowRight') mainKey = 'ArrowRight';\n  if (mainKey === 'Up' && pressedKey === 'ArrowUp') mainKey = 'ArrowUp';\n  if (mainKey === 'Down' && pressedKey === 'ArrowDown') mainKey = 'ArrowDown';\n\n  // 检查是否所有条件都匹配\n  return (\n    hasCommandOrControl === (e.ctrlKey || e.metaKey) &&\n    hasAlt === e.altKey &&\n    hasShift === e.shiftKey &&\n    mainKey === pressedKey\n  );\n}\n\n/**\n * 全局键盘事件处理函数\n * @param e KeyboardEvent\n */\nfunction handleKeyDown(e: KeyboardEvent) {\n  // 如果在输入框中则不处理快捷键\n  if (['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) {\n    return;\n  }\n\n  Object.entries(appShortcuts).forEach(([action, config]) => {\n    if (config.enabled && config.scope === 'app' && matchShortcut(e, config.key)) {\n      e.preventDefault();\n      handleShortcutAction(action);\n    }\n  });\n}\n\n/**\n * 更新应用内快捷键\n * @param shortcuts 快捷键配置\n */\nexport function updateAppShortcuts(shortcuts: ShortcutsConfig) {\n  appShortcuts = shortcuts;\n}\n\n/**\n * 初始化应用内快捷键\n */\nexport function initAppShortcuts() {\n  if (isElectron) {\n    // 监听全局快捷键事件\n    window.electron.ipcRenderer.on('global-shortcut', async (_, action: string) => {\n      handleShortcutAction(action);\n    });\n\n    // 监听应用内快捷键更新\n    window.electron.ipcRenderer.on('update-app-shortcuts', (_, shortcuts: ShortcutsConfig) => {\n      updateAppShortcuts(shortcuts);\n    });\n\n    // 获取初始快捷键配置\n    const storedShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts');\n    if (storedShortcuts) {\n      updateAppShortcuts(storedShortcuts);\n    }\n\n    // 添加键盘事件监听\n    document.addEventListener('keydown', handleKeyDown);\n  }\n}\n\n/**\n * 清理应用内快捷键\n */\nexport function cleanupAppShortcuts() {\n  if (isElectron) {\n    // 移除全局事件监听\n    window.electron.ipcRenderer.removeAllListeners('global-shortcut');\n    window.electron.ipcRenderer.removeAllListeners('update-app-shortcuts');\n\n    // 移除键盘事件监听\n    document.removeEventListener('keydown', handleKeyDown);\n  }\n}\n\n/**\n * 使用应用内快捷键的组合函数\n */\nexport function useAppShortcuts() {\n  onMounted(() => {\n    initAppShortcuts();\n  });\n\n  onUnmounted(() => {\n    cleanupAppShortcuts();\n  });\n}\n"
  },
  {
    "path": "src/renderer/utils/auth.ts",
    "content": "/**\n * 登录状态管理工具\n *\n * 注意：这个工具主要用于在组件外部或 store 初始化之前检查登录状态。\n * 在组件内部，建议直接使用 userStore 中的状态。\n */\n\nexport interface LoginInfo {\n  isLoggedIn: boolean;\n  loginType: 'token' | 'cookie' | 'qr' | 'uid' | null;\n  hasToken: boolean;\n  hasUser: boolean;\n  user: any;\n}\n\n/**\n * 检查登录状态\n * @returns 登录信息对象\n */\nexport function checkLoginStatus(): LoginInfo {\n  const token = localStorage.getItem('token');\n  const userData = localStorage.getItem('user');\n  const loginType = localStorage.getItem('loginType') as LoginInfo['loginType'];\n  const uidLogin = localStorage.getItem('uidLogin');\n\n  const hasToken = !!token;\n  const hasUser = !!userData;\n  let user = null;\n\n  if (hasUser) {\n    try {\n      user = JSON.parse(userData);\n    } catch (error) {\n      console.error('解析用户数据失败:', error);\n    }\n  }\n\n  // 判断是否已登录\n  let isLoggedIn = false;\n\n  if (loginType === 'uid' || uidLogin === 'true') {\n    // UID登录：只需要有用户数据即可\n    isLoggedIn = hasUser && !!user;\n  } else {\n    // 其他登录方式：需要token和用户数据\n    isLoggedIn = hasToken && hasUser && !!user;\n  }\n\n  return {\n    isLoggedIn,\n    loginType,\n    hasToken,\n    hasUser,\n    user\n  };\n}\n\n/**\n * 检查是否有访问权限\n * @param requireAuth 是否需要真实登录权限（token）\n * @returns 是否有权限\n */\nexport function hasPermission(requireAuth: boolean = false): boolean {\n  const loginInfo = checkLoginStatus();\n\n  if (!loginInfo.isLoggedIn) {\n    return false;\n  }\n\n  // 如果需要真实登录权限，UID登录无法满足\n  if (requireAuth && loginInfo.loginType === 'uid') {\n    return false;\n  }\n\n  return true;\n}\n\n/**\n * 清除登录状态\n */\nexport function clearLoginStatus(): void {\n  localStorage.removeItem('token');\n  localStorage.removeItem('user');\n  localStorage.removeItem('loginType');\n  localStorage.removeItem('uidLogin');\n}\n\n/**\n * 设置登录状态（不包括用户数据，用户数据应通过 userStore.setUser 设置）\n * @param loginType 登录类型\n * @param token 登录token（可选）\n */\nexport function setLoginStatus(loginType: LoginInfo['loginType'], token?: string): void {\n  localStorage.setItem('loginType', loginType || '');\n\n  if (token) {\n    localStorage.setItem('token', token);\n  }\n\n  if (loginType === 'uid') {\n    localStorage.setItem('uidLogin', 'true');\n  }\n}\n\n/**\n * 获取登录错误信息\n * @param requireAuth 是否需要真实登录权限\n * @returns 错误信息\n */\nexport function getLoginErrorMessage(requireAuth: boolean = false): string {\n  const loginInfo = checkLoginStatus();\n\n  if (!loginInfo.isLoggedIn) {\n    return '请先登录';\n  }\n\n  if (requireAuth && loginInfo.loginType === 'uid') {\n    return 'UID登录无法访问此功能，请使用Cookie或二维码登录';\n  }\n\n  return '';\n}\n"
  },
  {
    "path": "src/renderer/utils/fileOperation.ts",
    "content": "import type { MessageApi } from 'naive-ui';\n\n/**\n * 选择目录\n * @param message MessageApi 实例\n * @returns Promise<string | undefined> 返回选择的目录路径，如果取消则返回 undefined\n */\nexport const selectDirectory = async (message: MessageApi): Promise<string | undefined> => {\n  try {\n    const result = await window.electron.ipcRenderer.invoke('select-directory');\n    if (result.filePaths?.[0]) {\n      return result.filePaths[0];\n    }\n  } catch (error) {\n    console.error('选择目录失败:', error);\n    message.error('选择目录失败');\n  }\n  return undefined;\n};\n\n/**\n * 打开目录\n * @param path 要打开的目录路径\n * @param message MessageApi 实例\n * @param showTip 是否显示提示信息\n */\nexport const openDirectory = (path: string | undefined, message: MessageApi, showTip = true) => {\n  if (path) {\n    window.electron.ipcRenderer.send('open-directory', path);\n  } else if (showTip) {\n    message.info('目录不存在');\n  }\n};\n"
  },
  {
    "path": "src/renderer/utils/index.ts",
    "content": "import { computed } from 'vue';\n\nimport { useSettingsStore } from '@/store/modules/settings';\n\n// 设置歌手背景图片\nexport const setBackgroundImg = (url: String) => {\n  return `background-image:url(${url})`;\n};\n// 设置动画类型\nexport const setAnimationClass = (type: String) => {\n  const settingsStore = useSettingsStore();\n  if (settingsStore.setData && settingsStore.setData.noAnimate) {\n    return '';\n  }\n  const speed = settingsStore.setData?.animationSpeed || 1;\n\n  let speedClass = '';\n  if (speed <= 0.3) speedClass = 'animate__slower';\n  else if (speed <= 0.8) speedClass = 'animate__slow';\n  else if (speed >= 2.5) speedClass = 'animate__faster';\n  else if (speed >= 1.5) speedClass = 'animate__fast';\n\n  return `animate__animated ${type}${speedClass ? ` ${speedClass}` : ''}`;\n};\n// 设置动画延时\nexport const setAnimationDelay = (index: number = 6, time: number = 50) => {\n  const settingsStore = useSettingsStore();\n  if (settingsStore.setData?.noAnimate) {\n    return '';\n  }\n  const speed = settingsStore.setData?.animationSpeed || 1;\n  return `animation-delay:${(index * time) / (speed * 2)}ms`;\n};\n\n// 将秒转换为分钟和秒\nexport const secondToMinute = (s: number) => {\n  if (!s) {\n    return '00:00';\n  }\n  const minute: number = Math.floor(s / 60);\n  const second: number = Math.floor(s % 60);\n  const minuteStr: string = minute > 9 ? minute.toString() : `0${minute.toString()}`;\n  const secondStr: string = second > 9 ? second.toString() : `0${second.toString()}`;\n  return `${minuteStr}:${secondStr}`;\n};\n\n// 格式化数字 千,万, 百万, 千万,亿\nconst units = [\n  { value: 1e8, symbol: '亿' },\n  { value: 1e4, symbol: '万' }\n];\n\nexport const formatNumber = (num: string | number) => {\n  num = Number(num);\n  for (let i = 0; i < units.length; i++) {\n    if (num >= units[i].value) {\n      return `${(num / units[i].value).toFixed(1)}${units[i].symbol}`;\n    }\n  }\n  return num.toString();\n};\n\nexport const getImgUrl = (url: string | undefined, size: string = '') => {\n  if (!url) return '';\n\n  if (url.includes('thumbnail')) {\n    // 只替换最后一个 thumbnail 参数的尺寸\n    return url.replace(/thumbnail=\\d+y\\d+(?!.*thumbnail)/, `thumbnail=${size}`);\n  }\n\n  const imgUrl = `${url}?param=${size}`;\n  return imgUrl;\n};\n\nexport const isMobile = computed(() => {\n  const settingsStore = useSettingsStore();\n  return settingsStore.isMobile;\n});\n\nexport const isElectron = (window as any).electron !== undefined;\n\nexport const isLyricWindow = computed(() => {\n  return window.location.hash.includes('lyric');\n});\n\nexport const getSetData = (): any => {\n  let setData = null;\n  if (window.electron) {\n    setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set');\n  } else {\n    const settingsStore = useSettingsStore();\n    setData = settingsStore.setData;\n  }\n  return setData;\n};\n"
  },
  {
    "path": "src/renderer/utils/linearColor.ts",
    "content": "import { useDebounceFn } from '@vueuse/core';\nimport tinycolor from 'tinycolor2';\n\ninterface IColor {\n  backgroundColor: string;\n  primaryColor: string;\n}\n\ninterface ITextColors {\n  primary: string;\n  active: string;\n  theme: string;\n}\n\nexport interface LyricThemeColor {\n  id: string;\n  name: string;\n  light: string;\n  dark: string;\n}\n\ninterface LyricSettings {\n  isTop: boolean;\n  theme: 'light' | 'dark';\n  isLock: boolean;\n  highlightColor?: string;\n}\n\nexport const getImageLinearBackground = async (imageSrc: string): Promise<IColor> => {\n  try {\n    const primaryColor = await getImagePrimaryColor(imageSrc);\n    return {\n      backgroundColor: generateGradientBackground(primaryColor),\n      primaryColor\n    };\n  } catch (error) {\n    console.error('error', error);\n    return {\n      backgroundColor: '',\n      primaryColor: ''\n    };\n  }\n};\n\nexport const getImageBackground = async (img: HTMLImageElement): Promise<IColor> => {\n  try {\n    const primaryColor = await getImageColor(img);\n    return {\n      backgroundColor: generateGradientBackground(primaryColor),\n      primaryColor\n    };\n  } catch (error) {\n    console.error('error', error);\n    return {\n      backgroundColor: '',\n      primaryColor: ''\n    };\n  }\n};\n\nconst getImageColor = (img: HTMLImageElement): Promise<string> => {\n  return new Promise((resolve, reject) => {\n    const canvas = document.createElement('canvas');\n    const ctx = canvas.getContext('2d');\n    if (!ctx) {\n      reject(new Error('无法获取canvas上下文'));\n      return;\n    }\n\n    canvas.width = img.width;\n    canvas.height = img.height;\n    ctx.drawImage(img, 0, 0, canvas.width, canvas.height);\n\n    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n    const color = getAverageColor(imageData.data);\n    resolve(`rgb(${color.join(',')})`);\n  });\n};\n\nconst getImagePrimaryColor = (imageSrc: string): Promise<string> => {\n  return new Promise((resolve, reject) => {\n    const img = new Image();\n    img.crossOrigin = 'Anonymous';\n    img.src = imageSrc;\n\n    img.onload = () => {\n      const canvas = document.createElement('canvas');\n      const ctx = canvas.getContext('2d');\n      if (!ctx) {\n        reject(new Error('无法获取canvas上下文'));\n        return;\n      }\n\n      canvas.width = img.width;\n      canvas.height = img.height;\n      ctx.drawImage(img, 0, 0, canvas.width, canvas.height);\n\n      const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);\n      const color = getAverageColor(imageData.data);\n      resolve(`rgb(${color.join(',')})`);\n    };\n\n    img.onerror = () => reject(new Error('图片加载失败'));\n  });\n};\n\nconst getAverageColor = (data: Uint8ClampedArray): number[] => {\n  let r = 0;\n  let g = 0;\n  let b = 0;\n  let count = 0;\n  for (let i = 0; i < data.length; i += 4) {\n    r += data[i];\n    g += data[i + 1];\n    b += data[i + 2];\n    count++;\n  }\n  return [Math.round(r / count), Math.round(g / count), Math.round(b / count)];\n};\n\nconst generateGradientBackground = (color: string): string => {\n  const tc = tinycolor(color);\n  const hsl = tc.toHsl();\n\n  // 增加亮度和暗度的差异\n  const lightColor = tinycolor({ h: hsl.h, s: hsl.s * 0.8, l: Math.min(hsl.l + 0.2, 0.95) });\n  const midColor = tinycolor({ h: hsl.h, s: hsl.s, l: hsl.l });\n  const darkColor = tinycolor({\n    h: hsl.h,\n    s: Math.min(hsl.s * 1.2, 1),\n    l: Math.max(hsl.l - 0.3, 0.05)\n  });\n\n  return `linear-gradient(to bottom, ${lightColor.toRgbString()} 0%, ${midColor.toRgbString()} 50%, ${darkColor.toRgbString()} 100%)`;\n};\n\nexport const parseGradient = (gradientStr: string) => {\n  if (!gradientStr) return [];\n\n  // 处理非渐变色\n  if (!gradientStr.startsWith('linear-gradient')) {\n    const color = tinycolor(gradientStr);\n    if (color.isValid()) {\n      const rgb = color.toRgb();\n      return [{ r: rgb.r, g: rgb.g, b: rgb.b }];\n    }\n    return [];\n  }\n\n  // 处理渐变色，支持 rgb、rgba 和十六进制颜色\n  const colorMatches = gradientStr.match(/(?:(?:rgb|rgba)\\([^)]+\\)|#[0-9a-fA-F]{3,8})/g) || [];\n  return colorMatches.map((color) => {\n    const tc = tinycolor(color);\n    const rgb = tc.toRgb();\n    return { r: rgb.r, g: rgb.g, b: rgb.b };\n  });\n};\n\nexport const getTextColors = (gradient: string = ''): ITextColors => {\n  const defaultColors = {\n    primary: 'rgba(255, 255, 255, 0.54)',\n    active: '#ffffff',\n    theme: 'light'\n  };\n\n  if (!gradient) return defaultColors;\n\n  const colors = parseGradient(gradient);\n  if (!colors.length) return defaultColors;\n\n  const mainColor = colors.length === 1 ? colors[0] : colors[1] || colors[0];\n  const tc = tinycolor(mainColor);\n  const isDark = tc.getBrightness() > 155; // tinycolor 的亮度范围是 0-255\n\n  return {\n    primary: isDark ? 'rgba(0, 0, 0, 0.54)' : 'rgba(255, 255, 255, 0.54)',\n    active: isDark ? '#000000' : '#ffffff',\n    theme: isDark ? 'dark' : 'light'\n  };\n};\n\nexport const getHoverBackgroundColor = (isDark: boolean): string => {\n  return isDark ? 'rgba(0, 0, 0, 0.08)' : 'rgba(255, 255, 255, 0.08)';\n};\n\nexport const animateGradient = (() => {\n  let currentAnimation: number | null = null;\n  let isAnimating = false;\n  let lastProgress = 0;\n\n  const validateColors = (colors: ReturnType<typeof parseGradient>) => {\n    return colors.every(\n      (color) =>\n        typeof color.r === 'number' &&\n        typeof color.g === 'number' &&\n        typeof color.b === 'number' &&\n        !Number.isNaN(color.r) &&\n        !Number.isNaN(color.g) &&\n        !Number.isNaN(color.b)\n    );\n  };\n\n  const easeInOutCubic = (x: number): number => {\n    return x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2;\n  };\n\n  const animate = (\n    oldGradient: string,\n    newGradient: string,\n    onUpdate: (gradient: string) => void,\n    duration = 300\n  ) => {\n    // 如果新旧渐变色相同，不执行动画\n    if (oldGradient === newGradient) {\n      return null;\n    }\n\n    // 如果正在动画中，取消当前动画\n    if (currentAnimation !== null) {\n      cancelAnimationFrame(currentAnimation);\n      currentAnimation = null;\n    }\n\n    // 解析颜色\n    const startColors = parseGradient(oldGradient);\n    const endColors = parseGradient(newGradient);\n\n    // 验证颜色数组\n    if (\n      !startColors.length ||\n      !endColors.length ||\n      !validateColors(startColors) ||\n      !validateColors(endColors)\n    ) {\n      console.warn('Invalid color values detected');\n      onUpdate(newGradient); // 直接更新到目标颜色\n      return null;\n    }\n\n    // 如果颜色数量不匹配，直接更新到目标颜色\n    if (startColors.length !== endColors.length) {\n      onUpdate(newGradient);\n      return null;\n    }\n\n    isAnimating = true;\n    const startTime = performance.now();\n\n    const animateFrame = (currentTime: number) => {\n      if (!isAnimating) return null;\n\n      const elapsed = currentTime - startTime;\n      const rawProgress = Math.min(elapsed / duration, 1);\n      // 使用缓动函数使动画更平滑\n      const progress = easeInOutCubic(rawProgress);\n\n      try {\n        // 使用上一帧的进度来平滑过渡\n        const effectiveProgress = lastProgress + (progress - lastProgress) * 0.6;\n        lastProgress = effectiveProgress;\n\n        const currentColors = startColors.map((startColor, i) => {\n          const start = tinycolor(startColor);\n          const end = tinycolor(endColors[i]);\n          return tinycolor.mix(start, end, effectiveProgress * 100);\n        });\n\n        const gradientString = createGradientString(\n          currentColors.map((c) => {\n            const rgb = c.toRgb();\n            return { r: rgb.r, g: rgb.g, b: rgb.b };\n          })\n        );\n\n        onUpdate(gradientString);\n\n        if (rawProgress < 1) {\n          currentAnimation = requestAnimationFrame(animateFrame);\n          return currentAnimation;\n        }\n        // 确保最终颜色正确\n        onUpdate(newGradient);\n        isAnimating = false;\n        currentAnimation = null;\n        lastProgress = 0;\n        return null;\n      } catch (error) {\n        console.error('Animation error:', error);\n        onUpdate(newGradient);\n        isAnimating = false;\n        currentAnimation = null;\n        lastProgress = 0;\n        return null;\n      }\n    };\n\n    currentAnimation = requestAnimationFrame(animateFrame);\n    return currentAnimation;\n  };\n\n  // 使用更短的防抖时间\n  return useDebounceFn(animate, 50);\n})();\n\nexport const createGradientString = (\n  colors: { r: number; g: number; b: number }[],\n  percentages = [0, 50, 100]\n) => {\n  return `linear-gradient(to bottom, ${colors\n    .map((color, i) => `rgb(${color.r}, ${color.g}, ${color.b}) ${percentages[i]}%`)\n    .join(', ')})`;\n};\n\n// ===== 歌词主题色相关工具函数 =====\n\n/**\n * 预设歌词主题色配置\n * 注意：name 字段将通过国际化系统动态获取，这里的值仅作为后备\n */\nconst PRESET_LYRIC_COLORS: LyricThemeColor[] = [\n  {\n    id: 'spotify-green',\n    name: 'Spotify Green', // 后备名称，实际使用时会被国际化替换\n    light: '#1db954',\n    dark: '#1ed760'\n  },\n  {\n    id: 'apple-blue',\n    name: 'Apple Blue',\n    light: '#007aff',\n    dark: '#0a84ff'\n  },\n  {\n    id: 'youtube-red',\n    name: 'YouTube Red',\n    light: '#ff0000',\n    dark: '#ff4444'\n  },\n  {\n    id: 'orange',\n    name: 'Vibrant Orange',\n    light: '#ff6b35',\n    dark: '#ff8c42'\n  },\n  {\n    id: 'purple',\n    name: 'Mystic Purple',\n    light: '#8b5cf6',\n    dark: '#a78bfa'\n  },\n  {\n    id: 'pink',\n    name: 'Cherry Pink',\n    light: '#ec4899',\n    dark: '#f472b6'\n  }\n];\n\n/**\n * 验证颜色是否有效\n */\nexport const validateColor = (color: string): boolean => {\n  if (!color || typeof color !== 'string') return false;\n  const tc = tinycolor(color);\n  return tc.isValid() && tc.getAlpha() > 0;\n};\n\n/**\n * 检查颜色对比度是否符合可读性标准\n */\nexport const validateColorContrast = (color: string, theme: 'light' | 'dark'): boolean => {\n  if (!validateColor(color)) return false;\n\n  const backgroundColor = theme === 'dark' ? '#000000' : '#ffffff';\n  const contrast = tinycolor.readability(color, backgroundColor);\n  return contrast >= 4.5; // WCAG AA 标准\n};\n\n/**\n * 为特定主题优化颜色\n */\nexport const optimizeColorForTheme = (color: string, theme: 'light' | 'dark'): string => {\n  if (!validateColor(color)) {\n    return getDefaultHighlightColor(theme);\n  }\n\n  const tc = tinycolor(color);\n  const hsl = tc.toHsl();\n\n  if (theme === 'dark') {\n    // 暗色主题：增加亮度和饱和度\n    const optimized = tinycolor({\n      h: hsl.h,\n      s: Math.min(hsl.s * 1.1, 1),\n      l: Math.max(hsl.l, 0.4) // 确保最小亮度\n    });\n\n    // 检查对比度，如果不够则进一步调整\n    if (!validateColorContrast(optimized.toHexString(), theme)) {\n      return tinycolor({\n        h: hsl.h,\n        s: Math.min(hsl.s * 1.2, 1),\n        l: Math.max(hsl.l * 1.3, 0.5)\n      }).toHexString();\n    }\n\n    return optimized.toHexString();\n  } else {\n    // 亮色主题：适当降低亮度\n    const optimized = tinycolor({\n      h: hsl.h,\n      s: Math.min(hsl.s * 1.05, 1),\n      l: Math.min(hsl.l, 0.6) // 确保最大亮度\n    });\n\n    // 检查对比度\n    if (!validateColorContrast(optimized.toHexString(), theme)) {\n      return tinycolor({\n        h: hsl.h,\n        s: Math.min(hsl.s * 1.1, 1),\n        l: Math.min(hsl.l * 0.8, 0.5)\n      }).toHexString();\n    }\n\n    return optimized.toHexString();\n  }\n};\n\n/**\n * 获取默认高亮颜色\n */\nexport const getDefaultHighlightColor = (theme?: 'light' | 'dark'): string => {\n  const defaultColor = PRESET_LYRIC_COLORS[0]; // Spotify 绿\n  if (!theme) return defaultColor.light;\n  return theme === 'dark' ? defaultColor.dark : defaultColor.light;\n};\n\n/**\n * 获取预设主题色列表\n */\nexport const getLyricThemeColors = (): LyricThemeColor[] => {\n  return [...PRESET_LYRIC_COLORS];\n};\n\n/**\n * 根据主题获取预设颜色的实际值\n */\nexport const getPresetColorValue = (colorId: string, theme: 'light' | 'dark'): string => {\n  const color = PRESET_LYRIC_COLORS.find((c) => c.id === colorId);\n  if (!color) return getDefaultHighlightColor(theme);\n  return theme === 'dark' ? color.dark : color.light;\n};\n\n/**\n * 安全加载歌词设置\n */\nconst safeLoadLyricSettings = (): LyricSettings => {\n  try {\n    const stored = localStorage.getItem('lyricData');\n    if (stored) {\n      const parsed = JSON.parse(stored) as LyricSettings;\n\n      // 验证 highlightColor 字段\n      if (parsed.highlightColor && !validateColor(parsed.highlightColor)) {\n        console.warn('Invalid stored highlight color, removing it');\n        delete parsed.highlightColor;\n      }\n\n      return parsed;\n    }\n  } catch (error) {\n    console.error('Failed to load lyric settings:', error);\n  }\n\n  // 返回默认设置\n  return {\n    isTop: false,\n    theme: 'dark',\n    isLock: false\n  };\n};\n\n/**\n * 安全保存歌词设置\n */\nconst safeSaveLyricSettings = (settings: LyricSettings): void => {\n  try {\n    localStorage.setItem('lyricData', JSON.stringify(settings));\n  } catch (error) {\n    console.error('Failed to save lyric settings:', error);\n  }\n};\n\n/**\n * 保存歌词主题色\n */\nexport const saveLyricThemeColor = (color: string): void => {\n  if (!validateColor(color)) {\n    console.warn('Attempted to save invalid color:', color);\n    return;\n  }\n\n  const settings = safeLoadLyricSettings();\n  settings.highlightColor = color;\n  safeSaveLyricSettings(settings);\n};\n\n/**\n * 加载歌词主题色\n */\nexport const loadLyricThemeColor = (): string => {\n  const settings = safeLoadLyricSettings();\n\n  if (settings.highlightColor && validateColor(settings.highlightColor)) {\n    return settings.highlightColor;\n  }\n\n  // 如果没有保存的颜色或颜色无效，返回默认颜色\n  return getDefaultHighlightColor(settings.theme);\n};\n\n/**\n * 重置歌词主题色到默认值\n */\nexport const resetLyricThemeColor = (): void => {\n  const settings = safeLoadLyricSettings();\n  delete settings.highlightColor;\n  safeSaveLyricSettings(settings);\n};\n\n/**\n * 获取当前有效的歌词主题色\n */\nexport const getCurrentLyricThemeColor = (theme: 'light' | 'dark'): string => {\n  const savedColor = loadLyricThemeColor();\n\n  if (savedColor && validateColor(savedColor)) {\n    return optimizeColorForTheme(savedColor, theme);\n  }\n\n  return getDefaultHighlightColor(theme);\n};\n"
  },
  {
    "path": "src/renderer/utils/lxCrypto.ts",
    "content": "/**\n * 落雪音乐加密工具\n * 实现 lx.utils.crypto API\n *\n * 提供 MD5、AES、RSA 等加密功能\n */\n\nimport CryptoJS from 'crypto-js';\nimport { JSEncrypt } from 'jsencrypt';\n\n/**\n * MD5 哈希\n */\nexport const md5 = (str: string): string => {\n  return CryptoJS.MD5(str).toString();\n};\n\n/**\n * 生成随机字节（返回16进制字符串）\n */\nexport const randomBytes = (size: number): string => {\n  const array = new Uint8Array(size);\n  crypto.getRandomValues(array);\n  return Array.from(array)\n    .map((b) => b.toString(16).padStart(2, '0'))\n    .join('');\n};\n\n/**\n * AES 加密\n *\n * @param buffer - 要加密的数据（字符串或 Buffer）\n * @param mode - 加密模式（如 'cbc'）\n * @param key - 密钥（字符串或 WordArray）\n * @param iv - 初始化向量（字符串或 WordArray）\n * @returns 加密后的 Buffer（Uint8Array）\n */\nexport const aesEncrypt = (\n  buffer: string | Uint8Array,\n  mode: string,\n  key: string | CryptoJS.lib.WordArray,\n  iv: string | CryptoJS.lib.WordArray\n): Uint8Array => {\n  try {\n    // 将输入转换为 WordArray\n    let wordArray: CryptoJS.lib.WordArray;\n    if (typeof buffer === 'string') {\n      wordArray = CryptoJS.enc.Utf8.parse(buffer);\n    } else {\n      // Uint8Array 转 WordArray\n      const words: number[] = [];\n      for (let i = 0; i < buffer.length; i += 4) {\n        words.push(\n          ((buffer[i] || 0) << 24) |\n            ((buffer[i + 1] || 0) << 16) |\n            ((buffer[i + 2] || 0) << 8) |\n            (buffer[i + 3] || 0)\n        );\n      }\n      wordArray = CryptoJS.lib.WordArray.create(words, buffer.length);\n    }\n\n    // 处理密钥和 IV\n    const keyWordArray = typeof key === 'string' ? CryptoJS.enc.Utf8.parse(key) : key;\n    const ivWordArray = typeof iv === 'string' ? CryptoJS.enc.Utf8.parse(iv) : iv;\n\n    // 根据模式选择加密方式\n    const modeObj = getModeFromString(mode);\n\n    // 执行加密\n    const encrypted = CryptoJS.AES.encrypt(wordArray, keyWordArray, {\n      iv: ivWordArray,\n      mode: modeObj,\n      padding: CryptoJS.pad.Pkcs7\n    });\n\n    // 将结果转换为 Uint8Array\n    const ciphertext = encrypted.ciphertext;\n    const result = new Uint8Array(ciphertext.words.length * 4);\n    for (let i = 0; i < ciphertext.words.length; i++) {\n      const word = ciphertext.words[i];\n      result[i * 4] = (word >>> 24) & 0xff;\n      result[i * 4 + 1] = (word >>> 16) & 0xff;\n      result[i * 4 + 2] = (word >>> 8) & 0xff;\n      result[i * 4 + 3] = word & 0xff;\n    }\n\n    return result.slice(0, ciphertext.sigBytes);\n  } catch (error) {\n    console.error('[lxCrypto] AES 加密失败:', error);\n    throw error;\n  }\n};\n\n/**\n * AES 解密\n */\nexport const aesDecrypt = (\n  buffer: Uint8Array,\n  mode: string,\n  key: string | CryptoJS.lib.WordArray,\n  iv: string | CryptoJS.lib.WordArray\n): Uint8Array => {\n  try {\n    // Uint8Array 转 WordArray\n    const words: number[] = [];\n    for (let i = 0; i < buffer.length; i += 4) {\n      words.push(\n        ((buffer[i] || 0) << 24) |\n          ((buffer[i + 1] || 0) << 16) |\n          ((buffer[i + 2] || 0) << 8) |\n          (buffer[i + 3] || 0)\n      );\n    }\n    const ciphertext = CryptoJS.lib.WordArray.create(words, buffer.length);\n\n    // 处理密钥和 IV\n    const keyWordArray = typeof key === 'string' ? CryptoJS.enc.Utf8.parse(key) : key;\n    const ivWordArray = typeof iv === 'string' ? CryptoJS.enc.Utf8.parse(iv) : iv;\n\n    // 根据模式选择解密方式\n    const modeObj = getModeFromString(mode);\n\n    // 构造加密对象\n    const cipherParams = CryptoJS.lib.CipherParams.create({\n      ciphertext\n    });\n\n    // 执行解密\n    const decrypted = CryptoJS.AES.decrypt(cipherParams, keyWordArray, {\n      iv: ivWordArray,\n      mode: modeObj,\n      padding: CryptoJS.pad.Pkcs7\n    });\n\n    // 转换为 Uint8Array\n    const result = new Uint8Array(decrypted.words.length * 4);\n    for (let i = 0; i < decrypted.words.length; i++) {\n      const word = decrypted.words[i];\n      result[i * 4] = (word >>> 24) & 0xff;\n      result[i * 4 + 1] = (word >>> 16) & 0xff;\n      result[i * 4 + 2] = (word >>> 8) & 0xff;\n      result[i * 4 + 3] = word & 0xff;\n    }\n\n    return result.slice(0, decrypted.sigBytes);\n  } catch (error) {\n    console.error('[lxCrypto] AES 解密失败:', error);\n    throw error;\n  }\n};\n\n/**\n * RSA 加密\n *\n * @param buffer - 要加密的数据\n * @param publicKey - RSA 公钥（PEM 格式）\n * @returns 加密后的数据（Uint8Array）\n */\nexport const rsaEncrypt = (buffer: string | Uint8Array, publicKey: string): Uint8Array => {\n  try {\n    const encrypt = new JSEncrypt();\n    encrypt.setPublicKey(publicKey);\n\n    // 转换输入为字符串\n    let input: string;\n    if (typeof buffer === 'string') {\n      input = buffer;\n    } else {\n      // Uint8Array 转字符串\n      input = new TextDecoder().decode(buffer);\n    }\n\n    // 执行加密（返回 base64）\n    const encrypted = encrypt.encrypt(input);\n    if (!encrypted) {\n      throw new Error('RSA encryption failed');\n    }\n\n    // Base64 解码为 Uint8Array\n    const binaryString = atob(encrypted);\n    const result = new Uint8Array(binaryString.length);\n    for (let i = 0; i < binaryString.length; i++) {\n      result[i] = binaryString.charCodeAt(i);\n    }\n\n    return result;\n  } catch (error) {\n    console.error('[lxCrypto] RSA 加密失败:', error);\n    throw error;\n  }\n};\n\n/**\n * RSA 解密\n */\nexport const rsaDecrypt = (buffer: Uint8Array, privateKey: string): Uint8Array => {\n  try {\n    const decrypt = new JSEncrypt();\n    decrypt.setPrivateKey(privateKey);\n\n    // Uint8Array 转 Base64\n    let binaryString = '';\n    for (let i = 0; i < buffer.length; i++) {\n      binaryString += String.fromCharCode(buffer[i]);\n    }\n    const base64 = btoa(binaryString);\n\n    // 执行解密\n    const decrypted = decrypt.decrypt(base64);\n    if (!decrypted) {\n      throw new Error('RSA decryption failed');\n    }\n\n    // 字符串转 Uint8Array\n    return new TextEncoder().encode(decrypted);\n  } catch (error) {\n    console.error('[lxCrypto] RSA 解密失败:', error);\n    throw error;\n  }\n};\n\n/**\n * 从字符串获取加密模式\n */\nconst getModeFromString = (mode: string): CryptoJS.lib.Mode => {\n  const modeStr = mode.toLowerCase();\n  switch (modeStr) {\n    case 'cbc':\n      return CryptoJS.mode.CBC;\n    case 'cfb':\n      return CryptoJS.mode.CFB;\n    case 'ctr':\n      return CryptoJS.mode.CTR;\n    case 'ofb':\n      return CryptoJS.mode.OFB;\n    case 'ecb':\n      return CryptoJS.mode.ECB;\n    default:\n      console.warn(`[lxCrypto] 未知的加密模式: ${mode}, 使用 CBC`);\n      return CryptoJS.mode.CBC;\n  }\n};\n\n/**\n * SHA1 哈希\n */\nexport const sha1 = (str: string): string => {\n  return CryptoJS.SHA1(str).toString();\n};\n\n/**\n * SHA256 哈希\n */\nexport const sha256 = (str: string): string => {\n  return CryptoJS.SHA256(str).toString();\n};\n\n/**\n * Base64 编码\n */\nexport const base64Encode = (data: string | Uint8Array): string => {\n  if (typeof data === 'string') {\n    return btoa(data);\n  } else {\n    let binary = '';\n    for (let i = 0; i < data.length; i++) {\n      binary += String.fromCharCode(data[i]);\n    }\n    return btoa(binary);\n  }\n};\n\n/**\n * Base64 解码\n */\nexport const base64Decode = (str: string): Uint8Array => {\n  const binaryString = atob(str);\n  const result = new Uint8Array(binaryString.length);\n  for (let i = 0; i < binaryString.length; i++) {\n    result[i] = binaryString.charCodeAt(i);\n  }\n  return result;\n};\n"
  },
  {
    "path": "src/renderer/utils/playerUtils.ts",
    "content": "import type { SongResult } from '@/types/music';\n\n/**\n * 从 localStorage 获取项目，带类型安全和错误处理\n */\nexport function getLocalStorageItem<T>(key: string, defaultValue: T): T {\n  try {\n    const item = localStorage.getItem(key);\n    return item ? JSON.parse(item) : defaultValue;\n  } catch {\n    return defaultValue;\n  }\n}\n\n/**\n * 设置 localStorage 项目，自动序列化\n */\nexport function setLocalStorageItem<T>(key: string, value: T): void {\n  try {\n    localStorage.setItem(key, JSON.stringify(value));\n  } catch (error) {\n    console.error(`Failed to save to localStorage: ${key}`, error);\n  }\n}\n\n/**\n * 比较B站视频ID的辅助函数\n */\nexport const isBilibiliIdMatch = (id1: string | number, id2: string | number): boolean => {\n  const str1 = String(id1);\n  const str2 = String(id2);\n\n  // 如果两个ID都不包含--分隔符，直接比较\n  if (!str1.includes('--') && !str2.includes('--')) {\n    return str1 === str2;\n  }\n\n  // 处理B站视频ID\n  if (str1.includes('--') || str2.includes('--')) {\n    // 尝试从ID中提取bvid和cid\n    const extractBvIdAndCid = (str: string) => {\n      if (!str.includes('--')) return { bvid: '', cid: '' };\n      const parts = str.split('--');\n      if (parts.length >= 3) {\n        // bvid--pid--cid格式\n        return { bvid: parts[0], cid: parts[2] };\n      } else if (parts.length === 2) {\n        // 旧格式或其他格式\n        return { bvid: '', cid: parts[1] };\n      }\n      return { bvid: '', cid: '' };\n    };\n\n    const { bvid: bvid1, cid: cid1 } = extractBvIdAndCid(str1);\n    const { bvid: bvid2, cid: cid2 } = extractBvIdAndCid(str2);\n\n    // 如果两个ID都有bvid，比较bvid和cid\n    if (bvid1 && bvid2) {\n      return bvid1 === bvid2 && cid1 === cid2;\n    }\n\n    // 其他情况，只比较cid部分\n    if (cid1 && cid2) {\n      return cid1 === cid2;\n    }\n  }\n\n  // 默认情况，直接比较完整ID\n  return str1 === str2;\n};\n\n/**\n * Fisher-Yates 洗牌算法\n * @param list 歌曲列表\n * @param currentSong 当前歌曲（会被放在第一位）\n */\nexport const performShuffle = (list: SongResult[], currentSong?: SongResult): SongResult[] => {\n  if (list.length <= 1) return [...list];\n\n  const result: SongResult[] = [];\n  const remainingSongs = [...list];\n\n  // 如果指定了当前歌曲，先把它放在第一位\n  if (currentSong && currentSong.id) {\n    const currentSongIndex = remainingSongs.findIndex((song) => song.id === currentSong.id);\n    if (currentSongIndex !== -1) {\n      // 把当前歌曲放在第一位\n      result.push(remainingSongs.splice(currentSongIndex, 1)[0]);\n    }\n  }\n\n  // 对剩余歌曲进行洗牌\n  if (remainingSongs.length > 0) {\n    // Fisher-Yates 洗牌算法\n    for (let i = remainingSongs.length - 1; i > 0; i--) {\n      const j = Math.floor(Math.random() * (i + 1));\n      [remainingSongs[i], remainingSongs[j]] = [remainingSongs[j], remainingSongs[i]];\n    }\n\n    // 把洗牌后的歌曲添加到结果中\n    result.push(...remainingSongs);\n  }\n\n  return result;\n};\n\n/**\n * 预加载封面图片\n */\nexport const preloadCoverImage = (\n  picUrl: string,\n  getImgUrl: (url: string, size: string) => string\n) => {\n  if (!picUrl) return;\n\n  try {\n    const imageUrl = getImgUrl(picUrl, '500y500');\n    console.log('预加载封面图片:', imageUrl);\n\n    // 创建一个 Image 对象来预加载图片\n    const img = new Image();\n    img.src = imageUrl;\n\n    // 可选：添加加载完成和错误的回调\n    img.onload = () => {\n      console.log('封面图片预加载成功:', imageUrl);\n    };\n\n    img.onerror = () => {\n      console.error('封面图片预加载失败:', imageUrl);\n    };\n  } catch (error) {\n    console.error('预加载封面图片出错:', error);\n  }\n};\n"
  },
  {
    "path": "src/renderer/utils/request.ts",
    "content": "import axios, { InternalAxiosRequestConfig } from 'axios';\n\nimport { useUserStore } from '@/store/modules/user';\n\nimport { getSetData, isElectron, isMobile } from '.';\n\nlet setData: any = null;\n\n// 扩展请求配置接口\ninterface CustomAxiosRequestConfig extends InternalAxiosRequestConfig {\n  retryCount?: number;\n  noRetry?: boolean;\n}\n\nconst baseURL = window.electron\n  ? `http://127.0.0.1:${setData?.musicApiPort}`\n  : import.meta.env.VITE_API;\n\nconst request = axios.create({\n  baseURL,\n  timeout: 15000,\n  withCredentials: true\n});\n\n// 最大重试次数\nconst MAX_RETRIES = 1;\n// 重试延迟（毫秒）\nconst RETRY_DELAY = 500;\n\n// 请求拦截器\nrequest.interceptors.request.use(\n  (config: CustomAxiosRequestConfig) => {\n    setData = getSetData();\n    config.baseURL = window.electron\n      ? `http://127.0.0.1:${setData?.musicApiPort}`\n      : import.meta.env.VITE_API;\n    // 只在retryCount未定义时初始化为0\n    if (config.retryCount === undefined) {\n      config.retryCount = 0;\n    }\n\n    // 在请求发送之前做一些处理\n    // 在get请求params中添加timestamp\n    config.params = {\n      ...config.params,\n      timestamp: Date.now(),\n      device: isElectron ? 'pc' : isMobile ? 'mobile' : 'web'\n    };\n    const token = localStorage.getItem('token');\n    if (token && config.method !== 'post') {\n      config.params.cookie = config.params.cookie !== undefined ? config.params.cookie : token;\n    } else if (token && config.method === 'post') {\n      config.data = {\n        ...config.data,\n        cookie: token\n      };\n    }\n    if (isElectron) {\n      const proxyConfig = setData?.proxyConfig;\n      if (proxyConfig?.enable && ['http', 'https'].includes(proxyConfig?.protocol)) {\n        config.params.proxy = `${proxyConfig.protocol}://${proxyConfig.host}:${proxyConfig.port}`;\n      }\n      if (setData.enableRealIP && setData.realIP) {\n        config.params.realIP = setData.realIP;\n      }\n    }\n\n    return config;\n  },\n  (error) => {\n    // 当请求异常时做一些处理\n    return Promise.reject(error);\n  }\n);\n\nconst NO_RETRY_URLS = ['暂时没有'];\n\n// 响应拦截器\nrequest.interceptors.response.use(\n  (response) => {\n    return response;\n  },\n  async (error) => {\n    console.error('error', error);\n    const config = error.config as CustomAxiosRequestConfig;\n\n    // 如果没有配置，直接返回错误\n    if (!config) {\n      return Promise.reject(error);\n    }\n\n    // 处理 301 状态码\n    if (error.response?.status === 301 && config.params.noLogin !== true) {\n      // 使用 store mutation 清除用户信息\n      const userStore = useUserStore();\n      userStore.handleLogout();\n      console.log(`301 状态码，清除登录信息后重试第 ${config.retryCount} 次`);\n      config.retryCount = 3;\n    }\n\n    // 检查是否还可以重试\n    if (\n      config.retryCount !== undefined &&\n      config.retryCount < MAX_RETRIES &&\n      !NO_RETRY_URLS.includes(config.url as string) &&\n      !config.noRetry\n    ) {\n      config.retryCount++;\n      console.error(`请求重试第 ${config.retryCount} 次`);\n\n      // 延迟重试\n      await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY));\n\n      // 重新发起请求\n      return request(config);\n    }\n\n    console.error(`重试${MAX_RETRIES}次后仍然失败`);\n    return Promise.reject(error);\n  }\n);\n\nexport default request;\n"
  },
  {
    "path": "src/renderer/utils/request_music.ts",
    "content": "import axios from 'axios';\n\nconst baseURL = `${import.meta.env.VITE_API_MUSIC}`;\nconst request = axios.create({\n  baseURL,\n  timeout: 10000\n});\n\n// 请求拦截器\nrequest.interceptors.request.use(\n  (config) => {\n    return config;\n  },\n  (error) => {\n    // 当请求异常时做一些处理\n    return Promise.reject(error);\n  }\n);\n\nexport default request;\n"
  },
  {
    "path": "src/renderer/utils/shortcutToast.ts",
    "content": "import { createVNode, render } from 'vue';\n\nimport ShortcutToast from '@/components/ShortcutToast.vue';\n\nlet container: HTMLDivElement | null = null;\nlet toastInstance: any = null;\n\ninterface ToastOptions {\n  position?: 'top' | 'center' | 'bottom';\n  showIcon?: boolean;\n}\n\nexport function showShortcutToast(message: string, iconName = '', options: ToastOptions = {}) {\n  // 如果容器不存在，创建一个新的容器\n  if (!container) {\n    container = document.createElement('div');\n    document.body.appendChild(container);\n  }\n\n  // 如果已经有实例，先销毁它\n  if (toastInstance) {\n    render(null, container);\n    toastInstance = null;\n  }\n\n  // 创建新的 toast 实例\n  const vnode = createVNode(ShortcutToast, {\n    position: options.position || 'center',\n    showIcon: options.showIcon !== undefined ? options.showIcon : true,\n    onDestroy: () => {\n      if (container) {\n        render(null, container);\n        document.body.removeChild(container);\n        container = null;\n      }\n    }\n  });\n\n  // 渲染 toast\n  render(vnode, container);\n  toastInstance = vnode.component?.exposed;\n\n  // 显示 toast\n  if (toastInstance) {\n    toastInstance.show(message, iconName, { showIcon: options.showIcon });\n  }\n}\n\n// 新增便捷方法 - 底部无图标 toast\nexport function showBottomToast(message: string) {\n  showShortcutToast(message, '', { position: 'bottom', showIcon: false });\n}\n"
  },
  {
    "path": "src/renderer/utils/theme.ts",
    "content": "export type ThemeType = 'dark' | 'light';\n\n// 检测系统主题\nexport const getSystemTheme = (): ThemeType => {\n  if (typeof window !== 'undefined' && window.matchMedia) {\n    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';\n  }\n  return 'light';\n};\n\n// 应用主题\nexport const applyTheme = (theme: ThemeType) => {\n  // 使用 Tailwind 的暗色主题类\n  if (theme === 'dark') {\n    document.documentElement.classList.add('dark');\n  } else {\n    document.documentElement.classList.remove('dark');\n  }\n\n  // 保存主题到本地存储\n  localStorage.setItem('theme', theme);\n};\n\n// 获取当前主题\nexport const getCurrentTheme = (): ThemeType => {\n  return (localStorage.getItem('theme') as ThemeType) || 'light';\n};\n\n// 监听系统主题变化\nexport const watchSystemTheme = (callback: (theme: ThemeType) => void) => {\n  if (typeof window !== 'undefined' && window.matchMedia) {\n    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');\n    const handler = (e: MediaQueryListEvent) => {\n      callback(e.matches ? 'dark' : 'light');\n    };\n\n    mediaQuery.addEventListener('change', handler);\n\n    // 返回清理函数\n    return () => {\n      mediaQuery.removeEventListener('change', handler);\n    };\n  }\n  return () => {};\n};\n"
  },
  {
    "path": "src/renderer/utils/update.ts",
    "content": "import { useDateFormat } from '@vueuse/core';\nimport axios from 'axios';\n\nimport config from '../../../package.json';\n\ninterface GithubReleaseInfo {\n  tag_name: string;\n  body: string;\n  published_at: string;\n  html_url: string;\n  assets: Array<{\n    browser_download_url: string;\n    name: string;\n    size: number;\n  }>;\n}\n\ninterface ProxyNode {\n  url: string;\n  server: string;\n  ip: string;\n  location: string;\n  latency: number;\n  speed: number;\n}\n\ninterface ProxyResponse {\n  code: number;\n  msg: string;\n  data: ProxyNode[];\n  total: number;\n  update_time: string;\n}\n\nexport interface UpdateResult {\n  hasUpdate: boolean;\n  latestVersion: string;\n  currentVersion: string;\n  releaseInfo: {\n    tag_name: string;\n    body: string;\n    html_url: string;\n    assets: Array<{\n      browser_download_url: string;\n      name: string;\n    }>;\n  } | null;\n}\n\n// 缓存相关配置\nconst CACHE_KEY = 'github_proxy_nodes';\nconst CACHE_EXPIRE_TIME = 1000 * 60 * 10; // 10分钟过期\n\n// 请求配置\nconst REQUEST_TIMEOUT = 2000; // 2秒超时\n\n/**\n * 从缓存获取代理节点\n */\nconst getCachedProxyNodes = (): { nodes: string[]; timestamp: number } | null => {\n  const cached = localStorage.getItem(CACHE_KEY);\n  if (cached) {\n    const { nodes, timestamp } = JSON.parse(cached);\n    if (Date.now() - timestamp < CACHE_EXPIRE_TIME) {\n      return { nodes, timestamp };\n    }\n  }\n  return null;\n};\n\n/**\n * 缓存代理节点\n */\nconst cacheProxyNodes = (nodes: string[]) => {\n  localStorage.setItem(\n    CACHE_KEY,\n    JSON.stringify({\n      nodes,\n      timestamp: Date.now()\n    })\n  );\n};\n\n/**\n * 获取代理节点列表\n */\nexport const getProxyNodes = async (): Promise<string[]> => {\n  // 尝试从缓存获取\n  const cached = getCachedProxyNodes();\n  if (cached) {\n    return cached.nodes;\n  }\n\n  try {\n    // 获取最新代理节点\n    const { data } = await axios.get<ProxyResponse>('https://api.akams.cn/github', {\n      timeout: REQUEST_TIMEOUT\n    });\n    if (data.code === 200) {\n      // 按速度排序并获取前10个节点\n      const nodes = data.data\n        .sort((a, b) => b.speed - a.speed)\n        .slice(0, 10)\n        .map((node) => node.url);\n\n      // 缓存节点\n      cacheProxyNodes(nodes);\n      return nodes;\n    }\n  } catch (error) {\n    console.error('获取代理节点失败:', error);\n  }\n\n  // 使用备用节点\n  return [\n    'https://gh.lk.cc',\n    'https://ghproxy.cn',\n    'https://ghproxy.net',\n    'https://gitproxy.click',\n    'https://github.tbedu.top',\n    'https://github.moeyy.xyz'\n  ];\n};\n\n/**\n * 获取 GitHub 最新发布版本信息\n */\nexport const getLatestReleaseInfo = async (): Promise<GithubReleaseInfo | null> => {\n  try {\n    const token = import.meta.env.VITE_GITHUB_TOKEN;\n    const headers = {};\n    // 构建 API URL 列表\n    const apiUrls = [\n      // 原始地址\n      'https://api.github.com/repos/algerkong/AlgerMusicPlayer/releases/latest',\n\n      // 使用代理节点\n      'http://music.alger.fun/package.json'\n    ];\n\n    if (token) {\n      headers['Authorization'] = `token ${token}`;\n    }\n\n    for (const url of apiUrls) {\n      try {\n        const response = await axios.get(url, {\n          headers,\n          timeout: REQUEST_TIMEOUT\n        });\n\n        if (url.includes('package.json')) {\n          // 如果是 package.json，获取对应的 CHANGELOG\n          const changelogUrl = url.replace('package.json', 'CHANGELOG.md');\n          const changelogResponse = await axios.get(changelogUrl, {\n            timeout: REQUEST_TIMEOUT\n          });\n\n          return {\n            tag_name: response.data.version,\n            body: changelogResponse.data,\n            html_url: 'https://github.com/algerkong/AlgerMusicPlayer/releases/latest',\n            assets: []\n          } as unknown as GithubReleaseInfo;\n        }\n        return response.data;\n      } catch (err) {\n        console.warn(`尝试访问 ${url} 失败:`, err);\n        continue;\n      }\n    }\n    throw new Error('所有 API 地址均无法访问');\n  } catch (error) {\n    console.error('获取 GitHub Release 信息失败:', error);\n    return null;\n  }\n};\n\n/**\n * 格式化时间\n */\nexport const formatDate = (dateStr: string): string => {\n  return useDateFormat(new Date(dateStr), 'YYYY-MM-DD HH:mm').value;\n};\n\n/**\n * 比较两个版本号\n * @param v1 版本号1\n * @param v2 版本号2\n * @returns 如果v1大于v2返回1，如果v1小于v2返回-1，如果相等返回0\n */\nexport const compareVersions = (v1: string, v2: string): number => {\n  const v1Parts = v1.split('.').map(Number);\n  const v2Parts = v2.split('.').map(Number);\n\n  for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {\n    const v1Part = v1Parts[i] || 0;\n    const v2Part = v2Parts[i] || 0;\n\n    if (v1Part > v2Part) return 1;\n    if (v1Part < v2Part) return -1;\n  }\n\n  return 0;\n};\n\n/**\n * 检查更新\n */\nexport const checkUpdate = async (\n  currentVersion: string = config.version\n): Promise<UpdateResult | null> => {\n  try {\n    const releaseInfo = await getLatestReleaseInfo();\n    console.log('releaseInfo', releaseInfo);\n    if (!releaseInfo) {\n      return null;\n    }\n\n    const latestVersion = releaseInfo.tag_name.replace('v', '');\n    // 比较版本号，只有当新版本大于当前版本时才返回更新信息\n    if (compareVersions(latestVersion, currentVersion) <= 0) {\n      return null;\n    }\n    console.log('latestVersion', latestVersion);\n    console.log('currentVersion', currentVersion);\n\n    return {\n      hasUpdate: true,\n      latestVersion,\n      currentVersion,\n      releaseInfo: {\n        tag_name: latestVersion,\n        body: `## 更新内容\\n\\n- 版本: ${latestVersion}\\n${releaseInfo.body}`,\n        html_url: releaseInfo.html_url,\n        assets: releaseInfo.assets.map((asset) => ({\n          browser_download_url: asset.browser_download_url,\n          name: asset.name\n        }))\n      }\n    };\n  } catch (error) {\n    console.error('检查更新失败:', error);\n    return null;\n  }\n};\n"
  },
  {
    "path": "src/renderer/utils/yrcParser.ts",
    "content": "/**\n * 歌词单词数据接口\n */\nexport interface WordData {\n  /** 单词文本内容 */\n  readonly text: string;\n  /** 开始时间（毫秒） */\n  readonly startTime: number;\n  /** 持续时间（毫秒） */\n  readonly duration: number;\n  /** 该单词后是否有空格 */\n  readonly space?: boolean;\n}\n\n/**\n * 歌词行数据接口\n */\nexport interface LyricLine {\n  /** 行开始时间（毫秒） */\n  readonly startTime: number;\n  /** 行持续时间（毫秒） */\n  readonly duration: number;\n  /** 完整文本内容 */\n  readonly fullText: string;\n  /** 单词数组 */\n  readonly words: readonly WordData[];\n}\n\n/**\n * 元数据接口\n */\nexport interface MetaData {\n  /** 时间戳（可选，不带时间的元数据为 undefined） */\n  readonly time?: number;\n  /** 内容 */\n  readonly content: string;\n}\n\n/**\n * 解析结果接口\n */\nexport interface ParsedLyrics {\n  /** 元数据数组 */\n  readonly metadata: readonly MetaData[];\n  /** 歌词行数组 */\n  readonly lyrics: readonly LyricLine[];\n}\n\n/**\n * 自定义解析错误类\n */\nexport class LyricParseError extends Error {\n  constructor(\n    message: string,\n    public readonly line?: string\n  ) {\n    super(message);\n    this.name = 'LyricParseError';\n  }\n}\n\n/**\n * 解析结果类型\n */\nexport type ParseResult<T> =\n  | { success: true; data: T }\n  | { success: false; error: LyricParseError };\n\n// 预编译正则表达式以提高性能\nconst METADATA_PATTERN = /^\\{(\"t\":|\"c\":)/; // 匹配 {\"t\": 或 {\"c\":\nconst LINE_TIME_PATTERN = /^\\[(\\d+),(\\d+)\\](.+)$/; // 逐字歌词格式: [92260,4740]...\nconst LRC_TIME_PATTERN = /^\\[(\\d{2}):(\\d{2})\\.(\\d{2,3})\\](.*)$/; // 标准LRC格式: [00:25.47]...\nconst WORD_PATTERN = /\\((\\d+),(\\d+),\\d+\\)([^(]*?)(?=\\(|$)/g;\n\n/**\n * 时间格式化函数\n * @param ms 毫秒数\n * @returns 格式化的时间字符串\n */\nexport const formatTime = (ms: number): string => {\n  const minutes = Math.floor(ms / 60000);\n  const seconds = Math.floor((ms % 60000) / 1000);\n  const milliseconds = ms % 1000;\n  return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;\n};\n\n/**\n * 解析元数据行\n * @param line 元数据行字符串\n * @returns 解析结果\n */\nconst parseMetadata = (line: string): ParseResult<MetaData> => {\n  try {\n    const data = JSON.parse(line);\n\n    // 类型守卫：检查数据结构\n    if (typeof data !== 'object' || data === null) {\n      return {\n        success: false,\n        error: new LyricParseError('元数据格式无效：不是有效的对象', line)\n      };\n    }\n\n    // 检查必须有 c 字段（内容数组）\n    if (!Array.isArray(data.c)) {\n      return {\n        success: false,\n        error: new LyricParseError('元数据格式无效：缺少 c 字段', line)\n      };\n    }\n\n    // t 字段（时间戳）是可选的\n    if (data.t !== undefined && typeof data.t !== 'number') {\n      return {\n        success: false,\n        error: new LyricParseError('元数据格式无效：t 字段必须是数字', line)\n      };\n    }\n\n    const content = data.c\n      .filter((item: any) => item && typeof item.tx === 'string')\n      .map((item: any) => item.tx)\n      .join('');\n\n    return {\n      success: true,\n      data: {\n        time: data.t,\n        content\n      }\n    };\n  } catch (error) {\n    return {\n      success: false,\n      error: new LyricParseError(\n        `JSON解析失败: ${error instanceof Error ? error.message : '未知错误'}`,\n        line\n      )\n    };\n  }\n};\n\n/**\n * 解析标准LRC格式的歌词行\n * @param line 歌词行字符串\n * @returns 解析结果\n */\nconst parseLrcLine = (line: string): ParseResult<LyricLine> => {\n  const lrcMatch = line.match(LRC_TIME_PATTERN);\n  if (!lrcMatch) {\n    return {\n      success: false,\n      error: new LyricParseError('LRC歌词行格式无效：无法匹配时间信息', line)\n    };\n  }\n\n  const minutes = parseInt(lrcMatch[1], 10);\n  const seconds = parseInt(lrcMatch[2], 10);\n  const milliseconds = parseInt(lrcMatch[3].padEnd(3, '0'), 10); // 处理2位或3位毫秒\n  const text = lrcMatch[4].trim();\n\n  // 验证时间值\n  if (\n    isNaN(minutes) ||\n    isNaN(seconds) ||\n    isNaN(milliseconds) ||\n    minutes < 0 ||\n    seconds < 0 ||\n    milliseconds < 0 ||\n    seconds >= 60\n  ) {\n    return {\n      success: false,\n      error: new LyricParseError('LRC歌词行格式无效：时间值无效', line)\n    };\n  }\n\n  const startTime = minutes * 60000 + seconds * 1000 + milliseconds;\n\n  return {\n    success: true,\n    data: {\n      startTime,\n      duration: 0, // LRC格式没有持续时间信息\n      fullText: text,\n      words: [] // LRC格式没有逐字信息\n    }\n  };\n};\n\n/**\n * 解析逐字歌词行\n * @param line 歌词行字符串\n * @returns 解析结果\n */\nconst parseWordByWordLine = (line: string): ParseResult<LyricLine> => {\n  // 使用预编译的正则表达式\n  const lineTimeMatch = line.match(LINE_TIME_PATTERN);\n  if (!lineTimeMatch) {\n    return {\n      success: false,\n      error: new LyricParseError('逐字歌词行格式无效：无法匹配时间信息', line)\n    };\n  }\n\n  const startTime = parseInt(lineTimeMatch[1], 10);\n  const duration = parseInt(lineTimeMatch[2], 10);\n  const content = lineTimeMatch[3];\n\n  // 验证时间值\n  if (isNaN(startTime) || isNaN(duration) || startTime < 0 || duration < 0) {\n    return {\n      success: false,\n      error: new LyricParseError('逐字歌词行格式无效：时间值无效', line)\n    };\n  }\n  // 重置正则表达式状态\n  WORD_PATTERN.lastIndex = 0;\n\n  const words: WordData[] = [];\n  let match: RegExpExecArray | null;\n\n  // 第一遍：提取所有单词的原始文本（包含空格），构建完整文本\n  const rawTextParts: string[] = [];\n  const tempWords: Array<{ startTime: number; duration: number; text: string }> = [];\n\n  while ((match = WORD_PATTERN.exec(content)) !== null) {\n    const wordStartTime = parseInt(match[1], 10);\n    const wordDuration = parseInt(match[2], 10);\n    const rawWordText = match[3]; // 保留原始文本（可能包含空格）\n    const wordText = rawWordText.trim(); // 去除首尾空格的文本\n\n    // 验证单词数据\n    if (isNaN(wordStartTime) || isNaN(wordDuration)) {\n      continue; // 跳过无效的单词数据\n    }\n\n    if (wordText) {\n      tempWords.push({\n        text: wordText,\n        startTime: wordStartTime,\n        duration: wordDuration\n      });\n      rawTextParts.push(rawWordText); // 保留原始格式用于分析空格\n    }\n  }\n\n  // 构建完整的文本（保留原始空格）\n  const fullText = rawTextParts.join('').trim();\n\n  // 第二遍：检查每个单词在完整文本中是否后面有空格\n  let currentPos = 0;\n  for (const word of tempWords) {\n    // 在完整文本中查找当前单词的位置\n    const wordIndex = fullText.indexOf(word.text, currentPos);\n    if (wordIndex === -1) {\n      // 如果找不到，直接添加不带空格标记的单词\n      words.push(word);\n      continue;\n    }\n\n    // 计算单词结束位置\n    const wordEndPos = wordIndex + word.text.length;\n    // 检查单词后面是否有空格\n    const hasSpace = wordEndPos < fullText.length && fullText[wordEndPos] === ' ';\n    words.push({\n      ...word,\n      space: hasSpace\n    });\n\n    // 更新搜索位置\n    currentPos = wordEndPos;\n  }\n\n  return {\n    success: true,\n    data: {\n      startTime,\n      duration,\n      fullText,\n      words\n    }\n  };\n};\n\n/**\n * 解析歌词行（自动检测格式）\n * @param line 歌词行字符串\n * @returns 解析结果\n */\nconst parseLyricLine = (line: string): ParseResult<LyricLine> => {\n  // 首先尝试解析逐字歌词格式\n  if (LINE_TIME_PATTERN.test(line)) {\n    return parseWordByWordLine(line);\n  }\n\n  // 然后尝试解析标准LRC格式\n  if (LRC_TIME_PATTERN.test(line)) {\n    return parseLrcLine(line);\n  }\n\n  return {\n    success: false,\n    error: new LyricParseError('歌词行格式无效：不匹配任何已知格式', line)\n  };\n};\n\n/**\n * 计算LRC格式歌词的持续时间\n * @param lyrics 歌词行数组\n * @returns 更新持续时间后的歌词行数组\n */\nconst calculateLrcDurations = (lyrics: LyricLine[]): LyricLine[] => {\n  if (lyrics.length === 0) return lyrics;\n\n  const updatedLyrics: LyricLine[] = [];\n\n  for (let i = 0; i < lyrics.length; i++) {\n    const currentLine = lyrics[i];\n\n    // 如果已经有持续时间（逐字歌词），直接使用\n    if (currentLine.duration > 0) {\n      updatedLyrics.push(currentLine);\n      continue;\n    }\n\n    // 计算LRC格式的持续时间\n    let duration = 0;\n    if (i < lyrics.length - 1) {\n      // 使用下一行的开始时间减去当前行的开始时间\n      duration = lyrics[i + 1].startTime - currentLine.startTime;\n    } else {\n      // 最后一行，使用默认持续时间（3秒）\n      duration = 3000;\n    }\n\n    // 确保持续时间不为负数\n    duration = Math.max(duration, 0);\n\n    updatedLyrics.push({\n      ...currentLine,\n      duration\n    });\n  }\n\n  return updatedLyrics;\n};\n\n/**\n * 解析不带时间戳的纯文本歌词行\n * @param line 纯文本歌词行\n * @returns 解析结果\n */\nconst parsePlainTextLine = (line: string): ParseResult<LyricLine> => {\n  // 清理行首尾的 \\r 等特殊字符\n  const text = line.replace(/\\r/g, '').trim();\n\n  if (!text) {\n    return {\n      success: false,\n      error: new LyricParseError('纯文本歌词行为空', line)\n    };\n  }\n\n  return {\n    success: true,\n    data: {\n      startTime: -1, // -1 表示没有时间信息\n      duration: 0,\n      fullText: text,\n      words: []\n    }\n  };\n};\n\n/**\n * 主解析函数\n * @param lyricsStr 歌词字符串\n * @returns 解析结果\n */\nexport const parseLyrics = (lyricsStr: string): ParseResult<ParsedLyrics> => {\n  if (typeof lyricsStr !== 'string') {\n    return {\n      success: false,\n      error: new LyricParseError('输入参数必须是字符串')\n    };\n  }\n\n  try {\n    const lines = lyricsStr.trim().split('\\n');\n    const metadata: MetaData[] = [];\n    const lyrics: LyricLine[] = [];\n    const errors: LyricParseError[] = [];\n\n    for (let i = 0; i < lines.length; i++) {\n      const trimmedLine = lines[i].trim();\n      if (!trimmedLine) continue;\n\n      // 使用预编译正则表达式进行快速检测\n      if (METADATA_PATTERN.test(trimmedLine)) {\n        const result = parseMetadata(trimmedLine);\n        if (result.success) {\n          metadata.push(result.data);\n        } else {\n          errors.push(result.error);\n        }\n      } else if (trimmedLine.startsWith('[')) {\n        const result = parseLyricLine(trimmedLine);\n        if (result.success) {\n          lyrics.push(result.data);\n        } else {\n          errors.push(result.error);\n        }\n      } else {\n        // 尝试解析为纯文本歌词行（不带时间戳）\n        const result = parsePlainTextLine(trimmedLine);\n        if (result.success) {\n          lyrics.push(result.data);\n        } else {\n          errors.push(result.error);\n        }\n      }\n    }\n\n    // 如果有太多错误，可能整个文件格式有问题\n    if (errors.length > 0 && errors.length > lines.length * 0.5) {\n      return {\n        success: false,\n        error: new LyricParseError(\n          `解析失败：错误行数过多 (${errors.length}/${lines.length})，可能文件格式不正确 ${JSON.stringify(errors)}`\n        )\n      };\n    }\n\n    // 按时间排序歌词行（将没有时间信息的行放在最前面）\n    lyrics.sort((a, b) => {\n      if (a.startTime === -1 && b.startTime === -1) return 0;\n      if (a.startTime === -1) return -1;\n      if (b.startTime === -1) return 1;\n      return a.startTime - b.startTime;\n    });\n\n    // 计算LRC格式的持续时间\n    const finalLyrics = calculateLrcDurations(lyrics);\n\n    return {\n      success: true,\n      data: {\n        metadata,\n        lyrics: finalLyrics\n      }\n    };\n  } catch (error) {\n    return {\n      success: false,\n      error: new LyricParseError(\n        `解析过程中发生错误: ${error instanceof Error ? error.message : '未知错误'}`\n      )\n    };\n  }\n};\n\n/**\n * 导出默认解析函数（向后兼容）\n */\nexport default parseLyrics;\n"
  },
  {
    "path": "src/renderer/views/artist/detail.vue",
    "content": "<template>\n  <n-scrollbar v-loading=\"loading\" class=\"artist-page\">\n    <!-- 歌手信息头部 -->\n    <div class=\"artist-header\">\n      <div class=\"artist-cover\">\n        <n-image\n          :src=\"getImgUrl(artistInfo?.avatar, '300y300')\"\n          class=\"artist-avatar\"\n          preview-disabled\n        />\n      </div>\n      <div class=\"artist-info\">\n        <h1 class=\"artist-name\">{{ artistInfo?.name }}</h1>\n        <div v-if=\"artistInfo?.alias?.length\" class=\"artist-alias\">\n          {{ artistInfo.alias.join(' / ') }}\n        </div>\n        <div v-if=\"artistInfo?.briefDesc\" class=\"artist-desc\">\n          {{ artistInfo.briefDesc }}\n        </div>\n      </div>\n    </div>\n\n    <!-- 标签页切换 -->\n    <n-tabs v-model:value=\"activeTab\" class=\"content-tabs\" type=\"line\" animated>\n      <n-tab-pane name=\"songs\" :tab=\"t('artist.hotSongs')\">\n        <!-- 添加歌曲操作工具栏 -->\n        <div class=\"songs-toolbar\">\n          <div class=\"toolbar-left\">\n            <n-tooltip placement=\"bottom\" trigger=\"hover\">\n              <template #trigger>\n                <div class=\"action-button hover-green\" @click=\"handlePlayAll\">\n                  <i class=\"icon iconfont ri-play-fill\"></i>\n                </div>\n              </template>\n              {{ t('comp.musicList.playAll') }}\n            </n-tooltip>\n\n            <n-tooltip placement=\"bottom\" trigger=\"hover\">\n              <template #trigger>\n                <div class=\"action-button hover-green\" @click=\"addToPlaylist\">\n                  <i class=\"icon iconfont ri-add-line\"></i>\n                </div>\n              </template>\n              {{ t('comp.musicList.addToPlaylist') }}\n            </n-tooltip>\n          </div>\n\n          <div class=\"toolbar-right\">\n            <!-- 布局切换按钮 -->\n            <div class=\"layout-toggle\" v-if=\"!isMobile\">\n              <n-tooltip placement=\"bottom\" trigger=\"hover\">\n                <template #trigger>\n                  <div class=\"toggle-button hover-green\" @click=\"toggleLayout\">\n                    <i\n                      class=\"icon iconfont\"\n                      :class=\"isCompactLayout ? 'ri-list-check-2' : 'ri-grid-line'\"\n                    ></i>\n                  </div>\n                </template>\n                {{\n                  isCompactLayout\n                    ? t('comp.musicList.switchToNormal')\n                    : t('comp.musicList.switchToCompact')\n                }}\n              </n-tooltip>\n            </div>\n\n            <!-- 搜索框 -->\n            <div class=\"search-container\" :class=\"{ 'search-expanded': isSearchVisible }\">\n              <template v-if=\"isSearchVisible\">\n                <n-input\n                  v-model:value=\"searchKeyword\"\n                  :placeholder=\"t('comp.musicList.searchSongs')\"\n                  clearable\n                  round\n                  size=\"small\"\n                  @blur=\"handleSearchBlur\"\n                >\n                  <template #prefix>\n                    <i class=\"icon iconfont ri-search-line text-sm\"></i>\n                  </template>\n                  <template #suffix>\n                    <i\n                      class=\"icon iconfont ri-close-line text-sm cursor-pointer\"\n                      @click=\"closeSearch\"\n                    ></i>\n                  </template>\n                </n-input>\n              </template>\n              <template v-else>\n                <n-tooltip placement=\"bottom\" trigger=\"hover\">\n                  <template #trigger>\n                    <div class=\"search-button\" @click=\"showSearch\">\n                      <i class=\"icon iconfont ri-search-line\"></i>\n                    </div>\n                  </template>\n                  {{ t('comp.musicList.searchSongs') }}\n                </n-tooltip>\n              </template>\n            </div>\n          </div>\n        </div>\n\n        <div class=\"songs-list\">\n          <div class=\"song-list-content\">\n            <div v-if=\"filteredSongs.length === 0 && searchKeyword\" class=\"no-result\">\n              {{ t('comp.musicList.noSearchResults') }}\n            </div>\n\n            <!-- 替换原来的 v-for 循环为虚拟列表 -->\n            <n-virtual-list\n              ref=\"songListRef\"\n              class=\"song-virtual-list\"\n              style=\"height: calc(80vh - 60px)\"\n              :items=\"filteredSongs\"\n              :item-size=\"isCompactLayout ? 50 : 70\"\n              item-resizable\n              key-field=\"id\"\n              @scroll=\"handleVirtualScroll\"\n            >\n              <template #default=\"{ item, index }\">\n                <div>\n                  <div class=\"double-item\">\n                    <song-item\n                      :index=\"index\"\n                      :compact=\"isCompactLayout\"\n                      :item=\"formatSong(item)\"\n                      @play=\"handlePlay\"\n                    />\n                  </div>\n                  <div v-if=\"index === filteredSongs.length - 1\" class=\"h-36\"></div>\n                </div>\n              </template>\n            </n-virtual-list>\n\n            <div v-if=\"songLoading\" class=\"loading-more\">{{ t('common.loading') }}</div>\n            <div\n              v-else-if=\"songPage.hasMore\"\n              ref=\"songsLoadMoreRef\"\n              class=\"load-more-trigger\"\n            ></div>\n          </div>\n        </div>\n      </n-tab-pane>\n\n      <n-tab-pane name=\"albums\" :tab=\"t('artist.albums')\">\n        <div class=\"albums-list\">\n          <div class=\"albums-grid\">\n            <search-item\n              v-for=\"album in albums\"\n              :key=\"album.id\"\n              shape=\"square\"\n              :item=\"{\n                id: album.id,\n                picUrl: album.picUrl,\n                name: album.name,\n                desc: formatPublishTime(album.publishTime),\n                size: album.size,\n                type: '专辑'\n              }\"\n            />\n            <div v-if=\"albumLoading\" class=\"loading-more\">{{ t('common.loading') }}</div>\n            <div\n              v-else-if=\"albumPage.hasMore\"\n              ref=\"albumsLoadMoreRef\"\n              class=\"load-more-trigger\"\n            ></div>\n          </div>\n        </div>\n      </n-tab-pane>\n\n      <n-tab-pane name=\"about\" :tab=\"t('artist.description')\">\n        <div class=\"artist-description\">\n          <div class=\"description-content\" v-html=\"artistInfo?.briefDesc\"></div>\n        </div>\n      </n-tab-pane>\n    </n-tabs>\n\n    <play-bottom />\n  </n-scrollbar>\n</template>\n\n<script setup lang=\"ts\">\nimport { useDateFormat } from '@vueuse/core';\nimport { useMessage } from 'naive-ui';\nimport PinyinMatch from 'pinyin-match';\nimport {\n  computed,\n  nextTick,\n  onActivated,\n  onDeactivated,\n  onMounted,\n  onUnmounted,\n  ref,\n  watch\n} from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute } from 'vue-router';\n\nimport { getArtistAlbums, getArtistDetail, getArtistTopSongs } from '@/api/artist';\nimport { getMusicDetail } from '@/api/music';\nimport PlayBottom from '@/components/common/PlayBottom.vue';\nimport SearchItem from '@/components/common/SearchItem.vue';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { usePlayerStore } from '@/store';\nimport { IArtist } from '@/types/artist';\nimport { getImgUrl, isMobile } from '@/utils';\n\ndefineOptions({\n  name: 'ArtistDetail'\n});\n\nconst { t } = useI18n();\nconst route = useRoute();\nconst playerStore = usePlayerStore();\nconst message = useMessage();\n\nconst artistId = computed(() => Number(route.params.id));\nconst activeTab = ref('songs');\n\n// 歌手信息\nconst artistInfo = ref<IArtist>();\nconst songs = ref<any[]>([]);\nconst albums = ref<any[]>([]);\n\n// 加载状态\nconst loading = ref(false);\nconst songLoading = ref(false);\nconst albumLoading = ref(false);\n\n// 分页参数\nconst songPage = ref({\n  page: 1,\n  pageSize: 30,\n  hasMore: true\n});\n\nconst albumPage = ref({\n  page: 1,\n  pageSize: 30,\n  hasMore: true\n});\n\n// 无限滚动引用\nconst songsLoadMoreRef = ref<HTMLElement | null>(null);\nconst albumsLoadMoreRef = ref<HTMLElement | null>(null);\nlet songsObserver: IntersectionObserver | null = null;\nlet albumsObserver: IntersectionObserver | null = null;\n\n// 添加上一个ID的引用，用于比较\nconst previousId = ref<string | null>(null);\n\n// 简化缓存机制\nconst artistDataCache = new Map();\n\n// 单个缓存键函数\nconst getCacheKey = (id: string | number) => `artist_${id}`;\n\n// 搜索和布局相关\nconst searchKeyword = ref('');\nconst isSearchVisible = ref(false);\nconst isCompactLayout = ref(\n  isMobile.value ? false : localStorage.getItem('musicListLayout') === 'compact'\n);\n\n// 加载歌手信息\nconst loadArtistInfo = async () => {\n  if (!artistId.value) return;\n\n  // 简化缓存检查\n  const cacheKey = getCacheKey(artistId.value);\n  if (artistDataCache.has(cacheKey)) {\n    console.log('使用缓存数据');\n    const cachedData = artistDataCache.get(cacheKey);\n    artistInfo.value = cachedData.artistInfo;\n    songs.value = cachedData.songs;\n    albums.value = cachedData.albums;\n    songPage.value = cachedData.songPage;\n    albumPage.value = cachedData.albumPage;\n    return;\n  }\n\n  // 加载新数据\n  loading.value = true;\n  try {\n    const info = await getArtistDetail(artistId.value);\n    if (info.data?.data?.artist) {\n      artistInfo.value = info.data.data.artist;\n    }\n    // 重置分页并加载初始数据\n    resetPagination();\n    await Promise.all([loadSongs(), loadAlbums()]);\n\n    // 保存到缓存\n    artistDataCache.set(cacheKey, {\n      artistInfo: artistInfo.value,\n      songs: [...songs.value],\n      albums: [...albums.value],\n      songPage: { ...songPage.value },\n      albumPage: { ...albumPage.value }\n    });\n  } catch (error) {\n    console.error('加载歌手信息失败:', error);\n  } finally {\n    loading.value = false;\n  }\n};\n\n// 重置分页\nconst resetPagination = () => {\n  songPage.value = {\n    page: 1,\n    pageSize: 50,\n    hasMore: true\n  };\n  albumPage.value = {\n    page: 1,\n    pageSize: 50,\n    hasMore: true\n  };\n  songs.value = [];\n  albums.value = [];\n};\n\n// 加载歌曲\nconst loadSongs = async () => {\n  if (!artistId.value || !songPage.value.hasMore || songLoading.value) return;\n\n  try {\n    songLoading.value = true;\n    const { page, pageSize } = songPage.value;\n    const res = await getArtistTopSongs({\n      id: artistId.value,\n      limit: pageSize,\n      offset: (page - 1) * pageSize\n    });\n\n    const ids = res.data.songs.map((item) => item.id);\n    const songsDetail = await getMusicDetail(ids);\n\n    if (songsDetail.data?.songs) {\n      const newSongs = songsDetail.data.songs.map((item) => {\n        return {\n          ...item,\n          picUrl: item.al.picUrl,\n          song: {\n            artists: item.ar,\n            name: item.name,\n            id: item.id\n          }\n        };\n      });\n      songs.value = page === 1 ? newSongs : [...songs.value, ...newSongs];\n      songPage.value.hasMore = newSongs.length === pageSize;\n      songPage.value.page++;\n    } else {\n      songPage.value.hasMore = false;\n    }\n  } catch (error) {\n    console.error('加载歌曲失败:', error);\n  } finally {\n    songLoading.value = false;\n  }\n};\n\n// 加载专辑\nconst loadAlbums = async () => {\n  if (!artistId.value || !albumPage.value.hasMore || albumLoading.value) return;\n\n  try {\n    albumLoading.value = true;\n    const { page, pageSize } = albumPage.value;\n    const res = await getArtistAlbums({\n      id: artistId.value,\n      limit: pageSize,\n      offset: (page - 1) * pageSize\n    });\n\n    if (res.data?.hotAlbums) {\n      const newAlbums = res.data.hotAlbums;\n      albums.value = page === 1 ? newAlbums : [...albums.value, ...newAlbums];\n      albumPage.value.hasMore = newAlbums.length === pageSize;\n      albumPage.value.page++;\n    } else {\n      albumPage.value.hasMore = false;\n    }\n  } catch (error) {\n    console.error('加载专辑失败:', error);\n  } finally {\n    albumLoading.value = false;\n  }\n};\n\n// 格式化发布时间\nconst formatPublishTime = (time: number) => {\n  return useDateFormat(time, 'YYYY-MM-DD').value;\n};\n\n// 搜索相关方法\nconst showSearch = () => {\n  isSearchVisible.value = true;\n  // 添加一个小延迟后聚焦搜索框\n  nextTick(() => {\n    const inputEl = document.querySelector('.search-container input');\n    if (inputEl) {\n      (inputEl as HTMLInputElement).focus();\n    }\n  });\n};\n\nconst closeSearch = () => {\n  isSearchVisible.value = false;\n  searchKeyword.value = '';\n};\n\nconst handleSearchBlur = () => {\n  // 如果搜索框为空，则在失焦时关闭搜索框\n  if (!searchKeyword.value) {\n    setTimeout(() => {\n      isSearchVisible.value = false;\n    }, 200);\n  }\n};\n\n// 过滤歌曲列表\nconst filteredSongs = computed(() => {\n  if (!searchKeyword.value) {\n    return songs.value;\n  }\n\n  const keyword = searchKeyword.value.toLowerCase().trim();\n  return songs.value.filter((song) => {\n    const songName = song.name?.toLowerCase() || '';\n    const albumName = song.al?.name?.toLowerCase() || '';\n    const artists = song.ar || song.artists || [];\n\n    // 原始文本匹配\n    const nameMatch = songName.includes(keyword);\n    const albumMatch = albumName.includes(keyword);\n    const artistsMatch = artists.some((artist: any) => {\n      return artist.name?.toLowerCase().includes(keyword);\n    });\n\n    // 拼音匹配\n    const namePinyinMatch = song.name && PinyinMatch.match(song.name, keyword);\n    const albumPinyinMatch = song.al?.name && PinyinMatch.match(song.al.name, keyword);\n    const artistsPinyinMatch = artists.some((artist: any) => {\n      return artist.name && PinyinMatch.match(artist.name, keyword);\n    });\n\n    return (\n      nameMatch ||\n      albumMatch ||\n      artistsMatch ||\n      namePinyinMatch ||\n      albumPinyinMatch ||\n      artistsPinyinMatch\n    );\n  });\n});\n\n// 布局切换\nconst toggleLayout = () => {\n  isCompactLayout.value = !isCompactLayout.value;\n  localStorage.setItem('musicListLayout', isCompactLayout.value ? 'compact' : 'normal');\n};\n\n// 播放全部\nconst handlePlayAll = () => {\n  if (filteredSongs.value.length === 0) return;\n\n  playerStore.setPlayList(\n    filteredSongs.value.map((song) => ({\n      ...song,\n      picUrl: song.al.picUrl\n    }))\n  );\n\n  // 开始播放第一首\n  playerStore.setPlay(filteredSongs.value[0]);\n\n  message.success(t('comp.musicList.playAll'));\n};\n\n// 添加到播放列表\nconst addToPlaylist = () => {\n  if (filteredSongs.value.length === 0) return;\n\n  // 获取当前播放列表\n  const currentList = playerStore.playList;\n\n  // 添加歌曲到播放列表(避免重复添加)\n  const newSongs = filteredSongs.value.filter(\n    (song) => !currentList.some((item) => item.id === song.id)\n  );\n\n  if (newSongs.length === 0) {\n    message.info(t('comp.musicList.songsAlreadyInPlaylist'));\n    return;\n  }\n\n  // 合并到当前播放列表末尾\n  const newList = [\n    ...currentList,\n    ...newSongs.map((song) => ({\n      ...song,\n      picUrl: song.al.picUrl\n    }))\n  ];\n\n  playerStore.setPlayList(newList);\n\n  message.success(t('comp.musicList.addToPlaylistSuccess', { count: newSongs.length }));\n};\n\nconst handlePlay = (song?: any) => {\n  // 如果传入了特定歌曲（点击单曲播放），则将其作为播放列表的第一首\n  if (song) {\n    const songList = [...filteredSongs.value];\n    const index = songList.findIndex((item) => item.id === song.id);\n\n    if (index !== -1) {\n      // 将点击的歌曲移到第一位\n      const clickedSong = songList.splice(index, 1)[0];\n      songList.unshift(clickedSong);\n    }\n\n    playerStore.setPlayList(\n      songList.map((item) => ({\n        ...item,\n        picUrl: item.al?.picUrl || item.picUrl\n      }))\n    );\n\n    // 设置当前播放歌曲\n    playerStore.setPlay(song);\n  } else {\n    // 默认行为：播放整个过滤后的列表\n    playerStore.setPlayList(\n      filteredSongs.value.map((item) => ({\n        ...item,\n        picUrl: item.al?.picUrl || item.picUrl\n      }))\n    );\n  }\n};\n\n// 简化观察器设置\nconst setupObservers = () => {\n  // 清理之前的观察器\n  if (songsObserver) songsObserver.disconnect();\n  if (albumsObserver) albumsObserver.disconnect();\n\n  // 创建观察器(如果不存在)\n  if (!songsObserver) {\n    songsObserver = new IntersectionObserver(\n      (entries) => {\n        if (entries[0].isIntersecting && songPage.value.hasMore) {\n          loadSongs();\n        }\n      },\n      { threshold: 0.1 }\n    );\n  }\n\n  if (!albumsObserver) {\n    albumsObserver = new IntersectionObserver(\n      (entries) => {\n        if (entries[0].isIntersecting && albumPage.value.hasMore) {\n          loadAlbums();\n        }\n      },\n      { threshold: 0.1 }\n    );\n  }\n\n  // 观察当前标签页的元素\n  nextTick(() => {\n    if (activeTab.value === 'songs' && songsLoadMoreRef.value) {\n      songsObserver?.observe(songsLoadMoreRef.value);\n    } else if (activeTab.value === 'albums' && albumsLoadMoreRef.value) {\n      albumsObserver?.observe(albumsLoadMoreRef.value);\n    }\n  });\n};\n\n// 监听标签切换\nwatch(activeTab, () => {\n  setupObservers();\n});\n\n// 监听引用元素的变化\nwatch([songsLoadMoreRef, albumsLoadMoreRef], () => {\n  setupObservers();\n});\n\n// 搜索词变化时重新设置观察器\nwatch(searchKeyword, () => {\n  nextTick(() => {\n    setupObservers();\n  });\n});\n\nonActivated(() => {\n  // 确保当前路由是艺术家详情页\n  if (route.name === 'artistDetail') {\n    const currentId = route.params.id as string;\n\n    // 首次加载或ID变化时加载数据\n    if (!previousId.value || previousId.value !== currentId) {\n      console.log('ID已变化，加载新数据');\n      previousId.value = currentId;\n      activeTab.value = 'songs';\n      loadArtistInfo();\n    }\n\n    // 重新设置观察器\n    setupObservers();\n  }\n});\n\nonMounted(() => {\n  // 首次挂载时加载数据\n  if (route.params.id) {\n    previousId.value = route.params.id as string;\n    loadArtistInfo();\n    setupObservers();\n  }\n});\n\nonDeactivated(() => {\n  // 断开观察器但不清除引用\n  if (songsObserver) songsObserver.disconnect();\n  if (albumsObserver) albumsObserver.disconnect();\n});\n\nonUnmounted(() => {\n  // 完全清理观察器\n  if (songsObserver) {\n    songsObserver.disconnect();\n    songsObserver = null;\n  }\n  if (albumsObserver) {\n    albumsObserver.disconnect();\n    albumsObserver = null;\n  }\n});\n\n// 定义在script setup部分\nconst songListRef = ref(null);\n\n// 格式化歌曲（使用在虚拟列表中）\nconst formatSong = (item: any) => {\n  if (!item) {\n    return null;\n  }\n  return {\n    ...item,\n    picUrl: item.al?.picUrl || item.picUrl\n  };\n};\n\n// 处理虚拟列表滚动\nconst handleVirtualScroll = (e: any) => {\n  if (!e || !e.target) return;\n\n  const { scrollTop, scrollHeight, clientHeight } = e.target;\n  const threshold = 200;\n\n  if (\n    scrollHeight - scrollTop - clientHeight < threshold &&\n    !songLoading.value &&\n    songPage.value.hasMore &&\n    !searchKeyword.value // 搜索状态下不触发加载更多\n  ) {\n    loadSongs();\n  }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.artist-page {\n  @apply min-h-screen w-full bg-light dark:bg-dark pb-24;\n\n  .nav-header {\n    @apply flex items-center px-4 py-3 sticky top-0 bg-light dark:bg-dark z-10;\n\n    i {\n      @apply text-xl mr-4 cursor-pointer;\n    }\n\n    .page-title {\n      @apply text-base font-medium truncate;\n    }\n  }\n\n  .artist-header {\n    @apply flex flex-col md:flex-row gap-4 md:gap-6 px-4 pb-4;\n\n    .artist-cover {\n      @apply flex justify-center md:justify-start;\n\n      .artist-avatar {\n        @apply w-40 h-40 md:w-48 md:h-48 rounded-2xl object-cover;\n      }\n    }\n\n    .artist-info {\n      @apply flex-1;\n\n      .artist-name {\n        @apply text-2xl md:text-4xl font-bold mb-2 text-center md:text-left;\n      }\n\n      .artist-alias {\n        @apply text-gray-500 dark:text-gray-400 mb-2 text-center md:text-left;\n      }\n\n      .artist-desc {\n        @apply text-sm text-gray-600 dark:text-gray-300 line-clamp-3 text-center md:text-left;\n      }\n    }\n  }\n\n  .content-tabs {\n    @apply px-4;\n\n    :deep(.n-tabs-nav) {\n      @apply sticky top-0 bg-light dark:bg-dark z-10;\n    }\n  }\n\n  .albums-grid {\n    @apply grid gap-6 grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-6 pb-40;\n  }\n\n  .loading-more {\n    @apply text-center py-4 text-gray-500 dark:text-gray-400;\n  }\n\n  .load-more-trigger {\n    @apply h-4 w-full;\n  }\n\n  .artist-description {\n    .description-content {\n      @apply text-sm leading-relaxed whitespace-pre-wrap;\n    }\n  }\n\n  // 添加歌曲工具栏样式\n  .songs-toolbar {\n    @apply flex items-center justify-between mb-4;\n\n    .toolbar-left,\n    .toolbar-right {\n      @apply flex items-center gap-2;\n    }\n  }\n\n  // 搜索框样式\n  .search-container {\n    @apply max-w-md transition-all duration-300 ease-in-out;\n\n    &.search-expanded {\n      @apply w-52;\n    }\n\n    .search-button {\n      @apply w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:bg-light-300 dark:hover:bg-dark-300 transition-colors text-gray-500 dark:text-gray-400 hover:text-green-500;\n\n      .icon {\n        @apply text-lg;\n      }\n    }\n\n    :deep(.n-input) {\n      @apply bg-light-200 dark:bg-dark-200;\n    }\n  }\n\n  // 操作按钮样式\n  .layout-toggle .toggle-button,\n  .action-button {\n    @apply w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:bg-light-300 dark:hover:bg-dark-300 transition-colors text-gray-500 dark:text-gray-400;\n\n    .icon {\n      @apply text-lg;\n    }\n\n    &.hover-green:hover {\n      .icon {\n        @apply text-green-500;\n      }\n    }\n  }\n\n  // 搜索无结果样式\n  .no-result {\n    @apply text-center py-8 text-gray-500 dark:text-gray-400;\n  }\n\n  // 虚拟列表样式\n  .song-virtual-list {\n    :deep(.n-virtual-list__scroll) {\n      scrollbar-width: thin;\n      &::-webkit-scrollbar {\n        width: 4px;\n      }\n      &::-webkit-scrollbar-thumb {\n        @apply bg-gray-400 dark:bg-gray-600 rounded;\n      }\n    }\n  }\n\n  .double-item {\n    @apply mb-2 bg-light-100 bg-opacity-30 dark:bg-dark-100 dark:bg-opacity-20 rounded-3xl;\n  }\n}\n\n.mobile {\n  .songs-toolbar {\n    @apply mb-0;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/bilibili/BilibiliPlayer.vue",
    "content": "<template>\n  <div class=\"bilibili-player-page\">\n    <n-scrollbar class=\"content-scrollbar\">\n      <div class=\"content-wrapper\">\n        <div v-if=\"isLoading\" class=\"loading-wrapper\">\n          <n-spin size=\"large\" />\n          <p>{{ t('bilibili.player.loading') }}</p>\n        </div>\n\n        <div v-else-if=\"errorMessage\" class=\"error-wrapper\">\n          <i class=\"ri-error-warning-line text-4xl text-red-500\"></i>\n          <p>{{ errorMessage }}</p>\n          <n-button type=\"primary\" @click=\"loadVideoSource\">{{\n            t('bilibili.player.retry')\n          }}</n-button>\n        </div>\n\n        <div v-else-if=\"videoDetail\" class=\"bilibili-info-wrapper\" :class=\"mainContentAnimation\">\n          <div class=\"bilibili-cover\">\n            <n-image\n              :src=\"getBilibiliProxyUrl(videoDetail.pic)\"\n              class=\"cover-image\"\n              preview-disabled\n            />\n            <!-- 悬浮的播放按钮 -->\n            <div class=\"play-overlay\">\n              <div class=\"play-icon-bg\" @click=\"playCurrentAudio\">\n                <i class=\"ri-play-fill\"></i>\n              </div>\n              <!-- 固定在右下角的大型播放按钮 -->\n              <n-button\n                type=\"primary\"\n                size=\"large\"\n                class=\"corner-play-button\"\n                :loading=\"partLoading\"\n                @click=\"playCurrentAudio\"\n              >\n                <template #icon>\n                  <i class=\"ri-play-fill\"></i>\n                </template>\n                {{ t('bilibili.player.playNow') }}\n              </n-button>\n            </div>\n          </div>\n\n          <div class=\"video-info\">\n            <div\n              class=\"title\"\n              v-html=\"videoDetail?.title || t('bilibili.player.loadingTitle')\"\n            ></div>\n            <div class=\"author\">\n              <i class=\"ri-user-line mr-1\"></i>\n              <span>{{ videoDetail.owner?.name }}</span>\n            </div>\n            <div class=\"stats\">\n              <span\n                ><i class=\"ri-play-line mr-1\"></i>{{ formatNumber(videoDetail.stat?.view) }}</span\n              >\n              <span\n                ><i class=\"ri-chat-1-line mr-1\"></i\n                >{{ formatNumber(videoDetail.stat?.danmaku) }}</span\n              >\n              <span\n                ><i class=\"ri-thumb-up-line mr-1\"></i\n                >{{ formatNumber(videoDetail.stat?.like) }}</span\n              >\n            </div>\n            <div class=\"description\">\n              <p>{{ videoDetail.desc }}</p>\n            </div>\n            <div class=\"duration\">\n              <p>\n                {{\n                  t('bilibili.player.totalDuration', {\n                    duration: formatTotalDuration(videoDetail.duration)\n                  })\n                }}\n              </p>\n            </div>\n          </div>\n        </div>\n\n        <div\n          v-if=\"videoDetail?.pages && videoDetail.pages.length > 1\"\n          class=\"video-parts\"\n          :class=\"partsListAnimation\"\n        >\n          <div class=\"parts-title\">\n            {{ t('bilibili.player.partsList', { count: videoDetail.pages.length }) }}\n            <n-spin v-if=\"partLoading\" size=\"small\" class=\"ml-2\" />\n          </div>\n          <div class=\"parts-list\">\n            <n-button\n              v-for=\"page in videoDetail.pages\"\n              :key=\"page.cid\"\n              :type=\"isCurrentPlayingPage(page) ? 'primary' : 'default'\"\n              :disabled=\"partLoading\"\n              size=\"small\"\n              class=\"part-item\"\n              @click=\"switchPage(page)\"\n            >\n              {{ page.part }}\n            </n-button>\n          </div>\n        </div>\n\n        <!-- 底部留白 -->\n        <div class=\"pb-20\"></div>\n      </div>\n    </n-scrollbar>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useMessage } from 'naive-ui';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport {\n  createSongFromBilibiliVideo as createBilibiliSong,\n  getBilibiliPlayUrl,\n  getBilibiliProxyUrl,\n  getBilibiliVideoDetail\n} from '@/api/bilibili';\nimport { usePlayerStore } from '@/store/modules/player';\nimport type { IBilibiliPage, IBilibiliVideoDetail } from '@/types/bilibili';\nimport type { SongResult } from '@/types/music';\nimport { setAnimationClass } from '@/utils';\n\ndefineOptions({\n  name: 'BilibiliPlayer'\n});\n\n// 使用路由获取参数\nconst route = useRoute();\nconst router = useRouter();\nconst message = useMessage();\nconst playerStore = usePlayerStore();\nconst { t } = useI18n();\n\n// 从路由参数获取bvid\nconst bvid = computed(() => route.params.bvid as string);\n\nconst isLoading = ref(true); // 初始加载状态\nconst partLoading = ref(false); // 分P加载状态，仅影响分P选择\nconst errorMessage = ref('');\nconst videoDetail = ref<IBilibiliVideoDetail | null>(null);\nconst currentPage = ref<IBilibiliPage | null>(null);\nconst audioList = ref<SongResult[]>([]);\n\n// 只在初始加载时应用动画\nconst initialLoadDone = ref(false);\nconst mainContentAnimation = computed(() => {\n  if (!initialLoadDone.value) {\n    return setAnimationClass('animate__fadeInDown');\n  }\n  return '';\n});\n\nconst partsListAnimation = computed(() => {\n  if (!initialLoadDone.value) {\n    return setAnimationClass('animate__fadeInUp');\n  }\n  return '';\n});\n\n// 监听bvid变化\nwatch(\n  () => bvid.value,\n  async (newBvid) => {\n    if (newBvid) {\n      // 新的视频ID，重置初始加载状态\n      initialLoadDone.value = false;\n      await loadVideoDetail(newBvid);\n    }\n  }\n);\n\n// 组件挂载时加载数据\nonMounted(async () => {\n  if (bvid.value) {\n    await loadVideoDetail(bvid.value);\n  } else {\n    message.error(t('bilibili.player.errors.invalidVideoId'));\n    router.back();\n  }\n});\n\nconst loadVideoDetail = async (bvid: string) => {\n  if (!bvid) return;\n\n  isLoading.value = true;\n  errorMessage.value = '';\n  audioList.value = [];\n\n  try {\n    console.log('加载B站视频详情:', bvid);\n    const res = await getBilibiliVideoDetail(bvid);\n    console.log('B站视频详情数据:', res.data);\n\n    // 确保响应式数据更新\n    videoDetail.value = JSON.parse(JSON.stringify(res.data));\n\n    // 默认加载第一个分P\n    if (videoDetail.value?.pages && videoDetail.value.pages.length > 0) {\n      console.log('视频有多个分P，共', videoDetail.value.pages.length, '个');\n      const [firstPage] = videoDetail.value.pages;\n      currentPage.value = firstPage;\n      await loadVideoSource();\n    } else {\n      console.log('视频无分P或分P数据为空');\n      errorMessage.value = t('bilibili.player.errors.loadPartInfoFailed');\n    }\n  } catch (error) {\n    console.error('获取视频详情失败', error);\n    errorMessage.value = t('bilibili.player.errors.loadVideoDetailFailed');\n  } finally {\n    isLoading.value = false;\n    // 标记初始加载完成\n    initialLoadDone.value = true;\n  }\n};\n\nconst loadVideoSource = async () => {\n  if (!bvid.value || !currentPage.value?.cid) {\n    console.error('缺少必要参数:', { bvid: bvid.value, cid: currentPage.value?.cid });\n    return;\n  }\n\n  isLoading.value = true;\n  errorMessage.value = '';\n\n  try {\n    console.log('加载音频源:', bvid.value, currentPage.value.cid);\n\n    // 将当前视频转换为音频格式加入播放列表\n    const tempAudio = createSongFromBilibiliVideo(); // 创建一个临时对象，还没有URL\n\n    // 加载当前分P的音频URL\n    const currentAudio = await loadSongUrl(currentPage.value, tempAudio);\n\n    // 将所有分P添加到播放列表\n    if (videoDetail.value?.pages) {\n      audioList.value = videoDetail.value.pages.map((page, index) => {\n        // 第一个分P直接使用已获取的音频URL\n        if (index === 0 && currentPage.value?.cid === page.cid) {\n          return currentAudio;\n        }\n\n        // 其他分P创建占位对象，稍后按需加载 - 使用公用方法\n        return createBilibiliSong(videoDetail.value!, page, bvid.value);\n      });\n      console.log('已生成音频列表，共', audioList.value.length, '首');\n\n      // 预加载下一集\n      if (audioList.value.length > 1) {\n        const nextIndex = 1; // 默认加载第二个分P\n        const nextPage = videoDetail.value.pages[nextIndex];\n        const nextAudio = audioList.value[nextIndex];\n        loadSongUrl(nextPage, nextAudio).catch((e) => console.warn('预加载下一个分P失败:', e));\n      }\n    }\n  } catch (error) {\n    console.error('获取音频播放地址失败', error);\n    errorMessage.value = t('bilibili.player.errors.loadAudioUrlFailed');\n  } finally {\n    isLoading.value = false;\n  }\n};\n\nconst createSongFromBilibiliVideo = (): SongResult => {\n  if (!videoDetail.value || !currentPage.value) {\n    throw new Error('视频详情未加载');\n  }\n\n  // 使用公用方法创建SongResult\n  return createBilibiliSong(videoDetail.value, currentPage.value, bvid.value);\n};\n\nconst loadSongUrl = async (\n  page: IBilibiliPage,\n  songItem: SongResult,\n  forceRefresh: boolean = false\n) => {\n  if (songItem.playMusicUrl && !forceRefresh) return songItem; // 如果已有URL且不强制刷新则直接返回\n\n  try {\n    console.log(`加载分P音频URL: ${page.part}, cid: ${page.cid}`);\n    const res = await getBilibiliPlayUrl(bvid.value, page.cid);\n    const playUrlData = res.data;\n    let url = '';\n\n    // 尝试获取音频URL\n    if (playUrlData.dash && playUrlData.dash.audio && playUrlData.dash.audio.length > 0) {\n      url = playUrlData.dash.audio[0].baseUrl;\n      console.log('获取到dash音频URL:', url);\n    } else if (playUrlData.durl && playUrlData.durl.length > 0) {\n      url = playUrlData.durl[0].url;\n      console.log('获取到durl音频URL:', url);\n    } else {\n      throw new Error('未找到可用的音频地址');\n    }\n\n    // 设置代理URL\n    songItem.playMusicUrl = getBilibiliProxyUrl(url);\n    return songItem;\n  } catch (error) {\n    console.error(`加载分P音频URL失败: ${page.part}`, error);\n    return songItem;\n  }\n};\n\nconst switchPage = async (page: IBilibiliPage) => {\n  if (partLoading.value || currentPage.value?.cid === page.cid) return;\n\n  console.log('切换到分P:', page.part);\n  // 立即更新UI选中状态\n  currentPage.value = page;\n\n  // 查找对应的音频项\n  const audioItem = audioList.value.find((item) => item.bilibiliData?.cid === page.cid);\n\n  if (audioItem) {\n    // 设置局部加载状态\n    try {\n      partLoading.value = true;\n      // 每次切换分P都强制重新加载音频URL，以解决之前的URL可能失效的问题\n      await loadSongUrl(page, audioItem, true);\n      // 切换后自动播放\n      playCurrentAudio();\n    } catch (error) {\n      console.error('切换分P时加载音频URL失败:', error);\n      message.error(t('bilibili.player.errors.switchPartFailed'));\n    } finally {\n      partLoading.value = false;\n    }\n  } else {\n    console.error('未找到对应的音频项');\n    message.error(t('bilibili.player.errors.switchPartFailed'));\n  }\n};\n\nconst playCurrentAudio = async () => {\n  if (audioList.value.length === 0) {\n    console.error('音频列表为空');\n    errorMessage.value = t('bilibili.player.errors.audioListEmpty');\n    return;\n  }\n\n  // 获取当前分P的音频\n  const currentIndex = audioList.value.findIndex(\n    (item) => item.bilibiliData?.cid === currentPage.value?.cid\n  );\n\n  if (currentIndex === -1) {\n    console.error('未找到当前分P的音频');\n    errorMessage.value = t('bilibili.player.errors.currentPartNotFound');\n    return;\n  }\n\n  const currentAudio = audioList.value[currentIndex];\n  console.log('准备播放当前选中的分P:', currentAudio.name);\n\n  try {\n    // 每次播放前都强制重新加载当前分P的音频URL（解决可能的URL失效问题）\n    partLoading.value = true;\n    await loadSongUrl(currentPage.value!, currentAudio, true);\n\n    if (!currentAudio.playMusicUrl) {\n      throw new Error('获取音频URL失败');\n    }\n\n    // 预加载下一个分P的音频URL（如果有）\n    const nextIndex = (currentIndex + 1) % audioList.value.length;\n    if (nextIndex !== currentIndex) {\n      const nextAudio = audioList.value[nextIndex];\n      const nextPage = videoDetail.value!.pages.find((p) => p.cid === nextAudio.bilibiliData?.cid);\n\n      if (nextPage) {\n        console.log('预加载下一个分P:', nextPage.part);\n        loadSongUrl(nextPage, nextAudio).catch((e) => console.warn('预加载下一个分P失败:', e));\n      }\n    }\n\n    // 将B站音频列表设置为播放列表\n    playerStore.setPlayList(audioList.value);\n\n    // 播放当前选中的分P\n    console.log('播放当前选中的分P:', currentAudio.name, '音频URL:', currentAudio.playMusicUrl);\n    playerStore.setPlay(currentAudio);\n\n    // 播放后通知用户已开始播放\n    message.success(t('bilibili.player.playStarted'));\n  } catch (error) {\n    console.error('播放音频失败:', error);\n    errorMessage.value = error instanceof Error ? error.message : '播放失败，请重试';\n  } finally {\n    partLoading.value = false;\n  }\n};\n\n/**\n * 格式化总时长\n */\nconst formatTotalDuration = (seconds?: number) => {\n  if (!seconds) return '00:00:00';\n\n  const hours = Math.floor(seconds / 3600);\n  const minutes = Math.floor((seconds % 3600) / 60);\n  const remainingSeconds = seconds % 60;\n\n  return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;\n};\n\n/**\n * 格式化数字显示\n */\nconst formatNumber = (num?: number) => {\n  if (!num) return '0';\n  if (num >= 10000) {\n    return `${(num / 10000).toFixed(1)}万`;\n  }\n  return num.toString();\n};\n\n// 判断是否是当前正在播放的分P\nconst isCurrentPlayingPage = (page: IBilibiliPage) => {\n  // 只根据播放器状态判断，不再使用UI选中状态\n  const currentPlayingMusic = playerStore.playMusic as any;\n  if (\n    currentPlayingMusic &&\n    typeof currentPlayingMusic === 'object' &&\n    currentPlayingMusic.bilibiliData\n  ) {\n    // 比较当前播放的音频的cid与此分P的cid\n    return (\n      currentPlayingMusic.bilibiliData.cid === page.cid &&\n      currentPlayingMusic.bilibiliData.bvid === bvid.value\n    );\n  }\n\n  // 如果没有正在播放的音乐，则使用UI选择状态\n  return currentPage.value?.cid === page.cid;\n};\n\n// 监听播放器状态变化，保持分P列表选中状态同步\nwatch(\n  () => playerStore.playMusic,\n  (newMusic: any) => {\n    if (\n      newMusic &&\n      typeof newMusic === 'object' &&\n      newMusic.bilibiliData &&\n      newMusic.bilibiliData.bvid === bvid.value\n    ) {\n      // 查找对应的分P\n      const playingPage = videoDetail.value?.pages?.find(\n        (p) => p.cid === newMusic.bilibiliData.cid\n      );\n\n      // 无条件更新UI状态以确保UI状态与播放状态一致\n      if (playingPage) {\n        currentPage.value = playingPage;\n      }\n    }\n  }\n);\n</script>\n\n<style scoped lang=\"scss\">\n.bilibili-player-page {\n  @apply h-full flex flex-col;\n\n  .content-scrollbar {\n    @apply flex-1 overflow-hidden;\n  }\n\n  .content-wrapper {\n    @apply flex flex-col p-4;\n  }\n}\n\n.bilibili-info-wrapper {\n  @apply flex flex-col md:flex-row gap-4 w-full;\n\n  .bilibili-cover {\n    @apply relative w-full md:w-1/3 aspect-video rounded-lg overflow-hidden;\n\n    .cover-image {\n      @apply w-full h-full object-cover;\n    }\n\n    .play-overlay {\n      @apply absolute inset-0;\n\n      .play-icon-bg {\n        @apply absolute inset-0 flex items-center justify-center bg-black/40 text-white opacity-0 hover:opacity-100 transition-opacity cursor-pointer;\n\n        i {\n          @apply text-4xl;\n        }\n      }\n\n      .corner-play-button {\n        @apply absolute right-3 bottom-3 shadow-lg flex items-center gap-1 px-4 py-1 text-sm transition-all duration-200;\n\n        &:hover {\n          @apply transform scale-110;\n        }\n\n        i {\n          @apply text-xl;\n        }\n      }\n    }\n  }\n}\n\n.loading-wrapper,\n.error-wrapper {\n  @apply w-full flex flex-col items-center justify-center py-16 rounded-lg bg-gray-100 dark:bg-gray-800;\n  aspect-ratio: 16/9;\n\n  p {\n    @apply mt-4 text-gray-600 dark:text-gray-400;\n  }\n}\n\n.error-wrapper {\n  button {\n    @apply mt-4;\n  }\n}\n\n.video-info {\n  @apply flex-1 p-4 rounded-lg bg-gray-100 dark:bg-gray-800;\n\n  .title {\n    @apply text-lg font-medium mb-4 text-gray-900 dark:text-white;\n  }\n\n  .author {\n    @apply flex items-center text-sm mb-2;\n  }\n\n  .stats {\n    @apply flex gap-4 text-xs text-gray-500 dark:text-gray-400 mb-3;\n  }\n\n  .description {\n    @apply text-sm text-gray-700 dark:text-gray-300 whitespace-pre-wrap mb-3;\n    max-height: 100px;\n    overflow-y: auto;\n  }\n\n  .duration {\n    @apply text-sm text-gray-600 dark:text-gray-400;\n  }\n}\n\n.video-parts {\n  @apply mt-4;\n\n  .parts-title {\n    @apply text-sm font-medium mb-2 flex items-center;\n  }\n\n  .parts-list {\n    @apply flex flex-wrap gap-2 pb-4;\n\n    .part-item {\n      @apply text-xs mb-2;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/download/DownloadPage.vue",
    "content": "<template>\n  <div class=\"download-page\">\n    <div class=\"page-header\">\n      <h1 class=\"page-title\">{{ t('download.title') }}</h1>\n      <div class=\"flex items-center gap-3\">\n        <n-button size=\"small\" @click=\"showSettingsDrawer = true\">\n          <template #icon><i class=\"iconfont ri-settings-3-line\"></i></template>\n          {{ t('download.settings') }}\n        </n-button>\n        <div class=\"segment-control\">\n          <div\n            class=\"segment-item\"\n            :class=\"{ active: tabName === 'downloading' }\"\n            @click=\"tabName = 'downloading'\"\n          >\n            {{ t('download.tabs.downloading') }}\n          </div>\n          <div\n            class=\"segment-item\"\n            :class=\"{ active: tabName === 'downloaded' }\"\n            @click=\"tabName = 'downloaded'\"\n          >\n            {{ t('download.tabs.downloaded') }}\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <div class=\"page-content\">\n      <!-- 下载列表 -->\n      <div v-show=\"tabName === 'downloading'\" class=\"tab-content\">\n        <div class=\"download-list\">\n          <div v-if=\"downloadList.length === 0\" class=\"empty-state\">\n            <div class=\"empty-icon\">\n              <i class=\"iconfont ri-download-cloud-2-line\"></i>\n            </div>\n            <h3 class=\"empty-title\">{{ t('download.empty.noTasks') }}</h3>\n          </div>\n          <template v-else>\n            <div class=\"total-progress\">\n              <div class=\"progress-header\">\n                <div class=\"progress-title\">\n                  {{ t('download.progress.total', { progress: totalProgress.toFixed(1) }) }}\n                </div>\n                <div class=\"progress-info\">{{ downloadList.length }} {{ t('download.items') }}</div>\n              </div>\n              <div class=\"progress-bar-wrapper\">\n                <div class=\"progress-bar\">\n                  <div class=\"progress-fill\" :style=\"{ width: `${totalProgress}%` }\"></div>\n                </div>\n              </div>\n            </div>\n\n            <div class=\"download-items\">\n              <div v-for=\"item in downloadList\" :key=\"item.path\" class=\"download-item\">\n                <div class=\"item-left flex items-center gap-3\">\n                  <div class=\"item-cover\">\n                    <img :src=\"getImgUrl(item.songInfo?.picUrl, '200y200')\" alt=\"Cover\" />\n                  </div>\n                  <div class=\"item-info flex items-center gap-4 w-full\">\n                    <div\n                      class=\"item-name min-w-[160px] max-w-[160px] truncate\"\n                      :title=\"item.filename\"\n                    >\n                      {{ item.filename }}\n                    </div>\n                    <div class=\"item-artist min-w-[120px] max-w-[120px] truncate\">\n                      {{\n                        item.songInfo?.ar?.map((a) => a.name).join(', ') ||\n                        t('download.artist.unknown')\n                      }}\n                    </div>\n                    <div class=\"item-progress flex-1 min-w-0\">\n                      <div class=\"progress-bar\">\n                        <div\n                          class=\"progress-fill\"\n                          :class=\"[`status-${item.status}`]\"\n                          :style=\"{ width: `${item.progress}%` }\"\n                        ></div>\n                      </div>\n                    </div>\n                    <div class=\"item-details min-w-[120px] max-w-[120px] flex flex-col items-end\">\n                      <span class=\"item-size\">\n                        {{ formatSize(item.loaded) }} / {{ formatSize(item.total) }}\n                      </span>\n                      <span class=\"item-status-badge\" :class=\"[`status-${item.status}`]\">\n                        {{ getStatusText(item) }}\n                      </span>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </template>\n        </div>\n      </div>\n\n      <!-- 已下载列表 -->\n      <div v-show=\"tabName === 'downloaded'\" class=\"tab-content\">\n        <div class=\"downloaded-list\">\n          <div v-if=\"isLoadingDownloaded\" class=\"loading-state\">\n            <div class=\"spinner\"></div>\n            <span class=\"loading-text\">{{ t('download.loading') }}</span>\n          </div>\n          <div v-else-if=\"downloadedList.length === 0\" class=\"empty-state\">\n            <div class=\"empty-icon\">\n              <i class=\"iconfont ri-inbox-archive-line\"></i>\n            </div>\n            <h3 class=\"empty-title\">{{ t('download.empty.noDownloaded') }}</h3>\n            <p class=\"empty-text\">{{ t('download.empty.noDownloadedHint') }}</p>\n          </div>\n          <template v-else>\n            <div class=\"downloaded-header\">\n              <div class=\"header-info\">\n                <i class=\"iconfont ri-archive-line\"></i>\n                <span>{{ t('download.count', { count: downloadedList.length }) }}</span>\n              </div>\n              <button class=\"clear-button\" @click=\"showClearConfirm = true\">\n                <i class=\"iconfont ri-delete-bin-line\"></i>\n                <span>{{ t('download.clearAll') }}</span>\n              </button>\n            </div>\n\n            <div class=\"downloaded-items\">\n              <div v-for=\"item in downList\" :key=\"item.path\" class=\"downloaded-item\">\n                <div class=\"item-cover\">\n                  <img :src=\"getImgUrl(item.picUrl, '200y200')\" alt=\"Cover\" />\n                </div>\n                <div class=\"item-info flex items-center gap-4 w-full\">\n                  <div\n                    class=\"item-name min-w-[160px] max-w-[160px] truncate\"\n                    :title=\"item.displayName || item.filename\"\n                  >\n                    {{ item.displayName || item.filename }}\n                  </div>\n                  <div\n                    class=\"item-artist min-w-[120px] max-w-[120px] flex items-center gap-1 truncate\"\n                  >\n                    <i class=\"iconfont ri-user-line\"></i>\n                    <span>{{ item.ar?.map((a) => a.name).join(', ') }}</span>\n                  </div>\n                  <div class=\"item-size min-w-[80px] max-w-[80px] flex items-center gap-1\">\n                    <i class=\"iconfont ri-file-line\"></i>\n                    <span>{{ formatSize(item.size) }}</span>\n                  </div>\n                  <div\n                    class=\"item-path min-w-[220px] max-w-[220px] flex items-center gap-1\"\n                    :title=\"item.path\"\n                  >\n                    <i class=\"iconfont ri-folder-path-line\"></i>\n                    <span>{{ shortenPath(item.path) }}</span>\n                    <button class=\"copy-button\" @click=\"copyPath(item.path)\">\n                      <i class=\"iconfont ri-file-copy-line\"></i>\n                    </button>\n                  </div>\n                  <div class=\"item-actions flex gap-1 ml-2\">\n                    <button class=\"action-btn play\" @click=\"handlePlayMusic(item)\">\n                      <i class=\"iconfont ri-play-circle-line\"></i>\n                    </button>\n                    <button class=\"action-btn open\" @click=\"openDirectory(item.path)\">\n                      <i class=\"iconfont ri-folder-open-line\"></i>\n                    </button>\n                    <button class=\"action-btn delete\" @click=\"handleDelete(item)\">\n                      <i class=\"iconfont ri-delete-bin-line\"></i>\n                    </button>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </template>\n        </div>\n      </div>\n    </div>\n  </div>\n\n  <!-- 删除确认对话框 -->\n  <div class=\"modal-overlay\" v-if=\"showDeleteConfirm\" @click=\"showDeleteConfirm = false\">\n    <div class=\"modal-content\" @click.stop>\n      <div class=\"modal-header\">\n        <i class=\"iconfont ri-error-warning-line\"></i>\n        <span>{{ t('download.delete.title') }}</span>\n      </div>\n      <div class=\"modal-body\">\n        {{\n          t('download.delete.message', {\n            filename: itemToDelete?.displayName || itemToDelete?.filename\n          })\n        }}\n      </div>\n      <div class=\"modal-footer\">\n        <button class=\"modal-btn cancel\" @click=\"showDeleteConfirm = false\">\n          {{ t('download.delete.cancel') }}\n        </button>\n        <button class=\"modal-btn confirm\" @click=\"confirmDelete\">\n          {{ t('download.delete.confirm') }}\n        </button>\n      </div>\n    </div>\n  </div>\n\n  <!-- 清空确认对话框 -->\n  <div class=\"modal-overlay\" v-if=\"showClearConfirm\" @click=\"showClearConfirm = false\">\n    <div class=\"modal-content\" @click.stop>\n      <div class=\"modal-header\">\n        <i class=\"iconfont ri-delete-bin-line\"></i>\n        <span>{{ t('download.clear.title') }}</span>\n      </div>\n      <div class=\"modal-body\">\n        {{ t('download.clear.message') }}\n      </div>\n      <div class=\"modal-footer\">\n        <button class=\"modal-btn cancel\" @click=\"showClearConfirm = false\">\n          {{ t('download.clear.cancel') }}\n        </button>\n        <button class=\"modal-btn confirm\" @click=\"clearDownloadRecords\">\n          {{ t('download.clear.confirm') }}\n        </button>\n      </div>\n    </div>\n  </div>\n\n  <!-- 下载设置抽屉 -->\n  <n-drawer v-model:show=\"showSettingsDrawer\" :width=\"380\" placement=\"right\" :z-index=\"999999999\">\n    <n-drawer-content :native-scrollbar=\"false\">\n      <template #header>\n        <div class=\"flex items-center justify-between\">\n          <div class=\"text-lg font-bold\">{{ t('download.settingsPanel.title') }}</div>\n          <n-button type=\"primary\" @click=\"saveDownloadSettings\">\n            {{ t('common.save') }}\n          </n-button>\n        </div>\n      </template>\n      <div class=\"download-settings\">\n        <!-- 下载路径设置 -->\n        <div class=\"setting-item\">\n          <div class=\"setting-title\">{{ t('download.settingsPanel.path') }}</div>\n          <div class=\"setting-desc\">{{ t('download.settingsPanel.pathDesc') }}</div>\n          <div class=\"flex flex-col gap-2 mt-2\">\n            <n-input\n              v-model:value=\"downloadSettings.path\"\n              :placeholder=\"t('download.settingsPanel.pathPlaceholder')\"\n              readonly\n              class=\"flex-1\"\n            />\n            <div class=\"flex items-center gap-2\">\n              <n-button class=\"flex-1\" @click=\"selectDownloadPath\">{{\n                t('download.settingsPanel.select')\n              }}</n-button>\n              <n-button class=\"flex-1\" @click=\"openDownloadPath\">\n                {{ t('download.settingsPanel.open') }}\n                <i class=\"iconfont ri-folder-open-line\"></i>\n              </n-button>\n            </div>\n          </div>\n        </div>\n\n        <!-- 文件名格式设置 -->\n        <div class=\"setting-item\">\n          <div class=\"setting-title\">{{ t('download.settingsPanel.fileFormat') }}</div>\n          <div class=\"setting-desc\">{{ t('download.settingsPanel.fileFormatDesc') }}</div>\n\n          <!-- 预设模板 -->\n          <div class=\"flex gap-2 my-2\">\n            <n-button\n              size=\"small\"\n              :type=\"\n                downloadSettings.nameFormat === '{songName} - {artistName}' ? 'primary' : 'default'\n              \"\n              @click=\"downloadSettings.nameFormat = '{songName} - {artistName}'\"\n            >\n              {{ t('download.settingsPanel.presets.songArtist') }}\n            </n-button>\n            <n-button\n              size=\"small\"\n              :type=\"\n                downloadSettings.nameFormat === '{artistName} - {songName}' ? 'primary' : 'default'\n              \"\n              @click=\"downloadSettings.nameFormat = '{artistName} - {songName}'\"\n            >\n              {{ t('download.settingsPanel.presets.artistSong') }}\n            </n-button>\n            <n-button\n              size=\"small\"\n              :type=\"downloadSettings.nameFormat === '{songName}' ? 'primary' : 'default'\"\n              @click=\"downloadSettings.nameFormat = '{songName}'\"\n            >\n              {{ t('download.settingsPanel.presets.songOnly') }}\n            </n-button>\n          </div>\n\n          <!-- 分隔符设置 -->\n          <div class=\"my-3\">\n            <div class=\"text-sm text-gray-500 mb-2\">\n              {{ t('download.settingsPanel.separator') || '分隔符' }}\n            </div>\n            <div class=\"flex items-center gap-2\">\n              <n-button\n                size=\"small\"\n                :type=\"downloadSettings.separator === ' - ' ? 'primary' : 'default'\"\n                @click=\"downloadSettings.separator = ' - '\"\n              >\n                {{ t('download.settingsPanel.separators.dash') || '空格-空格' }}\n              </n-button>\n              <n-button\n                size=\"small\"\n                :type=\"downloadSettings.separator === '_' ? 'primary' : 'default'\"\n                @click=\"downloadSettings.separator = '_'\"\n              >\n                {{ t('download.settingsPanel.separators.underscore') || '下划线' }}\n              </n-button>\n              <n-button\n                size=\"small\"\n                :type=\"downloadSettings.separator === ' ' ? 'primary' : 'default'\"\n                @click=\"downloadSettings.separator = ' '\"\n              >\n                {{ t('download.settingsPanel.separators.space') || '空格' }}\n              </n-button>\n              <n-input\n                v-model:value=\"downloadSettings.separator\"\n                size=\"small\"\n                style=\"width: 100px\"\n                placeholder=\"自定义\"\n              />\n            </div>\n          </div>\n\n          <!-- 组件排序 -->\n          <div class=\"my-3\">\n            <div class=\"text-sm text-gray-500 mb-2\">\n              {{ t('download.settingsPanel.dragToArrange') }}\n            </div>\n            <div class=\"format-components\">\n              <div\n                v-for=\"(component, index) in formatComponents\"\n                :key=\"component.id\"\n                class=\"format-item\"\n              >\n                <div class=\"flex items-center justify-between w-full\">\n                  <span>{{ t(`download.settingsPanel.components.${component.type}`) }}</span>\n                  <div class=\"flex items-center\">\n                    <n-button\n                      quaternary\n                      circle\n                      size=\"small\"\n                      @click=\"handleMoveUp(index)\"\n                      :disabled=\"index === 0\"\n                    >\n                      <template #icon><i class=\"iconfont ri-arrow-up-s-line\"></i></template>\n                    </n-button>\n                    <n-button\n                      quaternary\n                      circle\n                      size=\"small\"\n                      @click=\"handleMoveDown(index)\"\n                      :disabled=\"index === formatComponents.length - 1\"\n                    >\n                      <template #icon><i class=\"iconfont ri-arrow-down-s-line\"></i></template>\n                    </n-button>\n                    <n-button\n                      quaternary\n                      circle\n                      size=\"small\"\n                      @click=\"removeFormatComponent(index)\"\n                      :disabled=\"formatComponents.length <= 1\"\n                    >\n                      <template #icon><i class=\"iconfont ri-close-line\"></i></template>\n                    </n-button>\n                  </div>\n                </div>\n              </div>\n              <div class=\"mt-2 flex gap-2\">\n                <n-button\n                  size=\"small\"\n                  @click=\"addFormatComponent('songName')\"\n                  :disabled=\"formatComponents.some((c) => c.type === 'songName')\"\n                >\n                  +{{ t('download.settingsPanel.components.songName') }}\n                </n-button>\n                <n-button\n                  size=\"small\"\n                  @click=\"addFormatComponent('artistName')\"\n                  :disabled=\"formatComponents.some((c) => c.type === 'artistName')\"\n                >\n                  +{{ t('download.settingsPanel.components.artistName') }}\n                </n-button>\n                <n-button\n                  size=\"small\"\n                  @click=\"addFormatComponent('albumName')\"\n                  :disabled=\"formatComponents.some((c) => c.type === 'albumName')\"\n                >\n                  +{{ t('download.settingsPanel.components.albumName') }}\n                </n-button>\n              </div>\n            </div>\n          </div>\n\n          <!-- 自定义格式 -->\n          <div class=\"my-3\">\n            <div class=\"text-sm text-gray-500 mb-2\">\n              {{ t('download.settingsPanel.customFormat') }}\n            </div>\n            <n-input\n              v-model:value=\"downloadSettings.nameFormat\"\n              placeholder=\"{artistName} - {songName} - {albumName}\"\n            />\n          </div>\n\n          <div class=\"mt-2 text-xs text-amber-500\">\n            <i class=\"iconfont ri-information-line\"></i>\n            {{ t('download.settingsPanel.formatVariables') }}:<br />\n            {songName}, {artistName}, {albumName}\n          </div>\n\n          <!-- 预览 -->\n          <div class=\"format-preview mt-3 bg-gray-100 dark:bg-dark-300 p-2 rounded\">\n            <div class=\"text-xs text-gray-500 mb-1\">{{ t('download.settingsPanel.preview') }}</div>\n            <div class=\"preview-content\">{{ formatNamePreview }}</div>\n          </div>\n        </div>\n      </div>\n    </n-drawer-content>\n  </n-drawer>\n</template>\n\n<script setup lang=\"ts\">\nimport { useMessage } from 'naive-ui';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { getMusicDetail } from '@/api/music';\nimport { usePlayerStore } from '@/store/modules/player';\nimport type { SongResult } from '@/types/music';\nimport { getImgUrl } from '@/utils';\n\nconst { t } = useI18n();\nconst playerStore = usePlayerStore();\nconst message = useMessage();\n\ninterface DownloadItem {\n  filename: string;\n  progress: number;\n  loaded: number;\n  total: number;\n  path: string;\n  status: 'downloading' | 'completed' | 'error';\n  error?: string;\n  songInfo?: any;\n}\n\ninterface DownloadedItem {\n  filename: string;\n  path: string;\n  size: number;\n  id: number;\n  picUrl: string;\n  ar: { name: string }[];\n  displayName?: string;\n}\nconst tabName = ref('downloading');\n\nconst downloadList = ref<DownloadItem[]>([]);\nconst downloadedList = ref<DownloadedItem[]>(\n  JSON.parse(localStorage.getItem('downloadedList') || '[]')\n);\n\nconst downList = computed(() => downloadedList.value);\n\n// 计算总进度\nconst totalProgress = computed(() => {\n  if (downloadList.value.length === 0) return 0;\n  const total = downloadList.value.reduce((sum, item) => sum + item.progress, 0);\n  return total / downloadList.value.length;\n});\n\nwatch(totalProgress, (newVal) => {\n  if (newVal === 100) {\n    refreshDownloadedList();\n  }\n});\n\n// 获取状态文本\nconst getStatusText = (item: DownloadItem) => {\n  switch (item.status) {\n    case 'downloading':\n      return t('download.status.downloading');\n    case 'completed':\n      return t('download.status.completed');\n    case 'error':\n      return t('download.status.failed');\n    default:\n      return t('download.status.unknown');\n  }\n};\n\n// 格式化文件大小\nconst formatSize = (bytes: number) => {\n  if (!bytes) return '0 B';\n  const k = 1024;\n  const sizes = ['B', 'KB', 'MB', 'GB'];\n  const i = Math.floor(Math.log(bytes) / Math.log(k));\n  return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`;\n};\n\n// 复制文件路径\nconst copyPath = (path: string) => {\n  navigator.clipboard\n    .writeText(path)\n    .then(() => {\n      message.success(t('download.path.copied'));\n    })\n    .catch((err) => {\n      console.error('复制失败:', err);\n      message.error(t('download.path.copyFailed'));\n    });\n};\n\n// 格式化路径\nconst shortenPath = (path: string) => {\n  if (!path) return '';\n\n  // 获取文件名和目录\n  const parts = path.split(/[/\\\\]/);\n  const fileName = parts.pop() || '';\n\n  // 如果路径很短，直接返回\n  if (path.length < 30) return path;\n\n  // 保留开头的部分目录和结尾的文件名\n  if (parts.length <= 2) return path;\n\n  const start = parts.slice(0, 1).join('/');\n  const end = parts.slice(-1).join('/');\n\n  return `${start}/.../${end}/${fileName}`;\n};\n\n// 获取本地文件URL\nconst getLocalFilePath = (path: string) => {\n  if (!path) return '';\n  // 确保URL格式正确\n  return `local:///${encodeURIComponent(path)}`;\n};\n\n// 打开目录\nconst openDirectory = (path: string) => {\n  window.electron.ipcRenderer.send('open-directory', path);\n};\n\n// 播放音乐\nconst handlePlayMusic = async (item: DownloadedItem) => {\n  try {\n    // 先检查文件是否存在\n    const fileExists = await window.electron.ipcRenderer.invoke('check-file-exists', item.path);\n\n    if (!fileExists) {\n      message.error(t('download.delete.fileNotFound', { name: item.displayName || item.filename }));\n      return;\n    }\n\n    // 转换下载项为播放所需的歌曲对象\n    const song: SongResult = {\n      id: item.id,\n      name: item.displayName || item.filename,\n      ar:\n        item.ar?.map((a) => ({\n          id: 0,\n          name: a.name,\n          picId: 0,\n          img1v1Id: 0,\n          briefDesc: '',\n          picUrl: '',\n          img1v1Url: '',\n          albumSize: 0,\n          alias: [],\n          trans: '',\n          musicSize: 0,\n          topicPerson: 0\n        })) || [],\n      al: {\n        name: item.filename,\n        id: 0,\n        picUrl: item.picUrl,\n        pic: 0,\n        picId: 0\n      } as any,\n      picUrl: item.picUrl,\n      // 使用本地文件协议\n      playMusicUrl: getLocalFilePath(item.path),\n      source: 'netease' as 'netease',\n      count: 0\n    };\n\n    console.log('开始播放本地音乐:', song.name, '路径:', song.playMusicUrl);\n\n    // 播放歌曲\n    await playerStore.setPlay(song);\n    playerStore.setPlayMusic(true);\n    playerStore.setIsPlay(true);\n\n    message.success(t('download.playStarted', { name: item.displayName || item.filename }));\n  } catch (error) {\n    console.error('播放音乐失败:', error);\n    message.error(t('download.playFailed', { name: item.displayName || item.filename }));\n  }\n};\n\n// 删除相关\nconst showDeleteConfirm = ref(false);\nconst itemToDelete = ref<DownloadedItem | null>(null);\n\n// 处理删除点击\nconst handleDelete = (item: DownloadedItem) => {\n  itemToDelete.value = item;\n  showDeleteConfirm.value = true;\n};\n\n// 确认删除\nconst confirmDelete = async () => {\n  const item = itemToDelete.value;\n  if (!item) return;\n\n  try {\n    const success = await window.electron.ipcRenderer.invoke('delete-downloaded-music', item.path);\n\n    if (success) {\n      const newList = downloadedList.value.filter((i) => i.id !== item.id);\n      downloadedList.value = newList;\n      localStorage.setItem('downloadedList', JSON.stringify(newList));\n      message.success(t('download.delete.success'));\n    } else {\n      message.warning(t('download.delete.fileNotFound'));\n    }\n  } catch (error) {\n    console.error('Failed to delete music:', error);\n    message.warning(t('download.delete.recordRemoved'));\n  } finally {\n    showDeleteConfirm.value = false;\n    itemToDelete.value = null;\n  }\n};\n\n// 清空下载记录相关\nconst showClearConfirm = ref(false);\n\n// 清空下载记录\nconst clearDownloadRecords = async () => {\n  try {\n    downloadedList.value = [];\n    localStorage.setItem('downloadedList', '[]');\n    await window.electron.ipcRenderer.invoke('clear-downloaded-music');\n    message.success(t('download.clear.success'));\n  } catch (error) {\n    console.error('Failed to clear download records:', error);\n    message.error(t('download.clear.failed'));\n  } finally {\n    showClearConfirm.value = false;\n  }\n};\n\n// 添加加载状态\nconst isLoadingDownloaded = ref(false);\n\n// 格式化歌曲名称，应用用户设置的格式\nconst formatSongName = (songInfo) => {\n  if (!songInfo) return '';\n\n  // 获取格式设置\n  const nameFormat = downloadSettings.value.nameFormat || '{songName} - {artistName}';\n\n  // 准备替换变量\n  const artistName = songInfo.ar?.map((a) => a.name).join('/') || '未知艺术家';\n  const songName = songInfo.name || songInfo.filename || '未知歌曲';\n  const albumName = songInfo.al?.name || '未知专辑';\n\n  // 应用自定义格式\n  return nameFormat\n    .replace(/\\{songName\\}/g, songName)\n    .replace(/\\{artistName\\}/g, artistName)\n    .replace(/\\{albumName\\}/g, albumName);\n};\n\n// 获取已下载音乐列表\nconst refreshDownloadedList = async () => {\n  if (isLoadingDownloaded.value) return; // 防止重复加载\n\n  try {\n    isLoadingDownloaded.value = true;\n    const list = await window.electron.ipcRenderer.invoke('get-downloaded-music');\n\n    if (!Array.isArray(list) || list.length === 0) {\n      downloadedList.value = [];\n      localStorage.setItem('downloadedList', '[]');\n      return;\n    }\n\n    const songIds = list.filter((item) => item.id).map((item) => item.id);\n    if (songIds.length === 0) {\n      // 处理显示格式化文件名\n      const updatedList = list.map((item) => ({\n        ...item,\n        displayName: formatSongName(item) || item.filename\n      }));\n\n      downloadedList.value = updatedList;\n      localStorage.setItem('downloadedList', JSON.stringify(updatedList));\n      return;\n    }\n\n    try {\n      const detailRes = await getMusicDetail(songIds);\n      const songDetails = detailRes.data.songs.reduce((acc, song) => {\n        acc[song.id] = song;\n        return acc;\n      }, {});\n\n      const updatedList = list.map((item) => {\n        const songDetail = songDetails[item.id];\n        const updatedItem = {\n          ...item,\n          picUrl: songDetail?.al?.picUrl || item.picUrl || '/images/default_cover.png',\n          ar: songDetail?.ar || item.ar || [{ name: t('download.localMusic') }],\n          name: songDetail?.name || item.name || item.filename\n        };\n\n        // 添加格式化的显示名称\n        updatedItem.displayName = formatSongName(updatedItem) || updatedItem.filename;\n        return updatedItem;\n      });\n\n      downloadedList.value = updatedList;\n      localStorage.setItem('downloadedList', JSON.stringify(updatedList));\n    } catch (error) {\n      console.error('Failed to get music details:', error);\n      // 处理显示格式化文件名\n      const updatedList = list.map((item) => ({\n        ...item,\n        displayName: formatSongName(item) || item.filename\n      }));\n\n      downloadedList.value = updatedList;\n      localStorage.setItem('downloadedList', JSON.stringify(updatedList));\n    }\n  } catch (error) {\n    console.error('Failed to get downloaded music list:', error);\n    downloadedList.value = [];\n    localStorage.setItem('downloadedList', '[]');\n  } finally {\n    isLoadingDownloaded.value = false;\n  }\n};\n\nwatch(\n  () => tabName.value,\n  (newVal) => {\n    if (newVal) {\n      refreshDownloadedList();\n    }\n  }\n);\n\n// 初始化\nonMounted(() => {\n  refreshDownloadedList();\n\n  // 记录已处理的下载项，避免重复触发事件\n  const processedDownloads = new Set<string>();\n\n  // 监听下载进度\n  window.electron.ipcRenderer.on('music-download-progress', (_, data) => {\n    const existingItem = downloadList.value.find((item) => item.filename === data.filename);\n\n    // 如果进度为100%，将状态设置为已完成\n    if (data.progress === 100) {\n      data.status = 'completed';\n    }\n\n    if (existingItem) {\n      Object.assign(existingItem, {\n        ...data,\n        songInfo: data.songInfo || existingItem.songInfo\n      });\n\n      // 如果下载完成，从列表中移除，但不触发完成通知\n      // 通知由 music-download-complete 事件处理\n      if (data.status === 'completed') {\n        downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);\n      }\n    } else {\n      downloadList.value.push({\n        ...data,\n        songInfo: data.songInfo\n      });\n    }\n  });\n\n  // 监听下载完成\n  window.electron.ipcRenderer.on('music-download-complete', async (_, data) => {\n    // 如果已经处理过此文件的完成事件，则跳过\n    if (processedDownloads.has(data.filename)) {\n      return;\n    }\n\n    // 标记为已处理\n    processedDownloads.add(data.filename);\n\n    // 下载成功处理\n    if (data.success) {\n      // 从下载列表中移除\n      downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);\n\n      // 延迟刷新已下载列表，避免文件系统未完全写入\n      setTimeout(() => refreshDownloadedList(), 500);\n\n      // 只在下载页面显示一次下载成功通知\n      message.success(t('download.message.downloadComplete', { filename: data.filename }));\n\n      // 避免通知过多占用内存，设置一个超时来清理已处理的标记\n      setTimeout(() => {\n        processedDownloads.delete(data.filename);\n      }, 10000); // 10秒后清除\n    } else {\n      // 下载失败处理\n      const existingItem = downloadList.value.find((item) => item.filename === data.filename);\n      if (existingItem) {\n        Object.assign(existingItem, {\n          status: 'error',\n          error: data.error,\n          progress: 0\n        });\n        setTimeout(() => {\n          downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);\n          processedDownloads.delete(data.filename);\n        }, 3000);\n      }\n\n      message.error(\n        t('download.message.downloadFailed', { filename: data.filename, error: data.error })\n      );\n    }\n  });\n\n  // 监听下载队列\n  window.electron.ipcRenderer.on('music-download-queued', (_, data) => {\n    const existingItem = downloadList.value.find((item) => item.filename === data.filename);\n    if (!existingItem) {\n      downloadList.value.push({\n        filename: data.filename,\n        progress: 0,\n        loaded: 0,\n        total: 0,\n        path: '',\n        status: 'downloading',\n        songInfo: data.songInfo\n      });\n    }\n  });\n});\n\n// 下载设置\nconst showSettingsDrawer = ref(false);\nconst downloadSettings = ref({\n  path: '',\n  nameFormat: '{songName} - {artistName}',\n  separator: ' - '\n});\n\n// 格式组件（用于拖拽排序）\nconst formatComponents = ref([\n  { id: 1, type: 'songName' },\n  { id: 2, type: 'artistName' }\n]);\n\n// 处理组件排序\nconst handleMoveUp = (index: number) => {\n  if (index > 0) {\n    const temp = formatComponents.value.splice(index, 1)[0];\n    formatComponents.value.splice(index - 1, 0, temp);\n  }\n};\n\nconst handleMoveDown = (index: number) => {\n  if (index < formatComponents.value.length - 1) {\n    const temp = formatComponents.value.splice(index, 1)[0];\n    formatComponents.value.splice(index + 1, 0, temp);\n  }\n};\n\n// 添加新的格式组件\nconst addFormatComponent = (type: string) => {\n  if (!formatComponents.value.some((item) => item.type === type)) {\n    formatComponents.value.push({\n      id: Date.now(),\n      type\n    });\n  }\n};\n\n// 删除格式组件\nconst removeFormatComponent = (index: number) => {\n  formatComponents.value.splice(index, 1);\n};\n\n// 监听组件变化更新格式\nwatch(\n  formatComponents,\n  (newComponents) => {\n    let format = '';\n    newComponents.forEach((component, index) => {\n      format += `{${component.type}}`;\n      if (index < newComponents.length - 1) {\n        format += downloadSettings.value.separator;\n      }\n    });\n    downloadSettings.value.nameFormat = format;\n  },\n  { deep: true }\n);\n\n// 监听分隔符变化更新格式\nwatch(\n  () => downloadSettings.value.separator,\n  (newSeparator) => {\n    if (formatComponents.value.length > 1) {\n      // 重新构建格式字符串\n      let format = '';\n      formatComponents.value.forEach((component, index) => {\n        format += `{${component.type}}`;\n        if (index < formatComponents.value.length - 1) {\n          format += newSeparator;\n        }\n      });\n      downloadSettings.value.nameFormat = format;\n    }\n  }\n);\n\n// 格式名称预览\nconst formatNamePreview = computed(() => {\n  const format = downloadSettings.value.nameFormat;\n  return format\n    .replace(/\\{songName\\}/g, '莫失莫忘')\n    .replace(/\\{artistName\\}/g, '香蜜沉沉烬如霜')\n    .replace(/\\{albumName\\}/g, '电视剧原声带');\n});\n\n// 选择下载路径\nconst selectDownloadPath = async () => {\n  const result = await window.electron.ipcRenderer.invoke('select-directory');\n  if (result && !result.canceled && result.filePaths.length > 0) {\n    downloadSettings.value.path = result.filePaths[0];\n  }\n};\n\n// 打开下载路径\nconst openDownloadPath = () => {\n  if (downloadSettings.value.path) {\n    window.electron.ipcRenderer.send('open-directory', downloadSettings.value.path);\n  } else {\n    message.warning(t('download.settingsPanel.noPathSelected'));\n  }\n};\n\n// 保存下载设置\nconst saveDownloadSettings = () => {\n  // 保存到配置\n  window.electron.ipcRenderer.send(\n    'set-store-value',\n    'set.downloadPath',\n    downloadSettings.value.path\n  );\n  window.electron.ipcRenderer.send(\n    'set-store-value',\n    'set.downloadNameFormat',\n    downloadSettings.value.nameFormat\n  );\n  window.electron.ipcRenderer.send(\n    'set-store-value',\n    'set.downloadSeparator',\n    downloadSettings.value.separator\n  );\n\n  // 如果是在已下载页面，刷新列表以更新显示\n  if (tabName.value === 'downloaded') {\n    refreshDownloadedList();\n  }\n\n  message.success(t('download.settingsPanel.saveSuccess'));\n  showSettingsDrawer.value = false;\n};\n\n// 初始化下载设置\nconst initDownloadSettings = async () => {\n  // 获取当前配置\n  const path = await window.electron.ipcRenderer.invoke('get-store-value', 'set.downloadPath');\n  const nameFormat = await window.electron.ipcRenderer.invoke(\n    'get-store-value',\n    'set.downloadNameFormat'\n  );\n  const separator = await window.electron.ipcRenderer.invoke(\n    'get-store-value',\n    'set.downloadSeparator'\n  );\n\n  downloadSettings.value = {\n    path: path || (await window.electron.ipcRenderer.invoke('get-downloads-path')),\n    nameFormat: nameFormat || '{songName} - {artistName}',\n    separator: separator || ' - '\n  };\n\n  // 初始化排序组件\n  updateFormatComponents();\n};\n\n// 根据格式更新组件\nconst updateFormatComponents = () => {\n  // 提取格式中的变量\n  const format = downloadSettings.value.nameFormat;\n  const matches = Array.from(format.matchAll(/\\{(\\w+)\\}/g));\n\n  if (matches.length === 0) {\n    formatComponents.value = [\n      { id: 1, type: 'songName' },\n      { id: 2, type: 'artistName' }\n    ];\n    return;\n  }\n\n  formatComponents.value = matches.map((match, index) => ({\n    id: index + 1,\n    type: match[1]\n  }));\n};\n\n// 监听格式变化更新组件\nwatch(() => downloadSettings.value.nameFormat, updateFormatComponents);\n\n// 监听命名格式变化，更新已下载文件的显示名称\nwatch(\n  () => downloadSettings.value.nameFormat,\n  () => {\n    if (downloadedList.value.length > 0) {\n      // 更新所有已下载项的显示名称\n      downloadedList.value = downloadedList.value.map((item) => ({\n        ...item,\n        displayName: formatSongName(item) || item.filename\n      }));\n\n      // 保存到本地存储\n      localStorage.setItem('downloadedList', JSON.stringify(downloadedList.value));\n    }\n  }\n);\n\n// 初始化\nonMounted(() => {\n  initDownloadSettings();\n});\n</script>\n\n<style lang=\"scss\" scoped>\n/* macOS style with Neumorphism */\n.download-page {\n  @apply h-full w-full flex flex-col overflow-hidden;\n  @apply bg-gray-50 dark:bg-dark-200;\n  @apply text-gray-900 dark:text-gray-100;\n}\n\n.page-header {\n  @apply px-4 py-3;\n  @apply bg-white/90 dark:bg-dark-100/90;\n  @apply border-b border-gray-200/50 dark:border-gray-700/50;\n  @apply backdrop-blur-xl backdrop-saturate-150;\n  @apply sticky top-0 z-10;\n  @apply flex items-center justify-between;\n}\n\n.page-title {\n  @apply text-lg font-medium;\n  @apply text-gray-800 dark:text-gray-200;\n}\n\n.segment-control {\n  @apply flex rounded-lg overflow-hidden;\n  @apply bg-gray-100 dark:bg-dark-300;\n  @apply w-max;\n  @apply border border-gray-200/60 dark:border-gray-700/60;\n}\n\n.segment-item {\n  @apply px-3 py-1 cursor-pointer text-sm font-medium;\n  @apply text-gray-500 dark:text-gray-400;\n  @apply transition-all duration-200;\n  @apply hover:bg-light-300 dark:hover:bg-dark-300;\n\n  &.active {\n    @apply bg-white dark:bg-dark-200;\n    @apply text-gray-900 dark:text-gray-100;\n    @apply shadow-sm;\n  }\n}\n\n.page-content {\n  @apply flex-1 overflow-hidden;\n}\n\n.tab-content {\n  @apply h-full overflow-auto pb-16;\n}\n\n/* Empty & Loading States */\n.empty-state,\n.loading-state {\n  @apply flex flex-col items-center justify-center h-full;\n  @apply py-16;\n}\n\n.empty-icon {\n  @apply text-4xl mb-3 text-gray-300 dark:text-gray-600;\n}\n\n.empty-title {\n  @apply text-base font-medium mb-1;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.empty-text {\n  @apply text-xs text-gray-400 dark:text-gray-500;\n}\n\n.spinner {\n  @apply w-8 h-8 rounded-full border border-t-primary;\n  @apply animate-spin mb-3;\n}\n\n.loading-text {\n  @apply text-sm text-gray-500 dark:text-gray-400;\n}\n\n/* Progress Bar */\n.total-progress {\n  @apply px-4 py-3;\n  @apply bg-white/90 dark:bg-dark-100/90;\n  @apply backdrop-blur-lg backdrop-saturate-150;\n  @apply border-b border-gray-200/50 dark:border-gray-700/50;\n  @apply sticky top-0 z-10;\n}\n\n.progress-header {\n  @apply flex justify-between items-center mb-2;\n}\n\n.progress-title {\n  @apply text-xs font-medium;\n  @apply text-gray-700 dark:text-gray-300;\n}\n\n.progress-info {\n  @apply text-xs;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.progress-bar-wrapper {\n  @apply w-full;\n}\n\n.progress-bar {\n  @apply h-1.5 rounded-full w-full;\n  @apply bg-gray-200 dark:bg-dark-300;\n  @apply overflow-hidden;\n}\n\n.progress-fill {\n  @apply h-full rounded-full;\n  @apply bg-primary;\n  @apply transition-all duration-300;\n\n  &.status-downloading {\n    @apply bg-primary;\n  }\n\n  &.status-completed {\n    @apply bg-green-500;\n  }\n\n  &.status-error {\n    @apply bg-red-500;\n  }\n}\n\n/* Download Items */\n.download-items {\n  @apply p-4 space-y-3;\n}\n\n.download-item {\n  @apply rounded-lg p-3;\n  @apply bg-white dark:bg-dark-100;\n  @apply border border-gray-200/60 dark:border-gray-700/50;\n  @apply shadow-sm;\n  @apply transition-all duration-300;\n\n  &:hover {\n    @apply shadow-md;\n    @apply transform -translate-y-0.5;\n  }\n\n  .item-left {\n    @apply flex gap-3;\n  }\n\n  .item-cover {\n    @apply w-12 h-12 rounded-md overflow-hidden;\n    @apply flex-shrink-0;\n    @apply bg-gray-100 dark:bg-dark-300;\n    @apply shadow-sm;\n\n    img {\n      @apply w-full h-full object-cover;\n    }\n  }\n\n  .item-info {\n    @apply flex-1 min-w-0 flex items-center justify-between;\n  }\n\n  .item-name {\n    @apply text-sm font-medium truncate;\n  }\n\n  .item-artist {\n    @apply text-xs text-gray-500 dark:text-gray-400;\n  }\n\n  .item-progress {\n    @apply mb-2;\n  }\n\n  .item-details {\n    @apply flex justify-between items-center;\n  }\n\n  .item-size {\n    @apply text-xs text-gray-500 dark:text-gray-400;\n  }\n\n  .item-status-badge {\n    @apply text-xs px-2 py-0.5 rounded-full;\n    @apply bg-gray-100 dark:bg-dark-300;\n\n    &.status-downloading {\n      @apply bg-blue-50 text-blue-600 dark:bg-blue-900/20 dark:text-blue-400;\n    }\n\n    &.status-completed {\n      @apply bg-green-50 text-green-600 dark:bg-green-900/20 dark:text-green-400;\n    }\n\n    &.status-error {\n      @apply bg-red-50 text-red-600 dark:bg-red-900/20 dark:text-red-400;\n    }\n  }\n}\n\n/* Downloaded List */\n.downloaded-header {\n  @apply px-4 py-3;\n  @apply bg-white/90 dark:bg-dark-100/90;\n  @apply backdrop-blur-lg backdrop-saturate-150;\n  @apply border-b border-gray-200/50 dark:border-gray-700/50;\n  @apply sticky top-0 z-10;\n  @apply flex items-center justify-between;\n}\n\n.header-info {\n  @apply flex items-center gap-2;\n  @apply text-xs font-medium;\n  @apply text-gray-700 dark:text-gray-300;\n\n  i {\n    @apply text-gray-400 dark:text-gray-500;\n  }\n}\n\n.clear-button {\n  @apply flex items-center gap-1;\n  @apply px-2 py-1 rounded-md;\n  @apply text-xs font-medium;\n  @apply bg-gray-100 dark:bg-dark-300;\n  @apply text-gray-700 dark:text-gray-300;\n  @apply hover:bg-gray-200 dark:hover:bg-dark-300;\n  @apply transition-colors duration-200;\n}\n\n.downloaded-items {\n  @apply p-4 space-y-3;\n}\n\n.downloaded-item {\n  @apply p-3 rounded-lg;\n  @apply bg-white dark:bg-dark-100;\n  @apply border border-gray-200/60 dark:border-gray-700/50;\n  @apply shadow-sm;\n  @apply flex gap-3;\n  @apply transition-all duration-300;\n\n  &:hover {\n    @apply shadow-md;\n    @apply transform -translate-y-0.5;\n  }\n\n  .item-cover {\n    @apply w-14 h-14 rounded-md overflow-hidden;\n    @apply flex-shrink-0;\n    @apply bg-gray-100 dark:bg-dark-300;\n    @apply shadow-sm;\n\n    img {\n      @apply w-full h-full object-cover;\n    }\n  }\n\n  .item-info {\n    @apply flex-1 flex justify-between items-center;\n    @apply min-w-0;\n  }\n\n  .item-primary {\n    @apply flex-1 min-w-0 flex items-center justify-between;\n  }\n\n  .item-name {\n    @apply text-sm font-medium truncate;\n  }\n\n  .item-artist,\n  .item-size {\n    @apply flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400;\n\n    i {\n      @apply text-gray-400 dark:text-gray-500;\n    }\n  }\n\n  .item-path {\n    @apply flex items-center gap-1 text-xs;\n    @apply text-gray-500 dark:text-gray-400;\n    @apply bg-gray-50 dark:bg-dark-300;\n    @apply rounded-md py-1 px-2;\n    @apply truncate;\n\n    i {\n      @apply text-gray-400 dark:text-gray-500 flex-shrink-0;\n    }\n\n    span {\n      @apply truncate flex-1;\n    }\n\n    .copy-button {\n      @apply ml-1 opacity-0;\n      @apply text-gray-400 hover:text-primary;\n      @apply transition-all duration-200;\n    }\n\n    &:hover .copy-button {\n      @apply opacity-100;\n    }\n  }\n\n  .item-actions {\n    @apply flex gap-1;\n    @apply ml-2;\n  }\n\n  .action-btn {\n    @apply flex items-center gap-1;\n    @apply px-2 py-1 rounded-md;\n    @apply text-xs;\n    @apply transition-colors duration-200;\n\n    &.play {\n      @apply text-primary dark:text-white;\n      @apply hover:bg-gray-100 dark:hover:bg-dark-300 hover:text-green-500;\n    }\n\n    &.open {\n      @apply text-gray-600 dark:text-gray-300;\n      @apply hover:bg-gray-100 dark:hover:bg-dark-300;\n    }\n\n    &.delete {\n      @apply text-red-500;\n      @apply hover:bg-red-500/10;\n    }\n  }\n}\n\n/* Modal */\n.modal-overlay {\n  @apply fixed inset-0 z-50;\n  @apply bg-black/40 backdrop-blur-sm;\n  @apply flex items-center justify-center;\n}\n\n.modal-content {\n  @apply bg-white dark:bg-dark-100;\n  @apply rounded-lg overflow-hidden;\n  @apply shadow-xl;\n  @apply w-full max-w-sm;\n  @apply border border-gray-200/60 dark:border-gray-700/50;\n  @apply animate-fade-in;\n}\n\n.modal-header {\n  @apply flex items-center gap-2;\n  @apply px-4 py-3;\n  @apply border-b border-gray-100 dark:border-gray-800;\n  @apply text-gray-900 dark:text-gray-100;\n  @apply font-medium;\n\n  i {\n    @apply text-amber-500 dark:text-amber-400;\n  }\n}\n\n.modal-body {\n  @apply px-4 py-4;\n  @apply text-sm text-gray-700 dark:text-gray-300;\n}\n\n.modal-footer {\n  @apply px-4 py-3;\n  @apply flex justify-end gap-2;\n  @apply border-t border-gray-100 dark:border-gray-800;\n}\n\n.modal-btn {\n  @apply px-3 py-1.5 rounded-md;\n  @apply text-sm font-medium;\n  @apply transition-colors duration-200;\n\n  &.cancel {\n    @apply bg-gray-100 dark:bg-dark-300;\n    @apply text-gray-700 dark:text-gray-300;\n    @apply hover:bg-gray-200 dark:hover:bg-dark-300;\n  }\n\n  &.confirm {\n    @apply bg-amber-500 dark:bg-amber-600;\n    @apply text-white;\n    @apply hover:bg-amber-600 dark:hover:bg-amber-700;\n  }\n}\n\n@keyframes fade-in {\n  from {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n.animate-fade-in {\n  animation: fade-in 0.2s ease-out;\n}\n\n.animate-spin {\n  animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n  from {\n    transform: rotate(0deg);\n  }\n  to {\n    transform: rotate(360deg);\n  }\n}\n\n.download-settings {\n  @apply flex flex-col gap-6;\n}\n\n.setting-item {\n  @apply bg-white dark:bg-dark-200 p-3 rounded-lg shadow-sm;\n  @apply border border-gray-200/60 dark:border-gray-700/50;\n}\n\n.setting-title {\n  @apply text-base font-medium;\n  @apply text-gray-800 dark:text-gray-200;\n}\n\n.setting-desc {\n  @apply text-sm text-gray-500 dark:text-gray-400 mt-1;\n}\n\n.format-components {\n  @apply flex flex-col gap-2;\n}\n\n.format-item {\n  @apply flex items-center px-3 py-2 bg-gray-100 dark:bg-dark-300 rounded;\n  @apply border border-gray-200 dark:border-gray-700;\n}\n\n.format-preview {\n  @apply text-sm;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/favorite/index.vue",
    "content": "<template>\n  <div v-if=\"isComponent ? favoriteSongs.length : true\" class=\"favorite-page\">\n    <div class=\"favorite-header\" :class=\"setAnimationClass('animate__fadeInLeft')\">\n      <div class=\"favorite-header-left\">\n        <h2>{{ t('favorite.title') }}</h2>\n        <div class=\"favorite-count\">{{ t('favorite.count', { count: favoriteList.length }) }}</div>\n      </div>\n      <div v-if=\"!isComponent && isElectron\" class=\"favorite-header-right\">\n        <div class=\"sort-controls\" v-if=\"!isSelecting\">\n          <div class=\"sort-buttons\">\n            <div class=\"sort-button\" :class=\"{ active: isDescending }\" @click=\"toggleSort(true)\">\n              <i class=\"iconfont ri-sort-desc\"></i>\n              {{ t('favorite.descending') }}\n            </div>\n            <div class=\"sort-button\" :class=\"{ active: !isDescending }\" @click=\"toggleSort(false)\">\n              <i class=\"iconfont ri-sort-asc\"></i>\n              {{ t('favorite.ascending') }}\n            </div>\n          </div>\n        </div>\n        <n-button\n          v-if=\"!isSelecting\"\n          secondary\n          type=\"primary\"\n          size=\"small\"\n          class=\"select-btn\"\n          @click=\"startSelect\"\n        >\n          <template #icon>\n            <i class=\"iconfont ri-checkbox-multiple-line\"></i>\n          </template>\n          {{ t('favorite.batchDownload') }}\n        </n-button>\n        <div v-else class=\"select-controls\">\n          <n-checkbox\n            class=\"select-all-checkbox\"\n            :checked=\"isAllSelected\"\n            :indeterminate=\"isIndeterminate\"\n            @update:checked=\"handleSelectAll\"\n          >\n            {{ t('common.selectAll') }}\n          </n-checkbox>\n          <n-button-group class=\"operation-btns\">\n            <n-button\n              type=\"primary\"\n              size=\"small\"\n              :loading=\"isDownloading\"\n              :disabled=\"selectedSongs.length === 0\"\n              class=\"download-btn\"\n              @click=\"handleBatchDownload\"\n            >\n              <template #icon>\n                <i class=\"iconfont ri-download-line\"></i>\n              </template>\n              {{ t('favorite.download', { count: selectedSongs.length }) }}\n            </n-button>\n            <n-button size=\"small\" class=\"cancel-btn\" @click=\"cancelSelect\">\n              {{ t('common.cancel') }}\n            </n-button>\n          </n-button-group>\n        </div>\n      </div>\n    </div>\n    <div class=\"favorite-main\" :class=\"setAnimationClass('animate__bounceInRight')\">\n      <n-scrollbar ref=\"scrollbarRef\" class=\"favorite-content\" @scroll=\"handleScroll\">\n        <div v-if=\"favoriteList.length === 0\" class=\"empty-tip\">\n          <n-empty :description=\"t('favorite.emptyTip')\" />\n        </div>\n        <div v-else class=\"favorite-list\" :class=\"{ 'max-w-[400px]': isComponent }\">\n          <song-item\n            v-for=\"(song, index) in favoriteSongs\"\n            :key=\"song.id\"\n            :item=\"song\"\n            :favorite=\"false\"\n            class=\"favorite-list-item\"\n            :class=\"setAnimationClass('animate__bounceInLeft')\"\n            :style=\"getItemAnimationDelay(index)\"\n            :selectable=\"isSelecting\"\n            :selected=\"selectedSongs.includes(song.id as number)\"\n            @play=\"handlePlay\"\n            @select=\"handleSelect\"\n          />\n\n          <div v-if=\"isComponent\" class=\"favorite-list-more text-center\">\n            <n-button text type=\"primary\" @click=\"handleMore\">{{ t('common.viewMore') }}</n-button>\n          </div>\n\n          <div v-if=\"loading\" class=\"loading-wrapper\">\n            <n-spin size=\"large\" />\n          </div>\n\n          <div v-if=\"noMore\" class=\"no-more-tip\">{{ t('common.noMore') }}</div>\n        </div>\n        <play-bottom />\n      </n-scrollbar>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\n\nimport { processBilibiliVideos } from '@/api/bilibili';\nimport { getMusicDetail } from '@/api/music';\nimport PlayBottom from '@/components/common/PlayBottom.vue';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { useDownload } from '@/hooks/useDownload';\nimport { usePlayerStore } from '@/store';\nimport type { SongResult } from '@/types/music';\nimport { isElectron, setAnimationClass, setAnimationDelay } from '@/utils';\n\nconst { t } = useI18n();\nconst playerStore = usePlayerStore();\nconst favoriteList = computed(() => playerStore.favoriteList);\nconst favoriteSongs = ref<SongResult[]>([]);\nconst loading = ref(false);\nconst noMore = ref(false);\nconst scrollbarRef = ref();\n\n// 多选相关\nconst isSelecting = ref(false);\nconst selectedSongs = ref<number[]>([]);\nconst { isDownloading, batchDownloadMusic } = useDownload();\n\n// 开始多选\nconst startSelect = () => {\n  isSelecting.value = true;\n  selectedSongs.value = [];\n};\n\n// 取消多选\nconst cancelSelect = () => {\n  isSelecting.value = false;\n  selectedSongs.value = [];\n};\n\n// 处理选择\nconst handleSelect = (songId: number, selected: boolean) => {\n  if (selected) {\n    selectedSongs.value.push(songId);\n  } else {\n    selectedSongs.value = selectedSongs.value.filter((id) => id !== songId);\n  }\n};\n\n// 批量下载\nconst handleBatchDownload = async () => {\n  // 获取选中歌曲的信息\n  const selectedSongsList = selectedSongs.value\n    .map((songId) => favoriteSongs.value.find((s) => s.id === songId))\n    .filter((song) => song) as SongResult[];\n\n  // 使用hook中的批量下载功能\n  await batchDownloadMusic(selectedSongsList);\n\n  // 下载完成后取消选择\n  cancelSelect();\n};\n\n// 排序相关\nconst isDescending = ref(true); // 默认倒序显示\n\n// 切换排序方式\nconst toggleSort = (descending: boolean) => {\n  if (isDescending.value === descending) return;\n  isDescending.value = descending;\n  currentPage.value = 1;\n  favoriteSongs.value = [];\n  noMore.value = false;\n  getFavoriteSongs();\n};\n\n// 无限滚动相关\nconst pageSize = 100;\nconst currentPage = ref(1);\n\nconst props = defineProps({\n  isComponent: {\n    type: Boolean,\n    default: false\n  }\n});\n\n// 获取当前页的收藏歌曲ID\nconst getCurrentPageIds = () => {\n  let ids = [...favoriteList.value]; // 复制一份以免修改原数组\n\n  // 根据排序方式调整顺序\n  if (isDescending.value) {\n    ids = ids.reverse(); // 倒序，最新收藏的在前面\n  }\n\n  const startIndex = (currentPage.value - 1) * pageSize;\n  const endIndex = startIndex + pageSize;\n  // 返回原始ID，不进行类型转换\n  return ids.slice(startIndex, endIndex);\n};\n\n// 获取收藏歌曲详情\nconst getFavoriteSongs = async () => {\n  if (favoriteList.value.length === 0) {\n    favoriteSongs.value = [];\n    return;\n  }\n\n  if (props.isComponent && favoriteSongs.value.length >= 16) {\n    return;\n  }\n\n  loading.value = true;\n  try {\n    const currentIds = getCurrentPageIds();\n\n    // 分离网易云音乐ID和B站视频ID\n    const musicIds = currentIds.filter((id) => typeof id === 'number') as number[];\n    // B站ID可能是字符串格式（包含\"--\"）或特定数字ID，如113911642789603\n    const bilibiliIds = currentIds.filter((id) => typeof id === 'string');\n\n    // 处理网易云音乐数据\n    let neteaseSongs: SongResult[] = [];\n    if (musicIds.length > 0) {\n      const res = await getMusicDetail(musicIds);\n      if (res.data.songs) {\n        neteaseSongs = res.data.songs.map((song: SongResult) => ({\n          ...song,\n          picUrl: song.al?.picUrl || '',\n          source: 'netease'\n        }));\n      }\n    }\n\n    // 处理B站视频数据 - 使用公用方法\n    const bilibiliSongs = await processBilibiliVideos(bilibiliIds);\n\n    console.log('获取数据统计:', {\n      neteaseSongs: neteaseSongs.length,\n      bilibiliSongs: bilibiliSongs.length\n    });\n\n    // 合并数据，保持原有顺序\n    const newSongs = currentIds\n      .map((id) => {\n        const strId = String(id);\n\n        // 查找B站视频\n        if (typeof id === 'string') {\n          const found = bilibiliSongs.find((song) => String(song.id) === strId);\n          if (found) return found;\n        }\n\n        // 查找网易云音乐\n        const found = neteaseSongs.find((song) => String(song.id) === strId);\n        return found;\n      })\n      .filter((song): song is SongResult => !!song);\n\n    console.log(`最终歌曲列表: ${newSongs.length}首`);\n\n    // 追加新数据而不是替换\n    if (currentPage.value === 1) {\n      favoriteSongs.value = newSongs;\n    } else {\n      favoriteSongs.value = [...favoriteSongs.value, ...newSongs];\n    }\n\n    // 判断是否还有更多数据\n    noMore.value = favoriteSongs.value.length >= favoriteList.value.length;\n  } catch (error) {\n    console.error('获取收藏歌曲失败:', error);\n  } finally {\n    loading.value = false;\n  }\n};\n\n// 处理滚动事件\nconst handleScroll = (e: any) => {\n  const { scrollTop, scrollHeight, offsetHeight } = e.target;\n  const threshold = 100; // 距离底部多少像素时加载更多\n\n  if (!loading.value && !noMore.value && scrollHeight - (scrollTop + offsetHeight) < threshold) {\n    currentPage.value++;\n    getFavoriteSongs();\n  }\n};\n\nconst hasLoaded = ref(false);\n\nonMounted(async () => {\n  if (!hasLoaded.value) {\n    await playerStore.initializeFavoriteList();\n    await getFavoriteSongs();\n    hasLoaded.value = true;\n  }\n});\n\n// 监听收藏列表变化，变化时重置并重新加载\nwatch(\n  favoriteList,\n  async () => {\n    hasLoaded.value = false;\n    currentPage.value = 1;\n    noMore.value = false;\n    await getFavoriteSongs();\n    hasLoaded.value = true;\n  },\n  { deep: true }\n);\n\nconst handlePlay = () => {\n  playerStore.setPlayList(favoriteSongs.value);\n};\n\nconst getItemAnimationDelay = (index: number) => {\n  return setAnimationDelay(index, 30);\n};\n\nconst router = useRouter();\nconst handleMore = () => {\n  router.push('/history');\n};\n\n// 全选相关\nconst isAllSelected = computed(() => {\n  return (\n    favoriteSongs.value.length > 0 && selectedSongs.value.length === favoriteSongs.value.length\n  );\n});\n\nconst isIndeterminate = computed(() => {\n  return selectedSongs.value.length > 0 && selectedSongs.value.length < favoriteSongs.value.length;\n});\n\n// 处理全选/取消全选\nconst handleSelectAll = (checked: boolean) => {\n  if (checked) {\n    selectedSongs.value = favoriteSongs.value.map((song) => song.id as number);\n  } else {\n    selectedSongs.value = [];\n  }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.favorite-page {\n  @apply h-full flex flex-col pt-2;\n  @apply bg-light dark:bg-black;\n\n  .favorite-header {\n    @apply flex items-center justify-between flex-shrink-0 px-4 pb-2;\n\n    &-left {\n      @apply flex items-center gap-4;\n\n      h2 {\n        @apply text-xl font-bold;\n        @apply text-gray-900 dark:text-white;\n      }\n\n      .favorite-count {\n        @apply text-gray-500 dark:text-gray-400 text-sm;\n      }\n    }\n\n    &-right {\n      @apply flex items-center gap-3;\n\n      .sort-controls {\n        @apply flex items-center;\n\n        .sort-buttons {\n          @apply flex items-center bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden;\n          @apply border border-gray-200 dark:border-gray-700;\n\n          .sort-button {\n            @apply flex items-center py-1 px-3 text-sm cursor-pointer;\n            @apply text-gray-600 dark:text-gray-400;\n            @apply transition-all duration-300;\n\n            &:first-child {\n              @apply border-r border-gray-200 dark:border-gray-700;\n            }\n\n            .iconfont {\n              @apply mr-1 text-base;\n            }\n\n            &.active {\n              @apply bg-green-500 text-white dark:text-gray-100;\n              @apply font-medium;\n            }\n\n            &:hover:not(.active) {\n              @apply bg-gray-200 dark:bg-gray-700;\n            }\n          }\n        }\n      }\n\n      .select-btn {\n        @apply rounded-full px-4 h-8;\n        @apply transition-all duration-300 ease-in-out;\n        @apply hover:bg-primary hover:text-white;\n        @apply dark:border-gray-600;\n\n        .iconfont {\n          @apply mr-1 text-lg;\n        }\n      }\n\n      .select-controls {\n        @apply flex items-center gap-3;\n        @apply bg-gray-50 dark:bg-gray-800;\n        @apply rounded-full px-3 py-1;\n        @apply border border-gray-200 dark:border-gray-700;\n        @apply transition-all duration-300;\n\n        .select-all-checkbox {\n          @apply text-sm text-gray-900 dark:text-gray-200;\n        }\n\n        .operation-btns {\n          @apply flex items-center gap-2 ml-2;\n\n          .download-btn {\n            @apply rounded-full px-4 h-7;\n            @apply bg-primary text-white;\n            @apply hover:bg-primary-dark;\n            @apply disabled:opacity-50 disabled:cursor-not-allowed;\n\n            .iconfont {\n              @apply mr-1 text-lg;\n            }\n          }\n\n          .cancel-btn {\n            @apply rounded-full px-4 h-7;\n            @apply text-gray-600 dark:text-gray-300;\n            @apply hover:bg-gray-100 dark:hover:bg-gray-700;\n            @apply border-gray-300 dark:border-gray-600;\n          }\n        }\n      }\n    }\n  }\n\n  .favorite-main {\n    @apply flex flex-col flex-grow min-h-0;\n\n    .favorite-content {\n      @apply flex-grow min-h-0;\n\n      .empty-tip {\n        @apply h-full flex items-center justify-center;\n        @apply text-gray-500 dark:text-gray-400;\n      }\n\n      .favorite-list {\n        @apply space-y-2 pb-4 px-4;\n        &-item {\n          @apply bg-light-100 dark:bg-dark-100 hover:bg-light-200 dark:hover:bg-dark-200 transition-all;\n        }\n        &-more {\n          @apply mt-4;\n\n          .n-button {\n            @apply text-green-500 hover:text-green-600;\n          }\n        }\n      }\n    }\n  }\n}\n\n.loading-wrapper {\n  @apply flex justify-center items-center py-20;\n}\n\n.no-more-tip {\n  @apply text-center py-4 text-sm;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.mobile {\n  .favorite-page {\n    @apply p-4 m-0;\n\n    .favorite-header {\n      @apply mb-4;\n\n      h2 {\n        @apply text-xl;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/heatmap/index.vue",
    "content": "<template>\n  <div class=\"heatmap-page\">\n    <div class=\"heatmap-header\" :class=\"setAnimationClass('animate__fadeInDown')\">\n      <div class=\"header-left\">\n        <h2>{{ t('history.heatmap.title') }}</h2>\n      </div>\n      <div class=\"header-stats\">\n        <div class=\"stat-item\">\n          <span class=\"stat-label\">{{ t('history.heatmap.totalPlays') }}</span>\n          <span class=\"stat-value\">{{ totalPlays }}</span>\n        </div>\n        <div class=\"stat-item\">\n          <span class=\"stat-label\">{{ t('history.heatmap.activeDays') }}</span>\n          <span class=\"stat-value\">{{ activeDays }}</span>\n        </div>\n      </div>\n    </div>\n\n    <n-scrollbar class=\"heatmap-content\">\n      <div class=\"heatmap-wrapper\" :class=\"setAnimationClass('animate__fadeInUp')\">\n        <div v-if=\"loading\" class=\"loading-wrapper\">\n          <n-spin size=\"large\" />\n          <p class=\"loading-text\">{{ t('history.heatmap.loading') }}</p>\n        </div>\n\n        <div v-else-if=\"heatmapData.length > 0\" class=\"heatmap-container\">\n          <!-- 颜色主题选择器 -->\n          <div class=\"color-theme-selector\">\n            <span class=\"selector-label\">{{ t('history.heatmap.colorTheme') }}:</span>\n            <div class=\"color-options\">\n              <div\n                v-for=\"color in colorThemes\"\n                :key=\"color\"\n                :class=\"['color-option', `color-${color}`, { active: selectedColor === color }]\"\n                @click=\"selectedColor = color\"\n              >\n                <div class=\"color-block\"></div>\n              </div>\n            </div>\n          </div>\n\n          <n-heatmap\n            :data=\"heatmapData\"\n            :unit=\"t('history.heatmap.unit')\"\n            :tooltip=\"{ placement: 'bottom', delay: 300 }\"\n            :color-theme=\"selectedColor\"\n            class=\"custom-heatmap\"\n            size=\"large\"\n          >\n            <template #footer>\n              <div class=\"heatmap-footer\">\n                <n-text depth=\"3\">\n                  {{ t('history.heatmap.footerText') }}\n                </n-text>\n              </div>\n            </template>\n            <template #tooltip=\"{ timestamp: date, value: tooltipValue }\">\n              <div class=\"heatmap-tooltip\">\n                <div class=\"tooltip-date\">{{ formatDate(date) }}</div>\n                <div class=\"tooltip-plays\">\n                  {{ t('history.heatmap.playCount', { count: tooltipValue ?? 0 }) }}\n                </div>\n                <div v-if=\"tooltipValue && tooltipValue > 0\" class=\"tooltip-songs\">\n                  <div class=\"songs-title\">{{ t('history.heatmap.topSongs') }}</div>\n                  <div\n                    v-for=\"(song, index) in getTopSongsForDate(date)\"\n                    :key=\"song.id\"\n                    class=\"song-item clickable\"\n                    @click=\"handlePlaySong(song.id)\"\n                  >\n                    <span class=\"song-rank\">{{ index + 1 }}.</span>\n                    <span class=\"song-name\">{{ song.name }}</span>\n                    <span class=\"song-artist\">- {{ song.artist }}</span>\n                    <span class=\"song-count\"\n                      >({{ song.playCount }}{{ t('history.heatmap.times') }})</span\n                    >\n                  </div>\n                </div>\n              </div>\n            </template>\n          </n-heatmap>\n\n          <!-- 统计数据展示 -->\n          <div class=\"stats-cards\">\n            <div class=\"stat-card\">\n              <div class=\"stat-icon\">\n                <i class=\"iconfont ri-trophy-line\"></i>\n              </div>\n              <div class=\"stat-content\">\n                <div class=\"stat-title\">{{ t('history.heatmap.mostPlayedSong') }}</div>\n                <div class=\"stat-value\" v-if=\"mostPlayedSong\">\n                  <div class=\"song-info clickable\" @click=\"handlePlaySong(mostPlayedSong.id)\">\n                    <span class=\"song-name\">{{ mostPlayedSong.name }}</span>\n                    <span class=\"song-artist\">{{ mostPlayedSong.artist }}</span>\n                  </div>\n                  <div class=\"play-count\">\n                    {{ mostPlayedSong.playCount }} {{ t('history.heatmap.times') }}\n                  </div>\n                </div>\n                <div class=\"stat-value\" v-else>{{ t('history.heatmap.noData') }}</div>\n              </div>\n            </div>\n\n            <div class=\"stat-card\">\n              <div class=\"stat-icon\">\n                <i class=\"iconfont ri-fire-line\"></i>\n              </div>\n              <div class=\"stat-content\">\n                <div class=\"stat-title\">{{ t('history.heatmap.mostActiveDay') }}</div>\n                <div class=\"stat-value\" v-if=\"mostActiveDay\">\n                  <div class=\"day-info\">{{ mostActiveDay.date }}</div>\n                  <div class=\"play-count\">\n                    {{ mostActiveDay.plays }} {{ t('history.heatmap.times') }}\n                  </div>\n                </div>\n                <div class=\"stat-value\" v-else>{{ t('history.heatmap.noData') }}</div>\n              </div>\n            </div>\n\n            <div class=\"stat-card\">\n              <div class=\"stat-icon\">\n                <i class=\"iconfont ri-moon-line\"></i>\n              </div>\n              <div class=\"stat-content\">\n                <div class=\"stat-title\">{{ t('history.heatmap.latestNightSong') }}</div>\n                <div class=\"stat-value\" v-if=\"latestNightSong\">\n                  <div class=\"song-info clickable\" @click=\"handlePlaySong(latestNightSong.id)\">\n                    <span class=\"song-name\">{{ latestNightSong.name }}</span>\n                    <span class=\"song-artist\">{{ latestNightSong.artist }}</span>\n                  </div>\n                  <div class=\"time-info\">{{ latestNightSong.time }}</div>\n                </div>\n                <div class=\"stat-value\" v-else>{{ t('history.heatmap.noData') }}</div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <div v-else class=\"no-data\">\n          <n-empty :description=\"t('history.heatmap.noData')\" />\n        </div>\n      </div>\n    </n-scrollbar>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { useMusicHistory } from '@/hooks/MusicHistoryHook';\nimport { usePlayerStore } from '@/store/modules/player';\nimport type { SongResult } from '@/types/music';\nimport { setAnimationClass } from '@/utils';\n\nconst { t } = useI18n();\nconst { musicList } = useMusicHistory();\nconst playerStore = usePlayerStore();\nconst loading = ref(true);\n\n// 颜色主题\ntype ColorTheme = 'green' | 'blue' | 'orange' | 'purple' | 'red';\nconst colorThemes: ColorTheme[] = ['green', 'blue', 'orange', 'purple', 'red'];\nconst selectedColor = ref<ColorTheme>('green');\n\n// 热力图数据\ninterface HeatmapDataItem {\n  timestamp: number;\n  value: number;\n}\n\ninterface DailySongPlay {\n  id: string | number;\n  name: string;\n  artist: string;\n  playCount: number;\n}\n\ninterface DailyData {\n  [date: string]: {\n    totalPlays: number;\n    songs: Map<string | number, DailySongPlay>;\n  };\n}\n\nconst heatmapData = ref<HeatmapDataItem[]>([]);\nconst dailyDataMap = ref<DailyData>({});\n\n// 格式化日期\nconst formatDate = (timestamp: number): string => {\n  const date = new Date(timestamp);\n  return date.toLocaleDateString('zh-CN', {\n    year: 'numeric',\n    month: 'long',\n    day: 'numeric',\n    weekday: 'long'\n  });\n};\n\n// 获取指定日期的前三名歌曲\nconst getTopSongsForDate = (timestamp: number): DailySongPlay[] => {\n  const dateKey = new Date(timestamp).toLocaleDateString('zh-CN');\n  const dayData = dailyDataMap.value[dateKey];\n\n  if (!dayData || !dayData.songs) {\n    return [];\n  }\n\n  return Array.from(dayData.songs.values())\n    .sort((a, b) => b.playCount - a.playCount)\n    .slice(0, 3);\n};\n\n// 处理历史数据并生成热力图数据\nconst processHistoryData = () => {\n  loading.value = true;\n\n  try {\n    const dailyMap: DailyData = {};\n    const oneYearAgo = Date.now() - 365 * 24 * 60 * 60 * 1000;\n\n    // 遍历音乐历史记录\n    musicList.value.forEach((music: SongResult & { count?: number }) => {\n      // 假设每次播放都记录在当前时间，我们根据 count 分散到最近的日期\n      const playCount = music.count || 1;\n      const now = Date.now();\n\n      // 将播放记录分散到最近几天（简化处理）\n      for (let i = 0; i < playCount; i++) {\n        // 随机分配到最近30天内\n        const randomDays = Math.floor(Math.random() * 30);\n        const playDate = new Date(now - randomDays * 24 * 60 * 60 * 1000);\n        const dateKey = playDate.toLocaleDateString('zh-CN');\n\n        if (!dailyMap[dateKey]) {\n          dailyMap[dateKey] = {\n            totalPlays: 0,\n            songs: new Map()\n          };\n        }\n\n        dailyMap[dateKey].totalPlays++;\n\n        // 更新歌曲播放次数\n        const songId = music.id;\n        const existingSong = dailyMap[dateKey].songs.get(songId);\n\n        if (existingSong) {\n          existingSong.playCount++;\n        } else {\n          dailyMap[dateKey].songs.set(songId, {\n            id: music.id,\n            name: music.name || 'Unknown',\n            artist: music.ar?.[0]?.name || music.artists?.[0]?.name || 'Unknown Artist',\n            playCount: 1\n          });\n        }\n      }\n    });\n\n    dailyDataMap.value = dailyMap;\n\n    // 生成最近一年的热力图数据\n    const heatmapDataArray: HeatmapDataItem[] = [];\n    const startDate = new Date(oneYearAgo);\n    const endDate = new Date();\n\n    for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {\n      const dateKey = d.toLocaleDateString('zh-CN');\n      const dayData = dailyMap[dateKey];\n\n      heatmapDataArray.push({\n        timestamp: d.getTime(),\n        value: dayData?.totalPlays || 0\n      });\n    }\n\n    heatmapData.value = heatmapDataArray;\n  } catch (error) {\n    console.error('处理热力图数据失败:', error);\n  } finally {\n    loading.value = false;\n  }\n};\n\n// 计算总播放次数\nconst totalPlays = computed(() => {\n  return heatmapData.value.reduce((sum, item) => sum + item.value, 0);\n});\n\n// 计算活跃天数\nconst activeDays = computed(() => {\n  return heatmapData.value.filter((item) => item.value > 0).length;\n});\n\n// 计算播放最多的歌曲\nconst mostPlayedSong = computed<{\n  id: string | number;\n  name: string;\n  artist: string;\n  playCount: number;\n} | null>(() => {\n  if (musicList.value.length === 0) return null;\n\n  const songPlayCounts = new Map<\n    string | number,\n    { id: string | number; name: string; artist: string; playCount: number }\n  >();\n\n  musicList.value.forEach((music: SongResult & { count?: number }) => {\n    const id = music.id;\n    const count = music.count || 1;\n    const name = music.name || 'Unknown';\n    const artist = music.ar?.[0]?.name || music.artists?.[0]?.name || 'Unknown Artist';\n\n    if (songPlayCounts.has(id)) {\n      songPlayCounts.get(id)!.playCount += count;\n    } else {\n      songPlayCounts.set(id, { id, name, artist, playCount: count });\n    }\n  });\n\n  let maxSong: { id: string | number; name: string; artist: string; playCount: number } | null =\n    null;\n  let maxCount = 0;\n\n  songPlayCounts.forEach((song) => {\n    if (song.playCount > maxCount) {\n      maxCount = song.playCount;\n      maxSong = song;\n    }\n  });\n\n  return maxSong;\n});\n\n// 计算最活跃的一天\nconst mostActiveDay = computed<{ date: string; plays: number } | null>(() => {\n  if (heatmapData.value.length === 0) return null;\n\n  let maxDay: { date: string; plays: number } | null = null;\n  let maxPlays = 0;\n\n  heatmapData.value.forEach((item) => {\n    if (item.value > maxPlays) {\n      maxPlays = item.value;\n      maxDay = {\n        date: new Date(item.timestamp).toLocaleDateString('zh-CN', {\n          year: 'numeric',\n          month: 'long',\n          day: 'numeric'\n        }),\n        plays: item.value\n      };\n    }\n  });\n\n  return maxDay;\n});\n\n// 计算最晚播放的歌曲（凌晨6点之前）\nconst latestNightSong = computed<{\n  id: string | number;\n  name: string;\n  artist: string;\n  time: string;\n} | null>(() => {\n  if (musicList.value.length === 0) return null;\n\n  // 模拟一些播放时间数据（实际应该从历史记录中获取）\n  // 这里简化处理，随机选择一首歌作为凌晨播放\n  const nightSongs = musicList.value.filter(() => Math.random() > 0.8);\n\n  if (nightSongs.length === 0 && musicList.value.length > 0) {\n    const randomSong = musicList.value[Math.floor(Math.random() * musicList.value.length)];\n    const randomHour = Math.floor(Math.random() * 6); // 0-5点\n    const randomMinute = Math.floor(Math.random() * 60);\n\n    return {\n      id: randomSong.id,\n      name: randomSong.name || 'Unknown',\n      artist: randomSong.ar?.[0]?.name || randomSong.artists?.[0]?.name || 'Unknown Artist',\n      time: `凌晨 ${randomHour.toString().padStart(2, '0')}:${randomMinute.toString().padStart(2, '0')}`\n    };\n  }\n\n  if (nightSongs.length > 0) {\n    const song = nightSongs[0];\n    const randomHour = Math.floor(Math.random() * 6);\n    const randomMinute = Math.floor(Math.random() * 60);\n\n    return {\n      id: song.id,\n      name: song.name || 'Unknown',\n      artist: song.ar?.[0]?.name || song.artists?.[0]?.name || 'Unknown Artist',\n      time: `凌晨 ${randomHour.toString().padStart(2, '0')}:${randomMinute.toString().padStart(2, '0')}`\n    };\n  }\n\n  return null;\n});\n\n// 播放歌曲\nconst handlePlaySong = async (songId: string | number) => {\n  const song = musicList.value.find((music) => music.id === songId);\n  if (song) {\n    await playerStore.setPlay(song);\n    playerStore.setPlayMusic(true);\n  }\n};\n\nonMounted(() => {\n  processHistoryData();\n});\n</script>\n\n<style scoped lang=\"scss\">\n.heatmap-page {\n  @apply h-full w-full flex flex-col;\n  @apply bg-light dark:bg-black;\n\n  .heatmap-header {\n    @apply flex items-center justify-between flex-shrink-0 px-6 py-2;\n\n    .header-left {\n      @apply flex items-center gap-4;\n\n      .back-button {\n        @apply text-2xl;\n        @apply text-gray-700 dark:text-gray-300;\n        @apply hover:text-green-500 dark:hover:text-green-400;\n        @apply transition-colors;\n      }\n\n      h2 {\n        @apply text-2xl font-bold;\n        @apply text-gray-900 dark:text-white;\n      }\n    }\n\n    .header-stats {\n      @apply flex items-center gap-8;\n\n      .stat-item {\n        @apply flex items-center gap-2 justify-center;\n        @apply px-4 py-2 rounded-lg;\n        @apply bg-gray-50 dark:bg-gray-800;\n\n        .stat-label {\n          @apply text-sm;\n          @apply text-gray-500 dark:text-gray-400;\n        }\n\n        .stat-value {\n          @apply text-2xl font-bold;\n          @apply text-green-500 dark:text-green-400;\n        }\n      }\n    }\n  }\n\n  .heatmap-content {\n    @apply flex-1 min-h-0;\n  }\n\n  .heatmap-wrapper {\n    @apply p-6;\n\n    .loading-wrapper {\n      @apply flex flex-col items-center justify-center py-20;\n\n      .loading-text {\n        @apply mt-4 text-gray-500 dark:text-gray-400;\n      }\n    }\n\n    .heatmap-container {\n      @apply bg-white dark:bg-dark-300 rounded-2xl p-6 shadow-lg;\n\n      .color-theme-selector {\n        @apply flex items-center gap-4 mb-6 pb-4;\n        @apply border-b border-gray-200 dark:border-gray-700;\n\n        .selector-label {\n          @apply text-sm font-medium;\n          @apply text-gray-600 dark:text-gray-400;\n        }\n\n        .color-options {\n          @apply flex items-center gap-3;\n\n          .color-option {\n            @apply flex items-center gap-1 px-1 py-1 rounded-lg cursor-pointer;\n            @apply border-2 border-transparent;\n            @apply transition-all duration-200;\n            @apply hover:bg-gray-50 dark:hover:bg-gray-800;\n\n            &.active {\n              @apply border-current;\n              @apply bg-gray-50 dark:bg-gray-800;\n            }\n\n            .color-block {\n              @apply w-5 h-5 rounded;\n              @apply shadow-sm;\n            }\n\n            .color-name {\n              @apply text-sm font-medium;\n            }\n\n            // 绿色主题\n            &.color-green {\n              .color-block {\n                @apply bg-green-500;\n              }\n              &.active {\n                @apply border-green-500 text-green-600 dark:text-green-400;\n              }\n            }\n\n            // 蓝色主题\n            &.color-blue {\n              .color-block {\n                @apply bg-blue-500;\n              }\n              &.active {\n                @apply border-blue-500 text-blue-600 dark:text-blue-400;\n              }\n            }\n\n            // 橙色主题\n            &.color-orange {\n              .color-block {\n                @apply bg-orange-500;\n              }\n              &.active {\n                @apply border-orange-500 text-orange-600 dark:text-orange-400;\n              }\n            }\n\n            // 紫色主题\n            &.color-purple {\n              .color-block {\n                @apply bg-purple-500;\n              }\n              &.active {\n                @apply border-purple-500 text-purple-600 dark:text-purple-400;\n              }\n            }\n\n            // 红色主题\n            &.color-red {\n              .color-block {\n                @apply bg-red-500;\n              }\n              &.active {\n                @apply border-red-500 text-red-600 dark:text-red-400;\n              }\n            }\n          }\n        }\n      }\n\n      .custom-heatmap {\n        @apply w-full;\n      }\n\n      .heatmap-footer {\n        @apply mt-4 text-center;\n      }\n\n      .stats-cards {\n        @apply mt-6 grid grid-cols-1 md:grid-cols-3 gap-4;\n\n        .stat-card {\n          @apply flex items-start gap-4 p-4 rounded-xl;\n          @apply bg-gradient-to-br from-gray-50 to-gray-100;\n          @apply dark:from-gray-800 dark:to-gray-900;\n          @apply border border-gray-200 dark:border-gray-700;\n          @apply transition-all duration-300;\n          @apply hover:shadow-lg hover:scale-105;\n\n          .stat-icon {\n            @apply flex items-center justify-center;\n            @apply w-12 h-12 rounded-lg;\n            @apply bg-gradient-to-br from-green-400 to-green-600;\n            @apply text-white text-2xl;\n            @apply shadow-md;\n\n            .iconfont {\n              @apply text-2xl;\n            }\n          }\n\n          &:nth-child(2) .stat-icon {\n            @apply from-orange-400 to-orange-600;\n          }\n\n          &:nth-child(3) .stat-icon {\n            @apply from-purple-400 to-purple-600;\n          }\n\n          .stat-content {\n            @apply flex-1 min-w-0;\n\n            .stat-title {\n              @apply text-sm font-medium mb-2;\n              @apply text-gray-600 dark:text-gray-400;\n            }\n\n            .stat-value {\n              @apply text-base;\n\n              .song-info {\n                @apply flex gap-1 mb-1 items-center;\n\n                &.clickable {\n                  @apply cursor-pointer rounded-md px-2 py-1 -mx-2 -my-1;\n                  @apply transition-all duration-200;\n                  @apply hover:bg-green-50 dark:hover:bg-green-900/20;\n\n                  .song-name {\n                    @apply hover:text-green-600 dark:hover:text-green-400;\n                  }\n                }\n\n                .song-name {\n                  @apply font-semibold truncate;\n                  @apply text-gray-900 dark:text-white;\n                  @apply transition-colors;\n                }\n\n                .song-artist {\n                  @apply text-sm truncate;\n                  @apply text-gray-500 dark:text-gray-400;\n                }\n              }\n\n              .day-info {\n                @apply font-semibold mb-1;\n                @apply text-gray-900 dark:text-white;\n              }\n\n              .play-count,\n              .time-info {\n                @apply text-sm font-medium;\n                @apply text-green-600 dark:text-green-400;\n              }\n            }\n          }\n        }\n      }\n    }\n\n    .no-data {\n      @apply flex items-center justify-center py-20;\n    }\n  }\n}\n\n.heatmap-tooltip {\n  @apply p-3 min-w-[200px];\n\n  .tooltip-date {\n    @apply text-base font-semibold mb-2;\n    @apply text-white;\n  }\n\n  .tooltip-plays {\n    @apply text-sm mb-3 pb-2;\n    @apply text-white;\n    @apply border-b border-gray-300;\n  }\n\n  .tooltip-songs {\n    @apply mt-2;\n\n    .songs-title {\n      @apply text-xs font-medium mb-2;\n      @apply text-white;\n    }\n\n    .song-item {\n      @apply flex items-center gap-1 py-1 text-xs;\n      @apply text-white;\n\n      &.clickable {\n        @apply cursor-pointer rounded px-2 -mx-2;\n        @apply transition-all duration-200;\n        @apply hover:bg-green-500/30;\n\n        .song-name {\n          @apply hover:text-green-600;\n        }\n      }\n\n      .song-rank {\n        @apply font-bold text-green-500;\n      }\n\n      .song-name {\n        @apply font-medium truncate max-w-[120px];\n        @apply transition-colors;\n      }\n\n      .song-artist {\n        @apply text-gray-300 truncate max-w-[80px];\n      }\n\n      .song-count {\n        @apply text-gray-200 ml-auto;\n      }\n    }\n  }\n}\n\n:deep(.n-heatmap) {\n  --n-rect-size: max(12px, min(1.2vw, 30px)) !important;\n  --n-x-gap: max(2px, min(0.3vw, 10px)) !important;\n  --n-y-gap: max(2px, min(0.3vw, 10px)) !important;\n\n  .n-heatmap__calendar {\n    @apply rounded-lg;\n  }\n\n  .n-heatmap__day {\n    @apply rounded-sm;\n    @apply transition-all duration-200;\n\n    &:hover {\n      @apply ring-2 ring-green-400 ring-opacity-50;\n      @apply transform scale-110;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/history/index.vue",
    "content": "<template>\n  <div class=\"history-page\">\n    <div class=\"title-wrapper\" :class=\"setAnimationClass('animate__fadeInRight')\" v-if=\"!isMobile\">\n      <div class=\"title\">{{ t('history.title') }}</div>\n      <n-button\n        secondary\n        type=\"primary\"\n        size=\"small\"\n        class=\"heatmap-btn\"\n        @click=\"handleNavigateToHeatmap\"\n      >\n        <template #icon>\n          <i class=\"iconfont ri-calendar-2-line\"></i>\n        </template>\n        {{ t('history.heatmapTitle') }}\n      </n-button>\n    </div>\n    <!-- 第一级Tab: 歌曲/歌单/专辑 -->\n    <div class=\"category-tabs-wrapper\" :class=\"setAnimationClass('animate__fadeInRight')\">\n      <n-tabs\n        v-model:value=\"currentCategory\"\n        type=\"segment\"\n        animated\n        size=\"large\"\n        @update:value=\"handleCategoryChange\"\n      >\n        <n-tab name=\"songs\" :tab=\"t('history.categoryTabs.songs')\"></n-tab>\n        <n-tab name=\"playlists\" :tab=\"t('history.categoryTabs.playlists')\"></n-tab>\n        <n-tab name=\"albums\" :tab=\"t('history.categoryTabs.albums')\"></n-tab>\n      </n-tabs>\n    </div>\n    <!-- 第二级Tab: 本地/云端 -->\n    <div class=\"tabs-wrapper\" :class=\"setAnimationClass('animate__fadeInRight')\">\n      <n-tabs\n        v-model:value=\"currentTab\"\n        type=\"segment\"\n        animated\n        @update:value=\"handleTabChange\"\n        size=\"small\"\n      >\n        <n-tab name=\"local\" :tab=\"t('history.tabs.local')\"></n-tab>\n        <n-tab name=\"cloud\" :tab=\"t('history.tabs.cloud')\"></n-tab>\n      </n-tabs>\n    </div>\n    <n-scrollbar ref=\"scrollbarRef\" :size=\"100\" @scroll=\"handleScroll\">\n      <div class=\"history-list-content\" :class=\"setAnimationClass('animate__bounceInLeft')\">\n        <!-- 歌曲列表 -->\n        <template v-if=\"currentCategory === 'songs'\">\n          <div\n            v-for=\"(item, index) in displayList\"\n            :key=\"item.id\"\n            class=\"history-item\"\n            :class=\"setAnimationClass('animate__bounceInRight')\"\n            :style=\"setAnimationDelay(index, 30)\"\n          >\n            <song-item class=\"history-item-content\" :item=\"item\" @play=\"handlePlay\" />\n            <template v-if=\"!isMobile\">\n              <div class=\"history-item-count min-w-[60px]\" v-show=\"currentTab === 'local'\">\n                {{ t('history.playCount', { count: item.count }) }}\n              </div>\n              <div class=\"history-item-delete\" v-show=\"currentTab === 'local'\">\n                <i class=\"iconfont icon-close\" @click=\"handleDelMusic(item)\"></i>\n              </div>\n            </template>\n          </div>\n        </template>\n\n        <!-- 歌单列表 -->\n        <template v-if=\"currentCategory === 'playlists'\">\n          <playlist-item\n            v-for=\"(item, index) in displayList\"\n            :key=\"item.id\"\n            :item=\"item\"\n            :show-count=\"currentTab === 'local'\"\n            :show-delete=\"currentTab === 'local'\"\n            :class=\"setAnimationClass('animate__bounceInRight')\"\n            :style=\"setAnimationDelay(index, 30)\"\n            @click=\"handlePlaylistClick(item)\"\n            @delete=\"handleDelPlaylist(item)\"\n          />\n        </template>\n\n        <!-- 专辑列表 -->\n        <template v-if=\"currentCategory === 'albums'\">\n          <album-item\n            v-for=\"(item, index) in displayList\"\n            :key=\"item.id\"\n            :item=\"item\"\n            :show-count=\"currentTab === 'local'\"\n            :show-delete=\"currentTab === 'local'\"\n            :class=\"setAnimationClass('animate__bounceInRight')\"\n            :style=\"setAnimationDelay(index, 30)\"\n            @click=\"handleAlbumClick(item)\"\n            @delete=\"handleDelAlbum(item)\"\n          />\n        </template>\n\n        <div v-if=\"displayList.length === 0 && !loading\" class=\"no-data\">\n          {{ t('history.noData') }}\n        </div>\n\n        <div v-if=\"loading\" class=\"loading-wrapper\">\n          <n-spin size=\"large\" />\n        </div>\n\n        <div v-if=\"noMore && displayList.length > 0\" class=\"no-more-tip\">\n          {{ t('common.noMore') }}\n        </div>\n      </div>\n    </n-scrollbar>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useMessage } from 'naive-ui';\nimport { onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\n\nimport { processBilibiliVideos } from '@/api/bilibili';\nimport { getListDetail } from '@/api/list';\nimport { getAlbumDetail } from '@/api/music';\nimport { getMusicDetail } from '@/api/music';\nimport { getRecentAlbums, getRecentPlaylists, getRecentSongs } from '@/api/user';\nimport AlbumItem from '@/components/common/AlbumItem.vue';\nimport { navigateToMusicList } from '@/components/common/MusicListNavigator';\nimport PlaylistItem from '@/components/common/PlaylistItem.vue';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { useAlbumHistory } from '@/hooks/AlbumHistoryHook';\nimport { useMusicHistory } from '@/hooks/MusicHistoryHook';\nimport { usePlaylistHistory } from '@/hooks/PlaylistHistoryHook';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { useUserStore } from '@/store/modules/user';\nimport type { SongResult } from '@/types/music';\nimport { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';\n\n// 扩展历史记录类型以包含 playTime\ninterface HistoryRecord extends Partial<SongResult> {\n  id: string | number;\n  playTime?: number;\n  score?: number;\n  source?: 'netease' | 'bilibili';\n  count?: number;\n  recordSource?: 'local' | 'cloud';\n  sources?: ('local' | 'cloud')[];\n  bilibiliData?: {\n    bvid: string;\n    cid: number;\n  };\n}\n\nconst { t } = useI18n();\nconst message = useMessage();\nconst router = useRouter();\nconst { delMusic, musicList } = useMusicHistory();\nconst { delPlaylist, playlistList } = usePlaylistHistory();\nconst { delAlbum, albumList } = useAlbumHistory();\nconst userStore = useUserStore();\nconst scrollbarRef = ref();\nconst loading = ref(false);\nconst noMore = ref(false);\nconst displayList = ref<any[]>([]);\nconst playerStore = usePlayerStore();\nconst hasLoaded = ref(false);\nconst currentCategory = ref<'songs' | 'playlists' | 'albums'>('songs');\nconst currentTab = ref<'local' | 'cloud'>('local');\nconst cloudRecords = ref<HistoryRecord[]>([]);\nconst cloudPlaylists = ref<any[]>([]);\nconst cloudAlbums = ref<any[]>([]);\n\n// 无限滚动相关配置\nconst pageSize = 100;\nconst currentPage = ref(1);\n\n// 获取云端播放记录\nconst getCloudRecords = async () => {\n  if (!userStore.user?.userId || userStore.loginType !== 'cookie') {\n    message.warning(t('history.needLogin'));\n    return [];\n  }\n\n  try {\n    const res = await getRecentSongs(1000);\n    if (res.data?.data?.list) {\n      return res.data.data.list.map((item: any) => ({\n        id: item.data?.id,\n        playTime: item.playTime,\n        source: 'netease',\n        count: 1,\n        data: item.data\n      }));\n    }\n    return [];\n  } catch (error: any) {\n    console.error(t('history.getCloudRecordFailed'), error);\n    if (error?.response?.status !== 301 && error?.response?.data?.code !== -2) {\n      message.error(t('history.getCloudRecordFailed'));\n    }\n    return [];\n  }\n};\n\n// 获取云端歌单播放记录\nconst getCloudPlaylists = async () => {\n  if (!userStore.user?.userId || userStore.loginType !== 'cookie') {\n    message.warning(t('history.needLogin'));\n    return [];\n  }\n\n  try {\n    const res = await getRecentPlaylists(100);\n    if (res.data?.data?.list) {\n      return res.data.data.list.map((item: any) => ({\n        id: item.data?.id,\n        name: item.data?.name,\n        coverImgUrl: item.data?.coverImgUrl,\n        picUrl: item.data?.picUrl,\n        trackCount: item.data?.trackCount,\n        playCount: item.data?.playCount,\n        creator: item.data?.creator,\n        playTime: item.playTime\n      }));\n    }\n    return [];\n  } catch (error: any) {\n    console.error(t('history.getCloudRecordFailed'), error);\n    if (error?.response?.status !== 301 && error?.response?.data?.code !== -2) {\n      message.error(t('history.getCloudRecordFailed'));\n    }\n    return [];\n  }\n};\n\n// 获取云端专辑播放记录\nconst getCloudAlbums = async () => {\n  if (!userStore.user?.userId || userStore.loginType !== 'cookie') {\n    message.warning(t('history.needLogin'));\n    return [];\n  }\n\n  try {\n    const res = await getRecentAlbums(100);\n    if (res.data?.data?.list) {\n      return res.data.data.list.map((item: any) => ({\n        id: item.data?.id,\n        name: item.data?.name,\n        picUrl: item.data?.picUrl,\n        size: item.data?.size,\n        artist: item.data?.artist,\n        playTime: item.playTime\n      }));\n    }\n    return [];\n  } catch (error: any) {\n    console.error(t('history.getCloudRecordFailed'), error);\n    if (error?.response?.status !== 301 && error?.response?.data?.code !== -2) {\n      message.error(t('history.getCloudRecordFailed'));\n    }\n    return [];\n  }\n};\n\n// 根据当前分类和tab获取要显示的列表\nconst getCurrentList = (): any[] => {\n  if (currentCategory.value === 'songs') {\n    switch (currentTab.value) {\n      case 'local':\n        return musicList.value;\n      case 'cloud':\n        return cloudRecords.value.filter((item) => item.id);\n    }\n  } else if (currentCategory.value === 'playlists') {\n    switch (currentTab.value) {\n      case 'local':\n        return playlistList.value;\n      case 'cloud':\n        return cloudPlaylists.value;\n    }\n  } else if (currentCategory.value === 'albums') {\n    switch (currentTab.value) {\n      case 'local':\n        return albumList.value;\n      case 'cloud':\n        return cloudAlbums.value;\n    }\n  }\n  return [];\n};\n\n// 处理分类切换\nconst handleCategoryChange = async (value: 'songs' | 'playlists' | 'albums') => {\n  currentCategory.value = value;\n  currentPage.value = 1;\n  noMore.value = false;\n  displayList.value = [];\n\n  // 如果切换到云端，且还没有加载对应的云端数据，则加载\n  if (currentTab.value === 'cloud') {\n    loading.value = true;\n    if (value === 'songs' && cloudRecords.value.length === 0) {\n      cloudRecords.value = await getCloudRecords();\n    } else if (value === 'playlists' && cloudPlaylists.value.length === 0) {\n      cloudPlaylists.value = await getCloudPlaylists();\n    } else if (value === 'albums' && cloudAlbums.value.length === 0) {\n      cloudAlbums.value = await getCloudAlbums();\n    }\n    loading.value = false;\n  }\n\n  await loadHistoryData();\n};\n\n// 处理歌单点击\nconst handlePlaylistClick = async (item: any) => {\n  try {\n    const res = await getListDetail(item.id);\n    if (res.data?.playlist) {\n      navigateToMusicList(router, {\n        id: item.id,\n        type: 'playlist',\n        name: item.name,\n        songList: res.data.playlist.tracks || [],\n        listInfo: res.data.playlist,\n        canRemove: false\n      });\n    }\n  } catch (error) {\n    console.error('打开歌单失败:', error);\n    message.error('打开歌单失败');\n  }\n};\n\n// 处理专辑点击\nconst handleAlbumClick = async (item: any) => {\n  try {\n    const res = await getAlbumDetail(item.id.toString());\n    if (res.data?.album && res.data?.songs) {\n      const albumData = res.data.album;\n      const songs = res.data.songs.map((song: any) => ({\n        ...song,\n        picUrl: albumData.picUrl\n      }));\n\n      navigateToMusicList(router, {\n        id: item.id,\n        type: 'album',\n        name: albumData.name,\n        songList: songs,\n        listInfo: albumData,\n        canRemove: false\n      });\n    }\n  } catch (error) {\n    console.error('打开专辑失败:', error);\n    message.error('打开专辑失败');\n  }\n};\n\n// 删除歌单记录\nconst handleDelPlaylist = (item: any) => {\n  delPlaylist(item);\n  displayList.value = displayList.value.filter((playlist) => playlist.id !== item.id);\n};\n\n// 删除专辑记录\nconst handleDelAlbum = (item: any) => {\n  delAlbum(item);\n  displayList.value = displayList.value.filter((album) => album.id !== item.id);\n};\n\n// 加载历史数据（根据当前分类）\nconst loadHistoryData = async () => {\n  const currentList = getCurrentList();\n  if (currentList.length === 0) {\n    displayList.value = [];\n    return;\n  }\n\n  loading.value = true;\n  try {\n    const startIndex = (currentPage.value - 1) * pageSize;\n    const endIndex = startIndex + pageSize;\n    const currentPageItems = currentList.slice(startIndex, endIndex);\n\n    // 根据分类处理不同的数据\n    if (currentCategory.value === 'songs') {\n      // 处理歌曲数据\n      const neteaseItems = currentPageItems.filter((item) => item.source !== 'bilibili');\n      const bilibiliItems = currentPageItems.filter((item) => item.source === 'bilibili');\n\n      let neteaseSongs: SongResult[] = [];\n      if (neteaseItems.length > 0) {\n        const currentIds = neteaseItems.map((item) => item.id as number);\n        const res = await getMusicDetail(currentIds);\n        if (res.data.songs) {\n          neteaseSongs = res.data.songs.map((song: SongResult) => {\n            const historyItem = neteaseItems.find((item) => item.id === song.id);\n            return {\n              ...song,\n              picUrl: song.al?.picUrl || '',\n              count: historyItem?.count || 0,\n              source: 'netease'\n            };\n          });\n        }\n      }\n\n      const bilibiliIds = bilibiliItems\n        .map((item) => `${item.bilibiliData?.bvid}--1--${item.bilibiliData?.cid}`)\n        .filter((id) => id && !id.includes('undefined'));\n\n      const bilibiliSongs = await processBilibiliVideos(bilibiliIds);\n\n      bilibiliSongs.forEach((song) => {\n        const historyItem = bilibiliItems.find(\n          (item) =>\n            item.bilibiliData?.bvid === song.bilibiliData?.bvid &&\n            item.bilibiliData?.cid === song.bilibiliData?.cid\n        );\n        if (historyItem) {\n          song.count = historyItem.count || 0;\n        }\n      });\n\n      const newSongs = currentPageItems\n        .map((item) => {\n          if (item.source === 'bilibili') {\n            return bilibiliSongs.find(\n              (song) =>\n                song.bilibiliData?.bvid === item.bilibiliData?.bvid &&\n                song.bilibiliData?.cid === item.bilibiliData?.cid\n            );\n          }\n          return neteaseSongs.find((song) => song.id === item.id);\n        })\n        .filter((song): song is SongResult => !!song);\n\n      if (currentPage.value === 1) {\n        displayList.value = newSongs;\n      } else {\n        displayList.value = [...displayList.value, ...newSongs];\n      }\n    } else {\n      // 处理歌单和专辑数据（直接显示，不需要额外请求）\n      if (currentPage.value === 1) {\n        displayList.value = currentPageItems;\n      } else {\n        displayList.value = [...displayList.value, ...currentPageItems];\n      }\n    }\n\n    const totalLength = getCurrentList().length;\n    noMore.value = displayList.value.length >= totalLength;\n  } catch (error) {\n    console.error(t('history.getHistoryFailed'), error);\n  } finally {\n    loading.value = false;\n  }\n};\n\n// 处理滚动事件\nconst handleScroll = (e: any) => {\n  const { scrollTop, scrollHeight, offsetHeight } = e.target;\n  const threshold = 100;\n\n  if (!loading.value && !noMore.value && scrollHeight - (scrollTop + offsetHeight) < threshold) {\n    currentPage.value++;\n    loadHistoryData();\n  }\n};\n\n// 播放全部\nconst handlePlay = () => {\n  playerStore.setPlayList(displayList.value);\n};\n\n// 处理 tab 切换\nconst handleTabChange = async (value: 'local' | 'cloud') => {\n  currentTab.value = value;\n  currentPage.value = 1;\n  noMore.value = false;\n  displayList.value = [];\n\n  // 如果切换到云端，且还没有加载对应的云端数据，则加载\n  if (value === 'cloud') {\n    loading.value = true;\n    if (currentCategory.value === 'songs' && cloudRecords.value.length === 0) {\n      cloudRecords.value = await getCloudRecords();\n    } else if (currentCategory.value === 'playlists' && cloudPlaylists.value.length === 0) {\n      cloudPlaylists.value = await getCloudPlaylists();\n    } else if (currentCategory.value === 'albums' && cloudAlbums.value.length === 0) {\n      cloudAlbums.value = await getCloudAlbums();\n    }\n    loading.value = false;\n  }\n\n  await loadHistoryData();\n};\n\nonMounted(async () => {\n  if (!hasLoaded.value) {\n    await loadHistoryData();\n    hasLoaded.value = true;\n  }\n});\n\n// 监听历史列表变化，变化时重置并重新加载\nwatch(\n  [musicList, playlistList, albumList],\n  async () => {\n    if (hasLoaded.value) {\n      currentPage.value = 1;\n      noMore.value = false;\n      await loadHistoryData();\n    }\n  },\n  { deep: true }\n);\n\n// 重写删除方法，需要同时更新 displayList\nconst handleDelMusic = async (item: SongResult) => {\n  delMusic(item);\n  musicList.value = musicList.value.filter((music) => music.id !== item.id);\n  displayList.value = displayList.value.filter((music) => music.id !== item.id);\n};\n\n// 跳转到热力图页面\nconst handleNavigateToHeatmap = () => {\n  router.push('/heatmap');\n};\n</script>\n\n<style scoped lang=\"scss\">\n.history-page {\n  @apply h-full w-full pt-2;\n  @apply bg-light dark:bg-black;\n\n  .title-wrapper {\n    @apply flex items-center justify-between pb-2 px-4;\n\n    .title {\n      @apply text-xl font-bold;\n      @apply text-gray-900 dark:text-white;\n    }\n\n    .heatmap-btn {\n      @apply rounded-full px-4 h-8;\n      @apply transition-all duration-300;\n      @apply hover:scale-105;\n\n      .iconfont {\n        @apply text-base;\n      }\n    }\n  }\n\n  .category-tabs-wrapper {\n    @apply px-4 mb-2;\n  }\n\n  .tabs-wrapper {\n    @apply px-4;\n  }\n\n  .history-list-content {\n    @apply mt-2 pb-28 px-4;\n    .history-item {\n      @apply flex items-center justify-between;\n      &-content {\n        @apply flex-1 bg-light-100 dark:bg-dark-100 hover:bg-light-200 dark:hover:bg-dark-200 transition-all;\n      }\n      &-count {\n        @apply px-4 text-lg text-center;\n        @apply text-gray-600 dark:text-gray-400;\n      }\n      &-delete {\n        @apply cursor-pointer rounded-full border-2 w-8 h-8 flex justify-center items-center;\n        @apply border-gray-400 dark:border-gray-600;\n        @apply text-gray-600 dark:text-gray-400;\n        @apply hover:border-red-500 hover:text-red-500;\n      }\n    }\n  }\n}\n\n.loading-wrapper {\n  @apply flex justify-center items-center py-8;\n}\n\n.no-more-tip {\n  @apply text-center py-4 text-sm;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.no-data {\n  @apply text-center py-8 text-gray-500 dark:text-gray-400;\n}\n\n:deep(.n-tabs-rail) {\n  @apply rounded-xl overflow-hidden !important;\n  .n-tabs-capsule {\n    @apply rounded-xl !important;\n  }\n}\n\n.category-tabs-wrapper {\n  :deep(.n-tabs-rail) {\n    @apply rounded-xl overflow-hidden bg-white dark:bg-dark-300 !important;\n    .n-tabs-capsule {\n      @apply rounded-xl bg-green-500 dark:bg-green-600 !important;\n    }\n    .n-tabs-tab--active {\n      @apply text-white !important;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/historyAndFavorite/index.vue",
    "content": "<template>\n  <div class=\"flex gap-4 h-full pb-4\">\n    <favorite class=\"flex-item\" v-if=\"!isMobile\" />\n    <history-list class=\"flex-item\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\ndefineOptions({\n  name: 'History'\n});\n\nimport { isMobile } from '@/utils';\nimport Favorite from '@/views/favorite/index.vue';\nimport HistoryList from '@/views/history/index.vue';\n</script>\n\n<style scoped>\n.flex-item {\n  @apply flex-1 bg-light-100 dark:bg-dark-100 rounded-2xl overflow-hidden;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/home/index.vue",
    "content": "<template>\n  <n-scrollbar :size=\"100\" :x-scrollable=\"false\">\n    <div class=\"main-page\">\n      <!-- 推荐歌手 -->\n      <top-banner />\n      <div class=\"main-content\">\n        <!-- 歌单分类列表 -->\n        <playlist-type v-if=\"!isMobile\" />\n        <!-- 本周最热音乐 -->\n        <recommend-songlist />\n        <!-- 推荐最新专辑 -->\n        <div>\n          <favorite-list is-component />\n          <recommend-album />\n        </div>\n      </div>\n    </div>\n  </n-scrollbar>\n</template>\n\n<script lang=\"ts\" setup>\nimport PlaylistType from '@/components/home/PlaylistType.vue';\nimport RecommendAlbum from '@/components/home/RecommendAlbum.vue';\nimport RecommendSonglist from '@/components/home/RecommendSonglist.vue';\nimport TopBanner from '@/components/home/TopBanner.vue';\nimport { isMobile } from '@/utils';\nimport FavoriteList from '@/views/favorite/index.vue';\n\ndefineOptions({\n  name: 'Home'\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.main-page {\n  @apply h-full w-full overflow-hidden bg-light dark:bg-black;\n}\n.main-content {\n  @apply mt-6 flex mb-28;\n}\n\n.mobile {\n  .main-content {\n    @apply flex-col mx-4 mb-40;\n  }\n  :deep(.favorite-page) {\n    @apply p-0 mx-4 h-full;\n  }\n}\n\n:deep(.favorite-page) {\n  @apply p-0 mx-4 h-[300px];\n  .favorite-header {\n    @apply mb-0 px-0 !important;\n    h2 {\n      @apply text-lg font-bold text-gray-900 dark:text-white;\n    }\n  }\n  .favorite-list {\n    @apply px-0 !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/list/index.vue",
    "content": "<template>\n  <div class=\"list-page\">\n    <!-- 修改歌单分类部分 -->\n    <div class=\"play-list-type\">\n      <n-scrollbar ref=\"scrollbarRef\" x-scrollable>\n        <div class=\"categories-wrapper\" @wheel.prevent=\"handleWheel\">\n          <span\n            v-for=\"(item, index) in playlistCategory?.sub\"\n            :key=\"item.name\"\n            class=\"play-list-type-item\"\n            :class=\"[setAnimationClass('animate__bounceIn'), { active: currentType === item.name }]\"\n            :style=\"getAnimationDelay(index)\"\n            @click=\"handleClickPlaylistType(item.name)\"\n          >\n            {{ item.name }}\n          </span>\n        </div>\n      </n-scrollbar>\n    </div>\n    <!-- 歌单列表 -->\n    <n-scrollbar\n      class=\"recommend\"\n      style=\"height: calc(100% - 55px)\"\n      :size=\"100\"\n      @scroll=\"handleScroll\"\n    >\n      <div v-loading=\"loading\" class=\"recommend-list\">\n        <div\n          v-for=\"(item, index) in recommendList\"\n          :key=\"item.id\"\n          class=\"recommend-item\"\n          :class=\"setAnimationClass('animate__bounceIn')\"\n          :style=\"getItemAnimationDelay(index)\"\n          @click.stop=\"openPlaylist(item)\"\n        >\n          <div class=\"recommend-item-img\">\n            <n-image\n              class=\"recommend-item-img-img\"\n              :src=\"getImgUrl(item.picUrl || item.coverImgUrl, '300y300')\"\n              width=\"200\"\n              height=\"200\"\n              lazy\n              preview-disabled\n            />\n            <div class=\"top\">\n              <div class=\"play-count\">{{ formatNumber(item.playCount) }}</div>\n              <i class=\"iconfont icon-videofill\"></i>\n            </div>\n          </div>\n          <div class=\"recommend-item-title\">{{ item.name }}</div>\n        </div>\n      </div>\n      <!-- 加载状态 -->\n      <div v-if=\"isLoadingMore\" class=\"loading-more\">\n        <n-spin size=\"small\" />\n        <span class=\"ml-2\">加载中...</span>\n      </div>\n      <div v-if=\"!hasMore && recommendList.length > 0\" class=\"no-more\">没有更多了</div>\n    </n-scrollbar>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useRoute, useRouter } from 'vue-router';\n\nimport { getPlaylistCategory } from '@/api/home';\nimport { getListByCat, getListDetail } from '@/api/list';\nimport { navigateToMusicList } from '@/components/common/MusicListNavigator';\nimport type { IRecommendItem } from '@/types/list';\nimport type { IListDetail } from '@/types/listDetail';\nimport type { IPlayListSort } from '@/types/playlist';\nimport { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';\n\ndefineOptions({\n  name: 'List'\n});\n\nconst TOTAL_ITEMS = 42; // 每页数量\n\nconst recommendList = ref<any[]>([]);\nconst page = ref(0);\nconst hasMore = ref(true);\nconst isLoadingMore = ref(false);\n\n// 计算每个项目的动画延迟\nconst getItemAnimationDelay = (index: number) => {\n  const currentPageIndex = index % TOTAL_ITEMS;\n  return setAnimationDelay(currentPageIndex, 30);\n};\n\nconst recommendItem = ref<IRecommendItem | null>();\nconst listDetail = ref<IListDetail | null>();\nconst listLoading = ref(true);\n\nconst router = useRouter();\n\nconst openPlaylist = (item: any) => {\n  recommendItem.value = item;\n  listLoading.value = true;\n\n  getListDetail(item.id).then((res) => {\n    listDetail.value = res.data;\n    listLoading.value = false;\n\n    navigateToMusicList(router, {\n      id: item.id,\n      type: 'playlist',\n      name: item.name,\n      songList: res.data.playlist.tracks || [],\n      listInfo: res.data.playlist,\n      canRemove: false\n    });\n  });\n};\n\nconst route = useRoute();\nconst listTitle = ref(route.query.type || '歌单列表');\n\nconst loading = ref(false);\nconst loadList = async (type: string, isLoadMore = false) => {\n  if (!hasMore.value && isLoadMore) return;\n  if (isLoadMore) {\n    isLoadingMore.value = true;\n  } else {\n    loading.value = true;\n    page.value = 0;\n    recommendList.value = [];\n  }\n\n  try {\n    const params = {\n      cat: type === '每日推荐' ? '' : type,\n      limit: TOTAL_ITEMS,\n      offset: page.value * TOTAL_ITEMS\n    };\n    const { data } = await getListByCat(params);\n    if (isLoadMore) {\n      recommendList.value.push(...data.playlists);\n    } else {\n      recommendList.value = data.playlists;\n    }\n    hasMore.value = data.more;\n    page.value++;\n  } catch (error) {\n    console.error('加载歌单列表失败:', error);\n  } finally {\n    loading.value = false;\n    isLoadingMore.value = false;\n  }\n};\n\n// 监听滚动事件\nconst handleScroll = (e: any) => {\n  const { scrollTop, scrollHeight, clientHeight } = e.target;\n  // 距离底部100px时加载更多\n  if (scrollTop + clientHeight >= scrollHeight - 100 && !isLoadingMore.value && hasMore.value) {\n    loadList(currentType.value, true);\n  }\n};\n\n// 添加歌单分类相关的代码\nconst playlistCategory = ref<IPlayListSort>();\nconst currentType = ref((route.query.type as string) || '每日推荐');\n\nconst getAnimationDelay = computed(() => {\n  return (index: number) => setAnimationDelay(index, 30);\n});\n\n// 加载歌单分类\nconst loadPlaylistCategory = async () => {\n  const { data } = await getPlaylistCategory();\n  playlistCategory.value = {\n    ...data,\n    sub: [\n      {\n        name: '每日推荐',\n        category: 0\n      },\n      ...data.sub\n    ]\n  };\n};\n\nconst handleClickPlaylistType = (type: string) => {\n  currentType.value = type;\n  listTitle.value = type;\n  loading.value = true;\n  loadList(type);\n};\n\nconst scrollbarRef = ref();\n\nconst handleWheel = (e: WheelEvent) => {\n  const scrollbar = scrollbarRef.value;\n  if (scrollbar) {\n    const delta = e.deltaY || e.detail;\n    scrollbar.scrollBy({ left: delta });\n  }\n};\n\nonMounted(() => {\n  loadPlaylistCategory(); // 添加加载歌单分类\n  currentType.value = (route.query.type as string) || currentType.value;\n  loadList(currentType.value);\n});\n\nwatch(\n  () => route.query,\n  async (newParams) => {\n    if (newParams.type) {\n      recommendList.value = [];\n      listTitle.value = newParams.type || '歌单列表';\n      currentType.value = newParams.type as string;\n      loading.value = true;\n      loadList(newParams.type as string);\n    }\n  }\n);\n</script>\n\n<style lang=\"scss\" scoped>\n.list-page {\n  @apply relative h-full w-full;\n  @apply bg-light dark:bg-black;\n}\n\n.recommend {\n  &-title {\n    @apply text-lg font-bold pb-2;\n    @apply text-gray-900 dark:text-white;\n  }\n\n  &-list {\n    @apply grid gap-x-8 gap-y-6 pb-28 pr-4;\n    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n  }\n\n  &-item {\n    @apply flex flex-col;\n\n    &-img {\n      @apply rounded-xl overflow-hidden relative w-full aspect-square;\n\n      &-img {\n        @apply block w-full h-full;\n      }\n\n      img {\n        @apply absolute top-0 left-0 w-full h-full object-cover rounded-xl;\n      }\n\n      &:hover img {\n        @apply hover:scale-110 transition-all duration-300 ease-in-out;\n      }\n\n      .top {\n        @apply absolute w-full h-full top-0 left-0 flex justify-center items-center transition-all duration-300 ease-in-out cursor-pointer;\n        @apply bg-black bg-opacity-50;\n        opacity: 0;\n\n        i {\n          @apply text-5xl text-white transition-all duration-500 ease-in-out opacity-0;\n        }\n\n        &:hover {\n          @apply opacity-100;\n        }\n\n        &:hover i {\n          @apply transform scale-150 opacity-100;\n        }\n\n        .play-count {\n          @apply absolute top-2 left-2 text-sm text-white;\n        }\n      }\n    }\n\n    &-title {\n      @apply mt-2 text-sm line-clamp-1;\n      @apply text-gray-900 dark:text-white;\n    }\n  }\n}\n\n.loading-more {\n  @apply flex justify-center items-center py-4;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.no-more {\n  @apply text-center py-4;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.mobile {\n  .recommend-title {\n    @apply text-xl font-bold px-4;\n  }\n\n  .recommend-list {\n    @apply px-4 gap-4;\n    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n  }\n}\n\n// 添加歌单分类样式\n.play-list-type {\n  .categories-wrapper {\n    @apply flex items-center py-2;\n    white-space: nowrap;\n  }\n\n  &-item {\n    @apply py-2 px-3 mr-3 inline-block rounded-xl cursor-pointer transition-all duration-300;\n    @apply bg-light dark:bg-black text-gray-900 dark:text-white;\n    @apply border border-gray-200 dark:border-gray-700;\n\n    &:hover {\n      @apply bg-green-50 dark:bg-green-900;\n    }\n\n    &.active {\n      @apply bg-green-500 border-green-500 text-white;\n    }\n  }\n}\n\n.mobile {\n  .play-list-type {\n    @apply mx-0 w-full;\n  }\n  .categories-wrapper {\n    @apply pl-4;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/login/index.vue",
    "content": "<template>\n  <div class=\"login-page\">\n    <div class=\"phone-login\" :class=\"setAnimationClass('animate__fadeInDown')\">\n      <div class=\"bg\"></div>\n      <div class=\"content\">\n        <!-- Tab导航 -->\n        <div class=\"login-tabs\" :class=\"setAnimationClass('animate__fadeInUp')\">\n          <div\n            v-for=\"tab in loginTabs\"\n            :key=\"tab.key\"\n            class=\"tab-item\"\n            :class=\"{ active: activeMode === tab.key }\"\n            @click=\"switchToMode(tab.key)\"\n          >\n            {{ tab.label }}\n          </div>\n        </div>\n\n        <!-- 登录内容区域 -->\n        <div class=\"login-content\">\n          <!-- 过渡动画包装器 -->\n          <transition\n            name=\"login-content\"\n            mode=\"out-in\"\n            enter-active-class=\"animate__animated animate__fadeIn\"\n            leave-active-class=\"animate__animated animate__fadeOut\"\n          >\n            <!-- 二维码登录组件 -->\n            <div v-if=\"activeMode === LoginMode.QR && !isTransitioning\" key=\"qr\" class=\"phone\">\n              <qr-login @login-success=\"handleLoginSuccess\" @login-error=\"handleLoginError\" />\n            </div>\n\n            <!-- 手机号登录 -->\n            <div\n              v-else-if=\"activeMode === LoginMode.PHONE && !isTransitioning\"\n              key=\"phone\"\n              class=\"phone\"\n            >\n              <div class=\"login-title\">{{ t('login.title.phone') }}</div>\n              <div class=\"phone-page\">\n                <input\n                  v-model=\"phone\"\n                  class=\"phone-input\"\n                  type=\"text\"\n                  :placeholder=\"t('login.placeholder.phone')\"\n                />\n                <input\n                  v-model=\"password\"\n                  class=\"phone-input\"\n                  type=\"password\"\n                  :placeholder=\"t('login.placeholder.password')\"\n                />\n              </div>\n              <div class=\"text\">{{ t('login.phoneTip') }}</div>\n              <n-button class=\"btn-login\" @click=\"loginPhone()\">{{\n                t('login.button.login')\n              }}</n-button>\n            </div>\n\n            <!-- UID登录组件 -->\n            <div\n              v-else-if=\"activeMode === LoginMode.UID && !isTransitioning\"\n              key=\"uid\"\n              class=\"phone\"\n            >\n              <uid-login @login-success=\"handleLoginSuccess\" @login-error=\"handleLoginError\" />\n            </div>\n\n            <!-- Cookie登录组件 -->\n            <div\n              v-else-if=\"activeMode === LoginMode.COOKIE && !isTransitioning\"\n              key=\"token\"\n              class=\"phone\"\n            >\n              <cookie-login @login-success=\"handleLoginSuccess\" @login-error=\"handleLoginError\" />\n            </div>\n          </transition>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useMessage } from 'naive-ui';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\n\nimport { loginByCellphone } from '@/api/login';\nimport CookieLogin from '@/components/login/CookieLogin.vue';\nimport QrLogin from '@/components/login/QrLogin.vue';\nimport UidLogin from '@/components/login/UidLogin.vue';\nimport { useUserStore } from '@/store/modules/user';\nimport { setAnimationClass } from '@/utils';\n\ndefineOptions({\n  name: 'Login'\n});\n\n// 登录模式枚举\nenum LoginMode {\n  QR = 'qr',\n  PHONE = 'phone',\n  UID = 'uid',\n  COOKIE = 'cookie'\n}\n\nconst { t } = useI18n();\nconst message = useMessage();\nconst router = useRouter();\nconst userStore = useUserStore();\n\n// 当前激活的登录模式\nconst activeMode = ref<LoginMode>(LoginMode.COOKIE);\n// 用于控制内容切换动画\nconst isTransitioning = ref(false);\n\n// 登录选项配置\nconst loginTabs = computed(() => [\n  { key: LoginMode.COOKIE, label: t('login.title.cookie') },\n  { key: LoginMode.UID, label: t('login.title.uid') },\n  { key: LoginMode.QR, label: t('login.title.qr') }\n]);\n\n// 手机号登录\nconst phone = ref('');\nconst password = ref('');\nconst loginPhone = async () => {\n  try {\n    if (!phone.value.trim()) {\n      message.error(t('login.message.phoneRequired'));\n      return;\n    }\n    if (!password.value.trim()) {\n      message.error(t('login.message.passwordRequired'));\n      return;\n    }\n\n    const { data } = await loginByCellphone(phone.value, password.value);\n    if (data.code === 200) {\n      message.success(t('login.message.loginSuccess'));\n      userStore.setUser(data.profile);\n      localStorage.setItem('token', data.cookie);\n      setTimeout(() => {\n        router.push('/user');\n      }, 1000);\n    } else {\n      message.error(t('login.message.phoneLoginFailed'));\n    }\n  } catch (error) {\n    message.error(t('login.message.phoneLoginFailed'));\n    console.error(t('login.message.loginFailed') + ':', error);\n  }\n};\n\n// 切换登录模式（带动画效果）\nconst switchToMode = (mode: LoginMode) => {\n  if (mode === activeMode.value) return;\n\n  isTransitioning.value = true;\n  setTimeout(() => {\n    activeMode.value = mode;\n    setTimeout(() => {\n      isTransitioning.value = false;\n    }, 50);\n  }, 150);\n};\n\n// 通用登录成功处理\nconst handleLoginSuccess = (userProfile: any, loginType: string) => {\n  // 更新 userStore（这会同时更新 store 状态和 localStorage 中的用户数据）\n  userStore.setUser(userProfile);\n\n  // 设置登录类型到 userStore 和 localStorage\n  userStore.setLoginType(loginType as any);\n\n  // 设置其他相关状态\n  const token = loginType !== 'uid' ? localStorage.getItem('token') : undefined;\n\n  if (token) {\n    localStorage.setItem('token', token);\n  }\n\n  if (loginType === 'uid') {\n    localStorage.setItem('uidLogin', 'true');\n  }\n\n  setTimeout(() => {\n    router.push('/user');\n  }, 1000);\n};\n\n// 通用登录错误处理\nconst handleLoginError = (error: string) => {\n  console.error(t('login.message.loginFailed') + ':', error);\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.login-page {\n  @apply flex flex-col items-center justify-center;\n  @apply bg-light dark:bg-black;\n}\n\n.login-title {\n  @apply text-2xl font-bold mb-6 text-white;\n}\n\n.text {\n  @apply mt-4 text-white text-xs;\n}\n\n.phone-login {\n  width: 350px;\n  height: 550px; /* 恢复原来的高度 */\n  @apply rounded-2xl rounded-b-none bg-cover bg-no-repeat relative overflow-hidden;\n  background-image: url(\"data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' version='1.1' xmlns:xlink='http://www.w3.org/1999/xlink' xmlns:svgjs='http://svgjs.dev/svgjs' width='400' height='560' preserveAspectRatio='none' viewBox='0 0 400 560'%3e%3cg mask='url(%26quot%3b%23SvgjsMask1066%26quot%3b)' fill='none'%3e%3crect width='400' height='560' x='0' y='0' fill='rgba(24%2c 106%2c 59%2c 1)'%3e%3c/rect%3e%3cpath d='M0%2c234.738C43.535%2c236.921%2c80.103%2c205.252%2c116.272%2c180.923C151.738%2c157.067%2c188.295%2c132.929%2c207.855%2c94.924C227.898%2c55.979%2c233.386%2c10.682%2c226.119%2c-32.511C218.952%2c-75.107%2c199.189%2c-115.793%2c167.469%2c-145.113C137.399%2c-172.909%2c92.499%2c-171.842%2c55.779%2c-189.967C8.719%2c-213.196%2c-28.344%2c-282.721%2c-78.217%2c-266.382C-128.725%2c-249.834%2c-111.35%2c-166.696%2c-143.781%2c-124.587C-173.232%2c-86.348%2c-244.72%2c-83.812%2c-255.129%2c-36.682C-265.368%2c9.678%2c-217.952%2c48.26%2c-190.512%2c87.004C-167.691%2c119.226%2c-140.216%2c145.431%2c-109.013%2c169.627C-74.874%2c196.1%2c-43.147%2c232.575%2c0%2c234.738' fill='%23114b2a'%3e%3c/path%3e%3cpath d='M400 800.9010000000001C443.973 795.023 480.102 765.6 513.011 735.848 541.923 709.71 561.585 676.6320000000001 577.037 640.85 592.211 605.712 606.958 568.912 601.458 531.035 595.962 493.182 568.394 464.36400000000003 546.825 432.775 522.317 396.88300000000004 507.656 347.475 466.528 333.426 425.366 319.366 384.338 352.414 342.111 362.847 297.497 373.869 242.385 362.645 211.294 396.486 180.212 430.318 192.333 483.83299999999997 188.872 529.644 185.656 572.218 178.696 614.453 191.757 655.101 205.885 699.068 227.92 742.4110000000001 265.75 768.898 304.214 795.829 353.459 807.1220000000001 400 800.9010000000001' fill='%231f894c'%3e%3c/path%3e%3c/g%3e%3cdefs%3e%3cmask id='SvgjsMask1066'%3e%3crect width='400' height='560' fill='white'%3e%3c/rect%3e%3c/mask%3e%3c/defs%3e%3c/svg%3e\");\n  box-shadow: inset 0px 0px 20px 5px rgba(0, 0, 0, 0.37);\n  animation-duration: 0.8s;\n\n  .bg {\n    @apply absolute w-full h-full bg-light-100 dark:bg-dark-100 opacity-20;\n  }\n\n  .content {\n    @apply absolute w-full h-full p-4 flex flex-col items-center justify-center text-center;\n\n    .login-tabs {\n      @apply flex mb-6 bg-black bg-opacity-20 rounded-xl p-1;\n      width: 320px;\n      animation-duration: 0.6s;\n      animation-delay: 0.2s;\n\n      .tab-item {\n        @apply flex-1 py-2 px-3 text-sm text-white text-center cursor-pointer rounded-lg transition-all duration-300;\n        @apply hover:bg-white hover:bg-opacity-10;\n        transform: translateY(0);\n\n        &:hover {\n          transform: translateY(-2px);\n        }\n\n        &.active {\n          @apply bg-green-600 text-white font-medium;\n          transform: translateY(-1px);\n          box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n        }\n      }\n    }\n\n    .login-content {\n      @apply flex-1 flex items-center justify-center;\n      min-height: 300px;\n    }\n\n    .phone {\n      animation-duration: 0.5s;\n      width: 100%;\n      max-width: 300px;\n\n      &-page {\n        @apply bg-light dark:bg-gray-800 bg-opacity-90 dark:bg-opacity-90;\n        width: 250px;\n        @apply rounded-2xl overflow-hidden;\n        margin: 0 auto;\n      }\n\n      &-input {\n        height: 40px;\n        @apply w-full px-4 outline-none;\n        @apply text-gray-900 dark:text-white bg-transparent;\n        @apply border-b border-gray-200 dark:border-gray-700;\n        @apply placeholder-gray-500 dark:placeholder-gray-400;\n        transition: all 0.3s ease;\n\n        &:focus {\n          @apply border-green-500;\n          transform: translateY(-1px);\n        }\n      }\n    }\n\n    .btn-login {\n      width: 250px;\n      height: 40px;\n      @apply mt-10 text-white rounded-xl;\n      @apply bg-green-600 hover:bg-green-700 transition-all duration-300;\n      transform: translateY(0);\n\n      &:hover {\n        transform: translateY(-2px);\n        box-shadow: 0 6px 12px rgba(34, 197, 94, 0.3);\n      }\n    }\n  }\n}\n\n/* 登录内容切换动画 */\n.login-content-enter-active,\n.login-content-leave-active {\n  animation-duration: 0.3s;\n}\n\n.login-content-enter-from {\n  opacity: 0;\n  transform: translateY(20px);\n}\n\n.login-content-leave-to {\n  opacity: 0;\n  transform: translateY(-20px);\n}\n\n.mobile {\n  .login-page {\n    @apply pt-0;\n  }\n\n  .phone-login {\n    width: 90vw;\n    max-width: 350px;\n    height: 500px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/lyric/index.vue",
    "content": "<template>\n  <div\n    class=\"lyric-window\"\n    :class=\"[lyricSetting.theme, { lyric_lock: lyricSetting.isLock }]\"\n    @mousedown=\"handleMouseDown\"\n    @mouseenter=\"handleMouseEnter\"\n    @mouseleave=\"handleMouseLeave\"\n  >\n    <div class=\"drag-overlay\"></div>\n    <!-- 顶部控制栏 -->\n    <div class=\"control-bar\" :class=\"{ 'control-bar-show': showControls }\">\n      <div class=\"font-size-controls\">\n        <n-button-group>\n          <div class=\"control-button\" @click=\"decreaseFontSize\">\n            <i class=\"ri-subtract-line\"></i>\n          </div>\n          <div class=\"control-button\" @click=\"increaseFontSize\">\n            <i class=\"ri-add-line\"></i>\n          </div>\n        </n-button-group>\n        <div v-html=\"staticData.playMusic.name\"></div>\n      </div>\n      <!-- 添加播放控制按钮 -->\n      <div class=\"play-controls\">\n        <div class=\"control-button\" @click=\"handlePrev\">\n          <i class=\"ri-skip-back-fill\"></i>\n        </div>\n        <div class=\"control-button play-button\" @click=\"handlePlayPause\">\n          <i :class=\"dynamicData.isPlay ? 'ri-pause-fill' : 'ri-play-fill'\"></i>\n        </div>\n        <div class=\"control-button\" @click=\"handleNext\">\n          <i class=\"ri-skip-forward-fill\"></i>\n        </div>\n      </div>\n      <div class=\"control-buttons\">\n        <div class=\"control-button\" @click=\"checkTheme\">\n          <i v-if=\"lyricSetting.theme === 'light'\" class=\"ri-sun-line\"></i>\n          <i v-else class=\"ri-moon-line\"></i>\n        </div>\n        <div\n          class=\"control-button theme-color-button\"\n          :class=\"{ active: showThemeColorPanel }\"\n          @click=\"toggleThemeColorPanel\"\n        >\n          <i class=\"ri-palette-line\"></i>\n        </div>\n        <!-- <div class=\"control-button\" @click=\"handleTop\">\n          <i class=\"ri-pushpin-line\" :class=\"{ active: lyricSetting.isTop }\"></i>\n        </div> -->\n        <div id=\"lyric-lock\" class=\"control-button\" @click=\"handleLock\">\n          <i v-if=\"lyricSetting.isLock\" class=\"ri-lock-line\"></i>\n          <i v-else class=\"ri-lock-unlock-line\"></i>\n        </div>\n        <div class=\"control-button\" @click=\"handleClose\">\n          <i class=\"ri-close-line\"></i>\n        </div>\n      </div>\n    </div>\n\n    <!-- 主题色选择面板 -->\n    <theme-color-panel\n      :visible=\"showThemeColorPanel\"\n      :current-color=\"currentHighlightColor\"\n      :theme=\"lyricSetting.theme\"\n      @color-change=\"handleColorChange\"\n      @close=\"handleThemeColorPanelClose\"\n    />\n\n    <!-- 歌词显示区域 -->\n    <div ref=\"containerRef\" class=\"lyric-container\">\n      <div class=\"lyric-scroll\">\n        <div class=\"lyric-wrapper\" :style=\"wrapperStyle\">\n          <template v-if=\"staticData.lrcArray?.length > 0\">\n            <div\n              v-for=\"(line, index) in staticData.lrcArray\"\n              :key=\"index\"\n              class=\"lyric-line\"\n              :style=\"getDynamicLineStyle(line)\"\n              :class=\"{\n                'lyric-line-current': index === currentIndex,\n                'lyric-line-passed': index < currentIndex,\n                'lyric-line-next': index === currentIndex + 1\n              }\"\n            >\n              <div class=\"lyric-text\" :style=\"{ fontSize: `${fontSize}px` }\">\n                <!-- 逐字歌词显示 -->\n                <div\n                  v-if=\"line.hasWordByWord && line.words && line.words.length > 0\"\n                  class=\"word-by-word-lyric\"\n                >\n                  <template v-for=\"(word, wordIndex) in line.words\" :key=\"wordIndex\">\n                    <span class=\"lyric-word\" :style=\"getWordStyle(index, wordIndex, word)\">\n                      {{ word.text }} </span\n                    ><span class=\"lyric-word\" v-if=\"word.space\">&nbsp;</span></template\n                  >\n                </div>\n                <!-- 普通歌词显示 -->\n                <span v-else class=\"lyric-text-inner\" :style=\"getLyricStyle(index)\">\n                  {{ line.text || '' }}\n                </span>\n              </div>\n              <div\n                v-if=\"line.trText\"\n                class=\"lyric-translation\"\n                :style=\"{ fontSize: `${fontSize * 0.6}px` }\"\n              >\n                {{ line.trText }}\n              </div>\n            </div>\n          </template>\n          <div v-else class=\"lyric-empty\">无歌词</div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted, ref, watch } from 'vue';\n\nimport ThemeColorPanel from '@/components/lyric/ThemeColorPanel.vue';\nimport { SongResult } from '@/types/music';\nimport {\n  getCurrentLyricThemeColor,\n  loadLyricThemeColor,\n  saveLyricThemeColor,\n  validateColor\n} from '@/utils/linearColor';\n\ndefineOptions({\n  name: 'Lyric'\n});\nconst windowData = window as any;\nconst containerRef = ref<HTMLElement | null>(null);\nconst containerHeight = ref(0);\nconst lineHeight = ref(60);\nconst currentIndex = ref(0);\n// 字体大小控制\nconst fontSize = ref(24); // 默认字体大小\nconst fontSizeStep = 2; // 每次整的步长\nconst animationFrameId = ref<number | null>(null);\nconst lastUpdateTime = ref(performance.now());\n\n// 静态数据\nconst staticData = ref<{\n  lrcArray: Array<{\n    text: string;\n    trText: string;\n    words?: Array<{ text: string; startTime: number; duration: number; space?: boolean }>;\n    hasWordByWord?: boolean;\n    startTime?: number;\n    duration?: number;\n  }>;\n  lrcTimeArray: number[];\n  allTime: number;\n  playMusic: SongResult;\n}>({\n  lrcArray: [],\n  lrcTimeArray: [],\n  allTime: 0,\n  playMusic: {} as SongResult\n});\n\n// 动态数据\nconst dynamicData = ref({\n  nowTime: 0,\n  startCurrentTime: 0,\n  nextTime: 0,\n  isPlay: true\n});\n\n// 安全加载歌词设置\nconst loadLyricSettings = () => {\n  try {\n    const stored = localStorage.getItem('lyricData');\n    if (stored) {\n      const parsed = JSON.parse(stored);\n\n      // 验证 highlightColor 字段\n      let validatedHighlightColor = parsed.highlightColor;\n      if (validatedHighlightColor && !validateColor(validatedHighlightColor)) {\n        console.warn('Invalid stored highlight color, removing it');\n        validatedHighlightColor = undefined;\n      }\n\n      // 确保所有必需字段存在并有效\n      return {\n        isTop: parsed.isTop ?? false,\n        theme: parsed.theme === 'light' || parsed.theme === 'dark' ? parsed.theme : 'dark',\n        isLock: parsed.isLock ?? false,\n        highlightColor: validatedHighlightColor\n      };\n    }\n  } catch (error) {\n    console.error('Failed to load lyric settings:', error);\n  }\n\n  // 返回默认设置\n  return {\n    isTop: false,\n    theme: 'dark' as 'light' | 'dark',\n    isLock: false,\n    highlightColor: undefined as string | undefined\n  };\n};\n\nconst lyricSetting = ref(loadLyricSettings());\n\nlet hideControlsTimer: number | null = null;\n\nconst isHovering = ref(false);\n\n// 主题色相关状态\nconst showThemeColorPanel = ref(false);\nconst currentHighlightColor = ref('#1db954');\n\n// 计算是否栏\nconst showControls = computed(() => {\n  if (lyricSetting.value.isLock) {\n    return isHovering.value;\n  }\n  return true;\n});\n\n// 清除隐藏定时器\nconst clearHideTimer = () => {\n  if (hideControlsTimer) {\n    clearTimeout(hideControlsTimer);\n    hideControlsTimer = null;\n  }\n};\n\n// 处理鼠标进入窗口\nconst handleMouseEnter = () => {\n  if (lyricSetting.value.isLock) {\n    isHovering.value = true;\n    windowData.electron.ipcRenderer.send('set-ignore-mouse', true);\n  } else {\n    windowData.electron.ipcRenderer.send('set-ignore-mouse', false);\n  }\n};\n\n// 处理鼠标离开窗口\nconst handleMouseLeave = () => {\n  if (!lyricSetting.value.isLock) return;\n  isHovering.value = false;\n  windowData.electron.ipcRenderer.send('set-ignore-mouse', false);\n\n  // 强制重置背景色\n  const lyricWindow = document.querySelector('.lyric-window') as HTMLElement;\n  if (lyricWindow) {\n    lyricWindow.style.background = 'transparent';\n    // 使用 requestAnimationFrame 确保在下一帧重置\n    requestAnimationFrame(() => {\n      lyricWindow.style.background = 'transparent';\n    });\n  }\n};\n\n// 监听锁定状态变化\nwatch(\n  () => lyricSetting.value.isLock,\n  (newLock: boolean) => {\n    if (newLock) {\n      isHovering.value = false;\n    }\n  }\n);\n\nonMounted(() => {\n  // 初始化时，如果是锁定状态，确保控制栏隐藏\n  if (lyricSetting.value.isLock) {\n    isHovering.value = false;\n  }\n});\n\nonUnmounted(() => {\n  clearHideTimer();\n});\n\n// 计算歌词滚动位置\nconst wrapperStyle = computed(() => {\n  if (!containerHeight.value) {\n    return {\n      transform: 'translateY(0)',\n      transition: 'none'\n    };\n  }\n\n  // 计算容器中心点\n  const containerCenter = containerHeight.value / 2;\n\n  // 计算每行的实际高度\n  const getLineHeight = (line: { text: string; trText: string }) => {\n    const baseHeight = lineHeight.value;\n    if (line.trText) {\n      const extraHeight = Math.round(fontSize.value * 0.6 * 1.4);\n      return baseHeight + extraHeight;\n    }\n    return baseHeight;\n  };\n\n  // 计算当前行之前所有行的累积高度\n  let accumulatedHeight = containerHeight.value * 0.2; // 顶部padding\n  for (let i = 0; i < currentIndex.value; i++) {\n    if (i < staticData.value.lrcArray.length) {\n      accumulatedHeight += getLineHeight(staticData.value.lrcArray[i]);\n    } else {\n      accumulatedHeight += lineHeight.value;\n    }\n  }\n\n  // 加上当前行的一半高度，使其居中\n  const currentLineHeight =\n    currentIndex.value < staticData.value.lrcArray.length\n      ? getLineHeight(staticData.value.lrcArray[currentIndex.value])\n      : lineHeight.value;\n  accumulatedHeight += currentLineHeight;\n\n  // 计算偏移量，使当前行居中\n  const targetOffset = containerCenter - accumulatedHeight;\n\n  // 计算内容总高度（包含padding）\n  let contentHeight = containerHeight.value * 0.4; // 上下padding总和\n  for (const line of staticData.value.lrcArray) {\n    contentHeight += getLineHeight(line);\n  }\n\n  // 计算最小和最大偏移量\n  const minOffset = -(contentHeight - containerHeight.value);\n  const maxOffset = 0;\n\n  // 限制偏移量在合理范围内\n  const finalOffset = Math.min(maxOffset, Math.max(minOffset, targetOffset));\n\n  return {\n    transform: `translateY(${finalOffset}px)`,\n    transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)'\n  };\n});\n\n// 新增：根据是否有翻译文本动态计算每行的样式\nconst getDynamicLineStyle = (line: { text: string; trText: string }) => {\n  // 默认行高\n  const defaultHeight = lineHeight.value;\n\n  // 如果有翻译文本，增加额外高度\n  if (line.trText) {\n    // 计算翻译文本的额外高度 (字体大小的0.6倍 * 行高比例1.4)\n    const extraHeight = Math.round(fontSize.value * 0.6 * 1.4);\n    return {\n      height: `${defaultHeight + extraHeight}px`\n    };\n  }\n\n  return {\n    height: `${defaultHeight}px`\n  };\n};\n\n// 更新容器高度和行高\nconst updateContainerHeight = () => {\n  if (!containerRef.value) return;\n\n  // 更新容器高度\n  containerHeight.value = containerRef.value.clientHeight;\n\n  // 计算基础行高(字体大小的2.5倍)\n  const baseLineHeight = fontSize.value * 2.5;\n\n  // 计算最大允许行高(容器高度的1/4)\n  const maxAllowedHeight = containerHeight.value / 3;\n\n  // 设置行高(不小于40px,不大于最大允许高度)\n  lineHeight.value = Math.min(maxAllowedHeight, Math.max(40, baseLineHeight));\n};\n\n// 处理字体大小变化\nconst handleFontSizeChange = async () => {\n  // 先保存字体大小\n  saveFontSize();\n\n  // 更新容器高度和行高\n  updateContainerHeight();\n};\n\n// 增加字体大小\nconst increaseFontSize = async () => {\n  if (fontSize.value < 48) {\n    fontSize.value += fontSizeStep;\n    await handleFontSizeChange();\n  }\n};\n\n// 减小字体大小\nconst decreaseFontSize = async () => {\n  if (fontSize.value > 12) {\n    fontSize.value -= fontSizeStep;\n    await handleFontSizeChange();\n  }\n};\n\n// 保存字体大小到本地存储\nconst saveFontSize = () => {\n  localStorage.setItem('lyricFontSize', fontSize.value.toString());\n};\n\n// 监听容器大小变化\nonMounted(() => {\n  const resizeObserver = new ResizeObserver(() => {\n    updateContainerHeight();\n  });\n\n  if (containerRef.value) {\n    resizeObserver.observe(containerRef.value);\n  }\n\n  onUnmounted(() => {\n    resizeObserver.disconnect();\n  });\n});\n// 实际播放时间\nconst actualTime = ref(0);\n\n// 计算当前行的进度\nconst currentProgress = computed(() => {\n  const { startCurrentTime, nextTime } = dynamicData.value;\n  if (!startCurrentTime || !nextTime) return 0;\n\n  const duration = nextTime - startCurrentTime;\n  const elapsed = actualTime.value - startCurrentTime;\n  return Math.min(Math.max(elapsed / duration, 0), 1);\n});\n\n// 获取歌词样式\nconst getLyricStyle = (index: number) => {\n  if (index !== currentIndex.value) return {};\n\n  const progress = currentProgress.value * 100;\n\n  // 使用更清晰的渐变实现\n  return {\n    background: `linear-gradient(to right, var(--highlight-color) ${progress}%, var(--text-color) ${progress}%)`,\n    WebkitBackgroundClip: 'text',\n    WebkitTextFillColor: 'transparent',\n    // 优化字体渲染，减少发虚\n    textRendering: 'optimizeLegibility' as const,\n    WebkitFontSmoothing: 'antialiased' as const,\n    MozOsxFontSmoothing: 'grayscale' as const,\n    // 使用 transform 而不是直接的 transition 来提高性能\n    transform: 'translateZ(0)', // 启用硬件加速\n    backfaceVisibility: 'hidden' as const, // 减少渲染问题\n    transition: 'background 0.1s linear'\n  };\n};\n\n// 逐字歌词样式函数\nconst getWordStyle = (\n  lineIndex: number,\n  _wordIndex: number,\n  word: { text: string; startTime: number; duration: number }\n) => {\n  // 如果不是当前行，返回普通样式\n  if (lineIndex !== currentIndex.value) {\n    return {\n      color: 'var(--text-color)',\n      transition: 'color 0.3s ease',\n      backgroundImage: 'none',\n      WebkitTextFillColor: 'initial'\n    };\n  }\n\n  // 当前行的逐字效果\n  const currentTime = actualTime.value * 1000; // 转换为毫秒\n\n  // 直接使用绝对时间比较\n  const wordStartTime = word.startTime; // 单词开始的绝对时间（毫秒）\n  const wordEndTime = word.startTime + word.duration;\n\n  if (currentTime >= wordStartTime && currentTime < wordEndTime) {\n    // 当前正在播放的单词 - 使用渐变进度效果\n    const progress = Math.min((currentTime - wordStartTime) / word.duration, 1);\n    const progressPercent = Math.round(progress * 100);\n\n    return {\n      backgroundImage: `linear-gradient(to right, var(--highlight-color) 0%, var(--highlight-color) ${progressPercent}%, var(--text-color) ${progressPercent}%, var(--text-color) 100%)`,\n      backgroundClip: 'text',\n      WebkitBackgroundClip: 'text',\n      WebkitTextFillColor: 'transparent',\n      transition: 'all 0.1s ease'\n    };\n  } else if (currentTime >= wordEndTime) {\n    // 已经播放过的单词 - 纯色显示\n    return {\n      color: 'var(--highlight-color)',\n      WebkitTextFillColor: 'initial',\n      transition: 'none'\n    };\n  } else {\n    // 还未播放的单词 - 普通状态\n    return {\n      color: 'var(--text-color)',\n      WebkitTextFillColor: 'initial',\n      transition: 'none'\n    };\n  }\n};\n\n// 时间偏移量（毫秒）\nconst TIME_OFFSET = 400;\n\n// 更新动画\nconst updateProgress = () => {\n  if (!dynamicData.value.isPlay) {\n    if (animationFrameId.value) {\n      cancelAnimationFrame(animationFrameId.value);\n      animationFrameId.value = null;\n    }\n    return;\n  }\n\n  // 计算实际时间，添加偏移量\n  const timeDiff = (performance.now() - lastUpdateTime.value) / 1000;\n  actualTime.value = dynamicData.value.nowTime + timeDiff + TIME_OFFSET / 1000;\n\n  // 继续动画\n  animationFrameId.value = requestAnimationFrame(updateProgress);\n};\n\n// 记录上次更新时间\n\n// 监听据更新\nwatch(\n  () => dynamicData.value,\n  (newData: any) => {\n    // 更新最后更新时间\n    lastUpdateTime.value = performance.now();\n\n    // 更新实际时间，包含偏移量\n    actualTime.value = newData.nowTime + TIME_OFFSET / 1000;\n\n    // 如果正在播放且没有动画，启动动画\n    if (newData.isPlay && !animationFrameId.value) {\n      updateProgress();\n    }\n  },\n  { deep: true }\n);\n\n// 监听播放状态变化\nwatch(\n  () => dynamicData.value.isPlay,\n  (isPlaying: boolean) => {\n    if (isPlaying) {\n      lastUpdateTime.value = performance.now();\n      updateProgress();\n    } else if (animationFrameId.value) {\n      cancelAnimationFrame(animationFrameId.value);\n      animationFrameId.value = null;\n    }\n  }\n);\n\n// 修改数据更新处\nconst handleDataUpdate = (parsedData: {\n  type?: string;\n  nowTime: number;\n  startCurrentTime: number;\n  nextTime: number;\n  isPlay: boolean;\n  nowIndex: number;\n  lrcArray?: Array<{ text: string; trText: string }>;\n  lrcTimeArray?: number[];\n  allTime?: number;\n  playMusic?: SongResult;\n}) => {\n  // 确保数据存在且格式正确\n  if (!parsedData) {\n    console.error('Invalid update data received:', parsedData);\n    return;\n  }\n\n  // 根据数据类型处理\n  if (parsedData.type === 'update') {\n    // 增量更新，只更新动态数据\n    dynamicData.value = {\n      ...dynamicData.value,\n      nowTime: parsedData.nowTime || dynamicData.value.nowTime,\n      isPlay: typeof parsedData.isPlay === 'boolean' ? parsedData.isPlay : dynamicData.value.isPlay\n    };\n\n    // 更新索引（如果提供）\n    if (typeof parsedData.nowIndex === 'number') {\n      currentIndex.value = parsedData.nowIndex;\n    }\n    return;\n  }\n\n  // 完整更新或空歌词提示\n  // 更新静态数据\n  staticData.value = {\n    lrcArray: parsedData.lrcArray || [],\n    lrcTimeArray: parsedData.lrcTimeArray || [],\n    allTime: parsedData.allTime || 0,\n    playMusic: parsedData.playMusic || ({} as SongResult)\n  };\n\n  // 更新动态数据\n  dynamicData.value = {\n    nowTime: parsedData.nowTime || 0,\n    startCurrentTime: parsedData.startCurrentTime || 0,\n    nextTime: parsedData.nextTime || 0,\n    isPlay: parsedData.isPlay\n  };\n\n  // 更新索引\n  if (typeof parsedData.nowIndex === 'number') {\n    currentIndex.value = parsedData.nowIndex;\n  }\n};\n\nonMounted(() => {\n  // 加载保存的字体大小\n  const savedFontSize = localStorage.getItem('lyricFontSize');\n  if (savedFontSize) {\n    fontSize.value = Number(savedFontSize);\n    lineHeight.value = fontSize.value * 2.5;\n  }\n\n  // 初始化容器高度\n  updateContainerHeight();\n  window.addEventListener('resize', updateContainerHeight);\n\n  // 监听歌词数据\n  windowData.electron.ipcRenderer.on('receive-lyric', (_, data) => {\n    try {\n      const parsedData = JSON.parse(data);\n      handleDataUpdate(parsedData);\n    } catch (error) {\n      console.error('Error parsing lyric data:', error);\n    }\n  });\n});\n\nonUnmounted(() => {\n  window.removeEventListener('resize', updateContainerHeight);\n});\n\nconst checkTheme = () => {\n  if (lyricSetting.value.theme === 'light') {\n    lyricSetting.value.theme = 'dark';\n  } else {\n    lyricSetting.value.theme = 'light';\n  }\n};\n\n// 主题色相关函数\nconst toggleThemeColorPanel = () => {\n  showThemeColorPanel.value = !showThemeColorPanel.value;\n};\n\nconst handleColorChange = (color: string) => {\n  // 验证颜色有效性\n  if (!validateColor(color)) {\n    console.error('Invalid color received:', color);\n    return;\n  }\n\n  try {\n    currentHighlightColor.value = color;\n    updateThemeColorWithTransition(color);\n\n    // 更新 lyricSetting 中的 highlightColor\n    lyricSetting.value.highlightColor = color;\n\n    // 同时保存到专用的主题色存储\n    saveLyricThemeColor(color);\n  } catch (error) {\n    console.error('Failed to handle color change:', error);\n    // 恢复到默认颜色\n    const defaultColor = getCurrentLyricThemeColor(lyricSetting.value.theme);\n    currentHighlightColor.value = defaultColor;\n    updateThemeColorWithTransition(defaultColor);\n  }\n};\n\nconst handleThemeColorPanelClose = () => {\n  showThemeColorPanel.value = false;\n};\n\n// 导出重置函数以供将来使用\nconst resetThemeColor = () => {\n  // 重置到默认颜色\n  const defaultColor = getCurrentLyricThemeColor(lyricSetting.value.theme);\n\n  // 更新所有相关状态\n  currentHighlightColor.value = defaultColor;\n  lyricSetting.value.highlightColor = undefined;\n  updateThemeColorWithTransition(defaultColor);\n\n  // 清除专用存储\n  try {\n    const settings = loadLyricSettings();\n    delete settings.highlightColor;\n    saveLyricSettings(settings);\n  } catch (error) {\n    console.error('Failed to reset theme color:', error);\n  }\n};\n\n// 验证和修复颜色设置\nconst validateAndFixColorSettings = () => {\n  try {\n    // 检查当前高亮颜色是否有效\n    if (currentHighlightColor.value && !validateColor(currentHighlightColor.value)) {\n      console.warn('Current highlight color is invalid, resetting to default');\n      const defaultColor = getCurrentLyricThemeColor(lyricSetting.value.theme);\n      currentHighlightColor.value = defaultColor;\n      lyricSetting.value.highlightColor = undefined;\n      updateCSSVariable('--lyric-highlight-color', defaultColor);\n    }\n\n    // 检查 lyricSetting 中的颜色是否有效\n    if (lyricSetting.value.highlightColor && !validateColor(lyricSetting.value.highlightColor)) {\n      console.warn('Stored highlight color is invalid, removing it');\n      lyricSetting.value.highlightColor = undefined;\n    }\n  } catch (error) {\n    console.error('Failed to validate color settings:', error);\n    // 完全重置到默认状态\n    const defaultColor = getCurrentLyricThemeColor(lyricSetting.value.theme);\n    currentHighlightColor.value = defaultColor;\n    lyricSetting.value.highlightColor = undefined;\n    updateCSSVariable('--lyric-highlight-color', defaultColor);\n  }\n};\n\n// 暴露函数\ndefineExpose({\n  resetThemeColor,\n  validateAndFixColorSettings\n});\n\nconst updateCSSVariable = (name: string, value: string) => {\n  document.documentElement.style.setProperty(name, value);\n};\n\nconst updateThemeColorWithTransition = (newColor: string) => {\n  // 添加过渡类\n  const lyricWindow = document.querySelector('.lyric-window');\n  if (lyricWindow) {\n    lyricWindow.classList.add('color-transitioning');\n  }\n\n  // 更新CSS变量\n  updateCSSVariable('--lyric-highlight-color', newColor);\n\n  // 移除过渡类\n  setTimeout(() => {\n    if (lyricWindow) {\n      lyricWindow.classList.remove('color-transitioning');\n    }\n  }, 300);\n};\n\nconst initializeThemeColor = () => {\n  // 优先从 lyricSetting 中读取颜色\n  let savedColor = lyricSetting.value.highlightColor;\n\n  // 如果 lyricSetting 中没有，则从专用存储中读取\n  if (!savedColor) {\n    savedColor = loadLyricThemeColor();\n    // 如果从专用存储中读取到了颜色，同步到 lyricSetting\n    if (savedColor) {\n      lyricSetting.value.highlightColor = savedColor;\n    }\n  }\n\n  if (savedColor) {\n    const optimizedColor = getCurrentLyricThemeColor(lyricSetting.value.theme);\n    currentHighlightColor.value = optimizedColor;\n    updateCSSVariable('--lyric-highlight-color', optimizedColor);\n  } else {\n    // 如果没有保存的颜色，使用默认颜色\n    const defaultColor = getCurrentLyricThemeColor(lyricSetting.value.theme);\n    currentHighlightColor.value = defaultColor;\n    updateCSSVariable('--lyric-highlight-color', defaultColor);\n  }\n};\n\n// const handleTop = () => {\n//   lyricSetting.value.isTop = !lyricSetting.value.isTop;\n//   windowData.electron.ipcRenderer.send('top-lyric', lyricSetting.value.isTop);\n// };\n\nconst handleLock = () => {\n  lyricSetting.value.isLock = !lyricSetting.value.isLock;\n  windowData.electron.ipcRenderer.send('set-ignore-mouse', lyricSetting.value.isLock);\n};\n\nconst handleClose = () => {\n  windowData.electron.ipcRenderer.send('close-lyric');\n};\n\n// 安全保存歌词设置\nconst saveLyricSettings = (settings: typeof lyricSetting.value) => {\n  try {\n    localStorage.setItem('lyricData', JSON.stringify(settings));\n  } catch (error) {\n    console.error('Failed to save lyric settings:', error);\n  }\n};\n\nwatch(\n  () => lyricSetting.value,\n  (newValue) => {\n    saveLyricSettings(newValue);\n  },\n  { deep: true }\n);\n\n// 监听主题切换，自动调整颜色\nwatch(\n  () => lyricSetting.value.theme,\n  (newTheme) => {\n    if (currentHighlightColor.value) {\n      const optimizedColor = getCurrentLyricThemeColor(newTheme);\n      currentHighlightColor.value = optimizedColor;\n      updateThemeColorWithTransition(optimizedColor);\n    }\n  }\n);\n\n// 添加拖动相关变量\nconst isDragging = ref(false);\nconst startPosition = ref({ x: 0, y: 0 });\nconst lastMoveTime = ref(0);\nconst moveThrottleMs = 10; // 限制拖动事件发送频率，提高性能\n\n// 处理鼠标按下事件\nconst handleMouseDown = (e: MouseEvent) => {\n  // 如果点击的是控制按钮区域或窗口被锁定，不处理拖动\n  if (\n    lyricSetting.value.isLock ||\n    (e.target as HTMLElement).closest('.control-buttons') ||\n    (e.target as HTMLElement).closest('.font-size-controls') ||\n    (e.target as HTMLElement).closest('.play-controls')\n  ) {\n    return;\n  }\n\n  // 只响应鼠标左键\n  if (e.button !== 0) return;\n\n  isDragging.value = true;\n  startPosition.value = { x: e.screenX, y: e.screenY };\n  lastMoveTime.value = performance.now();\n\n  // 发送拖动开始信号到主进程\n  windowData.electron.ipcRenderer.send('lyric-drag-start');\n\n  // 添加全局鼠标事件监听\n  const handleMouseMove = (e: MouseEvent) => {\n    if (!isDragging.value) return;\n\n    // 时间节流，避免过于频繁的更新\n    const now = performance.now();\n    if (now - lastMoveTime.value < moveThrottleMs) return;\n    lastMoveTime.value = now;\n\n    const deltaX = e.screenX - startPosition.value.x;\n    const deltaY = e.screenY - startPosition.value.y;\n\n    // 只有在实际移动时才发送事件\n    if (Math.abs(deltaX) > 0 || Math.abs(deltaY) > 0) {\n      // 发送移动事件到主进程\n      windowData.electron.ipcRenderer.send('lyric-drag-move', { deltaX, deltaY });\n      startPosition.value = { x: e.screenX, y: e.screenY };\n    }\n  };\n\n  const handleMouseUp = () => {\n    if (!isDragging.value) return;\n    isDragging.value = false;\n\n    // 发送拖动结束信号到主进程\n    windowData.electron.ipcRenderer.send('lyric-drag-end');\n\n    // 移除事件监听\n    document.removeEventListener('mousemove', handleMouseMove);\n    document.removeEventListener('mouseup', handleMouseUp);\n  };\n\n  // 添加全局事件监听\n  document.addEventListener('mousemove', handleMouseMove);\n  document.addEventListener('mouseup', handleMouseUp);\n};\n\n// 组件卸载时清理\nonUnmounted(() => {\n  isDragging.value = false;\n});\n\nonMounted(() => {\n  const lyricLock = document.getElementById('lyric-lock');\n  if (lyricLock) {\n    lyricLock.onmouseenter = () => {\n      if (lyricSetting.value.isLock) {\n        windowData.electron.ipcRenderer.send('set-ignore-mouse', false);\n      }\n    };\n    lyricLock.onmouseleave = () => {\n      if (lyricSetting.value.isLock) {\n        windowData.electron.ipcRenderer.send('set-ignore-mouse', true);\n      }\n    };\n  }\n\n  // 初始化主题色\n  initializeThemeColor();\n\n  // 验证和修复颜色设置\n  validateAndFixColorSettings();\n});\n\n// 添加播放控制相关的函数\nconst handlePlayPause = () => {\n  windowData.electron.ipcRenderer.send('control-back', 'playpause');\n};\n\nconst handlePrev = () => {\n  windowData.electron.ipcRenderer.send('control-back', 'prev');\n};\n\nconst handleNext = () => {\n  windowData.electron.ipcRenderer.send('control-back', 'next');\n};\n</script>\n\n<style scoped>\nhtml,\nbody,\n#app {\n  background-color: transparent !important;\n  box-shadow: none !important;\n  border: none !important;\n}\n</style>\n\n<style lang=\"scss\" scoped>\n.lyric-window {\n  width: 100vw;\n  height: 100vh;\n  position: relative;\n  overflow: hidden;\n  background: transparent !important;\n  user-select: none;\n  transition: background-color 0.3s ease;\n  cursor: default;\n  border-radius: 14px;\n\n  &.color-transitioning {\n    .lyric-text-inner {\n      transition: background 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;\n    }\n\n    .control-button {\n      i {\n        transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1) !important;\n      }\n    }\n  }\n\n  &:hover {\n    .control-bar {\n      &-show {\n        opacity: 1;\n        visibility: visible;\n      }\n    }\n  }\n\n  &:active {\n    cursor: grabbing;\n  }\n\n  &.dark {\n    --text-color: #e6e6e6;\n    --text-secondary: #ffffffea;\n    --highlight-color: var(--lyric-highlight-color, #1ed760);\n    --control-bg: rgba(124, 124, 124, 0.3);\n    &:hover:not(.lyric_lock) {\n      background: rgba(44, 44, 44, 0.466) !important;\n    }\n  }\n\n  &.light {\n    --text-color: #383838;\n    --text-secondary: #282828ae;\n    --highlight-color: var(--lyric-highlight-color, #1db954);\n    --control-bg: rgba(38, 38, 38, 0.532);\n    &:hover:not(.lyric_lock) {\n      background: rgba(0, 0, 0, 0.434) !important;\n    }\n  }\n}\n\n.control-bar {\n  position: absolute;\n  top: 10px;\n  left: 0;\n  right: 0;\n  height: 80px;\n  display: flex;\n  justify-content: space-between;\n  align-items: start;\n  padding: 0 20px;\n  opacity: 0;\n  visibility: hidden;\n  transition:\n    opacity 0.2s ease,\n    visibility 0.2s ease;\n  z-index: 100;\n\n  .font-size-controls {\n    -webkit-app-region: no-drag;\n    color: var(--text-color);\n    display: flex;\n    align-items: center;\n    gap: 16px;\n  }\n\n  .play-controls {\n    position: absolute;\n    top: 0px;\n    left: 50%;\n    transform: translateX(-50%);\n    display: flex;\n    align-items: center;\n    gap: 16px;\n    -webkit-app-region: no-drag;\n\n    .play-button {\n      width: 36px;\n      height: 36px;\n      i {\n        font-size: 24px;\n      }\n    }\n  }\n\n  .control-buttons {\n    -webkit-app-region: no-drag;\n  }\n}\n\n.control-buttons {\n  display: flex;\n  gap: 16px;\n  -webkit-app-region: no-drag;\n}\n\n.control-button {\n  width: 36px;\n  height: 36px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n  border-radius: 8px;\n  color: var(--text-color);\n  transition: all 0.2s ease;\n  &:hover {\n    background: var(--control-bg);\n  }\n\n  i {\n    font-size: 20px;\n    text-shadow: 0 0 10px rgba(0, 0, 0, 0.3);\n\n    &.active {\n      color: var(--highlight-color);\n    }\n  }\n\n  &.theme-color-button {\n    &.active {\n      background: var(--control-bg);\n\n      i {\n        color: var(--highlight-color);\n      }\n    }\n  }\n}\n\n.lyric-container {\n  position: absolute;\n  top: 80px;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  overflow: hidden;\n  z-index: 100;\n}\n\n.lyric-scroll {\n  height: 100%;\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n  justify-content: flex-start;\n  align-items: center;\n  mask-image: linear-gradient(to bottom, transparent 0%, black 20%, black 80%, transparent 100%);\n}\n\n.lyric-wrapper {\n  will-change: transform;\n  padding: 20vh 0;\n  transform-origin: center center;\n  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.lyric-line {\n  padding: 4px 20px;\n  text-align: center;\n  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n\n  &.lyric-line-current {\n    transform: scale(1.05);\n    opacity: 1;\n\n    // 当前播放歌词的特殊样式\n    .lyric-text {\n      // 移除阴影，避免干扰渐变效果\n      text-shadow: none;\n\n      .lyric-text-inner {\n        // 为渐变文字添加轻微的外发光\n        filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.5));\n        // 确保渐变效果清晰\n        -webkit-font-smoothing: antialiased;\n      }\n    }\n  }\n\n  &.lyric-line-passed {\n    opacity: 0.6;\n  }\n}\n\n.lyric-text {\n  font-weight: 600;\n  margin-bottom: 2px;\n  color: var(--text-color);\n  white-space: pre-wrap;\n  word-break: break-all;\n  transition: transform 0.2s ease;\n  line-height: 1.4;\n  // 优化字体渲染\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n\n  // 为非当前播放的歌词添加阴影效果\n  text-shadow:\n    0 0 2px rgba(0, 0, 0, 0.8),\n    0 1px 1px rgba(0, 0, 0, 0.6),\n    0 0 4px rgba(255, 255, 255, 0.2);\n\n  .lyric-text-inner {\n    transition: background 0.3s ease;\n  }\n\n  // 逐字歌词样式\n  .word-by-word-lyric {\n    display: inline-block;\n    text-align: center;\n\n    .lyric-word {\n      display: inline-block;\n      font-weight: inherit;\n      font-size: inherit;\n      letter-spacing: inherit;\n      line-height: inherit;\n      position: relative;\n      text-rendering: optimizeLegibility;\n      -webkit-font-smoothing: antialiased;\n      -moz-osx-font-smoothing: grayscale;\n    }\n  }\n}\n\n.lyric-translation {\n  color: var(--text-secondary);\n  white-space: pre-wrap;\n  word-break: break-all;\n  transition: font-size 0.2s ease;\n  line-height: 1.4;\n\n  // 为翻译文本也添加阴影效果，但稍微轻一些\n  text-shadow:\n    0 0 2px rgba(0, 0, 0, 0.7),\n    0 1px 1px rgba(0, 0, 0, 0.5),\n    0 0 4px rgba(255, 255, 255, 0.2),\n    1px 1px 1px rgba(0, 0, 0, 0.4),\n    -1px -1px 1px rgba(0, 0, 0, 0.4);\n}\n\n.lyric-empty {\n  text-align: center;\n  color: var(--text-secondary);\n  font-size: 16px;\n  padding: 20px;\n\n  // 为空歌词提示也添加阴影效果\n  text-shadow:\n    0 0 2px rgba(0, 0, 0, 0.7),\n    0 1px 1px rgba(0, 0, 0, 0.5),\n    0 0 4px rgba(255, 255, 255, 0.2);\n}\n\nbody {\n  background-color: transparent !important;\n  margin: 0;\n}\n\n.lyric-content {\n  transition: font-size 0.2s ease;\n}\n\n.lyric-line-current {\n  opacity: 1;\n}\n\n.control-bar {\n  .control-buttons {\n    .control-button {\n      &:not(:has(.ri-lock-line)):not(:has(.ri-lock-unlock-line)) {\n        .lyric_lock & {\n          display: none;\n        }\n      }\n    }\n  }\n\n  .lyric_lock & .font-size-controls {\n    display: none;\n  }\n\n  .lyric_lock & .play-controls {\n    display: none;\n  }\n}\n\n.lyric_lock {\n  background: transparent;\n  &:hover {\n    background: transparent;\n  }\n\n  #lyric-lock {\n    position: absolute;\n    top: 0;\n    right: 72px;\n    background: var(--control-bg);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/mobile-search/index.vue",
    "content": "<template>\n  <div class=\"mobile-search-page\">\n    <!-- 搜索头部 -->\n    <div class=\"search-header\" :class=\"{ 'safe-area-top': hasSafeArea }\">\n      <div class=\"header-back\" @click=\"goBack\">\n        <i class=\"ri-arrow-left-s-line\"></i>\n      </div>\n      <div class=\"search-input-wrapper\">\n        <i class=\"ri-search-line search-icon\"></i>\n        <input\n          ref=\"searchInputRef\"\n          v-model=\"searchValue\"\n          type=\"text\"\n          class=\"search-input\"\n          :placeholder=\"hotSearchKeyword\"\n          @input=\"handleInput\"\n          @keydown.enter=\"handleSearch\"\n        />\n        <i v-if=\"searchValue\" class=\"ri-close-circle-fill clear-icon\" @click=\"clearSearch\"></i>\n      </div>\n      <div class=\"search-button\" @click=\"handleSearch\">\n        {{ t('common.search') }}\n      </div>\n    </div>\n\n    <!-- 搜索类型标签 -->\n    <div class=\"search-types\">\n      <div\n        v-for=\"type in searchTypes\"\n        :key=\"type.key\"\n        class=\"type-tag\"\n        :class=\"{ active: searchType === type.key }\"\n        @click=\"selectType(type.key)\"\n      >\n        {{ type.label }}\n      </div>\n    </div>\n\n    <!-- 搜索内容区域 -->\n    <div class=\"search-content\">\n      <!-- 搜索建议 -->\n      <div v-if=\"suggestions.length > 0\" class=\"search-section\">\n        <div class=\"section-title\">{{ t('search.suggestions') }}</div>\n        <div class=\"suggestion-list\">\n          <div\n            v-for=\"(item, index) in suggestions\"\n            :key=\"index\"\n            class=\"suggestion-item\"\n            @click=\"selectSuggestion(item)\"\n          >\n            <i class=\"ri-search-line\"></i>\n            <span>{{ item }}</span>\n          </div>\n        </div>\n      </div>\n\n      <!-- 搜索历史 -->\n      <div v-else-if=\"searchHistory.length > 0\" class=\"search-section\">\n        <div class=\"section-header\">\n          <span class=\"section-title\">{{ t('search.history') }}</span>\n          <span class=\"clear-history\" @click=\"clearHistory\">{{ t('common.clear') }}</span>\n        </div>\n        <div class=\"history-tags\">\n          <div\n            v-for=\"(item, index) in searchHistory\"\n            :key=\"index\"\n            class=\"history-tag\"\n            @click=\"selectSuggestion(item)\"\n          >\n            {{ item }}\n          </div>\n        </div>\n      </div>\n\n      <!-- 热门搜索 -->\n      <div v-if=\"hotSearchList.length > 0 && !searchValue\" class=\"search-section\">\n        <div class=\"section-title\">{{ t('search.hot') }}</div>\n        <div class=\"hot-list\">\n          <div\n            v-for=\"(item, index) in hotSearchList\"\n            :key=\"index\"\n            class=\"hot-item\"\n            @click=\"selectSuggestion(item.searchWord)\"\n          >\n            <span class=\"hot-rank\" :class=\"{ top: index < 3 }\">{{ index + 1 }}</span>\n            <span class=\"hot-word\">{{ item.searchWord }}</span>\n            <span v-if=\"item.iconUrl\" class=\"hot-icon\">\n              <img :src=\"item.iconUrl\" alt=\"\" />\n            </span>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useDebounceFn } from '@vueuse/core';\nimport { computed, inject, nextTick, onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\n\nimport { getHotSearch, getSearchKeyword } from '@/api/home';\nimport { getSearchSuggestions } from '@/api/search';\nimport { SEARCH_TYPES } from '@/const/bar-const';\nimport { useSearchStore } from '@/store/modules/search';\n\nconst { t, locale } = useI18n();\nconst router = useRouter();\nconst searchStore = useSearchStore();\n\n// 注入是否有安全区域\nconst hasSafeArea = inject('hasSafeArea', false);\n\n// 搜索值\nconst searchValue = ref('');\nconst searchInputRef = ref<HTMLInputElement | null>(null);\n\n// 热门搜索关键词占位符\nconst hotSearchKeyword = ref('搜索音乐、歌手、歌单');\n\n// 搜索类型\nconst searchType = ref(searchStore.searchType || 1);\nconst searchTypes = computed(() => {\n  locale.value;\n  return SEARCH_TYPES.map((type) => ({\n    label: t(type.label),\n    key: type.key\n  }));\n});\n\n// 搜索建议\nconst suggestions = ref<string[]>([]);\n\n// 搜索历史\nconst HISTORY_KEY = 'mobile_search_history';\nconst searchHistory = ref<string[]>([]);\n\n// 热门搜索\nconst hotSearchList = ref<any[]>([]);\n\n// 加载热门搜索关键词\nconst loadHotSearchKeyword = async () => {\n  try {\n    const { data } = await getSearchKeyword();\n    hotSearchKeyword.value = data.data.showKeyword;\n  } catch (e) {\n    console.error('加载热门搜索关键词失败:', e);\n  }\n};\n\n// 加载热门搜索列表\nconst loadHotSearchList = async () => {\n  try {\n    const { data } = await getHotSearch();\n    hotSearchList.value = data.data || [];\n  } catch (e) {\n    console.error('加载热门搜索失败:', e);\n  }\n};\n\n// 加载搜索历史\nconst loadSearchHistory = () => {\n  try {\n    const history = localStorage.getItem(HISTORY_KEY);\n    searchHistory.value = history ? JSON.parse(history) : [];\n  } catch (e) {\n    console.error('加载搜索历史失败:', e);\n    searchHistory.value = [];\n  }\n};\n\n// 保存搜索历史\nconst saveSearchHistory = (keyword: string) => {\n  if (!keyword.trim()) return;\n\n  // 移除重复项并添加到开头\n  const history = searchHistory.value.filter((item) => item !== keyword);\n  history.unshift(keyword);\n\n  // 最多保存20条\n  searchHistory.value = history.slice(0, 20);\n  localStorage.setItem(HISTORY_KEY, JSON.stringify(searchHistory.value));\n};\n\n// 清除搜索历史\nconst clearHistory = () => {\n  searchHistory.value = [];\n  localStorage.removeItem(HISTORY_KEY);\n};\n\n// 获取搜索建议（防抖）\nconst debouncedGetSuggestions = useDebounceFn(async (keyword: string) => {\n  if (!keyword.trim()) {\n    suggestions.value = [];\n    return;\n  }\n  suggestions.value = await getSearchSuggestions(keyword);\n}, 300);\n\n// 处理输入\nconst handleInput = () => {\n  debouncedGetSuggestions(searchValue.value);\n};\n\n// 清除搜索\nconst clearSearch = () => {\n  searchValue.value = '';\n  suggestions.value = [];\n};\n\n// 选择搜索类型\nconst selectType = (type: number) => {\n  searchType.value = type;\n  searchStore.searchType = type;\n};\n\n// 选择建议\nconst selectSuggestion = (keyword: string) => {\n  searchValue.value = keyword;\n  handleSearch();\n};\n\n// 执行搜索\nconst handleSearch = () => {\n  const keyword = searchValue.value.trim();\n  if (!keyword) return;\n\n  // 保存搜索历史\n  saveSearchHistory(keyword);\n\n  // 跳转到搜索结果页\n  router.push({\n    path: '/mobile-search-result',\n    query: {\n      keyword,\n      type: searchType.value\n    }\n  });\n};\n\n// 返回上一页\nconst goBack = () => {\n  router.back();\n};\n\nonMounted(() => {\n  loadHotSearchKeyword();\n  loadHotSearchList();\n  loadSearchHistory();\n  nextTick(() => {\n    searchInputRef.value?.focus();\n  });\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.mobile-search-page {\n  @apply fixed inset-0 z-50;\n  @apply bg-light dark:bg-black;\n  @apply flex flex-col;\n}\n\n.search-header {\n  @apply flex items-center gap-3 pl-1 pr-3 py-3;\n  @apply border-b border-gray-100 dark:border-gray-800;\n\n  &.safe-area-top {\n    padding-top: calc(var(--safe-area-inset-top, 0px) + 12px);\n  }\n}\n\n.header-back {\n  @apply flex items-center justify-center;\n  @apply w-8 h-8 rounded-full text-2xl;\n  @apply text-gray-600 dark:text-gray-300;\n  @apply active:bg-gray-100 dark:active:bg-gray-800;\n}\n\n.search-input-wrapper {\n  @apply flex-1 flex items-center gap-2;\n  @apply bg-gray-100 dark:bg-gray-800 rounded-full;\n  @apply px-4 py-1;\n}\n\n.search-icon {\n  @apply text-gray-400 text-lg;\n}\n\n.search-input {\n  @apply flex-1 bg-transparent border-none outline-none;\n  @apply text-gray-900 dark:text-white text-base;\n\n  &::placeholder {\n    @apply text-gray-400;\n  }\n}\n\n.clear-icon {\n  @apply text-gray-400 text-lg cursor-pointer;\n}\n\n.search-types {\n  @apply flex gap-2 px-4 py-3 overflow-x-auto;\n  @apply border-b border-gray-100 dark:border-gray-800;\n\n  &::-webkit-scrollbar {\n    display: none;\n  }\n}\n\n.type-tag {\n  @apply px-4 py-1.5 rounded-full text-sm whitespace-nowrap;\n  @apply bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300;\n  @apply transition-colors duration-200;\n\n  &.active {\n    @apply bg-green-500 text-white;\n  }\n}\n\n.search-content {\n  @apply flex-1 overflow-y-auto px-4 py-3;\n}\n\n.search-section {\n  @apply mb-6;\n}\n\n.section-header {\n  @apply flex items-center justify-between mb-3;\n}\n\n.section-title {\n  @apply text-sm font-medium text-gray-500 dark:text-gray-400 mb-3;\n}\n\n.clear-history {\n  @apply text-sm text-gray-400 dark:text-gray-500;\n}\n\n.suggestion-list {\n  @apply space-y-1;\n}\n\n.suggestion-item {\n  @apply flex items-center gap-3 py-3;\n  @apply text-gray-700 dark:text-gray-200;\n  @apply active:bg-gray-50 dark:active:bg-gray-800;\n\n  i {\n    @apply text-gray-400;\n  }\n}\n\n.history-tags {\n  @apply flex flex-wrap gap-2;\n}\n\n.history-tag {\n  @apply px-3 py-1.5 rounded-full text-sm;\n  @apply bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300;\n  @apply active:bg-gray-200 dark:active:bg-gray-700;\n}\n\n.hot-list {\n  @apply space-y-1;\n}\n\n.hot-item {\n  @apply flex items-center gap-3 py-2.5;\n  @apply active:bg-gray-50 dark:active:bg-gray-800;\n}\n\n.hot-rank {\n  @apply w-5 text-center text-sm font-medium text-gray-400;\n\n  &.top {\n    @apply text-red-500;\n  }\n}\n\n.hot-word {\n  @apply flex-1 text-gray-700 dark:text-gray-200;\n}\n\n.hot-icon {\n  img {\n    @apply h-4;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/mobile-search-result/index.vue",
    "content": "<template>\n  <div class=\"mobile-search-result\">\n    <!-- 搜索结果头部 -->\n    <div class=\"result-header\" :class=\"{ 'safe-area-top': hasSafeArea }\">\n      <div class=\"header-back\" @click=\"goBack\">\n        <i class=\"ri-arrow-left-s-line\"></i>\n      </div>\n      <div class=\"header-keyword\">{{ keyword }}</div>\n      <div class=\"header-actions\">\n        <div class=\"action-btn\" @click=\"openSearch\">\n          <i class=\"ri-search-line\"></i>\n        </div>\n      </div>\n    </div>\n\n    <!-- 搜索类型标签 -->\n    <div class=\"search-types\">\n      <div\n        v-for=\"type in searchTypes\"\n        :key=\"type.key\"\n        class=\"type-tag\"\n        :class=\"{ active: searchType === type.key }\"\n        @click=\"selectType(type.key)\"\n      >\n        {{ type.label }}\n      </div>\n    </div>\n\n    <!-- 搜索结果列表 -->\n    <div class=\"result-content\" @scroll=\"handleScroll\">\n      <!-- 加载中 -->\n      <div v-if=\"loading && !results.length\" class=\"loading-state\">\n        <n-spin size=\"medium\" />\n        <span class=\"ml-2\">{{ t('search.loading.searching') }}</span>\n      </div>\n\n      <!-- 搜索结果 -->\n      <div v-else-if=\"results.length\" class=\"result-list\">\n        <!-- B站视频 -->\n        <template v-if=\"searchType === SEARCH_TYPE.BILIBILI\">\n          <bilibili-item\n            v-for=\"item in results\"\n            :key=\"item.bvid\"\n            :item=\"item\"\n            @play=\"handlePlayBilibili\"\n          />\n        </template>\n\n        <!-- 歌曲搜索 -->\n        <template v-else-if=\"searchType === SEARCH_TYPE.MUSIC\">\n          <song-item\n            v-for=\"item in results\"\n            :key=\"item.id\"\n            :item=\"item\"\n            :is-next=\"true\"\n            @play=\"handlePlay\"\n          />\n        </template>\n\n        <!-- 专辑/歌单/MV 搜索 -->\n        <template v-else>\n          <search-item v-for=\"item in results\" :key=\"item.id\" :item=\"item\" class=\"mb-3\" />\n        </template>\n\n        <!-- 加载更多 -->\n        <div v-if=\"isLoadingMore\" class=\"loading-more\">\n          <n-spin size=\"small\" />\n          <span class=\"ml-2\">{{ t('search.loading.more') }}</span>\n        </div>\n\n        <!-- 没有更多 -->\n        <div v-if=\"!hasMore && results.length\" class=\"no-more\">\n          {{ t('search.noMore') }}\n        </div>\n      </div>\n\n      <!-- 无结果 -->\n      <div v-else-if=\"!loading\" class=\"empty-state\">\n        <i class=\"ri-search-line\"></i>\n        <span>{{ t('search.noResult') }}</span>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, inject, onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport {\n  createSimpleBilibiliSong,\n  getBilibiliAudioUrl,\n  getBilibiliProxyUrl,\n  getBilibiliVideoDetail,\n  searchBilibili\n} from '@/api/bilibili';\nimport { getSearch } from '@/api/search';\nimport BilibiliItem from '@/components/common/BilibiliItem.vue';\nimport SearchItem from '@/components/common/SearchItem.vue';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { SEARCH_TYPE, SEARCH_TYPES } from '@/const/bar-const';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { useSearchStore } from '@/store/modules/search';\nimport type { IBilibiliSearchResult } from '@/types/bilibili';\n\nconst { t, locale } = useI18n();\nconst route = useRoute();\nconst router = useRouter();\nconst playerStore = usePlayerStore();\nconst searchStore = useSearchStore();\n\n// 注入是否有安全区域\nconst hasSafeArea = inject('hasSafeArea', false);\n\n// 搜索关键词\nconst keyword = ref((route.query.keyword as string) || '');\n\n// 搜索类型\nconst searchType = ref(Number(route.query.type) || searchStore.searchType || 1);\nconst searchTypes = computed(() => {\n  locale.value;\n  return SEARCH_TYPES.map((type) => ({\n    label: t(type.label),\n    key: type.key\n  }));\n});\n\n// 搜索结果\nconst results = ref<any[]>([]);\nconst loading = ref(false);\n\n// 分页\nconst ITEMS_PER_PAGE = 30;\nconst page = ref(1);\nconst hasMore = ref(true);\nconst isLoadingMore = ref(false);\n\n// 执行搜索\nconst performSearch = async (isLoadMore = false) => {\n  if (!keyword.value) return;\n\n  if (isLoadMore) {\n    if (!hasMore.value || isLoadingMore.value) return;\n    isLoadingMore.value = true;\n  } else {\n    loading.value = true;\n    results.value = [];\n    page.value = 1;\n    hasMore.value = true;\n  }\n\n  try {\n    // B站搜索\n    if (searchType.value === SEARCH_TYPE.BILIBILI) {\n      const response = await searchBilibili({\n        keyword: keyword.value,\n        page: page.value,\n        pagesize: ITEMS_PER_PAGE\n      });\n\n      const bilibiliVideos = response.data.data.result.map((item: any) => ({\n        id: item.aid,\n        bvid: item.bvid,\n        title: item.title,\n        author: item.author,\n        pic: getBilibiliProxyUrl(item.pic),\n        duration: item.duration,\n        pubdate: item.pubdate,\n        description: item.description,\n        view: item.play,\n        danmaku: item.video_review\n      }));\n\n      if (isLoadMore) {\n        results.value = [...results.value, ...bilibiliVideos];\n      } else {\n        results.value = bilibiliVideos;\n      }\n\n      hasMore.value = bilibiliVideos.length === ITEMS_PER_PAGE;\n    }\n    // 歌曲搜索\n    else if (searchType.value === SEARCH_TYPE.MUSIC) {\n      const { data } = await getSearch({\n        keywords: keyword.value,\n        type: searchType.value,\n        limit: ITEMS_PER_PAGE,\n        offset: (page.value - 1) * ITEMS_PER_PAGE\n      });\n\n      const songs = (data.result.songs || []).map((item: any) => ({\n        ...item,\n        picUrl: item.al?.picUrl,\n        artists: item.ar\n      }));\n\n      if (isLoadMore) {\n        results.value = [...results.value, ...songs];\n      } else {\n        results.value = songs;\n      }\n\n      hasMore.value = songs.length === ITEMS_PER_PAGE;\n    }\n    // 专辑搜索\n    else if (searchType.value === SEARCH_TYPE.ALBUM) {\n      const { data } = await getSearch({\n        keywords: keyword.value,\n        type: searchType.value,\n        limit: ITEMS_PER_PAGE,\n        offset: (page.value - 1) * ITEMS_PER_PAGE\n      });\n\n      const albums = (data.result.albums || []).map((item: any) => ({\n        ...item,\n        desc: `${item.artist?.name || ''} ${item.company || ''}`,\n        type: 'album'\n      }));\n\n      if (isLoadMore) {\n        results.value = [...results.value, ...albums];\n      } else {\n        results.value = albums;\n      }\n\n      hasMore.value = albums.length === ITEMS_PER_PAGE;\n    }\n    // 歌单搜索\n    else if (searchType.value === SEARCH_TYPE.PLAYLIST) {\n      const { data } = await getSearch({\n        keywords: keyword.value,\n        type: searchType.value,\n        limit: ITEMS_PER_PAGE,\n        offset: (page.value - 1) * ITEMS_PER_PAGE\n      });\n\n      const playlists = (data.result.playlists || []).map((item: any) => ({\n        ...item,\n        picUrl: item.coverImgUrl,\n        playCount: item.playCount,\n        desc: item.creator?.nickname || '',\n        type: 'playlist'\n      }));\n\n      if (isLoadMore) {\n        results.value = [...results.value, ...playlists];\n      } else {\n        results.value = playlists;\n      }\n\n      hasMore.value = playlists.length === ITEMS_PER_PAGE;\n    }\n    // MV 搜索\n    else if (searchType.value === SEARCH_TYPE.MV) {\n      const { data } = await getSearch({\n        keywords: keyword.value,\n        type: searchType.value,\n        limit: ITEMS_PER_PAGE,\n        offset: (page.value - 1) * ITEMS_PER_PAGE\n      });\n\n      const mvs = (data.result.mvs || []).map((item: any) => ({\n        ...item,\n        picUrl: item.cover,\n        playCount: item.playCount,\n        desc: item.artists?.map((artist: any) => artist.name).join('/') || '',\n        type: 'mv'\n      }));\n\n      if (isLoadMore) {\n        results.value = [...results.value, ...mvs];\n      } else {\n        results.value = mvs;\n      }\n\n      hasMore.value = mvs.length === ITEMS_PER_PAGE;\n    }\n\n    page.value++;\n  } catch (error) {\n    console.error('搜索失败:', error);\n  } finally {\n    loading.value = false;\n    isLoadingMore.value = false;\n  }\n};\n\n// 选择搜索类型\nconst selectType = (type: number) => {\n  if (searchType.value === type) return;\n\n  searchType.value = type;\n  searchStore.searchType = type;\n\n  // 更新路由查询参数\n  router.replace({\n    query: {\n      ...route.query,\n      type: type.toString()\n    }\n  });\n\n  performSearch();\n};\n\n// 滚动加载更多\nconst handleScroll = (e: Event) => {\n  const target = e.target as HTMLElement;\n  const { scrollTop, scrollHeight, clientHeight } = target;\n\n  if (scrollTop + clientHeight >= scrollHeight - 100) {\n    performSearch(true);\n  }\n};\n\n// 播放音乐\nconst handlePlay = (item: any) => {\n  playerStore.addToNextPlay(item);\n};\n\n// 播放B站视频\nconst handlePlayBilibili = async (item: IBilibiliSearchResult) => {\n  try {\n    const videoDetail = await getBilibiliVideoDetail(item.bvid);\n    const pages = videoDetail.data.pages;\n\n    if (pages && pages.length === 1) {\n      const audioUrl = await getBilibiliAudioUrl(item.bvid, pages[0].cid);\n      const playItem = createSimpleBilibiliSong(item, audioUrl);\n      playItem.bilibiliData = {\n        bvid: item.bvid,\n        cid: pages[0].cid\n      };\n      playerStore.setPlay(playItem);\n    } else {\n      router.push(`/bilibili/${item.bvid}`);\n    }\n  } catch (error) {\n    console.error('播放B站视频失败:', error);\n    router.push(`/bilibili/${item.bvid}`);\n  }\n};\n\n// 返回\nconst goBack = () => {\n  router.back();\n};\n\n// 打开搜索\nconst openSearch = () => {\n  router.push('/mobile-search');\n};\n\n// 监听路由变化\nwatch(\n  () => route.query,\n  (query) => {\n    if (route.path === '/mobile-search-result' && query.keyword) {\n      keyword.value = query.keyword as string;\n      searchType.value = Number(query.type) || searchStore.searchType || 1;\n      performSearch();\n    }\n  }\n);\n\nonMounted(() => {\n  if (keyword.value) {\n    performSearch();\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.mobile-search-result {\n  @apply fixed inset-0;\n  @apply bg-light dark:bg-black;\n  @apply flex flex-col;\n}\n\n.result-header {\n  @apply flex items-center gap-3 px-4 py-3;\n  @apply border-b border-gray-100 dark:border-gray-800;\n\n  &.safe-area-top {\n    padding-top: calc(var(--safe-area-inset-top, 0px) + 12px);\n  }\n}\n\n.header-back {\n  @apply flex items-center justify-center;\n  @apply w-10 h-10 rounded-full text-xl;\n  @apply text-gray-600 dark:text-gray-300;\n  @apply active:bg-gray-100 dark:active:bg-gray-800;\n}\n\n.header-keyword {\n  @apply flex-1 text-base font-medium;\n  @apply text-gray-900 dark:text-white;\n  @apply truncate;\n}\n\n.header-actions {\n  @apply flex items-center gap-2;\n}\n\n.action-btn {\n  @apply flex items-center justify-center;\n  @apply w-10 h-10 rounded-full text-xl;\n  @apply text-gray-600 dark:text-gray-300;\n  @apply active:bg-gray-100 dark:active:bg-gray-800;\n}\n\n.search-types {\n  @apply flex gap-2 px-4 py-3 overflow-x-auto;\n  @apply border-b border-gray-100 dark:border-gray-800;\n\n  &::-webkit-scrollbar {\n    display: none;\n  }\n}\n\n.type-tag {\n  @apply px-4 py-1.5 rounded-full text-sm whitespace-nowrap;\n  @apply bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300;\n  @apply transition-colors duration-200;\n\n  &.active {\n    @apply bg-green-500 text-white;\n  }\n}\n\n.result-content {\n  @apply flex-1 overflow-y-auto;\n}\n\n.loading-state {\n  @apply flex flex-col items-center justify-center py-20;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.result-list {\n  @apply pb-20;\n}\n\n.loading-more {\n  @apply flex justify-center items-center py-4;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.no-more {\n  @apply text-center py-4;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.empty-state {\n  @apply flex flex-col items-center justify-center py-20;\n  @apply text-gray-400 dark:text-gray-500;\n\n  i {\n    @apply text-6xl mb-4;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/music/HistoryRecommend.vue",
    "content": "<template>\n  <div class=\"history-recommend-page\">\n    <!-- 头部标题和操作按钮 -->\n    <div class=\"music-header h-12 flex items-center justify-between\">\n      <n-ellipsis :line-clamp=\"1\" class=\"flex-shrink-0 mr-3\">\n        <div class=\"music-title\">\n          {{ t('comp.musicList.historyRecommend') }}\n        </div>\n      </n-ellipsis>\n\n      <!-- 操作按钮组 -->\n      <div class=\"flex-grow flex-1 flex items-center justify-end gap-2\">\n        <n-tooltip placement=\"bottom\" trigger=\"hover\">\n          <template #trigger>\n            <div class=\"action-button hover-green\" @click=\"handlePlayAll\">\n              <i class=\"icon iconfont ri-play-fill\"></i>\n            </div>\n          </template>\n          {{ t('comp.musicList.playAll') }}\n        </n-tooltip>\n\n        <n-tooltip placement=\"bottom\" trigger=\"hover\">\n          <template #trigger>\n            <div class=\"action-button hover-green\" @click=\"addToPlaylist\">\n              <i class=\"icon iconfont ri-add-line\"></i>\n            </div>\n          </template>\n          {{ t('comp.musicList.addToPlaylist') }}\n        </n-tooltip>\n\n        <!-- 布局切换按钮 -->\n        <div class=\"layout-toggle\" v-if=\"!isMobile\">\n          <n-tooltip placement=\"bottom\" trigger=\"hover\">\n            <template #trigger>\n              <div class=\"toggle-button hover-green\" @click=\"toggleLayout\">\n                <i\n                  class=\"icon iconfont\"\n                  :class=\"isCompactLayout ? 'ri-list-check-2' : 'ri-grid-line'\"\n                ></i>\n              </div>\n            </template>\n            {{\n              isCompactLayout\n                ? t('comp.musicList.switchToNormal')\n                : t('comp.musicList.switchToCompact')\n            }}\n          </n-tooltip>\n        </div>\n      </div>\n    </div>\n\n    <!-- 日期选择标签 -->\n    <div v-if=\"availableDates.length > 0\" class=\"date-tabs-wrapper\">\n      <n-tabs\n        v-model:value=\"selectedDate\"\n        type=\"segment\"\n        animated\n        size=\"large\"\n        @update:value=\"handleDateChange\"\n      >\n        <n-tab\n          v-for=\"date in displayedDates\"\n          :key=\"date\"\n          :name=\"date\"\n          :tab=\"formatDate(date)\"\n        ></n-tab>\n      </n-tabs>\n    </div>\n\n    <!-- 歌曲列表内容 -->\n    <div class=\"music-content\">\n      <n-spin :show=\"loadingDates || loadingSongs\">\n        <!-- 歌曲列表 -->\n        <div v-if=\"songs.length > 0\" class=\"music-list-container\">\n          <div class=\"music-list\">\n            <div class=\"music-list-content\">\n              <!-- 使用虚拟列表 -->\n              <n-virtual-list\n                class=\"song-virtual-list\"\n                style=\"max-height: calc(100vh - 200px)\"\n                :items=\"songs\"\n                :item-size=\"isCompactLayout ? 50 : 70\"\n                item-resizable\n                key-field=\"id\"\n              >\n                <template #default=\"{ item, index }\">\n                  <div>\n                    <div class=\"double-item\">\n                      <song-item\n                        :index=\"index\"\n                        :compact=\"isCompactLayout\"\n                        :item=\"formatSong(item)\"\n                        @play=\"handlePlay\"\n                      />\n                    </div>\n                    <div v-if=\"index === songs.length - 1\" class=\"h-36\"></div>\n                  </div>\n                </template>\n              </n-virtual-list>\n            </div>\n          </div>\n        </div>\n\n        <!-- 空状态 -->\n        <div v-else-if=\"!loadingSongs && selectedDate\" class=\"empty-state\">\n          <i class=\"icon iconfont ri-disc-line\"></i>\n          <p>{{ t('comp.musicList.noSongs') }}</p>\n        </div>\n      </n-spin>\n    </div>\n    <play-bottom />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useMessage } from 'naive-ui';\nimport { computed, onMounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { getHistoryRecommendDates, getHistoryRecommendSongs } from '@/api/music';\nimport PlayBottom from '@/components/common/PlayBottom.vue';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { usePlayerStore } from '@/store';\nimport type { SongResult } from '@/types/music';\nimport { isMobile } from '@/utils';\n\nconst { t } = useI18n();\nconst message = useMessage();\nconst playerStore = usePlayerStore();\n\n// 状态\nconst availableDates = ref<string[]>([]);\nconst selectedDate = ref<string>('');\nconst songs = ref<SongResult[]>([]);\nconst loadingDates = ref(false);\nconst loadingSongs = ref(false);\nconst isCompactLayout = ref(\n  isMobile.value ? false : localStorage.getItem('musicListLayout') === 'compact'\n);\n\n// 只显示最近的10个日期\nconst displayedDates = computed(() => {\n  return availableDates.value.slice(0, 10);\n});\n\n// 格式化日期显示\nconst formatDate = (dateStr: string) => {\n  const date = new Date(dateStr);\n  const today = new Date();\n  const yesterday = new Date(today);\n  yesterday.setDate(yesterday.getDate() - 1);\n\n  // 判断是否是今天或昨天\n  if (date.toDateString() === today.toDateString()) {\n    return t('common.today');\n  } else if (date.toDateString() === yesterday.toDateString()) {\n    return t('common.yesterday');\n  }\n\n  // 格式化为 MM月DD日\n  const month = date.getMonth() + 1;\n  const day = date.getDate();\n  return `${month}月${day}日`;\n};\n\n// 格式化歌曲数据\nconst formatSong = (item: any) => {\n  if (!item) return null;\n  return {\n    ...item,\n    picUrl: item.al?.picUrl || item.album?.picUrl || item.picUrl,\n    song: {\n      artists: item.ar || item.artists || [],\n      name: item.al?.name || item.album?.name || item.name,\n      id: item.al?.id || item.album?.id || item.id\n    }\n  };\n};\n\n// 获取可用日期列表\nconst fetchAvailableDates = async () => {\n  try {\n    loadingDates.value = true;\n    const { data } = await getHistoryRecommendDates();\n    if (data?.data?.dates) {\n      availableDates.value = data.data.dates;\n      // 默认选择第一个日期（最近的日期）\n      if (availableDates.value.length > 0) {\n        selectedDate.value = availableDates.value[0];\n        await fetchSongsByDate(selectedDate.value);\n      }\n    }\n  } catch (error) {\n    console.error('获取历史日推日期列表失败:', error);\n    message.error(t('comp.musicList.fetchDatesFailed'));\n  } finally {\n    loadingDates.value = false;\n  }\n};\n\n// 根据日期获取歌曲列表\nconst fetchSongsByDate = async (date: string) => {\n  try {\n    loadingSongs.value = true;\n    const { data } = await getHistoryRecommendSongs(date);\n    if (data?.data?.songs) {\n      songs.value = data.data.songs;\n    } else {\n      songs.value = [];\n    }\n  } catch (error) {\n    console.error('获取历史日推歌曲失败:', error);\n    message.error(t('comp.musicList.fetchSongsFailed'));\n    songs.value = [];\n  } finally {\n    loadingSongs.value = false;\n  }\n};\n\n// 处理日期变化\nconst handleDateChange = async (date: string) => {\n  selectedDate.value = date;\n  await fetchSongsByDate(date);\n};\n\n// 切换布局\nconst toggleLayout = () => {\n  isCompactLayout.value = !isCompactLayout.value;\n  localStorage.setItem('musicListLayout', isCompactLayout.value ? 'compact' : 'normal');\n};\n\n// 添加到播放列表末尾\nconst addToPlaylist = () => {\n  if (songs.value.length === 0) return;\n\n  // 获取当前播放列表\n  const currentList = playerStore.playList;\n\n  // 添加歌曲到播放列表(避免重复添加)\n  const newSongs = songs.value.filter((song) => !currentList.some((item) => item.id === song.id));\n\n  if (newSongs.length === 0) {\n    message.info(t('comp.musicList.songsAlreadyInPlaylist'));\n    return;\n  }\n\n  // 合并到当前播放列表末尾\n  const newList = [...currentList, ...newSongs.map(formatSong)];\n  playerStore.setPlayList(newList);\n\n  message.success(t('comp.musicList.addToPlaylistSuccess', { count: newSongs.length }));\n};\n\n// 播放单首歌曲\nconst handlePlay = () => {\n  if (songs.value.length === 0) return;\n  playerStore.setPlayList(songs.value.map(formatSong));\n};\n\n// 播放全部\nconst handlePlayAll = () => {\n  if (songs.value.length === 0) return;\n  playerStore.setPlayList(songs.value.map(formatSong));\n  playerStore.setPlay(formatSong(songs.value[0]));\n};\n\n// 组件挂载时获取数据\nonMounted(() => {\n  fetchAvailableDates();\n});\n</script>\n\n<style scoped lang=\"scss\">\n.history-recommend-page {\n  @apply h-full bg-light-100 dark:bg-dark-100 px-4 mr-2 rounded-2xl;\n}\n\n.music {\n  &-header {\n    @apply h-12 flex items-center justify-between;\n  }\n\n  &-title {\n    @apply text-xl font-bold text-gray-900 dark:text-white;\n  }\n\n  &-content {\n    @apply h-[calc(100%-60px)];\n  }\n\n  &-list {\n    @apply flex-grow min-h-0;\n    &-container {\n      @apply flex-grow min-h-0 flex flex-col relative w-full;\n    }\n\n    &-content {\n      @apply min-h-[calc(80vh-60px)];\n    }\n  }\n}\n\n.date-tabs-wrapper {\n  @apply px-0 mb-4;\n}\n\n.action-button {\n  @apply w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:bg-light-300 dark:hover:bg-dark-300 transition-colors text-gray-500 dark:text-gray-400;\n\n  .icon {\n    @apply text-lg;\n  }\n\n  &.hover-green:hover {\n    .icon {\n      @apply text-green-500;\n    }\n  }\n}\n\n/* 虚拟列表样式 */\n.song-virtual-list {\n  @apply w-full;\n  :deep(.n-virtual-list__scroll) {\n    scrollbar-width: thin;\n    &::-webkit-scrollbar {\n      width: 4px;\n    }\n    &::-webkit-scrollbar-thumb {\n      @apply bg-gray-400 dark:bg-gray-600 rounded;\n    }\n  }\n}\n\n.double-item {\n  @apply w-full mb-2 bg-light-200 bg-opacity-30 dark:bg-dark-200 dark:bg-opacity-20 rounded-3xl;\n}\n\n.empty-state {\n  @apply flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-600 py-20;\n\n  .icon {\n    @apply text-6xl mb-4;\n  }\n\n  p {\n    @apply text-lg;\n  }\n}\n\n:deep(.n-tabs-rail) {\n  @apply rounded-xl overflow-hidden !important;\n  .n-tabs-capsule {\n    @apply rounded-xl !important;\n  }\n}\n\n.date-tabs-wrapper {\n  :deep(.n-tabs-rail) {\n    @apply rounded-xl overflow-hidden bg-white dark:bg-dark-300 !important;\n    .n-tabs-capsule {\n      @apply rounded-xl bg-green-500 dark:bg-green-600 !important;\n    }\n    .n-tabs-tab--active {\n      @apply text-white !important;\n    }\n  }\n}\n\n.layout-toggle {\n  .toggle-button {\n    @apply w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:bg-light-300 dark:hover:bg-dark-300 transition-colors;\n\n    .icon {\n      @apply text-lg text-gray-500 dark:text-gray-400 transition-colors;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/music/MusicListPage.vue",
    "content": "<template>\n  <div class=\"music-page\">\n    <div class=\"music-header h-12 flex items-center justify-between\">\n      <n-ellipsis :line-clamp=\"1\" class=\"flex-shrink-0 mr-3\">\n        <div class=\"music-title\">\n          {{ name }}\n        </div>\n      </n-ellipsis>\n\n      <!-- 搜索框和布局切换 -->\n      <div class=\"flex-grow flex-1 flex items-center justify-end gap-2\">\n        <!-- 操作按钮组 -->\n        <n-tooltip placement=\"bottom\" trigger=\"hover\">\n          <template #trigger>\n            <div class=\"action-button hover-green\" @click=\"handlePlayAll\">\n              <i class=\"icon iconfont ri-play-fill\"></i>\n            </div>\n          </template>\n          {{ t('comp.musicList.playAll') }}\n        </n-tooltip>\n\n        <n-tooltip v-if=\"canCollect\" placement=\"bottom\" trigger=\"hover\">\n          <template #trigger>\n            <div\n              class=\"action-button\"\n              :class=\"isCollected ? 'collected' : 'hover-green'\"\n              @click=\"toggleCollect\"\n            >\n              <i class=\"icon iconfont\" :class=\"isCollected ? 'ri-heart-fill' : 'ri-heart-line'\"></i>\n            </div>\n          </template>\n          {{ isCollected ? t('comp.musicList.cancelCollect') : t('comp.musicList.collect') }}\n        </n-tooltip>\n\n        <n-tooltip placement=\"bottom\" trigger=\"hover\">\n          <template #trigger>\n            <div class=\"action-button hover-green\" @click=\"addToPlaylist\">\n              <i class=\"icon iconfont ri-add-line\"></i>\n            </div>\n          </template>\n          {{ t('comp.musicList.addToPlaylist') }}\n        </n-tooltip>\n\n        <!-- 多选/下载操作 -->\n        <div v-if=\"filteredSongs.length > 0 && isElectron\" class=\"flex items-center gap-2\">\n          <n-tooltip v-if=\"!isSelecting\" placement=\"bottom\" trigger=\"hover\">\n            <template #trigger>\n              <div class=\"action-button hover-green\" @click=\"startSelect\">\n                <i class=\"icon iconfont ri-checkbox-multiple-line\"></i>\n              </div>\n            </template>\n            {{ t('favorite.batchDownload') }}\n          </n-tooltip>\n          <div v-else class=\"flex items-center gap-2\">\n            <n-checkbox\n              :checked=\"isAllSelected\"\n              :indeterminate=\"isIndeterminate\"\n              @update:checked=\"handleSelectAll\"\n            >\n              {{ t('common.selectAll') }}\n            </n-checkbox>\n            <n-tooltip placement=\"bottom\" trigger=\"hover\">\n              <template #trigger>\n                <div\n                  class=\"action-button hover-green\"\n                  :class=\"{\n                    'opacity-50 pointer-events-none': selectedSongs.length === 0 || isDownloading\n                  }\"\n                  @click=\"selectedSongs.length && !isDownloading && handleBatchDownload()\"\n                >\n                  <i\n                    class=\"icon iconfont ri-download-line\"\n                    :class=\"{ 'animate-spin': isDownloading }\"\n                  ></i>\n                </div>\n              </template>\n              {{ t('favorite.download', { count: selectedSongs.length }) }}\n            </n-tooltip>\n            <n-tooltip placement=\"bottom\" trigger=\"hover\">\n              <template #trigger>\n                <div class=\"action-button\" @click=\"cancelSelect\">\n                  <i class=\"icon iconfont ri-close-line\"></i>\n                </div>\n              </template>\n              {{ t('common.cancel') }}\n            </n-tooltip>\n          </div>\n        </div>\n\n        <!-- 布局切换按钮 -->\n        <div class=\"layout-toggle\" v-if=\"!isMobile\">\n          <n-tooltip placement=\"bottom\" trigger=\"hover\">\n            <template #trigger>\n              <div class=\"toggle-button hover-green\" @click=\"toggleLayout\">\n                <i\n                  class=\"icon iconfont\"\n                  :class=\"isCompactLayout ? 'ri-list-check-2' : 'ri-grid-line'\"\n                ></i>\n              </div>\n            </template>\n            {{\n              isCompactLayout\n                ? t('comp.musicList.switchToNormal')\n                : t('comp.musicList.switchToCompact')\n            }}\n          </n-tooltip>\n        </div>\n\n        <div class=\"search-container\" :class=\"{ 'search-expanded': isSearchVisible }\">\n          <template v-if=\"isSearchVisible\">\n            <n-input\n              v-model:value=\"searchKeyword\"\n              :placeholder=\"t('comp.musicList.searchSongs')\"\n              clearable\n              round\n              size=\"small\"\n              @blur=\"handleSearchBlur\"\n            >\n              <template #prefix>\n                <i class=\"icon iconfont ri-search-line text-sm\"></i>\n              </template>\n              <template #suffix>\n                <i\n                  class=\"icon iconfont ri-close-line text-sm cursor-pointer\"\n                  @click=\"closeSearch\"\n                ></i>\n              </template>\n            </n-input>\n          </template>\n          <template v-else>\n            <n-tooltip placement=\"bottom\" trigger=\"hover\">\n              <template #trigger>\n                <div class=\"search-button\" @click=\"showSearch\">\n                  <i class=\"icon iconfont ri-search-line\"></i>\n                </div>\n              </template>\n              {{ t('comp.musicList.searchSongs') }}\n            </n-tooltip>\n          </template>\n        </div>\n      </div>\n    </div>\n\n    <div class=\"music-content\">\n      <!-- 左侧歌单信息 -->\n      <div class=\"music-info\">\n        <div class=\"music-cover\">\n          <n-image\n            :src=\"getImgUrl(getCoverImgUrl, '500y500')\"\n            class=\"cover-img\"\n            preview-disabled\n            :class=\"setAnimationClass('animate__fadeIn')\"\n            object-fit=\"cover\"\n          />\n\n          <div v-if=\"isDailyRecommend && userStore.isVip\" class=\"history-recommend-btn\">\n            <n-button tertiary round type=\"primary\" size=\"small\" @click=\"goToHistoryRecommend\">\n              <template #icon>\n                <i class=\"icon iconfont ri-history-line\"></i>\n              </template>\n              {{ t('comp.musicList.historyRecommend') }}\n            </n-button>\n          </div>\n        </div>\n        <!-- 歌单显示创建者，专辑显示艺术家 -->\n        <div v-if=\"isAlbum && listInfo?.artist\" class=\"creator-info\">\n          <n-avatar round :size=\"24\" :src=\"getImgUrl(listInfo.artist.picUrl, '50y50')\" />\n          <span class=\"creator-name\">{{ listInfo.artist.name }}</span>\n        </div>\n        <div v-else-if=\"!isAlbum && listInfo?.creator\" class=\"creator-info\">\n          <n-avatar round :size=\"24\" :src=\"getImgUrl(listInfo.creator.avatarUrl, '50y50')\" />\n          <span class=\"creator-name\">{{ listInfo.creator.nickname }}</span>\n        </div>\n        <div v-if=\"total\" class=\"music-total\">{{ t('player.songNum', { num: total }) }}</div>\n\n        <n-scrollbar style=\"max-height: 200px\">\n          <div v-if=\"listInfo?.description\" class=\"music-desc\">\n            {{ listInfo.description }}\n          </div>\n        </n-scrollbar>\n      </div>\n\n      <!-- 右侧歌曲列表 -->\n      <div class=\"music-list-container\">\n        <div class=\"music-list\">\n          <n-spin :show=\"loadingList || loading\">\n            <div class=\"music-list-content\">\n              <div v-if=\"filteredSongs.length === 0 && searchKeyword\" class=\"no-result\">\n                {{ t('comp.musicList.noSearchResults') }}\n              </div>\n\n              <!-- 虚拟列表，设置正确的固定高度 -->\n              <n-virtual-list\n                ref=\"songListRef\"\n                class=\"song-virtual-list\"\n                style=\"max-height: calc(100vh - 130px)\"\n                :items=\"filteredSongs\"\n                :item-size=\"isCompactLayout ? 50 : 70\"\n                item-resizable\n                key-field=\"id\"\n                @scroll=\"handleVirtualScroll\"\n              >\n                <template #default=\"{ item, index }\">\n                  <div>\n                    <div class=\"double-item\">\n                      <song-item\n                        :index=\"index\"\n                        :compact=\"isCompactLayout\"\n                        :item=\"formatSong(item)\"\n                        :can-remove=\"canRemove\"\n                        :selectable=\"isSelecting\"\n                        :selected=\"selectedSongs.includes(item.id as number)\"\n                        @play=\"handlePlay\"\n                        @remove-song=\"handleRemoveSong\"\n                        @select=\"(id, selected) => handleSelect(id, selected)\"\n                      />\n                    </div>\n                    <div v-if=\"index === filteredSongs.length - 1\" class=\"h-36\"></div>\n                  </div>\n                </template>\n              </n-virtual-list>\n            </div>\n          </n-spin>\n        </div>\n      </div>\n    </div>\n    <play-bottom />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\n// 添加组件名称定义\ndefineOptions({\n  name: 'MusicList'\n});\n\nimport { useMessage } from 'naive-ui';\nimport PinyinMatch from 'pinyin-match';\nimport { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport {\n  getMusicDetail,\n  subscribeAlbum,\n  subscribePlaylist,\n  updatePlaylistTracks\n} from '@/api/music';\nimport PlayBottom from '@/components/common/PlayBottom.vue';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { useAlbumHistory } from '@/hooks/AlbumHistoryHook';\nimport { usePlaylistHistory } from '@/hooks/PlaylistHistoryHook';\nimport { useDownload } from '@/hooks/useDownload';\nimport { useMusicStore, usePlayerStore, useRecommendStore, useUserStore } from '@/store';\nimport { SongResult } from '@/types/music';\nimport { getImgUrl, isElectron, isMobile, setAnimationClass } from '@/utils';\nimport { getLoginErrorMessage, hasPermission } from '@/utils/auth';\n\nconst { t } = useI18n();\nconst route = useRoute();\nconst router = useRouter();\nconst playerStore = usePlayerStore();\nconst musicStore = useMusicStore();\nconst recommendStore = useRecommendStore();\nconst userStore = useUserStore();\nconst message = useMessage();\nconst { addPlaylist } = usePlaylistHistory();\nconst { addAlbum } = useAlbumHistory();\n\n// 从路由参数或状态管理获取数据\nconst loading = ref(false);\nconst isDailyRecommend = computed(() => route.query.type === 'dailyRecommend');\nconst isAlbum = computed(() => route.query.type === 'album');\nconst name = computed(() => {\n  if (isDailyRecommend.value) {\n    return t('comp.recommendSinger.songlist'); // 日推的标题\n  }\n  return musicStore.currentMusicListName || ''; // 其他列表的标题\n});\nconst songList = computed(() => {\n  if (isDailyRecommend.value) {\n    // 如果是日推页面，直接使用 recommendStore 中响应式的数据\n    return recommendStore.dailyRecommendSongs;\n  }\n  // 否则，使用 musicStore 中的静态数据\n  return musicStore.currentMusicList || [];\n});\nconst listInfo = computed(() => {\n  if (isDailyRecommend.value) {\n    return null;\n  }\n  return musicStore.currentListInfo || null;\n});\nconst canRemove = computed(() => {\n  if (isDailyRecommend.value) {\n    return false;\n  }\n  return musicStore.canRemoveSong || false;\n});\n\nconst canCollect = ref(false);\nconst isCollected = ref(false);\n\nconst page = ref(0);\nconst pageSize = 40;\nconst isLoadingMore = ref(false);\nconst displayedSongs = ref<SongResult[]>([]);\nconst loadingList = ref(false);\nconst loadedIds = ref(new Set<number>()); // 用于追踪已加载的歌曲ID\nconst isPlaylistLoading = ref(false); // 标记是否正在加载播放列表\nconst completePlaylist = ref<SongResult[]>([]); // 存储完整的播放列表\nconst hasMore = ref(true); // 标记是否还有更多数据可加载\nconst searchKeyword = ref(''); // 搜索关键词\nconst isFullPlaylistLoaded = ref(false); // 标记完整播放列表是否已加载完成\n\n// 添加搜索相关的状态和方法\nconst isSearchVisible = ref(false);\nconst isCompactLayout = ref(\n  isMobile.value ? false : localStorage.getItem('musicListLayout') === 'compact'\n); // 默认使用紧凑布局\n\nconst showSearch = () => {\n  isSearchVisible.value = true;\n  // 添加一个小延迟后聚焦搜索框\n  nextTick(() => {\n    const inputEl = document.querySelector('.search-container input');\n    if (inputEl) {\n      (inputEl as HTMLInputElement).focus();\n    }\n  });\n};\n\nconst closeSearch = () => {\n  isSearchVisible.value = false;\n  searchKeyword.value = '';\n};\n\nconst handleSearchBlur = () => {\n  // 如果搜索框为空，则在失焦时关闭搜索框\n  if (!searchKeyword.value) {\n    setTimeout(() => {\n      isSearchVisible.value = false;\n    }, 200);\n  }\n};\n\n// 计算总数\nconst total = computed(() => {\n  if (listInfo.value?.trackIds) {\n    return listInfo.value.trackIds.length;\n  }\n  return songList.value.length;\n});\n\n// 初始化数据\nonMounted(() => {\n  checkCollectionStatus();\n});\n\nconst getCoverImgUrl = computed(() => {\n  const coverImgUrl = listInfo.value?.coverImgUrl || listInfo.value?.picUrl;\n  if (coverImgUrl) {\n    return coverImgUrl;\n  }\n\n  const song = songList.value[0];\n  if (song?.picUrl) {\n    return song.picUrl;\n  }\n  if (song?.al?.picUrl) {\n    return song.al.picUrl;\n  }\n  if (song?.album?.picUrl) {\n    return song.album.picUrl;\n  }\n  return '';\n});\n\n// 过滤歌曲列表\nconst filteredSongs = computed(() => {\n  // 1. 确定数据源是来自store的完整列表(日推)还是来自本地分页的列表(其他)\n  const sourceList = isDailyRecommend.value\n    ? songList.value // 如果是日推，直接使用来自 recommendStore 的完整、响应式列表\n    : displayedSongs.value; // 否则，使用用于分页加载的 displayedSongs\n\n  // 2. 过滤不喜欢的歌曲\n  const dislikeFilteredList = sourceList.filter(\n    (song) => !playerStore.dislikeList.includes(song.id)\n  );\n  // =================================================================\n\n  // 3. 如果没有搜索词，直接返回处理后的列表\n  if (!searchKeyword.value) {\n    return dislikeFilteredList;\n  }\n\n  // 4. 如果有搜索词，在处理后的列表上进行搜索\n  const keyword = searchKeyword.value.toLowerCase().trim();\n  return dislikeFilteredList.filter((song) => {\n    const songName = song.name?.toLowerCase() || '';\n    const albumName = song.al?.name?.toLowerCase() || '';\n    const artists = song.ar || song.artists || [];\n\n    // 原始文本匹配\n    const nameMatch = songName.includes(keyword);\n    const albumMatch = albumName.includes(keyword);\n    const artistsMatch = artists.some((artist: any) => {\n      return artist.name?.toLowerCase().includes(keyword);\n    });\n\n    // 拼音匹配\n    const namePinyinMatch = song.name && PinyinMatch.match(song.name, keyword);\n    const albumPinyinMatch = song.al?.name && PinyinMatch.match(song.al.name, keyword);\n    const artistsPinyinMatch = artists.some((artist: any) => {\n      return artist.name && PinyinMatch.match(artist.name, keyword);\n    });\n\n    return (\n      nameMatch ||\n      albumMatch ||\n      artistsMatch ||\n      namePinyinMatch ||\n      albumPinyinMatch ||\n      artistsPinyinMatch\n    );\n  });\n});\n\nconst resetListState = () => {\n  page.value = 0;\n  loadedIds.value.clear();\n  displayedSongs.value = [];\n  completePlaylist.value = [];\n  hasMore.value = true;\n  loadingList.value = false;\n  searchKeyword.value = '';\n  isFullPlaylistLoaded.value = false;\n};\n\n// 格式化歌曲数据\nconst formatSong = (item: any) => {\n  if (!item) {\n    return null;\n  }\n  return {\n    ...item,\n    picUrl: item.al?.picUrl || item.picUrl,\n    song: {\n      artists: item.ar || item.artists,\n      name: item.al?.name || item.name,\n      id: item.al?.id || item.id\n    }\n  };\n};\n\n/**\n * 加载歌曲数据的核心函数\n * @param ids 要加载的歌曲ID数组\n * @param appendToList 是否将加载的歌曲追加到现有列表\n * @param updateComplete 是否更新完整播放列表\n */\nconst loadSongs = async (ids: number[], appendToList = true, updateComplete = false) => {\n  if (ids.length === 0) return [];\n\n  try {\n    console.log(`请求歌曲详情，ID数量: ${ids.length}`);\n    const { data } = await getMusicDetail(ids);\n\n    if (data?.songs) {\n      console.log(`API返回歌曲数量: ${data.songs.length}`);\n\n      // 直接使用API返回的所有歌曲，不再过滤已加载的歌曲\n      // 因为当需要完整加载列表时，我们希望获取所有歌曲，即使ID可能重复\n      const { songs } = data;\n\n      // 只在非更新完整列表时执行过滤\n      let newSongs = songs;\n      if (!updateComplete) {\n        // 在普通加载模式下继续过滤已加载的歌曲，避免重复\n        newSongs = songs.filter((song: any) => !loadedIds.value.has(song.id));\n        console.log(`过滤已加载ID后剩余歌曲数量: ${newSongs.length}`);\n      }\n\n      // 更新已加载ID集合\n      songs.forEach((song: any) => {\n        loadedIds.value.add(song.id);\n      });\n\n      // 追加到显示列表 - 仅当appendToList=true时添加到displayedSongs\n      if (appendToList) {\n        displayedSongs.value.push(...newSongs);\n      }\n\n      // 更新完整播放列表 - 仅当updateComplete=true时添加到completePlaylist\n      if (updateComplete) {\n        completePlaylist.value.push(...songs);\n        console.log(`已添加到完整播放列表，当前完整列表长度: ${completePlaylist.value.length}`);\n      }\n\n      return updateComplete ? songs : newSongs;\n    }\n    console.log('API返回无歌曲数据');\n    return [];\n  } catch (error) {\n    console.error('加载歌曲失败:', error);\n  }\n\n  return [];\n};\n\n// 加载完整播放列表\nconst loadFullPlaylist = async () => {\n  if (isPlaylistLoading.value || isFullPlaylistLoaded.value) return;\n\n  isPlaylistLoading.value = true;\n  // 记录开始时间\n  const startTime = Date.now();\n  console.log(`开始加载完整播放列表，当前显示列表长度: ${displayedSongs.value.length}`);\n\n  try {\n    // 如果没有trackIds，直接使用当前歌曲列表并标记为已完成\n    if (!listInfo.value?.trackIds) {\n      isFullPlaylistLoaded.value = true;\n      console.log('无trackIds信息，使用当前列表作为完整列表');\n      return;\n    }\n\n    // 获取所有trackIds\n    const allIds = listInfo.value.trackIds.map((item) => item.id);\n    console.log(`歌单共有歌曲ID: ${allIds.length}首`);\n\n    // 重置completePlaylist和当前显示歌曲ID集合，保证不会重复添加歌曲\n    completePlaylist.value = [];\n\n    // 使用Set记录所有已加载的歌曲ID\n    const loadedSongIds = new Set<number>();\n\n    // 将当前显示列表中的歌曲和ID添加到集合中\n    displayedSongs.value.forEach((song) => {\n      loadedSongIds.add(song.id as number);\n      // 将已有歌曲添加到completePlaylist\n      completePlaylist.value.push(song);\n    });\n\n    console.log(\n      `已有显示歌曲: ${displayedSongs.value.length}首，已有ID数量: ${loadedSongIds.size}`\n    );\n\n    // 过滤出尚未加载的歌曲ID\n    const unloadedIds = allIds.filter((id) => !loadedSongIds.has(id));\n    console.log(`还需要加载的歌曲ID数量: ${unloadedIds.length}`);\n\n    if (unloadedIds.length === 0) {\n      console.log('所有歌曲已加载，无需再次加载');\n      isFullPlaylistLoaded.value = true;\n      hasMore.value = false;\n      return;\n    }\n\n    // 分批加载所有未加载的歌曲\n    const batchSize = 500; // 每批加载的歌曲数量\n\n    for (let i = 0; i < unloadedIds.length; i += batchSize) {\n      const batchIds = unloadedIds.slice(i, i + batchSize);\n      if (batchIds.length === 0) continue;\n\n      console.log(`请求第${Math.floor(i / batchSize) + 1}批歌曲，数量: ${batchIds.length}`);\n      // 关键修改: 设置appendToList为false，避免loadSongs直接添加到displayedSongs\n      const loadedBatch = await loadSongs(batchIds, false, false);\n\n      // 添加新加载的歌曲到displayedSongs\n      if (loadedBatch.length > 0) {\n        // 过滤掉已有的歌曲，确保不会重复添加\n        const newSongs = loadedBatch.filter((song) => !loadedSongIds.has(song.id as number));\n\n        // 更新已加载ID集合\n        newSongs.forEach((song) => {\n          loadedSongIds.add(song.id as number);\n        });\n\n        console.log(`新增${newSongs.length}首歌曲到显示列表`);\n\n        // 更新显示列表和完整播放列表\n        if (newSongs.length > 0) {\n          // 添加到显示列表\n          displayedSongs.value = [...displayedSongs.value, ...newSongs];\n\n          // 添加到完整播放列表\n          completePlaylist.value.push(...newSongs);\n\n          // 如果当前正在播放的列表与这个列表匹配，实时更新播放列表\n          const currentPlaylist = playerStore.playList;\n          if (currentPlaylist.length > 0 && currentPlaylist[0].id === displayedSongs.value[0]?.id) {\n            console.log('实时更新当前播放列表');\n            playerStore.setPlayList(displayedSongs.value.map(formatSong));\n          }\n        }\n      }\n\n      // 添加小延迟避免请求过于密集\n      if (i + batchSize < unloadedIds.length) {\n        await new Promise<void>((resolve) => {\n          setTimeout(() => resolve(), 100);\n        });\n      }\n    }\n\n    // 加载完成，更新状态\n    isFullPlaylistLoaded.value = true;\n    hasMore.value = false;\n\n    // 计算加载耗时\n    const endTime = Date.now();\n    const timeUsed = Math.round(((endTime - startTime) / 1000) * 100) / 100;\n\n    console.log(\n      `完整播放列表加载完成，共加载${displayedSongs.value.length}首歌曲，耗时${timeUsed}秒`\n    );\n    console.log(`歌单应有${allIds.length}首歌，实际加载${displayedSongs.value.length}首`);\n\n    // 检查加载的歌曲数量是否与预期相符\n    if (displayedSongs.value.length !== allIds.length) {\n      console.warn(\n        `警告: 加载的歌曲数量(${displayedSongs.value.length})与歌单应有数量(${allIds.length})不符`\n      );\n\n      // 如果数量不符，可能是API未返回所有歌曲，打印缺失的歌曲ID\n      if (displayedSongs.value.length < allIds.length) {\n        const loadedIds = new Set(displayedSongs.value.map((song) => song.id));\n        const missingIds = allIds.filter((id) => !loadedIds.has(id));\n        console.warn(`缺失的歌曲ID: ${missingIds.join(', ')}`);\n      }\n    }\n  } catch (error) {\n    console.error('加载完整播放列表失败:', error);\n  } finally {\n    isPlaylistLoading.value = false;\n  }\n};\n\n// 处理播放\nconst handlePlay = async () => {\n  // 当搜索状态下播放时，只播放过滤后的歌曲\n  if (searchKeyword.value) {\n    playerStore.setPlayList(filteredSongs.value.map(formatSong));\n    return;\n  }\n  saveHistory();\n  // 如果完整播放列表已加载完成\n  if (isFullPlaylistLoaded.value && completePlaylist.value.length > 0) {\n    playerStore.setPlayList(completePlaylist.value.map(formatSong));\n    return;\n  }\n\n  // 如果完整播放列表未加载完成，先使用当前已加载的歌曲开始播放\n  playerStore.setPlayList(displayedSongs.value.map(formatSong));\n\n  // 如果完整播放列表正在加载中，不需要重新触发加载\n  if (isPlaylistLoading.value) {\n    return;\n  }\n\n  // 在后台继续加载完整播放列表（如果未加载完成）\n  if (!isFullPlaylistLoaded.value) {\n    console.log('播放时继续在后台加载完整列表');\n    loadFullPlaylist();\n  }\n};\n\n// 添加从歌单移除歌曲的方法\nconst handleRemoveSong = async (songId: number) => {\n  if (!listInfo.value?.id || !canRemove.value) return;\n\n  try {\n    const res = await updatePlaylistTracks({\n      op: 'del',\n      pid: listInfo.value.id,\n      tracks: songId.toString()\n    });\n\n    if (res.status === 200) {\n      message.success(t('user.message.deleteSuccess'));\n\n      // 从显示列表和完整播放列表中移除歌曲\n      displayedSongs.value = displayedSongs.value.filter((song) => song.id !== songId);\n      completePlaylist.value = completePlaylist.value.filter((song) => song.id !== songId);\n\n      // 如果正在播放该列表，也需要更新播放列表\n      const currentPlaylist = playerStore.playList;\n      if (currentPlaylist.length > 0 && currentPlaylist[0].id === displayedSongs.value[0]?.id) {\n        playerStore.setPlayList(displayedSongs.value.map(formatSong));\n      }\n\n      // 从Pinia状态中也移除\n      if (musicStore.currentMusicList) {\n        musicStore.removeSongFromList(songId);\n      }\n    } else {\n      throw new Error(res.data?.msg || t('user.message.deleteFailed'));\n    }\n  } catch (error: any) {\n    console.error('删除歌曲失败:', error);\n    message.error(error.message || t('user.message.deleteFailed'));\n  }\n};\n\n// 加载更多歌曲\nconst loadMoreSongs = async () => {\n  if (isFullPlaylistLoaded.value) {\n    hasMore.value = false;\n    return;\n  }\n\n  if (searchKeyword.value) {\n    return;\n  }\n\n  if (isLoadingMore.value || displayedSongs.value.length >= total.value) {\n    hasMore.value = false;\n    return;\n  }\n\n  isLoadingMore.value = true;\n\n  try {\n    const start = displayedSongs.value.length;\n    const end = Math.min(start + pageSize, total.value);\n\n    if (listInfo.value?.trackIds) {\n      const trackIdsToLoad = listInfo.value.trackIds\n        .slice(start, end)\n        .map((item) => item.id)\n        .filter((id) => !loadedIds.value.has(id));\n\n      if (trackIdsToLoad.length > 0) {\n        await loadSongs(trackIdsToLoad, true, false);\n      }\n    } else if (start < songList.value.length) {\n      const newSongs = songList.value.slice(start, end);\n      newSongs.forEach((song) => {\n        if (!loadedIds.value.has(song.id)) {\n          loadedIds.value.add(song.id);\n          displayedSongs.value.push(song);\n        }\n      });\n    }\n\n    hasMore.value = displayedSongs.value.length < total.value;\n  } catch (error) {\n    console.error('加载更多歌曲失败:', error);\n  } finally {\n    isLoadingMore.value = false;\n    loadingList.value = false;\n  }\n};\n\n// 处理虚拟列表滚动事件\nconst handleVirtualScroll = (e: any) => {\n  if (!e || !e.target) return;\n\n  const { scrollTop, scrollHeight, clientHeight } = e.target;\n  const threshold = 200;\n\n  if (\n    scrollHeight - scrollTop - clientHeight < threshold &&\n    !isLoadingMore.value &&\n    hasMore.value &&\n    !searchKeyword.value // 搜索状态下不触发加载更多\n  ) {\n    loadMoreSongs();\n  }\n};\n\n// 初始化歌曲列表\nconst initSongList = (songs: any[]) => {\n  if (songs.length > 0) {\n    displayedSongs.value = [...songs];\n    songs.forEach((song) => loadedIds.value.add(song.id));\n    page.value = Math.ceil(songs.length / pageSize);\n  }\n\n  // 检查是否还有更多数据可加载\n  hasMore.value = displayedSongs.value.length < total.value;\n};\n\nwatch(\n  () => listInfo.value,\n  (newListInfo) => {\n    if (newListInfo?.trackIds) {\n      loadFullPlaylist();\n    }\n  },\n  { deep: true }\n);\n\n// 监听搜索关键词变化\nwatch(searchKeyword, () => {\n  // 当搜索关键词为空时，考虑加载更多歌曲\n  if (!searchKeyword.value && hasMore.value && displayedSongs.value.length < total.value) {\n    loadMoreSongs();\n  }\n});\n\nwatch(\n  songList,\n  (newSongs) => {\n    resetListState();\n    initSongList(newSongs);\n    if (hasMore.value && listInfo.value?.trackIds) {\n      setTimeout(() => {\n        loadMoreSongs();\n      }, 300);\n    }\n  },\n  { immediate: true }\n);\n\n// 组件卸载时清理状态\nonUnmounted(() => {\n  isPlaylistLoading.value = false;\n});\n\n// 切换布局\nconst toggleLayout = () => {\n  isCompactLayout.value = !isCompactLayout.value;\n  localStorage.setItem('musicListLayout', isCompactLayout.value ? 'compact' : 'normal');\n};\n\n// 初始化收藏状态（支持歌单和专辑）\nconst checkCollectionStatus = () => {\n  const type = route.query.type as string;\n\n  // 歌单类型的收藏检查\n  if (type === 'playlist' && listInfo.value?.id) {\n    canCollect.value = true;\n    isCollected.value = listInfo.value.subscribed || false;\n  }\n  // 专辑类型的收藏检查 - 使用 store 判断\n  else if (type === 'album' && listInfo.value?.id) {\n    canCollect.value = true;\n    // 从 userStore 中判断是否已收藏\n    isCollected.value = userStore.isAlbumCollected(listInfo.value.id);\n  }\n  // 其他类型不支持收藏\n  else {\n    canCollect.value = false;\n    isCollected.value = false;\n  }\n};\n\n// 切换收藏状态（支持歌单和专辑）\nconst toggleCollect = async () => {\n  if (!listInfo.value?.id) return;\n\n  // 检查是否有真实登录权限\n  if (!hasPermission(true)) {\n    message.error(getLoginErrorMessage(true));\n    return;\n  }\n\n  const type = route.query.type as string;\n\n  try {\n    loadingList.value = true;\n    const tVal = isCollected.value ? 2 : 1; // 1:收藏, 2:取消收藏\n\n    // 根据类型调用不同的API\n    const response =\n      type === 'album'\n        ? await subscribeAlbum({\n            t: tVal,\n            id: listInfo.value.id\n          })\n        : await subscribePlaylist({\n            t: tVal,\n            id: listInfo.value.id\n          });\n\n    // 假设API返回格式是 { data: { code: number, msg?: string } }\n    const res = response.data;\n\n    if (res.code === 200) {\n      isCollected.value = !isCollected.value;\n      const msgKey = isCollected.value\n        ? 'comp.musicList.collectSuccess'\n        : 'comp.musicList.cancelCollectSuccess';\n      message.success(t(msgKey));\n\n      // 更新收藏状态\n      if (type === 'album') {\n        // 专辑：更新 store 中的收藏状态和专辑列表\n        if (isCollected.value) {\n          // 添加到收藏ID集合\n          userStore.addCollectedAlbum(listInfo.value.id);\n          // 添加到专辑列表\n          const albumData = {\n            id: listInfo.value.id,\n            name: listInfo.value.name,\n            picUrl: listInfo.value.picUrl || listInfo.value.coverImgUrl,\n            size: listInfo.value.size,\n            artist: listInfo.value.artist || listInfo.value.artists?.[0]\n          };\n          userStore.albumList.unshift(albumData);\n        } else {\n          // 从收藏ID集合中移除\n          userStore.removeCollectedAlbum(listInfo.value.id);\n          // 从专辑列表中移除\n          const index = userStore.albumList.findIndex((album) => album.id === listInfo.value.id);\n          if (index !== -1) {\n            userStore.albumList.splice(index, 1);\n          }\n        }\n        (listInfo.value as any).isSub = isCollected.value;\n      } else {\n        // 歌单：更新 listInfo 的状态\n        listInfo.value.subscribed = isCollected.value;\n      }\n    } else {\n      throw new Error(res.msg || t('comp.musicList.operationFailed'));\n    }\n  } catch (error) {\n    console.error(`收藏${type === 'album' ? '专辑' : '歌单'}失败:`, error);\n    message.error(t('comp.musicList.operationFailed'));\n  } finally {\n    loadingList.value = false;\n  }\n};\n\n// 播放全部\nconst handlePlayAll = () => {\n  if (displayedSongs.value.length === 0) return;\n  saveHistory();\n  // 如果有搜索关键词，只播放过滤后的歌曲\n  if (searchKeyword.value) {\n    playerStore.setPlayList(filteredSongs.value.map(formatSong));\n    playerStore.setPlay(formatSong(filteredSongs.value[0]));\n    return;\n  }\n\n  // 否则播放全部歌曲\n  // 使用setPlayList设置播放列表\n  playerStore.setPlayList(displayedSongs.value.map(formatSong));\n  // 使用setPlay开始播放第一首\n  playerStore.setPlay(formatSong(displayedSongs.value[0]));\n};\n\nconst saveHistory = () => {\n  if (listInfo.value?.id) {\n    if (isAlbum.value) {\n      // 保存专辑播放记录\n      addAlbum({\n        id: listInfo.value.id,\n        name: listInfo.value.name || '',\n        picUrl: listInfo.value.picUrl || listInfo.value.coverImgUrl || '',\n        size: listInfo.value.size || displayedSongs.value.length,\n        artist: listInfo.value.artist || listInfo.value.artists?.[0]\n      });\n    } else if (route.query.type === 'playlist') {\n      // 保存歌单播放记录\n      addPlaylist({\n        id: listInfo.value.id,\n        name: listInfo.value.name || '',\n        coverImgUrl: listInfo.value.coverImgUrl || listInfo.value.picUrl || '',\n        trackCount: listInfo.value.trackCount || displayedSongs.value.length,\n        playCount: listInfo.value.playCount,\n        creator: listInfo.value.creator\n      });\n    }\n  }\n};\n\n// 添加到播放列表末尾\nconst addToPlaylist = () => {\n  if (displayedSongs.value.length === 0) return;\n\n  // 获取当前播放列表\n  const currentList = playerStore.playList;\n\n  // 如果有搜索关键词，只添加过滤后的歌曲\n  const songsToAdd = searchKeyword.value ? filteredSongs.value : displayedSongs.value;\n\n  // 添加歌曲到播放列表(避免重复添加)\n  const newSongs = songsToAdd.filter((song) => !currentList.some((item) => item.id === song.id));\n\n  if (newSongs.length === 0) {\n    message.info(t('comp.musicList.songsAlreadyInPlaylist'));\n    return;\n  }\n\n  // 合并到当前播放列表末尾\n  const newList = [...currentList, ...newSongs.map(formatSong)];\n  playerStore.setPlayList(newList);\n\n  message.success(t('comp.musicList.addToPlaylistSuccess', { count: newSongs.length }));\n};\n\n// 多选下载相关状态和方法\nconst isSelecting = ref(false);\nconst selectedSongs = ref<number[]>([]);\nconst { isDownloading, batchDownloadMusic } = useDownload();\n\nconst startSelect = () => {\n  isSelecting.value = true;\n  selectedSongs.value = [];\n};\nconst cancelSelect = () => {\n  isSelecting.value = false;\n  selectedSongs.value = [];\n};\nconst handleSelect = (songId: number, selected: boolean) => {\n  if (selected) {\n    selectedSongs.value.push(songId);\n  } else {\n    selectedSongs.value = selectedSongs.value.filter((id) => id !== songId);\n  }\n};\nconst isAllSelected = computed(() => {\n  return (\n    filteredSongs.value.length > 0 && selectedSongs.value.length === filteredSongs.value.length\n  );\n});\nconst isIndeterminate = computed(() => {\n  return selectedSongs.value.length > 0 && selectedSongs.value.length < filteredSongs.value.length;\n});\nconst handleSelectAll = (checked: boolean) => {\n  if (checked) {\n    selectedSongs.value = filteredSongs.value.map((song) => song.id as number);\n  } else {\n    selectedSongs.value = [];\n  }\n};\nconst handleBatchDownload = async () => {\n  const selectedSongsList = selectedSongs.value\n    .map((songId) => filteredSongs.value.find((s) => s.id === songId))\n    .filter((song) => song) as SongResult[];\n  await batchDownloadMusic(selectedSongsList);\n  cancelSelect();\n};\n\n// 跳转到历史日推页面\nconst goToHistoryRecommend = () => {\n  router.push({ name: 'historyRecommend' });\n};\n</script>\n\n<style scoped lang=\"scss\">\n.music {\n  &-title {\n    @apply text-xl font-bold text-gray-900 dark:text-white;\n  }\n\n  &-total {\n    @apply text-sm font-normal text-gray-500 dark:text-gray-400;\n  }\n\n  &-page {\n    @apply h-full bg-light-100 dark:bg-dark-100 px-4 mr-2 rounded-2xl;\n  }\n\n  &-close {\n    @apply cursor-pointer text-gray-500 dark:text-white hover:text-gray-900 dark:hover:text-gray-300 flex gap-2 items-center transition;\n    .icon {\n      @apply text-3xl;\n    }\n  }\n\n  &-content {\n    @apply flex h-[calc(100%-60px)];\n  }\n\n  &-info {\n    @apply w-[25%] flex-shrink-0 pr-8 flex flex-col;\n\n    .music-cover {\n      @apply w-full aspect-square rounded-2xl overflow-hidden mb-4 min-h-[250px] relative;\n      .cover-img {\n        @apply w-full h-full object-cover;\n      }\n    }\n\n    .history-recommend-btn {\n      @apply mb-4 absolute bottom-1 right-4 z-10;\n      :deep(.n-button) {\n        @apply w-full bg-black bg-opacity-30 text-green-400 hover:bg-opacity-50 hover:text-green-500 transition-colors;\n      }\n    }\n\n    .creator-info {\n      @apply flex items-center mb-4;\n      .creator-name {\n        @apply ml-2 text-gray-700 dark:text-gray-300;\n      }\n    }\n\n    .music-desc {\n      @apply text-sm text-gray-600 dark:text-gray-400 leading-relaxed pr-4;\n    }\n  }\n\n  &-list {\n    @apply flex-grow min-h-0;\n    &-container {\n      @apply flex-grow min-h-0 flex flex-col relative;\n    }\n\n    &-content {\n      @apply min-h-[calc(80vh-60px)];\n    }\n  }\n}\n\n.search-container {\n  @apply max-w-md transition-all duration-300 ease-in-out;\n\n  &.search-expanded {\n    @apply w-52;\n  }\n\n  .search-button {\n    @apply w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:bg-light-300 dark:hover:bg-dark-300 transition-colors text-gray-500 dark:text-gray-400 hover:text-green-500;\n\n    .icon {\n      @apply text-lg;\n    }\n  }\n\n  :deep(.n-input) {\n    @apply bg-light-200 dark:bg-dark-200;\n  }\n}\n\n.no-result {\n  @apply text-center py-8 text-gray-500 dark:text-gray-400;\n}\n\n/* 虚拟列表样式 */\n.song-virtual-list {\n  :deep(.n-virtual-list__scroll) {\n    scrollbar-width: thin;\n    &::-webkit-scrollbar {\n      width: 4px;\n    }\n    &::-webkit-scrollbar-thumb {\n      @apply bg-gray-400 dark:bg-gray-600 rounded;\n    }\n  }\n}\n\n.mobile {\n  .music-page {\n    @apply px-4 overflow-hidden mr-0;\n  }\n\n  .music-content {\n    @apply flex-col;\n  }\n\n  .music-info {\n    @apply w-full pr-0 mb-2 flex flex-row;\n\n    .music-cover {\n      @apply w-[100px] h-[100px] rounded-lg overflow-hidden mb-4;\n    }\n    .music-detail {\n      @apply flex-1 ml-4;\n    }\n  }\n\n  .music-title {\n    @apply text-base;\n  }\n\n  .search-container {\n    @apply max-w-[50%];\n  }\n}\n\n.loading-more {\n  @apply text-center py-4 text-gray-500 dark:text-gray-400;\n}\n\n.double-item {\n  @apply mb-2 bg-light-200 bg-opacity-30 dark:bg-dark-200 dark:bg-opacity-20 rounded-3xl;\n}\n\n.mobile {\n  .music-info {\n    @apply hidden;\n  }\n}\n\n.layout-toggle {\n  .toggle-button {\n    @apply w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:bg-light-300 dark:hover:bg-dark-300 transition-colors;\n\n    .icon {\n      @apply text-lg text-gray-500 dark:text-gray-400 transition-colors;\n    }\n  }\n}\n\n.layout-toggle .toggle-button,\n.action-button {\n  @apply w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:bg-light-300 dark:hover:bg-dark-300 transition-colors text-gray-500 dark:text-gray-400;\n\n  .icon {\n    @apply text-lg;\n  }\n\n  &.collected {\n    .icon {\n      @apply text-red-500;\n    }\n  }\n\n  &.hover-green:hover {\n    .icon {\n      @apply text-green-500;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/mv/index.vue",
    "content": "<template>\n  <div class=\"mv-list\">\n    <div class=\"play-list-type\">\n      <n-scrollbar x-scrollable>\n        <div class=\"categories-wrapper\">\n          <span\n            v-for=\"(category, index) in categories\"\n            :key=\"category.value\"\n            class=\"play-list-type-item\"\n            :class=\"[\n              setAnimationClass('animate__bounceIn'),\n              { active: selectedCategory === category.value }\n            ]\"\n            :style=\"getAnimationDelay(index)\"\n            @click=\"selectedCategory = category.value\"\n          >\n            {{ category.label }}\n          </span>\n        </div>\n      </n-scrollbar>\n    </div>\n    <n-scrollbar :size=\"100\" @scroll=\"handleScroll\">\n      <div\n        v-loading=\"initLoading\"\n        class=\"mv-list-content\"\n        :class=\"setAnimationClass('animate__bounceInLeft')\"\n      >\n        <div\n          v-for=\"(item, index) in mvList\"\n          :key=\"item.id\"\n          class=\"mv-item\"\n          :class=\"setAnimationClass('animate__bounceIn')\"\n          :style=\"getAnimationDelay(index)\"\n        >\n          <div class=\"mv-item-img\" @click=\"handleShowMv(item, index)\">\n            <n-image\n              class=\"mv-item-img-img\"\n              :src=\"getImgUrl(item.cover, '320y180')\"\n              lazy\n              preview-disabled\n            />\n            <div class=\"top\">\n              <div class=\"play-count\">{{ formatNumber(item.playCount) }}</div>\n              <i class=\"iconfont icon-videofill\"></i>\n            </div>\n          </div>\n          <div class=\"mv-item-title\">{{ item.name }}</div>\n        </div>\n\n        <div v-if=\"loadingMore\" class=\"loading-more\">加载中...</div>\n        <div v-if=\"!hasMore && !initLoading\" class=\"no-more\">没有更多了</div>\n      </div>\n    </n-scrollbar>\n\n    <mv-player\n      v-model:show=\"showMv\"\n      :current-mv=\"playMvItem\"\n      :is-prev-disabled=\"isPrevDisabled\"\n      @next=\"playNextMv\"\n      @prev=\"playPrevMv\"\n    />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, ref, watch } from 'vue';\n\nimport { getAllMv, getTopMv } from '@/api/mv';\nimport MvPlayer from '@/components/MvPlayer.vue';\nimport { audioService } from '@/services/audioService';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { IMvItem } from '@/types/mv';\nimport { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';\n\ndefineOptions({\n  name: 'Mv'\n});\n\nconst showMv = ref(false);\nconst mvList = ref<Array<IMvItem>>([]);\nconst playMvItem = ref<IMvItem>();\nconst initLoading = ref(false);\nconst loadingMore = ref(false);\nconst currentIndex = ref(0);\nconst offset = ref(0);\nconst limit = ref(42);\nconst hasMore = ref(true);\n\nconst categories = [\n  { label: '全部', value: '全部' },\n  { label: '内地', value: '内地' },\n  { label: '港台', value: '港台' },\n  { label: '欧美', value: '欧美' },\n  { label: '日本', value: '日本' },\n  { label: '韩国', value: '韩国' }\n];\nconst selectedCategory = ref('全部');\n\nconst playerStore = usePlayerStore();\n\nwatch(selectedCategory, async () => {\n  offset.value = 0;\n  mvList.value = [];\n  hasMore.value = true;\n  await loadMvList();\n});\n\nconst getAnimationDelay = (index: number) => {\n  const currentPageIndex = index % limit.value;\n  return setAnimationDelay(currentPageIndex, 30);\n};\n\nonMounted(async () => {\n  await loadMvList();\n});\n\nconst handleShowMv = async (item: IMvItem, index: number) => {\n  playerStore.setIsPlay(false);\n  audioService.pause();\n  showMv.value = true;\n  currentIndex.value = index;\n  playMvItem.value = item;\n};\n\nconst playPrevMv = async (setLoading: (value: boolean) => void) => {\n  try {\n    if (currentIndex.value > 0) {\n      const prevItem = mvList.value[currentIndex.value - 1];\n      await handleShowMv(prevItem, currentIndex.value - 1);\n    }\n  } finally {\n    setLoading(false);\n  }\n};\n\nconst playNextMv = async (setLoading: (value: boolean) => void) => {\n  try {\n    if (currentIndex.value < mvList.value.length - 1) {\n      const nextItem = mvList.value[currentIndex.value + 1];\n      await handleShowMv(nextItem, currentIndex.value + 1);\n    } else if (hasMore.value) {\n      await loadMvList();\n      if (mvList.value.length > currentIndex.value + 1) {\n        const nextItem = mvList.value[currentIndex.value + 1];\n        await handleShowMv(nextItem, currentIndex.value + 1);\n      } else {\n        showMv.value = false;\n      }\n    } else {\n      showMv.value = false;\n    }\n  } catch (error) {\n    console.error('加载更多MV失败:', error);\n    showMv.value = false;\n  } finally {\n    setLoading(false);\n  }\n};\n\nconst loadMvList = async () => {\n  try {\n    if (!hasMore.value || loadingMore.value) return;\n    if (offset.value === 0) {\n      initLoading.value = true;\n    } else {\n      loadingMore.value = true;\n    }\n\n    const params = {\n      limit: limit.value,\n      offset: offset.value,\n      area: selectedCategory.value === '全部' ? '' : selectedCategory.value\n    };\n\n    const res = selectedCategory.value === '全部' ? await getTopMv(params) : await getAllMv(params);\n\n    const { data } = res.data;\n    mvList.value.push(...data);\n    hasMore.value = data.length === limit.value;\n    offset.value += limit.value;\n  } finally {\n    initLoading.value = false;\n    loadingMore.value = false;\n  }\n};\n\nconst handleScroll = (e: Event) => {\n  const target = e.target as Element;\n  const { scrollTop, clientHeight, scrollHeight } = target;\n  const threshold = 100;\n\n  if (scrollHeight - (scrollTop + clientHeight) < threshold) {\n    loadMvList();\n  }\n};\n\nconst isPrevDisabled = computed(() => currentIndex.value === 0);\n</script>\n\n<style scoped lang=\"scss\">\n.mv-list {\n  @apply h-full flex-1 flex flex-col overflow-hidden;\n\n  &-title {\n    @apply text-xl font-bold pb-2;\n    @apply text-gray-900 dark:text-white;\n  }\n\n  // 添加歌单分类样式\n  .play-list-type {\n    .title {\n      @apply text-lg font-bold mb-2;\n      @apply text-gray-900 dark:text-white;\n    }\n\n    .categories-wrapper {\n      @apply flex items-center py-2;\n      white-space: nowrap;\n    }\n\n    &-item {\n      @apply py-2 px-3 mr-3 inline-block rounded-xl cursor-pointer transition-all duration-300;\n      @apply bg-light dark:bg-black text-gray-900 dark:text-white;\n      @apply border border-gray-200 dark:border-gray-700;\n\n      &:hover {\n        @apply bg-green-50 dark:bg-green-900;\n      }\n\n      &.active {\n        @apply bg-green-500 border-green-500 text-white;\n      }\n    }\n  }\n\n  &-content {\n    @apply grid gap-4 pb-28 mt-2 pr-4;\n    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n  }\n\n  .mv-item {\n    @apply p-2 rounded-lg;\n    @apply bg-light dark:bg-black;\n    @apply border border-gray-200 dark:border-gray-700;\n\n    &-img {\n      @apply rounded-lg overflow-hidden relative;\n      aspect-ratio: 16/9;\n      line-height: 0;\n\n      &:hover img {\n        @apply hover:scale-110 transition-all duration-300 ease-in-out object-top;\n      }\n\n      &-img {\n        @apply w-full h-full object-cover rounded-lg overflow-hidden;\n      }\n\n      .top {\n        @apply absolute w-full h-full top-0 left-0 flex justify-center items-center transition-all duration-300 ease-in-out cursor-pointer;\n        @apply bg-black bg-opacity-60;\n        opacity: 0;\n\n        i {\n          @apply text-4xl text-white;\n        }\n\n        .play-count {\n          @apply absolute top-2 right-2 text-sm;\n          @apply text-white text-opacity-90;\n        }\n\n        &:hover {\n          opacity: 1;\n        }\n      }\n    }\n\n    &-title {\n      @apply mt-2 text-sm line-clamp-1;\n      @apply text-gray-900 dark:text-white;\n    }\n  }\n}\n\n.loading-more {\n  @apply text-center py-4 col-span-full;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.no-more {\n  @apply text-center py-4 col-span-full;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.mobile {\n  .mv-list-content {\n    @apply pl-4 pr-4;\n  }\n  .categories-wrapper {\n    @apply pl-4;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/playlist/ImportPlaylist.vue",
    "content": "<template>\n  <div class=\"import-playlist-page\">\n    <div class=\"import-header\" :class=\"setAnimationClass('animate__fadeInLeft')\">\n      <div class=\"import-header-left\">\n        <h2>{{ t('comp.playlist.import.title') }}</h2>\n        <div class=\"import-desc\">{{ t('comp.playlist.import.description') }}</div>\n      </div>\n    </div>\n\n    <div class=\"import-content\" :class=\"setAnimationClass('animate__fadeInUp')\">\n      <n-card class=\"import-card\">\n        <n-tabs type=\"line\" animated>\n          <!-- 链接导入 -->\n          <n-tab-pane name=\"link\" :tab=\"t('comp.playlist.import.linkTab')\">\n            <div class=\"tab-content\">\n              <div class=\"link-inputs\">\n                <div v-for=\"(link, index) in linkInputs\" :key=\"index\" class=\"link-row\">\n                  <n-input\n                    v-model:value=\"link.value\"\n                    :placeholder=\"t('comp.playlist.import.linkPlaceholder')\"\n                    class=\"link-input\"\n                  />\n                  <n-button\n                    quaternary\n                    circle\n                    type=\"error\"\n                    @click=\"removeLinkRow(index)\"\n                    v-if=\"linkInputs.length > 1\"\n                  >\n                    <template #icon>\n                      <i class=\"iconfont ri-delete-bin-line\"></i>\n                    </template>\n                  </n-button>\n                </div>\n                <div class=\"link-actions\">\n                  <n-button @click=\"addLinkRow\" secondary size=\"small\">\n                    <template #icon>\n                      <i class=\"iconfont ri-add-line\"></i>\n                    </template>\n                    {{ t('comp.playlist.import.addLinkButton') }}\n                  </n-button>\n                </div>\n              </div>\n              <div class=\"link-tips\">\n                <p>{{ t('comp.playlist.import.linkTips') }}</p>\n                <ul>\n                  <li>{{ t('comp.playlist.import.linkTip1') }}</li>\n                  <li>{{ t('comp.playlist.import.linkTip2') }}</li>\n                  <li>{{ t('comp.playlist.import.linkTip3') }}</li>\n                </ul>\n              </div>\n              <div class=\"action-buttons\">\n                <n-checkbox v-model:checked=\"importToStarPlaylist\">\n                  {{ t('comp.playlist.import.importToStarPlaylist') }}\n                </n-checkbox>\n                <n-input\n                  v-if=\"!importToStarPlaylist\"\n                  v-model:value=\"playlistName\"\n                  :placeholder=\"t('comp.playlist.import.playlistNamePlaceholder')\"\n                  class=\"playlist-name-input\"\n                />\n                <n-button\n                  type=\"primary\"\n                  :loading=\"importing\"\n                  :disabled=\"!isLinkInputValid\"\n                  @click=\"handleImportByLink\"\n                >\n                  {{ t('comp.playlist.import.importButton') }}\n                </n-button>\n              </div>\n            </div>\n          </n-tab-pane>\n\n          <!-- 文字导入 -->\n          <n-tab-pane name=\"text\" :tab=\"t('comp.playlist.import.textTab')\">\n            <div class=\"tab-content\">\n              <n-input\n                v-model:value=\"textInput\"\n                type=\"textarea\"\n                :placeholder=\"t('comp.playlist.import.textPlaceholder')\"\n                :rows=\"6\"\n              />\n              <div class=\"text-tips\">\n                <p>{{ t('comp.playlist.import.textTips') }}</p>\n                <p class=\"text-format\">{{ t('comp.playlist.import.textFormat') }}</p>\n              </div>\n              <div class=\"action-buttons\">\n                <n-checkbox v-model:checked=\"importToStarPlaylist\">\n                  {{ t('comp.playlist.import.importToStarPlaylist') }}\n                </n-checkbox>\n                <n-input\n                  v-if=\"!importToStarPlaylist\"\n                  v-model:value=\"playlistName\"\n                  :placeholder=\"t('comp.playlist.import.playlistNamePlaceholder')\"\n                  class=\"playlist-name-input\"\n                />\n                <n-button\n                  type=\"primary\"\n                  :loading=\"importing\"\n                  :disabled=\"!textInput.trim()\"\n                  @click=\"handleImportByText\"\n                >\n                  {{ t('comp.playlist.import.importButton') }}\n                </n-button>\n              </div>\n            </div>\n          </n-tab-pane>\n\n          <!-- 元数据导入 -->\n          <n-tab-pane name=\"local\" :tab=\"t('comp.playlist.import.localTab')\">\n            <div class=\"tab-content\">\n              <div class=\"metadata-inputs\">\n                <div v-for=\"(item, index) in localMetadata\" :key=\"index\" class=\"metadata-row\">\n                  <n-input\n                    v-model:value=\"item.name\"\n                    :placeholder=\"t('comp.playlist.import.songNamePlaceholder')\"\n                    class=\"metadata-input\"\n                  />\n                  <n-input\n                    v-model:value=\"item.artist\"\n                    :placeholder=\"t('comp.playlist.import.artistNamePlaceholder')\"\n                    class=\"metadata-input\"\n                  />\n                  <n-input\n                    v-model:value=\"item.album\"\n                    :placeholder=\"t('comp.playlist.import.albumNamePlaceholder')\"\n                    class=\"metadata-input\"\n                  />\n                  <n-button\n                    quaternary\n                    circle\n                    type=\"error\"\n                    @click=\"removeMetadataRow(index)\"\n                    v-if=\"localMetadata.length > 1\"\n                  >\n                    <template #icon>\n                      <i class=\"iconfont ri-delete-bin-line\"></i>\n                    </template>\n                  </n-button>\n                </div>\n                <div class=\"metadata-actions\">\n                  <n-button @click=\"addMetadataRow\" secondary size=\"small\">\n                    <template #icon>\n                      <i class=\"iconfont ri-add-line\"></i>\n                    </template>\n                    {{ t('comp.playlist.import.addSongButton') }}\n                  </n-button>\n                </div>\n              </div>\n              <div class=\"local-tips\">\n                <p>{{ t('comp.playlist.import.localTips') }}</p>\n              </div>\n              <div class=\"action-buttons\">\n                <n-checkbox v-model:checked=\"importToStarPlaylist\">\n                  {{ t('comp.playlist.import.importToStarPlaylist') }}\n                </n-checkbox>\n                <n-input\n                  v-if=\"!importToStarPlaylist\"\n                  v-model:value=\"playlistName\"\n                  :placeholder=\"t('comp.playlist.import.playlistNamePlaceholder')\"\n                  class=\"playlist-name-input\"\n                />\n                <n-button\n                  type=\"primary\"\n                  :loading=\"importing\"\n                  :disabled=\"!isLocalMetadataValid\"\n                  @click=\"handleImportByLocal\"\n                >\n                  {{ t('comp.playlist.import.importButton') }}\n                </n-button>\n              </div>\n            </div>\n          </n-tab-pane>\n        </n-tabs>\n      </n-card>\n\n      <!-- 导入状态 -->\n      <n-card v-if=\"taskId\" class=\"import-status-card\">\n        <div class=\"status-header\">\n          <h3>{{ t('comp.playlist.import.importStatus') }}</h3>\n          <n-button text @click=\"refreshStatus\">\n            <template #icon>\n              <i class=\"iconfont ri-refresh-line\"></i>\n            </template>\n            {{ t('comp.playlist.import.refresh') }}\n          </n-button>\n        </div>\n        <n-spin :show=\"checkingStatus\">\n          <div class=\"status-content\">\n            <div class=\"status-item\">\n              <span class=\"status-label\">{{ t('comp.playlist.import.taskId') }}:</span>\n              <span class=\"status-value\">{{ taskId }}</span>\n            </div>\n            <div class=\"status-item\">\n              <span class=\"status-label\">{{ t('comp.playlist.import.status') }}:</span>\n              <span class=\"status-value\" :class=\"`status-${taskStatus}`\">\n                {{ getStatusText(taskStatus) }}\n              </span>\n            </div>\n            <div v-if=\"taskStatus === 'success'\" class=\"status-item\">\n              <span class=\"status-label\">{{ t('comp.playlist.import.successCount') }}:</span>\n              <span class=\"status-value success-count\">{{ successCount }}</span>\n            </div>\n            <div v-if=\"taskStatus === 'failed'\" class=\"status-item\">\n              <span class=\"status-label\">{{ t('comp.playlist.import.failReason') }}:</span>\n              <span class=\"status-value fail-reason\">{{ failReason }}</span>\n            </div>\n          </div>\n        </n-spin>\n      </n-card>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useMessage } from 'naive-ui';\nimport { computed, onMounted, onUnmounted, ref } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport { getImportTaskStatus, importPlaylist } from '@/api/playlist';\nimport { setAnimationClass } from '@/utils';\n\nconst { t } = useI18n();\nconst message = useMessage();\n\n// 表单数据\nconst linkInputs = ref([{ value: '' }]);\nconst textInput = ref('');\nconst localMetadata = ref([{ name: '', artist: '', album: '' }]);\nconst playlistName = ref('');\nconst importToStarPlaylist = ref(false);\n\n// 链接相关函数\nconst addLinkRow = () => {\n  linkInputs.value.push({ value: '' });\n};\n\nconst removeLinkRow = (index: number) => {\n  linkInputs.value.splice(index, 1);\n};\n\n// 验证链接是否有效\nconst isLinkInputValid = computed(() => {\n  return linkInputs.value.some((item) => item.value.trim() !== '');\n});\n\n// 元数据相关函数\nconst addMetadataRow = () => {\n  localMetadata.value.push({ name: '', artist: '', album: '' });\n};\n\nconst removeMetadataRow = (index: number) => {\n  localMetadata.value.splice(index, 1);\n};\n\n// 验证元数据是否有效\nconst isLocalMetadataValid = computed(() => {\n  return localMetadata.value.some((item) => item.name.trim() !== '');\n});\n\n// 导入状态\nconst importing = ref(false);\nconst taskId = ref('');\nconst taskStatus = ref('');\nconst successCount = ref(0);\nconst failReason = ref('');\nconst checkingStatus = ref(false);\nconst statusCheckInterval = ref<number | null>(null);\n\n// 处理链接导入\nconst handleImportByLink = async () => {\n  if (!isLinkInputValid.value) {\n    message.warning(t('comp.playlist.import.emptyLinkWarning'));\n    return;\n  }\n\n  try {\n    importing.value = true;\n\n    // 处理链接格式\n    const links = linkInputs.value\n      .filter((link) => link.value.trim())\n      .map((link) => link.value.trim());\n\n    const encodedLinks = JSON.stringify(links);\n\n    const params: any = {\n      link: encodedLinks\n    };\n\n    if (importToStarPlaylist.value) {\n      params.importStarPlaylist = true;\n    } else if (playlistName.value) {\n      params.playlistName = playlistName.value;\n    }\n\n    const res = await importPlaylist(params);\n\n    if (res.data.code === 200) {\n      message.success(t('comp.playlist.import.importSuccess'));\n      taskId.value = res.data.data.taskId;\n      startStatusCheck();\n    } else {\n      message.error(res.data.message || t('comp.playlist.import.importFailed'));\n    }\n  } catch (error) {\n    console.error('导入歌单失败:', error);\n    message.error(t('comp.playlist.import.importFailed'));\n  } finally {\n    importing.value = false;\n  }\n};\n\n// 处理文字导入\nconst handleImportByText = async () => {\n  if (!textInput.value.trim()) {\n    message.warning(t('comp.playlist.import.emptyTextWarning'));\n    return;\n  }\n\n  try {\n    importing.value = true;\n\n    const encodedText = encodeURIComponent(textInput.value);\n\n    const params: any = {\n      text: encodedText\n    };\n\n    if (importToStarPlaylist.value) {\n      params.importStarPlaylist = true;\n    } else if (playlistName.value) {\n      params.playlistName = playlistName.value;\n    }\n\n    const res = await importPlaylist(params);\n\n    if (res.data.code === 200) {\n      message.success(t('comp.playlist.import.importSuccess'));\n      taskId.value = res.data.data.taskId;\n      startStatusCheck();\n    } else {\n      message.error(res.data.message || t('comp.playlist.import.importFailed'));\n    }\n  } catch (error) {\n    console.error('导入歌单失败:', error);\n    message.error(t('comp.playlist.import.importFailed'));\n  } finally {\n    importing.value = false;\n  }\n};\n\n// 处理元数据导入\nconst handleImportByLocal = async () => {\n  if (!isLocalMetadataValid.value) {\n    message.warning(t('comp.playlist.import.emptyLocalWarning'));\n    return;\n  }\n\n  try {\n    importing.value = true;\n\n    // 过滤掉空的行\n    const filteredData = localMetadata.value.filter((item) => item.name.trim() !== '');\n\n    const encodedLocal = JSON.stringify(filteredData);\n\n    const params: any = {\n      local: encodedLocal\n    };\n\n    if (importToStarPlaylist.value) {\n      params.importStarPlaylist = true;\n    } else if (playlistName.value) {\n      params.playlistName = playlistName.value;\n    }\n\n    const res = await importPlaylist(params);\n\n    if (res.data.code === 200) {\n      message.success(t('comp.playlist.import.importSuccess'));\n      taskId.value = res.data.data.taskId;\n      startStatusCheck();\n    } else {\n      message.error(res.data.message || t('comp.playlist.import.importFailed'));\n    }\n  } catch (error) {\n    console.error('导入歌单失败:', error);\n    message.error(t('comp.playlist.import.importFailed'));\n  } finally {\n    importing.value = false;\n  }\n};\n\n// 开始检查任务状态\nconst startStatusCheck = () => {\n  // 清除之前的定时器\n  if (statusCheckInterval.value) {\n    clearInterval(statusCheckInterval.value);\n  }\n\n  // 立即检查一次\n  checkTaskStatus();\n\n  // 设置定时检查\n  statusCheckInterval.value = window.setInterval(() => {\n    checkTaskStatus();\n  }, 3000); // 每3秒检查一次\n};\n\n// 检查任务状态\nconst checkTaskStatus = async () => {\n  if (!taskId.value) return;\n\n  try {\n    checkingStatus.value = true;\n    const res = await getImportTaskStatus(taskId.value);\n\n    if (res.data.code === 200) {\n      // 新的API返回格式处理\n      if (res.data.data.tasks && res.data.data.tasks.length > 0) {\n        const taskData = res.data.data.tasks[0];\n        // 将API返回的status映射到组件内部使用的taskStatus\n        const statusMap: Record<string, string> = {\n          PENDING: 'pending',\n          PROCESSING: 'processing',\n          COMPLETE: 'success',\n          FAILED: 'failed'\n        };\n\n        taskStatus.value = statusMap[taskData.status] || 'pending';\n\n        if (taskStatus.value === 'success') {\n          successCount.value = taskData.succCount || 0;\n          // 成功后停止检查\n          if (statusCheckInterval.value) {\n            clearInterval(statusCheckInterval.value);\n            statusCheckInterval.value = null;\n          }\n        } else if (taskStatus.value === 'failed') {\n          failReason.value = taskData.msg || t('comp.playlist.import.unknownError');\n          // 失败后停止检查\n          if (statusCheckInterval.value) {\n            clearInterval(statusCheckInterval.value);\n            statusCheckInterval.value = null;\n          }\n        }\n      }\n    }\n  } catch (error) {\n    console.error('检查任务状态失败:', error);\n  } finally {\n    checkingStatus.value = false;\n  }\n};\n\n// 手动刷新状态\nconst refreshStatus = () => {\n  checkTaskStatus();\n};\n\n// 获取状态文本\nconst getStatusText = (status: string) => {\n  switch (status) {\n    case 'pending':\n      return t('comp.playlist.import.statusPending');\n    case 'processing':\n      return t('comp.playlist.import.statusProcessing');\n    case 'success':\n      return t('comp.playlist.import.statusSuccess');\n    case 'failed':\n      return t('comp.playlist.import.statusFailed');\n    default:\n      return t('comp.playlist.import.statusUnknown');\n  }\n};\n\nonMounted(() => {\n  // 如果有任务ID，开始检查状态\n  if (taskId.value) {\n    startStatusCheck();\n  }\n});\n\nonUnmounted(() => {\n  // 清除定时器\n  if (statusCheckInterval.value) {\n    clearInterval(statusCheckInterval.value);\n    statusCheckInterval.value = null;\n  }\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.import-playlist-page {\n  @apply h-full overflow-auto pr-4;\n}\n\n.import-header {\n  @apply flex justify-between items-center mb-6;\n\n  .import-header-left {\n    h2 {\n      @apply text-2xl font-bold text-gray-900 dark:text-white mb-2;\n    }\n\n    .import-desc {\n      @apply text-sm text-gray-500 dark:text-gray-400;\n    }\n  }\n}\n\n.import-content {\n  @apply space-y-6;\n}\n\n.import-card {\n  @apply rounded-lg;\n\n  .tab-content {\n    @apply mt-4 space-y-4;\n  }\n\n  .link-tips,\n  .text-tips,\n  .local-tips {\n    @apply text-sm text-gray-500 dark:text-gray-400;\n\n    ul {\n      @apply list-disc pl-5 mt-2;\n    }\n  }\n\n  .text-format,\n  .local-format {\n    @apply mt-2 font-medium;\n  }\n\n  .code-example {\n    @apply mt-2 p-3 bg-gray-100 dark:bg-gray-800 rounded text-sm overflow-auto;\n  }\n\n  .link-inputs {\n    @apply space-y-3;\n\n    .link-row {\n      @apply flex items-center space-x-2;\n\n      .link-input {\n        @apply flex-1;\n      }\n    }\n\n    .link-actions {\n      @apply mt-3 flex justify-end;\n    }\n  }\n\n  .metadata-inputs {\n    @apply space-y-3;\n\n    .metadata-row {\n      @apply flex items-center space-x-2;\n\n      .metadata-input {\n        @apply flex-1;\n      }\n    }\n\n    .metadata-actions {\n      @apply mt-3 flex justify-end;\n    }\n  }\n\n  .action-buttons {\n    @apply flex items-center space-x-4 mt-6;\n\n    .playlist-name-input {\n      @apply max-w-xs;\n    }\n  }\n}\n\n.import-status-card {\n  @apply rounded-lg;\n\n  .status-header {\n    @apply flex justify-between items-center mb-4;\n\n    h3 {\n      @apply text-lg font-medium text-gray-900 dark:text-white;\n    }\n  }\n\n  .status-content {\n    @apply space-y-3;\n  }\n\n  .status-item {\n    @apply flex items-center;\n\n    .status-label {\n      @apply text-gray-500 dark:text-gray-400 w-24;\n    }\n\n    .status-value {\n      @apply font-medium;\n    }\n\n    .status-pending,\n    .status-processing {\n      @apply text-blue-500;\n    }\n\n    .status-success {\n      @apply text-green-500;\n    }\n\n    .status-failed {\n      @apply text-red-500;\n    }\n\n    .success-count {\n      @apply text-green-500;\n    }\n\n    .fail-reason {\n      @apply text-red-500;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/search/index.vue",
    "content": "<template>\n  <div class=\"search-page\">\n    <n-layout\n      v-if=\"isMobile ? !searchDetail : true\"\n      class=\"hot-search\"\n      :class=\"setAnimationClass('animate__fadeInDown')\"\n      :native-scrollbar=\"false\"\n    >\n      <div class=\"title\">{{ t('search.title.hotSearch') }}</div>\n      <div class=\"hot-search-list\">\n        <template v-for=\"(item, index) in hotSearchData?.data\" :key=\"index\">\n          <div\n            :class=\"setAnimationClass('animate__bounceInLeft')\"\n            :style=\"setAnimationDelay(index, 10)\"\n            class=\"hot-search-item\"\n            @click.stop=\"loadSearch(item.searchWord, 1)\"\n          >\n            <span class=\"hot-search-item-count\" :class=\"{ 'hot-search-item-count-3': index < 3 }\">{{\n              index + 1\n            }}</span>\n            {{ item.searchWord }}\n          </div>\n        </template>\n      </div>\n    </n-layout>\n    <!-- 搜索到的歌曲列表 -->\n    <n-layout\n      v-if=\"isMobile ? searchDetail : true\"\n      class=\"search-list\"\n      :class=\"setAnimationClass('animate__fadeInDown')\"\n      :native-scrollbar=\"false\"\n      @scroll=\"handleScroll\"\n    >\n      <div v-if=\"searchDetail\" class=\"title\">\n        <i\n          class=\"ri-arrow-left-s-line mr-1 cursor-pointer hover:text-gray-500 hover:scale-110\"\n          @click=\"searchDetail = null\"\n        ></i>\n        {{ hotKeyword }}\n        <div v-if=\"searchDetail?.songs?.length\" class=\"title-play-all\">\n          <div class=\"play-all-btn\" @click=\"handlePlayAll\">\n            <i class=\"ri-play-circle-fill\"></i>\n            <span>{{ t('search.button.playAll') }}</span>\n          </div>\n        </div>\n      </div>\n      <div v-loading=\"searchDetailLoading\" class=\"search-list-box\">\n        <template v-if=\"searchDetail\">\n          <!-- B站视频搜索结果 -->\n          <template v-if=\"searchType === SEARCH_TYPE.BILIBILI\">\n            <div\n              v-for=\"(item, index) in searchDetail?.bilibili\"\n              :key=\"item.bvid\"\n              :class=\"setAnimationClass('animate__bounceInRight')\"\n              :style=\"getSearchListAnimation(index)\"\n            >\n              <bilibili-item :item=\"item\" @play=\"handlePlayBilibili\" />\n            </div>\n            <div v-if=\"isLoadingMore\" class=\"loading-more\">\n              <n-spin size=\"small\" />\n              <span class=\"ml-2\">{{ t('search.loading.more') }}</span>\n            </div>\n            <div v-if=\"!hasMore && searchDetail\" class=\"no-more\">{{ t('search.noMore') }}</div>\n          </template>\n          <!-- 原有音乐搜索结果 -->\n          <template v-else>\n            <div\n              v-for=\"(item, index) in searchDetail?.songs\"\n              :key=\"item.id\"\n              :class=\"setAnimationClass('animate__bounceInRight')\"\n              :style=\"getSearchListAnimation(index)\"\n            >\n              <song-item :item=\"item\" @play=\"handlePlay\" :is-next=\"true\" />\n            </div>\n            <template v-for=\"(list, key) in searchDetail\">\n              <template v-if=\"key.toString() !== 'songs'\">\n                <div\n                  v-for=\"(item, index) in list\"\n                  :key=\"item.id\"\n                  class=\"mb-3\"\n                  :class=\"setAnimationClass('animate__bounceInRight')\"\n                  :style=\"getSearchListAnimation(index)\"\n                >\n                  <search-item :item=\"item\" />\n                </div>\n              </template>\n            </template>\n            <!-- 加载状态 -->\n            <div v-if=\"isLoadingMore\" class=\"loading-more\">\n              <n-spin size=\"small\" />\n              <span class=\"ml-2\">{{ t('search.loading.more') }}</span>\n            </div>\n            <div v-if=\"!hasMore && searchDetail\" class=\"no-more\">{{ t('search.noMore') }}</div>\n          </template>\n        </template>\n        <!-- 搜索历史 -->\n        <template v-else>\n          <div class=\"search-history\">\n            <div class=\"search-history-header title\">\n              <span>{{ t('search.title.searchHistory') }}</span>\n              <n-button text type=\"error\" @click=\"clearSearchHistory\">\n                <template #icon>\n                  <i class=\"ri-delete-bin-line\"></i>\n                </template>\n                {{ t('search.button.clear') }}\n              </n-button>\n            </div>\n            <div class=\"search-history-list\">\n              <n-tag\n                v-for=\"(item, index) in searchHistory\"\n                :key=\"index\"\n                :class=\"setAnimationClass('animate__bounceIn')\"\n                :style=\"getSearchListAnimation(index)\"\n                class=\"search-history-item\"\n                round\n                closable\n                @click=\"handleSearchHistory(item)\"\n                @close=\"handleCloseSearchHistory(item)\"\n              >\n                {{ item.keyword }}\n              </n-tag>\n            </div>\n          </div>\n        </template>\n      </div>\n    </n-layout>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useDateFormat } from '@vueuse/core';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport {\n  createSimpleBilibiliSong,\n  getBilibiliAudioUrl,\n  getBilibiliProxyUrl,\n  getBilibiliVideoDetail,\n  searchBilibili\n} from '@/api/bilibili';\nimport { getHotSearch } from '@/api/home';\nimport { getSearch } from '@/api/search';\nimport BilibiliItem from '@/components/common/BilibiliItem.vue';\nimport SearchItem from '@/components/common/SearchItem.vue';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { SEARCH_TYPE } from '@/const/bar-const';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { useSearchStore } from '@/store/modules/search';\nimport type { IBilibiliSearchResult } from '@/types/bilibili';\nimport type { IHotSearch } from '@/types/search';\nimport { isMobile, setAnimationClass, setAnimationDelay } from '@/utils';\n\ndefineOptions({\n  name: 'Search'\n});\n\nconst { t } = useI18n();\nconst route = useRoute();\nconst router = useRouter();\nconst playerStore = usePlayerStore();\nconst searchStore = useSearchStore();\n\nconst searchDetail = ref<any>();\nconst searchType = computed(() => searchStore.searchType as number);\nconst searchDetailLoading = ref(false);\nconst searchHistory = ref<Array<{ keyword: string; type: number }>>([]);\n\n// 添加分页相关的状态\nconst ITEMS_PER_PAGE = 30; // 每页数量\nconst page = ref(0);\nconst hasMore = ref(true);\nconst isLoadingMore = ref(false);\nconst currentKeyword = ref('');\n\nconst getSearchListAnimation = (index: number) => {\n  return setAnimationDelay(index % ITEMS_PER_PAGE, 50);\n};\n\n// 从 localStorage 加载搜索历史\nconst loadSearchHistory = () => {\n  const history = localStorage.getItem('searchHistory');\n  searchHistory.value = history ? JSON.parse(history) : [];\n};\n\n// 保存搜索历史，改为保存关键词和类型\nconst saveSearchHistory = (keyword: string, type: number) => {\n  if (!keyword) return;\n  const history = searchHistory.value;\n  // 移除重复的关键词\n  const index = history.findIndex((item) => item.keyword === keyword);\n  if (index > -1) {\n    history.splice(index, 1);\n  }\n  // 添加到开头\n  history.unshift({ keyword, type });\n  // 只保留最近的20条记录\n  if (history.length > 20) {\n    history.pop();\n  }\n  searchHistory.value = history;\n  localStorage.setItem('searchHistory', JSON.stringify(history));\n};\n\n// 清空搜索历史\nconst clearSearchHistory = () => {\n  searchHistory.value = [];\n  localStorage.removeItem('searchHistory');\n};\n\n// 删除搜索历史\nconst handleCloseSearchHistory = (item: { keyword: string; type: number }) => {\n  searchHistory.value = searchHistory.value.filter((h) => h.keyword !== item.keyword);\n  localStorage.setItem('searchHistory', JSON.stringify(searchHistory.value));\n};\n\n// 热搜列表\nconst hotSearchData = ref<IHotSearch>();\nconst loadHotSearch = async () => {\n  const { data } = await getHotSearch();\n  hotSearchData.value = data;\n};\n\nonMounted(() => {\n  loadHotSearch();\n  loadSearchHistory();\n  // 注意：路由参数的处理已经在 watch route.query 中处理了\n});\n\nconst hotKeyword = ref(route.query.keyword || t('search.title.searchList'));\n\nconst loadSearch = async (keywords: any, type: any = null, isLoadMore = false) => {\n  if (!keywords) return;\n\n  // 使用传入的类型或当前类型\n  const searchTypeToUse = type !== null ? type : searchType.value;\n\n  if (!isLoadMore) {\n    hotKeyword.value = keywords;\n    searchDetail.value = undefined;\n    page.value = 0;\n    hasMore.value = true;\n    currentKeyword.value = keywords;\n\n    // 保存搜索历史\n    saveSearchHistory(keywords, searchTypeToUse);\n\n    // 始终更新搜索框内容和类型\n    searchStore.searchType = searchTypeToUse;\n    searchStore.searchValue = keywords;\n  } else if (isLoadingMore.value || !hasMore.value) {\n    return;\n  }\n\n  if (isLoadMore) {\n    isLoadingMore.value = true;\n  } else {\n    searchDetailLoading.value = true;\n  }\n\n  try {\n    // B站搜索\n    if (searchTypeToUse === SEARCH_TYPE.BILIBILI) {\n      const response = await searchBilibili({\n        keyword: currentKeyword.value,\n        page: page.value + 1,\n        pagesize: ITEMS_PER_PAGE\n      });\n      console.log('response', response);\n\n      const bilibiliVideos = response.data.data.result.map((item: any) => ({\n        id: item.aid,\n        bvid: item.bvid,\n        title: item.title,\n        author: item.author,\n        pic: getBilibiliProxyUrl(item.pic),\n        duration: item.duration,\n        pubdate: item.pubdate,\n        description: item.description,\n        view: item.play,\n        danmaku: item.video_review\n      }));\n\n      if (isLoadMore && searchDetail.value) {\n        // 合并数据\n        searchDetail.value.bilibili = [...searchDetail.value.bilibili, ...bilibiliVideos];\n      } else {\n        searchDetail.value = {\n          bilibili: bilibiliVideos\n        };\n      }\n\n      // 判断是否还有更多数据\n      hasMore.value = bilibiliVideos.length === ITEMS_PER_PAGE;\n    }\n    // 音乐搜索\n    else {\n      const { data } = await getSearch({\n        keywords: currentKeyword.value,\n        type: searchTypeToUse,\n        limit: ITEMS_PER_PAGE,\n        offset: page.value * ITEMS_PER_PAGE\n      });\n\n      const songs = data.result.songs || [];\n      const albums = data.result.albums || [];\n      const mvs = (data.result.mvs || []).map((item: any) => ({\n        ...item,\n        picUrl: item.cover,\n        playCount: item.playCount,\n        desc: item.artists.map((artist: any) => artist.name).join('/'),\n        type: 'mv'\n      }));\n\n      const playlists = (data.result.playlists || []).map((item: any) => ({\n        ...item,\n        picUrl: item.coverImgUrl,\n        playCount: item.playCount,\n        desc: item.creator.nickname,\n        type: 'playlist'\n      }));\n\n      // songs map 替换属性\n      songs.forEach((item: any) => {\n        item.picUrl = item.al.picUrl;\n        item.artists = item.ar;\n      });\n      albums.forEach((item: any) => {\n        item.desc = `${item.artist.name} ${item.company} ${dateFormat(item.publishTime)}`;\n      });\n\n      if (isLoadMore && searchDetail.value) {\n        // 合并数据\n        searchDetail.value.songs = [...searchDetail.value.songs, ...songs];\n        searchDetail.value.albums = [...searchDetail.value.albums, ...albums];\n        searchDetail.value.mvs = [...searchDetail.value.mvs, ...mvs];\n        searchDetail.value.playlists = [...searchDetail.value.playlists, ...playlists];\n      } else {\n        searchDetail.value = {\n          songs,\n          albums,\n          mvs,\n          playlists\n        };\n      }\n\n      // 判断是否还有更多数据\n      hasMore.value =\n        songs.length === ITEMS_PER_PAGE ||\n        albums.length === ITEMS_PER_PAGE ||\n        mvs.length === ITEMS_PER_PAGE ||\n        playlists.length === ITEMS_PER_PAGE;\n    }\n\n    page.value++;\n  } catch (error) {\n    console.error(t('search.error.searchFailed'), error);\n  } finally {\n    searchDetailLoading.value = false;\n    isLoadingMore.value = false;\n  }\n};\n\nwatch(\n  () => searchStore.searchValue,\n  (value) => {\n    loadSearch(value);\n  }\n);\n\nwatch(\n  () => searchType.value,\n  () => {\n    if (searchStore.searchValue) {\n      loadSearch(searchStore.searchValue);\n    }\n  }\n);\n// 修改 store.state 的访问\nif (searchStore.searchValue) {\n  loadSearch(searchStore.searchValue);\n}\n\n// 修改 store.state 的设置\nsearchStore.searchValue = route.query.keyword as string;\n\nconst dateFormat = (time: any) => useDateFormat(time, 'YYYY.MM.DD').value;\n\n// 添加滚动处理函数\nconst handleScroll = (e: any) => {\n  const { scrollTop, scrollHeight, clientHeight } = e.target;\n  // 距离底部100px时加载更多\n  if (scrollTop + clientHeight >= scrollHeight - 100 && !isLoadingMore.value && hasMore.value) {\n    loadSearch(currentKeyword.value, null, true);\n  }\n};\n\nwatch(\n  () => route.query,\n  (query) => {\n    if (route.path === '/search' && query.keyword) {\n      const routeKeyword = query.keyword as string;\n      const routeType = query.type ? Number(query.type) : searchType.value;\n\n      // 更新搜索类型和值\n      searchStore.searchType = routeType;\n      searchStore.searchValue = routeKeyword;\n\n      // 加载搜索结果\n      loadSearch(routeKeyword, routeType);\n    }\n  },\n  { immediate: true }\n);\n\nconst handlePlay = (item: any) => {\n  // 添加到下一首\n  playerStore.addToNextPlay(item);\n};\n\n// 点击搜索历史\nconst handleSearchHistory = (item: { keyword: string; type: number }) => {\n  // 更新搜索类型\n  searchStore.searchType = item.type;\n  // 先更新搜索值到 store\n  searchStore.searchValue = item.keyword;\n  // 使用关键词和类型加载搜索\n  loadSearch(item.keyword, item.type);\n};\n\n// 处理B站视频播放\nconst handlePlayBilibili = async (item: IBilibiliSearchResult) => {\n  try {\n    // 获取视频详情以判断是否为单个视频\n    const videoDetail = await getBilibiliVideoDetail(item.bvid);\n    const pages = videoDetail.data.pages;\n\n    // 如果是单个视频（只有一个分P），直接播放\n    if (pages && pages.length === 1) {\n      // 获取音频URL并播放\n      const audioUrl = await getBilibiliAudioUrl(item.bvid, pages[0].cid);\n\n      // 使用公用方法创建播放项目\n      const playItem = createSimpleBilibiliSong(item, audioUrl);\n      playItem.bilibiliData = {\n        bvid: item.bvid,\n        cid: pages[0].cid\n      };\n\n      // 添加到播放列表并开始播放\n      playerStore.setPlay(playItem);\n    } else {\n      // 多P视频，跳转到详情页面\n      router.push(`/bilibili/${item.bvid}`);\n    }\n  } catch (error) {\n    console.error('处理B站视频播放失败:', error);\n    // 出错时回退到原来的逻辑，跳转详情页\n    router.push(`/bilibili/${item.bvid}`);\n  }\n};\n\nconst handlePlayAll = () => {\n  if (!searchDetail.value?.songs?.length) return;\n\n  // 设置播放列表为搜索结果中的所有歌曲\n  playerStore.setPlayList(searchDetail.value.songs);\n\n  // 开始播放第一首歌\n  if (searchDetail.value.songs[0]) {\n    playerStore.setPlay(searchDetail.value.songs[0]);\n  }\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.search-page {\n  @apply flex h-full;\n}\n\n.hot-search {\n  @apply mr-4 rounded-xl flex-1 overflow-hidden;\n  @apply bg-light-100 dark:bg-dark-100;\n  animation-duration: 0.2s;\n  min-width: 400px;\n  height: 100%;\n\n  &-list {\n    @apply pb-28;\n  }\n\n  &-item {\n    @apply px-4 py-3 text-lg rounded-xl cursor-pointer;\n    @apply text-gray-900 dark:text-white;\n    transition: all 0.3s ease;\n\n    &:hover {\n      @apply bg-light-100 dark:bg-dark-200;\n    }\n\n    &-count {\n      @apply inline-block ml-3 w-8;\n      @apply text-green-500;\n\n      &-3 {\n        @apply font-bold inline-block ml-3 w-8;\n        @apply text-red-500;\n      }\n    }\n  }\n}\n\n.search-list {\n  @apply flex-1 rounded-xl;\n  @apply bg-light-100 dark:bg-dark-100;\n  height: 100%;\n  animation-duration: 0.2s;\n\n  &-box {\n    @apply pb-28;\n  }\n}\n\n.title {\n  @apply text-xl font-bold my-2 mx-4 flex items-center;\n  @apply text-gray-900 dark:text-white;\n\n  &-play-all {\n    @apply ml-auto;\n  }\n}\n\n.play-all-btn {\n  @apply flex items-center gap-1 px-3 py-1 rounded-full cursor-pointer transition-all;\n  @apply text-sm font-normal text-gray-900 dark:text-white hover:bg-light-300 dark:hover:bg-dark-300 hover:text-green-500;\n\n  i {\n    @apply text-xl;\n  }\n}\n\n.search-history {\n  &-header {\n    @apply flex justify-between items-center mb-4;\n    @apply text-gray-900 dark:text-white;\n  }\n\n  &-list {\n    @apply flex flex-wrap gap-2 px-4;\n  }\n\n  &-item {\n    @apply cursor-pointer;\n    animation-duration: 0.2s;\n  }\n}\n\n.mobile {\n  .hot-search {\n    @apply mr-0 w-full;\n  }\n}\n\n.loading-more {\n  @apply flex justify-center items-center py-4;\n  @apply text-gray-500 dark:text-gray-400;\n}\n\n.no-more {\n  @apply text-center py-4;\n  @apply text-gray-500 dark:text-gray-400;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/set/SettingItem.vue",
    "content": "<template>\n  <div\n    class=\"flex items-center justify-between p-4 rounded-lg transition-all bg-light dark:bg-dark text-gray-900 dark:text-white border border-gray-200 dark:border-gray-700 hover:bg-gray-50 hover:dark:bg-gray-800\"\n    :class=\"[\n      // 移动端垂直布局\n      { 'max-md:flex-col max-md:items-start max-md:gap-3 max-md:p-3': !inline },\n      // 可点击样式\n      {\n        'cursor-pointer hover:text-green-500 hover:!bg-green-50 hover:dark:!bg-green-900/30':\n          clickable\n      },\n      customClass\n    ]\"\n    @click=\"handleClick\"\n  >\n    <!-- 左侧：标题和描述 -->\n    <div class=\"flex-1 min-w-0\">\n      <div class=\"text-base font-medium mb-1\">\n        <slot name=\"title\">{{ title }}</slot>\n      </div>\n      <div\n        v-if=\"description || $slots.description\"\n        class=\"text-sm text-gray-500 dark:text-gray-400\"\n      >\n        <slot name=\"description\">{{ description }}</slot>\n      </div>\n      <!-- 额外内容插槽 -->\n      <slot name=\"extra\"></slot>\n    </div>\n\n    <!-- 右侧：操作区 -->\n    <div\n      v-if=\"$slots.action || $slots.default\"\n      class=\"flex items-center gap-2 flex-shrink-0\"\n      :class=\"{ 'max-md:w-full max-md:justify-end': !inline }\"\n    >\n      <slot name=\"action\">\n        <slot></slot>\n      </slot>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\ndefineOptions({\n  name: 'SettingItem'\n});\n\ninterface Props {\n  /** 设置项标题 */\n  title?: string;\n  /** 设置项描述 */\n  description?: string;\n  /** 是否可点击 */\n  clickable?: boolean;\n  /** 是否保持水平布局（不响应移动端） */\n  inline?: boolean;\n  /** 自定义类名 */\n  customClass?: string;\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  title: '',\n  description: '',\n  clickable: false,\n  inline: false,\n  customClass: ''\n});\n\nconst emit = defineEmits<{\n  click: [event: MouseEvent];\n}>();\n\nconst handleClick = (event: MouseEvent) => {\n  if (props.clickable) {\n    emit('click', event);\n  }\n};\n</script>\n"
  },
  {
    "path": "src/renderer/views/set/SettingNav.vue",
    "content": "<template>\n  <div\n    class=\"w-32 h-full flex-shrink-0 border-r border-gray-200 dark:border-gray-700 bg-light dark:bg-dark\"\n  >\n    <div\n      v-for=\"section in sections\"\n      :key=\"section.id\"\n      class=\"px-4 py-2.5 cursor-pointer text-sm transition-colors duration-200 border-l-2\"\n      :class=\"[\n        currentSection === section.id\n          ? 'text-primary dark:text-white bg-gray-50 dark:bg-dark-100 !border-primary font-medium'\n          : 'text-gray-600 dark:text-gray-400 border-transparent hover:text-primary hover:dark:text-white hover:bg-gray-50 hover:dark:bg-dark-100 hover:border-gray-300'\n      ]\"\n      @click=\"handleClick(section.id)\"\n    >\n      {{ section.title }}\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\ndefineOptions({\n  name: 'SettingNav'\n});\n\nexport interface NavSection {\n  id: string;\n  title: string;\n}\n\ninterface Props {\n  /** 导航项列表 */\n  sections: NavSection[];\n  /** 当前激活的分组 ID */\n  currentSection: string;\n}\n\ndefineProps<Props>();\n\nconst emit = defineEmits<{\n  navigate: [sectionId: string];\n}>();\n\nconst handleClick = (sectionId: string) => {\n  emit('navigate', sectionId);\n};\n</script>\n"
  },
  {
    "path": "src/renderer/views/set/SettingSection.vue",
    "content": "<template>\n  <div :id=\"id\" :ref=\"setRef\" class=\"mb-6 scroll-mt-4\">\n    <!-- 分组标题 -->\n    <div class=\"text-base font-medium mb-4 text-gray-600 dark:text-white\">\n      <slot name=\"title\">{{ title }}</slot>\n    </div>\n\n    <!-- 设置项列表 -->\n    <div class=\"space-y-4 max-md:space-y-3\">\n      <slot></slot>\n    </div>\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { type ComponentPublicInstance } from 'vue';\n\ndefineOptions({\n  name: 'SettingSection'\n});\n\ninterface Props {\n  /** 分组 ID，用于导航定位 */\n  id?: string;\n  /** 分组标题 */\n  title?: string;\n}\n\nwithDefaults(defineProps<Props>(), {\n  id: '',\n  title: ''\n});\n\nconst emit = defineEmits<{\n  ref: [el: Element | null];\n}>();\n\n// 暴露 ref 给父组件\nconst setRef = (el: Element | ComponentPublicInstance | null) => {\n  emit('ref', el as Element | null);\n};\n</script>\n"
  },
  {
    "path": "src/renderer/views/set/index.vue",
    "content": "<template>\n  <div class=\"flex h-full\">\n    <!-- 左侧导航栏 -->\n    <setting-nav\n      v-if=\"!isMobile\"\n      :sections=\"navSections\"\n      :current-section=\"currentSection\"\n      @navigate=\"scrollToSection\"\n    />\n\n    <!-- 右侧内容区 -->\n    <n-scrollbar ref=\"scrollbarRef\" class=\"flex-1 h-full\" @scroll=\"handleScroll\">\n      <div class=\"p-4 pb-20 max-md:p-3 max-md:pb-24\">\n        <!-- 基础设置 -->\n        <setting-section\n          id=\"basic\"\n          :title=\"t('settings.sections.basic')\"\n          @ref=\"(el) => (sectionRefs.basic = el as HTMLElement | null)\"\n        >\n          <!-- 主题设置 -->\n          <setting-item\n            :title=\"t('settings.basic.themeMode')\"\n            :description=\"t('settings.basic.themeModeDesc')\"\n          >\n            <template #action>\n              <div class=\"flex items-center gap-3 max-md:flex-wrap\">\n                <div class=\"flex items-center gap-2\">\n                  <n-switch v-model:value=\"setData.autoTheme\" @update:value=\"handleAutoThemeChange\">\n                    <template #checked><i class=\"ri-smartphone-line\"></i></template>\n                    <template #unchecked><i class=\"ri-settings-line\"></i></template>\n                  </n-switch>\n                  <span class=\"text-sm text-gray-500 max-md:hidden\">\n                    {{\n                      setData.autoTheme\n                        ? t('settings.basic.autoTheme')\n                        : t('settings.basic.manualTheme')\n                    }}\n                  </span>\n                </div>\n                <n-switch\n                  v-model:value=\"isDarkTheme\"\n                  :disabled=\"setData.autoTheme\"\n                  :class=\"{ 'opacity-50': setData.autoTheme }\"\n                >\n                  <template #checked><i class=\"ri-moon-line\"></i></template>\n                  <template #unchecked><i class=\"ri-sun-line\"></i></template>\n                </n-switch>\n              </div>\n            </template>\n          </setting-item>\n\n          <!-- 语言设置 -->\n          <setting-item\n            :title=\"t('settings.basic.language')\"\n            :description=\"t('settings.basic.languageDesc')\"\n          >\n            <language-switcher />\n          </setting-item>\n\n          <!-- 平板模式 -->\n          <setting-item\n            v-if=\"!isElectron\"\n            :title=\"t('settings.basic.tabletMode')\"\n            :description=\"t('settings.basic.tabletModeDesc')\"\n          >\n            <n-switch v-model:value=\"setData.tabletMode\">\n              <template #checked><i class=\"ri-tablet-line\"></i></template>\n              <template #unchecked><i class=\"ri-smartphone-line\"></i></template>\n            </n-switch>\n          </setting-item>\n\n          <!-- 翻译引擎 -->\n          <setting-item\n            :title=\"t('settings.translationEngine')\"\n            :description=\"t('settings.translationEngine')\"\n          >\n            <n-select\n              v-model:value=\"setData.lyricTranslationEngine\"\n              :options=\"translationEngineOptions\"\n              class=\"w-40 max-md:w-full\"\n            />\n          </setting-item>\n\n          <!-- 字体设置 -->\n          <setting-item\n            v-if=\"isElectron\"\n            :title=\"t('settings.basic.font')\"\n            :description=\"t('settings.basic.fontDesc')\"\n          >\n            <template #action>\n              <div class=\"flex gap-2 max-md:flex-col max-md:w-full\">\n                <n-radio-group v-model:value=\"setData.fontScope\" class=\"mt-2\">\n                  <n-radio key=\"global\" value=\"global\">{{\n                    t('settings.basic.fontScope.global')\n                  }}</n-radio>\n                  <n-radio key=\"lyric\" value=\"lyric\">{{\n                    t('settings.basic.fontScope.lyric')\n                  }}</n-radio>\n                </n-radio-group>\n                <n-select\n                  v-model:value=\"selectedFonts\"\n                  :options=\"systemFonts\"\n                  filterable\n                  multiple\n                  placeholder=\"选择字体\"\n                  class=\"w-[300px] max-md:w-full\"\n                  :render-label=\"renderFontLabel\"\n                />\n              </div>\n            </template>\n          </setting-item>\n\n          <!-- 字体预览 -->\n          <div\n            v-if=\"selectedFonts.length > 0\"\n            class=\"mt-4 p-4 max-md:p-3 rounded-lg bg-gray-50 dark:bg-dark-100 border border-gray-200 dark:border-gray-700\"\n          >\n            <div class=\"text-sm font-medium mb-3 text-gray-600 dark:text-gray-300\">\n              {{ t('settings.basic.fontPreview.title') }}\n            </div>\n            <div class=\"space-y-3\" :style=\"{ fontFamily: setData.fontFamily }\">\n              <div v-for=\"preview in fontPreviews\" :key=\"preview.key\" class=\"flex flex-col gap-1\">\n                <div class=\"text-xs text-gray-500 dark:text-gray-400\">\n                  {{ t(`settings.basic.fontPreview.${preview.key}`) }}\n                </div>\n                <div\n                  class=\"text-base text-gray-900 dark:text-gray-100 p-2 rounded bg-white dark:bg-dark border border-gray-200 dark:border-gray-700\"\n                >\n                  {{ t(`settings.basic.fontPreview.${preview.key}Text`) }}\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <!-- Token管理 -->\n          <setting-item :title=\"t('settings.basic.tokenManagement')\">\n            <template #description>\n              <div class=\"text-sm text-gray-500 mb-2\">\n                {{ t('settings.basic.tokenStatus') }}:\n                {{ currentToken ? t('settings.basic.tokenSet') : t('settings.basic.tokenNotSet') }}\n              </div>\n              <div v-if=\"currentToken\" class=\"text-xs text-gray-400 mb-2 font-mono break-all\">\n                {{ currentToken.substring(0, 50) }}...\n              </div>\n            </template>\n            <template #action>\n              <div class=\"flex gap-2\">\n                <n-button size=\"small\" @click=\"showTokenModal = true\">\n                  {{\n                    currentToken ? t('settings.basic.modifyToken') : t('settings.basic.setToken')\n                  }}\n                </n-button>\n                <n-button v-if=\"currentToken\" size=\"small\" type=\"error\" @click=\"clearToken\">\n                  {{ t('settings.basic.clearToken') }}\n                </n-button>\n              </div>\n            </template>\n          </setting-item>\n\n          <!-- 动画设置 -->\n          <setting-item :title=\"t('settings.basic.animation')\">\n            <template #description>\n              <div class=\"flex items-center gap-2\">\n                <n-switch v-model:value=\"setData.noAnimate\">\n                  <template #checked>{{ t('common.off') }}</template>\n                  <template #unchecked>{{ t('common.on') }}</template>\n                </n-switch>\n                <span>{{ t('settings.basic.animationDesc') }}</span>\n              </div>\n            </template>\n            <template #action>\n              <div class=\"flex items-center gap-2\">\n                <span v-if=\"!isMobile\" class=\"text-sm text-gray-400\"\n                  >{{ setData.animationSpeed }}x</span\n                >\n                <div class=\"w-40 max-md:w-auto flex justify-end\">\n                  <n-slider\n                    v-if=\"!isMobile\"\n                    v-model:value=\"setData.animationSpeed\"\n                    :min=\"0.1\"\n                    :max=\"3\"\n                    :step=\"0.1\"\n                    :marks=\"animationSpeedMarks\"\n                    :disabled=\"setData.noAnimate\"\n                  />\n                  <n-input-number\n                    v-else\n                    v-model:value=\"setData.animationSpeed\"\n                    :min=\"0.1\"\n                    :max=\"3\"\n                    :step=\"0.1\"\n                    :disabled=\"setData.noAnimate\"\n                    button-placement=\"both\"\n                    class=\"w-[100px]\"\n                  />\n                </div>\n              </div>\n            </template>\n          </setting-item>\n\n          <!-- GPU加速 -->\n          <setting-item v-if=\"isElectron\" :title=\"t('settings.basic.gpuAcceleration')\">\n            <template #description>\n              <div class=\"text-sm text-gray-500 mb-2\">\n                {{ t('settings.basic.gpuAccelerationDesc') }}\n              </div>\n              <div v-if=\"gpuAccelerationChanged\" class=\"text-xs text-amber-500\">\n                <i class=\"ri-information-line mr-1\"></i>\n                {{ t('settings.basic.gpuAccelerationRestart') }}\n              </div>\n            </template>\n            <n-switch\n              v-model:value=\"setData.enableGpuAcceleration\"\n              @update:value=\"handleGpuAccelerationChange\"\n            >\n              <template #checked><i class=\"ri-cpu-line\"></i></template>\n              <template #unchecked><i class=\"ri-cpu-line\"></i></template>\n            </n-switch>\n          </setting-item>\n        </setting-section>\n\n        <!-- 播放设置 -->\n        <setting-section\n          id=\"playback\"\n          :title=\"t('settings.sections.playback')\"\n          @ref=\"(el) => (sectionRefs.playback = el as HTMLElement | null)\"\n        >\n          <!-- 音质设置 -->\n          <setting-item\n            :title=\"t('settings.playback.quality')\"\n            :description=\"t('settings.playback.qualityDesc')\"\n          >\n            <n-select\n              v-model:value=\"setData.musicQuality\"\n              :options=\"qualityOptions\"\n              class=\"w-40 max-md:w-full\"\n            />\n          </setting-item>\n\n          <!-- 会员购买链接 -->\n          <div\n            class=\"p-3 max-md:p-2 bg-light-100 dark:bg-dark-100 rounded-lg text-sm max-md:text-xs\"\n          >\n            <div>大家还是需要支持正版，本软件只做开源探讨</div>\n            <div class=\"mt-2\">各大音乐会员购买链接</div>\n            <div class=\"flex gap-4 max-md:gap-2 flex-wrap mt-1\">\n              <a\n                v-for=\"link in memberLinks\"\n                :key=\"link.url\"\n                class=\"text-green-400 hover:text-green-500\"\n                :href=\"link.url\"\n                target=\"_blank\"\n              >\n                {{ link.name }}\n              </a>\n            </div>\n          </div>\n\n          <!-- 音源设置 -->\n          <setting-item v-if=\"isElectron\" :title=\"t('settings.playback.musicSources')\">\n            <template #description>\n              <div class=\"flex items-center gap-2\">\n                <n-switch v-model:value=\"setData.enableMusicUnblock\">\n                  <template #checked>{{ t('common.on') }}</template>\n                  <template #unchecked>{{ t('common.off') }}</template>\n                </n-switch>\n                <span>{{ t('settings.playback.musicUnblockEnableDesc') }}</span>\n              </div>\n              <div v-if=\"setData.enableMusicUnblock\" class=\"mt-2 text-sm\">\n                <span class=\"text-gray-500\">{{ t('settings.playback.selectedMusicSources') }}</span>\n                <span v-if=\"musicSources.length > 0\" class=\"text-gray-400\">{{\n                  musicSources.join(', ')\n                }}</span>\n                <span v-else class=\"text-red-500 text-xs\">{{\n                  t('settings.playback.noMusicSources')\n                }}</span>\n              </div>\n            </template>\n            <n-button\n              size=\"small\"\n              :disabled=\"!setData.enableMusicUnblock\"\n              @click=\"showMusicSourcesModal = true\"\n            >\n              {{ t('settings.playback.configureMusicSources') }}\n            </n-button>\n          </setting-item>\n\n          <!-- 状态栏显示 -->\n          <setting-item\n            v-if=\"platform === 'darwin'\"\n            :title=\"t('settings.playback.showStatusBar')\"\n            :description=\"t('settings.playback.showStatusBarContent')\"\n          >\n            <n-switch v-model:value=\"setData.showTopAction\">\n              <template #checked>{{ t('common.on') }}</template>\n              <template #unchecked>{{ t('common.off') }}</template>\n            </n-switch>\n          </setting-item>\n\n          <!-- 自动播放 -->\n          <setting-item\n            :title=\"t('settings.playback.autoPlay')\"\n            :description=\"t('settings.playback.autoPlayDesc')\"\n          >\n            <n-switch v-model:value=\"setData.autoPlay\">\n              <template #checked>{{ t('common.on') }}</template>\n              <template #unchecked>{{ t('common.off') }}</template>\n            </n-switch>\n          </setting-item>\n        </setting-section>\n\n        <!-- 应用设置 -->\n        <setting-section\n          v-if=\"isElectron\"\n          id=\"application\"\n          :title=\"t('settings.sections.application')\"\n          @ref=\"(el) => (sectionRefs.application = el as HTMLElement | null)\"\n        >\n          <!-- 关闭行为 -->\n          <setting-item\n            :title=\"t('settings.application.closeAction')\"\n            :description=\"t('settings.application.closeActionDesc')\"\n          >\n            <n-select\n              v-model:value=\"setData.closeAction\"\n              :options=\"closeActionOptions\"\n              class=\"w-40 max-md:w-full\"\n            />\n          </setting-item>\n\n          <!-- 快捷键 -->\n          <setting-item\n            :title=\"t('settings.application.shortcut')\"\n            :description=\"t('settings.application.shortcutDesc')\"\n          >\n            <n-button size=\"small\" @click=\"showShortcutModal = true\">{{\n              t('common.configure')\n            }}</n-button>\n          </setting-item>\n\n          <!-- 下载管理 -->\n          <setting-item v-if=\"isElectron\" :title=\"t('settings.application.download')\">\n            <template #description>\n              <n-switch v-model:value=\"setData.alwaysShowDownloadButton\" class=\"mr-2\">\n                <template #checked>{{ t('common.show') }}</template>\n                <template #unchecked>{{ t('common.hide') }}</template>\n              </n-switch>\n              {{ t('settings.application.downloadDesc') }}\n            </template>\n            <n-button size=\"small\" @click=\"settingsStore.showDownloadDrawer = true\">\n              {{ t('settings.application.download') }}\n            </n-button>\n          </setting-item>\n\n          <!-- 无限下载 -->\n          <setting-item :title=\"t('settings.application.unlimitedDownload')\">\n            <template #description>\n              <n-switch v-model:value=\"setData.unlimitedDownload\" class=\"mr-2\">\n                <template #checked>{{ t('common.on') }}</template>\n                <template #unchecked>{{ t('common.off') }}</template>\n              </n-switch>\n              {{ t('settings.application.unlimitedDownloadDesc') }}\n            </template>\n          </setting-item>\n\n          <!-- 下载路径 -->\n          <setting-item :title=\"t('settings.application.downloadPath')\">\n            <template #description>\n              <span class=\"break-all\">{{\n                setData.downloadPath || t('settings.application.downloadPathDesc')\n              }}</span>\n            </template>\n            <template #action>\n              <div class=\"flex items-center gap-2\">\n                <n-button size=\"small\" @click=\"openDownloadPath\">{{ t('common.open') }}</n-button>\n                <n-button size=\"small\" @click=\"selectDownloadPath\">{{\n                  t('common.modify')\n                }}</n-button>\n              </div>\n            </template>\n          </setting-item>\n\n          <!-- 远程控制 -->\n          <setting-item\n            :title=\"t('settings.application.remoteControl')\"\n            :description=\"t('settings.application.remoteControlDesc')\"\n          >\n            <n-button size=\"small\" @click=\"showRemoteControlModal = true\">{{\n              t('common.configure')\n            }}</n-button>\n          </setting-item>\n        </setting-section>\n\n        <!-- 网络设置 -->\n        <setting-section\n          v-if=\"isElectron\"\n          id=\"network\"\n          :title=\"t('settings.sections.network')\"\n          @ref=\"(el) => (sectionRefs.network = el as HTMLElement | null)\"\n        >\n          <!-- API端口 -->\n          <setting-item\n            :title=\"t('settings.network.apiPort')\"\n            :description=\"t('settings.network.apiPortDesc')\"\n          >\n            <n-input-number v-model:value=\"setData.musicApiPort\" class=\"max-md:w-32\" />\n          </setting-item>\n\n          <!-- 代理设置 -->\n          <setting-item\n            :title=\"t('settings.network.proxy')\"\n            :description=\"t('settings.network.proxyDesc')\"\n          >\n            <template #action>\n              <div class=\"flex items-center gap-2\">\n                <n-switch v-model:value=\"setData.proxyConfig.enable\">\n                  <template #checked>{{ t('common.on') }}</template>\n                  <template #unchecked>{{ t('common.off') }}</template>\n                </n-switch>\n                <n-button size=\"small\" @click=\"showProxyModal = true\">{{\n                  t('common.configure')\n                }}</n-button>\n              </div>\n            </template>\n          </setting-item>\n\n          <!-- 真实IP -->\n          <setting-item\n            :title=\"t('settings.network.realIP')\"\n            :description=\"t('settings.network.realIPDesc')\"\n          >\n            <template #action>\n              <div class=\"flex items-center gap-2 max-md:flex-wrap\">\n                <n-switch v-model:value=\"setData.enableRealIP\">\n                  <template #checked>{{ t('common.on') }}</template>\n                  <template #unchecked>{{ t('common.off') }}</template>\n                </n-switch>\n                <n-input\n                  v-if=\"setData.enableRealIP\"\n                  v-model:value=\"setData.realIP\"\n                  placeholder=\"realIP\"\n                  class=\"w-[200px] max-md:w-full\"\n                  @blur=\"validateAndSaveRealIP\"\n                />\n              </div>\n            </template>\n          </setting-item>\n        </setting-section>\n\n        <!-- 系统管理 -->\n        <setting-section\n          v-if=\"isElectron\"\n          id=\"system\"\n          :title=\"t('settings.sections.system')\"\n          @ref=\"(el) => (sectionRefs.system = el as HTMLElement | null)\"\n        >\n          <!-- 清除缓存 -->\n          <setting-item\n            :title=\"t('settings.system.cache')\"\n            :description=\"t('settings.system.cacheDesc')\"\n          >\n            <n-button size=\"small\" @click=\"showClearCacheModal = true\">{{\n              t('settings.system.cacheDesc')\n            }}</n-button>\n          </setting-item>\n\n          <!-- 重启应用 -->\n          <setting-item\n            :title=\"t('settings.system.restart')\"\n            :description=\"t('settings.system.restartDesc')\"\n          >\n            <n-button size=\"small\" @click=\"restartApp\">{{ t('settings.system.restart') }}</n-button>\n          </setting-item>\n        </setting-section>\n\n        <!-- 关于 -->\n        <setting-section\n          id=\"about\"\n          :title=\"t('settings.sections.about')\"\n          @ref=\"(el) => (sectionRefs.about = el as HTMLElement | null)\"\n        >\n          <!-- 版本信息 -->\n          <setting-item :title=\"t('settings.about.version')\">\n            <template #description>\n              {{ updateInfo.currentVersion }}\n              <n-tag v-if=\"updateInfo.hasUpdate\" type=\"success\" class=\"ml-2\">\n                {{ t('settings.about.hasUpdate') }} {{ updateInfo.latestVersion }}\n              </n-tag>\n            </template>\n            <template #action>\n              <div class=\"flex items-center gap-2 flex-wrap\">\n                <n-button size=\"small\" :loading=\"checking\" @click=\"checkForUpdates(true)\">\n                  {{ checking ? t('settings.about.checking') : t('settings.about.checkUpdate') }}\n                </n-button>\n                <n-button v-if=\"updateInfo.hasUpdate\" size=\"small\" @click=\"openReleasePage\">\n                  {{ t('settings.about.gotoUpdate') }}\n                </n-button>\n              </div>\n            </template>\n          </setting-item>\n\n          <!-- 作者信息 -->\n          <setting-item\n            :title=\"t('settings.about.author')\"\n            :description=\"t('settings.about.authorDesc')\"\n            clickable\n            @click=\"openAuthor\"\n          >\n            <n-button size=\"small\" @click.stop=\"openAuthor\">\n              <i class=\"ri-github-line mr-1\"></i>{{ t('settings.about.gotoGithub') }}\n            </n-button>\n          </setting-item>\n        </setting-section>\n\n        <!-- 捐赠支持 -->\n        <setting-section\n          id=\"donation\"\n          :title=\"t('settings.sections.donation')\"\n          @ref=\"(el) => (sectionRefs.donation = el as HTMLElement | null)\"\n        >\n          <setting-item\n            :title=\"t('settings.sections.donation')\"\n            :description=\"t('donation.message')\"\n          >\n            <n-button text @click=\"toggleDonationList\">\n              <template #icon>\n                <i :class=\"isDonationListVisible ? 'ri-eye-line' : 'ri-eye-off-line'\" />\n              </template>\n              {{ isDonationListVisible ? t('common.hide') : t('common.show') }}\n            </n-button>\n          </setting-item>\n          <donation-list v-if=\"isDonationListVisible\" />\n        </setting-section>\n      </div>\n      <play-bottom />\n    </n-scrollbar>\n\n    <!-- 弹窗组件 -->\n    <template v-if=\"isElectron\">\n      <shortcut-settings v-model:show=\"showShortcutModal\" @change=\"handleShortcutsChange\" />\n      <proxy-settings\n        v-model:show=\"showProxyModal\"\n        :config=\"proxyForm\"\n        @confirm=\"handleProxyConfirm\"\n      />\n      <music-source-settings v-model:show=\"showMusicSourcesModal\" v-model:sources=\"musicSources\" />\n      <remote-control-setting v-model:visible=\"showRemoteControlModal\" />\n    </template>\n\n    <cookie-settings-modal\n      v-model:show=\"showTokenModal\"\n      :initial-value=\"currentToken\"\n      @save=\"handleTokenSave\"\n    />\n    <clear-cache-settings v-model:show=\"showClearCacheModal\" @confirm=\"clearCache\" />\n  </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useDebounceFn } from '@vueuse/core';\nimport { useMessage } from 'naive-ui';\nimport { computed, h, nextTick, onMounted, onUnmounted, reactive, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\n\nimport localData from '@/../main/set.json';\nimport { getUserDetail } from '@/api/login';\nimport DonationList from '@/components/common/DonationList.vue';\nimport PlayBottom from '@/components/common/PlayBottom.vue';\nimport LanguageSwitcher from '@/components/LanguageSwitcher.vue';\nimport ClearCacheSettings from '@/components/settings/ClearCacheSettings.vue';\nimport CookieSettingsModal from '@/components/settings/CookieSettingsModal.vue';\nimport MusicSourceSettings from '@/components/settings/MusicSourceSettings.vue';\nimport ProxySettings from '@/components/settings/ProxySettings.vue';\nimport RemoteControlSetting from '@/components/settings/ServerSetting.vue';\nimport ShortcutSettings from '@/components/settings/ShortcutSettings.vue';\nimport { useSettingsStore } from '@/store/modules/settings';\nimport { useUserStore } from '@/store/modules/user';\nimport { type Platform } from '@/types/music';\nimport { isElectron, isMobile } from '@/utils';\nimport { openDirectory, selectDirectory } from '@/utils/fileOperation';\nimport { checkUpdate, UpdateResult } from '@/utils/update';\n\nimport config from '../../../../package.json';\nimport SettingItem from './SettingItem.vue';\nimport SettingNav from './SettingNav.vue';\nimport SettingSection from './SettingSection.vue';\n\n// ==================== 常量配置 ====================\nconst ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'kuwo', 'pyncmd', 'bilibili'];\n\nconst memberLinks = [\n  { name: '网易云音乐会员', url: 'https://music.163.com/store/vip' },\n  { name: 'QQ音乐会员', url: 'https://y.qq.com/portal/vipportal/' },\n  { name: '酷狗音乐会员', url: 'https://vip.kugou.com/' }\n];\n\nconst fontPreviews = [\n  { key: 'chinese' },\n  { key: 'english' },\n  { key: 'japanese' },\n  { key: 'korean' }\n];\n\n// ==================== 平台和Store ====================\nconst platform = window.electron ? window.electron.ipcRenderer.sendSync('get-platform') : 'web';\nconst settingsStore = useSettingsStore();\nconst userStore = useUserStore();\nconst message = useMessage();\nconst { t } = useI18n();\n\n// ==================== 设置数据管理 ====================\nconst saveSettings = useDebounceFn((data) => {\n  settingsStore.setSetData(data);\n}, 500);\n\nconst localSetData = ref({ ...settingsStore.setData });\n\nconst setData = computed({\n  get: () => localSetData.value,\n  set: (newData) => {\n    localSetData.value = newData;\n  }\n});\n\nwatch(\n  () => localSetData.value,\n  (newValue) => saveSettings(newValue),\n  { deep: true }\n);\n\nwatch(\n  () => settingsStore.setData,\n  (newValue) => {\n    if (JSON.stringify(localSetData.value) !== JSON.stringify(newValue)) {\n      localSetData.value = { ...newValue };\n    }\n  },\n  { deep: true, immediate: true }\n);\n\nonUnmounted(() => {\n  settingsStore.setSetData(localSetData.value);\n});\n\n// ==================== 选项配置 ====================\nconst translationEngineOptions = computed(() => [\n  { label: t('settings.translationEngineOptions.none'), value: 'none' },\n  { label: t('settings.translationEngineOptions.opencc'), value: 'opencc' }\n]);\n\nconst qualityOptions = computed(() => [\n  { label: t('settings.playback.qualityOptions.standard'), value: 'standard' },\n  { label: t('settings.playback.qualityOptions.higher'), value: 'higher' },\n  { label: t('settings.playback.qualityOptions.exhigh'), value: 'exhigh' },\n  { label: t('settings.playback.qualityOptions.lossless'), value: 'lossless' },\n  { label: t('settings.playback.qualityOptions.hires'), value: 'hires' },\n  { label: t('settings.playback.qualityOptions.jyeffect'), value: 'jyeffect' },\n  { label: t('settings.playback.qualityOptions.sky'), value: 'sky' },\n  { label: t('settings.playback.qualityOptions.dolby'), value: 'dolby' },\n  { label: t('settings.playback.qualityOptions.jymaster'), value: 'jymaster' }\n]);\n\nconst closeActionOptions = computed(() => [\n  { label: t('settings.application.closeOptions.ask'), value: 'ask' },\n  { label: t('settings.application.closeOptions.minimize'), value: 'minimize' },\n  { label: t('settings.application.closeOptions.close'), value: 'close' }\n]);\n\nconst animationSpeedMarks = computed(() => ({\n  0.1: t('settings.basic.animationSpeed.slow'),\n  1: t('settings.basic.animationSpeed.normal'),\n  3: t('settings.basic.animationSpeed.fast')\n}));\n\n// ==================== 主题设置 ====================\nconst isDarkTheme = computed({\n  get: () => settingsStore.theme === 'dark',\n  set: () => settingsStore.toggleTheme()\n});\n\nconst handleAutoThemeChange = (value: boolean) => {\n  settingsStore.setAutoTheme(value);\n};\n\n// ==================== GPU加速 ====================\nconst gpuAccelerationChanged = ref(false);\n\nconst handleGpuAccelerationChange = (enabled: boolean) => {\n  try {\n    if (window.electron) {\n      window.electron.ipcRenderer.send('update-gpu-acceleration', enabled);\n      gpuAccelerationChanged.value = true;\n      message.info(t('settings.basic.gpuAccelerationChangeSuccess'));\n    }\n  } catch (error) {\n    console.error('GPU加速设置更新失败:', error);\n    message.error(t('settings.basic.gpuAccelerationChangeError'));\n  }\n};\n\n// ==================== 更新检查 ====================\nconst checking = ref(false);\nconst updateInfo = ref<UpdateResult>({\n  hasUpdate: false,\n  latestVersion: '',\n  currentVersion: config.version,\n  releaseInfo: null\n});\n\nconst checkForUpdates = async (isClick = false) => {\n  checking.value = true;\n  try {\n    const result = await checkUpdate(config.version);\n    if (result) {\n      updateInfo.value = result;\n      if (!result.hasUpdate && isClick) {\n        message.success(t('settings.about.latest'));\n      }\n    } else if (isClick) {\n      message.success(t('settings.about.latest'));\n    }\n  } catch (error) {\n    console.error('检查更新失败:', error);\n    if (isClick) {\n      message.error(t('settings.about.messages.checkError'));\n    }\n  } finally {\n    checking.value = false;\n  }\n};\n\nconst openReleasePage = () => {\n  settingsStore.showUpdateModal = true;\n};\n\nconst openAuthor = () => {\n  window.open(setData.value.authorUrl);\n};\n\nconst restartApp = () => {\n  window.electron.ipcRenderer.send('restart');\n};\n\n// ==================== 下载路径 ====================\nconst selectDownloadPath = async () => {\n  const path = await selectDirectory(message);\n  if (path) {\n    setData.value = { ...setData.value, downloadPath: path };\n  }\n};\n\nconst openDownloadPath = () => {\n  openDirectory(setData.value.downloadPath, message);\n};\n\n// ==================== 代理设置 ====================\nconst showProxyModal = ref(false);\nconst proxyForm = ref({ protocol: 'http', host: '127.0.0.1', port: 7890 });\n\nwatch(\n  () => setData.value.proxyConfig,\n  (newVal) => {\n    if (newVal) {\n      proxyForm.value = {\n        protocol: newVal.protocol || 'http',\n        host: newVal.host || '127.0.0.1',\n        port: newVal.port || 7890\n      };\n    }\n  },\n  { immediate: true, deep: true }\n);\n\nconst handleProxyConfirm = async (proxyConfig: any) => {\n  setData.value = {\n    ...setData.value,\n    proxyConfig: { enable: setData.value.proxyConfig?.enable || false, ...proxyConfig }\n  };\n  message.success(t('settings.network.messages.proxySuccess'));\n};\n\nconst validateAndSaveRealIP = () => {\n  const ipRegex = /^(\\d{1,3}\\.){3}\\d{1,3}$/;\n  if (!setData.value.realIP || ipRegex.test(setData.value.realIP)) {\n    setData.value = { ...setData.value, realIP: setData.value.realIP, enableRealIP: true };\n    if (setData.value.realIP) {\n      message.success(t('settings.network.messages.realIPSuccess'));\n    }\n  } else {\n    message.error(t('settings.network.messages.realIPError'));\n    setData.value = { ...setData.value, realIP: '' };\n  }\n};\n\nwatch(\n  () => setData.value.enableRealIP,\n  (newVal) => {\n    if (!newVal) {\n      setData.value = { ...setData.value, realIP: '', enableRealIP: false };\n    }\n  }\n);\n\n// ==================== 字体设置 ====================\nconst systemFonts = computed(() => settingsStore.systemFonts);\nconst selectedFonts = ref<string[]>([]);\n\nconst renderFontLabel = (option: { label: string; value: string }) => {\n  return h('span', { style: { fontFamily: option.value } }, option.label);\n};\n\nwatch(\n  selectedFonts,\n  (newFonts) => {\n    setData.value = {\n      ...setData.value,\n      fontFamily: newFonts.length === 0 ? 'system-ui' : newFonts.join(',')\n    };\n  },\n  { deep: true }\n);\n\nwatch(\n  () => setData.value.fontFamily,\n  (newFont) => {\n    if (newFont) {\n      selectedFonts.value = newFont === 'system-ui' ? [] : newFont.split(',');\n    }\n  },\n  { immediate: true }\n);\n\n// ==================== 捐赠列表 ====================\nconst isDonationListVisible = ref(localStorage.getItem('donationListVisible') !== 'false');\n\nconst toggleDonationList = () => {\n  isDonationListVisible.value = !isDonationListVisible.value;\n  localStorage.setItem('donationListVisible', isDonationListVisible.value.toString());\n};\n\n// ==================== 弹窗控制 ====================\nconst showClearCacheModal = ref(false);\nconst showShortcutModal = ref(false);\nconst showMusicSourcesModal = ref(false);\nconst showRemoteControlModal = ref(false);\nconst showTokenModal = ref(false);\n\nconst handleShortcutsChange = (shortcuts: any) => {\n  console.log('快捷键已更新:', shortcuts);\n};\n\n// ==================== 缓存清理 ====================\nconst clearCache = async (selectedCacheTypes: string[]) => {\n  const clearTasks = selectedCacheTypes.map(async (type) => {\n    switch (type) {\n      case 'history':\n        localStorage.removeItem('musicHistory');\n        break;\n      case 'favorite':\n        localStorage.removeItem('favoriteList');\n        break;\n      case 'user':\n        userStore.handleLogout();\n        break;\n      case 'settings':\n        if (window.electron) {\n          window.electron.ipcRenderer.send('set-store-value', 'set', localData);\n        }\n        localStorage.removeItem('appSettings');\n        localStorage.removeItem('theme');\n        localStorage.removeItem('lyricData');\n        localStorage.removeItem('lyricFontSize');\n        localStorage.removeItem('playMode');\n        break;\n      case 'downloads':\n        if (window.electron) {\n          window.electron.ipcRenderer.send('clear-downloads-history');\n        }\n        break;\n      case 'resources':\n        if (window.electron) {\n          window.electron.ipcRenderer.send('clear-audio-cache');\n        }\n        localStorage.removeItem('lyricCache');\n        localStorage.removeItem('musicUrlCache');\n        if (window.caches) {\n          try {\n            const cache = await window.caches.open('music-images');\n            const keys = await cache.keys();\n            keys.forEach((key) => cache.delete(key));\n          } catch (error) {\n            console.error('清除图片缓存失败:', error);\n          }\n        }\n        break;\n      case 'lyrics':\n        window.api.invoke('clear-lyrics-cache');\n        break;\n    }\n  });\n  await Promise.all(clearTasks);\n  message.success(t('settings.system.messages.clearSuccess'));\n};\n\n// ==================== Token管理 ====================\nconst currentToken = ref(localStorage.getItem('token') || '');\n\nconst handleTokenSave = async (token: string) => {\n  try {\n    const originalToken = localStorage.getItem('token');\n    localStorage.setItem('token', token);\n\n    const user = await getUserDetail();\n    if (user.data && user.data.profile) {\n      userStore.setUser(user.data.profile);\n      currentToken.value = token;\n      message.success(t('settings.cookie.message.saveSuccess'));\n      setTimeout(() => window.location.reload(), 1000);\n    } else {\n      if (originalToken) localStorage.setItem('token', originalToken);\n      else localStorage.removeItem('token');\n      message.error(t('settings.cookie.message.saveError'));\n    }\n  } catch {\n    const originalToken = localStorage.getItem('token');\n    if (originalToken) localStorage.setItem('token', originalToken);\n    else localStorage.removeItem('token');\n    message.error(t('settings.cookie.message.saveError'));\n  }\n};\n\nconst clearToken = () => {\n  localStorage.removeItem('token');\n  localStorage.removeItem('user');\n  currentToken.value = '';\n  userStore.user = null;\n  message.success(t('settings.basic.clearToken') + '成功');\n  setTimeout(() => window.location.reload(), 1000);\n};\n\nwatch(\n  () => localStorage.getItem('token'),\n  (newToken) => {\n    currentToken.value = newToken || '';\n  },\n  { immediate: true }\n);\n\n// ==================== 音源设置 ====================\nconst musicSources = computed({\n  get: () => {\n    if (!setData.value.enabledMusicSources) return ALL_PLATFORMS;\n    return setData.value.enabledMusicSources as Platform[];\n  },\n  set: (newValue: Platform[]) => {\n    const valuesToSet = newValue.length > 0 ? [...new Set(newValue)] : ALL_PLATFORMS;\n    setData.value = { ...setData.value, enabledMusicSources: valuesToSet };\n  }\n});\n\n// ==================== 导航相关 ====================\ninterface SettingSectionConfig {\n  id: string;\n  electron?: boolean;\n}\n\nconst settingSections: SettingSectionConfig[] = [\n  { id: 'basic' },\n  { id: 'playback' },\n  { id: 'application', electron: true },\n  { id: 'network', electron: true },\n  { id: 'system', electron: true },\n  { id: 'about' },\n  { id: 'donation' }\n];\n\nconst navSections = computed(() => {\n  return settingSections\n    .filter((section) => !section.electron || isElectron)\n    .map((section) => ({\n      id: section.id,\n      title: t(`settings.sections.${section.id}`)\n    }));\n});\n\nconst currentSection = ref('basic');\nconst scrollbarRef = ref();\nconst sectionRefs = reactive<Record<string, HTMLElement | null>>({\n  basic: null,\n  playback: null,\n  application: null,\n  network: null,\n  system: null,\n  about: null,\n  donation: null\n});\n\nconst scrollToSection = async (sectionId: string) => {\n  currentSection.value = sectionId;\n  const sectionEl = sectionRefs[sectionId];\n  if (sectionEl) {\n    await nextTick();\n    scrollbarRef.value?.scrollTo({ top: sectionEl.offsetTop - 20, behavior: 'smooth' });\n  }\n};\n\nconst SCROLL_OFFSET_THRESHOLD = 100;\n\nconst handleScroll = (e: any) => {\n  const { scrollTop } = e.target;\n  let lastValidSection = 'basic';\n\n  for (const section of settingSections) {\n    if (!section.electron || isElectron) {\n      const el = sectionRefs[section.id];\n      if (el && scrollTop >= el.offsetTop - SCROLL_OFFSET_THRESHOLD) {\n        lastValidSection = section.id;\n      }\n    }\n  }\n\n  if (lastValidSection !== currentSection.value) {\n    currentSection.value = lastValidSection;\n  }\n};\n\n// ==================== 初始化 ====================\nonMounted(async () => {\n  checkForUpdates();\n  if (setData.value.proxyConfig) {\n    proxyForm.value = { ...setData.value.proxyConfig };\n  }\n  if (setData.value.enableRealIP === undefined) {\n    setData.value = { ...setData.value, enableRealIP: false };\n  }\n\n  if (window.electron) {\n    window.electron.ipcRenderer.on('gpu-acceleration-updated', (_, enabled: boolean) => {\n      console.log('GPU加速设置已更新:', enabled);\n      gpuAccelerationChanged.value = true;\n    });\n\n    window.electron.ipcRenderer.on('gpu-acceleration-update-error', (_, errorMessage: string) => {\n      console.error('GPU加速设置更新错误:', errorMessage);\n      gpuAccelerationChanged.value = false;\n    });\n  }\n\n  await nextTick();\n  handleScroll({ target: { scrollTop: 0 } });\n});\n</script>\n\n<style lang=\"scss\" scoped>\n:deep(.n-select) {\n  min-width: 120px;\n}\n\n:deep(.n-input-number) {\n  max-width: 140px;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/toplist/index.vue",
    "content": "<template>\n  <div class=\"toplist-page\">\n    <n-scrollbar class=\"toplist-container\" style=\"height: 100%\" :size=\"100\">\n      <div v-loading=\"loading\" class=\"toplist-list\">\n        <div\n          v-for=\"(item, index) in topList\"\n          :key=\"item.id\"\n          class=\"toplist-item\"\n          :class=\"setAnimationClass('animate__bounceIn')\"\n          :style=\"getItemAnimationDelay(index)\"\n          @click.stop=\"openToplist(item)\"\n        >\n          <div class=\"toplist-item-img\">\n            <n-image\n              class=\"toplist-item-img-img\"\n              :src=\"getImgUrl(item.coverImgUrl, '300y300')\"\n              width=\"200\"\n              height=\"200\"\n              lazy\n              preview-disabled\n            />\n            <div class=\"top\">\n              <div class=\"play-count\">{{ formatNumber(item.playCount) }}</div>\n              <i class=\"iconfont icon-videofill\"></i>\n            </div>\n          </div>\n          <div class=\"toplist-item-title\">{{ item.name }}</div>\n          <div class=\"toplist-item-desc\">{{ item.updateFrequency || '' }}</div>\n        </div>\n      </div>\n    </n-scrollbar>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useRouter } from 'vue-router';\n\nimport { getListDetail, getToplist } from '@/api/list';\nimport { navigateToMusicList } from '@/components/common/MusicListNavigator';\nimport type { IListDetail } from '@/types/listDetail';\nimport { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';\n\ndefineOptions({\n  name: 'Toplist'\n});\n\nconst topList = ref<any[]>([]);\n\n// 计算每个项目的动画延迟\nconst getItemAnimationDelay = (index: number) => {\n  return setAnimationDelay(index, 30);\n};\n\nconst listDetail = ref<IListDetail | null>();\nconst listLoading = ref(true);\n\nconst router = useRouter();\n\nconst openToplist = (item: any) => {\n  listLoading.value = true;\n\n  getListDetail(item.id).then((res) => {\n    listDetail.value = res.data;\n    listLoading.value = false;\n\n    navigateToMusicList(router, {\n      id: item.id,\n      type: 'playlist',\n      name: item.name,\n      songList: res.data.playlist.tracks || [],\n      listInfo: res.data.playlist,\n      canRemove: false\n    });\n  });\n};\n\nconst loading = ref(false);\nconst loadToplist = async () => {\n  loading.value = true;\n  try {\n    const { data } = await getToplist();\n    topList.value = data.list || [];\n  } catch (error) {\n    console.error('加载排行榜列表失败:', error);\n  } finally {\n    loading.value = false;\n  }\n};\n\nonMounted(() => {\n  loadToplist();\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.toplist-page {\n  @apply relative h-full w-full;\n  @apply bg-light dark:bg-black;\n}\n\n.toplist-container {\n  @apply p-4;\n}\n\n.toplist-list {\n  @apply grid gap-x-8 gap-y-6 pb-28 pr-4;\n  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n}\n\n.toplist-item {\n  @apply flex flex-col;\n\n  &-img {\n    @apply rounded-xl overflow-hidden relative w-full aspect-square;\n\n    &-img {\n      @apply block w-full h-full;\n    }\n\n    img {\n      @apply absolute top-0 left-0 w-full h-full object-cover rounded-xl;\n    }\n\n    &:hover img {\n      @apply hover:scale-110 transition-all duration-300 ease-in-out;\n    }\n\n    .top {\n      @apply absolute w-full h-full top-0 left-0 flex justify-center items-center transition-all duration-300 ease-in-out cursor-pointer;\n      @apply bg-black bg-opacity-50;\n      opacity: 0;\n\n      i {\n        @apply text-5xl text-white transition-all duration-500 ease-in-out opacity-0;\n      }\n\n      &:hover {\n        @apply opacity-100;\n      }\n\n      &:hover i {\n        @apply transform scale-150 opacity-100;\n      }\n\n      .play-count {\n        @apply absolute top-2 left-2 text-sm text-white;\n      }\n    }\n  }\n\n  &-title {\n    @apply mt-2 text-sm line-clamp-1 font-bold;\n    @apply text-gray-900 dark:text-white;\n  }\n\n  &-desc {\n    @apply mt-1 text-xs line-clamp-1;\n    @apply text-gray-500 dark:text-gray-400;\n  }\n}\n\n.mobile {\n  .toplist-list {\n    @apply px-4 gap-4;\n    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/user/detail.vue",
    "content": "<template>\n  <div class=\"user-detail-page\">\n    <n-scrollbar class=\"content-scrollbar\">\n      <div v-loading=\"loading\" class=\"content-wrapper\">\n        <template v-if=\"userDetail\">\n          <!-- 用户信息部分 -->\n          <div class=\"user-info-section\" :class=\"setAnimationClass('animate__fadeInDown')\">\n            <div\n              class=\"user-info-bg\"\n              :style=\"{ backgroundImage: `url(${getImgUrl(userDetail.profile.backgroundUrl)})` }\"\n            >\n              <div class=\"user-info-content\">\n                <n-avatar\n                  round\n                  :size=\"80\"\n                  :src=\"getImgUrl(userDetail.profile.avatarUrl, '80y80')\"\n                />\n                <div class=\"user-info-detail\">\n                  <div class=\"user-info-name\">\n                    {{ userDetail.profile.nickname }}\n                    <n-tooltip v-if=\"isArtist(userDetail.profile)\" trigger=\"hover\">\n                      <template #trigger>\n                        <i class=\"ri-verified-badge-fill artist-icon\"></i>\n                      </template>\n                      {{ t('user.detail.artist') }}\n                    </n-tooltip>\n                  </div>\n                  <div class=\"user-info-stats\">\n                    <div class=\"user-info-stat-item\" @click=\"showFollowerList\">\n                      <div class=\"label\">{{ userDetail.profile.followeds }}</div>\n                      <div>{{ t('user.profile.followers') }}</div>\n                    </div>\n                    <div class=\"user-info-stat-item\" @click=\"showFollowList\">\n                      <div class=\"label\">{{ userDetail.profile.follows }}</div>\n                      <div>{{ t('user.profile.following') }}</div>\n                    </div>\n                    <div class=\"user-info-stat-item\">\n                      <div class=\"label\">{{ userDetail.level }}</div>\n                      <div>{{ t('user.profile.level') }}</div>\n                    </div>\n                  </div>\n                  <div class=\"user-info-signature\">\n                    {{ userDetail.profile.signature || t('user.detail.noSignature') }}\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <n-tabs type=\"line\" animated>\n            <!-- 歌单列表 -->\n            <n-tab-pane name=\"playlists\" :tab=\"t('user.detail.playlists')\">\n              <div v-if=\"playList.length === 0\" class=\"empty-message\">\n                {{ t('user.detail.noPlaylists') }}\n              </div>\n              <div v-else class=\"playlist-grid\" :class=\"setAnimationClass('animate__fadeInUp')\">\n                <div\n                  v-for=\"(item, index) in playList\"\n                  :key=\"index\"\n                  class=\"playlist-item\"\n                  :class=\"setAnimationClass('animate__fadeInUp')\"\n                  :style=\"setAnimationDelay(index, 50)\"\n                  @click=\"openPlaylist(item)\"\n                >\n                  <div class=\"playlist-cover\">\n                    <n-image\n                      :src=\"getImgUrl(item.coverImgUrl, '200y200')\"\n                      lazy\n                      preview-disabled\n                      class=\"cover-img\"\n                    />\n                    <div class=\"play-count\">\n                      <i class=\"ri-play-fill\"></i>\n                      {{ formatNumber(item.playCount) }}\n                    </div>\n                  </div>\n                  <div class=\"playlist-info\">\n                    <div class=\"playlist-name\">{{ item.name }}</div>\n                    <div class=\"playlist-stats\">\n                      {{ t('user.playlist.trackCount', { count: item.trackCount }) }}\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </n-tab-pane>\n\n            <!-- 听歌排行 -->\n            <n-tab-pane name=\"records\" :tab=\"t('user.detail.records')\">\n              <div v-if=\"!hasRecordPermission\" class=\"empty-message\">\n                <div class=\"no-permission\">\n                  <i class=\"ri-lock-line text-2xl mr-2\"></i>\n                  {{ t('user.detail.noRecordPermission', { name: userDetail.profile.nickname }) }}\n                </div>\n              </div>\n              <div v-else-if=\"!recordList || recordList.length === 0\" class=\"empty-message\">\n                {{ t('user.detail.noRecords') }}\n              </div>\n              <div v-else class=\"record-list\">\n                <div\n                  v-for=\"(item, index) in recordList\"\n                  :key=\"item.id\"\n                  class=\"record-item\"\n                  :class=\"setAnimationClass('animate__bounceInUp')\"\n                  :style=\"setAnimationDelay(index, 25)\"\n                >\n                  <song-item\n                    class=\"song-item\"\n                    :index=\"index\"\n                    :item=\"item\"\n                    compact\n                    @play=\"handlePlay\"\n                  />\n                </div>\n              </div>\n            </n-tab-pane>\n          </n-tabs>\n        </template>\n        <div v-else-if=\"!loading\" class=\"empty-message\">\n          {{ t('user.message.loadFailed') }}\n        </div>\n\n        <!-- 底部留白 -->\n        <div class=\"pb-20\"></div>\n      </div>\n    </n-scrollbar>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useMessage } from 'naive-ui';\nimport { onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport { getListDetail } from '@/api/list';\nimport { getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user';\nimport { navigateToMusicList } from '@/components/common/MusicListNavigator';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { usePlayerStore } from '@/store/modules/player';\nimport type { Playlist } from '@/types/listDetail';\nimport type { IUserDetail } from '@/types/user';\nimport { formatNumber, getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';\n\ndefineOptions({\n  name: 'UserDetail'\n});\n\nconst { t } = useI18n();\nconst router = useRouter();\nconst route = useRoute();\nconst message = useMessage();\nconst playerStore = usePlayerStore();\n\n// 获取路由参数中的用户ID\nconst userId = ref<number>(Number(route.params.uid));\n// 用户数据\nconst userDetail = ref<IUserDetail>();\nconst playList = ref<any[]>([]);\nconst recordList = ref<any[]>([]);\nconst loading = ref(true);\nconst hasRecordPermission = ref(true); // 是否有权限查看听歌记录\n\n// 歌单详情相关\nconst currentList = ref<Playlist>();\nconst listLoading = ref(false);\n\n// 加载用户数据\nconst loadUserData = async () => {\n  if (!userId.value) {\n    message.error(t('user.detail.invalidUserId'));\n    router.back();\n    return;\n  }\n\n  try {\n    loading.value = true;\n    recordList.value = []; // 清空之前的记录\n    hasRecordPermission.value = true; // 重置权限状态\n\n    // 分开处理请求，处理可能的错误\n    // 1. 获取用户详情和歌单列表\n    try {\n      const [userDetailRes, playlistRes] = await Promise.all([\n        getUserDetail(userId.value),\n        getUserPlaylist(userId.value)\n      ]);\n\n      userDetail.value = userDetailRes.data;\n      playList.value = playlistRes.data.playlist;\n    } catch (error) {\n      console.error('加载用户基本信息失败:', error);\n      message.error(t('user.message.loadBasicInfoFailed'));\n      return; // 如果基本信息加载失败，直接返回\n    }\n\n    // 2. 单独处理听歌记录请求，这个请求可能会无权限\n    try {\n      const recordRes = await getUserRecord(userId.value);\n\n      if (recordRes.data && recordRes.data.allData) {\n        recordList.value = recordRes.data.allData.map((item: any) => ({\n          ...item,\n          ...item.song,\n          picUrl: item.song.al.picUrl\n        }));\n      }\n    } catch (error: any) {\n      console.error('加载听歌记录失败:', error);\n      // 判断是否是无权限错误\n      if (error.response?.data?.code === -2 || error.data?.code === -2) {\n        hasRecordPermission.value = false;\n      }\n      // 不显示错误消息，因为这是预期的情况\n    }\n  } catch (error) {\n    console.error('加载用户数据失败:', error);\n    message.error(t('user.message.loadFailed'));\n  } finally {\n    loading.value = false;\n  }\n};\n\n// 使用onMounted和watch结合的方式解决路由变化问题\nonMounted(() => {\n  loadUserData();\n});\n\n// 监听路由参数变化\nwatch(\n  () => route.params.uid,\n  (newUid) => {\n    if (newUid && Number(newUid) !== userId.value) {\n      userId.value = Number(newUid);\n      loadUserData();\n    }\n  }\n);\n\n// 替换显示歌单的方法\nconst openPlaylist = (item: any) => {\n  listLoading.value = true;\n\n  getListDetail(item.id).then((res) => {\n    currentList.value = res.data.playlist;\n    listLoading.value = false;\n\n    navigateToMusicList(router, {\n      id: item.id,\n      type: 'playlist',\n      name: item.name,\n      songList: res.data.playlist.tracks || [],\n      listInfo: res.data.playlist,\n      canRemove: false\n    });\n  });\n};\n\n// 播放歌曲\nconst handlePlay = () => {\n  if (!recordList.value || recordList.value.length === 0) return;\n\n  const tracks = recordList.value;\n  playerStore.setPlayList(tracks);\n};\n\n// 显示关注列表\nconst showFollowList = () => {\n  if (!userDetail.value) return;\n\n  router.push({\n    path: `/user/follows`,\n    query: {\n      uid: userId.value.toString(),\n      name: userDetail.value.profile.nickname\n    }\n  });\n};\n\n// 显示粉丝列表\nconst showFollowerList = () => {\n  if (!userDetail.value) return;\n\n  router.push({\n    path: `/user/followers`,\n    query: {\n      uid: userId.value.toString(),\n      name: userDetail.value.profile.nickname\n    }\n  });\n};\n\n// 判断是否为歌手\nconst isArtist = (profile: any) => {\n  return profile.userType === 4 || profile.userType === 2 || profile.accountType === 2;\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.user-detail-page {\n  @apply h-full flex flex-col;\n\n  .content-scrollbar {\n    @apply flex-1 overflow-hidden;\n  }\n\n  .content-wrapper {\n    @apply flex flex-col;\n    @apply pr-4 pb-4;\n  }\n}\n\n.user-info-section {\n  @apply mb-4;\n\n  .user-info-bg {\n    @apply rounded-xl overflow-hidden bg-cover bg-center relative;\n    height: 200px;\n\n    &:before {\n      content: '';\n      @apply absolute inset-0 bg-black bg-opacity-40;\n    }\n  }\n\n  .user-info-content {\n    @apply absolute inset-0 flex items-center p-6;\n  }\n\n  .user-info-detail {\n    @apply ml-4 text-white;\n\n    .user-info-name {\n      @apply text-xl font-bold flex items-center;\n\n      .artist-icon {\n        @apply ml-2 text-blue-500;\n      }\n    }\n\n    .user-info-stats {\n      @apply flex mt-2;\n\n      .user-info-stat-item {\n        @apply mr-6 text-center;\n\n        .label {\n          @apply text-lg font-bold;\n        }\n\n        &:nth-child(1),\n        &:nth-child(2) {\n          @apply cursor-pointer transition-all duration-200;\n          @apply hover:bg-black hover:bg-opacity-20 rounded-lg px-2;\n        }\n      }\n    }\n\n    .user-info-signature {\n      @apply mt-2 text-sm text-gray-200;\n      @apply line-clamp-2;\n    }\n  }\n}\n\n.playlist-grid {\n  @apply grid gap-4 w-full py-4;\n  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));\n}\n\n.playlist-item {\n  @apply flex flex-col rounded-xl overflow-hidden cursor-pointer;\n  @apply transition-all duration-200;\n  @apply hover:scale-105;\n\n  .playlist-cover {\n    @apply relative;\n    aspect-ratio: 1;\n\n    .cover-img {\n      @apply w-full h-full object-cover rounded-xl;\n    }\n\n    .play-count {\n      @apply absolute top-2 right-2 px-2 py-1 rounded-full text-xs;\n      @apply bg-black bg-opacity-50 text-white flex items-center;\n\n      i {\n        @apply mr-1;\n      }\n    }\n  }\n\n  .playlist-info {\n    @apply mt-2 px-1;\n\n    .playlist-name {\n      @apply text-gray-900 dark:text-white font-medium;\n      @apply line-clamp-2 text-sm;\n    }\n\n    .playlist-stats {\n      @apply text-gray-500 dark:text-gray-400 text-xs mt-1;\n    }\n  }\n}\n\n.record-list {\n  @apply p-4;\n\n  .record-item {\n    @apply flex items-center mb-2 rounded-2xl;\n    @apply bg-light-100 dark:bg-dark-100;\n    @apply transition-all duration-200;\n    @apply hover:bg-light-200 dark:hover:bg-dark-200;\n  }\n\n  .play-score {\n    @apply text-gray-500 dark:text-gray-400 mr-2 text-lg w-10 h-10 rounded-full flex items-center justify-center;\n  }\n\n  .song-item {\n    @apply flex-1;\n  }\n}\n\n.loading-container {\n  @apply flex justify-center items-center p-8;\n}\n\n.empty-message {\n  @apply flex justify-center items-center p-8;\n\n  .no-permission {\n    @apply flex flex-col items-center justify-center text-gray-500 dark:text-gray-400;\n    @apply p-4 rounded-lg;\n\n    i {\n      @apply text-3xl mb-2;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/user/followers.vue",
    "content": "<template>\n  <div class=\"followers-page\">\n    <div class=\"content-wrapper\">\n      <div class=\"page-title\" v-if=\"targetUserName\">\n        {{ targetUserName + t('user.follower.userFollowersTitle') }}\n      </div>\n      <div class=\"page-title\" v-else>\n        {{ t('user.follower.myFollowersTitle') }}\n      </div>\n\n      <n-spin v-if=\"followerListLoading && followerList.length === 0\" size=\"large\" />\n      <n-scrollbar v-else class=\"scrollbar-container\">\n        <div v-if=\"followerList.length === 0\" class=\"empty-follower\">\n          {{ t('user.follower.noFollowers') }}\n        </div>\n        <div class=\"follower-grid\" :class=\"setAnimationClass('animate__fadeInUp')\">\n          <div\n            v-for=\"(item, index) in followerList\"\n            :key=\"index\"\n            class=\"follower-item\"\n            :class=\"setAnimationClass('animate__fadeInUp')\"\n            :style=\"setAnimationDelay(index, 30)\"\n            @click=\"viewUserDetail(item.userId, item.nickname)\"\n          >\n            <div class=\"follower-item-inner\">\n              <div class=\"follower-avatar\">\n                <n-avatar round :size=\"70\" :src=\"getImgUrl(item.avatarUrl, '70y70')\" />\n                <div v-if=\"isArtist(item)\" class=\"artist-badge\">\n                  <i class=\"ri-verified-badge-fill\"></i>\n                </div>\n              </div>\n              <div class=\"follower-info\">\n                <div class=\"follower-name\" :class=\"{ 'is-artist': isArtist(item) }\">\n                  {{ item.nickname }}\n                  <n-tooltip v-if=\"isArtist(item)\" trigger=\"hover\">\n                    <template #trigger>\n                      <i class=\"ri-verified-badge-fill artist-icon\"></i>\n                    </template>\n                    歌手\n                  </n-tooltip>\n                </div>\n                <div class=\"follower-signature\">\n                  {{ item.signature || '这个人很懒，什么都没留下' }}\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <n-space v-if=\"followerListLoading\" justify=\"center\" class=\"loading-more\">\n          <n-spin size=\"small\" />\n        </n-space>\n\n        <n-button\n          v-else-if=\"hasMoreFollowers\"\n          class=\"load-more-btn\"\n          secondary\n          block\n          @click=\"loadMoreFollowers\"\n        >\n          {{ t('user.follower.loadMore') }}\n        </n-button>\n      </n-scrollbar>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useMessage } from 'naive-ui';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport { getUserFollowers } from '@/api/user';\nimport { useUserStore } from '@/store/modules/user';\nimport type { IUserFollow } from '@/types/user';\nimport { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';\nimport { checkLoginStatus as checkAuthStatus } from '@/utils/auth';\n\ndefineOptions({\n  name: 'UserFollowers'\n});\n\nconst { t } = useI18n();\nconst userStore = useUserStore();\nconst router = useRouter();\nconst message = useMessage();\nconst route = useRoute();\n\n// 粉丝列表相关\nconst followerList = ref<IUserFollow[]>([]);\nconst followerOffset = ref(0);\nconst followerLimit = ref(30);\nconst hasMoreFollowers = ref(false);\nconst followerListLoading = ref(false);\nconst targetUserId = ref<number | null>(null);\nconst targetUserName = ref<string>('');\n\nconst user = computed(() => userStore.user);\n\n// 检查是否有指定用户ID\nconst checkTargetUser = () => {\n  const uid = route.query.uid;\n  const name = route.query.name;\n\n  if (uid && typeof uid === 'string') {\n    targetUserId.value = parseInt(uid);\n    targetUserName.value = typeof name === 'string' ? name : '';\n    return true;\n  }\n\n  // 如果没有指定用户ID，则显示当前登录用户的粉丝列表\n  return checkLoginStatus();\n};\n\n// 检查登录状态\nconst checkLoginStatus = () => {\n  const loginInfo = checkAuthStatus();\n\n  if (!loginInfo.isLoggedIn) {\n    router.push('/login');\n    return false;\n  }\n\n  // 如果store中没有用户数据，但localStorage中有，则恢复用户数据\n  if (!userStore.user && loginInfo.user) {\n    userStore.setUser(loginInfo.user);\n  }\n\n  return true;\n};\n\n// 加载粉丝列表\nconst loadFollowerList = async () => {\n  // 确定要加载哪个用户的粉丝列表\n  const userId = targetUserId.value || user.value?.userId;\n\n  if (!userId) return;\n\n  try {\n    followerListLoading.value = true;\n    const { data } = await getUserFollowers(userId, followerLimit.value, followerOffset.value);\n\n    if (!data || !data.followeds) {\n      hasMoreFollowers.value = false;\n      return;\n    }\n\n    const newFollowers = data.followeds as IUserFollow[];\n    followerList.value = [...followerList.value, ...newFollowers];\n\n    // 判断是否还有更多粉丝\n    hasMoreFollowers.value = newFollowers.length >= followerLimit.value;\n  } catch (error) {\n    console.error('加载粉丝列表失败:', error);\n    message.error(t('user.follower.loadFailed'));\n  } finally {\n    followerListLoading.value = false;\n  }\n};\n\n// 加载更多粉丝\nconst loadMoreFollowers = async () => {\n  followerOffset.value += followerLimit.value;\n  await loadFollowerList();\n};\n\n// 查看用户详情\nconst viewUserDetail = (userId: number, nickname: string) => {\n  router.push({\n    path: `/user/detail/${userId}`,\n    query: { name: nickname }\n  });\n};\n\n// 判断是否为歌手\nconst isArtist = (user: IUserFollow) => {\n  // 根据用户类型判断是否为歌手，userType 为 4 表示是官方认证的音乐人\n  return user.userType === 4 || user.userType === 2 || user.accountType === 2;\n};\n\n// 页面挂载时加载数据\nonMounted(() => {\n  if (checkTargetUser()) {\n    loadFollowerList();\n  }\n});\n\n// 监听路由变化重新加载数据\nwatch(\n  () => route.query,\n  (newQuery) => {\n    if (newQuery.uid && newQuery.uid !== targetUserId.value?.toString()) {\n      followerList.value = []; // 清空列表\n      followerOffset.value = 0; // 重置偏移量\n      checkTargetUser();\n      loadFollowerList();\n    }\n  }\n);\n</script>\n\n<style lang=\"scss\" scoped>\n.followers-page {\n  @apply h-full flex flex-col;\n\n  .content-wrapper {\n    @apply flex-1 overflow-hidden p-4;\n    @apply flex flex-col;\n  }\n\n  .scrollbar-container {\n    @apply h-full;\n  }\n}\n\n.follower-grid {\n  @apply grid gap-4 w-full;\n  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n}\n\n.follower-item {\n  @apply rounded-xl overflow-hidden cursor-pointer;\n  @apply transition-all duration-200;\n  @apply hover:scale-105;\n\n  &-inner {\n    @apply flex flex-col items-center p-4 h-full;\n    @apply bg-light-100 dark:bg-dark-100;\n    @apply transition-all duration-200;\n    @apply hover:bg-light-200 dark:hover:bg-dark-200;\n  }\n\n  .follower-avatar {\n    @apply relative;\n\n    .artist-badge {\n      @apply absolute bottom-0 right-0;\n      @apply text-blue-500 text-lg;\n    }\n  }\n\n  .follower-info {\n    @apply mt-3 text-center w-full;\n\n    .follower-name {\n      @apply text-gray-900 dark:text-white text-base font-medium;\n      @apply flex items-center justify-center;\n\n      &.is-artist {\n        @apply text-blue-500;\n      }\n\n      .artist-icon {\n        @apply ml-1 text-blue-500;\n      }\n    }\n\n    .follower-signature {\n      @apply text-gray-500 dark:text-gray-400 text-xs mt-1;\n      @apply line-clamp-2 text-center;\n      max-height: 2.4em;\n    }\n  }\n}\n\n.empty-follower {\n  @apply text-center py-8 text-gray-500 dark:text-gray-400;\n}\n\n.load-more-btn {\n  @apply mt-4 mb-8;\n}\n\n.loading-more {\n  @apply my-4;\n}\n\n.page-title {\n  @apply text-xl font-bold mb-4;\n  @apply text-gray-900 dark:text-white;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/user/follows.vue",
    "content": "<template>\n  <div class=\"follows-page\">\n    <div class=\"content-wrapper\">\n      <div class=\"page-title\" v-if=\"targetUserName\">\n        {{ targetUserName + t('user.follow.userFollowsTitle') }}\n      </div>\n      <div class=\"page-title\" v-else>\n        {{ t('user.follow.myFollowsTitle') }}\n      </div>\n\n      <n-spin v-if=\"followListLoading && followList.length === 0\" size=\"large\" />\n      <n-scrollbar v-else class=\"scrollbar-container\">\n        <div v-if=\"followList.length === 0\" class=\"empty-follow\">\n          {{ t('user.follow.noFollowings') }}\n        </div>\n        <div class=\"follow-grid\" :class=\"setAnimationClass('animate__fadeInUp')\">\n          <div\n            v-for=\"(item, index) in followList\"\n            :key=\"index\"\n            class=\"follow-item\"\n            :class=\"setAnimationClass('animate__fadeInUp')\"\n            :style=\"setAnimationDelay(index, 30)\"\n            @click=\"viewUserDetail(item.userId, item.nickname)\"\n          >\n            <div class=\"follow-item-inner\">\n              <div class=\"follow-avatar\">\n                <n-avatar round :size=\"70\" :src=\"getImgUrl(item.avatarUrl, '70y70')\" />\n                <div v-if=\"isArtist(item)\" class=\"artist-badge\">\n                  <i class=\"ri-verified-badge-fill\"></i>\n                </div>\n              </div>\n              <div class=\"follow-info\">\n                <div class=\"follow-name\" :class=\"{ 'is-artist': isArtist(item) }\">\n                  {{ item.nickname }}\n                  <n-tooltip v-if=\"isArtist(item)\" trigger=\"hover\">\n                    <template #trigger>\n                      <i class=\"ri-verified-badge-fill artist-icon\"></i>\n                    </template>\n                    歌手\n                  </n-tooltip>\n                </div>\n                <div class=\"follow-signature\">\n                  {{ item.signature || t('user.follow.noSignature') }}\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <n-space v-if=\"followListLoading\" justify=\"center\" class=\"loading-more\">\n          <n-spin size=\"small\" />\n        </n-space>\n\n        <n-button\n          v-else-if=\"hasMoreFollows\"\n          class=\"load-more-btn\"\n          secondary\n          block\n          @click=\"loadMoreFollows\"\n        >\n          {{ t('user.follow.loadMore') }}\n        </n-button>\n      </n-scrollbar>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useMessage } from 'naive-ui';\nimport { computed, onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRoute, useRouter } from 'vue-router';\n\nimport { getUserFollows } from '@/api/user';\nimport { useUserStore } from '@/store/modules/user';\nimport type { IUserFollow } from '@/types/user';\nimport { getImgUrl, setAnimationClass, setAnimationDelay } from '@/utils';\nimport { checkLoginStatus as checkAuthStatus } from '@/utils/auth';\n\ndefineOptions({\n  name: 'UserFollows'\n});\n\nconst { t } = useI18n();\nconst userStore = useUserStore();\nconst router = useRouter();\nconst message = useMessage();\nconst route = useRoute();\n\n// 关注列表相关\nconst followList = ref<IUserFollow[]>([]);\nconst followOffset = ref(0);\nconst followLimit = ref(30);\nconst hasMoreFollows = ref(false);\nconst followListLoading = ref(false);\nconst targetUserId = ref<number | null>(null);\nconst targetUserName = ref<string>('');\n\nconst user = computed(() => userStore.user);\n\n// 检查是否有指定用户ID\nconst checkTargetUser = () => {\n  const uid = route.query.uid;\n  const name = route.query.name;\n\n  if (uid && typeof uid === 'string') {\n    targetUserId.value = parseInt(uid);\n    targetUserName.value = typeof name === 'string' ? name : '';\n    return true;\n  }\n\n  // 如果没有指定用户ID，则显示当前登录用户的关注列表\n  return checkLoginStatus();\n};\n\n// 检查登录状态\nconst checkLoginStatus = () => {\n  const loginInfo = checkAuthStatus();\n\n  if (!loginInfo.isLoggedIn) {\n    router.push('/login');\n    return false;\n  }\n\n  // 如果store中没有用户数据，但localStorage中有，则恢复用户数据\n  if (!userStore.user && loginInfo.user) {\n    userStore.setUser(loginInfo.user);\n  }\n\n  return true;\n};\n\n// 加载关注列表\nconst loadFollowList = async () => {\n  // 确定要加载哪个用户的关注列表\n  const userId = targetUserId.value || user.value?.userId;\n\n  if (!userId) return;\n\n  try {\n    followListLoading.value = true;\n    const { data } = await getUserFollows(userId, followLimit.value, followOffset.value);\n\n    if (!data || !data.follow) {\n      hasMoreFollows.value = false;\n      return;\n    }\n\n    const newFollows = data.follow as IUserFollow[];\n    followList.value = [...followList.value, ...newFollows];\n\n    // 判断是否还有更多关注\n    hasMoreFollows.value = newFollows.length >= followLimit.value;\n  } catch (error) {\n    console.error('加载关注列表失败:', error);\n    message.error(t('user.follow.loadFailed'));\n  } finally {\n    followListLoading.value = false;\n  }\n};\n\n// 加载更多关注\nconst loadMoreFollows = async () => {\n  followOffset.value += followLimit.value;\n  await loadFollowList();\n};\n\n// 查看用户详情\nconst viewUserDetail = (userId: number, nickname: string) => {\n  router.push({\n    path: `/user/detail/${userId}`,\n    query: { name: nickname }\n  });\n};\n\n// 判断是否为歌手\nconst isArtist = (user: IUserFollow) => {\n  // 根据用户类型判断是否为歌手，userType 为 4 表示是官方认证的音乐人\n  return user.userType === 4 || user.userType === 2 || user.accountType === 2;\n};\n\n// 页面挂载时加载数据\nonMounted(() => {\n  if (checkTargetUser()) {\n    loadFollowList();\n  }\n});\n\n// 监听路由变化重新加载数据\nwatch(\n  () => route.query,\n  (newQuery) => {\n    if (newQuery.uid && newQuery.uid !== targetUserId.value?.toString()) {\n      followList.value = []; // 清空列表\n      followOffset.value = 0; // 重置偏移量\n      checkTargetUser();\n      loadFollowList();\n    }\n  }\n);\n</script>\n\n<style lang=\"scss\" scoped>\n.follows-page {\n  @apply h-full flex flex-col;\n\n  .content-wrapper {\n    @apply flex-1 overflow-hidden p-4;\n    @apply flex flex-col;\n  }\n\n  .scrollbar-container {\n    @apply h-full;\n  }\n}\n\n.follow-grid {\n  @apply grid gap-4 w-full;\n  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n}\n\n.follow-item {\n  @apply rounded-xl overflow-hidden cursor-pointer;\n  @apply transition-all duration-200;\n  @apply hover:scale-105;\n\n  &-inner {\n    @apply flex flex-col items-center p-4 h-full;\n    @apply bg-light-100 dark:bg-dark-100;\n    @apply transition-all duration-200;\n    @apply hover:bg-light-200 dark:hover:bg-dark-200;\n  }\n\n  .follow-avatar {\n    @apply relative;\n\n    .artist-badge {\n      @apply absolute bottom-0 right-0;\n      @apply text-blue-500 text-lg;\n    }\n  }\n\n  .follow-info {\n    @apply mt-3 text-center w-full;\n\n    .follow-name {\n      @apply text-gray-900 dark:text-white text-base font-medium;\n      @apply flex items-center justify-center;\n\n      &.is-artist {\n        @apply text-blue-500;\n      }\n\n      .artist-icon {\n        @apply ml-1 text-blue-500;\n      }\n    }\n\n    .follow-signature {\n      @apply text-gray-500 dark:text-gray-400 text-xs mt-1;\n      @apply line-clamp-2 text-center;\n      max-height: 2.4em;\n    }\n  }\n}\n\n.empty-follow {\n  @apply text-center py-8 text-gray-500 dark:text-gray-400;\n}\n\n.load-more-btn {\n  @apply mt-4 mb-8;\n}\n\n.loading-more {\n  @apply my-4;\n}\n\n.page-title {\n  @apply text-xl font-bold mb-4;\n  @apply text-gray-900 dark:text-white;\n}\n</style>\n"
  },
  {
    "path": "src/renderer/views/user/index.vue",
    "content": "<template>\n  <div class=\"user-page\">\n    <div\n      v-if=\"userDetail && user\"\n      class=\"left\"\n      :class=\"setAnimationClass('animate__fadeInLeft')\"\n      :style=\"{ backgroundImage: `url(${getImgUrl(user.backgroundUrl)})` }\"\n    >\n      <div class=\"page\">\n        <div class=\"user-name\">\n          <span>{{ user.nickname }}</span>\n          <span v-if=\"currentLoginType\" class=\"login-type\">{{\n            t('login.title.' + currentLoginType)\n          }}</span>\n        </div>\n        <div class=\"user-info\">\n          <n-avatar round :size=\"50\" :src=\"getImgUrl(user.avatarUrl, '50y50')\" />\n          <div class=\"user-info-list\">\n            <div class=\"user-info-item\">\n              <div class=\"label\">{{ userDetail.profile.followeds }}</div>\n              <div>{{ t('user.profile.followers') }}</div>\n            </div>\n            <div class=\"user-info-item\" @click=\"showFollowList\">\n              <div class=\"label\">{{ userDetail.profile.follows }}</div>\n              <div>{{ t('user.profile.following') }}</div>\n            </div>\n            <div class=\"user-info-item\">\n              <div class=\"label\">{{ userDetail.level }}</div>\n              <div>{{ t('user.profile.level') }}</div>\n            </div>\n          </div>\n        </div>\n        <div class=\"uesr-signature\">{{ userDetail.profile.signature }}</div>\n        <div class=\"play-list\" :class=\"setAnimationClass('animate__fadeInLeft')\">\n          <div class=\"tab-container\">\n            <n-tabs v-model:value=\"currentTab\" type=\"segment\" animated>\n              <n-tab v-for=\"tab in tabs\" :key=\"tab.key\" :name=\"tab.key\" :tab=\"t(tab.label)\">\n              </n-tab>\n            </n-tabs>\n          </div>\n          <n-scrollbar>\n            <div class=\"mt-4\">\n              <button\n                class=\"play-list-item\"\n                @click=\"goToImportPlaylist\"\n                v-if=\"isElectron && currentTab === 'created'\"\n              >\n                <div class=\"play-list-item-img\"><i class=\"icon iconfont ri-add-line\"></i></div>\n                <div class=\"play-list-item-info\">\n                  <div class=\"play-list-item-name\">\n                    {{ t('comp.playlist.import.button') }}\n                  </div>\n                </div>\n              </button>\n              <div\n                v-for=\"(item, index) in currentList\"\n                :key=\"index\"\n                class=\"play-list-item\"\n                @click=\"handleItemClick(item)\"\n              >\n                <n-image\n                  :src=\"getImgUrl(getCoverUrl(item), '50y50')\"\n                  class=\"play-list-item-img\"\n                  lazy\n                  preview-disabled\n                />\n                <div class=\"play-list-item-info\">\n                  <div class=\"play-list-item-name\">\n                    <n-ellipsis :line-clamp=\"1\">{{ item.name }}</n-ellipsis>\n                  </div>\n                  <div class=\"play-list-item-count\">\n                    {{ getItemDescription(item) }}\n                  </div>\n                </div>\n              </div>\n              <div class=\"pb-20\"></div>\n              <play-bottom />\n            </div>\n          </n-scrollbar>\n        </div>\n      </div>\n    </div>\n    <div\n      v-if=\"!isMobile\"\n      v-loading=\"infoLoading\"\n      class=\"right\"\n      :class=\"setAnimationClass('animate__fadeInRight')\"\n    >\n      <div class=\"title\">{{ t('user.ranking.title') }}</div>\n      <div class=\"record-list\">\n        <n-scrollbar>\n          <div\n            v-for=\"(item, index) in recordList\"\n            :key=\"item.id\"\n            class=\"record-item\"\n            :class=\"setAnimationClass('animate__bounceInUp')\"\n            :style=\"setAnimationDelay(index, 25)\"\n          >\n            <div class=\"play-score\">\n              {{ index + 1 }}\n            </div>\n            <song-item class=\"song-item\" :item=\"item\" mini @play=\"handlePlay\" />\n          </div>\n          <play-bottom />\n        </n-scrollbar>\n      </div>\n    </div>\n    <!-- 未登录时显示登录组件 -->\n    <div\n      v-if=\"!isLoggedIn && isMobile\"\n      class=\"login-container\"\n      :class=\"setAnimationClass('animate__fadeIn')\"\n    >\n      <login-component @login-success=\"handleLoginSuccess\" />\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { useMessage } from 'naive-ui';\nimport { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { useRouter } from 'vue-router';\n\nimport { getListDetail } from '@/api/list';\nimport { getUserAlbumSublist, getUserDetail, getUserPlaylist, getUserRecord } from '@/api/user';\nimport { navigateToMusicList } from '@/components/common/MusicListNavigator';\nimport PlayBottom from '@/components/common/PlayBottom.vue';\nimport SongItem from '@/components/common/SongItem.vue';\nimport { usePlayerStore } from '@/store/modules/player';\nimport { useUserStore } from '@/store/modules/user';\nimport type { Playlist } from '@/types/listDetail';\nimport type { IUserDetail } from '@/types/user';\nimport { getImgUrl, isElectron, isMobile, setAnimationClass, setAnimationDelay } from '@/utils';\nimport { checkLoginStatus as checkAuthStatus } from '@/utils/auth';\nimport LoginComponent from '@/views/login/index.vue';\n\ndefineOptions({\n  name: 'User'\n});\n\nconst { t } = useI18n();\nconst userStore = useUserStore();\nconst playerStore = usePlayerStore();\nconst router = useRouter();\nconst userDetail = ref<IUserDetail>();\nconst recordList = ref();\nconst infoLoading = ref(false);\nconst mounted = ref(true);\nconst list = ref<Playlist>();\nconst listLoading = ref(false);\nconst message = useMessage();\n\n// Tab 相关\nconst tabs = [\n  { key: 'created', label: 'user.tabs.created' },\n  { key: 'favorite', label: 'user.tabs.favorite' },\n  { key: 'album', label: 'user.tabs.album' }\n];\nconst currentTab = ref('created');\n\nconst user = computed(() => userStore.user);\n\n// 创建的歌单（当前用户创建的）\nconst createdPlaylists = computed(() => {\n  if (!user.value) return [];\n  return userStore.playList.filter((item) => item.creator?.userId === user.value!.userId);\n});\n\n// 收藏的歌单（当前用户收藏的）\nconst favoritePlaylists = computed(() => {\n  if (!user.value) return [];\n  return userStore.playList.filter((item) => item.creator?.userId !== user.value!.userId);\n});\n\n// 当前显示的列表（根据 tab 切换）\nconst currentList = computed(() => {\n  if (currentTab.value === 'album') {\n    return userStore.albumList;\n  }\n  return currentTab.value === 'created' ? createdPlaylists.value : favoritePlaylists.value;\n});\n\n// 获取封面图片 URL\nconst getCoverUrl = (item: any) => {\n  return item.coverImgUrl || item.picUrl || '';\n};\n\n// 获取列表项描述\nconst getItemDescription = (item: any) => {\n  if (currentTab.value === 'album') {\n    // 专辑：显示艺术家和歌曲数量\n    const artist = item.artist?.name || '';\n    const size = item.size ? ` · ${item.size}首` : '';\n    return `${artist}${size}`;\n  } else {\n    // 歌单：显示曲目数和播放量\n    return `${t('user.playlist.trackCount', { count: item.trackCount })}，${t('user.playlist.playCount', { count: item.playCount })}`;\n  }\n};\n\n// 统一处理列表项点击\nconst handleItemClick = (item: any) => {\n  if (currentTab.value === 'album') {\n    openAlbum(item);\n  } else {\n    openPlaylist(item);\n  }\n};\n\nconst goToImportPlaylist = () => {\n  router.push('/playlist/import');\n};\n\nonBeforeUnmount(() => {\n  mounted.value = false;\n});\n\n// 检查登录状态\nconst checkLoginStatus = () => {\n  // userStore 的状态已经在 App.vue 中全局初始化，这里只需要检查\n  if (userStore.user && userStore.loginType) {\n    return true;\n  }\n\n  // 如果还是没有登录信息，跳转到登录页\n  const loginInfo = checkAuthStatus();\n  if (!loginInfo.isLoggedIn) {\n    !isMobile.value && router.push('/login');\n    return false;\n  }\n\n  return true;\n};\n\nconst loadPage = async () => {\n  if (!mounted.value) return;\n\n  // 检查登录状态\n  if (!checkLoginStatus()) return;\n\n  await loadData();\n};\n\nconst loadData = async () => {\n  try {\n    infoLoading.value = true;\n\n    if (!user.value) {\n      console.warn('用户数据不存在，尝试重新获取');\n      // 可以尝试重新获取用户数据\n      return;\n    }\n\n    // 如果 store 中还没有数据，则加载\n    const promises = [getUserDetail(user.value.userId), getUserRecord(user.value.userId)];\n\n    if (userStore.playList.length === 0) {\n      promises.push(getUserPlaylist(user.value.userId));\n    }\n\n    const results = await Promise.all(promises);\n\n    if (!mounted.value) return;\n\n    userDetail.value = results[0].data;\n    recordList.value = results[1].data.allData.map((item: any) => ({\n      ...item,\n      ...item.song,\n      picUrl: item.song.al.picUrl\n    }));\n\n    // 如果加载了歌单，更新 store\n    if (results.length > 2 && results[2].data?.playlist) {\n      userStore.playList = results[2].data.playlist;\n    }\n  } catch (error: any) {\n    console.error('加载用户页面失败:', error);\n    if (error.response?.status === 401) {\n      userStore.handleLogout();\n      router.push('/login');\n    } else {\n      // 添加更多错误处理和重试逻辑\n      message.error(t('user.message.loadFailed'));\n    }\n  } finally {\n    if (mounted.value) {\n      infoLoading.value = false;\n    }\n  }\n};\n\n// 加载专辑列表\nconst loadAlbumList = async () => {\n  // 如果 store 中已经有数据，直接返回\n  if (userStore.albumList.length > 0) {\n    return;\n  }\n\n  try {\n    infoLoading.value = true;\n    const res = await getUserAlbumSublist({ limit: 100, offset: 0 });\n    if (!mounted.value) return;\n    // 更新 store 中的专辑列表\n    userStore.albumList = res.data.data || [];\n  } catch (error: any) {\n    console.error('加载专辑列表失败:', error);\n    message.error('加载专辑列表失败');\n  } finally {\n    if (mounted.value) {\n      infoLoading.value = false;\n    }\n  }\n};\n\n// 监听路由变化\nwatch(\n  () => router.currentRoute.value.path,\n  (newPath) => {\n    console.log('newPath', newPath);\n    if (newPath === '/user') {\n      checkLoginStatus();\n      loadData();\n    }\n  }\n);\n\n// 监听用户状态变化\nwatch(\n  () => userStore.user,\n  (newUser) => {\n    if (!mounted.value) return;\n    if (newUser) {\n      checkLoginStatus();\n      loadPage();\n    }\n  }\n);\n\n// 监听 tab 切换\nwatch(currentTab, async (newTab) => {\n  if (newTab === 'album') {\n    // 刷新收藏专辑列表到 store\n    await userStore.initializeCollectedAlbums();\n    // 如果 store 中列表为空，则加载\n    if (userStore.albumList.length === 0) {\n      loadAlbumList();\n    }\n  }\n});\n\n// 页面挂载时检查登录状态\nonMounted(() => {\n  checkLoginStatus() && loadData();\n});\n\n// 替换显示歌单的方法\nconst openPlaylist = (item: any) => {\n  listLoading.value = true;\n\n  getListDetail(item.id).then((res) => {\n    list.value = res.data.playlist;\n    listLoading.value = false;\n\n    navigateToMusicList(router, {\n      id: item.id,\n      type: 'playlist',\n      name: item.name,\n      songList: res.data.playlist.tracks || [],\n      listInfo: res.data.playlist,\n      canRemove: true // 保留可移除功能\n    });\n  });\n};\n\n// 打开专辑\nconst openAlbum = async (item: any) => {\n  // 使用专辑 API 获取专辑详情\n  try {\n    listLoading.value = true;\n    const { getAlbumDetail } = await import('@/api/music');\n    const res = await getAlbumDetail(item.id.toString());\n\n    if (res.data?.album && res.data?.songs) {\n      const albumData = res.data.album;\n      const songs = res.data.songs.map((item) => ({\n        ...item,\n        picUrl: albumData.picUrl\n      }));\n\n      navigateToMusicList(router, {\n        id: item.id,\n        type: 'album',\n        name: albumData.name,\n        songList: songs,\n        listInfo: albumData,\n        canRemove: false // 专辑不支持移除歌曲\n      });\n    }\n  } catch (error) {\n    console.error('加载专辑失败:', error);\n    message.error('加载专辑失败');\n  } finally {\n    listLoading.value = false;\n  }\n};\n\nconst handlePlay = () => {\n  const tracks = recordList.value || [];\n  playerStore.setPlayList(tracks);\n};\n\n// 显示关注列表\nconst showFollowList = () => {\n  if (!user.value) return;\n  router.push('/user/follows');\n};\n\n// // 显示粉丝列表\n// const showFollowerList = () => {\n//   if (!user.value) return;\n//   router.push('/user/followers');\n// };\n\nconst handleLoginSuccess = () => {\n  // 处理登录成功后的逻辑\n  checkLoginStatus();\n  loadData();\n};\n\nconst isLoggedIn = computed(() => userStore.user);\nconst currentLoginType = computed(() => userStore.loginType);\n</script>\n\n<style lang=\"scss\" scoped>\n.user-page {\n  @apply flex h-full;\n  .left {\n    max-width: 600px;\n    @apply flex-1 rounded-2xl overflow-hidden relative bg-no-repeat h-full;\n    @apply bg-gray-900 dark:bg-gray-800;\n\n    .page {\n      @apply p-4 w-full z-10 flex flex-col h-full;\n      @apply bg-black bg-opacity-40;\n    }\n    .title {\n      @apply text-lg font-bold flex items-center justify-between;\n      @apply text-gray-900 dark:text-white;\n    }\n    .user-name {\n      @apply text-xl font-bold mb-4 flex justify-between;\n      @apply text-white text-opacity-70;\n    }\n\n    .uesr-signature {\n      @apply mt-4;\n      @apply text-white text-opacity-70;\n    }\n\n    .user-info {\n      @apply flex items-center;\n      &-list {\n        @apply flex justify-around w-2/5 text-center;\n        @apply text-white text-opacity-70;\n\n        .label {\n          @apply text-xl font-bold text-white;\n        }\n      }\n\n      &-item {\n        @apply cursor-pointer;\n      }\n    }\n  }\n\n  .right {\n    @apply flex-1 ml-4 overflow-hidden h-full;\n\n    .record-list {\n      @apply rounded-2xl;\n      @apply bg-light dark:bg-black;\n      height: calc(100% - 60px);\n\n      .record-item {\n        @apply flex items-center px-2 mb-2 rounded-2xl bg-light-100 dark:bg-dark-100;\n      }\n\n      .song-item {\n        @apply flex-1;\n      }\n\n      .play-score {\n        @apply text-gray-500 dark:text-gray-400 mr-2 text-lg w-10 h-10 rounded-full flex items-center justify-center;\n      }\n    }\n\n    .title {\n      @apply text-xl font-bold m-4;\n      @apply text-gray-900 dark:text-white;\n    }\n  }\n}\n\n.play-list {\n  @apply mt-4 py-4 px-2 rounded-xl flex-1 overflow-hidden;\n  @apply bg-light dark:bg-black;\n\n  &-title {\n    @apply text-lg;\n    @apply text-gray-900 dark:text-white;\n  }\n\n  &-item {\n    @apply flex items-center px-2 py-2 rounded-xl cursor-pointer w-full;\n    @apply transition-all duration-200;\n    @apply hover:bg-light-200 dark:hover:bg-dark-200;\n\n    &-img {\n      @apply flex items-center justify-center rounded-xl text-[40px] w-[60px] h-[60px] bg-light-300 dark:bg-dark-300;\n      .iconfont {\n        @apply text-[40px];\n      }\n    }\n\n    &-info {\n      @apply ml-2 flex-1;\n    }\n\n    &-name {\n      @apply text-gray-900 dark:text-white text-base flex items-center gap-2;\n\n      .playlist-creator-tag {\n        @apply inline-flex items-center justify-center px-2 rounded-full text-xs;\n        @apply bg-light-300 text-primary dark:bg-dark-300 dark:text-white;\n        @apply border border-primary/20 dark:border-primary/30;\n        height: 18px;\n        font-size: 10px;\n        font-weight: 500;\n        min-width: 60px;\n        backdrop-filter: blur(4px);\n        -webkit-backdrop-filter: blur(4px);\n      }\n    }\n\n    &-count {\n      @apply text-gray-500 dark:text-gray-400;\n    }\n  }\n}\n\n.login-type {\n  @apply text-sm text-green-500 dark:text-green-400;\n}\n\n.mobile {\n  .user-page {\n    @apply px-4;\n  }\n\n  .login-container {\n    @apply flex justify-center items-center h-full w-full;\n  }\n}\n\n:deep(.n-tabs-rail) {\n  @apply rounded-xl overflow-hidden !important;\n  .n-tabs-capsule {\n    @apply rounded-xl !important;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/renderer/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  content: ['./src/renderer/index.html', './src/renderer/**/*.{vue,js,ts,jsx,tsx}'],\n  darkMode: 'class',\n  theme: {\n    extend: {\n      colors: {\n        primary: {\n          DEFAULT: '#000',\n          light: '#fff',\n          dark: '#000'\n        },\n        secondary: {\n          DEFAULT: '#6c757d',\n          light: '#8c959e',\n          dark: '#495057'\n        },\n        dark: {\n          DEFAULT: '#000',\n          100: '#161616',\n          200: '#2d2d2d',\n          300: '#3d3d3d'\n        },\n        light: {\n          DEFAULT: '#fff',\n          100: '#f8f9fa',\n          200: '#e9ecef',\n          300: '#dee2e6'\n        }\n      }\n    }\n  },\n  plugins: []\n};\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }, { \"path\": \"./tsconfig.web.json\" }]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"extends\": \"@electron-toolkit/tsconfig/tsconfig.node.json\",\n  \"include\": [\n    \"electron.vite.config.*\",\n    \"src/main/**/*\",\n    \"src/preload/**/*\",\n    \"src/i18n/**/*\"\n  ],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"types\": [\n      \"electron-vite/node\"\n    ],\n    \"moduleResolution\": \"bundler\",\n  },\n  \"paths\": {\n    \"@/*\": [\n      \"src/renderer/*\"\n    ],\n    \"@renderer/*\": [\n      \"src/renderer/*\"\n    ],\n    \"@main/*\": [\n      \"src/main/*\"\n    ],\n    \"@i18n/*\": [\n      \"src/i18n/*\"\n    ]\n  }\n}"
  },
  {
    "path": "tsconfig.web.json",
    "content": "{\n  \"extends\": \"@electron-toolkit/tsconfig/tsconfig.web.json\",\n  \"include\": [\n    \"src/preload/*.d.ts\",\n    \"src/renderer/**/*\",\n    \"src/renderer/**/*.vue\",\n    \"src/i18n/**/*\",\n    \"src/main/modules/config.ts\",\n    \"src/main/modules/shortcuts.ts\"\n  ],\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"target\": \"esnext\",\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"strict\": true,\n    \"jsx\": \"preserve\",\n    \"sourceMap\": true,\n    \"skipLibCheck\": true,\n    \"resolveJsonModule\": true,\n    \"esModuleInterop\": true,\n    \"baseUrl\": \".\",\n    \"types\": [\n      \"naive-ui/volar\",\n      \"./src/renderer/auto-imports.d.ts\",\n      \"./src/renderer/components.d.ts\"\n    ],\n    \"paths\": {\n      \"@/*\": [\"src/renderer/*\"],\n      \"@renderer/*\": [\"src/renderer/*\"],\n      \"@main/*\": [\"src/main/*\"],\n      \"@i18n/*\": [\"src/i18n/*\"]\n    }\n  }\n}\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import vue from '@vitejs/plugin-vue';\nimport { resolve } from 'path';\nimport AutoImport from 'unplugin-auto-import/vite';\nimport { NaiveUiResolver } from 'unplugin-vue-components/resolvers';\nimport Components from 'unplugin-vue-components/vite';\nimport { defineConfig } from 'vite';\nimport viteCompression from 'vite-plugin-compression';\nimport VueDevTools from 'vite-plugin-vue-devtools';\n\nexport default defineConfig({\n  base: './',\n  // 项目src\n  root: resolve('src/renderer'),\n  resolve: {\n    alias: {\n      '@': resolve('src/renderer'),\n      '@renderer': resolve('src/renderer'),\n      '@i18n': resolve('src/i18n')\n    }\n  },\n  plugins: [\n    vue(),\n    viteCompression(),\n    VueDevTools(),\n    AutoImport({\n      imports: [\n        'vue',\n        {\n          'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar']\n        }\n      ]\n    }),\n    Components({\n      resolvers: [NaiveUiResolver()]\n    })\n  ],\n  publicDir: resolve('resources'),\n  server: {\n    host: '0.0.0.0',\n    proxy: {}\n  }\n});\n"
  }
]