Repository: algerkong/AlgerMusicPlayer Branch: main Commit: cd1c09889f4c Files: 322 Total size: 1.6 MB Directory structure: gitextract_wrew8kq0/ ├── .github/ │ ├── CODEOWNERS │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.zh-CN.yml │ │ ├── config.yml │ │ └── feature-report.zh-CN.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ ├── issue-shoot.md │ └── workflows/ │ ├── build.yml │ └── deploy.yml ├── .gitignore ├── .husky/ │ ├── pre-commit │ └── pre-push ├── .prettierignore ├── CHANGELOG.md ├── DEV.md ├── LICENSE ├── README.md ├── android/ │ └── .gitignore ├── build/ │ ├── entitlements.mac.plist │ ├── icon.icns │ └── installer.nsh ├── dev-app-update.yml ├── docs/ │ └── custom-api-readme.md ├── electron.vite.config.ts ├── eslint.config.mjs ├── package.json ├── postcss.config.js ├── prettier.config.js ├── resources/ │ ├── html/ │ │ └── remote-control.html │ ├── icon.icns │ └── manifest.json ├── src/ │ ├── i18n/ │ │ ├── lang/ │ │ │ ├── en-US/ │ │ │ │ ├── artist.ts │ │ │ │ ├── bilibili.ts │ │ │ │ ├── common.ts │ │ │ │ ├── comp.ts │ │ │ │ ├── donation.ts │ │ │ │ ├── download.ts │ │ │ │ ├── favorite.ts │ │ │ │ ├── history.ts │ │ │ │ ├── login.ts │ │ │ │ ├── player.ts │ │ │ │ ├── search.ts │ │ │ │ ├── settings.ts │ │ │ │ ├── songItem.ts │ │ │ │ └── user.ts │ │ │ ├── ja-JP/ │ │ │ │ ├── artist.ts │ │ │ │ ├── bilibili.ts │ │ │ │ ├── common.ts │ │ │ │ ├── comp.ts │ │ │ │ ├── donation.ts │ │ │ │ ├── download.ts │ │ │ │ ├── favorite.ts │ │ │ │ ├── history.ts │ │ │ │ ├── login.ts │ │ │ │ ├── player.ts │ │ │ │ ├── search.ts │ │ │ │ ├── settings.ts │ │ │ │ ├── songItem.ts │ │ │ │ └── user.ts │ │ │ ├── ko-KR/ │ │ │ │ ├── artist.ts │ │ │ │ ├── bilibili.ts │ │ │ │ ├── common.ts │ │ │ │ ├── comp.ts │ │ │ │ ├── donation.ts │ │ │ │ ├── download.ts │ │ │ │ ├── favorite.ts │ │ │ │ ├── history.ts │ │ │ │ ├── login.ts │ │ │ │ ├── player.ts │ │ │ │ ├── search.ts │ │ │ │ ├── settings.ts │ │ │ │ ├── songItem.ts │ │ │ │ └── user.ts │ │ │ ├── zh-CN/ │ │ │ │ ├── artist.ts │ │ │ │ ├── bilibili.ts │ │ │ │ ├── common.ts │ │ │ │ ├── comp.ts │ │ │ │ ├── donation.ts │ │ │ │ ├── download.ts │ │ │ │ ├── favorite.ts │ │ │ │ ├── history.ts │ │ │ │ ├── login.ts │ │ │ │ ├── player.ts │ │ │ │ ├── search.ts │ │ │ │ ├── settings.ts │ │ │ │ ├── songItem.ts │ │ │ │ └── user.ts │ │ │ └── zh-Hant/ │ │ │ ├── artist.ts │ │ │ ├── bilibili.ts │ │ │ ├── common.ts │ │ │ ├── comp.ts │ │ │ ├── donation.ts │ │ │ ├── download.ts │ │ │ ├── favorite.ts │ │ │ ├── history.ts │ │ │ ├── login.ts │ │ │ ├── player.ts │ │ │ ├── search.ts │ │ │ ├── settings.ts │ │ │ ├── songItem.ts │ │ │ └── user.ts │ │ ├── languages.ts │ │ ├── main.ts │ │ ├── renderer.ts │ │ └── utils.ts │ ├── main/ │ │ ├── index.ts │ │ ├── lyric.ts │ │ ├── modules/ │ │ │ ├── cache.ts │ │ │ ├── config.ts │ │ │ ├── deviceInfo.ts │ │ │ ├── fileManager.ts │ │ │ ├── fonts.ts │ │ │ ├── loginWindow.ts │ │ │ ├── lxMusicHttp.ts │ │ │ ├── otherApi.ts │ │ │ ├── remoteControl.ts │ │ │ ├── shortcuts.ts │ │ │ ├── tray.ts │ │ │ ├── update.ts │ │ │ ├── window-size.ts │ │ │ └── window.ts │ │ ├── server.ts │ │ ├── set.json │ │ └── unblockMusic.ts │ ├── preload/ │ │ ├── index.d.ts │ │ └── index.ts │ └── renderer/ │ ├── App.vue │ ├── api/ │ │ ├── artist.ts │ │ ├── bilibili.ts │ │ ├── donation.ts │ │ ├── gdmusic.ts │ │ ├── home.ts │ │ ├── list.ts │ │ ├── login.ts │ │ ├── lxMusicStrategy.ts │ │ ├── music.ts │ │ ├── musicParser.ts │ │ ├── mv.ts │ │ ├── parseFromCustomApi.ts │ │ ├── playlist.ts │ │ ├── search.ts │ │ └── user.ts │ ├── assets/ │ │ ├── css/ │ │ │ └── base.css │ │ └── icon/ │ │ ├── iconfont.css │ │ ├── iconfont.js │ │ └── iconfont.json │ ├── components/ │ │ ├── Coffee.vue │ │ ├── EQControl.vue │ │ ├── LanguageSwitcher.vue │ │ ├── MusicList.vue │ │ ├── MvPlayer.vue │ │ ├── ShortcutToast.vue │ │ ├── TrafficWarningDrawer.vue │ │ ├── common/ │ │ │ ├── AlbumItem.vue │ │ │ ├── ArtistDrawer.vue │ │ │ ├── BilibiliItem.vue │ │ │ ├── DisclaimerModal.vue │ │ │ ├── DonationList.vue │ │ │ ├── DownloadDrawer.vue │ │ │ ├── InstallAppModal.vue │ │ │ ├── MobileUpdateModal.vue │ │ │ ├── MusicListNavigator.ts │ │ │ ├── PlayBottom.vue │ │ │ ├── PlayListsItem.vue │ │ │ ├── PlaylistDrawer.vue │ │ │ ├── PlaylistItem.vue │ │ │ ├── ResponsiveModal.vue │ │ │ ├── SearchItem.vue │ │ │ ├── SongItem.vue │ │ │ ├── UpdateModal.vue │ │ │ └── songItemCom/ │ │ │ ├── BaseSongItem.vue │ │ │ ├── CompactSongItem.vue │ │ │ ├── ListSongItem.vue │ │ │ ├── MiniSongItem.vue │ │ │ ├── SongItemDropdown.vue │ │ │ └── StandardSongItem.vue │ │ ├── cover/ │ │ │ └── Cover3D.vue │ │ ├── home/ │ │ │ ├── PlaylistType.vue │ │ │ ├── RecommendAlbum.vue │ │ │ ├── RecommendSonglist.vue │ │ │ └── TopBanner.vue │ │ ├── login/ │ │ │ ├── CookieLogin.vue │ │ │ ├── QrLogin.vue │ │ │ └── UidLogin.vue │ │ ├── lyric/ │ │ │ ├── LyricCorrectionControl.vue │ │ │ ├── LyricSettings.vue │ │ │ ├── MusicFull.vue │ │ │ ├── MusicFullMobile.vue │ │ │ ├── MusicFullWrapper.vue │ │ │ └── ThemeColorPanel.vue │ │ ├── player/ │ │ │ ├── AdvancedControlsPopover.vue │ │ │ ├── MiniPlayBar.vue │ │ │ ├── MobilePlayBar.vue │ │ │ ├── MobilePlayerSettings.vue │ │ │ ├── PlayBar.vue │ │ │ ├── PlayingListDrawer.vue │ │ │ ├── ReparsePopover.vue │ │ │ ├── SimplePlayBar.vue │ │ │ ├── SleepTimer.vue │ │ │ └── SleepTimerTop.vue │ │ └── settings/ │ │ ├── ClearCacheSettings.vue │ │ ├── CookieSettingsModal.vue │ │ ├── MusicSourceSettings.vue │ │ ├── ProxySettings.vue │ │ ├── ServerSetting.vue │ │ └── ShortcutSettings.vue │ ├── const/ │ │ └── bar-const.ts │ ├── directive/ │ │ ├── index.ts │ │ └── loading/ │ │ ├── index.ts │ │ └── index.vue │ ├── hooks/ │ │ ├── AlbumHistoryHook.ts │ │ ├── IndexDBHook.ts │ │ ├── MusicHistoryHook.ts │ │ ├── MusicHook.ts │ │ ├── PlaylistHistoryHook.ts │ │ ├── useArtist.ts │ │ ├── useDownload.ts │ │ ├── usePlayMode.ts │ │ ├── usePlayerHooks.ts │ │ ├── useSongItem.ts │ │ └── useZoom.ts │ ├── index.css │ ├── index.html │ ├── layout/ │ │ ├── AppLayout.vue │ │ ├── MiniLayout.vue │ │ ├── MobileLayout.vue │ │ └── components/ │ │ ├── AppMenu.vue │ │ ├── MobileHeader.vue │ │ ├── SearchBar.vue │ │ ├── TitleBar.vue │ │ └── index.ts │ ├── main.ts │ ├── router/ │ │ ├── home.ts │ │ ├── index.ts │ │ └── other.ts │ ├── services/ │ │ ├── LxMusicSourceRunner.ts │ │ ├── SongSourceConfigManager.ts │ │ ├── audioService.ts │ │ ├── eqService.ts │ │ ├── lyricTranslation.ts │ │ ├── playbackRequestManager.ts │ │ ├── preloadService.ts │ │ └── translation-engines/ │ │ ├── index.ts │ │ └── opencc.ts │ ├── shims-vue.d.ts │ ├── store/ │ │ ├── index.ts │ │ └── modules/ │ │ ├── favorite.ts │ │ ├── intelligenceMode.ts │ │ ├── lyric.ts │ │ ├── menu.ts │ │ ├── music.ts │ │ ├── player.ts │ │ ├── playerCore.ts │ │ ├── playlist.ts │ │ ├── recommend.ts │ │ ├── search.ts │ │ ├── settings.ts │ │ ├── sleepTimer.ts │ │ └── user.ts │ ├── types/ │ │ ├── album.ts │ │ ├── artist.ts │ │ ├── bilibili.ts │ │ ├── day_recommend.ts │ │ ├── electron.d.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── listDetail.ts │ │ ├── lxMusic.ts │ │ ├── lyric.ts │ │ ├── music.ts │ │ ├── mv.ts │ │ ├── opencc-rust.d.ts │ │ ├── playlist.ts │ │ ├── search.ts │ │ ├── singer.ts │ │ └── user.ts │ ├── utils/ │ │ ├── appShortcuts.ts │ │ ├── auth.ts │ │ ├── fileOperation.ts │ │ ├── index.ts │ │ ├── linearColor.ts │ │ ├── lxCrypto.ts │ │ ├── playerUtils.ts │ │ ├── request.ts │ │ ├── request_music.ts │ │ ├── shortcutToast.ts │ │ ├── theme.ts │ │ ├── update.ts │ │ └── yrcParser.ts │ ├── views/ │ │ ├── artist/ │ │ │ └── detail.vue │ │ ├── bilibili/ │ │ │ └── BilibiliPlayer.vue │ │ ├── download/ │ │ │ └── DownloadPage.vue │ │ ├── favorite/ │ │ │ └── index.vue │ │ ├── heatmap/ │ │ │ └── index.vue │ │ ├── history/ │ │ │ └── index.vue │ │ ├── historyAndFavorite/ │ │ │ └── index.vue │ │ ├── home/ │ │ │ └── index.vue │ │ ├── list/ │ │ │ └── index.vue │ │ ├── login/ │ │ │ └── index.vue │ │ ├── lyric/ │ │ │ └── index.vue │ │ ├── mobile-search/ │ │ │ └── index.vue │ │ ├── mobile-search-result/ │ │ │ └── index.vue │ │ ├── music/ │ │ │ ├── HistoryRecommend.vue │ │ │ └── MusicListPage.vue │ │ ├── mv/ │ │ │ └── index.vue │ │ ├── playlist/ │ │ │ └── ImportPlaylist.vue │ │ ├── search/ │ │ │ └── index.vue │ │ ├── set/ │ │ │ ├── SettingItem.vue │ │ │ ├── SettingNav.vue │ │ │ ├── SettingSection.vue │ │ │ └── index.vue │ │ ├── toplist/ │ │ │ └── index.vue │ │ └── user/ │ │ ├── detail.vue │ │ ├── followers.vue │ │ ├── follows.vue │ │ └── index.vue │ └── vite-env.d.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.web.json └── vite.config.ts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/CODEOWNERS ================================================ * @algerkong ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.zh-CN.yml ================================================ name: 反馈 Bug description: 通过 github 模板进行 Bug 反馈。 title: '描述问题的标题' body: - type: markdown attributes: value: | # 欢迎你的参与 Issue 列表接受 bug 报告或是新功能请求。 在发布一个 Issue 前,请确保: - 在Issue中搜索过你的问题。(你的问题可能已有人提出,也可能已在最新版本中被修正) - 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在,不要在旧 Issue 下面留言,请建一个新的 issue。 - type: input id: reproduce attributes: label: 重现链接 description: 请提供尽可能精简的 CodePen、CodeSandbox 或 GitHub 仓库的链接。请不要填无关链接,否则你的 Issue 将被关闭。 placeholder: 请填写 - type: textarea id: reproduceSteps attributes: label: 重现步骤 description: 请清晰的描述重现该 Issue 的步骤,这能帮助我们快速定位问题。没有清晰重现步骤将不会被修复,标有 'need reproduction' 的 Issue 在 7 天内不提供相关步骤,将被关闭。 placeholder: 请填写 - type: textarea id: expect attributes: label: 期望结果 placeholder: 请填写 - type: textarea id: actual attributes: label: 实际结果 placeholder: 请填写 - type: input id: frameworkVersion attributes: label: 框架版本 placeholder: Vue(3.3.0) - type: input id: browsersVersion attributes: label: 浏览器版本 placeholder: Chrome(8.213.231.123) - type: input id: systemVersion attributes: label: 系统版本 placeholder: MacOS(11.2.3) - type: input id: nodeVersion attributes: label: Node版本 placeholder: 请填写 - type: textarea id: remarks attributes: label: 补充说明 description: 可以是遇到这个 bug 的业务场景、上下文等信息。 placeholder: 请填写 ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true contact_links: - name: url: about: ================================================ FILE: .github/ISSUE_TEMPLATE/feature-report.zh-CN.yml ================================================ name: 反馈新功能 description: 通过 github 模板进行新功能反馈。 title: '描述问题的标题' body: - type: markdown attributes: value: | # 欢迎你的参与 在发布一个 Issue 前,请确保: - 在 Issue 中搜索过你的问题。(你的问题可能已有人提出,也可能已在最新版本中被修正) - 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在,不要在旧 Issue 下面留言,请建一个新的 issue。 - type: textarea id: functionContent attributes: label: 这个功能解决了什么问题 description: 请详尽说明这个需求的用例和场景。最重要的是:解释清楚是怎样的用户体验需求催生了这个功能上的需求。我们将考虑添加在现有 API 无法轻松实现的功能。新功能的用例也应当足够常见。 placeholder: 请填写 validations: required: true - type: textarea id: functionalExpectations attributes: label: 你建议的方案是什么 placeholder: 请填写 validations: required: true ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### 🤔 这个 PR 的性质是? - [ ] 日常 bug 修复 - [ ] 新特性提交 - [ ] 文档改进 - [ ] 演示代码改进 - [ ] 组件样式/交互改进 - [ ] CI/CD 改进 - [ ] 重构 - [ ] 代码风格优化 - [ ] 测试用例 - [ ] 分支合并 - [ ] 其他 ### 🔗 相关 Issue ### 💡 需求背景和解决方案 ### 📝 更新日志 - fix(组件名称): 处理问题或特性描述 ... - [ ] 本条 PR 不需要纳入 Changelog ### ☑️ 请求合并前的自查清单 ⚠️ 请自检并全部**勾选全部选项**。⚠️ - [ ] 文档已补充或无须补充 - [ ] 代码演示已提供或无须提供 - [ ] TypeScript 定义已补充或无须补充 - [ ] Changelog 已提供或无须提供 ================================================ FILE: .github/dependabot.yml ================================================ # Basic dependabot.yml file with # minimum configuration for two package managers version: 2 updates: # Enable version updates for npm - package-ecosystem: 'npm' # Look for `package.json` and `lock` files in the `root` directory directory: '/' # Check the npm registry for updates every day (weekdays) schedule: interval: 'monthly' # Enable version updates for Docker - package-ecosystem: 'docker' # Look for a `Dockerfile` in the `root` directory directory: '/' # Check for updates once a week schedule: interval: 'monthly' ================================================ FILE: .github/issue-shoot.md ================================================ ## IssueShoot - 预估时长: {{ .duration }} - 期望完成时间: {{ .deadline }} - 开发难度: {{ .level }} - 参与人数: 1 - 需求对接人: ivringpeng - 验收标准: 实现期望改造效果,提 PR 并通过验收无误 - 备注: 最终激励以实际提交 `pull request` 并合并为准 ================================================ FILE: .github/workflows/build.yml ================================================ name: Build and Release on: push: tags: - 'v*' jobs: release: runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, windows-latest, ubuntu-latest] steps: - name: Check out Git repository uses: actions/checkout@v4 - name: Install Node.js uses: actions/setup-node@v4 with: node-version: 18 - name: Install Dependencies run: npm install # MacOS Build - name: Build MacOS if: matrix.os == 'macos-latest' run: | export ELECTRON_BUILDER_EXTRA_ARGS="--universal" npm run build:mac env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} CSC_IDENTITY_AUTO_DISCOVERY: false DEBUG: electron-builder # Windows Build - name: Build Windows if: matrix.os == 'windows-latest' run: npm run build:win env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Linux Build - name: Build Linux if: matrix.os == 'ubuntu-latest' run: | sudo apt-get update sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf npm run build:linux env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Get version from tag - name: Get version from tag id: get_version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV shell: bash # Read release notes - name: Read release notes id: release_notes run: | NOTES=$(awk "/## \[v${{ env.VERSION }}\]/{p=1;print;next} /## \[v/{p=0}p" CHANGELOG.md) echo "NOTES<> $GITHUB_ENV echo "$NOTES" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV shell: bash # Upload artifacts - name: Upload artifacts uses: softprops/action-gh-release@v1 with: files: | dist/*.dmg dist/*.exe dist/*.deb dist/*.rpm dist/*.AppImage dist/latest*.yml dist/*.blockmap body: ${{ env.NOTES }} draft: false prerelease: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/deploy.yml ================================================ name: Deploy Web on: push: branches: - main # 或者您的主分支名称 workflow_dispatch: # 允许手动触发 jobs: build-and-deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: 创建环境变量文件 run: | echo "VITE_API=${{ secrets.VITE_API }}" > .env.production.local echo "VITE_API_MUSIC=${{ secrets.VITE_API_MUSIC }}" >> .env.production.local # 添加其他需要的环境变量 cat .env.production.local # 查看创建的文件内容,调试用 - name: Install Dependencies run: npm install - name: Build run: npm run build - name: Deploy to Server uses: appleboy/scp-action@master with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USERNAME }} key: ${{ secrets.DEPLOY_KEY }} source: 'out/renderer/*' target: ${{ secrets.DEPLOY_PATH }} strip_components: 2 - name: Execute Remote Commands uses: appleboy/ssh-action@master with: host: ${{ secrets.SERVER_HOST }} username: ${{ secrets.SERVER_USERNAME }} key: ${{ secrets.DEPLOY_KEY }} script: | cd ${{ secrets.DEPLOY_PATH }} echo "部署完成于 $(date)" ================================================ FILE: .gitignore ================================================ node_modules .DS_Store dist dist-ssr *.local dist_electron .idea # lock yarn.lock pnpm-lock.yaml package-lock.json dist.zip .vscode bun.lockb bun.lock .env.*.local out .cursorrules .github/deploy_keys resources/android/**/* android/app/release .cursor .windsurf .agent .auto-imports.d.ts .components.d.ts src/renderer/auto-imports.d.ts src/renderer/components.d.ts ================================================ FILE: .husky/pre-commit ================================================ echo "对已暂存文件运行 lint-staged..." npx lint-staged echo "运行类型检查..." npm run typecheck echo "所有检查通过,准备提交..." ================================================ FILE: .husky/pre-push ================================================ echo "对已暂存文件运行 lint-staged..." npx lint-staged echo "运行类型检查..." npm run typecheck echo "所有检查通过,准备提交..." ================================================ FILE: .prettierignore ================================================ out dist pnpm-lock.yaml LICENSE.md tsconfig.json tsconfig.*.json ================================================ FILE: CHANGELOG.md ================================================ # 更新日志 ## v5.0.0 ### ✨ 新功能 - LX Music 音源脚本导入 - 逐字歌词,支持全屏歌词和桌面歌词同步显示 - 心动模式播放 - 移动设备整体页面风格和效果优化 - 移动端添加平板模式设置 - 歌词页面样式控制优化 支持背景、宽度、字体粗细等个性化设置 - 历史日推查看 - 播放记录热力图 - 历史记录支持本地和云端记录 - 用户页面收藏专辑展示 - 添加 GPU 硬件加速设置 - 菜单展开状态保存 - 感谢 [harenchi](https://github.com/souvenp) 的贡献 - 搜索建议 - 感谢 [harenchi](https://github.com/souvenp) 的贡献 - 歌词繁体中文翻译模块,集成 OpenCC 引擎 - 感谢 [Leko](https://github.com/lekoOwO) 的贡献 - 自定义 API源 支持 [自定义源文档](https://github.com/algerkong/AlgerMusicPlayer/blob/main/docs/custom-api-readme.md) - 感谢 [harenchi](https://github.com/souvenp) 的贡献 ### 🐛 Bug 修复 - 修复随机播放顺序异常 - 修复音源解析错误处理 - 修复 Mini 播放栏主题颜色问题 - 修复桌面歌词透明模式标题栏显示 - 修复逐字歌词字间距 - 修复远程控制设置无法保存 - 修复下载无损格式返回 HiRes 音质 - 感谢 [harenchi](https://github.com/souvenp) 的贡献 - 兼容 pnpm 包管理器 - 感谢 [Leko](https://github.com/lekoOwO) 的贡献 ### 🎨 优化 - 音源解析缓存 - 完善多语言国际化 - 优化播放检测和错误处理 - FLAC 元数据和封面图片处理 - 感谢 [harenchi](https://github.com/souvenp) 的贡献 - 日推不感兴趣调用官方接口 - 感谢 [harenchi](https://github.com/souvenp) 的贡献 - 代码提交流程优化,添加 lint-staged ## 赞赏支持☕️ [赞赏列表](https://donate.alger.fun/donate)
微信赞赏 支付宝赞赏
WeChat QRcode
☕️喝点咖啡继续干
Alipay QRcode
🍔来个汉堡
================================================ FILE: DEV.md ================================================ # Alger Music Player 开发文档 ## 项目结构 ### 技术栈 - **前端框架**:Vue 3 + TypeScript - **UI 组件库**:naive-ui - **样式框架**:Tailwind CSS - **图标库**:remixicon - **状态管理**:Pinia - **工具库**:VueUse - **构建工具**:Vite, electron-vite - **打包工具**:electron-builder - **国际化**:vue-i18n - **HTTP 客户端**:axios - **本地存储**:electron-store localstorage - **网易云音乐 API**:netease-cloud-music-api - **音乐解锁**:@unblockneteasemusic/server ### 项目结构 ``` AlgerMusicPlayer/ ├── build/                  # 构建相关文件 ├── docs/                   # 项目文档 ├── node_modules/           # 依赖包 ├── out/                    # 构建输出目录 ├── resources/              # 资源文件 ├── src/                    # 源代码 │   ├── i18n/               # 国际化配置 │   │   ├── lang/           # 语言包 │   │   ├── main.ts         # 主进程国际化入口 │   │   └── renderer.ts     # 渲染进程国际化入口 │   ├── main/               # Electron 主进程 │   │   ├── modules/        # 主进程模块 │   │   ├── index.ts        # 主进程入口 │   │   ├── lyric.ts        # 歌词处理 │   │   ├── server.ts       # 服务器 │   │   ├── set.json        # 设置 │   │   └── unblockMusic.ts # 音乐解锁 │   ├── preload/            # 预加载脚本 │   │   ├── index.ts        # 预加载脚本入口 │   │   └── index.d.ts      # 预加载脚本类型声明 │   └── renderer/           # Vue 渲染进程 │       ├── api/            # API 请求 │       ├── assets/         # 静态资源 │       ├── components/     # 组件 │       │   ├── common/     # 通用组件 │       │   ├── home/       # 首页组件 │       │   ├── lyric/      # 歌词组件 │       │   ├── settings/   # 设置组件 │       │   └── ...         # 其他组件 │       ├── const/          # 常量定义 │       ├── directive/      # 自定义指令 │       ├── hooks/          # 自定义 Hooks │       ├── layout/         # 布局组件 │       ├── router/         # 路由配置 │       ├── services/       # 服务 │       ├── store/          # Pinia 状态管理 │       │   ├── modules/    # Pinia 模块 │       │   └── index.ts    # Pinia 入口 │       ├── type/           # 类型定义 │       ├── types/          # 更多类型定义 │       ├── utils/          # 工具函数 │       ├── views/          # 页面视图 │       ├── App.vue         # 根组件 │       ├── index.css       # 全局样式 │       ├── index.html      # HTML 模板 │       ├── main.ts         # 渲染进程入口 │       └── ...             # 其他文件 ├── .env.development        # 开发环境变量 ├── .env.development.local  # 本地开发环境变量 ├── .env.production.local   # 本地生产环境变量 ├── .eslintrc.cjs           # ESLint 配置 ├── .gitignore              # Git 忽略文件 ├── .prettierrc.yaml        # Prettier 配置 ├── electron-builder.yml    # electron-builder 配置 ├── electron.vite.config.ts # electron-vite 配置 ├── package.json            # 项目配置 ├── postcss.config.js       # PostCSS 配置 ├── tailwind.config.js      # Tailwind 配置 ├── tsconfig.json           # TypeScript 配置 ├── tsconfig.node.json      # 节点 TypeScript 配置 └── tsconfig.web.json       # Web TypeScript 配置 ``` ### 主要组件说明 #### 主进程 (src/main) 主进程负责创建窗口、处理系统层面的交互以及与渲染进程的通信。 - **index.ts**: 应用主入口,负责创建窗口和应用生命周期管理 - **lyric.ts**: 歌词解析和处理 - **unblockMusic.ts**: 网易云音乐解锁功能 - **server.ts**: 本地服务器 #### 预加载脚本 (src/preload) 预加载脚本在渲染进程加载前执行,提供了渲染进程和主进程之间的桥接功能。 #### 渲染进程 (src/renderer) 渲染进程是基于 Vue 3 的前端应用,负责 UI 渲染和用户交互。 - **components/**: 包含各种 UI 组件   - **common/**: 通用组件   - **home/**: 首页相关组件   - **lyric/**: 歌词显示组件   - **settings/**: 设置界面组件   - **MusicList.vue**: 音乐列表组件   - **MvPlayer.vue**: MV 播放器   - **EQControl.vue**: 均衡器控制   - **...**: 其他组件 - **store/**: Pinia 状态管理   - **modules/**: 各功能模块的状态管理   - **index.ts**: 状态管理入口 - **views/**: 页面视图组件 - **router/**: 路由配置 - **api/**: API 请求封装 - **utils/**: 工具函数 ### 开发指南 #### 命名约定 - 目录使用 kebab-case (如: components/auth-wizard) - 组件文件名使用 PascalCase (如: AuthWizard.vue) - 可组合式函数使用 camelCase (如: useAuthState.ts) #### 代码风格 - 使用 Composition API 和 ` ================================================ FILE: resources/manifest.json ================================================ { "name": "Alger Music PWA", "icons": [ { "src": "./icon.png", "type": "image/png", "sizes": "256x256" } ] } ================================================ FILE: src/i18n/lang/en-US/artist.ts ================================================ export default { hotSongs: 'Hot Songs', albums: 'Albums', description: 'Artist Introduction' }; ================================================ FILE: src/i18n/lang/en-US/bilibili.ts ================================================ export default { player: { loading: 'Loading audio...', retry: 'Retry', playNow: 'Play Now', loadingTitle: 'Loading...', totalDuration: 'Total Duration: {duration}', partsList: 'Parts List ({count} episodes)', playStarted: 'Playback started', switchingPart: 'Switching to part: {part}', preloadingNext: 'Preloading next part: {part}', playingCurrent: 'Playing current selected part: {name}', num: 'M', errors: { invalidVideoId: 'Invalid video ID', loadVideoDetailFailed: 'Failed to load video details', loadPartInfoFailed: 'Unable to load video part information', loadAudioUrlFailed: 'Failed to get audio playback URL', videoDetailNotLoaded: 'Video details not loaded', missingParams: 'Missing required parameters', noAvailableAudioUrl: 'No available audio URL found', loadPartAudioFailed: 'Failed to load part audio URL', audioListEmpty: 'Audio list is empty, please retry', currentPartNotFound: 'Current part audio not found', audioUrlFailed: 'Failed to get audio URL', playFailed: 'Playback failed, please retry', getAudioUrlFailed: 'Failed to get audio URL, please retry', audioNotFound: 'Corresponding audio not found, please retry', preloadFailed: 'Failed to preload next part', switchPartFailed: 'Failed to load audio URL when switching parts' }, console: { loadingDetail: 'Loading Bilibili video details', detailData: 'Bilibili video detail data', multipleParts: 'Video has multiple parts, total {count}', noPartsData: 'Video has no parts or part data is empty', loadingAudioSource: 'Loading audio source', generatedAudioList: 'Generated audio list, total {count}', getDashAudioUrl: 'Got dash audio URL', getDurlAudioUrl: 'Got durl audio URL', loadingPartAudio: 'Loading part audio URL: {part}, cid: {cid}', loadPartAudioFailed: 'Failed to load part audio URL: {part}', switchToPart: 'Switching to part: {part}', audioNotFoundInList: 'Corresponding audio item not found', preparingToPlay: 'Preparing to play current selected part: {name}', preloadingNextPart: 'Preloading next part: {part}', playingSelectedPart: 'Playing current selected part: {name}, audio URL: {url}', preloadNextFailed: 'Failed to preload next part' } } }; ================================================ FILE: src/i18n/lang/en-US/common.ts ================================================ export default { play: 'Play', next: 'Next', previous: 'Previous', volume: 'Volume', settings: 'Settings', search: 'Search', loading: 'Loading...', loadingMore: 'Loading more...', alipay: 'Alipay', wechat: 'WeChat Pay', on: 'On', off: 'Off', show: 'Show', hide: 'Hide', confirm: 'Confirm', cancel: 'Cancel', configure: 'Configure', open: 'Open', modify: 'Modify', success: 'Operation Successful', error: 'Operation Failed', warning: 'Warning', info: 'Info', save: 'Save', delete: 'Delete', refresh: 'Refresh', retry: 'Retry', reset: 'Reset', back: 'Back', copySuccess: 'Copied to clipboard', copyFailed: 'Copy failed', validation: { required: 'This field is required', invalidInput: 'Invalid input', selectRequired: 'Please select an option', numberRange: 'Please enter a number between {min} and {max}', ipAddress: 'Please enter a valid IP address', portNumber: 'Please enter a valid port number (1-65535)' }, viewMore: 'View More', noMore: 'No more', selectAll: 'Select All', expand: 'Expand', collapse: 'Collapse', songCount: '{count} songs', language: 'Language', today: 'Today', yesterday: 'Yesterday', tray: { show: 'Show', quit: 'Quit', playPause: 'Play/Pause', prev: 'Previous', next: 'Next', pause: 'Pause', play: 'Play', favorite: 'Favorite' } }; ================================================ FILE: src/i18n/lang/en-US/comp.ts ================================================ export default { installApp: { description: 'Install the application for a better experience', noPrompt: 'Do not prompt again', install: 'Install now', cancel: 'Cancel', download: 'Download', downloadFailed: 'Download failed', downloadComplete: 'Download complete', downloadProblem: 'Download problem? Go to', downloadProblemLinkText: 'Download the latest version' }, playlistDrawer: { title: 'Add to playlist', createPlaylist: 'Create new playlist', cancelCreate: 'Cancel create', create: 'Create', playlistName: 'Playlist name', privatePlaylist: 'Private playlist', publicPlaylist: 'Public playlist', createSuccess: 'Playlist created successfully', createFailed: 'Playlist creation failed', addSuccess: 'Song added successfully', addFailed: 'Song addition failed', private: 'Private', public: 'Public', count: 'songs', loginFirst: 'Please login first', getPlaylistFailed: 'Get playlist failed', inputPlaylistName: 'Please enter the playlist name' }, update: { title: 'New version found', currentVersion: 'Current version', cancel: 'Do not update', prepareDownload: 'Preparing to download...', downloading: 'Downloading...', nowUpdate: 'Update now', downloadFailed: 'Download failed, please try again or download manually', startFailed: 'Start download failed, please try again or download manually', noDownloadUrl: 'No suitable installation package found for the current system, please download manually', installConfirmTitle: 'Install Update', installConfirmContent: 'Do you want to close the application and install the update?', manualInstallTip: 'If the installer does not open automatically after closing the application, please find the file in your download folder and open it manually.', yesInstall: 'Install Now', noThanks: 'Later', fileLocation: 'File Location', copy: 'Copy Path', copySuccess: 'Path copied to clipboard', copyFailed: 'Copy failed', backgroundDownload: 'Background Download' }, disclaimer: { title: 'Terms of Use', warning: 'This application is a development test version. Functions are not yet perfect, and there may be many problems and bugs. It is for learning and exchange only.', item1: 'This application is for personal learning, research and technical exchange only. Please do not use it for any commercial purposes.', item2: 'Please delete it within 24 hours after downloading. If you need to use it for a long time, please support the genuine music service.', item3: 'By using this application, you understand and assume the relevant risks. The developer is not responsible for any loss.', agree: 'I have read and agree', disagree: 'Disagree and Exit' }, donate: { title: 'Support Developer', subtitle: 'Your support is my motivation', tip: 'Donation is completely voluntary. All functions can be used normally without donation. Thank you for your understanding and support!', wechat: 'WeChat', alipay: 'Alipay', wechatQR: 'WeChat QR Code', alipayQR: 'Alipay QR Code', scanTip: 'Please use your phone to scan the QR code above to donate', enterApp: 'Enter App', noForce: 'No forced donation, click to enter' }, coffee: { title: 'Buy me a coffee', alipay: 'Alipay', wechat: 'Wechat', alipayQR: 'Alipay QR code', wechatQR: 'Wechat QR code', coffeeDesc: 'A cup of coffee, a support', coffeeDescLinkText: 'View more', groupText: 'Wechat Public Account: AlgerMusic', messages: { copySuccess: 'Copied to clipboard' }, donateList: 'Buy me a coffee' }, playlistType: { title: 'Playlist Category', showAll: 'Show all', hide: 'Hide some' }, recommendAlbum: { title: 'Latest Album' }, recommendSinger: { title: 'Daily Recommendation', songlist: 'Daily Recommendation List' }, recommendSonglist: { title: 'Weekly Hot Music' }, searchBar: { login: 'Login', toLogin: 'To Login', logout: 'Logout', set: 'Settings', theme: 'Theme', restart: 'Restart', refresh: 'Refresh', currentVersion: 'Current Version', searchPlaceholder: 'Search for something...', zoom: 'Zoom', zoom100: 'Zoom 100%', resetZoom: 'Reset Zoom', zoomDefault: 'Default Zoom' }, titleBar: { closeTitle: 'Choose how to close', minimizeToTray: 'Minimize to Tray', exitApp: 'Exit App', rememberChoice: 'Remember my choice', closeApp: 'Close App' }, userPlayList: { title: "{name}'s Playlist" }, musicList: { searchSongs: 'Search Songs', noSearchResults: 'No search results', switchToNormal: 'Switch to normal layout', switchToCompact: 'Switch to compact layout', playAll: 'Play All', collect: 'Collect', collectSuccess: 'Collect Success', cancelCollectSuccess: 'Cancel Collect Success', cancelCollect: 'Cancel Collect', addToPlaylist: 'Add to Playlist', addToPlaylistSuccess: 'Add to Playlist Success', operationFailed: 'Operation Failed', songsAlreadyInPlaylist: 'Songs already in playlist', historyRecommend: 'Daily History', fetchDatesFailed: 'Failed to fetch dates', fetchSongsFailed: 'Failed to fetch songs', noSongs: 'No songs' }, playlist: { import: { button: 'Import Playlist', title: 'Import Playlist', description: 'Import playlists via metadata, text, or links', linkTab: 'Import by Link', textTab: 'Import by Text', localTab: 'Import by Metadata', linkPlaceholder: 'Enter playlist links, one per line', textPlaceholder: 'Enter song information in format: Song Name Artist Name', localPlaceholder: 'Enter song metadata in JSON format', linkTips: 'Supported link sources:', linkTip1: 'Copy links after sharing playlists to WeChat/Weibo/QQ', linkTip2: 'Directly copy playlist/profile links', linkTip3: 'Directly copy article links', textTips: 'Enter song information, one song per line', textFormat: 'Format: Song Name Artist Name', localTips: 'Add song metadata', localFormat: 'Format example:', songNamePlaceholder: 'Song Name', artistNamePlaceholder: 'Artist Name', albumNamePlaceholder: 'Album Name', addSongButton: 'Add Song', addLinkButton: 'Add Link', importToStarPlaylist: 'Import to My Favorite Music', playlistNamePlaceholder: 'Enter playlist name', importButton: 'Start Import', emptyLinkWarning: 'Please enter playlist links', emptyTextWarning: 'Please enter song information', emptyLocalWarning: 'Please enter song metadata', invalidJsonFormat: 'Invalid JSON format', importSuccess: 'Import task created successfully', importFailed: 'Import failed', importStatus: 'Import Status', refresh: 'Refresh', taskId: 'Task ID', status: 'Status', successCount: 'Success Count', failReason: 'Failure Reason', unknownError: 'Unknown error', statusPending: 'Pending', statusProcessing: 'Processing', statusSuccess: 'Success', statusFailed: 'Failed', statusUnknown: 'Unknown', taskList: 'Task List', taskListTitle: 'Import Task List', action: 'Action', select: 'Select', fetchTaskListFailed: 'Failed to fetch task list', noTasks: 'No import tasks', clearTasks: 'Clear Tasks', clearTasksConfirmTitle: 'Confirm Clear', clearTasksConfirmContent: 'Are you sure you want to clear all import task records? This action cannot be undone.', confirm: 'Confirm', cancel: 'Cancel', clearTasksSuccess: 'Task list cleared', clearTasksFailed: 'Failed to clear task list' } }, settings: 'Settings', user: 'User', toplist: 'Toplist', history: 'History', list: 'Playlist', mv: 'MV', home: 'Home', search: 'Search' }; ================================================ FILE: src/i18n/lang/en-US/donation.ts ================================================ export default { description: 'Your donation will be used to support development and maintenance work, including but not limited to server maintenance, domain name renewal, etc.', message: 'You can leave your email or github name when leaving a message.', refresh: 'Refresh List', toDonateList: 'Buy me a coffee', title: 'Donation List', noMessage: 'No Message' }; ================================================ FILE: src/i18n/lang/en-US/download.ts ================================================ export default { title: 'Download Manager', localMusic: 'Local Music', count: '{count} songs in total', clearAll: 'Clear All', settings: 'Settings', tabs: { downloading: 'Downloading', downloaded: 'Downloaded' }, empty: { noTasks: 'No download tasks', noDownloaded: 'No downloaded songs', noDownloadedHint: 'Download your favorite songs to listen offline' }, progress: { total: 'Total Progress: {progress}%' }, items: 'items', status: { downloading: 'Downloading', completed: 'Completed', failed: 'Failed', unknown: 'Unknown' }, artist: { unknown: 'Unknown Artist' }, delete: { title: 'Delete Confirmation', message: 'Are you sure you want to delete "{filename}"? This action cannot be undone.', confirm: 'Delete', cancel: 'Cancel', success: 'Successfully deleted', failed: 'Failed to delete', fileNotFound: 'File not found or moved, removed from records', recordRemoved: 'Failed to delete file, but removed from records' }, clear: { title: 'Clear Download Records', message: 'Are you sure you want to clear all download records? This will not delete the actual music files, but will clear all records.', confirm: 'Clear', cancel: 'Cancel', success: 'Download records cleared' }, message: { downloadComplete: '{filename} download completed', downloadFailed: '{filename} download failed: {error}', alreadyDownloading: '{filename} is already downloading' }, loading: 'Loading...', playStarted: 'Play started: {name}', playFailed: 'Play failed: {name}', path: { copied: 'Path copied to clipboard', copyFailed: 'Failed to copy path' }, settingsPanel: { title: 'Download Settings', path: 'Download Location', pathDesc: 'Set where your music files will be saved', pathPlaceholder: 'Please select download path', noPathSelected: 'Please select download path first', select: 'Select Folder', open: 'Open Folder', fileFormat: 'Filename Format', fileFormatDesc: 'Set how downloaded music files will be named', customFormat: 'Custom Format', separator: 'Separator', separators: { dash: 'Space-dash-space', underscore: 'Underscore', space: 'Space' }, dragToArrange: 'Sort or use arrow buttons to arrange:', formatVariables: 'Available variables', preview: 'Preview:', saveSuccess: 'Download settings saved', presets: { songArtist: 'Song - Artist', artistSong: 'Artist - Song', songOnly: 'Song only' }, components: { songName: 'Song name', artistName: 'Artist name', albumName: 'Album name' } } }; ================================================ FILE: src/i18n/lang/en-US/favorite.ts ================================================ export default { title: 'Favorites', count: 'Total {count}', batchDownload: 'Batch Download', selectAll: 'All', download: 'Download ({count})', cancel: 'Cancel', emptyTip: 'No favorite songs yet', viewMore: 'View More', noMore: 'No more', downloadSuccess: 'Download completed', downloadFailed: 'Download failed', downloading: 'Downloading, please wait...', selectSongsFirst: 'Please select songs to download first', descending: 'Descending', ascending: 'Ascending' }; ================================================ FILE: src/i18n/lang/en-US/history.ts ================================================ export default { title: 'Play History', heatmapTitle: 'Heatmap', playCount: '{count}', getHistoryFailed: 'Failed to get play history', categoryTabs: { songs: 'Songs', playlists: 'Playlists', albums: 'Albums' }, tabs: { all: 'All Records', local: 'Local Records', cloud: 'Cloud Records' }, getCloudRecordFailed: 'Failed to get cloud records', needLogin: 'Please login with cookie to view cloud records', merging: 'Merging records...', noDescription: 'No description', noData: 'No records', newKey: 'New translation', heatmap: { title: 'Play Heatmap', loading: 'Loading data...', unit: 'plays', footerText: 'Hover to view details', playCount: 'Played {count} times', topSongs: 'Top songs of the day', times: 'times', totalPlays: 'Total Plays', activeDays: 'Active Days', noData: 'No play records', colorTheme: 'Color Theme', colors: { green: 'Green', blue: 'Blue', orange: 'Orange', purple: 'Purple', red: 'Red' }, mostPlayedSong: 'Most Played Song', mostActiveDay: 'Most Active Day', latestNightSong: 'Latest Night Song' } }; ================================================ FILE: src/i18n/lang/en-US/login.ts ================================================ export default { title: { qr: 'QR Code Login', phone: 'Phone Login', cookie: 'Cookie Login', uid: 'UID Login' }, qrTip: 'Scan with NetEase Cloud Music APP', phoneTip: 'Login with NetEase Cloud account', tokenTip: 'Enter a valid NetEase Cloud Music Cookie to login', uidTip: 'Enter User ID for quick login', placeholder: { phone: 'Phone Number', password: 'Password', cookie: 'Please enter NetEase Cloud Music Cookie (token)', uid: 'Please enter User ID (UID)' }, button: { login: 'Login', switchToQr: 'QR Code Login', switchToPhone: 'Phone Login', switchToToken: 'Use Cookie Login', switchToUid: 'UID Login', backToQr: 'Back to QR Code Login', cookieLogin: 'Cookie Login', autoGetCookie: 'Auto Get Cookie', refresh: 'Click to Refresh', refreshing: 'Refreshing...', refreshQr: 'Refresh QR Code' }, message: { loginSuccess: 'Login successful', loginFailed: 'Login failed', tokenLoginSuccess: 'Cookie login successful', uidLoginSuccess: 'UID login successful', loadError: 'Error loading login information', qrCheckError: 'Error checking QR code status', tokenRequired: 'Please enter Cookie', tokenInvalid: 'Invalid Cookie, please check and try again', uidRequired: 'Please enter User ID', uidInvalid: 'Invalid User ID or user does not exist', uidLoginFailed: 'UID login failed, please check if User ID is correct', phoneRequired: 'Please enter phone number', passwordRequired: 'Please enter password', phoneLoginFailed: 'Phone login failed, please check if phone number and password are correct', autoGetCookieSuccess: 'Auto get Cookie successful', autoGetCookieFailed: 'Auto get Cookie failed', autoGetCookieTip: 'Will open NetEase Cloud Music login page, please complete login and close the window', qrCheckFailed: 'Failed to check QR code status, please refresh and try again', qrLoading: 'Loading QR code...', qrExpired: 'QR code has expired, please click to refresh', qrExpiredShort: 'QR code expired', qrExpiredWarning: 'QR code has expired, please click to refresh for a new one', qrScanned: 'QR code scanned, please confirm login on your phone', qrScannedShort: 'Scanned', qrScannedInfo: 'QR code scanned, please confirm login on your phone', qrConfirmed: 'Login successful, redirecting...', qrGenerating: 'Generating QR code...' }, qrTitle: 'NetEase Cloud Music QR Code Login', uidWarning: 'Note: UID login is only for viewing user public information and cannot access features that require login permissions.' }; ================================================ FILE: src/i18n/lang/en-US/player.ts ================================================ export default { nowPlaying: 'Now Playing', playlist: 'Playlist', lyrics: 'Lyrics', previous: 'Previous', play: 'Play', pause: 'Pause', next: 'Next', volumeUp: 'Volume Up', volumeDown: 'Volume Down', mute: 'Mute', unmute: 'Unmute', songNum: 'Song Number: {num}', addCorrection: 'Add {num} seconds', subtractCorrection: 'Subtract {num} seconds', playFailed: 'Play Failed, Play Next Song', parseFailedPlayNext: 'Song parsing failed, playing next', consecutiveFailsError: 'Playback error, possibly due to network issues or invalid source. Please switch playlist or try again later', playMode: { sequence: 'Sequence', loop: 'Loop', random: 'Random' }, fullscreen: { enter: 'Enter Fullscreen', exit: 'Exit Fullscreen' }, close: 'Close', modeHint: { single: 'Single', list: 'Next' }, lrc: { noLrc: 'No lyrics, please enjoy', noAutoScroll: 'This lyrics does not support auto-scroll' }, reparse: { title: 'Select Music Source', desc: 'Click a source to directly reparse the current song. This source will be used next time this song plays.', success: 'Reparse successful', failed: 'Reparse failed', warning: 'Please select a music source', bilibiliNotSupported: 'Bilibili videos do not support reparsing', processing: 'Processing...', clear: 'Clear Custom Source', customApiFailed: 'Custom API parsing failed, trying built-in sources...', customApiError: 'Custom API request error, trying built-in sources...' }, playBar: { expand: 'Expand Lyrics', collapse: 'Collapse Lyrics', like: 'Like', lyric: 'Lyric', noSongPlaying: 'No song playing', eq: 'Equalizer', playList: 'Play List', reparse: 'Reparse', miniPlayBar: 'Mini Play Bar', playMode: { sequence: 'Sequence', loop: 'Loop', random: 'Random' }, play: 'Play', pause: 'Pause', prev: 'Previous', next: 'Next', volume: 'Volume', favorite: 'Favorite {name}', unFavorite: 'Unfavorite {name}', playbackSpeed: 'Playback Speed', advancedControls: 'Advanced Controls', intelligenceMode: { title: 'Intelligence Mode', needCookieLogin: 'Please login with Cookie method to use Intelligence Mode', noFavoritePlaylist: 'Favorite playlist not found', noLikedSongs: 'You have no liked songs yet', loading: 'Loading Intelligence Mode', success: 'Loaded {count} songs', failed: 'Failed to get Intelligence Mode list', error: 'Intelligence Mode error' } }, eq: { title: 'Equalizer', reset: 'Reset', on: 'On', off: 'Off', bass: 'Bass', midrange: 'Midrange', treble: 'Treble', presets: { flat: 'Flat', pop: 'Pop', rock: 'Rock', classical: 'Classical', jazz: 'Jazz', electronic: 'Electronic', hiphop: 'Hip-Hop', rb: 'R&B', metal: 'Metal', vocal: 'Vocal', dance: 'Dance', acoustic: 'Acoustic', custom: 'Custom' } }, // Playback settings settings: { title: 'Playback Settings', playbackSpeed: 'Playback Speed' }, // Sleep timer related sleepTimer: { title: 'Sleep Timer', cancel: 'Cancel Timer', timeMode: 'By Time', songsMode: 'By Songs', playlistEnd: 'After Playlist', afterPlaylist: 'After Playlist Ends', activeUntilEnd: 'Active until end of playlist', minutes: 'min', hours: 'hr', songs: 'songs', set: 'Set', timerSetSuccess: 'Timer set for {minutes} minutes', songsSetSuccess: 'Timer set for {songs} songs', playlistEndSetSuccess: 'Timer set to end after playlist', timerCancelled: 'Sleep timer cancelled', timerEnded: 'Sleep timer ended', playbackStopped: 'Music playback stopped', minutesRemaining: '{minutes} min remaining', songsRemaining: '{count} songs remaining', activeTime: 'Timer Active', activeSongs: 'Counting Songs', activeEnd: 'End After List' }, playList: { clearAll: 'Clear Playlist', alreadyEmpty: 'Playlist is already empty', cleared: 'Playlist cleared', empty: 'Playlist is empty', clearConfirmTitle: 'Clear Playlist', clearConfirmContent: 'This will clear all songs in the playlist and stop the current playback. Continue?' } }; ================================================ FILE: src/i18n/lang/en-US/search.ts ================================================ export default { title: { hotSearch: 'Hot Search', searchList: 'Search Results', searchHistory: 'Search History' }, button: { clear: 'Clear', back: 'Back', playAll: 'Play All' }, loading: { more: 'Loading...', failed: 'Search failed', searching: 'Searching...' }, noMore: 'No more results', error: { searchFailed: 'Search failed' }, search: { single: 'Single', album: 'Album', playlist: 'Playlist', mv: 'MV', bilibili: 'Bilibili' }, history: 'Search History', hot: 'Hot Searches', suggestions: 'Search Suggestions' }; ================================================ FILE: src/i18n/lang/en-US/settings.ts ================================================ export default { theme: 'Theme', language: 'Language', regard: 'About', logout: 'Logout', sections: { basic: 'Basic Settings', playback: 'Playback Settings', application: 'Application Settings', network: 'Network Settings', system: 'System Management', donation: 'Donation', about: 'About' }, basic: { themeMode: 'Theme Mode', themeModeDesc: 'Switch between light/dark theme', autoTheme: 'Follow System', manualTheme: 'Manual Switch', language: 'Language Settings', languageDesc: 'Change display language', tokenManagement: 'Cookie Management', tokenManagementDesc: 'Manage NetEase Cloud Music login Cookie', tokenStatus: 'Current Cookie Status', tokenSet: 'Set', tokenNotSet: 'Not Set', setToken: 'Set Cookie', modifyToken: 'Modify Cookie', clearToken: 'Clear Cookie', font: 'Font Settings', fontDesc: 'Select fonts, prioritize fonts in order', fontScope: { global: 'Global', lyric: 'Lyrics Only' }, animation: 'Animation Speed', animationDesc: 'Enable/disable animations', animationSpeed: { slow: 'Very Slow', normal: 'Normal', fast: 'Very Fast' }, fontPreview: { title: 'Font Preview', chinese: 'Chinese', english: 'English', japanese: 'Japanese', korean: 'Korean', chineseText: '静夜思 床前明月光 疑是地上霜', englishText: 'The quick brown fox jumps over the lazy dog', japaneseText: 'あいうえお かきくけこ さしすせそ', koreanText: '가나다라마 바사아자차 카타파하' }, gpuAcceleration: 'GPU Acceleration', gpuAccelerationDesc: 'Enable or disable hardware acceleration, can improve rendering performance but may increase GPU load', gpuAccelerationRestart: 'Changing GPU acceleration settings requires application restart to take effect', gpuAccelerationChangeSuccess: 'GPU acceleration settings updated, restart application to take effect', gpuAccelerationChangeError: 'Failed to update GPU acceleration settings', tabletMode: 'Tablet Mode', tabletModeDesc: 'Enabling tablet mode allows using PC-style interface on mobile devices' }, playback: { quality: 'Audio Quality', qualityDesc: 'Select music playback quality (VIP)', qualityOptions: { standard: 'Standard', higher: 'Higher', exhigh: 'Extreme', lossless: 'Lossless', hires: 'Hi-Res', jyeffect: 'HD Surround', sky: 'Immersive', dolby: 'Dolby Atmos', jymaster: 'Master' }, musicSources: 'Music Sources', musicSourcesDesc: 'Select music sources for song resolution', musicSourcesWarning: 'At least one music source must be selected', musicUnblockEnable: 'Enable Music Unblocking', musicUnblockEnableDesc: 'When enabled, attempts to resolve unplayable songs', configureMusicSources: 'Configure Sources', selectedMusicSources: 'Selected sources:', noMusicSources: 'No sources selected', gdmusicInfo: 'GD Music Station intelligently resolves music from multiple platforms automatically', autoPlay: 'Auto Play', autoPlayDesc: 'Auto resume playback when reopening the app', showStatusBar: 'Show Status Bar', showStatusBarContent: 'You can display the music control function in your mac status bar (effective after a restart)', fallbackParser: 'Fallback Parser (GD Music)', fallbackParserDesc: 'When "GD Music" is checked and regular sources fail, this service will be used.', parserGD: 'GD Music (Built-in)', parserCustom: 'Custom API', // Source labels sourceLabels: { migu: 'Migu', kugou: 'Kugou', pyncmd: 'NetEase (Built-in)', bilibili: 'Bilibili', gdmusic: 'GD Music', custom: 'Custom API' }, customApi: { sectionTitle: 'Custom API Settings', importConfig: 'Import JSON Config', currentSource: 'Current Source', notImported: 'No custom source imported yet.', importSuccess: 'Successfully imported source: {name}', importFailed: 'Import failed: {message}', enableHint: 'Import a JSON config file to enable', status: { imported: 'Custom Source Imported', notImported: 'Not Imported' } }, lxMusic: { tabs: { sources: 'Source Selection', lxMusic: 'LX Music', customApi: 'Custom API' }, scripts: { title: 'Imported Scripts', importLocal: 'Import Local', importOnline: 'Import Online', urlPlaceholder: 'Enter LX Music Script URL', importBtn: 'Import', empty: 'No imported LX Music scripts', notConfigured: 'Not configured (Configure in LX Music Tab)', importHint: 'Import compatible custom API plugins to extend sources', noScriptWarning: 'Please import LX Music script first', noSelectionWarning: 'Please select an LX Music source first', notFound: 'Source not found', switched: 'Switched to source: {name}', deleted: 'Deleted source: {name}', enterUrl: 'Please enter script URL', invalidUrl: 'Invalid URL format', invalidScript: 'Invalid LX Music script, globalThis.lx code not found', nameRequired: 'Name cannot be empty', renameSuccess: 'Rename successful' } } }, application: { closeAction: 'Close Action', closeActionDesc: 'Choose action when closing window', closeOptions: { ask: 'Ask Every Time', minimize: 'Minimize to Tray', close: 'Exit Directly' }, shortcut: 'Shortcut Settings', shortcutDesc: 'Customize global shortcuts', download: 'Download Management', downloadDesc: 'Always show download list button', unlimitedDownload: 'Unlimited Download', unlimitedDownloadDesc: 'Enable unlimited download mode for music , default limit 300 songs', downloadPath: 'Download Directory', downloadPathDesc: 'Choose download location for music files', remoteControl: 'Remote Control', remoteControlDesc: 'Set remote control function' }, network: { apiPort: 'Music API Port', apiPortDesc: 'Restart required after modification', proxy: 'Proxy Settings', proxyDesc: 'Enable proxy when unable to access music', proxyHost: 'Proxy Host', proxyHostPlaceholder: 'Enter proxy host', proxyPort: 'Proxy Port', proxyPortPlaceholder: 'Enter proxy port', realIP: 'RealIP Settings', realIPDesc: 'Use realIP parameter with mainland China IP to resolve access restrictions abroad', messages: { proxySuccess: 'Proxy settings saved, restart required to take effect', proxyError: 'Please check your input', realIPSuccess: 'RealIP settings saved', realIPError: 'Please enter a valid IP address' } }, system: { cache: 'Cache Management', cacheDesc: 'Clear cache', cacheClearTitle: 'Select cache types to clear:', cacheTypes: { history: { label: 'Play History', description: 'Clear played song records' }, favorite: { label: 'Favorites', description: 'Clear local favorite songs (cloud favorites not affected)' }, user: { label: 'User Data', description: 'Clear login info and user-related data' }, settings: { label: 'App Settings', description: 'Clear all custom app settings' }, downloads: { label: 'Download History', description: 'Clear download history (downloaded files not affected)' }, resources: { label: 'Music Resources', description: 'Clear cached music files, lyrics and other resources' }, lyrics: { label: 'Lyrics Resources', description: 'Clear cached lyrics resources' } }, restart: 'Restart', restartDesc: 'Restart application', messages: { clearSuccess: 'Cache cleared successfully, some settings will take effect after restart' } }, about: { version: 'Version', checkUpdate: 'Check for Updates', checking: 'Checking...', latest: 'Already latest version', hasUpdate: 'New version available', gotoUpdate: 'Go to Update', gotoGithub: 'Go to Github', author: 'Author', authorDesc: 'algerkong Give a star🌟', messages: { checkError: 'Failed to check for updates, please try again later' } }, validation: { selectProxyProtocol: 'Please select proxy protocol', proxyHost: 'Please enter proxy host', portNumber: 'Please enter a valid port number (1-65535)' }, lyricSettings: { title: 'Lyric Settings', tabs: { display: 'Display', interface: 'Interface', typography: 'Typography', background: 'Background', mobile: 'Mobile' }, pureMode: 'Pure Mode', hideCover: 'Hide Cover', centerDisplay: 'Center Display', showTranslation: 'Show Translation', hideLyrics: 'Hide Lyrics', hidePlayBar: 'Hide Play Bar', hideMiniPlayBar: 'Hide Mini Play Bar', showMiniPlayBar: 'Show Mini Play Bar', backgroundTheme: 'Background Theme', themeOptions: { default: 'Default', light: 'Light', dark: 'Dark' }, fontSize: 'Font Size', fontSizeMarks: { small: 'Small', medium: 'Medium', large: 'Large' }, fontWeight: 'Font Weight', fontWeightMarks: { thin: 'Thin', normal: 'Normal', bold: 'Bold' }, letterSpacing: 'Letter Spacing', letterSpacingMarks: { compact: 'Compact', default: 'Default', loose: 'Loose' }, lineHeight: 'Line Height', lineHeightMarks: { compact: 'Compact', default: 'Default', loose: 'Loose' }, contentWidth: 'Content Width', mobileLayout: 'Mobile Layout', layoutOptions: { default: 'Default', ios: 'iOS Style', android: 'Android Style' }, mobileCoverStyle: 'Cover Style', coverOptions: { record: 'Record', square: 'Square', full: 'Full Screen' }, lyricLines: 'Lyric Lines', mobileUnavailable: 'This setting is only available on mobile devices', // Background settings background: { useCustomBackground: 'Use Custom Background', backgroundMode: 'Background Mode', modeOptions: { solid: 'Solid', gradient: 'Gradient', image: 'Image', css: 'CSS' }, solidColor: 'Select Color', presetColors: 'Preset Colors', customColor: 'Custom Color', gradientEditor: 'Gradient Editor', gradientColors: 'Gradient Colors', gradientDirection: 'Gradient Direction', directionOptions: { toBottom: 'Top to Bottom', toRight: 'Left to Right', toBottomRight: 'Top Left to Bottom Right', angle45: '45 Degrees', toTop: 'Bottom to Top', toLeft: 'Right to Left' }, addColor: 'Add Color', removeColor: 'Remove Color', imageUpload: 'Upload Image', imagePreview: 'Image Preview', clearImage: 'Clear Image', imageBlur: 'Blur', imageBrightness: 'Brightness', customCss: 'Custom CSS Style', customCssPlaceholder: 'Enter CSS style, e.g.: background: linear-gradient(...)', customCssHelp: 'Supports any CSS background property', reset: 'Reset to Default', fileSizeLimit: 'Image size limit: 20MB', invalidImageFormat: 'Invalid image format', imageTooLarge: 'Image too large, please select an image smaller than 20MB' } }, translationEngine: 'Lyric Translation Engine', translationEngineOptions: { none: 'Off', opencc: 'OpenCC Traditionalize' }, themeColor: { title: 'Lyric Theme Color', presetColors: 'Preset Colors', customColor: 'Custom Color', preview: 'Preview', previewText: 'Lyric Effect', colorNames: { 'spotify-green': 'Spotify Green', 'apple-blue': 'Apple Blue', 'youtube-red': 'YouTube Red', orange: 'Vibrant Orange', purple: 'Mystic Purple', pink: 'Cherry Pink' }, tooltips: { openColorPicker: 'Open Color Picker', closeColorPicker: 'Close Color Picker' }, placeholder: '#1db954' }, shortcutSettings: { title: 'Shortcut Settings', shortcut: 'Shortcut', shortcutDesc: 'Customize global shortcuts', shortcutConflict: 'Shortcut Conflict', inputPlaceholder: 'Click to input shortcut', resetShortcuts: 'Reset', disableAll: 'Disable All', enableAll: 'Enable All', togglePlay: 'Play/Pause', prevPlay: 'Previous', nextPlay: 'Next', volumeUp: 'Volume Up', volumeDown: 'Volume Down', toggleFavorite: 'Favorite/Unfavorite', toggleWindow: 'Show/Hide Window', scopeGlobal: 'Global', scopeApp: 'App Only', enabled: 'Enabled', disabled: 'Disabled', messages: { resetSuccess: 'Shortcuts reset successfully, please save', conflict: 'Shortcut conflict, please reset', saveSuccess: 'Shortcuts saved successfully', saveError: 'Failed to save shortcuts', cancelEdit: 'Edit cancelled', disableAll: 'All shortcuts disabled, please save to apply', enableAll: 'All shortcuts enabled, please save to apply' } }, remoteControl: { title: 'Remote Control', enable: 'Enable Remote Control', port: 'Port', allowedIps: 'Allowed IPs', addIp: 'Add IP', emptyListHint: 'Empty list means allow all IPs', saveSuccess: 'Remote control settings saved', accessInfo: 'Remote control access address:' }, cookie: { title: 'Cookie Settings', description: 'Please enter NetEase Cloud Music Cookie:', placeholder: 'Please paste the complete Cookie...', help: { format: 'Cookie usually starts with "MUSIC_U="', source: 'Can be obtained from browser developer tools network requests', storage: 'Cookie will be automatically saved to local storage after setting' }, action: { save: 'Save Cookie', paste: 'Paste', clear: 'Clear' }, validation: { required: 'Please enter Cookie', format: 'Cookie format may be incorrect, please check if it contains MUSIC_U' }, message: { saveSuccess: 'Cookie saved successfully', saveError: 'Failed to save Cookie', pasteSuccess: 'Pasted successfully', pasteError: 'Paste failed, please copy manually' }, info: { length: 'Current length: {length} characters' } } }; ================================================ FILE: src/i18n/lang/en-US/songItem.ts ================================================ export default { menu: { play: 'Play', playNext: 'Play Next', download: 'Download', addToPlaylist: 'Add to Playlist', favorite: 'Like', unfavorite: 'Unlike', removeFromPlaylist: 'Remove from Playlist', dislike: 'Dislike', undislike: 'Undislike' }, message: { downloading: 'Downloading, please wait...', downloadFailed: 'Download failed', downloadQueued: 'Added to download queue', addedToNextPlay: 'Added to play next', getUrlFailed: 'Failed to get music download URL, please check if logged in' }, dialog: { dislike: { title: 'Dislike', content: 'Are you sure you want to dislike this song?', positiveText: 'Dislike', negativeText: 'Cancel' } } }; ================================================ FILE: src/i18n/lang/en-US/user.ts ================================================ export default { profile: { followers: 'Followers', following: 'Following', level: 'Level' }, playlist: { created: 'Created Playlists', mine: 'Mine', trackCount: '{count} tracks', playCount: 'Played {count} times' }, tabs: { created: 'Created', favorite: 'Favorite', album: 'Album' }, ranking: { title: 'Listening History', playCount: '{count} times' }, follow: { title: 'Follow List', viewPlaylist: 'View Playlist', noFollowings: 'No Followings', loadMore: 'Load More', noSignature: 'This guy is lazy, nothing left', userFollowsTitle: "'s Followings", myFollowsTitle: 'My Followings' }, follower: { title: 'Follower List', noFollowers: 'No Followers', loadMore: 'Load More', userFollowersTitle: "'s Followers", myFollowersTitle: 'My Followers' }, detail: { playlists: 'Playlists', records: 'Listening History', noPlaylists: 'No Playlists', noRecords: 'No Listening History', artist: 'Artist', noSignature: 'This guy is lazy, nothing left', invalidUserId: 'Invalid User ID', noRecordPermission: "{name} doesn't let you see your listening history" }, message: { loadFailed: 'Failed to load user page', deleteSuccess: 'Successfully deleted', deleteFailed: 'Failed to delete' } }; ================================================ FILE: src/i18n/lang/ja-JP/artist.ts ================================================ export default { hotSongs: '人気楽曲', albums: 'アルバム', description: 'アーティスト紹介' }; ================================================ FILE: src/i18n/lang/ja-JP/bilibili.ts ================================================ export default { player: { loading: 'オーディオ読み込み中...', retry: '再試行', playNow: '今すぐ再生', loadingTitle: '読み込み中...', totalDuration: '総再生時間: {duration}', partsList: 'パートリスト ({count}話)', playStarted: '再生を開始しました', switchingPart: 'パートを切り替え中: {part}', preloadingNext: '次のパートをプリロード中: {part}', playingCurrent: '現在選択されたパートを再生中: {name}', num: '万', errors: { invalidVideoId: '無効な動画ID', loadVideoDetailFailed: '動画詳細の取得に失敗しました', loadPartInfoFailed: '動画パート情報の読み込みができません', loadAudioUrlFailed: 'オーディオ再生URLの取得に失敗しました', videoDetailNotLoaded: '動画詳細が読み込まれていません', missingParams: '必要なパラメータが不足しています', noAvailableAudioUrl: '利用可能なオーディオURLが見つかりません', loadPartAudioFailed: 'パートオーディオURLの読み込みに失敗しました', audioListEmpty: 'オーディオリストが空です。再試行してください', currentPartNotFound: '現在のパートのオーディオが見つかりません', audioUrlFailed: 'オーディオURLの取得に失敗しました', playFailed: '再生に失敗しました。再試行してください', getAudioUrlFailed: 'オーディオURLの取得に失敗しました。再試行してください', audioNotFound: '対応するオーディオが見つかりません。再試行してください', preloadFailed: '次のパートのプリロードに失敗しました', switchPartFailed: 'パート切り替え時のオーディオURL読み込みに失敗しました' }, console: { loadingDetail: 'Bilibiliビデオ詳細を読み込み中', detailData: 'Bilibiliビデオ詳細データ', multipleParts: 'ビデオに複数のパートがあります。合計{count}個', noPartsData: 'ビデオにパートがないか、パートデータが空です', loadingAudioSource: 'オーディオソースを読み込み中', generatedAudioList: 'オーディオリストを生成しました。合計{count}個', getDashAudioUrl: 'dashオーディオURLを取得しました', getDurlAudioUrl: 'durlオーディオURLを取得しました', loadingPartAudio: 'パートオーディオURLを読み込み中: {part}, cid: {cid}', loadPartAudioFailed: 'パートオーディオURLの読み込みに失敗: {part}', switchToPart: 'パートに切り替え中: {part}', audioNotFoundInList: '対応するオーディオアイテムが見つかりません', preparingToPlay: '現在選択されたパートの再生準備中: {name}', preloadingNextPart: '次のパートをプリロード中: {part}', playingSelectedPart: '現在選択されたパートを再生中: {name}、オーディオURL: {url}', preloadNextFailed: '次のパートのプリロードに失敗しました' } } }; ================================================ FILE: src/i18n/lang/ja-JP/common.ts ================================================ export default { play: '再生', next: '次の曲', previous: '前の曲', volume: '音量', settings: '設定', search: '検索', loading: '読み込み中...', loadingMore: 'さらに読み込み中...', alipay: 'Alipay', wechat: 'WeChat Pay', on: 'オン', off: 'オフ', show: '表示', hide: '非表示', confirm: '確認', cancel: 'キャンセル', configure: '設定', open: '開く', modify: '変更', success: '操作成功', error: '操作失敗', warning: '警告', info: 'お知らせ', save: '保存', delete: '削除', refresh: '更新', retry: '再試行', reset: 'リセット', back: '戻る', copySuccess: 'クリップボードにコピーしました', copyFailed: 'コピーに失敗しました', validation: { required: 'この項目は必須です', invalidInput: '無効な入力です', selectRequired: 'オプションを選択してください', numberRange: '{min}から{max}の間の数値を入力してください' }, viewMore: 'もっと見る', noMore: 'これ以上ありません', selectAll: '全選択', expand: '展開', collapse: '折りたたみ', songCount: '{count}曲', language: '言語', today: '今日', yesterday: '昨日', tray: { show: '表示', quit: '終了', playPause: '再生/一時停止', prev: '前の曲', next: '次の曲', pause: '一時停止', play: '再生', favorite: 'お気に入り' } }; ================================================ FILE: src/i18n/lang/ja-JP/comp.ts ================================================ export default { installApp: { description: 'アプリをインストールして、より良い体験を', noPrompt: '今後表示しない', install: '今すぐインストール', cancel: '後でインストール', download: 'ダウンロード', downloadFailed: 'ダウンロード失敗', downloadComplete: 'ダウンロード完了', downloadProblem: 'ダウンロードに問題がありますか?', downloadProblemLinkText: '最新版をダウンロード' }, playlistDrawer: { title: 'プレイリストに追加', createPlaylist: '新しいプレイリストを作成', cancelCreate: '作成をキャンセル', create: '作成', playlistName: 'プレイリスト名', privatePlaylist: 'プライベートプレイリスト', publicPlaylist: 'パブリックプレイリスト', createSuccess: 'プレイリストの作成に成功しました', createFailed: 'プレイリストの作成に失敗しました', addSuccess: '楽曲の追加に成功しました', addFailed: '楽曲の追加に失敗しました', private: 'プライベート', public: 'パブリック', count: '曲', loginFirst: 'まずログインしてください', getPlaylistFailed: 'プレイリストの取得に失敗しました', inputPlaylistName: 'プレイリスト名を入力してください' }, update: { title: '新しいバージョンが見つかりました', currentVersion: '現在のバージョン', cancel: '後で更新', prepareDownload: 'ダウンロード準備中...', downloading: 'ダウンロード中...', nowUpdate: '今すぐ更新', downloadFailed: 'ダウンロードに失敗しました。再試行するか手動でダウンロードしてください', startFailed: 'ダウンロードの開始に失敗しました。再試行するか手動でダウンロードしてください', noDownloadUrl: '現在のシステムに適したインストールパッケージが見つかりません。手動でダウンロードしてください', installConfirmTitle: '更新をインストール', installConfirmContent: 'アプリを閉じて更新をインストールしますか?', manualInstallTip: 'アプリを閉じた後にインストーラーが正常に起動しない場合は、ダウンロードフォルダでファイルを見つけて手動で開いてください。', yesInstall: '今すぐインストール', noThanks: '後でインストール', fileLocation: 'ファイルの場所', copy: 'パスをコピー', copySuccess: 'パスをクリップボードにコピーしました', copyFailed: 'コピーに失敗しました', backgroundDownload: 'バックグラウンドダウンロード' }, disclaimer: { title: '使用上の注意', warning: 'このアプリは開発テスト版であり、機能が不完全で、多くの問題やバグが存在する可能性があります。学習と交流のみを目的としています。', item1: 'このアプリは個人の学習、研究、技術交流のみを目的としています。商業目的で使用しないでください。', item2: 'ダウンロード後24時間以内に削除してください。長期使用を希望される場合は、正規の音楽サービスをサポートしてください。', item3: 'このアプリを使用することで、関連するリスクを理解し、負担するものとします。開発者は一切の損失に対して責任を負いません。', agree: '以上の内容を読み、同意します', disagree: '同意せずに終了' }, donate: { title: '開発者を支援', subtitle: '皆様のサポートが私の原動力です', tip: '寄付は完全に任意です。寄付しなくてもすべての機能を通常通り使用できます。ご理解とご支援に感謝します!', wechat: 'WeChat', alipay: 'Alipay', wechatQR: 'WeChat 受取コード', alipayQR: 'Alipay 受取コード', scanTip: 'スマートフォンのアプリで上記のQRコードをスキャンして寄付してください', enterApp: 'アプリに入る', noForce: '寄付は強制ではありません。クリックして入れます' }, coffee: { title: 'コーヒーをおごる', alipay: 'Alipay', wechat: 'WeChat Pay', alipayQR: 'Alipay QRコード', wechatQR: 'WeChat QRコード', coffeeDesc: '一杯のコーヒー、一つのサポート', coffeeDescLinkText: 'もっと見る', groupText: '微信公众号:AlgerMusic', messages: { copySuccess: 'クリップボードにコピーしました' }, donateList: 'コーヒーをおごる' }, playlistType: { title: 'プレイリストカテゴリ', showAll: 'すべて表示', hide: '一部を非表示' }, recommendAlbum: { title: '最新アルバム' }, recommendSinger: { title: '毎日のおすすめ', songlist: '毎日のおすすめリスト' }, recommendSonglist: { title: '今週の人気音楽' }, searchBar: { login: 'ログイン', toLogin: 'ログインへ', logout: 'ログアウト', set: '設定', theme: 'テーマ', restart: '再起動', refresh: '更新', currentVersion: '現在のバージョン', searchPlaceholder: '何かを検索してみましょう...', zoom: 'ページズーム', zoom100: '標準ズーム100%', resetZoom: 'クリックしてズームをリセット', zoomDefault: '標準ズーム' }, titleBar: { closeTitle: '閉じる方法を選択してください', minimizeToTray: 'トレイに最小化', exitApp: 'アプリを終了', rememberChoice: '選択を記憶する', closeApp: 'アプリを閉じる' }, userPlayList: { title: '{name}のよく聞く音楽' }, musicList: { searchSongs: '楽曲を検索', noSearchResults: '関連する楽曲が見つかりませんでした', switchToNormal: 'デフォルトレイアウトに切り替え', switchToCompact: 'コンパクトレイアウトに切り替え', playAll: 'すべて再生', collect: 'お気に入り', collectSuccess: 'お気に入りに追加しました', cancelCollectSuccess: 'お気に入りから削除しました', operationFailed: '操作に失敗しました', cancelCollect: 'お気に入りから削除', addToPlaylist: 'プレイリストに追加', addToPlaylistSuccess: 'プレイリストに追加しました', songsAlreadyInPlaylist: '楽曲は既にプレイリストに存在します', historyRecommend: '履歴の日次推薦', fetchDatesFailed: '日付リストの取得に失敗しました', fetchSongsFailed: '楽曲リストの取得に失敗しました', noSongs: '楽曲がありません' }, playlist: { import: { button: 'プレイリストインポート', title: 'プレイリストインポート', description: 'メタデータ/テキスト/リンクの3つの方法でプレイリストをインポートできます', linkTab: 'リンクインポート', textTab: 'テキストインポート', localTab: 'メタデータインポート', linkPlaceholder: 'プレイリストのリンクを入力してください(1行に1つ)', textPlaceholder: '楽曲情報を入力してください。形式:楽曲名 アーティスト名', localPlaceholder: 'JSON形式の楽曲メタデータを入力してください', linkTips: 'サポートされているリンクソース:', linkTip1: 'プレイリストをWeChat/Weibo/QQでシェアした後、リンクをコピー', linkTip2: 'プレイリスト/個人ページのリンクを直接コピー', linkTip3: '記事のリンクを直接コピー', textTips: '楽曲情報を入力してください(1行に1曲)', textFormat: '形式:楽曲名 アーティスト名', localTips: '楽曲メタデータを追加してください', localFormat: '形式例:', songNamePlaceholder: '楽曲名', artistNamePlaceholder: 'アーティスト名', albumNamePlaceholder: 'アルバム名', addSongButton: '楽曲を追加', addLinkButton: 'リンクを追加', importToStarPlaylist: 'お気に入りの音楽にインポート', playlistNamePlaceholder: 'プレイリスト名を入力してください', importButton: 'インポート開始', emptyLinkWarning: 'プレイリストのリンクを入力してください', emptyTextWarning: '楽曲情報を入力してください', emptyLocalWarning: '楽曲メタデータを入力してください', invalidJsonFormat: 'JSON形式が正しくありません', importSuccess: 'インポートタスクの作成に成功しました', importFailed: 'インポートに失敗しました', importStatus: 'インポート状況', refresh: '更新', taskId: 'タスクID', status: 'ステータス', successCount: '成功数', failReason: '失敗理由', unknownError: '不明なエラー', statusPending: '処理待ち', statusProcessing: '処理中', statusSuccess: 'インポート成功', statusFailed: 'インポート失敗', statusUnknown: '不明なステータス', taskList: 'タスクリスト', taskListTitle: 'インポートタスクリスト', action: '操作', select: '選択', fetchTaskListFailed: 'タスクリストの取得に失敗しました', noTasks: 'インポートタスクがありません', clearTasks: 'タスクをクリア', clearTasksConfirmTitle: 'クリア確認', clearTasksConfirmContent: 'すべてのインポートタスク記録をクリアしますか?この操作は元に戻せません。', confirm: '確認', cancel: 'キャンセル', clearTasksSuccess: 'タスクリストをクリアしました', clearTasksFailed: 'タスクリストのクリアに失敗しました' } }, settings: '設定', user: 'ユーザー', toplist: 'ランキング', history: 'お気に入り履歴', list: 'プレイリスト', mv: 'MV', home: 'ホーム', search: '検索' }; ================================================ FILE: src/i18n/lang/ja-JP/donation.ts ================================================ export default { description: 'あなたの寄付は開発・保守作業をサポートするために使用され、サーバー保守、ドメイン更新などが含まれます。', message: 'メッセージを残す際は、メールアドレスやGitHubユーザー名を記載してください。', refresh: 'リストを更新', toDonateList: 'コーヒーをおごる', noMessage: 'メッセージがありません', title: '寄付リスト' }; ================================================ FILE: src/i18n/lang/ja-JP/download.ts ================================================ export default { title: 'ダウンロード管理', localMusic: 'ローカル音楽', count: '合計{count}曲', clearAll: '記録をクリア', settings: '設定', tabs: { downloading: 'ダウンロード中', downloaded: 'ダウンロード済み' }, empty: { noTasks: 'ダウンロードタスクがありません', noDownloaded: 'ダウンロード済みの楽曲がありません' }, progress: { total: '全体の進行状況: {progress}%' }, status: { downloading: 'ダウンロード中', completed: '完了', failed: '失敗', unknown: '不明' }, artist: { unknown: '不明なアーティスト' }, delete: { title: '削除確認', message: '楽曲「{filename}」を削除しますか?この操作は元に戻せません。', confirm: '削除確認', cancel: 'キャンセル', success: '削除成功', failed: '削除失敗', fileNotFound: 'ファイルが存在しないか移動されました。記録から削除しました', recordRemoved: 'ファイルの削除に失敗しましたが、記録から削除しました' }, clear: { title: 'ダウンロード記録をクリア', message: 'すべてのダウンロード記録をクリアしますか?この操作はダウンロード済みの音楽ファイルを削除しませんが、すべての記録をクリアします。', confirm: 'クリア確認', cancel: 'キャンセル', success: 'ダウンロード記録をクリアしました' }, message: { downloadComplete: '{filename}のダウンロードが完了しました', downloadFailed: '{filename}のダウンロードに失敗しました: {error}' }, loading: '読み込み中...', playStarted: '再生開始: {name}', playFailed: '再生失敗: {name}', path: { copied: 'パスをクリップボードにコピーしました', copyFailed: 'パスのコピーに失敗しました' }, settingsPanel: { title: 'ダウンロード設定', path: 'ダウンロード場所', pathDesc: '音楽ファイルのダウンロード保存場所を設定', pathPlaceholder: 'ダウンロードパスを選択してください', noPathSelected: 'まずダウンロードパスを選択してください', select: 'フォルダを選択', open: 'フォルダを開く', fileFormat: 'ファイル名形式', fileFormatDesc: '音楽ダウンロード時のファイル命名形式を設定', customFormat: 'カスタム形式', separator: '区切り文字', separators: { dash: 'スペース-スペース', underscore: 'アンダースコア', space: 'スペース' }, dragToArrange: 'ドラッグで並び替えまたは矢印ボタンで順序を調整:', formatVariables: '使用可能な変数', preview: 'プレビュー効果:', saveSuccess: 'ダウンロード設定を保存しました', presets: { songArtist: '楽曲名 - アーティスト名', artistSong: 'アーティスト名 - 楽曲名', songOnly: '楽曲名のみ' }, components: { songName: '楽曲名', artistName: 'アーティスト名', albumName: 'アルバム名' } } }; ================================================ FILE: src/i18n/lang/ja-JP/favorite.ts ================================================ export default { title: 'お気に入り', count: '合計{count}曲', batchDownload: '一括ダウンロード', download: 'ダウンロード ({count})', emptyTip: 'まだお気に入りの楽曲がありません', downloadSuccess: 'ダウンロード完了', downloadFailed: 'ダウンロード失敗', downloading: 'ダウンロード中です。しばらくお待ちください...', selectSongsFirst: 'まずダウンロードする楽曲を選択してください', descending: '降順', ascending: '昇順' }; ================================================ FILE: src/i18n/lang/ja-JP/history.ts ================================================ export default { title: '再生履歴', heatmapTitle: 'ヒートマップ', playCount: '{count}', getHistoryFailed: '履歴の取得に失敗しました', tabs: { all: 'すべての記録', local: 'ローカル記録', cloud: 'クラウド記録' }, categoryTabs: { songs: '楽曲', playlists: 'プレイリスト', albums: 'アルバム' }, noDescription: '説明なし', noData: '記録なし', getCloudRecordFailed: 'クラウド記録の取得に失敗しました', needLogin: 'cookieを使用してログインしてクラウド記録を表示できます', merging: '記録を統合中...', heatmap: { title: '再生ヒートマップ', loading: 'データを読み込み中...', unit: '回再生', footerText: 'ホバーして詳細を表示', playCount: '{count} 回再生', topSongs: 'その日の人気曲', times: '回', totalPlays: '総再生回数', activeDays: 'アクティブ日数', noData: '再生記録がありません', colorTheme: 'カラーテーマ', colors: { green: 'グリーン', blue: 'ブルー', orange: 'オレンジ', purple: 'パープル', red: 'レッド' }, mostPlayedSong: '最も再生された曲', mostActiveDay: '最もアクティブな日', latestNightSong: '深夜に再生した曲' } }; ================================================ FILE: src/i18n/lang/ja-JP/login.ts ================================================ export default { title: { qr: 'QRコードログイン', phone: '電話番号ログイン', cookie: 'Cookieログイン', uid: 'UIDログイン' }, qrTip: 'NetEase Cloudアプリでログイン', phoneTip: 'NetEase Cloudアカウントでログイン', tokenTip: '有効なNetEase Cloud MusicのCookieを入力してログイン', uidTip: 'ユーザーIDを入力してクイックログイン', placeholder: { phone: '電話番号', password: 'パスワード', cookie: 'NetEase Cloud MusicのCookie(token)を入力してください', uid: 'ユーザーID(UID)を入力してください' }, button: { login: 'ログイン', switchToQr: 'QRコードログイン', switchToPhone: '電話番号ログイン', switchToToken: 'Cookieログインを使用', switchToUid: 'UIDログイン', backToQr: 'QRコードログインに戻る', cookieLogin: 'Cookieログイン', autoGetCookie: 'Cookie自動取得', refresh: 'クリックしてリフレッシュ', refreshing: 'リフレッシュ中...', refreshQr: 'QRコードをリフレッシュ' }, message: { loginSuccess: 'ログイン成功', tokenLoginSuccess: 'Cookieログイン成功', uidLoginSuccess: 'UIDログイン成功', loadError: 'ログイン情報の読み込み中にエラーが発生しました', qrCheckError: 'QRコードの状態確認中にエラーが発生しました', tokenRequired: 'Cookieを入力してください', tokenInvalid: 'Cookieが無効です。確認して再試行してください', uidRequired: 'ユーザーIDを入力してください', uidInvalid: 'ユーザーIDが無効またはユーザーが存在しません', uidLoginFailed: 'UIDログインに失敗しました。ユーザーIDが正しいか確認してください', autoGetCookieSuccess: 'Cookie自動取得成功', autoGetCookieFailed: 'Cookie自動取得失敗', autoGetCookieTip: 'NetEase Cloud Musicのログインページを開きます。ログイン完了後、ウィンドウを閉じてください', loginFailed: 'ログイン失敗', phoneRequired: '電話番号を入力してください', passwordRequired: 'パスワードを入力してください', phoneLoginFailed: '電話番号でのログインに失敗しました。電話番号とパスワードが正しいか確認してください', qrCheckFailed: 'QRコードの状態確認に失敗しました。リフレッシュして再試行してください', qrLoading: 'QRコードを読み込み中...', qrExpired: 'QRコードの期限が切れました。クリックしてリフレッシュしてください', qrExpiredShort: 'QRコード期限切れ', qrExpiredWarning: 'QRコードの期限が切れました。クリックして新しいQRコードを取得してください', qrScanned: 'QRコードがスキャンされました。スマートフォンでログインを確認してください', qrScannedShort: 'スキャン済み', qrScannedInfo: 'QRコードがスキャンされました。スマートフォンでログインを確認してください', qrConfirmed: 'ログイン成功、リダイレクト中...', qrGenerating: 'QRコードを生成中...' }, qrTitle: 'NetEase Cloud Music QRコードログイン', uidWarning: '注意:UIDログインはユーザーの公開情報を表示するためのみ使用でき、ログイン権限が必要な機能にはアクセスできません。' }; ================================================ FILE: src/i18n/lang/ja-JP/player.ts ================================================ export default { nowPlaying: '再生中', playlist: 'プレイリスト', lyrics: '歌詞', previous: '前へ', play: '再生', pause: '一時停止', next: '次へ', volumeUp: '音量を上げる', volumeDown: '音量を下げる', mute: 'ミュート', unmute: 'ミュート解除', songNum: '楽曲総数:{num}', addCorrection: '{num}秒早める', subtractCorrection: '{num}秒遅らせる', playFailed: '現在の楽曲の再生に失敗しました。次の曲を再生します', parseFailedPlayNext: '楽曲の解析に失敗しました。次の曲を再生します', consecutiveFailsError: '再生エラーが発生しました。ネットワークの問題または無効な音源の可能性があります。プレイリストを切り替えるか、後でもう一度お試しください', playMode: { sequence: '順次再生', loop: 'リピート再生', random: 'ランダム再生' }, fullscreen: { enter: 'フルスクリーン', exit: 'フルスクリーン終了' }, close: '閉じる', modeHint: { single: 'リピート再生', list: '自動で次の曲を再生' }, lrc: { noLrc: '歌詞がありません。お楽しみください', noAutoScroll: '本歌詞は自動スクロールをサポートしていません' }, reparse: { title: '解析音源を選択', desc: '音源をクリックして直接解析します。次回この楽曲を再生する際は選択した音源を使用します', success: '再解析成功', failed: '再解析失敗', warning: '音源を選択してください', bilibiliNotSupported: 'Bilibili動画は再解析をサポートしていません', processing: '解析中...', clear: 'カスタム音源をクリア', customApiFailed: 'カスタムAPIの解析に失敗しました。内蔵音源を試しています...', customApiError: 'カスタムAPIのリクエストでエラーが発生しました。内蔵音源を試しています...' }, playBar: { expand: '歌詞を展開', collapse: '歌詞を折りたたみ', like: 'いいね', lyric: '歌詞', noSongPlaying: '再生中の楽曲がありません', eq: 'イコライザー', playList: 'プレイリスト', reparse: '再解析', playMode: { sequence: '順次再生', loop: 'ループ再生', random: 'ランダム再生' }, play: '再生開始', pause: '再生一時停止', prev: '前の曲', next: '次の曲', volume: '音量', favorite: '{name}をお気に入りに追加しました', unFavorite: '{name}をお気に入りから削除しました', miniPlayBar: 'ミニ再生バー', playbackSpeed: '再生速度', advancedControls: 'その他の設定', intelligenceMode: { title: 'インテリジェンスモード', needCookieLogin: 'Cookie方式でログインしてからインテリジェンスモードを使用してください', noFavoritePlaylist: '「お気に入りの音楽」プレイリストが見つかりません', noLikedSongs: 'まだ「いいね」した楽曲がありません', loading: 'インテリジェンスモードを読み込み中', success: '{count} 曲を読み込みました', failed: 'インテリジェンスモードのリスト取得に失敗しました', error: 'インテリジェンスモードの再生でエラーが発生しました' } }, eq: { title: 'イコライザー', reset: 'リセット', on: 'オン', off: 'オフ', bass: '低音', midrange: '中音', treble: '高音', presets: { flat: 'フラット', pop: 'ポップ', rock: 'ロック', classical: 'クラシック', jazz: 'ジャズ', electronic: 'エレクトロニック', hiphop: 'ヒップホップ', rb: 'R&B', metal: 'メタル', vocal: 'ボーカル', dance: 'ダンス', acoustic: 'アコースティック', custom: 'カスタム' } }, // プレイヤー設定 settings: { title: '再生設定', playbackSpeed: '再生速度' }, // タイマー機能関連 sleepTimer: { title: 'スリープタイマー', cancel: 'タイマーをキャンセル', timeMode: '時間で停止', songsMode: '楽曲数で停止', playlistEnd: 'プレイリスト終了後に停止', afterPlaylist: 'プレイリスト終了後に停止', activeUntilEnd: 'リスト終了まで再生', minutes: '分', hours: '時間', songs: '曲', set: '設定', timerSetSuccess: '{minutes}分後に停止するよう設定しました', songsSetSuccess: '{songs}曲再生後に停止するよう設定しました', playlistEndSetSuccess: 'プレイリスト終了後に停止するよう設定しました', timerCancelled: 'スリープタイマーをキャンセルしました', timerEnded: 'スリープタイマーが作動しました', playbackStopped: '音楽再生を停止しました', minutesRemaining: '残り{minutes}分', songsRemaining: '残り{count}曲' }, playList: { clearAll: 'プレイリストをクリア', alreadyEmpty: 'プレイリストは既に空です', cleared: 'プレイリストをクリアしました', empty: 'プレイリストが空です', clearConfirmTitle: 'プレイリストをクリア', clearConfirmContent: 'これによりプレイリスト内のすべての楽曲がクリアされ、現在の再生が停止されます。続行しますか?' } }; ================================================ FILE: src/i18n/lang/ja-JP/search.ts ================================================ export default { title: { hotSearch: '人気検索リスト', searchList: '検索リスト', searchHistory: '検索履歴' }, button: { clear: 'クリア', back: '戻る', playAll: 'リストを再生' }, loading: { more: '読み込み中...', failed: '検索に失敗しました', searching: '検索中...' }, noMore: 'これ以上ありません', error: { searchFailed: '検索に失敗しました' }, search: { single: '楽曲', album: 'アルバム', playlist: 'プレイリスト', mv: 'MV', bilibili: 'Bilibili' }, history: '検索履歴', hot: '人気検索', suggestions: '検索候補' }; ================================================ FILE: src/i18n/lang/ja-JP/settings.ts ================================================ export default { theme: 'テーマ', language: '言語', regard: 'について', logout: 'ログアウト', sections: { basic: '基本設定', playback: '再生設定', application: 'アプリケーション設定', network: 'ネットワーク設定', system: 'システム管理', donation: '寄付サポート', about: 'について' }, basic: { themeMode: 'テーマモード', themeModeDesc: 'ライト/ダークテーマの切り替え', autoTheme: 'システムに従う', manualTheme: '手動切り替え', language: '言語設定', languageDesc: '表示言語を切り替え', tokenManagement: 'Cookie管理', tokenManagementDesc: 'NetEase Cloud MusicログインCookieを管理', tokenStatus: '現在のCookieステータス', tokenSet: '設定済み', tokenNotSet: '未設定', setToken: 'Cookieを設定', modifyToken: 'Cookieを変更', clearToken: 'Cookieをクリア', font: 'フォント設定', fontDesc: 'フォントを選択します。前に配置されたフォントが優先されます', fontScope: { global: 'グローバル', lyric: '歌詞のみ' }, animation: 'アニメーション速度', animationDesc: 'アニメーションを有効にするかどうか', animationSpeed: { slow: '非常に遅い', normal: '通常', fast: '非常に速い' }, fontPreview: { title: 'フォントプレビュー', chinese: '中国語', english: 'English', japanese: '日本語', korean: '韓国語', chineseText: '静夜思 床前明月光 疑是地上霜', englishText: 'The quick brown fox jumps over the lazy dog', japaneseText: 'あいうえお かきくけこ さしすせそ', koreanText: '가나다라마 바사아자차 카타파하' }, gpuAcceleration: 'GPUアクセラレーション', gpuAccelerationDesc: 'ハードウェアアクセラレーションを有効または無効にします。レンダリングパフォーマンスを向上させますが、GPU負荷が増える可能性があります', gpuAccelerationRestart: 'GPUアクセラレーション設定の変更はアプリの再起動後に有効になります', gpuAccelerationChangeSuccess: 'GPUアクセラレーション設定を更新しました。アプリの再起動後に有効になります', gpuAccelerationChangeError: 'GPUアクセラレーション設定の更新に失敗しました', tabletMode: 'タブレットモード', tabletModeDesc: 'タブレットモードを有効にすると、モバイルデバイスでPCスタイルのインターフェースを使用できます' }, playback: { quality: '音質設定', qualityDesc: '音楽再生の音質を選択(NetEase Cloud VIP)', qualityOptions: { standard: '標準', higher: '高音質', exhigh: '超高音質', lossless: 'ロスレス', hires: 'Hi-Res', jyeffect: 'HD サラウンド', sky: 'イマーシブサラウンド', dolby: 'Dolby Atmos', jymaster: '超高解像度マスター' }, musicSources: '音源設定', musicSourcesDesc: '音楽解析に使用する音源プラットフォームを選択', musicSourcesWarning: '少なくとも1つの音源プラットフォームを選択する必要があります', musicUnblockEnable: '音楽解析を有効にする', musicUnblockEnableDesc: '有効にすると、再生できない音楽の解析を試みます', configureMusicSources: '音源を設定', selectedMusicSources: '選択された音源:', noMusicSources: '音源が選択されていません', gdmusicInfo: 'GD音楽台は複数のプラットフォーム音源を自動解析し、最適な結果を自動選択できます', autoPlay: '自動再生', autoPlayDesc: 'アプリを再起動した際に自動的に再生を継続するかどうか', showStatusBar: 'ステータスバーコントロール機能を表示するかどうか', showStatusBarContent: 'Macのステータスバーに音楽コントロール機能を表示できます(再起動後に有効)', fallbackParser: '代替解析サービス (GD音楽台)', fallbackParserDesc: '「GD音楽台」にチェックが入っていて、通常の音源で再生できない場合、このサービスが使用されます。', parserGD: 'GD 音楽台 (内蔵)', parserCustom: 'カスタム API', sourceLabels: { migu: 'Migu', kugou: 'Kugou', pyncmd: 'NetEase (内蔵)', bilibili: 'Bilibili', gdmusic: 'GD 音楽台', custom: 'カスタム API' }, customApi: { sectionTitle: 'カスタム API 設定', enableHint: 'カスタム API を有効にするには、まずカスタム API をインポートする必要があります。', importConfig: 'JSON設定をインポート', currentSource: '現在の音源', notImported: 'カスタム音源はまだインポートされていません。', importSuccess: '音源のインポートに成功しました: {name}', importFailed: 'インポートに失敗しました: {message}', status: { imported: 'カスタム音源インポート済み', notImported: '未インポート' } }, lxMusic: { tabs: { sources: '音源選択', lxMusic: '落雪音源', customApi: 'カスタムAPI' }, scripts: { title: 'インポート済みのスクリプト', importLocal: 'ローカルインポート', importOnline: 'オンラインインポート', urlPlaceholder: '落雪音源スクリプトのURLを入力', importBtn: 'インポート', empty: 'インポート済みの落雪音源はありません', notConfigured: '未設定(落雪音源タブで設定してください)', importHint: '互換性のあるカスタムAPIプラグインをインポートして音源を拡張します', noScriptWarning: '先に落雪音源スクリプトをインポートしてください', noSelectionWarning: '先に落雪音源を選択してください', notFound: '音源が存在しません', switched: '音源を切り替えました: {name}', deleted: '音源を削除しました: {name}', enterUrl: 'スクリプトURLを入力してください', invalidUrl: '無効なURL形式', invalidScript: '無効な落雪音源スクリプトです(globalThis.lxが見つかりません)', nameRequired: '名前を空にすることはできません', renameSuccess: '名前を変更しました' } } }, application: { closeAction: '閉じる動作', closeActionDesc: 'ウィンドウを閉じる際の動作を選択', closeOptions: { ask: '毎回確認', minimize: 'トレイに最小化', close: '直接終了' }, shortcut: 'ショートカット設定', shortcutDesc: 'グローバルショートカットをカスタマイズ', download: 'ダウンロード管理', downloadDesc: 'ダウンロードリストボタンを常に表示するかどうか', unlimitedDownload: '無制限ダウンロード', unlimitedDownloadDesc: '有効にすると音楽を無制限でダウンロードします(ダウンロード失敗の可能性があります)。デフォルトは300曲制限', downloadPath: 'ダウンロードディレクトリ', downloadPathDesc: '音楽ファイルのダウンロード場所を選択', remoteControl: 'リモートコントロール', remoteControlDesc: 'リモートコントロール機能を設定' }, network: { apiPort: '音楽APIポート', apiPortDesc: '変更後はアプリの再起動が必要です', proxy: 'プロキシ設定', proxyDesc: '音楽にアクセスできない場合はプロキシを有効にできます', proxyHost: 'プロキシアドレス', proxyHostPlaceholder: 'プロキシアドレスを入力してください', proxyPort: 'プロキシポート', proxyPortPlaceholder: 'プロキシポートを入力してください', realIP: 'realIP設定', realIPDesc: '制限により、このプロジェクトは海外での使用が制限されます。realIPパラメータを使用して国内IPを渡すことで解決できます', messages: { proxySuccess: 'プロキシ設定を保存しました。アプリ再起動後に有効になります', proxyError: '入力が正しいかどうか確認してください', realIPSuccess: '実IPアドレス設定を保存しました', realIPError: '有効なIPアドレスを入力してください' } }, system: { cache: 'キャッシュ管理', cacheDesc: 'キャッシュをクリア', cacheClearTitle: 'クリアするキャッシュタイプを選択してください:', cacheTypes: { history: { label: '再生履歴', description: '再生した楽曲の記録をクリア' }, favorite: { label: 'お気に入り記録', description: 'ローカルのお気に入り楽曲記録をクリア(クラウドのお気に入りには影響しません)' }, user: { label: 'ユーザーデータ', description: 'ログイン情報とユーザー関連データをクリア' }, settings: { label: 'アプリ設定', description: 'アプリのすべてのカスタム設定をクリア' }, downloads: { label: 'ダウンロード記録', description: 'ダウンロード履歴をクリア(ダウンロード済みファイルは削除されません)' }, resources: { label: '音楽リソース', description: '読み込み済みの音楽ファイル、歌詞などのリソースキャッシュをクリア' }, lyrics: { label: '歌詞リソース', description: '読み込み済みの歌詞リソースキャッシュをクリア' } }, restart: '再起動', restartDesc: 'アプリを再起動', messages: { clearSuccess: 'クリア成功。一部の設定は再起動後に有効になります' } }, about: { version: 'バージョン', checkUpdate: '更新を確認', checking: '確認中...', latest: '現在最新バージョンです', hasUpdate: '新しいバージョンが見つかりました', gotoUpdate: '更新へ', gotoGithub: 'Githubへ', author: '作者', authorDesc: 'algerkong スターを付けてください🌟', messages: { checkError: '更新確認に失敗しました。後でもう一度お試しください' } }, validation: { selectProxyProtocol: 'プロキシプロトコルを選択してください', proxyHost: 'プロキシアドレスを入力してください', portNumber: '有効なポート番号を入力してください(1-65535)' }, lyricSettings: { title: '歌詞設定', tabs: { display: '表示', interface: 'インターフェース', typography: 'テキスト', background: '背景', mobile: 'モバイル' }, pureMode: 'ピュアモード', hideCover: 'カバーを非表示', centerDisplay: '中央表示', showTranslation: '翻訳を表示', hideLyrics: '歌詞を非表示', hidePlayBar: '再生バーを非表示', hideMiniPlayBar: 'ミニ再生バーを非表示', showMiniPlayBar: 'ミニ再生バーを表示', backgroundTheme: '背景テーマ', themeOptions: { default: 'デフォルト', light: 'ライト', dark: 'ダーク' }, fontSize: 'フォントサイズ', fontSizeMarks: { small: '小', medium: '中', large: '大' }, fontWeight: 'フォントの太さ', fontWeightMarks: { thin: '細い', normal: '通常', bold: '太い' }, letterSpacing: '文字間隔', letterSpacingMarks: { compact: 'コンパクト', default: 'デフォルト', loose: 'ゆったり' }, lineHeight: '行の高さ', lineHeightMarks: { compact: 'コンパクト', default: 'デフォルト', loose: 'ゆったり' }, contentWidth: 'コンテンツ幅', mobileLayout: 'モバイルレイアウト', layoutOptions: { default: 'デフォルト', ios: 'iOSスタイル', android: 'Androidスタイル' }, mobileCoverStyle: 'カバースタイル', coverOptions: { record: 'レコード', square: '正方形', full: 'フルスクリーン' }, lyricLines: '歌詞行数', mobileUnavailable: 'この設定はモバイルでのみ利用可能です', // 背景設定 background: { useCustomBackground: 'カスタム背景を使用', backgroundMode: '背景モード', modeOptions: { solid: '単色', gradient: 'グラデーション', image: '画像', css: 'CSS' }, solidColor: '色を選択', presetColors: 'プリセットカラー', customColor: 'カスタムカラー', gradientEditor: 'グラデーションエディター', gradientColors: 'グラデーションカラー', gradientDirection: 'グラデーション方向', directionOptions: { toBottom: '上から下', toRight: '左から右', toBottomRight: '左上から右下', angle45: '45度', toTop: '下から上', toLeft: '右から左' }, addColor: '色を追加', removeColor: '色を削除', imageUpload: '画像をアップロード', imagePreview: '画像プレビュー', clearImage: '画像をクリア', imageBlur: 'ぼかし', imageBrightness: '明るさ', customCss: 'カスタム CSS スタイル', customCssPlaceholder: 'CSSスタイルを入力、例: background: linear-gradient(...)', customCssHelp: '任意のCSS background プロパティをサポート', reset: 'デフォルトにリセット', fileSizeLimit: '画像サイズ制限: 20MB', invalidImageFormat: '無効な画像形式', imageTooLarge: '画像が大きすぎます。20MB未満の画像を選択してください' } }, translationEngine: '歌詞翻訳エンジン', translationEngineOptions: { none: 'オフ', opencc: 'OpenCC 繁体字化' }, themeColor: { title: '歌詞テーマカラー', presetColors: 'プリセットカラー', customColor: 'カスタムカラー', preview: 'プレビュー効果', previewText: '歌詞効果', colorNames: { 'spotify-green': 'Spotify グリーン', 'apple-blue': 'Apple ブルー', 'youtube-red': 'YouTube レッド', orange: 'バイタルオレンジ', purple: 'ミステリアスパープル', pink: 'サクラピンク' }, tooltips: { openColorPicker: 'カラーパレットを開く', closeColorPicker: 'カラーパレットを閉じる' }, placeholder: '#1db954' }, shortcutSettings: { title: 'ショートカット設定', shortcut: 'ショートカット', shortcutDesc: 'ショートカットをカスタマイズ', shortcutConflict: 'ショートカットの競合', inputPlaceholder: 'クリックしてショートカットを入力', resetShortcuts: 'デフォルトに戻す', disableAll: 'すべて無効', enableAll: 'すべて有効', togglePlay: '再生/一時停止', prevPlay: '前の曲', nextPlay: '次の曲', volumeUp: '音量を上げる', volumeDown: '音量を下げる', toggleFavorite: 'お気に入り/お気に入り解除', toggleWindow: 'ウィンドウ表示/非表示', scopeGlobal: 'グローバル', scopeApp: 'アプリ内', enabled: '有効', disabled: '無効', messages: { resetSuccess: 'デフォルトのショートカットに戻しました。保存を忘れずに', conflict: '競合するショートカットがあります。再設定してください', saveSuccess: 'ショートカット設定を保存しました', saveError: 'ショートカットの保存に失敗しました。再試行してください', cancelEdit: '変更をキャンセルしました', disableAll: 'すべてのショートカットを無効にしました。保存を忘れずに', enableAll: 'すべてのショートカットを有効にしました。保存を忘れずに' } }, remoteControl: { title: 'リモートコントロール', enable: 'リモートコントロールを有効にする', port: 'サービスポート', allowedIps: '許可されたIPアドレス', addIp: 'IPを追加', emptyListHint: '空のリストはすべてのIPアクセスを許可することを意味します', saveSuccess: 'リモートコントロール設定を保存しました', accessInfo: 'リモートコントロールアクセスアドレス:' }, cookie: { title: 'Cookie設定', description: 'NetEase Cloud MusicのCookieを入力してください:', placeholder: '完全なCookieを貼り付けてください...', help: { format: 'Cookieは通常「MUSIC_U=」で始まります', source: 'ブラウザの開発者ツールのネットワークリクエストから取得できます', storage: 'Cookie設定後、自動的にローカルストレージに保存されます' }, action: { save: 'Cookieを保存', paste: '貼り付け', clear: 'クリア' }, validation: { required: 'Cookieを入力してください', format: 'Cookie形式が正しくない可能性があります。MUSIC_Uが含まれているか確認してください' }, message: { saveSuccess: 'Cookieの保存に成功しました', saveError: 'Cookieの保存に失敗しました', pasteSuccess: '貼り付けに成功しました', pasteError: '貼り付けに失敗しました。手動でコピーしてください' }, info: { length: '現在の長さ:{length} 文字' } } }; ================================================ FILE: src/i18n/lang/ja-JP/songItem.ts ================================================ export default { menu: { play: '再生', playNext: '次に再生', download: '楽曲をダウンロード', addToPlaylist: 'プレイリストに追加', favorite: 'いいね', unfavorite: 'いいね解除', removeFromPlaylist: 'プレイリストから削除', dislike: '嫌い', undislike: '嫌い解除' }, message: { downloading: 'ダウンロード中です。しばらくお待ちください...', downloadFailed: 'ダウンロードに失敗しました', downloadQueued: 'ダウンロードキューに追加しました', addedToNextPlay: '次の再生に追加しました', getUrlFailed: '音楽ダウンロードアドレスの取得に失敗しました。ログインしているか確認してください' }, dialog: { dislike: { title: 'お知らせ!', content: 'この楽曲を嫌いにしますか?再度アクセスすると毎日のおすすめから除外されます。', positiveText: '嫌い', negativeText: 'キャンセル' } } }; ================================================ FILE: src/i18n/lang/ja-JP/user.ts ================================================ export default { profile: { followers: 'フォロワー', following: 'フォロー中', level: 'レベル' }, playlist: { created: '作成したプレイリスト', mine: '私が作成した', trackCount: '{count}曲', playCount: '{count}回再生' }, tabs: { created: '作成', favorite: 'お気に入り', album: 'アルバム' }, ranking: { title: '聴取ランキング', playCount: '{count}回' }, follow: { title: 'フォローリスト', viewPlaylist: 'プレイリストを見る', noFollowings: 'フォローがありません', loadMore: 'さらに読み込み', noSignature: 'この人は怠け者で、何も残していません', userFollowsTitle: 'のフォロー', myFollowsTitle: '私のフォロー' }, follower: { title: 'フォロワーリスト', noFollowers: 'フォロワーがいません', loadMore: 'さらに読み込み', userFollowersTitle: 'のフォロワー', myFollowersTitle: '私のフォロワー' }, detail: { playlists: 'プレイリスト', records: '聴取ランキング', noPlaylists: 'プレイリストがありません', noRecords: '聴取記録がありません', artist: 'アーティスト', noSignature: 'この人は怠け者で、何も残していません', invalidUserId: '無効なユーザーID', noRecordPermission: '{name}は聴取ランキングを見せてくれません' }, message: { loadFailed: 'ユーザーページの読み込みに失敗しました', deleteSuccess: '削除成功', deleteFailed: '削除失敗' } }; ================================================ FILE: src/i18n/lang/ko-KR/artist.ts ================================================ export default { hotSongs: '인기 곡', albums: '앨범', description: '아티스트 소개' }; ================================================ FILE: src/i18n/lang/ko-KR/bilibili.ts ================================================ export default { player: { loading: '오디오 로딩 중...', retry: '다시 시도', playNow: '지금 재생', loadingTitle: '로딩 중...', totalDuration: '총 재생시간: {duration}', partsList: '파트 목록 ({count}화)', playStarted: '재생이 시작되었습니다', switchingPart: '파트 전환 중: {part}', preloadingNext: '다음 파트 미리 로딩 중: {part}', playingCurrent: '현재 선택된 파트 재생 중: {name}', num: '만', errors: { invalidVideoId: '유효하지 않은 비디오 ID', loadVideoDetailFailed: '비디오 세부정보 로드 실패', loadPartInfoFailed: '비디오 파트 정보를 로드할 수 없습니다', loadAudioUrlFailed: '오디오 재생 URL 가져오기 실패', videoDetailNotLoaded: '비디오 세부정보가 로드되지 않았습니다', missingParams: '필수 매개변수가 누락되었습니다', noAvailableAudioUrl: '사용 가능한 오디오 URL을 찾을 수 없습니다', loadPartAudioFailed: '파트 오디오 URL 로드 실패', audioListEmpty: '오디오 목록이 비어있습니다. 다시 시도해주세요', currentPartNotFound: '현재 파트의 오디오를 찾을 수 없습니다', audioUrlFailed: '오디오 URL 가져오기 실패', playFailed: '재생 실패. 다시 시도해주세요', getAudioUrlFailed: '오디오 URL 가져오기 실패. 다시 시도해주세요', audioNotFound: '해당 오디오를 찾을 수 없습니다. 다시 시도해주세요', preloadFailed: '다음 파트 미리 로딩 실패', switchPartFailed: '파트 전환 시 오디오 URL 로드 실패' }, console: { loadingDetail: 'Bilibili 비디오 세부정보 로딩 중', detailData: 'Bilibili 비디오 세부정보 데이터', multipleParts: '비디오에 여러 파트가 있습니다. 총 {count}개', noPartsData: '비디오에 파트가 없거나 파트 데이터가 비어있습니다', loadingAudioSource: '오디오 소스 로딩 중', generatedAudioList: '오디오 목록을 생성했습니다. 총 {count}개', getDashAudioUrl: 'dash 오디오 URL을 가져왔습니다', getDurlAudioUrl: 'durl 오디오 URL을 가져왔습니다', loadingPartAudio: '파트 오디오 URL 로딩 중: {part}, cid: {cid}', loadPartAudioFailed: '파트 오디오 URL 로드 실패: {part}', switchToPart: '파트로 전환 중: {part}', audioNotFoundInList: '해당 오디오 항목을 찾을 수 없습니다', preparingToPlay: '현재 선택된 파트 재생 준비 중: {name}', preloadingNextPart: '다음 파트 미리 로딩 중: {part}', playingSelectedPart: '현재 선택된 파트 재생 중: {name}, 오디오 URL: {url}', preloadNextFailed: '다음 파트 미리 로딩 실패' } } }; ================================================ FILE: src/i18n/lang/ko-KR/common.ts ================================================ export default { play: '재생', next: '다음 곡', previous: '이전 곡', volume: '볼륨', settings: '설정', search: '검색', loading: '로딩 중...', loadingMore: '더 불러오기...', alipay: '알리페이', wechat: '위챗 페이', on: '켜기', off: '끄기', show: '표시', hide: '숨기기', confirm: '확인', cancel: '취소', configure: '구성', open: '열기', modify: '수정', success: '작업 성공', error: '작업 실패', warning: '경고', info: '알림', save: '저장', delete: '삭제', refresh: '새로고침', retry: '다시 시도', reset: '재설정', back: '뒤로', copySuccess: '클립보드에 복사됨', copyFailed: '복사 실패', validation: { required: '이 항목은 필수입니다', invalidInput: '잘못된 입력', selectRequired: '옵션을 선택해주세요', numberRange: '{min}에서 {max} 사이의 숫자를 입력해주세요' }, viewMore: '더 보기', noMore: '더 이상 없음', selectAll: '전체 선택', expand: '펼치기', collapse: '접기', songCount: '{count}곡', language: '언어', today: '오늘', yesterday: '어제', tray: { show: '표시', quit: '종료', playPause: '재생/일시정지', prev: '이전 곡', next: '다음 곡', pause: '일시정지', play: '재생', favorite: '즐겨찾기' } }; ================================================ FILE: src/i18n/lang/ko-KR/comp.ts ================================================ export default { installApp: { description: '앱을 설치하여 더 나은 경험을 얻으세요', noPrompt: '다시 묻지 않기', install: '지금 설치', cancel: '나중에 설치', download: '다운로드', downloadFailed: '다운로드 실패', downloadComplete: '다운로드 완료', downloadProblem: '다운로드에 문제가 있나요?', downloadProblemLinkText: '최신 버전 다운로드' }, playlistDrawer: { title: '플레이리스트에 추가', createPlaylist: '새 플레이리스트 만들기', cancelCreate: '만들기 취소', create: '만들기', playlistName: '플레이리스트 이름', privatePlaylist: '비공개 플레이리스트', publicPlaylist: '공개 플레이리스트', createSuccess: '플레이리스트 생성 성공', createFailed: '플레이리스트 생성 실패', addSuccess: '곡 추가 성공', addFailed: '곡 추가 실패', private: '비공개', public: '공개', count: '곡', loginFirst: '먼저 로그인해주세요', getPlaylistFailed: '플레이리스트 가져오기 실패', inputPlaylistName: '플레이리스트 이름을 입력해주세요' }, update: { title: '새 버전 발견', currentVersion: '현재 버전', cancel: '나중에 업데이트', prepareDownload: '다운로드 준비 중...', downloading: '다운로드 중...', nowUpdate: '지금 업데이트', downloadFailed: '다운로드 실패, 다시 시도하거나 수동으로 다운로드해주세요', startFailed: '다운로드 시작 실패, 다시 시도하거나 수동으로 다운로드해주세요', noDownloadUrl: '현재 시스템에 적합한 설치 패키지를 찾을 수 없습니다. 수동으로 다운로드해주세요', installConfirmTitle: '업데이트 설치', installConfirmContent: '앱을 닫고 업데이트를 설치하시겠습니까?', manualInstallTip: '앱을 닫은 후 설치 프로그램이 정상적으로 나타나지 않으면 다운로드 폴더에서 파일을 찾아 수동으로 열어주세요.', yesInstall: '지금 설치', noThanks: '나중에 설치', fileLocation: '파일 위치', copy: '경로 복사', copySuccess: '경로가 클립보드에 복사됨', copyFailed: '복사 실패', backgroundDownload: '백그라운드 다운로드' }, disclaimer: { title: '이용 안내', warning: '본 앱은 개발 테스트 버전으로 기능이 아직 미흡하며, 다수의 문제와 버그가 존재할 수 있습니다. 학습 및 교류 목적으로만 사용하십시오.', item1: '본 앱은 개인의 학습, 연구 및 기술 교류 목적으로만 사용되며, 상업적 용도로 사용하지 마십시오.', item2: '다운로드 후 24시간 이내에 삭제해 주십시오. 장기 사용을 원하시면 정품 음악 서비스를 이용해 주십시오.', item3: '본 앱을 사용함으로써 관련 위험을 이해하고 감수하는 것으로 간주합니다. 개발자는 어떠한 손실에 대해서도 책임을 지지 않습니다.', agree: '숙지하였으며 이에 동의합니다', disagree: '동의하지 않음 및 정지' }, donate: { title: '개발자 지원', subtitle: '여러분의 지원이 저의 원동력입니다', tip: '후원은 완전히 자율적입니다. 후원하지 않더라도 모든 기능을 정상적으로 사용할 수 있습니다. 이해와 지원에 감사드립니다!', wechat: 'WeChat', alipay: 'Alipay', wechatQR: 'WeChat 결제 코드', alipayQR: 'Alipay 결제 코드', scanTip: '휴대전화로 위 QR 코드를 스캔하여 후원해 주세요', enterApp: '앱 시작하기', noForce: '후원은 강제가 아닙니다. 클릭하여 시작할 수 있습니다' }, coffee: { title: '커피 한 잔 사주세요', alipay: '알리페이', wechat: '위챗 페이', alipayQR: '알리페이 결제 QR코드', wechatQR: '위챗 결제 QR코드', coffeeDesc: '커피 한 잔, 하나의 지원', coffeeDescLinkText: '더 보기', groupText: '微信公众号:AlgerMusic', messages: { copySuccess: '클립보드에 복사됨' }, donateList: '커피 한 잔 사주세요' }, playlistType: { title: '플레이리스트 분류', showAll: '모두 표시', hide: '일부 숨기기' }, recommendAlbum: { title: '최신 앨범' }, recommendSinger: { title: '일일 추천', songlist: '일일 추천 목록' }, recommendSonglist: { title: '이번 주 인기 음악' }, searchBar: { login: '로그인', toLogin: '로그인하기', logout: '로그아웃', set: '설정', theme: '테마', restart: '재시작', refresh: '새로고침', currentVersion: '현재 버전', searchPlaceholder: '검색해보세요...', zoom: '페이지 확대/축소', zoom100: '표준 확대/축소 100%', resetZoom: '클릭하여 확대/축소 재설정', zoomDefault: '표준 확대/축소' }, titleBar: { closeTitle: '닫기 방법을 선택해주세요', minimizeToTray: '트레이로 최소화', exitApp: '앱 종료', rememberChoice: '선택 기억하기', closeApp: '앱 닫기' }, userPlayList: { title: '{name}의 자주 듣는 음악' }, musicList: { searchSongs: '곡 검색', noSearchResults: '관련 곡을 찾을 수 없습니다', switchToNormal: '기본 레이아웃으로 전환', switchToCompact: '컴팩트 레이아웃으로 전환', playAll: '모두 재생', collect: '수집', collectSuccess: '수집 성공', cancelCollectSuccess: '수집 취소 성공', operationFailed: '작업 실패', cancelCollect: '수집 취소', addToPlaylist: '재생 목록에 추가', addToPlaylistSuccess: '재생 목록에 추가 성공', songsAlreadyInPlaylist: '곡이 이미 재생 목록에 있습니다', historyRecommend: '일일 기록 권장', fetchDatesFailed: '날짜를 가져오지 못했습니다', fetchSongsFailed: '곡을 가져오지 못했습니다', noSongs: '노래 없음' }, playlist: { import: { button: '플레이리스트 가져오기', title: '플레이리스트 가져오기', description: '메타데이터/텍스트/링크 세 가지 방법으로 플레이리스트 가져오기 지원', linkTab: '링크 가져오기', textTab: '텍스트 가져오기', localTab: '메타데이터 가져오기', linkPlaceholder: '플레이리스트 링크를 입력하세요. 한 줄에 하나씩', textPlaceholder: '곡 정보를 입력하세요. 형식: 곡명 가수명', localPlaceholder: 'JSON 형식의 곡 메타데이터를 입력하세요', linkTips: '지원되는 링크 소스:', linkTip1: '플레이리스트를 위챗/웨이보/QQ로 공유한 후 링크 복사', linkTip2: '플레이리스트/개인 홈페이지 링크 직접 복사', linkTip3: '기사 링크 직접 복사', textTips: '곡 정보를 입력하세요. 한 줄에 한 곡씩', textFormat: '형식: 곡명 가수명', localTips: '곡 메타데이터를 추가해주세요', localFormat: '형식 예시:', songNamePlaceholder: '곡명', artistNamePlaceholder: '아티스트명', albumNamePlaceholder: '앨범명', addSongButton: '곡 추가', addLinkButton: '링크 추가', importToStarPlaylist: '내가 좋아하는 음악으로 가져오기', playlistNamePlaceholder: '플레이리스트 이름을 입력하세요', importButton: '가져오기 시작', emptyLinkWarning: '플레이리스트 링크를 입력해주세요', emptyTextWarning: '곡 정보를 입력해주세요', emptyLocalWarning: '곡 메타데이터를 입력해주세요', invalidJsonFormat: 'JSON 형식이 올바르지 않습니다', importSuccess: '가져오기 작업 생성 성공', importFailed: '가져오기 실패', importStatus: '가져오기 상태', refresh: '새로고침', taskId: '작업 ID', status: '상태', successCount: '성공 수', failReason: '실패 이유', unknownError: '알 수 없는 오류', statusPending: '처리 대기 중', statusProcessing: '처리 중', statusSuccess: '가져오기 성공', statusFailed: '가져오기 실패', statusUnknown: '알 수 없는 상태', taskList: '작업 목록', taskListTitle: '가져오기 작업 목록', action: '작업', select: '선택', fetchTaskListFailed: '작업 목록 가져오기 실패', noTasks: '가져오기 작업이 없습니다', clearTasks: '작업 지우기', clearTasksConfirmTitle: '지우기 확인', clearTasksConfirmContent: '모든 가져오기 작업 기록을 지우시겠습니까? 이 작업은 되돌릴 수 없습니다.', confirm: '확인', cancel: '취소', clearTasksSuccess: '작업 목록이 지워졌습니다', clearTasksFailed: '작업 목록 지우기 실패' } }, settings: '설정', user: '사용자', toplist: '순위', history: '수집 기록', list: '플레이리스트', mv: 'MV', home: '홈', search: '검색' }; ================================================ FILE: src/i18n/lang/ko-KR/donation.ts ================================================ export default { description: '귀하의 기부는 서버 유지보수, 도메인 갱신 등을 포함한 개발 및 유지보수 작업을 지원하는 데 사용됩니다.', message: '메시지를 남길 때 이메일이나 GitHub 이름을 남겨주세요.', refresh: '목록 새로고침', toDonateList: '커피 한 잔 사주세요', noMessage: '메시지가 없습니다', title: '기부 목록' }; ================================================ FILE: src/i18n/lang/ko-KR/download.ts ================================================ export default { title: '다운로드 관리', localMusic: '로컬 음악', count: '총 {count}곡', clearAll: '기록 지우기', settings: '설정', tabs: { downloading: '다운로드 중', downloaded: '다운로드 완료' }, empty: { noTasks: '다운로드 작업이 없습니다', noDownloaded: '다운로드된 곡이 없습니다' }, progress: { total: '전체 진행률: {progress}%' }, status: { downloading: '다운로드 중', completed: '완료', failed: '실패', unknown: '알 수 없음' }, artist: { unknown: '알 수 없는 가수' }, delete: { title: '삭제 확인', message: '곡 "{filename}"을(를) 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.', confirm: '삭제 확인', cancel: '취소', success: '삭제 성공', failed: '삭제 실패', fileNotFound: '파일이 존재하지 않거나 이동되었습니다. 기록에서 제거되었습니다', recordRemoved: '파일 삭제 실패, 하지만 기록에서 제거되었습니다' }, clear: { title: '다운로드 기록 지우기', message: '모든 다운로드 기록을 지우시겠습니까? 이 작업은 다운로드된 음악 파일을 삭제하지 않지만 모든 기록을 지웁니다.', confirm: '지우기 확인', cancel: '취소', success: '다운로드 기록이 지워졌습니다' }, message: { downloadComplete: '{filename} 다운로드 완료', downloadFailed: '{filename} 다운로드 실패: {error}' }, loading: '로딩 중...', playStarted: '재생 시작: {name}', playFailed: '재생 실패: {name}', path: { copied: '경로가 클립보드에 복사됨', copyFailed: '경로 복사 실패' }, settingsPanel: { title: '다운로드 설정', path: '다운로드 위치', pathDesc: '음악 파일 다운로드 저장 위치 설정', pathPlaceholder: '다운로드 경로를 선택해주세요', noPathSelected: '먼저 다운로드 경로를 선택해주세요', select: '폴더 선택', open: '폴더 열기', fileFormat: '파일명 형식', fileFormatDesc: '음악 다운로드 시 파일 이름 형식 설정', customFormat: '사용자 정의 형식', separator: '구분자', separators: { dash: '공백-공백', underscore: '밑줄', space: '공백' }, dragToArrange: '드래그하여 정렬하거나 화살표 버튼을 사용하여 순서 조정:', formatVariables: '사용 가능한 변수', preview: '미리보기 효과:', saveSuccess: '다운로드 설정이 저장됨', presets: { songArtist: '곡명 - 가수명', artistSong: '가수명 - 곡명', songOnly: '곡명만' }, components: { songName: '곡명', artistName: '가수명', albumName: '앨범명' } } }; ================================================ FILE: src/i18n/lang/ko-KR/favorite.ts ================================================ export default { title: '내 수집', count: '총 {count}곡', batchDownload: '일괄 다운로드', download: '다운로드 ({count})', emptyTip: '아직 수집한 곡이 없습니다', downloadSuccess: '다운로드 완료', downloadFailed: '다운로드 실패', downloading: '다운로드 중입니다. 잠시만 기다려주세요...', selectSongsFirst: '먼저 다운로드할 곡을 선택해주세요', descending: '내림차순', ascending: '오름차순' }; ================================================ FILE: src/i18n/lang/ko-KR/history.ts ================================================ export default { title: '재생 기록', heatmapTitle: '히트맵', playCount: '{count}', getHistoryFailed: '기록 가져오기 실패', tabs: { all: '전체 기록', local: '로컬 기록', cloud: '클라우드 기록' }, categoryTabs: { songs: '곡', playlists: '플레이리스트', albums: '앨범' }, noDescription: '설명 없음', noData: '기록 없음', getCloudRecordFailed: '클라우드 기록 가져오기 실패', needLogin: 'cookie를 사용하여 로그인하여 클라우드 기록을 볼 수 있습니다', merging: '기록 병합 중...', heatmap: { title: '재생 히트맵', loading: '데이터 로딩 중...', unit: '회 재생', footerText: '마우스를 올려서 자세히 보기', playCount: '{count}회 재생', topSongs: '오늘의 인기곡', times: '회', totalPlays: '총 재생 횟수', activeDays: '활동 일수', noData: '재생 기록이 없습니다', colorTheme: '색상 테마', colors: { green: '그린', blue: '블루', orange: '오렌지', purple: '퍼플', red: '레드' }, mostPlayedSong: '가장 많이 재생한 노래', mostActiveDay: '가장 활발한 날', latestNightSong: '가장 늘게 재생한 노래' } }; ================================================ FILE: src/i18n/lang/ko-KR/login.ts ================================================ export default { title: { qr: 'QR코드 로그인', phone: '휴대폰 번호 로그인', cookie: 'Cookie 로그인', uid: 'UID 로그인' }, qrTip: '넷이즈 클라우드 뮤직 앱으로 QR코드를 스캔하여 로그인', phoneTip: '넷이즈 클라우드 계정으로 로그인', tokenTip: '유효한 넷이즈 클라우드 뮤직 Cookie을 입력하여 로그인', uidTip: '사용자 ID를 입력하여 빠른 로그인', placeholder: { phone: '휴대폰 번호', password: '비밀번호', cookie: '넷이즈 클라우드 뮤직 Cookie(token)을 입력하세요', uid: '사용자 ID(UID)를 입력하세요' }, button: { login: '로그인', switchToQr: 'QR코드 로그인', switchToPhone: '휴대폰 번호 로그인', switchToToken: 'Cookie 로그인 사용', switchToUid: 'UID 로그인', backToQr: 'QR코드 로그인으로 돌아가기', cookieLogin: 'Cookie 로그인', autoGetCookie: 'Cookie 자동 가져오기', refresh: '새로고침', refreshing: '새로고침 중...', refreshQr: 'QR코드 새로고침' }, message: { loginSuccess: '로그인 성공', tokenLoginSuccess: 'Cookie 로그인 성공', uidLoginSuccess: 'UID 로그인 성공', loadError: '로그인 정보 로드 중 오류 발생', qrCheckError: 'QR코드 상태 확인 중 오류 발생', tokenRequired: 'Cookie을 입력하세요', tokenInvalid: 'Cookie이 유효하지 않습니다. 확인 후 다시 시도하세요', uidRequired: '사용자 ID를 입력하세요', uidInvalid: '사용자 ID가 유효하지 않거나 사용자가 존재하지 않습니다', uidLoginFailed: 'UID 로그인에 실패했습니다. 사용자 ID가 올바른지 확인하세요', autoGetCookieSuccess: 'Cookie 자동 가져오기 성공', autoGetCookieFailed: 'Cookie 자동 가져오기 실패', autoGetCookieTip: '넷이즈 클라우드 뮤직 로그인 페이지를 열겠습니다. 로그인 완료 후 창을 닫아주세요', loginFailed: '로그인 실패', phoneRequired: '휴대폰 번호를 입력하세요', passwordRequired: '비밀번호를 입력하세요', phoneLoginFailed: '휴대폰 번호 로그인 실패, 휴대폰 번호와 비밀번호가 올바른지 확인하세요', qrCheckFailed: 'QR코드 상태 확인 실패, 새로고침하여 다시 시도하세요', qrLoading: 'QR코드 로딩 중...', qrExpired: 'QR코드가 만료되었습니다. 클릭하여 새로고침하세요', qrExpiredShort: 'QR코드 만료됨', qrExpiredWarning: 'QR코드가 만료되었습니다. 클릭하여 새로운 QR코드를 받으세요', qrScanned: 'QR코드가 스캔되었습니다. 휴대폰에서 로그인을 확인하세요', qrScannedShort: '스캔됨', qrScannedInfo: 'QR코드가 스캔되었습니다. 휴대폰에서 로그인을 확인하세요', qrConfirmed: '로그인 성공, 리다이렉트 중...', qrGenerating: 'QR코드를 생성 중...' }, qrTitle: '넷이즈 클라우드 뮤직 QR코드 로그인', uidWarning: '주의: UID 로그인은 사용자 공개 정보를 확인하는 데만 사용할 수 있으며, 로그인 권한이 필요한 기능에 액세스할 수 없습니다.' }; ================================================ FILE: src/i18n/lang/ko-KR/player.ts ================================================ export default { nowPlaying: '현재 재생 중', playlist: '재생 목록', lyrics: '가사', previous: '이전', play: '재생', pause: '일시정지', next: '다음', volumeUp: '볼륨 증가', volumeDown: '볼륨 감소', mute: '음소거', unmute: '음소거 해제', songNum: '총 곡 수: {num}', addCorrection: '{num}초 앞당기기', subtractCorrection: '{num}초 지연', playFailed: '현재 곡 재생 실패, 다음 곡 재생', parseFailedPlayNext: '곡 분석 실패, 다음 곡 재생', consecutiveFailsError: '재생 오류가 발생했습니다. 네트워크 문제 또는 유효하지 않은 음원일 수 있습니다. 재생 목록을 변경하거나 나중에 다시 시도하세요', playMode: { sequence: '순차 재생', loop: '한 곡 반복', random: '랜덤 재생' }, fullscreen: { enter: '전체화면', exit: '전체화면 종료' }, close: '닫기', modeHint: { single: '한 곡 반복', list: '자동으로 다음 곡 재생' }, lrc: { noLrc: '가사가 없습니다. 음악을 감상해주세요', noAutoScroll: '본 가사는 자동 스크롤을 지원하지 않습니다' }, reparse: { title: '음원 선택', desc: '음원을 클릭하여 직접 분석하세요. 다음에 이 곡을 재생할 때 선택한 음원을 사용합니다', success: '재분석 성공', failed: '재분석 실패', warning: '음원을 선택해주세요', bilibiliNotSupported: 'B站 비디오는 재분석을 지원하지 않습니다', processing: '분석 중...', clear: '사용자 정의 음원 지우기', customApiFailed: '사용자 정의 API 분석 실패, 기본 음원을 시도합니다...', customApiError: '사용자 정의 API 요청 오류, 기본 음원을 시도합니다...' }, playBar: { expand: '가사 펼치기', collapse: '가사 접기', like: '좋아요', lyric: '가사', noSongPlaying: '재생 중인 곡이 없습니다', eq: '이퀄라이저', playList: '재생 목록', reparse: '재분석', playMode: { sequence: '순차 재생', loop: '반복 재생', random: '랜덤 재생' }, play: '재생 시작', pause: '재생 일시정지', prev: '이전 곡', next: '다음 곡', volume: '볼륨', favorite: '{name} 즐겨찾기 추가됨', unFavorite: '{name} 즐겨찾기 해제됨', miniPlayBar: '미니 재생바', playbackSpeed: '재생 속도', advancedControls: '고급 설정', intelligenceMode: { title: '인텔리전스 모드', needCookieLogin: '쿠키 방식으로 로그인한 후 인텔리전스 모드를 사용할 수 있습니다', noFavoritePlaylist: '내가 좋아하는 음악 재생목록을 찾을 수 없습니다', noLikedSongs: '아직 좋아한 노래가 없습니다', loading: '인텔리전스 모드를 불러오는 중', success: '총 {count}곡을 불러왔습니다', failed: '인텔리전스 모드 목록을 가져오는 데 실패했습니다', error: '인텔리전스 모드 재생 오류' } }, eq: { title: '이퀄라이저', reset: '재설정', on: '켜기', off: '끄기', bass: '저음', midrange: '중음', treble: '고음', presets: { flat: '플랫', pop: '팝', rock: '록', classical: '클래식', jazz: '재즈', electronic: '일렉트로닉', hiphop: '힙합', rb: 'R&B', metal: '메탈', vocal: '보컬', dance: '댄스', acoustic: '어쿠스틱', custom: '사용자 정의' } }, // 플레이어 설정 settings: { title: '재생 설정', playbackSpeed: '재생 속도' }, sleepTimer: { title: '타이머 종료', cancel: '타이머 취소', timeMode: '시간으로 종료', songsMode: '곡 수로 종료', playlistEnd: '재생 목록 완료 후 종료', afterPlaylist: '재생 목록 완료 후 종료', activeUntilEnd: '목록 끝까지 재생', minutes: '분', hours: '시간', songs: '곡', set: '설정', timerSetSuccess: '{minutes}분 후 종료로 설정됨', songsSetSuccess: '{songs}곡 재생 후 종료로 설정됨', playlistEndSetSuccess: '재생 목록 완료 후 종료로 설정됨', timerCancelled: '타이머 종료 취소됨', timerEnded: '타이머 종료 실행됨', playbackStopped: '음악 재생이 중지됨', minutesRemaining: '남은 시간 {minutes}분', songsRemaining: '남은 곡 수 {count}곡' }, playList: { clearAll: '재생 목록 비우기', alreadyEmpty: '재생 목록이 이미 비어있습니다', cleared: '재생 목록이 비워졌습니다', empty: '재생 목록이 비어있습니다', clearConfirmTitle: '재생 목록 비우기', clearConfirmContent: '재생 목록의 모든 곡을 삭제하고 현재 재생을 중지합니다. 계속하시겠습니까?' } }; ================================================ FILE: src/i18n/lang/ko-KR/search.ts ================================================ export default { title: { hotSearch: '인기 검색', searchList: '검색 목록', searchHistory: '검색 기록' }, button: { clear: '지우기', back: '뒤로', playAll: '재생 목록' }, loading: { more: '로딩 중...', failed: '검색 실패', searching: '검색 중...' }, noMore: '더 이상 없음', error: { searchFailed: '검색 실패' }, search: { single: '단일곡', album: '앨범', playlist: '플레이리스트', mv: 'MV', bilibili: 'B站' }, history: '검색 기록', hot: '인기 검색', suggestions: '검색 제안' }; ================================================ FILE: src/i18n/lang/ko-KR/settings.ts ================================================ export default { theme: '테마', language: '언어', regard: '정보', logout: '로그아웃', sections: { basic: '기본 설정', playback: '재생 설정', application: '애플리케이션 설정', network: '네트워크 설정', system: '시스템 관리', donation: '후원 지원', about: '정보' }, basic: { themeMode: '테마 모드', themeModeDesc: '낮/밤 테마 전환', autoTheme: '시스템 따라가기', manualTheme: '수동 전환', language: '언어 설정', languageDesc: '표시 언어 전환', tokenManagement: 'Cookie 관리', tokenManagementDesc: '넷이즈 클라우드 뮤직 로그인 Cookie 관리', tokenStatus: '현재 Cookie 상태', tokenSet: '설정됨', tokenNotSet: '설정되지 않음', setToken: 'Cookie 설정', modifyToken: 'Cookie 수정', clearToken: 'Cookie 지우기', font: '폰트 설정', fontDesc: '폰트 선택, 앞에 있는 폰트를 우선 사용', fontScope: { global: '전역', lyric: '가사만' }, animation: '애니메이션 속도', animationDesc: '애니메이션 활성화 여부', animationSpeed: { slow: '매우 느림', normal: '보통', fast: '매우 빠름' }, fontPreview: { title: '폰트 미리보기', chinese: '中文', english: 'English', japanese: '日本語', korean: '한국어', chineseText: '静夜思 床前明月光 疑是地上霜', englishText: 'The quick brown fox jumps over the lazy dog', japaneseText: 'あいうえお かきくけこ さしすせそ', koreanText: '가나다라마 바사아자차 카타파하' }, gpuAcceleration: 'GPU 가속', gpuAccelerationDesc: 'GPU 가속을 사용하면 애니메이션이 빠르게 재생되고 애니메이션이 느리게 재생되는 것보다 느릴 수 있습니다.', gpuAccelerationRestart: 'GPU 가속 설정을 변경하면 애플리케이션을 다시 시작해야 합니다', gpuAccelerationChangeSuccess: 'GPU 가속 설정이 업데이트되었습니다. 애플리케이션을 다시 시작하여 적용하십시오', gpuAccelerationChangeError: 'GPU 가속 설정 업데이트에 실패했습니다', tabletMode: '태블릿 모드', tabletModeDesc: '태블릿 모드를 사용하면 모바일 기기에서 PC 스타일의 인터페이스를 사용할 수 있습니다' }, playback: { quality: '음질 설정', qualityDesc: '음악 재생 음질 선택 (넷이즈 클라우드 VIP)', qualityOptions: { standard: '표준', higher: '높음', exhigh: '매우 높음', lossless: '무손실', hires: 'Hi-Res', jyeffect: 'HD 서라운드', sky: '몰입형 서라운드', dolby: '돌비 애트모스', jymaster: '초고화질 마스터' }, musicSources: '음원 설정', musicSourcesDesc: '음악 해석에 사용할 음원 플랫폼 선택', musicSourcesWarning: '최소 하나의 음원 플랫폼을 선택해야 합니다', musicUnblockEnable: '음악 해석 활성화', musicUnblockEnableDesc: '활성화하면 재생할 수 없는 음악을 해석하려고 시도합니다', configureMusicSources: '음원 구성', selectedMusicSources: '선택된 음원:', noMusicSources: '음원이 선택되지 않음', gdmusicInfo: 'GD 뮤직은 여러 플랫폼 음원을 자동으로 해석하고 최적의 결과를 자동 선택합니다', autoPlay: '자동 재생', autoPlayDesc: '앱을 다시 열 때 자동으로 재생을 계속할지 여부', showStatusBar: '상태바 제어 기능 표시 여부', showStatusBarContent: 'Mac 상태바에 음악 제어 기능을 표시할 수 있습니다 (재시작 후 적용)', fallbackParser: '대체 분석 서비스 (GD Music)', fallbackParserDesc: '"GD Music"을 선택하고 일반 음원을 사용할 수 없을 때 이 서비스를 사용합니다.', parserGD: 'GD Music (내장)', parserCustom: '사용자 지정 API', // 음원 라벨 sourceLabels: { migu: 'Migu', kugou: 'Kugou', pyncmd: 'NetEase (내장)', bilibili: 'Bilibili', gdmusic: 'GD Music', custom: '사용자 지정 API' }, customApi: { sectionTitle: '사용자 지정 API 설정', importConfig: 'JSON 설정 가져오기', currentSource: '현재 음원', notImported: '아직 사용자 지정 음원을 가져오지 않았습니다.', importSuccess: '음원 가져오기 성공: {name}', importFailed: '가져오기 실패: {message}', enableHint: '사용하려면 먼저 JSON 구성 파일을 가져오세요', status: { imported: '사용자 지정 음원 가져옴', notImported: '가져오지 않음' } }, lxMusic: { tabs: { sources: '음원 선택', lxMusic: '낙설 음원', customApi: '사용자 정의 API' }, scripts: { title: '가져온 스크립트', importLocal: '로컬 가져오기', importOnline: '온라인 가져오기', urlPlaceholder: '낙설 음원 스크립트 URL 입력', importBtn: '가져오기', empty: '가져온 낙설 음원이 없습니다', notConfigured: '설정되지 않음 (낙설 음원 탭에서 설정하세요)', importHint: '소스 확장을 위해 호환되는 사용자 정의 API 플러그인을 가져옵니다', noScriptWarning: '먼저 낙설 음원 스크립트를 가져오세요', noSelectionWarning: '먼저 낙설 음원 소스를 선택하세요', notFound: '음원이 존재하지 않습니다', switched: '음원으로 전환되었습니다: {name}', deleted: '음원이 삭제되었습니다: {name}', enterUrl: '스크립트 URL을 입력하세요', invalidUrl: '유효하지 않은 URL 형식', invalidScript: '유효하지 않은 낙설 음원 스크립트입니다 (globalThis.lx 코드를 찾을 수 없음)', nameRequired: '이름은 비워둘 수 없습니다', renameSuccess: '이름이 변경되었습니다' } } }, application: { closeAction: '닫기 동작', closeActionDesc: '창을 닫을 때의 동작 선택', closeOptions: { ask: '매번 묻기', minimize: '트레이로 최소화', close: '직접 종료' }, shortcut: '단축키 설정', shortcutDesc: '전역 단축키 사용자 정의', download: '다운로드 관리', downloadDesc: '다운로드 목록 버튼을 항상 표시할지 여부', unlimitedDownload: '무제한 다운로드', unlimitedDownloadDesc: '활성화하면 음악을 무제한으로 다운로드합니다 (다운로드 실패가 발생할 수 있음), 기본 제한 300곡', downloadPath: '다운로드 디렉토리', downloadPathDesc: '음악 파일의 다운로드 위치 선택', remoteControl: '원격 제어', remoteControlDesc: '원격 제어 기능 설정' }, network: { apiPort: '음악 API 포트', apiPortDesc: '수정 후 앱을 재시작해야 합니다', proxy: '프록시 설정', proxyDesc: '음악에 액세스할 수 없을 때 프록시를 활성화할 수 있습니다', proxyHost: '프록시 주소', proxyHostPlaceholder: '프록시 주소를 입력하세요', proxyPort: '프록시 포트', proxyPortPlaceholder: '프록시 포트를 입력하세요', realIP: 'realIP 설정', realIPDesc: '제한으로 인해 이 프로젝트는 해외에서 사용할 때 제한을 받을 수 있으며, realIP 매개변수를 사용하여 국내 IP를 전달하여 해결할 수 있습니다', messages: { proxySuccess: '프록시 설정이 저장되었습니다. 앱을 재시작한 후 적용됩니다', proxyError: '입력이 올바른지 확인하세요', realIPSuccess: '실제 IP 설정이 저장되었습니다', realIPError: '유효한 IP 주소를 입력하세요' } }, system: { cache: '캐시 관리', cacheDesc: '캐시 지우기', cacheClearTitle: '지울 캐시 유형을 선택하세요:', cacheTypes: { history: { label: '재생 기록', description: '재생한 곡 기록 지우기' }, favorite: { label: '즐겨찾기 기록', description: '로컬 즐겨찾기 곡 기록 지우기 (클라우드 즐겨찾기에는 영향 없음)' }, user: { label: '사용자 데이터', description: '로그인 정보 및 사용자 관련 데이터 지우기' }, settings: { label: '앱 설정', description: '앱의 모든 사용자 정의 설정 지우기' }, downloads: { label: '다운로드 기록', description: '다운로드 기록 지우기 (다운로드된 파일은 삭제되지 않음)' }, resources: { label: '음악 리소스', description: '로드된 음악 파일, 가사 등 리소스 캐시 지우기' }, lyrics: { label: '가사 리소스', description: '로드된 가사 리소스 캐시 지우기' } }, restart: '재시작', restartDesc: '앱 재시작', messages: { clearSuccess: '지우기 성공, 일부 설정은 재시작 후 적용됩니다' } }, about: { version: '버전', checkUpdate: '업데이트 확인', checking: '확인 중...', latest: '현재 최신 버전입니다', hasUpdate: '새 버전 발견', gotoUpdate: '업데이트하러 가기', gotoGithub: 'Github로 이동', author: '작성자', authorDesc: 'algerkong 별점🌟 부탁드려요', messages: { checkError: '업데이트 확인 실패, 나중에 다시 시도하세요' } }, validation: { selectProxyProtocol: '프록시 프로토콜을 선택하세요', proxyHost: '프록시 주소를 입력하세요', portNumber: '유효한 포트 번호를 입력하세요 (1-65535)' }, lyricSettings: { title: '가사 설정', tabs: { display: '표시', interface: '인터페이스', typography: '텍스트', background: '배경', mobile: '모바일' }, pureMode: '순수 모드', hideCover: '커버 숨기기', centerDisplay: '중앙 표시', showTranslation: '번역 표시', hideLyrics: '가사 숨기기', hidePlayBar: '재생바 숨기기', hideMiniPlayBar: '미니 재생바 숨기기', showMiniPlayBar: '미니 재생바 표시', backgroundTheme: '배경 테마', themeOptions: { default: '기본', light: '밝음', dark: '어둠' }, fontSize: '폰트 크기', fontSizeMarks: { small: '작음', medium: '중간', large: '큼' }, fontWeight: '글꼴 두께', fontWeightMarks: { thin: '가늘게', normal: '보통', bold: '굵게' }, letterSpacing: '글자 간격', letterSpacingMarks: { compact: '좁음', default: '기본', loose: '넓음' }, lineHeight: '줄 높이', lineHeightMarks: { compact: '좁음', default: '기본', loose: '넓음' }, contentWidth: '콘텐츠 너비', mobileLayout: '모바일 레이아웃', layoutOptions: { default: '기본', ios: 'iOS 스타일', android: '안드로이드 스타일' }, mobileCoverStyle: '커버 스타일', coverOptions: { record: '레코드', square: '정사각형', full: '전체화면' }, lyricLines: '가사 줄 수', mobileUnavailable: '이 설정은 모바일에서만 사용 가능합니다', // 배경 설정 background: { useCustomBackground: '사용자 정의 배경 사용', backgroundMode: '배경 모드', modeOptions: { solid: '단색', gradient: '그라데이션', image: '이미지', css: 'CSS' }, solidColor: '색상 선택', presetColors: '프리셋 색상', customColor: '사용자 정의 색상', gradientEditor: '그라데이션 편집기', gradientColors: '그라데이션 색상', gradientDirection: '그라데이션 방향', directionOptions: { toBottom: '위에서 아래로', toRight: '왼쪽에서 오른쪽으로', toBottomRight: '왼쪽 위에서 오른쪽 아래로', angle45: '45도', toTop: '아래에서 위로', toLeft: '오른쪽에서 왼쪽으로' }, addColor: '색상 추가', removeColor: '색상 제거', imageUpload: '이미지 업로드', imagePreview: '이미지 미리보기', clearImage: '이미지 지우기', imageBlur: '흐림', imageBrightness: '밝기', customCss: '사용자 정의 CSS 스타일', customCssPlaceholder: 'CSS 스타일 입력, 예: background: linear-gradient(...)', customCssHelp: '모든 CSS background 속성 지원', reset: '기본값으로 재설정', fileSizeLimit: '이미지 크기 제한: 20MB', invalidImageFormat: '잘못된 이미지 형식', imageTooLarge: '이미지가 너무 큽니다. 20MB 미만의 이미지를 선택하세요' } }, translationEngine: '가사 번역 엔진', translationEngineOptions: { none: '닫기', opencc: 'OpenCC 중국어 번체' }, themeColor: { title: '가사 테마 색상', presetColors: '미리 설정된 색상', customColor: '사용자 정의 색상', preview: '미리보기 효과', previewText: '가사 효과', colorNames: { 'spotify-green': 'Spotify 그린', 'apple-blue': '애플 블루', 'youtube-red': 'YouTube 레드', orange: '활력 오렌지', purple: '신비 퍼플', pink: '벚꽃 핑크' }, tooltips: { openColorPicker: '색상 선택기 열기', closeColorPicker: '색상 선택기 닫기' }, placeholder: '#1db954' }, shortcutSettings: { title: '단축키 설정', shortcut: '단축키', shortcutDesc: '단축키 사용자 정의', shortcutConflict: '단축키 충돌', inputPlaceholder: '클릭하여 단축키 입력', resetShortcuts: '기본값 복원', disableAll: '모두 비활성화', enableAll: '모두 활성화', togglePlay: '재생/일시정지', prevPlay: '이전 곡', nextPlay: '다음 곡', volumeUp: '볼륨 증가', volumeDown: '볼륨 감소', toggleFavorite: '즐겨찾기/즐겨찾기 취소', toggleWindow: '창 표시/숨기기', scopeGlobal: '전역', scopeApp: '앱 내', enabled: '활성화', disabled: '비활성화', messages: { resetSuccess: '기본 단축키로 복원되었습니다. 저장을 잊지 마세요', conflict: '충돌하는 단축키가 있습니다. 다시 설정하세요', saveSuccess: '단축키 설정이 저장되었습니다', saveError: '단축키 저장 실패, 다시 시도하세요', cancelEdit: '수정이 취소되었습니다', disableAll: '모든 단축키가 비활성화되었습니다. 저장을 잊지 마세요', enableAll: '모든 단축키가 활성화되었습니다. 저장을 잊지 마세요' } }, remoteControl: { title: '원격 제어', enable: '원격 제어 활성화', port: '서비스 포트', allowedIps: '허용된 IP 주소', addIp: 'IP 추가', emptyListHint: '빈 목록은 모든 IP 액세스를 허용함을 의미합니다', saveSuccess: '원격 제어 설정이 저장되었습니다', accessInfo: '원격 제어 액세스 주소:' }, cookie: { title: 'Cookie 설정', description: '넷이즈 클라우드 뮤직의 Cookie를 입력하세요:', placeholder: '완전한 Cookie를 붙여넣으세요...', help: { format: 'Cookie는 일반적으로 "MUSIC_U="로 시작합니다', source: '브라우저 개발자 도구의 네트워크 요청에서 얻을 수 있습니다', storage: 'Cookie 설정 후 자동으로 로컬 저장소에 저장됩니다' }, action: { save: 'Cookie 저장', paste: '붙여넣기', clear: '지우기' }, validation: { required: 'Cookie를 입력하세요', format: 'Cookie 형식이 올바르지 않을 수 있습니다. MUSIC_U가 포함되어 있는지 확인하세요' }, message: { saveSuccess: 'Cookie 저장 성공', saveError: 'Cookie 저장 실패', pasteSuccess: '붙여넣기 성공', pasteError: '붙여넣기 실패, 수동으로 복사하세요' }, info: { length: '현재 길이: {length} 문자' } } }; ================================================ FILE: src/i18n/lang/ko-KR/songItem.ts ================================================ export default { menu: { play: '재생', playNext: '다음에 재생', download: '곡 다운로드', addToPlaylist: '플레이리스트에 추가', favorite: '좋아요', unfavorite: '좋아요 취소', removeFromPlaylist: '플레이리스트에서 삭제', dislike: '싫어요', undislike: '싫어요 취소' }, message: { downloading: '다운로드 중입니다. 잠시 기다려주세요...', downloadFailed: '다운로드 실패', downloadQueued: '다운로드 대기열에 추가됨', addedToNextPlay: '다음 재생에 추가됨', getUrlFailed: '음악 다운로드 주소 가져오기 실패, 로그인 상태를 확인하세요' }, dialog: { dislike: { title: '알림!', content: '이 곡을 싫어한다고 확인하시겠습니까? 다시 들어가면 일일 추천에서 제외됩니다.', positiveText: '싫어요', negativeText: '취소' } } }; ================================================ FILE: src/i18n/lang/ko-KR/user.ts ================================================ export default { profile: { followers: '팔로워', following: '팔로잉', level: '레벨' }, playlist: { created: '생성한 플레이리스트', mine: '내가 만든', trackCount: '{count}곡', playCount: '{count}회 재생' }, tabs: { created: '생성', favorite: '즐겨찾기', album: '앨범' }, ranking: { title: '음악 청취 순위', playCount: '{count}회' }, follow: { title: '팔로잉 목록', viewPlaylist: '플레이리스트 보기', noFollowings: '팔로잉이 없습니다', loadMore: '더 보기', noSignature: '이 사람은 게을러서 아무것도 남기지 않았습니다', userFollowsTitle: '의 팔로잉', myFollowsTitle: '내 팔로잉' }, follower: { title: '팔로워 목록', noFollowers: '팔로워가 없습니다', loadMore: '더 보기', userFollowersTitle: '의 팔로워', myFollowersTitle: '내 팔로워' }, detail: { playlists: '플레이리스트', records: '음악 청취 순위', noPlaylists: '플레이리스트가 없습니다', noRecords: '음악 청취 기록이 없습니다', artist: '아티스트', noSignature: '이 사람은 게을러서 아무것도 남기지 않았습니다', invalidUserId: '사용자 ID가 유효하지 않습니다', noRecordPermission: '{name}님이 음악 청취 순위를 보지 못하게 했습니다' }, message: { loadFailed: '사용자 페이지 로드 실패', deleteSuccess: '삭제 성공', deleteFailed: '삭제 실패' } }; ================================================ FILE: src/i18n/lang/zh-CN/artist.ts ================================================ export default { hotSongs: '热门歌曲', albums: '专辑', description: '艺人介绍' }; ================================================ FILE: src/i18n/lang/zh-CN/bilibili.ts ================================================ export default { player: { loading: '听书加载中...', retry: '重试', playNow: '立即播放', loadingTitle: '加载中...', totalDuration: '总时长: {duration}', partsList: '分P列表 (共{count}集)', playStarted: '已开始播放', switchingPart: '切换到分P: {part}', preloadingNext: '预加载下一个分P: {part}', playingCurrent: '播放当前选中的分P: {name}', num: '万', errors: { invalidVideoId: '视频ID无效', loadVideoDetailFailed: '获取视频详情失败', loadPartInfoFailed: '无法加载视频分P信息', loadAudioUrlFailed: '获取音频播放地址失败', videoDetailNotLoaded: '视频详情未加载', missingParams: '缺少必要参数', noAvailableAudioUrl: '未找到可用的音频地址', loadPartAudioFailed: '加载分P音频URL失败', audioListEmpty: '音频列表为空,请重试', currentPartNotFound: '未找到当前分P的音频', audioUrlFailed: '获取音频URL失败', playFailed: '播放失败,请重试', getAudioUrlFailed: '获取音频地址失败,请重试', audioNotFound: '未找到对应的音频,请重试', preloadFailed: '预加载下一个分P失败', switchPartFailed: '切换分P时加载音频URL失败' }, console: { loadingDetail: '加载B站视频详情', detailData: 'B站视频详情数据', multipleParts: '视频有多个分P,共{count}个', noPartsData: '视频无分P或分P数据为空', loadingAudioSource: '加载音频源', generatedAudioList: '已生成音频列表,共{count}首', getDashAudioUrl: '获取到dash音频URL', getDurlAudioUrl: '获取到durl音频URL', loadingPartAudio: '加载分P音频URL: {part}, cid: {cid}', loadPartAudioFailed: '加载分P音频URL失败: {part}', switchToPart: '切换到分P: {part}', audioNotFoundInList: '未找到对应的音频项', preparingToPlay: '准备播放当前选中的分P: {name}', preloadingNextPart: '预加载下一个分P: {part}', playingSelectedPart: '播放当前选中的分P: {name},音频URL: {url}', preloadNextFailed: '预加载下一个分P失败' } } }; ================================================ FILE: src/i18n/lang/zh-CN/common.ts ================================================ export default { play: '播放', next: '下一首', previous: '上一首', volume: '音量', settings: '设置', search: '搜索', loading: '加载中...', loadingMore: '加载更多...', alipay: '支付宝', wechat: '微信支付', on: '开启', off: '关闭', show: '显示', hide: '隐藏', confirm: '确认', cancel: '取消', configure: '配置', open: '打开', modify: '修改', success: '操作成功', error: '操作失败', warning: '警告', info: '提示', save: '保存', delete: '删除', refresh: '刷新', retry: '重试', reset: '重置', back: '返回', copySuccess: '已复制到剪贴板', copyFailed: '复制失败', validation: { required: '此项是必填的', invalidInput: '输入无效', selectRequired: '请选择一个选项', numberRange: '请输入 {min} 到 {max} 之间的数字' }, viewMore: '查看更多', noMore: '没有更多了', selectAll: '全选', expand: '展开', collapse: '收起', songCount: '{count}首', language: '语言', today: '今天', yesterday: '昨天', tray: { show: '显示', quit: '退出', playPause: '播放/暂停', prev: '上一首', next: '下一首', pause: '暂停', play: '播放', favorite: '收藏' } }; ================================================ FILE: src/i18n/lang/zh-CN/comp.ts ================================================ export default { installApp: { description: '安装应用程序,获得更好的体验', noPrompt: '不再提示', install: '立即安装', cancel: '暂不安装', download: '下载', downloadFailed: '下载失败', downloadComplete: '下载完成', downloadProblem: '下载遇到问题?去', downloadProblemLinkText: '下载最新版本' }, playlistDrawer: { title: '添加到歌单', createPlaylist: '创建新歌单', cancelCreate: '取消创建', create: '创建', playlistName: '歌单名称', privatePlaylist: '私密歌单', publicPlaylist: '公开歌单', createSuccess: '歌单创建成功', createFailed: '歌单创建失败', addSuccess: '歌曲添加成功', addFailed: '歌曲添加失败', private: '私密', public: '公开', count: '首歌曲', loginFirst: '请先登录', getPlaylistFailed: '获取歌单失败', inputPlaylistName: '请输入歌单名称' }, update: { title: '发现新版本', currentVersion: '当前版本', cancel: '暂不更新', prepareDownload: '准备下载...', downloading: '下载中...', nowUpdate: '立即更新', downloadFailed: '下载失败,请重试或手动下载', startFailed: '启动下载失败,请重试或手动下载', noDownloadUrl: '未找到适合当前系统的安装包,请手动下载', installConfirmTitle: '安装更新', installConfirmContent: '是否关闭应用并安装更新?', manualInstallTip: '如果关闭应用后没有正常弹出安装程序,请至下载文件夹查找文件并手动打开。', yesInstall: '立即安装', noThanks: '稍后安装', fileLocation: '文件位置', copy: '复制路径', copySuccess: '路径已复制到剪贴板', copyFailed: '复制失败', backgroundDownload: '后台下载' }, disclaimer: { title: '使用须知', warning: '本应用为开发测试版本,功能尚不完善,可能存在较多问题和 Bug,仅供学习交流使用。', item1: '本应用仅供个人学习、研究和技术交流使用,请勿用于任何商业用途。', item2: '请在下载后 24 小时内删除,如需长期使用请支持正版音乐服务。', item3: '使用本应用即表示您理解并承担相关风险,开发者不对任何损失负责。', agree: '我已阅读并同意', disagree: '不同意并退出' }, donate: { title: '支持开发者', subtitle: '您的支持是我前进的动力', tip: '捐赠完全自愿,不捐赠也可以正常使用所有功能,感谢您的理解与支持!', wechat: '微信', alipay: '支付宝', wechatQR: '微信收款码', alipayQR: '支付宝收款码', scanTip: '请使用手机扫描上方二维码进行捐赠', enterApp: '进入应用', noForce: '不强制捐赠,点击即可进入' }, coffee: { title: '请我喝咖啡', alipay: '支付宝', wechat: '微信支付', alipayQR: '支付宝收款码', wechatQR: '微信收款码', coffeeDesc: '一杯咖啡,一份支持', coffeeDescLinkText: '查看更多', groupText: '微信公众号:AlgerMusic', messages: { copySuccess: '已复制到剪贴板' }, donateList: '请我喝咖啡' }, playlistType: { title: '歌单分类', showAll: '显示全部', hide: '隐藏一些' }, recommendAlbum: { title: '最新专辑' }, recommendSinger: { title: '每日推荐', songlist: '每日推荐列表' }, recommendSonglist: { title: '本周最热音乐' }, searchBar: { login: '登录', toLogin: '去登录', logout: '退出登录', set: '设置', theme: '主题', restart: '重启', refresh: '刷新', currentVersion: '当前版本', searchPlaceholder: '搜索点什么吧...', zoom: '页面缩放', zoom100: '标准缩放100%', resetZoom: '点击重置缩放', zoomDefault: '标准缩放' }, titleBar: { closeTitle: '请选择关闭方式', minimizeToTray: '最小化到托盘', exitApp: '退出应用', rememberChoice: '记住我的选择', closeApp: '关闭应用' }, userPlayList: { title: '{name}的常听' }, musicList: { searchSongs: '搜索歌曲', noSearchResults: '没有找到相关歌曲', switchToNormal: '切换到默认布局', switchToCompact: '切换到紧凑布局', playAll: '播放全部', collect: '收藏', collectSuccess: '收藏成功', cancelCollectSuccess: '取消收藏成功', operationFailed: '操作失败', cancelCollect: '取消收藏', addToPlaylist: '添加到播放列表', addToPlaylistSuccess: '添加到播放列表成功', songsAlreadyInPlaylist: '歌曲已存在于播放列表中', historyRecommend: '历史日推', fetchDatesFailed: '获取日期列表失败', fetchSongsFailed: '获取歌曲列表失败', noSongs: '暂无歌曲' }, playlist: { import: { button: '歌单导入', title: '歌单导入', description: '支持通过元数据/文字/链接三种方式导入歌单', linkTab: '链接导入', textTab: '文字导入', localTab: '元数据导入', linkPlaceholder: '请输入歌单链接,每行一个', textPlaceholder: '请输入歌曲信息,格式为:歌曲名 歌手名', localPlaceholder: '请输入JSON格式的歌曲元数据', linkTips: '支持的链接来源:', linkTip1: '将歌单分享到微信/微博/QQ后复制链接', linkTip2: '直接复制歌单/个人主页链接', linkTip3: '直接复制文章链接', textTips: '请输入歌曲信息,每行一首歌', textFormat: '格式:歌曲名 歌手名', localTips: '请添加歌曲元数据', localFormat: '格式示例:', songNamePlaceholder: '歌曲名称', artistNamePlaceholder: '艺术家名称', albumNamePlaceholder: '专辑名称', addSongButton: '添加歌曲', addLinkButton: '添加链接', importToStarPlaylist: '导入到我喜欢的音乐', playlistNamePlaceholder: '请输入歌单名称', importButton: '开始导入', emptyLinkWarning: '请输入歌单链接', emptyTextWarning: '请输入歌曲信息', emptyLocalWarning: '请输入歌曲元数据', invalidJsonFormat: 'JSON格式不正确', importSuccess: '导入任务创建成功', importFailed: '导入失败', importStatus: '导入状态', refresh: '刷新', taskId: '任务ID', status: '状态', successCount: '成功数量', failReason: '失败原因', unknownError: '未知错误', statusPending: '等待处理', statusProcessing: '处理中', statusSuccess: '导入成功', statusFailed: '导入失败', statusUnknown: '未知状态', taskList: '任务列表', taskListTitle: '导入任务列表', action: '操作', select: '选择', fetchTaskListFailed: '获取任务列表失败', noTasks: '暂无导入任务', clearTasks: '清除任务', clearTasksConfirmTitle: '确认清除', clearTasksConfirmContent: '确定要清除所有导入任务记录吗?此操作不可恢复。', confirm: '确认', cancel: '取消', clearTasksSuccess: '任务列表已清除', clearTasksFailed: '清除任务列表失败' } }, settings: '设置', user: '用户', toplist: '排行榜', history: '收藏历史', list: '歌单', mv: 'MV', home: '首页', search: '搜索' }; ================================================ FILE: src/i18n/lang/zh-CN/donation.ts ================================================ export default { description: '您的捐赠将用于支持开发和维护工作,包括但不限于服务器维护、域名续费等。', message: '留言时可留下您的邮箱或 github名称。', refresh: '刷新列表', toDonateList: '请我喝咖啡', noMessage: '暂无留言', title: '捐赠列表' }; ================================================ FILE: src/i18n/lang/zh-CN/download.ts ================================================ export default { title: '下载管理', localMusic: '本地音乐', count: '共 {count} 首歌曲', clearAll: '清空记录', settings: '设置', tabs: { downloading: '下载中', downloaded: '已下载' }, empty: { noTasks: '暂无下载任务', noDownloaded: '暂无已下载歌曲' }, progress: { total: '总进度: {progress}%' }, status: { downloading: '下载中', completed: '已完成', failed: '失败', unknown: '未知' }, artist: { unknown: '未知歌手' }, delete: { title: '删除确认', message: '确定要删除歌曲 "{filename}" 吗?此操作不可恢复。', confirm: '确定删除', cancel: '取消', success: '删除成功', failed: '删除失败', fileNotFound: '文件不存在或已被移动,已从记录中移除', recordRemoved: '文件删除失败,但已从记录中移除' }, clear: { title: '清空下载记录', message: '确定要清空所有下载记录吗?此操作不会删除已下载的音乐文件,但将清空所有记录。', confirm: '确定清空', cancel: '取消', success: '下载记录已清空' }, message: { downloadComplete: '{filename} 下载完成', downloadFailed: '{filename} 下载失败: {error}' }, loading: '加载中...', playStarted: '开始播放: {name}', playFailed: '播放失败: {name}', path: { copied: '路径已复制到剪贴板', copyFailed: '复制路径失败' }, settingsPanel: { title: '下载设置', path: '下载位置', pathDesc: '设置音乐文件下载保存的位置', pathPlaceholder: '请选择下载路径', noPathSelected: '请先选择下载路径', select: '选择文件夹', open: '打开文件夹', fileFormat: '文件名格式', fileFormatDesc: '设置下载音乐时的文件命名格式', customFormat: '自定义格式', separator: '分隔符', separators: { dash: '空格-空格', underscore: '下划线', space: '空格' }, dragToArrange: '拖动排序或使用箭头按钮调整顺序:', formatVariables: '可用变量', preview: '预览效果:', saveSuccess: '下载设置已保存', presets: { songArtist: '歌曲名 - 歌手名', artistSong: '歌手名 - 歌曲名', songOnly: '仅歌曲名' }, components: { songName: '歌曲名', artistName: '歌手名', albumName: '专辑名' } } }; ================================================ FILE: src/i18n/lang/zh-CN/favorite.ts ================================================ export default { title: '我的收藏', count: '共 {count} 首', batchDownload: '批量下载', download: '下载 ({count})', emptyTip: '还没有收藏歌曲', downloadSuccess: '下载完成', downloadFailed: '下载失败', downloading: '正在下载中,请稍候...', selectSongsFirst: '请先选择要下载的歌曲', descending: '降', ascending: '升' }; ================================================ FILE: src/i18n/lang/zh-CN/history.ts ================================================ export default { title: '播放历史', heatmapTitle: '热力图', playCount: '{count}', getHistoryFailed: '获取历史记录失败', categoryTabs: { songs: '歌曲', playlists: '歌单', albums: '专辑' }, tabs: { all: '全部记录', local: '本地记录', cloud: '云端记录' }, getCloudRecordFailed: '获取云端记录失败', needLogin: '请使用cookie登录以查看云端记录', merging: '正在合并记录...', noDescription: '暂无描述', noData: '暂无记录', heatmap: { title: '播放热力图', loading: '正在加载数据...', unit: '次播放', footerText: '鼠标悬停查看详细信息', playCount: '播放 {count} 次', topSongs: '当天热门歌曲', times: '次', totalPlays: '总播放次数', activeDays: '活跃天数', noData: '暂无播放记录', colorTheme: '配色方案', colors: { green: '绿色', blue: '蓝色', orange: '橙色', purple: '紫色', red: '红色' }, mostPlayedSong: '播放最多的歌曲', mostActiveDay: '最活跃的一天', latestNightSong: '最晚播放的歌曲' } }; ================================================ FILE: src/i18n/lang/zh-CN/login.ts ================================================ export default { title: { qr: '扫码登录', phone: '手机号登录', cookie: 'Cookie登录', uid: 'UID登录' }, qrTip: '使用网易云APP扫码登录', phoneTip: '使用网易云账号登录', tokenTip: '输入有效的网易云音乐Cookie即可登录', uidTip: '输入用户ID快速登录', placeholder: { phone: '手机号', password: '密码', cookie: '请输入网易云音乐Cookie(token)', uid: '请输入用户ID(UID)' }, button: { login: '登录', switchToQr: '扫码登录', switchToPhone: '手机号登录', switchToToken: '使用Cookie登录', switchToUid: 'UID登录', backToQr: '返回二维码登录', cookieLogin: 'Cookie登录', autoGetCookie: '自动获取Cookie', refresh: '点击刷新', refreshing: '刷新中...', refreshQr: '刷新二维码' }, message: { loginSuccess: '登录成功', loginFailed: '登录失败', tokenLoginSuccess: 'Cookie登录成功', uidLoginSuccess: 'UID登录成功', loadError: '加载登录信息时出错', qrCheckError: '检查二维码状态时出错', tokenRequired: '请输入Cookie', tokenInvalid: 'Cookie无效,请检查后重试', uidRequired: '请输入用户ID', uidInvalid: '用户ID无效或用户不存在', uidLoginFailed: 'UID登录失败,请检查用户ID是否正确', phoneRequired: '请输入手机号', passwordRequired: '请输入密码', phoneLoginFailed: '手机号登录失败,请检查手机号和密码是否正确', autoGetCookieSuccess: '自动获取Cookie成功', autoGetCookieFailed: '自动获取Cookie失败', autoGetCookieTip: '将打开网易云音乐登录页面,请完成登录后关闭窗口', qrCheckFailed: '检查二维码状态失败,请刷新重试', qrLoading: '正在加载二维码...', qrExpired: '二维码已过期,请点击刷新', qrExpiredShort: '二维码已过期', qrExpiredWarning: '二维码已过期,请点击刷新获取新的二维码', qrScanned: '已扫码,请在手机上确认登录', qrScannedShort: '已扫码', qrScannedInfo: '已扫码,请在手机上确认登录', qrConfirmed: '登录成功,正在跳转...', qrGenerating: '正在生成二维码...' }, qrTitle: '扫码登录网易云音乐', uidWarning: '注意:UID登录仅用于查看用户公开信息,无法访问需要登录权限的功能' }; ================================================ FILE: src/i18n/lang/zh-CN/player.ts ================================================ export default { nowPlaying: '正在播放', playlist: '播放列表', lyrics: '歌词', previous: '上一个', play: '播放', pause: '暂停', next: '下一个', volumeUp: '音量增加', volumeDown: '音量减少', mute: '静音', unmute: '取消静音', songNum: '歌曲总数:{num}', addCorrection: '提前 {num} 秒', subtractCorrection: '延迟 {num} 秒', playFailed: '当前歌曲播放失败,播放下一首', parseFailedPlayNext: '歌曲解析失败,播放下一首', consecutiveFailsError: '播放遇到错误,可能是网络波动或解析源失效,请切换播放列表或稍后重试', playMode: { sequence: '顺序播放', loop: '单曲循环', random: '随机播放' }, fullscreen: { enter: '全屏', exit: '退出全屏' }, close: '关闭', modeHint: { single: '单曲循环', list: '自动播放下一个' }, lrc: { noLrc: '暂无歌词, 请欣赏', noAutoScroll: '本歌词不支持自动滚动' }, reparse: { title: '选择解析音源', desc: '点击音源直接进行解析,下次播放此歌曲时将使用所选音源', success: '重新解析成功', failed: '重新解析失败', warning: '请选择一个音源', bilibiliNotSupported: 'B站视频不支持重新解析', processing: '解析中...', clear: '清除自定义音源', customApiFailed: '自定义API解析失败,正在尝试使用内置音源...', customApiError: '自定义API请求出错,正在尝试使用内置音源...' }, playBar: { expand: '展开歌词', collapse: '收起歌词', like: '喜欢', lyric: '歌词', noSongPlaying: '没有正在播放的歌曲', eq: '均衡器', playList: '播放列表', reparse: '重新解析', playMode: { sequence: '顺序播放', loop: '循环播放', random: '随机播放' }, play: '开始播放', pause: '暂停播放', prev: '上一首', next: '下一首', volume: '音量', favorite: '已收藏{name}', unFavorite: '已取消收藏{name}', miniPlayBar: '迷你播放栏', playbackSpeed: '播放速度', advancedControls: '更多设置', intelligenceMode: { title: '心动模式', needCookieLogin: '请使用 Cookie 方式登录后使用心动模式', noFavoritePlaylist: '未找到我喜欢的音乐歌单', noLikedSongs: '您还没有喜欢的歌曲', loading: '正在加载心动模式', success: '已加载 {count} 首歌曲', failed: '获取心动模式列表失败', error: '心动模式播放出错' } }, eq: { title: '均衡器', reset: '重置', on: '开启', off: '关闭', bass: '低音', midrange: '中音', treble: '高音', presets: { flat: '平坦', pop: '流行', rock: '摇滚', classical: '古典', jazz: '爵士', electronic: '电子', hiphop: '嘻哈', rb: 'R&B', metal: '金属', vocal: '人声', dance: '舞曲', acoustic: '原声', custom: '自定义' } }, // 播放器设置 settings: { title: '播放设置', playbackSpeed: '播放速度' }, // 定时关闭功能相关 sleepTimer: { title: '定时关闭', cancel: '取消定时', timeMode: '按时间关闭', songsMode: '按歌曲数关闭', playlistEnd: '播放完列表后关闭', afterPlaylist: '播放完列表后关闭', activeUntilEnd: '播放至列表结束', minutes: '分钟', hours: '小时', songs: '首歌', set: '设置', timerSetSuccess: '已设置{minutes}分钟后关闭', songsSetSuccess: '已设置播放{songs}首歌后关闭', playlistEndSetSuccess: '已设置播放完列表后关闭', timerCancelled: '已取消定时关闭', timerEnded: '定时关闭已触发', playbackStopped: '音乐播放已停止', minutesRemaining: '剩余{minutes}分钟', songsRemaining: '剩余{count}首歌' }, playList: { clearAll: '清空播放列表', alreadyEmpty: '播放列表已经为空', cleared: '已清空播放列表', empty: '播放列表为空', clearConfirmTitle: '清空播放列表', clearConfirmContent: '这将清空所有播放列表中的歌曲并停止当前播放。是否继续?' } }; ================================================ FILE: src/i18n/lang/zh-CN/search.ts ================================================ export default { title: { hotSearch: '热搜列表', searchList: '搜索列表', searchHistory: '搜索历史' }, button: { clear: '清空', back: '返回', playAll: '播放列表' }, loading: { more: '加载中...', failed: '搜索失败', searching: '搜索中...' }, noMore: '没有更多了', error: { searchFailed: '搜索失败' }, search: { single: '单曲', album: '专辑', playlist: '歌单', mv: 'MV', bilibili: 'B站' }, history: '搜索历史', hot: '热门搜索', suggestions: '搜索建议' }; ================================================ FILE: src/i18n/lang/zh-CN/settings.ts ================================================ export default { theme: '主题', language: '语言', regard: '关于', logout: '退出登录', sections: { basic: '基础设置', playback: '播放设置', application: '应用设置', network: '网络设置', system: '系统管理', donation: '捐赠支持', about: '关于' }, basic: { themeMode: '主题模式', themeModeDesc: '切换日间/夜间主题', autoTheme: '跟随系统', manualTheme: '手动切换', language: '语言设置', languageDesc: '切换显示语言', tokenManagement: 'Cookie管理', tokenManagementDesc: '管理网易云音乐登录Cookie', tokenStatus: '当前Cookie状态', tokenSet: '已设置', tokenNotSet: '未设置', setToken: '设置Cookie', modifyToken: '修改Cookie', clearToken: '清除Cookie', font: '字体设置', fontDesc: '选择字体,优先使用排在前面的字体', fontScope: { global: '全局', lyric: '仅歌词' }, animation: '动画速度', animationDesc: '是否开启动画', animationSpeed: { slow: '极慢', normal: '正常', fast: '极快' }, fontPreview: { title: '字体预览', chinese: '中文', english: 'English', japanese: '日本語', korean: '한국어', chineseText: '静夜思 床前明月光 疑是地上霜', englishText: 'The quick brown fox jumps over the lazy dog', japaneseText: 'あいうえお かきくけこ さしすせそ', koreanText: '가나다라마 바사아자차 카타파하' }, gpuAcceleration: 'GPU加速', gpuAccelerationDesc: '启用或禁用硬件加速,可以提高渲染性能但可能会增加GPU负载', gpuAccelerationRestart: '更改GPU加速设置需要重启应用后生效', gpuAccelerationChangeSuccess: 'GPU加速设置已更新,重启应用后生效', gpuAccelerationChangeError: 'GPU加速设置更新失败', tabletMode: '平板模式', tabletModeDesc: '启用后将在移动设备上使用PC样式界面,适合平板等大屏设备' }, playback: { quality: '音质设置', qualityDesc: '选择音乐播放音质(网易云VIP)', qualityOptions: { standard: '标准', higher: '较高', exhigh: '极高', lossless: '无损', hires: 'Hi-Res', jyeffect: '高清环绕声', sky: '沉浸环绕声', dolby: '杜比全景声', jymaster: '超清母带' }, musicSources: '音源设置', musicSourcesDesc: '选择音乐解析使用的音源平台', musicSourcesWarning: '至少需要选择一个音源平台', musicUnblockEnable: '启用音乐解析', musicUnblockEnableDesc: '开启后将尝试解析无法播放的音乐', configureMusicSources: '配置音源', selectedMusicSources: '已选音源:', noMusicSources: '未选择音源', gdmusicInfo: 'GD音乐台可自动解析多个平台音源,自动选择最佳结果', autoPlay: '自动播放', autoPlayDesc: '重新打开应用时是否自动继续播放', showStatusBar: '是否显示状态栏控制功能', showStatusBarContent: '可以在您的mac状态栏显示音乐控制功能(重启后生效)', fallbackParser: 'GD音乐台(music.gdstudio.xyz)设置', fallbackParserDesc: 'GD音乐台将自动尝试多个音乐平台进行解析,无需额外配置。优先级高于其他解析方式,但是请求可能较慢。感谢(music.gdstudio.xyz)\n', parserGD: 'GD 音乐台 (内置)', parserCustom: '自定义 API', // 音源标签 sourceLabels: { migu: '咪咕音乐', kugou: '酷狗音乐', pyncmd: '网易云(内置)', bilibili: 'Bilibili', gdmusic: 'GD音乐台', custom: '自定义 API' }, // 自定义API相关的提示 customApi: { sectionTitle: '自定义 API 设置', importConfig: '导入 JSON 配置', currentSource: '当前音源', notImported: '尚未导入自定义音源。', importSuccess: '成功导入音源: {name}', importFailed: '导入失败: {message}', enableHint: '请先导入 JSON 配置文件才能启用', status: { imported: '已导入自定义音源', notImported: '未导入' } }, lxMusic: { tabs: { sources: '音源选择', lxMusic: '落雪音源', customApi: '自定义API' }, scripts: { title: '已导入的音源脚本', importLocal: '本地导入', importOnline: '在线导入', urlPlaceholder: '输入落雪音源脚本 URL', importBtn: '导入', empty: '暂无已导入的落雪音源', notConfigured: '未配置 (请去落雪音源Tab配置)', importHint: '导入兼容的自定义 API 插件以扩展音源', noScriptWarning: '请先导入落雪音源脚本', noSelectionWarning: '请先选择一个落雪音源', notFound: '音源不存在', switched: '已切换到音源: {name}', deleted: '已删除音源: {name}', enterUrl: '请输入脚本 URL', invalidUrl: '无效的 URL 格式', invalidScript: '无效的落雪音源脚本,未找到 globalThis.lx 相关代码', nameRequired: '名称不能为空', renameSuccess: '重命名成功' } } }, application: { closeAction: '关闭行为', closeActionDesc: '选择关闭窗口时的行为', closeOptions: { ask: '每次询问', minimize: '最小化到托盘', close: '直接退出' }, shortcut: '快捷键设置', shortcutDesc: '自定义全局快捷键', download: '下载管理', downloadDesc: '是否始终显示下载列表按钮', unlimitedDownload: '无限制下载', unlimitedDownloadDesc: '开启后将无限制下载音乐(可能出现下载失败的情况), 默认限制 300 首', downloadPath: '下载目录', downloadPathDesc: '选择音乐文件的下载位置', remoteControl: '远程控制', remoteControlDesc: '设置远程控制功能' }, network: { apiPort: '音乐API端口', apiPortDesc: '修改后需要重启应用', proxy: '代理设置', proxyDesc: '无法访问音乐时可以开启代理', proxyHost: '代理地址', proxyHostPlaceholder: '请输入代理地址', proxyPort: '代理端口', proxyPortPlaceholder: '请输入代理端口', realIP: 'realIP设置', realIPDesc: '由于限制,此项目在国外使用会受到限制可使用realIP参数,传进国内IP解决', messages: { proxySuccess: '代理设置已保存,重启应用后生效', proxyError: '请检查输入是否正确', realIPSuccess: '真实IP设置已保存', realIPError: '请输入有效的IP地址' } }, system: { cache: '缓存管理', cacheDesc: '清除缓存', cacheClearTitle: '请选择要清除的缓存类型:', cacheTypes: { history: { label: '播放历史', description: '清除播放过的歌曲记录' }, favorite: { label: '收藏记录', description: '清除本地收藏的歌曲记录(不会影响云端收藏)' }, user: { label: '用户数据', description: '清除登录信息和用户相关数据' }, settings: { label: '应用设置', description: '清除应用的所有自定义设置' }, downloads: { label: '下载记录', description: '清除下载历史记录(不会删除已下载的文件)' }, resources: { label: '音乐资源', description: '清除已加载的音乐文件、歌词等资源缓存' }, lyrics: { label: '歌词资源', description: '清除已加载的歌词资源缓存' } }, restart: '重启', restartDesc: '重启应用', messages: { clearSuccess: '清除成功,部分设置在重启后生效' } }, about: { version: '版本', checkUpdate: '检查更新', checking: '检查中...', latest: '当前已是最新版本', hasUpdate: '发现新版本', gotoUpdate: '前往更新', gotoGithub: '前往 Github', author: '作者', authorDesc: 'algerkong 点个star🌟呗', messages: { checkError: '检查更新失败,请稍后重试' } }, validation: { selectProxyProtocol: '请选择代理协议', proxyHost: '请输入代理地址', portNumber: '请输入有效的端口号(1-65535)' }, lyricSettings: { title: '歌词设置', tabs: { display: '显示', interface: '界面', typography: '文字', background: '背景', mobile: '移动端' }, pureMode: '纯净模式', hideCover: '隐藏封面', centerDisplay: '居中显示', showTranslation: '显示翻译', hideLyrics: '隐藏歌词', hidePlayBar: '隐藏播放栏', hideMiniPlayBar: '隐藏迷你播放栏', showMiniPlayBar: '显示迷你播放栏', backgroundTheme: '背景主题', themeOptions: { default: '默认', light: '亮色', dark: '暗色' }, fontSize: '字体大小', fontSizeMarks: { small: '小', medium: '中', large: '大' }, fontWeight: '字体粗细', fontWeightMarks: { thin: '细', normal: '常规', bold: '粗' }, letterSpacing: '字间距', letterSpacingMarks: { compact: '紧凑', default: '默认', loose: '宽松' }, lineHeight: '行高', lineHeightMarks: { compact: '紧凑', default: '默认', loose: '宽松' }, contentWidth: '内容区宽度', mobileLayout: '移动端布局', layoutOptions: { default: '默认', ios: 'iOS风格', android: '安卓风格' }, mobileCoverStyle: '封面样式', coverOptions: { record: '唱片', square: '方形', full: '全屏' }, lyricLines: '歌词行数', mobileUnavailable: '此设置仅在移动端可用', // 背景设置 background: { useCustomBackground: '使用自定义背景', backgroundMode: '背景模式', modeOptions: { solid: '纯色', gradient: '渐变', image: '图片', css: 'CSS' }, solidColor: '选择颜色', presetColors: '预设颜色', customColor: '自定义颜色', gradientEditor: '渐变编辑器', gradientColors: '渐变颜色', gradientDirection: '渐变方向', directionOptions: { toBottom: '上到下', toRight: '左到右', toBottomRight: '左上到右下', angle45: '45度', toTop: '下到上', toLeft: '右到左' }, addColor: '添加颜色', removeColor: '移除颜色', imageUpload: '上传图片', imagePreview: '图片预览', clearImage: '清除图片', imageBlur: '模糊度', imageBrightness: '明暗度', customCss: '自定义 CSS 样式', customCssPlaceholder: '输入 CSS 样式,如: background: linear-gradient(...)', customCssHelp: '支持任意 CSS background 属性', reset: '重置为默认', fileSizeLimit: '图片大小限制: 20MB', invalidImageFormat: '无效的图片格式', imageTooLarge: '图片过大,请选择小于 20MB 的图片' } }, translationEngine: '歌詞翻譯引擎', translationEngineOptions: { none: '关闭', opencc: 'OpenCC 繁化' }, themeColor: { title: '歌词主题色', presetColors: '预设颜色', customColor: '自定义颜色', preview: '预览效果', previewText: '歌词效果', colorNames: { 'spotify-green': 'Spotify 绿', 'apple-blue': '苹果蓝', 'youtube-red': 'YouTube 红', orange: '活力橙', purple: '神秘紫', pink: '樱花粉' }, tooltips: { openColorPicker: '打开色板', closeColorPicker: '关闭色板' }, placeholder: '#1db954' }, shortcutSettings: { title: '快捷键设置', shortcut: '快捷键', shortcutDesc: '自定义快捷键', shortcutConflict: '快捷键冲突', inputPlaceholder: '点击输入快捷键', resetShortcuts: '恢复默认', disableAll: '全部禁用', enableAll: '全部启用', togglePlay: '播放/暂停', prevPlay: '上一首', nextPlay: '下一首', volumeUp: '音量增加', volumeDown: '音量减少', toggleFavorite: '收藏/取消收藏', toggleWindow: '显示/隐藏窗口', scopeGlobal: '全局', scopeApp: '应用内', enabled: '启用', disabled: '禁用', messages: { resetSuccess: '已恢复默认快捷键,请记得保存', conflict: '存在冲突的快捷键,请重新设置', saveSuccess: '快捷键设置已保存', saveError: '保存快捷键失败,请重试', cancelEdit: '已取消修改', disableAll: '已禁用所有快捷键,请记得保存', enableAll: '已启用所有快捷键,请记得保存' } }, remoteControl: { title: '远程控制', enable: '启用远程控制', port: '服务端口', allowedIps: '允许的IP地址', addIp: '添加IP', emptyListHint: '空列表表示允许所有IP访问', saveSuccess: '远程控制设置已保存', accessInfo: '远程控制访问地址:' }, cookie: { title: 'Cookie设置', description: '请输入网易云音乐的Cookie:', placeholder: '请粘贴完整的Cookie...', help: { format: 'Cookie通常以 "MUSIC_U=" 开头', source: '可以从浏览器开发者工具的网络请求中获取', storage: 'Cookie设置后将自动保存到本地存储' }, action: { save: '保存Cookie', paste: '粘贴', clear: '清空' }, validation: { required: '请输入Cookie', format: 'Cookie格式可能不正确,请检查是否包含MUSIC_U' }, message: { saveSuccess: 'Cookie保存成功', saveError: 'Cookie保存失败', pasteSuccess: '粘贴成功', pasteError: '粘贴失败,请手动复制' }, info: { length: '当前长度:{length} 字符' } } }; ================================================ FILE: src/i18n/lang/zh-CN/songItem.ts ================================================ export default { menu: { play: '播放', playNext: '下一首播放', download: '下载歌曲', addToPlaylist: '添加到歌单', favorite: '喜欢', unfavorite: '取消喜欢', removeFromPlaylist: '从歌单中删除', dislike: '不喜欢', undislike: '取消不喜欢' }, message: { downloading: '正在下载中,请稍候...', downloadFailed: '下载失败', downloadQueued: '已加入下载队列', addedToNextPlay: '已添加到下一首播放', getUrlFailed: '获取音乐下载地址失败,请检查是否登录' }, dialog: { dislike: { title: '提示!', content: '确认不喜欢这首歌吗?再次进入将从每日推荐中排除。', positiveText: '不喜欢', negativeText: '取消' } } }; ================================================ FILE: src/i18n/lang/zh-CN/user.ts ================================================ export default { profile: { followers: '粉丝', following: '关注', level: '等级' }, playlist: { created: '创建的歌单', mine: '我创建的', trackCount: '{count}首', playCount: '播放{count}次' }, tabs: { created: '创建', favorite: '收藏', album: '专辑' }, ranking: { title: '听歌排行', playCount: '{count}次' }, follow: { title: '关注列表', viewPlaylist: '查看歌单', noFollowings: '暂无关注', loadMore: '加载更多', noSignature: '这个家伙很懒,什么都没留下', userFollowsTitle: '的关注', myFollowsTitle: '我的关注' }, follower: { title: '粉丝列表', noFollowers: '暂无粉丝', loadMore: '加载更多', userFollowersTitle: '的粉丝', myFollowersTitle: '我的粉丝' }, detail: { playlists: '歌单', records: '听歌排行', noPlaylists: '暂无歌单', noRecords: '暂无听歌记录', artist: '歌手', noSignature: '这个人很懒,什么都没留下', invalidUserId: '用户ID无效', noRecordPermission: '{name}不让你看听歌排行' }, message: { loadFailed: '加载用户页面失败', deleteSuccess: '删除成功', deleteFailed: '删除失败' } }; ================================================ FILE: src/i18n/lang/zh-Hant/artist.ts ================================================ export default { hotSongs: '熱門歌曲', albums: '專輯', description: '藝人介紹' }; ================================================ FILE: src/i18n/lang/zh-Hant/bilibili.ts ================================================ export default { player: { loading: '聽書載入中...', retry: '重試', playNow: '立即播放', loadingTitle: '載入中...', totalDuration: '總時長: {duration}', partsList: '分P列表 (共{count}集)', playStarted: '已開始播放', switchingPart: '切換到分P: {part}', preloadingNext: '預載入下一個分P: {part}', playingCurrent: '播放當前選中的分P: {name}', num: '萬', errors: { invalidVideoId: '影片ID無效', loadVideoDetailFailed: '獲取影片詳情失敗', loadPartInfoFailed: '無法載入影片分P資訊', loadAudioUrlFailed: '獲取音訊播放地址失敗', videoDetailNotLoaded: '影片詳情未載入', missingParams: '缺少必要參數', noAvailableAudioUrl: '未找到可用的音訊地址', loadPartAudioFailed: '載入分P音訊URL失敗', audioListEmpty: '音訊列表為空,請重試', currentPartNotFound: '未找到當前分P的音訊', audioUrlFailed: '獲取音訊URL失敗', playFailed: '播放失敗,請重試', getAudioUrlFailed: '獲取音訊地址失敗,請重試', audioNotFound: '未找到對應的音訊,請重試', preloadFailed: '預載入下一個分P失敗', switchPartFailed: '切換分P時載入音訊URL失敗' }, console: { loadingDetail: '載入B站影片詳情', detailData: 'B站影片詳情資料', multipleParts: '影片有多個分P,共{count}個', noPartsData: '影片無分P或分P資料為空', loadingAudioSource: '載入音訊來源', generatedAudioList: '已生成音訊列表,共{count}首', getDashAudioUrl: '獲取到dash音訊URL', getDurlAudioUrl: '獲取到durl音訊URL', loadingPartAudio: '載入分P音訊URL: {part}, cid: {cid}', loadPartAudioFailed: '載入分P音訊URL失敗: {part}', switchToPart: '切換到分P: {part}', audioNotFoundInList: '未找到對應的音訊項目', preparingToPlay: '準備播放當前選中的分P: {name}', preloadingNextPart: '預載入下一個分P: {part}', playingSelectedPart: '播放當前選中的分P: {name},音訊URL: {url}', preloadNextFailed: '預載入下一個分P失敗' } } }; ================================================ FILE: src/i18n/lang/zh-Hant/common.ts ================================================ export default { play: '播放', next: '下一首', previous: '上一首', volume: '音量', settings: '設定', search: '搜尋', loading: '載入中...', loadingMore: '載入更多...', alipay: '支付寶', wechat: '微信支付', on: '開啟', off: '關閉', show: '顯示', hide: '隱藏', confirm: '確認', cancel: '取消', configure: '設定', open: '開啟', modify: '修改', success: '操作成功', error: '操作失敗', warning: '警告', info: '提示', save: '儲存', delete: '刪除', refresh: '重新整理', retry: '重試', reset: '重設', back: '返回', copySuccess: '已複製到剪貼簿', copyFailed: '複製失敗', validation: { required: '此項為必填', invalidInput: '輸入無效', selectRequired: '請選擇一個選項', numberRange: '請輸入 {min} 到 {max} 之間的數字' }, viewMore: '查看更多', noMore: '沒有更多了', selectAll: '全選', expand: '展開', collapse: '收合', songCount: '{count}首', language: '語言', today: '今天', yesterday: '昨天', tray: { show: '顯示', quit: '退出', playPause: '播放/暫停', prev: '上一首', next: '下一首', pause: '暫停', play: '播放', favorite: '收藏' } }; ================================================ FILE: src/i18n/lang/zh-Hant/comp.ts ================================================ export default { installApp: { description: '安裝應用程式,獲得更好的體驗', noPrompt: '不再提示', install: '立即安裝', cancel: '暫不安裝', download: '下載', downloadFailed: '下載失敗', downloadComplete: '下載完成', downloadProblem: '下載遇到問題?去', downloadProblemLinkText: '下載最新版本' }, playlistDrawer: { title: '新增至播放清單', createPlaylist: '建立新播放清單', cancelCreate: '取消建立', create: '建立', playlistName: '播放清單名稱', privatePlaylist: '私人播放清單', publicPlaylist: '公開播放清單', createSuccess: '播放清單建立成功', createFailed: '播放清單建立失敗', addSuccess: '歌曲新增成功', addFailed: '歌曲新增失敗', private: '私人', public: '公開', count: '首歌曲', loginFirst: '請先登入', getPlaylistFailed: '取得播放清單失敗', inputPlaylistName: '請輸入播放清單名稱' }, update: { title: '發現新版本', currentVersion: '目前版本', cancel: '暫不更新', prepareDownload: '準備下載...', downloading: '下載中...', nowUpdate: '立即更新', downloadFailed: '下載失敗,請重試或手動下載', startFailed: '啟動下載失敗,請重試或手動下載', noDownloadUrl: '未找到適合目前系統的安裝包,請手動下載', installConfirmTitle: '安裝更新', installConfirmContent: '是否關閉應用程式並安裝更新?', manualInstallTip: '如果關閉應用程式後沒有正常彈出安裝程式,請至下載資料夾尋找檔案並手動開啟。', yesInstall: '立即安裝', noThanks: '稍後安裝', fileLocation: '檔案位置', copy: '複製路徑', copySuccess: '路徑已複製到剪貼簿', copyFailed: '複製失敗', backgroundDownload: '背景下載' }, disclaimer: { title: '使用說明', warning: '本程式為開發測試版本,功能尚未完善,可能存在諸多問題及臭蟲,僅供學習交流使用。', item1: '本程式僅供個人學習、研究及技術交流之目的,不得用於任何商業用途。', item2: '請在下載後 24 小時內刪除,若對您有所幫助,請支持正版音樂。', item3: '使用本程式即代表您已了解並同意相關風險,開發者對任何損失概不負責。', agree: '我已了解並同意', disagree: '不同意並退出' }, donate: { title: '支援開發者', subtitle: '您的支援是我持續更新的動力', tip: '捐贈完全採自願原則。即使不捐贈,您依然可以正常使用所有功能。感謝您的理解與支援!', wechat: '微信支付', alipay: '支付寶', wechatQR: '微信收款碼', alipayQR: '支付寶收款碼', scanTip: '請使用手機 App 掃描 QR Code 進行捐贈', enterApp: '進入程式', noForce: '捐贈並非強制,您可以點擊按鈕直接進入' }, coffee: { title: '請我喝杯咖啡', alipay: '支付寶', wechat: '微信支付', alipayQR: '支付寶收款碼', wechatQR: '微信收款碼', coffeeDesc: '一杯咖啡,一份支持', coffeeDescLinkText: '查看更多', groupText: '微信公众号:AlgerMusic', messages: { copySuccess: '已複製到剪貼簿' }, donateList: '請我喝杯咖啡' }, playlistType: { title: '播放清單分類', showAll: '顯示全部', hide: '隱藏部分' }, recommendAlbum: { title: '最新專輯' }, recommendSinger: { title: '每日推薦', songlist: '每日推薦清單' }, recommendSonglist: { title: '本週最熱音樂' }, searchBar: { login: '登入', toLogin: '去登入', logout: '登出', set: '設定', theme: '主題', restart: '重新啟動', refresh: '重新整理', currentVersion: '目前版本', searchPlaceholder: '搜尋點什麼吧...', zoom: '頁面縮放', zoom100: '標準縮放100%', resetZoom: '點擊重設縮放', zoomDefault: '標準縮放' }, titleBar: { closeTitle: '請選擇關閉方式', minimizeToTray: '最小化到系統匣', exitApp: '退出應用程式', rememberChoice: '記住我的選擇', closeApp: '關閉應用程式' }, userPlayList: { title: '{name}的常聽' }, musicList: { searchSongs: '搜尋歌曲', noSearchResults: '沒有找到相關歌曲', switchToNormal: '切換到預設版面', switchToCompact: '切換到緊湊版面', playAll: '播放全部', collect: '收藏', collectSuccess: '收藏成功', cancelCollectSuccess: '取消收藏成功', operationFailed: '操作失敗', cancelCollect: '取消收藏', addToPlaylist: '新增至播放清單', addToPlaylistSuccess: '新增至播放清單成功', songsAlreadyInPlaylist: '歌曲已存在於播放清單中', historyRecommend: '歷史日推', fetchDatesFailed: '獲取日期列表失敗', fetchSongsFailed: '獲取歌曲列表失敗', noSongs: '暫無歌曲' }, playlist: { import: { button: '播放清單匯入', title: '播放清單匯入', description: '支援透過元資料/文字/連結三種方式匯入播放清單', linkTab: '連結匯入', textTab: '文字匯入', localTab: '元資料匯入', linkPlaceholder: '請輸入播放清單連結,每行一個', textPlaceholder: '請輸入歌曲資訊,格式為:歌曲名 歌手名', localPlaceholder: '請輸入JSON格式的歌曲元資料', linkTips: '支援的連結來源:', linkTip1: '將播放清單分享到微信/微博/QQ後複製連結', linkTip2: '直接複製播放清單/個人主頁連結', linkTip3: '直接複製文章連結', textTips: '請輸入歌曲資訊,每行一首歌', textFormat: '格式:歌曲名 歌手名', localTips: '請新增歌曲元資料', localFormat: '格式範例:', songNamePlaceholder: '歌曲名稱', artistNamePlaceholder: '藝人名稱', albumNamePlaceholder: '專輯名稱', addSongButton: '新增歌曲', addLinkButton: '新增連結', importToStarPlaylist: '匯入到我喜歡的音樂', playlistNamePlaceholder: '請輸入播放清單名稱', importButton: '開始匯入', emptyLinkWarning: '請輸入播放清單連結', emptyTextWarning: '請輸入歌曲資訊', emptyLocalWarning: '請輸入歌曲元資料', invalidJsonFormat: 'JSON格式不正確', importSuccess: '匯入任務建立成功', importFailed: '匯入失敗', importStatus: '匯入狀態', refresh: '重新整理', taskId: '任務ID', status: '狀態', successCount: '成功數量', failReason: '失敗原因', unknownError: '未知錯誤', statusPending: '等待處理', statusProcessing: '處理中', statusSuccess: '匯入成功', statusFailed: '匯入失敗', statusUnknown: '未知狀態', taskList: '任務清單', taskListTitle: '匯入任務清單', action: '操作', select: '選擇', fetchTaskListFailed: '取得任務清單失敗', noTasks: '暫無匯入任務', clearTasks: '清除任務', clearTasksConfirmTitle: '確認清除', clearTasksConfirmContent: '確定要清除所有匯入任務記錄嗎?此操作不可恢復。', confirm: '確認', cancel: '取消', clearTasksSuccess: '任務清單已清除', clearTasksFailed: '清除任務清單失敗' } }, settings: '設定', user: '使用者', toplist: '排行榜', history: '收藏歷史', list: '播放清單', mv: 'MV', home: '首頁', search: '搜尋' }; ================================================ FILE: src/i18n/lang/zh-Hant/donation.ts ================================================ export default { description: '您的捐贈將用於支持開發和維護工作,包括但不限於伺服器維護、域名續費等。', message: '留言時可留下您的電子郵件或 github 名稱。', refresh: '重新整理列表', toDonateList: '請我喝杯咖啡', noMessage: '暫無留言', title: '捐贈列表' }; ================================================ FILE: src/i18n/lang/zh-Hant/download.ts ================================================ export default { title: '下載管理', localMusic: '本機音樂', count: '共 {count} 首歌曲', clearAll: '清空記錄', settings: '設定', tabs: { downloading: '下載中', downloaded: '已下載' }, empty: { noTasks: '暫無下載任務', noDownloaded: '暫無已下載歌曲' }, progress: { total: '總進度: {progress}%' }, status: { downloading: '下載中', completed: '已完成', failed: '失敗', unknown: '未知' }, artist: { unknown: '未知歌手' }, delete: { title: '刪除確認', message: '確定要刪除歌曲 "{filename}" 嗎?此操作不可恢復。', confirm: '確定刪除', cancel: '取消', success: '刪除成功', failed: '刪除失敗', fileNotFound: '檔案不存在或已被移動,已從記錄中移除', recordRemoved: '檔案刪除失敗,但已從記錄中移除' }, clear: { title: '清空下載記錄', message: '確定要清空所有下載記錄嗎?此操作不會刪除已下載的音樂檔案,但將清空所有記錄。', confirm: '確定清空', cancel: '取消', success: '下載記錄已清空' }, message: { downloadComplete: '{filename} 下載完成', downloadFailed: '{filename} 下載失敗: {error}' }, loading: '載入中...', playStarted: '開始播放: {name}', playFailed: '播放失敗: {name}', path: { copied: '路徑已複製到剪貼簿', copyFailed: '複製路徑失敗' }, settingsPanel: { title: '下載設定', path: '下載位置', pathDesc: '設定音樂檔案下載儲存的位置', pathPlaceholder: '請選擇下載路徑', noPathSelected: '請先選擇下載路徑', select: '選擇資料夾', open: '開啟資料夾', fileFormat: '檔名格式', fileFormatDesc: '設定下載音樂時的檔案命名格式', customFormat: '自訂格式', separator: '分隔符號', separators: { dash: '空格-空格', underscore: '底線', space: '空格' }, dragToArrange: '拖曳排序或使用箭頭按鈕調整順序:', formatVariables: '可用變數', preview: '預覽效果:', saveSuccess: '下載設定已儲存', presets: { songArtist: '歌曲名 - 歌手名', artistSong: '歌手名 - 歌曲名', songOnly: '僅歌曲名' }, components: { songName: '歌曲名', artistName: '歌手名', albumName: '專輯名' } } }; ================================================ FILE: src/i18n/lang/zh-Hant/favorite.ts ================================================ export default { title: '我的收藏', count: '共 {count} 首', batchDownload: '批次下載', download: '下載 ({count})', emptyTip: '還沒有收藏歌曲', downloadSuccess: '下載完成', downloadFailed: '下載失敗', downloading: '正在下載中,請稍候...', selectSongsFirst: '請先選擇要下載的歌曲', descending: '降', ascending: '升' }; ================================================ FILE: src/i18n/lang/zh-Hant/history.ts ================================================ export default { title: '播放歷史', heatmapTitle: '熱力圖', playCount: '{count}', getHistoryFailed: '取得歷史記錄失敗', categoryTabs: { songs: '歌曲', playlists: '歌單', albums: '專輯' }, tabs: { all: '全部記錄', local: '本地記錄', cloud: '雲端記錄' }, getCloudRecordFailed: '取得雲端記錄失敗', needLogin: '請使用cookie登入以查看雲端記錄', merging: '正在合併記錄...', noDescription: '暫無描述', noData: '暫無記錄', heatmap: { title: '播放熱力圖', loading: '正在載入數據...', unit: '次播放', footerText: '滑鼠懸停查看詳細信息', playCount: '播放 {count} 次', topSongs: '當天熱門歌曲', times: '次', totalPlays: '總播放次數', activeDays: '活躍天數', noData: '暫無播放記錄', colorTheme: '配色方案', colors: { green: '綠色', blue: '藍色', orange: '橙色', purple: '紫色', red: '紅色' }, mostPlayedSong: '播放最多的歌曲', mostActiveDay: '最活躍的一天', latestNightSong: '最晚播放的歌曲' } }; ================================================ FILE: src/i18n/lang/zh-Hant/login.ts ================================================ export default { title: { qr: '掃碼登入', phone: '手機號登入', cookie: 'Cookie登入', uid: 'UID登入' }, qrTip: '使用網易雲APP掃碼登入', phoneTip: '使用網易雲帳號登入', tokenTip: '輸入有效的網易雲音樂Cookie即可登入', uidTip: '輸入使用者ID快速登入', placeholder: { phone: '手機號', password: '密碼', cookie: '請輸入網易雲音樂Cookie(token)', uid: '請輸入使用者ID(UID)' }, button: { login: '登入', switchToQr: '掃碼登入', switchToPhone: '手機號登入', switchToToken: '使用Cookie登入', switchToUid: 'UID登入', backToQr: '返回二維碼登入', cookieLogin: 'Cookie登入', autoGetCookie: '自動取得Cookie', refresh: '點擊刷新', refreshing: '刷新中...', refreshQr: '刷新二維碼' }, message: { loginSuccess: '登入成功', tokenLoginSuccess: 'Cookie登入成功', uidLoginSuccess: 'UID登入成功', loadError: '載入登入資訊時出錯', qrCheckError: '檢查二維碼狀態時出錯', tokenRequired: '請輸入Cookie', tokenInvalid: 'Cookie無效,請檢查後重試', uidRequired: '請輸入使用者ID', uidInvalid: '使用者ID無效或使用者不存在', uidLoginFailed: 'UID登入失敗,請檢查使用者ID是否正確', autoGetCookieSuccess: '自動取得Cookie成功', autoGetCookieFailed: '自動取得Cookie失敗', autoGetCookieTip: '將開啟網易雲音樂登入頁面,請完成登入後關閉視窗', loginFailed: '登入失敗', phoneRequired: '請輸入手機號', passwordRequired: '請輸入密碼', phoneLoginFailed: '手機號登入失敗,請檢查手機號和密碼是否正確', qrCheckFailed: '檢查二維碼狀態失敗,請刷新重試', qrLoading: '正在載入二維碼...', qrExpired: '二維碼已過期,請點擊刷新', qrExpiredShort: '二維碼已過期', qrExpiredWarning: '二維碼已過期,請點擊刷新獲取新的二維碼', qrScanned: '已掃碼,請在手機上確認登入', qrScannedShort: '已掃碼', qrScannedInfo: '已扫码,请在手机上确认登录', qrConfirmed: '登入成功,正在跳轉...', qrGenerating: '正在生成二維碼...' }, qrTitle: '掃碼登入網易雲音樂', uidWarning: '注意:UID登入僅用於查看使用者公開資訊,無法訪問需要登入權限的功能' }; ================================================ FILE: src/i18n/lang/zh-Hant/player.ts ================================================ export default { nowPlaying: '正在播放', playlist: '播放清單', lyrics: '歌詞', previous: '上一個', play: '播放', pause: '暫停', next: '下一個', volumeUp: '音量增加', volumeDown: '音量減少', mute: '靜音', unmute: '取消靜音', songNum: '歌曲總數:{num}', addCorrection: '提前 {num} 秒', subtractCorrection: '延遲 {num} 秒', playFailed: '目前歌曲播放失敗,播放下一首', parseFailedPlayNext: '歌曲解析失敗,播放下一首', consecutiveFailsError: '播放遇到錯誤,可能是網路波動或解析源失效,請切換播放清單或稍後重試', playMode: { sequence: '順序播放', loop: '單曲循環', random: '隨機播放' }, fullscreen: { enter: '全螢幕', exit: '退出全螢幕' }, close: '關閉', modeHint: { single: '單曲循環', list: '自動播放下一個' }, lrc: { noLrc: '暫無歌詞, 請欣賞', noAutoScroll: '本歌詞不支持自動滾動' }, reparse: { title: '選擇解析音源', desc: '點擊音源直接進行解析,下次播放此歌曲時將使用所選音源', success: '重新解析成功', failed: '重新解析失敗', warning: '請選擇一個音源', bilibiliNotSupported: 'B站影片不支援重新解析', processing: '解析中...', clear: '清除自訂音源', customApiFailed: '自定義API解析失敗,正在嘗試使用內置音源...', customApiError: '自定義API請求出錯,正在嘗試使用內置音源...' }, playBar: { expand: '展開歌詞', collapse: '收合歌詞', like: '喜歡', lyric: '歌詞', noSongPlaying: '沒有正在播放的歌曲', eq: '等化器', playList: '播放清單', reparse: '重新解析', playMode: { sequence: '順序播放', loop: '循環播放', random: '隨機播放' }, play: '開始播放', pause: '暫停播放', prev: '上一首', next: '下一首', volume: '音量', favorite: '已收藏{name}', unFavorite: '已取消收藏{name}', miniPlayBar: '迷你播放列', playbackSpeed: '播放速度', advancedControls: '更多設定', intelligenceMode: { title: '心動模式', needCookieLogin: '請使用 Cookie 方式登入後使用心動模式', noFavoritePlaylist: '未找到我喜歡的音樂歌單', noLikedSongs: '您還沒有喜歡的歌曲', loading: '正在載入心動模式', success: '已載入 {count} 首歌曲', failed: '取得心動模式清單失敗', error: '心動模式播放出錯' } }, eq: { title: '等化器', reset: '重設', on: '開啟', off: '關閉', bass: '低音', midrange: '中音', treble: '高音', presets: { flat: '平坦', pop: '流行', rock: '搖滾', classical: '古典', jazz: '爵士', electronic: '電子', hiphop: '嘻哈', rb: 'R&B', metal: '金屬', vocal: '人聲', dance: '舞曲', acoustic: '原聲', custom: '自訂' } }, // 播放器設定 settings: { title: '播放設定', playbackSpeed: '播放速度' }, // 定時關閉功能相關 sleepTimer: { title: '定時關閉', cancel: '取消定時', timeMode: '按時間關閉', songsMode: '按歌曲數關閉', playlistEnd: '播放完清單後關閉', afterPlaylist: '播放完清單後關閉', activeUntilEnd: '播放至清單結束', minutes: '分鐘', hours: '小時', songs: '首歌', set: '設定', timerSetSuccess: '已設定{minutes}分鐘後關閉', songsSetSuccess: '已設定播放{songs}首歌後關閉', playlistEndSetSuccess: '已設定播放完清單後關閉', timerCancelled: '已取消定時關閉', timerEnded: '定時關閉已觸發', playbackStopped: '音樂播放已停止', minutesRemaining: '剩餘{minutes}分鐘', songsRemaining: '剩餘{count}首歌' }, playList: { clearAll: '清空播放清單', alreadyEmpty: '播放清單已經為空', cleared: '已清空播放清單', empty: '播放清單為空', clearConfirmTitle: '清空播放清單', clearConfirmContent: '這將清空所有播放清單中的歌曲並停止目前播放。是否繼續?' } }; ================================================ FILE: src/i18n/lang/zh-Hant/search.ts ================================================ export default { title: { hotSearch: '熱搜列表', searchList: '搜尋列表', searchHistory: '搜尋歷史' }, button: { clear: '清空', back: '返回', playAll: '播放列表' }, loading: { more: '載入中...', failed: '搜尋失敗', searching: '搜尋中...' }, noMore: '沒有更多了', error: { searchFailed: '搜尋失敗' }, search: { single: '單曲', album: '專輯', playlist: '歌單', mv: 'MV', bilibili: 'B站' }, history: '搜尋歷史', hot: '熱門搜尋', suggestions: '搜尋建議' }; ================================================ FILE: src/i18n/lang/zh-Hant/settings.ts ================================================ export default { theme: '主題', language: '語言', regard: '關於', logout: '登出', sections: { basic: '基礎設定', playback: '播放設定', application: '應用程式設定', network: '網路設定', system: '系統管理', donation: '捐贈支持', about: '關於' }, basic: { themeMode: '主題模式', themeModeDesc: '切換日間/夜間主題', autoTheme: '跟隨系統', manualTheme: '手動切換', language: '語言設定', languageDesc: '切換顯示語言', tokenManagement: 'Cookie管理', tokenManagementDesc: '管理網易雲音樂登入Cookie', tokenStatus: '目前Cookie狀態', tokenSet: '已設定', tokenNotSet: '未設定', setToken: '設定Cookie', setCookie: '設定Cookie', modifyToken: '修改Cookie', clearToken: '清除Cookie', font: '字體設定', fontDesc: '選擇字體,優先使用排在前面的字體', fontScope: { global: '全域', lyric: '僅歌詞' }, animation: '動畫速度', animationDesc: '是否開起動畫', animationSpeed: { slow: '極慢', normal: '正常', fast: '極快' }, fontPreview: { title: '字體預覽', chinese: '中文', english: 'English', japanese: '日本語', korean: '한국어', chineseText: '靜夜思 床前明月光 疑是地上霜', englishText: 'The quick brown fox jumps over the lazy dog', japaneseText: 'あいうえお かきくけこ さしすせそ', koreanText: '가나다라마 바사아자차 카타파하' }, gpuAcceleration: 'GPU加速', gpuAccelerationDesc: '啟用或禁用硬體加速,可以提高渲染性能,但可能會增加GPU負載', gpuAccelerationRestart: '更改GPU加速設定需要重啟應用後生效', gpuAccelerationChangeSuccess: 'GPU加速設定已更新,重啟應用後生效', gpuAccelerationChangeError: 'GPU加速設定更新失敗', tabletMode: '平板模式', tabletModeDesc: '啟用後將在移動設備上使用PC樣式界面,適合平板等大屏設備' }, playback: { quality: '音質設定', qualityDesc: '選擇音樂播放音質(網易云VIP)', qualityOptions: { standard: '標準', higher: '較高', exhigh: '極高', lossless: '無損', hires: 'Hi-Res', jyeffect: '高清環繞聲', sky: '沉浸環繞聲', dolby: '杜比全景聲', jymaster: '超清母帶' }, musicSources: '音源設定', musicSourcesDesc: '選擇音樂解析使用的音源平台', musicSourcesWarning: '至少需要選擇一個音源平台', musicUnblockEnable: '啟用音樂解析', musicUnblockEnableDesc: '開啟後將嘗試解析無法播放的音樂', configureMusicSources: '設定音源', selectedMusicSources: '已選音源:', noMusicSources: '未選擇音源', gdmusicInfo: 'GD音樂台可自動解析多個平台音源,自動選擇最佳結果', autoPlay: '自動播放', autoPlayDesc: '重新開啟應用程式時是否自動繼續播放', showStatusBar: '是否顯示狀態列控制功能', showStatusBarContent: '可以在您的mac狀態列顯示音樂控制功能(重啟後生效)', fallbackParser: '備用解析服務 (GD音樂台)', fallbackParserDesc: '當勾選「GD音樂台」且常規音源無法播放時,將使用此服務嘗試解析。', parserGD: 'GD 音樂台 (內建)', parserCustom: '自訂 API', // 音源標籤 sourceLabels: { migu: '咪咕音樂', kugou: '酷狗音樂', pyncmd: '網易雲(內建)', bilibili: 'Bilibili', gdmusic: 'GD音樂台', custom: '自訂 API' }, customApi: { sectionTitle: '自訂 API 設定', importConfig: '匯入 JSON 設定', currentSource: '目前音源', notImported: '尚未匯入自訂音源。', importSuccess: '成功匯入音源:{name}', importFailed: '匯入失敗:{message}', enableHint: '請先匯入 JSON 設定檔才能啟用', status: { imported: '已匯入自訂音源', notImported: '未匯入' } }, lxMusic: { tabs: { sources: '音源選擇', lxMusic: '落雪音源', customApi: '自訂API' }, scripts: { title: '已匯入的音源腳本', importLocal: '本機匯入', importOnline: '線上匯入', urlPlaceholder: '輸入落雪音源腳本 URL', importBtn: '匯入', empty: '暫無已匯入的落雪音源', notConfigured: '未設定 (請至落雪音源分頁設定)', importHint: '匯入相容的自訂 API 外掛以擴充音源', noScriptWarning: '請先匯入落雪音源腳本', noSelectionWarning: '請先選擇一個落雪音源', notFound: '音源不存在', switched: '已切換到音源: {name}', deleted: '已刪除音源: {name}', enterUrl: '請輸入腳本 URL', invalidUrl: '無效的 URL 格式', invalidScript: '無效的落雪音源腳本,未找到 globalThis.lx 相關程式碼', nameRequired: '名稱不能為空', renameSuccess: '重新命名成功' } } }, application: { closeAction: '關閉行為', closeActionDesc: '選擇關閉視窗時的行為', closeOptions: { ask: '每次詢問', minimize: '最小化到系統匣', close: '直接退出' }, shortcut: '快捷鍵設定', shortcutDesc: '自訂全域快捷鍵', download: '下載管理', downloadDesc: '是否始終顯示下載清單按鈕', unlimitedDownload: '無限制下載', unlimitedDownloadDesc: '開啟後將無限制下載音樂(可能出現下載失敗的情況), 預設限制 300 首', downloadPath: '下載目錄', downloadPathDesc: '選擇音樂檔案的下載位置', remoteControl: '遠端控制', remoteControlDesc: '設定遠端控制功能' }, network: { apiPort: '音樂API連接埠', apiPortDesc: '修改後需要重啟應用程式', proxy: '代理設定', proxyDesc: '無法存取音樂時可以開啟代理', proxyHost: '代理位址', proxyHostPlaceholder: '請輸入代理位址', proxyPort: '代理連接埠', proxyPortPlaceholder: '請輸入代理連接埠', realIP: 'realIP設定', realIPDesc: '由於限制,此項目在國外使用會受到限制可使用realIP參數,傳進國內IP解決', messages: { proxySuccess: '代理設定已儲存,重啟應用程式後生效', proxyError: '請檢查輸入是否正確', realIPSuccess: '真實IP設定已儲存', realIPError: '請輸入有效的IP位址' } }, system: { cache: '快取管理', cacheDesc: '清除快取', cacheClearTitle: '請選擇要清除的快取類型:', cacheTypes: { history: { label: '播放歷史', description: '清除播放過的歌曲記錄' }, favorite: { label: '收藏記錄', description: '清除本機收藏的歌曲記錄(不會影響雲端收藏)' }, user: { label: '使用者資料', description: '清除登入資訊和使用者相關資料' }, settings: { label: '應用程式設定', description: '清除應用程式的所有自訂設定' }, downloads: { label: '下載記錄', description: '清除下載歷史記錄(不會刪除已下載的檔案)' }, resources: { label: '音樂資源', description: '清除已載入的音樂檔案、歌詞等資源快取' }, lyrics: { label: '歌詞資源', description: '清除已載入的歌詞資源快取' } }, restart: '重新啟動', restartDesc: '重新啟動應用程式', messages: { clearSuccess: '清除成功,部分設定在重啟後生效' } }, about: { version: '版本', checkUpdate: '檢查更新', checking: '檢查中...', latest: '目前已是最新版本', hasUpdate: '發現新版本', gotoUpdate: '前往更新', gotoGithub: '前往 Github', author: '作者', authorDesc: 'algerkong 點個star🌟呗', messages: { checkError: '檢查更新失敗,請稍後重試' } }, validation: { selectProxyProtocol: '請選擇代理協議', proxyHost: '請輸入代理位址', portNumber: '請輸入有效的連接埠號(1-65535)' }, lyricSettings: { title: '歌詞設定', tabs: { display: '顯示', interface: '介面', typography: '文字', background: '背景', mobile: '行動端' }, pureMode: '純淨模式', hideCover: '隱藏封面', centerDisplay: '置中顯示', showTranslation: '顯示翻譯', hideLyrics: '隱藏歌詞', hidePlayBar: '隱藏播放列', hideMiniPlayBar: '隱藏迷你播放列', showMiniPlayBar: '顯示迷你播放列', backgroundTheme: '背景主題', themeOptions: { default: '預設', light: '亮色', dark: '暗色' }, fontSize: '字體大小', fontSizeMarks: { small: '小', medium: '中', large: '大' }, fontWeight: '字體粗細', fontWeightMarks: { thin: '細', normal: '常規', bold: '粗' }, letterSpacing: '字間距', letterSpacingMarks: { compact: '緊湊', default: '預設', loose: '寬鬆' }, lineHeight: '行高', lineHeightMarks: { compact: '緊湊', default: '預設', loose: '寬鬆' }, contentWidth: '內容區寬度', mobileLayout: '行動端佈局', layoutOptions: { default: '預設', ios: 'iOS 風格', android: 'Android 風格' }, mobileCoverStyle: '封面風格', coverOptions: { record: '唱片', square: '方形', full: '全螢幕' }, lyricLines: '歌詞行數', mobileUnavailable: '此設定僅在行動端可用', // 背景設定 background: { useCustomBackground: '使用自訂背景', backgroundMode: '背景模式', modeOptions: { solid: '純色', gradient: '漸層', image: '圖片', css: 'CSS' }, solidColor: '選擇顏色', presetColors: '預設顏色', customColor: '自訂顏色', gradientEditor: '漸層編輯器', gradientColors: '漸層顏色', gradientDirection: '漸層方向', directionOptions: { toBottom: '上到下', toRight: '左到右', toBottomRight: '左上到右下', angle45: '45度', toTop: '下到上', toLeft: '右到左' }, addColor: '新增顏色', removeColor: '移除顏色', imageUpload: '上傳圖片', imagePreview: '圖片預覽', clearImage: '清除圖片', imageBlur: '模糊度', imageBrightness: '明暗度', customCss: '自訂 CSS 樣式', customCssPlaceholder: '輸入 CSS 樣式,如: background: linear-gradient(...)', customCssHelp: '支援任意 CSS background 屬性', reset: '重設為預設', fileSizeLimit: '圖片大小限制: 20MB', invalidImageFormat: '無效的圖片格式', imageTooLarge: '圖片過大,請選擇小於 20MB 的圖片' } }, themeColor: { title: '歌詞主題色', presetColors: '預設顏色', customColor: '自訂顏色', preview: '預覽效果', previewText: '歌詞效果', colorNames: { 'spotify-green': 'Spotify 綠', 'apple-blue': '蘋果藍', 'youtube-red': 'YouTube 紅', orange: '活力橙', purple: '神秘紫', pink: '櫻花粉' }, tooltips: { openColorPicker: '開啟色板', closeColorPicker: '關閉色板' }, placeholder: '#1db954' }, translationEngine: '歌詞翻譯引擎', translationEngineOptions: { none: '關閉', opencc: 'OpenCC 繁化' }, shortcutSettings: { title: '快捷鍵設定', shortcut: '快捷鍵', shortcutDesc: '自訂快捷鍵', shortcutConflict: '快捷鍵衝突', inputPlaceholder: '點擊輸入快捷鍵', resetShortcuts: '恢復預設', disableAll: '全部停用', enableAll: '全部啟用', togglePlay: '播放/暫停', prevPlay: '上一首', nextPlay: '下一首', volumeUp: '增加音量', volumeDown: '減少音量', toggleFavorite: '收藏/取消收藏', toggleWindow: '顯示/隱藏視窗', scopeGlobal: '全域', scopeApp: '應用程式內', enabled: '已啟用', disabled: '已停用', messages: { resetSuccess: '已恢復預設快捷鍵,請記得儲存', conflict: '存在快捷鍵衝突,請重新設定', saveSuccess: '快捷鍵設定已儲存', saveError: '快捷鍵儲存失敗,請重試', cancelEdit: '已取消修改', disableAll: '已停用所有快捷鍵,請記得儲存', enableAll: '已啟用所有快捷鍵,請記得儲存' } }, remoteControl: { title: '遠端控制', enable: '啟用遠端控制', port: '服務連接埠', allowedIps: '允許的 IP 位址', addIp: '新增 IP', emptyListHint: '空白清單表示允許所有 IP 存取', saveSuccess: '遠端控制設定已儲存', accessInfo: '遠端控制存取位址:' }, cookie: { title: 'Cookie設定', description: '請輸入網易雲音樂的Cookie:', placeholder: '請貼上完整的Cookie...', help: { format: 'Cookie通常以 "MUSIC_U=" 開頭', source: '可以從瀏覽器開發者工具的網路請求中取得', storage: 'Cookie設定後將自動儲存到本機儲存' }, action: { save: '儲存Cookie', paste: '貼上', clear: '清空' }, validation: { required: '請輸入Cookie', format: 'Cookie格式可能不正確,請檢查是否包含MUSIC_U' }, message: { saveSuccess: 'Cookie儲存成功', saveError: 'Cookie儲存失敗', pasteSuccess: '貼上成功', pasteError: '貼上失敗,請手動複製' }, info: { length: '目前長度:{length} 字元' } } }; ================================================ FILE: src/i18n/lang/zh-Hant/songItem.ts ================================================ export default { menu: { play: '播放', playNext: '下一首播放', download: '下載歌曲', addToPlaylist: '新增至播放清單', favorite: '喜歡', unfavorite: '取消喜歡', removeFromPlaylist: '從播放清單中刪除', dislike: '不喜歡', undislike: '取消不喜歡' }, message: { downloading: '正在下載中,請稍候...', downloadFailed: '下載失敗', downloadQueued: '已加入下載佇列', addedToNextPlay: '已新增至下一首播放', getUrlFailed: '取得音樂下載位址失敗,請檢查是否登入' }, dialog: { dislike: { title: '提示!', content: '確認不喜歡這首歌嗎?再次進入將從每日推薦中排除。', positiveText: '不喜歡', negativeText: '取消' } } }; ================================================ FILE: src/i18n/lang/zh-Hant/user.ts ================================================ export default { profile: { followers: '粉絲', following: '關注', level: '等級' }, playlist: { created: '建立的歌單', mine: '我建立的', trackCount: '{count}首', playCount: '播放{count}次' }, tabs: { created: '建立', favorite: '收藏', album: '專輯' }, ranking: { title: '聽歌排行', playCount: '{count}次' }, follow: { title: '關注列表', viewPlaylist: '查看歌單', noFollowings: '暫無關注', loadMore: '載入更多', noSignature: '這個傢伙很懶,什麼都沒留下', userFollowsTitle: '的關注', myFollowsTitle: '我的關注' }, follower: { title: '粉絲列表', noFollowers: '暫無粉絲', loadMore: '載入更多', userFollowersTitle: '的粉絲', myFollowersTitle: '我的粉絲' }, detail: { playlists: '歌單', records: '聽歌排行', noPlaylists: '暫無歌單', noRecords: '暫無聽歌記錄', artist: '歌手', noSignature: '這個人很懶,什麼都沒留下', invalidUserId: '使用者ID無效', noRecordPermission: '{name}不讓你聽歌排行' }, message: { loadFailed: '載入使用者頁面失敗', deleteSuccess: '刪除成功', deleteFailed: '刪除失敗' } }; ================================================ FILE: src/i18n/languages.ts ================================================ // 语言配置文件 - 集中管理语言相关的配置 // 语言显示名称映射 export const LANGUAGE_DISPLAY_NAMES: Record = { 'zh-CN': '简体中文', 'zh-Hant': '繁體中文', 'en-US': 'English', 'ja-JP': '日本語', 'ko-KR': '한국어' }; // 默认语言 export const DEFAULT_LANGUAGE = 'zh-CN'; // 回退语言 export const FALLBACK_LANGUAGE = 'en-US'; // 语言排序优先级(用于在UI中的显示顺序) export const LANGUAGE_PRIORITY: Record = { 'zh-CN': 1, 'zh-Hant': 2, 'en-US': 3, 'ja-JP': 4, 'ko-KR': 5 }; ================================================ FILE: src/i18n/main.ts ================================================ import { DEFAULT_LANGUAGE } from './languages'; import { buildLanguageMessages } from './utils'; // 使用工具函数构建语言消息对象 const messages = buildLanguageMessages(); type Language = keyof typeof messages; // 为主进程提供一个简单的 i18n 实现 const mainI18n = { global: { currentLocale: DEFAULT_LANGUAGE as Language, get locale() { return this.currentLocale; }, set locale(value: Language) { this.currentLocale = value; }, t(key: string) { const keys = key.split('.'); let current: any = messages[this.currentLocale]; for (const k of keys) { if (current[k] === undefined) { // 如果找不到翻译,返回键名 return key; } current = current[k]; } return current; }, messages } }; export type { Language }; export default mainI18n; ================================================ FILE: src/i18n/renderer.ts ================================================ import { createI18n } from 'vue-i18n'; import { DEFAULT_LANGUAGE, FALLBACK_LANGUAGE } from './languages'; import { buildLanguageMessages } from './utils'; // 使用工具函数构建语言消息对象 const messages = buildLanguageMessages(); const i18n = createI18n({ legacy: false, locale: DEFAULT_LANGUAGE, fallbackLocale: FALLBACK_LANGUAGE, messages, globalInjection: true, silentTranslationWarn: true, silentFallbackWarn: true }); export default i18n; ================================================ FILE: src/i18n/utils.ts ================================================ // 自动导入所有语言的所有翻译文件 const allLangModules = import.meta.glob('./lang/**/*.ts', { eager: true }); // 构建语言消息对象 export const buildLanguageMessages = () => { const messages: Record> = {}; Object.entries(allLangModules).forEach(([path, module]) => { // 解析路径,例如 './lang/zh-CN/common.ts' -> { lang: 'zh-CN', module: 'common' } const match = path.match(/\.\/lang\/([^/]+)\/([^/]+)\.ts$/); if (match) { const [, langCode, moduleName] = match; // 跳过 index 文件 if (moduleName !== 'index') { if (!messages[langCode]) { messages[langCode] = {}; } messages[langCode][moduleName] = (module as any).default; } } }); return messages; }; // 获取所有支持的语言 export const getSupportedLanguages = (): string[] => { const messages = buildLanguageMessages(); return Object.keys(messages); }; export const isLanguageSupported = (lang: string): boolean => { return getSupportedLanguages().includes(lang); }; import { LANGUAGE_DISPLAY_NAMES, LANGUAGE_PRIORITY } from './languages'; // 获取语言显示名称的映射 export const getLanguageDisplayNames = (): Record => { return LANGUAGE_DISPLAY_NAMES; }; // 生成语言选项数组,用于下拉选择等组件 export const getLanguageOptions = () => { const supportedLanguages = getSupportedLanguages(); const displayNames = getLanguageDisplayNames(); // 按优先级排序 const sortedLanguages = supportedLanguages.sort((a, b) => { const priorityA = LANGUAGE_PRIORITY[a] || 999; const priorityB = LANGUAGE_PRIORITY[b] || 999; return priorityA - priorityB; }); return sortedLanguages.map((lang) => ({ label: displayNames[lang] || lang, value: lang })); }; ================================================ FILE: src/main/index.ts ================================================ import { electronApp, optimizer } from '@electron-toolkit/utils'; import { app, ipcMain, nativeImage } from 'electron'; import { join } from 'path'; import type { Language } from '../i18n/main'; import i18n from '../i18n/main'; import { loadLyricWindow } from './lyric'; import { initializeConfig } from './modules/config'; import { initializeFileManager } from './modules/fileManager'; import { initializeFonts } from './modules/fonts'; import { initializeLoginWindow } from './modules/loginWindow'; import { initializeOtherApi } from './modules/otherApi'; import { initializeRemoteControl } from './modules/remoteControl'; import { initializeShortcuts, registerShortcuts } from './modules/shortcuts'; import { initializeTray, updateCurrentSong, updatePlayState, updateTrayMenu } from './modules/tray'; import { setupUpdateHandlers } from './modules/update'; import { createMainWindow, initializeWindowManager, setAppQuitting } from './modules/window'; import { initWindowSizeManager } from './modules/window-size'; import { startMusicApi } from './server'; import { initLxMusicHttp } from './modules/lxMusicHttp'; // 导入所有图标 const iconPath = join(__dirname, '../../resources'); const icon = nativeImage.createFromPath( process.platform === 'darwin' ? join(iconPath, 'icon.icns') : join(iconPath, 'icon.png') ); let mainWindow: Electron.BrowserWindow; // 初始化应用 function initialize(configStore: any) { // 使用已初始化的配置存储 const store = configStore; // 设置初始语言 const savedLanguage = store.get('set.language') as Language; if (savedLanguage) { i18n.global.locale = savedLanguage; } // 初始化文件管理 initializeFileManager(); // 初始化其他 API (搜索建议等) initializeOtherApi(); // 初始化窗口管理 initializeWindowManager(); // 初始化字体管理 initializeFonts(); // 初始化登录窗口 initializeLoginWindow(); // 创建主窗口 mainWindow = createMainWindow(icon); // 初始化托盘 initializeTray(iconPath, mainWindow); // 启动音乐API startMusicApi(); // 初始化落雪音乐 HTTP 请求处理 initLxMusicHttp(); // 加载歌词窗口 loadLyricWindow(ipcMain, mainWindow); // 初始化快捷键 initializeShortcuts(mainWindow); // 初始化远程控制服务 initializeRemoteControl(mainWindow); // 初始化更新处理程序 setupUpdateHandlers(mainWindow); } // 检查是否为第一个实例 const isSingleInstance = app.requestSingleInstanceLock(); if (!isSingleInstance) { app.quit(); } else { // 在应用准备就绪前初始化GPU加速设置 // 必须在 app.ready 之前调用 disableHardwareAcceleration try { // 初始化配置管理以获取GPU加速设置 const store = initializeConfig(); const enableGpuAcceleration = store.get('set.enableGpuAcceleration', true) as boolean; if (!enableGpuAcceleration) { console.log('GPU加速已禁用'); app.disableHardwareAcceleration(); } else { console.log('GPU加速已启用'); } } catch (error) { console.error('GPU加速设置初始化失败:', error); // 如果配置读取失败,默认启用GPU加速 } // 当第二个实例启动时,将焦点转移到第一个实例的窗口 app.on('second-instance', () => { if (mainWindow) { if (mainWindow.isMinimized()) { mainWindow.restore(); } mainWindow.show(); mainWindow.focus(); } }); // 应用程序准备就绪时的处理 app.whenReady().then(() => { // 设置应用ID electronApp.setAppUserModelId('com.alger.music'); // 监听窗口创建事件 app.on('browser-window-created', (_, window) => { optimizer.watchWindowShortcuts(window); }); // 初始化窗口大小管理器 initWindowSizeManager(); // 重新初始化配置管理以获取完整的配置存储 const store = initializeConfig(); // 初始化应用 initialize(store); // macOS 激活应用时的处理 app.on('activate', () => { if (mainWindow === null) initialize(store); }); }); // 监听快捷键更新 ipcMain.on('update-shortcuts', () => { registerShortcuts(mainWindow); }); // 监听语言切换 ipcMain.on('change-language', (_, locale: Language) => { // 更新主进程的语言设置 i18n.global.locale = locale; // 更新托盘菜单 updateTrayMenu(mainWindow); // 通知所有窗口语言已更改 mainWindow?.webContents.send('language-changed', locale); }); // 监听播放状态变化 ipcMain.on('update-play-state', (_, playing: boolean) => { updatePlayState(playing); }); // 监听当前歌曲变化 ipcMain.on('update-current-song', (_, song: any) => { updateCurrentSong(song); }); // 所有窗口关闭时的处理 app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); // 应用即将退出时的处理 app.on('before-quit', () => { // 设置退出标志 setAppQuitting(true); }); // 重启应用 ipcMain.on('restart', () => { app.relaunch(); app.exit(0); }); // 获取系统架构信息 ipcMain.on('get-arch', (event) => { event.returnValue = process.arch; }); } ================================================ FILE: src/main/lyric.ts ================================================ import { BrowserWindow, IpcMain, screen } from 'electron'; import Store from 'electron-store'; import path, { join } from 'path'; const store = new Store(); let lyricWindow: BrowserWindow | null = null; // 跟踪拖动状态 let isDragging = false; // 添加窗口大小变化防护 let originalSize = { width: 0, height: 0 }; const createWin = () => { console.log('Creating lyric window'); // 获取保存的窗口位置 const windowBounds = (store.get('lyricWindowBounds') as { x?: number; y?: number; width?: number; height?: number; displayId?: number; }) || {}; const { x, y, width, height, displayId } = windowBounds; // 获取所有屏幕的信息 const displays = screen.getAllDisplays(); let isValidPosition = false; let targetDisplay = displays[0]; // 默认使用主显示器 // 如果有显示器ID,尝试按ID匹配 if (displayId) { const matchedDisplay = displays.find((d) => d.id === displayId); if (matchedDisplay) { targetDisplay = matchedDisplay; console.log('Found matching display by ID:', displayId); } } // 验证位置是否在任何显示器的范围内 if (x !== undefined && y !== undefined) { for (const display of displays) { const { bounds } = display; if ( x >= bounds.x - 50 && // 允许一点偏移,避免卡在边缘 x < bounds.x + bounds.width + 50 && y >= bounds.y - 50 && y < bounds.y + bounds.height + 50 ) { isValidPosition = true; targetDisplay = display; break; } } } // 确保宽高合理 const defaultWidth = 800; const defaultHeight = 200; const maxWidth = 1600; // 设置最大宽度限制 const maxHeight = 800; // 设置最大高度限制 const validWidth = width && width > 0 && width <= maxWidth ? width : defaultWidth; const validHeight = height && height > 0 && height <= maxHeight ? height : defaultHeight; // 确定窗口位置 let windowX = isValidPosition ? x : undefined; let windowY = isValidPosition ? y : undefined; // 如果位置无效,默认在当前显示器中居中 if (windowX === undefined || windowY === undefined) { windowX = targetDisplay.bounds.x + (targetDisplay.bounds.width - validWidth) / 2; windowY = targetDisplay.bounds.y + (targetDisplay.bounds.height - validHeight) / 2; } lyricWindow = new BrowserWindow({ width: validWidth, height: validHeight, x: windowX, y: windowY, frame: false, show: false, transparent: true, opacity: 1, hasShadow: false, alwaysOnTop: true, resizable: true, roundedCorners: false, titleBarStyle: 'hidden', titleBarOverlay: false, // 添加跨屏幕支持选项 webPreferences: { preload: join(__dirname, '../preload/index.js'), sandbox: false, contextIsolation: true }, backgroundColor: '#00000000' }); // 监听窗口关闭事件 lyricWindow.on('closed', () => { if (lyricWindow) { lyricWindow.destroy(); lyricWindow = null; } }); // 监听窗口大小变化事件,保存新的尺寸 lyricWindow.on('resize', () => { // 如果正在拖动,忽略大小调整事件 if (isDragging) return; if (lyricWindow && !lyricWindow.isDestroyed()) { const [width, height] = lyricWindow.getSize(); const [x, y] = lyricWindow.getPosition(); // 保存窗口位置和大小 store.set('lyricWindowBounds', { x, y, width, height }); } }); lyricWindow.on('blur', () => lyricWindow && lyricWindow.setMaximizable(false)); return lyricWindow; }; export const loadLyricWindow = (ipcMain: IpcMain, mainWin: BrowserWindow): void => { const showLyricWindow = () => { if (lyricWindow && !lyricWindow.isDestroyed()) { if (lyricWindow.isMinimized()) { lyricWindow.restore(); } lyricWindow.focus(); lyricWindow.show(); return true; } return false; }; ipcMain.on('open-lyric', () => { console.log('Received open-lyric request'); if (showLyricWindow()) { return; } console.log('Creating new lyric window'); const win = createWin(); if (!win) { console.error('Failed to create lyric window'); return; } if (process.env.NODE_ENV === 'development') { win.webContents.openDevTools({ mode: 'detach' }); win.loadURL(`${process.env.ELECTRON_RENDERER_URL}/#/lyric`); } else { const distPath = path.resolve(__dirname, '../renderer'); win.loadURL(`file://${distPath}/index.html#/lyric`); } win.setMinimumSize(600, 200); win.setSkipTaskbar(true); win.once('ready-to-show', () => { console.log('Lyric window ready to show'); win.show(); }); }); ipcMain.on('send-lyric', (_, data) => { if (lyricWindow && !lyricWindow.isDestroyed()) { try { lyricWindow.webContents.send('receive-lyric', data); } catch (error) { console.error('Error processing lyric data:', error); } } }); ipcMain.on('top-lyric', (_, data) => { if (lyricWindow && !lyricWindow.isDestroyed()) { lyricWindow.setAlwaysOnTop(data); } }); ipcMain.on('close-lyric', () => { if (lyricWindow && !lyricWindow.isDestroyed()) { lyricWindow.webContents.send('lyric-window-close'); mainWin.webContents.send('lyric-control-back', 'close'); mainWin.webContents.send('lyric-window-closed'); lyricWindow.destroy(); lyricWindow = null; } }); // 处理鼠标事件 ipcMain.on('mouseenter-lyric', () => { if (lyricWindow && !lyricWindow.isDestroyed()) { lyricWindow.setIgnoreMouseEvents(true); } }); ipcMain.on('mouseleave-lyric', () => { if (lyricWindow && !lyricWindow.isDestroyed()) { lyricWindow.setIgnoreMouseEvents(false); } }); // 开始拖动时设置标志 ipcMain.on('lyric-drag-start', () => { isDragging = true; if (lyricWindow && !lyricWindow.isDestroyed()) { // 记录原始窗口大小 const [width, height] = lyricWindow.getSize(); originalSize = { width, height }; } }); // 结束拖动时清除标志 ipcMain.on('lyric-drag-end', () => { isDragging = false; if (lyricWindow && !lyricWindow.isDestroyed()) { // 确保窗口大小恢复原样 lyricWindow.setSize(originalSize.width, originalSize.height); } }); // 处理拖动移动 ipcMain.on('lyric-drag-move', (_, { deltaX, deltaY }) => { if (!lyricWindow || lyricWindow.isDestroyed() || !isDragging) return; const [currentX, currentY] = lyricWindow.getPosition(); // 使用记录的原始大小,而不是当前大小 const windowWidth = originalSize.width; const windowHeight = originalSize.height; // 计算新位置 const newX = currentX + deltaX; const newY = currentY + deltaY; try { // 获取当前鼠标所在的显示器 const mousePoint = screen.getCursorScreenPoint(); const currentDisplay = screen.getDisplayNearestPoint(mousePoint); // 拖动期间使用setBounds确保大小不变,使用false避免动画卡顿 lyricWindow.setBounds( { x: newX, y: newY, width: windowWidth, height: windowHeight }, false ); // 更新存储的位置 const windowBounds = { x: newX, y: newY, width: windowWidth, height: windowHeight, displayId: currentDisplay.id // 记录当前显示器ID,有助于多屏幕处理 }; store.set('lyricWindowBounds', windowBounds); } catch (error) { console.error('Error during window drag:', error); // 出错时尝试使用更简单的方法 lyricWindow.setPosition(newX, newY); } }); // 添加鼠标穿透事件处理 ipcMain.on('set-ignore-mouse', (_, shouldIgnore) => { if (!lyricWindow || lyricWindow.isDestroyed()) return; lyricWindow.setIgnoreMouseEvents(shouldIgnore, { forward: true }); }); // 添加播放控制处理 ipcMain.on('control-back', (_, command) => { console.log('command', command); if (mainWin && !mainWin.isDestroyed()) { console.log('Sending control-back command:', command); mainWin.webContents.send('lyric-control-back', command); } }); }; ================================================ FILE: src/main/modules/cache.ts ================================================ import { ipcMain } from 'electron'; import Store from 'electron-store'; interface LyricData { id: number; data: any; timestamp: number; } interface StoreSchema { lyrics: Record; } class CacheManager { private store: Store; constructor() { this.store = new Store({ name: 'lyrics', defaults: { lyrics: {} } }); } async cacheLyric(id: number, data: any) { try { const lyrics = this.store.get('lyrics'); lyrics[id] = { id, data, timestamp: Date.now() }; this.store.set('lyrics', lyrics); return true; } catch (error) { console.error('Error caching lyric:', error); return false; } } async getCachedLyric(id: number) { try { const lyrics = this.store.get('lyrics'); const result = lyrics[id]; if (!result) return undefined; // 检查缓存是否过期(24小时) if (Date.now() - result.timestamp > 24 * 60 * 60 * 1000) { delete lyrics[id]; this.store.set('lyrics', lyrics); return undefined; } return result.data; } catch (error) { console.error('Error getting cached lyric:', error); return undefined; } } async clearLyricCache() { try { this.store.set('lyrics', {}); return true; } catch (error) { console.error('Error clearing lyric cache:', error); return false; } } } export const cacheManager = new CacheManager(); export function initializeCacheManager() { // 添加歌词缓存相关的 IPC 处理 ipcMain.handle('cache-lyric', async (_, id: number, lyricData: any) => { return await cacheManager.cacheLyric(id, lyricData); }); ipcMain.handle('get-cached-lyric', async (_, id: number) => { return await cacheManager.getCachedLyric(id); }); ipcMain.handle('clear-lyric-cache', async () => { return await cacheManager.clearLyricCache(); }); } ================================================ FILE: src/main/modules/config.ts ================================================ import { app, ipcMain } from 'electron'; import Store from 'electron-store'; import set from '../set.json'; import { defaultShortcuts } from './shortcuts'; type SetConfig = { isProxy: boolean; proxyConfig: { enable: boolean; protocol: string; host: string; port: number; }; enableRealIP: boolean; realIP: string; noAnimate: boolean; animationSpeed: number; author: string; authorUrl: string; musicApiPort: number; closeAction: 'ask' | 'minimize' | 'close'; musicQuality: string; fontFamily: string; fontScope: 'global' | 'lyric'; language: string; showTopAction: boolean; enableGpuAcceleration: boolean; }; interface StoreType { set: SetConfig; shortcuts: typeof defaultShortcuts; } let store: Store; /** * 初始化配置管理 */ export function initializeConfig() { store = new Store({ name: 'config', defaults: { set: set as SetConfig, shortcuts: defaultShortcuts } }); store.get('set.downloadPath') || store.set('set.downloadPath', app.getPath('downloads')); // 定义ipcRenderer监听事件 ipcMain.on('set-store-value', (_, key, value) => { store.set(key, value); }); ipcMain.on('get-store-value', (_, key) => { const value = store.get(key); _.returnValue = value || ''; }); // GPU加速设置更新处理 // 注意:GPU加速设置必须在应用启动时在app.ready之前设置才能生效 ipcMain.on('update-gpu-acceleration', (event, enabled: boolean) => { try { console.log('GPU加速设置更新:', enabled); store.set('set.enableGpuAcceleration', enabled); // GPU加速设置需要重启应用才能生效 event.sender.send('gpu-acceleration-updated', enabled); console.log('GPU加速设置已保存,重启应用后生效'); } catch (error) { console.error('GPU加速设置更新失败:', error); const errorMessage = error instanceof Error ? error.message : String(error); event.sender.send('gpu-acceleration-update-error', errorMessage); } }); return store; } export function getStore() { return store; } ================================================ FILE: src/main/modules/deviceInfo.ts ================================================ import { app } from 'electron'; import Store from 'electron-store'; import { machineIdSync } from 'node-machine-id'; import os from 'os'; const store = new Store(); /** * 获取设备唯一标识符 * 优先使用存储的ID,如果没有则获取机器ID并存储 */ export function getDeviceId(): string { let deviceId = store.get('deviceId') as string | undefined; if (!deviceId) { try { // 使用node-machine-id获取设备唯一标识 deviceId = machineIdSync(true); } catch (error) { console.error('获取机器ID失败:', error); // 如果获取失败,使用主机名和MAC地址组合作为备选方案 const networkInterfaces = os.networkInterfaces(); let macAddress = ''; // 尝试获取第一个非内部网络接口的MAC地址 Object.values(networkInterfaces).forEach((interfaces) => { if (interfaces) { interfaces.forEach((iface) => { if (!iface.internal && !macAddress && iface.mac !== '00:00:00:00:00:00') { macAddress = iface.mac; } }); } }); deviceId = `${os.hostname()}-${macAddress}`.replace(/:/g, ''); } // 存储设备ID if (deviceId) { store.set('deviceId', deviceId); } else { // 如果所有方法都失败,使用随机ID deviceId = Math.random().toString(36).substring(2, 15); store.set('deviceId', deviceId); } } return deviceId; } /** * 获取系统信息 */ export function getSystemInfo() { return { osType: os.type(), osVersion: os.release(), osArch: os.arch(), platform: process.platform, appVersion: app.getVersion() }; } ================================================ FILE: src/main/modules/fileManager.ts ================================================ import axios from 'axios'; import { app, dialog, ipcMain, Notification, protocol, shell } from 'electron'; import Store from 'electron-store'; import { fileTypeFromFile } from 'file-type'; import { FlacTagMap, writeFlacTags } from 'flac-tagger'; import * as fs from 'fs'; import * as http from 'http'; import * as https from 'https'; import * as mm from 'music-metadata'; import * as NodeID3 from 'node-id3'; import * as os from 'os'; import * as path from 'path'; import sharp from 'sharp'; import { getStore } from './config'; const MAX_CONCURRENT_DOWNLOADS = 3; const downloadQueue: { url: string; filename: string; songInfo: any; type?: string }[] = []; let activeDownloads = 0; // 创建一个store实例用于存储下载历史 const downloadStore = new Store({ name: 'downloads', defaults: { history: [] } }); // 创建一个store实例用于存储音频缓存 const audioCacheStore = new Store({ name: 'audioCache', defaults: { cache: {} } }); // 保存已发送通知的文件,避免重复通知 const sentNotifications = new Map(); /** * 初始化文件管理相关的IPC监听 */ export function initializeFileManager() { // 注册本地文件协议 protocol.registerFileProtocol('local', (request, callback) => { try { const url = request.url; // local://C:/Users/xxx.mp3 let filePath = decodeURIComponent(url.replace('local:///', '')); // 兼容 local:///C:/Users/xxx.mp3 这种情况 if (/^\/[a-zA-Z]:\//.test(filePath)) { filePath = filePath.slice(1); } // 还原为系统路径格式 filePath = path.normalize(filePath); // 检查文件是否存在 if (!fs.existsSync(filePath)) { console.error('File not found:', filePath); callback({ error: -6 }); // net::ERR_FILE_NOT_FOUND return; } callback({ path: filePath }); } catch (error) { console.error('Error handling local protocol:', error); callback({ error: -2 }); // net::FAILED } }); // 检查文件是否存在 ipcMain.handle('check-file-exists', (_, filePath) => { try { return fs.existsSync(filePath); } catch (error) { console.error('Error checking if file exists:', error); return false; } }); // 获取支持的音频格式列表 ipcMain.handle('get-supported-audio-formats', () => { return { formats: [ { ext: 'mp3', name: 'MP3' }, { ext: 'm4a', name: 'M4A/AAC' }, { ext: 'flac', name: 'FLAC' }, { ext: 'wav', name: 'WAV' }, { ext: 'ogg', name: 'OGG Vorbis' }, { ext: 'aac', name: 'AAC' } ], default: 'mp3' }; }); // 通用的选择目录处理 ipcMain.handle('select-directory', async () => { const result = await dialog.showOpenDialog({ properties: ['openDirectory'], title: '选择目录' }); return result; }); // 通用的打开目录处理 ipcMain.on('open-directory', (_, filePath) => { try { // 验证文件路径 if (!filePath) { console.error('无效的文件路径: 路径为空'); return; } // 统一处理路径分隔符 const normalizedPath = path.normalize(filePath); if (fs.statSync(normalizedPath).isDirectory()) { shell.openPath(normalizedPath); } else { shell.showItemInFolder(normalizedPath); } } catch (error) { console.error('打开路径失败:', error); } }); // 获取默认下载路径 ipcMain.handle('get-downloads-path', () => { return app.getPath('downloads'); }); // 获取存储的配置值 ipcMain.handle('get-store-value', (_, key) => { const store = new Store(); return store.get(key); }); // 设置存储的配置值 ipcMain.on('set-store-value', (_, key, value) => { const store = new Store(); store.set(key, value); }); // 下载音乐处理 ipcMain.on('download-music', handleDownloadRequest); // 检查文件是否已下载 ipcMain.handle('check-music-downloaded', (_, filename: string) => { const store = new Store(); const downloadPath = (store.get('set.downloadPath') as string) || app.getPath('downloads'); const filePath = path.join(downloadPath, `${filename}.mp3`); return fs.existsSync(filePath); }); // 删除已下载的音乐 ipcMain.handle('delete-downloaded-music', async (_, filePath: string) => { try { if (fs.existsSync(filePath)) { // 先删除文件 try { await fs.promises.unlink(filePath); } catch (error) { console.error('Error deleting file:', error); } // 删除对应的歌曲信息 const store = new Store(); const songInfos = store.get('downloadedSongs', {}) as Record; delete songInfos[filePath]; store.set('downloadedSongs', songInfos); return true; } return false; } catch (error) { console.error('Error deleting file:', error); return false; } }); // 获取已下载音乐列表 ipcMain.handle('get-downloaded-music', async () => { try { const store = new Store(); const songInfos = store.get('downloadedSongs', {}) as Record; // 异步处理文件存在性检查 const entriesArray = Object.entries(songInfos); const validEntriesPromises = await Promise.all( entriesArray.map(async ([path, info]) => { try { const exists = await fs.promises .access(path) .then(() => true) .catch(() => false); return exists ? info : null; } catch (error) { console.error('Error checking file existence:', error); return null; } }) ); // 过滤有效的歌曲并排序 const validSongs = validEntriesPromises .filter((song) => song !== null) .sort((a, b) => (b.downloadTime || 0) - (a.downloadTime || 0)); // 更新存储,移除不存在的文件记录 const newSongInfos = validSongs.reduce((acc, song) => { if (song && song.path) { acc[song.path] = song; } return acc; }, {}); store.set('downloadedSongs', newSongInfos); return validSongs; } catch (error) { console.error('Error getting downloaded music:', error); return []; } }); // 检查歌曲是否已下载并返回本地路径 ipcMain.handle('check-song-downloaded', (_, songId: number) => { const store = new Store(); const songInfos = store.get('downloadedSongs', {}) as Record; // 通过ID查找已下载的歌曲 for (const [path, info] of Object.entries(songInfos)) { if (info.id === songId && fs.existsSync(path)) { return { isDownloaded: true, localPath: `local://${path}`, songInfo: info }; } } return { isDownloaded: false, localPath: '', songInfo: null }; }); // 添加清除下载历史的处理函数 ipcMain.on('clear-downloads-history', () => { downloadStore.set('history', []); }); // 添加清除已下载音乐记录的处理函数 ipcMain.handle('clear-downloaded-music', () => { const store = new Store(); store.set('downloadedSongs', {}); return true; }); // 添加清除音频缓存的处理函数 ipcMain.on('clear-audio-cache', () => { audioCacheStore.set('cache', {}); // 清除临时音频文件目录 const tempDir = path.join(app.getPath('userData'), 'AudioCache'); if (fs.existsSync(tempDir)) { try { fs.readdirSync(tempDir).forEach((file) => { const filePath = path.join(tempDir, file); if (file.endsWith('.mp3') || file.endsWith('.m4a')) { fs.unlinkSync(filePath); } }); } catch (error) { console.error('清除音频缓存文件失败:', error); } } }); // 处理导入自定义API插件的请求 ipcMain.handle('import-custom-api-plugin', async () => { const result = await dialog.showOpenDialog({ title: '选择自定义音源配置文件', filters: [{ name: 'JSON Files', extensions: ['json'] }], properties: ['openFile'] }); if (result.canceled || result.filePaths.length === 0) { return null; } const filePath = result.filePaths[0]; try { const fileContent = fs.readFileSync(filePath, 'utf-8'); // 基础验证,确保它是个合法的JSON并且包含关键字段 const pluginData = JSON.parse(fileContent); if (!pluginData.name || !pluginData.apiUrl) { throw new Error('无效的插件文件,缺少 name 或 apiUrl 字段。'); } return { name: pluginData.name, content: fileContent // 返回完整的JSON字符串 }; } catch (error: any) { console.error('读取或解析插件文件失败:', error); // 向渲染进程抛出错误,以便UI可以显示提示 throw new Error(`文件读取或解析失败: ${error.message}`); } }); // 处理导入落雪音源脚本的请求 ipcMain.handle('import-lx-music-script', async () => { const result = await dialog.showOpenDialog({ title: '选择落雪音源脚本文件', filters: [{ name: 'JavaScript Files', extensions: ['js'] }], properties: ['openFile'] }); if (result.canceled || result.filePaths.length === 0) { return null; } const filePath = result.filePaths[0]; try { const fileContent = fs.readFileSync(filePath, 'utf-8'); // 验证脚本格式:检查是否包含落雪音源特征 if ( !fileContent.includes('globalThis.lx') && !fileContent.includes('lx.on') && !fileContent.includes('EVENT_NAMES') ) { throw new Error('无效的落雪音源脚本,未找到 globalThis.lx 相关代码。'); } // 检查是否包含必要的元信息注释 const hasMetaComment = fileContent.includes('@name'); if (!hasMetaComment) { console.warn('警告: 脚本缺少 @name 元信息注释'); } return { name: path.basename(filePath, '.js'), content: fileContent }; } catch (error: any) { console.error('读取落雪音源脚本失败:', error); throw new Error(`脚本读取失败: ${error.message}`); } }); } /** * 处理下载请求 */ function handleDownloadRequest( event: Electron.IpcMainEvent, { url, filename, songInfo, type }: { url: string; filename: string; songInfo?: any; type?: string } ) { // 检查是否已经在队列中或正在下载 if (downloadQueue.some((item) => item.filename === filename)) { event.reply('music-download-error', { filename, error: '该歌曲已在下载队列中' }); return; } // 检查是否已下载 const store = new Store(); const songInfos = store.get('downloadedSongs', {}) as Record; // 检查是否已下载(通过ID) const isDownloaded = songInfo?.id && Object.values(songInfos).some((info: any) => info.id === songInfo.id); if (isDownloaded) { event.reply('music-download-error', { filename, error: '该歌曲已下载' }); return; } // 添加到下载队列 downloadQueue.push({ url, filename, songInfo, type }); event.reply('music-download-queued', { filename, songInfo }); // 尝试开始下载 processDownloadQueue(event); } /** * 处理下载队列 */ async function processDownloadQueue(event: Electron.IpcMainEvent) { if (activeDownloads >= MAX_CONCURRENT_DOWNLOADS || downloadQueue.length === 0) { return; } const { url, filename, songInfo, type } = downloadQueue.shift()!; activeDownloads++; try { await downloadMusic(event, { url, filename, songInfo, type }); } finally { activeDownloads--; processDownloadQueue(event); } } /** * 清理文件名中的非法字符 */ function sanitizeFilename(filename: string): string { // 替换 Windows 和 Unix 系统中的非法字符 return filename .replace(/[<>:"/\\|?*]/g, '_') // 替换特殊字符为下划线 .replace(/\s+/g, ' ') // 将多个空格替换为单个空格 .trim(); // 移除首尾空格 } /** * 下载音乐和歌词 */ async function downloadMusic( event: Electron.IpcMainEvent, { url, filename, songInfo, type = 'mp3' }: { url: string; filename: string; songInfo: any; type?: string } ) { let finalFilePath = ''; let writer: fs.WriteStream | null = null; let tempFilePath = ''; try { // 使用配置Store来获取设置 const configStore = getStore(); const downloadPath = (configStore.get('set.downloadPath') as string) || app.getPath('downloads'); const apiPort = configStore.get('set.musicApiPort') || 30488; // 获取文件名格式设置 const nameFormat = (configStore.get('set.downloadNameFormat') as string) || '{songName} - {artistName}'; // 根据格式创建文件名 let formattedFilename = filename; if (songInfo) { // 准备替换变量 const artistName = songInfo.ar?.map((a: any) => a.name).join('、') || '未知艺术家'; const songName = songInfo.name || filename; const albumName = songInfo.al?.name || '未知专辑'; // 应用自定义格式 formattedFilename = nameFormat .replace(/\{songName\}/g, songName) .replace(/\{artistName\}/g, artistName) .replace(/\{albumName\}/g, albumName); } // 清理文件名中的非法字符 const sanitizedFilename = sanitizeFilename(formattedFilename); // 创建临时文件路径 (在系统临时目录中创建) const tempDir = path.join(os.tmpdir(), 'AlgerMusicPlayerTemp'); // 确保临时目录存在 if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } tempFilePath = path.join(tempDir, `${Date.now()}_${sanitizedFilename}.tmp`); // 先获取文件大小 const headResponse = await axios.head(url); const totalSize = parseInt(headResponse.headers['content-length'] || '0', 10); // 开始下载到临时文件 const response = await axios({ url, method: 'GET', responseType: 'stream', timeout: 30000, // 30秒超时 httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true }) }); writer = fs.createWriteStream(tempFilePath); let downloadedSize = 0; // 使用 data 事件来跟踪下载进度 response.data.on('data', (chunk: Buffer) => { downloadedSize += chunk.length; const progress = Math.round((downloadedSize / totalSize) * 100); event.reply('music-download-progress', { filename, progress, loaded: downloadedSize, total: totalSize, path: tempFilePath, status: progress === 100 ? 'completed' : 'downloading', songInfo: songInfo || { name: filename, ar: [{ name: '本地音乐' }], picUrl: '/images/default_cover.png' } }); }); // 等待下载完成 await new Promise((resolve, reject) => { writer!.on('finish', () => resolve(undefined)); writer!.on('error', (error) => reject(error)); response.data.pipe(writer!); }); // 验证文件是否完整下载 const stats = fs.statSync(tempFilePath); if (stats.size !== totalSize) { throw new Error('文件下载不完整'); } // 检测文件类型 let fileExtension = ''; try { // 首先尝试使用file-type库检测 const fileType = await fileTypeFromFile(tempFilePath); if (fileType && fileType.ext) { fileExtension = `.${fileType.ext}`; console.log(`文件类型检测结果: ${fileType.mime}, 扩展名: ${fileExtension}`); } else { // 如果file-type无法识别,尝试使用music-metadata const metadata = await mm.parseFile(tempFilePath); if (metadata && metadata.format) { // 根据format.container或codec判断扩展名 const formatInfo = metadata.format; const container = formatInfo.container || ''; const codec = formatInfo.codec || ''; // 音频格式映射表 const formatMap = { mp3: ['MPEG', 'MP3', 'mp3'], aac: ['AAC'], flac: ['FLAC'], ogg: ['Ogg', 'Vorbis'], wav: ['WAV', 'PCM'], m4a: ['M4A', 'MP4'] }; // 查找匹配的格式 const format = Object.entries(formatMap).find(([_, keywords]) => keywords.some((keyword) => container.includes(keyword) || codec.includes(keyword)) ); // 设置文件扩展名,如果没找到则默认为mp3 fileExtension = format ? `.${format[0]}` : '.mp3'; console.log( `music-metadata检测结果: 容器:${container}, 编码:${codec}, 扩展名: ${fileExtension}` ); } else { // 两种方法都失败,使用传入的type或默认mp3 fileExtension = type ? `.${type}` : '.mp3'; console.log(`无法检测文件类型,使用默认扩展名: ${fileExtension}`); } } } catch (err) { console.error('检测文件类型失败:', err); // 检测失败,使用传入的type或默认mp3 fileExtension = type ? `.${type}` : '.mp3'; } // 使用检测到的文件扩展名创建最终文件路径 const filePath = path.join(downloadPath, `${sanitizedFilename}${fileExtension}`); // 检查文件是否已存在,如果存在则添加序号 finalFilePath = filePath; let counter = 1; while (fs.existsSync(finalFilePath)) { const ext = path.extname(filePath); const nameWithoutExt = filePath.slice(0, -ext.length); finalFilePath = `${nameWithoutExt} (${counter})${ext}`; counter++; } // 将临时文件移动到最终位置 fs.copyFileSync(tempFilePath, finalFilePath); fs.unlinkSync(tempFilePath); // 删除临时文件 // 下载歌词 let lyricData = null; let lyricsContent = ''; try { if (songInfo?.id) { // 下载歌词,使用配置的端口 const lyricsResponse = await axios.get( `http://localhost:${apiPort}/lyric?id=${songInfo.id}` ); if (lyricsResponse.data && (lyricsResponse.data.lrc || lyricsResponse.data.tlyric)) { lyricData = lyricsResponse.data; // 处理歌词内容 if (lyricsResponse.data.lrc && lyricsResponse.data.lrc.lyric) { lyricsContent = lyricsResponse.data.lrc.lyric; // 如果有翻译歌词,合并到主歌词中 if (lyricsResponse.data.tlyric && lyricsResponse.data.tlyric.lyric) { // 解析原歌词和翻译 const originalLyrics = parseLyrics(lyricsResponse.data.lrc.lyric); const translatedLyrics = parseLyrics(lyricsResponse.data.tlyric.lyric); // 合并歌词 const mergedLyrics = mergeLyrics(originalLyrics, translatedLyrics); lyricsContent = mergedLyrics; } } console.log('歌词已准备好,将写入元数据'); } } } catch (lyricError) { console.error('下载歌词失败:', lyricError); // 继续处理,不影响音乐下载 } // 下载封面 let coverImageBuffer: Buffer | null = null; try { if (songInfo?.picUrl || songInfo?.al?.picUrl) { const picUrl = songInfo.picUrl || songInfo.al?.picUrl; if (picUrl && picUrl !== '/images/default_cover.png') { const coverResponse = await axios({ url: picUrl.replace('http://', 'https://'), method: 'GET', responseType: 'arraybuffer', timeout: 10000 }); const originalCoverBuffer = Buffer.from(coverResponse.data); const TWO_MB = 2 * 1024 * 1024; // 检查图片大小是否超过2MB if (originalCoverBuffer.length > TWO_MB) { const originalSizeMB = (originalCoverBuffer.length / (1024 * 1024)).toFixed(2); console.log(`封面图大于2MB (${originalSizeMB} MB),开始压缩...`); try { // 使用 sharp 进行压缩 coverImageBuffer = await sharp(originalCoverBuffer) .resize({ width: 1600, height: 1600, fit: 'inside', withoutEnlargement: true }) .jpeg({ quality: 80, mozjpeg: true }) .toBuffer(); const compressedSizeMB = (coverImageBuffer.length / (1024 * 1024)).toFixed(2); console.log(`封面图压缩完成,新大小: ${compressedSizeMB} MB`); } catch (compressionError) { console.error('封面图压缩失败,将使用原图:', compressionError); coverImageBuffer = originalCoverBuffer; // 如果压缩失败,则回退使用原始图片 } } else { // 如果图片不大于2MB,直接使用原图 coverImageBuffer = originalCoverBuffer; } console.log('封面已准备好,将写入元数据'); } } } catch (coverError) { console.error('下载封面失败:', coverError); // 继续处理,不影响音乐下载 } const fileFormat = fileExtension.toLowerCase(); const artistNames = (songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('、') || '未知艺术家'; // 根据文件类型处理元数据 if (['.mp3'].includes(fileFormat)) { // 对MP3文件使用NodeID3处理ID3标签 try { // 在写入ID3标签前,先移除可能存在的旧标签 NodeID3.removeTags(finalFilePath); const tags = { title: songInfo?.name, artist: artistNames, TPE1: artistNames, TPE2: artistNames, album: songInfo?.al?.name || songInfo?.song?.album?.name || songInfo?.name || filename, APIC: { // 专辑封面 imageBuffer: coverImageBuffer, type: { id: 3, name: 'front cover' }, description: 'Album cover', mime: 'image/jpeg' }, USLT: { // 歌词 language: 'chi', description: 'Lyrics', text: lyricsContent || '' }, trackNumber: songInfo?.no || undefined, year: songInfo?.publishTime ? new Date(songInfo.publishTime).getFullYear().toString() : undefined }; const success = NodeID3.write(tags, finalFilePath); if (!success) { console.error('Failed to write ID3 tags'); } else { console.log('ID3 tags written successfully'); } } catch (err) { console.error('Error writing ID3 tags:', err); } } else if (['.flac'].includes(fileFormat)) { try { const tagMap: FlacTagMap = { TITLE: songInfo?.name, ARTIST: artistNames, ALBUM: songInfo?.al?.name || songInfo?.song?.album?.name || songInfo?.name || filename, LYRICS: lyricsContent || '', TRACKNUMBER: songInfo?.no ? String(songInfo.no) : undefined, DATE: songInfo?.publishTime ? new Date(songInfo.publishTime).getFullYear().toString() : undefined }; await writeFlacTags( { tagMap, picture: coverImageBuffer ? { buffer: coverImageBuffer, mime: 'image/jpeg' } : undefined }, finalFilePath ); console.log('FLAC tags written successfully'); } catch (err) { console.error('Error writing FLAC tags:', err); } } // 保存下载信息 try { const songInfos = configStore.get('downloadedSongs', {}) as Record; const defaultInfo = { name: filename, ar: [{ name: '本地音乐' }], picUrl: '/images/default_cover.png' }; const newSongInfo = { id: songInfo?.id || 0, name: songInfo?.name || filename, filename, picUrl: songInfo?.picUrl || songInfo?.al?.picUrl || defaultInfo.picUrl, ar: songInfo?.ar || defaultInfo.ar, al: songInfo?.al || { picUrl: songInfo?.picUrl || defaultInfo.picUrl, name: songInfo?.name || filename }, size: totalSize, path: finalFilePath, downloadTime: Date.now(), type: fileExtension.substring(1), // 去掉前面的点号,只保留扩展名 lyric: lyricData }; // 保存到下载记录 songInfos[finalFilePath] = newSongInfo; configStore.set('downloadedSongs', songInfos); // 添加到下载历史 const history = downloadStore.get('history', []) as any[]; history.unshift(newSongInfo); downloadStore.set('history', history); // 避免重复发送通知 const notificationId = `download-${finalFilePath}`; if (!sentNotifications.has(notificationId)) { sentNotifications.set(notificationId, true); // 发送桌面通知 try { const artistNames = (songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('、') || '未知艺术家'; const notification = new Notification({ title: '下载完成', body: `${songInfo?.name || filename} - ${artistNames}`, silent: false }); notification.on('click', () => { shell.showItemInFolder(finalFilePath); }); notification.show(); // 60秒后清理通知记录,释放内存 setTimeout(() => { sentNotifications.delete(notificationId); }, 60000); } catch (notifyError) { console.error('发送通知失败:', notifyError); } } // 发送下载完成事件,确保只发送一次 event.reply('music-download-complete', { success: true, path: finalFilePath, filename, size: totalSize, songInfo: newSongInfo }); } catch (error) { console.error('Error saving download info:', error); throw new Error('保存下载信息失败'); } } catch (error: any) { console.error('Download error:', error); // 清理未完成的下载 if (writer) { writer.end(); } // 清理临时文件 if (tempFilePath && fs.existsSync(tempFilePath)) { try { fs.unlinkSync(tempFilePath); } catch (e) { console.error('Failed to delete temporary file:', e); } } // 清理未完成的最终文件 if (finalFilePath && fs.existsSync(finalFilePath)) { try { fs.unlinkSync(finalFilePath); } catch (e) { console.error('Failed to delete incomplete download:', e); } } event.reply('music-download-complete', { success: false, error: error.message || '下载失败', filename }); } } // 辅助函数 - 解析歌词文本成时间戳和内容的映射 function parseLyrics(lyricsText: string): Map { const lyricMap = new Map(); const lines = lyricsText.split('\n'); for (const line of lines) { // 匹配时间标签,形如 [00:00.000] const timeTagMatches = line.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/g); if (!timeTagMatches) continue; // 提取歌词内容(去除时间标签) const content = line.replace(/\[\d{2}:\d{2}(\.\d{1,3})?\]/g, '').trim(); if (!content) continue; // 将每个时间标签与歌词内容关联 for (const timeTag of timeTagMatches) { lyricMap.set(timeTag, content); } } return lyricMap; } // 辅助函数 - 合并原文歌词和翻译歌词 function mergeLyrics( originalLyrics: Map, translatedLyrics: Map ): string { const mergedLines: string[] = []; // 对每个时间戳,组合原始歌词和翻译 for (const [timeTag, originalContent] of originalLyrics.entries()) { const translatedContent = translatedLyrics.get(timeTag); // 添加原始歌词行 mergedLines.push(`${timeTag}${originalContent}`); // 如果有翻译,添加翻译行(时间戳相同,这样可以和原歌词同步显示) if (translatedContent) { mergedLines.push(`${timeTag}${translatedContent}`); } } // 按时间顺序排序 mergedLines.sort((a, b) => { const timeA = a.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/)?.[0] || ''; const timeB = b.match(/\[\d{2}:\d{2}(\.\d{1,3})?\]/)?.[0] || ''; return timeA.localeCompare(timeB); }); return mergedLines.join('\n'); } ================================================ FILE: src/main/modules/fonts.ts ================================================ import { ipcMain } from 'electron'; import { getFonts } from 'font-list'; /** * 清理字体名称 * @param fontName 原始字体名称 * @returns 清理后的字体名称 */ function cleanFontName(fontName: string): string { return fontName .trim() .replace(/^["']|["']$/g, '') // 移除首尾的引号 .replace(/\s+/g, ' '); // 将多个空格替换为单个空格 } /** * 获取系统字体列表 */ async function getSystemFonts(): Promise { try { // 使用 font-list 获取系统字体 const fonts = await getFonts(); // 清理字体名称并去重 const cleanedFonts = [...new Set(fonts.map(cleanFontName))]; // 添加系统默认字体并排序 return ['system-ui', ...cleanedFonts].sort(); } catch (error) { console.error('获取系统字体失败:', error); // 如果获取失败,至少返回系统默认字体 return ['system-ui']; } } /** * 初始化字体管理模块 */ export function initializeFonts() { // 添加获取系统字体的 IPC 处理 ipcMain.handle('get-system-fonts', async () => { return await getSystemFonts(); }); } ================================================ FILE: src/main/modules/loginWindow.ts ================================================ import { BrowserWindow, ipcMain, session } from 'electron'; import { join } from 'path'; import i18n from '../../i18n/main'; let loginWindow: BrowserWindow | null = null; const loginUrl = 'https://music.163.com/#/login/'; const loginTitle = i18n.global.t('login.qrTitle'); /** * 打开登录窗口获取Cookie */ const openLoginWindow = async (mainWin: BrowserWindow) => { let loginTimer: NodeJS.Timeout; // 如果登录窗口已存在,则聚焦并返回 if (loginWindow && !loginWindow.isDestroyed()) { loginWindow.focus(); return; } const loginSession = session.fromPartition('persist:login'); // 清除 Cookie await loginSession.clearStorageData({ storages: ['cookies', 'localstorage'] }); loginWindow = new BrowserWindow({ parent: mainWin, title: loginTitle, width: 1280, height: 800, center: true, autoHideMenuBar: true, webPreferences: { session: loginSession, sandbox: false, webSecurity: false, preload: join(__dirname, '../../preload/index.js') } }); // 打开网易云登录页面 loginWindow.loadURL(loginUrl); // 阻止新窗口创建 loginWindow.webContents.setWindowOpenHandler(() => { return { action: 'deny' }; }); // 检查是否登录 const checkLogin = async () => { try { if (!loginWindow || loginWindow.isDestroyed()) { if (loginTimer) clearInterval(loginTimer); return; } const MUSIC_U = await loginSession.cookies.get({ name: 'MUSIC_U' }); if (MUSIC_U && MUSIC_U?.length > 0) { if (loginTimer) clearInterval(loginTimer); const value = `MUSIC_U=${MUSIC_U[0].value};`; mainWin?.webContents.send('send-cookies', value); // 关闭登录窗口 loginWindow.destroy(); loginWindow = null; } } catch (error) { console.error('检查登录状态失败:', error); } }; // 循环检查登录状态 loginWindow.webContents.once('did-finish-load', () => { loginWindow?.show(); loginTimer = setInterval(checkLogin, 500); loginWindow?.on('closed', () => { if (loginTimer) clearInterval(loginTimer); loginWindow = null; }); }); }; /** * 初始化登录窗口相关的IPC监听 */ export function initializeLoginWindow() { ipcMain.on('open-login', (event) => { const mainWin = BrowserWindow.fromWebContents(event.sender); if (mainWin) { openLoginWindow(mainWin); } }); } export default openLoginWindow; ================================================ FILE: src/main/modules/lxMusicHttp.ts ================================================ /** * 落雪音乐 HTTP 请求处理(主进程) * 绕过渲染进程的 CORS 限制 */ import { ipcMain } from 'electron'; import fetch, { type RequestInit } from 'node-fetch'; interface LxHttpRequest { url: string; options: { method?: string; headers?: Record; body?: string; form?: Record; formData?: Record; timeout?: number; }; requestId: string; } interface LxHttpResponse { statusCode: number; headers: Record; body: any; } // 取消控制器映射 const abortControllers = new Map(); /** * 初始化 HTTP 请求处理 */ export const initLxMusicHttp = () => { // 处理 HTTP 请求 ipcMain.handle( 'lx-music-http-request', async (_, request: LxHttpRequest): Promise => { const { url, options, requestId } = request; const controller = new AbortController(); // 保存取消控制器 abortControllers.set(requestId, controller); try { console.log(`[LxMusicHttp] 请求: ${options.method || 'GET'} ${url}`); const fetchOptions: RequestInit = { method: options.method || 'GET', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', ...(options.headers || {}) }, signal: controller.signal }; // 处理请求体 if (options.body) { fetchOptions.body = options.body; } else if (options.form) { const formData = new URLSearchParams(options.form); fetchOptions.body = formData.toString(); fetchOptions.headers = { ...fetchOptions.headers, 'Content-Type': 'application/x-www-form-urlencoded' }; } else if (options.formData) { // node-fetch 的 FormData 需要特殊处理 const FormData = (await import('form-data')).default; const formData = new FormData(); for (const [key, value] of Object.entries(options.formData)) { formData.append(key, value); } fetchOptions.body = formData as any; // FormData 会自动设置 Content-Type } // 设置超时 const timeout = options.timeout || 30000; const timeoutId = setTimeout(() => { console.warn(`[LxMusicHttp] 请求超时: ${url}`); controller.abort(); }, timeout); const response = await fetch(url, fetchOptions); clearTimeout(timeoutId); console.log(`[LxMusicHttp] 响应: ${response.status} ${url}`); // 读取响应体 const rawBody = await response.text(); // 尝试解析 JSON let parsedBody: any = rawBody; const contentType = response.headers.get('content-type') || ''; if ( contentType.includes('application/json') || rawBody.startsWith('{') || rawBody.startsWith('[') ) { try { parsedBody = JSON.parse(rawBody); } catch { // 解析失败则使用原始字符串 } } // 转换 headers 为普通对象 const headers: Record = {}; response.headers.forEach((value, key) => { headers[key] = value; }); const result: LxHttpResponse = { statusCode: response.status, headers, body: parsedBody }; return result; } catch (error: any) { console.error(`[LxMusicHttp] 请求失败: ${url}`, error.message); throw error; } finally { // 清理取消控制器 abortControllers.delete(requestId); } } ); // 处理请求取消 ipcMain.handle('lx-music-http-cancel', (_, requestId: string) => { const controller = abortControllers.get(requestId); if (controller) { console.log(`[LxMusicHttp] 取消请求: ${requestId}`); controller.abort(); abortControllers.delete(requestId); } }); console.log('[LxMusicHttp] HTTP 请求处理已初始化'); }; /** * 清理所有正在进行的请求 */ export const cleanupLxMusicHttp = () => { for (const [requestId, controller] of abortControllers.entries()) { console.log(`[LxMusicHttp] 清理请求: ${requestId}`); controller.abort(); } abortControllers.clear(); }; ================================================ FILE: src/main/modules/otherApi.ts ================================================ import axios from 'axios'; import { ipcMain } from 'electron'; /** * 初始化其他杂项 API(如搜索建议等) */ export function initializeOtherApi() { // 搜索建议(从酷狗获取) ipcMain.handle('get-search-suggestions', async (_, keyword: string) => { if (!keyword || !keyword.trim()) { return []; } try { console.log(`[Main Process Proxy] Forwarding suggestion request for: ${keyword}`); const response = await axios.get('http://msearchcdn.kugou.com/new/app/i/search.php', { params: { cmd: 302, keyword: keyword }, timeout: 5000 }); return response.data; } catch (error: any) { console.error('[Main Process Proxy] Failed to fetch search suggestions:', error.message); return []; } }); } ================================================ FILE: src/main/modules/remoteControl.ts ================================================ import cors from 'cors'; import { ipcMain } from 'electron'; import express from 'express'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { getStore } from './config'; // 定义远程控制相关接口 export interface RemoteControlConfig { enabled: boolean; port: number; allowedIps: string[]; } // 默认配置 export const defaultRemoteControlConfig: RemoteControlConfig = { enabled: false, port: 31888, allowedIps: [] }; let app: express.Application | null = null; let server: any = null; let mainWindowRef: Electron.BrowserWindow | null = null; let currentSong: any = null; let isPlaying: boolean = false; // 获取本地IP地址 function getLocalIpAddresses(): string[] { const interfaces = os.networkInterfaces(); const addresses: string[] = []; for (const key in interfaces) { const iface = interfaces[key]; if (iface) { for (const alias of iface) { if (alias.family === 'IPv4' && !alias.internal) { addresses.push(alias.address); } } } } return addresses; } // 初始化远程控制服务 export function initializeRemoteControl(mainWindow: Electron.BrowserWindow) { mainWindowRef = mainWindow; const store = getStore() as any; let config = store.get('remoteControl') as RemoteControlConfig; // 如果配置不存在,使用默认配置 if (!config) { config = defaultRemoteControlConfig; store.set('remoteControl', config); } // 监听当前歌曲变化 ipcMain.on('update-current-song', (_, song: any) => { currentSong = song; }); // 监听播放状态变化 ipcMain.on('update-play-state', (_, playing: boolean) => { isPlaying = playing; }); // 监听远程控制配置变化 ipcMain.on('update-remote-control-config', (_, newConfig: RemoteControlConfig) => { if (server) { stopServer(); } store.set('remoteControl', newConfig); if (newConfig.enabled) { startServer(newConfig); } }); // 获取远程控制配置 ipcMain.handle('get-remote-control-config', () => { const config = store.get('remoteControl') as RemoteControlConfig; return config || defaultRemoteControlConfig; }); // 获取本地IP地址 ipcMain.handle('get-local-ip-addresses', () => { return getLocalIpAddresses(); }); // 如果启用了远程控制,启动服务器 if (config.enabled) { startServer(config); } } // 启动远程控制服务器 function startServer(config: RemoteControlConfig) { if (!mainWindowRef) { console.error('主窗口未初始化,无法启动远程控制服务'); return; } app = express(); // 跨域配置 app.use(cors()); app.use(express.json()); // IP 过滤中间件 app.use((req, res, next) => { const clientIp = req.ip || req.socket.remoteAddress || ''; const cleanIp = clientIp.replace(/^::ffff:/, ''); // 移除IPv6前缀 console.log('config', config); if (config.allowedIps.length === 0 || config.allowedIps.includes(cleanIp)) { next(); } else { res.status(403).json({ error: '未授权的IP地址' }); } }); // 路由配置 setupRoutes(app); // 启动服务器 try { server = app.listen(config.port, () => { console.log(`远程控制服务已启动,监听端口: ${config.port}`); }); } catch (error) { console.error('启动远程控制服务失败:', error); } } // 停止远程控制服务器 function stopServer() { if (server) { server.close(); server = null; app = null; console.log('远程控制服务已停止'); } } // 设置路由 function setupRoutes(app: express.Application) { // 获取当前播放状态 app.get('/api/status', (_, res) => { res.json({ isPlaying, currentSong }); }); // 播放/暂停 app.post('/api/toggle-play', (_, res) => { if (!mainWindowRef) { return res.status(500).json({ error: '主窗口未初始化' }); } mainWindowRef.webContents.send('global-shortcut', 'togglePlay'); res.json({ success: true, message: '已发送播放/暂停指令' }); }); // 上一首 app.post('/api/prev', (_, res) => { if (!mainWindowRef) { return res.status(500).json({ error: '主窗口未初始化' }); } mainWindowRef.webContents.send('global-shortcut', 'prevPlay'); res.json({ success: true, message: '已发送上一首指令' }); }); // 下一首 app.post('/api/next', (_, res) => { if (!mainWindowRef) { return res.status(500).json({ error: '主窗口未初始化' }); } mainWindowRef.webContents.send('global-shortcut', 'nextPlay'); res.json({ success: true, message: '已发送下一首指令' }); }); // 音量加 app.post('/api/volume-up', (_, res) => { if (!mainWindowRef) { return res.status(500).json({ error: '主窗口未初始化' }); } mainWindowRef.webContents.send('global-shortcut', 'volumeUp'); res.json({ success: true, message: '已发送音量加指令' }); }); // 音量减 app.post('/api/volume-down', (_, res) => { if (!mainWindowRef) { return res.status(500).json({ error: '主窗口未初始化' }); } mainWindowRef.webContents.send('global-shortcut', 'volumeDown'); res.json({ success: true, message: '已发送音量减指令' }); }); // 收藏/取消收藏 app.post('/api/toggle-favorite', (_, res) => { if (!mainWindowRef) { return res.status(500).json({ error: '主窗口未初始化' }); } mainWindowRef.webContents.send('global-shortcut', 'toggleFavorite'); res.json({ success: true, message: '已发送收藏/取消收藏指令' }); }); // 提供远程控制界面HTML app.get('/', (_, res) => { try { const resourcesPath = process.resourcesPath || ''; const isDev = process.env.NODE_ENV === 'development'; const htmlPath = path.join(process.cwd(), 'resources', 'html', 'remote-control.html'); const finalPath = isDev ? htmlPath : path.join(resourcesPath, 'html', 'remote-control.html'); if (fs.existsSync(finalPath)) { res.sendFile(finalPath); } else { res.status(404).send('远程控制界面文件未找到'); console.error('远程控制界面文件不存在:', finalPath); } } catch (error) { console.error('加载远程控制界面失败:', error); res.status(500).send('加载远程控制界面失败'); } }); } ================================================ FILE: src/main/modules/shortcuts.ts ================================================ import { globalShortcut, ipcMain } from 'electron'; import { getStore } from './config'; // 添加获取平台信息的 IPC 处理程序 ipcMain.on('get-platform', (event) => { event.returnValue = process.platform; }); // 定义快捷键配置接口 export interface ShortcutConfig { key: string; enabled: boolean; scope: 'global' | 'app'; } export interface ShortcutsConfig { [key: string]: ShortcutConfig; } // 定义默认快捷键 export const defaultShortcuts: ShortcutsConfig = { togglePlay: { key: 'CommandOrControl+Alt+P', enabled: true, scope: 'global' }, prevPlay: { key: 'Alt+Left', enabled: true, scope: 'global' }, nextPlay: { key: 'Alt+Right', enabled: true, scope: 'global' }, volumeUp: { key: 'Alt+Up', enabled: true, scope: 'app' }, volumeDown: { key: 'Alt+Down', enabled: true, scope: 'app' }, toggleFavorite: { key: 'CommandOrControl+Alt+L', enabled: true, scope: 'app' }, toggleWindow: { key: 'CommandOrControl+Alt+Shift+M', enabled: true, scope: 'global' } }; let mainWindowRef: Electron.BrowserWindow | null = null; // 注册快捷键 export function registerShortcuts( mainWindow: Electron.BrowserWindow, shortcutsConfig?: ShortcutsConfig ) { mainWindowRef = mainWindow; const store = getStore(); const shortcuts = shortcutsConfig || (store.get('shortcuts') as ShortcutsConfig) || defaultShortcuts; // 注销所有已注册的快捷键 globalShortcut.unregisterAll(); // 对旧格式数据进行兼容处理 if (shortcuts && typeof shortcuts.togglePlay === 'string') { // 将 shortcuts 强制转换为 unknown,再转为 Record const oldShortcuts = { ...shortcuts } as unknown as Record; const newShortcuts: ShortcutsConfig = {}; Object.entries(oldShortcuts).forEach(([key, value]) => { newShortcuts[key] = { key: value, enabled: true, scope: ['volumeUp', 'volumeDown', 'toggleFavorite'].includes(key) ? 'app' : 'global' }; }); store.set('shortcuts', newShortcuts); registerShortcuts(mainWindow, newShortcuts); return; } // 注册全局快捷键 Object.entries(shortcuts).forEach(([action, config]) => { const { key, enabled, scope } = config as ShortcutConfig; // 只注册启用且作用域为全局的快捷键 if (!enabled || scope !== 'global') return; try { switch (action) { case 'toggleWindow': globalShortcut.register(key, () => { if (mainWindow.isVisible()) { mainWindow.hide(); } else { mainWindow.show(); } }); break; default: globalShortcut.register(key, () => { mainWindow.webContents.send('global-shortcut', action); }); break; } } catch (error) { console.error(`注册快捷键 ${key} 失败:`, error); } }); // 通知渲染进程更新应用内快捷键 mainWindow.webContents.send('update-app-shortcuts', shortcuts); } // 初始化快捷键 export function initializeShortcuts(mainWindow: Electron.BrowserWindow) { mainWindowRef = mainWindow; registerShortcuts(mainWindow); // 监听禁用快捷键事件 ipcMain.on('disable-shortcuts', () => { globalShortcut.unregisterAll(); }); // 监听启用快捷键事件 ipcMain.on('enable-shortcuts', () => { if (mainWindowRef) { registerShortcuts(mainWindowRef); } }); // 监听快捷键更新事件 ipcMain.on('update-shortcuts', (_, shortcutsConfig: ShortcutsConfig) => { if (mainWindowRef) { registerShortcuts(mainWindowRef, shortcutsConfig); } }); } ================================================ FILE: src/main/modules/tray.ts ================================================ import { app, BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions, nativeImage, Tray } from 'electron'; import { join } from 'path'; import i18n from '../../i18n/main'; import { getLanguageOptions } from '../../i18n/utils'; import { getStore } from './config'; // 歌曲信息接口定义 interface SongInfo { name: string; song: { artists: Array<{ name: string; [key: string]: any }>; [key: string]: any; }; [key: string]: any; } let tray: Tray | null = null; // 为macOS状态栏添加控制图标 let playPauseTray: Tray | null = null; let prevTray: Tray | null = null; let nextTray: Tray | null = null; let songTitleTray: Tray | null = null; let isPlaying = false; let currentSong: SongInfo | null = null; // 使用自动导入的语言选项 const LANGUAGES = getLanguageOptions(); // 更新播放状态 export function updatePlayState(playing: boolean) { isPlaying = playing; if (tray) { updateTrayMenu(BrowserWindow.getAllWindows()[0]); } // 更新播放/暂停图标 updateStatusBarTray(); } // 获取艺术家名称字符串 function getArtistString(song: SongInfo | null): string { if (!song || !song.song || !song.song.artists) return ''; return song.song.artists.map((item) => item.name).join(' / '); } // 获取歌曲完整标题(歌曲名 - 艺术家) function getSongTitle(song: SongInfo | null): string { if (!song) return '未播放'; const artistStr = getArtistString(song); return artistStr ? `${song.name} - ${artistStr}` : song.name; } // 截断歌曲标题,防止菜单中显示过长 function getTruncatedSongTitle(song: SongInfo | null, maxLength: number = 14): string { const fullTitle = getSongTitle(song); if (fullTitle.length <= maxLength) return fullTitle; return fullTitle.slice(0, maxLength) + '...'; } // 更新当前播放的音乐信息 export function updateCurrentSong(song: SongInfo | null) { currentSong = song; if (tray) { updateTrayMenu(BrowserWindow.getAllWindows()[0]); } // 更新状态栏歌曲信息 updateStatusBarTray(); } // 确保 macOS 状态栏图标能正确显示 function getProperIconSize() { // macOS 状态栏通常高度为22像素 const height = 18; const width = 18; return { width, height }; } // 更新macOS状态栏图标 function updateStatusBarTray() { if (process.platform !== 'darwin') return; const iconSize = getProperIconSize(); // 更新歌曲标题显示 if (songTitleTray) { if (currentSong) { // 限制歌曲名显示长度,添加作者名 const songName = currentSong.name.slice(0, 10); let title = songName; const artistStr = getArtistString(currentSong); // 如果有艺术家名称,添加到标题中 if (artistStr) { title = `${songName} - ${artistStr.slice(0, 6)}${artistStr.length > 6 ? '..' : ''}`; } // 设置标题和提示 songTitleTray.setTitle(title, { fontType: 'monospacedDigit' // 使用等宽字体以确保更好的可读性 }); // 完整信息放在tooltip中 const fullTitle = getSongTitle(currentSong); songTitleTray.setToolTip(fullTitle); console.log('更新状态栏歌曲显示:', title, '完整信息:', fullTitle); } else { songTitleTray.setTitle('未播放', { fontType: 'monospacedDigit' }); songTitleTray.setToolTip('未播放'); console.log('更新状态栏歌曲显示: 未播放'); } } // 更新播放/暂停图标 if (playPauseTray) { // 使用PNG图标替代文本 const iconPath = join( app.getAppPath(), 'resources/icons', isPlaying ? 'pause.png' : 'play.png' ); const icon = nativeImage.createFromPath(iconPath).resize(iconSize); icon.setTemplateImage(true); // 设置为模板图片,适合macOS深色/浅色模式 playPauseTray.setImage(icon); playPauseTray.setToolTip( isPlaying ? i18n.global.t('common.tray.pause') : i18n.global.t('common.tray.play') ); } } // 导出更新菜单的函数 export function updateTrayMenu(mainWindow: BrowserWindow) { if (!tray) return; // 如果是macOS,设置TouchBar if (process.platform === 'darwin') { // macOS 上使用直接的控制按钮 const menu = new Menu(); // 当前播放的音乐信息 if (currentSong) { menu.append( new MenuItem({ label: getTruncatedSongTitle(currentSong), enabled: false, type: 'normal' }) ); menu.append(new MenuItem({ type: 'separator' })); } // 上一首、播放/暂停、下一首的菜单项 // 在macOS上临时使用文本菜单项替代图标,确保基本功能正常 menu.append( new MenuItem({ label: i18n.global.t('common.tray.prev'), type: 'normal', click: () => { mainWindow.webContents.send('global-shortcut', 'prevPlay'); } }) ); menu.append( new MenuItem({ label: i18n.global.t(isPlaying ? 'common.tray.pause' : 'common.tray.play'), type: 'normal', click: () => { mainWindow.webContents.send('global-shortcut', 'togglePlay'); } }) ); // 收藏 menu.append( new MenuItem({ label: i18n.global.t('common.tray.favorite'), type: 'normal', click: () => { console.log('[Tray] 发送收藏命令 - macOS菜单'); mainWindow.webContents.send('global-shortcut', 'toggleFavorite'); } }) ); menu.append( new MenuItem({ label: i18n.global.t('common.tray.next'), type: 'normal', click: () => { mainWindow.webContents.send('global-shortcut', 'nextPlay'); } }) ); // 分隔符 menu.append(new MenuItem({ type: 'separator' })); // 显示主窗口 menu.append( new MenuItem({ label: i18n.global.t('common.tray.show'), type: 'normal', click: () => { mainWindow.show(); } }) ); // 语言切换子菜单 const languageSubmenu = Menu.buildFromTemplate( LANGUAGES.map(({ label, value }) => ({ label, type: 'radio', checked: i18n.global.locale === value, click: () => { i18n.global.locale = value; updateTrayMenu(mainWindow); mainWindow.webContents.send('language-changed', value); } })) ); menu.append( new MenuItem({ label: i18n.global.t('common.language'), type: 'submenu', submenu: languageSubmenu }) ); // 退出按钮 menu.append( new MenuItem({ label: i18n.global.t('common.tray.quit'), type: 'normal', click: () => { app.quit(); } }) ); tray.setContextMenu(menu); } else { // Windows 和 Linux 使用原来的菜单样式 const menuTemplate: MenuItemConstructorOptions[] = [ // 当前播放的音乐信息 ...((currentSong ? [ { label: getTruncatedSongTitle(currentSong), enabled: false, type: 'normal' }, { type: 'separator' } ] : []) as MenuItemConstructorOptions[]), { label: i18n.global.t('common.tray.show'), type: 'normal', click: () => { mainWindow.show(); } }, { label: i18n.global.t('common.tray.favorite'), type: 'normal', click: () => { console.log('[Tray] 发送收藏命令 - Windows/Linux菜单'); mainWindow.webContents.send('global-shortcut', 'toggleFavorite'); } }, { type: 'separator' }, { label: i18n.global.t('common.tray.prev'), type: 'normal', click: () => { mainWindow.webContents.send('global-shortcut', 'prevPlay'); } }, { label: i18n.global.t(isPlaying ? 'common.tray.pause' : 'common.tray.play'), type: 'normal', click: () => { mainWindow.webContents.send('global-shortcut', 'togglePlay'); } }, { label: i18n.global.t('common.tray.next'), type: 'normal', click: () => { mainWindow.webContents.send('global-shortcut', 'nextPlay'); } }, { type: 'separator' }, { label: i18n.global.t('common.language'), type: 'submenu', submenu: LANGUAGES.map(({ label, value }) => ({ label, type: 'radio', checked: i18n.global.locale === value, click: () => { i18n.global.locale = value; updateTrayMenu(mainWindow); mainWindow.webContents.send('language-changed', value); } })) }, { type: 'separator' }, { label: i18n.global.t('common.tray.quit'), type: 'normal', click: () => { app.quit(); } } ]; const contextMenu = Menu.buildFromTemplate(menuTemplate); tray.setContextMenu(contextMenu); } } // 初始化状态栏Tray function initializeStatusBarTray(mainWindow: BrowserWindow) { const store = getStore(); if (process.platform !== 'darwin' || !store.get('set.showTopAction')) return; const iconSize = getProperIconSize(); // 创建下一首按钮(调整顺序,先创建下一首按钮) const nextIcon = nativeImage .createFromPath(join(app.getAppPath(), 'resources/icons', 'next.png')) .resize(iconSize); nextIcon.setTemplateImage(true); // 设置为模板图片,适合macOS深色/浅色模式 nextTray = new Tray(nextIcon); nextTray.setToolTip(i18n.global.t('common.tray.next')); nextTray.on('click', () => { mainWindow.webContents.send('global-shortcut', 'nextPlay'); }); // 创建播放/暂停按钮 const playPauseIcon = nativeImage .createFromPath(join(app.getAppPath(), 'resources/icons', isPlaying ? 'pause.png' : 'play.png')) .resize(iconSize); playPauseIcon.setTemplateImage(true); // 设置为模板图片,适合macOS深色/浅色模式 playPauseTray = new Tray(playPauseIcon); playPauseTray.setToolTip( isPlaying ? i18n.global.t('common.tray.pause') : i18n.global.t('common.tray.play') ); playPauseTray.on('click', () => { mainWindow.webContents.send('global-shortcut', 'togglePlay'); }); // 创建上一首按钮(调整顺序,最后创建上一首按钮) const prevIcon = nativeImage .createFromPath(join(app.getAppPath(), 'resources/icons', 'prev.png')) .resize(iconSize); prevIcon.setTemplateImage(true); // 设置为模板图片,适合macOS深色/浅色模式 prevTray = new Tray(prevIcon); prevTray.setToolTip(i18n.global.t('common.tray.prev')); prevTray.on('click', () => { mainWindow.webContents.send('global-shortcut', 'prevPlay'); }); // 创建歌曲信息显示 - 需要使用特殊处理 const titleIcon = nativeImage .createFromPath(join(app.getAppPath(), 'resources/icons', 'note.png')) .resize({ width: 16, height: 16 }); titleIcon.setTemplateImage(true); songTitleTray = new Tray(titleIcon); // 初始化显示文本 const initialText = getSongTitle(currentSong); // 在macOS上,特别设置title来显示文本,确保它能正确显示 songTitleTray.setTitle(initialText, { fontType: 'monospacedDigit' // 使用等宽字体以确保更好的可读性 }); songTitleTray.setToolTip(initialText); songTitleTray.on('click', () => { mainWindow.show(); }); // 强制更新一次所有图标 updateStatusBarTray(); // 打印调试信息 console.log('状态栏初始化完成,歌曲显示标题:', initialText); } /** * 初始化系统托盘 */ export function initializeTray(iconPath: string, mainWindow: BrowserWindow) { // 根据平台选择合适的图标 const iconSize = process.platform === 'darwin' ? 18 : 16; const iconFile = process.platform === 'darwin' ? 'icon_16x16.png' : 'icon_16x16.png'; const trayIcon = nativeImage .createFromPath(join(iconPath, iconFile)) .resize({ width: iconSize, height: iconSize }); tray = new Tray(trayIcon); // 设置托盘图标的提示文字 tray.setToolTip('Alger Music Player'); // 初始化菜单 updateTrayMenu(mainWindow); // 初始化状态栏控制按钮 (macOS) initializeStatusBarTray(mainWindow); // 在 macOS 上,点击图标时显示菜单 if (process.platform === 'darwin') { tray.on('click', () => { if (tray) { tray.popUpContextMenu(); } }); } else { // 在其他平台上,点击图标时切换窗口显示状态 tray.on('click', () => { if (mainWindow.isVisible()) { mainWindow.hide(); } else { mainWindow.show(); } }); } return tray; } ================================================ FILE: src/main/modules/update.ts ================================================ import axios from 'axios'; import { spawn } from 'child_process'; import { app, BrowserWindow, ipcMain } from 'electron'; import * as fs from 'fs'; import * as path from 'path'; export function setupUpdateHandlers(_mainWindow: BrowserWindow) { ipcMain.on('start-download', async (event, url: string) => { try { const response = await axios({ url, method: 'GET', responseType: 'stream', onDownloadProgress: (progressEvent: { loaded: number; total?: number }) => { if (!progressEvent.total) return; const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100); const downloaded = (progressEvent.loaded / 1024 / 1024).toFixed(2); const total = (progressEvent.total / 1024 / 1024).toFixed(2); event.sender.send('download-progress', percent, `已下载 ${downloaded}MB / ${total}MB`); } }); const fileName = url.split('/').pop() || 'update.exe'; const downloadPath = path.join(app.getPath('downloads'), fileName); // 创建写入流 const writer = fs.createWriteStream(downloadPath); // 将响应流写入文件 response.data.pipe(writer); // 处理写入完成 writer.on('finish', () => { event.sender.send('download-complete', true, downloadPath); }); // 处理写入错误 writer.on('error', (error) => { console.error('Write file error:', error); event.sender.send('download-complete', false, ''); }); } catch (error) { console.error('Download failed:', error); event.sender.send('download-complete', false, ''); } }); ipcMain.on('install-update', (_event, filePath: string) => { if (!fs.existsSync(filePath)) { console.error('Installation file not found:', filePath); return; } const { platform } = process; // 先启动安装程序,再退出应用 try { if (platform === 'win32') { // 使用spawn替代exec,并使用detached选项确保子进程独立运行 const child = spawn(filePath, [], { detached: true, stdio: 'ignore' }); child.unref(); } else if (platform === 'darwin') { // 挂载 DMG 文件 const child = spawn('open', [filePath], { detached: true, stdio: 'ignore' }); child.unref(); } else if (platform === 'linux') { const ext = path.extname(filePath); if (ext === '.AppImage') { // 先添加执行权限 fs.chmodSync(filePath, '755'); const child = spawn(filePath, [], { detached: true, stdio: 'ignore' }); child.unref(); } else if (ext === '.deb') { const child = spawn('pkexec', ['dpkg', '-i', filePath], { detached: true, stdio: 'ignore' }); child.unref(); } } // 给安装程序一点时间启动 setTimeout(() => { app.quit(); }, 500); } catch (error) { console.error('启动安装程序失败:', error); // 尽管出错,仍然尝试退出应用 app.quit(); } }); } ================================================ FILE: src/main/modules/window-size.ts ================================================ import { app, BrowserWindow, ipcMain, screen } from 'electron'; import Store from 'electron-store'; const store = new Store(); // 默认窗口尺寸 export const DEFAULT_MAIN_WIDTH = 1200; export const DEFAULT_MAIN_HEIGHT = 780; export const DEFAULT_MINI_WIDTH = 340; export const DEFAULT_MINI_HEIGHT = 64; export const DEFAULT_MINI_EXPANDED_HEIGHT = 400; // 用于存储窗口状态的键名 export const WINDOW_STATE_KEY = 'windowState'; // 最小窗口尺寸 let MIN_WIDTH = Math.round(DEFAULT_MAIN_WIDTH * 0.5); let MIN_HEIGHT = Math.round(DEFAULT_MAIN_HEIGHT * 0.5); // 标记IPC处理程序是否已注册 let ipcHandlersRegistered = false; /** * 窗口状态类型定义 */ export interface WindowState { width: number; height: number; x?: number; y?: number; isMaximized: boolean; } /** * 窗口大小管理器 * 负责保存、恢复和维护窗口大小状态 */ class WindowSizeManager { private store: Store; private mainWindow: BrowserWindow | null = null; private savedState: WindowState | null = null; private isInitialized: boolean = false; constructor() { this.store = store; // 初始化时不做与screen相关的操作,等app ready后再初始化 } /** * 初始化窗口大小管理器 * 必须在app ready后调用 */ initialize(): void { if (!app.isReady()) { console.warn('WindowSizeManager.initialize() 必须在 app ready 之后调用!'); return; } if (this.isInitialized) { return; } this.initMinimumWindowSize(); this.setupIPCHandlers(); this.isInitialized = true; console.log('窗口大小管理器初始化完成'); } /** * 设置主窗口引用 */ setMainWindow(win: BrowserWindow): void { if (!this.isInitialized) { this.initialize(); } this.mainWindow = win; // 读取保存的状态 this.savedState = this.getWindowState(); // 监听重要事件 this.setupEventListeners(win); // 立即保存初始状态 this.saveWindowState(win); } /** * 初始化最小窗口尺寸 */ private initMinimumWindowSize(): void { if (!app.isReady()) { console.warn('不能在 app ready 之前访问 screen 模块'); return; } try { const { width: workAreaWidth, height: workAreaHeight } = screen.getPrimaryDisplay().workArea; // 根据工作区大小设置合理的最小尺寸 MIN_WIDTH = Math.min(Math.round(DEFAULT_MAIN_WIDTH * 0.5), Math.round(workAreaWidth * 0.3)); MIN_HEIGHT = Math.min( Math.round(DEFAULT_MAIN_HEIGHT * 0.5), Math.round(workAreaHeight * 0.3) ); console.log(`设置最小窗口尺寸: ${MIN_WIDTH}x${MIN_HEIGHT}`); } catch (error) { console.error('初始化最小窗口尺寸失败:', error); // 使用默认值 MIN_WIDTH = Math.round(DEFAULT_MAIN_WIDTH * 0.5); MIN_HEIGHT = Math.round(DEFAULT_MAIN_HEIGHT * 0.5); } } /** * 设置事件监听器 */ private setupEventListeners(win: BrowserWindow): void { // 监听窗口大小调整事件 win.on('resize', () => { if (!win.isDestroyed() && !win.isMinimized()) { this.saveWindowState(win); } }); // 监听窗口移动事件 win.on('move', () => { if (!win.isDestroyed() && !win.isMinimized()) { this.saveWindowState(win); } }); // 监听窗口最大化事件 win.on('maximize', () => { if (!win.isDestroyed()) { this.saveWindowState(win); } }); // 监听窗口从最大化恢复事件 win.on('unmaximize', () => { if (!win.isDestroyed()) { this.saveWindowState(win); } }); // 监听窗口关闭事件,确保保存最终状态 win.on('close', () => { if (!win.isDestroyed()) { this.saveWindowState(win); } }); // 在页面加载完成后确保窗口大小正确 win.webContents.on('did-finish-load', () => { this.enforceCorrectSize(win); }); // 在窗口准备好显示时确保尺寸正确 win.on('ready-to-show', () => { this.enforceCorrectSize(win); }); } /** * 强制应用正确的窗口大小 */ private enforceCorrectSize(win: BrowserWindow): void { if (!this.savedState || win.isMaximized() || win.isMinimized() || win.isDestroyed()) { return; } const [currentWidth, currentHeight] = win.getSize(); if ( Math.abs(currentWidth - this.savedState.width) > 2 || Math.abs(currentHeight - this.savedState.height) > 2 ) { console.log( `强制调整窗口大小: 当前=${currentWidth}x${currentHeight}, 目标=${this.savedState.width}x${this.savedState.height}` ); // 临时禁用minimum size限制 const [minWidth, minHeight] = win.getMinimumSize(); win.setMinimumSize(1, 1); // 强制设置正确大小 win.setSize(this.savedState.width, this.savedState.height, false); // 恢复原始minimum size win.setMinimumSize(minWidth, minHeight); // 验证尺寸设置是否成功 const [newWidth, newHeight] = win.getSize(); console.log(`调整后窗口大小: ${newWidth}x${newHeight}`); // 如果调整后的大小仍然与目标不一致,尝试再次调整 if ( Math.abs(newWidth - this.savedState.width) > 1 || Math.abs(newHeight - this.savedState.height) > 1 ) { console.log(`窗口大小调整后仍不一致,将再次尝试调整`); setTimeout(() => { if (!win.isDestroyed() && !win.isMaximized() && !win.isMinimized()) { win.setSize(this.savedState!.width, this.savedState!.height, false); } }, 50); } // // 开始尺寸强制执行 // this.startSizeEnforcement(win); } } /** * 开启尺寸强制执行定时器 */ // private startSizeEnforcement(win: BrowserWindow): void { // // 清除之前的定时器 // if (this.enforceTimer) { // clearInterval(this.enforceTimer); // this.enforceTimer = null; // } // this.enforceCount = 0; // // 创建新的定时器,每50ms检查一次窗口大小 // this.enforceTimer = setInterval(() => { // if (this.enforceCount >= this.MAX_ENFORCE_COUNT || // !this.savedState || // win.isDestroyed() || // win.isMaximized() || // win.isMinimized()) { // // 达到最大检查次数或不需要检查,清除定时器 // if (this.enforceTimer) { // clearInterval(this.enforceTimer); // this.enforceTimer = null; // } // return; // } // const [currentWidth, currentHeight] = win.getSize(); // if (Math.abs(currentWidth - this.savedState.width) > 2 || // Math.abs(currentHeight - this.savedState.height) > 2) { // console.log(`[定时检查] 强制调整窗口大小: 当前=${currentWidth}x${currentHeight}, 目标=${this.savedState.width}x${this.savedState.height}`); // // 临时禁用minimum size限制 // const [minWidth, minHeight] = win.getMinimumSize(); // win.setMinimumSize(1, 1); // // 强制设置正确大小 // win.setSize(this.savedState.width, this.savedState.height, false); // // 恢复原始minimum size // win.setMinimumSize(minWidth, minHeight); // // 验证尺寸设置是否成功 // const [newWidth, newHeight] = win.getSize(); // if (Math.abs(newWidth - this.savedState.width) <= 1 && // Math.abs(newHeight - this.savedState.height) <= 1) { // console.log(`窗口大小已成功调整为目标尺寸: ${newWidth}x${newHeight}`); // } // } // this.enforceCount++; // }, 50); // } /** * 获取窗口创建选项 */ getWindowOptions(): Electron.BrowserWindowConstructorOptions { // 确保初始化 if (!this.isInitialized && app.isReady()) { this.initialize(); } // 读取保存的状态 const savedState = this.getWindowState(); // 准备选项 const options: Electron.BrowserWindowConstructorOptions = { width: savedState?.width || DEFAULT_MAIN_WIDTH, height: savedState?.height || DEFAULT_MAIN_HEIGHT, minWidth: MIN_WIDTH, minHeight: MIN_HEIGHT, show: false, frame: false, webPreferences: { nodeIntegration: false, contextIsolation: true } }; // 如果有保存的位置,且位置有效,则使用该位置 if (savedState?.x !== undefined && savedState?.y !== undefined && app.isReady()) { if (this.isPositionVisible(savedState.x, savedState.y)) { options.x = savedState.x; options.y = savedState.y; } } console.log( `窗口创建选项: 大小=${options.width}x${options.height}, 位置=(${options.x}, ${options.y})` ); return options; } /** * 应用窗口初始状态 * 在窗口创建后调用 */ applyInitialState(win: BrowserWindow): void { const savedState = this.getWindowState(); if (!savedState) { win.center(); return; } // 如果需要最大化,直接最大化 if (savedState.isMaximized) { console.log('应用已保存的最大化状态'); win.maximize(); } // 如果位置无效,则居中显示 else if ( !app.isReady() || savedState.x === undefined || savedState.y === undefined || !this.isPositionVisible(savedState.x, savedState.y) ) { console.log('保存的位置无效,窗口居中显示'); win.center(); } } /** * 保存窗口状态 */ saveWindowState(win: BrowserWindow): WindowState { // 如果窗口已销毁,则返回之前的状态或默认状态 console.log('win.isDestroyed()', win.isDestroyed()); if (win.isDestroyed()) { return ( this.savedState || { width: DEFAULT_MAIN_WIDTH, height: DEFAULT_MAIN_HEIGHT, isMaximized: false } ); } // 检查是否是mini模式窗口(根据窗口大小判断) const [currentWidth, currentHeight] = win.getSize(); const isMiniMode = currentWidth === DEFAULT_MINI_WIDTH && currentHeight === DEFAULT_MINI_HEIGHT; const isMaximized = win.isMaximized(); let state: WindowState; if (isMaximized) { // 如果窗口处于最大化状态,保存最大化标志 // 由于 Electron 的限制,最大化状态下 getBounds() 可能不准确 // 所以我们尽量保留之前保存的非最大化时的大小 const currentBounds = win.getBounds(); const previousSize = this.savedState && !this.savedState.isMaximized ? { width: this.savedState.width, height: this.savedState.height } : { width: currentBounds.width, height: currentBounds.height }; state = { width: previousSize.width, height: previousSize.height, x: currentBounds.x, y: currentBounds.y, isMaximized: true }; console.log('state IsMaximized', state); } else if (win.isMinimized()) { // 最小化状态下不保存窗口大小,因为可能不准确 console.log('state IsMinimized', this.savedState); return ( this.savedState || { width: DEFAULT_MAIN_WIDTH, height: DEFAULT_MAIN_HEIGHT, isMaximized: false } ); } else { // 正常状态下保存当前大小和位置 const [width, height] = win.getSize(); const [x, y] = win.getPosition(); state = { width, height, x, y, isMaximized: false }; console.log('state IsNormal', state); } // 如果是mini模式,不保存到持久化存储,只返回状态用于内存中的恢复 if (isMiniMode) { console.log('检测到mini模式窗口,不保存到持久化存储'); return state; } // 保存状态到存储 this.store.set(WINDOW_STATE_KEY, state); console.log(`已保存窗口状态: ${JSON.stringify(state)}`); // 更新内部状态 this.savedState = state; console.log('state', state); return state; } /** * 获取保存的窗口状态 */ getWindowState(): WindowState | null { const state = this.store.get(WINDOW_STATE_KEY) as WindowState | undefined; if (!state) { console.log('未找到保存的窗口状态,将使用默认值'); return null; } // 验证尺寸,确保不小于最小值 const validatedState: WindowState = { width: Math.max(MIN_WIDTH, state.width || DEFAULT_MAIN_WIDTH), height: Math.max(MIN_HEIGHT, state.height || DEFAULT_MAIN_HEIGHT), x: state.x, y: state.y, isMaximized: !!state.isMaximized }; console.log(`读取保存的窗口状态: ${JSON.stringify(validatedState)}`); return validatedState; } /** * 检查位置是否在可见屏幕范围内 */ isPositionVisible(x: number, y: number): boolean { if (!app.isReady()) { return false; } try { const displays = screen.getAllDisplays(); for (const display of displays) { const { x: screenX, y: screenY, width, height } = display.workArea; if (x >= screenX && x < screenX + width && y >= screenY && y < screenY + height) { return true; } } } catch (error) { console.error('检查位置可见性失败:', error); return false; } return false; } /** * 计算适合当前缩放比的缩放因子 */ calculateContentZoomFactor(): number { // 只有在 app 准备好后才能使用screen if (!app.isReady()) { return 1; } try { // 获取系统的缩放因子 const { scaleFactor } = screen.getPrimaryDisplay(); // 缩放因子默认为1 let zoomFactor = 1; // 只在高DPI情况下调整 if (scaleFactor > 1) { // 自定义逻辑来根据不同的缩放比例进行调整 if (scaleFactor >= 2.5) { // 极高缩放比,例如4K屏幕用200%+缩放 zoomFactor = 0.7; } else if (scaleFactor >= 2) { // 高缩放比,例如200% zoomFactor = 0.8; } else if (scaleFactor >= 1.5) { // 中等缩放比,例如150% zoomFactor = 0.85; } else if (scaleFactor > 1.25) { // 略高缩放比,例如125%-149% zoomFactor = 0.9; } else { // 低缩放比,不做调整 zoomFactor = 1; } } // 获取用户的自定义缩放设置(如果有) const userZoomFactor = this.store.get('set.contentZoomFactor') as number | undefined; if (userZoomFactor) { zoomFactor = userZoomFactor; } return zoomFactor; } catch (error) { console.error('计算内容缩放因子失败:', error); return 1; } } /** * 应用页面内容缩放 */ applyContentZoom(win: BrowserWindow): void { const zoomFactor = this.calculateContentZoomFactor(); win.webContents.setZoomFactor(zoomFactor); if (app.isReady()) { try { console.log( `应用页面缩放因子: ${zoomFactor}, 系统缩放比: ${screen.getPrimaryDisplay().scaleFactor}` ); } catch (error) { console.error('获取系统缩放比失败:', error); } } else { console.log(`应用页面缩放因子: ${zoomFactor}`); } } /** * 初始化IPC消息处理程序 */ setupIPCHandlers(): void { // 防止重复注册IPC处理程序 if (ipcHandlersRegistered) { console.log('IPC处理程序已注册,跳过重复注册'); return; } console.log('注册窗口大小相关的IPC处理程序'); // 标记为已注册 ipcHandlersRegistered = true; // 安全地移除已存在的处理程序(如果有) const removeHandlerSafely = (channel: string) => { try { ipcMain.removeHandler(channel); } catch (error) { console.warn(`移除IPC处理程序 ${channel} 时出错:`, error); } }; // 为需要使用handle方法的通道先移除已有处理程序 removeHandlerSafely('get-content-zoom'); removeHandlerSafely('get-system-scale-factor'); // 注册新的处理程序 ipcMain.on('set-content-zoom', (event, zoomFactor) => { const win = BrowserWindow.fromWebContents(event.sender); if (win && !win.isDestroyed()) { win.webContents.setZoomFactor(zoomFactor); this.store.set('set.contentZoomFactor', zoomFactor); } }); ipcMain.handle('get-content-zoom', (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win && !win.isDestroyed()) { return win.webContents.getZoomFactor(); } return 1; }); ipcMain.handle('get-system-scale-factor', () => { if (!app.isReady()) { return 1; } try { return screen.getPrimaryDisplay().scaleFactor; } catch (error) { console.error('获取系统缩放因子失败:', error); return 1; } }); ipcMain.on('reset-content-zoom', (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win && !win.isDestroyed()) { this.store.delete('set.contentZoomFactor'); this.applyContentZoom(win); } }); ipcMain.on('resize-window', (event, width, height) => { const win = BrowserWindow.fromWebContents(event.sender); if (win && !win.isDestroyed()) { console.log(`接收到调整窗口大小请求: ${width}x${height}`); // 确保尺寸不小于最小值 const adjustedWidth = Math.max(width, MIN_WIDTH); const adjustedHeight = Math.max(height, MIN_HEIGHT); // 设置窗口的大小 win.setSize(adjustedWidth, adjustedHeight); console.log(`窗口大小已调整为: ${adjustedWidth}x${adjustedHeight}`); // 保存窗口状态 this.saveWindowState(win); } }); ipcMain.on('resize-mini-window', (event, showPlaylist) => { const win = BrowserWindow.fromWebContents(event.sender); if (win && !win.isDestroyed()) { if (showPlaylist) { console.log(`扩大迷你窗口至 ${DEFAULT_MINI_WIDTH} x ${DEFAULT_MINI_EXPANDED_HEIGHT}`); win.setMinimumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); win.setMaximumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_EXPANDED_HEIGHT); win.setSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_EXPANDED_HEIGHT, false); } else { console.log(`缩小迷你窗口至 ${DEFAULT_MINI_WIDTH} x ${DEFAULT_MINI_HEIGHT}`); win.setMaximumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); win.setMinimumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); win.setSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT, false); } } }); // 只在app ready后设置显示器变化监听 if (app.isReady()) { // 监听显示器变化事件 screen.on('display-metrics-changed', (_event, _display, changedMetrics) => { if (this.mainWindow && !this.mainWindow.isDestroyed()) { // 当缩放因子变化时,重新应用页面缩放 if (changedMetrics.includes('scaleFactor')) { this.applyContentZoom(this.mainWindow); } // 重新初始化最小尺寸 this.initMinimumWindowSize(); } }); } // 监听 store 中的缩放设置变化 this.store.onDidChange('set.contentZoomFactor', () => { if (this.mainWindow && !this.mainWindow.isDestroyed()) { this.applyContentZoom(this.mainWindow); } }); } } // 创建窗口大小管理器实例 const windowSizeManager = new WindowSizeManager(); // 导出初始化函数 export const initWindowSizeManager = (): void => { // 等待app ready后再初始化 if (app.isReady()) { windowSizeManager.initialize(); } else { app.on('ready', () => { windowSizeManager.initialize(); }); } }; // 导出实例方法 export const getWindowOptions = (): Electron.BrowserWindowConstructorOptions => { return windowSizeManager.getWindowOptions(); }; export const applyInitialState = (win: BrowserWindow): void => { windowSizeManager.applyInitialState(win); }; export const saveWindowState = (win: BrowserWindow): WindowState => { return windowSizeManager.saveWindowState(win); }; export const getWindowState = (): WindowState | null => { return windowSizeManager.getWindowState(); }; export const applyContentZoom = (win: BrowserWindow): void => { windowSizeManager.applyContentZoom(win); }; export const initWindowSizeHandlers = (mainWindow: BrowserWindow | null): void => { // 确保app ready后再初始化 if (!app.isReady()) { app.on('ready', () => { if (mainWindow) { windowSizeManager.setMainWindow(mainWindow); } }); } else { if (mainWindow) { windowSizeManager.setMainWindow(mainWindow); } } }; export const calculateMinimumWindowSize = (): { minWidth: number; minHeight: number } => { return { minWidth: MIN_WIDTH, minHeight: MIN_HEIGHT }; }; ================================================ FILE: src/main/modules/window.ts ================================================ import { is } from '@electron-toolkit/utils'; import { app, BrowserWindow, globalShortcut, ipcMain, nativeImage, screen, session, shell } from 'electron'; import Store from 'electron-store'; import { join } from 'path'; import { applyContentZoom, applyInitialState, DEFAULT_MAIN_HEIGHT, DEFAULT_MAIN_WIDTH, DEFAULT_MINI_HEIGHT, DEFAULT_MINI_WIDTH, getWindowOptions, getWindowState, initWindowSizeHandlers, saveWindowState, WindowState } from './window-size'; const store = new Store(); // 保存主窗口引用,以便在 activate 事件中使用 let mainWindowInstance: BrowserWindow | null = null; let isPlaying = false; let isAppQuitting = false; // 保存迷你模式前的窗口状态 let preMiniModeState: WindowState = { width: DEFAULT_MAIN_WIDTH, height: DEFAULT_MAIN_HEIGHT, x: undefined, y: undefined, isMaximized: false }; /** * 设置应用退出状态 */ export function setAppQuitting(quitting: boolean) { isAppQuitting = quitting; } /** * 初始化代理设置 */ function initializeProxy() { const defaultConfig = { enable: false, protocol: 'http', host: '127.0.0.1', port: 7890 }; const proxyConfig = store.get('set.proxyConfig', defaultConfig) as { enable: boolean; protocol: string; host: string; port: number; }; if (proxyConfig?.enable) { const proxyRules = `${proxyConfig.protocol}://${proxyConfig.host}:${proxyConfig.port}`; session.defaultSession.setProxy({ proxyRules }); } else { session.defaultSession.setProxy({ proxyRules: '' }); } } function setThumbarButtons(window: BrowserWindow) { window.setThumbarButtons([ { tooltip: 'prev', icon: nativeImage.createFromPath(join(app.getAppPath(), 'resources/icons', 'prev.png')), click() { window.webContents.send('global-shortcut', 'prevPlay'); } }, { tooltip: isPlaying ? 'pause' : 'play', icon: nativeImage.createFromPath( join(app.getAppPath(), 'resources/icons', isPlaying ? 'pause.png' : 'play.png') ), click() { window.webContents.send('global-shortcut', 'togglePlay'); } }, { tooltip: 'next', icon: nativeImage.createFromPath(join(app.getAppPath(), 'resources/icons', 'next.png')), click() { window.webContents.send('global-shortcut', 'nextPlay'); } } ]); } /** * 初始化窗口管理相关的IPC监听 */ export function initializeWindowManager() { // 初始化代理设置 initializeProxy(); ipcMain.on('minimize-window', (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win) { win.minimize(); } }); ipcMain.on('maximize-window', (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win) { if (win.isMaximized()) { win.unmaximize(); } else { win.maximize(); } // 状态保存在事件监听器中处理 } }); ipcMain.on('close-window', (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win) { // 在 macOS 上,关闭窗口不应该退出应用,而是隐藏窗口 if (process.platform === 'darwin') { win.hide(); } else { win.destroy(); app.quit(); } } }); // 强制退出应用(用于免责声明拒绝等场景) ipcMain.on('quit-app', () => { setAppQuitting(true); app.quit(); }); ipcMain.on('mini-tray', (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win) { win.hide(); } }); ipcMain.on('mini-window', (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win) { // 保存当前窗口状态,以便之后恢复 preMiniModeState = saveWindowState(win); console.log('保存正常模式状态用于恢复:', JSON.stringify(preMiniModeState)); // 获取屏幕工作区尺寸 const display = screen.getDisplayMatching(win.getBounds()); const { width: screenWidth, x: screenX } = display.workArea; // 设置迷你窗口的大小和位置 win.unmaximize(); win.setMinimumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); win.setMaximumSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT); win.setSize(DEFAULT_MINI_WIDTH, DEFAULT_MINI_HEIGHT, false); // 禁用动画 // 将迷你窗口放在工作区的右上角,留出一些边距 win.setPosition( screenX + screenWidth - DEFAULT_MINI_WIDTH - 20, display.workArea.y + 20, false ); win.setAlwaysOnTop(true); win.setSkipTaskbar(false); win.setResizable(false); // 导航到迷你模式路由 win.webContents.send('navigate', '/mini'); // 发送事件到渲染进程,通知切换到迷你模式 win.webContents.send('mini-mode', true); // 迷你窗口使用默认的缩放比 win.webContents.setZoomFactor(1); } }); // 恢复窗口 ipcMain.on('restore-window', (event) => { const win = BrowserWindow.fromWebContents(event.sender); if (win) { // 恢复窗口的大小调整功能 win.setResizable(true); win.setMaximumSize(0, 0); // 取消最大尺寸限制 console.log('从迷你模式恢复,使用保存的状态:', JSON.stringify(preMiniModeState)); // 设置适当的最小尺寸 win.setMinimumSize( Math.max(DEFAULT_MAIN_WIDTH * 0.5, 600), Math.max(DEFAULT_MAIN_HEIGHT * 0.5, 400) ); // 恢复窗口状态 win.setAlwaysOnTop(false); win.setSkipTaskbar(false); // 导航回主页面 win.webContents.send('navigate', '/'); // 发送事件到渲染进程,通知退出迷你模式 win.webContents.send('mini-mode', false); // 应用保存的状态 setTimeout(() => { // 如果有保存的位置,则应用 if (preMiniModeState.x !== undefined && preMiniModeState.y !== undefined) { win.setPosition(preMiniModeState.x, preMiniModeState.y, false); } else { win.center(); } // 使用存储的迷你模式前的状态 if (preMiniModeState.isMaximized) { win.maximize(); } else { // 设置正确的窗口大小 win.setSize(preMiniModeState.width, preMiniModeState.height, false); } // 应用页面缩放 applyContentZoom(win); // 确保窗口大小被正确应用 setTimeout(() => { if (!win.isDestroyed() && !win.isMaximized() && !win.isMinimized()) { // 再次验证窗口大小 const [width, height] = win.getSize(); if ( Math.abs(width - preMiniModeState.width) > 2 || Math.abs(height - preMiniModeState.height) > 2 ) { console.log( `恢复后窗口大小不一致,再次调整: 当前=${width}x${height}, 目标=${preMiniModeState.width}x${preMiniModeState.height}` ); win.setSize(preMiniModeState.width, preMiniModeState.height, false); } } }, 150); }, 50); } }); ipcMain.on('update-play-state', (_, playing: boolean) => { isPlaying = playing; if (mainWindowInstance) { setThumbarButtons(mainWindowInstance); } }); // 监听代理设置变化 store.onDidChange('set.proxyConfig', () => { initializeProxy(); }); // 初始化窗口大小和缩放相关的IPC处理程序 initWindowSizeHandlers(mainWindowInstance); // 监听 macOS 下点击 Dock 图标的事件 app.on('activate', () => { // 当应用被激活时,检查主窗口是否存在 if (mainWindowInstance && !mainWindowInstance.isDestroyed()) { // 如果窗口存在但被隐藏,则显示窗口 if (!mainWindowInstance.isVisible()) { mainWindowInstance.show(); } } }); } /** * 创建主窗口 */ export function createMainWindow(icon: Electron.NativeImage): BrowserWindow { console.log('开始创建主窗口...'); // 获取窗口创建选项 const options = getWindowOptions(); // 添加图标和预加载脚本 options.icon = icon; options.webPreferences = { preload: join(__dirname, '../preload/index.js'), sandbox: false, contextIsolation: true, webSecurity: false }; console.log( `创建窗口,使用选项: ${JSON.stringify({ width: options.width, height: options.height, x: options.x, y: options.y, minWidth: options.minWidth, minHeight: options.minHeight })}` ); // 创建窗口 const mainWindow = new BrowserWindow(options); // 移除菜单 mainWindow.removeMenu(); // 应用初始状态 (例如最大化状态) applyInitialState(mainWindow); // 更新 preMiniModeState,以便迷你模式可以正确恢复 const savedState = getWindowState(); if (savedState) { preMiniModeState = { ...savedState }; } mainWindow.on('show', () => { setThumbarButtons(mainWindow); }); // 处理窗口关闭事件 mainWindow.on('close', (event) => { // 在 macOS 上,阻止默认的关闭行为,改为隐藏窗口 if (process.platform === 'darwin') { // 检查是否是应用正在退出 if (!isAppQuitting) { event.preventDefault(); mainWindow.hide(); return; } } // 在其他平台上,或者应用正在退出时,允许正常关闭 }); mainWindow.on('ready-to-show', () => { const [width, height] = mainWindow.getSize(); console.log(`窗口显示前的大小: ${width}x${height}`); // 强制确保窗口使用正确的大小 if (savedState && !savedState.isMaximized) { mainWindow.setSize(savedState.width, savedState.height, false); } // 显示窗口 mainWindow.show(); // 应用页面内容缩放 applyContentZoom(mainWindow); // 再次检查窗口大小是否正确应用 setTimeout(() => { if (!mainWindow.isDestroyed() && !mainWindow.isMaximized()) { const [currentWidth, currentHeight] = mainWindow.getSize(); if (savedState && !savedState.isMaximized) { if ( Math.abs(currentWidth - savedState.width) > 2 || Math.abs(currentHeight - savedState.height) > 2 ) { console.log( `窗口大小不匹配,再次调整: 当前=${currentWidth}x${currentHeight}, 目标=${savedState.width}x${savedState.height}` ); mainWindow.setSize(savedState.width, savedState.height, false); } } } }, 100); }); mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url); return { action: 'deny' }; }); // HMR for renderer base on electron-vite cli. // Load the remote URL for development or the local html file for production. if (is.dev && process.env.ELECTRON_RENDERER_URL) { mainWindow.webContents.openDevTools({ mode: 'detach' }); mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL); // 注册快捷键 打开开发者工具 globalShortcut.register('CommandOrControl+Shift+I', () => { mainWindow.webContents.openDevTools({ mode: 'detach' }); }); } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')); } initWindowSizeHandlers(mainWindow); // 保存主窗口引用 mainWindowInstance = mainWindow; return mainWindow; } ================================================ FILE: src/main/server.ts ================================================ import { ipcMain } from 'electron'; import Store from 'electron-store'; import fs from 'fs'; import server from 'netease-cloud-music-api-alger/server'; import os from 'os'; import path from 'path'; import { type Platform, unblockMusic } from './unblockMusic'; const store = new Store(); if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) { fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8'); } // 设置音乐解析的处理程序 ipcMain.handle('unblock-music', async (_event, id, songData, enabledSources) => { try { const result = await unblockMusic(id, songData, 1, enabledSources as Platform[]); return result; } catch (error) { console.error('音乐解析失败:', error); return { error: (error as Error).message || '未知错误' }; } }); /** * 检查端口是否可用 */ function checkPortAvailable(port: number): Promise { return new Promise((resolve) => { const net = require('net'); const tester = net .createServer() .once('error', () => { resolve(false); }) .once('listening', () => { tester.close(() => resolve(true)); }) .listen(port); }); } async function startMusicApi(): Promise { console.log('MUSIC API STARTING...'); const settings = store.get('set') as any; let port = settings?.musicApiPort || 30488; const maxRetries = 10; // 检查端口是否可用,如果不可用则尝试下一个端口 for (let i = 0; i < maxRetries; i++) { const isAvailable = await checkPortAvailable(port); if (isAvailable) { break; } console.log(`端口 ${port} 被占用,尝试切换到端口 ${port + 1}`); port++; } // 如果端口发生变化,保存新端口到配置 const originalPort = settings?.musicApiPort || 30488; if (port !== originalPort) { console.log(`端口从 ${originalPort} 切换到 ${port}`); store.set('set', { ...settings, musicApiPort: port }); } try { await server.serveNcmApi({ port }); console.log(`MUSIC API STARTED on port ${port}`); } catch (error) { console.error(`MUSIC API 启动失败:`, error); throw error; } } export { startMusicApi }; ================================================ FILE: src/main/set.json ================================================ { "isProxy": false, "proxyConfig": { "enable": false, "protocol": "http", "host": "127.0.0.1", "port": 7890 }, "enableRealIP": false, "realIP": "", "noAnimate": false, "animationSpeed": 1, "author": "Alger", "authorUrl": "https://github.com/algerkong", "musicApiPort": 30488, "closeAction": "ask", "musicQuality": "higher", "lyricTranslationEngine": "none", "fontFamily": "system-ui", "fontScope": "global", "autoPlay": false, "downloadPath": "", "language": "zh-CN", "alwaysShowDownloadButton": false, "unlimitedDownload": false, "enableMusicUnblock": true, "enabledMusicSources": ["migu", "kugou", "pyncmd"], "showTopAction": false, "contentZoomFactor": 1, "autoTheme": false, "manualTheme": "light", "isMenuExpanded": false, "customApiPlugin": "", "customApiPluginName": "", "lxMusicScripts": [], "activeLxMusicApiId": null, "enableGpuAcceleration": true } ================================================ FILE: src/main/unblockMusic.ts ================================================ import match from '@unblockneteasemusic/server'; type Platform = 'qq' | 'migu' | 'kugou' | 'kuwo' | 'pyncmd' | 'joox' | 'bilibili'; interface SongData { name: string; artists: Array<{ name: string }>; album?: { name: string }; ar?: Array<{ name: string }>; al?: { name: string }; } interface ResponseData { url: string; br: number; size: number; md5?: string; platform?: Platform; gain?: number; } interface UnblockResult { data: { data: ResponseData; params: { id: number; type: 'song'; }; }; } // 所有可用平台 export const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'kuwo', 'pyncmd', 'bilibili']; /** * 确保对象数据结构完整,处理null或undefined的情况 * @param data 需要处理的数据对象 */ function ensureDataStructure(data: any): any { // 如果数据本身为空,则返回一个基本结构 if (!data) { return { name: '', artists: [], album: { name: '' } }; } // 确保name字段存在 if (data.name === undefined || data.name === null) { data.name = ''; } // 确保artists字段存在且为数组 if (!data.artists || !Array.isArray(data.artists)) { data.artists = data.ar && Array.isArray(data.ar) ? data.ar : []; } // 确保artists中的每个元素都有name属性 if (data.artists.length > 0) { data.artists = data.artists.map((artist) => { return artist ? { name: artist.name || '' } : { name: '' }; }); } // 确保album对象存在并有name属性 if (!data.album || typeof data.album !== 'object') { data.album = data.al && typeof data.al === 'object' ? data.al : { name: '' }; } if (!data.album.name) { data.album.name = ''; } return data; } /** * 音乐解析函数 * @param id 歌曲ID * @param songData 歌曲信息 * @param retryCount 重试次数 * @param enabledPlatforms 启用的平台列表,默认为所有平台 * @returns Promise */ const unblockMusic = async ( id: number | string, songData: SongData, retryCount = 1, enabledPlatforms?: Platform[] ): Promise => { // 过滤 enabledPlatforms,确保只包含 ALL_PLATFORMS 中存在的平台 const filteredPlatforms = enabledPlatforms ? enabledPlatforms.filter((platform) => ALL_PLATFORMS.includes(platform)) : ALL_PLATFORMS; // 处理歌曲数据,确保数据结构完整 const processedSongData = ensureDataStructure(songData); const retry = async (attempt: number): Promise => { try { const data = await match(parseInt(String(id), 10), filteredPlatforms, processedSongData); const result: UnblockResult = { data: { data, params: { id: parseInt(String(id), 10), type: 'song' } } }; return result; } catch (err) { if (attempt < retryCount) { // 延迟重试,每次重试增加延迟时间 await new Promise((resolve) => setTimeout(resolve, 100 * attempt)); return retry(attempt + 1); } // 所有重试都失败后,抛出详细错误 throw new Error( `音乐解析失败 (ID: ${id}): ${err instanceof Error ? err.message : '未知错误'}` ); } }; return retry(1); }; export { type Platform, type ResponseData, type SongData, unblockMusic, type UnblockResult }; ================================================ FILE: src/preload/index.d.ts ================================================ import { ElectronAPI } from '@electron-toolkit/preload'; interface API { minimize: () => void; maximize: () => void; close: () => void; quitApp: () => void; dragStart: (data: any) => void; miniTray: () => void; miniWindow: () => void; restore: () => void; restart: () => void; resizeWindow: (width: number, height: number) => void; resizeMiniWindow: (showPlaylist: boolean) => void; openLyric: () => void; sendLyric: (data: any) => void; sendSong: (data: any) => void; unblockMusic: (id: number, data: any, enabledSources?: string[]) => Promise; onLyricWindowClosed: (callback: () => void) => void; startDownload: (url: string) => void; onDownloadProgress: (callback: (progress: number, status: string) => void) => void; onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => void; onLanguageChanged: (callback: (locale: string) => void) => void; removeDownloadListeners: () => void; importCustomApiPlugin: () => Promise<{ name: string; content: string } | null>; importLxMusicScript: () => Promise<{ name: string; content: string } | null>; invoke: (channel: string, ...args: any[]) => Promise; getSearchSuggestions: (keyword: string) => Promise; lxMusicHttpRequest: (request: { url: string; options: any; requestId: string }) => Promise; lxMusicHttpCancel: (requestId: string) => Promise; } // 自定义IPC渲染进程通信接口 interface IpcRenderer { send: (channel: string, ...args: any[]) => void; invoke: (channel: string, ...args: any[]) => Promise; on: (channel: string, listener: (...args: any[]) => void) => () => void; removeAllListeners: (channel: string) => void; } declare global { interface Window { electron: ElectronAPI; api: API; ipcRenderer: IpcRenderer; $message: any; } } ================================================ FILE: src/preload/index.ts ================================================ import { electronAPI } from '@electron-toolkit/preload'; import { contextBridge, ipcRenderer } from 'electron'; // Custom APIs for renderer const api = { minimize: () => ipcRenderer.send('minimize-window'), maximize: () => ipcRenderer.send('maximize-window'), close: () => ipcRenderer.send('close-window'), quitApp: () => ipcRenderer.send('quit-app'), dragStart: (data) => ipcRenderer.send('drag-start', data), miniTray: () => ipcRenderer.send('mini-tray'), miniWindow: () => ipcRenderer.send('mini-window'), restore: () => ipcRenderer.send('restore-window'), restart: () => ipcRenderer.send('restart'), resizeWindow: (width, height) => ipcRenderer.send('resize-window', width, height), resizeMiniWindow: (showPlaylist) => ipcRenderer.send('resize-mini-window', showPlaylist), openLyric: () => ipcRenderer.send('open-lyric'), sendLyric: (data) => ipcRenderer.send('send-lyric', data), sendSong: (data) => ipcRenderer.send('update-current-song', data), unblockMusic: (id, data, enabledSources) => ipcRenderer.invoke('unblock-music', id, data, enabledSources), importCustomApiPlugin: () => ipcRenderer.invoke('import-custom-api-plugin'), importLxMusicScript: () => ipcRenderer.invoke('import-lx-music-script'), // 歌词窗口关闭事件 onLyricWindowClosed: (callback: () => void) => { ipcRenderer.on('lyric-window-closed', () => callback()); }, // 更新相关 startDownload: (url: string) => ipcRenderer.send('start-download', url), onDownloadProgress: (callback: (progress: number, status: string) => void) => { ipcRenderer.on('download-progress', (_event, progress, status) => callback(progress, status)); }, onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => { ipcRenderer.on('download-complete', (_event, success, filePath) => callback(success, filePath)); }, // 语言相关 onLanguageChanged: (callback: (locale: string) => void) => { ipcRenderer.on('language-changed', (_event, locale) => { callback(locale); }); }, removeDownloadListeners: () => { ipcRenderer.removeAllListeners('download-progress'); ipcRenderer.removeAllListeners('download-complete'); }, // 歌词缓存相关 invoke: (channel: string, ...args: any[]) => { const validChannels = [ 'get-lyrics', 'clear-lyrics-cache', 'get-system-fonts', 'get-cached-lyric', 'cache-lyric', 'clear-lyric-cache' ]; if (validChannels.includes(channel)) { return ipcRenderer.invoke(channel, ...args); } return Promise.reject(new Error(`未授权的 IPC 通道: ${channel}`)); }, // 搜索建议 getSearchSuggestions: (keyword: string) => ipcRenderer.invoke('get-search-suggestions', keyword), // 落雪音乐 HTTP 请求(绕过 CORS) lxMusicHttpRequest: (request: { url: string; options: any; requestId: string }) => ipcRenderer.invoke('lx-music-http-request', request), lxMusicHttpCancel: (requestId: string) => ipcRenderer.invoke('lx-music-http-cancel', requestId) }; // 创建带类型的ipcRenderer对象,暴露给渲染进程 const ipc = { // 发送消息到主进程(无返回值) send: (channel: string, ...args: any[]) => { ipcRenderer.send(channel, ...args); }, // 调用主进程方法(有返回值) invoke: (channel: string, ...args: any[]) => { return ipcRenderer.invoke(channel, ...args); }, // 监听主进程消息 on: (channel: string, listener: (...args: any[]) => void) => { ipcRenderer.on(channel, (_, ...args) => listener(...args)); return () => { ipcRenderer.removeListener(channel, listener); }; }, // 移除所有监听器 removeAllListeners: (channel: string) => { ipcRenderer.removeAllListeners(channel); } }; // Use `contextBridge` APIs to expose Electron APIs to // renderer only if context isolation is enabled, otherwise // just add to the DOM global. if (process.contextIsolated) { try { contextBridge.exposeInMainWorld('electron', electronAPI); contextBridge.exposeInMainWorld('api', api); contextBridge.exposeInMainWorld('ipcRenderer', ipc); } catch (error) { console.error(error); } } else { // @ts-ignore (define in dts) window.electron = electronAPI; // @ts-ignore (define in dts) window.api = api; // @ts-ignore (define in dts) window.ipcRenderer = ipc; } ================================================ FILE: src/renderer/App.vue ================================================ ================================================ FILE: src/renderer/api/artist.ts ================================================ import request from '@/utils/request'; // 获取歌手详情 export const getArtistDetail = (id) => { return request.get('/artist/detail', { params: { id } }); }; // 获取歌手热门歌曲 export const getArtistTopSongs = (params) => { return request.get('/artist/songs', { params: { ...params, order: 'hot' } }); }; // 获取歌手专辑 export const getArtistAlbums = (params) => { return request.get('/artist/album', { params }); }; ================================================ FILE: src/renderer/api/bilibili.ts ================================================ import type { IBilibiliPage, IBilibiliPlayUrl, IBilibiliVideoDetail } from '@/types/bilibili'; import type { SongResult } from '@/types/music'; import { getSetData, isElectron } from '@/utils'; import request from '@/utils/request'; interface ISearchParams { keyword: string; page?: number; pagesize?: number; search_type?: string; } /** * 搜索B站视频(带自动重试) * 最多重试10次,每次间隔100ms * @param params 搜索参数 */ export const searchBilibili = async (params: ISearchParams): Promise => { console.log('调用B站搜索API,参数:', params); const maxRetries = 10; const delayMs = 100; const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); let lastError: unknown = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const response = await request.get('/bilibili/search', { params }); console.log('B站搜索API响应:', response); const hasTitle = Boolean(response?.data?.data?.result?.length); if (response?.status === 200 && hasTitle) { return response; } lastError = new Error( `搜索结果不符合成功条件(缺少 data.title ) (attempt ${attempt}/${maxRetries})` ); console.warn('B站搜索API响应不符合要求,将重试。调试信息:', { status: response?.status, hasData: Boolean(response?.data), hasInnerData: Boolean(response?.data?.data), title: response?.data?.data?.title }); } catch (error) { lastError = error; console.warn(`B站搜索API错误[第${attempt}次],将重试:`, error); } if (attempt === maxRetries) { console.error('B站搜索API重试达到上限,仍然失败'); if (lastError instanceof Error) throw lastError; throw new Error('B站搜索失败且达到最大重试次数'); } await delay(delayMs); } // 理论上不会到达这里,添加以满足TS控制流分析 throw new Error('B站搜索在重试后未返回有效结果'); }; interface IBilibiliResponse { code: number; message: string; ttl: number; data: T; } /** * 获取B站视频详情 * @param bvid B站视频BV号 * @returns 视频详情响应 */ export const getBilibiliVideoDetail = ( bvid: string ): Promise> => { console.log('调用B站视频详情API,bvid:', bvid); return new Promise((resolve, reject) => { request .get('/bilibili/video/detail', { params: { bvid } }) .then((response) => { console.log('B站视频详情API响应:', response.status); // 检查响应状态和数据格式 if (response.status === 200 && response.data && response.data.data) { console.log('B站视频详情API成功,标题:', response.data.data.title); resolve(response.data); } else { console.error('B站视频详情API响应格式不正确:', response.data); reject(new Error('获取视频详情响应格式不正确')); } }) .catch((error) => { console.error('B站视频详情API错误:', error); reject(error); }); }); }; /** * 获取B站视频播放地址 * @param bvid B站视频BV号 * @param cid 视频分P的id * @param qn 视频质量,默认为0 * @param fnval 视频格式标志,默认为80 * @param fnver 视频格式版本,默认为0 * @param fourk 是否允许4K视频,默认为1 * @returns 视频播放地址响应 */ export const getBilibiliPlayUrl = ( bvid: string, cid: number, qn: number = 0, fnval: number = 80, fnver: number = 0, fourk: number = 1 ): Promise> => { console.log('调用B站视频播放地址API,bvid:', bvid, 'cid:', cid); return new Promise((resolve, reject) => { request .get('/bilibili/playurl', { params: { bvid, cid, qn, fnval, fnver, fourk } }) .then((response) => { console.log('B站视频播放地址API响应:', response.status); // 检查响应状态和数据格式 if (response.status === 200 && response.data && response.data.data) { if (response.data.data.dash?.audio?.length > 0) { console.log( 'B站视频播放地址API成功,获取到', response.data.data.dash.audio.length, '个音频地址' ); } else if (response.data.data.durl?.length > 0) { console.log( 'B站视频播放地址API成功,获取到', response.data.data.durl.length, '个播放地址' ); } resolve(response.data); } else { console.error('B站视频播放地址API响应格式不正确:', response.data); reject(new Error('获取视频播放地址响应格式不正确')); } }) .catch((error) => { console.error('B站视频播放地址API错误:', error); reject(error); }); }); }; export const getBilibiliProxyUrl = (url: string) => { const setData = getSetData(); const baseURL = isElectron ? `http://127.0.0.1:${setData?.musicApiPort}` : import.meta.env.VITE_API; const AUrl = url.startsWith('http') ? url : `https:${url}`; return `${baseURL}/bilibili/stream-proxy?url=${encodeURIComponent(AUrl)}`; }; export const getBilibiliAudioUrl = async (bvid: string, cid: number): Promise => { console.log('获取B站音频URL', { bvid, cid }); try { const res = await getBilibiliPlayUrl(bvid, cid); const playUrlData = res.data; let url = ''; if (playUrlData.dash && playUrlData.dash.audio && playUrlData.dash.audio.length > 0) { url = playUrlData.dash.audio[playUrlData.dash.audio.length - 1].baseUrl; } else if (playUrlData.durl && playUrlData.durl.length > 0) { url = playUrlData.durl[0].url; } else { throw new Error('未找到可用的音频地址'); } return getBilibiliProxyUrl(url); } catch (error) { console.error('获取B站音频URL失败:', error); throw error; } }; // 根据音乐名称搜索并直接返回音频URL export const searchAndGetBilibiliAudioUrl = async (keyword: string): Promise => { try { // 搜索B站视频,取第一页第一个结果 const res = await searchBilibili({ keyword, page: 1, pagesize: 1 }); if (!res) { throw new Error('B站搜索返回为空'); } const result = res.data?.data?.result; if (!result || result.length === 0) { throw new Error('未找到相关B站视频'); } const first = result[0]; const bvid = first.bvid; // 需要获取视频详情以获得cid const detailRes = await getBilibiliVideoDetail(bvid); const pages = detailRes.data.pages; if (!pages || pages.length === 0) { throw new Error('未找到视频分P信息'); } const cid = pages[0].cid; // 获取音频URL return await getBilibiliAudioUrl(bvid, cid); } catch (error) { console.error('根据名称搜索B站音频URL失败:', error); throw error; } }; /** * 解析B站ID格式 * @param biliId B站ID,可能是字符串格式(bvid--pid--cid) * @returns 解析后的对象 {bvid, pid, cid} 或 null */ export const parseBilibiliId = ( biliId: string | number ): { bvid: string; pid: string; cid: number } | null => { const strBiliId = String(biliId); if (strBiliId.includes('--')) { const [bvid, pid, cid] = strBiliId.split('--'); if (!bvid || !pid || !cid) { console.warn(`B站ID格式错误: ${strBiliId}, 正确格式应为 bvid--pid--cid`); return null; } return { bvid, pid, cid: Number(cid) }; } return null; }; /** * 创建默认的Artist对象 * @param name 艺术家名称 * @param id 艺术家ID * @returns Artist对象 */ const createDefaultArtist = (name: string, id: number = 0) => ({ name, id, picId: 0, img1v1Id: 0, briefDesc: '', img1v1Url: '', albumSize: 0, alias: [], trans: '', musicSize: 0, topicPerson: 0, picUrl: '' }); /** * 创建默认的Album对象 * @param name 专辑名称 * @param picUrl 专辑图片URL * @param artistName 艺术家名称 * @param artistId 艺术家ID * @returns Album对象 */ const createDefaultAlbum = ( name: string, picUrl: string, artistName: string, artistId: number = 0 ) => ({ name, picUrl, id: 0, type: '', size: 0, picId: 0, blurPicUrl: '', companyId: 0, pic: 0, publishTime: 0, description: '', tags: '', company: '', briefDesc: '', artist: createDefaultArtist(artistName, artistId), songs: [], alias: [], status: 0, copyrightId: 0, commentThreadId: '', artists: [], subType: '', transName: null, onSale: false, mark: 0, picId_str: '' }); /** * 创建基础的B站SongResult对象 * @param config 配置对象 * @returns SongResult对象 */ const createBaseBilibiliSong = (config: { id: string | number; name: string; picUrl: string; artistName: string; artistId?: number; albumName: string; bilibiliData?: { bvid: string; cid: number }; playMusicUrl?: string; duration?: number; }): SongResult => { const { id, name, picUrl, artistName, artistId = 0, albumName, bilibiliData, playMusicUrl, duration } = config; const baseResult: SongResult = { id, name, picUrl, ar: [createDefaultArtist(artistName, artistId)], al: createDefaultAlbum(albumName, picUrl, artistName, artistId), count: 0, source: 'bilibili' as const }; if (bilibiliData) { baseResult.bilibiliData = bilibiliData; } if (playMusicUrl) { baseResult.playMusicUrl = playMusicUrl; } if (duration !== undefined) { baseResult.duration = duration; } return baseResult as SongResult; }; /** * 从B站视频详情和分P信息创建SongResult对象 * @param videoDetail B站视频详情 * @param page 分P信息 * @param bvid B站视频ID * @returns SongResult对象 */ export const createSongFromBilibiliVideo = ( videoDetail: IBilibiliVideoDetail, page: IBilibiliPage, bvid: string ): SongResult => { const pageName = page.part || ''; const title = `${pageName} - ${videoDetail.title}`; const songId = `${bvid}--${page.page}--${page.cid}`; const picUrl = getBilibiliProxyUrl(videoDetail.pic); return createBaseBilibiliSong({ id: songId, name: title, picUrl, artistName: videoDetail.owner.name, artistId: videoDetail.owner.mid, albumName: videoDetail.title, bilibiliData: { bvid, cid: page.cid } }); }; /** * 创建简化的SongResult对象(用于搜索结果直接播放) * @param item 搜索结果项 * @param audioUrl 音频URL * @returns SongResult对象 */ export const createSimpleBilibiliSong = (item: any, audioUrl: string): SongResult => { const duration = typeof item.duration === 'string' ? 0 : item.duration * 1000; // 转换为毫秒 return createBaseBilibiliSong({ id: item.id, name: item.title, picUrl: item.pic, artistName: item.author, albumName: item.title, playMusicUrl: audioUrl, duration }); }; /** * 批量处理B站视频,从ID列表获取SongResult列表 * @param bilibiliIds B站ID列表 * @returns SongResult列表 */ export const processBilibiliVideos = async ( bilibiliIds: (string | number)[] ): Promise => { const bilibiliSongs: SongResult[] = []; for (const biliId of bilibiliIds) { const parsedId = parseBilibiliId(biliId); if (!parsedId) continue; try { const res = await getBilibiliVideoDetail(parsedId.bvid); const videoDetail = res.data; // 找到对应的分P const page = videoDetail.pages.find((p) => p.cid === parsedId.cid); if (!page) { console.warn(`未找到对应的分P: cid=${parsedId.cid}`); continue; } const songData = createSongFromBilibiliVideo(videoDetail, page, parsedId.bvid); bilibiliSongs.push(songData); } catch (error) { console.error(`获取B站视频详情失败 (${biliId}):`, error); } } return bilibiliSongs; }; ================================================ FILE: src/renderer/api/donation.ts ================================================ import axios from 'axios'; export interface Donor { id: number; name: string; amount: number; date: string; message?: string; avatar?: string; badge: string; badgeColor: string; } /** * 获取捐赠列表 */ export const getDonationList = async (): Promise => { const { data } = await axios.get('http://donate.alger.fun/api/donations'); return data; }; ================================================ FILE: src/renderer/api/gdmusic.ts ================================================ import axios from 'axios'; import type { MusicSourceType } from '@/types/music'; /** * GD音乐台解析服务 */ export interface GDMusicResponse { url: string; br: number; size: number; md5: string; platform: string; gain: number; } export interface ParsedMusicResult { data: { data: GDMusicResponse; params: { id: number; type: string; }; }; } /** * 从GD音乐台解析音乐URL * @param id 音乐ID * @param data 音乐数据,包含名称和艺术家信息 * @param quality 音质设置 * @param timeout 超时时间(毫秒),默认15000ms * @returns 解析后的音乐URL及相关信息 */ export const parseFromGDMusic = async ( id: number, data: any, quality: string = '999', timeout: number = 15000 ): Promise => { // 创建一个超时Promise const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error('GD音乐台解析超时')); }, timeout); }); try { // 使用Promise.race竞争主解析流程和超时 return await Promise.race([ (async () => { // 处理不同数据结构 if (!data) { console.error('GD音乐台解析:歌曲数据为空'); throw new Error('歌曲数据为空'); } const songName = data.name || ''; let artistNames = ''; // 处理不同的艺术家字段结构 if (data.artists && Array.isArray(data.artists)) { artistNames = data.artists.map((artist) => artist.name).join(' '); } else if (data.ar && Array.isArray(data.ar)) { artistNames = data.ar.map((artist) => artist.name).join(' '); } else if (data.artist) { artistNames = typeof data.artist === 'string' ? data.artist : ''; } const searchQuery = `${songName} ${artistNames}`.trim(); if (!searchQuery || searchQuery.length < 2) { console.error('GD音乐台解析:搜索查询过短', { name: songName, artists: artistNames }); throw new Error('搜索查询过短'); } // 所有可用的音乐源 netease、joox、tidal const allSources = ['joox', 'tidal', 'netease'] as MusicSourceType[]; console.log('GD音乐台开始搜索:', searchQuery); // 依次尝试所有音源 for (const source of allSources) { try { const result = await searchAndGetUrl(source, searchQuery, quality); if (result) { console.log(`GD音乐台成功通过 ${result.source} 解析音乐!`); // 返回符合原API格式的数据 return { data: { data: { url: result.url.replace(/\\/g, ''), br: parseInt(result.br, 10) * 1000 || 320000, size: result.size || 0, md5: '', platform: 'gdmusic', gain: 0 }, params: { id: parseInt(String(id), 10), type: 'song' } } }; } } catch (error) { console.error(`GD音乐台 ${source} 音源解析失败:`, error); // 该音源失败,继续尝试下一个音源 continue; } } console.log('GD音乐台所有音源均解析失败'); return null; })(), timeoutPromise ]); } catch (error: any) { if (error.message === 'GD音乐台解析超时') { console.error('GD音乐台解析超时(15秒):', error); } else { console.error('GD音乐台解析完全失败:', error); } return null; } }; interface GDMusicUrlResult { url: string; br: string; size: number; source: string; } const baseUrl = 'https://music-api.gdstudio.xyz/api.php'; /** * 在指定音源搜索歌曲并获取URL * @param source 音源 * @param searchQuery 搜索关键词 * @param quality 音质 * @returns 音乐URL结果 */ async function searchAndGetUrl( source: MusicSourceType, searchQuery: string, quality: string ): Promise { // 1. 搜索歌曲 const searchUrl = `${baseUrl}?types=search&source=${source}&name=${encodeURIComponent(searchQuery)}&count=1&pages=1`; console.log(`GD音乐台尝试音源 ${source} 搜索:`, searchUrl); const searchResponse = await axios.get(searchUrl, { timeout: 5000 }); if (searchResponse.data && Array.isArray(searchResponse.data) && searchResponse.data.length > 0) { const firstResult = searchResponse.data[0]; if (!firstResult || !firstResult.id) { console.log(`GD音乐台 ${source} 搜索结果无效`); return null; } const trackId = firstResult.id; const trackSource = firstResult.source || source; // 2. 获取歌曲URL const songUrl = `${baseUrl}?types=url&source=${trackSource}&id=${trackId}&br=${quality}`; console.log(`GD音乐台尝试获取 ${trackSource} 歌曲URL:`, songUrl); const songResponse = await axios.get(songUrl, { timeout: 5000 }); if (songResponse.data && songResponse.data.url) { return { url: songResponse.data.url, br: songResponse.data.br, size: songResponse.data.size || 0, source: trackSource }; } else { console.log(`GD音乐台 ${trackSource} 未返回有效URL`); return null; } } else { console.log(`GD音乐台 ${source} 搜索结果为空`); return null; } } ================================================ FILE: src/renderer/api/home.ts ================================================ import { IData } from '@/types'; import { IAlbumNew } from '@/types/album'; import { IDayRecommend } from '@/types/day_recommend'; import { IRecommendMusic } from '@/types/music'; import { IPlayListSort } from '@/types/playlist'; import { IHotSearch, ISearchKeyword } from '@/types/search'; import { IHotSinger } from '@/types/singer'; import request from '@/utils/request'; interface IHotSingerParams { offset: number; limit: number; } interface IRecommendMusicParams { limit: number; } // 获取热门歌手 export const getHotSinger = (params: IHotSingerParams) => { return request.get('/top/artists', { params }); }; // 获取搜索推荐词 export const getSearchKeyword = () => { return request.get('/search/default'); }; // 获取热门搜索 export const getHotSearch = () => { return request.get('/search/hot/detail'); }; // 获取歌单分类 export const getPlaylistCategory = () => { return request.get('/playlist/catlist'); }; // 获取推荐音乐 export const getRecommendMusic = (params: IRecommendMusicParams) => { return request.get('/personalized/newsong', { params }); }; // 获取每日推荐 export const getDayRecommend = () => { return request.get>>('/recommend/songs'); }; // 获取最新专辑推荐 export const getNewAlbum = () => { return request.get('/album/newest'); }; ================================================ FILE: src/renderer/api/list.ts ================================================ import { IList } from '@/types/list'; import type { IListDetail } from '@/types/listDetail'; import request from '@/utils/request'; interface IListByTagParams { tag: string; before: number; limit: number; } interface IListByCatParams { cat: string; offset: number; limit: number; } // 根据tag 获取歌单列表 export function getListByTag(params: IListByTagParams) { return request.get('/top/playlist/highquality', { params }); } // 根据cat 获取歌单列表 export function getListByCat(params: IListByCatParams) { return request.get('/top/playlist', { params }); } // 获取推荐歌单 export function getRecommendList(limit: number = 30) { return request.get('/personalized', { params: { limit } }); } // 获取歌单详情 export function getListDetail(id: number | string) { return request.get('/playlist/detail', { params: { id } }); } // 获取专辑内容 export function getAlbum(id: number | string) { return request.get('/album', { params: { id } }); } // 获取排行榜列表 export function getToplist() { return request.get('/toplist'); } ================================================ FILE: src/renderer/api/login.ts ================================================ import request from '@/utils/request'; // 创建二维码key // /login/qr/key export function getQrKey() { return request.get('/login/qr/key'); } // 创建二维码 // /login/qr/create export function createQr(key: any) { return request.get('/login/qr/create', { params: { key, qrimg: true } }); } // 获取二维码状态 // /login/qr/check export function checkQr(key: any) { return request.get('/login/qr/check', { params: { key, noCookie: true } }); } // 获取登录状态 // /login/status export function getLoginStatus() { return request.get('/login/status'); } // 获取用户信息 // /user/account export function getUserDetail() { return request.get('/user/account'); } // 退出登录 // /logout export function logout() { return request.get('/logout'); } // 手机号登录 // /login/cellphone export function loginByCellphone(phone: string, password: string) { return request.post('/login/cellphone', { phone, password }); } // UID登录 - 通过用户ID获取用户信息 // /user/detail export function loginByUid(uid: string | number) { return request.get('/user/detail', { params: { uid } }); } ================================================ FILE: src/renderer/api/lxMusicStrategy.ts ================================================ /** * 落雪音乐 (LX Music) 音源解析策略 * * 实现 MusicSourceStrategy 接口,作为落雪音源的解析入口 */ import { getLxMusicRunner, initLxMusicRunner } from '@/services/LxMusicSourceRunner'; import { useSettingsStore } from '@/store'; import type { LxMusicInfo, LxQuality, LxSourceKey } from '@/types/lxMusic'; import { LX_SOURCE_NAMES, QUALITY_TO_LX } from '@/types/lxMusic'; import type { SongResult } from '@/types/music'; import type { MusicParseResult } from './musicParser'; import { CacheManager } from './musicParser'; /** * 解析可能是 API 端点的 URL,获取真实音频 URL * 一些音源脚本返回的是 API 端点,需要额外请求才能获取真实音频 URL */ const resolveAudioUrl = async (url: string): Promise => { try { // 检查是否看起来像 API 端点(包含 /api/ 且有查询参数) const isApiEndpoint = url.includes('/api/') || (url.includes('?') && url.includes('type=url')); if (!isApiEndpoint) { // 看起来像直接的音频 URL,直接返回 return url; } console.log('[LxMusicStrategy] 检测到 API 端点,尝试解析真实 URL:', url); // 尝试获取真实 URL const response = await fetch(url, { method: 'HEAD', redirect: 'manual' // 不自动跟随重定向 }); // 检查是否是重定向 if (response.status >= 300 && response.status < 400) { const location = response.headers.get('Location'); if (location) { console.log('[LxMusicStrategy] API 返回重定向 URL:', location); return location; } } // 如果 HEAD 请求没有重定向,尝试 GET 请求 const getResponse = await fetch(url, { redirect: 'follow' }); // 检查 Content-Type const contentType = getResponse.headers.get('Content-Type') || ''; // 如果是音频类型,返回最终 URL if (contentType.includes('audio/') || contentType.includes('application/octet-stream')) { console.log('[LxMusicStrategy] 解析到音频 URL:', getResponse.url); return getResponse.url; } // 如果是 JSON,尝试解析 if (contentType.includes('application/json') || contentType.includes('text/json')) { const json = await getResponse.json(); console.log('[LxMusicStrategy] API 返回 JSON:', json); // 尝试从 JSON 中提取 URL(常见字段) const audioUrl = json.url || json.data?.url || json.audio_url || json.link || json.src; if (audioUrl && typeof audioUrl === 'string') { console.log('[LxMusicStrategy] 从 JSON 中提取音频 URL:', audioUrl); return audioUrl; } } // 如果都不是,返回原始 URL(可能直接可用) console.warn('[LxMusicStrategy] 无法解析 API 端点,返回原始 URL'); return url; } catch (error) { console.error('[LxMusicStrategy] URL 解析失败:', error); // 解析失败时返回原始 URL return url; } }; /** * 将 SongResult 转换为 LxMusicInfo 格式 */ const convertToLxMusicInfo = (songResult: SongResult): LxMusicInfo => { const artistName = songResult.ar && songResult.ar.length > 0 ? songResult.ar.map((a) => a.name).join('、') : songResult.artists && songResult.artists.length > 0 ? songResult.artists.map((a) => a.name).join('、') : ''; const albumName = songResult.al?.name || (songResult.album as any)?.name || ''; const albumId = songResult.al?.id || (songResult.album as any)?.id || ''; // 计算时长(秒转分钟:秒格式) const duration = songResult.dt || songResult.duration || 0; const minutes = Math.floor(duration / 60000); const seconds = Math.floor((duration % 60000) / 1000); const interval = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; return { songmid: songResult.id, name: songResult.name, singer: artistName, album: albumName, albumId, source: 'wy', // 默认使用网易云作为源,因为我们的数据来自网易云 interval, img: songResult.picUrl || songResult.al?.picUrl || '' }; }; /** * 获取最佳匹配的落雪音源 * 因为我们的数据来自网易云,优先尝试 wy 音源 */ const getBestMatchingSource = ( availableSources: LxSourceKey[], _songSource?: string ): LxSourceKey | null => { // 优先级顺序:网易云 > 酷我 > 咪咕 > 酷狗 > QQ音乐 const priority: LxSourceKey[] = ['wy', 'kw', 'mg', 'kg', 'tx']; for (const source of priority) { if (availableSources.includes(source)) { return source; } } return availableSources[0] || null; }; /** * 落雪音乐解析策略 */ export class LxMusicStrategy { name = 'lxMusic'; priority = 0; // 最高优先级 /** * 检查是否可以处理 */ canHandle(sources: string[], settingsStore?: any): boolean { // 检查是否启用了落雪音源 if (!sources.includes('lxMusic')) { return false; } // 检查是否有激活的音源 const activeLxApiId = settingsStore?.setData?.activeLxMusicApiId; if (!activeLxApiId) { return false; } // 检查音源列表中是否存在该 ID const lxMusicScripts = settingsStore?.setData?.lxMusicScripts || []; const activeScript = lxMusicScripts.find((script: any) => script.id === activeLxApiId); return Boolean(activeScript && activeScript.script); } /** * 解析音乐 URL */ async parse( id: number, data: SongResult, quality?: string, _sources?: string[] ): Promise { // 检查失败缓存 if (CacheManager.isInFailedCache(id, this.name)) { return null; } try { const settingsStore = useSettingsStore(); // 获取激活的音源 ID const activeLxApiId = settingsStore.setData?.activeLxMusicApiId; if (!activeLxApiId) { console.log('[LxMusicStrategy] 未选择激活的落雪音源'); return null; } // 从音源列表中获取激活的脚本 const lxMusicScripts = settingsStore.setData?.lxMusicScripts || []; const activeScript = lxMusicScripts.find((script: any) => script.id === activeLxApiId); if (!activeScript || !activeScript.script) { console.log('[LxMusicStrategy] 未找到激活的落雪音源脚本'); return null; } console.log(`[LxMusicStrategy] 使用激活的音源: ${activeScript.name} (ID: ${activeScript.id})`); // 获取或初始化执行器 let runner = getLxMusicRunner(); if (!runner || !runner.isInitialized()) { console.log('[LxMusicStrategy] 初始化落雪音源执行器...'); runner = await initLxMusicRunner(activeScript.script); } // 获取可用音源 const sources = runner.getSources(); const availableSourceKeys = Object.keys(sources) as LxSourceKey[]; if (availableSourceKeys.length === 0) { console.log('[LxMusicStrategy] 没有可用的落雪音源'); CacheManager.addFailedCache(id, this.name); return null; } // 选择最佳音源 const bestSource = getBestMatchingSource(availableSourceKeys); if (!bestSource) { console.log('[LxMusicStrategy] 无法找到匹配的音源'); CacheManager.addFailedCache(id, this.name); return null; } console.log(`[LxMusicStrategy] 使用音源: ${LX_SOURCE_NAMES[bestSource]} (${bestSource})`); // 转换歌曲信息 const lxMusicInfo = convertToLxMusicInfo(data); // 转换音质 const lxQuality: LxQuality = QUALITY_TO_LX[quality || 'higher'] || '320k'; // 获取音乐 URL const rawUrl = await runner.getMusicUrl(bestSource, lxMusicInfo, lxQuality); if (!rawUrl) { console.log('[LxMusicStrategy] 获取 URL 失败'); CacheManager.addFailedCache(id, this.name); return null; } console.log('[LxMusicStrategy] 脚本返回 URL:', rawUrl.substring(0, 80) + '...'); // 解析可能是 API 端点的 URL const resolvedUrl = await resolveAudioUrl(rawUrl); if (!resolvedUrl) { console.log('[LxMusicStrategy] URL 解析失败'); CacheManager.addFailedCache(id, this.name); return null; } console.log('[LxMusicStrategy] 最终音频 URL:', resolvedUrl.substring(0, 80) + '...'); return { data: { code: 200, message: 'success', data: { url: resolvedUrl, source: `lx-${bestSource}`, quality: lxQuality } } }; } catch (error) { console.error('[LxMusicStrategy] 解析失败:', error); CacheManager.addFailedCache(id, this.name); return null; } } } ================================================ FILE: src/renderer/api/music.ts ================================================ import { musicDB } from '@/hooks/MusicHook'; import { useSettingsStore, useUserStore } from '@/store'; import type { ILyric } from '@/types/lyric'; import type { SongResult } from '@/types/music'; import request from '@/utils/request'; import { MusicParser, type MusicParseResult } from './musicParser'; const { addData, getData, deleteData } = musicDB; // 获取音乐音质详情 export const getMusicQualityDetail = (id: number) => { return request.get('/song/music/detail', { params: { id } }); }; // 根据音乐Id获取音乐播放URl export const getMusicUrl = async (id: number, isDownloaded: boolean = false) => { const userStore = useUserStore(); const settingStore = useSettingsStore(); // 判断是否登录 try { if (userStore.user && isDownloaded && userStore.user.vipType !== 0) { const url = '/song/download/url/v1'; const res = await request.get(url, { params: { id, level: settingStore.setData.musicQuality || 'higher', encodeType: settingStore.setData.musicQuality == 'lossless' ? 'aac' : 'flac', // level为lossless时,encodeType=flac时网易云会返回hires音质,encodeType=aac时网易云会返回lossless音质 cookie: `${localStorage.getItem('token')} os=pc;` } }); if (res.data.data.url) { return { data: { data: [{ ...res.data.data }] } }; } } } catch (error) { console.error('error', error); } return await request.get('/song/url/v1', { params: { id, level: settingStore.setData.musicQuality || 'higher', encodeType: settingStore.setData.musicQuality == 'lossless' ? 'aac' : 'flac' } }); }; // 获取歌曲详情 export const getMusicDetail = (ids: Array) => { return request.get('/song/detail', { params: { ids: ids.join(',') } }); }; // 根据音乐Id获取音乐歌词 export const getMusicLrc = async (id: number) => { const TEN_DAYS_MS = 10 * 24 * 60 * 60 * 1000; // 10天的毫秒数 try { // 尝试获取缓存的歌词 const cachedLyric = await getData('music_lyric', id); if (cachedLyric?.createTime && Date.now() - cachedLyric.createTime < TEN_DAYS_MS) { return { ...cachedLyric }; } // 获取新的歌词数据 const res = await request.get('/lyric/new', { params: { id } }); // 只有在成功获取新数据后才删除旧缓存并添加新缓存 if (res?.data) { if (cachedLyric) { await deleteData('music_lyric', id); } addData('music_lyric', { id, data: res.data, createTime: Date.now() }); } return res; } catch (error) { console.error('获取歌词失败:', error); throw error; // 向上抛出错误,让调用者处理 } }; /** * 获取解析后的音乐URL * @param id 歌曲ID * @param data 歌曲数据 * @returns 解析结果 */ export const getParsingMusicUrl = async ( id: number, data: SongResult ): Promise => { return await MusicParser.parseMusic(id, data); }; // 收藏歌曲 export const likeSong = (id: number, like: boolean = true) => { return request.get('/like', { params: { id, like } }); }; // 将每日推荐中的歌曲标记为不感兴趣,并获取一首新歌 export const dislikeRecommendedSong = (id: number | string) => { return request.get('/recommend/songs/dislike', { params: { id } }); }; // 获取用户喜欢的音乐列表 export const getLikedList = (uid: number) => { return request.get('/likelist', { params: { uid, noLogin: true } }); }; // 创建歌单 export const createPlaylist = (params: { name: string; privacy: number }) => { return request.post('/playlist/create', params); }; // 添加或删除歌单歌曲 export const updatePlaylistTracks = (params: { op: 'add' | 'del'; pid: number; tracks: string; }) => { return request.post('/playlist/tracks', params); }; /** * 根据类型获取列表数据 * @param type 列表类型 album/playlist * @param id 列表ID */ export function getMusicListByType(type: string, id: string) { if (type === 'album') { return getAlbumDetail(id); } else if (type === 'playlist') { return getPlaylistDetail(id); } return Promise.reject(new Error('未知列表类型')); } /** * 获取专辑详情 * @param id 专辑ID */ export function getAlbumDetail(id: string) { return request({ url: '/album', method: 'get', params: { id } }); } /** * 获取歌单详情 * @param id 歌单ID */ export function getPlaylistDetail(id: string) { return request({ url: '/playlist/detail', method: 'get', params: { id } }); } export function subscribePlaylist(params: { t: number; id: number }) { return request({ url: '/playlist/subscribe', method: 'post', params }); } /** * 收藏/取消收藏专辑 * @param params t: 1 收藏, 2 取消收藏; id: 专辑id */ export function subscribeAlbum(params: { t: number; id: number }) { return request({ url: '/album/sub', method: 'post', params }); } /** * 获取历史日推可用日期列表 */ export function getHistoryRecommendDates() { return request({ url: '/history/recommend/songs', method: 'get' }); } /** * 获取历史日推详情数据 * @param date 日期,格式:YYYY-MM-DD */ export function getHistoryRecommendSongs(date: string) { return request({ url: '/history/recommend/songs/detail', method: 'get', params: { date } }); } /** * 心动模式/智能播放 * @param params id: 歌曲id, pid: 歌单id, sid: 要开始播放的歌曲id(可选) */ export function getIntelligenceList(params: { id: number; pid: number; sid?: number }) { return request({ url: '/playmode/intelligence/list', method: 'get', params }); } ================================================ FILE: src/renderer/api/musicParser.ts ================================================ import { cloneDeep } from 'lodash'; import { musicDB } from '@/hooks/MusicHook'; import { SongSourceConfigManager } from '@/services/SongSourceConfigManager'; import { useSettingsStore } from '@/store'; import type { SongResult } from '@/types/music'; import { isElectron } from '@/utils'; import requestMusic from '@/utils/request_music'; import { searchAndGetBilibiliAudioUrl } from './bilibili'; import type { ParsedMusicResult } from './gdmusic'; import { parseFromGDMusic } from './gdmusic'; import { LxMusicStrategy } from './lxMusicStrategy'; import { parseFromCustomApi } from './parseFromCustomApi'; const { saveData, getData, deleteData } = musicDB; /** * 音乐解析结果接口 */ export interface MusicParseResult { data: { code: number; message: string; data?: { url: string; [key: string]: any; }; }; } /** * 缓存配置 */ const CACHE_CONFIG = { // 音乐URL缓存时间:30分钟 MUSIC_URL_CACHE_TIME: 30 * 60 * 1000, // 失败缓存时间:1分钟(减少到 1 分钟以便更快恢复) FAILED_CACHE_TIME: 1 * 60 * 1000, // 重试配置 MAX_RETRY_COUNT: 2, RETRY_DELAY: 1000 }; /** * 内存失败缓存(替代 IndexedDB,更轻量且应用重启后自动失效) */ const failedCacheMap = new Map(); /** * 缓存管理器 */ export class CacheManager { /** * 获取缓存的音乐URL */ static async getCachedMusicUrl( id: number, musicSources?: string[] ): Promise { try { const cached = await getData('music_url_cache', id); if ( cached?.createTime && Date.now() - cached.createTime < CACHE_CONFIG.MUSIC_URL_CACHE_TIME ) { // 检查缓存的音源配置是否与当前配置一致 const cachedSources = cached.musicSources || []; const currentSources = musicSources || []; // 如果音源配置不一致,清除缓存 if (JSON.stringify(cachedSources.sort()) !== JSON.stringify(currentSources.sort())) { console.log(`音源配置已变更,清除歌曲 ${id} 的缓存`); await deleteData('music_url_cache', id); return null; } console.log(`使用缓存的音乐URL: ${id}`); return cached.data; } // 清理过期缓存 if (cached) { await deleteData('music_url_cache', id); } } catch (error) { console.warn('获取缓存失败:', error); } return null; } /** * 缓存音乐URL */ static async setCachedMusicUrl( id: number, result: MusicParseResult, musicSources?: string[] ): Promise { try { // 深度克隆数据,确保可以被 IndexedDB 存储 await saveData('music_url_cache', { id, data: cloneDeep(result), musicSources: cloneDeep(musicSources || []), createTime: Date.now() }); console.log(`缓存音乐URL成功: ${id}`); } catch (error) { console.error('缓存音乐URL失败:', error); } } /** * 检查是否在失败缓存期内(使用内存缓存) */ static isInFailedCache(id: number, strategyName: string): boolean { const cacheKey = `${id}_${strategyName}`; const cachedTime = failedCacheMap.get(cacheKey); if (cachedTime && Date.now() - cachedTime < CACHE_CONFIG.FAILED_CACHE_TIME) { console.log(`策略 ${strategyName} 在失败缓存期内,跳过`); return true; } // 清理过期缓存 if (cachedTime) { failedCacheMap.delete(cacheKey); } return false; } /** * 添加失败缓存(使用内存缓存) */ static addFailedCache(id: number, strategyName: string): void { const cacheKey = `${id}_${strategyName}`; failedCacheMap.set(cacheKey, Date.now()); console.log( `添加失败缓存成功: ${strategyName} (缓存时间: ${CACHE_CONFIG.FAILED_CACHE_TIME / 1000}秒)` ); } /** * 清除指定歌曲的失败缓存 */ static clearFailedCache(id: number): void { const keysToDelete: string[] = []; failedCacheMap.forEach((_, key) => { if (key.startsWith(`${id}_`)) { keysToDelete.push(key); } }); keysToDelete.forEach((key) => failedCacheMap.delete(key)); if (keysToDelete.length > 0) { console.log(`清除歌曲 ${id} 的失败缓存: ${keysToDelete.length} 项`); } } /** * 清除指定歌曲的所有缓存 */ static async clearMusicCache(id: number): Promise { try { // 清除URL缓存 await deleteData('music_url_cache', id); console.log(`清除歌曲 ${id} 的URL缓存`); // 清除失败缓存 - 需要遍历所有策略 const strategies = ['custom', 'bilibili', 'gdmusic', 'unblockMusic']; for (const strategy of strategies) { const cacheKey = `${id}_${strategy}`; try { await deleteData('music_failed_cache', cacheKey); } catch { // 忽略删除不存在缓存的错误 } } console.log(`清除歌曲 ${id} 的失败缓存`); } catch (error) { console.error('清除缓存失败:', error); } } } /** * 重试工具 */ class RetryHelper { /** * 带重试的异步执行 */ static async withRetry( fn: () => Promise, maxRetries = CACHE_CONFIG.MAX_RETRY_COUNT, delay = CACHE_CONFIG.RETRY_DELAY ): Promise { let lastError: Error; for (let i = 0; i <= maxRetries; i++) { try { return await fn(); } catch (error) { lastError = error as Error; if (i < maxRetries) { console.log(`重试第 ${i + 1} 次,延迟 ${delay}ms`); await new Promise((resolve) => setTimeout(resolve, delay)); delay *= 2; // 指数退避 } } } throw lastError!; } } /** * 从Bilibili获取音频URL * @param data 歌曲数据 * @returns 解析结果 */ const getBilibiliAudio = async (data: SongResult) => { const songName = data?.name || ''; const artistName = Array.isArray(data?.ar) && data.ar.length > 0 && data.ar[0]?.name ? data.ar[0].name : ''; const albumName = data?.al && typeof data.al === 'object' && data.al?.name ? data.al.name : ''; const searchQuery = [songName, artistName, albumName].filter(Boolean).join(' ').trim(); console.log('开始搜索bilibili音频:', searchQuery); const url = await searchAndGetBilibiliAudioUrl(searchQuery); return { data: { code: 200, message: 'success', data: { url } } }; }; /** * 从GD音乐台获取音频URL * @param id 歌曲ID * @param data 歌曲数据 * @returns 解析结果,失败时返回null */ const getGDMusicAudio = async (id: number, data: SongResult): Promise => { try { const gdResult = await parseFromGDMusic(id, data, '999'); if (gdResult) { return gdResult; } } catch (error) { console.error('GD音乐台解析失败:', error); } return null; }; /** * 使用unblockMusic解析音频URL * @param id 歌曲ID * @param data 歌曲数据 * @param sources 音源列表 * @returns 解析结果 */ const getUnblockMusicAudio = (id: number, data: SongResult, sources: any[]) => { const filteredSources = sources.filter((source) => source !== 'gdmusic'); console.log(`使用unblockMusic解析,音源:`, filteredSources); return window.api.unblockMusic(id, cloneDeep(data), cloneDeep(filteredSources)); }; /** * 统一的解析结果适配器 */ const adaptParseResult = (result: any): MusicParseResult | null => { if (!result) return null; // 如果已经是标准格式 if (result.data?.code !== undefined && result.data?.message !== undefined) { return result; } // 适配GD音乐台的返回格式 if (result.data?.data?.url) { return { data: { code: 200, message: 'success', data: { url: result.data.data.url, ...result.data.data } } }; } // 适配其他格式 if (result.url) { return { data: { code: 200, message: 'success', data: { url: result.url, ...result } } }; } return null; }; /** * 音源解析策略接口 */ interface MusicSourceStrategy { name: string; priority: number; canHandle: (sources: string[], settingsStore?: any) => boolean; parse: ( id: number, data: SongResult, quality?: string, sources?: string[] ) => Promise; } /** * 自定义API解析策略 */ class CustomApiStrategy implements MusicSourceStrategy { name = 'custom'; priority = 1; canHandle(sources: string[], settingsStore?: any): boolean { return sources.includes('custom') && Boolean(settingsStore?.setData?.customApiPlugin); } async parse(id: number, data: SongResult, quality = 'higher'): Promise { // 检查失败缓存 if (CacheManager.isInFailedCache(id, this.name)) { return null; } try { console.log('尝试使用自定义API解析...'); const result = await RetryHelper.withRetry(async () => { return await parseFromCustomApi(id, data, quality); }); const adaptedResult = adaptParseResult(result); if (adaptedResult?.data?.data?.url) { console.log('自定义API解析成功'); return adaptedResult; } // 解析失败,添加失败缓存 CacheManager.addFailedCache(id, this.name); return null; } catch (error) { console.error('自定义API解析失败:', error); CacheManager.addFailedCache(id, this.name); return null; } } } /** * Bilibili解析策略 */ class BilibiliStrategy implements MusicSourceStrategy { name = 'bilibili'; priority = 2; canHandle(sources: string[]): boolean { return sources.includes('bilibili'); } async parse(id: number, data: SongResult): Promise { // 检查失败缓存 if (CacheManager.isInFailedCache(id, this.name)) { return null; } try { console.log('尝试使用Bilibili解析...'); const result = await RetryHelper.withRetry(async () => { return await getBilibiliAudio(data); }); const adaptedResult = adaptParseResult(result); if (adaptedResult?.data?.data?.url) { console.log('Bilibili解析成功'); return adaptedResult; } // 解析失败,添加失败缓存 CacheManager.addFailedCache(id, this.name); return null; } catch (error) { console.error('Bilibili解析失败:', error); CacheManager.addFailedCache(id, this.name); return null; } } } /** * GD音乐台解析策略 */ class GDMusicStrategy implements MusicSourceStrategy { name = 'gdmusic'; priority = 3; canHandle(sources: string[]): boolean { return sources.includes('gdmusic'); } async parse(id: number, data: SongResult): Promise { // 检查失败缓存 if (CacheManager.isInFailedCache(id, this.name)) { return null; } try { console.log('尝试使用GD音乐台解析...'); const result = await RetryHelper.withRetry(async () => { return await getGDMusicAudio(id, data); }); const adaptedResult = adaptParseResult(result); if (adaptedResult?.data?.data?.url) { console.log('GD音乐台解析成功'); return adaptedResult; } // 解析失败,添加失败缓存 CacheManager.addFailedCache(id, this.name); return null; } catch (error) { console.error('GD音乐台解析失败:', error); CacheManager.addFailedCache(id, this.name); return null; } } } /** * UnblockMusic解析策略 */ class UnblockMusicStrategy implements MusicSourceStrategy { name = 'unblockMusic'; priority = 4; canHandle(sources: string[]): boolean { const unblockSources = sources.filter( (source) => !['custom', 'bilibili', 'gdmusic'].includes(source) ); return unblockSources.length > 0; } async parse( id: number, data: SongResult, _quality?: string, sources?: string[] ): Promise { // 检查失败缓存 if (CacheManager.isInFailedCache(id, this.name)) { return null; } try { const unblockSources = (sources || []).filter( (source) => !['custom', 'bilibili', 'gdmusic'].includes(source) ); console.log('尝试使用UnblockMusic解析:', unblockSources); const result = await RetryHelper.withRetry(async () => { return await getUnblockMusicAudio(id, data, unblockSources); }); const adaptedResult = adaptParseResult(result); if (adaptedResult?.data?.data?.url) { console.log('UnblockMusic解析成功'); return adaptedResult; } // 解析失败,添加失败缓存 CacheManager.addFailedCache(id, this.name); return null; } catch (error) { console.error('UnblockMusic解析失败:', error); CacheManager.addFailedCache(id, this.name); return null; } } } /** * 音源策略工厂 */ class MusicSourceStrategyFactory { private static strategies: MusicSourceStrategy[] = [ new LxMusicStrategy(), new CustomApiStrategy(), new BilibiliStrategy(), new GDMusicStrategy(), new UnblockMusicStrategy() ]; /** * 获取可用的解析策略 * @param sources 音源列表 * @param settingsStore 设置存储 * @returns 排序后的可用策略列表 */ static getAvailableStrategies(sources: string[], settingsStore?: any): MusicSourceStrategy[] { return this.strategies .filter((strategy) => strategy.canHandle(sources, settingsStore)) .sort((a, b) => a.priority - b.priority); } } /** * 获取音源配置 * @param id 歌曲ID * @param settingsStore 设置存储 * @returns 音源列表和音质设置 */ const getMusicConfig = (id: number, settingsStore?: any) => { let musicSources: string[] = []; let quality = 'higher'; try { // 尝试获取歌曲自定义音源(使用 SongSourceConfigManager) const songConfig = SongSourceConfigManager.getConfig(id); if (songConfig && songConfig.sources.length > 0) { musicSources = songConfig.sources; console.log(`使用歌曲 ${id} 自定义音源:`, musicSources); } // 如果没有自定义音源,使用全局设置 if (musicSources.length === 0) { musicSources = settingsStore?.setData?.enabledMusicSources || []; console.log('使用全局音源设置:', musicSources); } quality = settingsStore?.setData?.musicQuality || 'higher'; } catch (error) { console.error('读取音源配置失败,使用默认配置:', error); musicSources = []; quality = 'higher'; } return { musicSources, quality }; }; /** * 音乐解析器主类 */ export class MusicParser { /** * 解析音乐URL * @param id 歌曲ID * @param data 歌曲数据 * @returns 解析结果 */ static async parseMusic(id: number, data: SongResult): Promise { const startTime = performance.now(); try { // 非Electron环境直接使用API请求 if (!isElectron) { console.log('非Electron环境,使用API请求'); return await requestMusic.get('/music', { params: { id } }); } // 获取设置存储 let settingsStore: any; try { settingsStore = useSettingsStore(); } catch (error) { console.error('无法获取设置存储,使用后备方案:', error); return await requestMusic.get('/music', { params: { id } }); } // 获取音源配置 const { musicSources, quality } = getMusicConfig(id, settingsStore); // 检查缓存(传入音源配置用于验证缓存有效性) console.log(`检查歌曲 ${id} 的缓存...`); const cachedResult = await CacheManager.getCachedMusicUrl(id, musicSources); if (cachedResult) { const endTime = performance.now(); console.log(`✅ 命中缓存,歌曲 ${id},耗时: ${(endTime - startTime).toFixed(2)}ms`); return cachedResult; } console.log(`❌ 未命中缓存,歌曲 ${id},开始解析...`); // 检查音乐解析功能是否启用 if (!settingsStore?.setData?.enableMusicUnblock) { console.log('音乐解析功能已禁用'); return { data: { code: 404, message: '音乐解析功能已禁用', data: undefined } }; } if (musicSources.length === 0) { console.warn('没有配置可用的音源,使用后备方案'); return await requestMusic.get('/music', { params: { id } }); } // 获取可用的解析策略 const availableStrategies = MusicSourceStrategyFactory.getAvailableStrategies( musicSources, settingsStore ); if (availableStrategies.length === 0) { console.warn('没有可用的解析策略,使用后备方案'); return await requestMusic.get('/music', { params: { id } }); } console.log( `开始解析歌曲 ${id},可用策略:`, availableStrategies.map((s) => s.name) ); // 按优先级依次尝试解析策略 for (const strategy of availableStrategies) { try { const result = await strategy.parse(id, data, quality, musicSources); if (result?.data?.data?.url) { const endTime = performance.now(); console.log( `解析成功,使用策略: ${strategy.name},耗时: ${(endTime - startTime).toFixed(2)}ms` ); // 缓存成功结果(包含音源配置) await CacheManager.setCachedMusicUrl(id, result, musicSources); return result; } console.log(`策略 ${strategy.name} 解析失败,继续尝试下一个策略`); } catch (error) { console.error(`策略 ${strategy.name} 解析异常:`, error); // 继续尝试下一个策略 } } console.warn('所有解析策略都失败了,使用后备方案'); } catch (error) { console.error('MusicParser.parseMusic 执行异常,使用后备方案:', error); } // 后备方案:使用API请求 try { console.log('使用后备方案:API请求'); const result = await requestMusic.get('/music', { params: { id } }); // 如果后备方案成功,也进行缓存 if (result?.data?.data?.url) { console.log('后备方案成功,缓存结果'); await CacheManager.setCachedMusicUrl(id, result, []); } return result; } catch (apiError) { console.error('API请求也失败了:', apiError); const endTime = performance.now(); console.log(`总耗时: ${(endTime - startTime).toFixed(2)}ms`); return { data: { code: 500, message: '所有解析方式都失败了', data: undefined } }; } } } ================================================ FILE: src/renderer/api/mv.ts ================================================ import { IData } from '@/types'; import { IMvUrlData } from '@/types/mv'; import request from '@/utils/request'; interface MvParams { limit?: number; offset?: number; area?: string; } // 获取 mv 排行 export const getTopMv = (params: MvParams) => { return request({ url: '/mv/all', method: 'get', params }); }; // 获取所有mv export const getAllMv = (params: MvParams) => { return request({ url: '/mv/all', method: 'get', params }); }; // 获取 mv 数据 export const getMvDetail = (mvid: string) => { return request.get('/mv/detail', { params: { mvid } }); }; // 获取 mv 地址 export const getMvUrl = (id: Number) => { return request.get>('/mv/url', { params: { id } }); }; ================================================ FILE: src/renderer/api/parseFromCustomApi.ts ================================================ import axios from 'axios'; import { get } from 'lodash'; import { useSettingsStore } from '@/store'; import type { ParsedMusicResult } from './gdmusic'; /** * 定义自定义API JSON插件的结构 */ interface CustomApiPlugin { name: string; apiUrl: string; method?: 'GET' | 'POST'; params: Record; qualityMapping?: Record; responseUrlPath: string; } /** * 从用户导入的自定义API JSON配置中解析音乐URL */ export const parseFromCustomApi = async ( id: number, _songData: any, quality: string = 'higher', timeout: number = 10000 ): Promise => { const settingsStore = useSettingsStore(); const pluginString = settingsStore.setData.customApiPlugin; if (!pluginString) { return null; } let plugin: CustomApiPlugin; try { plugin = JSON.parse(pluginString); if (!plugin.apiUrl || !plugin.params || !plugin.responseUrlPath) { console.error('自定义API:JSON配置文件格式不正确。'); return null; } } catch (error) { console.error('自定义API:解析JSON配置文件失败。', error); return null; } console.log(`自定义API:正在使用插件 [${plugin.name}] 进行解析...`); try { // 1. 准备请求参数,替换占位符 const finalParams: Record = {}; for (const [key, value] of Object.entries(plugin.params)) { if (value === '{songId}') { finalParams[key] = String(id); } else if (value === '{quality}') { // 使用 qualityMapping (如果存在) 进行音质翻译,否则直接使用原quality finalParams[key] = plugin.qualityMapping?.[quality] ?? quality; } else { // 固定值参数 finalParams[key] = value; } } // 2. 判断请求方法,默认为GET const method = plugin.method?.toUpperCase() === 'POST' ? 'POST' : 'GET'; let response; // 3. 根据方法发送不同的请求 if (method === 'POST') { console.log('自定义API:发送 POST 请求到:', plugin.apiUrl, '参数:', finalParams); response = await axios.post(plugin.apiUrl, finalParams, { timeout }); } else { // 默认为 GET const finalUrl = `${plugin.apiUrl}?${new URLSearchParams(finalParams).toString()}`; console.log('自定义API:发送 GET 请求到:', finalUrl); response = await axios.get(finalUrl, { timeout }); } // 4. 使用 lodash.get 安全地从响应数据中提取URL const musicUrl = get(response.data, plugin.responseUrlPath); if (musicUrl && typeof musicUrl === 'string') { console.log('自定义API:成功获取URL!'); // 5. 组装成应用所需的标准格式并返回 return { data: { data: { url: musicUrl, br: parseInt(quality) * 1000, size: 0, md5: '', platform: plugin.name.toLowerCase().replace(/\s/g, ''), gain: 0 }, params: { id, type: 'song' } } }; } else { console.error('自定义API:根据路径未能从响应中找到URL:', plugin.responseUrlPath); return null; } } catch (error) { console.error(`自定义API [${plugin.name}] 执行失败:`, error); return null; } }; ================================================ FILE: src/renderer/api/playlist.ts ================================================ import request from '@/utils/request'; /** * 歌单导入 - 元数据/文字/链接导入 * @param params 导入参数 */ export function importPlaylist(params: { local?: string; text?: string; link?: string; importStarPlaylist?: boolean; playlistName?: string; }) { return request.post('/playlist/import/name/task/create', params); } /** * 歌单导入 - 任务状态 * @param id 任务ID */ export function getImportTaskStatus(id: string | number) { return request({ url: '/playlist/import/task/status', method: 'get', params: { id } }); } ================================================ FILE: src/renderer/api/search.ts ================================================ import { isElectron } from '@/utils'; import request from '@/utils/request'; interface IParams { keywords: string; type: number; limit?: number; offset?: number; } // 搜索内容 export const getSearch = (params: IParams) => { return request.get('/cloudsearch', { params }); }; /** * 搜索建议接口返回的数据结构 */ interface Suggestion { keyword: string; } interface KugouSuggestionResponse { data: Suggestion[]; } // 网易云搜索建议返回的数据结构(部分字段) interface NeteaseSuggestResult { result?: { songs?: Array<{ name: string }>; artists?: Array<{ name: string }>; albums?: Array<{ name: string }>; }; code?: number; } /** * 从酷狗获取搜索建议 * @param keyword 搜索关键词 */ export const getSearchSuggestions = async (keyword: string) => { console.log('[API] getSearchSuggestions: 开始执行'); if (!keyword || !keyword.trim()) { return Promise.resolve([]); } console.log(`[API] getSearchSuggestions: 准备请求,关键词: "${keyword}"`); try { let responseData: KugouSuggestionResponse; if (isElectron) { console.log('[API] Running in Electron, using IPC proxy.'); responseData = await window.api.getSearchSuggestions(keyword); } else { // 非 Electron 环境下,使用网易云接口 const res = await request.get('/search/suggest', { params: { keywords: keyword } }); const result = res?.data?.result || {}; const names: string[] = []; if (Array.isArray(result.songs)) names.push(...result.songs.map((s) => s.name)); if (Array.isArray(result.artists)) names.push(...result.artists.map((a) => a.name)); if (Array.isArray(result.albums)) names.push(...result.albums.map((al) => al.name)); // 去重并截取前10个 const unique = Array.from(new Set(names)).slice(0, 10); console.log('[API] getSearchSuggestions: 网易云建议解析成功:', unique); return unique; } if (responseData && Array.isArray(responseData.data)) { const suggestions = responseData.data.map((item) => item.keyword).slice(0, 10); console.log('[API] getSearchSuggestions: 成功解析建议:', suggestions); return suggestions; } console.warn('[API] getSearchSuggestions: 响应数据格式不正确,返回空数组。'); return []; } catch (error) { console.error('[API] getSearchSuggestions: 请求失败,错误信息:', error); return []; } }; ================================================ FILE: src/renderer/api/user.ts ================================================ import type { IUserDetail, IUserFollow } from '@/types/user'; import request from '@/utils/request'; // /user/detail export function getUserDetail(uid: number) { return request.get('/user/detail', { params: { uid } }); } // /user/playlist export function getUserPlaylist(uid: number, limit: number = 30, offset: number = 0) { return request.get('/user/playlist', { params: { uid, limit, offset } }); } // 播放历史 // /user/record?uid=32953014&type=1 export function getUserRecord(uid: number, type: number = 0) { return request.get('/user/record', { params: { uid, type }, noRetry: true } as any); } // 最近播放-歌曲 // /record/recent/song export function getRecentSongs(limit: number = 100) { return request.get('/record/recent/song', { params: { limit }, noRetry: true } as any); } // 最近播放-歌单 // /record/recent/playlist export function getRecentPlaylists(limit: number = 100) { return request.get('/record/recent/playlist', { params: { limit }, noRetry: true } as any); } // 最近播放-专辑 // /record/recent/album export function getRecentAlbums(limit: number = 100) { return request.get('/record/recent/album', { params: { limit }, noRetry: true } as any); } // 获取用户关注列表 // /user/follows?uid=32953014 export function getUserFollows(uid: number, limit: number = 30, offset: number = 0) { return request.get('/user/follows', { params: { uid, limit, offset } }); } // 获取用户粉丝列表 export function getUserFollowers(uid: number, limit: number = 30, offset: number = 0) { return request.post('/user/followeds', { uid, limit, offset }); } // 获取用户账号信息 export const getUserAccount = () => { return request({ url: '/user/account', method: 'get' }); }; // 获取用户详情 export const getUserDetailInfo = (params: { uid: string | number }) => { return request({ url: '/user/detail', method: 'get', params }); }; // 获取用户关注列表 export const getUserFollowsInfo = (params: { uid: string | number; limit?: number; offset?: number; }) => { return request<{ follow: IUserFollow[]; more: boolean; }>({ url: '/user/follows', method: 'get', params }); }; // 获取用户歌单 export const getUserPlaylists = (params: { uid: string | number }) => { return request({ url: '/user/playlist', method: 'get', params }); }; // 获取已收藏专辑列表 export const getUserAlbumSublist = (params?: { limit?: number; offset?: number }) => { return request({ url: '/album/sublist', method: 'get', params: { limit: params?.limit || 25, offset: params?.offset || 0 } }); }; ================================================ FILE: src/renderer/assets/css/base.css ================================================ body { /* background-color: #000; */ overflow: hidden; } .n-popover:has(.music-play) { border-radius: 1.5rem !important; } .n-popover { border-radius: 0.5rem !important; overflow: hidden !important; } .n-popover:has(.transparent-popover) { background-color: transparent !important; padding: 0 !important; } .settings-slider .n-slider-mark { font-size: 10px !important; } ================================================ FILE: src/renderer/assets/icon/iconfont.css ================================================ @font-face { font-family: 'iconfont'; /* Project id 2685283 */ src: url('iconfont.woff2?t=1703643214551') format('woff2'), url('iconfont.woff?t=1703643214551') format('woff'), url('iconfont.ttf?t=1703643214551') format('truetype'); } .iconfont { font-family: 'iconfont' !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .icon-list:before { content: '\e603'; } .icon-maxsize:before { content: '\e692'; } .icon-close:before { content: '\e616'; } .icon-minisize:before { content: '\e602'; } .icon-shuaxin:before { content: '\e627'; } .icon-icon_error:before { content: '\e615'; } .icon-a-3User:before { content: '\e601'; } .icon-Chat:before { content: '\e605'; } .icon-Category:before { content: '\e606'; } .icon-Document:before { content: '\e607'; } .icon-Heart:before { content: '\e608'; } .icon-Hide:before { content: '\e609'; } .icon-Home:before { content: '\e60a'; } .icon-a-Image2:before { content: '\e60b'; } .icon-Profile:before { content: '\e60c'; } .icon-Search:before { content: '\e60d'; } .icon-Paper:before { content: '\e60e'; } .icon-Play:before { content: '\e60f'; } .icon-Setting:before { content: '\e610'; } .icon-a-TicketStar:before { content: '\e611'; } .icon-a-VolumeOff:before { content: '\e612'; } .icon-a-VolumeUp:before { content: '\e613'; } .icon-a-VolumeDown:before { content: '\e614'; } .icon-stop:before { content: '\e600'; } .icon-next:before { content: '\e6a9'; } .icon-prev:before { content: '\e6ac'; } .icon-play:before { content: '\e6aa'; } .icon-xiasanjiaoxing:before { content: '\e642'; } .icon-videofill:before { content: '\e7c7'; } .icon-favorfill:before { content: '\e64b'; } .icon-favor:before { content: '\e64c'; } .icon-loading:before { content: '\e64f'; } .icon-search:before { content: '\e65c'; } .icon-likefill:before { content: '\e668'; } .icon-like:before { content: '\e669'; } .icon-notificationfill:before { content: '\e66a'; } .icon-notification:before { content: '\e66b'; } .icon-evaluate:before { content: '\e672'; } .icon-homefill:before { content: '\e6bb'; } .icon-link:before { content: '\e6bf'; } .icon-roundaddfill:before { content: '\e6d8'; } .icon-roundadd:before { content: '\e6d9'; } .icon-add:before { content: '\e6da'; } .icon-appreciatefill:before { content: '\e6e3'; } .icon-forwardfill:before { content: '\e6ea'; } .icon-voicefill:before { content: '\e6f0'; } .icon-wefill:before { content: '\e6f4'; } .icon-keyboard:before { content: '\e71b'; } .icon-picfill:before { content: '\e72c'; } .icon-markfill:before { content: '\e730'; } .icon-presentfill:before { content: '\e732'; } .icon-peoplefill:before { content: '\e735'; } .icon-read:before { content: '\e742'; } .icon-backwardfill:before { content: '\e74d'; } .icon-playfill:before { content: '\e74f'; } .icon-all:before { content: '\e755'; } .icon-hotfill:before { content: '\e757'; } .icon-recordfill:before { content: '\e7a4'; } .icon-full:before { content: '\e7bc'; } .icon-favor_fill_light:before { content: '\e7ec'; } .icon-round_favor_fill:before { content: '\e80a'; } .icon-round_location_fill:before { content: '\e80b'; } .icon-round_like_fill:before { content: '\e80c'; } .icon-round_people_fill:before { content: '\e80d'; } .icon-round_skin_fill:before { content: '\e80e'; } .icon-broadcast_fill:before { content: '\e81d'; } .icon-card_fill:before { content: '\e81f'; } ================================================ FILE: src/renderer/assets/icon/iconfont.js ================================================ ((window._iconfont_svg_string_2685283 = ''), (function (a) { var c = (c = document.getElementsByTagName('script'))[c.length - 1]; const l = c.getAttribute('data-injectcss'); var c = c.getAttribute('data-disable-injectsvg'); if (!c) { let o; let i; var t; var h; var s; const d = function (c, l) { l.parentNode.insertBefore(c, l); }; if (l && !a.__iconfont__svg__cssinject__) { a.__iconfont__svg__cssinject__ = !0; try { document.write( '' ); } catch (c) { console && console.log(c); } } ((o = function () { let c; let l = document.createElement('div'); ((l.innerHTML = a._iconfont_svg_string_2685283), (l = l.getElementsByTagName('svg')[0]) && (l.setAttribute('aria-hidden', 'true'), (l.style.position = 'absolute'), (l.style.width = 0), (l.style.height = 0), (l.style.overflow = 'hidden'), (l = l), (c = document.body).firstChild ? d(l, c.firstChild) : c.appendChild(l))); }), document.addEventListener ? ~['complete', 'loaded', 'interactive'].indexOf(document.readyState) ? setTimeout(o, 0) : ((i = function () { (document.removeEventListener('DOMContentLoaded', i, !1), o()); }), document.addEventListener('DOMContentLoaded', i, !1)) : document.attachEvent && ((t = o), (h = a.document), (s = !1), e(), (h.onreadystatechange = function () { h.readyState == 'complete' && ((h.onreadystatechange = null), p()); }))); } function p() { s || ((s = !0), t()); } function e() { try { h.documentElement.doScroll('left'); } catch { return void setTimeout(e, 50); } p(); } })(window)); ================================================ FILE: src/renderer/assets/icon/iconfont.json ================================================ { "id": "2685283", "name": "music", "font_family": "iconfont", "css_prefix_text": "icon-", "description": "", "glyphs": [ { "icon_id": "1111849", "name": "list", "font_class": "list", "unicode": "e603", "unicode_decimal": 58883 }, { "icon_id": "1306794", "name": "maxsize", "font_class": "maxsize", "unicode": "e692", "unicode_decimal": 59026 }, { "icon_id": "4437591", "name": "close", "font_class": "close", "unicode": "e616", "unicode_decimal": 58902 }, { "icon_id": "5383753", "name": "minisize", "font_class": "minisize", "unicode": "e602", "unicode_decimal": 58882 }, { "icon_id": "13075017", "name": "刷新", "font_class": "shuaxin", "unicode": "e627", "unicode_decimal": 58919 }, { "icon_id": "24457556", "name": "icon_error", "font_class": "icon_error", "unicode": "e615", "unicode_decimal": 58901 }, { "icon_id": "24492642", "name": "3 User", "font_class": "a-3User", "unicode": "e601", "unicode_decimal": 58881 }, { "icon_id": "24492643", "name": "Chat", "font_class": "Chat", "unicode": "e605", "unicode_decimal": 58885 }, { "icon_id": "24492646", "name": "Category", "font_class": "Category", "unicode": "e606", "unicode_decimal": 58886 }, { "icon_id": "24492661", "name": "Document", "font_class": "Document", "unicode": "e607", "unicode_decimal": 58887 }, { "icon_id": "24492662", "name": "Heart", "font_class": "Heart", "unicode": "e608", "unicode_decimal": 58888 }, { "icon_id": "24492665", "name": "Hide", "font_class": "Hide", "unicode": "e609", "unicode_decimal": 58889 }, { "icon_id": "24492667", "name": "Home", "font_class": "Home", "unicode": "e60a", "unicode_decimal": 58890 }, { "icon_id": "24492678", "name": "Image 2", "font_class": "a-Image2", "unicode": "e60b", "unicode_decimal": 58891 }, { "icon_id": "24492684", "name": "Profile", "font_class": "Profile", "unicode": "e60c", "unicode_decimal": 58892 }, { "icon_id": "24492685", "name": "Search", "font_class": "Search", "unicode": "e60d", "unicode_decimal": 58893 }, { "icon_id": "24492687", "name": "Paper", "font_class": "Paper", "unicode": "e60e", "unicode_decimal": 58894 }, { "icon_id": "24492690", "name": "Play", "font_class": "Play", "unicode": "e60f", "unicode_decimal": 58895 }, { "icon_id": "24492698", "name": "Setting", "font_class": "Setting", "unicode": "e610", "unicode_decimal": 58896 }, { "icon_id": "24492708", "name": "Ticket Star", "font_class": "a-TicketStar", "unicode": "e611", "unicode_decimal": 58897 }, { "icon_id": "24492712", "name": "Volume Off", "font_class": "a-VolumeOff", "unicode": "e612", "unicode_decimal": 58898 }, { "icon_id": "24492713", "name": "Volume Up", "font_class": "a-VolumeUp", "unicode": "e613", "unicode_decimal": 58899 }, { "icon_id": "24492714", "name": "Volume Down", "font_class": "a-VolumeDown", "unicode": "e614", "unicode_decimal": 58900 }, { "icon_id": "18875422", "name": "暂停 停止 灰色", "font_class": "stop", "unicode": "e600", "unicode_decimal": 58880 }, { "icon_id": "15262786", "name": "1_music82", "font_class": "next", "unicode": "e6a9", "unicode_decimal": 59049 }, { "icon_id": "15262807", "name": "1_music83", "font_class": "prev", "unicode": "e6ac", "unicode_decimal": 59052 }, { "icon_id": "15262830", "name": "1_music81", "font_class": "play", "unicode": "e6aa", "unicode_decimal": 59050 }, { "icon_id": "15367", "name": "下三角形", "font_class": "xiasanjiaoxing", "unicode": "e642", "unicode_decimal": 58946 }, { "icon_id": "1096518", "name": "video_fill", "font_class": "videofill", "unicode": "e7c7", "unicode_decimal": 59335 }, { "icon_id": "29930", "name": "favor_fill", "font_class": "favorfill", "unicode": "e64b", "unicode_decimal": 58955 }, { "icon_id": "29931", "name": "favor", "font_class": "favor", "unicode": "e64c", "unicode_decimal": 58956 }, { "icon_id": "29934", "name": "loading", "font_class": "loading", "unicode": "e64f", "unicode_decimal": 58959 }, { "icon_id": "29947", "name": "search", "font_class": "search", "unicode": "e65c", "unicode_decimal": 58972 }, { "icon_id": "30417", "name": "like_fill", "font_class": "likefill", "unicode": "e668", "unicode_decimal": 58984 }, { "icon_id": "30418", "name": "like", "font_class": "like", "unicode": "e669", "unicode_decimal": 58985 }, { "icon_id": "30419", "name": "notification_fill", "font_class": "notificationfill", "unicode": "e66a", "unicode_decimal": 58986 }, { "icon_id": "30420", "name": "notification", "font_class": "notification", "unicode": "e66b", "unicode_decimal": 58987 }, { "icon_id": "30434", "name": "evaluate", "font_class": "evaluate", "unicode": "e672", "unicode_decimal": 58994 }, { "icon_id": "33519", "name": "home_fill", "font_class": "homefill", "unicode": "e6bb", "unicode_decimal": 59067 }, { "icon_id": "34922", "name": "link", "font_class": "link", "unicode": "e6bf", "unicode_decimal": 59071 }, { "icon_id": "38744", "name": "round_add_fill", "font_class": "roundaddfill", "unicode": "e6d8", "unicode_decimal": 59096 }, { "icon_id": "38746", "name": "round_add", "font_class": "roundadd", "unicode": "e6d9", "unicode_decimal": 59097 }, { "icon_id": "38747", "name": "add", "font_class": "add", "unicode": "e6da", "unicode_decimal": 59098 }, { "icon_id": "43903", "name": "appreciate_fill", "font_class": "appreciatefill", "unicode": "e6e3", "unicode_decimal": 59107 }, { "icon_id": "52506", "name": "forward_fill", "font_class": "forwardfill", "unicode": "e6ea", "unicode_decimal": 59114 }, { "icon_id": "55448", "name": "voice_fill", "font_class": "voicefill", "unicode": "e6f0", "unicode_decimal": 59120 }, { "icon_id": "61146", "name": "we_fill", "font_class": "wefill", "unicode": "e6f4", "unicode_decimal": 59124 }, { "icon_id": "90847", "name": "keyboard", "font_class": "keyboard", "unicode": "e71b", "unicode_decimal": 59163 }, { "icon_id": "127305", "name": "pic_fill", "font_class": "picfill", "unicode": "e72c", "unicode_decimal": 59180 }, { "icon_id": "143738", "name": "mark_fill", "font_class": "markfill", "unicode": "e730", "unicode_decimal": 59184 }, { "icon_id": "143740", "name": "present_fill", "font_class": "presentfill", "unicode": "e732", "unicode_decimal": 59186 }, { "icon_id": "158873", "name": "people_fill", "font_class": "peoplefill", "unicode": "e735", "unicode_decimal": 59189 }, { "icon_id": "176313", "name": "read", "font_class": "read", "unicode": "e742", "unicode_decimal": 59202 }, { "icon_id": "212324", "name": "backward_fill", "font_class": "backwardfill", "unicode": "e74d", "unicode_decimal": 59213 }, { "icon_id": "212328", "name": "play_fill", "font_class": "playfill", "unicode": "e74f", "unicode_decimal": 59215 }, { "icon_id": "240126", "name": "all", "font_class": "all", "unicode": "e755", "unicode_decimal": 59221 }, { "icon_id": "240128", "name": "hot_fill", "font_class": "hotfill", "unicode": "e757", "unicode_decimal": 59223 }, { "icon_id": "747747", "name": "record_fill", "font_class": "recordfill", "unicode": "e7a4", "unicode_decimal": 59300 }, { "icon_id": "1005712", "name": "full", "font_class": "full", "unicode": "e7bc", "unicode_decimal": 59324 }, { "icon_id": "1512759", "name": "favor_fill_light", "font_class": "favor_fill_light", "unicode": "e7ec", "unicode_decimal": 59372 }, { "icon_id": "4110741", "name": "round_favor_fill", "font_class": "round_favor_fill", "unicode": "e80a", "unicode_decimal": 59402 }, { "icon_id": "4110743", "name": "round_location_fill", "font_class": "round_location_fill", "unicode": "e80b", "unicode_decimal": 59403 }, { "icon_id": "4110745", "name": "round_like_fill", "font_class": "round_like_fill", "unicode": "e80c", "unicode_decimal": 59404 }, { "icon_id": "4110746", "name": "round_people_fill", "font_class": "round_people_fill", "unicode": "e80d", "unicode_decimal": 59405 }, { "icon_id": "4110750", "name": "round_skin_fill", "font_class": "round_skin_fill", "unicode": "e80e", "unicode_decimal": 59406 }, { "icon_id": "11778953", "name": "broadcast_fill", "font_class": "broadcast_fill", "unicode": "e81d", "unicode_decimal": 59421 }, { "icon_id": "12625085", "name": "card_fill", "font_class": "card_fill", "unicode": "e81f", "unicode_decimal": 59423 } ] } ================================================ FILE: src/renderer/components/Coffee.vue ================================================ ================================================ FILE: src/renderer/components/EQControl.vue ================================================ ================================================ FILE: src/renderer/components/LanguageSwitcher.vue ================================================ ================================================ FILE: src/renderer/components/MusicList.vue ================================================ ================================================ FILE: src/renderer/components/MvPlayer.vue ================================================ ================================================ FILE: src/renderer/components/ShortcutToast.vue ================================================ ================================================ FILE: src/renderer/components/TrafficWarningDrawer.vue ================================================ ================================================ FILE: src/renderer/components/common/AlbumItem.vue ================================================ ================================================ FILE: src/renderer/components/common/ArtistDrawer.vue ================================================ ================================================ FILE: src/renderer/components/common/BilibiliItem.vue ================================================ ================================================ FILE: src/renderer/components/common/DisclaimerModal.vue ================================================ ================================================ FILE: src/renderer/components/common/DonationList.vue ================================================ ================================================ FILE: src/renderer/components/common/DownloadDrawer.vue ================================================ ================================================ FILE: src/renderer/components/common/InstallAppModal.vue ================================================ ================================================ FILE: src/renderer/components/common/MobileUpdateModal.vue ================================================ ================================================ FILE: src/renderer/components/common/MusicListNavigator.ts ================================================ import { Router } from 'vue-router'; import { useMusicStore } from '@/store/modules/music'; /** * 导航到音乐列表页面的通用方法 * @param router Vue路由实例 * @param options 导航选项 */ export function navigateToMusicList( router: Router, options: { id?: string | number; type?: 'album' | 'playlist' | 'dailyRecommend' | string; name: string; songList: any[]; listInfo?: any; canRemove?: boolean; } ) { const musicStore = useMusicStore(); const { id, type, name, songList, listInfo, canRemove = false } = options; // 如果是每日推荐,不需要设置 musicStore,直接从 recommendStore 获取 if (type !== 'dailyRecommend') { musicStore.setCurrentMusicList(songList, name, listInfo, canRemove); } else { // 确保 musicStore 的数据被清空,避免显示旧的列表 musicStore.clearCurrentMusicList(); } // 路由跳转 if (id) { router.push({ name: 'musicList', params: { id }, query: { type } }); } else { router.push({ name: 'musicList', query: { type: 'dailyRecommend' } }); } } ================================================ FILE: src/renderer/components/common/PlayBottom.vue ================================================ ================================================ FILE: src/renderer/components/common/PlayListsItem.vue ================================================ ================================================ FILE: src/renderer/components/common/PlaylistDrawer.vue ================================================ ================================================ FILE: src/renderer/components/common/PlaylistItem.vue ================================================ ================================================ FILE: src/renderer/components/common/ResponsiveModal.vue ================================================ ================================================ FILE: src/renderer/components/common/SearchItem.vue ================================================ ================================================ FILE: src/renderer/components/common/SongItem.vue ================================================ ================================================ FILE: src/renderer/components/common/UpdateModal.vue ================================================ ================================================ FILE: src/renderer/components/common/songItemCom/BaseSongItem.vue ================================================ ================================================ FILE: src/renderer/components/common/songItemCom/CompactSongItem.vue ================================================ ================================================ FILE: src/renderer/components/common/songItemCom/ListSongItem.vue ================================================ ================================================ FILE: src/renderer/components/common/songItemCom/MiniSongItem.vue ================================================ ================================================ FILE: src/renderer/components/common/songItemCom/SongItemDropdown.vue ================================================ ================================================ FILE: src/renderer/components/common/songItemCom/StandardSongItem.vue ================================================ ================================================ FILE: src/renderer/components/cover/Cover3D.vue ================================================ ================================================ FILE: src/renderer/components/home/PlaylistType.vue ================================================ ================================================ FILE: src/renderer/components/home/RecommendAlbum.vue ================================================ ================================================ FILE: src/renderer/components/home/RecommendSonglist.vue ================================================ ================================================ FILE: src/renderer/components/home/TopBanner.vue ================================================ ================================================ FILE: src/renderer/components/login/CookieLogin.vue ================================================ ================================================ FILE: src/renderer/components/lyric/MusicFull.vue ================================================ ================================================ FILE: src/renderer/components/lyric/MusicFullMobile.vue ================================================ ================================================ FILE: src/renderer/components/lyric/MusicFullWrapper.vue ================================================ ================================================ FILE: src/renderer/components/lyric/ThemeColorPanel.vue ================================================ ================================================ FILE: src/renderer/components/player/AdvancedControlsPopover.vue ================================================ ================================================ FILE: src/renderer/components/player/MiniPlayBar.vue ================================================ ================================================ FILE: src/renderer/components/player/MobilePlayBar.vue ================================================ ================================================ FILE: src/renderer/components/player/MobilePlayerSettings.vue ================================================ ================================================ FILE: src/renderer/components/player/PlayBar.vue ================================================ ================================================ FILE: src/renderer/components/player/PlayingListDrawer.vue ================================================ ================================================ FILE: src/renderer/components/player/ReparsePopover.vue ================================================ ================================================ FILE: src/renderer/components/player/SimplePlayBar.vue ================================================ ================================================ FILE: src/renderer/components/player/SleepTimer.vue ================================================ ================================================ FILE: src/renderer/components/player/SleepTimerTop.vue ================================================ ================================================ FILE: src/renderer/components/settings/ClearCacheSettings.vue ================================================ ================================================ FILE: src/renderer/components/settings/CookieSettingsModal.vue ================================================ ================================================ FILE: src/renderer/components/settings/MusicSourceSettings.vue ================================================ ================================================ FILE: src/renderer/components/settings/ProxySettings.vue ================================================ ================================================ FILE: src/renderer/components/settings/ServerSetting.vue ================================================ ================================================ FILE: src/renderer/components/settings/ShortcutSettings.vue ================================================ ================================================ FILE: src/renderer/const/bar-const.ts ================================================ export const USER_SET_OPTIONS = [ // { // label: '打卡', // key: 'card', // }, // { // label: '听歌升级', // key: 'card_music', // }, // { // label: '歌曲次数', // key: 'listen', // }, { label: '退出登录', key: 'logout' }, { label: '设置', key: 'set' } ]; export const SEARCH_TYPES = [ { label: 'search.search.single', // 单曲 key: 1 }, { label: 'search.search.album', // 专辑 key: 10 }, { label: 'search.search.playlist', // 歌单 key: 1000 }, { label: 'search.search.mv', // MV key: 1004 }, { label: 'search.search.bilibili', // B站 key: 2000 } ]; export const SEARCH_TYPE = { MUSIC: 1, // 单曲 ALBUM: 10, // 专辑 ARTIST: 100, // 歌手 PLAYLIST: 1000, // 歌单 MV: 1004, // MV BILIBILI: 2000 // B站视频 } as const; ================================================ FILE: src/renderer/directive/index.ts ================================================ import { vLoading } from './loading/index'; const directives = { loading: vLoading }; export default directives; ================================================ FILE: src/renderer/directive/loading/index.ts ================================================ import { createVNode, render, VNode } from 'vue'; import Loading from './index.vue'; const vnode: VNode = createVNode(Loading) as VNode; export const vLoading = { // 在绑定元素的父组件 及他自己的所有子节点都挂载完成后调用 mounted: (el: HTMLElement) => { render(vnode, el); }, // 在绑定元素的父组件 及他自己的所有子节点都更新后调用 updated: (el: HTMLElement, binding: any) => { if (binding.value) { vnode?.component?.exposed?.show(); } else { vnode?.component?.exposed?.hide(); } // 动态添加删除自定义class: loading-parent formatterClass(el, binding); }, // 绑定元素的父组件卸载后调用 unmounted: () => { vnode?.component?.exposed?.hide(); } }; function formatterClass(el: HTMLElement, binding: any) { const classStr = el.getAttribute('class'); const tagetClass: number = classStr?.indexOf('loading-parent') as number; if (binding.value) { if (tagetClass === -1) { el.setAttribute('class', `${classStr} loading-parent`); } } else if (tagetClass > -1) { const classArray: Array = classStr?.split('') as string[]; classArray.splice(tagetClass - 1, tagetClass + 15); el.setAttribute('class', classArray?.join('')); } } ================================================ FILE: src/renderer/directive/loading/index.vue ================================================ ================================================ FILE: src/renderer/hooks/AlbumHistoryHook.ts ================================================ import { useLocalStorage } from '@vueuse/core'; import { ref, watch } from 'vue'; // 专辑历史记录类型 export interface AlbumHistoryItem { id: number; name: string; picUrl?: string; size?: number; // 歌曲数量 artist?: { name: string; id: number; }; count?: number; // 播放次数 lastPlayTime?: number; // 最后播放时间 } export const useAlbumHistory = () => { const albumHistory = useLocalStorage('albumHistory', []); const addAlbum = (album: AlbumHistoryItem) => { const index = albumHistory.value.findIndex((item) => item.id === album.id); const now = Date.now(); if (index !== -1) { // 如果已存在,更新播放次数和时间,并移到最前面 albumHistory.value[index].count = (albumHistory.value[index].count || 0) + 1; albumHistory.value[index].lastPlayTime = now; albumHistory.value.unshift(albumHistory.value.splice(index, 1)[0]); } else { // 如果不存在,添加新记录 albumHistory.value.unshift({ ...album, count: 1, lastPlayTime: now }); } }; const delAlbum = (album: AlbumHistoryItem) => { const index = albumHistory.value.findIndex((item) => item.id === album.id); if (index !== -1) { albumHistory.value.splice(index, 1); } }; const albumList = ref(albumHistory.value); watch( () => albumHistory.value, () => { albumList.value = albumHistory.value; }, { deep: true } ); return { albumHistory, albumList, addAlbum, delAlbum }; }; ================================================ FILE: src/renderer/hooks/IndexDBHook.ts ================================================ import { ref } from 'vue'; // 定义表配置的泛型接口 export interface StoreConfig { name: T; keyPath?: string; } // 创建一个使用 IndexedDB 的组合函数 const useIndexedDB = async >>( dbName: string, stores: StoreConfig[], version: number = 1 ) => { const db = ref(null); // 打开数据库并创建表 const initDB = () => { return new Promise((resolve, reject) => { const request = indexedDB.open(dbName, version); request.onupgradeneeded = (event: any) => { const db = event.target.result; stores.forEach((store) => { if (!db.objectStoreNames.contains(store.name)) { db.createObjectStore(store.name, { keyPath: store.keyPath || 'id', autoIncrement: true }); } }); }; request.onsuccess = (event: any) => { db.value = event.target.result; resolve(); }; request.onerror = (event: any) => { reject(event.target.error); }; }); }; await initDB(); // 通用新增数据 const addData = (storeName: K, value: S[K]) => { return new Promise((resolve, reject) => { if (!db.value) return reject('数据库未初始化'); const tx = db.value.transaction(storeName, 'readwrite'); const store = tx.objectStore(storeName); const request = store.add(value); request.onsuccess = () => { console.log('成功'); resolve(); }; request.onerror = (event) => { console.error('新增失败:', (event.target as IDBRequest).error); reject((event.target as IDBRequest).error); }; }); }; // 通用保存数据(新增或更新) const saveData = (storeName: K, value: S[K]) => { return new Promise((resolve, reject) => { if (!db.value) return reject('数据库未初始化'); const tx = db.value.transaction(storeName, 'readwrite'); const store = tx.objectStore(storeName); const request = store.put(value); request.onsuccess = () => { console.log('成功'); resolve(); }; request.onerror = (event) => { reject((event.target as IDBRequest).error); }; }); }; // 通用获取数据 const getData = (storeName: K, key: string | number) => { return new Promise((resolve, reject) => { if (!db.value) return reject('数据库未初始化'); const tx = db.value.transaction(storeName, 'readonly'); const store = tx.objectStore(storeName); const request = store.get(key); request.onsuccess = (event) => { if (event.target) { resolve((event.target as IDBRequest).result); } else { reject('事件目标为空'); } }; request.onerror = (event) => { reject((event.target as IDBRequest).error); }; }); }; // 删除数据 const deleteData = (storeName: K, key: string | number) => { return new Promise((resolve, reject) => { if (!db.value) return reject('数据库未初始化'); const tx = db.value.transaction(storeName, 'readwrite'); const store = tx.objectStore(storeName); const request = store.delete(key); request.onsuccess = () => { console.log('删除成功'); resolve(); }; request.onerror = (event) => { reject((event.target as IDBRequest).error); }; }); }; // 查询所有数据 const getAllData = (storeName: K) => { return new Promise((resolve, reject) => { if (!db.value) return reject('数据库未初始化'); const tx = db.value.transaction(storeName, 'readonly'); const store = tx.objectStore(storeName); const request = store.getAll(); request.onsuccess = (event) => { if (event.target) { resolve((event.target as IDBRequest).result); } else { reject('事件目标为空'); } }; request.onerror = (event) => { reject((event.target as IDBRequest).error); }; }); }; // 分页查询数据 const getDataWithPagination = (storeName: K, page: number, pageSize: number) => { return new Promise((resolve, reject) => { if (!db.value) return reject('数据库未初始化'); const tx = db.value.transaction(storeName, 'readonly'); const store = tx.objectStore(storeName); const request = store.openCursor(); const results: S[K][] = []; let index = 0; const skip = (page - 1) * pageSize; request.onsuccess = (event: any) => { const cursor = event.target.result; if (!cursor) { resolve(results); return; } if (index >= skip && results.length < pageSize) { results.push(cursor.value); } index++; cursor.continue(); }; request.onerror = (event: any) => { reject(event.target.error); }; }); }; return { initDB, addData, saveData, getData, deleteData, getAllData, getDataWithPagination }; }; export default useIndexedDB; ================================================ FILE: src/renderer/hooks/MusicHistoryHook.ts ================================================ import { useLocalStorage } from '@vueuse/core'; import type { SongResult } from '@/types/music'; export const useMusicHistory = () => { const musicHistory = useLocalStorage('musicHistory', []); const addMusic = (music: SongResult) => { const index = musicHistory.value.findIndex((item) => item.id === music.id); if (index !== -1) { musicHistory.value[index].count = (musicHistory.value[index].count || 0) + 1; musicHistory.value.unshift(musicHistory.value.splice(index, 1)[0]); } else { musicHistory.value.unshift({ ...music, count: 1 }); } }; const delMusic = (music: SongResult) => { const index = musicHistory.value.findIndex((item) => item.id === music.id); if (index !== -1) { musicHistory.value.splice(index, 1); } }; const musicList = ref(musicHistory.value); watch( () => musicHistory.value, () => { musicList.value = musicHistory.value; } ); return { musicHistory, musicList, addMusic, delMusic }; }; ================================================ FILE: src/renderer/hooks/MusicHook.ts ================================================ import { cloneDeep } from 'lodash'; import { createDiscreteApi } from 'naive-ui'; import { computed, type ComputedRef, nextTick, onUnmounted, ref, watch } from 'vue'; import useIndexedDB from '@/hooks/IndexDBHook'; import { audioService } from '@/services/audioService'; import type { usePlayerStore } from '@/store'; import type { Artist, ILyricText, SongResult } from '@/types/music'; import { isElectron } from '@/utils'; import { getTextColors } from '@/utils/linearColor'; import { parseLyrics } from '@/utils/yrcParser'; const windowData = window as any; // 全局 playerStore 引用,通过 initMusicHook 函数注入 let playerStore: ReturnType | null = null; // 初始化函数,接受 store 实例 export const initMusicHook = (store: ReturnType) => { playerStore = store; // 创建 computed 属性 playMusic = computed(() => getPlayerStore().playMusic as SongResult); artistList = computed( () => (getPlayerStore().playMusic.ar || getPlayerStore().playMusic?.song?.artists) as Artist[] ); // 在 store 注入后初始化需要 store 的功能 setupKeyboardListeners(); initProgressAnimation(); setupMusicWatchers(); setupCorrectionTimeWatcher(); setupPlayStateWatcher(); }; // 获取 playerStore 的辅助函数 const getPlayerStore = () => { if (!playerStore) { throw new Error('MusicHook not initialized. Call initMusicHook first.'); } return playerStore; }; export const lrcArray = ref([]); // 歌词数组 export const lrcTimeArray = ref([]); // 歌词时间数组 export const nowTime = ref(0); // 当前播放时间 export const allTime = ref(0); // 总播放时间 export const nowIndex = ref(0); // 当前播放歌词 export const currentLrcProgress = ref(0); // 来存储当前歌词的进度 export const sound = ref(audioService.getCurrentSound()); export const isLyricWindowOpen = ref(false); // 新增状态 export const textColors = ref(getTextColors()); // 这些 computed 属性需要在初始化后创建 export let playMusic: ComputedRef; export let artistList: ComputedRef; export const musicDB = await useIndexedDB( 'musicDB', [ { name: 'music', keyPath: 'id' }, { name: 'music_lyric', keyPath: 'id' }, { name: 'api_cache', keyPath: 'id' }, { name: 'music_url_cache', keyPath: 'id' }, { name: 'music_failed_cache', keyPath: 'id' } ], 3 ); // 键盘事件处理器,在初始化后设置 const setupKeyboardListeners = () => { document.onkeyup = (e) => { // 检查事件目标是否是输入框元素 const target = e.target as HTMLElement; if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA') { return; } const store = getPlayerStore(); switch (e.code) { case 'Space': if (store.play) { store.setPlayMusic(false); audioService.getCurrentSound()?.pause(); } else { store.setPlayMusic(true); audioService.getCurrentSound()?.play(); } break; default: } }; }; const { message } = createDiscreteApi(['message']); // 全局变量 let progressAnimationInitialized = false; let globalAnimationFrameId: number | null = null; const lastSavedTime = ref(0); // 全局停止函数 const stopProgressAnimation = () => { if (globalAnimationFrameId) { cancelAnimationFrame(globalAnimationFrameId); globalAnimationFrameId = null; } }; // 全局更新函数 const updateProgress = () => { if (!getPlayerStore().play) { stopProgressAnimation(); return; } const currentSound = sound.value; if (!currentSound) { console.log('进度更新:无效的 sound 对象'); // 不是立即返回,而是设置定时器稍后再次尝试 globalAnimationFrameId = setTimeout(() => { requestAnimationFrame(updateProgress); }, 100) as unknown as number; return; } if (typeof currentSound.seek !== 'function') { console.log('进度更新:无效的 seek 函数'); // 不是立即返回,而是设置定时器稍后再次尝试 globalAnimationFrameId = setTimeout(() => { requestAnimationFrame(updateProgress); }, 100) as unknown as number; return; } try { const { start, end } = currentLrcTiming.value; if (typeof start !== 'number' || typeof end !== 'number' || start === end) { globalAnimationFrameId = requestAnimationFrame(updateProgress); return; } let currentTime; try { // 获取当前播放位置 currentTime = currentSound.seek() as number; // 减少更新频率,避免频繁更新UI const timeDiff = Math.abs(currentTime - nowTime.value); if (timeDiff > 0.2 || Math.floor(currentTime) !== Math.floor(nowTime.value)) { nowTime.value = currentTime; } // 保存当前播放进度到 localStorage (每秒保存一次,避免频繁写入) if ( Math.floor(currentTime) % 2 === 0 && Math.floor(currentTime) !== Math.floor(lastSavedTime.value) ) { lastSavedTime.value = currentTime; if (getPlayerStore().playMusic && getPlayerStore().playMusic.id) { localStorage.setItem( 'playProgress', JSON.stringify({ songId: getPlayerStore().playMusic.id, progress: currentTime }) ); } } } catch (seekError) { console.error('调用 seek() 方法出错:', seekError); globalAnimationFrameId = requestAnimationFrame(updateProgress); return; } if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) { console.error('无效的当前时间:', currentTime); globalAnimationFrameId = requestAnimationFrame(updateProgress); return; } const elapsed = currentTime - start; const duration = end - start; const progress = (elapsed / duration) * 100; // 确保进度在 0-100 之间 currentLrcProgress.value = Math.min(Math.max(progress, 0), 100); } catch (error) { console.error('更新进度出错:', error); } // 继续下一帧更新,但降低更新频率为60帧中更新10帧 globalAnimationFrameId = setTimeout(() => { requestAnimationFrame(updateProgress); }, 100) as unknown as number; }; // 全局启动函数 const startProgressAnimation = () => { stopProgressAnimation(); // 先停止之前的动画 updateProgress(); }; // 全局初始化函数 const initProgressAnimation = () => { if (progressAnimationInitialized) return; console.log('初始化进度动画'); progressAnimationInitialized = true; // 监听播放状态变化,这里使用防抖,避免频繁触发 let debounceTimer: any = null; watch( () => getPlayerStore().play, (newIsPlaying) => { console.log('播放状态变化:', newIsPlaying); // 清除之前的定时器 if (debounceTimer) { clearTimeout(debounceTimer); } // 使用防抖,延迟 100ms 再执行 debounceTimer = setTimeout(() => { if (newIsPlaying) { // 确保 sound 对象有效时才启动进度更新 if (sound.value) { console.log('sound 对象已存在,立即启动进度更新'); startProgressAnimation(); } else { console.log('等待 sound 对象初始化...'); // 定时检查 sound 对象是否已初始化 const checkInterval = setInterval(() => { if (sound.value) { clearInterval(checkInterval); console.log('sound 对象已初始化,开始进度更新'); startProgressAnimation(); } }, 100); // 设置超时,防止无限等待 setTimeout(() => { clearInterval(checkInterval); console.log('等待 sound 对象超时,已停止等待'); }, 5000); } } else { stopProgressAnimation(); } }, 100); } ); // 监听当前歌词索引变化 watch(nowIndex, () => { currentLrcProgress.value = 0; if (getPlayerStore().play) { startProgressAnimation(); } }); // 监听音频对象变化 watch(sound, (newSound) => { console.log('sound 对象变化:', !!newSound); if (newSound && getPlayerStore().play) { startProgressAnimation(); } }); }; /** * 解析歌词字符串并转换为ILyricText格式 * @param lyricsStr 歌词字符串 * @returns 解析后的歌词数据 */ const parseLyricsString = async ( lyricsStr: string ): Promise<{ lrcArray: ILyricText[]; lrcTimeArray: number[]; hasWordByWord: boolean }> => { if (!lyricsStr || typeof lyricsStr !== 'string') { return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false }; } try { const parseResult = parseLyrics(lyricsStr); console.log('parseResult', parseResult); if (!parseResult.success) { console.error('歌词解析失败:', parseResult.error.message); return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false }; } const { lyrics } = parseResult.data; const lrcArray: ILyricText[] = []; const lrcTimeArray: number[] = []; let hasWordByWord = false; for (const line of lyrics) { // 检查是否有逐字歌词 const hasWords = line.words && line.words.length > 0; if (hasWords) { hasWordByWord = true; } lrcArray.push({ text: line.fullText, trText: '', // 翻译文本稍后处理 words: hasWords ? line.words.map((word) => ({ ...word })) : undefined, hasWordByWord: hasWords, startTime: line.startTime, duration: line.duration }); lrcTimeArray.push(line.startTime); } return { lrcArray, lrcTimeArray, hasWordByWord }; } catch (error) { console.error('解析歌词时发生错误:', error); return { lrcArray: [], lrcTimeArray: [], hasWordByWord: false }; } }; // 设置音乐相关的监听器 const setupMusicWatchers = () => { const store = getPlayerStore(); // 监听 playerStore.playMusic 的变化以更新歌词数据 watch( () => store.playMusic.id, async (newId, oldId) => { // 如果没有歌曲ID,清空歌词 if (!newId) { lrcArray.value = []; lrcTimeArray.value = []; nowIndex.value = 0; return; } // 避免相同ID的重复执行(但允许初始化时执行) if (newId === oldId && lrcArray.value.length > 0) return; // 歌曲切换时重置歌词索引 if (newId !== oldId) { nowIndex.value = 0; } await nextTick(async () => { console.log('歌曲切换,更新歌词数据'); // 检查是否有原始歌词字符串需要解析 const lyricData = playMusic.value.lyric; if (lyricData && typeof lyricData === 'string') { // 如果歌词是字符串格式,使用新的解析器 const { lrcArray: parsedLrcArray, lrcTimeArray: parsedTimeArray, hasWordByWord } = await parseLyricsString(lyricData); lrcArray.value = parsedLrcArray; lrcTimeArray.value = parsedTimeArray; // 更新歌曲的歌词数据结构 if (playMusic.value.lyric && typeof playMusic.value.lyric === 'object') { playMusic.value.lyric.hasWordByWord = hasWordByWord; } } else { // 使用现有的歌词数据结构 const rawLrc = lyricData?.lrcArray || []; lrcTimeArray.value = lyricData?.lrcTimeArray || []; try { const { translateLyrics } = await import('@/services/lyricTranslation'); lrcArray.value = await translateLyrics(rawLrc as any); } catch (e) { console.error('翻译歌词失败,使用原始歌词:', e); lrcArray.value = rawLrc as any; } } // 当歌词数据更新时,如果歌词窗口打开,则发送数据 if (isElectron && isLyricWindowOpen.value) { console.log('歌词窗口已打开,同步最新歌词数据'); // 不管歌词数组是否为空,都发送最新数据 sendLyricToWin(); // 再次延迟发送,确保歌词窗口已完全加载 setTimeout(() => { sendLyricToWin(); }, 500); } }); }, { immediate: true } ); }; const setupAudioListeners = () => { let interval: any = null; const clearInterval = () => { if (interval) { window.clearInterval(interval); interval = null; } }; // 清理所有事件监听器 audioService.clearAllListeners(); // 监听seek开始事件,立即更新UI audioService.on('seek_start', (time) => { // 直接更新显示位置,不检查拖动状态 nowTime.value = time; }); // 监听seek完成事件 audioService.on('seek', () => { try { const currentSound = sound.value; if (currentSound) { // 立即更新显示时间,不进行任何检查 const currentTime = currentSound.seek() as number; if (typeof currentTime === 'number' && !Number.isNaN(currentTime)) { nowTime.value = currentTime; // 检查是否需要更新歌词 const newIndex = getLrcIndex(nowTime.value); if (newIndex !== nowIndex.value) { nowIndex.value = newIndex; if (isElectron && isLyricWindowOpen.value) { sendLyricToWin(); } } } } } catch (error) { console.error('处理seek事件出错:', error); } }); // 立即更新一次时间和进度(解决初始化时进度条不显示问题) const updateCurrentTimeAndDuration = () => { const currentSound = audioService.getCurrentSound(); if (currentSound) { try { // 更新当前时间和总时长 const currentTime = currentSound.seek() as number; if (typeof currentTime === 'number' && !Number.isNaN(currentTime)) { nowTime.value = currentTime; allTime.value = currentSound.duration() as number; } } catch (error) { console.error('初始化时间和进度失败:', error); } } }; // 立即执行一次更新 updateCurrentTimeAndDuration(); // 监听播放 audioService.on('play', () => { getPlayerStore().setPlayMusic(true); if (isElectron) { window.api.sendSong(cloneDeep(getPlayerStore().playMusic)); } clearInterval(); interval = window.setInterval(() => { try { const currentSound = sound.value; if (!currentSound) { console.error('Invalid sound object: sound is null or undefined'); clearInterval(); return; } // 确保 seek 方法存在且可调用 if (typeof currentSound.seek !== 'function') { console.error('Invalid sound object: seek function not available'); clearInterval(); return; } const currentTime = currentSound.seek() as number; if (typeof currentTime !== 'number' || Number.isNaN(currentTime)) { console.error('Invalid current time:', currentTime); clearInterval(); return; } nowTime.value = currentTime; allTime.value = currentSound.duration() as number; const newIndex = getLrcIndex(nowTime.value); if (newIndex !== nowIndex.value) { nowIndex.value = newIndex; // 注意:我们不在这里设置 currentLrcProgress 为 0 // 因为这会与全局进度更新冲突 if (isElectron && isLyricWindowOpen.value) { sendLyricToWin(); } } if (isElectron && isLyricWindowOpen.value) { sendLyricToWin(); } } catch (error) { console.error('Error in interval:', error); clearInterval(); } }, 50); }); // 监听暂停 audioService.on('pause', () => { console.log('音频暂停事件触发'); getPlayerStore().setPlayMusic(false); clearInterval(); if (isElectron && isLyricWindowOpen.value) { sendLyricToWin(); } }); const replayMusic = async () => { try { // 如果当前有音频实例,先停止并销毁 if (sound.value) { sound.value.stop(); sound.value.unload(); sound.value = null; } // 重新播放当前歌曲 if (getPlayerStore().playMusicUrl && playMusic.value) { const newSound = await audioService.play(getPlayerStore().playMusicUrl, playMusic.value); sound.value = newSound as Howl; setupAudioListeners(); } else { console.error('No music URL or playMusic data available'); getPlayerStore().nextPlay(); } } catch (error) { console.error('Error replaying song:', error); getPlayerStore().nextPlay(); } }; // 监听结束 audioService.on('end', () => { console.log('音频播放结束事件触发'); clearInterval(); if (getPlayerStore().playMode === 1) { // 单曲循环模式 if (sound.value) { replayMusic(); } } else { // 顺序播放、列表循环、随机播放模式都使用统一的nextPlay方法 getPlayerStore().nextPlay(); } }); audioService.on('previoustrack', () => { getPlayerStore().prevPlay(); }); audioService.on('nexttrack', () => { getPlayerStore().nextPlay(); }); return clearInterval; }; export const play = () => { const currentSound = audioService.getCurrentSound(); if (currentSound) { currentSound.play(); // 在播放时也进行状态检测,防止URL已过期导致无声 getPlayerStore().checkPlaybackState(getPlayerStore().playMusic); } }; export const pause = () => { const currentSound = audioService.getCurrentSound(); if (currentSound) { try { // 保存当前播放进度 const currentTime = currentSound.seek() as number; if (getPlayerStore().playMusic && getPlayerStore().playMusic.id) { localStorage.setItem( 'playProgress', JSON.stringify({ songId: getPlayerStore().playMusic.id, progress: currentTime }) ); } audioService.pause(); } catch (error) { console.error('暂停播放出错:', error); } } }; // 歌词矫正时间映射(每首歌独立) const CORRECTION_KEY = 'lyric-correction-map'; const correctionTimeMap = ref>({}); // 初始化 correctionTimeMap const loadCorrectionMap = () => { try { const raw = localStorage.getItem(CORRECTION_KEY); correctionTimeMap.value = raw ? JSON.parse(raw) : {}; } catch { correctionTimeMap.value = {}; } }; const saveCorrectionMap = () => { localStorage.setItem(CORRECTION_KEY, JSON.stringify(correctionTimeMap.value)); }; loadCorrectionMap(); // 歌词矫正时间,当前歌曲 export const correctionTime = ref(0); // 设置歌词矫正时间的监听器 const setupCorrectionTimeWatcher = () => { // 切歌时自动读取矫正时间 watch( () => playMusic.value?.id, (id) => { if (!id) return; correctionTime.value = correctionTimeMap.value[id] ?? 0; }, { immediate: true } ); }; /** * 调整歌词矫正时间(每首歌独立) * @param delta 增加/减少的秒数(正为加,负为减) */ export const adjustCorrectionTime = (delta: number) => { const id = playMusic.value?.id; if (!id) return; const newVal = Math.max(-10, Math.min(10, (correctionTime.value ?? 0) + delta)); correctionTime.value = newVal; correctionTimeMap.value[id] = newVal; saveCorrectionMap(); }; // 获取当前播放歌词 export const isCurrentLrc = (index: number, time: number): boolean => { const currentTime = lrcTimeArray.value[index]; // 如果是最后一句歌词,只需要判断时间是否大于等于当前句的开始时间 if (index === lrcTimeArray.value.length - 1) { const correctedTime = time + correctionTime.value; return correctedTime >= currentTime; } // 非最后一句歌词,需要判断时间在当前句和下一句之间 const nextTime = lrcTimeArray.value[index + 1]; const correctedTime = time + correctionTime.value; return correctedTime >= currentTime && correctedTime < nextTime; }; // 获取当前播放歌词INDEX export const getLrcIndex = (time: number): number => { const correctedTime = time + correctionTime.value; // 如果歌词数组为空,返回当前索引 if (lrcTimeArray.value.length === 0) { return nowIndex.value; } // 处理最后一句歌词的情况 const lastIndex = lrcTimeArray.value.length - 1; if (correctedTime >= lrcTimeArray.value[lastIndex]) { nowIndex.value = lastIndex; return lastIndex; } // 查找当前时间对应的歌词索引 for (let i = 0; i < lrcTimeArray.value.length - 1; i++) { const currentTime = lrcTimeArray.value[i]; const nextTime = lrcTimeArray.value[i + 1]; if (correctedTime >= currentTime && correctedTime < nextTime) { nowIndex.value = i; return i; } } return nowIndex.value; }; // 获取当前播放歌词进度 const currentLrcTiming = computed(() => { const start = lrcTimeArray.value[nowIndex.value] || 0; const end = lrcTimeArray.value[nowIndex.value + 1] || start + 1; return { start, end }; }); // 获取歌词样式 export const getLrcStyle = (index: number) => { const currentTime = nowTime.value + correctionTime.value; const start = lrcTimeArray.value[index]; const end = lrcTimeArray.value[index + 1] ?? start + 1; if (currentTime >= start && currentTime < end) { // 当前句,显示进度 const progress = ((currentTime - start) / (end - start)) * 100; return { backgroundImage: `linear-gradient(to right, #ffffff ${progress}%, #ffffff8a ${progress}%)`, backgroundClip: 'text', WebkitBackgroundClip: 'text', color: 'transparent', transition: 'background-image 0.1s linear' }; } // 其它句 return {}; }; // 播放进度 export const useLyricProgress = () => { // 如果已经在全局更新进度,立即返回 return { getLrcStyle }; }; // 设置当前播放时间 export const setAudioTime = (index: number) => { const currentSound = sound.value; if (!currentSound) return; currentSound.seek(lrcTimeArray.value[index]); currentSound.play(); }; // 获取当前播放的歌词 export const getCurrentLrc = () => { const index = getLrcIndex(nowTime.value); return { currentLrc: lrcArray.value[index], nextLrc: lrcArray.value[index + 1] }; }; // 获取一句歌词播放时间几秒到几秒 export const getLrcTimeRange = (index: number) => ({ currentTime: lrcTimeArray.value[index], nextTime: lrcTimeArray.value[index + 1] }); // 监听歌词数组变化,当切换歌曲时重新初始化歌词窗口 watch( () => lrcArray.value, (newLrcArray) => { if (newLrcArray.length > 0 && isElectron && isLyricWindowOpen.value) { sendLyricToWin(); } } ); // 发送歌词更新数据 export const sendLyricToWin = () => { if (!isElectron || !isLyricWindowOpen.value) { return; } // 检查是否有播放的歌曲 if (!playMusic.value || !playMusic.value.id) { return; } try { // 记录歌词发送状态 if (lrcArray.value && lrcArray.value.length > 0) { const nowIndex = getLrcIndex(nowTime.value); // 构建完整的歌词更新数据 const updateData = { type: 'full', nowIndex, nowTime: nowTime.value, startCurrentTime: lrcTimeArray.value[nowIndex] || 0, nextTime: lrcTimeArray.value[nowIndex + 1] || 0, isPlay: getPlayerStore().play, lrcArray: lrcArray.value, lrcTimeArray: lrcTimeArray.value, allTime: allTime.value, playMusic: playMusic.value }; // 发送数据到歌词窗口 window.api.sendLyric(JSON.stringify(updateData)); } else { console.log('No lyric data available, sending empty lyric message'); // 发送没有歌词的提示 const emptyLyricData = { type: 'empty', nowIndex: 0, nowTime: nowTime.value, startCurrentTime: 0, nextTime: 0, isPlay: getPlayerStore().play, lrcArray: [{ text: '当前歌曲暂无歌词', trText: '' }], lrcTimeArray: [0], allTime: allTime.value, playMusic: playMusic.value }; window.api.sendLyric(JSON.stringify(emptyLyricData)); } } catch (error) { console.error('Error sending lyric update:', error); } }; // 歌词同步定时器 let lyricSyncInterval: any = null; // 开始歌词同步 const startLyricSync = () => { // 清除已有的定时器 if (lyricSyncInterval) { clearInterval(lyricSyncInterval); } // 每秒同步一次歌词数据 lyricSyncInterval = setInterval(() => { if (isElectron && isLyricWindowOpen.value && getPlayerStore().play && playMusic.value?.id) { // 发送当前播放进度的更新 try { const updateData = { type: 'update', nowIndex: getLrcIndex(nowTime.value), nowTime: nowTime.value, isPlay: getPlayerStore().play }; window.api.sendLyric(JSON.stringify(updateData)); } catch (error) { console.error('发送歌词进度更新失败:', error); } } }, 1000); }; // 停止歌词同步 const stopLyricSync = () => { if (lyricSyncInterval) { clearInterval(lyricSyncInterval); lyricSyncInterval = null; } }; // 修改openLyric函数,添加定时同步 export const openLyric = () => { if (!isElectron) return; // 检查是否有播放中的歌曲 if (!playMusic.value || !playMusic.value.id) { console.log('没有正在播放的歌曲,无法打开歌词窗口'); return; } console.log('Opening lyric window with current song:', playMusic.value?.name); isLyricWindowOpen.value = !isLyricWindowOpen.value; if (isLyricWindowOpen.value) { // 立即打开窗口 window.api.openLyric(); // 确保有歌词数据,如果没有,则使用默认的"无歌词"提示 if (!lrcArray.value || lrcArray.value.length === 0) { // 如果当前播放的歌曲有ID但没有歌词,则尝试加载歌词 console.log('尝试加载歌词数据...'); // 发送默认的"无歌词"数据 const emptyLyricData = { type: 'empty', nowIndex: 0, nowTime: nowTime.value, startCurrentTime: 0, nextTime: 0, isPlay: getPlayerStore().play, lrcArray: [{ text: '加载歌词中...', trText: '' }], lrcTimeArray: [0], allTime: allTime.value, playMusic: playMusic.value }; window.api.sendLyric(JSON.stringify(emptyLyricData)); } else { // 发送完整歌词数据 sendLyricToWin(); } // 设置定时器,确保500ms后再次发送数据,以防窗口加载延迟 setTimeout(() => { sendLyricToWin(); }, 500); // 启动歌词同步 startLyricSync(); } else { closeLyric(); // 停止歌词同步 stopLyricSync(); } }; // 修改closeLyric函数,确保停止定时同步 export const closeLyric = () => { if (!isElectron) return; isLyricWindowOpen.value = false; // 确保状态更新 windowData.electron.ipcRenderer.send('close-lyric'); // 停止歌词同步 stopLyricSync(); }; // 设置播放状态监听器 const setupPlayStateWatcher = () => { // 在组件挂载时设置对播放状态的监听 watch( () => getPlayerStore().play, (isPlaying) => { // 如果歌词窗口打开,根据播放状态控制同步 if (isElectron && isLyricWindowOpen.value) { if (isPlaying) { startLyricSync(); } else { // 如果暂停播放,发送一次暂停状态的更新 const pauseData = { type: 'update', isPlay: false }; window.api.sendLyric(JSON.stringify(pauseData)); } } } ); }; // 在组件卸载时清理资源 onUnmounted(() => { stopLyricSync(); }); // 导出歌词解析函数供外部使用 export { parseLyricsString }; // 添加播放控制命令监听 if (isElectron) { windowData.electron.ipcRenderer.on('lyric-control-back', (_, command: string) => { switch (command) { case 'playpause': if (getPlayerStore().play) { getPlayerStore().setPlayMusic(false); audioService.getCurrentSound()?.pause(); } else { getPlayerStore().setPlayMusic(true); audioService.getCurrentSound()?.play(); } break; case 'prev': getPlayerStore().prevPlay(); break; case 'next': getPlayerStore().nextPlay(); break; case 'close': isLyricWindowOpen.value = false; // 确保状态更新 break; default: console.log('Unknown command:', command); break; } }); } // 在组件挂载时设置监听器 export const initAudioListeners = async () => { try { // 确保有正在播放的音乐 if (!getPlayerStore().playMusic || !getPlayerStore().playMusic.id) { console.log('没有正在播放的音乐,跳过音频监听器初始化'); return; } // 确保有音频实例 const initialSound = audioService.getCurrentSound(); if (!initialSound) { console.log('没有音频实例,等待音频加载...'); // 等待音频加载完成 await new Promise((resolve) => { const checkInterval = setInterval(() => { const sound = audioService.getCurrentSound(); if (sound) { clearInterval(checkInterval); resolve(); } }, 100); // 设置超时 setTimeout(() => { clearInterval(checkInterval); console.log('等待音频加载超时'); resolve(); }, 5000); }); } // 初始化音频监听器 setupAudioListeners(); // 监听歌词窗口关闭事件 if (isElectron) { window.api.onLyricWindowClosed(() => { isLyricWindowOpen.value = false; }); } // 获取最新的音频实例 const finalSound = audioService.getCurrentSound(); if (finalSound) { // 更新全局 sound 引用 sound.value = finalSound; } else { console.warn('无法获取音频实例,跳过进度更新初始化'); } } catch (error) { console.error('初始化音频监听器失败:', error); } }; // 监听URL过期事件,自动重新获取URL并恢复播放 audioService.on('url_expired', async (expiredTrack) => { if (!expiredTrack) return; console.log('检测到URL过期事件,准备重新获取URL', expiredTrack.name); try { // 使用 handlePlayMusic 重新播放,它会自动处理 URL 获取和状态跟踪 // 我们将 isFirstPlay 设为 true 以强制获取新 URL const trackToPlay = { ...expiredTrack, isFirstPlay: true, playMusicUrl: undefined }; await getPlayerStore().handlePlayMusic(trackToPlay, getPlayerStore().play); message.success('已自动恢复播放'); } catch (error) { console.error('处理URL过期事件失败:', error); message.error('恢复播放失败,请手动点击播放'); } }); // 添加音频就绪事件监听器 window.addEventListener('audio-ready', ((event: CustomEvent) => { try { const { sound: newSound } = event.detail; if (newSound) { // 更新本地 sound 引用 sound.value = newSound as Howl; // 设置音频监听器 setupAudioListeners(); // 获取当前播放位置并更新显示 const currentPosition = newSound.seek() as number; if (typeof currentPosition === 'number' && !Number.isNaN(currentPosition)) { nowTime.value = currentPosition; } console.log('音频就绪,已设置监听器并更新进度显示'); } } catch (error) { console.error('处理音频就绪事件出错:', error); } }) as EventListener); ================================================ FILE: src/renderer/hooks/PlaylistHistoryHook.ts ================================================ import { useLocalStorage } from '@vueuse/core'; import { ref, watch } from 'vue'; // 歌单历史记录类型 export interface PlaylistHistoryItem { id: number; name: string; coverImgUrl?: string; picUrl?: string; // 兼容字段 trackCount?: number; playCount?: number; creator?: { nickname: string; userId: number; }; count?: number; // 播放次数 lastPlayTime?: number; // 最后播放时间 } export const usePlaylistHistory = () => { const playlistHistory = useLocalStorage('playlistHistory', []); const addPlaylist = (playlist: PlaylistHistoryItem) => { const index = playlistHistory.value.findIndex((item) => item.id === playlist.id); const now = Date.now(); if (index !== -1) { // 如果已存在,更新播放次数和时间,并移到最前面 playlistHistory.value[index].count = (playlistHistory.value[index].count || 0) + 1; playlistHistory.value[index].lastPlayTime = now; playlistHistory.value.unshift(playlistHistory.value.splice(index, 1)[0]); } else { // 如果不存在,添加新记录 playlistHistory.value.unshift({ ...playlist, count: 1, lastPlayTime: now }); } }; const delPlaylist = (playlist: PlaylistHistoryItem) => { const index = playlistHistory.value.findIndex((item) => item.id === playlist.id); if (index !== -1) { playlistHistory.value.splice(index, 1); } }; const playlistList = ref(playlistHistory.value); watch( () => playlistHistory.value, () => { playlistList.value = playlistHistory.value; }, { deep: true } ); return { playlistHistory, playlistList, addPlaylist, delPlaylist }; }; ================================================ FILE: src/renderer/hooks/useArtist.ts ================================================ import { useRouter } from 'vue-router'; export const useArtist = () => { const router = useRouter(); /** * 跳转到歌手详情页 * @param id 歌手ID */ const navigateToArtist = (id: number) => { router.push(`/artist/detail/${id}`); }; return { navigateToArtist }; }; ================================================ FILE: src/renderer/hooks/useDownload.ts ================================================ import { cloneDeep } from 'lodash'; import { useMessage } from 'naive-ui'; import { ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { getSongUrl } from '@/store/modules/player'; import type { SongResult } from '@/types/music'; import { isElectron } from '@/utils'; const ipcRenderer = isElectron ? window.electron.ipcRenderer : null; // 全局下载管理(闭包模式) const createDownloadManager = () => { // 正在下载的文件集合 const activeDownloads = new Set(); // 已经发送了通知的文件集合(避免重复通知) const notifiedDownloads = new Set(); // 事件监听器是否已初始化 let isInitialized = false; // 监听器引用(用于清理) let completeListener: ((event: any, data: any) => void) | null = null; let errorListener: ((event: any, data: any) => void) | null = null; return { // 添加下载 addDownload: (filename: string) => { activeDownloads.add(filename); }, // 移除下载 removeDownload: (filename: string) => { activeDownloads.delete(filename); // 延迟清理通知记录 setTimeout(() => { notifiedDownloads.delete(filename); }, 5000); }, // 标记文件已通知 markNotified: (filename: string) => { notifiedDownloads.add(filename); }, // 检查文件是否已通知 isNotified: (filename: string) => { return notifiedDownloads.has(filename); }, // 清理所有下载 clearDownloads: () => { activeDownloads.clear(); notifiedDownloads.clear(); }, // 初始化事件监听器 initEventListeners: (message: any, t: any) => { if (isInitialized) return; // 移除可能存在的旧监听器 if (completeListener) { ipcRenderer?.removeListener('music-download-complete', completeListener); } if (errorListener) { ipcRenderer?.removeListener('music-download-error', errorListener); } // 创建新的监听器 completeListener = (_event, data) => { if (!data.filename || !activeDownloads.has(data.filename)) return; // 如果该文件已经通知过,则跳过 if (notifiedDownloads.has(data.filename)) return; // 标记为已通知 notifiedDownloads.add(data.filename); // 从活动下载移除 activeDownloads.delete(data.filename); }; errorListener = (_event, data) => { if (!data.filename || !activeDownloads.has(data.filename)) return; // 如果该文件已经通知过,则跳过 if (notifiedDownloads.has(data.filename)) return; // 标记为已通知 notifiedDownloads.add(data.filename); // 显示失败通知 message.error( t('songItem.message.downloadFailed', { filename: data.filename, error: data.error || '未知错误' }) ); // 从活动下载移除 activeDownloads.delete(data.filename); }; // 添加监听器 ipcRenderer?.on('music-download-complete', completeListener); ipcRenderer?.on('music-download-error', errorListener); isInitialized = true; }, // 清理事件监听器 cleanupEventListeners: () => { if (!isInitialized) return; if (completeListener) { ipcRenderer?.removeListener('music-download-complete', completeListener); completeListener = null; } if (errorListener) { ipcRenderer?.removeListener('music-download-error', errorListener); errorListener = null; } isInitialized = false; }, // 获取活跃下载数量 getActiveDownloadCount: () => { return activeDownloads.size; }, // 检查是否有特定文件正在下载 hasDownload: (filename: string) => { return activeDownloads.has(filename); } }; }; // 创建单例下载管理器 const downloadManager = createDownloadManager(); export const useDownload = () => { const { t } = useI18n(); const message = useMessage(); const isDownloading = ref(false); // 初始化事件监听器 downloadManager.initEventListeners(message, t); /** * 下载单首音乐 * @param song 歌曲信息 * @returns Promise */ const downloadMusic = async (song: SongResult) => { if (isDownloading.value) { message.warning(t('songItem.message.downloading')); return; } try { isDownloading.value = true; const musicUrl = (await getSongUrl(song.id as number, cloneDeep(song), true)) as any; if (!musicUrl) { throw new Error(t('songItem.message.getUrlFailed')); } // 构建文件名 const artistNames = (song.ar || song.song?.artists)?.map((a) => a.name).join(','); const filename = `${song.name} - ${artistNames}`; // 检查是否已在下载 if (downloadManager.hasDownload(filename)) { isDownloading.value = false; return; } // 添加到活动下载集合 downloadManager.addDownload(filename); const songData = cloneDeep(song); songData.ar = songData.ar || songData.song?.artists; // 发送下载请求 ipcRenderer?.send('download-music', { url: typeof musicUrl === 'string' ? musicUrl : musicUrl.url, filename, songInfo: { ...songData, downloadTime: Date.now() }, type: musicUrl.type }); message.success(t('songItem.message.downloadQueued')); // 简化的监听逻辑,基本通知由全局监听器处理 setTimeout(() => { isDownloading.value = false; }, 2000); } catch (error: any) { console.error('Download error:', error); isDownloading.value = false; message.error(error.message || t('songItem.message.downloadFailed')); } }; /** * 批量下载音乐 * @param songs 歌曲列表 * @returns Promise */ const batchDownloadMusic = async (songs: SongResult[]) => { if (isDownloading.value) { message.warning(t('favorite.downloading')); return; } if (songs.length === 0) { message.warning(t('favorite.selectSongsFirst')); return; } try { isDownloading.value = true; message.success(t('favorite.downloading')); let successCount = 0; let failCount = 0; const totalCount = songs.length; // 下载进度追踪 const trackProgress = () => { if (successCount + failCount === totalCount) { isDownloading.value = false; message.success(t('favorite.downloadSuccess')); } }; // 并行获取所有歌曲的下载链接 const downloadUrls = await Promise.all( songs.map(async (song) => { try { const data = (await getSongUrl(song.id, song, true)) as any; return { song, ...data }; } catch (error) { console.error(`获取歌曲 ${song.name} 下载链接失败:`, error); failCount++; return { song, url: null }; } }) ); // 开始下载有效的链接 downloadUrls.forEach(({ song, url, type }) => { if (!url) { failCount++; trackProgress(); return; } const songData = cloneDeep(song); const filename = `${song.name} - ${(song.ar || song.song?.artists)?.map((a) => a.name).join(',')}`; // 检查是否已在下载 if (downloadManager.hasDownload(filename)) { failCount++; trackProgress(); return; } // 添加到活动下载集合 downloadManager.addDownload(filename); const songInfo = { ...songData, ar: songData.ar || songData.song?.artists, downloadTime: Date.now() }; ipcRenderer?.send('download-music', { url, filename, songInfo, type }); successCount++; }); // 所有下载开始后,检查进度 trackProgress(); } catch (error) { console.error('下载失败:', error); isDownloading.value = false; message.destroyAll(); message.error(t('favorite.downloadFailed')); } }; return { isDownloading, downloadMusic, batchDownloadMusic }; }; ================================================ FILE: src/renderer/hooks/usePlayMode.ts ================================================ import { computed } from 'vue'; import { useI18n } from 'vue-i18n'; import { usePlayerStore } from '@/store/modules/player'; /** * 播放模式相关的 Hook * 提供播放模式的图标、文本和切换功能 */ export function usePlayMode() { const { t } = useI18n(); const playerStore = usePlayerStore(); // 当前播放模式 const playMode = computed(() => playerStore.playMode); // 播放模式图标 const playModeIcon = computed(() => { switch (playMode.value) { case 0: return 'ri-repeat-2-line'; case 1: return 'ri-repeat-one-line'; case 2: return 'ri-shuffle-line'; case 3: return 'ri-heart-pulse-line'; default: return 'ri-repeat-2-line'; } }); // 播放模式文本 const playModeText = computed(() => { switch (playMode.value) { case 0: return t('player.playBar.playMode.sequence'); case 1: return t('player.playBar.playMode.loop'); case 2: return t('player.playBar.playMode.random'); case 3: return t('player.playBar.intelligenceMode.title'); default: return t('player.playBar.playMode.sequence'); } }); // 切换播放模式 const togglePlayMode = () => { playerStore.togglePlayMode(); }; return { playMode, playModeIcon, playModeText, togglePlayMode }; } ================================================ FILE: src/renderer/hooks/usePlayerHooks.ts ================================================ import { cloneDeep } from 'lodash'; import { createDiscreteApi } from 'naive-ui'; import i18n from '@/../i18n/renderer'; import { getBilibiliAudioUrl } from '@/api/bilibili'; import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music'; import { playbackRequestManager } from '@/services/playbackRequestManager'; import { SongSourceConfigManager } from '@/services/SongSourceConfigManager'; import type { ILyric, ILyricText, IWordData, SongResult } from '@/types/music'; import { getImgUrl } from '@/utils'; import { getImageLinearBackground } from '@/utils/linearColor'; import { parseLyrics as parseYrcLyrics } from '@/utils/yrcParser'; const { message } = createDiscreteApi(['message']); /** * 获取歌曲播放URL(独立函数) */ export const getSongUrl = async ( id: string | number, songData: SongResult, isDownloaded: boolean = false, requestId?: string ) => { const numericId = typeof id === 'string' ? parseInt(id, 10) : id; // 动态导入 settingsStore const { useSettingsStore } = await import('@/store/modules/settings'); const settingsStore = useSettingsStore(); try { // 在开始处理前验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[getSongUrl] 请求已失效: ${requestId}`); throw new Error('Request cancelled'); } if (songData.playMusicUrl) { return songData.playMusicUrl; } if (songData.source === 'bilibili' && songData.bilibiliData) { console.log('加载B站音频URL'); if (!songData.playMusicUrl && songData.bilibiliData.bvid && songData.bilibiliData.cid) { try { songData.playMusicUrl = await getBilibiliAudioUrl( songData.bilibiliData.bvid, songData.bilibiliData.cid ); // 验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[getSongUrl] 获取B站URL后请求已失效: ${requestId}`); throw new Error('Request cancelled'); } return songData.playMusicUrl; } catch (error) { console.error('重启后获取B站音频URL失败:', error); return ''; } } return songData.playMusicUrl || ''; } // ==================== 自定义API最优先 ==================== const globalSources = settingsStore.setData.enabledMusicSources || []; const useCustomApiGlobally = globalSources.includes('custom'); const songConfig = SongSourceConfigManager.getConfig(id); const useCustomApiForSong = songConfig?.sources.includes('custom' as any) ?? false; // 如果全局或歌曲专属设置中启用了自定义API,则最优先尝试 if ((useCustomApiGlobally || useCustomApiForSong) && settingsStore.setData.customApiPlugin) { console.log(`优先级 1: 尝试使用自定义API解析歌曲 ${id}...`); try { const { parseFromCustomApi } = await import('@/api/parseFromCustomApi'); const customResult = await parseFromCustomApi( numericId, cloneDeep(songData), settingsStore.setData.musicQuality || 'higher' ); // 验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[getSongUrl] 自定义API解析后请求已失效: ${requestId}`); throw new Error('Request cancelled'); } if ( customResult && customResult.data && customResult.data.data && customResult.data.data.url ) { console.log('自定义API解析成功!'); if (isDownloaded) return customResult.data.data as any; return customResult.data.data.url; } else { console.log('自定义API解析失败,将使用默认降级流程...'); message.warning(i18n.global.t('player.reparse.customApiFailed')); } } catch (error) { console.error('调用自定义API时发生错误:', error); if ((error as Error).message === 'Request cancelled') { throw error; } message.error(i18n.global.t('player.reparse.customApiError')); } } // 如果有自定义音源设置,直接使用getParsingMusicUrl获取URL if (songConfig && songData.source !== 'bilibili') { try { console.log(`使用自定义音源解析歌曲 ID: ${id}`); const res = await getParsingMusicUrl(numericId, cloneDeep(songData)); console.log('res', res); // 验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[getSongUrl] 自定义音源解析后请求已失效: ${requestId}`); throw new Error('Request cancelled'); } if (res && res.data && res.data.data && res.data.data.url) { return res.data.data.url; } console.warn('自定义音源解析失败,使用默认音源'); } catch (error) { console.error('error', error); if ((error as Error).message === 'Request cancelled') { throw error; } console.error('自定义音源解析出错:', error); } } // 正常获取URL流程 const { data } = await getMusicUrl(numericId, isDownloaded); // 验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[getSongUrl] 获取官方URL后请求已失效: ${requestId}`); throw new Error('Request cancelled'); } if (data && data.data && data.data[0]) { const songDetail = data.data[0]; const hasNoUrl = !songDetail.url; const isTrial = !!songDetail.freeTrialInfo; if (hasNoUrl || isTrial) { console.log(`官方URL无效 (无URL: ${hasNoUrl}, 试听: ${isTrial}),进入内置备用解析...`); const res = await getParsingMusicUrl(numericId, cloneDeep(songData)); // 验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[getSongUrl] 备用解析后请求已失效: ${requestId}`); throw new Error('Request cancelled'); } if (isDownloaded) return res?.data?.data as any; return res?.data?.data?.url || null; } console.log('官方API解析成功!'); if (isDownloaded) return songDetail as any; return songDetail.url; } console.log('官方API返回数据结构异常,进入内置备用解析...'); const res = await getParsingMusicUrl(numericId, cloneDeep(songData)); // 验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[getSongUrl] 备用解析后请求已失效: ${requestId}`); throw new Error('Request cancelled'); } if (isDownloaded) return res?.data?.data as any; return res?.data?.data?.url || null; } catch (error) { if ((error as Error).message === 'Request cancelled') { throw error; } console.error('官方API请求失败,进入内置备用解析流程:', error); const res = await getParsingMusicUrl(numericId, cloneDeep(songData)); if (isDownloaded) return res?.data?.data as any; return res?.data?.data?.url || null; } }; /** * useSongUrl hook(兼容旧代码) */ export const useSongUrl = () => { return { getSongUrl }; }; /** * 使用新的yrcParser解析歌词(独立函数) */ const parseLyrics = (lyricsString: string): { lyrics: ILyricText[]; times: number[] } => { if (!lyricsString || typeof lyricsString !== 'string') { return { lyrics: [], times: [] }; } try { const parseResult = parseYrcLyrics(lyricsString); if (!parseResult.success) { console.error('歌词解析失败:', parseResult.error.message); return { lyrics: [], times: [] }; } const { lyrics: parsedLyrics } = parseResult.data; const lyrics: ILyricText[] = []; const times: number[] = []; for (const line of parsedLyrics) { // 检查是否有逐字歌词 const hasWords = line.words && line.words.length > 0; lyrics.push({ text: line.fullText, trText: '', // 翻译文本稍后处理 words: hasWords ? (line.words as IWordData[]) : undefined, hasWordByWord: hasWords, startTime: line.startTime, duration: line.duration }); // 时间数组使用秒为单位(与原有逻辑保持一致) times.push(line.startTime / 1000); } return { lyrics, times }; } catch (error) { console.error('解析歌词时发生错误:', error); return { lyrics: [], times: [] }; } }; /** * 加载歌词(独立函数) */ export const loadLrc = async (id: string | number): Promise => { if (typeof id === 'string' && id.includes('--')) { console.log('B站音频,无需加载歌词'); return { lrcTimeArray: [], lrcArray: [], hasWordByWord: false }; } try { const numericId = typeof id === 'string' ? parseInt(id, 10) : id; const { data } = await getMusicLrc(numericId); const { lyrics, times } = parseLyrics(data?.yrc?.lyric || data?.lrc?.lyric); // 检查是否有逐字歌词 let hasWordByWord = false; for (const lyric of lyrics) { if (lyric.hasWordByWord) { hasWordByWord = true; break; } } if (data.tlyric && data.tlyric.lyric) { const { lyrics: tLyrics } = parseLyrics(data.tlyric.lyric); // 按索引顺序一一对应翻译歌词 if (tLyrics.length === lyrics.length) { // 数量相同,直接按索引对应 lyrics.forEach((item, index) => { item.trText = item.text && tLyrics[index] ? tLyrics[index].text : ''; }); } else { // 数量不同,构建时间戳映射并尝试匹配 const tLyricMap = new Map(); tLyrics.forEach((lyric) => { if (lyric.text && lyric.startTime !== undefined) { const timeInSeconds = lyric.startTime / 1000; tLyricMap.set(timeInSeconds, lyric.text); } }); // 为每句歌词查找最接近的翻译 lyrics.forEach((item, index) => { if (!item.text) { item.trText = ''; return; } const currentTime = times[index]; let closestTime = -1; let minDiff = 2.0; // 最大允许差异2秒 // 查找最接近的时间戳 for (const [tTime] of tLyricMap.entries()) { const diff = Math.abs(tTime - currentTime); if (diff < minDiff) { minDiff = diff; closestTime = tTime; } } item.trText = closestTime !== -1 ? tLyricMap.get(closestTime) || '' : ''; }); } } else { // 没有翻译歌词,清空 trText lyrics.forEach((item) => { item.trText = ''; }); } return { lrcTimeArray: times, lrcArray: lyrics, hasWordByWord }; } catch (err) { console.error('Error loading lyrics:', err); return { lrcTimeArray: [], lrcArray: [], hasWordByWord: false }; } }; /** * useLyrics hook(兼容旧代码) */ export const useLyrics = () => { return { loadLrc, parseLyrics }; }; /** * 获取歌曲详情 */ export const useSongDetail = () => { const { getSongUrl } = useSongUrl(); const getSongDetail = async (playMusic: SongResult, requestId?: string) => { // 验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[getSongDetail] 请求已失效: ${requestId}`); throw new Error('Request cancelled'); } if (playMusic.source === 'bilibili') { try { if (!playMusic.playMusicUrl && playMusic.bilibiliData) { playMusic.playMusicUrl = await getBilibiliAudioUrl( playMusic.bilibiliData.bvid, playMusic.bilibiliData.cid ); } // 验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[getSongDetail] B站URL获取后请求已失效: ${requestId}`); throw new Error('Request cancelled'); } playMusic.playLoading = false; return { ...playMusic } as SongResult; } catch (error) { console.error('获取B站音频详情失败:', error); playMusic.playLoading = false; throw error; } } if (playMusic.expiredAt && playMusic.expiredAt < Date.now()) { console.info(`歌曲已过期,重新获取: ${playMusic.name}`); playMusic.playMusicUrl = undefined; } try { const playMusicUrl = playMusic.playMusicUrl || (await getSongUrl(playMusic.id, playMusic, false, requestId)); // 验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[getSongDetail] URL获取后请求已失效: ${requestId}`); throw new Error('Request cancelled'); } playMusic.createdAt = Date.now(); // 半小时后过期 playMusic.expiredAt = playMusic.createdAt + 1800000; const { backgroundColor, primaryColor } = playMusic.backgroundColor && playMusic.primaryColor ? playMusic : await getImageLinearBackground(getImgUrl(playMusic?.picUrl, '30y30')); // 验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[getSongDetail] 背景色获取后请求已失效: ${requestId}`); throw new Error('Request cancelled'); } playMusic.playLoading = false; return { ...playMusic, playMusicUrl, backgroundColor, primaryColor } as SongResult; } catch (error) { if ((error as Error).message === 'Request cancelled') { throw error; } console.error('获取音频URL失败:', error); playMusic.playLoading = false; throw error; } }; return { getSongDetail }; }; ================================================ FILE: src/renderer/hooks/useSongItem.ts ================================================ import { useMessage } from 'naive-ui'; import { computed, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { usePlayerStore, useRecommendStore } from '@/store'; import type { SongResult } from '@/types/music'; import { getImgUrl } from '@/utils'; import { getImageBackground } from '@/utils/linearColor'; import { dislikeRecommendedSong } from '../api/music'; import { useArtist } from './useArtist'; import { useDownload } from './useDownload'; export function useSongItem(props: { item: SongResult; canRemove?: boolean }) { const { t } = useI18n(); const playerStore = usePlayerStore(); const recommendStore = useRecommendStore(); const message = useMessage(); const { downloadMusic } = useDownload(); const { navigateToArtist } = useArtist(); // 状态变量 const showDropdown = ref(false); const dropdownX = ref(0); const dropdownY = ref(0); const isHovering = ref(false); // 计算属性 const play = computed(() => playerStore.isPlay); const playMusic = computed(() => playerStore.playMusic); const playLoading = computed( () => playMusic.value.id === props.item.id && playMusic.value.playLoading ); const isPlaying = computed(() => playMusic.value.id === props.item.id); // 收藏与不喜欢状态 const isFavorite = computed(() => { const numericId = typeof props.item.id === 'string' ? parseInt(props.item.id, 10) : props.item.id; return playerStore.favoriteList.includes(numericId); }); const isDislike = computed(() => { const numericId = typeof props.item.id === 'string' ? parseInt(props.item.id, 10) : props.item.id; return playerStore.dislikeList.includes(numericId); }); // 获取艺术家列表 const artists = computed(() => { return (props.item.ar || props.item.song?.artists)?.slice(0, 4) || []; }); // 处理图片加载 const handleImageLoad = async (imageElement: HTMLImageElement) => { if (!imageElement) return; const { backgroundColor, primaryColor } = await getImageBackground(imageElement); props.item.backgroundColor = backgroundColor; props.item.primaryColor = primaryColor; }; // 播放音乐 const playMusicEvent = async (item: SongResult) => { try { const result = await playerStore.setPlay(item); if (!result) { throw new Error('播放失败'); } return true; } catch (error) { console.error('播放出错:', error); return false; } }; // 切换收藏状态 const toggleFavorite = async (e: Event) => { e && e.stopPropagation(); const numericId = typeof props.item.id === 'string' ? parseInt(props.item.id, 10) : props.item.id; if (isFavorite.value) { playerStore.removeFromFavorite(numericId); } else { playerStore.addToFavorite(numericId); } }; // 判断当前歌曲是否为每日推荐歌曲 const isDailyRecommendSong = computed(() => { return recommendStore.dailyRecommendSongs.some((song) => song.id === props.item.id); }); // 切换不喜欢状态 const toggleDislike = async (e: Event) => { e && e.stopPropagation(); if (isDislike.value) { playerStore.removeFromDislikeList(props.item.id); return; } playerStore.addToDislikeList(props.item.id); // 只有当前歌曲是每日推荐歌曲时才调用接口 if (!isDailyRecommendSong.value) { return; } try { console.log('发送不感兴趣请求,歌曲ID:', props.item.id); const numericId = typeof props.item.id === 'string' ? parseInt(props.item.id) : props.item.id; const response = await dislikeRecommendedSong(numericId); if (response.data.data) { console.log(response); const newSongData = response.data.data; const newSong: SongResult = { ...newSongData, name: newSongData.name, id: newSongData.id, picUrl: newSongData.al?.picUrl || newSongData.album?.picUrl, ar: newSongData.ar || newSongData.artists, al: newSongData.al || newSongData.album, song: { ...newSongData.song, id: newSongData.id, name: newSongData.name, artists: newSongData.ar || newSongData.artists, album: newSongData.al || newSongData.album }, source: 'netease', count: 0 }; recommendStore.replaceSongInDailyRecommend(props.item.id, newSong); } else { console.warn('标记不感兴趣API成功,但未返回新歌曲。', response.data); } } catch (error) { console.error('发送不感兴趣请求时出错:', error); } }; // 添加到下一首播放 const handlePlayNext = () => { playerStore.addToNextPlay(props.item); message.success(t('songItem.message.addedToNextPlay')); }; // 获取歌曲时长 const getDuration = (item: SongResult): number => { if (item.duration) return item.duration; if (typeof item.dt === 'number') return item.dt; return 0; }; // 格式化时长 const formatDuration = (ms: number): string => { if (!ms) return '--:--'; const totalSeconds = Math.floor(ms / 1000); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; }; // 处理右键菜单 const handleContextMenu = (e: MouseEvent) => { e.preventDefault(); showDropdown.value = true; dropdownX.value = e.clientX; dropdownY.value = e.clientY; }; // 处理菜单点击 const handleMenuClick = (e: MouseEvent) => { e.preventDefault(); showDropdown.value = true; dropdownX.value = e.clientX; dropdownY.value = e.clientY; }; // 处理艺术家点击 const handleArtistClick = (id: number) => { navigateToArtist(id); }; // 鼠标悬停处理 const handleMouseEnter = () => { isHovering.value = true; }; const handleMouseLeave = () => { isHovering.value = false; }; return { t, play, playMusic, playLoading, isPlaying, isFavorite, isDislike, artists, showDropdown, dropdownX, dropdownY, isHovering, playerStore, message, getImgUrl, handleImageLoad, playMusicEvent, toggleFavorite, toggleDislike, handlePlayNext, getDuration, formatDuration, handleContextMenu, handleMenuClick, handleArtistClick, handleMouseEnter, handleMouseLeave, downloadMusic }; } ================================================ FILE: src/renderer/hooks/useZoom.ts ================================================ import { ref } from 'vue'; /** * 页面缩放功能的组合式API * 提供页面缩放相关的状态和方法 */ export function useZoom() { // 缩放相关常量 const MIN_ZOOM = 0.5; const MAX_ZOOM = 1.5; const ZOOM_STEP = 0.05; // 5%的步长 // 当前缩放因子 const zoomFactor = ref(1); // 初始化获取当前缩放比例 const initZoomFactor = async () => { try { const currentZoom = await window.ipcRenderer.invoke('get-content-zoom'); zoomFactor.value = currentZoom; } catch (error) { console.error('获取缩放比例失败:', error); } }; // 增加缩放比例,保证100%为节点 const increaseZoom = () => { let newZoom; // 如果当前缩放低于100%并且增加后会超过100%,则直接设为100% if (zoomFactor.value < 1.0 && zoomFactor.value + ZOOM_STEP > 1.0) { newZoom = 1.0; // 精确设置为100% } else { newZoom = Math.min(MAX_ZOOM, Math.round((zoomFactor.value + ZOOM_STEP) * 20) / 20); } setZoomFactor(newZoom); }; // 减少缩放比例,保证100%为节点 const decreaseZoom = () => { let newZoom; // 如果当前缩放大于100%并且减少后会低于100%,则直接设为100% if (zoomFactor.value > 1.0 && zoomFactor.value - ZOOM_STEP < 1.0) { newZoom = 1.0; // 精确设置为100% } else { newZoom = Math.max(MIN_ZOOM, Math.round((zoomFactor.value - ZOOM_STEP) * 20) / 20); } setZoomFactor(newZoom); }; // 重置缩放比例到系统建议值 const resetZoom = async () => { try { setZoomFactor(1); } catch (error) { console.error('重置缩放比例失败:', error); } }; // 设置为100%标准缩放 const setZoom100 = () => { setZoomFactor(1.0); }; // 设置缩放比例 const setZoomFactor = (zoom: number) => { window.ipcRenderer.send('set-content-zoom', zoom); zoomFactor.value = zoom; }; // 检查是否为100%缩放 const isZoom100 = () => { return Math.abs(zoomFactor.value - 1.0) < 0.001; }; return { zoomFactor, initZoomFactor, increaseZoom, decreaseZoom, resetZoom, setZoom100, setZoomFactor, isZoom100, MIN_ZOOM, MAX_ZOOM, ZOOM_STEP }; } ================================================ FILE: src/renderer/index.css ================================================ /* ./src/index.css */ /*! @import */ @tailwind base; @tailwind components; @tailwind utilities; .n-image img { background-color: #111111; width: 100%; } .n-slider-handle-indicator--top { @apply bg-transparent text-2xl px-2 py-1 shadow-none mb-0 text-white bg-dark-300 dark:bg-gray-800 bg-opacity-80 rounded-lg !important; mix-blend-mode: difference !important; } .v-binder-follower-container:has(.n-slider-handle-indicator--top) { z-index: 999999999 !important; } .text-el { @apply overflow-ellipsis overflow-hidden whitespace-nowrap; } .theme-dark { --bg-color: #000; --text-color: #fff; --bg-color-100: #161616; --bg-color-200: #2d2d2d; --bg-color-300: #3d3d3d; --text-color: #f8f9fa; --text-color-100: #e9ecef; --text-color-200: #dee2e6; --text-color-300: #dde0e3; --primary-color: #22c55e; } .theme-light { --bg-color: #fff; --text-color: #000; --bg-color-100: #f8f9fa; --bg-color-200: #e9ecef; --bg-color-300: #dee2e6; --text-color: #000; --text-color-100: #161616; --text-color-200: #2d2d2d; --text-color-300: #3d3d3d; --primary-color: #22c55e; } .theme-gray { --bg-color: #f8f9fa; --text-color: #000; --bg-color-100: #e9ecef; --bg-color-200: #dee2e6; --bg-color-300: #dde0e3; --text-color: #000; --text-color-100: #161616; --text-color-200: #2d2d2d; --text-color-300: #3d3d3d; --primary-color: #22c55e; } :root { --text-color: #000000dd; --safe-area-inset-top: 0px; --safe-area-inset-right: 0px; --safe-area-inset-bottom: 10px; --safe-area-inset-left: 0px; } :root[class='dark'] { --text-color: #ffffffdd; } ================================================ FILE: src/renderer/index.html ================================================ AlgerMusicPlayer | algerkong
================================================ FILE: src/renderer/layout/AppLayout.vue ================================================ ================================================ FILE: src/renderer/layout/MiniLayout.vue ================================================ ================================================ FILE: src/renderer/layout/MobileLayout.vue ================================================ ================================================ FILE: src/renderer/layout/components/AppMenu.vue ================================================ ================================================ FILE: src/renderer/layout/components/MobileHeader.vue ================================================ ================================================ FILE: src/renderer/layout/components/SearchBar.vue ================================================ ================================================ FILE: src/renderer/layout/components/TitleBar.vue ================================================ ================================================ FILE: src/renderer/layout/components/index.ts ================================================ import AppMenu from './AppMenu.vue'; import PlayBar from './PlayBar.vue'; import SearchBar from './SearchBar.vue'; export { AppMenu, PlayBar, SearchBar }; ================================================ FILE: src/renderer/main.ts ================================================ import './index.css'; import 'animate.css'; import 'remixicon/fonts/remixicon.css'; import { createApp } from 'vue'; import i18n from '@/../i18n/renderer'; import router from '@/router'; import pinia from '@/store'; import App from './App.vue'; import directives from './directive'; import { initAppShortcuts } from './utils/appShortcuts'; const app = createApp(App); Object.keys(directives).forEach((key: string) => { app.directive(key, directives[key as keyof typeof directives]); }); app.use(pinia); app.use(router); app.use(i18n as any); app.mount('#app'); // 初始化应用内快捷键 initAppShortcuts(); ================================================ FILE: src/renderer/router/home.ts ================================================ const layoutRouter = [ { path: '/', name: 'home', meta: { title: 'comp.home', icon: 'icon-Home', keepAlive: true, isMobile: true }, component: () => import('@/views/home/index.vue') }, { path: '/search', name: 'search', meta: { title: 'comp.search', noScroll: true, icon: 'icon-Search', keepAlive: true }, component: () => import('@/views/search/index.vue') }, { path: '/list', name: 'list', meta: { title: 'comp.list', icon: 'icon-Paper', keepAlive: true, isMobile: true }, component: () => import('@/views/list/index.vue') }, { path: '/toplist', name: 'toplist', meta: { title: 'comp.toplist', icon: 'ri-bar-chart-grouped-fill', keepAlive: true, isMobile: true }, component: () => import('@/views/toplist/index.vue') }, { path: '/mv', name: 'mv', meta: { title: 'comp.mv', icon: 'icon-recordfill', keepAlive: true, isMobile: false }, component: () => import('@/views/mv/index.vue') }, { path: '/history', name: 'history', component: () => import('@/views/historyAndFavorite/index.vue'), meta: { title: 'comp.history', icon: 'icon-a-TicketStar', keepAlive: true, isMobile: true } }, { path: '/user', name: 'user', meta: { title: 'comp.user', icon: 'icon-Profile', keepAlive: true, noScroll: true, isMobile: true }, component: () => import('@/views/user/index.vue') }, { path: '/set', name: 'set', meta: { title: 'comp.settings', icon: 'ri-settings-3-fill', keepAlive: true, noScroll: true, back: true }, component: () => import('@/views/set/index.vue') } ]; export default layoutRouter; ================================================ FILE: src/renderer/router/index.ts ================================================ import { createRouter, createWebHashHistory } from 'vue-router'; import AppLayout from '@/layout/AppLayout.vue'; import MiniLayout from '@/layout/MiniLayout.vue'; import homeRouter from '@/router/home'; import otherRouter from '@/router/other'; import { useSettingsStore } from '@/store/modules/settings'; import { useUserStore } from '../store/modules/user'; function getUserId(): string | null { const userStore = useUserStore(); return userStore.user?.userId?.toString() || null; } // 由于 Vue Router 守卫在创建前不能直接使用组合式 API // 我们创建一个辅助函数来获取 store 实例 let _settingsStore: ReturnType | null = null; const getSettingsStore = () => { if (!_settingsStore) { _settingsStore = useSettingsStore(); } return _settingsStore; }; const loginRouter = { path: '/login', name: 'login', meta: { keepAlive: true, title: '登录', icon: 'icon-Home', back: true }, component: () => import('@/views/login/index.vue') }; const routes = [ { path: '/', component: AppLayout, children: [...homeRouter, loginRouter, ...otherRouter] }, { path: '/lyric', component: () => import('@/views/lyric/index.vue') }, { path: '/mini', component: MiniLayout } ]; const router = createRouter({ routes, history: createWebHashHistory() }); // 添加全局前置守卫 router.beforeEach((to, _, next) => { const settingsStore = getSettingsStore(); // 如果是迷你模式 if (settingsStore.isMiniMode) { // 只允许访问 /mini 路由 if (to.path === '/mini') { next(); } else { next(false); // 阻止导航 } } else if (to.path === '/mini') { // 如果不是迷你模式但想访问 /mini 路由,重定向到首页 next('/'); } else { // 其他情况正常导航 next(); } }); // 添加全局后置钩子,记录页面访问 router.afterEach((to) => { const pageName = to.name?.toString() || to.path; // 使用setTimeout避免阻塞路由导航 setTimeout(() => { const userId = getUserId(); console.log('pageName', pageName, userId); }, 100); }); export default router; ================================================ FILE: src/renderer/router/other.ts ================================================ const otherRouter = [ { path: '/user/follows', name: 'userFollows', meta: { title: '关注列表', keepAlive: false, showInMenu: false, back: true }, component: () => import('@/views/user/follows.vue') }, { path: '/user/followers', name: 'userFollowers', meta: { title: '粉丝列表', keepAlive: false, showInMenu: false, back: true }, component: () => import('@/views/user/followers.vue') }, { path: '/downloads', name: 'downloads', meta: { title: '下载管理', keepAlive: true, showInMenu: true, back: true, icon: 'ri-download-cloud-2-line' }, component: () => import('@/views/download/DownloadPage.vue') }, { path: '/user/detail/:uid', name: 'userDetail', meta: { title: '用户详情', keepAlive: false, showInMenu: false, back: true }, component: () => import('@/views/user/detail.vue') }, { path: '/artist/detail/:id', name: 'artistDetail', meta: { title: '歌手详情', keepAlive: true, showInMenu: false, back: true }, component: () => import('@/views/artist/detail.vue') }, { path: '/bilibili/:bvid', name: 'bilibiliPlayer', meta: { title: 'B站听书', keepAlive: true, showInMenu: false, back: true }, component: () => import('@/views/bilibili/BilibiliPlayer.vue') }, { path: '/music-list/:id?', name: 'musicList', meta: { title: '音乐列表', keepAlive: false, showInMenu: false, back: true }, component: () => import('@/views/music/MusicListPage.vue') }, { path: '/playlist/import', name: 'playlistImport', meta: { title: '歌单导入', keepAlive: true, back: true }, component: () => import('@/views/playlist/ImportPlaylist.vue') }, { path: '/heatmap', name: 'heatmap', meta: { title: '播放热力图', keepAlive: true, showInMenu: false, back: true }, component: () => import('@/views/heatmap/index.vue') }, { path: '/history-recommend', name: 'historyRecommend', meta: { title: '历史日推', keepAlive: true, showInMenu: false, back: true }, component: () => import('@/views/music/HistoryRecommend.vue') }, { path: '/mobile-search', name: 'mobileSearch', meta: { title: '搜索', keepAlive: false, showInMenu: false, back: true }, component: () => import('@/views/mobile-search/index.vue') }, { path: '/mobile-search-result', name: 'mobileSearchResult', meta: { title: '搜索结果', keepAlive: false, showInMenu: false, back: true }, component: () => import('@/views/mobile-search-result/index.vue') } ]; export default otherRouter; ================================================ FILE: src/renderer/services/LxMusicSourceRunner.ts ================================================ /** * 落雪音乐 (LX Music) 音源脚本执行器 * * 核心职责: * 1. 解析脚本元信息 * 2. 在隔离环境中执行用户脚本 * 3. 模拟 globalThis.lx API * 4. 处理初始化和音乐解析请求 */ import type { LxInitedData, LxLyricResult, LxMusicInfo, LxQuality, LxScriptInfo, LxSourceConfig, LxSourceKey } from '@/types/lxMusic'; import * as lxCrypto from '@/utils/lxCrypto'; /** * 解析脚本头部注释中的元信息 */ export const parseScriptInfo = (script: string): LxScriptInfo => { const info: LxScriptInfo = { name: '未知音源', rawScript: script }; // 尝试匹配不同格式的头部注释块 // 支持 /** ... */ 和 /* ... */ 格式 const headerMatch = script.match(/^\/\*+[\s\S]*?\*\//); if (!headerMatch) { console.warn('[parseScriptInfo] 未找到脚本头部注释块'); return info; } const header = headerMatch[0]; console.log('[parseScriptInfo] 解析脚本头部:', header.substring(0, 200)); // 解析各个字段(支持 * 前缀和无前缀两种格式) const nameMatch = header.match(/@name\s+(.+?)(?:\r?\n|\*\/)/); if (nameMatch) { info.name = nameMatch[1].trim().replace(/^\*\s*/, ''); console.log('[parseScriptInfo] 解析到名称:', info.name); } else { console.warn('[parseScriptInfo] 未找到 @name 标签'); } const descMatch = header.match(/@description\s+(.+?)(?:\r?\n|\*\/)/); if (descMatch) { info.description = descMatch[1].trim().replace(/^\*\s*/, ''); } const versionMatch = header.match(/@version\s+(.+?)(?:\r?\n|\*\/)/); if (versionMatch) { info.version = versionMatch[1].trim().replace(/^\*\s*/, ''); console.log('[parseScriptInfo] 解析到版本:', info.version); } const authorMatch = header.match(/@author\s+(.+?)(?:\r?\n|\*\/)/); if (authorMatch) { info.author = authorMatch[1].trim().replace(/^\*\s*/, ''); } const homepageMatch = header.match(/@homepage\s+(.+?)(?:\r?\n|\*\/)/); if (homepageMatch) { info.homepage = homepageMatch[1].trim().replace(/^\*\s*/, ''); } return info; }; /** * 落雪音源脚本执行器 * 使用 Worker 或 iframe 隔离执行用户脚本 */ export class LxMusicSourceRunner { private script: string; private scriptInfo: LxScriptInfo; private sources: Partial> = {}; private requestHandler: ((data: any) => Promise) | null = null; private initialized = false; private initPromise: Promise | null = null; // 临时存储最后一次 HTTP 请求返回的音乐 URL(用于脚本返回 undefined 时的后备) private lastMusicUrl: string | null = null; constructor(script: string) { this.script = script; this.scriptInfo = parseScriptInfo(script); } /** * 获取脚本信息 */ getScriptInfo(): LxScriptInfo { return this.scriptInfo; } /** * 获取支持的音源列表 */ getSources(): Partial> { return this.sources; } /** * 初始化执行器 */ async initialize(): Promise { if (this.initPromise) return this.initPromise; this.initPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('脚本初始化超时')); }, 10000); try { // 创建沙盒环境并执行脚本 this.executeSandboxed( (initedData) => { clearTimeout(timeout); this.sources = initedData.sources; this.initialized = true; console.log('[LxMusicRunner] 初始化成功:', initedData.sources); resolve(initedData); }, (error) => { clearTimeout(timeout); reject(error); } ); } catch (error) { clearTimeout(timeout); reject(error); } }); return this.initPromise; } /** * 在沙盒中执行脚本 */ private executeSandboxed( onInited: (data: LxInitedData) => void, onError: (error: Error) => void ): void { // 构建沙盒执行环境 const sandbox = this.createSandbox(onInited, onError); try { // 使用 Function 构造器在受限环境中执行 // 注意:不能使用 const/let 声明 globalThis,因为它是保留标识符 const sandboxedScript = ` (function() { ${sandbox.apiSetup} ${this.script} }).call(this); `; // 创建执行上下文 const context = sandbox.context; const executor = new Function(sandboxedScript); // 在隔离上下文中执行,context 将作为 this executor.call(context); } catch (error) { onError(error as Error); } } /** * 创建沙盒环境 */ private createSandbox( onInited: (data: LxInitedData) => void, _onError: (error: Error) => void ): { apiSetup: string; context: any } { const self = this; // 创建 globalThis.lx 对象 // 版本号使用落雪音乐最新版本以通过脚本版本检测 const context = { lx: { version: '2.8.0', env: 'desktop', appInfo: { version: '2.8.0', versionNum: 208, locale: 'zh-cn' }, currentScriptInfo: this.scriptInfo, EVENT_NAMES: { inited: 'inited', request: 'request', updateAlert: 'updateAlert' }, on: (eventName: string, handler: (data: any) => Promise) => { if (eventName === 'request') { self.requestHandler = handler; } }, send: (eventName: string, data: any) => { if (eventName === 'inited') { onInited(data as LxInitedData); } else if (eventName === 'updateAlert') { console.log('[LxMusicRunner] 更新提醒:', data); } }, request: ( url: string, options: any, callback: (err: Error | null, resp: any, body: any) => void ) => { return self.handleHttpRequest(url, options, callback); }, utils: { buffer: { from: (data: any, _encoding?: string) => { if (typeof data === 'string') { return new TextEncoder().encode(data); } return new Uint8Array(data); }, bufToString: (buffer: Uint8Array, encoding?: string) => { return new TextDecoder(encoding || 'utf-8').decode(buffer); } }, crypto: { md5: lxCrypto.md5, sha1: lxCrypto.sha1, sha256: lxCrypto.sha256, randomBytes: lxCrypto.randomBytes, aesEncrypt: lxCrypto.aesEncrypt, aesDecrypt: lxCrypto.aesDecrypt, rsaEncrypt: lxCrypto.rsaEncrypt, rsaDecrypt: lxCrypto.rsaDecrypt, base64Encode: lxCrypto.base64Encode, base64Decode: lxCrypto.base64Decode }, zlib: { inflate: async (buffer: ArrayBuffer) => { try { const ds = new DecompressionStream('deflate'); const writer = ds.writable.getWriter(); writer.write(buffer); writer.close(); const reader = ds.readable.getReader(); const chunks: Uint8Array[] = []; let done = false; while (!done) { const result = await reader.read(); done = result.done; if (result.value) chunks.push(result.value); } const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result.buffer; } catch { return buffer; } }, deflate: async (buffer: ArrayBuffer) => { try { const cs = new CompressionStream('deflate'); const writer = cs.writable.getWriter(); writer.write(buffer); writer.close(); const reader = cs.readable.getReader(); const chunks: Uint8Array[] = []; let done = false; while (!done) { const result = await reader.read(); done = result.done; if (result.value) chunks.push(result.value); } const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0); const result = new Uint8Array(totalLength); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result.buffer; } catch { return buffer; } } } } }, console: { log: (...args: any[]) => console.log('[LxScript]', ...args), error: (...args: any[]) => console.error('[LxScript]', ...args), warn: (...args: any[]) => console.warn('[LxScript]', ...args), info: (...args: any[]) => console.info('[LxScript]', ...args) }, setTimeout, setInterval, clearTimeout, clearInterval, Promise, JSON, Object, Array, String, Number, Boolean, Date, Math, RegExp, Error, Map, Set, WeakMap, WeakSet, Symbol, Proxy, Reflect, encodeURIComponent, decodeURIComponent, encodeURI, decodeURI, atob, btoa, TextEncoder, TextDecoder, Uint8Array, ArrayBuffer, crypto }; // 只设置 lx 和 globalThis,不解构变量避免与脚本内部声明冲突 const apiSetup = ` var lx = this.lx; var globalThis = this; `; return { apiSetup, context }; } /** * 处理 HTTP 请求(优先使用主进程,绕过 CORS 限制) */ private handleHttpRequest( url: string, options: any, callback: (err: Error | null, resp: any, body: any) => void ): () => void { console.log(`[LxMusicRunner] HTTP 请求: ${options.method || 'GET'} ${url}`); const timeout = options.timeout || 30000; const requestId = `lx_http_${Date.now()}_${Math.random().toString(36).substring(7)}`; // 尝试使用主进程 HTTP 请求(如果可用) const hasMainProcessHttp = typeof window.api?.lxMusicHttpRequest === 'function'; if (hasMainProcessHttp) { // 使用主进程 HTTP 请求(绕过 CORS) console.log(`[LxMusicRunner] 使用主进程 HTTP 请求`); window.api .lxMusicHttpRequest({ url, options: { ...options, timeout }, requestId }) .then((response: any) => { console.log(`[LxMusicRunner] HTTP 响应: ${response.statusCode} ${url}`); // 如果响应中包含 URL,缓存下来以备后用 if (response.body && response.body.url && typeof response.body.url === 'string') { this.lastMusicUrl = response.body.url; } callback(null, response, response.body); }) .catch((error: Error) => { console.error(`[LxMusicRunner] HTTP 请求失败: ${url}`, error.message); callback(error, null, null); }); // 返回取消函数 return () => { void window.api?.lxMusicHttpCancel?.(requestId); }; } else { // 回退到渲染进程 fetch(可能受 CORS 限制) console.log(`[LxMusicRunner] 主进程 HTTP 不可用,使用渲染进程 fetch`); const controller = new AbortController(); const fetchOptions: RequestInit = { method: options.method || 'GET', headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', ...options.headers }, signal: controller.signal, mode: 'cors', credentials: 'omit' }; if (options.body) { fetchOptions.body = options.body; } else if (options.form) { fetchOptions.body = new URLSearchParams(options.form); fetchOptions.headers = { ...fetchOptions.headers, 'Content-Type': 'application/x-www-form-urlencoded' }; } else if (options.formData) { const formData = new FormData(); for (const [key, value] of Object.entries(options.formData)) { formData.append(key, value as string); } fetchOptions.body = formData; } const timeoutId = setTimeout(() => { console.warn(`[LxMusicRunner] HTTP 请求超时: ${url}`); controller.abort(); }, timeout); fetch(url, fetchOptions) .then(async (response) => { clearTimeout(timeoutId); console.log(`[LxMusicRunner] HTTP 响应: ${response.status} ${url}`); const rawBody = await response.text(); // 尝试解析 JSON let parsedBody: any = rawBody; const contentType = response.headers.get('content-type') || ''; if ( contentType.includes('application/json') || rawBody.startsWith('{') || rawBody.startsWith('[') ) { try { parsedBody = JSON.parse(rawBody); if (parsedBody && parsedBody.url && typeof parsedBody.url === 'string') { this.lastMusicUrl = parsedBody.url; } } catch { // 解析失败则使用原始字符串 } } callback( null, { statusCode: response.status, headers: Object.fromEntries(response.headers.entries()), body: parsedBody }, parsedBody ); }) .catch((error) => { clearTimeout(timeoutId); console.error(`[LxMusicRunner] HTTP 请求失败: ${url}`, error.message); callback(error, null, null); }); // 返回取消函数 return () => controller.abort(); } } /** * 获取音乐 URL */ async getMusicUrl( source: LxSourceKey, musicInfo: LxMusicInfo, quality: LxQuality ): Promise { if (!this.initialized) { await this.initialize(); } if (!this.requestHandler) { throw new Error('脚本未注册请求处理器'); } const sourceConfig = this.sources[source]; if (!sourceConfig) { throw new Error(`脚本不支持音源: ${source}`); } if (!sourceConfig.actions.includes('musicUrl')) { throw new Error(`音源 ${source} 不支持获取音乐 URL`); } // 选择最佳音质 let targetQuality = quality; if (!sourceConfig.qualitys.includes(quality)) { // 按优先级选择可用音质 const qualityPriority: LxQuality[] = ['flac24bit', 'flac', '320k', '128k']; for (const q of qualityPriority) { if (sourceConfig.qualitys.includes(q)) { targetQuality = q; break; } } } console.log(`[LxMusicRunner] 请求音乐 URL: 音源=${source}, 音质=${targetQuality}`); try { const result = await this.requestHandler({ source, action: 'musicUrl', info: { type: targetQuality, musicInfo } }); console.log(`[LxMusicRunner] 脚本返回结果:`, result, typeof result); // 脚本可能返回对象或字符串 let url: string | undefined; if (typeof result === 'string') { url = result; } else if (result && typeof result === 'object') { // 某些脚本可能返回 { url: '...' } 格式 url = result.url || result.data || result; } if (typeof url !== 'string' || !url) { // 如果脚本返回 undefined,尝试使用缓存的 URL if (this.lastMusicUrl) { console.log('[LxMusicRunner] 脚本返回 undefined,使用缓存的 URL'); url = this.lastMusicUrl; this.lastMusicUrl = null; // 清除缓存 } else { console.error('[LxMusicRunner] 无效的返回值:', result); throw new Error(result?.message || result?.msg || '获取音乐 URL 失败'); } } console.log('[LxMusicRunner] 获取到 URL:', url.substring(0, 80) + '...'); return url; } catch (error) { console.error('[LxMusicRunner] 获取音乐 URL 失败:', error); throw error; } } /** * 获取歌词 */ async getLyric(source: LxSourceKey, musicInfo: LxMusicInfo): Promise { if (!this.initialized) { await this.initialize(); } if (!this.requestHandler) { return null; } const sourceConfig = this.sources[source]; if (!sourceConfig || !sourceConfig.actions.includes('lyric')) { return null; } try { const result = await this.requestHandler({ source, action: 'lyric', info: { type: null, musicInfo } }); return result as LxLyricResult; } catch (error) { console.error('[LxMusicRunner] 获取歌词失败:', error); return null; } } /** * 获取封面图 */ async getPic(source: LxSourceKey, musicInfo: LxMusicInfo): Promise { if (!this.initialized) { await this.initialize(); } if (!this.requestHandler) { return null; } const sourceConfig = this.sources[source]; if (!sourceConfig || !sourceConfig.actions.includes('pic')) { return null; } try { const url = await this.requestHandler({ source, action: 'pic', info: { type: null, musicInfo } }); return typeof url === 'string' ? url : null; } catch (error) { console.error('[LxMusicRunner] 获取封面失败:', error); return null; } } /** * 检查是否已初始化 */ isInitialized(): boolean { return this.initialized; } } // 全局单例 let runnerInstance: LxMusicSourceRunner | null = null; /** * 获取落雪音源执行器实例 */ export const getLxMusicRunner = (): LxMusicSourceRunner | null => { return runnerInstance; }; /** * 设置落雪音源执行器实例 */ export const setLxMusicRunner = (runner: LxMusicSourceRunner | null): void => { runnerInstance = runner; }; /** * 初始化落雪音源执行器(从脚本内容) */ export const initLxMusicRunner = async (script: string): Promise => { // 销毁旧实例 runnerInstance = null; // 创建新实例 const runner = new LxMusicSourceRunner(script); await runner.initialize(); runnerInstance = runner; return runner; }; ================================================ FILE: src/renderer/services/SongSourceConfigManager.ts ================================================ /** * 歌曲音源配置管理器 * * 职责: * 1. 统一管理每首歌曲的自定义音源配置 * 2. 提供清晰的读取/写入/清除 API * 3. 区分"手动"和"自动"设置的音源 * 4. 管理已尝试的音源列表(按歌曲隔离) */ import type { Platform } from '@/types/music'; // 歌曲音源配置类型 export type SongSourceConfig = { sources: Platform[]; type: 'manual' | 'auto'; updatedAt: number; }; // 内存中缓存已尝试的音源(按歌曲隔离) const triedSourcesMap = new Map>(); const triedSourceDiffsMap = new Map>(); // localStorage key 前缀 const STORAGE_KEY_PREFIX = 'song_source_'; const STORAGE_TYPE_KEY_PREFIX = 'song_source_type_'; /** * 歌曲音源配置管理器 */ export class SongSourceConfigManager { /** * 获取歌曲的自定义音源配置 */ static getConfig(songId: number | string): SongSourceConfig | null { const id = String(songId); const sourcesStr = localStorage.getItem(`${STORAGE_KEY_PREFIX}${id}`); const typeStr = localStorage.getItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`); if (!sourcesStr) { return null; } try { const sources = JSON.parse(sourcesStr) as Platform[]; if (!Array.isArray(sources) || sources.length === 0) { return null; } return { sources, type: typeStr === 'auto' ? 'auto' : 'manual', updatedAt: Date.now() }; } catch (error) { console.error(`[SongSourceConfigManager] 解析歌曲 ${id} 配置失败:`, error); return null; } } /** * 设置歌曲的自定义音源配置 */ static setConfig( songId: number | string, sources: Platform[], type: 'manual' | 'auto' = 'manual' ): void { const id = String(songId); if (!sources || sources.length === 0) { this.clearConfig(songId); return; } try { localStorage.setItem(`${STORAGE_KEY_PREFIX}${id}`, JSON.stringify(sources)); localStorage.setItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`, type); console.log(`[SongSourceConfigManager] 设置歌曲 ${id} 音源: ${sources.join(', ')} (${type})`); } catch (error) { console.error(`[SongSourceConfigManager] 保存歌曲 ${id} 配置失败:`, error); } } /** * 清除歌曲的自定义配置 */ static clearConfig(songId: number | string): void { const id = String(songId); localStorage.removeItem(`${STORAGE_KEY_PREFIX}${id}`); localStorage.removeItem(`${STORAGE_TYPE_KEY_PREFIX}${id}`); // 同时清除内存中的已尝试音源 this.clearTriedSources(songId); console.log(`[SongSourceConfigManager] 清除歌曲 ${id} 配置`); } /** * 检查歌曲是否有自定义配置 */ static hasConfig(songId: number | string): boolean { return this.getConfig(songId) !== null; } /** * 检查配置类型是否为手动设置 */ static isManualConfig(songId: number | string): boolean { const config = this.getConfig(songId); return config?.type === 'manual'; } // ==================== 已尝试音源管理 ==================== /** * 获取歌曲已尝试的音源列表 */ static getTriedSources(songId: number | string): Set { const id = String(songId); if (!triedSourcesMap.has(id)) { triedSourcesMap.set(id, new Set()); } return triedSourcesMap.get(id)!; } /** * 添加已尝试的音源 */ static addTriedSource(songId: number | string, source: string): void { const id = String(songId); const tried = this.getTriedSources(id); tried.add(source); console.log(`[SongSourceConfigManager] 歌曲 ${id} 添加已尝试音源: ${source}`); } /** * 清除歌曲的已尝试音源 */ static clearTriedSources(songId: number | string): void { const id = String(songId); triedSourcesMap.delete(id); triedSourceDiffsMap.delete(id); console.log(`[SongSourceConfigManager] 清除歌曲 ${id} 已尝试音源`); } /** * 获取歌曲已尝试音源的时长差异 */ static getTriedSourceDiffs(songId: number | string): Map { const id = String(songId); if (!triedSourceDiffsMap.has(id)) { triedSourceDiffsMap.set(id, new Map()); } return triedSourceDiffsMap.get(id)!; } /** * 设置音源的时长差异 */ static setTriedSourceDiff(songId: number | string, source: string, diff: number): void { const id = String(songId); const diffs = this.getTriedSourceDiffs(id); diffs.set(source, diff); } /** * 查找最佳匹配的音源(时长差异最小) */ static findBestMatchingSource(songId: number | string): { source: string; diff: number } | null { const diffs = this.getTriedSourceDiffs(songId); if (diffs.size === 0) { return null; } let bestSource = ''; let minDiff = Infinity; for (const [source, diff] of diffs.entries()) { if (diff < minDiff) { minDiff = diff; bestSource = source; } } return bestSource ? { source: bestSource, diff: minDiff } : null; } /** * 清理所有内存缓存(用于测试或重置) */ static clearAllMemoryCache(): void { triedSourcesMap.clear(); triedSourceDiffsMap.clear(); console.log('[SongSourceConfigManager] 清除所有内存缓存'); } } // 导出单例实例方便使用 export const songSourceConfig = SongSourceConfigManager; ================================================ FILE: src/renderer/services/audioService.ts ================================================ import { Howl, Howler } from 'howler'; import type { SongResult } from '@/types/music'; import { isElectron } from '@/utils'; // 导入isElectron常量 class AudioService { private currentSound: Howl | null = null; private pendingSound: Howl | null = null; private currentTrack: SongResult | null = null; private context: AudioContext | null = null; private filters: BiquadFilterNode[] = []; private source: MediaElementAudioSourceNode | null = null; private gainNode: GainNode | null = null; private bypass = false; private playbackRate = 1.0; // 添加播放速度属性 // 预设的 EQ 频段 private readonly frequencies = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]; // 默认的 EQ 设置 private defaultEQSettings: { [key: string]: number } = { '31': 0, '62': 0, '125': 0, '250': 0, '500': 0, '1000': 0, '2000': 0, '4000': 0, '8000': 0, '16000': 0 }; private retryCount = 0; private seekLock = false; private seekDebounceTimer: NodeJS.Timeout | null = null; // 添加操作锁防止并发操作 private operationLock = false; private operationLockTimer: NodeJS.Timeout | null = null; private operationLockTimeout = 5000; // 5秒超时 private operationLockStartTime: number = 0; private operationLockId: string = ''; constructor() { if ('mediaSession' in navigator) { this.initMediaSession(); } // 从本地存储加载 EQ 开关状态 const bypassState = localStorage.getItem('eqBypass'); this.bypass = bypassState ? JSON.parse(bypassState) : false; // 页面加载时立即强制重置操作锁 this.forceResetOperationLock(); // 添加页面卸载事件,确保离开页面时清除锁 window.addEventListener('beforeunload', () => { this.forceResetOperationLock(); }); } private initMediaSession() { navigator.mediaSession.setActionHandler('play', () => { this.currentSound?.play(); }); navigator.mediaSession.setActionHandler('pause', () => { this.currentSound?.pause(); }); navigator.mediaSession.setActionHandler('stop', () => { this.stop(); }); navigator.mediaSession.setActionHandler('seekto', (event) => { if (event.seekTime && this.currentSound) { // this.currentSound.seek(event.seekTime); this.seek(event.seekTime); } }); navigator.mediaSession.setActionHandler('seekbackward', (event) => { if (this.currentSound) { const currentTime = this.currentSound.seek() as number; this.seek(currentTime - (event.seekOffset || 10)); } }); navigator.mediaSession.setActionHandler('seekforward', (event) => { if (this.currentSound) { const currentTime = this.currentSound.seek() as number; this.seek(currentTime + (event.seekOffset || 10)); } }); navigator.mediaSession.setActionHandler('previoustrack', () => { // 这里需要通过回调通知外部 this.emit('previoustrack'); }); navigator.mediaSession.setActionHandler('nexttrack', () => { // 这里需要通过回调通知外部 this.emit('nexttrack'); }); } private updateMediaSessionMetadata(track: SongResult) { try { if (!('mediaSession' in navigator)) return; const artists = track.ar ? track.ar.map((a) => a.name) : track.song.artists?.map((a) => a.name); const album = track.al ? track.al.name : track.song.album.name; const artwork = ['96', '128', '192', '256', '384', '512'].map((size) => ({ src: `${track.picUrl}?param=${size}y${size}`, type: 'image/jpg', sizes: `${size}x${size}` })); const metadata = { title: track.name || '', artist: artists ? artists.join(',') : '', album: album || '', artwork }; navigator.mediaSession.metadata = new window.MediaMetadata(metadata); } catch (error) { console.error('更新媒体会话元数据时出错:', error); } } private updateMediaSessionState(isPlaying: boolean) { if (!('mediaSession' in navigator)) return; navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused'; this.updateMediaSessionPositionState(); } private updateMediaSessionPositionState() { try { if (!this.currentSound || !('mediaSession' in navigator)) return; if ('setPositionState' in navigator.mediaSession) { navigator.mediaSession.setPositionState({ duration: this.currentSound.duration(), playbackRate: this.playbackRate, position: this.currentSound.seek() as number }); } } catch (error) { console.error('更新媒体会话位置状态时出错:', error); } } // 事件处理相关 private callbacks: { [key: string]: Function[] } = {}; private emit(event: string, ...args: any[]) { const eventCallbacks = this.callbacks[event]; if (eventCallbacks) { eventCallbacks.forEach((callback) => callback(...args)); } } on(event: string, callback: Function) { if (!this.callbacks[event]) { this.callbacks[event] = []; } this.callbacks[event].push(callback); } off(event: string, callback: Function) { const eventCallbacks = this.callbacks[event]; if (eventCallbacks) { this.callbacks[event] = eventCallbacks.filter((cb) => cb !== callback); } } // EQ 相关方法 public isEQEnabled(): boolean { return !this.bypass; } public setEQEnabled(enabled: boolean) { this.bypass = !enabled; localStorage.setItem('eqBypass', JSON.stringify(this.bypass)); if (this.source && this.gainNode && this.context) { this.applyBypassState(); } } public setEQFrequencyGain(frequency: string, gain: number) { const filterIndex = this.frequencies.findIndex((f) => f.toString() === frequency); if (filterIndex !== -1 && this.filters[filterIndex]) { this.filters[filterIndex].gain.setValueAtTime(gain, this.context?.currentTime || 0); this.saveEQSettings(frequency, gain); } } public resetEQ() { this.filters.forEach((filter) => { filter.gain.setValueAtTime(0, this.context?.currentTime || 0); }); localStorage.removeItem('eqSettings'); } public getAllEQSettings(): { [key: string]: number } { return this.loadEQSettings(); } private saveEQSettings(frequency: string, gain: number) { const settings = this.loadEQSettings(); settings[frequency] = gain; localStorage.setItem('eqSettings', JSON.stringify(settings)); } private loadEQSettings(): { [key: string]: number } { const savedSettings = localStorage.getItem('eqSettings'); return savedSettings ? JSON.parse(savedSettings) : { ...this.defaultEQSettings }; } private async disposeEQ(keepContext = false) { try { // 清理音频节点连接 if (this.source) { this.source.disconnect(); this.source = null; } // 清理滤波器 this.filters.forEach((filter) => { try { filter.disconnect(); } catch (e) { console.warn('清理滤波器时出错:', e); } }); this.filters = []; // 清理增益节点 if (this.gainNode) { this.gainNode.disconnect(); this.gainNode = null; } // 如果不需要保持上下文,则关闭它 if (!keepContext && this.context) { try { await this.context.close(); this.context = null; } catch (e) { console.warn('关闭音频上下文时出错:', e); } } } catch (error) { console.error('清理EQ资源时出错:', error); } } private async setupEQ(sound: Howl) { try { if (!isElectron) { console.log('Web环境中跳过EQ设置,避免CORS问题'); this.bypass = true; return; } const howl = sound as any; const audioNode = howl._sounds?.[0]?._node; if (!audioNode || !(audioNode instanceof HTMLMediaElement)) { if (this.retryCount < 3) { console.warn('等待音频节点初始化,重试次数:', this.retryCount + 1); await new Promise((resolve) => setTimeout(resolve, 100)); this.retryCount++; return await this.setupEQ(sound); } throw new Error('无法获取音频节点,请重试'); } this.retryCount = 0; // 确保使用 Howler 的音频上下文 this.context = Howler.ctx as AudioContext; if (!this.context || this.context.state === 'closed') { Howler.ctx = new AudioContext(); this.context = Howler.ctx; Howler.masterGain = this.context.createGain(); Howler.masterGain.connect(this.context.destination); } if (this.context.state === 'suspended') { await this.context.resume(); } // 清理现有连接 await this.disposeEQ(true); try { // 检查节点是否已经有源 const existingSource = (audioNode as any).source as MediaElementAudioSourceNode; if (existingSource?.context === this.context) { console.log('复用现有音频源节点'); this.source = existingSource; } else { // 创建新的源节点 console.log('创建新的音频源节点'); this.source = this.context.createMediaElementSource(audioNode); (audioNode as any).source = this.source; } } catch (e) { console.error('创建音频源节点失败:', e); throw e; } // 创建增益节点 this.gainNode = this.context.createGain(); // 创建滤波器 this.filters = this.frequencies.map((freq) => { const filter = this.context!.createBiquadFilter(); filter.type = 'peaking'; filter.frequency.value = freq; filter.Q.value = 1; filter.gain.value = this.loadEQSettings()[freq.toString()] || 0; return filter; }); // 应用EQ状态 this.applyBypassState(); // 从 localStorage 应用音量到增益节点 const savedVolume = localStorage.getItem('volume'); if (savedVolume) { this.applyVolume(parseFloat(savedVolume)); } else { this.applyVolume(1); } console.log('EQ initialization successful'); } catch (error) { console.error('EQ initialization failed:', error); await this.disposeEQ(); throw error; } } private applyBypassState() { if (!this.source || !this.gainNode || !this.context) return; try { // 断开所有现有连接 this.source.disconnect(); this.filters.forEach((filter) => filter.disconnect()); this.gainNode.disconnect(); if (this.bypass) { // EQ被禁用时,直接连接到输出 this.source.connect(this.gainNode); this.gainNode.connect(this.context.destination); } else { // EQ启用时,通过滤波器链连接 this.source.connect(this.filters[0]); this.filters.forEach((filter, index) => { if (index < this.filters.length - 1) { filter.connect(this.filters[index + 1]); } }); this.filters[this.filters.length - 1].connect(this.gainNode); this.gainNode.connect(this.context.destination); } } catch (error) { console.error('应用EQ状态时出错:', error); } } // 设置操作锁,带超时自动释放 private setOperationLock(): boolean { // 生成唯一的锁ID const lockId = Date.now().toString() + Math.random().toString(36).substring(2, 9); // 如果锁已经存在,检查是否超时 if (this.operationLock) { const currentTime = Date.now(); const lockDuration = currentTime - this.operationLockStartTime; // 如果锁持续时间超过2秒,直接强制重置 if (lockDuration > 2000) { console.warn(`操作锁已激活 ${lockDuration}ms,超过安全阈值,强制重置`); this.forceResetOperationLock(); } else { console.log(`操作锁激活中,持续时间 ${lockDuration}ms`); return false; } } this.operationLock = true; this.operationLockStartTime = Date.now(); this.operationLockId = lockId; // 将锁信息存储到 localStorage(仅用于调试,实际不依赖此值) try { localStorage.setItem( 'audioOperationLock', JSON.stringify({ id: this.operationLockId, startTime: this.operationLockStartTime }) ); } catch (error) { console.error('存储操作锁信息失败:', error); } // 清除之前的定时器 if (this.operationLockTimer) { clearTimeout(this.operationLockTimer); } // 设置超时自动释放锁 this.operationLockTimer = setTimeout(() => { console.warn('操作锁超时自动释放'); this.releaseOperationLock(); }, this.operationLockTimeout); return true; } // 释放操作锁 public releaseOperationLock(): void { this.operationLock = false; this.operationLockStartTime = 0; // 从 localStorage 中移除锁信息 try { localStorage.removeItem('audioOperationLock'); } catch (error) { console.error('清除存储的操作锁信息失败:', error); } if (this.operationLockTimer) { clearTimeout(this.operationLockTimer); this.operationLockTimer = null; } } // 强制重置操作锁,用于特殊情况 public forceResetOperationLock(): void { console.log('强制重置操作锁'); this.operationLock = false; this.operationLockStartTime = 0; this.operationLockId = ''; if (this.operationLockTimer) { clearTimeout(this.operationLockTimer); this.operationLockTimer = null; } // 清除存储的锁 localStorage.removeItem('audioOperationLock'); } // 播放控制相关 public play( url: string, track: SongResult, isPlay: boolean = true, seekTime: number = 0, existingSound?: Howl ): Promise { // 每次调用play方法时,尝试强制重置锁(注意:仅在页面刷新后的第一次播放时应用) if (!this.currentSound) { console.log('首次播放请求,强制重置操作锁'); this.forceResetOperationLock(); } // 如果有操作锁,且不是同一个 track 的操作,则等待 if (this.operationLock) { console.log('audioService: 操作锁激活中,等待...'); return Promise.reject(new Error('操作锁激活中')); } if (!this.setOperationLock()) { console.log('audioService: 获取操作锁失败'); return Promise.reject(new Error('操作锁激活中')); } // 如果操作锁已激活,但持续时间超过安全阈值,强制重置 if (this.operationLock) { const currentTime = Date.now(); const lockDuration = currentTime - this.operationLockStartTime; if (lockDuration > 2000) { console.warn(`操作锁已激活 ${lockDuration}ms,超过安全阈值,强制重置`); this.forceResetOperationLock(); } } // 获取锁 if (!this.setOperationLock()) { console.log('audioService: 操作锁激活,强制执行当前播放请求'); // 如果只是要继续播放当前音频,直接执行 if (this.currentSound && !url && !track) { if (this.seekLock && this.seekDebounceTimer) { clearTimeout(this.seekDebounceTimer); this.seekLock = false; } this.currentSound.play(); return Promise.resolve(this.currentSound); } // 强制释放锁并继续执行 this.forceResetOperationLock(); // 这里不再返回错误,而是继续执行播放逻辑 } // 如果没有提供新的 URL 和 track,且当前有音频实例,则继续播放 if (this.currentSound && !url && !track) { // 如果有进行中的seek操作,等待其完成 if (this.seekLock && this.seekDebounceTimer) { clearTimeout(this.seekDebounceTimer); this.seekLock = false; } this.currentSound.play(); this.releaseOperationLock(); return Promise.resolve(this.currentSound); } // 如果没有提供必要的参数,返回错误 if (!url || !track) { this.releaseOperationLock(); return Promise.reject(new Error('缺少必要参数: url和track')); } // 检查是否是同一首歌曲的无缝切换(Hot-Swap) const isHotSwap = this.currentTrack && track && this.currentTrack.id === track.id && this.currentSound; if (isHotSwap) { console.log('audioService: 检测到同一首歌曲的源切换,启用无缝切换模式'); } return new Promise((resolve, reject) => { let retryCount = 0; const maxRetries = 1; // 如果有正在加载的 pendingSound,先清理掉 if (this.pendingSound) { console.log('audioService: 清理正在加载的 pendingSound'); this.pendingSound.unload(); this.pendingSound = null; } const tryPlay = async () => { try { console.log('audioService: 开始创建音频对象'); // 确保 Howler 上下文已初始化 if (!Howler.ctx) { console.log('audioService: 初始化 Howler 上下文'); Howler.ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); } // 确保使用同一个音频上下文 if (Howler.ctx.state === 'closed') { console.log('audioService: 重新创建音频上下文'); Howler.ctx = new (window.AudioContext || (window as any).webkitAudioContext)(); this.context = Howler.ctx; Howler.masterGain = this.context.createGain(); Howler.masterGain.connect(this.context.destination); } // 恢复上下文状态 if (Howler.ctx.state === 'suspended') { console.log('audioService: 恢复暂停的音频上下文'); await Howler.ctx.resume(); } // 非热切换模式下,先停止并清理现有的音频实例 if (!isHotSwap && this.currentSound) { console.log('audioService: 停止并清理现有的音频实例'); // 确保任何进行中的seek操作被取消 if (this.seekLock && this.seekDebounceTimer) { clearTimeout(this.seekDebounceTimer); this.seekLock = false; } this.currentSound.stop(); this.currentSound.unload(); this.currentSound = null; } // 清理 EQ 但保持上下文 (热切换时暂时不清理,等切换完成后再处理) if (!isHotSwap) { console.log('audioService: 清理 EQ'); await this.disposeEQ(true); } // 如果不是热切换,立即更新 currentTrack if (!isHotSwap) { this.currentTrack = track; } // 如果不是热切换,立即更新 currentTrack if (!isHotSwap) { this.currentTrack = track; } let newSound: Howl; if (existingSound) { console.log('audioService: 使用预加载的 Howl 对象'); newSound = existingSound; // 确保 volume 和 rate 正确 newSound.volume(1); // 内部 volume 设为 1,由 Howler.masterGain 控制实际音量 newSound.rate(this.playbackRate); // 重新绑定事件监听器,因为 PreloadService 可能没有绑定这些 // 注意:Howler 允许重复绑定,但最好先清理(如果无法清理,就直接绑定,Howler 是 EventEmitter) // 这里我们假设 existingSound 是干净的或者我们只绑定我们需要关心的 } else { console.log('audioService: 创建新的 Howl 对象'); newSound = new Howl({ src: [url], html5: true, autoplay: false, volume: 1, // 禁用 Howler.js 音量控制 rate: this.playbackRate, format: ['mp3', 'aac'] }); } // 统一设置事件处理 const setupEvents = () => { newSound.off('loaderror'); newSound.off('playerror'); newSound.off('load'); newSound.on('loaderror', (_, error) => { console.error('Audio load error:', error); if (retryCount < maxRetries && !existingSound) { // 预加载的音频通常已经 loaded,不应重试 retryCount++; console.log(`Retrying playback (${retryCount}/${maxRetries})...`); setTimeout(tryPlay, 1000 * retryCount); } else { this.emit('url_expired', track); this.releaseOperationLock(); if (isHotSwap) this.pendingSound = null; reject(new Error('音频加载失败,请尝试切换其他歌曲')); } }); newSound.on('playerror', (_, error) => { console.error('Audio play error:', error); if (retryCount < maxRetries) { retryCount++; console.log(`Retrying playback (${retryCount}/${maxRetries})...`); setTimeout(tryPlay, 1000 * retryCount); } else { this.emit('url_expired', track); this.releaseOperationLock(); if (isHotSwap) this.pendingSound = null; reject(new Error('音频播放失败,请尝试切换其他歌曲')); } }); const onLoaded = async () => { try { // 如果是热切换,现在执行切换逻辑 if (isHotSwap) { console.log('audioService: 执行无缝切换'); // 1. 获取当前播放进度或使用指定的 seekTime let targetPos = 0; if (seekTime > 0) { // 如果有指定的 seekTime(如恢复播放进度),优先使用 targetPos = seekTime; console.log(`audioService: 使用指定的 seekTime: ${seekTime}s`); } else if (this.currentSound) { // 否则同步当前进度 targetPos = this.currentSound.seek() as number; } // 2. 同步新音频进度 newSound.seek(targetPos); // 3. 初始化新音频的 EQ await this.disposeEQ(true); await this.setupEQ(newSound); // 4. 播放新音频 if (isPlay) { newSound.play(); } // 5. 停止旧音频 if (this.currentSound) { this.currentSound.stop(); this.currentSound.unload(); } // 6. 更新引用 this.currentSound = newSound; this.currentTrack = track; this.pendingSound = null; console.log(`audioService: 无缝切换完成,进度同步至 ${targetPos}s`); } else { // 普通加载逻辑 await this.setupEQ(newSound); this.currentSound = newSound; } // 重新应用已保存的音量 const savedVolume = localStorage.getItem('volume'); if (savedVolume) { this.applyVolume(parseFloat(savedVolume)); } if (this.currentSound) { try { if (!isHotSwap && seekTime > 0) { this.currentSound.seek(seekTime); } console.log('audioService: 音频加载成功,设置 EQ'); this.updateMediaSessionMetadata(track); this.updateMediaSessionPositionState(); this.emit('load'); if (!isHotSwap) { console.log('audioService: 音频完全初始化,isPlay =', isPlay); if (isPlay) { console.log('audioService: 开始播放'); this.currentSound.play(); } } resolve(this.currentSound); } catch (error) { console.error('Audio initialization failed:', error); reject(error); } } } catch (error) { console.error('Audio initialization failed:', error); reject(error); } }; if (newSound.state() === 'loaded') { onLoaded(); } else { newSound.once('load', onLoaded); } }; setupEvents(); if (isHotSwap) { this.pendingSound = newSound; } else { this.currentSound = newSound; } // 设置音频事件监听 (play, pause, end, seek) // ... (保持原有的事件监听逻辑不变,但需要确保绑定到 newSound) const soundInstance = newSound; if (soundInstance) { // 清除旧的监听器以防重复 soundInstance.off('play'); soundInstance.off('pause'); soundInstance.off('end'); soundInstance.off('seek'); soundInstance.on('play', () => { if (this.currentSound === soundInstance) { this.updateMediaSessionState(true); this.emit('play'); } }); soundInstance.on('pause', () => { if (this.currentSound === soundInstance) { this.updateMediaSessionState(false); this.emit('pause'); } }); soundInstance.on('end', () => { if (this.currentSound === soundInstance) { this.emit('end'); } }); soundInstance.on('seek', () => { if (this.currentSound === soundInstance) { this.updateMediaSessionPositionState(); this.emit('seek'); } }); } } catch (error) { console.error('Error creating audio instance:', error); this.releaseOperationLock(); reject(error); } }; tryPlay(); }).finally(() => { // 无论成功或失败都解除操作锁 this.releaseOperationLock(); }); } getCurrentSound() { return this.currentSound; } getCurrentTrack() { return this.currentTrack; } stop() { // 强制重置操作锁并继续执行 this.forceResetOperationLock(); try { if (this.currentSound) { try { // 确保任何进行中的seek操作被取消 if (this.seekLock && this.seekDebounceTimer) { clearTimeout(this.seekDebounceTimer); this.seekLock = false; } this.currentSound.stop(); this.currentSound.unload(); } catch (error) { console.error('停止音频失败:', error); } this.currentSound = null; } this.currentTrack = null; if ('mediaSession' in navigator) { navigator.mediaSession.playbackState = 'none'; } this.disposeEQ(); } catch (error) { console.error('停止音频时发生错误:', error); } } setVolume(volume: number) { this.applyVolume(volume); } seek(time: number) { // 直接强制重置操作锁 this.forceResetOperationLock(); if (this.currentSound) { try { // 直接执行seek操作 this.currentSound.seek(time); // 触发seek事件 this.updateMediaSessionPositionState(); this.emit('seek', time); } catch (error) { console.error('Seek操作失败:', error); } } } pause() { this.forceResetOperationLock(); if (this.currentSound) { try { // 确保任何进行中的seek操作被取消 if (this.seekLock && this.seekDebounceTimer) { clearTimeout(this.seekDebounceTimer); this.seekLock = false; } this.currentSound.pause(); } catch (error) { console.error('暂停音频失败:', error); } } } clearAllListeners() { this.callbacks = {}; } public getCurrentPreset(): string | null { return localStorage.getItem('currentPreset'); } public setCurrentPreset(preset: string): void { localStorage.setItem('currentPreset', preset); } public setPlaybackRate(rate: number) { if (!this.currentSound) return; this.playbackRate = rate; // Howler 的 rate() 在 html5 模式下不生效 this.currentSound.rate(rate); // 取出底层 HTMLAudioElement,改原生 playbackRate const sounds = (this.currentSound as any)._sounds as any[]; sounds.forEach(({ _node }) => { if (_node instanceof HTMLAudioElement) { _node.playbackRate = rate; } }); // 同步给 Media Session UI if ('mediaSession' in navigator && 'setPositionState' in navigator.mediaSession) { navigator.mediaSession.setPositionState({ duration: this.currentSound.duration(), playbackRate: rate, position: this.currentSound.seek() as number }); } } public getPlaybackRate(): number { return this.playbackRate; } // 新的音量调节方法 private applyVolume(volume: number) { // 确保值在0到1之间 const normalizedVolume = Math.max(0, Math.min(1, volume)); // 使用线性缩放音量 const linearVolume = normalizedVolume; // 将音量应用到所有相关节点 if (this.gainNode) { // 立即设置音量 this.gainNode.gain.cancelScheduledValues(this.context!.currentTime); this.gainNode.gain.setValueAtTime(linearVolume, this.context!.currentTime); } else { this.currentSound?.volume(linearVolume); } // 保存值 localStorage.setItem('volume', linearVolume.toString()); console.log('Volume applied (linear):', linearVolume); } // 添加方法检查当前音频是否在加载状态 isLoading(): boolean { if (!this.currentSound) return false; // 检查Howl对象的内部状态 // 如果状态为1表示已经加载但未完成,状态为2表示正在加载 const state = (this.currentSound as any)._state; // 如果操作锁激活也认为是加载状态 return this.operationLock || state === 'loading' || state === 1; } // 检查音频是否真正在播放 isActuallyPlaying(): boolean { if (!this.currentSound) return false; try { // 综合判断: // 1. Howler API是否报告正在播放 // 2. 是否不在加载状态 // 3. 确保音频上下文状态正常 const isPlaying = this.currentSound.playing(); const isLoading = this.isLoading(); const contextRunning = Howler.ctx && Howler.ctx.state === 'running'; // 只有在三个条件都满足时才认为是真正在播放 return isPlaying && !isLoading && contextRunning; } catch (error) { console.error('检查播放状态出错:', error); return false; } } } export const audioService = new AudioService(); ================================================ FILE: src/renderer/services/eqService.ts ================================================ import { Howl, Howler } from 'howler'; import Tuna from 'tunajs'; // 类型定义扩展 interface HowlSound { _sounds: Array<{ _node: HTMLMediaElement & { destination?: MediaElementAudioSourceNode; }; }>; } export interface EQSettings { [key: string]: number; } export class EQService { private context: AudioContext | null = null; private tuna: any = null; private equalizer: any = null; private source: MediaElementAudioSourceNode | null = null; private gainNode: GainNode | null = null; private bypass = false; // 预设频率 private readonly frequencies = [31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000]; // 默认EQ设置 private defaultEQSettings: EQSettings = Object.fromEntries( this.frequencies.map((f) => [f.toString(), 0]) ); constructor() { this.loadSavedSettings(); this.bypass = localStorage.getItem('eqBypass') === 'true'; this.initializeUserGestureHandler(); } // 初始化用户手势处理 private initializeUserGestureHandler() { const handler = async () => { if (this.context?.state === 'suspended') { await this.context.resume(); } document.removeEventListener('click', handler); }; document.addEventListener('click', handler); } // 初始化音频上下文 public async setupAudioContext(howl: Howl) { try { // 使用Howler的现有上下文 this.context = (Howler.ctx as AudioContext) || new AudioContext(); // 初始化Howler的音频系统(如果需要) if (!Howler.ctx) { Howler.ctx = this.context; Howler.masterGain = this.context.createGain(); Howler.masterGain.connect(this.context.destination); } // 确保上下文处于运行状态 if (this.context.state === 'suspended') { await this.context.resume(); } const sound = (howl as unknown as HowlSound)._sounds[0]; if (!sound?._node) throw new Error('无法获取音频节点'); // 清理现有资源 await this.dispose(); // 创建新的处理链 this.tuna = new Tuna(this.context); // 创建/复用源节点 if (!sound._node.destination) { this.source = this.context.createMediaElementSource(sound._node); sound._node.destination = this.source; } else { this.source = sound._node.destination; } // 创建效果节点 this.gainNode = this.context.createGain(); this.equalizer = new this.tuna.Equalizer({ frequencies: this.frequencies, gains: this.frequencies.map((f) => this.getSavedGain(f.toString())), bypass: this.bypass }); // 连接节点链 this.source!.connect(this.equalizer.input).connect(this.gainNode).connect(Howler.masterGain); // 恢复音量设置 const volume = localStorage.getItem('volume'); this.gainNode.gain.value = volume ? parseFloat(volume) : 1; } catch (error) { console.error('音频上下文初始化失败:', error); await this.dispose(); throw error; } } // EQ功能开关 public setEnabled(enabled: boolean) { this.bypass = !enabled; localStorage.setItem('eqBypass', JSON.stringify(this.bypass)); if (this.equalizer) this.equalizer.bypass = this.bypass; } public isEnabled(): boolean { return !this.bypass; } // 调整频率增益 public setFrequencyGain(frequency: string, gain: number) { const index = this.frequencies.findIndex((f) => f.toString() === frequency); if (index !== -1 && this.equalizer) { this.equalizer.setGain(index, gain); this.saveSettings(frequency, gain); } } // 重置EQ设置 public resetEQ() { this.frequencies.forEach((f) => { this.setFrequencyGain(f.toString(), 0); }); localStorage.removeItem('eqSettings'); } // 获取当前设置 public getAllSettings(): EQSettings { return this.loadSavedSettings(); } // 保存/加载设置 private saveSettings(frequency: string, gain: number) { const settings = this.loadSavedSettings(); settings[frequency] = gain; localStorage.setItem('eqSettings', JSON.stringify(settings)); } private loadSavedSettings(): EQSettings { const saved = localStorage.getItem('eqSettings'); return saved ? JSON.parse(saved) : { ...this.defaultEQSettings }; } private getSavedGain(frequency: string): number { return this.loadSavedSettings()[frequency] || 0; } // 清理资源 public async dispose() { try { [this.source, this.equalizer, this.gainNode].forEach((node) => { if (node) { node.disconnect(); // 特殊清理Tuna节点 if (node instanceof Tuna.Equalizer) node.destroy(); } }); if (this.context && this.context !== Howler.ctx) { await this.context.close(); } this.context = null; this.tuna = null; this.source = null; this.equalizer = null; this.gainNode = null; } catch (error) { console.error('资源清理失败:', error); } } } export const eqService = new EQService(); ================================================ FILE: src/renderer/services/lyricTranslation.ts ================================================ import { useSettingsStore } from '@/store/modules/settings'; import type { ILyricText } from '@/types/music'; /** * Translate lyric lines according to selected engine. * Supports runtime-loading `opencc-rust` from CDN and caches the converter. */ export async function translateLyrics(lines: ILyricText[] | undefined) { if (!lines || lines.length === 0) return lines || []; const settingsStore = useSettingsStore(); const engine = settingsStore.setData?.lyricTranslationEngine || 'none'; switch (engine) { case 'opencc': { const mod: any = await import('./translation-engines/opencc'); const engineMod = await mod.ensureOpenccConverter(); return engineMod.translateLines(lines); } default: { return lines.map((l) => ({ ...l, trText: l.trText || '' })); } } } export default { translateLyrics }; ================================================ FILE: src/renderer/services/playbackRequestManager.ts ================================================ /** * 播放请求管理器 * 负责管理播放请求的队列、取消、状态跟踪,防止竞态条件 */ import type { SongResult } from '@/types/music'; /** * 请求状态枚举 */ export enum RequestStatus { PENDING = 'pending', ACTIVE = 'active', COMPLETED = 'completed', CANCELLED = 'cancelled', FAILED = 'failed' } /** * 播放请求接口 */ export interface PlaybackRequest { id: string; song: SongResult; status: RequestStatus; timestamp: number; abortController?: AbortController; } /** * 播放请求管理器类 */ class PlaybackRequestManager { private currentRequestId: string | null = null; private requestMap: Map = new Map(); private requestCounter = 0; /** * 生成唯一的请求ID */ private generateRequestId(): string { return `playback_${Date.now()}_${++this.requestCounter}`; } /** * 创建新的播放请求 * @param song 要播放的歌曲 * @returns 新请求的ID */ createRequest(song: SongResult): string { // 取消所有之前的请求 this.cancelAllRequests(); const requestId = this.generateRequestId(); const abortController = new AbortController(); const request: PlaybackRequest = { id: requestId, song, status: RequestStatus.PENDING, timestamp: Date.now(), abortController }; this.requestMap.set(requestId, request); this.currentRequestId = requestId; console.log(`[PlaybackRequestManager] 创建新请求: ${requestId}, 歌曲: ${song.name}`); return requestId; } /** * 激活请求(标记为正在处理) * @param requestId 请求ID */ activateRequest(requestId: string): boolean { const request = this.requestMap.get(requestId); if (!request) { console.warn(`[PlaybackRequestManager] 请求不存在: ${requestId}`); return false; } if (request.status === RequestStatus.CANCELLED) { console.warn(`[PlaybackRequestManager] 请求已被取消: ${requestId}`); return false; } request.status = RequestStatus.ACTIVE; console.log(`[PlaybackRequestManager] 激活请求: ${requestId}`); return true; } /** * 完成请求 * @param requestId 请求ID */ completeRequest(requestId: string): void { const request = this.requestMap.get(requestId); if (!request) { return; } request.status = RequestStatus.COMPLETED; console.log(`[PlaybackRequestManager] 完成请求: ${requestId}`); // 清理旧请求(保留最近3个) this.cleanupOldRequests(); } /** * 标记请求失败 * @param requestId 请求ID */ failRequest(requestId: string): void { const request = this.requestMap.get(requestId); if (!request) { return; } request.status = RequestStatus.FAILED; console.log(`[PlaybackRequestManager] 请求失败: ${requestId}`); } /** * 取消指定请求 * @param requestId 请求ID */ cancelRequest(requestId: string): void { const request = this.requestMap.get(requestId); if (!request) { return; } if (request.status === RequestStatus.CANCELLED) { return; } // 取消AbortController if (request.abortController && !request.abortController.signal.aborted) { request.abortController.abort(); } request.status = RequestStatus.CANCELLED; console.log(`[PlaybackRequestManager] 取消请求: ${requestId}, 歌曲: ${request.song.name}`); // 如果是当前请求,清除当前请求ID if (this.currentRequestId === requestId) { this.currentRequestId = null; } } /** * 取消所有请求 */ cancelAllRequests(): void { console.log(`[PlaybackRequestManager] 取消所有请求,当前请求数: ${this.requestMap.size}`); this.requestMap.forEach((request) => { if ( request.status !== RequestStatus.COMPLETED && request.status !== RequestStatus.CANCELLED ) { this.cancelRequest(request.id); } }); } /** * 检查请求是否仍然有效(是当前活动请求) * @param requestId 请求ID * @returns 是否有效 */ isRequestValid(requestId: string): boolean { // 检查是否是当前请求 if (this.currentRequestId !== requestId) { console.warn( `[PlaybackRequestManager] 请求已过期: ${requestId}, 当前请求: ${this.currentRequestId}` ); return false; } const request = this.requestMap.get(requestId); if (!request) { console.warn(`[PlaybackRequestManager] 请求不存在: ${requestId}`); return false; } // 检查请求状态 if (request.status === RequestStatus.CANCELLED) { console.warn(`[PlaybackRequestManager] 请求已被取消: ${requestId}`); return false; } return true; } /** * 检查请求是否应该中止(用于 AbortController) * @param requestId 请求ID * @returns AbortSignal 或 undefined */ getAbortSignal(requestId: string): AbortSignal | undefined { const request = this.requestMap.get(requestId); return request?.abortController?.signal; } /** * 获取当前请求ID */ getCurrentRequestId(): string | null { return this.currentRequestId; } /** * 获取请求信息 * @param requestId 请求ID */ getRequest(requestId: string): PlaybackRequest | undefined { return this.requestMap.get(requestId); } /** * 清理旧请求(保留最近3个) */ private cleanupOldRequests(): void { if (this.requestMap.size <= 3) { return; } // 按时间戳排序,保留最新的3个 const sortedRequests = Array.from(this.requestMap.values()).sort( (a, b) => b.timestamp - a.timestamp ); const toKeep = new Set(sortedRequests.slice(0, 3).map((r) => r.id)); const toDelete: string[] = []; this.requestMap.forEach((_, id) => { if (!toKeep.has(id)) { toDelete.push(id); } }); toDelete.forEach((id) => { this.requestMap.delete(id); }); if (toDelete.length > 0) { console.log(`[PlaybackRequestManager] 清理了 ${toDelete.length} 个旧请求`); } } /** * 重置管理器(用于调试或特殊情况) */ reset(): void { console.log('[PlaybackRequestManager] 重置管理器'); this.cancelAllRequests(); this.requestMap.clear(); this.currentRequestId = null; this.requestCounter = 0; } /** * 获取调试信息 */ getDebugInfo(): { currentRequestId: string | null; totalRequests: number; requestsByStatus: Record; } { const requestsByStatus: Record = { [RequestStatus.PENDING]: 0, [RequestStatus.ACTIVE]: 0, [RequestStatus.COMPLETED]: 0, [RequestStatus.CANCELLED]: 0, [RequestStatus.FAILED]: 0 }; this.requestMap.forEach((request) => { requestsByStatus[request.status]++; }); return { currentRequestId: this.currentRequestId, totalRequests: this.requestMap.size, requestsByStatus }; } } // 导出单例实例 export const playbackRequestManager = new PlaybackRequestManager(); ================================================ FILE: src/renderer/services/preloadService.ts ================================================ import { Howl } from 'howler'; import type { SongResult } from '@/types/music'; class PreloadService { private loadingPromises: Map> = new Map(); private preloadedSounds: Map = new Map(); /** * 加载并验证音频 * 如果已经在加载中,返回现有的 Promise * 如果已经加载完成,返回缓存的 Howl 实例 */ public async load(song: SongResult): Promise { if (!song || !song.id) { throw new Error('无效的歌曲对象'); } // 1. 检查是否有正在进行的加载 if (this.loadingPromises.has(song.id)) { console.log(`[PreloadService] 歌曲 ${song.name} 正在加载中,复用现有请求`); return this.loadingPromises.get(song.id)!; } // 2. 检查是否有已完成的缓存 if (this.preloadedSounds.has(song.id)) { const sound = this.preloadedSounds.get(song.id)!; if (sound.state() === 'loaded') { console.log(`[PreloadService] 歌曲 ${song.name} 已预加载完成,直接使用`); return sound; } else { // 如果缓存的音频状态不正常,清理并重新加载 this.preloadedSounds.delete(song.id); } } // 3. 开始新的加载过程 const loadPromise = this._performLoad(song); this.loadingPromises.set(song.id, loadPromise); try { const sound = await loadPromise; this.preloadedSounds.set(song.id, sound); return sound; } finally { this.loadingPromises.delete(song.id); } } /** * 执行实际的加载和验证逻辑 */ private async _performLoad(song: SongResult): Promise { console.log(`[PreloadService] 开始加载歌曲: ${song.name}`); if (!song.playMusicUrl) { throw new Error('歌曲没有 URL'); } // 创建初始音频实例 const sound = await this._createSound(song.playMusicUrl); // 检查时长 const duration = sound.duration(); const expectedDuration = (song.dt || 0) / 1000; // 时长差异只记录警告,不自动触发重新解析 // 用户可以通过 ReparsePopover 手动选择正确的音源 if ( expectedDuration > 0 && Math.abs(duration - expectedDuration) > 5 && song.source !== 'bilibili' ) { console.warn( `[PreloadService] 时长差异警告:实际 ${duration.toFixed(1)}s, 预期 ${expectedDuration.toFixed(1)}s (${song.name})` ); } return sound; } private _createSound(url: string): Promise { return new Promise((resolve, reject) => { const sound = new Howl({ src: [url], html5: true, preload: true, autoplay: false, onload: () => resolve(sound), onloaderror: (_, err) => reject(err) }); }); } /** * 取消特定歌曲的预加载(如果可能) * 注意:Promise 无法真正取消,但我们可以清理结果 */ public cancel(songId: string | number) { if (this.preloadedSounds.has(songId)) { const sound = this.preloadedSounds.get(songId)!; sound.unload(); this.preloadedSounds.delete(songId); } // loadingPromises 中的任务会继续执行,但因为 preloadedSounds 中没有记录, // 下次请求时会重新加载(或者我们可以让 _performLoad 检查一个取消标记,但这增加了复杂性) } /** * 获取已预加载的音频实例(如果存在) */ public getPreloadedSound(songId: string | number): Howl | undefined { return this.preloadedSounds.get(songId); } /** * 消耗(使用)已预加载的音频 * 从缓存中移除但不 unload(由调用方管理生命周期) * @returns 预加载的 Howl 实例,如果没有则返回 undefined */ public consume(songId: string | number): Howl | undefined { const sound = this.preloadedSounds.get(songId); if (sound) { this.preloadedSounds.delete(songId); console.log(`[PreloadService] 消耗预加载的歌曲: ${songId}`); return sound; } return undefined; } /** * 清理所有预加载资源 */ public clearAll() { this.preloadedSounds.forEach((sound) => sound.unload()); this.preloadedSounds.clear(); this.loadingPromises.clear(); } } export const preloadService = new PreloadService(); ================================================ FILE: src/renderer/services/translation-engines/index.ts ================================================ // Re-export available translation engines from this folder. // Add more engines here as separate files and re-export them. export * from './opencc'; ================================================ FILE: src/renderer/services/translation-engines/opencc.ts ================================================ import type { ILyricText } from '@/types/music'; let _inited = false; let _converter: any = null; export async function init(): Promise { if (_inited) return; const mod: any = await import('https://cdn.jsdelivr.net/npm/opencc-rust/dist/opencc-rust.mjs'); if (!mod?.initOpenccRust || !mod?.getConverter) { throw new Error('opencc-rust module missing expected exports'); } await mod.initOpenccRust(); _converter = mod.getConverter(); _inited = true; } export async function convert(text: string): Promise { await init(); if (!_converter) return text; return _converter.convert(text); } export async function convertLines(lines: string[]) { await init(); if (!_converter) return lines.slice(); const cjkRe = /[\u4e00-\u9fff]/; const convertOne = async (s: string) => { const src = (s || '').trim(); if (!src || !cjkRe.test(src)) return ''; try { return await _converter.convert(src); } catch (e) { console.warn('opencc convertLines item failed:', e); return ''; } }; const results = await Promise.all(lines.map((s) => convertOne(s))); return results; } export async function translateLines(lines: ILyricText[]) { if (!lines || lines.length === 0) return lines.slice(); const srcInfo = lines.map((l) => { const hasTr = !!(l.trText && l.trText.trim() !== ''); return { src: hasTr ? (l.trText || '').trim() : (l.text || '').trim(), targetIsTr: hasTr }; }); const srcLines = srcInfo.map((s) => s.src); const converted = await convertLines(srcLines); return lines.map((l, i) => { const { targetIsTr } = srcInfo[i]; const conv = converted[i] || ''; if (targetIsTr) { const tr = conv || l.trText || l.text || ''; return { ...l, trText: tr }; } const txt = conv || l.text || ''; return { ...l, text: txt }; }); } export default { init, convert, convertLines, translateLines }; // Ensure the engine is initialized and return public API. let _ensurePromise: Promise | null = null; export async function ensureOpenccConverter() { if (_ensurePromise) return _ensurePromise; _ensurePromise = (async () => { try { await init(); } catch (e) { console.warn('opencc ensureOpenccConverter init failed:', e); } return { init, convert, convertLines, translateLines }; })(); return _ensurePromise; } ================================================ FILE: src/renderer/shims-vue.d.ts ================================================ declare module '*.vue' { import { DefineComponent } from 'vue'; const component: DefineComponent<{}, {}, any>; export default component; } ================================================ FILE: src/renderer/store/index.ts ================================================ import { createPinia } from 'pinia'; import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'; import { markRaw } from 'vue'; import router from '@/router'; // 创建 pinia 实例 const pinia = createPinia(); pinia.use(piniaPluginPersistedstate); // 添加路由到 Pinia pinia.use(({ store }) => { store.router = markRaw(router); }); // 导出所有 store export * from './modules/lyric'; export * from './modules/menu'; export * from './modules/music'; export * from './modules/player'; export * from './modules/recommend'; export * from './modules/search'; export * from './modules/settings'; export * from './modules/user'; export default pinia; ================================================ FILE: src/renderer/store/modules/favorite.ts ================================================ import { defineStore } from 'pinia'; import { ref } from 'vue'; import { getLikedList, likeSong } from '@/api/music'; import { hasPermission } from '@/utils/auth'; import { getLocalStorageItem, isBilibiliIdMatch, setLocalStorageItem } from '@/utils/playerUtils'; /** * 收藏管理 Store * 负责:收藏列表、不喜欢列表的管理 */ export const useFavoriteStore = defineStore('favorite', () => { // ==================== 状态 ==================== const favoriteList = ref>(getLocalStorageItem('favoriteList', [])); const dislikeList = ref>(getLocalStorageItem('dislikeList', [])); // ==================== Actions ==================== /** * 添加到收藏列表 */ const addToFavorite = async (id: number | string) => { // 检查是否已存在 const isAlreadyInList = favoriteList.value.some((existingId) => typeof id === 'string' && id.includes('--') ? isBilibiliIdMatch(existingId, id) : existingId === id ); if (!isAlreadyInList) { favoriteList.value.push(id); setLocalStorageItem('favoriteList', favoriteList.value); // 只有在有真实登录权限时才调用API if (typeof id === 'number') { const { useUserStore } = await import('./user'); const userStore = useUserStore(); if (userStore.user && hasPermission(true)) { try { await likeSong(id, true); } catch (error) { console.error('收藏歌曲API调用失败:', error); } } } } }; /** * 从收藏列表移除 */ const removeFromFavorite = async (id: number | string) => { // 对于B站视频,需要根据bvid和cid来匹配 if (typeof id === 'string' && id.includes('--')) { favoriteList.value = favoriteList.value.filter( (existingId) => !isBilibiliIdMatch(existingId, id) ); } else { favoriteList.value = favoriteList.value.filter((existingId) => existingId !== id); // 只有在有真实登录权限时才调用API if (typeof id === 'number') { const { useUserStore } = await import('./user'); const userStore = useUserStore(); if (userStore.user && hasPermission(true)) { try { await likeSong(id, false); } catch (error) { console.error('取消收藏歌曲API调用失败:', error); } } } } setLocalStorageItem('favoriteList', favoriteList.value); }; /** * 添加到不喜欢列表 */ const addToDislikeList = (id: number | string) => { if (!dislikeList.value.includes(id)) { dislikeList.value.push(id); setLocalStorageItem('dislikeList', dislikeList.value); } }; /** * 从不喜欢列表移除 */ const removeFromDislikeList = (id: number | string) => { dislikeList.value = dislikeList.value.filter((existingId) => existingId !== id); setLocalStorageItem('dislikeList', dislikeList.value); }; /** * 初始化收藏列表(从服务器同步) */ const initializeFavoriteList = async () => { const { useUserStore } = await import('./user'); const userStore = useUserStore(); const localFavoriteList = localStorage.getItem('favoriteList'); const localList: number[] = localFavoriteList ? JSON.parse(localFavoriteList) : []; if (userStore.user && userStore.user.userId) { try { const res = await getLikedList(userStore.user.userId); if (res.data?.ids) { const serverList = res.data.ids.reverse(); const mergedList = Array.from(new Set([...localList, ...serverList])); favoriteList.value = mergedList; } else { favoriteList.value = localList; } } catch (error) { console.error('获取服务器收藏列表失败,使用本地数据:', error); favoriteList.value = localList; } } else { favoriteList.value = localList; } setLocalStorageItem('favoriteList', favoriteList.value); }; /** * 检查歌曲是否已收藏 */ const isFavorite = (id: number | string): boolean => { return favoriteList.value.some((existingId) => typeof id === 'string' && id.includes('--') ? isBilibiliIdMatch(existingId, id) : existingId === id ); }; /** * 检查歌曲是否在不喜欢列表中 */ const isDisliked = (id: number | string): boolean => { return dislikeList.value.includes(id); }; return { // 状态 favoriteList, dislikeList, // Actions addToFavorite, removeFromFavorite, addToDislikeList, removeFromDislikeList, initializeFavoriteList, isFavorite, isDisliked }; }); ================================================ FILE: src/renderer/store/modules/intelligenceMode.ts ================================================ import { createDiscreteApi } from 'naive-ui'; import { defineStore } from 'pinia'; import { ref } from 'vue'; import i18n from '@/../i18n/renderer'; import { getLikedList } from '@/api/music'; import type { Platform } from '@/types/music'; import { getLocalStorageItem, setLocalStorageItem } from '@/utils/playerUtils'; const { message } = createDiscreteApi(['message']); /** * 心动模式管理 Store * 负责:心动模式的播放和状态管理 */ export const useIntelligenceModeStore = defineStore('intelligenceMode', () => { // ==================== 状态 ==================== const isIntelligenceMode = ref(getLocalStorageItem('isIntelligenceMode', false)); const intelligenceModeInfo = ref<{ playlistId: number; seedSongId: number; } | null>(getLocalStorageItem('intelligenceModeInfo', null)); // ==================== Actions ==================== /** * 播放心动模式 */ const playIntelligenceMode = async () => { const { useUserStore } = await import('./user'); const { usePlayerCoreStore } = await import('./playerCore'); const { usePlaylistStore } = await import('./playlist'); const userStore = useUserStore(); const playerCore = usePlayerCoreStore(); const playlistStore = usePlaylistStore(); const { t } = i18n.global; // 检查是否使用cookie登录 if (!userStore.user || userStore.loginType !== 'cookie') { message.warning(t('player.playBar.intelligenceMode.needCookieLogin')); return; } try { // 获取用户歌单列表 if (userStore.playList.length === 0) { await userStore.initializePlaylist(); } // 找到"我喜欢的音乐"歌单 const favoritePlaylist = userStore.playList.find( (pl: any) => pl.userId === userStore.user?.userId && pl.specialType === 5 ); if (!favoritePlaylist) { message.warning(t('player.playBar.intelligenceMode.noFavoritePlaylist')); return; } // 获取喜欢的歌曲列表 const likedListRes = await getLikedList(userStore.user.userId); const likedIds = likedListRes.data?.ids || []; if (likedIds.length === 0) { message.warning(t('player.playBar.intelligenceMode.noLikedSongs')); return; } // 随机选择一首歌曲 const randomSongId = likedIds[Math.floor(Math.random() * likedIds.length)]; // 调用心动模式API const { getIntelligenceList } = await import('@/api/music'); const res = await getIntelligenceList({ id: randomSongId, pid: favoritePlaylist.id }); if (res.data?.data && res.data.data.length > 0) { const intelligenceSongs = res.data.data.map((item: any) => ({ id: item.id, name: item.songInfo.name, picUrl: item.songInfo.al?.picUrl, source: 'netease' as Platform, song: item.songInfo, ...item.songInfo, playLoading: false })); // 设置心动模式状态 isIntelligenceMode.value = true; intelligenceModeInfo.value = { playlistId: favoritePlaylist.id, seedSongId: randomSongId }; playlistStore.playMode = 3; // 设置播放模式为心动模式 setLocalStorageItem('isIntelligenceMode', true); setLocalStorageItem('intelligenceModeInfo', intelligenceModeInfo.value); setLocalStorageItem('playMode', playlistStore.playMode); // 替换播放列表并开始播放 playlistStore.setPlayList(intelligenceSongs, false, true); await playerCore.handlePlayMusic(intelligenceSongs[0], true); } else { message.error(t('player.playBar.intelligenceMode.failed')); } } catch (error) { console.error('心动模式播放失败:', error); message.error(t('player.playBar.intelligenceMode.error')); } }; /** * 清除心动模式状态 */ const clearIntelligenceMode = () => { isIntelligenceMode.value = false; intelligenceModeInfo.value = null; setLocalStorageItem('isIntelligenceMode', false); localStorage.removeItem('intelligenceModeInfo'); }; return { // 状态 isIntelligenceMode, intelligenceModeInfo, // Actions playIntelligenceMode, clearIntelligenceMode }; }); ================================================ FILE: src/renderer/store/modules/lyric.ts ================================================ import { defineStore } from 'pinia'; import { ref } from 'vue'; export const useLyricStore = defineStore('lyric', () => { const lyric = ref({}); const setLyric = (newLyric: any) => { lyric.value = newLyric; }; return { lyric, setLyric }; }); ================================================ FILE: src/renderer/store/modules/menu.ts ================================================ import { defineStore } from 'pinia'; import { ref } from 'vue'; import homeRouter from '@/router/home'; export const useMenuStore = defineStore('menu', () => { const menus = ref(homeRouter); const setMenus = (newMenus: any[]) => { menus.value = newMenus; }; return { menus, setMenus }; }); ================================================ FILE: src/renderer/store/modules/music.ts ================================================ import { defineStore } from 'pinia'; interface MusicState { currentMusicList: any[] | null; currentMusicListName: string; currentListInfo: any | null; canRemoveSong: boolean; } export const useMusicStore = defineStore('music', { state: (): MusicState => ({ currentMusicList: null, currentMusicListName: '', currentListInfo: null, canRemoveSong: false }), actions: { // 设置当前音乐列表 setCurrentMusicList(list: any[], name: string, listInfo: any = null, canRemove = false) { this.currentMusicList = list; this.currentMusicListName = name; this.currentListInfo = listInfo; this.canRemoveSong = canRemove; }, // 清除当前音乐列表 clearCurrentMusicList() { this.currentMusicList = null; this.currentMusicListName = ''; this.currentListInfo = null; this.canRemoveSong = false; }, // 从列表中移除一首歌曲 removeSongFromList(id: number) { if (!this.currentMusicList) return; const index = this.currentMusicList.findIndex((song) => song.id === id); if (index !== -1) { this.currentMusicList.splice(index, 1); } } } }); ================================================ FILE: src/renderer/store/modules/player.ts ================================================ /** * - usePlayerCoreStore: 核心播放控制(播放/暂停、音量、速度) * - usePlaylistStore: 播放列表管理(列表、索引、模式、上下一首) * - useFavoriteStore: 收藏管理(收藏列表、不喜欢列表) * - useSleepTimerStore: 定时关闭(时间/歌曲数/列表结束) * - useIntelligenceModeStore: 心动模式 */ import { defineStore, storeToRefs } from 'pinia'; import { computed } from 'vue'; // 导入所有拆分的子 stores import { useFavoriteStore } from './favorite'; import { useIntelligenceModeStore } from './intelligenceMode'; import { usePlayerCoreStore } from './playerCore'; import { usePlaylistStore } from './playlist'; import { type SleepTimerInfo, SleepTimerType, useSleepTimerStore } from './sleepTimer'; export { type SleepTimerInfo, SleepTimerType }; export { getSongUrl, loadLrc, useLyrics, useSongDetail, useSongUrl } from '@/hooks/usePlayerHooks'; export { isBilibiliIdMatch } from '@/utils/playerUtils'; /** * 聚合 Player Store */ export const usePlayerStore = defineStore('player', () => { // 获取所有子 stores const playerCore = usePlayerCoreStore(); const playlist = usePlaylistStore(); const favorite = useFavoriteStore(); const sleepTimer = useSleepTimerStore(); const intelligenceMode = useIntelligenceModeStore(); // 使用 storeToRefs 获取响应式引用 const { play, isPlay, playMusic, playMusicUrl, musicFull, playbackRate, volume, userPlayIntent } = storeToRefs(playerCore); const { playList, playListIndex, playMode, originalPlayList, playListDrawerVisible } = storeToRefs(playlist); const { favoriteList, dislikeList } = storeToRefs(favorite); const { sleepTimer: sleepTimerState, showSleepTimer } = storeToRefs(sleepTimer); const { isIntelligenceMode, intelligenceModeInfo } = storeToRefs(intelligenceMode); // ==================== Computed ==================== const currentSong = computed(() => playerCore.currentSong); const isPlaying = computed(() => playerCore.isPlaying); const currentPlayList = computed(() => playlist.currentPlayList); const currentPlayListIndex = computed(() => playlist.currentPlayListIndex); // 定时器相关 computed const currentSleepTimer = computed(() => sleepTimer.currentSleepTimer); const hasSleepTimerActive = computed(() => sleepTimer.hasSleepTimerActive); const sleepTimerRemainingTime = computed(() => sleepTimer.sleepTimerRemainingTime); const sleepTimerRemainingSongs = computed(() => sleepTimer.sleepTimerRemainingSongs); // ==================== 初始化方法 ==================== /** * 初始化播放状态(从 localStorage 恢复) */ const initializePlayState = async () => { await playerCore.initializePlayState(); await playlist.initializePlaylist(); }; /** * 初始化收藏列表(从服务器同步) */ const initializeFavoriteList = async () => { await favorite.initializeFavoriteList(); }; // ==================== 返回所有状态和方法 ==================== return { // ========== 核心播放控制 (PlayerCore) ========== play, isPlay, playMusic, playMusicUrl, musicFull, playbackRate, volume, userPlayIntent, // PlayerCore - Computed currentSong, isPlaying, // PlayerCore - Actions setIsPlay: playerCore.setIsPlay, setMusicFull: playerCore.setMusicFull, setPlayMusic: playerCore.setPlayMusic, setPlaybackRate: playerCore.setPlaybackRate, setVolume: playerCore.setVolume, getVolume: playerCore.getVolume, increaseVolume: playerCore.increaseVolume, decreaseVolume: playerCore.decreaseVolume, handlePlayMusic: playerCore.handlePlayMusic, playAudio: playerCore.playAudio, handlePause: playerCore.handlePause, checkPlaybackState: playerCore.checkPlaybackState, reparseCurrentSong: playerCore.reparseCurrentSong, // ========== 播放列表管理 (Playlist) ========== playList, playListIndex, playMode, originalPlayList, playListDrawerVisible, // Playlist - Computed currentPlayList, currentPlayListIndex, // Playlist - Actions setPlayList: playlist.setPlayList, addToNextPlay: playlist.addToNextPlay, removeFromPlayList: playlist.removeFromPlayList, clearPlayAll: playlist.clearPlayAll, togglePlayMode: playlist.togglePlayMode, shufflePlayList: playlist.shufflePlayList, restoreOriginalOrder: playlist.restoreOriginalOrder, preloadNextSongs: playlist.preloadNextSongs, nextPlay: playlist.nextPlay, prevPlay: playlist.prevPlay, setPlayListDrawerVisible: playlist.setPlayListDrawerVisible, setPlay: playlist.setPlay, // ========== 收藏管理 (Favorite) ========== favoriteList, dislikeList, // Favorite - Actions addToFavorite: favorite.addToFavorite, removeFromFavorite: favorite.removeFromFavorite, addToDislikeList: favorite.addToDislikeList, removeFromDislikeList: favorite.removeFromDislikeList, // ========== 定时关闭 (SleepTimer) ========== sleepTimer: sleepTimerState, showSleepTimer, // SleepTimer - Computed currentSleepTimer, hasSleepTimerActive, sleepTimerRemainingTime, sleepTimerRemainingSongs, // SleepTimer - Actions setSleepTimerByTime: sleepTimer.setSleepTimerByTime, setSleepTimerBySongs: sleepTimer.setSleepTimerBySongs, setSleepTimerAtPlaylistEnd: sleepTimer.setSleepTimerAtPlaylistEnd, clearSleepTimer: sleepTimer.clearSleepTimer, // ========== 心动模式 (IntelligenceMode) ========== isIntelligenceMode, intelligenceModeInfo, // IntelligenceMode - Actions playIntelligenceMode: intelligenceMode.playIntelligenceMode, // ========== 初始化方法 ========== initializePlayState, initializeFavoriteList }; }); ================================================ FILE: src/renderer/store/modules/playerCore.ts ================================================ import { cloneDeep } from 'lodash'; import { createDiscreteApi } from 'naive-ui'; import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; import i18n from '@/../i18n/renderer'; import { getBilibiliAudioUrl } from '@/api/bilibili'; import { getParsingMusicUrl } from '@/api/music'; import { useMusicHistory } from '@/hooks/MusicHistoryHook'; import { useLyrics, useSongDetail } from '@/hooks/usePlayerHooks'; import { audioService } from '@/services/audioService'; import { playbackRequestManager } from '@/services/playbackRequestManager'; import { preloadService } from '@/services/preloadService'; import { SongSourceConfigManager } from '@/services/SongSourceConfigManager'; import type { Platform, SongResult } from '@/types/music'; import { getImgUrl } from '@/utils'; import { getImageLinearBackground } from '@/utils/linearColor'; const musicHistory = useMusicHistory(); const { message } = createDiscreteApi(['message']); /** * 核心播放控制 Store * 负责:播放/暂停、当前歌曲、音频URL、音量、播放速度、全屏状态 */ export const usePlayerCoreStore = defineStore( 'playerCore', () => { // ==================== 状态 ==================== const play = ref(false); const isPlay = ref(false); const playMusic = ref({} as SongResult); const playMusicUrl = ref(''); const musicFull = ref(false); const playbackRate = ref(1.0); const volume = ref(1); const userPlayIntent = ref(false); // 用户是否想要播放 let checkPlayTime: NodeJS.Timeout | null = null; // ==================== Computed ==================== const currentSong = computed(() => playMusic.value); const isPlaying = computed(() => isPlay.value); // ==================== Actions ==================== /** * 设置播放状态 */ const setIsPlay = (value: boolean) => { isPlay.value = value; play.value = value; window.electron?.ipcRenderer.send('update-play-state', value); }; /** * 设置全屏状态 */ const setMusicFull = (value: boolean) => { musicFull.value = value; }; /** * 设置播放速度 */ const setPlaybackRate = (rate: number) => { playbackRate.value = rate; audioService.setPlaybackRate(rate); }; /** * 设置音量 */ const setVolume = (newVolume: number) => { const normalizedVolume = Math.max(0, Math.min(1, newVolume)); volume.value = normalizedVolume; audioService.setVolume(normalizedVolume); }; /** * 获取音量 */ const getVolume = () => volume.value; /** * 增加音量 */ const increaseVolume = (step: number = 0.1) => { const newVolume = Math.min(1, volume.value + step); setVolume(newVolume); return newVolume; }; /** * 减少音量 */ const decreaseVolume = (step: number = 0.1) => { const newVolume = Math.max(0, volume.value - step); setVolume(newVolume); return newVolume; }; /** * 播放状态检测 */ const checkPlaybackState = (song: SongResult, requestId?: string, timeout: number = 4000) => { if (checkPlayTime) { clearTimeout(checkPlayTime); } const sound = audioService.getCurrentSound(); if (!sound) return; // 如果没有提供 requestId,创建一个临时标识 const actualRequestId = requestId || `check_${Date.now()}`; const onPlayHandler = () => { console.log(`[${actualRequestId}] 播放事件触发,歌曲成功开始播放`); audioService.off('play', onPlayHandler); audioService.off('playerror', onPlayErrorHandler); }; const onPlayErrorHandler = async () => { console.log('播放错误事件触发,检查是否需要重新获取URL'); audioService.off('play', onPlayHandler); audioService.off('playerror', onPlayErrorHandler); // 如果有 requestId,验证其有效性 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log('请求已过期,跳过重试'); return; } if (userPlayIntent.value && play.value) { console.log('播放失败,尝试刷新URL并重新播放'); playMusic.value.playMusicUrl = undefined; const refreshedSong = { ...song, isFirstPlay: true }; await handlePlayMusic(refreshedSong, true); } }; audioService.on('play', onPlayHandler); audioService.on('playerror', onPlayErrorHandler); checkPlayTime = setTimeout(() => { // 如果有 requestId,验证其有效性 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log('请求已过期,跳过超时重试'); audioService.off('play', onPlayHandler); audioService.off('playerror', onPlayErrorHandler); return; } if (!audioService.isActuallyPlaying() && userPlayIntent.value && play.value) { console.log(`${timeout}ms后歌曲未真正播放且用户仍希望播放,尝试重新获取URL`); audioService.off('play', onPlayHandler); audioService.off('playerror', onPlayErrorHandler); playMusic.value.playMusicUrl = undefined; (async () => { const refreshedSong = { ...song, isFirstPlay: true }; await handlePlayMusic(refreshedSong, true); })(); } }, timeout); }; /** * 核心播放处理函数 */ const handlePlayMusic = async (music: SongResult, isPlay: boolean = true) => { // 如果是新歌曲,重置已尝试的音源(使用 SongSourceConfigManager 按歌曲隔离) if (music.id !== playMusic.value.id) { SongSourceConfigManager.clearTriedSources(music.id); } // 创建新的播放请求并取消之前的所有请求 const requestId = playbackRequestManager.createRequest(music); console.log(`[handlePlayMusic] 开始处理歌曲: ${music.name}, 请求ID: ${requestId}`); const currentSound = audioService.getCurrentSound(); if (currentSound) { console.log('主动停止并卸载当前音频实例'); currentSound.stop(); currentSound.unload(); } // 验证请求是否仍然有效 if (!playbackRequestManager.isRequestValid(requestId)) { console.log(`[handlePlayMusic] 请求已失效: ${requestId}`); return false; } // 激活请求 if (!playbackRequestManager.activateRequest(requestId)) { console.log(`[handlePlayMusic] 无法激活请求: ${requestId}`); return false; } const originalMusic = { ...music }; const { loadLrc } = useLyrics(); const { getSongDetail } = useSongDetail(); // 并行加载歌词和背景色 const [lyrics, { backgroundColor, primaryColor }] = await Promise.all([ (async () => { if (music.lyric && music.lyric.lrcTimeArray.length > 0) { return music.lyric; } return await loadLrc(music.id); })(), (async () => { if (music.backgroundColor && music.primaryColor) { return { backgroundColor: music.backgroundColor, primaryColor: music.primaryColor }; } return await getImageLinearBackground(getImgUrl(music?.picUrl, '30y30')); })() ]); // 在更新状态前再次验证请求 if (!playbackRequestManager.isRequestValid(requestId)) { console.log(`[handlePlayMusic] 加载歌词/背景色后请求已失效: ${requestId}`); return false; } // 设置歌词和背景色 music.lyric = lyrics; music.backgroundColor = backgroundColor; music.primaryColor = primaryColor; music.playLoading = true; // 更新 playMusic playMusic.value = music; play.value = isPlay; // 更新标题 let title = music.name; if (music.source === 'netease' && music?.song?.artists) { title += ` - ${music.song.artists.reduce( (prev: string, curr: any) => `${prev}${curr.name}/`, '' )}`; } else if (music.source === 'bilibili' && music?.song?.ar?.[0]) { title += ` - ${music.song.ar[0].name}`; } document.title = 'AlgerMusic - ' + title; try { // 添加到历史记录 musicHistory.addMusic(music); // 获取歌曲详情 const updatedPlayMusic = await getSongDetail(originalMusic, requestId); // 在获取详情后再次验证请求 if (!playbackRequestManager.isRequestValid(requestId)) { console.log(`[handlePlayMusic] 获取歌曲详情后请求已失效: ${requestId}`); playbackRequestManager.failRequest(requestId); return false; } updatedPlayMusic.lyric = lyrics; playMusic.value = updatedPlayMusic; playMusicUrl.value = updatedPlayMusic.playMusicUrl as string; music.playMusicUrl = updatedPlayMusic.playMusicUrl as string; // 在拆分后补充:触发预加载下一首/下下首(与 playlist store 保持一致) try { const { usePlaylistStore } = await import('./playlist'); const playlistStore = usePlaylistStore(); // 基于当前歌曲在播放列表中的位置来预加载 const list = playlistStore.playList; if (Array.isArray(list) && list.length > 0) { const idx = list.findIndex( (item: SongResult) => item.id === updatedPlayMusic.id && item.source === updatedPlayMusic.source ); if (idx !== -1) { setTimeout(() => { playlistStore.preloadNextSongs(idx); }, 3000); } } } catch (e) { console.warn('预加载触发失败(可能是依赖未加载或循环依赖),已忽略:', e); } let playInProgress = false; try { if (playInProgress) { console.warn('播放操作正在进行中,避免重复调用'); return true; } playInProgress = true; const result = await playAudio(requestId); playInProgress = false; if (result) { playbackRequestManager.completeRequest(requestId); return true; } else { playbackRequestManager.failRequest(requestId); return false; } } catch (error) { console.error('自动播放音频失败:', error); playInProgress = false; playbackRequestManager.failRequest(requestId); return false; } } catch (error) { console.error('处理播放音乐失败:', error); message.error(i18n.global.t('player.playFailed')); if (playMusic.value) { playMusic.value.playLoading = false; } playbackRequestManager.failRequest(requestId); return false; } }; /** * 播放音频 */ const playAudio = async (requestId?: string) => { if (!playMusicUrl.value || !playMusic.value) return null; // 如果提供了 requestId,验证请求是否仍然有效 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[playAudio] 请求已失效: ${requestId}`); return null; } try { const shouldPlay = play.value; console.log('播放音频,当前播放状态:', shouldPlay ? '播放' : '暂停'); // 检查保存的进度 let initialPosition = 0; const savedProgress = JSON.parse(localStorage.getItem('playProgress') || '{}'); console.log( '[playAudio] 读取保存的进度:', savedProgress, '当前歌曲ID:', playMusic.value.id ); if (savedProgress.songId === playMusic.value.id) { initialPosition = savedProgress.progress; console.log('[playAudio] 恢复播放进度:', initialPosition); } // B站视频URL检查 if ( playMusic.value.source === 'bilibili' && (!playMusicUrl.value || playMusicUrl.value === 'undefined') ) { console.log('B站视频URL无效,尝试重新获取'); if (playMusic.value.bilibiliData) { try { const proxyUrl = await getBilibiliAudioUrl( playMusic.value.bilibiliData.bvid, playMusic.value.bilibiliData.cid ); // 再次验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[playAudio] 获取B站URL后请求已失效: ${requestId}`); return null; } (playMusic.value as any).playMusicUrl = proxyUrl; playMusicUrl.value = proxyUrl; } catch (error) { console.error('获取B站音频URL失败:', error); message.error(i18n.global.t('player.playFailed')); return null; } } } // 使用 PreloadService 获取音频 // 优先使用已预加载的 sound(通过 consume 获取并从缓存中移除) // 如果没有预加载,则进行加载 let sound: Howl; try { // 先尝试消耗预加载的 sound const preloadedSound = preloadService.consume(playMusic.value.id); if (preloadedSound && preloadedSound.state() === 'loaded') { console.log(`[playAudio] 使用预加载的音频: ${playMusic.value.name}`); sound = preloadedSound; } else { // 没有预加载或预加载状态不正常,需要加载 console.log(`[playAudio] 没有预加载,开始加载: ${playMusic.value.name}`); sound = await preloadService.load(playMusic.value); } } catch (error) { console.error('PreloadService 加载失败:', error); // 如果 PreloadService 失败,尝试直接播放作为回退 // 但通常 PreloadService 失败意味着 URL 问题 throw error; } // 播放新音频,传入已加载的 sound 实例 const newSound = await audioService.play( playMusicUrl.value, playMusic.value, shouldPlay, initialPosition || 0, sound ); // 播放后再次验证请求 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log(`[playAudio] 播放后请求已失效: ${requestId}`); newSound.stop(); newSound.unload(); return null; } // 添加播放状态检测 if (shouldPlay && requestId) { checkPlaybackState(playMusic.value, requestId); } // 发布音频就绪事件 window.dispatchEvent( new CustomEvent('audio-ready', { detail: { sound: newSound, shouldPlay } }) ); // 时长检查已在 preloadService.ts 中完成 return newSound; } catch (error) { console.error('播放音频失败:', error); setPlayMusic(false); const errorMsg = error instanceof Error ? error.message : String(error); // 操作锁错误处理 if (errorMsg.includes('操作锁激活')) { console.log('由于操作锁正在使用,将在1000ms后重试'); try { audioService.forceResetOperationLock(); console.log('已强制重置操作锁'); } catch (e) { console.error('重置操作锁失败:', e); } setTimeout(() => { // 验证请求是否仍然有效再重试 if (requestId && !playbackRequestManager.isRequestValid(requestId)) { console.log('重试时请求已失效,跳过重试'); return; } if (userPlayIntent.value && play.value) { playAudio(requestId).catch((e) => { console.error('重试播放失败:', e); }); } }, 1000); } else { console.warn('播放音频失败(非操作锁错误),由调用方处理重试'); } message.error(i18n.global.t('player.playFailed')); return null; } }; /** * 暂停播放 */ const handlePause = async () => { try { const currentSound = audioService.getCurrentSound(); if (currentSound) { currentSound.pause(); } setPlayMusic(false); userPlayIntent.value = false; } catch (error) { console.error('暂停播放失败:', error); } }; /** * 设置播放/暂停 */ const setPlayMusic = async (value: boolean | SongResult) => { if (typeof value === 'boolean') { setIsPlay(value); userPlayIntent.value = value; } else { await handlePlayMusic(value); play.value = true; isPlay.value = true; userPlayIntent.value = true; } }; /** * 使用指定音源重新解析当前歌曲 */ const reparseCurrentSong = async (sourcePlatform: Platform, isAuto: boolean = false) => { try { const currentSong = playMusic.value; if (!currentSong || !currentSong.id) { console.warn('没有有效的播放对象'); return false; } if (currentSong.source === 'bilibili') { console.warn('B站视频不支持重新解析'); return false; } // 使用 SongSourceConfigManager 保存配置 SongSourceConfigManager.setConfig( currentSong.id, [sourcePlatform], isAuto ? 'auto' : 'manual' ); const currentSound = audioService.getCurrentSound(); if (currentSound) { currentSound.pause(); } const numericId = typeof currentSong.id === 'string' ? parseInt(currentSong.id, 10) : currentSong.id; console.log(`使用音源 ${sourcePlatform} 重新解析歌曲 ${numericId}`); const songData = cloneDeep(currentSong); const res = await getParsingMusicUrl(numericId, songData); if (res && res.data && res.data.data && res.data.data.url) { const newUrl = res.data.data.url; console.log(`解析成功,获取新URL: ${newUrl.substring(0, 50)}...`); const updatedMusic = { ...currentSong, playMusicUrl: newUrl, expiredAt: Date.now() + 1800000 }; await handlePlayMusic(updatedMusic, true); // 更新播放列表中的歌曲信息 const { usePlaylistStore } = await import('./playlist'); const playlistStore = usePlaylistStore(); playlistStore.updateSong(updatedMusic); return true; } else { console.warn(`使用音源 ${sourcePlatform} 解析失败`); return false; } } catch (error) { console.error('重新解析失败:', error); return false; } }; /** * 初始化播放状态 */ const initializePlayState = async () => { const { useSettingsStore } = await import('./settings'); const settingStore = useSettingsStore(); if (playMusic.value && Object.keys(playMusic.value).length > 0) { try { console.log('恢复上次播放的音乐:', playMusic.value.name); const isPlaying = settingStore.setData.autoPlay; if (playMusic.value.source === 'bilibili' && playMusic.value.bilibiliData) { console.log('恢复B站视频播放', playMusic.value.bilibiliData); playMusic.value.playMusicUrl = undefined; } await handlePlayMusic( { ...playMusic.value, isFirstPlay: true, playMusicUrl: undefined }, isPlaying ); } catch (error) { console.error('重新获取音乐链接失败:', error); play.value = false; isPlay.value = false; playMusic.value = {} as SongResult; playMusicUrl.value = ''; } } setTimeout(() => { audioService.setPlaybackRate(playbackRate.value); }, 2000); }; return { // 状态 play, isPlay, playMusic, playMusicUrl, musicFull, playbackRate, volume, userPlayIntent, // Computed currentSong, isPlaying, // Actions setIsPlay, setMusicFull, setPlayMusic, setPlaybackRate, setVolume, getVolume, increaseVolume, decreaseVolume, handlePlayMusic, playAudio, handlePause, checkPlaybackState, reparseCurrentSong, initializePlayState }; }, { persist: { key: 'player-core-store', storage: localStorage, pick: ['playMusic', 'playMusicUrl', 'playbackRate', 'volume', 'isPlay'] } } ); ================================================ FILE: src/renderer/store/modules/playlist.ts ================================================ import { useThrottleFn } from '@vueuse/core'; import { createDiscreteApi } from 'naive-ui'; import { defineStore, storeToRefs } from 'pinia'; import { computed, ref, shallowRef } from 'vue'; import i18n from '@/../i18n/renderer'; import { useSongDetail } from '@/hooks/usePlayerHooks'; import { preloadService } from '@/services/preloadService'; import type { SongResult } from '@/types/music'; import { getImgUrl } from '@/utils'; import { performShuffle, preloadCoverImage } from '@/utils/playerUtils'; import { useIntelligenceModeStore } from './intelligenceMode'; import { usePlayerCoreStore } from './playerCore'; import { useSleepTimerStore } from './sleepTimer'; const { message } = createDiscreteApi(['message']); /** * 播放列表管理 Store * 负责:播放列表、索引、播放模式、预加载、上/下一首 */ export const usePlaylistStore = defineStore( 'playlist', () => { // ==================== 状态 ==================== // 状态将由 pinia-plugin-persistedstate 自动从 localStorage 恢复 const playList = shallowRef([]); const playListIndex = ref(0); const playMode = ref(0); const originalPlayList = shallowRef([]); const playListDrawerVisible = ref(false); // 连续失败计数器(用于防止无限循环) const consecutiveFailCount = ref(0); const MAX_CONSECUTIVE_FAILS = 5; // 最大连续失败次数 const SINGLE_TRACK_MAX_RETRIES = 3; // 单曲最大重试次数 // ==================== Computed ==================== const currentPlayList = computed(() => playList.value); const currentPlayListIndex = computed(() => playListIndex.value); // ==================== Actions ==================== /** * 获取歌曲详情并预加载 */ const fetchSongs = async (startIndex: number, endIndex: number) => { try { const songs = playList.value.slice( Math.max(0, startIndex), Math.min(endIndex, playList.value.length) ); const { getSongDetail } = useSongDetail(); const detailedSongs = await Promise.all( songs.map(async (song: SongResult) => { try { if (!song.playMusicUrl || (song.source === 'netease' && !song.backgroundColor)) { return await getSongDetail(song); } return song; } catch (error) { console.error('获取歌曲详情失败:', error); return song; } }) ); const nextSong = detailedSongs[0]; if (nextSong && !(nextSong.lyric && nextSong.lyric.lrcTimeArray.length > 0)) { try { const { useLyrics } = await import('@/hooks/usePlayerHooks'); const { loadLrc } = useLyrics(); nextSong.lyric = await loadLrc(nextSong.id); } catch (error) { console.error('加载歌词失败:', error); } } detailedSongs.forEach((song, index) => { if (song && startIndex + index < playList.value.length) { playList.value[startIndex + index] = song; } }); // 预加载下一首歌曲的音频和封面 if (nextSong) { if (nextSong.playMusicUrl) { preloadService.load(nextSong); } if (nextSong.picUrl) { preloadCoverImage(nextSong.picUrl, getImgUrl); } } } catch (error) { console.error('获取歌曲列表失败:', error); } }; /** * 智能预加载下一首歌曲 */ const preloadNextSongs = (currentIndex: number) => { if (playList.value.length <= 1) return; let nextIndex: number; if (playMode.value === 0) { // 顺序播放模式 if (currentIndex >= playList.value.length - 1) { return; } nextIndex = currentIndex + 1; } else { // 循环播放和随机播放模式 nextIndex = (currentIndex + 1) % playList.value.length; } const endIndex = Math.min(nextIndex + 2, playList.value.length); if (nextIndex < playList.value.length) { fetchSongs(nextIndex, endIndex); // 循环模式且接近列表末尾,预加载列表开头 if ( (playMode.value === 1 || playMode.value === 2) && nextIndex + 1 >= playList.value.length && playList.value.length > 2 ) { setTimeout(() => { fetchSongs(0, 1); }, 1000); } } }; /** * 应用随机播放 */ const shufflePlayList = () => { console.log('[PlaylistStore] shufflePlayList called'); if (playList.value.length === 0) return; // 保存原始列表 if (originalPlayList.value.length === 0) { console.log('[PlaylistStore] Saving original list, length:', playList.value.length); originalPlayList.value = [...playList.value]; } const currentSong = playList.value[playListIndex.value]; console.log('[PlaylistStore] Current song before shuffle:', currentSong?.name); // 执行洗牌 const shuffled = performShuffle([...playList.value], currentSong); // 确保触发 shallowRef 的响应式 playList.value = [...shuffled]; playListIndex.value = 0; console.log('[PlaylistStore] List shuffled, new length:', playList.value.length); console.log('[PlaylistStore] New first song:', playList.value[0]?.name); }; /** * 恢复原始播放列表顺序 */ const restoreOriginalOrder = () => { console.log('[PlaylistStore] restoreOriginalOrder called'); if (originalPlayList.value.length === 0) return; const currentSong = playList.value[playListIndex.value]; console.log('[PlaylistStore] Current song before restore:', currentSong?.name); playList.value = [...originalPlayList.value]; originalPlayList.value = []; // 找到当前歌曲在原始列表中的索引 if (currentSong) { const index = playList.value.findIndex((s) => s.id === currentSong.id); if (index !== -1) { playListIndex.value = index; } } console.log('[PlaylistStore] Original order restored, new index:', playListIndex.value); }; /** * 设置播放列表 */ const setPlayList = ( list: SongResult[], keepIndex: boolean = false, fromIntelligenceMode: boolean = false ) => { // 如果不是从心动模式调用,清除心动模式状态 if (!fromIntelligenceMode) { const intelligenceStore = useIntelligenceModeStore(); if (intelligenceStore.isIntelligenceMode) { intelligenceStore.clearIntelligenceMode(); } } if (list.length === 0) { playList.value = []; playListIndex.value = 0; originalPlayList.value = []; return; } const playerCore = usePlayerCoreStore(); const { playMusic } = storeToRefs(playerCore); // 根据当前播放模式处理新的播放列表 if (playMode.value === 2) { // 随机模式 console.log('随机模式下设置新播放列表,保存原始顺序并洗牌'); originalPlayList.value = [...list]; const currentSong = playMusic.value; const shuffledList = performShuffle(list, currentSong); if (currentSong && currentSong.id) { const currentSongIndex = shuffledList.findIndex((song) => song.id === currentSong.id); playListIndex.value = currentSongIndex !== -1 ? 0 : keepIndex ? Math.max(0, playListIndex.value) : 0; } else { playListIndex.value = keepIndex ? Math.max(0, playListIndex.value) : 0; } playList.value = shuffledList; } else { console.log('顺序/循环模式下设置新播放列表'); if (originalPlayList.value.length > 0) { originalPlayList.value = []; } if (!keepIndex) { const foundIndex = list.findIndex((item) => item.id === playMusic.value.id); playListIndex.value = foundIndex !== -1 ? foundIndex : 0; } playList.value = list; } // pinia-plugin-persistedstate 会自动保存状态 }; /** * 添加到下一首播放 */ const addToNextPlay = (song: SongResult) => { const list = [...playList.value]; const currentIndex = playListIndex.value; // 如果歌曲已在播放列表中,先移除 const existingIndex = list.findIndex((item) => item.id === song.id); if (existingIndex !== -1) { list.splice(existingIndex, 1); if (existingIndex <= currentIndex) { playListIndex.value = Math.max(0, playListIndex.value - 1); } } // 插入到当前播放歌曲的下一个位置 const insertIndex = playListIndex.value + 1; list.splice(insertIndex, 0, song); setPlayList(list, true); }; /** * 从播放列表移除歌曲 */ const removeFromPlayList = (id: number | string) => { const index = playList.value.findIndex((item) => item.id === id); if (index === -1) return; const playerCore = usePlayerCoreStore(); const { playMusic } = storeToRefs(playerCore); // 如果删除的是当前播放的歌曲,先切换到下一首 if (id === playMusic.value.id) { nextPlay(); } const newPlayList = [...playList.value]; newPlayList.splice(index, 1); setPlayList(newPlayList); }; /** * 清空播放列表 */ const clearPlayAll = async () => { const { audioService } = await import('@/services/audioService'); const playerCore = usePlayerCoreStore(); audioService.pause(); setTimeout(() => { playerCore.playMusic = {} as SongResult; playerCore.playMusicUrl = ''; playList.value = []; playListIndex.value = 0; originalPlayList.value = []; // 只清除 playerCore 的 localStorage(这些由 playerCore store 管理) localStorage.removeItem('currentPlayMusic'); localStorage.removeItem('currentPlayMusicUrl'); // playlist 状态由 pinia-plugin-persistedstate 自动管理 }, 500); }; /** * 切换播放模式 */ const togglePlayMode = async () => { const { useUserStore } = await import('./user'); const userStore = useUserStore(); const wasRandom = playMode.value === 2; const wasIntelligence = playMode.value === 3; let newMode = (playMode.value + 1) % 4; // 如果要切换到心动模式,但用户未使用cookie登录,则跳过 if (newMode === 3 && (!userStore.user || userStore.loginType !== 'cookie')) { console.log('跳过心动模式:需要cookie登录'); newMode = 0; } const isRandom = newMode === 2; const isIntelligence = newMode === 3; console.log(`[PlaylistStore] togglePlayMode: ${playMode.value} -> ${newMode}`); playMode.value = newMode; // 切换到随机模式时洗牌 if (isRandom && !wasRandom && playList.value.length > 0) { shufflePlayList(); console.log('切换到随机模式,洗牌播放列表'); } // 从随机模式切换出去时恢复原始顺序 if (!isRandom && wasRandom) { restoreOriginalOrder(); console.log('切换出随机模式,恢复原始顺序'); } // 切换到心动模式 if (isIntelligence && !wasIntelligence) { console.log('切换到心动模式'); const intelligenceStore = useIntelligenceModeStore(); await intelligenceStore.playIntelligenceMode(); } // 从心动模式切换出去 if (!isIntelligence && wasIntelligence) { console.log('退出心动模式'); const intelligenceStore = useIntelligenceModeStore(); intelligenceStore.clearIntelligenceMode(); } }; /** * 下一首 * @param singleTrackRetryCount 单曲重试次数(同一首歌的重试) */ const _nextPlay = async (singleTrackRetryCount: number = 0) => { try { if (playList.value.length === 0) { return; } const playerCore = usePlayerCoreStore(); const sleepTimerStore = useSleepTimerStore(); // 检查是否超过最大连续失败次数 if (consecutiveFailCount.value >= MAX_CONSECUTIVE_FAILS) { console.error(`[nextPlay] 连续${MAX_CONSECUTIVE_FAILS}首歌曲播放失败,停止播放`); message.warning(i18n.global.t('player.consecutiveFailsError')); consecutiveFailCount.value = 0; // 重置计数器 playerCore.setIsPlay(false); return; } // 检查是否是播放列表的最后一首且设置了播放列表结束定时 if ( playMode.value === 0 && playListIndex.value === playList.value.length - 1 && sleepTimerStore.sleepTimer.type === 'end' ) { sleepTimerStore.stopPlayback(); return; } const currentIndex = playListIndex.value; const nowPlayListIndex = (playListIndex.value + 1) % playList.value.length; const nextSong = { ...playList.value[nowPlayListIndex] }; console.log( `[nextPlay] 尝试播放: ${nextSong.name}, 索引: ${currentIndex} -> ${nowPlayListIndex}, 单曲重试: ${singleTrackRetryCount}/${SINGLE_TRACK_MAX_RETRIES}, 连续失败: ${consecutiveFailCount.value}/${MAX_CONSECUTIVE_FAILS}` ); console.log( '[nextPlay] Current mode:', playMode.value, 'Playlist length:', playList.value.length ); // 先尝试播放歌曲 const success = await playerCore.handlePlayMusic(nextSong, true); if (success) { // 播放成功,重置所有计数器并更新索引 consecutiveFailCount.value = 0; playListIndex.value = nowPlayListIndex; console.log(`[nextPlay] 播放成功,索引已更新为: ${nowPlayListIndex}`); console.log( '[nextPlay] New current song in list:', playList.value[playListIndex.value]?.name ); sleepTimerStore.handleSongChange(); } else { console.error(`[nextPlay] 播放失败: ${nextSong.name}`); // 单曲重试逻辑 if (singleTrackRetryCount < SINGLE_TRACK_MAX_RETRIES) { console.log( `[nextPlay] 单曲重试 ${singleTrackRetryCount + 1}/${SINGLE_TRACK_MAX_RETRIES}` ); // 不更新索引,重试同一首歌 setTimeout(() => { _nextPlay(singleTrackRetryCount + 1); }, 1000); } else { // 单曲重试次数用尽,递增连续失败计数,尝试下一首 consecutiveFailCount.value++; console.log( `[nextPlay] 单曲重试用尽,连续失败计数: ${consecutiveFailCount.value}/${MAX_CONSECUTIVE_FAILS}` ); if (playList.value.length > 1) { // 更新索引到失败的歌曲位置,这样下次递归调用会继续往下 playListIndex.value = nowPlayListIndex; message.warning(i18n.global.t('player.parseFailedPlayNext')); // 延迟后尝试下一首(重置单曲重试计数) setTimeout(() => { _nextPlay(0); }, 500); } else { // 只有一首歌且失败 message.error(i18n.global.t('player.playFailed')); playerCore.setIsPlay(false); } } } } catch (error) { console.error('切换下一首出错:', error); } }; const nextPlay = useThrottleFn(_nextPlay, 500); /** * 上一首 */ const _prevPlay = async () => { try { if (playList.value.length === 0) { return; } const playerCore = usePlayerCoreStore(); const currentIndex = playListIndex.value; const nowPlayListIndex = (playListIndex.value - 1 + playList.value.length) % playList.value.length; const prevSong = { ...playList.value[nowPlayListIndex] }; console.log( `[prevPlay] 尝试播放上一首: ${prevSong.name}, 索引: ${currentIndex} -> ${nowPlayListIndex}` ); let success = false; let retryCount = 0; const maxRetries = 2; // 先尝试播放歌曲,成功后再更新索引 while (!success && retryCount < maxRetries) { success = await playerCore.handlePlayMusic(prevSong); if (!success) { retryCount++; console.error(`播放上一首失败,尝试 ${retryCount}/${maxRetries}`); if (retryCount >= maxRetries) { console.error('多次尝试播放失败,将从播放列表中移除此歌曲'); const newPlayList = [...playList.value]; newPlayList.splice(nowPlayListIndex, 1); if (newPlayList.length > 0) { const keepCurrentIndexPosition = true; setPlayList(newPlayList, keepCurrentIndexPosition); if (newPlayList.length === 1) { playListIndex.value = 0; } else { const newPrevIndex = (playListIndex.value - 1 + newPlayList.length) % newPlayList.length; playListIndex.value = newPrevIndex; } setTimeout(() => { prevPlay(); }, 300); return; } else { console.error('播放列表为空,停止尝试'); break; } } } } if (success) { // 播放成功,更新索引 playListIndex.value = nowPlayListIndex; console.log(`[prevPlay] 播放成功,索引已更新为: ${nowPlayListIndex}`); } else { console.error(`[prevPlay] 播放上一首失败,保持当前索引: ${currentIndex}`); playerCore.setIsPlay(false); message.error(i18n.global.t('player.playFailed')); } } catch (error) { console.error('切换上一首出错:', error); } }; const prevPlay = useThrottleFn(_prevPlay, 500); /** * 设置播放列表抽屉显示状态 */ const setPlayListDrawerVisible = (value: boolean) => { playListDrawerVisible.value = value; }; /** * 设置播放(兼容旧API) */ const setPlay = async (song: SongResult) => { try { const playerCore = usePlayerCoreStore(); // 检查URL是否已过期 if (song.expiredAt && song.expiredAt < Date.now()) { console.info(`歌曲URL已过期,重新获取: ${song.name}`); song.playMusicUrl = undefined; song.expiredAt = undefined; } // 如果是当前正在播放的音乐,则切换播放/暂停状态 if ( playerCore.playMusic.id === song.id && playerCore.playMusic.playMusicUrl === song.playMusicUrl && !song.isFirstPlay ) { if (playerCore.play) { playerCore.setPlayMusic(false); const { audioService } = await import('@/services/audioService'); audioService.getCurrentSound()?.pause(); playerCore.userPlayIntent = false; } else { playerCore.setPlayMusic(true); playerCore.userPlayIntent = true; const { audioService } = await import('@/services/audioService'); const sound = audioService.getCurrentSound(); if (sound) { sound.play(); // 在恢复播放时也进行状态检测,防止URL已过期导致无声 playerCore.checkPlaybackState(playerCore.playMusic); } } return; } if (song.isFirstPlay) { song.isFirstPlay = false; } // 查找歌曲在播放列表中的索引 const songIndex = playList.value.findIndex( (item: SongResult) => item.id === song.id && item.source === song.source ); // 更新播放索引 if (songIndex !== -1 && songIndex !== playListIndex.value) { console.log('歌曲索引不匹配,更新为:', songIndex); playListIndex.value = songIndex; } const success = await playerCore.handlePlayMusic(song); // playerCore 的状态由其自己的 store 管理 if (success) { playerCore.isPlay = true; // 预加载下一首歌曲 if (songIndex !== -1) { setTimeout(() => { preloadNextSongs(playListIndex.value); }, 3000); } } return success; } catch (error) { console.error('设置播放失败:', error); return false; } }; /** * 初始化播放列表 * 注意:状态已由 pinia-plugin-persistedstate 自动恢复 * 这里只需要处理特殊逻辑(如随机模式的恢复) */ const initializePlaylist = async () => { // 重启后恢复随机播放状态 if (playMode.value === 2 && playList.value.length > 0) { if (originalPlayList.value.length === 0) { console.log('重启后恢复随机播放模式,重新洗牌播放列表'); shufflePlayList(); } else { console.log('重启后恢复随机播放模式,播放列表已是洗牌状态'); } } }; return { // 状态 playList, playListIndex, playMode, originalPlayList, playListDrawerVisible, // Computed currentPlayList, currentPlayListIndex, // Actions setPlayList, addToNextPlay, removeFromPlayList, clearPlayAll, togglePlayMode, shufflePlayList, restoreOriginalOrder, preloadNextSongs, nextPlay: nextPlay as unknown as typeof _nextPlay, prevPlay: prevPlay as unknown as typeof _prevPlay, setPlayListDrawerVisible, setPlay, initializePlaylist, fetchSongs, updateSong: (song: SongResult) => { const index = playList.value.findIndex( (item) => item.id === song.id && item.source === song.source ); if (index !== -1) { playList.value[index] = song; // 触发响应式更新 playList.value = [...playList.value]; } } }; }, { // 配置 pinia-plugin-persistedstate persist: { key: 'playlist-store', storage: localStorage, // 持久化所有状态,除了 playListDrawerVisible(UI 状态不需要持久化) pick: ['playList', 'playListIndex', 'playMode', 'originalPlayList'] } } ); ================================================ FILE: src/renderer/store/modules/recommend.ts ================================================ import { defineStore } from 'pinia'; import { ref } from 'vue'; import { getDayRecommend } from '@/api/home'; import type { IDayRecommend } from '@/types/day_recommend'; import type { SongResult } from '@/types/music'; export const useRecommendStore = defineStore('recommend', () => { const dailyRecommendSongs = ref([]); const fetchDailyRecommendSongs = async () => { try { const { data } = await getDayRecommend(); const recommendData = data.data as unknown as IDayRecommend; if (recommendData && Array.isArray(recommendData.dailySongs)) { dailyRecommendSongs.value = recommendData.dailySongs as any; console.log(`[Recommend Store] 已加载 ${recommendData.dailySongs.length} 首每日推荐歌曲。`); } else { dailyRecommendSongs.value = []; } } catch (error) { console.error('[Recommend Store] 获取每日推荐失败:', error); dailyRecommendSongs.value = []; } }; const replaceSongInDailyRecommend = (oldSongId: number | string, newSong: SongResult) => { const index = dailyRecommendSongs.value.findIndex((song) => song.id === oldSongId); if (index !== -1) { dailyRecommendSongs.value.splice(index, 1, newSong as any); console.log(`[Recommend Store] 已将歌曲 ${oldSongId} 替换为 ${newSong.name}`); } else { console.warn(`[Recommend Store] 未在日推列表中找到要替换的歌曲ID: ${oldSongId}`); } }; return { dailyRecommendSongs, fetchDailyRecommendSongs, replaceSongInDailyRecommend }; }); ================================================ FILE: src/renderer/store/modules/search.ts ================================================ import { defineStore } from 'pinia'; import { ref } from 'vue'; export const useSearchStore = defineStore('search', () => { const searchValue = ref(''); const searchType = ref(1); const setSearchValue = (value: string) => { searchValue.value = value; }; const setSearchType = (type: number) => { searchType.value = type; }; return { searchValue, searchType, setSearchValue, setSearchType }; }); ================================================ FILE: src/renderer/store/modules/settings.ts ================================================ import { cloneDeep, isArray, mergeWith } from 'lodash'; import { defineStore } from 'pinia'; import { ref, watch } from 'vue'; import setDataDefault from '@/../main/set.json'; import homeRouter from '@/router/home'; import { useMenuStore } from '@/store/modules/menu'; import { isElectron } from '@/utils'; import { applyTheme, getCurrentTheme, getSystemTheme, ThemeType, watchSystemTheme } from '@/utils/theme'; export const useSettingsStore = defineStore('settings', () => { const theme = ref(getCurrentTheme()); const isMobile = ref(false); const isMiniMode = ref(false); const showUpdateModal = ref(false); const showArtistDrawer = ref(false); const currentArtistId = ref(null); const systemFonts = ref<{ label: string; value: string }[]>([ { label: '系统默认', value: 'system-ui' } ]); const showDownloadDrawer = ref(false); // 系统主题监听器清理函数 let systemThemeCleanup: (() => void) | null = null; // 先声明 setData ref 但不初始化 const setData = ref({}); // 先定义 setSetData 函数 const setSetData = (data: any) => { // 合并现有设置和新设置 const mergedData = { ...setData.value, ...data }; if (isElectron) { window.electron.ipcRenderer.send('set-store-value', 'set', cloneDeep(mergedData)); } else { localStorage.setItem('appSettings', JSON.stringify(cloneDeep(mergedData))); } setData.value = cloneDeep(mergedData); }; // 初始化时先从存储中读取设置 const getInitialSettings = () => { // 从存储中获取保存的设置 const savedSettings = isElectron ? window.electron.ipcRenderer.sendSync('get-store-value', 'set') : JSON.parse(localStorage.getItem('appSettings') || '{}'); // 自定义合并策略:如果是数组,直接使用源数组(覆盖默认值) const customizer = (_objValue: any, srcValue: any) => { if (isArray(srcValue)) { return srcValue; } return undefined; }; // 合并默认设置和保存的设置 const mergedSettings = mergeWith({}, setDataDefault, savedSettings, customizer); // 更新设置并返回 setSetData(mergedSettings); return mergedSettings; }; // 初始化 setData setData.value = getInitialSettings(); /** * 保存导入的自定义API插件 * @param plugin 包含name和content的对象 */ const setCustomApiPlugin = (plugin: { name: string; content: string }) => { setSetData({ customApiPlugin: plugin.content, customApiPluginName: plugin.name }); }; const toggleTheme = () => { if (setData.value.autoTheme) { // 如果是自动模式,切换到手动模式并设置相反的主题 const newTheme = theme.value === 'dark' ? 'light' : 'dark'; setSetData({ autoTheme: false, manualTheme: newTheme }); theme.value = newTheme; applyTheme(newTheme); // 停止监听系统主题 if (systemThemeCleanup) { systemThemeCleanup(); systemThemeCleanup = null; } } else { // 手动模式下正常切换 const newTheme = theme.value === 'dark' ? 'light' : 'dark'; theme.value = newTheme; setSetData({ manualTheme: newTheme }); applyTheme(newTheme); } }; const setAutoTheme = (auto: boolean) => { setSetData({ autoTheme: auto }); if (auto) { // 启用自动模式 const systemTheme = getSystemTheme(); theme.value = systemTheme; applyTheme(systemTheme); // 开始监听系统主题变化 systemThemeCleanup = watchSystemTheme((newTheme) => { if (setData.value.autoTheme) { theme.value = newTheme; applyTheme(newTheme); } }); } else { // 切换到手动模式 const manualTheme = setData.value.manualTheme || 'light'; theme.value = manualTheme; applyTheme(manualTheme); // 停止监听系统主题 if (systemThemeCleanup) { systemThemeCleanup(); systemThemeCleanup = null; } } }; const setMiniMode = (value: boolean) => { isMiniMode.value = value; }; const setShowUpdateModal = (value: boolean) => { showUpdateModal.value = value; }; const setShowArtistDrawer = (show: boolean) => { showArtistDrawer.value = show; if (!show) { currentArtistId.value = null; } }; const setCurrentArtistId = (id: number) => { currentArtistId.value = id; }; const setSystemFonts = (fonts: string[]) => { systemFonts.value = [ { label: '系统默认', value: 'system-ui' }, ...fonts.map((font) => ({ label: font, value: font })) ]; }; const setShowDownloadDrawer = (show: boolean) => { showDownloadDrawer.value = show; }; const setLanguage = (language: string) => { setSetData({ language }); if (isElectron) { window.electron.ipcRenderer.send('change-language', language); } }; const initializeSettings = () => { // const savedSettings = getInitialSettings(); // setData.value = savedSettings; }; const initializeTheme = () => { // 根据设置初始化主题 if (setData.value.autoTheme) { setAutoTheme(true); } else { const manualTheme = setData.value.manualTheme || getCurrentTheme(); theme.value = manualTheme; applyTheme(manualTheme); } }; const initializeSystemFonts = async () => { if (!isElectron) return; if (systemFonts.value.length > 1) return; try { const fonts = await window.api.invoke('get-system-fonts'); setSystemFonts(fonts); } catch (error) { console.error('获取系统字体失败:', error); } }; // 计算移动端状态的函数 const calculateMobileStatus = () => { const userAgentFlag = navigator.userAgent.match( /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i ); const isMobileWidth = window.innerWidth < 500; const isMobileDevice = !!userAgentFlag || isMobileWidth; const tabletMode = setData.value?.tabletMode; return isMobileDevice && !tabletMode; }; // 更新移动端状态和DOM类 const updateMobileStatus = () => { const menuStore = useMenuStore(); const shouldUseMobileStyle = calculateMobileStatus(); // 更新store状态 if (shouldUseMobileStyle) { menuStore.setMenus(homeRouter.filter((item) => item.meta.isMobile)); } else { menuStore.setMenus(homeRouter); } // 更新DOM类 if (shouldUseMobileStyle) { document.documentElement.classList.add('mobile'); document.documentElement.classList.remove('pc'); } else { document.documentElement.classList.add('pc'); document.documentElement.classList.remove('mobile'); } isMobile.value = shouldUseMobileStyle; }; // 监听平板模式变化 watch( () => setData.value?.tabletMode, () => { updateMobileStatus(); }, { immediate: true } ); // 监听窗口大小变化 if (typeof window !== 'undefined') { window.addEventListener('resize', updateMobileStatus); } return { setData, theme, isMobile, isMiniMode, showUpdateModal, showArtistDrawer, currentArtistId, systemFonts, showDownloadDrawer, setSetData, toggleTheme, setAutoTheme, setMiniMode, setShowUpdateModal, setShowArtistDrawer, setCurrentArtistId, setSystemFonts, setShowDownloadDrawer, setLanguage, initializeSettings, initializeTheme, initializeSystemFonts, setCustomApiPlugin }; }); ================================================ FILE: src/renderer/store/modules/sleepTimer.ts ================================================ import { defineStore } from 'pinia'; import { computed, ref } from 'vue'; import i18n from '@/../i18n/renderer'; import { getLocalStorageItem, setLocalStorageItem } from '@/utils/playerUtils'; // 定时关闭类型 export enum SleepTimerType { NONE = 'none', TIME = 'time', SONGS = 'songs', PLAYLIST_END = 'end' } // 定时关闭信息 export interface SleepTimerInfo { type: SleepTimerType; value: number; endTime?: number; startSongIndex?: number; remainingSongs?: number; } /** * 定时关闭管理 Store * 负责:定时关闭功能 */ export const useSleepTimerStore = defineStore('sleepTimer', () => { // ==================== 状态 ==================== const sleepTimer = ref( getLocalStorageItem('sleepTimer', { type: SleepTimerType.NONE, value: 0 }) ); const showSleepTimer = ref(false); const timerInterval = ref(null); // ==================== Computed ==================== const currentSleepTimer = computed(() => sleepTimer.value); const hasSleepTimerActive = computed(() => sleepTimer.value.type !== SleepTimerType.NONE); const sleepTimerRemainingTime = computed(() => { if (sleepTimer.value.type === SleepTimerType.TIME && sleepTimer.value.endTime) { const remaining = Math.max(0, sleepTimer.value.endTime - Date.now()); return Math.ceil(remaining / 60000); } return 0; }); const sleepTimerRemainingSongs = computed(() => { if (sleepTimer.value.type === SleepTimerType.SONGS) { return sleepTimer.value.remainingSongs || 0; } return 0; }); // ==================== Actions ==================== /** * 按时间设置定时关闭 */ const setSleepTimerByTime = (minutes: number) => { clearSleepTimer(); if (minutes <= 0) { return false; } const endTime = Date.now() + minutes * 60 * 1000; sleepTimer.value = { type: SleepTimerType.TIME, value: minutes, endTime }; setLocalStorageItem('sleepTimer', sleepTimer.value); timerInterval.value = window.setInterval(() => { checkSleepTimer(); }, 1000) as unknown as number; console.log(`设置定时关闭: ${minutes}分钟后`); return true; }; /** * 按歌曲数设置定时关闭 */ const setSleepTimerBySongs = async (songs: number) => { clearSleepTimer(); if (songs <= 0) { return false; } const { usePlaylistStore } = await import('./playlist'); const playlistStore = usePlaylistStore(); sleepTimer.value = { type: SleepTimerType.SONGS, value: songs, startSongIndex: playlistStore.playListIndex, remainingSongs: songs }; setLocalStorageItem('sleepTimer', sleepTimer.value); console.log(`设置定时关闭: 再播放${songs}首歌后`); return true; }; /** * 播放列表结束时关闭 */ const setSleepTimerAtPlaylistEnd = () => { clearSleepTimer(); sleepTimer.value = { type: SleepTimerType.PLAYLIST_END, value: 0 }; setLocalStorageItem('sleepTimer', sleepTimer.value); console.log('设置定时关闭: 播放列表结束时'); return true; }; /** * 取消定时关闭 */ const clearSleepTimer = () => { if (timerInterval.value) { window.clearInterval(timerInterval.value); timerInterval.value = null; } sleepTimer.value = { type: SleepTimerType.NONE, value: 0 }; setLocalStorageItem('sleepTimer', sleepTimer.value); console.log('取消定时关闭'); return true; }; /** * 检查定时关闭是否应该触发 */ const checkSleepTimer = () => { if (sleepTimer.value.type === SleepTimerType.NONE) { return; } if (sleepTimer.value.type === SleepTimerType.TIME && sleepTimer.value.endTime) { if (Date.now() >= sleepTimer.value.endTime) { stopPlayback(); } } }; /** * 停止播放并清除定时器 */ const stopPlayback = async () => { console.log('定时器触发:停止播放'); const { usePlayerCoreStore } = await import('./playerCore'); const playerCore = usePlayerCoreStore(); const { audioService } = await import('@/services/audioService'); if (playerCore.isPlaying) { playerCore.setIsPlay(false); audioService.pause(); } // 发送通知 if (window.electron?.ipcRenderer) { window.electron.ipcRenderer.send('show-notification', { title: i18n.global.t('player.sleepTimer.timerEnded'), body: i18n.global.t('player.sleepTimer.playbackStopped') }); } clearSleepTimer(); }; /** * 监听歌曲变化,处理按歌曲数定时和播放列表结束定时 */ const handleSongChange = async () => { console.log('歌曲已切换,检查定时器状态:', sleepTimer.value); // 处理按歌曲数定时 if ( sleepTimer.value.type === SleepTimerType.SONGS && sleepTimer.value.remainingSongs !== undefined ) { sleepTimer.value.remainingSongs--; console.log(`剩余歌曲数: ${sleepTimer.value.remainingSongs}`); setLocalStorageItem('sleepTimer', sleepTimer.value); if (sleepTimer.value.remainingSongs <= 0) { console.log('已播放完设定的歌曲数,停止播放'); stopPlayback(); setTimeout(() => { stopPlayback(); }, 1000); } } // 处理播放列表结束定时 if (sleepTimer.value.type === SleepTimerType.PLAYLIST_END) { const { usePlaylistStore } = await import('./playlist'); const playlistStore = usePlaylistStore(); const isLastSong = playlistStore.playListIndex === playlistStore.playList.length - 1; if (isLastSong && playlistStore.playMode !== 1) { console.log('已到达播放列表末尾,将在当前歌曲结束后停止播放'); sleepTimer.value = { type: SleepTimerType.SONGS, value: 1, remainingSongs: 1 }; setLocalStorageItem('sleepTimer', sleepTimer.value); } } }; /** * 设置定时器弹窗显示状态 */ const setShowSleepTimer = (value: boolean) => { showSleepTimer.value = value; }; return { // 状态 sleepTimer, showSleepTimer, // Computed currentSleepTimer, hasSleepTimerActive, sleepTimerRemainingTime, sleepTimerRemainingSongs, // Actions setSleepTimerByTime, setSleepTimerBySongs, setSleepTimerAtPlaylistEnd, clearSleepTimer, checkSleepTimer, stopPlayback, handleSongChange, setShowSleepTimer }; }); ================================================ FILE: src/renderer/store/modules/user.ts ================================================ import { defineStore } from 'pinia'; import { ref } from 'vue'; import { logout } from '@/api/login'; import { getLikedList } from '@/api/music'; import { getUserAlbumSublist, getUserPlaylist } from '@/api/user'; import { clearLoginStatus } from '@/utils/auth'; interface UserData { userId: number; [key: string]: any; } function getLocalStorageItem(key: string, defaultValue: T): T { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch { return defaultValue; } } export const useUserStore = defineStore('user', () => { // 状态 const user = ref(getLocalStorageItem('user', null)); const loginType = ref<'token' | 'cookie' | 'qr' | 'uid' | null>( getLocalStorageItem('loginType', null) ); const searchValue = ref(''); const searchType = ref(1); // 收藏的专辑 ID 列表 const collectedAlbumIds = ref>(new Set()); // 用户的歌单列表 const playList = ref([]); // 用户的专辑列表 const albumList = ref([]); // 方法 const setUser = (userData: UserData) => { user.value = userData; localStorage.setItem('user', JSON.stringify(userData)); }; const setLoginType = (type: typeof loginType.value) => { loginType.value = type; if (type) { localStorage.setItem('loginType', type); } else { localStorage.removeItem('loginType'); } }; const handleLogout = async () => { try { await logout(); user.value = null; loginType.value = null; collectedAlbumIds.value.clear(); playList.value = []; albumList.value = []; clearLoginStatus(); // 刷新 window.location.reload(); } catch (error) { console.error('登出失败:', error); // 即使API调用失败,也要清除本地状态 user.value = null; loginType.value = null; collectedAlbumIds.value.clear(); playList.value = []; albumList.value = []; clearLoginStatus(); window.location.reload(); } }; const setSearchValue = (value: string) => { searchValue.value = value; }; const setSearchType = (type: number) => { searchType.value = type; }; // 初始化歌单列表 const initializePlaylist = async () => { if (!user.value) { playList.value = []; return; } try { const { data } = await getUserPlaylist(user.value.userId, 1000, 0); playList.value = data?.playlist || []; console.log(`已加载 ${playList.value.length} 个歌单`); } catch (error) { console.error('获取歌单列表失败:', error); playList.value = []; } }; // 初始化专辑列表 const initializeAlbumList = async () => { if (!user.value || !localStorage.getItem('token')) { albumList.value = []; return; } try { const { data } = await getUserAlbumSublist({ limit: 1000, offset: 0 }); albumList.value = data?.data || []; console.log(`已加载 ${albumList.value.length} 个收藏专辑`); } catch (error) { console.error('获取专辑列表失败:', error); albumList.value = []; } }; // 初始化收藏的专辑ID列表 const initializeCollectedAlbums = async () => { if (!user.value || !localStorage.getItem('token')) { collectedAlbumIds.value.clear(); return; } try { const { data } = await getUserAlbumSublist({ limit: 1000, offset: 0 }); const albumIds = (data?.data || []).map((album: any) => album.id); collectedAlbumIds.value = new Set(albumIds); console.log(`已加载 ${albumIds.length} 个收藏专辑ID`); } catch (error) { console.error('获取收藏专辑列表失败:', error); collectedAlbumIds.value.clear(); } }; // 添加收藏专辑 const addCollectedAlbum = (albumId: number) => { collectedAlbumIds.value.add(albumId); }; // 移除收藏专辑 const removeCollectedAlbum = (albumId: number) => { collectedAlbumIds.value.delete(albumId); }; // 检查专辑是否已收藏 const isAlbumCollected = (albumId: number) => { return collectedAlbumIds.value.has(albumId); }; // 判断用户是否为VIP const isVip = computed(() => { if (!user.value) return false; // vipType: 0 非VIP, 11 VIP return user.value.vipType && user.value.vipType !== 0; }); // 初始化 const initializeUser = async () => { const savedUser = getLocalStorageItem('user', null); if (savedUser) { user.value = savedUser; // 如果用户已登录,获取收藏列表 if (localStorage.getItem('token')) { try { // 并行加载歌单、专辑和收藏ID列表 await Promise.all([ initializePlaylist(), initializeAlbumList(), initializeCollectedAlbums() ]); const { data } = await getLikedList(savedUser.userId); return data?.ids || []; } catch (error) { console.error('获取收藏列表失败:', error); return []; } } } return []; }; return { // 状态 user, loginType, searchValue, searchType, collectedAlbumIds, playList, albumList, isVip, // 方法 setUser, setLoginType, handleLogout, setSearchValue, setSearchType, initializeUser, initializePlaylist, initializeAlbumList, initializeCollectedAlbums, addCollectedAlbum, removeCollectedAlbum, isAlbumCollected }; }); ================================================ FILE: src/renderer/types/album.ts ================================================ export interface IAlbumNew { code: number; albums: Album[]; } export interface Album { name: string; id: number; type: string; size: number; picId: number; blurPicUrl: string; companyId: number; pic: number; picUrl: string; publishTime: number; description: string; tags: string; company: string; briefDesc: string; artist: Artist; songs?: any; alias: string[]; status: number; copyrightId: number; commentThreadId: string; artists: Artist2[]; paid: boolean; onSale: boolean; picId_str: string; } interface Artist2 { name: string; id: number; picId: number; img1v1Id: number; briefDesc: string; picUrl: string; img1v1Url: string; albumSize: number; alias: any[]; trans: string; musicSize: number; topicPerson: number; img1v1Id_str: string; } interface Artist { name: string; id: number; picId: number; img1v1Id: number; briefDesc: string; picUrl: string; img1v1Url: string; albumSize: number; alias: string[]; trans: string; musicSize: number; topicPerson: number; picId_str?: string; img1v1Id_str: string; transNames?: string[]; } ================================================ FILE: src/renderer/types/artist.ts ================================================ export interface IArtistDetail { videoCount: number; vipRights: VipRights; identify: Identify; artist: IArtist; blacklist: boolean; preferShow: number; showPriMsg: boolean; secondaryExpertIdentiy: SecondaryExpertIdentiy[]; eventCount: number; user: User; } interface User { backgroundUrl: string; birthday: number; detailDescription: string; authenticated: boolean; gender: number; city: number; signature: null; description: string; remarkName: null; shortUserName: string; accountStatus: number; locationStatus: number; avatarImgId: number; defaultAvatar: boolean; province: number; nickname: string; expertTags: null; djStatus: number; avatarUrl: string; accountType: number; authStatus: number; vipType: number; userName: string; followed: boolean; userId: number; lastLoginIP: string; lastLoginTime: number; authenticationTypes: number; mutual: boolean; createTime: number; anchor: boolean; authority: number; backgroundImgId: number; userType: number; experts: null; avatarDetail: AvatarDetail; } interface AvatarDetail { userType: number; identityLevel: number; identityIconUrl: string; } interface SecondaryExpertIdentiy { expertIdentiyId: number; expertIdentiyName: string; expertIdentiyCount: number; } export interface IArtist { id: number; cover: string; avatar: string; name: string; transNames: any[]; alias: any[]; identities: any[]; identifyTag: string[]; briefDesc: string; rank: Rank; albumSize: number; musicSize: number; mvSize: number; } interface Rank { rank: number; type: number; } interface Identify { imageUrl: string; imageDesc: string; actionUrl: string; } interface VipRights { rightsInfoDetailDtoList: RightsInfoDetailDtoList[]; oldProtocol: boolean; redVipAnnualCount: number; redVipLevel: number; now: number; } interface RightsInfoDetailDtoList { vipCode: number; expireTime: number; iconUrl: null; dynamicIconUrl: null; vipLevel: number; signIap: boolean; signDeduct: boolean; signIapDeduct: boolean; sign: boolean; } ================================================ FILE: src/renderer/types/bilibili.ts ================================================ export interface IBilibiliSearchResult { id: number; bvid: string; title: string; pic: string; duration: number | string; pubdate: number; ctime: number; author: string; view: number; danmaku: number; owner: { mid: number; name: string; face: string; }; stat: { view: number; danmaku: number; reply: number; favorite: number; coin: number; share: number; like: number; }; } export interface IBilibiliVideoDetail { aid: number; bvid: string; title: string; pic: string; desc: string; duration: number; pubdate: number; ctime: number; owner: { mid: number; name: string; face: string; }; stat: { view: number; danmaku: number; reply: number; favorite: number; coin: number; share: number; like: number; }; pages: IBilibiliPage[]; } export interface IBilibiliPage { cid: number; page: number; part: string; duration: number; dimension: { width: number; height: number; rotate: number; }; } export interface IBilibiliPlayUrl { durl?: { order: number; length: number; size: number; ahead: string; vhead: string; url: string; backup_url: string[]; }[]; dash?: { duration: number; minBufferTime: number; min_buffer_time: number; video: IBilibiliDashItem[]; audio: IBilibiliDashItem[]; }; support_formats: { quality: number; format: string; new_description: string; display_desc: string; }[]; accept_quality: number[]; accept_description: string[]; quality: number; format: string; timelength: number; high_format: string; } export interface IBilibiliDashItem { id: number; baseUrl: string; base_url: string; backupUrl: string[]; backup_url: string[]; bandwidth: number; mimeType: string; mime_type: string; codecs: string; width?: number; height?: number; frameRate?: string; frame_rate?: string; startWithSap?: number; start_with_sap?: number; codecid: number; } ================================================ FILE: src/renderer/types/day_recommend.ts ================================================ export interface IDayRecommend { dailySongs: DailySong[]; orderSongs: any[]; recommendReasons: RecommendReason[]; mvResourceInfos: null; } interface RecommendReason { songId: number; reason: string; reasonId: string; targetUrl: null; } interface DailySong { name: string; id: number; pst: number; t: number; ar: Ar[]; alia: string[]; pop: number; st: number; rt: null | string; fee: number; v: number; crbt: null; cf: string; al: Al; dt: number; h: H; m: H; l: H; sq: H | null; hr: H | null; a: null; cd: string; no: number; rtUrl: null; ftype: number; rtUrls: any[]; djId: number; copyright: number; s_id: number; mark: number; originCoverType: number; originSongSimpleData: OriginSongSimpleDatum | null; tagPicList: null; resourceState: boolean; version: number; songJumpInfo: null; entertainmentTags: null; single: number; noCopyrightRcmd: null; rtype: number; rurl: null; mst: number; cp: number; mv: number; publishTime: number; reason: null | string; videoInfo: VideoInfo; recommendReason: null | string; privilege: Privilege; alg: string; tns?: string[]; s_ctrp?: string; } interface Privilege { id: number; fee: number; payed: number; realPayed: number; st: number; pl: number; dl: number; sp: number; cp: number; subp: number; cs: boolean; maxbr: number; fl: number; pc: null; toast: boolean; flag: number; paidBigBang: boolean; preSell: boolean; playMaxbr: number; downloadMaxbr: number; maxBrLevel: string; playMaxBrLevel: string; downloadMaxBrLevel: string; plLevel: string; dlLevel: string; flLevel: string; rscl: null; freeTrialPrivilege: FreeTrialPrivilege; rightSource: number; chargeInfoList: ChargeInfoList[]; } interface ChargeInfoList { rate: number; chargeUrl: null; chargeMessage: null; chargeType: number; } interface FreeTrialPrivilege { resConsumable: boolean; userConsumable: boolean; listenType: number; cannotListenReason: number; playReason: null; } interface VideoInfo { moreThanOne: boolean; video: Video | null; } interface Video { vid: string; type: number; title: string; playTime: number; coverUrl: string; publishTime: number; artists: null; alias: null; } interface OriginSongSimpleDatum { songId: number; name: string; artists: Artist[]; albumMeta: Artist; } interface Artist { id: number; name: string; } interface H { br: number; fid: number; size: number; vd: number; sr: number; } interface Al { id: number; name: string; picUrl: string; tns: string[]; pic_str?: string; pic: number; } interface Ar { id: number; name: string; tns: any[]; alias: any[]; } ================================================ FILE: src/renderer/types/electron.d.ts ================================================ export interface IElectronAPI { minimize: () => void; maximize: () => void; close: () => void; dragStart: (_data: string) => void; miniTray: () => void; restart: () => void; openLyric: () => void; sendLyric: (_data: string) => void; unblockMusic: (_id: number) => Promise; importCustomApiPlugin: () => Promise<{ name: string; content: string } | null>; importLxMusicScript: () => Promise<{ name: string; content: string } | null>; onLanguageChanged: (_callback: (_locale: string) => void) => void; store: { get: (_key: string) => Promise; set: (_key: string, _value: any) => Promise; delete: (_key: string) => Promise; }; } declare global { interface Window { api: IElectronAPI; } } ================================================ FILE: src/renderer/types/index.ts ================================================ export interface IData { code: number; data: T; result: T; } ================================================ FILE: src/renderer/types/list.ts ================================================ export interface IList { playlists: Playlist[]; code: number; more: boolean; lasttime: number; total: number; } export interface Playlist { name: string; id: number; trackNumberUpdateTime: number; status: number; userId: number; createTime: number; updateTime: number; subscribedCount: number; trackCount: number; cloudTrackCount: number; coverImgUrl: string; coverImgId: number; description: string; tags: string[]; playCount: number; trackUpdateTime: number; specialType: number; totalDuration: number; creator: Creator; tracks?: any; subscribers: Subscriber[]; subscribed: boolean; commentThreadId: string; newImported: boolean; adType: number; highQuality: boolean; privacy: number; ordered: boolean; anonimous: boolean; coverStatus: number; recommendInfo?: any; shareCount: number; coverImgId_str?: string; commentCount: number; copywriter: string; tag: string; } interface Subscriber { defaultAvatar: boolean; province: number; authStatus: number; followed: boolean; avatarUrl: string; accountStatus: number; gender: number; city: number; birthday: number; userId: number; userType: number; nickname: string; signature: string; description: string; detailDescription: string; avatarImgId: number; backgroundImgId: number; backgroundUrl: string; authority: number; mutual: boolean; expertTags?: any; experts?: any; djStatus: number; vipType: number; remarkName?: any; authenticationTypes: number; avatarDetail?: any; avatarImgIdStr: string; backgroundImgIdStr: string; anchor: boolean; avatarImgId_str?: string; } interface Creator { defaultAvatar: boolean; province: number; authStatus: number; followed: boolean; avatarUrl: string; accountStatus: number; gender: number; city: number; birthday: number; userId: number; userType: number; nickname: string; signature: string; description: string; detailDescription: string; avatarImgId: number; backgroundImgId: number; backgroundUrl: string; authority: number; mutual: boolean; expertTags?: string[]; experts?: Expert; djStatus: number; vipType: number; remarkName?: any; authenticationTypes: number; avatarDetail?: AvatarDetail; avatarImgIdStr: string; backgroundImgIdStr: string; anchor: boolean; avatarImgId_str?: string; } interface AvatarDetail { userType: number; identityLevel: number; identityIconUrl: string; } interface Expert { '2': string; '1'?: string; } // 推荐歌单 export interface IRecommendList { hasTaste: boolean; code: number; category: number; result: IRecommendItem[]; } export interface IRecommendItem { id: number; type: number; name: string; copywriter: string; picUrl: string; canDislike: boolean; trackNumberUpdateTime: number; playCount: number; trackCount: number; highQuality: boolean; alg: string; } ================================================ FILE: src/renderer/types/listDetail.ts ================================================ export interface IListDetail { code: number; relatedVideos?: any; playlist: Playlist; urls?: any; privileges: Privilege[]; sharedPrivilege?: any; resEntrance?: any; } interface Privilege { id: number; fee: number; payed: number; realPayed: number; st: number; pl: number; dl: number; sp: number; cp: number; subp: number; cs: boolean; maxbr: number; fl: number; pc?: any; toast: boolean; flag: number; paidBigBang: boolean; preSell: boolean; playMaxbr: number; downloadMaxbr: number; rscl?: any; freeTrialPrivilege: FreeTrialPrivilege; chargeInfoList: ChargeInfoList[]; } interface ChargeInfoList { rate: number; chargeUrl?: any; chargeMessage?: any; chargeType: number; } interface FreeTrialPrivilege { resConsumable: boolean; userConsumable: boolean; } export interface Playlist { id: number; name: string; coverImgId: number; coverImgUrl: string; coverImgId_str: string; adType: number; userId: number; createTime: number; status: number; opRecommend: boolean; highQuality: boolean; newImported: boolean; updateTime: number; trackCount: number; specialType: number; privacy: number; trackUpdateTime: number; commentThreadId: string; playCount: number; trackNumberUpdateTime: number; subscribedCount: number; cloudTrackCount: number; ordered: boolean; description: string; tags: string[]; updateFrequency?: any; backgroundCoverId: number; backgroundCoverUrl?: any; titleImage: number; titleImageUrl?: any; englishTitle?: any; officialPlaylistType?: any; subscribers: Subscriber[]; subscribed: boolean; creator: Subscriber; tracks: Track[]; videoIds?: any; videos?: any; trackIds: TrackId[]; shareCount: number; commentCount: number; remixVideo?: any; sharedUsers?: any; historySharedUsers?: any; } interface TrackId { id: number; v: number; t: number; at: number; alg?: any; uid: number; rcmdReason: string; } interface Track { name: string; id: number; pst: number; t: number; ar: Ar[]; alia: string[]; pop: number; st: number; rt?: string; fee: number; v: number; crbt?: any; cf: string; al: Al; dt: number; h: H; m: H; l?: H; a?: any; cd: string; no: number; rtUrl?: any; ftype: number; rtUrls: any[]; djId: number; copyright: number; s_id: number; mark: number; originCoverType: number; originSongSimpleData?: any; single: number; noCopyrightRcmd?: any; mst: number; cp: number; mv: number; rtype: number; rurl?: any; publishTime: number; tns?: string[]; } interface H { br: number; fid: number; size: number; vd: number; } interface Al { id: number; name: string; picUrl: string; tns: any[]; pic_str?: string; pic: number; } interface Ar { id: number; name: string; tns: any[]; alias: any[]; } interface Subscriber { defaultAvatar: boolean; province: number; authStatus: number; followed: boolean; avatarUrl: string; accountStatus: number; gender: number; city: number; birthday: number; userId: number; userType: number; nickname: string; signature: string; description: string; detailDescription: string; avatarImgId: number; backgroundImgId: number; backgroundUrl: string; authority: number; mutual: boolean; expertTags?: any; experts?: any; djStatus: number; vipType: number; remarkName?: any; authenticationTypes: number; avatarDetail?: any; backgroundImgIdStr: string; anchor: boolean; avatarImgIdStr: string; avatarImgId_str: string; } ================================================ FILE: src/renderer/types/lxMusic.ts ================================================ /** * 落雪音乐 (LX Music) 自定义源类型定义 * * 参考文档: https://lxmusic.toside.cn/desktop/custom-source */ /** * 脚本元信息(从注释头解析) */ export type LxScriptInfo = { name: string; description?: string; version?: string; author?: string; homepage?: string; rawScript: string; }; /** * 支持的音质类型 */ export type LxQuality = '128k' | '320k' | 'flac' | 'flac24bit'; /** * 支持的音源 key * - kw: 酷我 * - kg: 酷狗 * - tx: QQ音乐 * - wy: 网易云 * - mg: 咪咕 * - local: 本地音乐 */ export type LxSourceKey = 'kw' | 'kg' | 'tx' | 'wy' | 'mg' | 'local'; /** * 音源配置 */ export type LxSourceConfig = { name: string; type: 'music'; actions: ('musicUrl' | 'lyric' | 'pic')[]; qualitys: LxQuality[]; }; /** * 初始化事件数据 */ export type LxInitedData = { openDevTools?: boolean; sources: Partial>; }; /** * 请求事件数据 */ export type LxRequestData = { source: LxSourceKey; action: 'musicUrl' | 'lyric' | 'pic'; info: { type: LxQuality | null; musicInfo: LxMusicInfo; }; }; /** * 落雪音乐信息格式 * 需要从 SongResult 转换而来 */ export type LxMusicInfo = { songmid: string | number; name: string; singer: string; album?: string; albumId?: string | number; source?: string; interval?: string; img?: string; types?: { type: LxQuality; size?: string }[]; }; /** * 歌词返回格式 */ export type LxLyricResult = { lyric: string; tlyric?: string | null; rlyric?: string | null; lxlyric?: string | null; }; /** * 存储在 settings 中的单个落雪音源配置 */ export type LxMusicScriptConfig = { id: string; // 唯一标识 name: string; // 用户自定义名称,可编辑 script: string; // 脚本内容 info: LxScriptInfo; // 解析的脚本元信息 sources: LxSourceKey[]; enabled: boolean; // 是否启用 createdAt: number; // 创建时间戳 }; /** * 存储在 settings 中的落雪音源列表 */ export type LxMusicApiList = { apis: LxMusicScriptConfig[]; activeId: string | null; // 当前激活的音源 ID }; /** * globalThis.lx API 的事件名称 */ export const LX_EVENT_NAMES = { inited: 'inited', request: 'request', updateAlert: 'updateAlert' } as const; /** * 落雪音源 key 到平台名称的映射 */ export const LX_SOURCE_NAMES: Record = { kw: '酷我', kg: '酷狗', tx: 'QQ音乐', wy: '网易云', mg: '咪咕', local: '本地' }; /** * 本项目音质到落雪音质的映射 */ export const QUALITY_TO_LX: Record = { standard: '128k', higher: '320k', exhigh: '320k', lossless: 'flac', hires: 'flac24bit', jyeffect: 'flac', sky: 'flac', dolby: 'flac', jymaster: 'flac24bit' }; ================================================ FILE: src/renderer/types/lyric.ts ================================================ export interface LyricConfig { hideCover: boolean; centerLyrics: boolean; fontSize: number; letterSpacing: number; fontWeight: number; lineHeight: number; showTranslation: boolean; theme: 'default' | 'light' | 'dark'; hidePlayBar: boolean; translationEngine?: 'none' | 'opencc'; pureModeEnabled: boolean; hideMiniPlayBar: boolean; hideLyrics: boolean; contentWidth: number; // 内容区域宽度百分比 // 移动端配置 mobileLayout: 'default' | 'ios' | 'android'; mobileCoverStyle: 'record' | 'square' | 'full'; mobileShowLyricLines: number; // 背景自定义功能 useCustomBackground: boolean; // 是否使用自定义背景 backgroundMode: 'solid' | 'gradient' | 'image' | 'css'; // 背景模式 solidColor: string; // 纯色背景颜色值 gradientColors: { colors: string[]; // 渐变颜色数组 direction: string; // 渐变方向 }; backgroundImage?: string; // 图片背景 (Base64 或 URL) imageBlur: number; // 图片模糊度 (0-20px) imageBrightness: number; // 图片明暗度 (0-200%, 100为正常) customCss?: string; // 自定义 CSS 样式 } export const DEFAULT_LYRIC_CONFIG: LyricConfig = { hideCover: false, centerLyrics: false, fontSize: 22, letterSpacing: 0, fontWeight: 500, lineHeight: 2, showTranslation: true, theme: 'default', hidePlayBar: true, hideMiniPlayBar: false, pureModeEnabled: false, hideLyrics: false, contentWidth: 75, // 默认100%宽度 // 移动端默认配置 mobileLayout: 'ios', mobileCoverStyle: 'full', mobileShowLyricLines: 3, // 翻译引擎: 'none' or 'opencc' translationEngine: 'none', // 背景自定义功能默认值 useCustomBackground: false, backgroundMode: 'solid', solidColor: '#1a1a1a', gradientColors: { colors: ['#1a1a1a', '#000000'], direction: 'to bottom' }, backgroundImage: undefined, imageBlur: 0, imageBrightness: 100, customCss: undefined }; export interface ILyric { sgc: boolean; sfy: boolean; qfy: boolean; lrc: Lrc; klyric: Lrc; tlyric: Lrc; code: number; } interface Lrc { version: number; lyric: string; } ================================================ FILE: src/renderer/types/music.ts ================================================ // 音乐平台类型 export type Platform = | 'qq' | 'migu' | 'kugou' | 'kuwo' | 'pyncmd' | 'joox' | 'bilibili' | 'gdmusic' | 'lxMusic'; // 默认平台列表 export const DEFAULT_PLATFORMS: Platform[] = [ 'lxMusic', 'migu', 'kugou', 'kuwo', 'pyncmd', 'bilibili' ]; export interface IRecommendMusic { code: number; category: number; result: SongResult[]; } // 逐字歌词单词数据 export interface IWordData { text: string; startTime: number; duration: number; space?: boolean; } export interface ILyricText { text: string; trText: string; words?: IWordData[]; hasWordByWord?: boolean; startTime?: number; duration?: number; } export interface ILyric { lrcTimeArray: number[]; lrcArray: ILyricText[]; // 新增字段标识是否包含逐字歌词 hasWordByWord?: boolean; } export interface SongResult { id: string | number; name: string; picUrl: string; playCount?: number; song?: any; copywriter?: string; type?: number; canDislike?: boolean; program?: any; alg?: string; ar: Artist[]; artists?: Artist[]; al: Album; album?: Album; count: number; playMusicUrl?: string; playLoading?: boolean; lyric?: ILyric; backgroundColor?: string; primaryColor?: string; bilibiliData?: { bvid: string; cid: number; }; source?: 'netease' | 'bilibili'; // 过期时间 expiredAt?: number; // 获取时间 createdAt?: number; // 时长 duration?: number; dt?: number; isFirstPlay?: boolean; } export interface Song { name: string; id: number; position: number; alias: string[]; status: number; fee: number; copyrightId: number; disc: string; no: number; artists: Artist[]; album: Album; starred: boolean; popularity: number; score: number; starredNum: number; duration: number; playedNum: number; dayPlays: number; hearTime: number; ringtone: string; crbt?: any; audition?: any; copyFrom: string; commentThreadId: string; rtUrl?: any; ftype: number; rtUrls: any[]; copyright: number; transName?: any; sign?: any; mark: number; originCoverType: number; originSongSimpleData?: any; single: number; noCopyrightRcmd?: any; rtype: number; rurl?: any; mvid: number; bMusic: BMusic; mp3Url?: any; hMusic: BMusic; mMusic: BMusic; lMusic: BMusic; exclusive: boolean; privilege: Privilege; count?: number; playLoading?: boolean; picUrl?: string; ar: Artist[]; } interface Privilege { id: number; fee: number; payed: number; st: number; pl: number; dl: number; sp: number; cp: number; subp: number; cs: boolean; maxbr: number; fl: number; toast: boolean; flag: number; preSell: boolean; playMaxbr: number; downloadMaxbr: number; rscl?: any; freeTrialPrivilege: FreeTrialPrivilege; chargeInfoList: ChargeInfoList[]; } interface ChargeInfoList { rate: number; chargeUrl?: any; chargeMessage?: any; chargeType: number; } interface FreeTrialPrivilege { resConsumable: boolean; userConsumable: boolean; } interface BMusic { name?: any; id: number; size: number; extension: string; sr: number; dfsId: number; bitrate: number; playTime: number; volumeDelta: number; } interface Album { name: string; id: number; type: string; size: number; picId: number; blurPicUrl: string; companyId: number; pic: number; picUrl: string; publishTime: number; description: string; tags: string; company: string; briefDesc: string; artist: Artist; songs: any[]; alias: string[]; status: number; copyrightId: number; commentThreadId: string; artists: Artist[]; subType: string; transName?: any; onSale: boolean; mark: number; picId_str: string; } export interface Artist { name: string; id: number; picId: number; img1v1Id: number; briefDesc: string; picUrl: string; img1v1Url: string; albumSize: number; alias: any[]; trans: string; musicSize: number; topicPerson: number; } export interface IPlayMusicUrl { data: Datum[]; code: number; } interface Datum { id: number; url: string; br: number; size: number; md5: string; code: number; expi: number; type: string; gain: number; fee: number; uf?: any; payed: number; flag: number; canExtend: boolean; freeTrialInfo?: any; level: string; encodeType: string; freeTrialPrivilege: FreeTrialPrivilege; freeTimeTrialPrivilege: FreeTimeTrialPrivilege; urlSource: number; } interface FreeTimeTrialPrivilege { resConsumable: boolean; userConsumable: boolean; type: number; remainTime: number; } interface FreeTrialPrivilege { resConsumable: boolean; userConsumable: boolean; } export interface IArtists { id: number; name: string; picUrl: string | null; alias: string[]; albumSize: number; picId: number; fansGroup: null; img1v1Url: string; img1v1: number; trans: null; } // 音乐源类型定义 export type MusicSourceType = | 'tencent' | 'kugou' | 'migu' | 'netease' | 'joox' | 'ytmusic' | 'spotify' | 'qobuz' | 'deezer' | 'gdmusic'; // 更多音乐相关的类型可以在这里定义 ================================================ FILE: src/renderer/types/mv.ts ================================================ export interface IMvItem { id: number; cover: string; name: string; playCount: number; briefDesc?: any; desc?: any; artistName: string; artistId: number; duration: number; mark: number; mv: IMvData; lastRank: number; score: number; subed: boolean; artists: Artist[]; transNames?: string[]; alias?: string[]; } export interface IMvData { authId: number; status: number; id: number; title: string; subTitle: string; appTitle: string; aliaName: string; transName: string; pic4v3: number; pic16v9: number; caption: number; captionLanguage: string; style?: any; mottos: string; oneword?: any; appword: string; stars?: any; desc: string; area: string; type: string; subType: string; neteaseonly: number; upban: number; topWeeks: string; publishTime: string; online: number; score: number; plays: number; monthplays: number; weekplays: number; dayplays: number; fee: number; artists: Artist[]; videos: Video[]; } interface Video { tagSign: TagSign; tag: string; url: string; duration: number; size: number; width: number; height: number; container: string; md5: string; check: boolean; } interface TagSign { br: number; type: string; tagSign: string; resolution: number; mvtype: string; } interface Artist { id: number; name: string; } // { // "id": 14686812, // "url": "http://vodkgeyttp8.vod.126.net/cloudmusic/e18b/core/aa57/6f56150a35613ef77fc70b253bea4977.mp4?wsSecret=84a301277e05143de1dd912d2a4dbb0d&wsTime=1703668700", // "r": 1080, // "size": 215391070, // "md5": "", // "code": 200, // "expi": 3600, // "fee": 0, // "mvFee": 0, // "st": 0, // "promotionVo": null, // "msg": "" // } export interface IMvUrlData { id: number; url: string; r: number; size: number; md5: string; code: number; expi: number; fee: number; mvFee: number; st: number; promotionVo: null | any; msg: string; } ================================================ FILE: src/renderer/types/opencc-rust.d.ts ================================================ declare module 'https://cdn.jsdelivr.net/npm/opencc-rust/dist/opencc-rust.mjs' { export function initOpenccRust(): Promise; export function getConverter(): { convert: (text: string) => Promise; }; } // Allow wildcard import if different CDN URL is used declare module 'opencc-rust' { export function initOpenccRust(): Promise; export function getConverter(): { convert: (text: string) => Promise; }; } declare module './translation-engines/opencc' { export function init(): Promise; export function convert(text: string): Promise; } ================================================ FILE: src/renderer/types/playlist.ts ================================================ export interface IPlayListSort { code: number; all: SortAll; sub: SortAll[]; categories: SortCategories; } interface SortCategories { '0': string; '1': string; '2': string; '3': string; '4': string; } interface SortAll { name: string; resourceCount?: number; imgId?: number; imgUrl?: any; type?: number; category?: number; resourceType?: number; hot?: boolean; activity?: boolean; } ================================================ FILE: src/renderer/types/search.ts ================================================ export interface ISearchKeyword { code: number; message?: any; data: SearchKeywordData; } interface SearchKeywordData { showKeyword: string; realkeyword: string; searchType: number; action: number; alg: string; gap: number; source?: any; bizQueryInfo: string; } export interface IHotSearch { code: number; data: Datum[]; message: string; } interface Datum { searchWord: string; score: number; content: string; source: number; iconType: number; iconUrl?: string; url: string; alg: string; } export interface ISearchDetail { result: Result; code: number; } interface Result { song: Song2; code: number; mlog: Mlog2; playList: PlayList2; artist: Artist3; album: Album3; video: Video2; sim_query: Simquery2; djRadio: DjRadio2; rec_type?: any; talk: Talk2; rec_query: null[]; user: User2; order: string[]; } interface User2 { moreText: string; more: boolean; users: User[]; resourceIds: number[]; } interface User { defaultAvatar: boolean; province: number; authStatus: number; followed: boolean; avatarUrl: string; accountStatus: number; gender: number; city: number; birthday: number; userId: number; userType: number; nickname: string; signature: string; description: string; detailDescription: string; avatarImgId: number; backgroundImgId: number; backgroundUrl: string; authority: number; mutual: boolean; expertTags?: any; experts?: any; djStatus: number; vipType: number; remarkName?: any; authenticationTypes: number; avatarDetail?: any; anchor: boolean; avatarImgIdStr: string; backgroundImgIdStr: string; avatarImgId_str: string; alg: string; } interface Talk2 { more: boolean; talks: Talk[]; resourceIds: number[]; } interface Talk { talkId: number; shareUrl: string; talkName: string; shareCover: ShareCover; showCover: ShareCover; talkDes: string; follows: number; participations: number; showParticipations: number; status: number; time?: any; hasTag: boolean; alg: string; mlogCount: number; commentCount: number; } interface ShareCover { picKey: string; nosKey: string; width: number; height: number; url: string; } interface DjRadio2 { moreText: string; djRadios: DjRadio[]; more: boolean; resourceIds: number[]; } interface DjRadio { id: number; dj: Dj; name: string; picUrl: string; desc: string; subCount: number; programCount: number; createTime: number; categoryId: number; category: string; radioFeeType: number; feeScope: number; buyed: boolean; videos?: any; finished: boolean; underShelf: boolean; purchaseCount: number; price: number; originalPrice: number; discountPrice?: any; lastProgramCreateTime: number; lastProgramName?: any; lastProgramId: number; picId: number; rcmdText?: string; hightQuality: boolean; whiteList: boolean; liveInfo?: any; playCount: number; icon?: any; composeVideo: boolean; shareCount: number; likedCount: number; alg: string; commentCount: number; } interface Dj { defaultAvatar: boolean; province: number; authStatus: number; followed: boolean; avatarUrl: string; accountStatus: number; gender: number; city: number; birthday: number; userId: number; userType: number; nickname: string; signature: string; description: string; detailDescription: string; avatarImgId: number; backgroundImgId: number; backgroundUrl: string; authority: number; mutual: boolean; expertTags?: any; experts?: any; djStatus: number; vipType: number; remarkName?: any; authenticationTypes: number; avatarDetail?: any; anchor: boolean; avatarImgIdStr: string; backgroundImgIdStr: string; avatarImgId_str: string; } interface Simquery2 { sim_querys: Simquery[]; more: boolean; } interface Simquery { keyword: string; alg: string; } interface Video2 { moreText: string; more: boolean; videos: Video[]; resourceIds: number[]; } interface Video { coverUrl: string; title: string; durationms: number; playTime: number; type: number; creator: Creator2[]; aliaName?: any; transName?: any; vid: string; markTypes?: number[]; alg: string; } interface Creator2 { userId: number; userName: string; } interface Album3 { moreText: string; albums: Album2[]; more: boolean; resourceIds: number[]; } interface Album2 { name: string; id: number; type: string; size: number; picId: number; blurPicUrl: string; companyId: number; pic: number; picUrl: string; publishTime: number; description: string; tags: string; company?: string; briefDesc: string; artist: Artist4; songs?: any; alias: string[]; status: number; copyrightId: number; commentThreadId: string; artists: Artist5[]; paid: boolean; onSale: boolean; picId_str: string; alg: string; } interface Artist5 { name: string; id: number; picId: number; img1v1Id: number; briefDesc: string; picUrl: string; img1v1Url: string; albumSize: number; alias: any[]; trans: string; musicSize: number; topicPerson: number; img1v1Id_str: string; } interface Artist4 { name: string; id: number; picId: number; img1v1Id: number; briefDesc: string; picUrl: string; img1v1Url: string; albumSize: number; alias: string[]; trans: string; musicSize: number; topicPerson: number; picId_str: string; img1v1Id_str: string; alia: string[]; } interface Artist3 { moreText: string; artists: Artist2[]; more: boolean; resourceIds: number[]; } interface Artist2 { id: number; name: string; picUrl: string; alias: string[]; albumSize: number; picId: number; img1v1Url: string; img1v1: number; mvSize: number; followed: boolean; alg: string; alia?: string[]; trans?: any; accountId?: number; } interface PlayList2 { moreText: string; more: boolean; playLists: PlayList[]; resourceIds: number[]; } interface PlayList { id: number; name: string; coverImgUrl: string; creator: Creator; subscribed: boolean; trackCount: number; userId: number; playCount: number; bookCount: number; specialType: number; officialTags: string[]; description: string; highQuality: boolean; track: Track; alg: string; } interface Track { name: string; id: number; position: number; alias: any[]; status: number; fee: number; copyrightId: number; disc: string; no: number; artists: Artist[]; album: Album; starred: boolean; popularity: number; score: number; starredNum: number; duration: number; playedNum: number; dayPlays: number; hearTime: number; ringtone?: string; crbt?: any; audition?: any; copyFrom: string; commentThreadId: string; rtUrl?: any; ftype: number; rtUrls: any[]; copyright: number; mvid: number; rtype: number; rurl?: any; hMusic: HMusic; mMusic: HMusic; lMusic: HMusic; bMusic: HMusic; mp3Url?: any; transNames?: string[]; } interface HMusic { name?: any; id: number; size: number; extension: string; sr: number; dfsId: number; bitrate: number; playTime: number; volumeDelta: number; } interface Album { name: string; id: number; type: string; size: number; picId: number; blurPicUrl: string; companyId: number; pic: number; picUrl: string; publishTime: number; description: string; tags: string; company?: string; briefDesc: string; artist: Artist; songs: any[]; alias: any[]; status: number; copyrightId: number; commentThreadId: string; artists: Artist[]; picId_str?: string; } interface Artist { name: string; id: number; picId: number; img1v1Id: number; briefDesc: string; picUrl: string; img1v1Url: string; albumSize: number; alias: any[]; trans: string; musicSize: number; } interface Creator { nickname: string; userId: number; userType: number; avatarUrl: string; authStatus: number; expertTags?: any; experts?: any; } interface Mlog2 { moreText: string; more: boolean; mlogs: Mlog[]; resourceIds: any[]; } interface Mlog { id: string; type: number; mlogBaseDataType: number; position?: any; resource: Resource; alg: string; reason?: any; matchField: number; matchFieldContent: string; sameCity: boolean; } interface Resource { mlogBaseData: MlogBaseData; mlogExtVO: MlogExtVO; userProfile: UserProfile; status: number; shareUrl: string; } interface UserProfile { userId: number; nickname: string; avatarUrl: string; followed: boolean; userType: number; isAnchor: boolean; } interface MlogExtVO { likedCount: number; commentCount: number; playCount: number; song?: any; canCollect?: any; artistName?: any; rcmdInfo?: any; strongPushMark?: any; strongPushIcon?: any; specialTag?: any; channelTag: string; artists: any[]; } interface MlogBaseData { id: string; type: number; text: string; interveneText?: string; pubTime: number; coverUrl: string; coverHeight: number; coverWidth: number; coverColor: number; coverPicKey: string; coverDynamicUrl?: any; audio?: any; threadId: string; duration: number; } interface Song2 { moreText: string; songs: Song[]; more: boolean; ksongInfos: KsongInfos; resourceIds: number[]; } interface KsongInfos { '347230': _347230; } interface _347230 { androidDownloadUrl: string; accompanyId: string; deeplinkUrl: string; } interface Song { name: string; id: number; pst: number; t: number; ar: Ar[]; alia: any[]; pop: number; st: number; rt: string; fee: number; v: number; crbt?: any; cf: string; al: Al; dt: number; h: H; m: H; l: H; a?: any; cd: string; no: number; rtUrl?: any; ftype: number; rtUrls: any[]; djId: number; copyright: number; s_id: number; mark: number; originCoverType: number; originSongSimpleData?: any; resourceState: boolean; version: number; single: number; noCopyrightRcmd?: any; rtype: number; rurl?: any; mst: number; cp: number; mv: number; publishTime: number; showRecommend: boolean; recommendText: string; tns?: string[]; officialTags: any[]; privilege: Privilege; alg: string; specialTags: any[]; } interface Privilege { id: number; fee: number; payed: number; st: number; pl: number; dl: number; sp: number; cp: number; subp: number; cs: boolean; maxbr: number; fl: number; toast: boolean; flag: number; preSell: boolean; playMaxbr: number; downloadMaxbr: number; rscl?: any; freeTrialPrivilege: FreeTrialPrivilege; chargeInfoList: ChargeInfoList[]; } interface ChargeInfoList { rate: number; chargeUrl?: any; chargeMessage?: any; chargeType: number; } interface FreeTrialPrivilege { resConsumable: boolean; userConsumable: boolean; } interface H { br: number; fid: number; size: number; vd: number; } interface Al { id: number; name: string; picUrl: string; tns: any[]; pic_str?: string; pic: number; } interface Ar { id: number; name: string; tns: any[]; alias: string[]; alia?: string[]; } ================================================ FILE: src/renderer/types/singer.ts ================================================ export interface IHotSinger { code: number; more: boolean; artists: Artist[]; } export interface Artist { name: string; id: number; picId: number; img1v1Id: number; briefDesc: string; picUrl: string; img1v1Url: string; albumSize: number; alias: string[]; trans: string; musicSize: number; topicPerson: number; showPrivateMsg?: any; isSubed?: any; accountId?: number; picId_str?: string; img1v1Id_str: string; transNames?: string[]; followed: boolean; mvSize?: any; publishTime?: any; identifyTag?: any; alg?: any; fansCount?: any; cover?: string; avatar?: string; } ================================================ FILE: src/renderer/types/user.ts ================================================ export interface IUserDetail { level: number; listenSongs: number; userPoint: UserPoint; mobileSign: boolean; pcSign: boolean; profile: Profile; peopleCanSeeMyPlayRecord: boolean; bindings: Binding[]; adValid: boolean; code: number; createTime: number; createDays: number; profileVillageInfo: ProfileVillageInfo; } export interface IUserFollow { followed: boolean; follows: boolean; nickname: string; avatarUrl: string; userId: number; gender: number; signature: string; backgroundUrl: string; vipType: number; userType: number; accountType: number; } interface ProfileVillageInfo { title: string; imageUrl?: any; targetUrl: string; } interface Binding { userId: number; url: string; expiresIn: number; refreshTime: number; bindingTime: number; tokenJsonStr?: any; expired: boolean; id: number; type: number; } interface Profile { avatarDetail?: any; userId: number; avatarImgIdStr: string; backgroundImgIdStr: string; description: string; vipType: number; userType: number; createTime: number; nickname: string; avatarUrl: string; experts: any; expertTags?: any; djStatus: number; accountStatus: number; birthday: number; gender: number; province: number; city: number; defaultAvatar: boolean; avatarImgId: number; backgroundImgId: number; backgroundUrl: string; mutual: boolean; followed: boolean; remarkName?: any; authStatus: number; detailDescription: string; signature: string; authority: number; followeds: number; follows: number; blacklist: boolean; eventCount: number; allSubscribedCount: number; playlistBeSubscribedCount: number; avatarImgId_str: string; followTime?: any; followMe: boolean; artistIdentity: any[]; cCount: number; sDJPCount: number; playlistCount: number; sCount: number; newFollows: number; } interface UserPoint { userId: number; balance: number; updateTime: number; version: number; status: number; blockBalance: number; } ================================================ FILE: src/renderer/utils/appShortcuts.ts ================================================ import { onMounted, onUnmounted } from 'vue'; import i18n from '@/../i18n/renderer'; import { audioService } from '@/services/audioService'; import { usePlayerStore, useSettingsStore } from '@/store'; import { isElectron } from '.'; import { showShortcutToast } from './shortcutToast'; // 添加一个简单的防抖机制 let actionTimeout: NodeJS.Timeout | null = null; const ACTION_DELAY = 300; // 毫秒 // 添加一个操作锁,记录最后一次操作的时间和动作 let lastActionInfo = { action: '', timestamp: 0 }; interface ShortcutConfig { key: string; enabled: boolean; scope: 'global' | 'app'; } interface ShortcutsConfig { [key: string]: ShortcutConfig; } const { t } = i18n.global; // 全局存储快捷键配置 let appShortcuts: ShortcutsConfig = {}; /** * 处理快捷键动作 * @param action 快捷键动作 */ export async function handleShortcutAction(action: string) { const now = Date.now(); // 如果存在未完成的动作,则忽略当前请求 if (actionTimeout) { console.log('[AppShortcuts] 忽略快速连续的动作请求:', action); return; } // 检查是否是同一个动作的重复触发(300ms内) if (lastActionInfo.action === action && now - lastActionInfo.timestamp < ACTION_DELAY) { console.log( `[AppShortcuts] 忽略重复的 ${action} 动作,距上次仅 ${now - lastActionInfo.timestamp}ms` ); return; } // 更新最后一次操作信息 lastActionInfo = { action, timestamp: now }; // 设置防抖锁 actionTimeout = setTimeout(() => { actionTimeout = null; }, ACTION_DELAY); console.log(`[AppShortcuts] 执行动作: ${action}, 时间戳: ${now}`); const playerStore = usePlayerStore(); const settingsStore = useSettingsStore(); const showToast = (message: string, iconName: string) => { if (settingsStore.isMiniMode) { return; } showShortcutToast(message, iconName); }; try { switch (action) { case 'togglePlay': if (playerStore.play) { await audioService.pause(); showToast(t('player.playBar.pause'), 'ri-pause-circle-line'); } else { await audioService.getCurrentSound()?.play(); showToast(t('player.playBar.play'), 'ri-play-circle-line'); } break; case 'prevPlay': await playerStore.prevPlay(); showToast(t('player.playBar.prev'), 'ri-skip-back-line'); break; case 'nextPlay': await playerStore.nextPlay(); showToast(t('player.playBar.next'), 'ri-skip-forward-line'); break; case 'volumeUp': if (playerStore.getVolume() < 1) { const newVolume = playerStore.increaseVolume(0.1); showToast( `${t('player.playBar.volume')}${Math.round(newVolume * 100)}%`, 'ri-volume-up-line' ); } break; case 'volumeDown': if (playerStore.getVolume() > 0) { const newVolume = playerStore.decreaseVolume(0.1); showToast( `${t('player.playBar.volume')}${Math.round(newVolume * 100)}%`, 'ri-volume-down-line' ); } break; case 'toggleFavorite': { const isFavorite = playerStore.favoriteList.includes(Number(playerStore.playMusic.id)); const numericId = Number(playerStore.playMusic.id); console.log(`[AppShortcuts] toggleFavorite 当前状态: ${isFavorite}, ID: ${numericId}`); if (isFavorite) { playerStore.removeFromFavorite(numericId); console.log(`[AppShortcuts] 已从收藏中移除: ${numericId}`); } else { playerStore.addToFavorite(numericId); console.log(`[AppShortcuts] 已添加到收藏: ${numericId}`); } showToast( isFavorite ? t('player.playBar.unFavorite', { name: playerStore.playMusic.name }) : t('player.playBar.favorite', { name: playerStore.playMusic.name }), isFavorite ? 'ri-heart-line' : 'ri-heart-fill' ); break; } default: console.log('未知的快捷键动作:', action); break; } } catch (error) { console.error(`执行快捷键动作 ${action} 时出错:`, error); } finally { // 确保在出错时也能清除超时 clearTimeout(actionTimeout); actionTimeout = null; console.log( `[AppShortcuts] 动作完成: ${action}, 时间戳: ${Date.now()}, 耗时: ${Date.now() - now}ms` ); } } /** * 检查按键是否匹配快捷键 * @param e KeyboardEvent * @param shortcutKey 快捷键字符串 * @returns 是否匹配 */ function matchShortcut(e: KeyboardEvent, shortcutKey: string): boolean { const keys = shortcutKey.split('+'); const pressedKey = e.key.length === 1 ? e.key.toUpperCase() : e.key; // 检查修饰键 const hasCommandOrControl = keys.includes('CommandOrControl'); const hasAlt = keys.includes('Alt'); const hasShift = keys.includes('Shift'); // 检查主键 let mainKey = keys.find((k) => !['CommandOrControl', 'Alt', 'Shift'].includes(k)); if (!mainKey) return false; // 处理特殊键 if (mainKey === 'Left' && pressedKey === 'ArrowLeft') mainKey = 'ArrowLeft'; if (mainKey === 'Right' && pressedKey === 'ArrowRight') mainKey = 'ArrowRight'; if (mainKey === 'Up' && pressedKey === 'ArrowUp') mainKey = 'ArrowUp'; if (mainKey === 'Down' && pressedKey === 'ArrowDown') mainKey = 'ArrowDown'; // 检查是否所有条件都匹配 return ( hasCommandOrControl === (e.ctrlKey || e.metaKey) && hasAlt === e.altKey && hasShift === e.shiftKey && mainKey === pressedKey ); } /** * 全局键盘事件处理函数 * @param e KeyboardEvent */ function handleKeyDown(e: KeyboardEvent) { // 如果在输入框中则不处理快捷键 if (['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName)) { return; } Object.entries(appShortcuts).forEach(([action, config]) => { if (config.enabled && config.scope === 'app' && matchShortcut(e, config.key)) { e.preventDefault(); handleShortcutAction(action); } }); } /** * 更新应用内快捷键 * @param shortcuts 快捷键配置 */ export function updateAppShortcuts(shortcuts: ShortcutsConfig) { appShortcuts = shortcuts; } /** * 初始化应用内快捷键 */ export function initAppShortcuts() { if (isElectron) { // 监听全局快捷键事件 window.electron.ipcRenderer.on('global-shortcut', async (_, action: string) => { handleShortcutAction(action); }); // 监听应用内快捷键更新 window.electron.ipcRenderer.on('update-app-shortcuts', (_, shortcuts: ShortcutsConfig) => { updateAppShortcuts(shortcuts); }); // 获取初始快捷键配置 const storedShortcuts = window.electron.ipcRenderer.sendSync('get-store-value', 'shortcuts'); if (storedShortcuts) { updateAppShortcuts(storedShortcuts); } // 添加键盘事件监听 document.addEventListener('keydown', handleKeyDown); } } /** * 清理应用内快捷键 */ export function cleanupAppShortcuts() { if (isElectron) { // 移除全局事件监听 window.electron.ipcRenderer.removeAllListeners('global-shortcut'); window.electron.ipcRenderer.removeAllListeners('update-app-shortcuts'); // 移除键盘事件监听 document.removeEventListener('keydown', handleKeyDown); } } /** * 使用应用内快捷键的组合函数 */ export function useAppShortcuts() { onMounted(() => { initAppShortcuts(); }); onUnmounted(() => { cleanupAppShortcuts(); }); } ================================================ FILE: src/renderer/utils/auth.ts ================================================ /** * 登录状态管理工具 * * 注意:这个工具主要用于在组件外部或 store 初始化之前检查登录状态。 * 在组件内部,建议直接使用 userStore 中的状态。 */ export interface LoginInfo { isLoggedIn: boolean; loginType: 'token' | 'cookie' | 'qr' | 'uid' | null; hasToken: boolean; hasUser: boolean; user: any; } /** * 检查登录状态 * @returns 登录信息对象 */ export function checkLoginStatus(): LoginInfo { const token = localStorage.getItem('token'); const userData = localStorage.getItem('user'); const loginType = localStorage.getItem('loginType') as LoginInfo['loginType']; const uidLogin = localStorage.getItem('uidLogin'); const hasToken = !!token; const hasUser = !!userData; let user = null; if (hasUser) { try { user = JSON.parse(userData); } catch (error) { console.error('解析用户数据失败:', error); } } // 判断是否已登录 let isLoggedIn = false; if (loginType === 'uid' || uidLogin === 'true') { // UID登录:只需要有用户数据即可 isLoggedIn = hasUser && !!user; } else { // 其他登录方式:需要token和用户数据 isLoggedIn = hasToken && hasUser && !!user; } return { isLoggedIn, loginType, hasToken, hasUser, user }; } /** * 检查是否有访问权限 * @param requireAuth 是否需要真实登录权限(token) * @returns 是否有权限 */ export function hasPermission(requireAuth: boolean = false): boolean { const loginInfo = checkLoginStatus(); if (!loginInfo.isLoggedIn) { return false; } // 如果需要真实登录权限,UID登录无法满足 if (requireAuth && loginInfo.loginType === 'uid') { return false; } return true; } /** * 清除登录状态 */ export function clearLoginStatus(): void { localStorage.removeItem('token'); localStorage.removeItem('user'); localStorage.removeItem('loginType'); localStorage.removeItem('uidLogin'); } /** * 设置登录状态(不包括用户数据,用户数据应通过 userStore.setUser 设置) * @param loginType 登录类型 * @param token 登录token(可选) */ export function setLoginStatus(loginType: LoginInfo['loginType'], token?: string): void { localStorage.setItem('loginType', loginType || ''); if (token) { localStorage.setItem('token', token); } if (loginType === 'uid') { localStorage.setItem('uidLogin', 'true'); } } /** * 获取登录错误信息 * @param requireAuth 是否需要真实登录权限 * @returns 错误信息 */ export function getLoginErrorMessage(requireAuth: boolean = false): string { const loginInfo = checkLoginStatus(); if (!loginInfo.isLoggedIn) { return '请先登录'; } if (requireAuth && loginInfo.loginType === 'uid') { return 'UID登录无法访问此功能,请使用Cookie或二维码登录'; } return ''; } ================================================ FILE: src/renderer/utils/fileOperation.ts ================================================ import type { MessageApi } from 'naive-ui'; /** * 选择目录 * @param message MessageApi 实例 * @returns Promise 返回选择的目录路径,如果取消则返回 undefined */ export const selectDirectory = async (message: MessageApi): Promise => { try { const result = await window.electron.ipcRenderer.invoke('select-directory'); if (result.filePaths?.[0]) { return result.filePaths[0]; } } catch (error) { console.error('选择目录失败:', error); message.error('选择目录失败'); } return undefined; }; /** * 打开目录 * @param path 要打开的目录路径 * @param message MessageApi 实例 * @param showTip 是否显示提示信息 */ export const openDirectory = (path: string | undefined, message: MessageApi, showTip = true) => { if (path) { window.electron.ipcRenderer.send('open-directory', path); } else if (showTip) { message.info('目录不存在'); } }; ================================================ FILE: src/renderer/utils/index.ts ================================================ import { computed } from 'vue'; import { useSettingsStore } from '@/store/modules/settings'; // 设置歌手背景图片 export const setBackgroundImg = (url: String) => { return `background-image:url(${url})`; }; // 设置动画类型 export const setAnimationClass = (type: String) => { const settingsStore = useSettingsStore(); if (settingsStore.setData && settingsStore.setData.noAnimate) { return ''; } const speed = settingsStore.setData?.animationSpeed || 1; let speedClass = ''; if (speed <= 0.3) speedClass = 'animate__slower'; else if (speed <= 0.8) speedClass = 'animate__slow'; else if (speed >= 2.5) speedClass = 'animate__faster'; else if (speed >= 1.5) speedClass = 'animate__fast'; return `animate__animated ${type}${speedClass ? ` ${speedClass}` : ''}`; }; // 设置动画延时 export const setAnimationDelay = (index: number = 6, time: number = 50) => { const settingsStore = useSettingsStore(); if (settingsStore.setData?.noAnimate) { return ''; } const speed = settingsStore.setData?.animationSpeed || 1; return `animation-delay:${(index * time) / (speed * 2)}ms`; }; // 将秒转换为分钟和秒 export const secondToMinute = (s: number) => { if (!s) { return '00:00'; } const minute: number = Math.floor(s / 60); const second: number = Math.floor(s % 60); const minuteStr: string = minute > 9 ? minute.toString() : `0${minute.toString()}`; const secondStr: string = second > 9 ? second.toString() : `0${second.toString()}`; return `${minuteStr}:${secondStr}`; }; // 格式化数字 千,万, 百万, 千万,亿 const units = [ { value: 1e8, symbol: '亿' }, { value: 1e4, symbol: '万' } ]; export const formatNumber = (num: string | number) => { num = Number(num); for (let i = 0; i < units.length; i++) { if (num >= units[i].value) { return `${(num / units[i].value).toFixed(1)}${units[i].symbol}`; } } return num.toString(); }; export const getImgUrl = (url: string | undefined, size: string = '') => { if (!url) return ''; if (url.includes('thumbnail')) { // 只替换最后一个 thumbnail 参数的尺寸 return url.replace(/thumbnail=\d+y\d+(?!.*thumbnail)/, `thumbnail=${size}`); } const imgUrl = `${url}?param=${size}`; return imgUrl; }; export const isMobile = computed(() => { const settingsStore = useSettingsStore(); return settingsStore.isMobile; }); export const isElectron = (window as any).electron !== undefined; export const isLyricWindow = computed(() => { return window.location.hash.includes('lyric'); }); export const getSetData = (): any => { let setData = null; if (window.electron) { setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set'); } else { const settingsStore = useSettingsStore(); setData = settingsStore.setData; } return setData; }; ================================================ FILE: src/renderer/utils/linearColor.ts ================================================ import { useDebounceFn } from '@vueuse/core'; import tinycolor from 'tinycolor2'; interface IColor { backgroundColor: string; primaryColor: string; } interface ITextColors { primary: string; active: string; theme: string; } export interface LyricThemeColor { id: string; name: string; light: string; dark: string; } interface LyricSettings { isTop: boolean; theme: 'light' | 'dark'; isLock: boolean; highlightColor?: string; } export const getImageLinearBackground = async (imageSrc: string): Promise => { try { const primaryColor = await getImagePrimaryColor(imageSrc); return { backgroundColor: generateGradientBackground(primaryColor), primaryColor }; } catch (error) { console.error('error', error); return { backgroundColor: '', primaryColor: '' }; } }; export const getImageBackground = async (img: HTMLImageElement): Promise => { try { const primaryColor = await getImageColor(img); return { backgroundColor: generateGradientBackground(primaryColor), primaryColor }; } catch (error) { console.error('error', error); return { backgroundColor: '', primaryColor: '' }; } }; const getImageColor = (img: HTMLImageElement): Promise => { return new Promise((resolve, reject) => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('无法获取canvas上下文')); return; } canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const color = getAverageColor(imageData.data); resolve(`rgb(${color.join(',')})`); }); }; const getImagePrimaryColor = (imageSrc: string): Promise => { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = 'Anonymous'; img.src = imageSrc; img.onload = () => { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) { reject(new Error('无法获取canvas上下文')); return; } canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0, canvas.width, canvas.height); const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const color = getAverageColor(imageData.data); resolve(`rgb(${color.join(',')})`); }; img.onerror = () => reject(new Error('图片加载失败')); }); }; const getAverageColor = (data: Uint8ClampedArray): number[] => { let r = 0; let g = 0; let b = 0; let count = 0; for (let i = 0; i < data.length; i += 4) { r += data[i]; g += data[i + 1]; b += data[i + 2]; count++; } return [Math.round(r / count), Math.round(g / count), Math.round(b / count)]; }; const generateGradientBackground = (color: string): string => { const tc = tinycolor(color); const hsl = tc.toHsl(); // 增加亮度和暗度的差异 const lightColor = tinycolor({ h: hsl.h, s: hsl.s * 0.8, l: Math.min(hsl.l + 0.2, 0.95) }); const midColor = tinycolor({ h: hsl.h, s: hsl.s, l: hsl.l }); const darkColor = tinycolor({ h: hsl.h, s: Math.min(hsl.s * 1.2, 1), l: Math.max(hsl.l - 0.3, 0.05) }); return `linear-gradient(to bottom, ${lightColor.toRgbString()} 0%, ${midColor.toRgbString()} 50%, ${darkColor.toRgbString()} 100%)`; }; export const parseGradient = (gradientStr: string) => { if (!gradientStr) return []; // 处理非渐变色 if (!gradientStr.startsWith('linear-gradient')) { const color = tinycolor(gradientStr); if (color.isValid()) { const rgb = color.toRgb(); return [{ r: rgb.r, g: rgb.g, b: rgb.b }]; } return []; } // 处理渐变色,支持 rgb、rgba 和十六进制颜色 const colorMatches = gradientStr.match(/(?:(?:rgb|rgba)\([^)]+\)|#[0-9a-fA-F]{3,8})/g) || []; return colorMatches.map((color) => { const tc = tinycolor(color); const rgb = tc.toRgb(); return { r: rgb.r, g: rgb.g, b: rgb.b }; }); }; export const getTextColors = (gradient: string = ''): ITextColors => { const defaultColors = { primary: 'rgba(255, 255, 255, 0.54)', active: '#ffffff', theme: 'light' }; if (!gradient) return defaultColors; const colors = parseGradient(gradient); if (!colors.length) return defaultColors; const mainColor = colors.length === 1 ? colors[0] : colors[1] || colors[0]; const tc = tinycolor(mainColor); const isDark = tc.getBrightness() > 155; // tinycolor 的亮度范围是 0-255 return { primary: isDark ? 'rgba(0, 0, 0, 0.54)' : 'rgba(255, 255, 255, 0.54)', active: isDark ? '#000000' : '#ffffff', theme: isDark ? 'dark' : 'light' }; }; export const getHoverBackgroundColor = (isDark: boolean): string => { return isDark ? 'rgba(0, 0, 0, 0.08)' : 'rgba(255, 255, 255, 0.08)'; }; export const animateGradient = (() => { let currentAnimation: number | null = null; let isAnimating = false; let lastProgress = 0; const validateColors = (colors: ReturnType) => { return colors.every( (color) => typeof color.r === 'number' && typeof color.g === 'number' && typeof color.b === 'number' && !Number.isNaN(color.r) && !Number.isNaN(color.g) && !Number.isNaN(color.b) ); }; const easeInOutCubic = (x: number): number => { return x < 0.5 ? 4 * x * x * x : 1 - (-2 * x + 2) ** 3 / 2; }; const animate = ( oldGradient: string, newGradient: string, onUpdate: (gradient: string) => void, duration = 300 ) => { // 如果新旧渐变色相同,不执行动画 if (oldGradient === newGradient) { return null; } // 如果正在动画中,取消当前动画 if (currentAnimation !== null) { cancelAnimationFrame(currentAnimation); currentAnimation = null; } // 解析颜色 const startColors = parseGradient(oldGradient); const endColors = parseGradient(newGradient); // 验证颜色数组 if ( !startColors.length || !endColors.length || !validateColors(startColors) || !validateColors(endColors) ) { console.warn('Invalid color values detected'); onUpdate(newGradient); // 直接更新到目标颜色 return null; } // 如果颜色数量不匹配,直接更新到目标颜色 if (startColors.length !== endColors.length) { onUpdate(newGradient); return null; } isAnimating = true; const startTime = performance.now(); const animateFrame = (currentTime: number) => { if (!isAnimating) return null; const elapsed = currentTime - startTime; const rawProgress = Math.min(elapsed / duration, 1); // 使用缓动函数使动画更平滑 const progress = easeInOutCubic(rawProgress); try { // 使用上一帧的进度来平滑过渡 const effectiveProgress = lastProgress + (progress - lastProgress) * 0.6; lastProgress = effectiveProgress; const currentColors = startColors.map((startColor, i) => { const start = tinycolor(startColor); const end = tinycolor(endColors[i]); return tinycolor.mix(start, end, effectiveProgress * 100); }); const gradientString = createGradientString( currentColors.map((c) => { const rgb = c.toRgb(); return { r: rgb.r, g: rgb.g, b: rgb.b }; }) ); onUpdate(gradientString); if (rawProgress < 1) { currentAnimation = requestAnimationFrame(animateFrame); return currentAnimation; } // 确保最终颜色正确 onUpdate(newGradient); isAnimating = false; currentAnimation = null; lastProgress = 0; return null; } catch (error) { console.error('Animation error:', error); onUpdate(newGradient); isAnimating = false; currentAnimation = null; lastProgress = 0; return null; } }; currentAnimation = requestAnimationFrame(animateFrame); return currentAnimation; }; // 使用更短的防抖时间 return useDebounceFn(animate, 50); })(); export const createGradientString = ( colors: { r: number; g: number; b: number }[], percentages = [0, 50, 100] ) => { return `linear-gradient(to bottom, ${colors .map((color, i) => `rgb(${color.r}, ${color.g}, ${color.b}) ${percentages[i]}%`) .join(', ')})`; }; // ===== 歌词主题色相关工具函数 ===== /** * 预设歌词主题色配置 * 注意:name 字段将通过国际化系统动态获取,这里的值仅作为后备 */ const PRESET_LYRIC_COLORS: LyricThemeColor[] = [ { id: 'spotify-green', name: 'Spotify Green', // 后备名称,实际使用时会被国际化替换 light: '#1db954', dark: '#1ed760' }, { id: 'apple-blue', name: 'Apple Blue', light: '#007aff', dark: '#0a84ff' }, { id: 'youtube-red', name: 'YouTube Red', light: '#ff0000', dark: '#ff4444' }, { id: 'orange', name: 'Vibrant Orange', light: '#ff6b35', dark: '#ff8c42' }, { id: 'purple', name: 'Mystic Purple', light: '#8b5cf6', dark: '#a78bfa' }, { id: 'pink', name: 'Cherry Pink', light: '#ec4899', dark: '#f472b6' } ]; /** * 验证颜色是否有效 */ export const validateColor = (color: string): boolean => { if (!color || typeof color !== 'string') return false; const tc = tinycolor(color); return tc.isValid() && tc.getAlpha() > 0; }; /** * 检查颜色对比度是否符合可读性标准 */ export const validateColorContrast = (color: string, theme: 'light' | 'dark'): boolean => { if (!validateColor(color)) return false; const backgroundColor = theme === 'dark' ? '#000000' : '#ffffff'; const contrast = tinycolor.readability(color, backgroundColor); return contrast >= 4.5; // WCAG AA 标准 }; /** * 为特定主题优化颜色 */ export const optimizeColorForTheme = (color: string, theme: 'light' | 'dark'): string => { if (!validateColor(color)) { return getDefaultHighlightColor(theme); } const tc = tinycolor(color); const hsl = tc.toHsl(); if (theme === 'dark') { // 暗色主题:增加亮度和饱和度 const optimized = tinycolor({ h: hsl.h, s: Math.min(hsl.s * 1.1, 1), l: Math.max(hsl.l, 0.4) // 确保最小亮度 }); // 检查对比度,如果不够则进一步调整 if (!validateColorContrast(optimized.toHexString(), theme)) { return tinycolor({ h: hsl.h, s: Math.min(hsl.s * 1.2, 1), l: Math.max(hsl.l * 1.3, 0.5) }).toHexString(); } return optimized.toHexString(); } else { // 亮色主题:适当降低亮度 const optimized = tinycolor({ h: hsl.h, s: Math.min(hsl.s * 1.05, 1), l: Math.min(hsl.l, 0.6) // 确保最大亮度 }); // 检查对比度 if (!validateColorContrast(optimized.toHexString(), theme)) { return tinycolor({ h: hsl.h, s: Math.min(hsl.s * 1.1, 1), l: Math.min(hsl.l * 0.8, 0.5) }).toHexString(); } return optimized.toHexString(); } }; /** * 获取默认高亮颜色 */ export const getDefaultHighlightColor = (theme?: 'light' | 'dark'): string => { const defaultColor = PRESET_LYRIC_COLORS[0]; // Spotify 绿 if (!theme) return defaultColor.light; return theme === 'dark' ? defaultColor.dark : defaultColor.light; }; /** * 获取预设主题色列表 */ export const getLyricThemeColors = (): LyricThemeColor[] => { return [...PRESET_LYRIC_COLORS]; }; /** * 根据主题获取预设颜色的实际值 */ export const getPresetColorValue = (colorId: string, theme: 'light' | 'dark'): string => { const color = PRESET_LYRIC_COLORS.find((c) => c.id === colorId); if (!color) return getDefaultHighlightColor(theme); return theme === 'dark' ? color.dark : color.light; }; /** * 安全加载歌词设置 */ const safeLoadLyricSettings = (): LyricSettings => { try { const stored = localStorage.getItem('lyricData'); if (stored) { const parsed = JSON.parse(stored) as LyricSettings; // 验证 highlightColor 字段 if (parsed.highlightColor && !validateColor(parsed.highlightColor)) { console.warn('Invalid stored highlight color, removing it'); delete parsed.highlightColor; } return parsed; } } catch (error) { console.error('Failed to load lyric settings:', error); } // 返回默认设置 return { isTop: false, theme: 'dark', isLock: false }; }; /** * 安全保存歌词设置 */ const safeSaveLyricSettings = (settings: LyricSettings): void => { try { localStorage.setItem('lyricData', JSON.stringify(settings)); } catch (error) { console.error('Failed to save lyric settings:', error); } }; /** * 保存歌词主题色 */ export const saveLyricThemeColor = (color: string): void => { if (!validateColor(color)) { console.warn('Attempted to save invalid color:', color); return; } const settings = safeLoadLyricSettings(); settings.highlightColor = color; safeSaveLyricSettings(settings); }; /** * 加载歌词主题色 */ export const loadLyricThemeColor = (): string => { const settings = safeLoadLyricSettings(); if (settings.highlightColor && validateColor(settings.highlightColor)) { return settings.highlightColor; } // 如果没有保存的颜色或颜色无效,返回默认颜色 return getDefaultHighlightColor(settings.theme); }; /** * 重置歌词主题色到默认值 */ export const resetLyricThemeColor = (): void => { const settings = safeLoadLyricSettings(); delete settings.highlightColor; safeSaveLyricSettings(settings); }; /** * 获取当前有效的歌词主题色 */ export const getCurrentLyricThemeColor = (theme: 'light' | 'dark'): string => { const savedColor = loadLyricThemeColor(); if (savedColor && validateColor(savedColor)) { return optimizeColorForTheme(savedColor, theme); } return getDefaultHighlightColor(theme); }; ================================================ FILE: src/renderer/utils/lxCrypto.ts ================================================ /** * 落雪音乐加密工具 * 实现 lx.utils.crypto API * * 提供 MD5、AES、RSA 等加密功能 */ import CryptoJS from 'crypto-js'; import { JSEncrypt } from 'jsencrypt'; /** * MD5 哈希 */ export const md5 = (str: string): string => { return CryptoJS.MD5(str).toString(); }; /** * 生成随机字节(返回16进制字符串) */ export const randomBytes = (size: number): string => { const array = new Uint8Array(size); crypto.getRandomValues(array); return Array.from(array) .map((b) => b.toString(16).padStart(2, '0')) .join(''); }; /** * AES 加密 * * @param buffer - 要加密的数据(字符串或 Buffer) * @param mode - 加密模式(如 'cbc') * @param key - 密钥(字符串或 WordArray) * @param iv - 初始化向量(字符串或 WordArray) * @returns 加密后的 Buffer(Uint8Array) */ export const aesEncrypt = ( buffer: string | Uint8Array, mode: string, key: string | CryptoJS.lib.WordArray, iv: string | CryptoJS.lib.WordArray ): Uint8Array => { try { // 将输入转换为 WordArray let wordArray: CryptoJS.lib.WordArray; if (typeof buffer === 'string') { wordArray = CryptoJS.enc.Utf8.parse(buffer); } else { // Uint8Array 转 WordArray const words: number[] = []; for (let i = 0; i < buffer.length; i += 4) { words.push( ((buffer[i] || 0) << 24) | ((buffer[i + 1] || 0) << 16) | ((buffer[i + 2] || 0) << 8) | (buffer[i + 3] || 0) ); } wordArray = CryptoJS.lib.WordArray.create(words, buffer.length); } // 处理密钥和 IV const keyWordArray = typeof key === 'string' ? CryptoJS.enc.Utf8.parse(key) : key; const ivWordArray = typeof iv === 'string' ? CryptoJS.enc.Utf8.parse(iv) : iv; // 根据模式选择加密方式 const modeObj = getModeFromString(mode); // 执行加密 const encrypted = CryptoJS.AES.encrypt(wordArray, keyWordArray, { iv: ivWordArray, mode: modeObj, padding: CryptoJS.pad.Pkcs7 }); // 将结果转换为 Uint8Array const ciphertext = encrypted.ciphertext; const result = new Uint8Array(ciphertext.words.length * 4); for (let i = 0; i < ciphertext.words.length; i++) { const word = ciphertext.words[i]; result[i * 4] = (word >>> 24) & 0xff; result[i * 4 + 1] = (word >>> 16) & 0xff; result[i * 4 + 2] = (word >>> 8) & 0xff; result[i * 4 + 3] = word & 0xff; } return result.slice(0, ciphertext.sigBytes); } catch (error) { console.error('[lxCrypto] AES 加密失败:', error); throw error; } }; /** * AES 解密 */ export const aesDecrypt = ( buffer: Uint8Array, mode: string, key: string | CryptoJS.lib.WordArray, iv: string | CryptoJS.lib.WordArray ): Uint8Array => { try { // Uint8Array 转 WordArray const words: number[] = []; for (let i = 0; i < buffer.length; i += 4) { words.push( ((buffer[i] || 0) << 24) | ((buffer[i + 1] || 0) << 16) | ((buffer[i + 2] || 0) << 8) | (buffer[i + 3] || 0) ); } const ciphertext = CryptoJS.lib.WordArray.create(words, buffer.length); // 处理密钥和 IV const keyWordArray = typeof key === 'string' ? CryptoJS.enc.Utf8.parse(key) : key; const ivWordArray = typeof iv === 'string' ? CryptoJS.enc.Utf8.parse(iv) : iv; // 根据模式选择解密方式 const modeObj = getModeFromString(mode); // 构造加密对象 const cipherParams = CryptoJS.lib.CipherParams.create({ ciphertext }); // 执行解密 const decrypted = CryptoJS.AES.decrypt(cipherParams, keyWordArray, { iv: ivWordArray, mode: modeObj, padding: CryptoJS.pad.Pkcs7 }); // 转换为 Uint8Array const result = new Uint8Array(decrypted.words.length * 4); for (let i = 0; i < decrypted.words.length; i++) { const word = decrypted.words[i]; result[i * 4] = (word >>> 24) & 0xff; result[i * 4 + 1] = (word >>> 16) & 0xff; result[i * 4 + 2] = (word >>> 8) & 0xff; result[i * 4 + 3] = word & 0xff; } return result.slice(0, decrypted.sigBytes); } catch (error) { console.error('[lxCrypto] AES 解密失败:', error); throw error; } }; /** * RSA 加密 * * @param buffer - 要加密的数据 * @param publicKey - RSA 公钥(PEM 格式) * @returns 加密后的数据(Uint8Array) */ export const rsaEncrypt = (buffer: string | Uint8Array, publicKey: string): Uint8Array => { try { const encrypt = new JSEncrypt(); encrypt.setPublicKey(publicKey); // 转换输入为字符串 let input: string; if (typeof buffer === 'string') { input = buffer; } else { // Uint8Array 转字符串 input = new TextDecoder().decode(buffer); } // 执行加密(返回 base64) const encrypted = encrypt.encrypt(input); if (!encrypted) { throw new Error('RSA encryption failed'); } // Base64 解码为 Uint8Array const binaryString = atob(encrypted); const result = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { result[i] = binaryString.charCodeAt(i); } return result; } catch (error) { console.error('[lxCrypto] RSA 加密失败:', error); throw error; } }; /** * RSA 解密 */ export const rsaDecrypt = (buffer: Uint8Array, privateKey: string): Uint8Array => { try { const decrypt = new JSEncrypt(); decrypt.setPrivateKey(privateKey); // Uint8Array 转 Base64 let binaryString = ''; for (let i = 0; i < buffer.length; i++) { binaryString += String.fromCharCode(buffer[i]); } const base64 = btoa(binaryString); // 执行解密 const decrypted = decrypt.decrypt(base64); if (!decrypted) { throw new Error('RSA decryption failed'); } // 字符串转 Uint8Array return new TextEncoder().encode(decrypted); } catch (error) { console.error('[lxCrypto] RSA 解密失败:', error); throw error; } }; /** * 从字符串获取加密模式 */ const getModeFromString = (mode: string): CryptoJS.lib.Mode => { const modeStr = mode.toLowerCase(); switch (modeStr) { case 'cbc': return CryptoJS.mode.CBC; case 'cfb': return CryptoJS.mode.CFB; case 'ctr': return CryptoJS.mode.CTR; case 'ofb': return CryptoJS.mode.OFB; case 'ecb': return CryptoJS.mode.ECB; default: console.warn(`[lxCrypto] 未知的加密模式: ${mode}, 使用 CBC`); return CryptoJS.mode.CBC; } }; /** * SHA1 哈希 */ export const sha1 = (str: string): string => { return CryptoJS.SHA1(str).toString(); }; /** * SHA256 哈希 */ export const sha256 = (str: string): string => { return CryptoJS.SHA256(str).toString(); }; /** * Base64 编码 */ export const base64Encode = (data: string | Uint8Array): string => { if (typeof data === 'string') { return btoa(data); } else { let binary = ''; for (let i = 0; i < data.length; i++) { binary += String.fromCharCode(data[i]); } return btoa(binary); } }; /** * Base64 解码 */ export const base64Decode = (str: string): Uint8Array => { const binaryString = atob(str); const result = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { result[i] = binaryString.charCodeAt(i); } return result; }; ================================================ FILE: src/renderer/utils/playerUtils.ts ================================================ import type { SongResult } from '@/types/music'; /** * 从 localStorage 获取项目,带类型安全和错误处理 */ export function getLocalStorageItem(key: string, defaultValue: T): T { try { const item = localStorage.getItem(key); return item ? JSON.parse(item) : defaultValue; } catch { return defaultValue; } } /** * 设置 localStorage 项目,自动序列化 */ export function setLocalStorageItem(key: string, value: T): void { try { localStorage.setItem(key, JSON.stringify(value)); } catch (error) { console.error(`Failed to save to localStorage: ${key}`, error); } } /** * 比较B站视频ID的辅助函数 */ export const isBilibiliIdMatch = (id1: string | number, id2: string | number): boolean => { const str1 = String(id1); const str2 = String(id2); // 如果两个ID都不包含--分隔符,直接比较 if (!str1.includes('--') && !str2.includes('--')) { return str1 === str2; } // 处理B站视频ID if (str1.includes('--') || str2.includes('--')) { // 尝试从ID中提取bvid和cid const extractBvIdAndCid = (str: string) => { if (!str.includes('--')) return { bvid: '', cid: '' }; const parts = str.split('--'); if (parts.length >= 3) { // bvid--pid--cid格式 return { bvid: parts[0], cid: parts[2] }; } else if (parts.length === 2) { // 旧格式或其他格式 return { bvid: '', cid: parts[1] }; } return { bvid: '', cid: '' }; }; const { bvid: bvid1, cid: cid1 } = extractBvIdAndCid(str1); const { bvid: bvid2, cid: cid2 } = extractBvIdAndCid(str2); // 如果两个ID都有bvid,比较bvid和cid if (bvid1 && bvid2) { return bvid1 === bvid2 && cid1 === cid2; } // 其他情况,只比较cid部分 if (cid1 && cid2) { return cid1 === cid2; } } // 默认情况,直接比较完整ID return str1 === str2; }; /** * Fisher-Yates 洗牌算法 * @param list 歌曲列表 * @param currentSong 当前歌曲(会被放在第一位) */ export const performShuffle = (list: SongResult[], currentSong?: SongResult): SongResult[] => { if (list.length <= 1) return [...list]; const result: SongResult[] = []; const remainingSongs = [...list]; // 如果指定了当前歌曲,先把它放在第一位 if (currentSong && currentSong.id) { const currentSongIndex = remainingSongs.findIndex((song) => song.id === currentSong.id); if (currentSongIndex !== -1) { // 把当前歌曲放在第一位 result.push(remainingSongs.splice(currentSongIndex, 1)[0]); } } // 对剩余歌曲进行洗牌 if (remainingSongs.length > 0) { // Fisher-Yates 洗牌算法 for (let i = remainingSongs.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [remainingSongs[i], remainingSongs[j]] = [remainingSongs[j], remainingSongs[i]]; } // 把洗牌后的歌曲添加到结果中 result.push(...remainingSongs); } return result; }; /** * 预加载封面图片 */ export const preloadCoverImage = ( picUrl: string, getImgUrl: (url: string, size: string) => string ) => { if (!picUrl) return; try { const imageUrl = getImgUrl(picUrl, '500y500'); console.log('预加载封面图片:', imageUrl); // 创建一个 Image 对象来预加载图片 const img = new Image(); img.src = imageUrl; // 可选:添加加载完成和错误的回调 img.onload = () => { console.log('封面图片预加载成功:', imageUrl); }; img.onerror = () => { console.error('封面图片预加载失败:', imageUrl); }; } catch (error) { console.error('预加载封面图片出错:', error); } }; ================================================ FILE: src/renderer/utils/request.ts ================================================ import axios, { InternalAxiosRequestConfig } from 'axios'; import { useUserStore } from '@/store/modules/user'; import { getSetData, isElectron, isMobile } from '.'; let setData: any = null; // 扩展请求配置接口 interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig { retryCount?: number; noRetry?: boolean; } const baseURL = window.electron ? `http://127.0.0.1:${setData?.musicApiPort}` : import.meta.env.VITE_API; const request = axios.create({ baseURL, timeout: 15000, withCredentials: true }); // 最大重试次数 const MAX_RETRIES = 1; // 重试延迟(毫秒) const RETRY_DELAY = 500; // 请求拦截器 request.interceptors.request.use( (config: CustomAxiosRequestConfig) => { setData = getSetData(); config.baseURL = window.electron ? `http://127.0.0.1:${setData?.musicApiPort}` : import.meta.env.VITE_API; // 只在retryCount未定义时初始化为0 if (config.retryCount === undefined) { config.retryCount = 0; } // 在请求发送之前做一些处理 // 在get请求params中添加timestamp config.params = { ...config.params, timestamp: Date.now(), device: isElectron ? 'pc' : isMobile ? 'mobile' : 'web' }; const token = localStorage.getItem('token'); if (token && config.method !== 'post') { config.params.cookie = config.params.cookie !== undefined ? config.params.cookie : token; } else if (token && config.method === 'post') { config.data = { ...config.data, cookie: token }; } if (isElectron) { const proxyConfig = setData?.proxyConfig; if (proxyConfig?.enable && ['http', 'https'].includes(proxyConfig?.protocol)) { config.params.proxy = `${proxyConfig.protocol}://${proxyConfig.host}:${proxyConfig.port}`; } if (setData.enableRealIP && setData.realIP) { config.params.realIP = setData.realIP; } } return config; }, (error) => { // 当请求异常时做一些处理 return Promise.reject(error); } ); const NO_RETRY_URLS = ['暂时没有']; // 响应拦截器 request.interceptors.response.use( (response) => { return response; }, async (error) => { console.error('error', error); const config = error.config as CustomAxiosRequestConfig; // 如果没有配置,直接返回错误 if (!config) { return Promise.reject(error); } // 处理 301 状态码 if (error.response?.status === 301 && config.params.noLogin !== true) { // 使用 store mutation 清除用户信息 const userStore = useUserStore(); userStore.handleLogout(); console.log(`301 状态码,清除登录信息后重试第 ${config.retryCount} 次`); config.retryCount = 3; } // 检查是否还可以重试 if ( config.retryCount !== undefined && config.retryCount < MAX_RETRIES && !NO_RETRY_URLS.includes(config.url as string) && !config.noRetry ) { config.retryCount++; console.error(`请求重试第 ${config.retryCount} 次`); // 延迟重试 await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); // 重新发起请求 return request(config); } console.error(`重试${MAX_RETRIES}次后仍然失败`); return Promise.reject(error); } ); export default request; ================================================ FILE: src/renderer/utils/request_music.ts ================================================ import axios from 'axios'; const baseURL = `${import.meta.env.VITE_API_MUSIC}`; const request = axios.create({ baseURL, timeout: 10000 }); // 请求拦截器 request.interceptors.request.use( (config) => { return config; }, (error) => { // 当请求异常时做一些处理 return Promise.reject(error); } ); export default request; ================================================ FILE: src/renderer/utils/shortcutToast.ts ================================================ import { createVNode, render } from 'vue'; import ShortcutToast from '@/components/ShortcutToast.vue'; let container: HTMLDivElement | null = null; let toastInstance: any = null; interface ToastOptions { position?: 'top' | 'center' | 'bottom'; showIcon?: boolean; } export function showShortcutToast(message: string, iconName = '', options: ToastOptions = {}) { // 如果容器不存在,创建一个新的容器 if (!container) { container = document.createElement('div'); document.body.appendChild(container); } // 如果已经有实例,先销毁它 if (toastInstance) { render(null, container); toastInstance = null; } // 创建新的 toast 实例 const vnode = createVNode(ShortcutToast, { position: options.position || 'center', showIcon: options.showIcon !== undefined ? options.showIcon : true, onDestroy: () => { if (container) { render(null, container); document.body.removeChild(container); container = null; } } }); // 渲染 toast render(vnode, container); toastInstance = vnode.component?.exposed; // 显示 toast if (toastInstance) { toastInstance.show(message, iconName, { showIcon: options.showIcon }); } } // 新增便捷方法 - 底部无图标 toast export function showBottomToast(message: string) { showShortcutToast(message, '', { position: 'bottom', showIcon: false }); } ================================================ FILE: src/renderer/utils/theme.ts ================================================ export type ThemeType = 'dark' | 'light'; // 检测系统主题 export const getSystemTheme = (): ThemeType => { if (typeof window !== 'undefined' && window.matchMedia) { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } return 'light'; }; // 应用主题 export const applyTheme = (theme: ThemeType) => { // 使用 Tailwind 的暗色主题类 if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } // 保存主题到本地存储 localStorage.setItem('theme', theme); }; // 获取当前主题 export const getCurrentTheme = (): ThemeType => { return (localStorage.getItem('theme') as ThemeType) || 'light'; }; // 监听系统主题变化 export const watchSystemTheme = (callback: (theme: ThemeType) => void) => { if (typeof window !== 'undefined' && window.matchMedia) { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handler = (e: MediaQueryListEvent) => { callback(e.matches ? 'dark' : 'light'); }; mediaQuery.addEventListener('change', handler); // 返回清理函数 return () => { mediaQuery.removeEventListener('change', handler); }; } return () => {}; }; ================================================ FILE: src/renderer/utils/update.ts ================================================ import { useDateFormat } from '@vueuse/core'; import axios from 'axios'; import config from '../../../package.json'; interface GithubReleaseInfo { tag_name: string; body: string; published_at: string; html_url: string; assets: Array<{ browser_download_url: string; name: string; size: number; }>; } interface ProxyNode { url: string; server: string; ip: string; location: string; latency: number; speed: number; } interface ProxyResponse { code: number; msg: string; data: ProxyNode[]; total: number; update_time: string; } export interface UpdateResult { hasUpdate: boolean; latestVersion: string; currentVersion: string; releaseInfo: { tag_name: string; body: string; html_url: string; assets: Array<{ browser_download_url: string; name: string; }>; } | null; } // 缓存相关配置 const CACHE_KEY = 'github_proxy_nodes'; const CACHE_EXPIRE_TIME = 1000 * 60 * 10; // 10分钟过期 // 请求配置 const REQUEST_TIMEOUT = 2000; // 2秒超时 /** * 从缓存获取代理节点 */ const getCachedProxyNodes = (): { nodes: string[]; timestamp: number } | null => { const cached = localStorage.getItem(CACHE_KEY); if (cached) { const { nodes, timestamp } = JSON.parse(cached); if (Date.now() - timestamp < CACHE_EXPIRE_TIME) { return { nodes, timestamp }; } } return null; }; /** * 缓存代理节点 */ const cacheProxyNodes = (nodes: string[]) => { localStorage.setItem( CACHE_KEY, JSON.stringify({ nodes, timestamp: Date.now() }) ); }; /** * 获取代理节点列表 */ export const getProxyNodes = async (): Promise => { // 尝试从缓存获取 const cached = getCachedProxyNodes(); if (cached) { return cached.nodes; } try { // 获取最新代理节点 const { data } = await axios.get('https://api.akams.cn/github', { timeout: REQUEST_TIMEOUT }); if (data.code === 200) { // 按速度排序并获取前10个节点 const nodes = data.data .sort((a, b) => b.speed - a.speed) .slice(0, 10) .map((node) => node.url); // 缓存节点 cacheProxyNodes(nodes); return nodes; } } catch (error) { console.error('获取代理节点失败:', error); } // 使用备用节点 return [ 'https://gh.lk.cc', 'https://ghproxy.cn', 'https://ghproxy.net', 'https://gitproxy.click', 'https://github.tbedu.top', 'https://github.moeyy.xyz' ]; }; /** * 获取 GitHub 最新发布版本信息 */ export const getLatestReleaseInfo = async (): Promise => { try { const token = import.meta.env.VITE_GITHUB_TOKEN; const headers = {}; // 构建 API URL 列表 const apiUrls = [ // 原始地址 'https://api.github.com/repos/algerkong/AlgerMusicPlayer/releases/latest', // 使用代理节点 'http://music.alger.fun/package.json' ]; if (token) { headers['Authorization'] = `token ${token}`; } for (const url of apiUrls) { try { const response = await axios.get(url, { headers, timeout: REQUEST_TIMEOUT }); if (url.includes('package.json')) { // 如果是 package.json,获取对应的 CHANGELOG const changelogUrl = url.replace('package.json', 'CHANGELOG.md'); const changelogResponse = await axios.get(changelogUrl, { timeout: REQUEST_TIMEOUT }); return { tag_name: response.data.version, body: changelogResponse.data, html_url: 'https://github.com/algerkong/AlgerMusicPlayer/releases/latest', assets: [] } as unknown as GithubReleaseInfo; } return response.data; } catch (err) { console.warn(`尝试访问 ${url} 失败:`, err); continue; } } throw new Error('所有 API 地址均无法访问'); } catch (error) { console.error('获取 GitHub Release 信息失败:', error); return null; } }; /** * 格式化时间 */ export const formatDate = (dateStr: string): string => { return useDateFormat(new Date(dateStr), 'YYYY-MM-DD HH:mm').value; }; /** * 比较两个版本号 * @param v1 版本号1 * @param v2 版本号2 * @returns 如果v1大于v2返回1,如果v1小于v2返回-1,如果相等返回0 */ export const compareVersions = (v1: string, v2: string): number => { const v1Parts = v1.split('.').map(Number); const v2Parts = v2.split('.').map(Number); for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) { const v1Part = v1Parts[i] || 0; const v2Part = v2Parts[i] || 0; if (v1Part > v2Part) return 1; if (v1Part < v2Part) return -1; } return 0; }; /** * 检查更新 */ export const checkUpdate = async ( currentVersion: string = config.version ): Promise => { try { const releaseInfo = await getLatestReleaseInfo(); console.log('releaseInfo', releaseInfo); if (!releaseInfo) { return null; } const latestVersion = releaseInfo.tag_name.replace('v', ''); // 比较版本号,只有当新版本大于当前版本时才返回更新信息 if (compareVersions(latestVersion, currentVersion) <= 0) { return null; } console.log('latestVersion', latestVersion); console.log('currentVersion', currentVersion); return { hasUpdate: true, latestVersion, currentVersion, releaseInfo: { tag_name: latestVersion, body: `## 更新内容\n\n- 版本: ${latestVersion}\n${releaseInfo.body}`, html_url: releaseInfo.html_url, assets: releaseInfo.assets.map((asset) => ({ browser_download_url: asset.browser_download_url, name: asset.name })) } }; } catch (error) { console.error('检查更新失败:', error); return null; } }; ================================================ FILE: src/renderer/utils/yrcParser.ts ================================================ /** * 歌词单词数据接口 */ export interface WordData { /** 单词文本内容 */ readonly text: string; /** 开始时间(毫秒) */ readonly startTime: number; /** 持续时间(毫秒) */ readonly duration: number; /** 该单词后是否有空格 */ readonly space?: boolean; } /** * 歌词行数据接口 */ export interface LyricLine { /** 行开始时间(毫秒) */ readonly startTime: number; /** 行持续时间(毫秒) */ readonly duration: number; /** 完整文本内容 */ readonly fullText: string; /** 单词数组 */ readonly words: readonly WordData[]; } /** * 元数据接口 */ export interface MetaData { /** 时间戳(可选,不带时间的元数据为 undefined) */ readonly time?: number; /** 内容 */ readonly content: string; } /** * 解析结果接口 */ export interface ParsedLyrics { /** 元数据数组 */ readonly metadata: readonly MetaData[]; /** 歌词行数组 */ readonly lyrics: readonly LyricLine[]; } /** * 自定义解析错误类 */ export class LyricParseError extends Error { constructor( message: string, public readonly line?: string ) { super(message); this.name = 'LyricParseError'; } } /** * 解析结果类型 */ export type ParseResult = | { success: true; data: T } | { success: false; error: LyricParseError }; // 预编译正则表达式以提高性能 const METADATA_PATTERN = /^\{("t":|"c":)/; // 匹配 {"t": 或 {"c": const LINE_TIME_PATTERN = /^\[(\d+),(\d+)\](.+)$/; // 逐字歌词格式: [92260,4740]... const LRC_TIME_PATTERN = /^\[(\d{2}):(\d{2})\.(\d{2,3})\](.*)$/; // 标准LRC格式: [00:25.47]... const WORD_PATTERN = /\((\d+),(\d+),\d+\)([^(]*?)(?=\(|$)/g; /** * 时间格式化函数 * @param ms 毫秒数 * @returns 格式化的时间字符串 */ export const formatTime = (ms: number): string => { const minutes = Math.floor(ms / 60000); const seconds = Math.floor((ms % 60000) / 1000); const milliseconds = ms % 1000; return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`; }; /** * 解析元数据行 * @param line 元数据行字符串 * @returns 解析结果 */ const parseMetadata = (line: string): ParseResult => { try { const data = JSON.parse(line); // 类型守卫:检查数据结构 if (typeof data !== 'object' || data === null) { return { success: false, error: new LyricParseError('元数据格式无效:不是有效的对象', line) }; } // 检查必须有 c 字段(内容数组) if (!Array.isArray(data.c)) { return { success: false, error: new LyricParseError('元数据格式无效:缺少 c 字段', line) }; } // t 字段(时间戳)是可选的 if (data.t !== undefined && typeof data.t !== 'number') { return { success: false, error: new LyricParseError('元数据格式无效:t 字段必须是数字', line) }; } const content = data.c .filter((item: any) => item && typeof item.tx === 'string') .map((item: any) => item.tx) .join(''); return { success: true, data: { time: data.t, content } }; } catch (error) { return { success: false, error: new LyricParseError( `JSON解析失败: ${error instanceof Error ? error.message : '未知错误'}`, line ) }; } }; /** * 解析标准LRC格式的歌词行 * @param line 歌词行字符串 * @returns 解析结果 */ const parseLrcLine = (line: string): ParseResult => { const lrcMatch = line.match(LRC_TIME_PATTERN); if (!lrcMatch) { return { success: false, error: new LyricParseError('LRC歌词行格式无效:无法匹配时间信息', line) }; } const minutes = parseInt(lrcMatch[1], 10); const seconds = parseInt(lrcMatch[2], 10); const milliseconds = parseInt(lrcMatch[3].padEnd(3, '0'), 10); // 处理2位或3位毫秒 const text = lrcMatch[4].trim(); // 验证时间值 if ( isNaN(minutes) || isNaN(seconds) || isNaN(milliseconds) || minutes < 0 || seconds < 0 || milliseconds < 0 || seconds >= 60 ) { return { success: false, error: new LyricParseError('LRC歌词行格式无效:时间值无效', line) }; } const startTime = minutes * 60000 + seconds * 1000 + milliseconds; return { success: true, data: { startTime, duration: 0, // LRC格式没有持续时间信息 fullText: text, words: [] // LRC格式没有逐字信息 } }; }; /** * 解析逐字歌词行 * @param line 歌词行字符串 * @returns 解析结果 */ const parseWordByWordLine = (line: string): ParseResult => { // 使用预编译的正则表达式 const lineTimeMatch = line.match(LINE_TIME_PATTERN); if (!lineTimeMatch) { return { success: false, error: new LyricParseError('逐字歌词行格式无效:无法匹配时间信息', line) }; } const startTime = parseInt(lineTimeMatch[1], 10); const duration = parseInt(lineTimeMatch[2], 10); const content = lineTimeMatch[3]; // 验证时间值 if (isNaN(startTime) || isNaN(duration) || startTime < 0 || duration < 0) { return { success: false, error: new LyricParseError('逐字歌词行格式无效:时间值无效', line) }; } // 重置正则表达式状态 WORD_PATTERN.lastIndex = 0; const words: WordData[] = []; let match: RegExpExecArray | null; // 第一遍:提取所有单词的原始文本(包含空格),构建完整文本 const rawTextParts: string[] = []; const tempWords: Array<{ startTime: number; duration: number; text: string }> = []; while ((match = WORD_PATTERN.exec(content)) !== null) { const wordStartTime = parseInt(match[1], 10); const wordDuration = parseInt(match[2], 10); const rawWordText = match[3]; // 保留原始文本(可能包含空格) const wordText = rawWordText.trim(); // 去除首尾空格的文本 // 验证单词数据 if (isNaN(wordStartTime) || isNaN(wordDuration)) { continue; // 跳过无效的单词数据 } if (wordText) { tempWords.push({ text: wordText, startTime: wordStartTime, duration: wordDuration }); rawTextParts.push(rawWordText); // 保留原始格式用于分析空格 } } // 构建完整的文本(保留原始空格) const fullText = rawTextParts.join('').trim(); // 第二遍:检查每个单词在完整文本中是否后面有空格 let currentPos = 0; for (const word of tempWords) { // 在完整文本中查找当前单词的位置 const wordIndex = fullText.indexOf(word.text, currentPos); if (wordIndex === -1) { // 如果找不到,直接添加不带空格标记的单词 words.push(word); continue; } // 计算单词结束位置 const wordEndPos = wordIndex + word.text.length; // 检查单词后面是否有空格 const hasSpace = wordEndPos < fullText.length && fullText[wordEndPos] === ' '; words.push({ ...word, space: hasSpace }); // 更新搜索位置 currentPos = wordEndPos; } return { success: true, data: { startTime, duration, fullText, words } }; }; /** * 解析歌词行(自动检测格式) * @param line 歌词行字符串 * @returns 解析结果 */ const parseLyricLine = (line: string): ParseResult => { // 首先尝试解析逐字歌词格式 if (LINE_TIME_PATTERN.test(line)) { return parseWordByWordLine(line); } // 然后尝试解析标准LRC格式 if (LRC_TIME_PATTERN.test(line)) { return parseLrcLine(line); } return { success: false, error: new LyricParseError('歌词行格式无效:不匹配任何已知格式', line) }; }; /** * 计算LRC格式歌词的持续时间 * @param lyrics 歌词行数组 * @returns 更新持续时间后的歌词行数组 */ const calculateLrcDurations = (lyrics: LyricLine[]): LyricLine[] => { if (lyrics.length === 0) return lyrics; const updatedLyrics: LyricLine[] = []; for (let i = 0; i < lyrics.length; i++) { const currentLine = lyrics[i]; // 如果已经有持续时间(逐字歌词),直接使用 if (currentLine.duration > 0) { updatedLyrics.push(currentLine); continue; } // 计算LRC格式的持续时间 let duration = 0; if (i < lyrics.length - 1) { // 使用下一行的开始时间减去当前行的开始时间 duration = lyrics[i + 1].startTime - currentLine.startTime; } else { // 最后一行,使用默认持续时间(3秒) duration = 3000; } // 确保持续时间不为负数 duration = Math.max(duration, 0); updatedLyrics.push({ ...currentLine, duration }); } return updatedLyrics; }; /** * 解析不带时间戳的纯文本歌词行 * @param line 纯文本歌词行 * @returns 解析结果 */ const parsePlainTextLine = (line: string): ParseResult => { // 清理行首尾的 \r 等特殊字符 const text = line.replace(/\r/g, '').trim(); if (!text) { return { success: false, error: new LyricParseError('纯文本歌词行为空', line) }; } return { success: true, data: { startTime: -1, // -1 表示没有时间信息 duration: 0, fullText: text, words: [] } }; }; /** * 主解析函数 * @param lyricsStr 歌词字符串 * @returns 解析结果 */ export const parseLyrics = (lyricsStr: string): ParseResult => { if (typeof lyricsStr !== 'string') { return { success: false, error: new LyricParseError('输入参数必须是字符串') }; } try { const lines = lyricsStr.trim().split('\n'); const metadata: MetaData[] = []; const lyrics: LyricLine[] = []; const errors: LyricParseError[] = []; for (let i = 0; i < lines.length; i++) { const trimmedLine = lines[i].trim(); if (!trimmedLine) continue; // 使用预编译正则表达式进行快速检测 if (METADATA_PATTERN.test(trimmedLine)) { const result = parseMetadata(trimmedLine); if (result.success) { metadata.push(result.data); } else { errors.push(result.error); } } else if (trimmedLine.startsWith('[')) { const result = parseLyricLine(trimmedLine); if (result.success) { lyrics.push(result.data); } else { errors.push(result.error); } } else { // 尝试解析为纯文本歌词行(不带时间戳) const result = parsePlainTextLine(trimmedLine); if (result.success) { lyrics.push(result.data); } else { errors.push(result.error); } } } // 如果有太多错误,可能整个文件格式有问题 if (errors.length > 0 && errors.length > lines.length * 0.5) { return { success: false, error: new LyricParseError( `解析失败:错误行数过多 (${errors.length}/${lines.length}),可能文件格式不正确 ${JSON.stringify(errors)}` ) }; } // 按时间排序歌词行(将没有时间信息的行放在最前面) lyrics.sort((a, b) => { if (a.startTime === -1 && b.startTime === -1) return 0; if (a.startTime === -1) return -1; if (b.startTime === -1) return 1; return a.startTime - b.startTime; }); // 计算LRC格式的持续时间 const finalLyrics = calculateLrcDurations(lyrics); return { success: true, data: { metadata, lyrics: finalLyrics } }; } catch (error) { return { success: false, error: new LyricParseError( `解析过程中发生错误: ${error instanceof Error ? error.message : '未知错误'}` ) }; } }; /** * 导出默认解析函数(向后兼容) */ export default parseLyrics; ================================================ FILE: src/renderer/views/artist/detail.vue ================================================ ================================================ FILE: src/renderer/views/bilibili/BilibiliPlayer.vue ================================================ ================================================ FILE: src/renderer/views/download/DownloadPage.vue ================================================ ================================================ FILE: src/renderer/views/favorite/index.vue ================================================ ================================================ FILE: src/renderer/views/heatmap/index.vue ================================================ ================================================ FILE: src/renderer/views/history/index.vue ================================================ ================================================ FILE: src/renderer/views/historyAndFavorite/index.vue ================================================ ================================================ FILE: src/renderer/views/home/index.vue ================================================ ================================================ FILE: src/renderer/views/list/index.vue ================================================ ================================================ FILE: src/renderer/views/login/index.vue ================================================ ================================================ FILE: src/renderer/views/lyric/index.vue ================================================ ================================================ FILE: src/renderer/views/mobile-search/index.vue ================================================ ================================================ FILE: src/renderer/views/mobile-search-result/index.vue ================================================ ================================================ FILE: src/renderer/views/music/HistoryRecommend.vue ================================================ ================================================ FILE: src/renderer/views/music/MusicListPage.vue ================================================ ================================================ FILE: src/renderer/views/mv/index.vue ================================================ ================================================ FILE: src/renderer/views/playlist/ImportPlaylist.vue ================================================ ================================================ FILE: src/renderer/views/search/index.vue ================================================ ================================================ FILE: src/renderer/views/set/SettingItem.vue ================================================ ================================================ FILE: src/renderer/views/set/SettingNav.vue ================================================ ================================================ FILE: src/renderer/views/set/SettingSection.vue ================================================ ================================================ FILE: src/renderer/views/set/index.vue ================================================ ================================================ FILE: src/renderer/views/toplist/index.vue ================================================ ================================================ FILE: src/renderer/views/user/detail.vue ================================================ ================================================ FILE: src/renderer/views/user/followers.vue ================================================ ================================================ FILE: src/renderer/views/user/follows.vue ================================================ ================================================ FILE: src/renderer/views/user/index.vue ================================================ ================================================ FILE: src/renderer/vite-env.d.ts ================================================ /// ================================================ FILE: tailwind.config.js ================================================ /** @type {import('tailwindcss').Config} */ export default { content: ['./src/renderer/index.html', './src/renderer/**/*.{vue,js,ts,jsx,tsx}'], darkMode: 'class', theme: { extend: { colors: { primary: { DEFAULT: '#000', light: '#fff', dark: '#000' }, secondary: { DEFAULT: '#6c757d', light: '#8c959e', dark: '#495057' }, dark: { DEFAULT: '#000', 100: '#161616', 200: '#2d2d2d', 300: '#3d3d3d' }, light: { DEFAULT: '#fff', 100: '#f8f9fa', 200: '#e9ecef', 300: '#dee2e6' } } } }, plugins: [] }; ================================================ FILE: tsconfig.json ================================================ { "files": [], "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] } ================================================ FILE: tsconfig.node.json ================================================ { "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", "include": [ "electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/i18n/**/*" ], "compilerOptions": { "composite": true, "types": [ "electron-vite/node" ], "moduleResolution": "bundler", }, "paths": { "@/*": [ "src/renderer/*" ], "@renderer/*": [ "src/renderer/*" ], "@main/*": [ "src/main/*" ], "@i18n/*": [ "src/i18n/*" ] } } ================================================ FILE: tsconfig.web.json ================================================ { "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", "include": [ "src/preload/*.d.ts", "src/renderer/**/*", "src/renderer/**/*.vue", "src/i18n/**/*", "src/main/modules/config.ts", "src/main/modules/shortcuts.ts" ], "compilerOptions": { "composite": true, "target": "esnext", "module": "esnext", "moduleResolution": "node", "strict": true, "jsx": "preserve", "sourceMap": true, "skipLibCheck": true, "resolveJsonModule": true, "esModuleInterop": true, "baseUrl": ".", "types": [ "naive-ui/volar", "./src/renderer/auto-imports.d.ts", "./src/renderer/components.d.ts" ], "paths": { "@/*": ["src/renderer/*"], "@renderer/*": ["src/renderer/*"], "@main/*": ["src/main/*"], "@i18n/*": ["src/i18n/*"] } } } ================================================ FILE: vite.config.ts ================================================ import vue from '@vitejs/plugin-vue'; import { resolve } from 'path'; import AutoImport from 'unplugin-auto-import/vite'; import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'; import Components from 'unplugin-vue-components/vite'; import { defineConfig } from 'vite'; import viteCompression from 'vite-plugin-compression'; import VueDevTools from 'vite-plugin-vue-devtools'; export default defineConfig({ base: './', // 项目src root: resolve('src/renderer'), resolve: { alias: { '@': resolve('src/renderer'), '@renderer': resolve('src/renderer'), '@i18n': resolve('src/i18n') } }, plugins: [ vue(), viteCompression(), VueDevTools(), AutoImport({ imports: [ 'vue', { 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'] } ] }), Components({ resolvers: [NaiveUiResolver()] }) ], publicDir: resolve('resources'), server: { host: '0.0.0.0', proxy: {} } });