[
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: \"🐞 Bug报告\"\ndescription: 创建一个报告来帮助我们改进产品\ntitle: '[Bug]: '\nlabels: ['bug']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        **在开始之前...**\n\n        此表单仅用于提交Bug报告。如果您有使用问题或不确定这是否真的是一个Bug，请确保：\n\n        - 阅读[文档](https://music.moekoe.cn/)\n        - 搜索是否有类似的问题 - 它可能已经被回答或修复\n        - 提供有效的 Bug 描述和明确的问题内容，否则 issue 将可能被直接关闭或忽略处理\n\n        如果您发现一个旧的、已关闭的问题在最新版本中仍然存在，请使用下面的表单打开一个新问题。\n  - type: input\n    id: version\n    attributes:\n      label: 产品版本\n      placeholder: 例如：@ MoeKoe Music V1.4.3 - darwin\n    validations:\n      required: true\n  - type: textarea\n    id: steps-to-reproduce\n    attributes:\n      label: 复现步骤\n      description: |\n        我们需要做什么才能复现这个bug？请提供清晰简洁的复现说明，这对我们及时分类您的问题很重要。\n      placeholder: |\n        1. 打开...\n        2. 点击...\n        3. 滚动到...\n        4. 查看错误\n    validations:\n      required: true\n  - type: textarea\n    id: expected\n    attributes:\n      label: 预期行为\n      description: 您期望看到什么？\n    validations:\n      required: true\n  - type: textarea\n    id: actually-happening\n    attributes:\n      label: 实际行为\n      description: 实际发生了什么？\n    validations:\n      required: true\n  - type: textarea\n    id: system-info\n    attributes:\n      label: 系统信息\n      description: 操作系统、网络环境、设备等\n      placeholder: |\n        - 操作系统: [例如 Windows 10, macOS 12.0, Linux]\n        - 网络环境: [例如 中国, 日本, 移动, WiFi]\n        - 设备信息: [例如 笔记本, GTX1060, 16G, 台式机]\n  - type: textarea\n    id: additional-comments\n    attributes:\n      label: 其他补充说明\n      description: 例如：一些关于您如何遇到这个bug的背景/上下文。"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 🌈 社区交流\n    url: https://github.com/iAJue/MoeKoeMusic/discussions\n    about: 加入我们的社区进行交流和讨论\n  - name: 📚 项目文档\n    url: https://music.moekoe.cn/\n    about: 查看项目文档以获取更多帮助信息\n  - name: 🛠️ 开发与编译\n    url: https://github.com/iAJue/MoeKoeMusic?tab=readme-ov-file#%EF%B8%8F-%E5%BC%80%E5%8F%91\n    about: 了解项目如何进行开发和编译\n  - name: 🤝 贡献指南\n    url: https://github.com/iAJue/MoeKoeMusic/blob/main/CONTRIBUTING.md\n    about: 了解如何为项目做出贡献"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/discussion.yml",
    "content": "name: \"💬 讨论问题\"\ndescription: 提出一个需要讨论的话题或问题\ntitle: '[讨论]: '\nlabels: ['question']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        **欢迎参与讨论！**\n        \n        这个模板适用于那些不属于Bug报告或新特性请求的讨论话题。\n        例如：设计决策、架构问题、最佳实践问题等。\n        \n        在开始之前，请确保：\n        \n        - 您已经搜索过现有的讨论，避免重复\n        - 您的问题足够清晰，以便其他人能够理解和参与讨论\n        \n        或前往 [社区](https://github.com/iAJue/MoeKoeMusic/discussions) 进行讨论\n  - type: textarea\n    id: topic\n    attributes:\n      label: 讨论主题\n      description: 请简明扼要地描述您想讨论的主题\n      placeholder: 我想讨论关于...\n    validations:\n      required: true\n  - type: textarea\n    id: context\n    attributes:\n      label: 背景和上下文\n      description: 请提供一些背景信息，帮助其他人理解为什么这个话题值得讨论\n      placeholder: |\n        这个话题在以下情况下很重要...\n        我遇到了以下挑战...\n    validations:\n      required: true\n  - type: textarea\n    id: questions\n    attributes:\n      label: 关键问题\n      description: 您希望通过此讨论解答哪些问题？\n      placeholder: |\n        1. 我们应该如何处理...？\n        2. 什么是...的最佳实践？\n        3. 社区对...有什么看法？\n    validations:\n      required: true\n  - type: textarea\n    id: proposed-ideas\n    attributes:\n      label: 您的想法\n      description: 您对这个话题有什么想法或建议？分享您的初步思考\n      placeholder: 我认为我们可以...\n    validations:\n      required: false\n  - type: dropdown\n    id: topic-area\n    attributes:\n      label: 话题领域\n      description: 这个讨论主要涉及哪个领域？\n      options:\n        - 架构设计\n        - 用户体验\n        - 性能优化\n        - 开发流程\n        - 文档改进\n        - 社区建设\n        - 其他\n    validations:\n      required: false\n  - type: textarea\n    id: additional-info\n    attributes:\n      label: 补充信息\n      description: 还有什么其他信息可以帮助丰富这次讨论？\n      placeholder: 相关资源、链接、截图等...\n    validations:\n      required: false "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: \"✨ 新特性请求\"\ndescription: 为项目提出一个新想法或建议\ntitle: '[新需求]: '\nlabels: ['enhancement']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        **感谢您的新特性建议！**\n\n        请花点时间填写以下表单，以便我们更好地理解您的需求。\n        在提交之前，请确保：\n        \n        - 搜索现有的issues和讨论，确保这个特性尚未被提出\n        - 确认这是一个新特性而不是bug修复\n        - 有完整的上下文链，帮助我们评估是否是值得添加的特性\n  - type: textarea\n    id: problem-description\n    attributes:\n      label: 问题描述\n      description: 您想解决什么问题？请简明扼要地描述您遇到的问题或痛点。\n      placeholder: 我在使用产品时遇到的问题是...\n    validations:\n      required: true\n  - type: textarea\n    id: solution-description\n    attributes:\n      label: 解决方案描述\n      description: 您建议如何解决这个问题？请描述您期望的功能或改进。\n      placeholder: 我希望产品能够...\n    validations:\n      required: true\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: 替代方案\n      description: 您考虑过哪些替代解决方案或功能？\n      placeholder: 我也考虑过通过...来解决这个问题\n    validations:\n      required: false\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: 其他上下文\n      description: 您还有什么其他信息、截图或示例可以帮助我们更好地理解这个特性请求？\n      placeholder: 其他相关信息...\n    validations:\n      required: false\n  - type: dropdown\n    id: importance\n    attributes:\n      label: 重要程度\n      description: 您认为这个特性对您使用产品的重要程度如何？\n      options:\n        - 必需（无法没有它）\n        - 重要（显著改善体验）\n        - 一般（有帮助但不关键）\n        - 微小（锦上添花）\n    validations:\n      required: false"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/bug_fix.md",
    "content": "---\nname: 🐞 Bug Fix | 修复 Bug\nabout: 修复现有的问题或异常\ntitle: 'fix: 修复 [问题名称]'\nlabels: bug\n---\n\n## 🐛 修复了什么？What is fixed?\n\n请简要说明修复的 Bug 内容：\n> Describe the bug that has been fixed.\n\n---\n\n## 🔍 修复方法 How was it fixed?\n\n> 简述你是如何定位并解决该问题的。\nExplain your approach or steps taken to fix the issue.\n\n---\n\n## ✅ 测试验证 How did you test?\n\n- [ ] 本地测试通过 Passed local tests\n- [ ] 已测试关键路径功能 Tested critical paths\n- [ ] 附加截图或日志（如适用）Attached screenshot/logs (if applicable)\n\n---\n\n## 🔗 相关 Issues / Related Issues\n\n> Closes #xxx 或 Related to #xxx\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/docs_update.md",
    "content": "---\nname: 📝 Docs Update | 文档更新\nabout: 提交文档相关的修改\ntitle: 'docs: 更新 [文档主题]'\nlabels: documentation\n---\n\n## 📄 更新内容 Description\n\n请说明你更新了哪些文档，以及为什么更新它们：\n> Describe what parts of the documentation were updated and why.\n\n---\n\n## 📌 文档类型 Type of Docs\n\n- [ ] 使用说明 Usage guide\n- [ ] 开发者文档 Developer guide\n- [ ] API 文档 API references\n- [ ] 项目规范 Project rules\n- [ ] 其他 Other: _________\n\n---\n\n## ✅ 其他说明 Notes\n\n> 是否与代码变更有关？是否同步更新了？\nMention if this change is associated with any code changes or dependencies.\n\n---\n\n## 🔗 相关 Issues / Related Issues\n\n> Ref #xxx（如有关联）\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE/feature_request.md",
    "content": "---\nname: ✨ New Feature | 新功能\nabout: 实现一个新功能或对现有功能进行增强\ntitle: 'feat: 实现 [功能名称]'\nlabels: enhancement\n---\n\n## 🌟 实现了什么？What is added or changed?\n\n请说明新增了哪些功能、修改了哪些行为：\n> Describe the new functionality or changes introduced.\n\n---\n\n## 🎯 为什么需要这个改动？Why is it needed?\n\n> 请说明这个功能的背景、价值或需求来源。\nExplain the motivation behind this feature or change.\n\n---\n\n## 🧪 如何测试？How to test?\n\n- [ ] 添加了单元测试 Added unit tests\n- [ ] 手动验证通过 Verified manually\n- [ ] 不需要额外测试 No additional test needed\n\n---\n\n## 🔗 相关 Issues / Related Issues\n\n> Resolves #xxx 或 Implements #xxx\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## ✨ 变更类型 Type of Change\n\n- [ ] Bug 修复 (fix)\n- [ ] 新功能 (feat)\n- [ ] 文档更新 (docs)\n- [ ] 样式调整 (style)\n- [ ] 重构 (refactor)\n- [ ] 测试相关 (test)\n- [ ] 构建/工具 (chore)\n- [ ] 其他 (other):\n\n---\n\n## 📋 变更描述 Description\n\n请简要描述此次变更内容：\n> Describe your changes here.\n\n---\n\n## 🐛 如果是 Bug 修复，请描述问题和修复方法 Bug Fix\n\n- 问题是什么？What was the problem?\n- 如何修复的？How was it fixed?\n\n---\n\n## ✅ 测试验证 How did you test?\n\n- [ ] 本地测试通过 Passed local tests\n- [ ] 关键功能测试 Tested critical features\n- [ ] 其他测试描述 (如自动化测试、手动测试等)：\n> Please describe testing details\n\n---\n\n## 📚 相关 Issues / Related Issues\n\n> Closes #xxx 或 Related to #xxx\n\n---\n\n## 📷 截图 / Screenshots (如果适用)\n\n> 如果有界面变动，附上截图或录屏\n"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nWe actively maintain security updates for the following versions:\n\n| Version | Supported          |\n|---------|--------------------|\n| 1.x     | ✅                 |\n| 0.x     | ❌ (no longer supported) |\n\n## Reporting a Vulnerability\n\nIf you discover a security vulnerability in this project, please follow these steps:\n\n1. **Do not open a public issue.**  \n   Please email us **privately** to allow time for remediation before public disclosure.\n\n2. **Contact:**\n   - 📧 Email: [MoeJue@qq.com](mailto:MoeJue@qq.com)\n   - 🕒 Expected Response Time: 1–3 business days\n\n3. **Information to include in your report:**\n   - A clear and detailed description of the vulnerability\n   - Steps to reproduce the issue\n   - Potential impact or severity\n   - (Optional) Any suggested fix or patch\n\n4. **Responsible Disclosure Policy:**\n   We believe in responsible disclosure and commit to:\n   - Confirming and triaging valid reports promptly\n   - Patching and releasing a fix within **30 days**\n   - Crediting the reporter in the changelog (with consent)\n\n## Security Best Practices (for users)\n\nTo keep your environment safe, we recommend:\n- Always use the latest stable version\n- Avoid using deprecated versions\n- Keep your dependencies updated regularly\n- Use a secure environment (e.g. HTTPS, proper file permissions)\n\n---\n\n🔐 Thank you for helping us keep our project safe and secure!\n"
  },
  {
    "path": ".github/workflows/AiIssueCheck.yml",
    "content": "name: AI Issue Checker\n\non:\n  issues:\n    types: [opened, edited] \n\npermissions:\n  issues: write\n  pull-requests: write\n\njobs:\n  check-issue:\n    if: |\n      github.event_name == 'issues' &&\n      (\n        github.event.action == 'opened' ||\n        (\n          github.event.action == 'edited' &&\n          github.event.issue.state == 'open' &&\n          contains(github.event.issue.labels.*.name, 'invalid')\n        )\n      )\n    runs-on: ubuntu-latest\n    steps:\n      - name: Debug API Key existence\n        run: |\n          if [ -z \"${{ secrets.OPENAI_API_KEY }}\" ]; then\n            echo \"❌ OPENAI_API_KEY is NOT set\"\n            exit 1\n          else\n            echo \"✅ OPENAI_API_KEY is set\"\n            echo \"API Key length: ${#OPENAI_API_KEY}\"\n          fi\n        env:\n          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n\n      - name: Run AI Issue Validation\n        uses: actions/github-script@v6\n        env:\n          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n          ISSUE_RULES: ${{ vars.ISSUE_RULES }}\n          OPENAI_API_URL: ${{ secrets.OPENAI_API_URL }}\n        with:\n          script: |\n            const issue = context.payload.issue\n            const issueBody = issue.body || \"\"\n            const rules = process.env.ISSUE_RULES\n\n            const response = await fetch(`${process.env.OPENAI_API_URL}`, {\n              method: \"POST\",\n              headers: {\n                \"Content-Type\": \"application/json\",\n                \"Authorization\": `Bearer ${process.env.OPENAI_API_KEY}`\n              },\n              body: JSON.stringify({\n                model: \"gpt-4o-mini\",\n                messages: [\n                  { role: \"system\", content: rules },\n                  { role: \"user\", content: issueBody }\n                ],\n                max_tokens: 200\n              })\n            })\n\n            const data = await response.json()\n            let result\n            try {\n              result = JSON.parse(data.choices[0].message.content)\n            } catch (e) {\n              result = { valid: true, missing: [] }\n            }\n\n            const { data: comments } = await github.rest.issues.listComments({\n              issue_number: issue.number,\n              owner: context.repo.owner,\n              repo: context.repo.repo\n            })\n\n            const botComment = comments.find(c => c.user.type === \"Bot\" && c.body.includes(\"感谢提交 Issue\"))\n\n            if (!result.valid) {\n              const missingText = result.missing.join(\"、\")\n              const reasonText = result.reason\n              const newBody = `⚠️ 感谢提交 Issue，但你的内容缺少以下部分：**${missingText}**。\\n\\n**${reasonText}**\\n\\n请根据模板补充完整后再提交，谢谢！`\n\n              if (botComment) {\n                await github.rest.issues.updateComment({\n                  comment_id: botComment.id,\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  body: newBody\n                })\n              } else {\n                await github.rest.issues.createComment({\n                  issue_number: issue.number,\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  body: newBody\n                })\n              }\n\n              await github.rest.issues.addLabels({\n                issue_number: issue.number,\n                owner: context.repo.owner,\n                repo: context.repo.repo,\n                labels: [\"invalid\"]\n              })\n            } else {\n              const passBody = `✅ 感谢提交 Issue，你的 Issue 已经符合要求了，我们会尽快处理～\\n\\n💖 听说点了Star许愿的成功率更高哦~`\n\n              if (botComment) {\n                await github.rest.issues.updateComment({\n                  comment_id: botComment.id,\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  body: passBody\n                })\n              } else {\n                await github.rest.issues.createComment({\n                  issue_number: issue.number,\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  body: passBody\n                })\n              }\n\n              try {\n                await github.rest.issues.removeLabel({\n                  issue_number: issue.number,\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  name: \"invalid\"\n                })\n              } catch (e) {\n              }\n            }\n"
  },
  {
    "path": ".github/workflows/AutoCloseIssues.yml",
    "content": "name: Auto Close Invalid Issues\n\non:\n  schedule:\n    - cron: \"0 3 * * *\"   # 每天 UTC 03:00 运行一次\n  workflow_dispatch:\n\npermissions:\n  issues: write\n\njobs:\n  close-invalid:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Close invalid issues\n        uses: actions/github-script@v6\n        with:\n          script: |\n            const { data: issues } = await github.rest.issues.listForRepo({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              state: \"open\",\n              labels: \"invalid\",\n              per_page: 100\n            })\n\n            const now = new Date()\n            for (const issue of issues) {\n              const updatedAt = new Date(issue.updated_at)\n              const hoursSinceUpdate = (now - updatedAt) / (1000 * 60 * 60)\n\n              if (hoursSinceUpdate > 24) {\n                console.log(`Closing issue #${issue.number}`)\n\n                const { data: comments } = await github.rest.issues.listComments({\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  issue_number: issue.number\n                })\n\n                const botComment = comments.find(c => c.user.type === \"Bot\")\n\n                const message = \"⏳ 由于此 Issue 已标记为 **invalid** 并且超过 24 小时未更新，现自动关闭。\"\n\n                if (botComment) {\n                  await github.rest.issues.updateComment({\n                    owner: context.repo.owner,\n                    repo: context.repo.repo,\n                    comment_id: botComment.id,\n                    body: message\n                  })\n                } else {\n                  await github.rest.issues.createComment({\n                    owner: context.repo.owner,\n                    repo: context.repo.repo,\n                    issue_number: issue.number,\n                    body: message\n                  })\n                }\n\n                await github.rest.issues.update({\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  issue_number: issue.number,\n                  state: \"closed\"\n                })\n              }\n            }"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\n\non:\n  push:\n    branches:\n      - main\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\njobs:\n  # 创建版本号\n  increment-version:\n    if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && contains(github.event.head_commit.message, 'release'))\n    name: Increment Version\n    runs-on: ubuntu-latest\n    outputs:\n      version: ${{ steps.set-version.outputs.NEW_VERSION }}\n      body: ${{ steps.create-release.outputs.RELEASE_BODY }}\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n        with:\n          fetch-depth: 0\n          submodules: recursive\n      - name: Increment version\n        id: set-version\n        run: |\n          VERSION=$(git tag --sort=-v:refname | head -n 1)\n          MAJOR=$(echo $VERSION | awk -F. '{print $1}' | sed 's/v//')\n          MINOR=$(echo $VERSION | awk -F. '{print $2}')\n          PATCH=$(echo $VERSION | awk -F. '{print $3}')\n          PATCH=$((PATCH + 1))\n          if [ \"$PATCH\" -gt 9 ]; then\n            PATCH=0\n            MINOR=$((MINOR + 1))\n          fi\n          NEW_VERSION=\"v$MAJOR.$MINOR.$PATCH\"\n          echo \"::set-output name=NEW_VERSION::$NEW_VERSION\"\n\n      - name: Create release body\n        id: create-release\n        run: |\n          # 获取上一个tag\n          PREV_TAG=$(git describe --tags --abbrev=0)\n          # 获取最后一个tag到现在的所有commit信息\n          COMMITS=$(git log ${PREV_TAG}..HEAD --pretty=format:\"* %s\")\n          # 如果没有commit信息，使用默认消息\n          if [ -z \"$COMMITS\" ]; then\n            COMMITS=\"* Regular update and bug fixes\"\n          fi\n          # 创建changelog链接\n          CHANGELOG_LINK=\"https://github.com/${GITHUB_REPOSITORY}/compare/${PREV_TAG}...${{ steps.set-version.outputs.NEW_VERSION }}\"\n          # 创建release body\n          RELEASE_BODY=\"## What's Changed\\n\\n${COMMITS}\\n\\n**Full Changelog**: [${PREV_TAG}...${{ steps.set-version.outputs.NEW_VERSION }}](${CHANGELOG_LINK})\"\n          # 将release body转义后输出\n          echo \"RELEASE_BODY<<EOF\" >> $GITHUB_OUTPUT\n          echo -e \"$RELEASE_BODY\" >> $GITHUB_OUTPUT\n          echo \"EOF\" >> $GITHUB_OUTPUT\n\n      - name: Create GitHub Release\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ steps.set-version.outputs.NEW_VERSION }}\n          token: ${{ secrets.GITHUB_TOKEN }}\n          name: \"Release ${{ steps.set-version.outputs.NEW_VERSION }}\"\n          body: ${{ steps.create-release.outputs.RELEASE_BODY }}\n          prerelease: false\n          draft: false\n\n\n  build-and-upload:\n    name: Build and Upload\n    needs: increment-version\n    strategy:\n      matrix:\n        node: [lts/*]\n        os: [ubuntu-latest, windows-latest, macos-latest]\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 30\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          submodules: true\n\n      - name: Setup Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node }}\n          cache: 'npm' # use cache\n\n      - name: Build\n        env:\n          os: ${{ runner.os == 'Windows' && 'win' || runner.os == 'macOS' && 'macos' || 'linux' }}\n        run: |\n          npm run install-all\n          npm run build\n          npm run electron:build:${{ env.os }}\n\n      - name: Rename files - win\n        if: runner.os == 'Windows'\n        run: |\n          mv \"./dist_electron/MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe\" \"./MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe\"\n          mv \"./dist_electron/MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe.blockmap\" \"./MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe.blockmap\"\n          mv \"./dist_electron/latest.yml\" \"./latest.yml\"\n      - name: Rename files - mac\n        if: runner.os == 'macOS'\n        run: |\n          mv \"./dist_electron/MoeKoe Music-arm64.dmg\" \"./MoeKoe_Music_${{ needs.increment-version.outputs.version }}-arm64.dmg\"\n          mv \"./dist_electron/MoeKoe Music-x64.dmg\" \"./MoeKoe_Music_${{ needs.increment-version.outputs.version }}-x64.dmg\"\n      - name: Rename files - linux\n        if: runner.os == 'Linux'\n        run: |\n          mv \"./dist_electron/MoeKoe Music.AppImage\" \"./MoeKoe_Music_${{ needs.increment-version.outputs.version }}.AppImage\"\n          mv \"./dist_electron/MoeKoe Music.deb\" \"./MoeKoe_Music_${{ needs.increment-version.outputs.version }}_amd64.deb\"\n\n      - name: Upload to Release\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ needs.increment-version.outputs.version }}\n          files: |\n            ./MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe\n            ./MoeKoe_Music_Setup_${{ needs.increment-version.outputs.version }}.exe.blockmap\n            ./latest.yml\n\n            ./MoeKoe_Music_${{ needs.increment-version.outputs.version }}-arm64.dmg\n            ./MoeKoe_Music_${{ needs.increment-version.outputs.version }}-x64.dmg\n\n            ./MoeKoe_Music_${{ needs.increment-version.outputs.version }}.AppImage\n            ./MoeKoe_Music_${{ needs.increment-version.outputs.version }}_amd64.deb\n"
  },
  {
    "path": ".github/workflows/version.yml",
    "content": "name: Version Check\n\non:\n  workflow_dispatch:\n\npermissions:\n  contents: read\n\njobs:\n  # 计算版本号\n  check-version:\n    if: github.event_name == 'workflow_dispatch'\n    name: Check Next Version\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v2\n        with:\n          fetch-depth: 0\n      \n      - name: Get latest tag\n        run: |\n          echo \"Latest tag: $(git tag --sort=-v:refname | head -n 1)\"\n          \n      - name: Calculate next version\n        id: set-version\n        run: |\n          VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo \"v1.2.0\")\n          MAJOR=$(echo $VERSION | awk -F. '{print $1}' | sed 's/v//')\n          MINOR=$(echo $VERSION | awk -F. '{print $2}')\n          PATCH=$(echo $VERSION | awk -F. '{print $3}')\n          PATCH=$((PATCH + 1))\n          if [ \"$PATCH\" -gt 9 ]; then\n            PATCH=0\n            MINOR=$((MINOR + 1))\n          fi\n          NEW_VERSION=\"v$MAJOR.$MINOR.$PATCH\"\n          echo \"::set-output name=NEW_VERSION::$NEW_VERSION\"\n          \n      - name: Get previous tag\n        run: |\n          PREV_TAG=$(git describe --tags --abbrev=0)\n          echo \"Previous version: $PREV_TAG\"\n          \n      - name: Display version information\n        run: |\n          echo \"Current version: $(git describe --tags --abbrev=0)\"\n          echo \"Next version will be: ${{ steps.set-version.outputs.NEW_VERSION }}\"\n          echo \"Changes since last tag:\"\n          git log $(git describe --tags --abbrev=0)..HEAD --pretty=format:\"* %s\"\n        \n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n.history\n\nnode_modules\ndist_electron\ndist\ndist-ssr\n*.local\ntest\nbin/*\n.serena/*\napi/.serena/*\n\n# Editor directories and files\n.codebuddy/*\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\ndev-dist/*\npnpm-lock.yaml\npnpm-workspace.yaml\nvite.config.js*\n\nplugins/extensions/*"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"api\"]\n\tpath = api\n\turl = https://github.com/MakcRe/KuGouMusicApi\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# 贡献者公约\n\n## 我们的承诺\n\n为了营造一个开放和友好的环境，作为贡献者和维护者，我们承诺让每个人都能参与我们的项目和社区，享受无骚扰的体验，无论年龄、体型、残疾、种族、性别特征、性别认同和表达、经验水平、教育、社会经济地位、国籍、个人外貌、种族、宗教或性别认同和取向如何。\n\n## 我们的标准\n\n有助于创造积极环境的行为包括：\n\n- 使用友好和包容的语言\n- 尊重不同的观点和经验\n- 优雅地接受建设性的批评\n- 关注对社区最有利的事情\n- 对其他社区成员表示同理心\n\n不可接受的行为包括：\n\n- 使用性暗示的语言或图像\n- 发表侮辱性或贬损性的评论\n- 公开或私下的骚扰\n- 未经明确许可发布他人的私人信息\n- 其他不道德或专业上不适当的行为\n\n## 我们的责任\n\n项目维护者有责任澄清可接受行为的标准，并应对任何不可接受的行为采取适当和公平的纠正措施。\n\n项目维护者有权和责任删除、编辑或拒绝不符合本行为准则的评论、提交、代码、wiki编辑、问题和其他贡献，或暂时或永久地禁止任何贡献者进行其他他们认为不适当、威胁、冒犯或有害的行为。\n\n## 适用范围\n\n本行为准则适用于项目空间和公共空间，当个人代表项目或其社区时。代表项目或社区的例子包括使用官方项目电子邮件地址、通过官方社交媒体账号发布信息，或在线上或线下活动中作为指定代表。项目维护者可以进一步定义和澄清代表项目的情况。\n\n## 执行\n\n可以通过联系项目团队来举报虐待、骚扰或其他不可接受的行为。所有投诉都将被审查和调查，并会做出必要和适当的回应。项目团队有义务对事件报告者保密。\n\n不遵守或不执行行为准则的项目维护者可能会面临项目领导层决定的临时或永久性后果。\n\n## 归属\n\n本行为准则改编自[贡献者公约][homepage]，版本 1.4，可在 https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 查看\n\n[homepage]: https://www.contributor-covenant.org\n\n## 翻译\n\n本行为准则有多个语言版本。请访问 https://www.contributor-covenant.org/translations 查看其他语言版本。 "
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# 贡献指南\n\n感谢您考虑为我们的项目做出贡献！无论是报告bug、提出新功能建议，还是提交代码，您的参与都将帮助我们改进这个项目。\n\n## 目录\n\n- [行为准则](#行为准则)\n- [如何贡献](#如何贡献)\n  - [报告Bug](#报告Bug)\n  - [提出新特性](#提出新特性)\n  - [参与讨论](#参与讨论)\n  - [提交代码](#提交代码)\n- [开发流程](#开发流程)\n  - [环境设置](#环境设置)\n  - [代码风格](#代码风格)\n  - [测试](#测试)\n  - [提交规范](#提交规范)\n- [Pull Request流程](#Pull-Request流程)\n- [分支管理策略](#分支管理策略)\n- [发布流程](#发布流程)\n- [联系我们](#联系我们)\n\n## 行为准则\n\n本项目遵循[贡献者公约](https://www.contributor-covenant.org/)行为准则。参与本项目，即表示您同意遵守此准则。不可接受的行为可以向项目维护者报告。\n\n## 如何贡献\n\n### 报告Bug\n\n如果您发现了bug，请使用项目的Issue模板创建一个新的Issue，并尽可能详细地提供以下信息：\n\n- 查看已有的 Issues，确保该问题尚未被报告\n- 使用\"🐞 Bug报告\"模板\n- 清晰描述发生了什么以及您期望的行为\n- 提供详细的复现步骤\n- 如可能，提供错误的截图或录屏\n- 提供您使用的系统和浏览器信息\n\n### 提出新特性\n\n如果您希望看到新的功能或改进，请使用\"✨ 新特性请求\"模板创建一个Issue，并：\n\n- 清晰描述您希望解决的问题\n- 描述您想要的解决方案\n- 考虑其他可能的替代解决方案\n- 提供相关示例或参考（如果有）\n\n### 参与讨论\n\n对于需要更多社区意见的话题，请使用\"💬 讨论问题\"模板创建一个Issue，或者参与现有的讨论。您的洞见和观点对项目的发展至关重要。\n\n### 提交代码\n\n如果您希望通过代码贡献参与项目，请遵循以下步骤：\n\n1. 查看现有Issues，找到您感兴趣的任务或问题\n2. 在Issue上留言表示您计划处理这个问题，避免重复工作\n3. Fork项目仓库到您的个人账号\n4. 创建一个新的分支进行您的修改\n5. 编写代码和测试\n6. 提交Pull Request\n\n## 开发流程\n\n### 环境设置\n\n1. **Fork 本仓库**  \n   点击页面右上角的 `Fork` 按钮，复制仓库到你的 GitHub 账户。\n\n2. **克隆仓库到本地**  \n   使用 Git 克隆你 Fork 的仓库：\n   ```bash\n   git clone https://github.com/your-username/MoeKoeMusic.git\n   ```\n\n3. **创建一个新的分支**  \n   为你的功能或修复创建一个新的分支：\n   ```bash\n   git checkout -b your-feature-branch\n   ```\n\n4. **安装依赖并进行开发**  \n   请根据项目的文档安装所需的依赖，并开始开发。\n\n5. **提交更改并推送**  \n   完成开发后，提交并推送你的更改：\n   ```bash\n   git add .\n   git commit -m \"Your detailed commit message\"\n   git push origin your-feature-branch\n   ```\n\n6. **提交合并请求（PR）**  \n   提交 PR 之前，请确保你的分支已经更新，并且没有冲突。打开你的 GitHub 仓库，点击 Compare & pull request 提交 PR。\n\n### 代码风格\n\n我们使用ESLint和Prettier来保持代码风格一致。在提交代码前，请确保您的代码符合项目的风格指南：\n\n- 使用统一的代码格式（例如：缩进、空格、命名规范）\n- 遵循项目的代码结构\n- 请编写易于理解的代码，并添加必要的注释\n\n### 测试\n\n提交代码前，请确保所有测试通过：\n\n```bash\nnpm run test\n```\n\n如果您添加了新功能，也请同时添加相应的测试。\n\n### 提交规范\n\n我们使用[约定式提交](https://www.conventionalcommits.org/)规范来格式化提交信息，基本格式如下：\n\n```\n<类型>[可选的作用域]: <描述>\n\n[可选的正文]\n\n[可选的脚注]\n```\n\n常用的提交类型包括：\n\n- **feat**: 新功能\n- **fix**: 修复Bug\n- **docs**: 文档更新\n- **style**: 代码风格调整（不影响代码逻辑）\n- **refactor**: 代码重构\n- **perf**: 性能优化\n- **test**: 添加或更新测试\n- **chore**: 构建过程或辅助工具的变动\n\n另外我们也接受 **Gitmoji** 的提交风格:\n\n| Emoji | 类型          | 说明                    | 示例                                    |\n| ----- | ------------ | ----------------------- | ---------------------------------------- |\n| ✨    | `feat`       | 新功能（Feature）         | `✨ feat: 添加搜索功能`                |\n| 🐛    | `fix`        | 修复 Bug                 | `🐛 fix: 修复登录时闪退的问题`          |\n| ♻️    | `refactor`   | 代码重构（非功能性更改）   | `♻️ refactor: 重构用户模块代码结构`    |\n| 📝    | `docs`       | 修改文档                 | `📝 docs: 更新 README 安装说明`         |\n| 🎨    | `style`      | 格式/排版修改（不影响代码逻辑） | `🎨 style: 调整代码缩进和格式`    |\n| ✅    | `test`       | 添加或修改测试代码         | `✅ test: 添加用户服务单元测试`        |\n| 🚀    | `perf`       | 性能优化                 | `🚀 perf: 提升图片加载速度`              |\n| 🔧    | `chore`      | 构建配置/脚本/依赖等杂项   | `🔧 chore: 更新依赖包`                 |\n| 🔥    | `remove`     | 删除无用代码或文件         | `🔥 remove: 删除未使用的组件`           |\n| 📦    | `build`      | 打包相关改动（构建、CI）    | `📦 build: 配置打包输出目录`          |\n| 🔀    | `merge`      | 合并分支                 | `🔀 merge: 合并 dev 分支`               |\n| 🚧    | `wip`        | 开发中（Work In Progress） | `🚧 wip: 正在实现订单详情页面`         |\n| ⬆️    | `upgrade`    | 升级依赖                 | `⬆️ upgrade: 升级 Electron 到 v28`       |\n| ⬇️    | `downgrade`  | 降级依赖                 | `⬇️ downgrade: 降级 vue-router 到 v4.0.0`|\n| 🐳    | `docker`     | 与 Docker 相关的更改      | `🐳 docker: 添加 Dockerfile`            |\n| 💄    | `ui`         | 修改 UI 或样式           | `💄 ui: 优化按钮样式`                     |\n| 💥    | `breaking`   | 破坏性更新（需注意兼容）    | `💥 breaking: 移除旧版 API`            |\n| 📈    | `analytics`  | 数据分析或埋点代码         | `📈 analytics: 添加页面浏览统计`        |\n| 🔖\t  | `release`\t  | 正式发布一个版本          |\t`🔖 release: v1.2.0`                   |\n\n## Pull Request流程\n\n1. 确保您的Pull Request（PR）有一个清晰的标题和描述\n2. 将您的PR关联到相关的Issue（如果有）\n3. 确保所有自动化测试通过\n4. 至少需要一名项目维护者的代码审查和批准\n5. 如果需要更改，请在同一PR中进行修改\n6. 一旦获得批准，您的代码将被合并到主分支\n\n## 分支管理策略\n\n我们使用以下分支命名和管理策略：\n\n- `main`/`master`: 主分支，包含稳定、可发布的代码\n- `develop`: 开发分支，包含最新的开发代码\n- `feature/*`: 新功能分支，从`develop`分支创建\n- `bugfix/*`: Bug修复分支，从`develop`分支创建\n- `hotfix/*`: 紧急修复分支，从`main`分支创建\n- `release/*`: 发布准备分支，从`develop`分支创建\n\n## 发布流程\n\n我们使用[语义化版本](https://semver.org/)进行版本管理：\n\n- MAJOR版本：当你做了不兼容的API修改\n- MINOR版本：当你做了向下兼容的功能性新增\n- PATCH版本：当你做了向下兼容的问题修正\n\n## 联系我们\n\n如果您有任何问题或需要进一步的帮助，请通过以下方式联系我们：\n\n- 在GitHub上创建Issue\n- 在Blog中留言 [Blog](https://MoeJue.cn)\n\n再次感谢你对项目的支持和贡献！"
  },
  {
    "path": "Dockerfile",
    "content": "# Stage 1: Build Frontend\nFROM node:20-alpine AS frontend-builder\nWORKDIR /app\nCOPY package*.json ./\n# Remove electron and electron-builder from package.json\nRUN node -e \"const fs = require('fs'); const filePath = './package.json'; \\\n             let rawdata = fs.readFileSync(filePath); let packageJson = JSON.parse(rawdata); \\\n             if (packageJson.devDependencies) { \\\n               delete packageJson.devDependencies.electron; \\\n               delete packageJson.devDependencies['electron-builder']; \\\n             } \\\n             if (packageJson.dependencies) { \\\n               delete packageJson.dependencies.electron; \\\n             } \\\n             fs.writeFileSync(filePath, JSON.stringify(packageJson, null, 2));\"\nRUN npm install\nCOPY . .\nRUN npm run build:docker\n\n# Stage 2: Setup Combined App\nFROM node:20-alpine\nWORKDIR /app\n\n# Install Nginx\nRUN apk add --no-cache nginx\n\n# Copy API code\nCOPY ./api ./api\n# Install API dependencies\nWORKDIR /app/api\nRUN npm install --production\n# Reset WORKDIR to /app\nWORKDIR /app \n\n# Copy built frontend static assets from the builder stage\nCOPY --from=frontend-builder /app/dist/ ./dist/\n\n# Expose ports\n# For frontend served by 'serve'\nEXPOSE 8080 \n# For API\nEXPOSE 6521 \n\n# Copy Nginx configuration\nCOPY nginx.conf /etc/nginx/nginx.conf\n\n# Command to run both services\n# API runs from /app/api directory, frontend served by Nginx\n# CMD [\"sh\", \"-c\", \"cd /app/api && node app.js & nginx -g 'daemon off;'\"]\nCMD sh -c \"\\\n  echo 'client running @ http://127.0.0.1:8080/'; \\\n  cd /app/api && node app.js & nginx -g 'daemon off;'\"\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License."
  },
  {
    "path": "README.md",
    "content": "<br />\n<p align=\"center\">\n    <img src=\"https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png\" alt=\"Logo\" width=\"156\" height=\"156\">\n  <h2 align=\"center\" style=\"font-weight: 600\">MoeKoe Music</h2>\n  <p align=\"center\">\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest\"><img src=\"https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers\"><img src=\"https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases\"><img src=\"https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/iAJue\"><img src=\"https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square\" /></a>\n  </p>\n  <p align=\"center\">\n    一款开源简洁高颜值的酷狗第三方客户端\n    <br />\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/\" target=\"blank\"><strong>🌎 GitHub仓库</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/releases\" target=\"blank\"><strong>📦️ 下载安装包</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://MoeJue.cn\" target=\"blank\"><strong>💬 访问博客</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://Music.MoeKoe.cn\" target=\"blank\"><strong>🏠 项目主页</strong></a>\n  </p>\n  <p align=\"center\">\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/README.md\" target=\"blank\"><strong>🇨🇳 简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md\" target=\"blank\"><strong>🇨🇳 繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md\" target=\"blank\"><strong>🇯🇵 日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md\" target=\"blank\"><strong>🇺🇸 English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md\" target=\"blank\"><strong>🇰🇷 한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md\" target=\"blank\"><strong>🇷🇺 Русский</strong></a>\n    <br />\n    <br />\n  </p>\n</p>\n\n![images](https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png)\n\n## ❤️ 前言\n\n早在10年前后的样子,那会在用网页版QQ的时候我就已经开始使用酷狗音乐了(也是十来年的老粉了),所以这些年收藏的歌曲全部都在上面.后来我也尝试开始使用网易云或QQ音乐,也尝试把酷狗的歌单导入进去,但是效果都不尽人意.我听的大多是日漫OP,好多歌曲都没办法找到.\n\n兜兜转转最后还是回到酷狗,但是在Mac端的酷狗,时常可能会出现不能播放的情况,虽说界面没什么功能,但也挺好的.在网友的安利下,我现在一直是在酷狗的[概念版](https://t1.kugou.com/d2tBza3CSV2)上听歌,并且是市面上为数不多能免费听VIP歌曲的音乐播放软件了,力推.\n\n我在我的个人介绍页面说我特别喜欢听歌,尤其是日漫OP.怎么证明呢?(之前我网页版歌单也年久失修了)那就自己开发一个音乐播放器.\n\n\n## ✨ 特性\n\n- ✅ 使用 Vue.js 全家桶开发\n- 🔴 酷狗账号登录（扫码/手机/账号登录）\n- 📃 支持歌词显示\n- 📻 每日推荐歌曲\n- 🚫🤝 无任何社交功能\n- 🔗 官方服务器直连, 无任何第三方 API\n- ✔️ 每日自动领取VIP, 登录就是VIP\n- 🎨 主题色切换 \n- 👋 启动问候语\n- ⚙️ 多平台支持\n- 🛠 更多特性开发中\n\n## 📢 Todo List\n- [x] 📺 支持 MV 播放\n- [x] 🌚 Light/Dark Mode 自动切换\n- [x] 👆 支持 Touch Bar\n- [x] 🖥️ 支持 PWA，可在 Chrome/Edge 里点击地址栏右边的 ➕ 安装到电脑\n- [ ] 🎧 支持 Mpris\n- [x] ⌨️ 全局快捷键\n- [x] 🤟 多语言支持\n- [x] 📻 桌面歌词\n- [x] ⚙️ 系统架构优化\n- [x] 🎶 歌曲、歌单/收藏、取消\n\n更新日志请查看 [Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)\n\n## 📦️ 安装\n\n### 1. 客户端安装\n\n访问本项目的 [Releases](https://github.com/iAJue/MoeKoeMusic/releases) 页面下载安装包。\n\n### 2. WEB端安装（docker）\n\n* 注意：部署后请开放服务器对应端口才可使用，或者使用反向代理实现域名访问。\n\n    1. 方式一：快速启动（推荐）\n\n    ```\n    git clone https://github.com/iAJue/MoeKoeMusic.git\n    cd MoeKoeMusic\n    git submodule update --init --recursive\n    docker compose up -d &\n    ```\n\n    2. ~~方式二：使用docker-compose一键安装 （镜像暂未上传官方）~~\n    \n    ```\n    docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest\n    ```\n\n    3. 方式三：宝塔容器编排\n\n    * 远程镜像，版本可能会落后于官方\n    \n    ```\n    version: '3.3'\n    \n    services:\n      moekoe-music:\n        # 镜像地址\n        image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest\n        container_name: moekoe-music # 容器名\n        restart: unless-stopped # 自动重启\n        build:\n          context: .\n          dockerfile: Dockerfile\n        environment:\n          - PORT=6521\n          - platform=lite\n        ports: # 端口映射\n          - \"8080:8080\"  # 前端服务\n          - \"6521:6521\"  # 接口服务\n    \n    ```\n    \n    复制内容上面的内容，粘贴到宝塔面板的容器编排里面，编排名称为MoeKoeMusic，点击部署即可。\n\n### 3. 一键部署\n[![使用 EdgeOne Pages 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)\n\n需在环境变量(VITE_APP_API_URL)中填写自己的API地址\n\n## ⚙️ 开发\n\n1. 克隆本仓库\n\n```sh\ngit clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git\n```\n\n2. 进入目录并安装依赖\n\n```sh\ncd MoeKoeMusic\nnpm run install-all\n```\n3. 启动开发者模式\n```sh\nnpm run dev\n```\n4. 打包项目\n```sh\nnpm run build\n```\n5. 编译项目\n  - Windows: \n  ```sh\n  npm run electron:build:win [默认 NSIS 安装包]\n  ```\n  -\tLinux: \n  ```sh\n  npm run electron:build:linux [默认 AppImage 格式]\n  ```\n  -\tmacOS: \n  ```sh\n  npm run electron:build:macos [默认 macOS 双架构]\n  ```\n\n\n更多命令请查看 `package.json` 文件 `scripts` \n\n## 👷‍♂️ 编译客户端\n\n如果在 Release 页面没有找到适合你的设备的安装包的话，你可以根据下面的步骤来打包自己的客户端。\n\n1. 安装 [Node.js](https://nodejs.org/en/)，并确保 `Node.js` 版本 >= 18.0.0。\n\n2. 使用 `git clone https://github.com/iAJue/MoeKoeMusic.git` 克隆本仓库到本地。\n\n3. 使用 `npm install` 安装项目依赖。\n4. 编译API服务端\n    - Windows:\n        ```sh\n        npm run build:api:win\n        ```\n    - Linux:\n        ```sh\n        npm run build:api:linux\n        ```\n    - macOS:\n      ```sh\n      npm run build:api:macos\n      ```\n\n5. 选择下列的命令来打包适合的你的安装包，打包出来的文件在 `/dist_electron` 目录下。了解更多信息可访问 [electron-builder 文档](https://www.electron.build/cli)\n\n\n#### 1. 打包 macOS 平台\n   - 通用的 macOS 包（Intel 和 Apple Silicon 双架构）：\n   ```\n   npm run electron:build -- --mac --universal\n   ```\n   - 仅 Intel 架构：\n   ```\n   npm run electron:build -- --mac --x64\n   ```\n   - 仅 Apple Silicon 架构：\n   ```\n   npm run electron:build -- --mac --arm64\n   ```\n\n\n#### 2. 打包 Windows 平台\n\n   - 默认 NSIS 安装包（适合大多数 Windows 用户）：\n   ```\n   npm run electron:build -- --win\n   ```\n   - 为 Windows 创建 EXE 文件和 Squirrel 安装包：\n   ```\n   npm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel\n   ```\n       - --ia32 为 32 位 Windows 架构。\n       - --x64 为 64 位 Windows 架构。\n       - --arm64 为 ARM Windows 架构（Surface 等设备）。\n\n   - 为 Windows 生成便携式的 EXE 文件（免安装）：\n   ```\n   npm run electron:build -- --win --portable\n   ```\n#### 3. 打包 Linux 平台\n   - 默认 AppImage 格式（适用于大多数 Linux 发行版）：\n\n   ```\n   npm run electron:build -- --linux\n   ```\n   - snap（适用于 Ubuntu 和支持 snap 的发行版）：\n   ```\n   npm run electron:build -- --linux --target snap\n   ```\n   - \tdeb（适用于 Debian/Ubuntu 系列）：\n   ```\n   npm run electron:build -- --linux --target deb\n   ```\n   - rpm（适用于 Red Hat/Fedora 系列）：\n   ```\n   npm run electron:build -- --linux --target rpm\n   ```\n   - ARM64架构(ARM v8+): \n   ```\n   npm run build:api:linux-arm64 //编译API\n   npm run electron:build:linux-arm64 //编译主程序\n   ```\n\n#### 4. 打包所有平台\n\n  如果需要同时生成 Windows、macOS 和 Linux 的安装包，可以使用以下命令：\n  ```\n  npm run electron:build -- -mwl\n  ```\n\n#### 5. 自定义编译设置\n\n您可以根据需要添加其他选项来进一步自定义打包，例如指定 x64 和 arm64 架构，或选择不同的目标格式。\n\n## ⭐ 支持项目\n\n如果您觉得这个项目对您有帮助，欢迎给我们一个 Star！您的支持是我们持续改进的动力。\n\n[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)\n\n## ✅ 反馈\n\n如有任何问题或建议，欢迎提交 issue 或 pull request。\n\n## ⚠️ 免责声明\n0. 本程序是酷狗第三方客户端，并非酷狗官方，需要更完善的功能请下载官方客户端体验.\n1. 本项目仅供学习使用，请尊重版权，请勿利用此项目从事商业行为及非法用途！\n2. 使用本项目的过程中可能会产生版权数据。对于这些版权数据，本项目不拥有它们的所有权。为了避免侵权，使用者务必在 24 小时内清除使用本项目的过程中所产生的版权数据。\n3. 由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害（包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿，或任何及所有其他商业损害或损失）由使用者负责。        \n1. 禁止在违反当地法律法规的情况下使用本项目。对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担，本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。    \n2. 音乐平台不易，请尊重版权，支持正版。\n3. 本项目仅用于对技术可行性的探索及研究，不接受任何商业（包括但不限于广告等）合作及捐赠。\n4. 如果官方音乐平台觉得本项目不妥，可联系本项目更改或移除。\n            \n\n## 📜 开源许可\n\n本项目仅供个人学习研究使用，禁止用于商业及非法用途。\n\n基于 [GNU General Public License v2.0 (GPL-2.0)](https://github.com/iAJue/MoeKoeMusic/blob/main/LICENSE) 许可进行开源。\n\n## 👍 灵感来源\n\nAPI 源代码来自 [MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi) \n\n- [Apple Music](https://music.apple.com)\n- [YouTube Music](https://music.youtube.com)\n- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)\n- [酷狗音乐](https://kugou.com/)\n\n## 🖼️ 截图\n\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)\n\n\n## 🗓️ Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)\n"
  },
  {
    "path": "build/installer.nsh",
    "content": "!include \"MUI.nsh\"\n!define MUI_FINISHPAGE_LINK_LOCATION \"https://MoeJue.cn\"\n!define MUI_FINISHPAGE_LINK \"访问作者(阿珏酱)主页\"\n!define MUI_FINISHPAGE_SHOWREADME_TEXT \"访问 GitHub 项目主页\"\n!define MUI_FINISHPAGE_SHOWREADME \"https://github.com/iAJue/MoeKoeMusic\"\n!insertmacro MUI_PAGE_WELCOME\n\n; Register the moekoe:// protocol\n!macro customInstall\n  DeleteRegKey HKCR \"moekoe\"\n  WriteRegStr HKCR \"moekoe\" \"\" \"URL:MoeKoe Music Protocol\"\n  WriteRegStr HKCR \"moekoe\" \"URL Protocol\" \"\"\n  WriteRegStr HKCR \"moekoe\\DefaultIcon\" \"\" \"$INSTDIR\\${PRODUCT_NAME}.exe,0\"\n  WriteRegStr HKCR \"moekoe\\shell\" \"\" \"\"\n  WriteRegStr HKCR \"moekoe\\shell\\open\" \"\" \"\"\n  WriteRegStr HKCR \"moekoe\\shell\\open\\command\" \"\" '\"$INSTDIR\\${PRODUCT_NAME}.exe\" \"%1\"'\n!macroend"
  },
  {
    "path": "build/license.txt",
    "content": "MoeKoe Music Software License Agreement\n\n1. Terms of Use\nThis software is released under the GPL-2.0 license. You are free to use, modify, and distribute this software, but must comply with all terms of the GPL-2.0 license. This means any modified versions must also be open-sourced under the same license.\n\n2. Disclaimer\nThis software is provided \"as is\" without any express or implied warranties.\n\n3. Copyright Notice\nCopyright 2024 MoeKoe. All rights reserved.\n\n4. Usage Restrictions\n- Prohibited for any illegal purposes\n- Prohibited for commercial use\n\n5. Privacy Policy\nThis software collects necessary usage data to improve service quality.\n\n6. Termination\nAny violation of the terms in this agreement will result in automatic termination of the license.\n\nBy installing or using this software, you agree to accept all terms of this agreement."
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3.3'\n\nservices:\n  moekoe-music:\n    container_name: moekoe-music # 容器名\n    restart: unless-stopped # 自动重启\n    build:\n      context: .\n      dockerfile: Dockerfile\n\n    environment:\n      - PORT=6521\n      - platform=lite\n    ports: # 端口映射\n      - \"8080:8080\"  # 前端服务\n      - \"6521:6521\"  # 接口服务\n"
  },
  {
    "path": "docs/README_en.md",
    "content": "> **Note**: This English document may not be updated in a timely manner. For the latest content, please refer to the [Simplified Chinese version](https://github.com/iAJue/MoeKoeMusic/README.md).\n<br />\n<p align=\"center\">\n<img src=\"https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png \" alt=\"Logo\" width=\"156\" height=\"156\">\n<h2 align=\"center\" style=\"font-weight: 600\">MoeKoe Music</h2>\n  <p align=\"center\">\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest\"><img src=\"https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers\"><img src=\"https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases\"><img src=\"https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/iAJue\"><img src=\"https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square\" /></a>\n  </p>\n<p align=\"center\">\nAn open-source, concise, and aesthetically pleasing third-party client for KuGou\n<br />\n<a href=\"https://github.com/iAJue/MoeKoeMusic/\" target=\"blank\"><strong>🌎 GitHub Repository</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://github.com/iAJue/MoeKoeMusic/releases\" target=\"blank\"><strong>📦️ Download Packages</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://MoeJue.cn\" target=\"blank\"><strong>💬 Visit Blog</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://Music.MoeKoe.cn\" target=\"blank\"><strong>🏠 Project Homepage</strong></a>\n\n</p>\n<p align=\"center\">\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/README.md\" target=\"blank\"><strong>🇨🇳 简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md\" target=\"blank\"><strong>🇨🇳 繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md\" target=\"blank\"><strong>🇯🇵 日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md\" target=\"blank\"><strong>🇺🇸 English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md\" target=\"blank\"><strong>🇰🇷 한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md\" target=\"blank\"><strong>🇷🇺 Русский</strong></a>\n    <br />\n    <br />\n  </p>\n</p>\n\n![images]( https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png )\n\n## ❤️ Preface\n\nAs early as around 10 years ago, when I was using the web version of QQ, I had already started using Kugou Music (which I have been a fan of for over ten years), so all the songs I collected over the years were on it Later on, I also tried using NetEase Cloud or QQ Music, and tried importing Kugou's playlists into it, but the results were not satisfactory I mostly listen to Japanese anime OP, and I can't find many songs\n\nAfter wandering around, I finally returned to Kugou. However, on the Mac version of Kugou, there may often be situations where it cannot be played. Although the interface does not have many functions, it is still quite good With the support of netizens, I have been working on [Kugou's concept version](https://t1.kugou.com/d2tBza3CSV2)Listen to music online, and it is one of the few music playback software on the market that allows you to listen to VIP songs for free. It is highly recommended\n\nI said on my personal introduction page that I particularly enjoy listening to music, especially Japanese anime OP How can we prove it? (My web version of the playlist was also in disrepair for a long time before) So I'll develop my own music player\n\n\n##  ✨  characteristic\n\n-  ✅  Developing with Vue.js Family Bucket\n-  🔴  KuGou account login (scan code/phone/account login)\n-  📃  Support lyric display\n-  📻  Daily recommended songs\n-  🚫🤝  No social function\n-  🔗  Official server direct connection, without any third-party APIs\n-  ✔️  Automatically claim VIP every day, log in to become VIP\n-  🎨  Theme color switching\n-  👋  Initiate greetings\n-  ⚙️  Multi platform support\n-  🛠  More features under development\n\n## 📢 Todo List\n- [x]  📺  Support MV playback\n- [x]  🌚 Light/Dark Mode  Automatic switching\n- [x]  👆  Support Touch Bar\n- [x]  🖥️  Support PWA, you can click on the right side of the address bar in Chrome/Edge ➕  Install to computer\n- [ ]  🎧  Support Mpris\n- [x]  ⌨️   Global shortcut keys\n- [x]  🤟  Multi language support\n- [x]  📻  Desktop Lyrics\n- [x]  ⚙️  System architecture optimization\n- [x]  🎶  Songs, playlists/favorites, cancellation\n\nPlease check the  for the update log [Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)\n\n## 📦️ Installation\n\n### 1. Client Installation\n\nVisit the [Releases](https://github.com/iAJue/MoeKoeMusic/releases) page of this project to download the installation package.\n\n### 2. Web Installation (Docker)\n\n* Note: Please open the corresponding port on the server after deployment, or use a reverse proxy for domain access.\n\n  1. Method 1: Quick Start (Recommended)\n\n  ```\n  git clone https://github.com/iAJue/MoeKoeMusic.git\n  cd MoeKoeMusic\n  git submodule update --init --recursive\n  docker compose up -d &\n  ```\n\n  2. ~~Method 2: One-click installation using docker-compose (image not yet uploaded officially)~~\n  \n  ```\n  docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest\n  ```\n\n  3. Method 3: Baota Container Orchestration\n\n  * Remote image, version may be behind the official\n  \n  ```\n  version: '3.3'\n  \n  services:\n    moekoe-music:\n    # Image address\n    image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest\n    container_name: moekoe-music # Container name\n    restart: unless-stopped # Auto restart\n    build:\n      context: .\n      dockerfile: Dockerfile\n    environment:\n      - PORT=6521\n      - platform=lite\n    ports: # Port mapping\n      - \"8080:8080\"  # Frontend service\n      - \"6521:6521\"  # API service\n  \n  ```\n  \n  Copy the content above and paste it into the container orchestration in the Baota panel, name the orchestration as MoeKoeMusic, and click deploy.\n### 3. One-Click Deployment\n[![使用 EdgeOne Pages 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)\n\nYou need to fill in your own API address in the environment variable VITE_APP_API_URL.\n\n##  ⚙️  development\n\n1. Clone this repository\n\n```sh\ngit clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git\n```\n\n2. Enter the directory and install dependencies\n\n```sh\ncd MoeKoeMusic\nnpm run install-all\n```\n3. Launch developer mode\n```sh\nnpm run dev\n```\n4. Package project\n```sh\nnpm run build\n```\n5. Compile the project\n- Windows: \n```sh\nNpm run electron: build: win [default NSIS installation package]\n```\n-\tLinux: \n```sh\nNpm run electron: build: Linux [default AppImage format]\n```\n-\tmacOS: \n```sh\nNpm run electron: build: macos [default universal architecture]\n```\n\n\nFor more commands, please refer to the ` package.json ` file ` scripts `\n\n##  👷‍♂️  Compile client\n\nIf you cannot find the installation package suitable for your device on the Release page, you can follow the steps below to package your own client.\n\n1. Install [Node.js](https://nodejs.org/en/)And ensure that the 'Node. js' version is>=18.0.0.\n\n2. Use ` git clone https://github.com/iAJue/MoeKoeMusic.git `Clone this repository locally.\n\n3. Use 'npm install' to install project dependencies.\n4. Compile API server\n- Windows:\n```sh\nnpm run build:api:win\n```\n- Linux:\n```sh\nnpm run build:api:linux\n```\n- macOS:\n```sh\nnpm run build:api:macos\n```\n\n5. Choose the following command to package the appropriate installation package for you, and the packaged file should be located in the '/dits_electron' directory. For more information, please visit the [Electron Builder documentation](https://www.electron.build/cli )\n\n\n#### 1.  Package macOS platform\n- Universal macOS package (Intel and Apple Silicon dual architecture):\n```\nnpm run electron:build -- --mac --universal\n```\n- Only Intel architecture:\n```\nnpm run electron:build -- --mac --x64\n```\n- Only Apple Silicon architecture:\n```\nnpm run electron:build -- --mac --arm64\n```\n\n\n#### 2.  Package Windows Platform\n\n- Default NSIS installation package (suitable for most Windows users):\n```\nnpm run electron:build -- --win\n```\n- Create EXE files and Squirrel installation packages for Windows:\n```\nnpm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel\n```\n  - Ia32 is a 32-bit Windows architecture.\n  - X64 is a 64 bit Windows architecture.\n  - Arm64 is based on ARM Windows architecture (for devices such as Surface).\n\n- Generate portable EXE files for Windows (installation free):\n```\nnpm run electron:build -- --win --portable\n```\n#### 3.  Packaging Linux Platform\n- Default AppImage format (applicable to most Linux distributions):\n```\nnpm run electron:build -- --linux\n```\n- Snap (for Ubuntu and Snap supported distributions):\n```\nnpm run electron:build -- --linux --target snap\n```\n- Deb (applicable to the Debian/Ubuntu series):\n```\nnpm run electron:build -- --linux --target deb\n```\n- RPM (applicable to Red Hat/Fedora series):\n```\nnpm run electron:build -- --linux --target rpm\n```\n\n#### 4.  Package all platforms\n\nIf you need to generate installation packages for Windows, macOS, and Linux simultaneously, you can use the following command:\n```\nnpm run electron:build -- -mwl\n```\n\n#### 5.  Custom compilation settings\n\nYou can add other options as needed to further customize the packaging, such as specifying x64 and arm64 architectures, or selecting different target formats.\n\n## ⭐ Support This Project\n\nIf you find this project helpful, please consider giving us a star! Your support motivates us to keep improving.\n\n[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)\n\n##  ☑️  feedback\n\nIf you have any questions or suggestions, please feel free to submit an issue or pull request.\n\n## ⚠️ Disclaimers\n0. This program is a third-party client of KuGou, not an official KuGou client. If you need more complete functions, please download the official client to experience it\n1. This project is for learning purposes only. Please respect copyright and do not use this project for commercial activities or illegal purposes!\n2. Copyright data may be generated during the use of this project. For these copyrighted data, this project does not own them. To avoid infringement, users must clear any copyright data generated during the use of this project within 24 hours.\n3. The user shall be responsible for any direct, indirect, special, incidental, or consequential damages of any nature arising from the use of this project, including but not limited to damages caused by loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses.\n            \n1. It is prohibited to use this project in violation of local laws and regulations. Any illegal or irregular behavior caused by the use of this project by users who are aware or unaware that local laws and regulations do not allow it shall be borne by the users, and this project shall not be held responsible for any direct, indirect, special, incidental or consequential liability arising therefrom.\n            \n2. Music platforms are not easy, please respect copyright and support genuine versions.\n3. This project is only for the exploration and research of technical feasibility, and does not accept any commercial (including but not limited to advertising, etc.) cooperation or donations.\nIf the official music platform finds this project inappropriate, they can contact this project to make changes or remove it.\n            \n\n##  📜  Open source license\n\nThis project is for personal learning and research purposes only, and is prohibited from being used for commercial or illegal purposes.\n\nBased on [MIT license](https://opensource.org/licenses/MIT)License to open source.\n\n## 👍 Inspiration source\n\nThe API source code comes from [MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi ) \n\n- [Apple Music]( https://music.apple.com )\n- [YouTube Music]( https://music.youtube.com )\n- [YesPlayMusic]( https://github.com/qier222/YesPlayMusic )\n- [Cool Dog Music](https://kugou.com/ )\n\n##  🖼️  screenshot\n\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)\n\n## 🗓️ Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)\n"
  },
  {
    "path": "docs/README_ja.md",
    "content": "> **注意**: この日本語ドキュメントはタイムリーに更新されない場合があります。最新の内容については[簡体字中国語版](https://github.com/iAJue/MoeKoeMusic/README.md)をご参照ください。\n<br />\n<p align=\"center\">\n<img src=\"https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png\" alt=\"Logo\" width=\"156\" height=\"156\">\n<h2 align=\"center\" style=\"font-weight: 600\">MoeKoe Music</h2>\n  <p align=\"center\">\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest\"><img src=\"https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers\"><img src=\"https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases\"><img src=\"https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/iAJue\"><img src=\"https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square\" /></a>\n  </p>\n<p align=\"center\">\nオープンソースで簡潔で高ルックスのクールな犬のサードパーティクライアント\n<br />\n<a href=\"https://github.com/iAJue/MoeKoeMusic/\" target=\"blank\"><strong>🌎 GitHub倉庫</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://github.com/iAJue/MoeKoeMusic/releases\" target=\"blank\"><strong>📦️インストールパッケージのダウンロード</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://MoeJue.cn\" target=\"blank\"><strong>💬 ブログへのアクセス</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://Music.MoeKoe.cn\" target=\"blank\"><strong>🏠 プロジェクトホームページ</strong></a>\n</p>\n<p align=\"center\">\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/README.md\" target=\"blank\"><strong>🇨🇳 简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md\" target=\"blank\"><strong>🇨🇳 繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md\" target=\"blank\"><strong>🇯🇵 日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md\" target=\"blank\"><strong>🇺🇸 English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md\" target=\"blank\"><strong>🇰🇷 한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md\" target=\"blank\"><strong>🇷🇺 Русский</strong></a>\n    <br />\n    <br />\n</p>\n</p>\n\n![images](https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png)\n\n### ❤️ はじめに\n\n10年ほど前の様子では、Web版QQを使っている間に私はすでにクールな犬音楽を使い始めていたので(10年以上の古い粉でもある)、これらの年に所蔵されている曲はすべて上にあります。その後、私も網易雲やQQ音楽を使ってみたり、クールな犬の歌を導入してみたりしましたが、効果はあまりありませんでした。私が聴いているのはほとんど日漫OPで、多くの曲は見つけることができませんでした。\n\nぐるぐる回って結局クールドッグに戻るのですが、Mac側のクールドッグでは、時々再生できないことがあります。インタフェースはあまり機能していないとはいえ、いいですね。ネットユーザーのアンリの下で、私は今までクールな犬の[コンセプト版](https://t1.kugou.com/d2tBza3CSV2)で歌を聴いて、しかもVIP曲を無料で聴ける音楽再生ソフトは市販されていないので、力を入れてください。\n\n私は私の個人紹介ページで、私は特に歌を聴くのが好きだと言っています。特に日漫OP.どうやって証明しますか。(以前は私のウェブ版の歌単も長年修理を怠っていました)では、自分で音楽プレーヤーを開発します。\n\n\n## ✨ プロパティ\n\n- ✅ Vue.jsファミリーバケツを用いた開発\n- 🔴 クールドッグアカウント登録(スキャン/携帯/アカウント登録)\n- 📃 歌詞表示のサポート\n- 📻 毎日のおすすめ曲\n- 🚫🤝 ソーシャル機能なし\n- 🔗 サードパーティ製APIなしの公式サーバ直結\n- ✔️ VIPは毎日自動で受け取り、ログインするとVIPになります\n- 🎨 テーマカラー切り替え\n- 👋 開始の挨拶\n- ⚙️ マルチプラットフォームサポート\n- 🛠 その他の機能開発中\n\n## 📢 Todo List\n- [x] 📺 MV再生をサポート\n- [x] 🌚 Light/Dark Mode 自動切り替え\n- [x] 👆 Touch Bar対応\n- [x] 🖥️ PWA対応、Chrome/Edgeでアドレスバー右の➕ コンピュータにインストール\n- [ ] 🎧 Mprisのサポート\n- [x] ⌨️ ショートカットとグローバルショートカットのカスタマイズ\n- [x] 🤟 多言語サポート\n- [x] 📻 デスクトップ歌詞\n- [x] ⚙️ システムアーキテクチャの最適化\n- [x] 🎶 曲、歌/コレクション、キャンセル\n\n更新ログは[Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)\n\n## 📦️ インストール\n\n### 1. クライアントのインストール\n\n本プロジェクトの [Releases](https://github.com/iAJue/MoeKoeMusic/releases) ページにアクセスして、インストールパッケージをダウンロードしてください。\n\n### 2. WEB版のインストール（docker）\n\n* 注意：デプロイ後は、サーバーの対応ポートを開放する必要があります。または、リバースプロキシを使用してドメインアクセスを実現してください。\n\n    1. 方法一：クイックスタート（推奨）\n\n    ```\n    git clone https://github.com/iAJue/MoeKoeMusic.git\n    cd MoeKoeMusic\n    git submodule update --init --recursive\n    docker compose up -d &\n    ```\n\n    2. ~~方法二：docker-composeを使用したワンクリックインストール（イメージはまだ公式にアップロードされていません）~~\n    \n    ```\n    docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest\n    ```\n\n    3. 方法三：宝塔コンテナ編成\n\n    * リモートイメージ、バージョンは公式より遅れる可能性があります。\n    \n    ```\n    version: '3.3'\n    \n    services:\n      moekoe-music:\n        # イメージアドレス\n        image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest\n        container_name: moekoe-music # コンテナ名\n        restart: unless-stopped # 自動再起動\n        build:\n          context: .\n          dockerfile: Dockerfile\n        environment:\n          - PORT=6521\n          - platform=lite\n        ports: # ポートマッピング\n          - \"8080:8080\"  # フロントエンドサービス\n          - \"6521:6521\"  # APIサービス\n    \n    ```\n    \n    上記の内容をコピーして、宝塔パネルのコンテナ編成に貼り付け、編成名をMoeKoeMusicとして、デプロイをクリックしてください。\n### 3. ワンクリックデプロイ\n[![EdgeOne Pagesを使用してデプロイ](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)\n\n環境変数（VITE_APP_API_URL）に自分のAPIアドレスを入力する必要があります。\n\n## ⚙️ かいはつ\n\n1.本倉庫のクローニング\n\n```sh\ngit clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git\n```\n\n2.ディレクトリにアクセスして依存関係をインストールする\n\n```sh\ncd MoeKoeMusic\nnpm run install-all\n```\n3.開発者モデルの起動\n```sh\nnpm run dev\n```\n4.パッケージ項目\n```sh\nnpm run build\n```\n5.プロジェクトのコンパイル\n- Windows: \n```sh\nnpm run electron：build：win[デフォルトNSISインストールパッケージ]\n```\n-\tLinux: \n```sh\nnpm run electron：build：linux[デフォルトAppImageフォーマット]\n```\n-\tmacOS: \n```sh\nnpm run electron：build：macos[デフォルトの双架構]\n```\n\n\n詳細なコマンドは、「package.json」ファイル「scripts」を参照してください。\n\n## 👷‍♂️ クライアントのコンパイル\n\nReleaseページであなたに適したデバイスのインストールパッケージが見つからない場合は、次の手順に従って自分のクライアントをパッケージ化することができます。\n\n1. [ノード.js](https://nodejs.org/en/)を選択し、` Node.js `バージョン>=18.0.0であることを確認します。\n\n2. を使用する `git clonehttps://github.com/iAJue/MoeKoeMusic.git`この倉庫をローカルにクローニングします。\n\n3. `npm install `を使用してプロジェクト依存性をインストールします。\n4. APIサービス端末のコンパイル\n- Windows:\n```sh\nnpm run build:api:win\n```\n- Linux:\n```sh\nnpm run build:api:linux\n```\n- macOS:\n```sh\nnpm run build:api:macos\n```\n\n5. 次のコマンドを選択して適切なインストールパッケージをパッケージ化し、パッケージ化されたファイルは`/dist _ electron `ディレクトリの下にあります。詳細については、[electron-builderドキュメント](https://www.electron.build/cli)\n\n\n#### 1. パッケージmacOSプラットフォーム\n- 汎用のmacOSパッケージ(IntelとApple Siliconデュアルアーキテクチャ)：\n```\nnpm run electron:build -- --mac --universal\n```\n- Intelアーキテクチャのみ：\n```\nnpm run electron:build -- --mac --x64\n```\n- Apple Siliconアーキテクチャのみ：\n```\nnpm run electron:build -- --mac --arm64\n```\n\n\n#### 2. Windowsプラットフォームのパッケージ化\n\n- デフォルトNSISインストールパッケージ(ほとんどのWindowsユーザー向け)：\n```\nnpm run electron:build -- --win\n```\n- Windows用のEXEファイルとSquirrelインストールパッケージを作成するには：\n```\nnpm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel\n```\n---ia 32は32ビットWindowsアーキテクチャです。\n---x 64は64ビットWindowsアーキテクチャです。\n---arm 64はARM Windowsアーキテクチャ(Surfaceなどのデバイス)です。\n\n- Windows用にポータブルEXEファイルを生成する(インストール不要)：\n```\nnpm run electron:build -- --win --portable\n```\n#### 3. Linuxプラットフォームのパッケージ化\n- デフォルトのAppImageフォーマット(ほとんどのLinuxリリース用)：\n```\nnpm run electron:build -- --linux\n```\n- snap(Ubuntuおよびsnapをサポートするリリース用)：\n```\nnpm run electron:build -- --linux --target snap\n```\n- deb(Debian/Ubuntuシリーズ用)：\n```\nnpm run electron:build -- --linux --target deb\n```\n- rpm(Red Hat/Fedoraシリーズ用)：\n```\nnpm run electron:build -- --linux --target rpm\n```\n\n#### 4. すべてのプラットフォームをパッケージ化\n\nWindows、macOS、Linuxのインストールパッケージを同時に生成する必要がある場合は、次のコマンドを使用します。\n```\nnpm run electron:build -- -mwl\n```\n\n#### 5. コンパイル設定のカスタマイズ\n\n必要に応じて他のオプションを追加して、パッケージをさらにカスタマイズすることができます。たとえば、x 64とarm 64スキーマを指定したり、異なるターゲットフォーマットを選択したりすることができます。\n\n\n## ⭐ プロジェクトをサポート\n\nこのプロジェクトがお役に立った場合は、ぜひ星を付けてください！あなたのサポートが私たちの継続的な改善の原動力です。\n\n[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)\n\n\n## ☑️ フィードバック\n\n何か質問やアドバイスがあれば、issueまたはpull requestを提出してください。\n\n### ⚠️ 免責事項\n0. 本プログラムはクールドッグの第三者クライアントであり、クールドッグの公式ではなく、より完全な機能が必要な場合は公式クライアント体験をダウンロードしてください。\n1. 本プロジェクトは学習用にのみ使用されます。著作権を尊重し、このプロジェクトを利用して商業行為や不正な用途に従事しないでください。\n2. 本プロジェクトの使用中に著作権データが生成される可能性があります。これらの著作権データに対して、本プロジェクトは所有権を持っていません。権利侵害を回避するために、使用者は、本プロジェクトを使用する過程で生成された著作権データを24時間以内に消去しなければならない。\n3. 本事業の使用により生じた、本契約書の使用または使用不能によるいかなる性質を含む、直接的、間接的、特殊、偶発的または結果的な損害(名誉損失、操業停止、コンピュータ故障または故障による損害賠償、またはその他あらゆる商業的損害または損失を含むがこれらに限定されない)は、使用者が責任を負う。          \n1. 現地の法律法規に違反した場合の本事業の使用を禁止する。使用者が現地の法律法規で許可されていないことを知っているか知らないかのうちに本プロジェクトを使用したことによるいかなる違法行為も使用者が負担し、本プロジェクトはこれによる直接、間接、特殊、偶然、または結果的な責任を負わない。          \n2. 音楽プラットフォームは容易ではありません。著作権を尊重し、正規版をサポートしてください。\n3. 本プロジェクトは技術の実現可能性の探索と研究にのみ使用され、いかなる商業(広告などを含むがこれに限らない)の協力と寄付を受けない。\n4. 公式音楽プラットフォームが本プロジェクトに不具合があると感じた場合は、本プロジェクトに連絡して変更または削除することができます。\n            \n\n## 📜 オープンソースライセンス\n\n本プロジェクトは個人学習研究用にのみ使用され、商業用及び不法用途に使用することは禁止されている。\n\n[MIT license](https://opensource.org/licenses/MIT)オープンソースを許可する。\n\n### 👍 インスピレーションソース\n\nAPIソースコードは[MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi) \n\n- [Apple Music](https://music.apple.com)\n- [YouTube Music](https://music.youtube.com)\n- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)\n-[クールドッグミュージック](https://kugou.com/)\n\n## 🖼️ スクリーンショット\n\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)\n\n\n## 🗓️ スター履歴\n\n[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)\n"
  },
  {
    "path": "docs/README_ko.md",
    "content": "> **주의**: 이 한국어 문서는 업데이트가 지연될 수 있으니, 최신 내용은 [중국어 간체 버전](https://github.com/iAJue/MoeKoeMusic/README.md)을 참고하세요.\n<br />\n<p align=\"center\">\n<img src=\"https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png\" alt=\"Logo\" width=\"156\" height=\"156\">\n<h2 align=\"center\" style=\"font-weight: 600\">MoeKoe Music</h2>\n  <p align=\"center\">\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest\"><img src=\"https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers\"><img src=\"https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases\"><img src=\"https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/iAJue\"><img src=\"https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square\" /></a>\n  </p>\n<p align=\"center\">\n오픈 소스 간결하고 용모가 높은 쿨도그 제3자 클라이언트\n<br />\n<a href=\"https://github.com/iAJue/MoeKoeMusic/\" target=\"blank\"><strong>🌎 GitHub창고</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://github.com/iAJue/MoeKoeMusic/releases\" target=\"blank\"><strong>📦️ 설치 패키지 다운로드 </strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://MoeJue.cn\" target=\"blank\"><strong>💬 블로그 방문 </strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://Music.MoeKoe.cn\" target=\"blank\"><strong>🏠 프로젝트 홈페이지</strong></a>\n</p>\n<p align=\"center\">\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/README.md\" target=\"blank\"><strong>🇨🇳 简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md\" target=\"blank\"><strong>🇨🇳 繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md\" target=\"blank\"><strong>🇯🇵 日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md\" target=\"blank\"><strong>🇺🇸 English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md\" target=\"blank\"><strong>🇰🇷 한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md\" target=\"blank\"><strong>🇷🇺 Русский</strong></a>\n    <br />\n    <br />\n  </p>\n</p>\n\n![images](https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png)\n\n## ❤️ 머리말\n\n일찍이 10년 전후의 모습, 그것은 웹 페이지 QQ를 사용할 때 나는 이미 쿠거우 음악을 사용하기 시작했다 (또한 10여 년의 오랜 팬이다). 그래서 요 몇 년 동안 소장한 노래는 모두 위에 있다.후에 나도 왕이윈이나 QQ음악을 사용하기 시작했고 쿠거우의 노래 리스트를 도입하려고 시도했지만 효과가 모두 만족스럽지 못했다.내가 들은 것은 대부분 일본 만화 OP이다. 많은 노래를 찾을 수 없다.\n\n빙빙 돌다가 결국 쿠거우로 돌아간다. 그러나 Mac에 있는 쿠거우는 종종 재생할 수 없는 상황이 나타날 수 있다. 비록 인터페이스는 아무런 기능이 없지만 아주 좋다.네티즌의 안리하에 나는 지금 줄곧 쿨개의 [개념판](https://t1.kugou.com/d2tBza3CSV2) 에서 노래를 듣고, 또한 시중에서 VIP 노래를 무료로 들을 수 있는 몇 안 되는 음악 재생 소프트웨어입니다.\n\n나는 나의 개인 소개 페이지에서 내가 특히 노래 듣는 것을 좋아한다고 말했다. 특히 일본 만화 OP.어떻게 증명하죠?(이전에 내 웹페이지 플레이리스트도 오랫동안 수리를 하지 않았다.) 그럼 스스로 음악 플레이어를 하나 개발해라.\n\n\n## ✨ 특징\n\n- ✅ Vue.js 패밀리 버킷으로 개발\n- 🔴 쿠거우 계정 로그인 (코드/핸드폰/계정 로그인)\n- 📃 가사 표시 지원\n- 📻 매일 추천곡\n- 🚫🤝 소셜 기능 없음\n- 🔗 공식 서버 직접 연결, 타사 API 없음\n- ✔️ 매일 VIP 자동 수령, 로그인하면 VIP\n- 🎨 테마 색상 전환\n- 👋 시작 인사말\n- ⚙️ 다중 플랫폼 지원\n- 🛠 더 많은 기능 개발 중\n\n## 📢 Todo List\n- [x] 📺 뮤직비디오 재생 지원\n- [x] 🌚 Light/Dark Mode 자동 전환\n- [x] 👆 Touch Bar 지원\n- [x] 🖥️ PWA 지원, Chrome/Edge에서 주소 표시줄 오른쪽에 있는➕ 컴퓨터에 설치\n- [ ] 🎧 지원 Mpris\n- [x] ⌨️ 단축키 및 전역 단축키 사용자 정의\n- [x] 🤟 다국어 지원\n- [x] 📻 데스크톱 가사\n- [x] ⚙️ 시스템 아키텍처 최적화\n- [x] 🎶 노래, 트랙 리스트 / 모음, 취소\n\n로그를 업데이트하려면 [Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)\n\n## 📦️ 설치\n\n### 1. 클라이언트 설치\n\n본 프로젝트의 [Releases](https://github.com/iAJue/MoeKoeMusic/releases) 페이지를 방문하여 설치 패키지를 다운로드하세요.\n\n### 2. WEB 설치 (docker)\n\n* 주의: 배포 후 서버의 해당 포트를 개방해야 사용할 수 있습니다. 또는 역방향 프록시를 사용하여 도메인 접근을 구현할 수 있습니다.\n\n  1. 방법 1: 빠른 시작 (추천)\n\n  ```\n  git clone https://github.com/iAJue/MoeKoeMusic.git\n  cd MoeKoeMusic\n  git submodule update --init --recursive\n  docker compose up -d &\n  ```\n\n  2. ~~방법 2: docker-compose를 사용한 원클릭 설치 (이미지 미업로드 중)~~\n  \n  ```\n  docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest\n  ```\n\n  3. 방법 3: 바오타 컨테이너 오케스트레이션\n\n  * 원격 이미지, 버전이 공식보다 뒤떨어질 수 있음\n  \n  ```\n  version: '3.3'\n  \n  services:\n    moekoe-music:\n    # 이미지 주소\n    image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest\n    container_name: moekoe-music # 컨테이너명\n    restart: unless-stopped # 자동 재시작\n    build:\n      context: .\n      dockerfile: Dockerfile\n    environment:\n      - PORT=6521\n      - platform=lite\n    ports: # 포트 매핑\n      - \"8080:8080\"  # 프론트엔드 서비스\n      - \"6521:6521\"  # API 서비스\n  \n  ```\n  \n  위의 내용을 복사하여 바오타 패널의 컨테이너 오케스트레이션에 붙여넣고, 오케스트레이션 이름을 MoeKoeMusic으로 설정한 후 배포를 클릭하면 됩니다.\n### 3. 원클릭 배포\n[![使用 EdgeOne Pages 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)\n\n환경 변수 VITE_APP_API_URL에 본인의 API 주소를 입력해야 합니다.\n\n## ⚙️ 개발\n\n1. 본 창고 클론\n\n```sh\ngit clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git\n```\n\n2. 디렉터리에 들어가서 종속성 설치\n\n```sh\ncd MoeKoeMusic\nnpm run install-all\n```\n3. 개발자 모드 시작\n```sh\nnpm run dev\n```\n4. 프로젝트 패키지\n```sh\nnpm run build\n```\n5. 항목 컴파일\n- Windows: \n```sh\nnpm run electron:build:win [기본 NSIS 설치 패키지]\n```\n-\tLinux: \n```sh\nnpm run electron:build:linux [기본 AppImage 형식]\n```\n-\tmacOS: \n```sh\nnpm run electron:build:macos [기본 듀얼 아키텍처]\n```\n\n\n더 많은 명령은 `package.json` 파일 `scripts`를 참조하십시오.\n\n## 👷‍♂️ 클라이언트 컴파일\n\nRelease 페이지에서 장치에 맞는 설치 패키지를 찾지 못하면 다음 단계에 따라 클라이언트를 포장할 수 있습니다.\n\n1. 설치[Node.js](https://nodejs.org/en/) 및 `Node.js` 버전이 > = 18.0.0인지 확인합니다.\n\n2. `git clone 사용https://github.com/iAJue/MoeKoeMusic.git'본 창고를 로컬로 복제합니다.\n\n3. `npm install`을 사용하여 프로젝트 종속성을 설치합니다.\n4. API 서버 컴파일\n- Windows:\n```sh\nnpm run build:api:win\n```\n- Linux:\n```sh\nnpm run build:api:linux\n```\n- macOS:\n```sh\nnpm run build:api:macos\n```\n\n1. 다음 명령을 선택하여 적합한 설치 패키지를 포장합니다. 포장된 파일은'/dist_electron'디렉터리에 있습니다.자세한 내용은 [electron-builder 문서](https://www.electron.build/cli)\n\n\n#### 1. macOS 플랫폼 패키지\n- 범용 macOS 패키지(Intel 및 Apple Silicon 듀얼 아키텍처):\n```\nnpm run electron:build -- --mac --universal\n```\n- Intel 아키텍처만:\n```\nnpm run electron:build -- --mac --x64\n```\n- Apple Silicon 아키텍처만:\n```\nnpm run electron:build -- --mac --arm64\n```\n\n\n#### 2. Windows 플랫폼 패키지\n\n- 기본 NSIS 설치 패키지(대부분의 Windows 사용자용):\n```\nnpm run electron:build -- --win\n```\n- Windows용 EXE 파일 및 Squirrel 설치 패키지를 만듭니다.\n```\nnpm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel\n```\n-- ia32는 32비트 Windows 아키텍처입니다.\n\n---x64는 64비트 Windows 아키텍처입니다.\n\n-- arm64는 ARM Windows 아키텍처(Surface와 같은 장치)입니다.\n\n- Windows용 휴대용 EXE 파일(설치되지 않음) 생성:\n```\nnpm run electron:build -- --win --portable\n```\n\n#### 3. Linux 플랫폼 패키지\n- 기본 AppImage 형식(대부분의 Linux 배포용):\n```\nnpm run electron:build -- --linux\n```\n- snap(Ubuntu 및 snap 지원 릴리스용):\n```\nnpm run electron:build -- --linux --target snap\n```\n- deb(Debian/Ubuntu 시리즈용):\n```\nnpm run electron:build -- --linux --target deb\n```\n- rpm(Red Hat/Fedora 시리즈용):\n```\nnpm run electron:build -- --linux --target rpm\n```\n\n#### 4. 모든 플랫폼 패키지\n\nWindows, macOS 및 Linux를 모두 생성하는 설치 패키지가 필요한 경우 다음 명령을 사용할 수 있습니다.\n```\nnpm run electron:build -- -mwl\n```\n\n#### 5. 컴파일 설정 사용자 정의\n\nx64 및 arm64 스키마를 지정하거나 다른 대상 형식을 선택하는 등의 추가 옵션을 추가하여 패키지를 추가로 사용자 지정할 수 있습니다.\n\n\n## ⭐ 프로젝트 지원\n\n이 프로젝트가 도움이 되었다면 별을 눌러주세요! 여러분의 지원이 저희가 계속 개선할 수 있는 원동력입니다.\n\n[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)\n\n\n## ☑️ 피드백\n\n질문이나 제안이 있으면 issue 또는 pull request를 제출하십시오.\n\n## ⚠️ 면책 조항\n0. 본 프로그램은 쿠거우 제3자 클라이언트입니다. 쿠거우 공식이 아닙니다. 더 완벽한 기능이 필요하시면 공식 클라이언트 체험을 다운로드하십시오.\n1.본 프로젝트는 학습용입니다.저작권을 존중하고 이 프로젝트를 상업행위 및 불법용도로 이용하지 마십시오!\n2. 본 프로젝트를 사용하는 과정에서 저작권 데이터가 발생할 수 있습니다.본 프로젝트는 이러한 저작권 데이터에 대한 소유권이 없습니다.저작권 침해를 방지하기 위해 사용자는 본 프로젝트를 사용하는 과정에서 발생하는 저작권 데이터를 24시간 이내에 삭제해야 합니다.\n3. 본 프로젝트의 사용으로 인해 발생하는 본 계약 또는 본 프로젝트의 사용 또는 사용 불가능으로 인해 발생하는 모든 성격을 포함하는 직접, 간접, 특수, 우연 또는 결과적 손해(상업권 손실, 업무 정지, 컴퓨터 고장 또는 고장으로 인한 손해 배상 또는 기타 모든 상업적 손해 또는 손실을 포함하되 이에 국한되지 않음)는 사용자의 책임이다.\n            \n1. 현지 법률과 법규를 위반한 경우 본 프로젝트의 사용을 금지한다.사용자가 현지 법률과 법규가 허용하지 않는 것을 뻔히 알거나 모르는 상황에서 본 프로젝트를 사용하여 초래된 어떠한 위법 행위도 사용자가 부담하고 본 프로젝트는 이로 인해 초래된 어떠한 직접, 간접, 특수, 우연 또는 결과적 책임을 지지 않는다.\n            \n2. 음악 플랫폼은 쉽지 않습니다.저작권을 존중하고 정품을 지원하십시오.\n3. 본 프로젝트는 기술적 타당성에 대한 탐구 및 연구에만 사용되며 어떠한 상업(광고 등을 포함하되 이에 국한되지 않음) 합작과 기부도 받지 않습니다.\n4.공식 음악 플랫폼이 본 프로젝트가 부적절하다고 생각되면 본 프로젝트에 연락하여 변경하거나 제거할 수 있습니다.\n            \n\n## 📜 오픈 소스 라이센스\n\n본 프로젝트는 개인의 학습 연구에만 사용되며 상업 및 불법 용도로 사용되는 것을 금지합니다.\n\n기반 [MIT license](https://opensource.org/licenses/MIT) 오픈 소스로 라이센스를 부여합니다.\n\n## 👍 영감의 원천\n\nAPI 소스 코드는 [MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi) \n\n- [Apple Music](https://music.apple.com)\n- [YouTube Music](https://music.youtube.com)\n- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)\n- [쿨도그 뮤직](https://kugou.com/)\n\n## 🖼️ 캡처\n\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)\n\n## 🗓️ 스타 히스토리\n\n[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)\n"
  },
  {
    "path": "docs/README_ru.md",
    "content": "> **Note**: Этот документ на русском языке может не быть актуальным вовремя. Для актуального материала, пожалуйста, обратитесь к [версии на упрощенном китайском](https://github.com/iAJue/MoeKoeMusic/README.md).\n<br />\n<p align=\"center\">\n    <img src=\"https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png\" alt=\"Logo\" width=\"156\" height=\"156\">\n  <h2 align=\"center\" style=\"font-weight: 600\">MoeKoe Music</h2>\n  <p align=\"center\">\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest\"><img src=\"https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers\"><img src=\"https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases\"><img src=\"https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/iAJue\"><img src=\"https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square\" /></a>\n  </p>\n  <p align=\"center\">\n    Открытый, лаконичный и красивый сторонний клиент для Kugou\n    <br />\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/\" target=\"blank\"><strong>GitHub</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/releases\" target=\"blank\"><strong>Скачать</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://MoeJue.cn\" target=\"blank\"><strong>Блог</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://Music.MoeKoe.cn\" target=\"blank\"><strong>Сайт проекта</strong></a>\n  </p>\n  <p align=\"center\">\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/README.md\" target=\"blank\"><strong>简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md\" target=\"blank\"><strong>繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md\" target=\"blank\"><strong>日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md\" target=\"blank\"><strong>English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md\" target=\"blank\"><strong>한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md\" target=\"blank\"><strong>Русский</strong></a>\n    <br />\n    <br />\n  </p>\n</p>\n\n![images](https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png)\n\n## ❤️ Предисловие\n\nЕщё около 10 лет назад, когда я пользовался веб-версией QQ, я начал использовать Kugou Music (уже более десяти лет как фанат). За эти годы вся моя коллекция песен оказалась именно там. Позже я пытался перейти на NetEase Cloud или QQ Music и даже импортировать плейлисты из Kugou, но результаты были неудовлетворительными. Я в основном слушаю опенинги аниме, и многие треки просто невозможно найти на других платформах.\n\nВ итоге я вернулся к Kugou. Однако на Mac-версии Kugou периодически возникали проблемы с воспроизведением. По рекомендации друзей я теперь слушаю музыку через [концептуальную версию](https://t1.kugou.com/d2tBza3CSV2) Kugou — это одно из немногих приложений, где можно бесплатно слушать VIP-треки. Очень рекомендую.\n\nНа своей странице я писал, что очень люблю слушать музыку, особенно опенинги аниме. Как это доказать? Просто разработать собственный музыкальный плеер.\n\n\n## ✨ Особенности\n\n- ✅ Разработан на Vue.js\n- 🔴 Авторизация через аккаунт Kugou (QR-код/телефон/логин)\n- 📃 Отображение текстов песен\n- 📻 Ежедневные рекомендации\n- 🚫🤝 Никаких социальных функций\n- 🔗 Прямое подключение к официальным серверам, без сторонних API\n- ✔️ Автоматическое получение VIP ежедневно\n- 🎨 Переключение цветовых тем\n- 👋 Приветствие при запуске\n- ⚙️ Поддержка нескольких платформ\n- 🛠 Больше функций в разработке\n\n## 📢 Список задач\n- [x] 📺 Поддержка воспроизведения MV\n- [x] 🌚 Автоматическое переключение Light/Dark Mode\n- [x] 👆 Поддержка Touch Bar\n- [x] 🖥️ Поддержка PWA — можно установить через Chrome/Edge, нажав ➕ в адресной строке\n- [ ] 🎧 Поддержка Mpris\n- [x] ⌨️ Глобальные горячие клавиши\n- [x] 🤟 Мультиязычность\n- [x] 📻 Текст песни на рабочем столе\n- [x] ⚙️ Оптимизация архитектуры\n- [x] 🎶 Добавление/удаление песен и плейлистов в избранное\n\nИстория изменений: [Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)\n\n## 📦️ Установка\n\n### 1. Установка клиента\n\nСкачайте установочный пакет на странице [Releases](https://github.com/iAJue/MoeKoeMusic/releases).\n\n### 2. WEB-версия (Docker)\n\n* Примечание: после развёртывания откройте соответствующий порт на сервере или используйте обратный прокси.\n\n    1. Способ 1: Быстрый запуск (рекомендуется)\n\n    ```\n    git clone https://github.com/iAJue/MoeKoeMusic.git\n    cd MoeKoeMusic\n    git submodule update --init --recursive\n    docker compose up -d &\n    ```\n\n    2. ~~Способ 2: docker-compose (образ пока не загружен официально)~~\n\n    ```\n    docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest\n    ```\n\n    3. Способ 3: Через панель управления\n\n    * Удалённый образ, версия может отставать от официальной\n\n    ```\n    version: '3.3'\n\n    services:\n      moekoe-music:\n        image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest\n        container_name: moekoe-music\n        restart: unless-stopped\n        build:\n          context: .\n          dockerfile: Dockerfile\n        environment:\n          - PORT=6521\n          - platform=lite\n        ports:\n          - \"8080:8080\"  # Фронтенд\n          - \"6521:6521\"  # API\n\n    ```\n### 3. Развертывание в один клик\n[![Развернуть на EdgeOne Pages](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)\n\nУкажите свой API-адрес в переменной окружения (VITE_APP_API_URL)\n\n## ⚙️ Разработка\n\n1. Клонируйте репозиторий\n\n```sh\ngit clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git\n```\n\n2. Перейдите в директорию и установите зависимости\n\n```sh\ncd MoeKoeMusic\nnpm run install-all\n```\n3. Запустите режим разработки\n```sh\nnpm run dev\n```\n4. Сборка проекта\n```sh\nnpm run build\n```\n5. Компиляция\n  - Windows:\n  ```sh\n  npm run electron:build:win\n  ```\n  - Linux:\n  ```sh\n  npm run electron:build:linux\n  ```\n  - macOS:\n  ```sh\n  npm run electron:build:macos\n  ```\n\n\nБольше команд в файле `package.json` в секции `scripts`\n\n## 👷‍♂️ Сборка клиента\n\nЕсли на странице Release нет подходящего пакета для вашего устройства, вы можете собрать клиент самостоятельно.\n\n1. Установите [Node.js](https://nodejs.org/en/) версии >= 18.0.0.\n\n2. Клонируйте репозиторий: `git clone https://github.com/iAJue/MoeKoeMusic.git`\n\n3. Установите зависимости: `npm install`\n4. Скомпилируйте API-сервер\n    - Windows:\n        ```sh\n        npm run build:api:win\n        ```\n    - Linux:\n        ```sh\n        npm run build:api:linux\n        ```\n    - macOS:\n      ```sh\n      npm run build:api:macos\n      ```\n\n5. Выберите команду для сборки. Результат будет в директории `/dist_electron`. Подробнее: [electron-builder](https://www.electron.build/cli)\n\n\n#### 1. macOS\n   - Универсальный пакет (Intel и Apple Silicon):\n   ```\n   npm run electron:build -- --mac --universal\n   ```\n   - Только Intel:\n   ```\n   npm run electron:build -- --mac --x64\n   ```\n   - Только Apple Silicon:\n   ```\n   npm run electron:build -- --mac --arm64\n   ```\n\n\n#### 2. Windows\n\n   - NSIS-установщик (для большинства пользователей):\n   ```\n   npm run electron:build -- --win\n   ```\n   - EXE + Squirrel:\n   ```\n   npm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel\n   ```\n   - Портативная версия (без установки):\n   ```\n   npm run electron:build -- --win --portable\n   ```\n#### 3. Linux\n   - AppImage (для большинства дистрибутивов):\n   ```\n   npm run electron:build -- --linux\n   ```\n   - snap (Ubuntu и совместимые):\n   ```\n   npm run electron:build -- --linux --target snap\n   ```\n   - deb (Debian/Ubuntu):\n   ```\n   npm run electron:build -- --linux --target deb\n   ```\n   - rpm (Red Hat/Fedora):\n   ```\n   npm run electron:build -- --linux --target rpm\n   ```\n   - ARM64:\n   ```\n   npm run build:api:linux-aarch64\n   npm run electron:build:linux-aarch64\n   ```\n\n#### 4. Все платформы сразу\n  ```\n  npm run electron:build -- -mwl\n  ```\n\n#### 5. Дополнительные настройки\n\nВы можете добавить другие опции для настройки сборки, например, указать архитектуру x64 или arm64.\n\n## ⭐ Поддержать проект\n\nЕсли проект оказался полезным, поставьте Star! Ваша поддержка мотивирует нас продолжать развитие.\n\n[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)\n\n## ✅ Обратная связь\n\nПо любым вопросам или предложениям создавайте issue или pull request.\n\n## ⚠️ Отказ от ответственности\n0. Это сторонний клиент для Kugou, не официальное приложение. Для полного функционала используйте официальный клиент.\n1. Проект предназначен только для обучения. Уважайте авторские права и не используйте его в коммерческих или незаконных целях!\n2. При использовании проекта могут создаваться данные, защищённые авторским правом. Проект не владеет этими данными. Во избежание нарушения авторских прав удаляйте такие данные в течение 24 часов.\n3. Пользователь несёт ответственность за любой ущерб, возникший в результате использования проекта.\n4. Запрещено использовать проект в нарушение местного законодательства.\n5. Музыкальные платформы заслуживают поддержки. Уважайте авторские права, поддерживайте легальный контент.\n6. Проект предназначен только для исследования технических возможностей. Коммерческое сотрудничество и пожертвования не принимаются.\n7. Если правообладатели считают проект неуместным, свяжитесь с нами для изменения или удаления.\n\n\n## 📜 Лицензия\n\nПроект предназначен только для личного изучения. Коммерческое и незаконное использование запрещено.\n\nЛицензия: [GNU General Public License v2.0 (GPL-2.0)](https://github.com/iAJue/MoeKoeMusic/blob/main/LICENSE)\n\n## 👍 Источники вдохновения\n\nИсходный код API: [MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi)\n\n- [Apple Music](https://music.apple.com)\n- [YouTube Music](https://music.youtube.com)\n- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)\n- [Kugou Music](https://kugou.com/)\n\n## 🖼️ Скриншоты\n\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)\n\n\n## 🗓️ Star History\n\n[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)\n"
  },
  {
    "path": "docs/README_tw.md",
    "content": "> **注意**: 此繁體中文文檔可能更新不及時，最新內容請參考[簡體中文版本](https://github.com/iAJue/MoeKoeMusic/README.md)。\n<br />\n<p align=\"center\">\n<img src=\"https://github.com/iAJue/MoeKoeMusic/raw/main/images/logo.png \"alt=\"Logo\"width=\"156\"height=\"156\">\n<h2 align=\"center\"style=\"font-weight: 600\">MoeKoe Music</h2>\n  <p align=\"center\">\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases/latest\"><img src=\"https://img.shields.io/github/v/release/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/stargazers\"><img src=\"https://img.shields.io/github/stars/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/releases\"><img src=\"https://img.shields.io/github/downloads/MoeKoeMusic/MoeKoeMusic/total?style=flat-square\" /></a>\n    <a href=\"https://github.com/MoeKoeMusic/MoeKoeMusic/blob/main/LICENSE\"><img src=\"https://img.shields.io/github/license/MoeKoeMusic/MoeKoeMusic?style=flat-square\" /></a>\n    <a href=\"https://github.com/iAJue\"><img src=\"https://img.shields.io/badge/%F0%9F%8E%89_Create_by_iAJue-with_Love_%E2%9D%A4-pink?style=flat-square\" /></a>\n  </p>\n<p align=\"center\">\n一款開源簡潔高顏值的酷狗協力廠商用戶端\n<br />\n<a href=\"https://github.com/iAJue/MoeKoeMusic/\" target=\"blank\"><strong>🌎 GitHub 倉庫</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://github.com/iAJue/MoeKoeMusic/releases\" target=\"blank\"><strong>📦️ 下載安裝包</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://MoeJue.cn\" target=\"blank\"><strong>💬 訪問部落格</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n<a href=\"https://Music.MoeKoe.cn\" target=\"blank\"><strong>🏠 項目主頁</strong></a>\n</p>\n<p align=\"center\">\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/README.md\" target=\"blank\"><strong>🇨🇳 简体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_tw.md\" target=\"blank\"><strong>🇨🇳 繁体中文</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ja.md\" target=\"blank\"><strong>🇯🇵 日本語</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_en.md\" target=\"blank\"><strong>🇺🇸 English</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ko.md\" target=\"blank\"><strong>🇰🇷 한국어</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://github.com/iAJue/MoeKoeMusic/blob/main/docs/README_ru.md\" target=\"blank\"><strong>🇷🇺 Русский</strong></a>\n    <br />\n    <br />\n</p>\n</p>\n\n![images](https://github.com/iAJue/MoeKoeMusic/raw/main/images/1.png)\n\n## ❤️ 前言\n\n早在10年前後的樣子，那會在用網頁版QQ的時候我就已經開始使用酷狗音樂了(也是十來年的老粉了)，所以這些年收藏的歌曲全部都在上面. 後來我也嘗試開始使用網易雲或QQ音樂，也嘗試把酷狗的歌單導入進去，但是效果都不盡人意. 我聽的大多是日漫OP，好多歌曲都沒辦法找到.\n\n兜兜轉轉最後還是回到酷狗，但是在Mac端的酷狗，時常可能會出現不能播放的情况，雖說介面沒什麼功能，但也挺好的. 在網友的安利下，我現在一直是在酷狗的[概念版](https://t1.kugou.com/d2tBza3CSV2)上聽歌，並且是市面上為數不多能免費聽VIP歌曲的音樂播放軟體了，力推.\n\n我在我的個人介紹頁面說我特別喜歡聽歌，尤其是日漫OP. 怎麼證明呢？ (之前我網頁版歌單也年久失修了)那就自己開發一個音樂播放機.\n\n\n## ✨  特性\n\n- ✅  使用Vue.js全家桶開發\n- 🔴  酷狗帳號登入(掃碼/手機/帳號登入)\n- 📃  支持歌詞顯示\n- 📻  每日推薦歌曲\n- 🚫🤝  無任何社交功能\n- 🔗  官方服務器直連，無任何協力廠商API\n- ✔️  每日自動領取VIP，登入就是VIP\n- 🎨  主題色切換\n- 👋  啟動問候語\n- ⚙️  多平臺支持\n- 🛠  更多特性開發中\n\n## 📢 Todo List\n- [x] 📺  支持MV播放\n- [x] 🌚 Light/Dark Mode  自動切換\n- [x] 👆  支持Touch Bar\n- [x] 🖥️  支持PWA，可在Chrome/Edge裏點擊地址欄右邊的 ➕  安裝到電腦\n- [ ] 🎧  支持Mpris\n- [x] ⌨️  全域快速鍵\n- [x] 🤟  多語言支持\n- [x] 📻  案頭歌詞\n- [x] ⚙️  系統架構優化\n- [x] 🎶  歌曲、歌單/收藏、取消\n\n更新日誌請查看[Commits](https://github.com/iAJue/MoeKoeMusic/commits/main/)\n\n## 📦️ 安裝\n\n### 1. 用戶端安裝\n\n訪問本項目的 [Releases](https://github.com/iAJue/MoeKoeMusic/releases) 頁面下載安裝包。\n\n### 2. WEB端安裝（docker）\n\n* 注意：部署後請開放伺服器對應埠才可使用，或者使用反向代理實現功能變數名稱訪問。\n\n    1. 方式一：快速啟動（推薦）\n\n    ```\n    git clone https://github.com/iAJue/MoeKoeMusic.git\n    cd MoeKoeMusic\n    git submodule update --init --recursive\n    docker compose up -d &\n    ```\n\n    2. ~~方式二：使用docker-compose一鍵安裝 （映像暫未上傳官方）~~\n    \n    ```\n    docker run -d --name MoeKoeMusic -p 8080:8080 -p 6521:6521 -e PORT=6521 -e platform=lite iajue/moekoe-music:latest\n    ```\n\n    3. 方式三：寶塔容器編排\n\n    * 遠端映像，版本可能會落後於官方\n    \n    ```\n    version: '3.3'\n    \n    services:\n      moekoe-music:\n        # 映像地址\n        image: registry.cn-wulanchabu.aliyuncs.com/youngxj/moekoe-music:latest\n        container_name: moekoe-music # 容器名\n        restart: unless-stopped # 自動重啟\n        build:\n          context: .\n          dockerfile: Dockerfile\n        environment:\n          - PORT=6521\n          - platform=lite\n        ports: # 埠映射\n          - \"8080:8080\"  # 前端服務\n          - \"6521:6521\"  # 介面服務\n    \n    ```\n    \n    複製上面的內容，貼上到寶塔面板的容器編排裡面，編排名稱為MoeKoeMusic，點擊部署即可。\n### 3. 一鍵部署\n[![使用 EdgeOne Pages 部署](https://cdnstatic.tencentcs.com/edgeone/pages/deploy.svg)](https://edgeone.ai/pages/new?template=https://github.com/iAJue/moekoemusic&install-command=npm%20install&output-directory=dist&root-directory=.%2F&build-command=npm%20run%20build&env=VITE_APP_API_URL)\n\n需在環境變數(VITE_APP_API_URL)中填寫自己的API地址\n\n## ⚙️  開發\n\n1.尅隆本倉庫\n\n```sh\ngit clone --recurse-submodules https://github.com/iAJue/MoeKoeMusic.git\n```\n\n2.進入目錄並安裝依賴\n\n```sh\ncd MoeKoeMusic\nnpm run install-all\n```\n3.啟動開發者模式\n```sh\nnpm run dev\n```\n4.打包項目\n```sh\nnpm run build\n```\n5.編譯項目\n- Windows:\n```sh\nnpm run electron:build:win [默認NSIS安裝包]\n```\n- Linux:\n```sh\nnpm run electron:build:linux [默認AppImage格式]\n```\n- macOS:\n```sh\nnpm run electron:build:macos [默認雙架構]\n```\n\n\n更多命令請查看`package.json`檔案`scripts`\n\n## 👷‍♂️  編譯用戶端\n\n如果在Release頁面沒有找到適合你的設備的安裝包的話，你可以根據下麵的步驟來打包自己的用戶端。\n\n1.安裝[Node.js](https://nodejs.org/en/)，並確保`Node.js`版本>= 18.0.0。\n\n2.使用`git clone https://github.com/iAJue/MoeKoeMusic.git `尅隆本倉庫到本地。\n\n3.使用`npm install`安裝項目依賴。\n4.編譯API服務端\n- Windows:\n```sh\nnpm run build:api:win\n```\n- Linux:\n```sh\nnpm run build:api:linux\n```\n- macOS:\n```sh\nnpm run build:api:macos\n```\n\n5.選擇下列的命令來打包適合的你的安裝包，打包出來的檔案在`/dist_electron`目錄下。 瞭解更多資訊可訪問[electron-builder檔案](https://www.electron.build/cli)\n\n\n#### 1. 打包macOS平臺\n-通用的macOS包(Intel和Apple Silicon雙架構)：\n```\nnpm run electron:build -- --mac --universal\n```\n-僅Intel架構：\n```\nnpm run electron:build -- --mac --x64\n```\n-僅Apple Silicon架構：\n```\nnpm run electron:build -- --mac --arm64\n```\n\n\n#### 2. 打包Windows平臺\n\n-默認NSIS安裝包(適合大多數Windows用戶)：\n```\nnpm run electron:build -- --win\n```\n-為Windows創建EXE檔案和Squirrel安裝包：\n```\nnpm run electron:build -- --win --ia32 --x64 --arm64 --target squirrel\n```\n---ia32為32比特Windows架構。\n---x64為64比特Windows架構。\n---arm64為ARM Windows架構(Surface等設備)。\n\n-為Windows生成可擕式的EXE檔案(免安裝)：\n```\nnpm run electron:build -- --win --portable\n```\n#### 3. 打包Linux平臺\n-默認AppImage格式(適用於大多數Linux發行版本)：\n```\nnpm run electron:build -- --linux\n```\n- snap(適用於Ubuntu和支持snap的發行版本)：\n```\nnpm run electron:build -- --linux --target snap\n```\n- deb(適用於Debian/Ubuntu系列)：\n```\nnpm run electron:build -- --linux --target deb\n```\n- rpm(適用於Red Hat/Fedora系列)：\n```\nnpm run electron:build -- --linux --target rpm\n```\n\n#### 4. 打包所有平臺\n\n如果需要同時生成Windows、macOS和Linux的安裝包，可以使用以下命令：\n```\nnpm run electron:build -- -mwl\n```\n\n#### 5. 自定義編譯設定\n\n您可以根據需要添加其他選項來進一步自定義打包，例如指定x64和arm64架構，或選擇不同的目標格式。\n\n\n## ⭐ 支持項目\n\n如果您覺得這個項目對您有幫助，歡迎給我們一個 Star！您的支持是我們持續改進的動力。\n\n[![GitHub stars](https://img.shields.io/github/stars/iAJue/MoeKoeMusic.svg?style=social&label=Star)](https://github.com/iAJue/MoeKoeMusic)\n\n\n## ☑️  反饋\n\n如有任何問題或建議，歡迎提交issue或pull request。\n\n## ⚠️ 免責聲明\n0. 本程式是酷狗協力廠商用戶端，並非酷狗官方，需要更完善的功能請下載官方用戶端體驗.\n1. 本項目僅供學習使用，請尊重版權，請勿利用此項目從事商業行為及非法用途!\n2. 使用本項目的過程中可能會產生版權數據。 對於這些版權數據，本項目不擁有它們的所有權。 為了避免侵權，使用者務必在24小時內清除使用本項目的過程中所產生的版權數據。\n3. 由於使用本項目產生的包括由於本協定或由於使用或無法使用本項目而引起的任何性質的任何直接、間接、特殊、偶然或結果性損害(包括但不限於因商譽損失、停工、電腦故障或故障引起的損害賠償，或任何及所有其他商業損害或損失)由使用者負責。         \n1. 禁止在違反當地法律法規的情况下使用本項目。 對於使用者在明知或不知當地法律法規不允許的情况下使用本項目所造成的任何違法違規行為由使用者承擔，本項目不承擔由此造成的任何直接、間接、特殊、偶然或結果性責任。\n            \n2. 音樂平臺不易，請尊重版權，支持正版。\n3. 本項目僅用於對科技可行性的探索及研究，不接受任何商業(包括但不限於廣告等)合作及捐贈。\n4. 如果官方音樂平臺覺得本項目不妥，可聯系本項目更改或移除。\n            \n\n## 📜  開源許可\n\n本項目僅供個人學習研究使用，禁止用於商業及非法用途。\n\n基於[MIT license](https://opensource.org/licenses/MIT)許可進行開源。\n\n## 👍 靈感來源\n\nAPI原始程式碼來自[MakcRe/KuGouMusicApi](https://github.com/MakcRe/KuGouMusicApi)\n\n- [Apple Music](https://music.apple.com)\n- [YouTube Music](https://music.youtube.com)\n- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)\n- [酷狗音樂](https://kugou.com/)\n\n## 🖼️  截圖\n\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/2.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/3.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/4.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/5.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/6.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/7.png)\n![image](https://github.com/iAJue/MoeKoeMusic/raw/main/images/8.png)\n\n## 🗓️ Star 歷史\n\n[![Star History Chart](https://api.star-history.com/svg?repos=iAJue/MoeKoeMusic&type=Date)](https://www.star-history.com/#iAJue/MoeKoeMusic&Date)\n"
  },
  {
    "path": "electron/appServices.js",
    "content": "import { app, ipcMain, BrowserWindow, screen, Tray, Menu, TouchBar, globalShortcut, dialog, shell, nativeImage } from 'electron';\nimport path from 'path';\nimport { spawn } from 'child_process';\nimport log from 'electron-log';\nimport Store from 'electron-store';\nimport { fileURLToPath } from 'url';\nimport isDev from 'electron-is-dev';\nimport fs from 'fs';\nimport { exec } from 'child_process';\nimport { checkForUpdates } from './services/updater.js';\nimport { Notification } from 'electron';\nimport extensionManager from './extensions/extensionManager.js';\nimport { t } from './language/i18n.js';\nimport { bindExternalLinkHandler } from './services/externalLinkHandler.js';\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\nconst store = new Store();\nconst { TouchBarLabel, TouchBarButton, TouchBarGroup, TouchBarSpacer } = TouchBar;\nlet mainWindow = null;\nlet apiProcess = null;\nlet tray = null;\n\n// 创建主窗口\nexport function createWindow() {\n    const savedConfig = store.get('settings');\n    const useNativeTitleBar = savedConfig?.nativeTitleBar === 'on' ? true : false;\n    const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;\n\n    const windowWidth = Math.min(1200, screenWidth * 0.8);\n    const windowHeight = Math.min(938, screenHeight * 0.9);\n    const lastWindowState = store.get('windowState') || {};\n\n    let x = lastWindowState.x;\n    let y = lastWindowState.y;\n    let width = lastWindowState.width || windowWidth;\n    let height = lastWindowState.height || windowHeight;\n\n    width = Math.min(width, screenWidth);\n    height = Math.min(height, screenHeight);\n\n    const isValidPosition = x !== undefined && y !== undefined &&\n        x >= 0 && x <= screenWidth &&\n        y >= 0 && y <= screenHeight;\n\n    if (!isValidPosition) {\n        x = Math.floor((screenWidth - width) / 2);\n        y = Math.floor((screenHeight - height) / 2);\n    }\n\n    mainWindow = new BrowserWindow({\n        width: width,\n        height: height,\n        x: x,\n        y: y,\n        minWidth: 890,\n        minHeight: 750,\n        show: savedConfig?.startMinimized === 'on' ? false : true,\n        frame: useNativeTitleBar,\n        titleBarStyle: useNativeTitleBar ? 'default' : 'hiddenInset',\n        autoHideMenuBar: true,\n        webPreferences: {\n            preload: path.join(__dirname, 'preload.cjs'),\n            contextIsolation: true,\n            nodeIntegration: false,\n            sandbox: false,\n            webSecurity: false, // 禁用 CORS、同源策略\n            allowRunningInsecureContent: true, // 允许混合内容\n            zoomFactor: 1.0\n        },\n        icon: getIconPath('icon.ico')\n    });\n    bindExternalLinkHandler(mainWindow);\n\n    if (store.get('maximize')) {\n        mainWindow.maximize();\n    }\n\n    if (isDev) {\n        mainWindow.loadURL('http://localhost:8080');\n        mainWindow.webContents.openDevTools();\n    } else {\n        if (savedConfig?.networkMode == 'devnet') { //开发网\n            mainWindow.loadURL('http://localhost:8080');\n        } else if (savedConfig?.networkMode == 'testnet') { //测试网\n            mainWindow.loadURL('https://app.testnet.music.moekoe.cn');\n        } else { //主网\n            mainWindow.loadFile(path.join(__dirname, '../dist/index.html'));\n        }\n    }\n\n    mainWindow.webContents.once('dom-ready', () => {\n        extensionManager.loadChromeExtensions();\n    });\n\n    mainWindow.webContents.on('dom-ready', () => {\n        console.log('DOM Ready');\n    });\n\n    mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {\n        console.error('Failed to load:', errorCode, errorDescription);\n    });\n\n    mainWindow.once('ready-to-show', () => {\n        if (savedConfig?.startMinimized === 'on') {\n            mainWindow.hide();\n        }\n    });\n\n    mainWindow.webContents.on('did-finish-load', () => {\n        console.log('Page Loaded Successfully');\n        mainWindow.webContents.insertCSS('::-webkit-scrollbar { display: none; }');\n        if (!store.get('disclaimerAccepted')) {\n            mainWindow.webContents.send('show-disclaimer');\n        }\n        mainWindow.webContents.send('version', app.getVersion());\n    });\n\n    mainWindow.on('close', (event) => {\n        const savedConfig = store.get('settings');\n        if (savedConfig?.minimizeToTray === 'off') {\n            app.isQuitting = true;\n            app.quit();\n        }\n        if (!app.isQuitting) {\n            event.preventDefault();\n            mainWindow.hide();\n        }\n    });\n\n    if (process.platform === 'win32') {\n        setThumbarButtons(mainWindow);\n    }\n\n    if (savedConfig?.desktopLyrics === 'on') {\n        createLyricsWindow();\n    }\n    return mainWindow;\n}\n\nlet lyricsWindow;\n\nexport function createLyricsWindow() {\n    const { width: screenWidth, height: screenHeight } = screen.getPrimaryDisplay().workAreaSize;\n    const windowWidth = Math.floor(screenWidth * 0.7);\n    const windowHeight = 200;\n\n    const savedLyricsPosition = store.get('lyricsWindowPosition') || {};\n    const savedLyricsSize = store.get('lyricsWindowSize') || {\n        width: windowWidth,\n        height: windowHeight\n    };\n\n    let x = savedLyricsPosition.x;\n    let y = savedLyricsPosition.y;\n    let width = savedLyricsSize.width || windowWidth;\n    let height = savedLyricsSize.height || windowHeight;\n\n    // 限制窗口尺寸不超过屏幕\n    width = Math.min(width, screenWidth);\n    height = Math.min(height, screenHeight);\n\n    // 检查位置是否有效\n    const isValidPosition = x !== undefined && y !== undefined &&\n        x >= 0 && x <= screenWidth &&\n        y >= 0 && y <= screenHeight;\n\n    // 如果位置无效，设置默认位置\n    if (!isValidPosition) {\n        x = Math.floor((screenWidth - width) / 2);\n        y = screenHeight - height;\n    }\n\n    lyricsWindow = new BrowserWindow({\n        width: width,\n        height: height,\n        x: x,\n        y: y,\n        minWidth: 800,\n        minHeight: 200,\n        alwaysOnTop: true,\n        frame: false,\n        transparent: true,\n        resizable: true,\n        skipTaskbar: true,\n        hasShadow: false,\n        webPreferences: {\n            preload: path.join(__dirname, 'preload.cjs'),\n            contextIsolation: true,\n            nodeIntegration: false,\n            sandbox: false,\n            webSecurity: false, // 禁用 CORS、同源策略\n            allowRunningInsecureContent: true, // 允许混合内容\n            backgroundThrottling: false,\n            zoomFactor: 1.0\n        }\n    });\n\n    lyricsWindow.on('resize', () => {\n        const [width, height] = lyricsWindow.getSize();\n        store.set('lyricsWindowSize', { width, height });\n    });\n    mainWindow.lyricsWindow = lyricsWindow;\n    lyricsWindow.on('closed', () => {\n        mainWindow.lyricsWindow = null;\n    });\n    if (isDev) {\n        lyricsWindow.loadURL('http://localhost:8080/#/lyrics');\n        lyricsWindow.webContents.openDevTools({ mode: 'detach' });\n    } else {\n        lyricsWindow.loadFile(path.join(__dirname, '../dist/index.html'), {\n            hash: 'lyrics'\n        });\n    }\n\n\n\n    // 设置窗口置顶级别\n    lyricsWindow.setAlwaysOnTop(true, 'screen-saver');\n    lyricsWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });\n\n    // 允许窗口透明\n    lyricsWindow.setBackgroundColor('#00000000');\n}\n\nexport function createMvWindow() {\n    const { screenWidth, screenHeight } = screen.getPrimaryDisplay().workAreaSize;\n    return new BrowserWindow({\n        width: Math.min(screenWidth * 0.8, 1280),\n        height: Math.min(screenHeight * 0.8, 720),\n        frame: false,\n        transparent: true,\n        show: false,\n        titleBarStyle: 'hiddenInset',\n        autoHideMenuBar: true,\n        backgroundColor: '#00000000',\n        webPreferences: {\n            preload: path.join(__dirname, 'preload.cjs'),\n            contextIsolation: true,\n            nodeIntegration: false,\n            sandbox: false,\n            webSecurity: false, // 禁用 CORS、同源策略\n            allowRunningInsecureContent: true, // 允许混合内容\n            zoomFactor: 1.0,\n            devTools: isDev\n        },\n        icon: getIconPath('icon.ico')\n    });\n}\n\nconst getIconPath = (iconName, subPath = '') => path.join(\n    isDev ? __dirname + '/../build/icons' : process.resourcesPath + '/icons',\n    subPath,\n    iconName\n);\n\nexport function getTray() {\n    return tray;\n}\n\n// 创建托盘图标及菜单\nexport function createTray(mainWindow, title = '') {\n    if (tray && title) {\n        tray.setToolTip(title);\n        return tray;\n    }\n\n    let trayIconName\n    if (process.platform === 'linux') {\n        trayIconName = 'linux-icon.png'\n    } else if (process.platform === 'darwin') {\n        trayIconName = 'tray-icon.png'\n    } else {\n        trayIconName = 'tray-icon.ico'\n    }\n\n    tray = new Tray(getIconPath(trayIconName));\n    tray.setToolTip('MoeKoe Music');\n\n    const contextMenu = Menu.buildFromTemplate([\n        {\n            label: t('project-home'),\n            icon: getIconPath('home.png', 'menu'),\n            click: () => {\n                shell.openExternal('https://Music.MoeKoe.cn');\n            }\n        },\n        {\n            label: t('report-bug'),\n            icon: getIconPath('bug.png', 'menu'),\n            click: () => {\n                shell.openExternal('https://github.com/iAJue/MoeKoeMusic/issues');\n            }\n        },\n        {\n            label: t('prev-track'),\n            icon: getIconPath('prev.png', 'menu'),\n            accelerator: 'Alt+CommandOrControl+Left',\n            click: () => {\n                mainWindow.webContents.send('play-previous-track');\n            }\n        },\n        {\n            label: t('pause'),\n            accelerator: 'Alt+CommandOrControl+Space',\n            icon: getIconPath('play.png', 'menu'),\n            click: () => {\n                mainWindow.webContents.send('toggle-play-pause');\n            }\n        },\n        {\n            label: t('next-track'),\n            accelerator: 'Alt+CommandOrControl+Right',\n            icon: getIconPath('next.png', 'menu'),\n            click: () => {\n                mainWindow.webContents.send('play-next-track');\n            }\n        },\n        {\n            label: t('check-updates'),\n            icon: getIconPath('update.png', 'menu'),\n            click: () => {\n                checkForUpdates(false);\n            }\n        },\n        {\n            label: t('restart-app'),\n            icon: getIconPath('restart.png', 'menu'),\n            click: () => {\n                app.relaunch();\n                app.isQuitting = true;\n                app.quit();\n            }\n        },\n        {\n            label: t('show-hide'),\n            accelerator: 'CmdOrCtrl+Shift+S',\n            icon: getIconPath('show.png', 'menu'),\n            click: () => {\n                if (mainWindow) {\n                    if (mainWindow.isVisible()) {\n                        mainWindow.hide();\n                    } else {\n                        mainWindow.show();\n                    }\n                }\n            }\n        },\n        {\n            label: t('quit'),\n            accelerator: 'CmdOrCtrl+Q',\n            icon: getIconPath('quit.png', 'menu'),\n            click: () => {\n                app.isQuitting = true;\n                app.quit();\n            }\n        }\n    ]);\n\n    switch (process.platform) {\n        case 'linux':\n            tray.setContextMenu(contextMenu);\n            break;\n        default:\n            tray.on('right-click', () => {\n                tray.popUpContextMenu(contextMenu);\n            });\n    }\n    tray.on('click', () => {\n        if (!mainWindow.isVisible()) {\n            mainWindow.show();\n        } else if (!mainWindow.isFocused()) {\n            mainWindow.show();\n            mainWindow.focus();\n        } else {\n            mainWindow.hide(); //大概率永远不会执行\n        }\n    });\n    tray.on('double-click', () => {\n        mainWindow.show();\n    });\n    return tray;\n}\n\n// 创建 TouchBar\nexport function createTouchBar(mainWindow) {\n    const ICON_SIZE = 16;\n\n    let isPlaying = false;\n\n    const iconPath = (iconName) => {\n        const originalIcon = nativeImage.createFromPath(\n            getIconPath(`${iconName}.png`)\n        );\n\n        // 调整图标大小\n        return originalIcon.resize({\n            width: ICON_SIZE,\n            height: ICON_SIZE,\n        });\n    };\n\n    const prevButton = new TouchBarButton({\n        icon: iconPath(\"prev\"),\n        iconPosition: \"center\",\n        click: () => {\n            mainWindow.webContents.send(\"play-previous-track\");\n        },\n    });\n\n    const playPauseButton = new TouchBarButton({\n        icon: iconPath(isPlaying ? \"pause\" : \"play\"),\n        iconPosition: \"center\",\n        click: () => {\n            isPlaying = !isPlaying;\n            playPauseButton.icon = iconPath(isPlaying ? \"pause\" : \"play\");\n            mainWindow.webContents.send(\"toggle-play-pause\");\n        },\n    });\n\n    const nextButton = new TouchBarButton({\n        icon: iconPath(\"next\"),\n        iconPosition: \"center\",\n        click: () => {\n            mainWindow.webContents.send(\"play-next-track\");\n        },\n    });\n\n    // 歌词\n    const lyricsLabel = new TouchBarLabel({\n        label: t('no-lyrics'),\n        textColor: \"#FFFFFF\",\n    });\n\n    const touchBar = new TouchBar({\n        items: [\n            prevButton,\n            new TouchBarSpacer({ size: \"small\" }),\n            playPauseButton,\n            new TouchBarSpacer({ size: \"small\" }),\n            nextButton,\n            new TouchBarSpacer({ size: \"flexible\" }),\n            lyricsLabel,\n            new TouchBarSpacer({ size: \"flexible\" }),\n        ],\n    });\n\n    mainWindow.setTouchBar(touchBar);\n\n    // 监听播放状态变化\n    ipcMain.on(\"play-pause-action\", (event, playing) => {\n        isPlaying = playing;\n        playPauseButton.icon = iconPath(isPlaying ? \"pause\" : \"play\");\n    });\n\n    // 监听歌词更新\n    ipcMain.on(\"update-current-lyrics\", (event, currentLyric) => {\n        if (currentLyric) {\n            lyricsLabel.label = currentLyric;\n        }\n    });\n\n    return touchBar;\n}\n\n// 启动 API 服务器\nexport function startApiServer() {\n    return new Promise((resolve, reject) => {\n        let apiPath = '';\n        if (isDev) {\n            return resolve();\n            // apiPath = path.join(__dirname, '../api/app_api');\n        } else {\n            switch (process.platform) {\n                case 'win32':\n                    apiPath = path.join(process.resourcesPath, '../api', 'app_win.exe');\n                    break;\n                case 'darwin':\n                    apiPath = path.join(process.resourcesPath, '../api', 'app_macos');\n                    break;\n                case 'linux':\n                    apiPath = path.join(process.resourcesPath, '../api', 'app_linux');\n                    break;\n                default:\n                    reject(new Error(`Unsupported platform: ${process.platform}`));\n                    return;\n            }\n        }\n\n        log.info(`API路径: ${apiPath}`);\n\n        if (!fs.existsSync(apiPath)) {\n            const error = new Error(`API可执行文件未找到：${apiPath}`);\n            log.error(error.message);\n            reject(error);\n            return;\n        }\n\n        // 启动 API 服务器进程\n        const savedConfig = store.get('settings') || {};\n        const proxy = savedConfig?.proxy;\n        const proxyUrl = savedConfig?.proxyUrl;\n        const dataSource = savedConfig?.dataSource || 'concept';\n\n        const Args = [];\n        if (dataSource === 'concept') {\n            Args.push('--platform=lite');\n            log.info('API data source: concept (lite mode)');\n        }\n        if (proxy === 'on' && proxyUrl) {\n            const proxyAddress = String(proxyUrl).trim();\n            if (proxyAddress) {\n                Args.push(`--proxy=${proxyAddress}`);\n                log.info(`API proxy enabled: ${proxyAddress}`);\n            }\n        }\n        Args.push('--port=6521');\n        apiProcess = spawn(apiPath, Args, { windowsHide: true });\n\n        apiProcess.stdout.on('data', (data) => {\n            log.info(`API输出: ${data}`);\n            if (data.toString().includes('running')) {\n                console.log('API服务器已启动');\n                resolve();\n            }\n        });\n\n        apiProcess.stderr.on('data', (data) => {\n            log.error(`API 错误: ${data}`);\n            reject(data);\n        });\n\n        apiProcess.on('close', (code) => {\n            log.info(`API 关闭，退出码: ${code}`);\n        });\n\n        apiProcess.on('error', (error) => {\n            log.error('启动 API 失败:', error);\n            reject(error);\n        });\n    });\n}\n\n// 停止 API 服务器\nexport function stopApiServer() {\n    if (apiProcess) {\n        process.kill(apiProcess.pid, 'SIGKILL');\n        apiProcess = null;\n    }\n}\n\n// 注册快捷键\nexport function registerShortcut() {\n    try {\n        const settings = store.get('settings');\n        globalShortcut.unregisterAll();\n        let clickFunc = () => { app.isQuitting = true; };\n        if (process.platform === 'darwin') {\n            app.on('before-quit', clickFunc);\n        } else {\n            clickFunc = () => {\n                app.isQuitting = true;\n                app.quit();\n            };\n            if (settings?.shortcuts?.quitApp) {\n                globalShortcut.register(settings?.shortcuts?.quitApp, clickFunc);\n            } else if (!settings?.shortcuts) {\n                globalShortcut.register('CmdOrCtrl+Q', clickFunc);\n            }\n        }\n\n        clickFunc = () => {\n            if (mainWindow) {\n                if (mainWindow.isVisible()) {\n                    mainWindow.hide();\n                } else {\n                    mainWindow.show();\n                }\n            }\n        }\n        if (settings?.shortcuts?.mainWindow) {\n            globalShortcut.register(settings?.shortcuts?.mainWindow, clickFunc);\n        } else if (!settings?.shortcuts) {\n            globalShortcut.register('CmdOrCtrl+Shift+S', clickFunc);\n        }\n\n        clickFunc = () => mainWindow.webContents.send('play-previous-track');\n        if (settings?.shortcuts?.prevTrack) {\n            globalShortcut.register(settings?.shortcuts?.prevTrack, clickFunc);\n        } else if (!settings?.shortcuts) {\n            globalShortcut.register('Alt+CommandOrControl+Left', clickFunc);\n        }\n\n        clickFunc = () => mainWindow.webContents.send('play-next-track');\n        if (settings?.shortcuts?.nextTrack) {\n            globalShortcut.register(settings?.shortcuts?.nextTrack, clickFunc);\n        } else if (!settings?.shortcuts) {\n            globalShortcut.register('Alt+CommandOrControl+Right', clickFunc);\n        }\n\n        clickFunc = () => mainWindow.webContents.send('volume-up');\n        if (settings?.shortcuts?.volumeUp) {\n            globalShortcut.register(settings?.shortcuts?.volumeUp, clickFunc);\n        } else if (!settings?.shortcuts) {\n            globalShortcut.register('Alt+CommandOrControl+Up', clickFunc);\n        }\n\n        clickFunc = () => mainWindow.webContents.send('volume-down');\n        if (settings?.shortcuts?.volumeDown) {\n            globalShortcut.register(settings?.shortcuts?.volumeDown, clickFunc);\n        } else if (!settings?.shortcuts) {\n            globalShortcut.register('Alt+CommandOrControl+Down', clickFunc);\n        }\n\n        clickFunc = () => mainWindow.webContents.send('toggle-play-pause');\n        if (settings?.shortcuts?.playPause) {\n            globalShortcut.register(settings?.shortcuts?.playPause, clickFunc);\n        } else if (!settings?.shortcuts) {\n            globalShortcut.register('Alt+CommandOrControl+Space', clickFunc);\n        }\n\n        clickFunc = () => mainWindow.webContents.send('toggle-mute');\n        if (settings?.shortcuts?.mute) {\n            globalShortcut.register(settings?.shortcuts?.mute, clickFunc);\n        } else if (!settings?.shortcuts) {\n            globalShortcut.register('Alt+CommandOrControl+M', clickFunc);\n        }\n\n        clickFunc = () => mainWindow.webContents.send('toggle-like');\n        if (settings?.shortcuts?.like) {\n            globalShortcut.register(settings?.shortcuts?.like, clickFunc);\n        } else if (!settings?.shortcuts) {\n            globalShortcut.register('Alt+CommandOrControl+L', clickFunc);\n        }\n\n        clickFunc = () => mainWindow.webContents.send('toggle-mode');\n        if (settings?.shortcuts?.mode) {\n            globalShortcut.register(settings?.shortcuts?.mode, clickFunc);\n        } else if (!settings?.shortcuts) {\n            globalShortcut.register('Alt+CommandOrControl+P', clickFunc);\n        }\n\n        clickFunc = () => {\n            if (mainWindow.lyricsWindow) {\n                mainWindow.lyricsWindow.close();\n                mainWindow.lyricsWindow = null;\n                new Notification({\n                    title: t('desktop-lyrics-closed'),\n                    body: t('this-time-only'),\n                    icon: getIconPath('logo.png')\n                }).show();\n            } else {\n                createLyricsWindow();\n            }\n        }\n        if (settings?.shortcuts?.toggleDesktopLyrics) {\n            globalShortcut.register(settings.shortcuts.toggleDesktopLyrics, clickFunc);\n        } else if (!settings?.shortcuts) {\n            globalShortcut.register('Alt+Ctrl+D', clickFunc);\n        }\n    } catch {\n        dialog.showMessageBox({\n            type: 'error',\n            title: t('hint'),\n            message: t('shortcut-failed'),\n            buttons: [t('ok')]\n        });\n    }\n}\n\n// 播放启动问候语\nexport function playStartupSound() {\n    const savedConfig = store.get('settings');\n    if (!savedConfig || (savedConfig['greetings'] !== 'on' && savedConfig['greetings'] !== 'null')) {\n        return;\n    }\n    const audioFiles = [\n        '/assets/sound/yise-jp.mp3',\n        '/assets/sound/qiqi-jp.mp3',\n        '/assets/sound/qiqi-zh.mp3'\n    ];\n    const randomIndex = Math.floor(Math.random() * audioFiles.length);\n    const soundPath = isDev\n        ? path.join(__dirname, '..', 'public', audioFiles[randomIndex])\n        : path.join(process.resourcesPath, 'public', audioFiles[randomIndex]);\n    try {\n        switch (process.platform) {\n            case 'win32':\n                const escapedPath = soundPath.replace(/'/g, \"''\");\n                exec(`powershell -c \"Add-Type -AssemblyName PresentationCore; $player = New-Object System.Windows.Media.MediaPlayer; $player.Open('${escapedPath}'); $player.Play(); Start-Sleep -s 3; $player.Stop()\"`);\n                break;\n            case 'darwin':\n                exec(`afplay \"${soundPath}\"`);\n                break;\n            case 'linux':\n                exec(`paplay \"${soundPath}\"`, (error) => {\n                    if (error) {\n                        exec(`play \"${soundPath}\"`);\n                    }\n                });\n                break;\n        }\n    } catch (error) {\n        log.error('播放启动问候语失败:', error);\n    }\n}\n\n// 设置任务栏缩略图工具栏\nexport function setThumbarButtons(mainWindow, isPlaying = false) {\n    const buttons = [\n        {\n            tooltip: t('prev-track'),\n            icon: getIconPath('prev.png'),\n            click: () => {\n                mainWindow.webContents.send('play-previous-track');\n                setThumbarButtons(mainWindow, true);\n            }\n        },\n        {\n            tooltip: t('pause'),\n            icon: getIconPath('pause.png'),\n            click: () => {\n                mainWindow.webContents.send('toggle-play-pause');\n                setThumbarButtons(mainWindow, false);\n            }\n        },\n        {\n            tooltip: t('next-track'),\n            icon: getIconPath('next.png'),\n            click: () => {\n                mainWindow.webContents.send('play-next-track');\n                setThumbarButtons(mainWindow, true);\n            }\n        }\n    ];\n\n    if (!isPlaying) {\n        buttons[1] = {\n            tooltip: t('play'),\n            icon: getIconPath('play.png'),\n            click: () => {\n                mainWindow.webContents.send('toggle-play-pause');\n                setThumbarButtons(mainWindow, true);\n            }\n        };\n    }\n\n    mainWindow.setThumbarButtons(buttons);\n}\n\n// 处理自定义协议相关\nlet hash = \"\";\nlet listid = \"\";\nlet protocolMainWindow = null;\n\n// 注册自定义协议\nexport function registerProtocolHandler(mainWindow) {\n    const PROTOCOL = \"moekoe\";\n\n    // 保存mainWindow引用\n    if (mainWindow) {\n        protocolMainWindow = mainWindow;\n    }\n\n    // 注册协议\n    app.setAsDefaultProtocolClient(PROTOCOL, process.execPath);\n\n    // 处理启动参数\n    handleArgv(process.argv);\n\n    // 处理第二个实例的启动参数\n    app.on('second-instance', (event, commandLine) => {\n        if (protocolMainWindow) {\n            if (protocolMainWindow.isMinimized()) protocolMainWindow.restore();\n            protocolMainWindow.show();\n            protocolMainWindow.focus();\n            handleArgv(commandLine);\n        }\n    });\n\n    // 在macOS平台特别处理open-url事件\n    if (process.platform === 'darwin') {\n        app.on('open-url', (event, urlStr) => {\n            event.preventDefault();\n            handleUrl(urlStr);\n        });\n    }\n\n    return {\n        getHash: () => hash,\n        handleProtocolArgv: handleArgv\n    };\n}\n\n// 处理命令行参数\nfunction handleArgv(argv) {\n    const PROTOCOL = \"moekoe\";\n    const prefix = `${PROTOCOL}:`;\n    const url = argv.find(arg => arg.startsWith(prefix));\n    if (url) handleUrl(url);\n}\n\n// 处理URL\nfunction handleUrl(url) {\n    const urlObj = new URL(url);\n\n    // 提取所有参数并更新全局变量\n    hash = urlObj.searchParams.get(\"hash\") || \"\";\n    listid = urlObj.searchParams.get(\"listid\") || \"\";\n\n    // 根据路径和参数决定发送什么数据到渲染进程\n    if (protocolMainWindow && protocolMainWindow.webContents) {\n        // 将所有参数打包发送\n        protocolMainWindow.webContents.send('url-params', {\n            hash,\n            listid,\n            urlPath: urlObj.pathname.substring(1) // 去掉前导斜杠\n        });\n    }\n}\n\n// 如果有从URL启动的hash参数，在页面加载完成后发送\nexport function sendHashAfterLoad(mainWindow) {\n    if (mainWindow) {\n        protocolMainWindow = mainWindow;\n    }\n\n    if ((hash || listid) && protocolMainWindow) {\n        protocolMainWindow.webContents.on('did-finish-load', () => {\n            setTimeout(() => {\n                protocolMainWindow.webContents.send('url-params', {\n                    hash,\n                    listid,\n                    urlPath: 'share'\n                });\n            }, 1000);\n        });\n    }\n}\n"
  },
  {
    "path": "electron/extensions/extensionIPC.js",
    "content": "import { ipcMain, shell, BrowserWindow, dialog } from 'electron';\nimport path from 'path';\nimport fs from 'fs';\nimport log from 'electron-log';\nimport { fileURLToPath } from 'url';\nimport extensionManager from './extensionManager.js';\nimport { bindExternalLinkHandler } from '../services/externalLinkHandler.js';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// 获取插件图标数据\nfunction getExtensionIconData(extension, extensionPath) {\n    if (extension.manifest?.icons) {\n        const icons = extension.manifest.icons;\n        const iconSizes = Object.keys(icons);\n        if (iconSizes.length > 0) {\n            const iconSize = iconSizes[0];\n            const iconPath = icons[iconSize];\n            const fullIconPath = path.join(extensionPath, iconPath);\n            \n            try {\n                if (fs.existsSync(fullIconPath)) {\n                    const iconData = fs.readFileSync(fullIconPath);\n                    const ext = path.extname(iconPath).toLowerCase();\n                    let mimeType = 'image/png';\n                    if (ext === '.jpg' || ext === '.jpeg') {\n                        mimeType = 'image/jpeg';\n                    }\n                    return `data:${mimeType};base64,${iconData.toString('base64')}`;\n                }\n            } catch (error) {\n                log.error('读取插件图标失败:', error);\n            }\n        }\n    }\n    return null;\n}\n\n/**\n * 注册插件相关的 IPC 处理程序\n */\nexport function registerExtensionIPC() {\n    // 获取插件列表\n    ipcMain.handle('get-extensions', () => {\n        try {\n            const loadedExtensions = extensionManager.getLoadedExtensions();\n            const scannedExtensions = extensionManager.scanExtensions();\n            \n            const extensions = loadedExtensions.map(ext => {\n                const scannedExt = scannedExtensions.find(scanned => scanned.name === ext.name);\n                let iconData = null;\n                const manifestAuthor = ext.manifest?.author ?? scannedExt?.manifest?.author;\n                const authorName = typeof manifestAuthor === 'string'\n                    ? manifestAuthor\n                    : (manifestAuthor?.name || '');\n                const authorUrl = typeof manifestAuthor === 'object'\n                    ? (manifestAuthor?.url || '')\n                    : '';\n                \n                if (scannedExt?.path) {\n                    iconData = getExtensionIconData(ext, scannedExt.path);\n                }\n                \n                return {\n                    id: ext.id,\n                    pluginId: ext.manifest?.plugin_id || scannedExt?.manifest?.plugin_id || '',\n                    name: ext.name,\n                    directory: scannedExt?.directory || '',\n                    version: ext.version,\n                    enabled: true,\n                    description: ext.manifest?.description || '',\n                    author: authorName,\n                    authorUrl: authorUrl,\n                    permissions: ext.manifest?.permissions || [],\n                    iconData: iconData,\n                    moeKoeAdapted: ext.manifest?.moekoe === true || scannedExt?.manifest?.moekoe === true,\n                    minversion: ext.manifest?.minversion || scannedExt?.manifest?.minversion || ''\n                };\n            });\n            \n            return {\n                success: true,\n                extensions: extensions\n            };\n        } catch (error) {\n            log.error('获取插件列表失败:', error);\n            return {\n                success: false,\n                error: error.message,\n                extensions: []\n            };\n        }\n    });\n\n    // 获取详细插件信息\n    ipcMain.handle('get-extensions-detailed', () => {\n        try {\n            return extensionManager.scanExtensions();\n        } catch (error) {\n            log.error('获取详细插件信息失败:', error);\n            return [];\n        }\n    });\n\n    // 重新加载插件\n    ipcMain.handle('reload-extensions', () => {\n        try {\n            const result = extensionManager.reloadExtensions();\n            return result;\n        } catch (error) {\n            log.error('重新加载插件失败:', error);\n            return { success: false, message: error.message };\n        }\n    });\n\n    // 打开插件目录\n    ipcMain.handle('open-extensions-dir', () => {\n        try {\n            const extensionsDir = extensionManager.getExtensionsDirectory();\n            shell.openPath(extensionsDir);\n            return { success: true, path: extensionsDir };\n        } catch (error) {\n            log.error('打开插件目录失败:', error);\n            return { success: false, message: error.message };\n        }\n    });\n\n    // 打开插件弹窗\n    ipcMain.handle('open-extension-popup', (event, extensionId, extensionName) => {\n        try {\n            \n            // 创建新的弹窗窗口\n            const popupWindow = new BrowserWindow({\n                width: 400,\n                height: 600,\n                webPreferences: {\n                    preload: path.join(__dirname, '../preload.cjs'),\n                    nodeIntegration: false,\n                    contextIsolation: true,\n                    enableRemoteModule: false,\n                    sandbox: false,\n                    webSecurity: false // 允许加载插件内容\n                },\n                title: extensionName || '插件弹窗',\n                resizable: true,\n                minimizable: true,\n                maximizable: false,\n                alwaysOnTop: false,\n                show: false,\n                autoHideMenuBar: true, // 隐藏菜单栏\n                menuBarVisible: false  // 不显示菜单栏\n            });\n\n            // 完全移除菜单栏\n            popupWindow.setMenuBarVisibility(false);\n            popupWindow.removeMenu();\n\n            bindExternalLinkHandler(\n                popupWindow,\n                (url) => /^(https?:|mailto:|tel:)/i.test(url)\n            );\n\n            // 构建插件弹窗URL\n            const popupUrl = `chrome-extension://${extensionId}/popup.html`;\n            \n            popupWindow.loadURL(popupUrl).then(() => {\n                popupWindow.show();\n            }).catch((error) => {\n                log.error('加载插件弹窗失败:', error);\n                popupWindow.close();\n            });\n\n            return { success: true, extensionId };\n        } catch (error) {\n            log.error('打开插件弹窗失败:', error);\n            return { success: false, message: error.message };\n        }\n    });\n\n    // 安装插件\n    ipcMain.handle('install-extension', async (event, extensionPath) => {\n        try {\n            const result = await extensionManager.installExtension(extensionPath);\n            return result;\n        } catch (error) {\n            log.error('手动安装插件失败:', error);\n            return { success: false, message: error.message };\n        }\n    });\n\n    // 卸载插件\n    ipcMain.handle('uninstall-extension', (event, extensionId, extensionDir) => {\n        try {\n            const result = extensionManager.uninstallExtension(extensionId, extensionDir);\n            return result;\n        } catch (error) {\n            log.error('卸载插件失败:', error);\n            return { success: false, message: error.message };\n        }\n    });\n\n    // 验证插件清单\n    ipcMain.handle('validate-extension', async (event, extensionPath) => {\n        try {\n            const manifestPath = path.join(extensionPath, 'manifest.json');\n            const validation = extensionManager.validateManifest(manifestPath);\n            return validation;\n        } catch (error) {\n            log.error('验证插件失败:', error);\n            return { valid: false, error: error.message };\n        }\n    });\n\n    // 获取插件目录路径\n    ipcMain.handle('get-extensions-directory', () => {\n        try {\n            return {\n                success: true,\n                path: extensionManager.getExtensionsDirectory()\n            };\n        } catch (error) {\n            log.error('获取插件目录路径失败:', error);\n            return { success: false, message: error.message };\n        }\n    });\n\n    // 从zip安装插件\n    ipcMain.handle('install-plugin-from-zip', async (event, zipPath) => {\n        try {\n            const result = await extensionManager.installPluginFromZip(zipPath);\n            return result;\n        } catch (error) {\n            log.error('安装插件失败:', error);\n            return { success: false, message: error.message };\n        }\n    });\n    \n    // 从URL安装插件\n    ipcMain.handle('install-plugin-from-url', async (event, payload = {}) => {\n        try {\n            const result = await extensionManager.installPluginFromUrl(\n                payload.downloadUrl,\n                payload.extensionId,\n                payload.extensionDir\n            );\n            return result;\n        } catch (error) {\n            log.error('Failed to install remote plugin:', error);\n            return { success: false, message: error.message };\n        }\n    });\n    \n    // 显示文件选择对话框\n    ipcMain.handle('show-open-dialog', async (event, options) => {\n        try {\n            const result = await dialog.showOpenDialog({\n                ...options,\n                properties: [...(options.properties || [])],\n                filters: [...(options.filters || [])]\n            });\n\n            if (!result.canceled && result.filePaths.length > 0) {\n                return { success: true, filePath: result.filePaths[0] };\n            }\n            return { success: false, message: '未选择文件' };\n        } catch (error) {\n            log.error('打开文件对话框失败:', error);\n            return { success: false, message: error.message };\n        }\n    });\n\n    // 确保插件目录存在\n    ipcMain.handle('ensure-extensions-directory', () => {\n        try {\n            const path = extensionManager.ensureExtensionsDirectory();\n            return { success: true, path };\n        } catch (error) {\n            log.error('创建插件目录失败:', error);\n            return { success: false, message: error.message };\n        }\n    });\n\n    log.info('插件 IPC 处理程序已注册');\n}\n\n/**\n * 注销插件相关的 IPC 处理程序\n */\nexport function unregisterExtensionIPC() {\n    const channels = [\n        'get-extensions',\n        'get-extensions-detailed',\n        'reload-extensions',\n        'open-extensions-dir',\n        'open-extension-popup',\n        'install-extension',\n        'uninstall-extension',\n        'validate-extension',\n        'get-extensions-directory',\n        'ensure-extensions-directory',\n        'install-plugin-from-zip',\n        'install-plugin-from-url',\n        'show-open-dialog'\n    ];\n\n    channels.forEach(channel => {\n        ipcMain.removeHandler(channel);\n    });\n\n    log.info('插件 IPC 处理程序已注销');\n}\n\nexport default {\n    registerExtensionIPC,\n    unregisterExtensionIPC\n};\n"
  },
  {
    "path": "electron/extensions/extensionManager.js",
    "content": "import { session, app } from 'electron';\nimport path from 'path';\nimport fs from 'fs';\nimport log from 'electron-log';\nimport { fileURLToPath } from 'url';\nimport isDev from 'electron-is-dev';\nimport AdmZip from 'adm-zip';\n\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\n// Chrome 插件管理 - 根据环境选择正确的路径\nconst EXTENSIONS_DIR = !isDev\n    ? path.join(app.getPath('userData'), 'extensions')\n    : path.join(__dirname, '../../plugins/extensions');\n\n/**\n * 加载 Chrome 插件\n */\nexport function loadChromeExtensions() {\n    if (!fs.existsSync(EXTENSIONS_DIR)) {\n        fs.mkdirSync(EXTENSIONS_DIR, { recursive: true });\n        log.info('创建插件目录:', EXTENSIONS_DIR);\n    }\n\n    try {\n        const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })\n            .filter(dirent => dirent.isDirectory())\n            .map(dirent => dirent.name);\n\n        for (const extensionDir of extensionDirs) {\n            const extensionPath = path.join(EXTENSIONS_DIR, extensionDir);\n            const manifestPath = path.join(extensionPath, 'manifest.json');\n            \n            if (fs.existsSync(manifestPath)) {\n                try {\n                    const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n                    \n                    // 验证 manifest 格式\n                    if (manifest.manifest_version && manifest.name && manifest.version) {\n                        session.defaultSession.loadExtension(extensionPath, {\n                            allowFileAccess: true\n                        }).then((extension) => {\n                            log.info(`成功加载插件: ${manifest.name} (${extension.id})`);\n                        }).catch((error) => {\n                            log.error(`加载插件失败 ${extensionDir}:`, error);\n                        });\n                    } else {\n                        log.warn(`插件 ${extensionDir} 的 manifest.json 格式不正确`);\n                    }\n                } catch (error) {\n                    log.error(`解析插件 ${extensionDir} 的 manifest.json 失败:`, error);\n                }\n            } else {\n                log.warn(`插件目录 ${extensionDir} 缺少 manifest.json 文件`);\n            }\n        }\n    } catch (error) {\n        log.error('扫描插件目录失败:', error);\n    }\n}\n\n/**\n * 卸载所有插件\n */\nexport function unloadChromeExtensions() {\n    try {\n        const extensions = session.defaultSession.getAllExtensions();\n        extensions.forEach(extension => {\n            session.defaultSession.removeExtension(extension.id);\n            log.info(`卸载插件: ${extension.name}`);\n        });\n    } catch (error) {\n        log.error('卸载插件失败:', error);\n    }\n}\n\n/**\n * 获取已加载的插件列表\n */\nexport function getLoadedExtensions() {\n    try {\n        return session.defaultSession.getAllExtensions();\n    } catch (error) {\n        log.error('获取插件列表失败:', error);\n        return [];\n    }\n}\n\n/**\n * 安装单个插件\n * @param {string} extensionPath 插件路径\n */\nexport async function installExtension(extensionPath) {\n    try {\n        const extension = await session.defaultSession.loadExtension(extensionPath, {\n            allowFileAccess: true\n        });\n        log.info(`手动安装插件成功: ${extension.name}`);\n        return { success: true, extension: { id: extension.id, name: extension.name } };\n    } catch (error) {\n        log.error('手动安装插件失败:', error);\n        return { success: false, message: error.message };\n    }\n}\n\n/**\n * 卸载单个插件\n * @param {string} extensionId 插件ID\n */\nexport function uninstallExtension(extensionId, extensionDir = '') {\n    try {\n        let removedFromSession = false;\n        let removedFiles = false;\n        let targetDirPath = '';\n\n        targetDirPath = path.join(EXTENSIONS_DIR, path.basename(extensionDir.trim()));\n        try {\n            session.defaultSession.removeExtension(extensionId);\n            removedFromSession = true;\n            log.info(`卸载插件会话: ${extensionId}`);\n        } catch (error) {\n            log.warn(`卸载插件会话失败 ${extensionId}:`, error);\n        }\n\n        if (targetDirPath && fs.existsSync(targetDirPath)) {\n            fs.rmSync(targetDirPath, { recursive: true, force: true });\n            removedFiles = true;\n            log.info(`删除插件目录: ${targetDirPath}`);\n        }\n\n        if (!removedFromSession && !removedFiles) {\n            return { success: false, message: '未找到可卸载的插件会话或目录' };\n        }\n\n        return {\n            success: true,\n            removedFromSession,\n            removedFiles,\n            path: targetDirPath || ''\n        };\n    } catch (error) {\n        log.error('卸载插件失败:', error);\n        return { success: false, message: error.message };\n    }\n}\n\n/**\n * 重新加载所有插件\n */\nexport function reloadExtensions() {\n    try {\n        unloadChromeExtensions();\n        loadChromeExtensions();\n        return { success: true, message: '插件重新加载成功' };\n    } catch (error) {\n        log.error('重新加载插件失败:', error);\n        return { success: false, message: error.message };\n    }\n}\n\n/**\n * 获取插件目录路径\n */\nexport function getExtensionsDirectory() {\n    return EXTENSIONS_DIR;\n}\n\n/**\n * 检查插件目录是否存在\n */\nexport function ensureExtensionsDirectory() {\n    if (!fs.existsSync(EXTENSIONS_DIR)) {\n        fs.mkdirSync(EXTENSIONS_DIR, { recursive: true });\n        log.info('创建插件目录:', EXTENSIONS_DIR);\n    }\n    return EXTENSIONS_DIR;\n}\n\n/**\n * 验证插件清单文件\n * @param {string} manifestPath manifest.json 文件路径\n */\nexport function validateManifest(manifestPath) {\n    try {\n        if (!fs.existsSync(manifestPath)) {\n            return { valid: false, error: 'manifest.json 文件不存在' };\n        }\n\n        const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));\n        \n        // 检查必需字段\n        const requiredFields = ['manifest_version', 'name', 'version'];\n        for (const field of requiredFields) {\n            if (!manifest[field]) {\n                return { valid: false, error: `缺少必需字段: ${field}` };\n            }\n        }\n\n        // 检查 manifest 版本\n        if (manifest.manifest_version !== 3) {\n            return { valid: false, error: '仅支持 Manifest V3 格式' };\n        }\n\n        return { valid: true, manifest };\n    } catch (error) {\n        return { valid: false, error: `解析 manifest.json 失败: ${error.message}` };\n    }\n}\n\n/**\n * 获取插件详细信息\n * @param {string} extensionDir 插件目录名\n */\nexport function getExtensionInfo(extensionDir) {\n    const extensionPath = path.join(EXTENSIONS_DIR, extensionDir);\n    const manifestPath = path.join(extensionPath, 'manifest.json');\n    \n    const validation = validateManifest(manifestPath);\n    if (!validation.valid) {\n        return { error: validation.error };\n    }\n\n    const manifest = validation.manifest;\n    const stats = fs.statSync(extensionPath);\n    \n    return {\n        name: manifest.name,\n        version: manifest.version,\n        description: manifest.description || '',\n        author: manifest.author || '',\n        permissions: manifest.permissions || [],\n        path: extensionPath,\n        size: getDirectorySize(extensionPath),\n        lastModified: stats.mtime,\n        manifest: manifest\n    };\n}\n\n/**\n * 获取目录大小\n * @param {string} dirPath 目录路径\n */\nfunction getDirectorySize(dirPath) {\n    let totalSize = 0;\n    \n    try {\n        const files = fs.readdirSync(dirPath);\n        \n        for (const file of files) {\n            const filePath = path.join(dirPath, file);\n            const stats = fs.statSync(filePath);\n            \n            if (stats.isDirectory()) {\n                totalSize += getDirectorySize(filePath);\n            } else {\n                totalSize += stats.size;\n            }\n        }\n    } catch (error) {\n        log.error('计算目录大小失败:', error);\n    }\n    \n    return totalSize;\n}\n\n/**\n * 从zip文件安装插件\n * @param {string} zipPath zip文件路径\n * @returns {Promise<{success: boolean, message: string}>}\n */\nexport async function installPluginFromZip(zipPath) {\n    try {\n        const zip = new AdmZip(zipPath);\n        const zipEntries = zip.getEntries();\n\n        // 确保插件目录存在\n        ensureExtensionsDirectory();\n\n        // 查找包含 manifest.json 的第一级目录\n        let pluginEntry = null;\n        let manifestEntry = null;\n        for (const entry of zipEntries) {\n            const parts = entry.entryName.split('/');\n            if (parts.length === 2 && parts[1] === 'manifest.json') {\n                manifestEntry = entry;\n                pluginEntry = parts[0];\n                break;\n            }\n        }\n\n        if (!manifestEntry || !pluginEntry) {\n            return { success: false, message: '无效的插件包格式：未找到 manifest.json' };\n        }\n\n        // 验证 manifest\n        const manifestContent = zip.readAsText(manifestEntry);\n        try {\n            const manifest = JSON.parse(manifestContent);\n            // 直接验证 manifest 对象而不是文件路径\n            if (!manifest.manifest_version || !manifest.name || !manifest.version) {\n                return { success: false, message: '清单文件缺少必需字段' };\n            }\n            if (manifest.manifest_version !== 3) {\n                return { success: false, message: '仅支持 Manifest V3 格式' };\n            }\n        } catch (error) {\n            return { success: false, message: `manifest.json 解析失败: ${error.message}` };\n        }\n\n        // 获取真实的插件目录名（去除可能的 -main 后缀）\n        const pluginName = pluginEntry.replace(/-main$/, '');\n        const targetDir = path.join(EXTENSIONS_DIR, pluginName);\n        \n        // 如果目标目录已存在，先删除\n        if (fs.existsSync(targetDir)) {\n            fs.rmSync(targetDir, { recursive: true, force: true });\n        }\n\n        // 创建目标目录\n        fs.mkdirSync(targetDir, { recursive: true });\n\n        // 解压所有文件，保持目录结构\n        for (const entry of zipEntries) {\n            const entryName = entry.entryName;\n            // 检查是否属于目标插件目录\n            if (entryName.startsWith(pluginEntry + '/')) {\n                // 计算相对路径\n                const relativePath = entryName.substring(pluginEntry.length + 1);\n                if (relativePath) {  // 跳过空路径\n                    const targetPath = path.join(targetDir, relativePath);\n                    if (entry.isDirectory) {\n                        // 创建目录\n                        fs.mkdirSync(targetPath, { recursive: true });\n                    } else {\n                        // 解压文件\n                        const targetDirPath = path.dirname(targetPath);\n                        fs.mkdirSync(targetDirPath, { recursive: true });\n                        fs.writeFileSync(targetPath, entry.getData());\n                    }\n                }\n            }\n        }\n\n        // 加载新插件\n        const result = await installExtension(targetDir);\n        if (!result.success) {\n            // 如果加载失败，清理已解压的文件\n            if (fs.existsSync(targetDir)) {\n                fs.rmSync(targetDir, { recursive: true, force: true });\n            }\n            return { success: false, message: '插件加载失败：' + result.message };\n        }\n\n        return { \n            success: true, \n            message: `插件安装成功`,\n            extension: result.extension\n        };\n    } catch (error) {\n        log.error('安装插件失败:', error);\n        return { success: false, message: '安装插件失败：' + error.message };\n    }\n}\n\n/**\n * Download a zip package and install or update a plugin from a remote URL.\n * @param {string} downloadUrl\n * @param {string} extensionId\n * @param {string} extensionDir\n * @returns {Promise<{success: boolean, message: string}>}\n */\nexport async function installPluginFromUrl(downloadUrl, extensionId = '', extensionDir = '') {\n    const tempZipPath = path.join(app.getPath('temp'), `moekoe-plugin-${Date.now()}.zip`);\n\n    try {\n        if (!downloadUrl || typeof downloadUrl !== 'string') {\n            return { success: false, message: 'Invalid plugin download url' };\n        }\n\n        const response = await fetch(downloadUrl);\n        if (!response.ok) {\n            return {\n                success: false,\n                message: `Failed to download plugin package: ${response.status} ${response.statusText || ''}`.trim()\n            };\n        }\n\n        const arrayBuffer = await response.arrayBuffer();\n        const zipBuffer = Buffer.from(arrayBuffer);\n\n        if (!isZipBuffer(zipBuffer)) {\n            return { success: false, message: '下载内容不是有效的 zip 插件包' };\n        }\n\n        fs.writeFileSync(tempZipPath, zipBuffer);\n\n        if (extensionId || extensionDir) {\n            const uninstallResult = uninstallExtension(extensionId, extensionDir);\n            if (!uninstallResult.success) {\n                log.warn('Failed to remove existing plugin before update:', uninstallResult.message);\n            }\n        }\n\n        return await installPluginFromZip(tempZipPath);\n    } catch (error) {\n        log.error('Failed to install plugin from url:', error);\n        return { success: false, message: error.message };\n    } finally {\n        if (fs.existsSync(tempZipPath)) {\n            fs.rmSync(tempZipPath, { force: true });\n        }\n    }\n}\n\nfunction isZipBuffer(buffer) {\n    return Boolean(buffer) &&\n        buffer.length >= 4 &&\n        buffer[0] === 0x50 &&\n        buffer[1] === 0x4b &&\n        (\n            (buffer[2] === 0x03 && buffer[3] === 0x04) ||\n            (buffer[2] === 0x05 && buffer[3] === 0x06) ||\n            (buffer[2] === 0x07 && buffer[3] === 0x08)\n        );\n}\n\n/**\n * 格式化文件大小\n * @param {number} bytes 字节数\n */\nexport function formatFileSize(bytes) {\n    if (bytes === 0) return '0 B';\n    \n    const k = 1024;\n    const sizes = ['B', 'KB', 'MB', 'GB'];\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\n    \n    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n}\n\n/**\n * 扫描并获取所有插件信息\n */\nexport function scanExtensions() {\n    ensureExtensionsDirectory();\n    \n    try {\n        const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })\n            .filter(dirent => dirent.isDirectory())\n            .map(dirent => dirent.name);\n\n        const extensions = [];\n        \n        for (const extensionDir of extensionDirs) {\n            const info = getExtensionInfo(extensionDir);\n            if (!info.error) {\n                extensions.push({\n                    ...info,\n                    directory: extensionDir,\n                    installed: isExtensionInstalled(info.name)\n                });\n            } else {\n                log.warn(`插件 ${extensionDir} 信息获取失败:`, info.error);\n            }\n        }\n        \n        return extensions;\n    } catch (error) {\n        log.error('扫描插件失败:', error);\n        return [];\n    }\n}\n\n/**\n * 检查插件是否已安装\n * @param {string} extensionName 插件名称\n */\nfunction isExtensionInstalled(extensionName) {\n    try {\n        const loadedExtensions = getLoadedExtensions();\n        return loadedExtensions.some(ext => ext.name === extensionName);\n    } catch (error) {\n        return false;\n    }\n}\n\n// 默认导出所有功能\nexport default {\n    loadChromeExtensions,\n    unloadChromeExtensions,\n    getLoadedExtensions,\n    installExtension,\n    uninstallExtension,\n    reloadExtensions,\n    getExtensionsDirectory,\n    ensureExtensionsDirectory,\n    validateManifest,\n    getExtensionInfo,\n    formatFileSize,\n    scanExtensions,\n    installPluginFromZip,\n    installPluginFromUrl\n};\n"
  },
  {
    "path": "electron/extensions/extensions.js",
    "content": "// 插件系统统一入口文件\nimport extensionManager from './extensionManager.js';\nimport { registerExtensionIPC, unregisterExtensionIPC } from './extensionIPC.js';\nimport log from 'electron-log';\n\n/**\n * 初始化插件系统\n */\nexport function initializeExtensions() {\n    try {\n        // 确保插件目录存在\n        extensionManager.ensureExtensionsDirectory();\n        \n        // 注册 IPC 处理程序\n        registerExtensionIPC();\n        \n        // 加载插件\n        extensionManager.loadChromeExtensions();\n        \n        return { success: true };\n    } catch (error) {\n        log.error('插件系统初始化失败:', error);\n        return { success: false, error: error.message };\n    }\n}\n\n/**\n * 清理插件系统\n */\nexport function cleanupExtensions() {\n    try {\n        // 卸载所有插件\n        extensionManager.unloadChromeExtensions();\n        \n        // 注销 IPC 处理程序\n        unregisterExtensionIPC();\n        \n        return { success: true };\n    } catch (error) {\n        log.error('插件系统清理失败:', error);\n        return { success: false, error: error.message };\n    }\n}\n\n/**\n * 重启插件系统\n */\nexport function restartExtensions() {\n    try {\n        const cleanupResult = cleanupExtensions();\n        if (!cleanupResult.success) {\n            return cleanupResult;\n        }\n        \n        const initResult = initializeExtensions();\n        return initResult;\n    } catch (error) {\n        log.error('重启插件系统失败:', error);\n        return { success: false, error: error.message };\n    }\n}\n\nexport {\n    extensionManager,\n    registerExtensionIPC,\n    unregisterExtensionIPC\n};\n\nexport default {\n    initializeExtensions,\n    cleanupExtensions,\n    restartExtensions,\n    extensionManager,\n    registerExtensionIPC,\n    unregisterExtensionIPC\n};"
  },
  {
    "path": "electron/language/i18n.js",
    "content": "import Store from 'electron-store';\nimport { app } from 'electron';\n\nconst store = new Store();\n\nconst translations = {\n  'zh-CN': {\n    'project-home': '项目主页',\n    'report-bug': '反馈bug',\n    'prev-track': '上一首',\n    'pause': '暂停',\n    'play': '播放',\n    'next-track': '下一首',\n    'check-updates': '检查更新',\n    'restart-app': '重启应用',\n    'show-hide': '显示/隐藏',\n    'quit': '退出程序',\n    'no-lyrics': '暂无歌词',\n    'desktop-lyrics-closed': '桌面歌词已关闭',\n    'this-time-only': '仅本次生效',\n    'now-playing': '正在播放：',\n    'error': '错误',\n    'init-error': '初始化应用时发生错误。',\n    'api-error': 'API 服务启动失败，请检查！',\n    'ok': '确定',\n    'shortcut-failed': '快捷键注册失败，请重新尝试',\n    'hint': '提示',\n    'update-timeout': '更新服务器连接超时，请检查网络',\n    'update-failed': '更新检查失败，请重试',\n    'new-version': '发现新版本',\n    'new-version-msg': '发现新版本 {version}\\n\\n{notes}',\n    'no-release-notes': '暂无更新说明',\n    'update-now': '立即更新',\n    'later': '稍后提醒',\n    'update-hint': '更新提示',\n    'already-latest': '当前已是最新版本',\n    'update-ready': '更新就绪',\n    'update-ready-msg': '新版本已下载完成，立即安装？',\n    'install-now': '现在安装',\n    'install-later': '稍后安装',\n    'non-windows-update': '非 Windows 平台暂不支持在线更新，请前往官网或应用市场下载最新版本。'\n  },\n  'zh-TW': {\n    'project-home': '專案首頁',\n    'report-bug': '回報問題',\n    'prev-track': '上一首',\n    'pause': '暫停',\n    'play': '播放',\n    'next-track': '下一首',\n    'check-updates': '檢查更新',\n    'restart-app': '重新啟動',\n    'show-hide': '顯示/隱藏',\n    'quit': '結束程式',\n    'no-lyrics': '暫無歌詞',\n    'desktop-lyrics-closed': '桌面歌詞已關閉',\n    'this-time-only': '僅本次生效',\n    'now-playing': '正在播放：',\n    'error': '錯誤',\n    'init-error': '初始化應用時發生錯誤。',\n    'api-error': 'API 服務啟動失敗，請檢查！',\n    'ok': '確定',\n    'shortcut-failed': '快捷鍵註冊失敗，請重新嘗試',\n    'hint': '提示',\n    'update-timeout': '更新伺服器連接逾時，請檢查網絡',\n    'update-failed': '更新檢查失敗，請重試',\n    'new-version': '發現新版本',\n    'new-version-msg': '發現新版本 {version}\\n\\n{notes}',\n    'no-release-notes': '暫無更新說明',\n    'update-now': '立即更新',\n    'later': '稍後提醒',\n    'update-hint': '更新提示',\n    'already-latest': '當前已是最新版本',\n    'update-ready': '更新就緒',\n    'update-ready-msg': '新版本已下載完成，立即安裝？',\n    'install-now': '現在安裝',\n    'install-later': '稍後安裝',\n    'non-windows-update': '非 Windows 平台暫不支持線上更新，請前往官網或應用市場下載最新版本。'\n  },\n  'en': {\n    'project-home': 'Project Homepage',\n    'report-bug': 'Report Bug',\n    'prev-track': 'Previous',\n    'pause': 'Pause',\n    'play': 'Play',\n    'next-track': 'Next',\n    'check-updates': 'Check Updates',\n    'restart-app': 'Restart App',\n    'show-hide': 'Show/Hide',\n    'quit': 'Quit',\n    'no-lyrics': 'No Lyrics',\n    'desktop-lyrics-closed': 'Desktop Lyrics Closed',\n    'this-time-only': 'This session only',\n    'now-playing': 'Now Playing: ',\n    'error': 'Error',\n    'init-error': 'Error initializing app.',\n    'api-error': 'API service failed to start!',\n    'ok': 'OK',\n    'shortcut-failed': 'Shortcut registration failed',\n    'hint': 'Notice',\n    'update-timeout': 'Update server connection timeout',\n    'update-failed': 'Update check failed',\n    'new-version': 'New Version Available',\n    'new-version-msg': 'New version {version} available\\n\\n{notes}',\n    'no-release-notes': 'No release notes',\n    'update-now': 'Update Now',\n    'later': 'Later',\n    'update-hint': 'Update',\n    'already-latest': 'Already up to date',\n    'update-ready': 'Update Ready',\n    'update-ready-msg': 'Update downloaded. Install now?',\n    'install-now': 'Install Now',\n    'install-later': 'Later',\n    'non-windows-update': 'Auto-update is only available on Windows. Please download from the official website.'\n  },\n  'ru': {\n    'project-home': 'Страница проекта',\n    'report-bug': 'Сообщить об ошибке',\n    'prev-track': 'Предыдущий',\n    'pause': 'Пауза',\n    'play': 'Воспроизвести',\n    'next-track': 'Следующий',\n    'check-updates': 'Проверить обновления',\n    'restart-app': 'Перезапустить',\n    'show-hide': 'Показать/Скрыть',\n    'quit': 'Выход',\n    'no-lyrics': 'Нет текста',\n    'desktop-lyrics-closed': 'Текст на рабочем столе скрыт',\n    'this-time-only': 'Только для этого сеанса',\n    'now-playing': 'Сейчас играет: ',\n    'error': 'Ошибка',\n    'init-error': 'Ошибка инициализации приложения.',\n    'api-error': 'Не удалось запустить API!',\n    'ok': 'ОК',\n    'shortcut-failed': 'Не удалось зарегистрировать горячие клавиши',\n    'hint': 'Уведомление',\n    'update-timeout': 'Таймаут подключения к серверу обновлений',\n    'update-failed': 'Ошибка проверки обновлений',\n    'new-version': 'Доступна новая версия',\n    'new-version-msg': 'Доступна версия {version}\\n\\n{notes}',\n    'no-release-notes': 'Нет описания изменений',\n    'update-now': 'Обновить',\n    'later': 'Позже',\n    'update-hint': 'Обновление',\n    'already-latest': 'Установлена последняя версия',\n    'update-ready': 'Обновление готово',\n    'update-ready-msg': 'Обновление загружено. Установить сейчас?',\n    'install-now': 'Установить',\n    'install-later': 'Позже',\n    'non-windows-update': 'Автообновление доступно только на Windows. Скачайте с официального сайта.'\n  },\n  'ja': {\n    'project-home': 'プロジェクトホーム',\n    'report-bug': 'バグ報告',\n    'prev-track': '前の曲',\n    'pause': '一時停止',\n    'play': '再生',\n    'next-track': '次の曲',\n    'check-updates': '更新を確認',\n    'restart-app': '再起動',\n    'show-hide': '表示/非表示',\n    'quit': '終了',\n    'no-lyrics': '歌詞なし',\n    'desktop-lyrics-closed': 'デスクトップ歌詞を閉じました',\n    'this-time-only': '今回のみ有効',\n    'now-playing': '再生中：',\n    'error': 'エラー',\n    'init-error': 'アプリの初期化エラー。',\n    'api-error': 'APIサービスの起動に失敗しました！',\n    'ok': 'OK',\n    'shortcut-failed': 'ショートカット登録に失敗しました',\n    'hint': '通知',\n    'update-timeout': '更新サーバー接続タイムアウト',\n    'update-failed': '更新確認に失敗しました',\n    'new-version': '新しいバージョンが利用可能',\n    'new-version-msg': 'バージョン {version} が利用可能です\\n\\n{notes}',\n    'no-release-notes': 'リリースノートなし',\n    'update-now': '今すぐ更新',\n    'later': '後で',\n    'update-hint': '更新',\n    'already-latest': '最新バージョンです',\n    'update-ready': '更新準備完了',\n    'update-ready-msg': '更新がダウンロードされました。今すぐインストールしますか？',\n    'install-now': '今すぐインストール',\n    'install-later': '後で',\n    'non-windows-update': '自動更新はWindowsでのみ利用可能です。公式サイトからダウンロードしてください。'\n  },\n  'ko': {\n    'project-home': '프로젝트 홈',\n    'report-bug': '버그 신고',\n    'prev-track': '이전',\n    'pause': '일시정지',\n    'play': '재생',\n    'next-track': '다음',\n    'check-updates': '업데이트 확인',\n    'restart-app': '재시작',\n    'show-hide': '표시/숨기기',\n    'quit': '종료',\n    'no-lyrics': '가사 없음',\n    'desktop-lyrics-closed': '바탕화면 가사 닫힘',\n    'this-time-only': '이번 세션만',\n    'now-playing': '재생 중: ',\n    'error': '오류',\n    'init-error': '앱 초기화 오류.',\n    'api-error': 'API 서비스 시작 실패!',\n    'ok': '확인',\n    'shortcut-failed': '단축키 등록 실패',\n    'hint': '알림',\n    'update-timeout': '업데이트 서버 연결 시간 초과',\n    'update-failed': '업데이트 확인 실패',\n    'new-version': '새 버전 사용 가능',\n    'new-version-msg': '버전 {version} 사용 가능\\n\\n{notes}',\n    'no-release-notes': '릴리스 노트 없음',\n    'update-now': '지금 업데이트',\n    'later': '나중에',\n    'update-hint': '업데이트',\n    'already-latest': '최신 버전입니다',\n    'update-ready': '업데이트 준비 완료',\n    'update-ready-msg': '업데이트가 다운로드되었습니다. 지금 설치하시겠습니까?',\n    'install-now': '지금 설치',\n    'install-later': '나중에',\n    'non-windows-update': '자동 업데이트는 Windows에서만 사용 가능합니다. 공식 웹사이트에서 다운로드하세요.'\n  }\n};\n\nfunction getLocale() {\n  // First check user settings\n  const settings = store.get('settings');\n  if (settings?.language) {\n    return settings.language;\n  }\n  // Otherwise use system language\n  const systemLang = app.getLocale();\n  if (systemLang.startsWith('zh')) {\n    return systemLang === 'zh-TW' || systemLang === 'zh-HK' ? 'zh-TW' : 'zh-CN';\n  }\n  if (systemLang.startsWith('ru')) return 'ru';\n  if (systemLang.startsWith('ja')) return 'ja';\n  if (systemLang.startsWith('ko')) return 'ko';\n  if (systemLang.startsWith('en')) return 'en';\n  return 'zh-CN'; // fallback\n}\n\nexport function t(key) {\n  const locale = getLocale();\n  return translations[locale]?.[key] || translations['zh-CN']?.[key] || key;\n}\n\nexport default { t };\n"
  },
  {
    "path": "electron/main.js",
    "content": "import { app, ipcMain, globalShortcut, dialog, Notification, shell, session, powerSaveBlocker, nativeImage } from 'electron';\nimport {\n    createWindow, createTray, createTouchBar, startApiServer,\n    stopApiServer, registerShortcut,\n    playStartupSound, createLyricsWindow, setThumbarButtons,\n    registerProtocolHandler, sendHashAfterLoad, getTray, createMvWindow\n} from './appServices.js';\nimport { initializeExtensions, cleanupExtensions } from './extensions/extensions.js';\nimport { setupAutoUpdater } from './services/updater.js';\nimport apiService from './services/apiService.js';\nimport statusBarLyricsService from './services/statusBarLyricsService.js';\nimport Store from 'electron-store';\nimport path from 'path';\nimport { fileURLToPath } from 'url';\nimport { t } from './language/i18n.js';\n\nlet mainWindow = null;\nlet blockerId = null;\nconst store = new Store();\nconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\nconst gotTheLock = app.requestSingleInstanceLock();\nif (!gotTheLock) {\n    app.quit();\n    process.exit(0);\n} else {\n    let protocolHandler;\n    app.on('second-instance', (event, commandLine) => {\n        if (!protocolHandler) {\n            protocolHandler = registerProtocolHandler(null);\n        }\n        if (mainWindow) {\n            if (mainWindow.isMinimized()) mainWindow.restore();\n            mainWindow.show();\n            mainWindow.focus();\n        }\n        protocolHandler.handleProtocolArgv(commandLine);\n    });\n}\n\napp.on('ready', () => {\n    startApiServer().then(() => {\n        try {\n            mainWindow = createWindow();\n            createTray(mainWindow);\n\n            // 初始化状态栏歌词服务\n            statusBarLyricsService.init(mainWindow, store, getTray, createTray);\n\n            if (process.platform === \"darwin\" && store.get('settings')?.touchBar == 'on') createTouchBar(mainWindow);\n            playStartupSound();\n            registerShortcut();\n            setupAutoUpdater(mainWindow);\n            apiService.init(mainWindow);\n            registerProtocolHandler(mainWindow);\n            sendHashAfterLoad(mainWindow);\n            initializeExtensions();\n        } catch (error) {\n            console.log('初始化应用时发生错误:', error);\n            createTray(null);\n            dialog.showMessageBox({\n                type: 'error',\n                title: t('error'),\n                message: t('init-error'),\n                buttons: [t('ok')]\n            }).then(result => {\n                if (result.response === 0) {\n                    app.isQuitting = true;\n                    app.quit();\n                }\n            });\n        }\n    }).catch((error) => {\n        console.log('API 服务启动失败:', error);\n        createTray(null);\n        dialog.showMessageBox({\n            type: 'error',\n            title: t('error'),\n            message: t('api-error'),\n            buttons: [t('ok')]\n        }).then(result => {\n            if (result.response === 0) {\n                app.isQuitting = true;\n                app.quit();\n            }\n            return;\n        });\n    });\n});\n\nconst settings = store.get('settings');\nif (settings?.gpuAcceleration === 'on') {\n    app.disableHardwareAcceleration();\n    app.commandLine.appendSwitch('enable-transparent-visuals');\n    app.commandLine.appendSwitch('disable-gpu-compositing');\n}\n\nif (settings?.preventAppSuspension === 'on') {\n    blockerId = powerSaveBlocker.start('prevent-display-sleep');\n}\n\nif (settings?.highDpi === 'on') {\n    app.commandLine.appendSwitch('high-dpi-support', '1');\n    app.commandLine.appendSwitch('force-device-scale-factor', settings?.dpiScale || '1');\n}\n\nif (settings?.apiMode === 'on') {\n    apiService.start();\n}\n\n// 即将退出\napp.on('before-quit', () => {\n    if (mainWindow && !mainWindow.isMaximized()) {\n        const windowBounds = mainWindow.getBounds();\n        store.set('windowState', windowBounds);\n    }\n    if (blockerId !== null) {\n        powerSaveBlocker.stop(blockerId);\n    }\n\n    // 清理状态栏歌词服务\n    statusBarLyricsService.cleanup();\n\n    stopApiServer();\n    apiService.stop();\n    cleanupExtensions();\n});\n// 关闭所有窗口\napp.on('window-all-closed', () => {\n    if (process.platform !== 'darwin') {\n        app.isQuitting = true;\n        app.quit(); // 非 macOS 系统上关闭所有窗口后退出应用\n    }\n});\n// 图标被点击\napp.on('activate', () => {\n    if (mainWindow && !mainWindow.isVisible()) {\n        mainWindow.show();\n    } else if (!mainWindow) {\n        mainWindow = createWindow();\n    }\n});\n\n// 处理未捕获的异常\nprocess.on('uncaughtException', (error) => {\n    console.error('Unhandled Exception:', error);\n});\n\n// 监听渲染进程发送的免责声明结果\nipcMain.on('disclaimer-response', (event, accepted) => {\n    if (accepted) {\n        store.set('disclaimerAccepted', true);\n    } else {\n        app.quit();\n    }\n});\n\nipcMain.on('window-control', (event, action) => {\n    switch (action) {\n        case 'close':\n            if (store.get('settings')?.minimizeToTray === 'off') {\n                app.isQuitting = true;\n                app.quit();\n            } else {\n                mainWindow.close();\n            }\n            break;\n        case 'minimize':\n            mainWindow.minimize();\n            break;\n        case 'maximize':\n            if (mainWindow.isMaximized()) {\n                mainWindow.unmaximize();\n                store.set('maximize', false);\n            } else {\n                mainWindow.maximize();\n                store.set('maximize', true);\n            }\n            break;\n    }\n});\n\napp.on('will-quit', () => {\n    globalShortcut.unregisterAll();\n});\nipcMain.on('save-settings', (event, settings) => {\n    store.set('settings', settings);\n    if (['on', 'off'].includes(settings?.autoStart)) {\n        app.setLoginItemSettings({\n            openAtLogin: settings?.autoStart === 'on',\n            path: app.getPath('exe'),\n        });\n    }\n});\nipcMain.on('clear-settings', (event) => {\n    store.clear();\n    session.defaultSession.clearCache();\n    session.defaultSession.clearStorageData();\n    const userDataPath = app.getPath('userData');\n    shell.openPath(userDataPath);\n});\nipcMain.on('custom-shortcut', (event) => {\n    registerShortcut();\n});\n\nipcMain.on('lyrics-data', (event, lyricsData) => {\n    const lyricsWindow = mainWindow?.lyricsWindow;\n    if (lyricsWindow) {\n        lyricsWindow.webContents.send('lyrics-data', lyricsData);\n    }\n\n    // 状态栏歌词功能服务处理（仅支持Mac系统）\n    if (process.platform === 'darwin') {\n        statusBarLyricsService.handleLyricsData(lyricsData);\n    }\n});\n\nipcMain.on('server-lyrics', (event, lyricsData) => {\n    apiService.updateLyrics(lyricsData);\n});\n\n// 监听桌面歌词操作\nipcMain.on('desktop-lyrics-action', (event, action) => {\n    switch (action) {\n        case 'previous-song':\n            mainWindow.webContents.send('play-previous-track');\n            break;\n        case 'next-song':\n            mainWindow.webContents.send('play-next-track');\n            break;\n        case 'toggle-play':\n            mainWindow.webContents.send('toggle-play-pause');\n            break;\n        case 'close-lyrics':\n            const lyricsWindow = mainWindow.lyricsWindow;\n            if (lyricsWindow) {\n                lyricsWindow.close();\n                new Notification({\n                    title: t('desktop-lyrics-closed'),\n                    body: t('this-time-only'),\n                    icon: path.join(__dirname, '../build/icons/logo.png')\n                }).show();\n                mainWindow.lyricsWindow = null;\n            }\n            break;\n        case 'display-lyrics':\n            if (!mainWindow.lyricsWindow) createLyricsWindow();\n            break;\n    }\n});\n\nipcMain.on('set-ignore-mouse-events', (event, ignore) => {\n    const lyricsWindow = mainWindow.lyricsWindow;\n    if (lyricsWindow) {\n        lyricsWindow.setIgnoreMouseEvents(ignore, { forward: true });\n    }\n});\n\nipcMain.on('window-drag', (event, { mouseX, mouseY }) => {\n    const lyricsWindow = mainWindow.lyricsWindow;\n    if (!lyricsWindow) return\n    lyricsWindow.setPosition(mouseX, mouseY)\n    store.set('lyricsWindowPosition', { x: mouseX, y: mouseY });\n})\n\nipcMain.on('play-pause-action', (event, playing, currentTime) => {\n    const lyricsWindow = mainWindow.lyricsWindow;\n    if (lyricsWindow) {\n        lyricsWindow.webContents.send('playing-status', playing);\n    }\n    apiService.updatePlayerState({ isPlaying: playing, currentTime: currentTime });\n    setThumbarButtons(mainWindow, playing);\n})\n\nipcMain.on('open-url', (event, url) => {\n    shell.openExternal(url);\n})\n\nipcMain.on('set-tray-title', (event, title) => {\n    createTray(mainWindow, t('now-playing') + title);\n    mainWindow.setTitle(title);\n})\n\n\nipcMain.handle('open-mv-window', (e, url) => {\n    return (async () => {\n        const mvWindow = createMvWindow();\n        try {\n            await mvWindow.loadURL(url);\n            mvWindow.show();\n            return true;\n        } catch (error) {\n            console.error('[open-mv-window] loadURL failed:', url, error);\n            try {\n                mvWindow.close();\n            } catch {}\n            throw error;\n        }\n    })();\n});\n"
  },
  {
    "path": "electron/preload.cjs",
    "content": "const { contextBridge, ipcRenderer } = require('electron');\n\ncontextBridge.exposeInMainWorld('electron', {\n    ipcRenderer: {\n        send: (channel, ...args) => ipcRenderer.send(channel, ...args),\n        on: (channel, listener) => ipcRenderer.on(channel, listener),\n        once: (channel, listener) => ipcRenderer.once(channel, listener),\n        removeListener: (channel, listener) => ipcRenderer.removeListener(channel, listener),\n        removeAllListeners: (channel) => ipcRenderer.removeAllListeners(channel)\n    },\n    platform: process.platform\n});\n\n// 添加插件管理 API\ncontextBridge.exposeInMainWorld('electronAPI', {\n    // 插件管理\n    getExtensions: () => ipcRenderer.invoke('get-extensions'),\n    getExtensionsDetailed: () => ipcRenderer.invoke('get-extensions-detailed'),\n    reloadExtensions: () => ipcRenderer.invoke('reload-extensions'),\n    openExtensionsDir: () => ipcRenderer.invoke('open-extensions-dir'),\n    openExtensionPopup: (extensionId, extensionName) => ipcRenderer.invoke('open-extension-popup', extensionId, extensionName),\n    installExtension: (extensionPath) => ipcRenderer.invoke('install-extension', extensionPath),\n    uninstallExtension: (extensionId, extensionDir) => ipcRenderer.invoke('uninstall-extension', extensionId, extensionDir),\n    validateExtension: (extensionPath) => ipcRenderer.invoke('validate-extension', extensionPath),\n    getExtensionsDirectory: () => ipcRenderer.invoke('get-extensions-directory'),\n    ensureExtensionsDirectory: () => ipcRenderer.invoke('ensure-extensions-directory'),\n    installPluginFromZip: (zipPath) => ipcRenderer.invoke('install-plugin-from-zip', zipPath),\n    installPluginFromUrl: (downloadUrl, extensionId = '', extensionDir = '') => ipcRenderer.invoke('install-plugin-from-url', {\n        downloadUrl,\n        extensionId,\n        extensionDir,\n    }),\n    showOpenDialog: (options) => ipcRenderer.invoke('show-open-dialog', options),\n    openMvWindow: (url) => ipcRenderer.invoke('open-mv-window', url),\n});\n"
  },
  {
    "path": "electron/services/apiService.js",
    "content": "import { WebSocketServer } from 'ws';\n\nclass ApiService {\n    constructor() {\n        this.wsServer = null;\n        this.clients = new Set();\n        this.currentLyrics = null;\n        this.isPlaying = false;\n        this.currentTime = 0;\n        this.mainWindow = null;\n    }\n    init(mainWindow) {\n        this.mainWindow = mainWindow;\n    }\n    // 启动 WebSocket 服务器\n    start() {\n        if (this.wsServer) return; \n        this.wsServer = new WebSocketServer({ port: 6520 });\n        this.wsServer.on('connection', (ws) => {\n            this.clients.add(ws);\n            //发送欢迎信息\n            ws.send(JSON.stringify({\n                type: 'welcome',\n                data: '感谢接入MoeKoe Music，文档地址：https://music.moekoe.cn/'\n            }));\n\n            // 发送当前歌词\n            if (this.currentLyrics) {\n                ws.send(JSON.stringify({\n                    type: 'lyrics',\n                    data: this.currentLyrics\n                }));\n            }\n\n            // 发送当前播放状态\n            ws.send(JSON.stringify({\n                type: 'playerState',\n                data: {\n                    isPlaying: this.isPlaying,\n                    currentTime: this.currentTime\n                }\n            }));\n\n            // 处理来自客户端的消息\n            ws.on('message', (message) => {\n                try {\n                    const data = JSON.parse(message);\n                    console.log(data);\n                    if (data.type === 'control') {\n                        this.handleControlCommand(data.data);\n                    }\n                } catch (e) {\n                    console.error('无效的 WebSocket 消息', e);\n                }\n            });\n\n            ws.on('close', () => {\n                this.clients.delete(ws);\n                console.log('WebSocket 客户端已断开连接');\n            });\n        });\n\n        console.log('WebSocket server running at ws://127.0.0.1:6520');\n    }\n\n    stop() {\n        if (this.wsServer) {\n            for (const client of this.clients) {\n                client.close();\n            }\n            this.clients.clear();\n            this.wsServer.close();\n            this.wsServer = null;\n            console.log('WebSocket 服务器已停止');\n        }\n    }\n    \n    // 广播到所有客户端\n    broadcastToClients(data) {\n        if(!this.wsServer) return;\n        const message = JSON.stringify(data);\n        for (const client of this.clients) {\n            if (client.readyState === 1) {\n                client.send(message);\n            }\n        }\n    }\n\n    handleControlCommand(data) {\n        if (!this.mainWindow) return;\n        switch (data.command) {\n            case 'toggle': // 切换播放状态\n                this.mainWindow.webContents.send('toggle-play-pause');\n                break;\n            case 'next': // 下一首\n                this.mainWindow.webContents.send('play-next-track');\n                break;\n            case 'prev': // 上一首\n                this.mainWindow.webContents.send('play-previous-track');\n                break;\n        }\n    }\n    \n    // 更新歌词数据\n    updateLyrics(lyricsData) {\n        this.currentLyrics = lyricsData;\n        this.broadcastToClients({\n            type: 'lyrics',\n            data: lyricsData\n        });\n    }\n    \n    // 更新播放状态\n    updatePlayerState(state) {\n        this.isPlaying = state.isPlaying;\n        this.currentTime = state.currentTime;\n        this.broadcastToClients({\n            type: 'playerState',\n            data: state\n        });\n    }\n}\n\nconst apiService = new ApiService();\nexport default apiService; "
  },
  {
    "path": "electron/services/externalLinkHandler.js",
    "content": "import { shell } from 'electron';\n\nexport function shouldOpenExternally(targetUrl, currentUrl = '') {\n    try {\n        const target = new URL(targetUrl);\n        if (target.protocol === 'mailto:' || target.protocol === 'tel:') {\n            return true;\n        }\n        if (target.protocol !== 'http:' && target.protocol !== 'https:') {\n            return false;\n        }\n\n        if (!currentUrl) {\n            return true;\n        }\n\n        const current = new URL(currentUrl);\n        return target.origin !== current.origin;\n    } catch {\n        return false;\n    }\n}\n\nexport function bindExternalLinkHandler(win, openExternalPredicate = shouldOpenExternally) {\n    const { webContents } = win;\n\n    webContents.setWindowOpenHandler(({ url }) => {\n        if (openExternalPredicate(url, webContents.getURL())) {\n            shell.openExternal(url);\n        }\n        return { action: 'deny' };\n    });\n\n    webContents.on('will-navigate', (event, url) => {\n        if (openExternalPredicate(url, webContents.getURL())) {\n            event.preventDefault();\n            shell.openExternal(url);\n        }\n    });\n}\n\n"
  },
  {
    "path": "electron/services/statusBarLyricsService.js",
    "content": "import { ipcMain, nativeImage } from 'electron';\n\nclass StatusBarLyricsService {\n    constructor() {\n        this.mainWindow = null;\n        this.store = null;\n        this.tray = null;\n        this.clearLyricsTimeout = null;\n        this.lastStatusBarLyric = '';\n        this.lastTrayUpdateTime = 0;\n        this.lastTrayImageHash = '';\n        this.TRAY_UPDATE_THROTTLE = 30; // 30ms 节流\n        this.getTrayCallback = null;\n        this.createTrayCallback = null;\n    }\n\n    init(mainWindow, store, getTrayCallback, createTrayCallback) {\n        this.mainWindow = mainWindow;\n        this.store = store;\n        this.getTrayCallback = getTrayCallback;\n        this.createTrayCallback = createTrayCallback;\n\n        if (process.platform === 'darwin') {\n            this.registerListeners();\n            this.initializeOnStartup();  // 自动初始化\n        }\n    }\n\n    // 判断状态栏歌词是否开启\n    isStatusBarLyricsEnabled() {\n        if (process.platform !== 'darwin') return false;\n\n        const settings = this.store.get('settings') || {};\n        return settings.statusBarLyrics === 'on';\n    }\n\n    registerListeners() {\n        // 监听渲染进程生成的图片并更新 Tray\n        ipcMain.on('update-statusbar-image', (event, dataUrl) => {\n            this.handleUpdateImage(dataUrl);\n        });\n    }\n\n    // 处理歌词数据 (供 main.js 调用)\n    handleLyricsData(lyricsData) {\n        if (!this.isStatusBarLyricsEnabled()) {\n            this.handleDisabledState();\n            return;\n        }\n\n        const currentLyric = lyricsData?.currentLyric || '';\n\n        if (currentLyric) {\n            // 有歌词：清除防抖定时器，立即更新\n            if (this.clearLyricsTimeout) {\n                clearTimeout(this.clearLyricsTimeout);\n                this.clearLyricsTimeout = null;\n            }\n\n            if (currentLyric !== this.lastStatusBarLyric) {\n                if (this.mainWindow?.webContents) {\n                    this.mainWindow.webContents.send('generate-statusbar-image', currentLyric);\n                }\n                this.lastStatusBarLyric = currentLyric;\n            }\n        } else {\n            // 无歌词 (间奏)：启动 5秒 防抖\n            if (!this.clearLyricsTimeout && this.lastStatusBarLyric !== '') {\n                this.clearLyricsTimeout = setTimeout(() => {\n                    // 再次检查设置，确保这段时间没被关闭\n                    if (this.isStatusBarLyricsEnabled()) {\n                        if (this.mainWindow?.webContents) {\n                            this.mainWindow.webContents.send('generate-statusbar-image', ''); // 发送空字符触发占位符\n                        }\n                        this.lastStatusBarLyric = '';\n                    }\n                    this.clearLyricsTimeout = null;\n                }, 5000);\n            }\n        }\n    }\n\n    // 处理功能关闭时的状态清理\n    handleDisabledState() {\n        if (this.lastStatusBarLyric !== '') {\n            if (this.clearLyricsTimeout) {\n                clearTimeout(this.clearLyricsTimeout);\n                this.clearLyricsTimeout = null;\n            }\n\n            const tray = this.getTrayCallback ? this.getTrayCallback() : null;\n            if (tray && !tray.isDestroyed()) {\n                tray.setTitle(''); // 清除文字\n                tray.setImage(nativeImage.createEmpty()); // 清除图片\n\n                // 如果需要恢复原始 Tray 图标，这里可能需要重新调用 createTray\n                if (this.createTrayCallback) {\n                    this.createTrayCallback(this.mainWindow);\n                }\n\n                this.lastStatusBarLyric = '';\n            }\n        }\n    }\n\n    // 处理图片更新 (更新 Tray)\n    handleUpdateImage(dataUrl) {\n        // 节流\n        const now = Date.now();\n        if (now - this.lastTrayUpdateTime < this.TRAY_UPDATE_THROTTLE) return;\n\n        // Tray 检查\n        const tray = this.getTrayCallback ? this.getTrayCallback() : null;\n        if (!tray || tray.isDestroyed()) return;\n\n        if (!dataUrl) return;\n\n        // 哈希去重\n        const imageHash = dataUrl.slice(-100);\n        if (imageHash === this.lastTrayImageHash) return;\n\n        this.lastTrayUpdateTime = now;\n        this.lastTrayImageHash = imageHash;\n\n        try {\n            const base64Data = dataUrl.replace(/^data:image\\/png;base64,/, \"\");\n            const buffer = Buffer.from(base64Data, 'base64');\n            const image = nativeImage.createEmpty();\n\n            // 逻辑尺寸 200x22, 实际 Buffer 400x44 (@2x)\n            image.addRepresentation({\n                scaleFactor: 2.0,\n                width: 200,\n                height: 22,\n                buffer: buffer\n            });\n\n            image.setTemplateImage(true);\n\n            if (tray && !tray.isDestroyed()) {\n                tray.setImage(image);\n                tray.setTitle(''); // 确保不显示文字\n            }\n        } catch (e) {\n            console.error('[StatusBarService] Failed to set tray image:', e);\n        }\n    }\n\n    // 应用启动时初始化状态栏歌词（私有方法）\n    initializeOnStartup() {\n        if (!this.isStatusBarLyricsEnabled()) {\n            return;\n        }\n\n        // 等待窗口准备好后触发渲染\n        this.mainWindow.webContents.once('did-finish-load', () => {\n            setTimeout(() => {\n                if (this.mainWindow?.webContents) {\n                    console.log('[StatusBarLyricsService] 启动时主动触发状态栏歌词渲染');\n                    this.mainWindow.webContents.send('generate-statusbar-image', '');\n                }\n            }, 1000);\n        });\n    }\n\n    // 清理资源（应用退出时调用）\n    cleanup() {\n        // 清理定时器\n        if (this.clearLyricsTimeout) {\n            clearTimeout(this.clearLyricsTimeout);\n            this.clearLyricsTimeout = null;\n        }\n\n        // 清理 Tray（防止退出后闪烁）\n        const tray = this.getTrayCallback ? this.getTrayCallback() : null;\n        if (tray && !tray.isDestroyed()) {\n            try {\n                tray.setImage(nativeImage.createEmpty());\n                tray.setTitle('');\n                tray.destroy();\n            } catch (e) {\n                console.error('[StatusBarLyricsService] Error cleaning up tray:', e);\n            }\n        }\n    }\n}\n\nexport default new StatusBarLyricsService();\n"
  },
  {
    "path": "electron/services/updater.js",
    "content": "import { app, dialog } from 'electron';\nimport electronUpdater from 'electron-updater';\nconst { autoUpdater } = electronUpdater;\nimport Store from 'electron-store';\nimport { t } from '../language/i18n.js';\n\nconst store = new Store();\nautoUpdater.autoDownload = false; // 自动下载更新\nautoUpdater.autoInstallOnAppQuit = false; // 自动安装更新\n// 开发环境模拟打包状态\nObject.defineProperty(app, 'isPackaged', {\n    get() {\n        return true;\n    }\n});\n// 设置更新服务器地址\nexport function setupAutoUpdater(mainWindow) {\n    autoUpdater.setFeedURL({\n        provider: 'github',\n        owner: 'iAJue',\n        repo: 'MoeKoeMusic',\n        releaseType: 'release'\n    });\n\n    autoUpdater.channel = 'latest';\n    // 检查更新错误\n    autoUpdater.on('error', (error) => {\n        console.error('Update check failed:', error.message);\n        dialog.showMessageBox({\n        type: 'error',\n        message: error.message.includes('ETIMEDOUT')\n            ? t('update-timeout')\n            : t('update-failed')\n        });\n    });\n    // 检查到新版本\n    autoUpdater.on('update-available', (info) => {\n        const notes = info.releaseNotes?.replace(/<[^>]*>/g, '') || t('no-release-notes');\n        const msg = t('new-version-msg').replace('{version}', info.version).replace('{notes}', notes);\n        dialog.showMessageBox({\n            type: 'info',\n            title: t('new-version'),\n            message: msg,\n            buttons: [t('update-now'), t('later')],\n            cancelId: 1\n        }).then(result => {\n            if (result.response === 0) {\n                autoUpdater.downloadUpdate();\n            }\n        });\n    });\n    // 当前已是最新版本\n    autoUpdater.on('update-not-available', () => {\n        const settings = store.get('settings') || {};\n        if (!settings.silentCheck) {\n            dialog.showMessageBox({\n                type: 'info',\n                title: t('update-hint'),\n                message: t('already-latest'),\n                buttons: [t('ok')]\n            });\n        }\n    });\n    // 更新下载进度\n    autoUpdater.on('download-progress', (progressObj) => {\n        mainWindow.setProgressBar(progressObj.percent / 100);\n        mainWindow.webContents.send('update-progress', progressObj);\n    });\n    // 更新下载完成\n    autoUpdater.on('update-downloaded', () => {\n        mainWindow.setProgressBar(-1);\n        dialog.showMessageBox({\n            type: 'info',\n            title: t('update-ready'),\n            message: t('update-ready-msg'),\n            buttons: [t('install-now'), t('install-later')],\n            cancelId: 1\n        }).then(result => {\n            if (result.response === 0) {\n                autoUpdater.quitAndInstall(false, true);\n            }\n        });\n    });\n}\n// 检查更新\nexport function checkForUpdates(silent = false) {\n    if (process.platform !== 'win32') {\n        if (!silent) {\n            dialog.showMessageBox({\n                type: 'info',\n                title: t('update-hint'),\n                message: t('non-windows-update'),\n                buttons: [t('ok')]\n            });\n        }\n        return;\n    }\n\n    const settings = store.get('settings') || {};\n    if (silent) {\n        settings.silentCheck = true;\n        store.set('settings', settings);\n    } else {\n        settings.silentCheck = false;\n        store.set('settings', settings);\n    }\n\n    autoUpdater.checkForUpdates().catch(error => {\n        console.error('Update check error:', error);\n    });\n}\n"
  },
  {
    "path": "index.html",
    "content": "<!-- public/index.html -->\n<!DOCTYPE html>\n<html lang=\"zh\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"manifest\" href=\"/manifest.webmanifest\">\n    <link rel=\"icon\" href=\"/favicon.ico\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0\">\n    <title>MoeKoe 萌音</title>\n    <meta name=\"theme-color\" content=\"#FF91AF\">\n    <meta name=\"description\" content=\"MoeKoe 萌音,一个高颜值的kugou第三方播放器\">\n    <meta name=\"author\" content=\"MoeKoe,阿珏酱\">\n    <mata name=\"url\" content=\"https://MoeJue.cn/\">\n    <mata name=\"keywords\" content=\"MoeKoe,萌音,kugou,第三方播放器,阿珏酱\">\n    <link href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css\" rel=\"stylesheet\">\n</head>\n\n<body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.js\"></script>\n    <script>\n        const getBrowserLocale = () => {\n            const browserLang = navigator.language;\n            if (browserLang.startsWith('zh')) {\n                if (browserLang === 'zh-TW' || browserLang === 'zh-HK') {\n                    return 'zh-TW';\n                }\n                return 'zh-CN';\n            }\n            const lang = browserLang.split('-')[0];\n            return Object.keys(messages).includes(lang) ? lang : 'ja';\n        };\n        const settings = JSON.parse(localStorage.getItem('settings'));\n        document.documentElement.lang = settings?.language || getBrowserLocale();\n        const font = settings?.font || '';\n        const fontUrl = settings?.fontUrl || '';\n        if (fontUrl && font) {\n            const link = document.createElement('link');\n            link.href = fontUrl;\n            link.rel = 'stylesheet';\n            document.head.appendChild(link);\n            const style = document.createElement('style');\n            style.textContent = `* { font-family: \"${font}\", Arial, sans-serif; }`;\n            document.head.appendChild(style);\n        }\n    </script>\n</body>\n\n</html>"
  },
  {
    "path": "nginx.conf",
    "content": "worker_processes 1;\n\nevents {\n    worker_connections 1024;\n}\n\nhttp {\n    include mime.types;\n    default_type application/octet-stream;\n\n    sendfile on;\n\n    keepalive_timeout 65;\n\n    server {\n        listen 8080;\n        server_name localhost;\n\n        location / {\n            root /app/dist;\n            index index.html;\n            try_files $uri $uri/ /index.html;\n        }\n\n        location /api/ {\n            proxy_pass http://localhost:6521/;\n            proxy_set_header Host $host;\n            proxy_set_header X-Real-IP $remote_addr;\n            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n            proxy_set_header X-Forwarded-Proto $scheme;\n        }\n    }\n}"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"moekoemusic\",\n  \"version\": \"1.6.1\",\n  \"homepage\": \"https://github.com/iAJue/MoeKoeMusic\",\n  \"main\": \"electron/main.js\",\n  \"scripts\": {\n    \"install-all\": \"npm install && cd api && npm install\",\n    \"serve\": \"vite\",\n    \"build\": \"vite build\",\n    \"build:docker\": \"cross-env VITE_APP_API_URL=/api vite build\",\n    \"preview\": \"vite preview\",\n    \"electron:serve\": \"cross-env NODE_ENV=development electron .\",\n    \"api\": \"node api/app.js --platform=lite --port=6521\",\n    \"dev\": \"npm run api & npm run serve & npm run electron:serve\",\n    \"build:api:win\": \"npm run --prefix ./api pkgwin\",\n    \"build:api:linux\": \"npm run --prefix ./api pkglinux\",\n    \"build:api:linux-arm64\": \"npm run --prefix ./api pkglinux-arm64\",\n    \"build:api:macos\": \"npm run --prefix ./api pkgmacos\",\n    \"electron:build:win\": \"run-s build:api:win \\\"electron:build -- --win --publish never\\\"\",\n    \"electron:build:linux\": \"run-s build:api:linux \\\"electron:build -- --linux --publish never\\\"\",\n    \"electron:build:linux-arm64\": \"run-s build:api:linux-arm64 \\\"electron:build -- --linux --publish never\\\"\",\n    \"electron:build:macos\": \"run-s build:api:macos \\\"electron:build -- --mac --x64 --arm64 --publish never\\\"\",\n    \"electron:build:macos:universal\": \"run-s build:api:macos \\\"electron:build -- --mac --universal --publish never\\\"\",\n    \"electron:build:macos:x64\": \"run-s build:api:macos \\\"electron:build -- --mac --x64 --publish never\\\"\",\n    \"electron:build:macos:arm64\": \"run-s build:api:macos \\\"electron:build -- --mac --arm64 --publish never\\\"\",\n    \"electron:build\": \"cross-env NODE_ENV=production electron-builder\"\n  },\n  \"engines\": {\n    \"node\": \">=20\"\n  },\n  \"build\": {\n    \"publish\": {\n      \"provider\": \"github\",\n      \"owner\": \"iAJue\",\n      \"repo\": \"MoeKoeMusic\"\n    },\n    \"compression\": \"maximum\",\n    \"extraResources\": [\n      {\n        \"from\": \"build/icons/\",\n        \"to\": \"icons/\",\n        \"filter\": [\n          \"**/*\"\n        ]\n      }\n    ],\n    \"extraFiles\": [\n      {\n        \"from\": \"api/bin\",\n        \"to\": \"api/\",\n        \"filter\": [\n          \"**/*\"\n        ]\n      },\n      {\n        \"from\": \"public/assets\",\n        \"to\": \"assets\"\n      }\n    ],\n    \"appId\": \"cn.MoeKoe.Music\",\n    \"productName\": \"MoeKoe Music\",\n    \"copyright\": \"© 2024 MoeKoe\",\n    \"directories\": {\n      \"output\": \"dist_electron\"\n    },\n    \"files\": [\n      \"dist/**/*\",\n      \"electron/**/*\",\n      \"package.json\"\n    ],\n    \"icon\": \"build/icons/icon\",\n    \"protocols\": [\n      {\n        \"name\": \"MoeKoe Music Protocol\",\n        \"schemes\": [\n          \"moekoe\"\n        ]\n      }\n    ],\n    \"win\": {\n      \"icon\": \"build/icons/icon.ico\",\n      \"target\": [\n        {\n          \"target\": \"nsis\",\n          \"arch\": [\n            \"x64\",\n            \"ia32\"\n          ]\n        }\n      ],\n      \"artifactName\": \"MoeKoe_Music_Setup_v${version}.${ext}\"\n    },\n    \"nsis\": {\n      \"oneClick\": false,\n      \"allowToChangeInstallationDirectory\": true,\n      \"perMachine\": true,\n      \"installerIcon\": \"build/icons/icon.ico\",\n      \"uninstallerIcon\": \"build/icons/icon.ico\",\n      \"installerHeaderIcon\": \"build/icons/icon.ico\",\n      \"createDesktopShortcut\": true,\n      \"createStartMenuShortcut\": true,\n      \"shortcutName\": \"MoeKoe Music\",\n      \"include\": \"build/installer.nsh\",\n      \"license\": \"build/license.txt\",\n      \"language\": \"2052\",\n      \"warningsAsErrors\": true\n    },\n    \"mac\": {\n      \"x64ArchFiles\": \"*\",\n      \"icon\": \"build/icons/icon.icns\",\n      \"target\": [\n        \"dmg\"\n      ],\n      \"identity\": null,\n      \"artifactName\": \"${productName}-${arch}.${ext}\",\n      \"protocols\": [\n        {\n          \"name\": \"MoeKoe Music Protocol\",\n          \"schemes\": [\n            \"moekoe\"\n          ]\n        }\n      ]\n    },\n    \"dmg\": {\n      \"sign\": false\n    },\n    \"linux\": {\n      \"icon\": \"build/icons/linux_256x256.png\",\n      \"target\": [\n        \"AppImage\",\n        \"deb\"\n      ],\n      \"category\": \"Utility\",\n      \"artifactName\": \"${productName}.${ext}\"\n    }\n  },\n  \"type\": \"module\",\n  \"author\": {\n    \"name\": \"MoeJue\",\n    \"email\": \"MoeJue@qq.com\"\n  },\n  \"blog\": \"MoeJue.cn\",\n  \"license\": \"MIT\",\n  \"description\": \"MoeKoe Music\",\n  \"devDependencies\": {\n    \"@types/node\": \"^24.10.1\",\n    \"@vitejs/plugin-vue\": \"^6.0.2\",\n    \"@vue/compiler-sfc\": \"^3.5.12\",\n    \"cross-env\": \"^7.0.3\",\n    \"electron\": \"^39.0.0\",\n    \"electron-builder\": \"^25.1.8\",\n    \"esbuild\": \"0.25.11\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"vite\": \"^7.2.6\",\n    \"vite-plugin-pwa\": \"^1.2.0\",\n    \"vue-loader\": \"^17.3.1\",\n    \"wait-on\": \"^8.0.1\"\n  },\n  \"dependencies\": {\n    \"adm-zip\": \"^0.5.16\",\n    \"electron-is-dev\": \"^3.0.1\",\n    \"electron-log\": \"^5.2.0\",\n    \"electron-store\": \"^10.0.0\",\n    \"electron-updater\": \"^6.6.2\",\n    \"music-metadata\": \"^11.7.3\",\n    \"pinia\": \"^3.0.0\",\n    \"pinia-plugin-persistedstate\": \"^4.1.1\",\n    \"tree-kill\": \"^1.2.2\",\n    \"vue\": \"^3.5.12\",\n    \"vue-i18n\": \"^10.0.4\",\n    \"vue-router\": \"^4.4.5\",\n    \"vue3-virtual-scroller\": \"^0.2.3\",\n    \"ws\": \"^8.18.1\"\n  }\n}\n"
  },
  {
    "path": "src/App.vue",
    "content": "<template>\n    <div id=\"app\">\n        <TitleBar v-if=\"showTitleBar && !isLyricsRoute\" />\n        <RouterView />\n        <Disclaimer v-if=\"!isLyricsRoute\" />\n        <StatusBarLyrics ref=\"statusBarLyricsRef\" />\n    </div>\n</template>\n\n<script setup>\nimport { ref, computed, onMounted, onUnmounted } from 'vue';\nimport { useRoute } from 'vue-router';\nimport Disclaimer from '@/components/Disclaimer.vue';\nimport TitleBar from '@/components/TitleBar.vue';\nimport StatusBarLyrics from '@/components/StatusBarLyrics.vue';\nimport { MoeAuthStore } from '@/stores/store';\nimport logoImageSrc from '@/assets/images/tray/tray-icon@2x.png?url';\n\nconst route = useRoute();\nconst isLyricsRoute = computed(() => route.path === '/lyrics');\n\n// 状态栏歌词逻辑\nconst statusBarLyricsRef = ref(null);\nlet cleanupStatusBarIPC = null;\n\n// 动态控制 TitleBar 的显示\nconst showTitleBar = ref(true);\n\nonMounted(async () => {\n    const settings = JSON.parse(localStorage.getItem('settings')) || {};\n    showTitleBar.value = settings.nativeTitleBar !== 'on'; // 如果值为 'on'，则不显示 TitleBar\n\n    const MoeAuth = MoeAuthStore();\n    await MoeAuth.initDevice();\n\n    // 初始化状态栏歌词\n    cleanupStatusBarIPC = statusBarLyricsRef.value?.initStatusBar(logoImageSrc, settings);\n});\n\nonUnmounted(() => {\n    statusBarLyricsRef.value?.cleanupStatusBar();\n    cleanupStatusBarIPC?.();\n});\n</script>\n\n<style scoped>\n.container {\n    max-width: 1400px;\n    margin: 0 auto;\n    padding: 20px;\n}\n</style>\n"
  },
  {
    "path": "src/assets/style/PlayerControl.css",
    "content": "#lyrics-container {\n    height: 75vh;\n    overflow: hidden;\n    display: flex;\n    justify-content: center;\n    padding: 20px;\n    border-radius: 8px;\n    margin-top: 80px;\n    position: relative;\n    width: 60%;\n}\n\n#lyrics {\n    font-size: 24px;\n    line-height: 1.8;\n    white-space: pre-wrap;\n    color: #666;\n    transition: transform 0.6s ease;\n}\n\n.line-group {\n    margin-bottom: 12px;\n}\n\n.line {\n    border: 10px;\n    display: block;\n    border-radius: 10px;\n    padding: 0 10px;\n    width: fit-content;\n    transition: background-color .3s ease-in;\n}\n\n.line.left {\n    text-align: left;\n}\n\n.line.center {\n    text-align: center;\n    margin: 0 auto;\n}\n\n.line.translated {\n    color: #e3e3e3b1;\n}\n\n.line::after {\n    content: '\\f04b';\n    font-family: 'Font Awesome 6 Free';\n    font-weight: 900;\n    float: right;\n    display: block;\n    opacity: 0;\n    color: #e3e3e392;\n    margin-left: 10px;\n    transition: opacity .3s ease-in;\n}\n\n.line:hover {\n    cursor: pointer;\n    background-color: #e3e3e33b;\n}\n\n.line:hover::after {\n    opacity: 1;\n}\n\n.char {\n    display: inline-block;\n    color: transparent;\n    background: linear-gradient(to right, var(--primary-color) 50%, #e3e3e3e8 50%);\n    background-size: 200% 100%;\n    background-position: 100%;\n    -webkit-background-clip: text;\n    background-clip: text;\n    transition: background-position 0.6s ease;\n}\n\n.highlight {\n    background-position: 0%;\n}\n\n\n.player-container {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    width: 100%;\n    position: fixed;\n    bottom: 0px;\n    background: #FFF;\n    box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);\n    border-radius: 10px;\n    background: rgba(255, 255, 255, .7);\n    -webkit-backdrop-filter: blur(10px);\n    backdrop-filter: blur(10px);\n    z-index: 1;\n}\n\n.player-bar {\n    display: flex;\n    align-items: center;\n    padding: 10px;\n    width: 100%;\n    max-width: 880px;\n}\n\n.album-art {\n    width: 60px;\n    height: 60px;\n    border-radius: 5px;\n    margin-right: 15px;\n    background-color: #ddd;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    cursor: pointer;\n}\n\n.album-art img {\n    width: 64px;\n    height: 64px;\n    border-radius: 5px;\n}\n\n.album-art i {\n    font-size: 24px;\n    color: #666;\n}\n\n.song-info {\n    flex: 0 1 300px;\n    cursor: pointer;\n    margin-right: auto;\n    min-width: 0;\n    max-width: 300px;\n}\n\n.song-title-row {\n    display: inline-flex;\n    align-items: center;\n    gap: 8px;\n    max-width: 300px;\n    min-width: 0;\n    vertical-align: top;\n    margin-bottom: 5px;\n}\n\n.song-title-row .song-title {\n    margin-bottom: 0;\n}\n\n.song-title {\n    font-weight: bold;\n    margin-bottom: 5px;\n    flex: 0 1 auto;\n    min-width: 0;\n    max-width: 100%;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.quality-badge {\n    display: inline-flex;\n    align-items: center;\n    justify-content: center;\n    flex-shrink: 0;\n    border: 1px solid;\n    border-radius: 999px;\n    font-size: 11px;\n    background: transparent;\n    color: var(--color-primary);\n    border-color: var(--color-primary);\n    background-color: color-mix(in srgb, var(--color-primary) 8%, transparent);\n}\n\n.quality-badge.clickable {\n    cursor: pointer;\n}\n\n.quality-menu-wrapper {\n    position: relative;\n    flex-shrink: 0;\n    transform: translateY(-2px);\n}\n\n.quality-menu {\n    position: absolute;\n    bottom: calc(100% + 8px);\n    left: 0;\n    min-width: 110px;\n    padding: 6px;\n    border-radius: 10px;\n    background: rgba(255, 255, 255, 0.96);\n    box-shadow: 0 10px 24px rgba(0, 0, 0, 0.12);\n    border: 1px solid rgba(0, 0, 0, 0.06);\n    z-index: 10;\n}\n\n.quality-menu-item {\n    width: 100%;\n    border: none;\n    background: transparent;\n    text-align: left;\n    border-radius: 8px;\n    padding: 8px 10px;\n    font-size: 13px;\n    color: #303133;\n    cursor: pointer;\n}\n\n.quality-menu-item:hover,\n.quality-menu-item.active {\n    background: var(--color-primary-light);\n    color: var(--color-primary);\n}\n\n.quality-menu-item.disabled {\n    color: #909399;\n    cursor: default;\n}\n\n.fa-volume-up {\n    width: 10px;\n}\n\n.artist {\n    font-size: 0.9em;\n    color: #c3c3c3;\n    width: auto;\n    max-width: 300px;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.controls {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-grow: 1;\n}\n\n.control-btn {\n    background: none;\n    border: none;\n    font-size: 24px;\n    cursor: pointer;\n    padding: 0 10px;\n    color: #333;\n}\n\n\n.progress-bar {\n    width: 100%;\n    height: 6px;\n    background-color: #ddd;\n    position: relative;\n    border-radius: 5px;\n    overflow: visible;\n    cursor: pointer;\n}\n\n.progress-bar::before {\n    content: '';\n    position: absolute;\n    top: -10px;\n    left: 0;\n    right: 0;\n    bottom: -10px;\n    cursor: pointer;\n}\n\n.progress {\n    width: 30%;\n    height: 100%;\n    background-color: var(--primary-color);\n    position: absolute;\n    transition: width 0.2s ease;\n}\n\n.progress-handle {\n    width: 12px;\n    height: 12px;\n    background-color: #fff;\n    border: 2px solid var(--primary-color);\n    border-radius: 50%;\n    position: absolute;\n    top: 50%;\n    transform: translate(-50%, -50%);\n    opacity: 0;\n    transition: opacity 0.2s ease;\n    z-index: 2;\n    cursor: grab;\n    pointer-events: auto;\n}\n.progress-bar:active .progress-handle {\n    transform: translate(-50%, -50%) scale(1.2);\n    cursor: grabbing;\n}\n.progress-handle.dragging,\n.progress-bar:hover .progress-handle {\n    opacity: 1;\n}\n\n.progress-bar:hover .progress {\n    background-color: var(--primary-color);\n    opacity: 0.8;\n}\n\n.extra-controls {\n    display: flex;\n    align-items: center;\n    margin-left: auto;\n    gap: 4px;\n}\n\n.extra-btn {\n    background: none;\n    border: none;\n    font-size: 16px;\n    cursor: pointer;\n    padding: 0 8px;\n    color: #666;\n}\n\n\n.volume-control {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n}\n\n.volume-control i {\n    font-size: 18px;\n    color: #666;\n}\n\n.volume-slider {\n    width: 100px;\n    height: 6px;\n    background-color: #ddd;\n    border-radius: 3px;\n    position: relative;\n    overflow: hidden;\n    cursor: pointer;\n}\n\n.volume-progress {\n    height: 100%;\n    background-color: var(--primary-color);\n    position: absolute;\n    left: 0;\n    top: 0;\n    border-radius: 3px;\n}\n\n/* 全屏歌词界面样式 */\n.lyrics-bg {\n    position: fixed;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    top: 0;\n    z-index: 99;\n    background-repeat: no-repeat;\n    background-position: center;\n    background-size: cover;\n}\n\n.lyrics-screen {\n    position: fixed;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    top: 0;\n    backdrop-filter: blur(18px);\n    color: white;\n    display: flex;\n    justify-content: space-between;\n    padding: 20px;\n    background: #1616169c;\n}\n\n.close-btn {\n    position: absolute;\n    top: 30px;\n    right: 100px;\n    font-size: 24px;\n    cursor: pointer;\n    color: white;\n    z-index: 99;\n    text-align: right;\n    width: 100%;\n}\n\n.left-section {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    width: 40%;\n    margin-top: 135px;\n    margin-left: 20px;\n}\n\n.album-art-container {\n    cursor: pointer;\n    transition: transform 0.3s ease;\n    position: relative;\n}\n\n.album-art-container:hover {\n    transform: scale(1.02);\n}\n\n/* 普通封面模式 */\n.album-art-large {\n    position: relative;\n    animation: fadeInScale 0.6s ease-out;\n}\n\n.album-art-large img {\n    width: 400px;\n    height: 400px;\n    border-radius: 10px;\n    z-index: 9;\n    transition: all 0.3s ease;\n}\n\n/* 唱片播放器模式 */\n.vinyl-player {\n    position: relative;\n    width: 450px;\n    height: 450px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    animation: fadeInScale 0.6s ease-out;\n}\n\n/* 淡入缩放动画 */\n@keyframes fadeInScale {\n    0% {\n        opacity: 0;\n        transform: scale(0.8);\n    }\n    100% {\n        opacity: 1;\n        transform: scale(1);\n    }\n}\n\n/* 唱片圆盘 */\n.vinyl-disc {\n    position: relative;\n    width: 400px;\n    height: 400px;\n    border-radius: 50%;\n    background: radial-gradient(circle at center, #1a1a1a 0%, #1a1a1a 35%, #2a2a2a 35%, #2a2a2a 36%, #1a1a1a 36%, #1a1a1a 100%);\n    box-shadow: 0 0 30px rgba(0, 0, 0, 0.8), inset 0 0 20px rgba(0, 0, 0, 0.5);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.vinyl-disc::before {\n    content: '';\n    position: absolute;\n    width: 100%;\n    height: 100%;\n    border-radius: 50%;\n    background: repeating-radial-gradient(circle at center,\n        transparent 0px,\n        transparent 2px,\n        rgba(255, 255, 255, 0.03) 2px,\n        rgba(255, 255, 255, 0.03) 4px\n    );\n}\n\n/* .vinyl-disc::after {\n    content: '';\n    position: absolute;\n    width: 80px;\n    height: 80px;\n    border-radius: 50%;\n    background: radial-gradient(circle, #8B4513 0%, #654321 50%, #3d2817 100%);\n    box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);\n    z-index: 10;\n} */\n\n/* 唱片封面 */\n.vinyl-cover {\n    width: 240px;\n    height: 240px;\n    border-radius: 50%;\n    object-fit: cover;\n    z-index: 5;\n}\n\n/* 唱片旋转动画 - 始终应用动画 */\n.vinyl-disc {\n    animation: vinylRotate 5s linear infinite;\n    animation-play-state: paused;\n}\n\n.vinyl-disc.rotating {\n    animation-play-state: running;\n}\n\n@keyframes vinylRotate {\n    from {\n        transform: rotate(0deg);\n    }\n    to {\n        transform: rotate(360deg);\n    }\n}\n\n/* 磁头 */\n.tonearm {\n    position: absolute;\n    top: -50px;\n    right: 20px;\n    width: 160px;\n    height: 198px;\n    background: url('../assets/images/head.png') no-repeat center;\n    background-size: contain;\n    transform-origin: 85% 15%;\n    transform: rotate(-30deg);\n    transition: transform 0.6s ease-in-out;\n    z-index: 15;\n    pointer-events: none;\n    animation: tonearmSlideIn 0.8s ease-out;\n}\n\n/* 磁头滑入动画 */\n@keyframes tonearmSlideIn {\n    0% {\n        opacity: 0;\n        transform: rotate(-30deg) translateX(50px);\n    }\n    100% {\n        opacity: 1;\n        transform: rotate(-30deg) translateX(0);\n    }\n}\n\n/* 磁头播放状态 */\n.tonearm.playing {\n    transform: rotate(0deg);\n}\n\n.song-details {\n    text-align: center;\n    margin-top: 20px;\n}\n\n.player-controls {\n    display: flex;\n    gap: 15px;\n    margin-top: 20px;\n}\n\n.progress-bar-container {\n    display: flex;\n    align-items: center;\n    width: 100%;\n    margin: 10px 0;\n}\n\n.current-time,\n.duration {\n    color: white;\n    font-size: 12px;\n}\n\n.progress-bar {\n    flex-grow: 1;\n    height: 2px;\n    background-color: #555;\n    border-radius: 5px;\n    margin: 0 10px;\n    position: relative;\n}\n\n.progress {\n    width: 50%;\n    height: 100%;\n    background-color: var(--primary-color);\n    border-radius: 5px;\n}\n\n.player-controls {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    gap: 15px;\n    margin-top: 10px;\n}\n\n.player-controls .control-btn {\n    color: #fff;\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.volume-control {\n    display: flex;\n    align-items: center;\n    position: relative;\n}\n\n.volume-slider {\n    width: 100px;\n    margin-left: 10px;\n    position: relative;\n}\n\n.volume-slider input[type=\"range\"] {\n    width: 100%;\n    -webkit-appearance: none;\n    appearance: none;\n    height: 5px;\n    background: transparent;\n    position: relative;\n    z-index: 1;\n    pointer-events: none;\n    /* 禁止对 input 的点击事件 */\n}\n\n.volume-slider input[type=\"range\"]::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 12px;\n    height: 12px;\n    border-radius: 50%;\n    background: #4899f8;\n    cursor: pointer;\n    pointer-events: auto;\n    /* 允许 thumb 的拖动事件 */\n}\n\n.volume-slider input[type=\"range\"]::-moz-range-thumb {\n    width: 12px;\n    height: 12px;\n    border-radius: 50%;\n    background: #4899f8;\n    cursor: pointer;\n}\n\n.album-art-large .miku {\n    position: absolute;\n    height: 419px;\n    width: 401px;\n}\n\n.album-art-large .miku2 {\n    position: absolute;\n    height: 452px;\n    width: 404px;\n}\n\n.album-art-large .miku3 {\n    position: absolute;\n    height: 563px;\n}\n\n.no-lyrics {\n    color: var(--primary-color);\n    margin: auto;\n    font-size: 2em;\n}\n\n.loop-icon {\n    position: relative;\n    display: inline-block;\n}\n\n.loop-icon sup {\n    position: absolute;\n    font-size: 0.6em;\n}\n\n/* 全屏歌词界面的进度条样式 */\n.lyrics-screen .progress-bar {\n    flex-grow: 1;\n    height: 3px;\n    background-color: rgba(255, 255, 255, 0.3);\n    border-radius: 5px;\n    margin: 0 10px;\n    position: relative;\n    overflow: visible;\n}\n\n.lyrics-screen .progress-bar::before {\n    content: '';\n    position: absolute;\n    top: -12px;\n    left: 0;\n    right: 0;\n    bottom: -12px;\n    cursor: pointer;\n}\n\n.progress-handle.dragging {\n    opacity: 1;\n}\n\n.time-tooltip {\n    position: absolute;\n    top: -25px;\n    transform: translateX(-50%);\n    background-color: rgba(0, 0, 0, 0.7);\n    color: white;\n    padding: 2px 6px;\n    border-radius: 4px;\n    font-size: 12px;\n    pointer-events: none;\n    z-index: 10;\n}\n\n/* 歌词界面的时间提示样式 */\n.lyrics-screen .time-tooltip {\n    backdrop-filter: blur(4px);\n}\n\n/* 高潮点的样式 */\n.climax-point {\n    position: absolute;\n    width: 6px;\n    height: 6px;\n    border-radius: 50%;\n    top: 67%;\n    transform: translate(-50%, -50%);\n    z-index: 1;\n    pointer-events: none;\n}\n\n/* 普通界面的高潮点样式 */\n.player-container .climax-point {\n    background-color: var(--primary-color);\n    box-shadow: 0 0 4px var(--primary-color);\n}\n\n/* 歌词界面的高潮点样式 */\n.lyrics-screen .climax-point {\n    background-color: #ff69b4;\n    box-shadow: 0 0 4px #ff69b4;\n}\n\n/* 收藏按钮 */\n.like-btn:hover {\n    opacity: 1;\n    transform: scale(1.1);\n}\n\n.like-btn.active {\n    color: #ff4081;\n    opacity: 1;\n}\n\n.playback-speed {\n    position: relative;\n    display: inline-block;\n}\n\n.speed-menu {\n    position: absolute;\n    bottom: 100%;\n    left: 50%;\n    transform: translateX(-50%);\n    background: var(--background-color);\n    border: 1px solid var(--background-color);\n    border-radius: 4px;\n    margin-bottom: 8px;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n    z-index: 1000;\n}\n\n.speed-option {\n    padding: 6px 16px;\n    cursor: pointer;\n    white-space: nowrap;\n    color: var(--text-color);\n    transition: background-color 0.2s;\n}\n\n.speed-option:hover {\n    background-color: var(--secondary-color);\n}\n\n.speed-option.active {\n    background-color: var(--primary-color);\n    color: var(--primary-text-color);\n}\n\n.speed-menu::after {\n    content: '';\n    position: absolute;\n    top: 100%;\n    left: 50%;\n    transform: translateX(-50%);\n    border: 6px solid transparent;\n    border-top-color: var(--background-color);;\n}\n\n.close-btn {\n  cursor: pointer;\n  font-size: 24px;\n  color: #fff;\n  opacity: 0.8;\n  transition: opacity 0.3s;\n}\n\n.close-btn:hover {\n  opacity: 1;\n}\n\n.lyrics-mode-btn {\n  cursor: pointer;\n  font-size: 20px;\n  color: #fff;\n  opacity: 0.8;\n  transition: opacity 0.3s;\n  position: absolute;\n  right: 20px;\n  top: 10px;\n}\n\n.lyrics-mode-btn:hover {\n  opacity: 1;\n}\n\n.line.romanized {\n  font-size: 0.9em;\n  opacity: 0.9;\n  margin-top: 5px;\n  color: #e3e3e3b1;\n}\n.slide-up-enter-active {\n  transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);\n}\n\n.slide-up-leave-active {\n  transition: all 0.3s cubic-bezier(0.4, 0, 0.6, 1);\n}\n\n.slide-up-enter-from {\n  transform: translateY(100%);\n  opacity: 0;\n}\n\n.slide-up-leave-to {\n  transform: translateY(100%);\n  opacity: 0;\n}\n\n.slide-up-enter-to,\n.slide-up-leave-from {\n  transform: translateY(0);\n  opacity: 1;\n}\n\n/* 封面模式切换过渡动画 */\n.cover-fade-enter-active {\n  transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.cover-fade-leave-active {\n  transition: all 0.3s cubic-bezier(0.4, 0, 1, 1);\n}\n\n.cover-fade-enter-from {\n  opacity: 0;\n  transform: scale(0.9) translateY(20px);\n}\n\n.cover-fade-leave-to {\n  opacity: 0;\n  transform: scale(0.9) translateY(-20px);\n}\n\n.cover-fade-enter-to,\n.cover-fade-leave-from {\n  opacity: 1;\n  transform: scale(1) translateY(0);\n}\n"
  },
  {
    "path": "src/assets/themes/dark.css",
    "content": "/* 暗黑模式样式 */\nhtml.dark {\n    background-color: #121212;\n    color: #e1e1e1;\n    filter: brightness(0.8);\n    mix-blend-mode: multiply;\n}\n\nhtml.dark body {\n    background-color: #121212;\n}\n\nhtml.dark .settings-page {\n    background-color: #121212;\n}\n\nhtml.dark .settings-sidebar {\n    background-color: #1a1a1a;\n    border-right-color: #333;\n}\n\nhtml.dark .sidebar-item {\n    color: #e1e1e1;\n}\n\nhtml.dark .sidebar-item:hover:not(.active) {\n    background-color: #2a2a2a;\n}\n\nhtml.dark .sidebar-item.active {\n    background-color: rgba(255, 105, 180, 0.15);\n    color: var(--primary-color);\n}\n\nhtml.dark .setting-section h3 {\n    color: #e1e1e1;\n    border-bottom-color: #333;\n}\n\nhtml.dark .setting-card {\n    background-color: #1d1d1d;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);\n}\n\nhtml.dark .setting-card:hover {\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n}\n\nhtml.dark .setting-card-header {\n    color: #e1e1e1;\n}\n\nhtml.dark .setting-card-value {\n    background-color: #2a2a2a;\n    color: #e1e1e1;\n    border: 1px solid #333;\n}\n\nhtml.dark .refresh-hint {\n    color: #ff6b6b;\n}\n\nhtml.dark .modal-content {\n    background-color: #1d1d1d;\n    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);\n}\n\nhtml.dark .modal-content h3 {\n    color: #e1e1e1;\n}\n\nhtml.dark .modal-content li {\n    background-color: #2a2a2a;\n    color: #e1e1e1;\n}\n\nhtml.dark .modal-content li:hover {\n    background-color: #363636;\n}\n\nhtml.dark .modal-content button {\n    background-color: #2a2a2a;\n    color: #e1e1e1;\n}\n\nhtml.dark .modal-content button:hover {\n    background-color: #363636;\n}\n\n/* 播放器控件 */\nhtml.dark .player-container {\n    background-color: rgba(24, 24, 24) !important;\n    border-top: 1px solid #333 !important;\n}\n\nhtml.dark .player-container .control-button {\n    color: #e1e1e1 !important;\n}\n\nhtml.dark .player-container .control-button:hover {\n    color: var(--primary-color) !important;\n}\n\nhtml.dark .player-container .song-info {\n    color: #e1e1e1 !important;\n}\n\nhtml.dark .player-container .song-title {\n    color: #e1e1e1 !important;\n}\n\nhtml.dark .player-container .song-artist, html.dark .extension-item h4 {\n    color: #999 !important;\n}\n\nhtml.dark .player-container .progress-bar {\n    background-color: #4a4a4a !important;\n}\n\nhtml.dark .player-container .progress-bar .loaded {\n    background-color: #666 !important;\n}\n\n/* 卡片和列表项 */\nhtml.dark .playlist-item,\nhtml.dark .song-item {\n    background-color: #1d1d1d;\n    border-color: #333;\n}\n\n/* 输入框 */\nhtml.dark input {\n    color: #e1e1e1;\n    border-color: #333;\n    background: black;\n}\n\n/* 滚动条 */\nhtml.dark ::-webkit-scrollbar-track {\n    background-color: #2a2a2a;\n}\n\nhtml.dark ::-webkit-scrollbar-thumb {\n    background-color: #4a4a4a;\n}\n\n/* 头部导航 */\nhtml.dark header {\n    background-color: rgba(24, 24, 24) !important;\n    border-bottom: 1px solid #333;\n}\n\nhtml.dark header .logo {\n    color: #e1e1e1;\n}\n\nhtml.dark header .nav-item {\n    color: #e1e1e1;\n}\n\nhtml.dark header .nav-item:hover,\nhtml.dark header .nav-item.active {\n    color: var(--primary-color);\n}\n\nhtml.dark header .window-controls {\n    color: #e1e1e1;\n}\n\nhtml.dark header .window-controls span:hover {\n    background-color: #363636;\n}\n\nhtml.dark header .search-box {\n    background-color: #2a2a2a;\n    border-color: #333;\n}\n\nhtml.dark header .search-box input {\n    color: #e1e1e1;\n}\n\nhtml.dark header button {\n    background-color: transparent !important;\n    border: none !important;\n    color: #999 !important;\n}\n\nhtml.dark header .nav-arrow:disabled i {\n    color: #353535 !important;\n}\n\nhtml.dark header .search-box input::placeholder {\n    color: #999;\n}\n\nhtml.dark header .user-avatar {\n    border-color: #333;\n}\n\nhtml.dark .primary-btn , html.dark .more-btn-container .more-btn {\n    border: none !important;\n}\n\n/* 按钮 */\nhtml.dark button {\n    background-color: #2a2a2a !important;\n    color: #e1e1e1 !important;\n    border: 1px solid #373434 !important;\n}\n\nhtml.dark button:hover {\n    background-color: #363636 !important;\n}\n\n/* 链接 */\nhtml.dark a {\n    color: #e1e1e1;\n}\n\n/* 主内容区 */\nhtml.dark main {\n    background-color: #121212;\n}\n\n/* 音乐卡片 */\nhtml.dark .music-card {\n    background-color: #1d1d1d;\n    border-color: #333;\n    border-radius: 5px;\n}\n\nhtml.dark .music-card .title {\n    color: #e1e1e1;\n}\n\nhtml.dark .music-card .artist {\n    color: #999;\n}\n\nhtml.dark .music-card:hover {\n    background-color: #2a2a2a;\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);\n}\n\nhtml.dark .music-card .play-count {\n    color: #999;\n}\n\nhtml.dark .music-card .description {\n    color: #999;\n}\n\n/* PlaylistDetail 页面样式 */\nhtml.dark .detail-page .header .description {\n    color: #777;\n}\n\nhtml.dark .playlist-info .description {\n    color: #999;\n}\n\nhtml.dark .track-list .li {\n    border-bottom: none;\n    color: #999;\n}\n\nhtml.dark .track-list .li:hover {\n    background-color: #2a2a2a;\n}\n\n\nhtml.dark .playlist-info .meta {\n    color: #999;\n}\n\nhtml.dark .song-list {\n    background-color: #121212;\n}\n\nhtml.dark .song-item:hover {\n    background-color: #2a2a2a;\n}\n\nhtml.dark .song-item .song-name {\n    color: #e1e1e1;\n}\n\nhtml.dark .song-item .album-name, html.dark .song-item .song-album, html.dark .song-item .song-duration, html.dark .song-item .song-actions {\n    color: #999;\n}\n\nhtml.dark .song-item .song-actions button {\n    color: #e1e1e1;\n}\n\nhtml.dark .more-btn-container .dropdown-menu {\n    background-color: #1d1d1d;\n    border: none;\n    color: #e1e1e1;\n}\n\nhtml.dark .more-btn-container .dropdown-menu li:hover {\n    background-color: #2a2a2a;\n}\n\nhtml.dark .song-item .song-actions button:hover {\n    color: var(--primary-color);\n    background-color: #363636;\n} \n\nhtml.dark .sq-icon {\n    color: #2f74a5;\n}\nhtml.dark .vip-icon {\n    color: #b86222;\n}\n\nhtml.dark .context-menu, html.dark .context-menu ul {\n    background-color: #1d1d1d;\n    border:none;\n    border-radius: 5px;\n    color: #999;\n}\nhtml.dark .context-menu li:hover {\n    background-color: #2a2a2a !important;\n}\n\nhtml.dark .controls .control-btn,\nhtml.dark .player-controls .control-btn,\nhtml.dark .extra-controls .extra-btn {\n    background-color: transparent !important;\n    color: #999 !important;\n    border: none!important;\n}\n\nhtml.dark .search-bar input, html.dark .search-input {\n    background-color: #2a2a2a !important;\n    color: #e1e1e1 !important;\n    border: 1px solid #373434 !important;\n}\n\nhtml.dark .profile-menu {\n    background-color: #333333;\n    border:none;\n    border-radius: 5px;\n}\nhtml.dark .profile-menu li a{\n    color: #bcbcbc;\n}\nhtml.dark .profile-menu li a:hover {\n    background-color: #484848 !important;\n}\nhtml.dark .queue-popup{\n    background-color: #1d1d1d;\n    color: #e1e1e1;\n}\nhtml.dark .queue-popup h3{\n    color: #e1e1e1;\n}\nhtml.dark .queue-popup li {\n    border: none;\n}\nhtml.dark .queue-popup .queue-play-btn {\n    background-color: transparent !important;\n    border: none !important;\n}\nhtml.dark .modal-content{\n    color: #e1e1e1;\n}\nhtml.dark .search-results{\n    background-color: #121212;\n    color: #e1e1e1;\n}\nhtml.dark .search-results li{\n    border: none;\n}\nhtml.dark .search-results li:hover{\n    background-color: #2a2a2a;\n}\n\nhtml.dark .skeleton-grid .skeleton-card {\n    background-color: #1d1d1d;\n}\nhtml.dark .skeleton-grid .skeleton-title, html.dark .skeleton-grid .skeleton-text, html.dark .skeleton-grid .skeleton-image {\n    background-color: #2a2a2a;\n}\n\nhtml.dark .skeleton-loader .skeleton-item {\n    background-color: #1d1d1d;\n}\nhtml.dark .skeleton-loader .skeleton-cover, html.dark .skeleton-loader .skeleton-line {\n    background-color: #2a2a2a;\n}\n\n.radio-card .decorative-box {\n    box-shadow: -5px -5px 10px rgb(163 163 163 / 0%), 5px 5px 10px rgba(255, 255, 255, 0.1), inset 2px 2px 5px rgb(0 0 0 / 18%), inset -2px -2px 5px rgba(255, 255, 255, 0.05)\n}\n\nhtml.dark .radio-card .radio-content .radio-title, html.dark .ranking-title, html.dark .ranking-description, html.dark .radio-subtitle {\n    color: #e1e1e1;\n}\n\nhtml.dark .radio-disc {\n    box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2), inset 0 0 20px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(255, 255, 255, 0.8);\n    padding: 2px;\n}\n\nhtml.dark .ranking-container .rank-selector {\n    background: #2a2a2a;\n}\nhtml.dark .ranking-container .rank-chip {\n    background: #2a2a2a;\n    border:1px solid #333;\n}\nhtml.dark .ranking-container .rank-chip:hover {\n    background: #363636;\n}\n\nhtml.dark .ranking-container .ranking-item {\n    background: #2a2a2a!important;\n}\n\nhtml.dark .ranking-container .song-list {\n    scrollbar-color: #2a2a2a #1d1d1d;\n    background:linear-gradient(to right, rgba(100, 61, 73, 0.133), transparent)!important;\n    scrollbar-width: thin;\n}\n\nhtml.dark .ranking-container .song-list .song-item {\n    margin-bottom: 2px;\n}\n\nhtml.dark .queue-play-btn{\n    border: none !important;\n    background-color: rgba(0, 0, 0, 0.0)  !important;\n}\n\nhtml.dark .playlist-select-list li{\n    background-color: inherit;\n}\n\nhtml.dark .tab-button{\n    background-color: #121212!important;\n    border: none!important;\n}\n\nhtml.dark .search-tabs{\n    border-bottom: 1px solid #333;\n}\n\nhtml.dark .playlist-card , html.dark .album-card , html.dark .artist-card{\n    background-color: #1d1d1d;\n}\n\nhtml.dark .playlist-card .playlist-info .playlist-name , html.dark .album-card .album-info .album-name , html.dark .music-card .album-info, html.dark .artist-info .artist-name {\n    color: #e1e1e1;\n}\n\nhtml.dark .playlist-card .playlist-info .playlist-description {\n    color: #999;\n}\n\nhtml.dark .playlist-card .playlist-meta .meta-item , html.dark .album-card .album-meta .meta-item, html.dark .artist-card .artist-counts .count-item {\n    background-color: #1d1d1d;\n    border: 1px solid #333;\n}\n\nhtml.dark .artist-card .artist-avatar {\n    background: #1d1d1d;\n}\n\nhtml.dark .grid-skeleton .skeleton-grid .skeleton-grid-card  {\n    background: #1d1d1d;\n}\n\nhtml.dark .skeleton-grid-card .skeleton-avatar , html.dark .skeleton-grid-card .skeleton-line,html.dark .skeleton-grid-card .skeleton-cover {\n    background: #2a2a2a;\n}\n\nhtml.dark .skeleton-container .song-skeleton .skeleton-cover, html.dark .skeleton-container .song-skeleton .skeleton-line {\n    background: #1d1d1d;\n}\nhtml.dark .result-item {\n    border: none;\n}\nhtml.dark .result-item:hover {\n    background-color: #2a2a2a;\n}\n\nhtml.dark .modal {\n    background-color: #1d1d1d;\n}\n\nhtml.dark .settings-page .modal{\n    background-color: rgba(0, 0, 0, 0.6);\n}\n\nhtml.dark .shortcut-modal .shortcut-modal-content {\n    background-color: #1d1d1d;\n    color: #e1e1e1;\n}\nhtml.dark .shortcut-modal .shortcut-input{\n    background-color: #2a2a2a;\n}\n\nhtml.dark .shortcut-modal .shortcut-item {\n    border-bottom: 1px solid #333;\n}\n\nhtml.dark .scale-slider-container , html.dark .compatibility-option{\n    background-color: #2a2a2a;\n}\n\nhtml.dark .track-list-header-row{\n    color: #999;\n}\nhtml.dark .batch-actions-menu{\n    background-color: #1d1d1d;\n    border: none;\n    color: #999;\n}\nhtml.dark .batch-actions-menu li:hover{\n    background-color: #2a2a2a;\n    border-radius: 5px;\n}\n\n/* 登录页面暗黑模式样式 */\nhtml.dark .login-page {\n    background-color: #121212;\n}\n\nhtml.dark .login-container {\n    background-color: #1d1d1d;\n    border: 1px solid #333;\n    box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);\n}\n\nhtml.dark h2 {\n    color: #e1e1e1;\n}\n\nhtml.dark .form-input {\n    background-color: #2a2a2a;\n    border-color: #444;\n    color: #e1e1e1;\n}\n\nhtml.dark .form-input:focus {\n    border-color: var(--primary-color);\n    background-color: #333;\n}\n\nhtml.dark .clear-button {\n    color: #777;\n}\n\nhtml.dark .clear-button:hover {\n    color: #aaa;\n    background-color: rgba(255, 255, 255, 0.1);\n}\n\nhtml.dark .segmented-control {\n    border-color: #444;\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n}\n\nhtml.dark .segmented-button {\n    background: #2a2a2a;\n    color: #aaa;\n}\n\nhtml.dark .segmented-button:not(:last-child)::after {\n    background-color: #444;\n}\n\nhtml.dark .segmented-button:hover:not(.active) {\n    background-color: #333;\n    color: #e1e1e1;\n}\n\nhtml.dark .segmented-button.active {\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);\n}\n\nhtml.dark .disclaimer {\n    background-color: #2a2a2a;\n    border-top-color: #444;\n    color: #aaa;\n}\n\nhtml.dark .qr-login p {\n    color: #aaa;\n}\n\nhtml.dark .qr-code {\n    border-color: #444;\n    background-color: #2a2a2a;\n    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);\n}\n\nhtml.dark .empty-container, html.dark .extension-item {\n    background-color: #2a2a2a;\n    border-color: #444;\n    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);\n}\n\nhtml.dark .empty-text {\n    color: #aaa;\n}\n\nhtml.dark .register-link {\n    color: #aaa;\n}\n\nhtml.dark .register-link a:hover {\n    background-color: rgba(255, 255, 255, 0.1);\n}\n\nhtml.dark .primary-button, \nhtml.dark .append-button {\n    box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);\n}\n\nhtml.dark .primary-button:hover:not(:disabled), \nhtml.dark .append-button:hover:not(:disabled) {\n    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.4);\n}\n\nhtml.dark .primary-button:active:not(:disabled) {\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);\n}\n\nhtml.dark .sort-selector{\n    border: 1px solid #444;\n}\nhtml.dark .section-content{\n    color: #777;\n}\n"
  },
  {
    "path": "src/components/AlbumGrid.vue",
    "content": "<template>\n  <div class=\"album-grid\">\n    <div v-for=\"(album, index) in albums\" :key=\"index\" class=\"album-card\" @click=\"onAlbumClick(album)\">\n      <div class=\"album-cover\">\n        <img :src=\"(album.img || './assets/images/ico.png').replace('/240/','/480/')\"/>\n        <div class=\"album-overlay\">\n          <button class=\"play-button\">\n            <i class=\"fas fa-play\"></i>\n          </button>\n        </div>\n      </div>\n      <div class=\"album-info\">\n        <h3 class=\"album-name\" :title=\"album.albumname\">{{ album.albumname }}</h3>\n        <div class=\"album-artist\">\n          <span v-for=\"(singer, idx) in album.singers\" :key=\"idx\">\n            {{ singer.name }}{{ idx < album.singers.length - 1 ? '、' : '' }}\n          </span>\n        </div>\n        <div class=\"album-meta\">\n          <div class=\"meta-item\">\n            <i class=\"fas fa-calendar-alt\"></i>\n            <span>{{ album.publish_time }}</span>\n          </div>\n          <div class=\"meta-item\">\n            <i class=\"fas fa-music\"></i>\n            <span>{{ album.songcount }}首</span>\n          </div>\n          <div class=\"meta-item\" v-if=\"album.language\">\n            <i class=\"fas fa-globe\"></i>\n            <span>{{ album.language }}</span>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nconst props = defineProps({\n  albums: {\n    type: Array,\n    required: true,\n    default: () => []\n  }\n});\n\nconst emit = defineEmits(['album-click']);\n\nconst onAlbumClick = (album) => {\n  emit('album-click', album);\n};\n</script>\n\n<style scoped>\n.album-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n  gap: 20px;\n  margin-bottom: 20px;\n}\n\n.album-card {\n  display: flex;\n  flex-direction: column;\n  background-color: #fff;\n  border-radius: 10px;\n  overflow: hidden;\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);\n  transition: transform 0.3s, box-shadow 0.3s;\n  cursor: pointer;\n  height: 100%;\n}\n\n.album-card:hover {\n  transform: translateY(-5px);\n  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);\n}\n\n.album-cover {\n  position: relative;\n  width: 100%;\n  padding-top: 100%; /* 1:1 宽高比 */\n  overflow: hidden;\n}\n\n.album-cover img {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  transition: transform 0.5s;\n}\n\n.album-card:hover .album-cover img {\n  transform: scale(1.05);\n}\n\n.album-overlay {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-color: rgba(0, 0, 0, 0.3);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  opacity: 0;\n  transition: opacity 0.3s;\n}\n\n.album-card:hover .album-overlay {\n  opacity: 1;\n}\n\n.play-button {\n  width: 50px;\n  height: 50px;\n  border-radius: 50%;\n  background-color: var(--primary-color);\n  border: none;\n  color: white;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  cursor: pointer;\n  transition: transform 0.2s, background-color 0.2s;\n}\n\n.play-button:hover {\n  transform: scale(1.1);\n  background-color: var(--primary-color-dark, #d81e06);\n}\n\n.play-button i {\n  font-size: 20px;\n}\n\n.album-info {\n  padding: 15px;\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n}\n\n.album-name {\n  font-size: 16px;\n  font-weight: bold;\n  margin: 0 0 8px 0;\n  color: #333;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.album-artist {\n  font-size: 14px;\n  color: #666;\n  margin-bottom: 10px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.album-meta {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n  margin-top: auto;\n}\n\n.meta-item {\n  display: flex;\n  align-items: center;\n  gap: 5px;\n  font-size: 12px;\n  color: #888;\n  background-color: #f5f5f5;\n  padding: 4px 8px;\n  border-radius: 4px;\n}\n\n.meta-item i {\n  font-size: 12px;\n  color: var(--primary-color);\n}\n\n/* 响应式调整 */\n@media (max-width: 768px) {\n  .album-grid {\n    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n    gap: 15px;\n  }\n  \n  .album-name {\n    font-size: 14px;\n  }\n  \n  .album-artist {\n    font-size: 12px;\n  }\n  \n  .meta-item {\n    font-size: 10px;\n    padding: 3px 6px;\n  }\n}\n</style>"
  },
  {
    "path": "src/components/ArtistGrid.vue",
    "content": "<template>\n  <div class=\"artist-grid\">\n    <div v-for=\"(artist, index) in artists\" :key=\"index\" class=\"artist-card\" @click=\"onArtistClick(artist)\" :style=\"{'--artist-bg': `url(${artist.Avatar || './assets/images/ico.png'})`}\">\n      <div class=\"artist-avatar\">\n        <img :src=\"(artist.Avatar || './assets/images/ico.png').replace('/240/','/480/')\"/>\n      </div>\n      <div class=\"artist-info\">\n        <h3 class=\"artist-name\">{{ artist.AuthorName }}</h3>\n        <div class=\"artist-stats\">\n          <div class=\"stat-item\">\n            <i class=\"fas fa-fire\"></i>\n            <span>{{ artist.Heat }}</span>\n          </div>\n          <div class=\"stat-item\">\n            <i class=\"fas fa-users\"></i>\n            <span>{{ artist.FansNum }}</span>\n          </div>\n        </div>\n        <div class=\"artist-counts\">\n          <div class=\"count-item\">专辑: {{ artist.AlbumCount }}</div>\n          <div class=\"count-item\">单曲: {{ artist.AudioCount }}</div>\n          <div class=\"count-item\">视频: {{ artist.VideoCount }}</div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nconst props = defineProps({\n  artists: {\n    type: Array,\n    required: true,\n    default: () => []\n  }\n});\n\nconst emit = defineEmits(['artist-click']);\n\nconst onArtistClick = (artist) => {\n  emit('artist-click', artist);\n};\n</script>\n\n<style scoped>\n.artist-grid {\n  display: grid;\n  grid-template-columns: repeat(5, 1fr);\n  gap: 15px;\n  margin-bottom: 20px;\n}\n\n.artist-card {\n  display: flex;\n  flex-direction: column;\n  background-color: #fff;\n  border-radius: 10px;\n  overflow: hidden;\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);\n  transition: transform 0.3s, box-shadow 0.3s;\n  cursor: pointer;\n  position: relative;\n}\n\n.artist-avatar {\n  width: 100%;\n  height: 180px;\n  overflow: hidden;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: #f5f5f5;\n  position: relative;\n}\n\n.artist-avatar::before {\n  content: '';\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-image: var(--artist-bg);\n  background-size: cover;\n  background-position: center;\n  opacity: 0.2;\n  filter: blur(10px);\n  z-index: 0;\n}\n\n.artist-avatar:hover::before {\n  opacity: 0.15;\n}\n\n.artist-avatar, .artist-info {\n  position: relative;\n  z-index: 1;\n}\n\n.artist-card:hover {\n  transform: translateY(-5px);\n  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);\n}\n\n.artist-avatar {\n  width: 100%;\n  height: 180px;\n  overflow: hidden;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background-color: #f5f5f5;\n}\n\n.artist-avatar img {\n  width: 150px;\n  height: 150px;\n  object-fit: cover;\n  transition: transform 0.5s;\n  border-radius: 50%; /* 将头像改为圆形 */\n}\n\n.artist-card:hover .artist-avatar img {\n  transform: scale(1.05);\n}\n\n.artist-info {\n  padding: 15px;\n}\n\n.artist-name {\n  font-size: 18px;\n  font-weight: bold;\n  margin: 0 0 10px 0;\n  color: #333;\n  text-align: center;\n}\n\n.artist-stats {\n  display: flex;\n  gap: 15px;\n  margin-bottom: 10px;\n  justify-content: center;\n}\n\n.stat-item {\n  display: flex;\n  align-items: center;\n  gap: 5px;\n  color: #666;\n  font-size: 14px;\n}\n\n.stat-item i {\n  color: var(--primary-color);\n}\n\n.artist-counts {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n  margin-top: 10px;\n  justify-content: center;\n}\n\n.count-item {\n  background-color: #f5f5f5;\n  padding: 4px 8px;\n  border-radius: 4px;\n  font-size: 12px;\n  color: #666;\n}\n</style>"
  },
  {
    "path": "src/components/BirthdayEasterEgg.vue",
    "content": "<template>\n    <span v-if=\"isBirthdayToday\" class=\"birthday-badge\">\n        <i class=\"fas fa-birthday-cake\"></i>\n        生日快乐\n    </span>\n\n    <teleport to=\"body\">\n        <div v-if=\"show\" class=\"birthday-fullscreen\" aria-hidden=\"true\">\n            <canvas ref=\"confettiCanvas\" class=\"birthday-confetti-canvas\"></canvas>\n            <div class=\"ribbon-wrapper\">\n                <div class=\"ribbon-fold-left\"></div>\n                <div class=\"ribbon-fold-right\"></div>\n                <div class=\"ribbon\">\n                    <h1><span v-if=\"nickname\">{{ nickname }},</span>生日快乐</h1>\n                    <p>愿你拥有美好的一天！</p>\n                </div>\n            </div>\n        </div>\n    </teleport>\n</template>\n\n<script setup>\nimport { computed, onBeforeUnmount, ref, watch } from 'vue';\n\nconst props = defineProps({\n    birthday: String,\n    nickname: String,\n    playerControl: Object,\n    songHash: {\n        type: String,\n        default: '0F41D9534CA951FD50E98D187FD2F3BD',\n    },\n});\n\nconst show = ref(false);\nconst confettiCanvas = ref(null);\nlet hideTimer = null;\nlet confettiInstance = null;\n\nconst confettiCdnUrl = 'https://cdn.jsdelivr.net/npm/canvas-confetti@1.6.0/dist/confetti.browser.min.js';\n\nconst parseMonthDay = (birthday) => {\n    if (!birthday || typeof birthday !== 'string') return null;\n    const parts = birthday.trim().slice(0, 10).split('-');\n    if (parts.length < 3) return null;\n    const month = Number(parts[1]);\n    const day = Number(parts[2]);\n    if (!Number.isFinite(month) || !Number.isFinite(day)) return null;\n    if (month < 1 || month > 12) return null;\n    if (day < 1 || day > 31) return null;\n    return { month, day };\n};\n\nconst isBirthdayToday = computed(() => {\n    const md = parseMonthDay(props.birthday);\n    if (!md) return false;\n    const now = new Date();\n    return now.getMonth() + 1 === md.month && now.getDate() === md.day;\n});\n\nconst loadConfetti = () => {\n    if (typeof window === 'undefined') return Promise.resolve(null);\n    if (window.confetti) return Promise.resolve(window.confetti);\n\n    const existing = document.querySelector('script[data-canvas-confetti=\"1\"]');\n    if (existing) {\n        return new Promise((resolve, reject) => {\n            existing.addEventListener('load', () => resolve(window.confetti), { once: true });\n            existing.addEventListener('error', () => reject(new Error('Failed to load canvas-confetti')), { once: true });\n        });\n    }\n\n    return new Promise((resolve, reject) => {\n        const script = document.createElement('script');\n        script.src = confettiCdnUrl;\n        script.async = true;\n        script.defer = true;\n        script.dataset.canvasConfetti = '1';\n        script.onload = () => resolve(window.confetti);\n        script.onerror = () => reject(new Error('Failed to load canvas-confetti'));\n        document.head.appendChild(script);\n    });\n};\n\nconst fireConfettiFromBottom = async () => {\n    const confetti = await loadConfetti();\n    if (!confettiCanvas.value || typeof confetti?.create !== 'function') return;\n\n    if (!confettiInstance) {\n        confettiInstance = confetti.create(confettiCanvas.value, { resize: true, useWorker: true });\n    }\n\n    const base = {\n        origin: { x: 0.5, y: 1 },\n        angle: 90,\n        spread: 70,\n        startVelocity: 55,\n        gravity: 1,\n        ticks: 240,\n        scalar: 1,\n    };\n\n    confettiInstance({ ...base, particleCount: 160 });\n    window.setTimeout(() => confettiInstance?.({ ...base, particleCount: 110, spread: 95, startVelocity: 48 }), 170);\n    window.setTimeout(() => confettiInstance?.({ ...base, particleCount: 80, spread: 115, startVelocity: 40, gravity: 1.15 }), 360);\n};\n\nconst tryAutoPlay = async () => {\n    const add = props.playerControl?.addSongToQueue;\n    if (typeof add !== 'function') return;\n    try {\n        await add(props.songHash, `祝${props.nickname||'你'}生日快乐 🎉`, './assets/images/ico.png');\n    } catch {\n    }\n};\n\nconst playIntroOnce = async () => {\n    if (!isBirthdayToday.value) return;\n\n    const key = `birthday_egg_intro`;\n    if (localStorage.getItem(key)) return;\n    localStorage.setItem(key, '1');\n\n    show.value = true;\n    fireConfettiFromBottom();\n    await tryAutoPlay();\n\n    if (hideTimer) window.clearTimeout(hideTimer);\n    hideTimer = window.setTimeout(() => {\n        show.value = false;\n    }, 6500);\n};\n\nwatch(() => props.birthday, playIntroOnce, { immediate: true });\n\nonBeforeUnmount(() => {\n    if (hideTimer) window.clearTimeout(hideTimer);\n    confettiInstance?.reset?.();\n});\n</script>\n\n<style scoped>\n.birthday-badge {\n    display: inline-flex;\n    align-items: center;\n    gap: 6px;\n    margin-left: 10px;\n    padding: 2px 10px;\n    border-radius: 999px;\n    font-size: 12px;\n    line-height: 1;\n    color: #fff;\n    user-select: none;\n    background: linear-gradient(90deg, rgba(255, 77, 77, 0.35), rgba(255, 214, 10, 0.35), rgba(96, 165, 250, 0.35));\n    border: 1px solid rgba(255, 255, 255, 0.35);\n    backdrop-filter: blur(6px);\n    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);\n}\n\n.birthday-fullscreen {\n    position: fixed;\n    inset: 0;\n    z-index: 9998;\n    pointer-events: none;\n    background:\n        radial-gradient(circle at 20% 20%, rgba(255, 255, 255, 0.75), transparent 45%),\n        radial-gradient(circle at 80% 30%, rgba(255, 255, 255, 0.70), transparent 40%),\n        radial-gradient(circle at 40% 80%, rgba(255, 255, 255, 0.65), transparent 45%),\n        linear-gradient(180deg, rgba(163, 214, 255, 0.45), rgba(255, 255, 255, 0.20));\n    animation: birthdayFade 6500ms ease-in-out forwards;\n}\n\n.birthday-confetti-canvas {\n    position: absolute;\n    inset: 0;\n    width: 100%;\n    height: 100%;\n    pointer-events: none;\n}\n\n.ribbon-wrapper {\n    position: absolute;\n    left: 50%;\n    top: 50%;\n    transform: translate(-50%, -50%);\n    z-index: 2;\n}\n\n.ribbon {\n    background: linear-gradient(to bottom, #ffb3c7 0%, #ffe3ec 55%, #fff 100%);\n    padding: 15px 60px;\n    position: relative;\n    z-index: 2;\n    box-shadow: 0 10px 28px rgba(0, 0, 0, 0.16);\n    border-radius: 8px;\n    text-align: center;\n    transform: perspective(700px) rotateX(6deg);\n    animation: ribbonIn 520ms cubic-bezier(0.2, 0.9, 0.2, 1);\n}\n\n.ribbon h1 {\n    margin: 0;\n    color: #ff4d6d;\n    font-size: 2.4rem;\n    font-weight: 800;\n    letter-spacing: 3px;\n    text-shadow: 1px 1px 0 rgba(255, 238, 245, 0.95);\n}\n\n.ribbon p {\n    margin: 0;\n    margin-top: -6px;\n    color: #ff7fa0;\n    font-size: 1rem;\n    font-family: \"Arial\", sans-serif;\n    font-weight: 700;\n}\n\n.ribbon::before,\n.ribbon::after {\n    content: \"\";\n    position: absolute;\n    top: 60%;\n    height: 100%;\n    width: 80px;\n    background: linear-gradient(to bottom, #ffb3c7 0%, #ffe3ec 55%, #fff 100%);\n    z-index: -1;\n    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 20% 50%);\n    box-shadow: 0 10px 22px rgba(0, 0, 0, 0.12);\n}\n\n.ribbon::before {\n    left: -40px;\n    transform: translateY(-60%) rotate(-10deg);\n    border-right: none;\n}\n\n.ribbon::after {\n    right: -40px;\n    transform: translateY(-60%) scaleX(-1) rotate(-10deg);\n    border-left: none;\n}\n\n.ribbon-fold-left,\n.ribbon-fold-right {\n    position: absolute;\n    top: 100%;\n    width: 0;\n    height: 0;\n    border-style: solid;\n    z-index: 0;\n}\n\n.ribbon-fold-left {\n    left: -8px;\n    border-width: 0 15px 15px 0;\n    border-color: transparent rgba(0, 0, 0, 0.16) transparent transparent;\n    top: 100%;\n}\n\n.ribbon-fold-right {\n    right: -8px;\n    border-width: 15px 15px 0 0;\n    border-color: rgba(0, 0, 0, 0.16) transparent transparent transparent;\n    top: 100%;\n}\n\n@keyframes ribbonIn {\n    0% { transform: perspective(700px) rotateX(6deg) translateY(-18px); opacity: 0; }\n    100% { transform: perspective(700px) rotateX(6deg) translateY(0); opacity: 1; }\n}\n\n@keyframes birthdayFade {\n    0% { opacity: 0; }\n    10% { opacity: 1; }\n    85% { opacity: 1; }\n    100% { opacity: 0; }\n}\n</style>\n"
  },
  {
    "path": "src/components/ContextMenu.vue",
    "content": "<template>\n    <div v-if=\"showContextMenu\" :style=\"{ top: `${menuPosition.y}px`, left: `${menuPosition.x}px` }\"\n        class=\"context-menu\">\n        <ul>\n            <li @mouseenter=\"fetchPlaylists\" @mouseleave=\"hideSubMenu\">\n                <i class=\"fa-solid fa-plus\"></i>\n                {{ MoeAuth.isAuthenticated ? $t('tian-jia-ge-dan') : $t('qing-xian-deng-lu') }} <i\n                    class=\"fa-solid fa-chevron-right\"></i>\n                <ul v-if=\"MoeAuth.isAuthenticated && showSubMenu\" class=\"submenu\">\n                    <li v-for=\"playlist in playlists\" :key=\"playlist.listid\"\n                        @click=\"addToPlaylist(playlist.listid, contextSong)\">\n                        {{ playlist.name }}\n                    </li>\n                </ul>\n            </li>\n            <li v-if=\"contextSong.mvhash\" @click=\"playMV(contextSong.mvhash)\"><i class=\"fa-solid fa-video\"></i> 播放MV</li>\n            <li @click=\"shareSong(contextSong)\"><i class=\"fa-solid fa-share-nodes\"></i> 分享</li>\n            <li v-if=\"MoeAuth.isAuthenticated && listId && contextSong.userid === MoeAuth.UserInfo.userid\" @click=\"cancel()\"><i class=\"fa-solid fa-heart\"></i> 取消收藏</li>\n            <li v-if=\"MoeAuth.isAuthenticated\" @click=\"addToNext(contextSong)\"><i class=\"fa-solid fa-arrow-right\"></i> 添加到下一首</li>\n        </ul>\n    </div>\n</template>\n\n<script setup>\nimport { ref, onMounted, onBeforeUnmount } from 'vue';\nimport { useRouter } from 'vue-router';\nimport { get } from '../utils/request';\nimport { MoeAuthStore } from '../stores/store';\nimport i18n from '@/utils/i18n';\nimport { share } from '@/utils/utils';\n\nconst router = useRouter();\nconst MoeAuth = MoeAuthStore();\nconst showContextMenu = ref(false);\nconst showSubMenu = ref(false);\nconst menuPosition = ref({ x: 0, y: 0 });\nconst playlists = ref([]);\nconst listId = ref(0);\nconst contextSong = ref(null);\nlet events;\n// 右键菜单显示与隐藏\nconst openContextMenu = (event, song, Id) => {\n    events = event\n    event.preventDefault();\n    showContextMenu.value = true;\n    listId.value = Id;\n    menuPosition.value = { x: event.clientX, y: event.clientY };\n    contextSong.value = song;\n};\nconst hideContextMenu = () => {\n    showContextMenu.value = false;\n    showSubMenu.value = false;\n};\n// 获取歌单列表\nconst fetchPlaylists = async () => {\n    if(!MoeAuth.isAuthenticated) return;\n    showSubMenu.value = true;\n    try {\n        const playlistResponse = await get('/user/playlist',{\n            pagesize:100\n        });\n        if (playlistResponse.status === 1) {\n            playlists.value = playlistResponse.data.info.filter(playlist => playlist.list_create_userid === MoeAuth.UserInfo.userid);\n        }\n    } catch (error) {\n        $message.error(i18n.global.t('huo-qu-ge-dan-shi-bai'));\n    }\n};\n\n// 分享歌曲功能\nconst shareSong = (song) => {\n    if (!song) return;\n    share(song.OriSongName, song.FileHash);\n    hideContextMenu();\n};\n\n// 添加到歌单功能\nconst addToPlaylist = async (listid, song) => {\n    try {\n        await get(`/playlist/tracks/add?listid=${listid}&data=${encodeURIComponent(song.OriSongName.replace(',', ''))}|${song.FileHash}`);\n        hideContextMenu();\n        $message.success(i18n.global.t('cheng-gong-tian-jia-dao-ge-dan'));\n    } catch (error) {\n        $message.error(i18n.global.t('tian-jia-dao-ge-dan-shi-bai'))\n    }\n};\n// 取消收藏功能\nconst cancel = async () => {\n    try {\n        await get(`/playlist/tracks/del?listid=${listId.value}&fileids=${contextSong.value.fileid}`);\n        emit('songRemoved', contextSong.value.fileid);\n        hideContextMenu();\n        $message.success(i18n.global.t('cheng-gong-qu-xiao-shou-cang'));\n    } catch (error) {\n        $message.error(i18n.global.t('qu-xiao-shou-cang-shi-bai'))\n    }\n};\n\nconst props = defineProps({\n    playerControl: Object\n});\n\nconst emit = defineEmits(['songRemoved']);\n\nconst addToNext = async (song) => {\n    let songNameParts = song?.OriSongName.split(' - ');\n    props.playerControl.addToNext(song.FileHash, songNameParts[1], song.cover, songNameParts[0], song.timeLength);\n    $message.success(i18n.global.t('tian-jia-cheng-gong'))\n    hideContextMenu();\n};\n\nconst hideSubMenu = () => {\n    showSubMenu.value = false;\n};\n\n// 播放MV\nconst playMV = async (mvhash) => {\n    try {\n        hideContextMenu();\n        props.playerControl?.pause?.();\n        const title = contextSong.value?.OriSongName || '视频播放';\n\n        const resolved = router.resolve({\n            path: '/video',\n            query: { hash: mvhash, title }\n        });\n        const base = window.location.href.split('#')[0];\n        const href = resolved.href || '';\n        const fullUrl = href.startsWith('#')\n            ? `${base}${href}`\n            : `${base}#${href.startsWith('/') ? href : `/${href}`}`;\n\n        if (window.electronAPI) {\n            await window.electronAPI.openMvWindow(fullUrl);\n        } else {\n            const width = 960;\n            const height = 620;\n            const left = Math.max(0, Math.round((window.screen.width - width) / 2));\n            const top = Math.max(0, Math.round((window.screen.height - height) / 2));\n            const features = `popup=yes,width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=no`;\n\n            const popup = window.open(fullUrl, 'moekoe-mv', features);\n            if (popup) {\n                popup.focus?.();\n            } else {\n                await router.push(resolved);\n            }\n        }\n    } catch (error) {\n        $message.error('打开视频播放器失败');\n    }\n};\n\nconst handleClickOutside = (event) => {\n    if (!event.target.closest(\".context-menu\")) {\n        hideContextMenu();\n    }\n};\nonMounted(() => {\n    document.addEventListener('click', handleClickOutside);\n    document.addEventListener('scroll', hideContextMenu);\n});\nonBeforeUnmount(() => {\n    document.removeEventListener('click', handleClickOutside);\n    document.removeEventListener('scroll', hideContextMenu);\n});\n\ndefineExpose({ openContextMenu }); \n</script>\n\n<style scoped>\n.context-menu {\n    position: fixed;\n    background-color: white;\n    border: 1px solid #ddd;\n    border-radius: 10px;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);\n    z-index: 1000;\n}\n\n.context-menu ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n}\n\n.context-menu li {\n    padding: 8px 14px;\n    cursor: pointer;\n    position: relative;\n    border-radius: 10px;\n}\n\n.context-menu li:hover {\n    background-color: var(--background-color)\n}\n\n.submenu {\n    position: absolute;\n    left: 100%;\n    top: 0;\n    background-color: white;\n    border: 1px solid #ddd;\n    border-radius: 10px;\n    box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);\n    padding: 5px 0;\n}\n\n.submenu li {\n    width: 150px;\n}\n</style>\n"
  },
  {
    "path": "src/components/CustomModal.vue",
    "content": "<template>\n    <div>\n        <!-- Alert 模态框 -->\n        <div v-if=\"showAlert\" class=\"modal-overlay\">\n            <div class=\"modal\">\n                <h3>{{ alertMessage }}</h3>\n                <button @click=\"confirmAlert\" class=\"btn\">{{ i18n.global.t('que-ding') }}</button>\n            </div>\n        </div>\n\n        <!-- Confirm 模态框 -->\n        <div v-if=\"showConfirm\" class=\"modal-overlay\">\n            <div class=\"modal\">\n                <h3>{{ confirmMessage }}</h3>\n                <div class=\"buttons\">\n                    <button @click=\"confirmAction(true)\" class=\"btn\">{{ i18n.global.t('que-ding') }}</button>\n                    <button @click=\"confirmAction(false)\" class=\"btn\">{{ i18n.global.t('qu-xiao') }}</button>\n                </div>\n            </div>\n        </div>\n\n        <!-- Prompt 模态框 -->\n        <div v-if=\"showPrompt\" class=\"modal-overlay\">\n            <div class=\"modal\">\n                <h3>{{ promptMessage }}</h3>\n                <input type=\"text\" v-model=\"promptInput\" class=\"prompt-input\" />\n                <div class=\"buttons\">\n                    <button @click=\"submitPrompt\" class=\"btn\">{{ i18n.global.t('que-ding') }}</button>\n                    <button @click=\"closePrompt\" class=\"btn\">{{ i18n.global.t('qu-xiao') }}</button>\n                </div>\n            </div>\n        </div>\n\n        <!-- Loading 遮罩 -->\n        <div v-if=\"showLoading\" class=\"loading-overlay\">\n            <div class=\"loading-spinner\"></div>\n            <p class=\"loading-text\">{{ i18n.global.t('shao-nv-qi-dao-zhong') }}</p>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref } from 'vue';\nimport i18n from '@/utils/i18n';\n// 该组件代码来自萌音商城(MoeKoe.cn) © 阿珏酱\n// window.$modal.alert('这是一个 Alert'); // 直接调用 window.$modal\n// const result = await window.$modal.confirm('这是一个 Confirm');\n// const result = await window.$modal.prompt('请输入内容：', '默认值');\n// window.$modal.showLoading(); //开启\n// window.$modal.hideLoading(); //关闭\n\n// 控制模态框的状态\nconst showAlert = ref(false);\nconst showConfirm = ref(false);\nconst showPrompt = ref(false);\nconst showLoading = ref(false);\n\n// 消息内容\nconst alertMessage = ref('');\nconst confirmMessage = ref('');\nconst promptMessage = ref('');\nconst promptInput = ref('');\n\n// Alert 方法\nlet alertResolve;\nconst customAlert = (message) => {\n    alertMessage.value = message;\n    showAlert.value = true;\n    return new Promise((resolve) => {\n        alertResolve = resolve;\n    });\n};\n\nconst confirmAlert = () => {\n    showAlert.value = false;\n    alertResolve(); // 在点击确定按钮时，执行 resolve 以继续后续代码\n};\n\n// Confirm 方法\nlet confirmResolve;\nconst customConfirm = (message) => {\n    confirmMessage.value = message;\n    showConfirm.value = true;\n    return new Promise((resolve) => {\n        confirmResolve = resolve;\n    });\n};\n\nconst confirmAction = (confirmed) => {\n    showConfirm.value = false;\n    confirmResolve(confirmed);\n};\n\n// Prompt 方法\nlet promptResolve;\nconst customPrompt = (message, defaultValue = '') => {\n    promptMessage.value = message;\n    promptInput.value = defaultValue;\n    showPrompt.value = true;\n    return new Promise((resolve) => {\n        promptResolve = resolve;\n    });\n};\n\nconst submitPrompt = () => {\n    showPrompt.value = false;\n    promptResolve(promptInput.value);\n};\n\nconst closePrompt = () => {\n    showPrompt.value = false;\n};\n\n// Loading 方法\nconst showCustomLoading = () => {\n    showLoading.value = true;\n};\n\nconst hideCustomLoading = () => {\n    showLoading.value = false;\n};\n\n// 暴露方法供父组件使用\ndefineExpose({\n    customAlert,\n    customConfirm,\n    customPrompt,\n    showCustomLoading,\n    hideCustomLoading\n});\n</script>\n\n<style scoped>\n.modal-overlay {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background-color: rgba(0, 0, 0, 0.5);\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    z-index: 9999999;\n}\n\n.modal {\n    background-color: white;\n    padding: 30px;\n    border-radius: 10px;\n    text-align: center;\n    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n    width: 400px;\n}\n\n.modal h3{\n    overflow-wrap: anywhere;\n    color: var(--primary-color);\n}\n\n.buttons {\n    display: flex;\n    justify-content: space-around;\n    margin-top: 15px;\n}\n\n.prompt-input {\n    width: 379px;\n    padding: 10px;\n    margin-top: 15px;\n    border-radius: 5px;\n    border: 1px solid var(--primary-color);\n}\n\n.btn {\n    background-color: var(--primary-color);\n    color: white;\n    border: none;\n    padding: 10px 20px;\n    border-radius: 25px;\n    cursor: pointer;\n    transition: all 0.3s;\n    margin-top: 10px;\n    font-size: 20px;\n    width: auto;\n}\n\n\n.loading-overlay {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background-color: rgba(0, 0, 0, 0.5);\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    z-index: 9999;\n    flex-direction: column;\n}\n\n.loading-spinner {\n    border: 8px solid rgba(255, 255, 255, 0.3);\n    border-top: 8px solid var(--primary-color);\n    border-radius: 50%;\n    width: 40px;\n    height: 40px;\n    animation: spin 1s linear infinite;\n}\n/* 文字样式 */\n.loading-text {\n    margin-top: 11px;\n    font-size: 1.1rem;\n    color: #ff85a2;\n    font-weight: bold;\n    font-family: 'Poppins', sans-serif;\n    text-align: center;\n    letter-spacing: 1px;\n    margin-left: 24px;\n}\n\n@keyframes spin {\n    0% {\n        transform: rotate(0deg);\n    }\n\n    100% {\n        transform: rotate(360deg);\n    }\n}\n</style>"
  },
  {
    "path": "src/components/Disclaimer.vue",
    "content": "<template>\n    <div v-if=\"showModal\" class=\"modal-overlay\">\n        <div class=\"modal-content\">\n            <h2>{{ $t('yong-hu-tiao-kuan') }}</h2>\n            <p>{{ $t('1-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan') }}</p>\n            <p>{{ $t('2-ben-xiang-mu-jin-gong-xue-xi-jiao-liu-shi-yong-nin-zai-shi-yong-guo-cheng-zhong-ying-zun-zhong-ban-quan-bu-de-yong-yu-shang-ye-huo-fei-fa-yong-tu') }}</p>\n            <p>{{ $t('3-zai-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-sheng-cheng-ban-quan-nei-rong-ben-xiang-mu-bu-yong-you-zhe-xie-ban-quan-nei-rong-de-suo-you-quan-wei-le-bi-mian-qin-quan-hang-wei-nin-xu-zai-24-xiao-shi-nei-qing-chu-you-ben-xiang-mu-chan-sheng-de-ban-quan-nei-rong') }}</p>\n            <p>{{ $t('4-ben-xiang-mu-de-kai-fa-zhe-bu-dui-yin-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-suo-dao-zhi-de-ren-he-sun-hai-cheng-dan-ze-ren-bao-kuo-dan-bu-xian-yu-shu-ju-diu-shi-ting-gong-ji-suan-ji-gu-zhang-huo-qi-ta-jing-ji-sun-shi') }}</p>\n            <p>{{ $t('5-nin-bu-de-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-yin-wei-fan-fa-lv-fa-gui-suo-dao-zhi-de-ren-he-fa-lv-hou-guo-you-yong-hu-cheng-dan') }}</p>\n            <p>{{ $t('6-ben-xiang-mu-jin-yong-yu-ji-shu-tan-suo-he-yan-jiu-bu-jie-shou-ren-he-shang-ye-he-zuo-guang-gao-huo-juan-zeng-ru-guo-guan-fang-yin-le-ping-tai-dui-ci-xiang-mu-cun-you-yi-lv-ke-sui-shi-lian-xi-kai-fa-zhe-yi-chu-xiang-guan-nei-rong') }}</p>\n            <p>{{ $t('tong-yi-ji-xu-shi-yong-ben-xiang-mu-nin-ji-jie-shou-yi-shang-tiao-kuan-sheng-ming-nei-rong') }}</p>\n            <div class=\"button-group\">\n                <button @click=\"agree\">{{ $t('tong-yi') }}</button>\n                <button @click=\"disagree\">{{ $t('bu-tong-yi') }}</button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref, onMounted } from 'vue';\n\nconst showModal = ref(false);\n\nonMounted(() => {\n    if(isElectron()){\n        window.electron.ipcRenderer.on('show-disclaimer', () => {\n            showModal.value = true;\n        });\n        return\n    }\n    if(!localStorage.getItem('disclaimerAccepted')){\n        showModal.value = true;\n    }\n});\nconst isElectron = () => {\n    return typeof window !== 'undefined' && typeof window.electron !== 'undefined';\n};\nconst agree = () => {\n    showModal.value = false;\n    if(isElectron()){\n        window.electron.ipcRenderer.send('disclaimer-response', true);\n        return;\n    }\n    localStorage.setItem('disclaimerAccepted', true);\n};\n\nconst disagree = () => {\n    if(isElectron()){\n        window.electron.ipcRenderer.send('disclaimer-response', false);\n    }\n    window.close();\n};\n</script>\n\n<style scoped>\n.modal-overlay {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background: rgba(0, 0, 0, 0.6);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    z-index: 1000;\n}\n\n.modal-content {\n    background: #fff;\n    padding: 20px;\n    border-radius: 8px;\n    max-width: 750px;\n    width: 80%;\n}\n\n.button-group {\n    display: flex;\n    justify-content: space-around;\n    margin-top: 15px;\n}\n\nbutton {\n    padding: 10px 20px;\n    border: none;\n    cursor: pointer;\n    border-radius: 5px;\n    background-color:var(--primary-color);\n    color: white;\n}\n\n</style>"
  },
  {
    "path": "src/components/ExtensionManager.vue",
    "content": "<template>\n    <template v-if=\"isElectron()\">\n        <div class=\"extensions-toolbar\">\n            <div class=\"extensions-tabs\">\n                <button class=\"tab-btn\" :class=\"{ active: currentView === 'installed' }\" @click=\"currentView = 'installed'\">\n                    已安装插件\n                </button>\n                <button class=\"tab-btn\" :class=\"{ active: currentView === 'market' }\" @click=\"currentView = 'market'\">\n                    插件市场\n                </button>\n            </div>\n\n            <div v-if=\"currentView === 'installed'\" class=\"extensions-actions\">\n                <button @click=\"refreshExtensions(true)\" class=\"extension-btn primary\" :disabled=\"extensionsLoading\">\n                    <i class=\"fas fa-sync-alt\"></i>\n                    {{ extensionsLoading ? t('jia-zai-zhong') : t('shua-xin-cha-jian') }}\n                </button>\n                <button @click=\"openExtensionsDir\" class=\"extension-btn secondary\">\n                    <i class=\"fas fa-folder-open\"></i>\n                    {{ t('da-kai-cha-jian-mu-lu') }}\n                </button>\n                <button @click=\"installPlugin\" class=\"extension-btn success\" :disabled=\"extensionsLoading\">\n                    <i class=\"fas fa-upload\"></i>\n                    {{ t('an-zhuang-cha-jian') }}\n                </button>\n            </div>\n\n            <div v-else class=\"market-actions\">\n                <div class=\"market-search\">\n                    <i class=\"fas fa-search\"></i>\n                    <input v-model.trim=\"marketSearch\" type=\"text\" placeholder=\"搜索插件名称、作者或描述\" />\n                </div>\n                <button @click=\"fetchMarketPlugins(true)\" class=\"extension-btn primary\" :disabled=\"marketLoading\">\n                    <i class=\"fas fa-rotate-right\"></i>\n                    {{ marketLoading ? '加载中' : '刷新市场' }}\n                </button>\n                <button @click=\"openPluginsRepo\" class=\"extension-btn secondary\">\n                    <i class=\"fas fa-arrow-up-right-from-square\"></i>\n                    上架&举报\n                </button>\n            </div>\n        </div>\n\n        <div v-if=\"currentView === 'installed'\">\n            <div v-if=\"!extensionsLoading && extensions.length > 0\" class=\"extensions-list\">\n                <div v-for=\"extension in extensions\" :key=\"extension.id\" class=\"market-item installed-item\">\n                    <div class=\"market-item-header\">\n                        <div class=\"market-title-group\">\n                            <div class=\"extension-icon\">\n                            <img\n                                v-if=\"extension.iconData\"\n                                :src=\"extension.iconData\"\n                                :alt=\"extension.name\"\n                                @error=\"handleIconError\"\n                                class=\"extension-icon-img\"\n                            />\n                            <i v-else class=\"fas fa-puzzle-piece\"></i>\n                            </div>\n                            <div class=\"market-title-text\">\n                                <h4>{{ extension.name }}</h4>\n                                <p>{{ extension.description || '暂无描述' }}</p>\n                            </div>\n                        </div>\n                        <div class=\"market-status-group\">\n                            <span class=\"market-badge installed\">已安装</span>\n                            <button @click=\"openExtensionPopup(extension.id, extension.name)\" class=\"extension-btn secondary\" :disabled=\"extensionsLoading\">\n                                <i class=\"fas fa-up-right-from-square\"></i>\n                                {{ t('da-kai-tan-chuang') }}\n                            </button>\n                            <button @click=\"uninstallExtension(extension.id, extension.name, extension.directory)\" class=\"extension-btn danger\" :disabled=\"extensionsLoading\">\n                                <i class=\"fas fa-trash\"></i>\n                                {{ t('xie-zai') }}\n                            </button>\n                        </div>\n                    </div>\n\n                    <div class=\"market-meta\">\n                        <span>{{ t('ban-ben') }} {{ extension.version || '未知' }}</span>\n                        <span class=\"author-meta\">\n                            作者\n                            <a :href=\"extension.authorUrl || 'javascript:void(0)'\" :target=\"extension.authorUrl ? '_blank' : '_self'\" rel=\"noopener noreferrer\">\n                                {{ extension.author || '未知' }}\n                            </a>\n                        </span>\n                        <span>ID {{ extension.pluginId || extension.id }}</span>\n                    </div>\n\n                    <p v-if=\"!extension.moeKoeAdapted\" class=\"extension-compatibility-warning installed-warning\">\n                        <i class=\"fas fa-exclamation-triangle\" aria-hidden=\"true\"></i>\n                        <span>该插件未对萌音适配，可能存在兼容性问题</span>\n                    </p>\n                    <p v-if=\"isCurrentAppVersionLowerThanMin(extension.minversion)\" class=\"extension-compatibility-warning installed-warning\">\n                        <i class=\"fas fa-exclamation-triangle\" aria-hidden=\"true\"></i>\n                        <span>当前萌音版本较低，插件最低支持 V{{ extension.minversion }}</span>\n                    </p>\n                </div>\n            </div>\n\n            <div v-else-if=\"!extensionsLoading && extensions.length === 0\" class=\"extensions-empty\">\n                <div class=\"empty-icon\">\n                    <i class=\"fas fa-puzzle-piece\"></i>\n                </div>\n                <h4>{{ t('zan-wu-cha-jian') }}</h4>\n                <p>{{ t('jiang-cha-jian-wen-jian-jia-fang-ru-cha-jian-mu-lu') }}</p>\n            </div>\n\n            <div v-if=\"extensionsLoading\" class=\"extensions-loading\">\n                <i class=\"fas fa-spinner fa-spin\"></i>\n                <p>{{ t('zheng-zai-jia-zai-cha-jian') }}</p>\n            </div>\n        </div>\n\n        <div v-else class=\"market-panel\">\n            <div v-if=\"marketLoading\" class=\"extensions-loading\">\n                <i class=\"fas fa-spinner fa-spin\"></i>\n                <p>正在加载插件市场...</p>\n            </div>\n\n            <div v-else-if=\"marketError\" class=\"market-feedback error\">\n                <div class=\"empty-icon\">\n                    <i class=\"fas fa-circle-exclamation\"></i>\n                </div>\n                <h4>插件市场加载失败</h4>\n                <p>{{ marketError }}</p>\n            </div>\n\n            <div v-else-if=\"pagedMarketPlugins.length > 0\" class=\"market-list\">\n                <div v-for=\"plugin in pagedMarketPlugins\" :key=\"plugin.uniqueKey\" class=\"market-item\">\n                    <div class=\"market-item-header\">\n                        <div class=\"market-title-group\">\n                            <div class=\"extension-icon market-icon\">\n                                <img\n                                    v-if=\"plugin.icon\"\n                                    :src=\"plugin.icon\"\n                                    :alt=\"plugin.name\"\n                                    @error=\"handleMarketIconError\"\n                                    class=\"extension-icon-img\"\n                                />\n                                <i v-else class=\"fas fa-store\"></i>\n                            </div>\n                            <div class=\"market-title-text\">\n                                <h4>\n                                    {{ plugin.name }}\n                                    <span v-if=\"isCurrentAppVersionLowerThanMin(plugin.minversion)\" class=\"market-min-version-inline\">\n                                        需V{{ plugin.minversion }}+\n                                    </span>\n                                </h4>\n                                <p>{{ plugin.description || '暂无描述' }}</p>\n                            </div>\n                        </div>\n                        <div class=\"market-status-group\">\n                            <span class=\"market-badge\" :class=\"resolveMarketState(plugin).badgeClass\">\n                                {{ resolveMarketState(plugin).badgeText }}\n                            </span>\n                            <button\n                                class=\"extension-btn\"\n                                :class=\"resolveMarketState(plugin).buttonClass\"\n                                :disabled=\"marketActionLoading === plugin.uniqueKey || !plugin.downloadUrl\"\n                                @click=\"handleMarketInstall(plugin)\"\n                            >\n                                <i v-if=\"marketActionLoading === plugin.uniqueKey\" class=\"fas fa-spinner fa-spin\"></i>\n                                <i v-else :class=\"resolveMarketState(plugin).buttonIcon\"></i>\n                                {{ marketActionLoading === plugin.uniqueKey ? '处理中' : resolveMarketState(plugin).buttonText }}\n                            </button>\n                        </div>\n                    </div>\n\n                    <div class=\"market-meta\">\n                        <span>版本 {{ plugin.version || '未知' }}</span>\n                        <span class=\"author-meta\">作者 <span>{{ plugin.author || '未知' }}</span></span>\n                        <span v-if=\"plugin.repositoryUrl\">\n                            <a :href=\"plugin.repositoryUrl\" target=\"_blank\" rel=\"noopener noreferrer\">项目地址</a>\n                        </span>\n                    </div>\n\n                    <div v-if=\"plugin.tags.length > 0\" class=\"market-tags\">\n                        <span v-for=\"tag in plugin.tags\" :key=\"tag\" class=\"market-tag\">{{ tag }}</span>\n                    </div>\n                </div>\n\n                <div v-if=\"marketTotalPages > 1\" class=\"market-pagination\">\n                    <button class=\"extension-btn secondary small\" :disabled=\"marketPage === 1\" @click=\"marketPage -= 1\">\n                        上一页\n                    </button>\n                    <span>第 {{ marketPage }} / {{ marketTotalPages }} 页</span>\n                    <button class=\"extension-btn secondary small\" :disabled=\"marketPage === marketTotalPages\" @click=\"marketPage += 1\">\n                        下一页\n                    </button>\n                </div>\n            </div>\n\n            <div v-else class=\"market-feedback\">\n                <div class=\"empty-icon\">\n                    <i class=\"fas fa-store-slash\"></i>\n                </div>\n                <h4>{{ marketPlugins.length === 0 ? '暂无可用插件' : '没有匹配的插件' }}</h4>\n                <p>{{ marketPlugins.length === 0 ? '插件市场当前没有可展示的插件。' : '换个关键词试试。' }}</p>\n            </div>\n        </div>\n    </template>\n    <div v-else class=\"extensions-empty\">\n        <div class=\"empty-icon\">\n            <i class=\"fas fa-puzzle-piece\"></i>\n        </div>\n        <h4>{{ t('web-cha-jian-ti-shi') }}</h4>\n    </div>\n</template>\n\n<script setup>\nimport { computed, ref, onMounted, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nconst { t } = useI18n()\nconst MARKET_URL = 'https://raw.githubusercontent.com/MoeKoeMusic/MoeKoeMusic-Plugins/refs/heads/main/plugins.json'\nconst MARKET_PAGE_SIZE = 5\n\nconst extensions = ref([])\nconst extensionsLoading = ref(false)\nconst currentView = ref('installed')\nconst marketPlugins = ref([])\nconst marketLoading = ref(false)\nconst marketLoaded = ref(false)\nconst marketError = ref('')\nconst marketSearch = ref('')\nconst marketPage = ref(1)\nconst marketActionLoading = ref('')\nconst currentAppVersion = ref('')\n\nconst normalizedInstalledExtensions = computed(() => extensions.value)\n\nconst filteredMarketPlugins = computed(() => {\n    const keyword = marketSearch.value.trim().toLowerCase()\n    if (!keyword) {\n        return marketPlugins.value\n    }\n\n    return marketPlugins.value.filter(plugin => {\n        const text = [\n            plugin.name,\n            plugin.description,\n            plugin.author,\n            plugin.id,\n            plugin.directory,\n            ...(plugin.tags || [])\n        ].filter(Boolean).join(' ').toLowerCase()\n\n        return text.includes(keyword)\n    })\n})\n\nconst marketTotalPages = computed(() => {\n    return Math.max(1, Math.ceil(filteredMarketPlugins.value.length / MARKET_PAGE_SIZE))\n})\n\nconst pagedMarketPlugins = computed(() => {\n    const start = (marketPage.value - 1) * MARKET_PAGE_SIZE\n    return filteredMarketPlugins.value.slice(start, start + MARKET_PAGE_SIZE)\n})\n\nwatch(marketSearch, () => {\n    marketPage.value = 1\n})\n\nwatch(filteredMarketPlugins, () => {\n    if (marketPage.value > marketTotalPages.value) {\n        marketPage.value = marketTotalPages.value\n    }\n})\n\nwatch(currentView, async view => {\n    if (view === 'market' && !marketLoaded.value && !marketLoading.value) {\n        await fetchMarketPlugins()\n    }\n})\n\nconst refreshExtensions = async (reload = false) => {\n    extensionsLoading.value = true\n    try {\n        if (reload) {\n            const reloadResult = await window.electronAPI?.reloadExtensions()\n            if (!reloadResult?.success) {\n                console.error('Failed to reload plugins:', reloadResult?.message)\n            }\n        }\n\n        await new Promise(resolve => setTimeout(resolve, 300))\n        const result = await window.electronAPI?.getExtensions()\n        if (result?.success) {\n            extensions.value = result.extensions || []\n        } else {\n            console.error('Failed to get plugins:', result?.error)\n        }\n    } catch (error) {\n        console.error('Error refreshing plugins:', error)\n    } finally {\n        extensionsLoading.value = false\n    }\n}\n\nconst fetchMarketPlugins = async (force = false) => {\n    if (!force && marketLoaded.value) {\n        return\n    }\n\n    marketLoading.value = true\n    marketError.value = ''\n\n    try {\n        const response = await fetch(MARKET_URL, {\n            method: 'GET',\n            cache: 'no-store'\n        })\n\n        if (!response.ok) {\n            throw new Error(`请求失败: ${response.status} ${response.statusText || ''}`.trim())\n        }\n\n        const payload = await response.json()\n        const normalized = normalizeMarketPayload(payload)\n\n        marketPlugins.value = normalized.filter(plugin => plugin.status === 'active')\n        marketLoaded.value = true\n        marketPage.value = 1\n    } catch (error) {\n        marketPlugins.value = []\n        marketLoaded.value = false\n        marketError.value = error?.message || '无法读取插件市场数据'\n        console.error('Failed to fetch plugin market:', error)\n    } finally {\n        marketLoading.value = false\n    }\n}\n\nconst normalizeMarketPayload = payload => {\n    const list = Array.isArray(payload)\n        ? payload\n        : payload?.plugins || payload?.items || payload?.data || []\n\n    if (!Array.isArray(list)) {\n        throw new Error('插件市场数据格式不正确')\n    }\n\n    return list.map((item, index) => normalizeMarketPlugin(item, index)).filter(Boolean)\n}\n\nconst normalizeMarketPlugin = (item, index) => {\n    if (!item || typeof item !== 'object') {\n        return null\n    }\n\n    const snapshot = item.snapshot && typeof item.snapshot === 'object' ? item.snapshot : {}\n    const repositoryValue = item.repositoryUrl || ''\n    const downloadUrl = normalizeUrl(item.downloadUrl)\n\n    const plugin = {\n        uniqueKey: item.id,\n        id: String(item.id).trim(),\n        name: String(item.name).trim(),\n        directory: String(item.id).trim(),\n        version: String(item.version).trim(),\n        description: String(item.description).trim(),\n        author: String(item.author).trim(),\n        status: String(item.status || '').trim().toLowerCase(),\n        icon: normalizeUrl(item.iconUrl),\n        tags: Array.isArray(item.tags) ? item.tags.map(tag => String(tag).trim()).filter(Boolean) : [],\n        repositoryUrl: normalizeUrl(repositoryValue),\n        downloadUrl,\n        branch: String(snapshot.branch ).trim(),\n        commitSha: String(snapshot.commitSha).trim(),\n        minversion: item.minversion\n    }\n\n    if (!plugin.name) {\n        return null\n    }\n\n    return plugin\n}\n\nconst normalizeUrl = value => {\n    if (typeof value !== 'string') {\n        return ''\n    }\n\n    const trimmed = value.trim()\n    if (!trimmed) {\n        return ''\n    }\n\n    if (trimmed.includes('github.com') && trimmed.includes('/blob/')) {\n        return trimmed.replace('https://github.com/', 'https://raw.githubusercontent.com/').replace('/blob/', '/')\n    }\n\n    return trimmed\n}\n\nconst resolvePluginDownloadUrl = plugin => {\n    const normalized = normalizeUrl(plugin?.downloadUrl)\n    if (!normalized) {\n        return ''\n    }\n\n    if (/\\.zip($|\\?)/i.test(normalized)) {\n        return normalized\n    }\n\n    try {\n        const url = new URL(normalized)\n        if (url.hostname !== 'github.com') {\n            return normalized\n        }\n\n        const segments = url.pathname.split('/').filter(Boolean)\n        if (segments.length < 2) {\n            return normalized\n        }\n\n        const [owner, repo, type, ...rest] = segments\n        const ref = rest.join('/')\n        const fallbackBranch = plugin?.branch || 'main'\n        const archiveRef = value => {\n            const resolvedRef = String(value || '').trim()\n            if (!resolvedRef) {\n                return ''\n            }\n\n            if (/^[0-9a-f]{7,40}$/i.test(resolvedRef)) {\n                return `https://github.com/${owner}/${repo}/archive/${resolvedRef}.zip`\n            }\n\n            return `https://github.com/${owner}/${repo}/archive/refs/heads/${encodeURIComponent(resolvedRef)}.zip`\n        }\n\n        if (!type) {\n            return archiveRef(plugin?.commitSha || fallbackBranch) || normalized\n        }\n\n        if (type === 'tree') {\n            return archiveRef(ref) || normalized\n        }\n\n        if (type === 'blob') {\n            const rawUrl = normalized.replace('https://github.com/', 'https://raw.githubusercontent.com/').replace('/blob/', '/')\n            return /\\.zip($|\\?)/i.test(rawUrl) ? rawUrl : normalized\n        }\n    } catch (error) {\n        console.error('Failed to resolve plugin download url:', error)\n    }\n\n    return normalized\n}\n\nconst findInstalledExtension = plugin => {\n    const pluginId = String(plugin?.id || '').trim().toLowerCase()\n    if (!pluginId) {\n        return null\n    }\n\n    return normalizedInstalledExtensions.value.find(extension => {\n        return String(extension?.pluginId || '').trim().toLowerCase() === pluginId\n    }) || null\n}\n\nconst compareVersions = (currentVersion, latestVersion) => {\n    const currentTokens = tokenizeVersion(currentVersion)\n    const latestTokens = tokenizeVersion(latestVersion)\n    const length = Math.max(currentTokens.length, latestTokens.length)\n\n    for (let index = 0; index < length; index += 1) {\n        const currentToken = currentTokens[index] ?? 0\n        const latestToken = latestTokens[index] ?? 0\n\n        if (typeof currentToken === 'number' && typeof latestToken === 'number') {\n            if (currentToken !== latestToken) {\n                return currentToken > latestToken ? 1 : -1\n            }\n            continue\n        }\n\n        const currentText = String(currentToken)\n        const latestText = String(latestToken)\n        const result = currentText.localeCompare(latestText)\n        if (result !== 0) {\n            return result > 0 ? 1 : -1\n        }\n    }\n\n    return 0\n}\n\nconst tokenizeVersion = version => {\n    return version\n        .split(/[\\.\\-_]/)\n        .filter(Boolean)\n        .map(part => (/^\\d+$/.test(part) ? Number(part) : part.toLowerCase()))\n}\n\nconst isCurrentAppVersionLowerThanMin = minVersion => {\n    const required = minVersion\n    const current = currentAppVersion.value\n\n    if (!required || !current) {\n        return false\n    }\n\n    return compareVersions(current, required) < 0\n}\n\nconst resolveMarketState = plugin => {\n    const installedExtension = findInstalledExtension(plugin)\n\n    if (!plugin.downloadUrl) {\n        return {\n            badgeClass: 'unknown',\n            badgeText: '缺少下载地址',\n            buttonClass: 'secondary',\n            buttonIcon: 'fas fa-ban',\n            buttonText: '无法安装'\n        }\n    }\n\n    if (!installedExtension) {\n        return {\n            badgeClass: 'available',\n            badgeText: '未安装',\n            buttonClass: 'success',\n            buttonIcon: 'fas fa-download',\n            buttonText: '安装'\n        }\n    }\n\n    const versionDiff = compareVersions(installedExtension.version, plugin.version)\n    if (versionDiff < 0) {\n        return {\n            badgeClass: 'update',\n            badgeText: `可更新 ${installedExtension.version} -> ${plugin.version}`,\n            buttonClass: 'primary',\n            buttonIcon: 'fas fa-arrow-up',\n            buttonText: '更新'\n        }\n    }\n\n    return {\n        badgeClass: 'installed',\n        badgeText: `已安装 ${installedExtension.version}`,\n        buttonClass: 'secondary',\n        buttonIcon: 'fas fa-check',\n        buttonText: '重新安装'\n    }\n}\n\nconst handleMarketInstall = async plugin => {\n    const installedExtension = findInstalledExtension(plugin)\n    const state = resolveMarketState(plugin)\n\n    const resolvedDownloadUrl = resolvePluginDownloadUrl(plugin)\n\n    if (!resolvedDownloadUrl) {\n        showAlert('该插件缺少下载地址，无法安装。')\n        return\n    }\n\n    if (installedExtension && state.buttonText === '重新安装') {\n        const confirmed = await showConfirm(`插件 ${plugin.name} 已是最新版本，仍然重新安装吗？`)\n        if (!confirmed) {\n            return\n        }\n    }\n\n    marketActionLoading.value = plugin.uniqueKey\n\n    try {\n        const result = await window.electronAPI?.installPluginFromUrl(\n            resolvedDownloadUrl,\n            installedExtension?.id || '',\n            installedExtension?.directory || ''\n        )\n\n        if (!result?.success) {\n            throw new Error(result?.message || '安装失败')\n        }\n\n        await refreshExtensions(true)\n        showAlert(installedExtension ? `插件 ${plugin.name} 更新成功` : `插件 ${plugin.name} 安装成功`)\n    } catch (error) {\n        console.error('Failed to install plugin from market:', error)\n        showAlert(`插件 ${plugin.name} ${installedExtension ? '更新' : '安装'}失败: ${error?.message || '未知错误'}`)\n    } finally {\n        marketActionLoading.value = ''\n    }\n}\n\nconst openExtensionsDir = async () => {\n    try {\n        const result = await window.electronAPI?.openExtensionsDir()\n        if (!result?.success) {\n            console.error('Failed to open plugins directory:', result?.error)\n        }\n    } catch (error) {\n        console.error('Error opening plugins directory:', error)\n    }\n}\n\nconst openPluginsRepo = () => {\n    window.open('https://github.com/MoeKoeMusic/MoeKoeMusic-Plugins', '_blank', 'noopener,noreferrer')\n}\n\nconst openExtensionPopup = async (extensionId, extensionName) => {\n    try {\n        const result = await window.electronAPI?.openExtensionPopup(extensionId, extensionName)\n        if (!result?.success) {\n            showAlert(`${t('da-kai-tan-chuang-shi-bai')}: ${result?.message || t('wei-zhi-cuo-wu')}`)\n        }\n    } catch (error) {\n        showAlert(`${t('da-kai-tan-chuang-shi-bai')}: ${error.message}`)\n    }\n}\n\nconst uninstallExtension = async (extensionId, extensionName, extensionDir) => {\n    try {\n        const confirmed = await showConfirm(t('que-ren-xie-zai-cha-jian').replace('name', extensionName))\n        if (!confirmed) {\n            return\n        }\n\n        const result = await window.electronAPI?.uninstallExtension(extensionId, extensionDir)\n        if (result?.success) {\n            await refreshExtensions()\n        } else {\n            showAlert(`${t('xie-zai-cha-jian-shi-bai')}: ${result?.error || t('wei-zhi-cuo-wu')}`)\n        }\n    } catch (error) {\n        showAlert(`${t('xie-zai-cha-jian-shi-bai')}: ${error.message}`)\n    }\n}\n\nconst handleIconError = event => {\n    event.target.style.display = 'none'\n    const iconContainer = event.target.parentElement\n    if (iconContainer && !iconContainer.querySelector('i')) {\n        const icon = document.createElement('i')\n        icon.className = 'fas fa-puzzle-piece'\n        iconContainer.appendChild(icon)\n    }\n}\n\nconst handleMarketIconError = event => {\n    event.target.style.display = 'none'\n    const iconContainer = event.target.parentElement\n    if (iconContainer && !iconContainer.querySelector('i')) {\n        const icon = document.createElement('i')\n        icon.className = 'fas fa-store'\n        iconContainer.appendChild(icon)\n    }\n}\n\nconst installPlugin = async () => {\n    try {\n        const result = await window.electronAPI?.showOpenDialog({\n            properties: ['openFile'],\n            filters: [\n                { name: t('cha-jian-bao'), extensions: ['zip'] }\n            ]\n        })\n\n        if (result?.filePath) {\n            await handlePluginInstall(result.filePath)\n        }\n    } catch (error) {\n        showAlert(`${t('xuan-ze-wen-jian-shi-bai')}: ${error.message}`)\n    }\n}\n\nconst handlePluginInstall = async filePath => {\n    try {\n        extensionsLoading.value = true\n        const result = await window.electronAPI?.installPluginFromZip(filePath)\n        if (result?.success) {\n            showAlert(t('cha-jian-an-zhuang-cheng-gong'))\n            await refreshExtensions()\n        } else {\n            showAlert(`${t('an-zhuang-cha-jian-shi-bai')}: ${result?.message || t('wei-zhi-cuo-wu')}`)\n        }\n    } catch (error) {\n        showAlert(`${t('an-zhuang-cha-jian-chu-cuo')}: ${error.message}`)\n    } finally {\n        extensionsLoading.value = false\n    }\n}\n\nconst showAlert = message => {\n    return window.$modal.alert(message)\n}\n\nconst showConfirm = async message => {\n    return window.$modal.confirm(message)\n}\n\nconst isElectron = () => {\n    return typeof window !== 'undefined' && typeof window.electron !== 'undefined'\n}\n\nonMounted(async () => {\n    if (isElectron()) {\n        currentAppVersion.value = localStorage.getItem('version')\n        await refreshExtensions()\n    }\n})\n</script>\n\n<style scoped>\n.extensions-toolbar {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    gap: 5px;\n    margin-bottom: 24px;\n    flex-wrap: wrap;\n}\n\n.extensions-tabs {\n    display: flex;\n    gap: 8px;\n    padding: 6px;\n    border-radius: 10px;\n    background: rgba(127, 127, 127, 0.12);\n}\n\n.tab-btn {\n    border: none;\n    background: transparent;\n    color: var(--text-color, #333);\n    padding: 10px 16px;\n    border-radius: 8px;\n    cursor: pointer;\n    font-size: 14px;\n    font-weight: 600;\n    transition: all 0.2s ease;\n}\n\n.tab-btn.active {\n    background: var(--color-primary, #ff69b4);\n    color: #fff;\n    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);\n}\n\n.extensions-actions,\n.market-actions {\n    display: flex;\n    gap: 12px;\n    flex-wrap: wrap;\n    align-items: center;\n}\n\n.market-search {\n    display: flex;\n    align-items: center;\n    padding: 0 14px;\n    height: 40px;\n    border-radius: 10px;\n    border: 1px solid var(--border-color, #d9d9d9);\n    background: var(--background-color, #fff);\n}\n\n.market-search i {\n    color: #888;\n}\n\n.market-search input {\n    flex: 1;\n    border: none;\n    outline: none;\n    background: transparent;\n    color: var(--text-color, #333);\n    font-size: 14px;\n}\n\n.extension-btn {\n    padding: 8px 16px;\n    border: none;\n    border-radius: 8px;\n    cursor: pointer;\n    font-size: 14px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: 8px;\n    transition: all 0.2s ease;\n}\n\n.extension-btn:disabled {\n    opacity: 0.65;\n    cursor: not-allowed;\n}\n\n.extension-btn.primary {\n    background: #2563eb;\n    color: white;\n}\n\n.extension-btn.primary:hover:not(:disabled) {\n    background: #1d4ed8;\n}\n\n.extension-btn.success {\n    background: #16a34a;\n    color: white;\n}\n\n.extension-btn.success:hover:not(:disabled) {\n    background: #15803d;\n}\n\n.extension-btn.secondary {\n    background: #6b7280;\n    color: white;\n}\n\n.extension-btn.secondary:hover:not(:disabled) {\n    background: #4b5563;\n}\n\n.extension-btn.danger {\n    background: #dc2626;\n    color: white;\n}\n\n.extension-btn.danger:hover:not(:disabled) {\n    background: #b91c1c;\n}\n\n.extension-btn.small {\n    padding: 6px 10px;\n    font-size: 12px;\n}\n\n.extensions-list,\n.market-list {\n    display: flex;\n    flex-direction: column;\n    gap: 16px;\n}\n\n.extension-item,\n.market-item {\n    display: flex;\n    justify-content: space-between;\n    gap: 16px;\n    padding: 18px;\n    border: 1px solid var(--border-color, #e5e7eb);\n    border-radius: 14px;\n    background: linear-gradient(135deg, rgba(255, 255, 255, 0.92), rgba(247, 247, 247, 0.98));\n    box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);\n}\n\n.extension-info,\n.market-title-group {\n    display: flex;\n    align-items: center;\n    gap: 16px;\n    flex: 1;\n    min-width: 0;\n}\n\n.extension-icon {\n    width: 52px;\n    height: 52px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: white;\n    border-radius: 12px;\n    font-size: 22px;\n    background: linear-gradient(135deg, #fb7185, #2563eb);\n    overflow: hidden;\n    flex-shrink: 0;\n}\n\n.market-icon {\n    background: linear-gradient(135deg, #f59e0b, #f97316);\n}\n\n.extension-icon-img {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n}\n\n.extension-details,\n.market-title-text {\n    min-width: 0;\n}\n\n.extension-details h4,\n.market-title-text h4 {\n    margin: 0 0 6px 0;\n    font-size: 16px;\n    color: var(--text-color, #222);\n}\n\n.market-min-version-inline {\n    display: inline-flex;\n    align-items: center;\n    padding: 2px 8px;\n    margin-left: 8px;\n    border-radius: 999px;\n    border: 1px solid rgba(180, 83, 9, 0.28);\n    background: rgba(180, 83, 9, 0.12);\n    color: #b45309;\n    font-size: 10px;\n    font-weight: 700;\n    line-height: 1.2;\n    white-space: nowrap;\n    position: relative;\n    top: -3px;\n}\n\n.extension-details p,\n.market-title-text p,\n.market-meta {\n    margin: 0;\n    font-size: 13px;\n    color: #666;\n}\n\n.extension-description {\n    max-width: 480px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.extension-version {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    flex-wrap: wrap;\n}\n\n.extension-version a,\n.market-meta a {\n    color: #2563eb;\n    text-decoration: none;\n}\n\n.extension-compatibility-warning {\n    color: #b45309 !important;\n    font-size: 12px !important;\n    display: flex !important;\n    align-items: center !important;\n    gap: 6px !important;\n}\n\n.installed-warning {\n    margin: 0;\n}\n\n.extension-actions,\n.market-status-group {\n    display: flex;\n    align-items: center;\n    gap: 12px;\n    flex-wrap: wrap;\n    justify-content: flex-end;\n}\n\n.extension-status,\n.market-badge {\n    padding: 4px 10px;\n    border-radius: 999px;\n    font-size: 12px;\n    font-weight: 700;\n    white-space: nowrap;\n}\n\n.extension-status.enabled,\n.market-badge.installed {\n    background: #dcfce7;\n    color: #166534;\n}\n\n.market-badge.available {\n    background: #e0f2fe;\n    color: #075985;\n}\n\n.market-badge.update {\n    background: #fef3c7;\n    color: #92400e;\n}\n\n.market-badge.unknown {\n    background: #f3f4f6;\n    color: #4b5563;\n}\n\n.market-panel {\n    min-height: 240px;\n}\n\n.market-item {\n    flex-direction: column;\n}\n\n.market-item-header {\n    display: flex;\n    justify-content: space-between;\n    gap: 16px;\n    align-items: flex-start;\n}\n\n.market-meta {\n    display: flex;\n    gap: 16px;\n    flex-wrap: wrap;\n}\n\n.market-meta span {\n    min-width: 0;\n}\n\n.market-meta .author-meta {\n    display: inline-flex;\n    align-items: center;\n    gap: 4px;\n    min-width: 0;\n    max-width: 220px;\n    white-space: nowrap;\n}\n\n.market-meta .author-meta a {\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.market-tags {\n    display: flex;\n    gap: 8px;\n    flex-wrap: wrap;\n}\n\n.market-tag {\n    padding: 4px 10px;\n    border-radius: 999px;\n    font-size: 12px;\n    background: rgba(37, 99, 235, 0.1);\n    color: #1d4ed8;\n}\n\n.market-pagination {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: 12px;\n    margin-top: 8px;\n}\n\n.extensions-empty,\n.extensions-loading,\n.market-feedback {\n    text-align: center;\n    padding: 48px 20px;\n    color: #666;\n}\n\n.market-feedback.error {\n    border: 1px solid rgba(220, 38, 38, 0.15);\n    border-radius: 14px;\n    background: rgba(254, 242, 242, 0.85);\n}\n\n.empty-icon {\n    font-size: 48px;\n    color: #c4c4c4;\n    margin-bottom: 16px;\n}\n\n.extensions-empty h4,\n.market-feedback h4 {\n    margin: 0 0 8px 0;\n    color: var(--text-color, #333);\n}\n\n.extensions-empty p,\n.market-feedback p {\n    margin: 0 0 20px 0;\n}\n\n.extensions-loading i {\n    font-size: 24px;\n    margin-bottom: 12px;\n}\n\n@media (max-width: 768px) {\n    .extensions-toolbar,\n    .market-item-header {\n        flex-direction: column;\n        align-items: stretch;\n    }\n\n    .extensions-tabs,\n    .market-actions,\n    .extensions-actions,\n    .market-search {\n        width: 100%;\n    }\n\n    .market-search {\n        min-width: 0;\n    }\n\n    .extension-actions,\n    .market-status-group {\n        justify-content: flex-start;\n    }\n}\n</style>\n"
  },
  {
    "path": "src/components/Header.vue",
    "content": "<template>\n    <header>\n        <nav class=\"navigation\">\n            <div class=\"navigation\">\n                <button class=\"nav-arrow\" @click=\"goBack\" :disabled=\"!canGoBack\">\n                    <i class=\"fas fa-chevron-left\"></i>\n                </button>\n                <button class=\"nav-arrow\" @click=\"goForward\" :disabled=\"!canGoForward\">\n                    <i class=\"fas fa-chevron-right\"></i>\n                </button>\n                <button class=\"nav-arrow\" @click=\"refreshPage\">\n                    <i class=\"fas fa-redo\"></i>\n                </button>\n            </div>\n            <div class=\"nav-links\">\n                <router-link to=\"/\">{{ $t('shou-ye') }}</router-link>\n                <router-link to=\"/discover\">{{ $t('fa-xian') }}</router-link>\n                <router-link to=\"/library\">{{ $t('yin-le-ku') }}</router-link>\n            </div>\n            <div class=\"search-profile\">\n                <div class=\"search-bar\">\n                    <input v-model=\"searchQuery\" type=\"text\" :placeholder=\"$t('sou-suo-yin-le-ge-shou-ge-dan')\" @keydown.enter=\"getSearch\">\n                </div>\n                <div class=\"profile\" @click=\"toggleProfile\">\n                    <img :src=\"MoeAuth.UserInfo ? MoeAuth.UserInfo.pic : './assets/images/profile.jpg'\"\n                        alt=\"Profile Picture\">\n                    <div class=\"profile-menu\" v-if=\"showProfile\">\n                        <ul>\n                            <li>\n                                <router-link to=\"/settings\">\n                                    <i class=\"fas fa-cog\"></i> {{ $t('she-zhi') }}\n                                </router-link>\n                            </li>\n                            <li>\n                                <a v-if=\"MoeAuth.isAuthenticated\" @click=\"logout\"><i\n                                        class=\"fas fa-sign-out-alt\"></i>{{ $t('tui-chu') }}</a>\n                                <router-link to=\"/login\" v-else>\n                                    <i class=\"fas fa-sign-in-alt\"></i> {{ $t('deng-lu') }}\n                                </router-link>\n                            </li>\n                            <li>\n                                <a @click=\"openRegisterUrl(downloadUrl || 'https://github.com/iAJue/MoeKoeMusic/releases')\" style=\"position: relative;\">\n                                    <i class=\"fab fa-github\"></i> {{ $t('geng-xin') }}\n                                    <i v-if=\"showNewBadge\" class=\"new-badge\">new</i>\n                                </a>\n                            </li>\n                            <li>\n                                <a @click=\"Disclaimer()\">\n                                    <i class=\"fas fa-info-circle\"></i> {{ $t('guan-yu') }}\n                                </a>\n                            </li>\n                        </ul>\n                    </div>\n                </div>\n            </div>\n        </nav>\n    </header>\n    <div v-if=\"isDisclaimerVisible\" class=\"modal-overlay\" @click=\"Disclaimer\">\n        <div class=\"modal-content\" @click.stop>\n            <h2>{{ $t('mian-ze-sheng-ming') }}</h2>\n            <p>{{ $t('0-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan') }}</p>\n            <p>{{ $t('1-ben-xiang-mu-jin-gong-xue-xi-shi-yong-qing-zun-zhong-ban-quan-qing-wu-li-yong-ci-xiang-mu-cong-shi-shang-ye-hang-wei-ji-fei-fa-yong-tu') }}</p>\n            <p>{{ $t('2-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-chan-sheng-ban-quan-shu-ju-dui-yu-zhe-xie-ban-quan-shu-ju-ben-xiang-mu-bu-yong-you-ta-men-de-suo-you-quan-wei-le-bi-mian-qin-quan-shi-yong-zhe-wu-bi-zai-24-xiao-shi-nei-qing-chu-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-suo-chan-sheng-de-ban-quan-shu-ju') }}</p>\n            <p>{{ $t('3-you-yu-shi-yong-ben-xiang-mu-chan-sheng-de-bao-kuo-you-yu-ben-xie-yi-huo-you-yu-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-er-yin-qi-de-ren-he-xing-zhi-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-sun-hai-bao-kuo-dan-bu-xian-yu-yin-shang-yu-sun-shi-ting-gong-ji-suan-ji-gu-zhang-huo-gu-zhang-yin-qi-de-sun-hai-pei-chang-huo-ren-he-ji-suo-you-qi-ta-shang-ye-sun-hai-huo-sun-shi-you-shi-yong-zhe-fu-ze') }}\n            </p>\n            <p>{{ $t('4-jin-zhi-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-dui-yu-shi-yong-zhe-zai-ming-zhi-huo-bu-zhi-dang-di-fa-lv-fa-gui-bu-yun-xu-de-qing-kuang-xia-shi-yong-ben-xiang-mu-suo-zao-cheng-de-ren-he-wei-fa-wei-gui-hang-wei-you-shi-yong-zhe-cheng-dan-ben-xiang-mu-bu-cheng-dan-you-ci-zao-cheng-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-ze-ren') }}\n            </p>\n            <p>{{ $t('5-yin-le-ping-tai-bu-yi-qing-zun-zhong-ban-quan-zhi-chi-zheng-ban') }}</p>\n            <p>{{ $t('6-ben-xiang-mu-jin-yong-yu-dui-ji-shu-ke-hang-xing-de-tan-suo-ji-yan-jiu-bu-jie-shou-ren-he-shang-ye-bao-kuo-dan-bu-xian-yu-guang-gao-deng-he-zuo-ji-juan-zeng') }}</p>\n            <p>{{ $t('7-ru-guo-guan-fang-yin-le-ping-tai-jue-de-ben-xiang-mu-bu-tuo-ke-lian-xi-ben-xiang-mu-geng-gai-huo-yi-chu') }}</p>\n            <button @click=\"Disclaimer\">{{ $t('guan-bi-an-niu') }}</button>\n            <div class=\"version-number\">© MoeKoe Music <span v-if=\"appVersion\">V{{ appVersion }} - {{ platform }}</span></div>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref, onMounted, onUnmounted } from 'vue';\nimport { useRouter, useRoute } from 'vue-router';\nimport { MoeAuthStore } from '../stores/store';\nimport { openRegisterUrl } from '../utils/utils';\nimport { useI18n } from 'vue-i18n';\nconst MoeAuth = MoeAuthStore();\nconst searchQuery = ref('');\nconst isDisclaimerVisible = ref(false);\nconst router = useRouter();\nconst route = useRoute();\nconst canGoBack = ref(false);\nconst canGoForward = ref(false);\nconst forwardStack = ref([]);\nconst { t } = useI18n();\nconst showNewBadge = ref(false);\nconst downloadUrl = ref('');\nconst appVersion = ref('');\nconst platform = ref('');\nonMounted(() => {\n    updateNavigationStatus();\n    if (window.electron) {\n        window.electron.ipcRenderer.on('version', (_event, version) => {\n            appVersion.value = version;\n            fetchLatestVersion();\n            platform.value = window.electron.platform;\n            localStorage.setItem('version', version);\n        });\n    }\n});\nconst Disclaimer = () => {\n    isDisclaimerVisible.value = !isDisclaimerVisible.value;\n};\nconst updateNavigationStatus = () => {\n    canGoBack.value = window.history.length > 1;\n    canGoForward.value = forwardStack.value.length > 0;\n};\nconst goBack = () => {\n    if (canGoBack.value) {\n        forwardStack.value.push(route.fullPath);\n        router.back();\n    }\n    updateNavigationStatus();\n};\nconst goForward = () => {\n    if (canGoForward.value) {\n        const forwardRoute = forwardStack.value.pop();\n        router.push(forwardRoute);\n    }\n    updateNavigationStatus();\n};\nrouter.afterEach(() => {\n    updateNavigationStatus();\n});\nconst refreshPage = () => {\n    window.location.reload();\n};\nconst logout = async () => {\n    const result = await window.$modal.confirm(t('ni-que-ren-yao-tui-chu-deng-lu-ma'));\n    if (result) {\n        MoeAuth.clearData();\n        router.push({ path: '/' });   \n    }\n}\nconst showProfile = ref(false);\n\nconst toggleProfile = () => {\n    showProfile.value = !showProfile.value;\n};\nconst getSearch = () => {\n    if (searchQuery.value.trim() !== '') {\n        if (searchQuery.value.includes('collection_')) {\n            router.push({\n                path: '/PlaylistDetail',\n                query: { global_collection_id: searchQuery.value }\n            });\n            return;\n        }\n        router.push({\n            path: '/search',\n            query: { q: searchQuery.value }\n        });\n    }\n};\nonMounted(() => {\n    document.addEventListener('click', handleClickOutside);\n});\n\nonUnmounted(() => {\n    document.removeEventListener('click', handleClickOutside);\n});\n\nconst handleClickOutside = (event) => {\n    const queueProfile = document.querySelector('.profile-menu');\n    if (queueProfile && !queueProfile.contains(event.target) && !event.target.closest('.profile')) {\n        showProfile.value = false;\n    }\n};\n\nconst fetchLatestVersion = async () => {\n    try {\n        const response = await fetch('https://api.github.com/repos/iAJue/MoeKoeMusic/releases/latest');\n        const data = await response.json();\n        downloadUrl.value = data.html_url;\n        const latestVersion = data.tag_name.replace(/^v/, '');\n        if (isVersionLower(appVersion.value, latestVersion)) {\n            showNewBadge.value = true; \n        }\n    } catch (error) {\n        console.error('获取最新版本号失败:', error);\n    }\n};\n\nconst isVersionLower = (current, latest) => {\n    const currentParts = current.split('.').map(Number);\n    const latestParts = latest.split('.').map(Number);\n    for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {\n        if ((latestParts[i] || 0) > (currentParts[i] || 0)) {\n            return true;\n        } else if ((latestParts[i] || 0) < (currentParts[i] || 0)) {\n            return false;\n        }\n    }\n    return false;\n};\n</script>\n<style scoped>\n.navigation {\n    display: flex;\n    gap: 10px;\n}\n\n.nav-arrow {\n    background: none;\n    border: none;\n    cursor: pointer;\n    padding: 10px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.nav-arrow:disabled i {\n    color: #ccc;\n    cursor: not-allowed;\n}\n\n.nav-arrow i {\n    font-size: 24px;\n    color: #333;\n}\n\n.nav-arrow:hover {\n    background-color: #f0f0f0;\n}\n\n\nbutton {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    padding: 8px;\n    background: transparent;\n    margin: 4px;\n    border-radius: 25%;\n    transition: .2s\n}\n\nbutton .svg-icon {\n    color: var(--color-text);\n    height: 16px;\n    width: 16px\n}\n\nbutton:first-child {\n    margin-left: 0\n}\n\nbutton:hover {\n    background: var(--color-secondary-bg-for-transparent)\n}\n\nbutton:active {\n    transform: scale(.92)\n}\n\nheader {\n    background-color: #fff;\n    padding: 15px 0;\n    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n    position: fixed;\n    width: 100%;\n    top: 0px;\n    z-index: 9;\n}\n\n.nav-arrow,\n.nav-links a,\n.search-bar input,\n.profile,\n.profile img {\n    -webkit-app-region: no-drag;\n}\n\n.navigation {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    max-width: 1200px;\n    margin: 0 auto;\n    padding: 0 20px;\n}\n\n.nav-links {\n    display: flex;\n    gap: 30px;\n    justify-content: center;\n    flex-grow: 1;\n}\n\n.nav-links a {\n    text-decoration: none;\n    color: var(--primary-color);\n    -webkit-app-region: no-drag;\n    font-size: 18px;\n    font-weight: 700;\n    border-radius: 6px;\n    padding: 6px 10px;\n    transition: .2s;\n    -webkit-user-drag: none;\n    margin-right: 12px;\n    margin-left: 12px\n}\n\n.nav-links a:hover {\n    background: var(--color-secondary-bg-for-transparent)\n}\n\n.nav-links a:active {\n    transform: scale(.92);\n    transition: .2s\n}\n\n.nav-links a.active {\n    color: var(--color-primary)\n}\n\n.search-profile {\n    display: flex;\n    align-items: center;\n    gap: 20px;\n}\n\n.search-bar input {\n    padding: 8px 15px;\n    border-radius: 20px;\n    border: 1px solid var(--secondary-color);\n    font-size: 14px;\n    width: 200px;\n    transition: width 0.3s ease;\n}\n\n.search-bar input:focus {\n    width: 250px;\n    outline: none;\n    border-color: var(--primary-color);\n}\n\n.profile {\n    width: 40px;\n    height: 40px;\n    border-radius: 50%;\n    background-color: var(--secondary-color);\n    cursor: pointer;\n    position: relative;\n}\n\n.profile img {\n    width: 41px;\n    border-radius: 50%;\n}\n\n.profile-menu {\n    position: absolute;\n    top: 50px;\n    right: 0;\n    background-color: #fff;\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n    border-radius: 8px;\n    padding: 10px;\n    width: 150px;\n    display: flex;\n    flex-direction: column;\n    gap: 10px;\n    animation: fadeInOut 0.3s ease-in-out;\n}\n\n@keyframes fadeInOut {\n    0% {\n        opacity: 0;\n    }\n\n    100% {\n        opacity: 1;\n    }\n}\n\n.profile-menu ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n}\n\n.profile-menu li a {\n    display: flex;\n    align-items: center;\n    gap: 15px;\n    cursor: pointer;\n    padding: 7px 5px;\n    border-radius: 5px;\n    color: #000;\n    text-decoration: none;\n}\n\n.profile-menu li a:hover {\n    background-color: var(--secondary-color);\n}\n\n.modal-overlay {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background: rgba(0, 0, 0, 0.5);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    z-index: 1000;\n}\n\n.modal-content {\n    position: relative;\n    background: #fff;\n    padding: 20px;\n    border-radius: 8px;\n    max-width: 700px;\n    width: 90%;\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);\n    text-align: left;\n    animation: fadeIn 0.3s ease;\n}\n\n.modal-content h2 {\n    margin-top: 0;\n    color: var(--primary-color);\n}\n\n.modal-content p {\n    margin: 10px 0;\n    line-height: 1.6;\n}\n\n.modal-content button {\n    margin-top: 15px;\n    padding: 8px 12px;\n    background-color: var(--primary-color);\n    color: #fff;\n    border: none;\n    border-radius: 5px;\n    cursor: pointer;\n}\n\n\n@keyframes fadeIn {\n    from {\n        opacity: 0;\n        transform: scale(0.95);\n    }\n\n    to {\n        opacity: 1;\n        transform: scale(1);\n    }\n}\n\n.new-badge {\n    position: absolute;\n    top: 1px;\n    left: 67px;\n    background-color: red;\n    color: white;\n    padding: 0px 4px;\n    border-radius: 5px;\n    font-size: 14px;\n}\n\n.version-number {\n    position: absolute;\n    bottom: 10px;\n    right: 10px;\n    font-size: 12px;\n    color: #666;\n}\n</style>\n"
  },
  {
    "path": "src/components/MessageNotification.vue",
    "content": "<template>\n    <transition-group name=\"message-fade\" tag=\"div\" class=\"message-container\" v-show=\"messages.length\">\n        <div v-for=\"msg in messages\" :key=\"msg.id\" :class=\"['message', `message-${msg.type}`]\"\n            @click=\"removeMessage(msg.id)\">\n            <div class=\"message-content\">\n                <div class=\"message-icon\" v-if=\"msg.type !== 'default'\">\n                    <div v-if=\"msg.type === 'success'\" class=\"icon-success\"></div>\n                    <div v-else-if=\"msg.type === 'error'\" class=\"icon-error\"></div>\n                    <div v-else-if=\"msg.type === 'warning'\" class=\"icon-warning\"></div>\n                    <div v-else-if=\"msg.type === 'info'\" class=\"icon-info\"></div>\n                </div>\n                <span>{{ msg.content }}</span>\n            </div>\n        </div>\n    </transition-group>\n</template>\n\n<script setup>\nimport { ref } from 'vue';\n\nconst messages = ref([]);\nlet messageId = 0;\n\n// 添加消息\nconst addMessage = (content, type = 'default', duration = 3000) => {\n    const id = messageId++;\n    messages.value.push({ id, content, type, duration });\n\n    // 设置自动移除\n    setTimeout(() => {\n        removeMessage(id);\n    }, duration);\n\n    return id;\n};\n\n// 移除消息\nconst removeMessage = (id) => {\n    const index = messages.value.findIndex(msg => msg.id === id);\n    if (index !== -1) {\n        const msg = messages.value[index];\n        msg.isLeaving = true;\n\n        // 添加一个短暂的延迟，让离开动画有时间播放\n        setTimeout(() => {\n            messages.value = messages.value.filter(m => m.id !== id);\n        }, 300);\n    }\n};\n\n// 暴露方法\ndefineExpose({\n    addMessage,\n    removeMessage,\n    success: (content, duration) => addMessage(content, 'success', duration),\n    error: (content, duration) => addMessage(content, 'error', duration),\n    warning: (content, duration) => addMessage(content, 'warning', duration),\n    info: (content, duration) => addMessage(content, 'info', duration),\n});\n</script>\n\n<style scoped>\n.message-container {\n    position: fixed;\n    top: 60px;\n    left: 50%;\n    transform: translateX(-50%);\n    z-index: 9999;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    pointer-events: none;\n}\n\n.message {\n    margin-bottom: 12px;\n    padding: 12px 16px;\n    border-radius: 8px;\n    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);\n    pointer-events: auto;\n    display: flex;\n    flex-direction: column;\n    transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);\n    background-color: white;\n    color: #333;\n    position: relative;\n    overflow: hidden;\n    cursor: pointer;\n    backdrop-filter: blur(10px);\n    min-width: 200px;\n    max-width: 480px;\n    width: auto;\n}\n\n.message:hover {\n    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);\n    transform: translateY(-2px);\n}\n\n.message-content {\n    display: flex;\n    align-items: center;\n    width: 100%;\n}\n\n.message-icon {\n    margin-right: 12px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.message-success {\n    background-color: rgba(240, 249, 235, 0.95);\n    border-left: 4px solid #67c23a;\n    color: #67c23a;\n}\n\n.message-success .message-progress {\n    background-color: #67c23a;\n}\n\n.message-error {\n    background-color: rgba(254, 240, 240, 0.95);\n    border-left: 4px solid #f56c6c;\n    color: #f56c6c;\n}\n\n.message-error .message-progress {\n    background-color: #f56c6c;\n}\n\n.message-warning {\n    background-color: rgba(253, 246, 236, 0.95);\n    border-left: 4px solid #e6a23c;\n    color: #e6a23c;\n}\n\n.message-warning .message-progress {\n    background-color: #e6a23c;\n}\n\n.message-info {\n    background-color: rgba(244, 244, 245, 0.95);\n    border-left: 4px solid #909399;\n    color: #909399;\n}\n\n.message-info .message-progress {\n    background-color: #909399;\n}\n\n.message-default {\n    border-left: 4px solid #409eff;\n}\n\n.message-default .message-progress {\n    background-color: #409eff;\n}\n\n/* 图标样式 */\n.icon-success,\n.icon-error,\n.icon-warning,\n.icon-info {\n    width: 20px;\n    height: 20px;\n    border-radius: 50%;\n    position: relative;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.icon-success {\n    background-color: #67c23a;\n}\n\n.icon-success::before {\n    content: '';\n    position: absolute;\n    width: 10px;\n    height: 6px;\n    border-left: 2px solid white;\n    border-bottom: 2px solid white;\n    transform: rotate(-45deg);\n}\n\n.icon-error {\n    background-color: #f56c6c;\n}\n\n.icon-error::before,\n.icon-error::after {\n    content: '';\n    position: absolute;\n    width: 12px;\n    height: 2px;\n    background-color: white;\n}\n\n.icon-error::before {\n    transform: rotate(45deg);\n}\n\n.icon-error::after {\n    transform: rotate(-45deg);\n}\n\n.icon-warning {\n    background-color: #e6a23c;\n}\n\n.icon-warning::before {\n    content: '!';\n    color: white;\n    font-weight: bold;\n    font-size: 14px;\n}\n\n.icon-info {\n    background-color: #909399;\n}\n\n.icon-info::before {\n    content: 'i';\n    color: white;\n    font-weight: bold;\n    font-size: 14px;\n}\n\n/* 动画 */\n.message-fade-enter-active {\n    transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);\n}\n\n.message-fade-leave-active {\n    transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);\n    position: absolute;\n}\n\n.message-fade-enter-from {\n    opacity: 0;\n    transform: translateY(-100%);\n}\n\n.message-fade-leave-to {\n    opacity: 0;\n    transform: translateY(-100%);\n}\n\n.message-fade-move {\n    transition: transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);\n}\n</style>"
  },
  {
    "path": "src/components/PlayerControl.vue",
    "content": "<template>\n    <div class=\"player-container\">\n        <div class=\"progress-bar\" @mousedown=\"onProgressDragStart\" @click=\"updateProgressFromEvent\"\n            @mousemove=\"updateTimeTooltip\" @mouseleave=\"hideTimeTooltip\">\n            <div class=\"progress\" :style=\"{ width: progressWidth + '%' }\"></div>\n            <div class=\"progress-handle\" :style=\"{ left: progressWidth + '%' }\"></div>\n            <div v-for=\"(point, index) in climaxPoints\" :key=\"index\" class=\"climax-point\"\n                :style=\"{ left: point.position + '%' }\">\n            </div>\n            <div v-if=\"showTimeTooltip\" class=\"time-tooltip\" :style=\"{ left: tooltipPosition + 'px' }\">\n                {{ tooltipTime }}\n            </div>\n        </div>\n        <div class=\"player-bar\">\n            <div class=\"album-art\" @click=\"toggleLyrics(currentSong.hash, currentTime)\">\n                <img v-if=\"currentSong.img\" :src=\"currentSong.img\" alt=\"Album Art\" />\n                <i v-else class=\"fas fa-music\"></i>\n            </div>\n            <div class=\"song-info\" @click=\"toggleLyrics(currentSong.hash, currentTime)\">\n                <div class=\"song-title-row\">\n                    <div class=\"song-title\" @click.stop=\"searchSong(currentSong.name)\">{{ currentSong?.name || \"MoeKoeMusic\" }}</div>\n                    <div v-if=\"currentSong?.qualityLabel\" class=\"quality-menu-wrapper\" @click.stop>\n                        <button\n                            type=\"button\"\n                            :class=\"['quality-badge', { clickable: canSwitchQuality }]\"\n                            @click.stop=\"toggleQualityMenu\"\n                        >\n                            {{ currentSong.qualityLabel }}\n                        </button>\n                        <div v-if=\"qualityMenuOpen && canSwitchQuality\" class=\"quality-menu\">\n                            <div v-if=\"!currentSong?.qualityOptions?.length\" class=\"quality-menu-item disabled\">暂无可选音质</div>\n                            <button\n                                v-for=\"option in currentSong.qualityOptions\"\n                                :key=\"`${option.value}-${option.hash}`\"\n                                type=\"button\"\n                                class=\"quality-menu-item\"\n                                :class=\"{ active: isCurrentQualityOption(option) }\"\n                                @click.stop=\"switchQuality(option)\"\n                            >\n                                {{ option.label }}\n                            </button>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"artist\" @click.stop=\"searchSong(currentSong.author)\">{{ currentSong?.author || \"MoeJue\" }}</div>\n            </div>\n            <div class=\"controls\">\n                <button class=\"control-btn\" :title=\"t('shang-yi-shou')\" @click=\"playSongFromQueue('previous')\">\n                    <i class=\"fas fa-step-backward\"></i>\n                </button>\n                <button class=\"control-btn\" :title=\"t('zan-ting-bo-fang')\" @click=\"togglePlayPause\">\n                    <i :class=\"playing ? 'fas fa-pause' : 'fas fa-play'\"></i>\n                </button>\n                <button class=\"control-btn\" :title=\"t('xia-yi-shou')\" @click=\"playSongFromQueue('next')\">\n                    <i class=\"fas fa-step-forward\"></i>\n                </button>\n            </div>\n            <div class=\"extra-controls\">\n                <button class=\"extra-btn\" :title=\"t('zhuo-mian-ge-ci')\" v-if=\"isElectron()\" @click=\"desktopLyrics\"><i\n                        class=\"fas\">词</i></button>\n                <div class=\"playback-speed\">\n                    <button class=\"extra-btn\" @click=\"toggleSpeedMenu\" :title=\"t('bo-fang-su-du')\">\n                        <i class=\"fas fa-tachometer-alt\"></i>\n                    </button>\n                    <div v-if=\"showSpeedMenu\" class=\"speed-menu\">\n                        <div v-for=\"speed in playbackSpeeds\" :key=\"speed\" class=\"speed-option\"\n                            :class=\"{ active: currentSpeed === speed }\" @click=\"changePlaybackSpeed(speed)\">\n                            {{ speed }}x\n                        </div>\n                    </div>\n                </div>\n                <button class=\"extra-btn\" :title=\"t('wo-xi-huan')\" @click=\"playlistSelect.toLike()\"><i\n                        class=\"fas fa-heart\"></i></button>\n                <button class=\"extra-btn\" :title=\"t('shou-cang-zhi')\" @click=\"playlistSelect.fetchPlaylists()\"><i\n                        class=\"fas fa-add\"></i></button>\n                <button class=\"extra-btn\" :title=\"t('fen-xiang-ge-qu')\" @click=\"share(currentSong.name, currentSong.hash)\"><i\n                        class=\"fas fa-share\"></i></button>\n                <button class=\"extra-btn\" @click=\"togglePlaybackMode\">\n                    <i v-if=\"currentPlaybackModeIndex != '2'\" :class=\"currentPlaybackMode.icon\"\n                        :title=\"currentPlaybackMode.title\"></i>\n                    <span v-else class=\"loop-icon\" :title=\"currentPlaybackMode.title\">\n                        <i class=\"fas fa-repeat\"></i>\n                        <sup>1</sup>\n                    </span>\n                </button>\n                <button class=\"extra-btn\" :title=\"t('bo-fang-lie-biao')\" @click=\"queueList.openQueue()\"><i class=\"fas fa-list\"></i></button>\n                <!-- 音量控制 -->\n                <div class=\"volume-control\" @wheel=\"handleVolumeScroll\">\n                    <i :class=\"isMuted ? 'fas fa-volume-mute' : 'fas fa-volume-up'\" @click=\"toggleMute\"></i>\n                    <div class=\"volume-slider\" @mousedown=\"onDragStart\">\n                        <div class=\"volume-progress\" :style=\"{ width: volume + '%' }\"></div>\n                        <input type=\"range\" min=\"0\" max=\"100\" v-model=\"volume\" @input=\"changeVolume\" />\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n\n    <!-- 播放队列 -->\n    <QueueList :current-song=\"currentSong\" @add-song-to-queue=\"onQueueSongAdd\"\n        @add-cloud-music-to-queue=\"onQueueCloudSongAdd\" @add-local-music-to-queue=\"onQueueLocalSongAdd\" ref=\"queueList\" />\n\n    <!-- 全屏歌词界面 -->\n    <transition name=\"slide-up\">\n        <div v-if=\"showLyrics\" class=\"lyrics-bg\"\n            :style=\"(lyricsBackground == 'on' ? ({ backgroundImage: `url(${currentSong?.img || 'https://random.MoeJue.cn/randbg.php'})` }) : ({ background: 'var(--secondary-color)' }))\">\n            <div class=\"lyrics-screen\">\n                <div class=\"close-btn\">\n                    <i class=\"fas fa-chevron-down\" @click=\"toggleLyrics(currentSong.hash, currentTime)\"></i>\n                </div>\n                <div class=\"lyrics-mode-btn\" v-if=\"hasMultiLyricsMode\" @click=\"switchLyricsMode\" :title=\"lyricsMode === 'translation' ? t('qie-huan-dao-yin-yi') : t('qie-huan-dao-fan-yi')\">\n                    <i class=\"fas fa-language\"></i>\n                </div>\n\n                <div class=\"left-section\">\n                    <div class=\"album-art-container\" @click=\"toggleCoverMode\">\n                        <transition name=\"cover-fade\" mode=\"out-in\">\n                            <div v-if=\"coverMode === 'vinyl'\" key=\"vinyl\" class=\"vinyl-player\">\n                                <!-- 唱片播放器模式 -->\n                                <div class=\"vinyl-disc\" :class=\"{ 'rotating': playing }\">\n                                    <img :src=\"currentSong?.img || './assets/images/!.png'\" alt=\"Album Art\" class=\"vinyl-cover\" />\n                                </div>\n                                <div class=\"tonearm\" :class=\"{ 'playing': playing }\"></div>\n                            </div>\n                            <div v-else key=\"square\" class=\"album-art-large\">\n                                <!-- 普通封面模式 -->\n                                <img v-if=\"easterEggImage\" :src=\"easterEggImage.src\" :class=\"easterEggClass\" alt=\"Easter Egg\" />\n                                <img :src=\"currentSong?.img || './assets/images/!.png'\" alt=\"Album Art\" />\n                            </div>\n                        </transition>\n                    </div>\n                    <div class=\"song-details\">\n                        <div class=\"song-title\" @click=\"searchSong(currentSong.name)\">{{ currentSong?.name }}</div>\n                        <div class=\"artist\" @click=\"searchSong(currentSong.author)\">{{ currentSong?.author }}</div>\n                    </div>\n\n                    <!-- 播放进度条 -->\n                    <div class=\"progress-bar-container\">\n                        <span class=\"current-time\">{{ formattedCurrentTime }}</span>\n                        <div class=\"progress-bar\" @mousedown=\"onProgressDragStart\" @click=\"updateProgressFromEvent\"\n                            @mousemove=\"updateTimeTooltip\" @mouseleave=\"hideTimeTooltip\">\n                            <div class=\"progress\" :style=\"{ width: progressWidth + '%' }\"></div>\n                            <div class=\"progress-handle\" :style=\"{ left: progressWidth + '%' }\"></div>\n                            <div v-for=\"(point, index) in climaxPoints\" :key=\"index\" class=\"climax-point\"\n                                :style=\"{ left: point.position + '%' }\">\n                            </div>\n                            <div v-if=\"showTimeTooltip\" class=\"time-tooltip\" :style=\"{ left: tooltipPosition + 'px' }\">\n                                {{ tooltipTime }}\n                            </div>\n                        </div>\n                        <span class=\"duration\">{{ formattedDuration }}</span>\n                    </div>\n\n                    <div class=\"player-controls\">\n                        <button class=\"control-btn like-btn\" :title=\"t('wo-xi-huan')\" @click=\"playlistSelect.toLike()\">\n                            <i class=\"fas fa-heart\"></i>\n                        </button>\n                        <button class=\"control-btn\" :title=\"t('shang-yi-shou')\" @click=\"playSongFromQueue('previous')\">\n                            <i class=\"fas fa-step-backward\"></i>\n                        </button>\n                        <button class=\"control-btn\" :title=\"t('zan-ting-bo-fang')\" @click=\"togglePlayPause\">\n                            <i :class=\"playing ? 'fas fa-pause' : 'fas fa-play'\"></i>\n                        </button>\n                        <button class=\"control-btn\" :title=\"t('xia-yi-shou')\" @click=\"playSongFromQueue('next')\">\n                            <i class=\"fas fa-step-forward\"></i>\n                        </button>\n                        <button class=\"control-btn\" :title=\"t('qie-huan-bo-fang-mo-shi')\" @click=\"togglePlaybackMode\">\n                            <i v-if=\"currentPlaybackModeIndex != '2'\" :class=\"currentPlaybackMode.icon\" :title=\"currentPlaybackMode.title\"></i>\n                            <span v-else class=\"loop-icon\" :title=\"currentPlaybackMode.title\">\n                                <i class=\"fas fa-repeat\"></i>\n                                <sup>1</sup>\n                            </span>\n                        </button>\n                    </div>\n                </div>\n                <div id=\"lyrics-container\" @wheel=\"handleLyricsWheel\">\n                    <div v-if=\"lyricsData.length > 0\" id=\"lyrics\"\n                        :style=\"{ fontSize: lyricsFontSize, transform: `translateY(${scrollAmount ? scrollAmount + 'px' : '50%'})` }\">\n                        <div class=\"line-group\" v-for=\"(lineData, lineIndex) in lyricsData\" :key=\"lineIndex\">\n                            <div class=\"line\" @click=\"handleLyricsClick(lineIndex)\" :class=\"{ click: lyricsFlag, [lyricsAlign]: true }\">\n                                <span v-for=\"(charData, charIndex) in lineData.characters\" :key=\"charIndex\" class=\"char\"\n                                    :class=\"{ highlight: charData.highlighted }\">\n                                    {{ charData.char }}\n                                </span>\n                            </div>\n                            <div class=\"line translated\" :class=\"{ [lyricsAlign]: true }\" v-show=\"lineData.translated && lyricsMode === 'translation'\">{{ lineData.translated }}</div>\n                            <div class=\"line romanized\" :class=\"{ [lyricsAlign]: true }\" v-show=\"lineData.romanized && lyricsMode === 'romanization'\">{{ lineData.romanized }}</div>\n                        </div>\n                    </div>\n                    <div v-else class=\"no-lyrics\">{{ SongTips }}</div>\n                </div>\n            </div>\n        </div>\n    </transition>\n\n    <!-- 歌单选择模态框 -->\n    <PlaylistSelectModal ref=\"playlistSelect\" :current-song=\"currentSong\" :playlists=\"playlists\" />\n\n</template>\n\n<script setup>\nimport { ref, computed, onMounted, onUnmounted, watch } from 'vue';\nimport { useMusicQueueStore } from '../stores/musicQueue';\nimport { useI18n } from 'vue-i18n';\nimport PlaylistSelectModal from './PlaylistSelectModal.vue';\nimport QueueList from './QueueList.vue';\nimport { useRouter } from 'vue-router';\nimport { getCover, getAudioOutputDeviceSignature, share } from '../utils/utils';\n\n// 从统一入口导入所有模块\nimport {\n    useAudioController,\n    useLyricsHandler,\n    useProgressBar,\n    usePlaybackMode,\n    useMediaSession,\n    useSongQueue,\n    useHelpers\n} from './player';\n\n// 基础设置\nconst queueList = ref(null);\nconst playlistSelect = ref(null);\nconst qualityMenuOpen = ref(false);\nconst { t } = useI18n();\nconst router = useRouter();\nconst musicQueueStore = useMusicQueueStore();\nconst playlists = ref([]);\nconst currentTime = ref(0);\nconst lyricsFontSize = ref('24px');\nconst lyricsAlign = ref('center');\nconst lyricsBackground = ref('on');\nconst sliderElement = ref(null);\nconst coverMode = ref(localStorage.getItem('lyrics-cover-mode') || 'square');\n\nconst isDragging = ref(false);\nconst lyricsFlag = ref(false);\n\n// 辅助函数\nconst { isElectron, throttle, getVip, desktopLyrics } = useHelpers(t);\n\n// Easter Egg 相关\nconst easterEggImages = [\n    { src: './assets/images/miku.png', class: 'miku' },\n    { src: './assets/images/miku2.png', class: 'miku2' },\n    { src: './assets/images/miku3.png', class: 'miku3' }\n];\n\nconst easterEggImage = computed(() => {\n    const author = currentSong.value?.author || '';\n    if (author.includes('初音') || author.includes('Miku')) {\n        const randomIndex = Math.floor(Math.random() * easterEggImages.length);\n        return easterEggImages[randomIndex];\n    }\n    return null;\n});\n\nconst easterEggClass = computed(() => easterEggImage.value?.class || '');\nconst canSwitchQuality = computed(() => {\n    return !!currentSong.value?.hash && !currentSong.value?.isLocal && !currentSong.value?.isCloud;\n});\nconst isCurrentQualityOption = (option) => {\n    return currentSong.value?.resolvedQuality === option.value && currentSong.value?.playHash === option.hash;\n};\nconst toggleQualityMenu = () => {\n    if (!canSwitchQuality.value) return;\n\n    qualityMenuOpen.value = !qualityMenuOpen.value;\n};\nconst switchQuality = async (option) => {\n    if (!canSwitchQuality.value || isCurrentQualityOption(option)) {\n        qualityMenuOpen.value = false;\n        return;\n    }\n\n    const previousTime = audio.currentTime || 0;\n    const wasPlaying = playing.value;\n\n    qualityMenuOpen.value = false;\n    clearAutoSwitchTimer();\n    audio.pause();\n    playing.value = false;\n\n    const result = await addSongToQueue(\n        currentSong.value.hash,\n        currentSong.value.name,\n        currentSong.value.img,\n        currentSong.value.author,\n        false,\n        option.value,\n        currentSong.value.qualityOptions\n    );\n\n    if (result && result.song) {\n        await playSong(result.song);\n        if (audio.duration) {\n            audio.currentTime = Math.min(previousTime, audio.duration || previousTime);\n        } else {\n            audio.addEventListener('loadedmetadata', () => {\n                audio.currentTime = Math.min(previousTime, audio.duration || previousTime);\n            }, { once: true });\n        }\n\n        if (!wasPlaying) {\n            pausePlayback();\n        }\n    } else if (result && result.shouldPlayNext) {\n        handleAutoSwitch();\n    }\n};\n\n// 初始化事件回调\nconst onSongEnd = () => {\n    if (currentPlaybackModeIndex.value == 2) return;\n    playSongFromQueue('next');\n};\n\n// 用于记录上次发送的歌词，避免重复发送\nlet lastSentLyric = '';\nlet lastSentTime = 0;\n\n// 节流处理的时间更新函数\nconst updateCurrentTime = throttle(() => {\n    currentTime.value = audio.currentTime;\n    if (!isProgressDragging.value) {\n        progressWidth.value = (currentTime.value / audio.duration) * 100;\n    }\n\n    // 更新SMTC位置状态\n    if (audio.duration && currentSong.value?.hash) {\n        mediaSession.updatePositionState(audio.currentTime, audio.duration, currentSpeed.value);\n    }\n\n    const savedConfig = JSON.parse(localStorage.getItem('settings') || '{}');\n    const hasLyricsData = Array.isArray(lyricsData.value) && lyricsData.value.length > 0;\n    \n    const statusBarLyricsEnabled = savedConfig?.statusBarLyrics === 'on';\n    const desktopLyricsEnabled = savedConfig?.desktopLyrics === 'on';\n\n    if (audio) {\n        if (savedConfig?.lyricsAlign != lyricsAlign.value) lyricsAlign.value = savedConfig.lyricsAlign;\n\n        if (hasLyricsData) {\n            highlightCurrentChar(audio.currentTime, !lyricsFlag.value);\n        }\n\n        // 只在有歌曲且正在播放时才发送 IPC\n        if (isElectron() && audio.src && playing.value && (desktopLyricsEnabled || statusBarLyricsEnabled)) {\n            const currentLine = hasLyricsData ? getCurrentLineText(audio.currentTime) : '';\n            \n            // 只有歌词真正变化时才发送（防抖）\n            const currentTimeMs = Date.now();\n            if (currentLine !== lastSentLyric || currentTimeMs - lastSentTime > 1000) {\n                lastSentLyric = currentLine;\n                lastSentTime = currentTimeMs;\n                \n                // 使用 JSON 序列化确保对象可以被克隆\n                try {\n                    const lyricsPayload = hasLyricsData ? JSON.parse(JSON.stringify(lyricsData.value)) : [];\n                    window.electron.ipcRenderer.send('lyrics-data', {\n                        currentTime: audio.currentTime,\n                        lyricsData: lyricsPayload,\n                        currentSongHash: currentSong.value?.hash || '',\n                        currentLyric: currentLine\n                    });\n                } catch (e) {\n                    // 如果序列化失败，只发送必要的数据\n                    window.electron.ipcRenderer.send('lyrics-data', {\n                        currentTime: audio.currentTime,\n                        lyricsData: [],\n                        currentSongHash: currentSong.value?.hash || '',\n                        currentLyric: currentLine\n                    });\n                }\n            }\n        }\n        \n        if (isElectron() && audio.src && playing.value && savedConfig?.apiMode === 'on') {\n            try {\n                const serverLyricsPayload = hasLyricsData && originalLyrics.value ? JSON.parse(JSON.stringify(originalLyrics.value)) : [];\n                const currentSongPayload = currentSong.value ? JSON.parse(JSON.stringify(currentSong.value)) : null;\n                window.electron.ipcRenderer.send('server-lyrics', {\n                    currentTime: audio.currentTime,\n                    lyricsData: serverLyricsPayload,\n                    currentSong: currentSongPayload,\n                    duration: audio.duration\n                });\n            } catch (e) {\n                // 序列化失败时跳过\n            }\n        }\n        \n        if (isElectron() && audio.src && playing.value && window.electron.platform == 'darwin' && savedConfig?.touchBar == 'on') {\n            const currentLine = hasLyricsData ? getCurrentLineText(audio.currentTime) : '';\n            window.electron.ipcRenderer.send(\"update-current-lyrics\", currentLine);\n        }\n    }\n\n    if (!hasLyricsData && isElectron() && (desktopLyricsEnabled || statusBarLyricsEnabled || savedConfig?.apiMode === 'on')) {\n        if (isLyrics === false) return;\n        getCurrentLyrics();\n    }\n\n    localStorage.setItem('player_progress', audio.currentTime);\n}, 200);\n\n// 初始化各个模块\nconst audioController = useAudioController({ onSongEnd, updateCurrentTime });\nconst { playing, isMuted, volume, changeVolume, audio, playbackRate, setPlaybackRate, applyLoudnessNormalization, ensureAudioContextRunning, toggleLoudnessNormalization, loudnessNormalizationEnabled, currentLoudnessGain, webAudioInitialized } = audioController;\n\nconst lyricsHandler = useLyricsHandler(t);\nconst { lyricsData, originalLyrics, showLyrics, scrollAmount, SongTips, lyricsMode, toggleLyrics, getLyrics, highlightCurrentChar, resetLyricsHighlight, getCurrentLineText, scrollToCurrentLine, toggleLyricsMode } = lyricsHandler;\n\n// 获取当前播放时间的歌词行索引\nconst getCurrentLineIndex = (currentTime) => {\n    if (!lyricsData.value || lyricsData.value.length === 0) return -1;\n    for (let i = 0; i < lyricsData.value.length; i++) {\n        const line = lyricsData.value[i];\n        if (line.characters && line.characters.length > 0) {\n            const startTime = line.characters[0].startTime / 1000;\n            if (startTime > currentTime) {\n                return Math.max(0, i - 1);\n            }\n        }\n    }\n    return lyricsData.value.length - 1;\n};\n\nconst progressBar = useProgressBar(audio, resetLyricsHighlight);\nconst { progressWidth, isProgressDragging, showTimeTooltip, tooltipPosition, tooltipTime, climaxPoints, formatTime, getMusicHighlights, onProgressDragStart, updateProgressFromEvent, updateTimeTooltip, hideTimeTooltip } = progressBar;\n\nconst playbackMode = usePlaybackMode(t, audio);\nconst { currentPlaybackModeIndex, currentPlaybackMode, playedSongsStack, currentStackIndex, togglePlaybackMode } = playbackMode;\n\nconst mediaSession = useMediaSession();\n\nconst songQueue = useSongQueue(t, musicQueueStore, queueList);\nconst { currentSong, NextSong, addSongToQueue, addCloudMusicToQueue, addLocalMusicToQueue, addLocalPlaylistToQueue, addToNext, getPlaylistAllSongs, addPlaylistToQueue, addCloudPlaylistToQueue } = songQueue;\n\n// 添加自动切换定时器引用\nlet autoSwitchTimer = null;\n// 恢复歌词正常滚动计时器\nlet lyricScrollTimer = null;\n// 自动切换计数器和最大重试次数\nlet autoSwitchCount = 0;\nconst maxAutoSwitchRetries = 3;\n\n// 处理自动切换逻辑的函数\nconst handleAutoSwitch = () => {\n    console.log('[PlayerControl] 检查自动切换重试次数:', autoSwitchCount, '/', maxAutoSwitchRetries);\n    if (autoSwitchCount < maxAutoSwitchRetries) {\n        autoSwitchCount++;\n        console.log(`[PlayerControl] 自动切换尝试 ${autoSwitchCount}/${maxAutoSwitchRetries}`);\n        autoSwitchTimer = setTimeout(() => {\n            playSongFromQueue('next');\n        }, 3000);\n        return true;\n    } else {\n        console.log('[PlayerControl] 已达到最大重试次数，停止自动切换');\n        window.$modal.alert('已达到最大重试次数，请手动选择歌曲');\n        autoSwitchCount = 0;\n        return false;\n    }\n};\n\n// 清除自动切换定时器的函数\nconst clearAutoSwitchTimer = () => {\n    if (autoSwitchTimer) {\n        console.log('[PlayerControl] 取消自动切换到下一首');\n        clearTimeout(autoSwitchTimer);\n        autoSwitchTimer = null;\n    }\n};\n\n// 恢复歌词正常滚动的节流函数\nconst restoreLyricsScroll = throttle(() => {\n    if (lyricScrollTimer) clearTimeout(lyricScrollTimer);\n    lyricScrollTimer = setTimeout(() => {\n        console.log('[PlayerControl] 恢复歌词正常滚动');\n        lyricScrollTimer = null;\n        lyricsFlag.value = false;\n        const currentLine = getCurrentLineText(audio.currentTime);\n        scrollToCurrentLine(currentLine);\n    }, 5000);\n}, 1000);\n\n// 获取歌词的节流函数\nlet isLyrics;\nconst getCurrentLyrics = throttle(async() => {\n    if (currentSong.value.hash) {\n        isLyrics = await getLyrics(currentSong.value.hash);\n    }\n}, 1000);\n\n// 计算属性\nconst formattedCurrentTime = computed(() => formatTime(currentTime.value));\nconst formattedDuration = computed(() => formatTime(currentSong.value?.timeLength || 0));\n\nwatch(() => currentSong.value.hash, () => {\n    qualityMenuOpen.value = false;\n});\n\n// 判断是否有多种歌词模式（同时有翻译和音译）\nconst hasMultiLyricsMode = computed(() => {\n    if (!lyricsData.value || lyricsData.value.length === 0) return false;\n    \n    // 检查是否至少有一行同时包含翻译和音译\n    return lyricsData.value.some(line => line.translated && line.romanized);\n});\n\n// 切换歌词显示模式（翻译/音译）\nconst switchLyricsMode = () => {\n    toggleLyricsMode();\n};\n\n// 切换封面模式（正方形/唱片）\nconst toggleCoverMode = () => {\n    coverMode.value = coverMode.value === 'square' ? 'vinyl' : 'square';\n    localStorage.setItem('lyrics-cover-mode', coverMode.value);\n};\n\n// 播放歌曲\nconst playSong = async (song) => {\n    clearAutoSwitchTimer();\n\n    try {\n        console.log('[PlayerControl] 开始播放歌曲:', song.name);\n\n        // 检查歌曲对象和URL是否有效\n        if (!song || !song.url) {\n            console.error('[PlayerControl] 无效的歌曲或URL:', song);\n            window.$modal.alert(t('bo-fang-shi-bai-qu-mu-wei-kong'));\n            playing.value = false;\n            return;\n        }\n\n        currentSong.value = structuredClone(song);\n\n        // 应用响度规格化（如果已启用 Web Audio）\n        if (song.loudnessNormalization) {\n            console.log('[PlayerControl] 应用响度规格化:', song.loudnessNormalization);\n            applyLoudnessNormalization(song.loudnessNormalization);\n        } else {\n            console.log('[PlayerControl] 歌曲无响度规格化数据');\n            applyLoudnessNormalization(null);\n        }\n\n        audio.src = song.url;\n\n        // 确保 AudioContext 处于运行状态（如果已启用）\n        await ensureAudioContextRunning();\n\n        setPlaybackRate(currentSpeed.value);\n        console.log('[PlayerControl] 设置音频源:', song.url);\n\n        try {\n            mediaSession.changeMediaSession(currentSong.value);\n            // 更新SMTC位置状态\n            if (audio.duration) {\n                mediaSession.updatePositionState(audio.currentTime, audio.duration, currentSpeed.value);\n            }\n            const playPromise = audio.play();\n\n            if (playPromise !== undefined) {\n                await playPromise;\n                console.log('[PlayerControl] 成功开始播放歌曲');\n                playing.value = true;\n            }\n        } catch (playError) {\n            console.warn('[PlayerControl] 播放被中断，尝试重新播放:', playError);\n            // 等待一小段时间后重试\n            await new Promise(resolve => setTimeout(resolve, 100));\n\n            try {\n                await audio.play();\n                playing.value = true;\n            } catch (retryError) {\n                console.error('[PlayerControl] 重试播放失败:', retryError);\n                window.$modal.alert(t('bo-fang-shi-bai'));\n                playing.value = false;\n            }\n        }\n\n        // 设置标题\n        if (song.name && song.author) {\n            document.title = song.name + \" - \" + song.author;\n        } else if (song.name) {\n            document.title = song.name;\n        }\n\n        // 清空歌词数据\n        lyricsData.value = [];\n        if(song?.isLocal) return;\n        // 保存当前歌曲到本地存储\n        localStorage.setItem('current_song', JSON.stringify(currentSong.value));\n\n        getVip();\n        // 获取歌词\n        getCurrentLyrics();\n        getMusicHighlights(currentSong.value.hash);\n    } catch (error) {\n        console.error('[PlayerControl] 播放音乐时发生错误:', error);\n        playing.value = false;\n        window.$modal.alert(t('bo-fang-chu-cuo'));\n    }\n};\n\n// 切换播放/暂停\nconst togglePlayPause = async () => {\n    if (!currentSong.value.hash) {\n        console.log('[PlayerControl] 没有当前歌曲，尝试播放队列中的下一首');\n        playSongFromQueue('next');\n        return;\n    } else if (!audio.src) {\n        console.log('[PlayerControl] 音频源为空，尝试重新设置');\n        if (currentSong.value.url) {\n            console.log('[PlayerControl] 从当前歌曲获取URL:', currentSong.value.url);\n            audio.src = currentSong.value.url;\n        } else {\n            console.log('[PlayerControl] 重新从队列获取歌曲');\n            const songIndex = musicQueueStore.queue.findIndex(song => song.hash === currentSong.value.hash);\n            if (songIndex !== -1) {\n                const song = musicQueueStore.queue[songIndex];\n                if (song.url) {\n                    console.log('[PlayerControl] 从队列中的歌曲获取URL:', song.url);\n                    currentSong.value.url = song.url;\n                    audio.src = song.url;\n                } else if (song.isCloud) {\n                    console.log('[PlayerControl] 云音乐没有URL，重新获取');\n                    addCloudMusicToQueue(song.hash, song.name, song.author, song.timeLength, song.img);\n                    return;\n                }else if(song.isLocal){\n                    console.log('[PlayerControl] 本地音乐没有URL，重新获取');\n                    addLocalMusicToQueue(song);\n                } else {\n                    console.log('[PlayerControl] 歌曲没有URL，重新获取');\n                    addSongToQueue(song.hash, song.name, song.img, song.author);\n                    return;\n                }\n            } else {\n                console.log('[PlayerControl] 歌曲不在队列中，播放下一首');\n                playSongFromQueue('next');\n                return;\n            }\n        }\n    }\n\n    if (playing.value) {\n        console.log('[PlayerControl] 暂停播放');\n        audio.pause();\n        playing.value = false;\n    } else {\n        console.log('[PlayerControl] 开始播放');\n        try {\n            await audio.play();\n            playing.value = true;\n        } catch (retryError) {\n            console.error('[PlayerControl] 播放失败:', retryError);\n            window.$modal.alert(t('bo-fang-shi-bai'));\n        }\n    }\n};\n\n// 从队列中播放歌曲\nconst playSongFromQueue = async (direction) => {\n    clearAutoSwitchTimer();\n\n    if (musicQueueStore.queue.length === 0) {\n        console.log('[PlayerControl] 队列为空');\n        window.$modal.alert(t('ni-huan-mei-you-tian-jia-ge-quo-kuai-qu-tian-jia-ba'));\n        return;\n    }\n\n    console.log(`[PlayerControl] 从队列播放${direction === 'next' ? '下' : '上'}一首`);\n    audio.pause();\n    playing.value = false;\n    if (direction == 'next' && NextSong.value.length > 0) {\n        // 添加下一首播放\n        console.log('[PlayerControl] 播放预定的下一首:', NextSong.value[0].name);\n        const songData = NextSong.value[0];\n        NextSong.value.shift();\n\n        try {\n            const result = await addSongToQueue(songData.hash, songData.name, songData.img, songData.author);\n            // 如果返回了歌曲对象，直接播放\n            if (result && result.song) {\n                console.log('[PlayerControl] 获取到下一首歌曲，开始播放:', result.song.name);\n                await playSong(result.song);\n            } else if (result && result.shouldPlayNext) {\n                console.log('[PlayerControl] 预定的下一首无法播放');\n                handleAutoSwitch();\n            } else {\n                console.error('[PlayerControl] 无法获取下一首歌曲信息');\n            }\n        } catch (error) {\n            console.error('[PlayerControl] 获取下一首歌曲时出错:', error);\n        }\n        return;\n    }\n\n    const currentIndex = musicQueueStore.queue.findIndex(song => song.hash === currentSong.value.hash);\n    console.log('[PlayerControl] 当前歌曲索引:', currentIndex);\n    let targetIndex;\n\n    // 处理不同播放模式\n    if (currentIndex === -1) {\n        targetIndex = 0;\n    } else if (currentPlaybackModeIndex.value === 0) {\n        // 随机播放\n        targetIndex = handleRandomPlayback(direction, currentIndex);\n    } else {\n        // 顺序播放或单曲循环\n        targetIndex = direction === 'previous'\n            ? (currentIndex === 0 ? musicQueueStore.queue.length - 1 : currentIndex - 1)\n            : (currentIndex + 1) % musicQueueStore.queue.length;\n    }\n\n    console.log('[PlayerControl] 目标歌曲索引:', targetIndex);\n\n    // 播放目标索引的歌曲\n    const targetSong = musicQueueStore.queue[targetIndex];\n    console.log('[PlayerControl] 开始播放目标歌曲:', targetSong.name);\n\n    try {\n        let result;\n        if (targetSong.isCloud) {\n            result = await addCloudMusicToQueue(\n                targetSong.hash,\n                targetSong.name,\n                targetSong.author,\n                targetSong.timeLength,\n                targetSong.img,\n                false // 不重置播放位置，只获取URL\n            );\n        } else if (targetSong.isLocal) {\n            result = await addLocalMusicToQueue(targetSong, false);\n        } else {\n            result = await addSongToQueue(\n                targetSong.hash,\n                targetSong.name,\n                targetSong.img,\n                targetSong.author,\n                false // 不重置播放位置，只获取URL\n            );\n        }\n\n        // 检查返回结果并播放\n        if (result && result.song) {\n            console.log('[PlayerControl] 成功获取歌曲URL，开始播放:', result.song.name);\n            await playSong(result.song);\n        } else if (result && result.shouldPlayNext) {\n            console.log('[PlayerControl] 云盘歌曲无法播放');\n            handleAutoSwitch();\n        } else {\n            console.error('[PlayerControl] 无法获取歌曲URL');\n        }\n    } catch (error) {\n        console.error('[PlayerControl] 切换歌曲时发生错误:', error);\n        // 如果出错，尝试播放下一首\n        if (direction === 'next') {\n            console.log('[PlayerControl] 发生错误，3秒后尝试播放下一首');\n            handleAutoSwitch();\n        }\n    }\n};\n\n// 处理随机播放逻辑\nconst handleRandomPlayback = (direction, currentIndex) => {\n    if (direction === 'previous' && currentStackIndex.value > 0) {\n        // 返回上一首随机歌曲\n        currentStackIndex.value--;\n        return playedSongsStack.value[currentStackIndex.value];\n    } else if (direction === 'previous') {\n        // 向前随机一首新歌曲\n        let newIndex;\n        let attempts = 0;\n        const maxAttempts = musicQueueStore.queue.length * 2; // 防止死循环\n        \n        do {\n            newIndex = Math.floor(Math.random() * musicQueueStore.queue.length);\n            attempts++;\n            \n            // 如果尝试次数过多，直接返回\n            if (attempts >= maxAttempts) {\n                break;\n            }\n        } while (playedSongsStack.value.length > 0 && \n                 (newIndex === playedSongsStack.value[currentStackIndex.value] ||\n                  (musicQueueStore.queue.length >= 10 && playedSongsStack.value.length > 0 && \n                   playedSongsStack.value.slice(-Math.min(10, playedSongsStack.value.length)).includes(newIndex))));\n\n        playedSongsStack.value.unshift(newIndex);\n        return newIndex;\n    } else if (direction === 'next' && currentStackIndex.value < playedSongsStack.value.length - 1) {\n        // 前进到下一首已随机过的歌曲\n        currentStackIndex.value++;\n        return playedSongsStack.value[currentStackIndex.value];\n    } else if (direction === 'next') {\n        // 随机一首新歌曲\n        let newIndex;\n        let attempts = 0;\n        const maxAttempts = musicQueueStore.queue.length * 2; // 防止死循环\n        \n        do {\n            newIndex = Math.floor(Math.random() * musicQueueStore.queue.length);\n            attempts++;\n            \n            // 如果尝试次数过多，直接返回\n            if (attempts >= maxAttempts) {\n                break;\n            }\n        } while (playedSongsStack.value.length > 0 && \n                 (newIndex === playedSongsStack.value[currentStackIndex.value] ||\n                  (musicQueueStore.queue.length >= 10 && playedSongsStack.value.length > 0 && \n                   playedSongsStack.value.slice(-Math.min(10, playedSongsStack.value.length)).includes(newIndex))));\n\n        // 截断未来的历史记录\n        if (currentStackIndex.value < playedSongsStack.value.length - 1) {\n            playedSongsStack.value = playedSongsStack.value.slice(0, currentStackIndex.value + 1);\n        }\n\n        // 添加新歌曲到历史记录\n        playedSongsStack.value.push(newIndex);\n        currentStackIndex.value = playedSongsStack.value.length - 1;\n        return newIndex;\n    }\n};\n\n// 音量拖动相关函数\nconst setVolumeOnClick = (event) => {\n    const slider = event.target.closest('.volume-slider');\n    if (slider) {\n        const sliderWidth = slider.offsetWidth;\n        const offsetX = event.offsetX;\n        volume.value = Math.round((offsetX / sliderWidth) * 100);\n        changeVolume();\n        console.log('[PlayerControl] 点击设置音量:', volume.value, '实际audio.volume:', audio.volume);\n    }\n};\n\nconst onDragStart = (event) => {\n    sliderElement.value = event.target.closest('.volume-slider');\n    if (sliderElement.value) {\n        isDragging.value = true;\n        setVolumeOnClick(event);\n        document.addEventListener('mousemove', onDrag);\n        document.addEventListener('mouseup', onDragEnd);\n    }\n};\nconst onDrag = (event) => {\n    if (isDragging.value && sliderElement.value) {\n        const sliderWidth = sliderElement.value.offsetWidth;\n        const rect = sliderElement.value.getBoundingClientRect();\n        const offsetX = event.clientX - rect.left;\n        const newVolume = Math.max(0, Math.min(100, Math.round((offsetX / sliderWidth) * 100)));\n        volume.value = newVolume;\n        changeVolume();\n        console.log('[PlayerControl] 拖动设置音量:', volume.value, '实际audio.volume:', audio.volume);\n    }\n};\nconst onDragEnd = () => {\n    isDragging.value = false;\n    sliderElement.value = null;\n    document.removeEventListener('mousemove', onDrag);\n    document.removeEventListener('mouseup', onDragEnd);\n};\n\n// 音量滚轮事件\nconst handleVolumeScroll = (event) => {\n    event.preventDefault();\n    const delta = Math.sign(event.deltaY) * -1;\n    volume.value = Math.min(Math.max(volume.value + delta * 10, 0), 100);\n    changeVolume();\n    console.log('[PlayerControl] 滚轮设置音量:', volume.value, '实际audio.volume:', audio.volume);\n};\n\n// 歌词滚轮控制播放进度\nconst handleLyricsWheel = (event) => {\n    if (!audio.duration || !currentSong.value?.hash) return;\n    \n    event.preventDefault();\n    const lyricsContainer = document.getElementById('lyrics-container');\n    if (!lyricsContainer) return;\n    const lineGroups = document.querySelectorAll('.line-group');\n    const firstLineElement = lineGroups[0];\n    const lastLineElement = lineGroups[lineGroups.length - 1];\n    if (!firstLineElement || (firstLineElement == lastLineElement)) return;\n    const lineHeight = lastLineElement.offsetHeight;\n    const containerHeight = lyricsContainer.offsetHeight;\n    // 计算滚动的距离\n    const scrollNumber = scrollAmount.value - (event.deltaY * 1.5);\n    const maxScrollNumber = ((containerHeight - firstLineElement.offsetHeight) / 2);\n    const miniScrollNumber = -lastLineElement.offsetTop + (containerHeight / 2) - (lineHeight / 2);\n    if (scrollNumber > maxScrollNumber) scrollAmount.value = maxScrollNumber;\n    else if (scrollNumber < miniScrollNumber) scrollAmount.value = miniScrollNumber;\n    else scrollAmount.value = scrollNumber;\n    lyricsFlag.value = true;\n    restoreLyricsScroll();\n};\n\nconst handleLyricsClick = (lineIndex) => {\n    // if (!lyricsFlag.value) return;\n    console.log('[PlayerControl] 点击歌词:', lineIndex);\n    const lineStartTime = lyricsData.value[lineIndex].characters[0].startTime;\n    audio.currentTime = lineStartTime / 1000;\n    resetLyricsHighlight(audio.currentTime);\n    scrollToCurrentLine(lineIndex);\n    lyricsFlag.value = false;\n    if (lyricScrollTimer) clearTimeout(lyricScrollTimer);\n    lyricScrollTimer = null;\n    // 如果音乐暂停了，自动开始播放\n    if (!playing.value) {\n        audio.play();\n    }\n}\n\n// 复制全部歌词到剪贴板\nconst copyLyricsToClipboard = async () => {\n    if (!showLyrics.value || !lyricsData.value || lyricsData.value.length === 0) {\n        return;\n    }\n    try {\n        let lyricsText = '';\n        lyricsData.value.forEach((lineData) => {\n            const originalLine = lineData.characters.map(char => char.char).join('');\n            lyricsText += originalLine + '\\n';\n            if (lineData.translated) {\n                lyricsText += lineData.translated + '\\n';\n            }\n            if (lineData.romanized) {\n                lyricsText += lineData.romanized + '\\n';\n            }\n            if (lineData.translated || lineData.romanized) {\n                lyricsText += '\\n';\n            }\n        });\n        await navigator.clipboard.writeText(lyricsText.trim());\n        $message.success('歌词已复制到剪贴板');\n    } catch (error) {\n        $message.error('复制歌词失败');\n    }\n};\n\n// 键盘快捷键\nconst handleKeyDown = (event) => {\n    const isInputFocused = ['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName);\n    if (isInputFocused) return;\n\n    switch (event.code) {\n        case 'Space':\n            event.preventDefault();\n            togglePlayPause();\n            break;\n        case 'ArrowLeft':\n            playSongFromQueue('previous');\n            break;\n        case 'ArrowRight':\n            playSongFromQueue('next');\n            break;\n        case 'Escape':\n            if (showLyrics.value) toggleLyrics(currentSong.value.hash, audio.currentTime);\n            break;\n        case 'KeyC':\n            // Ctrl+C 或 Cmd+C 复制歌词（仅在全屏歌词界面）\n            if ((event.ctrlKey || event.metaKey) && showLyrics.value) {\n                event.preventDefault();\n                copyLyricsToClipboard();\n            }\n            break;\n    }\n};\n\n// 初始化系统媒体快捷键\nconst setupMediaShortcuts = () => {\n    if (!isElectron()) return;\n\n    window.electron.ipcRenderer.on('play-previous-track', () => playSongFromQueue('previous'));\n    window.electron.ipcRenderer.on('play-next-track', () => playSongFromQueue('next'));\n    window.electron.ipcRenderer.on('volume-up', () => {\n        volume.value = Math.min(volume.value + 10, 100);\n        changeVolume();\n    });\n    window.electron.ipcRenderer.on('volume-down', () => {\n        volume.value = Math.max(volume.value - 10, 0);\n        changeVolume();\n    });\n    window.electron.ipcRenderer.on('toggle-play-pause', togglePlayPause);\n    window.electron.ipcRenderer.on('toggle-mute', toggleMute);\n    window.electron.ipcRenderer.on('toggle-like', () => playlistSelect.value.toLike());\n    window.electron.ipcRenderer.on('toggle-mode', togglePlaybackMode);\n    window.electron.ipcRenderer.on('url-params', (_event, data) => {\n        console.log('[PlayerControl] 接收到URL参数:', data);\n\n        // 处理歌曲哈希参数\n        if (data.hash) {\n            console.log('[PlayerControl] 从URL启动播放歌曲:', data.hash);\n            songQueue.privilegeSong(data.hash).then(res => {\n                if (res.status == 1) {\n                    const songInfo = res.data[0];\n                    addSongToQueue(songInfo.hash, songInfo.albumname, getCover(songInfo.info.image, 480), songInfo.singername)\n                }\n            })\n        }else if (data.listid) {\n            // 处理歌单ID参数\n            console.log('[PlayerControl] 从URL启动跳转到歌单:', data.listid);\n            router.push({\n                path: '/PlaylistDetail',\n                query: { global_collection_id: data.listid }\n            });\n        }\n    });\n};\n\n// 切换静音\nconst toggleMute = () => {\n    isMuted.value = !isMuted.value;\n    audio.muted = isMuted.value;\n    if (isMuted.value) volume.value = 0;\n    else volume.value = audio.volume * 100;\n    localStorage.setItem('player_volume', volume.value);\n    console.log('[PlayerControl] 切换静音:', isMuted.value, '音量:', volume.value, '实际audio.volume:', audio.volume);\n};\n\nconst pausePlayback = (reason) => {\n    clearAutoSwitchTimer();\n    if (!audio.paused) audio.pause();\n    playing.value = false;\n    mediaSession.clearPositionState?.();\n    if (reason) console.log('[PlayerControl] 暂停播放:', reason);\n};\n\nconst showSpeedMenu = ref(false);\nconst currentSpeed = ref(1.0);\nconst playbackSpeeds = [0.5, 0.75, 1.0, 1.25, 1.5, 2.0];\n\n// 监听音频输出设备变化（例如插拔耳机/切换声卡），变化时暂停播放\nlet cleanupAudioOutputDeviceWatcher = null;\nlet lastAudioOutputDeviceSignature = null;\nlet audioOutputDeviceChangeHandler = null;\n\nconst setupAudioOutputDeviceWatcher = () => {\n    if (cleanupAudioOutputDeviceWatcher) return;\n    if (typeof navigator === 'undefined' || !navigator.mediaDevices) return;\n\n    void getAudioOutputDeviceSignature().then(signature => {\n        lastAudioOutputDeviceSignature = signature;\n    }).catch(() => {\n        lastAudioOutputDeviceSignature = null;\n    });\n\n    const handler = throttle(() => {\n        void (async () => {\n            try {\n                const signature = await getAudioOutputDeviceSignature();\n                if (signature === null) return;\n                if (lastAudioOutputDeviceSignature === null) {\n                    lastAudioOutputDeviceSignature = signature;\n                    return;\n                }\n                if (signature !== lastAudioOutputDeviceSignature) {\n                    lastAudioOutputDeviceSignature = signature;\n                    if (!audio.paused) pausePlayback('检测到音频输出设备变化');\n                }\n            } catch (error) {\n                console.warn('[PlayerControl] 获取音频输出设备信息失败:', error);\n            }\n        })();\n    }, 800);\n\n    if (navigator.mediaDevices.addEventListener) {\n        navigator.mediaDevices.addEventListener('devicechange', handler);\n        cleanupAudioOutputDeviceWatcher = () => {\n            navigator.mediaDevices.removeEventListener('devicechange', handler);\n        };\n    } else if ('ondevicechange' in navigator.mediaDevices) {\n        const previous = navigator.mediaDevices.ondevicechange;\n        navigator.mediaDevices.ondevicechange = handler;\n        cleanupAudioOutputDeviceWatcher = () => {\n            navigator.mediaDevices.ondevicechange = previous;\n        };\n    }\n};\n\nconst setAudioOutputDeviceWatcherEnabled = (enabled) => {\n    if (enabled) {\n        setupAudioOutputDeviceWatcher();\n        return;\n    }\n    cleanupAudioOutputDeviceWatcher?.();\n    cleanupAudioOutputDeviceWatcher = null;\n    lastAudioOutputDeviceSignature = null;\n};\n\nlet audioOutputDeviceWatchChangeHandler = null;\n\nconst applyAudioOutputDevice = async (deviceId) => {\n    if (typeof audio?.setSinkId !== 'function') {\n        console.warn('[PlayerControl] 当前环境不支持切换音频输出设备（setSinkId不可用）');\n        return false;\n    }\n\n    const sinkId = deviceId || 'default';\n    try {\n        await audio.setSinkId(sinkId);\n        console.log('[PlayerControl] 已切换音频输出设备:', sinkId);\n        return true;\n    } catch (error) {\n        console.warn('[PlayerControl] 切换音频输出设备失败:', error);\n        window.$modal.alert('切换音频输出设备失败,请刷新页面后重试');\n        return false;\n    }\n};\n\n// 切换速度菜单\nconst toggleSpeedMenu = () => {\n    showSpeedMenu.value = !showSpeedMenu.value;\n};\n\n// 改变播放速度\nconst changePlaybackSpeed = (speed) => {\n    currentSpeed.value = speed;\n    setPlaybackRate(speed);\n    showSpeedMenu.value = false;\n    \n    // 更新SMTC位置状态以反映新的播放速率\n    if (audio.duration && currentSong.value?.hash) {\n        mediaSession.updatePositionState(audio.currentTime, audio.duration, speed);\n    }\n};\n\n// 跳转到搜索页面搜索歌曲\nconst searchSong = (songName) => {\n    // 关闭全屏歌词\n    if (showLyrics.value) {\n        toggleLyrics(currentSong.value.hash, audio.currentTime);\n    }\n    if (!songName) return;\n    router.push({\n        path: '/search',\n        query: { q: songName }\n    });\n};\n\n// 组件挂载\nonMounted(() => {\n    console.log('[PlayerControl] 组件挂载');\n\n    // 初始化音频设置\n    audioController.initAudio();\n\n    const savedSettings = JSON.parse(localStorage.getItem('settings') || '{}');\n    setAudioOutputDeviceWatcherEnabled(savedSettings.pauseOnAudioOutputChange === 'on');\n    void applyAudioOutputDevice(savedSettings.audioOutputDevice);\n\n    audioOutputDeviceWatchChangeHandler = (event) => {\n        const enabled = !!event?.detail?.enabled;\n        setAudioOutputDeviceWatcherEnabled(enabled);\n    };\n    window.addEventListener('audio-output-device-watch-change', audioOutputDeviceWatchChangeHandler);\n\n    audioOutputDeviceChangeHandler = (event) => {\n        const deviceId = event?.detail?.deviceId || 'default';\n        void applyAudioOutputDevice(deviceId);\n    };\n    window.addEventListener('audio-output-device-change', audioOutputDeviceChangeHandler);\n\n    // 监听响度规格化开关变更\n    const handleLoudnessChange = (event) => {\n        const enabled = event.detail.enabled;\n        console.log('[PlayerControl] 响度规格化开关变更:', enabled);\n        toggleLoudnessNormalization(enabled);\n    };\n    window.addEventListener('loudness-normalization-change', handleLoudnessChange);\n\n    // 初始化歌曲和播放状态\n    const current_song = localStorage.getItem('current_song');\n    if (current_song) {\n        try {\n            const savedSong = JSON.parse(current_song);\n            currentSong.value = savedSong;\n\n            // 如果有URL，恢复播放源\n            if (savedSong.url) {\n                if(savedSong.isLocal) return;\n                console.log('[PlayerControl] 从缓存恢复音频源:', savedSong.url);\n                audio.src = savedSong.url;\n            } else {\n                console.log('[PlayerControl] 缓存的歌曲没有URL');\n            }\n        } catch (error) {\n            console.error('[PlayerControl] 解析保存的歌曲信息失败:', error);\n        }\n    }\n\n    // 初始化播放模式\n    playbackMode.initPlaybackMode();\n\n    // 初始化设置\n    const settings = JSON.parse(localStorage.getItem('settings') || '{}');\n    if (settings) {\n        lyricsBackground.value = settings?.lyricsBackground || 'on';\n        lyricsFontSize.value = settings?.lyricsFontSize || '24px';\n    }\n\n    // 设置媒体会话\n    mediaSession.initMediaSession({\n        togglePlayPause,\n        playPrevious: () => playSongFromQueue('previous'),\n        playNext: () => playSongFromQueue('next'),\n        seekBackward: (seekOffset) => {\n            if (audio.currentTime > seekOffset) {\n                audio.currentTime -= seekOffset;\n            } else {\n                audio.currentTime = 0;\n            }\n        },\n        seekForward: (seekOffset) => {\n            if (audio.currentTime + seekOffset < audio.duration) {\n                audio.currentTime += seekOffset;\n            } else {\n                audio.currentTime = audio.duration;\n            }\n        },\n        seekTo: (seekTime) => {\n            if (seekTime >= 0 && seekTime <= audio.duration) {\n                audio.currentTime = seekTime;\n            }\n        }\n    });\n\n    // 设置系统媒体快捷键\n    setupMediaShortcuts();\n\n    // 恢复播放进度\n    if (current_song && localStorage.getItem('player_progress')) {\n        const savedProgress = localStorage.getItem('player_progress');\n        audio.currentTime = savedProgress;\n        console.log('[PlayerControl] 恢复播放进度:', savedProgress);\n        progressWidth.value = (audio.currentTime / currentSong.value.timeLength) * 100;\n    }\n\n    // 恢复播放速度设置\n    const savedSpeed = localStorage.getItem('player_speed');\n    if (savedSpeed) {\n        currentSpeed.value = parseFloat(savedSpeed);\n        setPlaybackRate(currentSpeed.value);\n    }\n\n    // 获取VIP\n    getVip();\n\n    // 添加事件监听\n    document.addEventListener('keydown', handleKeyDown);\n\n    // 设置特定于PlayerControl的监听器\n    audio.addEventListener('pause', () => {\n        playing.value = false;\n        console.log('[PlayerControl] 暂停事件');\n        // 暂停时清除SMTC位置状态\n        mediaSession.clearPositionState();\n        if (isElectron()) window.electron.ipcRenderer.send('play-pause-action', playing.value, audio.currentTime);\n    });\n\n    audio.addEventListener('play', () => {\n        playing.value = true;\n        console.log('[PlayerControl] 播放事件');\n        if (!lyricsData.value.length) getCurrentLyrics();\n        if (isElectron()) window.electron.ipcRenderer.send('play-pause-action', playing.value, audio.currentTime);\n    });\n\n    audio.addEventListener('error', (e) => {\n        console.log('[PlayerControl] 音频错误代码:', audio.error?.code);\n        console.error('[PlayerControl] 音频错误:', e);\n        if(audio.error?.code == 4){\n            addSongToQueue(currentSong.value.hash, currentSong.value.name, currentSong.value.img, currentSong.value.author);\n        }else{\n            window.$modal.alert(t('yin-pin-jia-zai-shi-bai'));\n        }\n    });\n\n    console.log('[PlayerControl] 音频初始化完成');\n});\n\n// 监听歌词数据变化，同步歌词到当前播放进度\nwatch(lyricsData, (newLyrics) => {\n    if (newLyrics && newLyrics.length > 0 && audio.currentTime > 0) {\n        console.log('[PlayerControl] 歌词数据加载完成，同步到当前播放进度:', audio.currentTime);\n        highlightCurrentChar(audio.currentTime, false);\n        const currentLineIndex = getCurrentLineIndex(audio.currentTime);\n        scrollToCurrentLine(currentLineIndex);\n    }\n});\n\n// 组件卸载清理\nonUnmounted(() => {\n    // 清除自动切换定时器\n    clearAutoSwitchTimer();\n\n    if (audioOutputDeviceWatchChangeHandler) {\n        window.removeEventListener('audio-output-device-watch-change', audioOutputDeviceWatchChangeHandler);\n        audioOutputDeviceWatchChangeHandler = null;\n    }\n    if (audioOutputDeviceChangeHandler) {\n        window.removeEventListener('audio-output-device-change', audioOutputDeviceChangeHandler);\n        audioOutputDeviceChangeHandler = null;\n    }\n\n    cleanupAudioOutputDeviceWatcher?.();\n    cleanupAudioOutputDeviceWatcher = null;\n\n    // 移除响度规格化事件监听\n    window.removeEventListener('loudness-normalization-change', () => {});\n\n    // 使用AudioController的销毁方法清理基本监听器\n    audioController.destroy();\n\n    // 清理组件特定的监听器\n    audio.removeEventListener('pause', () => { });\n    audio.removeEventListener('play', () => { });\n    audio.removeEventListener('error', () => { });\n\n    // 清理系统媒体快捷键\n    if (isElectron()) {\n        window.electron.ipcRenderer.removeAllListeners('play-previous-track');\n        window.electron.ipcRenderer.removeAllListeners('play-next-track');\n        window.electron.ipcRenderer.removeAllListeners('volume-up');\n        window.electron.ipcRenderer.removeAllListeners('volume-down');\n        window.electron.ipcRenderer.removeAllListeners('toggle-play-pause');\n        window.electron.ipcRenderer.removeAllListeners('toggle-mute');\n        window.electron.ipcRenderer.removeAllListeners('toggle-like');\n        window.electron.ipcRenderer.removeAllListeners('toggle-mode');\n    }\n\n    // 清理键盘事件\n    document.removeEventListener('keydown', handleKeyDown);\n});\n\n// 对外暴露接口\ndefineExpose({\n    playing,\n    pause: () => {\n        pausePlayback();\n    },\n    addSongToQueue: async (hash, name, img, author) => {\n        clearAutoSwitchTimer();\n\n        console.log('[PlayerControl] 外部调用addSongToQueue:', name);\n        audio.pause();\n        playing.value = false;\n        const result = await addSongToQueue(hash, name, img, author);\n        if (result && result.song) {\n            await playSong(result.song);\n        } else if (result && result.shouldPlayNext) {\n            console.log('[PlayerControl] 歌曲无法播放');\n            handleAutoSwitch();\n        }\n        return result;\n    },\n    addLocalMusicToQueue: async (localSong) => {\n        clearAutoSwitchTimer();\n\n        console.log('[PlayerControl] 外部调用addLocalMusicToQueue:', localSong.name);\n        audio.pause();\n        playing.value = false;\n        \n        const result = await addLocalMusicToQueue(localSong);\n        if (result && result.song) {\n            await playSong(result.song);\n            console.log('[PlayerControl] 本地音乐播放成功:', localSong.name);\n            return { song: result.song };\n        } else {\n            console.error('[PlayerControl] 播放本地音乐失败');\n            return { error: true };\n        }\n    },\n    addLocalPlaylistToQueue: async (localSongs, append = false) => {\n        console.log('[PlayerControl] 外部调用addLocalPlaylistToQueue:', localSongs.length, '首歌曲');\n        \n        const queueSongs = await addLocalPlaylistToQueue(localSongs, append);\n        \n        // 如果不是追加模式，自动播放第一首\n        if (!append && queueSongs.length > 0) {\n            let songIndex = 0;\n            \n            // 如果是随机播放模式，则随机选择一首歌曲\n            if (currentPlaybackModeIndex.value == 0) {\n                songIndex = Math.floor(Math.random() * queueSongs.length);\n                console.log('[PlayerControl] 随机模式下添加本地歌单后随机播放:', queueSongs[songIndex].name);\n            } else {\n                console.log('[PlayerControl] 添加本地歌单后自动播放第一首:', queueSongs[0].name);\n            }\n            \n            clearAutoSwitchTimer();\n            audio.pause();\n            playing.value = false;\n            \n            const result = await addLocalMusicToQueue(queueSongs[songIndex]);\n            if (result && result.song) {\n                await playSong(result.song);\n            }\n        }\n        \n        return queueSongs;\n    },\n    getPlaylistAllSongs,\n    addPlaylistToQueue: async (info, append = false) => {\n        const songs = await addPlaylistToQueue(info, append);\n        if (songs && songs.length > 0 && !append) {\n            // 根据播放模式决定播放哪首歌曲\n            let songIndex = 0;\n\n            // 如果是随机播放模式，则随机选择一首歌曲\n            if (currentPlaybackModeIndex.value == 0) {\n                songIndex = Math.floor(Math.random() * songs.length);\n                console.log('[PlayerControl] 随机模式下添加歌单后随机播放:', songs[songIndex].name);\n            } else {\n                console.log('[PlayerControl] 添加歌单后自动播放第一首:', songs[0].name);\n            }\n            audio.pause();\n            playing.value = false;\n            // 播放选中的歌曲\n            const result = await addSongToQueue(\n                songs[songIndex].hash,\n                songs[songIndex].name,\n                songs[songIndex].img,\n                songs[songIndex].author,\n                true\n            );\n            if (result && result.song) {\n                await playSong(result.song);\n            }\n        }\n        return songs;\n    },\n    addToNext,\n    addCloudMusicToQueue: async (hash, name, author, timeLength, cover) => {\n        clearAutoSwitchTimer();\n\n        console.log('[PlayerControl] 外部调用addCloudMusicToQueue:', name);\n        audio.pause();\n        playing.value = false;\n        const result = await addCloudMusicToQueue(hash, name, author, timeLength, cover);\n        if (result && result.song) {\n            await playSong(result.song);\n    } else if (result && result.shouldPlayNext) {\n        console.log('[PlayerControl] 云盘歌曲无法播放');\n        handleAutoSwitch();\n        }\n        return result;\n    },\n    addCloudPlaylistToQueue: async (songs, append = false) => {\n        const queueSongs = await addCloudPlaylistToQueue(songs, append);\n        if (queueSongs && queueSongs.length > 0 && !append) {\n            // 根据播放模式决定播放哪首歌曲\n            let songIndex = 0;\n\n            // 如果是随机播放模式，则随机选择一首歌曲\n            if (currentPlaybackModeIndex.value == 0) {\n                songIndex = Math.floor(Math.random() * queueSongs.length);\n                console.log('[PlayerControl] 随机模式下添加云盘歌单后随机播放:', queueSongs[songIndex].name);\n            } else {\n                console.log('[PlayerControl] 添加云盘歌单后自动播放第一首:', queueSongs[0].name);\n            }\n\n            // 播放选中的歌曲\n            const result = await addCloudMusicToQueue(\n                queueSongs[songIndex].hash,\n                queueSongs[songIndex].name,\n                queueSongs[songIndex].author,\n                queueSongs[songIndex].timeLength,\n                queueSongs[songIndex].cover,\n                true\n            );\n            if (result && result.song) {\n                await playSong(result.song);\n            }\n        }\n        return queueSongs;\n    },\n    currentSong\n});\n\n// 从播放队列接收事件\nconst onQueueSongAdd = async (hash, name, img, author) => {\n    clearAutoSwitchTimer();\n\n    console.log('[PlayerControl] 从播放队列收到addSongToQueue事件:', name);\n    audio.pause();\n    playing.value = false;\n    const result = await addSongToQueue(hash, name, img, author);\n    if (result && result.song) {\n        await playSong(result.song);\n    } else if (result && result.shouldPlayNext) {\n        console.log('[PlayerControl] 歌曲无法播放');\n        handleAutoSwitch();\n    }\n};\n\nconst onQueueCloudSongAdd = async (hash, name, author, timeLength, cover) => {\n    clearAutoSwitchTimer();\n\n    console.log('[PlayerControl] 从播放队列收到addCloudMusicToQueue事件:', name);\n    const result = await addCloudMusicToQueue(hash, name, author, timeLength, cover);\n    if (result && result.song) {\n        await playSong(result.song);\n    } else if (result && result.shouldPlayNext) {\n        console.log('[PlayerControl] 云盘歌曲无法播放');\n        handleAutoSwitch();\n    }\n};\n\nconst onQueueLocalSongAdd = async (item) => {\n    clearAutoSwitchTimer();\n    audio.pause();\n    playing.value = false;\n    \n    console.log('[PlayerControl] 从播放队列收到addLocalMusicToQueue事件:', item.name);\n    const result = await addLocalMusicToQueue(item);\n    if (result && result.song) {\n        await playSong(result.song);\n    } else if (result && result.shouldPlayNext) {\n        console.log('[PlayerControl] 本地音乐无法播放');\n        handleAutoSwitch();\n    }\n};\n</script>\n\n<style scoped>\n@import '@/assets/style/PlayerControl.css';\n</style>\n"
  },
  {
    "path": "src/components/PlaylistGrid.vue",
    "content": "<template>\n  <div class=\"playlist-grid\">\n    <div v-for=\"(playlist, index) in playlists\" :key=\"index\" class=\"playlist-card\" @click=\"onPlaylistClick(playlist)\">\n      <div class=\"playlist-cover\">\n        <img :src=\"(playlist.img || './assets/images/ico.png').replace('/150/','/480/')\"/>\n        <div class=\"playlist-overlay\">\n          <button class=\"play-button\">\n            <i class=\"fas fa-play\"></i>\n          </button>\n        </div>\n      </div>\n      <div class=\"playlist-info\">\n        <h3 class=\"playlist-name\" :title=\"playlist.specialname\">{{ playlist.specialname }}</h3>\n        <div class=\"playlist-creator\">\n          <i class=\"fas fa-user\"></i>\n          <span>{{ playlist.nickname }}</span>\n        </div>\n        <div class=\"playlist-meta\">\n          <div class=\"meta-item\">\n            <i class=\"fas fa-music\"></i>\n            <span>{{ playlist.song_count }}首</span>\n          </div>\n          <div class=\"meta-item\">\n            <i class=\"fas fa-play-circle\"></i>\n            <span>{{ formatPlayCount(playlist.play_count) }}</span>\n          </div>\n          <div class=\"meta-item\" v-if=\"playlist.publish_time\">\n            <i class=\"fas fa-calendar-alt\"></i>\n            <span>{{ formatDate(playlist.publish_time) }}</span>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script setup>\nconst props = defineProps({\n  playlists: {\n    type: Array,\n    required: true,\n    default: () => []\n  }\n});\n\nconst emit = defineEmits(['playlist-click']);\n\nconst onPlaylistClick = (playlist) => {\n  emit('playlist-click', playlist);\n};\n\n// 格式化播放次数\nconst formatPlayCount = (count) => {\n  if (!count) return '0';\n  count = parseInt(count);\n  if (count < 10000) {\n    return count.toString();\n  } else if (count < 100000000) {\n    return (count / 10000).toFixed(1) + '万';\n  } else {\n    return (count / 100000000).toFixed(1) + '亿';\n  }\n};\n\n// 格式化日期\nconst formatDate = (dateStr) => {\n  if (!dateStr) return '';\n  // 只保留年月日\n  return dateStr.split(' ')[0];\n};\n</script>\n\n<style scoped>\n.playlist-grid {\n  display: grid;\n  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));\n  gap: 20px;\n  margin-bottom: 20px;\n}\n\n.playlist-card {\n  display: flex;\n  flex-direction: column;\n  background-color: #fff;\n  border-radius: 10px;\n  overflow: hidden;\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);\n  transition: transform 0.3s, box-shadow 0.3s;\n  cursor: pointer;\n  height: 100%;\n}\n\n.playlist-card:hover {\n  transform: translateY(-5px);\n  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);\n}\n\n.playlist-cover {\n  position: relative;\n  width: 100%;\n  padding-top: 100%; /* 1:1 宽高比 */\n  overflow: hidden;\n}\n\n.playlist-cover img {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  transition: transform 0.5s;\n}\n\n.playlist-card:hover .playlist-cover img {\n  transform: scale(1.05);\n}\n\n.playlist-overlay {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-color: rgba(0, 0, 0, 0.3);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  opacity: 0;\n  transition: opacity 0.3s;\n}\n\n.playlist-card:hover .playlist-overlay {\n  opacity: 1;\n}\n\n.play-button {\n  width: 50px;\n  height: 50px;\n  border-radius: 50%;\n  background-color: var(--primary-color);\n  border: none;\n  color: white;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  cursor: pointer;\n  transition: transform 0.2s, background-color 0.2s;\n}\n\n.play-button:hover {\n  transform: scale(1.1);\n  background-color: var(--primary-color-dark, #d81e06);\n}\n\n.play-button i {\n  font-size: 20px;\n}\n\n.playlist-info {\n  padding: 15px;\n  display: flex;\n  flex-direction: column;\n  flex: 1;\n}\n\n.playlist-name {\n  font-size: 16px;\n  font-weight: bold;\n  margin: 0 0 8px 0;\n  color: #333;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.playlist-creator {\n  display: flex;\n  align-items: center;\n  gap: 5px;\n  font-size: 14px;\n  color: #666;\n  margin-bottom: 10px;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.playlist-creator i {\n  font-size: 12px;\n  color: var(--primary-color);\n}\n\n.playlist-meta {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n  margin-top: auto;\n}\n\n.meta-item {\n  display: flex;\n  align-items: center;\n  gap: 5px;\n  font-size: 12px;\n  color: #888;\n  background-color: #f5f5f5;\n  padding: 4px 8px;\n  border-radius: 4px;\n}\n\n.meta-item i {\n  font-size: 12px;\n  color: var(--primary-color);\n}\n\n/* 响应式调整 */\n@media (max-width: 768px) {\n  .playlist-grid {\n    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n    gap: 15px;\n  }\n  \n  .playlist-name {\n    font-size: 14px;\n  }\n  \n  .playlist-creator {\n    font-size: 12px;\n  }\n  \n  .meta-item {\n    font-size: 10px;\n    padding: 3px 6px;\n  }\n}\n</style>"
  },
  {
    "path": "src/components/PlaylistSelectModal.vue",
    "content": "<template>\n    <transition name=\"fade\">\n        <div v-if=\"isOpen\" class=\"modal\">\n            <div class=\"modal-content\">\n                <h3>{{ t('shou-cang-dao') }}</h3>\n                <ul class=\"playlist-select-list\" v-if=\"playlists.length > 0\">\n                    <li v-for=\"playlist in playlists\" \n                        :key=\"playlist.list_id\" \n                        @click=\"addToPlaylist(playlist.listid, currentSong); isOpen = false\">\n                        {{ playlist.name }}\n                    </li>\n                </ul>\n                <div v-else>{{ t('mei-you-ge-dan') }}</div>\n                <button class=\"close-btn-modal\" @click=\"isOpen = false\">{{ t('guan-bi-an-niu') }}</button>\n            </div>\n        </div>\n    </transition>\n</template>\n\n<script setup>\nimport { ref } from 'vue';\nimport { get } from '../utils/request';\nimport { useI18n } from 'vue-i18n';\nimport { MoeAuthStore } from '../stores/store';\n\nconst MoeAuth = MoeAuthStore();\nconst { t } = useI18n();\n\nconst props = defineProps({\n    currentSong: {\n        type: Object,\n        required: true\n    }\n});\nconst playlists = ref([]);\nconst isOpen = ref(false);\n\nconst validateUserAndSong = () => {\n    if (!MoeAuth.isAuthenticated) {\n        window.$modal.alert(t('qing-xian-deng-lu'));\n        return false;\n    }\n    if (Array.isArray(props.currentSong)) {\n        if(!props.currentSong[0].hash){\n            window.$modal.alert('没有选择正确的歌曲');\n            return false;\n        }\n    }else if (!props.currentSong.hash) {\n        window.$modal.alert(t('mei-you-zheng-zai-bo-fang-de-ge-qu'));\n        return false;\n    }\n    if (props.currentSong.isCloud) {\n        window.$modal.alert('云盘音乐不支持添加到歌单');\n        return false;\n    }\n    if(props.currentSong.isLocal){\n        window.$modal.alert('本地音乐不支持添加到歌单');\n        return false;\n    }\n    return true;\n};\n\nconst fetchPlaylists = async () => {\n    try {\n        const playlistResponse = await get('/user/playlist', {\n            pagesize: 100\n        });\n        if (playlistResponse.status !== 1) {\n            $message.error(t('huo-qu-ge-dan-shi-bai'));\n            return;\n        }\n        playlists.value = playlistResponse.data.info.filter(\n            playlist => playlist.list_create_userid === MoeAuth.UserInfo.userid\n        );\n        isOpen.value = true;\n    } catch (error) {\n        $message.error(t('huo-qu-ge-dan-shi-bai'));\n        isOpen.value = false;\n    }\n};\n\nconst addToPlaylist = async (listid, song) => {\n    if (!validateUserAndSong()) return;\n    let song_data = '';\n    if(Array.isArray(song)){\n        song_data = song.map(s => `${encodeURIComponent(s.name.replace(',', ''))}|${s.hash}`).join(',');\n    }else{\n        song_data = `${encodeURIComponent(song.name.replace(',', ''))}|${song.hash}`;\n    }\n    try {\n        await get(`/playlist/tracks/add?listid=${listid}&data=${song_data}`);\n        $message.success(t('cheng-gong-tian-jia-dao-ge-dan'));\n    } catch (error) {\n        $message.error(t('tian-jia-dao-ge-dan-shi-bai'));\n    }\n    isOpen.value = false;\n};\n\nconst toLike = () => {\n    const like_id = localStorage.getItem('like');\n    if(!like_id) {window.$modal.alert('先去看看你的收藏夹吧');return;}\n    addToPlaylist(like_id, props.currentSong);\n};\n\ndefineExpose({\n    toLike,\n    fetchPlaylists,\n    validateUserAndSong,\n    addToPlaylist\n});\n</script>\n\n<style scoped>\n.modal {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background-color: rgba(0, 0, 0, 0.5);\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    z-index: 1000;\n}\n\n.modal-content {\n    background-color: var(--background-color);\n    padding: 20px;\n    border-radius: 8px;\n    min-width: 300px;\n    max-width: 90%;\n    max-height: 90vh;\n    overflow-y: auto;\n}\n\n.playlist-select-list {\n    list-style: none;\n    padding: 0;\n    margin: 15px 0;\n    max-height: 300px;\n    overflow-y: auto;\n}\n\n.playlist-select-list li {\n    padding: 10px;\n    cursor: pointer;\n    transition: background-color 0.2s;\n    border-radius: 8px;\n}\n\n.playlist-select-list li:hover {\n    background-color: var(--secondary-color);\n}\n\n.modal .close-btn-modal {\n    width: 100%;\n    padding: 8px;\n    background-color: var(--primary-color);\n    color: white;\n    border: none;\n    border-radius: 4px;\n    cursor: pointer;\n    margin-top: 10px;\n}\n\n.modal .close-btn-modal:hover {\n    opacity: 0.9;\n}\n</style>"
  },
  {
    "path": "src/components/QueueList.vue",
    "content": "<template>\n    <transition name=\"fade\">\n        <div v-if=\"showQueue\" class=\"queue-popup\">\n            <div class=\"queue-header\">\n                <h3>\n                    <span>{{ $t('bo-fang-lie-biao') }}</span> ({{ musicQueueStore.queue.length }})\n                    <i class=\"fas fa-trash-alt close-store\" @click=\"musicQueueStore.clearQueue()\" title=\"close\"></i>\n                </h3>\n            </div>\n\n            <RecycleScroller :items=\"musicQueueStore.queue\" :item-size=\"50\" key-field=\"id\" :buffer=\"200\"\n                :items-limit=\"2000\" :prerender=\"Math.min(10, musicQueueStore.queue.length)\" ref=\"queueScroller\"\n                class=\"queue-list\">\n                <template #default=\"{ item, index }\">\n                    <li class=\"queue-item\" :class=\"{ 'playing': currentSong.hash == item.hash }\" :key=\"item.id\">\n                        <div class=\"queue-song-info\">\n                            <span class=\"queue-song-title\">{{ item.name }}</span>\n                            <span class=\"queue-artist\">{{ $formatMilliseconds(item.timeLength) }}</span>\n                        </div>\n                        <div class=\"queue-actions\">\n                            <button v-if=\"currentSong.hash == item.hash\"\n                                class=\"queue-play-btn fas fa-music\"></button>\n                            <template v-else>\n                                <button class=\"queue-play-btn\" @click=\"playQueueItem(item)\"><i class=\"fas fa-play\"></i></button>\n                                <i class=\"fas fa-times close-store\"\n                                    @click=\"removeSongFromQueue(index);\"></i>\n                            </template>\n                        </div>\n                    </li>\n                </template>\n            </RecycleScroller>\n        </div>\n    </transition>\n</template>\n\n<script setup>\nimport { ref, nextTick, onMounted, onUnmounted } from 'vue';\nimport { RecycleScroller } from 'vue3-virtual-scroller';\nimport { useMusicQueueStore } from '../stores/musicQueue';\nimport 'vue3-virtual-scroller/dist/vue3-virtual-scroller.css';\n\nconst props = defineProps({\n    currentSong: {\n        type: Object,\n        required: true\n    }\n});\n\nconst emit = defineEmits(['update:showQueue', 'addSongToQueue', 'addCloudMusicToQueue', 'addLocalMusicToQueue']);\n\nconst musicQueueStore = useMusicQueueStore();\nconst queueScroller = ref(null);\nconst showQueue = ref(false);\n\n// 从队列中删除歌曲\nconst removeSongFromQueue = (index) => {\n    const updatedQueue = [...musicQueueStore.queue];\n    updatedQueue.splice(index, 1);\n    updatedQueue.forEach((song, i) => {\n        song.id = i + 1;\n    });\n    musicQueueStore.setQueue(updatedQueue);\n};\n\n// 播放队列中的歌曲项\nconst playQueueItem = (item) => {\n    console.log('[QueueList] 点击播放队列中的歌曲:', item.name);\n    showQueue.value = false; // 点击后关闭播放队列面板\n    if (item.isCloud) {\n        emit('addCloudMusicToQueue', item.hash, item.name, item.author, item.timeLength, item.img);\n    }else if(item.isLocal){\n        emit('addLocalMusicToQueue', item);\n    } else {\n        emit('addSongToQueue', item.hash, item.name, item.img, item.author);\n    }\n};\n\n// 滚动到当前播放歌曲位置\nconst openQueue = async () => {\n    showQueue.value = !showQueue.value;\n    if (showQueue.value) {\n        await nextTick();\n        setTimeout(() => {\n            const currentIndex = musicQueueStore.queue.findIndex(song => song.hash === props.currentSong.hash);\n            queueScroller.value.scrollToItem(currentIndex - 3);\n        }, 100);\n    }\n};\n\nconst handleClickOutside = (event) => {\n    const queuePopup = document.querySelector('.queue-popup');\n    if (queuePopup && !queuePopup.contains(event.target) && !event.target.closest('.extra-btn')) {\n        showQueue.value = false;\n    }\n};\n\nonMounted(() => {\n    document.addEventListener('click', handleClickOutside);\n});\n\nonUnmounted(() => {\n    document.removeEventListener('click', handleClickOutside);\n});\n\ndefineExpose({\n    openQueue,\n    removeSongFromQueue\n});\n</script>\n<style scoped>\n.queue-popup {\n    position: fixed;\n    right: 20px;\n    bottom: 100px;\n    width: 300px;\n    max-height: 400px;\n    background: #fff;\n    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);\n    border-radius: 10px;\n    padding: 10px;\n    z-index: 2;\n    overflow-y: auto;\n}\n\n.queue-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 10px;\n    position: sticky;\n    top: 0px;\n    z-index: 1;\n    border-radius: 5px;\n    padding: 5px;\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n\n}\n\n.queue-header h3 {\n    margin: 0;\n    font-size: 1.2em;\n    color: var(--text-color);\n}\n\n\n.queue-list {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n    flex: 1;\n    overflow-y: auto;\n    height: 350px;\n    scroll-behavior: smooth;\n}\n\n.queue-item {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    border-bottom: 1px solid #ddd;\n}\n\n.queue-song-info {\n    display: flex;\n    flex-direction: column;\n}\n\n.queue-song-title {\n    font-weight: bold;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    width: 250px;\n}\n\n.queue-artist {\n    font-size: 0.9em;\n    color: #666;\n}\n\n.queue-actions {\n    display: flex;\n    align-items: center;\n}\n\n.queue-play-btn {\n    background: none;\n    border: none;\n    font-size: 16px;\n    color: var(--primary-color);\n    cursor: pointer;\n}\n\n.close-store {\n    margin-left: 8px;\n    cursor: pointer;\n    font-size: 14px;\n}\n\n.queue-item.playing .queue-song-title {\n    color: var(--primary-color);\n    font-weight: bold;\n}\n\n.queue-item.playing .queue-artist {\n    color: var(--primary-color);\n}\n</style>"
  },
  {
    "path": "src/components/StatusBarLyrics.vue",
    "content": "<template>\n    <!-- 离屏 Canvas 用于生成顶部状态栏StatusBar图片 (逻辑宽 200pt * 2 = 400px, 高 22pt * 2 = 44px) -->\n    <canvas ref=\"canvasRef\" width=\"400\" height=\"44\" style=\"display: none;\"></canvas>\n</template>\n\n<script setup>\nimport { ref } from 'vue';\n\n// 配置常量\nconst CONFIG = {\n    SCROLL_SPEED: 8,         // 滚动速度 8px/frame (约240px/s)\n    FRAME_INTERVAL: 33,      // 30 FPS\n    LOGO_WIDTH: 50,          // Logo 区域宽度\n    FONT_SIZE: 26,           // 字体大小\n    PAUSE_AT_START: 1200,    // 滚动前暂停时间 (ms)\n    MAX_RENDER_FAILS: 3,     // 最大重试次数\n};\n\n// 状态管理 (改为闭包内的局部变量，如果需要多实例才用工厂模式，这里单例即可)\nconst scrollState = {\n    animationTimer: null,\n    fullText: '',\n    textWidth: 0,\n    scrollOffset: 0,\n    isScrolling: false,\n    renderFailCount: 0,\n    hasPlayedLyrics: false,\n};\n\nconst canvasRef = ref(null);\nconst logoImage = ref(null);\n\n// 清除状态\nconst clearScrollState = () => {\n    if (scrollState.animationTimer) {\n        clearTimeout(scrollState.animationTimer);\n        scrollState.animationTimer = null;\n    }\n    scrollState.fullText = '';\n    scrollState.textWidth = 0;\n    scrollState.scrollOffset = 0;\n    scrollState.isScrolling = false;\n    scrollState.renderFailCount = 0;\n};\n\n// 渲染一帧\nconst renderFrameWithOffset = (text, offsetX = 0, isMarquee = false) => {\n    const canvas = canvasRef.value;\n    if (!canvas) return;\n\n    const ctx = canvas.getContext('2d');\n    if (!ctx) return;\n\n    const width = canvas.width;\n    const height = canvas.height;\n\n    // 清空\n    ctx.clearRect(0, 0, width, height);\n\n    // 宽度锚点\n    ctx.fillStyle = 'rgba(255, 255, 255, 0.05)';\n    ctx.fillRect(0, 0, 1, height);\n    ctx.fillRect(width - 1, 0, 1, height);\n\n    // 布局计算\n    const CONTENT_START_X = CONFIG.LOGO_WIDTH;\n    const CONTENT_WIDTH = width - CONTENT_START_X;\n    const CONTENT_CENTER_X = CONTENT_START_X + (CONTENT_WIDTH / 2);\n\n    // 字体\n    const fontFamily = '-apple-system, BlinkMacSystemFont, \"SF Pro Text\", \"Helvetica Neue\", Arial';\n    ctx.font = `600 ${CONFIG.FONT_SIZE}px ${fontFamily}`;\n    ctx.fillStyle = '#FFFFFF';\n    ctx.textBaseline = 'middle';\n\n    // 1. 绘制 Logo\n    if (logoImage.value && logoImage.value.complete && logoImage.value.naturalHeight !== 0) {\n        try {\n            const targetHeight = 32;\n            const scale = targetHeight / logoImage.value.naturalHeight;\n            const targetWidth = logoImage.value.naturalWidth * scale;\n            const x = (CONFIG.LOGO_WIDTH - targetWidth) / 2;\n            const y = (height - targetHeight) / 2;\n            ctx.drawImage(logoImage.value, x, y, targetWidth, targetHeight);\n        } catch (e) { /* ignore */ }\n    }\n\n    // 2. 准备文本\n    let textToDraw = text;\n    let isPlaceholder = false;\n\n    if (!text || text.trim().length === 0) {\n        if (scrollState.hasPlayedLyrics) {\n            textToDraw = '♩ ♩ ♩';\n        } else {\n            textToDraw = '♪ MoeKoeMusic - 萌音';\n        }\n        isPlaceholder = true;\n    } else {\n        scrollState.hasPlayedLyrics = true;\n    }\n\n    // 3. 绘制文本 (带裁剪)\n    ctx.save();\n    ctx.beginPath();\n    ctx.rect(CONTENT_START_X, 0, CONTENT_WIDTH, height);\n    ctx.clip();\n\n    if (isPlaceholder) {\n        ctx.textAlign = 'center';\n        ctx.fillText(textToDraw, CONTENT_CENTER_X, height / 2 + 2);\n    } else if (isMarquee) {\n        ctx.textAlign = 'left';\n        const textX = CONTENT_START_X + 10 - offsetX;\n        ctx.fillText(textToDraw, textX, height / 2 + 2);\n    } else {\n        ctx.textAlign = 'center';\n        ctx.fillText(textToDraw, CONTENT_CENTER_X, height / 2 + 2);\n    }\n\n    ctx.restore();\n\n    // 4. 发送给主进程\n    try {\n        const dataUrl = canvas.toDataURL('image/png');\n        if (window.electron && window.electron.ipcRenderer) {\n            window.electron.ipcRenderer.send('update-statusbar-image', dataUrl);\n        }\n        scrollState.renderFailCount = 0;\n    } catch (e) {\n        console.error('[Status Bar] Render failed:', e);\n        scrollState.renderFailCount++;\n        if (scrollState.renderFailCount >= CONFIG.MAX_RENDER_FAILS) {\n            clearScrollState();\n        }\n    }\n};\n\n// 动画循环\nconst startMarqueeAnimation = () => {\n    const canvas = canvasRef.value;\n    const contentWidth = canvas ? (canvas.width - CONFIG.LOGO_WIDTH - 20) : 330;\n    const maxOffset = scrollState.textWidth - contentWidth;\n\n    const animate = () => {\n        if (!scrollState.isScrolling) {\n            scrollState.animationTimer = null;\n            return;\n        }\n\n        scrollState.scrollOffset += CONFIG.SCROLL_SPEED;\n        let shouldStop = false;\n\n        if (scrollState.scrollOffset >= maxOffset) {\n            scrollState.scrollOffset = maxOffset;\n            shouldStop = true;\n        }\n\n        renderFrameWithOffset(scrollState.fullText, scrollState.scrollOffset, true);\n\n        if (shouldStop) {\n            scrollState.animationTimer = null;\n            return;\n        }\n        scrollState.animationTimer = setTimeout(animate, CONFIG.FRAME_INTERVAL);\n    };\n\n    if (scrollState.animationTimer) clearTimeout(scrollState.animationTimer);\n    scrollState.animationTimer = setTimeout(animate, CONFIG.FRAME_INTERVAL);\n};\n\n// 获取文本宽度\nconst getTextWidth = (text) => {\n    const canvas = canvasRef.value;\n    if (!canvas) return 0;\n    const ctx = canvas.getContext('2d');\n    if (!ctx) return 0;\n\n    const fontFamily = '-apple-system, BlinkMacSystemFont, \"SF Pro Text\", \"Helvetica Neue\", Arial';\n    ctx.font = `600 ${CONFIG.FONT_SIZE}px ${fontFamily}`;\n    return ctx.measureText(text).width;\n};\n\n// 公共方法：更新显示\nconst updateStatusBarImage = (text) => {\n    if (!canvasRef.value) return;\n\n    // 去重\n    if (text === scrollState.fullText && scrollState.isScrolling) return;\n\n    clearScrollState();\n\n    if (!text || text.trim().length === 0) {\n        renderFrameWithOffset('', 0, false);\n        return;\n    }\n\n    const textWidth = getTextWidth(text);\n    const contentWidth = canvasRef.value.width - CONFIG.LOGO_WIDTH - 20;\n\n    scrollState.fullText = text;\n    scrollState.textWidth = textWidth;\n\n    if (textWidth <= contentWidth) {\n        // 短歌词\n        scrollState.isScrolling = false;\n        renderFrameWithOffset(text, 0, false);\n    } else {\n        // 长歌词\n        scrollState.isScrolling = true;\n        scrollState.scrollOffset = 0;\n        renderFrameWithOffset(text, 0, true);\n\n        setTimeout(() => {\n            if (scrollState.isScrolling && scrollState.fullText === text) {\n                startMarqueeAnimation();\n            }\n        }, CONFIG.PAUSE_AT_START);\n    }\n};\n\n// 初始化图片\nconst initLogo = (src) => {\n    const img = new Image();\n    img.src = src;\n    img.onload = () => {\n        logoImage.value = img;\n        // 首次加载后渲染一次占位符\n        updateStatusBarImage('');\n    };\n};\n\n// 初始化状态栏歌词\nconst initStatusBar = (logoSrc, settings) => {\n    if (!window.electron?.ipcRenderer) return null;\n    const shouldEnable = settings.statusBarLyrics === 'on';\n    if (!shouldEnable) return null;\n\n    // 初始化 Logo\n    initLogo(logoSrc);\n\n    // 清理旧的监听器\n    try {\n        window.electron.ipcRenderer.removeAllListeners('generate-statusbar-image');\n    } catch (e) { }\n\n    // 创建消息处理器\n    const handler = (_event, text) => {\n        try {\n            updateStatusBarImage(text);\n        } catch (e) { }\n    };\n\n    // 注册监听器\n    window.electron.ipcRenderer.on('generate-statusbar-image', handler);\n\n    // 返回清理函数\n    return () => {\n        if (window.electron?.ipcRenderer) {\n            try {\n                window.electron.ipcRenderer.removeListener('generate-statusbar-image', handler);\n            } catch (e) { }\n        }\n    };\n};\n\ndefineExpose({\n    initStatusBar,\n    cleanupStatusBar: clearScrollState,\n});\n</script>\n\n"
  },
  {
    "path": "src/components/TitleBar.vue",
    "content": "<template>\n  <div class=\"titlebar\">\n    <div class=\"window-controls\" v-if=\"isElectron && !isMac && $route.name !== 'VideoPlayer'\">\n      <button class=\"control-button\" @click=\"minimizeWindow\" id=\"minBtn\"></button>\n      <button class=\"control-button\" @click=\"maximizeWindow\" id=\"maxBtn\"></button>\n      <button class=\"control-button\" @click=\"closeWindow\" id=\"closeBtn\"></button>\n    </div>\n  </div>\n</template>\n\n<script setup>\nconst isElectron = typeof window !== 'undefined' && typeof window.electron !== 'undefined';\nconst isMac = isElectron && window.electron.platform == 'darwin';\nconst closeWindow = () => window.electron.ipcRenderer.send('window-control', 'close');\nconst minimizeWindow = () => window.electron.ipcRenderer.send('window-control', 'minimize');\nconst maximizeWindow = () => window.electron.ipcRenderer.send('window-control', 'maximize');\n</script>\n\n<style scoped>\n.titlebar {\n  -webkit-app-region: drag;\n  height: 32px;\n  padding: 0 12px;\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  z-index: 10;\n  position: fixed;\n  width: 100%;\n}\n\n.window-controls {\n  -webkit-app-region: no-drag;\n  display: flex;\n  gap: 8px;\n  margin-right: 30px;\n  margin-bottom: 8px;\n}\n\n.control-button {\n  width: 12px;\n  height: 12px;\n  border-radius: 50%;\n  border: none;\n  padding: 0;\n  margin: 0;\n  cursor: pointer;\n  transition: all 0.2s ease;\n  background-color: transparent;\n  background-size: 10px;\n  background-repeat: no-repeat;\n  background-position: center;\n}\n\n#closeBtn {\n  background-color: #ff5f57 !important;\n}\n\n#minBtn {\n  background-color: #ffbd2e !important;\n}\n\n#maxBtn {\n  background-color: #28c940 !important;\n}\n\n.control-button:hover {\n  background-color: rgba(255, 255, 255, 0.1);\n}\n\n.control-button:hover#closeBtn {\n  background-image: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 16 16\"><path fill=\"black\" d=\"M5 5l6 6M5 11l6-6\" stroke=\"black\" stroke-width=\"1.5\" stroke-linecap=\"round\"/></svg>');\n}\n\n.control-button:hover#minBtn {\n  background-image: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" fill=\"black\" viewBox=\"0 0 16 16\"><path fill-rule=\"evenodd\" d=\"M2 8a1 1 0 011-1h10a1 1 0 110 2H3a1 1 0 01-1-1z\"/></svg>');\n}\n\n.control-button:hover#maxBtn {\n  background-image: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"12\" height=\"12\" viewBox=\"0 0 16 16\"><path fill=\"none\" stroke=\"black\" stroke-width=\"1.5\" d=\"M3 3 L6 3 L3 6 M3 3 L3 6 M13 3 L10 3 L13 6 M13 3 L13 6 M3 13 L6 13 L3 10 M3 13 L3 10 M13 13 L10 13 L13 10 M13 13 L13 10\"/></svg>');\n}\n\n.control-button:focus {\n  outline: none;\n}\n\n.content {\n  padding: 20px;\n}\n\n.content h1 {\n  margin-bottom: 15px;\n  color: #2f3241;\n}\n\n.content p {\n  color: #444;\n  line-height: 1.6;\n  margin-bottom: 10px;\n}\n\n.titlebar-text {\n  flex-grow: 1;\n  margin-left: 12px;\n  font-size: 13px;\n  color: #333;\n}\n</style>"
  },
  {
    "path": "src/components/player/AudioController.js",
    "content": "import { ref } from 'vue';\n\nexport default function useAudioController({ onSongEnd, updateCurrentTime }) {\n    const audio = new Audio();\n    // 设置 crossOrigin 以支持 Web Audio API 跨域访问\n    audio.crossOrigin = 'anonymous';\n\n    const playing = ref(false);\n    const isMuted = ref(false);\n    const volume = ref(66);\n    const playbackRate = ref(1.0);\n\n    // Web Audio API 用于动态增益\n    const audioContext = ref(null);\n    const sourceNode = ref(null);\n    const gainNode = ref(null);\n    const currentLoudnessGain = ref(1.0); // 当前响度增益系数\n    const loudnessNormalizationEnabled = ref(false); // 响度规格化开关，默认关闭\n    const webAudioInitialized = ref(false); // 标记 Web Audio 是否已初始化\n\n    // 初始化 Web Audio API - 只在启用响度规格化时调用，并且应该在用户交互时调用\n    const initWebAudio = () => {\n        try {\n            // 只有启用时才初始化 Web Audio API\n            if (!loudnessNormalizationEnabled.value) {\n                console.log('[AudioController] 响度规格化未启用，使用原生音频播放');\n                return false;\n            }\n\n            if (!audioContext.value) {\n                audioContext.value = new (window.AudioContext || window.webkitAudioContext)();\n                console.log('[AudioController] Web Audio API 初始化成功');\n                console.log('[AudioController] AudioContext 初始状态:', audioContext.value.state);\n\n                // 立即创建音频图连接\n                try {\n                    sourceNode.value = audioContext.value.createMediaElementSource(audio);\n                    gainNode.value = audioContext.value.createGain();\n                    sourceNode.value.connect(gainNode.value);\n                    gainNode.value.connect(audioContext.value.destination);\n\n                    // 设置初始增益\n                    gainNode.value.gain.setValueAtTime(currentLoudnessGain.value, audioContext.value.currentTime);\n\n                    webAudioInitialized.value = true;\n                    console.log('[AudioController] Web Audio 音频图创建完成');\n                    console.log('[AudioController] 初始增益值:', gainNode.value.gain.value);\n                } catch (sourceError) {\n                    console.error('[AudioController] 创建音频源失败（可能是CORS问题）:', sourceError);\n                    // 清理已创建的资源\n                    if (audioContext.value) {\n                        audioContext.value.close();\n                        audioContext.value = null;\n                    }\n                    webAudioInitialized.value = false;\n                    console.warn('[AudioController] 由于CORS限制，响度规格化已禁用，使用原生播放');\n                    return false;\n                }\n            }\n\n            return true;\n        } catch (error) {\n            console.error('[AudioController] Web Audio API 初始化失败:', error);\n            webAudioInitialized.value = false;\n            return false;\n        }\n    };\n\n    // 应用响度规格化\n    const applyLoudnessNormalization = (loudnessData) => {\n        // 如果 Web Audio 未初始化，不做任何处理\n        if (!webAudioInitialized.value || !loudnessNormalizationEnabled.value) {\n            console.log('[AudioController] Web Audio 未启用，跳过响度规格化');\n            return;\n        }\n\n        console.log('[AudioController] 开始应用响度规格化, loudnessData:', loudnessData);\n\n        if (!loudnessData) {\n            console.log('[AudioController] 歌曲无响度规格化数据，使用默认增益');\n            currentLoudnessGain.value = 1.0;\n\n            // 更新 gainNode\n            if (gainNode.value && audioContext.value) {\n                gainNode.value.gain.setValueAtTime(1.0, audioContext.value.currentTime);\n                console.log('[AudioController] 重置音频增益为 1.0, 当前增益值:', gainNode.value.gain.value);\n            }\n            return;\n        }\n\n        try {\n            const { volume, volumeGain, volumePeak } = loudnessData;\n\n            // 响度规格化算法\n            // volume: LUFS 值 (例如 -11.4 表示音频响度为 -11.4 LUFS)\n            // volumeGain: 建议的增益调整值 (dB)\n            // volumePeak: 峰值 (0-1)\n\n            // 目标响度为 -14 LUFS (Spotify 标准)\n            const targetLoudness = -14.0;\n            const loudnessAdjustment = targetLoudness - volume;\n\n            // 计算增益系数 (dB 转线性)\n            // gain = 10^(dB/20)\n            let gainAdjustment = Math.pow(10, loudnessAdjustment / 20);\n\n            // 应用 volumeGain (如果 API 已经提供了增益建议)\n            if (volumeGain !== 0) {\n                gainAdjustment *= Math.pow(10, volumeGain / 20);\n            }\n\n            // 防止削波: 如果应用增益后峰值会超过 1.0，则限制增益\n            if (volumePeak > 0 && volumePeak * gainAdjustment > 0.95) {\n                gainAdjustment = 0.95 / volumePeak;\n                console.log('[AudioController] 限制增益以防止削波');\n            }\n\n            // 限制增益范围 (0.1 到 3.0，即 -20dB 到 +9.5dB)\n            currentLoudnessGain.value = Math.max(0.1, Math.min(3.0, gainAdjustment));\n\n            console.log('[AudioController] 响度规格化:', {\n                volume: volume + ' LUFS',\n                volumeGain: volumeGain + ' dB',\n                volumePeak,\n                adjustment: loudnessAdjustment.toFixed(2) + ' dB',\n                finalGain: (20 * Math.log10(currentLoudnessGain.value)).toFixed(2) + ' dB',\n                gainMultiplier: currentLoudnessGain.value.toFixed(3)\n            });\n\n            // 应用新的增益\n            if (gainNode.value && audioContext.value) {\n                gainNode.value.gain.setValueAtTime(currentLoudnessGain.value, audioContext.value.currentTime);\n                console.log('[AudioController] 增益已应用, 当前增益值:', gainNode.value.gain.value);\n            }\n        } catch (error) {\n            console.error('[AudioController] 应用响度规格化失败:', error);\n            currentLoudnessGain.value = 1.0;\n            // 发生错误时也要重置增益\n            if (gainNode.value && audioContext.value) {\n                gainNode.value.gain.setValueAtTime(1.0, audioContext.value.currentTime);\n            }\n        }\n    };\n\n    // 确保 AudioContext 处于运行状态（如果未初始化则先初始化，然后恢复）\n    const ensureAudioContextRunning = async () => {\n        // 如果启用了响度规格化但还未初始化 Web Audio，先初始化\n        if (loudnessNormalizationEnabled.value && !webAudioInitialized.value) {\n            console.log('[AudioController] 首次播放，初始化 Web Audio API...');\n            if (!initWebAudio()) {\n                console.warn('[AudioController] Web Audio API 初始化失败，使用原生播放');\n                return;\n            }\n        }\n\n        // 如果已初始化，确保 AudioContext 处于运行状态\n        if (webAudioInitialized.value && audioContext.value) {\n            console.log('[AudioController] 检查 AudioContext 状态:', audioContext.value.state);\n\n            if (audioContext.value.state === 'suspended') {\n                console.log('[AudioController] AudioContext 处于 suspended，尝试恢复...');\n                try {\n                    await audioContext.value.resume();\n                    console.log('[AudioController] AudioContext 已恢复为:', audioContext.value.state);\n                } catch (error) {\n                    console.error('[AudioController] 恢复 AudioContext 失败:', error);\n                }\n            } else {\n                console.log('[AudioController] AudioContext 状态正常:', audioContext.value.state);\n            }\n\n            // 验证音频图连接\n            if (gainNode.value) {\n                console.log('[AudioController] 当前增益节点值:', gainNode.value.gain.value);\n            }\n        }\n    };\n\n    // 切换响度规格化\n    const toggleLoudnessNormalization = (enabled) => {\n        const previousState = loudnessNormalizationEnabled.value;\n        loudnessNormalizationEnabled.value = enabled;\n\n        // 保存到 settings\n        const settings = JSON.parse(localStorage.getItem('settings') || '{}');\n        settings.loudnessNormalization = enabled ? 'on' : 'off';\n        localStorage.setItem('settings', JSON.stringify(settings));\n\n        // 如果 Web Audio 已经初始化，只能调整增益，无法完全禁用\n        if (webAudioInitialized.value) {\n            if (gainNode.value && audioContext.value) {\n                const newGain = enabled ? currentLoudnessGain.value : 1.0;\n                gainNode.value.gain.setValueAtTime(newGain, audioContext.value.currentTime);\n                console.log('[AudioController] 响度规格化', enabled ? '已启用' : '已禁用', ', 增益:', newGain);\n            }\n        } else if (enabled && !previousState) {\n            // 如果之前未启用，现在要启用，需要初始化 Web Audio\n            console.warn('[AudioController] 启用响度规格化需要刷新页面才能生效');\n            // 尝试初始化（但可能已经太晚了，audio 元素可能已经在使用中）\n            // initWebAudio();\n        }\n\n        console.log('[AudioController] 响度规格化开关变更:', enabled ? '开启' : '关闭');\n    };\n\n    // 初始化音频设置\n    const initAudio = () => {\n        const savedVolume = localStorage.getItem('player_volume');\n        if (savedVolume !== null) volume.value = parseFloat(savedVolume);\n        isMuted.value = volume.value === 0;\n        audio.volume = volume.value / 100;\n        audio.muted = isMuted.value;\n\n        // 初始化播放速度\n        const savedSpeed = localStorage.getItem('player_speed');\n        if (savedSpeed !== null) {\n            playbackRate.value = parseFloat(savedSpeed);\n            audio.playbackRate = playbackRate.value;\n        }\n\n        // 检查是否启用响度规格化，但不立即初始化 Web Audio\n        // Web Audio 将在首次播放时初始化，以确保在用户手势上下文中\n        const savedSettings = JSON.parse(localStorage.getItem('settings') || '{}');\n        const savedNormalization = savedSettings.loudnessNormalization || 'off';\n        loudnessNormalizationEnabled.value = savedNormalization === 'on';\n\n        audio.addEventListener('ended', onSongEnd);\n        audio.addEventListener('pause', handleAudioEvent);\n        audio.addEventListener('play', handleAudioEvent);\n        audio.addEventListener('timeupdate', updateCurrentTime);\n\n        console.log('[AudioController] 初始化完成，音量设置为:', audio.volume, 'volume值:', volume.value, '播放速度:', audio.playbackRate);\n        console.log('[AudioController] 响度规格化状态:', loudnessNormalizationEnabled.value ? '已启用（将在首次播放时初始化）' : '未启用');\n    };\n\n    // 处理播放/暂停事件\n    const handleAudioEvent = (event) => {\n        if (event.type === 'play') {\n            playing.value = true;\n        } else if (event.type === 'pause') {\n            playing.value = false;\n        }\n        console.log(`[AudioController] ${event.type}事件: playing=${playing.value}`);\n        if (typeof window !== 'undefined' && typeof window.electron !== 'undefined') {\n            window.electron.ipcRenderer.send('play-pause-action', playing.value, audio.currentTime);\n        }\n    };\n\n    // 切换播放/暂停\n    const togglePlayPause = async () => {\n        console.log(`[AudioController] 切换播放状态: playing=${playing.value}, src=${audio.src}`);\n        if (playing.value) {\n            audio.pause();\n            playing.value = false;\n        } else {\n            try {\n                // 在播放前确保 AudioContext 处于 running 状态（如果已启用）\n                await ensureAudioContextRunning();\n\n                await audio.play();\n                playing.value = true;\n            } catch (error) {\n                console.error('[AudioController] 播放失败:', error);\n                return false;\n            }\n        }\n        return true;\n    };\n\n    // 切换静音\n    const toggleMute = () => {\n        isMuted.value = !isMuted.value;\n        audio.muted = isMuted.value;\n        console.log(`[AudioController] 切换静音: muted=${isMuted.value}`);\n        if (isMuted.value) {\n            volume.value = 0;\n        } else {\n            volume.value = audio.volume * 100;\n        }\n        localStorage.setItem('player_volume', volume.value);\n    };\n\n    // 修改音量\n    const changeVolume = () => {\n        audio.volume = volume.value / 100;\n        localStorage.setItem('player_volume', volume.value);\n        isMuted.value = volume.value === 0;\n        audio.muted = isMuted.value;\n        console.log(`[AudioController] 修改音量: volume=${volume.value}, audio.volume=${audio.volume}, muted=${isMuted.value}`);\n    };\n\n    // 设置进度\n    const setCurrentTime = (time) => {\n        audio.currentTime = time;\n        console.log(`[AudioController] 设置进度: time=${time}`);\n    };\n\n    // 设置播放速度\n    const setPlaybackRate = (speed) => {\n        playbackRate.value = speed;\n        audio.playbackRate = speed;\n        localStorage.setItem('player_speed', speed);\n        console.log('[AudioController] 设置播放速度:', speed);\n    };\n\n    // 销毁时清理\n    const destroy = () => {\n        console.log('[AudioController] 销毁音频控制器');\n        audio.pause();\n        audio.load();\n        audio.removeEventListener('play', handleAudioEvent);\n        audio.removeEventListener('ended', onSongEnd);\n        audio.removeEventListener('pause', handleAudioEvent);\n        audio.removeEventListener('timeupdate', updateCurrentTime);\n\n        // 清理 Web Audio 资源\n        if (webAudioInitialized.value) {\n            if (sourceNode.value) {\n                sourceNode.value.disconnect();\n            }\n            if (gainNode.value) {\n                gainNode.value.disconnect();\n            }\n            if (audioContext.value) {\n                audioContext.value.close();\n            }\n        }\n    };\n\n    return {\n        audio,\n        playing,\n        isMuted,\n        volume,\n        playbackRate,\n        initAudio,\n        togglePlayPause,\n        toggleMute,\n        changeVolume,\n        setCurrentTime,\n        setPlaybackRate,\n        destroy,\n        // 响度规格化相关\n        applyLoudnessNormalization,\n        ensureAudioContextRunning,\n        toggleLoudnessNormalization,\n        loudnessNormalizationEnabled,\n        currentLoudnessGain,\n        webAudioInitialized\n    };\n} \n"
  },
  {
    "path": "src/components/player/Helpers.js",
    "content": "import { ref } from 'vue';\nimport { get } from '../../utils/request';\nimport { MoeAuthStore } from '../../stores/store';\n\n\nexport function useHelpers(t) {\n  const isInputFocused = ref(false);\n  \n  // 环境检测\n  const isElectron = () => {\n    return typeof window !== 'undefined' && typeof window.electron !== 'undefined';\n  };\n  \n  // 音量滚轮处理\n  const handleVolumeScroll = (event, volume, changeVolume) => {\n    event.preventDefault();\n    const delta = Math.sign(event.deltaY) * -1;\n    volume.value = Math.min(Math.max(volume.value + delta * 10, 0), 100);\n    changeVolume();\n  };\n  \n  // 检查输入框焦点\n  const checkFocus = () => {\n    isInputFocused.value = ['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName);\n  };\n  \n  // 键盘快捷键处理\n  const handleKeyDown = (event, handlers, isInputFocused) => {\n    if(isInputFocused) return;\n    \n    switch (event.code) {\n      case 'Space':\n        event.preventDefault();\n        handlers.togglePlayPause();\n        break;\n      case 'ArrowLeft':\n        handlers.playSongFromQueue('previous');\n        break;\n      case 'ArrowRight':\n        handlers.playSongFromQueue('next');\n        break;\n      case 'Escape':\n        if(handlers.showLyrics){\n          handlers.toggleLyrics();\n        }\n        break;\n    }\n  };\n  \n  // 桌面歌词控制\n  const desktopLyrics = () => {\n    if (!isElectron()) return;\n    \n    let savedConfig = JSON.parse(localStorage.getItem('settings')) || {};\n    if(!savedConfig?.desktopLyrics) savedConfig.desktopLyrics = 'off';\n    let action = savedConfig?.desktopLyrics === 'off' ? 'display-lyrics' : 'close-lyrics';\n    window.electron.ipcRenderer.send('desktop-lyrics-action', action);\n    savedConfig.desktopLyrics = action === 'display-lyrics' ? 'on' : 'off';\n    localStorage.setItem('settings', JSON.stringify(savedConfig));\n  };\n  \n  // 节流函数\n  const throttle = (func, delay) => {\n    let lastTime = 0;\n    return function (...args) {\n      const now = Date.now();\n      if (now - lastTime >= delay) {\n        lastTime = now;\n        func.apply(this, args);\n      }\n    };\n  };\n  \n  // 获取VIP\n  const getVip = async () => {\n    if (typeof MoeAuthStore !== 'function') return;\n\n    const MoeAuth = MoeAuthStore();\n    if (!MoeAuth.isAuthenticated) return;\n\n    const todayKey = new Date().toISOString().split('T')[0];\n    const lastVipDate = localStorage.getItem('lastVipRequestDate');\n\n    if (lastVipDate === todayKey) {\n      return;\n    }\n\n    try {\n      await get('/youth/day/vip',{\n        receive_day: todayKey\n      });\n      await new Promise(resolve => setTimeout(resolve, 500));\n      await get('/youth/day/vip/upgrade');\n    } catch (error) {\n      console.error('领取VIP失败:', error);\n    }\n    localStorage.setItem('lastVipRequestDate', todayKey);\n  };\n  \n  return {\n    isInputFocused,\n    isElectron,\n    handleVolumeScroll,\n    checkFocus,\n    handleKeyDown,\n    desktopLyrics,\n    throttle,\n    getVip\n  };\n} "
  },
  {
    "path": "src/components/player/LyricsHandler.js",
    "content": "import { ref, nextTick } from 'vue';\nimport { get } from '../../utils/request';\n\nexport default function useLyricsHandler(t) {\n    const lyricsData = ref([]);\n    const originalLyrics = ref('');\n    const showLyrics = ref(false);\n    const scrollAmount = ref(null);\n    const SongTips = ref(t('zan-wu-ge-ci'));\n    const lyricsMode = ref('translation'); // 'translation' 翻译模式 或 'romanization' 音译模式\n    let currentLineIndex = 0;\n\n    // 显示/隐藏歌词\n    const toggleLyrics = (hash, currentTime) => {\n        showLyrics.value = !showLyrics.value;\n        SongTips.value = t('huo-qu-ge-ci-zhong');\n        // 如果显示歌词，滚动到当前播放行\n        if (!lyricsData.value.length && hash) getLyrics(hash);\n        else if (showLyrics.value) {\n            nextTick(() => {\n                // 从全局 audio 对象获取当前播放时间\n                const currentLineIndex = getCurrentLineIndex(currentTime);\n                if (currentLineIndex !== -1) scrollToCurrentLine(currentLineIndex);\n                else centerFirstLine();\n            });\n        }\n        \n        return showLyrics.value;\n    };\n\n    // 获取歌词\n    const getLyrics = async (hash) => {\n        try {\n            const settings = JSON.parse(localStorage.getItem('settings') || '{}');\n            if (!showLyrics.value &&\n                (settings?.desktopLyrics !== 'on' && settings?.apiMode !== 'on' && settings?.statusBarLyrics !== 'on' && settings?.touchBar !== 'on')) {\n                return;\n            }\n\n            console.log('[LyricsHandler] 请求歌词……');\n            const lyricSearchResponse = await get(`/search/lyric?hash=${hash}`);\n            if (lyricSearchResponse.status !== 200 || lyricSearchResponse.candidates.length === 0) {\n                SongTips.value = t('zan-wu-ge-ci');\n                return false;\n            }\n\n            // 明确指定使用KRC格式\n            const lyricResponse = await get(`/lyric?id=${lyricSearchResponse.candidates[0].id}&accesskey=${lyricSearchResponse.candidates[0].accesskey}&fmt=krc&decode=true`);\n            if (lyricResponse.status !== 200) {\n                SongTips.value = t('huo-qu-ge-ci-shi-bai');\n                return false;\n            }\n            parseLyrics(lyricResponse.decodeContent, settings?.lyricsTranslation === 'on');\n            originalLyrics.value = lyricResponse.decodeContent;\n            centerFirstLine();\n            return true;\n        } catch (error) {\n            SongTips.value = t('huo-qu-ge-ci-shi-bai');\n        }\n    };\n\n    // 解析歌词\n    const parseLyrics = (text, parseTranslation = true) => {\n        let translationLyrics = [];\n        let romanizationLyrics = [];\n        const lines = text.split('\\n');\n        try {\n            const languageLine = lines.find(line => line.match(/\\[language:(.*)\\]/));\n            if (parseTranslation && languageLine) {\n                const languageCode = languageLine.slice(10, -2);\n                if (languageCode) {\n                    try {\n                        // 确保 languageCode 是有效的 Base64 编码\n                        // 替换可能导致 Base64 解码失败的字符\n                        const cleanedCode = languageCode.replace(/[^A-Za-z0-9+/=]/g, '');\n                        // 添加缺失的填充字符\n                        const paddedCode = cleanedCode.padEnd(cleanedCode.length + (4 - cleanedCode.length % 4) % 4, '=');\n                        const decodedData = decodeURIComponent(escape(atob(paddedCode)));\n                        const languageData = JSON.parse(decodedData);\n\n                        // 获取翻译歌词 (type === 1)\n                        const translation = languageData?.content?.find(section => section.type === 1);\n                        if (translation?.lyricContent) {\n                            translationLyrics = translation.lyricContent;\n                        }\n                        \n                        // 获取音译歌词 (type === 0)\n                        const romanization = languageData?.content?.find(section => section.type === 0);\n                        if (romanization?.lyricContent) {\n                            romanizationLyrics = romanization.lyricContent;\n                        }\n                    } catch (decodeError) {\n                        console.warn('[LyricsHandler] Base64 解码失败:', decodeError);\n                    }\n                }\n            }\n        } catch (error) {\n            console.warn('[LyricsHandler] 解析翻译歌词失败！');\n        }\n\n        const parsedLyrics = [];\n        const charRegex = /<(\\d+),(\\d+),\\d+>([^<]+)/g;\n\n        lines.forEach(line => {\n            // 匹配主时间标签 [start,duration]\n            const lineMatch = line.match(/^\\[(\\d+),(\\d+)\\](.*)/);\n            if (!lineMatch) return;\n\n            const start = parseInt(lineMatch[1]);\n            const lyricContent = lineMatch[3];\n            const characters = [];\n            \n            // 解析字符级时间标签 <start,duration,unknown>text\n            let charMatch;\n\n            while ((charMatch = charRegex.exec(lyricContent)) !== null) {\n                const text = charMatch[3];\n                const charDuration = parseInt(charMatch[2]);\n                const charStart = start + parseInt(charMatch[1]);\n                \n                // 直接使用文本组，不拆分\n                characters.push({\n                    char: text,\n                    startTime: charStart,\n                    endTime: charStart + charDuration,\n                    highlighted: false\n                });\n            }\n\n            // 如果没有找到字符级时间标签，使用行级时间标签进行等分\n            if (characters.length === 0) {\n                const duration = parseInt(lineMatch[2]);\n                const lyric = lyricContent.replace(/<.*?>/g, '');\n                if (lyric.trim()) {\n                    for (let index = 0; index < lyric.length; index++) {\n                        characters.push({\n                            char: lyric[index],\n                            startTime: start + (index * duration) / lyric.length,\n                            endTime: start + ((index + 1) * duration) / lyric.length,\n                            highlighted: false\n                        });\n                    }\n                }\n            }\n\n            // 保存有效歌词行\n            if (characters.length > 0) {\n                parsedLyrics.push({ characters });\n            }\n        });\n\n        // 添加翻译歌词\n        if (translationLyrics.length) {\n            parsedLyrics.forEach((line, index) => {\n                if (translationLyrics[index] && translationLyrics[index][0]) {\n                    line.translated = translationLyrics[index][0];\n                }\n            });\n        }\n\n        // 添加音译歌词\n        if (romanizationLyrics.length) {\n            parsedLyrics.forEach((line, index) => {\n                if (romanizationLyrics[index]) {\n                    // 将音译歌词数组合并为一个字符串\n                    line.romanized = romanizationLyrics[index].join('');\n                }\n            });\n        }\n\n        lyricsData.value = parsedLyrics;\n    };\n\n    // 切换歌词显示模式（翻译/音译）\n    const toggleLyricsMode = () => {\n        lyricsMode.value = lyricsMode.value === 'translation' ? 'romanization' : 'translation';\n        return lyricsMode.value;\n    };\n\n    // 居中显示第一行歌词\n    const centerFirstLine = () => {\n        const lyricsContainer = document.getElementById('lyrics-container');\n        if (!lyricsContainer) return;\n        const containerHeight = lyricsContainer.offsetHeight;\n        const lyricsElement = document.getElementById('lyrics');\n        if (!lyricsElement) return;\n        const lyricsHeight = lyricsElement.offsetHeight;\n        scrollAmount.value = (containerHeight - lyricsHeight) / 2;\n    };\n\n    // 滚动到当前歌词行\n    const scrollToCurrentLine = (lineIndex) => {\n        if (currentLineIndex === lineIndex) return;\n        \n        currentLineIndex = lineIndex;\n        const lyricsContainer = document.getElementById('lyrics-container');\n        if (!lyricsContainer) return false;\n        const containerHeight = lyricsContainer.offsetHeight;\n        const lineElement = document.querySelectorAll('.line-group')[lineIndex];\n        if (lineElement) {\n            const lineHeight = lineElement.offsetHeight;\n            scrollAmount.value = -lineElement.offsetTop + (containerHeight / 2) - (lineHeight / 2);\n        }\n    };\n\n    // 高亮当前字符\n    const highlightCurrentChar = (currentTime, scroll = true) => {\n        const currentTimeMs = currentTime * 1000;\n        let currentActiveLineIndex = -1;\n        \n        lyricsData.value.forEach((lineData, lineIndex) => {\n            let isLineActive = false;\n            let hasHighlightedChar = false;\n            \n            lineData.characters.forEach((charData) => {\n                // 更精确的时间判断\n                if (currentTimeMs >= charData.startTime && currentTimeMs <= charData.endTime) {\n                    if (!charData.highlighted) {\n                        charData.highlighted = true;\n                        hasHighlightedChar = true;\n                    }\n                    isLineActive = true;\n                } else if (currentTimeMs > charData.endTime) {\n                    // 已经播放过的字符保持高亮\n                    if (!charData.highlighted) {\n                        charData.highlighted = true;\n                    }\n                } else {\n                    // 还未播放的字符取消高亮\n                    charData.highlighted = false;\n                }\n            });\n\n            // 如果当前行有活跃字符，记录为当前行\n            if (isLineActive) {\n                currentActiveLineIndex = lineIndex;\n            }\n\n            // 处理滚动\n            if (scroll && hasHighlightedChar) {\n                scrollToCurrentLine(lineIndex);\n            }\n        });\n        \n        // 如果没有找到活跃行，尝试找到最接近的行\n        if (currentActiveLineIndex === -1 && lyricsData.value.length > 0) {\n            for (let i = 0; i < lyricsData.value.length; i++) {\n                const lineData = lyricsData.value[i];\n                const firstChar = lineData.characters[0];\n                const lastChar = lineData.characters[lineData.characters.length - 1];\n                \n                if (firstChar && lastChar && \n                    currentTimeMs >= firstChar.startTime && \n                    currentTimeMs <= lastChar.endTime) {\n                    currentActiveLineIndex = i;\n                    break;\n                }\n            }\n        }\n    };\n\n    // 重置歌词高亮状态\n    const resetLyricsHighlight = (currentTime) => {\n        if (!lyricsData.value) return;\n\n        const currentTimeMs = currentTime * 1000;\n        let currentActiveLineIndex = -1;\n\n        lyricsData.value.forEach((lineData, lineIndex) => {\n            let isCurrentLine = false;\n            \n            lineData.characters.forEach(charData => {\n                // 更精确的时间判断\n                if (currentTimeMs >= charData.startTime && currentTimeMs <= charData.endTime) {\n                    charData.highlighted = true;\n                    isCurrentLine = true;\n                } else if (currentTimeMs > charData.endTime) {\n                    // 已经播放过的字符保持高亮\n                    charData.highlighted = true;\n                } else {\n                    // 还未播放的字符取消高亮\n                    charData.highlighted = false;\n                }\n            });\n\n            if (isCurrentLine) {\n                currentActiveLineIndex = lineIndex;\n                scrollToCurrentLine(lineIndex);\n            }\n        });\n    };\n\n    // 获取当前播放行索引\n    const getCurrentLineIndex = (currentTime) => {\n        if (!lyricsData.value || lyricsData.value.length === 0) return -1;\n\n        const currentTimeMs = currentTime * 1000;\n        for (let index = 0; index < lyricsData.value.length; index++) {\n            const lineData = lyricsData.value[index];\n            const nextLineData = lyricsData.value[index + 1];\n            const firstChar = lineData.characters[0];\n            const nextFirstChar = nextLineData?.characters[0];\n\n            if (\n                firstChar && nextFirstChar &&\n                currentTimeMs >= firstChar.startTime &&\n                currentTimeMs <= nextFirstChar.startTime\n            ) return index + 1;\n        }\n        return lyricsData.value.length - 1;\n    };\n\n    // 获取当前行歌词文本\n    const getCurrentLineText = (currentTime) => {\n        if (!lyricsData.value || lyricsData.value.length === 0) return \"\";\n\n        for (const lineData of lyricsData.value) {\n            const firstChar = lineData.characters[0];\n            const lastChar = lineData.characters[lineData.characters.length - 1];\n\n            if (\n                firstChar && lastChar &&\n                currentTime * 1000 >= firstChar.startTime &&\n                currentTime * 1000 <= lastChar.endTime\n            ) {\n                return lineData.characters.map((char) => char.char).join(\"\");\n            }\n        }\n        return \"\";\n    };\n\n    return {\n        lyricsData,\n        originalLyrics,\n        showLyrics,\n        scrollAmount,\n        SongTips,\n        lyricsMode,\n        toggleLyrics,\n        getLyrics,\n        highlightCurrentChar,\n        resetLyricsHighlight,\n        getCurrentLineText,\n        scrollToCurrentLine,\n        toggleLyricsMode\n    };\n}"
  },
  {
    "path": "src/components/player/MediaSession.js",
    "content": "export default function useMediaSession() {\n  // 初始化媒体会话\n  const initMediaSession = (handlers) => {\n    if (!(\"mediaSession\" in navigator) || !navigator.mediaSession) return;\n    \n    // 基本播放控制\n    navigator.mediaSession.setActionHandler('play', handlers.togglePlayPause);\n    navigator.mediaSession.setActionHandler('pause', handlers.togglePlayPause);\n    navigator.mediaSession.setActionHandler('previoustrack', handlers.playPrevious);\n    navigator.mediaSession.setActionHandler('nexttrack', handlers.playNext);\n    \n    // SMTC 时间轴控制\n    navigator.mediaSession.setActionHandler('seekbackward', (details) => {\n      if (handlers.seekBackward) {\n        const seekOffset = details.seekOffset || 10; // 默认快退10秒\n        handlers.seekBackward(seekOffset);\n      }\n    });\n    \n    navigator.mediaSession.setActionHandler('seekforward', (details) => {\n      if (handlers.seekForward) {\n        const seekOffset = details.seekOffset || 10; // 默认快进10秒\n        handlers.seekForward(seekOffset);\n      }\n    });\n    \n    navigator.mediaSession.setActionHandler('seekto', (details) => {\n      if (handlers.seekTo && details.seekTime !== undefined) {\n        handlers.seekTo(details.seekTime);\n      }\n    });\n  };\n  \n  // 更新媒体会话信息\n  const changeMediaSession = (song) => {\n    if (!(\"mediaSession\" in navigator) || !navigator.mediaSession) return;\n\n    const defaultArtwork = './assets/images/logo.png';\n    const checkImageAccessibility = (src) => {\n      return new Promise((resolve) => {\n        const img = new Image();\n        img.onload = () => resolve(src);\n        img.onerror = () => resolve(defaultArtwork);\n        img.src = src;\n      });\n    };\n\n    const updateMediaSession = async () => {\n      try {\n        const artworkSrc = await checkImageAccessibility(song.img || defaultArtwork);\n        navigator.mediaSession.metadata = new MediaMetadata({\n          title: song.name,\n          artist: song.author,\n          album: song.album,\n          artwork: [{ src: artworkSrc }]\n        });\n      } catch (error) {\n        console.error(\"Failed to update media session metadata:\", error);\n      }\n    };\n    \n    updateMediaSession();\n  };\n\n  // 更新播放位置状态\n  const updatePositionState = (currentTime, duration, playbackRate = 1.0) => {\n    if (!(\"mediaSession\" in navigator) || !navigator.mediaSession) return;\n    \n    try {\n      if (typeof currentTime === 'number' && typeof duration === 'number' && \n          currentTime >= 0 && duration > 0 && currentTime <= duration) {\n        navigator.mediaSession.setPositionState({\n          duration: duration,\n          playbackRate: playbackRate,\n          position: currentTime\n        });\n      }\n    } catch (error) {\n      console.error(\"Failed to update position state:\", error);\n    }\n  };\n\n  // 清除位置状态\n  const clearPositionState = () => {\n    if (!(\"mediaSession\" in navigator) || !navigator.mediaSession) return;\n    \n    try {\n      navigator.mediaSession.setPositionState(null);\n    } catch (error) {\n      console.error(\"Failed to clear position state:\", error);\n    }\n  };\n  \n  return {\n    initMediaSession,\n    changeMediaSession,\n    updatePositionState,\n    clearPositionState\n  };\n} "
  },
  {
    "path": "src/components/player/PlaybackMode.js",
    "content": "import { ref, computed } from 'vue';\n\nexport default function usePlaybackMode(t, audio) {\n  const playbackModes = ref([\n    { icon: 'fas fa-random', title: t('sui-ji-bo-fang') },\n    { icon: 'fas fa-refresh', title: t('lie-biao-xun-huan') },\n    { icon: '', title: t('dan-qu-xun-huan') }\n  ]);\n  \n  const currentPlaybackModeIndex = ref(1); \n  const currentPlaybackMode = computed(() => playbackModes.value[currentPlaybackModeIndex.value]);\n  const playedSongsStack = ref([]);\n  const currentStackIndex = ref(-1);\n  \n  // 初始化播放模式\n  const initPlaybackMode = () => {\n    const savedMode = localStorage.getItem('player_playback_mode');\n    currentPlaybackModeIndex.value = savedMode !== null ? parseInt(savedMode, 10) : 1;\n    audio.loop = currentPlaybackModeIndex.value === 2;\n    console.log('[PlaybackMode] 初始化播放模式:', currentPlaybackModeIndex.value);\n  };\n  \n  // 切换播放模式\n  const togglePlaybackMode = () => {\n    currentPlaybackModeIndex.value = (currentPlaybackModeIndex.value + 1) % playbackModes.value.length;\n    audio.loop = currentPlaybackModeIndex.value === 2;\n    playedSongsStack.value = [];\n    currentStackIndex.value = -1;\n    localStorage.setItem('player_playback_mode', currentPlaybackModeIndex.value.toString());\n    console.log('[PlaybackMode] 切换播放模式:', currentPlaybackModeIndex.value);\n  };\n  \n  return {\n    playbackModes,\n    currentPlaybackModeIndex,\n    currentPlaybackMode,\n    playedSongsStack,\n    currentStackIndex,\n    initPlaybackMode,\n    togglePlaybackMode\n  };\n} "
  },
  {
    "path": "src/components/player/ProgressBar.js",
    "content": "import { ref } from 'vue';\nimport { get } from '../../utils/request';\n\nexport default function useProgressBar(audio, resetLyricsHighlight) {\n    const progressWidth = ref(0);\n    const isProgressDragging = ref(false);\n    const isDraggingHandle = ref(false);\n    const showTimeTooltip = ref(false);\n    const tooltipPosition = ref(0);\n    const tooltipTime = ref('0:00');\n    const activeProgressBar = ref(null);\n    const climaxPoints = ref([]);\n\n    // 格式化时间\n    const formatTime = (seconds) => {\n        if (seconds > 1000) seconds = seconds / 1000;\n        const minutes = Math.floor(seconds / 60);\n        const secs = Math.floor(seconds % 60);\n        return `${minutes}:${secs.toString().padStart(2, '0')}`;\n    };\n\n    // 获取歌曲高潮点\n    const getMusicHighlights = async (hash) => {\n        try {\n            const response = await get(`/song/climax?hash=${hash}`);\n            if (response.status !== 1) {\n                climaxPoints.value = [];\n                return;\n            }\n            climaxPoints.value = response.data.map(point => ({\n                position: (parseInt(point.start_time) / 1000 / audio.duration) * 100,\n                duration: parseInt(point.timelength) / 1000\n            }));\n        } catch (error) {\n            climaxPoints.value = [];\n        }\n    };\n\n    // 进度条拖动开始\n    const onProgressDragStart = (event) => {\n        event.preventDefault();\n\n        const currentProgressBar = event.target.closest('.progress-bar');\n        if (!currentProgressBar) return;\n\n        // 检查是否点击在小圆点上\n        const handle = event.target.closest('.progress-handle');\n        if (!handle) {\n            if (!audio.duration) return;\n            const rect = currentProgressBar.getBoundingClientRect();\n            const offsetX = Math.max(0, Math.min(event.clientX - rect.left, currentProgressBar.offsetWidth));\n            const percentage = (offsetX / currentProgressBar.offsetWidth) * 100;\n            progressWidth.value = Math.max(0, Math.min(percentage, 100));\n        }\n\n        isProgressDragging.value = true;\n        isDraggingHandle.value = true;\n        activeProgressBar.value = currentProgressBar;\n\n        document.addEventListener('mousemove', onProgressDrag);\n        document.addEventListener('mouseup', onProgressDragEnd);\n    };\n\n    // 进度条拖动中\n    const onProgressDrag = (event) => {\n        event.preventDefault();\n        if (isProgressDragging.value && activeProgressBar.value) {\n            const rect = activeProgressBar.value.getBoundingClientRect();\n            const offsetX = Math.max(0, Math.min(event.clientX - rect.left, activeProgressBar.value.offsetWidth));\n            const percentage = (offsetX / activeProgressBar.value.offsetWidth) * 100;\n            progressWidth.value = Math.max(0, Math.min(percentage, 100));\n\n            // 更新时间提示\n            tooltipPosition.value = offsetX;\n            const time = (percentage / 100) * audio.duration;\n            tooltipTime.value = formatTime(time);\n        }\n    };\n\n    // 进度条拖动结束\n    const onProgressDragEnd = (event) => {\n        if (isProgressDragging.value && activeProgressBar.value) {\n            const rect = activeProgressBar.value.getBoundingClientRect();\n            const offsetX = Math.max(0, Math.min(event.clientX - rect.left, activeProgressBar.value.offsetWidth));\n            const percentage = (offsetX / activeProgressBar.value.offsetWidth) * 100;\n            const newTime = (percentage / 100) * audio.duration;\n\n            audio.currentTime = Math.max(0, Math.min(newTime, audio.duration));\n            resetLyricsHighlight(audio.currentTime);\n        }\n\n        isProgressDragging.value = false;\n        isDraggingHandle.value = false;\n        showTimeTooltip.value = false;\n        activeProgressBar.value = null;\n        document.removeEventListener('mousemove', onProgressDrag);\n        document.removeEventListener('mouseup', onProgressDragEnd);\n    };\n\n    // 点击进度条更新进度\n    const updateProgressFromEvent = (event) => {\n        if (isProgressDragging.value) return; // 如果正在拖动则不处理点击\n\n        const progressBar = event.target.closest('.progress-bar');\n        if (!progressBar || !audio.duration) return;\n\n        const rect = progressBar.getBoundingClientRect();\n        const offsetX = Math.max(0, Math.min(event.clientX - rect.left, progressBar.offsetWidth));\n        const percentage = (offsetX / progressBar.offsetWidth) * 100;\n        const newTime = (percentage / 100) * audio.duration;\n\n        audio.currentTime = Math.max(0, Math.min(newTime, audio.duration));\n        progressWidth.value = percentage;\n        resetLyricsHighlight(audio.currentTime);\n    };\n\n    // 更新时间提示\n    const updateTimeTooltip = (event) => {\n        const progressBar = event.target.closest('.progress-bar');\n        if (!progressBar || !audio.duration) return;\n\n        const rect = progressBar.getBoundingClientRect();\n        const offsetX = Math.max(0, Math.min(event.clientX - rect.left, progressBar.offsetWidth));\n\n        const tooltipWidth = 50;\n        if (offsetX < tooltipWidth / 2) {\n            tooltipPosition.value = tooltipWidth / 2;\n        } else if (offsetX > progressBar.offsetWidth - tooltipWidth / 2) {\n            tooltipPosition.value = progressBar.offsetWidth - tooltipWidth / 2;\n        } else {\n            tooltipPosition.value = offsetX;\n        }\n\n        const percentage = (offsetX / progressBar.offsetWidth);\n        const time = percentage * audio.duration;\n        tooltipTime.value = formatTime(time);\n\n        showTimeTooltip.value = true;\n    };\n\n    // 隐藏时间提示\n    const hideTimeTooltip = () => {\n        if (!isProgressDragging.value) {\n            showTimeTooltip.value = false;\n        }\n    };\n\n    return {\n        progressWidth,\n        isProgressDragging,\n        showTimeTooltip,\n        tooltipPosition,\n        tooltipTime,\n        climaxPoints,\n        formatTime,\n        getMusicHighlights,\n        onProgressDragStart,\n        updateProgressFromEvent,\n        updateTimeTooltip,\n        hideTimeTooltip\n    };\n} "
  },
  {
    "path": "src/components/player/SongQueue.js",
    "content": "import { ref } from 'vue';\nimport { get } from '../../utils/request';\nimport { MoeAuthStore } from '../../stores/store';\n\nconst QUALITY_LEVELS = ['128', '320', 'flac', 'high', 'viper_atmos', 'viper_clear', 'viper_tape'];\nconst QUALITY_LABELS = {\n    '128': '标准',\n    '320': '高品',\n    flac: 'FLAC',\n    high: 'Hi-Res',\n    viper_atmos: '全景声',\n    viper_clear: '超清',\n    viper_tape: '母带'\n};\n\nconst normalizeQuality = (quality) => {\n    return QUALITY_LEVELS.includes(quality) ? quality : '128';\n};\n\nconst getFallbackChain = (quality) => QUALITY_LEVELS.slice(0, QUALITY_LEVELS.indexOf(normalizeQuality(quality)) + 1).reverse();\n\nconst getQualityLabel = (quality) => QUALITY_LABELS[quality] || '';\n\nconst getPrivilegeVariants = (response) => {\n    const variants = [];\n\n    for (const item of response?.data || []) {\n        for (const variant of [item, ...(item?.relate_goods || [])]) {\n            if (!variant?.hash || variant?.level === 0 || !QUALITY_LEVELS.includes(variant?.quality)) continue;\n            variants.push(variant);\n        }\n    }\n\n    return variants;\n};\n\nconst getQualityOptions = (response) => {\n    const qualityOptions = new Map();\n\n    for (const variant of getPrivilegeVariants(response)) {\n        if (qualityOptions.has(variant.quality)) continue;\n        qualityOptions.set(variant.quality, {\n            value: variant.quality,\n            hash: variant.hash,\n            label: getQualityLabel(variant.quality)\n        });\n    }\n\n    return [...qualityOptions.values()].sort((a, b) => QUALITY_LEVELS.indexOf(b.value) - QUALITY_LEVELS.indexOf(a.value));\n};\n\nconst getPrivilegeCandidates = (qualityOptions, quality, originalHash) => {\n    const candidatesByQuality = new Map();\n\n    for (const option of qualityOptions) {\n        if (!candidatesByQuality.has(option.value)) {\n            candidatesByQuality.set(option.value, {\n                hash: option.hash,\n                quality: option.value\n            });\n        }\n    }\n\n    const fallbackChain = getFallbackChain(quality);\n    const candidates = fallbackChain.map(itemQuality => candidatesByQuality.get(itemQuality)).filter(Boolean);\n\n    return candidates.length > 0 ? candidates : fallbackChain.map(itemQuality => ({\n        hash: originalHash,\n        quality: itemQuality\n    }));\n};\n\nexport default function useSongQueue(t, musicQueueStore, queueList = null) {\n    const currentSong = ref({\n        name: '',\n        author: '',\n        img: '',\n        url: '',\n        hash: '',\n        playHash: '',\n        resolvedQuality: '',\n        qualityLabel: '',\n        qualityOptions: []\n    });\n    const NextSong = ref([]);\n    const timeoutId = ref(null);\n\n    // 添加歌曲到队列并播放\n    const addSongToQueue = async (hash, name, img, author, isReset = true, qualityOverride = '', cachedQualityOptions = []) => {\n        if(!hash) return { error: true };\n        const currentSongHash = currentSong.value.hash;\n        if (typeof window !== 'undefined' && typeof window.electron !== 'undefined') {\n            window.electron.ipcRenderer.send('set-tray-title', name + ' - ' + author);\n        }\n\n        try {\n            clearTimeout(timeoutId.value);\n            currentSong.value.author = author;\n            currentSong.value.name = name;\n            currentSong.value.img = img;\n            currentSong.value.hash = hash;\n            currentSong.value.playHash = hash;\n            currentSong.value.resolvedQuality = '';\n            currentSong.value.qualityLabel = '';\n            currentSong.value.qualityOptions = [];\n\n            console.log('[SongQueue] 获取歌曲:', hash, name);\n\n            const settings = JSON.parse(localStorage.getItem('settings') || '{}');\n            const data = {\n                hash: hash\n            };\n\n            // 根据用户设置确定请求参数\n            const MoeAuth = typeof MoeAuthStore === 'function' ? MoeAuthStore() : { isAuthenticated: false };\n            const isAuth = !!MoeAuth.isAuthenticated;\n\n            let response = null;\n            let selectedCandidate = { hash, quality: '' };\n            let qualityOptions = [];\n\n            if (!isAuth) {\n                data.free_part = 1;\n                response = await get('/song/url', data);\n            } else {\n                const q = normalizeQuality(qualityOverride || settings?.quality);\n                const fallbackCandidates = getFallbackChain(q).map(itemQuality => ({\n                    hash,\n                    quality: itemQuality\n                }));\n                let candidates = fallbackCandidates;\n                qualityOptions = Array.isArray(cachedQualityOptions) ? cachedQualityOptions.map(option => ({ ...option })) : [];\n\n                try {\n                    if (qualityOptions.length === 0) {\n                        const privilegeResponse = await get(`/privilege/lite`, { hash: hash });\n                        qualityOptions = getQualityOptions(privilegeResponse);\n                    }\n                    candidates = getPrivilegeCandidates(qualityOptions, q, hash);\n                } catch (error) {\n                    if (error.response?.data?.error?.includes('验证')) {\n                        throw error;\n                    }\n                    if (error.response?.data?.status == 2) {\n                        throw error;\n                    }\n                    console.error('[SongQueue] 获取歌曲详情失败，回退到原始哈希请求:', error);\n                }\n\n                for (const candidate of candidates) {\n                    try {\n                        const candidateResponse = await get('/song/url', {\n                            hash: candidate.hash,\n                            quality: candidate.quality\n                        });\n\n                        if (candidateResponse.status !== 1) {\n                            response = candidateResponse;\n                            continue;\n                        }\n\n                        if (candidateResponse.extName == 'mp4') {\n                            console.log('[SongQueue] 歌曲格式为MP4，尝试获取下一档音质');\n                            response = candidateResponse;\n                            continue;\n                        }\n\n                        if (!candidateResponse.url || !candidateResponse.url[0]) {\n                            response = candidateResponse;\n                            continue;\n                        }\n\n                        response = candidateResponse;\n                        selectedCandidate = candidate;\n                        break;\n                    } catch (error) {\n                        if (error.response?.data?.error?.includes('验证')) {\n                            throw error;\n                        }\n                        if (error.response?.data?.status == 2) {\n                            throw error;\n                        }\n                        console.error('[SongQueue] 获取候选音质失败:', error);\n                    }\n                }\n            }\n\n            if (!response || response.status !== 1) {\n                console.error('[SongQueue] 获取音乐URL失败:', response);\n                currentSong.value.author = currentSong.value.name = t('huo-qu-yin-le-shi-bai');\n                if (response?.status == 3) {\n                    currentSong.value.name = t('gai-ge-qu-zan-wu-ban-quan');\n                }\n                if (musicQueueStore.queue.length === 0) return { error: true };\n                currentSong.value.author = t('3-miao-hou-zi-dong-qie-huan-xia-yi-shou');\n\n                // 返回需要切换到下一首的标志，而不是直接调用playSongFromQueue\n                return { error: true, shouldPlayNext: true };\n            }\n\n            // 设置URL\n            if (response.url && response.url[0]) {\n                currentSong.value.url = response.url[0];\n                currentSong.value.playHash = selectedCandidate.hash || hash;\n                currentSong.value.resolvedQuality = selectedCandidate.quality || '';\n                currentSong.value.qualityLabel = getQualityLabel(selectedCandidate.quality);\n                currentSong.value.qualityOptions = qualityOptions.map(option => ({ ...option }));\n                console.log('[SongQueue] 获取到音乐URL:', currentSong.value.url);\n            } else {\n                console.error('[SongQueue] 未获取到音乐URL');\n                currentSong.value.author = currentSong.value.name = t('huo-qu-yin-le-shi-bai');\n                return { error: true };\n            }\n\n            // 创建歌曲对象\n            const song = {\n                id: musicQueueStore.queue.length + 1,\n                hash: hash,\n                playHash: selectedCandidate.hash || hash,\n                resolvedQuality: selectedCandidate.quality || '',\n                qualityLabel: getQualityLabel(selectedCandidate.quality),\n                qualityOptions: qualityOptions.map(option => ({ ...option })),\n                name: name,\n                img: img,\n                author: author,\n                timeLength: response.timeLength,\n                url: response.url[0],\n                // 响度规格化参数\n                loudnessNormalization: {\n                    volume: response.volume || 0,\n                    volumeGain: response.volume_gain || 0,\n                    volumePeak: response.volume_peak || 1\n                }\n            };\n\n            // 根据是否需要重置播放位置\n            if (isReset) {\n                localStorage.setItem('player_progress', 0);\n            }\n\n            // 更新队列\n            const existingSongIndex = musicQueueStore.queue.findIndex(song => song.hash === hash);\n            if (existingSongIndex === -1) {\n                const currentIndex = musicQueueStore.queue.findIndex(song => song.hash == currentSongHash);\n                if (currentIndex !== -1) {\n                    musicQueueStore.queue.splice(currentIndex + 1, 0, song);\n                } else {\n                    musicQueueStore.addSong(song);\n                }\n            } else {\n                // 如果歌曲已存在，只更新当前歌曲的信息，不修改队列\n                currentSong.value = song;\n            }\n\n            // 返回歌曲对象\n            return { song };\n        } catch (error) {\n            console.error('[SongQueue] 获取音乐地址出错:', error);\n            currentSong.value.author = currentSong.value.name = t('huo-qu-yin-le-di-zhi-shi-bai');\n            if (error.response?.data?.error?.includes('验证')) {\n                window.$modal.alert('账户风控,请稍候重试!');\n                return { error: true};\n            }\n            if (error.response?.data?.status == 2) {\n                window.$modal.alert(t('deng-lu-shi-xiao-qing-zhong-xin-deng-lu'));\n                return { error: true};\n            }\n            if (musicQueueStore.queue.length === 0) return { error: true };\n            currentSong.value.author = t('3-miao-hou-zi-dong-qie-huan-xia-yi-shou');\n\n            // 返回需要切换到下一首的标志，而不是直接调用playSongFromQueue\n            return { error: true, shouldPlayNext: true };\n        }\n    };\n\n    // 添加云盘歌曲到播放列表\n    const addCloudMusicToQueue = async (hash, name, author, timeLength, cover, isReset = true) => {\n        const currentSongHash = currentSong.value.hash;\n        if (typeof window !== 'undefined' && typeof window.electron !== 'undefined') {\n            window.electron.ipcRenderer.send('set-tray-title', name + ' - ' + author);\n        }\n\n        try {\n            clearTimeout(timeoutId.value);\n            currentSong.value.author = author;\n            currentSong.value.name = name;\n            currentSong.value.hash = hash;\n            currentSong.value.img = cover;\n            currentSong.value.qualityLabel = '';\n            currentSong.value.qualityOptions = [];\n\n            console.log('[SongQueue] 获取云盘歌曲:', hash, name);\n\n            const response = await get('/user/cloud/url', { hash });\n            if (response.status !== 1) {\n                console.error('[SongQueue] 获取云盘音乐URL失败:', response);\n                currentSong.value.author = currentSong.value.name = t('huo-qu-yin-le-shi-bai');\n                if (musicQueueStore.queue.length === 0) return { error: true };\n                currentSong.value.author = t('3-miao-hou-zi-dong-qie-huan-xia-yi-shou');\n\n                // 返回需要切换到下一首的标志，而不是直接调用playSongFromQueue\n                return { error: true, shouldPlayNext: true };\n            }\n\n            // 设置URL\n            if (response.data && response.data.url) {\n                currentSong.value.url = response.data.url;\n                console.log('[SongQueue] 获取到云盘音乐URL:', currentSong.value.url);\n            } else {\n                console.error('[SongQueue] 未获取到云盘音乐URL');\n                currentSong.value.author = currentSong.value.name = t('huo-qu-yin-le-shi-bai');\n                return { error: true };\n            }\n\n            // 创建歌曲对象\n            const song = {\n                id: musicQueueStore.queue.length + 1,\n                hash: hash,\n                name: name,\n                author: author,\n                img: cover,\n                timeLength: timeLength || 0,\n                url: response.data.url,\n                isCloud: true\n            };\n\n            // 根据是否需要重置播放位置\n            if (isReset) {\n                localStorage.setItem('player_progress', 0);\n            }\n\n            // 更新队列\n            const existingSongIndex = musicQueueStore.queue.findIndex(song => song.hash === hash);\n            if (existingSongIndex === -1) {\n                const currentIndex = musicQueueStore.queue.findIndex(song => song.hash == currentSongHash);\n                if (currentIndex !== -1) {\n                    musicQueueStore.queue.splice(currentIndex + 1, 0, song);\n                } else {\n                    musicQueueStore.addSong(song);\n                }\n            } else {\n                // 如果歌曲已存在，只更新当前歌曲的信息，不修改队列\n                currentSong.value = song;\n            }\n\n            // 返回歌曲对象\n            return { song };\n        } catch (error) {\n            console.error('[SongQueue] 获取云盘音乐地址出错:', error);\n            currentSong.value.author = currentSong.value.name = t('huo-qu-yin-le-di-zhi-shi-bai');\n            if (musicQueueStore.queue.length === 0) return { error: true };\n            currentSong.value.author = t('3-miao-hou-zi-dong-qie-huan-xia-yi-shou');\n\n            // 返回需要切换到下一首的标志，而不是直接调用playSongFromQueue\n            return { error: true, shouldPlayNext: true };\n        }\n    };\n\n    // 添加下一首\n    const addToNext = (hash, name, img, author, timeLength) => {\n        const existingSongIndex = musicQueueStore.queue.findIndex(song => song.hash === hash);\n        if (existingSongIndex !== -1 && typeof queueList?.value?.removeSongFromQueue === 'function') {\n            queueList.value.removeSongFromQueue(existingSongIndex);\n        }\n\n        const currentIndex = musicQueueStore.queue.findIndex(song => song.hash === currentSong.value.hash);\n        musicQueueStore.queue.splice(currentIndex !== -1 ? currentIndex + 1 : musicQueueStore.queue.length, 0, {\n            id: musicQueueStore.queue.length + 1,\n            hash: hash,\n            name: name,\n            img: img,\n            author: author,\n            timeLength: timeLength,\n        });\n\n        NextSong.value.push({\n            id: musicQueueStore.queue.length + 1,\n            hash: hash,\n            name: name,\n            img: img,\n            author: author,\n            timeLength: timeLength,\n        });\n    };\n\n    // 获取歌单全部歌曲\n    const getPlaylistAllSongs = async (id) => {\n        try {\n            let allSongs = [];\n            for (let page = 1; page <= 4; page++) {\n                const url = `/playlist/track/all?id=${id}&pagesize=300&page=${page}`;\n                const response = await get(url);\n                if (response.status !== 1) {\n                    window.$modal.alert(t('huo-qu-ge-dan-shi-bai'));\n                    return;\n                }\n                if (Object.keys(response.data.info).length === 0) break;\n                allSongs = allSongs.concat(response.data.info);\n                if (response.data.info.length < 300) break;\n            }\n            return allSongs;\n        } catch (error) {\n            console.error(error);\n            window.$modal.alert(t('huo-qu-ge-dan-shi-bai'));\n            return null;\n        }\n    };\n\n    // 添加歌单到播放列表\n    const addPlaylistToQueue = async (info, append = false) => {\n        let songs = [];\n        if (!append) {\n            musicQueueStore.clearQueue();\n        } else {\n            songs = [...musicQueueStore.queue];\n        }\n\n        const newSongs = info.map((song, index) => {\n            return {\n                id: songs.length + index + 1,\n                hash: song.hash,\n                name: song.name,\n                img: song.cover?.replace(\"{size}\", 480) || './assets/images/ico.png',\n                author: song.author,\n                timeLength: song.timelen\n            };\n        });\n\n        if (append) {\n            songs = [...songs, ...newSongs];\n        } else {\n            songs = newSongs;\n        }\n\n        musicQueueStore.queue = songs;\n        return songs;\n    };\n\n    // 批量添加云盘歌曲到播放列表\n    const addCloudPlaylistToQueue = async (songs, append = false) => {\n        let queueSongs = [];\n        if (!append) {\n            musicQueueStore.clearQueue();\n        } else {\n            queueSongs = [...musicQueueStore.queue];\n        }\n\n        const newSongs = songs.map((song, index) => {\n            return {\n                id: queueSongs.length + index + 1,\n                hash: song.hash,\n                name: song.name,\n                author: song.author,\n                timeLength: song.timelen || 0,\n                url: song.url,\n                isCloud: true\n            };\n        });\n\n        if (append) {\n            queueSongs = [...queueSongs, ...newSongs];\n        } else {\n            queueSongs = newSongs;\n        }\n\n        musicQueueStore.queue = queueSongs;\n        return queueSongs;\n    };\n\n    // 添加本地音乐到队列并播放\n    const addLocalMusicToQueue = async (localItem, isReset = true) => {\n\n        const currentSongHash = currentSong.value.hash;\n\n        if (typeof window !== 'undefined' && typeof window.electron !== 'undefined') {\n            window.electron.ipcRenderer.send('set-tray-title', (localItem.displayName || localItem.name) + ' - ' + (localItem.author || '未知艺术家'));\n        }\n\n        try {\n            clearTimeout(timeoutId.value);\n            \n            // 设置当前歌曲信息\n            currentSong.value.author = localItem.author || '未知艺术家';\n            currentSong.value.name = localItem.displayName || localItem.name;\n            currentSong.value.img = localItem.cover || './assets/images/ico.png';\n            currentSong.value.hash = `local_${localItem.name}_${localItem.file.size}_${localItem.file.lastModified}`;\n            currentSong.value.qualityLabel = '';\n            currentSong.value.qualityOptions = [];\n\n            // 创建本地文件的 URL\n            const url = URL.createObjectURL(localItem.file);\n            currentSong.value.url = url;\n            console.log('[SongQueue] 创建本地音乐URL:', url);\n\n            // 创建歌曲对象\n            const song = {\n                // id: musicQueueStore.queue.length + 1,\n                hash: currentSong.value.hash,\n                name: currentSong.value.name,\n                img: currentSong.value.img,\n                author: currentSong.value.author,\n                timeLength: localItem.timelen || (localItem.duration * 1000),\n                url: url,\n                isLocal: true\n            };\n\n            // 根据是否需要重置播放位置\n            // if (isReset) {\n            //     localStorage.setItem('player_progress', 0);\n            // }\n\n            // // 更新队列\n            // const existingSongIndex = musicQueueStore.queue.findIndex(song => song.hash === currentSong.value.hash);\n            // if (existingSongIndex === -1) {\n            //     const currentIndex = musicQueueStore.queue.findIndex(song => song.hash == currentSongHash);\n            //     if (currentIndex !== -1) {\n            //         musicQueueStore.queue.splice(currentIndex + 1, 0, song);\n            //     } else {\n            //         musicQueueStore.addSong(song);\n            //     }\n            // } else {\n            //     // 如果歌曲已存在，只更新当前歌曲的信息，不修改队列\n            //     currentSong.value = song;\n            // }\n\n            // 返回歌曲对象\n            return { song };\n        } catch (error) {\n            console.error('[SongQueue] 获取本地音乐地址出错:', error);\n            currentSong.value.author = currentSong.value.name = t('huo-qu-ben-di-yin-le-di-zhi-shi-bai');\n            // if (musicQueueStore.queue.length === 0) return { error: true };\n            currentSong.value.author = t('3-miao-hou-zi-dong-qie-huan-xia-yi-shou');\n\n            // 返回需要切换到下一首的标志，而不是直接调用playSongFromQueue\n            return { error: true, shouldPlayNext: true };\n        }\n    };\n\n    // 批量添加本地音乐到播放列表\n    const addLocalPlaylistToQueue = async (localSongs, append = false) => {\n        console.log('[SongQueue] 添加本地播放列表:', localSongs.length, '首歌曲');\n        \n        try {\n            // if (!append) {\n            //     musicQueueStore.clearQueue();\n            // }\n            \n            const queueSongs = [];\n            for (const item of localSongs) {\n                const localSong = {\n                    // id: musicQueueStore.queue.length + queueSongs.length + 1,\n                    hash: `local_${item.name}_${item.file.size}_${item.file.lastModified}`,\n                    name: item.displayName || item.name,\n                    author: item.author || '未知艺术家',\n                    img: item.cover || './assets/images/ico.png',\n                    timeLength: item.timelen || (item.duration * 1000),\n                    isLocal: true,\n                    file: item.file\n                };\n                queueSongs.push(localSong);\n            }\n            \n            // 添加到队列\n            // if (append) {\n            //     musicQueueStore.queue = [...musicQueueStore.queue, ...queueSongs];\n            // } else {\n            //     musicQueueStore.queue = queueSongs;\n            // }\n            \n            return queueSongs;\n        } catch (error) {\n            console.error('[SongQueue] 添加本地播放列表失败:', error);\n            return [];\n        }\n    };\n\n    // 获取歌曲详情\n    const privilegeSong = async (hash) => {\n        const response = await get(`/privilege/lite`,{hash:hash});\n        return response;\n    }\n\n    return {\n        currentSong,\n        NextSong,\n        addSongToQueue,\n        addCloudMusicToQueue,\n        addLocalMusicToQueue,\n        addLocalPlaylistToQueue,\n        addToNext,\n        getPlaylistAllSongs,\n        addPlaylistToQueue,\n        addCloudPlaylistToQueue,\n        privilegeSong\n    };\n}\n"
  },
  {
    "path": "src/components/player/index.js",
    "content": "// 导出所有组件模块\nimport useAudioController from './AudioController';\nimport useLyricsHandler from './LyricsHandler';\nimport useProgressBar from './ProgressBar';\nimport usePlaybackMode from './PlaybackMode';\nimport useMediaSession from './MediaSession';\nimport useSongQueue from './SongQueue';\nimport { useHelpers } from './Helpers';\n\nexport {\n  useAudioController,\n  useLyricsHandler,\n  useProgressBar,\n  usePlaybackMode,\n  useMediaSession,\n  useSongQueue,\n  useHelpers\n}; "
  },
  {
    "path": "src/language/en.json",
    "content": "{\n  \"tui-jian\": \"Recommendations\",\n  \"tui-jian-ge-qu\": \"Recommended Tracks\",\n  \"tui-jian-ge-dan\": \"Recommended Playlists\",\n  \"fa-xian\": \"Discover\",\n  \"yu-yan\": \"Language\",\n  \"zhu-se-tiao\": \"Theme Color\",\n  \"wai-guan\": \"Appearance\",\n  \"native-title-bar\": \"Native Window Decorations\",\n  \"sheng-yin\": \"Sound\",\n  \"yin-zhi-xuan-ze\": \"Audio Quality\",\n  \"qi-dong-wen-hou-yu\": \"Startup Greeting\",\n  \"ge-ci\": \"Lyrics\",\n  \"xian-shi-ge-ci-bei-jing\": \"Show Album Art as Lyrics Background\",\n  \"xian-shi-zhuo-mian-ge-ci\": \"Show Desktop Lyrics\",\n  \"ge-ci-zi-ti-da-xiao\": \"Fullscreen Lyrics Font Size\",\n  \"guan-bi\": \"Disable\",\n  \"guan-bi-an-niu\": \"Close\",\n  \"shao-nv-fen\": \"Pink\",\n  \"qian-se\": \"Light\",\n  \"pu-tong-yin-zhi\": \"Standard Quality — 128 Kbps\",\n  \"da-kai\": \"Enable\",\n  \"zhong\": \"Medium\",\n  \"kai-qi\": \"Enable\",\n  \"xuan-ze-yu-yan\": \"Select Language\",\n  \"xuan-ze-zhu-se-tiao\": \"Select Theme Color\",\n  \"nan-nan-lan\": \"Sky Blue\",\n  \"tou-ding-lv\": \"Mint Green\",\n  \"xuan-ze-wai-guan\": \"Select Appearance\",\n  \"zi-dong\": \"Auto\",\n  \"shen-se\": \"Dark\",\n  \"gao-yin-zhi-320kbps\": \"High Quality — 320 Kbps\",\n  \"xiao\": \"Small\",\n  \"da\": \"Large\",\n  \"sou-suo-jie-guo\": \"Search Results\",\n  \"shang-yi-ye\": \"Previous\",\n  \"xia-yi-ye\": \"Next\",\n  \"di\": \"Page\",\n  \"ye\": \"\",\n  \"gong\": \"of\",\n  \"bo-fang\": \"Play\",\n  \"ge-qu-lie-biao\": \"Track List\",\n  \"deng-lu-ni-de-ku-gou-zhang-hao\": \"Log in to Your Kugou Account\",\n  \"qing-shu-ru-shou-ji-hao\": \"Enter Phone Number\",\n  \"qing-shu-ru-yan-zheng-ma\": \"Enter Verification Code\",\n  \"fa-song-yan-zheng-ma\": \"Send Code\",\n  \"li-ji-deng-lu\": \"Log In\",\n  \"qing-shu-ru-deng-lu-you-xiang\": \"Enter Username\",\n  \"qing-shu-ru-mi-ma\": \"Enter Password\",\n  \"you-xiang-deng-lu\": \"Username Login\",\n  \"er-wei-ma\": \"QR Code\",\n  \"login-tips\": \"MoeKoe does not store your account information on any server. Your password is encrypted locally before being sent to Kugou's official servers. MoeKoe is not an official Kugou website — enter your credentials at your own risk. Not all accounts support password login.\",\n  \"shi-yong-yan-zheng-ma-deng-lu\": \"Log in with SMS Code\",\n  \"shou-ji-hao-deng-lu\": \"Phone Login\",\n  \"sao-ma-deng-lu\": \"QR Code Login\",\n  \"qing-shu-ru-shou-ji-hao-ma\": \"Enter Phone Number\",\n  \"shou-ji-hao-ge-shi-cuo-wu\": \"Invalid Phone Number\",\n  \"qing-shu-ru-you-xiang\": \"Enter Username\",\n  \"you-xiang-ge-shi-cuo-wu\": \"Invalid Email Format\",\n  \"qing-shi-yong-ku-gou-sao-miao-er-wei-ma-deng-lu\": \"Scan QR Code with the Kugou App\",\n  \"deng-lu-cheng-gong\": \"Login Successful\",\n  \"deng-lu-shi-bai\": \"Login Failed\",\n  \"yan-zheng-ma-fa-song-shi-bai\": \"Failed to Send Code\",\n  \"yan-zheng-ma-yi-fa-song\": \"Code Sent\",\n  \"wu-xiang-ying-shu-ju\": \"No Response from Server\",\n  \"huo-qu-er-wei-ma-shi-bai\": \"Failed to Get QR Code\",\n  \"er-wei-ma-sheng-cheng-shi-bai\": \"Failed to Generate QR Code\",\n  \"er-wei-ma-deng-lu-cheng-gong\": \"QR Code Login Successful\",\n  \"er-wei-ma-yi-guo-qi-qing-zhong-xin-sheng-cheng\": \"QR Code Expired. Please Regenerate\",\n  \"er-wei-ma-jian-ce-shi-bai\": \"QR Code Verification Failed\",\n  \"deng-lu-shi-bai-0\": \"Login Failed: \",\n  \"yan-zheng-ma-fa-song-shi-bai-0\": \"Failed to Send Code: \",\n  \"yong-hu\": \"User\",\n  \"yi-sao-ma-deng-dai-que-ren\": \"QR Code Scanned. Confirm in App\",\n  \"yong-hu-tou-xiang\": \"Avatar\",\n  \"de-yin-le-ku\": \"'s Music Library\",\n  \"wo-xi-huan-ting\": \"Liked Tracks\",\n  \"wo-chuang-jian-de-ge-dan\": \"My Playlists\",\n  \"wo-shou-cang-de-ge-dan\": \"Saved Playlists\",\n  \"wo-guan-zhu-de-ge-shou\": \"Following Artists\",\n  \"deng-lu-shi-xiao-qing-zhong-xin-deng-lu\": \"Session Expired. Please Log In Again\",\n  \"gai-nian-ban\": \"Concept Version:\",\n  \"chang-ting-ban\": \"Standard Version:\",\n  \"shou-ge\": \"tracks\",\n  \"shou-ye\": \"Home\",\n  \"yin-le-ku\": \"Library\",\n  \"sou-suo-yin-le-ge-shou-ge-dan\": \"Search music, artists, playlists...\",\n  \"she-zhi\": \"Settings\",\n  \"tui-chu\": \"Log Out\",\n  \"deng-lu\": \"Log In\",\n  \"geng-xin\": \"Update\",\n  \"guan-yu\": \"About\",\n  \"mian-ze-sheng-ming\": \"Disclaimer\",\n  \"0-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"0. This is an unofficial Kugou client. For full features, use the official app.\",\n  \"1-ben-xiang-mu-jin-gong-xue-xi-shi-yong-qing-zun-zhong-ban-quan-qing-wu-li-yong-ci-xiang-mu-cong-shi-shang-ye-hang-wei-ji-fei-fa-yong-tu\": \"1. This project is for educational purposes only. Please respect copyright and do not use it for commercial or illegal purposes.\",\n  \"2-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-chan-sheng-ban-quan-shu-ju-dui-yu-zhe-xie-ban-quan-shu-ju-ben-xiang-mu-bu-yong-you-ta-men-de-suo-you-quan-wei-le-bi-mian-qin-quan-shi-yong-zhe-wu-bi-zai-24-xiao-shi-nei-qing-chu-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-suo-chan-sheng-de-ban-quan-shu-ju\": \"2. Usage may generate copyrighted data. This project does not own such data. To avoid infringement, delete any copyrighted data within 24 hours.\",\n  \"3-you-yu-shi-yong-ben-xiang-mu-chan-sheng-de-bao-kuo-you-yu-ben-xie-yi-huo-you-yu-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-er-yin-qi-de-ren-he-xing-zhi-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-sun-hai-bao-kuo-dan-bu-xian-yu-yin-shang-yu-sun-shi-ting-gong-ji-suan-ji-gu-zhang-huo-gu-zhang-yin-qi-de-sun-hai-pei-chang-huo-ren-he-ji-suo-you-qi-ta-shang-ye-sun-hai-huo-sun-shi-you-shi-yong-zhe-fu-ze\": \"3. Users are responsible for any direct or indirect damages arising from use or inability to use this project.\",\n  \"4-jin-zhi-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-dui-yu-shi-yong-zhe-zai-ming-zhi-huo-bu-zhi-dang-di-fa-lv-fa-gui-bu-yun-xu-de-qing-kuang-xia-shi-yong-ben-xiang-mu-suo-zao-cheng-de-ren-he-wei-fa-wei-gui-hang-wei-you-shi-yong-zhe-cheng-dan-ben-xiang-mu-bu-cheng-dan-you-ci-zao-cheng-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-ze-ren\": \"4. Do not use this project in violation of local laws. Users are responsible for any legal violations.\",\n  \"5-yin-le-ping-tai-bu-yi-qing-zun-zhong-ban-quan-zhi-chi-zheng-ban\": \"5. Please respect copyright and support legitimate music platforms.\",\n  \"6-ben-xiang-mu-jin-yong-yu-dui-ji-shu-ke-hang-xing-de-tan-suo-ji-yan-jiu-bu-jie-shou-ren-he-shang-ye-bao-kuo-dan-bu-xian-yu-guang-gao-deng-he-zuo-ji-juan-zeng\": \"6. This project is for technical research only. No commercial cooperation or donations accepted.\",\n  \"7-ru-guo-guan-fang-yin-le-ping-tai-jue-de-ben-xiang-mu-bu-tuo-ke-lian-xi-ben-xiang-mu-geng-gai-huo-yi-chu\": \"7. If rights holders have concerns, they may contact developers for removal.\",\n  \"yong-hu-tiao-kuan\": \"Terms of Use\",\n  \"1-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"1. This is an unofficial Kugou client. For full features, use the official app.\",\n  \"2-ben-xiang-mu-jin-gong-xue-xi-jiao-liu-shi-yong-nin-zai-shi-yong-guo-cheng-zhong-ying-zun-zhong-ban-quan-bu-de-yong-yu-shang-ye-huo-fei-fa-yong-tu\": \"2. This project is for educational purposes. Respect copyright and do not use for commercial or illegal purposes.\",\n  \"3-zai-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-sheng-cheng-ban-quan-nei-rong-ben-xiang-mu-bu-yong-you-zhe-xie-ban-quan-nei-rong-de-suo-you-quan-wei-le-bi-mian-qin-quan-hang-wei-nin-xu-zai-24-xiao-shi-nei-qing-chu-you-ben-xiang-mu-chan-sheng-de-ban-quan-nei-rong\": \"3. Usage may generate copyrighted content. Delete such content within 24 hours.\",\n  \"4-ben-xiang-mu-de-kai-fa-zhe-bu-dui-yin-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-suo-dao-zhi-de-ren-he-sun-hai-cheng-dan-ze-ren-bao-kuo-dan-bu-xian-yu-shu-ju-diu-shi-ting-gong-ji-suan-ji-gu-zhang-huo-qi-ta-jing-ji-sun-shi\": \"4. Developers are not liable for any damages from use of this project.\",\n  \"5-nin-bu-de-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-yin-wei-fan-fa-lv-fa-gui-suo-dao-zhi-de-ren-he-fa-lv-hou-guo-you-yong-hu-cheng-dan\": \"5. Do not use this project in violation of local laws. Users bear all legal consequences.\",\n  \"6-ben-xiang-mu-jin-yong-yu-ji-shu-tan-suo-he-yan-jiu-bu-jie-shou-ren-he-shang-ye-he-zuo-guang-gao-huo-juan-zeng-ru-guo-guan-fang-yin-le-ping-tai-dui-ci-xiang-mu-cun-you-yi-lv-ke-sui-shi-lian-xi-kai-fa-zhe-yi-chu-xiang-guan-nei-rong\": \"6. This project is for technical research only. Rights holders may contact developers for removal.\",\n  \"tong-yi-ji-xu-shi-yong-ben-xiang-mu-nin-ji-jie-shou-yi-shang-tiao-kuan-sheng-ming-nei-rong\": \"By continuing to use this project, you accept the above terms.\",\n  \"tong-yi\": \"Accept\",\n  \"bu-tong-yi\": \"Decline\",\n  \"que-ding\": \"OK\",\n  \"qu-xiao\": \"Cancel\",\n  \"shao-nv-qi-dao-zhong\": \"Loading...\",\n  \"zan-wu-ge-ci\": \"No Lyrics Available\",\n  \"ni-huan-mei-you-tian-jia-ge-quo-kuai-qu-tian-jia-ba\": \"No tracks yet. Add some!\",\n  \"huo-qu-ge-dan-shi-bai\": \"Failed to Load Playlist\",\n  \"huo-qu-yin-le-shi-bai\": \"Failed to Load Music\",\n  \"3-miao-hou-zi-dong-qie-huan-xia-yi-shou\": \"Next track in 3 seconds\",\n  \"huo-qu-yin-le-di-zhi-shi-bai\": \"Failed to Get Track URL\",\n  \"huo-qu-ge-ci-zhong\": \"Loading Lyrics...\",\n  \"huo-qu-ge-ci-shi-bai\": \"Failed to Load Lyrics\",\n  \"dan-qu-xun-huan\": \"Repeat Track\",\n  \"lie-biao-xun-huan\": \"Repeat Playlist\",\n  \"shun-xu-bo-fang\": \"Sequential\",\n  \"sui-ji-bo-fang\": \"Shuffle\",\n  \"bo-fang-lie-biao\": \"Play Queue\",\n  \"zheng-zai-sheng-cheng-er-wei-ma\": \"Generating QR Code...\",\n  \"zhe-li-shi-mo-du-mei-you\": \"Nothing Here Yet\",\n  \"wu-sun-yin-zhi-1104kbps\": \"Lossless — 1104 Kbps\",\n  \"gao-pin-zhi-yin-le-xu-yao-deng-lu-hou-cai-neng-bo-fango\": \"Log in for High Quality Playback\",\n  \"tian-jia-ge-dan\": \"Add to Playlist\",\n  \"qing-xian-deng-lu\": \"Please Log In First\",\n  \"cheng-gong-tian-jia-dao-ge-dan\": \"Added to Playlist\",\n  \"tian-jia-dao-ge-dan-shi-bai\": \"Failed to Add to Playlist\",\n  \"cheng-gong-qu-xiao-shou-cang\": \"Removed from Favorites\",\n  \"qu-xiao-shou-cang-shi-bai\": \"Failed to Remove\",\n  \"ni-que-ren-yao-tui-chu-deng-lu-ma\": \"Log Out?\",\n  \"gai-ge-qu-zan-wu-ban-quan\": \"Track Unavailable (No License)\",\n  \"wo-shou-cang-de-zhuan-ji\": \"Saved Albums\",\n  \"bo-fang-shi-bai\": \"Playback Failed\",\n  \"wo-guan-zhu-de-hao-you\": \"Friends\",\n  \"tian-jia-cheng-gong\": \"Added\",\n  \"chuang-jian-ge-dan\": \"Create Playlist\",\n  \"qing-shu-ru-xin-de-ge-dan-ming-cheng\": \"Enter Playlist Name\",\n  \"chuang-jian-shi-bai\": \"Failed to Create\",\n  \"que-ren-shan-chu-ge-dan\": \"Delete Playlist?\",\n  \"shou-cang-cheng-gong\": \"Added to Favorites\",\n  \"shou-cang-shi-bai\": \"Failed to Add to Favorites\",\n  \"wo-guan-zhu-de-yi-ren\": \"Following Musicians\",\n  \"sou-suo-ge-qu\": \"Search Tracks...\",\n  \"dang-qian-bo-fang-ge-qu\": \"Now Playing\",\n  \"fan-hui-ding-bu\": \"Back to Top\",\n  \"yi-fu-zhi-fen-xiang-ma-qing-zai-moekoe-ke-hu-duan-zhong-fang-wen\": \"Code Copied. Open in MoeKoe\",\n  \"kou-ling-yi-fu-zhi,kuai-ba-ge-qu-fen-xiang-gei-peng-you-ba\": \"Copied! Share with Friends\",\n  \"ge-qu-shu-ju-cuo-wu\": \"Playlist Not Found\",\n  \"xi-tong\": \"System\",\n  \"quan-ju-kuai-jie-jian\": \"Global Shortcuts\",\n  \"zi-ding-yi-kuai-jie-jian\": \"Customize Shortcuts\",\n  \"kuai-jie-jian-she-zhi\": \"Keyboard Shortcuts\",\n  \"xian-shi-yin-cang-zhu-chuang-kou\": \"Show/Hide Window\",\n  \"tui-chu-zhu-cheng-xu\": \"Quit Application\",\n  \"shang-yi-shou\": \"Previous Track\",\n  \"xia-yi-shou\": \"Next Track\",\n  \"zan-ting-bo-fang\": \"Play/Pause\",\n  \"yin-liang-zeng-jia\": \"Volume Up\",\n  \"yin-liang-jian-xiao\": \"Volume Down\",\n  \"jing-yin\": \"Mute\",\n  \"bao-cun\": \"Save\",\n  \"fei-ke-hu-duan-huan-jing-wu-fa-qi-yong\": \"Not Available in Web Version\",\n  \"qing-an-xia-xiu-shi-jian\": \"Press Modifier Key\",\n  \"qing-an-xia-qi-ta-jian\": \"+ [Press a Key]\",\n  \"kuai-jie-jian-bi-xu-bao-han-zhi-shao-yi-ge-xiu-shi-jian-ctrlaltshiftcommand\": \"Shortcut Must Include Ctrl/Alt/Shift/Command\",\n  \"gai-kuai-jie-jian-yu\": \"This Shortcut Conflicts with \",\n  \"de-kuai-jie-jian-chong-tu\": \"\",\n  \"cun-zai-wu-xiao-de-kuai-jie-jian-she-zhi-qing-que-bao-mei-ge-kuai-jie-jian-du-bao-han-xiu-shi-jian\": \"Invalid Shortcut. Add a Modifier Key\",\n  \"bao-cun-she-zhi-shi-bai\": \"Failed to Save Settings\",\n  \"mei-ri-tui-jian\": \"Daily Recommendations\",\n  \"mei-you-zheng-zai-bo-fang-de-ge-qu\": \"Nothing Playing\",\n  \"shou-cang-dao\": \"Save to\",\n  \"mei-you-ge-dan\": \"No Playlists\",\n  \"jin-yong-gpu-jia-su-zhong-qi-sheng-xiao\": \"Disable GPU Acceleration\",\n  \"jie-mian\": \"Interface\",\n  \"guan-bi-shi-minimize-to-tray\": \"Minimize to Tray on Close\",\n  \"mi-gan-cheng\": \"Orange\",\n  \"zhu-ce\": \"No Account?\",\n  \"xin-zeng-zhang-hao-qing-xian-zai-guan-fang-ke-hu-duan-zhong-deng-lu-yi-ci\": \"New Account? Log in to Official App First\",\n  \"shua-xin-hou-sheng-xiao\": \"(After Refresh)\",\n  \"zhong-qi-hou-sheng-xiao\": \"(After Restart)\",\n  \"shi-pei-gao-dpi\": \"High DPI Support\",\n  \"hires-yin-zhi\": \"Hi-Res\",\n  \"kui-she-chao-qing-yin-zhi\": \"Viper Ultra HD\",\n  \"tian-jia-wo-xi-huan\": \"Add to Liked\",\n  \"qie-huan-bo-fang-mo-shi\": \"Change Playback Mode\",\n  \"ke-yong\": \"Available\",\n  \"wo-de-yun-pan\": \"My Cloud\",\n  \"yun-pan-ge-qu-shu\": \"Cloud Tracks\",\n  \"yun-pan-miao-shu\": \"Your uploaded music files\",\n  \"yun-pan-ge-qu\": \"Cloud Music\",\n  \"cong-yun-pan-shan-chu\": \"Remove from Cloud\",\n  \"que-ren-shan-chu-yun-pan-ge-qu\": \"Remove Selected Tracks from Cloud?\",\n  \"shang-chuan-yin-le\": \"Upload\",\n  \"pi-liang-cao-zuo\": \"Select Multiple\",\n  \"pwa-app\": \"PWA App\",\n  \"install\": \"Install\",\n  \"yin-pin-jia-zai-shi-bai\": \"Failed to Load Audio\",\n  \"guan-zhu\": \"Following\",\n  \"fen-si\": \"Followers\",\n  \"hao-you\": \"Friends\",\n  \"fang-wen\": \"Views\",\n  \"qian-dao\": \"Check In\",\n  \"xiao-shi\": \"hrs\",\n  \"fen-zhong\": \"min\",\n  \"le-ling\": \"Member Since\",\n  \"nian\": \"yrs\",\n  \"ting-ge-shi-chang\": \"Listening Time\",\n  \"zheng-zai-jia-zai-quan-bu-ge-qu\": \"Loading All Tracks...\",\n  \"bo-fang-chu-cuo\": \"Playback Error\",\n  \"bo-fang-shi-bai-qu-mu-wei-kong\": \"Playlist Is Empty\",\n  \"shan-chu-cheng-gong\": \"Deleted\",\n  \"tian-jia-dao-bo-fang-lie-biao-cheng-gong\": \"Added to Queue\",\n  \"hot\": \"Hot\",\n  \"new\": \"New\",\n  \"yun-pan-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"Cloud tracks cannot be added to playlists\",\n  \"xian-qu-kan-kan-ni-de-shou-cang-jia-ba\": \"Check your favorites first\",\n  \"yi-da-dao-zui-da-chong-shi-ci-shu\": \"Max retries reached. Please select a track manually\",\n  \"hui-fu-chu-chang-she-zhi-cheng-gong\": \"Settings reset. Restart to apply\",\n  \"liu-lan-qi-bu-zhi-chi-file-system-api\": \"Browser doesn't support File System API. Use Chrome 86+ or Edge 86+\",\n  \"cha-jian-an-zhuang-cheng-gong\": \"Plugin installed\",\n  \"zhi-chi-http-https-dai-li\": \"Only HTTP/HTTPS proxies are supported\",\n  \"ben-di-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"Local tracks cannot be added to playlists\",\n  \"mei-you-xuan-ze-zheng-que-de-ge-qu\": \"No track selected\",\n  \"zhuang-tai-lan-ge-ci-jin-zhi-chi-mac\": \"Menu bar lyrics only available on macOS\",\n  \"qian-dao-shi-bai\": \"Check-in failed. Don't check in too frequently\",\n  \"huo-qu-vip-shi-bai\": \"Failed to get VIP. Available once per day\",\n  \"qing-zai-web-huan-jing-xia-an-zhuang\": \"Install in web version\",\n  \"qing-shu-ru-you-xiao-de-url\": \"Enter a valid URL\",\n  \"zhe-shi-yi-ge-alert\": \"Alert\",\n  \"fei-mac-bu-zhi-chi-touchbar\": \"TouchBar only available on Mac\",\n  \"zi-ti-she-zhi\": \"Font Settings\",\n  \"mo-ren-zi-ti\": \"Default Font\",\n  \"hui-fu-chu-chang-she-zhi\": \"Reset Settings\",\n  \"zi-ti-url-di-zhi\": \"Font URL\",\n  \"qing-shu-ru-zi-ti-url-di-zhi\": \"Enter font URL\",\n  \"zi-ti-ming-cheng\": \"Font Name\",\n  \"qing-shu-ru-zi-ti-ming-cheng\": \"Enter font name\",\n  \"cha-jian\": \"Plugins\",\n  \"shua-xin-cha-jian\": \"Refresh\",\n  \"da-kai-cha-jian-mu-lu\": \"Open Folder\",\n  \"an-zhuang-cha-jian\": \"Install\",\n  \"zan-wu-cha-jian\": \"No Plugins\",\n  \"jiang-cha-jian-wen-jian-jia-fang-ru-cha-jian-mu-lu\": \"Place plugin folder in the plugins directory and click Refresh\",\n  \"kai-ji-zi-qi-dong\": \"Launch at Startup\",\n  \"wang-luo-mo-shi\": \"Network Mode\",\n  \"zhu-wang\": \"Main Network\",\n  \"qi-dong-shi-zui-xiao-hua\": \"Start Minimized\",\n  \"zu-zhi-xi-tong-xiu-mian\": \"Prevent Sleep\",\n  \"api-mo-shi\": \"API Mode\",\n  \"wang-luo-dai-li\": \"Network Proxy\",\n  \"zhuang-tai-lan-ge-ci\": \"Menu Bar Lyrics\",\n  \"ge-ci-fan-yi\": \"Lyrics Translation\",\n  \"dui-qi-fang-shi\": \"Alignment\",\n  \"ju-zhong\": \"Center\",\n  \"ju-zuo\": \"Left\",\n  \"ju-you\": \"Right\",\n  \"ping-heng-yin-pin-xiang-du\": \"Normalize Loudness\",\n  \"shu-ju-yuan\": \"Data Source\",\n  \"suo-fang-yin-zi\": \"Scale Factor\",\n  \"tiao-zheng-hou-xu-zhong-qi\": \"Restart app after adjustment\",\n  \"api-di-zhi\": \"API Address\",\n  \"websocket-di-zhi\": \"WebSocket Address\",\n  \"mo-ren-api-ti-shi\": \"These are default API addresses. Custom modification not supported in this version\",\n  \"dai-li-placeholder\": \"Enter HTTP/HTTPS proxy address, e.g.: http://127.0.0.1:7890\",\n  \"zheng-zai-ce-shi\": \"Testing...\",\n  \"ce-shi-lian-jie\": \"Test Connection\",\n  \"bao-cun-she-zhi-an-niu\": \"Save\",\n  \"qing-shu-ru-dai-li-di-zhi\": \"Enter proxy server address\",\n  \"ce-wang\": \"Testnet\",\n  \"kai-fa-wang\": \"Devnet\",\n  \"qi-yong\": \"Enabled\",\n  \"jin-yong\": \"Disabled\",\n  \"dai-li-di-zhi\": \"Proxy Address\",\n  \"gai-nian-ban-xuan-xiang\": \"Concept\",\n  \"zheng-shi-ban\": \"Official\",\n  \"dai-li-lian-jie-cheng-gong\": \"Proxy connected, IP: \",\n  \"dai-li-lian-jie-shi-bai\": \"Proxy connection failed: \",\n  \"lian-jie-chao-shi\": \"Connection timeout\",\n  \"lian-jie-cuo-wu\": \"Connection error: \",\n  \"jin-zhi-chi-mac\": \" (macOS only)\",\n  \"xian-shi-yin-cang-zhuo-mian-ge-ci\": \"Show/Hide Desktop Lyrics\",\n  \"ni-que-ren-hui-fu-chu-chang\": \"Reset all settings? This cannot be undone!\",\n  \"bang-zhu\": \"Help\",\n  \"dian-ji-she-zhi-kuai-jie-jian\": \"Click to set shortcut\",\n  \"wang-luo-jie-dian\": \"Network Node\",\n  \"zi-ti-wen-jian-di-zhi\": \"Font File URL\",\n  \"jia-zai-zhong\": \"Loading...\",\n  \"ban-ben\": \"Version\",\n  \"yi-qi-yong\": \"Enabled\",\n  \"da-kai-tan-chuang\": \"Settings\",\n  \"xie-zai\": \"Uninstall\",\n  \"zheng-zai-jia-zai-cha-jian\": \"Loading plugins...\",\n  \"web-cha-jian-ti-shi\": \"In web version, manage extensions via chrome://extensions/\",\n  \"da-kai-tan-chuang-shi-bai\": \"Failed to open plugin window\",\n  \"que-ren-xie-zai-cha-jian\": \"Uninstall plugin name?\",\n  \"xie-zai-cha-jian-shi-bai\": \"Failed to uninstall plugin\",\n  \"xuan-ze-wen-jian-shi-bai\": \"Failed to select file\",\n  \"an-zhuang-cha-jian-shi-bai\": \"Failed to install plugin\",\n  \"an-zhuang-cha-jian-chu-cuo\": \"Error installing plugin\",\n  \"cha-jian-bao\": \"Plugin archive\",\n  \"zhuo-mian-ge-ci\": \"Desktop Lyrics\",\n  \"bo-fang-su-du\": \"Playback Speed\",\n  \"wo-xi-huan\": \"Like\",\n  \"shou-cang-zhi\": \"Save to\",\n  \"fen-xiang-ge-qu\": \"Share\",\n  \"qie-huan-dao-yin-yi\": \"Switch to Romanization\",\n  \"qie-huan-dao-fan-yi\": \"Switch to Translation\",\n  \"wei-zhi-cuo-wu\": \"Unknown error\"\n}\n"
  },
  {
    "path": "src/language/ja.json",
    "content": "{\n  \"tui-jian\": \"推薦する\",\n  \"tui-jian-ge-qu\": \"おすすめの曲\",\n  \"tui-jian-ge-dan\": \"おすすめのプレイリスト\",\n  \"fa-xian\": \"発見する\",\n  \"da\": \"大きい\",\n  \"da-kai\": \"開ける\",\n  \"gao-yin-zhi-320kbps\": \"高品音質 - 320Kbps\",\n  \"ge-ci\": \"歌詞\",\n  \"ge-ci-zi-ti-da-xiao\": \"フルスクリーンの歌詞フォントサイズ\",\n  \"guan-bi\": \"閉鎖\",\n  \"guan-bi-an-niu\": \"閉じる\",\n  \"kai-qi\": \"オンにする\",\n  \"nan-nan-lan\": \"スカイブルー\",\n  \"pu-tong-yin-zhi\": \"通常の音質 - 128Kbps\",\n  \"qi-dong-wen-hou-yu\": \"挨拶を始める\",\n  \"qian-se\": \"明るい色\",\n  \"shao-nv-fen\": \"ガーリーピンク\",\n  \"shen-se\": \"暗い\",\n  \"sheng-yin\": \"音\",\n  \"tou-ding-lv\": \"ミントグリーン\",\n  \"wai-guan\": \"外観\",\n  \"native-title-bar\":\"ネイティブタイトルバー\",\n  \"xian-shi-ge-ci-bei-jing\": \"フルスクリーンの歌詞の背景カバーを表示します\",\n  \"xian-shi-zhuo-mian-ge-ci\": \"デスクトップの歌詞を表示する\",\n  \"xiao\": \"小さい\",\n  \"xuan-ze-wai-guan\": \"外観の選択\",\n  \"xuan-ze-yu-yan\": \"言語を選択してください\",\n  \"xuan-ze-zhu-se-tiao\": \"メインカラーを選択してください\",\n  \"yin-zhi-xuan-ze\": \"音質の選択\",\n  \"yu-yan\": \"言語\",\n  \"zhong\": \"真ん中\",\n  \"zhu-se-tiao\": \"メインカラー\",\n  \"zi-dong\": \"自動\",\n  \"0-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"0. このプログラムは Kugou のサードパーティ クライアントであり、Kugou 株式会社ではありません。より完全な機能が必要な場合は、株式会社クライアントをダウンロードして体験してください。\",\n  \"1-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"1. このプログラムは Kugou のサードパーティ クライアントであり、Kugou 株式会社ではありません。より完全な機能が必要な場合は、株式会社クライアントをダウンロードして体験してください。\",\n  \"1-ben-xiang-mu-jin-gong-xue-xi-shi-yong-qing-zun-zhong-ban-quan-qing-wu-li-yong-ci-xiang-mu-cong-shi-shang-ye-hang-wei-ji-fei-fa-yong-tu\": \"1. このプロジェクトは学習のみを目的としています。著作権を尊重してください。商業活動や違法な目的には使用しないでください。\",\n  \"2-ben-xiang-mu-jin-gong-xue-xi-jiao-liu-shi-yong-nin-zai-shi-yong-guo-cheng-zhong-ying-zun-zhong-ban-quan-bu-de-yong-yu-shang-ye-huo-fei-fa-yong-tu\": \"2. このプロジェクトは学習とコミュニケーションのみを目的としており、使用中は著作権を尊重する必要があり、商業目的または違法な目的で使用することはできません。\",\n  \"2-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-chan-sheng-ban-quan-shu-ju-dui-yu-zhe-xie-ban-quan-shu-ju-ben-xiang-mu-bu-yong-you-ta-men-de-suo-you-quan-wei-le-bi-mian-qin-quan-shi-yong-zhe-wu-bi-zai-24-xiao-shi-nei-qing-chu-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-suo-chan-sheng-de-ban-quan-shu-ju\": \"2. 本プロジェクトの利用中に著作権データが発生する場合があります。\\nこのプロジェクトは、これらの著作権で保護されたデータの所有権を所有しません。\\n侵害を避けるために、ユーザーはこのプロジェクトの使用中に生成された著作権データを 24 時間以内に消去する必要があります。\",\n  \"3-miao-hou-zi-dong-qie-huan-xia-yi-shou\": \"3秒後に自動的に次の曲に切り替わります\",\n  \"3-you-yu-shi-yong-ben-xiang-mu-chan-sheng-de-bao-kuo-you-yu-ben-xie-yi-huo-you-yu-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-er-yin-qi-de-ren-he-xing-zhi-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-sun-hai-bao-kuo-dan-bu-xian-yu-yin-shang-yu-sun-shi-ting-gong-ji-suan-ji-gu-zhang-huo-gu-zhang-yin-qi-de-sun-hai-pei-chang-huo-ren-he-ji-suo-you-qi-ta-shang-ye-sun-hai-huo-sun-shi-you-shi-yong-zhe-fu-ze\": \"3. 本契約または本プロジェクトの使用または使用不能から生じる、あらゆる性質の直接的、間接的、特別、付随的または結果的損害（信用の喪失、業務の停止、コンピュータの故障または誤動作に起因する損害を含みますがこれらに限定されません） 、またはその他すべての商業上の損害または損失）は、ユーザーの責任となります。\",\n  \"3-zai-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-sheng-cheng-ban-quan-nei-rong-ben-xiang-mu-bu-yong-you-zhe-xie-ban-quan-nei-rong-de-suo-you-quan-wei-le-bi-mian-qin-quan-hang-wei-nin-xu-zai-24-xiao-shi-nei-qing-chu-you-ben-xiang-mu-chan-sheng-de-ban-quan-nei-rong\": \"3. 本プロジェクトの利用過程において、著作権で保護されたコンテンツが生成される場合があります。\\nこのプロジェクトは、これらの著作権で保護されたコンテンツの所有権を所有しません。\\n侵害を避けるために、このプロジェクトによって生成された著作権で保護されたコンテンツを 24 時間以内に削除する必要があります。\",\n  \"4-ben-xiang-mu-de-kai-fa-zhe-bu-dui-yin-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-suo-dao-zhi-de-ren-he-sun-hai-cheng-dan-ze-ren-bao-kuo-dan-bu-xian-yu-shu-ju-diu-shi-ting-gong-ji-suan-ji-gu-zhang-huo-qi-ta-jing-ji-sun-shi\": \"4. 本プロジェクトの開発者は、データの損失、ダウンタイム、コンピュータの故障、またはその他の経済的損失を含むがこれらに限定されない、本プロジェクトの使用または使用不能から生じるいかなる損害についても責任を負いません。\",\n  \"4-jin-zhi-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-dui-yu-shi-yong-zhe-zai-ming-zhi-huo-bu-zhi-dang-di-fa-lv-fa-gui-bu-yun-xu-de-qing-kuang-xia-shi-yong-ben-xiang-mu-suo-zao-cheng-de-ren-he-wei-fa-wei-gui-hang-wei-you-shi-yong-zhe-cheng-dan-ben-xiang-mu-bu-cheng-dan-you-ci-zao-cheng-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-ze-ren\": \"4. 現地の法令に違反して本プロジェクトを使用することは禁止されています。\\nユーザーは、現地の法律や規制がこのプロジェクトの使用を許可していないことを知っているかどうかにかかわらず、ユーザーが引き起こした違法行為の責任を負うものとし、それによって生じる直接的、間接的、特別、偶発的、結果的責任を負いません。 。\",\n  \"5-nin-bu-de-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-yin-wei-fan-fa-lv-fa-gui-suo-dao-zhi-de-ren-he-fa-lv-hou-guo-you-yong-hu-cheng-dan\": \"5. 現地の法律や規制に違反してこのプロジェクトを使用することはできません。\\n法令違反による法的責任は利用者の負担となります。\",\n  \"5-yin-le-ping-tai-bu-yi-qing-zun-zhong-ban-quan-zhi-chi-zheng-ban\": \"5. 音楽プラットフォームは簡単ではありません。著作権を尊重し、本物のプラットフォームをサポートしてください。\",\n  \"6-ben-xiang-mu-jin-yong-yu-dui-ji-shu-ke-hang-xing-de-tan-suo-ji-yan-jiu-bu-jie-shou-ren-he-shang-ye-bao-kuo-dan-bu-xian-yu-guang-gao-deng-he-zuo-ji-juan-zeng\": \"6. 本プロジェクトは技術的実現可能性の探究・研究のみを目的としており、営利（広告等を含みますがこれに限定されません）の協力・寄付等は一切受け付けておりません。\",\n  \"6-ben-xiang-mu-jin-yong-yu-ji-shu-tan-suo-he-yan-jiu-bu-jie-shou-ren-he-shang-ye-he-zuo-guang-gao-huo-juan-zeng-ru-guo-guan-fang-yin-le-ping-tai-dui-ci-xiang-mu-cun-you-yi-lv-ke-sui-shi-lian-xi-kai-fa-zhe-yi-chu-xiang-guan-nei-rong\": \"6. このプロジェクトは技術の探索と研究のみに使用され、商業的な協力、広告、寄付は一切受け付けません。\\n音楽会社プラットフォームがこのプロジェクトに関して懸念がある場合は、いつでも開発者に連絡して関連コンテンツを削除することができます。\",\n  \"7-ru-guo-guan-fang-yin-le-ping-tai-jue-de-ben-xiang-mu-bu-tuo-ke-lian-xi-ben-xiang-mu-geng-gai-huo-yi-chu\": \"7. 音楽会社プラットフォームがこのプロジェクトが不適切であると判断した場合は、このプロジェクトに連絡して変更または削除することができます。\",\n  \"bo-fang\": \"遊ぶ\",\n  \"bo-fang-lie-biao\": \"プレイリスト\",\n  \"bu-tong-yi\": \"同意しない\",\n  \"chang-ting-ban\": \"無料試聴版：\",\n  \"dan-qu-xun-huan\": \"シングルループ\",\n  \"de-yin-le-ku\": \"の音楽ライブラリ\",\n  \"deng-lu\": \"ログイン\",\n  \"deng-lu-cheng-gong\": \"ログイン成功\",\n  \"deng-lu-ni-de-ku-gou-zhang-hao\": \"Kugou アカウントにログインします\",\n  \"deng-lu-shi-bai\": \"ログインに失敗しました\",\n  \"deng-lu-shi-bai-0\": \"ログインに失敗しました、\",\n  \"deng-lu-shi-xiao-qing-zhong-xin-deng-lu\": \"ログインに失敗しました。再度ログインしてください\",\n  \"di\": \"いいえ。\",\n  \"er-wei-ma\": \"QRコード\",\n  \"er-wei-ma-deng-lu-cheng-gong\": \"QRコードログイン成功\",\n  \"er-wei-ma-jian-ce-shi-bai\": \"QRコードの検出に失敗しました\",\n  \"er-wei-ma-sheng-cheng-shi-bai\": \"QRコードの生成に失敗しました\",\n  \"er-wei-ma-yi-guo-qi-qing-zhong-xin-sheng-cheng\": \"QRコードの有効期限が切れていますので再生成してください\",\n  \"fa-song-yan-zheng-ma\": \"確認コードを送信する\",\n  \"gai-nian-ban\": \"コンセプトバージョン:\",\n  \"ge-qu-lie-biao\": \"曲リスト\",\n  \"geng-xin\": \"更新する\",\n  \"gong\": \"一般\",\n  \"guan-yu\": \"について\",\n  \"huo-qu-ge-ci-shi-bai\": \"歌詞の取得に失敗しました\",\n  \"huo-qu-ge-ci-zhong\": \"歌詞を取得しています...\",\n  \"huo-qu-ge-dan-shi-bai\": \"プレイリストの取得に失敗しました\",\n  \"huo-qu-yin-le-di-zhi-shi-bai\": \"音楽アドレスの取得に失敗しました\",\n  \"huo-qu-yin-le-shi-bai\": \"音楽の取得に失敗しました\",\n  \"li-ji-deng-lu\": \"今すぐログインしてください\",\n  \"lie-biao-xun-huan\": \"リストループ\",\n  \"login-tips\": \"MoeKoe は、アカウント情報をクラウドに保存しないことを約束します。\\nあなたのパスワードはローカルで暗号化され、Kugou 株式会社に送信されます。 \\nMengyin は Kugou の株式会社 Web サイトではありません。アカウント情報を入力する前に慎重に検討してください。すべてのアカウントがアカウントパスワードログインをサポートしているわけではありません\",\n  \"mian-ze-sheng-ming\": \"免責事項\",\n  \"ni-huan-mei-you-tian-jia-ge-quo-kuai-qu-tian-jia-ba\": \"まだ曲を追加していませんので、追加してください。\",\n  \"qing-shi-yong-ku-gou-sao-miao-er-wei-ma-deng-lu\": \"Kugou を使用して QR コードをスキャンしてログインしてください\",\n  \"qing-shu-ru-deng-lu-you-xiang\": \"ログインアカウントを入力してください\",\n  \"qing-shu-ru-mi-ma\": \"パスワードを入力してください\",\n  \"qing-shu-ru-shou-ji-hao\": \"携帯電話番号を入力してください\",\n  \"qing-shu-ru-shou-ji-hao-ma\": \"携帯電話番号を入力してください\",\n  \"qing-shu-ru-yan-zheng-ma\": \"確認コードを入力してください\",\n  \"qing-shu-ru-you-xiang\": \"アカウント番号を入力してください\",\n  \"qu-xiao\": \"キャンセル\",\n  \"que-ding\": \"もちろん\",\n  \"sao-ma-deng-lu\": \"コードをスキャンしてログインします\",\n  \"shang-yi-ye\": \"前のページへ\",\n  \"shao-nv-qi-dao-zhong\": \"少女は祈っている……。\",\n  \"she-zhi\": \"設定\",\n  \"shi-yong-yan-zheng-ma-deng-lu\": \"確認コードを使用してログインします。\",\n  \"shou-ge\": \"首の歌\",\n  \"shou-ji-hao-deng-lu\": \"携帯電話番号ログイン\",\n  \"shou-ji-hao-ge-shi-cuo-wu\": \"携帯電話番号の形式エラー\",\n  \"shou-ye\": \"フロントページ\",\n  \"shun-xu-bo-fang\": \"順番に再生する\",\n  \"sou-suo-jie-guo\": \"検索結果\",\n  \"sou-suo-yin-le-ge-shou-ge-dan\": \"音楽、歌手、プレイリスト、共有コードを検索...\",\n  \"sui-ji-bo-fang\": \"シャッフル\",\n  \"tong-yi\": \"同意する\",\n  \"tong-yi-ji-xu-shi-yong-ben-xiang-mu-nin-ji-jie-shou-yi-shang-tiao-kuan-sheng-ming-nei-rong\": \"このプロジェクトの使用を継続することに同意すると、上記の規約と声明に同意したことになります。\",\n  \"tui-chu\": \"やめる\",\n  \"wo-chuang-jian-de-ge-dan\": \"私が作成したプレイリスト\",\n  \"wo-guan-zhu-de-ge-shou\": \"私がフォローしている歌手\",\n  \"wo-shou-cang-de-ge-dan\": \"私のプレイリストのコレクション\",\n  \"wo-xi-huan-ting\": \"聞くのが好きです\",\n  \"wu-xiang-ying-shu-ju\": \"応答データがありません\",\n  \"xia-yi-ye\": \"次のページ\",\n  \"yan-zheng-ma-fa-song-shi-bai\": \"確認コードの送信に失敗しました\",\n  \"yan-zheng-ma-fa-song-shi-bai-0\": \"確認コードの送信に失敗しました。\",\n  \"yan-zheng-ma-yi-fa-song\": \"認証コードが送信されました\",\n  \"ye\": \"ページ\",\n  \"yi-sao-ma-deng-dai-que-ren\": \"コードをスキャンしました。確認を待っています\",\n  \"yin-le-ku\": \"音楽ライブラリ\",\n  \"yong-hu\": \"ユーザー\",\n  \"yong-hu-tiao-kuan\": \"ユーザー規約\",\n  \"yong-hu-tou-xiang\": \"ユーザーのアバター\",\n  \"you-xiang-deng-lu\": \"アカウントログイン\",\n  \"you-xiang-ge-shi-cuo-wu\": \"メール形式エラー\",\n  \"zan-wu-ge-ci\": \"まだ歌詞がありません\",\n  \"huo-qu-er-wei-ma-shi-bai\": \"QRコードの取得に失敗しました\",\n  \"zheng-zai-sheng-cheng-er-wei-ma\": \"QRコードを生成中...\",\n  \"zhe-li-shi-mo-du-mei-you\": \"ここには何もない\",\n  \"wu-sun-yin-zhi-1104kbps\": \"ロスレス音質 - 1104kbps\",\n  \"gao-pin-zhi-yin-le-xu-yao-deng-lu-hou-cai-neng-bo-fango\": \"高品質の音楽を再生するにはログインが必要です~\",\n  \"tian-jia-ge-dan\": \"プレイリストに追加\",\n  \"qing-xian-deng-lu\": \"まずログインしてください\",\n  \"cheng-gong-tian-jia-dao-ge-dan\": \"プレイリストに追加されました！\",\n  \"tian-jia-dao-ge-dan-shi-bai\": \"プレイリストに追加できませんでした!\",\n  \"cheng-gong-qu-xiao-shou-cang\": \"お気に入りをキャンセルする\",\n  \"qu-xiao-shou-cang-shi-bai\": \"キャンセルに失敗しました\",\n  \"ni-que-ren-yao-tui-chu-deng-lu-ma\": \"ログアウトしてもよろしいですか?\",\n  \"gai-ge-qu-zan-wu-ban-quan\": \"この曲には著作権がありません\",\n  \"wo-shou-cang-de-zhuan-ji\": \"私がコレクションしたアルバム\",\n  \"bo-fang-shi-bai\": \"再生に失敗しました\",\n  \"wo-guan-zhu-de-hao-you\": \"私がフォローしている友達\",\n  \"tian-jia-cheng-gong\": \"正常に追加されました\",\n  \"chuang-jian-ge-dan\": \"プレイリストの作成\",\n  \"qing-shu-ru-xin-de-ge-dan-ming-cheng\": \"新しいプレイリスト名を入力してください\",\n  \"chuang-jian-shi-bai\": \"作成に失敗しました\",\n  \"que-ren-shan-chu-ge-dan\": \"プレイリストを削除しますか?\",\n  \"shou-cang-shi-bai\": \"収集に失敗しました\",\n  \"shou-cang-cheng-gong\": \"収集に成功しました\",\n  \"wo-guan-zhu-de-yi-ren\": \"私がフォローしているミュージシャン\",\n  \"sou-suo-ge-qu\": \"曲を検索...\",\n  \"fan-hui-ding-bu\": \"トップに戻る\",\n  \"dang-qian-bo-fang-ge-qu\": \"現在再生中の曲\",\n  \"yi-fu-zhi-fen-xiang-ma-qing-zai-moekoe-ke-hu-duan-zhong-fang-wen\": \"共有コードがコピーされました。MoeKoe クライアントで表示してください。\",\n  \"ge-qu-shu-ju-cuo-wu\": \"プレイリストが存在しません\",\n  \"bao-cun\": \"保存\",\n  \"bao-cun-she-zhi-shi-bai\": \"設定の保存に失敗しました\",\n  \"cun-zai-wu-xiao-de-kuai-jie-jian-she-zhi-qing-que-bao-mei-ge-kuai-jie-jian-du-bao-han-xiu-shi-jian\": \"無効なショートカット キー設定があります。各ショートカット キーに修飾キーが含まれていることを確認してください。\",\n  \"de-kuai-jie-jian-chong-tu\": \"ショートカットキーの競合\",\n  \"fei-ke-hu-duan-huan-jing-wu-fa-qi-yong\": \"非クライアント環境では有効にできません\",\n  \"gai-kuai-jie-jian-yu\": \"このショートカットキーは、\",\n  \"jing-yin\": \"ミュート\",\n  \"kuai-jie-jian-bi-xu-bao-han-zhi-shao-yi-ge-xiu-shi-jian-ctrlaltshiftcommand\": \"ショートカットキーには、少なくとも1つの修飾子（ctrl/alt/shift/command）を含める必要があります\",\n  \"kuai-jie-jian-she-zhi\": \"ショートカットキーの設定\",\n  \"qing-an-xia-qi-ta-jian\": \"[他のキーを押してください]\",\n  \"qing-an-xia-xiu-shi-jian\": \"修飾キーを押してください\",\n  \"quan-ju-kuai-jie-jian\": \"グローバルショートカットキー\",\n  \"shang-yi-shou\": \"最後\",\n  \"tui-chu-zhu-cheng-xu\": \"メインプログラムを終了する\",\n  \"xi-tong\": \"システム\",\n  \"xia-yi-shou\": \"次\",\n  \"xian-shi-yin-cang-zhu-chuang-kou\": \"メインウィンドウを表示/非表示にします\",\n  \"yin-liang-jian-xiao\": \"音量を下げる\",\n  \"yin-liang-zeng-jia\": \"音量の増加\",\n  \"zan-ting-bo-fang\": \"一時停止/再生\",\n  \"zi-ding-yi-kuai-jie-jian\": \"カスタムショートカットキー\",\n  \"mei-ri-tui-jian\": \"毎日の推奨\",\n  \"mei-you-zheng-zai-bo-fang-de-ge-qu\": \"演奏している曲はありません\",\n  \"shou-cang-dao\": \"集める\",\n  \"mei-you-ge-dan\": \"曲リストはありません\",\n  \"jin-yong-gpu-jia-su-zhong-qi-sheng-xiao\": \"GPU加速度を無効にします\",\n  \"jie-mian\": \"インタフェース\",\n  \"guan-bi-shi-minimize-to-tray\": \"窓を閉めるときにトレイに移動します\",\n  \"mi-gan-cheng\": \"ミアンオレンジ\",\n  \"zhu-ce\": \"まだアカウントを持っていませんか？\",\n  \"xin-zeng-zhang-hao-qing-xian-zai-guan-fang-ke-hu-duan-zhong-deng-lu-yi-ci\": \"新しいアカウントについては、公式クライアントにログインしてください\",\n  \"shua-xin-hou-sheng-xiao\": \"（リフレッシュ後に効果的）\",\n  \"zhong-qi-hou-sheng-xiao\": \"（再起動後に有効）\",\n  \"shi-pei-gao-dpi\": \"高DPIサポートを有効にする\",\n  \"kui-she-chao-qing-yin-zhi\": \"Viper Ultra-definition Sound Quality\",\n  \"hires-yin-zhi\": \"高解像度の音質\",\n  \"tian-jia-wo-xi-huan\": \"私のお気に入りに追加してください\",\n  \"qie-huan-bo-fang-mo-shi\": \"再生モードを切り替えます\",\n  \"pwa-app\": \"PWA アプリケーション\",\n  \"install\": \"インストール\",\n  \"yin-pin-jia-zai-shi-bai\": \"音楽のロードに失敗しました\",\n  \"zheng-zai-jia-zai-quan-bu-ge-qu\": \"すべての曲を読み込んでいます...\",\n  \"bo-fang-chu-cuo\": \"再生エラー\",\n  \"bo-fang-shi-bai-qu-mu-wei-kong\": \"再生失敗、曲目が空です\",\n  \"shan-chu-cheng-gong\": \"削除成功\",\n  \"tian-jia-dao-bo-fang-lie-biao-cheng-gong\": \"再生リストに追加しました\",\n  \"hot\": \"人気\",\n  \"new\": \"最新\",\n  \"yun-pan-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"クラウド音楽はプレイリストに追加できません\",\n  \"xian-qu-kan-kan-ni-de-shou-cang-jia-ba\": \"お気に入りを確認してください\",\n  \"yi-da-dao-zui-da-chong-shi-ci-shu\": \"最大リトライ回数に達しました。曲を手動で選択してください\",\n  \"hui-fu-chu-chang-she-zhi-cheng-gong\": \"初期設定に戻しました。アプリを再起動してください\",\n  \"liu-lan-qi-bu-zhi-chi-file-system-api\": \"ブラウザはFile System APIをサポートしていません。Chrome 86+またはEdge 86+をご使用ください\",\n  \"cha-jian-an-zhuang-cheng-gong\": \"プラグインをインストールしました\",\n  \"zhi-chi-http-https-dai-li\": \"HTTP/HTTPSプロキシのみサポート\",\n  \"ben-di-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"ローカル音楽はプレイリストに追加できません\",\n  \"mei-you-xuan-ze-zheng-que-de-ge-qu\": \"曲が選択されていません\",\n  \"zhuang-tai-lan-ge-ci-jin-zhi-chi-mac\": \"メニューバー歌詞はmacOSのみ対応\",\n  \"qian-dao-shi-bai\": \"チェックイン失敗。頻繁にチェックインしないでください\",\n  \"huo-qu-vip-shi-bai\": \"VIP取得失敗。1日1回のみ取得可能\",\n  \"qing-zai-web-huan-jing-xia-an-zhuang\": \"Web版でインストールしてください\",\n  \"qing-shu-ru-you-xiao-de-url\": \"有効なURLを入力してください\",\n  \"zhe-shi-yi-ge-alert\": \"お知らせ\",\n  \"fei-mac-bu-zhi-chi-touchbar\": \"TouchBarはMacのみ対応\",\n  \"zi-ti-she-zhi\": \"フォント設定\",\n  \"mo-ren-zi-ti\": \"デフォルトフォント\",\n  \"hui-fu-chu-chang-she-zhi\": \"初期設定に戻す\",\n  \"zi-ti-url-di-zhi\": \"フォントURL\",\n  \"qing-shu-ru-zi-ti-url-di-zhi\": \"フォントURLを入力\",\n  \"zi-ti-ming-cheng\": \"フォント名\",\n  \"qing-shu-ru-zi-ti-ming-cheng\": \"フォント名を入力\",\n  \"cha-jian\": \"プラグイン\",\n  \"shua-xin-cha-jian\": \"プラグイン更新\",\n  \"da-kai-cha-jian-mu-lu\": \"プラグインフォルダを開く\",\n  \"an-zhuang-cha-jian\": \"プラグインをインストール\",\n  \"zan-wu-cha-jian\": \"プラグインなし\",\n  \"jiang-cha-jian-wen-jian-jia-fang-ru-cha-jian-mu-lu\": \"プラグインフォルダをディレクトリに入れて、更新ボタンをクリック\",\n  \"kai-ji-zi-qi-dong\": \"自動起動\",\n  \"wang-luo-mo-shi\": \"ネットワークモード\",\n  \"zhu-wang\": \"メインネット\",\n  \"qi-dong-shi-zui-xiao-hua\": \"起動時に最小化\",\n  \"zu-zhi-xi-tong-xiu-mian\": \"スリープを防止\",\n  \"api-mo-shi\": \"APIモード\",\n  \"wang-luo-dai-li\": \"ネットワークプロキシ\",\n  \"zhuang-tai-lan-ge-ci\": \"メニューバー歌詞\",\n  \"ge-ci-fan-yi\": \"歌詞翻訳\",\n  \"dui-qi-fang-shi\": \"配置\",\n  \"ju-zhong\": \"中央\",\n  \"ju-zuo\": \"左\",\n  \"ju-you\": \"右\",\n  \"ping-heng-yin-pin-xiang-du\": \"ラウドネス正規化\",\n  \"shu-ju-yuan\": \"データソース\",\n  \"suo-fang-yin-zi\": \"スケールファクター\",\n  \"tiao-zheng-hou-xu-zhong-qi\": \"調整後は再起動が必要\",\n  \"api-di-zhi\": \"APIアドレス\",\n  \"websocket-di-zhi\": \"WebSocketアドレス\",\n  \"mo-ren-api-ti-shi\": \"これはデフォルトのAPIアドレスです。現在のバージョンではカスタム変更はサポートされていません\",\n  \"dai-li-placeholder\": \"HTTP/HTTPSプロキシアドレスを入力、例：http://127.0.0.1:7890\",\n  \"zheng-zai-ce-shi\": \"テスト中...\",\n  \"ce-shi-lian-jie\": \"接続テスト\",\n  \"bao-cun-she-zhi-an-niu\": \"保存\",\n  \"qing-shu-ru-dai-li-di-zhi\": \"プロキシサーバーアドレスを入力\",\n  \"ce-wang\": \"テストネット\",\n  \"kai-fa-wang\": \"開発ネット\",\n  \"qi-yong\": \"有効\",\n  \"jin-yong\": \"無効\",\n  \"dai-li-di-zhi\": \"プロキシアドレス\",\n  \"gai-nian-ban-xuan-xiang\": \"コンセプト版\",\n  \"zheng-shi-ban\": \"正式版\",\n  \"dai-li-lian-jie-cheng-gong\": \"プロキシ接続成功、IP：\",\n  \"dai-li-lian-jie-shi-bai\": \"プロキシ接続失敗：\",\n  \"lian-jie-chao-shi\": \"接続タイムアウト\",\n  \"lian-jie-cuo-wu\": \"接続エラー：\",\n  \"jin-zhi-chi-mac\": \"（macOSのみ）\",\n  \"xian-shi-yin-cang-zhuo-mian-ge-ci\": \"デスクトップ歌詞を表示/非表示\",\n  \"ni-que-ren-hui-fu-chu-chang\": \"初期設定に戻しますか？この操作は元に戻せません！\",\n  \"bang-zhu\": \"ヘルプ\",\n  \"dian-ji-she-zhi-kuai-jie-jian\": \"クリックしてショートカットを設定\",\n  \"wang-luo-jie-dian\": \"ネットワークノード\",\n  \"zi-ti-wen-jian-di-zhi\": \"フォントファイルURL\",\n  \"jia-zai-zhong\": \"読み込み中...\",\n  \"ban-ben\": \"バージョン\",\n  \"yi-qi-yong\": \"有効\",\n  \"da-kai-tan-chuang\": \"設定\",\n  \"xie-zai\": \"アンインストール\",\n  \"zheng-zai-jia-zai-cha-jian\": \"プラグインを読み込み中...\",\n  \"web-cha-jian-ti-shi\": \"Web版では chrome://extensions/ で拡張機能を管理してください\",\n  \"da-kai-tan-chuang-shi-bai\": \"ポップアップを開けませんでした\",\n  \"que-ren-xie-zai-cha-jian\": \"プラグイン name をアンインストールしますか？\",\n  \"xie-zai-cha-jian-shi-bai\": \"プラグインのアンインストールに失敗しました\",\n  \"xuan-ze-wen-jian-shi-bai\": \"ファイル選択に失敗しました\",\n  \"an-zhuang-cha-jian-shi-bai\": \"プラグインのインストールに失敗しました\",\n  \"an-zhuang-cha-jian-chu-cuo\": \"プラグインのインストール中にエラー\",\n  \"cha-jian-bao\": \"プラグインパッケージ\",\n  \"zhuo-mian-ge-ci\": \"デスクトップ歌詞\",\n  \"bo-fang-su-du\": \"再生速度\",\n  \"wo-xi-huan\": \"お気に入り\",\n  \"shou-cang-zhi\": \"保存先\",\n  \"fen-xiang-ge-qu\": \"共有\",\n  \"qie-huan-dao-yin-yi\": \"ローマ字に切替\",\n  \"qie-huan-dao-fan-yi\": \"翻訳に切替\",\n  \"wei-zhi-cuo-wu\": \"不明なエラー\"\n}\n"
  },
  {
    "path": "src/language/ko.json",
    "content": "{\n  \"tui-jian\": \"추천하다\",\n  \"tui-jian-ge-qu\": \"추천곡\",\n  \"tui-jian-ge-dan\": \"추천 재생목록\",\n  \"fa-xian\": \"발견하다\",\n  \"da\": \"큰\",\n  \"da-kai\": \"열려 있는\",\n  \"gao-yin-zhi-320kbps\": \"고품질 사운드 - 320Kbps\",\n  \"ge-ci\": \"가사\",\n  \"ge-ci-zi-ti-da-xiao\": \"전체 화면 가사 글꼴 크기\",\n  \"guan-bi\": \"폐쇄\",\n  \"guan-bi-an-niu\": \"닫기\",\n  \"kai-qi\": \"켜다\",\n  \"nan-nan-lan\": \"하늘색\",\n  \"pu-tong-yin-zhi\": \"일반 음질 - 128Kbps\",\n  \"qi-dong-wen-hou-yu\": \"인사말 시작\",\n  \"qian-se\": \"연한 색\",\n  \"shao-nv-fen\": \"만나고 핑크\",\n  \"shen-se\": \"어두운\",\n  \"sheng-yin\": \"소리\",\n  \"tou-ding-lv\": \"민트 그린\",\n  \"wai-guan\": \"모습\",\n  \"native-title-bar\":\"네이티브 타이틀 바\",\n  \"xian-shi-ge-ci-bei-jing\": \"전체 스크린 가사 배경 표지 표시\",\n  \"xian-shi-zhuo-mian-ge-ci\": \"데스크톱 가사 표시\",\n  \"xiao\": \"작은\",\n  \"xuan-ze-wai-guan\": \"외관 선택\",\n  \"xuan-ze-yu-yan\": \"언어 선택\",\n  \"xuan-ze-zhu-se-tiao\": \"메인 컬러 선택\",\n  \"yin-zhi-xuan-ze\": \"음질 선택\",\n  \"yu-yan\": \"언어\",\n  \"zhong\": \"가운데\",\n  \"zhu-se-tiao\": \"메인 컬러\",\n  \"zi-dong\": \"오토매틱\",\n  \"0-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"0. 이 프로그램은 Kugou 공식이 아닌 타사 클라이언트입니다. 더 완전한 기능이 필요한 경우 공식 클라이언트를 다운로드하여 사용해 보세요.\",\n  \"1-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"1. 이 프로그램은 Kugou 공식이 아닌 타사 클라이언트입니다. 더 완전한 기능이 필요한 경우 공식 클라이언트를 다운로드하여 사용해 보세요.\",\n  \"1-ben-xiang-mu-jin-gong-xue-xi-shi-yong-qing-zun-zhong-ban-quan-qing-wu-li-yong-ci-xiang-mu-cong-shi-shang-ye-hang-wei-ji-fei-fa-yong-tu\": \"1. 이 프로젝트는 학습 목적으로만 제작되었습니다. 저작권을 존중해 주시고, 상업적인 활동이나 불법적인 목적으로 사용하지 마세요.\",\n  \"2-ben-xiang-mu-jin-gong-xue-xi-jiao-liu-shi-yong-nin-zai-shi-yong-guo-cheng-zhong-ying-zun-zhong-ban-quan-bu-de-yong-yu-shang-ye-huo-fei-fa-yong-tu\": \"2. 본 프로젝트는 학습과 소통을 위한 목적으로만 사용되어야 하며, 상업적 또는 불법적인 목적으로 사용할 수 없습니다.\",\n  \"2-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-chan-sheng-ban-quan-shu-ju-dui-yu-zhe-xie-ban-quan-shu-ju-ben-xiang-mu-bu-yong-you-ta-men-de-suo-you-quan-wei-le-bi-mian-qin-quan-shi-yong-zhe-wu-bi-zai-24-xiao-shi-nei-qing-chu-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-suo-chan-sheng-de-ban-quan-shu-ju\": \"2. 본 프로젝트 이용 과정에서 저작권 데이터가 생성될 수 있습니다. \\n본 프로젝트는 이러한 저작권이 있는 데이터의 소유권을 보유하지 않습니다. \\n침해를 방지하기 위해 사용자는 본 프로젝트 이용 과정에서 생성된 저작권 데이터를 24시간 이내에 삭제해야 합니다.\",\n  \"3-miao-hou-zi-dong-qie-huan-xia-yi-shou\": \"3초 후 자동으로 다음곡으로 전환\",\n  \"3-you-yu-shi-yong-ben-xiang-mu-chan-sheng-de-bao-kuo-you-yu-ben-xie-yi-huo-you-yu-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-er-yin-qi-de-ren-he-xing-zhi-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-sun-hai-bao-kuo-dan-bu-xian-yu-yin-shang-yu-sun-shi-ting-gong-ji-suan-ji-gu-zhang-huo-gu-zhang-yin-qi-de-sun-hai-pei-chang-huo-ren-he-ji-suo-you-qi-ta-shang-ye-sun-hai-huo-sun-shi-you-shi-yong-zhe-fu-ze\": \"3. 본 계약 또는 본 프로젝트의 사용 또는 사용 불가능으로 인해 발생하는 모든 종류의 직간접적, 특별, 우발적 또는 결과적 손해(영업권 상실, 작업 중단, 컴퓨터 오작동 또는 오작동으로 인한 손해를 포함하되 이에 국한되지 않음) , 또는 기타 모든 상업적 손해나 손실)은 사용자의 책임입니다.\",\n  \"3-zai-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-sheng-cheng-ban-quan-nei-rong-ben-xiang-mu-bu-yong-you-zhe-xie-ban-quan-nei-rong-de-suo-you-quan-wei-le-bi-mian-qin-quan-hang-wei-nin-xu-zai-24-xiao-shi-nei-qing-chu-you-ben-xiang-mu-chan-sheng-de-ban-quan-nei-rong\": \"3. 본 프로젝트 이용과정에서 저작권으로 보호되는 콘텐츠가 생성될 수 있습니다. \\n본 프로젝트는 이러한 저작권이 있는 콘텐츠에 대한 소유권을 보유하지 않습니다. \\n침해를 방지하려면 이 프로젝트에서 생성된 저작권이 있는 콘텐츠를 24시간 이내에 제거해야 합니다.\",\n  \"4-ben-xiang-mu-de-kai-fa-zhe-bu-dui-yin-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-suo-dao-zhi-de-ren-he-sun-hai-cheng-dan-ze-ren-bao-kuo-dan-bu-xian-yu-shu-ju-diu-shi-ting-gong-ji-suan-ji-gu-zhang-huo-qi-ta-jing-ji-sun-shi\": \"4. 이 프로젝트의 개발자는 데이터 손실, 다운타임, 컴퓨터 오류 또는 기타 경제적 손실을 포함하되 이에 국한되지 않고 이 프로젝트를 사용하거나 사용할 수 없음으로 인해 발생하는 모든 손해에 대해 책임을 지지 않습니다.\",\n  \"4-jin-zhi-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-dui-yu-shi-yong-zhe-zai-ming-zhi-huo-bu-zhi-dang-di-fa-lv-fa-gui-bu-yun-xu-de-qing-kuang-xia-shi-yong-ben-xiang-mu-suo-zao-cheng-de-ren-he-wei-fa-wei-gui-hang-wei-you-shi-yong-zhe-cheng-dan-ben-xiang-mu-bu-cheng-dan-you-ci-zao-cheng-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-ze-ren\": \"4. 본 프로젝트를 현지 법규에 위반하여 이용하는 것은 금지되어 있습니다. \\n현지 법률 및 규정이 본 프로젝트의 사용을 허용하지 않는다는 사실을 사용자가 고의로 또는 알지 못하여 발생한 불법 행위에 대한 책임은 사용자에게 있습니다. 본 프로젝트는 이로 인해 발생하는 직접적, 간접적, 특수적, 우발적 또는 결과적 책임을 지지 않습니다. .\",\n  \"5-nin-bu-de-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-yin-wei-fan-fa-lv-fa-gui-suo-dao-zhi-de-ren-he-fa-lv-hou-guo-you-yong-hu-cheng-dan\": \"5. 현지 법규를 위반하여 본 프로젝트를 이용할 수 없습니다. \\n법령 위반으로 인해 발생하는 모든 법적 책임은 이용자에게 있습니다.\",\n  \"5-yin-le-ping-tai-bu-yi-qing-zun-zhong-ban-quan-zhi-chi-zheng-ban\": \"5. 뮤직 플랫폼은 쉽지 않습니다. 저작권을 존중하고 정품을 지원해주세요.\",\n  \"6-ben-xiang-mu-jin-yong-yu-dui-ji-shu-ke-hang-xing-de-tan-suo-ji-yan-jiu-bu-jie-shou-ren-he-shang-ye-bao-kuo-dan-bu-xian-yu-guang-gao-deng-he-zuo-ji-juan-zeng\": \"6. 본 프로젝트는 기술적 타당성 탐색 및 연구 목적으로만 사용되며 어떠한 상업적(광고 등을 포함하되 이에 국한되지 않음) 협력 및 기부를 받지 않습니다.\",\n  \"6-ben-xiang-mu-jin-yong-yu-ji-shu-tan-suo-he-yan-jiu-bu-jie-shou-ren-he-shang-ye-he-zuo-guang-gao-huo-juan-zeng-ru-guo-guan-fang-yin-le-ping-tai-dui-ci-xiang-mu-cun-you-yi-lv-ke-sui-shi-lian-xi-kai-fa-zhe-yi-chu-xiang-guan-nei-rong\": \"6. 본 프로젝트는 기술 탐구 및 연구 목적으로만 사용되며 어떠한 상업적 협력, 광고 또는 기부도 받지 않습니다. \\n공식 뮤직 플랫폼이 본 프로젝트에 대해 우려하는 경우 언제든지 개발자에게 연락하여 관련 콘텐츠를 삭제할 수 있습니다.\",\n  \"7-ru-guo-guan-fang-yin-le-ping-tai-jue-de-ben-xiang-mu-bu-tuo-ke-lian-xi-ben-xiang-mu-geng-gai-huo-yi-chu\": \"7. 공식 음원 플랫폼에서 본 프로젝트가 부적절하다고 판단하는 경우 본 프로젝트에 연락하여 변경 또는 삭제를 요청할 수 있습니다.\",\n  \"bo-fang\": \"놀다\",\n  \"bo-fang-lie-biao\": \"재생목록\",\n  \"bu-tong-yi\": \"맞지 않다\",\n  \"chang-ting-ban\": \"무료 듣기 버전:\",\n  \"dan-qu-xun-huan\": \"단일 루프\",\n  \"de-yin-le-ku\": \"님의 음악 라이브러리\",\n  \"deng-lu\": \"로그인\",\n  \"deng-lu-cheng-gong\": \"로그인 성공\",\n  \"deng-lu-ni-de-ku-gou-zhang-hao\": \"Kugou 계정에 로그인하세요\",\n  \"deng-lu-shi-bai\": \"로그인 실패\",\n  \"deng-lu-shi-xiao-qing-zhong-xin-deng-lu\": \"로그인에 실패했습니다. 다시 로그인해 주세요.\",\n  \"di\": \"아니요.\",\n  \"er-wei-ma\": \"QR 코드\",\n  \"er-wei-ma-deng-lu-cheng-gong\": \"QR코드 로그인 성공\",\n  \"er-wei-ma-jian-ce-shi-bai\": \"QR 코드 감지 실패\",\n  \"er-wei-ma-sheng-cheng-shi-bai\": \"QR 코드 생성 실패\",\n  \"er-wei-ma-yi-guo-qi-qing-zhong-xin-sheng-cheng\": \"QR코드가 만료되었습니다. 다시 생성해주세요.\",\n  \"fa-song-yan-zheng-ma\": \"인증코드 보내기\",\n  \"gai-nian-ban\": \"컨셉 버전:\",\n  \"ge-qu-lie-biao\": \"노래 목록\",\n  \"geng-xin\": \"고쳐 쓰다\",\n  \"gong\": \"흔한\",\n  \"guan-yu\": \"~에 대한\",\n  \"huo-qu-er-wei-ma-shi-bai\": \"QR 코드를 가져오지 못했습니다.\",\n  \"huo-qu-ge-ci-shi-bai\": \"가사를 가져오지 못했습니다.\",\n  \"huo-qu-ge-ci-zhong\": \"가사 가져오는 중...\",\n  \"huo-qu-ge-dan-shi-bai\": \"재생목록을 가져오지 못했습니다.\",\n  \"huo-qu-yin-le-di-zhi-shi-bai\": \"음악 주소를 가져오지 못했습니다.\",\n  \"huo-qu-yin-le-shi-bai\": \"음악을 가져오지 못했습니다.\",\n  \"li-ji-deng-lu\": \"지금 로그인하세요\",\n  \"lie-biao-xun-huan\": \"목록 루프\",\n  \"login-tips\": \"MoeKoe은 귀하의 계정 정보를 클라우드에 저장하지 않을 것을 약속합니다. \\n귀하의 비밀번호는 로컬로 암호화된 후 Kugou 공식에게 전송됩니다. \\nMengyin은 Kugou의 공식 웹사이트가 아닙니다. 계정 정보를 입력하기 전에 신중하게 고려하시기 바랍니다. 모든 계정이 계정 비밀번호 로그인을 지원하는 것은 아닙니다.\",\n  \"mian-ze-sheng-ming\": \"부인 성명\",\n  \"ni-huan-mei-you-tian-jia-ge-quo-kuai-qu-tian-jia-ba\": \"아직 노래를 추가하지 않았습니다. 계속해서 추가하세요!\",\n  \"qing-shi-yong-ku-gou-sao-miao-er-wei-ma-deng-lu\": \"로그인하려면 Kugou를 사용하여 QR 코드를 스캔하세요.\",\n  \"qing-shu-ru-deng-lu-you-xiang\": \"로그인 계정을 입력하십시오\",\n  \"qing-shu-ru-mi-ma\": \"비밀번호를 입력해주세요\",\n  \"qing-shu-ru-shou-ji-hao\": \"휴대폰번호를 입력해주세요\",\n  \"qing-shu-ru-shou-ji-hao-ma\": \"휴대폰번호를 입력해주세요\",\n  \"qing-shu-ru-yan-zheng-ma\": \"인증번호를 입력해주세요\",\n  \"qing-shu-ru-you-xiang\": \"계정 번호를 입력하십시오\",\n  \"qu-xiao\": \"취소\",\n  \"que-ding\": \"확신하는\",\n  \"sao-ma-deng-lu\": \"로그인하려면 코드를 스캔하세요\",\n  \"shang-yi-ye\": \"이전 페이지\",\n  \"shao-nv-qi-dao-zhong\": \"소녀는 기도하고 있다....\",\n  \"she-zhi\": \"설정\",\n  \"shi-yong-yan-zheng-ma-deng-lu\": \"인증코드를 사용하여 로그인하세요.\",\n  \"shou-ge\": \"노래\",\n  \"shou-ji-hao-deng-lu\": \"휴대폰번호 로그인\",\n  \"shou-ji-hao-ge-shi-cuo-wu\": \"휴대폰 번호 형식 오류\",\n  \"shou-ye\": \"첫 페이지\",\n  \"shun-xu-bo-fang\": \"순차적으로 재생\",\n  \"sou-suo-jie-guo\": \"검색결과\",\n  \"sou-suo-yin-le-ge-shou-ge-dan\": \"음악, 가수, 재생목록 검색, 코드 공유...\",\n  \"sui-ji-bo-fang\": \"혼합\",\n  \"tong-yi\": \"동의하다\",\n  \"tong-yi-ji-xu-shi-yong-ben-xiang-mu-nin-ji-jie-shou-yi-shang-tiao-kuan-sheng-ming-nei-rong\": \"이 프로젝트를 계속 사용하는 데 동의하면 위의 이용 약관에 동의하는 것입니다.\",\n  \"tui-chu\": \"그만두다\",\n  \"wo-chuang-jian-de-ge-dan\": \"내가 만든 재생목록\",\n  \"wo-guan-zhu-de-ge-shou\": \"내가 팔로우하는 가수\",\n  \"wo-shou-cang-de-ge-dan\": \"내 재생목록 모음\",\n  \"wo-xi-huan-ting\": \"나는 듣는 것을 좋아한다\",\n  \"wu-xiang-ying-shu-ju\": \"응답 데이터 없음\",\n  \"xia-yi-ye\": \"다음 페이지\",\n  \"yan-zheng-ma-fa-song-shi-bai\": \"인증코드 전송 실패\",\n  \"yan-zheng-ma-fa-song-shi-bai-0\": \"인증코드 전송에 실패했습니다.\",\n  \"yan-zheng-ma-yi-fa-song\": \"인증 코드가 전송되었습니다\",\n  \"ye\": \"페이지\",\n  \"yi-sao-ma-deng-dai-que-ren\": \"스캔된 코드, 확인 대기 중\",\n  \"yin-le-ku\": \"음악 도서관\",\n  \"yong-hu\": \"사용자\",\n  \"yong-hu-tiao-kuan\": \"사용자 약관\",\n  \"yong-hu-tou-xiang\": \"사용자 아바타\",\n  \"you-xiang-deng-lu\": \"계정 로그인\",\n  \"you-xiang-ge-shi-cuo-wu\": \"이메일 형식 오류\",\n  \"zan-wu-ge-ci\": \"아직 가사가 없습니다\",\n  \"deng-lu-shi-bai-0\": \"로그인에 실패했습니다.\",\n  \"zheng-zai-sheng-cheng-er-wei-ma\": \"QR 코드 생성 중...\",\n  \"zhe-li-shi-mo-du-mei-you\": \"여기에는 아무것도 없습니다\",\n  \"wu-sun-yin-zhi-1104kbps\": \"무손실 음질 - 1104kbps\",\n  \"gao-pin-zhi-yin-le-xu-yao-deng-lu-hou-cai-neng-bo-fango\": \"고음질 음악은 로그인이 필요합니다~\",\n  \"tian-jia-ge-dan\": \"재생목록에 추가\",\n  \"qing-xian-deng-lu\": \"먼저 로그인해주세요\",\n  \"cheng-gong-tian-jia-dao-ge-dan\": \"재생목록에 추가되었습니다!\",\n  \"tian-jia-dao-ge-dan-shi-bai\": \"재생목록에 추가하지 못했습니다!\",\n  \"cheng-gong-qu-xiao-shou-cang\": \"즐겨찾기 취소\",\n  \"qu-xiao-shou-cang-shi-bai\": \"취소 실패\",\n  \"ni-que-ren-yao-tui-chu-deng-lu-ma\": \"정말로 로그아웃하시겠습니까?\",\n  \"gai-ge-qu-zan-wu-ban-quan\": \"이 곡에는 저작권이 없습니다.\",\n  \"wo-shou-cang-de-zhuan-ji\": \"내가 컬렉션한 앨범\",\n  \"bo-fang-shi-bai\": \"재생 실패\",\n  \"wo-guan-zhu-de-hao-you\": \"내가 팔로우하는 친구들\",\n  \"tian-jia-cheng-gong\": \"성공적으로 추가되었습니다\",\n  \"chuang-jian-ge-dan\": \"재생목록 만들기\",\n  \"qing-shu-ru-xin-de-ge-dan-ming-cheng\": \"새 재생목록 이름을 입력하세요.\",\n  \"chuang-jian-shi-bai\": \"생성 실패\",\n  \"que-ren-shan-chu-ge-dan\": \"재생목록을 삭제하시겠습니까?\",\n  \"shou-cang-shi-bai\": \"수집 실패\",\n  \"shou-cang-cheng-gong\": \"수집 성공\",\n  \"wo-guan-zhu-de-yi-ren\": \"내가 팔로우하는 음악가\",\n  \"sou-suo-ge-qu\": \"노래 검색...\",\n  \"fan-hui-ding-bu\": \"맨 위로 돌아가기\",\n  \"dang-qian-bo-fang-ge-qu\": \"현재 재생 중인 노래\",\n  \"yi-fu-zhi-fen-xiang-ma-qing-zai-moekoe-ke-hu-duan-zhong-fang-wen\": \"공유 코드가 복사되었습니다. MoeKoe 클라이언트에서 확인하세요.\",\n  \"ge-qu-shu-ju-cuo-wu\": \"재생목록이 존재하지 않습니다\",\n  \"bao-cun\": \"구하다\",\n  \"bao-cun-she-zhi-shi-bai\": \"설정이 실패했습니다\",\n  \"cun-zai-wu-xiao-de-kuai-jie-jian-she-zhi-qing-que-bao-mei-ge-kuai-jie-jian-du-bao-han-xiu-shi-jian\": \"잘못된 단축키 설정이 있습니다. 각 단축키에 수정자 키가 포함되어 있는지 확인하세요.\",\n  \"de-kuai-jie-jian-chong-tu\": \"바로 가기 키 충돌\",\n  \"fei-ke-hu-duan-huan-jing-wu-fa-qi-yong\": \"비클라이언트 환경, 활성화할 수 없음\",\n  \"gai-kuai-jie-jian-yu\": \"똥 키와\",\n  \"jing-yin\": \"무음\",\n  \"kuai-jie-jian-bi-xu-bao-han-zhi-shao-yi-ge-xiu-shi-jian-ctrlaltshiftcommand\": \"바로 가기 키에는 하나 이상의 수정 자 (ctrl/alt/shift/command)가 포함되어야합니다.\",\n  \"kuai-jie-jian-she-zhi\": \"단축키 설정\",\n  \"qing-an-xia-qi-ta-jian\": \"[다른 키를 누르십시오]\",\n  \"qing-an-xia-xiu-shi-jian\": \"수정자 키를 눌러주세요\",\n  \"quan-ju-kuai-jie-jian\": \"글로벌 바로 가기 키\",\n  \"shang-yi-shou\": \"이전 노래\",\n  \"tui-chu-zhu-cheng-xu\": \"메인 프로그램 종료\",\n  \"xi-tong\": \"체계\",\n  \"xia-yi-shou\": \"다음\",\n  \"xian-shi-yin-cang-zhu-chuang-kou\": \"기본 창을 표시/숨기십시오\",\n  \"yin-liang-jian-xiao\": \"감소하다\",\n  \"yin-liang-zeng-jia\": \"볼륨 증가\",\n  \"zan-ting-bo-fang\": \"일시 정지/놀이\",\n  \"zi-ding-yi-kuai-jie-jian\": \"맞춤형 단축키 키\",\n  \"mei-ri-tui-jian\": \"매일 추천\",\n  \"mei-you-zheng-zai-bo-fang-de-ge-qu\": \"연주하는 노래는 없습니다\",\n  \"shou-cang-dao\": \"모으다\",\n  \"mei-you-ge-dan\": \"노래 목록이 없습니다\",\n  \"jin-yong-gpu-jia-su-zhong-qi-sheng-xiao\": \"GPU 가속도를 비활성화합니다\",\n  \"jie-mian\": \"인터페이스\",\n  \"guan-bi-shi-minimize-to-tray\": \"창을 닫을 때 트레이로 이동하십시오\",\n  \"mi-gan-cheng\": \"미안 오렌지\",\n  \"zhu-ce\": \"아직 계정이 없습니까?\",\n  \"xin-zeng-zhang-hao-qing-xian-zai-guan-fang-ke-hu-duan-zhong-deng-lu-yi-ci\": \"새 계정을 위해 공식 고객에게 로그인하십시오.\",\n  \"shua-xin-hou-sheng-xiao\": \"(새로 고침 후 효과)\",\n  \"zhong-qi-hou-sheng-xiao\": \"(재시작 후 효과)\",\n  \"shi-pei-gao-dpi\": \"고DPI 지원 활성화\",\n  \"kui-she-chao-qing-yin-zhi\": \"Viper Ultra-Definition 음질\",\n  \"hires-yin-zhi\": \"고해상도 음질\",\n  \"tian-jia-wo-xi-huan\": \"내가 좋아하는 것에 추가하십시오\",\n  \"qie-huan-bo-fang-mo-shi\": \"재생 모드를 전환합니다\",\n  \"pwa-app\": \"PWA 응용 프로그램\",\n  \"install\": \"설치\",\n  \"yin-pin-jia-zai-shi-bai\": \"음원 로드 실패\",\n  \"zheng-zai-jia-zai-quan-bu-ge-qu\": \"모든 노래를 로드하는 중...\",\n  \"bo-fang-chu-cuo\": \"재생 오류\",\n  \"bo-fang-shi-bai-qu-mu-wei-kong\": \"재생 실패, 곡 목록이 비어 있습니다\",\n  \"shan-chu-cheng-gong\": \"삭제 완료\",\n  \"tian-jia-dao-bo-fang-lie-biao-cheng-gong\": \"재생 목록에 추가됨\",\n  \"hot\": \"인기\",\n  \"new\": \"최신\",\n  \"yun-pan-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"클라우드 음악은 재생목록에 추가할 수 없습니다\",\n  \"xian-qu-kan-kan-ni-de-shou-cang-jia-ba\": \"즐겨찾기를 확인하세요\",\n  \"yi-da-dao-zui-da-chong-shi-ci-shu\": \"최대 재시도 횟수에 도달했습니다. 직접 곡을 선택하세요\",\n  \"hui-fu-chu-chang-she-zhi-cheng-gong\": \"초기 설정으로 복원되었습니다. 앱을 다시 시작하세요\",\n  \"liu-lan-qi-bu-zhi-chi-file-system-api\": \"브라우저가 File System API를 지원하지 않습니다. Chrome 86+ 또는 Edge 86+를 사용하세요\",\n  \"cha-jian-an-zhuang-cheng-gong\": \"플러그인 설치 완료\",\n  \"zhi-chi-http-https-dai-li\": \"HTTP/HTTPS 프록시만 지원\",\n  \"ben-di-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"로컬 음악은 재생목록에 추가할 수 없습니다\",\n  \"mei-you-xuan-ze-zheng-que-de-ge-qu\": \"곡이 선택되지 않았습니다\",\n  \"zhuang-tai-lan-ge-ci-jin-zhi-chi-mac\": \"메뉴바 가사는 macOS만 지원\",\n  \"qian-dao-shi-bai\": \"체크인 실패. 너무 자주 체크인하지 마세요\",\n  \"huo-qu-vip-shi-bai\": \"VIP 획득 실패. 하루에 한 번만 가능\",\n  \"qing-zai-web-huan-jing-xia-an-zhuang\": \"웹 버전에서 설치하세요\",\n  \"qing-shu-ru-you-xiao-de-url\": \"유효한 URL을 입력하세요\",\n  \"zhe-shi-yi-ge-alert\": \"알림\",\n  \"fei-mac-bu-zhi-chi-touchbar\": \"TouchBar는 Mac에서만 지원\",\n  \"zi-ti-she-zhi\": \"글꼴 설정\",\n  \"mo-ren-zi-ti\": \"기본 글꼴\",\n  \"hui-fu-chu-chang-she-zhi\": \"초기 설정으로 복원\",\n  \"zi-ti-url-di-zhi\": \"글꼴 URL\",\n  \"qing-shu-ru-zi-ti-url-di-zhi\": \"글꼴 URL 입력\",\n  \"zi-ti-ming-cheng\": \"글꼴 이름\",\n  \"qing-shu-ru-zi-ti-ming-cheng\": \"글꼴 이름 입력\",\n  \"cha-jian\": \"플러그인\",\n  \"shua-xin-cha-jian\": \"플러그인 새로고침\",\n  \"da-kai-cha-jian-mu-lu\": \"플러그인 폴더 열기\",\n  \"an-zhuang-cha-jian\": \"플러그인 설치\",\n  \"zan-wu-cha-jian\": \"플러그인 없음\",\n  \"jiang-cha-jian-wen-jian-jia-fang-ru-cha-jian-mu-lu\": \"플러그인 폴더를 디렉토리에 넣고 새로고침 버튼을 클릭하세요\",\n  \"kai-ji-zi-qi-dong\": \"자동 시작\",\n  \"wang-luo-mo-shi\": \"네트워크 모드\",\n  \"zhu-wang\": \"메인넷\",\n  \"qi-dong-shi-zui-xiao-hua\": \"시작 시 최소화\",\n  \"zu-zhi-xi-tong-xiu-mian\": \"절전 모드 방지\",\n  \"api-mo-shi\": \"API 모드\",\n  \"wang-luo-dai-li\": \"네트워크 프록시\",\n  \"zhuang-tai-lan-ge-ci\": \"메뉴바 가사\",\n  \"ge-ci-fan-yi\": \"가사 번역\",\n  \"dui-qi-fang-shi\": \"정렬\",\n  \"ju-zhong\": \"가운데\",\n  \"ju-zuo\": \"왼쪽\",\n  \"ju-you\": \"오른쪽\",\n  \"ping-heng-yin-pin-xiang-du\": \"음량 정규화\",\n  \"shu-ju-yuan\": \"데이터 소스\",\n  \"suo-fang-yin-zi\": \"배율\",\n  \"tiao-zheng-hou-xu-zhong-qi\": \"조정 후 재시작 필요\",\n  \"api-di-zhi\": \"API 주소\",\n  \"websocket-di-zhi\": \"WebSocket 주소\",\n  \"mo-ren-api-ti-shi\": \"기본 API 주소입니다. 현재 버전에서는 사용자 정의 변경을 지원하지 않습니다\",\n  \"dai-li-placeholder\": \"HTTP/HTTPS 프록시 주소 입력, 예: http://127.0.0.1:7890\",\n  \"zheng-zai-ce-shi\": \"테스트 중...\",\n  \"ce-shi-lian-jie\": \"연결 테스트\",\n  \"bao-cun-she-zhi-an-niu\": \"저장\",\n  \"qing-shu-ru-dai-li-di-zhi\": \"프록시 서버 주소 입력\",\n  \"ce-wang\": \"테스트넷\",\n  \"kai-fa-wang\": \"개발넷\",\n  \"qi-yong\": \"활성화\",\n  \"jin-yong\": \"비활성화\",\n  \"dai-li-di-zhi\": \"프록시 주소\",\n  \"gai-nian-ban-xuan-xiang\": \"컨셉 버전\",\n  \"zheng-shi-ban\": \"정식 버전\",\n  \"dai-li-lian-jie-cheng-gong\": \"프록시 연결 성공, IP: \",\n  \"dai-li-lian-jie-shi-bai\": \"프록시 연결 실패: \",\n  \"lian-jie-chao-shi\": \"연결 시간 초과\",\n  \"lian-jie-cuo-wu\": \"연결 오류: \",\n  \"jin-zhi-chi-mac\": \" (macOS만 지원)\",\n  \"xian-shi-yin-cang-zhuo-mian-ge-ci\": \"데스크톱 가사 표시/숨기기\",\n  \"ni-que-ren-hui-fu-chu-chang\": \"초기 설정으로 복원하시겠습니까? 이 작업은 되돌릴 수 없습니다!\",\n  \"bang-zhu\": \"도움말\",\n  \"dian-ji-she-zhi-kuai-jie-jian\": \"클릭하여 단축키 설정\",\n  \"wang-luo-jie-dian\": \"네트워크 노드\",\n  \"zi-ti-wen-jian-di-zhi\": \"글꼴 파일 URL\",\n  \"jia-zai-zhong\": \"로딩 중...\",\n  \"ban-ben\": \"버전\",\n  \"yi-qi-yong\": \"활성화됨\",\n  \"da-kai-tan-chuang\": \"설정\",\n  \"xie-zai\": \"제거\",\n  \"zheng-zai-jia-zai-cha-jian\": \"플러그인 로딩 중...\",\n  \"web-cha-jian-ti-shi\": \"웹 버전에서는 chrome://extensions/에서 확장 프로그램을 관리하세요\",\n  \"da-kai-tan-chuang-shi-bai\": \"팝업을 열 수 없습니다\",\n  \"que-ren-xie-zai-cha-jian\": \"플러그인 name 을(를) 제거하시겠습니까?\",\n  \"xie-zai-cha-jian-shi-bai\": \"플러그인 제거 실패\",\n  \"xuan-ze-wen-jian-shi-bai\": \"파일 선택 실패\",\n  \"an-zhuang-cha-jian-shi-bai\": \"플러그인 설치 실패\",\n  \"an-zhuang-cha-jian-chu-cuo\": \"플러그인 설치 중 오류\",\n  \"cha-jian-bao\": \"플러그인 패키지\",\n  \"zhuo-mian-ge-ci\": \"데스크톱 가사\",\n  \"bo-fang-su-du\": \"재생 속도\",\n  \"wo-xi-huan\": \"좋아요\",\n  \"shou-cang-zhi\": \"저장 위치\",\n  \"fen-xiang-ge-qu\": \"공유\",\n  \"qie-huan-dao-yin-yi\": \"로마자로 전환\",\n  \"qie-huan-dao-fan-yi\": \"번역으로 전환\",\n  \"wei-zhi-cuo-wu\": \"알 수 없는 오류\"\n}\n"
  },
  {
    "path": "src/language/ru.json",
    "content": "{\n  \"tui-jian\": \"Рекомендации\",\n  \"tui-jian-ge-qu\": \"Рекомендуемые треки\",\n  \"tui-jian-ge-dan\": \"Рекомендуемые плейлисты\",\n  \"fa-xian\": \"Обзор\",\n  \"yu-yan\": \"Язык\",\n  \"zhu-se-tiao\": \"Цветовая тема\",\n  \"wai-guan\": \"Оформление\",\n  \"native-title-bar\": \"Системный заголовок окна\",\n  \"sheng-yin\": \"Звук\",\n  \"yin-zhi-xuan-ze\": \"Качество звука\",\n  \"qi-dong-wen-hou-yu\": \"Приветствие при запуске\",\n  \"ge-ci\": \"Тексты песен\",\n  \"xian-shi-ge-ci-bei-jing\": \"Показывать обложку на фоне текста\",\n  \"xian-shi-zhuo-mian-ge-ci\": \"Показывать текст на рабочем столе\",\n  \"ge-ci-zi-ti-da-xiao\": \"Размер шрифта текста\",\n  \"guan-bi\": \"Нет\",\n  \"guan-bi-an-niu\": \"Закрыть\",\n  \"shao-nv-fen\": \"Розовый\",\n  \"qian-se\": \"Светлая\",\n  \"pu-tong-yin-zhi\": \"Обычное качество — 128 Кбит/с\",\n  \"da-kai\": \"Да\",\n  \"zhong\": \"Средний\",\n  \"kai-qi\": \"Да\",\n  \"xuan-ze-yu-yan\": \"Выбор языка\",\n  \"xuan-ze-zhu-se-tiao\": \"Выбор цветовой темы\",\n  \"nan-nan-lan\": \"Небесно-голубой\",\n  \"tou-ding-lv\": \"Мятный\",\n  \"xuan-ze-wai-guan\": \"Выбор оформления\",\n  \"zi-dong\": \"Авто\",\n  \"shen-se\": \"Тёмная\",\n  \"gao-yin-zhi-320kbps\": \"Высокое качество — 320 Кбит/с\",\n  \"xiao\": \"Маленький\",\n  \"da\": \"Большой\",\n  \"sou-suo-jie-guo\": \"Результаты поиска\",\n  \"shang-yi-ye\": \"Назад\",\n  \"xia-yi-ye\": \"Вперёд\",\n  \"di\": \"Стр.\",\n  \"ye\": \"\",\n  \"gong\": \"из\",\n  \"bo-fang\": \"Воспроизвести\",\n  \"ge-qu-lie-biao\": \"Список треков\",\n  \"deng-lu-ni-de-ku-gou-zhang-hao\": \"Войдите в аккаунт Kugou\",\n  \"qing-shu-ru-shou-ji-hao\": \"Введите номер телефона\",\n  \"qing-shu-ru-yan-zheng-ma\": \"Введите код подтверждения\",\n  \"fa-song-yan-zheng-ma\": \"Отправить код\",\n  \"li-ji-deng-lu\": \"Войти\",\n  \"qing-shu-ru-deng-lu-you-xiang\": \"Введите логин\",\n  \"qing-shu-ru-mi-ma\": \"Введите пароль\",\n  \"you-xiang-deng-lu\": \"Вход по логину\",\n  \"er-wei-ma\": \"QR-код\",\n  \"login-tips\": \"MoeKoe не сохраняет данные вашего аккаунта на сервере. Пароль шифруется локально перед отправкой на серверы Kugou. MoeKoe не является официальным сайтом Kugou — вводите данные на свой страх и риск. Не все аккаунты поддерживают вход по паролю.\",\n  \"shi-yong-yan-zheng-ma-deng-lu\": \"Войти по коду из SMS\",\n  \"shou-ji-hao-deng-lu\": \"Вход по телефону\",\n  \"sao-ma-deng-lu\": \"Вход по QR-коду\",\n  \"qing-shu-ru-shou-ji-hao-ma\": \"Введите номер телефона\",\n  \"shou-ji-hao-ge-shi-cuo-wu\": \"Неверный формат номера\",\n  \"qing-shu-ru-you-xiang\": \"Введите логин\",\n  \"you-xiang-ge-shi-cuo-wu\": \"Неверный формат email\",\n  \"qing-shi-yong-ku-gou-sao-miao-er-wei-ma-deng-lu\": \"Отсканируйте QR-код в приложении Kugou\",\n  \"deng-lu-cheng-gong\": \"Вход выполнен\",\n  \"deng-lu-shi-bai\": \"Ошибка входа\",\n  \"yan-zheng-ma-fa-song-shi-bai\": \"Не удалось отправить код\",\n  \"yan-zheng-ma-yi-fa-song\": \"Код отправлен\",\n  \"wu-xiang-ying-shu-ju\": \"Нет ответа от сервера\",\n  \"huo-qu-er-wei-ma-shi-bai\": \"Не удалось получить QR-код\",\n  \"er-wei-ma-sheng-cheng-shi-bai\": \"Не удалось сгенерировать QR-код\",\n  \"er-wei-ma-deng-lu-cheng-gong\": \"Вход по QR-коду выполнен\",\n  \"er-wei-ma-yi-guo-qi-qing-zhong-xin-sheng-cheng\": \"QR-код устарел, сгенерируйте новый\",\n  \"er-wei-ma-jian-ce-shi-bai\": \"Ошибка проверки QR-кода\",\n  \"deng-lu-shi-bai-0\": \"Ошибка входа: \",\n  \"yan-zheng-ma-fa-song-shi-bai-0\": \"Не удалось отправить код: \",\n  \"yong-hu\": \"Пользователь\",\n  \"yi-sao-ma-deng-dai-que-ren\": \"QR-код отсканирован, подтвердите вход\",\n  \"yong-hu-tou-xiang\": \"Аватар\",\n  \"de-yin-le-ku\": \" — Музыкальная библиотека\",\n  \"wo-xi-huan-ting\": \"Мне нравится\",\n  \"wo-chuang-jian-de-ge-dan\": \"Мои плейлисты\",\n  \"wo-shou-cang-de-ge-dan\": \"Сохранённые плейлисты\",\n  \"wo-guan-zhu-de-ge-shou\": \"Мои исполнители\",\n  \"deng-lu-shi-xiao-qing-zhong-xin-deng-lu\": \"Сессия истекла, войдите снова\",\n  \"gai-nian-ban\": \"Концептуальная версия:\",\n  \"chang-ting-ban\": \"Стандартная версия:\",\n  \"shou-ge\": \"треков\",\n  \"shou-ye\": \"Главная\",\n  \"yin-le-ku\": \"Библиотека\",\n  \"sou-suo-yin-le-ge-shou-ge-dan\": \"Поиск музыки, исполнителей, плейлистов...\",\n  \"she-zhi\": \"Настройки\",\n  \"tui-chu\": \"Выход\",\n  \"deng-lu\": \"Вход\",\n  \"geng-xin\": \"Обновление\",\n  \"guan-yu\": \"О программе\",\n  \"mian-ze-sheng-ming\": \"Отказ от ответственности\",\n  \"0-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"0. Это неофициальный клиент Kugou. Для полного функционала используйте официальное приложение.\",\n  \"1-ben-xiang-mu-jin-gong-xue-xi-shi-yong-qing-zun-zhong-ban-quan-qing-wu-li-yong-ci-xiang-mu-cong-shi-shang-ye-hang-wei-ji-fei-fa-yong-tu\": \"1. Проект предназначен только для обучения. Уважайте авторские права и не используйте его в коммерческих или незаконных целях.\",\n  \"2-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-chan-sheng-ban-quan-shu-ju-dui-yu-zhe-xie-ban-quan-shu-ju-ben-xiang-mu-bu-yong-you-ta-men-de-suo-you-quan-wei-le-bi-mian-qin-quan-shi-yong-zhe-wu-bi-zai-24-xiao-shi-nei-qing-chu-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-suo-chan-sheng-de-ban-quan-shu-ju\": \"2. При использовании могут создаваться данные, защищённые авторским правом. Проект не владеет этими данными. Во избежание нарушений удалите их в течение 24 часов.\",\n  \"3-you-yu-shi-yong-ben-xiang-mu-chan-sheng-de-bao-kuo-you-yu-ben-xie-yi-huo-you-yu-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-er-yin-qi-de-ren-he-xing-zhi-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-sun-hai-bao-kuo-dan-bu-xian-yu-yin-shang-yu-sun-shi-ting-gong-ji-suan-ji-gu-zhang-huo-gu-zhang-yin-qi-de-sun-hai-pei-chang-huo-ren-he-ji-suo-you-qi-ta-shang-ye-sun-hai-huo-sun-shi-you-shi-yong-zhe-fu-ze\": \"3. Пользователь несёт ответственность за любой прямой или косвенный ущерб, связанный с использованием или невозможностью использования проекта.\",\n  \"4-jin-zhi-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-dui-yu-shi-yong-zhe-zai-ming-zhi-huo-bu-zhi-dang-di-fa-lv-fa-gui-bu-yun-xu-de-qing-kuang-xia-shi-yong-ben-xiang-mu-suo-zao-cheng-de-ren-he-wei-fa-wei-gui-hang-wei-you-shi-yong-zhe-cheng-dan-ben-xiang-mu-bu-cheng-dan-you-ci-zao-cheng-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-ze-ren\": \"4. Запрещено использовать проект в нарушение местного законодательства. Ответственность за нарушения несёт пользователь.\",\n  \"5-yin-le-ping-tai-bu-yi-qing-zun-zhong-ban-quan-zhi-chi-zheng-ban\": \"5. Поддержите правообладателей — слушайте музыку легально.\",\n  \"6-ben-xiang-mu-jin-yong-yu-dui-ji-shu-ke-hang-xing-de-tan-suo-ji-yan-jiu-bu-jie-shou-ren-he-shang-ye-bao-kuo-dan-bu-xian-yu-guang-gao-deng-he-zuo-ji-juan-zeng\": \"6. Проект предназначен только для технических исследований. Коммерческое сотрудничество и пожертвования не принимаются.\",\n  \"7-ru-guo-guan-fang-yin-le-ping-tai-jue-de-ben-xiang-mu-bu-tuo-ke-lian-xi-ben-xiang-mu-geng-gai-huo-yi-chu\": \"7. Если правообладатели имеют претензии, они могут связаться с разработчиками для изменения или удаления проекта.\",\n  \"yong-hu-tiao-kuan\": \"Пользовательское соглашение\",\n  \"1-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"1. Это неофициальный клиент Kugou. Для полного функционала используйте официальное приложение.\",\n  \"2-ben-xiang-mu-jin-gong-xue-xi-jiao-liu-shi-yong-nin-zai-shi-yong-guo-cheng-zhong-ying-zun-zhong-ban-quan-bu-de-yong-yu-shang-ye-huo-fei-fa-yong-tu\": \"2. Проект предназначен для обучения. Уважайте авторские права и не используйте его в коммерческих или незаконных целях.\",\n  \"3-zai-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-sheng-cheng-ban-quan-nei-rong-ben-xiang-mu-bu-yong-you-zhe-xie-ban-quan-nei-rong-de-suo-you-quan-wei-le-bi-mian-qin-quan-hang-wei-nin-xu-zai-24-xiao-shi-nei-qing-chu-you-ben-xiang-mu-chan-sheng-de-ban-quan-nei-rong\": \"3. При использовании могут создаваться данные, защищённые авторским правом. Удалите их в течение 24 часов.\",\n  \"4-ben-xiang-mu-de-kai-fa-zhe-bu-dui-yin-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-suo-dao-zhi-de-ren-he-sun-hai-cheng-dan-ze-ren-bao-kuo-dan-bu-xian-yu-shu-ju-diu-shi-ting-gong-ji-suan-ji-gu-zhang-huo-qi-ta-jing-ji-sun-shi\": \"4. Разработчики не несут ответственности за любой ущерб от использования проекта.\",\n  \"5-nin-bu-de-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-yin-wei-fan-fa-lv-fa-gui-suo-dao-zhi-de-ren-he-fa-lv-hou-guo-you-yong-hu-cheng-dan\": \"5. Запрещено использовать проект в нарушение законодательства. Ответственность несёт пользователь.\",\n  \"6-ben-xiang-mu-jin-yong-yu-ji-shu-tan-suo-he-yan-jiu-bu-jie-shou-ren-he-shang-ye-he-zuo-guang-gao-huo-juan-zeng-ru-guo-guan-fang-yin-le-ping-tai-dui-ci-xiang-mu-cun-you-yi-lv-ke-sui-shi-lian-xi-kai-fa-zhe-yi-chu-xiang-guan-nei-rong\": \"6. Проект предназначен для технических исследований. Коммерческое сотрудничество не принимается. Правообладатели могут связаться с разработчиками.\",\n  \"tong-yi-ji-xu-shi-yong-ben-xiang-mu-nin-ji-jie-shou-yi-shang-tiao-kuan-sheng-ming-nei-rong\": \"Продолжая использовать проект, вы принимаете условия соглашения.\",\n  \"tong-yi\": \"Принять\",\n  \"bu-tong-yi\": \"Отклонить\",\n  \"que-ding\": \"ОК\",\n  \"qu-xiao\": \"Отмена\",\n  \"shao-nv-qi-dao-zhong\": \"Загрузка...\",\n  \"zan-wu-ge-ci\": \"Текст недоступен\",\n  \"ni-huan-mei-you-tian-jia-ge-quo-kuai-qu-tian-jia-ba\": \"Здесь пока пусто. Добавьте треки!\",\n  \"huo-qu-ge-dan-shi-bai\": \"Не удалось загрузить плейлист\",\n  \"huo-qu-yin-le-shi-bai\": \"Не удалось загрузить музыку\",\n  \"3-miao-hou-zi-dong-qie-huan-xia-yi-shou\": \"Следующий трек через 3 сек.\",\n  \"huo-qu-yin-le-di-zhi-shi-bai\": \"Не удалось получить ссылку на трек\",\n  \"huo-qu-ge-ci-zhong\": \"Загрузка текста...\",\n  \"huo-qu-ge-ci-shi-bai\": \"Не удалось загрузить текст\",\n  \"dan-qu-xun-huan\": \"Повтор трека\",\n  \"lie-biao-xun-huan\": \"Повтор плейлиста\",\n  \"shun-xu-bo-fang\": \"По порядку\",\n  \"sui-ji-bo-fang\": \"Случайный порядок\",\n  \"bo-fang-lie-biao\": \"Очередь воспроизведения\",\n  \"zheng-zai-sheng-cheng-er-wei-ma\": \"Генерация QR-кода...\",\n  \"zhe-li-shi-mo-du-mei-you\": \"Здесь пока ничего нет\",\n  \"wu-sun-yin-zhi-1104kbps\": \"Lossless — 1104 Кбит/с\",\n  \"gao-pin-zhi-yin-le-xu-yao-deng-lu-hou-cai-neng-bo-fango\": \"Для высокого качества войдите в аккаунт\",\n  \"tian-jia-ge-dan\": \"Добавить в плейлист\",\n  \"qing-xian-deng-lu\": \"Сначала войдите в аккаунт\",\n  \"cheng-gong-tian-jia-dao-ge-dan\": \"Добавлено в плейлист\",\n  \"tian-jia-dao-ge-dan-shi-bai\": \"Не удалось добавить в плейлист\",\n  \"cheng-gong-qu-xiao-shou-cang\": \"Удалено из избранного\",\n  \"qu-xiao-shou-cang-shi-bai\": \"Не удалось удалить\",\n  \"ni-que-ren-yao-tui-chu-deng-lu-ma\": \"Выйти из аккаунта?\",\n  \"gai-ge-qu-zan-wu-ban-quan\": \"Трек недоступен (нет лицензии)\",\n  \"wo-shou-cang-de-zhuan-ji\": \"Сохранённые альбомы\",\n  \"bo-fang-shi-bai\": \"Ошибка воспроизведения\",\n  \"wo-guan-zhu-de-hao-you\": \"Мои друзья\",\n  \"tian-jia-cheng-gong\": \"Добавлено\",\n  \"chuang-jian-ge-dan\": \"Создать плейлист\",\n  \"qing-shu-ru-xin-de-ge-dan-ming-cheng\": \"Введите название плейлиста\",\n  \"chuang-jian-shi-bai\": \"Не удалось создать\",\n  \"que-ren-shan-chu-ge-dan\": \"Удалить плейлист?\",\n  \"shou-cang-cheng-gong\": \"Добавлено в избранное\",\n  \"shou-cang-shi-bai\": \"Не удалось добавить в избранное\",\n  \"wo-guan-zhu-de-yi-ren\": \"Мои музыканты\",\n  \"sou-suo-ge-qu\": \"Поиск треков...\",\n  \"dang-qian-bo-fang-ge-qu\": \"Сейчас играет\",\n  \"fan-hui-ding-bu\": \"Наверх\",\n  \"yi-fu-zhi-fen-xiang-ma-qing-zai-moekoe-ke-hu-duan-zhong-fang-wen\": \"Код скопирован. Откройте его в MoeKoe\",\n  \"kou-ling-yi-fu-zhi,kuai-ba-ge-qu-fen-xiang-gei-peng-you-ba\": \"Скопировано! Поделитесь с друзьями\",\n  \"ge-qu-shu-ju-cuo-wu\": \"Плейлист не найден\",\n  \"xi-tong\": \"Система\",\n  \"quan-ju-kuai-jie-jian\": \"Глобальные горячие клавиши\",\n  \"zi-ding-yi-kuai-jie-jian\": \"Настроить горячие клавиши\",\n  \"kuai-jie-jian-she-zhi\": \"Горячие клавиши\",\n  \"xian-shi-yin-cang-zhu-chuang-kou\": \"Показать/скрыть окно\",\n  \"tui-chu-zhu-cheng-xu\": \"Выход из программы\",\n  \"shang-yi-shou\": \"Предыдущий трек\",\n  \"xia-yi-shou\": \"Следующий трек\",\n  \"zan-ting-bo-fang\": \"Пауза/воспроизведение\",\n  \"yin-liang-zeng-jia\": \"Громче\",\n  \"yin-liang-jian-xiao\": \"Тише\",\n  \"jing-yin\": \"Без звука\",\n  \"bao-cun\": \"Сохранить\",\n  \"fei-ke-hu-duan-huan-jing-wu-fa-qi-yong\": \"Недоступно в веб-версии\",\n  \"qing-an-xia-xiu-shi-jian\": \"Нажмите модификатор\",\n  \"qing-an-xia-qi-ta-jian\": \"+ [нажмите клавишу]\",\n  \"kuai-jie-jian-bi-xu-bao-han-zhi-shao-yi-ge-xiu-shi-jian-ctrlaltshiftcommand\": \"Комбинация должна содержать Ctrl/Alt/Shift/Command\",\n  \"gai-kuai-jie-jian-yu\": \"Эта комбинация конфликтует с \",\n  \"de-kuai-jie-jian-chong-tu\": \"\",\n  \"cun-zai-wu-xiao-de-kuai-jie-jian-she-zhi-qing-que-bao-mei-ge-kuai-jie-jian-du-bao-han-xiu-shi-jian\": \"Некорректная комбинация. Добавьте модификатор\",\n  \"bao-cun-she-zhi-shi-bai\": \"Не удалось сохранить настройки\",\n  \"mei-ri-tui-jian\": \"Рекомендации дня\",\n  \"mei-you-zheng-zai-bo-fang-de-ge-qu\": \"Ничего не воспроизводится\",\n  \"shou-cang-dao\": \"Сохранить в\",\n  \"mei-you-ge-dan\": \"Нет плейлистов\",\n  \"jin-yong-gpu-jia-su-zhong-qi-sheng-xiao\": \"Отключить GPU-ускорение\",\n  \"jie-mian\": \"Интерфейс\",\n  \"guan-bi-shi-minimize-to-tray\": \"Сворачивать в трей при закрытии\",\n  \"mi-gan-cheng\": \"Оранжевый\",\n  \"zhu-ce\": \"Нет аккаунта?\",\n  \"xin-zeng-zhang-hao-qing-xian-zai-guan-fang-ke-hu-duan-zhong-deng-lu-yi-ci\": \"Новый аккаунт? Сначала войдите в официальном приложении\",\n  \"shua-xin-hou-sheng-xiao\": \"(после перезагрузки страницы)\",\n  \"zhong-qi-hou-sheng-xiao\": \"(после перезапуска)\",\n  \"shi-pei-gao-dpi\": \"Поддержка высокого DPI\",\n  \"hires-yin-zhi\": \"Hi-Res\",\n  \"kui-she-chao-qing-yin-zhi\": \"Viper Ultra HD\",\n  \"tian-jia-wo-xi-huan\": \"Добавить в «Мне нравится»\",\n  \"qie-huan-bo-fang-mo-shi\": \"Сменить режим воспроизведения\",\n  \"ke-yong\": \"Доступно\",\n  \"wo-de-yun-pan\": \"Моё облако\",\n  \"yun-pan-ge-qu-shu\": \"Треков в облаке\",\n  \"yun-pan-miao-shu\": \"Здесь хранится загруженная вами музыка\",\n  \"yun-pan-ge-qu\": \"Облачные треки\",\n  \"cong-yun-pan-shan-chu\": \"Удалить из облака\",\n  \"que-ren-shan-chu-yun-pan-ge-qu\": \"Удалить выбранные треки из облака?\",\n  \"shang-chuan-yin-le\": \"Загрузить\",\n  \"pi-liang-cao-zuo\": \"Выбрать несколько\",\n  \"pwa-app\": \"PWA-приложение\",\n  \"install\": \"Установить\",\n  \"yin-pin-jia-zai-shi-bai\": \"Не удалось загрузить аудио\",\n  \"guan-zhu\": \"Подписки\",\n  \"fen-si\": \"Подписчики\",\n  \"hao-you\": \"Друзья\",\n  \"fang-wen\": \"Просмотры\",\n  \"qian-dao\": \"Отметиться\",\n  \"xiao-shi\": \"ч.\",\n  \"fen-zhong\": \"мин.\",\n  \"le-ling\": \"Стаж\",\n  \"nian\": \"г.\",\n  \"ting-ge-shi-chang\": \"Время прослушивания\",\n  \"zheng-zai-jia-zai-quan-bu-ge-qu\": \"Загрузка всех треков...\",\n  \"bo-fang-chu-cuo\": \"Ошибка воспроизведения\",\n  \"bo-fang-shi-bai-qu-mu-wei-kong\": \"Список воспроизведения пуст\",\n  \"shan-chu-cheng-gong\": \"Удалено\",\n  \"tian-jia-dao-bo-fang-lie-biao-cheng-gong\": \"Добавлено в очередь\",\n  \"hot\": \"Популярное\",\n  \"new\": \"Новинки\",\n  \"yun-pan-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"Облачные треки нельзя добавить в плейлист\",\n  \"xian-qu-kan-kan-ni-de-shou-cang-jia-ba\": \"Сначала загляните в избранное\",\n  \"yi-da-dao-zui-da-chong-shi-ci-shu\": \"Превышено число попыток, выберите трек вручную\",\n  \"hui-fu-chu-chang-she-zhi-cheng-gong\": \"Настройки сброшены. Перезапустите приложение\",\n  \"liu-lan-qi-bu-zhi-chi-file-system-api\": \"Браузер не поддерживает File System API. Используйте Chrome 86+ или Edge 86+\",\n  \"cha-jian-an-zhuang-cheng-gong\": \"Плагин установлен\",\n  \"zhi-chi-http-https-dai-li\": \"Поддерживаются только HTTP/HTTPS прокси\",\n  \"ben-di-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"Локальные треки нельзя добавить в плейлист\",\n  \"mei-you-xuan-ze-zheng-que-de-ge-qu\": \"Трек не выбран\",\n  \"zhuang-tai-lan-ge-ci-jin-zhi-chi-mac\": \"Текст в строке меню только для macOS\",\n  \"qian-dao-shi-bai\": \"Ошибка отметки. Не отмечайтесь слишком часто\",\n  \"huo-qu-vip-shi-bai\": \"Не удалось получить VIP. Доступно раз в день\",\n  \"qing-zai-web-huan-jing-xia-an-zhuang\": \"Установите в веб-версии\",\n  \"qing-shu-ru-you-xiao-de-url\": \"Введите корректный URL\",\n  \"zhe-shi-yi-ge-alert\": \"Уведомление\",\n  \"fei-mac-bu-zhi-chi-touchbar\": \"TouchBar доступен только на Mac\",\n  \"zi-ti-she-zhi\": \"Настройки шрифта\",\n  \"mo-ren-zi-ti\": \"Шрифт по умолчанию\",\n  \"hui-fu-chu-chang-she-zhi\": \"Сбросить настройки\",\n  \"zi-ti-url-di-zhi\": \"URL шрифта\",\n  \"qing-shu-ru-zi-ti-url-di-zhi\": \"Введите URL шрифта\",\n  \"zi-ti-ming-cheng\": \"Название шрифта\",\n  \"qing-shu-ru-zi-ti-ming-cheng\": \"Введите название шрифта\",\n  \"cha-jian\": \"Плагины\",\n  \"shua-xin-cha-jian\": \"Обновить\",\n  \"da-kai-cha-jian-mu-lu\": \"Открыть папку\",\n  \"an-zhuang-cha-jian\": \"Установить\",\n  \"zan-wu-cha-jian\": \"Нет плагинов\",\n  \"jiang-cha-jian-wen-jian-jia-fang-ru-cha-jian-mu-lu\": \"Поместите папку плагина в каталог и нажмите «Обновить»\",\n  \"kai-ji-zi-qi-dong\": \"Автозапуск\",\n  \"wang-luo-mo-shi\": \"Сетевой режим\",\n  \"zhu-wang\": \"Основная сеть\",\n  \"qi-dong-shi-zui-xiao-hua\": \"Сворачивать при запуске\",\n  \"zu-zhi-xi-tong-xiu-mian\": \"Предотвращать спящий режим\",\n  \"api-mo-shi\": \"Режим API\",\n  \"wang-luo-dai-li\": \"Сетевой прокси\",\n  \"zhuang-tai-lan-ge-ci\": \"Текст в строке меню\",\n  \"ge-ci-fan-yi\": \"Перевод текста\",\n  \"dui-qi-fang-shi\": \"Выравнивание\",\n  \"ju-zhong\": \"По центру\",\n  \"ju-zuo\": \"По левому краю\",\n  \"ju-you\": \"По правому краю\",\n  \"ping-heng-yin-pin-xiang-du\": \"Нормализация громкости\",\n  \"shu-ju-yuan\": \"Источник данных\",\n  \"suo-fang-yin-zi\": \"Масштаб\",\n  \"tiao-zheng-hou-xu-zhong-qi\": \"Изменение вступит в силу после перезапуска\",\n  \"api-di-zhi\": \"Адрес API\",\n  \"websocket-di-zhi\": \"Адрес WebSocket\",\n  \"mo-ren-api-ti-shi\": \"Это адреса API по умолчанию. Изменение не поддерживается в текущей версии\",\n  \"dai-li-placeholder\": \"Введите адрес HTTP/HTTPS прокси, например: http://127.0.0.1:7890\",\n  \"zheng-zai-ce-shi\": \"Проверка...\",\n  \"ce-shi-lian-jie\": \"Проверить\",\n  \"bao-cun-she-zhi-an-niu\": \"Сохранить\",\n  \"qing-shu-ru-dai-li-di-zhi\": \"Введите адрес прокси-сервера\",\n  \"ce-wang\": \"Тестовая сеть\",\n  \"kai-fa-wang\": \"Сеть разработки\",\n  \"qi-yong\": \"Включено\",\n  \"jin-yong\": \"Выключено\",\n  \"dai-li-di-zhi\": \"Адрес прокси\",\n  \"gai-nian-ban-xuan-xiang\": \"Концептуальная\",\n  \"zheng-shi-ban\": \"Официальная\",\n  \"dai-li-lian-jie-cheng-gong\": \"Прокси подключён, IP: \",\n  \"dai-li-lian-jie-shi-bai\": \"Ошибка подключения к прокси: \",\n  \"lian-jie-chao-shi\": \"Таймаут подключения\",\n  \"lian-jie-cuo-wu\": \"Ошибка подключения: \",\n  \"jin-zhi-chi-mac\": \" (только macOS)\",\n  \"xian-shi-yin-cang-zhuo-mian-ge-ci\": \"Показать/скрыть текст на рабочем столе\",\n  \"ni-que-ren-hui-fu-chu-chang\": \"Сбросить все настройки? Это действие необратимо!\",\n  \"bang-zhu\": \"Справка\",\n  \"dian-ji-she-zhi-kuai-jie-jian\": \"Нажмите для настройки\",\n  \"wang-luo-jie-dian\": \"Сетевой узел\",\n  \"zi-ti-wen-jian-di-zhi\": \"URL файла шрифта\",\n  \"jia-zai-zhong\": \"Загрузка...\",\n  \"ban-ben\": \"Версия\",\n  \"yi-qi-yong\": \"Включён\",\n  \"da-kai-tan-chuang\": \"Настройки\",\n  \"xie-zai\": \"Удалить\",\n  \"zheng-zai-jia-zai-cha-jian\": \"Загрузка плагинов...\",\n  \"web-cha-jian-ti-shi\": \"В веб-версии управляйте расширениями через chrome://extensions/\",\n  \"da-kai-tan-chuang-shi-bai\": \"Не удалось открыть окно плагина\",\n  \"que-ren-xie-zai-cha-jian\": \"Удалить плагин name?\",\n  \"xie-zai-cha-jian-shi-bai\": \"Не удалось удалить плагин\",\n  \"xuan-ze-wen-jian-shi-bai\": \"Не удалось выбрать файл\",\n  \"an-zhuang-cha-jian-shi-bai\": \"Не удалось установить плагин\",\n  \"an-zhuang-cha-jian-chu-cuo\": \"Ошибка при установке плагина\",\n  \"cha-jian-bao\": \"Архив плагина\",\n  \"zhuo-mian-ge-ci\": \"Текст на рабочем столе\",\n  \"bo-fang-su-du\": \"Скорость воспроизведения\",\n  \"wo-xi-huan\": \"Мне нравится\",\n  \"shou-cang-zhi\": \"Сохранить в\",\n  \"fen-xiang-ge-qu\": \"Поделиться\",\n  \"qie-huan-dao-yin-yi\": \"Показать транслитерацию\",\n  \"qie-huan-dao-fan-yi\": \"Показать перевод\",\n  \"wei-zhi-cuo-wu\": \"Неизвестная ошибка\"\n}\n"
  },
  {
    "path": "src/language/zh-CN.json",
    "content": "{\n  \"tui-jian\": \"推荐\",\n  \"tui-jian-ge-qu\": \"推荐歌曲\",\n  \"tui-jian-ge-dan\": \"推荐歌单\",\n  \"fa-xian\": \"发现\",\n  \"yu-yan\": \"语言\",\n  \"zhu-se-tiao\": \"主色调\",\n  \"wai-guan\": \"外观\",\n  \"native-title-bar\":\"原生窗口装饰器\",\n  \"sheng-yin\": \"声音\",\n  \"yin-zhi-xuan-ze\": \"音质选择\",\n  \"qi-dong-wen-hou-yu\": \"启动问候语\",\n  \"ge-ci\": \"歌词\",\n  \"xian-shi-ge-ci-bei-jing\": \"显示全屏歌词背景封面\",\n  \"xian-shi-zhuo-mian-ge-ci\": \"显示桌面歌词\",\n  \"ge-ci-zi-ti-da-xiao\": \"全屏歌词字体大小\",\n  \"guan-bi\": \"关闭\",\n  \"guan-bi-an-niu\": \"关闭\",\n  \"shao-nv-fen\": \"少女粉\",\n  \"qian-se\": \"浅色\",\n  \"pu-tong-yin-zhi\": \"普通音质 - 128Kbps\",\n  \"da-kai\": \"打开\",\n  \"zhong\": \"中\",\n  \"kai-qi\": \"开启\",\n  \"xuan-ze-yu-yan\": \"选择语言\",\n  \"xuan-ze-zhu-se-tiao\": \"选择主色调\",\n  \"nan-nan-lan\": \"天空蓝\",\n  \"tou-ding-lv\": \"薄荷绿\",\n  \"xuan-ze-wai-guan\": \"选择外观\",\n  \"zi-dong\": \"自动\",\n  \"shen-se\": \"深色\",\n  \"gao-yin-zhi-320kbps\": \"高品音质 - 320Kbps\",\n  \"xiao\": \"小\",\n  \"da\": \"大\",\n  \"sou-suo-jie-guo\": \"搜索结果\",\n  \"shang-yi-ye\": \"上一页\",\n  \"xia-yi-ye\": \"下一页\",\n  \"di\": \"第\",\n  \"ye\": \"页\",\n  \"gong\": \"共\",\n  \"bo-fang\": \"播放\",\n  \"ge-qu-lie-biao\": \"歌曲列表\",\n  \"deng-lu-ni-de-ku-gou-zhang-hao\": \"登录你的酷狗账号\",\n  \"qing-shu-ru-shou-ji-hao\": \"请输入手机号\",\n  \"qing-shu-ru-yan-zheng-ma\": \"请输入验证码\",\n  \"fa-song-yan-zheng-ma\": \"发送验证码\",\n  \"li-ji-deng-lu\": \"立即登录\",\n  \"qing-shu-ru-deng-lu-you-xiang\": \"请输入登录账号\",\n  \"qing-shu-ru-mi-ma\": \"请输入密码\",\n  \"you-xiang-deng-lu\": \"账号登录\",\n  \"er-wei-ma\": \"二维码\",\n  \"login-tips\": \"萌音 承诺不会保存你的任何账号信息到云端。你的密码会在本地进行加密后再传输到酷狗官方。萌音并非酷狗官方网站，输入账号信息前请慎重考虑,非所有账号都支持账号密码登录.\",\n  \"shi-yong-yan-zheng-ma-deng-lu\": \"使用验证码登录.\",\n  \"shou-ji-hao-deng-lu\": \"手机号登录\",\n  \"sao-ma-deng-lu\": \"扫码登录\",\n  \"qing-shu-ru-shou-ji-hao-ma\": \"请输入手机号码\",\n  \"shou-ji-hao-ge-shi-cuo-wu\": \"手机号格式错误\",\n  \"qing-shu-ru-you-xiang\": \"请输入账号\",\n  \"you-xiang-ge-shi-cuo-wu\": \"邮箱格式错误\",\n  \"qing-shi-yong-ku-gou-sao-miao-er-wei-ma-deng-lu\": \"请使用酷狗扫描二维码登录\",\n  \"deng-lu-cheng-gong\": \"登录成功\",\n  \"deng-lu-shi-bai\": \"登录失败\",\n  \"yan-zheng-ma-fa-song-shi-bai\": \"验证码发送失败\",\n  \"yan-zheng-ma-yi-fa-song\": \"验证码已发送\",\n  \"wu-xiang-ying-shu-ju\": \"无响应数据\",\n  \"huo-qu-er-wei-ma-shi-bai\": \"获取二维码失败\",\n  \"er-wei-ma-sheng-cheng-shi-bai\": \"二维码生成失败\",\n  \"er-wei-ma-deng-lu-cheng-gong\": \"二维码登录成功\",\n  \"er-wei-ma-yi-guo-qi-qing-zhong-xin-sheng-cheng\": \"二维码已过期，请重新生成\",\n  \"er-wei-ma-jian-ce-shi-bai\": \"二维码检测失败\",\n  \"deng-lu-shi-bai-0\": \"登录失败，\",\n  \"yan-zheng-ma-fa-song-shi-bai-0\": \"验证码发送失败，\",\n  \"yong-hu\": \"用户\",\n  \"yi-sao-ma-deng-dai-que-ren\": \"已扫码,等待确认\",\n  \"yong-hu-tou-xiang\": \"用户头像\",\n  \"de-yin-le-ku\": \"的音乐库\",\n  \"wo-xi-huan-ting\": \"我喜欢听\",\n  \"wo-chuang-jian-de-ge-dan\": \"我创建的歌单\",\n  \"wo-shou-cang-de-ge-dan\": \"我收藏的歌单\",\n  \"wo-guan-zhu-de-ge-shou\": \"我关注的歌手\",\n  \"deng-lu-shi-xiao-qing-zhong-xin-deng-lu\": \"登录失效,请重新登录\",\n  \"gai-nian-ban\": \"概念版:\",\n  \"chang-ting-ban\": \"畅听版:\",\n  \"shou-ge\": \"首歌\",\n  \"shou-ye\": \"首页\",\n  \"yin-le-ku\": \"音乐库\",\n  \"sou-suo-yin-le-ge-shou-ge-dan\": \"搜索音乐、歌手、歌单、分享码...\",\n  \"she-zhi\": \"设置\",\n  \"tui-chu\": \"退出\",\n  \"deng-lu\": \"登录\",\n  \"geng-xin\": \"更新\",\n  \"guan-yu\": \"关于\",\n  \"mian-ze-sheng-ming\": \"免责声明\",\n  \"0-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"0. 本程序是酷狗第三方客户端，并非酷狗官方，需要更完善的功能请下载官方客户端体验.\",\n  \"1-ben-xiang-mu-jin-gong-xue-xi-shi-yong-qing-zun-zhong-ban-quan-qing-wu-li-yong-ci-xiang-mu-cong-shi-shang-ye-hang-wei-ji-fei-fa-yong-tu\": \"1. 本项目仅供学习使用，请尊重版权，请勿利用此项目从事商业行为及非法用途！\",\n  \"2-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-chan-sheng-ban-quan-shu-ju-dui-yu-zhe-xie-ban-quan-shu-ju-ben-xiang-mu-bu-yong-you-ta-men-de-suo-you-quan-wei-le-bi-mian-qin-quan-shi-yong-zhe-wu-bi-zai-24-xiao-shi-nei-qing-chu-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-suo-chan-sheng-de-ban-quan-shu-ju\": \"2. 使用本项目的过程中可能会产生版权数据。对于这些版权数据，本项目不拥有它们的所有权。为了避免侵权，使用者务必在 24 小时内清除使用本项目的过程中所产生的版权数据。\",\n  \"3-you-yu-shi-yong-ben-xiang-mu-chan-sheng-de-bao-kuo-you-yu-ben-xie-yi-huo-you-yu-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-er-yin-qi-de-ren-he-xing-zhi-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-sun-hai-bao-kuo-dan-bu-xian-yu-yin-shang-yu-sun-shi-ting-gong-ji-suan-ji-gu-zhang-huo-gu-zhang-yin-qi-de-sun-hai-pei-chang-huo-ren-he-ji-suo-you-qi-ta-shang-ye-sun-hai-huo-sun-shi-you-shi-yong-zhe-fu-ze\": \"3.由于使用本项目产生的包括由于本协议或由于使用或无法使用本项目而引起的任何性质的任何直接、间接、特殊、偶然或结果性损害（包括但不限于因商誉损失、停工、计算机故障或故障引起的损害赔偿，或任何及所有其他商业损害或损失）由使用者负责。\",\n  \"4-jin-zhi-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-dui-yu-shi-yong-zhe-zai-ming-zhi-huo-bu-zhi-dang-di-fa-lv-fa-gui-bu-yun-xu-de-qing-kuang-xia-shi-yong-ben-xiang-mu-suo-zao-cheng-de-ren-he-wei-fa-wei-gui-hang-wei-you-shi-yong-zhe-cheng-dan-ben-xiang-mu-bu-cheng-dan-you-ci-zao-cheng-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-ze-ren\": \"4. 禁止在违反当地法律法规的情况下使用本项目。对于使用者在明知或不知当地法律法规不允许的情况下使用本项目所造成的任何违法违规行为由使用者承担，本项目不承担由此造成的任何直接、间接、特殊、偶然或结果性责任。\",\n  \"5-yin-le-ping-tai-bu-yi-qing-zun-zhong-ban-quan-zhi-chi-zheng-ban\": \"5. 音乐平台不易，请尊重版权，支持正版。\",\n  \"6-ben-xiang-mu-jin-yong-yu-dui-ji-shu-ke-hang-xing-de-tan-suo-ji-yan-jiu-bu-jie-shou-ren-he-shang-ye-bao-kuo-dan-bu-xian-yu-guang-gao-deng-he-zuo-ji-juan-zeng\": \"6. 本项目仅用于对技术可行性的探索及研究，不接受任何商业（包括但不限于广告等）合作及捐赠。\",\n  \"7-ru-guo-guan-fang-yin-le-ping-tai-jue-de-ben-xiang-mu-bu-tuo-ke-lian-xi-ben-xiang-mu-geng-gai-huo-yi-chu\": \"7. 如果官方音乐平台觉得本项目不妥，可联系本项目更改或移除。\",\n  \"yong-hu-tiao-kuan\": \"用户条款\",\n  \"1-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"1. 本程序是酷狗第三方客户端，并非酷狗官方，需要更完善的功能请下载官方客户端体验.\",\n  \"2-ben-xiang-mu-jin-gong-xue-xi-jiao-liu-shi-yong-nin-zai-shi-yong-guo-cheng-zhong-ying-zun-zhong-ban-quan-bu-de-yong-yu-shang-ye-huo-fei-fa-yong-tu\": \"2. 本项目仅供学习交流使用，您在使用过程中应尊重版权，不得用于商业或非法用途。\",\n  \"3-zai-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-sheng-cheng-ban-quan-nei-rong-ben-xiang-mu-bu-yong-you-zhe-xie-ban-quan-nei-rong-de-suo-you-quan-wei-le-bi-mian-qin-quan-hang-wei-nin-xu-zai-24-xiao-shi-nei-qing-chu-you-ben-xiang-mu-chan-sheng-de-ban-quan-nei-rong\": \"3. 在使用本项目的过程中，可能会生成版权内容。本项目不拥有这些版权内容的所有权。为了避免侵权行为，您需在 24 小时内清除由本项目产生的版权内容。\",\n  \"4-ben-xiang-mu-de-kai-fa-zhe-bu-dui-yin-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-suo-dao-zhi-de-ren-he-sun-hai-cheng-dan-ze-ren-bao-kuo-dan-bu-xian-yu-shu-ju-diu-shi-ting-gong-ji-suan-ji-gu-zhang-huo-qi-ta-jing-ji-sun-shi\": \"4. 本项目的开发者不对因使用或无法使用本项目所导致的任何损害承担责任，包括但不限于数据丢失、停工、计算机故障或其他经济损失。\",\n  \"5-nin-bu-de-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-yin-wei-fan-fa-lv-fa-gui-suo-dao-zhi-de-ren-he-fa-lv-hou-guo-you-yong-hu-cheng-dan\": \"5. 您不得在违反当地法律法规的情况下使用本项目。因违反法律法规所导致的任何法律后果由用户承担。\",\n  \"6-ben-xiang-mu-jin-yong-yu-ji-shu-tan-suo-he-yan-jiu-bu-jie-shou-ren-he-shang-ye-he-zuo-guang-gao-huo-juan-zeng-ru-guo-guan-fang-yin-le-ping-tai-dui-ci-xiang-mu-cun-you-yi-lv-ke-sui-shi-lian-xi-kai-fa-zhe-yi-chu-xiang-guan-nei-rong\": \"6. 本项目仅用于技术探索和研究，不接受任何商业合作、广告或捐赠。如果官方音乐平台对此项目存有疑虑，可随时联系开发者移除相关内容。\",\n  \"tong-yi-ji-xu-shi-yong-ben-xiang-mu-nin-ji-jie-shou-yi-shang-tiao-kuan-sheng-ming-nei-rong\": \"同意继续使用本项目，您即接受以上条款声明内容。\",\n  \"tong-yi\": \"同意\",\n  \"bu-tong-yi\": \"不同意\",\n  \"que-ding\": \"确定\",\n  \"qu-xiao\": \"取消\",\n  \"shao-nv-qi-dao-zhong\": \"少女祈祷中....\",\n  \"zan-wu-ge-ci\": \"暂无歌词\",\n  \"ni-huan-mei-you-tian-jia-ge-quo-kuai-qu-tian-jia-ba\": \"你还没有添加歌曲哦，快去添加吧！\",\n  \"huo-qu-ge-dan-shi-bai\": \"获取歌单失败\",\n  \"huo-qu-yin-le-shi-bai\": \"获取音乐失败\",\n  \"3-miao-hou-zi-dong-qie-huan-xia-yi-shou\": \"3秒后自动切换下一首\",\n  \"huo-qu-yin-le-di-zhi-shi-bai\": \"获取音乐地址失败\",\n  \"huo-qu-ge-ci-zhong\": \"获取歌词中...\",\n  \"huo-qu-ge-ci-shi-bai\": \"获取歌词失败\",\n  \"dan-qu-xun-huan\": \"单曲循环\",\n  \"lie-biao-xun-huan\": \"列表循环\",\n  \"shun-xu-bo-fang\": \"顺序播放\",\n  \"sui-ji-bo-fang\": \"随机播放\",\n  \"bo-fang-lie-biao\": \"播放列表\",\n  \"zheng-zai-sheng-cheng-er-wei-ma\": \"正在生成二维码...\",\n  \"zhe-li-shi-mo-du-mei-you\": \"这里什么都没有\",\n  \"wu-sun-yin-zhi-1104kbps\": \"无损音质 - 1104kbps\",\n  \"gao-pin-zhi-yin-le-xu-yao-deng-lu-hou-cai-neng-bo-fango\": \"高品质音乐需要登录后才能播放哦~\",\n  \"tian-jia-ge-dan\": \"添加到歌单\",\n  \"qing-xian-deng-lu\": \"请先登录\",\n  \"cheng-gong-tian-jia-dao-ge-dan\": \"成功添加到歌单！\",\n  \"tian-jia-dao-ge-dan-shi-bai\": \"添加到歌单失败！\",\n  \"cheng-gong-qu-xiao-shou-cang\": \"取消收藏\",\n  \"qu-xiao-shou-cang-shi-bai\": \"取消失败\",\n  \"ni-que-ren-yao-tui-chu-deng-lu-ma\": \"你确认要退出登录吗?\",\n  \"gai-ge-qu-zan-wu-ban-quan\": \"该歌曲暂无版权\",\n  \"wo-shou-cang-de-zhuan-ji\": \"我收藏的专辑\",\n  \"bo-fang-shi-bai\": \"播放失败\",\n  \"wo-guan-zhu-de-hao-you\": \"我关注的好友\",\n  \"tian-jia-cheng-gong\": \"添加成功\",\n  \"chuang-jian-ge-dan\": \"创建歌单\",\n  \"qing-shu-ru-xin-de-ge-dan-ming-cheng\": \"请输入新的歌单名称\",\n  \"chuang-jian-shi-bai\": \"创建失败\",\n  \"que-ren-shan-chu-ge-dan\": \"确认删除歌单？\",\n  \"shou-cang-cheng-gong\": \"收藏成功\",\n  \"shou-cang-shi-bai\": \"收藏失败\",\n  \"wo-guan-zhu-de-yi-ren\": \"我关注的音乐人\",\n  \"sou-suo-ge-qu\": \"搜索歌曲...\",\n  \"dang-qian-bo-fang-ge-qu\": \"当前播放歌曲\",\n  \"fan-hui-ding-bu\": \"返回顶部\",\n  \"yi-fu-zhi-fen-xiang-ma-qing-zai-moekoe-ke-hu-duan-zhong-fang-wen\": \"已复制分享码，请在MoeKoe客户端中查看\",\n  \"kou-ling-yi-fu-zhi,kuai-ba-ge-qu-fen-xiang-gei-peng-you-ba\": \"已复制，快把歌曲分享给朋友吧~\",\n  \"ge-qu-shu-ju-cuo-wu\": \"歌单不存在\",\n  \"xi-tong\": \"系统\",\n  \"quan-ju-kuai-jie-jian\": \"全局快捷键\",\n  \"zi-ding-yi-kuai-jie-jian\": \"自定义快捷键\",\n  \"kuai-jie-jian-she-zhi\": \"快捷键设置\",\n  \"xian-shi-yin-cang-zhu-chuang-kou\": \"显示/隐藏主窗口\",\n  \"tui-chu-zhu-cheng-xu\": \"退出主程序\",\n  \"shang-yi-shou\": \"上一首\",\n  \"xia-yi-shou\": \"下一首\",\n  \"zan-ting-bo-fang\": \"暂停/播放\",\n  \"yin-liang-zeng-jia\": \"音量增加\",\n  \"yin-liang-jian-xiao\": \"音量减小\",\n  \"jing-yin\": \"静音\",\n  \"bao-cun\": \"保存\",\n  \"fei-ke-hu-duan-huan-jing-wu-fa-qi-yong\": \"非客户端环境，无法启用\",\n  \"qing-an-xia-xiu-shi-jian\": \"请按下修饰键\",\n  \"qing-an-xia-qi-ta-jian\": \"+ [请按下其他键]\",\n  \"kuai-jie-jian-bi-xu-bao-han-zhi-shao-yi-ge-xiu-shi-jian-ctrlaltshiftcommand\": \"快捷键必须包含至少一个修饰键(Ctrl/Alt/Shift/Command)\",\n  \"gai-kuai-jie-jian-yu\": \"该快捷键与\",\n  \"de-kuai-jie-jian-chong-tu\": \"的快捷键冲突\",\n  \"cun-zai-wu-xiao-de-kuai-jie-jian-she-zhi-qing-que-bao-mei-ge-kuai-jie-jian-du-bao-han-xiu-shi-jian\": \"存在无效的快捷键设置，请确保每个快捷键都包含修饰键\",\n  \"bao-cun-she-zhi-shi-bai\": \"保存设置失败\",\n  \"mei-ri-tui-jian\": \"每日推荐\",\n  \"mei-you-zheng-zai-bo-fang-de-ge-qu\": \"没有在播放的歌曲\",\n  \"shou-cang-dao\": \"收藏到\",\n  \"mei-you-ge-dan\": \"还没有歌单\",\n  \"jin-yong-gpu-jia-su-zhong-qi-sheng-xiao\": \"禁用GPU加速\",\n  \"jie-mian\": \"界面\",\n  \"guan-bi-shi-minimize-to-tray\": \"关闭窗口时到托盘\",\n  \"mi-gan-cheng\": \"蜜柑橙\",\n  \"zhu-ce\": \"还没有账号？\",\n  \"xin-zeng-zhang-hao-qing-xian-zai-guan-fang-ke-hu-duan-zhong-deng-lu-yi-ci\": \"新注册账号请先在官方客户端中登录一次\",\n  \"shua-xin-hou-sheng-xiao\": \"(刷新后生效)\",\n  \"zhong-qi-hou-sheng-xiao\": \"(重启后生效)\",\n  \"shi-pei-gao-dpi\": \"启用高DPI支持\",\n  \"hires-yin-zhi\": \"Hi-Res音质\",\n  \"kui-she-chao-qing-yin-zhi\": \"蝰蛇超清音质\",\n  \"tian-jia-wo-xi-huan\": \"添加至我喜欢\",\n  \"qie-huan-bo-fang-mo-shi\": \"切换播放模式\",\n  \"ke-yong\": \"可用\",\n  \"wo-de-yun-pan\": \"我的云盘\",\n  \"yun-pan-ge-qu-shu\": \"云盘歌曲数\",\n  \"yun-pan-miao-shu\": \"这里存放您上传到云盘的音乐文件，支持在线播放和管理。\",\n  \"yun-pan-ge-qu\": \"云盘歌曲\",\n  \"cong-yun-pan-shan-chu\": \"从云盘删除\",\n  \"que-ren-shan-chu-yun-pan-ge-qu\": \"确认从云盘删除选中歌曲？\",\n  \"shang-chuan-yin-le\": \"上传\",\n  \"pi-liang-cao-zuo\": \"批量操作\",\n  \"pwa-app\": \"PWA应用\",\n  \"install\": \"安装\",\n  \"yin-pin-jia-zai-shi-bai\": \"音频加载失败\",\n  \"guan-zhu\": \"关注\",\n  \"fen-si\": \"粉丝\",\n  \"hao-you\": \"好友\",\n  \"fang-wen\": \"访问\",\n  \"qian-dao\": \"签到\",\n  \"xiao-shi\": \"小时\",\n  \"fen-zhong\": \"分钟\",\n  \"le-ling\": \"乐龄\",\n  \"nian\": \"年\",\n  \"ting-ge-shi-chang\": \"听歌时长\",\n  \"zheng-zai-jia-zai-quan-bu-ge-qu\": \"正在加载全部歌曲...\",\n  \"bo-fang-chu-cuo\": \"播放出错\",\n  \"bo-fang-shi-bai-qu-mu-wei-kong\": \"播放失败，曲目为空\",\n  \"shan-chu-cheng-gong\": \"删除成功\",\n  \"tian-jia-dao-bo-fang-lie-biao-cheng-gong\": \"添加到播放列表成功\",\n  \"hot\": \"热门\",\n  \"new\": \"最新\",\n  \"yun-pan-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"云盘音乐不支持添加到歌单\",\n  \"xian-qu-kan-kan-ni-de-shou-cang-jia-ba\": \"先去看看你的收藏夹吧\",\n  \"yi-da-dao-zui-da-chong-shi-ci-shu\": \"已达到最大重试次数，请手动选择歌曲\",\n  \"hui-fu-chu-chang-she-zhi-cheng-gong\": \"恢复出厂设置成功，请重启应用\",\n  \"liu-lan-qi-bu-zhi-chi-file-system-api\": \"浏览器不支持 File System API，请使用 Chrome 86+ 或 Edge 86+\",\n  \"cha-jian-an-zhuang-cheng-gong\": \"插件安装成功\",\n  \"zhi-chi-http-https-dai-li\": \"仅支持 HTTP/HTTPS 代理\",\n  \"ben-di-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"本地音乐不支持添加到歌单\",\n  \"mei-you-xuan-ze-zheng-que-de-ge-qu\": \"没有选择正确的歌曲\",\n  \"zhuang-tai-lan-ge-ci-jin-zhi-chi-mac\": \"状态栏歌词仅支持 macOS\",\n  \"qian-dao-shi-bai\": \"签到失败，请勿频繁签到\",\n  \"huo-qu-vip-shi-bai\": \"获取VIP失败，每天仅可获取一次\",\n  \"qing-zai-web-huan-jing-xia-an-zhuang\": \"请在 Web 环境下安装\",\n  \"qing-shu-ru-you-xiao-de-url\": \"请输入有效的 URL\",\n  \"zhe-shi-yi-ge-alert\": \"提示\",\n  \"fei-mac-bu-zhi-chi-touchbar\": \"非 Mac 不支持 TouchBar\",\n  \"zi-ti-she-zhi\": \"字体设置\",\n  \"mo-ren-zi-ti\": \"默认字体\",\n  \"hui-fu-chu-chang-she-zhi\": \"重置应用\",\n  \"zi-ti-url-di-zhi\": \"字体URL地址\",\n  \"qing-shu-ru-zi-ti-url-di-zhi\": \"请输入字体URL地址\",\n  \"zi-ti-ming-cheng\": \"字体名称\",\n  \"qing-shu-ru-zi-ti-ming-cheng\": \"请输入字体名称\",\n  \"cha-jian\": \"插件\",\n  \"shua-xin-cha-jian\": \"刷新插件\",\n  \"da-kai-cha-jian-mu-lu\": \"打开插件目录\",\n  \"an-zhuang-cha-jian\": \"安装插件\",\n  \"zan-wu-cha-jian\": \"暂无插件\",\n  \"jiang-cha-jian-wen-jian-jia-fang-ru-cha-jian-mu-lu\": \"将插件文件夹放入插件目录中，然后点击刷新按钮\",\n  \"kai-ji-zi-qi-dong\": \"开机自启动\",\n  \"wang-luo-mo-shi\": \"网络模式\",\n  \"zhu-wang\": \"主网\",\n  \"qi-dong-shi-zui-xiao-hua\": \"启动时最小化\",\n  \"zu-zhi-xi-tong-xiu-mian\": \"阻止系统休眠\",\n  \"api-mo-shi\": \"API模式\",\n  \"wang-luo-dai-li\": \"网络代理\",\n  \"zhuang-tai-lan-ge-ci\": \"状态栏歌词\",\n  \"ge-ci-fan-yi\": \"歌词翻译\",\n  \"dui-qi-fang-shi\": \"对齐方式\",\n  \"ju-zhong\": \"居中\",\n  \"ju-zuo\": \"居左\",\n  \"ju-you\": \"居右\",\n  \"ping-heng-yin-pin-xiang-du\": \"平衡音频响度\",\n  \"shu-ju-yuan\": \"数据源\",\n  \"suo-fang-yin-zi\": \"缩放因子\",\n  \"tiao-zheng-hou-xu-zhong-qi\": \"调整后需重启\",\n  \"api-di-zhi\": \"API地址\",\n  \"websocket-di-zhi\": \"WebSocket地址\",\n  \"mo-ren-api-ti-shi\": \"这是默认API地址，当前版本不支持自定义修改\",\n  \"dai-li-placeholder\": \"输入HTTP/HTTPS代理地址，例如：http://127.0.0.1:7890\",\n  \"zheng-zai-ce-shi\": \"正在测试...\",\n  \"ce-shi-lian-jie\": \"测试连接\",\n  \"bao-cun-she-zhi-an-niu\": \"保存\",\n  \"qing-shu-ru-dai-li-di-zhi\": \"请输入代理服务器地址\",\n  \"ce-wang\": \"测试网\",\n  \"kai-fa-wang\": \"开发网\",\n  \"qi-yong\": \"启用\",\n  \"jin-yong\": \"禁用\",\n  \"dai-li-di-zhi\": \"代理地址\",\n  \"gai-nian-ban-xuan-xiang\": \"概念版\",\n  \"zheng-shi-ban\": \"正式版\",\n  \"dai-li-lian-jie-cheng-gong\": \"代理连接成功，IP：\",\n  \"dai-li-lian-jie-shi-bai\": \"代理连接失败：\",\n  \"lian-jie-chao-shi\": \"连接超时\",\n  \"lian-jie-cuo-wu\": \"连接错误：\",\n  \"jin-zhi-chi-mac\": \"（仅支持macOS）\",\n  \"xian-shi-yin-cang-zhuo-mian-ge-ci\": \"显示/隐藏桌面歌词\",\n  \"ni-que-ren-hui-fu-chu-chang\": \"确定恢复出厂设置？此操作不可逆！\",\n  \"bang-zhu\": \"帮助\",\n  \"dian-ji-she-zhi-kuai-jie-jian\": \"点击设置快捷键\",\n  \"wang-luo-jie-dian\": \"网络节点\",\n  \"zi-ti-wen-jian-di-zhi\": \"字体文件地址\",\n  \"jia-zai-zhong\": \"加载中...\",\n  \"ban-ben\": \"版本\",\n  \"yi-qi-yong\": \"已启用\",\n  \"da-kai-tan-chuang\": \"设置\",\n  \"xie-zai\": \"卸载\",\n  \"zheng-zai-jia-zai-cha-jian\": \"正在加载插件...\",\n  \"web-cha-jian-ti-shi\": \"Web端请直接在浏览器插件中心 chrome://extensions/ 进行管理\",\n  \"da-kai-tan-chuang-shi-bai\": \"打开插件弹窗失败\",\n  \"que-ren-xie-zai-cha-jian\": \"确定要卸载插件name吗？\",\n  \"xie-zai-cha-jian-shi-bai\": \"卸载插件失败\",\n  \"xuan-ze-wen-jian-shi-bai\": \"选择文件失败\",\n  \"an-zhuang-cha-jian-shi-bai\": \"安装插件失败\",\n  \"an-zhuang-cha-jian-chu-cuo\": \"安装插件时出错\",\n  \"cha-jian-bao\": \"插件包\",\n  \"zhuo-mian-ge-ci\": \"桌面歌词\",\n  \"bo-fang-su-du\": \"播放速度\",\n  \"wo-xi-huan\": \"我喜欢\",\n  \"shou-cang-zhi\": \"收藏至\",\n  \"fen-xiang-ge-qu\": \"分享歌曲\",\n  \"qie-huan-dao-yin-yi\": \"切换到音译\",\n  \"qie-huan-dao-fan-yi\": \"切换到翻译\",\n  \"wei-zhi-cuo-wu\": \"未知错误\"\n}\n"
  },
  {
    "path": "src/language/zh-TW.json",
    "content": "{\n  \"tui-jian\": \"推薦\",\n  \"tui-jian-ge-qu\": \"推薦歌曲\",\n  \"tui-jian-ge-dan\": \"推薦歌單\",\n  \"fa-xian\": \"發現\",\n  \"da\": \"大\",\n  \"da-kai\": \"打開\",\n  \"gao-yin-zhi-320kbps\": \"高品音質 - 320Kbps\",\n  \"ge-ci\": \"歌詞\",\n  \"ge-ci-zi-ti-da-xiao\": \"全屏歌詞字體大小\",\n  \"guan-bi\": \"關閉\",\n  \"guan-bi-an-niu\": \"關閉\",\n  \"kai-qi\": \"開啟\",\n  \"nan-nan-lan\": \"天空藍\",\n  \"pu-tong-yin-zhi\": \"普通音質 - 128Kbps\",\n  \"qi-dong-wen-hou-yu\": \"啟動問候語\",\n  \"qian-se\": \"淺色\",\n  \"shao-nv-fen\": \"少女粉\",\n  \"shen-se\": \"深色\",\n  \"sheng-yin\": \"聲音\",\n  \"tou-ding-lv\": \"薄荷綠\",\n  \"wai-guan\": \"外觀\",\n  \"native-title-bar\":\"原生窗口装饰器\",\n  \"xian-shi-ge-ci-bei-jing\": \"顯示全屏歌詞背景封面\",\n  \"xian-shi-zhuo-mian-ge-ci\": \"顯示桌面歌詞\",\n  \"xiao\": \"小\",\n  \"xuan-ze-wai-guan\": \"選擇外觀\",\n  \"xuan-ze-yu-yan\": \"選擇語言\",\n  \"xuan-ze-zhu-se-tiao\": \"選擇主色調\",\n  \"yin-zhi-xuan-ze\": \"音質選擇\",\n  \"yu-yan\": \"語言\",\n  \"zhong\": \"中\",\n  \"zhu-se-tiao\": \"主色調\",\n  \"zi-dong\": \"自動\",\n  \"0-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"0. 本程式是酷狗第三方用戶端，並非酷狗官方，需要更完善的功能請下載官方客戶端體驗.\",\n  \"1-ben-cheng-xu-shi-ku-gou-di-san-fang-ke-hu-duan-bing-fei-ku-gou-guan-fang-xu-yao-geng-wan-shan-de-gong-neng-qing-xia-zai-guan-fang-ke-hu-duan-ti-yan\": \"1. 本程式是酷狗第三方客戶端，並非酷狗官方，需要更完善的功能請下載官方客戶端體驗.\",\n  \"1-ben-xiang-mu-jin-gong-xue-xi-shi-yong-qing-zun-zhong-ban-quan-qing-wu-li-yong-ci-xiang-mu-cong-shi-shang-ye-hang-wei-ji-fei-fa-yong-tu\": \"1. 本項目僅供學習使用，請尊重版權，請勿利用此項目從事商業行為及非法用途！\",\n  \"2-ben-xiang-mu-jin-gong-xue-xi-jiao-liu-shi-yong-nin-zai-shi-yong-guo-cheng-zhong-ying-zun-zhong-ban-quan-bu-de-yong-yu-shang-ye-huo-fei-fa-yong-tu\": \"2. 本項目僅供學習交流使用，您在使用過程中應尊重版權，不得用於商業或非法用途。\",\n  \"2-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-chan-sheng-ban-quan-shu-ju-dui-yu-zhe-xie-ban-quan-shu-ju-ben-xiang-mu-bu-yong-you-ta-men-de-suo-you-quan-wei-le-bi-mian-qin-quan-shi-yong-zhe-wu-bi-zai-24-xiao-shi-nei-qing-chu-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-suo-chan-sheng-de-ban-quan-shu-ju\": \"2. 使用本項目的過程中可能會產生版權資料。\\n對於這些版權數據，本項目不擁有它們的所有權。\\n為了避免侵權，使用者請務必在 24 小時內清除使用本項目的過程中所產生的版權資料。\",\n  \"3-miao-hou-zi-dong-qie-huan-xia-yi-shou\": \"3秒後自動切換下一首\",\n  \"3-you-yu-shi-yong-ben-xiang-mu-chan-sheng-de-bao-kuo-you-yu-ben-xie-yi-huo-you-yu-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-er-yin-qi-de-ren-he-xing-zhi-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-sun-hai-bao-kuo-dan-bu-xian-yu-yin-shang-yu-sun-shi-ting-gong-ji-suan-ji-gu-zhang-huo-gu-zhang-yin-qi-de-sun-hai-pei-chang-huo-ren-he-ji-suo-you-qi-ta-shang-ye-sun-hai-huo-sun-shi-you-shi-yong-zhe-fu-ze\": \"3.因使用本項目產生的包括因本協議或因使用或無法使用本項目而引起的任何性質的任何直接、間接、特殊、偶然或結果性損害（包括但不限於因商譽損失、停工、\\n電腦故障或故障所引起的損害賠償，或任何及所有其他商業損害或損失）由使用者負責。\",\n  \"3-zai-shi-yong-ben-xiang-mu-de-guo-cheng-zhong-ke-neng-hui-sheng-cheng-ban-quan-nei-rong-ben-xiang-mu-bu-yong-you-zhe-xie-ban-quan-nei-rong-de-suo-you-quan-wei-le-bi-mian-qin-quan-hang-wei-nin-xu-zai-24-xiao-shi-nei-qing-chu-you-ben-xiang-mu-chan-sheng-de-ban-quan-nei-rong\": \"3. 在使用本項目的過程中，可能會產生版權內容。\\n本項目不擁有這些版權內容的所有權。\\n為了避免侵權行為，您需在 24 小時內清除本項目產生的版權內容。\",\n  \"4-ben-xiang-mu-de-kai-fa-zhe-bu-dui-yin-shi-yong-huo-wu-fa-shi-yong-ben-xiang-mu-suo-dao-zhi-de-ren-he-sun-hai-cheng-dan-ze-ren-bao-kuo-dan-bu-xian-yu-shu-ju-diu-shi-ting-gong-ji-suan-ji-gu-zhang-huo-qi-ta-jing-ji-sun-shi\": \"4. 本專案的開發者不對因使用或無法使用本項目所導致的任何損害承擔責任，包括但不限於資料遺失、停工、電腦故障或其他經濟損失。\",\n  \"4-jin-zhi-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-dui-yu-shi-yong-zhe-zai-ming-zhi-huo-bu-zhi-dang-di-fa-lv-fa-gui-bu-yun-xu-de-qing-kuang-xia-shi-yong-ben-xiang-mu-suo-zao-cheng-de-ren-he-wei-fa-wei-gui-hang-wei-you-shi-yong-zhe-cheng-dan-ben-xiang-mu-bu-cheng-dan-you-ci-zao-cheng-de-ren-he-zhi-jie-jian-jie-te-shu-ou-ran-huo-jie-guo-xing-ze-ren\": \"4. 禁止在違反當地法律法規的情況下使用本項目。\\n對於使用者在明知或不知當地法律法規不允許的情況下使用本項目所造成的任何違法違規行為由使用者承擔，本項目不承擔由此造成的任何直接、間接、特殊、偶然或結果性責任\\n。\",\n  \"5-nin-bu-de-zai-wei-fan-dang-di-fa-lv-fa-gui-de-qing-kuang-xia-shi-yong-ben-xiang-mu-yin-wei-fan-fa-lv-fa-gui-suo-dao-zhi-de-ren-he-fa-lv-hou-guo-you-yong-hu-cheng-dan\": \"5. 您不得在違反當地法律法規的情況下使用本項目。\\n因違反法律法規所導致的任何法律後果由使用者承擔。\",\n  \"5-yin-le-ping-tai-bu-yi-qing-zun-zhong-ban-quan-zhi-chi-zheng-ban\": \"5. 音樂平台不易，請尊重版權，支持正版。\",\n  \"6-ben-xiang-mu-jin-yong-yu-ji-shu-tan-suo-he-yan-jiu-bu-jie-shou-ren-he-shang-ye-he-zuo-guang-gao-huo-juan-zeng-ru-guo-guan-fang-yin-le-ping-tai-dui-ci-xiang-mu-cun-you-yi-lv-ke-sui-shi-lian-xi-kai-fa-zhe-yi-chu-xiang-guan-nei-rong\": \"6. 本計畫僅用於技術探索和研究，不接受任何商業合作、廣告或捐贈。\\n若官方音樂平台對此專案存有疑慮，可隨時聯絡開發者移除相關內容。\",\n  \"7-ru-guo-guan-fang-yin-le-ping-tai-jue-de-ben-xiang-mu-bu-tuo-ke-lian-xi-ben-xiang-mu-geng-gai-huo-yi-chu\": \"7. 若官方音樂平台覺得本項目不妥，可聯絡本項目更改或移除。\",\n  \"bo-fang\": \"播放\",\n  \"bo-fang-lie-biao\": \"播放清單\",\n  \"bu-tong-yi\": \"不同意\",\n  \"chang-ting-ban\": \"暢聽版:\",\n  \"dan-qu-xun-huan\": \"單曲循環\",\n  \"de-yin-le-ku\": \"的音樂庫\",\n  \"deng-lu\": \"登入\",\n  \"deng-lu-cheng-gong\": \"登入成功\",\n  \"deng-lu-ni-de-ku-gou-zhang-hao\": \"登入你的酷狗帳號\",\n  \"deng-lu-shi-bai\": \"登入失敗\",\n  \"deng-lu-shi-bai-0\": \"登入失敗，\",\n  \"deng-lu-shi-xiao-qing-zhong-xin-deng-lu\": \"登入失效,請重新登入\",\n  \"di\": \"第\",\n  \"er-wei-ma\": \"QR 圖碼\",\n  \"er-wei-ma-deng-lu-cheng-gong\": \"QR 圖碼登入成功\",\n  \"er-wei-ma-jian-ce-shi-bai\": \"二維碼偵測失敗\",\n  \"er-wei-ma-sheng-cheng-shi-bai\": \"二維碼產生失敗\",\n  \"er-wei-ma-yi-guo-qi-qing-zhong-xin-sheng-cheng\": \"二維碼已過期，請重新生成\",\n  \"fa-song-yan-zheng-ma\": \"發送驗證碼\",\n  \"gai-nian-ban\": \"概念版:\",\n  \"geng-xin\": \"更新\",\n  \"gong\": \"共\",\n  \"guan-yu\": \"關於\",\n  \"huo-qu-er-wei-ma-shi-bai\": \"取得二維碼失敗\",\n  \"huo-qu-ge-ci-shi-bai\": \"取得歌詞失敗\",\n  \"huo-qu-ge-ci-zhong\": \"取得歌詞中...\",\n  \"huo-qu-ge-dan-shi-bai\": \"取得歌單失敗\",\n  \"huo-qu-yin-le-di-zhi-shi-bai\": \"取得音樂地址失敗\",\n  \"huo-qu-yin-le-shi-bai\": \"獲取音樂失敗\",\n  \"li-ji-deng-lu\": \"立即登入\",\n  \"lie-biao-xun-huan\": \"清單循環\",\n  \"login-tips\": \"萌音 承諾不會將你的任何帳號資訊保存到雲端。\\n你的密碼會在本地加密後再傳送到酷狗官方。\\n萌音並非酷狗官方網站，輸入帳號資訊前請慎重考慮,非所有帳號都支持帳號密碼登錄.\",\n  \"mian-ze-sheng-ming\": \"免責聲明\",\n  \"ni-huan-mei-you-tian-jia-ge-quo-kuai-qu-tian-jia-ba\": \"你還沒加歌曲哦，快去添加吧！\",\n  \"qing-shi-yong-ku-gou-sao-miao-er-wei-ma-deng-lu\": \"請使用酷狗掃描二維碼登錄\",\n  \"qing-shu-ru-deng-lu-you-xiang\": \"請輸入登錄賬號\",\n  \"qing-shu-ru-mi-ma\": \"請輸入密碼\",\n  \"qing-shu-ru-shou-ji-hao\": \"請輸入手機號\",\n  \"qing-shu-ru-shou-ji-hao-ma\": \"請輸入手機號碼\",\n  \"qing-shu-ru-yan-zheng-ma\": \"請輸入驗證碼\",\n  \"qing-shu-ru-you-xiang\": \"請輸入賬號\",\n  \"qu-xiao\": \"取消\",\n  \"que-ding\": \"確定\",\n  \"sao-ma-deng-lu\": \"掃碼登入\",\n  \"shang-yi-ye\": \"上一頁\",\n  \"shao-nv-qi-dao-zhong\": \"少女祈禱中....\",\n  \"she-zhi\": \"設定\",\n  \"shi-yong-yan-zheng-ma-deng-lu\": \"使用驗證碼登入.\",\n  \"shou-ge\": \"首歌\",\n  \"shou-ji-hao-deng-lu\": \"手機號登入\",\n  \"shou-ji-hao-ge-shi-cuo-wu\": \"手機號碼格式錯誤\",\n  \"shou-ye\": \"首頁\",\n  \"shun-xu-bo-fang\": \"順序播放\",\n  \"sou-suo-jie-guo\": \"搜尋結果\",\n  \"sou-suo-yin-le-ge-shou-ge-dan\": \"搜尋音樂、歌手、歌單、分享碼...\",\n  \"sui-ji-bo-fang\": \"隨機播放\",\n  \"tong-yi\": \"同意\",\n  \"tong-yi-ji-xu-shi-yong-ben-xiang-mu-nin-ji-jie-shou-yi-shang-tiao-kuan-sheng-ming-nei-rong\": \"同意繼續使用本項目，您即接受以上條款聲明內容。\",\n  \"tui-chu\": \"退出\",\n  \"wo-chuang-jian-de-ge-dan\": \"我創建的歌單\",\n  \"wo-guan-zhu-de-ge-shou\": \"我關注的歌手\",\n  \"wo-shou-cang-de-ge-dan\": \"我收藏的歌單\",\n  \"wo-xi-huan-ting\": \"我喜歡聽\",\n  \"wu-xiang-ying-shu-ju\": \"無回應數據\",\n  \"xia-yi-ye\": \"下一頁\",\n  \"yan-zheng-ma-fa-song-shi-bai\": \"驗證碼發送失敗\",\n  \"yan-zheng-ma-fa-song-shi-bai-0\": \"驗證碼發送失敗，\",\n  \"yan-zheng-ma-yi-fa-song\": \"驗證碼已發送\",\n  \"ye\": \"頁\",\n  \"yi-sao-ma-deng-dai-que-ren\": \"已掃碼,等待確認\",\n  \"yin-le-ku\": \"音樂庫\",\n  \"yong-hu\": \"使用者\",\n  \"yong-hu-tiao-kuan\": \"使用者條款\",\n  \"yong-hu-tou-xiang\": \"使用者頭像\",\n  \"you-xiang-deng-lu\": \"賬號登錄\",\n  \"you-xiang-ge-shi-cuo-wu\": \"郵箱格式錯誤\",\n  \"zan-wu-ge-ci\": \"暫無歌詞\",\n  \"6-ben-xiang-mu-jin-yong-yu-dui-ji-shu-ke-hang-xing-de-tan-suo-ji-yan-jiu-bu-jie-shou-ren-he-shang-ye-bao-kuo-dan-bu-xian-yu-guang-gao-deng-he-zuo-ji-juan-zeng\": \"6. 本計畫僅用於技術可行性的探索及研究，不接受任何商業（包括但不限於廣告等）合作及捐贈。\",\n  \"ge-qu-lie-biao\": \"歌曲清單\",\n  \"zheng-zai-sheng-cheng-er-wei-ma\": \"正在產生二維碼...\",\n  \"zhe-li-shi-mo-du-mei-you\": \"這裡什麼都沒有\",\n  \"wu-sun-yin-zhi-1104kbps\": \"無損音質 - 1104kbps\",\n  \"gao-pin-zhi-yin-le-xu-yao-deng-lu-hou-cai-neng-bo-fango\": \"高品質音樂需要登入後才能播放哦~\",\n  \"tian-jia-ge-dan\": \"加入歌單\",\n  \"qing-xian-deng-lu\": \"請先登入\",\n  \"cheng-gong-tian-jia-dao-ge-dan\": \"成功加入歌單！\",\n  \"tian-jia-dao-ge-dan-shi-bai\": \"加入歌單失敗！\",\n  \"cheng-gong-qu-xiao-shou-cang\": \"取消收藏\",\n  \"qu-xiao-shou-cang-shi-bai\": \"取消失敗\",\n  \"ni-que-ren-yao-tui-chu-deng-lu-ma\": \"你確認要登出登入嗎?\",\n  \"gai-ge-qu-zan-wu-ban-quan\": \"該歌曲暫無版權\",\n  \"wo-shou-cang-de-zhuan-ji\": \"我收藏的專輯\",\n  \"bo-fang-shi-bai\": \"播放失敗\",\n  \"wo-guan-zhu-de-hao-you\": \"我關注的好友\",\n  \"tian-jia-cheng-gong\": \"添加成功\",\n  \"chuang-jian-ge-dan\": \"建立歌單\",\n  \"qing-shu-ru-xin-de-ge-dan-ming-cheng\": \"請輸入新的歌單名稱\",\n  \"chuang-jian-shi-bai\": \"創建失敗\",\n  \"que-ren-shan-chu-ge-dan\": \"確認刪除歌單？\",\n  \"shou-cang-shi-bai\": \"收藏失敗\",\n  \"shou-cang-cheng-gong\": \"收藏成功\",\n  \"wo-guan-zhu-de-yi-ren\": \"我關注的音樂人\",\n  \"sou-suo-ge-qu\": \"搜尋歌曲...\",\n  \"fan-hui-ding-bu\": \"回到頂部\",\n  \"dang-qian-bo-fang-ge-qu\": \"目前播放歌曲\",\n  \"yi-fu-zhi-fen-xiang-ma-qing-zai-moekoe-ke-hu-duan-zhong-fang-wen\": \"已複製分享碼，請在MoeKoe客戶端中查看\",\n  \"ge-qu-shu-ju-cuo-wu\": \"歌單不存在\",\n  \"bao-cun\": \"儲存\",\n  \"bao-cun-she-zhi-shi-bai\": \"保存設置失敗\",\n  \"cun-zai-wu-xiao-de-kuai-jie-jian-she-zhi-qing-que-bao-mei-ge-kuai-jie-jian-du-bao-han-xiu-shi-jian\": \"存在無效的快捷鍵設置，請確保每個快捷鍵都包含修飾鍵\",\n  \"de-kuai-jie-jian-chong-tu\": \"的快捷鍵衝突\",\n  \"fei-ke-hu-duan-huan-jing-wu-fa-qi-yong\": \"非客戶端環境，無法啟用\",\n  \"gai-kuai-jie-jian-yu\": \"該快捷鍵與\",\n  \"jing-yin\": \"靜音\",\n  \"kuai-jie-jian-bi-xu-bao-han-zhi-shao-yi-ge-xiu-shi-jian-ctrlaltshiftcommand\": \"快速鍵必須包含至少一個修飾鍵(Ctrl/Alt/Shift/Command)\",\n  \"kuai-jie-jian-she-zhi\": \"快捷鍵設置\",\n  \"qing-an-xia-qi-ta-jian\": \"[請按下其他按鍵]\",\n  \"qing-an-xia-xiu-shi-jian\": \"請按下修飾鍵\",\n  \"quan-ju-kuai-jie-jian\": \"全域快速鍵\",\n  \"shang-yi-shou\": \"上一首\",\n  \"tui-chu-zhu-cheng-xu\": \"退出主程序\",\n  \"xi-tong\": \"系統\",\n  \"xia-yi-shou\": \"下一首\",\n  \"xian-shi-yin-cang-zhu-chuang-kou\": \"顯示/隱藏主視窗\",\n  \"yin-liang-jian-xiao\": \"音量減小\",\n  \"yin-liang-zeng-jia\": \"音量增加\",\n  \"zan-ting-bo-fang\": \"暫停/播放\",\n  \"zi-ding-yi-kuai-jie-jian\": \"自定義快捷鍵\",\n  \"mei-ri-tui-jian\": \"每日推薦\",\n  \"mei-you-zheng-zai-bo-fang-de-ge-qu\": \"沒有在播放的歌曲\",\n  \"shou-cang-dao\": \"收藏到\",\n  \"mei-you-ge-dan\": \"還沒有歌單\",\n  \"jin-yong-gpu-jia-su-zhong-qi-sheng-xiao\": \"禁用GPU加速\",\n  \"jie-mian\": \"介面\",\n  \"guan-bi-shi-minimize-to-tray\": \"關閉窗口時到托盤\",\n  \"mi-gan-cheng\": \"蜜柑橙\",\n  \"zhu-ce\": \"還沒有賬號？\",\n  \"xin-zeng-zhang-hao-qing-xian-zai-guan-fang-ke-hu-duan-zhong-deng-lu-yi-ci\": \"新註冊賬號請先在官方客戶端中登錄一次\",\n  \"shua-xin-hou-sheng-xiao\": \"(刷新後生效)\",\n  \"zhong-qi-hou-sheng-xiao\": \"(重啟後生效)\",\n  \"shi-pei-gao-dpi\": \"啟用高DPI支持\",\n  \"kui-she-chao-qing-yin-zhi\": \"蝰蛇超清音質\",\n  \"hires-yin-zhi\": \"Hi-Res音質\",\n  \"tian-jia-wo-xi-huan\": \"添加至我喜歡\",\n  \"qie-huan-bo-fang-mo-shi\": \"切換播放模式\",\n  \"pwa-app\": \"PWA應用程序\",\n  \"install\": \"安裝\",\n  \"yin-pin-jia-zai-shi-bai\": \"音頻加載失敗\",\n  \"zheng-zai-jia-zai-quan-bu-ge-qu\": \"正在載入全部歌曲...\",\n  \"bo-fang-chu-cuo\": \"播放出錯\",\n  \"bo-fang-shi-bai-qu-mu-wei-kong\": \"播放失敗，曲目為空\",\n  \"shan-chu-cheng-gong\": \"刪除成功\",\n  \"tian-jia-dao-bo-fang-lie-biao-cheng-gong\": \"已添加到播放清單\",\n  \"hot\": \"熱門\",\n  \"new\": \"最新\",\n  \"yun-pan-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"雲盤音樂不支持添加到歌單\",\n  \"xian-qu-kan-kan-ni-de-shou-cang-jia-ba\": \"先去看看你的收藏夾吧\",\n  \"yi-da-dao-zui-da-chong-shi-ci-shu\": \"已達到最大重試次數，請手動選擇歌曲\",\n  \"hui-fu-chu-chang-she-zhi-cheng-gong\": \"恢復出廠設置成功，請重啟應用\",\n  \"liu-lan-qi-bu-zhi-chi-file-system-api\": \"瀏覽器不支持 File System API，請使用 Chrome 86+ 或 Edge 86+\",\n  \"cha-jian-an-zhuang-cheng-gong\": \"插件安裝成功\",\n  \"zhi-chi-http-https-dai-li\": \"僅支持 HTTP/HTTPS 代理\",\n  \"ben-di-yin-le-bu-zhi-chi-tian-jia-dao-ge-dan\": \"本地音樂不支持添加到歌單\",\n  \"mei-you-xuan-ze-zheng-que-de-ge-qu\": \"沒有選擇正確的歌曲\",\n  \"zhuang-tai-lan-ge-ci-jin-zhi-chi-mac\": \"狀態欄歌詞僅支持 macOS\",\n  \"qian-dao-shi-bai\": \"簽到失敗，請勿頻繁簽到\",\n  \"huo-qu-vip-shi-bai\": \"獲取VIP失敗，每天僅可獲取一次\",\n  \"qing-zai-web-huan-jing-xia-an-zhuang\": \"請在 Web 環境下安裝\",\n  \"qing-shu-ru-you-xiao-de-url\": \"請輸入有效的 URL\",\n  \"zhe-shi-yi-ge-alert\": \"提示\",\n  \"fei-mac-bu-zhi-chi-touchbar\": \"非 Mac 不支持 TouchBar\",\n  \"zi-ti-she-zhi\": \"字體設置\",\n  \"mo-ren-zi-ti\": \"默認字體\",\n  \"hui-fu-chu-chang-she-zhi\": \"重置應用\",\n  \"zi-ti-url-di-zhi\": \"字體URL地址\",\n  \"qing-shu-ru-zi-ti-url-di-zhi\": \"請輸入字體URL地址\",\n  \"zi-ti-ming-cheng\": \"字體名稱\",\n  \"qing-shu-ru-zi-ti-ming-cheng\": \"請輸入字體名稱\",\n  \"cha-jian\": \"插件\",\n  \"shua-xin-cha-jian\": \"刷新插件\",\n  \"da-kai-cha-jian-mu-lu\": \"打開插件目錄\",\n  \"an-zhuang-cha-jian\": \"安裝插件\",\n  \"zan-wu-cha-jian\": \"暫無插件\",\n  \"jiang-cha-jian-wen-jian-jia-fang-ru-cha-jian-mu-lu\": \"將插件文件夾放入插件目錄中，然後點擊刷新按鈕\",\n  \"kai-ji-zi-qi-dong\": \"開機自啟動\",\n  \"wang-luo-mo-shi\": \"網絡模式\",\n  \"zhu-wang\": \"主網\",\n  \"qi-dong-shi-zui-xiao-hua\": \"啟動時最小化\",\n  \"zu-zhi-xi-tong-xiu-mian\": \"阻止系統休眠\",\n  \"api-mo-shi\": \"API模式\",\n  \"wang-luo-dai-li\": \"網絡代理\",\n  \"zhuang-tai-lan-ge-ci\": \"狀態欄歌詞\",\n  \"ge-ci-fan-yi\": \"歌詞翻譯\",\n  \"dui-qi-fang-shi\": \"對齊方式\",\n  \"ju-zhong\": \"居中\",\n  \"ju-zuo\": \"居左\",\n  \"ju-you\": \"居右\",\n  \"ping-heng-yin-pin-xiang-du\": \"平衡音頻響度\",\n  \"shu-ju-yuan\": \"數據源\",\n  \"suo-fang-yin-zi\": \"縮放因子\",\n  \"tiao-zheng-hou-xu-zhong-qi\": \"調整後需重啟\",\n  \"api-di-zhi\": \"API地址\",\n  \"websocket-di-zhi\": \"WebSocket地址\",\n  \"mo-ren-api-ti-shi\": \"這是默認API地址，當前版本不支持自定義修改\",\n  \"dai-li-placeholder\": \"輸入HTTP/HTTPS代理地址，例如：http://127.0.0.1:7890\",\n  \"zheng-zai-ce-shi\": \"正在測試...\",\n  \"ce-shi-lian-jie\": \"測試連接\",\n  \"bao-cun-she-zhi-an-niu\": \"保存\",\n  \"qing-shu-ru-dai-li-di-zhi\": \"請輸入代理服務器地址\",\n  \"ce-wang\": \"測試網\",\n  \"kai-fa-wang\": \"開發網\",\n  \"qi-yong\": \"啟用\",\n  \"jin-yong\": \"禁用\",\n  \"dai-li-di-zhi\": \"代理地址\",\n  \"gai-nian-ban-xuan-xiang\": \"概念版\",\n  \"zheng-shi-ban\": \"正式版\",\n  \"dai-li-lian-jie-cheng-gong\": \"代理連接成功，IP：\",\n  \"dai-li-lian-jie-shi-bai\": \"代理連接失敗：\",\n  \"lian-jie-chao-shi\": \"連接超時\",\n  \"lian-jie-cuo-wu\": \"連接錯誤：\",\n  \"jin-zhi-chi-mac\": \"（僅支持macOS）\",\n  \"xian-shi-yin-cang-zhuo-mian-ge-ci\": \"顯示/隱藏桌面歌詞\",\n  \"ni-que-ren-hui-fu-chu-chang\": \"確定恢復出廠設置？此操作不可逆！\",\n  \"bang-zhu\": \"幫助\",\n  \"dian-ji-she-zhi-kuai-jie-jian\": \"點擊設置快捷鍵\",\n  \"wang-luo-jie-dian\": \"網絡節點\",\n  \"zi-ti-wen-jian-di-zhi\": \"字體文件地址\",\n  \"jia-zai-zhong\": \"載入中...\",\n  \"ban-ben\": \"版本\",\n  \"yi-qi-yong\": \"已啟用\",\n  \"da-kai-tan-chuang\": \"設定\",\n  \"xie-zai\": \"卸載\",\n  \"zheng-zai-jia-zai-cha-jian\": \"正在載入插件...\",\n  \"web-cha-jian-ti-shi\": \"Web端請直接在瀏覽器插件中心 chrome://extensions/ 進行管理\",\n  \"da-kai-tan-chuang-shi-bai\": \"打開插件彈窗失敗\",\n  \"que-ren-xie-zai-cha-jian\": \"確定要卸載插件name嗎？\",\n  \"xie-zai-cha-jian-shi-bai\": \"卸載插件失敗\",\n  \"xuan-ze-wen-jian-shi-bai\": \"選擇文件失敗\",\n  \"an-zhuang-cha-jian-shi-bai\": \"安裝插件失敗\",\n  \"an-zhuang-cha-jian-chu-cuo\": \"安裝插件時出錯\",\n  \"cha-jian-bao\": \"插件包\",\n  \"zhuo-mian-ge-ci\": \"桌面歌詞\",\n  \"bo-fang-su-du\": \"播放速度\",\n  \"wo-xi-huan\": \"我喜歡\",\n  \"shou-cang-zhi\": \"收藏至\",\n  \"fen-xiang-ge-qu\": \"分享歌曲\",\n  \"qie-huan-dao-yin-yi\": \"切換到音譯\",\n  \"qie-huan-dao-fan-yi\": \"切換到翻譯\",\n  \"wei-zhi-cuo-wu\": \"未知錯誤\"\n}\n"
  },
  {
    "path": "src/layouts/HomeLayout.vue",
    "content": "<template>\n    <Header />\n    <main>\n        <div v-if=\"!isOnline\" class=\"network-status\">\n            网络连接已断开\n        </div>\n        <router-view :playerControl=\"playerControl\"></router-view>\n    </main>\n    <PlayerControl ref=\"playerControl\" />\n</template>\n\n<script setup>\nimport { ref, onMounted, onUnmounted } from 'vue';\nimport Header from \"@/components/Header.vue\";\nimport PlayerControl from \"@/components/PlayerControl.vue\";\nimport { setTheme, applyColorTheme } from '../utils/utils';\nconst playerControl = ref(null);\nconst isOnline = ref(navigator.onLine);\n\n// 监听网络状态变化\nconst handleNetworkChange = (online) => {\n    isOnline.value = online;\n    \n    const title = online ? '网络已连接' : '网络已断开';\n    const body = online ? '您已恢复网络连接' : '请检查网络设置';\n    \n    new Notification(title, {\n        body,\n        icon: './assets/images/logo.png'\n    });\n};\n\nonMounted(() => {\n    const savedConfig = JSON.parse(localStorage.getItem('settings'));\n    if (savedConfig) {\n        applyColorTheme(savedConfig['themeColor']);\n    }\n    const savedTheme = localStorage.getItem('theme');\n    if (savedTheme) {\n        setTheme(savedTheme);\n    }\n\n    // 添加网络状态监听\n    window.addEventListener('online', () => handleNetworkChange(true));\n    window.addEventListener('offline', () => handleNetworkChange(false));\n    \n    if (Notification.permission !== 'granted') {\n        Notification.requestPermission();\n    }\n});\n\n// 组件卸载时移除事件监听\nonUnmounted(() => {\n    window.removeEventListener('online', () => handleNetworkChange(true));\n    window.removeEventListener('offline', () => handleNetworkChange(false));\n});\n</script>\n\n<style>\n:root {\n    /* 粉红色主色调 - 用于主要按钮、强调元素 */\n    --primary-color: #FF69B4;\n    /* 浅粉红色辅助色 - 用于次要按钮、提示信息 */\n    --secondary-color: #FFB6C1;\n    /* 文本颜色 - 用于正文内容 */\n    --text-color: #333;\n    /* 浅粉色背景 - 用于页面主背景 */\n    --background-color: #FFF0F5;\n    /* 次要背景色 - 用于卡片、侧边栏背景 */\n    --background-color-secondary: #FFE6F0;\n    /* 高亮色 - 用于交互元素如按钮、链接 */\n    --color-primary: #ea33e4;\n    /* 高亮色的浅色版本 - 用于选中状态背景 */\n    --color-primary-light: rgba(255, 105, 180, 0.1);\n    /* 边框颜色 - 用于分隔线、边框 */\n    --border-color: #FFD9E6;\n    /* 悬停颜色 - 用于元素悬停状态 */\n    --hover-color: #FFE9F2;\n    /* 半透明背景 - 用于覆盖层、提示框 */\n    --color-secondary-bg-for-transparent: rgba(209, 209, 214, 0.28);\n    /* 阴影颜色 - 用于卡片、弹窗阴影 */\n    --color-box-shadow: rgba(255, 105, 180, 0.2);\n}\n\n* {\n    user-select: none;\n}\n\nbody,\nhtml {\n    margin: 0;\n    padding: 0;\n    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;\n    background-color: #FFF;\n    color: var(--text-color);\n    height: 100%;\n}\n\nbody {\n    -ms-overflow-style: none;\n    scrollbar-width: none;\n}\n\n::-webkit-scrollbar {\n    width: 0;\n    height: 0;\n}\n\nmain {\n    min-height: calc(100vh - 80px - 188px);\n    max-width: 1200px;\n    margin: 0 auto;\n    margin-bottom: 150px;\n    padding-top: 80px;\n    padding-bottom: 150px;\n}\n\na {\n    text-decoration: none;\n    color: inherit;\n    display: block;\n}\n\n.network-status {\n    position: fixed;\n    top: 80px;\n    left: 0;\n    right: 0;\n    background-color: #ff4757;\n    color: white;\n    text-align: center;\n    padding: 8px;\n    z-index: 1000;\n}\n</style>"
  },
  {
    "path": "src/main.js",
    "content": "import { createApp } from 'vue';\nimport { createPinia } from 'pinia';\nimport piniaPersistedstate from 'pinia-plugin-persistedstate';\nimport App from './App.vue';\nimport router from './router/router';\nimport { formatMilliseconds, getCover, applyColorTheme, setTheme } from '../src/utils/utils';\nimport ModalPlugin from './plugins/ModalPlugin';\nimport MessagePlugin from './plugins/MessagePlugin';\nimport i18n from './utils/i18n';\nimport '@/assets/themes/dark.css';\nimport { registerSW } from 'virtual:pwa-register'\n\nconst app = createApp(App);\nconst pinia = createPinia();\npinia.use(piniaPersistedstate);\napp.config.globalProperties.$getCover = getCover;\napp.config.globalProperties.$formatMilliseconds = formatMilliseconds;\napp.config.globalProperties.$applyColorTheme = applyColorTheme;\napp.config.globalProperties.$setTheme = setTheme;\napp.config.errorHandler = (err, vm, info) => {\n  console.error(`全局捕获异常: ${info}`, err);\n};\napp.config.warnHandler = (msg, vm, trace) => {\n  console.warn(`全局捕获警告: ${msg}`, trace);\n};\nwindow.addEventListener('unhandledrejection', event => {\n  console.error('未处理的 Promise 拒绝:', event.reason);\n  // window.$modal.alert('系统错误');\n});\n\nif (!window.electron) {\n  registerSW({\n    onNeedRefresh() {\n      console.log('有新内容可用，请刷新页面')\n    },\n    onOfflineReady() {\n      console.log('应用已准备好离线工作')\n    }\n  })\n}\n\napp.use(pinia);\napp.use(router);\napp.use(i18n);\napp.use(ModalPlugin);\napp.use(MessagePlugin);\n\napp.mount('#app');\n"
  },
  {
    "path": "src/plugins/MessagePlugin.js",
    "content": "import { createApp, ref } from 'vue';\nimport MessageNotification from '@/components/MessageNotification.vue';\n\nexport default {\n    install() {\n        const messageInstance = ref(null);\n\n        const mountMessage = () => {\n            if (!messageInstance.value) {\n                const MessageComponent = createApp(MessageNotification);\n                const div = document.createElement('div');\n                document.body.appendChild(div);\n                messageInstance.value = MessageComponent.mount(div);\n            }\n        };\n\n        // 创建消息方法\n        const message = (content, type = 'default', duration = 3000) => {\n            mountMessage();\n            return messageInstance.value.addMessage(content, type, duration);\n        };\n\n        // 创建不同类型的消息方法\n        const success = (content, duration = 3000) => {\n            mountMessage();\n            return messageInstance.value.success(content, duration);\n        };\n\n        const error = (content, duration = 3000) => {\n            mountMessage();\n            return messageInstance.value.error(content, duration);\n        };\n\n        const warning = (content, duration = 3000) => {\n            mountMessage();\n            return messageInstance.value.warning(content, duration);\n        };\n\n        const info = (content, duration = 3000) => {\n            mountMessage();\n            return messageInstance.value.info(content, duration);\n        };\n\n        // 将方法挂载到全局\n        window.$message = {\n            message,\n            success,\n            error,\n            warning,\n            info\n        };\n    },\n};"
  },
  {
    "path": "src/plugins/ModalPlugin.js",
    "content": "import { createApp, ref } from 'vue';\nimport CustomModal from '@/components/CustomModal.vue';\n\nexport default {\n    install() {\n        const modal = ref(null);\n\n        const mountModal = () => {\n            if (!modal.value) {\n                const ModalComponent = createApp(CustomModal);\n                const div = document.createElement('div');\n                document.body.appendChild(div);\n                modal.value = ModalComponent.mount(div);\n            }\n        };\n\n        const customAlert = (message) => {\n            mountModal();\n            return modal.value.customAlert(message);\n        };\n\n        const customConfirm = (message) => {\n            mountModal();\n            return modal.value.customConfirm(message);\n        };\n\n        const customPrompt = (message, defaultValue = '') => {\n            mountModal();\n            return modal.value.customPrompt(message, defaultValue);\n        };\n\n        const showLoading = () => {\n            mountModal();\n            modal.value.showCustomLoading();\n        };\n\n        const hideLoading = () => {\n            mountModal();\n            modal.value.hideCustomLoading();\n        };\n        window.$modal = {\n            alert: customAlert,\n            confirm: customConfirm,\n            prompt: customPrompt,\n            showLoading,\n            hideLoading,\n        };\n    },\n};"
  },
  {
    "path": "src/router/router.js",
    "content": "import { createRouter, createWebHashHistory } from 'vue-router';\nimport HomeLayout from '@/layouts/HomeLayout.vue';\nimport Home from '@/views/Home.vue';\nimport Discover from '@/views/Discover.vue';\nimport Library from '@/views/Library.vue';\nimport Login from '@/views/Login.vue';\nimport Settings from '@/views/Settings.vue';\nimport PlaylistDetail from '@/views/PlaylistDetail.vue';\nimport Search from '@/views/Search.vue';\nimport Lyrics from '@/views/Lyrics.vue';\nimport Ranking from '@/views/Ranking.vue';\nimport CloudDrive from '@/views/CloudDrive.vue';\nimport LocalMusic from '@/views/LocalMusic.vue';\nimport VideoPlayer from '@/views/VideoPlayer.vue';\nimport { MoeAuthStore } from '@/stores/store';\n\n\nconst routes = [\n    {\n        path: '/',\n        component: HomeLayout,\n        children: [\n            { path: '', name: 'Index', component: Home },\n            { path: '/share', name: 'Share', component: Home },\n            { path: '/discover', name: 'Discover', component: Discover },\n            { path: '/library', name: 'Library', component: Library, meta: { requiresAuth: true } },\n            { path: '/login', name: 'Login', component: Login },\n            { path: '/settings', name: 'Settings', component: Settings },\n            { path: '/playlistDetail', name: 'PlaylistDetail', component: PlaylistDetail },\n            { path: '/search', name: 'Search', component: Search },\n            { path: '/ranking', name: 'Ranking', component: Ranking },\n            { path: '/CloudDrive', name: 'CloudDrive', component: CloudDrive },\n            { path: '/LocalMusic', name: 'LocalMusic', component: LocalMusic },\n        ],\n    },\n    { path: '/lyrics', name: 'Lyrics', component: Lyrics },\n    { path: '/video', name: 'VideoPlayer', component: VideoPlayer },\n];\n\nconst router = createRouter({\n    history: createWebHashHistory(),\n    routes,\n    scrollBehavior(to, from, savedPosition) {\n        if (savedPosition) {\n            return new Promise((resolve) => {\n                setTimeout(() => {\n                    resolve({\n                        ...savedPosition,\n                        behavior: 'smooth'\n                    });\n                }, 100);\n            });\n        }\n        if (to.hash) {\n            return {\n                el: to.hash,\n                behavior: 'smooth',\n                top: 80, \n            };\n        }\n        if (to.path === from.path && JSON.stringify(to.params) === JSON.stringify(from.params)) {\n            return false;\n        }\n        return new Promise((resolve) => {\n            setTimeout(() => {\n                resolve({ top: 0, behavior: 'smooth' });\n            }, 50);\n        });\n    }\n});\n\n// 全局导航守卫\nrouter.beforeEach((to, from, next) => {\n    console.log('完整的路由地址:', to.fullPath);\n    const MoeAuth = MoeAuthStore()\n    // 检查是否需要登录\n    if (to.matched.some(record => record.meta.requiresAuth)) {\n        if (!MoeAuth.isAuthenticated) {\n            next({\n                path: '/login',\n                query: { redirect: to.fullPath } \n            });\n        } \n    } \n    next();\n});\n\nexport default router;"
  },
  {
    "path": "src/stores/musicQueue.js",
    "content": "import { defineStore } from 'pinia';\n\nexport const useMusicQueueStore = defineStore('MusicQueue', {\n    state: () => ({\n        queue: [], // 播放列表\n    }),\n    actions: {\n        // 添加歌曲到播放队列\n        addSong(song) {\n            this.queue.push(song);\n        },\n        // 设置整个队列\n        setQueue(newQueue) {\n            this.queue = newQueue;\n        },\n        // 获取播放队列\n        getQueue() {\n            return this.queue;\n        },\n        // 清空指定歌曲\n        removeSong(index) {\n            this.queue.splice(index, 1);\n        },\n        // 清空播放队列\n        clearQueue() {\n            this.queue = [];\n        },\n    },\n    persist: {\n        enabled: true,\n        strategies: [\n            {\n                key: 'MusicQueue',\n                storage: localStorage,\n                paths: ['queue'],\n            },\n        ],\n    },\n});"
  },
  {
    "path": "src/stores/store.js",
    "content": "import { defineStore } from 'pinia';\nimport axios from 'axios';\nimport { getApiBaseUrl } from '../utils/apiBaseUrl';\n\n// 用于设备注册的独立 axios 实例（不带拦截器，避免循环依赖）\nconst registerDeviceApi = axios.create({\n    baseURL: getApiBaseUrl(),\n    timeout: 10000,\n});\n\nexport const MoeAuthStore = defineStore('MoeData', {\n    state: () => ({\n        UserInfo: null, // 用户信息\n        Config: null, // 配置信息\n        Device: null, // 设备信息\n    }),\n    actions: {\n        fetchConfig(key) {\n            if (!this.Config) return null;\n            const configItem = this.Config.find(item => item.key === key);\n            return configItem ? configItem.value : null;\n        },\n        async setData(data) {\n            if (data.UserInfo) this.UserInfo = data.UserInfo;\n            if (data.Config) this.Config = data.Config;\n        },\n        clearData() {\n            this.UserInfo = null; // 清除用户信息\n        },\n        async initDevice() {\n            if (this.Device) return this.Device;\n            try {\n                const response = await registerDeviceApi.get('/register/dev');\n                const device = response?.data?.data;\n                if (device) {\n                    this.Device = device;\n                    return device;\n                }\n            } catch (error) {\n                console.error('Failed to register device:', error);\n            }\n            return null;\n        }\n    },\n    getters: {\n        isAuthenticated: (state) => !!state.UserInfo && !!state.UserInfo, // 是否已登录\n    },\n    persist: {\n        enabled: true,\n        strategies: [\n            {\n                key: 'MoeData',\n                storage: localStorage,\n                paths: ['UserInfo', 'Config', 'Device'],\n            },\n        ],\n    },\n});"
  },
  {
    "path": "src/utils/apiBaseUrl.js",
    "content": "export const DEFAULT_API_BASE_URL =\n    import.meta.env.VITE_APP_API_URL || 'http://127.0.0.1:6521';\n\nexport function normalizeApiBaseUrl(input) {\n    const result = validateApiBaseUrl(input);\n    return result.ok ? result.value : '';\n}\n\nexport function validateApiBaseUrl(input) {\n    const raw = (input ?? '').toString().trim();\n    if (!raw) return { ok: true, value: '' };\n\n    let url;\n    try {\n        url = new URL(raw);\n    } catch {\n        return { ok: false, value: '', error: '请输入完整的 http(s):// 地址' };\n    }\n\n    if (!['http:', 'https:'].includes(url.protocol)) {\n        return { ok: false, value: '', error: '仅支持 http:// 或 https://' };\n    }\n\n    return { ok: true, value: raw.replace(/\\/+$/, '') };\n}\n\nexport function getApiBaseUrl() {\n    try {\n        const settingsRaw = localStorage.getItem('settings');\n        const settings = settingsRaw ? JSON.parse(settingsRaw) : {};\n        const custom = normalizeApiBaseUrl(settings?.apiBaseUrl);\n        return custom || DEFAULT_API_BASE_URL;\n    } catch {\n        return DEFAULT_API_BASE_URL;\n    }\n}\n\nexport function joinApiUrl(baseUrl, path = '/') {\n    const base = (baseUrl || '').replace(/\\/+$/, '');\n    const rel = (path || '').replace(/^\\/+/, '');\n    return rel ? `${base}/${rel}` : `${base}/`;\n}\n\nexport async function testApiBaseUrl(baseUrl, options = {}) {\n    const { path = '/register/dev', timeoutMs = 8000 } = options;\n    const target = joinApiUrl(baseUrl, path);\n\n    const controller = new AbortController();\n    const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n    try {\n        const response = await fetch(target, {\n            method: 'GET',\n            headers: { 'Accept': 'application/json' },\n            signal: controller.signal,\n        });\n\n        if (!response.ok) {\n            return { ok: false, status: response.status, statusText: response.statusText };\n        }\n\n        const data = await response.json().catch(() => null);\n\n        const dfid = data?.data?.dfid;\n        if (typeof dfid !== 'string' || !dfid) {\n            return { ok: false, error: 'no_dfid', data };\n        }\n\n        return { ok: true, data, dfid };\n    } catch (error) {\n        if (error?.name === 'AbortError') return { ok: false, error: 'timeout' };\n        return { ok: false, error: error?.message || String(error) };\n    } finally {\n        clearTimeout(timeoutId);\n    }\n}\n"
  },
  {
    "path": "src/utils/i18n.js",
    "content": "import { createI18n } from 'vue-i18n';\nimport en from '../language/en.json';\nimport ja from '../language/ja.json';\nimport ko from '../language/ko.json';\nimport ru from '../language/ru.json';\nimport zh_CN from '../language/zh-CN.json';\nimport zh_TW from '../language/zh-TW.json';\n\nconst messages = {\n  en,\n  ja,\n  ko,\n  ru,\n  'zh-CN': zh_CN,\n  'zh-TW': zh_TW,\n};\n\nconst getBrowserLocale = () => {\n  const browserLang = navigator.language;\n  if (browserLang.startsWith('zh')) {\n    if (browserLang === 'zh-TW' || browserLang === 'zh-HK') {\n      return 'zh-TW';\n    }\n    return 'zh-CN';\n  }\n  const lang = browserLang.split('-')[0];\n  return Object.keys(messages).includes(lang) ? lang : 'ja';\n};\n\nconst defaultLocale = JSON.parse(localStorage.getItem('settings'))?.['language'] || getBrowserLocale();\n\nconst i18n = createI18n({\n  locale: defaultLocale,\n  fallbackLocale: 'zh-CN',\n  messages,\n});\n\nexport default i18n;"
  },
  {
    "path": "src/utils/request.js",
    "content": "// src/services/request.js\nimport axios from 'axios';\nimport { MoeAuthStore } from '../stores/store';\nimport { getApiBaseUrl } from './apiBaseUrl';\n\n// 创建一个 axios 实例\nconst httpClient = axios.create({\n    baseURL: getApiBaseUrl(),\n    timeout: 10000,\n    headers: {\n        'Content-Type': 'application/json',\n    },\n    withCredentials: true,\n});\n\n// 请求拦截器\nhttpClient.interceptors.request.use(\n    config => {\n        const MoeAuth = MoeAuthStore();\n        const token = MoeAuth.UserInfo?.token;\n        const userid = MoeAuth.UserInfo?.userid;\n        const t1 = MoeAuth.UserInfo?.t1;\n        const dfid = MoeAuth.Device?.dfid;\n        const mid = MoeAuth.Device?.mid;\n        const guid = MoeAuth.Device?.guid;\n        const serverDev = MoeAuth.Device?.serverDev;\n        const mac = MoeAuth.Device?.mac;\n\n        const authParts = [];\n        if (token) authParts.push(`token=${(token)}`);\n        if (userid) authParts.push(`userid=${(userid)}`);\n        if (dfid) authParts.push(`dfid=${(dfid)}`);\n        if (t1) authParts.push(`t1=${(t1)}`);\n        if (mid) authParts.push(`KUGOU_API_MID=${(mid)}`);\n        if (guid) authParts.push(`KUGOU_API_GUID=${(guid)}`);\n        if (serverDev) authParts.push(`KUGOU_API_DEV=${(serverDev)}`);\n        if (mac) authParts.push(`KUGOU_API_MAC=${(mac)}`);\n\n        if (authParts.length > 0) {\n            config.headers = {\n                ...config.headers,\n                Authorization: authParts.join(';')\n            };\n        }\n        return config;\n    },\n    error => Promise.reject(error)\n);\n\n// 响应拦截器\nhttpClient.interceptors.response.use(\n    response => {\n        return response.data;\n    },\n    error => {\n        if (error.response) {\n            console.error(`http error status:${error.response.status}`,error.response.data);\n            if (error.response?.data?.data) {\n                console.error(error.response.data.data);\n            // } else {\n            //     $message.error('服务器错误,请稍后再试!');\n            }\n        } else if (error.request) {\n            console.error('No response received:', error.request);\n            $message.error('服务器未响应,请稍后再试!');\n        } else {\n            console.error('Error:', error.message);\n            $message.error('请求错误,请稍后再试!');\n        }\n        return Promise.reject(error);\n    }\n);\n\n// 封装 GET 请求\nexport const get = async (url, params = {}, config = {}, onSuccess = null, onError = null) => {\n    try {\n        const response = await httpClient.get(url, { params, ...config });\n        if (onSuccess) onSuccess(response);\n        return response;\n    } catch (error) {\n        if (onError) onError(error);\n        throw error;\n    }\n};\n\n// 封装 POST 请求\nexport const post = async (url, data = {}, config = {}, onSuccess = null, onError = null) => {\n    try {\n        const response = await httpClient.post(url, data, config);\n        if (onSuccess) onSuccess(response);\n        return response;\n    } catch (error) {\n        if (onError) onError(error);\n        throw error;\n    }\n};\n\n// 封装 PUT 请求\nexport const put = async (url, data = {}, config = {}, onSuccess = null, onError = null) => {\n    try {\n        const response = await httpClient.put(url, data, config);\n        if (onSuccess) onSuccess(response);\n        return response;\n    } catch (error) {\n        if (onError) onError(error);\n        throw error;\n    }\n};\n\n// 封装 DELETE 请求\nexport const del = async (url, config = {}, onSuccess = null, onError = null) => {\n    try {\n        const response = await httpClient.delete(url, config);\n        if (onSuccess) onSuccess(response);\n        return response;\n    } catch (error) {\n        if (onError) onError(error);\n        throw error;\n    }\n};\n\n// 封装 PATCH 请求\nexport const patch = async (url, data = {}, config = {}, onSuccess = null, onError = null) => {\n    try {\n        const response = await httpClient.patch(url, data, config);\n        if (onSuccess) onSuccess(response);\n        return response;\n    } catch (error) {\n        if (onError) onError(error);\n        throw error;\n    }\n};\n\n// 封装上传图片请求\nexport const uploadImage = async (url, file, additionalData = {}, config = {}, onSuccess = null, onError = null) => {\n    try {\n        const formData = new FormData();\n        formData.append('file', file);\n\n        // 如果有其他数据（如关联的商品信息等），也可以添加到 formData\n        for (const key in additionalData) {\n            if (Object.prototype.hasOwnProperty.call(additionalData, key)) {\n                formData.append(key, additionalData[key]);\n            }\n        }\n\n        // 需要确保 Content-Type 被设置为 multipart/form-data\n        const response = await httpClient.post(url, formData, {\n            ...config,\n            headers: {\n                ...config.headers,\n                'Content-Type': 'multipart/form-data'\n            }\n        });\n\n        if (onSuccess) onSuccess(response);\n        return response;\n    } catch (error) {\n        if (onError) onError(error);\n        throw error;\n    }\n};\n\n// 导出 httpClient 以便在需要的时候直接使用 axios 实例\nexport default httpClient;"
  },
  {
    "path": "src/utils/utils.js",
    "content": "import i18n from '@/utils/i18n';\n\nexport const applyColorTheme = (theme) => {\n    let colors;\n    if (theme === 'blue') {\n        colors = {\n            '--primary-color': '#4A90E2',\n            '--secondary-color': '#AEDFF7',\n            '--background-color': '#E8F4FA',\n            '--background-color-secondary': '#D9EEFA',\n            '--color-primary': '#2A6DAF',\n            '--color-primary-light': 'rgba(74, 144, 226, 0.1)',\n            '--border-color': '#C5E0F5',\n            '--hover-color': '#D1E9F9',\n            '--color-secondary-bg-for-transparent': 'rgba(174, 223, 247, 0.28)',\n            '--color-box-shadow': 'rgba(74, 144, 226, 0.2)',\n        };\n    } else if (theme === 'green') {\n        colors = {\n            '--primary-color': '#34C759',\n            '--secondary-color': '#A7F3D0',\n            '--background-color': '#E5F9F0',\n            '--background-color-secondary': '#D0F5E6',\n            '--color-primary': '#28A745',\n            '--color-primary-light': 'rgba(52, 199, 89, 0.1)',\n            '--border-color': '#B8ECD7',\n            '--hover-color': '#C9F2E2',\n            '--color-secondary-bg-for-transparent': 'rgba(167, 243, 208, 0.28)',\n            '--color-box-shadow': 'rgba(52, 199, 89, 0.2)',\n        };\n    } else if (theme === 'orange') {\n        colors = {\n            '--primary-color': '#ff6b6b',\n            '--secondary-color': '#FFB6C1',\n            '--background-color': '#FFF0F5',\n            '--background-color-secondary': '#FFE6EC',\n            '--color-primary': '#ea33e4',\n            '--color-primary-light': 'rgba(255, 107, 107, 0.1)',\n            '--border-color': '#FFDCE3',\n            '--hover-color': '#FFE9EF',\n            '--color-secondary-bg-for-transparent': 'rgba(209, 209, 214, 0.28)',\n            '--color-box-shadow': 'rgba(255, 105, 180, 0.2)',\n        };\n    } else {\n        colors = {\n            '--primary-color': '#FF69B4',\n            '--secondary-color': '#FFB6C1',\n            '--background-color': '#FFF0F5',\n            '--background-color-secondary': '#FFE6F0',\n            '--color-primary': '#ea33e4',\n            '--color-primary-light': 'rgba(255, 105, 180, 0.1)',\n            '--border-color': '#FFD9E6',\n            '--hover-color': '#FFE9F2',\n            '--color-secondary-bg-for-transparent': 'rgba(209, 209, 214, 0.28)',\n            '--color-box-shadow': 'rgba(255, 105, 180, 0.2)',\n        };\n    }\n\n    Object.keys(colors).forEach(key => {\n        document.documentElement.style.setProperty(key, colors[key]);\n    });\n};\n\n\nexport const getCover = (coverUrl, size) => {\n    if (!coverUrl) return './assets/images/ico.png';\n    return coverUrl.replace(\"{size}\", size);\n};\n\nexport const formatMilliseconds = (time) => {\n    const milliseconds = time > 3600 ? time : time * 1000;\n    const totalSeconds = Math.floor(milliseconds / 1000);\n    const minutes = Math.floor(totalSeconds / 60);\n    const seconds = totalSeconds % 60;\n    return `${minutes}分${seconds}秒`;\n};\n\nexport const requestMicrophonePermission = async () => {\n    if (typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia) return false;\n\n    try {\n        if (navigator.permissions?.query) {\n            const status = await navigator.permissions.query({ name: 'microphone' });\n\n            if (status.state === 'granted') {\n                // 不会弹窗\n                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n                stream.getTracks().forEach(track => track.stop());\n                return true;\n            }\n\n            if (status.state === 'denied') return false;\n        }\n    } catch {\n        // permissions API 在部分环境不可用/会抛错（例如 Safari），直接走 getUserMedia\n    }\n\n    try {\n        // 可能弹窗申请权限\n        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });\n        stream.getTracks().forEach(track => track.stop());\n        return true;\n    } catch {\n        return false;\n    }\n};\n\nexport const getAudioOutputDeviceSignature = async () => {\n    if (typeof navigator === 'undefined' || !navigator.mediaDevices?.enumerateDevices) return null;\n    const devices = await navigator.mediaDevices.enumerateDevices();\n    const signatures = devices\n        .filter(device => device.kind === 'audiooutput')\n        .map(device => `${device.deviceId || ''}:${device.groupId || ''}`)\n        .sort();\n    return signatures.join('|');\n};\n\nlet themeMediaQueryListener = null;\nexport const setTheme = (theme) => {\n    const html = document.documentElement;\n    const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');\n\n    if (themeMediaQueryListener) {\n        prefersDarkScheme.removeEventListener('change', themeMediaQueryListener);\n        themeMediaQueryListener = null;\n    }\n\n    const applyTheme = (isDark) => {\n        if (isDark) {\n            html.classList.add('dark');\n        } else {\n            html.classList.remove('dark');\n        }\n    };\n\n    switch (theme) {\n        case 'dark':\n            applyTheme(true);\n            localStorage.setItem('theme', 'dark');\n            break;\n        case 'light':\n            applyTheme(false);\n            localStorage.setItem('theme', 'light');\n            break;\n        case 'auto':\n            localStorage.setItem('theme', 'auto');\n            applyTheme(prefersDarkScheme.matches);\n            themeMediaQueryListener = (e) => {\n                applyTheme(e.matches);\n            };\n            prefersDarkScheme.addEventListener('change', themeMediaQueryListener);\n            break;\n    }\n};\n\nexport const openRegisterUrl = (registerUrl) => {\n    if (window.electron) {\n        window.electron.ipcRenderer.send('open-url', registerUrl);\n    } else {\n        window.open(registerUrl, '_blank');\n    }\n};\n\n// 分享\nimport { MoeAuthStore } from '../stores/store';\nexport const share = (songName, id, type = 0, songDesc = '') => {\n    let text = '';\n    const MoeAuth = MoeAuthStore();\n    let userName = '萌音';\n    if(MoeAuth.isAuthenticated) {\n        userName = MoeAuth.UserInfo?.nickname || '萌音';\n    };\n    // 客户端分享\n    let shareUrl = '';\n    if (window.electron) {\n        if(type == 0){\n            // 歌曲\n            shareUrl = `https://music.moekoe.cn/share/?hash=${id}`;\n        }else{\n            // 歌单\n            shareUrl = `moekoe://share?listid=${id}`;\n        }\n    } else {\n        //  Web / H5 逻辑\n        shareUrl = (window.location.host + '/#/') + (type == 0 ? `share/?hash=${id}` : `share?listid=${id}`);\n    }\n    text = `你的好友@${userName}分享了${songDesc}《${songName}》给你,快去听听吧! ${shareUrl}`;\n\n    navigator.clipboard.writeText(text);\n    $message.success(\n        i18n.global.t('kou-ling-yi-fu-zhi,kuai-ba-ge-qu-fen-xiang-gei-peng-you-ba')\n    );\n};\n"
  },
  {
    "path": "src/views/CloudDrive.vue",
    "content": "<template>\n    <div class=\"detail-page\">\n        <!-- 头部信息区域 -->\n        <div class=\"header\">\n            <img class=\"cover-art\" :src=\"`./assets/images/cloud.png`\" />\n            <div class=\"info\">\n                <h1 class=\"title\">{{ $t('wo-de-yun-pan') }}</h1>\n                <p class=\"subtitle\">{{ $t('yun-pan-ge-qu-shu') }}: {{ tracks.length }}</p>\n                <div class=\"storage-info\" v-if=\"storageInfo.totalSize > 0\">\n                    <div class=\"storage-progress\">\n                        <div class=\"storage-progress-bar\" :style=\"{width: (storageInfo.usedSize / storageInfo.totalSize * 100) + '%'}\"></div>\n                    </div>\n                    <div class=\"storage-text\">\n                        {{ formatStorageSize(storageInfo.usedSize) }} / {{ formatStorageSize(storageInfo.totalSize) }}\n                        ({{ $t('ke-yong') }}: {{ formatStorageSize(storageInfo.availableSize) }})\n                    </div>\n                </div>\n                <div class=\"description\">{{ $t('yun-pan-miao-shu') }}</div>\n                <div class=\"actions\">\n                    <button class=\"primary-btn\" @click=\"addPlaylistToQueue($event)\">\n                        <i class=\"fas fa-play\"></i> {{ $t('bo-fang') }}\n                    </button>\n                    <button class=\"upload-btn\" @click=\"uploadMusic\">\n                        <i class=\"fas fa-upload\"></i> {{ $t('shang-chuan-yin-le') }}\n                    </button>\n                </div>\n            </div>\n        </div>\n\n        <!-- 导航按钮 -->\n        <i class=\"location-arrow fas fa-location-arrow\" @click=\"scrollToItem\" :title=\"t('dang-qian-bo-fang-ge-qu')\"></i>\n        <img :src=\"`./assets/images/lemon.gif`\" class=\"scroll-bottom-img\" @click=\"scrollToFirstItem\" :title=\"t('fan-hui-ding-bu')\"/>\n\n        <!-- 歌曲列表 -->\n        <div class=\"track-list-container\">\n            <div class=\"track-list-header\">\n                <h2 class=\"track-list-title\"><span>{{ $t('yun-pan-ge-qu') }}</span> ( {{ tracks.length }} )</h2>\n                <div class=\"track-list-actions\">\n                    <div class=\"batch-action-container\">\n                        <button class=\"batch-action-btn\" @click=\"toggleBatchSelection\" :class=\"{ 'active': batchSelectionMode }\">\n                            <input type=\"checkbox\" v-model=\"batchSelectionMode\" /> {{ $t('pi-liang-cao-zuo') }}\n                            <span v-if=\"selectedTracks.length > 0\" class=\"selected-count\">{{ selectedTracks.length }}</span>\n                        </button>\n                        <div v-if=\"batchSelectionMode && isBatchMenuVisible && selectedTracks.length > 0\" class=\"batch-actions-menu\">\n                            <ul>\n                                <li @click=\"appendSelectedToQueue\"><i class=\"fas fa-list\"></i> 添加到播放列表</li>\n                                <li @click=\"deleteSelectedFromCloud\"><i class=\"fas fa-trash-alt\"></i> {{ $t('cong-yun-pan-shan-chu') }}</li>\n                            </ul>\n                        </div>\n                    </div>\n                    <button class=\"view-mode-btn\" @click=\"toggleListMode\" :title=\"listMode === 'list' ? '切换到网格视图' : '切换到列表视图'\">\n                        <i class=\"fas\" :class=\"listMode === 'list' ? 'fa-th' : 'fa-list'\"></i>\n                    </button>\n                    <input type=\"text\" v-model=\"searchQuery\" @keyup.enter=\"searchTracks\" :placeholder=\"t('sou-suo-ge-qu')\" class=\"search-input\" />\n                </div>\n            </div>\n\n            <!-- 表头 -->\n            <div class=\"track-list-header-row\">\n                <div class=\"track-checkbox-header\" v-if=\"batchSelectionMode\">\n                    <input type=\"checkbox\" :checked=\"isAllSelected\" @click=\"toggleSelectAll\">\n                </div>\n                <div class=\"track-number-header\" v-else>♪</div>\n                <div class=\"track-title-header\" @click=\"sortTracks('name')\">\n                    文件名 <i class=\"fas\" :class=\"getSortIconClass('name')\"></i>\n                </div>\n                <div class=\"track-artist-header\" @click=\"sortTracks('author')\">\n                    歌手 <i class=\"fas\" :class=\"getSortIconClass('author')\"></i>\n                </div>\n                <div class=\"track-size-header\" @click=\"sortTracks('size')\">\n                    文件大小 <i class=\"fas\" :class=\"getSortIconClass('size')\"></i>\n                </div>\n                <div class=\"track-timelen-header\" @click=\"sortTracks('timelen')\">\n                    时间 <i class=\"fas\" :class=\"getSortIconClass('timelen')\"></i>\n                </div>\n            </div>\n\n            <RecycleScroller ref=\"recycleScrollerRef\" :items=\"filteredTracks\" :item-size=\"listMode === 'list' ? 50 : 70\" class=\"track-list\" key-field=\"hash\">\n                <template #default=\"{ item, index }\">\n                    <div class=\"li\" :key=\"item.hash\"\n                        :class=\"{ 'cover-view': listMode === 'grid', 'selected': selectedTracks.includes(index) }\"\n                        @click=\"batchSelectionMode ? selectTrack(index, $event) : playSong(item.hash, item.name, item.author, item.timelen, item.cover)\">\n                        \n                        <!-- 复选框或序号 -->\n                        <div class=\"track-checkbox\" v-if=\"batchSelectionMode\">\n                            <input type=\"checkbox\" :checked=\"selectedTracks.includes(index)\" @click.stop=\"selectTrack(index, $event)\">\n                        </div>\n                        <div class=\"track-number\" v-else>{{ index + 1 }}</div>\n\n                        <!-- 网格模式封面 -->\n                        <div class=\"track-cover\" v-if=\"listMode === 'grid'\">\n                            <img :src=\"item.cover || './assets/images/ico.png'\" alt=\"Cover\">\n                            <div class=\"track-cover-overlay\" :class=\"{ 'playing': props.playerControl?.currentSong.hash == item.hash }\">\n                                <i :class=\"props.playerControl?.currentSong.hash == item.hash ? 'fas fa-music' : 'fas fa-play'\"></i>\n                            </div>\n                        </div>\n\n                        <!-- 歌曲信息 -->\n                        <div class=\"track-title\" :title=\"item.name\">{{ item.name }}\n                            <span v-if=\"item.qualityInfo\" class=\"icon\" :class=\"item.qualityInfo.class\">{{ item.qualityInfo.text }}</span>\n                        </div>\n                        <div class=\"track-artist\" :title=\"item.author\">{{ item.author }}</div>\n                        <div class=\"track-size\" :title=\"item.filesize\">{{ item.filesize }}</div>\n                        <div class=\"track-timelen\">\n                            <button v-if=\"props.playerControl?.currentSong.hash == item.hash && listMode === 'list'\" \n                                class=\"queue-play-btn fas fa-music\"></button>\n                            {{ $formatMilliseconds(item.timelen) }}\n                        </div>\n                    </div>\n                </template>\n            </RecycleScroller>\n        </div>\n\n        <div class=\"note-container\">\n            <transition-group name=\"fly-note\">\n                <div v-for=\"note in flyingNotes\" :key=\"note.id\" class=\"flying-note\" :style=\"note.style\">♪</div>\n            </transition-group>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref, onMounted, onBeforeUnmount, computed } from 'vue';\nimport { RecycleScroller } from 'vue3-virtual-scroller';\nimport { get } from '../utils/request';\nimport { useRouter } from 'vue-router';\nimport { MoeAuthStore } from '../stores/store';\nimport { useI18n } from 'vue-i18n';\n\n\nconst { t } = useI18n();\nconst MoeAuth = MoeAuthStore();\nconst router = useRouter();\n\n// 通用状态\nconst tracks = ref([]);\nconst filteredTracks = ref([]);\nconst searchQuery = ref('');\nconst pageSize = ref(100);\nconst recycleScrollerRef = ref(null);\nconst loading = ref(true);\nconst flyingNotes = ref([]);\nlet noteId = 0;\n\n// 云盘存储空间信息\nconst storageInfo = ref({\n    totalSize: 0,\n    usedSize: 0,\n    availableSize: 0\n});\n\n// 批量选择相关状态\nconst batchSelectionMode = ref(false);\nconst isBatchMenuVisible = ref(false);\nconst selectedTracks = ref([]);\nlet lastSelectedIndex = -1;\n\n// 排序状态\nconst sortField = ref('');\nconst sortOrder = ref('asc');\n\n// 列表模式状态\nconst listMode = ref(localStorage.getItem('cloudDriveListMode') || 'list');\n\n// 判断是否全选\nconst isAllSelected = computed(() => {\n    return selectedTracks.value.length === filteredTracks.value.length && filteredTracks.value.length > 0;\n});\n\nconst props = defineProps({\n    playerControl: Object\n});\n\nonMounted(() => {\n    loadData();\n    document.addEventListener('click', handleClickOutside);\n});\n\nonBeforeUnmount(() => {\n    document.removeEventListener('click', handleClickOutside);\n});\n\nconst loadData = async () => {\n    if (!MoeAuth.isAuthenticated) {\n        router.push('/login');\n        return;\n    }\n    await fetchCloudTracks();\n};\n\n// 获取云盘歌曲\nconst fetchCloudTracks = async () => {\n    let allTracks = [];\n    let currentPage = 1;\n    \n    try {\n        const firstPageResponse = await get('/user/cloud', {\n            page: currentPage,\n            pagesize: pageSize.value\n        });\n        \n        if (firstPageResponse.status === 1) {\n            // 处理存储空间信息\n            if (firstPageResponse.data.type_size) {\n                const { max_size, used_size, availble_size } = firstPageResponse.data;\n                storageInfo.value = {\n                    totalSize: max_size || 0,\n                    usedSize: used_size || 0,\n                    availableSize: availble_size || 0\n                };\n            }\n            \n            // 处理歌曲列表\n            const songList = firstPageResponse.data.list || firstPageResponse.data.info || [];\n            allTracks = formatTrackList(songList);\n            tracks.value = allTracks;\n            filteredTracks.value = allTracks;\n            currentPage++;\n            \n            // 获取剩余页面数据\n            if (firstPageResponse.data.list_count > pageSize.value) {\n                const totalPages = Math.ceil(firstPageResponse.data.list_count / pageSize.value);\n                for (let i = 1; i < totalPages && currentPage <= totalPages; i++) {\n                    const nextPageData = await fetchCloudPage(currentPage);\n                    if (!nextPageData || nextPageData.length === 0) break;\n                    \n                    allTracks = allTracks.concat(nextPageData);\n                    tracks.value = allTracks;\n                    filteredTracks.value = allTracks;\n                    currentPage++;\n                }\n            }\n        }\n    } catch (error) {\n        $message.error(t('ge-qu-shu-ju-cuo-wu'));\n        console.error('获取云盘歌曲失败:', error);\n    } finally {\n        loading.value = false;\n    }\n};\n\n// 获取单页云盘数据\nconst fetchCloudPage = async (page) => {\n    try {\n        const response = await get('/user/cloud', {\n            page,\n            pagesize: pageSize.value\n        });\n        \n        if (response.status === 1) {\n            const songList = response.data.list || response.data.info || [];\n            return formatTrackList(songList);\n        }\n    } catch (error) {\n        console.error('获取更多云盘歌曲失败:', error);\n    }\n    return [];\n};\n\n// 获取音质信息\nconst getQualityInfo = (bitrate) => {\n    switch(bitrate) {\n        case 3:\n            return { text: 'HQ', class: 'hq-icon' };\n        case 4:\n            return { text: 'SQ', class: 'sq-icon' };\n        case 5:\n            return { text: 'HR', class: 'hr-icon' };\n        default:\n            return null;\n    }\n};\n\n// 格式化歌曲列表数据\nconst formatTrackList = (songList) => {\n    return songList.map(track => {\n        const qualityInfo = getQualityInfo(track.bitrate || 0);\n        return {\n            hash: track.hash || '',\n            OriSongName: track.filename || '',\n            name: track.name,\n            author: track.author_name || '云盘音乐',\n            album: track.album_name || '云盘音乐',\n            timelen: track.timelen || 0,\n            qualityInfo: qualityInfo,\n            filesize: formatStorageSize(track.size) || 0,\n            bitrate: track.bitrate || 0,\n            cover: track?.album_info?.sizable_cover?.replace(\"{size}\", 480) || track?.authors?.[0]?.sizable_avatar?.replace(\"{size}\", 480)\n        };\n    });\n};\n\n// 切换列表模式\nconst toggleListMode = () => {\n    listMode.value = listMode.value === 'list' ? 'grid' : 'list';\n    localStorage.setItem('cloudDriveListMode', listMode.value);\n};\n\n// 格式化存储空间大小\nconst formatStorageSize = (bytes) => {\n    if (!bytes || bytes === 0) return '0 B';\n    const k = 1024;\n    const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\n    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n};\n\n// 搜索歌曲\nconst searchTracks = () => {\n    filteredTracks.value = tracks.value.filter(track => \n        track.name.toLowerCase().trim().includes(searchQuery.value.toLowerCase().trim()) ||\n        track.author.toLowerCase().trim().includes(searchQuery.value.toLowerCase().trim())\n    );\n};\n\n// 播放歌曲\nconst playSong = async (hash, name, author, timeLength, cover) => {\n    name = name && name.includes(' - ') ? name.split(' - ')[1] : name;\n    props.playerControl.addCloudMusicToQueue(hash, name, author, timeLength, cover);\n};\n\n// 添加整个播放列表到队列\nconst addPlaylistToQueue = async (event, append = false) => {\n    const playButton = event.currentTarget;\n    const rect = playButton.getBoundingClientRect();\n    const note = {\n        id: noteId++,\n        style: {\n            '--start-x': `${rect.left + rect.width/2}px`,\n            '--start-y': `${rect.top + rect.height/2}px`,\n            'left': '0',\n            'top': '0'\n        }\n    };\n    flyingNotes.value.push(note);\n    setTimeout(() => {\n        flyingNotes.value = flyingNotes.value.filter(n => n.id !== note.id);\n    }, 1500);\n    props.playerControl.addCloudPlaylistToQueue(filteredTracks.value, append);\n};\n\nconst uploadMusic = () => {\n    $message.info('上传功能正在开发中...');\n};\n\n// 滚动到当前播放歌曲\nconst scrollToItem = () => {\n    const currentIndex = filteredTracks.value.findIndex(song => song.hash === props.playerControl.currentSong.hash);\n    if (currentIndex !== -1) {\n        recycleScrollerRef.value.scrollToItem(currentIndex - 3, { behavior: 'smooth' });\n    }\n};\n\n// 滚动到顶部\nconst scrollToFirstItem = () => {\n    recycleScrollerRef.value.scrollToItem(0, { behavior: 'smooth' });\n    window.scrollTo({\n        top: 0,\n        behavior: 'smooth',\n        scrollSource: 'manual-button-click' \n    });\n};\n\nconst handleClickOutside = (event) => {\n    const batchActionsMenu = document.querySelector('.batch-actions-menu');\n    const batchActionBtn = document.querySelector('.batch-action-btn');\n    if (batchActionsMenu && !batchActionsMenu.contains(event.target) && !batchActionBtn.contains(event.target)) {\n        isBatchMenuVisible.value = false;\n    }\n};\n\n// 切换批量选择模式\nconst toggleBatchSelection = () => {\n    if (batchSelectionMode.value) {\n        // 如果已经在批量选择模式，则切换菜单显示或退出模式\n        if (isBatchMenuVisible.value) {\n            // 如果菜单已经显示，则点击后退出批量选择模式\n            batchSelectionMode.value = false;\n            isBatchMenuVisible.value = false;\n            selectedTracks.value = [];\n            lastSelectedIndex = -1;\n        } else {\n            // 如果菜单未显示，则显示菜单\n            isBatchMenuVisible.value = true;\n        }\n    } else {\n        // 首次进入批量选择模式\n        batchSelectionMode.value = true;\n        isBatchMenuVisible.value = false;\n    }\n};\n\n// 选择/取消选择歌曲\nconst selectTrack = (index, event) => {\n    if (event.shiftKey && lastSelectedIndex !== -1) {\n        // Shift 键多选\n        const start = Math.min(lastSelectedIndex, index);\n        const end = Math.max(lastSelectedIndex, index);\n        \n        for (let i = start; i <= end; i++) {\n            if (!selectedTracks.value.includes(i)) {\n                selectedTracks.value.push(i);\n            }\n        }\n    } else if (event.ctrlKey || event.metaKey) {\n        // Ctrl/Cmd 键选择性多选\n        const existingIndex = selectedTracks.value.indexOf(index);\n        if (existingIndex === -1) {\n            selectedTracks.value.push(index);\n        } else {\n            selectedTracks.value.splice(existingIndex, 1);\n        }\n    } else {\n        // 普通点击\n        const existingIndex = selectedTracks.value.indexOf(index);\n        if (existingIndex === -1) {\n            selectedTracks.value = [index];\n        } else {\n            selectedTracks.value = [];\n        }\n    }\n    \n    lastSelectedIndex = index;\n};\n\n// 将选中歌曲添加到播放队列（追加到当前队列）\nconst appendSelectedToQueue = async () => {\n    if (selectedTracks.value.length === 0) return;\n    const selectedSongs = selectedTracks.value.map(index => filteredTracks.value[index]);\n    await props.playerControl.addCloudPlaylistToQueue(selectedSongs, true);\n    $message.success(t('tian-jia-dao-bo-fang-lie-biao-cheng-gong'));\n    isBatchMenuVisible.value = false;\n};\n\n// 从云盘中删除选中的歌曲\nconst deleteSelectedFromCloud = async () => {\n    if (selectedTracks.value.length === 0) return;\n    const result = await window.$modal.confirm(t('que-ren-shan-chu-yun-pan-ge-qu'));\n    if (result) {\n        $message.info('删除功能正在开发中...');\n        \n        // selectedTracks.value.sort((a, b) => b - a).forEach(index => {\n        //     filteredTracks.value.splice(index, 1);\n        //     tracks.value = tracks.value.filter((_, i) => \n        //         !selectedTracks.value.includes(i)\n        //     );\n        // });\n        // filteredTracks.value = [...tracks.value];\n        // selectedTracks.value = [];\n        // $message.success(t('shan-chu-cheng-gong'));\n    }\n    isBatchMenuVisible.value = false;\n};\n\n// 切换全选/取消全选\nconst toggleSelectAll = () => {\n    if (isAllSelected.value) {\n        selectedTracks.value = [];\n    } else {\n        selectedTracks.value = Array.from({ length: filteredTracks.value.length }, (_, i) => i);\n    }\n};\n\n// 根据字段排序\nconst sortTracks = (field) => {\n    if (sortField.value === field) {\n        sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';\n    } else {\n        sortField.value = field;\n        sortOrder.value = 'asc';\n    }\n    \n    filteredTracks.value = [...filteredTracks.value].sort((a, b) => {\n        let valueA, valueB;\n        \n        if (field === 'timelen') {\n            valueA = a[field] || 0;\n            valueB = b[field] || 0;\n        } else if (field === 'size') {\n            const parseSize = (sizeStr) => {\n                if (!sizeStr) return 0;\n                const match = sizeStr.match(/^([\\d.]+)\\s*([KMGTP]?B)$/i);\n                if (!match) return 0;\n                const [, num, unit] = match;\n                const value = parseFloat(num);\n                const units = { 'B': 1, 'KB': 1024, 'MB': 1024 * 1024, 'GB': 1024 * 1024 * 1024, 'TB': 1024 * 1024 * 1024 * 1024 };\n                return value * (units[unit.toUpperCase()] || 1);\n            };\n            valueA = parseSize(a.filesize);\n            valueB = parseSize(b.filesize);\n        } else {\n            valueA = (a[field] || '').toLowerCase();\n            valueB = (b[field] || '').toLowerCase();\n        }\n        \n        if (sortOrder.value === 'asc') {\n            return valueA > valueB ? 1 : -1;\n        } else {\n            return valueA < valueB ? 1 : -1;\n        }\n    });\n    \n    if (batchSelectionMode.value) {\n        selectedTracks.value = [];\n    }\n};\n\nconst getSortIconClass = (field) => {\n    if (sortField.value !== field) {\n        return 'fa-sort';\n    }\n    return sortOrder.value === 'asc' ? 'fa-sort-up' : 'fa-sort-down';\n};\n\n\n</script>\n\n<style scoped>\n.detail-page {\n    padding: 20px;\n}\n\n/* 头部样式 */\n.header {\n    display: flex;\n    align-items: center;\n    margin-bottom: 40px;\n}\n\n.cover-art {\n    width: 200px;\n    height: 200px;\n    border-radius: 10px;\n    margin-right: 20px;\n    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\n    object-fit: cover;\n}\n\n.info {\n    max-width: 600px;\n}\n\n.title {\n    font-size: 36px;\n    font-weight: bold;\n    width: 800px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    margin: 0;\n    color: var(--primary-color);\n}\n\n.subtitle {\n    font-size: 18px;\n    color: #666;\n}\n\n.storage-info {\n    margin: 10px 0;\n    width: 100%;\n    max-width: 600px;\n}\n\n.storage-progress {\n    height: 6px;\n    background-color: #e0e0e0;\n    border-radius: 3px;\n    overflow: hidden;\n    margin-bottom: 5px;\n}\n\n.storage-progress-bar {\n    height: 100%;\n    background-color: var(--primary-color);\n    border-radius: 3px;\n}\n\n.storage-text {\n    font-size: 14px;\n    color: #666;\n    display: flex;\n    justify-content: space-between;\n}\n\n.description {\n    white-space: pre-wrap;\n    line-height: 1.6;\n    color: var(--text-color);\n    margin-bottom: 20px;\n    font-size: 16px;\n    max-height: 200px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: break-spaces;\n    overflow-y: auto;\n}\n\n.actions {\n    display: flex;\n    gap: 10px;\n}\n\n.primary-btn, .upload-btn {\n    background-color: #ff69b4;\n    color: white;\n    border: none;\n    padding: 10px 20px;\n    border-radius: 5px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n}\n\n.upload-btn {\n    background-color: #4CAF50;\n}\n\n.primary-btn i, .upload-btn i {\n    margin-right: 5px;\n}\n\n/* 歌曲列表样式 */\n.track-list-container {\n    margin-top: 30px;\n}\n\n.track-list-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 10px;\n}\n\n.track-list-title {\n    font-size: 24px;\n    font-weight: bold;\n    margin-bottom: 10px;\n    color: var(--primary-color);\n}\n\n/* 搜索和批量操作按钮 */\n.track-list-actions {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n}\n\n.batch-action-container {\n    position: relative;\n}\n\n.batch-action-btn {\n    background-color: transparent;\n    border: 1px solid var(--secondary-color);\n    padding: 5px 10px;\n    border-radius: 5px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: var(--text-color);\n    position: relative;\n}\n\n.batch-action-btn.active {\n    background-color: var(--primary-color);\n    color: white;\n}\n\n.selected-count {\n    position: absolute;\n    top: -8px;\n    right: -8px;\n    background-color: red;\n    color: white;\n    border-radius: 50%;\n    width: 20px;\n    height: 20px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 12px;\n    font-weight: bold;\n}\n\n.batch-actions-menu {\n    position: absolute;\n    top: 100%;\n    left: 0;\n    background-color: white;\n    border: 1px solid #ccc;\n    border-radius: 5px;\n    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n    z-index: 50;\n    margin-top: 5px;\n    width: 200px;\n}\n\n.batch-actions-menu ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n}\n\n.batch-actions-menu li {\n    padding: 10px 15px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    white-space: nowrap;\n}\n\n.batch-actions-menu li i {\n    margin-right: 10px;\n    width: 16px;\n    text-align: center;\n}\n\n.batch-actions-menu li:hover {\n    background-color: #f0f0f0;\n}\n\n.search-input {\n    width: 250px;\n    padding: 8px;\n    border: 1px solid var(--secondary-color);\n    border-radius: 20px;\n    box-sizing: border-box;\n    padding-left: 15px;\n}\n\n.track-list {\n    height: 800px;\n    scrollbar-width: thin;\n    scrollbar-color: transparent transparent; \n    overflow: auto;\n}\n\n.track-list::-webkit-scrollbar {\n    width: 8px !important; \n    display: block !important;\n}\n\n.track-list:hover {\n    scrollbar-color: var(--primary-color) transparent;\n}\n\n.li {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 10px;\n    border-bottom: 1px solid #eee;\n    border-radius: 5px;\n    cursor: pointer;\n}\n\n.li:hover {\n    background-color: var(--background-color);\n}\n\n.li.selected {\n    background-color: rgba(var(--primary-color-rgb), 0.1);\n}\n\n/* 歌曲多选 */\n.track-checkbox {\n    margin-right: 10px;\n    width: 30px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.track-number {\n    font-weight: bold;\n    margin-right: 10px;\n    width: 30px;\n}\n\n.track-title {\n    flex: 2;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.track-size{\n    flex: 0.5;\n    text-align: center;\n}\n\n.track-artist {\n    flex: 1;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 0 10px;\n}\n\n.track-album {\n    flex: 1;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 0 10px;\n}\n\n.track-timelen {\n    width: 95px;\n    text-align: right;\n}\n\n.icon {\n    margin-left: 5px;\n    border: 1px solid;\n    border-radius: 5px;\n    font-size: 10px;\n    padding-left: 6px;\n    padding-right: 6px;\n}\n\n.vip-icon {\n    color: #ff6d00;\n}\n\n.hq-icon {\n    color: #0094ff;\n    border-color: #0094ff;\n}\n\n.sq-icon {\n    color: #00c853;\n    border-color: #00c853;\n}\n\n.hr-icon {\n    color: #ff6d00;\n    border-color: #ff6d00;\n}\n\n.queue-play-btn {\n    background: none;\n    border: none;\n    font-size: 16px;\n    color: var(--primary-color);\n    cursor: pointer;\n}\n\n/* 歌手简介部分 */\n.content-section {\n    margin-top: 50px;\n    border-top: 1px dotted var(--secondary-color);\n}\n\n.intro-section {\n    margin-bottom: 30px;\n}\n\n.intro-section h3 {\n    color: var(--primary-color);\n    margin-bottom: 15px;\n}\n\n.section-content {\n    white-space: pre-wrap;\n    line-height: 1.6;\n    color: var(--text-color);\n}\n\n/* 导航按钮 */\n.location-arrow {\n    position: fixed;\n    bottom: 168px;\n    right: 14px;\n    z-index: 1;\n    cursor: pointer;\n    font-size: 37px;\n    color: var(--primary-color);\n}\n\n.scroll-bottom-img {\n    position: fixed;\n    width: 60px;\n    height: 60px;\n    bottom: 110px;\n    right: 88px;\n    z-index: 1;\n    cursor: pointer;\n}\n\n/* 下拉菜单 */\n.more-btn-container {\n    position: relative;\n}\n\n.dropdown-menu {\n    position: absolute;\n    background-color: white;\n    border: 1px solid #ccc;\n    border-radius: 5px;\n    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n    top: 50px;\n    z-index: 50;\n}\n\n.dropdown-menu ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n}\n\n.dropdown-menu li {\n    padding: 10px;\n    cursor: pointer;\n}\n\n.dropdown-menu li:hover {\n    background-color: #f0f0f0;\n}\n\n/* 音符动画 */\n.note-container {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n    pointer-events: none;\n    overflow: hidden;\n}\n\n.flying-note {\n    position: absolute;\n    font-size: 36px;\n    color: var(--primary-color);\n    pointer-events: none;\n    transform-origin: center;\n}\n\n.fly-note-enter-active {\n    animation: fly-note 2s ease-out forwards;\n}\n\n.fly-note-leave-active {\n    animation: fly-note 2s ease-out forwards;\n}\n\n@keyframes fly-note {\n    0% {\n        transform: translate(var(--start-x), calc(var(--start-y) - 50px)) rotate(0deg) scale(1.2);\n        opacity: 0.9;\n    }\n    20% {\n        transform: translate(calc(var(--start-x) + 20px), calc(var(--start-y) - 70px)) rotate(45deg) scale(1.3);\n        opacity: 0.85;\n    }\n    100% {\n        transform: translate(80vw, 100vh) rotate(360deg) scale(0.6);\n        opacity: 0;\n    }\n}\n\n/* 表头样式 */\n.track-list-header-row {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 10px;\n    border-bottom: 1px solid var(--primary-color);\n    font-weight: bold;\n    background-color: rgba(var(--primary-color-rgb), 0.1);\n    border-radius: 5px 5px 0 0;\n}\n\n.track-checkbox-header {\n    margin-right: 10px;\n    width: 30px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.track-number-header {\n    font-weight: bold;\n    margin-right: 10px;\n    width: 30px;\n}\n\n.track-title-header, .track-artist-header, .track-album-header, .track-timelen-header, .track-size-header {\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n}\n\n.track-title-header {\n    flex: 2;\n}\n\n.track-size-header{\n    flex: 0.5;\n    padding: 0 10px;\n}\n\n.track-artist-header, .track-album-header {\n    flex: 1;\n    padding: 0 10px;\n}\n\n.track-timelen-header {\n    text-align: right;\n}\n\n.track-title-header i, .track-artist-header i, .track-album-header i, .track-timelen-header i {\n    margin-left: 5px;\n    font-size: 14px;\n}\n\n.track-list-header-row:hover {\n    background-color: rgba(var(--primary-color-rgb), 0.15);\n}\n\n/* 视图模式切换按钮 */\n.view-mode-btn {\n    background-color: transparent;\n    border: 1px solid var(--secondary-color);\n    padding: 5px 10px;\n    border-radius: 5px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: var(--text-color);\n    width: 36px;\n    height: 31px;\n    transition: all 0.3s ease;\n}\n\n.view-mode-btn:hover {\n    background-color: rgba(var(--primary-color-rgb), 0.1);\n}\n\n.view-mode-btn i {\n    font-size: 16px;\n}\n\n/* 网格视图样式 */\n.li.cover-view {\n    height: 60px;\n    padding: 5px 10px;\n    display: flex;\n    align-items: center;\n    border-bottom: 1px solid #eee;\n    border-radius: 5px;\n}\n\n.li.cover-view:hover {\n    background-color: var(--background-color);\n}\n\n.track-cover {\n    position: relative;\n    width: 50px;\n    height: 50px;\n    margin-right: 15px;\n    overflow: hidden;\n    border-radius: 4px;\n    flex-shrink: 0;\n}\n\n.track-cover img {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    transition: transform 0.3s ease;\n}\n\n.li.cover-view:hover .track-cover img {\n    transform: scale(1.05);\n}\n\n.track-cover-overlay {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0,0,0,0.5);\n    opacity: 0;\n    transition: opacity 0.3s ease;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: white;\n    font-size: 20px;\n}\n\n.li.cover-view:hover .track-cover-overlay {\n    opacity: 1;\n}\n\n.track-cover-overlay.playing {\n    opacity: 1;\n}\n\n/* 调整封面视图下的其他元素样式 */\n.li.cover-view .track-title {\n    flex: 2;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.li.cover-view .track-artist {\n    flex: 1;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 0 10px;\n}\n\n.li.cover-view .track-size {\n    flex: 0.5;\n    text-align: center;\n}\n\n.li.cover-view .track-timelen {\n    width: 95px;\n    text-align: right;\n}\n\n.li.cover-view .track-checkbox,\n.li.cover-view .track-number {\n    margin-right: 10px;\n    width: 30px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n</style>\n"
  },
  {
    "path": "src/views/Discover.vue",
    "content": "<template>\n    <div class=\"discover-page\">\n        <h2 class=\"section-title\">{{ $t('fa-xian') }}</h2>\n        \n        <div class=\"category-container\">\n            <div class=\"main-categories\">\n                <button v-for=\"(category, index) in categories\" \n                    :key=\"index\" \n                    @click=\"selectMainCategory(index)\"\n                    :class=\"{ active: selectedMainCategory === index }\">\n                    {{ category.tag_name }}\n                </button>\n            </div>\n            \n            <div class=\"sub-categories\">\n                <button v-for=\"(tab, index) in currentSubCategories\" \n                    :key=\"index\" \n                    @click=\"selectSubCategory(index)\"\n                    :class=\"{ active: selectedSubCategory === index }\"\n                    :tag_id=\"tab.tag_id\">\n                    {{ tab.tag_name }}\n                </button>\n            </div>\n        </div>\n\n        <div v-if=\"isLoading\" class=\"skeleton-grid\">\n            <div class=\"skeleton-card\" v-for=\"n in 10\" :key=\"n\">\n                <div class=\"skeleton-image\"></div>\n                <div class=\"skeleton-info\">\n                    <div class=\"skeleton-title\"></div>\n                    <div class=\"skeleton-text\"></div>\n                </div>\n            </div>\n        </div>\n        \n        <div v-else class=\"music-grid\">\n            <div class=\"music-card\" v-for=\"(playlist, index) in playlistList\" :key=\"index\">\n                <router-link :to=\"{\n                    path: '/PlaylistDetail',\n                    query: { global_collection_id: playlist.global_collection_id }\n                }\">\n                    <img :src=\"$getCover(playlist.flexible_cover, 240)\" class=\"music-image\" />\n                    <div class=\"music-info\">\n                        <h3>{{ playlist.specialname }}</h3>\n                        <p>{{ playlist.intro }}</p>\n                    </div>\n                </router-link>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref, onMounted, computed } from \"vue\";\nimport { get } from '../utils/request';\nimport { useRouter } from 'vue-router';\nconst router = useRouter();\nconst categories = ref([]); \nconst selectedMainCategory = ref(0);\nconst selectedSubCategory = ref(0);\nconst tag_id = ref(0);\nconst playlistList = ref([]);\nconst isLoading = ref(true);\nconst currentSubCategories = computed(() => {\n    if (categories.value.length === 0) return [];\n    return categories.value[selectedMainCategory.value]?.son || [];\n});\n\nonMounted(() => {\n    tags();\n});\n\nconst tags = async () => {\n    const response = await get('/playlist/tags');\n    if (response.status == 1) {\n        categories.value = response.data;\n        if (categories.value.length > 0) {\n            const query = router.currentRoute.value.query;\n            if (query.main && query.sub) {\n                selectedMainCategory.value = parseInt(query.main);\n                selectedSubCategory.value = parseInt(query.sub);\n                if (categories.value[selectedMainCategory.value]?.son?.[selectedSubCategory.value]) {\n                    tag_id.value = categories.value[selectedMainCategory.value].son[selectedSubCategory.value].tag_id;\n                }\n            } else {\n                tag_id.value = categories.value[0].son[0].tag_id;\n            }\n            playlist();\n        }\n    }\n}\n\nconst selectMainCategory = (index) => {\n    playlistList.value = [];\n    isLoading.value = true;\n    selectedMainCategory.value = index;\n    selectedSubCategory.value = 0;\n    if (currentSubCategories.value.length > 0) {\n        tag_id.value = currentSubCategories.value[0].tag_id;\n        router.replace({ \n            path: '/discover', \n            query: { \n                main: index,\n                sub: 0,\n                tag: currentSubCategories.value[0].tag_id \n            } \n        });\n        playlist();\n    }\n};\n\nconst selectSubCategory = (index) => {\n    playlistList.value = [];\n    isLoading.value = true;\n    selectedSubCategory.value = index;\n    tag_id.value = currentSubCategories.value[index].tag_id;\n    router.replace({ \n        path: '/discover', \n        query: { \n            main: selectedMainCategory.value,\n            sub: index,\n            tag: currentSubCategories.value[index].tag_id \n        } \n    });\n    playlist();\n};\n\nconst playlist = async () => {\n    const response = await get(`/top/playlist?withsong=0&category_id=${tag_id.value}`);\n    if (response.status == 1) {\n        playlistList.value = response.data.special_list\n    }\n    isLoading.value = false;\n}\n</script>\n\n<style scoped>\n.discover-page {\n    padding: 20px;\n}\n\n.section-title {\n    font-size: 28px;\n    font-weight: bold;\n    margin-bottom: 30px;\n    color: var(--primary-color);\n}\n\n.category-container {\n    margin-bottom: 30px;\n}\n\n.main-categories {\n    display: flex;\n    gap: 10px;\n    margin-bottom: 15px;\n}\n\n.sub-categories {\n    display: flex;\n    gap: 10px;\n    flex-wrap: wrap;\n    margin-bottom: 20px;\n}\n\n.main-categories button {\n    background-color: var(--secondary-color);\n    color: #fff;\n    border: none;\n    padding: 10px 20px;\n    border-radius: 20px;\n    cursor: pointer;\n    font-size: 15px;\n}\n\n.main-categories button.active {\n    background-color: var(--primary-color);\n}\n\n.sub-categories button {\n    background-color: #f5f5f5;\n    border: none;\n    padding: 8px 15px;\n    border-radius: 15px;\n    cursor: pointer;\n    font-size: 14px;\n}\n\n.sub-categories button.active {\n    background-color: var(--secondary-color);\n    color: #fff;\n}\n\n.music-grid {\n    display: flex;\n    gap: 15px;\n    flex-wrap: wrap;\n    justify-content: space-evenly;\n}\n\n.music-card {\n    background-color: #fff;\n    border-radius: 10px;\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n    transition: transform 0.3s ease, box-shadow 0.3s ease;\n    padding: 10px;\n    text-align: center;\n    width: 180px;\n}\n\n.music-card:hover {\n    transform: translateY(-5px);\n    box-shadow: 0 10px 20px var(--color-box-shadow)\n}\n\n.music-card img {\n    width: 100%;\n    border-radius: 8px;\n}\n\n.music-info h3 {\n    font-size: 16px;\n    margin: 10px 0 5px;\n}\n\n.music-info p {\n    font-size: 12px;\n    color: #666;\n    display: -webkit-box;\n    -webkit-line-clamp: 2;\n    -webkit-box-orient: vertical;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    max-height: 50px;\n    line-height: 25px;\n}\n\n.skeleton-grid {\n    display: flex;\n    gap: 15px;\n    flex-wrap: wrap;\n    justify-content: space-evenly;\n}\n\n.skeleton-card {\n    background-color: #f0f0f0;\n    border-radius: 10px;\n    padding: 10px;\n    width: 200px;\n    text-align: center;\n    height: 250px;\n}\n\n.skeleton-image {\n    width: 100%;\n    height: 200px;\n    background-color: #e0e0e0;\n    border-radius: 8px;\n}\n\n.skeleton-info {\n    margin-top: 10px;\n}\n\n.skeleton-title {\n    width: 60%;\n    height: 16px;\n    background-color: #e0e0e0;\n    margin: 10px auto;\n    border-radius: 4px;\n}\n\n.skeleton-text {\n    width: 80%;\n    height: 12px;\n    background-color: #e0e0e0;\n    margin: 5px auto;\n    border-radius: 4px;\n}\n</style>"
  },
  {
    "path": "src/views/Home.vue",
    "content": "<template>\n    <div class=\"container\">\n        <h2 class=\"section-title\">{{ $t('tui-jian') }}</h2>\n        <div class=\"recommendations\">\n            <div class=\"recommend-card gradient-background\">\n                <div class=\"radio-card\">\n                    <div class=\"radio-left\">\n                        <div class=\"disc-container\">\n                            <img :src=\"`./assets/images/home/hutao1.png`\" class=\"radio-disc\">\n                        </div>\n                        <div class=\"decorative-box\">\n                            <div class=\"music-bars\">\n                                <div class=\"bar\"></div>\n                                <div class=\"bar\"></div>\n                                <div class=\"bar\"></div>\n                                <div class=\"bar\"></div>\n                            </div>\n                        </div>\n                        <div class=\"play-button\" @click=\"playFM\"></div>\n                        <div class=\"note-container\">\n                            <transition-group name=\"fly-note\">\n                                <div v-for=\"note in flyingNotes\" :key=\"note.id\" class=\"flying-note\" :style=\"note.style\">\n                                    ♪</div>\n                            </transition-group>\n                        </div>\n                    </div>\n                    <div class=\"radio-content gradient-background\">\n                        <div class=\"radio-title\">\n                            <span class=\"heart-icon\">💖</span>\n                            MoeKoe Radio\n                            <span class=\"shuffle-icon\" @click=\"toggleMode\">{{ modeIcon }}</span>\n                        </div>\n                        <div class=\"radio-subtitle\">{{ radioSubtitle }}</div>\n                    </div>\n                </div>\n            </div>\n            <div class=\"recommend-card\">\n                <router-link :to=\"{\n                    path: '/Ranking'\n                }\" class=\"ranking-entry\">\n                    <div class=\"ranking-content\">\n                        <img :src=\"`./assets/images/home/hutao2.png`\" class=\"ranking-icon\">\n                        <h3 class=\"ranking-title\">排行榜</h3>\n                        <div class=\"ranking-description\">发现你的专属好歌</div>\n                    </div>\n                </router-link>\n            </div>\n\n            <div class=\"recommend-card\">\n                <div class=\"playlist-entry gradient-background\">\n                    <router-link :to=\"{\n                        path: '/PlaylistDetail',\n                        query: { global_collection_id: 'collection_3_25230245_24_0' }\n                    }\">\n                        <div class=\"playlist-content\">\n                            <div class=\"playlist-icon\">\n                                <img :src=\"`./assets/images/home/hutao.png`\" />\n                            </div>\n                            <div class=\"ranking-description\">送给也喜欢音乐的你</div>\n                        </div>\n                    </router-link>\n                </div>\n            </div>\n        </div>\n\n        <h2 class=\"section-title\">\n            <img :src=\"`./assets/images/home/mama.png`\" class=\"mama\" @click=\"addAllSongsToQueue\">\n            {{ $t('mei-ri-tui-jian') }}\n        </h2>\n        <div v-if=\"isLoading\" class=\"skeleton-loader\">\n            <div v-for=\"n in 16\" :key=\"n\" class=\"skeleton-item\">\n                <div class=\"skeleton-cover\"></div>\n                <div class=\"skeleton-info\">\n                    <div class=\"skeleton-line\"></div>\n                    <div class=\"skeleton-line short\"></div>\n                </div>\n            </div>\n        </div>\n        <div v-else class=\"song-list\">\n            <div class=\"song-item\" v-for=\"(song, index) in songs\" :key=\"index\"\n                @click=\"playSong(song['hash'], song.ori_audio_name, $getCover(song.sizable_cover, 480), song.author_name)\"\n                @contextmenu.prevent=\"showContextMenu($event, song)\">\n                <img :src=\"$getCover(song.sizable_cover, 64)\" :alt=\"song.ori_audio_name\" class=\"song-cover\">\n                <div class=\"song-info\">\n                    <div class=\"song-title\">{{ song.ori_audio_name }}</div>\n                    <div class=\"song-artist\">{{ song.author_name }}</div>\n                </div>\n            </div>\n        </div>\n        <h2 class=\"section-title\">{{ $t('tui-jian-ge-dan') }}</h2>\n        <div class=\"playlist-grid\">\n            <div class=\"playlist-item\" v-for=\"(playlist, index) in special_list\" :key=\"index\">\n                <router-link :to=\"{\n                    path: '/PlaylistDetail',\n                    query: { global_collection_id: playlist.global_collection_id }\n                }\">\n                    <img :src=\"$getCover(playlist.flexible_cover, 240)\" class=\"playlist-cover\">\n                    <div class=\"playlist-info\">\n                        <div class=\"playlist-title\">{{ playlist.specialname }}</div>\n                        <div class=\"playlist-description\">{{ playlist.intro }}</div>\n                    </div>\n                </router-link>\n            </div>\n        </div>\n        <ContextMenu ref=\"contextMenuRef\" :playerControl=\"playerControl\" />\n    </div>\n</template>\n\n<script setup>\nimport { ref, onMounted, computed, onUpdated } from \"vue\";\nimport { get } from '../utils/request';\nimport ContextMenu from '../components/ContextMenu.vue';\nimport { useRoute,useRouter } from 'vue-router';\nimport { getCover } from '../utils/utils';\n\nconst router = useRouter();\nconst route = useRoute();\nconst songs = ref([]);\nconst special_list = ref([]);\nconst isLoading = ref(true);\nconst playSong = (hash, name, img, author) => {\n    props.playerControl.addSongToQueue(hash, name, img, author);\n};\nconst contextMenuRef = ref(null);\nconst showContextMenu = (event, song) => {\n    if (contextMenuRef.value) {\n        contextMenuRef.value.openContextMenu(event, {\n            OriSongName: song.filename,\n            FileHash: song.hash,\n            cover: song.sizable_cover?.replace(\"{size}\", 480) || './assets/images/ico.png',\n            timeLength: song.time_length\n        });\n    }\n};\nconst props = defineProps({\n    playerControl: Object\n});\n\nconst currentMode = ref('1');\nconst modes = ['1', '2', '3', '4', '6'];\n\nconst modeIcon = computed(() => {\n    switch (currentMode.value) {\n        case '1': return '💖';\n        case '2': return '🎶';\n        case '3': return '🔥';\n        case '4': return '💎';\n        case '6': return '👑';\n        default: return '💖';\n    }\n});\n\nconst radioSubtitle = computed(() => {\n    switch (currentMode.value) {\n        case '1': return '私人专属好歌推荐';\n        case '2': return '经典怀旧金曲精选';\n        case '3': return '热门好歌随心听';\n        case '4': return '小众宝藏佳作发现';\n        case '6': return 'VIP专属音乐推荐';\n        default: return '根据你的听歌喜好推荐';\n    }\n});\n\nconst toggleMode = () => {\n    const currentIndex = modes.indexOf(currentMode.value);\n    const nextIndex = (currentIndex + 1) % modes.length;\n    currentMode.value = modes[nextIndex];\n};\n\nconst flyingNotes = ref([]);\nlet noteId = 0;\n\nconst playFM = async (event) => {\n    try {\n        const playButton = event.currentTarget;\n        const rect = playButton.getBoundingClientRect();\n        const note = {\n            id: noteId++,\n            style: {\n                '--start-x': `${rect.left + rect.width / 2}px`,\n                '--start-y': `${rect.top + rect.height / 2}px`,\n                'left': '0',\n                'top': '0'\n            }\n        };\n        flyingNotes.value.push(note);\n        setTimeout(() => {\n            flyingNotes.value = flyingNotes.value.filter(n => n.id !== note.id);\n        }, 1500);\n\n        const response = await get('/top/card', {\n            params: {\n                card_id: currentMode.value\n            }\n        });\n\n        if (response.status === 1 && response.data?.song_list?.length > 0) {\n            const newSongs = response.data.song_list.map(song => {\n                return {\n                    hash: song.hash,\n                    name: song.songname,\n                    cover: song.sizable_cover?.replace(\"{size}\", 480),\n                    author: song.author_name,\n                    timelen: song.time_length\n                }\n            })\n            props.playerControl.addPlaylistToQueue(newSongs);\n        }\n    } catch (error) {\n        console.error('FM播放出错:', error);\n    }\n};\n\nonMounted(() => {\n    recommend();\n    playlist();\n});\n\nonUpdated(async () => {\n    await new Promise(resolve => setTimeout(resolve, 1000));\n    if(!window.electron){\n        if(route.query.hash){\n            privilegeSong(route.query.hash).then(res=>{\n                if(res.status==1){\n                    const songInfo = res.data[0];\n                    playSong(songInfo.hash,songInfo.albumname,getCover(songInfo.info.image, 480),songInfo.singername)\n                    router.push('/');\n                }\n            })\n        }else if(route.query.listid){\n            router.push({\n                path: '/PlaylistDetail',\n                query: { global_collection_id: route.query.listid }\n            });\n        }\n    }\n})\n\nconst recommend = async () => {\n    const response = await get('/everyday/recommend');\n    if (response.status == 1) {\n        songs.value = response.data.song_list.sort(() => Math.random() - 0.5);\n    }\n    isLoading.value = false;\n}\n\nconst playlist = async () => {\n    const response = await get(`/top/playlist?category_id=0`);\n    if (response.status == 1) {\n        special_list.value = response.data.special_list;\n    }\n}\n\nconst privilegeSong = async (hash) => {\n    const response = await get(`/privilege/lite`,{hash:hash});\n    return response;\n}\nconst addAllSongsToQueue = () => {\n    props.playerControl.addPlaylistToQueue(songs.value.map(song => ({\n        hash: song.hash,\n        name: song.ori_audio_name,\n        cover: song.sizable_cover?.replace(\"{size}\", 480),\n        author: song.author_name,\n        timelen: song.time_length\n    })));\n};\n\n</script>\n\n<style scoped>\n.container {\n    max-width: 1400px;\n    margin: 0 auto;\n    padding: 20px;\n}\n\n.section-title {\n    font-size: 28px;\n    font-weight: bold;\n    margin-bottom: 30px;\n    color: var(--primary-color);\n}\n\n.section-title .mama{\n    position: absolute;\n    height: 40px;\n    margin-left: 117px;\n    cursor: cell;\n}\n.recommendations {\n    display: flex;\n    gap: 35px;\n    margin-bottom: 40px;\n}\n\n.recommend-card {\n    width: 400px;\n    height: 200px;\n    border-radius: 15px;\n    overflow: hidden;\n    transition: transform 0.3s ease, box-shadow 0.3s ease;\n}\n\n.recommend-card:hover {\n    transform: translateY(-5px);\n    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);\n}\n\n.recommend-image {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    border-radius: 15px;\n}\n\n.play-icon {\n    font-size: 30px;\n    color: white;\n    cursor: pointer;\n}\n\n.card-content {\n    display: flex;\n    align-items: center;\n}\n\n.song-list {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 12px;\n    margin-top: 20px;\n    justify-content: flex-start;\n}\n\n.song-item {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    width: calc(20% - 30px);\n    background-color: #fff;\n    padding: 10px;\n    border-radius: 10px;\n    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n    transition: transform 0.3s ease;\n    cursor: pointer;\n}\n\n.song-item:hover {\n    transform: translateY(-5px);\n}\n\n.song-cover {\n    width: 50px;\n    height: 50px;\n    border-radius: 5px;\n}\n\n.song-info {\n    display: flex;\n    flex-direction: column;\n    flex: 1;\n    min-width: 0;\n}\n\n.song-title {\n    font-size: 16px;\n    font-weight: bold;\n    color: var(--primary-color);\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.song-artist {\n    font-size: 14px;\n    color: #666;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.playlist-grid {\n    display: flex;\n    gap: 35px;\n    flex-wrap: wrap;\n    justify-content: space-evenly;\n}\n\n.playlist-item {\n    background-color: #fff;\n    border-radius: 10px;\n    overflow: hidden;\n    transition: transform 0.3s ease, box-shadow 0.3s ease;\n    cursor: pointer;\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n    width: calc(16.666% - 30px);\n}\n\n@media screen and (max-width: 1400px) {\n    .playlist-grid {\n        gap: 25px;\n    }\n    \n    .playlist-item {\n        width: calc(20% - 20px);\n    }\n}\n\n@media screen and (max-width: 1200px) {\n    .playlist-grid {\n        gap: 20px;\n    }\n    \n    .playlist-item {\n        width: calc(25% - 15px);\n    }\n}\n\n@media screen and (max-width: 1024px) {\n    .playlist-grid {\n        gap: 18px;\n    }\n    \n    .playlist-item {\n        width: calc(25% - 14px);\n    }\n}\n\n@media screen and (max-width: 768px) {\n    .playlist-grid {\n        gap: 15px;\n    }\n    \n    .playlist-item {\n        width: calc(33.333% - 10px);\n        min-width: 150px;\n    }\n    \n    .playlist-title {\n        font-size: 14px;\n    }\n    \n    .playlist-description {\n        font-size: 12px;\n    }\n}\n\n@media screen and (max-width: 576px) {\n    .playlist-grid {\n        gap: 12px;\n    }\n    \n    .playlist-item {\n        width: calc(50% - 6px);\n        min-width: 140px;\n    }\n}\n.playlist-item:hover {\n    transform: translateY(-5px);\n    box-shadow: 0 10px 20px var(--color-box-shadow);\n}\n\n.playlist-cover {\n    width: 100%;\n    aspect-ratio: 1;\n    object-fit: cover;\n}\n\n.playlist-info {\n    padding: 15px;\n}\n\n.playlist-title {\n    font-weight: bold;\n    margin-bottom: 5px;\n    font-size: 16px;\n    color: var(--primary-color);\n}\n\n.playlist-description {\n    color: #666;\n    font-size: 14px;\n    display: -webkit-box;\n    -webkit-line-clamp: 2;\n    -webkit-box-orient: vertical;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    max-height: 50px;\n    line-height: 25px;\n}\n\n.skeleton-loader {\n    display: flex;\n    flex-wrap: wrap;\n    justify-content: space-between;\n    margin-top: 10px;\n}\n\n.skeleton-item {\n    display: flex;\n    align-items: center;\n    margin-bottom: 10px;\n    width: 250px;\n    border-radius: 10px;\n    padding-left: 10px;\n    background-color: #f0f0f0;\n    height: 68px;\n}\n\n.skeleton-cover {\n    width: 50px;\n    height: 50px;\n    margin-right: 10px;\n    border-radius: 10px;\n    background-color: #e0e0e0;\n}\n\n.skeleton-info {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    max-width: 190px;\n}\n\n.skeleton-line {\n    height: 10px;\n    background-color: #e0e0e0;\n    margin-bottom: 5px;\n    border-radius: 5px;\n    width: 150px;\n}\n\n.radio-card {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    border-radius: 15px;\n}\n\n.radio-left {\n    flex: 0;\n    margin-top: 7px;\n    display: flex;\n    align-items: center;\n    width: 100%;\n    justify-content: space-between;\n}\n\n.disc-container {\n    position: relative;\n    order: 1;\n}\n\n.radio-disc {\n    width: 125px;\n    height: 125px;\n    object-fit: cover;\n    border-radius: 50%;\n    box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.2),\n        inset 0 0 20px rgba(0, 0, 0, 0.1),\n        0 2px 4px rgba(255, 255, 255, 0.8);\n    padding: 2px;\n}\n\n.decorative-box {\n    width: 60px;\n    height: 60px;\n    position: relative;\n    border-radius: 12px;\n    transform: perspective(500px) rotateY(-15deg);\n    transition: transform 0.3s ease;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    margin-left: 10px;\n}\n\n.music-bars {\n    display: flex;\n    align-items: flex-end;\n    gap: 3px;\n    height: 30px;\n}\n\n.bar {\n    width: 3px;\n    background: #4a90e2;\n    border-radius: 3px;\n    animation: sound-wave 1.2s ease-in-out infinite;\n}\n\n.bar:nth-child(1) {\n    height: 15px;\n    animation-delay: 0s;\n}\n\n.bar:nth-child(2) {\n    height: 20px;\n    animation-delay: 0.2s;\n}\n\n.bar:nth-child(3) {\n    height: 12px;\n    animation-delay: 0.4s;\n}\n\n.bar:nth-child(4) {\n    height: 18px;\n    animation-delay: 0.6s;\n}\n\n@keyframes sound-wave {\n\n    0%,\n    100% {\n        transform: scaleY(1);\n    }\n\n    50% {\n        transform: scaleY(0.5);\n    }\n}\n\n.play-button {\n    order: 3;\n    width: 40px;\n    height: 40px;\n    border-radius: 50%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    cursor: pointer;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n    transition: all 0.2s ease;\n    margin-right: 20px;\n    margin-top: -57px;\n    position: relative;\n    font-size: 20px;\n    color: #333;\n}\n\n.play-button::after {\n    content: '♪';\n    transition: all 0.2s ease;\n}\n\n.play-button:hover {\n    transform: scale(1.05);\n    background: var(--primary-color);\n    color: #fff;\n}\n\n.play-button:hover::after {\n    border-color: none;\n}\n\n.play-button::after {\n    border: none;\n    margin-left: 0;\n}\n\n.radio-title {\n    justify-content: center;\n    font-size: 22px;\n    font-weight: bold;\n    color: white;\n    display: flex;\n    align-items: center;\n    gap: 8px;\n}\n\n.heart-icon {\n    font-size: 20px;\n}\n\n.shuffle-icon {\n    font-size: 20px;\n    color: #666;\n    cursor: pointer;\n    transition: transform 0.3s ease;\n}\n\n.shuffle-icon:hover {\n    transform: scale(1.1);\n    color: var(--primary-color);\n}\n\n.radio-subtitle {\n    font-size: 15px;\n    color: white;\n    text-align: center;\n}\n\n.note-container {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n    pointer-events: none;\n    overflow: hidden;\n}\n\n.flying-note {\n    position: absolute;\n    font-size: 36px;\n    color: var(--primary-color);\n    pointer-events: none;\n    transform-origin: center;\n}\n\n.fly-note-enter-active {\n    animation: fly-note 2s ease-out forwards;\n}\n\n.fly-note-leave-active {\n    animation: fly-note 2s ease-out forwards;\n}\n\n@keyframes fly-note {\n    0% {\n        transform: translate(var(--start-x), calc(var(--start-y) - 50px)) rotate(0deg) scale(1.2);\n        opacity: 0.9;\n    }\n\n    20% {\n        transform: translate(calc(var(--start-x) + 20px), calc(var(--start-y) - 70px)) rotate(45deg) scale(1.3);\n        opacity: 0.85;\n    }\n\n    100% {\n        transform: translate(80vw, 100vh) rotate(360deg) scale(0.6);\n        opacity: 0;\n    }\n}\n\n.ranking-entry {\n    display: block;\n    width: 100%;\n    height: 100%;\n    text-decoration: none;\n    background: linear-gradient(135deg, var(--primary-color), #9f92ff);\n    border-radius: 15px;\n    overflow: hidden;\n}\n\n.ranking-content {\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    color: white;\n    position: relative;\n}\n\n.ranking-icon{\n    width: 135px\n}\n\n.ranking-title {\n    font-size: 24px;\n    font-weight: bold;\n    margin-bottom: 0px;\n    margin-top: 0px;\n}\n\n.ranking-description {\n    font-size: 16px;\n    opacity: 0.9;\n}\n\n.recommend-card.gradient-background {\n    background: linear-gradient(135deg, var(--primary-color), #8ff2ff);\n    color: white;\n}\n\n.playlist-entry {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    width: 100%;\n    height: 100%;\n    border-radius: 15px;\n    background: linear-gradient(135deg, var(--primary-color), #cfff82);\n    color: white;\n    text-align: center;\n    transition: transform 0.3s ease, box-shadow 0.3s ease;\n}\n\n.playlist-entry:hover {\n    box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);\n}\n\n.playlist-content {\n    position: relative;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    width: 100%;\n    height: 100%;\n    justify-content: flex-end;\n}\n\n.playlist-icon {\n    width: 144px;\n    height: 144px;\n}\n\n.playlist-icon img {\n    width: 100%;\n    height: 100%;\n}\n</style>\n"
  },
  {
    "path": "src/views/Library.vue",
    "content": "<template>\n    <div class=\"library-page\">\n        <div class=\"profile-section\">\n            <div class=\"profile-header\" :style=\"`background-image: url(${userDetail.bg_pic || './assets/images/banner.png'})`\">\n                <div class=\"profile-info\">\n                    <img class=\"profile-pic\" :src=\"user.pic\" :alt=\"$t('yong-hu-tou-xiang')\" />\n                    <div class=\"user-details\">\n                        <div class=\"user-name-row\">\n                            <h2 class=\"user-name\">{{ user.nickname }}</h2>\n                            <span class=\"user-level\">Lv.{{ userDetail.p_grade || 0 }}</span>\n                            <BirthdayEasterEgg :birthday=\"userDetail.birthday\" :nickname=\"user.nickname\"\n                                :player-control=\"props.playerControl\" />\n                            <img v-if=\"userVip[0] && userVip[0].is_vip == 1\" class=\"user-vip-icon\"\n                                :src=\"`./assets/images/${userVip[0].product_type === 'svip' ? 'vip' : 'vip2'}.png`\"\n                                :title=\"`${$t('gai-nian-ban')} ${userVip[0].vip_end_time}`\" />\n                            <img v-if=\"userVip[1] && userVip[1].is_vip == 1\" class=\"user-vip-icon\"\n                                :src=\"`./assets/images/${userVip[1].product_type === 'svip' ? 'vip' : 'vip2'}.png`\"\n                                :title=\"`${$t('chang-ting-ban')} ${userVip[1].vip_end_time}`\" />\n                        </div>\n                        <div class=\"user-signature\">{{ userDetail.descri || '' }}</div>\n                        <div class=\"user-stats\">\n                            <div class=\"stat-item\"><span class=\"stat-value\">{{ userDetail.follows || 0 }}</span><span class=\"stat-label\">{{ $t('guan-zhu') }}</span></div>\n                            <div class=\"stat-item\"><span class=\"stat-value\">{{ userDetail.fans || 0 }}</span><span class=\"stat-label\">{{ $t('fen-si') }}</span></div>\n                            <div class=\"stat-item\"><span class=\"stat-value\">{{ userDetail.friends || 0 }}</span><span class=\"stat-label\">{{ $t('hao-you') }}</span></div>\n                            <div class=\"stat-item\"><span class=\"stat-value\">{{ userDetail.hvisitors || 0 }}</span><span class=\"stat-label\">{{ $t('fang-wen') }}</span></div>\n                        </div>\n                        <div class=\"user-meta\">\n                            <span class=\"user-gender\">\n                                <i :class=\"userDetail.gender === 1 ? 'fas fa-mars' : 'fas fa-venus'\"></i>\n                            </span>\n                            <span class=\"user-duration\">{{ formatDuration(userDetail.duration || 0) }} {{ $t('ting-ge-shi-chang') }}</span>\n                            <span class=\"user-age\">{{ formatRegTime(userDetail.rtime || 0) }}</span>\n                        </div>\n                        <div class=\"user-actions\">\n                            <span class=\"action-button\" @click=\"signIn\">{{ $t('qian-dao') }}</span>\n                            <span class=\"action-button\" @click=\"getVip\">VIP</span>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n        <h2 class=\"section-title\" @click=\"addAllSongsToQueue\">{{ $t('wo-xi-huan-ting') }}</h2>\n        <div class=\"favorite-section\">\n            <div class=\"song-list\">\n                <div v-if=\"isLoading\" class=\"skeleton-loader\">\n                    <div v-for=\"n in 16\" :key=\"n\" class=\"skeleton-item\">\n                        <div class=\"skeleton-cover\"></div>\n                        <div class=\"skeleton-info\">\n                            <div class=\"skeleton-line\"></div>\n                            <div class=\"skeleton-line short\"></div>\n                        </div>\n                    </div>\n                </div>\n                <ul v-if=\"listenHistory.length > 0\">\n                    <li v-for=\"(song, index) in listenHistory\" :key=\"index\" class=\"song-item\"\n                        @click=\"playSong(song['hash'], song.name.split(' - ')[1] || song.name, $getCover(song.image, 480), song.singername)\">\n                        <img :src=\"$getCover(song.image, 120)\" class=\"album-cover\" />\n                        <div class=\"song-info\">\n                            <p class=\"album-name\">{{ song.name.split(' - ')[1] || song.name }}</p>\n                            <p class=\"singer-name\">{{ song.singername }}</p>\n                        </div>\n                    </li>\n                </ul>\n                <div v-else class=\"empty-container\">\n                    <div class=\"empty-image\">\n                        <img src=\"/assets/images/empty.png\" alt=\"暂无数据\" />\n                    </div>\n                    <div class=\"empty-description\">{{ t('zhe-li-shi-mo-du-mei-you') }}</div>\n                </div>\n            </div>\n        </div>\n\n        <!-- 分类导航 -->\n        <div class=\"category-tabs\">\n            <button v-for=\"(tab, index) in categories\" :key=\"index\" :class=\"{ 'active': selectedCategory === index }\"\n                @click=\"selectCategory(index)\">\n                {{ tab }}\n            </button>\n        </div>\n\n        <!-- 音乐卡片网格（显示歌单或关注的歌手） -->\n        <div class=\"music-grid\">\n            <template v-if=\"selectedCategory === 0 || selectedCategory === 1 || selectedCategory === 2\">\n                <div v-if=\"selectedCategory === 0 && !isLoading\" class=\"music-card create-playlist-button\">\n                    <router-link :to=\"{\n                        path: '/CloudDrive'\n                    }\">\n                        <img :src=\"`./assets/images/cloud-disk.png`\" class=\"album-image\" />\n                        <div class=\"album-info\">\n                            <h3>我的云盘</h3>\n                            <p>(*/ω＼*)</p>\n                        </div>\n                    </router-link>\n                </div>\n                <div v-if=\"selectedCategory === 0 && !isLoading\" class=\"music-card create-playlist-button\">\n                    <router-link :to=\"{\n                        path: '/LocalMusic'\n                    }\">\n                        <img :src=\"`./assets/images/local-music.png`\" class=\"album-image\" />\n                        <div class=\"album-info\">\n                            <h3>本地音乐</h3>\n                            <p>(〃'▽'〃)</p>\n                        </div>\n                    </router-link>\n                </div>\n                <div class=\"music-card\"\n                    v-for=\"(item, index) in (selectedCategory === 0 ? userPlaylists : selectedCategory === 1 ? collectedPlaylists : collectedAlbums)\"\n                    :key=\"index\">\n                    <router-link :to=\"{\n                        path: '/PlaylistDetail',\n                        query: { global_collection_id: item.list_create_gid || item.global_collection_id, listid: item.listid}\n                    }\">\n                        <img :src=\"item.pic ? $getCover(item.pic, 480) : './assets/images/live.png'\"\n                            class=\"album-image\" />\n                        <div class=\"album-info\">\n                            <h3>{{ item.name }}</h3>\n                            <p>{{ item.count }} <span>{{ $t('shou-ge') }}</span></p>\n                        </div>\n                    </router-link>\n                </div>\n                <div v-if=\"selectedCategory === 0 && !isLoading\" class=\"music-card create-playlist-button\">\n                    <i class=\"fas fa-plus\"></i>\n                    <img :src=\"`./assets/images/ti111mg.png`\" class=\"album-image\" @click=\"createPlaylist\"/>\n                    <div class=\"album-info\" @click=\"createPlaylist\">\n                        <h3>{{ $t('chuang-jian-ge-dan') }}</h3>\n                        <p>(≧∀≦)♪</p>\n                    </div>\n                </div>\n            </template>\n            <div v-if=\"selectedCategory === 3 || selectedCategory === 4\" class=\"music-card\"\n                v-for=\"(artist, index) in (selectedCategory === 3 ? followedArtists : selectedCategory === 4 ? collectedFriends  : [])\" :key=\"index\"\n                @click=\"goToArtistDetail(artist)\">\n                <img :src=\"artist.pic\" class=\"album-image\" />\n                <div class=\"album-info\">\n                    <h3>{{ artist.nickname }}</h3>\n                </div>\n            </div>\n        </div>\n        <div v-if=\"\n        (selectedCategory == 0 && userPlaylists.length === 0) || \n        (selectedCategory == 1 && collectedPlaylists.length === 0) || \n        (selectedCategory == 2 && collectedAlbums.length === 0) || \n        (selectedCategory == 3 && followedArtists.length === 0) || \n        (selectedCategory == 4 && collectedFriends.length === 0)\"\n            class=\"empty-container\">\n            <div class=\"empty-image\">\n                <img src=\"/assets/images/empty.png\" alt=\"暂无数据\" />\n            </div>\n            <div class=\"empty-description\">{{ t('zhe-li-shi-mo-du-mei-you') }}</div>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref, onMounted } from 'vue';\nimport { get } from '../utils/request';\nimport { MoeAuthStore } from '../stores/store';\nimport { useRouter } from 'vue-router';\nimport { useI18n } from 'vue-i18n';\nimport BirthdayEasterEgg from '../components/BirthdayEasterEgg.vue';\nconst { t } = useI18n();\nconst router = useRouter();\nconst MoeAuth = MoeAuthStore();\nconst user = ref({});\nconst userPlaylists = ref([]); // 创建的歌单\nconst collectedPlaylists = ref([]); // 收藏的歌单\nconst collectedAlbums = ref([]); // 收藏的专辑\nconst collectedFriends = ref([]); // 好友\nconst followedArtists = ref([]); // 关注的歌手\nconst listenHistory = ref([]); // 听歌历史\nconst userVip = ref({});\nconst userDetail = ref({}); // 新增：用户详细信息\nconst categories = ref([t('wo-chuang-jian-de-ge-dan'), t('wo-shou-cang-de-ge-dan'), t('wo-shou-cang-de-zhuan-ji'), t('wo-guan-zhu-de-ge-shou'), t('wo-guan-zhu-de-hao-you')]);\nconst selectedCategory = ref(0);\nconst isLoading = ref(true); \n\nconst selectCategory = (index) => {\n    selectedCategory.value = index;\n    router.replace({ path: '/library', query: { category: index } });\n};\n\n// 格式化听歌时长（分钟转为小时和分钟）\nconst formatDuration = (minutes) => {\n    if (!minutes) return '0';\n    const hours = Math.floor(minutes / 60);\n    const mins = minutes % 60;\n    if (hours > 0) {\n        return `${hours}${t('xiao-shi')} ${mins}${t('fen-zhong')}`;\n    }\n    return `${mins}${t('fen-zhong')}`;\n};\n\n// 格式化注册时间\nconst formatRegTime = (timestamp) => {\n    if (!timestamp) return '';\n    const registerDate = new Date(timestamp * 1000);\n    const now = new Date();\n    const years = now.getFullYear() - registerDate.getFullYear();\n    return `${t('le-ling')} ${years} ${t('nian')}`;\n};\n\nconst playSong = (hash, name, img, author) => {\n    props.playerControl.addSongToQueue(hash, name, img, author);\n};\n\nconst props = defineProps({\n    playerControl: Object\n});\n\nonMounted(() => {\n    if (MoeAuth.isAuthenticated) {\n        user.value = MoeAuth.UserInfo;\n        // 获取用户vip信息\n        getVipInfo();\n    }\n});\nconst getUserDetails = () => {\n    // 获取用户详细信息\n    getUserDetail();\n    // 获取用户听歌历史\n    getlisten().finally(() => {\n        isLoading.value = false; \n    })\n    // 获取用户创建和收藏的歌单\n    getplaylist()\n    // 获取用户关注的歌手\n    getfollow()\n    selectedCategory.value = parseInt(router.currentRoute.value.query.category || 0);\n}\n\n// 获取用户详细信息\nconst getUserDetail = async () => {\n    try {\n        const detailResponse = await get('/user/detail');\n        if (detailResponse.status === 1) {\n            userDetail.value = detailResponse.data;\n        }\n    } catch (error) {\n        console.error('Failed to get user details:', error);\n    }\n}\n\nconst getVipInfo = async () => {\n    try {\n        const VipInfoResponse = await get('/user/vip/detail');\n        if (VipInfoResponse.status === 1) {\n            userVip.value = VipInfoResponse.data.busi_vip\n            getUserDetails();\n        }\n    } catch (error) {\n        window.$modal.alert(t('deng-lu-shi-xiao-qing-zhong-xin-deng-lu'));\n        router.push('/login');\n    }\n}\n\nconst getlisten = async () => {\n    const historyResponse = await get('/user/listen', { type: 1 });\n    if (historyResponse.status === 1) {\n        const allLists = historyResponse.data.lists;\n        const shuffled = allLists.sort(() => 0.5 - Math.random());\n        listenHistory.value = shuffled.slice(0, 16);\n    }\n}\nconst getfollow = async () => {\n    const followResponse = await get('/user/follow');\n    if (followResponse.status === 1) {\n        if (followResponse.data.total == 0) return;\n        const artists = followResponse.data.lists.map(artist => ({\n            ...artist,\n            pic: artist.pic.replace('/100/', '/480/')\n        }));\n        collectedFriends.value = artists.filter(artist => !artist.singerid);\n        followedArtists.value = artists.filter(artist => artist.source == 7);\n    }\n}\nconst getplaylist = async () => {\n    try {\n        const playlistResponse = await get('/user/playlist',{\n            pagesize:500,\n            t: localStorage.getItem('t')\n        });\n        if (playlistResponse.status === 1) {\n            const sortedInfo = playlistResponse.data.info.sort((a, b) => {\n                if (a.sort !== b.sort) {\n                    return a.sort - b.sort;\n                }\n                return 0;\n            });\n\n            userPlaylists.value = sortedInfo.filter(playlist => {\n                if (playlist.name == '我喜欢') {\n                    localStorage.setItem('like', playlist.listid);\n                }\n                return playlist.list_create_userid === user.value.userid || playlist.name === '我喜欢';\n            }).sort((a, b) => a.name === '我喜欢' ? -1 : 1);\n\n            collectedPlaylists.value = sortedInfo.filter(playlist => \n                playlist.list_create_userid !== user.value.userid && !playlist.authors\n            );\n\n            collectedAlbums.value = sortedInfo.filter(playlist => \n                playlist.list_create_userid !== user.value.userid && playlist.authors\n            );\n            \n            const collectedIds = [];\n            sortedInfo.forEach(playlist => {\n                if (playlist.list_create_userid !== user.value.userid) {\n                    collectedIds.push({\n                        list_create_listid: playlist.list_create_listid, \n                        listid: playlist.listid\n                    });\n                }\n            });\n            localStorage.setItem('collectedPlaylists', JSON.stringify(collectedIds));\n        }\n    } catch (error) {\n        window.$modal.alert(t('xin-zeng-zhang-hao-qing-xian-zai-guan-fang-ke-hu-duan-zhong-deng-lu-yi-ci')); \n    }\n}\nconst createPlaylist = async () => {\n    const result = await window.$modal.prompt(t('qing-shu-ru-xin-de-ge-dan-ming-cheng'), '');\n    if (result) {\n        try {\n            const playlistResponse = await get('/playlist/add', { name: result, list_create_userid: user.value.userid });\n            if (playlistResponse.status === 1) {\n                localStorage.setItem('t', Date.now());\n                getplaylist()\n            }\n        } catch (error) {\n            window.$modal.alert(t('chuang-jian-shi-bai'));\n        }\n    }\n}\n\nconst goToArtistDetail = (artist) => {\n    if (!artist.singerid) return;\n    router.push({\n        path: '/PlaylistDetail',\n        query: { \n            singerid: artist.singerid,\n            unfollow: true\n        }\n    });\n};\nconst signIn = async () => {\n    try {\n        const res = await get('/youth/vip');\n        if (res.status === 1) {\n            window.$modal.alert(`签到成功，获得${res.data.award_vip_hour}小时VIP时长`);\n        }\n    } catch (error) {\n        window.$modal.alert('签到失败![该接口将在未来被移除]');\n    }\n}\nconst getVip = async () => {\n    try{\n        const todayKey = new Date().toISOString().split('T')[0];\n        const vipResponse = await get('/youth/day/vip',{\n            receive_day: todayKey\n        });\n        const result = await window.$modal.confirm('是否继续升级至概念版VIP,享受更高音质?');\n        if(result){\n            try{\n                const vipResponse = await get('/youth/day/vip/upgrade');\n                if (vipResponse.status === 1) {\n                    window.$modal.alert('升级成功，获得1天概念版VIP');\n                }\n            } catch (error) {\n                window.$modal.alert(error.error_msg || '升级VIP失败, 一天仅限一次');\n            }\n        }else if (vipResponse.status === 1) {\n            window.$modal.alert(`签到成功，获得1天畅听VIP`);\n        }\n    } catch (error) {\n        if(error.response.data.error_code == 131001){\n            window.$modal.alert('你今天已经签到过了');\n            return;\n        }else if(error.response.data.error_code == 20028){\n            window.$modal.alert('当前账号风控,请前往手机端领取');\n            return;\n        }\n        window.$modal.alert('获取VIP失败-' + error.response.data.error_code);\n    }\n}\nconst addAllSongsToQueue = () => {\n    props.playerControl.addPlaylistToQueue(listenHistory.value.map(song => ({\n        hash: song.hash,\n        name: song.name,\n        cover: song.image?.replace(\"{size}\", 480),\n        author: song.author_name,\n        timelen: song.duration\n    })));\n};\n</script>\n\n<style scoped>\n.sign-in {\n    cursor: pointer;\n    color: var(--primary-color);\n    margin-left: 10px;\n    border-radius: 5px;\n    padding: 2px 8px;\n    border: 1px solid var(--primary-color);\n    font-size: 12px;\n}\n\n.library-page {\n    padding: 20px;\n}\n\n.user-level {\n    width: 50px;\n    margin-left: 10px;\n    cursor: pointer;\n}\n\n\n.section-title {\n    font-size: 28px;\n    font-weight: bold;\n    margin-bottom: 30px;\n    color: var(--primary-color);\n    cursor: cell;\n    margin-bottom: 0px;\n    display: inline-block;\n}\n\n.profile-section {\n    display: flex;\n    align-items: center;\n}\n\n.profile-header {\n    width: 100%;\n    height: 100%; \n    background-size: cover;\n    background-position: center;\n    border-radius: 15px;\n    margin-bottom: 20px;\n    display: flex;\n    align-items: flex-end;\n    padding: 20px;\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n    position: relative;\n    overflow: visible;\n    transition: background-image 1s ease-in-out;\n}\n\n.profile-header::before {\n    content: '';\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background: linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.6) 100%);\n    border-radius: 15px;\n    z-index: 1;\n}\n\n.profile-info {\n    display: flex;\n    align-items: flex-end;\n    gap: 15px;\n    color: white;\n    text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);\n    width: 100%;\n    z-index: 2;\n}\n\n.profile-pic {\n    border-radius: 50%;\n    width: 90px;\n    height: 90px;\n    border: 3px solid white;\n    box-shadow: 0 0 10px rgba(0, 0, 0, 0.3);\n    margin-bottom: 10px;\n    position: relative;\n    top: -20px;\n}\n\n.user-details {\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-end;\n    height: 100%;\n    flex: 1;\n}\n\n.user-name-row {\n    display: flex;\n    align-items: center;\n    margin-bottom: 2px;\n}\n\n.user-name {\n    font-size: 28px;\n    font-weight: bold;\n    margin: 0;\n}\n\n.user-level {\n    font-size: 14px;\n    background-color: rgba(255, 255, 255, 0.2);\n    padding: 2px 8px;\n    border-radius: 10px;\n    color: white;\n}\n\n.user-vip-icon {\n    height: 22px;\n    margin-left: 10px;\n}\n\n.user-signature {\n    font-size: 14px;\n    color: #eee;\n    margin-bottom: 8px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    display: -webkit-box;\n    -webkit-line-clamp: 1;\n    -webkit-box-orient: vertical;\n}\n\n.user-stats {\n    display: flex;\n    justify-content: flex-start;\n    gap: 20px;\n    margin-bottom: 5px;\n    font-size: 14px;\n    color: #fff;\n}\n\n.stat-item {\n    text-align: center;\n}\n\n.stat-value {\n    font-size: 18px;\n    font-weight: bold;\n    display: inline-block;\n    margin-right: 3px;\n}\n\n.stat-label {\n    display: inline-block;\n    font-size: 12px;\n    color: rgba(255, 255, 255, 0.8);\n}\n\n.user-meta {\n    display: flex;\n    align-items: center;\n    gap: 15px;\n    font-size: 12px;\n    color: #fff;\n    margin-bottom: 10px;\n}\n\n.user-gender i {\n    font-size: 16px;\n    color: #fff;\n}\n\n.user-duration,\n.user-age {\n    background-color: rgba(255, 255, 255, 0.2);\n    padding: 3px 8px;\n    border-radius: 10px;\n    color: white;\n}\n\n.user-actions {\n    display: flex;\n    gap: 10px;\n    margin-top: 5px;\n}\n\n.action-button {\n    background-color: rgba(255, 255, 255, 0.2);\n    padding: 4px 10px;\n    border-radius: 10px;\n    color: white;\n    cursor: pointer;\n    font-size: 12px;\n    border: 1px solid rgba(255, 255, 255, 0.3);\n    transition: background-color 0.3s ease;\n}\n\n.action-button:hover {\n    background-color: rgba(255, 255, 255, 0.3);\n}\n\n.favorite-section {\n    display: flex;\n    justify-content: space-between;\n}\n\n.favorite-playlist {\n    background-color: var(--background-color);\n    padding: 20px;\n    border-radius: 12px;\n    flex: 1;\n    margin-right: 20px;\n    border: 1px solid var(--secondary-color);\n    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);\n    margin-bottom: 20px;\n}\n\n.playlist-info p {\n    margin: 10px 0;\n}\n\n.play-button {\n    display: inline-flex;\n    align-items: center;\n    gap: 5px;\n    background-color: var(--secondary-color);\n    color: white;\n    border: none;\n    border-radius: 25px;\n    padding: 10px 15px;\n    cursor: pointer;\n}\n\n.play-button i {\n    font-size: 16px;\n}\n\n.song-list {\n    flex: 1;\n}\n\n.song-list ul {\n    list-style: none;\n    padding: 0;\n    display: flex;\n    flex-wrap: wrap;\n    justify-content: space-between;\n}\n\n.song-list li {\n    display: flex;\n    align-items: center;\n    margin-bottom: 10px;\n    width: 250px;\n    cursor: pointer;\n    border-radius: 10px;\n    padding-left: 10px;\n}\n\n.song-list li:hover {\n    background-color: var(--background-color);\n}\n\n.song-list img {\n    width: 50px;\n    height: 50px;\n    margin-right: 10px;\n    border-radius: 6px;\n}\n\n.category-tabs {\n    display: flex;\n    gap: 20px;\n    margin-bottom: 20px;\n}\n\n.category-tabs button {\n    padding: 10px 15px;\n    border: none;\n    background-color: #f5f5f5;\n    border-radius: 20px;\n    cursor: pointer;\n}\n\n.category-tabs button.active {\n    background-color: var(--primary-color);\n    color: white;\n}\n\n.music-grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));\n    gap: 20px;\n}\n\n.music-card {\n    text-align: center;\n    cursor: pointer;\n}\n\n.album-image {\n    width: 100%;\n    border-radius: 12px;\n}\n\n.album-info h3 {\n    margin: 10px 0 5px;\n    font-size: 16px;\n}\n\n.album-info p {\n    color: #666;\n    font-size: 14px;\n}\n\n.song-item {\n    display: flex;\n    align-items: center;\n    margin-bottom: 10px;\n}\n\n.album-cover {\n    width: 50px;\n    height: 50px;\n    margin-right: 10px;\n    border-radius: 5px;\n}\n\n.song-info {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    max-width: 190px;\n}\n\n.album-name,\n.singer-name {\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.album-name {\n    font-weight: bold;\n    margin-bottom: -5px;\n    font-size: 14px;\n    color: #333;\n}\n\n.singer-name {\n    font-size: 12px;\n    color: #666;\n}\n\n.skeleton-loader {\n    display: flex;\n    flex-wrap: wrap;\n    justify-content: space-between;\n    margin-top: 10px;\n}\n\n.skeleton-item {\n    display: flex;\n    align-items: center;\n    margin-bottom: 10px;\n    width: 250px;\n    border-radius: 10px;\n    padding-left: 10px;\n    background-color: #f0f0f0;\n    height: 68px;\n}\n\n.skeleton-cover {\n    width: 50px;\n    height: 50px;\n    margin-right: 10px;\n    border-radius: 10px;\n    background-color: #e0e0e0;\n}\n\n.skeleton-info {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    max-width: 190px;\n}\n\n.skeleton-line {\n    height: 10px;\n    background-color: #e0e0e0;\n    margin-bottom: 5px;\n    border-radius: 5px;\n    width: 150px;\n}\n\n.create-playlist-button {\n    color: var(--primary-color);\n    border-radius: 10px;\n    cursor: pointer;\n    position: relative;\n}\n\n.create-playlist-button i {\n    font-size: 30px;\n    position: absolute;\n    top: 32%;\n    left: 29%;\n}\n\n/* 空状态容器样式 */\n.empty-container {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    padding: 40px 0;\n    width: 100%;\n}\n\n.empty-image {\n    margin-bottom: 20px;\n    display: flex;\n    justify-content: center;\n}\n\n.empty-image img {\n    width: 200px;\n    height: 200px;\n    opacity: 0.6;\n}\n\n.empty-description {\n    color: #909399;\n    font-size: 14px;\n    text-align: center;\n    margin-left: 60px;\n}\n</style>\n"
  },
  {
    "path": "src/views/LocalMusic.vue",
    "content": "<template>\n    <div class=\"detail-page\">\n        <div class=\"header\">\n            <img class=\"cover-art\" :src=\"`./assets/images/local.png`\" />\n            <div class=\"info\">\n                <h1 class=\"title\">本地音乐</h1>\n                <p class=\"subtitle\">本地歌曲数: {{ musicFiles.length }}</p>\n                <div class=\"folder-info\" v-if=\"currentFolder\">\n                    <div class=\"folder-path\">\n                        <i class=\"fas fa-folder\"></i>\n                        <span>{{ currentFolder.name }}</span>\n                    </div>\n                </div>\n                <div class=\"description\">这里存放着你授权的文件夹中的歌曲，支持 MP3、FLAC、WAV、AAC、OGG、M4A 等格式</div>\n                <div class=\"actions\">\n                    <button class=\"primary-btn\" @click=\"addPlaylistToQueue($event)\" v-if=\"musicFiles.length > 0\">\n                        <i class=\"fas fa-play\"></i> 播放全部\n                    </button>\n                    <button class=\"upload-btn\" @click=\"selectFolder\" :disabled=\"loading\">\n                        <i class=\"fas fa-folder-open\"></i> {{ currentFolder ? '重新选择文件夹' : '选择音乐文件夹' }}\n                    </button>\n                    <button v-if=\"currentFolder\" class=\"upload-btn\" @click=\"refreshFolder\" :disabled=\"refreshing\">\n                        <i class=\"fas fa-sync-alt\"></i> 刷新\n                    </button>\n                </div>\n            </div>\n        </div>\n\n        <!-- 导航按钮 -->\n        <i class=\"location-arrow fas fa-location-arrow\" @click=\"scrollToItem\" title=\"当前播放歌曲\"></i>\n        <img :src=\"`./assets/images/lemon.gif`\" class=\"scroll-bottom-img\" @click=\"scrollToFirstItem\" title=\"返回顶部\"/>\n\n        <!-- 歌曲列表 -->\n        <div class=\"track-list-container\" v-if=\"!loading\">\n            <div class=\"track-list-header\">\n                <h2 class=\"track-list-title\"><span>本地歌曲</span> ( {{ filteredTracks.length }} )</h2>\n                <div class=\"track-list-actions\">\n                    <div class=\"batch-action-container\">\n                        <button class=\"batch-action-btn\" @click=\"toggleBatchSelection\" :class=\"{ 'active': batchSelectionMode }\">\n                            <input type=\"checkbox\" v-model=\"batchSelectionMode\" /> 批量操作\n                            <span v-if=\"selectedTracks.length > 0\" class=\"selected-count\">{{ selectedTracks.length }}</span>\n                        </button>\n                        <div v-if=\"batchSelectionMode && isBatchMenuVisible && selectedTracks.length > 0\" class=\"batch-actions-menu\">\n                            <ul>\n                                <li @click=\"appendSelectedToQueue\"><i class=\"fas fa-list\"></i> 添加到播放列表</li>\n                            </ul>\n                        </div>\n                    </div>\n                    <button class=\"view-mode-btn\" @click=\"toggleListMode\" :title=\"listMode === 'list' ? '切换到网格视图' : '切换到列表视图'\">\n                        <i class=\"fas\" :class=\"listMode === 'list' ? 'fa-th' : 'fa-list'\"></i>\n                    </button>\n                    <input type=\"text\" v-model=\"searchQuery\" @keyup.enter=\"searchTracks\" placeholder=\"搜索歌曲\" class=\"search-input\" />\n                </div>\n            </div>\n\n            <!-- 表头 -->\n            <div class=\"track-list-header-row\" v-if=\"musicFiles.length > 0\">\n                <div class=\"track-checkbox-header\" v-if=\"batchSelectionMode\">\n                    <input type=\"checkbox\" :checked=\"isAllSelected\" @click=\"toggleSelectAll\">\n                </div>\n                <div class=\"track-number-header\" v-else>♪</div>\n                <div class=\"track-title-header\" @click=\"sortTracks('name')\">\n                    文件名 <i class=\"fas\" :class=\"getSortIconClass('name')\"></i>\n                </div>\n                <div class=\"track-artist-header\" @click=\"sortTracks('author')\">\n                    歌手 <i class=\"fas\" :class=\"getSortIconClass('author')\"></i>\n                </div>\n                <div class=\"track-album-header\" @click=\"sortTracks('album')\">\n                    专辑 <i class=\"fas\" :class=\"getSortIconClass('album')\"></i>\n                </div>\n                <div class=\"track-size-header\" @click=\"sortTracks('size')\">\n                    文件大小 <i class=\"fas\" :class=\"getSortIconClass('size')\"></i>\n                </div>\n                <div class=\"track-timelen-header\" @click=\"sortTracks('timelen')\">\n                    时间 <i class=\"fas\" :class=\"getSortIconClass('timelen')\"></i>\n                </div>\n            </div>\n\n            <RecycleScroller ref=\"recycleScrollerRef\" :items=\"filteredTracks\" :item-size=\"listMode === 'list' ? 50 : 70\" class=\"track-list\" key-field=\"name\" v-if=\"musicFiles.length > 0\">\n                <template #default=\"{ item, index }\">\n                    <div class=\"li\" :key=\"item.name\"\n                        :class=\"{ 'cover-view': listMode === 'grid', 'selected': selectedTracks.includes(index) }\"\n                        @click=\"batchSelectionMode ? selectTrack(index, $event) : playSong(item)\">\n                        \n                        <!-- 复选框或序号 -->\n                        <div class=\"track-checkbox\" v-if=\"batchSelectionMode\">\n                            <input type=\"checkbox\" :checked=\"selectedTracks.includes(index)\" @click.stop=\"selectTrack(index, $event)\">\n                        </div>\n                        <div class=\"track-number\" v-else>{{ index + 1 }}</div>\n\n                        <!-- 网格模式封面 -->\n                        <div class=\"track-cover\" v-if=\"listMode === 'grid'\">\n                            <img :src=\"item.cover || './assets/images/ico.png'\" alt=\"Cover\">\n                            <div class=\"track-cover-overlay\" :class=\"{ 'playing': props.playerControl?.currentSong.name == item.name }\">\n                                <i :class=\"props.playerControl?.currentSong.name == item.name ? 'fas fa-music' : 'fas fa-play'\"></i>\n                            </div>\n                        </div>\n\n                        <!-- 歌曲信息 -->\n                        <div class=\"track-title\" :title=\"item.name\">{{ item.displayName }}\n                            <span v-if=\"item.qualityInfo\" class=\"icon\" :class=\"item.qualityInfo.class\">{{ item.qualityInfo.text }}</span>\n                        </div>\n                        <div class=\"track-artist\" :title=\"item.author\">{{ item.author }}</div>\n                        <div class=\"track-album\" :title=\"item.album\">{{ item.album }}</div>\n                        <div class=\"track-size\" :title=\"item.filesize\">{{ item.filesize }}</div>\n                        <div class=\"track-timelen\">\n                            <button v-if=\"props.playerControl?.currentSong.name == item.name && listMode === 'list'\" \n                                class=\"queue-play-btn fas fa-music\"></button>\n                            {{ formatDuration(item.duration) }}\n                        </div>\n                    </div>\n                </template>\n            </RecycleScroller>\n\n            <!-- 空状态 -->\n            <div v-if=\"musicFiles.length === 0 && currentFolder\" class=\"empty-state\">\n                <img src=\"/assets/images/empty1.png\">\n                <p>该文件夹中没有找到音乐文件</p>\n                <p class=\"hint\">支持的格式: MP3, FLAC, WAV, AAC, OGG, M4A</p>\n            </div>\n\n            <!-- 欢迎状态 -->\n            <div v-if=\"!currentFolder\" class=\"welcome-state\">\n                <img src=\"/assets/images/empty1.png\">\n                <h3>欢迎使用MoeKoe Music</h3>\n                <p>请选择并授权以访问您的本地音乐文件夹</p>\n            </div>\n        </div>\n\n        <!-- 加载状态 -->\n        <div v-if=\"loading\" class=\"loading-state\">\n            <div class=\"loading-spinner\"></div>\n            <p>正在扫描音乐文件...</p>\n        </div>\n\n        <div class=\"note-container\">\n            <transition-group name=\"fly-note\">\n                <div v-for=\"note in flyingNotes\" :key=\"note.id\" class=\"flying-note\" :style=\"note.style\">♪</div>\n            </transition-group>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref, onMounted, onBeforeUnmount, computed } from 'vue';\nimport { RecycleScroller } from 'vue3-virtual-scroller';\nimport { parseBlob } from 'music-metadata';\n\n// Props\nconst props = defineProps({\n    playerControl: Object\n});\n\n// 通用状态\nconst currentFolder = ref(null);\nconst musicFiles = ref([]);\nconst filteredTracks = ref([]);\nconst searchQuery = ref('');\nconst recycleScrollerRef = ref(null);\nconst loading = ref(false);\nconst refreshing = ref(false);\nconst flyingNotes = ref([]);\nlet noteId = 0;\n\n// 批量选择相关状态\nconst batchSelectionMode = ref(false);\nconst isBatchMenuVisible = ref(false);\nconst selectedTracks = ref([]);\nlet lastSelectedIndex = -1;\n\n// 排序状态\nconst sortField = ref('');\nconst sortOrder = ref('asc');\n\n// 列表模式状态\nconst listMode = ref(localStorage.getItem('localMusicListMode') || 'list');\n\n// 支持的音乐文件格式\nconst supportedFormats = ['.mp3', '.flac', '.wav', '.aac', '.ogg', '.m4a', '.wma'];\n\n// IndexedDB 相关\nconst DB_NAME = 'LocalMusicDB';\nconst DB_VERSION = 1;\nconst STORE_NAME = 'folderHandles';\n\n// 判断是否全选\nconst isAllSelected = computed(() => {\n    return selectedTracks.value.length === filteredTracks.value.length && filteredTracks.value.length > 0;\n});\n\nonMounted(() => {\n    $message.warning('该页面处于测试阶段，功能还未完善')\n    loadLastFolder();\n    document.addEventListener('click', handleClickOutside);\n});\n\nonBeforeUnmount(() => {\n    document.removeEventListener('click', handleClickOutside);\n});\n\n// 初始化 IndexedDB\nconst initDB = () => {\n    return new Promise((resolve, reject) => {\n        const request = indexedDB.open(DB_NAME, DB_VERSION);\n        \n        request.onerror = () => reject(request.error);\n        request.onsuccess = () => resolve(request.result);\n        \n        request.onupgradeneeded = (event) => {\n            const db = event.target.result;\n            if (!db.objectStoreNames.contains(STORE_NAME)) {\n                db.createObjectStore(STORE_NAME, { keyPath: 'id' });\n            }\n        };\n    });\n};\n\n// 保存文件夹句柄到 IndexedDB\nconst saveFolderHandle = async (handle) => {\n    try {\n        const db = await initDB();\n        const transaction = db.transaction([STORE_NAME], 'readwrite');\n        const store = transaction.objectStore(STORE_NAME);\n        \n        await store.put({\n            id: 'lastSelectedFolder',\n            handle: handle,\n            name: handle.name,\n            timestamp: Date.now()\n        });\n        \n        console.log('文件夹句柄已保存');\n    } catch (error) {\n        console.error('保存文件夹句柄失败:', error);\n    }\n};\n\n// 从 IndexedDB 读取文件夹句柄\nconst loadFolderHandle = async () => {\n    try {\n        const db = await initDB();\n        const transaction = db.transaction([STORE_NAME], 'readonly');\n        const store = transaction.objectStore(STORE_NAME);\n        \n        return new Promise((resolve, reject) => {\n            const request = store.get('lastSelectedFolder');\n            request.onsuccess = () => {\n                const result = request.result;\n                if (result && result.handle) {\n                    resolve(result.handle);\n                } else {\n                    resolve(null);\n                }\n            };\n            request.onerror = () => reject(request.error);\n        });\n    } catch (error) {\n        console.error('读取文件夹句柄失败:', error);\n        return null;\n    }\n};\n\n// 检查是否支持 File System Access API\nconst checkFileSystemSupport = () => {\n    if (!('showDirectoryPicker' in window)) {\n        window.$modal.alert('您的浏览器不支持 File System Access API，请使用 Chrome 86+ 或 Edge 86+');\n        return false;\n    }\n    return true;\n};\n\n// 加载上次选择的文件夹\nconst loadLastFolder = async () => {\n    if (!checkFileSystemSupport()) return;\n    \n    try {\n        loading.value = true;\n        const savedHandle = await loadFolderHandle();\n        if (savedHandle) {\n            // 验证句柄是否仍然有效\n            const permission = await savedHandle.queryPermission({ mode: 'read' });\n            if (permission === 'granted') {\n                currentFolder.value = savedHandle;\n                await scanMusicFiles(savedHandle);\n                console.log('已自动加载上次选择的文件夹:', savedHandle.name);\n            } else {\n                // 请求权限\n                const newPermission = await savedHandle.requestPermission({ mode: 'read' });\n                if (newPermission === 'granted') {\n                    currentFolder.value = savedHandle;\n                    await scanMusicFiles(savedHandle);\n                    console.log('已重新获取权限并加载文件夹:', savedHandle.name);\n                }\n            }\n        }\n    } catch (error) {\n        console.error('自动加载文件夹失败:', error);\n    }\n    loading.value = false;\n};\n\n// 选择文件夹\nconst selectFolder = async () => {\n    if (!checkFileSystemSupport()) return;\n\n    try {\n        loading.value = true;\n        const dirHandle = await window.showDirectoryPicker();\n        currentFolder.value = dirHandle;\n        \n        // 保存句柄到 IndexedDB\n        await saveFolderHandle(dirHandle);\n        \n        // 扫描音乐文件\n        await scanMusicFiles(dirHandle);\n        \n        console.log(`已选择文件夹: ${dirHandle.name}`);\n    } catch (error) {\n        if (error.name !== 'AbortError') {\n            console.error('选择文件夹失败:', error);\n        }\n    } finally {\n        loading.value = false;\n    }\n};\n\n// 刷新文件夹\nconst refreshFolder = async () => {\n    if (!currentFolder.value) return;\n    \n    try {\n        refreshing.value = true;\n        await scanMusicFiles(currentFolder.value);\n        console.log('刷新完成');\n    } catch (error) {\n        console.error('刷新失败:', error);\n    } finally {\n        refreshing.value = false;\n    }\n};\n\n\n// 读取音频元数据\nconst readAudioMetadata = async (file) => {\n    try {\n        const metadata = await parseBlob(file);\n        \n        let coverUrl = './assets/images/ico.png';\n        \n        // 提取封面图片\n        if (metadata.common.picture && metadata.common.picture.length > 0) {\n            const picture = metadata.common.picture[0];\n            const blob = new Blob([picture.data], { type: picture.format });\n            coverUrl = URL.createObjectURL(blob);\n        }\n        \n        return {\n            title: file.name || '未知',\n            artist: metadata.common.artist || '未知',\n            album: metadata.common.album || '本地音乐',\n            year: metadata.common.year || null,\n            genre: metadata.common.genre ? metadata.common.genre.join(', ') : null,\n            track: metadata.common.track ? metadata.common.track.no : null,\n            duration: metadata.format.duration || 0,\n            bitrate: metadata.format.bitrate || null,\n            sampleRate: metadata.format.sampleRate || null,\n            cover: coverUrl\n        };\n    } catch (error) {\n        console.warn('parseBlob读取失败，跳过该歌曲:', error);\n        return null;\n    }\n};\n\n// 递归扫描目录\nconst scanDirectory = async (dirHandle, files = []) => {\n    try {\n        for await (const entry of dirHandle.values()) {\n            if (entry.kind === 'file') {\n                const file = await entry.getFile();\n                const extension = '.' + getFileExtension(file.name).toLowerCase();\n                \n                if (supportedFormats.includes(extension)) {\n                    // 读取音频元数据\n                    const metadata = await readAudioMetadata(file);\n                    \n                    if (metadata === null) {\n                        console.log(`跳过无法解析的歌曲: ${file.name}`);\n                        continue;\n                    }\n                    \n                    files.push({\n                        name: file.name,\n                        displayName: metadata.title,\n                        author: metadata.artist,\n                        album: metadata.album,\n                        year: metadata.year,\n                        genre: metadata.genre,\n                        track: metadata.track,\n                        size: file.size,\n                        filesize: formatFileSize(file.size),\n                        type: file.type,\n                        file: file,\n                        handle: entry,\n                        duration: metadata.duration,\n                        timelen: metadata.duration * 1000, // 转换为毫秒\n                        bitrate: metadata.bitrate,\n                        sampleRate: metadata.sampleRate,\n                        cover: metadata.cover,\n                        qualityInfo: getQualityInfo(extension, metadata.bitrate, metadata.sampleRate)\n                    });\n                }\n            } else if (entry.kind === 'directory') {\n                // 递归扫描子文件夹\n                await scanDirectory(entry, files);\n            }\n        }\n    } catch (error) {\n        console.error('扫描目录失败:', error);\n    }\n    \n    return files;\n};\n\n// 扫描音乐文件\nconst scanMusicFiles = async (dirHandle) => {\n    try {\n        // 递归扫描所有子文件夹\n        const files = await scanDirectory(dirHandle);\n        \n        // 按艺术家和标题排序\n        files.sort((a, b) => {\n            const artistCompare = a.author.localeCompare(b.author);\n            if (artistCompare !== 0) return artistCompare;\n            return a.displayName.localeCompare(b.displayName);\n        });\n        \n        musicFiles.value = files;\n        filteredTracks.value = files;\n        \n    } catch (error) {\n        console.error('扫描文件失败:', error);\n    }\n};\n\n// 获取音质信息\nconst getQualityInfo = (extension, bitrate, sampleRate) => {\n    // 无损格式\n    if (['.flac', '.wav', '.ape', '.alac'].includes(extension)) {\n        if (sampleRate >= 96000) {\n            return { text: 'HR', class: 'hr-icon' }; // Hi-Res\n        } else {\n            return { text: 'SQ', class: 'sq-icon' }; // Studio Quality\n        }\n    }\n    \n    \n    // 有损格式根据比特率判断\n    if (bitrate) {\n        if (bitrate >= 320) {\n            return { text: 'HQ', class: 'hq-icon' }; // High Quality\n        } else if (bitrate >= 192) {\n            return { text: 'MQ', class: 'mq-icon' }; // Medium Quality\n        }\n    }\n    \n    // 特殊格式标识\n    switch(extension) {\n        case '.m4a':\n        case '.aac':\n            return { text: 'AAC', class: 'hq-icon' };\n        case '.ogg':\n            return { text: 'OGG', class: 'hq-icon' };\n        default:\n            return null;\n    }\n};\n\n// 播放歌曲\nconst playSong = async (item) => {\n    try {\n        console.log('[LocalMusic] 开始播放本地音乐:', item.name);\n        if (props.playerControl) {\n            await props.playerControl.addLocalMusicToQueue(item);\n        }\n    } catch (error) {\n        console.error('[LocalMusic] 播放本地音乐失败:', error);\n        window.$modal?.alert('播放失败: ' + error.message);\n    }\n};\n\n// 添加整个播放列表到队列\nconst addPlaylistToQueue = async (event, append = false) => {\n    $message.warning('建设中');return;\n    const playButton = event.currentTarget;\n    const rect = playButton.getBoundingClientRect();\n    const note = {\n        id: noteId++,\n        style: {\n            '--start-x': `${rect.left + rect.width/2}px`,\n            '--start-y': `${rect.top + rect.height/2}px`,\n            'left': '0',\n            'top': '0'\n        }\n    };\n    flyingNotes.value.push(note);\n    setTimeout(() => {\n        flyingNotes.value = flyingNotes.value.filter(n => n.id !== note.id);\n    }, 1500);\n    \n    if (props.playerControl) {\n        console.log('[LocalMusic] 添加本地播放列表到队列:', filteredTracks.value.length, '首歌曲');\n        await props.playerControl.addLocalPlaylistToQueue(filteredTracks.value, append);\n    }\n};\n\n// 搜索歌曲\nconst searchTracks = () => {\n    filteredTracks.value = musicFiles.value.filter(track => \n        track.name.toLowerCase().trim().includes(searchQuery.value.toLowerCase().trim()) ||\n        track.author.toLowerCase().trim().includes(searchQuery.value.toLowerCase().trim())\n    );\n};\n\n// 切换列表模式\nconst toggleListMode = () => {\n    listMode.value = listMode.value === 'list' ? 'grid' : 'list';\n    localStorage.setItem('localMusicListMode', listMode.value);\n};\n\n// 格式化文件大小\nconst formatFileSize = (bytes) => {\n    if (!bytes || bytes === 0) return '0 B';\n    const k = 1024;\n    const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];\n    const i = Math.floor(Math.log(bytes) / Math.log(k));\n    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];\n};\n\n// 格式化时长\nconst formatDuration = (seconds) => {\n    if (!seconds || seconds === 0) return '00:00';\n    const mins = Math.floor(seconds / 60);\n    const secs = Math.floor(seconds % 60);\n    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;\n};\n\n// 获取文件扩展名\nconst getFileExtension = (filename) => {\n    return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2);\n};\n\n// 滚动到当前播放歌曲\nconst scrollToItem = () => {\n    const currentIndex = filteredTracks.value.findIndex(song => song.name === props.playerControl?.currentSong.name);\n    if (currentIndex !== -1) {\n        recycleScrollerRef.value.scrollToItem(currentIndex - 3, { behavior: 'smooth' });\n    }\n};\n\n// 滚动到顶部\nconst scrollToFirstItem = () => {\n    if (recycleScrollerRef.value) {\n        recycleScrollerRef.value.scrollToItem(0, { behavior: 'smooth' });\n    }\n    window.scrollTo({\n        top: 0,\n        behavior: 'smooth',\n        scrollSource: 'manual-button-click' \n    });\n};\n\nconst handleClickOutside = (event) => {\n    const batchActionsMenu = document.querySelector('.batch-actions-menu');\n    const batchActionBtn = document.querySelector('.batch-action-btn');\n    if (batchActionsMenu && !batchActionsMenu.contains(event.target) && !batchActionBtn.contains(event.target)) {\n        isBatchMenuVisible.value = false;\n    }\n};\n\n// 切换批量选择模式\nconst toggleBatchSelection = () => {\n    if (batchSelectionMode.value) {\n        if (isBatchMenuVisible.value) {\n            batchSelectionMode.value = false;\n            isBatchMenuVisible.value = false;\n            selectedTracks.value = [];\n            lastSelectedIndex = -1;\n        } else {\n            isBatchMenuVisible.value = true;\n        }\n    } else {\n        batchSelectionMode.value = true;\n        isBatchMenuVisible.value = false;\n    }\n};\n\n// 选择/取消选择歌曲\nconst selectTrack = (index, event) => {\n    if (event.shiftKey && lastSelectedIndex !== -1) {\n        const start = Math.min(lastSelectedIndex, index);\n        const end = Math.max(lastSelectedIndex, index);\n        \n        for (let i = start; i <= end; i++) {\n            if (!selectedTracks.value.includes(i)) {\n                selectedTracks.value.push(i);\n            }\n        }\n    } else if (event.ctrlKey || event.metaKey) {\n        const existingIndex = selectedTracks.value.indexOf(index);\n        if (existingIndex === -1) {\n            selectedTracks.value.push(index);\n        } else {\n            selectedTracks.value.splice(existingIndex, 1);\n        }\n    } else {\n        const existingIndex = selectedTracks.value.indexOf(index);\n        if (existingIndex === -1) {\n            selectedTracks.value = [index];\n        } else {\n            selectedTracks.value = [];\n        }\n    }\n    \n    lastSelectedIndex = index;\n};\n\n// 将选中歌曲添加到播放队列\nconst appendSelectedToQueue = async () => {\n    $message.warning('还未完善');return;\n    if (selectedTracks.value.length === 0) return;\n    const selectedSongs = selectedTracks.value.map(index => filteredTracks.value[index]);\n    if (props.playerControl) {\n        console.log('[LocalMusic] 添加选中的本地歌曲到播放列表:', selectedSongs.length, '首');\n        await props.playerControl.addLocalPlaylistToQueue(selectedSongs, true);\n        console.log('[LocalMusic] 选中歌曲添加到播放列表成功');\n    }\n    isBatchMenuVisible.value = false;\n    // 清空选择\n    selectedTracks.value = [];\n};\n\n// 切换全选/取消全选\nconst toggleSelectAll = () => {\n    if (isAllSelected.value) {\n        selectedTracks.value = [];\n    } else {\n        selectedTracks.value = Array.from({ length: filteredTracks.value.length }, (_, i) => i);\n    }\n};\n\n// 根据字段排序\nconst sortTracks = (field) => {\n    if (sortField.value === field) {\n        sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';\n    } else {\n        sortField.value = field;\n        sortOrder.value = 'asc';\n    }\n    \n    filteredTracks.value = [...filteredTracks.value].sort((a, b) => {\n        let valueA, valueB;\n        \n        if (field === 'timelen') {\n            valueA = a.duration || 0;\n            valueB = b.duration || 0;\n        } else if (field === 'size') {\n            valueA = a.size || 0;\n            valueB = b.size || 0;\n        } else {\n            valueA = (a[field] || '').toLowerCase();\n            valueB = (b[field] || '').toLowerCase();\n        }\n        \n        if (sortOrder.value === 'asc') {\n            return valueA > valueB ? 1 : -1;\n        } else {\n            return valueA < valueB ? 1 : -1;\n        }\n    });\n    \n    if (batchSelectionMode.value) {\n        selectedTracks.value = [];\n    }\n};\n\nconst getSortIconClass = (field) => {\n    if (sortField.value !== field) {\n        return 'fa-sort';\n    }\n    return sortOrder.value === 'asc' ? 'fa-sort-up' : 'fa-sort-down';\n};\n</script>\n\n<style scoped>\n.detail-page {\n    padding: 20px;\n}\n\n.header {\n    display: flex;\n    align-items: center;\n    margin-bottom: 40px;\n}\n\n.cover-art {\n    width: 200px;\n    height: 200px;\n    border-radius: 10px;\n    margin-right: 20px;\n    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\n    object-fit: cover;\n}\n\n.info {\n    max-width: 600px;\n}\n\n.title {\n    font-size: 36px;\n    font-weight: bold;\n    width: 800px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    margin: 0;\n    color: var(--primary-color);\n}\n\n.subtitle {\n    font-size: 18px;\n    color: #666;\n}\n\n.folder-info {\n    margin: 10px 0;\n}\n\n.folder-path {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    color: #666;\n    font-size: 14px;\n}\n\n.folder-path i {\n    color: var(--primary-color);\n}\n\n.description {\n    white-space: pre-wrap;\n    line-height: 1.6;\n    color: var(--text-color);\n    margin-bottom: 20px;\n    font-size: 16px;\n    max-height: 200px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: break-spaces;\n    overflow-y: auto;\n}\n\n.actions {\n    display: flex;\n    gap: 10px;\n}\n\n.primary-btn, .upload-btn {\n    background-color: var(--primary-color);\n    color: white;\n    border: none;\n    padding: 10px 20px;\n    border-radius: 5px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n}\n\n.upload-btn {\n    background-color: var(--primary-color);\n}\n\n.primary-btn i, .upload-btn i {\n    margin-right: 5px;\n}\n\n.primary-btn:disabled, .upload-btn:disabled {\n    opacity: 0.6;\n    cursor: not-allowed;\n}\n\n/* 歌曲列表样式 */\n.track-list-container {\n    margin-top: 30px;\n}\n\n.track-list-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 10px;\n}\n\n.track-list-title {\n    font-size: 24px;\n    font-weight: bold;\n    margin-bottom: 10px;\n    color: var(--primary-color);\n}\n\n.track-list-actions {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n}\n\n.batch-action-container {\n    position: relative;\n}\n\n.batch-action-btn {\n    background-color: transparent;\n    border: 1px solid var(--secondary-color);\n    padding: 5px 10px;\n    border-radius: 5px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: var(--text-color);\n    position: relative;\n}\n\n.batch-action-btn.active {\n    background-color: var(--primary-color);\n    color: white;\n}\n\n.selected-count {\n    position: absolute;\n    top: -8px;\n    right: -8px;\n    background-color: red;\n    color: white;\n    border-radius: 50%;\n    width: 20px;\n    height: 20px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 12px;\n    font-weight: bold;\n}\n\n.batch-actions-menu {\n    position: absolute;\n    top: 100%;\n    left: 0;\n    background-color: white;\n    border: 1px solid #ccc;\n    border-radius: 5px;\n    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n    z-index: 50;\n    margin-top: 5px;\n    width: 200px;\n}\n\n.batch-actions-menu ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n}\n\n.batch-actions-menu li {\n    padding: 10px 15px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    white-space: nowrap;\n}\n\n.batch-actions-menu li i {\n    margin-right: 10px;\n    width: 16px;\n    text-align: center;\n}\n\n.batch-actions-menu li:hover {\n    background-color: #f0f0f0;\n}\n\n.view-mode-btn {\n    background-color: transparent;\n    border: 1px solid var(--secondary-color);\n    padding: 5px 10px;\n    border-radius: 5px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: var(--text-color);\n    width: 36px;\n    height: 31px;\n    transition: all 0.3s ease;\n}\n\n.view-mode-btn:hover {\n    background-color: rgba(var(--primary-color-rgb), 0.1);\n}\n\n.view-mode-btn i {\n    font-size: 16px;\n}\n\n.search-input {\n    width: 250px;\n    padding: 8px;\n    border: 1px solid var(--secondary-color);\n    border-radius: 20px;\n    box-sizing: border-box;\n    padding-left: 15px;\n}\n\n.track-list {\n    height: 800px;\n    scrollbar-width: thin;\n    scrollbar-color: transparent transparent; \n    overflow: auto;\n}\n\n.track-list::-webkit-scrollbar {\n    width: 8px !important; \n    display: block !important;\n}\n\n.track-list:hover {\n    scrollbar-color: var(--primary-color) transparent;\n}\n\n.li {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 10px;\n    border-bottom: 1px solid #eee;\n    border-radius: 5px;\n    cursor: pointer;\n}\n\n.li:hover {\n    background-color: var(--background-color);\n}\n\n.li.selected {\n    background-color: rgba(var(--primary-color-rgb), 0.1);\n}\n\n/* 歌曲多选 */\n.track-checkbox {\n    margin-right: 10px;\n    width: 30px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.track-number {\n    font-weight: bold;\n    margin-right: 10px;\n    width: 30px;\n}\n\n.track-title {\n    flex: 2;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.track-size{\n    flex: 0.5;\n    text-align: center;\n}\n\n.track-artist {\n    flex: 1;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 0 10px;\n}\n\n.track-album {\n    flex: 1;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 0 10px;\n}\n\n.track-size{\n    flex: 0.5;\n    text-align: center;\n}\n\n.icon {\n    margin-left: 5px;\n    border: 1px solid;\n    border-radius: 5px;\n    font-size: 10px;\n    padding-left: 6px;\n    padding-right: 6px;\n}\n\n.hq-icon {\n    color: #0094ff;\n    border-color: #0094ff;\n}\n\n.hr-icon {\n    color: #ff6d00;\n    border-color: #ff6d00;\n}\n\n.queue-play-btn {\n    background: none;\n    border: none;\n    font-size: 16px;\n    color: var(--primary-color);\n    cursor: pointer;\n}\n\n/* 导航按钮 */\n.location-arrow {\n    position: fixed;\n    bottom: 168px;\n    right: 14px;\n    z-index: 1;\n    cursor: pointer;\n    font-size: 37px;\n    color: var(--primary-color);\n}\n\n.scroll-bottom-img {\n    position: fixed;\n    width: 60px;\n    height: 60px;\n    bottom: 110px;\n    right: 88px;\n    z-index: 1;\n    cursor: pointer;\n}\n\n/* 音符动画 */\n.note-container {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n    pointer-events: none;\n    overflow: hidden;\n}\n\n.flying-note {\n    position: absolute;\n    font-size: 36px;\n    color: var(--primary-color);\n    pointer-events: none;\n    transform-origin: center;\n}\n\n.fly-note-enter-active {\n    animation: fly-note 2s ease-out forwards;\n}\n\n.fly-note-leave-active {\n    animation: fly-note 2s ease-out forwards;\n}\n\n@keyframes fly-note {\n    0% {\n        transform: translate(var(--start-x), calc(var(--start-y) - 50px)) rotate(0deg) scale(1.2);\n        opacity: 0.9;\n    }\n    20% {\n        transform: translate(calc(var(--start-x) + 20px), calc(var(--start-y) - 70px)) rotate(45deg) scale(1.3);\n        opacity: 0.85;\n    }\n    100% {\n        transform: translate(80vw, 100vh) rotate(360deg) scale(0.6);\n        opacity: 0;\n    }\n}\n\n/* 表头样式 */\n.track-list-header-row {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 10px;\n    border-bottom: 1px solid var(--primary-color);\n    font-weight: bold;\n    background-color: rgba(var(--primary-color-rgb), 0.1);\n    border-radius: 5px 5px 0 0;\n}\n\n.track-checkbox-header {\n    margin-right: 10px;\n    width: 30px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.track-number-header {\n    font-weight: bold;\n    margin-right: 10px;\n    width: 30px;\n}\n\n.track-title-header, .track-artist-header, .track-timelen-header, .track-size-header {\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n}\n\n.track-title-header {\n    flex: 2;\n}\n\n.track-size-header{\n    flex: 0.5;\n    padding: 0 10px;\n}\n\n.track-artist-header {\n    flex: 1;\n    padding: 0 10px;\n}\n\n.track-album-header {\n    flex: 1;\n    padding: 0 10px;\n}\n\n.track-timelen-header {\n    text-align: right;\n}\n\n.track-title-header i, .track-artist-header i, .track-album-header i, .track-timelen-header i, .track-size-header i {\n    margin-left: 5px;\n    font-size: 14px;\n}\n\n.track-list-header-row:hover {\n    background-color: rgba(var(--primary-color-rgb), 0.15);\n}\n\n/* 网格视图样式 */\n.li.cover-view {\n    height: 60px;\n    padding: 5px 10px;\n    display: flex;\n    align-items: center;\n    border-bottom: 1px solid #eee;\n    border-radius: 5px;\n}\n\n.li.cover-view:hover {\n    background-color: var(--background-color);\n}\n\n.track-cover {\n    position: relative;\n    width: 50px;\n    height: 50px;\n    margin-right: 15px;\n    overflow: hidden;\n    border-radius: 4px;\n    flex-shrink: 0;\n}\n\n.track-cover img {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    transition: transform 0.3s ease;\n}\n\n.li.cover-view:hover .track-cover img {\n    transform: scale(1.05);\n}\n\n.track-cover-overlay {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0,0,0,0.5);\n    opacity: 0;\n    transition: opacity 0.3s ease;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: white;\n    font-size: 20px;\n}\n\n.li.cover-view:hover .track-cover-overlay {\n    opacity: 1;\n}\n\n.track-cover-overlay.playing {\n    opacity: 1;\n}\n\n.li.cover-view .track-title {\n    flex: 2;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.li.cover-view .track-artist {\n    flex: 1;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 0 10px;\n}\n\n.li.cover-view .track-album {\n    flex: 1;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 0 10px;\n}\n\n.li.cover-view .track-size {\n    flex: 0.5;\n    text-align: center;\n}\n\n.li.cover-view .track-timelen {\n    width: 95px;\n    text-align: right;\n}\n\n.li.cover-view .track-checkbox,\n.li.cover-view .track-number {\n    margin-right: 10px;\n    width: 30px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n/* 空状态和欢迎状态 */\n.empty-state,\n.welcome-state {\n    text-align: center;\n    padding: 60px 20px;\n    color: #666;\n}\n\n.empty-state i,\n.welcome-state i {\n    font-size: 48px;\n    color: #ddd;\n    margin-bottom: 20px;\n}\n\n.welcome-state h3 {\n    margin: 0px 0 10px;\n    color: #333;\n}\n\n.welcome-state p {\n    margin-bottom: 30px;\n    color: #666;\n}\n\n.hint {\n    font-size: 12px;\n    color: #999;\n    margin-top: 10px;\n}\n\n/* 加载状态 */\n.loading-state {\n    text-align: center;\n    padding: 60px 20px;\n    color: #666;\n}\n\n.loading-state p {\n    margin-top: 20px;\n}\n\n.loading-spinner {\n    width: 40px;\n    height: 40px;\n    border: 4px solid #f3f3f3;\n    border-top: 4px solid var(--primary-color);\n    border-radius: 50%;\n    animation: spin 1s linear infinite;\n    margin: 0 auto;\n}\n\n@keyframes spin {\n    0% { transform: rotate(0deg); }\n    100% { transform: rotate(360deg); }\n}\n</style>\n"
  },
  {
    "path": "src/views/Login.vue",
    "content": "<template>\n    <div class=\"login-page\">\n        <div class=\"login-container\">\n            <img src=\"https://www.kugou.com/yy/static/images/play/logo.png\" alt=\"App Logo\" class=\"logo\" />\n            <h2>{{ $t('deng-lu-ni-de-ku-gou-zhang-hao') }}</h2>\n            <div class=\"logintype-menu\">\n                <div class=\"segmented-control\">\n                    <button \n                        v-for=\"option in options\" \n                        :key=\"option\" \n                        :class=\"['segmented-button', { active: loginType === option }]\"\n                        @click=\"loginType = option; handleTabSwitch(option);\"\n                    >\n                        {{ option }}\n                    </button>\n                </div>\n            </div>\n            <div v-if=\"loginType === t('shou-ji-hao-deng-lu')\">\n                <!-- 账号选择界面 -->\n                <div v-if=\"showAccountSelection\" class=\"account-selection\">\n                    <p class=\"selection-tip\">该手机绑定多个账号，请选择要登录的账号</p>\n                    <div class=\"account-list\">\n                        <div \n                            v-for=\"account in accountList\" \n                            :key=\"account.userid\"\n                            class=\"account-item\"\n                            @click=\"selectAccount(account)\"\n                        >\n                            <div class=\"account-avatar\">\n                                <img :src=\"account.pic || './assets/images/profile.jpg'\" :alt=\"account.nickname\" />\n                            </div>\n                            <div class=\"account-info\">\n                                <div class=\"account-name\">{{ account.nickname || '未命名用户' }}</div>\n                                <div class=\"account-status\">\n                                    <span class=\"svip-badge\">Lv {{ account.p_grade }}</span>\n                                    <span class=\"user-level\">UID：{{ account.userid }}</span>\n                                </div>\n                            </div>\n                            <div class=\"select-arrow\">→</div>\n                        </div>\n                    </div>\n                    <button \n                        type=\"button\" \n                        class=\"back-button\" \n                        @click=\"backToLogin\"\n                    >\n                        返回登录\n                    </button>\n                </div>\n\n                <!-- 原登录表单 -->\n                <form v-else @submit.prevent class=\"login-form\">\n                    <div class=\"form-item\" :class=\"{ 'has-error': phoneFormErrors.mobile }\">\n                        <div class=\"input-wrapper\">\n                            <input \n                                v-model=\"phoneForm.mobile\" \n                                :placeholder=\"$t('qing-shu-ru-shou-ji-hao')\" \n                                class=\"form-input\"\n                                @blur=\"validateField('mobile', phoneForm.mobile)\"\n                            />\n                            <button type=\"button\" class=\"clear-button\" @click=\"phoneForm.mobile = ''\" v-if=\"phoneForm.mobile\">×</button>\n                        </div>\n                        <div class=\"error-message\" v-if=\"phoneFormErrors.mobile\">{{ phoneFormErrors.mobile }}</div>\n                    </div>\n                    <div class=\"form-item\" :class=\"{ 'has-error': phoneFormErrors.code }\">\n                        <div class=\"input-wrapper with-button\">\n                            <input \n                                v-model=\"phoneForm.code\" \n                                :placeholder=\"$t('qing-shu-ru-yan-zheng-ma')\" \n                                class=\"form-input form-code\"\n                                @blur=\"validateField('code', phoneForm.code)\"\n                            />\n                            <button type=\"button\" class=\"clear-button\" @click=\"phoneForm.code = ''\" v-if=\"phoneForm.code\">×</button>\n                            <button \n                                type=\"button\" \n                                class=\"append-button\" \n                                @click=\"sendCaptcha\" \n                                :disabled=\"!phoneForm.mobile || countdown > 0 || isSendingCaptcha\"\n                            >\n                                <span v-if=\"isSendingCaptcha\" class=\"loading-spinner\"></span>\n                                {{ countdown > 0 ? `${countdown}s` : $t('fa-song-yan-zheng-ma') }}\n                            </button>\n                        </div>\n                        <div class=\"error-message\" v-if=\"phoneFormErrors.code\">{{ phoneFormErrors.code }}</div>\n                    </div>\n                    <button \n                        type=\"button\" \n                        class=\"primary-button\" \n                        @click=\"phoneLogin\" \n                        :disabled=\"isPhoneLoginLoading\"\n                    >\n                        <span v-if=\"isPhoneLoginLoading\" class=\"loading-spinner\"></span>\n                        {{ $t('li-ji-deng-lu') }}\n                    </button>\n                </form>\n            </div>\n\n            <div v-if=\"loginType === t('you-xiang-deng-lu')\">\n                <form @submit.prevent class=\"login-form\">\n                    <div class=\"form-item\" :class=\"{ 'has-error': emailFormErrors.email }\">\n                        <div class=\"input-wrapper\">\n                            <input \n                                v-model=\"emailForm.email\" \n                                :placeholder=\"$t('qing-shu-ru-deng-lu-you-xiang')\" \n                                class=\"form-input\"\n                                @blur=\"validateField('email', emailForm.email)\"\n                            />\n                            <button type=\"button\" class=\"clear-button\" @click=\"emailForm.email = ''\" v-if=\"emailForm.email\">×</button>\n                        </div>\n                        <div class=\"error-message\" v-if=\"emailFormErrors.email\">{{ emailFormErrors.email }}</div>\n                    </div>\n                    <div class=\"form-item\" :class=\"{ 'has-error': emailFormErrors.password }\">\n                        <div class=\"input-wrapper\">\n                            <input \n                                v-model=\"emailForm.password\" \n                                type=\"password\" \n                                :placeholder=\"$t('qing-shu-ru-mi-ma')\" \n                                class=\"form-input\"\n                                @blur=\"validateField('password', emailForm.password)\"\n                            />\n                            <button type=\"button\" class=\"clear-button\" @click=\"emailForm.password = ''\" v-if=\"emailForm.password\">×</button>\n                        </div>\n                        <div class=\"error-message\" v-if=\"emailFormErrors.password\">{{ emailFormErrors.password }}</div>\n                    </div>\n                    <button \n                        type=\"button\" \n                        class=\"primary-button\" \n                        @click=\"emailLogin\" \n                        :disabled=\"isEmailLoginLoading\"\n                    >\n                        <span v-if=\"isEmailLoginLoading\" class=\"loading-spinner\"></span>\n                        {{ $t('you-xiang-deng-lu') }}\n                    </button>\n                </form>\n            </div>\n\n            <div v-if=\"loginType === t('sao-ma-deng-lu')\">\n                <div class=\"qr-login\">\n                    <p>{{ tips }}</p>\n                    <img :src=\"qrCode\" v-if=\"qrCode\" :alt=\"$t('er-wei-ma')\" class=\"qr-code\" />\n                    <div class=\"empty-container\" v-else>\n                        <div class=\"empty-icon\"><i class=\"fa fa-qrcode\"></i></div>\n                        <div class=\"empty-text\">{{ t('zheng-zai-sheng-cheng-er-wei-ma') }}</div>\n                    </div>\n                </div>\n            </div>\n\n            <p class=\"disclaimer\">\n                {{ $t('login-tips') }}<b>{{ $t('tui-jian') }}</b>{{ $t('shi-yong-yan-zheng-ma-deng-lu') }}\n            </p>\n            <p class=\"register-link\">\n                <a @click.prevent=\"openRegisterUrl('https://activity.kugou.com/getvips/v-4163b2d0/index.html')\" href=\"#\">{{ $t('zhu-ce') }}</a>\n            </p>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref, reactive } from 'vue';\nimport { get } from '../utils/request';\nimport { MoeAuthStore } from '../stores/store';\nimport { useRouter, useRoute } from 'vue-router';\nimport { useI18n } from 'vue-i18n';\nimport { openRegisterUrl } from '../utils/utils';\nconst { t } = useI18n();\nconst loginType = ref(t('shou-ji-hao-deng-lu'));\nconst options = [t('shou-ji-hao-deng-lu'), t('you-xiang-deng-lu'), t('sao-ma-deng-lu')];\n\nconst MoeAuth = MoeAuthStore();\nconst router = useRouter();\nconst route = useRoute();\n\nconst emailForm = reactive({\n    email: '',\n    password: ''\n});\n\nconst phoneForm = reactive({\n    mobile: '',\n    code: ''\n});\n\nconst showAccountSelection = ref(false);\nconst accountList = ref([]);\n\n// 表单验证错误信息\nconst phoneFormErrors = reactive({\n    mobile: '',\n    code: ''\n});\n\nconst emailFormErrors = reactive({\n    email: '',\n    password: ''\n});\n\n// 验证字段\nconst validateField = (field, value) => {\n    if (field === 'mobile') {\n        if (!value) {\n            phoneFormErrors.mobile = t('qing-shu-ru-shou-ji-hao-ma');\n        } else if (!/^1\\d{10}$/.test(value)) {\n            phoneFormErrors.mobile = t('shou-ji-hao-ge-shi-cuo-wu');\n        } else {\n            phoneFormErrors.mobile = '';\n        }\n    } else if (field === 'code') {\n        if (!value) {\n            phoneFormErrors.code = t('qing-shu-ru-yan-zheng-ma');\n        } else {\n            phoneFormErrors.code = '';\n        }\n    } else if (field === 'email') {\n        if (!value) {\n            emailFormErrors.email = t('qing-shu-ru-you-xiang');\n        } else {\n            emailFormErrors.email = '';\n        }\n    } else if (field === 'password') {\n        if (!value) {\n            emailFormErrors.password = t('qing-shu-ru-mi-ma');\n        } else {\n            emailFormErrors.password = '';\n        }\n    }\n};\n\nconst qrKey = ref('');\nconst qrCode = ref('');\nconst tips = ref(t('qing-shi-yong-ku-gou-sao-miao-er-wei-ma-deng-lu'));\nconst isSendingCaptcha = ref(false);\nconst countdown = ref(0);\nconst isPhoneLoginLoading = ref(false);\nconst isEmailLoginLoading = ref(false);\nconst interval = ref(null);\n\n// 账号密码登录\nconst emailLogin = async () => {\n    if (!emailForm.email) {\n        $message.error(t('qing-shu-ru-you-xiang'));\n        return;\n    }\n    if (!emailForm.password) {\n        $message.error(t('qing-shu-ru-mi-ma'));\n        return;\n    }\n    isEmailLoginLoading.value = true;\n    try {\n        const response = await get(`/login?username=${emailForm.email}&password=${emailForm.password}`);\n        if (response.status === 1) {\n            MoeAuth.setData({ UserInfo: response.data });\n            router.push(route.query.redirect || '/library');\n            $message.success(t('deng-lu-cheng-gong'));\n        }\n    } catch (error) {\n        console.error(error.response.data);\n        $message.error(error.response?.data?.data || t('deng-lu-shi-bai'));\n    } finally {\n        isEmailLoginLoading.value = false;\n    }\n};\n\n// 发送验证码\nconst sendCaptcha = async () => {\n    if (!phoneForm.mobile) {\n        $message.warning(t('qing-shu-ru-shou-ji-hao'));\n        return;\n    }\n    // 验证手机号格式\n    const mobilePattern = /^1\\d{10}$/;\n    if (!mobilePattern.test(phoneForm.mobile)) {\n        $message.warning(t('shou-ji-hao-ge-shi-cuo-wu'));\n        return;\n    }\n    isSendingCaptcha.value = true;\n    try {\n        const response = await get(`/captcha/sent?mobile=${phoneForm.mobile}`);\n        if (response.status === 1) {\n            $message.success(t('yan-zheng-ma-yi-fa-song'));\n            countdown.value = 60;\n            const timer = setInterval(() => {\n                countdown.value--;\n                if (countdown.value <= 0) {\n                    clearInterval(timer);\n                }\n            }, 1000);\n        }\n    } catch (error) {\n        console.error(error.response.data);\n        $message.error(error.response.data.data || t('yan-zheng-ma-fa-song-shi-bai'));\n    } finally {\n        isSendingCaptcha.value = false;\n    }\n};\n\nconst phoneLogin = async (selectedUserId = null) => {\n    if (!phoneForm.mobile) {\n        $message.warning(t('qing-shu-ru-shou-ji-hao'));\n        return;\n    }\n    if (!phoneForm.code) {\n        $message.warning(t('qing-shu-ru-yan-zheng-ma'));\n        return;\n    }\n    isPhoneLoginLoading.value = true;\n    try {\n        let url = `/login/cellphone?mobile=${phoneForm.mobile}&code=${phoneForm.code}`;\n        if (selectedUserId) {\n            url += `&userid=${selectedUserId}`;\n        }\n        const response = await get(url);\n        if (response.status === 1) {\n            MoeAuth.setData({ UserInfo: response.data });\n            router.push(route.query.redirect || '/library');\n            $message.success(t('deng-lu-cheng-gong'));\n        }\n    } catch (error) {\n        if (error.response.data?.data?.info_list && !selectedUserId) {\n            accountList.value = error.response.data.data.info_list;\n            showAccountSelection.value = true;\n        } else {\n            $message.error(error.response.data?.data || t('deng-lu-shi-bai'));\n        }\n        console.error(error.response.data);\n    } finally {\n        isPhoneLoginLoading.value = false;\n    }\n};\n\n// 切换登录方式\nconst handleTabSwitch = (value) => {\n    clearInterval(interval.value);\n    if (value === t('sao-ma-deng-lu')) {\n        getQrCode();\n    }\n};\n\n// 获取二维码\nconst getQrCode = async () => {\n    try {\n        // 获取二维码 key\n        const keyResponse = await get('/login/qr/key');\n        if (keyResponse.status === 1) {\n            qrKey.value = keyResponse.data.qrcode;\n\n            // 使用 key 创建二维码\n            const qrResponse = await get(`/login/qr/create?key=${qrKey.value}&qrimg=true`);\n            if (qrResponse.code === 200) {\n                qrCode.value = qrResponse.data.base64;\n                checkQrStatus();\n            } else {\n                $message.error(t('huo-qu-er-wei-ma-shi-bai'));\n            }\n        } else {\n            $message.error(t('er-wei-ma-sheng-cheng-shi-bai'));\n        }\n    } catch {\n        $message.error(t('er-wei-ma-sheng-cheng-shi-bai'));\n    }\n};\n\n// 选择账号登录\nconst selectAccount = async (account) => {\n    isPhoneLoginLoading.value = true;\n    await phoneLogin(account.userid);\n};\n\n// 返回登录界面\nconst backToLogin = () => {\n    showAccountSelection.value = false;\n    accountList.value = [];\n};\n\n// 检查二维码扫描状态\nconst checkQrStatus = async () => {\n    interval.value = setInterval(async () => {\n        try {\n            const response = await get(`/login/qr/check?key=${qrKey.value}&timestamp=${Date.now()}`, {} ,{\n                headers: {\n                    'Cache-Control': 'no-cache'\n                }\n            });\n            if (response.status === 1) {\n                if (response.data.status === 2) {\n                    tips.value = t('yong-hu')+` ${response.data.nickname} `+ t('yi-sao-ma-deng-dai-que-ren');\n                } else if (response.data.status === 4) {\n                    clearInterval(interval.value);\n                    MoeAuth.setData({ UserInfo: response.data });\n                    router.push(route.query.redirect || '/library');\n                    $message.success(t('er-wei-ma-deng-lu-cheng-gong'));\n                } else if (response.data.status === 0) {\n                    clearInterval(interval.value);\n                    $message.error(t('er-wei-ma-yi-guo-qi-qing-zhong-xin-sheng-cheng'));\n                }\n            }\n        } catch {\n            clearInterval(interval.value);\n            $message.error(t('er-wei-ma-jian-ce-shi-bai'));\n        }\n    }, 1000);\n};\n\n</script>\n\n<style scoped>\n.login-page {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  position: relative;\n  margin-top: 100px;\n}\n\n\n.login-container {\n  background-color: #fff;\n  border-radius: 20px;\n  width: 400px;\n  box-shadow: 0 15px 30px rgba(0, 0, 0, 0.1);\n  text-align: center;\n  padding: 30px 25px;\n  position: relative;\n  overflow: hidden;\n  z-index: 1;\n  transition: all 0.4s ease;\n  border: 1px solid rgba(255, 255, 255, 0.2);\n  padding-bottom: 0px;\n}\n\n.login-container::before {\n  content: '';\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 3px;\n  background: var(--primary-color);\n  border-radius: 3px;\n}\n\n.logo {\n  width: 65px;\n  margin: 0 auto 0px;\n  display: block;\n  filter: drop-shadow(0 4px 6px rgba(0, 0, 0, 0.1));\n  transition: all 0.5s ease;\n}\n\n.logo:hover {\n  transform: scale(1.1) rotate(5deg);\n  filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.15));\n}\n\nh2 {\n  color: #2c3e50;\n  margin-bottom: 20px;\n  font-weight: 600;\n  font-size: 1.4rem;\n  position: relative;\n  display: inline-block;\n}\n\nh2::after {\n  content: '';\n  position: absolute;\n  bottom: -6px;\n  left: 30%;\n  width: 40%;\n  height: 3px;\n  background: linear-gradient(90deg, transparent, var(--primary-color), transparent);\n  border-radius: 3px;\n}\n\n.login-form {\n  display: flex;\n  flex-direction: column;\n  gap: 16px;\n  margin-top: 8px;\n}\n\n.form-item {\n  margin-bottom: 12px;\n  text-align: left;\n  position: relative;\n}\n\n.form-item.has-error .form-input {\n  border-color: #f56c6c;\n  box-shadow: 0 0 0 2px rgba(245, 108, 108, 0.1);\n}\n\n.form-item .form-code {\n  border-radius: 10px 0 0 10px;\n}\n\n.input-wrapper {\n  position: relative;\n  display: flex;\n  align-items: center;\n}\n\n.input-wrapper.with-button {\n  display: flex;\n}\n\n.form-input {\n  width: 100%;\n  height: 42px;\n  line-height: 42px;\n  padding: 0 14px;\n  border: 1px solid #dcdfe6;\n  border-radius: 10px;\n  transition: all 0.3s;\n  outline: none;\n  box-sizing: border-box;\n  font-size: 14px;\n  background-color: #f9fafc;\n}\n\n.form-input:focus {\n  border-color: var(--primary-color);\n  box-shadow: 0 0 0 3px var(--color-box-shadow);\n  background-color: #fff;\n}\n\n.clear-button {\n  position: absolute;\n  right: 14px;\n  background: none;\n  border: none;\n  color: #c0c4cc;\n  cursor: pointer;\n  font-size: 16px;\n  padding: 0;\n  z-index: 1;\n  transition: all 0.2s;\n  width: 20px;\n  height: 20px;\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.clear-button:hover {\n  color: #909399;\n  background-color: rgba(0, 0, 0, 0.05);\n}\n\n.with-button .clear-button {\n  right: 110px;\n}\n\n.append-button {\n  min-width: 100px;\n  height: 42px;\n  background: var(--primary-color);\n  color: white;\n  border: none;\n  border-radius: 0 10px 10px 0;\n  cursor: pointer;\n  padding: 0 12px;\n  white-space: nowrap;\n  font-weight: 500;\n  transition: all 0.3s;\n  font-size: 13px;\n  position: relative;\n  overflow: hidden;\n}\n\n.append-button::before {\n  content: '';\n  position: absolute;\n  top: 0;\n  left: -100%;\n  width: 100%;\n  height: 100%;\n  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);\n  transition: all 0.6s;\n}\n\n.append-button:hover:not(:disabled)::before {\n  left: 100%;\n}\n\n.append-button:hover:not(:disabled) {\n  background: var(--primary-color);\n  box-shadow: 0 4px 10px var(--color-box-shadow);\n}\n\n.error-message {\n  color: #f56c6c;\n  font-size: 12px;\n  margin-top: 4px;\n  display: flex;\n  align-items: center;\n  animation: fadeIn 0.3s ease;\n}\n\n@keyframes fadeIn {\n  from { opacity: 0; transform: translateY(-5px); }\n  to { opacity: 1; transform: translateY(0); }\n}\n\n.error-message::before {\n  content: \"!\";\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  width: 14px;\n  height: 14px;\n  background-color: #f56c6c;\n  color: white;\n  border-radius: 50%;\n  margin-right: 6px;\n  font-size: 10px;\n  font-weight: bold;\n}\n\n.primary-button {\n  width: 100%;\n  height: 42px;\n  background: var(--primary-color);\n  color: white;\n  border: none;\n  border-radius: 10px;\n  cursor: pointer;\n  font-size: 15px;\n  font-weight: 500;\n  position: relative;\n  transition: all 0.3s;\n  box-shadow: 0 6px 12px var(--color-box-shadow);\n  overflow: hidden;\n}\n\n.primary-button::before {\n  content: '';\n  position: absolute;\n  top: 0;\n  left: -100%;\n  width: 100%;\n  height: 100%;\n  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);\n  transition: all 0.6s;\n}\n\n.primary-button:hover:not(:disabled)::before {\n  left: 100%;\n}\n\n.primary-button:hover:not(:disabled) {\n  background: var(--primary-color);\n  box-shadow: 0 8px 16px var(--color-box-shadow);\n  transform: translateY(-2px);\n}\n\n.primary-button:active:not(:disabled) {\n  transform: translateY(0);\n  box-shadow: 0 4px 8px var(--color-box-shadow);\n}\n\n.primary-button:disabled {\n  background: linear-gradient(90deg, #a0cfff, #b8dcff);\n  cursor: not-allowed;\n  box-shadow: none;\n}\n\n.loading-spinner {\n  display: inline-block;\n  width: 14px;\n  height: 14px;\n  border: 2px solid rgba(255, 255, 255, 0.3);\n  border-radius: 50%;\n  border-top-color: #fff;\n  animation: spin 0.8s linear infinite;\n  margin-right: 6px;\n  vertical-align: middle;\n}\n\n@keyframes spin {\n  to { transform: rotate(360deg); }\n}\n\n.qr-login {\n  text-align: center;\n  margin-top: 15px;\n  padding: 8px;\n}\n\n.qr-login p {\n  margin-bottom: 12px;\n  color: #606266;\n  font-size: 14px;\n}\n\n.qr-code {\n  width: 180px;\n  height: 180px;\n  border-radius: 14px;\n  border: 1px solid #eaeaea;\n  padding: 10px;\n  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.06);\n  transition: all 0.4s;\n  background-color: white;\n  margin: 0 auto;\n}\n\n.qr-code:hover {\n  transform: scale(1.03);\n  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);\n}\n\n.empty-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  height: 200px;\n  border: 1px solid #eaeaea;\n  border-radius: 14px;\n  margin: 0 auto;\n  width: 200px;\n  background-color: #f9fafc;\n  box-shadow: 0 8px 16px rgba(0, 0, 0, 0.06);\n  transition: all 0.3s;\n}\n\n.empty-container:hover {\n  box-shadow: 0 12px 24px rgba(0, 0, 0, 0.1);\n  transform: translateY(-3px);\n}\n\n.empty-icon {\n  font-size: 50px;\n  margin-bottom: 14px;\n  color: var(--primary-color);\n  animation: pulse 2s infinite;\n}\n\n@keyframes pulse {\n  0% { opacity: 0.6; transform: scale(0.95); }\n  50% { opacity: 1; transform: scale(1.05); }\n  100% { opacity: 0.6; transform: scale(0.95); }\n}\n\n.empty-text {\n  color: #606266;\n  font-size: 14px;\n  font-weight: 500;\n}\n\n.disclaimer {\n  font-size: 12px;\n  color: #909399;\n  margin-top: 20px;\n  line-height: 1.5;\n  border-top: 1px solid #ebeef5;\n  padding-top: 14px;\n  text-align: left;\n  background-color: #f9fafc;\n  padding: 14px;\n  border-radius: 10px;\n  position: relative;\n}\n\n.disclaimer::before {\n  content: '!';\n  position: absolute;\n  top: -10px;\n  left: 20px;\n  width: 18px;\n  height: 18px;\n  background-color: var(--primary-color);\n  color: white;\n  border-radius: 50%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-weight: bold;\n  font-size: 11px;\n}\n\n.logintype-menu {\n  margin-bottom: 20px;\n}\n\n.segmented-control {\n  display: flex;\n  width: 100%;\n  border-radius: 12px;\n  overflow: hidden;\n  border: 1px solid #e4e7ed;\n  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.04);\n  position: relative;\n  z-index: 1;\n}\n\n.segmented-button {\n  flex: 1;\n  padding: 10px 0;\n  text-align: center;\n  background: #f5f7fa;\n  border: none;\n  cursor: pointer;\n  transition: all 0.3s;\n  font-size: 14px;\n  color: #606266;\n  position: relative;\n  overflow: hidden;\n  font-weight: 500;\n}\n\n.segmented-button:not(:last-child)::after {\n  content: '';\n  position: absolute;\n  right: 0;\n  top: 20%;\n  height: 60%;\n  width: 1px;\n  background-color: #e4e7ed;\n}\n\n.segmented-button:hover:not(.active) {\n  background-color: #ebeef5;\n  color: #303133;\n}\n\n.segmented-button.active {\n  background: var(--primary-color);\n  color: white;\n  font-weight: 600;\n  box-shadow: 0 4px 8px var(--color-box-shadow);\n}\n\n.segmented-button.active::after {\n  display: none;\n}\n\n.register-link {\n  text-align: center;\n  color: #606266;\n  margin-top: 18px;\n  font-size: 13px;\n}\n\n.register-link a {\n  color: var(--primary-color);\n  text-decoration: none;\n  cursor: pointer;\n  font-weight: 600;\n  transition: all 0.3s;\n  border-radius: 6px;\n  display: inline-block;\n  margin-top: 3px;\n}\n\n.register-link a:hover {\n  color: var(--primary-color);\n  background-color: var(--color-secondary-bg-for-transparent);\n  transform: translateY(-2px);\n  box-shadow: 0 4px 8px var(--color-box-shadow);\n}\n\n/* 账号选择界面样式 */\n.account-selection {\n  text-align: center;\n  margin-top: 8px;\n}\n\n.account-selection h3 {\n  color: var(--text-color);\n  margin-bottom: 8px;\n  font-weight: 600;\n  font-size: 1.2rem;\n}\n\n.selection-tip {\n  color: var(--text-color);\n  opacity: 0.7;\n  font-size: 13px;\n  margin-bottom: 20px;\n  line-height: 1.4;\n}\n\n.account-list {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n  margin-bottom: 20px;\n}\n\n.account-item {\n  display: flex;\n  align-items: center;\n  padding: 12px 16px;\n  border: 1px solid var(--border-color);\n  border-radius: 12px;\n  cursor: pointer;\n  transition: all 0.3s;\n  position: relative;\n  overflow: hidden;\n}\n\n.account-item::before {\n  content: '';\n  position: absolute;\n  top: 0;\n  left: -100%;\n  width: 100%;\n  height: 100%;\n  background: linear-gradient(90deg, transparent, var(--color-primary-light), transparent);\n  transition: all 0.6s;\n}\n\n.account-item:hover {\n  border-color: var(--primary-color);\n  background-color: var(--hover-color);\n  box-shadow: 0 4px 12px var(--color-box-shadow);\n  transform: translateY(-2px);\n}\n\n.account-item:hover::before {\n  left: 100%;\n}\n\n.account-item:active {\n  transform: translateY(0);\n  box-shadow: 0 2px 6px var(--color-box-shadow);\n}\n\n.account-avatar {\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n  margin-right: 12px;\n  overflow: hidden;\n  flex-shrink: 0;\n  border: 2px solid var(--border-color);\n  transition: all 0.3s;\n}\n\n.account-item:hover .account-avatar {\n  border-color: var(--primary-color);\n  transform: scale(1.05);\n}\n\n.account-avatar img {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n}\n\n.account-info {\n  flex: 1;\n  text-align: left;\n}\n\n.account-name {\n  font-weight: 600;\n  color: var(--text-color);\n  font-size: 15px;\n  margin-bottom: 4px;\n}\n\n.account-status {\n  display: flex;\n  align-items: center;\n  gap: 6px;\n  font-size: 12px;\n}\n\n.vip-badge {\n  background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n  color: white;\n  padding: 2px 6px;\n  border-radius: 4px;\n  font-weight: bold;\n  font-size: 10px;\n}\n\n.svip-badge {\n  background: linear-gradient(45deg, var(--color-primary), var(--primary-color));\n  color: white;\n  padding: 2px 6px;\n  border-radius: 4px;\n  font-weight: bold;\n  font-size: 10px;\n}\n\n.user-level {\n  color: var(--text-color);\n  opacity: 0.6;\n  font-size: 11px;\n}\n\n.select-arrow {\n  color: var(--primary-color);\n  font-size: 18px;\n  font-weight: bold;\n  transition: all 0.3s;\n}\n\n.account-item:hover .select-arrow {\n  transform: translateX(4px);\n}\n\n.back-button {\n  width: 100%;\n  height: 40px;\n  background: var(--background-color-secondary);\n  color: var(--text-color);\n  border: 1px solid var(--border-color);\n  border-radius: 10px;\n  cursor: pointer;\n  font-size: 14px;\n  font-weight: 500;\n  transition: all 0.3s;\n}\n\n.back-button:hover {\n  background: var(--hover-color);\n  color: var(--text-color);\n  border-color: var(--primary-color);\n}\n\n\n/* 响应式调整 */\n@media (max-width: 480px) {\n  .login-container {\n    width: 100%;\n    padding: 25px 18px;\n    border-radius: 18px;\n  }\n  \n  .form-input, .primary-button, .append-button {\n    height: 40px;\n  }\n  \n  h2 {\n    font-size: 1.3rem;\n  }\n  \n  .qr-code, .empty-container {\n    width: 170px;\n    height: 170px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/views/Lyrics.vue",
    "content": "<template>\n    <div class=\"lyrics-container\" :class=\"{ 'locked': isLocked, 'hovering': isHovering && !isLocked }\">\n        <!-- 控制栏 -->\n        <div class=\"controls-overlay\" ref=\"controlsOverlay\">\n            <div class=\"controls-wrapper\" :class=\"{ 'locked-controls': isLocked }\">\n                <template v-if=\"!isLocked\">\n                    <div class=\"color-controls\">\n                        <button \n                            class=\"color-button\"\n                            title=\"默认颜色\"\n                            @click=\"$refs.defaultColorInput.click()\"\n                        >\n                            <div class=\"color-preview\" :style=\"{ backgroundColor: defaultColor }\"></div>\n                        </button>\n                        <button \n                            class=\"color-button\"\n                            title=\"高亮颜色\"\n                            @click=\"$refs.highlightColorInput.click()\"\n                        >\n                            <div class=\"color-preview\" :style=\"{ backgroundColor: highlightColor }\"></div>\n                        </button>\n                        <input\n                            ref=\"defaultColorInput\"\n                            type=\"color\"\n                            :value=\"defaultColor\"\n                            @input=\"e => handleColorChange(e.target.value, 'default')\"\n                            class=\"hidden-color-input\"\n                        >\n                        <input\n                            ref=\"highlightColorInput\"\n                            type=\"color\"\n                            :value=\"highlightColor\"\n                            @input=\"e => handleColorChange(e.target.value, 'highlight')\"\n                            class=\"hidden-color-input\"\n                        >\n                    </div>\n                    <button @click=\"changeFontSize(-2)\" class=\"font-control\" title=\"减小字体\">\n                        <i class=\"fas fa-minus\"></i>\n                        <i class=\"fas fa-font\"></i>\n                    </button>\n                    <button @click=\"sendAction('previous-song')\" title=\"上一首\">\n                        <i class=\"fas fa-step-backward\"></i>\n                    </button>\n                    <button @click=\"togglePlay\" :title=\"isPlaying ? '暂停' : '播放'\">\n                        <i :class=\"isPlaying ? 'fas fa-pause' : 'fas fa-play'\"></i>\n                    </button>\n                    <button @click=\"sendAction('next-song')\" title=\"下一首\">\n                        <i class=\"fas fa-step-forward\"></i>\n                    </button>\n                    <button @click=\"changeFontSize(2)\" class=\"font-control\" title=\"增大字体\">\n                        <i class=\"fas fa-font\"></i>\n                        <i class=\"fas fa-plus\"></i>\n                    </button>\n                    <button @click=\"toggleLock\" class=\"lock-button\" :title=\"isLocked ? '解锁' : '锁定'\">\n                        <i :class=\"isLocked ? 'fas fa-lock' : 'fas fa-lock-open'\"></i>\n                    </button>\n                    <button @click=\"sendAction('close-lyrics')\" title=\"关闭歌词\">\n                        <i class=\"fas fa-times\"></i>\n                    </button>\n                </template>\n                <template v-else>\n                    <button @click=\"toggleLock\" class=\"lock-button\" :title=\"isLocked ? '解锁' : '锁定'\">\n                        <i :class=\"isLocked ? 'fas fa-lock' : 'fas fa-lock-open'\"></i>\n                    </button>\n                </template>\n            </div>\n        </div>\n        <!-- 歌词内容 -->\n        <div \n            class=\"lyrics-content-wrapper\"\n            ref=\"lyricsContainerRef\"\n            :class=\"{ 'locked': isLocked }\"\n            :style=\"containerStyle\"\n        >\n            <template v-if=\"lyrics.length\">\n                <div class=\"lyrics-line\">\n                    <div class=\"lyrics-content\" \n                        ref=\"activeLyricsContentRef\"\n                        :style=\"currentLineStyle\"\n                        :class=\"{ 'hovering': isHovering && !isLocked }\"\n                    >\n                        <span class=\"lyrics-text\">\n                            <span class=\"lyrics-layer lyrics-layer-default\" :style=\"defaultLineStyle\">\n                                {{ lyrics[displayedLines[0]]?.text || '' }}\n                            </span>\n                            <span class=\"lyrics-layer lyrics-layer-highlight\" :style=\"getLineHighlightStyle(displayedLines[0])\">\n                                {{ lyrics[displayedLines[0]]?.text || '' }}\n                            </span>\n                        </span>\n                    </div>\n                </div>\n                <div class=\"lyrics-line\" v-if=\"lyrics[displayedLines[0]]?.translated\">\n                    <div class=\"lyrics-content\" :class=\"{ 'hovering': isHovering && !isLocked }\">\n                        <span class=\"lyrics-text lyrics-text-static\">\n                            <span class=\"lyrics-layer lyrics-layer-default\" :style=\"defaultLineStyle\">\n                                {{ lyrics[displayedLines[0]].translated }}\n                            </span>\n                        </span>\n                    </div>\n                </div>\n                <div class=\"lyrics-line\" v-else-if=\"lyrics[displayedLines[1]]\">\n                    <div class=\"lyrics-content\" :class=\"{ 'hovering': isHovering && !isLocked }\">\n                        <span class=\"lyrics-text\">\n                            <span class=\"lyrics-layer lyrics-layer-default\" :style=\"defaultLineStyle\">\n                                {{ lyrics[displayedLines[1]]?.text || '' }}\n                            </span>\n                            <span class=\"lyrics-layer lyrics-layer-highlight\" :style=\"getLineHighlightStyle(displayedLines[1])\">\n                                {{ lyrics[displayedLines[1]]?.text || '' }}\n                            </span>\n                        </span>\n                    </div>\n                </div>\n            </template>\n            <div v-else class=\"lyrics-content hovering nolyrics\">暂无歌词</div>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'\n\nlet currentSongHash = ''\n\nconst isPlaying = ref(false)\nconst isLocked = ref(false)\nconst controlsOverlay = ref(null)\nconst lyricsContainerRef = ref(null)\nconst activeLyricsContentRef = ref(null)\n\nconst currentTime = ref(0)\nconst currentLineIndex = ref(0)\nconst lyrics = ref([])\nconst currentLineScrollX = ref(0)\nconst isDragging = ref(false)\nconst dragOffset = ref({ x: 0, y: 0 })\nconst currentLineStyle = computed(() => ({\n    transform: `translateX(${currentLineScrollX.value}px)`\n}))\n\nconst throttle = (func, delay) => {\n    let lastTime = 0\n    return (...args) => {\n        const now = Date.now()\n        if (now - lastTime >= delay) {\n            lastTime = now\n            func(...args)\n        }\n    }\n}\n\n// Linux 下 setIgnoreMouseEvents 的 forward 选项不可用，避免直接吃掉鼠标事件导致无法点击\nconst isLinux = window?.electron?.platform === 'linux'\nconst setIgnoreMouseEvents = (ignore) => {\n    if (isLinux) return\n    window.electron.ipcRenderer.send('set-ignore-mouse-events', ignore)\n}\n\nconst sendWindowDrag = throttle((mouseX, mouseY) => {\n    window.electron.ipcRenderer.send('window-drag', { mouseX, mouseY })\n}, 16)\n\nconst displayedLines = ref([0, 1]) \nconst defaultColor = ref(localStorage.getItem('lyrics-default-color') || '#D4D4D4')\nconst highlightColor = ref(localStorage.getItem('lyrics-highlight-color') || 'var(--primary-color)')\nconst FALLBACK_COLOR = '#D4D4D4'\nlet colorContext = null\n\nconst clampColorChannel = (value) => Math.max(0, Math.min(255, Math.round(value)))\n\nconst getColorContext = () => {\n    if (colorContext) return colorContext\n    const canvas = document.createElement('canvas')\n    colorContext = canvas.getContext('2d')\n    return colorContext\n}\n\nconst parseHexColor = (hex) => {\n    const cleanHex = hex.replace('#', '')\n    if (cleanHex.length === 3) {\n        return {\n            r: parseInt(cleanHex[0] + cleanHex[0], 16),\n            g: parseInt(cleanHex[1] + cleanHex[1], 16),\n            b: parseInt(cleanHex[2] + cleanHex[2], 16),\n        }\n    }\n    return {\n        r: parseInt(cleanHex.slice(0, 2), 16),\n        g: parseInt(cleanHex.slice(2, 4), 16),\n        b: parseInt(cleanHex.slice(4, 6), 16),\n    }\n}\n\nconst parseRgbColorString = (rgbString) => {\n    const match = rgbString.match(/rgba?\\(([^)]+)\\)/i)\n    if (!match) return null\n    const [r = '0', g = '0', b = '0'] = match[1].split(',').map((item) => item.trim())\n    return {\n        r: clampColorChannel(Number.parseFloat(r)),\n        g: clampColorChannel(Number.parseFloat(g)),\n        b: clampColorChannel(Number.parseFloat(b)),\n    }\n}\n\nconst resolveCssVarColor = (color) => {\n    const rawColor = `${color || ''}`.trim()\n    if (!rawColor) return FALLBACK_COLOR\n    if (!rawColor.startsWith('var(')) return rawColor\n\n    const varMatch = rawColor.match(/^var\\(\\s*([^,\\s)]+)\\s*(?:,\\s*(.+))?\\)$/)\n    if (!varMatch) return FALLBACK_COLOR\n\n    const [, cssVarName, fallback = ''] = varMatch\n    const cssValue = getComputedStyle(document.documentElement).getPropertyValue(cssVarName).trim()\n    return cssValue || fallback.trim() || FALLBACK_COLOR\n}\n\nconst parseColorToRgb = (color) => {\n    const ctx = getColorContext()\n    if (!ctx) return parseHexColor(FALLBACK_COLOR)\n\n    ctx.fillStyle = FALLBACK_COLOR\n    ctx.fillStyle = resolveCssVarColor(color)\n    const normalized = ctx.fillStyle\n\n    if (normalized.startsWith('#')) return parseHexColor(normalized)\n    if (normalized.startsWith('rgb')) {\n        const rgb = parseRgbColorString(normalized)\n        if (rgb) return rgb\n    }\n    return parseHexColor(FALLBACK_COLOR)\n}\n\nconst rgbToHex = ({ r, g, b }) => `#${[r, g, b].map((channel) => clampColorChannel(channel).toString(16).padStart(2, '0')).join('')}`\n\nconst shiftRgb = (rgb, offset) => ({\n    r: clampColorChannel(rgb.r + offset),\n    g: clampColorChannel(rgb.g + offset),\n    b: clampColorChannel(rgb.b + offset),\n})\n\nconst buildVerticalGradient = (baseColor) => {\n    const rgb = parseColorToRgb(baseColor)\n    const topColor = rgbToHex(shiftRgb(rgb, -38))\n    const bottomColor = rgbToHex(shiftRgb(rgb, 28))\n    return `linear-gradient(to bottom, ${topColor}, ${bottomColor})`\n}\n\nconst defaultLineStyle = computed(() => ({\n    background: buildVerticalGradient(defaultColor.value),\n    backgroundClip: 'text',\n    WebkitBackgroundClip: 'text',\n    WebkitTextFillColor: 'transparent',\n    color: 'transparent',\n    textShadow: '0 1px 2px rgba(0, 0, 0, 0.2)',\n    fontWeight: 'bold',\n}))\n\nconst handleColorChange = (color, type) => {\n    if (type === 'default') {\n        defaultColor.value = color\n        localStorage.setItem('lyrics-default-color', color)\n    } else {\n        highlightColor.value = color\n        localStorage.setItem('lyrics-highlight-color', color)\n    }\n}\n\nconst sendAction = (action) => {\n    window.electron.ipcRenderer.send('desktop-lyrics-action', action)\n}\n\nconst togglePlay = () => {\n    isPlaying.value = !isPlaying.value\n    sendAction('toggle-play')\n}\n\nconst toggleLock = () => {\n    isLocked.value = !isLocked.value\n    localStorage.setItem('lyrics-lock', isLocked.value)\n    if (isLocked.value) {\n        isHovering.value = false\n        setIgnoreMouseEvents(true)\n    }\n}\n\n// 更新当前行索引\nconst updateCurrentLineIndex = () => {\n    const currentTimeMs = currentTime.value\n    \n    for (let i = 0; i < lyrics.value.length; i++) {\n        const line = lyrics.value[i]\n        if (!line?.characters?.length) continue\n        \n        const lineStartTime = line.characters[0].startTime\n        const lineEndTime = line.characters[line.characters.length - 1].endTime\n        \n        if (currentTimeMs >= lineStartTime && currentTimeMs <= lineEndTime) {\n            if (currentLineIndex.value !== i) {\n                currentLineIndex.value = i\n                updateDisplayedLines()\n            }\n            break\n        }\n    }\n}\n\nconst updateDisplayedLines = () => {\n    const currentIdx = currentLineIndex.value\n    if (lyrics.value[currentIdx]?.translated?.length) {\n        displayedLines.value = [currentIdx];\n        return\n    }\n    setTimeout(() => {\n        if (currentIdx % 2) displayedLines.value = [currentIdx + 1, currentIdx]\n        else displayedLines.value = [currentIdx, currentIdx + 1]\n        currentLineScrollX.value = 0\n    }, 200)\n}\n\n// 开始拖动\nconst startDrag = (event) => {\n    if (isLocked.value) return\n    \n    // 只有在悬停状态下才允许拖动（即只有先碰到歌词文本后才能拖动）\n    if (isHovering.value) {\n        isDragging.value = true\n        dragOffset.value = {\n            x: event.clientX,\n            y: event.clientY\n        }\n    }\n}\n\n// 检查鼠标是否在交互区域\nconst checkMousePosition = (event) => {\n    if (isLocked.value) {\n        // 检查鼠标是否在歌词文本上或控制按钮上\n        // const isMouseOnLyrics = event.target.closest('.lyrics-content') !== null\n        const isMouseInControls = event.target.closest('.controls-overlay') !== null || event.target.closest('.lock-button') !== null\n        \n        // 当鼠标在歌词文本上或者在控制按钮上时，显示控制按钮\n        if (isMouseInControls) {\n            document.querySelector('.controls-overlay')?.classList.add('show-locked-controls')\n        } else {\n            document.querySelector('.controls-overlay')?.classList.remove('show-locked-controls')\n        }\n        \n        setIgnoreMouseEvents(!(isMouseInControls))\n        return\n    }\n    \n    // 使用更可靠的方法检查鼠标位置\n    const lyricsContainer = document.querySelector('.lyrics-container')\n    if (!lyricsContainer) return\n    \n    const rect = lyricsContainer.getBoundingClientRect()\n    const isMouseInContainer = (\n        event.clientX >= rect.left &&\n        event.clientX <= rect.right &&\n        event.clientY >= rect.top &&\n        event.clientY <= rect.bottom\n    )\n    \n    // 检查鼠标是否在歌词文本上或控制栏上\n    const isMouseOnLyrics = event.target.closest('.lyrics-content') !== null\n    const isMouseInControls = event.target.closest('.controls-overlay') !== null\n    \n    // 如果鼠标在歌词文本上或控制栏上，激活悬停状态\n    if ((isMouseOnLyrics || isMouseInControls) && !isLocked.value) {\n        isHovering.value = true\n    }\n    \n    // 只有当鼠标完全离开容器时才重置悬停状态\n    if (!isMouseInContainer && !isLocked.value) {\n        isHovering.value = false\n    }\n    \n    // 设置鼠标事件穿透，当在控制区域或悬停状态时不穿透\n    setIgnoreMouseEvents(!(isMouseInControls || isHovering.value))\n}\n\nwindow.electron.ipcRenderer.on('lyrics-data', (_event, data) => {\n    if (data.currentTime < 1) {\n        lyrics.value = processLyricsData(data.lyricsData);\n    }\n    else if (data.lyricsData.length && data.currentSongHash != currentSongHash) {\n        currentSongHash = data.currentSongHash\n        lyrics.value = processLyricsData(data.lyricsData);\n        currentLineIndex.value = 0;\n        currentTime.value = 0;\n        currentLineScrollX.value = 0;\n        displayedLines.value = [0, 1];\n    } \n    currentTime.value = data.currentTime * 1000;\n    updateCurrentLineIndex();\n})\n\n// 处理歌词数据，添加完整的文本\nconst processLyricsData = (lyricsData) => {\n    return lyricsData.map(line => {\n        if (line.characters && line.characters.length) {\n            // 为每行添加完整文本\n            line.text = line.characters.map(char => char.char).join('');\n        }\n        return line;\n    });\n}\n\nwindow.electron.ipcRenderer.on('playing-status', (_event, playing)=>{\n    isPlaying.value = !!playing\n})\n\nconst fontSize = ref(32)\nconst changeFontSize = (delta) => {\n    fontSize.value = Math.max(12, Math.min(72, fontSize.value + delta))\n    localStorage.setItem('lyrics-font-size', fontSize.value)\n}\n\nonMounted(() => {\n    isLocked.value = localStorage.getItem('lyrics-lock') === 'true'\n    setIgnoreMouseEvents(true)\n    \n    document.addEventListener('mousemove', checkMousePosition)\n    document.addEventListener('mousedown', startDrag)\n    document.addEventListener('mousemove', onDrag)\n    document.addEventListener('mouseup', endDrag)\n    fontSize.value = parseInt(localStorage.getItem('lyrics-font-size') || '32')\n    setInterval(() => {isPlaying.value && (currentTime.value += 5)}, 5)\n})\n\nconst onDrag = (event) => {\n    if (!isDragging.value) return\n\n    const deltaX = event.screenX - dragOffset.value.x\n    const deltaY = event.screenY - dragOffset.value.y\n\n    sendWindowDrag(deltaX, deltaY)\n}\n\nconst endDrag = () => {\n    isDragging.value = false\n}\n\nonBeforeUnmount(() => {\n    document.removeEventListener('mousemove', checkMousePosition)\n    document.removeEventListener('mousedown', startDrag)\n    document.removeEventListener('mousemove', onDrag)\n    document.removeEventListener('mouseup', endDrag)\n})\n\nconst isHovering = ref(false)\n\nconst containerStyle = computed(() => ({\n    fontSize: `${fontSize.value}px`\n}))\n\nconst computeHighlightMetrics = (lineIndex) => {\n    const line = lyrics.value[lineIndex]\n    if (!line || !line.characters || !line.characters.length) {\n        return {\n            progress: 0\n        }\n    }\n\n    const characters = line.characters\n    const text = line.text || ''\n    const safeTextLength = Math.max(1, text.length)\n    const lineStartTime = characters[0].startTime\n    const lineEndTime = characters[characters.length - 1].endTime\n    const lineDuration = Math.max(1, lineEndTime - lineStartTime)\n\n    if (currentTime.value < lineStartTime) {\n        return {\n            progress: 0\n        }\n    }\n\n    if (currentTime.value >= lineEndTime) {\n        return {\n            progress: 1\n        }\n    }\n\n    let charBasedPosition = 0\n    for (let i = 0; i < characters.length; i++) {\n        const char = characters[i]\n        const startTime = char.startTime\n        const endTime = char.endTime\n\n        if (currentTime.value >= startTime && currentTime.value <= endTime) {\n            const charDuration = Math.max(1, endTime - startTime)\n            const progress = (currentTime.value - startTime) / charDuration\n            const charWidth = 100 / safeTextLength\n            charBasedPosition = (i / safeTextLength * 100) + (progress * charWidth)\n            break\n        }\n\n        if (currentTime.value > endTime) {\n            charBasedPosition = ((i + 1) / safeTextLength) * 100\n        }\n    }\n\n    const lineProgress = Math.min(1, Math.max(0, (currentTime.value - lineStartTime) / lineDuration))\n    let highlightPosition = Math.max(charBasedPosition, lineProgress * 100)\n    highlightPosition = Math.max(0, Math.min(100, highlightPosition))\n\n    return {\n        progress: highlightPosition / 100\n    }\n}\n\nconst getLineHighlightStyle = (lineIndex) => ({\n    width: `${(computeHighlightMetrics(lineIndex).progress * 100).toFixed(3)}%`,\n    background: buildVerticalGradient(highlightColor.value),\n    backgroundClip: 'text',\n    WebkitBackgroundClip: 'text',\n    WebkitTextFillColor: 'transparent',\n    color: 'transparent',\n    textShadow: '0 1px 2px rgba(0, 0, 0, 0.2)',\n    fontWeight: 'bold',\n})\nconst getHighlightProgress = (lineIndex) => computeHighlightMetrics(lineIndex).progress\n\nconst updateActiveLineScroll = () => {\n    const activeIndex = displayedLines.value[0];\n    const containerEl = lyricsContainerRef.value;\n    const contentEl = activeLyricsContentRef.value;\n\n    if (activeIndex === undefined || !containerEl || !contentEl) {\n        return;\n    }\n\n    const activeLine = lyrics.value[activeIndex];\n    if (!activeLine) {\n        if (currentLineScrollX.value !== 0) currentLineScrollX.value = 0;\n        return;\n    }\n\n    const containerWidth = containerEl.clientWidth || 0;\n    const contentWidth = contentEl.scrollWidth || 0;\n    const overflow = contentWidth - containerWidth;\n\n    if (overflow <= 0) {\n        if (currentLineScrollX.value !== 0) currentLineScrollX.value = 0;\n        return;\n    }\n\n    const progress = Math.min(1, Math.max(0, getHighlightProgress(activeIndex)));\n\n    if (progress <= 0) {\n        if (currentLineScrollX.value !== 0) currentLineScrollX.value = 0;\n        return;\n    }\n\n    const targetOffset = -overflow * progress;\n    const clampedOffset = Math.max(-overflow, Math.min(0, targetOffset));\n\n    if (currentLineScrollX.value !== clampedOffset) {\n        currentLineScrollX.value = clampedOffset;\n    }\n};\n\nconst scheduleActiveLineScroll = throttle(() => {\n    if (typeof requestAnimationFrame === 'function') {\n        requestAnimationFrame(updateActiveLineScroll);\n    } else {\n        updateActiveLineScroll();\n    }\n}, 50);\n\nwatch([currentTime, currentLineIndex, () => displayedLines.value], () => {\n    scheduleActiveLineScroll();\n}, { immediate: true });\n\nwatch(() => lyrics.value, () => {\n    scheduleActiveLineScroll();\n});\n\nwatch(fontSize, () => {\n    scheduleActiveLineScroll();\n});\n</script>\n\n<style>\nbody,\nhtml {\n    background-color: rgba(0, 0, 0, 0);\n}\n</style>\n<style scoped>\n.lyrics-text {\n    display: inline-block;\n    position: relative;\n    transform: translateZ(0);\n    white-space: pre;\n    letter-spacing: 0.5px;\n}\n\n.lyrics-layer {\n    display: block;\n    background-clip: text;\n    -webkit-background-clip: text;\n    color: transparent;\n    font-weight: bold;\n    white-space: pre;\n    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n\n.lyrics-layer-default {\n    position: relative;\n}\n\n.lyrics-layer-highlight {\n    position: absolute;\n    left: 0;\n    top: 0;\n    overflow: hidden;\n    width: 0;\n    max-width: 100%;\n    will-change: width;\n}\n\n.lyrics-text-static .lyrics-layer {\n    position: relative;\n}\n\n.lyrics-container {\n    backdrop-filter: blur(10px);\n    border-radius: 12px;\n    user-select: none;\n    display: flex;\n    flex-direction: column;\n    justify-content: flex-end;\n    align-items: center;\n    cursor: inherit;\n    font-weight: bold;\n    position: fixed;\n    bottom: 0;\n    left: 0;\n    right: 0;\n    height: auto;\n    transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);\n    transform: translateZ(0); \n    /* margin: 8px; 移出事件和窗口缩放冲突，暂未解决 */\n    padding: 8px 0;\n    overflow: hidden;\n}\n\n.lyrics-container.hovering {\n    background-color: rgba(0, 0, 0, 0.4);\n    cursor: move;\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);\n    border: 1px solid rgba(255, 255, 255, 0.08);\n}\n\n.lyrics-content-wrapper {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n    width: 100%;\n    transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);\n}\n\n.controls-overlay {\n    opacity: 0;\n    transition: opacity 0.4s cubic-bezier(0.4, 0.0, 0.2, 1);\n    margin-bottom: 10px;\n    height: 40px;\n    position: relative;\n    z-index: 10;\n    pointer-events: auto; /* 确保控制栏可以接收鼠标事件 */\n}\n\n.lyrics-container.hovering .controls-overlay {\n    opacity: 1;\n}\n\n.lyrics-container.locked .controls-overlay {\n    opacity: 0;\n}\n\n.lyrics-container.locked .controls-overlay.show-locked-controls {\n    opacity: 1;\n}\n\n.controls-wrapper {\n    display: flex;\n    gap: 15px;\n    justify-content: center;\n    background: rgba(30, 30, 30, 0.75);\n    padding: 6px 12px;\n    border-radius: 20px;\n    backdrop-filter: blur(4px);\n    transition: all 0.3s ease;\n    width: auto;\n    min-width: 430px;\n    border: 1px solid rgba(255, 255, 255, 0.1);\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);\n}\n\n.lock-button {\n    position: relative;\n    z-index: 3;\n}\n\n.lock-button i {\n    font-size: 13px !important;\n}\n\n.controls-wrapper.locked-controls {\n    background: rgba(30, 30, 30, 0.75);\n    padding: 6px;\n    width: auto;\n    min-width: auto;\n    border-radius: 50%;\n}\n\n.controls-wrapper button {\n    background: rgba(50, 50, 50, 0.7);\n    border: 1px solid rgba(255, 255, 255, 0.15) !important;\n    color: white;\n    cursor: pointer;\n    width: 28px !important;\n    height: 28px !important;\n    border-radius: 50%;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);\n    transform: scale(1);\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n}\n\n.controls-wrapper button:hover {\n    transform: scale(1.1);\n    background: rgba(80, 80, 80, 0.8);\n    border-color: rgba(255, 255, 255, 0.25) !important;\n}\n\n.controls-wrapper button:active {\n    transform: scale(0.95);\n}\n\n.controls-wrapper i {\n    font-size: 16px;\n}\n\n.lyrics-line {\n    overflow: hidden;\n    position: relative;\n    filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.2));\n    opacity: 1;\n    transform: translateY(0);\n    will-change: background-position;\n}\n\n.lyrics-content {\n    display: inline-block;\n    white-space: nowrap;\n    transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);\n    border-radius: 6px;\n    transform: translateX(0);\n    background-color: transparent;\n}\n\n.lyrics-container:not(.locked) .lyrics-content.hovering:hover {\n    cursor: move;\n}\n\n.nolyrics{\n    margin-bottom: 30px;\n}\n\n.controls-wrapper:not(.locked-controls) {\n    cursor: move;\n}\n\n.font-size-controls {\n    display: none;\n}\n\n.font-control {\n    opacity: 0.8;\n    padding: 0 6px;\n    display: flex;\n    align-items: center;\n    gap: 2px;\n    width: auto !important;\n    transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);\n    transform: scale(1);\n}\n\n.font-control:hover {\n    opacity: 1;\n    transform: scale(1.05);\n}\n\n.font-control i {\n    font-size: 12px;\n}\n\n.font-control i.fa-font {\n    font-size: 14px;\n    margin: 0 1px;\n}\n\n.font-icon {\n    display: none;\n}\n\n.color-controls {\n    display: flex;\n    gap: 4px;\n    align-items: center;\n}\n\n.color-button {\n    padding: 2px !important;\n    width: 24px !important;\n    height: 24px !important;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    border: 1px solid rgba(255, 255, 255, 0.2) !important;\n    transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);\n    transform: scale(1);\n}\n\n.color-button:hover {\n    transform: scale(1.1);\n    border-color: rgba(255, 255, 255, 0.4) !important;\n}\n\n.color-preview {\n    width: 16px;\n    height: 16px;\n    border-radius: 4px;\n    border: 1px solid rgba(255, 255, 255, 0.3);\n    transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n}\n\n.hidden-color-input {\n    position: absolute;\n    visibility: hidden;\n    width: 0;\n    height: 0;\n    padding: 0;\n    margin: 0;\n    border: none;\n}\n</style>\n"
  },
  {
    "path": "src/views/PlaylistDetail.vue",
    "content": "<template>\n    <div class=\"detail-page\">\n        <!-- 头部信息区域 -->\n        <div class=\"header\">\n            <img class=\"cover-art\" :class=\"isArtist ? 'artist-avatar' : ''\" :data-playlist-id=\"detail.listid || null\"\n                :src=\"isArtist ? ($getCover(detail.sizable_avatar, 480)) : (detail.pic ? $getCover(detail.pic, 480) : './assets/images/live.png')\" />\n            <div class=\"info\">\n                <h1 class=\"title\">{{ isArtist ? detail.author_name : detail.name }}</h1>\n                <p class=\"subtitle\" v-if=\"!isArtist && !isAlbum\">{{ detail.publish_date }} | {{ detail.list_create_username }}</p>\n                <p class=\"subtitle\" v-if=\"isAlbum\">{{ detail.publish_date }}</p>\n                <div class=\"stats\" v-if=\"isArtist\">\n                    <span>歌曲: {{ detail.song_count }}</span>\n                    <span>专辑: {{ detail.album_count }}</span>\n                    <span>MV: {{ detail.mv_count }}</span>\n                    <span>粉丝: {{ detail.fansnums }}</span>\n                </div>\n                <p class=\"meta\" v-if=\"!isArtist && !isAlbum\">{{ detail.tags }}</p>\n                <div class=\"description\">{{ isArtist ? detail.intro : detail.intro }}</div>\n                <div class=\"actions\">\n                    <button class=\"primary-btn\" @click=\"addPlaylistToQueue($event)\">\n                        <i class=\"fas fa-play\"></i> {{ $t('bo-fang') }}\n                    </button>\n                    <button class=\"follow-btn\" v-if=\"isArtist\" @click=\"toggleFollow\" :disabled=\"followLoading\">\n                        <i class=\"fas fa-heart\"></i> {{ isFollowed ? '已关注' : '关注' }}\n                    </button>\n                    <button class=\"fav-btn\" v-if=\"!isArtist && !isAlbum && detail.list_create_userid != MoeAuth.UserInfo?.userid && !route.query.listid\"\n                        @click=\"toggleFavorite(detail.list_create_gid)\" :class=\"{ 'active': isPlaylistFavorited }\">\n                        <i class=\"fas fa-heart\"></i>\n                    </button>\n                    <div class=\"more-btn-container\" v-if=\"!isArtist && !isAlbum\">\n                        <button class=\"more-btn\" @click=\"toggleDropdown\">\n                            <i class=\"fas fa-ellipsis-h\"></i>\n                        </button>\n                        <div v-if=\"isDropdownVisible\" class=\"dropdown-menu\">\n                            <ul>\n                                <li @click=\"deletePlaylist(detail.listid)\" v-if=\"(detail.list_create_userid == MoeAuth.UserInfo?.userid || route.query.listid) && detail.sort > 1\">\n                                    <i class=\"fas fa-trash-alt\"></i>\n                                </li>\n                                <li @click=\"sharePlaylist\">\n                                    <i class=\"fas fa-share-alt\"></i>\n                                </li>\n                                <li @click=\"addPlaylistToQueue($event,true)\" title=\"添加至播放列表\">\n                                    <i class=\"fas fa-add\"></i>\n                                </li>\n                            </ul>\n                        </div>\n                    </div>\n                </div>\n            </div>\n        </div>\n\n        <!-- 导航按钮 -->\n        <i class=\"location-arrow fas fa-crosshairs\" @click=\"scrollToItem\" :title=\"t('dang-qian-bo-fang-ge-qu')\"></i>\n        <i class=\"scroll-bottom-img fas fa-angle-double-up\" @click=\"scrollToFirstItem\" :title=\"t('fan-hui-ding-bu')\"></i>\n\n        <!-- 歌曲列表 -->\n        <div class=\"track-list-container\">\n            <div class=\"track-list-header\">\n                <h2 class=\"track-list-title\"><span>{{ $t('ge-qu-lie-biao') }}</span> ( {{ displayTrackCount }} )</h2>\n                <div class=\"track-list-actions\">\n                    <div class=\"batch-action-container\">\n                        <button class=\"batch-action-btn\" @click=\"toggleBatchSelection\" :class=\"{ 'active': batchSelectionMode }\">\n                            <input type=\"checkbox\" v-model=\"batchSelectionMode\" /> 批量操作\n                            <span v-if=\"selectedTracks.length > 0\" class=\"selected-count\">{{ selectedTracks.length }}</span>\n                        </button>\n                        <div v-if=\"batchSelectionMode && isBatchMenuVisible && selectedTracks.length > 0\" class=\"batch-actions-menu\">\n                            <ul>\n                                <li @click=\"appendSelectedToQueue\"><i class=\"fas fa-list\"></i> 添加到播放列表 </li>\n                                <li @click=\"addSelectedToOtherPlaylist\" v-if=\"MoeAuth.UserInfo?.userid\"><i class=\"fas fa-folder-plus\"></i> 添加到其他歌单</li>\n                                <li v-if=\"!isArtist && detail.list_create_userid == MoeAuth.UserInfo?.userid && route.query.listid\" \n                                    @click=\"removeSelectedFromPlaylist\"><i class=\"fas fa-trash-alt\"></i> 取消收藏</li>\n                            </ul>\n                        </div>\n                    </div>\n                    <!-- 歌手歌曲排序选择 -->\n                    <div v-if=\"isArtist\" class=\"sort-selector\">\n                        <button class=\"sort-btn\" :class=\"{ 'active': artistSortType === 'hot' }\" @click=\"changeArtistSort('hot')\">\n                            热门\n                        </button>\n                        <button class=\"sort-btn\" :class=\"{ 'active': artistSortType === 'new' }\" @click=\"changeArtistSort('new')\">\n                            最新\n                        </button>\n                    </div>\n                    <button class=\"view-mode-btn\" @click=\"toggleViewMode\" :title=\"viewMode === 'list' ? '切换到网格视图' : '切换到列表视图'\">\n                        <i class=\"fas\" :class=\"viewMode === 'list' ? 'fa-th' : 'fa-list'\"></i>\n                    </button>\n                    <input type=\"text\" v-model=\"searchQuery\" @keyup.enter=\"searchTracks\" :placeholder=\"t('sou-suo-ge-qu')\" class=\"search-input\" />\n                </div>\n            </div>\n\n            <!-- 表头 -->\n            <div class=\"track-list-header-row\">\n                <div class=\"track-checkbox-header\" v-if=\"batchSelectionMode\">\n                    <input type=\"checkbox\" :checked=\"isAllSelected\" @click=\"toggleSelectAll\">\n                </div>\n                <div class=\"track-number-header\" v-else>♪</div>\n                <div class=\"track-title-header\" @click=\"sortTracks('name')\">\n                    歌名 <i class=\"fas\" :class=\"getSortIconClass('name')\"></i>\n                </div>\n                <div class=\"track-artist-header\" @click=\"sortTracks('author')\">\n                    歌手 <i class=\"fas\" :class=\"getSortIconClass('author')\"></i>\n                </div>\n                <div class=\"track-album-header\" @click=\"sortTracks('album')\">\n                    专辑 <i class=\"fas\" :class=\"getSortIconClass('album')\"></i>\n                </div>\n                <div class=\"track-timelen-header\" @click=\"sortTracks('timelen')\">\n                    时间 <i class=\"fas\" :class=\"getSortIconClass('timelen')\"></i>\n                </div>\n            </div>\n\n            <!-- 搜索加载动画 -->\n            <div v-if=\"isSearching\" class=\"search-loading-overlay\">\n                <div class=\"search-loading-spinner\">\n                    <i class=\"fas fa-spinner fa-spin\"></i>\n                    <span>{{ $t('zheng-zai-jia-zai-quan-bu-ge-qu') }}</span>\n                </div>\n            </div>\n\n            <RecycleScroller v-else ref=\"recycleScrollerRef\" :items=\"filteredTracks\" :item-size=\"viewMode === 'list' ? 50 : 70\" class=\"track-list\" key-field=\"hash\" @scroll=\"handleScroll\">\n                <template #default=\"{ item, index }\">\n                    <div class=\"li\" :key=\"item.hash\"\n                        :class=\"{ 'cover-view': viewMode === 'grid', 'selected': selectedTracks.includes(index) }\"\n                        @click=\"batchSelectionMode ? selectTrack(index, $event) : playSong(item.hash, item.name, item.cover, item.author)\"\n                        @contextmenu.prevent=\"showContextMenu($event, item)\">\n\n                        <!-- 复选框或序号 -->\n                        <div class=\"track-checkbox\" v-if=\"batchSelectionMode\">\n                            <input type=\"checkbox\" :checked=\"selectedTracks.includes(index)\" @click.stop=\"selectTrack(index, $event)\">\n                        </div>\n                        <div class=\"track-number\" v-else :class=\"{ 'current': isCurrentSong(item.hash) }\">\n                            <div v-if=\"isCurrentPlaying(item.hash)\" class=\"sound-wave\">\n                                <span></span><span></span><span></span>\n                            </div>\n                            <span v-else>{{ index + 1 }}</span>\n                        </div>\n\n                        <!-- 网格模式封面 -->\n                        <div class=\"track-cover\" v-if=\"viewMode === 'grid'\">\n                            <img :src=\"item.cover || './assets/images/ico.png'\" alt=\"Cover\">\n                            <div class=\"track-cover-overlay\">\n                                <i :class=\"props.playerControl?.currentSong.hash == item.hash ? 'fas fa-music' : 'fas fa-play'\"></i>\n                            </div>\n                        </div>\n\n                        <!-- 歌曲信息 -->\n                        <div class=\"track-title-container\">\n                            <div class=\"track-title\" :title=\"item.name\" :class=\"{ 'current': isCurrentSong(item.hash) }\">\n                                <span class=\"track-title-text\">{{ item.name }}</span>\n                                <span class=\"track-title-tags\">\n                                    <span v-if=\"item.privilege == 10\" class=\"icon vip-icon\">VIP</span>\n                                    <span v-if=\"item.isSQ\" class=\"icon sq-icon\">SQ</span>\n                                    <span v-else-if=\"item.isHQ\" class=\"icon sq-icon\">HQ</span>\n                                    <span v-if=\"item.mvhash\" class=\"icon mv-icon\">MV</span>\n                                </span>\n                            </div>\n                            <div v-if=\"viewMode === 'grid' && item.remark\" :title=\"item.remark\" class=\"track-remark\">{{ item.remark }}</div>\n                        </div>\n                        <div class=\"track-artist\" :title=\"item.author\">{{ item.author }}</div>\n                        <div class=\"track-album\" :title=\"item.album\">{{ item.album }}</div>\n                        <div class=\"track-timelen\">\n                            <button v-if=\"props.playerControl?.currentSong.hash == item.hash && viewMode === 'list'\"\n                                class=\"queue-play-btn fas fa-music\"></button>\n                            {{ $formatMilliseconds(item.timelen) }}\n                        </div>\n                    </div>\n                </template>\n            </RecycleScroller>\n        </div>\n\n        <!-- 歌手简介部分 -->\n        <div class=\"content-section\" v-if=\"isArtist && detail.long_intro && detail.long_intro.length\">\n            <div v-for=\"(section, index) in detail.long_intro\" :key=\"index\" class=\"intro-section\">\n                <h3>{{ section.title }}</h3>\n                <div class=\"section-content\">{{ section.content }}</div>\n            </div>\n        </div>\n\n        <ContextMenu ref=\"contextMenuRef\" :playerControl=\"playerControl\" @songRemoved=\"handleSongRemoved\" />\n        <div class=\"note-container\">\n            <transition-group name=\"fly-note\">\n                <div v-for=\"note in flyingNotes\" :key=\"note.id\" class=\"flying-note\" :style=\"note.style\">♪</div>\n            </transition-group>\n        </div>\n    </div>\n    <PlaylistSelectModal ref=\"playlistSelect\" :current-song=\"songs\"/>\n</template>\n\n<script setup>\nimport { ref, onMounted, watch, onBeforeUnmount, computed } from 'vue';\nimport { RecycleScroller } from 'vue3-virtual-scroller';\nimport ContextMenu from '../components/ContextMenu.vue';\nimport PlaylistSelectModal from '../components/PlaylistSelectModal.vue';\nimport { get } from '../utils/request';\nimport { useRoute, useRouter } from 'vue-router';\nimport { MoeAuthStore } from '../stores/store';\nimport { useI18n } from 'vue-i18n';\nimport { share } from '@/utils/utils';\n\nconst playlistSelect = ref(null);\nconst { t } = useI18n();\nconst MoeAuth = MoeAuthStore();\nconst router = useRouter();\nconst route = useRoute();\n\n// 判断是歌手还是歌单还是专辑\nconst isArtist = computed(() => !!route.query.singerid);\nconst isAlbum = computed(() => !!route.query.albumid);\n\n// 通用状态\nconst detail = ref({});\nconst tracks = ref([]);\nconst filteredTracks = ref([]);\nconst searchQuery = ref('');\nconst basePageSize = 60; // 基础页面大小\nconst hasMore = ref(true);\nconst isLoadingMore = ref(false);\nconst totalCount = ref(0);\nconst contextMenuRef = ref(null);\nconst recycleScrollerRef = ref(null);\nconst loading = ref(true);\nconst isSearching = ref(false); // 搜索加载状态\nconst isDropdownVisible = ref(false);\nconst flyingNotes = ref([]);\nlet noteId = 0;\n\n// 请求次数追踪，用于计算下一次的pageSize\nconst requestCount = ref(0);\n\n// 计算下一次请求的pageSize\nconst getPageSize = () => {\n    if (requestCount.value < 2) {\n        return basePageSize;\n    } else {\n        return Math.min(basePageSize * Math.pow(2, requestCount.value - 1), 240);\n    }\n};\n\n// 获取下一次请求的page 60 60 120 240 240\nconst getPage = () => {\n    if (requestCount.value === 0) {\n        return 1;\n    } else if (requestCount.value <= 3) {\n        // pageSize 还在递增阶段 (60, 60, 120, 240)，page 固定为 2\n        return 2;\n    } else {\n        // pageSize 达到最大值 240 后，通过递增 page 继续加载\n        // requestCount=4 时 page=3, requestCount=5 时 page=4, ...\n        return requestCount.value - 1;\n    }\n};\n\n// 歌手特有状态\nconst isFollowed = ref(true);\nconst followLoading = ref(false);\nconst collectedPlaylists = ref([]);\n// 判断歌单是否被收藏\nconst isPlaylistFavorited = ref(false);\n\n// 更新收藏状态\nconst updateFavoriteStatus = () => {\n    if (!detail.value.list_create_listid) {\n        isPlaylistFavorited.value = false;\n        return;\n    }\n    collectedPlaylists.value = JSON.parse(localStorage.getItem('collectedPlaylists') || '[]');\n    isPlaylistFavorited.value = collectedPlaylists.value.some(item => item.list_create_listid === detail.value.list_create_listid);\n};\n\n// 批量选择相关状态\nconst batchSelectionMode = ref(false);\nconst isBatchMenuVisible = ref(false);\nconst selectedTracks = ref([]);\nlet lastSelectedIndex = -1;\nconst songs = ref([]);\n\n// 排序状态\nconst sortField = ref('');\nconst sortOrder = ref('asc');\nconst artistSortType = ref('hot'); // 歌手歌曲排序类型：hot(热门) 或 new(最新)\n\n// 判断是否全选\nconst isAllSelected = computed(() => {\n    return selectedTracks.value.length === filteredTracks.value.length && filteredTracks.value.length > 0;\n});\n\n// 视图模式相关状态\nconst viewMode = ref('list'); // 'list' or 'grid'\n\n// 计算显示的歌曲数量\nconst displayTrackCount = computed(() => {\n    // 当还有更多数据未加载时，显示 totalCount；否则显示实际加载的 tracks.length\n    return hasMore.value ? totalCount.value : tracks.value.length;\n});\n\nconst props = defineProps({\n    playerControl: Object\n});\n\nonMounted(() => {\n    isFollowed.value = !!route.query.unfollow;\n    const savedViewMode = localStorage.getItem('trackViewMode');\n    if (savedViewMode) {\n        viewMode.value = savedViewMode;\n    }\n    loadData();\n    document.addEventListener('click', handleClickOutside);\n});\n\nonBeforeUnmount(() => {\n    document.removeEventListener('click', handleClickOutside);\n});\n\nwatch(() => [route.query.global_collection_id, route.query.singerid, route.query.albumid], () => {\n    loadData();\n});\n\nconst loadData = async () => {\n    if(!route.query.global_collection_id && !route.query.singerid && !route.query.albumid) {\n        router.push('/library');\n        return;\n    }\n    if (isArtist.value) {\n        getArtistInfo();\n        fetchArtistSongs();\n    } else if (isAlbum.value) {\n        getAlbumInfo();\n        fetchAlbumSongs();\n    } else {\n        updateFavoriteStatus();\n        await fetchPlaylistTracks();\n    }\n};\n\n// 获取歌手信息\nconst getArtistInfo = async () => {\n    try {\n        const response = await get('/artist/detail', {\n            id: route.query.singerid\n        });\n        if (response.status === 1) {\n            detail.value = {\n                ...response.data,\n                id: route.query.singerid\n            };\n        }\n    } catch (error) {\n        console.error('获取歌手信息失败:', error);\n    }\n};\n\n// 获取专辑信息\nconst getAlbumInfo = async () => {\n    try {\n        const response = await get('/album/detail', {\n            id: route.query.albumid\n        });\n        if (response.status === 1 && response.data && response.data.length > 0) {\n            const albumData = response.data[0]; // 数据在 data[0] 中\n            detail.value = {\n                name: albumData.album_name || '',\n                pic: albumData.sizable_cover || albumData.cover || '',\n                publish_date: albumData.publish_date || '',\n                intro: albumData.intro || '',\n                song_count: 0, // 专辑详情接口没有返回歌曲数量，从歌曲列表接口获取\n                id: route.query.albumid\n            };\n        }\n    } catch (error) {\n        console.error('获取专辑信息失败:', error);\n    }\n};\n\n// 获取歌手歌曲\nconst fetchArtistSongs = async () => {\n    requestCount.value = 0;\n    hasMore.value = true;\n\n    try {\n        const curPage = getPage();\n        const curPageSize = getPageSize();\n\n        const response = await get('/artist/audios', {\n            id: route.query.singerid,\n            sort: artistSortType.value,\n            page: curPage,\n            pagesize: curPageSize\n        });\n\n        if (response.status === 1) {\n            totalCount.value = detail.value.song_count || 0;\n            const rawSongs = response.data || [];\n            const formattedTracks = rawSongs\n            .filter(track => !!track.hash)\n            .map(track => ({\n                hash: track.hash || '',\n                remark: track.remark || '',\n                OriSongName: track.audio_name + ' - ' + track.author_name,\n                name: track.audio_name || '',\n                author: track.author_name || '',\n                album: track.album_name || '',\n                cover: track.trans_param.union_cover?.replace(\"{size}\", 480) || '',\n                timelen: track.timelength || 0,\n                isSQ: !!track.hash_flac,\n                isHQ: !!track.hash_320,\n                privilege: track.privilege || 0,\n                mvhash: track.mvhash || '',\n                originalData: track\n            }));\n\n            tracks.value = formattedTracks;\n            filteredTracks.value = formattedTracks;\n            requestCount.value++; // 增加请求计数\n\n            // 判断是否还有更多数据\n            hasMore.value = rawSongs.length >= curPageSize && tracks.value.length < totalCount.value;\n        }\n    } catch (error) {\n        window.$modal.alert(t('ge-qu-shu-ju-cuo-wu'));\n        return;\n    }\n\n    loading.value = false;\n\n    ensureBufferData();\n};\n\n// 获取专辑歌曲\nconst fetchAlbumSongs = async () => {\n    requestCount.value = 0;\n    hasMore.value = true;\n\n    try {\n        const albumPageSize = 50; // 专辑固定使用 pagesize=50\n        const curPage = 1;\n\n        const response = await get('/album/songs', {\n            id: route.query.albumid,\n            page: curPage,\n            pagesize: albumPageSize\n        });\n\n        if (response.status === 1) {\n            totalCount.value = response.data.total || 0;\n            // 更新专辑歌曲数量\n            if (detail.value.song_count === 0) {\n                detail.value.song_count = response.data.total || 0;\n            }\n            const rawSongs = response.data.songs || [];\n            const formattedTracks = rawSongs\n            .filter(track => track.audio_info?.hash)\n            .map(track => {\n                const audioInfo = track.audio_info;\n                const base = track.base;\n                const albumInfo = track.album_info;\n                const mvHash = track.mvdata && track.mvdata.length > 0 ? track.mvdata[0].hash : '';\n\n                return {\n                    hash: audioInfo.hash || '',\n                    remark: track.extra?.remark || '',\n                    OriSongName: base.audio_name + ' - ' + base.author_name,\n                    name: base.audio_name || '',\n                    author: base.author_name || '',\n                    album: albumInfo?.album_name || '',\n                    cover: track.trans_param?.union_cover?.replace(\"{size}\", 480) || '',\n                    timelen: audioInfo.duration || 0,\n                    isSQ: !!audioInfo.hash_flac,\n                    isHQ: !!audioInfo.hash_320,\n                    privilege: track.copyright?.privilege || 0,\n                    mvhash: mvHash,\n                    originalData: track\n                };\n            });\n\n            tracks.value = formattedTracks;\n            filteredTracks.value = formattedTracks;\n            requestCount.value++; // 增加请求计数\n\n            // 判断是否还有更多数据\n            hasMore.value = rawSongs.length >= albumPageSize && tracks.value.length < totalCount.value;\n        }\n    } catch (error) {\n        window.$modal.alert(t('ge-qu-shu-ju-cuo-wu'));\n        return;\n    }\n\n    loading.value = false;\n\n    ensureBufferData();\n};\n\n// 获取歌单歌曲\nconst fetchPlaylistTracks = async () => {\n    requestCount.value = 0; // 重置请求计数\n    hasMore.value = true;\n\n    try {\n        const curPage = getPage();\n        const curPageSize = getPageSize();\n\n        const response = await get('/playlist/track/all', {\n            id: route.query.global_collection_id,\n            page: curPage,\n            pagesize: curPageSize\n        });\n\n        if (response.status === 1) {\n            detail.value = response.data?.list_info;\n            totalCount.value = detail.value.count || 0;\n            const rawSongs = response.data?.songs || [];\n            const formattedTracks = rawSongs\n            .filter(track => !!track.hash)\n            .map(track => {\n                const nameParts = track.name.split(' - ');\n                return {\n                    hash: track.hash || '',\n                    remark: track.remark || '',\n                    OriSongName: track.name,\n                    name: nameParts.length > 1 ? nameParts[1] : track.name,\n                    author: nameParts.length > 1 ? nameParts[0] : '',\n                    album: track.albuminfo?.name || '',\n                    cover: track.cover?.replace(\"{size}\", 480) || '',\n                    timelen: track.timelen || 0,\n                    isSQ: track.relate_goods && track.relate_goods.length > 2,\n                    isHQ: track.relate_goods && track.relate_goods.length > 1,\n                    privilege: track.privilege || 0,\n                    mvhash: track.mvhash || '',\n                    originalData: track\n                };\n            });\n\n            tracks.value = formattedTracks;\n            filteredTracks.value = formattedTracks;\n            requestCount.value++; // 增加请求计数\n            hasMore.value = rawSongs.length >= curPageSize && tracks.value.length < totalCount.value;\n        }\n    } catch (error) {\n        window.$modal.alert(t('ge-qu-shu-ju-cuo-wu'));\n        return;\n    }\n\n    loading.value = false;\n\n    ensureBufferData();\n};\n\n// 加载更多歌曲\nconst loadMoreTracks = async () => {\n    if (isLoadingMore.value || !hasMore.value) return;\n\n    isLoadingMore.value = true;\n\n    try {\n        if (isArtist.value) {\n            // 加载更多歌手歌曲\n            const curPage = getPage();\n            const curPageSize = getPageSize();\n\n            const response = await get('/artist/audios', {\n                id: route.query.singerid,\n                sort: artistSortType.value,\n                page: curPage,\n                pagesize: curPageSize\n            });\n\n            if (response.status === 1 && response.data.length > 0) {\n                const rawSongs = response.data;\n                const formattedTracks = rawSongs\n                .filter(track => !!track.hash)\n                .map(track => ({\n                    hash: track.hash || '',\n                    OriSongName: track.audio_name + ' - ' + track.author_name,\n                    name: track.audio_name || '',\n                    author: track.author_name || '',\n                    album: track.album_name || '',\n                    cover: track.trans_param.union_cover?.replace(\"{size}\", 480) || '',\n                    timelen: track.timelength || 0,\n                    isSQ: !!track.hash_flac,\n                    isHQ: !!track.hash_320,\n                    privilege: track.privilege || 0,\n                    mvhash: track.mvhash || '',\n                    originalData: track\n                }));\n\n                tracks.value = [...tracks.value, ...formattedTracks];\n                filteredTracks.value = tracks.value;\n                requestCount.value++; // 增加请求计数\n                hasMore.value = rawSongs.length >= curPageSize && tracks.value.length < totalCount.value;\n            } else {\n                hasMore.value = false;\n            }\n        } else if (isAlbum.value) {\n            // 加载更多专辑歌曲\n            const albumPageSize = 50; // 专辑固定使用 pagesize=50\n            const curPage = Math.floor(tracks.value.length / albumPageSize) + 1;\n\n            const response = await get('/album/songs', {\n                id: route.query.albumid,\n                page: curPage,\n                pagesize: albumPageSize\n            });\n\n            if (response.status === 1 && response.data.songs?.length > 0) {\n                const rawSongs = response.data.songs;\n                const formattedTracks = rawSongs\n                .filter(track => track.audio_info?.hash)\n                .map(track => {\n                    const audioInfo = track.audio_info;\n                    const base = track.base;\n                    const albumInfo = track.album_info;\n                    const mvHash = track.mvdata && track.mvdata.length > 0 ? track.mvdata[0].hash : '';\n\n                    return {\n                        hash: audioInfo.hash || '',\n                        remark: track.extra?.remark || '',\n                        OriSongName: base.audio_name + ' - ' + base.author_name,\n                        name: base.audio_name || '',\n                        author: base.author_name || '',\n                        album: albumInfo?.album_name || '',\n                        cover: track.trans_param?.union_cover?.replace(\"{size}\", 480) || '',\n                        timelen: audioInfo.duration || 0,\n                        isSQ: !!audioInfo.hash_flac,\n                        isHQ: !!audioInfo.hash_320,\n                        privilege: track.copyright?.privilege || 0,\n                        mvhash: mvHash,\n                        originalData: track\n                    };\n                });\n\n                tracks.value = [...tracks.value, ...formattedTracks];\n                filteredTracks.value = tracks.value;\n                requestCount.value++; // 增加请求计数\n                hasMore.value = rawSongs.length >= albumPageSize && tracks.value.length < totalCount.value;\n            } else {\n                hasMore.value = false;\n            }\n        } else {\n            // 加载更多歌单歌曲\n            const curPage = getPage();\n            const curPageSize = getPageSize();\n\n            const response = await get('/playlist/track/all', {\n                id: route.query.global_collection_id,\n                page: curPage,\n                pagesize: curPageSize\n            });\n\n            if (response.status === 1 && response.data.songs?.length > 0) {\n                const rawSongs = response.data.songs;\n                const formattedTracks = rawSongs\n                .filter(track => !!track.hash)\n                .map(track => {\n                    const nameParts = track.name.split(' - ');\n                    return {\n                        hash: track.hash || '',\n                        OriSongName: track.name,\n                        name: nameParts.length > 1 ? nameParts[1] : track.name,\n                        author: nameParts.length > 1 ? nameParts[0] : '',\n                        album: track.albuminfo?.name || '',\n                        cover: track.cover?.replace(\"{size}\", 480) || '',\n                        timelen: track.timelen || 0,\n                        isSQ: track.relate_goods && track.relate_goods.length > 2,\n                        isHQ: track.relate_goods && track.relate_goods.length > 1,\n                        privilege: track.privilege || 0,\n                        mvhash: track.mvhash || '',\n                        originalData: track\n                    };\n                });\n\n                tracks.value = [...tracks.value, ...formattedTracks];\n                filteredTracks.value = tracks.value;\n                requestCount.value++; // 增加请求计数\n                hasMore.value = rawSongs.length >= curPageSize && tracks.value.length < totalCount.value;\n            } else {\n                hasMore.value = false;\n            }\n        }\n    } catch (error) {\n        console.error('加载更多歌曲失败:', error);\n    } finally {\n        isLoadingMore.value = false;\n        // 加载完成后继续检查是否需要加载更多以保持3页缓冲\n        ensureBufferData();\n    }\n};\n\n// 记录最后的滚动位置信息\nlet lastVisibleBottomIndex = 0;\n\n// 确保始终有足够的缓冲数据\nconst ensureBufferData = () => {\n    const totalItems = filteredTracks.value.length;\n    const remainingItems = totalItems - lastVisibleBottomIndex;\n    const bufferSize = 90;\n\n    if (remainingItems < bufferSize && hasMore.value && !isLoadingMore.value) {\n        loadMoreTracks();\n    }\n};\n\nconst handleScroll = (event) => {\n    const { scrollTop, clientHeight } = event.target;\n    const itemSize = viewMode.value === 'list' ? 50 : 70;\n    // 计算当前可见区域底部对应的item索引\n    const visibleBottomIndex = Math.ceil((scrollTop + clientHeight) / itemSize);\n    lastVisibleBottomIndex = visibleBottomIndex;\n\n    ensureBufferData();\n};\n\n// 搜索歌曲\nconst searchTracks = async () => {\n    if (hasMore.value) {\n        isSearching.value = true;\n        try {\n            await loadAndAppendRemainingTracks();\n        } finally {\n            isSearching.value = false;\n        }\n    }\n    filteredTracks.value = tracks.value.filter(track =>\n        track.name.toLowerCase().trim().includes(searchQuery.value.toLowerCase().trim()) ||\n        track.author.toLowerCase().trim().includes(searchQuery.value.toLowerCase().trim())\n    );\n};\n\n// 播放歌曲\nconst playSong = (hash, name, img, author) => {\n    props.playerControl.addSongToQueue(hash, name, img, author);\n};\n\n// 加载所有剩余歌曲并追加到播放队列\nconst loadAndAppendRemainingTracks = async () => {\n    const loadedHashes = new Set(filteredTracks.value);\n\n    while (hasMore.value) {\n        if (isLoadingMore.value) {\n            // 等待当前加载完成\n            await new Promise(resolve => setTimeout(resolve, 100));\n            continue;\n        }\n        await loadMoreTracks();\n        // 找出新加载的歌曲（不在之前已加载集合中的）\n        const newTracks = filteredTracks.value.filter(t => !loadedHashes.has(t));\n        if (newTracks.length > 0) {\n            // 将新歌曲追加到播放队列\n            props.playerControl.addPlaylistToQueue(newTracks, true);\n            // 更新已加载集合\n            newTracks.forEach(t => {\n                loadedHashes.add(t);\n            });\n        }\n    }\n};\n\n// 添加整个播放列表到队列\nconst addPlaylistToQueue = (event, append = false) => {\n    const playButton = event.currentTarget;\n    const rect = playButton.getBoundingClientRect();\n    const note = {\n        id: noteId++,\n        style: {\n            '--start-x': `${rect.left + rect.width/2}px`,\n            '--start-y': `${rect.top + rect.height/2}px`,\n            'left': '0',\n            'top': '0'\n        }\n    };\n    flyingNotes.value.push(note);\n    setTimeout(() => {\n        flyingNotes.value = flyingNotes.value.filter(n => n.id !== note.id);\n    }, 1500);\n\n    // 先将当前已加载的歌曲加入播放队列并开始播放\n    props.playerControl.addPlaylistToQueue(filteredTracks.value, append);\n\n    // 如果还有未加载的歌曲，后台继续加载并追加到队列\n    if (hasMore.value) {\n        loadAndAppendRemainingTracks();\n    }\n};\n\n// 切换关注状态\nconst toggleFollow = async () => {\n    if (!MoeAuth.isAuthenticated) {\n        window.$modal.alert(t('qing-xian-deng-lu'));\n        return;\n    }\n    followLoading.value = true;\n    try {\n        const response = await get(isFollowed.value ? '/artist/unfollow' : '/artist/follow', {\n            id: route.query.singerid\n        });\n        if (response.status === 1) {\n            isFollowed.value = !isFollowed.value;\n        }\n    } catch (error) {\n        console.error('切换关注状态失败:', error);\n    } finally {\n        followLoading.value = false;\n        localStorage.setItem('t', Date.now());\n    }\n};\n\n// 收藏歌单\nconst toggleFavorite = async (id) => {\n    if (!MoeAuth.isAuthenticated) {\n        window.$modal.alert(t('qing-xian-deng-lu'));\n        return;\n    }\n    \n    try {\n        if (isPlaylistFavorited.value) {\n            const playlist = collectedPlaylists.value.find(p => p.list_create_listid === detail.value.list_create_listid);\n            if (playlist) {\n                await get('/playlist/del', { listid: playlist.listid });\n                const newCollectedPlaylists = collectedPlaylists.value.filter(item => \n                    item.list_create_listid !== detail.value.list_create_listid\n                );\n                localStorage.setItem('collectedPlaylists', JSON.stringify(newCollectedPlaylists));\n                isPlaylistFavorited.value = false;\n                $message.success('取消收藏成功');\n            }\n        } else {\n            const response = await get('/playlist/add', { \n                name: detail.value.name, \n                list_create_userid: MoeAuth.UserInfo.userid, \n                type: 1,\n                list_create_gid: id \n            });\n            if (response.status === 1) {\n                const newPlaylist = {\n                    list_create_listid: detail.value.list_create_listid,\n                    listid: response.data.info.listid\n                };\n                const currentPlaylists = JSON.parse(localStorage.getItem('collectedPlaylists') || '[]');\n                currentPlaylists.push(newPlaylist);\n                localStorage.setItem('collectedPlaylists', JSON.stringify(currentPlaylists));\n                isPlaylistFavorited.value = true;\n                $message.success('收藏成功');\n            }\n        }\n        localStorage.setItem('t', Date.now());\n    } catch (error) {\n        $message.error(isPlaylistFavorited.value ? t('qu-xiao-shou-cang-shi-bai') : t('shou-cang-shi-bai'));\n    }\n};\n\n// 删除歌单\nconst deletePlaylist = async () => {\n    isDropdownVisible.value = false;\n    const result = await window.$modal.confirm(t('que-ren-shan-chu-ge-dan'));\n    if (result) {\n        await get('/playlist/del', { listid: route.query.listid });\n        localStorage.setItem('t', Date.now());\n        router.back();\n    }\n};\n\n// 分享歌单\nconst sharePlaylist = () => {\n    isDropdownVisible.value = false;\n    share(detail.value.name,route.query.global_collection_id, 1);\n};\n\n// 右键菜单\nconst showContextMenu = (event, song) => {\n    if (contextMenuRef.value) {\n        contextMenuRef.value.openContextMenu(event, {\n            OriSongName: song.OriSongName,\n            FileHash: song.hash,\n            fileid: song.originalData.fileid,\n            userid: isArtist.value ? null : detail.value.list_create_userid,\n            timeLength: song.timelen,\n            cover: song.cover,\n            mvhash: song.mvhash,\n        }, isArtist.value ? null : detail.value.listid);\n    }\n};\n\n// 滚动到当前播放歌曲\nconst scrollToItem = () => {\n    const currentIndex = filteredTracks.value.findIndex(song => song.hash === props.playerControl.currentSong.hash);\n    if (currentIndex !== -1) {\n        recycleScrollerRef.value.scrollToItem(currentIndex - 3, { behavior: 'smooth' });\n    }\n};\n\n// 滚动到顶部\nconst scrollToFirstItem = () => {\n    recycleScrollerRef.value.scrollToItem(0, { behavior: 'smooth' });\n    window.scrollTo({\n        top: 0,\n        behavior: 'smooth',\n        scrollSource: 'manual-button-click' \n    });\n};\n\n// 处理下拉菜单点击外部关闭\nconst handleClickOutside = (event) => {\n    const dropdown = document.querySelector('.dropdown-menu');\n    const moreBtn = document.querySelector('.more-btn');\n    if (dropdown && !dropdown.contains(event.target) && !moreBtn.contains(event.target)) {\n        isDropdownVisible.value = false;\n    }\n    \n    // 处理批量操作菜单\n    const batchActionsMenu = document.querySelector('.batch-actions-menu');\n    const batchActionBtn = document.querySelector('.batch-action-btn');\n    if (batchActionsMenu && !batchActionsMenu.contains(event.target) && !batchActionBtn.contains(event.target)) {\n        isBatchMenuVisible.value = false;\n    }\n};\n\n// 切换下拉菜单显示状态\nconst toggleDropdown = () => {\n    isDropdownVisible.value = !isDropdownVisible.value;\n};\n\n// 切换批量选择模式\nconst toggleBatchSelection = () => {\n    if (batchSelectionMode.value) {\n        // 如果已经在批量选择模式，则切换菜单显示或退出模式\n        if (isBatchMenuVisible.value) {\n            // 如果菜单已经显示，则点击后退出批量选择模式\n            batchSelectionMode.value = false;\n            isBatchMenuVisible.value = false;\n            selectedTracks.value = [];\n            lastSelectedIndex = -1;\n        } else {\n            // 如果菜单未显示，则显示菜单\n            isBatchMenuVisible.value = true;\n        }\n    } else {\n        // 首次进入批量选择模式\n        batchSelectionMode.value = true;\n        isBatchMenuVisible.value = false;\n    }\n};\n\n// 选择/取消选择歌曲\nconst selectTrack = (index, event) => {\n    if (event.shiftKey && lastSelectedIndex !== -1) {\n        // Shift 键多选\n        const start = Math.min(lastSelectedIndex, index);\n        const end = Math.max(lastSelectedIndex, index);\n        \n        for (let i = start; i <= end; i++) {\n            if (!selectedTracks.value.includes(i)) {\n                selectedTracks.value.push(i);\n            }\n        }\n    } else {\n        // 普通点击\n        const existingIndex = selectedTracks.value.indexOf(index);\n        if (existingIndex === -1) {\n            selectedTracks.value.push(index);\n        } else {\n            selectedTracks.value.splice(existingIndex, 1);\n        }\n    }\n    \n    lastSelectedIndex = index;\n};\n\n// 将选中歌曲添加到播放队列（追加到当前队列）\nconst appendSelectedToQueue = async () => {\n    if (selectedTracks.value.length === 0) return;\n    const selectedSongs = selectedTracks.value.map(index => filteredTracks.value[index]);\n    await props.playerControl.addPlaylistToQueue(selectedSongs, true);\n    $message.success('添加到播放列表成功');\n    isBatchMenuVisible.value = false;\n};\n\n// 将选中歌曲添加到其他歌单\nconst addSelectedToOtherPlaylist = async () => {\n    if (selectedTracks.value.length === 0) return;\n    const selectedSongs = selectedTracks.value.map(index => filteredTracks.value[index]);\n    songs.value =  selectedSongs;\n    await playlistSelect.value.fetchPlaylists();\n    isBatchMenuVisible.value = false;\n};\n\n// 从歌单中移除选中的歌曲\nconst removeSelectedFromPlaylist = async () => {\n    if (selectedTracks.value.length === 0) return;\n    const result = await window.$modal.confirm('确定要移除选中的歌曲吗？');\n    if (result) {\n        const selectedSongs = selectedTracks.value.map(index => filteredTracks.value[index]);\n        try {\n            const fileids = selectedSongs.map(song => song.originalData.fileid).join(',');\n            await get('/playlist/tracks/del', {\n                listid: route.query.listid,\n                fileids: fileids\n            });\n            selectedTracks.value.sort((a, b) => b - a).forEach(index => {\n                filteredTracks.value.splice(index, 1);\n                tracks.value = tracks.value.filter((_, i) => \n                    !selectedTracks.value.includes(i)\n                );\n            });\n            filteredTracks.value = tracks.value;\n            selectedTracks.value = [];\n            $message.success('歌曲已从歌单中移除');\n        } catch (err) {\n            $message.error('移除歌曲失败');\n            return;\n        }\n    }\n    isBatchMenuVisible.value = false;\n};\n\n// 切换全选/取消全选\nconst toggleSelectAll = () => {\n    if (isAllSelected.value) {\n        selectedTracks.value = [];\n    } else {\n        selectedTracks.value = Array.from({ length: filteredTracks.value.length }, (_, i) => i);\n    }\n};\n\n// 根据字段排序\nconst sortTracks = async (field) => {\n    if (hasMore.value) {\n        isSearching.value = true;\n        try {\n            await loadAndAppendRemainingTracks();\n        } finally {\n            isSearching.value = false;\n        }\n    }\n    if (sortField.value === field) {\n        sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';\n    } else {\n        sortField.value = field;\n        sortOrder.value = 'asc';\n    }\n    \n    filteredTracks.value = [...filteredTracks.value].sort((a, b) => {\n        let valueA, valueB;\n        \n        if (field === 'timelen') {\n            valueA = a[field] || 0;\n            valueB = b[field] || 0;\n        } else {\n            valueA = (a[field] || '').toLowerCase();\n            valueB = (b[field] || '').toLowerCase();\n        }\n        \n        if (sortOrder.value === 'asc') {\n            return valueA > valueB ? 1 : -1;\n        } else {\n            return valueA < valueB ? 1 : -1;\n        }\n    });\n    \n    if (batchSelectionMode.value) {\n        selectedTracks.value = [];\n    }\n};\n\nconst getSortIconClass = (field) => {\n    if (sortField.value !== field) {\n        return 'fa-sort';\n    }\n    return sortOrder.value === 'asc' ? 'fa-sort-up' : 'fa-sort-down';\n};\n\nconst handleSongRemoved = (fileid) => {\n    tracks.value = tracks.value.filter(track => track.originalData?.fileid !== fileid);\n    filteredTracks.value = filteredTracks.value.filter(track => track.originalData?.fileid !== fileid);\n};\n\n// 切换视图模式\nconst toggleViewMode = () => {\n    viewMode.value = viewMode.value === 'list' ? 'grid' : 'list';\n    localStorage.setItem('trackViewMode', viewMode.value);\n};\n\n// 切换歌手歌曲排序方式\nconst changeArtistSort = (sortType) => {\n    if (artistSortType.value !== sortType) {\n        artistSortType.value = sortType;\n        // 重新获取歌手歌曲\n        fetchArtistSongs();\n    }\n};\n\n// 判断是否为当前歌曲（不管是否正在播放）\nconst isCurrentSong = (hash) => {\n    return props.playerControl?.currentSong?.hash === hash;\n};\n\n// 判断是否为当前正在播放的歌曲\nconst isCurrentPlaying = (hash) => {\n    return isCurrentSong(hash) && props.playerControl?.playing;\n};\n</script>\n\n<style scoped>\n.detail-page {\n    padding: 20px;\n}\n\n/* 头部样式 */\n.header {\n    display: flex;\n    align-items: center;\n    margin-bottom: 40px;\n}\n\n.cover-art {\n    width: 200px;\n    height: 200px;\n    border-radius: 10px;\n    margin-right: 20px;\n    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);\n    object-fit: cover;\n}\n\n.artist-avatar {\n    border-radius: 50%;\n}\n\n.info {\n    max-width: 600px;\n}\n\n.title {\n    font-size: 36px;\n    font-weight: bold;\n    width: 800px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    margin: 0;\n    color: var(--primary-color);\n}\n\n.subtitle {\n    font-size: 18px;\n    color: #666;\n}\n\n.meta {\n    font-size: 14px;\n    margin-bottom: 10px;\n    color: #999;\n}\n\n.stats {\n    display: flex;\n    gap: 20px;\n    color: #666;\n    margin-top: 10px;\n}\n\n.description {\n    white-space: pre-wrap;\n    line-height: 1.6;\n    color: var(--text-color);\n    margin-bottom: 20px;\n    font-size: 16px;\n    max-height: 200px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: break-spaces;\n    overflow-y: auto;\n}\n\n.actions {\n    display: flex;\n    gap: 10px;\n}\n\n.primary-btn, .follow-btn {\n    background-color: #ff69b4;\n    color: white;\n    border: none;\n    padding: 10px 20px;\n    border-radius: 5px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n}\n\n.primary-btn i, .follow-btn i {\n    margin-right: 5px;\n}\n\n.follow-btn:disabled {\n    opacity: 0.7;\n    cursor: not-allowed;\n}\n\n.fav-btn,\n.more-btn {\n    background-color: transparent;\n    padding: 10px;\n    border-radius: 5px;\n    cursor: pointer;\n    border: 1px solid var(--secondary-color);\n    height: 100%;\n}\n\n.fav-btn i {\n    color: #999;\n}\n\n.fav-btn.active i {\n    color: var(--primary-color);\n}\n\n/* 歌曲列表样式 */\n.track-list-container {\n    margin-top: 30px;\n}\n\n.track-list-header {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 10px;\n}\n\n.track-list-title {\n    font-size: 24px;\n    font-weight: bold;\n    margin-bottom: 10px;\n    color: var(--primary-color);\n}\n\n/* 搜索和批量操作按钮 */\n.track-list-actions {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n}\n\n.batch-action-container {\n    position: relative;\n}\n\n.batch-action-btn {\n    background-color: transparent;\n    border: 1px solid var(--secondary-color);\n    padding: 5px 10px;\n    border-radius: 5px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: var(--text-color);\n    position: relative;\n}\n\n.batch-action-btn.active {\n    background-color: var(--primary-color);\n    color: white;\n}\n\n/* 视图模式切换按钮 */\n.view-mode-btn {\n    background-color: transparent;\n    border: 1px solid var(--secondary-color);\n    padding: 5px 10px;\n    border-radius: 5px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: var(--text-color);\n    width: 36px;\n    height: 31px;\n    transition: all 0.3s ease;\n}\n\n.view-mode-btn:hover {\n    background-color: rgba(var(--primary-color-rgb), 0.1);\n}\n\n.view-mode-btn i {\n    font-size: 16px;\n}\n\n.selected-count {\n    position: absolute;\n    top: -8px;\n    right: -8px;\n    background-color: red;\n    color: white;\n    border-radius: 50%;\n    width: 20px;\n    height: 20px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    font-size: 12px;\n    font-weight: bold;\n}\n\n.batch-actions-menu {\n    position: absolute;\n    top: 100%;\n    left: 0;\n    background-color: white;\n    border: 1px solid #ccc;\n    border-radius: 5px;\n    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n    z-index: 50;\n    margin-top: 5px;\n    width: 200px;\n}\n\n.batch-actions-menu ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n}\n\n.batch-actions-menu li {\n    padding: 10px 15px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    white-space: nowrap;\n}\n\n.batch-actions-menu li i {\n    margin-right: 10px;\n    width: 16px;\n    text-align: center;\n}\n\n.batch-actions-menu li:hover {\n    background-color: #f0f0f0;\n}\n\n/* 排序选择器样式 */\n.sort-selector {\n    display: flex;\n    border: 1px solid var(--secondary-color);\n    border-radius: 5px;\n    overflow: hidden;\n}\n\n.sort-btn {\n    background-color: transparent;\n    border: none;\n    padding: 5px 15px;\n    cursor: pointer;\n    color: var(--text-color);\n    transition: all 0.3s ease;\n    font-size: 14px;\n}\n\n.sort-btn:not(:last-child) {\n    border-right: 1px solid var(--secondary-color);\n}\n\n.sort-btn:hover {\n    background-color: rgba(var(--primary-color-rgb), 0.1);\n}\n\n.sort-btn.active {\n    background-color: var(--primary-color);\n    color: white;\n}\n\n.search-input {\n    width: 250px;\n    padding: 8px;\n    border: 1px solid var(--secondary-color);\n    border-radius: 20px;\n    box-sizing: border-box;\n    padding-left: 15px;\n}\n\n.track-list {\n    height: 800px;\n    scrollbar-width: thin;\n    scrollbar-color: transparent transparent;\n    overflow: auto;\n}\n\n/* 搜索加载动画 */\n.search-loading-overlay {\n    height: 800px;\n    display: flex;\n    align-items: flex-start;\n    justify-content: center;\n    padding-top: 150px;\n    border-radius: 0 0 5px 5px;\n}\n\n.search-loading-spinner {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    gap: 15px;\n    color: var(--text-color);\n}\n\n.search-loading-spinner i {\n    font-size: 48px;\n    color: var(--primary-color);\n}\n\n.search-loading-spinner span {\n    font-size: 16px;\n    color: #999;\n}\n\n\n.track-list::-webkit-scrollbar {\n    width: 8px !important; \n    display: block !important;\n}\n\n.track-list:hover {\n    scrollbar-color: var(--primary-color) transparent;\n}\n\n.li {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 10px;\n    border-bottom: 1px solid #eee;\n    border-radius: 5px;\n    cursor: pointer;\n}\n\n.li:hover {\n    background-color: var(--background-color);\n}\n\n.li.selected {\n    background-color: rgba(var(--primary-color-rgb), 0.1);\n}\n\n/* 歌曲多选 */\n.track-checkbox {\n    margin-right: 10px;\n    width: 30px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.track-number {\n    font-weight: bold;\n    margin-right: 10px;\n    width: 30px;\n    display: flex;\n    align-items: flex-end;\n    justify-content: center;\n    height: 20px;\n}\n\n.track-number.current {\n    color: var(--primary-color);\n}\n\n.track-title.current {\n    color: var(--primary-color);\n}\n\n/* 声波动画 */\n.sound-wave {\n    display: flex;\n    align-items: flex-end;\n    gap: 2px;\n    height: 16px;\n}\n\n.sound-wave span {\n    width: 3px;\n    background-color: var(--primary-color);\n    animation: wave 0.8s ease-in-out infinite;\n}\n\n.sound-wave span:nth-child(1) {\n    height: 6px;\n    animation-delay: 0s;\n}\n\n.sound-wave span:nth-child(2) {\n    height: 12px;\n    animation-delay: 0.2s;\n}\n\n.sound-wave span:nth-child(3) {\n    height: 8px;\n    animation-delay: 0.4s;\n}\n\n@keyframes wave {\n    0%, 100% {\n        transform: scaleY(0.5);\n    }\n    50% {\n        transform: scaleY(1);\n    }\n}\n\n.track-title-container {\n    flex: 2;\n    display: flex;\n    flex-direction: column;\n    overflow: hidden;\n}\n\n.track-title {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    min-width: 0;\n}\n\n.track-title-text {\n    flex: 0 1 auto;\n    max-width: 100%;\n    min-width: 0;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.track-title-tags {\n    display: flex;\n    align-items: center;\n    gap: 5px;\n    flex-shrink: 0;\n}\n\n.track-remark {\n    font-size: 12px;\n    color: #999;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    margin-top: 2px;\n}\n\n.track-artist {\n    flex: 1;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 0 10px;\n}\n\n.track-album {\n    flex: 1;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 0 10px;\n}\n\n.track-timelen {\n    width: 95px;\n    text-align: right;\n}\n\n.icon {\n    margin-left: 5px;\n    border: 1px solid;\n    border-radius: 5px;\n    font-size: 10px;\n    padding-left: 6px;\n    padding-right: 6px;\n}\n\n.track-title-tags .icon {\n    margin-left: 0;\n}\n\n.vip-icon {\n    color: #ff6d00;\n}\n\n.sq-icon {\n    color: #0094ff;\n}\n\n.mv-icon {\n    color: #ff1744;\n}\n\n.queue-play-btn {\n    background: none;\n    border: none;\n    font-size: 16px;\n    color: var(--primary-color);\n    cursor: pointer;\n}\n\n/* 歌手简介部分 */\n.content-section {\n    margin-top: 50px;\n    border-top: 1px dotted var(--secondary-color);\n}\n\n.intro-section {\n    margin-bottom: 30px;\n}\n\n.intro-section h3 {\n    color: var(--primary-color);\n    margin-bottom: 15px;\n}\n\n.section-content {\n    white-space: pre-wrap;\n    line-height: 1.6;\n    color: var(--text-color);\n}\n\n/* 导航按钮 */\n.location-arrow {\n    position: fixed;\n    bottom: 168px;\n    right: 14px;\n    z-index: 1;\n    cursor: pointer;\n    font-size: 20px;\n    color: var(--primary-color);\n}\n\n.scroll-bottom-img {\n    position: fixed;\n    bottom: 100px;\n    right: 10px;\n    z-index: 1;\n    cursor: pointer;\n    font-size: 20px;\n    color: var(--primary-color);\n}\n\n/* 下拉菜单 */\n.more-btn-container {\n    position: relative;\n}\n\n.dropdown-menu {\n    position: absolute;\n    background-color: white;\n    border: 1px solid #ccc;\n    border-radius: 5px;\n    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n    top: 50px;\n    z-index: 50;\n}\n\n.dropdown-menu ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n}\n\n.dropdown-menu li {\n    padding: 10px;\n    cursor: pointer;\n}\n\n.dropdown-menu li:hover {\n    background-color: #f0f0f0;\n}\n\n/* 音符动画 */\n.note-container {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100vw;\n    height: 100vh;\n    pointer-events: none;\n    overflow: hidden;\n}\n\n.flying-note {\n    position: absolute;\n    font-size: 36px;\n    color: var(--primary-color);\n    pointer-events: none;\n    transform-origin: center;\n}\n\n.fly-note-enter-active {\n    animation: fly-note 2s ease-out forwards;\n}\n\n.fly-note-leave-active {\n    animation: fly-note 2s ease-out forwards;\n}\n\n@keyframes fly-note {\n    0% {\n        transform: translate(var(--start-x), calc(var(--start-y) - 50px)) rotate(0deg) scale(1.2);\n        opacity: 0.9;\n    }\n    20% {\n        transform: translate(calc(var(--start-x) + 20px), calc(var(--start-y) - 70px)) rotate(45deg) scale(1.3);\n        opacity: 0.85;\n    }\n    100% {\n        transform: translate(80vw, 100vh) rotate(360deg) scale(0.6);\n        opacity: 0;\n    }\n}\n\n/* 表头样式 */\n.track-list-header-row {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 10px;\n    border-bottom: 1px solid var(--primary-color);\n    font-weight: bold;\n    background-color: rgba(var(--primary-color-rgb), 0.1);\n    border-radius: 5px 5px 0 0;\n}\n\n.track-checkbox-header {\n    margin-right: 10px;\n    width: 30px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.track-number-header {\n    font-weight: bold;\n    margin-right: 10px;\n    width: 30px;\n}\n\n.track-title-header, .track-artist-header, .track-album-header, .track-timelen-header {\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n}\n\n.track-title-header {\n    flex: 2;\n}\n\n.track-artist-header, .track-album-header {\n    flex: 1;\n    padding: 0 10px;\n}\n\n.track-timelen-header {\n    width: 95px;\n    text-align: right;\n}\n\n.track-title-header i, .track-artist-header i, .track-album-header i, .track-timelen-header i {\n    margin-left: 5px;\n    font-size: 14px;\n}\n\n.track-list-header-row:hover {\n    background-color: rgba(var(--primary-color-rgb), 0.15);\n}\n\n/* 网格视图样式 */\n.li.cover-view {\n    height: 60px;\n    padding: 5px 10px;\n    display: flex;\n    align-items: center;\n    border-bottom: 1px solid #eee;\n    border-radius: 5px;\n}\n\n.li.cover-view:hover {\n    background-color: var(--background-color);\n}\n\n.track-cover {\n    position: relative;\n    width: 50px;\n    height: 50px;\n    margin-right: 15px;\n    overflow: hidden;\n    border-radius: 4px;\n    flex-shrink: 0;\n}\n\n.track-cover img {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    transition: transform 0.3s ease;\n}\n\n.li.cover-view:hover .track-cover img {\n    transform: scale(1.05);\n}\n\n.track-cover-overlay {\n    position: absolute;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0,0,0,0.5);\n    opacity: 0;\n    transition: opacity 0.3s ease;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    color: white;\n    font-size: 20px;\n}\n\n.li.cover-view:hover .track-cover-overlay {\n    opacity: 1;\n}\n\n.track-list {\n    height: 800px;\n    scrollbar-width: thin;\n    scrollbar-color: transparent transparent; \n    overflow: auto;\n}\n\n/* 调整封面视图下的其他元素样式 */\n.li.cover-view .track-title-container {\n    flex: 2;\n    display: flex;\n    flex-direction: column;\n    overflow: hidden;\n}\n\n.li.cover-view .track-title {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    min-width: 0;\n}\n\n.li.cover-view .track-title-text {\n    flex: 0 1 auto;\n    max-width: 100%;\n    min-width: 0;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.li.cover-view .track-title-tags {\n    display: flex;\n    align-items: center;\n    gap: 5px;\n    flex-shrink: 0;\n}\n\n.li.cover-view .track-remark {\n    font-size: 12px;\n    color: #999;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    margin-top: 2px;\n}\n\n.li.cover-view .track-artist {\n    flex: 1;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 0 10px;\n}\n\n.li.cover-view .track-album {\n    flex: 1;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    padding: 0 10px;\n}\n\n.li.cover-view .track-timelen {\n    width: 95px;\n    text-align: right;\n}\n\n.li.cover-view .track-checkbox,\n.li.cover-view .track-number {\n    margin-right: 10px;\n    width: 30px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n</style>\n"
  },
  {
    "path": "src/views/Ranking.vue",
    "content": "<template>\n    <div class=\"ranking-container\">\n        <!-- 添加榜单选择区域 -->\n        <div class=\"rank-selector\">\n            <div \n                v-for=\"rank in allRanks\" \n                :key=\"rank.rankid\"\n                class=\"rank-chip\"\n                :class=\"{ active: selectedRankIds.includes(rank.rankid) }\"\n                @click=\"toggleRank(rank)\"\n            >\n                {{ rank.rankname }}\n            </div>\n        </div>\n\n        <!-- 现有的榜单展示区域 -->\n        <div class=\"ranking-list\">\n            <div class=\"ranking-item\" v-for=\"(rank, index) in displayedRanks\" :key=\"index\">\n                <div class=\"rank-header\">\n                    <div class=\"rank-cover\">\n                        <img :src=\"$getCover(rank.imgurl, 640)\">\n                    </div>\n                    <div class=\"rank-info\">\n                        <h2 class=\"rank-title\" :style=\"{ color: rank.album_cover_color }\">{{ rank.rankname }}</h2>\n                        <span class=\"rank-update\">{{ formatIntro(rank.intro) }}</span>\n                    </div>\n                    <div class=\"rank-play-btn\" @click.stop=\"handlePlayClick($event, rank.songs)\">\n                        <i class=\"fas fa-play\"></i>\n                    </div>\n                </div>\n                <div class=\"song-list\" @scroll=\"handleScroll($event, rank.rankid)\">\n                    <div class=\"song-item\" v-for=\"(song, sIndex) in rank.songs\" :key=\"sIndex\" @click=\"props.playerControl.addSongToQueue(song.deprecated.hash, song.songname, $getCover(song.trans_param.union_cover, 480), song.author_name)\">\n                        <div class=\"song-rank\">\n                            <span class=\"song-index\" :class=\"{'top-three': sIndex < 3}\">{{ sIndex + 1 }}</span>\n                        </div>\n                        <div class=\"song-cover\">\n                            <img :src=\"$getCover(song.trans_param.union_cover, 120)\">\n                            <div class=\"hover-play\">\n                                <i class=\"fas fa-play\"></i>\n                            </div>\n                        </div>\n                        <div class=\"song-info\">\n                            <div class=\"song-content\">\n                                <div class=\"song-main\">\n                                    <div class=\"song-name\">{{ song.songname }}</div>\n                                    <div class=\"song-author\">{{ song.author_name }}</div>\n                                </div>\n                                <div class=\"song-meta\">\n                                    <span class=\"album\">{{ song.album_name }}</span>\n                                    <span class=\"duration\">{{ $formatMilliseconds(song.deprecated.duration) }}</span>\n                                </div>\n                            </div>\n                        </div>\n                    </div>\n                    \n                    <div v-if=\"rankPagination[rank.rankid]?.loading\" class=\"loading-indicator\">\n                        <div class=\"loading-spinner\"></div>\n                        <span>加载中...</span>\n                    </div>\n                    \n                    <div v-else-if=\"!rankPagination[rank.rankid]?.hasMore && rank.songs?.length > 0\" class=\"no-more-indicator\">\n                        <span>已加载全部歌曲</span>\n                    </div>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref, onMounted } from 'vue';\nimport { get } from '../utils/request';\n\nconst allRanks = ref([]);\nconst displayedRanks = ref([]);\nconst selectedRankIds = ref([]);\nconst pagesize = 30;\nconst rankPagination = ref({});\n\nconst props = defineProps({\n    playerControl: Object\n});\n\nconst saveSelectedRanks = () => {\n    localStorage.setItem('selectedRankIds', JSON.stringify(selectedRankIds.value));\n};\n\nconst initRankPagination = (rankId) => {\n    if (!rankPagination.value[rankId]) {\n        rankPagination.value[rankId] = {\n            currentPage: 1,\n            loading: false,\n            hasMore: true\n        };\n    }\n};\n\n// 加载榜单歌曲数据\nconst loadRankSongs = async (rankId, page = 1, append = false) => {\n    const pagination = rankPagination.value[rankId];\n    if (pagination.loading) return;\n    \n    pagination.loading = true;\n    \n    try {\n        const songsResponse = await get(`/rank/audio?rankid=${rankId}&page=${page}&pagesize=${pagesize}`);\n        if (songsResponse.status === 1) {\n            const newSongs = songsResponse.data.songlist || [];\n            const rank = displayedRanks.value.find(r => r.rankid === rankId);\n            \n            if (rank) {\n                if (append) {\n                    rank.songs = [...(rank.songs || []), ...newSongs];\n                } else {\n                    rank.songs = newSongs;\n                }\n            }\n            \n            // 如果返回的歌曲数量少于pagesize，说明没有更多数据了\n            if (newSongs.length < pagesize) {\n                pagination.hasMore = false;\n            }\n            \n            pagination.currentPage = page;\n        }\n    } catch (error) {\n        console.error('加载榜单歌曲失败:', error);\n    } finally {\n        pagination.loading = false;\n    }\n};\n\n// 加载指定的榜单\nconst loadSelectedRanks = async (rankList, rankIds) => {\n    for (const rankId of rankIds) {\n        const rank = rankList.find(r => r.rankid === rankId);\n        if (rank) {\n            selectedRankIds.value.push(rank.rankid);\n            initRankPagination(rank.rankid);\n            displayedRanks.value.push(rank);\n            await loadRankSongs(rank.rankid, 1, false);\n        }\n    }\n};\n\n// 随机选择并加载榜单\nconst loadRandomRanks = async (rankList, count = 4) => {\n    const randomRanks = rankList.sort(() => 0.5 - Math.random()).slice(0, count);\n    \n    for (const rank of randomRanks) {\n        selectedRankIds.value.push(rank.rankid);\n        initRankPagination(rank.rankid);\n        displayedRanks.value.push(rank);\n        await loadRankSongs(rank.rankid, 1, false);\n    }\n    saveSelectedRanks();\n};\n\n// 切换榜单选择状态\nconst toggleRank = async (rank) => {\n    const index = selectedRankIds.value.indexOf(rank.rankid);\n    \n    if (index === -1 && selectedRankIds.value.length < 6) {\n        selectedRankIds.value.push(rank.rankid);\n        initRankPagination(rank.rankid);\n        displayedRanks.value.push(rank);\n        await loadRankSongs(rank.rankid, 1, false);\n    } else if (index !== -1) {\n        selectedRankIds.value.splice(index, 1);\n        displayedRanks.value = displayedRanks.value.filter(r => r.rankid !== rank.rankid);\n        // 清理分页状态\n        delete rankPagination.value[rank.rankid];\n    }\n    saveSelectedRanks();\n};\n\nconst formatIntro = (intro) => {\n    if (!intro) return '';\n    const parts = intro.split('\\n');\n    const sortRule = parts.find(p => p.includes('排序方式：'))?.replace('排序方式：', '').trim() || '';\n    const updateFreq = parts.find(p => p.includes('更新频率：'))?.replace('更新频率：', '').trim() || '';\n    \n    if (sortRule && updateFreq) {\n        return `${sortRule} (${updateFreq})`;\n    }\n    return intro;\n};\n\n// 添加播放整个榜单\nconst playRankSongs = (songs) => {\n    if (props.playerControl && songs?.length) {\n        const newTracks = songs.map(song => ({ \n            hash: song.deprecated.hash,\n            author: song.author_name, \n            name: song.songname,\n            cover: song.trans_param.union_cover?.replace(\"{size}\", 120),\n            timelen: song.deprecated.duration\n        }))\n        props.playerControl.addPlaylistToQueue(newTracks);\n    }\n};\n\n// 处理滚动事件，实现无限滚动\nconst handleScroll = (event, rankId) => {\n    const element = event.target;\n    const pagination = rankPagination.value[rankId];\n    \n    if (!pagination || pagination.loading || !pagination.hasMore) {\n        return;\n    }\n    \n    // 检查是否滚动到底部附近（距离底部50px时开始加载）\n    const scrollTop = element.scrollTop;\n    const scrollHeight = element.scrollHeight;\n    const clientHeight = element.clientHeight;\n    \n    if (scrollTop + clientHeight >= scrollHeight - 50) {\n        const nextPage = pagination.currentPage + 1;\n        loadRankSongs(rankId, nextPage, true);\n    }\n};\n\n// 处理播放按钮点击\nconst handlePlayClick = (event, songs) => {\n    const note = document.createElement('i');\n    note.className = 'fas fa-music music-note';\n    const x = event.clientX;\n    const y = event.clientY;\n    note.style.left = x + 'px';\n    note.style.top = y + 'px';\n    \n    document.body.appendChild(note);\n    const targetX = window.innerWidth - 300;\n    const targetY = window.innerHeight - 100;\n    \n    const deltaX = targetX - x;\n    const deltaY = targetY - y;\n    \n    requestAnimationFrame(() => {\n        note.style.transform = `translate(${deltaX}px, ${deltaY}px)`;\n        note.style.opacity = '0';\n    });\n    \n    setTimeout(() => {\n        document.body.removeChild(note);\n    }, 1000);\n    \n    playRankSongs(songs);\n};\n\nonMounted(async () => {\n    const response = await get('/rank/list');\n    if (response.status === 1) {\n        allRanks.value = response.data.info;\n        \n        const savedRankIds = localStorage.getItem('selectedRankIds');\n        if (savedRankIds) {\n            const rankIds = JSON.parse(savedRankIds);\n            await loadSelectedRanks(allRanks.value, rankIds);\n        } else {\n            await loadRandomRanks(allRanks.value, 4);\n        }\n    }\n});\n</script>\n\n<style scoped>\n.ranking-container {\n    display: flex;\n    flex-direction: column;\n    gap: 20px;\n    padding: 20px;\n}\n\n.rank-selector {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 12px;\n    padding: 16px;\n    background: #ffffff;\n    border-radius: 16px;\n    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\n}\n\n.rank-chip {\n    padding: 8px 16px;\n    border-radius: 20px;\n    background: #f5f5f5;\n    color: #666;\n    font-size: 14px;\n    cursor: pointer;\n    transition: all 0.3s ease;\n}\n\n.rank-chip:hover {\n    background: #eeeeee;\n    transform: translateY(-2px);\n}\n\n.rank-chip.active {\n    background: var(--primary-color)!important;\n    color: white;\n}\n\n.ranking-list {\n    display: grid;\n    grid-template-columns: repeat(2, 1fr);\n    gap: 20px;\n    padding: 20px;\n}\n\n.ranking-item {\n    background: #ffffff;\n    border-radius: 16px;\n    overflow: hidden;\n    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);\n    transition: all 0.3s ease;\n    height: 600px;\n    display: flex;\n    flex-direction: column;\n}\n\n.ranking-item:hover {\n    transform: translateY(-4px);\n    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);\n}\n\n.rank-header {\n    display: flex;\n    align-items: center;\n    padding: 20px;\n    position: relative;\n    background: linear-gradient(to right, rgba(100, 61, 73, 0.133), transparent)\n}\n\n.rank-cover {\n    width: 100px;\n    height: 100px;\n    border-radius: 12px;\n    overflow: hidden;\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n}\n\n.rank-cover img {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    transition: transform 0.3s ease;\n}\n\n.rank-cover:hover img {\n    transform: scale(1.05);\n}\n\n.rank-info {\n    flex: 1;\n    margin-left: 20px;\n}\n\n.rank-title {\n    font-size: 24px;\n    font-weight: 600;\n    margin: 0 0 8px 0;\n}\n\n.rank-update {\n    font-size: 13px;\n    color: #666;\n}\n\n.rank-play-btn {\n    position: absolute;\n    top: 20px;\n    right: 20px;\n    width: 40px;\n    height: 40px;\n    border-radius: 50%;\n    background: rgba(255, 255, 255, 0.9);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    cursor: pointer;\n    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n    transition: all 0.3s ease;\n}\n\n.rank-play-btn:hover {\n    transform: scale(1.1);\n    background: var(--primary-color);\n}\n\n.rank-play-btn:hover i {\n    color: white;\n}\n\n.rank-play-btn i {\n    font-size: 20px;\n    color: var(--primary-color);\n    transition: color 0.3s ease;\n}\n\n.song-list {\n    flex: 1;\n    overflow-y: auto;\n    padding: 0 12px;\n}\n\n.song-item {\n    display: flex;\n    align-items: center;\n    padding: 12px 8px;\n    border-radius: 8px;\n    transition: all 0.2s ease;\n    cursor: pointer;\n}\n\n.song-item:hover {\n    background: #f8f9fa;\n}\n\n.song-rank {\n    width: 40px;\n    text-align: center;\n}\n\n.song-index {\n    font-size: 16px;\n    font-weight: 500;\n    color: #999;\n}\n\n.song-index.top-three {\n    font-size: 18px;\n    font-weight: 600;\n    background: linear-gradient(45deg, #ff6b6b, #ff8787);\n    -webkit-background-clip: text;\n    -webkit-text-fill-color: transparent;\n}\n\n.song-cover {\n    position: relative;\n    width: 48px;\n    height: 48px;\n    border-radius: 8px;\n    overflow: hidden;\n    margin: 0 16px;\n}\n\n.song-cover img {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n}\n\n.hover-play {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background: rgba(0, 0, 0, 0.4);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    opacity: 0;\n    transition: opacity 0.2s ease;\n}\n\n.song-item:hover .hover-play {\n    opacity: 1;\n}\n\n.hover-play i {\n    color: white;\n    font-size: 24px;\n}\n\n.song-info {\n    flex: 1;\n    min-width: 0;\n    padding-right: 12px;\n}\n\n.song-content {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    width: 100%;\n}\n\n.song-main {\n    flex: 2;\n    min-width: 0;\n    margin-right: 16px;\n}\n\n.song-name {\n    font-size: 14px;\n    font-weight: 500;\n    color: #333;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.song-author {\n    font-size: 13px;\n    color: #666;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n}\n\n.song-meta {\n    flex: 1;\n    display: flex;\n    align-items: center;\n    justify-content: flex-end;\n    min-width: 0;\n}\n\n.album {\n    flex: 1;\n    font-size: 12px;\n    color: #999;\n    white-space: nowrap;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    margin-right: 16px;\n    text-align: right;\n}\n\n.duration {\n    font-size: 12px;\n    color: #999;\n    flex-shrink: 0;\n    min-width: 45px;\n    text-align: right;\n}\n\n/* 自定义滚动条 */\n.song-list::-webkit-scrollbar {\n    width: 6px;\n}\n\n.song-list::-webkit-scrollbar-thumb {\n    background: #ddd;\n    border-radius: 3px;\n}\n\n.song-list::-webkit-scrollbar-track {\n    background: #f5f5f5;\n}\n\n.loading-indicator {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 20px;\n    color: #666;\n    font-size: 14px;\n    gap: 8px;\n}\n\n.loading-spinner {\n    width: 16px;\n    height: 16px;\n    border: 2px solid #f3f3f3;\n    border-top: 2px solid var(--primary-color);\n    border-radius: 50%;\n    animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n    0% { transform: rotate(0deg); }\n    100% { transform: rotate(360deg); }\n}\n\n.no-more-indicator {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 16px;\n    color: #999;\n    font-size: 13px;\n    border-top: 1px solid #f0f0f0;\n    margin-top: 8px;\n}\n\n@media (max-width: 1200px) {\n    .ranking-list {\n        grid-template-columns: repeat(2, 1fr);\n        gap: 15px;\n        padding: 15px;\n    }\n    \n    .ranking-item {\n        height: 500px;\n        min-height: 500px;\n    }\n    \n    .rank-header {\n        padding: 15px;\n    }\n    \n    .rank-cover {\n        width: 80px;\n        height: 80px;\n    }\n    \n    .rank-title {\n        font-size: 20px;\n    }\n    \n    .rank-update {\n        font-size: 12px;\n    }\n}\n\n@media (max-width: 768px) {\n    .ranking-container {\n        padding: 10px;\n    }\n    \n    .rank-selector {\n        padding: 12px;\n        gap: 8px;\n    }\n    \n    .rank-chip {\n        padding: 6px 12px;\n        font-size: 12px;\n    }\n    \n    .ranking-list {\n        gap: 10px;\n        padding: 10px;\n        grid-template-columns: 1fr;\n    }\n    \n    .ranking-item {\n        height: 400px;\n    }\n    \n    .rank-cover {\n        width: 60px;\n        height: 60px;\n    }\n    \n    .rank-info {\n        margin-left: 10px;\n    }\n    \n    .rank-title {\n        font-size: 16px;\n        margin: 0 0 4px 0;\n    }\n    \n    .song-cover {\n        width: 40px;\n        height: 40px;\n        margin: 0 10px;\n    }\n    \n    .song-rank {\n        width: 30px;\n    }\n    \n    .song-name {\n        font-size: 13px;\n    }\n    \n    .song-author {\n        font-size: 12px;\n    }\n    \n    .album {\n        display: none;\n    }\n}\n\n:global(.music-note) {\n    position: fixed;\n    color: #ff6b6b;\n    font-size: 24px;\n    pointer-events: none;\n    z-index: 9999;\n    transition: all 1s cubic-bezier(0.4, 0, 0.2, 1);\n}\n</style>"
  },
  {
    "path": "src/views/Search.vue",
    "content": "<template>\n    <div class=\"search-page\">\n        <div class=\"search-results\">\n            <h2 class=\"section-title\">{{ $t('sou-suo-jie-guo') }}</h2>\n            <!-- 添加搜索类型标签栏 -->\n            <div class=\"search-tabs\">\n                <button \n                    v-for=\"tab in searchTabs\" \n                    :key=\"tab.type\" \n                    :class=\"['tab-button', { active: searchType === tab.type }]\"\n                    @click=\"changeSearchType(tab.type)\"\n                >\n                    {{ tab.name }}\n                </button>\n            </div>\n            <!-- 骨架屏加载效果 -->\n            <div v-if=\"isLoading\" class=\"skeleton-container\">\n                <!-- 歌曲骨架屏 -->\n                <div v-if=\"searchType === 'song'\" class=\"song-skeleton\">\n                    <div v-for=\"i in 10\" :key=\"i\" class=\"skeleton-item result-item\">\n                        <div class=\"skeleton-cover\"></div>\n                        <div class=\"skeleton-info\">\n                            <div class=\"skeleton-line\"></div>\n                            <div class=\"skeleton-line short\"></div>\n                        </div>\n                        <div class=\"skeleton-meta\">\n                            <div class=\"skeleton-line tiny\"></div>\n                            <div class=\"skeleton-line tiny\"></div>\n                        </div>\n                    </div>\n                </div>\n                \n                <!-- 歌手/专辑/歌单共用骨架屏 -->\n                <div v-else class=\"grid-skeleton\">\n                    <div class=\"skeleton-grid\">\n                        <div v-for=\"i in 12\" :key=\"i\" :class=\"['skeleton-grid-card', {\n                            'skeleton-artist-card': searchType === 'author',\n                            'skeleton-album-card': searchType === 'album',\n                            'skeleton-playlist-card': searchType === 'special'\n                        }]\">\n                            <div :class=\"[searchType === 'author' ? 'skeleton-avatar' : 'skeleton-cover square']\"></div>\n                            <div class=\"skeleton-line\"></div>\n                            <div v-if=\"searchType !== 'author'\" class=\"skeleton-line short\"></div>\n                        </div>\n                    </div>\n                </div>\n            </div>\n            \n            <template v-else-if=\"searchResults.length > 0\">\n                <!-- 歌曲搜索结果 -->\n                <ul v-if=\"searchType === 'song'\">\n                    <li v-for=\"(result, index) in searchResults\" :key=\"index\" class=\"result-item\"\n                        @click=\"playSong(result?.HQFileHash || result?.SQFileHash || result?.FileHash, result.OriSongName, $getCover(result.Image, 480), result.SingerName)\"\n                        @contextmenu.prevent=\"showContextMenu($event, result)\">\n                        <img :src=\"$getCover(result.Image, 100)\" alt=\"Cover\" />\n                        <div class=\"result-info\">\n                            <p class=\"result-name\">{{ result.OriSongName }}</p>\n                            <p class=\"result-type\">{{ result.SingerName }}</p>\n                        </div>\n                        <div class=\"result-meta\">\n                            <div class=\"meta-column\">\n                                <p class=\"result-duration\">{{ $formatMilliseconds(result.Duration) }}</p>\n                                <p class=\"result-publish-date\">{{ result.PublishDate }}</p>\n                            </div>\n                        </div>\n                    </li>\n                </ul>\n                \n                <!-- 歌手搜索结果 -->\n                <ArtistGrid v-else-if=\"searchType === 'author'\" :artists=\"searchResults\" @artist-click=\"handleArtistClick\" />\n                \n                <!-- 专辑搜索结果 -->\n                <AlbumGrid v-else-if=\"searchType === 'album'\" :albums=\"searchResults\" @album-click=\"handleAlbumClick\" />\n                \n                <!-- 歌单搜索结果 -->\n                <PlaylistGrid v-else-if=\"searchType === 'special'\" :playlists=\"searchResults\" @playlist-click=\"handlePlaylistClick\" />\n\n                <div class=\"pagination\">\n                    <button @click=\"prevPage\" :disabled=\"currentPage === 1\">{{ $t('shang-yi-ye') }}</button>\n                    <div class=\"page-numbers\">\n                        <button v-for=\"pageNum in displayedPageNumbers\" :key=\"pageNum\" :class=\"['page-number', {\n                            active: pageNum === currentPage,\n                            'ellipsis': pageNum === '...'\n                        }]\" @click=\"pageNum !== '...' && goToPage(pageNum)\" :disabled=\"pageNum === '...'\">\n                            {{ pageNum }}\n                        </button>\n                    </div>\n                    <button @click=\"nextPage\" :disabled=\"currentPage === totalPages\">{{ $t('xia-yi-ye') }}</button>\n                </div>\n            </template>\n        </div>\n    </div>\n    <ContextMenu ref=\"contextMenuRef\" :playerControl=\"playerControl\" />\n</template>\n<script setup>\nimport { ref, onMounted, watch, computed } from 'vue';\nimport ContextMenu from '../components/ContextMenu.vue';\nimport AlbumGrid from '../components/AlbumGrid.vue';\nimport PlaylistGrid from '../components/PlaylistGrid.vue';\nimport ArtistGrid from '../components/ArtistGrid.vue';\nimport { get } from '../utils/request';\nimport { useRoute, useRouter } from 'vue-router';\nconst route = useRoute();\nconst router = useRouter();\nconst searchQuery = ref(route.query.q || '');\nconst searchType = ref(route.query.type || 'song'); \nconst searchResults = ref([]);\nconst currentPage = ref(1);\nconst pageSize = ref(30);\nconst totalPages = ref(1);\nconst contextMenuRef = ref(null);\nconst isLoading = ref(false);\n\nconst searchTabs = [\n    { type: 'song', name: '单曲' },\n    { type: 'special', name: '歌单' },\n    { type: 'album', name: '专辑' },\n    { type: 'author', name: '歌手' }\n];\n\n// 切换搜索类型\nconst changeSearchType = (type) => {\n    searchType.value = type;\n    currentPage.value = 1; // 切换类型时重置页码\n    \n    // 更新URL参数\n    router.push({\n        query: { \n            ...route.query,\n            type: type \n        }\n    });\n    performSearch();\n};\n\nconst showContextMenu = (event, song) => {\n    if (contextMenuRef.value) {\n        song.cover = song.Image?.replace(\"{size}\", 480) || './assets/images/ico.png',\n        song.timeLength = song.Duration;\n        song.OriSongName = song.FileName;\n        contextMenuRef.value.openContextMenu(event, song);\n    }\n};\n\nonMounted(() => {\n    if (route.query.type) {\n        searchType.value = route.query.type;\n    }\n    performSearch();\n});\n\nwatch(() => route.query.q, (newQuery) => {\n    currentPage.value = 1;\n    searchQuery.value = newQuery;\n    performSearch();\n});\n\nconst props = defineProps({\n    playerControl: Object\n});\n\nconst playSong = (hash, name, img, author) => {\n    props.playerControl.addSongToQueue(hash, name, img, author);\n};\n\nconst performSearch = async () => {\n    if (!searchQuery.value) return;\n    isLoading.value = true;\n    try {\n        const response = await get(`/search?keywords=${encodeURIComponent(searchQuery.value)}&page=${currentPage.value}&pagesize=${pageSize.value}&type=${searchType.value}`)\n        if (response.status === 1) {\n            searchResults.value = response.data.lists;\n            totalPages.value = Math.ceil(response.data.total / pageSize.value);\n        }\n    } catch (error) {\n        console.error(\"搜索请求失败\", error);\n    } finally {\n        isLoading.value = false;\n    }\n};\n\n// 分页操作\nconst nextPage = () => {\n    if (currentPage.value < totalPages.value) {\n        currentPage.value++;\n        performSearch();\n    }\n};\n\nconst prevPage = () => {\n    if (currentPage.value > 1) {\n        currentPage.value--;\n        performSearch();\n    }\n};\n\nconst displayedPageNumbers = computed(() => {\n    const delta = 2; // 当前页前后显示的页码数\n    let pages = [];\n\n    if (totalPages.value <= 7) {\n        // 如果总页数小于等于7，显示所有页码\n        for (let i = 1; i <= totalPages.value; i++) {\n            pages.push(i);\n        }\n    } else {\n        // 始终显示第一页\n        pages.push(1);\n\n        // 计算中间页码的范围\n        let leftBound = Math.max(2, currentPage.value - delta);\n        let rightBound = Math.min(totalPages.value - 1, currentPage.value + delta);\n\n        // 添加左边的省略号\n        if (leftBound > 2) {\n            pages.push('...');\n        }\n\n        // 添加中间的页码\n        for (let i = leftBound; i <= rightBound; i++) {\n            pages.push(i);\n        }\n\n        // 添加右边的省略号\n        if (rightBound < totalPages.value - 1) {\n            pages.push('...');\n        }\n\n        // 始终显示最后一页\n        pages.push(totalPages.value);\n    }\n\n    return pages;\n});\n\nconst goToPage = (page) => {\n    currentPage.value = page;\n    performSearch();\n};\n\nconst handleAlbumClick = (album) => {\n    router.push(`/PlaylistDetail?albumid=${album.albumid}`);\n};\n\nconst handlePlaylistClick = (playlist) => {\n    router.push({\n        path: `/PlaylistDetail`,\n        query: { global_collection_id: playlist.gid }\n    });\n};\n\nconst handleArtistClick = (artist) => {\n    router.push({\n        path: '/PlaylistDetail',\n        query: { \n            singerid: artist.AuthorId\n        }\n    });\n};\n</script>\n\n<style scoped>\n.search-results {\n    padding: 20px;\n}\n\n.search-tabs {\n    display: flex;\n    margin-bottom: 20px;\n    border-bottom: 1px solid #eee;\n}\n\n.tab-button {\n    padding: 10px 20px;\n    background: none;\n    border: none;\n    cursor: pointer;\n    font-size: 16px;\n    color: #666;\n    position: relative;\n    transition: all 0.3s;\n    border-radius: 5px 5px 0 0;\n}\n\n.tab-button:hover {\n    color: var(--primary-color);\n}\n\n.tab-button.active {\n    color: var(--primary-color);\n    font-weight: bold;\n}\n\n.tab-button.active::after {\n    content: '';\n    position: absolute;\n    bottom: -1px;\n    left: 0;\n    width: 100%;\n    height: 2px;\n    background-color: var(--primary-color);\n}\n\n.result-item {\n    display: flex;\n    align-items: center;\n    padding: 10px;\n    border-bottom: 1px solid #f0f0f0;\n    transition: background-color 0.3s;\n    cursor: pointer;\n    border-radius: 5px;\n    gap: 10px;\n}\n\n.result-item:hover {\n    background-color: #f5f5f5;\n}\n\n.result-item img {\n    width: 50px;\n    height: 50px;\n    border-radius: 5px;\n    margin-right: 10px;\n}\n\n.result-info {\n    display: flex;\n    flex-direction: column;\n    flex: 1;\n    min-width: 0; /* 防止flex子项溢出 */\n}\n\n.result-meta {\n    display: flex;\n    margin-left: auto;\n    min-width: 120px;\n    justify-content: flex-end;\n    padding-right: 20px;\n}\n\n.meta-column {\n    display: flex;\n    flex-direction: column;\n    align-items: flex-end;\n    gap: 6px;\n}\n\n.result-name {\n    font-size: 16px;\n    font-weight: bold;\n    height: 23px;\n    margin: 0;\n    max-width: 900px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.result-duration,\n.result-publish-date {\n    font-size: 14px;\n    color: #888;\n    margin: 0;\n    white-space: nowrap;\n}\n\n.result-duration {\n    color: #666;\n}\n\n.result-publish-date {\n    font-size: 12px;\n    color: #999;\n}\n\n.result-type {\n    font-size: 14px;\n    color: #666;\n    margin: 6px 0 0 0;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.pagination {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    margin: 20px 0;\n    gap: 10px;\n}\n\n.page-numbers {\n    display: flex;\n    gap: 5px;\n}\n\n.page-number {\n    padding: 8px 12px;\n    background-color: #f5f5f5;\n    border: 1px solid #ddd;\n    border-radius: 4px;\n    cursor: pointer;\n    color: #333;\n    min-width: 40px;\n    transition: all 0.3s;\n}\n\n.page-number:hover {\n    background-color: var(--primary-color);\n    color: white;\n}\n\n.page-number.active {\n    background-color: var(--primary-color);\n    color: white;\n    border-color: var(--primary-color);\n}\n\n.pagination button {\n    padding: 8px 15px;\n    background-color: white;\n    color: #333;\n    border: 1px solid #ddd;\n    border-radius: 8px;\n    cursor: pointer;\n    transition: all 0.3s;\n}\n\n.pagination button:hover:not(:disabled) {\n    background-color: var(--primary-color);\n    color: white;\n}\n\n.pagination button:disabled {\n    background-color: white;\n    color: #999;\n    cursor: not-allowed;\n    border-color: #ddd;\n}\n\n.section-title {\n    font-size: 28px;\n    font-weight: bold;\n    margin-bottom: 30px;\n    color: var(--primary-color);\n}\n\n.page-number.ellipsis {\n    background-color: transparent;\n    border: none;\n    cursor: default;\n    pointer-events: none;\n    padding: 8px 8px;\n    min-width: 30px;\n}\n\n.page-number.ellipsis:hover {\n    background-color: transparent;\n    color: #333;\n}\n\n\n</style>\n\n<!-- 添加骨架屏样式 -->\n<style scoped>\n/* 骨架屏动画 */\n@keyframes shimmer {\n    0% {\n        background-position: -468px 0;\n    }\n    100% {\n        background-position: 468px 0;\n    }\n}\n\n.skeleton-container {\n    width: 100%;\n}\n\n.skeleton-item {\n    margin-bottom: 15px;\n}\n\n.skeleton-cover, .skeleton-avatar {\n    width: 50px;\n    height: 50px;\n    border-radius: 5px;\n    background: linear-gradient(to right, #f0f0f0 8%, #e0e0e0 18%, #f0f0f0 33%);\n    background-size: 800px 104px;\n    animation: shimmer 1.5s linear infinite forwards;\n}\n\n.skeleton-avatar {\n    border-radius: 50%;\n    width: 100px;\n    height: 100px;\n    margin: 0 auto 10px;\n}\n\n.skeleton-cover.square {\n    width: 150px;\n    height: 150px;\n    margin: 0 auto 10px;\n}\n\n.skeleton-info {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n}\n\n.skeleton-meta {\n    display: flex;\n    flex-direction: column;\n    gap: 6px;\n    min-width: 120px;\n    align-items: flex-end;\n}\n\n.skeleton-line {\n    height: 16px;\n    background: linear-gradient(to right, #f0f0f0 8%, #e0e0e0 18%, #f0f0f0 33%);\n    background-size: 800px 104px;\n    animation: shimmer 1.5s linear infinite forwards;\n    border-radius: 3px;\n    width: 100%;\n    margin-top: 5px;\n}\n\n.skeleton-line.short {\n    width: 60%;\n}\n\n.skeleton-line.tiny {\n    width: 40%;\n    height: 12px;\n}\n\n.skeleton-grid {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n    gap: 20px;\n}\n\n.skeleton-artist-card, .skeleton-album-card, .skeleton-playlist-card {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    padding: 15px;\n    background-color: #f9f9f9;\n    border-radius: 8px;\n    transition: transform 0.3s;\n}\n</style>"
  },
  {
    "path": "src/views/Settings.vue",
    "content": "<template>\n    <div class=\"settings-page\">\n        <div class=\"settings-sidebar\">\n            <div v-for=\"(section, sectionIndex) in settingSections\" :key=\"sectionIndex\" \n                 class=\"sidebar-item\" \n                 :class=\"{ active: activeTab === sectionIndex }\"\n                 @click=\"activeTab = sectionIndex\">\n                <i :class=\"getSectionIcon(section.title)\"></i>\n                <span>{{ section.title }}</span>\n            </div>\n        </div>\n        \n        <div class=\"settings-content\">\n            <div v-for=\"(section, sectionIndex) in settingSections\" :key=\"sectionIndex\" \n                 class=\"setting-section\" \n                 v-show=\"activeTab === sectionIndex\">\n                <h3>{{ section.title }}</h3>\n                <ExtensionManager v-if=\"section.title === t('cha-jian')\" />\n                <div v-else class=\"settings-cards\">\n                    <div v-for=\"(item, itemIndex) in section.items\" :key=\"itemIndex\"\n                        class=\"setting-card\" @click=\"item.action ? item.action(item.helpLink) : openSelection(item.key, item.helpLink)\">\n                        <div class=\"setting-card-header\">\n                            <i :class=\"getItemIcon(item.key)\"></i>\n                            <span>{{ item.label }}</span>\n                            <span v-if=\"item.showRefreshHint && showRefreshHint[item.key]\" class=\"refresh-hint\">\n                                {{ item.refreshHintText }}\n                            </span>\n                        </div>\n                        <div class=\"setting-card-value\">\n                            <span>{{ item.icon }}{{ item.customText || selectedSettings[item.key]?.displayText }}</span>\n                            <i class=\"fas fa-chevron-right\"></i>\n                        </div>\n                    </div>\n                </div>\n            </div>\n\n            <div class=\"reset-settings-container\">\n                <button @click=\"openResetConfirmation\" class=\"reset-settings-button\">\n                    <i class=\"fas fa-sync-alt\"></i>\n                    {{ $t('hui-fu-chu-chang-she-zhi') }}\n                </button>\n            </div>\n            <div class=\"version-info\">\n                <p>© MoeKoe Music</p>\n                <span v-if=\"appVersion\">V{{ appVersion }} - {{ platform }}</span>\n            </div>\n        </div>\n\n        <div v-if=\"isSelectionOpen\" class=\"modal\">\n            <div class=\"modal-content\">\n                <a\n                    v-if=\"currentHelpLink\"\n                    class=\"help-link\"\n                    @click=\"openHelpLink\"\n                    :title=\"$t('bang-zhu')\"\n                    :aria-label=\"$t('bang-zhu')\"\n                >\n                    <i class=\"fas fa-question-circle\"></i>\n                </a>\n                <h3>{{ selectionTypeMap[selectionType].title }}</h3>\n                <ul v-if=\"selectionType !== 'font' && selectionType !== 'audioOutputDevice'\">\n                    <li v-for=\"option in selectionTypeMap[selectionType].options\" :key=\"option\" @click=\"selectOption(option)\">\n                        {{ option.displayText }}\n                    </li>\n                </ul>\n\n                <ul v-else-if=\"selectionType === 'audioOutputDevice'\">\n                    <li v-if=\"audioOutputDevicesLoading\">正在获取设备列表...</li>\n                    <li v-else-if=\"audioOutputDeviceOptions.length === 0\">未检测到音频输出设备</li>\n                    <li v-else v-for=\"option in audioOutputDeviceOptions\" :key=\"option.value\" @click=\"selectOption(option)\">\n                        {{ option.displayText }}\n                    </li>\n                </ul>\n\n                <div v-if=\"selectionType === 'font'\" class=\"api-settings-container\" @focusout=\"handleFontFocusOut\">\n                    <div class=\"api-setting-item\">\n                        <label>{{ $t('zi-ti-url-di-zhi') }}</label>\n                        <input type=\"text\" v-model=\"fontUrlInput\" class=\"api-input\" :placeholder=\"$t('qing-shu-ru-zi-ti-url-di-zhi')\" />\n                    </div>\n                    <div class=\"api-setting-item\">\n                        <label>{{ $t('zi-ti-ming-cheng') }}</label>\n                        <input type=\"text\" v-model=\"fontFamilyInput\" class=\"api-input\" :placeholder=\"$t('qing-shu-ru-zi-ti-ming-cheng')\" />\n                    </div>\n                </div>\n\n                <div v-if=\"selectionType === 'highDpi'\" class=\"scale-slider-container\">\n                    <div class=\"scale-slider-label\">{{ $t('suo-fang-yin-zi') }}: {{ dpiScale }} <span class=\"scale-slider-hint\">{{ $t('tiao-zheng-hou-xu-zhong-qi') }}</span></div>\n                    <div class=\"scale-slider-wrapper\">\n                        <input\n                            type=\"range\"\n                            min=\"0.5\"\n                            max=\"2\"\n                            step=\"0.1\"\n                            v-model=\"dpiScale\"\n                            class=\"scale-slider\"\n                        />\n                        <div class=\"scale-marks\">\n                            <span>0.5</span>\n                            <span>1.0</span>\n                            <span>1.5</span>\n                            <span>2.0</span>\n                        </div>\n                    </div>\n                </div>\n\n                <div v-if=\"selectionType === 'apiMode' && selectedSettings.apiMode.value === 'on'\" class=\"api-settings-container\">\n                    <div class=\"api-setting-item\">\n                        <label>{{ $t('api-di-zhi') }}</label>\n                        <input type=\"text\" :value=\"defaultApiBaseUrl\" readonly class=\"api-input\" />\n                    </div>\n                    <div class=\"api-setting-item\">\n                        <label>{{ $t('websocket-di-zhi') }}</label>\n                        <input type=\"text\" value=\"ws://127.0.0.1:6520\" readonly class=\"api-input\" />\n                    </div>\n                    <div class=\"api-hint\">\n                        {{ $t('mo-ren-api-ti-shi') }}\n                    </div>\n                </div>\n                <div v-if=\"selectionType === 'apiBaseUrlMode' && selectedSettings.apiBaseUrlMode.value === 'custom'\" class=\"api-settings-container\">\n                    <div class=\"api-setting-item\">\n                        <input\n                            type=\"text\"\n                            v-model=\"apiBaseUrlForm.url\"\n                            class=\"api-input\"\n                            :placeholder=\"`RPC地址（留空使用默认：${defaultApiBaseUrl}）`\"\n                        />\n                    </div>\n                    <div class=\"proxy-actions\">\n                        <button\n                            @click=\"testApiBaseUrl\"\n                            :disabled=\"apiBaseUrlForm.testing\"\n                            class=\"test-button\"\n                        >\n                            {{ apiBaseUrlForm.testing ? $t('zheng-zai-ce-shi') : $t('ce-shi-lian-jie') }}\n                        </button>\n                        <button class=\"primary\" @click=\"saveApiBaseUrl\">\n                            {{ $t('bao-cun-she-zhi-an-niu') }}\n                        </button>\n                    </div>\n                    <div v-if=\"apiBaseUrlForm.testResult\" :class=\"['proxy-test-result', apiBaseUrlForm.testStatus]\">\n                        {{ apiBaseUrlForm.testResult }}\n                    </div>\n                </div>\n                <div v-if=\"selectionType === 'proxy' && selectedSettings.proxy.value === 'on'\" class=\"proxy-settings-container\">\n                    <div class=\"api-setting-item\">\n                        <input\n                            type=\"text\"\n                            v-model=\"proxyForm.url\"\n                            class=\"api-input\"\n                            :placeholder=\"$t('dai-li-placeholder')\"\n                        />\n                    </div>\n                    <div class=\"proxy-actions\">\n                        <button\n                            @click=\"testProxyConnection\"\n                            :disabled=\"proxyForm.testing\"\n                            class=\"test-button\"\n                        >\n                            {{ proxyForm.testing ? $t('zheng-zai-ce-shi') : $t('ce-shi-lian-jie') }}\n                        </button>\n                        <button class=\"primary\" @click=\"saveProxy\">\n                            {{ $t('bao-cun-she-zhi-an-niu') }}\n                        </button>\n                    </div>\n                    <div v-if=\"proxyForm.testResult\" :class=\"['proxy-test-result', proxyForm.testStatus]\">\n                        {{ proxyForm.testResult }}\n                    </div>\n                </div>\n                <button @click=\"closeSelection\">{{ $t('guan-bi-an-niu') }}</button>\n            </div>\n        </div>\n\n        <!-- 快捷键设置弹窗 -->\n        <div v-if=\"showShortcutModal\" class=\"shortcut-modal\">\n            <div class=\"shortcut-modal-content\">\n                <h3>{{ $t('kuai-jie-jian-she-zhi') }}</h3>\n                <div class=\"shortcut-list\">\n                    <div class=\"shortcut-item\" v-for=\"(config, key) in shortcutConfigs\" :key=\"key\">\n                        <span>{{ config.label }}</span>\n                        <div class=\"shortcut-input\"\n                             @click=\"startRecording(key)\"\n                             :class=\"{ 'recording': recordingKey === key }\">\n                            {{ shortcuts[key] || $t('dian-ji-she-zhi-kuai-jie-jian') }}\n                            <div v-if=\"shortcuts[key]\"\n                                 class=\"clear-shortcut\"\n                                 @click.stop=\"clearShortcut(key)\">\n                                ×\n                            </div>\n                        </div>\n                    </div>\n                </div>\n                <div class=\"shortcut-modal-footer\">\n                    <button @click=\"closeShortcutSettings\">{{ $t('qu-xiao') }}</button>\n                    <button @click=\"saveShortcuts\" class=\"primary\">{{ $t('bao-cun') }}</button>\n                </div>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref, onMounted, getCurrentInstance, onUnmounted, computed, reactive } from 'vue';\nimport { useI18n } from 'vue-i18n';\nimport { MoeAuthStore } from '../stores/store';\nimport ExtensionManager from '@/components/ExtensionManager.vue';\nimport { requestMicrophonePermission } from '../utils/utils';\nimport { DEFAULT_API_BASE_URL, validateApiBaseUrl, testApiBaseUrl as testApiBaseUrlRequest } from '@/utils/apiBaseUrl';\n\nconst MoeAuth = MoeAuthStore();\nconst { t } = useI18n();\nconst { proxy } = getCurrentInstance();\nconst appVersion = ref('');\nconst platform = ref('');\nconst activeTab = ref(0);\nconst defaultApiBaseUrl = DEFAULT_API_BASE_URL;\n\n// 设置配置\nconst selectedSettings = ref({\n    language: { displayText: '🌏 ' + t('zi-dong'), value: '' },\n    themeColor: { displayText: t('shao-nv-fen'), value: 'pink' },\n    theme: { displayText: '☀️ ' + t('qian-se'), value: 'light' },\n    nativeTitleBar: { displayText: t('guan-bi'), value: 'off' },\n    quality: { displayText: '标准音质 - 128Kbps', value: '128' },\n    lyricsBackground: { displayText: t('da-kai'), value: 'on' },\n    desktopLyrics: { displayText: t('guan-bi'), value: 'off' },\n    statusBarLyrics: { displayText: t('guan-bi'), value: 'off' },\n    lyricsFontSize: { displayText: t('zhong'), value: '24px' },\n    lyricsTranslation: { displayText: t('da-kai'), value: 'on' },\n    lyricsAlign: { displayText: t('ju-zhong'), value: 'center' },\n    font: { displayText: t('mo-ren-zi-ti'), value: '' },\n    fontUrl: { displayText: t('mo-ren-zi-ti'), value: '' },\n    greetings: { displayText: t('kai-qi'), value: 'on' },\n    gpuAcceleration: { displayText: t('guan-bi'), value: 'off' },\n    minimizeToTray: { displayText: t('da-kai'), value: 'on' },\n    highDpi: { displayText: t('guan-bi'), value: 'off' },\n    dpiScale: { displayText: '1.0', value: '1.0' },\n    apiMode: { displayText: t('guan-bi'), value: 'off' },\n    touchBar: { displayText: t('guan-bi'), value: 'off' },\n    autoStart: { displayText: t('guan-bi'), value: 'off' },\n    startMinimized: { displayText: t('guan-bi'), value: 'off' },\n    preventAppSuspension: { displayText: t('guan-bi'), value: 'off' },\n    networkMode: { displayText: t('zhu-wang'), value: 'mainnet' },\n    proxy: { displayText: t('guan-bi'), value: 'off' },\n    proxyUrl: { displayText: '', value: '' },\n    apiBaseUrlMode: { displayText: '默认', value: 'default' },\n    apiBaseUrl: { displayText: '', value: '' },\n    dataSource: { displayText: t('gai-nian-ban-xuan-xiang'), value: 'concept' },\n    loudnessNormalization: { displayText: t('guan-bi'), value: 'off' },\n    pauseOnAudioOutputChange: { displayText: t('guan-bi'), value: 'off' },\n    audioOutputDevice: { displayText: '默认', value: 'default' },\n});\n\n// 设置分区配置\nconst settingSections = computed(() => [\n    {\n        title: t('jie-mian'),\n        items: [\n            {\n                key: 'language',\n                label: t('yu-yan')\n            },\n            {\n                key: 'themeColor',\n                label: t('zhu-se-tiao'),\n                icon: '🎨 '\n            },\n            {\n                key: 'theme',\n                label: t('wai-guan')\n            },\n            {\n                key: 'nativeTitleBar',\n                label: t('native-title-bar'),\n                showRefreshHint: true,\n                refreshHintText: t('zhong-qi-hou-sheng-xiao')\n            },\n            {\n                key: 'font',\n                label: t('zi-ti-she-zhi'),\n                showRefreshHint: true,\n                refreshHintText: t('shua-xin-hou-sheng-xiao'),\n                helpLink:'https://music.moekoe.cn/guide/font-settings.html'\n            }\n        ]\n    },\n    {\n        title: t('sheng-yin'),\n        items: [\n            {\n                key: 'quality',\n                label: t('yin-zhi-xuan-ze'),\n                icon: '🎧 '\n            },\n            {\n                key: 'loudnessNormalization',\n                label: t('ping-heng-yin-pin-xiang-du'),\n                icon: '🎚️ ',\n                showRefreshHint: true,\n                refreshHintText: t('shua-xin-hou-sheng-xiao')\n            },\n            {\n                key: 'pauseOnAudioOutputChange',\n                label: '输出设备变化自动暂停',\n                icon: '🎧 ',\n                helpLink:'https://music.moekoe.cn/guide/auto-pause-on-output-device-change.html'\n            },\n            {\n                key: 'audioOutputDevice',\n                label: '音频输出设备',\n                icon: '🔊 ',\n                helpLink:'https://music.moekoe.cn/guide/audio-output-device.html'\n            },\n            {\n                key: 'greetings',\n                label: t('qi-dong-wen-hou-yu'),\n                icon: '👋 '\n            },\n            {\n                key: 'dataSource',\n                label: t('shu-ju-yuan'),\n                icon: '🔌 ',\n                showRefreshHint: true,\n                refreshHintText: t('zhong-qi-hou-sheng-xiao'),\n                helpLink:'https://music.moekoe.cn/guide/data-source.html'\n            }\n        ]\n    },\n    {\n        title: t('ge-ci'),\n        items: [\n            {\n                key: 'lyricsBackground',\n                label: t('xian-shi-ge-ci-bei-jing'),\n                showRefreshHint: true,\n                refreshHintText: t('shua-xin-hou-sheng-xiao')\n            },\n            {\n                key: 'lyricsFontSize',\n                label: t('ge-ci-zi-ti-da-xiao'),\n                showRefreshHint: true,\n                refreshHintText: t('shua-xin-hou-sheng-xiao')\n            },\n            {\n                key: 'desktopLyrics',\n                label: t('xian-shi-zhuo-mian-ge-ci')\n            },\n            {\n                key: 'statusBarLyrics',\n                label: t('zhuang-tai-lan-ge-ci'),\n                showRefreshHint: true,\n                refreshHintText: t('zhong-qi-hou-sheng-xiao')\n            },\n            {\n                key: 'lyricsTranslation',\n                label: t('ge-ci-fan-yi')\n            },\n            {\n                key: 'lyricsAlign',\n                label: t('dui-qi-fang-shi'),\n            }\n        ]\n    },\n    {\n        title: t('cha-jian'),\n        items: []\n    },\n    {\n        title: t('xi-tong'),\n        items: [\n            {\n                key: 'gpuAcceleration',\n                label: t('jin-yong-gpu-jia-su-zhong-qi-sheng-xiao'),\n                showRefreshHint: true,\n                refreshHintText: t('zhong-qi-hou-sheng-xiao')\n            },\n            {\n                key: 'highDpi',\n                label: t('shi-pei-gao-dpi'),\n                showRefreshHint: true,\n                refreshHintText: t('zhong-qi-hou-sheng-xiao')\n            },\n            {\n                key: 'minimizeToTray',\n                label: t('guan-bi-shi-minimize-to-tray')\n            },\n            {\n                key: 'autoStart',\n                label: t('kai-ji-zi-qi-dong')\n            },\n            {\n                key: 'networkMode',\n                label: t('wang-luo-mo-shi'),\n                showRefreshHint: true,\n                refreshHintText: t('zhong-qi-hou-sheng-xiao'),\n                helpLink:'https://music.moekoe.cn/guide/network-modes.html'\n            },\n            {\n                key: 'startMinimized',\n                label: t('qi-dong-shi-zui-xiao-hua')\n            },\n            {\n                key: 'preventAppSuspension',\n                label: t('zu-zhi-xi-tong-xiu-mian'),\n                showRefreshHint: true,\n                refreshHintText: t('zhong-qi-hou-sheng-xiao')\n            },\n            {\n                key: 'apiMode',\n                label: t('api-mo-shi'),\n                showRefreshHint: true,\n                refreshHintText: t('zhong-qi-hou-sheng-xiao')\n            },\n             {\n                key: 'apiBaseUrlMode',\n                label: 'RPC地址',\n                showRefreshHint: true,\n                refreshHintText: t('shua-xin-hou-sheng-xiao'),\n                helpLink:'https://music.moekoe.cn/guide/rpc-api-base-url.html'\n             },\n            {\n                key: 'touchBar',\n                label: 'TouchBar',\n                showRefreshHint: true,\n                refreshHintText: t('zhong-qi-hou-sheng-xiao')\n            },\n            {\n                key: 'shortcuts',\n                label: t('quan-ju-kuai-jie-jian'),\n                customText: t('zi-ding-yi-kuai-jie-jian'),\n                action: openShortcutSettings\n            },\n            {\n                key: 'pwa',\n                label: t('pwa-app'),\n                customText: t('install'),\n                action: installPWA\n            },\n            {\n                key: 'proxy',\n                label: t('wang-luo-dai-li'),\n                showRefreshHint: true,\n                refreshHintText: t('zhong-qi-hou-sheng-xiao'),\n                helpLink:'https://music.moekoe.cn/guide/proxy-settings.html'\n            }\n        ]\n    }\n]);\n\n// 获取每个部分的图标\nconst getSectionIcon = (title) => {\n    const iconMap = {\n        [t('jie-mian')]: 'fas fa-palette',\n        [t('sheng-yin')]: 'fas fa-volume-up',\n        [t('ge-ci')]: 'fas fa-music',\n        [t('cha-jian')]: 'fas fa-puzzle-piece',\n        [t('xi-tong')]: 'fas fa-cog'\n    };\n    return iconMap[title] || 'fas fa-cog';\n};\n\n// 获取每个设置项的图标\nconst getItemIcon = (key) => {\n    const iconMap = {\n        'language': 'fas fa-language',\n        'themeColor': 'fas fa-paint-brush',\n        'theme': 'fas fa-moon',\n        'nativeTitleBar': 'fas fa-window-maximize',\n        'font': 'fas fa-font',\n        'quality': 'fas fa-headphones',\n        'loudnessNormalization': 'fas fa-sliders-h',\n        'pauseOnAudioOutputChange': 'fas fa-exchange-alt',\n        'audioOutputDevice': 'fas fa-volume-up',\n        'greetings': 'fas fa-comment',\n        'lyricsBackground': 'fas fa-image',\n        'lyricsFontSize': 'fas fa-text-height',\n        'desktopLyrics': 'fas fa-desktop',\n        'statusBarLyrics': 'fas fa-align-justify',\n        'lyricsTranslation': 'fas fa-language',\n        'lyricsAlign': 'fas fa-align-center',\n        'gpuAcceleration': 'fas fa-microchip',\n        'highDpi': 'fas fa-expand',\n        'minimizeToTray': 'fas fa-window-minimize',\n        'autoStart': 'fas fa-power-off',\n        'startMinimized': 'fas fa-compress',\n        'preventAppSuspension': 'fas fa-clock',\n        'apiMode': 'fas fa-code',\n        'apiBaseUrlMode': 'fas fa-link',\n        'touchBar': 'fas fa-tablet-alt',\n        'shortcuts': 'fas fa-keyboard',\n        'pwa': 'fas fa-mobile-alt',\n        'proxy': 'fas fa-random'\n    };\n    return iconMap[key] || 'fas fa-sliders-h';\n};\n\nconst isSelectionOpen = ref(false);\nconst currentHelpLink = ref('');\nconst selectionType = ref('');\nconst fontUrlInput = ref('');\nconst fontFamilyInput = ref('');\n\n// 选项配置\nconst selectionTypeMap = {\n    language: {\n        title: t('xuan-ze-yu-yan'),\n        options: [\n            { displayText: '🇨🇳 简体中文', value: 'zh-CN' },\n            { displayText: '🇨🇳 繁體中文', value: 'zh-TW' },\n            { displayText: '🇺🇸 English', value: 'en' },\n            { displayText: '🇷🇺 Русский', value: 'ru' },\n            { displayText: '🇯🇵 日本語', value: 'ja' },\n            { displayText: '🇰🇷 한국어', value: 'ko' }\n        ]\n    },\n    themeColor: {\n        title: t('xuan-ze-zhu-se-tiao'),\n        options: [\n            { displayText: t('shao-nv-fen'), value: 'pink' },\n            { displayText: t('nan-nan-lan'), value: 'blue' },\n            { displayText: t('tou-ding-lv'), value: 'green' },\n            { displayText: t('mi-gan-cheng'), value: 'orange' }\n        ]\n    },\n    theme: {\n        title: t('xuan-ze-wai-guan'),\n        options: [\n            { displayText: '🌗 ' + t('zi-dong'), value: 'auto' },\n            { displayText: '☀️ ' + t('qian-se'), value: 'light' },\n            { displayText: '🌙 ' + t('shen-se'), value: 'dark' }\n        ]\n    },\n    nativeTitleBar: {\n        title: t('native-title-bar'),\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    quality: {\n        title: t('yin-zhi-xuan-ze'),\n        options: [\n            { displayText: '标准音质 - 128Kbps', value: '128' },\n            { displayText: '高品音质 - 320Kbps', value: '320' },\n            { displayText: 'FLAC 无损', value: 'flac' },\n            { displayText: 'Hi-Res 无损', value: 'high' },\n            { displayText: '蝰蛇全景', value: 'viper_atmos' },\n            { displayText: '蝰蛇超清', value: 'viper_clear' },\n            { displayText: '蝰蛇母带', value: 'viper_tape' }\n        ]\n    },\n    lyricsBackground: {\n        title: t('xian-shi-ge-ci-bei-jing'),\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    desktopLyrics: {\n        title: t('xian-shi-zhuo-mian-ge-ci'),\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    statusBarLyrics: {\n        title: t('zhuang-tai-lan-ge-ci'),\n        options: [\n            { displayText: t('da-kai') + t('jin-zhi-chi-mac'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    lyricsFontSize: {\n        title: t('ge-ci-zi-ti-da-xiao'),\n        options: [\n            { displayText: t('xiao'), value: '20px' },\n            { displayText: t('zhong'), value: '24px' },\n            { displayText: t('da'), value: '32px' }\n        ]\n    },\n    greetings: {\n        title: t('qi-dong-wen-hou-yu'),\n        options: [\n            { displayText: t('kai-qi'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    gpuAcceleration: {\n        title: t('jin-yong-gpu-jia-su-zhong-qi-sheng-xiao'),\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    minimizeToTray: {\n        title: t('guan-bi-shi-minimize-to-tray'),\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    highDpi: {\n        title: t('shi-pei-gao-dpi'),\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    lyricsTranslation: {\n        title: t('ge-ci-fan-yi'),\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    lyricsAlign: {\n        title: t('dui-qi-fang-shi'),\n        options: [\n            { displayText: t('ju-zuo'), value: 'left' },\n            { displayText: t('ju-zhong'), value: 'center' },\n        ]\n    },\n    dpiScale: {\n        title: t('suo-fang-yin-zi'),\n        options: [\n            { displayText: '1.0', value: '1.0' }\n        ]\n    },\n    font: {\n        title: t('zi-ti-she-zhi'),\n        options: [\n            { displayText: t('mo-ren-zi-ti'), value: '' }\n        ]\n    },\n    fontUrl: {\n        title: t('zi-ti-wen-jian-di-zhi'),\n        options: [\n            { displayText: t('mo-ren-zi-ti'), value: '' }\n        ]\n    },\n    apiMode: {\n        title: t('api-mo-shi'),\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    apiBaseUrlMode: {\n        title: 'RPC地址',\n        options: [\n            { displayText: '默认', value: 'default' },\n            { displayText: '自定义', value: 'custom' }\n        ]\n    },\n    touchBar: {\n        title: 'TouchBar',\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    autoStart: {\n        title: t('kai-ji-zi-qi-dong'),\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    startMinimized: {\n        title: t('qi-dong-shi-zui-xiao-hua'),\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    preventAppSuspension: {\n        title: t('zu-zhi-xi-tong-xiu-mian'),\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    networkMode: {\n        title: t('wang-luo-jie-dian'),\n        options: [\n            { displayText: t('zhu-wang'), value: 'mainnet' },\n            { displayText: t('ce-wang'), value: 'testnet' },\n            { displayText: t('kai-fa-wang'), value: 'devnet' }\n        ]\n    },\n    proxy: {\n        title: t('wang-luo-dai-li'),\n        options: [\n            { displayText: t('qi-yong'), value: 'on' },\n            { displayText: t('jin-yong'), value: 'off' }\n        ]\n    },\n    proxyUrl: {\n        title: t('dai-li-di-zhi'),\n        options: []\n    },\n    dataSource: {\n        title: t('shu-ju-yuan'),\n        options: [\n            { displayText: t('gai-nian-ban-xuan-xiang'), value: 'concept' },\n            { displayText: t('zheng-shi-ban'), value: 'official' }\n        ]\n    },\n    loudnessNormalization: {\n        title: t('ping-heng-yin-pin-xiang-du')+'(实验性)',\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    pauseOnAudioOutputChange: {\n        title: '输出设备变化自动暂停(实验性)',\n        options: [\n            { displayText: t('da-kai'), value: 'on' },\n            { displayText: t('guan-bi'), value: 'off' }\n        ]\n    },\n    audioOutputDevice: {\n        title: '音频输出设备(实验性)',\n        options: []\n    },\n\n};\n\nconst showRefreshHint = ref({\n    nativeTitleBar: false,\n    lyricsBackground: false,\n    lyricsFontSize: false,\n    lyricsAlign: false,\n    gpuAcceleration: false,\n    highDpi: false,\n    font: false,\n    touchBar: false,\n    preventAppSuspension: false,\n    networkMode: false,\n    apiMode: false,\n    apiBaseUrlMode: false,\n    proxy: false,\n    dataSource: false,\n    statusBarLyrics: false,\n});\n\nconst audioOutputDeviceOptions = ref([]);\nconst audioOutputDevicesLoading = ref(false);\n\nconst updateAudioOutputDeviceDisplayText = async (deviceId) => {\n    if (!deviceId || deviceId === 'default') {\n        selectedSettings.value.audioOutputDevice = { displayText: '默认', value: 'default' };\n        return;\n    }\n\n    let displayText = `已选择设备 (${deviceId.slice(0, 8)}...)`;\n    try {\n        if (navigator?.mediaDevices?.enumerateDevices) {\n            const devices = await navigator.mediaDevices.enumerateDevices();\n            const matched = devices.find(d => d.kind === 'audiooutput' && d.deviceId === deviceId);\n            if (matched?.label) displayText = matched.label;\n        }\n    } catch {\n        // 忽略枚举失败\n    }\n\n    selectedSettings.value.audioOutputDevice = { displayText, value: deviceId };\n};\n\nconst loadAudioOutputDevices = async () => {\n    if (typeof navigator === 'undefined' || !navigator.mediaDevices?.enumerateDevices) {\n        audioOutputDeviceOptions.value = [];\n        return;\n    }\n\n    audioOutputDevicesLoading.value = true;\n\n    try {\n        const devices = await navigator.mediaDevices.enumerateDevices();\n        const outputs = devices.filter(d => d.kind === 'audiooutput');\n\n        const options = [{ displayText: '默认', value: 'default' }];\n        let unnamedIndex = 1;\n\n        for (const output of outputs) {\n            if (!output.deviceId) continue;\n            const displayText = output.label || `输出设备 ${unnamedIndex++}`;\n            options.push({ displayText, value: output.deviceId });\n        }\n\n        const seen = new Set();\n        audioOutputDeviceOptions.value = options.filter(opt => {\n            if (seen.has(opt.value)) return false;\n            seen.add(opt.value);\n            return true;\n        });\n    } catch {\n        audioOutputDeviceOptions.value = [{ displayText: '默认', value: 'default' }];\n    } finally {\n        audioOutputDevicesLoading.value = false;\n    }\n};\n\nconst openSelection = (type, helpLink) => {\n    isSelectionOpen.value = true;\n    selectionType.value = type;\n    currentHelpLink.value = helpLink || selectionTypeMap[type]?.helpLink || '';\n\n    if (type === 'highDpi') {\n        dpiScale.value = parseFloat(selectedSettings.value.dpiScale?.value || '1.0');\n    }\n\n    if (type === 'font') {\n        fontUrlInput.value = selectedSettings.value.fontUrl?.value || '';\n        fontFamilyInput.value = selectedSettings.value.font?.value || '';\n    }\n    \n    if (type === 'proxy') {\n        proxyForm.url = selectedSettings.value.proxyUrl?.value || '';\n    }\n\n    if (type === 'apiBaseUrlMode') {\n        apiBaseUrlForm.url = selectedSettings.value.apiBaseUrl?.value || '';\n        apiBaseUrlForm.testResult = '';\n        apiBaseUrlForm.testStatus = '';\n    }\n\n    if (type === 'audioOutputDevice') {\n        void loadAudioOutputDevices();\n    }\n};\n\nconst openHelpLink = () => {\n    const url = currentHelpLink.value;\n    if (!url) return;\n    if (isElectron()) {\n        window.electron.ipcRenderer.send('open-url', url);\n    } else {\n        window.open(url, '_blank');\n    }\n};\n\n    const selectOption = async (option) => {\n    const electronFeatures = ['desktopLyrics', 'statusBarLyrics', 'gpuAcceleration', 'minimizeToTray', 'highDpi', 'nativeTitleBar', 'touchBar', 'autoStart', 'startMinimized', 'preventAppSuspension', 'networkMode', 'poxySettings', 'apiMode', 'dataSource', 'statusBarLyrics'];\n    if (!isElectron() && electronFeatures.includes(selectionType.value)) {\n        window.$modal.alert(t('fei-ke-hu-duan-huan-jing-wu-fa-qi-yong'));\n        return;\n    }\n    if(selectionType.value == 'touchBar' && window.electron.platform != 'darwin'){\n        window.$modal.alert(t('fei-mac-bu-zhi-chi-touchbar'));\n        return;\n    }\n    if(selectionType.value == 'statusBarLyrics' && window.electron.platform != 'darwin'){\n        window.$modal.alert(t('zhuang-tai-lan-ge-ci-jin-zhi-chi-mac'));\n        return;\n    }\n    selectedSettings.value[selectionType.value] = option;\n    const actions = {\n        'themeColor': () => proxy.$applyColorTheme(option.value),\n        'theme': () => proxy.$setTheme(option.value),\n        'language': () => {\n            proxy.$i18n.locale = option.value;\n            document.documentElement.lang = option.value;\n        },\n        'quality': () => {\n            if (!MoeAuth.isAuthenticated) {\n                window.$modal.alert(t('gao-pin-zhi-yin-le-xu-yao-deng-lu-hou-cai-neng-bo-fango'));\n                return;\n            }\n        },\n        'highDpi': () => {\n            selectedSettings.value.dpiScale = {\n                value: dpiScale.value.toString(),\n                displayText: dpiScale.value.toString()\n            };\n        },\n        'desktopLyrics': () => {\n            const action = option.value === 'on' ? 'display-lyrics' : 'close-lyrics';\n            window.electron.ipcRenderer.send('desktop-lyrics-action', action);\n        },\n        'loudnessNormalization': () => {\n            // 触发响度规格化开关变更事件\n            window.dispatchEvent(new CustomEvent('loudness-normalization-change', {\n                detail: { enabled: option.value === 'on' }\n            }));\n        },\n        'pauseOnAudioOutputChange': async () => {\n            if (option.value === 'on') {\n                const granted = await requestMicrophonePermission();\n                if (!granted) {\n                    selectedSettings.value.pauseOnAudioOutputChange = {\n                        displayText: t('guan-bi'),\n                        value: 'off'\n                    };\n                    window.dispatchEvent(new CustomEvent('audio-output-device-watch-change', {\n                        detail: { enabled: false }\n                    }));\n                    window.$modal.alert('音频权限申请失败，无法启用该功能');\n                    return;\n                }\n            }\n\n            window.dispatchEvent(new CustomEvent('audio-output-device-watch-change', {\n                detail: { enabled: option.value === 'on' }\n            }));\n        },\n        'apiBaseUrlMode': () => {\n            if (option.value === 'default') {\n                selectedSettings.value.apiBaseUrl = { displayText: '', value: '' };\n            }\n        },\n        'audioOutputDevice': async () => {\n            window.dispatchEvent(new CustomEvent('audio-output-device-change', {\n                detail: { deviceId: option.value }\n            }));\n        }\n    };\n    await actions[selectionType.value]?.();\n    saveSettings();\n    if(!['apiMode','font','fontUrl', 'proxy', 'apiBaseUrlMode'].includes(selectionType.value)) closeSelection();\n    const refreshHintTypes = ['nativeTitleBar','lyricsBackground', 'lyricsFontSize', 'gpuAcceleration', 'highDpi', 'apiMode', 'apiBaseUrlMode', 'touchBar', 'preventAppSuspension', 'networkMode', 'font', 'proxy', 'dataSource', 'loudnessNormalization', 'statusBarLyrics'];\n    if (refreshHintTypes.includes(selectionType.value)) {\n        showRefreshHint.value[selectionType.value] = true;\n    }\n};\n\nconst updateFontSetting = async (key) => {\n    const prevType = selectionType.value;\n    const value = key === 'font' ? (fontFamilyInput.value || '') : (fontUrlInput.value || '');\n    const displayText = key === 'font' ? (value || t('mo-ren-zi-ti')) : (value || t('mo-ren-zi-ti'));\n    selectionType.value = key;\n    await selectOption({ displayText, value });\n    selectionType.value = prevType;\n};\n\nconst handleFontFocusOut = async (e) => {\n    const container = e.currentTarget;\n    if (container && e.relatedTarget && container.contains(e.relatedTarget)) return;\n    await updateFontSetting('fontUrl');\n    await updateFontSetting('font');\n};\n\nconst isElectron = () => {\n    return typeof window !== 'undefined' && typeof window.electron !== 'undefined';\n};\nconst saveSettings = () => {\n    const settingsToSave = Object.fromEntries(\n        Object.entries(selectedSettings.value).map(([key, setting]) => [key, setting.value])\n    );\n    settingsToSave.shortcuts = shortcuts.value;\n    localStorage.setItem('settings', JSON.stringify(settingsToSave));\n    isElectron() && window.electron.ipcRenderer.send('save-settings', JSON.parse(JSON.stringify(settingsToSave)));\n};\n\nconst closeSelection = () => {\n    isSelectionOpen.value = false;\n};\n\nonMounted(() => {\n    const savedSettings = JSON.parse(localStorage.getItem('settings'));\n    \n    if (savedSettings) {\n        if (savedSettings.apiBaseUrlMode === undefined) {\n            const legacyUrl = savedSettings.apiBaseUrl || '';\n            savedSettings.apiBaseUrlMode = legacyUrl ? 'custom' : 'default';\n        }\n        for (const key in savedSettings) {\n            if (key === 'shortcuts') continue;\n            if (key === 'audioOutputDevice') continue;\n            if (key === 'quality') {\n                const option = selectionTypeMap[key].options.find(option => option.value === savedSettings[key]) || selectionTypeMap[key].options[0];\n                selectedSettings.value[key] = { ...option };\n                continue;\n            }\n            if (key === 'apiBaseUrlMode') {\n                const value = savedSettings[key] || 'default';\n                selectedSettings.value[key] = {\n                    displayText: value === 'custom' ? '自定义' : '默认',\n                    value: value\n                };\n                continue;\n            }\n            if (key === 'apiBaseUrl') {\n                const value = savedSettings[key] || '';\n                selectedSettings.value[key] = { displayText: '', value: value };\n                continue;\n            }\n            if (key === 'proxyUrl') {\n                const value = savedSettings[key];\n                selectedSettings.value[key] = {\n                    displayText: value,\n                    value: value\n                };\n                continue;\n            }\n            if (selectionTypeMap[key] && selectionTypeMap[key].options) {\n                if (key === 'font') {\n                    const value = savedSettings[key];\n                    selectedSettings.value[key] = {\n                        displayText: value || t('mo-ren-zi-ti'),\n                        value: value\n                    };\n                } else {\n                    // Always get displayText from current translation, not from localStorage\n                    const option = selectionTypeMap[key].options.find(\n                        (opt) => opt.value === savedSettings[key]\n                    );\n                    const displayText = option?.displayText || '🌏 ' + t('zi-dong');\n                    selectedSettings.value[key] = { displayText, value: savedSettings[key] };\n                }\n            }\n        }\n    }\n    if (savedSettings?.shortcuts) {\n        shortcuts.value = savedSettings.shortcuts;\n    } else {\n        shortcuts.value = Object.entries(shortcutConfigs.value).reduce((acc, [key, config]) => {\n            acc[key] = config.defaultValue;\n            return acc;\n        }, {});\n    }\n    if(isElectron()){\n        appVersion.value = localStorage.getItem('version');\n        platform.value = window.electron.platform;\n    }\n\n    if (savedSettings?.audioOutputDevice !== undefined) {\n        void updateAudioOutputDeviceDisplayText(savedSettings.audioOutputDevice);\n    }\n});\n\nconst showShortcutModal = ref(false);\nconst recordingKey = ref('');\nconst shortcuts = ref({});\nconst proxyForm = reactive({url: '', testing: false, testResult: '', testStatus: '' });\nconst apiBaseUrlForm = reactive({ url: '', testing: false, testResult: '', testStatus: '' });\n\nconst testApiBaseUrl = async () => {\n    const validation = validateApiBaseUrl(apiBaseUrlForm.url);\n    if (!validation.ok) {\n        apiBaseUrlForm.testResult = validation.error;\n        apiBaseUrlForm.testStatus = 'error';\n        return;\n    }\n\n    const candidate = validation.value || defaultApiBaseUrl;\n    apiBaseUrlForm.testing = true;\n    apiBaseUrlForm.testResult = t('zheng-zai-ce-shi');\n    apiBaseUrlForm.testStatus = 'testing';\n\n    const result = await testApiBaseUrlRequest(candidate, { path: '/register/dev' });\n    apiBaseUrlForm.testing = false;\n\n    if (result.ok) {\n        apiBaseUrlForm.testResult = '连接成功';\n        apiBaseUrlForm.testStatus = 'success';\n    } else if (result.error === 'timeout') {\n        apiBaseUrlForm.testResult = t('lian-jie-chao-shi');\n        apiBaseUrlForm.testStatus = 'error';\n    } else if (result.error === 'no_dfid') {\n        apiBaseUrlForm.testResult = 'RPC端点协议不符合';\n        apiBaseUrlForm.testStatus = 'error';\n    } else if (typeof result.status === 'number') {\n        apiBaseUrlForm.testResult = `连接失败：${result.status} ${result.statusText || ''}`.trim();\n        apiBaseUrlForm.testStatus = 'error';\n    } else {\n        apiBaseUrlForm.testResult = `连接错误：${result.error || ''}`.trim();\n        apiBaseUrlForm.testStatus = 'error';\n    }\n};\n\nconst saveApiBaseUrl = () => {\n    const validation = validateApiBaseUrl(apiBaseUrlForm.url);\n    if (!validation.ok) {\n        window.$modal.alert(validation.error);\n        return;\n    }\n\n    const value = validation.value;\n    if (!value) {\n        selectedSettings.value.apiBaseUrlMode = { displayText: '默认', value: 'default' };\n        selectedSettings.value.apiBaseUrl = { displayText: '', value: '' };\n    } else {\n        selectedSettings.value.apiBaseUrlMode = { displayText: '自定义', value: 'custom' };\n        selectedSettings.value.apiBaseUrl = { displayText: '', value: value };\n    }\n    saveSettings();\n    showRefreshHint.value.apiBaseUrlMode = true;\n    closeSelection();\n};\n\nconst testProxyConnection = async () => {\n    const proxyUrl = proxyForm.url.trim();\n    if (!proxyUrl) {\n        proxyForm.testResult = t('qing-shu-ru-dai-li-di-zhi');\n        proxyForm.testStatus = 'error';\n        return;\n    }\n\n    try {\n        const url = new URL(proxyUrl);\n        if (!['http:', 'https:'].includes(url.protocol)) {\n            proxyForm.testResult = t('zhi-chi-http-https-dai-li');\n            proxyForm.testStatus = 'error';\n            return;\n        }\n    } catch (e) {\n        proxyForm.testResult = t('qing-shu-ru-you-xiao-de-url');\n        proxyForm.testStatus = 'error';\n        return;\n    }\n\n    proxyForm.testing = true;\n    proxyForm.testResult = t('zheng-zai-ce-shi');\n    proxyForm.testStatus = 'testing';\n\n    try {\n        const controller = new AbortController();\n        const timeoutId = setTimeout(() => controller.abort(), 10000);\n\n        const proxyUrl = new URL(proxyForm.url.trim());\n        const fetchOptions = {\n            method: 'GET',\n            headers: {\n                'Accept': 'application/json',\n                'Proxy-Authorization': `Basic ${btoa(`${proxyUrl.username || ''}:${proxyUrl.password || ''}`)}`,\n            },\n            signal: controller.signal,\n            agent: {\n                protocol: proxyUrl.protocol,\n                host: proxyUrl.hostname,\n                port: proxyUrl.port,\n                auth: proxyUrl.username && proxyUrl.password ? \n                    `${proxyUrl.username}:${proxyUrl.password}` : undefined\n            }\n        };\n\n        const response = await fetch('https://api.ipify.org?format=json', fetchOptions);\n        clearTimeout(timeoutId);\n\n        if (response.ok) {\n            const data = await response.json();\n            proxyForm.testResult = t('dai-li-lian-jie-cheng-gong') + data.ip;\n            proxyForm.testStatus = 'success';\n        } else {\n            proxyForm.testResult = t('dai-li-lian-jie-shi-bai') + response.statusText;\n            proxyForm.testStatus = 'error';\n        }\n    } catch (error) {\n        if (error.name === 'AbortError') {\n            proxyForm.testResult = t('lian-jie-chao-shi');\n        } else {\n            proxyForm.testResult = t('lian-jie-cuo-wu') + error.message;\n        }\n        proxyForm.testStatus = 'error';\n    } finally {\n        proxyForm.testing = false;\n    }\n};\n\nconst saveProxy = () => {\n    const proxyUrl = proxyForm.url.trim();\n\n    try {\n        if (proxyUrl) {\n            const url = new URL(proxyUrl);\n            if (!['http:', 'https:'].includes(url.protocol)) {\n                window.$modal.alert(t('zhi-chi-http-https-dai-li'));\n                return;\n            }\n        }\n    } catch (e) {\n        window.$modal.alert(t('qing-shu-ru-you-xiao-de-url'));\n        return;\n    }\n\n    // 更新代理状态\n    selectedSettings.value.proxy = {\n        displayText: proxyUrl ? t('qi-yong') : t('jin-yong'),\n        value: proxyUrl ? 'on' : 'off'\n    };\n    \n    // 更新代理地址\n    selectedSettings.value.proxyUrl = {\n        displayText: proxyUrl,\n        value: proxyUrl\n    };\n\n    saveSettings();\n    closeSelection();\n};\n\nconst shortcutConfigs = ref({\n    mainWindow: {\n        label: t('xian-shi-yin-cang-zhu-chuang-kou'),\n        defaultValue: 'Ctrl+Shift+S'\n    },\n    quitApp: {\n        label: t('tui-chu-zhu-cheng-xu'),\n        defaultValue: 'Ctrl+Q'\n    },\n    prevTrack: {\n        label: t('shang-yi-shou'),\n        defaultValue: 'Alt+Ctrl+Left'\n    },\n    nextTrack: {\n        label: t('xia-yi-shou'),\n        defaultValue: 'Alt+Ctrl+Right'\n    },\n    playPause: {\n        label: t('zan-ting-bo-fang'),\n        defaultValue: 'Alt+Ctrl+Space'\n    },\n    volumeUp: {\n        label: t('yin-liang-zeng-jia'),\n        defaultValue: 'Alt+Ctrl+Up'\n    },\n    volumeDown: {\n        label: t('yin-liang-jian-xiao'),\n        defaultValue: 'Alt+Ctrl+Down'\n    },\n    mute: {\n        label: t('jing-yin'),\n        defaultValue: 'Alt+Ctrl+M'\n    },\n    like: {\n        label: t('tian-jia-wo-xi-huan'),\n        defaultValue: 'Alt+Ctrl+L'\n    },\n    mode: {\n        label: t('qie-huan-bo-fang-mo-shi'),\n        defaultValue: 'Alt+Ctrl+P'\n    },\n    toggleDesktopLyrics: {\n        label: t('xian-shi-yin-cang-zhuo-mian-ge-ci'),\n        defaultValue: 'Alt+Ctrl+D'\n    }\n});\n\nconst openShortcutSettings = () => {\n    showShortcutModal.value = true;\n};\n\nconst closeShortcutSettings = () => {\n    showShortcutModal.value = false;\n    recordingKey.value = '';\n};\n\nconst startRecording = (key) => {\n    recordingKey.value = key;\n    shortcuts.value[key] = t('qing-an-xia-xiu-shi-jian');\n    window.addEventListener('keydown', recordShortcut);\n};\n\nconst recordShortcut = (e) => {\n    if (!recordingKey.value) return;\n\n    e.preventDefault();\n    const keys = [];\n\n    // 修饰键\n    if (e.metaKey) keys.push('CommandOrControl');\n    if (e.ctrlKey) keys.push('Ctrl');\n    if (e.altKey) keys.push('Alt');\n    if (e.shiftKey) keys.push('Shift');\n\n    // 如果按下了修饰键，更新提示\n    if (keys.length > 0 && ['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) {\n        shortcuts.value[recordingKey.value] = keys.join('+') + t('qing-an-xia-qi-ta-jian');\n        return;\n    }\n\n    // 特殊键映射\n    const specialKeys = {\n        ' ': 'Space',\n        'ArrowUp': 'Up',\n        'ArrowDown': 'Down',\n        'ArrowLeft': 'Left',\n        'ArrowRight': 'Right',\n        'Escape': 'Esc',\n        'Backspace': 'Backspace',\n        'Delete': 'Delete',\n        'Enter': 'Return',\n        'Tab': 'Tab',\n        'PageUp': 'PageUp',\n        'PageDown': 'PageDown',\n        'Home': 'Home',\n        'End': 'End',\n        '+': 'numadd',\n        '-': 'numsub',\n        '*': 'nummult',\n        '/': 'numdiv',\n        '=': 'Equal',\n        '.': 'Dot',\n        'NumpadDecimal': 'numdec'\n    };\n\n    const key = specialKeys[e.key] || e.key.toUpperCase();\n\n    // 只有当按下的不是单独的修饰键时才结束记录\n    if (!['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) {\n        keys.push(key);\n\n        if (keys.length > 0) {\n            // 检查是否包含必要的修饰键\n            if (!keys.some(k => ['Ctrl', 'Alt', 'Shift', 'CommandOrControl'].includes(k))) {\n                window.$modal.alert(t('kuai-jie-jian-bi-xu-bao-han-zhi-shao-yi-ge-xiu-shi-jian-ctrlaltshiftcommand'));\n                return;\n            }\n\n            // 检查快捷键冲突\n            const newShortcut = keys.join('+');\n            const conflictKey = Object.entries(shortcuts.value).find(([k, v]) =>\n                v === newShortcut && k !== recordingKey.value\n            );\n\n            if (conflictKey) {\n                window.$modal.alert(t('gai-kuai-jie-jian-yu')+conflictKey[0]+t('de-kuai-jie-jian-chong-tu'));\n                return;\n            }\n\n            shortcuts.value[recordingKey.value] = newShortcut;\n            recordingKey.value = '';\n            window.removeEventListener('keydown', recordShortcut);\n        }\n    }\n};\n\n// 添加快捷键验证函数\nconst validateShortcut = (shortcut) => {\n    const keys = shortcut.split('+');\n    return keys.some(k => ['Ctrl', 'Alt', 'Shift', 'Command'].includes(k));\n};\n\n// 修改 saveShortcuts 函数，添加检查\nconst saveShortcuts = () => {\n    if (!isElectron()) {\n        window.$modal.alert(t('fei-ke-hu-duan-huan-jing-wu-fa-qi-yong'));\n        return;\n    }\n\n    // 验证所有快捷键\n    const invalidShortcuts = Object.entries(shortcuts.value).filter(([key, value]) =>\n        value && !validateShortcut(value)\n    );\n\n    if (invalidShortcuts.length > 0) {\n        window.$modal.alert(t('cun-zai-wu-xiao-de-kuai-jie-jian-she-zhi-qing-que-bao-mei-ge-kuai-jie-jian-du-bao-han-xiu-shi-jian'));\n        return;\n    }\n\n    try {\n        let settingsToSave = JSON.parse(localStorage.getItem('settings')) || {};\n        settingsToSave.shortcuts = shortcuts.value;\n        localStorage.setItem('settings', JSON.stringify(settingsToSave));\n        window.electron.ipcRenderer.send('save-settings',  JSON.parse(JSON.stringify(settingsToSave)));\n        window.electron.ipcRenderer.send('custom-shortcut');\n    } catch (error) {\n        console.error('保存设置失败:', error);\n        window.$modal.alert(t('bao-cun-she-zhi-shi-bai'));\n    }\n\n    closeShortcutSettings();\n};\n\nonUnmounted(() => {\n    window.removeEventListener('keydown', recordShortcut);\n});\n\nconst clearShortcut = (key) => {\n    shortcuts.value[key] = '';\n};\n\nconst dpiScale = ref(1.0);\n\nconst openResetConfirmation = async () => {\n    const result = await window.$modal.confirm(t('ni-que-ren-hui-fu-chu-chang'));\n    if(result){\n        localStorage.clear();\n        isElectron() && window.electron.ipcRenderer.send('clear-settings');\n        window.$modal.alert(t('hui-fu-chu-chang-she-zhi-cheng-gong'));\n    }\n};\n\nlet deferredPrompt;\nif(!isElectron()){\n    window.addEventListener('beforeinstallprompt', (e) => {\n        e.preventDefault();\n        deferredPrompt = e;\n    });\n}\n\nconst installPWA = async () => {\n    if(isElectron()){\n        window.$modal.alert(t('qing-zai-web-huan-jing-xia-an-zhuang'));\n        return;\n    }\n    deferredPrompt.prompt();\n    const { outcome } = await deferredPrompt.userChoice;\n    \n    if (outcome === 'accepted') {\n        console.log('User accepted the PWA installation');\n        deferredPrompt = null;\n    } else {\n        console.log('User declined the PWA installation');\n    }\n};\n</script>\n\n<style scoped>\n.settings-page {\n    display: flex;\n    height: 100vh;\n    overflow: hidden;\n    box-shadow: 0 0 30px rgba(0, 0, 0, 0.15);\n    border-radius: 8px;\n    margin-bottom: -80px;\n}\n\n.settings-sidebar {\n    width: 220px;\n    box-shadow: 0 0 10px rgba(0, 0, 0, 0.15);\n    padding: 20px 0;\n    overflow-y: auto;\n}\n\n.sidebar-item {\n    padding: 12px 20px;\n    margin: 4px 10px;\n    border-radius: 8px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    transition: all 0.2s ease;\n}\n\n.sidebar-item i {\n    margin-right: 12px;\n    font-size: 16px;\n    width: 20px;\n    text-align: center;\n}\n\n.sidebar-item.active {\n    background-color: var(--color-primary-light, rgba(255, 105, 180, 0.1));\n    color: var(--color-primary, #ff69b4);\n    font-weight: 500;\n}\n\n.sidebar-item:hover:not(.active) {\n    background-color: var(--hover-color, #efefef);\n}\n\n.settings-content {\n    flex: 1;\n    padding: 20px;\n    overflow-y: auto;\n}\n\n.setting-section {\n    animation: fadeIn 0.3s ease;\n}\n\n.setting-section h3 {\n    font-size: 22px;\n    font-weight: 600;\n    margin-bottom: 20px;\n    padding-bottom: 10px;\n    border-bottom: 1px solid var(--border-color, #eaeaea);\n}\n\n.settings-cards {\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n    gap: 16px;\n}\n\n.setting-card {\n    border-radius: 12px;\n    padding: 16px;\n    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);\n    transition: all 0.2s ease;\n    cursor: pointer;\n}\n\n.setting-card:hover {\n    transform: translateY(-2px);\n    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);\n}\n\n.setting-card-header {\n    display: flex;\n    align-items: center;\n    margin-bottom: 12px;\n}\n\n.setting-card-header i {\n    color: var(--color-primary, #ff69b4);\n    margin-right: 10px;\n    font-size: 16px;\n}\n\n.setting-card-value {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 8px 12px;\n    border-radius: 6px;\n    font-size: 14px;\n    border: 1px solid var(--border-color, #eaeaea);\n}\n\n.setting-card-value i {\n    color: #999;\n    font-size: 12px;\n}\n\n.refresh-hint {\n    color: #ff4d4f;\n    font-size: 12px;\n    margin-left: 8px;\n}\n\n.modal {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0, 0, 0, 0.6);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    animation: fadeIn 0.3s ease-in-out;\n    z-index: 9;\n}\n\n.modal-content {\n    background: white;\n    padding: 25px;\n    border-radius: 12px;\n    width: 90%;\n    max-width: 400px;\n    text-align: center;\n    box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);\n    animation: slideIn 0.3s ease-in-out;\n    position: relative;\n}\n\n.modal-content h3 {\n    font-size: 20px;\n    margin-bottom: 20px;\n    color: #333;\n}\n\n.modal-content ul {\n    list-style: none;\n    padding: 0;\n    margin: 0;\n}\n\n.modal-content li {\n    padding: 12px;\n    margin: 6px 0;\n    background-color: var(--background-color);\n    border-radius: 8px;\n    cursor: pointer;\n    transition: background-color 0.2s;\n}\n\n.modal-content li:hover {\n    background-color:var(--secondary-color);\n}\n\n.modal-content button {\n    margin-top: 20px;\n    padding: 10px 20px;\n    background-color: var(--color-primary);\n    color: white;\n    border: none;\n    border-radius: 8px;\n    cursor: pointer;\n    font-size: 16px;\n    transition: background-color 0.3s;\n}\n\n.modal-content button:hover {\n    background-color: var(--color-primary)\n}\n\n.help-link {\n    position: absolute;\n    top: 12px;\n    right: 12px;\n    color: var(--color-primary);\n    cursor: pointer;\n    text-decoration: none;\n    font-size: 18px;\n}\n\n.help-link:hover {\n    opacity: 0.85;\n}\n\n@keyframes fadeIn {\n    from { opacity: 0; }\n    to { opacity: 1; }\n}\n\n@keyframes slideIn {\n    from { transform: translateY(-20px); }\n    to { transform: translateY(0); }\n}\n\n.shortcut-modal {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0, 0, 0, 0.6);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    z-index: 1000;\n}\n\n.shortcut-modal-content {\n    background: white;\n    border-radius: 12px;\n    padding: 20px;\n    width: 90%;\n    max-width: 500px;\n}\n\n.shortcut-modal-content h3 {\n    margin: 0 0 20px 0;\n    font-size: 18px;\n    text-align: center;\n}\n\n.shortcut-list {\n    margin-bottom: 20px;\n    max-height: 60vh;\n    overflow-y: auto;\n}\n\n.shortcut-item {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 10px 0;\n    border-bottom: 1px solid #eee;\n}\n\n.shortcut-input {\n    position: relative;\n    background: #f5f5f5;\n    padding: 8px 16px;\n    border-radius: 6px;\n    cursor: pointer;\n    min-width: 150px;\n    text-align: center;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: 8px;\n    font-size: 15px;\n}\n\n.shortcut-input.recording {\n    background: var(--color-primary);\n    color: white;\n}\n\n.shortcut-input.recording .clear-shortcut {\n    background: rgba(255, 255, 255, 0.2);\n    color: white;\n}\n\n.shortcut-input.recording .clear-shortcut:hover {\n    background: rgba(255, 255, 255, 0.3);\n    color: white;\n}\n\n.clear-shortcut {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 15px;\n    height: 15px;\n    border-radius: 50%;\n    background: rgba(0, 0, 0, 0.1);\n    cursor: pointer;\n    font-size: 14px;\n    color: #666;\n    transition: all 0.2s;\n    position: absolute;\n    right: 5px;\n}\n\n.shortcut-modal-footer {\n    display: flex;\n    justify-content: flex-end;\n    gap: 12px;\n    margin-top: 20px;\n}\n\n.shortcut-modal-footer button {\n    padding: 8px 20px;\n    border-radius: 6px;\n    border: none;\n    cursor: pointer;\n}\n\n.shortcut-modal-footer button.primary {\n    background: var(--color-primary);\n    color: white;\n}\n\n.version-info {\n    text-align: center;\n    margin-top: 20px;\n    font-size: 14px;\n    color: #666;\n}\n\n.reset-settings-container {\n    display: flex;\n    justify-content: center;\n    margin: 30px 0 20px 0;\n}\n\n.reset-settings-button {\n    background-color: #f44336;\n    color: white;\n    border: none;\n    border-radius: 8px;\n    padding: 10px 20px;\n    font-size: 14px;\n    cursor: pointer;\n    transition: background-color 0.3s;\n    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n    display: flex;\n    align-items: center;\n    gap: 8px;\n}\n\n.reset-settings-button:hover {\n    background-color: #e53935;\n}\n\n.scale-slider-container {\n    margin-top: 15px;\n    text-align: left;\n    padding: 15px;\n    background-color: var(--background-color);\n    border-radius: 8px;\n}\n\n.scale-slider-label {\n    font-weight: bold;\n    margin-bottom: 10px;\n}\n\n.scale-slider-hint {\n    font-size: 12px;\n    color: #666;\n}\n\n.scale-slider-wrapper {\n    position: relative;\n    padding-bottom: 20px;\n}\n\n.scale-slider {\n    width: 100%;\n    height: 6px;\n    -webkit-appearance: none;\n    appearance: none;\n    background: #ddd;\n    outline: none;\n    border-radius: 3px;\n}\n\n.scale-slider::-webkit-slider-thumb {\n    -webkit-appearance: none;\n    appearance: none;\n    width: 18px;\n    height: 18px;\n    border-radius: 50%;\n    background: var(--color-primary);\n    cursor: pointer;\n}\n\n.scale-slider::-moz-range-thumb {\n    width: 18px;\n    height: 18px;\n    border-radius: 50%;\n    background: var(--color-primary);\n    cursor: pointer;\n    border: none;\n}\n\n.scale-marks {\n    display: flex;\n    justify-content: space-between;\n    margin-top: 5px;\n    font-size: 12px;\n    color: #666;\n}\n\n.api-settings-container {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n}\n\n.api-settings-container .api-setting-item {\n    display: flex;\n    flex-direction: column;\n    align-items: flex-start;\n    margin-bottom: 10px;\n    width: 100%;\n}\n\n.api-settings-container .api-setting-item label {\n    font-size: 14px;\n    color: #333;\n    margin-bottom: 5px;\n}\n\n.api-settings-container .api-setting-item .api-input, .proxy-settings-container .api-input {\n    width: 100%;\n    height: 35px;\n    border: 1px solid #ccc;\n    border-radius: 5px;\n    padding: 5px;\n    padding-left: 10px;\n    box-sizing: border-box;\n}\n\n.api-settings-container .api-hint {\n    font-size: 12px;\n    color: #999;\n    text-align: center;\n}\n\n.proxy-actions {\n    display: flex;\n    gap: 12px;\n    width: 100%;\n}\n\n.proxy-actions button {\n    flex: 1;\n    min-width: 0;\n    padding: 8px 0;\n    border-radius: 6px;\n}\n\n.proxy-test-result {\n    font-size: 13px;\n    line-height: 18px;\n    margin-top: 5px;\n}\n\n.proxy-test-result.success {\n    color: #4caf50;\n}\n\n.proxy-test-result.error {\n    color: #e53935;\n}\n</style>\n\n"
  },
  {
    "path": "src/views/VideoPlayer.vue",
    "content": "<template>\n    <div class=\"video-player-page\">\n        <div class=\"video-container\">\n            <div class=\"video-header\">\n                <button class=\"back-btn\" @click=\"() => isElectron? closeWindow(): goBack()\">\n                    <i class=\"fas\" :class=\"{ 'fa-xmark': isElectron, 'fa-arrow-left': !isElectron }\"></i>\n                    {{ isElectron? '关闭': '返回' }}\n                </button>\n                <h2 class=\"video-title\">{{ videoTitle }}</h2>\n            </div>\n\n            <div class=\"video-wrapper\" v-if=\"videoUrl\">\n                <video\n                    ref=\"videoPlayer\"\n                    class=\"video-element\"\n                    :src=\"videoUrl\"\n                    controls\n                    autoplay\n                    @error=\"handleVideoError\"\n                    @loadedmetadata=\"handleVideoLoaded\"\n                >\n                    您的浏览器不支持视频播放\n                </video>\n            </div>\n\n            <div class=\"loading-container\" v-else-if=\"loading\">\n                <i class=\"fas fa-spinner fa-spin\"></i>\n                <p>正在加载视频...</p>\n            </div>\n\n            <div class=\"error-container\" v-else-if=\"error\">\n                <i class=\"fas fa-exclamation-circle\"></i>\n                <p>{{ error }}</p>\n                <button class=\"retry-btn\" @click=\"retryLoad\">\n                    <i class=\"fas fa-redo\"></i> 重试\n                </button>\n            </div>\n        </div>\n    </div>\n</template>\n\n<script setup>\nimport { ref, onMounted, onBeforeUnmount } from 'vue';\nimport { useRoute, useRouter } from 'vue-router';\nimport { get } from '../utils/request';\n\nconst route = useRoute();\nconst router = useRouter();\n\nconst videoPlayer = ref(null);\nconst videoUrl = ref('');\nconst videoTitle = ref('视频播放');\nconst loading = ref(true);\nconst error = ref('');\n\nconst isElectron = !!window.electron;\n\nconst mvhash = ref(route.query.hash || '');\n\nconst fetchVideoUrl = async () => {\n    if (!mvhash.value) {\n        error.value = '缺少视频参数';\n        loading.value = false;\n        return;\n    }\n\n    try {\n        loading.value = true;\n        error.value = '';\n\n        const videoResponse = await get('/video/url', {\n            hash: mvhash.value\n        });\n\n        if (videoResponse.status === 1 && videoResponse.data) {\n            // 获取第一个键（动态的hash值）\n            const videoKey = Object.keys(videoResponse.data)[0];\n            const videoData = videoResponse.data[videoKey];\n\n            if (videoData && (videoData.backupdownurl || videoData.downurl)) {\n                // 优先使用 backupdownurl，如果是数组则取第一个\n                const backupUrl = Array.isArray(videoData.backupdownurl)\n                    ? videoData.backupdownurl[0]\n                    : videoData.backupdownurl;\n\n                videoUrl.value = backupUrl || videoData.downurl;\n\n                if (route.query.title) {\n                    videoTitle.value = decodeURIComponent(route.query.title);\n                }\n            } else {\n                error.value = '获取视频播放地址失败';\n            }\n        } else {\n            error.value = '获取视频信息失败';\n        }\n    } catch (err) {\n        error.value = '加载视频失败，请稍后重试';\n    } finally {\n        loading.value = false;\n    }\n};\n\nconst retryLoad = () => {\n    fetchVideoUrl();\n};\n\nconst handleVideoLoaded = () => {\n    console.log('视频加载完成');\n};\n\nconst handleVideoError = (e) => {\n    error.value = '视频播放失败，可能是视频格式不支持或网络问题';\n};\n\nconst goBack = () => {\n    router.back();\n};\n\nconst closeWindow = () => {\n    window.close();\n};\n\nonMounted(() => {\n    fetchVideoUrl();\n});\n\nonBeforeUnmount(() => {\n    if (videoPlayer.value) {\n        videoPlayer.value.pause();\n        videoPlayer.value.src = '';\n    }\n});\n</script>\n\n<style scoped>\n.video-player-page {\n    width: 100%;\n    height: 100vh;\n    background-color: #000;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.video-container {\n    width: 100%;\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n}\n\n.video-header {\n    background-color: rgba(0, 0, 0, 0.8);\n    padding: 15px 20px;\n    display: flex;\n    align-items: center;\n    gap: 20px;\n}\n\n.back-btn {\n    background: transparent;\n    border: none;\n    color: white;\n    font-size: 16px;\n    cursor: pointer;\n    padding: 8px 16px;\n    border-radius: 4px;\n    transition: background-color 0.3s;\n}\n\n.back-btn:hover {\n    background-color: rgba(255, 255, 255, 0.1);\n}\n\n.video-title {\n    color: white;\n    font-size: 18px;\n    margin: 0;\n    flex: 1;\n}\n\n.video-wrapper {\n    flex: 1;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background-color: #000;\n}\n\n.video-element {\n    width: 100%;\n    height: 100%;\n    max-height: 100%;\n    object-fit: contain;\n}\n\n.loading-container,\n.error-container {\n    flex: 1;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    color: white;\n    gap: 15px;\n}\n\n.loading-container i {\n    font-size: 48px;\n    color: #42b983;\n}\n\n.error-container i {\n    font-size: 48px;\n    color: #ff6b6b;\n}\n\n.loading-container p,\n.error-container p {\n    font-size: 16px;\n    margin: 0;\n}\n\n.retry-btn {\n    background-color: #42b983;\n    color: white;\n    border: none;\n    padding: 10px 20px;\n    border-radius: 4px;\n    font-size: 14px;\n    cursor: pointer;\n    transition: background-color 0.3s;\n}\n\n.retry-btn:hover {\n    background-color: #35a372;\n}\n\n.retry-btn i {\n    margin-right: 5px;\n}\n</style>\n"
  }
]