├── .dockerignore ├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc.cjs ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc-example ├── .prettierignore ├── .prettierrc.yaml ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE.txt ├── README.md ├── auto-imports.d.ts ├── components.d.ts ├── docker-compose.yml ├── electron-builder.yml ├── electron.vite.config.mjs ├── electron ├── index.html ├── main │ ├── index.js │ ├── mainIpcMain.js │ ├── startMainServer.js │ ├── startNcmServer.js │ └── utils │ │ ├── checkUpdates.js │ │ ├── createGlobalShortcut.js │ │ ├── createSystemTray.js │ │ ├── getNeteaseMusicUrl.js │ │ ├── kwDES.js │ │ └── readDirAsync.js └── preload │ └── index.mjs ├── index.html ├── nginx.conf ├── package.json ├── public ├── favicon.ico ├── favicon.png ├── font │ ├── font.min.css │ └── font_files │ │ ├── HarmonyOS_Regular.a.woff2 │ │ ├── HarmonyOS_Regular.a0.woff2 │ │ ├── HarmonyOS_Regular.a1.woff2 │ │ ├── HarmonyOS_Regular.aa.woff2 │ │ ├── HarmonyOS_Regular.ab.woff2 │ │ ├── HarmonyOS_Regular.ac.woff2 │ │ ├── HarmonyOS_Regular.ad.woff2 │ │ ├── HarmonyOS_Regular.ae.woff2 │ │ ├── HarmonyOS_Regular.af.woff2 │ │ ├── HarmonyOS_Regular.ag.woff2 │ │ ├── HarmonyOS_Regular.ah.woff2 │ │ ├── HarmonyOS_Regular.ai.woff2 │ │ ├── HarmonyOS_Regular.aj.woff2 │ │ ├── HarmonyOS_Regular.ak.woff2 │ │ ├── HarmonyOS_Regular.al.woff2 │ │ ├── HarmonyOS_Regular.am.woff2 │ │ ├── HarmonyOS_Regular.an.woff2 │ │ ├── HarmonyOS_Regular.ao.woff2 │ │ ├── HarmonyOS_Regular.ap.woff2 │ │ ├── HarmonyOS_Regular.aq.woff2 │ │ ├── HarmonyOS_Regular.ar.woff2 │ │ ├── HarmonyOS_Regular.as.woff2 │ │ ├── HarmonyOS_Regular.at.woff2 │ │ ├── HarmonyOS_Regular.au.woff2 │ │ ├── HarmonyOS_Regular.av.woff2 │ │ ├── HarmonyOS_Regular.aw.woff2 │ │ ├── HarmonyOS_Regular.ax.woff2 │ │ ├── HarmonyOS_Regular.ay.woff2 │ │ ├── HarmonyOS_Regular.az.woff2 │ │ ├── HarmonyOS_Regular.b.woff2 │ │ ├── HarmonyOS_Regular.c.woff2 │ │ ├── HarmonyOS_Regular.d.woff2 │ │ ├── HarmonyOS_Regular.e.woff2 │ │ ├── HarmonyOS_Regular.f.woff2 │ │ ├── HarmonyOS_Regular.g.woff2 │ │ ├── HarmonyOS_Regular.h.woff2 │ │ ├── HarmonyOS_Regular.i.woff2 │ │ ├── HarmonyOS_Regular.j.woff2 │ │ ├── HarmonyOS_Regular.k.woff2 │ │ ├── HarmonyOS_Regular.l.woff2 │ │ ├── HarmonyOS_Regular.m.woff2 │ │ ├── HarmonyOS_Regular.n.woff2 │ │ ├── HarmonyOS_Regular.o.woff2 │ │ ├── HarmonyOS_Regular.p.woff2 │ │ ├── HarmonyOS_Regular.q.woff2 │ │ ├── HarmonyOS_Regular.r.woff2 │ │ ├── HarmonyOS_Regular.s.woff2 │ │ ├── HarmonyOS_Regular.t.woff2 │ │ ├── HarmonyOS_Regular.u.woff2 │ │ ├── HarmonyOS_Regular.v.woff2 │ │ ├── HarmonyOS_Regular.w.woff2 │ │ ├── HarmonyOS_Regular.x.woff2 │ │ ├── HarmonyOS_Regular.y.woff2 │ │ └── HarmonyOS_Regular.z.woff2 └── images │ ├── icons │ ├── afdian.jpg │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-192x192.png │ ├── favicon-256x256.png │ ├── favicon-32x32.png │ ├── favicon-512x512.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── favicon.png │ ├── favicon.svg │ ├── logo-dark.png │ ├── logo-light.png │ ├── mstile-150x150.png │ ├── next-dark.png │ ├── next-light.png │ ├── open-dark.png │ ├── open-light.png │ ├── pause-dark.png │ ├── pause-light.png │ ├── play-dark.png │ ├── play-light.png │ ├── power-dark.png │ ├── power-light.png │ ├── prev-dark.png │ ├── prev-light.png │ ├── safari-pinned-tab.svg │ ├── setting-dark.png │ └── setting-light.png │ └── pic │ ├── album.jpg │ ├── album.png │ ├── artist.jpg │ ├── avatar.jpg │ ├── cover-2.jpg │ ├── cover.jpg │ ├── default.jpg │ ├── hot-list.png │ ├── like.jpg │ ├── pl-1.jpg │ ├── pl-2.jpg │ ├── pl-3.jpg │ ├── pl-4.jpg │ ├── pointer.png │ ├── radar-private.jpg │ ├── radar-treasure.jpg │ ├── record.png │ ├── song.jpg │ ├── video.png │ └── vip.png ├── screenshots ├── TuneFree - 主页面.jpg ├── TuneFree - 发现页面.jpg ├── TuneFree - 播放页面.jpg ├── TuneFree - 本地音乐.jpg ├── TuneFree - 歌单页面.jpg ├── TuneFree - 评论页面.jpg ├── TuneFree-播放.png ├── TuneFree-首页.png ├── TuneFree.jpg └── build.png ├── src ├── App.vue ├── api │ ├── album.js │ ├── artist.js │ ├── cloud.js │ ├── comment.js │ ├── dj.js │ ├── login.js │ ├── other.js │ ├── playlist.js │ ├── recommend.js │ ├── search.js │ ├── song.js │ ├── update.js │ ├── user.js │ └── video.js ├── assets │ ├── emoji.json │ ├── icon.json │ ├── idMeta.json │ └── themeColor.json ├── components │ ├── Cover │ │ ├── CoverDropdown.vue │ │ ├── CoverPlayBtn.vue │ │ ├── MainCover.vue │ │ ├── SpecialCover.vue │ │ └── SpecialCoverCard.vue │ ├── Global │ │ ├── MainLayout.vue │ │ ├── Menu.vue │ │ ├── Pagination.vue │ │ ├── Playlist.vue │ │ ├── Provider.vue │ │ └── SvgIcon.vue │ ├── List │ │ ├── CommentList.vue │ │ ├── SongList.vue │ │ ├── SongListDrawer.vue │ │ └── SongListDropdown.vue │ ├── Modal │ │ ├── AddPlaylist.vue │ │ ├── CloudSongMatch.vue │ │ ├── CreatePlaylist.vue │ │ ├── DownloadSong.vue │ │ ├── Login.vue │ │ ├── LoginPhone.vue │ │ ├── LoginQRCode.vue │ │ ├── PlaylistUpdate.vue │ │ └── UpCloudSong.vue │ ├── Nav │ │ ├── MainNav.vue │ │ └── UserData.vue │ ├── Player │ │ ├── CountDown.vue │ │ ├── FullPlayer.vue │ │ ├── Lyric.vue │ │ ├── MainControl.vue │ │ ├── PlayerControl.vue │ │ ├── PlayerCover.vue │ │ ├── PrivateFm.vue │ │ └── Spectrum.vue │ ├── Search │ │ ├── SearchDropdown.vue │ │ ├── SearchHot.vue │ │ ├── SearchInp.vue │ │ └── SearchSuggestions.vue │ └── WinDom │ │ └── TitleBar.vue ├── main.js ├── router │ ├── index.js │ └── routes.js ├── stores │ ├── index.js │ ├── indexedDB.js │ ├── musicData.js │ ├── siteData.js │ ├── siteSettings.js │ └── siteStatus.js ├── style │ ├── animate.scss │ └── main.scss ├── utils │ ├── Player.js │ ├── auth.js │ ├── base64.js │ ├── color-utils.js │ ├── cover-color.js │ ├── debounce.js │ ├── formRules.js │ ├── formatData.js │ ├── globalEvents.js │ ├── globalShortcut.js │ ├── helper.js │ ├── parseLyric.js │ ├── request.js │ ├── throttle.js │ ├── timeTools.js │ └── userSignIn.js └── views │ ├── Artist │ ├── albums.vue │ ├── hot.vue │ ├── index.vue │ ├── songs.vue │ └── videos.vue │ ├── Cloud.vue │ ├── Comment.vue │ ├── DailySongs.vue │ ├── DesktopLyric.vue │ ├── Discover │ ├── artists.vue │ ├── index.vue │ ├── new.vue │ ├── playlists.vue │ └── toplists.vue │ ├── Dj │ ├── index.vue │ └── type.vue │ ├── History.vue │ ├── Home.vue │ ├── Like │ ├── albums.vue │ ├── artists.vue │ ├── djs.vue │ ├── index.vue │ ├── playlists.vue │ └── videos.vue │ ├── List │ ├── album.vue │ ├── dj.vue │ └── playlist.vue │ ├── Local │ ├── albums.vue │ ├── artists.vue │ ├── index.vue │ └── songs.vue │ ├── Player.vue │ ├── Search │ ├── albums.vue │ ├── artists.vue │ ├── djs.vue │ ├── index.vue │ ├── playlists.vue │ ├── songs.vue │ └── videos.vue │ ├── Setting │ └── index.vue │ ├── Song.vue │ ├── State │ ├── 403.vue │ ├── 404.vue │ └── 500.vue │ └── Test.vue └── vercel.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile* 4 | docker-compose* 5 | .dockerignore 6 | .git 7 | .github 8 | .gitignore 9 | README.md 10 | LICENSE 11 | .vscode 12 | dist 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 根配置文件 2 | ## 编辑器在查找配置时会停止查找更高层次的配置文件 3 | root = true 4 | 5 | # 通配符,匹配所有文件 6 | [*] 7 | # 设置字符集为 UTF-8,确保文件中的文本使用 UTF-8 编码 8 | charset = utf-8 9 | # 使用空格作为缩进风格 10 | indent_style = space 11 | # 设置每个缩进级别的空格数量为 2 12 | indent_size = 2 13 | # 设置行尾换行符为LF(Line Feed) 14 | end_of_line = lf 15 | # 在文件的末尾插入一个新行 16 | insert_final_newline = true 17 | # 删除每一行末尾的尾随空格 18 | trim_trailing_whitespace = true 19 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 程序配置 2 | ## 程序名称 3 | MAIN_VITE_TITLE = "TuneFree" 4 | ## 程序主端口 5 | MAIN_VITE_MAIN_PORT = 7899 6 | ## 程序开发环境运行端口 7 | MAIN_VITE_DEV_PORT = 6499 8 | 9 | # 全局 API 配置 10 | ## API 运行地址 11 | MAIN_VITE_SERVER_HOST = 127.0.0.1 12 | ## API 运行端口 13 | MAIN_VITE_SERVER_PORT = 11451 14 | ## API 在线地址( 网址结尾不要加 / ) 15 | ### 用于非客户端( 浏览器环境 ) 16 | RENDERER_VITE_SERVER_URL = /api 17 | 18 | # 程序信息 19 | RENDERER_VITE_SITE_TITLE = "tunefree" 20 | RENDERER_VITE_SITE_ANTHOR = "是青旨啊" 21 | RENDERER_VITE_SITE_KEYWORDS = "tunefree,云音乐,播放器,在线音乐,在线播放器,音乐播放器" 22 | RENDERER_VITE_SITE_DES = "一个简约的在线音乐播放器,具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能" 23 | RENDERER_VITE_SITE_URL = "https://tunefree.fun" 24 | 25 | # Cookie 26 | ## 咪咕音乐 Cookie 27 | 28 | # 公告配置 29 | ## 若无需公告,请将标题或内容任意一项设为空即可 30 | ## 公告类型 31 | RENDERER_VITE_ANN_TYPE = "info" 32 | ## 公告标题 33 | RENDERER_VITE_ANN_TITLE = "" 34 | ## 公告内容 35 | RENDERER_VITE_ANN_CONTENT = "" 36 | ## 公告时长(毫秒)不可超过 999999 37 | RENDERER_VITE_ANN_DURATION = 8000 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | out 4 | .gitignore 5 | auto-imports.d.ts 6 | components.d.ts 7 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution"); 3 | 4 | module.exports = { 5 | extends: [ 6 | "eslint:recommended", 7 | "plugin:vue/vue3-recommended", 8 | "@electron-toolkit", 9 | "@vue/eslint-config-prettier", 10 | ], 11 | rules: { 12 | "vue/v-on-event-hyphenation": "off", 13 | "vue/require-default-prop": "off", 14 | "vue/multi-word-component-names": "off", 15 | "vue/attribute-hyphenation": "off", 16 | }, 17 | ignorePatterns: [ 18 | "node_modules/", 19 | "build/", 20 | "dist/", 21 | "out/", 22 | "components.d.ts", 23 | "auto-imports.d.ts", 24 | ], 25 | globals: { 26 | defineProps: true, 27 | defineEmits: true, 28 | withDefaults: true, 29 | h: true, 30 | vue: true, 31 | ref: true, 32 | reactive: true, 33 | computed: true, 34 | watch: true, 35 | provide: true, 36 | inject: true, 37 | defineComponent: true, 38 | onBeforeMount: true, 39 | onBeforeUnmount: true, 40 | onUnmounted: true, 41 | onMounted: true, 42 | nextTick: true, 43 | watchEffect: true, 44 | electron: true, 45 | $message: true, 46 | $dialog: true, 47 | $loadingBar: true, 48 | $changeLogin: true, 49 | $notification: true, 50 | $changeThemeColor: true, 51 | $canNotConnect: true, 52 | $refreshCloudCatch: true, 53 | $cleanAll: true, 54 | $player: true, 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | out 17 | .npmrc 18 | 19 | 20 | /cypress/videos/ 21 | /cypress/screenshots/ 22 | 23 | # Editor directories and files 24 | .vscode/* 25 | .vscode 26 | !.vscode/extensions.json 27 | .idea 28 | *.suo 29 | *.ntvs* 30 | *.njsproj 31 | *.sln 32 | *.sw? 33 | -------------------------------------------------------------------------------- /.npmrc-example: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com 2 | disturl=https://registry.npmmirror.com/-/binary/node 3 | # ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ 4 | ELECTRON_MIRROR=https://registry.npmmirror.com/-/binary/electron/ 5 | shamefully-hoist=true 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | pnpm-lock.yaml 4 | LICENSE.md 5 | tsconfig.json 6 | tsconfig.*.json 7 | auto-imports.d.ts 8 | components.d.ts 9 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | # 是否使用单引号而不是双引号 2 | singleQuote: false 3 | # 是否在语句末尾使用分号 4 | semi: true 5 | # 每行的最大打印宽度 6 | printWidth: 100 7 | # 是否在对象和数组的末尾加上逗号 8 | trailingComma: all 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "githubPullRequests.ignoredPullRequestBranches": [ 3 | "master" 4 | ] 5 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # build 2 | FROM node:18-alpine as builder 3 | 4 | RUN apk update && apk add --no-cache git 5 | 6 | WORKDIR /app 7 | 8 | COPY package*.json ./ 9 | 10 | RUN npm install 11 | 12 | COPY . . 13 | 14 | RUN [ ! -e ".env" ] && cp .env.example .env || true 15 | 16 | RUN npm run build 17 | 18 | # nginx 19 | FROM nginx:1.25.3-alpine-slim as app 20 | 21 | COPY --from=builder /app/out/renderer /usr/share/nginx/html 22 | 23 | COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf 24 | 25 | RUN apk add --no-cache npm 26 | 27 | RUN npm install -g NeteaseCloudMusicApi 28 | 29 | CMD nginx && npx NeteaseCloudMusicApi 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # TuneFree 3 | 4 |

教你一点礼仪,别他妈跑到别人作品下面瞎几把吹说TuneFree比你做的好,别他妈吹牛逼了,我也是二开的别人软件,没啥技术含量,别tm到处引战给我丢脸了

5 | 6 | 7 | > 随心所欲,音乐无界 - 音遇自由 8 | 9 | > [!IMPORTANT] 10 | > 11 | > 由于 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 项目已停止维护,由于接口状态的不可确定性,将无法保障功能的正常使用,本项目将会停止新功能的开发,进入无限期停更状态。本项目基于开源项目[SPlayer](https://github.com/imsyy/SPlayer)进行开发,故产生的一切责任都与SPlayer以及他的开发者无关! 12 | > 13 | > Tips:该文档大部分内容引用了SPlayer仓库中的README.md 14 | 15 | ![main](/screenshots/TuneFree.jpg) 16 | 17 | 18 | ## 👀 Demo 19 | 20 | - [TuneFree网页端](https://pt.sayqz.com) 21 | - [TuneFree for Windows](https://sayqz.lanpv.com/iN56k1o4ms3i) 22 | - [TuneFree for Android](https://sayqz.lanpv.com/ipru71xs00qj) 23 | 24 | ## 🎉 功能 25 | 26 | - 🎵 **支持播放网易云音乐所有的付费资源** 27 | - 🆕 软件内检查更新 28 | - 🤳 独立Android客户端 29 | - ✨ 支持扫码登录 30 | - 📱 支持手机号登录 31 | - 📅 自动进行每日签到及云贝签到 32 | - 🎨 封面主题色自适应 33 | - 📁 本地歌曲管理及分类(建议先使用 [音乐标签](https://www.cnblogs.com/vinlxc/p/11347744.html) 进行匹配后再使用) 34 | - ⬇️ 下载歌曲(最高支持 超清母带) 35 | - ➕ 新建歌单及歌单编辑 36 | - ❤️ 收藏 / 取消收藏歌单或歌手 37 | - 🎶 每日推荐歌曲 38 | - 📻 私人 FM 39 | - ☁️ 云盘音乐上传 40 | - 📂 云盘内歌曲播放 41 | - 🔄 云盘内歌曲纠正 42 | - 🗑️ 云盘歌曲删除 43 | - 📝 支持逐字歌词 44 | - 🔄 歌词滚动以及歌词翻译 45 | - 📹 MV 与视频播放 46 | - 🎶 音乐频谱显示 47 | - ⏭️ 音乐渐入渐出 48 | - 🔄 支持 PWA 49 | - 💬 支持评论区及评论点赞 50 | - 🌓 明暗模式自动 / 手动切换 51 | - 📱 移动端基础适配 52 | 53 | 54 | ## 🖼️ 预览 55 | 56 | > 开发中,仅供参考 57 | 58 |
59 | 主页面 60 | 61 | ![主页面](/screenshots/TuneFree%20-%20主页面.jpg) 62 | 63 |
64 | 65 |
66 | 播放页面 67 | 68 | ![播放页面](/screenshots/TuneFree%20-%20播放页面.jpg) 69 | 70 |
71 | 72 |
73 | 发现页面 74 | 75 | ![发现页面](/screenshots/TuneFree%20-%20发现页面.jpg) 76 | 77 |
78 | 79 |
80 | 歌单页面 81 | 82 | ![发现页面](/screenshots/TuneFree%20-%20歌单页面.jpg) 83 | 84 |
85 | 86 |
87 | 评论页面 88 | 89 | ![发现页面](/screenshots/TuneFree%20-%20评论页面.jpg) 90 | 91 |
92 | 93 |
94 | 本地音乐 95 | 96 | ![发现页面](/screenshots/TuneFree%20-%20本地音乐.jpg) 97 | 98 |
99 | 100 | ## 📦️ 获取 101 | ### TuneFree 102 | 通常情况下,可以在蓝奏云中获取到TuneFree各版本安装包 103 | - [蓝奏云](https://sayqz.lanpv.com/s/tunefree) 104 | 105 | ## ⚙️ 本地部署 106 | 107 | 1. 本地部署需要用到 `Node.js`。可前往 [Node.js 官网](https://nodejs.org/zh-cn/) 下载安装包,请下载最新稳定版 108 | 2. 安装 pnpm、electron-builder 109 | 110 | ```bash 111 | npm install pnpm electron-builder -g 112 | ``` 113 | 114 | 3. 克隆仓库并拉取至本地,此处不再赘述 115 | 4. 使用 `pnpm install` 安装项目依赖(若安装过程中遇到网络错误,请使用国内镜像源替代,此处不再赘述) 116 | 5. 复制 `/.env.example` 文件并重命名为 `/.env` 并修改配置 117 | 6. 打包客户端,请依据你的系统类型来选择,打包成功后,会输出安装包或可执行文件在 `/dist` 目录中,可自行安装 118 | 119 | | 命令 | 系统类型 | 120 | | ------------------ | -------- | 121 | | `pnpm build:win` | Windows | 122 | | `pnpm build:linux` | Linux | 123 | | `pnpm build:mac` | MacOS | 124 | 125 | ## 📢 免责声明 126 | 127 | 再次声明,本项目基于开源项目SPlayer进行二次开发,故产生的一切责任都与SPlayer以及他的开发者无关!,仅供个人学习研究使用,禁止用于商业及非法用途。同时,本项目开发者承诺 没有任何破解网易音乐等相关情况,只是使用了我自己会员账号的Cookie进行了请求,请求使用的链接为网易云音乐官方的API 128 | 129 | 130 | ## 📜 开源许可 131 | - 原项目基于[GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html)许可进行开源 132 | - 以下是来自原项目的开源许可 133 | 1. **修改和分发:** 任何对本项目的修改和分发都必须基于 AGPL-3.0 进行,源代码必须一并提供 134 | 2. **派生作品:** 任何派生作品必须同样采用 AGPL-3.0,并在适当的地方注明原始项目的许可证 135 | 3. **注明原作者:** 在任何修改、派生作品或其他分发中,必须在适当的位置明确注明原作者及其贡献 136 | 4. **免责声明:** 根据 AGPL-3.0,本项目不提供任何明示或暗示的担保。请详细阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 以了解完整的免责声明内容 137 | 5. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目 138 | 6. **许可证链接:** 请阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 了解更多详情 139 | - TuneFree将遵守该开源许可,感谢SPlayer这一优秀的项目!更多详细说明请前往[SPlayer](https://github.com/imsyy/SPlayer)仓库了解! 140 | 141 | 142 | ## ⭐ Star History 143 | 144 | [![Star History Chart](https://api.star-history.com/svg?repos=GSQZ/TuneFree&type=Date)](https://star-history.com/#GSQZ/TuneFree&Date) 145 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // noinspection JSUnusedGlobalSymbols 5 | // Generated by unplugin-auto-import 6 | export {} 7 | declare global { 8 | const EffectScope: typeof import('vue')['EffectScope'] 9 | const computed: typeof import('vue')['computed'] 10 | const createApp: typeof import('vue')['createApp'] 11 | const customRef: typeof import('vue')['customRef'] 12 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 13 | const defineComponent: typeof import('vue')['defineComponent'] 14 | const effectScope: typeof import('vue')['effectScope'] 15 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 16 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 17 | const h: typeof import('vue')['h'] 18 | const inject: typeof import('vue')['inject'] 19 | const isProxy: typeof import('vue')['isProxy'] 20 | const isReactive: typeof import('vue')['isReactive'] 21 | const isReadonly: typeof import('vue')['isReadonly'] 22 | const isRef: typeof import('vue')['isRef'] 23 | const markRaw: typeof import('vue')['markRaw'] 24 | const nextTick: typeof import('vue')['nextTick'] 25 | const onActivated: typeof import('vue')['onActivated'] 26 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 27 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 28 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 29 | const onDeactivated: typeof import('vue')['onDeactivated'] 30 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 31 | const onMounted: typeof import('vue')['onMounted'] 32 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 33 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 34 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 35 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 36 | const onUnmounted: typeof import('vue')['onUnmounted'] 37 | const onUpdated: typeof import('vue')['onUpdated'] 38 | const provide: typeof import('vue')['provide'] 39 | const reactive: typeof import('vue')['reactive'] 40 | const readonly: typeof import('vue')['readonly'] 41 | const ref: typeof import('vue')['ref'] 42 | const resolveComponent: typeof import('vue')['resolveComponent'] 43 | const shallowReactive: typeof import('vue')['shallowReactive'] 44 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 45 | const shallowRef: typeof import('vue')['shallowRef'] 46 | const toRaw: typeof import('vue')['toRaw'] 47 | const toRef: typeof import('vue')['toRef'] 48 | const toRefs: typeof import('vue')['toRefs'] 49 | const toValue: typeof import('vue')['toValue'] 50 | const triggerRef: typeof import('vue')['triggerRef'] 51 | const unref: typeof import('vue')['unref'] 52 | const useAttrs: typeof import('vue')['useAttrs'] 53 | const useCssModule: typeof import('vue')['useCssModule'] 54 | const useCssVars: typeof import('vue')['useCssVars'] 55 | const useDialog: typeof import('naive-ui')['useDialog'] 56 | const useLoadingBar: typeof import('naive-ui')['useLoadingBar'] 57 | const useMessage: typeof import('naive-ui')['useMessage'] 58 | const useNotification: typeof import('naive-ui')['useNotification'] 59 | const useSlots: typeof import('vue')['useSlots'] 60 | const watch: typeof import('vue')['watch'] 61 | const watchEffect: typeof import('vue')['watchEffect'] 62 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 63 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 64 | } 65 | // for type re-export 66 | declare global { 67 | // @ts-ignore 68 | export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue' 69 | import('vue') 70 | } 71 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | SPlayer: 3 | build: 4 | context: . 5 | image: splayer 6 | container_name: SPlayer 7 | volumes: 8 | - /etc/localtime:/etc/localtime:ro 9 | - /etc/timezone:/etc/timezone:ro 10 | ports: 11 | - 7899:7899 12 | restart: always 13 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | # 应用程序的唯一标识符 2 | appId: com.sayqz.tunefree 3 | # 应用程序的产品名称 4 | productName: TuneFree 5 | copyright: Copyright © Sayqz 2024 6 | # 构建资源所在的目录 7 | directories: 8 | buildResources: build 9 | # 包含在最终应用程序构建中的文件列表,这里使用通配符 ! 表示排除不需要的文件 10 | files: 11 | - "!**/.vscode/*" 12 | - "!src/*" 13 | - "!electron.vite.config.{js,ts,mjs,cjs}" 14 | - "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}" 15 | - "!{.env,.env.*,.npmrc,pnpm-lock.yaml}" 16 | # 哪些文件将不会被压缩,而是解压到构建目录 17 | asarUnpack: 18 | - public/** 19 | # Windows 平台配置 20 | win: 21 | # 可执行文件名 22 | executableName: TuneFree 23 | # 应用程序的图标文件路径 24 | icon: public/images/icons/favicon-512x512.png 25 | # 构建类型 26 | target: nsis 27 | # NSIS 安装器配置 28 | nsis: 29 | # 一键式安装程序还是辅助安装程序 30 | oneClick: false 31 | # 安装程序的生成名称 32 | artifactName: ${productName}-${version}-${arch}-setup.${ext} 33 | # 创建的桌面快捷方式名称 34 | shortcutName: ${productName} 35 | # 卸载时显示的名称 36 | uninstallDisplayName: ${productName} 37 | # 创建桌面图标 38 | createDesktopShortcut: always 39 | # 是否允许 UAC 提升权限 40 | allowElevation: true 41 | # 是否允许用户更改安装目录 42 | allowToChangeInstallationDirectory: true 43 | # 安装包图标 44 | installerIcon: public/images/icons/favicon.ico 45 | # 卸载命令图标 46 | uninstallerIcon: public/images/icons/favicon.ico 47 | # macOS 平台配置 48 | mac: 49 | # 可执行文件名 50 | executableName: TuneFree 51 | # 应用程序的图标文件路径 52 | icon: public/images/icons/favicon-512x512.png 53 | # 权限继承的文件路径 54 | entitlementsInherit: build/entitlements.mac.plist 55 | # 扩展信息,如权限描述 56 | extendInfo: 57 | - NSCameraUsageDescription: Application requests access to the device's camera. 58 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 59 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 60 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 61 | # 是否启用应用程序的 Notarization(苹果的安全审核) 62 | notarize: false 63 | darkModeSupport: true 64 | category: public.app-category.music 65 | # macOS 平台的 DMG 配置 66 | dmg: 67 | # DMG 文件的生成名称 68 | artifactName: ${productName}-${version}.${ext} 69 | # Linux 平台配置 70 | linux: 71 | # 可执行文件名 72 | executableName: TuneFree 73 | # 应用程序的图标文件路径 74 | icon: public/images/icons/favicon-512x512.png 75 | # 构建类型 76 | target: 77 | - AppImage 78 | - deb 79 | - rpm 80 | - tar.gz 81 | # 维护者信息 82 | maintainer: imsyy.top 83 | # 应用程序类别 84 | category: Audio;Music 85 | artifactName: ${productName}-${version}-${arch}.${ext} 86 | # 是否在构建之前重新编译原生模块 87 | npmRebuild: false 88 | -------------------------------------------------------------------------------- /electron/index.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/electron/index.html -------------------------------------------------------------------------------- /electron/main/startMainServer.js: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import express from "express"; 3 | import expressProxy from "express-http-proxy"; 4 | 5 | /** 6 | * 启动主服务器 7 | * @returns {import('http').Server} HTTP 服务器实例 8 | */ 9 | export const startMainServer = async () => { 10 | const { MAIN_VITE_MAIN_PORT, MAIN_VITE_SERVER_HOST, MAIN_VITE_SERVER_PORT } = import.meta.env; 11 | const port = MAIN_VITE_MAIN_PORT ?? 7899; 12 | const apiHost = `http://${MAIN_VITE_SERVER_HOST}:${MAIN_VITE_SERVER_PORT}`; 13 | const expressApp = express(); 14 | // 代理 15 | expressApp.use("/", express.static(join(__dirname, "../renderer/"))); 16 | expressApp.use("/api", expressProxy(apiHost)); 17 | // 启动 Express 应用服务器,并监听指定端口 18 | return expressApp.listen(port, "127.0.0.1"); 19 | }; 20 | -------------------------------------------------------------------------------- /electron/main/startNcmServer.js: -------------------------------------------------------------------------------- 1 | import netEaseApi from "NeteaseCloudMusicApi"; 2 | 3 | /** 4 | * 启动网易云音乐 API 服务器 5 | * 6 | * @async 7 | * @param {Object} options - 服务器配置 8 | * @param {number} [options.port=11451] - 服务器端口 9 | * @param {string} [options.host="127.0.0.1"] - 服务器主机地址 10 | * @returns {Promise} 返回一个 Promise,在 API 服务器成功启动后 resolve 11 | */ 12 | export const startNcmServer = async ( 13 | options = { 14 | port: 11451, 15 | host: "127.0.0.1", 16 | }, 17 | ) => { 18 | console.log(options); 19 | return await netEaseApi.serveNcmApi(options); 20 | }; 21 | -------------------------------------------------------------------------------- /electron/main/utils/checkUpdates.js: -------------------------------------------------------------------------------- 1 | import { is } from "@electron-toolkit/utils"; 2 | 3 | // 导出一个空函数,或者根本不导出 4 | export const configureAutoUpdater = () => { 5 | if (is.dev) return false; 6 | 7 | // 自动更新代码已被移除 8 | console.log("自动更新已被禁用"); 9 | 10 | // 这里可以包含应用程序启动时需要执行的其他初始化代码 11 | // ... 12 | 13 | return true; 14 | }; 15 | -------------------------------------------------------------------------------- /electron/main/utils/createGlobalShortcut.js: -------------------------------------------------------------------------------- 1 | import { globalShortcut } from "electron"; 2 | 3 | /** 4 | * 注册全局快捷键 5 | * @param {BrowserWindow} win - 程序窗口 6 | */ 7 | const createGlobalShortcut = (win) => { 8 | 9 | //全局播放控制 10 | globalShortcut.register("CmdOrCtrl+Alt+P", () => { 11 | win.webContents.send("playOrPause"); 12 | }); 13 | 14 | //全局下一首 15 | globalShortcut.register("CmdOrCtrl+Alt+Right", () => { 16 | win.webContents.send("playNextOrPrev", "next"); 17 | }); 18 | 19 | //全局上一首 20 | globalShortcut.register("CmdOrCtrl+Alt+Left", () => { 21 | win.webContents.send("playNextOrPrev", "prev"); 22 | }); 23 | 24 | // 刷新程序 25 | globalShortcut.register("CmdOrCtrl+Shift+R", () => { 26 | if (win && win.isFocused()) win?.reload(); 27 | }); 28 | 29 | // 打开开发者工具 30 | globalShortcut.register("CmdOrCtrl+Shift+I", () => { 31 | if (win && win.isFocused()) { 32 | win?.webContents.openDevTools({ 33 | mode: "right", 34 | activate: true, 35 | }); 36 | } 37 | }); 38 | }; 39 | 40 | export default createGlobalShortcut; 41 | -------------------------------------------------------------------------------- /electron/main/utils/createSystemTray.js: -------------------------------------------------------------------------------- 1 | import { Tray, Menu, app, ipcMain, nativeImage, nativeTheme } from "electron"; 2 | import packageJson from "/package.json"; 3 | import { join } from "path"; 4 | 5 | // 当前歌曲数据 6 | let playSongName = "当前暂无播放歌曲"; 7 | let playSongState = false; 8 | 9 | /** 10 | * 创建系统托盘 11 | * @param {BrowserWindow} win - 程序窗口 12 | */ 13 | const createSystemTray = (win) => { 14 | // 系统托盘 15 | const mainTray = new Tray( 16 | nativeImage 17 | .createFromPath( 18 | join( 19 | __dirname, 20 | process.platform === "win32" 21 | ? "../../public/images/icons/favicon.ico" 22 | : "../../public/images/icons/favicon-32x32.png", 23 | ), 24 | ) 25 | .resize({ 26 | height: 32, 27 | width: 32, 28 | }), 29 | ); 30 | // 应用内菜单 31 | Menu.setApplicationMenu(createTrayMenu(win)); 32 | // 默认名称 33 | win.setTitle(app.getName()); 34 | mainTray.setTitle(app.getName()); 35 | mainTray.setToolTip(app.getName()); 36 | // 左键事件 37 | mainTray.on("click", () => win.show()); 38 | // 托盘菜单 39 | mainTray.setContextMenu(createTrayMenu(win)); 40 | // 系统主题改变 41 | nativeTheme.on("updated", () => { 42 | mainTray.setContextMenu(createTrayMenu(win)); 43 | }); 44 | // 播放歌曲改变 45 | ipcMain.on("songNameChange", (_, val) => { 46 | playSongName = val; 47 | win.setTitle(val); 48 | mainTray.setTitle(val); 49 | mainTray.setToolTip(val); 50 | mainTray.setContextMenu(createTrayMenu(win)); 51 | }); 52 | // 播放状态改变 53 | ipcMain.on("songStateChange", (_, val) => { 54 | playSongState = val; 55 | mainTray.setContextMenu(createTrayMenu(win)); 56 | }); 57 | }; 58 | 59 | // 生成图标 60 | const createIcon = (name) => { 61 | // 系统是否为暗色 62 | const isDarkMode = nativeTheme.shouldUseDarkColors; 63 | // 返回图标 64 | return nativeImage 65 | .createFromPath( 66 | isDarkMode 67 | ? join(__dirname, `../../public/images/icons/${name}-dark.png`) 68 | : join(__dirname, `../../public/images/icons/${name}-light.png`), 69 | ) 70 | .resize({ width: 16, height: 16 }); 71 | }; 72 | // 生成右键菜单 73 | const createTrayMenu = (win) => { 74 | // 返回菜单 75 | return Menu.buildFromTemplate([ 76 | { 77 | label: `TuneFree ${packageJson.version}`, 78 | icon: createIcon("logo"), 79 | accelerator: "Esc", 80 | click() { 81 | win.webContents.send("closePlayer"); 82 | }, 83 | }, 84 | { 85 | label: playSongName, 86 | icon: createIcon("open"), 87 | click() { 88 | win.show(); 89 | win.focus(); 90 | win.webContents.send("showPlayer"); 91 | }, 92 | }, 93 | { 94 | type: "separator", 95 | }, 96 | { 97 | label: "上一曲", 98 | icon: createIcon("prev"), 99 | accelerator: "CmdOrCtrl+Left", 100 | click: () => { 101 | win.webContents.send("playNextOrPrev", "prev"); 102 | }, 103 | }, 104 | { 105 | label: playSongState ? "暂停" : "播放", 106 | icon: createIcon(playSongState ? "pause" : "play"), 107 | accelerator: "Space", 108 | click: () => { 109 | win.webContents.send("playOrPause"); 110 | }, 111 | }, 112 | { 113 | label: "下一曲", 114 | icon: createIcon("next"), 115 | accelerator: "CmdOrCtrl+Right", 116 | click: () => { 117 | win.webContents.send("playNextOrPrev", "next"); 118 | }, 119 | }, 120 | { 121 | type: "separator", 122 | }, 123 | { 124 | label: "全局设置", 125 | icon: createIcon("setting"), 126 | click: () => { 127 | win.show(); 128 | win.focus(); 129 | win.webContents.send("open-setting"); 130 | }, 131 | }, 132 | { 133 | type: "separator", 134 | }, 135 | { 136 | label: "退出", 137 | icon: createIcon("power"), 138 | click: () => { 139 | win.close(); 140 | app.isQuiting = true; 141 | app.quit(); 142 | }, 143 | }, 144 | ]); 145 | }; 146 | 147 | export default createSystemTray; 148 | -------------------------------------------------------------------------------- /electron/preload/index.mjs: -------------------------------------------------------------------------------- 1 | import { contextBridge } from "electron"; 2 | import { electronAPI } from "@electron-toolkit/preload"; 3 | 4 | // 如果启用了上下文隔离,使用 `contextBridge` 将 Electron API 暴露给渲染进程 5 | if (process.contextIsolated) { 6 | try { 7 | // 使用 contextBridge 暴露 electronAPI 到渲染进程的全局对象中 8 | contextBridge.exposeInMainWorld("electron", electronAPI); 9 | } catch (error) { 10 | console.error(error); 11 | } 12 | } else { 13 | // 如果上下文隔离未启用,将 electronAPI 添加到 DOM 全局对象 14 | window.electron = electronAPI; 15 | } 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | %RENDERER_VITE_SITE_TITLE% 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | gzip on; 3 | listen 7899; 4 | listen [::]:7899; 5 | server_name localhost; 6 | 7 | location / { 8 | root /usr/share/nginx/html; 9 | index index.html; 10 | try_files $uri $uri/ /index.html; 11 | } 12 | 13 | location @rewrites { 14 | rewrite ^(.*)$ /index.html last; 15 | } 16 | 17 | location /api/ { 18 | proxy_buffers 16 32k; 19 | proxy_buffer_size 128k; 20 | proxy_busy_buffers_size 128k; 21 | proxy_set_header Host $host; 22 | proxy_set_header X-Real-IP $remote_addr; 23 | proxy_set_header X-Forwarded-For $remote_addr; 24 | proxy_set_header X-Forwarded-Host $remote_addr; 25 | proxy_set_header X-NginX-Proxy true; 26 | proxy_pass http://localhost:3000/; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tunefree", 3 | "version": "1.2.0", 4 | "description": "TuneFree - 音遇自由", 5 | "main": "./out/main/index.js", 6 | "author": "是青旨啊", 7 | "home": "https://tunefree.fun", 8 | "github": "https://github.com/GSQZ", 9 | "license": "AGPL-3.0", 10 | "license-file": "LICENSE", 11 | "engines": { 12 | "node": ">=18.16.0", 13 | "npm": ">=9.6.7", 14 | "pnpm": ">=8.14.0" 15 | }, 16 | "type": "module", 17 | "scripts": { 18 | "format": "prettier --write .", 19 | "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix", 20 | "start": "electron-vite preview", 21 | "dev": "electron-vite dev --watch", 22 | "build": "electron-vite build", 23 | "postinstall": "electron-builder install-app-deps", 24 | "build:win": "npm run build && electron-builder --win --config --publish never", 25 | "build:win-arm64": "npm run build && electron-builder --win --arm64 --config --publish never", 26 | "build:mac": "npm run build && electron-builder --mac --config --publish never", 27 | "build:linux": "npm run build && electron-builder --linux --config --publish never", 28 | "build:linux-arm64": "npm run build && electron-builder --linux --arm64 --config --publish never" 29 | }, 30 | "dependencies": { 31 | "@electron-toolkit/preload": "^3.0.0", 32 | "@electron-toolkit/utils": "^3.0.0", 33 | "@material/material-color-utilities": "^0.2.7", 34 | "NeteaseCloudMusicApi": "^4.23.1", 35 | "axios": "^1.7.9", 36 | "form-data": "^4.0.1", 37 | "colorthief": "^2.4.0", 38 | "electron-dl": "^3.5.1", 39 | "electron-localshortcut": "^3.2.1", 40 | "electron-store": "^8.1.0", 41 | "electron-updater": "^6.1.7", 42 | "express": "^4.18.2", 43 | "express-http-proxy": "^2.0.0", 44 | "howler": "^2.2.4", 45 | "js-cookie": "^3.0.5", 46 | "localforage": "^1.10.0", 47 | "music-metadata": "7.14.0", 48 | "node-taglib-sharp": "^5.2.3", 49 | "pinia": "^2.1.7", 50 | "pinia-plugin-persistedstate": "^3.2.1", 51 | "plyr": "^3.7.8", 52 | "screenfull": "^6.0.2", 53 | "vue-router": "^4.2.5", 54 | "vue-slider-component": "4.1.0-beta.7", 55 | "jszip": "3.10.1" 56 | }, 57 | "devDependencies": { 58 | "@electron-toolkit/eslint-config": "^1.0.2", 59 | "@rushstack/eslint-patch": "^1.7.0", 60 | "@vitejs/plugin-vue": "^5.0.3", 61 | "@vue/eslint-config-prettier": "^9.0.0", 62 | "ajv": "^8.12.0", 63 | "electron": "^28.3.3", 64 | "electron-builder": "^24.9.1", 65 | "electron-log": "^5.0.3", 66 | "electron-vite": "^2.0.0", 67 | "eslint": "^8.56.0", 68 | "eslint-plugin-vue": "^9.20.1", 69 | "naive-ui": "^2.37.3", 70 | "prettier": "^3.2.4", 71 | "sass": "^1.70.0", 72 | "terser": "^5.27.0", 73 | "unplugin-auto-import": "^0.17.3", 74 | "unplugin-vue-components": "^0.26.0", 75 | "vite": "^5.0.12", 76 | "vite-plugin-compression": "^0.5.1", 77 | "vite-plugin-pwa": "^0.17.4", 78 | "vue": "^3.5.11", 79 | "jszip": "3.10.1" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/favicon.png -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.a.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.a.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.a0.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.a0.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.a1.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.a1.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.aa.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.aa.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ab.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ab.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ac.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ac.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ad.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ad.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ae.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ae.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.af.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.af.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ag.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ag.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ah.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ah.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ai.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ai.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.aj.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.aj.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ak.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ak.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.al.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.al.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.am.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.am.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.an.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.an.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ao.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ao.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ap.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ap.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.aq.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.aq.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ar.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ar.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.as.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.as.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.at.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.at.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.au.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.au.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.av.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.av.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.aw.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.aw.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ax.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ax.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.ay.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.ay.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.az.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.az.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.b.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.b.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.c.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.c.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.d.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.d.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.e.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.e.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.f.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.f.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.g.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.g.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.h.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.h.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.i.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.i.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.j.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.j.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.k.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.k.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.l.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.l.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.m.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.m.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.n.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.n.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.o.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.o.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.p.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.p.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.q.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.q.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.r.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.r.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.s.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.s.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.t.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.t.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.u.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.u.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.v.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.v.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.w.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.w.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.x.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.x.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.y.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.y.woff2 -------------------------------------------------------------------------------- /public/font/font_files/HarmonyOS_Regular.z.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/font/font_files/HarmonyOS_Regular.z.woff2 -------------------------------------------------------------------------------- /public/images/icons/afdian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/afdian.jpg -------------------------------------------------------------------------------- /public/images/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/images/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/images/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/images/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/images/icons/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/favicon-192x192.png -------------------------------------------------------------------------------- /public/images/icons/favicon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/favicon-256x256.png -------------------------------------------------------------------------------- /public/images/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/images/icons/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/favicon-512x512.png -------------------------------------------------------------------------------- /public/images/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/favicon-96x96.png -------------------------------------------------------------------------------- /public/images/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/favicon.ico -------------------------------------------------------------------------------- /public/images/icons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/favicon.png -------------------------------------------------------------------------------- /public/images/icons/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/images/icons/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/logo-dark.png -------------------------------------------------------------------------------- /public/images/icons/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/logo-light.png -------------------------------------------------------------------------------- /public/images/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/mstile-150x150.png -------------------------------------------------------------------------------- /public/images/icons/next-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/next-dark.png -------------------------------------------------------------------------------- /public/images/icons/next-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/next-light.png -------------------------------------------------------------------------------- /public/images/icons/open-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/open-dark.png -------------------------------------------------------------------------------- /public/images/icons/open-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/open-light.png -------------------------------------------------------------------------------- /public/images/icons/pause-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/pause-dark.png -------------------------------------------------------------------------------- /public/images/icons/pause-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/pause-light.png -------------------------------------------------------------------------------- /public/images/icons/play-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/play-dark.png -------------------------------------------------------------------------------- /public/images/icons/play-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/play-light.png -------------------------------------------------------------------------------- /public/images/icons/power-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/power-dark.png -------------------------------------------------------------------------------- /public/images/icons/power-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/power-light.png -------------------------------------------------------------------------------- /public/images/icons/prev-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/prev-dark.png -------------------------------------------------------------------------------- /public/images/icons/prev-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/prev-light.png -------------------------------------------------------------------------------- /public/images/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /public/images/icons/setting-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/setting-dark.png -------------------------------------------------------------------------------- /public/images/icons/setting-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/icons/setting-light.png -------------------------------------------------------------------------------- /public/images/pic/album.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/album.jpg -------------------------------------------------------------------------------- /public/images/pic/album.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/album.png -------------------------------------------------------------------------------- /public/images/pic/artist.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/artist.jpg -------------------------------------------------------------------------------- /public/images/pic/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/avatar.jpg -------------------------------------------------------------------------------- /public/images/pic/cover-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/cover-2.jpg -------------------------------------------------------------------------------- /public/images/pic/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/cover.jpg -------------------------------------------------------------------------------- /public/images/pic/default.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/default.jpg -------------------------------------------------------------------------------- /public/images/pic/hot-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/hot-list.png -------------------------------------------------------------------------------- /public/images/pic/like.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/like.jpg -------------------------------------------------------------------------------- /public/images/pic/pl-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/pl-1.jpg -------------------------------------------------------------------------------- /public/images/pic/pl-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/pl-2.jpg -------------------------------------------------------------------------------- /public/images/pic/pl-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/pl-3.jpg -------------------------------------------------------------------------------- /public/images/pic/pl-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/pl-4.jpg -------------------------------------------------------------------------------- /public/images/pic/pointer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/pointer.png -------------------------------------------------------------------------------- /public/images/pic/radar-private.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/radar-private.jpg -------------------------------------------------------------------------------- /public/images/pic/radar-treasure.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/radar-treasure.jpg -------------------------------------------------------------------------------- /public/images/pic/record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/record.png -------------------------------------------------------------------------------- /public/images/pic/song.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/song.jpg -------------------------------------------------------------------------------- /public/images/pic/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/video.png -------------------------------------------------------------------------------- /public/images/pic/vip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/public/images/pic/vip.png -------------------------------------------------------------------------------- /screenshots/TuneFree - 主页面.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/screenshots/TuneFree - 主页面.jpg -------------------------------------------------------------------------------- /screenshots/TuneFree - 发现页面.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/screenshots/TuneFree - 发现页面.jpg -------------------------------------------------------------------------------- /screenshots/TuneFree - 播放页面.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/screenshots/TuneFree - 播放页面.jpg -------------------------------------------------------------------------------- /screenshots/TuneFree - 本地音乐.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/screenshots/TuneFree - 本地音乐.jpg -------------------------------------------------------------------------------- /screenshots/TuneFree - 歌单页面.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/screenshots/TuneFree - 歌单页面.jpg -------------------------------------------------------------------------------- /screenshots/TuneFree - 评论页面.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/screenshots/TuneFree - 评论页面.jpg -------------------------------------------------------------------------------- /screenshots/TuneFree-播放.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/screenshots/TuneFree-播放.png -------------------------------------------------------------------------------- /screenshots/TuneFree-首页.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/screenshots/TuneFree-首页.png -------------------------------------------------------------------------------- /screenshots/TuneFree.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/screenshots/TuneFree.jpg -------------------------------------------------------------------------------- /screenshots/build.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GSQZ/TuneFree/6f1feb056098943fd26c65a2f1e5d3b03615916f/screenshots/build.png -------------------------------------------------------------------------------- /src/api/album.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 专辑部分 5 | */ 6 | 7 | /** 8 | * 获取专辑内容 9 | * @param {number} id - 专辑id 10 | */ 11 | export const getAlbumDetail = (id) => { 12 | return axios({ 13 | method: "GET", 14 | url: "/album", 15 | params: { 16 | id, 17 | timestamp: new Date().getTime(), 18 | }, 19 | }); 20 | }; 21 | 22 | /** 23 | * 收藏/取消收藏专辑 24 | * @param {number} t - 操作类型,1为收藏,2为取消收藏 25 | * @param {number} id - 专辑id 26 | */ 27 | export const likeAlbum = (t, id) => { 28 | return axios({ 29 | method: "GET", 30 | url: "/album/sub", 31 | params: { 32 | t, 33 | id, 34 | timestamp: new Date().getTime(), 35 | }, 36 | }); 37 | }; 38 | -------------------------------------------------------------------------------- /src/api/artist.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 歌手部分 5 | */ 6 | 7 | /** 8 | * 歌手分类列表 9 | * @param {number} type - 歌手类型(-1:全部 1:男歌手 2:女歌手 3:乐队) 10 | * @param {number} area - 歌手区域(-1:全部 7:华语 96:欧美 8:日本 16:韩国 0:其他) 11 | * @param {number} [limit=50] - 返回数量,默认 50 12 | * @param {number} [offset=0] - 偏移数量,默认 0 13 | * @param {number} initial - 首字母索引查找参数 14 | */ 15 | export const getArtistList = (type = -1, area = -1, offset = 0, initial = -1, limit = 50) => { 16 | return axios({ 17 | method: "GET", 18 | url: "/artist/list", 19 | params: { 20 | type, 21 | area, 22 | offset, 23 | initial, 24 | limit, 25 | }, 26 | }); 27 | }; 28 | 29 | /** 30 | * 获取歌手详情 31 | * @param {number} id - 歌手id 32 | */ 33 | export const getArtistDetail = (id) => { 34 | return axios({ 35 | method: "GET", 36 | url: "/artist/detail", 37 | params: { 38 | id, 39 | }, 40 | }); 41 | }; 42 | 43 | /** 44 | * 获取歌手部分信息和热门歌曲 45 | * @param {number} id - 歌手id 46 | */ 47 | export const getArtistSongs = (id) => { 48 | return axios({ 49 | method: "GET", 50 | url: "/artists", 51 | params: { 52 | id, 53 | timestamp: new Date().getTime(), 54 | }, 55 | }); 56 | }; 57 | 58 | /** 59 | * 获取歌手全部歌曲 60 | * @param {number} id - 歌手id 61 | * @param {number} [limit=50] - 返回数量,默认50 62 | * @param {number} [offset=0] - 偏移数量,默认0 63 | * @param {string} order - hot: 热门, time: 时间 64 | */ 65 | export const getArtistAllSongs = (id, limit = 50, offset = 0, order = "hot") => { 66 | return axios({ 67 | method: "GET", 68 | url: "/artist/songs", 69 | params: { 70 | id, 71 | limit, 72 | offset, 73 | order, 74 | timestamp: new Date().getTime(), 75 | }, 76 | }); 77 | }; 78 | 79 | /** 80 | * 获取歌手专辑 81 | * @param {number} id - 歌手id 82 | * @param {number} [limit=50] - 返回数量,默认50 83 | * @param {number} [offset=0] - 偏移数量,默认0 84 | */ 85 | export const getArtistAblums = (id, limit = 50, offset = 0) => { 86 | return axios({ 87 | method: "GET", 88 | url: "/artist/album", 89 | params: { 90 | id, 91 | limit, 92 | offset, 93 | }, 94 | }); 95 | }; 96 | 97 | /** 98 | * 获取歌手视频 99 | * @param {number} id - 歌手id 100 | * @param {number} [limit=50] - 返回数量,默认50 101 | * @param {number} [offset=0] - 偏移数量,默认0 102 | */ 103 | export const getArtistVideos = (id, limit = 50, offset = 0) => { 104 | return axios({ 105 | method: "GET", 106 | url: "/artist/mv", 107 | params: { 108 | id, 109 | limit, 110 | offset, 111 | }, 112 | }); 113 | }; 114 | 115 | /** 116 | * 收藏/取消收藏歌手 117 | * @param {number} t - 操作类型,1 为收藏,其他为取消收藏 118 | * @param {number} id - 歌手id 119 | */ 120 | export const likeArtist = (t, id) => { 121 | return axios({ 122 | method: "GET", 123 | url: "/artist/sub", 124 | params: { 125 | t, 126 | id, 127 | timestamp: new Date().getTime(), 128 | }, 129 | }); 130 | }; 131 | -------------------------------------------------------------------------------- /src/api/cloud.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 云盘部分 5 | */ 6 | 7 | /** 8 | * 获取云盘数据 9 | * @param {number} [limit=50] - 返回数量,默认 50 10 | * @param {number} [offset=0] - 偏移数量,默认 0 11 | */ 12 | export const getUserCloud = (limit = 50, offset = 0) => { 13 | return axios({ 14 | method: "GET", 15 | url: "/user/cloud", 16 | params: { 17 | limit, 18 | offset, 19 | timestamp: new Date().getTime(), 20 | }, 21 | }); 22 | }; 23 | 24 | /** 25 | * 用户云盘歌曲删除 26 | * @param {string} id - 歌曲的id 27 | */ 28 | export const setCloudDel = (id) => { 29 | return axios({ 30 | method: "GET", 31 | url: "/user/cloud/del", 32 | params: { 33 | id, 34 | timestamp: new Date().getTime(), 35 | }, 36 | }); 37 | }; 38 | 39 | /** 40 | * 云盘歌曲信息匹配纠正 41 | * @param {string} uid - 用户 id 42 | * @param {string} sid - 原歌曲 id 43 | * @param {string} asid - 要匹配的歌曲 id 44 | */ 45 | export const setCloudMatch = (uid, sid, asid) => { 46 | return axios({ 47 | method: "GET", 48 | url: "/cloud/match", 49 | params: { 50 | uid, 51 | sid, 52 | asid, 53 | timestamp: new Date().getTime(), 54 | }, 55 | }); 56 | }; 57 | 58 | /** 59 | * 用户云盘上传 60 | * @param {File} file - 要上传的文件 61 | */ 62 | export const upCloudSong = (file, onUploadProgress) => { 63 | const formData = new FormData(); 64 | formData.append("songFile", file); 65 | return axios({ 66 | url: "/cloud", 67 | method: "POST", 68 | hiddenBar: true, 69 | params: { 70 | timestamp: new Date().getTime(), 71 | }, 72 | data: formData, 73 | headers: { 74 | "Content-Type": "multipart/form-data", 75 | }, 76 | timeout: 200000, 77 | onUploadProgress, 78 | }); 79 | }; 80 | -------------------------------------------------------------------------------- /src/api/comment.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 评论部分 5 | */ 6 | 7 | /** 8 | * 获取评论 9 | * @param {number} id - 对应资源的 id 10 | * @param {string} type - 对应资源的类型,0: 歌曲, 1: mv, 2: 歌单, 3: 专辑, 4: 电台节目, 5: 视频, 6: 动态, 7: 电台 11 | * @param {number} pageNo - 分页参数,第 N 页, 默认 1 12 | * @param {number} pageSize - 分页参数,每页多少条数据, 默认 20 13 | * @param {number} sortType - 排序方式, 1:按推荐排序, 2:按热度排序, 3:按时间排序 14 | * @param {number} cursor - 当 sortType 为 3 时且页数不是第一页时需传入, 值为上一条数据的 time 15 | */ 16 | export const getComment = (id, type = 0, pageNo = 1, sortType = 3, pageSize = 20, cursor) => { 17 | return axios({ 18 | method: "GET", 19 | url: "/comment/new", 20 | params: { 21 | id, 22 | type, 23 | pageNo, 24 | pageSize, 25 | sortType, 26 | cursor, 27 | timestamp: new Date().getTime(), 28 | }, 29 | }); 30 | }; 31 | 32 | /** 33 | * 获取热门评论 34 | * @param {number} id - 对应资源的 id 35 | * @param {string} type - 对应资源的类型,0: 歌曲, 1: mv, 2: 歌单, 3: 专辑, 4: 电台节目, 5: 视频, 6: 动态, 7: 电台 36 | * @param {number} limit - 取出评论数量 , 默认为 20 37 | * @param {number} offset - 偏移数量 , 用于分页 , 如 :( 评论页数 -1)*20, 其中 20 为 limit 的值 38 | * @param {string} before - 分页参数,取上一页最后一项的 time 获取下一页数据(获取超过 5000 条评论的时候需要用到) 39 | */ 40 | export const getHotComment = (id, type = 0, limit = 20, offset = 0, before) => { 41 | return axios({ 42 | method: "GET", 43 | url: "/comment/hot", 44 | params: { 45 | id, 46 | type, 47 | limit, 48 | offset, 49 | before, 50 | timestamp: new Date().getTime(), 51 | }, 52 | }); 53 | }; 54 | 55 | /** 56 | * 发送 / 删除评论 57 | * @param {number} id - 对应资源的id 58 | * @param {number} commentId - 回复的评论 id(回复评论时必填) 59 | * @param {string} content - 要发送的内容 60 | * @param {number} t - 是否点赞,0为删除,1为发送,2为回复 61 | * @param {number} [type=0] - 对应资源的类型,默认为0(歌曲) 62 | */ 63 | export const sendComment = (id, commentId = null, content, t, type = 0) => { 64 | return axios({ 65 | method: "GET", 66 | url: "/comment", 67 | params: { 68 | id, 69 | commentId, 70 | content, 71 | t, 72 | type, 73 | timestamp: new Date().getTime(), 74 | }, 75 | }); 76 | }; 77 | 78 | /** 79 | * 评论点赞 80 | * @param {number} id - 对应资源的 id 81 | * @param {number} cid - 评论的 id 82 | * @param {number} t - 操作, 1 为点赞, 其他为取消点赞 83 | * @param {number} type - 资源类型 / 0: 歌曲 / 1: mv / 2: 歌单 / 3: 专辑 / 4: 电台节目 / 5: 视频 / 6: 动态 / 7: 电台 84 | */ 85 | export const likeComment = (id, cid, t, type = 0) => { 86 | return axios({ 87 | method: "GET", 88 | url: "/comment/like", 89 | params: { 90 | id, 91 | cid, 92 | t, 93 | type, 94 | timestamp: new Date().getTime(), 95 | }, 96 | }); 97 | }; 98 | -------------------------------------------------------------------------------- /src/api/dj.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 电台部分 5 | */ 6 | 7 | /** 8 | * 获取电台 - 分类 9 | */ 10 | export const getDjCatelist = () => { 11 | return axios({ 12 | method: "GET", 13 | url: "/dj/catelist", 14 | }); 15 | }; 16 | 17 | /** 18 | * 获取电台 - 推荐 19 | */ 20 | export const getDjRecommend = () => { 21 | return axios({ 22 | method: "GET", 23 | url: "/dj/recommend", 24 | }); 25 | }; 26 | 27 | /** 28 | * 电台个性推荐 29 | */ 30 | export const getDjPersonalRec = () => { 31 | return axios({ 32 | method: "GET", 33 | url: "/dj/personalize/recommend", 34 | }); 35 | }; 36 | 37 | /** 38 | * 获取电台 - 推荐类型 39 | */ 40 | export const getDjCategoryRec = () => { 41 | return axios({ 42 | method: "GET", 43 | url: "/dj/category/recommend", 44 | }); 45 | }; 46 | 47 | /** 48 | * 私人 DJ 49 | */ 50 | export const getPrivateDj = () => { 51 | return axios({ 52 | method: "GET", 53 | url: "/aidj/content/rcmd", 54 | }); 55 | }; 56 | 57 | /** 58 | * 电台 - 类别热门电台 59 | * @param {string} cateId - 类别 id 60 | * @param {number} [limit=50] - 返回数量,默认 50 61 | * @param {number} [offset=0] - 偏移数量,默认 0 62 | */ 63 | export const getRadioHot = (cateId, limit = 50, offset = 0) => { 64 | return axios({ 65 | method: "GET", 66 | url: "/dj/radio/hot", 67 | params: { 68 | cateId, 69 | limit, 70 | offset, 71 | }, 72 | }); 73 | }; 74 | 75 | /** 76 | * 电台 - 分类推荐 77 | * @param {string} type - 类别 id 78 | */ 79 | export const getRecType = (type) => { 80 | return axios({ 81 | method: "GET", 82 | url: "/dj/recommend/type", 83 | params: { 84 | type, 85 | }, 86 | }); 87 | }; 88 | 89 | /** 90 | * 电台 - 详情 91 | * @param {string} rid - 电台 的 id 92 | */ 93 | export const getDjDetail = (rid) => { 94 | return axios({ 95 | method: "GET", 96 | url: "/dj/detail", 97 | params: { 98 | rid, 99 | }, 100 | }); 101 | }; 102 | 103 | /** 104 | * 电台 - 节目 105 | * @param {string} rid - 电台 的 id 106 | * @param {number} [limit=50] - 返回数量,默认 50 107 | * @param {number} [offset=0] - 偏移数量,默认 0 108 | */ 109 | export const getDjProgram = (rid, limit = 50, offset = 0) => { 110 | return axios({ 111 | method: "GET", 112 | url: "/dj/program", 113 | params: { 114 | rid, 115 | limit, 116 | offset, 117 | }, 118 | }); 119 | }; 120 | 121 | /** 122 | * 电台 - 订阅 123 | * @param {number} rid - 电台 的 id 124 | * @param {number} t - 操作类型,1为收藏,0为取消收藏 125 | */ 126 | export const likeDj = (rid, t) => { 127 | return axios({ 128 | method: "GET", 129 | url: "/dj/sub", 130 | params: { 131 | rid, 132 | t, 133 | timestamp: new Date().getTime(), 134 | }, 135 | }); 136 | }; 137 | -------------------------------------------------------------------------------- /src/api/login.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 登录部分 5 | */ 6 | 7 | /** 8 | * 生成二维码 key 9 | */ 10 | export const getQrKey = () => { 11 | return axios({ 12 | method: "GET", 13 | noCookie: true, 14 | url: "/login/qr/key", 15 | params: { 16 | timestamp: new Date().getTime(), 17 | }, 18 | }); 19 | }; 20 | 21 | /** 22 | * 生成二维码 23 | * @param {string} key 二维码key 24 | * @param {boolean} qrimg 是否生成二维码图片,默认为true 25 | */ 26 | export const qrCreate = (key, qrimg = true) => { 27 | return axios({ 28 | method: "GET", 29 | noCookie: true, 30 | url: "/login/qr/create", 31 | params: { 32 | key, 33 | qrimg, 34 | timestamp: new Date().getTime(), 35 | }, 36 | }); 37 | }; 38 | 39 | /** 40 | * 检查二维码状态 41 | * @param {string} key 二维码key 42 | */ 43 | export const checkQr = (key) => { 44 | return axios({ 45 | method: "GET", 46 | noCookie: true, 47 | url: "/login/qr/check", 48 | params: { 49 | key, 50 | timestamp: new Date().getTime(), 51 | }, 52 | }); 53 | }; 54 | 55 | /** 56 | * 登录 57 | * @param {string} code 授权码 58 | */ 59 | export const toLogin = async (code) => { 60 | try { 61 | const response = await fetch(`https://auth.sayqz.com/?path=info&code=${code}`); 62 | const data = await response.json(); 63 | 64 | if (data.code === 200 && data.data) { 65 | return { 66 | code: 200, 67 | cookie: data.data.netease_cookie 68 | }; 69 | } 70 | return { 71 | code: data.code, 72 | message: data.message 73 | }; 74 | } catch (error) { 75 | console.error('登录请求失败:', error); 76 | return { 77 | code: 500, 78 | message: '登录失败,请重试' 79 | }; 80 | } 81 | }; 82 | 83 | /** 84 | * 发送验证码 85 | * @param {string} phone 手机号码 86 | */ 87 | export const sentCaptcha = (phone) => { 88 | return axios({ 89 | method: "GET", 90 | noCookie: true, 91 | url: "/captcha/sent", 92 | params: { 93 | phone, 94 | timestamp: new Date().getTime(), 95 | }, 96 | }); 97 | }; 98 | 99 | /** 100 | * 验证验证码是否正确 101 | * @param {string} phone 手机号码 102 | * @param {string} captcha 验证码 103 | */ 104 | export const verifyCaptcha = (phone, captcha) => { 105 | return axios({ 106 | method: "GET", 107 | noCookie: true, 108 | url: "/captcha/verify", 109 | params: { 110 | phone, 111 | captcha, 112 | timestamp: new Date().getTime(), 113 | }, 114 | }); 115 | }; 116 | 117 | // 获取登录状态 118 | export const getLoginState = () => { 119 | return axios({ 120 | method: "GET", 121 | url: "/login/status", 122 | params: { 123 | timestamp: new Date().getTime(), 124 | }, 125 | }); 126 | }; 127 | 128 | // 刷新登录 129 | export const refreshLogin = () => { 130 | return axios({ 131 | method: "GET", 132 | url: "/login/refresh", 133 | params: { 134 | timestamp: new Date().getTime(), 135 | }, 136 | }); 137 | }; 138 | 139 | /** 140 | * 退出登录 141 | */ 142 | export const logOut = () => { 143 | return axios({ 144 | method: "GET", 145 | url: "/logout", 146 | params: { 147 | timestamp: new Date().getTime(), 148 | }, 149 | }); 150 | }; 151 | -------------------------------------------------------------------------------- /src/api/other.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 杂项 5 | */ 6 | 7 | /** 8 | * 获取轮播图 9 | * @param {number} type - 资源类型 / 0: pc / 1: android / 2: iphone / 3: ipad 10 | */ 11 | export const getBanner = (type = 0) => { 12 | return axios({ 13 | method: "GET", 14 | url: `/banner?type=${type}`, 15 | }); 16 | }; 17 | 18 | /** 19 | * 资源点赞 ( MV,电台,视频 ) 20 | * @param {number} id - 资源 id 21 | * @param {number} t - 操作, 1 为点赞, 其他为取消点赞 22 | * @param {number} type - 资源类型 / 0: 歌曲 / 1: mv / 2: 歌单 / 3: 专辑 / 4: 电台节目 / 5: 视频 / 6: 动态 / 7: 电台 23 | */ 24 | export const resourceLike = (id, t = 1, type = 0) => { 25 | return axios({ 26 | method: "GET", 27 | url: "/resource/like", 28 | params: { 29 | id, 30 | t, 31 | type, 32 | }, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/api/recommend.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | import idMeta from "@/assets/idMeta.json"; 3 | 4 | /** 5 | * 推荐部分 6 | */ 7 | 8 | /** 9 | * 每日推荐 - 需要登录 10 | * @param {string} [type] - 推荐类型,songs 日推歌曲 / resource 推荐歌单 11 | */ 12 | export const getDailyRec = (type = "songs") => { 13 | return axios({ 14 | method: "GET", 15 | url: `/recommend/${type}`, 16 | params: { 17 | timestamp: new Date().getTime(), 18 | }, 19 | }); 20 | }; 21 | 22 | /** 23 | * 推荐内容 24 | * @param {string} [type] - 推荐类型,mv MV / newsong 新音乐 / djprogram 电台 / privatecontent 独家放送 25 | * @param {number} [limit=12] - 返回结果的数量,默认为 12 26 | */ 27 | export const getPersonalized = (type, limit = 12) => { 28 | const url = type ? `/personalized/${type}` : "/personalized"; 29 | return axios({ 30 | method: "GET", 31 | url, 32 | params: { 33 | limit, 34 | }, 35 | }); 36 | }; 37 | 38 | /** 39 | * 雷达歌单 40 | */ 41 | export const getRadarPlaylist = async () => { 42 | const allRadar = idMeta.radarPlaylist.map((playlist) => { 43 | return axios({ 44 | method: "GET", 45 | url: "/playlist/detail", 46 | params: { id: playlist.id }, 47 | }); 48 | }); 49 | const result = await Promise.allSettled(allRadar); 50 | return result.map((res) => res?.value.playlist); 51 | }; 52 | 53 | /** 54 | * 热门歌手列表 55 | * @param {number} [limit=6] - 要返回的歌手数量,默认为 6 个 56 | */ 57 | export const getTopArtists = (limit = 6) => { 58 | return axios({ 59 | method: "GET", 60 | url: "/top/artists", 61 | params: { 62 | limit, 63 | }, 64 | }); 65 | }; 66 | 67 | /** 68 | * 新歌速递 69 | * @param {number} type - 全部:0 / 华语:7 / 欧美:96 / 韩国:16 / 日本:8 70 | */ 71 | export const getNewSong = (type = 0) => { 72 | return axios({ 73 | method: "GET", 74 | url: "/top/song", 75 | params: { 76 | type, 77 | }, 78 | }); 79 | }; 80 | 81 | /** 82 | * 新碟上架 - 首页 83 | */ 84 | export const getNewAlbum = () => { 85 | return axios({ 86 | method: "GET", 87 | url: "/album/newest", 88 | }); 89 | }; 90 | 91 | /** 92 | * 新碟上架 - 全部 93 | * @param {string} [cat='ALL'] - ALL:全部 / ZH:华语 / EA:欧美 / KR:韩国 / JP:日本 94 | * @param {number} [limit=50] - 返回数量,默认50 95 | * @param {number} [offset=0] - 偏移数量,默认0 96 | */ 97 | export const getAllNewAlbum = (area = "ALL", limit = 50, offset = 0) => { 98 | return axios({ 99 | method: "GET", 100 | url: "/album/new", 101 | params: { 102 | area, 103 | limit, 104 | offset, 105 | }, 106 | }); 107 | }; 108 | 109 | /** 110 | * 获取私人FM数据 111 | */ 112 | export const getPersonalFm = () => { 113 | return axios({ 114 | method: "GET", 115 | url: "/personal_fm", 116 | params: { 117 | timestamp: new Date().getTime(), 118 | }, 119 | }); 120 | }; 121 | 122 | /** 123 | * 将指定歌曲加入垃圾桶 124 | * @param {number} id 歌曲ID 125 | */ 126 | export const setFmToTrash = (id) => { 127 | return axios({ 128 | method: "GET", 129 | url: "/fm_trash", 130 | params: { 131 | id, 132 | timestamp: new Date().getTime(), 133 | }, 134 | }); 135 | }; 136 | -------------------------------------------------------------------------------- /src/api/search.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 搜索部分 5 | */ 6 | 7 | /** 8 | * 热搜列表 - 详细 9 | */ 10 | export const getSearchHot = () => { 11 | return axios({ 12 | method: "GET", 13 | url: "/search/hot/detail", 14 | params: { 15 | timestamp: new Date().getTime(), 16 | }, 17 | }); 18 | }; 19 | 20 | /** 21 | * 搜索建议 22 | * @param {string} keywords - 搜索关键词 23 | * @param {Boolean} mobile - 如果传入 true 则返回移动端数据 24 | */ 25 | export const getSearchSuggest = (keywords, mobile = false) => { 26 | return axios({ 27 | method: "GET", 28 | url: "/search/suggest", 29 | params: { 30 | keywords, 31 | ...(mobile && { type: "mobile" }), 32 | }, 33 | }); 34 | }; 35 | 36 | /** 37 | * 默认搜索关键词 38 | */ 39 | export const getSearchDefault = () => { 40 | return axios({ 41 | method: "GET", 42 | url: "/search/default", 43 | params: { 44 | timestamp: new Date().getTime(), 45 | }, 46 | }); 47 | }; 48 | 49 | /** 50 | * 搜索结果 51 | * @param {string} keywords - 搜索关键词 52 | * @param {number} [limit=30] - 返回数量,默认30 53 | * @param {number} [offset=0] - 偏移数量,默认0 54 | * @param {number} [type=1] - 可选参数,搜索类型。1表示单曲,10表示专辑,100表示歌手,1000表示歌单,1002表示用户,1004表示MV,1006表示歌词,1009表示电台,1014表示视频,1018表示综合,2000表示声音。默认为 1 55 | */ 56 | export const getSearchRes = (keywords, limit = 50, offset = 0, type = 1) => { 57 | return axios({ 58 | method: "GET", 59 | url: "/cloudsearch", 60 | params: { 61 | keywords, 62 | limit, 63 | offset, 64 | type, 65 | }, 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /src/api/song.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 歌曲部分 5 | */ 6 | 7 | // 音质等级映射 8 | const LEVEL_MAP = { 9 | 128000: "standard", // 标准 10 | 320000: "higher", // 较高 11 | 400000: "exhigh", // 极高 12 | 900000: "lossless", // 无损 13 | 1200000: "hires", // Hi-Res 14 | 1500000: "jymaster", // 超清母带 15 | 2000000: "sky" // 沉浸环绕声 16 | }; 17 | 18 | // 转换音质等级 19 | const convertLevel = (br) => { 20 | if (typeof br === 'string' && Object.values(LEVEL_MAP).includes(br)) { 21 | return br; 22 | } 23 | const brNumber = Number(br); 24 | // 找到最接近的等级 25 | const levels = Object.keys(LEVEL_MAP).map(Number); 26 | const closest = levels.reduce((prev, curr) => { 27 | return Math.abs(curr - brNumber) < Math.abs(prev - brNumber) ? curr : prev; 28 | }); 29 | return LEVEL_MAP[closest] || "standard"; 30 | }; 31 | 32 | /** 33 | * 获取指定音乐的详情 34 | * @param {string} ids - 要获取详情的音乐ID,多个ID用逗号分隔 35 | */ 36 | export const getSongDetail = (ids) => { 37 | const timestamp = new Date().getTime(); 38 | return axios({ 39 | method: "POST", 40 | url: `/song/detail?timestamp=${timestamp}`, 41 | data: { 42 | ids, 43 | }, 44 | }); 45 | }; 46 | 47 | /** 48 | * 获取音乐 URL 49 | * @param {number} id - 要获取音乐的 ID。 50 | * @param {number|string} br - 音质等级,可以是数字码率或字符串等级名称 51 | */ 52 | export const getSongUrl = (id, br = "hires") => { 53 | return axios({ 54 | method: "GET", 55 | url: "https://auth.sayqz.com/?path=song/url", 56 | params: { 57 | id, 58 | level: convertLevel(br) 59 | }, 60 | }); 61 | }; 62 | 63 | /** 64 | * 获取指定音乐的歌词 65 | * @param {number} id - 要获取歌词的音乐ID 66 | */ 67 | export const getSongLyric = (id) => { 68 | return axios({ 69 | method: "GET", 70 | url: "/lyric/new", 71 | params: { 72 | id, 73 | }, 74 | }); 75 | }; 76 | 77 | /** 78 | * 获取客户端歌曲下载 79 | * @param {number} id - 要下载的音乐ID 80 | * @param {number|string} br - 音质等级,可以是数字码率或字符串等级名称 81 | */ 82 | export const getSongDownload = async (id, br = "hires") => { 83 | try { 84 | const res = await axios({ 85 | method: "GET", 86 | url: "https://auth.sayqz.com/?path=song/url", 87 | params: { 88 | id, 89 | level: convertLevel(br) 90 | }, 91 | }); 92 | 93 | if (res.code === 200 && res.data?.[0]) { 94 | const songData = res.data[0]; 95 | // 只有当歌曲URL存在且状态码为200时才返回成功 96 | if (songData.url && songData.code === 200) { 97 | return { 98 | code: 200, 99 | data: { 100 | url: songData.url, 101 | size: songData.size, 102 | md5: songData.md5, 103 | type: songData.type, 104 | level: songData.level, 105 | br: songData.br 106 | } 107 | }; 108 | } 109 | // 如果歌曲URL不存在或状态码不为200,返回错误信息 110 | return { 111 | code: songData.code || 404, 112 | message: songData.message || "歌曲暂时无法下载" 113 | }; 114 | } 115 | return { 116 | code: res.code, 117 | message: res.message || "获取下载链接失败" 118 | }; 119 | } catch (error) { 120 | console.error("获取下载链接失败:", error); 121 | return { 122 | code: 500, 123 | message: "获取下载链接失败,请重试" 124 | }; 125 | } 126 | }; 127 | 128 | /** 129 | * 听歌打卡 130 | * @param {number} id - 音乐ID 131 | * @param {number} sourceid - 来源ID 132 | */ 133 | export const songScrobble = (id, sourceid = 0, time = 0) => { 134 | return axios({ 135 | method: "GET", 136 | url: "/scrobble", 137 | params: { 138 | id, 139 | sourceid, 140 | time, 141 | timestamp: new Date().getTime(), 142 | }, 143 | }); 144 | }; 145 | -------------------------------------------------------------------------------- /src/api/update.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 检查应用更新 5 | */ 6 | export const checkUpdate = async () => { 7 | try { 8 | const response = await fetch('https://auth.sayqz.com/?path=app/info&platform=pc'); 9 | const data = await response.json(); 10 | 11 | if (data.code === 200 && data.data) { 12 | return { 13 | code: 200, 14 | data: { 15 | version: data.data.version, 16 | updateUrl: data.data.update_url, 17 | announcement: data.data.announcement, 18 | ads: data.data.ads 19 | } 20 | }; 21 | } 22 | return { 23 | code: data.code, 24 | message: data.message 25 | }; 26 | } catch (error) { 27 | console.error('检查更新失败:', error); 28 | return { 29 | code: 500, 30 | message: '检查更新失败,请重试' 31 | }; 32 | } 33 | }; -------------------------------------------------------------------------------- /src/api/video.js: -------------------------------------------------------------------------------- 1 | import axios from "@/utils/request"; 2 | 3 | /** 4 | * 视频 5 | */ 6 | 7 | /** 8 | * 获取指定 MV 的详细信息 9 | * @param {number} mvid - MV ID 10 | */ 11 | export const getVideoDetail = (mvid) => { 12 | return axios({ 13 | method: "GET", 14 | url: "/mv/detail", 15 | params: { 16 | mvid, 17 | }, 18 | }); 19 | }; 20 | 21 | /** 22 | * 获取指定 MV 的点赞转发评论数 23 | * @param {number} mvid - MV ID 24 | */ 25 | export const getVideoInfo = (mvid) => { 26 | return axios({ 27 | method: "GET", 28 | url: "/mv/detail/info", 29 | params: { 30 | mvid, 31 | timestamp: new Date().getTime(), 32 | }, 33 | }); 34 | }; 35 | 36 | /** 37 | * 获取指定 MV 的播放地址 38 | * @param {number} id - 要查询的MV ID 39 | * @param {string} [r=null] - 分辨率。默认值为null 40 | */ 41 | export const getVideoUrl = (id, r = null) => { 42 | return axios({ 43 | method: "GET", 44 | hiddenBar: true, 45 | url: "/mv/url", 46 | params: { 47 | id, 48 | r, 49 | }, 50 | }); 51 | }; 52 | 53 | /** 54 | * 获取与指定 MV 相似的 MV 列表 55 | * @param {number} mvid - 要查询的 MV ID 56 | */ 57 | export const getSimiVideo = (mvid) => { 58 | return axios({ 59 | method: "GET", 60 | url: "/simi/mv", 61 | params: { 62 | mvid, 63 | }, 64 | }); 65 | }; 66 | 67 | /** 68 | * 收藏/取消收藏视频 69 | * @param {number} t - 操作类型,1为收藏,2为取消收藏 70 | * @param {number} id - 视频id 71 | */ 72 | export const likeVideo = (t, id) => { 73 | return axios({ 74 | method: "GET", 75 | url: "/video/sub", 76 | params: { 77 | t, 78 | id, 79 | timestamp: new Date().getTime(), 80 | }, 81 | }); 82 | }; 83 | 84 | /** 85 | * 收藏/取消收藏 MV 86 | * @param {number} t - 操作类型,1为收藏,2为取消收藏 87 | * @param {number} mvid - MV id 88 | */ 89 | export const likeMv = (t, mvid) => { 90 | return axios({ 91 | method: "GET", 92 | url: "/mv/sub", 93 | params: { 94 | t, 95 | mvid, 96 | timestamp: new Date().getTime(), 97 | }, 98 | }); 99 | }; 100 | 101 | /** 102 | * 全部 mv 103 | * @param {string} area - 地区,可选值为全部,内地,港台,欧美,日本,韩国,不填则为全部 104 | * @param {string} type - 类型,可选值为全部,官方版,原生,现场版,网易出品,不填则为全部 105 | * @param {string} order - 排序,可选值为上升最快,最热,最新,不填则为上升最快 106 | * @param {number} [limit=12] - 返回数量,默认12 107 | * @param {number} [offset=0] - 偏移数量,默认0 108 | */ 109 | export const allMv = (area, type, order, limit = 12, offset = 0) => { 110 | return axios({ 111 | method: "GET", 112 | url: "/mv/all", 113 | params: { 114 | area, 115 | type, 116 | order, 117 | limit, 118 | offset, 119 | }, 120 | }); 121 | }; 122 | -------------------------------------------------------------------------------- /src/assets/emoji.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "emoji": "😄", 4 | "emjName": "大笑" 5 | }, 6 | { 7 | "emoji": "😊", 8 | "emjName": "可爱" 9 | }, 10 | { 11 | "emoji": "😄", 12 | "emjName": "憨笑" 13 | }, 14 | { 15 | "emoji": "😘", 16 | "emjName": "亲亲" 17 | }, 18 | { 19 | "emoji": "😄", 20 | "emjName": "流泪" 21 | }, 22 | { 23 | "emoji": "😊", 24 | "emjName": "亲" 25 | }, 26 | { 27 | "emoji": "😐", 28 | "emjName": "呆" 29 | }, 30 | { 31 | "emoji": "😢", 32 | "emjName": "哀伤" 33 | }, 34 | { 35 | "emoji": "😁", 36 | "emjName": "呲牙" 37 | }, 38 | { 39 | "emoji": "😝", 40 | "emjName": "吐舌" 41 | }, 42 | { 43 | "emoji": "😄", 44 | "emjName": "撇嘴" 45 | }, 46 | { 47 | "emoji": "😡", 48 | "emjName": "怒" 49 | }, 50 | { 51 | "emoji": "😏", 52 | "emjName": "奸笑" 53 | }, 54 | { 55 | "emoji": "😅", 56 | "emjName": "汗" 57 | }, 58 | { 59 | "emoji": "😣", 60 | "emjName": "痛苦" 61 | }, 62 | { 63 | "emoji": "😱", 64 | "emjName": "惶恐" 65 | }, 66 | { 67 | "emoji": "🤒", 68 | "emjName": "生病" 69 | }, 70 | { 71 | "emoji": "😷", 72 | "emjName": "口罩" 73 | }, 74 | { 75 | "emoji": "😭", 76 | "emjName": "大哭" 77 | }, 78 | { 79 | "emoji": "😵", 80 | "emjName": "晕" 81 | }, 82 | { 83 | "emoji": "😡", 84 | "emjName": "发怒" 85 | }, 86 | { 87 | "emoji": "😀", 88 | "emjName": "开心" 89 | }, 90 | { 91 | "emoji": "😈", 92 | "emjName": "鬼脸" 93 | }, 94 | { 95 | "emoji": "😠", 96 | "emjName": "皱眉" 97 | }, 98 | { 99 | "emoji": "😷", 100 | "emjName": "流感" 101 | }, 102 | { 103 | "emoji": "❤️", 104 | "emjName": "爱心" 105 | }, 106 | { 107 | "emoji": "💔", 108 | "emjName": "心碎" 109 | }, 110 | { 111 | "emoji": "💗", 112 | "emjName": "钟情" 113 | }, 114 | { 115 | "emoji": "⭐", 116 | "emjName": "星星" 117 | }, 118 | { 119 | "emoji": "💢", 120 | "emjName": "生气" 121 | }, 122 | { 123 | "emoji": "💩", 124 | "emjName": "便便" 125 | }, 126 | { 127 | "emoji": "💪", 128 | "emjName": "强" 129 | }, 130 | { 131 | "emoji": "💤", 132 | "emjName": "弱" 133 | }, 134 | { 135 | "emoji": "🙏", 136 | "emjName": "拜" 137 | }, 138 | { 139 | "emoji": "🤝", 140 | "emjName": "牵手" 141 | }, 142 | { 143 | "emoji": "💃", 144 | "emjName": "跳舞" 145 | }, 146 | { 147 | "emoji": "🚫", 148 | "emjName": "禁止" 149 | }, 150 | { 151 | "emoji": "👉", 152 | "emjName": "这边" 153 | }, 154 | { 155 | "emoji": "😍", 156 | "emjName": "爱意" 157 | }, 158 | { 159 | "emoji": "💏", 160 | "emjName": "示爱" 161 | }, 162 | { 163 | "emoji": "💋", 164 | "emjName": "嘴唇" 165 | }, 166 | { 167 | "emoji": "🐶", 168 | "emjName": "狗" 169 | }, 170 | { 171 | "emoji": "😸", 172 | "emjName": "猫" 173 | }, 174 | { 175 | "emoji": "🐷", 176 | "emjName": "猪" 177 | }, 178 | { 179 | "emoji": "🐰", 180 | "emjName": "兔子" 181 | }, 182 | { 183 | "emoji": "🐔", 184 | "emjName": "小鸡" 185 | }, 186 | { 187 | "emoji": "🐓", 188 | "emjName": "公鸡" 189 | }, 190 | { 191 | "emoji": "👻", 192 | "emjName": "幽灵" 193 | }, 194 | { 195 | "emoji": "🎅", 196 | "emjName": "圣诞" 197 | }, 198 | { 199 | "emoji": "👽", 200 | "emjName": "外星" 201 | }, 202 | { 203 | "emoji": "💎", 204 | "emjName": "钻石" 205 | }, 206 | { 207 | "emoji": "🎁", 208 | "emjName": "礼物" 209 | }, 210 | { 211 | "emoji": "👦", 212 | "emjName": "男孩" 213 | }, 214 | { 215 | "emoji": "👧", 216 | "emjName": "女孩" 217 | }, 218 | { 219 | "emoji": "🍰", 220 | "emjName": "蛋糕" 221 | }, 222 | { 223 | "emoji": "🔞", 224 | "emjName": "18" 225 | }, 226 | { 227 | "emoji": "🔵", 228 | "emjName": "圈" 229 | }, 230 | { 231 | "emoji": "❌", 232 | "emjName": "叉" 233 | }, 234 | { 235 | "emoji": "😱", 236 | "emjName": "惊恐" 237 | }, 238 | { 239 | "emoji": "😍", 240 | "emjName": "色" 241 | } 242 | ] 243 | -------------------------------------------------------------------------------- /src/assets/idMeta.json: -------------------------------------------------------------------------------- 1 | { 2 | "radarPlaylist": [ 3 | { 4 | "id": 3136952023, 5 | "name": "私人雷达" 6 | }, 7 | { 8 | "id": 5320167908, 9 | "name": "时光雷达" 10 | }, 11 | { 12 | "id": 5327906368, 13 | "name": "乐迷雷达" 14 | }, 15 | { 16 | "id": 5362359247, 17 | "name": "宝藏雷达" 18 | }, 19 | { 20 | "id": 5300458264, 21 | "name": "新歌雷达" 22 | }, 23 | { 24 | "id": 5341776086, 25 | "name": "神秘雷达" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /src/assets/themeColor.json: -------------------------------------------------------------------------------- 1 | { 2 | "red": { 3 | "name": "欢快派对", 4 | "label": "red", 5 | "primaryColor": "#fe7971", 6 | "primaryColorHover": "#F57B74", 7 | "primaryColorSuppl": "#F57B74", 8 | "primaryColorPressed": "#F64B41" 9 | }, 10 | "orange": { 11 | "name": "柑橘桔梦", 12 | "label": "orange", 13 | "primaryColor": "#ff8c00", 14 | "primaryColorHover": "#ffa033", 15 | "primaryColorSuppl": "#ffa033", 16 | "primaryColorPressed": "#ff6b00" 17 | }, 18 | "blue": { 19 | "name": "深海蓝梦", 20 | "label": "blue", 21 | "primaryColor": "#3b5998", 22 | "primaryColorHover": "#5475a3", 23 | "primaryColorSuppl": "#8b9dc3", 24 | "primaryColorPressed": "#2d4373" 25 | }, 26 | "pink": { 27 | "name": "粉色梦幻", 28 | "label": "pink", 29 | "primaryColor": "#e91e63", 30 | "primaryColorHover": "#f06292", 31 | "primaryColorSuppl": "#f06292", 32 | "primaryColorPressed": "#c2185b" 33 | }, 34 | "brown": { 35 | "name": "深棕林荫", 36 | "label": "brown", 37 | "primaryColor": "#795548", 38 | "primaryColorHover": "#8d6e63", 39 | "primaryColorSuppl": "#8d6e63", 40 | "primaryColorPressed": "#5d4037" 41 | }, 42 | "indigo": { 43 | "name": "星空靛蓝", 44 | "label": "indigo", 45 | "primaryColor": "#3f51b5", 46 | "primaryColorHover": "#5c6bc0", 47 | "primaryColorSuppl": "#5c6bc0", 48 | "primaryColorPressed": "#3949ab" 49 | }, 50 | "green": { 51 | "name": "生命绿洲", 52 | "label": "green", 53 | "primaryColor": "#2ecc71", 54 | "primaryColorHover": "#3ddc88", 55 | "primaryColorSuppl": "#3ddc88", 56 | "primaryColorPressed": "#27ae60" 57 | }, 58 | "purple": { 59 | "name": "皇室紫梦", 60 | "label": "purple", 61 | "primaryColor": "#9c27b0", 62 | "primaryColorHover": "#ba68c8", 63 | "primaryColorSuppl": "#ba68c8", 64 | "primaryColorPressed": "#7b1fa2" 65 | }, 66 | "yellow": { 67 | "name": "金色阳光", 68 | "label": "yellow", 69 | "primaryColor": "#FBC02D", 70 | "primaryColorHover": "#FFD54F", 71 | "primaryColorSuppl": "#FFD54F", 72 | "primaryColorPressed": "#FFC107" 73 | }, 74 | "teal": { 75 | "name": "海洋碧绿", 76 | "label": "teal", 77 | "primaryColor": "#009688", 78 | "primaryColorHover": "#26a69a", 79 | "primaryColorSuppl": "#26a69a", 80 | "primaryColorPressed": "#00796b" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/components/Cover/CoverDropdown.vue: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 88 | -------------------------------------------------------------------------------- /src/components/Cover/SpecialCover.vue: -------------------------------------------------------------------------------- 1 | 2 | 64 | 65 | 123 | 124 | 148 | -------------------------------------------------------------------------------- /src/components/Global/MainLayout.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 33 | 34 | 51 | -------------------------------------------------------------------------------- /src/components/Global/Pagination.vue: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 57 | 58 | 65 | -------------------------------------------------------------------------------- /src/components/Modal/CreatePlaylist.vue: -------------------------------------------------------------------------------- 1 | 2 | 37 | 38 | 78 | -------------------------------------------------------------------------------- /src/components/Modal/LoginPhone.vue: -------------------------------------------------------------------------------- 1 | 2 | 32 | 33 | 78 | 79 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/components/Modal/PlaylistUpdate.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 141 | -------------------------------------------------------------------------------- /src/components/Player/CountDown.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 79 | 80 | 118 | -------------------------------------------------------------------------------- /src/components/Search/SearchDropdown.vue: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 109 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import { createPinia } from "pinia"; 3 | import { checkPlatform } from "@/utils/helper"; 4 | import App from "@/App.vue"; 5 | import router from "@/router"; 6 | import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; 7 | import packageJson from "@/../package.json"; 8 | 9 | // 全局样式 10 | import "@/style/main.scss"; 11 | import "@/style/animate.scss"; 12 | 13 | // 是否为 Electron 14 | const isElectron = checkPlatform.electron(); 15 | 16 | // 根据设备类型动态添加 17 | const linkElement = document.createElement("link"); 18 | linkElement.rel = "stylesheet"; 19 | linkElement.href = isElectron 20 | ? `${import.meta.env.BASE_URL}font/font.min.css` 21 | : "https://s1.hdslb.com/bfs/static/jinkela/long/font/regular.css"; 22 | document.head.appendChild(linkElement); 23 | document.body.classList.add(isElectron ? "electron" : null); 24 | 25 | // 程序重置 26 | window.$cleanAll = (tip = true) => { 27 | if (tip) { 28 | const isConfirmed = window.confirm(`确认要重置${isElectron ? "应用程序" : "该站点"}吗?`); 29 | if (!isConfirmed) return false; 30 | } 31 | // 清除 localStorage 32 | localStorage.clear(); 33 | // 清除 IndexedDB 数据库 34 | indexedDB.deleteDatabase("filesDB"); 35 | // 清除所有 Cookie 36 | document.cookie.split(";").forEach((cookie) => { 37 | var eqPos = cookie.indexOf("="); 38 | var name = eqPos > -1 ? cookie.substring(0, eqPos) : cookie; 39 | document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; 40 | }); 41 | // 清除缓存 42 | if (caches) { 43 | caches.keys().then((names) => { 44 | for (let name of names) caches.delete(name); 45 | }); 46 | } 47 | return "已重置应用,请" + (isElectron ? "重启应用" : "刷新页面"); 48 | }; 49 | 50 | // 版权声明 51 | const logoText = import.meta.env.RENDERER_VITE_SITE_TITLE; 52 | const copyrightNotice = `\n\n版本: ${packageJson.version}\n作者: ${packageJson.author}\n作者主页: ${packageJson.home}\nGitHub: ${packageJson.github}`; 53 | console.info( 54 | `%c${logoText} %c ${copyrightNotice}`, 55 | "color:#f55e55;font-size:26px;font-weight:bold;", 56 | "font-size:16px", 57 | ); 58 | console.info( 59 | "若站点出现异常,可尝试在下方输入 %c$cleanAll()%c 然后按回车来重置", 60 | "background: #eaeffd;color:#f55e55;padding: 4px 6px;border-radius:8px;", 61 | "background:unset;color:unset;", 62 | ); 63 | 64 | // 挂载 65 | const app = createApp(App); 66 | // pinia 67 | const pinia = createPinia(); 68 | pinia.use(piniaPluginPersistedstate); 69 | app.use(pinia); 70 | // router 71 | app.use(router); 72 | // app 73 | app.mount("#app"); 74 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { nextTick } from "vue"; 2 | import { createRouter, createWebHashHistory } from "vue-router"; 3 | import { checkPlatform } from "@/utils/helper"; 4 | import { isLogin } from "@/utils/auth"; 5 | import routes from "@/router/routes"; 6 | 7 | // 基础配置 8 | const router = createRouter({ 9 | history: createWebHashHistory(import.meta.env.BASE_URL), 10 | routes, 11 | }); 12 | 13 | // 页面回顶 14 | const scrollToTop = () => { 15 | nextTick().then(() => { 16 | const mainLayout = document.getElementById("main-layout"); 17 | mainLayout?.scrollIntoView({ behavior: "smooth" }); 18 | }); 19 | }; 20 | 21 | // 路由守卫 22 | router.beforeEach((to, from, next) => { 23 | // 开始进度条 24 | if (to.path !== from.path) { 25 | if (typeof $loadingBar !== "undefined" && !checkPlatform.electron()) { 26 | $loadingBar.start(); 27 | } 28 | } 29 | // 判断是否需要登录 30 | if (to.meta.needLogin) { 31 | if (isLogin()) { 32 | next(); 33 | } else { 34 | $message.warning("请登录后使用"); 35 | if (typeof $loadingBar !== "undefined" && !checkPlatform.electron()) { 36 | $loadingBar.error(); 37 | } 38 | if (typeof $changeLogin !== "undefined") $changeLogin(); 39 | } 40 | } 41 | // 是否为本地功能 42 | else if (to.meta.needLocal) { 43 | if (checkPlatform.electron()) { 44 | next(); 45 | } else { 46 | $message.error("客户端独占功能"); 47 | if (typeof $loadingBar !== "undefined" && !checkPlatform.electron()) { 48 | $loadingBar.error(); 49 | } 50 | next("/403"); 51 | } 52 | } else { 53 | next(); 54 | } 55 | }); 56 | 57 | router.afterEach(() => { 58 | // 结束进度条 59 | if (typeof $loadingBar !== "undefined" && !checkPlatform.electron()) $loadingBar.finish(); 60 | // 页面回顶 61 | scrollToTop(); 62 | }); 63 | 64 | export default router; 65 | -------------------------------------------------------------------------------- /src/stores/index.js: -------------------------------------------------------------------------------- 1 | // stores 2 | import useSiteDataStore from "@/stores/siteData"; 3 | import useSiteStatusStore from "@/stores/siteStatus"; 4 | import useSiteSettingsStore from "@/stores/siteSettings"; 5 | import useMusicDataStore from "@/stores/musicData"; 6 | import useIndexedDBStore from "@/stores/indexedDB"; 7 | 8 | export const siteData = () => useSiteDataStore(); 9 | export const siteStatus = () => useSiteStatusStore(); 10 | export const siteSettings = () => useSiteSettingsStore(); 11 | export const musicData = () => useMusicDataStore(); 12 | export const indexedDBData = () => useIndexedDBStore(); 13 | -------------------------------------------------------------------------------- /src/stores/indexedDB.js: -------------------------------------------------------------------------------- 1 | // indexedDB 2 | import { defineStore } from "pinia"; 3 | import localforage from "localforage"; 4 | 5 | const useIndexedDBStore = defineStore("indexedDB", { 6 | state: () => ({ 7 | filesDB: localforage.createInstance({ 8 | name: "filesDB", 9 | }), 10 | usersDB: localforage.createInstance({ 11 | name: "usersDB", 12 | }), 13 | }), 14 | getters: {}, 15 | actions: { 16 | async setfilesDB(key, value) { 17 | try { 18 | await this.filesDB.setItem(key, value); 19 | } catch (error) { 20 | console.error("Error setting data in IndexedDB:", error); 21 | throw error; 22 | } 23 | }, 24 | async getfilesDB(key) { 25 | try { 26 | const value = await this.filesDB.getItem(key); 27 | return value; 28 | } catch (error) { 29 | console.error("Error retrieving data from IndexedDB:", error); 30 | throw error; 31 | } 32 | }, 33 | }, 34 | }); 35 | 36 | export default useIndexedDBStore; 37 | -------------------------------------------------------------------------------- /src/stores/siteSettings.js: -------------------------------------------------------------------------------- 1 | // 站点设置 2 | import { defineStore } from "pinia"; 3 | 4 | const useSiteSettingsStore = defineStore("siteSettings", { 5 | state: () => { 6 | return { 7 | // 基础配置 8 | closeTip: true, // 关闭软件提醒弹窗 9 | closeType: "hide", // 关闭方式 close 直接关闭 / hide 最小化到任务栏 10 | showTaskbarProgress: false, // 显示歌曲任务栏进度 11 | showSearchHistory: true, // 搜索历史 12 | autoSignIn: true, // 自动签到 13 | showGithub: true, 14 | showSider: true, // 显示侧边栏 15 | siderShowCover: false, // 侧边栏显示封面 16 | // 主题部分 17 | themeType: "dark", 18 | themeAuto: false, 19 | themeTypeName: "red", 20 | themeTypeData: {}, 21 | themeAutoCover: true, // 主题色跟随封面 22 | themeAutoCoverType: "secondary", 23 | // 播放部分 24 | playCoverType: "cover", // 播放器样式 25 | songLevel: "exhigh", // 歌曲音质 26 | autoPlay: false, // 程序启动时自动播放 27 | songVolumeFade: true, // 歌曲渐入渐出 28 | useUnmServer: true, // 是否使用网易云解灰 29 | countDownShow: true, // 是否显示前奏等待 30 | bottomLyricShow: true, // 底栏歌词显示 31 | playerBackgroundType: "blur", // 播放器背景类别 animation 流动 / blur 模糊 32 | memorySeek: true, // 记忆上次播放位置 33 | playSearch: false, // 是否播放全部搜索结果 34 | showPlaylistCount: true, // 是否显示播放列表数量 35 | showSpectrums: false, // 是否显示音乐频谱 36 | useMusicCache: false, // 是否采用音乐缓存 37 | // 数量部分 38 | loadSize: 50, // 每页加载数量 39 | // 歌词部分 40 | lrcMousePause: false, // 鼠标移入歌词区域暂停滚动 41 | lyricsFontSize: 46, // 歌词大小 42 | lyricsBlur: false, // 歌词模糊 43 | showYrc: true, // 是否显示逐字歌词 44 | showYrcAnimation: true, // 是否显示逐字歌词动画 45 | lyricsPosition: "left", // 歌词位置 46 | lyricsBlock: "start", // 歌词滚动位置 47 | showTransl: true, // 是否显示歌词翻译 48 | showRoma: true, // 是否显示歌词音译 49 | // 下载部分 50 | downloadPath: null, // 默认下载路径 51 | downloadMeta: true, // 同时下载元信息 52 | downloadCover: true, // 同时下载封面 53 | downloadLyrics: true, // 同时下载歌词 54 | downloadLyricsToFile: true, 55 | downloadCoverToFile: true, 56 | }; 57 | }, 58 | getters: {}, 59 | actions: { 60 | // 调整主题类别 61 | setThemeType(value) { 62 | this.themeType = value; 63 | this.themeAuto = false; 64 | $message.info(`已切换至${value === "light" ? "浅色" : "深色"}模式`, { showIcon: false }); 65 | }, 66 | }, 67 | // 数据持久化 68 | persist: [ 69 | { 70 | key: "siteSettings", 71 | storage: localStorage, 72 | }, 73 | ], 74 | }); 75 | 76 | export default useSiteSettingsStore; 77 | -------------------------------------------------------------------------------- /src/stores/siteStatus.js: -------------------------------------------------------------------------------- 1 | // 站点状态 2 | import { defineStore } from "pinia"; 3 | 4 | const useSiteStatusStore = defineStore("siteStatus", { 5 | state: () => { 6 | return { 7 | // 菜单折叠状态 8 | asideMenuCollapsed: false, 9 | // 搜索框状态 10 | searchInputFocus: false, 11 | // 是否展示播放控制条 12 | showPlayBar: true, 13 | // 播放状态 14 | playState: false, 15 | playLoading: false, 16 | playUseOtherSource: false, 17 | // 播放列表状态 18 | playListShow: false, 19 | // 全屏播放器状态 20 | showFullPlayer: false, 21 | // 播放器功能显示 22 | playerControlShow: true, 23 | controlTimeOut: null, 24 | // 实时播放进度 25 | playSeek: 0, 26 | // 是否下一首 27 | hasNextSong: false, 28 | // 封面主题 29 | coverTheme: {}, 30 | coverBackground: null, 31 | // 纯净歌词模式 32 | pureLyricMode: false, 33 | // 音乐频谱数据 34 | spectrumsData: [], 35 | // 当前歌曲歌词播放索引 36 | playSongLyricIndex: -1, 37 | // 播放时长数据 38 | playTimeData: { 39 | currentTime: 0, 40 | duration: 0, 41 | bar: 0, 42 | played: "00:00", 43 | durationTime: "00:00", 44 | }, 45 | // 默认倍速 46 | playRate: 1, 47 | // 默认音量 48 | playVolume: 0.7, 49 | // 静音前音量 50 | playVolumeMute: 0, 51 | // 当前播放索引 52 | playIndex: 0, 53 | // 当前模式 54 | // normal 正常 / fm 私人 FM / dj 电台 55 | playMode: "normal", 56 | // normal 顺序播放 / random 随机播放 / repeat 单曲循环 57 | playSongMode: "normal", 58 | // 是否为心动模式 59 | playHeartbeatMode: false, 60 | }; 61 | }, 62 | getters: {}, 63 | actions: {}, 64 | // 数据持久化 65 | persist: [ 66 | { 67 | key: "siteStatus", 68 | storage: localStorage, 69 | paths: [ 70 | "asideMenuCollapsed", 71 | "pureLyricMode", 72 | "playRate", 73 | "playVolume", 74 | "playVolumeMute", 75 | "playIndex", 76 | "playMode", 77 | "playSongMode", 78 | "playHeartbeatMode", 79 | "playTimeData", 80 | ], 81 | }, 82 | ], 83 | }); 84 | 85 | export default useSiteStatusStore; 86 | -------------------------------------------------------------------------------- /src/style/animate.scss: -------------------------------------------------------------------------------- 1 | // fade 2 | .fade-enter-active, 3 | .fade-leave-active { 4 | transition: opacity 0.3s ease-in-out; 5 | } 6 | 7 | .fade-enter-from, 8 | .fade-leave-to { 9 | opacity: 0; 10 | } 11 | 12 | // fadeDown 13 | .fadeDown-enter-active, 14 | .fadeDown-leave-active { 15 | transition: 16 | opacity 0.3s ease, 17 | transform 0.3s ease; 18 | } 19 | 20 | .fadeDown-enter-from, 21 | .fadeDown-leave-to { 22 | opacity: 0; 23 | transform: translateY(-10px); 24 | } 25 | 26 | // router 27 | .router-enter-active, 28 | .router-leave-active { 29 | transition: all 0.2s ease; 30 | } 31 | 32 | .router-enter-from, 33 | .router-leave-to { 34 | opacity: 0; 35 | // transform: scale(0.98); 36 | transform: translateX(10px); 37 | } 38 | 39 | // shrink 40 | .shrink-enter-active, 41 | .shrink-leave-active { 42 | opacity: 1; 43 | transform: scale(1); 44 | transition: 45 | transform 0.3s, 46 | opacity 0.3s; 47 | } 48 | .shrink-enter-from, 49 | .shrink-leave-to { 50 | opacity: 0; 51 | transform: scale(0.4); 52 | } 53 | 54 | // up 55 | .up-enter-active, 56 | .up-leave-active { 57 | transform: translateY(0); 58 | transition: transform 0.5s cubic-bezier(0.65, 0.05, 0.36, 1); 59 | } 60 | .up-enter-from, 61 | .up-leave-to { 62 | transform: translateY(100%); 63 | } 64 | 65 | // left 66 | .left-enter-active, 67 | .left-leave-active { 68 | opacity: 1; 69 | transform: translateX(0); 70 | transition: 71 | transform 0.3s, 72 | opacity 0.3s; 73 | } 74 | .left-enter-from, 75 | .left-leave-to { 76 | transform: translateX(-100%); 77 | } 78 | 79 | @keyframes skeleton-loading { 80 | 0% { 81 | background-position: 100% 50%; 82 | } 83 | 84 | 100% { 85 | background-position: 0 50%; 86 | } 87 | } 88 | 89 | @keyframes coverRotate { 90 | 0% { 91 | transform: rotate(0deg) scale(1) translateX(0); 92 | } 93 | 50% { 94 | transform: rotate(180deg) scale(2) translateX(40%); 95 | } 96 | 100% { 97 | transform: rotate(360deg) scale(1) translateX(0); 98 | } 99 | } 100 | 101 | @keyframes playerCoverRotate { 102 | 0% { 103 | transform: rotate(0deg); 104 | } 105 | 100% { 106 | transform: rotate(360deg); 107 | } 108 | } 109 | 110 | @keyframes fade-spacing { 111 | 0% { 112 | opacity: 0; 113 | } 114 | 100% { 115 | opacity: 1; 116 | letter-spacing: 12px; 117 | } 118 | } 119 | 120 | @keyframes fade-down { 121 | 0% { 122 | opacity: 0; 123 | transform: translateY(10px); 124 | } 125 | 100% { 126 | opacity: 1; 127 | transform: translateY(0); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/style/main.scss: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | user-select: none; 5 | -webkit-user-drag: none; 6 | } 7 | 8 | html, 9 | body, 10 | #app { 11 | width: 100vw; 12 | height: 100vh; 13 | font-family: "HarmonyOS_Regular", sans-serif !important; 14 | overflow: hidden; 15 | } 16 | 17 | // n-text 18 | .n-text { 19 | display: -webkit-box; 20 | overflow: hidden; 21 | word-break: break-all; 22 | -webkit-box-orient: vertical; 23 | -webkit-line-clamp: 1; 24 | } 25 | 26 | // n-image 27 | .n-image { 28 | .cover-loading { 29 | position: relative; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | width: 100%; 34 | height: 0; 35 | padding-bottom: 100%; 36 | background-color: var(--n-color); 37 | border-radius: 8px; 38 | .n-spin-body { 39 | position: absolute; 40 | height: 100%; 41 | top: 0; 42 | } 43 | &.mv { 44 | padding-bottom: 56%; 45 | } 46 | .loading-img { 47 | position: absolute; 48 | top: 0; 49 | width: 100%; 50 | height: 100%; 51 | opacity: 1 !important; 52 | } 53 | } 54 | } 55 | 56 | // n-scrollbar 57 | .n-scrollbar { 58 | .n-scrollbar-rail { 59 | right: 0 !important; 60 | z-index: 20; 61 | } 62 | } 63 | 64 | // n-message 65 | .n-message-container { 66 | bottom: 90px !important; 67 | .n-message-wrapper { 68 | .n-message { 69 | border-radius: 12px; 70 | } 71 | } 72 | } 73 | 74 | // n-dropdown 75 | .n-dropdown { 76 | --n-border-radius: 8px !important; 77 | } 78 | 79 | // n-notification 80 | .n-notification { 81 | --n-border-radius: 8px !important; 82 | } 83 | 84 | // n-skeleton 85 | .n-skeleton { 86 | background: linear-gradient( 87 | 90deg, 88 | var(--n-color-end) 25%, 89 | var(--n-color-start) 37%, 90 | var(--n-color-end) 63% 91 | ); 92 | background-size: 400% 100%; 93 | animation: skeleton-loading 1.4s ease infinite; 94 | } 95 | 96 | // n-tabs 97 | .n-tabs { 98 | --n-tab-border-radius: 6px !important; 99 | .n-tabs-rail { 100 | position: relative; 101 | .n-tabs-tab-wrapper { 102 | .n-tabs-tab { 103 | &:hover { 104 | transition: 105 | color 0.3s, 106 | background-color 0.5s ease-in-out, 107 | box-shadow 0.3s; 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | // n-modal 115 | .n-modal { 116 | width: 60vw; 117 | max-width: 700px; 118 | min-width: min(24rem, 100vw); 119 | border-radius: 8px; 120 | } 121 | .n-modal-mask { 122 | backdrop-filter: blur(16px); 123 | } 124 | 125 | // n-result 126 | .n-result { 127 | min-height: calc(100vh - 178px); 128 | display: flex; 129 | flex-direction: column; 130 | justify-content: center; 131 | } 132 | 133 | // n-popover 134 | .n-popover { 135 | border-radius: 8px !important; 136 | backdrop-filter: blur(10px); 137 | } 138 | 139 | // n-back-top 140 | .n-back-top { 141 | transition: 142 | color 0.3s cubic-bezier(0.4, 0, 0.2, 1), 143 | box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1), 144 | background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1), 145 | transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); 146 | &:active { 147 | transform: scale(0.9); 148 | } 149 | } 150 | 151 | // n-data 152 | .n-data-table { 153 | --n-merged-th-color: transparent !important; 154 | --n-merged-td-color: transparent !important; 155 | --n-merged-border-color: transparent !important; 156 | --n-merged-th-color-hover: transparent !important; 157 | --n-merged-td-color-hover: transparent !important; 158 | --n-merged-td-color-striped: transparent !important; 159 | .n-data-table__pagination { 160 | margin-top: 40px; 161 | margin-bottom: 30px; 162 | justify-content: center; 163 | } 164 | } 165 | 166 | // n-image-preview 167 | .n-image-preview-container { 168 | .n-image-preview-overlay { 169 | backdrop-filter: blur(16px); 170 | } 171 | } 172 | 173 | // layout-toggle-bar 174 | .n-layout-toggle-bar { 175 | height: 44px !important; 176 | top: calc(50% - 22px) !important; 177 | .n-layout-toggle-bar__top, 178 | .n-layout-toggle-bar__bottom { 179 | height: 24px !important; 180 | } 181 | .n-layout-toggle-bar__bottom { 182 | top: 20px !important; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import { logOut } from "@/api/login"; 2 | import { siteData } from "@/stores"; 3 | import Cookies from "js-cookie"; 4 | 5 | /** 6 | * 将服务器返回的 Cookie 数据设置到浏览器的 Cookie 和本地存储中 7 | */ 8 | export const setCookies = (cookieValue) => { 9 | const cookies = cookieValue.split(";;"); 10 | cookies.map((cookie) => { 11 | document.cookie = cookie; 12 | const cookieKeyValue = cookie.split(";")[0].split("="); 13 | localStorage.setItem(`cookie-${cookieKeyValue[0]}`, cookieKeyValue[1]); 14 | }); 15 | }; 16 | 17 | /** 18 | * 获取指定 Cookie 值 19 | * @param {string} key - 要获取的 Cookie 键名 20 | * @returns {string|null} Cookie 值,如果不存在则返回 null 21 | */ 22 | export const getCookie = (key) => { 23 | return Cookies.get(key) ?? localStorage.getItem(`cookie-${key}`); 24 | }; 25 | 26 | /** 27 | * 移除指定 Cookie 28 | * @param {string} key - 要移除的 Cookie 键名 29 | */ 30 | export const removeCookie = (key) => { 31 | Cookies.remove(key); 32 | localStorage.removeItem(`cookie-${key}`); 33 | }; 34 | 35 | /** 36 | * 检查用户是否已登录 37 | * @returns {boolean} 如果用户已登录,则返回 true;否则返回 false 38 | */ 39 | export const isLogin = () => { 40 | const data = siteData(); 41 | const status = getCookie("MUSIC_U") === undefined || getCookie("MUSIC_U") === null; 42 | // 更改状态 43 | data.userLoginStatus = !status; 44 | return !status; 45 | }; 46 | 47 | /** 48 | * 退出用户登录 49 | */ 50 | export const toLogout = async (show = true) => { 51 | const data = siteData(); 52 | // 去除 cookie 53 | await logOut(); 54 | removeCookie("MUSIC_U"); 55 | removeCookie("__csrf"); 56 | sessionStorage.clear(); 57 | // 去除用户信息 58 | data.userLoginStatus = false; 59 | data.userData = {}; 60 | data.userLikeData = { 61 | songs: [], 62 | playlists: [], 63 | albums: [], 64 | mvs: [], 65 | }; 66 | data.dailySongsData = { 67 | timestamp: null, 68 | data: [], 69 | }; 70 | if (show) $message.success("成功退出登录"); 71 | }; 72 | -------------------------------------------------------------------------------- /src/utils/base64.js: -------------------------------------------------------------------------------- 1 | // https://github.com/niklasvh/base64-arraybuffer/blob/master/src/index.ts 2 | // Copyright (c) 2012 Niklas von Hertzen Licensed under the MIT license. 3 | 4 | const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 5 | 6 | // Use a lookup table to find the index. 7 | const lookup = typeof Uint8Array === "undefined" ? [] : new Uint8Array(256); 8 | for (let i = 0; i < chars.length; i++) { 9 | lookup[chars.charCodeAt(i)] = i; 10 | } 11 | 12 | export const encode = (arraybuffer) => { 13 | let bytes = new Uint8Array(arraybuffer), 14 | i, 15 | len = bytes.length, 16 | base64 = ""; 17 | 18 | for (i = 0; i < len; i += 3) { 19 | base64 += chars[bytes[i] >> 2]; 20 | base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; 21 | base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; 22 | base64 += chars[bytes[i + 2] & 63]; 23 | } 24 | 25 | if (len % 3 === 2) { 26 | base64 = base64.substring(0, base64.length - 1) + "="; 27 | } else if (len % 3 === 1) { 28 | base64 = base64.substring(0, base64.length - 2) + "=="; 29 | } 30 | 31 | return base64; 32 | }; 33 | 34 | export const decode = (base64) => { 35 | let bufferLength = base64.length * 0.75, 36 | len = base64.length, 37 | i, 38 | p = 0, 39 | encoded1, 40 | encoded2, 41 | encoded3, 42 | encoded4; 43 | 44 | if (base64[base64.length - 1] === "=") { 45 | bufferLength--; 46 | if (base64[base64.length - 2] === "=") { 47 | bufferLength--; 48 | } 49 | } 50 | 51 | const arraybuffer = new ArrayBuffer(bufferLength), 52 | bytes = new Uint8Array(arraybuffer); 53 | 54 | for (i = 0; i < len; i += 4) { 55 | encoded1 = lookup[base64.charCodeAt(i)]; 56 | encoded2 = lookup[base64.charCodeAt(i + 1)]; 57 | encoded3 = lookup[base64.charCodeAt(i + 2)]; 58 | encoded4 = lookup[base64.charCodeAt(i + 3)]; 59 | 60 | bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); 61 | bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); 62 | bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); 63 | } 64 | 65 | return arraybuffer; 66 | }; 67 | -------------------------------------------------------------------------------- /src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 防抖函数 3 | * @param {Function} func - 要进行防抖处理的函数 4 | * @param {number} delay - 延迟时间,单位毫秒 5 | * @param {string} errorMessage - 重复触发时的提醒消息 6 | * @returns {Function} - 返回一个新的函数,该函数在指定的时间间隔内最多只会执行一次 7 | */ 8 | const debounce = (func, delay, errorMessage) => { 9 | let timerId; 10 | let lastCallTime = 0; 11 | 12 | // 返回一个新的函数 13 | return (...args) => { 14 | const currentTime = Date.now(); 15 | const elapsed = currentTime - lastCallTime; 16 | 17 | // 如果两次触发时间间隔小于 delay,弹出提醒 18 | if (elapsed < delay && errorMessage) { 19 | $message.warning(errorMessage); 20 | return; 21 | } 22 | 23 | // 清除上一个定时器 24 | clearTimeout(timerId); 25 | 26 | // 设置新的定时器,在指定的延迟时间后执行函数 27 | timerId = setTimeout(() => { 28 | func.apply(this, args); 29 | lastCallTime = Date.now(); 30 | }, delay); 31 | }; 32 | }; 33 | 34 | export default debounce; 35 | -------------------------------------------------------------------------------- /src/utils/formRules.js: -------------------------------------------------------------------------------- 1 | // 表单规则 2 | const formRules = () => { 3 | return { 4 | // 普通验证 5 | textRule: { 6 | required: true, 7 | message: "请填写必要信息", 8 | trigger: "blur", 9 | }, 10 | // 数字验证 11 | numberRule: { 12 | type: "number", 13 | required: true, 14 | message: "请填写必要信息", 15 | trigger: "blur", 16 | }, 17 | // 邮箱验证 18 | emailRule: { 19 | required: true, 20 | validator(_, value) { 21 | if (!value) { 22 | return new Error("请输入电子邮箱"); 23 | } else if ( 24 | !/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/.test( 25 | value, 26 | ) 27 | ) { 28 | return new Error("请输入正确的电子邮箱"); 29 | } 30 | return true; 31 | }, 32 | trigger: ["input", "blur"], 33 | }, 34 | // 手机号验证 35 | mobileRule: { 36 | key: "phone", 37 | required: true, 38 | validator(_, value) { 39 | if (!value) { 40 | return new Error("请输入手机号码"); 41 | } else if (!/^(?:(?:\+|00)86)?1[3-9]\d{9}$/.test(value)) { 42 | return new Error("请输入正确的手机号码"); 43 | } 44 | return true; 45 | }, 46 | trigger: ["input", "blur"], 47 | }, 48 | }; 49 | }; 50 | 51 | export { formRules }; 52 | -------------------------------------------------------------------------------- /src/utils/globalEvents.js: -------------------------------------------------------------------------------- 1 | import { checkPlatform } from "@/utils/helper"; 2 | import { playOrPause, changePlayIndex } from "@/utils/Player"; 3 | import { siteStatus } from "@/stores"; 4 | 5 | /** 6 | * 全局事件 7 | * @param {import('vue-router').Router} router - router 8 | */ 9 | const globalEvents = (router) => { 10 | if (!checkPlatform.electron()) return false; 11 | // 显示播放器 12 | electron.ipcRenderer.on("showPlayer", () => { 13 | const status = siteStatus(); 14 | if (status.playMode === "dj") return false; 15 | status.showFullPlayer = true; 16 | }); 17 | // 关闭播放器 18 | electron.ipcRenderer.on("closePlayer", () => { 19 | const status = siteStatus(); 20 | if (status.playMode === "dj") return false; 21 | status.showFullPlayer = false; 22 | }); 23 | // 播放或暂停 24 | electron.ipcRenderer.on("playOrPause", () => { 25 | playOrPause(); 26 | }); 27 | // 上一曲或下一曲 28 | electron.ipcRenderer.on("playNextOrPrev", (_, val) => { 29 | changePlayIndex(val, true); 30 | }); 31 | // 全局设置 32 | electron.ipcRenderer.on("open-setting", () => { 33 | if (router) router.push("/setting"); 34 | const status = siteStatus(); 35 | status.showFullPlayer = false; 36 | }); 37 | }; 38 | 39 | export default globalEvents; 40 | -------------------------------------------------------------------------------- /src/utils/globalShortcut.js: -------------------------------------------------------------------------------- 1 | import { playOrPause, setVolume } from "@/utils/Player"; 2 | import { siteStatus } from "@/stores"; 3 | 4 | /** 5 | * 全局快捷键监听 6 | * @param {KeyboardEvent} e - 键盘事件对象 7 | * @param {import('vue-router').Router} router - router 8 | * @returns {boolean} - 如果事件对象不存在,则返回false 9 | */ 10 | const globalShortcut = (e, router) => { 11 | if (!e) return false; 12 | e.preventDefault(); 13 | e.stopPropagation(); 14 | 15 | // 播放或暂停 16 | if (e.code === "Space") { 17 | if (e.target.tagName === "INPUT") return false; 18 | if (router.currentRoute.value.name === "videos-player") return false; 19 | playOrPause(); 20 | } 21 | 22 | // 调整音量 23 | if (e.code === "ArrowUp" || e.code === "ArrowDown") { 24 | const status = siteStatus(); 25 | const volume = status.playVolume; 26 | const delta = e.code === "ArrowUp" ? 0.1 : -0.1; 27 | const newVolume = Math.min(1, Math.max(0, volume + delta)); 28 | setVolume(newVolume); 29 | status.playVolume = newVolume; 30 | } 31 | }; 32 | 33 | export default globalShortcut; 34 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import { checkPlatform } from "@/utils/helper"; 2 | import { getCookie, isLogin } from "@/utils/auth"; 3 | import axios from "axios"; 4 | 5 | // 全局地址 6 | if (checkPlatform.electron()) { 7 | axios.defaults.baseURL = "/api"; 8 | } else { 9 | axios.defaults.baseURL = import.meta.env["RENDERER_VITE_SERVER_URL"]; 10 | } 11 | 12 | // 基础配置 13 | axios.defaults.timeout = 15000; 14 | axios.defaults.withCredentials = true; 15 | 16 | // 请求拦截 17 | axios.interceptors.request.use( 18 | (request) => { 19 | // 当请求的接口为特定地址时不附加cookie参数和realIP 20 | if (!request.url.startsWith('https://api.tunefree.fun/ncm/song/')) { 21 | if (!request.params) request.params = {}; 22 | // 附加 cookie 23 | if (!request.noCookie && (isLogin() || getCookie("MUSIC_U") !== null)) { 24 | request.params.cookie = `MUSIC_U=${getCookie("MUSIC_U")}`; 25 | // 对于非 auth.sayqz.com 的请求,添加分号 26 | if (!request.url.startsWith('https://auth.sayqz.com')) { 27 | request.params.cookie += ';'; 28 | } 29 | } 30 | // 附加 realIP 31 | if (!checkPlatform.electron()) request.params.realIP = "116.25.146.177"; 32 | } else { 33 | // 对特定地址的请求,去除 cookie 和 realIP 34 | if (request.params && request.params.cookie) { 35 | delete request.params.cookie; 36 | } 37 | if (request.params && request.params.realIP) { 38 | delete request.params.realIP; 39 | } 40 | } 41 | 42 | // 去除 cookie 43 | if (request.noCookie) { 44 | request.params.noCookie = true; 45 | } 46 | 47 | // 发送请求 48 | return request; 49 | }, 50 | (error) => { 51 | console.error("请求失败,请稍后重试"); 52 | return Promise.reject(error); 53 | }, 54 | ); 55 | 56 | // 响应拦截 57 | axios.interceptors.response.use( 58 | (response) => { 59 | return response?.data; 60 | }, 61 | (error) => { 62 | // 从错误对象中获取响应信息 63 | const response = error.response; 64 | // 断网处理 65 | if (!response) $canNotConnect(error); 66 | // 状态码处理 67 | switch (response?.status) { 68 | case 400: 69 | console.error("客户端错误:", response.status, response.statusText); 70 | // 执行客户端错误的处理逻辑 71 | break; 72 | case 401: 73 | console.error("未授权:", response.status, response.statusText); 74 | // 执行未授权的处理逻辑 75 | break; 76 | case 403: 77 | console.error("禁止访问:", response.status, response.statusText); 78 | // 执行禁止访问的处理逻辑 79 | break; 80 | case 404: 81 | console.error("未找到资源:", response.status, response.statusText); 82 | // 执行未找到资源的处理逻辑 83 | break; 84 | case 500: 85 | console.error("服务器错误:", response.status, response.statusText); 86 | // 执行服务器错误的处理逻辑 87 | break; 88 | default: 89 | // 处理其他状态码或错误条件 90 | console.error("未处理的错误:", error.message); 91 | } 92 | // 继续传递错误 93 | return Promise.reject(error); 94 | }, 95 | ); 96 | 97 | export default axios; 98 | -------------------------------------------------------------------------------- /src/utils/throttle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 节流函数 3 | * @param {Function} func - 要进行节流处理的函数 4 | * @param {number} delay - 延迟时间,单位毫秒 5 | * @param {string} errorMessage - 重复触发时的提醒消息 6 | * @returns {Function} - 返回一个新的函数,该函数在指定的时间间隔内最多只会执行一次 7 | */ 8 | const throttle = (func, delay, errorMessage) => { 9 | let isThrottled = false; 10 | 11 | // 返回一个新的函数 12 | return (...args) => { 13 | if (!isThrottled) { 14 | isThrottled = true; 15 | func.apply(this, args); 16 | setTimeout(() => { 17 | isThrottled = false; 18 | }, delay); 19 | } else if (errorMessage) { 20 | $message.warning(errorMessage); 21 | } 22 | }; 23 | }; 24 | 25 | export default throttle; 26 | -------------------------------------------------------------------------------- /src/utils/userSignIn.js: -------------------------------------------------------------------------------- 1 | import { userDailySignin } from "@/api/user"; 2 | import { siteSettings } from "@/stores"; 3 | import { isLogin } from "@/utils/auth"; 4 | 5 | /** 6 | * 用户签到 7 | * https://github.com/Binaryify/NeteaseCloudMusicApi/issues/1387 8 | * 云贝签到本质上就是 Android 客户端每日签到 9 | */ 10 | const userSignIn = async () => { 11 | const settings = siteSettings(); 12 | try { 13 | if (!isLogin()) return false; 14 | const today = new Date().toLocaleDateString(); 15 | const lastSignInDate = sessionStorage.getItem("lastSignInDate"); 16 | if (lastSignInDate !== today) { 17 | const result = await userDailySignin(1); 18 | console.log("签到结果:", result); 19 | sessionStorage.setItem("lastSignInDate", today); 20 | if (result.status === 400) { 21 | return console.log("重复签到"); 22 | } 23 | $notification["success"]({ 24 | content: "签到通知", 25 | meta: "🎉 每日签到成功", 26 | duration: 3000, 27 | }); 28 | } else { 29 | console.log("今日已签到"); 30 | } 31 | } catch (error) { 32 | if (error.request.status === 400) { 33 | console.log("重复签到"); 34 | sessionStorage.setItem("lastSignInDate", new Date().toLocaleDateString()); 35 | return false; 36 | } 37 | settings.autoSignIn = false; 38 | console.error("签到过程中发生错误:", error); 39 | $notification["error"]({ 40 | content: "签到通知", 41 | meta: "签到过程中发生错误,已关闭自动签到,详细信息可查看控制台输出,请及时向开发者报告", 42 | duration: 8000, 43 | }); 44 | } 45 | }; 46 | 47 | export default userSignIn; 48 | -------------------------------------------------------------------------------- /src/views/Artist/albums.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 93 | -------------------------------------------------------------------------------- /src/views/Artist/hot.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 63 | 64 | 85 | -------------------------------------------------------------------------------- /src/views/Artist/songs.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 95 | -------------------------------------------------------------------------------- /src/views/Artist/videos.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 100 | -------------------------------------------------------------------------------- /src/views/DailySongs.vue: -------------------------------------------------------------------------------- 1 | 2 | 48 | 49 | 103 | 104 | 131 | -------------------------------------------------------------------------------- /src/views/Discover/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 45 | 46 | 59 | -------------------------------------------------------------------------------- /src/views/Discover/toplists.vue: -------------------------------------------------------------------------------- 1 | 2 | 20 | 21 | 59 | -------------------------------------------------------------------------------- /src/views/Dj/type.vue: -------------------------------------------------------------------------------- 1 | 2 | 27 | 28 | 60 | 61 | 79 | -------------------------------------------------------------------------------- /src/views/History.vue: -------------------------------------------------------------------------------- 1 | 2 | 29 | 30 | 50 | 51 | 80 | -------------------------------------------------------------------------------- /src/views/Like/albums.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /src/views/Like/artists.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /src/views/Like/djs.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /src/views/Like/index.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 92 | 93 | 130 | -------------------------------------------------------------------------------- /src/views/Like/playlists.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 79 | 80 | 108 | -------------------------------------------------------------------------------- /src/views/Like/videos.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 26 | -------------------------------------------------------------------------------- /src/views/Local/artists.vue: -------------------------------------------------------------------------------- 1 | 2 | 36 | 37 | 110 | 111 | 143 | -------------------------------------------------------------------------------- /src/views/Local/songs.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/views/Search/albums.vue: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 106 | -------------------------------------------------------------------------------- /src/views/Search/artists.vue: -------------------------------------------------------------------------------- 1 | 2 | 24 | 25 | 89 | -------------------------------------------------------------------------------- /src/views/Search/djs.vue: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 107 | -------------------------------------------------------------------------------- /src/views/Search/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 35 | 36 | 67 | 68 | 87 | -------------------------------------------------------------------------------- /src/views/Search/playlists.vue: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 107 | -------------------------------------------------------------------------------- /src/views/Search/songs.vue: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 107 | -------------------------------------------------------------------------------- /src/views/Search/videos.vue: -------------------------------------------------------------------------------- 1 | 2 | 31 | 32 | 107 | -------------------------------------------------------------------------------- /src/views/Song.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 46 | -------------------------------------------------------------------------------- /src/views/State/403.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /src/views/State/404.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /src/views/State/500.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /src/views/Test.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 70 | 71 | 93 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/:path*", "destination": "/index.html" }] 3 | } 4 | --------------------------------------------------------------------------------