diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03f2450 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +node_modules +npm-debug.log +Dockerfile* +docker-compose* +.dockerignore +.git +.github +.gitignore +README.md +LICENSE +.vscode +dist +dist_electron +build +images +script \ No newline at end of file diff --git a/.env.example b/.env.example index bd591ef..7c0058a 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ -VUE_APP_NETEASE_API_URL=http://127.0.0.1:3000 +VUE_APP_NETEASE_API_URL=/api VUE_APP_ELECTRON_API_URL=/api -VUE_APP_ELECTRON_API_URL_DEV=http://127.0.0.1:3000 -VUE_APP_ENABLE_SENTRY=false -DEV_SERVER_PORT=20201 \ No newline at end of file +VUE_APP_ELECTRON_API_URL_DEV=http://127.0.0.1:10754 +VUE_APP_LASTFM_API_KEY=09c55292403d961aa517ff7f5e8a3d9c +VUE_APP_LASTFM_API_SHARED_SECRET=307c9fda32b3904e53654baff215cb67 +DEV_SERVER_PORT=20201 + diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..fd24c66 --- /dev/null +++ b/.envrc @@ -0,0 +1,4 @@ +source_url "https://raw.githubusercontent.com/cachix/devenv/82c0147677e510b247d8b9165c54f73d32dfd899/direnvrc" "sha256-7u4iDd1nZpxL4tCzmPG0dQgC5V+/44Ba+tHkPob1v2k=" + +export NIXPKGS_ALLOW_INSECURE=1 +use devenv diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ac10488 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +* text eol=lf +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.mp3 binary +*.icns binary +*.gif binary diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 77cc6a7..4caf481 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,6 +1,15 @@ -name: Build/release +name: Release -on: [push, pull_request] +env: + YARN_INSTALL_NOPT: yarn add --ignore-platform --ignore-optional + +on: + push: + branches: + - master + tags: + - v* + workflow_dispatch: jobs: release: @@ -8,19 +17,75 @@ jobs: strategy: matrix: - os: [macos-latest, windows-latest] + os: [macos-latest, windows-latest, ubuntu-22.04] steps: - name: Check out Git repository - uses: actions/checkout@v2 + uses: actions/checkout@v3 + with: + submodules: "recursive" - name: Install Node.js, NPM and Yarn - uses: actions/setup-node@v1 + uses: actions/setup-node@v3 with: - node-version: 12.16.3 + node-version: 16 + cache: 'yarn' + + - name: Install RPM & Pacman (on Ubuntu) + if: runner.os == 'Linux' + run: | + sudo apt-get update && + sudo apt-get install --no-install-recommends -y rpm && + sudo apt-get install --no-install-recommends -y libarchive-tools && + sudo apt-get install --no-install-recommends -y libopenjp2-tools + + - name: Install Snapcraft (on Ubuntu) + uses: samuelmeuli/action-snapcraft@v1 + if: startsWith(matrix.os, 'ubuntu') + with: + snapcraft_token: ${{ secrets.snapcraft_token }} + + - id: get_unm_version + name: Get the installed UNM version + run: | + yarn --ignore-optional + unm_version=$(node -e "console.log(require('./node_modules/@unblockneteasemusic/rust-napi/package.json').version)") + echo "::set-output name=unmver::${unm_version}" + shell: bash + + - name: Install UNM dependencies for Windows + if: runner.os == 'Windows' + run: | + ${{ env.YARN_INSTALL_NOPT }} \ + @unblockneteasemusic/rust-napi-win32-x64-msvc@${{steps.get_unm_version.outputs.unmver}} + shell: bash + + - name: Install UNM dependencies for macOS + if: runner.os == 'macOS' + run: | + ${{ env.YARN_INSTALL_NOPT }} \ + @unblockneteasemusic/rust-napi-darwin-x64@${{steps.get_unm_version.outputs.unmver}} \ + @unblockneteasemusic/rust-napi-darwin-arm64@${{steps.get_unm_version.outputs.unmver}} \ + dmg-license + shell: bash + + - name: Install UNM dependencies for Linux + if: runner.os == 'Linux' + run: | + ${{ env.YARN_INSTALL_NOPT }} \ + @unblockneteasemusic/rust-napi-linux-x64-gnu@${{steps.get_unm_version.outputs.unmver}} \ + @unblockneteasemusic/rust-napi-linux-arm64-gnu@${{steps.get_unm_version.outputs.unmver}} \ + @unblockneteasemusic/rust-napi-linux-arm-gnueabihf@${{steps.get_unm_version.outputs.unmver}} + shell: bash - name: Build/release Electron app - uses: samuelmeuli/action-electron-builder@v1 + uses: samuelmeuli/action-electron-builder@v1.6.0 + env: + VUE_APP_NETEASE_API_URL: /api + VUE_APP_ELECTRON_API_URL: /api + VUE_APP_ELECTRON_API_URL_DEV: http://127.0.0.1:10754 + VUE_APP_LASTFM_API_KEY: 09c55292403d961aa517ff7f5e8a3d9c + VUE_APP_LASTFM_API_SHARED_SECRET: 307c9fda32b3904e53654baff215cb67 with: # GitHub token, automatically provided to the action # (No need to define this secret in the repo settings) @@ -32,14 +97,20 @@ jobs: use_vue_cli: true - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: YesPlayMusic-mac - path: dist_electron/*.dmg + path: dist_electron/*-universal.dmg if-no-files-found: ignore - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 with: name: YesPlayMusic-win - path: dist_electron/*.exe + path: dist_electron/*Setup*.exe + if-no-files-found: ignore + + - uses: actions/upload-artifact@v3 + with: + name: YesPlayMusic-linux + path: dist_electron/*.AppImage if-no-files-found: ignore diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml new file mode 100644 index 0000000..f8e0be8 --- /dev/null +++ b/.github/workflows/sync.yml @@ -0,0 +1,48 @@ +name: Upstream Sync + +permissions: + contents: write + issues: write + actions: write + +on: + schedule: + - cron: '0 * * * *' # every hour + workflow_dispatch: + +jobs: + sync_latest_from_upstream: + name: Sync latest commits from upstream repo + runs-on: ubuntu-latest + if: ${{ github.event.repository.fork }} + + steps: + - uses: actions/checkout@v4 + + - name: Clean issue notice + uses: actions-cool/issues-helper@v3 + with: + actions: 'close-issues' + labels: '🚨 Sync Fail' + + - name: Sync upstream changes + id: sync + uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 + with: + upstream_sync_repo: qier222/YesPlayMusic + upstream_sync_branch: master + target_sync_branch: master + target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set + test_mode: false + + - name: Sync check + if: failure() + uses: actions-cool/issues-helper@v3 + with: + actions: 'create-issue' + title: '🚨 同步失败 | Sync Fail' + labels: '🚨 Sync Fail' + body: | + Due to a change in the workflow file of the upstream repository, GitHub has automatically suspended the scheduled automatic update. You need to manually sync your fork. + + 由于上游仓库的 workflow 文件变更,导致 GitHub 自动暂停了本次自动更新,你需要手动 Sync Fork 一次。 diff --git a/.gitignore b/.gitignore index e02847c..d8dd8fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,6 @@ .DS_Store node_modules /dist -dist_electron -Icon? # local env files .env @@ -25,9 +23,21 @@ pnpm-debug.log* *.sw? .vercel -/netease_api #Electron-builder output /dist_electron NeteaseCloudMusicApi-master NeteaseCloudMusicApi-master.zip + +# Local Netlify folder +.netlify +vercel.json +# Devenv +.devenv* +devenv.local.nix + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/.npmrc b/.npmrc deleted file mode 100644 index adcf848..0000000 --- a/.npmrc +++ /dev/null @@ -1,4 +0,0 @@ -# 如果发现 npm / yarn 安装太慢,可以解除注释 -# registry=https://registry.npm.taobao.org/ -# ELECTRON_MIRROR=https://npm.taobao.org/mirrors/electron -# phantomjs_cdnurl=https://npm.taobao.org/dist/phantomjs \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..da2d398 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +14 \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 15709e8..13859d2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,5 +1,3 @@ build coverage dist -netease_api - diff --git a/.prettierrc b/.prettierrc index 86f82d1..38800a3 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,10 +3,9 @@ "tabWidth": 2, "useTabs": false, "semi": true, - "singleQuote": false, + "singleQuote": true, "jsxSingleQuote": true, - "jsxBracketSameLine": false, - "arrowParens": "always", + "arrowParens": "avoid", "endOfLine": "lf", "bracketSpacing": true, "htmlWhitespaceSensitivity": "strict" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2fb8f95 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM node:16.13.1-alpine as build +ENV VUE_APP_NETEASE_API_URL=/api +WORKDIR /app +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories &&\ + apk add --no-cache python3 make g++ git +COPY package.json yarn.lock ./ +RUN yarn install +COPY . . +RUN yarn config set electron_mirror https://npmmirror.com/mirrors/electron/ && \ + yarn build + +FROM nginx:1.20.2-alpine as app + +COPY --from=build /app/package.json /usr/local/lib/ + +RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories &&\ + apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main libuv \ + && apk add --no-cache --update-cache --repository http://dl-cdn.alpinelinux.org/alpine/v3.14/main nodejs npm \ + && npm i -g $(awk -F \" '{if($2=="NeteaseCloudMusicApi") print $2"@"$4}' /usr/local/lib/package.json) \ + && rm -f /usr/local/lib/package.json + +COPY --from=build /app/docker/nginx.conf.example /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html + +CMD nginx ; exec npx NeteaseCloudMusicApi \ No newline at end of file diff --git a/LICENSE b/LICENSE index d8911ac..7385fd2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 qier222 +Copyright (c) 2020-2023 qier222 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/README.md b/README.md index e6241fe..146f524 100644 --- a/README.md +++ b/README.md @@ -8,69 +8,221 @@

高颜值的第三方网易云播放器
- 🌎 访问DEMO  |   - 📦️ 下载安装包 + 🌎 访问DEMO  |   + 📦️ 下载安装包  |   + 💬 加入交流群

-[![Library][library-screenshot]](https://music.qier222.com) +[![Library][library-screenshot]](https://music.ineko.cc) + + +## 全新版本 +全新2.0 Alpha测试版已发布,欢迎前往 [Releases](https://github.com/qier222/YesPlayMusic/releases) 页面下载。 +当前版本将会进入维护模式,除重大bug修复外,不会再更新新功能。 ## ✨ 特性 - ✅ 使用 Vue.js 全家桶开发 -- 🔴 网易云账号登录 -- 📺 MV 播放 +- 🔴 网易云账号登录(扫码/手机/邮箱登录) +- 📺 支持 MV 播放 - 📃 支持歌词显示 +- 📻 支持私人 FM / 每日推荐歌曲 - 🚫🤝 无任何社交功能 - 🌎️ 海外用户可直接播放(需要登录网易云账号) -- 🔐 支持 [UnblockNeteaseMusic](https://github.com/nondanee/UnblockNeteaseMusic),自动使用 QQ/酷狗/酷我音源替换变灰歌曲链接 (网页版不支持) -- ⏭️ 支持 MediaSession API,可以使用系统快捷键操作上一首下一首 +- 🔐 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server#音源清单),自动使用[各类音源](https://github.com/UnblockNeteaseMusic/server#音源清单)替换变灰歌曲链接 (网页版不支持) + - 「各类音源」指默认启用的音源。 + - YouTube 音源需自行安装 `yt-dlp`。 - ✔️ 每日自动签到(手机端和电脑端同时签到) - 🌚 Light/Dark Mode 自动切换 - 👆 支持 Touch Bar - 🖥️ 支持 PWA,可在 Chrome/Edge 里点击地址栏右边的 ➕ 安装到电脑 -- 🙉 支持显示歌曲和专辑的 Explicit 标志 +- 🟥 支持 Last.fm Scrobble +- ☁️ 支持音乐云盘 +- ⌨️ 自定义快捷键和全局快捷键 +- 🎧 支持 Mpris - 🛠 更多特性开发中 ## 📦️ 安装 Electron 版本由 [@hawtim](https://github.com/hawtim) 和 [@qier222](https://github.com/qier222) 适配并维护,支持 macOS、Windows、Linux。 -访问本项目的 [Releases](https://github.com/qier222/YesPlayMusic/releases) 页面下载安装包,或者访问 [镜像下载站 (大陆访问更快)](https://dl.qier222.com/YesPlayMusic/) 下载。 +访问本项目的 [Releases](https://github.com/qier222/YesPlayMusic/releases) +页面下载安装包。 -## ⚙️ 部署至服务器 +- macOS 用户可以通过 Homebrew 来安装:`brew install --cask yesplaymusic` -除了下载安装包使用,你还可以将本项目部署到你的服务器上。 +- Windows 用户可以通过 Scoop 来安装:`scoop install extras/yesplaymusic` + +## ⚙️ 部署至 Vercel + +除了下载安装包使用,你还可以将本项目部署到 Vercel 或你的服务器上。下面是部署到 Vercel 的方法。 + +本项目的 Demo (https://music.qier222.com) 就是部署在 Vercel 上的网站。 + +[![Powered by Vercel](https://www.datocms-assets.com/31049/1618983297-powered-by-vercel.svg)](https://vercel.com/?utm_source=ohmusic&utm_campaign=oss) + +1. 部署网易云 API,详情参见 [Binaryify/NeteaseCloudMusicApi](https://neteasecloudmusicapi.vercel.app/#/?id=%e5%ae%89%e8%a3%85) + 。你也可以将 API 部署到 Vercel。 + +2. 点击本仓库右上角的 Fork,复制本仓库到你的 GitHub 账号。 + +3. 点击仓库的 Add File,选择 Create new file,输入 `vercel.json`,将下面的内容复制粘贴到文件中,并将 `https://your-netease-api.example.com` 替换为你刚刚部署的网易云 API 地址: + +```json +{ + "rewrites": [ + { + "source": "/api/:match*", + "destination": "https://your-netease-api.example.com/:match*" + } + ] +} +``` + +4. 打开 [Vercel.com](https://vercel.com),使用 GitHub 登录。 + +5. 点击 Import Git Repository 并选择你刚刚复制的仓库并点击 Import。 + +6. 点击 PERSONAL ACCOUNT 旁边的 Select。 + +7. 点击 Environment Variables,填写 Name 为 `VUE_APP_NETEASE_API_URL`,Value 为 `/api`,点击 Add。最后点击底部的 Deploy 就可以部署到 + Vercel 了。 + +## ⚙️ 部署到自己的服务器 + +除了部署到 Vercel,你还可以部署到自己的服务器上 1. 部署网易云 API,详情参见 [Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) - 2. 克隆本仓库 ```sh -git clone https://github.com/qier222/YesPlayMusic.git +git clone --recursive https://github.com/qier222/YesPlayMusic.git ``` 3. 安装依赖 ```sh yarn install + ``` -4. 复制 `/.env.example` 文件为 `/.env`,修改里面 `VUE_APP_NETEASE_API_URL` 的值为网易云 API 地址。本地开发的话可以填写 API 地址为 `http://localhost:3000`,YesPlayMusic 地址为 `http://localhost:8080` +4. (可选)使用 Nginx 反向代理 API,将 API 路径映射为 `/api`,如果 API 和网页不在同一个域名下的话(跨域),会有一些 bug。 + +5. 复制 `/.env.example` 文件为 `/.env`,修改里面 `VUE_APP_NETEASE_API_URL` 的值为网易云 API 地址。本地开发的话可以填写 API 地址为 `http://localhost:3000`,YesPlayMusic 地址为 `http://localhost:8080`。如果你使用了反向代理 API,可以填写 API 地址为 `/api`。 ``` VUE_APP_NETEASE_API_URL=http://localhost:3000 ``` -5. 编译打包 +6. 编译打包 ```sh yarn run build ``` -6. 将 `/dist` 目录下的文件上传到你的 Web 服务器 +7. 将 `/dist` 目录下的文件上传到你的 Web 服务器 + +## ⚙️ 宝塔面板 docker应用商店 部署 + +1. 安装宝塔面板,前往[宝塔面板官网](https://www.bt.cn/new/download.html) ,选择正式版的脚本下载安装。 + +2. 安装后登录宝塔面板,在左侧导航栏中点击 Docker,首次进入会提示安装Docker服务,点击立即安装,按提示完成安装 + +3. 安装完成后在应用商店中找到YesPlayMusic,点击安装,配置域名、端口等基本信息即可完成安装。 + +4. 安装后在浏览器输入上一步骤设置的域名即可访问。 + +## ⚙️ Docker 部署 + +1. 构建 Docker Image + +```sh +docker build -t yesplaymusic . +``` + +2. 启动 Docker Container + +```sh +docker run -d --name YesPlayMusic -p 80:80 yesplaymusic +``` + +3. Docker Compose 启动 + +```sh +docker-compose up -d +``` + +YesPlayMusic 地址为 `http://localhost` + +## ⚙️ 部署至 Replit + +1. 新建 Repl,选择 Bash 模板 + +2. 在 Replit shell 中运行以下命令 + +```sh +bash <(curl -s -L https://raw.githubusercontent.com/qier222/YesPlayMusic/main/install-replit.sh) +``` + +3. 首次运行成功后,只需点击绿色按钮 `Run` 即可再次运行 + +4. 由于 replit 个人版限制内存为 1G(教育版为 3G),构建过程中可能会失败,请再次运行上述命令或运行以下命令: + +```sh +cd /home/runner/${REPL_SLUG}/music && yarn install && yarn run build +``` + +## 👷‍♂️ 打包客户端 + +如果在 Release 页面没有找到适合你的设备的安装包的话,你可以根据下面的步骤来打包自己的客户端。 + +1. 打包 Electron 需要用到 Node.js 和 Yarn。可前往 [Node.js 官网](https://nodejs.org/zh-cn/) 下载安装包。安装 Node.js + 后可在终端里执行 `npm install -g yarn` 来安装 Yarn。 + +2. 使用 `git clone --recursive https://github.com/qier222/YesPlayMusic.git` 克隆本仓库到本地。 + +3. 使用 `yarn install` 安装项目依赖。 + +4. 复制 `/.env.example` 文件为 `/.env` 。 + +5. 选择下列表格的命令来打包适合的你的安装包,打包出来的文件在 `/dist_electron` 目录下。了解更多信息可访问 [electron-builder 文档](https://www.electron.build/cli) + +| 命令 | 说明 | +| ------------------------------------------ | ------------------------- | +| `yarn electron:build --windows nsis:ia32` | Windows 32 位 | +| `yarn electron:build --windows nsis:arm64` | Windows ARM | +| `yarn electron:build --linux deb:armv7l` | Debian armv7l(树莓派等) | +| `yarn electron:build --macos dir:arm64` | macOS ARM | + +## :computer: 配置开发环境 + +本项目由 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 提供 API。 + +运行本项目 + +```shell +# 安装依赖 +yarn install + +# 创建本地环境变量 +cp .env.example .env + +# 运行(网页端) +yarn serve + +# 运行(electron) +yarn electron:serve +``` + +本地运行 NeteaseCloudMusicApi,或者将 API [部署至 Vercel](#%EF%B8%8F-部署至-vercel) + +```shell +# 运行 API (默认 3000 端口) +yarn netease_api:run +``` ## ☑️ Todo @@ -86,6 +238,8 @@ yarn run build ## 灵感来源 +API 源代码来自 [Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) + - [Apple Music](https://music.apple.com) - [YouTube Music](https://music.youtube.com) - [Spotify](https://www.spotify.com) @@ -93,12 +247,14 @@ yarn run build ## 🖼️ 截图 -[![artist][artist-screenshot]](https://music.qier222.com) -[![album][album-screenshot]](https://music.qier222.com) -[![playlist][playlist-screenshot]](https://music.qier222.com) -[![explore][explore-screenshot]](https://music.qier222.com) -[![search][search-screenshot]](https://music.qier222.com) -[![home][home-screenshot]](https://music.qier222.com) +![lyrics][lyrics-screenshot] +![library-dark][library-dark-screenshot] +![album][album-screenshot] +![home-2][home-2-screenshot] +![artist][artist-screenshot] +![search][search-screenshot] +![home][home-screenshot] +![explore][explore-screenshot] @@ -107,6 +263,8 @@ yarn run build [artist-screenshot]: images/artist.png [explore-screenshot]: images/explore.png [home-screenshot]: images/home.png +[home-2-screenshot]: images/home-2.png +[lyrics-screenshot]: images/lyrics.png [library-screenshot]: images/library.png -[playlist-screenshot]: images/playlist.png +[library-dark-screenshot]: images/library-dark.png [search-screenshot]: images/search.png diff --git a/babel.config.js b/babel.config.js index c0fa85e..707ce4a 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,7 +1,11 @@ module.exports = { - presets: ["@vue/cli-plugin-babel/preset"], - plugins: [ - "@babel/plugin-proposal-nullish-coalescing-operator", - "@babel/plugin-proposal-optional-chaining", + presets: [ + [ + '@vue/cli-plugin-babel/preset', + { + useBuiltIns: 'usage', + shippedProposals: true, + }, + ], ], }; diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 0000000..9250350 --- /dev/null +++ b/devenv.lock @@ -0,0 +1,132 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1730412360, + "owner": "cachix", + "repo": "devenv", + "rev": "45847cb1f14a6d8cfa86ea943703c54a8798ae7e", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1730272153, + "owner": "nixos", + "repo": "nixpkgs", + "rev": "2d2a9ddbe3f2c00747398f3dc9b05f7f2ebb0f53", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1730327045, + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "080166c15633801df010977d9d7474b4a6c549d7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nodejs16": { + "locked": { + "lastModified": 1700230496, + "owner": "nixos", + "repo": "nixpkgs", + "rev": "a71323f68d4377d12c04a5410e214495ec598d4c", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "rev": "a71323f68d4377d12c04a5410e214495ec598d4c", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1730302582, + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "af8a16fe5c264f5e9e18bcee2859b40a656876cf", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "nixpkgs": "nixpkgs", + "nodejs16": "nodejs16", + "pre-commit-hooks": "pre-commit-hooks" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 0000000..fd6b9ce --- /dev/null +++ b/devenv.nix @@ -0,0 +1,53 @@ +{ pkgs, lib, config, inputs, ... }: + +let + nodejs16 = import inputs.nodejs16 { system = pkgs.stdenv.system; }; +in +{ + # https://devenv.sh/basics/ + env.GREET = "devenv"; + + # https://devenv.sh/packages/ + packages = [ pkgs.git ] ++ lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk; [ + frameworks.AppKit + ]); + + # https://devenv.sh/languages/ + languages.javascript.enable = true; + languages.javascript.package = nodejs16.pkgs.nodejs_16; + languages.javascript.corepack.enable = true; + # languages.rust.enable = true; + + # https://devenv.sh/processes/ + # processes.cargo-watch.exec = "cargo-watch"; + + # https://devenv.sh/services/ + # services.postgres.enable = true; + + # https://devenv.sh/scripts/ + scripts.hello.exec = '' + echo hello from $GREET + ''; + + enterShell = '' + hello + git --version + ''; + + # https://devenv.sh/tasks/ + # tasks = { + # "myproj:setup".exec = "mytool build"; + # "devenv:enterShell".after = [ "myproj:setup" ]; + # }; + + # https://devenv.sh/tests/ + enterTest = '' + echo "Running tests" + git --version | grep --color=auto "${pkgs.git.version}" + ''; + + # https://devenv.sh/pre-commit-hooks/ + # pre-commit.hooks.shellcheck.enable = true; + + # See full reference at https://devenv.sh/reference/options/ +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 0000000..7019c5c --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,19 @@ +# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json +inputs: + nixpkgs: + url: github:nixos/nixpkgs/nixpkgs-unstable + nodejs16: + url: github:nixos/nixpkgs/a71323f68d4377d12c04a5410e214495ec598d4c + +# https://github.com/cachix/devenv/issues/792#issuecomment-2043166453 +impure: true +# If you're using non-OSS software, you can set allowUnfree to true. +# allowUnfree: true + +# If you're willing to use a package that's vulnerable +# permittedInsecurePackages: +# - "openssl-1.1.1w" + +# If you have more than one devenv you can merge them +#imports: +# - ./backend diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0930218 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +services: + YesPlayMusic: + build: + context: . + image: yesplaymusic + container_name: YesPlayMusic + volumes: + - /etc/localtime:/etc/localtime:ro + - /etc/timezone:/etc/timezone:ro + - ./docker/nginx.conf.example:/etc/nginx/conf.d/default.conf:ro + ports: + - 80:80 + restart: always + depends_on: + - UnblockNeteaseMusic + environment: + - NODE_TLS_REJECT_UNAUTHORIZED=0 + networks: + my_network: + + UnblockNeteaseMusic: + image: pan93412/unblock-netease-music-enhanced + command: -o kugou kuwo migu bilibili pyncmd -p 80:443 -f 45.127.129.53 -e - + # environment: + # JSON_LOG: true + # LOG_LEVEL: debug + networks: + my_network: + aliases: + - music.163.com + - interface.music.163.com + - interface3.music.163.com + - interface.music.163.com.163jiasu.com + - interface3.music.163.com.163jiasu.com + restart: always + +networks: + my_network: + driver: bridge diff --git a/docker/nginx.conf.example b/docker/nginx.conf.example new file mode 100644 index 0000000..cdab218 --- /dev/null +++ b/docker/nginx.conf.example @@ -0,0 +1,28 @@ +server { + gzip on; + listen 80; + listen [::]:80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + index index.html; + try_files $uri $uri/ /index.html; + } + + location @rewrites { + rewrite ^(.*)$ /index.html last; + } + + location /api/ { + proxy_buffers 16 32k; + proxy_buffer_size 128k; + proxy_busy_buffers_size 128k; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Host $remote_addr; + proxy_set_header X-NginX-Proxy true; + proxy_pass http://localhost:3000/; + } +} diff --git a/images/album.png b/images/album.png index 66a7715..bf5936c 100644 Binary files a/images/album.png and b/images/album.png differ diff --git a/images/artist.png b/images/artist.png index d02c91a..7b5ac77 100644 Binary files a/images/artist.png and b/images/artist.png differ diff --git a/images/explore.png b/images/explore.png index bcf4f76..7fd0cfc 100644 Binary files a/images/explore.png and b/images/explore.png differ diff --git a/images/home-2.png b/images/home-2.png new file mode 100644 index 0000000..0b98d7d Binary files /dev/null and b/images/home-2.png differ diff --git a/images/home.png b/images/home.png index 3be5d9f..6de45c6 100644 Binary files a/images/home.png and b/images/home.png differ diff --git a/images/library-dark.png b/images/library-dark.png new file mode 100644 index 0000000..ca9f67a Binary files /dev/null and b/images/library-dark.png differ diff --git a/images/library.png b/images/library.png index d6bef0f..bd28ce0 100644 Binary files a/images/library.png and b/images/library.png differ diff --git a/images/lyrics.png b/images/lyrics.png new file mode 100644 index 0000000..522747a Binary files /dev/null and b/images/lyrics.png differ diff --git a/images/playlist.png b/images/playlist.png deleted file mode 100644 index 90aa61d..0000000 Binary files a/images/playlist.png and /dev/null differ diff --git a/images/search.png b/images/search.png index fd4320a..2befdfc 100644 Binary files a/images/search.png and b/images/search.png differ diff --git a/install-replit.sh b/install-replit.sh new file mode 100644 index 0000000..c17e438 --- /dev/null +++ b/install-replit.sh @@ -0,0 +1,28 @@ + #!/usr/bin/bash + +# 初始化 .replit 和 replit.nix +if [[ $1 == i ]];then + echo -e 'run = ["bash", "main.sh"]\n\nentrypoint = "main.sh"' >.replit + echo -e "{ pkgs }: {\n\t\tdeps = [\n\t\t\tpkgs.nodejs-16_x\n\t\t\tpkgs.yarn\n\t\t\tpkgs.bashInteractive\n\t\t];\n}" > replit.nix + exit +fi + +# 安装 +if [[ ! -d api ]];then + mkdir api + git clone https://github.com/Binaryify/NeteaseCloudMusicApi ./api && \ + cd api && npm install && cd .. +fi + +if [[ ! -d music ]];then + mkdir music + git clone https://github.com/qier222/YesPlayMusic ./music && \ + cd music && cp .env.example .env && npm install --force && npm run build && cd .. +fi + +# 启动 +PID=`ps -ef | grep npm | awk '{print $2}' | sed '$d'` + +if [[ ! -z ${PID} ]];then echo $PID | xargs kill;fi +nohup bash -c 'cd api && PORT=35216 node app.js' > api.log 2>&1 +nohup bash -c 'npx serve music/dist/' > music.log 2>&1 diff --git a/jsconfig.json b/jsconfig.json index 0d612a8..7787662 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -7,7 +7,8 @@ }, "target": "ES6", "module": "commonjs", - "allowSyntheticDefaultImports": true + "allowSyntheticDefaultImports": true, + "jsx": "preserve" }, "include": ["src/**/*"], "exclude": ["node_modules"] diff --git a/package.json b/package.json index d56130a..bb87fdd 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { - "name": "YesPlayMusic", - "version": "0.3.3", + "name": "yesplaymusic", + "version": "0.4.9", "private": true, - "description": "A third party music application for Netease Music", - "author": "hawtim", + "description": "A third party music player for Netease Music", + "author": "qier222", "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", @@ -18,57 +18,68 @@ "electron:publish": "vue-cli-service electron:build -mwl -p always", "postinstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps", - "prettier": "npx prettier --write ./src ./script", - "netease_api:run": "cd ./netease_api && npm run start", - "netease_api:pull": "node script/pull.js", - "netease_api:install": "cd ./netease_api && npm install", - "netease_api:setup": "npm run netease_api:pull && npm run netease_api:install" + "prettier": "npx prettier --write ./src", + "netease_api:run": "npx NeteaseCloudMusicApi" }, "main": "background.js", + "engines": { + "node": "14 || 16" + }, "dependencies": { - "@njzy/unblockneteasemusic": "^0.25.3", - "axios": "^0.21.0", - "big-integer": "^1.6.48", + "@unblockneteasemusic/rust-napi": "^0.4.0", + "NeteaseCloudMusicApi": "^4.23.3", + "axios": "^0.26.1", "change-case": "^4.1.2", + "cli-color": "^2.0.0", + "color": "^4.2.3", "core-js": "^3.6.5", "crypto-js": "^4.0.0", "dayjs": "^1.8.36", - "electron": "11.0.2", - "electron-context-menu": "^2.3.0", + "dexie": "^3.0.3", + "discord-rich-presence": "^0.0.8", + "electron": "^13.6.7", + "electron-builder": "^23.0.0", + "electron-context-menu": "^3.1.2", "electron-debug": "^3.1.0", - "electron-devtools-installer": "^3.1.1", - "electron-icon-builder": "^1.0.2", - "electron-is-dev": "^1.2.0", + "electron-devtools-installer": "^3.2", + "electron-icon-builder": "^2.0.1", + "electron-is-dev": "^2.0.0", "electron-log": "^4.3.0", - "electron-store": "^6.0.1", - "electron-updater": "^4.3.5", + "electron-store": "^8.0.1", + "electron-updater": "^5.0.1", + "esbuild": "^0.20.1", + "esbuild-loader": "^4.0.3", "express": "^4.17.1", "express-fileupload": "^1.2.0", "express-http-proxy": "^1.6.2", "extract-zip": "^2.0.1", - "howler": "^2.2.0", + "howler": "^2.2.3", "js-cookie": "^2.2.1", - "localforage": "^1.9.0", + "jsbi": "^4.1.0", "lodash": "^4.17.20", + "md5": "^2.3.0", + "mpris-service": "^2.1.2", + "music-metadata": "^7.5.3", + "node-vibrant": "^3.2.1-alpha.1", "nprogress": "^0.2.0", "pac-proxy-agent": "^4.1.0", "plyr": "^3.6.2", - "prettier": "2.1.2", + "qrcode": "^1.4.4", "register-service-worker": "^1.7.1", - "svg-sprite-loader": "^5.0.0", + "svg-sprite-loader": "^6.0.11", "tunnel": "^0.0.6", + "vscode-codicons": "^0.0.17", "vue": "^2.6.11", - "vue-analytics": "^5.22.1", - "vue-electron": "^1.0.6", + "vue-clipboard2": "^0.3.1", + "vue-gtag": "1", "vue-i18n": "^8.22.0", "vue-router": "^3.4.3", "vue-slider-component": "^3.2.5", - "vuex": "^3.4.0" + "vuex": "^3.4.0", + "x11": "^2.3.0" }, "devDependencies": { - "@sentry/browser": "^5.27.0", - "@sentry/integrations": "^5.27.0", - "@sentry/tracing": "^5.27.0", + "@types/node": "^17.0.0", "@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0", "@vue/cli-plugin-pwa": "~4.5.0", @@ -76,13 +87,21 @@ "@vue/cli-service": "~4.5.0", "babel-eslint": "^10.1.0", "eslint": "^6.7.2", - "eslint-plugin-vue": "^6.2.2", + "eslint-config-prettier": "^8.1.0", + "eslint-plugin-prettier": "^3.3.1", + "eslint-plugin-vue": "^7.9.0", "husky": "^4.3.0", + "prettier": "2.5.1", "sass": "^1.26.11", "sass-loader": "^10.0.2", - "vue-cli-plugin-electron-builder": "~2.0.0-rc.4", + "vue-cli-plugin-electron-builder": "~2.1.1", "vue-template-compiler": "^2.6.11" }, + "resolutions": { + "icon-gen": "3.0.0", + "degenerator": "2.2.0", + "electron-builder": "^23.0.0" + }, "eslintConfig": { "root": true, "env": { @@ -91,6 +110,8 @@ }, "extends": [ "plugin:vue/essential", + "plugin:vue/recommended", + "plugin:prettier/recommended", "eslint:recommended" ], "parserOptions": { diff --git a/public/img/icons/exit.png b/public/img/icons/exit.png new file mode 100644 index 0000000..01e21b2 Binary files /dev/null and b/public/img/icons/exit.png differ diff --git a/public/img/icons/left.png b/public/img/icons/left.png new file mode 100644 index 0000000..9e49d7e Binary files /dev/null and b/public/img/icons/left.png differ diff --git a/public/img/icons/like.png b/public/img/icons/like.png new file mode 100644 index 0000000..4bea102 Binary files /dev/null and b/public/img/icons/like.png differ diff --git a/public/img/icons/menu-dark@88.png b/public/img/icons/menu-dark@88.png new file mode 100644 index 0000000..a2feb00 Binary files /dev/null and b/public/img/icons/menu-dark@88.png differ diff --git a/public/img/icons/menu@88.png b/public/img/icons/menu-light@88.png similarity index 100% rename from public/img/icons/menu@88.png rename to public/img/icons/menu-light@88.png diff --git a/public/img/icons/pause.png b/public/img/icons/pause.png new file mode 100644 index 0000000..509d738 Binary files /dev/null and b/public/img/icons/pause.png differ diff --git a/public/img/icons/play.png b/public/img/icons/play.png new file mode 100644 index 0000000..90537c8 Binary files /dev/null and b/public/img/icons/play.png differ diff --git a/public/img/icons/repeat.png b/public/img/icons/repeat.png new file mode 100644 index 0000000..d4c3fc7 Binary files /dev/null and b/public/img/icons/repeat.png differ diff --git a/public/img/icons/right.png b/public/img/icons/right.png new file mode 100644 index 0000000..50c2e75 Binary files /dev/null and b/public/img/icons/right.png differ diff --git a/public/img/icons/unlike.png b/public/img/icons/unlike.png new file mode 100644 index 0000000..a0afa24 Binary files /dev/null and b/public/img/icons/unlike.png differ diff --git a/public/img/logos/lastfm.png b/public/img/logos/lastfm.png new file mode 100644 index 0000000..e3964f6 Binary files /dev/null and b/public/img/logos/lastfm.png differ diff --git a/public/img/logos/nyancat-stop.png b/public/img/logos/nyancat-stop.png new file mode 100644 index 0000000..72bf07e Binary files /dev/null and b/public/img/logos/nyancat-stop.png differ diff --git a/public/img/logos/yesplaymusic-white24x24.png b/public/img/logos/yesplaymusic-white24x24.png new file mode 100644 index 0000000..d8e4715 Binary files /dev/null and b/public/img/logos/yesplaymusic-white24x24.png differ diff --git a/public/img/touchbar/backward.png b/public/img/touchbar/backward.png new file mode 100644 index 0000000..83b2053 Binary files /dev/null and b/public/img/touchbar/backward.png differ diff --git a/public/img/touchbar/forward.png b/public/img/touchbar/forward.png new file mode 100644 index 0000000..5577bb5 Binary files /dev/null and b/public/img/touchbar/forward.png differ diff --git a/public/img/touchbar/like.png b/public/img/touchbar/like.png new file mode 100644 index 0000000..09f6534 Binary files /dev/null and b/public/img/touchbar/like.png differ diff --git a/public/img/touchbar/like_fill.png b/public/img/touchbar/like_fill.png new file mode 100644 index 0000000..bbc2727 Binary files /dev/null and b/public/img/touchbar/like_fill.png differ diff --git a/public/img/touchbar/next_up.png b/public/img/touchbar/next_up.png new file mode 100644 index 0000000..60d67a0 Binary files /dev/null and b/public/img/touchbar/next_up.png differ diff --git a/public/img/touchbar/page_next.png b/public/img/touchbar/page_next.png new file mode 100644 index 0000000..24ab389 Binary files /dev/null and b/public/img/touchbar/page_next.png differ diff --git a/public/img/touchbar/page_prev.png b/public/img/touchbar/page_prev.png new file mode 100644 index 0000000..52cb588 Binary files /dev/null and b/public/img/touchbar/page_prev.png differ diff --git a/public/img/touchbar/pause.png b/public/img/touchbar/pause.png new file mode 100644 index 0000000..083f680 Binary files /dev/null and b/public/img/touchbar/pause.png differ diff --git a/public/img/touchbar/play.png b/public/img/touchbar/play.png new file mode 100644 index 0000000..101b92f Binary files /dev/null and b/public/img/touchbar/play.png differ diff --git a/public/img/touchbar/search.png b/public/img/touchbar/search.png new file mode 100644 index 0000000..e62e725 Binary files /dev/null and b/public/img/touchbar/search.png differ diff --git a/public/index.html b/public/index.html index 193d42c..ca6d1ad 100644 --- a/public/index.html +++ b/public/index.html @@ -1,23 +1,25 @@ - - - - - - - <%= htmlWebpackPlugin.options.title %> - - - +
+ + + diff --git a/public/robots.txt b/public/robots.txt index eb05362..1f53798 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,2 +1,2 @@ User-agent: * -Disallow: +Disallow: / diff --git a/restyled.yml b/restyled.yml new file mode 100644 index 0000000..c31d3e8 --- /dev/null +++ b/restyled.yml @@ -0,0 +1,7 @@ +commit_template: 'style: with ${restyler.name}' +restylers: + - prettier + - prettier-json + - prettier-markdown + - prettier-yaml + - whitespace diff --git a/script/pull.js b/script/pull.js deleted file mode 100644 index 8f75eb8..0000000 --- a/script/pull.js +++ /dev/null @@ -1,103 +0,0 @@ -// node module -const fs = require("fs"); -const https = require("https"); -const resolve = require("path").resolve; -const join = require("path").resolve; -const extract = require("extract-zip"); - -// 函数参数 -const dest = resolve(__dirname, "../"); -const fileName = "NeteaseCloudMusicApi-master.zip"; -const options = { - hostname: "github.91chifun.workers.dev", - path: `//https://github.com/Binaryify/NeteaseCloudMusicApi/archive/master.zip`, -}; - -// 完整的流程控制 -/** - * 1. 检查本地文件是否已有 - * 2. 下载默认/指定版本的 zip 压缩包,等待下载 - * 3. 解压缩 - * 4. 进入目录安装依赖 npm install - */ - -function fix2(number) { - return number.toFixed(2); -} - -async function download(options, fileName, callback) { - return await new Promise((resolve, reject) => { - const destPath = join(__dirname, "../" + fileName); - // Check if exist - if (fs.existsSync(destPath)) return resolve(destPath); - - const file = fs.createWriteStream(destPath); - const request = https.get(options, (res) => { - let len = res.headers && parseInt(res.headers["content-length"], 10); - let cur = 0; - // 1048576 - bytes in 1Megabyte - const MEGA = 1048576; - let total = 0; - if (len) { - total = len / MEGA; - } - if (!len) { - console.log( - "Downloading, but can not get content-length, please be patient." - ); - } - res.on("data", (chunk) => { - if (len) { - cur += chunk.length; - console.log( - `Downloading ${fix2((100.0 * cur) / len)}% ${fix2( - cur / MEGA - )}/${fix2(total)}mb` - ); - } - }); - res.on("end", () => { - callback("Downloading complete!"); - }); - res.pipe(file); - file.on("finish", () => { - file.close(() => { - callback("File wrote complete!"); - resolve(destPath); - }); - }); - file.on("error", (err) => { - fs.unlink(destPath); - reject(err); - }); - request.on("error", (err) => { - console.log("Error: " + err.message); - }); - }); - }); -} - -async function unzip(source, target) { - try { - await extract(source, { - dir: target, - }); - console.log("Extraction complete"); - return true; - } catch (err) { - // handle any errors - if (err.message === "end of central directory record signature not found") { - console.log("Not a full_downloaded zip file, removed!"); - fs.unlinkSync(source); - } - return false; - } -} -// Download process -download(options, fileName, (text) => { - console.log(text); -}).then((path) => { - console.log(path); - // Unzip process - return unzip(path, dest); -}); diff --git a/src/App.vue b/src/App.vue index ff33a46..59392be 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,39 +1,43 @@ diff --git a/src/api/album.js b/src/api/album.js index ef39d14..18425d4 100644 --- a/src/api/album.js +++ b/src/api/album.js @@ -1,5 +1,6 @@ -import request from "@/utils/request"; -import { mapTrackPlayableStatus } from "@/utils/common"; +import request from '@/utils/request'; +import { mapTrackPlayableStatus } from '@/utils/common'; +import { cacheAlbum, getAlbumFromCache } from '@/utils/db'; /** * 获取专辑内容 @@ -7,15 +8,23 @@ import { mapTrackPlayableStatus } from "@/utils/common"; * @param {number} id */ export function getAlbum(id) { - return request({ - url: "/album", - method: "get", - params: { - id, - }, - }).then((data) => { - data.songs = mapTrackPlayableStatus(data.songs); - return data; + const fetchLatest = () => { + return request({ + url: '/album', + method: 'get', + params: { + id, + }, + }).then(data => { + cacheAlbum(id, data); + data.songs = mapTrackPlayableStatus(data.songs); + return data; + }); + }; + fetchLatest(); + + return getAlbumFromCache(id).then(result => { + return result ?? fetchLatest(); }); } @@ -32,8 +41,8 @@ export function getAlbum(id) { */ export function newAlbums(params) { return request({ - url: "/album/new", - method: "get", + url: '/album/new', + method: 'get', params, }); } @@ -46,8 +55,8 @@ export function newAlbums(params) { */ export function albumDynamicDetail(id) { return request({ - url: "/album/detail/dynamic", - method: "get", + url: '/album/detail/dynamic', + method: 'get', params: { id, timestamp: new Date().getTime() }, }); } @@ -63,8 +72,8 @@ export function albumDynamicDetail(id) { */ export function likeAAlbum(params) { return request({ - url: "/album/sub", - method: "post", + url: '/album/sub', + method: 'post', params, }); } diff --git a/src/api/artist.js b/src/api/artist.js index 31b0742..925df45 100644 --- a/src/api/artist.js +++ b/src/api/artist.js @@ -1,5 +1,7 @@ -import request from "@/utils/request"; -import { mapTrackPlayableStatus } from "@/utils/common"; +import request from '@/utils/request'; +import { mapTrackPlayableStatus } from '@/utils/common'; +import { isAccountLoggedIn } from '@/utils/auth'; +import { getTrackDetail } from '@/api/track'; /** * 获取歌手单曲 @@ -8,13 +10,19 @@ import { mapTrackPlayableStatus } from "@/utils/common"; */ export function getArtist(id) { return request({ - url: "/artists", - method: "get", + url: '/artists', + method: 'get', params: { id, timestamp: new Date().getTime(), }, - }).then((data) => { + }).then(async data => { + if (!isAccountLoggedIn()) { + const trackIDs = data.hotSongs.map(t => t.id); + const tracks = await getTrackDetail(trackIDs.join(',')); + data.hotSongs = tracks.songs; + return data; + } data.hotSongs = mapTrackPlayableStatus(data.hotSongs); return data; }); @@ -33,8 +41,8 @@ export function getArtist(id) { */ export function getArtistAlbum(params) { return request({ - url: "/artist/album", - method: "get", + url: '/artist/album', + method: 'get', params, }); } @@ -50,12 +58,14 @@ export function getArtistAlbum(params) { * @param {number=} type */ export function toplistOfArtists(type = null) { + let params = {}; + if (type) { + params.type = type; + } return request({ - url: "/toplist/artist", - method: "get", - params: { - type, - }, + url: '/toplist/artist', + method: 'get', + params, }); } /** @@ -67,8 +77,8 @@ export function toplistOfArtists(type = null) { */ export function artistMv(params) { return request({ - url: "/artist/mv", - method: "get", + url: '/artist/mv', + method: 'get', params, }); } @@ -84,8 +94,8 @@ export function artistMv(params) { */ export function followAArtist(params) { return request({ - url: "/artist/sub", - method: "post", + url: '/artist/sub', + method: 'post', params, }); } @@ -98,8 +108,8 @@ export function followAArtist(params) { */ export function similarArtists(id) { return request({ - url: "/simi/artist", - method: "post", + url: '/simi/artist', + method: 'post', params: { id }, }); } diff --git a/src/api/auth.js b/src/api/auth.js index 461ac01..294f551 100644 --- a/src/api/auth.js +++ b/src/api/auth.js @@ -1,4 +1,4 @@ -import request from "@/utils/request"; +import request from '@/utils/request'; /** * 手机登录 @@ -14,11 +14,12 @@ import request from "@/utils/request"; */ export function loginWithPhone(params) { return request({ - url: "/login/cellphone", - method: "post", + url: '/login/cellphone', + method: 'post', params, }); } + /** * 邮箱登录 * - email: 163 网易邮箱 @@ -31,12 +32,60 @@ export function loginWithPhone(params) { */ export function loginWithEmail(params) { return request({ - url: "/login", - method: "post", + url: '/login', + method: 'post', params, }); } +/** + * 二维码key生成接口 + */ +export function loginQrCodeKey() { + return request({ + url: '/login/qr/key', + method: 'get', + params: { + timestamp: new Date().getTime(), + }, + }); +} + +/** + * 二维码生成接口 + * 说明: 调用此接口传入上一个接口生成的key可生成二维码图片的base64和二维码信息, + * 可使用base64展示图片,或者使用二维码信息内容自行使用第三方二维码生产库渲染二维码 + * @param {Object} params + * @param {string} params.key + * @param {string=} params.qrimg 传入后会额外返回二维码图片base64编码 + */ +export function loginQrCodeCreate(params) { + return request({ + url: '/login/qr/create', + method: 'get', + params: { + ...params, + timestamp: new Date().getTime(), + }, + }); +} + +/** + * 二维码检测扫码状态接口 + * 说明: 轮询此接口可获取二维码扫码状态,800为二维码过期,801为等待扫码,802为待确认,803为授权登录成功(803状态码下会返回cookies) + * @param {string} key + */ +export function loginQrCodeCheck(key) { + return request({ + url: '/login/qr/check', + method: 'get', + params: { + key, + timestamp: new Date().getTime(), + }, + }); +} + /** * 刷新登录 * 说明 : 调用此接口 , 可刷新登录状态 @@ -44,8 +93,8 @@ export function loginWithEmail(params) { */ export function refreshCookie() { return request({ - url: "/login/refresh", - method: "post", + url: '/login/refresh', + method: 'post', }); } @@ -55,7 +104,7 @@ export function refreshCookie() { */ export function logout() { return request({ - url: "/logout", - method: "post", + url: '/logout', + method: 'post', }); } diff --git a/src/api/lastfm.js b/src/api/lastfm.js new file mode 100644 index 0000000..41ad9c5 --- /dev/null +++ b/src/api/lastfm.js @@ -0,0 +1,80 @@ +// Last.fm API documents 👉 https://www.last.fm/api + +import axios from 'axios'; +import md5 from 'crypto-js/md5'; + +const apiKey = process.env.VUE_APP_LASTFM_API_KEY; +const apiSharedSecret = process.env.VUE_APP_LASTFM_API_SHARED_SECRET; +const baseUrl = window.location.origin; +const url = 'https://ws.audioscrobbler.com/2.0/'; + +const sign = params => { + const sortParamsKeys = Object.keys(params).sort(); + const sortedParams = sortParamsKeys.reduce((acc, key) => { + acc[key] = params[key]; + return acc; + }, {}); + let signature = ''; + for (const [key, value] of Object.entries(sortedParams)) { + signature += `${key}${value}`; + } + return md5(signature + apiSharedSecret).toString(); +}; + +export function auth() { + const url = process.env.IS_ELECTRON + ? `https://www.last.fm/api/auth/?api_key=${apiKey}&cb=${baseUrl}/#/lastfm/callback` + : `https://www.last.fm/api/auth/?api_key=${apiKey}&cb=${baseUrl}/lastfm/callback`; + window.open(url); +} + +export function authGetSession(token) { + const signature = md5( + `api_key${apiKey}methodauth.getSessiontoken${token}${apiSharedSecret}` + ).toString(); + return axios({ + url, + method: 'GET', + params: { + method: 'auth.getSession', + format: 'json', + api_key: apiKey, + api_sig: signature, + token, + }, + }); +} + +export function trackUpdateNowPlaying(params) { + params.api_key = apiKey; + params.method = 'track.updateNowPlaying'; + params.sk = JSON.parse(localStorage.getItem('lastfm'))['key']; + const signature = sign(params); + + return axios({ + url, + method: 'POST', + params: { + ...params, + api_sig: signature, + format: 'json', + }, + }); +} + +export function trackScrobble(params) { + params.api_key = apiKey; + params.method = 'track.scrobble'; + params.sk = JSON.parse(localStorage.getItem('lastfm'))['key']; + const signature = sign(params); + + return axios({ + url, + method: 'POST', + params: { + ...params, + api_sig: signature, + format: 'json', + }, + }); +} diff --git a/src/api/mv.js b/src/api/mv.js index f294785..ecd2b10 100644 --- a/src/api/mv.js +++ b/src/api/mv.js @@ -1,4 +1,4 @@ -import request from "@/utils/request"; +import request from '@/utils/request'; /** * 获取 mv 数据 @@ -9,8 +9,8 @@ import request from "@/utils/request"; */ export function mvDetail(mvid) { return request({ - url: "/mv/detail", - method: "get", + url: '/mv/detail', + method: 'get', params: { mvid, timestamp: new Date().getTime(), @@ -30,8 +30,8 @@ export function mvDetail(mvid) { */ export function mvUrl(params) { return request({ - url: "/mv/url", - method: "get", + url: '/mv/url', + method: 'get', params, }); } @@ -43,8 +43,8 @@ export function mvUrl(params) { */ export function simiMv(mvid) { return request({ - url: "/simi/mv", - method: "get", + url: '/simi/mv', + method: 'get', params: { mvid }, }); } @@ -62,8 +62,8 @@ export function simiMv(mvid) { export function likeAMV(params) { params.timestamp = new Date().getTime(); return request({ - url: "/mv/sub", - method: "post", + url: '/mv/sub', + method: 'post', params, }); } diff --git a/src/api/others.js b/src/api/others.js index d1027c1..5c3cd63 100644 --- a/src/api/others.js +++ b/src/api/others.js @@ -1,5 +1,5 @@ -import request from "@/utils/request"; -import { mapTrackPlayableStatus } from "@/utils/common"; +import request from '@/utils/request'; +import { mapTrackPlayableStatus } from '@/utils/common'; /** * 搜索 @@ -18,12 +18,33 @@ import { mapTrackPlayableStatus } from "@/utils/common"; */ export function search(params) { return request({ - url: "/search", - method: "get", + url: '/search', + method: 'get', params, - }).then((data) => { + }).then(data => { if (data.result?.song !== undefined) data.result.song.songs = mapTrackPlayableStatus(data.result.song.songs); return data; }); } + +export function personalFM() { + return request({ + url: '/personal_fm', + method: 'get', + params: { + timestamp: new Date().getTime(), + }, + }); +} + +export function fmTrash(id) { + return request({ + url: '/fm_trash', + method: 'post', + params: { + timestamp: new Date().getTime(), + id, + }, + }); +} diff --git a/src/api/playlist.js b/src/api/playlist.js index b0e5994..08177ea 100644 --- a/src/api/playlist.js +++ b/src/api/playlist.js @@ -1,5 +1,5 @@ -import request from "@/utils/request"; -import { mapTrackPlayableStatus } from "@/utils/common"; +import request from '@/utils/request'; +import { mapTrackPlayableStatus } from '@/utils/common'; /** * 推荐歌单 @@ -11,8 +11,8 @@ import { mapTrackPlayableStatus } from "@/utils/common"; */ export function recommendPlaylist(params) { return request({ - url: "/personalized", - method: "get", + url: '/personalized', + method: 'get', params, }); } @@ -24,9 +24,12 @@ export function recommendPlaylist(params) { */ export function dailyRecommendPlaylist(params) { return request({ - url: "/recommend/resource", - method: "get", - params, + url: '/recommend/resource', + method: 'get', + params: { + params, + timestamp: Date.now(), + }, }); } /** @@ -43,14 +46,16 @@ export function getPlaylistDetail(id, noCache = false) { let params = { id }; if (noCache) params.timestamp = new Date().getTime(); return request({ - url: "/playlist/detail", - method: "get", + url: '/playlist/detail', + method: 'get', params, - }).then((data) => { - data.playlist.tracks = mapTrackPlayableStatus( - data.playlist.tracks, - data.privileges || [] - ); + }).then(data => { + if (data.playlist) { + data.playlist.tracks = mapTrackPlayableStatus( + data.playlist.tracks, + data.privileges || [] + ); + } return data; }); } @@ -67,8 +72,8 @@ export function getPlaylistDetail(id, noCache = false) { */ export function highQualityPlaylist(params) { return request({ - url: "/top/playlist/highquality", - method: "get", + url: '/top/playlist/highquality', + method: 'get', params, }); } @@ -86,8 +91,8 @@ export function highQualityPlaylist(params) { */ export function topPlaylist(params) { return request({ - url: "/top/playlist", - method: "get", + url: '/top/playlist', + method: 'get', params, }); } @@ -98,8 +103,8 @@ export function topPlaylist(params) { */ export function playlistCatlist() { return request({ - url: "/playlist/catlist", - method: "get", + url: '/playlist/catlist', + method: 'get', }); } @@ -109,8 +114,8 @@ export function playlistCatlist() { */ export function toplists() { return request({ - url: "/toplist", - method: "get", + url: '/toplist', + method: 'get', }); } @@ -126,8 +131,8 @@ export function toplists() { export function subscribePlaylist(params) { params.timestamp = new Date().getTime(); return request({ - url: "/playlist/subscribe", - method: "post", + url: '/playlist/subscribe', + method: 'post', params, }); } @@ -140,8 +145,8 @@ export function subscribePlaylist(params) { */ export function deletePlaylist(id) { return request({ - url: "/playlist/delete", - method: "post", + url: '/playlist/delete', + method: 'post', params: { id }, }); } @@ -160,8 +165,8 @@ export function deletePlaylist(id) { export function createPlaylist(params) { params.timestamp = new Date().getTime(); return request({ - url: "/playlist/create", - method: "post", + url: '/playlist/create', + method: 'post', params, }); } @@ -178,8 +183,47 @@ export function createPlaylist(params) { export function addOrRemoveTrackFromPlaylist(params) { params.timestamp = new Date().getTime(); return request({ - url: "/playlist/tracks", - method: "post", + url: '/playlist/tracks', + method: 'post', + params, + }); +} + +/** + * 每日推荐歌曲 + * 说明 : 调用此接口 , 可获得每日推荐歌曲 ( 需要登录 ) + * @param {Object} params + * @param {string} params.op + * @param {string} params.pid + */ +export function dailyRecommendTracks() { + return request({ + url: '/recommend/songs', + method: 'get', + params: { timestamp: new Date().getTime() }, + }).then(result => { + result.data.dailySongs = mapTrackPlayableStatus( + result.data.dailySongs, + result.data.privileges + ); + return result; + }); +} + +/** + * 心动模式/智能播放 + * 说明 : 登录后调用此接口 , 可获取心动模式/智能播放列表 必选参数 : id : 歌曲 id + * - id : 歌曲 id + * - pid : 歌单 id + * - sid : 要开始播放的歌曲的 id (可选参数) + * @param {Object} params + * @param {number=} params.id + * @param {number=} params.pid + */ +export function intelligencePlaylist(params) { + return request({ + url: '/playmode/intelligence/list', + method: 'get', params, }); } diff --git a/src/api/track.js b/src/api/track.js index 43a7d2a..490c91c 100644 --- a/src/api/track.js +++ b/src/api/track.js @@ -1,6 +1,13 @@ -import store from "@/store"; -import request from "@/utils/request"; -import { mapTrackPlayableStatus } from "@/utils/common"; +import store from '@/store'; +import request from '@/utils/request'; +import { mapTrackPlayableStatus } from '@/utils/common'; +import { + cacheTrackDetail, + getTrackDetailFromCache, + cacheLyric, + getLyricFromCache, +} from '@/utils/db'; + /** * 获取音乐 url * 说明 : 使用歌单详情接口后 , 能得到的音乐的 id, 但不能得到的音乐 url, 调用此接口, 传入的音乐 id( 可多个 , 用逗号隔开 ), 可以获取对应的音乐的 url, @@ -8,51 +15,85 @@ import { mapTrackPlayableStatus } from "@/utils/common"; * @param {string} id - 音乐的 id,例如 id=405998841,33894312 */ export function getMP3(id) { - let br = - store.state.settings?.musicQuality !== undefined - ? store.state.settings.musicQuality - : 320000; + const getBr = () => { + // 当返回的 quality >= 400000时,就会优先返回 hi-res + const quality = store.state.settings?.musicQuality ?? '320000'; + return quality === 'flac' ? '350000' : quality; + }; + return request({ - url: "/song/url", - method: "get", + url: '/song/url', + method: 'get', params: { id, - br, + br: getBr(), }, }); } + /** * 获取歌曲详情 * 说明 : 调用此接口 , 传入音乐 id(支持多个 id, 用 , 隔开), 可获得歌曲详情(注意:歌曲封面现在需要通过专辑内容接口获取) * @param {string} ids - 音乐 id, 例如 ids=405998841,33894312 */ export function getTrackDetail(ids) { - return request({ - url: "/song/detail", - method: "get", - params: { - ids, - }, - }).then((data) => { - data.songs = mapTrackPlayableStatus(data.songs, data.privileges); - return data; + const fetchLatest = () => { + return request({ + url: '/song/detail', + method: 'get', + params: { + ids, + }, + }).then(data => { + data.songs.map(song => { + const privileges = data.privileges.find(t => t.id === song.id); + cacheTrackDetail(song, privileges); + }); + data.songs = mapTrackPlayableStatus(data.songs, data.privileges); + return data; + }); + }; + fetchLatest(); + + let idsInArray = [String(ids)]; + if (typeof ids === 'string') { + idsInArray = ids.split(','); + } + + return getTrackDetailFromCache(idsInArray).then(result => { + if (result) { + result.songs = mapTrackPlayableStatus(result.songs, result.privileges); + } + return result ?? fetchLatest(); }); } + /** * 获取歌词 * 说明 : 调用此接口 , 传入音乐 id 可获得对应音乐的歌词 ( 不需要登录 ) * @param {number} id - 音乐 id */ - export function getLyric(id) { - return request({ - url: "/lyric", - method: "get", - params: { - id, - }, + const fetchLatest = () => { + return request({ + url: '/lyric', + method: 'get', + params: { + id, + }, + }).then(result => { + cacheLyric(id, result); + return result; + }); + }; + + fetchLatest(); + + return getLyricFromCache(id).then(result => { + return result ?? fetchLatest(); }); } + /** * 新歌速递 * 说明 : 调用此接口 , 可获取新歌速递 @@ -60,13 +101,14 @@ export function getLyric(id) { */ export function topSong(type) { return request({ - url: "/top/song", - method: "get", + url: '/top/song', + method: 'get', params: { type, }, }); } + /** * 喜欢音乐 * 说明 : 调用此接口 , 传入音乐 id, 可喜欢该音乐 @@ -79,8 +121,8 @@ export function topSong(type) { export function likeATrack(params) { params.timestamp = new Date().getTime(); return request({ - url: "/like", - method: "get", + url: '/like', + method: 'get', params, }); } @@ -99,8 +141,8 @@ export function likeATrack(params) { export function scrobble(params) { params.timestamp = new Date().getTime(); return request({ - url: "/scrobble", - method: "get", + url: '/scrobble', + method: 'get', params, }); } diff --git a/src/api/user.js b/src/api/user.js index aeeb719..ec02014 100644 --- a/src/api/user.js +++ b/src/api/user.js @@ -1,4 +1,4 @@ -import request from "@/utils/request"; +import request from '@/utils/request'; /** * 获取用户详情 @@ -8,10 +8,25 @@ import request from "@/utils/request"; */ export function userDetail(uid) { return request({ - url: "/user/detail", - method: "get", + url: '/user/detail', + method: 'get', params: { uid, + timestamp: new Date().getTime(), + }, + }); +} + +/** + * 获取账号详情 + * 说明 : 登录后调用此接口 ,可获取用户账号信息 + */ +export function userAccount() { + return request({ + url: '/user/account', + method: 'get', + params: { + timestamp: new Date().getTime(), }, }); } @@ -29,8 +44,25 @@ export function userDetail(uid) { */ export function userPlaylist(params) { return request({ - url: "/user/playlist", - method: "get", + url: '/user/playlist', + method: 'get', + params, + }); +} + +/** + * 获取用户播放记录 + * 说明 : 登录后调用此接口 , 传入用户 id, 可获取用户播放记录 + * - uid : 用户 id + * - type : type=1 时只返回 weekData, type=0 时返回 allData + * @param {Object} params + * @param {number} params.uid + * @param {number} params.type + */ +export function userPlayHistory(params) { + return request({ + url: '/user/record', + method: 'get', params, }); } @@ -43,8 +75,8 @@ export function userPlaylist(params) { */ export function userLikedSongsIDs(uid) { return request({ - url: "/likelist", - method: "get", + url: '/likelist', + method: 'get', params: { uid, timestamp: new Date().getTime(), @@ -60,8 +92,8 @@ export function userLikedSongsIDs(uid) { */ export function dailySignin(type = 0) { return request({ - url: "/daily_signin", - method: "post", + url: '/daily_signin', + method: 'post', params: { type, timestamp: new Date().getTime(), @@ -72,17 +104,18 @@ export function dailySignin(type = 0) { /** * 获取收藏的专辑(需要登录) * 说明 : 调用此接口可获取到用户收藏的专辑 - * - limit : 返回数量 , 默认为 30 - * - offset : 偏移数量,用于分页 , 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0 + * - limit : 返回数量 , 默认为 25 + * - offset : 偏移数量,用于分页 , 如 :( 页数 -1)*25, 其中 25 为 limit 的值 , 默认为 0 * @param {Object} params * @param {number} params.limit * @param {number=} params.offset */ -export function likedAlbums() { +export function likedAlbums(params) { return request({ - url: "/album/sublist", - method: "get", + url: '/album/sublist', + method: 'get', params: { + limit: params.limit, timestamp: new Date().getTime(), }, }); @@ -92,11 +125,12 @@ export function likedAlbums() { * 获取收藏的歌手(需要登录) * 说明 : 调用此接口可获取到用户收藏的歌手 */ -export function likedArtists() { +export function likedArtists(params) { return request({ - url: "/artist/sublist", - method: "get", + url: '/artist/sublist', + method: 'get', params: { + limit: params.limit, timestamp: new Date().getTime(), }, }); @@ -106,12 +140,82 @@ export function likedArtists() { * 获取收藏的MV(需要登录) * 说明 : 调用此接口可获取到用户收藏的MV */ -export function likedMVs() { +export function likedMVs(params) { return request({ - url: "/mv/sublist", - method: "get", + url: '/mv/sublist', + method: 'get', params: { + limit: params.limit, timestamp: new Date().getTime(), }, }); } + +/** + * 上传歌曲到云盘(需要登录) + */ +export function uploadSong(file) { + let formData = new FormData(); + formData.append('songFile', file); + return request({ + url: '/cloud', + method: 'post', + params: { + timestamp: new Date().getTime(), + }, + data: formData, + headers: { + 'Content-Type': 'multipart/form-data', + }, + timeout: 200000, + }).catch(error => { + alert(`上传失败,Error: ${error}`); + }); +} + +/** + * 获取云盘歌曲(需要登录) + * 说明 : 登录后调用此接口 , 可获取云盘数据 , 获取的数据没有对应 url, 需要再调用一 次 /song/url 获取 url + * - limit : 返回数量 , 默认为 200 + * - offset : 偏移数量,用于分页 , 如 :( 页数 -1)*200, 其中 200 为 limit 的值 , 默认为 0 + * @param {Object} params + * @param {number} params.limit + * @param {number=} params.offset + */ +export function cloudDisk(params = {}) { + params.timestamp = new Date().getTime(); + return request({ + url: '/user/cloud', + method: 'get', + params, + }); +} + +/** + * 获取云盘歌曲详情(需要登录) + */ +export function cloudDiskTrackDetail(id) { + return request({ + url: '/user/cloud/detail', + method: 'get', + params: { + timestamp: new Date().getTime(), + id, + }, + }); +} + +/** + * 删除云盘歌曲(需要登录) + * @param {Array} id + */ +export function cloudDiskTrackDelete(id) { + return request({ + url: '/user/cloud/del', + method: 'get', + params: { + timestamp: new Date().getTime(), + id, + }, + }); +} diff --git a/src/assets/css/global.scss b/src/assets/css/global.scss new file mode 100644 index 0000000..00c3915 --- /dev/null +++ b/src/assets/css/global.scss @@ -0,0 +1,135 @@ +@font-face { + font-family: 'Barlow'; + font-weight: normal; + src: url('~@/assets/fonts/Barlow-Regular.woff2') format('woff2'), + url('~@/assets/fonts/Barlow-Regular.ttf') format('truetype'); +} +@font-face { + font-family: 'Barlow'; + font-weight: medium; + src: url('~@/assets/fonts/Barlow-Medium.woff2') format('woff2'), + url('~@/assets/fonts/Barlow-Medium.ttf') format('truetype'); +} +@font-face { + font-family: 'Barlow'; + font-weight: 600; + src: url('~@/assets/fonts/Barlow-SemiBold.woff2') format('woff2'), + url('~@/assets/fonts/Barlow-SemiBold.ttf') format('truetype'); +} +@font-face { + font-family: 'Barlow'; + font-weight: bold; + src: url('~@/assets/fonts/Barlow-Bold.woff2') format('woff2'), + url('~@/assets/fonts/Barlow-Bold.ttf') format('truetype'); +} +@font-face { + font-family: 'Barlow'; + font-weight: 800; + src: url('~@/assets/fonts/Barlow-ExtraBold.woff2') format('woff2'), + url('~@/assets/fonts/Barlow-ExtraBold.ttf') format('truetype'); +} +@font-face { + font-family: 'Barlow'; + font-weight: 900; + src: url('~@/assets/fonts/Barlow-Black.woff2') format('woff2'), + url('~@/assets/fonts/Barlow-Black.ttf') format('truetype'); +} + +:root { + --color-body-bg: #ffffff; + --color-text: #000; + --color-primary: #335eea; + --color-primary-bg: #eaeffd; + --color-secondary: #7a7a7b; + --color-secondary-bg: #f5f5f7; + --color-navbar-bg: rgba(255, 255, 255, 0.86); + --color-primary-bg-for-transparent: rgba(189, 207, 255, 0.28); + --color-secondary-bg-for-transparent: rgba(209, 209, 214, 0.28); + --html-overflow-y: overlay; +} + +[data-theme='dark'] { + --color-body-bg: #222222; + --color-text: #ffffff; + --color-primary: #335eea; + --color-primary-bg: #bbcdff; + --color-secondary: #7a7a7b; + --color-secondary-bg: #323232; + --color-navbar-bg: rgba(34, 34, 34, 0.86); + --color-primary-bg-for-transparent: rgba(255, 255, 255, 0.12); + --color-secondary-bg-for-transparent: rgba(255, 255, 255, 0.08); +} + +#app, +input { + font-family: 'Barlow', ui-sans-serif, system-ui, -apple-system, + BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei, + Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei, sans-serif, + microsoft uighur; +} +body { + background-color: var(--color-body-bg); +} + +html { + overflow-y: var(--html-overflow-y); + min-width: 768px; + overscroll-behavior: none; +} + +select, +button { + font-family: inherit; +} +button { + background: none; + border: none; + cursor: pointer; + user-select: none; +} +input, +button { + &:focus { + outline: none; + } +} +a { + color: inherit; + text-decoration: none; + cursor: pointer; + &:hover { + text-decoration: underline; + } +} + +[data-electron='yes'] { + button, + .navigation-links a, + .playlist-info .description { + cursor: default !important; + } +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; + border-left: 1px solid rgba(128, 128, 128, 0.18); + background: var(--color-body-bg); +} + +::-webkit-scrollbar-thumb { + -webkit-border-radius: 10px; + border-radius: 10px; + background: rgba(128, 128, 128, 0.38); +} + +[data-theme='dark'] ::-webkit-scrollbar-thumb { + background: var(--color-secondary-bg); +} + +.user-select-none { + user-select: none; +} diff --git a/src/assets/css/plyr.css b/src/assets/css/plyr.css index 584ba89..1a128f0 100644 --- a/src/assets/css/plyr.css +++ b/src/assets/css/plyr.css @@ -265,17 +265,17 @@ a.plyr__control::before { display: none; } -.plyr [data-plyr="airplay"], -.plyr [data-plyr="captions"], -.plyr [data-plyr="fullscreen"], -.plyr [data-plyr="pip"] { +.plyr [data-plyr='airplay'], +.plyr [data-plyr='captions'], +.plyr [data-plyr='fullscreen'], +.plyr [data-plyr='pip'] { display: none; } -.plyr--airplay-supported [data-plyr="airplay"], -.plyr--captions-enabled [data-plyr="captions"], -.plyr--fullscreen-enabled [data-plyr="fullscreen"], -.plyr--pip-supported [data-plyr="pip"] { +.plyr--airplay-supported [data-plyr='airplay'], +.plyr--captions-enabled [data-plyr='captions'], +.plyr--fullscreen-enabled [data-plyr='fullscreen'], +.plyr--pip-supported [data-plyr='pip'] { display: inline-block; } @@ -288,11 +288,11 @@ a.plyr__control::before { transition: transform 0.3s ease; } -.plyr__menu .plyr__control[aria-expanded="true"] svg { +.plyr__menu .plyr__control[aria-expanded='true'] svg { transform: rotate(90deg); } -.plyr__menu .plyr__control[aria-expanded="true"] .plyr__tooltip { +.plyr__menu .plyr__control[aria-expanded='true'] .plyr__tooltip { display: none; } @@ -327,7 +327,7 @@ a.plyr__control::before { border: var(--plyr-menu-arrow-size, 4px) solid transparent; border-top-color: rgba(255, 255, 255, 0.9); border-top-color: var(--plyr-menu-background, rgba(255, 255, 255, 0.9)); - content: ""; + content: ''; height: 0; position: absolute; right: calc(((18px / 2) + calc(10px * 0.7)) - (4px / 2)); @@ -341,18 +341,18 @@ a.plyr__control::before { width: 0; } -.plyr__menu__container [role="menu"] { +.plyr__menu__container [role='menu'] { padding: calc(10px * 0.7); padding: calc(var(--plyr-control-spacing, 10px) * 0.7); } -.plyr__menu__container [role="menuitem"], -.plyr__menu__container [role="menuitemradio"] { +.plyr__menu__container [role='menuitem'], +.plyr__menu__container [role='menuitemradio'] { margin-top: 2px; } -.plyr__menu__container [role="menuitem"]:first-child, -.plyr__menu__container [role="menuitemradio"]:first-child { +.plyr__menu__container [role='menuitem']:first-child, +.plyr__menu__container [role='menuitemradio']:first-child { margin-top: 0; } @@ -386,7 +386,7 @@ a.plyr__control::before { .plyr__menu__container .plyr__control::after { border: 4px solid transparent; border: var(--plyr-menu-item-arrow-size, 4px) solid transparent; - content: ""; + content: ''; position: absolute; top: 50%; transform: translateY(-50%); @@ -441,7 +441,7 @@ a.plyr__control::before { background: var(--plyr-menu-back-border-color, #dcdfe5); box-shadow: 0 1px 0 #fff; box-shadow: 0 1px 0 var(--plyr-menu-back-border-shadow-color, #fff); - content: ""; + content: ''; height: 1px; left: 0; margin-top: calc(calc(10px * 0.7) / 2); @@ -457,19 +457,19 @@ a.plyr__control::before { border-right-color: currentColor; } -.plyr__menu__container .plyr__control[role="menuitemradio"] { +.plyr__menu__container .plyr__control[role='menuitemradio'] { padding-left: calc(10px * 0.7); padding-left: calc(var(--plyr-control-spacing, 10px) * 0.7); } -.plyr__menu__container .plyr__control[role="menuitemradio"]::after, -.plyr__menu__container .plyr__control[role="menuitemradio"]::before { +.plyr__menu__container .plyr__control[role='menuitemradio']::after, +.plyr__menu__container .plyr__control[role='menuitemradio']::before { border-radius: 100%; } -.plyr__menu__container .plyr__control[role="menuitemradio"]::before { +.plyr__menu__container .plyr__control[role='menuitemradio']::before { background: rgba(0, 0, 0, 0.1); - content: ""; + content: ''; display: block; flex-shrink: 0; height: 16px; @@ -479,7 +479,7 @@ a.plyr__control::before { width: 16px; } -.plyr__menu__container .plyr__control[role="menuitemradio"]::after { +.plyr__menu__container .plyr__control[role='menuitemradio']::after { background: #fff; border: 0; height: 6px; @@ -492,7 +492,7 @@ a.plyr__control::before { } .plyr__menu__container - .plyr__control[role="menuitemradio"][aria-checked="true"]::before { + .plyr__control[role='menuitemradio'][aria-checked='true']::before { background: #00b3ff; background: var( --plyr-control-toggle-checked-background, @@ -501,14 +501,14 @@ a.plyr__control::before { } .plyr__menu__container - .plyr__control[role="menuitemradio"][aria-checked="true"]::after { + .plyr__control[role='menuitemradio'][aria-checked='true']::after { opacity: 1; transform: translateY(-50%) scale(1); } .plyr__menu__container - .plyr__control[role="menuitemradio"].plyr__tab-focus::before, -.plyr__menu__container .plyr__control[role="menuitemradio"]:hover::before { + .plyr__control[role='menuitemradio'].plyr__tab-focus::before, +.plyr__menu__container .plyr__control[role='menuitemradio']:hover::before { background: rgba(35, 40, 47, 0.1); } @@ -524,7 +524,7 @@ a.plyr__control::before { pointer-events: none; } -.plyr--full-ui input[type="range"] { +.plyr--full-ui input[type='range'] { -webkit-appearance: none; background: 0 0; border: 0; @@ -547,7 +547,7 @@ a.plyr__control::before { width: 100%; } -.plyr--full-ui input[type="range"]::-webkit-slider-runnable-track { +.plyr--full-ui input[type='range']::-webkit-slider-runnable-track { background: 0 0; border: 0; border-radius: calc(5px / 2); @@ -566,7 +566,7 @@ a.plyr__control::before { ); } -.plyr--full-ui input[type="range"]::-webkit-slider-thumb { +.plyr--full-ui input[type='range']::-webkit-slider-thumb { background: #fff; background: var(--plyr-range-thumb-background, #fff); border: 0; @@ -596,7 +596,7 @@ a.plyr__control::before { ); } -.plyr--full-ui input[type="range"]::-moz-range-track { +.plyr--full-ui input[type='range']::-moz-range-track { background: 0 0; border: 0; border-radius: calc(5px / 2); @@ -608,7 +608,7 @@ a.plyr__control::before { user-select: none; } -.plyr--full-ui input[type="range"]::-moz-range-thumb { +.plyr--full-ui input[type='range']::-moz-range-thumb { background: #fff; background: var(--plyr-range-thumb-background, #fff); border: 0; @@ -628,7 +628,7 @@ a.plyr__control::before { width: var(--plyr-range-thumb-height, 13px); } -.plyr--full-ui input[type="range"]::-moz-range-progress { +.plyr--full-ui input[type='range']::-moz-range-progress { background: currentColor; border-radius: calc(5px / 2); border-radius: calc(var(--plyr-range-track-height, 5px) / 2); @@ -636,7 +636,7 @@ a.plyr__control::before { height: var(--plyr-range-track-height, 5px); } -.plyr--full-ui input[type="range"]::-ms-track { +.plyr--full-ui input[type='range']::-ms-track { background: 0 0; border: 0; border-radius: calc(5px / 2); @@ -650,7 +650,7 @@ a.plyr__control::before { color: transparent; } -.plyr--full-ui input[type="range"]::-ms-fill-upper { +.plyr--full-ui input[type='range']::-ms-fill-upper { background: 0 0; border: 0; border-radius: calc(5px / 2); @@ -663,7 +663,7 @@ a.plyr__control::before { user-select: none; } -.plyr--full-ui input[type="range"]::-ms-fill-lower { +.plyr--full-ui input[type='range']::-ms-fill-lower { background: 0 0; border: 0; border-radius: calc(5px / 2); @@ -677,7 +677,7 @@ a.plyr__control::before { background: currentColor; } -.plyr--full-ui input[type="range"]::-ms-thumb { +.plyr--full-ui input[type='range']::-ms-thumb { background: #fff; background: var(--plyr-range-thumb-background, #fff); border: 0; @@ -698,20 +698,20 @@ a.plyr__control::before { margin-top: 0; } -.plyr--full-ui input[type="range"]::-ms-tooltip { +.plyr--full-ui input[type='range']::-ms-tooltip { display: none; } -.plyr--full-ui input[type="range"]:focus { +.plyr--full-ui input[type='range']:focus { outline: 0; } -.plyr--full-ui input[type="range"]::-moz-focus-outer { +.plyr--full-ui input[type='range']::-moz-focus-outer { border: 0; } .plyr--full-ui - input[type="range"].plyr__tab-focus::-webkit-slider-runnable-track { + input[type='range'].plyr__tab-focus::-webkit-slider-runnable-track { outline-color: #00b3ff; outline-color: var( --plyr-tab-focus-color, @@ -722,7 +722,7 @@ a.plyr__control::before { outline-width: 3px; } -.plyr--full-ui input[type="range"].plyr__tab-focus::-moz-range-track { +.plyr--full-ui input[type='range'].plyr__tab-focus::-moz-range-track { outline-color: #00b3ff; outline-color: var( --plyr-tab-focus-color, @@ -733,7 +733,7 @@ a.plyr__control::before { outline-width: 3px; } -.plyr--full-ui input[type="range"].plyr__tab-focus::-ms-track { +.plyr--full-ui input[type='range'].plyr__tab-focus::-ms-track { outline-color: #00b3ff; outline-color: var( --plyr-tab-focus-color, @@ -769,7 +769,7 @@ a.plyr__control::before { } .plyr__time + .plyr__time::before { - content: "\2044"; + content: '\2044'; margin-right: 10px; margin-right: var(--plyr-control-spacing, 10px); } @@ -821,7 +821,7 @@ a.plyr__control::before { var(--plyr-tooltip-background, rgba(255, 255, 255, 0.9)); bottom: calc(4px * -1); bottom: calc(var(--plyr-tooltip-arrow-size, 4px) * -1); - content: ""; + content: ''; height: 0; left: 50%; position: absolute; @@ -906,7 +906,7 @@ a.plyr__control::before { position: relative; } -.plyr__progress input[type="range"], +.plyr__progress input[type='range'], .plyr__progress__buffer { margin-left: calc(13px * -0.5); margin-left: calc(var(--plyr-range-thumb-height, 13px) * -0.5); @@ -916,7 +916,7 @@ a.plyr__control::before { width: calc(100% + var(--plyr-range-thumb-height, 13px)); } -.plyr__progress input[type="range"] { +.plyr__progress input[type='range'] { position: relative; z-index: 2; } @@ -1024,7 +1024,7 @@ a.plyr__control::before { width: 20%; } -.plyr__volume input[type="range"] { +.plyr__volume input[type='range'] { margin-left: calc(10px / 2); margin-left: calc(var(--plyr-control-spacing, 10px) / 2); margin-right: calc(10px / 2); @@ -1054,7 +1054,7 @@ a.plyr__control::before { .plyr--audio .plyr__control.plyr__tab-focus, .plyr--audio .plyr__control:hover, -.plyr--audio .plyr__control[aria-expanded="true"] { +.plyr--audio .plyr__control[aria-expanded='true'] { background: #00b3ff; background: var( --plyr-audio-control-background-hover, @@ -1064,7 +1064,7 @@ a.plyr__control::before { color: var(--plyr-audio-control-color-hover, #fff); } -.plyr--full-ui.plyr--audio input[type="range"]::-webkit-slider-runnable-track { +.plyr--full-ui.plyr--audio input[type='range']::-webkit-slider-runnable-track { background-color: rgba(193, 200, 209, 0.6); background-color: var( --plyr-audio-range-track-background, @@ -1072,7 +1072,7 @@ a.plyr__control::before { ); } -.plyr--full-ui.plyr--audio input[type="range"]::-moz-range-track { +.plyr--full-ui.plyr--audio input[type='range']::-moz-range-track { background-color: rgba(193, 200, 209, 0.6); background-color: var( --plyr-audio-range-track-background, @@ -1080,7 +1080,7 @@ a.plyr__control::before { ); } -.plyr--full-ui.plyr--audio input[type="range"]::-ms-track { +.plyr--full-ui.plyr--audio input[type='range']::-ms-track { background-color: rgba(193, 200, 209, 0.6); background-color: var( --plyr-audio-range-track-background, @@ -1088,7 +1088,7 @@ a.plyr__control::before { ); } -.plyr--full-ui.plyr--audio input[type="range"]:active::-webkit-slider-thumb { +.plyr--full-ui.plyr--audio input[type='range']:active::-webkit-slider-thumb { box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2), 0 0 0 3px rgba(35, 40, 47, 0.1); box-shadow: var( @@ -1100,7 +1100,7 @@ a.plyr__control::before { var(--plyr-audio-range-thumb-active-shadow-color, rgba(35, 40, 47, 0.1)); } -.plyr--full-ui.plyr--audio input[type="range"]:active::-moz-range-thumb { +.plyr--full-ui.plyr--audio input[type='range']:active::-moz-range-thumb { box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2), 0 0 0 3px rgba(35, 40, 47, 0.1); box-shadow: var( @@ -1112,7 +1112,7 @@ a.plyr__control::before { var(--plyr-audio-range-thumb-active-shadow-color, rgba(35, 40, 47, 0.1)); } -.plyr--full-ui.plyr--audio input[type="range"]:active::-ms-thumb { +.plyr--full-ui.plyr--audio input[type='range']:active::-ms-thumb { box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2), 0 0 0 3px rgba(35, 40, 47, 0.1); box-shadow: var( @@ -1207,7 +1207,7 @@ a.plyr__control::before { .plyr--video .plyr__control.plyr__tab-focus, .plyr--video .plyr__control:hover, -.plyr--video .plyr__control[aria-expanded="true"] { +.plyr--video .plyr__control[aria-expanded='true'] { background: #00b3ff; background: var( --plyr-video-control-background-hover, @@ -1258,7 +1258,7 @@ a.plyr__control::before { display: block; } -.plyr--full-ui.plyr--video input[type="range"]::-webkit-slider-runnable-track { +.plyr--full-ui.plyr--video input[type='range']::-webkit-slider-runnable-track { background-color: rgba(255, 255, 255, 0.25); background-color: var( --plyr-video-range-track-background, @@ -1266,7 +1266,7 @@ a.plyr__control::before { ); } -.plyr--full-ui.plyr--video input[type="range"]::-moz-range-track { +.plyr--full-ui.plyr--video input[type='range']::-moz-range-track { background-color: rgba(255, 255, 255, 0.25); background-color: var( --plyr-video-range-track-background, @@ -1274,7 +1274,7 @@ a.plyr__control::before { ); } -.plyr--full-ui.plyr--video input[type="range"]::-ms-track { +.plyr--full-ui.plyr--video input[type='range']::-ms-track { background-color: rgba(255, 255, 255, 0.25); background-color: var( --plyr-video-range-track-background, @@ -1282,7 +1282,7 @@ a.plyr__control::before { ); } -.plyr--full-ui.plyr--video input[type="range"]:active::-webkit-slider-thumb { +.plyr--full-ui.plyr--video input[type='range']:active::-webkit-slider-thumb { box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2), 0 0 0 3px rgba(255, 255, 255, 0.5); box-shadow: var( @@ -1297,7 +1297,7 @@ a.plyr__control::before { ); } -.plyr--full-ui.plyr--video input[type="range"]:active::-moz-range-thumb { +.plyr--full-ui.plyr--video input[type='range']:active::-moz-range-thumb { box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2), 0 0 0 3px rgba(255, 255, 255, 0.5); box-shadow: var( @@ -1312,7 +1312,7 @@ a.plyr__control::before { ); } -.plyr--full-ui.plyr--video input[type="range"]:active::-ms-thumb { +.plyr--full-ui.plyr--video input[type='range']:active::-ms-thumb { box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2), 0 0 0 3px rgba(255, 255, 255, 0.5); box-shadow: var( @@ -1713,7 +1713,7 @@ a.plyr__control::before { var(--plyr-tooltip-background, rgba(255, 255, 255, 0.9)); bottom: calc(4px * -1); bottom: calc(var(--plyr-tooltip-arrow-size, 4px) * -1); - content: ""; + content: ''; height: 0; left: 50%; position: absolute; diff --git a/src/assets/css/slider.css b/src/assets/css/slider.css index 2e5f40d..071210b 100644 --- a/src/assets/css/slider.css +++ b/src/assets/css/slider.css @@ -89,7 +89,7 @@ } .nyancat .vue-slider-dot-handle { - background: url("/img/logos/nyancat.gif"); + background: url('/img/logos/nyancat.gif'); background-size: 36px; width: 36px; height: 24px; @@ -100,6 +100,11 @@ visibility: visible; } +.nyancat-stop .vue-slider-dot-handle { + background-image: url('/img/logos/nyancat-stop.png'); + transition: 300ms; +} + /* lyrics */ .lyrics-page .vue-slider-rail { background-color: rgba(128, 128, 128, 0.18); @@ -121,10 +126,19 @@ display: none; } -body[data-theme="dark"] .lyrics-page .vue-slider-process { +body[data-theme='dark'] .lyrics-page .vue-slider-process { background-color: #fafafa; } -body[data-theme="dark"] .lyrics-page .vue-slider-dot-handle { +body[data-theme='dark'] .lyrics-page .vue-slider-dot-handle { + background-color: #fff; +} + +.lyrics-page[data-theme='dark'] .vue-slider-rail { + background-color: rgba(255, 255, 255, 0.18); +} + +.lyrics-page[data-theme='dark'] .vue-slider-process, +.lyrics-page[data-theme='dark'] .vue-slider-dot-handle { background-color: #fff; } diff --git a/src/assets/fonts/Barlow-Black.ttf b/src/assets/fonts/Barlow-Black.ttf new file mode 100755 index 0000000..c4a4b05 Binary files /dev/null and b/src/assets/fonts/Barlow-Black.ttf differ diff --git a/src/assets/fonts/Barlow-Black.woff2 b/src/assets/fonts/Barlow-Black.woff2 new file mode 100644 index 0000000..b3b05b0 Binary files /dev/null and b/src/assets/fonts/Barlow-Black.woff2 differ diff --git a/src/assets/fonts/Barlow-Bold.ttf b/src/assets/fonts/Barlow-Bold.ttf new file mode 100755 index 0000000..cf78fd4 Binary files /dev/null and b/src/assets/fonts/Barlow-Bold.ttf differ diff --git a/src/assets/fonts/Barlow-Bold.woff2 b/src/assets/fonts/Barlow-Bold.woff2 new file mode 100644 index 0000000..1e3542c Binary files /dev/null and b/src/assets/fonts/Barlow-Bold.woff2 differ diff --git a/src/assets/fonts/Barlow-ExtraBold.ttf b/src/assets/fonts/Barlow-ExtraBold.ttf new file mode 100755 index 0000000..3d5f061 Binary files /dev/null and b/src/assets/fonts/Barlow-ExtraBold.ttf differ diff --git a/src/assets/fonts/Barlow-ExtraBold.woff2 b/src/assets/fonts/Barlow-ExtraBold.woff2 new file mode 100644 index 0000000..d5789b2 Binary files /dev/null and b/src/assets/fonts/Barlow-ExtraBold.woff2 differ diff --git a/src/assets/fonts/Barlow-Medium.ttf b/src/assets/fonts/Barlow-Medium.ttf new file mode 100755 index 0000000..1ed9192 Binary files /dev/null and b/src/assets/fonts/Barlow-Medium.ttf differ diff --git a/src/assets/fonts/Barlow-Medium.woff2 b/src/assets/fonts/Barlow-Medium.woff2 new file mode 100644 index 0000000..93e9bcb Binary files /dev/null and b/src/assets/fonts/Barlow-Medium.woff2 differ diff --git a/src/assets/fonts/Barlow-Regular.ttf b/src/assets/fonts/Barlow-Regular.ttf new file mode 100755 index 0000000..8bc6a2e Binary files /dev/null and b/src/assets/fonts/Barlow-Regular.ttf differ diff --git a/src/assets/fonts/Barlow-Regular.woff2 b/src/assets/fonts/Barlow-Regular.woff2 new file mode 100644 index 0000000..65b4987 Binary files /dev/null and b/src/assets/fonts/Barlow-Regular.woff2 differ diff --git a/src/assets/fonts/Barlow-SemiBold.ttf b/src/assets/fonts/Barlow-SemiBold.ttf new file mode 100755 index 0000000..7b7320b Binary files /dev/null and b/src/assets/fonts/Barlow-SemiBold.ttf differ diff --git a/src/assets/fonts/Barlow-SemiBold.woff2 b/src/assets/fonts/Barlow-SemiBold.woff2 new file mode 100644 index 0000000..3f582a0 Binary files /dev/null and b/src/assets/fonts/Barlow-SemiBold.woff2 differ diff --git a/src/assets/icons/arrow-down.svg b/src/assets/icons/arrow-down.svg new file mode 100644 index 0000000..333ce72 --- /dev/null +++ b/src/assets/icons/arrow-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/arrow-up-alt.svg b/src/assets/icons/arrow-up-alt.svg new file mode 100644 index 0000000..1980837 --- /dev/null +++ b/src/assets/icons/arrow-up-alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/arrow-up.svg b/src/assets/icons/arrow-up.svg new file mode 100644 index 0000000..bc5a0bd --- /dev/null +++ b/src/assets/icons/arrow-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/circle.svg b/src/assets/icons/circle.svg deleted file mode 100644 index eb210da..0000000 --- a/src/assets/icons/circle.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/icons/dropdown.svg b/src/assets/icons/dropdown.svg new file mode 100644 index 0000000..e1d0723 --- /dev/null +++ b/src/assets/icons/dropdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/expand.svg b/src/assets/icons/expand.svg deleted file mode 100644 index 41efe24..0000000 --- a/src/assets/icons/expand.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/assets/icons/fm.svg b/src/assets/icons/fm.svg new file mode 100644 index 0000000..fe760cd --- /dev/null +++ b/src/assets/icons/fm.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/fullscreen-exit.svg b/src/assets/icons/fullscreen-exit.svg new file mode 100644 index 0000000..f76f601 --- /dev/null +++ b/src/assets/icons/fullscreen-exit.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/fullscreen.svg b/src/assets/icons/fullscreen.svg new file mode 100644 index 0000000..e6128c0 --- /dev/null +++ b/src/assets/icons/fullscreen.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/icons/index.js b/src/assets/icons/index.js index 70c0243..ee45a56 100644 --- a/src/assets/icons/index.js +++ b/src/assets/icons/index.js @@ -1,8 +1,7 @@ -import Vue from "vue"; -import SvgIcon from "@/components/SvgIcon"; +import Vue from 'vue'; +import SvgIcon from '@/components/SvgIcon'; -Vue.component("svg-icon", SvgIcon); -const requireAll = (requireContext) => - requireContext.keys().map(requireContext); -const req = require.context("./", true, /\.svg$/); +Vue.component('svg-icon', SvgIcon); +const requireAll = requireContext => requireContext.keys().map(requireContext); +const req = require.context('./', true, /\.svg$/); requireAll(req); diff --git a/src/assets/icons/login.svg b/src/assets/icons/login.svg new file mode 100644 index 0000000..b4acd61 --- /dev/null +++ b/src/assets/icons/login.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/plus.svg b/src/assets/icons/plus.svg new file mode 100644 index 0000000..e254d52 --- /dev/null +++ b/src/assets/icons/plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/sort-up.svg b/src/assets/icons/sort-up.svg new file mode 100644 index 0000000..3efe4cf --- /dev/null +++ b/src/assets/icons/sort-up.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/assets/icons/thumbs-down.svg b/src/assets/icons/thumbs-down.svg new file mode 100644 index 0000000..02fe63b --- /dev/null +++ b/src/assets/icons/thumbs-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/icons/translation.svg b/src/assets/icons/translation.svg deleted file mode 100644 index 9cbd770..0000000 --- a/src/assets/icons/translation.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/background.js b/src/background.js index 3f46503..94dbbc0 100644 --- a/src/background.js +++ b/src/background.js @@ -1,37 +1,105 @@ -"use strict"; -import { app, protocol, BrowserWindow, shell, dialog } from "electron"; -import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; -import { startNeteaseMusicApi } from "./electron/services"; -import { initIpcMain } from "./electron/ipcMain.js"; -import { createMenu } from "./electron/menu"; -import { createTray } from "@/electron/tray"; -import { createTouchBar } from "./electron/touchBar"; -import { createDockMenu } from "./electron/dockMenu"; -import { autoUpdater } from "electron-updater"; -import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer"; -import express from "express"; -import expressProxy from "express-http-proxy"; -import Store from "electron-store"; +'use strict'; +import { + app, + protocol, + BrowserWindow, + shell, + dialog, + globalShortcut, + nativeTheme, + screen, +} from 'electron'; +import { + isWindows, + isMac, + isLinux, + isDevelopment, + isCreateTray, + isCreateMpris, +} from '@/utils/platform'; +import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'; +import { startNeteaseMusicApi } from './electron/services'; +import { initIpcMain } from './electron/ipcMain.js'; +import { createMenu } from './electron/menu'; +import { createTray } from '@/electron/tray'; +import { createTouchBar } from './electron/touchBar'; +import { createDockMenu } from './electron/dockMenu'; +import { registerGlobalShortcut } from './electron/globalShortcut'; +import { autoUpdater } from 'electron-updater'; +import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer'; +import { EventEmitter } from 'events'; +import express from 'express'; +import expressProxy from 'express-http-proxy'; +import Store from 'electron-store'; +import { createMpris, createDbus } from '@/electron/mpris'; +import { spawn } from 'child_process'; +const clc = require('cli-color'); +const log = text => { + console.log(`${clc.blueBright('[background.js]')} ${text}`); +}; + +const closeOnLinux = (e, win, store) => { + let closeOpt = store.get('settings.closeAppOption'); + if (closeOpt !== 'exit') { + e.preventDefault(); + } + + if (closeOpt === 'ask') { + dialog + .showMessageBox({ + type: 'info', + title: 'Information', + cancelId: 2, + defaultId: 0, + message: '确定要关闭吗?', + buttons: ['最小化到托盘', '直接退出'], + checkboxLabel: '记住我的选择', + }) + .then(result => { + if (result.checkboxChecked && result.response !== 2) { + win.webContents.send( + 'rememberCloseAppOption', + result.response === 0 ? 'minimizeToTray' : 'exit' + ); + } + + if (result.response === 0) { + win.hide(); //调用 最小化实例方法 + } else if (result.response === 1) { + win = null; + app.exit(); //exit()直接关闭客户端,不会执行quit(); + } + }) + .catch(err => { + log(err); + }); + } else if (closeOpt === 'exit') { + win = null; + app.quit(); + } else { + win.hide(); + } +}; class Background { constructor() { this.window = null; - this.tray = null; + this.ypmTrayImpl = null; this.store = new Store({ windowWidth: { - width: { type: "number", default: 1440 }, - height: { type: "number", default: 840 }, + width: { type: 'number', default: 1440 }, + height: { type: 'number', default: 840 }, }, }); this.neteaseMusicAPI = null; this.expressApp = null; - this.willQuitApp = process.platform === "darwin" ? false : true; + this.willQuitApp = !isMac; this.init(); } init() { - console.log("initializing"); + log('initializing'); // Make sure the app is singleton. if (!app.requestSingleInstanceLock()) return app.quit(); @@ -42,16 +110,21 @@ class Background { // create Express app this.createExpressApp(); - // init ipcMain - initIpcMain(this.window, this.store); - // Scheme must be registered before the app is ready protocol.registerSchemesAsPrivileged([ - { scheme: "app", privileges: { secure: true, standard: true } }, + { scheme: 'app', privileges: { secure: true, standard: true } }, ]); // handle app events this.handleAppEvents(); + + // disable chromium mpris + if (isCreateMpris) { + app.commandLine.appendSwitch( + 'disable-features', + 'HardwareMediaKeyHandling,MediaSessionService' + ); + } } async initDevtools() { @@ -59,162 +132,320 @@ class Background { try { await installExtension(VUEJS_DEVTOOLS); } catch (e) { - console.error("Vue Devtools failed to install:", e.toString()); + console.error('Vue Devtools failed to install:', e.toString()); } // Exit cleanly on request from parent process in development mode. - if (process.platform === "win32") { - process.on("message", (data) => { - if (data === "graceful-exit") { + if (isWindows) { + process.on('message', data => { + if (data === 'graceful-exit') { app.quit(); } }); } else { - process.on("SIGTERM", () => { + process.on('SIGTERM', () => { app.quit(); }); } } createExpressApp() { - console.log("creating express app"); + log('creating express app'); const expressApp = express(); - expressApp.use("/", express.static(__dirname + "/")); - expressApp.use("/api", expressProxy("http://127.0.0.1:10754")); - this.expressApp = expressApp.listen(27232); + expressApp.use('/', express.static(__dirname + '/')); + expressApp.use('/api', expressProxy('http://127.0.0.1:10754')); + expressApp.use('/player', (req, res) => { + this.window.webContents + .executeJavaScript('window.yesplaymusic.player') + .then(result => { + res.send({ + currentTrack: result._isPersonalFM + ? result._personalFMTrack + : result._currentTrack, + progress: result._progress, + }); + }); + }); + this.expressApp = expressApp.listen(27232, '127.0.0.1'); } createWindow() { - console.log("creating app window"); + log('creating app window'); - this.window = new BrowserWindow({ - width: this.store.get("window.width") | 1440, - height: this.store.get("window.height") | 840, + const appearance = this.store.get('settings.appearance'); + const showLibraryDefault = this.store.get('settings.showLibraryDefault'); + + const options = { + width: this.store.get('window.width') || 1440, + height: this.store.get('window.height') || 840, minWidth: 1080, minHeight: 720, - titleBarStyle: "hiddenInset", + titleBarStyle: 'hiddenInset', + frame: !( + isWindows || + (isLinux && this.store.get('settings.linuxEnableCustomTitlebar')) + ), + title: 'YesPlayMusic', + show: false, webPreferences: { webSecurity: false, nodeIntegration: true, + enableRemoteModule: true, + contextIsolation: false, }, - }); + backgroundColor: + ((appearance === undefined || appearance === 'auto') && + nativeTheme.shouldUseDarkColors) || + appearance === 'dark' + ? '#222' + : '#fff', + }; + + if (this.store.get('window.x') && this.store.get('window.y')) { + let x = this.store.get('window.x'); + let y = this.store.get('window.y'); + + let displays = screen.getAllDisplays(); + let isResetWindiw = false; + if (displays.length === 1) { + let { bounds } = displays[0]; + if ( + x < bounds.x || + x > bounds.x + bounds.width - 50 || + y < bounds.y || + y > bounds.y + bounds.height - 50 + ) { + isResetWindiw = true; + } + } else { + isResetWindiw = true; + for (let i = 0; i < displays.length; i++) { + let { bounds } = displays[i]; + if ( + x > bounds.x && + x < bounds.x + bounds.width && + y > bounds.y && + y < bounds.y - bounds.height + ) { + // 检测到APP窗口当前处于一个可用的屏幕里,break + isResetWindiw = false; + break; + } + } + } + + if (!isResetWindiw) { + options.x = x; + options.y = y; + } + } + + this.window = new BrowserWindow(options); // hide menu bar on Microsoft Windows and Linux this.window.setMenuBarVisibility(false); if (process.env.WEBPACK_DEV_SERVER_URL) { // Load the url of the dev server if in development mode - this.window.loadURL(process.env.WEBPACK_DEV_SERVER_URL); + this.window.loadURL( + showLibraryDefault + ? `${process.env.WEBPACK_DEV_SERVER_URL}/#/library` + : process.env.WEBPACK_DEV_SERVER_URL + ); if (!process.env.IS_TEST) this.window.webContents.openDevTools(); } else { - createProtocol("app"); - this.window.loadURL("http://localhost:27232"); + createProtocol('app'); + this.window.loadURL( + showLibraryDefault + ? 'http://localhost:27232/#/library' + : 'http://localhost:27232' + ); } } checkForUpdates() { + if (isDevelopment) return; + log('checkForUpdates'); autoUpdater.checkForUpdatesAndNotify(); - const showNewVersionMessage = (info) => { + const showNewVersionMessage = info => { dialog .showMessageBox({ - title: "发现新版本 v" + info.version, - message: "发现新版本 v" + info.version, - detail: "是否前往 Github 下载新版本安装包?", - buttons: ["下载", "取消"], - type: "question", + title: '发现新版本 v' + info.version, + message: '发现新版本 v' + info.version, + detail: '是否前往 GitHub 下载新版本安装包?', + buttons: ['下载', '取消'], + type: 'question', noLink: true, }) - .then((result) => { + .then(result => { if (result.response === 0) { shell.openExternal( - "https://github.com/qier222/YesPlayMusic/releases" + 'https://github.com/qier222/YesPlayMusic/releases' ); } }); }; - if (process.platform === "darwin") { - autoUpdater.on("update-available", (info) => { - showNewVersionMessage(info); - }); - } + autoUpdater.on('update-available', info => { + showNewVersionMessage(info); + }); } handleWindowEvents() { - this.window.once("ready-to-show", () => { - console.log("windows ready-to-show event"); + this.window.once('ready-to-show', () => { + log('window ready-to-show event'); this.window.show(); + this.store.set('window', this.window.getBounds()); }); - this.window.on("close", (e) => { - console.log("windows close event"); - if (this.willQuitApp) { - /* the user tried to quit the app */ - this.window = null; - app.quit(); + this.window.on('close', e => { + log('window close event'); + + if (isLinux) { + closeOnLinux(e, this.window, this.store); + } else if (isMac) { + if (this.willQuitApp) { + this.window = null; + app.quit(); + } else { + e.preventDefault(); + this.window.hide(); + } } else { - /* the user only tried to close the window */ - e.preventDefault(); - this.window.hide(); + let closeOpt = this.store.get('settings.closeAppOption'); + if (this.willQuitApp && (closeOpt === 'exit' || closeOpt === 'ask')) { + this.window = null; + app.quit(); + } else { + e.preventDefault(); + this.window.hide(); + } } }); - this.window.on("resize", () => { - let { height, width } = this.window.getBounds(); - this.store.set("window", { height, width }); + this.window.on('resized', () => { + this.store.set('window', this.window.getBounds()); }); - this.window.on("minimize", () => { - if ( - ["win32", "linux"].includes(process.platform) && - this.store.get("settings.minimizeToTray") - ) { - this.tray = createTray(this.window); - this.window.hide(); - } + this.window.on('moved', () => { + this.store.set('window', this.window.getBounds()); }); - this.window.webContents.on("new-window", function (e, url) { + this.window.on('maximize', () => { + this.window.webContents.send('isMaximized', true); + }); + + this.window.on('unmaximize', () => { + this.window.webContents.send('isMaximized', false); + }); + + this.window.webContents.on('new-window', function (e, url) { e.preventDefault(); + log('open url'); + const excludeHosts = ['www.last.fm']; + const exclude = excludeHosts.find(host => url.includes(host)); + if (exclude) { + const newWindow = new BrowserWindow({ + width: 800, + height: 600, + titleBarStyle: 'default', + title: 'YesPlayMusic', + webPreferences: { + webSecurity: false, + nodeIntegration: true, + enableRemoteModule: true, + contextIsolation: false, + }, + }); + newWindow.loadURL(url); + return; + } shell.openExternal(url); }); } handleAppEvents() { - app.on("ready", async () => { + app.on('ready', async () => { // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. - console.log("app ready event"); + log('app ready event'); // for development - if (process.env.NODE_ENV !== "production") { + if (isDevelopment) { this.initDevtools(); } // create window this.createWindow(); + this.window.once('ready-to-show', () => { + this.window.show(); + }); this.handleWindowEvents(); + // create tray + if (isCreateTray) { + this.trayEventEmitter = new EventEmitter(); + this.ypmTrayImpl = createTray(this.window, this.trayEventEmitter); + } + + // init ipcMain + initIpcMain(this.window, this.store, this.trayEventEmitter); + + // set proxy + const proxyRules = this.store.get('proxy'); + if (proxyRules) { + this.window.webContents.session.setProxy({ proxyRules }, result => { + log('finished setProxy', result); + }); + } + // check for updates this.checkForUpdates(); // create menu - createMenu(this.window); + createMenu(this.window, this.store); // create dock menu for macOS - app.dock.setMenu(createDockMenu(this.window)); + const createdDockMenu = createDockMenu(this.window); + if (createDockMenu && app.dock) app.dock.setMenu(createdDockMenu); // create touch bar - this.window.setTouchBar(createTouchBar(this.window)); + const createdTouchBar = createTouchBar(this.window); + if (createdTouchBar) this.window.setTouchBar(createdTouchBar); + + // register global shortcuts + if (this.store.get('settings.enableGlobalShortcut') !== false) { + registerGlobalShortcut(this.window, this.store); + } + + // try to start osdlyrics process on start + if (this.store.get('settings.enableOsdlyricsSupport')) { + await createDbus(this.window); + log('try to start osdlyrics process'); + const osdlyricsProcess = spawn('osdlyrics'); + + osdlyricsProcess.on('error', err => { + log(`failed to start osdlyrics: ${err.message}`); + }); + + osdlyricsProcess.on('exit', (code, signal) => { + log(`osdlyrics process exited with code ${code}, signal ${signal}`); + }); + } + + // create mpris + if (isCreateMpris) { + createMpris(this.window); + } }); - app.on("activate", () => { + app.on('activate', () => { // On macOS it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. - console.log("app activate event"); + log('app activate event'); if (this.window === null) { this.createWindow(); } else { @@ -222,19 +453,36 @@ class Background { } }); - app.on("window-all-closed", () => { - if (process.platform !== "darwin") { + app.on('window-all-closed', () => { + if (!isMac) { app.quit(); } }); - app.on("before-quit", () => { + app.on('before-quit', () => { this.willQuitApp = true; }); - app.on("quit", () => { + app.on('quit', () => { this.expressApp.close(); }); + + app.on('will-quit', () => { + // unregister all global shortcuts + globalShortcut.unregisterAll(); + }); + + if (!isMac) { + app.on('second-instance', (e, cl, wd) => { + if (this.window) { + this.window.show(); + if (this.window.isMinimized()) { + this.window.restore(); + } + this.window.focus(); + } + }); + } } } diff --git a/src/components/ArtistsInLine.vue b/src/components/ArtistsInLine.vue index b834f47..e2c619d 100644 --- a/src/components/ArtistsInLine.vue +++ b/src/components/ArtistsInLine.vue @@ -1,16 +1,21 @@ - + diff --git a/src/components/ButtonIcon.vue b/src/components/ButtonIcon.vue index efa14d7..d6afee3 100644 --- a/src/components/ButtonIcon.vue +++ b/src/components/ButtonIcon.vue @@ -4,7 +4,7 @@ diff --git a/src/components/ButtonTwoTone.vue b/src/components/ButtonTwoTone.vue index d2cde2b..c72739a 100644 --- a/src/components/ButtonTwoTone.vue +++ b/src/components/ButtonTwoTone.vue @@ -2,7 +2,7 @@ - {{ - track.no - }} + {{ trackNo }} -
+
{{ track.dt | formatTime }}
+ +
{{ track.playCount }}
+ + diff --git a/src/electron/dockMenu.js b/src/electron/dockMenu.js index 5b833c5..fe0eb24 100644 --- a/src/electron/dockMenu.js +++ b/src/electron/dockMenu.js @@ -1,24 +1,24 @@ -const { Menu } = require("electron"); +const { Menu } = require('electron'); export function createDockMenu(win) { return Menu.buildFromTemplate([ { - label: "Play", + label: 'Play', click() { - win.webContents.send("play"); + win.webContents.send('play'); }, }, - { type: "separator" }, + { type: 'separator' }, { - label: "Next", + label: 'Next', click() { - win.webContents.send("next"); + win.webContents.send('next'); }, }, { - label: "Previous", + label: 'Previous', click() { - win.webContents.send("previous"); + win.webContents.send('previous'); }, }, ]); diff --git a/src/electron/globalShortcut.js b/src/electron/globalShortcut.js new file mode 100644 index 0000000..c352eaf --- /dev/null +++ b/src/electron/globalShortcut.js @@ -0,0 +1,58 @@ +import defaultShortcuts from '@/utils/shortcuts'; +const { globalShortcut } = require('electron'); + +const clc = require('cli-color'); +const log = text => { + console.log(`${clc.blueBright('[globalShortcut.js]')} ${text}`); +}; + +export function registerGlobalShortcut(win, store) { + log('registerGlobalShortcut'); + let shortcuts = store.get('settings.shortcuts'); + if (shortcuts === undefined) { + shortcuts = defaultShortcuts; + } + + globalShortcut.register( + shortcuts.find(s => s.id === 'play').globalShortcut, + () => { + win.webContents.send('play'); + } + ); + globalShortcut.register( + shortcuts.find(s => s.id === 'next').globalShortcut, + () => { + win.webContents.send('next'); + } + ); + globalShortcut.register( + shortcuts.find(s => s.id === 'previous').globalShortcut, + () => { + win.webContents.send('previous'); + } + ); + globalShortcut.register( + shortcuts.find(s => s.id === 'increaseVolume').globalShortcut, + () => { + win.webContents.send('increaseVolume'); + } + ); + globalShortcut.register( + shortcuts.find(s => s.id === 'decreaseVolume').globalShortcut, + () => { + win.webContents.send('decreaseVolume'); + } + ); + globalShortcut.register( + shortcuts.find(s => s.id === 'like').globalShortcut, + () => { + win.webContents.send('like'); + } + ); + globalShortcut.register( + shortcuts.find(s => s.id === 'minimize').globalShortcut, + () => { + win.isVisible() ? win.hide() : win.show(); + } + ); +} diff --git a/src/electron/ipcMain.js b/src/electron/ipcMain.js index c6007fc..aa0e3d7 100644 --- a/src/electron/ipcMain.js +++ b/src/electron/ipcMain.js @@ -1,64 +1,323 @@ -import { app, ipcMain, dialog } from "electron"; -import match from "@njzy/unblockneteasemusic"; +import { app, dialog, globalShortcut, ipcMain } from 'electron'; +import UNM from '@unblockneteasemusic/rust-napi'; +import { registerGlobalShortcut } from '@/electron/globalShortcut'; +import cloneDeep from 'lodash/cloneDeep'; +import shortcuts from '@/utils/shortcuts'; +import { createMenu } from './menu'; +import { isCreateTray, isMac } from '@/utils/platform'; -export function initIpcMain(win, store) { - ipcMain.on("unblock-music", (event, track) => { - // 兼容 unblockneteasemusic 所使用的 api 字段 - track.alias = track.alia || []; - track.duration = track.dt || 0; - track.album = track.al || []; - track.artists = track.ar || []; +const clc = require('cli-color'); +const log = text => { + console.log(`${clc.blueBright('[ipcMain.js]')} ${text}`); +}; - const matchPromise = match(track.id, ["qq", "kuwo", "migu"], track); - const timeoutPromise = new Promise((_, reject) => { - setTimeout(() => { - reject("timeout"); - }, 3000); +const exitAsk = (e, win) => { + e.preventDefault(); //阻止默认行为 + dialog + .showMessageBox({ + type: 'info', + title: 'Information', + cancelId: 2, + defaultId: 0, + message: '确定要关闭吗?', + buttons: ['最小化', '直接退出'], + }) + .then(result => { + if (result.response == 0) { + e.preventDefault(); //阻止默认行为 + win.minimize(); //调用 最小化实例方法 + } else if (result.response == 1) { + win = null; + //app.quit(); + app.exit(); //exit()直接关闭客户端,不会执行quit(); + } + }) + .catch(err => { + log(err); }); - Promise.race([matchPromise, timeoutPromise]) - .then((res) => { - event.returnValue = res; - }) - .catch((err) => { - console.log("unblock music error: ", err); - event.returnValue = null; - }); +}; + +const exitAskWithoutMac = (e, win) => { + e.preventDefault(); //阻止默认行为 + dialog + .showMessageBox({ + type: 'info', + title: 'Information', + cancelId: 2, + defaultId: 0, + message: '确定要关闭吗?', + buttons: ['最小化到托盘', '直接退出'], + checkboxLabel: '记住我的选择', + }) + .then(result => { + if (result.checkboxChecked && result.response !== 2) { + win.webContents.send( + 'rememberCloseAppOption', + result.response === 0 ? 'minimizeToTray' : 'exit' + ); + } + + if (result.response === 0) { + e.preventDefault(); //阻止默认行为 + win.hide(); //调用 最小化实例方法 + } else if (result.response === 1) { + win = null; + //app.quit(); + app.exit(); //exit()直接关闭客户端,不会执行quit(); + } + }) + .catch(err => { + log(err); + }); +}; + +const client = require('discord-rich-presence')('818936529484906596'); + +/** + * Make data a Buffer. + * + * @param {?} data The data to convert. + * @returns {import("buffer").Buffer} The converted data. + */ +function toBuffer(data) { + if (data instanceof Buffer) { + return data; + } else { + return Buffer.from(data); + } +} + +/** + * Get the file base64 data from bilivideo. + * + * @param {string} url The URL to fetch. + * @returns {Promise} The file base64 data. + */ +async function getBiliVideoFile(url) { + const axios = await import('axios').then(m => m.default); + const response = await axios.get(url, { + headers: { + Referer: 'https://www.bilibili.com/', + 'User-Agent': 'okhttp/3.4.1', + }, + responseType: 'arraybuffer', }); - ipcMain.on("close", (e) => { - if (process.platform == "darwin") { - win.hide(); - } - e.preventDefault(); //阻止默认行为 - dialog - .showMessageBox({ - type: "info", - title: "Information", - cancelId: 2, - defaultId: 0, - message: "确定要关闭吗?", - buttons: ["最小化", "直接退出"], - }) - .then((result) => { - if (result.response == 0) { - e.preventDefault(); //阻止默认行为 - win.minimize(); //调用 最小化实例方法 - } else if (result.response == 1) { - win = null; - //app.quit(); - app.exit(); //exit()直接关闭客户端,不会执行quit(); + const buffer = toBuffer(response.data); + const encodedData = buffer.toString('base64'); + + return encodedData; +} + +/** + * Parse the source string (`a, b`) to source list `['a', 'b']`. + * + * @param {import("@unblockneteasemusic/rust-napi").Executor} executor + * @param {string} sourceString The source string. + * @returns {string[]} The source list. + */ +function parseSourceStringToList(executor, sourceString) { + const availableSource = executor.list(); + + return sourceString + .split(',') + .map(s => s.trim().toLowerCase()) + .filter(s => { + const isAvailable = availableSource.includes(s); + + if (!isAvailable) { + log(`This source is not one of the supported source: ${s}`); + } + + return isAvailable; + }); +} + +export function initIpcMain(win, store, trayEventEmitter) { + // WIP: Do not enable logging as it has some issues in non-blocking I/O environment. + // UNM.enableLogging(UNM.LoggingType.ConsoleEnv); + const unmExecutor = new UNM.Executor(); + + ipcMain.handle( + 'unblock-music', + /** + * + * @param {*} _ + * @param {string | null} sourceListString + * @param {Record} ncmTrack + * @param {UNM.Context} context + */ + async (_, sourceListString, ncmTrack, context) => { + // Formt the track input + // FIXME: Figure out the structure of Track + const song = { + id: ncmTrack.id && ncmTrack.id.toString(), + name: ncmTrack.name, + duration: ncmTrack.dt, + album: ncmTrack.al && { + id: ncmTrack.al.id && ncmTrack.al.id.toString(), + name: ncmTrack.al.name, + }, + artists: ncmTrack.ar + ? ncmTrack.ar.map(({ id, name }) => ({ + id: id && id.toString(), + name, + })) + : [], + }; + + const sourceList = + typeof sourceListString === 'string' + ? parseSourceStringToList(unmExecutor, sourceListString) + : ['ytdl', 'bilibili', 'pyncm', 'kugou']; + log(`[UNM] using source: ${sourceList.join(', ')}`); + log(`[UNM] using configuration: ${JSON.stringify(context)}`); + + try { + // TODO: tell users to install yt-dlp. + const matchedAudio = await unmExecutor.search( + sourceList, + song, + context + ); + const retrievedSong = await unmExecutor.retrieve(matchedAudio, context); + + // bilibili's audio file needs some special treatment + if (retrievedSong.url.includes('bilivideo.com')) { + retrievedSong.url = await getBiliVideoFile(retrievedSong.url); } - }) - .catch((err) => { - console.log(err); - }); + + log(`respond with retrieve song…`); + log(JSON.stringify(matchedAudio)); + return retrievedSong; + } catch (err) { + const errorMessage = err instanceof Error ? `${err.message}` : `${err}`; + log(`UnblockNeteaseMusic failed: ${errorMessage}`); + return null; + } + } + ); + + ipcMain.on('close', e => { + if (isMac) { + win.hide(); + exitAsk(e, win); + } else { + let closeOpt = store.get('settings.closeAppOption'); + if (closeOpt === 'exit') { + win = null; + //app.quit(); + app.exit(); //exit()直接关闭客户端,不会执行quit(); + } else if (closeOpt === 'minimizeToTray') { + e.preventDefault(); + win.hide(); + } else { + exitAskWithoutMac(e, win); + } + } }); - ipcMain.on("minimize", () => { + ipcMain.on('minimize', () => { win.minimize(); }); - ipcMain.on("settings", (event, options) => { - store.set("settings", options); + ipcMain.on('maximizeOrUnmaximize', () => { + win.isMaximized() ? win.unmaximize() : win.maximize(); }); + + ipcMain.on('settings', (event, options) => { + store.set('settings', options); + if (options.enableGlobalShortcut) { + registerGlobalShortcut(win, store); + } else { + log('unregister global shortcut'); + globalShortcut.unregisterAll(); + } + }); + + ipcMain.on('playDiscordPresence', (event, track) => { + client.updatePresence({ + details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','), + state: track.al.name, + endTimestamp: Date.now() + track.dt, + largeImageKey: track.al.picUrl, + largeImageText: 'Listening ' + track.name, + smallImageKey: 'play', + smallImageText: 'Playing', + instance: true, + }); + }); + + ipcMain.on('pauseDiscordPresence', (event, track) => { + client.updatePresence({ + details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','), + state: track.al.name, + largeImageKey: track.al.picUrl, + largeImageText: 'YesPlayMusic', + smallImageKey: 'pause', + smallImageText: 'Pause', + instance: true, + }); + }); + + ipcMain.on('setProxy', (event, config) => { + const proxyRules = `${config.protocol}://${config.server}:${config.port}`; + store.set('proxy', proxyRules); + win.webContents.session.setProxy( + { + proxyRules, + }, + () => { + log('finished setProxy'); + } + ); + }); + + ipcMain.on('removeProxy', (event, arg) => { + log('removeProxy'); + win.webContents.session.setProxy({}); + store.set('proxy', ''); + }); + + ipcMain.on('switchGlobalShortcutStatusTemporary', (e, status) => { + log('switchGlobalShortcutStatusTemporary'); + if (status === 'disable') { + globalShortcut.unregisterAll(); + } else { + registerGlobalShortcut(win, store); + } + }); + + ipcMain.on('updateShortcut', (e, { id, type, shortcut }) => { + log('updateShortcut'); + let shortcuts = store.get('settings.shortcuts'); + let newShortcut = shortcuts.find(s => s.id === id); + newShortcut[type] = shortcut; + store.set('settings.shortcuts', shortcuts); + + createMenu(win, store); + globalShortcut.unregisterAll(); + registerGlobalShortcut(win, store); + }); + + ipcMain.on('restoreDefaultShortcuts', () => { + log('restoreDefaultShortcuts'); + store.set('settings.shortcuts', cloneDeep(shortcuts)); + + createMenu(win, store); + globalShortcut.unregisterAll(); + registerGlobalShortcut(win, store); + }); + + if (isCreateTray) { + ipcMain.on('updateTrayTooltip', (_, title) => { + trayEventEmitter.emit('updateTooltip', title); + }); + ipcMain.on('updateTrayPlayState', (_, isPlaying) => { + trayEventEmitter.emit('updatePlayState', isPlaying); + }); + ipcMain.on('updateTrayLikeState', (_, isLiked) => { + trayEventEmitter.emit('updateLikeState', isLiked); + }); + } } diff --git a/src/electron/ipcRenderer.js b/src/electron/ipcRenderer.js index f9cfb4f..ea8faf9 100644 --- a/src/electron/ipcRenderer.js +++ b/src/electron/ipcRenderer.js @@ -1,70 +1,94 @@ +import store from '@/store'; + +const player = store.state.player; + export function ipcRenderer(vueInstance) { const self = vueInstance; // 添加专有的类名 - document.body.setAttribute("data-electron", "yes"); + document.body.setAttribute('data-electron', 'yes'); + document.body.setAttribute( + 'data-electron-os', + window.require('os').platform() + ); // ipc message channel - const electron = window.require("electron"); + const electron = window.require('electron'); const ipcRenderer = electron.ipcRenderer; // listens to the main process 'changeRouteTo' event and changes the route from // inside this Vue instance, according to what path the main process requires. // responds to Menu click() events at the main process and changes the route accordingly. - ipcRenderer.on("changeRouteTo", (event, path) => { + ipcRenderer.on('changeRouteTo', (event, path) => { self.$router.push(path); + if (store.state.showLyrics) { + store.commit('toggleLyrics'); + } }); - ipcRenderer.on("search", () => { + ipcRenderer.on('search', () => { // 触发数据响应 self.$refs.navbar.$refs.searchInput.focus(); self.$refs.navbar.inputFocus = true; }); - ipcRenderer.on("play", () => { - self.$refs.player.play(); + ipcRenderer.on('play', () => { + player.playOrPause(); }); - ipcRenderer.on("next", () => { - console.log("touchBar:next"); - self.$refs.player.next(); - }); - - ipcRenderer.on("previous", () => { - self.$refs.player.previous(); - }); - - ipcRenderer.on("increaseVolume", () => { - if (self.$refs.player.volume + 0.1 >= 1) { - return (self.$refs.player.volume = 1); + ipcRenderer.on('next', () => { + if (player.isPersonalFM) { + player.playNextFMTrack(); + } else { + player.playNextTrack(); } - self.$refs.player.volume += 0.1; }); - ipcRenderer.on("decreaseVolume", () => { - if (self.$refs.player.volume - 0.1 <= 0) { - return (self.$refs.player.volume = 0); + ipcRenderer.on('previous', () => { + player.playPrevTrack(); + }); + + ipcRenderer.on('increaseVolume', () => { + if (player.volume + 0.1 >= 1) { + return (player.volume = 1); } - self.$refs.player.volume -= 0.1; + player.volume += 0.1; }); - ipcRenderer.on("like", () => { - self.$refs.player.likeCurrentSong(); + ipcRenderer.on('decreaseVolume', () => { + if (player.volume - 0.1 <= 0) { + return (player.volume = 0); + } + player.volume -= 0.1; }); - ipcRenderer.on("repeat", () => { - self.$refs.player.repeat(); + ipcRenderer.on('like', () => { + store.dispatch('likeATrack', player.currentTrack.id); }); - ipcRenderer.on("shuffle", () => { - self.$refs.player.shuffle(); + ipcRenderer.on('repeat', () => { + player.switchRepeatMode(); }); - ipcRenderer.on("routerGo", (event, where) => { - console.log(where); + ipcRenderer.on('shuffle', () => { + player.switchShuffle(); + }); + + ipcRenderer.on('routerGo', (event, where) => { self.$refs.navbar.go(where); }); - ipcRenderer.on("nextUp", () => { + ipcRenderer.on('nextUp', () => { self.$refs.player.goToNextTracksPage(); }); + + ipcRenderer.on('rememberCloseAppOption', (event, value) => { + store.commit('updateSettings', { + key: 'closeAppOption', + value, + }); + }); + + ipcRenderer.on('setPosition', (event, position) => { + player._howler.seek(position); + }); } diff --git a/src/electron/menu.js b/src/electron/menu.js index 2258133..494f3fd 100644 --- a/src/electron/menu.js +++ b/src/electron/menu.js @@ -1,10 +1,16 @@ -const { app, Menu } = require("electron"); +import defaultShortcuts from '@/utils/shortcuts'; +const { app, Menu } = require('electron'); // import { autoUpdater } from "electron-updater" // const version = app.getVersion(); -const isMac = process.platform === "darwin"; +const isMac = process.platform === 'darwin'; + +export function createMenu(win, store) { + let shortcuts = store.get('settings.shortcuts'); + if (shortcuts === undefined) { + shortcuts = defaultShortcuts; + } -export function createMenu(win) { let menu = null; const template = [ ...(isMac @@ -12,142 +18,143 @@ export function createMenu(win) { { label: app.name, submenu: [ - { role: "about" }, - { type: "separator" }, - { role: "services" }, - { type: "separator" }, - { type: "separator" }, + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { type: 'separator' }, { - label: "Preferences...", - accelerator: (() => (isMac ? "CmdOrCtrl+," : "Ctrl+,"))(), + label: 'Preferences...', + accelerator: 'CmdOrCtrl+,', click: () => { - win.webContents.send("changeRouteTo", "/settings"); + win.webContents.send('changeRouteTo', '/settings'); }, - role: "preferences", + role: 'preferences', }, - { type: "separator" }, - { role: "hide" }, - { role: "hideothers" }, - { role: "unhide" }, - { type: "separator" }, - { role: "quit" }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideothers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' }, ], }, ] : []), { - label: "Edit", + label: 'Edit', submenu: [ - { role: "undo" }, - { role: "redo" }, - { type: "separator" }, - { role: "cut" }, - { role: "copy" }, - { role: "paste" }, + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { role: 'copy' }, + { role: 'paste' }, ...(isMac ? [ - { role: "delete" }, - { role: "selectAll" }, - { type: "separator" }, + { role: 'delete' }, + { role: 'selectAll' }, + { type: 'separator' }, { - label: "Speech", - submenu: [{ role: "startspeaking" }, { role: "stopspeaking" }], + label: 'Speech', + submenu: [{ role: 'startspeaking' }, { role: 'stopspeaking' }], }, ] - : [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]), + : [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]), { - label: "Search", - accelerator: "CmdOrCtrl+F", + label: 'Search', + accelerator: 'CmdOrCtrl+F', click: () => { - win.webContents.send("search"); + win.webContents.send('search'); }, }, ], }, { - label: "Controls", + label: 'Controls', submenu: [ { - label: "Play", + label: 'Play', + accelerator: shortcuts.find(s => s.id === 'play').shortcut, click: () => { - win.webContents.send("play"); + win.webContents.send('play'); }, }, { - label: "Next", - accelerator: "CmdOrCtrl+Right", + label: 'Next', + accelerator: shortcuts.find(s => s.id === 'next').shortcut, click: () => { - win.webContents.send("next"); + win.webContents.send('next'); }, }, { - label: "Previous", - accelerator: "CmdOrCtrl+Left", + label: 'Previous', + accelerator: shortcuts.find(s => s.id === 'previous').shortcut, click: () => { - win.webContents.send("previous"); + win.webContents.send('previous'); }, }, { - label: "Increase Volume", - accelerator: "CmdOrCtrl+Up", + label: 'Increase Volume', + accelerator: shortcuts.find(s => s.id === 'increaseVolume').shortcut, click: () => { - win.webContents.send("increaseVolume"); + win.webContents.send('increaseVolume'); }, }, { - label: "Decrease Volume", - accelerator: "CmdOrCtrl+Down", + label: 'Decrease Volume', + accelerator: shortcuts.find(s => s.id === 'decreaseVolume').shortcut, click: () => { - win.webContents.send("decreaseVolume"); + win.webContents.send('decreaseVolume'); }, }, { - label: "Like", - accelerator: "CmdOrCtrl+L", + label: 'Like', + accelerator: shortcuts.find(s => s.id === 'like').shortcut, click: () => { - win.webContents.send("like"); + win.webContents.send('like'); }, }, { - label: "Repeat", - accelerator: "Alt+R", + label: 'Repeat', + accelerator: 'Alt+R', click: () => { - win.webContents.send("repeat"); + win.webContents.send('repeat'); }, }, { - label: "Shuffle", - accelerator: "Alt+S", + label: 'Shuffle', + accelerator: 'Alt+S', click: () => { - win.webContents.send("shuffle"); + win.webContents.send('shuffle'); }, }, ], }, { - label: "Window", + label: 'Window', submenu: [ - { role: "close" }, - { role: "minimize" }, - { role: "zoom" }, - { role: "reload" }, - { role: "forcereload" }, - { role: "toggledevtools" }, - { type: "separator" }, - { role: "togglefullscreen" }, + { role: 'close' }, + { role: 'minimize' }, + { role: 'zoom' }, + { role: 'reload' }, + { role: 'forcereload' }, + { role: 'toggledevtools' }, + { type: 'separator' }, + { role: 'togglefullscreen' }, ...(isMac ? [ - { type: "separator" }, - { role: "front" }, - { type: "separator" }, + { type: 'separator' }, + { role: 'front' }, + { type: 'separator' }, { - role: "window", - id: "window", - label: "YesPlayMusic", - type: "checkbox", + role: 'window', + id: 'window', + label: 'YesPlayMusic', + type: 'checkbox', checked: true, click: () => { - const current = menu.getMenuItemById("window"); + const current = menu.getMenuItemById('window'); if (current.checked === false) { win.hide(); } else { @@ -156,29 +163,29 @@ export function createMenu(win) { }, }, ] - : [{ role: "close" }]), + : [{ role: 'close' }]), ], }, { - label: "Help", + label: 'Help', submenu: [ { - label: "Github", + label: 'GitHub', click: async () => { - const { shell } = require("electron"); - await shell.openExternal("https://github.com/qier222/YesPlayMusic"); + const { shell } = require('electron'); + await shell.openExternal('https://github.com/qier222/YesPlayMusic'); }, }, { - label: "Electron", + label: 'Electron', click: async () => { - const { shell } = require("electron"); - await shell.openExternal("https://electronjs.org"); + const { shell } = require('electron'); + await shell.openExternal('https://electronjs.org'); }, }, { - label: "开发者工具", - accelerator: "F12", + label: '开发者工具', + accelerator: 'F12', click: () => { win.webContents.openDevTools(); }, @@ -207,6 +214,7 @@ export function createMenu(win) { // ], // }); // } + menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); } diff --git a/src/electron/mpris.js b/src/electron/mpris.js new file mode 100644 index 0000000..c6cfeee --- /dev/null +++ b/src/electron/mpris.js @@ -0,0 +1,94 @@ +import dbus from 'dbus-next'; +import { ipcMain, app } from 'electron'; + +export function createMpris(window) { + const Player = require('mpris-service'); + const renderer = window.webContents; + + const player = Player({ + name: 'yesplaymusic', + identity: 'YesPlayMusic', + }); + + player.on('next', () => renderer.send('next')); + player.on('previous', () => renderer.send('previous')); + player.on('playpause', () => renderer.send('play')); + player.on('play', () => renderer.send('play')); + player.on('pause', () => renderer.send('play')); + player.on('quit', () => app.exit()); + player.on('position', args => + renderer.send('setPosition', args.position / 1000 / 1000) + ); + player.on('loopStatus', () => renderer.send('repeat')); + player.on('shuffle', () => renderer.send('shuffle')); + + ipcMain.on('player', (e, { playing }) => { + player.playbackStatus = playing + ? Player.PLAYBACK_STATUS_PLAYING + : Player.PLAYBACK_STATUS_PAUSED; + }); + + ipcMain.on('metadata', (e, metadata) => { + // 更新 Mpris 状态前将位置设为0, 否则 OSDLyrics 获取到的进度是上首音乐切换时的进度 + player.getPosition = () => 0; + player.metadata = { + 'mpris:trackid': player.objectPath('track/' + metadata.trackId), + 'mpris:artUrl': metadata.artwork[0].src, + 'mpris:length': metadata.length * 1000 * 1000, + 'xesam:title': metadata.title, + 'xesam:album': metadata.album, + 'xesam:artist': metadata.artist.split(','), + 'xesam:url': metadata.url, + }; + }); + + ipcMain.on('playerCurrentTrackTime', (e, position) => { + player.getPosition = () => position * 1000 * 1000; + player.seeked(position * 1000 * 1000); + }); + + ipcMain.on('seeked', (e, position) => { + player.seeked(position * 1000 * 1000); + }); + + ipcMain.on('switchRepeatMode', (e, mode) => { + switch (mode) { + case 'off': + player.loopStatus = Player.LOOP_STATUS_NONE; + break; + case 'one': + player.loopStatus = Player.LOOP_STATUS_TRACK; + break; + case 'on': + player.loopStatus = Player.LOOP_STATUS_PLAYLIST; + break; + } + }); + + ipcMain.on('switchShuffle', (e, shuffle) => { + player.shuffle = shuffle; + }); +} + +export async function createDbus(window) { + const bus = dbus.sessionBus(); + const Variant = dbus.Variant; + + const osdService = await bus.getProxyObject( + 'org.osdlyrics.Daemon', + '/org/osdlyrics/Lyrics' + ); + + const osdInterface = osdService.getInterface('org.osdlyrics.Lyrics'); + + ipcMain.on('sendLyrics', async (e, { track, lyrics }) => { + const metadata = { + title: new Variant('s', track.name), + artist: new Variant('s', track.ar.map(ar => ar.name).join(', ')), + }; + + await osdInterface.SetLyricContent(metadata, Buffer.from(lyrics)); + + window.webContents.send('saveLyricFinished'); + }); +} diff --git a/src/electron/services.js b/src/electron/services.js index 27f8667..3a1106f 100644 --- a/src/electron/services.js +++ b/src/electron/services.js @@ -1,58 +1,14 @@ -const express = require("express"); -const bodyParser = require("body-parser"); -const cache = require("../../netease_api/util/apicache").middleware; -const fileUpload = require("express-fileupload"); -import routes from "../../netease_api/routes"; +import clc from 'cli-color'; +import checkAuthToken from '../utils/checkAuthToken'; +import server from 'NeteaseCloudMusicApi/server'; -export function startNeteaseMusicApi() { - // Integrate API - const app = express(); +export async function startNeteaseMusicApi() { + // Let user know that the service is starting + console.log(`${clc.redBright('[NetEase API]')} initiating NCM API`); - // CORS & Preflight request - app.use((req, res, next) => { - if (req.path !== "/" && !req.path.includes(".")) { - res.set({ - "Access-Control-Allow-Credentials": true, - "Access-Control-Allow-Origin": req.headers.origin || "*", - "Access-Control-Allow-Headers": "X-Requested-With,Content-Type", - "Access-Control-Allow-Methods": "PUT,POST,GET,DELETE,OPTIONS", - "Content-Type": "application/json; charset=utf-8", - }); - } - req.method === "OPTIONS" ? res.status(204).end() : next(); - }); - - // cookie parser - app.use((req, res, next) => { - req.cookies = {}; - (req.headers.cookie || "").split(/\s*;\s*/).forEach((pair) => { - let crack = pair.indexOf("="); - if (crack < 1 || crack == pair.length - 1) return; - req.cookies[ - decodeURIComponent(pair.slice(0, crack)).trim() - ] = decodeURIComponent(pair.slice(crack + 1)).trim(); - }); - next(); - }); - - // body parser - app.use(bodyParser.json()); - app.use(bodyParser.urlencoded({ extended: false })); - - app.use(fileUpload()); - - // cache - app.use(cache("2 minutes", (req, res) => res.statusCode === 200)); - // router - - Object.keys(routes).forEach((route) => { - app.use(route, routes[route]); - }); - - const port = process.env.PORT || 10754; - const host = process.env.HOST || "127.0.0.1"; - - app.server = app.listen(port, host, () => { - console.log(`server running @ http://${host ? host : "localhost"}:${port}`); + // Load the NCM API. + await server.serveNcmApi({ + port: 10754, + moduleDefs: require('../ncmModDef'), }); } diff --git a/src/electron/touchBar.js b/src/electron/touchBar.js new file mode 100644 index 0000000..0541c93 --- /dev/null +++ b/src/electron/touchBar.js @@ -0,0 +1,97 @@ +const { TouchBar, nativeImage, ipcMain } = require('electron'); +const { TouchBarButton, TouchBarSpacer } = TouchBar; +const path = require('path'); + +export function createTouchBar(window) { + const renderer = window.webContents; + + // Icon follow touchbar design guideline. + // See: https://developer.apple.com/design/human-interface-guidelines/macos/touch-bar/touch-bar-icons-and-images/ + // Icon Resource: https://devimages-cdn.apple.com/design/resources/ + function getNativeIcon(name) { + return nativeImage.createFromPath( + // eslint-disable-next-line no-undef + path.join(__static, 'img/touchbar/', name) + ); + } + + const previousPage = new TouchBarButton({ + click: () => { + renderer.send('routerGo', 'back'); + }, + icon: getNativeIcon('page_prev.png'), + }); + + const nextPage = new TouchBarButton({ + click: () => { + renderer.send('routerGo', 'forward'); + }, + icon: getNativeIcon('page_next.png'), + }); + + const searchButton = new TouchBarButton({ + click: () => { + renderer.send('search'); + }, + icon: getNativeIcon('search.png'), + }); + + const playButton = new TouchBarButton({ + click: () => { + renderer.send('play'); + }, + icon: getNativeIcon('play.png'), + }); + + const previousTrackButton = new TouchBarButton({ + click: () => { + renderer.send('previous'); + }, + icon: getNativeIcon('backward.png'), + }); + + const nextTrackButton = new TouchBarButton({ + click: () => { + renderer.send('next'); + }, + icon: getNativeIcon('forward.png'), + }); + + const likeButton = new TouchBarButton({ + click: () => { + renderer.send('like'); + }, + icon: getNativeIcon('like.png'), + }); + + const nextUpButton = new TouchBarButton({ + click: () => { + renderer.send('nextUp'); + }, + icon: getNativeIcon('next_up.png'), + }); + + ipcMain.on('player', (e, { playing, likedCurrentTrack }) => { + playButton.icon = + playing === true ? getNativeIcon('pause.png') : getNativeIcon('play.png'); + likeButton.icon = likedCurrentTrack + ? getNativeIcon('like_fill.png') + : getNativeIcon('like.png'); + }); + + const touchBar = new TouchBar({ + items: [ + previousPage, + nextPage, + searchButton, + new TouchBarSpacer({ size: 'flexible' }), + previousTrackButton, + playButton, + nextTrackButton, + new TouchBarSpacer({ size: 'flexible' }), + likeButton, + nextUpButton, + ], + }); + return touchBar; +} diff --git a/src/electron/touchbar.js b/src/electron/touchbar.js deleted file mode 100644 index f8f791e..0000000 --- a/src/electron/touchbar.js +++ /dev/null @@ -1,85 +0,0 @@ -const { TouchBar, ipcMain } = require("electron"); -const { TouchBarButton, TouchBarSpacer } = TouchBar; - -export function createTouchBar(window) { - const renderer = window.webContents; - - // use SF Symbols as label, you probably will see a 􀂒 instead of real icon - - const previousPage = new TouchBarButton({ - label: "􀆉", - click: () => { - renderer.send("routerGo", "back"); - }, - }); - - const nextPage = new TouchBarButton({ - label: "􀆊", - click: () => { - renderer.send("routerGo", "forward"); - }, - }); - - const searchButton = new TouchBarButton({ - label: "􀊫", - click: () => { - renderer.send("search"); - }, - }); - - const playButton = new TouchBarButton({ - label: "􀊄", - click: () => { - renderer.send("play"); - }, - }); - - const previousTrackButton = new TouchBarButton({ - label: "􀊎", - click: () => { - renderer.send("previous"); - }, - }); - - const nextTrackButton = new TouchBarButton({ - label: "􀊐", - click: () => { - renderer.send("next"); - }, - }); - - const likeButton = new TouchBarButton({ - label: "􀊴", - click: () => { - renderer.send("like"); - }, - }); - - const nextUpButton = new TouchBarButton({ - label: "􀑬", - click: () => { - renderer.send("nextUp"); - }, - }); - - ipcMain.on("player", (e, { playing, likedCurrentTrack }) => { - playButton.label = playing === true ? "􀊆" : "􀊄"; - likeButton.label = likedCurrentTrack ? "􀊵" : "􀊴"; - }); - - const touchBar = new TouchBar({ - items: [ - previousPage, - nextPage, - searchButton, - new TouchBarSpacer({ size: "flexible" }), - previousTrackButton, - playButton, - nextTrackButton, - new TouchBarSpacer({ size: "flexible" }), - likeButton, - nextUpButton, - ], - }); - return touchBar; -} diff --git a/src/electron/tray.js b/src/electron/tray.js index 1b66e98..ed314f2 100644 --- a/src/electron/tray.js +++ b/src/electron/tray.js @@ -1,34 +1,216 @@ /* global __static */ -import path from "path"; -import { app, nativeImage, Tray, Menu } from "electron"; +import path from 'path'; +import { app, nativeImage, Tray, Menu, nativeTheme } from 'electron'; +import { isLinux } from '@/utils/platform'; + +function createMenuTemplate(win) { + return [ + { + label: '播放', + icon: nativeImage.createFromPath( + path.join(__static, 'img/icons/play.png') + ), + click: () => { + win.webContents.send('play'); + }, + id: 'play', + }, + { + label: '暂停', + icon: nativeImage.createFromPath( + path.join(__static, 'img/icons/pause.png') + ), + click: () => { + win.webContents.send('play'); + }, + id: 'pause', + visible: false, + }, + { + label: '上一首', + icon: nativeImage.createFromPath( + path.join(__static, 'img/icons/left.png') + ), + accelerator: 'CmdOrCtrl+Left', + click: () => { + win.webContents.send('previous'); + }, + }, + { + label: '下一首', + icon: nativeImage.createFromPath( + path.join(__static, 'img/icons/right.png') + ), + accelerator: 'CmdOrCtrl+Right', + click: () => { + win.webContents.send('next'); + }, + }, + { + label: '循环播放', + icon: nativeImage.createFromPath( + path.join(__static, 'img/icons/repeat.png') + ), + accelerator: 'Alt+R', + click: () => { + win.webContents.send('repeat'); + }, + }, + { + label: '加入喜欢', + icon: nativeImage.createFromPath( + path.join(__static, 'img/icons/like.png') + ), + accelerator: 'CmdOrCtrl+L', + click: () => { + win.webContents.send('like'); + }, + id: 'like', + }, + { + label: '取消喜欢', + icon: nativeImage.createFromPath( + path.join(__static, 'img/icons/unlike.png') + ), + accelerator: 'CmdOrCtrl+L', + click: () => { + win.webContents.send('like'); + }, + id: 'unlike', + visible: false, + }, + { + label: '退出', + icon: nativeImage.createFromPath( + path.join(__static, 'img/icons/exit.png') + ), + accelerator: 'CmdOrCtrl+W', + click: () => { + app.exit(); + }, + }, + ]; +} + +// linux下托盘的实现方式比较迷惑 +// right-click无法在linux下使用 +// click在默认行为下会弹出一个contextMenu,里面的唯一选项才会调用click事件 +// setContextMenu应该是目前唯一能在linux下使用托盘菜单api +// 但是无法区分鼠标左右键 + +// 发现openSUSE KDE环境可以区分鼠标左右键 +// 添加左键支持 +// 2022.05.17 +class YPMTrayLinuxImpl { + constructor(tray, win, emitter) { + this.tray = tray; + this.win = win; + this.emitter = emitter; + this.template = undefined; + this.initTemplate(); + this.contextMenu = Menu.buildFromTemplate(this.template); + + this.tray.setContextMenu(this.contextMenu); + this.handleEvents(); + } + + initTemplate() { + //在linux下,鼠标左右键都会呼出contextMenu + //所以此处单独为linux添加一个 显示主面板 选项 + this.template = [ + { + label: '显示主面板', + click: () => { + this.win.show(); + }, + }, + { + type: 'separator', + }, + ].concat(createMenuTemplate(this.win)); + } + + handleEvents() { + this.tray.on('click', () => { + this.win.show(); + }); + + this.emitter.on('updateTooltip', title => this.tray.setToolTip(title)); + this.emitter.on('updatePlayState', isPlaying => { + this.contextMenu.getMenuItemById('play').visible = !isPlaying; + this.contextMenu.getMenuItemById('pause').visible = isPlaying; + this.tray.setContextMenu(this.contextMenu); + }); + this.emitter.on('updateLikeState', isLiked => { + this.contextMenu.getMenuItemById('like').visible = !isLiked; + this.contextMenu.getMenuItemById('unlike').visible = isLiked; + this.tray.setContextMenu(this.contextMenu); + }); + } +} + +class YPMTrayWindowsImpl { + constructor(tray, win, emitter) { + this.tray = tray; + this.win = win; + this.emitter = emitter; + this.template = createMenuTemplate(win); + this.contextMenu = Menu.buildFromTemplate(this.template); + + this.isPlaying = false; + this.curDisplayPlaying = false; + + this.isLiked = false; + this.curDisplayLiked = false; + + this.handleEvents(); + } + + handleEvents() { + this.tray.on('click', () => { + this.win.show(); + }); + + this.tray.on('right-click', () => { + if (this.isPlaying !== this.curDisplayPlaying) { + this.curDisplayPlaying = this.isPlaying; + this.contextMenu.getMenuItemById('play').visible = !this.isPlaying; + this.contextMenu.getMenuItemById('pause').visible = this.isPlaying; + } + + if (this.isLiked !== this.curDisplayLiked) { + this.curDisplayLiked = this.isLiked; + this.contextMenu.getMenuItemById('like').visible = !this.isLiked; + this.contextMenu.getMenuItemById('unlike').visible = this.isLiked; + } + + this.tray.popUpContextMenu(this.contextMenu); + }); + + this.emitter.on('updateTooltip', title => this.tray.setToolTip(title)); + this.emitter.on( + 'updatePlayState', + isPlaying => (this.isPlaying = isPlaying) + ); + this.emitter.on('updateLikeState', isLiked => (this.isLiked = isLiked)); + } +} + +export function createTray(win, eventEmitter) { + // 感觉图标颜色应该不属于界面主题范畴,只需要跟随系统主题 + let iconTheme = nativeTheme.shouldUseDarkColors ? 'light' : 'dark'; -export function createTray(win) { let icon = nativeImage - .createFromPath(path.join(__static, "img/icons/menu@88.png")) + .createFromPath(path.join(__static, `img/icons/menu-${iconTheme}@88.png`)) .resize({ height: 20, width: 20, }); + let tray = new Tray(icon); + tray.setToolTip('YesPlayMusic'); - tray.setToolTip("YesPlayMusic"); - - tray.on("click", () => { - win.show(); - tray.destroy(); - }); - - tray.on("right-click", () => { - const contextMenu = Menu.buildFromTemplate([ - { - label: "Quit", - click: () => { - app.exit(); - }, - }, - ]); - tray.popUpContextMenu(contextMenu); - }); - - return tray; + return isLinux + ? new YPMTrayLinuxImpl(tray, win, eventEmitter) + : new YPMTrayWindowsImpl(tray, win, eventEmitter); } diff --git a/src/locale/index.js b/src/locale/index.js index 2d41179..e528213 100644 --- a/src/locale/index.js +++ b/src/locale/index.js @@ -1,18 +1,25 @@ -import Vue from "vue"; -import VueI18n from "vue-i18n"; -import store from "@/store"; +import Vue from 'vue'; +import VueClipboard from 'vue-clipboard2'; +import VueI18n from 'vue-i18n'; +import store from '@/store'; -import en from "./lang/en.js"; -import zhCN from "./lang/zh-CN.js"; +import en from './lang/en.js'; +import zhCN from './lang/zh-CN.js'; +import zhTW from './lang/zh-TW.js'; +import tr from './lang/tr.js'; +Vue.use(VueClipboard); Vue.use(VueI18n); const i18n = new VueI18n({ locale: store.state.settings.lang, messages: { en, - "zh-CN": zhCN, + 'zh-CN': zhCN, + 'zh-TW': zhTW, + tr, }, + silentTranslationWarn: true, }); export default i18n; diff --git a/src/locale/lang/en.js b/src/locale/lang/en.js index 379e693..d880e32 100644 --- a/src/locale/lang/en.js +++ b/src/locale/lang/en.js @@ -1,74 +1,92 @@ export default { common: { - play: "PLAY", - songs: "Songs", + play: 'PLAY', + songs: 'Songs', }, nav: { - home: "Home", - explore: "Explore", - library: "Library", - search: "Search", + home: 'Home', + explore: 'Explore', + library: 'Library', + search: 'Search', + github: 'GitHub Repo', }, footer: { - settings: "Settings", + settings: 'Settings', }, home: { - recommendPlaylist: "Recommended Playlists", - recommendArtist: "Recommended Artists", - newAlbum: "Latest Albums", - seeMore: "SEE MORE", - charts: "Charts", + recommendPlaylist: 'Recommended Playlists', + recommendArtist: 'Recommended Artists', + newAlbum: 'Latest Albums', + seeMore: 'SEE MORE', + charts: 'Charts', }, library: { sLibrary: "'s Library", - likedSongs: "Liked Songs", + likedSongs: 'Liked Songs', sLikedSongs: "'s Liked Songs", - playlists: "Playlists", - albums: "Albums", - artists: "Artists", - mvs: "MVs", + playlists: 'Playlists', + albums: 'Albums', + artists: 'Artists', + mvs: 'MVs', + cloudDisk: 'Cloud Disk', + newPlayList: 'New Playlist', + uploadSongs: 'Upload Songs', + playHistory: { + title: 'Play History', + week: 'Latest Week', + all: 'All Time', + }, + userProfileMenu: { + settings: 'Settings', + logout: 'Logout', + }, }, explore: { - explore: "Explore", - loadMore: "Load More", + explore: 'Explore', + loadMore: 'Load More', }, artist: { - latestRelease: "Latest Releases", - popularSongs: "Popular Songs", - showMore: "SHOW MORE", - showLess: "SHOW LESS", - EPsSingles: "EPs & Singles", - albums: "Albums", - withAlbums: "Albums", - artist: "Artist", - videos: "Music Videos", - following: "Following", - follow: "Follow", + latestRelease: 'Latest Releases', + latestMV: 'Latest MV', + popularSongs: 'Popular Songs', + showMore: 'SHOW MORE', + showLess: 'SHOW LESS', + EPsSingles: 'EPs & Singles', + albums: 'Albums', + withAlbums: 'Albums', + artist: 'Artist', + videos: 'Music Videos', + following: 'Following', + follow: 'Follow', + similarArtists: 'Similar Artists', + artistDesc: 'Artist Description', }, album: { - released: "Released", + released: 'Released', + albumDesc: 'Album Description', }, playlist: { - playlist: "Playlists", - updatedAt: "Updated at", + playlist: 'Playlists', + updatedAt: 'Updated at', + search: 'Search in playlist', }, login: { - accessToAll: "Access to all data", - loginText: "Login to Netease", - search: "Search account", - readonly: "Only access to public data", - usernameLogin: "Username Login", - searchHolder: "Your account username", + accessToAll: 'Access to all data', + loginText: 'Login to Netease', + search: 'Search account', + readonly: 'Only access to public data', + usernameLogin: 'Username Login', + searchHolder: 'Your account username', enterTip: "Press 'enter' to search", - choose: "Choose your account", - confirm: "Confirm", - countryCode: "Country code", - phone: "Phone", - email: "Email address", - password: "Password", - login: "Login", - loginWithEmail: "Login with Email", - loginWithPhone: "Login with Phone", + choose: 'Choose your account', + confirm: 'Confirm', + countryCode: 'Country code', + phone: 'Phone', + email: 'Email address', + password: 'Password', + login: 'Login', + loginWithEmail: 'Login with Email', + loginWithPhone: 'Login with Phone', notice: `YesPlayMusic promises not to save any of your account information to the cloud.
Your password will be MD5 encrypted locally and then transmitted to NetEase Music API.
YesPlayMusic is not the official website of NetEase Music, please consider carefully before entering account information. You can also go to YesPlayMusic's GitHub repository to build and use the self-hosted NetEase Music API.`, @@ -76,68 +94,166 @@ export default { YesPlayMusic promises not to save any of your account information to the cloud.
`, }, mv: { - moreVideo: "More Videos", + moreVideo: 'More Videos', }, next: { - nowPlaying: "Now Playing", - nextUp: "Next Up", + nowPlaying: 'Now Playing', + nextUp: 'Next Up', }, player: { - like: "Like", - previous: "Previous Song", - next: "Next Song", - repeat: "Repeat", - repeatTrack: "Repeat Track", - shuffle: "Shuffle", - play: "Play", - pause: "Pause", - mute: "Mute", - nextUp: "Next Up", + like: 'Like', + unlike: 'Unlike', + previous: 'Previous Song', + next: 'Next Song', + repeat: 'Repeat', + repeatTrack: 'Repeat Track', + shuffle: 'Shuffle', + reversed: 'Reversed', + play: 'Play', + pause: 'Pause', + mute: 'Mute', + nextUp: 'Next Up', + translationLyric: 'lyric (trans)', + PronunciationLyric: 'lyric (pronounce)', }, modal: { - close: "Close", + close: 'Close', }, search: { - artist: "Artists", - album: "Albums", - song: "Songs", - mv: "Music Videos", - playlist: "Playlists", - noResult: "No Results", - searchFor: "Search for", + artist: 'Artists', + album: 'Albums', + song: 'Songs', + mv: 'Music Videos', + playlist: 'Playlists', + noResult: 'No Results', + searchFor: 'Search for', }, settings: { - settings: "Settings", - logout: "LOGOUT", - language: "Languages", + settings: 'Settings', + logout: 'LOGOUT', + language: 'Languages', + lyric: 'Lyric', + others: 'Others', + customization: 'Customization', + MusicGenrePreference: { + text: 'Music Language Preference', + none: 'No preferences', + mandarin: 'Mandarin', + western: 'Europe & America', + korean: 'Korean', + japanese: 'Japanese', + }, musicQuality: { - text: "Music Quality", - low: "Low", - medium: "Medium", - high: "High", - lossless: "Lossless", + text: 'Music Quality', + low: 'Low', + medium: 'Medium', + high: 'High', + lossless: 'Lossless', }, + cacheLimit: { + text: 'Songs Cache limit', + none: 'None', + }, + lyricFontSize: { + text: 'Lyric Font Size', + small: 'Small', + medium: 'Medium', + large: 'Large (Default)', + xlarge: 'X-Large', + }, + deviceSelector: 'Audio Output Device', + permissionRequired: 'Microphone Permission Required', appearance: { - text: "Appearance", - auto: "Auto", - light: "Light", - dark: "Dark", + text: 'Appearance', + auto: 'Auto', + light: 'Light', + dark: 'Dark', + }, + automaticallyCacheSongs: 'Automatically cache songs', + clearSongsCache: 'Clear Songs Cache', + cacheCount: 'Cached {song} songs ({size})', + showLyricsTranslation: 'Show lyrics translation', + showPlaylistsByAppleMusic: 'Show playlists by Apple Music', + enableDiscordRichPresence: 'Enable Discord Rich Presence', + enableGlobalShortcut: 'Enable Global Shortcut', + showLibraryDefault: 'Show Library after App Launched', + subTitleDefault: 'Show Alias for Subtitle by default', + enableReversedMode: 'Enable Reversed Mode (Experimental)', + enableCustomTitlebar: 'Enable custom title bar (Need restart)', + showLyricsTime: 'Display current time', + lyricsBackground: { + text: 'Show Lyrics Background', + off: 'Off', + on: 'On', + dynamic: 'Dynamic (High GPU usage)', + }, + closeAppOption: { + text: 'Close App...', + ask: 'Ask', + exit: 'Exit', + minimizeToTray: 'Minimize to tray', + }, + enableOsdlyricsSupport: { + title: 'desktop lyrics support', + desc1: + 'Only takes effect under Linux. After enabled, it downloads the lyrics file to the local, and tries to launch OSDLyrics at startup.', + desc2: + 'Please ensure that you have installed OSDLyrics before turning on this.', + }, + unm: { + enable: 'Enable', + audioSource: { + title: 'Audio Sources', + }, + enableFlac: { + title: 'Enable FLAC Sources', + desc: 'To take effect, it may be required to clear the cache after enabling this function.', + }, + searchMode: { + title: 'Audio Search Mode', + fast: 'Speed Priority', + order: 'Order Priority', + }, + cookie: { + joox: 'Cookie for Joox use', + qq: 'Cookie for QQ use', + desc1: 'Click here for the configuration instruction. ', + desc2: 'Leave empty to pick up the default value', + }, + ytdl: 'The youtube-dl Executable File for YtDl', + proxy: { + title: 'Proxy Server for UNM', + desc1: + 'The proxy server to use for requesting services such as YouTube', + desc2: 'Leave empty to pick up the default value', + }, }, - automaticallyCacheSongs: "Automatically cache songs", - clearSongsCache: "Clear Songs Cache", - cacheCount: "Cached {song} songs ({size})", - showGitHubIcon: "Show GitHub icon", - showUnavailableSongInGreyStyle: "Show unavailable song in grey style", - showPlaylistsByAppleMusic: "Show playlists by Apple Music", }, contextMenu: { - play: "Play", - playNext: "Play Next", - saveToMyLikedSongs: "Save to my Liked Songs", - removeFromMyLikedSongs: "Remove from my Liked Songs", + play: 'Play', + addToQueue: 'Add to queue', + saveToMyLikedSongs: 'Save to my Liked Songs', + removeFromMyLikedSongs: 'Remove from my Liked Songs', + saveToLibrary: 'Save to library', + removeFromLibrary: 'Remove from library', + addToPlaylist: 'Add to playlist', + searchInPlaylist: 'Search in playlist', + copyUrl: 'Copy URL', + openInBrowser: 'Open in Browser', + allPlaylists: 'All Playlists', + minePlaylists: 'My Playlists', + likedPlaylists: 'Liked Playlists', + cardiacMode: 'Cardiac Mode', + copyLyric: 'Copy Lyric', + copyLyricWithTranslation: 'Copy Lyric With Translation', }, toast: { - savedToMyLikedSongs: "Saved to my Liked Songs", - removedFromMyLikedSongs: "Removed from my Liked Songs", + savedToPlaylist: 'Saved to playlist', + removedFromPlaylist: 'Removed from playlist', + savedToMyLikedSongs: 'Saved to my Liked Songs', + removedFromMyLikedSongs: 'Removed from my Liked Songs', + copied: 'Copied', + copyFailed: 'Copy failed: ', + needToLogin: 'Need to log into netease account', }, }; diff --git a/src/locale/lang/tr.js b/src/locale/lang/tr.js new file mode 100644 index 0000000..fda2939 --- /dev/null +++ b/src/locale/lang/tr.js @@ -0,0 +1,240 @@ +export default { + common: { + play: 'OYNAT', + songs: 'Müzikler', + }, + nav: { + home: 'Anasayfa', + explore: 'Keşfet', + library: 'Kitaplık', + search: 'Ara', + github: 'GitHub Repo', + }, + footer: { + settings: 'Ayarlar', + }, + home: { + recommendPlaylist: 'Önerilen Çalma Listeier', + recommendArtist: 'Önerilen Sanatçılar', + newAlbum: 'Son Çıkan Albümler', + seeMore: 'DAHA FAZLASI', + charts: 'Listeler', + }, + library: { + sLibrary: "'in Kütüphanesi", + likedSongs: 'Beğenilen Müzikler', + sLikedSongs: "'in Beğendiği Müzikler", + playlists: 'Çalma Listeleri', + albums: 'Albümler', + artists: 'Sanatçılar', + mvs: 'MVs', + cloudDisk: 'Cloud Disk', + newPlayList: 'Yeni Çalma Listesi', + uploadSongs: 'Upload Songs', + playHistory: { + title: 'Play History', + week: 'Latest Week', + all: 'All Time', + }, + userProfileMenu: { + settings: 'Ayarlar', + logout: 'Çıkış Yap', + }, + }, + explore: { + explore: 'Keşfet', + loadMore: 'Daha Fazlası', + }, + artist: { + latestRelease: 'Son Çıkanlar', + popularSongs: 'Popüler Müzikler', + showMore: 'Daha Fazlası', + showLess: 'Daha Azı', + EPsSingles: 'EPs & Singles', + albums: 'Albümler', + withAlbums: 'Albümler', + artist: 'Sanatçı', + videos: 'Müzik Videoları', + following: 'Takip Ediyor', + follow: 'Takip Et', + }, + album: { + released: 'Yayınlandı', + }, + playlist: { + playlist: 'Çalma Listeleri', + updatedAt: 'Tarihinde Güncellendş', + search: 'Çalma Listesinde Ara', + }, + login: { + accessToAll: 'Tüm verilere eriş', + loginText: "Netease'e giriş yap", + search: 'Hesap ara', + readonly: 'Sadece halka açık verilere erişir', + usernameLogin: 'Kullanıcı adı giriş', + searchHolder: 'Hesabının kullanıcı adı', + enterTip: "Aramak için 'enter'e basınız", + choose: 'Hesabını seç', + confirm: 'Onayla', + countryCode: 'Ülke kodu', + phone: 'Telefon', + email: 'Email adresi', + password: 'Şifre', + login: 'Giriş Yap', + loginWithEmail: 'Email ile giriş yap', + loginWithPhone: 'Phone ile giriş yap', + notice: `YesPlayMusic hesabınızın hiçbir bilgisini kaydetmeyeceğine dair söz veriyor
+ Şifren MD5 şifreleme ile yerel olarak şifrelenir ve daha sonra NetEase Müzik API'sine gönderilir
+ YesPlayMusic, NetEase Music'in resmi websitesi değildir, lütfen hesap bilgilerinizi girmeden önce dikkatlice düşününüz. Aynı zamanda, Kendi NetEase Musix API'nızı host etmek için YesPlayMusic'in GitHub Repo'suna gidebilirsiniz.`, + noticeElectron: `YesPlayMusic hesabınızın hiçbir bilgisini kaydetmeyeceğine dair söz veriyor
+ Şifren MD5 şifreleme ile yerel olarak şifrelenir ve daha sonra NetEase Müzik API'sine gönderilir
`, + }, + mv: { + moreVideo: 'Daha Fazla Video', + }, + next: { + nowPlaying: 'Şuan çalıyor', + nextUp: 'Sıradaki', + }, + player: { + like: 'Beğen', + unlike: 'Aksine', + previous: 'Önceki Müzik', + next: 'Sonraki Müzik', + repeat: 'Tekrarla', + repeatTrack: 'Parçayı Tekrarla', + shuffle: 'Karıştır', + play: 'Oynat', + pause: 'Durdur', + mute: 'Sesi kapat', + nextUp: 'Sıradaki', + translationLyric: 'şarkı sözleri (çeviri)', + PronunciationLyric: 'şarkı sözleri (çeviri)', + }, + modal: { + close: 'Kapat', + }, + search: { + artist: 'Sanatçılar', + album: 'Albümler', + song: 'Müzikler', + mv: 'Müzik Videoları', + playlist: 'Çalma Listeleri', + noResult: 'Sonuç Bulunamadı', + searchFor: 'Search for', + }, + settings: { + settings: 'Ayarlar', + logout: 'ÇIKIŞ YAP', + language: 'Diller', + lyric: 'Şarkı Sözleri', + others: 'Diğerleri', + customization: 'Özelleştirme', + MusicGenrePreference: { + text: 'Müzik Dili Tercihi', + none: 'Tercih yok', + mandarin: 'Çince dili', + western: 'Avrupa ve Amerika', + korean: 'Korece', + japanese: 'Japonca', + }, + musicQuality: { + text: 'Müzik Kalitesi', + low: 'Düşük', + medium: 'Orta', + high: 'Yüksek', + lossless: 'Kaliteli', + }, + cacheLimit: { + text: 'Şarkılar Önbellek sınırı', + none: 'Yok', + }, + lyricFontSize: { + text: 'Şarkı Sözleri Yazı Boyutu', + small: 'Küçük', + medium: 'Orta', + large: 'Büyük(Varsayılan)', + xlarge: 'Çok-Büyük', + }, + deviceSelector: 'Ses Çıkış Cihazı', + permissionRequired: 'Mikrofon izni gerekiyor', + appearance: { + text: 'Görünüş', + auto: 'Otomatik', + light: 'Aydınlık', + dark: 'Karanlık', + }, + automaticallyCacheSongs: 'Müzikleri otomatik çerezle', + clearSongsCache: 'Müzik çerezlerini temizle', + cacheCount: 'Çerezlenen {song} Müzikler ({size})', + showLyricsTranslation: 'Müzik sözlerinin çevirilerini göster', + showPlaylistsByAppleMusic: "Apple Music'in Çalma Listelerini Göster", + enableDiscordRichPresence: 'Discord gösterimini aktifleştir', + showLibraryDefault: 'Kitaplık Varsayılanını göster', + subTitleDefault: 'Show Alias for Subtitle by default', + enableReversedMode: 'Enable Reversed Mode (Experimental)', + enableCustomTitlebar: 'Enable custom title bar (Need restart)', + lyricsBackground: { + text: 'Şarkı Sözleri Arka Planını Göster', + off: 'kapalı', + on: 'açık', + dynamic: 'dinamik(Yüksek GPU kullanımı)', + }, + closeAppOption: { + text: 'Close App...', + ask: 'Ask', + exit: 'Exit', + minimizeToTray: 'Küçült', + }, + unm: { + enable: 'Enable', + audioSource: { + title: 'Audio Sources', + }, + enableFlac: { + title: 'Enable FLAC Sources', + desc: 'To take effect, it may be required to clear the cache after enabling this function.', + }, + searchMode: { + title: 'Audio Search Mode', + fast: 'Speed Priority', + order: 'Order Priority', + }, + cookie: { + joox: 'Cookie for Joox use', + qq: 'Cookie for QQ use', + desc1: 'Click here for the configuration instruction. ', + desc2: 'Leave empty to pick up the default value', + }, + ytdl: 'The youtube-dl Executable File for YtDl', + proxy: { + title: 'Proxy Server for UNM', + desc1: + 'The proxy server to use for requesting services such as YouTube', + desc2: 'Leave empty to pick up the default value', + }, + }, + }, + contextMenu: { + play: 'Oynat', + addToQueue: 'Sonrakini Oynat', + saveToMyLikedSongs: 'Beğendiğim Müziklere Kaydet', + removeFromMyLikedMüzikler: 'Beğendiğim Müziklerden Kaldır', + saveToLibrary: 'Save to library', + removeFromLibrary: 'Remove from library', + addToPlaylist: 'Add to playlist', + searchInPlaylist: 'Search in playlist', + copyUrl: 'Copy URL', + openInBrowser: 'Open in Browser', + allPlaylists: 'All Playlists', + minePlaylists: 'My Playlists', + likedPlaylists: 'Liked Playlists', + cardiacMode: 'Cardiac Mode', + copyLyric: 'Copy Lyric', + copyLyricWithTranslation: 'Copy Lyric With Translation', + }, + toast: { + savedToMyLikedSongs: 'Beğendiğim Müziklere Kaydet', + removedFromMyLikedSongs: 'Beğendiğim Müziklerden Kaldır', + }, +}; diff --git a/src/locale/lang/zh-CN.js b/src/locale/lang/zh-CN.js index 0d00f68..543132a 100644 --- a/src/locale/lang/zh-CN.js +++ b/src/locale/lang/zh-CN.js @@ -1,71 +1,89 @@ export default { common: { - play: "播放", - songs: "首歌", + play: '播放', + songs: '首歌', }, nav: { - home: "首页", - explore: "发现", - library: "音乐库", - search: "搜索", + home: '首页', + explore: '发现', + library: '音乐库', + search: '搜索', + github: 'GitHub 仓库', }, home: { - recommendPlaylist: "推荐歌单", - recommendArtist: "推荐艺人", - newAlbum: "新专速递", - seeMore: "查看全部", - charts: "排行榜", + recommendPlaylist: '推荐歌单', + recommendArtist: '推荐艺人', + newAlbum: '新专速递', + seeMore: '查看全部', + charts: '排行榜', }, library: { - sLibrary: "的音乐库", - likedSongs: "我喜欢的音乐", - sLikedSongs: "喜欢的音乐", - playlists: "歌单", - albums: "专辑", - artists: "艺人", - mvs: "MV", + sLibrary: '的音乐库', + likedSongs: '我喜欢的音乐', + sLikedSongs: '喜欢的音乐', + playlists: '歌单', + albums: '专辑', + artists: '艺人', + mvs: 'MV', + cloudDisk: '云盘', + newPlayList: '新建歌单', + uploadSongs: '上传歌曲', + playHistory: { + title: '听歌排行', + week: '最近一周', + all: '所有时间', + }, + userProfileMenu: { + settings: '设置', + logout: '登出', + }, }, explore: { - explore: "发现", - loadMore: "加载更多", + explore: '发现', + loadMore: '加载更多', }, artist: { - latestRelease: "最新发布", - popularSongs: "热门歌曲", - showMore: "显示更多", - showLess: "收起", - EPsSingles: "EP和单曲", - albums: "专辑", - withAlbums: "张专辑", - artist: "艺人", - videos: "个MV", - following: "已关注", - follow: "关注", + latestRelease: '最新发布', + latestMV: '最新 MV', + popularSongs: '热门歌曲', + showMore: '显示更多', + showLess: '收起', + EPsSingles: 'EP 和单曲', + albums: '专辑', + withAlbums: '张专辑', + artist: '艺人', + videos: '个 MV', + following: '正在关注', + follow: '关注', + similarArtists: '相似艺人', + artistDesc: '艺术家介绍', }, album: { - released: "发行于", + released: '发行于', + albumDesc: '专辑介绍', }, playlist: { - playlist: "歌单", - updatedAt: "最后更新于", + playlist: '歌单', + updatedAt: '最后更新于', + search: '搜索歌单音乐', }, login: { - accessToAll: "可访问全部数据", - loginText: "登录网易云账号", - search: "搜索网易云账号", - readonly: "只能读取账号公开数据", - usernameLogin: "用户名登录", - searchHolder: "请输入你的网易云用户名", - enterTip: "按 Enter 搜索", - choose: "在列表中选中你的账号", - confirm: "确认", - countryCode: "国际区号", - phone: "手机号", - email: "邮箱", - password: "密码", - login: "登录", - loginWithEmail: "使用邮箱登录", - loginWithPhone: "使用手机号登录", + accessToAll: '可访问全部数据', + loginText: '登录网易云账号', + search: '搜索网易云账号', + readonly: '只能读取账号公开数据', + usernameLogin: '用户名登录', + searchHolder: '请输入你的网易云用户名', + enterTip: '按 Enter 搜索', + choose: '在列表中选中你的账号', + confirm: '确认', + countryCode: '国际区号', + phone: '手机号', + email: '邮箱', + password: '密码', + login: '登录', + loginWithEmail: '邮箱登录', + loginWithPhone: '手机号登录', notice: `YesPlayMusic 承诺不会保存你的任何账号信息到云端。
你的密码会在本地进行 MD5 加密后再传输到网易云 API。
YesPlayMusic 并非网易云官方网站,输入账号信息前请慎重考虑。 你也可以前往 @@ -77,68 +95,164 @@ export default { YesPlayMusic 不会传输你的账号数据到任何非网易云音乐官方的服务器。
`, }, mv: { - moreVideo: "更多视频", + moreVideo: '更多视频', }, next: { - nowPlaying: "正在播放", - nextUp: "即将播放", + nowPlaying: '正在播放', + nextUp: '即将播放', }, player: { - like: "喜欢", - previous: "上一首", - next: "下一首", - repeat: "循环播放", - repeatTrack: "单曲循环", - shuffle: "随机播放", - play: "播放", - pause: "暂停", - mute: "静音", - nextUp: "播放列表", + like: '喜欢', + unlike: '取消喜欢', + previous: '上一首', + next: '下一首', + repeat: '循环播放', + repeatTrack: '单曲循环', + shuffle: '随机播放', + reversed: '倒序播放', + play: '播放', + pause: '暂停', + mute: '静音', + nextUp: '播放列表', + translationLyric: '歌词(译)', + PronunciationLyric: '歌词(音)', }, modal: { - close: "关闭", + close: '关闭', }, search: { - artist: "艺人", - album: "专辑", - song: "歌曲", - mv: "视频", - playlist: "歌单", - noResult: "暂无结果", - searchFor: "搜索", + artist: '艺人', + album: '专辑', + song: '歌曲', + mv: '视频', + playlist: '歌单', + noResult: '暂无结果', + searchFor: '搜索', }, settings: { - settings: "设置", - logout: "登出", - language: "语言", + settings: '设置', + logout: '登出', + language: '语言', + lyric: '歌词', + others: '其他', + customization: '自定义', + MusicGenrePreference: { + text: '音乐语种偏好', + none: '无偏好', + mandarin: '华语', + western: '欧美', + korean: '韩语', + japanese: '日语', + }, musicQuality: { - text: "音质选择", - low: "普通", - medium: "较高", - high: "极高", - lossless: "无损", + text: '音质选择', + low: '普通', + medium: '较高', + high: '极高', + lossless: '无损', }, + cacheLimit: { + text: '歌曲缓存上限', + none: '无限制', + }, + lyricFontSize: { + text: '歌词字体大小', + small: '小', + medium: '中', + large: '大(默认)', + xlarge: '超大', + }, + deviceSelector: '音频输出设备', + permissionRequired: '需要麦克风权限', appearance: { - text: "外观", - auto: "自动", - light: "浅色", - dark: "深色", + text: '外观', + auto: '自动', + light: '浅色', + dark: '深色', + }, + automaticallyCacheSongs: '自动缓存歌曲', + clearSongsCache: '清除歌曲缓存', + cacheCount: '已缓存 {song} 首 ({size})', + showLyricsTranslation: '显示歌词翻译', + showPlaylistsByAppleMusic: '首页显示来自 Apple Music 的歌单', + enableDiscordRichPresence: '启用 Discord Rich Presence', + enableGlobalShortcut: '启用全局快捷键', + showLibraryDefault: '启动后显示音乐库', + subTitleDefault: '副标题使用别名', + enableReversedMode: '启用倒序播放功能 (实验性功能)', + enableCustomTitlebar: '启用自定义标题栏 (重启后生效)', + lyricsBackground: { + text: '显示歌词背景', + off: '关闭', + on: '打开', + dynamic: '动态(GPU 占用较高)', + }, + showLyricsTime: '显示当前时间', + closeAppOption: { + text: '关闭主面板时...', + ask: '询问', + exit: '退出', + minimizeToTray: '最小化到托盘', + }, + enableOsdlyricsSupport: { + title: '桌面歌词支持', + desc1: + '仅 Linux 下生效。启用后会将歌词文件下载到本地,并在开启播放器时尝试拉起 OSDLyrics。', + desc2: '请在开启之前确保您已经正确安装了 OSDLyrics。', + }, + unm: { + enable: '启用', + audioSource: { + title: '备选音源', + }, + enableFlac: { + title: '启用 FLAC', + desc: '启用后需要清除歌曲缓存才能生效', + }, + searchMode: { + title: '音源搜索模式', + fast: '速度优先', + order: '顺序优先', + }, + cookie: { + joox: 'Joox 引擎的 Cookie', + qq: 'QQ 引擎的 Cookie', + desc1: '设置说明请参见此处', + desc2: ',留空则不进行相关设置', + }, + ytdl: 'YtDl 引擎要使用的 youtube-dl 可执行文件', + proxy: { + title: '用于 UNM 的代理服务器', + desc1: '请求如 YouTube 音源服务时要使用的代理服务器', + desc2: '留空则不进行相关设置', + }, }, - automaticallyCacheSongs: "自动缓存歌曲", - clearSongsCache: "清除歌曲缓存", - cacheCount: "已缓存 {song} 首 ({size})", - showGitHubIcon: "显示 GitHub 图标", - showUnavailableSongInGreyStyle: "显示不可播放的歌曲为灰色", - showPlaylistsByAppleMusic: "首页显示来自 Apple Music 的歌单", }, contextMenu: { - play: "播放", - playNext: "下一首播放", - saveToMyLikedSongs: "添加到我喜欢的音乐", - removeFromMyLikedSongs: "从喜欢的音乐中删除", + play: '播放', + addToQueue: '添加到队列', + saveToMyLikedSongs: '添加到我喜欢的音乐', + removeFromMyLikedSongs: '从喜欢的音乐中删除', + saveToLibrary: '保存到音乐库', + removeFromLibrary: '从音乐库删除', + addToPlaylist: '添加到歌单', + searchInPlaylist: '歌单内搜索', + copyUrl: '复制链接', + openInBrowser: '在浏览器中打开', + allPlaylists: '全部歌单', + minePlaylists: '创建的歌单', + likedPlaylists: '收藏的歌单', + cardiacMode: '心动模式', + copyLyric: '复制歌词', + copyLyricWithTranslation: '复制歌词(含翻译)', }, toast: { - savedToMyLikedSongs: "已添加到我喜欢的音乐", - removedFromMyLikedSongs: "已从喜欢的音乐中删除", + savedToPlaylist: '已添加到歌单', + removedFromPlaylist: '已从歌单中删除', + savedToMyLikedSongs: '已添加到我喜欢的音乐', + removedFromMyLikedSongs: '已从喜欢的音乐中删除', + copied: '已复制', + copyFailed: '复制失败:', + needToLogin: '此操作需要登录网易云帐号', }, }; diff --git a/src/locale/lang/zh-TW.js b/src/locale/lang/zh-TW.js new file mode 100644 index 0000000..384a0cf --- /dev/null +++ b/src/locale/lang/zh-TW.js @@ -0,0 +1,255 @@ +export default { + common: { + play: '播放', + songs: '首歌', + }, + nav: { + home: '首頁', + explore: '發現', + library: '音樂庫', + search: '搜尋', + github: 'GitHub Repo', + }, + home: { + recommendPlaylist: '推薦歌單', + recommendArtist: '推薦藝人', + newAlbum: '新曲上架', + seeMore: '查看全部', + charts: '排行榜', + }, + library: { + sLibrary: '的音樂庫', + likedSongs: '我喜歡的音樂', + sLikedSongs: '喜歡的音樂', + playlists: '歌單', + albums: '專輯', + artists: '藝人', + mvs: 'MV', + cloudDisk: '雲端硬碟', + newPlayList: '新增歌單', + uploadSongs: '上傳音樂', + playHistory: { + title: '聽歌排行', + week: '最近一周', + all: '所有時間', + }, + userProfileMenu: { + settings: '設定', + logout: '登出', + }, + }, + explore: { + explore: '探索', + loadMore: '載入更多', + }, + artist: { + latestRelease: '最新發佈', + popularSongs: '熱門歌曲', + showMore: '顯示更多', + showLess: '收起', + EPsSingles: 'EP 和單曲', + albums: '專輯', + withAlbums: '張專輯', + artist: '藝人', + videos: '個 MV', + following: '正在追蹤', + follow: '追蹤', + }, + album: { + released: '發行於', + }, + playlist: { + playlist: '歌單', + updatedAt: '最後更新於', + search: '搜尋歌單內音樂', + }, + login: { + accessToAll: '可存取全部資料', + loginText: '登入網易雲帳戶', + search: '搜尋網易雲帳戶', + readonly: '只能讀取帳戶公開資料', + usernameLogin: '使用者名稱登入', + searchHolder: '請輸入您的網易雲使用者名稱', + enterTip: '按 Enter 搜尋', + choose: '在選單中選擇你的帳戶', + confirm: '確認', + countryCode: '國際區碼', + phone: '手機號碼', + email: 'Email', + password: '密碼', + login: '登入', + loginWithEmail: '信箱登入', + loginWithPhone: '手機號碼登入', + notice: `YesPlayMusic 承諾不會保存您的任何帳戶資訊到雲端。
+ 您的密碼會在本地進行 MD5 加密後再傳輸到網易雲 API。
+ YesPlayMusic 並非網易雲官方網站,輸入帳戶資訊前請慎重考慮。 您也可以前往 + YesPlayMusic 的 GitHub 原始碼 Repo + 自行編譯並使用自託管的網易雲 API。`, + noticeElectron: `您的密碼會在本地進行 MD5 加密後再傳輸到網易雲 API。
+ YesPlayMusic 不會傳輸你的帳戶資料到任何非網易雲音樂官方的伺服器。
`, + }, + mv: { + moreVideo: '更多影片', + }, + next: { + nowPlaying: '正在播放', + nextUp: '即將播放', + }, + player: { + like: '喜歡', + unlike: '取消喜歡', + previous: '上一首', + next: '下一首', + repeat: '循環播放', + repeatTrack: '單曲循環', + shuffle: '隨機播放', + reversed: '倒序播放', + play: '播放', + pause: '暫停', + mute: '靜音', + nextUp: '播放清單', + translationLyric: '歌詞(譯)', + PronunciationLyric: '歌詞(音)', + }, + modal: { + close: '關閉', + }, + search: { + artist: '藝人', + album: '專輯', + song: '歌曲', + mv: '影片', + playlist: '歌單', + noResult: '暫無結果', + searchFor: '搜尋', + }, + settings: { + settings: '設定', + logout: '登出', + language: '語言', + lyric: '歌詞', + others: '其他', + customization: '自訂', + MusicGenrePreference: { + text: '音樂語種偏好', + none: '無偏好', + mandarin: '華語', + western: '歐美', + korean: '韓語', + japanese: '日語', + }, + musicQuality: { + text: '音質選擇', + low: '普通', + medium: '較高', + high: '極高', + lossless: '無損', + }, + cacheLimit: { + text: '歌曲快取上限', + none: '無限制', + }, + lyricFontSize: { + text: '歌詞字體大小', + small: '小', + medium: '中', + large: '大(預設)', + xlarge: '超大', + }, + deviceSelector: '音訊輸出裝置', + permissionRequired: '需要麥克風權限', + appearance: { + text: '外觀', + auto: '自動', + light: '淺色', + dark: '深色', + }, + automaticallyCacheSongs: '自動快取歌曲', + clearSongsCache: '清除歌曲快取', + cacheCount: '已快取 {song} 首 ({size})', + showLyricsTranslation: '顯示歌詞翻譯', + minimizeToTray: '最小化到工作列角落', + showPlaylistsByAppleMusic: '首頁顯示來自 Apple Music 的歌單', + enableDiscordRichPresence: '啟用 Discord Rich Presence', + enableGlobalShortcut: '啟用全域快捷鍵', + showLibraryDefault: '啟動後顯示音樂庫', + subTitleDefault: '副標題使用別名', + enableReversedMode: '啟用倒序播放功能 (實驗性功能)', + enableCustomTitlebar: '啟用自訂標題列(重新啟動後生效)', + showLyricsTime: '顯示目前時間', + lyricsBackground: { + text: '顯示歌詞背景', + off: '關閉', + on: '開啟', + dynamic: '動態(GPU 占用較高)', + }, + closeAppOption: { + text: '關閉主面板時...', + ask: '詢問', + exit: '退出', + minimizeToTray: '最小化到工作列角落', + }, + enableOsdlyricsSupport: { + title: '桌面歌詞支援', + desc1: + '只在 Linux 環境下生效。啟用後會將歌詞檔案下載至本機位置,並在開啟播放器時嘗試連帶啟動 OSDLyrics。', + desc2: '請在開啟之前確保您已經正確安裝了 OSDLyrics。', + }, + unm: { + enable: '啟用', + audioSource: { + title: '備選音源', + }, + enableFlac: { + title: '啟用 FLAC', + desc: '啟用後需要清除歌曲快取才能生效', + }, + searchMode: { + title: '音源搜尋模式', + fast: '速度優先', + order: '順序優先', + }, + cookie: { + joox: 'Joox 引擎的 Cookie', + qq: 'QQ 引擎的 Cookie', + desc1: '設定說明請參見此處', + desc2: ',留空則不進行相關設定', + }, + ytdl: 'YtDl 引擎要使用的 youtube-dl 執行檔', + proxy: { + title: '用於 UNM 的 Proxy 伺服器', + desc1: '請求如 YouTube 音源服務時要使用的 Proxy 伺服器', + desc2: '留空則不進行相關設定', + }, + }, + }, + contextMenu: { + play: '播放', + addToQueue: '新增至佇列', + saveToMyLikedSongs: '新增至我喜歡的音樂', + removeFromMyLikedSongs: '從喜歡的音樂中刪除', + saveToLibrary: '新增至音樂庫', + removeFromLibrary: '從音樂庫刪除', + addToPlaylist: '新增至歌單', + searchInPlaylist: '歌單內搜尋', + openInBrowser: '在瀏覽器中打開', + copyUrl: '複製連結', + allPlaylists: '全部歌單', + minePlaylists: '我建立的歌單', + likedPlaylists: '收藏的歌單', + cardiacMode: '心動模式', + copyLyric: '複製歌詞', + copyLyricWithTranslation: '複製歌詞(含翻譯)', + }, + toast: { + savedToPlaylist: '已新增至歌單', + removedFromPlaylist: '已從歌單中刪除', + savedToMyLikedSongs: '已新增至我喜歡的音樂', + removedFromMyLikedSongs: '已從喜歡的音樂中刪除', + copied: '已複製', + copyFailed: '複製失敗:', + needToLogin: '此動作需要登入網易雲帳戶', + }, +}; diff --git a/src/main.js b/src/main.js index 9a3fd57..50d73a2 100644 --- a/src/main.js +++ b/src/main.js @@ -1,48 +1,46 @@ -import Vue from "vue"; -import VueAnalytics from "vue-analytics"; -import App from "./App.vue"; -import router from "./router"; -import store from "./store"; -import i18n from "@/locale"; -import "@/assets/icons"; -import "@/utils/filters"; -import "./registerServiceWorker"; -import { dailyTask } from "@/utils/common"; +import Vue from 'vue'; +import VueGtag from 'vue-gtag'; +import App from './App.vue'; +import router from './router'; +import store from './store'; +import i18n from '@/locale'; +import '@/assets/icons'; +import '@/utils/filters'; +import './registerServiceWorker'; +import '@/assets/css/global.scss'; +import NProgress from 'nprogress'; +import '@/assets/css/nprogress.css'; -import * as Sentry from "@sentry/browser"; -import { Vue as VueIntegration } from "@sentry/integrations"; -import { Integrations } from "@sentry/tracing"; - -Vue.use(VueAnalytics, { - id: "UA-180189423-1", - router, -}); +window.resetApp = () => { + localStorage.clear(); + indexedDB.deleteDatabase('yesplaymusic'); + document.cookie.split(';').forEach(function (c) { + document.cookie = c + .replace(/^ +/, '') + .replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/'); + }); + return '已重置应用,请刷新页面(按Ctrl/Command + R)'; +}; +console.log( + '如出现问题,可尝试在本页输入 %cresetApp()%c 然后按回车重置应用。', + 'background: #eaeffd;color:#335eea;padding: 4px 6px;border-radius:3px;', + 'background:unset;color:unset;' +); +Vue.use( + VueGtag, + { + config: { id: 'G-KMJJCFZDKF' }, + }, + router +); Vue.config.productionTip = false; -if (process.env.VUE_APP_ENABLE_SENTRY === "true") { - Sentry.init({ - dsn: - "https://30aaa25152974f48971912a394ab6bc3@o436528.ingest.sentry.io/5477409", - integrations: [ - new VueIntegration({ - Vue, - tracing: true, - }), - new Integrations.BrowserTracing(), - ], - - // We recommend adjusting this value in production, or using tracesSampler - // for finer control - tracesSampleRate: 1.0, - }); -} - -dailyTask(); +NProgress.configure({ showSpinner: false, trickleSpeed: 100 }); new Vue({ i18n, store, router, - render: (h) => h(App), -}).$mount("#app"); + render: h => h(App), +}).$mount('#app'); diff --git a/src/ncmModDef.js b/src/ncmModDef.js new file mode 100644 index 0000000..043ce28 --- /dev/null +++ b/src/ncmModDef.js @@ -0,0 +1,617 @@ +module.exports = [ + { + identifier: 'user_update', + route: '/user/update', + module: require('NeteaseCloudMusicApi/module/user_update'), + }, + { + identifier: 'user_subcount', + route: '/user/subcount', + module: require('NeteaseCloudMusicApi/module/user_subcount'), + }, + { + identifier: 'user_replacephone', + route: '/user/replacephone', + module: require('NeteaseCloudMusicApi/module/user_replacephone'), + }, + { + identifier: 'user_record', + route: '/user/record', + module: require('NeteaseCloudMusicApi/module/user_record'), + }, + { + identifier: 'user_playlist', + route: '/user/playlist', + module: require('NeteaseCloudMusicApi/module/user_playlist'), + }, + { + identifier: 'user_level', + route: '/user/level', + module: require('NeteaseCloudMusicApi/module/user_level'), + }, + { + identifier: 'user_follows', + route: '/user/follows', + module: require('NeteaseCloudMusicApi/module/user_follows'), + }, + { + identifier: 'user_followeds', + route: '/user/followeds', + module: require('NeteaseCloudMusicApi/module/user_followeds'), + }, + { + identifier: 'user_event', + route: '/user/event', + module: require('NeteaseCloudMusicApi/module/user_event'), + }, + { + identifier: 'user_dj', + route: '/user/dj', + module: require('NeteaseCloudMusicApi/module/user_dj'), + }, + { + identifier: 'user_detail', + route: '/user/detail', + module: require('NeteaseCloudMusicApi/module/user_detail'), + }, + { + identifier: 'user_cloud_detail', + route: '/user/cloud/detail', + module: require('NeteaseCloudMusicApi/module/user_cloud_detail'), + }, + { + identifier: 'user_cloud_del', + route: '/user/cloud/del', + module: require('NeteaseCloudMusicApi/module/user_cloud_del'), + }, + { + identifier: 'user_cloud', + route: '/user/cloud', + module: require('NeteaseCloudMusicApi/module/user_cloud'), + }, + { + identifier: 'user_bindingcellphone', + route: '/user/bindingcellphone', + module: require('NeteaseCloudMusicApi/module/user_bindingcellphone'), + }, + { + identifier: 'user_binding', + route: '/user/binding', + module: require('NeteaseCloudMusicApi/module/user_binding'), + }, + { + identifier: 'user_audio', + route: '/user/audio', + module: require('NeteaseCloudMusicApi/module/user_audio'), + }, + { + identifier: 'user_account', + route: '/user/account', + module: require('NeteaseCloudMusicApi/module/user_account'), + }, + { + identifier: 'toplist_detail', + route: '/toplist/detail', + module: require('NeteaseCloudMusicApi/module/toplist_detail'), + }, + { + identifier: 'toplist_artist', + route: '/toplist/artist', + module: require('NeteaseCloudMusicApi/module/toplist_artist'), + }, + { + identifier: 'toplist', + route: '/toplist', + module: require('NeteaseCloudMusicApi/module/toplist'), + }, + { + identifier: 'topic_sublist', + route: '/topic/sublist', + module: require('NeteaseCloudMusicApi/module/topic_sublist'), + }, + { + identifier: 'topic_detail_event_hot', + route: '/topic/detail/event/hot', + module: require('NeteaseCloudMusicApi/module/topic_detail_event_hot'), + }, + { + identifier: 'topic_detail', + route: '/topic/detail', + module: require('NeteaseCloudMusicApi/module/topic_detail'), + }, + { + identifier: 'top_song', + route: '/top/song', + module: require('NeteaseCloudMusicApi/module/top_song'), + }, + { + identifier: 'top_playlist_highquality', + route: '/top/playlist/highquality', + module: require('NeteaseCloudMusicApi/module/top_playlist_highquality'), + }, + { + identifier: 'top_playlist', + route: '/top/playlist', + module: require('NeteaseCloudMusicApi/module/top_playlist'), + }, + { + identifier: 'top_mv', + route: '/top/mv', + module: require('NeteaseCloudMusicApi/module/top_mv'), + }, + { + identifier: 'top_list', + route: '/top/list', + module: require('NeteaseCloudMusicApi/module/top_list'), + }, + { + identifier: 'top_artists', + route: '/top/artists', + module: require('NeteaseCloudMusicApi/module/top_artists'), + }, + { + identifier: 'top_album', + route: '/top/album', + module: require('NeteaseCloudMusicApi/module/top_album'), + }, + { + identifier: 'song_url', + route: '/song/url', + module: require('NeteaseCloudMusicApi/module/song_url'), + }, + { + identifier: 'song_download_url', + route: '/song/download/url', + module: require('NeteaseCloudMusicApi/module/song_download_url'), + }, + { + identifier: 'song_detail', + route: '/song/detail', + module: require('NeteaseCloudMusicApi/module/song_detail'), + }, + { + identifier: 'simi_mv', + route: '/simi/mv', + module: require('NeteaseCloudMusicApi/module/simi_mv'), + }, + { + identifier: 'simi_artist', + route: '/simi/artist', + module: require('NeteaseCloudMusicApi/module/simi_artist'), + }, + { + identifier: 'search', + route: '/search', + module: require('NeteaseCloudMusicApi/module/search'), + }, + { + identifier: 'scrobble', + route: '/scrobble', + module: require('NeteaseCloudMusicApi/module/scrobble'), + }, + { + identifier: 'recommend_songs', + route: '/recommend/songs', + module: require('NeteaseCloudMusicApi/module/recommend_songs'), + }, + { + identifier: 'recommend_resource', + route: '/recommend/resource', + module: require('NeteaseCloudMusicApi/module/recommend_resource'), + }, + { + identifier: 'playmode_intelligence_list', + route: '/playmode/intelligence/list', + module: require('NeteaseCloudMusicApi/module/playmode_intelligence_list'), + }, + { + identifier: 'playlist_video_recent', + route: '/playlist/video/recent', + module: require('NeteaseCloudMusicApi/module/playlist_video_recent'), + }, + { + identifier: 'playlist_update', + route: '/playlist/update', + module: require('NeteaseCloudMusicApi/module/playlist_update'), + }, + { + identifier: 'playlist_tracks', + route: '/playlist/tracks', + module: require('NeteaseCloudMusicApi/module/playlist_tracks'), + }, + { + identifier: 'playlist_track_delete', + route: '/playlist/track/delete', + module: require('NeteaseCloudMusicApi/module/playlist_track_delete'), + }, + { + identifier: 'playlist_track_all', + route: '/playlist/track/all', + module: require('NeteaseCloudMusicApi/module/playlist_track_all'), + }, + { + identifier: 'playlist_track_add', + route: '/playlist/track/add', + module: require('NeteaseCloudMusicApi/module/playlist_track_add'), + }, + { + identifier: 'playlist_tags_update', + route: '/playlist/tags/update', + module: require('NeteaseCloudMusicApi/module/playlist_tags_update'), + }, + { + identifier: 'playlist_subscribers', + route: '/playlist/subscribers', + module: require('NeteaseCloudMusicApi/module/playlist_subscribers'), + }, + { + identifier: 'playlist_subscribe', + route: '/playlist/subscribe', + module: require('NeteaseCloudMusicApi/module/playlist_subscribe'), + }, + { + identifier: 'playlist_privacy', + route: '/playlist/privacy', + module: require('NeteaseCloudMusicApi/module/playlist_privacy'), + }, + { + identifier: 'playlist_order_update', + route: '/playlist/order/update', + module: require('NeteaseCloudMusicApi/module/playlist_order_update'), + }, + { + identifier: 'playlist_name_update', + route: '/playlist/name/update', + module: require('NeteaseCloudMusicApi/module/playlist_name_update'), + }, + { + identifier: 'playlist_mylike', + route: '/playlist/mylike', + module: require('NeteaseCloudMusicApi/module/playlist_mylike'), + }, + { + identifier: 'playlist_hot', + route: '/playlist/hot', + module: require('NeteaseCloudMusicApi/module/playlist_hot'), + }, + { + identifier: 'playlist_highquality_tags', + route: '/playlist/highquality/tags', + module: require('NeteaseCloudMusicApi/module/playlist_highquality_tags'), + }, + { + identifier: 'playlist_detail_dynamic', + route: '/playlist/detail/dynamic', + module: require('NeteaseCloudMusicApi/module/playlist_detail_dynamic'), + }, + { + identifier: 'playlist_detail', + route: '/playlist/detail', + module: require('NeteaseCloudMusicApi/module/playlist_detail'), + }, + { + identifier: 'playlist_desc_update', + route: '/playlist/desc/update', + module: require('NeteaseCloudMusicApi/module/playlist_desc_update'), + }, + { + identifier: 'playlist_delete', + route: '/playlist/delete', + module: require('NeteaseCloudMusicApi/module/playlist_delete'), + }, + { + identifier: 'playlist_create', + route: '/playlist/create', + module: require('NeteaseCloudMusicApi/module/playlist_create'), + }, + { + identifier: 'playlist_cover_update', + route: '/playlist/cover/update', + module: require('NeteaseCloudMusicApi/module/playlist_cover_update'), + }, + { + identifier: 'playlist_catlist', + route: '/playlist/catlist', + module: require('NeteaseCloudMusicApi/module/playlist_catlist'), + }, + { + identifier: 'personalized', + route: '/personalized', + module: require('NeteaseCloudMusicApi/module/personalized'), + }, + { + identifier: 'personal_fm', + route: '/personal_fm', + module: require('NeteaseCloudMusicApi/module/personal_fm'), + }, + { + identifier: 'mv_url', + route: '/mv/url', + module: require('NeteaseCloudMusicApi/module/mv_url'), + }, + { + identifier: 'mv_sublist', + route: '/mv/sublist', + module: require('NeteaseCloudMusicApi/module/mv_sublist'), + }, + { + identifier: 'mv_sub', + route: '/mv/sub', + module: require('NeteaseCloudMusicApi/module/mv_sub'), + }, + { + identifier: 'mv_first', + route: '/mv/first', + module: require('NeteaseCloudMusicApi/module/mv_first'), + }, + { + identifier: 'mv_exclusive_rcmd', + route: '/mv/exclusive/rcmd', + module: require('NeteaseCloudMusicApi/module/mv_exclusive_rcmd'), + }, + { + identifier: 'mv_detail_info', + route: '/mv/detail/info', + module: require('NeteaseCloudMusicApi/module/mv_detail_info'), + }, + { + identifier: 'mv_detail', + route: '/mv/detail', + module: require('NeteaseCloudMusicApi/module/mv_detail'), + }, + { + identifier: 'mv_all', + route: '/mv/all', + module: require('NeteaseCloudMusicApi/module/mv_all'), + }, + { + identifier: 'lyric', + route: '/lyric', + module: require('NeteaseCloudMusicApi/module/lyric'), + }, + { + identifier: 'logout', + route: '/logout', + module: require('NeteaseCloudMusicApi/module/logout'), + }, + { + identifier: 'login_status', + route: '/login/status', + module: require('NeteaseCloudMusicApi/module/login_status'), + }, + { + identifier: 'login_refresh', + route: '/login/refresh', + module: require('NeteaseCloudMusicApi/module/login_refresh'), + }, + { + identifier: 'login_qr_key', + route: '/login/qr/key', + module: require('NeteaseCloudMusicApi/module/login_qr_key'), + }, + { + identifier: 'login_qr_create', + route: '/login/qr/create', + module: require('NeteaseCloudMusicApi/module/login_qr_create'), + }, + { + identifier: 'login_qr_check', + route: '/login/qr/check', + module: require('NeteaseCloudMusicApi/module/login_qr_check'), + }, + { + identifier: 'login_cellphone', + route: '/login/cellphone', + module: require('NeteaseCloudMusicApi/module/login_cellphone'), + }, + { + identifier: 'login', + route: '/login', + module: require('NeteaseCloudMusicApi/module/login'), + }, + { + identifier: 'likelist', + route: '/likelist', + module: require('NeteaseCloudMusicApi/module/likelist'), + }, + { + identifier: 'like', + route: '/like', + module: require('NeteaseCloudMusicApi/module/like'), + }, + { + identifier: 'follow', + route: '/follow', + module: require('NeteaseCloudMusicApi/module/follow'), + }, + { + identifier: 'fm_trash', + route: '/fm_trash', + module: require('NeteaseCloudMusicApi/module/fm_trash'), + }, + { + identifier: 'daily_signin', + route: '/daily_signin', + module: require('NeteaseCloudMusicApi/module/daily_signin'), + }, + { + identifier: 'cloudsearch', + route: '/cloudsearch', + module: require('NeteaseCloudMusicApi/module/cloudsearch'), + }, + { + identifier: 'cloud', + route: '/cloud', + module: require('NeteaseCloudMusicApi/module/cloud'), + }, + { + identifier: 'check_music', + route: '/check/music', + module: require('NeteaseCloudMusicApi/module/check_music'), + }, + { + identifier: 'cellphone_existence_check', + route: '/cellphone/existence/check', + module: require('NeteaseCloudMusicApi/module/cellphone_existence_check'), + }, + { + identifier: 'captcha_verify', + route: '/captcha/verify', + module: require('NeteaseCloudMusicApi/module/captcha_verify'), + }, + { + identifier: 'captcha_sent', + route: '/captcha/sent', + module: require('NeteaseCloudMusicApi/module/captcha_sent'), + }, + { + identifier: 'calendar', + route: '/calendar', + module: require('NeteaseCloudMusicApi/module/calendar'), + }, + { + identifier: 'batch', + route: '/batch', + module: require('NeteaseCloudMusicApi/module/batch'), + }, + { + identifier: 'banner', + route: '/banner', + module: require('NeteaseCloudMusicApi/module/banner'), + }, + { + identifier: 'avatar_upload', + route: '/avatar/upload', + module: require('NeteaseCloudMusicApi/module/avatar_upload'), + }, + { + identifier: 'audio_match', + route: '/audio/match', + module: require('NeteaseCloudMusicApi/module/audio_match'), + }, + { + identifier: 'artists', + route: '/artists', + module: require('NeteaseCloudMusicApi/module/artists'), + }, + { + identifier: 'artist_video', + route: '/artist/video', + module: require('NeteaseCloudMusicApi/module/artist_video'), + }, + { + identifier: 'artist_top_song', + route: '/artist/top/song', + module: require('NeteaseCloudMusicApi/module/artist_top_song'), + }, + { + identifier: 'artist_sublist', + route: '/artist/sublist', + module: require('NeteaseCloudMusicApi/module/artist_sublist'), + }, + { + identifier: 'artist_sub', + route: '/artist/sub', + module: require('NeteaseCloudMusicApi/module/artist_sub'), + }, + { + identifier: 'artist_songs', + route: '/artist/songs', + module: require('NeteaseCloudMusicApi/module/artist_songs'), + }, + { + identifier: 'artist_new_song', + route: '/artist/new/song', + module: require('NeteaseCloudMusicApi/module/artist_new_song'), + }, + { + identifier: 'artist_new_mv', + route: '/artist/new/mv', + module: require('NeteaseCloudMusicApi/module/artist_new_mv'), + }, + { + identifier: 'artist_mv', + route: '/artist/mv', + module: require('NeteaseCloudMusicApi/module/artist_mv'), + }, + { + identifier: 'artist_list', + route: '/artist/list', + module: require('NeteaseCloudMusicApi/module/artist_list'), + }, + { + identifier: 'artist_fans', + route: '/artist/fans', + module: require('NeteaseCloudMusicApi/module/artist_fans'), + }, + { + identifier: 'artist_detail', + route: '/artist/detail', + module: require('NeteaseCloudMusicApi/module/artist_detail'), + }, + { + identifier: 'artist_desc', + route: '/artist/desc', + module: require('NeteaseCloudMusicApi/module/artist_desc'), + }, + { + identifier: 'artist_album', + route: '/artist/album', + module: require('NeteaseCloudMusicApi/module/artist_album'), + }, + { + identifier: 'album_sublist', + route: '/album/sublist', + module: require('NeteaseCloudMusicApi/module/album_sublist'), + }, + { + identifier: 'album_sub', + route: '/album/sub', + module: require('NeteaseCloudMusicApi/module/album_sub'), + }, + { + identifier: 'album_songsaleboard', + route: '/album/songsaleboard', + module: require('NeteaseCloudMusicApi/module/album_songsaleboard'), + }, + { + identifier: 'album_newest', + route: '/album/newest', + module: require('NeteaseCloudMusicApi/module/album_newest'), + }, + { + identifier: 'album_new', + route: '/album/new', + module: require('NeteaseCloudMusicApi/module/album_new'), + }, + { + identifier: 'album_list_style', + route: '/album/list/style', + module: require('NeteaseCloudMusicApi/module/album_list_style'), + }, + { + identifier: 'album_list', + route: '/album/list', + module: require('NeteaseCloudMusicApi/module/album_list'), + }, + { + identifier: 'album_detail_dynamic', + route: '/album/detail/dynamic', + module: require('NeteaseCloudMusicApi/module/album_detail_dynamic'), + }, + { + identifier: 'album_detail', + route: '/album/detail', + module: require('NeteaseCloudMusicApi/module/album_detail'), + }, + { + identifier: 'album', + route: '/album', + module: require('NeteaseCloudMusicApi/module/album'), + }, + { + identifier: 'activate_init_profile', + route: '/activate/init/profile', + module: require('NeteaseCloudMusicApi/module/activate_init_profile'), + }, +]; diff --git a/src/registerServiceWorker.js b/src/registerServiceWorker.js index 8c6d0d3..dc27e7c 100644 --- a/src/registerServiceWorker.js +++ b/src/registerServiceWorker.js @@ -1,37 +1,34 @@ /* eslint-disable no-console */ -import { register } from "register-service-worker"; +import { register } from 'register-service-worker'; -if ( - process.env.NODE_ENV === "production" && - process.env.IS_ELECTRON === "undefined" -) { +if (!process.env.IS_ELECTRON) { register(`${process.env.BASE_URL}service-worker.js`, { ready() { - console.log( - "App is being served from cache by a service worker.\n" + - "For more details, visit https://goo.gl/AFskqB" - ); + // console.log( + // "App is being served from cache by a service worker.\n" + + // "For more details, visit https://goo.gl/AFskqB" + // ); }, registered() { - console.log("Service worker has been registered."); + // console.log("Service worker has been registered."); }, cached() { - console.log("Content has been cached for offline use."); + // console.log("Content has been cached for offline use."); }, updatefound() { - console.log("New content is downloading."); + // console.log("New content is downloading."); }, updated() { - console.log("New content is available; please refresh."); + // console.log("New content is available; please refresh."); }, offline() { - console.log( - "No internet connection found. App is running in offline mode." - ); + // console.log( + // "No internet connection found. App is running in offline mode." + // ); }, error(error) { - console.error("Error during service worker registration:", error); + console.error('Error during service worker registration:', error); }, }); } diff --git a/src/router/index.js b/src/router/index.js index 1dab2f4..d38fa2d 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,150 +1,166 @@ -import Vue from "vue"; -import VueRouter from "vue-router"; -import NProgress from "nprogress"; -import "@/assets/css/nprogress.css"; -import { isLooseLoggedIn } from "@/utils/auth"; - -NProgress.configure({ showSpinner: false, trickleSpeed: 100 }); +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import { isLooseLoggedIn, isAccountLoggedIn } from '@/utils/auth'; Vue.use(VueRouter); const routes = [ { - path: "/", - name: "home", - component: () => import("@/views/home.vue"), + path: '/', + name: 'home', + component: () => import('@/views/home.vue'), + meta: { + keepAlive: true, + savePosition: true, + }, + }, + { + path: '/login', + name: 'login', + component: () => import('@/views/login.vue'), + }, + { + path: '/login/username', + name: 'loginUsername', + component: () => import('@/views/loginUsername.vue'), + }, + { + path: '/login/account', + name: 'loginAccount', + component: () => import('@/views/loginAccount.vue'), + }, + { + path: '/playlist/:id', + name: 'playlist', + component: () => import('@/views/playlist.vue'), + }, + { + path: '/album/:id', + name: 'album', + component: () => import('@/views/album.vue'), + }, + { + path: '/artist/:id', + name: 'artist', + component: () => import('@/views/artist.vue'), + meta: { + keepAlive: true, + savePosition: true, + }, + }, + { + path: '/artist/:id/mv', + name: 'artistMV', + component: () => import('@/views/artistMV.vue'), meta: { keepAlive: true, }, }, { - path: "/login", - name: "login", - component: () => import("@/views/login.vue"), + path: '/mv/:id', + name: 'mv', + component: () => import('@/views/mv.vue'), }, { - path: "/login/username", - name: "loginUsername", - component: () => import("@/views/loginUsername.vue"), + path: '/next', + name: 'next', + component: () => import('@/views/next.vue'), + meta: { + keepAlive: true, + savePosition: true, + }, }, { - path: "/login/account", - name: "loginAccount", - component: () => import("@/views/loginAccount.vue"), - }, - { - path: "/playlist/:id", - name: "playlist", - component: () => import("@/views/playlist.vue"), - }, - { - path: "/album/:id", - name: "album", - component: () => import("@/views/album.vue"), - }, - { - path: "/artist/:id", - name: "artist", - component: () => import("@/views/artist.vue"), + path: '/search/:keywords?', + name: 'search', + component: () => import('@/views/search.vue'), meta: { keepAlive: true, }, }, { - path: "/artist/:id/mv", - name: "artistMV", - component: () => import("@/views/artistMV.vue"), + path: '/search/:keywords/:type', + name: 'searchType', + component: () => import('@/views/searchType.vue'), + }, + { + path: '/new-album', + name: 'newAlbum', + component: () => import('@/views/newAlbum.vue'), + }, + { + path: '/explore', + name: 'explore', + component: () => import('@/views/explore.vue'), meta: { keepAlive: true, + savePosition: true, }, }, { - path: "/mv/:id", - name: "mv", - component: () => import("@/views/mv.vue"), - }, - { - path: "/next", - name: "next", - component: () => import("@/views/next.vue"), - meta: { - keepAlive: true, - }, - }, - { - path: "/search/:keywords?", - name: "search", - component: () => import("@/views/search.vue"), - meta: { - keepAlive: true, - }, - }, - { - path: "/search/:keywords/:type", - name: "searchType", - component: () => import("@/views/searchType.vue"), - }, - { - path: "/new-album", - name: "newAlbum", - component: () => import("@/views/newAlbum.vue"), - }, - { - path: "/explore", - name: "explore", - component: () => import("@/views/explore.vue"), - meta: { - keepAlive: true, - }, - }, - { - path: "/library", - name: "library", - component: () => import("@/views/library.vue"), + path: '/library', + name: 'library', + component: () => import('@/views/library.vue'), meta: { requireLogin: true, keepAlive: true, + savePosition: true, }, }, { - path: "/library/liked-songs", - name: "likedSongs", - component: () => import("@/views/playlist.vue"), + path: '/library/liked-songs', + name: 'likedSongs', + component: () => import('@/views/playlist.vue'), meta: { requireLogin: true, }, }, { - path: "/settings", - name: "settings", - component: () => import("@/views/settings.vue"), + path: '/settings', + name: 'settings', + component: () => import('@/views/settings.vue'), + }, + { + path: '/daily/songs', + name: 'dailySongs', + component: () => import('@/views/dailyTracks.vue'), + meta: { + requireAccountLogin: true, + }, + }, + { + path: '/lastfm/callback', + name: 'lastfmCallback', + component: () => import('@/views/lastfmCallback.vue'), }, ]; + const router = new VueRouter({ + mode: process.env.IS_ELECTRON ? 'hash' : 'history', routes, - scrollBehavior(to, from, savedPosition) { - if (savedPosition) { - return savedPosition; - } else { - return { x: 0, y: 0 }; - } - }, }); const originalPush = VueRouter.prototype.push; VueRouter.prototype.push = function push(location) { - return originalPush.call(this, location).catch((err) => err); + return originalPush.call(this, location).catch(err => err); }; router.beforeEach((to, from, next) => { // 需要登录的逻辑 + if (to.meta.requireAccountLogin) { + if (isAccountLoggedIn()) { + next(); + } else { + next({ path: '/login/account' }); + } + } if (to.meta.requireLogin) { if (isLooseLoggedIn()) { next(); } else { if (process.env.IS_ELECTRON === true) { - next({ path: "/login/account" }); + next({ path: '/login/account' }); } else { - next({ path: "/login" }); + next({ path: '/login' }); } } } else { @@ -152,13 +168,4 @@ router.beforeEach((to, from, next) => { } }); -router.afterEach((to) => { - if ( - to.matched.some((record) => !record.meta.keepAlive) && - !["settings"].includes(to.name) - ) { - NProgress.start(); - } -}); - export default router; diff --git a/src/store/actions.js b/src/store/actions.js index 83c27d3..83c0666 100644 --- a/src/store/actions.js +++ b/src/store/actions.js @@ -1,14 +1,30 @@ +// import store, { state, dispatch, commit } from "@/store"; +import { isAccountLoggedIn, isLooseLoggedIn } from '@/utils/auth'; +import { likeATrack } from '@/api/track'; +import { getPlaylistDetail } from '@/api/playlist'; +import { getTrackDetail } from '@/api/track'; +import { + userPlaylist, + userPlayHistory, + userLikedSongsIDs, + likedAlbums, + likedArtists, + likedMVs, + cloudDisk, + userAccount, +} from '@/api/user'; + export default { showToast({ state, commit }, text) { if (state.toast.timer !== null) { clearTimeout(state.toast.timer); - commit("updateToast", { show: false, text: "", timer: null }); + commit('updateToast', { show: false, text: '', timer: null }); } - commit("updateToast", { + commit('updateToast', { show: true, text, timer: setTimeout(() => { - commit("updateToast", { + commit('updateToast', { show: false, text: state.toast.text, timer: null, @@ -16,4 +32,170 @@ export default { }, 3200), }); }, + likeATrack({ state, commit, dispatch }, id) { + if (!isAccountLoggedIn()) { + dispatch('showToast', '此操作需要登录网易云账号'); + return; + } + let like = true; + if (state.liked.songs.includes(id)) like = false; + likeATrack({ id, like }) + .then(() => { + if (like === false) { + commit('updateLikedXXX', { + name: 'songs', + data: state.liked.songs.filter(d => d !== id), + }); + } else { + let newLikeSongs = state.liked.songs; + newLikeSongs.push(id); + commit('updateLikedXXX', { + name: 'songs', + data: newLikeSongs, + }); + } + dispatch('fetchLikedSongsWithDetails'); + }) + .catch(() => { + dispatch('showToast', '操作失败,专辑下架或版权锁定'); + }); + }, + fetchLikedSongs: ({ state, commit }) => { + if (!isLooseLoggedIn()) return; + if (isAccountLoggedIn()) { + return userLikedSongsIDs({ uid: state.data.user.userId }).then(result => { + if (result.ids) { + commit('updateLikedXXX', { + name: 'songs', + data: result.ids, + }); + } + }); + } else { + // TODO:搜索ID登录的用户 + } + }, + fetchLikedSongsWithDetails: ({ state, commit }) => { + return getPlaylistDetail(state.data.likedSongPlaylistID, true).then( + result => { + if (result.playlist?.trackIds?.length === 0) { + return new Promise(resolve => { + resolve(); + }); + } + return getTrackDetail( + result.playlist.trackIds + .slice(0, 12) + .map(t => t.id) + .join(',') + ).then(result => { + commit('updateLikedXXX', { + name: 'songsWithDetails', + data: result.songs, + }); + }); + } + ); + }, + fetchLikedPlaylist: ({ state, commit }) => { + if (!isLooseLoggedIn()) return; + if (isAccountLoggedIn()) { + return userPlaylist({ + uid: state.data.user?.userId, + limit: 2000, // 最多只加载2000个歌单(等有用户反馈问题再修) + timestamp: new Date().getTime(), + }).then(result => { + if (result.playlist) { + commit('updateLikedXXX', { + name: 'playlists', + data: result.playlist, + }); + // 更新用户”喜欢的歌曲“歌单ID + commit('updateData', { + key: 'likedSongPlaylistID', + value: result.playlist[0].id, + }); + } + }); + } else { + // TODO:搜索ID登录的用户 + } + }, + fetchLikedAlbums: ({ commit }) => { + if (!isAccountLoggedIn()) return; + return likedAlbums({ limit: 2000 }).then(result => { + if (result.data) { + commit('updateLikedXXX', { + name: 'albums', + data: result.data, + }); + } + }); + }, + fetchLikedArtists: ({ commit }) => { + if (!isAccountLoggedIn()) return; + return likedArtists({ limit: 2000 }).then(result => { + if (result.data) { + commit('updateLikedXXX', { + name: 'artists', + data: result.data, + }); + } + }); + }, + fetchLikedMVs: ({ commit }) => { + if (!isAccountLoggedIn()) return; + return likedMVs({ limit: 1000 }).then(result => { + if (result.data) { + commit('updateLikedXXX', { + name: 'mvs', + data: result.data, + }); + } + }); + }, + fetchCloudDisk: ({ commit }) => { + if (!isAccountLoggedIn()) return; + // FIXME: #1242 + return cloudDisk({ limit: 1000 }).then(result => { + if (result.data) { + commit('updateLikedXXX', { + name: 'cloudDisk', + data: result.data, + }); + } + }); + }, + fetchPlayHistory: ({ state, commit }) => { + if (!isAccountLoggedIn()) return; + return Promise.all([ + userPlayHistory({ uid: state.data.user?.userId, type: 0 }), + userPlayHistory({ uid: state.data.user?.userId, type: 1 }), + ]).then(result => { + const data = {}; + const dataType = { 0: 'allData', 1: 'weekData' }; + if (result[0] && result[1]) { + for (let i = 0; i < result.length; i++) { + const songData = result[i][dataType[i]].map(item => { + const song = item.song; + song.playCount = item.playCount; + return song; + }); + data[[dataType[i]]] = songData; + } + commit('updateLikedXXX', { + name: 'playHistory', + data: data, + }); + } + }); + }, + fetchUserProfile: ({ commit }) => { + if (!isAccountLoggedIn()) return; + return userAccount().then(result => { + if (result.code === 200) { + commit('updateData', { key: 'user', value: result.profile }); + } + }); + }, }; diff --git a/src/store/index.js b/src/store/index.js index 4562017..d4804d6 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -1,13 +1,13 @@ -import Vue from "vue"; -import Vuex from "vuex"; -import state from "./state"; -import mutations from "./mutations"; -import actions from "./actions"; -import { changeAppearance } from "@/utils/common"; -import Player from "@/utils/Player"; +import Vue from 'vue'; +import Vuex from 'vuex'; +import state from './state'; +import mutations from './mutations'; +import actions from './actions'; +import { changeAppearance } from '@/utils/common'; +import Player from '@/utils/Player'; // vuex 自定义插件 -import saveToLocalStorage from "./plugins/localStorage"; -import { getSendSettingsPlugin } from "./plugins/sendSettings"; +import saveToLocalStorage from './plugins/localStorage'; +import { getSendSettingsPlugin } from './plugins/sendSettings'; Vue.use(Vuex); @@ -26,18 +26,27 @@ const options = { const store = new Vuex.Store(options); if ([undefined, null].includes(store.state.settings.lang)) { - let lang = "en"; - if (navigator.language.slice(0, 2) === "zh") lang = "zh-CN"; - store.state.settings.lang = lang; - localStorage.setItem("settings", JSON.stringify(store.state.settings)); + const defaultLang = 'en'; + const langMapper = new Map() + .set('zh', 'zh-CN') + .set('zh-TW', 'zh-TW') + .set('en', 'en') + .set('tr', 'tr'); + store.state.settings.lang = + langMapper.get( + langMapper.has(navigator.language) + ? navigator.language + : navigator.language.slice(0, 2) + ) || defaultLang; + localStorage.setItem('settings', JSON.stringify(store.state.settings)); } changeAppearance(store.state.settings.appearance); window - .matchMedia("(prefers-color-scheme: dark)") - .addEventListener("change", () => { - if (store.state.settings.appearance === "auto") { + .matchMedia('(prefers-color-scheme: dark)') + .addEventListener('change', () => { + if (store.state.settings.appearance === 'auto') { changeAppearance(store.state.settings.appearance); } }); @@ -47,7 +56,7 @@ player = new Proxy(player, { set(target, prop, val) { // console.log({ prop, val }); target[prop] = val; - if (prop === "_howler") return true; + if (prop === '_howler') return true; target.saveSelfToLocalStorage(); target.sendSelfToIpcMain(); return true; diff --git a/src/store/initLocalStorage.js b/src/store/initLocalStorage.js index e08997e..9540efb 100644 --- a/src/store/initLocalStorage.js +++ b/src/store/initLocalStorage.js @@ -1,19 +1,44 @@ -import { playlistCategories } from "@/utils/staticData"; +import { playlistCategories } from '@/utils/staticData'; +import shortcuts from '@/utils/shortcuts'; + +console.debug('[debug][initLocalStorage.js]'); +const enabledPlaylistCategories = playlistCategories + .filter(c => c.enable) + .map(c => c.name); let localStorage = { player: {}, settings: { - playlistCategories, lang: null, - appearance: "auto", + musicLanguage: 'all', + appearance: 'auto', musicQuality: 320000, - showGithubIcon: true, + lyricFontSize: 28, + outputDevice: 'default', showPlaylistsByAppleMusic: true, - showUnavailableSongInGreyStyle: true, - automaticallyCacheSongs: false, + enableUnblockNeteaseMusic: true, + automaticallyCacheSongs: true, + cacheLimit: 8192, + enableReversedMode: false, nyancatStyle: false, showLyricsTranslation: true, - minimizeToTray: false, + lyricsBackground: true, + enableOsdlyricsSupport: false, + closeAppOption: 'ask', + enableDiscordRichPresence: false, + enableGlobalShortcut: true, + showLibraryDefault: false, + subTitleDefault: false, + linuxEnableCustomTitlebar: false, + enabledPlaylistCategories, + proxyConfig: { + protocol: 'noProxy', + server: '', + port: null, + }, + enableRealIP: false, + realIP: null, + shortcuts: shortcuts, }, data: { user: {}, @@ -25,7 +50,6 @@ let localStorage = { if (process.env.IS_ELECTRON === true) { localStorage.settings.automaticallyCacheSongs = true; - localStorage.settings.showUnavailableSongInGreyStyle = false; } export default localStorage; diff --git a/src/store/mutations.js b/src/store/mutations.js index 66a66af..d6a4b3b 100644 --- a/src/store/mutations.js +++ b/src/store/mutations.js @@ -1,7 +1,12 @@ +import shortcuts from '@/utils/shortcuts'; +import cloneDeep from 'lodash/cloneDeep'; + export default { - updateLikedSongs(state, trackIDs) { - state.liked.songs = trackIDs; - state.player.sendSelfToIpcMain(); + updateLikedXXX(state, { name, data }) { + state.liked[name] = data; + if (name === 'songs') { + state.player.sendSelfToIpcMain(); + } }, changeLang(state, lang) { state.settings.lang = lang; @@ -9,6 +14,12 @@ export default { changeMusicQuality(state, value) { state.settings.musicQuality = value; }, + changeLyricFontSize(state, value) { + state.settings.lyricFontSize = value; + }, + changeOutputDevice(state, deviceId) { + state.settings.outputDevice = deviceId; + }, updateSettings(state, { key, value }) { state.settings[key] = value; }, @@ -16,24 +27,52 @@ export default { state.data[key] = value; }, togglePlaylistCategory(state, name) { - let cat = state.settings.playlistCategories.find((c) => c.name === name); - cat.enable = !cat.enable; - state.settings.playlistCategories = state.settings.playlistCategories.map( - (c) => { - if (c.name === name) { - return cat; - } - return c; - } + const index = state.settings.enabledPlaylistCategories.findIndex( + c => c === name ); + if (index !== -1) { + state.settings.enabledPlaylistCategories = + state.settings.enabledPlaylistCategories.filter(c => c !== name); + } else { + state.settings.enabledPlaylistCategories.push(name); + } }, updateToast(state, toast) { state.toast = toast; }, updateModal(state, { modalName, key, value }) { state.modals[modalName][key] = value; + if (key === 'show') { + // 100ms的延迟是为等待右键菜单blur之后再disableScrolling + value === true + ? setTimeout(() => (state.enableScrolling = false), 100) + : (state.enableScrolling = true); + } }, toggleLyrics(state) { state.showLyrics = !state.showLyrics; }, + updateDailyTracks(state, dailyTracks) { + state.dailyTracks = dailyTracks; + }, + updateLastfm(state, session) { + state.lastfm = session; + }, + updateShortcut(state, { id, type, shortcut }) { + let newShortcut = state.settings.shortcuts.find(s => s.id === id); + newShortcut[type] = shortcut; + state.settings.shortcuts = state.settings.shortcuts.map(s => { + if (s.id !== id) return s; + return newShortcut; + }); + }, + restoreDefaultShortcuts(state) { + state.settings.shortcuts = cloneDeep(shortcuts); + }, + enableScrolling(state, status = null) { + state.enableScrolling = status ? status : !state.enableScrolling; + }, + updateTitle(state, title) { + state.title = title; + }, }; diff --git a/src/store/plugins/localStorage.js b/src/store/plugins/localStorage.js index dfb87ab..9907aa5 100644 --- a/src/store/plugins/localStorage.js +++ b/src/store/plugins/localStorage.js @@ -1,7 +1,7 @@ -export default (store) => { +export default store => { store.subscribe((mutation, state) => { // console.log(mutation); - localStorage.setItem("settings", JSON.stringify(state.settings)); - localStorage.setItem("data", JSON.stringify(state.data)); + localStorage.setItem('settings', JSON.stringify(state.settings)); + localStorage.setItem('data', JSON.stringify(state.data)); }); }; diff --git a/src/store/plugins/sendSettings.js b/src/store/plugins/sendSettings.js index e209dba..3bfe8ad 100644 --- a/src/store/plugins/sendSettings.js +++ b/src/store/plugins/sendSettings.js @@ -1,13 +1,11 @@ export function getSendSettingsPlugin() { - const electron = window.require("electron"); + const electron = window.require('electron'); const ipcRenderer = electron.ipcRenderer; - return (store) => { + return store => { store.subscribe((mutation, state) => { - console.log(mutation); - if (mutation.type !== "updateSettings") return; - ipcRenderer.send("settings", { - minimizeToTray: state.settings.minimizeToTray, - }); + // console.log(mutation); + if (mutation.type !== 'updateSettings') return; + ipcRenderer.send('settings', state.settings); }); }; } diff --git a/src/store/state.js b/src/store/state.js index 9f7065c..827bee0 100644 --- a/src/store/state.js +++ b/src/store/state.js @@ -1,19 +1,31 @@ -import initLocalStorage from "./initLocalStorage"; -import pkg from "../../package.json"; -import updateApp from "@/utils/updateApp"; +import initLocalStorage from './initLocalStorage'; +import pkg from '../../package.json'; +import updateApp from '@/utils/updateApp'; -if (localStorage.getItem("appVersion") === null) { - localStorage.setItem("settings", JSON.stringify(initLocalStorage.settings)); - localStorage.setItem("data", JSON.stringify(initLocalStorage.data)); - localStorage.setItem("appVersion", pkg.version); +if (localStorage.getItem('appVersion') === null) { + localStorage.setItem('settings', JSON.stringify(initLocalStorage.settings)); + localStorage.setItem('data', JSON.stringify(initLocalStorage.data)); + localStorage.setItem('appVersion', pkg.version); } updateApp(); export default { showLyrics: false, + enableScrolling: true, + title: 'YesPlayMusic', liked: { songs: [], + songsWithDetails: [], // 只有前12首 + playlists: [], + albums: [], + artists: [], + mvs: [], + cloudDisk: [], + playHistory: { + weekData: [], + allData: [], + }, }, contextMenu: { clickObjectID: 0, @@ -21,7 +33,7 @@ export default { }, toast: { show: false, - text: "", + text: '', timer: null, }, modals: { @@ -34,7 +46,9 @@ export default { afterCreateAddTrackID: 0, }, }, - player: JSON.parse(localStorage.getItem("player")), - settings: JSON.parse(localStorage.getItem("settings")), - data: JSON.parse(localStorage.getItem("data")), + dailyTracks: [], + lastfm: JSON.parse(localStorage.getItem('lastfm')) || {}, + player: JSON.parse(localStorage.getItem('player')), + settings: JSON.parse(localStorage.getItem('settings')), + data: JSON.parse(localStorage.getItem('data')), }; diff --git a/src/utils/Player.js b/src/utils/Player.js index 9bf98df..11af73a 100644 --- a/src/utils/Player.js +++ b/src/utils/Player.js @@ -1,48 +1,117 @@ -import { getTrackDetail, scrobble, getMP3 } from "@/api/track"; -import { shuffle } from "lodash"; -import { Howler, Howl } from "howler"; -import localforage from "localforage"; -import { cacheTrack } from "@/utils/db"; -import { getAlbum } from "@/api/album"; -import { getPlaylistDetail } from "@/api/playlist"; -import { getArtist } from "@/api/artist"; -import store from "@/store"; -import { isAccountLoggedIn } from "@/utils/auth"; +import { getAlbum } from '@/api/album'; +import { getArtist } from '@/api/artist'; +import { trackScrobble, trackUpdateNowPlaying } from '@/api/lastfm'; +import { fmTrash, personalFM } from '@/api/others'; +import { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist'; +import { getLyric, getMP3, getTrackDetail } from '@/api/track'; +import store from '@/store'; +import { isAccountLoggedIn } from '@/utils/auth'; +import { cacheTrackSource, getTrackSource } from '@/utils/db'; +import { isCreateMpris, isCreateTray } from '@/utils/platform'; +import { Howl, Howler } from 'howler'; +import shuffle from 'lodash/shuffle'; +import { decode as base642Buffer } from '@/utils/base64'; + +const PLAY_PAUSE_FADE_DURATION = 200; + +const INDEX_IN_PLAY_NEXT = -1; + +/** + * @readonly + * @enum {string} + */ +const UNPLAYABLE_CONDITION = { + PLAY_NEXT_TRACK: 'playNextTrack', + PLAY_PREV_TRACK: 'playPrevTrack', +}; const electron = - process.env.IS_ELECTRON === true ? window.require("electron") : null; + process.env.IS_ELECTRON === true ? window.require('electron') : null; const ipcRenderer = process.env.IS_ELECTRON === true ? electron.ipcRenderer : null; +const delay = ms => + new Promise(resolve => { + setTimeout(() => { + resolve(''); + }, ms); + }); +const excludeSaveKeys = [ + '_playing', + '_personalFMLoading', + '_personalFMNextLoading', +]; + +function setTitle(track) { + document.title = track + ? `${track.name} · ${track.ar[0].name} - YesPlayMusic` + : 'YesPlayMusic'; + if (isCreateTray) { + ipcRenderer?.send('updateTrayTooltip', document.title); + } + store.commit('updateTitle', document.title); +} + +function setTrayLikeState(isLiked) { + if (isCreateTray) { + ipcRenderer?.send('updateTrayLikeState', isLiked); + } +} export default class { constructor() { - this._enabled = false; - this._repeatMode = "off"; // off | on | one + // 播放器状态 + this._playing = false; // 是否正在播放中 + this._progress = 0; // 当前播放歌曲的进度 + this._enabled = false; // 是否启用Player + this._repeatMode = 'off'; // off | on | one this._shuffle = false; // true | false + this._reversed = false; this._volume = 1; // 0 to 1 this._volumeBeforeMuted = 1; // 用于保存静音前的音量 - this._list = []; - this._current = 0; // current track index - this._shuffledList = []; - this._shuffledCurrent = 0; - this._playlistSource = { type: "album", id: 123 }; - this._currentTrack = { id: 86827685 }; - this._playNextList = []; // 当这个list不为空时,会优先播放这个list的歌 - this._playing = false; + this._personalFMLoading = false; // 是否正在私人FM中加载新的track + this._personalFMNextLoading = false; // 是否正在缓存私人FM的下一首歌曲 + // 播放信息 + this._list = []; // 播放列表 + this._current = 0; // 当前播放歌曲在播放列表里的index + this._shuffledList = []; // 被随机打乱的播放列表,随机播放模式下会使用此播放列表 + this._shuffledCurrent = 0; // 当前播放歌曲在随机列表里面的index + this._playlistSource = { type: 'album', id: 123 }; // 当前播放列表的信息 + this._currentTrack = { id: 86827685 }; // 当前播放歌曲的详细信息 + this._playNextList = []; // 当这个list不为空时,会优先播放这个list的歌 + this._isPersonalFM = false; // 是否是私人FM模式 + this._personalFMTrack = { id: 0 }; // 私人FM当前歌曲 + this._personalFMNextTrack = { + id: 0, + }; // 私人FM下一首歌曲信息(为了快速加载下一首) + + /** + * The blob records for cleanup. + * + * @private + * @type {string[]} + */ + this.createdBlobRecords = []; + + // howler (https://github.com/goldfire/howler.js) this._howler = null; - Object.defineProperty(this, "_howler", { + Object.defineProperty(this, '_howler', { enumerable: false, }); + // init this._init(); + + window.yesplaymusic = {}; + window.yesplaymusic.player = this; } get repeatMode() { return this._repeatMode; } set repeatMode(mode) { - if (!["off", "on", "one"].includes(mode)) { + if (this._isPersonalFM) return; + if (!['off', 'on', 'one'].includes(mode)) { console.warn("repeatMode: invalid args, must be 'on' | 'off' | 'one'"); return; } @@ -52,21 +121,36 @@ export default class { return this._shuffle; } set shuffle(shuffle) { + if (this._isPersonalFM) return; if (shuffle !== true && shuffle !== false) { - console.warn("shuffle: invalid args, must be Boolean"); + console.warn('shuffle: invalid args, must be Boolean'); return; } this._shuffle = shuffle; if (shuffle) { this._shuffleTheList(); } + // 同步当前歌曲在列表中的下标 + this.current = this.list.indexOf(this.currentTrackID); + } + get reversed() { + return this._reversed; + } + set reversed(reversed) { + if (this._isPersonalFM) return; + if (reversed !== true && reversed !== false) { + console.warn('reversed: invalid args, must be Boolean'); + return; + } + console.log('changing reversed to:', reversed); + this._reversed = reversed; } get volume() { return this._volume; } set volume(volume) { this._volume = volume; - Howler.volume(volume); + this._howler?.volume(volume); } get list() { return this.shuffle ? this._shuffledList : this._list; @@ -93,200 +177,545 @@ export default class { get currentTrack() { return this._currentTrack; } + get currentTrackID() { + return this._currentTrack?.id ?? 0; + } get playlistSource() { return this._playlistSource; } get playNextList() { return this._playNextList; } + get isPersonalFM() { + return this._isPersonalFM; + } + get personalFMTrack() { + return this._personalFMTrack; + } + get currentTrackDuration() { + const trackDuration = this._currentTrack.dt || 1000; + let duration = ~~(trackDuration / 1000); + return duration > 1 ? duration - 1 : duration; + } + get progress() { + return this._progress; + } + set progress(value) { + if (this._howler) { + this._howler.seek(value); + if (isCreateMpris) { + ipcRenderer?.send('seeked', this._howler.seek()); + } + } + } + get isCurrentTrackLiked() { + return store.state.liked.songs.includes(this.currentTrack.id); + } _init() { - Howler.autoUnlock = false; this._loadSelfFromLocalStorage(); - this._replaceCurrentTrack(this._currentTrack.id, false).then(() => { - this._howler.seek(localStorage.getItem("playerCurrentTrackTime") ?? 0); - setInterval( - () => - localStorage.setItem("playerCurrentTrackTime", this._howler.seek()), - 1000 - ); - }); // update audio source and init howler - this._initMediaSession(); - Howler.volume(this.volume); + this._howler?.volume(this.volume); + + if (this._enabled) { + // 恢复当前播放歌曲 + this._replaceCurrentTrack(this.currentTrackID, false).then(() => { + this._howler?.seek(localStorage.getItem('playerCurrentTrackTime') ?? 0); + }); // update audio source and init howler + this._initMediaSession(); + } + + this._setIntervals(); + + // 初始化私人FM + if ( + this._personalFMTrack.id === 0 || + this._personalFMNextTrack.id === 0 || + this._personalFMTrack.id === this._personalFMNextTrack.id + ) { + personalFM().then(result => { + this._personalFMTrack = result.data[0]; + this._personalFMNextTrack = result.data[1]; + return this._personalFMTrack; + }); + } + } + _setPlaying(isPlaying) { + this._playing = isPlaying; + if (isCreateTray) { + ipcRenderer?.send('updateTrayPlayState', this._playing); + } + } + _setIntervals() { + // 同步播放进度 + // TODO: 如果 _progress 在别的地方被改变了, + // 这个定时器会覆盖之前改变的值,是bug + setInterval(() => { + if (this._howler === null) return; + this._progress = this._howler.seek(); + localStorage.setItem('playerCurrentTrackTime', this._progress); + if (isCreateMpris) { + ipcRenderer?.send('playerCurrentTrackTime', this._progress); + } + }, 1000); } _getNextTrack() { - // 返回 [trackID, index] + const next = this._reversed ? this.current - 1 : this.current + 1; + if (this._playNextList.length > 0) { - let trackID = this._playNextList.shift(); - return [trackID, this.current]; + let trackID = this._playNextList[0]; + return [trackID, INDEX_IN_PLAY_NEXT]; } - if (this.list.length === this.current + 1 && this.repeatMode === "on") { - // 当歌曲是列表最后一首 && 循环模式开启 - return [this.list[0], 0]; + + // 循环模式开启,则重新播放当前模式下的相对的下一首 + if (this.repeatMode === 'on') { + if (this._reversed && this.current === 0) { + // 倒序模式,当前歌曲是第一首,则重新播放列表最后一首 + return [this.list[this.list.length - 1], this.list.length - 1]; + } else if (this.list.length === this.current + 1) { + // 正序模式,当前歌曲是最后一首,则重新播放第一首 + return [this.list[0], 0]; + } } - return [this.list[this.current + 1], this.current + 1]; + + // 返回 [trackID, index] + return [this.list[next], next]; } _getPrevTrack() { - if (this.current === 0 && this.repeatMode === "on") { - // 当歌曲是列表第一首 && 循环模式开启 - return [this.list[this.list.length - 1], this.list.length - 1]; + const next = this._reversed ? this.current + 1 : this.current - 1; + + // 循环模式开启,则重新播放当前模式下的相对的下一首 + if (this.repeatMode === 'on') { + if (this._reversed && this.current === 0) { + // 倒序模式,当前歌曲是最后一首,则重新播放列表第一首 + return [this.list[0], 0]; + } else if (this.list.length === this.current + 1) { + // 正序模式,当前歌曲是第一首,则重新播放列表最后一首 + return [this.list[this.list.length - 1], this.list.length - 1]; + } } - return [this.list[this.current - 1], this.current - 1]; + + // 返回 [trackID, index] + return [this.list[next], next]; } - async _shuffleTheList(firstTrackID = this._currentTrack.id) { - let list = this._list.filter((tid) => tid !== firstTrackID); - if (firstTrackID === "first") list = this._list; + async _shuffleTheList(firstTrackID = this.currentTrackID) { + let list = this._list.filter(tid => tid !== firstTrackID); + if (firstTrackID === 'first') list = this._list; this._shuffledList = shuffle(list); - if (firstTrackID !== "first") this._shuffledList.unshift(firstTrackID); + if (firstTrackID !== 'first') this._shuffledList.unshift(firstTrackID); } - async _scrobble(complete = false) { - let time = this._howler.seek(); - if (complete) { - time = ~~(this._currentTrack.dt / 100); + async _scrobble(track, time, completed = false) { + console.debug( + `[debug][Player.js] scrobble track 👉 ${track.name} by ${track.ar[0].name} 👉 time:${time} completed: ${completed}` + ); + const trackDuration = ~~(track.dt / 1000); + time = completed ? trackDuration : ~~time; + if ( + store.state.lastfm.key !== undefined && + (time >= trackDuration / 2 || time >= 240) + ) { + const timestamp = ~~(new Date().getTime() / 1000) - time; + trackScrobble({ + artist: track.ar[0].name, + track: track.name, + timestamp, + album: track.al.name, + trackNumber: track.no, + duration: trackDuration, + }); } - scrobble({ - id: this._currentTrack.id, - sourceid: this.playlistSource.id, - time, - }); } _playAudioSource(source, autoplay = true) { Howler.unload(); this._howler = new Howl({ src: [source], html5: true, - format: ["mp3", "flac"], + preload: true, + format: ['mp3', 'flac'], + onend: () => { + this._nextTrackCallback(); + }, + }); + this._howler.on('loaderror', (_, errCode) => { + // https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code + // code 3: MEDIA_ERR_DECODE + if (errCode === 3) { + this._playNextTrack(this._isPersonalFM); + } else if (errCode === 4) { + // code 4: MEDIA_ERR_SRC_NOT_SUPPORTED + store.dispatch('showToast', `无法播放: 不支持的音频格式`); + this._playNextTrack(this._isPersonalFM); + } else { + const t = this.progress; + this._replaceCurrentTrackAudio(this.currentTrack, false, false).then( + replaced => { + // 如果 replaced 为 false,代表当前的 track 已经不是这里想要替换的track + // 此时则不修改当前的歌曲进度 + if (replaced) { + this._howler?.seek(t); + this.play(); + } + } + ); + } }); if (autoplay) { this.play(); - document.title = `${this._currentTrack.name} · ${this._currentTrack.ar[0].name} - YesPlayMusic`; + if (this._currentTrack.name) { + setTitle(this._currentTrack); + } + setTrayLikeState(store.state.liked.songs.includes(this.currentTrack.id)); } - this._howler.once("end", () => { - this._nextTrackCallback(); - }); + this.setOutputDevice(); + } + _getAudioSourceBlobURL(data) { + // Create a new object URL. + const source = URL.createObjectURL(new Blob([data])); + + // Clean up the previous object URLs since we've created a new one. + // Revoke object URLs can release the memory taken by a Blob, + // which occupied a large proportion of memory. + for (const url in this.createdBlobRecords) { + URL.revokeObjectURL(url); + } + + // Then, we replace the createBlobRecords with new one with + // our newly created object URL. + this.createdBlobRecords = [source]; + + return source; } _getAudioSourceFromCache(id) { - let tracks = localforage.createInstance({ name: "tracks" }); - return tracks.getItem(id).then((t) => { - if (t === null) return null; - const source = URL.createObjectURL(new Blob([t.mp3])); - return source; + return getTrackSource(id).then(t => { + if (!t) return null; + return this._getAudioSourceBlobURL(t.source); }); } _getAudioSourceFromNetease(track) { if (isAccountLoggedIn()) { - return getMP3(track.id).then((result) => { + return getMP3(track.id).then(result => { if (!result.data[0]) return null; if (!result.data[0].url) return null; if (result.data[0].freeTrialInfo !== null) return null; // 跳过只能试听的歌曲 - const source = result.data[0].url.replace(/^http:/, "https:"); + const source = result.data[0].url.replace(/^http:/, 'https:'); if (store.state.settings.automaticallyCacheSongs) { - cacheTrack(track.id, source); + cacheTrackSource(track, source, result.data[0].br); } return source; }); } else { - return new Promise((resolve) => { + return new Promise(resolve => { resolve(`https://music.163.com/song/media/outer/url?id=${track.id}`); }); } } - _getAudioSourceFromUnblockMusic(track) { - if (process.env.IS_ELECTRON !== true) return null; - const source = ipcRenderer.sendSync("unblock-music", track); - if (store.state.settings.automaticallyCacheSongs && source?.url) { - cacheTrack(track.id, source.url); + async _getAudioSourceFromUnblockMusic(track) { + console.debug(`[debug][Player.js] _getAudioSourceFromUnblockMusic`); + + if ( + process.env.IS_ELECTRON !== true || + store.state.settings.enableUnblockNeteaseMusic === false + ) { + return null; } - return source?.url; + + /** + * + * @param {string=} searchMode + * @returns {import("@unblockneteasemusic/rust-napi").SearchMode} + */ + const determineSearchMode = searchMode => { + /** + * FastFirst = 0 + * OrderFirst = 1 + */ + switch (searchMode) { + case 'fast-first': + return 0; + case 'order-first': + return 1; + default: + return 0; + } + }; + + const retrieveSongInfo = await ipcRenderer.invoke( + 'unblock-music', + store.state.settings.unmSource, + track, + { + enableFlac: store.state.settings.unmEnableFlac || null, + proxyUri: store.state.settings.unmProxyUri || null, + searchMode: determineSearchMode(store.state.settings.unmSearchMode), + config: { + 'joox:cookie': store.state.settings.unmJooxCookie || null, + 'qq:cookie': store.state.settings.unmQQCookie || null, + 'ytdl:exe': store.state.settings.unmYtDlExe || null, + }, + } + ); + + if (store.state.settings.automaticallyCacheSongs && retrieveSongInfo?.url) { + // 对于来自 bilibili 的音源 + // retrieveSongInfo.url 是音频数据的base64编码 + // 其他音源为实际url + const url = + retrieveSongInfo.source === 'bilibili' + ? `data:application/octet-stream;base64,${retrieveSongInfo.url}` + : retrieveSongInfo.url; + cacheTrackSource(track, url, 128000, `unm:${retrieveSongInfo.source}`); + } + + if (!retrieveSongInfo) { + return null; + } + + if (retrieveSongInfo.source !== 'bilibili') { + return retrieveSongInfo.url; + } + + const buffer = base642Buffer(retrieveSongInfo.url); + return this._getAudioSourceBlobURL(buffer); } _getAudioSource(track) { return this._getAudioSourceFromCache(String(track.id)) - .then((source) => { + .then(source => { return source ?? this._getAudioSourceFromNetease(track); }) - .then((source) => { + .then(source => { return source ?? this._getAudioSourceFromUnblockMusic(track); }); } _replaceCurrentTrack( id, autoplay = true, - ifUnplayableThen = "playNextTrack" + ifUnplayableThen = UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK ) { - return getTrackDetail(id).then((data) => { - let track = data.songs[0]; + if (autoplay && this._currentTrack.name) { + this._scrobble(this.currentTrack, this._howler?.seek()); + } + return getTrackDetail(id).then(data => { + const track = data.songs[0]; this._currentTrack = track; this._updateMediaSessionMetaData(track); - return this._getAudioSource(track).then((source) => { - if (source) { + return this._replaceCurrentTrackAudio( + track, + autoplay, + true, + ifUnplayableThen + ); + }); + } + /** + * @returns 是否成功加载音频,并使用加载完成的音频替换了howler实例 + */ + _replaceCurrentTrackAudio( + track, + autoplay, + isCacheNextTrack, + ifUnplayableThen = UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK + ) { + return this._getAudioSource(track).then(source => { + if (source) { + let replaced = false; + if (track.id === this.currentTrackID) { this._playAudioSource(source, autoplay); - return source; - } else { - store.dispatch("showToast", `无法播放 ${track.name}`); - ifUnplayableThen === "playNextTrack" - ? this.playNextTrack() - : this.playPrevTrack(); + replaced = true; } - }); + if (isCacheNextTrack) { + this._cacheNextTrack(); + } + return replaced; + } else { + store.dispatch('showToast', `无法播放 ${track.name}`); + switch (ifUnplayableThen) { + case UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK: + this._playNextTrack(this.isPersonalFM); + break; + case UNPLAYABLE_CONDITION.PLAY_PREV_TRACK: + this.playPrevTrack(); + break; + default: + store.dispatch( + 'showToast', + `undefined Unplayable condition: ${ifUnplayableThen}` + ); + break; + } + return false; + } + }); + } + _cacheNextTrack() { + let nextTrackID = this._isPersonalFM + ? this._personalFMNextTrack?.id ?? 0 + : this._getNextTrack()[0]; + if (!nextTrackID) return; + if (this._personalFMTrack.id == nextTrackID) return; + getTrackDetail(nextTrackID).then(data => { + let track = data.songs[0]; + this._getAudioSource(track); }); } _loadSelfFromLocalStorage() { - const player = JSON.parse(localStorage.getItem("player")); + const player = JSON.parse(localStorage.getItem('player')); if (!player) return; for (const [key, value] of Object.entries(player)) { this[key] = value; } } _initMediaSession() { - if ("mediaSession" in navigator) { - navigator.mediaSession.setActionHandler("play", () => { + if ('mediaSession' in navigator) { + navigator.mediaSession.setActionHandler('play', () => { this.play(); }); - navigator.mediaSession.setActionHandler("pause", () => { + navigator.mediaSession.setActionHandler('pause', () => { this.pause(); }); - navigator.mediaSession.setActionHandler("previoustrack", () => { + navigator.mediaSession.setActionHandler('previoustrack', () => { this.playPrevTrack(); }); - navigator.mediaSession.setActionHandler("nexttrack", () => { - this.playNextTrack(); + navigator.mediaSession.setActionHandler('nexttrack', () => { + this._playNextTrack(this.isPersonalFM); }); - navigator.mediaSession.setActionHandler("stop", () => { + navigator.mediaSession.setActionHandler('stop', () => { this.pause(); }); + navigator.mediaSession.setActionHandler('seekto', event => { + this.seek(event.seekTime); + this._updateMediaSessionPositionState(); + }); + navigator.mediaSession.setActionHandler('seekbackward', event => { + this.seek(this.seek() - (event.seekOffset || 10)); + this._updateMediaSessionPositionState(); + }); + navigator.mediaSession.setActionHandler('seekforward', event => { + this.seek(this.seek() + (event.seekOffset || 10)); + this._updateMediaSessionPositionState(); + }); } } _updateMediaSessionMetaData(track) { - if ("mediaSession" in navigator === false) { + if ('mediaSession' in navigator === false) { return; } - let artists = track.ar.map((a) => a.name); - navigator.mediaSession.metadata = new window.MediaMetadata({ + let artists = track.ar.map(a => a.name); + const metadata = { title: track.name, - artist: artists.join(","), + artist: artists.join(','), album: track.al.name, artwork: [ { - src: track.al.picUrl + "?param=512y512", - type: "image/jpg", - sizes: "512x512", + src: track.al.picUrl + '?param=224y224', + type: 'image/jpg', + sizes: '224x224', + }, + { + src: track.al.picUrl + '?param=512y512', + type: 'image/jpg', + sizes: '512x512', }, ], + length: this.currentTrackDuration, + trackId: this.current, + url: '/trackid/' + track.id, + }; + + navigator.mediaSession.metadata = new window.MediaMetadata(metadata); + if (isCreateMpris) { + this._updateMprisState(track, metadata); + } + } + // OSDLyrics 会检测 Mpris 状态并寻找对应歌词文件,所以要在更新 Mpris 状态之前保证歌词下载完成 + async _updateMprisState(track, metadata) { + if (!store.state.settings.enableOsdlyricsSupport) { + return ipcRenderer?.send('metadata', metadata); + } + + let lyricContent = await getLyric(track.id); + + if (!lyricContent.lrc || !lyricContent.lrc.lyric) { + return ipcRenderer?.send('metadata', metadata); + } + + ipcRenderer.send('sendLyrics', { + track, + lyrics: lyricContent.lrc.lyric, + }); + + ipcRenderer.on('saveLyricFinished', () => { + ipcRenderer?.send('metadata', metadata); }); } + _updateMediaSessionPositionState() { + if ('mediaSession' in navigator === false) { + return; + } + if ('setPositionState' in navigator.mediaSession) { + navigator.mediaSession.setPositionState({ + duration: ~~(this.currentTrack.dt / 1000), + playbackRate: 1.0, + position: this.seek(), + }); + } + } _nextTrackCallback() { - this._scrobble(true); - if (this.repeatMode === "one") { - this._replaceCurrentTrack(this._currentTrack.id); + this._scrobble(this._currentTrack, 0, true); + if (!this.isPersonalFM && this.repeatMode === 'one') { + this._replaceCurrentTrack(this.currentTrackID); + } else { + this._playNextTrack(this.isPersonalFM); + } + } + _loadPersonalFMNextTrack() { + if (this._personalFMNextLoading) { + return [false, undefined]; + } + this._personalFMNextLoading = true; + return personalFM() + .then(result => { + if (!result || !result.data) { + this._personalFMNextTrack = undefined; + } else { + this._personalFMNextTrack = result.data[0]; + this._cacheNextTrack(); // cache next track + } + this._personalFMNextLoading = false; + return [true, this._personalFMNextTrack]; + }) + .catch(() => { + this._personalFMNextTrack = undefined; + this._personalFMNextLoading = false; + return [false, this._personalFMNextTrack]; + }); + } + _playDiscordPresence(track, seekTime = 0) { + if ( + process.env.IS_ELECTRON !== true || + store.state.settings.enableDiscordRichPresence === false + ) { + return null; + } + let copyTrack = { ...track }; + copyTrack.dt -= seekTime * 1000; + ipcRenderer?.send('playDiscordPresence', copyTrack); + } + _pauseDiscordPresence(track) { + if ( + process.env.IS_ELECTRON !== true || + store.state.settings.enableDiscordRichPresence === false + ) { + return null; + } + ipcRenderer?.send('pauseDiscordPresence', track); + } + _playNextTrack(isPersonal) { + if (isPersonal) { + this.playNextFMTrack(); } else { this.playNextTrack(); } } - currentTrackID() { - const { list, current } = this._getListAndCurrent(); - return list[current]; - } appendTrack(trackID) { this.list.append(trackID); } @@ -294,42 +723,138 @@ export default class { // TODO: 切换歌曲时增加加载中的状态 const [trackID, index] = this._getNextTrack(); if (trackID === undefined) { - this._howler.stop(); + this._howler?.stop(); + this._setPlaying(false); return false; } - this.current = index; + let next = index; + if (index === INDEX_IN_PLAY_NEXT) { + this._playNextList.shift(); + next = this.current; + } + this.current = next; this._replaceCurrentTrack(trackID); return true; } + async playNextFMTrack() { + if (this._personalFMLoading) { + return false; + } + + this._isPersonalFM = true; + if (!this._personalFMNextTrack) { + this._personalFMLoading = true; + let result = null; + let retryCount = 5; + for (; retryCount >= 0; retryCount--) { + result = await personalFM().catch(() => null); + if (!result) { + this._personalFMLoading = false; + store.dispatch('showToast', 'personal fm timeout'); + return false; + } + if (result.data?.length > 0) { + break; + } else if (retryCount > 0) { + await delay(1000); + } + } + this._personalFMLoading = false; + + if (retryCount < 0) { + let content = '获取私人FM数据时重试次数过多,请手动切换下一首'; + store.dispatch('showToast', content); + console.log(content); + return false; + } + // 这里只能拿到一条数据 + this._personalFMTrack = result.data[0]; + } else { + if (this._personalFMNextTrack.id === this._personalFMTrack.id) { + return false; + } + this._personalFMTrack = this._personalFMNextTrack; + } + if (this._isPersonalFM) { + this._replaceCurrentTrack(this._personalFMTrack.id); + } + this._loadPersonalFMNextTrack(); + return true; + } playPrevTrack() { const [trackID, index] = this._getPrevTrack(); if (trackID === undefined) return false; this.current = index; - this._replaceCurrentTrack(trackID, true, "playPrevTrack"); + this._replaceCurrentTrack( + trackID, + true, + UNPLAYABLE_CONDITION.PLAY_PREV_TRACK + ); return true; } saveSelfToLocalStorage() { let player = {}; for (let [key, value] of Object.entries(this)) { - if (key === "_playing") continue; + if (excludeSaveKeys.includes(key)) continue; player[key] = value; } - localStorage.setItem("player", JSON.stringify(player)); + localStorage.setItem('player', JSON.stringify(player)); } pause() { - this._howler.pause(); - this._playing = false; - document.title = "YesPlayMusic"; + this._howler?.fade(this.volume, 0, PLAY_PAUSE_FADE_DURATION); + + this._howler?.once('fade', () => { + this._howler?.pause(); + this._setPlaying(false); + setTitle(null); + this._pauseDiscordPresence(this._currentTrack); + }); } play() { - this._howler.play(); - this._playing = true; - document.title = `${this._currentTrack.name} · ${this._currentTrack.ar[0].name} - YesPlayMusic`; + if (this._howler?.playing()) return; + + this._howler?.play(); + + this._howler?.once('play', () => { + this._howler?.fade(0, this.volume, PLAY_PAUSE_FADE_DURATION); + + // 播放时确保开启player. + // 避免因"忘记设置"导致在播放时播放器不显示的Bug + this._enabled = true; + this._setPlaying(true); + if (this._currentTrack.name) { + setTitle(this._currentTrack); + } + this._playDiscordPresence(this._currentTrack, this.seek()); + if (store.state.lastfm.key !== undefined) { + trackUpdateNowPlaying({ + artist: this.currentTrack.ar[0].name, + track: this.currentTrack.name, + album: this.currentTrack.al.name, + trackNumber: this.currentTrack.no, + duration: ~~(this.currentTrack.dt / 1000), + }); + } + }); } - seek(time = null) { - if (time !== null) this._howler.seek(time); + playOrPause() { + if (this._howler?.playing()) { + this.pause(); + } else { + this.play(); + } + } + seek(time = null, sendMpris = true) { + if (isCreateMpris && sendMpris && time) { + ipcRenderer?.send('seeked', time); + } + if (time !== null) { + this._howler?.seek(time); + if (this._playing) + this._playDiscordPresence(this._currentTrack, this.seek(null, false)); + } return this._howler === null ? 0 : this._howler.seek(); } mute() { @@ -340,14 +865,20 @@ export default class { this.volume = 0; } } + setOutputDevice() { + if (this._howler?._sounds.length <= 0 || !this._howler?._sounds[0]._node) { + return; + } + this._howler?._sounds[0]._node.setSinkId(store.state.settings.outputDevice); + } replacePlaylist( trackIDs, playlistSourceID, playlistSourceType, - autoPlayTrackID = "first" + autoPlayTrackID = 'first' ) { - if (!this._enabled) this._enabled = true; + this._isPersonalFM = false; this.list = trackIDs; this.current = 0; this._playlistSource = { @@ -355,41 +886,110 @@ export default class { id: playlistSourceID, }; if (this.shuffle) this._shuffleTheList(autoPlayTrackID); - if (autoPlayTrackID === "first") { + if (autoPlayTrackID === 'first') { this._replaceCurrentTrack(this.list[0]); } else { - this.current = trackIDs.indexOf(autoPlayTrackID); + this.current = this.list.indexOf(autoPlayTrackID); this._replaceCurrentTrack(autoPlayTrackID); } } - playAlbumByID(id, trackID = "first") { - getAlbum(id).then((data) => { - let trackIDs = data.songs.map((t) => t.id); - this.replacePlaylist(trackIDs, id, "album", trackID); + playAlbumByID(id, trackID = 'first') { + getAlbum(id).then(data => { + let trackIDs = data.songs.map(t => t.id); + this.replacePlaylist(trackIDs, id, 'album', trackID); }); } - playPlaylistByID(id, trackID = "first", noCache = false) { - getPlaylistDetail(id, noCache).then((data) => { - let trackIDs = data.playlist.trackIds.map((t) => t.id); - this.replacePlaylist(trackIDs, id, "playlist", trackID); + playPlaylistByID(id, trackID = 'first', noCache = false) { + console.debug( + `[debug][Player.js] playPlaylistByID 👉 id:${id} trackID:${trackID} noCache:${noCache}` + ); + getPlaylistDetail(id, noCache).then(data => { + let trackIDs = data.playlist.trackIds.map(t => t.id); + this.replacePlaylist(trackIDs, id, 'playlist', trackID); }); } - playArtistByID(id, trackID = "first") { - getArtist(id).then((data) => { - let trackIDs = data.hotSongs.map((t) => t.id); - this.replacePlaylist(trackIDs, id, "artist", trackID); + playArtistByID(id, trackID = 'first') { + getArtist(id).then(data => { + let trackIDs = data.hotSongs.map(t => t.id); + this.replacePlaylist(trackIDs, id, 'artist', trackID); + }); + } + playTrackOnListByID(id, listName = 'default') { + if (listName === 'default') { + this._current = this._list.findIndex(t => t === id); + } + this._replaceCurrentTrack(id); + } + playIntelligenceListById(id, trackID = 'first', noCache = false) { + getPlaylistDetail(id, noCache).then(data => { + const randomId = Math.floor( + Math.random() * (data.playlist.trackIds.length + 1) + ); + const songId = data.playlist.trackIds[randomId].id; + intelligencePlaylist({ id: songId, pid: id }).then(result => { + let trackIDs = result.data.map(t => t.id); + this.replacePlaylist(trackIDs, id, 'playlist', trackID); + }); }); } addTrackToPlayNext(trackID, playNow = false) { this._playNextList.push(trackID); - if (playNow) this.playNextTrack(); + if (playNow) { + this.playNextTrack(); + } + } + playPersonalFM() { + this._isPersonalFM = true; + if (this.currentTrackID !== this._personalFMTrack.id) { + this._replaceCurrentTrack(this._personalFMTrack.id, true); + } else { + this.playOrPause(); + } + } + async moveToFMTrash() { + this._isPersonalFM = true; + let id = this._personalFMTrack.id; + if (await this.playNextFMTrack()) { + fmTrash(id); + } } sendSelfToIpcMain() { if (process.env.IS_ELECTRON !== true) return false; - ipcRenderer.send("player", { + let liked = store.state.liked.songs.includes(this.currentTrack.id); + ipcRenderer?.send('player', { playing: this.playing, - likedCurrentTrack: store.state.liked.songs.includes(this.currentTrack.id), + likedCurrentTrack: liked, }); + setTrayLikeState(liked); + } + + switchRepeatMode() { + if (this._repeatMode === 'on') { + this.repeatMode = 'one'; + } else if (this._repeatMode === 'one') { + this.repeatMode = 'off'; + } else { + this.repeatMode = 'on'; + } + if (isCreateMpris) { + ipcRenderer?.send('switchRepeatMode', this.repeatMode); + } + } + switchShuffle() { + this.shuffle = !this.shuffle; + if (isCreateMpris) { + ipcRenderer?.send('switchShuffle', this.shuffle); + } + } + switchReversed() { + this.reversed = !this.reversed; + } + + clearPlayNextList() { + this._playNextList = []; + } + removeTrackFromQueue(index) { + this._playNextList.splice(index, 1); } } diff --git a/src/utils/auth.js b/src/utils/auth.js index 5376250..6c43863 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -1,33 +1,41 @@ -import Cookies from "js-cookie"; -import { logout } from "@/api/auth"; -import store from "@/store"; +import Cookies from 'js-cookie'; +import { logout } from '@/api/auth'; +import store from '@/store'; -export function doLogout() { - logout(); - // 网易云的接口会自动移除该 cookies - // Cookies.remove("MUSIC_U"); - // 更新状态仓库中的用户信息 - store.commit("updateData", { key: "user", value: {} }); - // 更新状态仓库中的登录状态 - store.commit("updateData", { key: "loginMode", value: null }); +export function setCookies(string) { + const cookies = string.split(';;'); + cookies.map(cookie => { + document.cookie = cookie; + const cookieKeyValue = cookie.split(';')[0].split('='); + localStorage.setItem(`cookie-${cookieKeyValue[0]}`, cookieKeyValue[1]); + }); +} + +export function getCookie(key) { + return Cookies.get(key) ?? localStorage.getItem(`cookie-${key}`); +} + +export function removeCookie(key) { + Cookies.remove(key); + localStorage.removeItem(`cookie-${key}`); } // MUSIC_U 只有在账户登录的情况下才有 export function isLoggedIn() { - return Cookies.get("MUSIC_U") !== undefined ? true : false; + return getCookie('MUSIC_U') !== undefined; } // 账号登录 export function isAccountLoggedIn() { return ( - Cookies.get("MUSIC_U") !== undefined && - store.state.data.loginMode === "account" + getCookie('MUSIC_U') !== undefined && + store.state.data.loginMode === 'account' ); } // 用户名搜索(用户数据为只读) export function isUsernameLoggedIn() { - return store.state.data.loginMode === "username"; + return store.state.data.loginMode === 'username'; } // 账户登录或者用户名搜索都判断为登录,宽松检查 @@ -35,18 +43,14 @@ export function isLooseLoggedIn() { return isAccountLoggedIn() || isUsernameLoggedIn(); } -export function getMusicU(string) { - const temp = string.split(";"); - if (!temp.length) { - return undefined; - } - const MUSIC_U = temp.find((item) => item.includes("MUSIC_U")); - if (MUSIC_U) { - return MUSIC_U.split("=")[1]; - } - return ""; -} - -export function setMusicU(key, value) { - return Cookies.set(key, value); +export function doLogout() { + logout(); + removeCookie('MUSIC_U'); + removeCookie('__csrf'); + // 更新状态仓库中的用户信息 + store.commit('updateData', { key: 'user', value: {} }); + // 更新状态仓库中的登录状态 + store.commit('updateData', { key: 'loginMode', value: null }); + // 更新状态仓库中的喜欢列表 + store.commit('updateData', { key: 'likedSongPlaylistID', value: undefined }); } diff --git a/src/utils/base64.js b/src/utils/base64.js new file mode 100644 index 0000000..99ac23c --- /dev/null +++ b/src/utils/base64.js @@ -0,0 +1,67 @@ +// https://github.com/niklasvh/base64-arraybuffer/blob/master/src/index.ts +// Copyright (c) 2012 Niklas von Hertzen Licensed under the MIT license. + +const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + +// Use a lookup table to find the index. +const lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256); +for (let i = 0; i < chars.length; i++) { + lookup[chars.charCodeAt(i)] = i; +} + +export const encode = arraybuffer => { + let bytes = new Uint8Array(arraybuffer), + i, + len = bytes.length, + base64 = ''; + + for (i = 0; i < len; i += 3) { + base64 += chars[bytes[i] >> 2]; + base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)]; + base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)]; + base64 += chars[bytes[i + 2] & 63]; + } + + if (len % 3 === 2) { + base64 = base64.substring(0, base64.length - 1) + '='; + } else if (len % 3 === 1) { + base64 = base64.substring(0, base64.length - 2) + '=='; + } + + return base64; +}; + +export const decode = base64 => { + let bufferLength = base64.length * 0.75, + len = base64.length, + i, + p = 0, + encoded1, + encoded2, + encoded3, + encoded4; + + if (base64[base64.length - 1] === '=') { + bufferLength--; + if (base64[base64.length - 2] === '=') { + bufferLength--; + } + } + + const arraybuffer = new ArrayBuffer(bufferLength), + bytes = new Uint8Array(arraybuffer); + + for (i = 0; i < len; i += 4) { + encoded1 = lookup[base64.charCodeAt(i)]; + encoded2 = lookup[base64.charCodeAt(i + 1)]; + encoded3 = lookup[base64.charCodeAt(i + 2)]; + encoded4 = lookup[base64.charCodeAt(i + 3)]; + + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + + return arraybuffer; +}; diff --git a/src/utils/checkAuthToken.js b/src/utils/checkAuthToken.js new file mode 100644 index 0000000..1086521 --- /dev/null +++ b/src/utils/checkAuthToken.js @@ -0,0 +1,8 @@ +import os from 'os'; +import fs from 'fs'; +import path from 'path'; + +// extract from NeteasyCloudMusicAPI/generateConfig.js and avoid bugs in there (generateConfig require main.js but the main.js has bugs) +if (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) { + fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8'); +} diff --git a/src/utils/common.js b/src/utils/common.js index 836f240..a89d500 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -1,14 +1,17 @@ -import { isAccountLoggedIn } from "./auth"; -import { refreshCookie } from "@/api/auth"; -import { dailySignin } from "@/api/user"; -import dayjs from "dayjs"; -import store from "@/store"; +import { isAccountLoggedIn } from './auth'; +import { refreshCookie } from '@/api/auth'; +import { dailySignin } from '@/api/user'; +import dayjs from 'dayjs'; +import store from '@/store'; export function isTrackPlayable(track) { let result = { playable: true, - reason: "", + reason: '', }; + if (track?.privilege?.pl > 0) { + return result; + } // cloud storage judgement logic if (isAccountLoggedIn() && track?.privilege?.cs) { return result; @@ -18,27 +21,28 @@ export function isTrackPlayable(track) { result.playable = true; } else { result.playable = false; - result.reason = "VIP Only"; + result.reason = 'VIP Only'; } } else if (track.fee === 4 || track.privilege?.fee === 4) { result.playable = false; - result.reason = "付费专辑"; + result.reason = '付费专辑'; } else if ( track.noCopyrightRcmd !== null && track.noCopyrightRcmd !== undefined ) { result.playable = false; - result.reason = "无版权"; + result.reason = '无版权'; } else if (track.privilege?.st < 0 && isAccountLoggedIn()) { result.playable = false; - result.reason = "已下架"; + result.reason = '已下架'; } return result; } export function mapTrackPlayableStatus(tracks, privileges = []) { - return tracks.map((t) => { - const privilege = privileges.find((item) => item.id === t.id) || {}; + if (tracks?.length === undefined) return tracks; + return tracks.map(t => { + const privilege = privileges.find(item => item.id === t.id) || {}; if (t.privilege) { Object.assign(t.privilege, privilege); } else { @@ -63,13 +67,13 @@ export function randomNum(minNum, maxNum) { } export function shuffleAList(list) { - let sortsList = list.map((t) => t.sort); + let sortsList = list.map(t => t.sort); for (let i = 1; i < sortsList.length; i++) { const random = Math.floor(Math.random() * (i + 1)); [sortsList[i], sortsList[random]] = [sortsList[random], sortsList[i]]; } let newSorts = {}; - list.map((track) => { + list.map(track => { newSorts[track.id] = sortsList.pop(); }); return newSorts; @@ -88,8 +92,8 @@ export function throttle(fn, time) { } export function updateHttps(url) { - if (!url) return ""; - return url.replace(/^http:/, "https:"); + if (!url) return ''; + return url.replace(/^http:/, 'https:'); } export function dailyTask() { @@ -98,97 +102,103 @@ export function dailyTask() { isAccountLoggedIn() && (lastDate === undefined || lastDate !== dayjs().date()) ) { - console.log("execute dailyTask"); - store.commit("updateData", { - key: "lastRefreshCookieDate", - value: dayjs().date(), + console.debug('[debug][common.js] execute dailyTask'); + refreshCookie().then(() => { + console.debug('[debug][common.js] 刷新cookie'); + store.commit('updateData', { + key: 'lastRefreshCookieDate', + value: dayjs().date(), + }); + }); + dailySignin(0).catch(() => { + console.debug('[debug][common.js] 手机端重复签到'); + }); + dailySignin(1).catch(() => { + console.debug('[debug][common.js] PC端重复签到'); }); - refreshCookie(); - dailySignin(0); - dailySignin(1); } } export function changeAppearance(appearance) { - if (appearance === "auto" || appearance === undefined) { - appearance = window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light"; + if (appearance === 'auto' || appearance === undefined) { + appearance = window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light'; } - document.body.setAttribute("data-theme", appearance); + document.body.setAttribute('data-theme', appearance); document .querySelector('meta[name="theme-color"]') - .setAttribute("content", appearance === "dark" ? "#222" : "#fff"); + .setAttribute('content', appearance === 'dark' ? '#222' : '#fff'); } export function splitSoundtrackAlbumTitle(title) { let keywords = [ - "Music from the Original Motion Picture Score", - "The Original Motion Picture Soundtrack", - "Original MGM Motion Picture Soundtrack", - "Complete Original Motion Picture Score", - "Original Music From The Motion Picture", - "Music From The Disney+ Original Movie", - "Original Music From The Netflix Film", - "Original Score to the Motion Picture", - "Original Motion Picture Soundtrack", - "Soundtrack from the Motion Picture", - "Original Television Soundtrack", - "Original Motion Picture Score", - "Music From the Motion Picture", - "Music From The Motion Picture", - "Complete Motion Picture Score", - "Music from the Motion Picture", - "Original Videogame Soundtrack", - "La Bande Originale du Film", - "Music from the Miniseries", - "Bande Originale du Film", - "Die Original Filmmusik", - "Original Soundtrack", - "Complete Score", - "Original Score", + 'Music from the Original Motion Picture Score', + 'The Original Motion Picture Soundtrack', + 'Original MGM Motion Picture Soundtrack', + 'Complete Original Motion Picture Score', + 'Original Music From The Motion Picture', + 'Music From The Disney+ Original Movie', + 'Original Music From The Netflix Film', + 'Original Score to the Motion Picture', + 'Original Motion Picture Soundtrack', + 'Soundtrack from the Motion Picture', + 'Original Television Soundtrack', + 'Original Motion Picture Score', + 'Music From the Motion Picture', + 'Music From The Motion Picture', + 'Complete Motion Picture Score', + 'Music from the Motion Picture', + 'Original Videogame Soundtrack', + 'La Bande Originale du Film', + 'Music from the Miniseries', + 'Bande Originale du Film', + 'Die Original Filmmusik', + 'Original Soundtrack', + 'Complete Score', + 'Original Score', ]; for (let keyword of keywords) { if (title.includes(keyword) === false) continue; return { title: title - .replace(`(${keyword})`, "") - .replace(`: ${keyword}`, "") - .replace(`[${keyword}]`, "") - .replace(`- ${keyword}`, "") - .replace(`${keyword}`, ""), + .replace(`(${keyword})`, '') + .replace(`: ${keyword}`, '') + .replace(`[${keyword}]`, '') + .replace(`- ${keyword}`, '') + .replace(`${keyword}`, ''), subtitle: keyword, }; } return { title: title, - subtitle: "", + subtitle: '', }; } export function splitAlbumTitle(title) { let keywords = [ - "Bonus Tracks Edition", - "Complete Edition", - "Deluxe Edition", - "Deluxe Version", - "Tour Edition", + 'Bonus Tracks Edition', + 'Complete Edition', + 'Deluxe Edition', + 'Deluxe Version', + 'Tour Edition', ]; for (let keyword of keywords) { if (title.includes(keyword) === false) continue; return { title: title - .replace(`(${keyword})`, "") - .replace(`: ${keyword}`, "") - .replace(`[${keyword}]`, "") - .replace(`- ${keyword}`, "") - .replace(`${keyword}`, ""), + .replace(`(${keyword})`, '') + .replace(`: ${keyword}`, '') + .replace(`[${keyword}]`, '') + .replace(`- ${keyword}`, '') + .replace(`${keyword}`, ''), subtitle: keyword, }; } return { title: title, - subtitle: "", + subtitle: '', }; } @@ -201,17 +211,17 @@ export function bytesToSize(bytes) { let lang = store.state.settings.lang; - if (bytes < kiloBytes) return bytes + (lang === "en" ? " Bytes" : "字节"); + if (bytes < kiloBytes) return bytes + (lang === 'en' ? ' Bytes' : '字节'); else if (bytes < megaBytes) - return (bytes / kiloBytes).toFixed(decimal) + " KB"; + return (bytes / kiloBytes).toFixed(decimal) + ' KB'; else if (bytes < gigaBytes) - return (bytes / megaBytes).toFixed(decimal) + " MB"; - else return (bytes / gigaBytes).toFixed(decimal) + " GB"; + return (bytes / megaBytes).toFixed(decimal) + ' MB'; + else return (bytes / gigaBytes).toFixed(decimal) + ' GB'; } export function formatTrackTime(value) { - if (!value) return ""; - let min = ~~((value / 60) % 60); - let sec = (~~(value % 60)).toString().padStart(2, "0"); + if (!value) return ''; + let min = ~~(value / 60); + let sec = (~~(value % 60)).toString().padStart(2, '0'); return `${min}:${sec}`; } diff --git a/src/utils/db.js b/src/utils/db.js index e7bb5bf..6dfabeb 100644 --- a/src/utils/db.js +++ b/src/utils/db.js @@ -1,54 +1,183 @@ -import axios from "axios"; -import localforage from "localforage"; +import axios from 'axios'; +import Dexie from 'dexie'; +import store from '@/store'; +// import pkg from "../../package.json"; -export function cacheTrack(id, url) { - let tracks = localforage.createInstance({ - name: "tracks", - }); +const db = new Dexie('yesplaymusic'); - // TODO: limit cache songs number - // tracks.length().then(function (length) { - // if (length > 2) { - // tracks.keys().then(function (keys) { - // tracks.removeItem(keys[keys.length - 2]); - // }); - // } - // }); +db.version(4).stores({ + trackDetail: '&id, updateTime', + lyric: '&id, updateTime', + album: '&id, updateTime', +}); - // TODO: cache track details +db.version(3) + .stores({ + trackSources: '&id, createTime', + }) + .upgrade(tx => + tx + .table('trackSources') + .toCollection() + .modify( + track => !track.createTime && (track.createTime = new Date().getTime()) + ) + ); + +db.version(1).stores({ + trackSources: '&id', +}); + +let tracksCacheBytes = 0; + +async function deleteExcessCache() { + if ( + store.state.settings.cacheLimit === false || + tracksCacheBytes < store.state.settings.cacheLimit * Math.pow(1024, 2) + ) { + return; + } + try { + const delCache = await db.trackSources.orderBy('createTime').first(); + await db.trackSources.delete(delCache.id); + tracksCacheBytes -= delCache.source.byteLength; + console.debug( + `[debug][db.js] deleteExcessCacheSucces, track: ${delCache.name}, size: ${delCache.source.byteLength}, cacheSize:${tracksCacheBytes}` + ); + deleteExcessCache(); + } catch (error) { + console.debug('[debug][db.js] deleteExcessCacheFailed', error); + } +} + +export function cacheTrackSource(trackInfo, url, bitRate, from = 'netease') { + if (!process.env.IS_ELECTRON) return; + const name = trackInfo.name; + const artist = + (trackInfo.ar && trackInfo.ar[0]?.name) || + (trackInfo.artists && trackInfo.artists[0]?.name) || + 'Unknown'; + let cover = trackInfo.al.picUrl; + if (cover.slice(0, 5) !== 'https') { + cover = 'https' + cover.slice(4); + } + axios.get(`${cover}?param=512y512`); + axios.get(`${cover}?param=224y224`); + axios.get(`${cover}?param=1024y1024`); return axios .get(url, { - responseType: "arraybuffer", + responseType: 'arraybuffer', }) - .then((response) => { - tracks.setItem(`${id}`, { mp3: response.data }); - return { mp3: response.data }; + .then(response => { + db.trackSources.put({ + id: trackInfo.id, + source: response.data, + bitRate, + from, + name, + artist, + createTime: new Date().getTime(), + }); + console.debug(`[debug][db.js] cached track 👉 ${name} by ${artist}`); + tracksCacheBytes += response.data.byteLength; + deleteExcessCache(); + return { trackID: trackInfo.id, source: response.data, bitRate }; }); } -export function countDBSize(dbName) { - let db = localforage.createInstance({ - name: dbName, +export function getTrackSource(id) { + return db.trackSources.get(Number(id)).then(track => { + if (!track) return null; + console.debug( + `[debug][db.js] get track from cache 👉 ${track.name} by ${track.artist}` + ); + return track; }); - let trackSizes = []; - return db - .iterate((value) => { - trackSizes.push(value.mp3.byteLength); +} + +export function cacheTrackDetail(track, privileges) { + db.trackDetail.put({ + id: track.id, + detail: track, + privileges: privileges, + updateTime: new Date().getTime(), + }); +} + +export function getTrackDetailFromCache(ids) { + return db.trackDetail + .filter(track => { + return ids.includes(String(track.id)); + }) + .toArray() + .then(tracks => { + const result = { songs: [], privileges: [] }; + ids.map(id => { + const one = tracks.find(t => String(t.id) === id); + result.songs.push(one?.detail); + result.privileges.push(one?.privileges); + }); + if (result.songs.includes(undefined)) { + return undefined; + } + return result; + }); +} + +export function cacheLyric(id, lyrics) { + db.lyric.put({ + id, + lyrics, + updateTime: new Date().getTime(), + }); +} + +export function getLyricFromCache(id) { + return db.lyric.get(Number(id)).then(result => { + if (!result) return undefined; + return result.lyrics; + }); +} + +export function cacheAlbum(id, album) { + db.album.put({ + id: Number(id), + album, + updateTime: new Date().getTime(), + }); +} + +export function getAlbumFromCache(id) { + return db.album.get(Number(id)).then(result => { + if (!result) return undefined; + return result.album; + }); +} + +export function countDBSize() { + const trackSizes = []; + return db.trackSources + .each(track => { + trackSizes.push(track.source.byteLength); }) .then(() => { - return { + const res = { bytes: trackSizes.reduce((s1, s2) => s1 + s2, 0), length: trackSizes.length, }; - }) - .catch((err) => { - console.log(err); + tracksCacheBytes = res.bytes; + console.debug( + `[debug][db.js] load tracksCacheBytes: ${tracksCacheBytes}` + ); + return res; }); } -export function clearDB(dbName) { - let db = localforage.createInstance({ - name: dbName, +export function clearDB() { + return new Promise(resolve => { + db.tables.forEach(function (table) { + table.clear(); + }); + resolve(); }); - return db.clear(); } diff --git a/src/utils/filters.js b/src/utils/filters.js index 521bf14..5f3ed82 100644 --- a/src/utils/filters.js +++ b/src/utils/filters.js @@ -1,63 +1,77 @@ -import Vue from "vue"; -import dayjs from "dayjs"; -import duration from "dayjs/plugin/duration"; -import relativeTime from "dayjs/plugin/relativeTime"; -import locale from "@/locale"; +import Vue from 'vue'; +import dayjs from 'dayjs'; +import duration from 'dayjs/plugin/duration'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import locale from '@/locale'; -Vue.filter("formatTime", (Milliseconds, format = "HH:MM:SS") => { - if (!Milliseconds) return ""; +Vue.filter('formatTime', (Milliseconds, format = 'HH:MM:SS') => { + if (!Milliseconds) return ''; dayjs.extend(duration); dayjs.extend(relativeTime); let time = dayjs.duration(Milliseconds); let hours = time.hours().toString(); let mins = time.minutes().toString(); - let seconds = time.seconds().toString().padStart(2, "0"); + let seconds = time.seconds().toString().padStart(2, '0'); - if (format === "HH:MM:SS") { - return hours !== "0" - ? `${hours}:${mins.padStart(2, "0")}:${seconds}` + if (format === 'HH:MM:SS') { + return hours !== '0' + ? `${hours}:${mins.padStart(2, '0')}:${seconds}` : `${mins}:${seconds}`; - } else if (format === "Human") { - const hoursUnit = locale.locale === "zh-CN" ? "小时" : "hr"; - const minitesUnit = locale.locale === "zh-CN" ? "分钟" : "min"; - return hours !== "0" + } else if (format === 'Human') { + let hoursUnit, minitesUnit; + switch (locale.locale) { + case 'zh-CN': + hoursUnit = '小时'; + minitesUnit = '分钟'; + break; + case 'zh-TW': + hoursUnit = '小時'; + minitesUnit = '分鐘'; + break; + default: + hoursUnit = 'hr'; + minitesUnit = 'min'; + break; + } + return hours !== '0' ? `${hours} ${hoursUnit} ${mins} ${minitesUnit}` : `${mins} ${minitesUnit}`; } }); -Vue.filter("formatDate", (timestamp, format = "MMM D, YYYY") => { - if (!timestamp) return ""; - if (locale.locale === "zh-CN") format = "YYYY年MM月DD日"; +Vue.filter('formatDate', (timestamp, format = 'MMM D, YYYY') => { + if (!timestamp) return ''; + if (locale.locale === 'zh-CN') format = 'YYYY年MM月DD日'; + else if (locale.locale === 'zh-TW') format = 'YYYY年MM月DD日'; return dayjs(timestamp).format(format); }); -Vue.filter("formatAlbumType", (type, album) => { - if (!type) return ""; - if (type === "EP/Single") { - return album.size === 1 ? "Single" : "EP"; - } else if (type === "Single") { - return "Single"; - } else if (type === "专辑") { - return "Album"; +Vue.filter('formatAlbumType', (type, album) => { + if (!type) return ''; + if (type === 'EP/Single') { + return album.size === 1 ? 'Single' : 'EP'; + } else if (type === 'Single') { + return 'Single'; + } else if (type === '专辑') { + return 'Album'; } else { return type; } }); -Vue.filter("resizeImage", (imgUrl, size = 512) => { - if (!imgUrl) return ""; +Vue.filter('resizeImage', (imgUrl, size = 512) => { + if (!imgUrl) return ''; let httpsImgUrl = imgUrl; - if (imgUrl.slice(0, 5) !== "https") { - httpsImgUrl = "https" + imgUrl.slice(4); + if (imgUrl.slice(0, 5) !== 'https') { + httpsImgUrl = 'https' + imgUrl.slice(4); } return `${httpsImgUrl}?param=${size}y${size}`; }); -Vue.filter("formatPlayCount", (count) => { - if (!count) return ""; - if (locale.locale === "zh-CN") { +Vue.filter('formatPlayCount', count => { + if (!count) return ''; + if (locale.locale === 'zh-CN') { if (count > 100000000) { return `${Math.floor((count / 100000000) * 100) / 100}亿`; // 2.32 亿 } @@ -68,6 +82,17 @@ Vue.filter("formatPlayCount", (count) => { return `${Math.floor((count / 10000) * 100) / 100}万`; // 2.3 万 } return count; + } else if (locale.locale === 'zh-TW') { + if (count > 100000000) { + return `${Math.floor((count / 100000000) * 100) / 100}億`; // 2.32 億 + } + if (count > 100000) { + return `${Math.floor((count / 10000) * 10) / 10}萬`; // 232.1 萬 + } + if (count > 10000) { + return `${Math.floor((count / 10000) * 100) / 100}萬`; // 2.3 萬 + } + return count; } else { if (count > 10000000) { return `${Math.floor((count / 1000000) * 10) / 10}M`; // 233.2M @@ -82,7 +107,7 @@ Vue.filter("formatPlayCount", (count) => { } }); -Vue.filter("toHttps", (url) => { - if (!url) return ""; - return url.replace(/^http:/, "https:"); +Vue.filter('toHttps', url => { + if (!url) return ''; + return url.replace(/^http:/, 'https:'); }); diff --git a/src/utils/lyrics.js b/src/utils/lyrics.js index 01e00fd..b883f46 100644 --- a/src/utils/lyrics.js +++ b/src/utils/lyrics.js @@ -1,33 +1,113 @@ -// copy from https://github.com/sl1673495/vue-netease-music/blob/master/src/utils/lrcparse.js - export function lyricParser(lrc) { return { - lyric: parseLyric(lrc?.lrc?.lyric || ""), - tlyric: parseLyric(lrc?.tlyric?.lyric || ""), + lyric: parseLyric(lrc?.lrc?.lyric || ''), + tlyric: parseLyric(lrc?.tlyric?.lyric || ''), + romalyric: parseLyric(lrc?.romalrc?.lyric || ''), lyricuser: lrc.lyricUser, transuser: lrc.transUser, }; } -export function parseLyric(lrc) { - const lyrics = lrc.split("\n"); - const lrcObj = []; - for (let i = 0; i < lyrics.length; i++) { - const lyric = decodeURIComponent(lyrics[i]); - const timeReg = /\[\d*:\d*((\.|:)\d*)*\]/g; - const timeRegExpArr = lyric.match(timeReg); - if (!timeRegExpArr) continue; - const content = lyric.replace(timeReg, ""); - for (let k = 0, h = timeRegExpArr.length; k < h; k++) { - const t = timeRegExpArr[k]; - const min = Number(String(t.match(/\[\d*/i)).slice(1)); - const sec = Number(String(t.match(/:\d*/i)).slice(1)); - const ms = Number(t.match(/\d*\]/i)[0].slice(0, 2)) / 100; - const time = min * 60 + sec + ms; - if (content !== "") { - lrcObj.push({ time: time, rawTime: timeRegExpArr[0], content }); +// regexr.com/6e52n +const extractLrcRegex = + /^(?(?:\[.+?\])+)(?!\[)(?.+)$/gm; +const extractTimestampRegex = + /\[(?\d+):(?\d+)(?:\.|:)*(?\d+)*\]/g; + +/** + * @typedef {{time: number, rawTime: string, content: string}} ParsedLyric + */ + +/** + * Parse the lyric string. + * + * @param {string} lrc The `lrc` input. + * @returns {ParsedLyric[]} The parsed lyric. + * @example parseLyric("[00:00.00] Hello, World!\n[00:00.10] Test\n"); + */ +function parseLyric(lrc) { + /** + * A sorted list of parsed lyric and its timestamp. + * + * @type {ParsedLyric[]} + * @see binarySearch + */ + const parsedLyrics = []; + + /** + * Find the appropriate index to push our parsed lyric. + * @param {ParsedLyric} lyric + */ + const binarySearch = lyric => { + let time = lyric.time; + + let low = 0; + let high = parsedLyrics.length - 1; + + while (low <= high) { + const mid = Math.floor((low + high) / 2); + const midTime = parsedLyrics[mid].time; + if (midTime === time) { + return mid; + } else if (midTime < time) { + low = mid + 1; + } else { + high = mid - 1; } } + + return low; + }; + + for (const line of lrc.trim().matchAll(extractLrcRegex)) { + const { lyricTimestamps, content } = line.groups; + + for (const timestamp of lyricTimestamps.matchAll(extractTimestampRegex)) { + const { min, sec, ms } = timestamp.groups; + const rawTime = timestamp[0]; + const time = Number(min) * 60 + Number(sec) + Number(ms ?? 0) * 0.001; + + /** @type {ParsedLyric} */ + const parsedLyric = { rawTime, time, content: trimContent(content) }; + parsedLyrics.splice(binarySearch(parsedLyric), 0, parsedLyric); + } + } + + return parsedLyrics; +} + +/** + * @param {string} content + * @returns {string} + */ +function trimContent(content) { + let t = content.trim(); + return t.length < 1 ? content : t; +} + +/** + * @param {string} lyric + */ +export async function copyLyric(lyric) { + const textToCopy = lyric; + if (navigator.clipboard && navigator.clipboard.writeText) { + try { + await navigator.clipboard.writeText(textToCopy); + } catch (err) { + alert('复制失败,请手动复制!'); + } + } else { + const tempInput = document.createElement('textarea'); + tempInput.value = textToCopy; + tempInput.style.position = 'absolute'; + tempInput.style.left = '-9999px'; + document.body.appendChild(tempInput); + tempInput.select(); + try { + document.execCommand('copy'); + } catch (err) { + alert('复制失败,请手动复制!'); + } + document.body.removeChild(tempInput); } - return lrcObj; } diff --git a/src/utils/nativeAlert.js b/src/utils/nativeAlert.js new file mode 100644 index 0000000..123cc70 --- /dev/null +++ b/src/utils/nativeAlert.js @@ -0,0 +1,30 @@ +/** + * Returns an alert-like function that fits current runtime environment + * + * This function is amid to solve a electron bug on Windows, that, when + * user dismissed a browser alert, elements cannot be focused + * for further editing unless switching to another window and then back + * + * @returns { (message:string) => void } + * Built-in alert function for browser environment + * A function wrapping {@link dialog.showMessageBoxSync} for electron environment + * + * @see {@link https://github.com/electron/electron/issues/19977} for upstream electron issue + */ +const nativeAlert = (() => { + if (process.env.IS_ELECTRON === true) { + const { dialog } = require('electron'); + if (dialog) { + return message => { + var options = { + type: 'warning', + message, + }; + dialog.showMessageBoxSync(null, options); + }; + } + } + return alert; +})(); + +export default nativeAlert; diff --git a/src/utils/platform.js b/src/utils/platform.js new file mode 100644 index 0000000..c386d8a --- /dev/null +++ b/src/utils/platform.js @@ -0,0 +1,7 @@ +export const isWindows = process.platform === 'win32'; +export const isMac = process.platform === 'darwin'; +export const isLinux = process.platform === 'linux'; +export const isDevelopment = process.env.NODE_ENV === 'development'; + +export const isCreateTray = isWindows || isLinux || isDevelopment; +export const isCreateMpris = isLinux; diff --git a/src/utils/playList.js b/src/utils/playList.js new file mode 100644 index 0000000..d135fc3 --- /dev/null +++ b/src/utils/playList.js @@ -0,0 +1,61 @@ +import router from '../router'; +import state from '../store/state'; +import { + recommendPlaylist, + dailyRecommendPlaylist, + getPlaylistDetail, +} from '@/api/playlist'; +import { isAccountLoggedIn } from '@/utils/auth'; + +export function hasListSource() { + return !state.player.isPersonalFM && state.player.playlistSource.id !== 0; +} + +export function goToListSource() { + router.push({ path: getListSourcePath() }); +} + +export function getListSourcePath() { + if (state.player.playlistSource.id === state.data.likedSongPlaylistID) { + return '/library/liked-songs'; + } else if (state.player.playlistSource.type === 'url') { + return state.player.playlistSource.id; + } else if (state.player.playlistSource.type === 'cloudDisk') { + return '/library'; + } else { + return `/${state.player.playlistSource.type}/${state.player.playlistSource.id}`; + } +} + +export async function getRecommendPlayList(limit, removePrivateRecommand) { + if (isAccountLoggedIn()) { + const playlists = await Promise.all([ + dailyRecommendPlaylist(), + recommendPlaylist({ limit }), + ]); + let recommend = playlists[0].recommend ?? []; + if (recommend.length) { + if (removePrivateRecommand) recommend = recommend.slice(1); + await replaceRecommendResult(recommend); + } + return recommend.concat(playlists[1].result).slice(0, limit); + } else { + const response = await recommendPlaylist({ limit }); + return response.result; + } +} + +async function replaceRecommendResult(recommend) { + for (let r of recommend) { + if (specialPlaylist.indexOf(r.id) > -1) { + const data = await getPlaylistDetail(r.id, true); + const playlist = data.playlist; + if (playlist) { + r.name = playlist.name; + r.picUrl = playlist.coverImgUrl; + } + } + } +} + +const specialPlaylist = [3136952023, 2829883282, 2829816518, 2829896389]; diff --git a/src/utils/request.js b/src/utils/request.js index 76dba79..6ac7bc3 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -1,9 +1,11 @@ -import axios from "axios"; +import router from '@/router'; +import { doLogout, getCookie } from '@/utils/auth'; +import axios from 'axios'; -let baseURL = ""; +let baseURL = ''; // Web 和 Electron 跑在不同端口避免同时启动时冲突 if (process.env.IS_ELECTRON) { - if (process.env.NODE_ENV === "production") { + if (process.env.NODE_ENV === 'production') { baseURL = process.env.VUE_APP_ELECTRON_API_URL; } else { baseURL = process.env.VUE_APP_ELECTRON_API_URL_DEV; @@ -18,16 +20,79 @@ const service = axios.create({ timeout: 15000, }); +service.interceptors.request.use(function (config) { + if (!config.params) config.params = {}; + if (baseURL.length) { + if ( + baseURL[0] !== '/' && + !process.env.IS_ELECTRON && + getCookie('MUSIC_U') !== null + ) { + config.params.cookie = `MUSIC_U=${getCookie('MUSIC_U')};`; + } + } else { + console.error("You must set up the baseURL in the service's config"); + } + + if (!process.env.IS_ELECTRON && !config.url.includes('/login')) { + config.params.realIP = '211.161.244.70'; + } + + // Force real_ip + const enableRealIP = JSON.parse( + localStorage.getItem('settings') + ).enableRealIP; + const realIP = JSON.parse(localStorage.getItem('settings')).realIP; + if (process.env.VUE_APP_REAL_IP) { + config.params.realIP = process.env.VUE_APP_REAL_IP; + } else if (enableRealIP) { + config.params.realIP = realIP; + } + + const proxy = JSON.parse(localStorage.getItem('settings')).proxyConfig; + if (['HTTP', 'HTTPS'].includes(proxy.protocol)) { + config.params.proxy = `${proxy.protocol}://${proxy.server}:${proxy.port}`; + } + + return config; +}); + service.interceptors.response.use( - (response) => { + response => { const res = response.data; return res; }, - (error) => { - const errMsg = `error: ${error}`; - console.log(errMsg); + async error => { + /** @type {import('axios').AxiosResponse | null} */ + let response; + let data; + if (error === 'TypeError: baseURL is undefined') { + response = error; + data = error; + console.error("You must set up the baseURL in the service's config"); + } else if (error.response) { + response = error.response; + data = response.data; + } - return Promise.reject(error); + if ( + response && + typeof data === 'object' && + data.code === 301 && + data.msg === '需要登录' + ) { + console.warn('Token has expired. Logout now!'); + + // 登出帳戶 + doLogout(); + + // 導向登入頁面 + if (process.env.IS_ELECTRON === true) { + router.push({ name: 'loginAccount' }); + } else { + router.push({ name: 'login' }); + } + } } ); diff --git a/src/utils/shortcuts.js b/src/utils/shortcuts.js new file mode 100644 index 0000000..1a6582c --- /dev/null +++ b/src/utils/shortcuts.js @@ -0,0 +1,47 @@ +// default shortcuts +// for more info, check https://www.electronjs.org/docs/api/accelerator + +export default [ + { + id: 'play', + name: '播放/暂停', + shortcut: 'CommandOrControl+P', + globalShortcut: 'Alt+CommandOrControl+P', + }, + { + id: 'next', + name: '下一首', + shortcut: 'CommandOrControl+Right', + globalShortcut: 'Alt+CommandOrControl+Right', + }, + { + id: 'previous', + name: '上一首', + shortcut: 'CommandOrControl+Left', + globalShortcut: 'Alt+CommandOrControl+Left', + }, + { + id: 'increaseVolume', + name: '增加音量', + shortcut: 'CommandOrControl+Up', + globalShortcut: 'Alt+CommandOrControl+Up', + }, + { + id: 'decreaseVolume', + name: '减少音量', + shortcut: 'CommandOrControl+Down', + globalShortcut: 'Alt+CommandOrControl+Down', + }, + { + id: 'like', + name: '喜欢歌曲', + shortcut: 'CommandOrControl+L', + globalShortcut: 'Alt+CommandOrControl+L', + }, + { + id: 'minimize', + name: '隐藏/显示播放器', + shortcut: 'CommandOrControl+M', + globalShortcut: 'Alt+CommandOrControl+M', + }, +]; diff --git a/src/utils/staticData.js b/src/utils/staticData.js index 4e792c2..0105b7e 100644 --- a/src/utils/staticData.js +++ b/src/utils/staticData.js @@ -1,47 +1,51 @@ export const byAppleMusic = [ { coverImgUrl: - "http://p2.music.126.net/GvYQoflE99eoeGi9jG4Bsw==/109951165375336156.jpg", - name: "Happy Hits", + 'https://p2.music.126.net/GvYQoflE99eoeGi9jG4Bsw==/109951165375336156.jpg', + name: 'Happy Hits', id: 5278068783, }, { coverImgUrl: - "http://p2.music.126.net/5CJeYN35LnzRDsv5Lcs0-Q==/109951165374966765.jpg", - name: "\u4e2d\u563b\u5408\u74a7", + 'https://p2.music.126.net/5CJeYN35LnzRDsv5Lcs0-Q==/109951165374966765.jpg', + name: '\u4e2d\u563b\u5408\u74a7', id: 5277771961, }, - { coverImgUrl: - "http://p1.music.126.net/cPaBXr1wZSg86ddl47AK7Q==/109951165375130918.jpg", - name: "Heartbreak Pop", + 'https://p1.music.126.net/cPaBXr1wZSg86ddl47AK7Q==/109951165375130918.jpg', + name: 'Heartbreak Pop', id: 5277965913, }, { coverImgUrl: - "http://p2.music.126.net/FDtX55P2NjccDna-LBj9PA==/109951165375065973.jpg", - name: "Festival Bangers", + 'https://p2.music.126.net/FDtX55P2NjccDna-LBj9PA==/109951165375065973.jpg', + name: 'Festival Bangers', id: 5277969451, }, { coverImgUrl: - "http://p2.music.126.net/hC0q2dGbOWHVfg4nkhIXPg==/109951165374881177.jpg", - name: "Bedtime Beats", + 'https://p2.music.126.net/hC0q2dGbOWHVfg4nkhIXPg==/109951165374881177.jpg', + name: 'Bedtime Beats', id: 5277778542, }, ]; export const playlistCategories = [ { - name: "全部", + name: '全部', enable: true, - bigCat: "static", + bigCat: 'static', }, + // { + // name: "For You", + // enable: true, + // bigCat: "static", + // }, { - name: "推荐歌单", + name: '推荐歌单', enable: true, - bigCat: "static", + bigCat: 'static', }, // { // name: "最新专辑", @@ -49,368 +53,368 @@ export const playlistCategories = [ // bigCat: "static", // }, { - name: "精品歌单", + name: '精品歌单', enable: true, - bigCat: "static", + bigCat: 'static', }, { - name: "官方", + name: '官方', enable: true, - bigCat: "static", + bigCat: 'static', }, { - name: "排行榜", + name: '排行榜', enable: true, - bigCat: "static", + bigCat: 'static', }, { - name: "华语", + name: '华语', enable: false, - bigCat: "语种", + bigCat: '语种', }, { - name: "欧美", + name: '欧美', enable: true, - bigCat: "语种", + bigCat: '语种', }, { - name: "日语", + name: '日语', enable: false, - bigCat: "语种", + bigCat: '语种', }, { - name: "韩语", + name: '韩语', enable: false, - bigCat: "语种", + bigCat: '语种', }, { - name: "粤语", + name: '粤语', enable: false, - bigCat: "语种", + bigCat: '语种', }, { - name: "流行", + name: '流行', enable: true, - bigCat: "风格", + bigCat: '风格', }, { - name: "摇滚", + name: '摇滚', enable: true, - bigCat: "风格", + bigCat: '风格', }, { - name: "民谣", + name: '民谣', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "电子", + name: '电子', enable: true, - bigCat: "风格", + bigCat: '风格', }, { - name: "舞曲", + name: '舞曲', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "说唱", + name: '说唱', enable: true, - bigCat: "风格", + bigCat: '风格', }, { - name: "轻音乐", + name: '轻音乐', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "爵士", + name: '爵士', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "乡村", + name: '乡村', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "R&B/Soul", + name: 'R&B/Soul', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "古典", + name: '古典', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "民族", + name: '民族', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "英伦", + name: '英伦', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "金属", + name: '金属', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "朋克", + name: '朋克', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "蓝调", + name: '蓝调', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "雷鬼", + name: '雷鬼', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "世界音乐", + name: '世界音乐', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "拉丁", + name: '拉丁', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "New Age", + name: 'New Age', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "古风", + name: '古风', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "后摇", + name: '后摇', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "Bossa Nova", + name: 'Bossa Nova', enable: false, - bigCat: "风格", + bigCat: '风格', }, { - name: "清晨", + name: '清晨', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "夜晚", + name: '夜晚', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "学习", + name: '学习', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "工作", + name: '工作', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "午休", + name: '午休', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "下午茶", + name: '下午茶', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "地铁", + name: '地铁', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "驾车", + name: '驾车', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "运动", + name: '运动', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "旅行", + name: '旅行', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "散步", + name: '散步', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "酒吧", + name: '酒吧', enable: false, - bigCat: "场景", + bigCat: '场景', }, { - name: "怀旧", + name: '怀旧', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "清新", + name: '清新', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "浪漫", + name: '浪漫', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "伤感", + name: '伤感', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "治愈", + name: '治愈', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "放松", + name: '放松', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "孤独", + name: '孤独', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "感动", + name: '感动', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "兴奋", + name: '兴奋', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "快乐", + name: '快乐', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "安静", + name: '安静', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "思念", + name: '思念', enable: false, - bigCat: "情感", + bigCat: '情感', }, { - name: "综艺", + name: '综艺', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "影视原声", + name: '影视原声', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "ACG", + name: 'ACG', enable: true, - bigCat: "主题", + bigCat: '主题', }, { - name: "儿童", + name: '儿童', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "校园", + name: '校园', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "游戏", + name: '游戏', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "70后", + name: '70后', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "80后", + name: '80后', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "90后", + name: '90后', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "网络歌曲", + name: '网络歌曲', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "KTV", + name: 'KTV', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "经典", + name: '经典', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "翻唱", + name: '翻唱', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "吉他", + name: '吉他', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "钢琴", + name: '钢琴', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "器乐", + name: '器乐', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "榜单", + name: '榜单', enable: false, - bigCat: "主题", + bigCat: '主题', }, { - name: "00后", + name: '00后', enable: false, - bigCat: "主题", + bigCat: '主题', }, ]; diff --git a/src/utils/updateApp.js b/src/utils/updateApp.js index ec1b777..25124d7 100644 --- a/src/utils/updateApp.js +++ b/src/utils/updateApp.js @@ -1,62 +1,62 @@ -import initLocalStorage from "@/store/initLocalStorage.js"; -import pkg from "../../package.json"; +import initLocalStorage from '@/store/initLocalStorage.js'; +import pkg from '../../package.json'; const updateSetting = () => { - const parsedSettings = JSON.parse(localStorage.getItem("settings")); - const { - playlistCategories, - showUnavailableSongInGreyStyle, - automaticallyCacheSongs, - nyancatStyle, - showLyricsTranslation, - minimizeToTray, - } = initLocalStorage.settings; + const parsedSettings = JSON.parse(localStorage.getItem('settings')); const settings = { - playlistCategories, - showUnavailableSongInGreyStyle, - automaticallyCacheSongs, - nyancatStyle, - showLyricsTranslation, - minimizeToTray, + ...initLocalStorage.settings, ...parsedSettings, }; - localStorage.setItem("settings", JSON.stringify(settings)); + if ( + settings.shortcuts.length !== initLocalStorage.settings.shortcuts.length + ) { + // 当新增 shortcuts 时 + const oldShortcutsId = settings.shortcuts.map(s => s.id); + const newShortcutsId = initLocalStorage.settings.shortcuts.filter( + s => oldShortcutsId.includes(s.id) === false + ); + newShortcutsId.map(id => { + settings.shortcuts.push( + initLocalStorage.settings.shortcuts.find(s => s.id === id) + ); + }); + } + + if (localStorage.getItem('appVersion') === '"0.3.9"') { + settings.lyricsBackground = true; + } + + localStorage.setItem('settings', JSON.stringify(settings)); }; const updateData = () => { - const parsedData = JSON.parse(localStorage.getItem("data")); + const parsedData = JSON.parse(localStorage.getItem('data')); const data = { ...parsedData, }; - localStorage.setItem("data", JSON.stringify(data)); + localStorage.setItem('data', JSON.stringify(data)); }; const updatePlayer = () => { - let parsedData = JSON.parse(localStorage.getItem("player")); - let appVersion = localStorage.getItem("appVersion"); + let parsedData = JSON.parse(localStorage.getItem('player')); + let appVersion = localStorage.getItem('appVersion'); if (appVersion === `"0.2.5"`) parsedData = {}; // 0.2.6版本重构了player const data = { - _repeatMode: "off", - _shuffle: false, - _list: [], - _current: 0, - _playlistSource: {}, - _volume: 1, - _volumeBeforeMuted: 1, - _currentTrack: {}, - _playNextList: [], - _enabled: false, - _shuffledList: [], - _shuffledCurrent: 0, ...parsedData, }; - localStorage.setItem("player", JSON.stringify(data)); + localStorage.setItem('player', JSON.stringify(data)); +}; + +const removeOldStuff = () => { + // remove old indexedDB databases created by localforage + indexedDB.deleteDatabase('tracks'); }; export default function () { updateSetting(); updateData(); updatePlayer(); - localStorage.setItem("appVersion", JSON.stringify(pkg.version)); + removeOldStuff(); + localStorage.setItem('appVersion', JSON.stringify(pkg.version)); } diff --git a/src/views/album.vue b/src/views/album.vue index 0dd3f3a..acc8a40 100644 --- a/src/views/album.vue +++ b/src/views/album.vue @@ -1,21 +1,21 @@ diff --git a/src/views/artist.vue b/src/views/artist.vue index 9079f24..d825436 100644 --- a/src/views/artist.vue +++ b/src/views/artist.vue @@ -1,46 +1,57 @@ diff --git a/src/views/artistMV.vue b/src/views/artistMV.vue index 7dcd941..ba0a8c1 100644 --- a/src/views/artistMV.vue +++ b/src/views/artistMV.vue @@ -1,32 +1,39 @@ diff --git a/src/views/dailyTracks.vue b/src/views/dailyTracks.vue new file mode 100644 index 0000000..6f07b0a --- /dev/null +++ b/src/views/dailyTracks.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/views/explore.vue b/src/views/explore.vue index 0ac4aff..90624d0 100644 --- a/src/views/explore.vue +++ b/src/views/explore.vue @@ -1,15 +1,15 @@ diff --git a/src/views/home.vue b/src/views/home.vue index fd719b9..b166e7e 100644 --- a/src/views/home.vue +++ b/src/views/home.vue @@ -1,72 +1,88 @@ @@ -132,6 +160,9 @@ export default { .index-row { margin-top: 54px; } +.index-row.first-row { + margin-top: 32px; +} .playlists { display: flex; flex-wrap: wrap; @@ -164,4 +195,11 @@ footer { justify-content: center; margin-top: 48px; } + +.for-you-row { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 24px; + margin-bottom: 78px; +} diff --git a/src/views/lastfmCallback.vue b/src/views/lastfmCallback.vue new file mode 100644 index 0000000..b09f7b2 --- /dev/null +++ b/src/views/lastfmCallback.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/src/views/library.vue b/src/views/library.vue index ffc3fb9..da24af7 100644 --- a/src/views/library.vue +++ b/src/views/library.vue @@ -1,12 +1,11 @@ @@ -135,8 +166,9 @@ export default { --plyr-control-radius: 8px; } -.mv { +.mv-page { width: 100%; + margin-top: 32px; } .current-video { width: 100%; @@ -175,11 +207,15 @@ export default { font-weight: 600; color: var(--color-text); opacity: 0.88; + margin-bottom: 12px; } } -.like-button { +.buttons { display: inline-block; + .button { + display: inline-block; + } .svg-icon { height: 18px; width: 18px; diff --git a/src/views/newAlbum.vue b/src/views/newAlbum.vue index 890cca5..b120c4f 100644 --- a/src/views/newAlbum.vue +++ b/src/views/newAlbum.vue @@ -1,48 +1,39 @@ @@ -112,5 +119,26 @@ h1 { margin-bottom: 18px; cursor: default; color: var(--color-text); + display: flex; + justify-content: space-between; + button { + color: var(--color-text); + border-radius: 8px; + padding: 0 14px; + display: flex; + justify-content: center; + align-items: center; + transition: 0.2s; + opacity: 0.68; + font-weight: 500; + &:hover { + opacity: 1; + background: var(--color-secondary-bg); + } + &:active { + opacity: 1; + transform: scale(0.92); + } + } } diff --git a/src/views/playlist.vue b/src/views/playlist.vue index 6377cb6..f735b84 100644 --- a/src/views/playlist.vue +++ b/src/views/playlist.vue @@ -1,40 +1,36 @@ diff --git a/src/views/search.vue b/src/views/search.vue index b3ff967..df000a7 100644 --- a/src/views/search.vue +++ b/src/views/search.vue @@ -1,81 +1,83 @@ @@ -235,7 +246,7 @@ export default { .row { display: flex; flex-wrap: wrap; - margin-top: 98px; + margin-top: 32px; .artists { flex: 1; diff --git a/src/views/searchType.vue b/src/views/searchType.vue index 055e824..2a6b9f1 100644 --- a/src/views/searchType.vue +++ b/src/views/searchType.vue @@ -1,57 +1,58 @@