├── .env.development ├── .eslintignore ├── .eslintrc.cjs ├── .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 ├── .prettierrc.yaml ├── CHANGELOG.md ├── README.md ├── android └── .gitignore ├── auto-imports.d.ts ├── build ├── entitlements.mac.plist ├── icon.icns ├── icon.ico ├── icon.png └── installer.nsh ├── components.d.ts ├── dev-app-update.yml ├── docs ├── image.png ├── image1.png ├── image2.png ├── image3.png ├── image4.png ├── image5.png └── image6.png ├── electron-builder.yml ├── electron.vite.config.ts ├── package.json ├── postcss.config.js ├── resources ├── favicon.ico ├── html │ └── remote-control.html ├── icon.icns ├── icon.ico ├── icon.png ├── icon_16x16.png ├── icons │ ├── next.png │ ├── pause.png │ ├── play.png │ └── prev.png └── manifest.json ├── src ├── i18n │ ├── lang │ │ ├── en-US │ │ │ ├── artist.ts │ │ │ ├── common.ts │ │ │ ├── comp.ts │ │ │ ├── donation.ts │ │ │ ├── download.ts │ │ │ ├── favorite.ts │ │ │ ├── history.ts │ │ │ ├── index.ts │ │ │ ├── login.ts │ │ │ ├── player.ts │ │ │ ├── search.ts │ │ │ ├── settings.ts │ │ │ ├── songItem.ts │ │ │ └── user.ts │ │ └── zh-CN │ │ │ ├── artist.ts │ │ │ ├── common.ts │ │ │ ├── comp.ts │ │ │ ├── donation.ts │ │ │ ├── download.ts │ │ │ ├── favorite.ts │ │ │ ├── history.ts │ │ │ ├── index.ts │ │ │ ├── login.ts │ │ │ ├── player.ts │ │ │ ├── search.ts │ │ │ ├── settings.ts │ │ │ ├── songItem.ts │ │ │ └── user.ts │ ├── main.ts │ └── renderer.ts ├── main │ ├── index.ts │ ├── lyric.ts │ ├── modules │ │ ├── cache.ts │ │ ├── config.ts │ │ ├── deviceInfo.ts │ │ ├── fileManager.ts │ │ ├── fonts.ts │ │ ├── remoteControl.ts │ │ ├── shortcuts.ts │ │ ├── statsService.ts │ │ ├── tray.ts │ │ ├── update.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 │ │ ├── music.ts │ │ ├── mv.ts │ │ ├── search.ts │ │ ├── stats.ts │ │ └── user.ts │ ├── assets │ │ ├── alipay.png │ │ ├── css │ │ │ └── base.css │ │ ├── icon.png │ │ ├── icon │ │ │ ├── iconfont.css │ │ │ ├── iconfont.js │ │ │ ├── iconfont.json │ │ │ ├── iconfont.ttf │ │ │ ├── iconfont.woff │ │ │ └── iconfont.woff2 │ │ ├── logo.png │ │ └── wechat.png │ ├── auto-imports.d.ts │ ├── components.d.ts │ ├── components │ │ ├── Coffee.vue │ │ ├── EQControl.vue │ │ ├── LanguageSwitcher.vue │ │ ├── MusicList.vue │ │ ├── MvPlayer.vue │ │ ├── ShortcutToast.vue │ │ ├── common │ │ │ ├── ArtistDrawer.vue │ │ │ ├── BilibiliItem.vue │ │ │ ├── DonationList.vue │ │ │ ├── DownloadDrawer.vue │ │ │ ├── InstallAppModal.vue │ │ │ ├── MusicListNavigator.ts │ │ │ ├── PlayBottom.vue │ │ │ ├── PlayListsItem.vue │ │ │ ├── PlaylistDrawer.vue │ │ │ ├── SearchItem.vue │ │ │ ├── SongItem.vue │ │ │ ├── UpdateModal.vue │ │ │ └── songItemCom │ │ │ │ ├── BaseSongItem.vue │ │ │ │ ├── CompactSongItem.vue │ │ │ │ ├── ListSongItem.vue │ │ │ │ ├── MiniSongItem.vue │ │ │ │ ├── SongItemDropdown.vue │ │ │ │ └── StandardSongItem.vue │ │ ├── home │ │ │ ├── PlaylistType.vue │ │ │ ├── RecommendAlbum.vue │ │ │ ├── RecommendSonglist.vue │ │ │ └── TopBanner.vue │ │ ├── lyric │ │ │ ├── LyricCorrectionControl.vue │ │ │ └── LyricSettings.vue │ │ ├── player │ │ │ ├── AdvancedControlsPopover.vue │ │ │ ├── MiniPlayBar.vue │ │ │ ├── MobilePlayBar.vue │ │ │ ├── PlayBar.vue │ │ │ ├── PlayingListDrawer.vue │ │ │ ├── ReparsePopover.vue │ │ │ ├── SleepTimer.vue │ │ │ └── SleepTimerTop.vue │ │ └── settings │ │ │ ├── ClearCacheSettings.vue │ │ │ ├── MusicSourceSettings.vue │ │ │ ├── ProxySettings.vue │ │ │ ├── ServerSetting.vue │ │ │ └── ShortcutSettings.vue │ ├── const │ │ └── bar-const.ts │ ├── directive │ │ ├── index.ts │ │ └── loading │ │ │ ├── index.ts │ │ │ └── index.vue │ ├── hooks │ │ ├── IndexDBHook.ts │ │ ├── MusicHistoryHook.ts │ │ ├── MusicHook.ts │ │ ├── MusicListHook.ts │ │ ├── useArtist.ts │ │ ├── useDownload.ts │ │ ├── useSongItem.ts │ │ └── useZoom.ts │ ├── index.css │ ├── index.html │ ├── layout │ │ ├── AppLayout.vue │ │ ├── MiniLayout.vue │ │ └── components │ │ │ ├── AppMenu.vue │ │ │ ├── MusicFull.vue │ │ │ ├── SearchBar.vue │ │ │ ├── TitleBar.vue │ │ │ ├── index.ts │ │ │ └── lrcFull.vue │ ├── main.ts │ ├── router │ │ ├── home.ts │ │ ├── index.ts │ │ └── other.ts │ ├── services │ │ ├── audioService.ts │ │ └── eqService.ts │ ├── shims-vue.d.ts │ ├── store │ │ ├── index.ts │ │ └── modules │ │ │ ├── lyric.ts │ │ │ ├── menu.ts │ │ │ ├── music.ts │ │ │ ├── player.ts │ │ │ ├── search.ts │ │ │ ├── settings.ts │ │ │ └── user.ts │ ├── type │ │ ├── album.ts │ │ ├── artist.ts │ │ ├── day_recommend.ts │ │ ├── index.ts │ │ ├── list.ts │ │ ├── listDetail.ts │ │ ├── lyric.ts │ │ ├── music.ts │ │ ├── mv.ts │ │ ├── playlist.ts │ │ ├── search.ts │ │ ├── singer.ts │ │ └── user.ts │ ├── types │ │ ├── bilibili.ts │ │ ├── electron.d.ts │ │ ├── lyric.ts │ │ └── music.ts │ ├── utils │ │ ├── appShortcuts.ts │ │ ├── fileOperation.ts │ │ ├── index.ts │ │ ├── linearColor.ts │ │ ├── request.ts │ │ ├── request_music.ts │ │ ├── shortcutToast.ts │ │ ├── theme.ts │ │ └── update.ts │ ├── views │ │ ├── artist │ │ │ └── detail.vue │ │ ├── bilibili │ │ │ └── BilibiliPlayer.vue │ │ ├── favorite │ │ │ └── index.vue │ │ ├── history │ │ │ └── index.vue │ │ ├── historyAndFavorite │ │ │ └── index.vue │ │ ├── home │ │ │ └── index.vue │ │ ├── list │ │ │ └── index.vue │ │ ├── login │ │ │ └── index.vue │ │ ├── lyric │ │ │ └── index.vue │ │ ├── music │ │ │ └── MusicListPage.vue │ │ ├── mv │ │ │ └── index.vue │ │ ├── search │ │ │ └── index.vue │ │ ├── set │ │ │ └── index.vue │ │ ├── toplist │ │ │ └── index.vue │ │ └── user │ │ │ ├── detail.vue │ │ │ ├── followers.vue │ │ │ ├── follows.vue │ │ │ └── index.vue │ └── vite-env.d.ts └── types │ └── shortcuts.ts ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json └── tsconfig.web.json /.env.development: -------------------------------------------------------------------------------- 1 | # 你的接口地址 (必填) 2 | VITE_API = http://127.0.0.1:30488 3 | # 音乐破解接口地址 web端 4 | VITE_API_MUSIC = *** 5 | 6 | 7 | # 本地运行代理地址 8 | VITE_API_LOCAL = /api 9 | VITE_API_MUSIC_PROXY = /music 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @algerkong 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.zh-CN.yml: -------------------------------------------------------------------------------- 1 | name: 反馈 Bug 2 | description: 通过 github 模板进行 Bug 反馈。 3 | title: "描述问题的标题" 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # 欢迎你的参与 9 | Issue 列表接受 bug 报告或是新功能请求。 10 | 11 | 在发布一个 Issue 前,请确保: 12 | - 在Issue中搜索过你的问题。(你的问题可能已有人提出,也可能已在最新版本中被修正) 13 | - 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在,不要在旧 Issue 下面留言,请建一个新的 issue。 14 | 15 | - type: input 16 | id: reproduce 17 | attributes: 18 | label: 重现链接 19 | description: 请提供尽可能精简的 CodePen、CodeSandbox 或 GitHub 仓库的链接。请不要填无关链接,否则你的 Issue 将被关闭。 20 | placeholder: 请填写 21 | 22 | - type: textarea 23 | id: reproduceSteps 24 | attributes: 25 | label: 重现步骤 26 | description: 请清晰的描述重现该 Issue 的步骤,这能帮助我们快速定位问题。没有清晰重现步骤将不会被修复,标有 'need reproduction' 的 Issue 在 7 天内不提供相关步骤,将被关闭。 27 | placeholder: 请填写 28 | 29 | - type: textarea 30 | id: expect 31 | attributes: 32 | label: 期望结果 33 | placeholder: 请填写 34 | 35 | - type: textarea 36 | id: actual 37 | attributes: 38 | label: 实际结果 39 | placeholder: 请填写 40 | 41 | - type: input 42 | id: frameworkVersion 43 | attributes: 44 | label: 框架版本 45 | placeholder: Vue(3.3.0) 46 | 47 | - type: input 48 | id: browsersVersion 49 | attributes: 50 | label: 浏览器版本 51 | placeholder: Chrome(8.213.231.123) 52 | 53 | - type: input 54 | id: systemVersion 55 | attributes: 56 | label: 系统版本 57 | placeholder: MacOS(11.2.3) 58 | 59 | - type: input 60 | id: nodeVersion 61 | attributes: 62 | label: Node版本 63 | placeholder: 请填写 64 | 65 | - type: textarea 66 | id: remarks 67 | attributes: 68 | label: 补充说明 69 | description: 可以是遇到这个 bug 的业务场景、上下文等信息。 70 | placeholder: 请填写 71 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | contact_links: 3 | - name: 4 | url: 5 | about: -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-report.zh-CN.yml: -------------------------------------------------------------------------------- 1 | name: 反馈新功能 2 | description: 通过 github 模板进行新功能反馈。 3 | title: "描述问题的标题" 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | # 欢迎你的参与 9 | 10 | 在发布一个 Issue 前,请确保: 11 | - 在 Issue 中搜索过你的问题。(你的问题可能已有人提出,也可能已在最新版本中被修正) 12 | - 如果你发现一个已经关闭的旧 Issue 在最新版本中仍然存在,不要在旧 Issue 下面留言,请建一个新的 issue。 13 | 14 | - type: textarea 15 | id: functionContent 16 | attributes: 17 | label: 这个功能解决了什么问题 18 | description: 请详尽说明这个需求的用例和场景。最重要的是:解释清楚是怎样的用户体验需求催生了这个功能上的需求。我们将考虑添加在现有 API 无法轻松实现的功能。新功能的用例也应当足够常见。 19 | placeholder: 请填写 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: functionalExpectations 25 | attributes: 26 | label: 你建议的方案是什么 27 | placeholder: 请填写 28 | validations: 29 | required: true 30 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 5 | 6 | ### 🤔 这个 PR 的性质是? 7 | 8 | - [ ] 日常 bug 修复 9 | - [ ] 新特性提交 10 | - [ ] 文档改进 11 | - [ ] 演示代码改进 12 | - [ ] 组件样式/交互改进 13 | - [ ] CI/CD 改进 14 | - [ ] 重构 15 | - [ ] 代码风格优化 16 | - [ ] 测试用例 17 | - [ ] 分支合并 18 | - [ ] 其他 19 | 20 | ### 🔗 相关 Issue 21 | 22 | 25 | 26 | ### 💡 需求背景和解决方案 27 | 28 | 33 | 34 | ### 📝 更新日志 35 | 36 | 39 | 40 | - fix(组件名称): 处理问题或特性描述 ... 41 | 42 | - [ ] 本条 PR 不需要纳入 Changelog 43 | 44 | ### ☑️ 请求合并前的自查清单 45 | 46 | ⚠️ 请自检并全部**勾选全部选项**。⚠️ 47 | 48 | - [ ] 文档已补充或无须补充 49 | - [ ] 代码演示已提供或无须提供 50 | - [ ] TypeScript 定义已补充或无须补充 51 | - [ ] Changelog 已提供或无须提供 52 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Basic dependabot.yml file with 2 | # minimum configuration for two package managers 3 | 4 | version: 2 5 | updates: 6 | # Enable version updates for npm 7 | - package-ecosystem: 'npm' 8 | # Look for `package.json` and `lock` files in the `root` directory 9 | directory: '/' 10 | # Check the npm registry for updates every day (weekdays) 11 | schedule: 12 | interval: 'monthly' 13 | 14 | # Enable version updates for Docker 15 | - package-ecosystem: 'docker' 16 | # Look for a `Dockerfile` in the `root` directory 17 | directory: '/' 18 | # Check for updates once a week 19 | schedule: 20 | interval: 'monthly' 21 | -------------------------------------------------------------------------------- /.github/issue-shoot.md: -------------------------------------------------------------------------------- 1 | ## IssueShoot 2 | - 预估时长: {{ .duration }} 3 | - 期望完成时间: {{ .deadline }} 4 | - 开发难度: {{ .level }} 5 | - 参与人数: 1 6 | - 需求对接人: ivringpeng 7 | - 验收标准: 实现期望改造效果,提 PR 并通过验收无误 8 | - 备注: 最终激励以实际提交 `pull request` 并合并为准 9 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | matrix: 14 | os: [macos-latest, windows-latest, ubuntu-latest] 15 | 16 | steps: 17 | - name: Check out Git repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Install Node.js 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 18 24 | 25 | - name: Install Dependencies 26 | run: npm install 27 | 28 | # MacOS Build 29 | - name: Build MacOS 30 | if: matrix.os == 'macos-latest' 31 | run: | 32 | export ELECTRON_BUILDER_EXTRA_ARGS="--universal" 33 | npm run build:mac 34 | env: 35 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | CSC_IDENTITY_AUTO_DISCOVERY: false 37 | DEBUG: electron-builder 38 | 39 | # Windows Build 40 | - name: Build Windows 41 | if: matrix.os == 'windows-latest' 42 | run: npm run build:win 43 | env: 44 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | # Linux Build 47 | - name: Build Linux 48 | if: matrix.os == 'ubuntu-latest' 49 | run: | 50 | sudo apt-get update 51 | sudo apt-get install -y libgtk-3-dev webkit2gtk-4.0 libappindicator3-dev librsvg2-dev patchelf 52 | npm run build:linux 53 | env: 54 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | # Get version from tag 57 | - name: Get version from tag 58 | id: get_version 59 | run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV 60 | shell: bash 61 | 62 | # Read release notes 63 | - name: Read release notes 64 | id: release_notes 65 | run: | 66 | NOTES=$(awk "/## \[v${{ env.VERSION }}\]/{p=1;print;next} /## \[v/{p=0}p" CHANGELOG.md) 67 | echo "NOTES<> $GITHUB_ENV 68 | echo "$NOTES" >> $GITHUB_ENV 69 | echo "EOF" >> $GITHUB_ENV 70 | shell: bash 71 | 72 | # Upload artifacts 73 | - name: Upload artifacts 74 | uses: softprops/action-gh-release@v1 75 | with: 76 | files: | 77 | dist/*.dmg 78 | dist/*.exe 79 | dist/*.deb 80 | dist/*.AppImage 81 | dist/latest*.yml 82 | dist/*.blockmap 83 | body: ${{ env.NOTES }} 84 | draft: false 85 | prerelease: false 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Web 2 | 3 | on: 4 | push: 5 | branches: 6 | - main # 或者您的主分支名称 7 | workflow_dispatch: # 允许手动触发 8 | 9 | jobs: 10 | build-and-deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: '18' 19 | 20 | - name: 创建环境变量文件 21 | run: | 22 | echo "VITE_API=${{ secrets.VITE_API }}" > .env.production.local 23 | echo "VITE_API_MUSIC=${{ secrets.VITE_API_MUSIC }}" >> .env.production.local 24 | # 添加其他需要的环境变量 25 | cat .env.production.local # 查看创建的文件内容,调试用 26 | 27 | - name: Install Dependencies 28 | run: npm install 29 | 30 | - name: Build 31 | run: npm run build 32 | 33 | - name: Deploy to Server 34 | uses: appleboy/scp-action@master 35 | with: 36 | host: ${{ secrets.SERVER_HOST }} 37 | username: ${{ secrets.SERVER_USERNAME }} 38 | key: ${{ secrets.DEPLOY_KEY }} 39 | source: "out/renderer/*" 40 | target: ${{ secrets.DEPLOY_PATH }} 41 | strip_components: 2 42 | 43 | - name: Execute Remote Commands 44 | uses: appleboy/ssh-action@master 45 | with: 46 | host: ${{ secrets.SERVER_HOST }} 47 | username: ${{ secrets.SERVER_USERNAME }} 48 | key: ${{ secrets.DEPLOY_KEY }} 49 | script: | 50 | cd ${{ secrets.DEPLOY_PATH }} 51 | echo "部署完成于 $(date)" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | 4 | dist 5 | dist-ssr 6 | *.local 7 | dist_electron 8 | .idea 9 | 10 | # lock 11 | yarn.lock 12 | pnpm-lock.yaml 13 | package-lock.json 14 | dist.zip 15 | 16 | .vscode 17 | 18 | bun.lockb 19 | bun.lock 20 | 21 | .env.*.local 22 | 23 | out 24 | 25 | .cursorrules 26 | 27 | .github/deploy_keys 28 | 29 | resources/android/**/* 30 | 31 | .cursor -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | echo "运行类型检查..." 2 | npm run typecheck -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | echo "运行类型检查..." 2 | npm run typecheck -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | singleQuote: true 2 | semi: true 3 | printWidth: 100 4 | trailingComma: none 5 | endOfLine: auto 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

🎵 Alger Music Player

3 |
4 |
5 | 6 | GitHub stars 7 | 8 | 9 | GitHub release 10 | 11 | 12 | 加入频道 13 | 14 | 15 | Telegram 16 | 17 | 18 | 赞助 19 | 20 |
21 |
22 |
23 | Featured|HelloGitHub 24 |
25 | 26 | 27 | [项目下安装以及常用问题文档](https://www.yuque.com/alger-pfg5q/ip4f1a/bmgmfmghnhgwghkm?singleDoc#) 28 | 29 | 主要功能如下 30 | - 🎵 音乐推荐 31 | - 🔐 网易云账号登录与同步 32 | - 📝 功能 33 | - 播放历史记录 34 | - 歌曲收藏管理 35 | - 歌单 MV 排行榜 每日推荐 36 | - 自定义快捷键配置(全局或应用内) 37 | - 🎨 界面与交互 38 | - 沉浸式歌词显示(点击左下角封面进入) 39 | - 独立桌面歌词窗口 40 | - 明暗主题切换 41 | - 迷你模式 42 | - 状态栏控制 43 | - 多语言支持 44 | 45 | - 🎼 音乐功能 46 | - 支持歌单、MV、专辑等完整音乐服务 47 | - 灰色音乐资源解析(基于 @unblockneteasemusic/server) 48 | - 音乐单独解析 49 | - EQ均衡器 50 | - 定时播放 远程控制播放 倍速播放 51 | - 高品质音乐 52 | - 音乐文件下载(支持右键下载和批量下载, 附带歌词封面等信息) 53 | - 搜索 MV 音乐 专辑 歌单 bilibili 54 | - 音乐单独选择音源解析 55 | - 🚀 技术特性 56 | - 本地化服务,无需依赖在线API (基于 netease-cloud-music-api) 57 | - 全平台适配(Desktop & Web & Mobile Web & Android<测试> & ios<后续>) 58 | 59 | ## 项目简介 60 | 一个第三方音乐播放器、本地服务、桌面歌词、音乐下载、最高音质 61 | 62 | ## 预览地址 63 | [http://music.alger.fun/](http://music.alger.fun/) 64 | 65 | ## 软件截图 66 | ![首页白](./docs/image.png) 67 | ![首页黑](./docs/image3.png) 68 | ![歌词](./docs/image6.png) 69 | ![桌面歌词](./docs/image2.png) 70 | ![设置页面](./docs/image4.png) 71 | ![音乐远程控制](./docs/image5.png) 72 | 73 | ## 项目启动 74 | ```bash 75 | npm install 76 | npm run dev 77 | ``` 78 | ## 项目打包 79 | ```bash 80 | # web 81 | npm run build 82 | # win 83 | npm run build:win 84 | # mac 85 | npm run build:mac 86 | # linux 87 | npm run build:linux 88 | ``` 89 | 90 | 91 | ## 赞赏☕️ 92 | [赞赏列表](http://donate.alger.fun/) 93 | | 微信赞赏 | 支付宝赞赏 | 94 | | :--------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------: | 95 | | WeChat QRcode
喝点咖啡继续干 | Wechat QRcode
来包辣条吧~ | 96 | 97 | 98 | ## 项目统计 99 | [![Stargazers over time](https://starchart.cc/algerkong/AlgerMusicPlayer.svg?variant=adaptive)](https://starchart.cc/algerkong/AlgerMusicPlayer) 100 | ![Alt](https://repobeats.axiom.co/api/embed/c4d01b3632e241c90cdec9508dfde86a7f54c9f5.svg "Repobeats analytics image") 101 | 102 | 103 | 104 | ## 欢迎提Issues 105 | 106 | ## 免责声明 107 | 本软件仅用于学习交流,禁止用于商业用途,否则后果自负。 108 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | # Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore 2 | 3 | # Built application files 4 | *.apk 5 | *.aar 6 | *.ap_ 7 | *.aab 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | # Uncomment the following line in case you need and you don't have the release build type files in your app 20 | # release/ 21 | 22 | # Gradle files 23 | .gradle/ 24 | build/ 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | 38 | # Android Studio captures folder 39 | captures/ 40 | 41 | # IntelliJ 42 | *.iml 43 | .idea/workspace.xml 44 | .idea/tasks.xml 45 | .idea/gradle.xml 46 | .idea/assetWizardSettings.xml 47 | .idea/dictionaries 48 | .idea/libraries 49 | # Android Studio 3 in .gitignore file. 50 | .idea/caches 51 | .idea/modules.xml 52 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 53 | .idea/navEditor.xml 54 | 55 | # Keystore files 56 | # Uncomment the following lines if you do not want to check your keystore files in. 57 | #*.jks 58 | #*.keystore 59 | 60 | # External native build folder generated in Android Studio 2.2 and later 61 | .externalNativeBuild 62 | .cxx/ 63 | 64 | # Google Services (e.g. APIs or Firebase) 65 | # google-services.json 66 | 67 | # Freeline 68 | freeline.py 69 | freeline/ 70 | freeline_project_description.json 71 | 72 | # fastlane 73 | fastlane/report.xml 74 | fastlane/Preview.html 75 | fastlane/screenshots 76 | fastlane/test_output 77 | fastlane/readme.md 78 | 79 | # Version control 80 | vcs.xml 81 | 82 | # lint 83 | lint/intermediates/ 84 | lint/generated/ 85 | lint/outputs/ 86 | lint/tmp/ 87 | # lint/reports/ 88 | 89 | # Android Profiling 90 | *.hprof 91 | 92 | # Cordova plugins for Capacitor 93 | capacitor-cordova-android-plugins 94 | 95 | # Copied web assets 96 | app/src/main/assets/public 97 | 98 | # Generated Config files 99 | app/src/main/assets/capacitor.config.json 100 | app/src/main/assets/capacitor.plugins.json 101 | app/src/main/res/xml/config.xml 102 | -------------------------------------------------------------------------------- /build/entitlements.mac.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.cs.allow-jit 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.allow-dyld-environment-variables 10 | 11 | com.apple.security.network.client 12 | 13 | com.apple.security.network.server 14 | 15 | com.apple.security.files.user-selected.read-write 16 | 17 | com.apple.security.files.downloads.read-write 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /build/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/build/icon.icns -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/build/icon.ico -------------------------------------------------------------------------------- /build/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/build/icon.png -------------------------------------------------------------------------------- /build/installer.nsh: -------------------------------------------------------------------------------- 1 | # 设置 Windows 7 兼容性 2 | ManifestDPIAware true 3 | ManifestSupportedOS all 4 | 5 | !macro customInit 6 | # 检查系统版本 7 | ${If} ${AtLeastWin7} 8 | # Windows 7 或更高版本 9 | ${Else} 10 | MessageBox MB_OK|MB_ICONSTOP "此应用程序需要 Windows 7 或更高版本。" 11 | Abort 12 | ${EndIf} 13 | !macroend -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {}; 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | NAvatar: typeof import('naive-ui')['NAvatar'] 11 | NButton: typeof import('naive-ui')['NButton'] 12 | NCheckbox: typeof import('naive-ui')['NCheckbox'] 13 | NConfigProvider: typeof import('naive-ui')['NConfigProvider'] 14 | NDialogProvider: typeof import('naive-ui')['NDialogProvider'] 15 | NDrawer: typeof import('naive-ui')['NDrawer'] 16 | NDropdown: typeof import('naive-ui')['NDropdown'] 17 | NEllipsis: typeof import('naive-ui')['NEllipsis'] 18 | NEmpty: typeof import('naive-ui')['NEmpty'] 19 | NImage: typeof import('naive-ui')['NImage'] 20 | NInput: typeof import('naive-ui')['NInput'] 21 | NInputNumber: typeof import('naive-ui')['NInputNumber'] 22 | NLayout: typeof import('naive-ui')['NLayout'] 23 | NMessageProvider: typeof import('naive-ui')['NMessageProvider'] 24 | NModal: typeof import('naive-ui')['NModal'] 25 | NPopover: typeof import('naive-ui')['NPopover'] 26 | NScrollbar: typeof import('naive-ui')['NScrollbar'] 27 | NSlider: typeof import('naive-ui')['NSlider'] 28 | NSpin: typeof import('naive-ui')['NSpin'] 29 | NSwitch: typeof import('naive-ui')['NSwitch'] 30 | RouterLink: typeof import('vue-router')['RouterLink'] 31 | RouterView: typeof import('vue-router')['RouterView'] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | provider: generic 2 | url: https://example.com/auto-updates 3 | updaterCacheDirName: electron-lan-file-updater 4 | -------------------------------------------------------------------------------- /docs/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/docs/image.png -------------------------------------------------------------------------------- /docs/image1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/docs/image1.png -------------------------------------------------------------------------------- /docs/image2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/docs/image2.png -------------------------------------------------------------------------------- /docs/image3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/docs/image3.png -------------------------------------------------------------------------------- /docs/image4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/docs/image4.png -------------------------------------------------------------------------------- /docs/image5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/docs/image5.png -------------------------------------------------------------------------------- /docs/image6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/docs/image6.png -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.electron.app 2 | productName: electron-lan-file 3 | directories: 4 | buildResources: build 5 | files: 6 | - '!**/.vscode/*' 7 | - '!src/*' 8 | - '!electron.vite.config.{js,ts,mjs,cjs}' 9 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 10 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 11 | - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' 12 | asarUnpack: 13 | - resources/** 14 | win: 15 | executableName: electron-lan-file 16 | nsis: 17 | artifactName: ${name}-${version}-setup.${ext} 18 | shortcutName: ${productName} 19 | uninstallDisplayName: ${productName} 20 | createDesktopShortcut: always 21 | mac: 22 | entitlementsInherit: build/entitlements.mac.plist 23 | extendInfo: 24 | - NSCameraUsageDescription: Application requests access to the device's camera. 25 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 26 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 27 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 28 | notarize: false 29 | dmg: 30 | artifactName: ${name}-${version}.${ext} 31 | linux: 32 | target: 33 | - AppImage 34 | - snap 35 | - deb 36 | maintainer: electronjs.org 37 | category: Utility 38 | appImage: 39 | artifactName: ${name}-${version}.${ext} 40 | npmRebuild: false 41 | publish: 42 | provider: generic 43 | url: https://example.com/auto-updates 44 | electronDownload: 45 | mirror: https://npmmirror.com/mirrors/electron/ 46 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue'; 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; 3 | import { resolve } from 'path'; 4 | import AutoImport from 'unplugin-auto-import/vite'; 5 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'; 6 | import Components from 'unplugin-vue-components/vite'; 7 | import viteCompression from 'vite-plugin-compression'; 8 | import VueDevTools from 'vite-plugin-vue-devtools'; 9 | 10 | export default defineConfig({ 11 | main: { 12 | plugins: [externalizeDepsPlugin()] 13 | }, 14 | preload: { 15 | plugins: [externalizeDepsPlugin()] 16 | }, 17 | renderer: { 18 | resolve: { 19 | alias: { 20 | '@': resolve('src/renderer'), 21 | '@renderer': resolve('src/renderer') 22 | } 23 | }, 24 | plugins: [ 25 | vue(), 26 | viteCompression(), 27 | VueDevTools(), 28 | AutoImport({ 29 | imports: [ 30 | 'vue', 31 | { 32 | 'naive-ui': ['useDialog', 'useMessage', 'useNotification', 'useLoadingBar'] 33 | } 34 | ] 35 | }), 36 | Components({ 37 | resolvers: [NaiveUiResolver()] 38 | }) 39 | ], 40 | publicDir: resolve('resources'), 41 | server: { 42 | proxy: { 43 | // with options 44 | [process.env.VITE_API_LOCAL as string]: { 45 | target: process.env.VITE_API, 46 | changeOrigin: true, 47 | rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_LOCAL}`), '') 48 | }, 49 | [process.env.VITE_API_MUSIC_PROXY as string]: { 50 | target: process.env.VITE_API_MUSIC, 51 | changeOrigin: true, 52 | rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_MUSIC_PROXY}`), '') 53 | }, 54 | [process.env.VITE_API_PROXY_MUSIC as string]: { 55 | target: process.env.VITE_API_PROXY, 56 | changeOrigin: true, 57 | rewrite: (path) => path.replace(new RegExp(`^${process.env.VITE_API_PROXY_MUSIC}`), '') 58 | } 59 | } 60 | } 61 | } 62 | }); 63 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/resources/favicon.ico -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/resources/icon.png -------------------------------------------------------------------------------- /resources/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/resources/icon_16x16.png -------------------------------------------------------------------------------- /resources/icons/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/resources/icons/next.png -------------------------------------------------------------------------------- /resources/icons/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/resources/icons/pause.png -------------------------------------------------------------------------------- /resources/icons/play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/resources/icons/play.png -------------------------------------------------------------------------------- /resources/icons/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/resources/icons/prev.png -------------------------------------------------------------------------------- /resources/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Alger Music PWA", 3 | "icons": [ 4 | { 5 | "src": "./icon.png", 6 | "type": "image/png", 7 | "sizes": "256x256" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/artist.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | hotSongs: 'Hot Songs', 3 | albums: 'Albums', 4 | description: 'Artist Introduction' 5 | }; 6 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/common.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | play: 'Play', 3 | next: 'Next', 4 | previous: 'Previous', 5 | volume: 'Volume', 6 | settings: 'Settings', 7 | search: 'Search', 8 | loading: 'Loading...', 9 | loadingMore: 'Loading more...', 10 | alipay: 'Alipay', 11 | wechat: 'WeChat Pay', 12 | on: 'On', 13 | off: 'Off', 14 | show: 'Show', 15 | hide: 'Hide', 16 | confirm: 'Confirm', 17 | cancel: 'Cancel', 18 | configure: 'Configure', 19 | open: 'Open', 20 | modify: 'Modify', 21 | success: 'Operation Successful', 22 | error: 'Operation Failed', 23 | warning: 'Warning', 24 | info: 'Info', 25 | save: 'Save', 26 | delete: 'Delete', 27 | refresh: 'Refresh', 28 | retry: 'Retry', 29 | reset: 'Reset', 30 | copySuccess: 'Copied to clipboard', 31 | copyFailed: 'Copy failed', 32 | validation: { 33 | required: 'This field is required', 34 | invalidInput: 'Invalid input', 35 | selectRequired: 'Please select an option', 36 | numberRange: 'Please enter a number between {min} and {max}', 37 | ipAddress: 'Please enter a valid IP address', 38 | portNumber: 'Please enter a valid port number (1-65535)' 39 | }, 40 | viewMore: 'View More', 41 | noMore: 'No more', 42 | expand: 'Expand', 43 | collapse: 'Collapse', 44 | songCount: '{count} songs', 45 | tray: { 46 | show: 'Show', 47 | quit: 'Quit', 48 | playPause: 'Play/Pause', 49 | prev: 'Previous', 50 | next: 'Next', 51 | pause: 'Pause', 52 | play: 'Play', 53 | favorite: 'Favorite' 54 | }, 55 | language: 'Language' 56 | }; 57 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/donation.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | description: 3 | 'Your donation will be used to support development and maintenance work, including but not limited to server maintenance, domain name renewal, etc.', 4 | message: 'You can leave your email or github name when leaving a message.', 5 | refresh: 'Refresh List', 6 | toDonateList: 'Buy me a coffee', 7 | title: 'Donation List', 8 | noMessage: 'No Message' 9 | }; 10 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/download.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Download Manager', 3 | localMusic: 'Local Music', 4 | count: '{count} songs in total', 5 | clearAll: 'Clear All', 6 | tabs: { 7 | downloading: 'Downloading', 8 | downloaded: 'Downloaded' 9 | }, 10 | empty: { 11 | noTasks: 'No download tasks', 12 | noDownloaded: 'No downloaded songs' 13 | }, 14 | progress: { 15 | total: 'Total Progress: {progress}%' 16 | }, 17 | status: { 18 | downloading: 'Downloading', 19 | completed: 'Completed', 20 | failed: 'Failed', 21 | unknown: 'Unknown' 22 | }, 23 | artist: { 24 | unknown: 'Unknown Artist' 25 | }, 26 | delete: { 27 | title: 'Delete Confirmation', 28 | message: 'Are you sure you want to delete "{filename}"? This action cannot be undone.', 29 | confirm: 'Delete', 30 | cancel: 'Cancel', 31 | success: 'Successfully deleted', 32 | failed: 'Failed to delete', 33 | fileNotFound: 'File not found or moved, removed from records', 34 | recordRemoved: 'Failed to delete file, but removed from records' 35 | }, 36 | clear: { 37 | title: 'Clear Download Records', 38 | message: 39 | 'Are you sure you want to clear all download records? This will not delete the actual music files, but will clear all records.', 40 | confirm: 'Clear', 41 | cancel: 'Cancel', 42 | success: 'Download records cleared' 43 | }, 44 | message: { 45 | downloadComplete: '{filename} download completed', 46 | downloadFailed: '{filename} download failed: {error}' 47 | }, 48 | loading: 'Loading...' 49 | }; 50 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/favorite.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Favorites', 3 | count: 'Total {count}', 4 | batchDownload: 'Batch Download', 5 | selectAll: 'All', 6 | download: 'Download ({count})', 7 | cancel: 'Cancel', 8 | emptyTip: 'No favorite songs yet', 9 | viewMore: 'View More', 10 | noMore: 'No more', 11 | downloadSuccess: 'Download completed', 12 | downloadFailed: 'Download failed', 13 | downloading: 'Downloading, please wait...', 14 | selectSongsFirst: 'Please select songs to download first', 15 | descending: 'Descending', 16 | ascending: 'Ascending' 17 | }; 18 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/history.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Play History', 3 | playCount: '{count}', 4 | getHistoryFailed: 'Failed to get play history' 5 | }; 6 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/index.ts: -------------------------------------------------------------------------------- 1 | import artist from './artist'; 2 | import common from './common'; 3 | import comp from './comp'; 4 | import donation from './donation'; 5 | import download from './download'; 6 | import favorite from './favorite'; 7 | import history from './history'; 8 | import login from './login'; 9 | import player from './player'; 10 | import search from './search'; 11 | import settings from './settings'; 12 | import songItem from './songItem'; 13 | import user from './user'; 14 | 15 | export default { 16 | common, 17 | donation, 18 | favorite, 19 | history, 20 | login, 21 | player, 22 | search, 23 | settings, 24 | songItem, 25 | user, 26 | download, 27 | comp, 28 | artist 29 | }; 30 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/login.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: { 3 | qr: 'QR Code Login', 4 | phone: 'Phone Login' 5 | }, 6 | qrTip: 'Scan with NetEase Cloud Music APP', 7 | phoneTip: 'Login with NetEase Cloud account', 8 | placeholder: { 9 | phone: 'Phone Number', 10 | password: 'Password' 11 | }, 12 | button: { 13 | login: 'Login', 14 | switchToQr: 'QR Code Login', 15 | switchToPhone: 'Phone Login' 16 | }, 17 | message: { 18 | loginSuccess: 'Login successful', 19 | loadError: 'Error loading login information', 20 | qrCheckError: 'Error checking QR code status' 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/player.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | nowPlaying: 'Now Playing', 3 | playlist: 'Playlist', 4 | lyrics: 'Lyrics', 5 | previous: 'Previous', 6 | play: 'Play', 7 | pause: 'Pause', 8 | next: 'Next', 9 | volumeUp: 'Volume Up', 10 | volumeDown: 'Volume Down', 11 | mute: 'Mute', 12 | unmute: 'Unmute', 13 | songNum: 'Song Number: {num}', 14 | addCorrection: 'Add {num} seconds', 15 | subtractCorrection: 'Subtract {num} seconds', 16 | playFailed: 'Play Failed, Play Next Song', 17 | playMode: { 18 | sequence: 'Sequence', 19 | loop: 'Loop', 20 | random: 'Random' 21 | }, 22 | fullscreen: { 23 | enter: 'Enter Fullscreen', 24 | exit: 'Exit Fullscreen' 25 | }, 26 | close: 'Close', 27 | modeHint: { 28 | single: 'Single', 29 | list: 'Next' 30 | }, 31 | lrc: { 32 | noLrc: 'No lyrics, please enjoy' 33 | }, 34 | reparse: { 35 | title: 'Select Music Source', 36 | desc: 'Click a source to directly reparse the current song. This source will be used next time this song plays.', 37 | success: 'Reparse successful', 38 | failed: 'Reparse failed', 39 | warning: 'Please select a music source', 40 | bilibiliNotSupported: 'Bilibili videos do not support reparsing', 41 | processing: 'Processing...' 42 | }, 43 | playBar: { 44 | expand: 'Expand Lyrics', 45 | collapse: 'Collapse Lyrics', 46 | like: 'Like', 47 | lyric: 'Lyric', 48 | noSongPlaying: 'No song playing', 49 | eq: 'Equalizer', 50 | playList: 'Play List', 51 | reparse: 'Reparse', 52 | playMode: { 53 | sequence: 'Sequence', 54 | loop: 'Loop', 55 | random: 'Random' 56 | }, 57 | play: 'Play', 58 | pause: 'Pause', 59 | prev: 'Previous', 60 | next: 'Next', 61 | volume: 'Volume', 62 | favorite: 'Favorite {name}', 63 | unFavorite: 'Unfavorite {name}', 64 | playbackSpeed: 'Playback Speed', 65 | advancedControls: 'Advanced Controls', 66 | }, 67 | eq: { 68 | title: 'Equalizer', 69 | reset: 'Reset', 70 | on: 'On', 71 | off: 'Off', 72 | bass: 'Bass', 73 | midrange: 'Midrange', 74 | treble: 'Treble', 75 | presets: { 76 | flat: 'Flat', 77 | pop: 'Pop', 78 | rock: 'Rock', 79 | classical: 'Classical', 80 | jazz: 'Jazz', 81 | electronic: 'Electronic', 82 | hiphop: 'Hip-Hop', 83 | rb: 'R&B', 84 | metal: 'Metal', 85 | vocal: 'Vocal', 86 | dance: 'Dance', 87 | acoustic: 'Acoustic', 88 | custom: 'Custom' 89 | } 90 | }, 91 | // Sleep timer related 92 | sleepTimer: { 93 | title: 'Sleep Timer', 94 | cancel: 'Cancel Timer', 95 | timeMode: 'By Time', 96 | songsMode: 'By Songs', 97 | playlistEnd: 'After Playlist', 98 | afterPlaylist: 'After Playlist Ends', 99 | activeUntilEnd: 'Active until end of playlist', 100 | minutes: 'min', 101 | hours: 'hr', 102 | songs: 'songs', 103 | set: 'Set', 104 | timerSetSuccess: 'Timer set for {minutes} minutes', 105 | songsSetSuccess: 'Timer set for {songs} songs', 106 | playlistEndSetSuccess: 'Timer set to end after playlist', 107 | timerCancelled: 'Sleep timer cancelled', 108 | timerEnded: 'Sleep timer ended', 109 | playbackStopped: 'Music playback stopped', 110 | minutesRemaining: '{minutes} min remaining', 111 | songsRemaining: '{count} songs remaining' 112 | }, 113 | playList: { 114 | clearAll: 'Clear Playlist', 115 | alreadyEmpty: 'Playlist is already empty', 116 | cleared: 'Playlist cleared', 117 | empty: 'Playlist is empty', 118 | clearConfirmTitle: 'Clear Playlist', 119 | clearConfirmContent: 'This will clear all songs in the playlist and stop the current playback. Continue?' 120 | } 121 | }; 122 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/search.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: { 3 | hotSearch: 'Hot Search', 4 | searchList: 'Search Results', 5 | searchHistory: 'Search History' 6 | }, 7 | button: { 8 | clear: 'Clear', 9 | back: 'Back', 10 | playAll: 'Play All' 11 | }, 12 | loading: { 13 | more: 'Loading...', 14 | failed: 'Search failed' 15 | }, 16 | noMore: 'No more results', 17 | error: { 18 | searchFailed: 'Search failed' 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/songItem.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | menu: { 3 | play: 'Play', 4 | playNext: 'Play Next', 5 | download: 'Download', 6 | addToPlaylist: 'Add to Playlist', 7 | favorite: 'Like', 8 | unfavorite: 'Unlike', 9 | removeFromPlaylist: 'Remove from Playlist', 10 | dislike: 'Dislike', 11 | undislike: 'Undislike', 12 | }, 13 | message: { 14 | downloading: 'Downloading, please wait...', 15 | downloadFailed: 'Download failed', 16 | downloadQueued: 'Added to download queue', 17 | addedToNextPlay: 'Added to play next', 18 | getUrlFailed: 'Failed to get music download URL, please check if logged in' 19 | }, 20 | dialog: { 21 | dislike:{ 22 | title: 'Dislike', 23 | content: 'Are you sure you want to dislike this song?', 24 | positiveText: 'Dislike', 25 | negativeText: 'Cancel' 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/i18n/lang/en-US/user.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | profile: { 3 | followers: 'Followers', 4 | following: 'Following', 5 | level: 'Level' 6 | }, 7 | playlist: { 8 | created: 'Created Playlists', 9 | mine: 'Mine', 10 | trackCount: '{count} tracks', 11 | playCount: 'Played {count} times' 12 | }, 13 | ranking: { 14 | title: 'Listening History', 15 | playCount: '{count} times' 16 | }, 17 | follow: { 18 | title: 'Follow List', 19 | viewPlaylist: 'View Playlist', 20 | noFollowings: 'No Followings', 21 | loadMore: 'Load More', 22 | noSignature: 'This guy is lazy, nothing left', 23 | userFollowsTitle: '\'s Followings', 24 | myFollowsTitle: 'My Followings' 25 | }, 26 | follower: { 27 | title: 'Follower List', 28 | noFollowers: 'No Followers', 29 | loadMore: 'Load More', 30 | userFollowersTitle: '\'s Followers', 31 | myFollowersTitle: 'My Followers' 32 | }, 33 | detail: { 34 | playlists: 'Playlists', 35 | records: 'Listening History', 36 | noPlaylists: 'No Playlists', 37 | noRecords: 'No Listening History', 38 | artist: 'Artist', 39 | noSignature: 'This guy is lazy, nothing left', 40 | invalidUserId: 'Invalid User ID', 41 | noRecordPermission: '{name} doesn\'t let you see your listening history' 42 | }, 43 | message: { 44 | loadFailed: 'Failed to load user page', 45 | deleteSuccess: 'Successfully deleted', 46 | deleteFailed: 'Failed to delete' 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/artist.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | hotSongs: '热门歌曲', 3 | albums: '专辑', 4 | description: '艺人介绍' 5 | }; 6 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/common.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | play: '播放', 3 | next: '下一首', 4 | previous: '上一首', 5 | volume: '音量', 6 | settings: '设置', 7 | search: '搜索', 8 | loading: '加载中...', 9 | loadingMore: '加载更多...', 10 | alipay: '支付宝', 11 | wechat: '微信支付', 12 | on: '开启', 13 | off: '关闭', 14 | show: '显示', 15 | hide: '隐藏', 16 | confirm: '确认', 17 | cancel: '取消', 18 | configure: '配置', 19 | open: '打开', 20 | modify: '修改', 21 | success: '操作成功', 22 | error: '操作失败', 23 | warning: '警告', 24 | info: '提示', 25 | save: '保存', 26 | delete: '删除', 27 | refresh: '刷新', 28 | retry: '重试', 29 | reset: '重置', 30 | copySuccess: '已复制到剪贴板', 31 | copyFailed: '复制失败', 32 | validation: { 33 | required: '此项是必填的', 34 | invalidInput: '输入无效', 35 | selectRequired: '请选择一个选项', 36 | numberRange: '请输入 {min} 到 {max} 之间的数字' 37 | }, 38 | viewMore: '查看更多', 39 | noMore: '没有更多了', 40 | selectAll: '全选', 41 | expand: '展开', 42 | collapse: '收起', 43 | songCount: '{count}首', 44 | language: '语言', 45 | tray: { 46 | show: '显示', 47 | quit: '退出', 48 | playPause: '播放/暂停', 49 | prev: '上一首', 50 | next: '下一首', 51 | pause: '暂停', 52 | play: '播放', 53 | favorite: '收藏' 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/comp.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | installApp: { 3 | description: '安装应用程序,获得更好的体验', 4 | noPrompt: '不再提示', 5 | install: '立即安装', 6 | cancel: '暂不安装', 7 | download: '下载', 8 | downloadFailed: '下载失败', 9 | downloadComplete: '下载完成', 10 | downloadProblem: '下载遇到问题?去', 11 | downloadProblemLinkText: '下载最新版本' 12 | }, 13 | playlistDrawer: { 14 | title: '添加到歌单', 15 | createPlaylist: '创建新歌单', 16 | cancelCreate: '取消创建', 17 | create: '创建', 18 | playlistName: '歌单名称', 19 | privatePlaylist: '私密歌单', 20 | publicPlaylist: '公开歌单', 21 | createSuccess: '歌单创建成功', 22 | createFailed: '歌单创建失败', 23 | addSuccess: '歌曲添加成功', 24 | addFailed: '歌曲添加失败', 25 | private: '私密', 26 | public: '公开', 27 | count: '首歌曲', 28 | loginFirst: '请先登录', 29 | getPlaylistFailed: '获取歌单失败', 30 | inputPlaylistName: '请输入歌单名称' 31 | }, 32 | update: { 33 | title: '发现新版本', 34 | currentVersion: '当前版本', 35 | cancel: '暂不更新', 36 | prepareDownload: '准备下载...', 37 | downloading: '下载中...', 38 | nowUpdate: '立即更新', 39 | downloadFailed: '下载失败,请重试或手动下载', 40 | startFailed: '启动下载失败,请重试或手动下载', 41 | noDownloadUrl: '未找到适合当前系统的安装包,请手动下载', 42 | installConfirmTitle: '安装更新', 43 | installConfirmContent: '是否关闭应用并安装更新?', 44 | manualInstallTip: '如果关闭应用后没有正常弹出安装程序,请至下载文件夹查找文件并手动打开。', 45 | yesInstall: '立即安装', 46 | noThanks: '稍后安装', 47 | fileLocation: '文件位置', 48 | copy: '复制路径', 49 | copySuccess: '路径已复制到剪贴板', 50 | copyFailed: '复制失败', 51 | backgroundDownload: '后台下载' 52 | }, 53 | coffee: { 54 | title: '请我喝咖啡', 55 | alipay: '支付宝', 56 | wechat: '微信支付', 57 | alipayQR: '支付宝收款码', 58 | wechatQR: '微信收款码', 59 | coffeeDesc: '一杯咖啡,一份支持', 60 | coffeeDescLinkText: '查看更多', 61 | qqGroup: 'QQ频道:algermusic', 62 | messages: { 63 | copySuccess: '已复制到剪贴板' 64 | }, 65 | donateList: '请我喝咖啡' 66 | }, 67 | playlistType: { 68 | title: '歌单分类', 69 | showAll: '显示全部', 70 | hide: '隐藏一些' 71 | }, 72 | recommendAlbum: { 73 | title: '最新专辑' 74 | }, 75 | recommendSinger: { 76 | title: '每日推荐', 77 | songlist: '每日推荐列表' 78 | }, 79 | recommendSonglist: { 80 | title: '本周最热音乐' 81 | }, 82 | searchBar: { 83 | login: '登录', 84 | toLogin: '去登录', 85 | logout: '退出登录', 86 | set: '设置', 87 | theme: '主题', 88 | restart: '重启', 89 | refresh: '刷新', 90 | currentVersion: '当前版本', 91 | searchPlaceholder: '搜索点什么吧...', 92 | zoom: '页面缩放', 93 | zoom100: '标准缩放100%', 94 | resetZoom: '点击重置缩放', 95 | zoomDefault: '标准缩放' 96 | }, 97 | titleBar: { 98 | closeTitle: '请选择关闭方式', 99 | minimizeToTray: '最小化到托盘', 100 | exitApp: '退出应用', 101 | rememberChoice: '记住我的选择', 102 | closeApp: '关闭应用' 103 | }, 104 | userPlayList: { 105 | title: '{name}的常听' 106 | }, 107 | musicList: { 108 | searchSongs: '搜索歌曲', 109 | noSearchResults: '没有找到相关歌曲', 110 | switchToNormal: '切换到默认布局', 111 | switchToCompact: '切换到紧凑布局', 112 | playAll: '播放全部', 113 | collect: '收藏', 114 | collectSuccess: '收藏成功', 115 | cancelCollectSuccess: '取消收藏成功', 116 | operationFailed: '操作失败', 117 | cancelCollect: '取消收藏', 118 | addToPlaylist: '添加到播放列表', 119 | addToPlaylistSuccess: '添加到播放列表成功', 120 | songsAlreadyInPlaylist: '歌曲已存在于播放列表中' 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/donation.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | description: '您的捐赠将用于支持开发和维护工作,包括但不限于服务器维护、域名续费等。', 3 | message: '留言时可留下您的邮箱或 github名称。', 4 | refresh: '刷新列表', 5 | toDonateList: '请我喝咖啡', 6 | noMessage: '暂无留言', 7 | title: '捐赠列表' 8 | }; 9 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/download.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: '下载管理', 3 | localMusic: '本地音乐', 4 | count: '共 {count} 首歌曲', 5 | clearAll: '清空记录', 6 | tabs: { 7 | downloading: '下载中', 8 | downloaded: '已下载' 9 | }, 10 | empty: { 11 | noTasks: '暂无下载任务', 12 | noDownloaded: '暂无已下载歌曲' 13 | }, 14 | progress: { 15 | total: '总进度: {progress}%' 16 | }, 17 | status: { 18 | downloading: '下载中', 19 | completed: '已完成', 20 | failed: '失败', 21 | unknown: '未知' 22 | }, 23 | artist: { 24 | unknown: '未知歌手' 25 | }, 26 | delete: { 27 | title: '删除确认', 28 | message: '确定要删除歌曲 "{filename}" 吗?此操作不可恢复。', 29 | confirm: '确定删除', 30 | cancel: '取消', 31 | success: '删除成功', 32 | failed: '删除失败', 33 | fileNotFound: '文件不存在或已被移动,已从记录中移除', 34 | recordRemoved: '文件删除失败,但已从记录中移除' 35 | }, 36 | clear: { 37 | title: '清空下载记录', 38 | message: '确定要清空所有下载记录吗?此操作不会删除已下载的音乐文件,但将清空所有记录。', 39 | confirm: '确定清空', 40 | cancel: '取消', 41 | success: '下载记录已清空' 42 | }, 43 | message: { 44 | downloadComplete: '{filename} 下载完成', 45 | downloadFailed: '{filename} 下载失败: {error}' 46 | }, 47 | loading: '加载中...' 48 | }; 49 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/favorite.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: '我的收藏', 3 | count: '共 {count} 首', 4 | batchDownload: '批量下载', 5 | download: '下载 ({count})', 6 | emptyTip: '还没有收藏歌曲', 7 | downloadSuccess: '下载完成', 8 | downloadFailed: '下载失败', 9 | downloading: '正在下载中,请稍候...', 10 | selectSongsFirst: '请先选择要下载的歌曲', 11 | descending: '降', 12 | ascending: '升' 13 | }; 14 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/history.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: '播放历史', 3 | playCount: '{count}', 4 | getHistoryFailed: '获取历史记录失败' 5 | }; 6 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/index.ts: -------------------------------------------------------------------------------- 1 | import artist from './artist'; 2 | import common from './common'; 3 | import comp from './comp'; 4 | import donation from './donation'; 5 | import download from './download'; 6 | import favorite from './favorite'; 7 | import history from './history'; 8 | import login from './login'; 9 | import player from './player'; 10 | import search from './search'; 11 | import settings from './settings'; 12 | import songItem from './songItem'; 13 | import user from './user'; 14 | 15 | export default { 16 | common, 17 | donation, 18 | favorite, 19 | history, 20 | login, 21 | player, 22 | search, 23 | settings, 24 | songItem, 25 | user, 26 | download, 27 | comp, 28 | artist 29 | }; 30 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/login.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: { 3 | qr: '扫码登录', 4 | phone: '手机号登录' 5 | }, 6 | qrTip: '使用网易云APP扫码登录', 7 | phoneTip: '使用网易云账号登录', 8 | placeholder: { 9 | phone: '手机号', 10 | password: '密码' 11 | }, 12 | button: { 13 | login: '登录', 14 | switchToQr: '扫码登录', 15 | switchToPhone: '手机号登录' 16 | }, 17 | message: { 18 | loginSuccess: '登录成功', 19 | loadError: '加载登录信息时出错', 20 | qrCheckError: '检查二维码状态时出错' 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/player.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | nowPlaying: '正在播放', 3 | playlist: '播放列表', 4 | lyrics: '歌词', 5 | previous: '上一个', 6 | play: '播放', 7 | pause: '暂停', 8 | next: '下一个', 9 | volumeUp: '音量增加', 10 | volumeDown: '音量减少', 11 | mute: '静音', 12 | unmute: '取消静音', 13 | songNum: '歌曲总数:{num}', 14 | addCorrection: '提前 {num} 秒', 15 | subtractCorrection: '延迟 {num} 秒', 16 | playFailed: '当前歌曲播放失败,播放下一首', 17 | playMode: { 18 | sequence: '顺序播放', 19 | loop: '循环播放', 20 | random: '随机播放' 21 | }, 22 | fullscreen: { 23 | enter: '全屏', 24 | exit: '退出全屏' 25 | }, 26 | close: '关闭', 27 | modeHint: { 28 | single: '单曲循环', 29 | list: '自动播放下一个' 30 | }, 31 | lrc: { 32 | noLrc: '暂无歌词, 请欣赏' 33 | }, 34 | reparse: { 35 | title: '选择解析音源', 36 | desc: '点击音源直接进行解析,下次播放此歌曲时将使用所选音源', 37 | success: '重新解析成功', 38 | failed: '重新解析失败', 39 | warning: '请选择一个音源', 40 | bilibiliNotSupported: 'B站视频不支持重新解析', 41 | processing: '解析中...' 42 | }, 43 | playBar: { 44 | expand: '展开歌词', 45 | collapse: '收起歌词', 46 | like: '喜欢', 47 | lyric: '歌词', 48 | noSongPlaying: '没有正在播放的歌曲', 49 | eq: '均衡器', 50 | playList: '播放列表', 51 | reparse: '重新解析', 52 | playMode: { 53 | sequence: '顺序播放', 54 | loop: '循环播放', 55 | random: '随机播放' 56 | }, 57 | play: '开始播放', 58 | pause: '暂停播放', 59 | prev: '上一首', 60 | next: '下一首', 61 | volume: '音量', 62 | favorite: '已收藏{name}', 63 | unFavorite: '已取消收藏{name}', 64 | miniPlayBar: '迷你播放栏', 65 | playbackSpeed: '播放速度', 66 | advancedControls: '更多设置s', 67 | }, 68 | eq: { 69 | title: '均衡器', 70 | reset: '重置', 71 | on: '开启', 72 | off: '关闭', 73 | bass: '低音', 74 | midrange: '中音', 75 | treble: '高音', 76 | presets: { 77 | flat: '平坦', 78 | pop: '流行', 79 | rock: '摇滚', 80 | classical: '古典', 81 | jazz: '爵士', 82 | electronic: '电子', 83 | hiphop: '嘻哈', 84 | rb: 'R&B', 85 | metal: '金属', 86 | vocal: '人声', 87 | dance: '舞曲', 88 | acoustic: '原声', 89 | custom: '自定义' 90 | } 91 | }, 92 | // 定时关闭功能相关 93 | sleepTimer: { 94 | title: '定时关闭', 95 | cancel: '取消定时', 96 | timeMode: '按时间关闭', 97 | songsMode: '按歌曲数关闭', 98 | playlistEnd: '播放完列表后关闭', 99 | afterPlaylist: '播放完列表后关闭', 100 | activeUntilEnd: '播放至列表结束', 101 | minutes: '分钟', 102 | hours: '小时', 103 | songs: '首歌', 104 | set: '设置', 105 | timerSetSuccess: '已设置{minutes}分钟后关闭', 106 | songsSetSuccess: '已设置播放{songs}首歌后关闭', 107 | playlistEndSetSuccess: '已设置播放完列表后关闭', 108 | timerCancelled: '已取消定时关闭', 109 | timerEnded: '定时关闭已触发', 110 | playbackStopped: '音乐播放已停止', 111 | minutesRemaining: '剩余{minutes}分钟', 112 | songsRemaining: '剩余{count}首歌' 113 | }, 114 | playList: { 115 | clearAll: '清空播放列表', 116 | alreadyEmpty: '播放列表已经为空', 117 | cleared: '已清空播放列表', 118 | empty: '播放列表为空', 119 | clearConfirmTitle: '清空播放列表', 120 | clearConfirmContent: '这将清空所有播放列表中的歌曲并停止当前播放。是否继续?' 121 | } 122 | }; 123 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/search.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: { 3 | hotSearch: '热搜列表', 4 | searchList: '搜索列表', 5 | searchHistory: '搜索历史' 6 | }, 7 | button: { 8 | clear: '清空', 9 | back: '返回', 10 | playAll: '播放列表' 11 | }, 12 | loading: { 13 | more: '加载中...', 14 | failed: '搜索失败' 15 | }, 16 | noMore: '没有更多了', 17 | error: { 18 | searchFailed: '搜索失败' 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/songItem.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | menu: { 3 | play: '播放', 4 | playNext: '下一首播放', 5 | download: '下载歌曲', 6 | addToPlaylist: '添加到歌单', 7 | favorite: '喜欢', 8 | unfavorite: '取消喜欢', 9 | removeFromPlaylist: '从歌单中删除', 10 | dislike: '不喜欢', 11 | undislike: '取消不喜欢', 12 | }, 13 | message: { 14 | downloading: '正在下载中,请稍候...', 15 | downloadFailed: '下载失败', 16 | downloadQueued: '已加入下载队列', 17 | addedToNextPlay: '已添加到下一首播放', 18 | getUrlFailed: '获取音乐下载地址失败,请检查是否登录' 19 | }, 20 | dialog: { 21 | dislike: { 22 | title: '提示!', 23 | content: '确认不喜欢这首歌吗?再次进入将从每日推荐中排除。', 24 | positiveText: '不喜欢', 25 | negativeText: '取消' 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/i18n/lang/zh-CN/user.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | profile: { 3 | followers: '粉丝', 4 | following: '关注', 5 | level: '等级' 6 | }, 7 | playlist: { 8 | created: '创建的歌单', 9 | mine: '我创建的', 10 | trackCount: '{count}首', 11 | playCount: '播放{count}次' 12 | }, 13 | ranking: { 14 | title: '听歌排行', 15 | playCount: '{count}次' 16 | }, 17 | follow: { 18 | title: '关注列表', 19 | viewPlaylist: '查看歌单', 20 | noFollowings: '暂无关注', 21 | loadMore: '加载更多', 22 | noSignature: '这个家伙很懒,什么都没留下', 23 | userFollowsTitle: '的关注', 24 | myFollowsTitle: '我的关注' 25 | }, 26 | follower: { 27 | title: '粉丝列表', 28 | noFollowers: '暂无粉丝', 29 | loadMore: '加载更多', 30 | userFollowersTitle: '的粉丝', 31 | myFollowersTitle: '我的粉丝' 32 | }, 33 | detail: { 34 | playlists: '歌单', 35 | records: '听歌排行', 36 | noPlaylists: '暂无歌单', 37 | noRecords: '暂无听歌记录', 38 | artist: '歌手', 39 | noSignature: '这个人很懒,什么都没留下', 40 | invalidUserId: '用户ID无效', 41 | noRecordPermission: '{name}不让你看听歌排行' 42 | }, 43 | message: { 44 | loadFailed: '加载用户页面失败', 45 | deleteSuccess: '删除成功', 46 | deleteFailed: '删除失败' 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /src/i18n/main.ts: -------------------------------------------------------------------------------- 1 | import enUS from './lang/en-US'; 2 | import zhCN from './lang/zh-CN'; 3 | 4 | const messages = { 5 | 'zh-CN': zhCN, 6 | 'en-US': enUS 7 | } as const; 8 | 9 | type Language = keyof typeof messages; 10 | 11 | // 为主进程提供一个简单的 i18n 实现 12 | const mainI18n = { 13 | global: { 14 | currentLocale: 'zh-CN' as Language, 15 | get locale() { 16 | return this.currentLocale; 17 | }, 18 | set locale(value: Language) { 19 | this.currentLocale = value; 20 | }, 21 | t(key: string) { 22 | const keys = key.split('.'); 23 | let current: any = messages[this.currentLocale]; 24 | for (const k of keys) { 25 | if (current[k] === undefined) { 26 | // 如果找不到翻译,返回键名 27 | return key; 28 | } 29 | current = current[k]; 30 | } 31 | return current; 32 | }, 33 | messages 34 | } 35 | }; 36 | 37 | export type { Language }; 38 | export default mainI18n; 39 | -------------------------------------------------------------------------------- /src/i18n/renderer.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n'; 2 | 3 | import enUS from './lang/en-US'; 4 | import zhCN from './lang/zh-CN'; 5 | 6 | const messages = { 7 | 'zh-CN': zhCN, 8 | 'en-US': enUS 9 | }; 10 | 11 | const i18n = createI18n({ 12 | legacy: false, 13 | locale: 'zh-CN', 14 | fallbackLocale: 'en-US', 15 | messages, 16 | globalInjection: true, 17 | silentTranslationWarn: true, 18 | silentFallbackWarn: true 19 | }); 20 | 21 | export default i18n; 22 | -------------------------------------------------------------------------------- /src/main/modules/cache.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import Store from 'electron-store'; 3 | 4 | interface LyricData { 5 | id: number; 6 | data: any; 7 | timestamp: number; 8 | } 9 | 10 | interface StoreSchema { 11 | lyrics: Record; 12 | } 13 | 14 | class CacheManager { 15 | private store: Store; 16 | 17 | constructor() { 18 | this.store = new Store({ 19 | name: 'lyrics', 20 | defaults: { 21 | lyrics: {} 22 | } 23 | }); 24 | } 25 | 26 | async cacheLyric(id: number, data: any) { 27 | try { 28 | const lyrics = this.store.get('lyrics'); 29 | lyrics[id] = { 30 | id, 31 | data, 32 | timestamp: Date.now() 33 | }; 34 | this.store.set('lyrics', lyrics); 35 | return true; 36 | } catch (error) { 37 | console.error('Error caching lyric:', error); 38 | return false; 39 | } 40 | } 41 | 42 | async getCachedLyric(id: number) { 43 | try { 44 | const lyrics = this.store.get('lyrics'); 45 | const result = lyrics[id]; 46 | 47 | if (!result) return undefined; 48 | 49 | // 检查缓存是否过期(24小时) 50 | if (Date.now() - result.timestamp > 24 * 60 * 60 * 1000) { 51 | delete lyrics[id]; 52 | this.store.set('lyrics', lyrics); 53 | return undefined; 54 | } 55 | 56 | return result.data; 57 | } catch (error) { 58 | console.error('Error getting cached lyric:', error); 59 | return undefined; 60 | } 61 | } 62 | 63 | async clearLyricCache() { 64 | try { 65 | this.store.set('lyrics', {}); 66 | return true; 67 | } catch (error) { 68 | console.error('Error clearing lyric cache:', error); 69 | return false; 70 | } 71 | } 72 | } 73 | 74 | export const cacheManager = new CacheManager(); 75 | 76 | export function initializeCacheManager() { 77 | // 添加歌词缓存相关的 IPC 处理 78 | ipcMain.handle('cache-lyric', async (_, id: number, lyricData: any) => { 79 | return await cacheManager.cacheLyric(id, lyricData); 80 | }); 81 | 82 | ipcMain.handle('get-cached-lyric', async (_, id: number) => { 83 | return await cacheManager.getCachedLyric(id); 84 | }); 85 | 86 | ipcMain.handle('clear-lyric-cache', async () => { 87 | return await cacheManager.clearLyricCache(); 88 | }); 89 | } 90 | -------------------------------------------------------------------------------- /src/main/modules/config.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from 'electron'; 2 | import Store from 'electron-store'; 3 | 4 | import set from '../set.json'; 5 | import { defaultShortcuts } from './shortcuts'; 6 | 7 | type SetConfig = { 8 | isProxy: boolean; 9 | proxyConfig: { 10 | enable: boolean; 11 | protocol: string; 12 | host: string; 13 | port: number; 14 | }; 15 | enableRealIP: boolean; 16 | realIP: string; 17 | noAnimate: boolean; 18 | animationSpeed: number; 19 | author: string; 20 | authorUrl: string; 21 | musicApiPort: number; 22 | closeAction: 'ask' | 'minimize' | 'close'; 23 | musicQuality: string; 24 | fontFamily: string; 25 | fontScope: 'global' | 'lyric'; 26 | language: string; 27 | showTopAction: boolean; 28 | }; 29 | interface StoreType { 30 | set: SetConfig; 31 | shortcuts: typeof defaultShortcuts; 32 | } 33 | 34 | let store: Store; 35 | 36 | /** 37 | * 初始化配置管理 38 | */ 39 | export function initializeConfig() { 40 | store = new Store({ 41 | name: 'config', 42 | defaults: { 43 | set: set as SetConfig, 44 | shortcuts: defaultShortcuts 45 | } 46 | }); 47 | 48 | store.get('set.downloadPath') || store.set('set.downloadPath', app.getPath('downloads')); 49 | 50 | // 定义ipcRenderer监听事件 51 | ipcMain.on('set-store-value', (_, key, value) => { 52 | store.set(key, value); 53 | }); 54 | 55 | ipcMain.on('get-store-value', (_, key) => { 56 | const value = store.get(key); 57 | _.returnValue = value || ''; 58 | }); 59 | 60 | return store; 61 | } 62 | 63 | export function getStore() { 64 | return store; 65 | } 66 | -------------------------------------------------------------------------------- /src/main/modules/deviceInfo.ts: -------------------------------------------------------------------------------- 1 | import { app } from 'electron'; 2 | import Store from 'electron-store'; 3 | import { machineIdSync } from 'node-machine-id'; 4 | import os from 'os'; 5 | 6 | const store = new Store(); 7 | 8 | /** 9 | * 获取设备唯一标识符 10 | * 优先使用存储的ID,如果没有则获取机器ID并存储 11 | */ 12 | export function getDeviceId(): string { 13 | let deviceId = store.get('deviceId') as string | undefined; 14 | 15 | if (!deviceId) { 16 | try { 17 | // 使用node-machine-id获取设备唯一标识 18 | deviceId = machineIdSync(true); 19 | } catch (error) { 20 | console.error('获取机器ID失败:', error); 21 | // 如果获取失败,使用主机名和MAC地址组合作为备选方案 22 | const networkInterfaces = os.networkInterfaces(); 23 | let macAddress = ''; 24 | 25 | // 尝试获取第一个非内部网络接口的MAC地址 26 | Object.values(networkInterfaces).forEach((interfaces) => { 27 | if (interfaces) { 28 | interfaces.forEach((iface) => { 29 | if (!iface.internal && !macAddress && iface.mac !== '00:00:00:00:00:00') { 30 | macAddress = iface.mac; 31 | } 32 | }); 33 | } 34 | }); 35 | 36 | deviceId = `${os.hostname()}-${macAddress}`.replace(/:/g, ''); 37 | } 38 | 39 | // 存储设备ID 40 | if (deviceId) { 41 | store.set('deviceId', deviceId); 42 | } else { 43 | // 如果所有方法都失败,使用随机ID 44 | deviceId = Math.random().toString(36).substring(2, 15); 45 | store.set('deviceId', deviceId); 46 | } 47 | } 48 | 49 | return deviceId; 50 | } 51 | 52 | /** 53 | * 获取系统信息 54 | */ 55 | export function getSystemInfo() { 56 | return { 57 | osType: os.type(), 58 | osVersion: os.release(), 59 | osArch: os.arch(), 60 | platform: process.platform, 61 | appVersion: app.getVersion() 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/main/modules/fonts.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import { getFonts } from 'font-list'; 3 | 4 | /** 5 | * 清理字体名称 6 | * @param fontName 原始字体名称 7 | * @returns 清理后的字体名称 8 | */ 9 | function cleanFontName(fontName: string): string { 10 | return fontName 11 | .trim() 12 | .replace(/^["']|["']$/g, '') // 移除首尾的引号 13 | .replace(/\s+/g, ' '); // 将多个空格替换为单个空格 14 | } 15 | 16 | /** 17 | * 获取系统字体列表 18 | */ 19 | async function getSystemFonts(): Promise { 20 | try { 21 | // 使用 font-list 获取系统字体 22 | const fonts = await getFonts(); 23 | // 清理字体名称并去重 24 | const cleanedFonts = [...new Set(fonts.map(cleanFontName))]; 25 | // 添加系统默认字体并排序 26 | return ['system-ui', ...cleanedFonts].sort(); 27 | } catch (error) { 28 | console.error('获取系统字体失败:', error); 29 | // 如果获取失败,至少返回系统默认字体 30 | return ['system-ui']; 31 | } 32 | } 33 | 34 | /** 35 | * 初始化字体管理模块 36 | */ 37 | export function initializeFonts() { 38 | // 添加获取系统字体的 IPC 处理 39 | ipcMain.handle('get-system-fonts', async () => { 40 | return await getSystemFonts(); 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/modules/shortcuts.ts: -------------------------------------------------------------------------------- 1 | import { globalShortcut, ipcMain } from 'electron'; 2 | 3 | import { getStore } from './config'; 4 | 5 | // 添加获取平台信息的 IPC 处理程序 6 | ipcMain.on('get-platform', (event) => { 7 | event.returnValue = process.platform; 8 | }); 9 | 10 | // 定义快捷键配置接口 11 | export interface ShortcutConfig { 12 | key: string; 13 | enabled: boolean; 14 | scope: 'global' | 'app'; 15 | } 16 | 17 | export interface ShortcutsConfig { 18 | [key: string]: ShortcutConfig; 19 | } 20 | 21 | // 定义默认快捷键 22 | export const defaultShortcuts: ShortcutsConfig = { 23 | togglePlay: { key: 'CommandOrControl+Alt+P', enabled: true, scope: 'global' }, 24 | prevPlay: { key: 'Alt+Left', enabled: true, scope: 'global' }, 25 | nextPlay: { key: 'Alt+Right', enabled: true, scope: 'global' }, 26 | volumeUp: { key: 'Alt+Up', enabled: true, scope: 'app' }, 27 | volumeDown: { key: 'Alt+Down', enabled: true, scope: 'app' }, 28 | toggleFavorite: { key: 'CommandOrControl+Alt+L', enabled: true, scope: 'app' }, 29 | toggleWindow: { key: 'CommandOrControl+Alt+Shift+M', enabled: true, scope: 'global' } 30 | }; 31 | 32 | let mainWindowRef: Electron.BrowserWindow | null = null; 33 | 34 | // 注册快捷键 35 | export function registerShortcuts( 36 | mainWindow: Electron.BrowserWindow, 37 | shortcutsConfig?: ShortcutsConfig 38 | ) { 39 | mainWindowRef = mainWindow; 40 | const store = getStore(); 41 | const shortcuts = 42 | shortcutsConfig || (store.get('shortcuts') as ShortcutsConfig) || defaultShortcuts; 43 | 44 | // 注销所有已注册的快捷键 45 | globalShortcut.unregisterAll(); 46 | 47 | // 对旧格式数据进行兼容处理 48 | if (shortcuts && typeof shortcuts.togglePlay === 'string') { 49 | // 将 shortcuts 强制转换为 unknown,再转为 Record 50 | const oldShortcuts = { ...shortcuts } as unknown as Record; 51 | const newShortcuts: ShortcutsConfig = {}; 52 | 53 | Object.entries(oldShortcuts).forEach(([key, value]) => { 54 | newShortcuts[key] = { 55 | key: value, 56 | enabled: true, 57 | scope: ['volumeUp', 'volumeDown', 'toggleFavorite'].includes(key) ? 'app' : 'global' 58 | }; 59 | }); 60 | 61 | store.set('shortcuts', newShortcuts); 62 | registerShortcuts(mainWindow, newShortcuts); 63 | return; 64 | } 65 | 66 | // 注册全局快捷键 67 | Object.entries(shortcuts).forEach(([action, config]) => { 68 | const { key, enabled, scope } = config as ShortcutConfig; 69 | 70 | // 只注册启用且作用域为全局的快捷键 71 | if (!enabled || scope !== 'global') return; 72 | 73 | try { 74 | switch (action) { 75 | case 'toggleWindow': 76 | globalShortcut.register(key, () => { 77 | if (mainWindow.isVisible()) { 78 | mainWindow.hide(); 79 | } else { 80 | mainWindow.show(); 81 | } 82 | }); 83 | break; 84 | default: 85 | globalShortcut.register(key, () => { 86 | mainWindow.webContents.send('global-shortcut', action); 87 | }); 88 | break; 89 | } 90 | } catch (error) { 91 | console.error(`注册快捷键 ${key} 失败:`, error); 92 | } 93 | }); 94 | 95 | // 通知渲染进程更新应用内快捷键 96 | mainWindow.webContents.send('update-app-shortcuts', shortcuts); 97 | } 98 | 99 | // 初始化快捷键 100 | export function initializeShortcuts(mainWindow: Electron.BrowserWindow) { 101 | mainWindowRef = mainWindow; 102 | registerShortcuts(mainWindow); 103 | 104 | // 监听禁用快捷键事件 105 | ipcMain.on('disable-shortcuts', () => { 106 | globalShortcut.unregisterAll(); 107 | }); 108 | 109 | // 监听启用快捷键事件 110 | ipcMain.on('enable-shortcuts', () => { 111 | if (mainWindowRef) { 112 | registerShortcuts(mainWindowRef); 113 | } 114 | }); 115 | 116 | // 监听快捷键更新事件 117 | ipcMain.on('update-shortcuts', (_, shortcutsConfig: ShortcutsConfig) => { 118 | if (mainWindowRef) { 119 | registerShortcuts(mainWindowRef, shortcutsConfig); 120 | } 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /src/main/modules/statsService.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { app } from 'electron'; 3 | import Store from 'electron-store'; 4 | 5 | import { getDeviceId, getSystemInfo } from './deviceInfo'; 6 | 7 | const store = new Store(); 8 | 9 | // 统计服务配置 10 | const STATS_API_URL = 'http://donate.alger.fun/state/api/stats'; 11 | 12 | /** 13 | * 记录应用安装/启动 14 | */ 15 | export async function recordInstallation(): Promise { 16 | try { 17 | const deviceId = getDeviceId(); 18 | const systemInfo = getSystemInfo(); 19 | 20 | // 发送请求到统计服务器 21 | await axios.post(`${STATS_API_URL}/installation`, { 22 | deviceId, 23 | osType: systemInfo.osType, 24 | osVersion: systemInfo.osVersion, 25 | appVersion: systemInfo.appVersion 26 | }); 27 | 28 | console.log('应用启动统计已记录'); 29 | 30 | // 记录最后一次启动时间 31 | store.set('lastStartTime', new Date().toISOString()); 32 | } catch (error) { 33 | console.error('记录应用启动统计失败:', error); 34 | } 35 | } 36 | 37 | /** 38 | * 设置 IPC 处理程序以接收渲染进程的统计请求 39 | * @param ipcMain Electron IPC主对象 40 | */ 41 | export function setupStatsHandlers(ipcMain: Electron.IpcMain): void { 42 | // 处理页面访问统计 43 | ipcMain.handle('record-visit', async (_, page: string, userId?: string) => { 44 | try { 45 | const deviceId = getDeviceId(); 46 | 47 | await axios.post(`${STATS_API_URL}/visit`, { 48 | deviceId, 49 | userId, 50 | page 51 | }); 52 | 53 | return { success: true }; 54 | } catch (error) { 55 | console.error('记录页面访问统计失败:', error); 56 | return { success: false, error: (error as Error).message }; 57 | } 58 | }); 59 | 60 | // 处理播放统计 61 | ipcMain.handle( 62 | 'record-play', 63 | async ( 64 | _, 65 | songData: { 66 | userId: string | null; 67 | songId: string | number; 68 | songName: string; 69 | artistName: string; 70 | duration?: number; 71 | completedPlay?: boolean; 72 | } 73 | ) => { 74 | try { 75 | const { songId, songName, artistName, duration = 0, completedPlay = false } = songData; 76 | const deviceId = getDeviceId(); 77 | 78 | await axios.post(`${STATS_API_URL}/play`, { 79 | deviceId, 80 | userId: songData.userId, 81 | songId: songId.toString(), 82 | songName, 83 | artistName, 84 | duration, 85 | completedPlay 86 | }); 87 | 88 | return { success: true }; 89 | } catch (error) { 90 | console.error('记录播放统计失败:', error); 91 | return { success: false, error: (error as Error).message }; 92 | } 93 | } 94 | ); 95 | 96 | // 处理获取统计摘要 97 | ipcMain.handle('get-stats-summary', async () => { 98 | try { 99 | const response = await axios.get(`${STATS_API_URL}/summary`); 100 | return response.data; 101 | } catch (error) { 102 | console.error('获取统计摘要失败:', error); 103 | throw error; 104 | } 105 | }); 106 | } 107 | 108 | /** 109 | * 应用启动时初始化统计服务 110 | */ 111 | export function initializeStats(): void { 112 | // 记录应用启动统计 113 | recordInstallation().catch((error) => { 114 | console.error('初始化统计服务失败:', error); 115 | }); 116 | 117 | // 注册应用退出时的回调 118 | app.on('will-quit', () => { 119 | // 可以在这里添加应用退出时的统计逻辑 120 | console.log('应用退出'); 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /src/main/modules/update.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { spawn } from 'child_process'; 3 | import { app, BrowserWindow, ipcMain } from 'electron'; 4 | import * as fs from 'fs'; 5 | import * as path from 'path'; 6 | 7 | export function setupUpdateHandlers(_mainWindow: BrowserWindow) { 8 | ipcMain.on('start-download', async (event, url: string) => { 9 | try { 10 | const response = await axios({ 11 | url, 12 | method: 'GET', 13 | responseType: 'stream', 14 | onDownloadProgress: (progressEvent: { loaded: number; total?: number }) => { 15 | if (!progressEvent.total) return; 16 | const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100); 17 | const downloaded = (progressEvent.loaded / 1024 / 1024).toFixed(2); 18 | const total = (progressEvent.total / 1024 / 1024).toFixed(2); 19 | event.sender.send('download-progress', percent, `已下载 ${downloaded}MB / ${total}MB`); 20 | } 21 | }); 22 | 23 | const fileName = url.split('/').pop() || 'update.exe'; 24 | const downloadPath = path.join(app.getPath('downloads'), fileName); 25 | 26 | // 创建写入流 27 | const writer = fs.createWriteStream(downloadPath); 28 | 29 | // 将响应流写入文件 30 | response.data.pipe(writer); 31 | 32 | // 处理写入完成 33 | writer.on('finish', () => { 34 | event.sender.send('download-complete', true, downloadPath); 35 | }); 36 | 37 | // 处理写入错误 38 | writer.on('error', (error) => { 39 | console.error('Write file error:', error); 40 | event.sender.send('download-complete', false, ''); 41 | }); 42 | } catch (error) { 43 | console.error('Download failed:', error); 44 | event.sender.send('download-complete', false, ''); 45 | } 46 | }); 47 | 48 | ipcMain.on('install-update', (_event, filePath: string) => { 49 | if (!fs.existsSync(filePath)) { 50 | console.error('Installation file not found:', filePath); 51 | return; 52 | } 53 | 54 | const { platform } = process; 55 | 56 | // 先启动安装程序,再退出应用 57 | try { 58 | if (platform === 'win32') { 59 | // 使用spawn替代exec,并使用detached选项确保子进程独立运行 60 | const child = spawn(filePath, [], { 61 | detached: true, 62 | stdio: 'ignore' 63 | }); 64 | child.unref(); 65 | } else if (platform === 'darwin') { 66 | // 挂载 DMG 文件 67 | const child = spawn('open', [filePath], { 68 | detached: true, 69 | stdio: 'ignore' 70 | }); 71 | child.unref(); 72 | } else if (platform === 'linux') { 73 | const ext = path.extname(filePath); 74 | if (ext === '.AppImage') { 75 | // 先添加执行权限 76 | fs.chmodSync(filePath, '755'); 77 | const child = spawn(filePath, [], { 78 | detached: true, 79 | stdio: 'ignore' 80 | }); 81 | child.unref(); 82 | } else if (ext === '.deb') { 83 | const child = spawn('pkexec', ['dpkg', '-i', filePath], { 84 | detached: true, 85 | stdio: 'ignore' 86 | }); 87 | child.unref(); 88 | } 89 | } 90 | 91 | // 给安装程序一点时间启动 92 | setTimeout(() => { 93 | app.quit(); 94 | }, 500); 95 | } catch (error) { 96 | console.error('启动安装程序失败:', error); 97 | // 尽管出错,仍然尝试退出应用 98 | app.quit(); 99 | } 100 | }); 101 | } 102 | -------------------------------------------------------------------------------- /src/main/server.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron'; 2 | import Store from 'electron-store'; 3 | import fs from 'fs'; 4 | import server from 'netease-cloud-music-api-alger/server'; 5 | import os from 'os'; 6 | import path from 'path'; 7 | 8 | import { unblockMusic, type Platform } from './unblockMusic'; 9 | 10 | const store = new Store(); 11 | if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) { 12 | fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8'); 13 | } 14 | 15 | // 设置音乐解析的处理程序 16 | ipcMain.handle('unblock-music', async (_event, id, songData, enabledSources) => { 17 | try { 18 | const result = await unblockMusic(id, songData, 1, enabledSources as Platform[]); 19 | return result; 20 | } catch (error) { 21 | console.error('音乐解析失败:', error); 22 | return { error: (error as Error).message || '未知错误' }; 23 | } 24 | }); 25 | 26 | async function startMusicApi(): Promise { 27 | console.log('MUSIC API STARTED'); 28 | 29 | const port = (store.get('set') as any).musicApiPort || 30488; 30 | 31 | await server.serveNcmApi({ 32 | port 33 | }); 34 | } 35 | 36 | export { startMusicApi }; 37 | -------------------------------------------------------------------------------- /src/main/set.json: -------------------------------------------------------------------------------- 1 | { 2 | "isProxy": false, 3 | "proxyConfig": { 4 | "enable": false, 5 | "protocol": "http", 6 | "host": "127.0.0.1", 7 | "port": 7890 8 | }, 9 | "enableRealIP": false, 10 | "realIP": "", 11 | "noAnimate": false, 12 | "animationSpeed": 1, 13 | "author": "Alger", 14 | "authorUrl": "https://github.com/algerkong", 15 | "musicApiPort": 30488, 16 | "closeAction": "ask", 17 | "musicQuality": "higher", 18 | "fontFamily": "system-ui", 19 | "fontScope": "global", 20 | "autoPlay": false, 21 | "downloadPath": "", 22 | "language": "zh-CN", 23 | "alwaysShowDownloadButton": false, 24 | "unlimitedDownload": false, 25 | "enableMusicUnblock": true, 26 | "enabledMusicSources": ["migu", "kugou", "pyncmd", "bilibili", "kuwo"], 27 | "showTopAction": false, 28 | "contentZoomFactor": 1 29 | } 30 | -------------------------------------------------------------------------------- /src/main/unblockMusic.ts: -------------------------------------------------------------------------------- 1 | import match from '@unblockneteasemusic/server'; 2 | 3 | type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili'; 4 | 5 | interface SongData { 6 | name: string; 7 | artists: Array<{ name: string }>; 8 | album?: { name: string }; 9 | ar?: Array<{ name: string }>; 10 | al?: { name: string }; 11 | } 12 | 13 | interface ResponseData { 14 | url: string; 15 | br: number; 16 | size: number; 17 | md5?: string; 18 | platform?: Platform; 19 | gain?: number; 20 | } 21 | 22 | interface UnblockResult { 23 | data: { 24 | data: ResponseData; 25 | params: { 26 | id: number; 27 | type: 'song'; 28 | }; 29 | }; 30 | } 31 | 32 | // 所有可用平台 33 | export const ALL_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'kuwo', 'bilibili']; 34 | 35 | /** 36 | * 音乐解析函数 37 | * @param id 歌曲ID 38 | * @param songData 歌曲信息 39 | * @param retryCount 重试次数 40 | * @param enabledPlatforms 启用的平台列表,默认为所有平台 41 | * @returns Promise 42 | */ 43 | const unblockMusic = async ( 44 | id: number | string, 45 | songData: SongData, 46 | retryCount = 1, 47 | enabledPlatforms?: Platform[] 48 | ): Promise => { 49 | // 过滤 enabledPlatforms,确保只包含 ALL_PLATFORMS 中存在的平台 50 | const filteredPlatforms = enabledPlatforms 51 | ? enabledPlatforms.filter(platform => ALL_PLATFORMS.includes(platform)) 52 | : ALL_PLATFORMS; 53 | 54 | songData.album = songData.album || songData.al; 55 | songData.artists = songData.artists || songData.ar; 56 | const retry = async (attempt: number): Promise => { 57 | try { 58 | const data = await match(parseInt(String(id), 10), filteredPlatforms, songData); 59 | const result: UnblockResult = { 60 | data: { 61 | data, 62 | params: { 63 | id: parseInt(String(id), 10), 64 | type: 'song' 65 | } 66 | } 67 | }; 68 | return result; 69 | } catch (err) { 70 | if (attempt < retryCount) { 71 | // 延迟重试,每次重试增加延迟时间 72 | await new Promise((resolve) => setTimeout(resolve, 100 * attempt)); 73 | return retry(attempt + 1); 74 | } 75 | 76 | // 所有重试都失败后,抛出详细错误 77 | throw new Error( 78 | `音乐解析失败 (ID: ${id}): ${err instanceof Error ? err.message : '未知错误'}` 79 | ); 80 | } 81 | }; 82 | 83 | return retry(1); 84 | }; 85 | 86 | export { type Platform, type ResponseData, type SongData, unblockMusic, type UnblockResult }; 87 | -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronAPI } from '@electron-toolkit/preload'; 2 | 3 | interface API { 4 | minimize: () => void; 5 | maximize: () => void; 6 | close: () => void; 7 | dragStart: (data: any) => void; 8 | miniTray: () => void; 9 | miniWindow: () => void; 10 | restore: () => void; 11 | restart: () => void; 12 | resizeWindow: (width: number, height: number) => void; 13 | resizeMiniWindow: (showPlaylist: boolean) => void; 14 | openLyric: () => void; 15 | sendLyric: (data: any) => void; 16 | sendSong: (data: any) => void; 17 | unblockMusic: (id: number, data: any, enabledSources?: string[]) => Promise; 18 | onLyricWindowClosed: (callback: () => void) => void; 19 | startDownload: (url: string) => void; 20 | onDownloadProgress: (callback: (progress: number, status: string) => void) => void; 21 | onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => void; 22 | onLanguageChanged: (callback: (locale: string) => void) => void; 23 | removeDownloadListeners: () => void; 24 | invoke: (channel: string, ...args: any[]) => Promise; 25 | } 26 | 27 | // 自定义IPC渲染进程通信接口 28 | interface IpcRenderer { 29 | send: (channel: string, ...args: any[]) => void; 30 | invoke: (channel: string, ...args: any[]) => Promise; 31 | on: (channel: string, listener: (...args: any[]) => void) => () => void; 32 | removeAllListeners: (channel: string) => void; 33 | } 34 | 35 | declare global { 36 | interface Window { 37 | electron: ElectronAPI; 38 | api: API; 39 | ipcRenderer: IpcRenderer; 40 | $message: any; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { electronAPI } from '@electron-toolkit/preload'; 2 | import { contextBridge, ipcRenderer } from 'electron'; 3 | 4 | // Custom APIs for renderer 5 | const api = { 6 | minimize: () => ipcRenderer.send('minimize-window'), 7 | maximize: () => ipcRenderer.send('maximize-window'), 8 | close: () => ipcRenderer.send('close-window'), 9 | dragStart: (data) => ipcRenderer.send('drag-start', data), 10 | miniTray: () => ipcRenderer.send('mini-tray'), 11 | miniWindow: () => ipcRenderer.send('mini-window'), 12 | restore: () => ipcRenderer.send('restore-window'), 13 | restart: () => ipcRenderer.send('restart'), 14 | resizeWindow: (width, height) => ipcRenderer.send('resize-window', width, height), 15 | resizeMiniWindow: (showPlaylist) => ipcRenderer.send('resize-mini-window', showPlaylist), 16 | openLyric: () => ipcRenderer.send('open-lyric'), 17 | sendLyric: (data) => ipcRenderer.send('send-lyric', data), 18 | sendSong: (data) => ipcRenderer.send('update-current-song', data), 19 | unblockMusic: (id, data, enabledSources) => ipcRenderer.invoke('unblock-music', id, data, enabledSources), 20 | // 歌词窗口关闭事件 21 | onLyricWindowClosed: (callback: () => void) => { 22 | ipcRenderer.on('lyric-window-closed', () => callback()); 23 | }, 24 | // 更新相关 25 | startDownload: (url: string) => ipcRenderer.send('start-download', url), 26 | onDownloadProgress: (callback: (progress: number, status: string) => void) => { 27 | ipcRenderer.on('download-progress', (_event, progress, status) => callback(progress, status)); 28 | }, 29 | onDownloadComplete: (callback: (success: boolean, filePath: string) => void) => { 30 | ipcRenderer.on('download-complete', (_event, success, filePath) => callback(success, filePath)); 31 | }, 32 | // 语言相关 33 | onLanguageChanged: (callback: (locale: string) => void) => { 34 | ipcRenderer.on('language-changed', (_event, locale) => { 35 | callback(locale); 36 | }); 37 | }, 38 | removeDownloadListeners: () => { 39 | ipcRenderer.removeAllListeners('download-progress'); 40 | ipcRenderer.removeAllListeners('download-complete'); 41 | }, 42 | // 歌词缓存相关 43 | invoke: (channel: string, ...args: any[]) => { 44 | const validChannels = [ 45 | 'get-lyrics', 46 | 'clear-lyrics-cache', 47 | 'get-system-fonts', 48 | 'get-cached-lyric', 49 | 'cache-lyric', 50 | 'clear-lyric-cache', 51 | // 统计相关 52 | 'record-visit', 53 | 'record-play', 54 | 'get-stats-summary' 55 | ]; 56 | if (validChannels.includes(channel)) { 57 | return ipcRenderer.invoke(channel, ...args); 58 | } 59 | return Promise.reject(new Error(`未授权的 IPC 通道: ${channel}`)); 60 | } 61 | }; 62 | 63 | // 创建带类型的ipcRenderer对象,暴露给渲染进程 64 | const ipc = { 65 | // 发送消息到主进程(无返回值) 66 | send: (channel: string, ...args: any[]) => { 67 | ipcRenderer.send(channel, ...args); 68 | }, 69 | // 调用主进程方法(有返回值) 70 | invoke: (channel: string, ...args: any[]) => { 71 | return ipcRenderer.invoke(channel, ...args); 72 | }, 73 | // 监听主进程消息 74 | on: (channel: string, listener: (...args: any[]) => void) => { 75 | ipcRenderer.on(channel, (_, ...args) => listener(...args)); 76 | return () => { 77 | ipcRenderer.removeListener(channel, listener); 78 | }; 79 | }, 80 | // 移除所有监听器 81 | removeAllListeners: (channel: string) => { 82 | ipcRenderer.removeAllListeners(channel); 83 | } 84 | }; 85 | 86 | // Use `contextBridge` APIs to expose Electron APIs to 87 | // renderer only if context isolation is enabled, otherwise 88 | // just add to the DOM global. 89 | if (process.contextIsolated) { 90 | try { 91 | contextBridge.exposeInMainWorld('electron', electronAPI); 92 | contextBridge.exposeInMainWorld('api', api); 93 | contextBridge.exposeInMainWorld('ipcRenderer', ipc); 94 | } catch (error) { 95 | console.error(error); 96 | } 97 | } else { 98 | // @ts-ignore (define in dts) 99 | window.electron = electronAPI; 100 | // @ts-ignore (define in dts) 101 | window.api = api; 102 | // @ts-ignore (define in dts) 103 | window.ipcRenderer = ipc; 104 | } 105 | -------------------------------------------------------------------------------- /src/renderer/api/artist.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | // 获取歌手详情 4 | export const getArtistDetail = (id) => { 5 | return request.get('/artist/detail', { params: { id } }); 6 | }; 7 | 8 | // 获取歌手热门歌曲 9 | export const getArtistTopSongs = (params) => { 10 | return request.get('/artist/songs', { 11 | params: { 12 | ...params, 13 | order: 'hot' 14 | } 15 | }); 16 | }; 17 | 18 | // 获取歌手专辑 19 | export const getArtistAlbums = (params) => { 20 | return request.get('/artist/album', { params }); 21 | }; 22 | -------------------------------------------------------------------------------- /src/renderer/api/donation.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export interface Donor { 4 | id: number; 5 | name: string; 6 | amount: number; 7 | date: string; 8 | message?: string; 9 | avatar?: string; 10 | badge: string; 11 | badgeColor: string; 12 | } 13 | 14 | /** 15 | * 获取捐赠列表 16 | */ 17 | export const getDonationList = async (): Promise => { 18 | const { data } = await axios.get('http://donate.alger.fun/api/donations'); 19 | return data; 20 | }; 21 | -------------------------------------------------------------------------------- /src/renderer/api/home.ts: -------------------------------------------------------------------------------- 1 | import { IData } from '@/type'; 2 | import { IAlbumNew } from '@/type/album'; 3 | import { IDayRecommend } from '@/type/day_recommend'; 4 | import { IRecommendMusic } from '@/type/music'; 5 | import { IPlayListSort } from '@/type/playlist'; 6 | import { IHotSearch, ISearchKeyword } from '@/type/search'; 7 | import { IHotSinger } from '@/type/singer'; 8 | import request from '@/utils/request'; 9 | 10 | interface IHotSingerParams { 11 | offset: number; 12 | limit: number; 13 | } 14 | 15 | interface IRecommendMusicParams { 16 | limit: number; 17 | } 18 | 19 | // 获取热门歌手 20 | export const getHotSinger = (params: IHotSingerParams) => { 21 | return request.get('/top/artists', { params }); 22 | }; 23 | 24 | // 获取搜索推荐词 25 | export const getSearchKeyword = () => { 26 | return request.get('/search/default'); 27 | }; 28 | 29 | // 获取热门搜索 30 | export const getHotSearch = () => { 31 | return request.get('/search/hot/detail'); 32 | }; 33 | 34 | // 获取歌单分类 35 | export const getPlaylistCategory = () => { 36 | return request.get('/playlist/catlist'); 37 | }; 38 | 39 | // 获取推荐音乐 40 | export const getRecommendMusic = (params: IRecommendMusicParams) => { 41 | return request.get('/personalized/newsong', { params }); 42 | }; 43 | 44 | // 获取每日推荐 45 | export const getDayRecommend = () => { 46 | return request.get>>('/recommend/songs'); 47 | }; 48 | 49 | // 获取最新专辑推荐 50 | export const getNewAlbum = () => { 51 | return request.get('/album/newest'); 52 | }; 53 | -------------------------------------------------------------------------------- /src/renderer/api/list.ts: -------------------------------------------------------------------------------- 1 | import { IList } from '@/type/list'; 2 | import type { IListDetail } from '@/type/listDetail'; 3 | import request from '@/utils/request'; 4 | 5 | interface IListByTagParams { 6 | tag: string; 7 | before: number; 8 | limit: number; 9 | } 10 | 11 | interface IListByCatParams { 12 | cat: string; 13 | offset: number; 14 | limit: number; 15 | } 16 | 17 | // 根据tag 获取歌单列表 18 | export function getListByTag(params: IListByTagParams) { 19 | return request.get('/top/playlist/highquality', { params }); 20 | } 21 | 22 | // 根据cat 获取歌单列表 23 | export function getListByCat(params: IListByCatParams) { 24 | return request.get('/top/playlist', { 25 | params 26 | }); 27 | } 28 | 29 | // 获取推荐歌单 30 | export function getRecommendList(limit: number = 30) { 31 | return request.get('/personalized', { params: { limit } }); 32 | } 33 | 34 | // 获取歌单详情 35 | export function getListDetail(id: number | string) { 36 | return request.get('/playlist/detail', { params: { id } }); 37 | } 38 | 39 | // 获取专辑内容 40 | export function getAlbum(id: number | string) { 41 | return request.get('/album', { params: { id } }); 42 | } 43 | 44 | // 获取排行榜列表 45 | export function getToplist() { 46 | return request.get('/toplist'); 47 | } 48 | -------------------------------------------------------------------------------- /src/renderer/api/login.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | // 创建二维码key 4 | // /login/qr/key 5 | export function getQrKey() { 6 | return request.get('/login/qr/key'); 7 | } 8 | 9 | // 创建二维码 10 | // /login/qr/create 11 | export function createQr(key: any) { 12 | return request.get('/login/qr/create', { params: { key, qrimg: true } }); 13 | } 14 | 15 | // 获取二维码状态 16 | // /login/qr/check 17 | export function checkQr(key: any) { 18 | return request.get('/login/qr/check', { params: { key, noCookie: true } }); 19 | } 20 | 21 | // 获取登录状态 22 | // /login/status 23 | export function getLoginStatus() { 24 | return request.get('/login/status'); 25 | } 26 | 27 | // 获取用户信息 28 | // /user/account 29 | export function getUserDetail() { 30 | return request.get('/user/account'); 31 | } 32 | 33 | // 退出登录 34 | // /logout 35 | export function logout() { 36 | return request.get('/logout'); 37 | } 38 | 39 | // 手机号登录 40 | // /login/cellphone 41 | export function loginByCellphone(phone: string, password: string) { 42 | return request.post('/login/cellphone', { 43 | phone, 44 | password 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/renderer/api/mv.ts: -------------------------------------------------------------------------------- 1 | import { IData } from '@/type'; 2 | import { IMvUrlData } from '@/type/mv'; 3 | import request from '@/utils/request'; 4 | 5 | interface MvParams { 6 | limit?: number; 7 | offset?: number; 8 | area?: string; 9 | } 10 | 11 | // 获取 mv 排行 12 | export const getTopMv = (params: MvParams) => { 13 | return request({ 14 | url: '/mv/all', 15 | method: 'get', 16 | params 17 | }); 18 | }; 19 | 20 | // 获取所有mv 21 | export const getAllMv = (params: MvParams) => { 22 | return request({ 23 | url: '/mv/all', 24 | method: 'get', 25 | params 26 | }); 27 | }; 28 | 29 | // 获取 mv 数据 30 | export const getMvDetail = (mvid: string) => { 31 | return request.get('/mv/detail', { 32 | params: { 33 | mvid 34 | } 35 | }); 36 | }; 37 | 38 | // 获取 mv 地址 39 | export const getMvUrl = (id: Number) => { 40 | return request.get>('/mv/url', { 41 | params: { 42 | id 43 | } 44 | }); 45 | }; 46 | -------------------------------------------------------------------------------- /src/renderer/api/search.ts: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request'; 2 | 3 | interface IParams { 4 | keywords: string; 5 | type: number; 6 | limit?: number; 7 | offset?: number; 8 | } 9 | // 搜索内容 10 | export const getSearch = (params: IParams) => { 11 | return request.get('/cloudsearch', { 12 | params 13 | }); 14 | }; 15 | -------------------------------------------------------------------------------- /src/renderer/api/stats.ts: -------------------------------------------------------------------------------- 1 | import { isElectron } from '@/utils'; 2 | 3 | import { useUserStore } from '../store/modules/user'; 4 | 5 | /** 6 | * 获取用户ID 7 | * @returns 用户ID或null 8 | */ 9 | function getUserId(): string | null { 10 | const userStore = useUserStore(); 11 | return userStore.user?.userId?.toString() || null; 12 | } 13 | 14 | /** 15 | * 记录页面访问 16 | * @param page 页面名称或路径 17 | */ 18 | export async function recordVisit(page: string): Promise { 19 | if (!isElectron) return; 20 | try { 21 | const userId = getUserId(); 22 | await window.api.invoke('record-visit', page, userId); 23 | console.log(`页面访问已记录: ${page}`); 24 | } catch (error) { 25 | console.error('记录页面访问失败:', error); 26 | } 27 | } 28 | 29 | /** 30 | * 记录歌曲播放 31 | * @param songId 歌曲ID 32 | * @param songName 歌曲名称 33 | * @param artistName 艺术家名称 34 | * @param duration 时长(秒) 35 | * @param completedPlay 是否完整播放 36 | */ 37 | export async function recordPlay( 38 | songId: string | number, 39 | songName: string, 40 | artistName: string, 41 | duration: number = 0, 42 | completedPlay: boolean = false 43 | ): Promise { 44 | if (!isElectron) return; 45 | try { 46 | const userId = getUserId(); 47 | 48 | await window.api.invoke('record-play', { 49 | userId, 50 | songId, 51 | songName, 52 | artistName, 53 | duration, 54 | completedPlay 55 | }); 56 | 57 | console.log(`歌曲播放已记录: ${songName}`); 58 | } catch (error) { 59 | console.error('记录歌曲播放失败:', error); 60 | } 61 | } 62 | 63 | /** 64 | * 获取统计摘要 65 | * @returns 统计数据摘要 66 | */ 67 | export async function getStatsSummary(): Promise { 68 | if (!isElectron) return null; 69 | try { 70 | return await window.api.invoke('get-stats-summary'); 71 | } catch (error) { 72 | console.error('获取统计摘要失败:', error); 73 | return null; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/renderer/api/user.ts: -------------------------------------------------------------------------------- 1 | import type { IUserDetail, IUserFollow } from '@/type/user'; 2 | import request from '@/utils/request'; 3 | 4 | // /user/detail 5 | export function getUserDetail(uid: number) { 6 | return request.get('/user/detail', { params: { uid } }); 7 | } 8 | 9 | // /user/playlist 10 | export function getUserPlaylist(uid: number, limit: number = 30, offset: number = 0) { 11 | return request.get('/user/playlist', { params: { uid, limit, offset } }); 12 | } 13 | 14 | // 播放历史 15 | // /user/record?uid=32953014&type=1 16 | export function getUserRecord(uid: number, type: number = 0) { 17 | 18 | return request.get('/user/record', { 19 | params: { uid, type }, 20 | noRetry: true 21 | } as any); 22 | } 23 | 24 | // 获取用户关注列表 25 | // /user/follows?uid=32953014 26 | export function getUserFollows(uid: number, limit: number = 30, offset: number = 0) { 27 | return request.get('/user/follows', { params: { uid, limit, offset } }); 28 | } 29 | 30 | // 获取用户粉丝列表 31 | export function getUserFollowers(uid: number, limit: number = 30, offset: number = 0) { 32 | return request.post('/user/followeds', { uid, limit, offset }); 33 | } 34 | 35 | // 获取用户账号信息 36 | export const getUserAccount = () => { 37 | return request({ 38 | url: '/user/account', 39 | method: 'get' 40 | }); 41 | }; 42 | 43 | // 获取用户详情 44 | export const getUserDetailInfo = (params: { uid: string | number }) => { 45 | return request({ 46 | url: '/user/detail', 47 | method: 'get', 48 | params 49 | }); 50 | }; 51 | 52 | // 获取用户关注列表 53 | export const getUserFollowsInfo = (params: { 54 | uid: string | number; 55 | limit?: number; 56 | offset?: number; 57 | }) => { 58 | return request<{ 59 | follow: IUserFollow[]; 60 | more: boolean; 61 | }>({ 62 | url: '/user/follows', 63 | method: 'get', 64 | params 65 | }); 66 | }; 67 | 68 | // 获取用户歌单 69 | export const getUserPlaylists = (params: { uid: string | number }) => { 70 | return request({ 71 | url: '/user/playlist', 72 | method: 'get', 73 | params 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /src/renderer/assets/alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/src/renderer/assets/alipay.png -------------------------------------------------------------------------------- /src/renderer/assets/css/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | /* background-color: #000; */ 3 | overflow: hidden; 4 | } 5 | 6 | .n-popover:has(.music-play) { 7 | border-radius: 1.5rem !important; 8 | } 9 | .n-popover { 10 | border-radius: 0.5rem !important; 11 | overflow: hidden !important; 12 | } 13 | .n-popover:has(.transparent-popover ) { 14 | background-color: transparent !important; 15 | padding: 0 !important; 16 | } 17 | 18 | .settings-slider .n-slider-mark { 19 | font-size: 10px !important; 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/src/renderer/assets/icon.png -------------------------------------------------------------------------------- /src/renderer/assets/icon/iconfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/src/renderer/assets/icon/iconfont.ttf -------------------------------------------------------------------------------- /src/renderer/assets/icon/iconfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/src/renderer/assets/icon/iconfont.woff -------------------------------------------------------------------------------- /src/renderer/assets/icon/iconfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/src/renderer/assets/icon/iconfont.woff2 -------------------------------------------------------------------------------- /src/renderer/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/src/renderer/assets/logo.png -------------------------------------------------------------------------------- /src/renderer/assets/wechat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/src/renderer/assets/wechat.png -------------------------------------------------------------------------------- /src/renderer/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | // biome-ignore lint: disable 7 | export {} 8 | declare global { 9 | const EffectScope: typeof import('vue')['EffectScope'] 10 | const computed: typeof import('vue')['computed'] 11 | const createApp: typeof import('vue')['createApp'] 12 | const customRef: typeof import('vue')['customRef'] 13 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 14 | const defineComponent: typeof import('vue')['defineComponent'] 15 | const effectScope: typeof import('vue')['effectScope'] 16 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 17 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 18 | const h: typeof import('vue')['h'] 19 | const inject: typeof import('vue')['inject'] 20 | const isProxy: typeof import('vue')['isProxy'] 21 | const isReactive: typeof import('vue')['isReactive'] 22 | const isReadonly: typeof import('vue')['isReadonly'] 23 | const isRef: typeof import('vue')['isRef'] 24 | const markRaw: typeof import('vue')['markRaw'] 25 | const nextTick: typeof import('vue')['nextTick'] 26 | const onActivated: typeof import('vue')['onActivated'] 27 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 28 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 29 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 30 | const onDeactivated: typeof import('vue')['onDeactivated'] 31 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 32 | const onMounted: typeof import('vue')['onMounted'] 33 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 34 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 35 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 36 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 37 | const onUnmounted: typeof import('vue')['onUnmounted'] 38 | const onUpdated: typeof import('vue')['onUpdated'] 39 | const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] 40 | const provide: typeof import('vue')['provide'] 41 | const reactive: typeof import('vue')['reactive'] 42 | const readonly: typeof import('vue')['readonly'] 43 | const ref: typeof import('vue')['ref'] 44 | const resolveComponent: typeof import('vue')['resolveComponent'] 45 | const shallowReactive: typeof import('vue')['shallowReactive'] 46 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 47 | const shallowRef: typeof import('vue')['shallowRef'] 48 | const toRaw: typeof import('vue')['toRaw'] 49 | const toRef: typeof import('vue')['toRef'] 50 | const toRefs: typeof import('vue')['toRefs'] 51 | const toValue: typeof import('vue')['toValue'] 52 | const triggerRef: typeof import('vue')['triggerRef'] 53 | const unref: typeof import('vue')['unref'] 54 | const useAttrs: typeof import('vue')['useAttrs'] 55 | const useCssModule: typeof import('vue')['useCssModule'] 56 | const useCssVars: typeof import('vue')['useCssVars'] 57 | const useDialog: typeof import('naive-ui')['useDialog'] 58 | const useId: typeof import('vue')['useId'] 59 | const useLoadingBar: typeof import('naive-ui')['useLoadingBar'] 60 | const useMessage: typeof import('naive-ui')['useMessage'] 61 | const useModel: typeof import('vue')['useModel'] 62 | const useNotification: typeof import('naive-ui')['useNotification'] 63 | const useSlots: typeof import('vue')['useSlots'] 64 | const useTemplateRef: typeof import('vue')['useTemplateRef'] 65 | const watch: typeof import('vue')['watch'] 66 | const watchEffect: typeof import('vue')['watchEffect'] 67 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 68 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 69 | } 70 | // for type re-export 71 | declare global { 72 | // @ts-ignore 73 | export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' 74 | import('vue') 75 | } 76 | -------------------------------------------------------------------------------- /src/renderer/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | // biome-ignore lint: disable 6 | export {} 7 | 8 | /* prettier-ignore */ 9 | declare module 'vue' { 10 | export interface GlobalComponents { 11 | NAlert: typeof import('naive-ui')['NAlert'] 12 | NAvatar: typeof import('naive-ui')['NAvatar'] 13 | NBadge: typeof import('naive-ui')['NBadge'] 14 | NButton: typeof import('naive-ui')['NButton'] 15 | NButtonGroup: typeof import('naive-ui')['NButtonGroup'] 16 | NCarousel: typeof import('naive-ui')['NCarousel'] 17 | NCarouselItem: typeof import('naive-ui')['NCarouselItem'] 18 | NCheckbox: typeof import('naive-ui')['NCheckbox'] 19 | NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup'] 20 | NCollapse: typeof import('naive-ui')['NCollapse'] 21 | NCollapseItem: typeof import('naive-ui')['NCollapseItem'] 22 | NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] 23 | NConfigProvider: typeof import('naive-ui')['NConfigProvider'] 24 | NDialogProvider: typeof import('naive-ui')['NDialogProvider'] 25 | NDivider: typeof import('naive-ui')['NDivider'] 26 | NDrawer: typeof import('naive-ui')['NDrawer'] 27 | NDrawerContent: typeof import('naive-ui')['NDrawerContent'] 28 | NDropdown: typeof import('naive-ui')['NDropdown'] 29 | NEllipsis: typeof import('naive-ui')['NEllipsis'] 30 | NEmpty: typeof import('naive-ui')['NEmpty'] 31 | NForm: typeof import('naive-ui')['NForm'] 32 | NFormItem: typeof import('naive-ui')['NFormItem'] 33 | NGrid: typeof import('naive-ui')['NGrid'] 34 | NGridItem: typeof import('naive-ui')['NGridItem'] 35 | NIcon: typeof import('naive-ui')['NIcon'] 36 | NImage: typeof import('naive-ui')['NImage'] 37 | NInput: typeof import('naive-ui')['NInput'] 38 | NInputNumber: typeof import('naive-ui')['NInputNumber'] 39 | NLayout: typeof import('naive-ui')['NLayout'] 40 | NMessageProvider: typeof import('naive-ui')['NMessageProvider'] 41 | NModal: typeof import('naive-ui')['NModal'] 42 | NPopover: typeof import('naive-ui')['NPopover'] 43 | NProgress: typeof import('naive-ui')['NProgress'] 44 | NRadio: typeof import('naive-ui')['NRadio'] 45 | NRadioGroup: typeof import('naive-ui')['NRadioGroup'] 46 | NScrollbar: typeof import('naive-ui')['NScrollbar'] 47 | NSelect: typeof import('naive-ui')['NSelect'] 48 | NSlider: typeof import('naive-ui')['NSlider'] 49 | NSpace: typeof import('naive-ui')['NSpace'] 50 | NSpin: typeof import('naive-ui')['NSpin'] 51 | NSwitch: typeof import('naive-ui')['NSwitch'] 52 | NTabPane: typeof import('naive-ui')['NTabPane'] 53 | NTabs: typeof import('naive-ui')['NTabs'] 54 | NTag: typeof import('naive-ui')['NTag'] 55 | NText: typeof import('naive-ui')['NText'] 56 | NTooltip: typeof import('naive-ui')['NTooltip'] 57 | NVirtualList: typeof import('naive-ui')['NVirtualList'] 58 | RouterLink: typeof import('vue-router')['RouterLink'] 59 | RouterView: typeof import('vue-router')['RouterView'] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/renderer/components/Coffee.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 93 | -------------------------------------------------------------------------------- /src/renderer/components/LanguageSwitcher.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 28 | -------------------------------------------------------------------------------- /src/renderer/components/ShortcutToast.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 54 | 55 | 92 | -------------------------------------------------------------------------------- /src/renderer/components/common/BilibiliItem.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 77 | 78 | 118 | -------------------------------------------------------------------------------- /src/renderer/components/common/InstallAppModal.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 82 | 83 | 129 | -------------------------------------------------------------------------------- /src/renderer/components/common/MusicListNavigator.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'vue-router'; 2 | import { useMusicStore } from '@/store/modules/music'; 3 | 4 | /** 5 | * 导航到音乐列表页面的通用方法 6 | * @param router Vue路由实例 7 | * @param options 导航选项 8 | */ 9 | export function navigateToMusicList( 10 | router: Router, 11 | options: { 12 | id?: string | number; 13 | type?: 'album' | 'playlist' | 'dailyRecommend' | string; 14 | name: string; 15 | songList: any[]; 16 | listInfo?: any; 17 | canRemove?: boolean; 18 | } 19 | ) { 20 | const musicStore = useMusicStore(); 21 | const { id, type, name, songList, listInfo, canRemove = false } = options; 22 | 23 | // 保存数据到状态管理 24 | musicStore.setCurrentMusicList(songList, name, listInfo, canRemove); 25 | 26 | // 路由跳转 27 | if (id) { 28 | router.push({ 29 | name: 'musicList', 30 | params: { id }, 31 | query: { type } 32 | }); 33 | } else { 34 | router.push({ 35 | name: 'musicList' 36 | }); 37 | } 38 | } -------------------------------------------------------------------------------- /src/renderer/components/common/PlayBottom.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /src/renderer/components/common/PlayListsItem.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algerkong/AlgerMusicPlayer/bfaa06b0d5a2be74611af776f6f43ddad9d55739/src/renderer/components/common/PlayListsItem.vue -------------------------------------------------------------------------------- /src/renderer/components/common/SongItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 62 | -------------------------------------------------------------------------------- /src/renderer/components/common/songItemCom/BaseSongItem.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 105 | 106 | -------------------------------------------------------------------------------- /src/renderer/components/home/RecommendAlbum.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 88 | 89 | 116 | -------------------------------------------------------------------------------- /src/renderer/components/home/RecommendSonglist.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 59 | 60 | 74 | -------------------------------------------------------------------------------- /src/renderer/components/lyric/LyricCorrectionControl.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 48 | 49 | -------------------------------------------------------------------------------- /src/renderer/components/settings/ClearCacheSettings.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | -------------------------------------------------------------------------------- /src/renderer/components/settings/MusicSourceSettings.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | -------------------------------------------------------------------------------- /src/renderer/const/bar-const.ts: -------------------------------------------------------------------------------- 1 | export const USER_SET_OPTIONS = [ 2 | // { 3 | // label: '打卡', 4 | // key: 'card', 5 | // }, 6 | // { 7 | // label: '听歌升级', 8 | // key: 'card_music', 9 | // }, 10 | // { 11 | // label: '歌曲次数', 12 | // key: 'listen', 13 | // }, 14 | { 15 | label: '退出登录', 16 | key: 'logout' 17 | }, 18 | { 19 | label: '设置', 20 | key: 'set' 21 | } 22 | ]; 23 | 24 | export const SEARCH_TYPES = [ 25 | { 26 | label: '单曲', 27 | key: 1 28 | }, 29 | { 30 | label: '专辑', 31 | key: 10 32 | }, 33 | // { 34 | // label: '歌手', 35 | // key: 100, 36 | // }, 37 | { 38 | label: '歌单', 39 | key: 1000 40 | }, 41 | // { 42 | // label: '用户', 43 | // key: 1002, 44 | // }, 45 | { 46 | label: 'MV', 47 | key: 1004 48 | }, 49 | { 50 | label: 'B站', 51 | key: 2000 52 | } 53 | // { 54 | // label: '歌词', 55 | // key: 1006, 56 | // }, 57 | // { 58 | // label: '电台', 59 | // key: 1009, 60 | // }, 61 | // { 62 | // label: '视频', 63 | // key: 1014, 64 | // }, 65 | // { 66 | // label: '综合', 67 | // key: 1018, 68 | // }, 69 | ]; 70 | 71 | export const SEARCH_TYPE = { 72 | MUSIC: 1, // 单曲 73 | ALBUM: 10, // 专辑 74 | ARTIST: 100, // 歌手 75 | PLAYLIST: 1000, // 歌单 76 | MV: 1004, // MV 77 | BILIBILI: 2000 // B站视频 78 | } as const; 79 | -------------------------------------------------------------------------------- /src/renderer/directive/index.ts: -------------------------------------------------------------------------------- 1 | import { vLoading } from './loading/index'; 2 | 3 | const directives = { 4 | loading: vLoading 5 | }; 6 | 7 | export default directives; 8 | -------------------------------------------------------------------------------- /src/renderer/directive/loading/index.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, render, VNode } from 'vue'; 2 | 3 | import Loading from './index.vue'; 4 | 5 | const vnode: VNode = createVNode(Loading) as VNode; 6 | 7 | export const vLoading = { 8 | // 在绑定元素的父组件 及他自己的所有子节点都挂载完成后调用 9 | mounted: (el: HTMLElement) => { 10 | render(vnode, el); 11 | }, 12 | // 在绑定元素的父组件 及他自己的所有子节点都更新后调用 13 | updated: (el: HTMLElement, binding: any) => { 14 | if (binding.value) { 15 | vnode?.component?.exposed?.show(); 16 | } else { 17 | vnode?.component?.exposed?.hide(); 18 | } 19 | // 动态添加删除自定义class: loading-parent 20 | formatterClass(el, binding); 21 | }, 22 | // 绑定元素的父组件卸载后调用 23 | unmounted: () => { 24 | vnode?.component?.exposed?.hide(); 25 | } 26 | }; 27 | 28 | function formatterClass(el: HTMLElement, binding: any) { 29 | const classStr = el.getAttribute('class'); 30 | const tagetClass: number = classStr?.indexOf('loading-parent') as number; 31 | if (binding.value) { 32 | if (tagetClass === -1) { 33 | el.setAttribute('class', `${classStr} loading-parent`); 34 | } 35 | } else if (tagetClass > -1) { 36 | const classArray: Array = classStr?.split('') as string[]; 37 | classArray.splice(tagetClass - 1, tagetClass + 15); 38 | el.setAttribute('class', classArray?.join('')); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/directive/loading/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 56 | 94 | -------------------------------------------------------------------------------- /src/renderer/hooks/MusicHistoryHook.ts: -------------------------------------------------------------------------------- 1 | // musicHistoryHooks 2 | import { useLocalStorage } from '@vueuse/core'; 3 | 4 | import { recordPlay } from '@/api/stats'; 5 | import type { SongResult } from '@/type/music'; 6 | 7 | export const useMusicHistory = () => { 8 | const musicHistory = useLocalStorage('musicHistory', []); 9 | 10 | const addMusic = (music: SongResult) => { 11 | const index = musicHistory.value.findIndex((item) => item.id === music.id); 12 | if (index !== -1) { 13 | musicHistory.value[index].count = (musicHistory.value[index].count || 0) + 1; 14 | musicHistory.value.unshift(musicHistory.value.splice(index, 1)[0]); 15 | } else { 16 | musicHistory.value.unshift({ ...music, count: 1 }); 17 | } 18 | 19 | // 记录播放统计 20 | if (music?.id && music?.name) { 21 | // 获取艺术家名称 22 | let artistName = '未知艺术家'; 23 | 24 | if (music.ar) { 25 | artistName = music.ar.map((artist) => artist.name).join('/'); 26 | } else if (music.song?.artists && music.song.artists.length > 0) { 27 | artistName = music.song.artists.map((artist) => artist.name).join('/'); 28 | } else if (music.artists) { 29 | artistName = music.artists.map((artist) => artist.name).join('/'); 30 | } 31 | 32 | // 发送播放统计 33 | recordPlay(music.id, music.name, artistName).catch((error) => 34 | console.error('记录播放统计失败:', error) 35 | ); 36 | } 37 | }; 38 | 39 | const delMusic = (music: SongResult) => { 40 | const index = musicHistory.value.findIndex((item) => item.id === music.id); 41 | if (index !== -1) { 42 | musicHistory.value.splice(index, 1); 43 | } 44 | }; 45 | const musicList = ref(musicHistory.value); 46 | watch( 47 | () => musicHistory.value, 48 | () => { 49 | musicList.value = musicHistory.value; 50 | } 51 | ); 52 | 53 | return { 54 | musicHistory, 55 | musicList, 56 | addMusic, 57 | delMusic 58 | }; 59 | }; 60 | -------------------------------------------------------------------------------- /src/renderer/hooks/useArtist.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'vue-router'; 2 | 3 | export const useArtist = () => { 4 | const router = useRouter(); 5 | 6 | /** 7 | * 跳转到歌手详情页 8 | * @param id 歌手ID 9 | */ 10 | const navigateToArtist = (id: number) => { 11 | router.push(`/artist/detail/${id}`); 12 | }; 13 | 14 | return { 15 | navigateToArtist 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/renderer/hooks/useZoom.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | /** 4 | * 页面缩放功能的组合式API 5 | * 提供页面缩放相关的状态和方法 6 | */ 7 | export function useZoom() { 8 | // 缩放相关常量 9 | const MIN_ZOOM = 0.5; 10 | const MAX_ZOOM = 1.5; 11 | const ZOOM_STEP = 0.05; // 5%的步长 12 | 13 | // 当前缩放因子 14 | const zoomFactor = ref(1); 15 | 16 | // 初始化获取当前缩放比例 17 | const initZoomFactor = async () => { 18 | try { 19 | const currentZoom = await window.ipcRenderer.invoke('get-content-zoom'); 20 | zoomFactor.value = currentZoom; 21 | } catch (error) { 22 | console.error('获取缩放比例失败:', error); 23 | } 24 | }; 25 | 26 | // 增加缩放比例,保证100%为节点 27 | const increaseZoom = () => { 28 | let newZoom; 29 | 30 | // 如果当前缩放低于100%并且增加后会超过100%,则直接设为100% 31 | if (zoomFactor.value < 1.0 && zoomFactor.value + ZOOM_STEP > 1.0) { 32 | newZoom = 1.0; // 精确设置为100% 33 | } else { 34 | newZoom = Math.min(MAX_ZOOM, Math.round((zoomFactor.value + ZOOM_STEP) * 20) / 20); 35 | } 36 | 37 | setZoomFactor(newZoom); 38 | }; 39 | 40 | // 减少缩放比例,保证100%为节点 41 | const decreaseZoom = () => { 42 | let newZoom; 43 | 44 | // 如果当前缩放大于100%并且减少后会低于100%,则直接设为100% 45 | if (zoomFactor.value > 1.0 && zoomFactor.value - ZOOM_STEP < 1.0) { 46 | newZoom = 1.0; // 精确设置为100% 47 | } else { 48 | newZoom = Math.max(MIN_ZOOM, Math.round((zoomFactor.value - ZOOM_STEP) * 20) / 20); 49 | } 50 | 51 | setZoomFactor(newZoom); 52 | }; 53 | 54 | // 重置缩放比例到系统建议值 55 | const resetZoom = async () => { 56 | try { 57 | window.ipcRenderer.send('reset-content-zoom'); 58 | // 重置后重新获取系统计算的缩放比例 59 | const newZoom = await window.ipcRenderer.invoke('get-content-zoom'); 60 | zoomFactor.value = newZoom; 61 | } catch (error) { 62 | console.error('重置缩放比例失败:', error); 63 | } 64 | }; 65 | 66 | // 设置为100%标准缩放 67 | const setZoom100 = () => { 68 | setZoomFactor(1.0); 69 | }; 70 | 71 | // 设置缩放比例 72 | const setZoomFactor = (zoom: number) => { 73 | window.ipcRenderer.send('set-content-zoom', zoom); 74 | zoomFactor.value = zoom; 75 | }; 76 | 77 | // 检查是否为100%缩放 78 | const isZoom100 = () => { 79 | return Math.abs(zoomFactor.value - 1.0) < 0.001; 80 | }; 81 | 82 | return { 83 | zoomFactor, 84 | initZoomFactor, 85 | increaseZoom, 86 | decreaseZoom, 87 | resetZoom, 88 | setZoom100, 89 | setZoomFactor, 90 | isZoom100, 91 | MIN_ZOOM, 92 | MAX_ZOOM, 93 | ZOOM_STEP 94 | }; 95 | } -------------------------------------------------------------------------------- /src/renderer/index.css: -------------------------------------------------------------------------------- 1 | /* ./src/index.css */ 2 | 3 | /*! @import */ 4 | @tailwind base; 5 | @tailwind components; 6 | @tailwind utilities; 7 | 8 | .n-image img { 9 | background-color: #111111; 10 | width: 100%; 11 | } 12 | 13 | .n-slider-handle-indicator--top { 14 | @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; 15 | mix-blend-mode: difference !important; 16 | } 17 | 18 | .v-binder-follower-container:has(.n-slider-handle-indicator--top) { 19 | z-index: 999999999 !important; 20 | } 21 | 22 | .text-el { 23 | @apply overflow-ellipsis overflow-hidden whitespace-nowrap; 24 | } 25 | 26 | .theme-dark { 27 | --bg-color: #000; 28 | --text-color: #fff; 29 | --bg-color-100: #161616; 30 | --bg-color-200: #2d2d2d; 31 | --bg-color-300: #3d3d3d; 32 | --text-color: #f8f9fa; 33 | --text-color-100: #e9ecef; 34 | --text-color-200: #dee2e6; 35 | --text-color-300: #dde0e3; 36 | --primary-color: #22c55e; 37 | } 38 | 39 | .theme-light { 40 | --bg-color: #fff; 41 | --text-color: #000; 42 | --bg-color-100: #f8f9fa; 43 | --bg-color-200: #e9ecef; 44 | --bg-color-300: #dee2e6; 45 | --text-color: #000; 46 | --text-color-100: #161616; 47 | --text-color-200: #2d2d2d; 48 | --text-color-300: #3d3d3d; 49 | --primary-color: #22c55e; 50 | } 51 | 52 | .theme-gray { 53 | --bg-color: #f8f9fa; 54 | --text-color: #000; 55 | --bg-color-100: #e9ecef; 56 | --bg-color-200: #dee2e6; 57 | --bg-color-300: #dde0e3; 58 | --text-color: #000; 59 | --text-color-100: #161616; 60 | --text-color-200: #2d2d2d; 61 | --text-color-300: #3d3d3d; 62 | --primary-color: #22c55e; 63 | } 64 | 65 | :root { 66 | --text-color: #000000dd; 67 | } 68 | 69 | :root[class='dark'] { 70 | --text-color: #ffffffdd; 71 | } 72 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 网抑云音乐 | AlgerKong | AlgerMusicPlayer 13 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 43 | 44 | 45 | 46 |
47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/renderer/layout/MiniLayout.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 11 | 12 | 17 | -------------------------------------------------------------------------------- /src/renderer/layout/components/AppMenu.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 75 | 76 | 136 | -------------------------------------------------------------------------------- /src/renderer/layout/components/TitleBar.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 124 | 125 | 150 | -------------------------------------------------------------------------------- /src/renderer/layout/components/index.ts: -------------------------------------------------------------------------------- 1 | import AppMenu from './AppMenu.vue'; 2 | import PlayBar from './PlayBar.vue'; 3 | import SearchBar from './SearchBar.vue'; 4 | 5 | export { AppMenu, PlayBar, SearchBar }; 6 | -------------------------------------------------------------------------------- /src/renderer/layout/components/lrcFull.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/renderer/main.ts: -------------------------------------------------------------------------------- 1 | import './index.css'; 2 | import 'animate.css'; 3 | import 'remixicon/fonts/remixicon.css'; 4 | 5 | import { createApp } from 'vue'; 6 | 7 | import i18n from '@/../i18n/renderer'; 8 | import router from '@/router'; 9 | import pinia from '@/store'; 10 | 11 | import App from './App.vue'; 12 | import directives from './directive'; 13 | import { initAppShortcuts } from './utils/appShortcuts'; 14 | 15 | const app = createApp(App); 16 | 17 | Object.keys(directives).forEach((key: string) => { 18 | app.directive(key, directives[key as keyof typeof directives]); 19 | }); 20 | 21 | app.use(pinia); 22 | app.use(router); 23 | app.use(i18n); 24 | app.mount('#app'); 25 | 26 | // 初始化应用内快捷键 27 | initAppShortcuts(); 28 | -------------------------------------------------------------------------------- /src/renderer/router/home.ts: -------------------------------------------------------------------------------- 1 | const layoutRouter = [ 2 | { 3 | path: '/', 4 | name: 'home', 5 | meta: { 6 | title: '首页', 7 | icon: 'icon-Home', 8 | keepAlive: true, 9 | isMobile: true 10 | }, 11 | component: () => import('@/views/home/index.vue') 12 | }, 13 | { 14 | path: '/search', 15 | name: 'search', 16 | meta: { 17 | title: '搜索', 18 | noScroll: true, 19 | icon: 'icon-Search', 20 | keepAlive: true, 21 | isMobile: true 22 | }, 23 | component: () => import('@/views/search/index.vue') 24 | }, 25 | { 26 | path: '/list', 27 | name: 'list', 28 | meta: { 29 | title: '歌单', 30 | icon: 'icon-Paper', 31 | keepAlive: true, 32 | isMobile: true 33 | }, 34 | component: () => import('@/views/list/index.vue') 35 | }, 36 | { 37 | path: '/toplist', 38 | name: 'toplist', 39 | meta: { 40 | title: '排行榜', 41 | icon: 'ri-bar-chart-grouped-fill', 42 | keepAlive: true, 43 | isMobile: true 44 | }, 45 | component: () => import('@/views/toplist/index.vue') 46 | }, 47 | { 48 | path: '/mv', 49 | name: 'mv', 50 | meta: { 51 | title: 'MV', 52 | icon: 'icon-recordfill', 53 | keepAlive: true, 54 | isMobile: false 55 | }, 56 | component: () => import('@/views/mv/index.vue') 57 | }, 58 | { 59 | path: '/history', 60 | name: 'history', 61 | component: () => import('@/views/historyAndFavorite/index.vue'), 62 | meta: { 63 | title: '收藏历史', 64 | icon: 'icon-a-TicketStar', 65 | keepAlive: true 66 | } 67 | }, 68 | { 69 | path: '/user', 70 | name: 'user', 71 | meta: { 72 | title: '用户', 73 | icon: 'icon-Profile', 74 | keepAlive: true, 75 | noScroll: true, 76 | isMobile: true 77 | }, 78 | component: () => import('@/views/user/index.vue') 79 | }, 80 | { 81 | path: '/set', 82 | name: 'set', 83 | meta: { 84 | title: '设置', 85 | icon: 'ri-settings-3-fill', 86 | keepAlive: true, 87 | noScroll: true 88 | }, 89 | component: () => import('@/views/set/index.vue') 90 | } 91 | ]; 92 | export default layoutRouter; 93 | -------------------------------------------------------------------------------- /src/renderer/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router'; 2 | 3 | import { recordVisit } from '@/api/stats'; 4 | import AppLayout from '@/layout/AppLayout.vue'; 5 | import MiniLayout from '@/layout/MiniLayout.vue'; 6 | import homeRouter from '@/router/home'; 7 | import otherRouter from '@/router/other'; 8 | import { useSettingsStore } from '@/store/modules/settings'; 9 | 10 | // 由于 Vue Router 守卫在创建前不能直接使用组合式 API 11 | // 我们创建一个辅助函数来获取 store 实例 12 | let _settingsStore: ReturnType | null = null; 13 | const getSettingsStore = () => { 14 | if (!_settingsStore) { 15 | _settingsStore = useSettingsStore(); 16 | } 17 | return _settingsStore; 18 | }; 19 | 20 | const loginRouter = { 21 | path: '/login', 22 | name: 'login', 23 | mate: { 24 | keepAlive: true, 25 | title: '登录', 26 | icon: 'icon-Home' 27 | }, 28 | component: () => import('@/views/login/index.vue') 29 | }; 30 | 31 | const setRouter = { 32 | path: '/set', 33 | name: 'set', 34 | mate: { 35 | keepAlive: true, 36 | title: '设置', 37 | icon: 'icon-Home' 38 | }, 39 | component: () => import('@/views/set/index.vue') 40 | }; 41 | 42 | const routes = [ 43 | { 44 | path: '/', 45 | component: AppLayout, 46 | children: [...homeRouter, loginRouter, setRouter, ...otherRouter] 47 | }, 48 | { 49 | path: '/lyric', 50 | component: () => import('@/views/lyric/index.vue') 51 | }, 52 | { 53 | path: '/mini', 54 | component: MiniLayout 55 | } 56 | ]; 57 | 58 | const router = createRouter({ 59 | routes, 60 | history: createWebHashHistory() 61 | }); 62 | 63 | // 添加全局前置守卫 64 | router.beforeEach((to, _, next) => { 65 | const settingsStore = getSettingsStore(); 66 | 67 | // 如果是迷你模式 68 | if (settingsStore.isMiniMode) { 69 | // 只允许访问 /mini 路由 70 | if (to.path === '/mini') { 71 | next(); 72 | } else { 73 | next(false); // 阻止导航 74 | } 75 | } else if (to.path === '/mini') { 76 | // 如果不是迷你模式但想访问 /mini 路由,重定向到首页 77 | next('/'); 78 | } else { 79 | // 其他情况正常导航 80 | next(); 81 | } 82 | }); 83 | 84 | // 添加全局后置钩子,记录页面访问 85 | router.afterEach((to) => { 86 | const pageName = to.name?.toString() || to.path; 87 | // 使用setTimeout避免阻塞路由导航 88 | setTimeout(() => { 89 | recordVisit(pageName).catch((error) => console.error('记录页面访问失败:', error)); 90 | }, 100); 91 | }); 92 | 93 | export default router; 94 | -------------------------------------------------------------------------------- /src/renderer/router/other.ts: -------------------------------------------------------------------------------- 1 | const otherRouter = [ 2 | { 3 | path: '/user/follows', 4 | name: 'userFollows', 5 | meta: { 6 | title: '关注列表', 7 | keepAlive: false, 8 | showInMenu: false, 9 | back: true 10 | }, 11 | component: () => import('@/views/user/follows.vue') 12 | }, 13 | { 14 | path: '/user/followers', 15 | name: 'userFollowers', 16 | meta: { 17 | title: '粉丝列表', 18 | keepAlive: false, 19 | showInMenu: false, 20 | back: true 21 | }, 22 | component: () => import('@/views/user/followers.vue') 23 | }, 24 | { 25 | path: '/user/detail/:uid', 26 | name: 'userDetail', 27 | meta: { 28 | title: '用户详情', 29 | keepAlive: false, 30 | showInMenu: false, 31 | back: true 32 | }, 33 | component: () => import('@/views/user/detail.vue') 34 | }, 35 | { 36 | path: '/artist/detail/:id', 37 | name: 'artistDetail', 38 | meta: { 39 | title: '歌手详情', 40 | keepAlive: true, 41 | showInMenu: false, 42 | back: true 43 | }, 44 | component: () => import('@/views/artist/detail.vue') 45 | }, 46 | { 47 | path: '/bilibili/:bvid', 48 | name: 'bilibiliPlayer', 49 | meta: { 50 | title: 'B站听书', 51 | keepAlive: true, 52 | showInMenu: false, 53 | back: true 54 | }, 55 | component: () => import('@/views/bilibili/BilibiliPlayer.vue') 56 | }, 57 | { 58 | path: '/music-list/:id?', 59 | name: 'musicList', 60 | meta: { 61 | title: '音乐列表', 62 | keepAlive: false, 63 | showInMenu: false, 64 | back: true 65 | }, 66 | component: () => import('@/views/music/MusicListPage.vue') 67 | } 68 | ]; 69 | export default otherRouter; 70 | -------------------------------------------------------------------------------- /src/renderer/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue'; 3 | 4 | const component: DefineComponent<{}, {}, any>; 5 | export default component; 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | import router from '@/router'; 3 | 4 | // 创建 pinia 实例 5 | const pinia = createPinia(); 6 | 7 | // 添加路由到 Pinia 8 | pinia.use(({ store }) => { 9 | store.router = markRaw(router); 10 | }); 11 | 12 | // 导出所有 store 13 | export * from './modules/lyric'; 14 | export * from './modules/menu'; 15 | export * from './modules/player'; 16 | export * from './modules/search'; 17 | export * from './modules/settings'; 18 | export * from './modules/user'; 19 | export * from './modules/music'; 20 | 21 | export default pinia; 22 | -------------------------------------------------------------------------------- /src/renderer/store/modules/lyric.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { ref } from 'vue'; 3 | 4 | export const useLyricStore = defineStore('lyric', () => { 5 | const lyric = ref({}); 6 | 7 | const setLyric = (newLyric: any) => { 8 | lyric.value = newLyric; 9 | }; 10 | 11 | return { 12 | lyric, 13 | setLyric 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /src/renderer/store/modules/menu.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { ref } from 'vue'; 3 | 4 | import homeRouter from '@/router/home'; 5 | 6 | export const useMenuStore = defineStore('menu', () => { 7 | const menus = ref(homeRouter); 8 | 9 | const setMenus = (newMenus: any[]) => { 10 | menus.value = newMenus; 11 | }; 12 | 13 | return { 14 | menus, 15 | setMenus 16 | }; 17 | }); 18 | -------------------------------------------------------------------------------- /src/renderer/store/modules/music.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | 3 | interface MusicState { 4 | currentMusicList: any[] | null; 5 | currentMusicListName: string; 6 | currentListInfo: any | null; 7 | canRemoveSong: boolean; 8 | } 9 | 10 | export const useMusicStore = defineStore('music', { 11 | state: (): MusicState => ({ 12 | currentMusicList: null, 13 | currentMusicListName: '', 14 | currentListInfo: null, 15 | canRemoveSong: false 16 | }), 17 | 18 | actions: { 19 | // 设置当前音乐列表 20 | setCurrentMusicList(list: any[], name: string, listInfo: any = null, canRemove = false) { 21 | this.currentMusicList = list; 22 | this.currentMusicListName = name; 23 | this.currentListInfo = listInfo; 24 | this.canRemoveSong = canRemove; 25 | }, 26 | 27 | // 清除当前音乐列表 28 | clearCurrentMusicList() { 29 | this.currentMusicList = null; 30 | this.currentMusicListName = ''; 31 | this.currentListInfo = null; 32 | this.canRemoveSong = false; 33 | }, 34 | 35 | // 从列表中移除一首歌曲 36 | removeSongFromList(id: number) { 37 | if (!this.currentMusicList) return; 38 | 39 | const index = this.currentMusicList.findIndex((song) => song.id === id); 40 | if (index !== -1) { 41 | this.currentMusicList.splice(index, 1); 42 | } 43 | } 44 | } 45 | }); -------------------------------------------------------------------------------- /src/renderer/store/modules/search.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { ref } from 'vue'; 3 | 4 | export const useSearchStore = defineStore('search', () => { 5 | const searchValue = ref(''); 6 | const searchType = ref(1); 7 | 8 | const setSearchValue = (value: string) => { 9 | searchValue.value = value; 10 | }; 11 | 12 | const setSearchType = (type: number) => { 13 | searchType.value = type; 14 | }; 15 | 16 | return { 17 | searchValue, 18 | searchType, 19 | setSearchValue, 20 | setSearchType 21 | }; 22 | }); 23 | -------------------------------------------------------------------------------- /src/renderer/store/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { ref } from 'vue'; 3 | 4 | import { logout } from '@/api/login'; 5 | import { getLikedList } from '@/api/music'; 6 | 7 | interface UserData { 8 | userId: number; 9 | [key: string]: any; 10 | } 11 | 12 | function getLocalStorageItem(key: string, defaultValue: T): T { 13 | try { 14 | const item = localStorage.getItem(key); 15 | return item ? JSON.parse(item) : defaultValue; 16 | } catch { 17 | return defaultValue; 18 | } 19 | } 20 | 21 | export const useUserStore = defineStore('user', () => { 22 | // 状态 23 | const user = ref(getLocalStorageItem('user', null)); 24 | const searchValue = ref(''); 25 | const searchType = ref(1); 26 | 27 | // 方法 28 | const setUser = (userData: UserData) => { 29 | user.value = userData; 30 | localStorage.setItem('user', JSON.stringify(userData)); 31 | }; 32 | 33 | const handleLogout = async () => { 34 | try { 35 | await logout(); 36 | user.value = null; 37 | localStorage.removeItem('user'); 38 | localStorage.removeItem('token'); 39 | // 刷新 40 | window.location.reload(); 41 | } catch (error) { 42 | console.error('登出失败:', error); 43 | } 44 | }; 45 | 46 | const setSearchValue = (value: string) => { 47 | searchValue.value = value; 48 | }; 49 | 50 | const setSearchType = (type: number) => { 51 | searchType.value = type; 52 | }; 53 | 54 | // 初始化 55 | const initializeUser = async () => { 56 | const savedUser = getLocalStorageItem('user', null); 57 | if (savedUser) { 58 | user.value = savedUser; 59 | // 如果用户已登录,获取收藏列表 60 | if (localStorage.getItem('token')) { 61 | try { 62 | const { data } = await getLikedList(savedUser.userId); 63 | return data?.ids || []; 64 | } catch (error) { 65 | console.error('获取收藏列表失败:', error); 66 | return []; 67 | } 68 | } 69 | } 70 | return []; 71 | }; 72 | 73 | return { 74 | // 状态 75 | user, 76 | searchValue, 77 | searchType, 78 | 79 | // 方法 80 | setUser, 81 | handleLogout, 82 | setSearchValue, 83 | setSearchType, 84 | initializeUser 85 | }; 86 | }); 87 | -------------------------------------------------------------------------------- /src/renderer/type/album.ts: -------------------------------------------------------------------------------- 1 | export interface IAlbumNew { 2 | code: number; 3 | albums: Album[]; 4 | } 5 | 6 | export interface Album { 7 | name: string; 8 | id: number; 9 | type: string; 10 | size: number; 11 | picId: number; 12 | blurPicUrl: string; 13 | companyId: number; 14 | pic: number; 15 | picUrl: string; 16 | publishTime: number; 17 | description: string; 18 | tags: string; 19 | company: string; 20 | briefDesc: string; 21 | artist: Artist; 22 | songs?: any; 23 | alias: string[]; 24 | status: number; 25 | copyrightId: number; 26 | commentThreadId: string; 27 | artists: Artist2[]; 28 | paid: boolean; 29 | onSale: boolean; 30 | picId_str: string; 31 | } 32 | 33 | interface Artist2 { 34 | name: string; 35 | id: number; 36 | picId: number; 37 | img1v1Id: number; 38 | briefDesc: string; 39 | picUrl: string; 40 | img1v1Url: string; 41 | albumSize: number; 42 | alias: any[]; 43 | trans: string; 44 | musicSize: number; 45 | topicPerson: number; 46 | img1v1Id_str: string; 47 | } 48 | 49 | interface Artist { 50 | name: string; 51 | id: number; 52 | picId: number; 53 | img1v1Id: number; 54 | briefDesc: string; 55 | picUrl: string; 56 | img1v1Url: string; 57 | albumSize: number; 58 | alias: string[]; 59 | trans: string; 60 | musicSize: number; 61 | topicPerson: number; 62 | picId_str?: string; 63 | img1v1Id_str: string; 64 | transNames?: string[]; 65 | } 66 | -------------------------------------------------------------------------------- /src/renderer/type/artist.ts: -------------------------------------------------------------------------------- 1 | export interface IArtistDetail { 2 | videoCount: number; 3 | vipRights: VipRights; 4 | identify: Identify; 5 | artist: IArtist; 6 | blacklist: boolean; 7 | preferShow: number; 8 | showPriMsg: boolean; 9 | secondaryExpertIdentiy: SecondaryExpertIdentiy[]; 10 | eventCount: number; 11 | user: User; 12 | } 13 | 14 | interface User { 15 | backgroundUrl: string; 16 | birthday: number; 17 | detailDescription: string; 18 | authenticated: boolean; 19 | gender: number; 20 | city: number; 21 | signature: null; 22 | description: string; 23 | remarkName: null; 24 | shortUserName: string; 25 | accountStatus: number; 26 | locationStatus: number; 27 | avatarImgId: number; 28 | defaultAvatar: boolean; 29 | province: number; 30 | nickname: string; 31 | expertTags: null; 32 | djStatus: number; 33 | avatarUrl: string; 34 | accountType: number; 35 | authStatus: number; 36 | vipType: number; 37 | userName: string; 38 | followed: boolean; 39 | userId: number; 40 | lastLoginIP: string; 41 | lastLoginTime: number; 42 | authenticationTypes: number; 43 | mutual: boolean; 44 | createTime: number; 45 | anchor: boolean; 46 | authority: number; 47 | backgroundImgId: number; 48 | userType: number; 49 | experts: null; 50 | avatarDetail: AvatarDetail; 51 | } 52 | 53 | interface AvatarDetail { 54 | userType: number; 55 | identityLevel: number; 56 | identityIconUrl: string; 57 | } 58 | 59 | interface SecondaryExpertIdentiy { 60 | expertIdentiyId: number; 61 | expertIdentiyName: string; 62 | expertIdentiyCount: number; 63 | } 64 | 65 | export interface IArtist { 66 | id: number; 67 | cover: string; 68 | avatar: string; 69 | name: string; 70 | transNames: any[]; 71 | alias: any[]; 72 | identities: any[]; 73 | identifyTag: string[]; 74 | briefDesc: string; 75 | rank: Rank; 76 | albumSize: number; 77 | musicSize: number; 78 | mvSize: number; 79 | } 80 | 81 | interface Rank { 82 | rank: number; 83 | type: number; 84 | } 85 | 86 | interface Identify { 87 | imageUrl: string; 88 | imageDesc: string; 89 | actionUrl: string; 90 | } 91 | 92 | interface VipRights { 93 | rightsInfoDetailDtoList: RightsInfoDetailDtoList[]; 94 | oldProtocol: boolean; 95 | redVipAnnualCount: number; 96 | redVipLevel: number; 97 | now: number; 98 | } 99 | 100 | interface RightsInfoDetailDtoList { 101 | vipCode: number; 102 | expireTime: number; 103 | iconUrl: null; 104 | dynamicIconUrl: null; 105 | vipLevel: number; 106 | signIap: boolean; 107 | signDeduct: boolean; 108 | signIapDeduct: boolean; 109 | sign: boolean; 110 | } 111 | -------------------------------------------------------------------------------- /src/renderer/type/day_recommend.ts: -------------------------------------------------------------------------------- 1 | export interface IDayRecommend { 2 | dailySongs: DailySong[]; 3 | orderSongs: any[]; 4 | recommendReasons: RecommendReason[]; 5 | mvResourceInfos: null; 6 | } 7 | 8 | interface RecommendReason { 9 | songId: number; 10 | reason: string; 11 | reasonId: string; 12 | targetUrl: null; 13 | } 14 | 15 | interface DailySong { 16 | name: string; 17 | id: number; 18 | pst: number; 19 | t: number; 20 | ar: Ar[]; 21 | alia: string[]; 22 | pop: number; 23 | st: number; 24 | rt: null | string; 25 | fee: number; 26 | v: number; 27 | crbt: null; 28 | cf: string; 29 | al: Al; 30 | dt: number; 31 | h: H; 32 | m: H; 33 | l: H; 34 | sq: H | null; 35 | hr: H | null; 36 | a: null; 37 | cd: string; 38 | no: number; 39 | rtUrl: null; 40 | ftype: number; 41 | rtUrls: any[]; 42 | djId: number; 43 | copyright: number; 44 | s_id: number; 45 | mark: number; 46 | originCoverType: number; 47 | originSongSimpleData: OriginSongSimpleDatum | null; 48 | tagPicList: null; 49 | resourceState: boolean; 50 | version: number; 51 | songJumpInfo: null; 52 | entertainmentTags: null; 53 | single: number; 54 | noCopyrightRcmd: null; 55 | rtype: number; 56 | rurl: null; 57 | mst: number; 58 | cp: number; 59 | mv: number; 60 | publishTime: number; 61 | reason: null | string; 62 | videoInfo: VideoInfo; 63 | recommendReason: null | string; 64 | privilege: Privilege; 65 | alg: string; 66 | tns?: string[]; 67 | s_ctrp?: string; 68 | } 69 | 70 | interface Privilege { 71 | id: number; 72 | fee: number; 73 | payed: number; 74 | realPayed: number; 75 | st: number; 76 | pl: number; 77 | dl: number; 78 | sp: number; 79 | cp: number; 80 | subp: number; 81 | cs: boolean; 82 | maxbr: number; 83 | fl: number; 84 | pc: null; 85 | toast: boolean; 86 | flag: number; 87 | paidBigBang: boolean; 88 | preSell: boolean; 89 | playMaxbr: number; 90 | downloadMaxbr: number; 91 | maxBrLevel: string; 92 | playMaxBrLevel: string; 93 | downloadMaxBrLevel: string; 94 | plLevel: string; 95 | dlLevel: string; 96 | flLevel: string; 97 | rscl: null; 98 | freeTrialPrivilege: FreeTrialPrivilege; 99 | rightSource: number; 100 | chargeInfoList: ChargeInfoList[]; 101 | } 102 | 103 | interface ChargeInfoList { 104 | rate: number; 105 | chargeUrl: null; 106 | chargeMessage: null; 107 | chargeType: number; 108 | } 109 | 110 | interface FreeTrialPrivilege { 111 | resConsumable: boolean; 112 | userConsumable: boolean; 113 | listenType: number; 114 | cannotListenReason: number; 115 | playReason: null; 116 | } 117 | 118 | interface VideoInfo { 119 | moreThanOne: boolean; 120 | video: Video | null; 121 | } 122 | 123 | interface Video { 124 | vid: string; 125 | type: number; 126 | title: string; 127 | playTime: number; 128 | coverUrl: string; 129 | publishTime: number; 130 | artists: null; 131 | alias: null; 132 | } 133 | 134 | interface OriginSongSimpleDatum { 135 | songId: number; 136 | name: string; 137 | artists: Artist[]; 138 | albumMeta: Artist; 139 | } 140 | 141 | interface Artist { 142 | id: number; 143 | name: string; 144 | } 145 | 146 | interface H { 147 | br: number; 148 | fid: number; 149 | size: number; 150 | vd: number; 151 | sr: number; 152 | } 153 | 154 | interface Al { 155 | id: number; 156 | name: string; 157 | picUrl: string; 158 | tns: string[]; 159 | pic_str?: string; 160 | pic: number; 161 | } 162 | 163 | interface Ar { 164 | id: number; 165 | name: string; 166 | tns: any[]; 167 | alias: any[]; 168 | } 169 | -------------------------------------------------------------------------------- /src/renderer/type/index.ts: -------------------------------------------------------------------------------- 1 | export interface IData { 2 | code: number; 3 | data: T; 4 | result: T; 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/type/list.ts: -------------------------------------------------------------------------------- 1 | export interface IList { 2 | playlists: Playlist[]; 3 | code: number; 4 | more: boolean; 5 | lasttime: number; 6 | total: number; 7 | } 8 | 9 | export interface Playlist { 10 | name: string; 11 | id: number; 12 | trackNumberUpdateTime: number; 13 | status: number; 14 | userId: number; 15 | createTime: number; 16 | updateTime: number; 17 | subscribedCount: number; 18 | trackCount: number; 19 | cloudTrackCount: number; 20 | coverImgUrl: string; 21 | coverImgId: number; 22 | description: string; 23 | tags: string[]; 24 | playCount: number; 25 | trackUpdateTime: number; 26 | specialType: number; 27 | totalDuration: number; 28 | creator: Creator; 29 | tracks?: any; 30 | subscribers: Subscriber[]; 31 | subscribed: boolean; 32 | commentThreadId: string; 33 | newImported: boolean; 34 | adType: number; 35 | highQuality: boolean; 36 | privacy: number; 37 | ordered: boolean; 38 | anonimous: boolean; 39 | coverStatus: number; 40 | recommendInfo?: any; 41 | shareCount: number; 42 | coverImgId_str?: string; 43 | commentCount: number; 44 | copywriter: string; 45 | tag: string; 46 | } 47 | 48 | interface Subscriber { 49 | defaultAvatar: boolean; 50 | province: number; 51 | authStatus: number; 52 | followed: boolean; 53 | avatarUrl: string; 54 | accountStatus: number; 55 | gender: number; 56 | city: number; 57 | birthday: number; 58 | userId: number; 59 | userType: number; 60 | nickname: string; 61 | signature: string; 62 | description: string; 63 | detailDescription: string; 64 | avatarImgId: number; 65 | backgroundImgId: number; 66 | backgroundUrl: string; 67 | authority: number; 68 | mutual: boolean; 69 | expertTags?: any; 70 | experts?: any; 71 | djStatus: number; 72 | vipType: number; 73 | remarkName?: any; 74 | authenticationTypes: number; 75 | avatarDetail?: any; 76 | avatarImgIdStr: string; 77 | backgroundImgIdStr: string; 78 | anchor: boolean; 79 | avatarImgId_str?: string; 80 | } 81 | 82 | interface Creator { 83 | defaultAvatar: boolean; 84 | province: number; 85 | authStatus: number; 86 | followed: boolean; 87 | avatarUrl: string; 88 | accountStatus: number; 89 | gender: number; 90 | city: number; 91 | birthday: number; 92 | userId: number; 93 | userType: number; 94 | nickname: string; 95 | signature: string; 96 | description: string; 97 | detailDescription: string; 98 | avatarImgId: number; 99 | backgroundImgId: number; 100 | backgroundUrl: string; 101 | authority: number; 102 | mutual: boolean; 103 | expertTags?: string[]; 104 | experts?: Expert; 105 | djStatus: number; 106 | vipType: number; 107 | remarkName?: any; 108 | authenticationTypes: number; 109 | avatarDetail?: AvatarDetail; 110 | avatarImgIdStr: string; 111 | backgroundImgIdStr: string; 112 | anchor: boolean; 113 | avatarImgId_str?: string; 114 | } 115 | 116 | interface AvatarDetail { 117 | userType: number; 118 | identityLevel: number; 119 | identityIconUrl: string; 120 | } 121 | 122 | interface Expert { 123 | '2': string; 124 | '1'?: string; 125 | } 126 | 127 | // 推荐歌单 128 | export interface IRecommendList { 129 | hasTaste: boolean; 130 | code: number; 131 | category: number; 132 | result: IRecommendItem[]; 133 | } 134 | 135 | export interface IRecommendItem { 136 | id: number; 137 | type: number; 138 | name: string; 139 | copywriter: string; 140 | picUrl: string; 141 | canDislike: boolean; 142 | trackNumberUpdateTime: number; 143 | playCount: number; 144 | trackCount: number; 145 | highQuality: boolean; 146 | alg: string; 147 | } 148 | -------------------------------------------------------------------------------- /src/renderer/type/lyric.ts: -------------------------------------------------------------------------------- 1 | export interface ILyric { 2 | sgc: boolean; 3 | sfy: boolean; 4 | qfy: boolean; 5 | lrc: Lrc; 6 | klyric: Lrc; 7 | tlyric: Lrc; 8 | code: number; 9 | } 10 | 11 | interface Lrc { 12 | version: number; 13 | lyric: string; 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/type/mv.ts: -------------------------------------------------------------------------------- 1 | export interface IMvItem { 2 | id: number; 3 | cover: string; 4 | name: string; 5 | playCount: number; 6 | briefDesc?: any; 7 | desc?: any; 8 | artistName: string; 9 | artistId: number; 10 | duration: number; 11 | mark: number; 12 | mv: IMvData; 13 | lastRank: number; 14 | score: number; 15 | subed: boolean; 16 | artists: Artist[]; 17 | transNames?: string[]; 18 | alias?: string[]; 19 | } 20 | 21 | export interface IMvData { 22 | authId: number; 23 | status: number; 24 | id: number; 25 | title: string; 26 | subTitle: string; 27 | appTitle: string; 28 | aliaName: string; 29 | transName: string; 30 | pic4v3: number; 31 | pic16v9: number; 32 | caption: number; 33 | captionLanguage: string; 34 | style?: any; 35 | mottos: string; 36 | oneword?: any; 37 | appword: string; 38 | stars?: any; 39 | desc: string; 40 | area: string; 41 | type: string; 42 | subType: string; 43 | neteaseonly: number; 44 | upban: number; 45 | topWeeks: string; 46 | publishTime: string; 47 | online: number; 48 | score: number; 49 | plays: number; 50 | monthplays: number; 51 | weekplays: number; 52 | dayplays: number; 53 | fee: number; 54 | artists: Artist[]; 55 | videos: Video[]; 56 | } 57 | 58 | interface Video { 59 | tagSign: TagSign; 60 | tag: string; 61 | url: string; 62 | duration: number; 63 | size: number; 64 | width: number; 65 | height: number; 66 | container: string; 67 | md5: string; 68 | check: boolean; 69 | } 70 | 71 | interface TagSign { 72 | br: number; 73 | type: string; 74 | tagSign: string; 75 | resolution: number; 76 | mvtype: string; 77 | } 78 | 79 | interface Artist { 80 | id: number; 81 | name: string; 82 | } 83 | 84 | // { 85 | // "id": 14686812, 86 | // "url": "http://vodkgeyttp8.vod.126.net/cloudmusic/e18b/core/aa57/6f56150a35613ef77fc70b253bea4977.mp4?wsSecret=84a301277e05143de1dd912d2a4dbb0d&wsTime=1703668700", 87 | // "r": 1080, 88 | // "size": 215391070, 89 | // "md5": "", 90 | // "code": 200, 91 | // "expi": 3600, 92 | // "fee": 0, 93 | // "mvFee": 0, 94 | // "st": 0, 95 | // "promotionVo": null, 96 | // "msg": "" 97 | // } 98 | 99 | export interface IMvUrlData { 100 | id: number; 101 | url: string; 102 | r: number; 103 | size: number; 104 | md5: string; 105 | code: number; 106 | expi: number; 107 | fee: number; 108 | mvFee: number; 109 | st: number; 110 | promotionVo: null | any; 111 | msg: string; 112 | } 113 | -------------------------------------------------------------------------------- /src/renderer/type/playlist.ts: -------------------------------------------------------------------------------- 1 | export interface IPlayListSort { 2 | code: number; 3 | all: SortAll; 4 | sub: SortAll[]; 5 | categories: SortCategories; 6 | } 7 | 8 | interface SortCategories { 9 | '0': string; 10 | '1': string; 11 | '2': string; 12 | '3': string; 13 | '4': string; 14 | } 15 | 16 | interface SortAll { 17 | name: string; 18 | resourceCount?: number; 19 | imgId?: number; 20 | imgUrl?: any; 21 | type?: number; 22 | category?: number; 23 | resourceType?: number; 24 | hot?: boolean; 25 | activity?: boolean; 26 | } 27 | -------------------------------------------------------------------------------- /src/renderer/type/singer.ts: -------------------------------------------------------------------------------- 1 | export interface IHotSinger { 2 | code: number; 3 | more: boolean; 4 | artists: Artist[]; 5 | } 6 | 7 | export interface Artist { 8 | name: string; 9 | id: number; 10 | picId: number; 11 | img1v1Id: number; 12 | briefDesc: string; 13 | picUrl: string; 14 | img1v1Url: string; 15 | albumSize: number; 16 | alias: string[]; 17 | trans: string; 18 | musicSize: number; 19 | topicPerson: number; 20 | showPrivateMsg?: any; 21 | isSubed?: any; 22 | accountId?: number; 23 | picId_str?: string; 24 | img1v1Id_str: string; 25 | transNames?: string[]; 26 | followed: boolean; 27 | mvSize?: any; 28 | publishTime?: any; 29 | identifyTag?: any; 30 | alg?: any; 31 | fansCount?: any; 32 | cover?: string; 33 | avatar?: string; 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/type/user.ts: -------------------------------------------------------------------------------- 1 | export interface IUserDetail { 2 | level: number; 3 | listenSongs: number; 4 | userPoint: UserPoint; 5 | mobileSign: boolean; 6 | pcSign: boolean; 7 | profile: Profile; 8 | peopleCanSeeMyPlayRecord: boolean; 9 | bindings: Binding[]; 10 | adValid: boolean; 11 | code: number; 12 | createTime: number; 13 | createDays: number; 14 | profileVillageInfo: ProfileVillageInfo; 15 | } 16 | 17 | export interface IUserFollow { 18 | followed: boolean; 19 | follows: boolean; 20 | nickname: string; 21 | avatarUrl: string; 22 | userId: number; 23 | gender: number; 24 | signature: string; 25 | backgroundUrl: string; 26 | vipType: number; 27 | userType: number; 28 | accountType: number; 29 | } 30 | 31 | interface ProfileVillageInfo { 32 | title: string; 33 | imageUrl?: any; 34 | targetUrl: string; 35 | } 36 | 37 | interface Binding { 38 | userId: number; 39 | url: string; 40 | expiresIn: number; 41 | refreshTime: number; 42 | bindingTime: number; 43 | tokenJsonStr?: any; 44 | expired: boolean; 45 | id: number; 46 | type: number; 47 | } 48 | 49 | interface Profile { 50 | avatarDetail?: any; 51 | userId: number; 52 | avatarImgIdStr: string; 53 | backgroundImgIdStr: string; 54 | description: string; 55 | vipType: number; 56 | userType: number; 57 | createTime: number; 58 | nickname: string; 59 | avatarUrl: string; 60 | experts: any; 61 | expertTags?: any; 62 | djStatus: number; 63 | accountStatus: number; 64 | birthday: number; 65 | gender: number; 66 | province: number; 67 | city: number; 68 | defaultAvatar: boolean; 69 | avatarImgId: number; 70 | backgroundImgId: number; 71 | backgroundUrl: string; 72 | mutual: boolean; 73 | followed: boolean; 74 | remarkName?: any; 75 | authStatus: number; 76 | detailDescription: string; 77 | signature: string; 78 | authority: number; 79 | followeds: number; 80 | follows: number; 81 | blacklist: boolean; 82 | eventCount: number; 83 | allSubscribedCount: number; 84 | playlistBeSubscribedCount: number; 85 | avatarImgId_str: string; 86 | followTime?: any; 87 | followMe: boolean; 88 | artistIdentity: any[]; 89 | cCount: number; 90 | sDJPCount: number; 91 | playlistCount: number; 92 | sCount: number; 93 | newFollows: number; 94 | } 95 | 96 | interface UserPoint { 97 | userId: number; 98 | balance: number; 99 | updateTime: number; 100 | version: number; 101 | status: number; 102 | blockBalance: number; 103 | } 104 | -------------------------------------------------------------------------------- /src/renderer/types/bilibili.ts: -------------------------------------------------------------------------------- 1 | export interface IBilibiliSearchResult { 2 | id: number; 3 | bvid: string; 4 | title: string; 5 | pic: string; 6 | duration: number | string; 7 | pubdate: number; 8 | ctime: number; 9 | author: string; 10 | view: number; 11 | danmaku: number; 12 | owner: { 13 | mid: number; 14 | name: string; 15 | face: string; 16 | }; 17 | stat: { 18 | view: number; 19 | danmaku: number; 20 | reply: number; 21 | favorite: number; 22 | coin: number; 23 | share: number; 24 | like: number; 25 | }; 26 | } 27 | 28 | export interface IBilibiliVideoDetail { 29 | aid: number; 30 | bvid: string; 31 | title: string; 32 | pic: string; 33 | desc: string; 34 | duration: number; 35 | pubdate: number; 36 | ctime: number; 37 | owner: { 38 | mid: number; 39 | name: string; 40 | face: string; 41 | }; 42 | stat: { 43 | view: number; 44 | danmaku: number; 45 | reply: number; 46 | favorite: number; 47 | coin: number; 48 | share: number; 49 | like: number; 50 | }; 51 | pages: IBilibiliPage[]; 52 | } 53 | 54 | export interface IBilibiliPage { 55 | cid: number; 56 | page: number; 57 | part: string; 58 | duration: number; 59 | dimension: { 60 | width: number; 61 | height: number; 62 | rotate: number; 63 | }; 64 | } 65 | 66 | export interface IBilibiliPlayUrl { 67 | durl?: { 68 | order: number; 69 | length: number; 70 | size: number; 71 | ahead: string; 72 | vhead: string; 73 | url: string; 74 | backup_url: string[]; 75 | }[]; 76 | dash?: { 77 | duration: number; 78 | minBufferTime: number; 79 | min_buffer_time: number; 80 | video: IBilibiliDashItem[]; 81 | audio: IBilibiliDashItem[]; 82 | }; 83 | support_formats: { 84 | quality: number; 85 | format: string; 86 | new_description: string; 87 | display_desc: string; 88 | }[]; 89 | accept_quality: number[]; 90 | accept_description: string[]; 91 | quality: number; 92 | format: string; 93 | timelength: number; 94 | high_format: string; 95 | } 96 | 97 | export interface IBilibiliDashItem { 98 | id: number; 99 | baseUrl: string; 100 | base_url: string; 101 | backupUrl: string[]; 102 | backup_url: string[]; 103 | bandwidth: number; 104 | mimeType: string; 105 | mime_type: string; 106 | codecs: string; 107 | width?: number; 108 | height?: number; 109 | frameRate?: string; 110 | frame_rate?: string; 111 | startWithSap?: number; 112 | start_with_sap?: number; 113 | codecid: number; 114 | } 115 | -------------------------------------------------------------------------------- /src/renderer/types/electron.d.ts: -------------------------------------------------------------------------------- 1 | export interface IElectronAPI { 2 | minimize: () => void; 3 | maximize: () => void; 4 | close: () => void; 5 | dragStart: (data: string) => void; 6 | miniTray: () => void; 7 | restart: () => void; 8 | openLyric: () => void; 9 | sendLyric: (data: string) => void; 10 | unblockMusic: (id: number) => Promise; 11 | onLanguageChanged: (callback: (locale: string) => void) => void; 12 | store: { 13 | get: (key: string) => Promise; 14 | set: (key: string, value: any) => Promise; 15 | delete: (key: string) => Promise; 16 | }; 17 | } 18 | 19 | declare global { 20 | interface Window { 21 | api: IElectronAPI; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/types/lyric.ts: -------------------------------------------------------------------------------- 1 | export interface LyricConfig { 2 | hideCover: boolean; 3 | centerLyrics: boolean; 4 | fontSize: number; 5 | letterSpacing: number; 6 | lineHeight: number; 7 | showTranslation: boolean; 8 | theme: 'default' | 'light' | 'dark'; 9 | hidePlayBar: boolean; 10 | pureModeEnabled: boolean; 11 | hideMiniPlayBar: boolean; 12 | hideLyrics: boolean; 13 | } 14 | 15 | export const DEFAULT_LYRIC_CONFIG: LyricConfig = { 16 | hideCover: false, 17 | centerLyrics: false, 18 | fontSize: 22, 19 | letterSpacing: 0, 20 | lineHeight: 2, 21 | showTranslation: true, 22 | theme: 'default', 23 | hidePlayBar: false, 24 | hideMiniPlayBar: true, 25 | pureModeEnabled: false, 26 | hideLyrics: false 27 | }; 28 | -------------------------------------------------------------------------------- /src/renderer/types/music.ts: -------------------------------------------------------------------------------- 1 | // 音乐平台类型 2 | export type Platform = 'qq' | 'migu' | 'kugou' | 'pyncmd' | 'joox' | 'kuwo' | 'bilibili' | 'gdmusic'; 3 | 4 | // 默认平台列表 5 | export const DEFAULT_PLATFORMS: Platform[] = ['migu', 'kugou', 'pyncmd', 'bilibili', 'kuwo']; -------------------------------------------------------------------------------- /src/renderer/utils/fileOperation.ts: -------------------------------------------------------------------------------- 1 | import type { MessageApi } from 'naive-ui'; 2 | 3 | /** 4 | * 选择目录 5 | * @param message MessageApi 实例 6 | * @returns Promise 返回选择的目录路径,如果取消则返回 undefined 7 | */ 8 | export const selectDirectory = async (message: MessageApi): Promise => { 9 | try { 10 | const result = await window.electron.ipcRenderer.invoke('select-directory'); 11 | if (result.filePaths?.[0]) { 12 | return result.filePaths[0]; 13 | } 14 | } catch (error) { 15 | message.error('选择目录失败'); 16 | } 17 | return undefined; 18 | }; 19 | 20 | /** 21 | * 打开目录 22 | * @param path 要打开的目录路径 23 | * @param message MessageApi 实例 24 | * @param showTip 是否显示提示信息 25 | */ 26 | export const openDirectory = (path: string | undefined, message: MessageApi, showTip = true) => { 27 | if (path) { 28 | window.electron.ipcRenderer.send('open-directory', path); 29 | } else if (showTip) { 30 | message.info('目录不存在'); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /src/renderer/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { useWindowSize } from '@vueuse/core'; 2 | import { computed } from 'vue'; 3 | 4 | import { useSettingsStore } from '@/store/modules/settings'; 5 | 6 | // 设置歌手背景图片 7 | export const setBackgroundImg = (url: String) => { 8 | return `background-image:url(${url})`; 9 | }; 10 | // 设置动画类型 11 | export const setAnimationClass = (type: String) => { 12 | const settingsStore = useSettingsStore(); 13 | if (settingsStore.setData && settingsStore.setData.noAnimate) { 14 | return ''; 15 | } 16 | const speed = settingsStore.setData?.animationSpeed || 1; 17 | 18 | let speedClass = ''; 19 | if (speed <= 0.3) speedClass = 'animate__slower'; 20 | else if (speed <= 0.8) speedClass = 'animate__slow'; 21 | else if (speed >= 2.5) speedClass = 'animate__faster'; 22 | else if (speed >= 1.5) speedClass = 'animate__fast'; 23 | 24 | return `animate__animated ${type}${speedClass ? ` ${speedClass}` : ''}`; 25 | }; 26 | // 设置动画延时 27 | export const setAnimationDelay = (index: number = 6, time: number = 50) => { 28 | const settingsStore = useSettingsStore(); 29 | if (settingsStore.setData?.noAnimate) { 30 | return ''; 31 | } 32 | const speed = settingsStore.setData?.animationSpeed || 1; 33 | return `animation-delay:${(index * time) / (speed * 2)}ms`; 34 | }; 35 | 36 | // 将秒转换为分钟和秒 37 | export const secondToMinute = (s: number) => { 38 | if (!s) { 39 | return '00:00'; 40 | } 41 | const minute: number = Math.floor(s / 60); 42 | const second: number = Math.floor(s % 60); 43 | const minuteStr: string = minute > 9 ? minute.toString() : `0${minute.toString()}`; 44 | const secondStr: string = second > 9 ? second.toString() : `0${second.toString()}`; 45 | return `${minuteStr}:${secondStr}`; 46 | }; 47 | 48 | // 格式化数字 千,万, 百万, 千万,亿 49 | const units = [ 50 | { value: 1e8, symbol: '亿' }, 51 | { value: 1e4, symbol: '万' } 52 | ]; 53 | 54 | export const formatNumber = (num: string | number) => { 55 | num = Number(num); 56 | for (let i = 0; i < units.length; i++) { 57 | if (num >= units[i].value) { 58 | return `${(num / units[i].value).toFixed(1)}${units[i].symbol}`; 59 | } 60 | } 61 | return num.toString(); 62 | }; 63 | 64 | export const getImgUrl = (url: string | undefined, size: string = '') => { 65 | if (!url) return ''; 66 | 67 | if (url.includes('thumbnail')) { 68 | // 只替换最后一个 thumbnail 参数的尺寸 69 | return url.replace(/thumbnail=\d+y\d+(?!.*thumbnail)/, `thumbnail=${size}`); 70 | } 71 | 72 | const imgUrl = `${url}?param=${size}`; 73 | return imgUrl; 74 | }; 75 | 76 | export const isMobile = computed(() => { 77 | const { width } = useWindowSize(); 78 | const userAgentFlag = navigator.userAgent.match( 79 | /(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i 80 | ); 81 | 82 | const isMobileWidth = width.value < 500; 83 | const isMobileDevice = !!userAgentFlag || isMobileWidth; 84 | 85 | const settingsStore = useSettingsStore(); 86 | settingsStore.isMobile = isMobileDevice; 87 | 88 | // 给html标签 添加或移除mobile类 89 | if (isMobileDevice) { 90 | document.documentElement.classList.add('mobile'); 91 | } else { 92 | document.documentElement.classList.add('pc'); 93 | document.documentElement.classList.remove('mobile'); 94 | } 95 | 96 | return isMobileDevice; 97 | }); 98 | 99 | export const isElectron = (window as any).electron !== undefined; 100 | 101 | export const isLyricWindow = computed(() => { 102 | return window.location.hash.includes('lyric'); 103 | }); 104 | 105 | export const getSetData = (): any => { 106 | let setData = null; 107 | if (window.electron) { 108 | setData = window.electron.ipcRenderer.sendSync('get-store-value', 'set'); 109 | } else { 110 | const settingsStore = useSettingsStore(); 111 | setData = settingsStore.setData; 112 | } 113 | return setData; 114 | }; 115 | -------------------------------------------------------------------------------- /src/renderer/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { InternalAxiosRequestConfig } from 'axios'; 2 | 3 | import { useUserStore } from '@/store/modules/user'; 4 | 5 | import { getSetData, isElectron, isMobile } from '.'; 6 | 7 | let setData: any = null; 8 | 9 | // 扩展请求配置接口 10 | interface CustomAxiosRequestConfig extends InternalAxiosRequestConfig { 11 | retryCount?: number; 12 | noRetry?: boolean; 13 | } 14 | 15 | const baseURL = window.electron 16 | ? `http://127.0.0.1:${setData?.musicApiPort}` 17 | : import.meta.env.VITE_API; 18 | 19 | const request = axios.create({ 20 | baseURL, 21 | timeout: 15000, 22 | withCredentials: true 23 | }); 24 | 25 | // 最大重试次数 26 | const MAX_RETRIES = 1; 27 | // 重试延迟(毫秒) 28 | const RETRY_DELAY = 500; 29 | 30 | // 请求拦截器 31 | request.interceptors.request.use( 32 | (config: CustomAxiosRequestConfig) => { 33 | setData = getSetData(); 34 | config.baseURL = window.electron 35 | ? `http://127.0.0.1:${setData?.musicApiPort}` 36 | : import.meta.env.VITE_API; 37 | // 只在retryCount未定义时初始化为0 38 | if (config.retryCount === undefined) { 39 | config.retryCount = 0; 40 | } 41 | 42 | // 在请求发送之前做一些处理 43 | // 在get请求params中添加timestamp 44 | config.params = { 45 | ...config.params, 46 | timestamp: Date.now(), 47 | device: isElectron ? 'pc' : isMobile ? 'mobile' : 'web' 48 | }; 49 | const token = localStorage.getItem('token'); 50 | if (token && config.method !== 'post') { 51 | config.params.cookie = config.params.cookie !== undefined ? config.params.cookie : token; 52 | } else if (token && config.method === 'post') { 53 | config.data = { 54 | ...config.data, 55 | cookie: token 56 | }; 57 | } 58 | if (isElectron) { 59 | const proxyConfig = setData?.proxyConfig; 60 | if (proxyConfig?.enable && ['http', 'https'].includes(proxyConfig?.protocol)) { 61 | config.params.proxy = `${proxyConfig.protocol}://${proxyConfig.host}:${proxyConfig.port}`; 62 | } 63 | if (setData.enableRealIP && setData.realIP) { 64 | config.params.realIP = setData.realIP; 65 | } 66 | } 67 | 68 | return config; 69 | }, 70 | (error) => { 71 | // 当请求异常时做一些处理 72 | return Promise.reject(error); 73 | } 74 | ); 75 | 76 | const NO_RETRY_URLS = ['暂时没有']; 77 | 78 | // 响应拦截器 79 | request.interceptors.response.use( 80 | (response) => { 81 | return response; 82 | }, 83 | async (error) => { 84 | console.error('error', error); 85 | const config = error.config as CustomAxiosRequestConfig; 86 | 87 | // 如果没有配置,直接返回错误 88 | if (!config) { 89 | return Promise.reject(error); 90 | } 91 | 92 | // 处理 301 状态码 93 | if (error.response?.status === 301 && config.params.noLogin !== true) { 94 | // 使用 store mutation 清除用户信息 95 | const userStore = useUserStore(); 96 | userStore.handleLogout(); 97 | console.log(`301 状态码,清除登录信息后重试第 ${config.retryCount} 次`); 98 | config.retryCount = 3; 99 | } 100 | 101 | // 检查是否还可以重试 102 | if ( 103 | config.retryCount !== undefined && 104 | config.retryCount < MAX_RETRIES && 105 | !NO_RETRY_URLS.includes(config.url as string) && 106 | !config.noRetry 107 | ) { 108 | config.retryCount++; 109 | console.error(`请求重试第 ${config.retryCount} 次`); 110 | 111 | // 延迟重试 112 | await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); 113 | 114 | // 重新发起请求 115 | return request(config); 116 | } 117 | 118 | console.error(`重试${MAX_RETRIES}次后仍然失败`); 119 | return Promise.reject(error); 120 | } 121 | ); 122 | 123 | export default request; 124 | -------------------------------------------------------------------------------- /src/renderer/utils/request_music.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | const baseURL = `${import.meta.env.VITE_API_MUSIC}`; 4 | const request = axios.create({ 5 | baseURL, 6 | timeout: 10000 7 | }); 8 | 9 | // 请求拦截器 10 | request.interceptors.request.use( 11 | (config) => { 12 | return config; 13 | }, 14 | (error) => { 15 | // 当请求异常时做一些处理 16 | return Promise.reject(error); 17 | } 18 | ); 19 | 20 | export default request; 21 | -------------------------------------------------------------------------------- /src/renderer/utils/shortcutToast.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, render } from 'vue'; 2 | 3 | import ShortcutToast from '@/components/ShortcutToast.vue'; 4 | 5 | let container: HTMLDivElement | null = null; 6 | let toastInstance: any = null; 7 | 8 | export function showShortcutToast(message: string, iconName: string) { 9 | // 如果容器不存在,创建一个新的容器 10 | if (!container) { 11 | container = document.createElement('div'); 12 | document.body.appendChild(container); 13 | } 14 | 15 | // 如果已经有实例,先销毁它 16 | if (toastInstance) { 17 | render(null, container); 18 | toastInstance = null; 19 | } 20 | 21 | // 创建新的 toast 实例 22 | const vnode = createVNode(ShortcutToast, { 23 | onDestroy: () => { 24 | if (container) { 25 | render(null, container); 26 | document.body.removeChild(container); 27 | container = null; 28 | } 29 | } 30 | }); 31 | 32 | // 渲染 toast 33 | render(vnode, container); 34 | toastInstance = vnode.component?.exposed; 35 | 36 | // 显示 toast 37 | if (toastInstance) { 38 | toastInstance.show(message, iconName); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/renderer/utils/theme.ts: -------------------------------------------------------------------------------- 1 | export type ThemeType = 'dark' | 'light'; 2 | 3 | // 应用主题 4 | export const applyTheme = (theme: ThemeType) => { 5 | // 使用 Tailwind 的暗色主题类 6 | if (theme === 'dark') { 7 | document.documentElement.classList.add('dark'); 8 | } else { 9 | document.documentElement.classList.remove('dark'); 10 | } 11 | 12 | // 保存主题到本地存储 13 | localStorage.setItem('theme', theme); 14 | }; 15 | 16 | // 获取当前主题 17 | export const getCurrentTheme = (): ThemeType => { 18 | return (localStorage.getItem('theme') as ThemeType) || 'light'; 19 | }; 20 | -------------------------------------------------------------------------------- /src/renderer/views/historyAndFavorite/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /src/renderer/views/home/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 33 | 34 | 64 | -------------------------------------------------------------------------------- /src/renderer/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/types/shortcuts.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 快捷键配置 3 | */ 4 | export interface ShortcutConfig { 5 | /** 快捷键字符串 */ 6 | key: string; 7 | /** 是否启用 */ 8 | enabled: boolean; 9 | /** 作用范围: global(全局) 或 app(仅应用内) */ 10 | scope: 'global' | 'app'; 11 | } 12 | 13 | /** 14 | * 快捷键配置集合 15 | */ 16 | export interface ShortcutsConfig { 17 | [key: string]: ShortcutConfig; 18 | } 19 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/renderer/index.html', './src/renderer/**/*.{vue,js,ts,jsx,tsx}'], 4 | darkMode: 'class', 5 | theme: { 6 | extend: { 7 | colors: { 8 | primary: { 9 | DEFAULT: '#000', 10 | light: '#fff', 11 | dark: '#000' 12 | }, 13 | secondary: { 14 | DEFAULT: '#6c757d', 15 | light: '#8c959e', 16 | dark: '#495057' 17 | }, 18 | dark: { 19 | DEFAULT: '#000', 20 | 100: '#161616', 21 | 200: '#2d2d2d', 22 | 300: '#3d3d3d' 23 | }, 24 | light: { 25 | DEFAULT: '#fff', 26 | 100: '#f8f9fa', 27 | 200: '#e9ecef', 28 | 300: '#dee2e6' 29 | } 30 | } 31 | } 32 | }, 33 | plugins: [] 34 | }; 35 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.node.json", 3 | "include": [ 4 | "electron.vite.config.*", 5 | "src/main/**/*", 6 | "src/preload/**/*", 7 | "src/i18n/**/*" 8 | ], 9 | "compilerOptions": { 10 | "composite": true, 11 | "types": [ 12 | "electron-vite/node" 13 | ], 14 | "moduleResolution": "bundler", 15 | } 16 | } -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@electron-toolkit/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "src/preload/*.d.ts", 5 | "src/renderer/**/*", 6 | "src/renderer/**/*.vue", 7 | "src/i18n/**/*", 8 | "src/main/modules/config.ts", 9 | "src/main/modules/shortcuts.ts" 10 | ], 11 | "compilerOptions": { 12 | "composite": true, 13 | "target": "esnext", 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "strict": true, 17 | "jsx": "preserve", 18 | "sourceMap": true, 19 | "skipLibCheck": true, 20 | "resolveJsonModule": true, 21 | "esModuleInterop": true, 22 | "baseUrl": ".", 23 | "types": [ 24 | "naive-ui/volar", 25 | "./auto-imports.d.ts", 26 | "./components.d.ts" 27 | ], 28 | "paths": { 29 | "@/*": ["src/renderer/*"], 30 | "@renderer/*": ["src/renderer/*"], 31 | "@main/*": ["src/main/*"] 32 | } 33 | } 34 | } 35 | --------------------------------------------------------------------------------