[
  {
    "path": ".dockerignore",
    "content": "node_modules\nnpm-debug.log\nDockerfile*\ndocker-compose*\n.dockerignore\n.git\n.github\n.gitignore\nREADME.md\nLICENSE\n.vscode\ndist\ndist_electron\nbuild\nimages\nscript"
  },
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = false\ninsert_final_newline = false"
  },
  {
    "path": ".envrc",
    "content": "source_url \"https://raw.githubusercontent.com/cachix/devenv/82c0147677e510b247d8b9165c54f73d32dfd899/direnvrc\" \"sha256-7u4iDd1nZpxL4tCzmPG0dQgC5V+/44Ba+tHkPob1v2k=\"\n\nexport NIXPKGS_ALLOW_INSECURE=1\nuse devenv\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text eol=lf\n# Denote all files that are truly binary and should not be modified.\n*.png binary\n*.jpg binary\n*.mp3 binary\n*.icns binary\n*.gif binary\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/----------.md",
    "content": "---\nname: 反馈问题或请求新功能\nabout: bug & feature\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n# 尽量每个 issue 只提一个 bug 或新功能\n\n### 提新 issue 前请确认 👉\n\n- 没人提过这个 issue（[这里看所有 issue](https://github.com/qier222/YesPlayMusic/issues)）\n- 项目的 Todo 里没有与你 issue 相关的内容（[这里看 Todo](https://github.com/qier222/YesPlayMusic/projects/1)）\n\n### 反馈 bug 需要的信息\n\n- 用的是网页版还是客户端\n- 浏览器名称或电脑操作系统\n- 控制台 Console 页面的截图（按 F12 可打开控制台）\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: Release\n\nenv:\n  YARN_INSTALL_NOPT: yarn add --ignore-platform --ignore-optional\n\non:\n  push:\n    branches:\n      - master\n    tags:\n      - v*\n  workflow_dispatch:\n\njobs:\n  release:\n    runs-on: ${{ matrix.os }}\n    env:\n      SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.snapcraft_token }}\n\n    strategy:\n      matrix:\n        os: [macos-latest, windows-latest, ubuntu-22.04]\n\n    steps:\n      - name: Check out Git repository\n        uses: actions/checkout@v3\n        with:\n          submodules: 'recursive'\n\n      - name: Install Node.js, NPM and Yarn\n        uses: actions/setup-node@v3\n        with:\n          node-version: 16\n          cache: 'yarn'\n\n      - name: Install RPM & Pacman (on Ubuntu)\n        if: runner.os == 'Linux'\n        run: |\n          sudo apt-get update &&\n          sudo apt-get install --no-install-recommends -y rpm &&\n          sudo apt-get install --no-install-recommends -y libarchive-tools &&\n          sudo apt-get install --no-install-recommends -y libopenjp2-tools\n\n      - name: Install Snapcraft (on Ubuntu)\n        uses: samuelmeuli/action-snapcraft@v3\n        if: startsWith(matrix.os, 'ubuntu')\n\n      - id: get_unm_version\n        name: Get the installed UNM version\n        run: |\n          yarn --ignore-optional\n          unm_version=$(node -e \"console.log(require('./node_modules/@unblockneteasemusic/rust-napi/package.json').version)\")\n          echo \"::set-output name=unmver::${unm_version}\"\n        shell: bash\n\n      - name: Install UNM dependencies for Windows\n        if: runner.os == 'Windows'\n        run: |\n          ${{ env.YARN_INSTALL_NOPT }} \\\n            @unblockneteasemusic/rust-napi-win32-x64-msvc@${{steps.get_unm_version.outputs.unmver}}\n        shell: bash\n\n      - name: Install UNM dependencies for macOS\n        if: runner.os == 'macOS'\n        run: |\n          ${{ env.YARN_INSTALL_NOPT }} \\\n            @unblockneteasemusic/rust-napi-darwin-x64@${{steps.get_unm_version.outputs.unmver}} \\\n            @unblockneteasemusic/rust-napi-darwin-arm64@${{steps.get_unm_version.outputs.unmver}} \\\n            dmg-license\n        shell: bash\n\n      - name: Install UNM dependencies for Linux\n        if: runner.os == 'Linux'\n        run: |\n          ${{ env.YARN_INSTALL_NOPT }} \\\n            @unblockneteasemusic/rust-napi-linux-x64-gnu@${{steps.get_unm_version.outputs.unmver}} \\\n            @unblockneteasemusic/rust-napi-linux-arm64-gnu@${{steps.get_unm_version.outputs.unmver}} \\\n            @unblockneteasemusic/rust-napi-linux-arm-gnueabihf@${{steps.get_unm_version.outputs.unmver}}\n        shell: bash\n\n      - name: Build/release Electron app\n        uses: samuelmeuli/action-electron-builder@v1.6.0\n        env:\n          VUE_APP_NETEASE_API_URL: /api\n          VUE_APP_ELECTRON_API_URL: /api\n          VUE_APP_ELECTRON_API_URL_DEV: http://127.0.0.1:10754\n          VUE_APP_LASTFM_API_KEY: 09c55292403d961aa517ff7f5e8a3d9c\n          VUE_APP_LASTFM_API_SHARED_SECRET: 307c9fda32b3904e53654baff215cb67\n        with:\n          # GitHub token, automatically provided to the action\n          # (No need to define this secret in the repo settings)\n          github_token: ${{ secrets.github_token }}\n\n          # If the commit is tagged with a version (e.g. \"v1.0.0\"),\n          # release the app after building\n          release: ${{ startsWith(github.ref, 'refs/tags/v') }}\n\n          use_vue_cli: true\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: YesPlayMusic-mac\n          path: dist_electron/*-universal.dmg\n          if-no-files-found: ignore\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: YesPlayMusic-win\n          path: dist_electron/*Setup*.exe\n          if-no-files-found: ignore\n\n      - uses: actions/upload-artifact@v4\n        with:\n          name: YesPlayMusic-linux\n          path: dist_electron/*.AppImage\n          if-no-files-found: ignore\n"
  },
  {
    "path": ".github/workflows/sync.yml",
    "content": "name: Upstream Sync\n\npermissions:\n  contents: write\n  issues: write\n  actions: write\n\non:\n  schedule:\n    - cron: '0 * * * *' # every hour\n  workflow_dispatch:\n\njobs:\n  sync_latest_from_upstream:\n    name: Sync latest commits from upstream repo\n    runs-on: ubuntu-latest\n    if: ${{ github.event.repository.fork }}\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - name: Clean issue notice\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'close-issues'\n          labels: '🚨 Sync Fail'\n\n      - name: Sync upstream changes\n        id: sync\n        uses: aormsby/Fork-Sync-With-Upstream-action@v3.4\n        with:\n          upstream_sync_repo: qier222/YesPlayMusic\n          upstream_sync_branch: master\n          target_sync_branch: master\n          target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set\n          test_mode: false\n\n      - name: Sync check\n        if: failure()\n        uses: actions-cool/issues-helper@v3\n        with:\n          actions: 'create-issue'\n          title: '🚨 同步失败 | Sync Fail'\n          labels: '🚨 Sync Fail'\n          body: |\n            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.\n\n            由于上游仓库的 workflow 文件变更，导致 GitHub 自动暂停了本次自动更新，你需要手动 Sync Fork 一次。\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n# local env files\n.env\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n\n.vercel\n\n#Electron-builder output\n/dist_electron\nNeteaseCloudMusicApi-master\nNeteaseCloudMusicApi-master.zip\n\n# Local Netlify folder\n.netlify\nvercel.json\n# Devenv\n.devenv*\ndevenv.local.nix\n\n# direnv\n.direnv\n\n# pre-commit\n.pre-commit-config.yaml\n"
  },
  {
    "path": ".nvmrc",
    "content": "14"
  },
  {
    "path": ".prettierignore",
    "content": "build\ncoverage\ndist\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"trailingComma\": \"es5\",\n  \"tabWidth\": 2,\n  \"useTabs\": false,\n  \"semi\": true,\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": true,\n  \"arrowParens\": \"avoid\",\n  \"endOfLine\": \"lf\",\n  \"bracketSpacing\": true,\n  \"htmlWhitespaceSensitivity\": \"strict\"\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:16.13.1-alpine AS build\nENV VUE_APP_NETEASE_API_URL=/api\nWORKDIR /app\nRUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories &&\\\n\tapk add --no-cache python3 make g++ git\nCOPY package.json yarn.lock ./\nRUN yarn config set electron_mirror https://npmmirror.com/mirrors/electron/ && \\\n    yarn config set registry https://registry.npmmirror.com && \\\n    sed -i 's/registry.yarnpkg.com/registry.npmmirror.com/g' yarn.lock && \\\n    sed -i 's/registry.npmjs.org/registry.npmmirror.com/g' yarn.lock && \\\n    yarn install\nCOPY . .\nRUN yarn build\n\nFROM nginx:1.20.2-alpine AS app\n\nCOPY --from=build /app/package.json /usr/local/lib/\n\nRUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \\\n  && apk add --no-cache libuv nodejs npm \\\n  && npm config set registry https://registry.npmmirror.com \\\n  && npm i -g $(awk -F \\\" '{if($2==\"@neteaseapireborn/api@latest\") print $2\"@\"$4}' /usr/local/lib/package.json) \\\n  && rm -f /usr/local/lib/package.json\n\nCOPY --from=build /app/docker/nginx.conf.example /etc/nginx/conf.d/default.conf\nCOPY --from=build /app/dist /usr/share/nginx/html\n\nCMD [\"sh\", \"-c\", \"nginx && exec npx @neteaseapireborn/api@latest\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020-2023 qier222\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n\t<a href=\"http://go.warp.dev/YesPlayMusic\" target=\"_blank\">\n\t\t<sup>Special thanks to:</sup>\n\t\t<br>\n\t\t<img alt=\"Warp sponsorship\" width=\"400\" src=\"https://github.com/warpdotdev/brand-assets/blob/main/Github/Sponsor/Warp-Github-LG-03.png?raw=true\">\n\t\t<br>\n\t\t<h>Warp is built for coding with multiple AI agents</b>\n\t\t<br>\n\t\t<sup>Available for macOS, Linux and Windows</sup>\n\t</a>\n</div>\n\n<br>\n\n---\n\n<br />\n<p align=\"center\">\n  <a href=\"https://music.qier222.com\" target=\"blank\">\n    <img src=\"images/logo.png\" alt=\"Logo\" width=\"156\" height=\"156\">\n  </a>\n  <h2 align=\"center\" style=\"font-weight: 600\">YesPlayMusic</h2>\n\n  <p align=\"center\">\n    高颜值的第三方网易云播放器\n    <br />\n    <a href=\"https://music.qier222.com\" target=\"blank\"><strong>🌎 访问DEMO</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"#%EF%B8%8F-安装\" target=\"blank\"><strong>📦️ 下载安装包</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;\n    <a href=\"https://t.me/yesplaymusic\" target=\"blank\"><strong>💬 加入交流群</strong></a>\n    <br />\n    <br />\n  </p>\n</p>\n\n[![Library][library-screenshot]](https://music.qier222.com)\n\n## 全新版本\n\n全新 2.0 Alpha 测试版已发布，欢迎前往 [Releases](https://github.com/qier222/YesPlayMusic/releases) 页面下载。\n当前版本将会进入维护模式，除重大 bug 修复外，不会再更新新功能。\n\n## ✨ 特性\n\n- ✅ 使用 Vue.js 全家桶开发\n- 🔴 网易云账号登录（扫码/手机/邮箱登录）\n- 📺 支持 MV 播放\n- 📃 支持歌词显示\n- 📻 支持私人 FM / 每日推荐歌曲\n- 🚫🤝 无任何社交功能\n- 🌎️ 海外用户可直接播放（需要登录网易云账号）\n- 🔐 支持 [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server#音源清单)，自动使用[各类音源](https://github.com/UnblockNeteaseMusic/server#音源清单)替换变灰歌曲链接 （网页版不支持）\n  - 「各类音源」指默认启用的音源。\n  - YouTube 音源需自行安装 `yt-dlp`。\n- ~~✔️ 每日自动签到（手机端和电脑端同时签到）~~\n- 🌚 Light/Dark Mode 自动切换\n- 👆 支持 Touch Bar\n- 🖥️ 支持 PWA，可在 Chrome/Edge 里点击地址栏右边的 ➕ 安装到电脑\n- 🟥 支持 Last.fm Scrobble\n- ☁️ 支持音乐云盘\n- ⌨️ 自定义快捷键和全局快捷键\n- 🎧 支持 Mpris\n- 🛠 更多特性开发中\n\n## 📦️ 安装\n\nElectron 版本由 [@hawtim](https://github.com/hawtim) 和 [@qier222](https://github.com/qier222) 适配并维护，支持 macOS、Windows、Linux。\n\n访问本项目的 [Releases](https://github.com/qier222/YesPlayMusic/releases)\n页面下载安装包。\n\n- macOS 用户可以通过 Homebrew 来安装：`brew install --cask yesplaymusic`\n\n- Windows 用户可以通过 Scoop 来安装：`scoop install extras/yesplaymusic`\n\n## 同类项目（排名无先后）\n\n欢迎大家通过 PR 分享你的项目，让更多人看到！\n\n- [algerkong/AlgerMusicPlayer](https://github.com/algerkong/AlgerMusicPlayer)\n- [asxez/MusicBox](https://github.com/asxez/MusicBox)\n- [lianchengwu/wmplayer](https://github.com/lianchengwu/wmplayer)\n\n## ⚙️ 部署至 Vercel\n\n除了下载安装包使用，你还可以将本项目部署到 Vercel 或你的服务器上。下面是部署到 Vercel 的方法。\n\n本项目的 Demo (https://music.qier222.com) 就是部署在 Vercel 上的网站。\n\n[![Powered by Vercel](https://www.datocms-assets.com/31049/1618983297-powered-by-vercel.svg)](https://vercel.com/?utm_source=ohmusic&utm_campaign=oss)\n\n1. 部署网易云 API，详情参见 [Binaryify/NeteaseCloudMusicApi](https://neteasecloudmusicapi.vercel.app/#/?id=%e5%ae%89%e8%a3%85)\n   。你也可以将 API 部署到 Vercel。\n\n2. 点击本仓库右上角的 Fork，复制本仓库到你的 GitHub 账号。\n\n3. 点击仓库的 Add File，选择 Create new file，输入 `vercel.json`，将下面的内容复制粘贴到文件中，并将 `https://your-netease-api.example.com` 替换为你刚刚部署的网易云 API 地址：\n\n```json\n{\n  \"rewrites\": [\n    {\n      \"source\": \"/api/:match*\",\n      \"destination\": \"https://your-netease-api.example.com/:match*\"\n    }\n  ]\n}\n```\n\n4. 打开 [Vercel.com](https://vercel.com)，使用 GitHub 登录。\n\n5. 点击 Import Git Repository 并选择你刚刚复制的仓库并点击 Import。\n\n6. 点击 PERSONAL ACCOUNT 旁边的 Select。\n\n7. 点击 Environment Variables，填写 Name 为 `VUE_APP_NETEASE_API_URL`，Value 为 `/api`，点击 Add。最后点击底部的 Deploy 就可以部署到\n   Vercel 了。\n\n## ⚙️ 部署到自己的服务器\n\n除了部署到 Vercel，你还可以部署到自己的服务器上\n\n1. 部署网易云 API，详情参见 [Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)\n2. 克隆本仓库\n\n```sh\ngit clone --recursive https://github.com/qier222/YesPlayMusic.git\n```\n\n3. 安装依赖\n\n```sh\nyarn install\n\n```\n\n4. （可选）使用 Nginx 反向代理 API，将 API 路径映射为 `/api`，如果 API 和网页不在同一个域名下的话（跨域），会有一些 bug。\n\n5. 复制 `/.env.example` 文件为 `/.env`，修改里面 `VUE_APP_NETEASE_API_URL` 的值为网易云 API 地址。本地开发的话可以填写 API 地址为 `http://localhost:3000`，YesPlayMusic 地址为 `http://localhost:8080`。如果你使用了反向代理 API，可以填写 API 地址为 `/api`。\n\n```\nVUE_APP_NETEASE_API_URL=http://localhost:3000\n```\n\n6. 编译打包\n\n```sh\nyarn run build\n```\n\n7. 将 `/dist` 目录下的文件上传到你的 Web 服务器\n\n## ⚙️ 宝塔面板 docker 应用商店 部署\n\n1. 安装宝塔面板，前往[宝塔面板官网](https://www.bt.cn/new/download.html) ，选择正式版的脚本下载安装。\n\n2. 安装后登录宝塔面板，在左侧导航栏中点击 Docker，首次进入会提示安装 Docker 服务，点击立即安装，按提示完成安装\n\n3. 安装完成后在应用商店中找到 YesPlayMusic，点击安装，配置域名、端口等基本信息即可完成安装。\n\n4. 安装后在浏览器输入上一步骤设置的域名即可访问。\n\n## ⚙️ Docker 部署\n\n1. 构建 Docker Image\n\n```sh\ndocker build -t yesplaymusic .\n```\n\n2. 启动 Docker Container\n\n```sh\ndocker run -d --name YesPlayMusic -p 80:80 yesplaymusic\n```\n\n3. Docker Compose 启动\n\n```sh\ndocker-compose up -d\n```\n\nYesPlayMusic 地址为 `http://localhost`\n\n## ⚙️ 部署至 Replit\n\n1. 新建 Repl，选择 Bash 模板\n\n2. 在 Replit shell 中运行以下命令\n\n```sh\nbash <(curl -s -L https://raw.githubusercontent.com/qier222/YesPlayMusic/main/install-replit.sh)\n```\n\n3. 首次运行成功后，只需点击绿色按钮 `Run` 即可再次运行\n\n4. 由于 replit 个人版限制内存为 1G（教育版为 3G），构建过程中可能会失败，请再次运行上述命令或运行以下命令：\n\n```sh\ncd /home/runner/${REPL_SLUG}/music && yarn install && yarn run build\n```\n\n## 👷‍♂️ 打包客户端\n\n如果在 Release 页面没有找到适合你的设备的安装包的话，你可以根据下面的步骤来打包自己的客户端。\n\n1. 打包 Electron 需要用到 Node.js 和 Yarn。可前往 [Node.js 官网](https://nodejs.org/zh-cn/) 下载安装包。安装 Node.js\n   后可在终端里执行 `npm install -g yarn` 来安装 Yarn。\n\n2. 使用 `git clone --recursive https://github.com/qier222/YesPlayMusic.git` 克隆本仓库到本地。\n\n3. 使用 `yarn install` 安装项目依赖。\n\n4. 复制 `/.env.example` 文件为 `/.env` 。\n\n5. 选择下列表格的命令来打包适合的你的安装包，打包出来的文件在 `/dist_electron` 目录下。了解更多信息可访问 [electron-builder 文档](https://www.electron.build/cli)\n\n| 命令                                       | 说明                      |\n| ------------------------------------------ | ------------------------- |\n| `yarn electron:build --windows nsis:ia32`  | Windows 32 位             |\n| `yarn electron:build --windows nsis:arm64` | Windows ARM               |\n| `yarn electron:build --linux deb:armv7l`   | Debian armv7l（树莓派等） |\n| `yarn electron:build --macos dir:arm64`    | macOS ARM                 |\n\n## :computer: 配置开发环境\n\n本项目由 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 提供 API。\n\n运行本项目\n\n```shell\n# 安装依赖\nyarn install\n\n# 创建本地环境变量\ncp .env.example .env\n\n# 运行（网页端）\nyarn serve\n\n# 运行（electron）\nyarn electron:serve\n```\n\n本地运行 NeteaseCloudMusicApi，或者将 API [部署至 Vercel](#%EF%B8%8F-部署至-vercel)\n\n```shell\n# 运行 API （默认 3000 端口）\nyarn netease_api:run\n```\n\n## ☑️ Todo\n\n查看 Todo 请访问本项目的 [Projects](https://github.com/qier222/YesPlayMusic/projects/1)\n\n欢迎提 Issue 和 Pull request。\n\n## 📜 开源许可\n\n本项目仅供个人学习研究使用，禁止用于商业及非法用途。\n\n基于 [MIT license](https://opensource.org/licenses/MIT) 许可进行开源。\n\n## 灵感来源\n\nAPI 源代码来自 [Binaryify/NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)\n\n- [Apple Music](https://music.apple.com)\n- [YouTube Music](https://music.youtube.com)\n- [Spotify](https://www.spotify.com)\n- [网易云音乐](https://music.163.com)\n\n## 🖼️ 截图\n\n![lyrics][lyrics-screenshot]\n![library-dark][library-dark-screenshot]\n![album][album-screenshot]\n![home-2][home-2-screenshot]\n![artist][artist-screenshot]\n![search][search-screenshot]\n![home][home-screenshot]\n![explore][explore-screenshot]\n\n<!-- MARKDOWN LINKS & IMAGES -->\n<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->\n\n[album-screenshot]: images/album.png\n[artist-screenshot]: images/artist.png\n[explore-screenshot]: images/explore.png\n[home-screenshot]: images/home.png\n[home-2-screenshot]: images/home-2.png\n[lyrics-screenshot]: images/lyrics.png\n[library-screenshot]: images/library.png\n[library-dark-screenshot]: images/library-dark.png\n[search-screenshot]: images/search.png\n"
  },
  {
    "path": "babel.config.js",
    "content": "module.exports = {\n  presets: [\n    [\n      '@vue/cli-plugin-babel/preset',\n      {\n        useBuiltIns: 'usage',\n        shippedProposals: true,\n      },\n    ],\n  ],\n};\n"
  },
  {
    "path": "devenv.nix",
    "content": "{ pkgs, lib, config, inputs, ... }:\n\nlet\n  nodejs16 = import inputs.nodejs16 { system = pkgs.stdenv.system; };\nin\n{\n  # https://devenv.sh/basics/\n  env.GREET = \"devenv\";\n\n  # https://devenv.sh/packages/\n  packages = [ pkgs.git ] ++ lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk; [\n    frameworks.AppKit\n  ]);\n\n  # https://devenv.sh/languages/\n  languages.javascript.enable = true;\n  languages.javascript.package = nodejs16.pkgs.nodejs_16;\n  languages.javascript.corepack.enable = true;\n  # languages.rust.enable = true;\n\n  # https://devenv.sh/processes/\n  # processes.cargo-watch.exec = \"cargo-watch\";\n\n  # https://devenv.sh/services/\n  # services.postgres.enable = true;\n\n  # https://devenv.sh/scripts/\n  scripts.hello.exec = ''\n    echo hello from $GREET\n  '';\n\n  enterShell = ''\n    hello\n    git --version\n  '';\n\n  # https://devenv.sh/tasks/\n  # tasks = {\n  #   \"myproj:setup\".exec = \"mytool build\";\n  #   \"devenv:enterShell\".after = [ \"myproj:setup\" ];\n  # };\n\n  # https://devenv.sh/tests/\n  enterTest = ''\n    echo \"Running tests\"\n    git --version | grep --color=auto \"${pkgs.git.version}\"\n  '';\n\n  # https://devenv.sh/pre-commit-hooks/\n  # pre-commit.hooks.shellcheck.enable = true;\n\n  # See full reference at https://devenv.sh/reference/options/\n}\n"
  },
  {
    "path": "devenv.yaml",
    "content": "# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json\ninputs:\n  nixpkgs:\n    url: github:nixos/nixpkgs/nixpkgs-unstable\n  nodejs16:\n    url: github:nixos/nixpkgs/a71323f68d4377d12c04a5410e214495ec598d4c\n\n# https://github.com/cachix/devenv/issues/792#issuecomment-2043166453\nimpure: true\n# If you're using non-OSS software, you can set allowUnfree to true.\n# allowUnfree: true\n\n# If you're willing to use a package that's vulnerable\n# permittedInsecurePackages:\n#  - \"openssl-1.1.1w\"\n\n# If you have more than one devenv you can merge them\n#imports:\n# - ./backend\n"
  },
  {
    "path": "docker/nginx.conf.example",
    "content": "server {\n  gzip on;\n  listen       80;\n  listen  [::]:80;\n  server_name  localhost;\n\n  location / {\n    root      /usr/share/nginx/html;\n    index     index.html;\n    try_files $uri $uri/ /index.html;\n  }\n\n  location @rewrites {\n    rewrite ^(.*)$ /index.html last;\n  }\n\n  location /api/ {\n    proxy_buffers           16 32k;\n    proxy_buffer_size       128k;\n    proxy_busy_buffers_size 128k;\n    proxy_set_header        Host $host;\n    proxy_set_header        X-Real-IP $remote_addr;\n    proxy_set_header        X-Forwarded-For $remote_addr;\n    proxy_set_header        X-Forwarded-Host $remote_addr;\n    proxy_set_header        X-NginX-Proxy true;\n    proxy_pass              http://localhost:3000/;\n  }\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  YesPlayMusic:\n    build:\n      context: .\n    image: yesplaymusic\n    container_name: YesPlayMusic\n    volumes:\n      - /etc/localtime:/etc/localtime:ro\n      - /etc/timezone:/etc/timezone:ro\n      - ./docker/nginx.conf.example:/etc/nginx/conf.d/default.conf:ro\n    ports:\n      - 80:80\n    restart: always\n    depends_on:\n      - UnblockNeteaseMusic\n    environment:\n      - NODE_TLS_REJECT_UNAUTHORIZED=0\n    networks:\n      my_network:\n\n  UnblockNeteaseMusic:\n    image: pan93412/unblock-netease-music-enhanced\n    command: -o kugou kuwo migu bilibili pyncmd -p 80:443 -f 45.127.129.53 -e -\n    # environment:\n    #   JSON_LOG: true\n    #   LOG_LEVEL: debug\n    networks:\n      my_network:\n        aliases:\n          - music.163.com\n          - interface.music.163.com\n          - interface3.music.163.com\n          - interface.music.163.com.163jiasu.com\n          - interface3.music.163.com.163jiasu.com\n    restart: always\n\nnetworks:\n  my_network:\n    driver: bridge\n"
  },
  {
    "path": "install-replit.sh",
    "content": " #!/usr/bin/bash\n\n# 初始化 .replit 和 replit.nix\nif [[ $1 == i ]];then\n    echo -e 'run = [\"bash\", \"main.sh\"]\\n\\nentrypoint = \"main.sh\"' >.replit\n    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\n    exit\nfi\n\n# 安装\nif [[ ! -d api ]];then\n    mkdir api\n    git clone https://github.com/neteasecloudmusicapienhanced/api-enhanced.git ./api &&  \\\n    cd api && npm install && cd ..\nfi\n\nif [[ ! -d music ]];then\n    mkdir music\n    git clone https://github.com/qier222/YesPlayMusic ./music && \\\n    cd music && cp .env.example .env && npm install --force && npm run build && cd ..\nfi\n\n# 启动\nPID=`ps -ef | grep npm | awk '{print $2}' | sed '$d'`\n\nif [[ ! -z ${PID} ]];then echo $PID | xargs kill;fi\nnohup bash -c 'cd api && PORT=35216 node app.js' > api.log 2>&1\nnohup bash -c 'npx serve music/dist/' > music.log 2>&1\n"
  },
  {
    "path": "jsconfig.json",
    "content": "{\n  // 支持 @ 的别名解析\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n    \"target\": \"ES6\",\n    \"module\": \"commonjs\",\n    \"allowSyntheticDefaultImports\": true,\n    \"jsx\": \"preserve\"\n  },\n  \"include\": [\"src/**/*\"],\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"yesplaymusic\",\n  \"version\": \"0.4.10\",\n  \"private\": true,\n  \"description\": \"A third party music player for Netease Music\",\n  \"author\": \"qier222<qier222@outlook.com>\",\n  \"scripts\": {\n    \"serve\": \"vue-cli-service serve\",\n    \"build\": \"vue-cli-service build\",\n    \"lint\": \"vue-cli-service lint\",\n    \"electron:build\": \"vue-cli-service electron:build -p never\",\n    \"electron:build-all\": \"vue-cli-service electron:build -p never -mwl\",\n    \"electron:build-mac\": \"vue-cli-service electron:build -p never -m\",\n    \"electron:build-win\": \"vue-cli-service electron:build -p never -w\",\n    \"electron:build-linux\": \"vue-cli-service electron:build -p never -l\",\n    \"electron:serve\": \"vue-cli-service electron:serve\",\n    \"electron:buildicon\": \"electron-icon-builder --input=./build/icons/icon.png --output=build --flatten\",\n    \"electron:publish\": \"vue-cli-service electron:build -mwl -p always\",\n    \"postinstall\": \"electron-builder install-app-deps\",\n    \"postuninstall\": \"electron-builder install-app-deps\",\n    \"prettier\": \"npx prettier --write ./src\",\n    \"netease_api:run\": \"npx @neteaseapireborn/api\"\n  },\n  \"main\": \"background.js\",\n  \"engines\": {\n    \"node\": \"14 || 16\"\n  },\n  \"dependencies\": {\n    \"@neteaseapireborn/api\": \"^4.29.7\",\n    \"@unblockneteasemusic/rust-napi\": \"^0.4.0\",\n    \"axios\": \"^0.26.1\",\n    \"change-case\": \"^4.1.2\",\n    \"cli-color\": \"^2.0.0\",\n    \"color\": \"^4.2.3\",\n    \"core-js\": \"^3.6.5\",\n    \"crypto-js\": \"^4.0.0\",\n    \"dayjs\": \"^1.8.36\",\n    \"dexie\": \"^3.0.3\",\n    \"discord-rich-presence\": \"^0.0.8\",\n    \"electron\": \"^13.6.7\",\n    \"electron-builder\": \"^23.0.0\",\n    \"electron-context-menu\": \"^3.1.2\",\n    \"electron-debug\": \"^3.1.0\",\n    \"electron-devtools-installer\": \"^3.2\",\n    \"electron-icon-builder\": \"^2.0.1\",\n    \"electron-is-dev\": \"^2.0.0\",\n    \"electron-log\": \"^4.3.0\",\n    \"electron-store\": \"^8.0.1\",\n    \"electron-updater\": \"^5.0.1\",\n    \"esbuild\": \"^0.20.1\",\n    \"esbuild-loader\": \"^4.0.3\",\n    \"express\": \"^4.17.1\",\n    \"express-fileupload\": \"^1.2.0\",\n    \"express-http-proxy\": \"^1.6.2\",\n    \"extract-zip\": \"^2.0.1\",\n    \"howler\": \"^2.2.3\",\n    \"js-cookie\": \"^2.2.1\",\n    \"jsbi\": \"^4.1.0\",\n    \"lodash\": \"^4.17.20\",\n    \"md5\": \"^2.3.0\",\n    \"mpris-service\": \"^2.1.2\",\n    \"music-metadata\": \"^7.5.3\",\n    \"node-vibrant\": \"^3.2.1-alpha.1\",\n    \"nprogress\": \"^0.2.0\",\n    \"pac-proxy-agent\": \"^4.1.0\",\n    \"plyr\": \"^3.6.2\",\n    \"qrcode\": \"^1.4.4\",\n    \"register-service-worker\": \"^1.7.1\",\n    \"svg-sprite-loader\": \"^6.0.11\",\n    \"tunnel\": \"^0.0.6\",\n    \"vscode-codicons\": \"^0.0.17\",\n    \"vue\": \"^2.6.11\",\n    \"vue-clipboard2\": \"^0.3.1\",\n    \"vue-gtag\": \"1\",\n    \"vue-i18n\": \"^8.22.0\",\n    \"vue-router\": \"^3.4.3\",\n    \"vue-slider-component\": \"^3.2.5\",\n    \"vuex\": \"^3.4.0\",\n    \"x11\": \"^2.3.0\"\n  },\n  \"devDependencies\": {\n    \"@types/node\": \"^17.0.0\",\n    \"@vue/cli-plugin-babel\": \"~4.5.0\",\n    \"@vue/cli-plugin-eslint\": \"~4.5.0\",\n    \"@vue/cli-plugin-pwa\": \"~4.5.0\",\n    \"@vue/cli-plugin-vuex\": \"~4.5.0\",\n    \"@vue/cli-service\": \"~4.5.0\",\n    \"babel-eslint\": \"^10.1.0\",\n    \"eslint\": \"^6.7.2\",\n    \"eslint-config-prettier\": \"^8.1.0\",\n    \"eslint-plugin-prettier\": \"^3.3.1\",\n    \"eslint-plugin-vue\": \"^7.9.0\",\n    \"husky\": \"^4.3.0\",\n    \"prettier\": \"2.5.1\",\n    \"sass\": \"^1.26.11\",\n    \"sass-loader\": \"^10.0.2\",\n    \"vue-cli-plugin-electron-builder\": \"~2.1.1\",\n    \"vue-template-compiler\": \"^2.6.11\"\n  },\n  \"resolutions\": {\n    \"icon-gen\": \"3.0.0\",\n    \"degenerator\": \"2.2.0\",\n    \"electron-builder\": \"^23.0.0\"\n  },\n  \"eslintConfig\": {\n    \"root\": true,\n    \"env\": {\n      \"node\": true,\n      \"browser\": true\n    },\n    \"extends\": [\n      \"plugin:vue/essential\",\n      \"plugin:vue/recommended\",\n      \"plugin:prettier/recommended\",\n      \"eslint:recommended\"\n    ],\n    \"parserOptions\": {\n      \"parser\": \"babel-eslint\"\n    },\n    \"globals\": {\n      \"ipcRenderer\": \"off\"\n    },\n    \"rules\": {}\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\",\n    \"not dead\"\n  ],\n  \"husky\": {\n    \"hooks\": {\n      \"pre-commit\": \"npm run prettier\"\n    }\n  }\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"utf-8\" />\n  <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n  <meta name=\"referrer\" content=\"no-referrer\" />\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n  <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.ico\" />\n  <title>\n    <%= htmlWebpackPlugin.options.title %>\n  </title>\n</head>\n\n<body>\n  <noscript>\n    <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work\n        properly without JavaScript enabled. Please enable it to\n        continue.</strong>\n  </noscript>\n  <div id=\"app\"></div>\n  <!-- built files will be auto injected -->\n</body>\n\n</html>\n"
  },
  {
    "path": "public/robots.txt",
    "content": "User-agent: *\nDisallow: /\n"
  },
  {
    "path": "restyled.yml",
    "content": "commit_template: 'style: with ${restyler.name}'\nrestylers:\n  - prettier\n  - prettier-json\n  - prettier-markdown\n  - prettier-yaml\n  - whitespace\n"
  },
  {
    "path": "src/App.vue",
    "content": "<template>\n  <div id=\"app\" :class=\"{ 'user-select-none': userSelectNone }\">\n    <Scrollbar v-show=\"!showLyrics\" ref=\"scrollbar\" />\n    <Navbar v-show=\"showNavbar\" ref=\"navbar\" />\n    <main\n      ref=\"main\"\n      :style=\"{ overflow: enableScrolling ? 'auto' : 'hidden' }\"\n      @scroll=\"handleScroll\"\n    >\n      <keep-alive>\n        <router-view v-if=\"$route.meta.keepAlive\"></router-view>\n      </keep-alive>\n      <router-view v-if=\"!$route.meta.keepAlive\"></router-view>\n    </main>\n    <transition name=\"slide-up\">\n      <Player v-if=\"enablePlayer\" v-show=\"showPlayer\" ref=\"player\" />\n    </transition>\n    <Toast />\n    <ModalAddTrackToPlaylist v-if=\"isAccountLoggedIn\" />\n    <ModalNewPlaylist v-if=\"isAccountLoggedIn\" />\n    <transition v-if=\"enablePlayer\" name=\"slide-up\">\n      <Lyrics v-show=\"showLyrics\" />\n    </transition>\n  </div>\n</template>\n\n<script>\nimport ModalAddTrackToPlaylist from './components/ModalAddTrackToPlaylist.vue';\nimport ModalNewPlaylist from './components/ModalNewPlaylist.vue';\nimport Scrollbar from './components/Scrollbar.vue';\nimport Navbar from './components/Navbar.vue';\nimport Player from './components/Player.vue';\nimport Toast from './components/Toast.vue';\nimport { ipcRenderer } from './electron/ipcRenderer';\nimport { isAccountLoggedIn, isLooseLoggedIn } from '@/utils/auth';\nimport Lyrics from './views/lyrics.vue';\nimport { mapState } from 'vuex';\n\nexport default {\n  name: 'App',\n  components: {\n    Navbar,\n    Player,\n    Toast,\n    ModalAddTrackToPlaylist,\n    ModalNewPlaylist,\n    Lyrics,\n    Scrollbar,\n  },\n  data() {\n    return {\n      isElectron: process.env.IS_ELECTRON, // true || undefined\n      userSelectNone: false,\n    };\n  },\n  computed: {\n    ...mapState(['showLyrics', 'settings', 'player', 'enableScrolling']),\n    isAccountLoggedIn() {\n      return isAccountLoggedIn();\n    },\n    showPlayer() {\n      return (\n        [\n          'mv',\n          'loginUsername',\n          'login',\n          'loginAccount',\n          'lastfmCallback',\n        ].includes(this.$route.name) === false\n      );\n    },\n    enablePlayer() {\n      return this.player.enabled && this.$route.name !== 'lastfmCallback';\n    },\n    showNavbar() {\n      return this.$route.name !== 'lastfmCallback';\n    },\n  },\n  created() {\n    if (this.isElectron) ipcRenderer(this);\n    window.addEventListener('keydown', this.handleKeydown);\n    this.fetchData();\n  },\n  methods: {\n    handleKeydown(e) {\n      if (e.code === 'Space') {\n        if (e.target.tagName === 'INPUT') return false;\n        if (this.$route.name === 'mv') return false;\n        e.preventDefault();\n        this.player.playOrPause();\n      }\n    },\n    fetchData() {\n      if (!isLooseLoggedIn()) return;\n      this.$store.dispatch('fetchLikedSongs');\n      this.$store.dispatch('fetchLikedSongsWithDetails');\n      this.$store.dispatch('fetchLikedPlaylist');\n      if (isAccountLoggedIn()) {\n        this.$store.dispatch('fetchLikedAlbums');\n        this.$store.dispatch('fetchLikedArtists');\n        this.$store.dispatch('fetchLikedMVs');\n        this.$store.dispatch('fetchCloudDisk');\n      }\n    },\n    handleScroll() {\n      this.$refs.scrollbar.handleScroll();\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\">\n#app {\n  width: 100%;\n  transition: all 0.4s;\n}\n\nmain {\n  position: fixed;\n  top: 0;\n  bottom: 0;\n  right: 0;\n  left: 0;\n  overflow: auto;\n  padding: 64px 10vw 96px 10vw;\n  box-sizing: border-box;\n  scrollbar-width: none; // firefox\n}\n\n@media (max-width: 1336px) {\n  main {\n    padding: 64px 5vw 96px 5vw;\n  }\n}\n\nmain::-webkit-scrollbar {\n  width: 0px;\n}\n\n.slide-up-enter-active,\n.slide-up-leave-active {\n  transition: transform 0.4s;\n}\n.slide-up-enter,\n.slide-up-leave-to {\n  transform: translateY(100%);\n}\n</style>\n"
  },
  {
    "path": "src/api/album.js",
    "content": "import request from '@/utils/request';\nimport { mapTrackPlayableStatus } from '@/utils/common';\nimport { cacheAlbum, getAlbumFromCache } from '@/utils/db';\n\n/**\n * 获取专辑内容\n * 说明 : 调用此接口 , 传入专辑 id, 可获得专辑内容\n * @param {number} id\n */\nexport function getAlbum(id) {\n  const fetchLatest = () => {\n    return request({\n      url: '/album',\n      method: 'get',\n      params: {\n        id,\n      },\n    }).then(data => {\n      cacheAlbum(id, data);\n      data.songs = mapTrackPlayableStatus(data.songs);\n      return data;\n    });\n  };\n  fetchLatest();\n\n  return getAlbumFromCache(id).then(result => {\n    return result ?? fetchLatest();\n  });\n}\n\n/**\n * 全部新碟\n * 说明 : 登录后调用此接口 ,可获取全部新碟\n * - limit - 返回数量 , 默认为 30\n * - offset - 偏移数量，用于分页 , 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0\n * - area - ALL:全部,ZH:华语,EA:欧美,KR:韩国,JP:日本\n * @param {Object} params\n * @param {number} params.limit\n * @param {number=} params.offset\n * @param {string} params.area\n */\nexport function newAlbums(params) {\n  return request({\n    url: '/album/new',\n    method: 'get',\n    params,\n  });\n}\n\n/**\n * 专辑动态信息\n * 说明 : 调用此接口 , 传入专辑 id, 可获得专辑动态信息,如是否收藏,收藏数,评论数,分享数\n * - id - 专辑id\n * @param {number} id\n */\nexport function albumDynamicDetail(id) {\n  return request({\n    url: '/album/detail/dynamic',\n    method: 'get',\n    params: { id, timestamp: new Date().getTime() },\n  });\n}\n\n/**\n * 收藏/取消收藏专辑\n * 说明 : 调用此接口,可收藏/取消收藏专辑\n * - id - 返专辑 id\n * - t - 1 为收藏,其他为取消收藏\n * @param {Object} params\n * @param {number} params.id\n * @param {number} params.t\n */\nexport function likeAAlbum(params) {\n  return request({\n    url: '/album/sub',\n    method: 'post',\n    params,\n  });\n}\n"
  },
  {
    "path": "src/api/artist.js",
    "content": "import request from '@/utils/request';\nimport { mapTrackPlayableStatus } from '@/utils/common';\nimport { isAccountLoggedIn } from '@/utils/auth';\nimport { getTrackDetail } from '@/api/track';\n\n/**\n * 获取歌手单曲\n * 说明 : 调用此接口 , 传入歌手 id, 可获得歌手部分信息和热门歌曲\n * @param {number} id - 歌手 id, 可由搜索接口获得\n */\nexport function getArtist(id) {\n  return request({\n    url: '/artists',\n    method: 'get',\n    params: {\n      id,\n      timestamp: new Date().getTime(),\n    },\n  }).then(async data => {\n    if (!isAccountLoggedIn()) {\n      const trackIDs = data.hotSongs.map(t => t.id);\n      const tracks = await getTrackDetail(trackIDs.join(','));\n      data.hotSongs = tracks.songs;\n      return data;\n    }\n    data.hotSongs = mapTrackPlayableStatus(data.hotSongs);\n    return data;\n  });\n}\n\n/**\n * 获取歌手专辑\n * 说明 : 调用此接口 , 传入歌手 id, 可获得歌手专辑内容\n * - id: 歌手 id\n * - limit: 取出数量 , 默认为 50\n * - offset: 偏移数量 , 用于分页 , 如 :( 页数 -1)*50, 其中 50 为 limit 的值 , 默认为 0\n * @param {Object} params\n * @param {number} params.id\n * @param {number=} params.limit\n * @param {number=} params.offset\n */\nexport function getArtistAlbum(params) {\n  return request({\n    url: '/artist/album',\n    method: 'get',\n    params,\n  });\n}\n\n/**\n * 歌手榜\n * 说明 : 调用此接口 , 可获取排行榜中的歌手榜\n * - type : 地区\n * 1: 华语\n * 2: 欧美\n * 3: 韩国\n * 4: 日本\n * @param {number=} type\n */\nexport function toplistOfArtists(type = null) {\n  let params = {};\n  if (type) {\n    params.type = type;\n  }\n  return request({\n    url: '/toplist/artist',\n    method: 'get',\n    params,\n  });\n}\n/**\n * 获取歌手 mv\n * 说明 : 调用此接口 , 传入歌手 id, 可获得歌手 mv 信息 , 具体 mv 播放地址可调 用/mv传入此接口获得的 mvid 来拿到 , 如 : /artist/mv?id=6452,/mv?mvid=5461064\n * @param {number} params.id 歌手 id, 可由搜索接口获得\n * @param {number} params.offset\n * @param {number} params.limit\n */\nexport function artistMv(params) {\n  return request({\n    url: '/artist/mv',\n    method: 'get',\n    params,\n  });\n}\n\n/**\n * 收藏歌手\n * 说明 : 调用此接口 , 传入歌手 id, 可收藏歌手\n * - id: 歌手 id\n * - t: 操作,1 为收藏,其他为取消收藏\n * @param {Object} params\n * @param {number} params.id\n * @param {number} params.t\n */\nexport function followAArtist(params) {\n  return request({\n    url: '/artist/sub',\n    method: 'post',\n    params,\n  });\n}\n\n/**\n * 相似歌手\n * 说明 : 调用此接口 , 传入歌手 id, 可获得相似歌手\n * - id: 歌手 id\n * @param {number} id\n */\nexport function similarArtists(id) {\n  return request({\n    url: '/simi/artist',\n    method: 'post',\n    params: { id },\n  });\n}\n"
  },
  {
    "path": "src/api/auth.js",
    "content": "import request from '@/utils/request';\n\n/**\n * 手机登录\n * - phone: 手机号码\n * - password: 密码\n * - countrycode: 国家码，用于国外手机号登录，例如美国传入：1\n * - md5_password: md5加密后的密码,传入后 password 将失效\n * @param {Object} params\n * @param {string} params.phone\n * @param {string} params.password\n * @param {string=} params.countrycode\n * @param {string=} params.md5_password\n */\nexport function loginWithPhone(params) {\n  return request({\n    url: '/login/cellphone',\n    method: 'post',\n    params,\n  });\n}\n\n/**\n * 邮箱登录\n * - email: 163 网易邮箱\n * - password: 密码\n * - md5_password: md5加密后的密码,传入后 password 将失效\n * @param {Object} params\n * @param {string} params.email\n * @param {string} params.password\n * @param {string=} params.md5_password\n */\nexport function loginWithEmail(params) {\n  return request({\n    url: '/login',\n    method: 'post',\n    params,\n  });\n}\n\n/**\n * 二维码key生成接口\n */\nexport function loginQrCodeKey() {\n  return request({\n    url: '/login/qr/key',\n    method: 'get',\n    params: {\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\n/**\n * 二维码生成接口\n * 说明: 调用此接口传入上一个接口生成的key可生成二维码图片的base64和二维码信息,\n * 可使用base64展示图片,或者使用二维码信息内容自行使用第三方二维码生产库渲染二维码\n * @param {Object} params\n * @param {string} params.key\n * @param {string=} params.qrimg 传入后会额外返回二维码图片base64编码\n */\nexport function loginQrCodeCreate(params) {\n  return request({\n    url: '/login/qr/create',\n    method: 'get',\n    params: {\n      ...params,\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\n/**\n * 二维码检测扫码状态接口\n * 说明: 轮询此接口可获取二维码扫码状态,800为二维码过期,801为等待扫码,802为待确认,803为授权登录成功(803状态码下会返回cookies)\n * @param {string} key\n */\nexport function loginQrCodeCheck(key) {\n  return request({\n    url: '/login/qr/check',\n    method: 'get',\n    params: {\n      key,\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\n/**\n * 刷新登录\n * 说明 : 调用此接口 , 可刷新登录状态\n * - 调用例子 : /login/refresh\n */\nexport function refreshCookie() {\n  return request({\n    url: '/login/refresh',\n    method: 'post',\n  });\n}\n\n/**\n * 退出登录\n * 说明 : 调用此接口 , 可退出登录\n */\nexport function logout() {\n  return request({\n    url: '/logout',\n    method: 'post',\n  });\n}\n"
  },
  {
    "path": "src/api/lastfm.js",
    "content": "// Last.fm API documents 👉 https://www.last.fm/api\n\nimport axios from 'axios';\nimport md5 from 'crypto-js/md5';\n\nconst apiKey = process.env.VUE_APP_LASTFM_API_KEY;\nconst apiSharedSecret = process.env.VUE_APP_LASTFM_API_SHARED_SECRET;\nconst baseUrl = window.location.origin;\nconst url = 'https://ws.audioscrobbler.com/2.0/';\n\nconst sign = params => {\n  const sortParamsKeys = Object.keys(params).sort();\n  const sortedParams = sortParamsKeys.reduce((acc, key) => {\n    acc[key] = params[key];\n    return acc;\n  }, {});\n  let signature = '';\n  for (const [key, value] of Object.entries(sortedParams)) {\n    signature += `${key}${value}`;\n  }\n  return md5(signature + apiSharedSecret).toString();\n};\n\nexport function auth() {\n  const url = process.env.IS_ELECTRON\n    ? `https://www.last.fm/api/auth/?api_key=${apiKey}&cb=${baseUrl}/#/lastfm/callback`\n    : `https://www.last.fm/api/auth/?api_key=${apiKey}&cb=${baseUrl}/lastfm/callback`;\n  window.open(url);\n}\n\nexport function authGetSession(token) {\n  const signature = md5(\n    `api_key${apiKey}methodauth.getSessiontoken${token}${apiSharedSecret}`\n  ).toString();\n  return axios({\n    url,\n    method: 'GET',\n    params: {\n      method: 'auth.getSession',\n      format: 'json',\n      api_key: apiKey,\n      api_sig: signature,\n      token,\n    },\n  });\n}\n\nexport function trackUpdateNowPlaying(params) {\n  params.api_key = apiKey;\n  params.method = 'track.updateNowPlaying';\n  params.sk = JSON.parse(localStorage.getItem('lastfm'))['key'];\n  const signature = sign(params);\n\n  return axios({\n    url,\n    method: 'POST',\n    params: {\n      ...params,\n      api_sig: signature,\n      format: 'json',\n    },\n  });\n}\n\nexport function trackScrobble(params) {\n  params.api_key = apiKey;\n  params.method = 'track.scrobble';\n  params.sk = JSON.parse(localStorage.getItem('lastfm'))['key'];\n  const signature = sign(params);\n\n  return axios({\n    url,\n    method: 'POST',\n    params: {\n      ...params,\n      api_sig: signature,\n      format: 'json',\n    },\n  });\n}\n"
  },
  {
    "path": "src/api/mv.js",
    "content": "import request from '@/utils/request';\n\n/**\n * 获取 mv 数据\n * 说明 : 调用此接口 , 传入 mvid ( 在搜索音乐的时候传 type=1004 获得 ) , 可获取对应 MV 数据 , 数据包含 mv 名字 , 歌手 , 发布时间 , mv 视频地址等数据 ,\n * 其中 mv 视频 网易做了防盗链处理 , 可能不能直接播放 , 需要播放的话需要调用 ' mv 地址' 接口\n * - 调用例子 : /mv/detail?mvid=5436712\n * @param {number} mvid mv 的 id\n */\nexport function mvDetail(mvid) {\n  return request({\n    url: '/mv/detail',\n    method: 'get',\n    params: {\n      mvid,\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\n/**\n * mv 地址\n * 说明 : 调用此接口 , 传入 mv id,可获取 mv 播放地址\n * - id: mv id\n * - r: 分辨率,默认1080,可从 /mv/detail 接口获取分辨率列表\n * - 调用例子 : /mv/url?id=5436712 /mv/url?id=10896407&r=1080\n * @param {Object} params\n * @param {number} params.id\n * @param {number=} params.r\n */\nexport function mvUrl(params) {\n  return request({\n    url: '/mv/url',\n    method: 'get',\n    params,\n  });\n}\n\n/**\n * 相似 mv\n * 说明 : 调用此接口 , 传入 mvid 可获取相似 mv\n * @param {number} mvid\n */\nexport function simiMv(mvid) {\n  return request({\n    url: '/simi/mv',\n    method: 'get',\n    params: { mvid },\n  });\n}\n\n/**\n * 收藏/取消收藏 MV\n * 说明 : 调用此接口,可收藏/取消收藏 MV\n * - mvid: mv id\n * - t: 1 为收藏,其他为取消收藏\n * @param {Object} params\n * @param {number} params.mvid\n * @param {number=} params.t\n */\n\nexport function likeAMV(params) {\n  params.timestamp = new Date().getTime();\n  return request({\n    url: '/mv/sub',\n    method: 'post',\n    params,\n  });\n}\n"
  },
  {
    "path": "src/api/others.js",
    "content": "import request from '@/utils/request';\nimport { mapTrackPlayableStatus } from '@/utils/common';\n\n/**\n * 搜索\n * 说明 : 调用此接口 , 传入搜索关键词可以搜索该音乐 / 专辑 / 歌手 / 歌单 / 用户 , 关键词可以多个 , 以空格隔开 ,\n * 如 \" 周杰伦 搁浅 \"( 不需要登录 ), 搜索获取的 mp3url 不能直接用 , 可通过 /song/url 接口传入歌曲 id 获取具体的播放链接\n * - keywords : 关键词\n * - limit : 返回数量 , 默认为 30\n * - offset : 偏移数量，用于分页 , 如 : 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0\n * - type: 搜索类型；默认为 1 即单曲 , 取值意义 : 1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单, 1002: 用户, 1004: MV, 1006: 歌词, 1009: 电台, 1014: 视频, 1018:综合\n * - 调用例子 : /search?keywords=海阔天空 /cloudsearch?keywords=海阔天空(更全)\n * @param {Object} params\n * @param {string} params.keywords\n * @param {number=} params.limit\n * @param {number=} params.offset\n * @param {number=} params.type\n */\nexport function search(params) {\n  return request({\n    url: '/search',\n    method: 'get',\n    params,\n  }).then(data => {\n    if (data.result?.song !== undefined)\n      data.result.song.songs = mapTrackPlayableStatus(data.result.song.songs);\n    return data;\n  });\n}\n\nexport function personalFM() {\n  return request({\n    url: '/personal_fm',\n    method: 'get',\n    params: {\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\nexport function fmTrash(id) {\n  return request({\n    url: '/fm_trash',\n    method: 'post',\n    params: {\n      timestamp: new Date().getTime(),\n      id,\n    },\n  });\n}\n"
  },
  {
    "path": "src/api/playlist.js",
    "content": "import request from '@/utils/request';\nimport { mapTrackPlayableStatus } from '@/utils/common';\n\n/**\n * 推荐歌单\n * 说明 : 调用此接口 , 可获取推荐歌单\n * - limit: 取出数量 , 默认为 30 (不支持 offset)\n * - 调用例子 : /personalized?limit=1\n * @param {Object} params\n * @param {number=} params.limit\n */\nexport function recommendPlaylist(params) {\n  return request({\n    url: '/personalized',\n    method: 'get',\n    params,\n  });\n}\n/**\n * 获取每日推荐歌单\n * 说明 : 调用此接口 , 可获得每日推荐歌单 ( 需要登录 )\n * @param {Object} params\n * @param {number=} params.limit\n */\nexport function dailyRecommendPlaylist(params) {\n  return request({\n    url: '/recommend/resource',\n    method: 'get',\n    params: {\n      params,\n      timestamp: Date.now(),\n    },\n  });\n}\n/**\n * 获取歌单详情\n * 说明 : 歌单能看到歌单名字, 但看不到具体歌单内容 , 调用此接口 , 传入歌单 id, 可以获取对应歌单内的所有的音乐(未登录状态只能获取不完整的歌单,登录后是完整的)，\n * 但是返回的trackIds是完整的，tracks 则是不完整的，可拿全部 trackIds 请求一次 song/detail 接口\n * 获取所有歌曲的详情 (https://github.com/Binaryify/NeteaseCloudMusicApi/issues/452)\n * - id : 歌单 id\n * - s : 歌单最近的 s 个收藏者, 默认为8\n * @param {number} id\n * @param {boolean=} noCache\n */\nexport function getPlaylistDetail(id, noCache = false) {\n  let params = { id };\n  if (noCache) params.timestamp = new Date().getTime();\n  return request({\n    url: '/playlist/detail',\n    method: 'get',\n    params,\n  }).then(data => {\n    if (data.playlist) {\n      data.playlist.tracks = mapTrackPlayableStatus(\n        data.playlist.tracks,\n        data.privileges || []\n      );\n    }\n    return data;\n  });\n}\n/**\n * 获取精品歌单\n * 说明 : 调用此接口 , 可获取精品歌单\n * - cat: tag, 比如 \" 华语 \"、\" 古风 \" 、\" 欧美 \"、\" 流行 \", 默认为 \"全部\", 可从精品歌单标签列表接口获取(/playlist/highquality/tags)\n * - limit: 取出歌单数量 , 默认为 20\n * - before: 分页参数,取上一页最后一个歌单的 updateTime 获取下一页数据\n * @param {Object} params\n * @param {string} params.cat\n * @param {number=} params.limit\n * @param {number} params.before\n */\nexport function highQualityPlaylist(params) {\n  return request({\n    url: '/top/playlist/highquality',\n    method: 'get',\n    params,\n  });\n}\n\n/**\n * 歌单 ( 网友精选碟 )\n * 说明 : 调用此接口 , 可获取网友精选碟歌单\n * - order: 可选值为 'new' 和 'hot', 分别对应最新和最热 , 默认为 'hot'\n * - cat: tag, 比如 \" 华语 \"、\" 古风 \" 、\" 欧美 \"、\" 流行 \", 默认为 \"全部\",可从歌单分类接口获取(/playlist/catlist)\n * - limit: 取出歌单数量 , 默认为 50\n * @param {Object} params\n * @param {string} params.order\n * @param {string} params.cat\n * @param {number=} params.limit\n */\nexport function topPlaylist(params) {\n  return request({\n    url: '/top/playlist',\n    method: 'get',\n    params,\n  });\n}\n\n/**\n * 歌单分类\n * 说明 : 调用此接口,可获取歌单分类,包含 category 信息\n */\nexport function playlistCatlist() {\n  return request({\n    url: '/playlist/catlist',\n    method: 'get',\n  });\n}\n\n/**\n * 所有榜单\n * 说明 : 调用此接口,可获取所有榜单 接口地址 : /toplist\n */\nexport function toplists() {\n  return request({\n    url: '/toplist',\n    method: 'get',\n  });\n}\n\n/**\n * 收藏/取消收藏歌单\n * 说明 : 调用此接口, 传入类型和歌单 id 可收藏歌单或者取消收藏歌单\n * - t : 类型,1:收藏,2:取消收藏\n * - id : 歌单 id\n * @param {Object} params\n * @param {number} params.t\n * @param {number} params.id\n */\nexport function subscribePlaylist(params) {\n  params.timestamp = new Date().getTime();\n  return request({\n    url: '/playlist/subscribe',\n    method: 'post',\n    params,\n  });\n}\n\n/**\n * 删除歌单\n * 说明 : 调用此接口 , 传入歌单id可删除歌单\n * - id : 歌单id,可多个,用逗号隔开\n *  * @param {number} id\n */\nexport function deletePlaylist(id) {\n  return request({\n    url: '/playlist/delete',\n    method: 'post',\n    params: { id },\n  });\n}\n\n/**\n * 新建歌单\n * 说明 : 调用此接口 , 传入歌单名字可新建歌单\n * - name : 歌单名\n * - privacy : 是否设置为隐私歌单，默认否，传'10'则设置成隐私歌单\n * - type : 歌单类型,默认'NORMAL',传 'VIDEO'则为视频歌单\n * @param {Object} params\n * @param {string} params.name\n * @param {number} params.privacy\n * @param {string} params.type\n */\nexport function createPlaylist(params) {\n  params.timestamp = new Date().getTime();\n  return request({\n    url: '/playlist/create',\n    method: 'post',\n    params,\n  });\n}\n\n/**\n * 对歌单添加或删除歌曲\n * 说明 : 调用此接口 , 可以添加歌曲到歌单或者从歌单删除某首歌曲 ( 需要登录 )\n * - op: 从歌单增加单曲为 add, 删除为 del\n * - pid: 歌单 id tracks: 歌曲 id,可多个,用逗号隔开\n * @param {Object} params\n * @param {string} params.op\n * @param {string} params.pid\n */\nexport function addOrRemoveTrackFromPlaylist(params) {\n  params.timestamp = new Date().getTime();\n  return request({\n    url: '/playlist/tracks',\n    method: 'post',\n    params,\n  });\n}\n\n/**\n * 每日推荐歌曲\n * 说明 : 调用此接口 , 可获得每日推荐歌曲 ( 需要登录 )\n * @param {Object} params\n * @param {string} params.op\n * @param {string} params.pid\n */\nexport function dailyRecommendTracks() {\n  return request({\n    url: '/recommend/songs',\n    method: 'get',\n    params: { timestamp: new Date().getTime() },\n  }).then(result => {\n    result.data.dailySongs = mapTrackPlayableStatus(\n      result.data.dailySongs,\n      result.data.privileges\n    );\n    return result;\n  });\n}\n\n/**\n * 心动模式/智能播放\n * 说明 : 登录后调用此接口 , 可获取心动模式/智能播放列表 必选参数 : id : 歌曲 id\n * - id : 歌曲 id\n * - pid : 歌单 id\n * - sid : 要开始播放的歌曲的 id (可选参数)\n * @param {Object} params\n * @param {number=} params.id\n * @param {number=} params.pid\n */\nexport function intelligencePlaylist(params) {\n  return request({\n    url: '/playmode/intelligence/list',\n    method: 'get',\n    params,\n  });\n}\n"
  },
  {
    "path": "src/api/track.js",
    "content": "import store from '@/store';\nimport request from '@/utils/request';\nimport { mapTrackPlayableStatus } from '@/utils/common';\nimport {\n  cacheTrackDetail,\n  getTrackDetailFromCache,\n  cacheLyric,\n  getLyricFromCache,\n} from '@/utils/db';\n\n/**\n * 获取音乐 url\n * 说明 : 使用歌单详情接口后 , 能得到的音乐的 id, 但不能得到的音乐 url, 调用此接口, 传入的音乐 id( 可多个 , 用逗号隔开 ), 可以获取对应的音乐的 url,\n * !!!未登录状态返回试听片段(返回字段包含被截取的正常歌曲的开始时间和结束时间)\n * @param {string} id - 音乐的 id，例如 id=405998841,33894312\n */\nexport function getMP3(id) {\n  const getBr = () => {\n    // 当返回的 quality >= 400000时，就会优先返回 hi-res\n    const quality = store.state.settings?.musicQuality ?? '320000';\n    return quality === 'flac' ? '350000' : quality;\n  };\n\n  return request({\n    url: '/song/url',\n    method: 'get',\n    params: {\n      id,\n      br: getBr(),\n    },\n  });\n}\n\n/**\n * 获取歌曲详情\n * 说明 : 调用此接口 , 传入音乐 id(支持多个 id, 用 , 隔开), 可获得歌曲详情(注意:歌曲封面现在需要通过专辑内容接口获取)\n * @param {string} ids - 音乐 id, 例如 ids=405998841,33894312\n */\nexport function getTrackDetail(ids) {\n  const fetchLatest = () => {\n    return request({\n      url: '/song/detail',\n      method: 'get',\n      params: {\n        ids,\n      },\n    }).then(data => {\n      data.songs.map(song => {\n        const privileges = data.privileges.find(t => t.id === song.id);\n        cacheTrackDetail(song, privileges);\n      });\n      data.songs = mapTrackPlayableStatus(data.songs, data.privileges);\n      return data;\n    });\n  };\n  fetchLatest();\n\n  let idsInArray = [String(ids)];\n  if (typeof ids === 'string') {\n    idsInArray = ids.split(',');\n  }\n\n  return getTrackDetailFromCache(idsInArray).then(result => {\n    if (result) {\n      result.songs = mapTrackPlayableStatus(result.songs, result.privileges);\n    }\n    return result ?? fetchLatest();\n  });\n}\n\n/**\n * 获取歌词\n * 说明 : 调用此接口 , 传入音乐 id 可获得对应音乐的歌词 ( 不需要登录 )\n * @param {number} id - 音乐 id\n */\nexport function getLyric(id) {\n  const fetchLatest = () => {\n    return request({\n      url: '/lyric',\n      method: 'get',\n      params: {\n        id,\n      },\n    }).then(result => {\n      cacheLyric(id, result);\n      return result;\n    });\n  };\n\n  fetchLatest();\n\n  return getLyricFromCache(id).then(result => {\n    return result ?? fetchLatest();\n  });\n}\n\n/**\n * 新歌速递\n * 说明 : 调用此接口 , 可获取新歌速递\n * @param {number} type - 地区类型 id, 对应以下: 全部:0 华语:7 欧美:96 日本:8 韩国:16\n */\nexport function topSong(type) {\n  return request({\n    url: '/top/song',\n    method: 'get',\n    params: {\n      type,\n    },\n  });\n}\n\n/**\n * 喜欢音乐\n * 说明 : 调用此接口 , 传入音乐 id, 可喜欢该音乐\n * - id - 歌曲 id\n * - like - 默认为 true 即喜欢 , 若传 false, 则取消喜欢\n * @param {Object} params\n * @param {number} params.id\n * @param {boolean=} [params.like]\n */\nexport function likeATrack(params) {\n  params.timestamp = new Date().getTime();\n  return request({\n    url: '/like',\n    method: 'get',\n    params,\n  });\n}\n\n/**\n * 听歌打卡\n * 说明 : 调用此接口 , 传入音乐 id, 来源 id，歌曲时间 time，更新听歌排行数据\n * - id - 歌曲 id\n * - sourceid - 歌单或专辑 id\n * - time - 歌曲播放时间,单位为秒\n * @param {Object} params\n * @param {number} params.id\n * @param {number} params.sourceid\n * @param {number=} params.time\n */\nexport function scrobble(params) {\n  params.timestamp = new Date().getTime();\n  return request({\n    url: '/scrobble',\n    method: 'get',\n    params,\n  });\n}\n"
  },
  {
    "path": "src/api/user.js",
    "content": "import request from '@/utils/request';\n\n/**\n * 获取用户详情\n * 说明 : 登录后调用此接口 , 传入用户 id, 可以获取用户详情\n * - uid : 用户 id\n * @param {number} uid\n */\nexport function userDetail(uid) {\n  return request({\n    url: '/user/detail',\n    method: 'get',\n    params: {\n      uid,\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\n/**\n * 获取账号详情\n * 说明 : 登录后调用此接口 ,可获取用户账号信息\n */\nexport function userAccount() {\n  return request({\n    url: '/user/account',\n    method: 'get',\n    params: {\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\n/**\n * 获取用户歌单\n * 说明 : 登录后调用此接口 , 传入用户 id, 可以获取用户歌单\n * - uid : 用户 id\n * - limit : 返回数量 , 默认为 30\n * - offset : 偏移数量，用于分页 , 如 :( 页数 -1)*30, 其中 30 为 limit 的值 , 默认为 0\n * @param {Object} params\n * @param {number} params.uid\n * @param {number} params.limit\n * @param {number=} params.offset\n */\nexport function userPlaylist(params) {\n  return request({\n    url: '/user/playlist',\n    method: 'get',\n    params,\n  });\n}\n\n/**\n * 获取用户播放记录\n * 说明 : 登录后调用此接口 , 传入用户 id, 可获取用户播放记录\n * - uid : 用户 id\n * - type : type=1 时只返回 weekData, type=0 时返回 allData\n * @param {Object} params\n * @param {number} params.uid\n * @param {number} params.type\n */\nexport function userPlayHistory(params) {\n  return request({\n    url: '/user/record',\n    method: 'get',\n    params,\n  });\n}\n\n/**\n * 喜欢音乐列表（需要登录）\n * 说明 : 调用此接口 , 传入用户 id, 可获取已喜欢音乐id列表(id数组)\n * - uid: 用户 id\n * @param {number} uid\n */\nexport function userLikedSongsIDs(uid) {\n  return request({\n    url: '/likelist',\n    method: 'get',\n    params: {\n      uid,\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\n/**\n * 每日签到\n * 说明 : 调用此接口可签到获取积分\n * -  type: 签到类型 , 默认 0, 其中 0 为安卓端签到 ,1 为 web/PC 签到\n * @param {number} type\n */\nexport function dailySignin(type = 0) {\n  return request({\n    url: '/daily_signin',\n    method: 'post',\n    params: {\n      type,\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\n/**\n * 获取收藏的专辑（需要登录）\n * 说明 : 调用此接口可获取到用户收藏的专辑\n * - limit : 返回数量 , 默认为 25\n * - offset : 偏移数量，用于分页 , 如 :( 页数 -1)*25, 其中 25 为 limit 的值 , 默认为 0\n * @param {Object} params\n * @param {number} params.limit\n * @param {number=} params.offset\n */\nexport function likedAlbums(params) {\n  return request({\n    url: '/album/sublist',\n    method: 'get',\n    params: {\n      limit: params.limit,\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\n/**\n * 获取收藏的歌手（需要登录）\n * 说明 : 调用此接口可获取到用户收藏的歌手\n */\nexport function likedArtists(params) {\n  return request({\n    url: '/artist/sublist',\n    method: 'get',\n    params: {\n      limit: params.limit,\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\n/**\n * 获取收藏的MV（需要登录）\n * 说明 : 调用此接口可获取到用户收藏的MV\n */\nexport function likedMVs(params) {\n  return request({\n    url: '/mv/sublist',\n    method: 'get',\n    params: {\n      limit: params.limit,\n      timestamp: new Date().getTime(),\n    },\n  });\n}\n\n/**\n * 上传歌曲到云盘（需要登录）\n */\nexport function uploadSong(file) {\n  let formData = new FormData();\n  formData.append('songFile', file);\n  return request({\n    url: '/cloud',\n    method: 'post',\n    params: {\n      timestamp: new Date().getTime(),\n    },\n    data: formData,\n    headers: {\n      'Content-Type': 'multipart/form-data',\n    },\n    timeout: 200000,\n  }).catch(error => {\n    alert(`上传失败，Error: ${error}`);\n  });\n}\n\n/**\n * 获取云盘歌曲（需要登录）\n * 说明 : 登录后调用此接口 , 可获取云盘数据 , 获取的数据没有对应 url, 需要再调用一 次 /song/url 获取 url\n * - limit : 返回数量 , 默认为 200\n * - offset : 偏移数量，用于分页 , 如 :( 页数 -1)*200, 其中 200 为 limit 的值 , 默认为 0\n * @param {Object} params\n * @param {number} params.limit\n * @param {number=} params.offset\n */\nexport function cloudDisk(params = {}) {\n  params.timestamp = new Date().getTime();\n  return request({\n    url: '/user/cloud',\n    method: 'get',\n    params,\n  });\n}\n\n/**\n * 获取云盘歌曲详情（需要登录）\n */\nexport function cloudDiskTrackDetail(id) {\n  return request({\n    url: '/user/cloud/detail',\n    method: 'get',\n    params: {\n      timestamp: new Date().getTime(),\n      id,\n    },\n  });\n}\n\n/**\n * 删除云盘歌曲（需要登录）\n * @param {Array} id\n */\nexport function cloudDiskTrackDelete(id) {\n  return request({\n    url: '/user/cloud/del',\n    method: 'get',\n    params: {\n      timestamp: new Date().getTime(),\n      id,\n    },\n  });\n}\n"
  },
  {
    "path": "src/assets/css/global.scss",
    "content": "@font-face {\n  font-family: 'Barlow';\n  font-weight: normal;\n  src: url('~@/assets/fonts/Barlow-Regular.woff2') format('woff2'),\n    url('~@/assets/fonts/Barlow-Regular.ttf') format('truetype');\n}\n@font-face {\n  font-family: 'Barlow';\n  font-weight: medium;\n  src: url('~@/assets/fonts/Barlow-Medium.woff2') format('woff2'),\n    url('~@/assets/fonts/Barlow-Medium.ttf') format('truetype');\n}\n@font-face {\n  font-family: 'Barlow';\n  font-weight: 600;\n  src: url('~@/assets/fonts/Barlow-SemiBold.woff2') format('woff2'),\n    url('~@/assets/fonts/Barlow-SemiBold.ttf') format('truetype');\n}\n@font-face {\n  font-family: 'Barlow';\n  font-weight: bold;\n  src: url('~@/assets/fonts/Barlow-Bold.woff2') format('woff2'),\n    url('~@/assets/fonts/Barlow-Bold.ttf') format('truetype');\n}\n@font-face {\n  font-family: 'Barlow';\n  font-weight: 800;\n  src: url('~@/assets/fonts/Barlow-ExtraBold.woff2') format('woff2'),\n    url('~@/assets/fonts/Barlow-ExtraBold.ttf') format('truetype');\n}\n@font-face {\n  font-family: 'Barlow';\n  font-weight: 900;\n  src: url('~@/assets/fonts/Barlow-Black.woff2') format('woff2'),\n    url('~@/assets/fonts/Barlow-Black.ttf') format('truetype');\n}\n\n:root {\n  --color-body-bg: #ffffff;\n  --color-text: #000;\n  --color-primary: #335eea;\n  --color-primary-bg: #eaeffd;\n  --color-secondary: #7a7a7b;\n  --color-secondary-bg: #f5f5f7;\n  --color-navbar-bg: rgba(255, 255, 255, 0.86);\n  --color-primary-bg-for-transparent: rgba(189, 207, 255, 0.28);\n  --color-secondary-bg-for-transparent: rgba(209, 209, 214, 0.28);\n  --html-overflow-y: overlay;\n}\n\n[data-theme='dark'] {\n  --color-body-bg: #222222;\n  --color-text: #ffffff;\n  --color-primary: #335eea;\n  --color-primary-bg: #bbcdff;\n  --color-secondary: #7a7a7b;\n  --color-secondary-bg: #323232;\n  --color-navbar-bg: rgba(34, 34, 34, 0.86);\n  --color-primary-bg-for-transparent: rgba(255, 255, 255, 0.12);\n  --color-secondary-bg-for-transparent: rgba(255, 255, 255, 0.08);\n}\n\n#app,\ninput {\n  font-family: 'Barlow', ui-sans-serif, system-ui, -apple-system,\n    BlinkMacSystemFont, Helvetica Neue, PingFang SC, Microsoft YaHei,\n    Source Han Sans SC, Noto Sans CJK SC, WenQuanYi Micro Hei, sans-serif,\n    microsoft uighur;\n}\nbody {\n  background-color: var(--color-body-bg);\n}\n\nhtml {\n  overflow-y: var(--html-overflow-y);\n  min-width: 768px;\n  overscroll-behavior: none;\n}\n\nselect,\nbutton {\n  font-family: inherit;\n}\nbutton {\n  background: none;\n  border: none;\n  cursor: pointer;\n  user-select: none;\n}\ninput,\nbutton {\n  &:focus {\n    outline: none;\n  }\n}\na {\n  color: inherit;\n  text-decoration: none;\n  cursor: pointer;\n  &:hover {\n    text-decoration: underline;\n  }\n}\n\n[data-electron='yes'] {\n  button,\n  .navigation-links a,\n  .playlist-info .description {\n    cursor: default !important;\n  }\n}\n\n::-webkit-scrollbar {\n  width: 8px;\n}\n\n::-webkit-scrollbar-track {\n  background: transparent;\n  border-left: 1px solid rgba(128, 128, 128, 0.18);\n  background: var(--color-body-bg);\n}\n\n::-webkit-scrollbar-thumb {\n  -webkit-border-radius: 10px;\n  border-radius: 10px;\n  background: rgba(128, 128, 128, 0.38);\n}\n\n[data-theme='dark'] ::-webkit-scrollbar-thumb {\n  background: var(--color-secondary-bg);\n}\n\n.user-select-none {\n  user-select: none;\n}\n"
  },
  {
    "path": "src/assets/css/nprogress.css",
    "content": "/* Make clicks pass-through */\n#nprogress {\n  pointer-events: none;\n}\n\n#nprogress .bar {\n  background: #335eea;\n\n  position: fixed;\n  z-index: 1031;\n  top: 0;\n  left: 0;\n\n  width: 100%;\n  height: 2px;\n}\n\n/* Fancy blur effect */\n#nprogress .peg {\n  display: block;\n  position: absolute;\n  right: 0px;\n  width: 100px;\n  height: 100%;\n  box-shadow: 0 0 10px #335eea, 0 0 5px #335eea;\n  opacity: 1;\n\n  -webkit-transform: rotate(3deg) translate(0px, -4px);\n  -ms-transform: rotate(3deg) translate(0px, -4px);\n  transform: rotate(3deg) translate(0px, -4px);\n}\n\n.nprogress-custom-parent {\n  overflow: hidden;\n  position: relative;\n}\n\n.nprogress-custom-parent #nprogress .bar {\n  position: absolute;\n}\n"
  },
  {
    "path": "src/assets/css/plyr.css",
    "content": "@keyframes plyr-progress {\n  to {\n    background-position: 25px 0;\n    background-position: var(--plyr-progress-loading-size, 25px) 0;\n  }\n}\n\n@keyframes plyr-popup {\n  0% {\n    opacity: 0.5;\n    transform: translateY(10px);\n  }\n\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n\n@keyframes plyr-fade-in {\n  from {\n    opacity: 0;\n  }\n\n  to {\n    opacity: 1;\n  }\n}\n\n.plyr {\n  -moz-osx-font-smoothing: grayscale;\n  -webkit-font-smoothing: antialiased;\n  align-items: center;\n  direction: ltr;\n  display: flex;\n  flex-direction: column;\n  font-family: inherit;\n  font-family: var(--plyr-font-family, inherit);\n  font-variant-numeric: tabular-nums;\n  font-weight: 400;\n  font-weight: var(--plyr-font-weight-regular, 400);\n  height: 100%;\n  line-height: 1.7;\n  line-height: var(--plyr-line-height, 1.7);\n  max-width: 100%;\n  min-width: 200px;\n  position: relative;\n  text-shadow: none;\n  transition: box-shadow 0.3s ease;\n  z-index: 0;\n}\n\n.plyr audio,\n.plyr iframe,\n.plyr video {\n  display: block;\n  height: 100%;\n  width: 100%;\n}\n\n.plyr button {\n  font: inherit;\n  line-height: inherit;\n  width: auto;\n}\n\n.plyr:focus {\n  outline: 0;\n}\n\n.plyr--full-ui {\n  box-sizing: border-box;\n}\n\n.plyr--full-ui *,\n.plyr--full-ui ::after,\n.plyr--full-ui ::before {\n  box-sizing: inherit;\n}\n\n.plyr--full-ui a,\n.plyr--full-ui button,\n.plyr--full-ui input,\n.plyr--full-ui label {\n  touch-action: manipulation;\n}\n\n.plyr__badge {\n  background: #4a5464;\n  background: var(--plyr-badge-background, #4a5464);\n  border-radius: 2px;\n  border-radius: var(--plyr-badge-border-radius, 2px);\n  color: #fff;\n  color: var(--plyr-badge-text-color, #fff);\n  font-size: 9px;\n  font-size: var(--plyr-font-size-badge, 9px);\n  line-height: 1;\n  padding: 3px 4px;\n}\n\n.plyr--full-ui ::-webkit-media-text-track-container {\n  display: none;\n}\n\n.plyr__captions {\n  animation: plyr-fade-in 0.3s ease;\n  bottom: 0;\n  display: none;\n  font-size: 13px;\n  font-size: var(--plyr-font-size-small, 13px);\n  left: 0;\n  padding: 10px;\n  padding: var(--plyr-control-spacing, 10px);\n  position: absolute;\n  text-align: center;\n  transition: transform 0.4s ease-in-out;\n  width: 100%;\n}\n\n.plyr__captions span:empty {\n  display: none;\n}\n\n@media (min-width: 480px) {\n  .plyr__captions {\n    font-size: 15px;\n    font-size: var(--plyr-font-size-base, 15px);\n    padding: calc(10px * 2);\n    padding: calc(var(--plyr-control-spacing, 10px) * 2);\n  }\n}\n\n@media (min-width: 768px) {\n  .plyr__captions {\n    font-size: 18px;\n    font-size: var(--plyr-font-size-large, 18px);\n  }\n}\n\n.plyr--captions-active .plyr__captions {\n  display: block;\n}\n\n.plyr:not(.plyr--hide-controls) .plyr__controls:not(:empty) ~ .plyr__captions {\n  transform: translateY(calc(10px * -4));\n  transform: translateY(calc(var(--plyr-control-spacing, 10px) * -4));\n}\n\n.plyr__caption {\n  background: rgba(0, 0, 0, 0.8);\n  background: var(--plyr-captions-background, rgba(0, 0, 0, 0.8));\n  border-radius: 2px;\n  -webkit-box-decoration-break: clone;\n  box-decoration-break: clone;\n  color: #fff;\n  color: var(--plyr-captions-text-color, #fff);\n  line-height: 185%;\n  padding: 0.2em 0.5em;\n  white-space: pre-wrap;\n}\n\n.plyr__caption div {\n  display: inline;\n}\n\n.plyr__control {\n  background: 0 0;\n  border: 0;\n  border-radius: 3px;\n  border-radius: var(--plyr-control-radius, 3px);\n  color: inherit;\n  cursor: pointer;\n  flex-shrink: 0;\n  overflow: visible;\n  padding: calc(10px * 0.7);\n  padding: calc(var(--plyr-control-spacing, 10px) * 0.7);\n  position: relative;\n  transition: all 0.3s ease;\n}\n\n.plyr__control svg {\n  display: block;\n  fill: currentColor;\n  height: 18px;\n  height: var(--plyr-control-icon-size, 18px);\n  pointer-events: none;\n  width: 18px;\n  width: var(--plyr-control-icon-size, 18px);\n}\n\n.plyr__control:focus {\n  outline: 0;\n}\n\n.plyr__control.plyr__tab-focus {\n  outline-color: #00b3ff;\n  outline-color: var(\n    --plyr-tab-focus-color,\n    var(--plyr-color-main, var(--plyr-color-main, #00b3ff))\n  );\n  outline-offset: 2px;\n  outline-style: dotted;\n  outline-width: 3px;\n}\n\na.plyr__control {\n  text-decoration: none;\n}\n\na.plyr__control::after,\na.plyr__control::before {\n  display: none;\n}\n\n.plyr__control.plyr__control--pressed .icon--not-pressed,\n.plyr__control.plyr__control--pressed .label--not-pressed,\n.plyr__control:not(.plyr__control--pressed) .icon--pressed,\n.plyr__control:not(.plyr__control--pressed) .label--pressed {\n  display: none;\n}\n\n.plyr--full-ui ::-webkit-media-controls {\n  display: none;\n}\n\n.plyr__controls {\n  align-items: center;\n  display: flex;\n  justify-content: flex-end;\n  text-align: center;\n}\n\n.plyr__controls .plyr__progress__container {\n  flex: 1;\n  min-width: 0;\n}\n\n.plyr__controls .plyr__controls__item {\n  margin-left: calc(10px / 4);\n  margin-left: calc(var(--plyr-control-spacing, 10px) / 4);\n}\n\n.plyr__controls .plyr__controls__item:first-child {\n  margin-left: 0;\n  margin-right: auto;\n}\n\n.plyr__controls .plyr__controls__item.plyr__progress__container {\n  padding-left: calc(10px / 4);\n  padding-left: calc(var(--plyr-control-spacing, 10px) / 4);\n}\n\n.plyr__controls .plyr__controls__item.plyr__time {\n  padding: 0 calc(10px / 2);\n  padding: 0 calc(var(--plyr-control-spacing, 10px) / 2);\n}\n\n.plyr__controls .plyr__controls__item.plyr__progress__container:first-child,\n.plyr__controls .plyr__controls__item.plyr__time + .plyr__time,\n.plyr__controls .plyr__controls__item.plyr__time:first-child {\n  padding-left: 0;\n}\n\n.plyr__controls:empty {\n  display: none;\n}\n\n.plyr [data-plyr='airplay'],\n.plyr [data-plyr='captions'],\n.plyr [data-plyr='fullscreen'],\n.plyr [data-plyr='pip'] {\n  display: none;\n}\n\n.plyr--airplay-supported [data-plyr='airplay'],\n.plyr--captions-enabled [data-plyr='captions'],\n.plyr--fullscreen-enabled [data-plyr='fullscreen'],\n.plyr--pip-supported [data-plyr='pip'] {\n  display: inline-block;\n}\n\n.plyr__menu {\n  display: flex;\n  position: relative;\n}\n\n.plyr__menu .plyr__control svg {\n  transition: transform 0.3s ease;\n}\n\n.plyr__menu .plyr__control[aria-expanded='true'] svg {\n  transform: rotate(90deg);\n}\n\n.plyr__menu .plyr__control[aria-expanded='true'] .plyr__tooltip {\n  display: none;\n}\n\n.plyr__menu__container {\n  animation: plyr-popup 0.2s ease;\n  background: rgba(255, 255, 255, 0.9);\n  background: var(--plyr-menu-background, rgba(255, 255, 255, 0.9));\n  border-radius: 8px;\n  bottom: 100%;\n  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);\n  box-shadow: var(--plyr-menu-shadow, 0 1px 2px rgba(0, 0, 0, 0.15));\n  color: #4a5464;\n  color: var(--plyr-menu-color, #4a5464);\n  font-size: 15px;\n  font-size: var(--plyr-font-size-base, 15px);\n  margin-bottom: 10px;\n  position: absolute;\n  right: -3px;\n  text-align: left;\n  white-space: nowrap;\n  z-index: 3;\n}\n\n.plyr__menu__container > div {\n  overflow: hidden;\n  transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1),\n    width 0.35s cubic-bezier(0.4, 0, 0.2, 1);\n}\n\n.plyr__menu__container::after {\n  border: 4px solid transparent;\n  border: var(--plyr-menu-arrow-size, 4px) solid transparent;\n  border-top-color: rgba(255, 255, 255, 0.9);\n  border-top-color: var(--plyr-menu-background, rgba(255, 255, 255, 0.9));\n  content: '';\n  height: 0;\n  position: absolute;\n  right: calc(((18px / 2) + calc(10px * 0.7)) - (4px / 2));\n  right: calc(\n    (\n        (var(--plyr-control-icon-size, 18px) / 2) +\n          calc(var(--plyr-control-spacing, 10px) * 0.7)\n      ) - (var(--plyr-menu-arrow-size, 4px) / 2)\n  );\n  top: 100%;\n  width: 0;\n}\n\n.plyr__menu__container [role='menu'] {\n  padding: calc(10px * 0.7);\n  padding: calc(var(--plyr-control-spacing, 10px) * 0.7);\n}\n\n.plyr__menu__container [role='menuitem'],\n.plyr__menu__container [role='menuitemradio'] {\n  margin-top: 2px;\n}\n\n.plyr__menu__container [role='menuitem']:first-child,\n.plyr__menu__container [role='menuitemradio']:first-child {\n  margin-top: 0;\n}\n\n.plyr__menu__container .plyr__control {\n  align-items: center;\n  color: #4a5464;\n  color: var(--plyr-menu-color, #4a5464);\n  display: flex;\n  font-size: 13px;\n  font-size: var(--plyr-font-size-menu, var(--plyr-font-size-small, 13px));\n  padding-bottom: calc(calc(10px * 0.7) / 1.5);\n  padding-bottom: calc(calc(var(--plyr-control-spacing, 10px) * 0.7) / 1.5);\n  padding-left: calc(calc(10px * 0.7) * 1.5);\n  padding-left: calc(calc(var(--plyr-control-spacing, 10px) * 0.7) * 1.5);\n  padding-right: calc(calc(10px * 0.7) * 1.5);\n  padding-right: calc(calc(var(--plyr-control-spacing, 10px) * 0.7) * 1.5);\n  padding-top: calc(calc(10px * 0.7) / 1.5);\n  padding-top: calc(calc(var(--plyr-control-spacing, 10px) * 0.7) / 1.5);\n  -webkit-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  width: 100%;\n}\n\n.plyr__menu__container .plyr__control > span {\n  align-items: inherit;\n  display: flex;\n  width: 100%;\n}\n\n.plyr__menu__container .plyr__control::after {\n  border: 4px solid transparent;\n  border: var(--plyr-menu-item-arrow-size, 4px) solid transparent;\n  content: '';\n  position: absolute;\n  top: 50%;\n  transform: translateY(-50%);\n}\n\n.plyr__menu__container .plyr__control--forward {\n  padding-right: calc(calc(10px * 0.7) * 4);\n  padding-right: calc(calc(var(--plyr-control-spacing, 10px) * 0.7) * 4);\n}\n\n.plyr__menu__container .plyr__control--forward::after {\n  border-left-color: #728197;\n  border-left-color: var(--plyr-menu-arrow-color, #728197);\n  right: calc((calc(10px * 0.7) * 1.5) - 4px);\n  right: calc(\n    (calc(var(--plyr-control-spacing, 10px) * 0.7) * 1.5) -\n      var(--plyr-menu-item-arrow-size, 4px)\n  );\n}\n\n.plyr__menu__container .plyr__control--forward.plyr__tab-focus::after,\n.plyr__menu__container .plyr__control--forward:hover::after {\n  border-left-color: currentColor;\n}\n\n.plyr__menu__container .plyr__control--back {\n  font-weight: 400;\n  font-weight: var(--plyr-font-weight-regular, 400);\n  margin: calc(10px * 0.7);\n  margin: calc(var(--plyr-control-spacing, 10px) * 0.7);\n  margin-bottom: calc(calc(10px * 0.7) / 2);\n  margin-bottom: calc(calc(var(--plyr-control-spacing, 10px) * 0.7) / 2);\n  padding-left: calc(calc(10px * 0.7) * 4);\n  padding-left: calc(calc(var(--plyr-control-spacing, 10px) * 0.7) * 4);\n  position: relative;\n  width: calc(100% - (calc(10px * 0.7) * 2));\n  width: calc(100% - (calc(var(--plyr-control-spacing, 10px) * 0.7) * 2));\n}\n\n.plyr__menu__container .plyr__control--back::after {\n  border-right-color: #728197;\n  border-right-color: var(--plyr-menu-arrow-color, #728197);\n  left: calc((calc(10px * 0.7) * 1.5) - 4px);\n  left: calc(\n    (calc(var(--plyr-control-spacing, 10px) * 0.7) * 1.5) -\n      var(--plyr-menu-item-arrow-size, 4px)\n  );\n}\n\n.plyr__menu__container .plyr__control--back::before {\n  background: #dcdfe5;\n  background: var(--plyr-menu-back-border-color, #dcdfe5);\n  box-shadow: 0 1px 0 #fff;\n  box-shadow: 0 1px 0 var(--plyr-menu-back-border-shadow-color, #fff);\n  content: '';\n  height: 1px;\n  left: 0;\n  margin-top: calc(calc(10px * 0.7) / 2);\n  margin-top: calc(calc(var(--plyr-control-spacing, 10px) * 0.7) / 2);\n  overflow: hidden;\n  position: absolute;\n  right: 0;\n  top: 100%;\n}\n\n.plyr__menu__container .plyr__control--back.plyr__tab-focus::after,\n.plyr__menu__container .plyr__control--back:hover::after {\n  border-right-color: currentColor;\n}\n\n.plyr__menu__container .plyr__control[role='menuitemradio'] {\n  padding-left: calc(10px * 0.7);\n  padding-left: calc(var(--plyr-control-spacing, 10px) * 0.7);\n}\n\n.plyr__menu__container .plyr__control[role='menuitemradio']::after,\n.plyr__menu__container .plyr__control[role='menuitemradio']::before {\n  border-radius: 100%;\n}\n\n.plyr__menu__container .plyr__control[role='menuitemradio']::before {\n  background: rgba(0, 0, 0, 0.1);\n  content: '';\n  display: block;\n  flex-shrink: 0;\n  height: 16px;\n  margin-right: 10px;\n  margin-right: var(--plyr-control-spacing, 10px);\n  transition: all 0.3s ease;\n  width: 16px;\n}\n\n.plyr__menu__container .plyr__control[role='menuitemradio']::after {\n  background: #fff;\n  border: 0;\n  height: 6px;\n  left: 12px;\n  opacity: 0;\n  top: 50%;\n  transform: translateY(-50%) scale(0);\n  transition: transform 0.3s ease, opacity 0.3s ease;\n  width: 6px;\n}\n\n.plyr__menu__container\n  .plyr__control[role='menuitemradio'][aria-checked='true']::before {\n  background: #00b3ff;\n  background: var(\n    --plyr-control-toggle-checked-background,\n    var(--plyr-color-main, var(--plyr-color-main, #00b3ff))\n  );\n}\n\n.plyr__menu__container\n  .plyr__control[role='menuitemradio'][aria-checked='true']::after {\n  opacity: 1;\n  transform: translateY(-50%) scale(1);\n}\n\n.plyr__menu__container\n  .plyr__control[role='menuitemradio'].plyr__tab-focus::before,\n.plyr__menu__container .plyr__control[role='menuitemradio']:hover::before {\n  background: rgba(35, 40, 47, 0.1);\n}\n\n.plyr__menu__container .plyr__menu__value {\n  align-items: center;\n  display: flex;\n  margin-left: auto;\n  margin-right: calc((calc(10px * 0.7) - 2) * -1);\n  margin-right: calc((calc(var(--plyr-control-spacing, 10px) * 0.7) - 2) * -1);\n  overflow: hidden;\n  padding-left: calc(calc(10px * 0.7) * 3.5);\n  padding-left: calc(calc(var(--plyr-control-spacing, 10px) * 0.7) * 3.5);\n  pointer-events: none;\n}\n\n.plyr--full-ui input[type='range'] {\n  -webkit-appearance: none;\n  background: 0 0;\n  border: 0;\n  border-radius: calc(13px * 2);\n  border-radius: calc(var(--plyr-range-thumb-height, 13px) * 2);\n  color: #00b3ff;\n  color: var(\n    --plyr-range-fill-background,\n    var(--plyr-color-main, var(--plyr-color-main, #00b3ff))\n  );\n  display: block;\n  height: calc((3px * 2) + 13px);\n  height: calc(\n    (var(--plyr-range-thumb-active-shadow-width, 3px) * 2) +\n      var(--plyr-range-thumb-height, 13px)\n  );\n  margin: 0;\n  padding: 0;\n  transition: box-shadow 0.3s ease;\n  width: 100%;\n}\n\n.plyr--full-ui input[type='range']::-webkit-slider-runnable-track {\n  background: 0 0;\n  border: 0;\n  border-radius: calc(5px / 2);\n  border-radius: calc(var(--plyr-range-track-height, 5px) / 2);\n  height: 5px;\n  height: var(--plyr-range-track-height, 5px);\n  -webkit-transition: box-shadow 0.3s ease;\n  transition: box-shadow 0.3s ease;\n  -webkit-user-select: none;\n  user-select: none;\n  background-image: linear-gradient(to right, currentColor 0, transparent 0);\n  background-image: linear-gradient(\n    to right,\n    currentColor var(--value, 0),\n    transparent var(--value, 0)\n  );\n}\n\n.plyr--full-ui input[type='range']::-webkit-slider-thumb {\n  background: #fff;\n  background: var(--plyr-range-thumb-background, #fff);\n  border: 0;\n  border-radius: 50%;\n  box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2);\n  box-shadow: var(\n    --plyr-range-thumb-shadow,\n    0 1px 1px rgba(35, 40, 47, 0.15),\n    0 0 0 1px rgba(35, 40, 47, 0.2)\n  );\n  height: 13px;\n  height: var(--plyr-range-thumb-height, 13px);\n  position: relative;\n  -webkit-transition: all 0.2s ease;\n  transition: all 0.2s ease;\n  width: 13px;\n  width: var(--plyr-range-thumb-height, 13px);\n  -webkit-appearance: none;\n  margin-top: calc(((13px - 5px) / 2) * -1);\n  margin-top: calc(\n    (\n        (\n            var(--plyr-range-thumb-height, 13px) -\n              var(--plyr-range-track-height, 5px)\n          ) / 2\n      ) * -1\n  );\n}\n\n.plyr--full-ui input[type='range']::-moz-range-track {\n  background: 0 0;\n  border: 0;\n  border-radius: calc(5px / 2);\n  border-radius: calc(var(--plyr-range-track-height, 5px) / 2);\n  height: 5px;\n  height: var(--plyr-range-track-height, 5px);\n  -moz-transition: box-shadow 0.3s ease;\n  transition: box-shadow 0.3s ease;\n  user-select: none;\n}\n\n.plyr--full-ui input[type='range']::-moz-range-thumb {\n  background: #fff;\n  background: var(--plyr-range-thumb-background, #fff);\n  border: 0;\n  border-radius: 50%;\n  box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2);\n  box-shadow: var(\n    --plyr-range-thumb-shadow,\n    0 1px 1px rgba(35, 40, 47, 0.15),\n    0 0 0 1px rgba(35, 40, 47, 0.2)\n  );\n  height: 13px;\n  height: var(--plyr-range-thumb-height, 13px);\n  position: relative;\n  -moz-transition: all 0.2s ease;\n  transition: all 0.2s ease;\n  width: 13px;\n  width: var(--plyr-range-thumb-height, 13px);\n}\n\n.plyr--full-ui input[type='range']::-moz-range-progress {\n  background: currentColor;\n  border-radius: calc(5px / 2);\n  border-radius: calc(var(--plyr-range-track-height, 5px) / 2);\n  height: 5px;\n  height: var(--plyr-range-track-height, 5px);\n}\n\n.plyr--full-ui input[type='range']::-ms-track {\n  background: 0 0;\n  border: 0;\n  border-radius: calc(5px / 2);\n  border-radius: calc(var(--plyr-range-track-height, 5px) / 2);\n  height: 5px;\n  height: var(--plyr-range-track-height, 5px);\n  -ms-transition: box-shadow 0.3s ease;\n  transition: box-shadow 0.3s ease;\n  -ms-user-select: none;\n  user-select: none;\n  color: transparent;\n}\n\n.plyr--full-ui input[type='range']::-ms-fill-upper {\n  background: 0 0;\n  border: 0;\n  border-radius: calc(5px / 2);\n  border-radius: calc(var(--plyr-range-track-height, 5px) / 2);\n  height: 5px;\n  height: var(--plyr-range-track-height, 5px);\n  -ms-transition: box-shadow 0.3s ease;\n  transition: box-shadow 0.3s ease;\n  -ms-user-select: none;\n  user-select: none;\n}\n\n.plyr--full-ui input[type='range']::-ms-fill-lower {\n  background: 0 0;\n  border: 0;\n  border-radius: calc(5px / 2);\n  border-radius: calc(var(--plyr-range-track-height, 5px) / 2);\n  height: 5px;\n  height: var(--plyr-range-track-height, 5px);\n  -ms-transition: box-shadow 0.3s ease;\n  transition: box-shadow 0.3s ease;\n  -ms-user-select: none;\n  user-select: none;\n  background: currentColor;\n}\n\n.plyr--full-ui input[type='range']::-ms-thumb {\n  background: #fff;\n  background: var(--plyr-range-thumb-background, #fff);\n  border: 0;\n  border-radius: 50%;\n  box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2);\n  box-shadow: var(\n    --plyr-range-thumb-shadow,\n    0 1px 1px rgba(35, 40, 47, 0.15),\n    0 0 0 1px rgba(35, 40, 47, 0.2)\n  );\n  height: 13px;\n  height: var(--plyr-range-thumb-height, 13px);\n  position: relative;\n  -ms-transition: all 0.2s ease;\n  transition: all 0.2s ease;\n  width: 13px;\n  width: var(--plyr-range-thumb-height, 13px);\n  margin-top: 0;\n}\n\n.plyr--full-ui input[type='range']::-ms-tooltip {\n  display: none;\n}\n\n.plyr--full-ui input[type='range']:focus {\n  outline: 0;\n}\n\n.plyr--full-ui input[type='range']::-moz-focus-outer {\n  border: 0;\n}\n\n.plyr--full-ui\n  input[type='range'].plyr__tab-focus::-webkit-slider-runnable-track {\n  outline-color: #00b3ff;\n  outline-color: var(\n    --plyr-tab-focus-color,\n    var(--plyr-color-main, var(--plyr-color-main, #00b3ff))\n  );\n  outline-offset: 2px;\n  outline-style: dotted;\n  outline-width: 3px;\n}\n\n.plyr--full-ui input[type='range'].plyr__tab-focus::-moz-range-track {\n  outline-color: #00b3ff;\n  outline-color: var(\n    --plyr-tab-focus-color,\n    var(--plyr-color-main, var(--plyr-color-main, #00b3ff))\n  );\n  outline-offset: 2px;\n  outline-style: dotted;\n  outline-width: 3px;\n}\n\n.plyr--full-ui input[type='range'].plyr__tab-focus::-ms-track {\n  outline-color: #00b3ff;\n  outline-color: var(\n    --plyr-tab-focus-color,\n    var(--plyr-color-main, var(--plyr-color-main, #00b3ff))\n  );\n  outline-offset: 2px;\n  outline-style: dotted;\n  outline-width: 3px;\n}\n\n.plyr__poster {\n  background-color: #000;\n  background-position: 50% 50%;\n  background-repeat: no-repeat;\n  background-size: contain;\n  height: 100%;\n  left: 0;\n  opacity: 0;\n  position: absolute;\n  top: 0;\n  transition: opacity 0.2s ease;\n  width: 100%;\n  z-index: 1;\n}\n\n.plyr--stopped.plyr__poster-enabled .plyr__poster {\n  opacity: 1;\n}\n\n.plyr__time {\n  font-size: 13px;\n  font-size: var(--plyr-font-size-time, var(--plyr-font-size-small, 13px));\n}\n\n.plyr__time + .plyr__time::before {\n  content: '\\2044';\n  margin-right: 10px;\n  margin-right: var(--plyr-control-spacing, 10px);\n}\n\n@media (max-width: calc(768px - 1)) {\n  .plyr__time + .plyr__time {\n    display: none;\n  }\n}\n\n.plyr__tooltip {\n  background: rgba(255, 255, 255, 0.9);\n  background: var(--plyr-tooltip-background, rgba(255, 255, 255, 0.9));\n  border-radius: 3px;\n  border-radius: var(--plyr-tooltip-radius, 3px);\n  bottom: 100%;\n  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);\n  box-shadow: var(--plyr-tooltip-shadow, 0 1px 2px rgba(0, 0, 0, 0.15));\n  color: #4a5464;\n  color: var(--plyr-tooltip-color, #4a5464);\n  font-size: 13px;\n  font-size: var(--plyr-font-size-small, 13px);\n  font-weight: 400;\n  font-weight: var(--plyr-font-weight-regular, 400);\n  left: 50%;\n  line-height: 1.3;\n  margin-bottom: calc(calc(10px / 2) * 2);\n  margin-bottom: calc(calc(var(--plyr-control-spacing, 10px) / 2) * 2);\n  opacity: 0;\n  padding: calc(10px / 2) calc(calc(10px / 2) * 1.5);\n  padding: calc(var(--plyr-control-spacing, 10px) / 2)\n    calc(calc(var(--plyr-control-spacing, 10px) / 2) * 1.5);\n  pointer-events: none;\n  position: absolute;\n  transform: translate(-50%, 10px) scale(0.8);\n  transform-origin: 50% 100%;\n  transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;\n  white-space: nowrap;\n  z-index: 2;\n}\n\n.plyr__tooltip::before {\n  border-left: 4px solid transparent;\n  border-left: var(--plyr-tooltip-arrow-size, 4px) solid transparent;\n  border-right: 4px solid transparent;\n  border-right: var(--plyr-tooltip-arrow-size, 4px) solid transparent;\n  border-top: 4px solid rgba(255, 255, 255, 0.9);\n  border-top: var(--plyr-tooltip-arrow-size, 4px) solid\n    var(--plyr-tooltip-background, rgba(255, 255, 255, 0.9));\n  bottom: calc(4px * -1);\n  bottom: calc(var(--plyr-tooltip-arrow-size, 4px) * -1);\n  content: '';\n  height: 0;\n  left: 50%;\n  position: absolute;\n  transform: translateX(-50%);\n  width: 0;\n  z-index: 2;\n}\n\n.plyr .plyr__control.plyr__tab-focus .plyr__tooltip,\n.plyr .plyr__control:hover .plyr__tooltip,\n.plyr__tooltip--visible {\n  opacity: 1;\n  transform: translate(-50%, 0) scale(1);\n}\n\n.plyr .plyr__control:hover .plyr__tooltip {\n  z-index: 3;\n}\n\n.plyr__controls > .plyr__control:first-child .plyr__tooltip,\n.plyr__controls > .plyr__control:first-child + .plyr__control .plyr__tooltip {\n  left: 0;\n  transform: translate(0, 10px) scale(0.8);\n  transform-origin: 0 100%;\n}\n\n.plyr__controls > .plyr__control:first-child .plyr__tooltip::before,\n.plyr__controls\n  > .plyr__control:first-child\n  + .plyr__control\n  .plyr__tooltip::before {\n  left: calc((18px / 2) + calc(10px * 0.7));\n  left: calc(\n    (var(--plyr-control-icon-size, 18px) / 2) +\n      calc(var(--plyr-control-spacing, 10px) * 0.7)\n  );\n}\n\n.plyr__controls > .plyr__control:last-child .plyr__tooltip {\n  left: auto;\n  right: 0;\n  transform: translate(0, 10px) scale(0.8);\n  transform-origin: 100% 100%;\n}\n\n.plyr__controls > .plyr__control:last-child .plyr__tooltip::before {\n  left: auto;\n  right: calc((18px / 2) + calc(10px * 0.7));\n  right: calc(\n    (var(--plyr-control-icon-size, 18px) / 2) +\n      calc(var(--plyr-control-spacing, 10px) * 0.7)\n  );\n  transform: translateX(50%);\n}\n\n.plyr__controls > .plyr__control:first-child .plyr__tooltip--visible,\n.plyr__controls\n  > .plyr__control:first-child\n  + .plyr__control\n  .plyr__tooltip--visible,\n.plyr__controls\n  > .plyr__control:first-child\n  + .plyr__control.plyr__tab-focus\n  .plyr__tooltip,\n.plyr__controls\n  > .plyr__control:first-child\n  + .plyr__control:hover\n  .plyr__tooltip,\n.plyr__controls > .plyr__control:first-child.plyr__tab-focus .plyr__tooltip,\n.plyr__controls > .plyr__control:first-child:hover .plyr__tooltip,\n.plyr__controls > .plyr__control:last-child .plyr__tooltip--visible,\n.plyr__controls > .plyr__control:last-child.plyr__tab-focus .plyr__tooltip,\n.plyr__controls > .plyr__control:last-child:hover .plyr__tooltip {\n  transform: translate(0, 0) scale(1);\n}\n\n.plyr__progress {\n  left: calc(13px * 0.5);\n  left: calc(var(--plyr-range-thumb-height, 13px) * 0.5);\n  margin-right: 13px;\n  margin-right: var(--plyr-range-thumb-height, 13px);\n  position: relative;\n}\n\n.plyr__progress input[type='range'],\n.plyr__progress__buffer {\n  margin-left: calc(13px * -0.5);\n  margin-left: calc(var(--plyr-range-thumb-height, 13px) * -0.5);\n  margin-right: calc(13px * -0.5);\n  margin-right: calc(var(--plyr-range-thumb-height, 13px) * -0.5);\n  width: calc(100% + 13px);\n  width: calc(100% + var(--plyr-range-thumb-height, 13px));\n}\n\n.plyr__progress input[type='range'] {\n  position: relative;\n  z-index: 2;\n}\n\n.plyr__progress .plyr__tooltip {\n  font-size: 13px;\n  font-size: var(--plyr-font-size-time, var(--plyr-font-size-small, 13px));\n  left: 0;\n}\n\n.plyr__progress__buffer {\n  -webkit-appearance: none;\n  background: 0 0;\n  border: 0;\n  border-radius: 100px;\n  height: 5px;\n  height: var(--plyr-range-track-height, 5px);\n  left: 0;\n  margin-top: calc((5px / 2) * -1);\n  margin-top: calc((var(--plyr-range-track-height, 5px) / 2) * -1);\n  padding: 0;\n  position: absolute;\n  top: 50%;\n}\n\n.plyr__progress__buffer::-webkit-progress-bar {\n  background: 0 0;\n}\n\n.plyr__progress__buffer::-webkit-progress-value {\n  background: currentColor;\n  border-radius: 100px;\n  min-width: 5px;\n  min-width: var(--plyr-range-track-height, 5px);\n  -webkit-transition: width 0.2s ease;\n  transition: width 0.2s ease;\n}\n\n.plyr__progress__buffer::-moz-progress-bar {\n  background: currentColor;\n  border-radius: 100px;\n  min-width: 5px;\n  min-width: var(--plyr-range-track-height, 5px);\n  -moz-transition: width 0.2s ease;\n  transition: width 0.2s ease;\n}\n\n.plyr__progress__buffer::-ms-fill {\n  border-radius: 100px;\n  -ms-transition: width 0.2s ease;\n  transition: width 0.2s ease;\n}\n\n.plyr--loading .plyr__progress__buffer {\n  animation: plyr-progress 1s linear infinite;\n  background-image: linear-gradient(\n    -45deg,\n    rgba(35, 40, 47, 0.6) 25%,\n    transparent 25%,\n    transparent 50%,\n    rgba(35, 40, 47, 0.6) 50%,\n    rgba(35, 40, 47, 0.6) 75%,\n    transparent 75%,\n    transparent\n  );\n  background-image: linear-gradient(\n    -45deg,\n    var(--plyr-progress-loading-background, rgba(35, 40, 47, 0.6)) 25%,\n    transparent 25%,\n    transparent 50%,\n    var(--plyr-progress-loading-background, rgba(35, 40, 47, 0.6)) 50%,\n    var(--plyr-progress-loading-background, rgba(35, 40, 47, 0.6)) 75%,\n    transparent 75%,\n    transparent\n  );\n  background-repeat: repeat-x;\n  background-size: 25px 25px;\n  background-size: var(--plyr-progress-loading-size, 25px)\n    var(--plyr-progress-loading-size, 25px);\n  color: transparent;\n}\n\n.plyr--video.plyr--loading .plyr__progress__buffer {\n  background-color: rgba(255, 255, 255, 0.25);\n  background-color: var(\n    --plyr-video-progress-buffered-background,\n    rgba(255, 255, 255, 0.25)\n  );\n}\n\n.plyr--audio.plyr--loading .plyr__progress__buffer {\n  background-color: rgba(193, 200, 209, 0.6);\n  background-color: var(\n    --plyr-audio-progress-buffered-background,\n    rgba(193, 200, 209, 0.6)\n  );\n}\n\n.plyr__volume {\n  align-items: center;\n  display: flex;\n  max-width: 110px;\n  min-width: 80px;\n  position: relative;\n  width: 20%;\n}\n\n.plyr__volume input[type='range'] {\n  margin-left: calc(10px / 2);\n  margin-left: calc(var(--plyr-control-spacing, 10px) / 2);\n  margin-right: calc(10px / 2);\n  margin-right: calc(var(--plyr-control-spacing, 10px) / 2);\n  position: relative;\n  z-index: 2;\n}\n\n.plyr--is-ios .plyr__volume {\n  min-width: 0;\n  width: auto;\n}\n\n.plyr--audio {\n  display: block;\n}\n\n.plyr--audio .plyr__controls {\n  background: #fff;\n  background: var(--plyr-audio-controls-background, #fff);\n  border-radius: inherit;\n  color: #4a5464;\n  color: var(--plyr-audio-control-color, #4a5464);\n  padding: 10px;\n  padding: var(--plyr-control-spacing, 10px);\n}\n\n.plyr--audio .plyr__control.plyr__tab-focus,\n.plyr--audio .plyr__control:hover,\n.plyr--audio .plyr__control[aria-expanded='true'] {\n  background: #00b3ff;\n  background: var(\n    --plyr-audio-control-background-hover,\n    var(--plyr-color-main, var(--plyr-color-main, #00b3ff))\n  );\n  color: #fff;\n  color: var(--plyr-audio-control-color-hover, #fff);\n}\n\n.plyr--full-ui.plyr--audio input[type='range']::-webkit-slider-runnable-track {\n  background-color: rgba(193, 200, 209, 0.6);\n  background-color: var(\n    --plyr-audio-range-track-background,\n    var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, 0.6))\n  );\n}\n\n.plyr--full-ui.plyr--audio input[type='range']::-moz-range-track {\n  background-color: rgba(193, 200, 209, 0.6);\n  background-color: var(\n    --plyr-audio-range-track-background,\n    var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, 0.6))\n  );\n}\n\n.plyr--full-ui.plyr--audio input[type='range']::-ms-track {\n  background-color: rgba(193, 200, 209, 0.6);\n  background-color: var(\n    --plyr-audio-range-track-background,\n    var(--plyr-audio-progress-buffered-background, rgba(193, 200, 209, 0.6))\n  );\n}\n\n.plyr--full-ui.plyr--audio input[type='range']:active::-webkit-slider-thumb {\n  box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2),\n    0 0 0 3px rgba(35, 40, 47, 0.1);\n  box-shadow: var(\n      --plyr-range-thumb-shadow,\n      0 1px 1px rgba(35, 40, 47, 0.15),\n      0 0 0 1px rgba(35, 40, 47, 0.2)\n    ),\n    0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px)\n      var(--plyr-audio-range-thumb-active-shadow-color, rgba(35, 40, 47, 0.1));\n}\n\n.plyr--full-ui.plyr--audio input[type='range']:active::-moz-range-thumb {\n  box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2),\n    0 0 0 3px rgba(35, 40, 47, 0.1);\n  box-shadow: var(\n      --plyr-range-thumb-shadow,\n      0 1px 1px rgba(35, 40, 47, 0.15),\n      0 0 0 1px rgba(35, 40, 47, 0.2)\n    ),\n    0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px)\n      var(--plyr-audio-range-thumb-active-shadow-color, rgba(35, 40, 47, 0.1));\n}\n\n.plyr--full-ui.plyr--audio input[type='range']:active::-ms-thumb {\n  box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2),\n    0 0 0 3px rgba(35, 40, 47, 0.1);\n  box-shadow: var(\n      --plyr-range-thumb-shadow,\n      0 1px 1px rgba(35, 40, 47, 0.15),\n      0 0 0 1px rgba(35, 40, 47, 0.2)\n    ),\n    0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px)\n      var(--plyr-audio-range-thumb-active-shadow-color, rgba(35, 40, 47, 0.1));\n}\n\n.plyr--audio .plyr__progress__buffer {\n  color: rgba(193, 200, 209, 0.6);\n  color: var(\n    --plyr-audio-progress-buffered-background,\n    rgba(193, 200, 209, 0.6)\n  );\n}\n\n.plyr--video {\n  overflow: hidden;\n}\n\n.plyr--video.plyr--menu-open {\n  overflow: visible;\n}\n\n.plyr__video-wrapper {\n  height: 100%;\n  margin: auto;\n  overflow: hidden;\n  position: relative;\n  width: 100%;\n}\n\n.plyr__video-embed,\n.plyr__video-wrapper--fixed-ratio {\n  height: 0;\n  padding-bottom: 56.25%;\n}\n\n.plyr__video-embed iframe,\n.plyr__video-wrapper--fixed-ratio video {\n  border: 0;\n  left: 0;\n  position: absolute;\n  top: 0;\n}\n\n.plyr--full-ui .plyr__video-embed > .plyr__video-embed__container {\n  padding-bottom: 240%;\n  position: relative;\n  transform: translateY(-38.28125%);\n}\n\n.plyr--video .plyr__controls {\n  background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75));\n  background: var(\n    --plyr-video-controls-background,\n    linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75))\n  );\n  border-bottom-left-radius: inherit;\n  border-bottom-right-radius: inherit;\n  bottom: 0;\n  color: #fff;\n  color: var(--plyr-video-control-color, #fff);\n  left: 0;\n  padding: calc(10px / 2);\n  padding: calc(var(--plyr-control-spacing, 10px) / 2);\n  padding-top: calc(10px * 2);\n  padding-top: calc(var(--plyr-control-spacing, 10px) * 2);\n  position: absolute;\n  right: 0;\n  transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out;\n  z-index: 3;\n}\n\n@media (min-width: 480px) {\n  .plyr--video .plyr__controls {\n    padding: 10px;\n    padding: var(--plyr-control-spacing, 10px);\n    padding-top: calc(10px * 3.5);\n    padding-top: calc(var(--plyr-control-spacing, 10px) * 3.5);\n  }\n}\n\n.plyr--video.plyr--hide-controls .plyr__controls {\n  opacity: 0;\n  pointer-events: none;\n  transform: translateY(100%);\n}\n\n.plyr--video .plyr__control.plyr__tab-focus,\n.plyr--video .plyr__control:hover,\n.plyr--video .plyr__control[aria-expanded='true'] {\n  background: #00b3ff;\n  background: var(\n    --plyr-video-control-background-hover,\n    var(--plyr-color-main, var(--plyr-color-main, #00b3ff))\n  );\n  color: #fff;\n  color: var(--plyr-video-control-color-hover, #fff);\n}\n\n.plyr__control--overlaid {\n  background: #00b3ff;\n  background: var(\n    --plyr-video-control-background-hover,\n    var(--plyr-color-main, var(--plyr-color-main, #00b3ff))\n  );\n  border: 0;\n  border-radius: 100%;\n  color: #fff;\n  color: var(--plyr-video-control-color, #fff);\n  display: none;\n  left: 50%;\n  opacity: 0.9;\n  padding: calc(10px * 1.5);\n  padding: calc(var(--plyr-control-spacing, 10px) * 1.5);\n  position: absolute;\n  top: 50%;\n  transform: translate(-50%, -50%);\n  transition: 0.3s;\n  z-index: 2;\n}\n\n.plyr__control--overlaid svg {\n  left: 2px;\n  position: relative;\n}\n\n.plyr__control--overlaid:focus,\n.plyr__control--overlaid:hover {\n  opacity: 1;\n}\n\n.plyr--playing .plyr__control--overlaid {\n  opacity: 0;\n  visibility: hidden;\n}\n\n.plyr--full-ui.plyr--video .plyr__control--overlaid {\n  display: block;\n}\n\n.plyr--full-ui.plyr--video input[type='range']::-webkit-slider-runnable-track {\n  background-color: rgba(255, 255, 255, 0.25);\n  background-color: var(\n    --plyr-video-range-track-background,\n    var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, 0.25))\n  );\n}\n\n.plyr--full-ui.plyr--video input[type='range']::-moz-range-track {\n  background-color: rgba(255, 255, 255, 0.25);\n  background-color: var(\n    --plyr-video-range-track-background,\n    var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, 0.25))\n  );\n}\n\n.plyr--full-ui.plyr--video input[type='range']::-ms-track {\n  background-color: rgba(255, 255, 255, 0.25);\n  background-color: var(\n    --plyr-video-range-track-background,\n    var(--plyr-video-progress-buffered-background, rgba(255, 255, 255, 0.25))\n  );\n}\n\n.plyr--full-ui.plyr--video input[type='range']:active::-webkit-slider-thumb {\n  box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2),\n    0 0 0 3px rgba(255, 255, 255, 0.5);\n  box-shadow: var(\n      --plyr-range-thumb-shadow,\n      0 1px 1px rgba(35, 40, 47, 0.15),\n      0 0 0 1px rgba(35, 40, 47, 0.2)\n    ),\n    0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px)\n      var(\n        --plyr-audio-range-thumb-active-shadow-color,\n        rgba(255, 255, 255, 0.5)\n      );\n}\n\n.plyr--full-ui.plyr--video input[type='range']:active::-moz-range-thumb {\n  box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2),\n    0 0 0 3px rgba(255, 255, 255, 0.5);\n  box-shadow: var(\n      --plyr-range-thumb-shadow,\n      0 1px 1px rgba(35, 40, 47, 0.15),\n      0 0 0 1px rgba(35, 40, 47, 0.2)\n    ),\n    0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px)\n      var(\n        --plyr-audio-range-thumb-active-shadow-color,\n        rgba(255, 255, 255, 0.5)\n      );\n}\n\n.plyr--full-ui.plyr--video input[type='range']:active::-ms-thumb {\n  box-shadow: 0 1px 1px rgba(35, 40, 47, 0.15), 0 0 0 1px rgba(35, 40, 47, 0.2),\n    0 0 0 3px rgba(255, 255, 255, 0.5);\n  box-shadow: var(\n      --plyr-range-thumb-shadow,\n      0 1px 1px rgba(35, 40, 47, 0.15),\n      0 0 0 1px rgba(35, 40, 47, 0.2)\n    ),\n    0 0 0 var(--plyr-range-thumb-active-shadow-width, 3px)\n      var(\n        --plyr-audio-range-thumb-active-shadow-color,\n        rgba(255, 255, 255, 0.5)\n      );\n}\n\n.plyr--video .plyr__progress__buffer {\n  color: rgba(255, 255, 255, 0.25);\n  color: var(\n    --plyr-video-progress-buffered-background,\n    rgba(255, 255, 255, 0.25)\n  );\n}\n\n.plyr:-webkit-full-screen {\n  background: #000;\n  border-radius: 0 !important;\n  height: 100%;\n  margin: 0;\n  width: 100%;\n}\n\n.plyr:-ms-fullscreen {\n  background: #000;\n  border-radius: 0 !important;\n  height: 100%;\n  margin: 0;\n  width: 100%;\n}\n\n.plyr:fullscreen {\n  background: #000;\n  border-radius: 0 !important;\n  height: 100%;\n  margin: 0;\n  width: 100%;\n}\n\n.plyr:-webkit-full-screen video {\n  height: 100%;\n}\n\n.plyr:-ms-fullscreen video {\n  height: 100%;\n}\n\n.plyr:fullscreen video {\n  height: 100%;\n}\n\n.plyr:-webkit-full-screen .plyr__video-wrapper {\n  height: 100%;\n  position: static;\n}\n\n.plyr:-ms-fullscreen .plyr__video-wrapper {\n  height: 100%;\n  position: static;\n}\n\n.plyr:fullscreen .plyr__video-wrapper {\n  height: 100%;\n  position: static;\n}\n\n.plyr:-webkit-full-screen.plyr--vimeo .plyr__video-wrapper {\n  height: 0;\n  position: relative;\n}\n\n.plyr:-ms-fullscreen.plyr--vimeo .plyr__video-wrapper {\n  height: 0;\n  position: relative;\n}\n\n.plyr:fullscreen.plyr--vimeo .plyr__video-wrapper {\n  height: 0;\n  position: relative;\n}\n\n.plyr:-webkit-full-screen .plyr__control .icon--exit-fullscreen {\n  display: block;\n}\n\n.plyr:-ms-fullscreen .plyr__control .icon--exit-fullscreen {\n  display: block;\n}\n\n.plyr:fullscreen .plyr__control .icon--exit-fullscreen {\n  display: block;\n}\n\n.plyr:-webkit-full-screen .plyr__control .icon--exit-fullscreen + svg {\n  display: none;\n}\n\n.plyr:-ms-fullscreen .plyr__control .icon--exit-fullscreen + svg {\n  display: none;\n}\n\n.plyr:fullscreen .plyr__control .icon--exit-fullscreen + svg {\n  display: none;\n}\n\n.plyr:-webkit-full-screen.plyr--hide-controls {\n  cursor: none;\n}\n\n.plyr:-ms-fullscreen.plyr--hide-controls {\n  cursor: none;\n}\n\n.plyr:fullscreen.plyr--hide-controls {\n  cursor: none;\n}\n\n@media (min-width: 1024px) {\n  .plyr:-webkit-full-screen .plyr__captions {\n    font-size: 21px;\n    font-size: var(--plyr-font-size-xlarge, 21px);\n  }\n\n  .plyr:-ms-fullscreen .plyr__captions {\n    font-size: 21px;\n    font-size: var(--plyr-font-size-xlarge, 21px);\n  }\n\n  .plyr:fullscreen .plyr__captions {\n    font-size: 21px;\n    font-size: var(--plyr-font-size-xlarge, 21px);\n  }\n}\n\n.plyr:-webkit-full-screen {\n  background: #000;\n  border-radius: 0 !important;\n  height: 100%;\n  margin: 0;\n  width: 100%;\n}\n\n.plyr:-webkit-full-screen video {\n  height: 100%;\n}\n\n.plyr:-webkit-full-screen .plyr__video-wrapper {\n  height: 100%;\n  position: static;\n}\n\n.plyr:-webkit-full-screen.plyr--vimeo .plyr__video-wrapper {\n  height: 0;\n  position: relative;\n}\n\n.plyr:-webkit-full-screen .plyr__control .icon--exit-fullscreen {\n  display: block;\n}\n\n.plyr:-webkit-full-screen .plyr__control .icon--exit-fullscreen + svg {\n  display: none;\n}\n\n.plyr:-webkit-full-screen.plyr--hide-controls {\n  cursor: none;\n}\n\n@media (min-width: 1024px) {\n  .plyr:-webkit-full-screen .plyr__captions {\n    font-size: 21px;\n    font-size: var(--plyr-font-size-xlarge, 21px);\n  }\n}\n\n.plyr:-moz-full-screen {\n  background: #000;\n  border-radius: 0 !important;\n  height: 100%;\n  margin: 0;\n  width: 100%;\n}\n\n.plyr:-moz-full-screen video {\n  height: 100%;\n}\n\n.plyr:-moz-full-screen .plyr__video-wrapper {\n  height: 100%;\n  position: static;\n}\n\n.plyr:-moz-full-screen.plyr--vimeo .plyr__video-wrapper {\n  height: 0;\n  position: relative;\n}\n\n.plyr:-moz-full-screen .plyr__control .icon--exit-fullscreen {\n  display: block;\n}\n\n.plyr:-moz-full-screen .plyr__control .icon--exit-fullscreen + svg {\n  display: none;\n}\n\n.plyr:-moz-full-screen.plyr--hide-controls {\n  cursor: none;\n}\n\n@media (min-width: 1024px) {\n  .plyr:-moz-full-screen .plyr__captions {\n    font-size: 21px;\n    font-size: var(--plyr-font-size-xlarge, 21px);\n  }\n}\n\n.plyr:-ms-fullscreen {\n  background: #000;\n  border-radius: 0 !important;\n  height: 100%;\n  margin: 0;\n  width: 100%;\n}\n\n.plyr:-ms-fullscreen video {\n  height: 100%;\n}\n\n.plyr:-ms-fullscreen .plyr__video-wrapper {\n  height: 100%;\n  position: static;\n}\n\n.plyr:-ms-fullscreen.plyr--vimeo .plyr__video-wrapper {\n  height: 0;\n  position: relative;\n}\n\n.plyr:-ms-fullscreen .plyr__control .icon--exit-fullscreen {\n  display: block;\n}\n\n.plyr:-ms-fullscreen .plyr__control .icon--exit-fullscreen + svg {\n  display: none;\n}\n\n.plyr:-ms-fullscreen.plyr--hide-controls {\n  cursor: none;\n}\n\n@media (min-width: 1024px) {\n  .plyr:-ms-fullscreen .plyr__captions {\n    font-size: 21px;\n    font-size: var(--plyr-font-size-xlarge, 21px);\n  }\n}\n\n.plyr--fullscreen-fallback {\n  background: #000;\n  border-radius: 0 !important;\n  height: 100%;\n  margin: 0;\n  width: 100%;\n  bottom: 0;\n  display: block;\n  left: 0;\n  position: fixed;\n  right: 0;\n  top: 0;\n  z-index: 10000000;\n}\n\n.plyr--fullscreen-fallback video {\n  height: 100%;\n}\n\n.plyr--fullscreen-fallback .plyr__video-wrapper {\n  height: 100%;\n  position: static;\n}\n\n.plyr--fullscreen-fallback.plyr--vimeo .plyr__video-wrapper {\n  height: 0;\n  position: relative;\n}\n\n.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen {\n  display: block;\n}\n\n.plyr--fullscreen-fallback .plyr__control .icon--exit-fullscreen + svg {\n  display: none;\n}\n\n.plyr--fullscreen-fallback.plyr--hide-controls {\n  cursor: none;\n}\n\n@media (min-width: 1024px) {\n  .plyr--fullscreen-fallback .plyr__captions {\n    font-size: 21px;\n    font-size: var(--plyr-font-size-xlarge, 21px);\n  }\n}\n\n.plyr__ads {\n  border-radius: inherit;\n  bottom: 0;\n  cursor: pointer;\n  left: 0;\n  overflow: hidden;\n  position: absolute;\n  right: 0;\n  top: 0;\n  z-index: -1;\n}\n\n.plyr__ads > div,\n.plyr__ads > div iframe {\n  height: 100%;\n  position: absolute;\n  width: 100%;\n}\n\n.plyr__ads::after {\n  background: #23282f;\n  border-radius: 2px;\n  bottom: 10px;\n  bottom: var(--plyr-control-spacing, 10px);\n  color: #fff;\n  content: attr(data-badge-text);\n  font-size: 11px;\n  padding: 2px 6px;\n  pointer-events: none;\n  position: absolute;\n  right: 10px;\n  right: var(--plyr-control-spacing, 10px);\n  z-index: 3;\n}\n\n.plyr__ads::after:empty {\n  display: none;\n}\n\n.plyr__cues {\n  background: currentColor;\n  display: block;\n  height: 5px;\n  height: var(--plyr-range-track-height, 5px);\n  left: 0;\n  margin: -var(--plyr-range-track-height, 5px) / 2 0 0;\n  opacity: 0.8;\n  position: absolute;\n  top: 50%;\n  width: 3px;\n  z-index: 3;\n}\n\n.plyr__preview-thumb {\n  background-color: rgba(255, 255, 255, 0.9);\n  background-color: var(--plyr-tooltip-background, rgba(255, 255, 255, 0.9));\n  border-radius: 3px;\n  bottom: 100%;\n  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.15);\n  box-shadow: var(--plyr-tooltip-shadow, 0 1px 2px rgba(0, 0, 0, 0.15));\n  margin-bottom: calc(calc(10px / 2) * 2);\n  margin-bottom: calc(calc(var(--plyr-control-spacing, 10px) / 2) * 2);\n  opacity: 0;\n  padding: 3px;\n  padding: var(--plyr-tooltip-radius, 3px);\n  pointer-events: none;\n  position: absolute;\n  transform: translate(0, 10px) scale(0.8);\n  transform-origin: 50% 100%;\n  transition: transform 0.2s 0.1s ease, opacity 0.2s 0.1s ease;\n  z-index: 2;\n}\n\n.plyr__preview-thumb--is-shown {\n  opacity: 1;\n  transform: translate(0, 0) scale(1);\n}\n\n.plyr__preview-thumb::before {\n  border-left: 4px solid transparent;\n  border-left: var(--plyr-tooltip-arrow-size, 4px) solid transparent;\n  border-right: 4px solid transparent;\n  border-right: var(--plyr-tooltip-arrow-size, 4px) solid transparent;\n  border-top: 4px solid rgba(255, 255, 255, 0.9);\n  border-top: var(--plyr-tooltip-arrow-size, 4px) solid\n    var(--plyr-tooltip-background, rgba(255, 255, 255, 0.9));\n  bottom: calc(4px * -1);\n  bottom: calc(var(--plyr-tooltip-arrow-size, 4px) * -1);\n  content: '';\n  height: 0;\n  left: 50%;\n  position: absolute;\n  transform: translateX(-50%);\n  width: 0;\n  z-index: 2;\n}\n\n.plyr__preview-thumb__image-container {\n  background: #c1c8d1;\n  border-radius: calc(3px - 1px);\n  border-radius: calc(var(--plyr-tooltip-radius, 3px) - 1px);\n  overflow: hidden;\n  position: relative;\n  z-index: 0;\n}\n\n.plyr__preview-thumb__image-container img {\n  height: 100%;\n  left: 0;\n  max-height: none;\n  max-width: none;\n  position: absolute;\n  top: 0;\n  width: 100%;\n}\n\n.plyr__preview-thumb__time-container {\n  bottom: 6px;\n  left: 0;\n  position: absolute;\n  right: 0;\n  white-space: nowrap;\n  z-index: 3;\n}\n\n.plyr__preview-thumb__time-container span {\n  background-color: rgba(0, 0, 0, 0.55);\n  border-radius: calc(3px - 1px);\n  border-radius: calc(var(--plyr-tooltip-radius, 3px) - 1px);\n  color: #fff;\n  font-size: 13px;\n  font-size: var(--plyr-font-size-time, var(--plyr-font-size-small, 13px));\n  padding: 3px 6px;\n}\n\n.plyr__preview-scrubbing {\n  bottom: 0;\n  filter: blur(1px);\n  height: 100%;\n  left: 0;\n  margin: auto;\n  opacity: 0;\n  overflow: hidden;\n  pointer-events: none;\n  position: absolute;\n  right: 0;\n  top: 0;\n  transition: opacity 0.3s ease;\n  width: 100%;\n  z-index: 1;\n}\n\n.plyr__preview-scrubbing--is-shown {\n  opacity: 1;\n}\n\n.plyr__preview-scrubbing img {\n  height: 100%;\n  left: 0;\n  max-height: none;\n  max-width: none;\n  object-fit: contain;\n  position: absolute;\n  top: 0;\n  width: 100%;\n}\n\n.plyr--no-transition {\n  transition: none !important;\n}\n\n.plyr__sr-only {\n  clip: rect(1px, 1px, 1px, 1px);\n  overflow: hidden;\n  border: 0 !important;\n  height: 1px !important;\n  padding: 0 !important;\n  position: absolute !important;\n  width: 1px !important;\n}\n\n.plyr [hidden] {\n  display: none !important;\n}\n"
  },
  {
    "path": "src/assets/css/slider.css",
    "content": "/* rail style */\n.vue-slider-rail {\n  background-color: rgba(128, 128, 128, 0.18);\n  border-radius: 15px;\n}\n\n/* process style */\n.vue-slider-process {\n  background-color: #335eea;\n  border-radius: 15px;\n}\n\n/* dot style */\n.vue-slider-dot-handle {\n  cursor: pointer;\n  width: 100%;\n  height: 100%;\n  border-radius: 50%;\n  background-color: #fff;\n  box-sizing: border-box;\n  box-shadow: 0.5px 0.5px 2px 1px rgba(0, 0, 0, 0.12);\n  visibility: hidden;\n}\n\n/* tooltip style  */\n.vue-slider-dot-tooltip-wrapper {\n  opacity: 0;\n  transition: all 1s;\n}\n\n.vue-slider-dot-tooltip-wrapper-show {\n  opacity: 1;\n}\n\n.vue-slider-dot-tooltip-inner {\n  font-size: 14px;\n  white-space: nowrap;\n  padding: 2px 6px;\n  min-width: 20px;\n  text-align: center;\n  color: #000;\n  border-radius: 5px;\n  border-color: #fff;\n  background-color: #fff;\n  box-sizing: content-box;\n  box-shadow: 0.5px 0.5px 2px 1px rgba(0, 0, 0, 0.08);\n}\n\n/* hover */\n.vue-slider:hover .vue-slider-dot-handle,\n.vue-slider:active .vue-slider-dot-handle {\n  visibility: visible;\n}\n\n/* volume style */\n.volume-control .vue-slider-process {\n  opacity: 0.8;\n  background-color: var(--color-text);\n  border-radius: 15px;\n}\n\n.volume-control:hover .vue-slider-process {\n  background-color: #335eea;\n}\n\n/* nyancat */\n\n.nyancat .vue-slider-rail {\n  background-color: rgba(128, 128, 128, 0.18);\n  padding: 2.5px 0px;\n  border-radius: 0;\n}\n\n.nyancat .vue-slider-process {\n  padding: 0px 1px;\n  top: -2px;\n  border-radius: 0;\n  background: -webkit-gradient(\n    linear,\n    left top,\n    left bottom,\n    color-stop(0, #f00),\n    color-stop(17%, #f90),\n    color-stop(33%, #ff0),\n    color-stop(50%, #3f0),\n    color-stop(67%, #09f),\n    color-stop(83%, #63f)\n  );\n}\n\n.nyancat .vue-slider-dot-handle {\n  background: url('/img/logos/nyancat.gif');\n  background-size: 36px;\n  width: 36px;\n  height: 24px;\n  margin-top: -6px;\n  box-shadow: none;\n  border-radius: 0;\n  box-sizing: border-box;\n  visibility: visible;\n}\n\n.nyancat-stop .vue-slider-dot-handle {\n  background-image: url('/img/logos/nyancat-stop.png');\n  transition: 300ms;\n}\n\n/* lyrics */\n.lyrics-page .vue-slider-rail {\n  background-color: rgba(128, 128, 128, 0.18);\n  border-radius: 2px;\n  height: 4px;\n  opacity: 0.88;\n}\n\n.lyrics-page .vue-slider-process {\n  background-color: #060606;\n}\n\n.lyrics-page .vue-slider-dot-handle {\n  background-color: #060606;\n  box-shadow: unset;\n}\n\n.lyrics-page .vue-slider-dot-tooltip {\n  display: none;\n}\n\nbody[data-theme='dark'] .lyrics-page .vue-slider-process {\n  background-color: #fafafa;\n}\n\nbody[data-theme='dark'] .lyrics-page .vue-slider-dot-handle {\n  background-color: #fff;\n}\n\n.lyrics-page[data-theme='dark'] .vue-slider-rail {\n  background-color: rgba(255, 255, 255, 0.18);\n}\n\n.lyrics-page[data-theme='dark'] .vue-slider-process,\n.lyrics-page[data-theme='dark'] .vue-slider-dot-handle {\n  background-color: #fff;\n}\n"
  },
  {
    "path": "src/assets/icons/index.js",
    "content": "import Vue from 'vue';\nimport SvgIcon from '@/components/SvgIcon';\n\nVue.component('svg-icon', SvgIcon);\nconst requireAll = requireContext => requireContext.keys().map(requireContext);\nconst req = require.context('./', true, /\\.svg$/);\nrequireAll(req);\n"
  },
  {
    "path": "src/background.js",
    "content": "'use strict';\nimport {\n  app,\n  protocol,\n  BrowserWindow,\n  shell,\n  dialog,\n  globalShortcut,\n  nativeTheme,\n  screen,\n} from 'electron';\nimport {\n  isWindows,\n  isMac,\n  isLinux,\n  isDevelopment,\n  isCreateTray,\n  isCreateMpris,\n} from '@/utils/platform';\nimport { createProtocol } from 'vue-cli-plugin-electron-builder/lib';\nimport { startNeteaseMusicApi } from './electron/services';\nimport { initIpcMain } from './electron/ipcMain.js';\nimport { createMenu } from './electron/menu';\nimport { createTray } from '@/electron/tray';\nimport { createTouchBar } from './electron/touchBar';\nimport { createDockMenu } from './electron/dockMenu';\nimport { registerGlobalShortcut } from './electron/globalShortcut';\nimport { autoUpdater } from 'electron-updater';\nimport installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';\nimport { EventEmitter } from 'events';\nimport express from 'express';\nimport expressProxy from 'express-http-proxy';\nimport Store from 'electron-store';\nimport { createMpris, createDbus } from '@/electron/mpris';\nimport { spawn } from 'child_process';\nconst clc = require('cli-color');\nconst log = text => {\n  console.log(`${clc.blueBright('[background.js]')} ${text}`);\n};\n\nconst closeOnLinux = (e, win, store) => {\n  let closeOpt = store.get('settings.closeAppOption');\n  if (closeOpt !== 'exit') {\n    e.preventDefault();\n  }\n\n  if (closeOpt === 'ask') {\n    dialog\n      .showMessageBox({\n        type: 'info',\n        title: 'Information',\n        cancelId: 2,\n        defaultId: 0,\n        message: '确定要关闭吗？',\n        buttons: ['最小化到托盘', '直接退出'],\n        checkboxLabel: '记住我的选择',\n      })\n      .then(result => {\n        if (result.checkboxChecked && result.response !== 2) {\n          win.webContents.send(\n            'rememberCloseAppOption',\n            result.response === 0 ? 'minimizeToTray' : 'exit'\n          );\n        }\n\n        if (result.response === 0) {\n          win.hide(); //调用 最小化实例方法\n        } else if (result.response === 1) {\n          win = null;\n          app.exit(); //exit()直接关闭客户端，不会执行quit();\n        }\n      })\n      .catch(err => {\n        log(err);\n      });\n  } else if (closeOpt === 'exit') {\n    win = null;\n    app.quit();\n  } else {\n    win.hide();\n  }\n};\n\nclass Background {\n  constructor() {\n    this.window = null;\n    this.ypmTrayImpl = null;\n    this.store = new Store({\n      windowWidth: {\n        width: { type: 'number', default: 1440 },\n        height: { type: 'number', default: 840 },\n      },\n    });\n    this.neteaseMusicAPI = null;\n    this.expressApp = null;\n    this.willQuitApp = !isMac;\n\n    this.init();\n  }\n\n  init() {\n    log('initializing');\n\n    // Make sure the app is singleton.\n    if (!app.requestSingleInstanceLock()) return app.quit();\n\n    // start netease music api\n    this.neteaseMusicAPI = startNeteaseMusicApi();\n\n    // create Express app\n    this.createExpressApp();\n\n    // Scheme must be registered before the app is ready\n    protocol.registerSchemesAsPrivileged([\n      { scheme: 'app', privileges: { secure: true, standard: true } },\n    ]);\n\n    // handle app events\n    this.handleAppEvents();\n\n    // disable chromium mpris\n    if (isCreateMpris) {\n      app.commandLine.appendSwitch(\n        'disable-features',\n        'HardwareMediaKeyHandling,MediaSessionService'\n      );\n    }\n  }\n\n  async initDevtools() {\n    // Install Vue Devtools extension\n    try {\n      await installExtension(VUEJS_DEVTOOLS);\n    } catch (e) {\n      console.error('Vue Devtools failed to install:', e.toString());\n    }\n\n    // Exit cleanly on request from parent process in development mode.\n    if (isWindows) {\n      process.on('message', data => {\n        if (data === 'graceful-exit') {\n          app.quit();\n        }\n      });\n    } else {\n      process.on('SIGTERM', () => {\n        app.quit();\n      });\n    }\n  }\n\n  createExpressApp() {\n    log('creating express app');\n\n    const expressApp = express();\n    expressApp.use('/', express.static(__dirname + '/'));\n    expressApp.use('/api', expressProxy('http://127.0.0.1:10754'));\n    expressApp.use('/player', (req, res) => {\n      this.window.webContents\n        .executeJavaScript('window.yesplaymusic.player')\n        .then(result => {\n          res.send({\n            currentTrack: result._isPersonalFM\n              ? result._personalFMTrack\n              : result._currentTrack,\n            progress: result._progress,\n          });\n        });\n    });\n    this.expressApp = expressApp.listen(27232, '127.0.0.1');\n  }\n\n  createWindow() {\n    log('creating app window');\n\n    const appearance = this.store.get('settings.appearance');\n    const showLibraryDefault = this.store.get('settings.showLibraryDefault');\n\n    const options = {\n      width: this.store.get('window.width') || 1440,\n      height: this.store.get('window.height') || 840,\n      minWidth: 1080,\n      minHeight: 720,\n      titleBarStyle: 'hiddenInset',\n      frame: !(\n        isWindows ||\n        (isLinux && this.store.get('settings.linuxEnableCustomTitlebar'))\n      ),\n      title: 'YesPlayMusic',\n      show: false,\n      webPreferences: {\n        webSecurity: false,\n        nodeIntegration: true,\n        enableRemoteModule: true,\n        contextIsolation: false,\n      },\n      backgroundColor:\n        ((appearance === undefined || appearance === 'auto') &&\n          nativeTheme.shouldUseDarkColors) ||\n        appearance === 'dark'\n          ? '#222'\n          : '#fff',\n    };\n\n    if (this.store.get('window.x') && this.store.get('window.y')) {\n      let x = this.store.get('window.x');\n      let y = this.store.get('window.y');\n\n      let displays = screen.getAllDisplays();\n      let isResetWindiw = false;\n      if (displays.length === 1) {\n        let { bounds } = displays[0];\n        if (\n          x < bounds.x ||\n          x > bounds.x + bounds.width - 50 ||\n          y < bounds.y ||\n          y > bounds.y + bounds.height - 50\n        ) {\n          isResetWindiw = true;\n        }\n      } else {\n        isResetWindiw = true;\n        for (let i = 0; i < displays.length; i++) {\n          let { bounds } = displays[i];\n          if (\n            x > bounds.x &&\n            x < bounds.x + bounds.width &&\n            y > bounds.y &&\n            y < bounds.y - bounds.height\n          ) {\n            // 检测到APP窗口当前处于一个可用的屏幕里，break\n            isResetWindiw = false;\n            break;\n          }\n        }\n      }\n\n      if (!isResetWindiw) {\n        options.x = x;\n        options.y = y;\n      }\n    }\n\n    this.window = new BrowserWindow(options);\n\n    // hide menu bar on Microsoft Windows and Linux\n    this.window.setMenuBarVisibility(false);\n\n    if (process.env.WEBPACK_DEV_SERVER_URL) {\n      // Load the url of the dev server if in development mode\n      this.window.loadURL(\n        showLibraryDefault\n          ? `${process.env.WEBPACK_DEV_SERVER_URL}/#/library`\n          : process.env.WEBPACK_DEV_SERVER_URL\n      );\n      if (!process.env.IS_TEST) this.window.webContents.openDevTools();\n    } else {\n      createProtocol('app');\n      this.window.loadURL(\n        showLibraryDefault\n          ? 'http://localhost:27232/#/library'\n          : 'http://localhost:27232'\n      );\n    }\n  }\n\n  checkForUpdates() {\n    if (isDevelopment) return;\n    log('checkForUpdates');\n    autoUpdater.checkForUpdatesAndNotify();\n\n    const showNewVersionMessage = info => {\n      dialog\n        .showMessageBox({\n          title: '发现新版本 v' + info.version,\n          message: '发现新版本 v' + info.version,\n          detail: '是否前往 GitHub 下载新版本安装包？',\n          buttons: ['下载', '取消'],\n          type: 'question',\n          noLink: true,\n        })\n        .then(result => {\n          if (result.response === 0) {\n            shell.openExternal(\n              'https://github.com/qier222/YesPlayMusic/releases'\n            );\n          }\n        });\n    };\n\n    autoUpdater.on('update-available', info => {\n      showNewVersionMessage(info);\n    });\n  }\n\n  handleWindowEvents() {\n    this.window.once('ready-to-show', () => {\n      log('window ready-to-show event');\n      this.window.show();\n      this.store.set('window', this.window.getBounds());\n    });\n\n    this.window.on('close', e => {\n      log('window close event');\n\n      if (isLinux) {\n        closeOnLinux(e, this.window, this.store);\n      } else if (isMac) {\n        if (this.willQuitApp) {\n          this.window = null;\n          app.quit();\n        } else {\n          e.preventDefault();\n          this.window.hide();\n        }\n      } else {\n        let closeOpt = this.store.get('settings.closeAppOption');\n        if (this.willQuitApp && (closeOpt === 'exit' || closeOpt === 'ask')) {\n          this.window = null;\n          app.quit();\n        } else {\n          e.preventDefault();\n          this.window.hide();\n        }\n      }\n    });\n\n    this.window.on('resized', () => {\n      this.store.set('window', this.window.getBounds());\n    });\n\n    this.window.on('moved', () => {\n      this.store.set('window', this.window.getBounds());\n    });\n\n    this.window.on('maximize', () => {\n      this.window.webContents.send('isMaximized', true);\n    });\n\n    this.window.on('unmaximize', () => {\n      this.window.webContents.send('isMaximized', false);\n    });\n\n    this.window.webContents.on('new-window', function (e, url) {\n      e.preventDefault();\n      log('open url');\n      const excludeHosts = ['www.last.fm'];\n      const exclude = excludeHosts.find(host => url.includes(host));\n      if (exclude) {\n        const newWindow = new BrowserWindow({\n          width: 800,\n          height: 600,\n          titleBarStyle: 'default',\n          title: 'YesPlayMusic',\n          webPreferences: {\n            webSecurity: false,\n            nodeIntegration: true,\n            enableRemoteModule: true,\n            contextIsolation: false,\n          },\n        });\n        newWindow.loadURL(url);\n        return;\n      }\n      shell.openExternal(url);\n    });\n  }\n\n  handleAppEvents() {\n    app.on('ready', async () => {\n      // This method will be called when Electron has finished\n      // initialization and is ready to create browser windows.\n      // Some APIs can only be used after this event occurs.\n      log('app ready event');\n\n      // for development\n      if (isDevelopment) {\n        this.initDevtools();\n      }\n\n      // create window\n      this.createWindow();\n      this.window.once('ready-to-show', () => {\n        this.window.show();\n      });\n      this.handleWindowEvents();\n\n      // create tray\n      if (isCreateTray) {\n        this.trayEventEmitter = new EventEmitter();\n        this.ypmTrayImpl = createTray(\n          this.window,\n          this.trayEventEmitter,\n          this.store\n        );\n      }\n\n      // init ipcMain\n      initIpcMain(this.window, this.store, this.trayEventEmitter);\n\n      // set proxy\n      const proxyRules = this.store.get('proxy');\n      if (proxyRules) {\n        this.window.webContents.session.setProxy({ proxyRules }, result => {\n          log('finished setProxy', result);\n        });\n      }\n\n      // check for updates\n      this.checkForUpdates();\n\n      // create menu\n      createMenu(this.window, this.store);\n\n      // create dock menu for macOS\n      const createdDockMenu = createDockMenu(this.window);\n      if (createDockMenu && app.dock) app.dock.setMenu(createdDockMenu);\n\n      // create touch bar\n      const createdTouchBar = createTouchBar(this.window);\n      if (createdTouchBar) this.window.setTouchBar(createdTouchBar);\n\n      // register global shortcuts\n      if (this.store.get('settings.enableGlobalShortcut') !== false) {\n        registerGlobalShortcut(this.window, this.store);\n      }\n\n      // try to start osdlyrics process on start\n      if (this.store.get('settings.enableOsdlyricsSupport')) {\n        await createDbus(this.window);\n        log('try to start osdlyrics process');\n        const osdlyricsProcess = spawn('osdlyrics');\n\n        osdlyricsProcess.on('error', err => {\n          log(`failed to start osdlyrics: ${err.message}`);\n        });\n\n        osdlyricsProcess.on('exit', (code, signal) => {\n          log(`osdlyrics process exited with code ${code}, signal ${signal}`);\n        });\n      }\n\n      // create mpris\n      if (isCreateMpris) {\n        createMpris(this.window);\n      }\n    });\n\n    app.on('activate', () => {\n      // On macOS it's common to re-create a window in the app when the\n      // dock icon is clicked and there are no other windows open.\n      log('app activate event');\n      if (this.window === null) {\n        this.createWindow();\n      } else {\n        this.window.show();\n      }\n    });\n\n    app.on('window-all-closed', () => {\n      if (!isMac) {\n        app.quit();\n      }\n    });\n\n    app.on('before-quit', () => {\n      this.willQuitApp = true;\n    });\n\n    app.on('quit', () => {\n      this.expressApp.close();\n    });\n\n    app.on('will-quit', () => {\n      // unregister all global shortcuts\n      globalShortcut.unregisterAll();\n    });\n\n    if (!isMac) {\n      app.on('second-instance', (e, cl, wd) => {\n        if (this.window) {\n          this.window.show();\n          if (this.window.isMinimized()) {\n            this.window.restore();\n          }\n          this.window.focus();\n        }\n      });\n    }\n  }\n}\n\nnew Background();\n"
  },
  {
    "path": "src/components/ArtistsInLine.vue",
    "content": "<template>\n  <span class=\"artist-in-line\">\n    {{ computedPrefix }}\n    <span v-for=\"(ar, index) in filteredArtists\" :key=\"index\">\n      <router-link v-if=\"ar.id !== 0\" :to=\"`/artist/${ar.id}`\">{{\n        ar.name\n      }}</router-link>\n      <span v-else>{{ ar.name }}</span>\n      <span v-if=\"index !== filteredArtists.length - 1\" class=\"separator\"\n        >,</span\n      >\n    </span>\n  </span>\n</template>\n\n<script>\nexport default {\n  name: 'ArtistInLine',\n  props: {\n    artists: {\n      type: Array,\n      required: true,\n    },\n    exclude: {\n      type: String,\n      default: '',\n    },\n    prefix: {\n      type: String,\n      default: '',\n    },\n  },\n  computed: {\n    filteredArtists() {\n      return this.artists.filter(a => a.name !== this.exclude);\n    },\n    computedPrefix() {\n      if (this.filteredArtists.length !== 0) return this.prefix;\n      else return '';\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.separator {\n  /* make separator distinct enough in long list */\n  margin-left: 1px;\n  margin-right: 4px;\n  position: relative;\n  top: 0.5px;\n}\n</style>\n"
  },
  {
    "path": "src/components/ButtonIcon.vue",
    "content": "<template>\n  <button class=\"button-icon\"><slot></slot></button>\n</template>\n\n<script>\nexport default {\n  name: 'ButtonIcon',\n};\n</script>\n\n<style lang=\"scss\" scoped>\nbutton {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 8px;\n  background: transparent;\n  margin: 4px;\n  border-radius: 25%;\n  transition: 0.2s;\n  .svg-icon {\n    color: var(--color-text);\n    height: 16px;\n    width: 16px;\n  }\n  &:first-child {\n    margin-left: 0;\n  }\n  &:hover {\n    background: var(--color-secondary-bg-for-transparent);\n  }\n  &:active {\n    transform: scale(0.92);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/ButtonTwoTone.vue",
    "content": "<template>\n  <button :style=\"buttonStyle\" :class=\"color\">\n    <svg-icon\n      v-if=\"iconClass !== null\"\n      :icon-class=\"iconClass\"\n      :style=\"{ marginRight: iconButton ? '0px' : '8px' }\"\n    />\n    <slot></slot>\n  </button>\n</template>\n\n<script>\nexport default {\n  name: 'ButtonTwoTone',\n  props: {\n    iconClass: {\n      type: String,\n      default: null,\n    },\n    iconButton: {\n      type: Boolean,\n      default: false,\n    },\n    horizontalPadding: {\n      type: Number,\n      default: 16,\n    },\n    color: {\n      type: String,\n      default: 'blue',\n    },\n    backgroundColor: {\n      type: String,\n      default: '',\n    },\n    textColor: {\n      type: String,\n      default: '',\n    },\n    shape: {\n      type: String,\n      default: 'square',\n    },\n  },\n  computed: {\n    buttonStyle() {\n      let styles = {\n        borderRadius: this.shape === 'round' ? '50%' : '8px',\n        padding: `8px ${this.horizontalPadding}px`,\n        // height: \"38px\",\n        width: this.shape === 'round' ? '38px' : 'auto',\n      };\n      if (this.backgroundColor !== '')\n        styles.backgroundColor = this.backgroundColor;\n      if (this.textColor !== '') styles.color = this.textColor;\n      return styles;\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\nbutton {\n  height: 40px;\n  min-width: 40px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 18px;\n  line-height: 18px;\n  font-weight: 600;\n  background-color: var(--color-primary-bg);\n  color: var(--color-primary);\n  margin-right: 12px;\n  transition: 0.2s;\n  user-select: none;\n  .svg-icon {\n    width: 16px;\n    height: 16px;\n  }\n  &:hover {\n    transform: scale(1.06);\n  }\n  &:active {\n    transform: scale(0.94);\n  }\n}\nbutton.grey {\n  background-color: var(--color-secondary-bg);\n  color: var(--color-text);\n  opacity: 0.78;\n}\nbutton.transparent {\n  background-color: transparent;\n}\n</style>\n"
  },
  {
    "path": "src/components/ContextMenu.vue",
    "content": "<template>\n  <div ref=\"contextMenu\" class=\"context-menu\">\n    <div\n      v-if=\"showMenu\"\n      ref=\"menu\"\n      class=\"menu\"\n      tabindex=\"-1\"\n      :style=\"{ top: top, left: left }\"\n      @blur=\"closeMenu\"\n      @click=\"closeMenu\"\n    >\n      <slot></slot>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapState } from 'vuex';\n\nexport default {\n  name: 'ContextMenu',\n  data() {\n    return {\n      showMenu: false,\n      top: '0px',\n      left: '0px',\n    };\n  },\n  computed: {\n    ...mapState(['player']),\n  },\n  methods: {\n    setMenu(top, left) {\n      let heightOffset = this.player.enabled ? 64 : 0;\n      let largestHeight =\n        window.innerHeight - this.$refs.menu.offsetHeight - heightOffset;\n      let largestWidth = window.innerWidth - this.$refs.menu.offsetWidth - 25;\n      if (top > largestHeight) top = largestHeight;\n      if (left > largestWidth) left = largestWidth;\n      this.top = top + 'px';\n      this.left = left + 'px';\n    },\n\n    closeMenu() {\n      this.showMenu = false;\n      if (this.$parent.closeMenu !== undefined) {\n        this.$parent.closeMenu();\n      }\n      this.$store.commit('enableScrolling', true);\n    },\n\n    openMenu(e) {\n      this.showMenu = true;\n      this.$nextTick(\n        function () {\n          this.$refs.menu.focus();\n          this.setMenu(e.y, e.x);\n        }.bind(this)\n      );\n      e.preventDefault();\n      this.$store.commit('enableScrolling', false);\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.context-menu {\n  width: 100%;\n  height: 100%;\n  user-select: none;\n}\n\n.menu {\n  position: fixed;\n  min-width: 136px;\n  max-width: 240px;\n  list-style: none;\n  background: rgba(255, 255, 255, 0.88);\n  box-shadow: 0 6px 12px -4px rgba(0, 0, 0, 0.08);\n  border: 1px solid rgba(0, 0, 0, 0.06);\n  backdrop-filter: blur(12px);\n  border-radius: 12px;\n  box-sizing: border-box;\n  padding: 6px;\n  z-index: 1000;\n  -webkit-app-region: no-drag;\n  transition: background 125ms ease-out, opacity 125ms ease-out,\n    transform 125ms ease-out;\n\n  &:focus {\n    outline: none;\n  }\n}\n\n[data-theme='dark'] {\n  .menu {\n    background: rgba(36, 36, 36, 0.78);\n    backdrop-filter: blur(16px) contrast(120%) brightness(60%);\n    border: 1px solid rgba(255, 255, 255, 0.08);\n    box-shadow: 0 0 6px rgba(255, 255, 255, 0.08);\n  }\n  .menu .item:hover {\n    color: var(--color-text);\n  }\n}\n\n@supports (-moz-appearance: none) {\n  .menu {\n    background-color: var(--color-body-bg) !important;\n  }\n}\n\n.menu .item {\n  font-weight: 600;\n  font-size: 14px;\n  padding: 10px 14px;\n  border-radius: 8px;\n  cursor: default;\n  color: var(--color-text);\n  display: flex;\n  align-items: center;\n  &:hover {\n    color: var(--color-primary);\n    background: var(--color-primary-bg-for-transparent);\n    transition: opacity 125ms ease-out, transform 125ms ease-out;\n  }\n  &:active {\n    opacity: 0.75;\n    transform: scale(0.95);\n  }\n\n  .svg-icon {\n    height: 16px;\n    width: 16px;\n    margin-right: 5px;\n  }\n}\n\nhr {\n  margin: 4px 10px;\n  background: rgba(128, 128, 128, 0.18);\n  height: 1px;\n  box-shadow: none;\n  border: none;\n}\n\n.item-info {\n  padding: 10px 10px;\n  display: flex;\n  align-items: center;\n  color: var(--color-text);\n  cursor: default;\n  img {\n    height: 38px;\n    width: 38px;\n    border-radius: 4px;\n  }\n  .info {\n    margin-left: 10px;\n  }\n  .title {\n    font-size: 16px;\n    font-weight: 600;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 1;\n    overflow: hidden;\n    word-break: break-all;\n  }\n  .subtitle {\n    font-size: 12px;\n    opacity: 0.68;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 1;\n    overflow: hidden;\n    word-break: break-all;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/Cover.vue",
    "content": "<template>\n  <div\n    class=\"cover\"\n    :class=\"{ 'cover-hover': coverHover }\"\n    @mouseover=\"focus = true\"\n    @mouseleave=\"focus = false\"\n    @click=\"clickCoverToPlay ? play() : goTo()\"\n  >\n    <div class=\"cover-container\">\n      <div class=\"shade\">\n        <button\n          v-show=\"focus\"\n          class=\"play-button\"\n          :style=\"playButtonStyles\"\n          @click.stop=\"play()\"\n          ><svg-icon icon-class=\"play\" />\n        </button>\n      </div>\n      <img :src=\"imageUrl\" :style=\"imageStyles\" loading=\"lazy\" />\n      <transition v-if=\"coverHover || alwaysShowShadow\" name=\"fade\">\n        <div\n          v-show=\"focus || alwaysShowShadow\"\n          class=\"shadow\"\n          :style=\"shadowStyles\"\n        ></div>\n      </transition>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  props: {\n    id: { type: Number, required: true },\n    type: { type: String, required: true },\n    imageUrl: { type: String, required: true },\n    fixedSize: { type: Number, default: 0 },\n    playButtonSize: { type: Number, default: 22 },\n    coverHover: { type: Boolean, default: true },\n    alwaysShowPlayButton: { type: Boolean, default: true },\n    alwaysShowShadow: { type: Boolean, default: false },\n    clickCoverToPlay: { type: Boolean, default: false },\n    shadowMargin: { type: Number, default: 12 },\n    radius: { type: Number, default: 12 },\n  },\n  data() {\n    return {\n      focus: false,\n    };\n  },\n  computed: {\n    imageStyles() {\n      let styles = {};\n      if (this.fixedSize !== 0) {\n        styles.width = this.fixedSize + 'px';\n        styles.height = this.fixedSize + 'px';\n      }\n      if (this.type === 'artist') styles.borderRadius = '50%';\n      return styles;\n    },\n    playButtonStyles() {\n      let styles = {};\n      styles.width = this.playButtonSize + '%';\n      styles.height = this.playButtonSize + '%';\n      return styles;\n    },\n    shadowStyles() {\n      let styles = {};\n      styles.backgroundImage = `url(${this.imageUrl})`;\n      if (this.type === 'artist') styles.borderRadius = '50%';\n      return styles;\n    },\n  },\n  methods: {\n    play() {\n      const player = this.$store.state.player;\n      const playActions = {\n        album: player.playAlbumByID,\n        playlist: player.playPlaylistByID,\n        artist: player.playArtistByID,\n      };\n      playActions[this.type].bind(player)(this.id);\n    },\n    goTo() {\n      this.$router.push({ name: this.type, params: { id: this.id } });\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.cover {\n  position: relative;\n  transition: transform 0.3s;\n}\n.cover-container {\n  position: relative;\n}\nimg {\n  border-radius: 0.75em;\n  width: 100%;\n  user-select: none;\n  aspect-ratio: 1 / 1;\n  border: 1px solid rgba(0, 0, 0, 0.04);\n}\n\n.cover-hover {\n  &:hover {\n    cursor: pointer;\n    /* transform: scale(1.02); */\n  }\n}\n\n.shade {\n  position: absolute;\n  top: 0;\n  height: calc(100% - 3px);\n  width: 100%;\n  background: transparent;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n.play-button {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  color: white;\n  backdrop-filter: blur(8px);\n  background: rgba(255, 255, 255, 0.14);\n  border: 1px solid rgba(255, 255, 255, 0.08);\n  height: 22%;\n  width: 22%;\n  border-radius: 50%;\n  cursor: default;\n  transition: 0.2s;\n  .svg-icon {\n    width: 50%;\n    margin: {\n      left: 4px;\n    }\n  }\n  &:hover {\n    background: rgba(255, 255, 255, 0.28);\n  }\n  &:active {\n    transform: scale(0.94);\n  }\n}\n\n.shadow {\n  position: absolute;\n  top: 12px;\n  height: 100%;\n  width: 100%;\n  filter: blur(16px) opacity(0.6);\n  transform: scale(0.92, 0.96);\n  z-index: -1;\n  background-size: cover;\n  border-radius: 0.75em;\n  aspect-ratio: 1 / 1;\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s;\n}\n.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/CoverRow.vue",
    "content": "<template>\n  <div class=\"cover-row\" :style=\"rowStyles\">\n    <div\n      v-for=\"item in items\"\n      :key=\"item.id\"\n      class=\"item\"\n      :class=\"{ artist: type === 'artist' }\"\n    >\n      <Cover\n        :id=\"item.id\"\n        :image-url=\"getImageUrl(item)\"\n        :type=\"type\"\n        :play-button-size=\"type === 'artist' ? 26 : playButtonSize\"\n      />\n      <div class=\"text\">\n        <div v-if=\"showPlayCount\" class=\"info\">\n          <span class=\"play-count\"\n            ><svg-icon icon-class=\"play\" />{{\n              item.playCount | formatPlayCount\n            }}\n          </span>\n        </div>\n        <div class=\"title\" :style=\"{ fontSize: subTextFontSize }\">\n          <span v-if=\"isExplicit(item)\" class=\"explicit-symbol\"\n            ><ExplicitSymbol\n          /></span>\n          <span v-if=\"isPrivacy(item)\" class=\"lock-icon\">\n            <svg-icon icon-class=\"lock\"\n          /></span>\n          <router-link :to=\"getTitleLink(item)\">{{ item.name }}</router-link>\n        </div>\n        <div v-if=\"type !== 'artist' && subText !== 'none'\" class=\"info\">\n          <span v-html=\"getSubText(item)\"></span>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport Cover from '@/components/Cover.vue';\nimport ExplicitSymbol from '@/components/ExplicitSymbol.vue';\n\nexport default {\n  name: 'CoverRow',\n  components: {\n    Cover,\n    ExplicitSymbol,\n  },\n  props: {\n    items: { type: Array, required: true },\n    type: { type: String, required: true },\n    subText: { type: String, default: 'none' },\n    subTextFontSize: { type: String, default: '16px' },\n    showPlayCount: { type: Boolean, default: false },\n    columnNumber: { type: Number, default: 5 },\n    gap: { type: String, default: '44px 24px' },\n    playButtonSize: { type: Number, default: 22 },\n  },\n  computed: {\n    rowStyles() {\n      return {\n        'grid-template-columns': `repeat(${this.columnNumber}, 1fr)`,\n        gap: this.gap,\n      };\n    },\n  },\n  methods: {\n    getSubText(item) {\n      if (this.subText === 'copywriter') return item.copywriter;\n      if (this.subText === 'description') return item.description;\n      if (this.subText === 'updateFrequency') return item.updateFrequency;\n      if (this.subText === 'creator') return 'by ' + item.creator.nickname;\n      if (this.subText === 'releaseYear')\n        return new Date(item.publishTime).getFullYear();\n      if (this.subText === 'artist') {\n        if (item.artist !== undefined)\n          return `<a href=\"/artist/${item.artist.id}\">${item.artist.name}</a>`;\n        if (item.artists !== undefined)\n          return `<a href=\"/artist/${item.artists[0].id}\">${item.artists[0].name}</a>`;\n      }\n      if (this.subText === 'albumType+releaseYear') {\n        let albumType = item.type;\n        if (item.type === 'EP/Single') {\n          albumType = item.size === 1 ? 'Single' : 'EP';\n        } else if (item.type === 'Single') {\n          albumType = 'Single';\n        } else if (item.type === '专辑') {\n          albumType = 'Album';\n        }\n        return `${albumType} · ${new Date(item.publishTime).getFullYear()}`;\n      }\n      if (this.subText === 'appleMusic') return 'by Apple Music';\n    },\n    isPrivacy(item) {\n      return this.type === 'playlist' && item.privacy === 10;\n    },\n    isExplicit(item) {\n      return this.type === 'album' && (item.mark & 1048576) === 1048576;\n    },\n    getTitleLink(item) {\n      return `/${this.type}/${item.id}`;\n    },\n    getImageUrl(item) {\n      if (item.img1v1Url) {\n        let img1v1ID = item.img1v1Url.split('/');\n        img1v1ID = img1v1ID[img1v1ID.length - 1];\n        if (img1v1ID === '5639395138885805.jpg') {\n          // 没有头像的歌手，网易云返回的img1v1Url并不是正方形的 😅😅😅\n          return 'https://p2.music.126.net/VnZiScyynLG7atLIZ2YPkw==/18686200114669622.jpg?param=512y512';\n        }\n      }\n      let img = item.img1v1Url || item.picUrl || item.coverImgUrl;\n      return `${img?.replace('http://', 'https://')}?param=512y512`;\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.cover-row {\n  display: grid;\n}\n\n.item {\n  color: var(--color-text);\n  .text {\n    margin-top: 8px;\n    .title {\n      font-size: 16px;\n      font-weight: 600;\n      line-height: 20px;\n      display: -webkit-box;\n      -webkit-box-orient: vertical;\n      -webkit-line-clamp: 2;\n      overflow: hidden;\n      word-break: break-all;\n    }\n    .info {\n      font-size: 12px;\n      opacity: 0.68;\n      line-height: 18px;\n      display: -webkit-box;\n      -webkit-box-orient: vertical;\n      -webkit-line-clamp: 2;\n      overflow: hidden;\n      word-break: break-word;\n    }\n  }\n}\n\n.item.artist {\n  display: flex;\n  flex-direction: column;\n  text-align: center;\n  .cover {\n    display: flex;\n  }\n  .title {\n    margin-top: 4px;\n  }\n}\n\n@media (max-width: 834px) {\n  .item .text .title {\n    font-size: 14px;\n  }\n}\n\n.explicit-symbol {\n  opacity: 0.28;\n  color: var(--color-text);\n  float: right;\n  .svg-icon {\n    margin-bottom: -3px;\n  }\n}\n\n.lock-icon {\n  opacity: 0.28;\n  color: var(--color-text);\n  margin-right: 4px;\n  // float: right;\n  .svg-icon {\n    height: 12px;\n    width: 12px;\n  }\n}\n\n.play-count {\n  font-weight: 600;\n  opacity: 0.58;\n  color: var(--color-text);\n  font-size: 12px;\n  .svg-icon {\n    margin-right: 3px;\n    height: 8px;\n    width: 8px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/DailyTracksCard.vue",
    "content": "<template>\n  <div class=\"daily-recommend-card\" @click=\"goToDailyTracks\">\n    <img :src=\"coverUrl\" loading=\"lazy\" />\n    <div class=\"container\">\n      <div class=\"title-box\">\n        <div class=\"title\">\n          <span>每</span>\n          <span>日</span>\n          <span>推</span>\n          <span>荐</span>\n        </div>\n      </div>\n    </div>\n    <button class=\"play-button\" @click.stop=\"playDailyTracks\">\n      <svg-icon icon-class=\"play\" />\n    </button>\n  </div>\n</template>\n\n<script>\nimport locale from '@/locale';\nimport { mapMutations, mapState, mapActions } from 'vuex';\nimport { dailyRecommendTracks } from '@/api/playlist';\nimport { isAccountLoggedIn } from '@/utils/auth';\nimport sample from 'lodash/sample';\n\nconst defaultCovers = [\n  'https://p2.music.126.net/0-Ybpa8FrDfRgKYCTJD8Xg==/109951164796696795.jpg',\n  'https://p2.music.126.net/QxJA2mr4hhb9DZyucIOIQw==/109951165422200291.jpg',\n  'https://p1.music.126.net/AhYP9TET8l-VSGOpWAKZXw==/109951165134386387.jpg',\n];\n\nexport default {\n  name: 'DailyTracksCard',\n  data() {\n    return { useAnimation: false };\n  },\n  computed: {\n    ...mapState(['dailyTracks']),\n    coverUrl() {\n      return `${\n        this.dailyTracks[0]?.al.picUrl || sample(defaultCovers)\n      }?param=1024y1024`;\n    },\n  },\n  created() {\n    if (this.dailyTracks.length === 0) this.loadDailyTracks();\n  },\n  methods: {\n    ...mapActions(['showToast']),\n    ...mapMutations(['updateDailyTracks']),\n    loadDailyTracks() {\n      if (!isAccountLoggedIn()) return;\n      dailyRecommendTracks()\n        .then(result => {\n          this.updateDailyTracks(result.data.dailySongs);\n        })\n        .catch(() => {});\n    },\n    goToDailyTracks() {\n      this.$router.push({ name: 'dailySongs' });\n    },\n    playDailyTracks() {\n      if (!isAccountLoggedIn()) {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      let trackIDs = this.dailyTracks.map(t => t.id);\n      this.$store.state.player.replacePlaylist(\n        trackIDs,\n        '/daily/songs',\n        'url',\n        this.dailyTracks[0].id\n      );\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.daily-recommend-card {\n  border-radius: 1rem;\n  height: 198px;\n  cursor: pointer;\n  position: relative;\n  overflow: hidden;\n  z-index: 1;\n}\n\nimg {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  animation: move 38s infinite;\n  animation-direction: alternate;\n  z-index: -1;\n}\n\n.container {\n  background: linear-gradient(to left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.28));\n  height: 198px;\n  width: 50%;\n  display: flex;\n  align-items: center;\n  border-radius: 0.94rem;\n}\n\n.title-box {\n  height: 148px;\n  width: 148px;\n  color: white;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  margin-left: 25px;\n  user-select: none;\n  .title {\n    height: 100%;\n    width: 100%;\n    font-weight: 600;\n    font-size: 64px;\n    line-height: 48px;\n    opacity: 0.96;\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    justify-items: center;\n    place-items: center;\n  }\n}\n\n.play-button {\n  backdrop-filter: blur(8px);\n  border: 1px solid rgba(255, 255, 255, 0.08);\n  color: white;\n  position: absolute;\n  right: 1.6rem;\n  bottom: 1.4rem;\n  background: rgba(255, 255, 255, 0.14);\n  border-radius: 50%;\n  margin-bottom: 2px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  height: 44px;\n  width: 44px;\n  transition: 0.2s;\n  cursor: default;\n\n  .svg-icon {\n    margin-left: 4px;\n    height: 16px;\n    width: 16px;\n  }\n\n  &:hover {\n    background: rgba(255, 255, 255, 0.44);\n  }\n  &:active {\n    transform: scale(0.94);\n  }\n}\n\n@keyframes move {\n  0% {\n    transform: translateY(0);\n  }\n  100% {\n    transform: translateY(-50%);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/ExplicitSymbol.vue",
    "content": "<template>\n  <svg-icon icon-class=\"explicit\" :style=\"svgStyle\"></svg-icon>\n</template>\n\n<script>\nimport SvgIcon from '@/components/SvgIcon.vue';\n\nexport default {\n  name: 'ExplicitSymbol',\n  components: {\n    SvgIcon,\n  },\n  props: {\n    size: {\n      type: Number,\n      default: 16,\n    },\n  },\n  data() {\n    return {\n      svgStyle: {},\n    };\n  },\n  created() {\n    this.svgStyle = {\n      height: this.size + 'px',\n      width: this.size + 'px',\n      position: 'relative',\n      left: '-1px',\n    };\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped></style>\n"
  },
  {
    "path": "src/components/FMCard.vue",
    "content": "<template>\n  <div class=\"fm\" :style=\"{ background }\" data-theme=\"dark\">\n    <img :src=\"nextTrackCover\" style=\"display: none\" loading=\"lazy\" />\n    <img\n      class=\"cover\"\n      :src=\"track.album && track.album.picUrl | resizeImage(512)\"\n      loading=\"lazy\"\n      @click=\"goToAlbum\"\n    />\n    <div class=\"right-part\">\n      <div class=\"info\">\n        <div class=\"title\">{{ track.name }}</div>\n        <div class=\"artist\"><ArtistsInLine :artists=\"artists\" /></div>\n      </div>\n      <div class=\"controls\">\n        <div class=\"buttons\">\n          <button-icon title=\"不喜欢\" @click.native=\"moveToFMTrash\">\n            <svg-icon id=\"thumbs-down\" icon-class=\"thumbs-down\" />\n          </button-icon>\n          <button-icon\n            :title=\"$t(isPlaying ? 'player.pause' : 'player.play')\"\n            class=\"play\"\n            @click.native=\"play\"\n          >\n            <svg-icon :icon-class=\"isPlaying ? 'pause' : 'play'\" />\n          </button-icon>\n          <button-icon :title=\"$t('player.next')\" @click.native=\"next\">\n            <svg-icon icon-class=\"next\" />\n          </button-icon>\n        </div>\n        <div class=\"card-name\"><svg-icon icon-class=\"fm\" />私人FM</div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport ButtonIcon from '@/components/ButtonIcon.vue';\nimport ArtistsInLine from '@/components/ArtistsInLine.vue';\nimport { mapState } from 'vuex';\nimport * as Vibrant from 'node-vibrant/dist/vibrant.worker.min.js';\nimport Color from 'color';\n\nexport default {\n  name: 'FMCard',\n  components: { ButtonIcon, ArtistsInLine },\n  data() {\n    return {\n      background: '',\n    };\n  },\n  computed: {\n    ...mapState(['player']),\n    track() {\n      return this.player.personalFMTrack;\n    },\n    isPlaying() {\n      return this.player.playing && this.player.isPersonalFM;\n    },\n    artists() {\n      return this.track.artists || this.track.ar || [];\n    },\n    nextTrackCover() {\n      return `${this.player._personalFMNextTrack?.album?.picUrl.replace(\n        'http://',\n        'https://'\n      )}?param=512y512`;\n    },\n  },\n  watch: {\n    track() {\n      this.getColor();\n    },\n  },\n  created() {\n    this.getColor();\n    window.ok = this.getColor;\n  },\n  methods: {\n    play() {\n      this.player.playPersonalFM();\n    },\n    next() {\n      this.player.playNextFMTrack();\n    },\n    goToAlbum() {\n      if (this.track.album.id === 0) return;\n      this.$router.push({ path: '/album/' + this.track.album.id });\n    },\n    moveToFMTrash() {\n      this.player.moveToFMTrash();\n    },\n    getColor() {\n      if (!this.player.personalFMTrack?.album?.picUrl) return;\n      const cover = `${this.player.personalFMTrack.album.picUrl.replace(\n        'http://',\n        'https://'\n      )}?param=512y512`;\n      Vibrant.from(cover, { colorCount: 1 })\n        .getPalette()\n        .then(palette => {\n          const color = Color.rgb(palette.Vibrant._rgb)\n            .darken(0.1)\n            .rgb()\n            .string();\n          const color2 = Color.rgb(palette.Vibrant._rgb)\n            .lighten(0.28)\n            .rotate(-30)\n            .rgb()\n            .string();\n          this.background = `linear-gradient(to top left, ${color}, ${color2})`;\n        });\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.fm {\n  padding: 1rem;\n  background: var(--color-secondary-bg);\n  border-radius: 1rem;\n  display: flex;\n  height: 198px;\n  box-sizing: border-box;\n}\n.cover {\n  height: 100%;\n  clip-path: border-box;\n  border-radius: 0.75rem;\n  margin-right: 1.2rem;\n  cursor: pointer;\n  user-select: none;\n}\n.right-part {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  color: var(--color-text);\n  width: 100%;\n  .title {\n    font-size: 1.6rem;\n    font-weight: 600;\n    margin-bottom: 0.6rem;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    overflow: hidden;\n    word-break: break-all;\n  }\n  .artist {\n    opacity: 0.68;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    overflow: hidden;\n    word-break: break-all;\n  }\n  .controls {\n    display: flex;\n    justify-content: space-between;\n    align-items: baseline;\n    margin-left: -0.4rem;\n    .buttons {\n      display: flex;\n    }\n    .button-icon {\n      margin: 0 8px 0 0;\n    }\n    .svg-icon {\n      width: 24px;\n      height: 24px;\n    }\n    .svg-icon#thumbs-down {\n      width: 22px;\n      height: 22px;\n    }\n    .card-name {\n      font-size: 1rem;\n      opacity: 0.18;\n      display: flex;\n      align-items: center;\n      font-weight: 600;\n      user-select: none;\n      .svg-icon {\n        width: 18px;\n        height: 18px;\n        margin-right: 6px;\n      }\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/LinuxTitlebar.vue",
    "content": "<template>\n  <div class=\"linux-titlebar\">\n    <div class=\"logo\">\n      <img src=\"img/logos/yesplaymusic-white24x24.png\" />\n    </div>\n    <div class=\"title\">{{ title }}</div>\n    <div class=\"controls\">\n      <div\n        class=\"button minimize codicon codicon-chrome-minimize\"\n        @click=\"windowMinimize\"\n      ></div>\n      <div\n        class=\"button max-restore codicon\"\n        :class=\"{\n          'codicon-chrome-restore': isMaximized,\n          'codicon-chrome-maximize': !isMaximized,\n        }\"\n        @click=\"windowMaxRestore\"\n      ></div>\n      <div\n        class=\"button close codicon codicon-chrome-close\"\n        @click=\"windowClose\"\n      ></div>\n    </div>\n  </div>\n</template>\n\n<script>\n// icons by https://github.com/microsoft/vscode-codicons\nimport 'vscode-codicons/dist/codicon.css';\n\nimport { mapState } from 'vuex';\n\nconst electron =\n  process.env.IS_ELECTRON === true ? window.require('electron') : null;\nconst ipcRenderer =\n  process.env.IS_ELECTRON === true ? electron.ipcRenderer : null;\n\nexport default {\n  name: 'LinuxTitlebar',\n  data() {\n    return {\n      isMaximized: false,\n    };\n  },\n  computed: {\n    ...mapState(['title']),\n  },\n  created() {\n    if (process.env.IS_ELECTRON === true) {\n      ipcRenderer.on('isMaximized', (_, value) => {\n        this.isMaximized = value;\n      });\n    }\n  },\n  methods: {\n    windowMinimize() {\n      ipcRenderer.send('minimize');\n    },\n    windowMaxRestore() {\n      ipcRenderer.send('maximizeOrUnmaximize');\n    },\n    windowClose() {\n      ipcRenderer.send('close');\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.linux-titlebar {\n  color: var(--color-text);\n  position: fixed;\n  left: 0;\n  top: 0;\n  right: 0;\n  -webkit-app-region: drag;\n  display: flex;\n  align-items: center;\n  --hover: #e6e6e6;\n  --active: #cccccc;\n\n  .logo {\n    padding: 0 8px;\n  }\n\n  .title {\n    padding: 8px;\n    font-size: 12px;\n    font-family: 'Segoe UI', 'Microsoft YaHei UI', 'Microsoft YaHei', sans-serif;\n    justify-self: center;\n    margin: 0 auto;\n  }\n  .controls {\n    height: 32px;\n    //margin-left: auto;\n    justify-content: flex-end;\n    display: flex;\n    .button {\n      height: 100%;\n      width: 46px;\n      font-size: 16px;\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      -webkit-app-region: no-drag;\n      &:hover {\n        background: var(--hover);\n      }\n      &:active {\n        background: var(--active);\n      }\n      &.close {\n        &:hover {\n          background: #c42c1b;\n          color: rgba(255, 255, 255, 0.8);\n        }\n        &:active {\n          background: #f1707a;\n          color: #000;\n        }\n      }\n    }\n  }\n}\n[data-theme='dark'] .linux-titlebar {\n  --hover: #191919;\n  --active: #333333;\n}\n</style>\n"
  },
  {
    "path": "src/components/Modal.vue",
    "content": "<template>\n  <div v-show=\"show\" class=\"shade\" @click=\"clickOutside\">\n    <div class=\"modal\" :style=\"modalStyles\" @click.stop>\n      <div class=\"header\">\n        <div class=\"title\">{{ title }}</div>\n        <button class=\"close\" @click=\"close\"\n          ><svg-icon icon-class=\"x\"\n        /></button>\n      </div>\n      <div class=\"content\"><slot></slot></div>\n      <div v-if=\"showFooter\" class=\"footer\">\n        <!-- <button>取消</button>\n        <button class=\"primary\">确定</button> -->\n        <slot name=\"footer\"></slot>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'Modal',\n  props: {\n    show: Boolean,\n    close: Function,\n    title: {\n      type: String,\n      default: 'Title',\n    },\n    showFooter: {\n      type: Boolean,\n      default: true,\n    },\n    width: {\n      type: String,\n      default: '50vw',\n    },\n    clickOutsideHide: {\n      type: Boolean,\n      default: false,\n    },\n    minWidth: {\n      type: String,\n      default: 'calc(min(23rem, 100vw))',\n    },\n  },\n  computed: {\n    modalStyles() {\n      return {\n        width: this.width,\n        minWidth: this.minWidth,\n      };\n    },\n  },\n  methods: {\n    clickOutside() {\n      if (this.clickOutsideHide) {\n        this.close();\n      }\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.shade {\n  background: rgba(255, 255, 255, 0.58);\n  position: fixed;\n  top: 0;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  z-index: 1000;\n}\n\n.modal {\n  background: rgba(255, 255, 255, 0.78);\n  box-shadow: 0 12px 16px -8px rgba(0, 0, 0, 0.1);\n  border: 1px solid rgba(0, 0, 0, 0.08);\n  backdrop-filter: blur(12px) opacity(1);\n  padding: 24px 0;\n  border-radius: 12px;\n  width: 50vw;\n  margin: auto 0;\n  font-size: 14px;\n  z-index: 100;\n  display: flex;\n  flex-direction: column;\n  max-height: calc(100vh - 128px - 64px);\n\n  ::-webkit-scrollbar {\n    width: 4px;\n  }\n  ::-webkit-scrollbar-track {\n    background: transparent;\n    border: unset;\n    width: 0;\n  }\n  ::-webkit-scrollbar-thumb {\n    background: var(--color-secondary-bg-for-transparent);\n  }\n}\n\n@supports (-moz-appearance: none) {\n  .modal {\n    background: var(--color-body-bg) !important;\n  }\n}\n\n.content {\n  overflow: auto;\n  overflow-x: hidden;\n  padding: 0 24px;\n}\n\n.header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin: 0 24px 24px 24px;\n  .title {\n    font-weight: 600;\n    font-size: 20px;\n  }\n  button {\n    color: var(--color-text);\n    border-radius: 50%;\n    height: 32px;\n    width: 32px;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    opacity: 0.68;\n    transition: 0.2s;\n    &:hover {\n      opacity: 1;\n      background: var(--color-secondary-bg-for-transparent);\n    }\n  }\n  .svg-icon {\n    height: 18px;\n    width: 18px;\n  }\n}\n\n.footer {\n  padding-top: 16px;\n  margin: 16px 24px 24px 24px;\n  border-top: 1px solid rgba(128, 128, 128, 0.18);\n  display: flex;\n  justify-content: flex-end;\n  margin-bottom: -8px;\n  button {\n    color: var(--color-text);\n    background: var(--color-secondary-bg-for-transparent);\n    border-radius: 8px;\n    padding: 6px 16px;\n    font-size: 14px;\n    margin-left: 12px;\n    transition: 0.2s;\n    &:active {\n      transform: scale(0.94);\n    }\n  }\n  button.primary {\n    color: var(--color-primary-bg);\n    background: var(--color-primary);\n    font-weight: 500;\n  }\n  button.block {\n    width: 100%;\n    margin-left: 0;\n    &:active {\n      transform: scale(0.98);\n    }\n  }\n}\n\n[data-theme='dark'] {\n  .shade {\n    background: rgba(0, 0, 0, 0.38);\n    color: var(--color-text);\n  }\n\n  .modal {\n    background: rgba(36, 36, 36, 0.88);\n    border: 1px solid rgba(255, 255, 255, 0.08);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/ModalAddTrackToPlaylist.vue",
    "content": "<template>\n  <Modal\n    class=\"add-track-to-playlist-modal\"\n    :show=\"show\"\n    :close=\"close\"\n    :show-footer=\"false\"\n    title=\"添加到歌单\"\n    width=\"25vw\"\n  >\n    <template slot=\"default\">\n      <div class=\"new-playlist-button\" @click=\"newPlaylist\"\n        ><svg-icon icon-class=\"plus\" />新建歌单</div\n      >\n      <div\n        v-for=\"playlist in ownPlaylists\"\n        :key=\"playlist.id\"\n        class=\"playlist\"\n        @click=\"addTrackToPlaylist(playlist.id)\"\n      >\n        <img :src=\"playlist.coverImgUrl | resizeImage(224)\" loading=\"lazy\" />\n        <div class=\"info\">\n          <div class=\"title\">{{ playlist.name }}</div>\n          <div class=\"track-count\">{{ playlist.trackCount }} 首</div>\n        </div>\n      </div>\n    </template>\n  </Modal>\n</template>\n\n<script>\nimport { mapActions, mapMutations, mapState } from 'vuex';\nimport Modal from '@/components/Modal.vue';\nimport locale from '@/locale';\nimport { addOrRemoveTrackFromPlaylist } from '@/api/playlist';\n\nexport default {\n  name: 'ModalAddTrackToPlaylist',\n  components: {\n    Modal,\n  },\n  data() {\n    return {\n      playlists: [],\n    };\n  },\n  computed: {\n    ...mapState(['modals', 'data', 'liked']),\n    show: {\n      get() {\n        return this.modals.addTrackToPlaylistModal.show;\n      },\n      set(value) {\n        this.updateModal({\n          modalName: 'addTrackToPlaylistModal',\n          key: 'show',\n          value,\n        });\n        if (value) {\n          this.$store.commit('enableScrolling', false);\n        } else {\n          this.$store.commit('enableScrolling', true);\n        }\n      },\n    },\n    ownPlaylists() {\n      return this.liked.playlists.filter(\n        p =>\n          p.creator.userId === this.data.user.userId &&\n          p.id !== this.data.likedSongPlaylistID\n      );\n    },\n  },\n  methods: {\n    ...mapMutations(['updateModal']),\n    ...mapActions(['showToast']),\n    close() {\n      this.show = false;\n    },\n    addTrackToPlaylist(playlistID) {\n      addOrRemoveTrackFromPlaylist({\n        op: 'add',\n        pid: playlistID,\n        tracks: this.modals.addTrackToPlaylistModal.selectedTrackID,\n      }).then(data => {\n        if (data.body.code === 200) {\n          this.show = false;\n          this.showToast(locale.t('toast.savedToPlaylist'));\n        } else {\n          this.showToast(data.body.message);\n        }\n      });\n    },\n    newPlaylist() {\n      this.updateModal({\n        modalName: 'newPlaylistModal',\n        key: 'afterCreateAddTrackID',\n        value: this.modals.addTrackToPlaylistModal.selectedTrackID,\n      });\n      this.close();\n      this.updateModal({\n        modalName: 'newPlaylistModal',\n        key: 'show',\n        value: true,\n      });\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.new-playlist-button {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  font-size: 16px;\n  font-weight: 500;\n  color: var(--color-text);\n  background: var(--color-secondary-bg-for-transparent);\n  border-radius: 8px;\n  height: 48px;\n  margin-bottom: 16px;\n  margin-right: 6px;\n  margin-left: 6px;\n  cursor: pointer;\n  transition: 0.2s;\n  .svg-icon {\n    width: 16px;\n    height: 16px;\n    margin-right: 8px;\n  }\n  &:hover {\n    color: var(--color-primary);\n    background: var(--color-primary-bg-for-transparent);\n  }\n}\n.playlist {\n  display: flex;\n  padding: 6px;\n  border-radius: 8px;\n  cursor: pointer;\n  &:hover {\n    background: var(--color-secondary-bg-for-transparent);\n  }\n  img {\n    border-radius: 8px;\n    height: 42px;\n    width: 42px;\n    margin-right: 12px;\n    border: 1px solid rgba(0, 0, 0, 0.04);\n  }\n  .info {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n  }\n  .title {\n    font-size: 16px;\n    font-weight: 500;\n    color: var(--color-text);\n    padding-right: 16px;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 1;\n    overflow: hidden;\n    word-break: break-all;\n  }\n  .track-count {\n    margin-top: 2px;\n    font-size: 13px;\n    opacity: 0.68;\n    color: var(--color-text);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/ModalNewPlaylist.vue",
    "content": "<template>\n  <Modal\n    class=\"add-playlist-modal\"\n    :show=\"show\"\n    :close=\"close\"\n    title=\"新建歌单\"\n    width=\"25vw\"\n  >\n    <template slot=\"default\">\n      <input\n        v-model=\"title\"\n        type=\"text\"\n        placeholder=\"歌单标题\"\n        maxlength=\"40\"\n      />\n      <div class=\"checkbox\">\n        <input\n          id=\"checkbox-private\"\n          v-model=\"privatePlaylist\"\n          type=\"checkbox\"\n        />\n        <label for=\"checkbox-private\">设置为隐私歌单</label>\n      </div>\n    </template>\n    <template slot=\"footer\">\n      <button class=\"primary block\" @click=\"createPlaylist\">创建</button>\n    </template>\n  </Modal>\n</template>\n\n<script>\nimport Modal from '@/components/Modal.vue';\nimport locale from '@/locale';\nimport { mapMutations, mapState, mapActions } from 'vuex';\nimport { createPlaylist, addOrRemoveTrackFromPlaylist } from '@/api/playlist';\n\nexport default {\n  name: 'ModalNewPlaylist',\n  components: {\n    Modal,\n  },\n  data() {\n    return {\n      title: '',\n      privatePlaylist: false,\n    };\n  },\n  computed: {\n    ...mapState(['modals']),\n    show: {\n      get() {\n        return this.modals.newPlaylistModal.show;\n      },\n      set(value) {\n        this.updateModal({\n          modalName: 'newPlaylistModal',\n          key: 'show',\n          value,\n        });\n        if (value) {\n          this.$store.commit('enableScrolling', false);\n        } else {\n          this.$store.commit('enableScrolling', true);\n        }\n      },\n    },\n  },\n  methods: {\n    ...mapMutations(['updateModal', 'updateData']),\n    ...mapActions(['showToast', 'fetchLikedPlaylist']),\n    close() {\n      this.show = false;\n      this.title = '';\n      this.privatePlaylist = false;\n      this.resetAfterCreateAddTrackID();\n    },\n    createPlaylist() {\n      let params = { name: this.title };\n      if (this.private) params.type = 10;\n      createPlaylist(params).then(data => {\n        if (data.code === 200) {\n          if (this.modals.newPlaylistModal.afterCreateAddTrackID !== 0) {\n            addOrRemoveTrackFromPlaylist({\n              op: 'add',\n              pid: data.id,\n              tracks: this.modals.newPlaylistModal.afterCreateAddTrackID,\n            }).then(data => {\n              if (data.body.code === 200) {\n                this.showToast(locale.t('toast.savedToPlaylist'));\n              } else {\n                this.showToast(data.body.message);\n              }\n              this.resetAfterCreateAddTrackID();\n            });\n          }\n          this.close();\n          this.showToast('成功创建歌单');\n          this.updateData({ key: 'libraryPlaylistFilter', value: 'mine' });\n          this.fetchLikedPlaylist();\n        }\n      });\n    },\n    resetAfterCreateAddTrackID() {\n      this.updateModal({\n        modalName: 'newPlaylistModal',\n        key: 'AfterCreateAddTrackID',\n        value: 0,\n      });\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.add-playlist-modal {\n  .content {\n    display: flex;\n    flex-direction: column;\n    input {\n      margin-bottom: 12px;\n    }\n    input[type='text'] {\n      width: calc(100% - 24px);\n      flex: 1;\n      background: var(--color-secondary-bg-for-transparent);\n      font-size: 16px;\n      border: none;\n      font-weight: 600;\n      padding: 8px 12px;\n      border-radius: 8px;\n      margin-top: -1px;\n      color: var(--color-text);\n      &:focus {\n        background: var(--color-primary-bg-for-transparent);\n        opacity: 1;\n      }\n      [data-theme='light'] &:focus {\n        color: var(--color-primary);\n      }\n    }\n    .checkbox {\n      input[type='checkbox' i] {\n        margin: 3px 3px 3px 4px;\n      }\n      display: flex;\n      align-items: center;\n      label {\n        font-size: 12px;\n      }\n      user-select: none;\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/MvRow.vue",
    "content": "<template>\n  <div class=\"mv-row\" :class=\"{ 'without-padding': withoutPadding }\">\n    <div v-for=\"mv in mvs\" :key=\"getID(mv)\" class=\"mv\">\n      <div\n        class=\"cover\"\n        @mouseover=\"hoverVideoID = getID(mv)\"\n        @mouseleave=\"hoverVideoID = 0\"\n        @click=\"goToMv(getID(mv))\"\n      >\n        <img :src=\"getUrl(mv)\" loading=\"lazy\" />\n        <transition name=\"fade\">\n          <div\n            v-show=\"hoverVideoID === getID(mv)\"\n            class=\"shadow\"\n            :style=\"{ background: 'url(' + getUrl(mv) + ')' }\"\n          ></div>\n        </transition>\n      </div>\n      <div class=\"info\">\n        <div class=\"title\">\n          <router-link :to=\"'/mv/' + getID(mv)\">{{ getTitle(mv) }}</router-link>\n        </div>\n        <div class=\"artist\" v-html=\"getSubtitle(mv)\"></div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'CoverVideo',\n  props: {\n    mvs: Array,\n    subtitle: {\n      type: String,\n      default: 'artist',\n    },\n    withoutPadding: { type: Boolean, default: false },\n  },\n  data() {\n    return {\n      hoverVideoID: 0,\n    };\n  },\n  methods: {\n    goToMv(id) {\n      let query = {};\n      if (this.$parent.player !== undefined) {\n        query = { autoplay: this.$parent.player.playing };\n      }\n      this.$router.push({ path: '/mv/' + id, query });\n    },\n    getUrl(mv) {\n      let url = mv.imgurl16v9 ?? mv.cover ?? mv.coverUrl;\n      return url.replace(/^http:/, 'https:') + '?param=464y260';\n    },\n    getID(mv) {\n      if (mv.id !== undefined) return mv.id;\n      if (mv.vid !== undefined) return mv.vid;\n    },\n    getTitle(mv) {\n      if (mv.name !== undefined) return mv.name;\n      if (mv.title !== undefined) return mv.title;\n    },\n    getSubtitle(mv) {\n      if (this.subtitle === 'artist') {\n        let artistName = 'null';\n        let artistID = 0;\n        if (mv.artistName !== undefined) {\n          artistName = mv.artistName;\n          artistID = mv.artistId;\n        } else if (mv.creator !== undefined) {\n          artistName = mv.creator[0].userName;\n          artistID = mv.creator[0].userId;\n        }\n        return `<a href=\"/artist/${artistID}\">${artistName}</a>`;\n      } else if (this.subtitle === 'publishTime') {\n        return mv.publishTime;\n      }\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.mv-row {\n  --col-num: 5;\n  display: grid;\n  grid-template-columns: repeat(var(--col-num), 1fr);\n  gap: 36px 24px;\n  padding: var(--main-content-padding);\n}\n\n.mv-row.without-padding {\n  padding: 0;\n}\n\n@media (max-width: 900px) {\n  .mv-row {\n    --col-num: 4;\n  }\n}\n\n@media (max-width: 800px) {\n  .mv-row {\n    --col-num: 3;\n  }\n}\n\n@media (max-width: 700px) {\n  .mv-row {\n    --col-num: 2;\n  }\n}\n\n@media (max-width: 550px) {\n  .mv-row {\n    --col-num: 1;\n  }\n}\n\n.mv {\n  color: var(--color-text);\n\n  .title {\n    font-size: 16px;\n    font-weight: 600;\n    opacity: 0.88;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    overflow: hidden;\n    word-break: break-all;\n  }\n  .artist {\n    font-size: 12px;\n    opacity: 0.68;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    overflow: hidden;\n  }\n}\n\n.cover {\n  position: relative;\n  transition: transform 0.3s;\n  &:hover {\n    cursor: pointer;\n  }\n}\nimg {\n  border-radius: 0.75em;\n  width: 100%;\n  user-select: none;\n}\n\n.shadow {\n  position: absolute;\n  top: 6px;\n  height: 100%;\n  width: 100%;\n  filter: blur(16px) opacity(0.4);\n  transform: scale(0.9, 0.9);\n  z-index: -1;\n  background-size: cover;\n  border-radius: 0.75em;\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s;\n}\n.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/Navbar.vue",
    "content": "<template>\n  <div>\n    <nav :class=\"{ 'has-custom-titlebar': hasCustomTitlebar }\">\n      <Win32Titlebar v-if=\"enableWin32Titlebar\" />\n      <LinuxTitlebar v-if=\"enableLinuxTitlebar\" />\n      <div class=\"navigation-buttons\">\n        <button-icon @click.native=\"go('back')\"\n          ><svg-icon icon-class=\"arrow-left\"\n        /></button-icon>\n        <button-icon @click.native=\"go('forward')\"\n          ><svg-icon icon-class=\"arrow-right\"\n        /></button-icon>\n      </div>\n      <div class=\"navigation-links\">\n        <router-link to=\"/\" :class=\"{ active: $route.name === 'home' }\">{{\n          $t('nav.home')\n        }}</router-link>\n        <router-link\n          to=\"/explore\"\n          :class=\"{ active: $route.name === 'explore' }\"\n          >{{ $t('nav.explore') }}</router-link\n        >\n        <router-link\n          to=\"/library\"\n          :class=\"{ active: $route.name === 'library' }\"\n          >{{ $t('nav.library') }}</router-link\n        >\n      </div>\n      <div class=\"right-part\">\n        <div class=\"search-box\">\n          <div class=\"container\" :class=\"{ active: inputFocus }\">\n            <svg-icon icon-class=\"search\" />\n            <div class=\"input\">\n              <input\n                ref=\"searchInput\"\n                v-model=\"keywords\"\n                type=\"search\"\n                :placeholder=\"inputFocus ? '' : $t('nav.search')\"\n                @keydown.enter=\"doSearch\"\n                @focus=\"inputFocus = true\"\n                @blur=\"inputFocus = false\"\n              />\n            </div>\n          </div>\n        </div>\n        <img\n          class=\"avatar\"\n          :src=\"avatarUrl\"\n          @click=\"showUserProfileMenu\"\n          loading=\"lazy\"\n        />\n      </div>\n    </nav>\n\n    <ContextMenu ref=\"userProfileMenu\">\n      <div class=\"item\" @click=\"toSettings\">\n        <svg-icon icon-class=\"settings\" />\n        {{ $t('library.userProfileMenu.settings') }}\n      </div>\n      <div v-if=\"!isLooseLoggedIn\" class=\"item\" @click=\"toLogin\">\n        <svg-icon icon-class=\"login\" />\n        {{ $t('login.login') }}\n      </div>\n      <div v-if=\"isLooseLoggedIn\" class=\"item\" @click=\"logout\">\n        <svg-icon icon-class=\"logout\" />\n        {{ $t('library.userProfileMenu.logout') }}\n      </div>\n      <hr />\n      <div class=\"item\" @click=\"toGitHub\">\n        <svg-icon icon-class=\"github\" />\n        {{ $t('nav.github') }}\n      </div>\n    </ContextMenu>\n  </div>\n</template>\n\n<script>\nimport { mapState } from 'vuex';\nimport { isLooseLoggedIn, doLogout } from '@/utils/auth';\n\n// import icons for win32 title bar\n// icons by https://github.com/microsoft/vscode-codicons\nimport 'vscode-codicons/dist/codicon.css';\n\nimport Win32Titlebar from '@/components/Win32Titlebar.vue';\nimport LinuxTitlebar from '@/components/LinuxTitlebar.vue';\nimport ContextMenu from '@/components/ContextMenu.vue';\nimport ButtonIcon from '@/components/ButtonIcon.vue';\n\nexport default {\n  name: 'Navbar',\n  components: {\n    Win32Titlebar,\n    LinuxTitlebar,\n    ButtonIcon,\n    ContextMenu,\n  },\n  data() {\n    return {\n      inputFocus: false,\n      langs: ['zh-CN', 'zh-TW', 'en', 'tr'],\n      keywords: '',\n      enableWin32Titlebar: false,\n      enableLinuxTitlebar: false,\n    };\n  },\n  computed: {\n    ...mapState(['settings', 'data']),\n    isLooseLoggedIn() {\n      return isLooseLoggedIn();\n    },\n    avatarUrl() {\n      return this.data?.user?.avatarUrl && this.isLooseLoggedIn\n        ? `${this.data?.user?.avatarUrl}?param=512y512`\n        : 'http://s4.music.126.net/style/web2/img/default/default_avatar.jpg?param=60y60';\n    },\n    hasCustomTitlebar() {\n      return this.enableWin32Titlebar || this.enableLinuxTitlebar;\n    },\n  },\n  created() {\n    if (process.platform === 'win32') {\n      this.enableWin32Titlebar = true;\n    } else if (\n      process.platform === 'linux' &&\n      this.settings.linuxEnableCustomTitlebar\n    ) {\n      this.enableLinuxTitlebar = true;\n    }\n  },\n  methods: {\n    go(where) {\n      if (where === 'back') this.$router.go(-1);\n      else this.$router.go(1);\n    },\n    doSearch() {\n      if (!this.keywords) return;\n      if (\n        this.$route.name === 'search' &&\n        this.$route.params.keywords === this.keywords\n      ) {\n        return;\n      }\n      this.$router.push({\n        name: 'search',\n        params: { keywords: this.keywords },\n      });\n    },\n    showUserProfileMenu(e) {\n      this.$refs.userProfileMenu.openMenu(e);\n    },\n    logout() {\n      if (!confirm('确定要退出登录吗？')) return;\n      doLogout();\n      this.$router.push({ name: 'home' });\n    },\n    toSettings() {\n      this.$router.push({ name: 'settings' });\n    },\n    toGitHub() {\n      window.open('https://github.com/qier222/YesPlayMusic');\n    },\n    toLogin() {\n      if (process.env.IS_ELECTRON === true) {\n        this.$router.push({ name: 'loginAccount' });\n      } else {\n        this.$router.push({ name: 'login' });\n      }\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\nnav {\n  position: fixed;\n  top: 0;\n  right: 0;\n  left: 0;\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  height: 64px;\n  padding: {\n    right: 10vw;\n    left: 10vw;\n  }\n  backdrop-filter: saturate(180%) blur(20px);\n\n  background-color: var(--color-navbar-bg);\n  z-index: 100;\n  -webkit-app-region: drag;\n}\n\n@media (max-width: 1336px) {\n  nav {\n    padding: 0 max(5vw, 90px);\n  }\n}\n\n@supports (-moz-appearance: none) {\n  nav {\n    background-color: var(--color-body-bg);\n  }\n}\n\nnav.has-custom-titlebar {\n  padding-top: 20px;\n  -webkit-app-region: no-drag;\n}\n\n.navigation-buttons {\n  flex: 1;\n  display: flex;\n  align-items: center;\n  .svg-icon {\n    height: 24px;\n    width: 24px;\n  }\n  button {\n    -webkit-app-region: no-drag;\n  }\n}\n@media (max-width: 970px) {\n  .navigation-buttons {\n    flex: unset;\n  }\n}\n\n.navigation-links {\n  flex: 1;\n  display: flex;\n  justify-content: center;\n  text-transform: uppercase;\n  user-select: none;\n  a {\n    -webkit-app-region: no-drag;\n    font-size: 18px;\n    font-weight: 700;\n    text-decoration: none;\n    border-radius: 6px;\n    padding: 6px 10px;\n    color: var(--color-text);\n    transition: 0.2s;\n    -webkit-user-drag: none;\n    margin: {\n      right: 12px;\n      left: 12px;\n    }\n    &:hover {\n      background: var(--color-secondary-bg-for-transparent);\n    }\n    &:active {\n      transform: scale(0.92);\n      transition: 0.2s;\n    }\n  }\n  a.active {\n    color: var(--color-primary);\n  }\n}\n\n.search {\n  .svg-icon {\n    height: 18px;\n    width: 18px;\n  }\n}\n\n.search-box {\n  display: flex;\n  justify-content: flex-end;\n  -webkit-app-region: no-drag;\n\n  .container {\n    display: flex;\n    align-items: center;\n    height: 32px;\n    background: var(--color-secondary-bg-for-transparent);\n    border-radius: 8px;\n    width: 200px;\n  }\n\n  .svg-icon {\n    height: 15px;\n    width: 15px;\n    color: var(--color-text);\n    opacity: 0.28;\n    margin: {\n      left: 8px;\n      right: 4px;\n    }\n  }\n\n  input {\n    font-size: 16px;\n    border: none;\n    background: transparent;\n    width: 96%;\n    font-weight: 600;\n    margin-top: -1px;\n    color: var(--color-text);\n  }\n\n  .active {\n    background: var(--color-primary-bg-for-transparent);\n    input,\n    .svg-icon {\n      opacity: 1;\n      color: var(--color-primary);\n    }\n  }\n}\n\n[data-theme='dark'] {\n  .search-box {\n    .active {\n      input,\n      .svg-icon {\n        color: var(--color-text);\n      }\n    }\n  }\n}\n\n.right-part {\n  flex: 1;\n  display: flex;\n  align-items: center;\n  justify-content: flex-end;\n  .avatar {\n    user-select: none;\n    height: 30px;\n    margin-left: 12px;\n    vertical-align: -7px;\n    border-radius: 50%;\n    cursor: pointer;\n    -webkit-app-region: no-drag;\n    -webkit-user-drag: none;\n    &:hover {\n      filter: brightness(80%);\n    }\n  }\n  .search-button {\n    display: none;\n    -webkit-app-region: no-drag;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/Player.vue",
    "content": "<template>\n  <div class=\"player\" @click=\"handleClick\" @mousedown=\"handleMouseDown\">\n    <div\n      class=\"progress-bar\"\n      :class=\"{\n        nyancat: settings.nyancatStyle,\n        'nyancat-stop': settings.nyancatStyle && !player.playing,\n      }\"\n      @click.stop\n    >\n      <vue-slider\n        v-model=\"player.progress\"\n        :min=\"0\"\n        :max=\"player.currentTrackDuration\"\n        :interval=\"1\"\n        :drag-on-click=\"true\"\n        :duration=\"0\"\n        :dot-size=\"12\"\n        :height=\"2\"\n        :tooltip-formatter=\"formatTrackTime\"\n        :lazy=\"true\"\n        :silent=\"true\"\n      ></vue-slider>\n    </div>\n    <div class=\"controls\">\n      <div class=\"playing\">\n        <div class=\"container\" @click.stop>\n          <img\n            :src=\"currentTrack.al && currentTrack.al.picUrl | resizeImage(224)\"\n            loading=\"lazy\"\n            @click=\"goToAlbum\"\n          />\n          <div class=\"track-info\" :title=\"audioSource\">\n            <div\n              :class=\"['name', { 'has-list': hasList() }]\"\n              @click=\"hasList() && goToList()\"\n            >\n              {{ currentTrack.name }}\n            </div>\n            <div class=\"artist\">\n              <span\n                v-for=\"(ar, index) in currentTrack.ar\"\n                :key=\"ar.id\"\n                @click=\"ar.id && goToArtist(ar.id)\"\n              >\n                <span :class=\"{ ar: ar.id }\"> {{ ar.name }} </span\n                ><span v-if=\"index !== currentTrack.ar.length - 1\">, </span>\n              </span>\n            </div>\n          </div>\n          <div class=\"like-button\">\n            <button-icon\n              :title=\"\n                player.isCurrentTrackLiked\n                  ? $t('player.unlike')\n                  : $t('player.like')\n              \"\n              @click.native=\"likeATrack(player.currentTrack.id)\"\n            >\n              <svg-icon\n                v-show=\"!player.isCurrentTrackLiked\"\n                icon-class=\"heart\"\n              ></svg-icon>\n              <svg-icon\n                v-show=\"player.isCurrentTrackLiked\"\n                icon-class=\"heart-solid\"\n              ></svg-icon>\n            </button-icon>\n          </div>\n        </div>\n        <div class=\"blank\"></div>\n      </div>\n      <div class=\"middle-control-buttons\">\n        <div class=\"blank\"></div>\n        <div class=\"container\" @click.stop>\n          <button-icon\n            v-show=\"!player.isPersonalFM\"\n            :title=\"$t('player.previous')\"\n            @click.native=\"playPrevTrack\"\n            ><svg-icon icon-class=\"previous\"\n          /></button-icon>\n          <button-icon\n            v-show=\"player.isPersonalFM\"\n            title=\"不喜欢\"\n            @click.native=\"moveToFMTrash\"\n            ><svg-icon icon-class=\"thumbs-down\"\n          /></button-icon>\n          <button-icon\n            class=\"play\"\n            :title=\"$t(player.playing ? 'player.pause' : 'player.play')\"\n            @click.native=\"playOrPause\"\n          >\n            <svg-icon :icon-class=\"player.playing ? 'pause' : 'play'\"\n          /></button-icon>\n          <button-icon :title=\"$t('player.next')\" @click.native=\"playNextTrack\"\n            ><svg-icon icon-class=\"next\"\n          /></button-icon>\n        </div>\n        <div class=\"blank\"></div>\n      </div>\n      <div class=\"right-control-buttons\">\n        <div class=\"blank\"></div>\n        <div class=\"container\" @click.stop>\n          <button-icon\n            :title=\"$t('player.nextUp')\"\n            :class=\"{\n              active: $route.name === 'next',\n              disabled: player.isPersonalFM,\n            }\"\n            @click.native=\"goToNextTracksPage\"\n            ><svg-icon icon-class=\"list\"\n          /></button-icon>\n          <button-icon\n            :class=\"{\n              active: player.repeatMode !== 'off',\n              disabled: player.isPersonalFM,\n            }\"\n            :title=\"\n              player.repeatMode === 'one'\n                ? $t('player.repeatTrack')\n                : $t('player.repeat')\n            \"\n            @click.native=\"switchRepeatMode\"\n          >\n            <svg-icon\n              v-show=\"player.repeatMode !== 'one'\"\n              icon-class=\"repeat\"\n            />\n            <svg-icon\n              v-show=\"player.repeatMode === 'one'\"\n              icon-class=\"repeat-1\"\n            />\n          </button-icon>\n          <button-icon\n            :class=\"{ active: player.shuffle, disabled: player.isPersonalFM }\"\n            :title=\"$t('player.shuffle')\"\n            @click.native=\"switchShuffle\"\n            ><svg-icon icon-class=\"shuffle\"\n          /></button-icon>\n          <button-icon\n            v-if=\"settings.enableReversedMode\"\n            :class=\"{ active: player.reversed, disabled: player.isPersonalFM }\"\n            :title=\"$t('player.reversed')\"\n            @click.native=\"switchReversed\"\n            ><svg-icon icon-class=\"sort-up\"\n          /></button-icon>\n          <div class=\"volume-control\">\n            <button-icon :title=\"$t('player.mute')\" @click.native=\"mute\">\n              <svg-icon v-show=\"volume > 0.5\" icon-class=\"volume\" />\n              <svg-icon v-show=\"volume === 0\" icon-class=\"volume-mute\" />\n              <svg-icon\n                v-show=\"volume <= 0.5 && volume !== 0\"\n                icon-class=\"volume-half\"\n              />\n            </button-icon>\n            <div class=\"volume-bar\">\n              <vue-slider\n                v-model=\"volume\"\n                :min=\"0\"\n                :max=\"1\"\n                :interval=\"0.01\"\n                :drag-on-click=\"true\"\n                :duration=\"0\"\n                tooltip=\"none\"\n                :dot-size=\"12\"\n              ></vue-slider>\n            </div>\n          </div>\n\n          <button-icon\n            class=\"lyrics-button\"\n            title=\"歌词\"\n            style=\"margin-left: 12px\"\n            @click.native=\"toggleLyrics\"\n            ><svg-icon icon-class=\"arrow-up\"\n          /></button-icon>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapState, mapMutations, mapActions } from 'vuex';\nimport '@/assets/css/slider.css';\n\nimport ButtonIcon from '@/components/ButtonIcon.vue';\nimport VueSlider from 'vue-slider-component';\nimport { goToListSource, hasListSource } from '@/utils/playList';\nimport { formatTrackTime } from '@/utils/common';\n\nexport default {\n  name: 'Player',\n  components: {\n    ButtonIcon,\n    VueSlider,\n  },\n  data() {\n    return {\n      mouseDownTarget: null,\n    };\n  },\n  computed: {\n    ...mapState(['player', 'settings', 'data']),\n    currentTrack() {\n      return this.player.currentTrack;\n    },\n    volume: {\n      get() {\n        return this.player.volume;\n      },\n      set(value) {\n        this.player.volume = value;\n      },\n    },\n    playing() {\n      return this.player.playing;\n    },\n    audioSource() {\n      return this.player._howler?._src.includes('kuwo.cn')\n        ? '音源来自酷我音乐'\n        : '';\n    },\n  },\n  mounted() {\n    this.setupMediaControls();\n    window.addEventListener('keydown', this.handleKeydown);\n  },\n  beforeDestroy() {\n    window.removeEventListener('keydown', this.handleKeydown);\n  },\n  methods: {\n    ...mapMutations(['toggleLyrics']),\n    ...mapActions(['showToast', 'likeATrack']),\n    handleClick(event) {\n      if (event.target == this.mouseDownTarget) {\n        this.toggleLyrics();\n      }\n    },\n    handleMouseDown(event) {\n      this.mouseDownTarget = event.target;\n    },\n    playPrevTrack() {\n      this.player.playPrevTrack();\n    },\n    playOrPause() {\n      this.player.playOrPause();\n    },\n    playNextTrack() {\n      if (this.player.isPersonalFM) {\n        this.player.playNextFMTrack();\n      } else {\n        this.player.playNextTrack();\n      }\n    },\n    goToNextTracksPage() {\n      if (this.player.isPersonalFM) return;\n      this.$route.name === 'next'\n        ? this.$router.go(-1)\n        : this.$router.push({ name: 'next' });\n    },\n    formatTrackTime(value) {\n      return formatTrackTime(value);\n    },\n    hasList() {\n      return hasListSource();\n    },\n    goToList() {\n      goToListSource();\n    },\n    goToAlbum() {\n      if (this.player.currentTrack.al.id === 0) return;\n      this.$router.push({ path: '/album/' + this.player.currentTrack.al.id });\n    },\n    goToArtist(id) {\n      this.$router.push({ path: '/artist/' + id });\n    },\n    moveToFMTrash() {\n      this.player.moveToFMTrash();\n    },\n    switchRepeatMode() {\n      this.player.switchRepeatMode();\n    },\n    switchShuffle() {\n      this.player.switchShuffle();\n    },\n    switchReversed() {\n      this.player.switchReversed();\n    },\n    mute() {\n      this.player.mute();\n    },\n\n    setupMediaControls() {\n      if ('mediaSession' in navigator) {\n        navigator.mediaSession.setActionHandler('play', () => {\n          this.playOrPause();\n        });\n        navigator.mediaSession.setActionHandler('pause', () => {\n          this.playOrPause();\n        });\n        navigator.mediaSession.setActionHandler('previoustrack', () => {\n          this.playPrevTrack();\n        });\n        navigator.mediaSession.setActionHandler('nexttrack', () => {\n          this.playNextTrack();\n        });\n      }\n    },\n\n    handleKeydown(event) {\n      switch (event.code) {\n        case 'MediaPlayPause':\n          this.playOrPause();\n          break;\n        case 'MediaTrackPrevious':\n          this.playPrevTrack();\n          break;\n        case 'MediaTrackNext':\n          this.playNextTrack();\n          break;\n        default:\n          break;\n      }\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.player {\n  position: fixed;\n  bottom: 0;\n  right: 0;\n  left: 0;\n  display: flex;\n  flex-direction: column;\n  justify-content: space-around;\n  height: 64px;\n  backdrop-filter: saturate(180%) blur(30px);\n  // background-color: rgba(255, 255, 255, 0.86);\n  background-color: var(--color-navbar-bg);\n  z-index: 100;\n}\n\n@supports (-moz-appearance: none) {\n  .player {\n    background-color: var(--color-body-bg);\n  }\n}\n\n.progress-bar {\n  margin-top: -6px;\n  margin-bottom: -6px;\n  width: 100%;\n}\n\n.controls {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  height: 100%;\n  padding: {\n    right: 10vw;\n    left: 10vw;\n  }\n}\n\n@media (max-width: 1336px) {\n  .controls {\n    padding: 0 5vw;\n  }\n}\n\n.blank {\n  flex-grow: 1;\n}\n\n.playing {\n  display: flex;\n}\n\n.playing .container {\n  display: flex;\n  align-items: center;\n  img {\n    height: 46px;\n    border-radius: 5px;\n    box-shadow: 0 6px 8px -2px rgba(0, 0, 0, 0.16);\n    cursor: pointer;\n    user-select: none;\n  }\n  .track-info {\n    height: 46px;\n    margin-left: 12px;\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    .name {\n      font-weight: 600;\n      font-size: 16px;\n      opacity: 0.88;\n      color: var(--color-text);\n      margin-bottom: 4px;\n      display: -webkit-box;\n      -webkit-box-orient: vertical;\n      -webkit-line-clamp: 1;\n      overflow: hidden;\n      word-break: break-all;\n    }\n    .has-list {\n      cursor: pointer;\n      &:hover {\n        text-decoration: underline;\n      }\n    }\n    .artist {\n      font-size: 12px;\n      opacity: 0.58;\n      color: var(--color-text);\n      display: -webkit-box;\n      -webkit-box-orient: vertical;\n      -webkit-line-clamp: 1;\n      overflow: hidden;\n      word-break: break-all;\n      span.ar {\n        cursor: pointer;\n        &:hover {\n          text-decoration: underline;\n        }\n      }\n    }\n  }\n}\n\n.middle-control-buttons {\n  display: flex;\n}\n\n.middle-control-buttons .container {\n  flex: 1;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 0 8px;\n  .button-icon {\n    margin: 0 8px;\n  }\n  .play {\n    height: 42px;\n    width: 42px;\n    .svg-icon {\n      width: 24px;\n      height: 24px;\n    }\n  }\n}\n\n.right-control-buttons {\n  display: flex;\n}\n\n.right-control-buttons .container {\n  display: flex;\n  justify-content: flex-end;\n  align-items: center;\n  .expand {\n    margin-left: 24px;\n    .svg-icon {\n      height: 24px;\n      width: 24px;\n    }\n  }\n  .active .svg-icon {\n    color: var(--color-primary);\n  }\n  .volume-control {\n    margin-left: 4px;\n    display: flex;\n    align-items: center;\n    .volume-bar {\n      width: 84px;\n    }\n  }\n}\n\n.like-button {\n  margin-left: 16px;\n}\n\n.button-icon.disabled {\n  cursor: default;\n  opacity: 0.38;\n  &:hover {\n    background: none;\n  }\n  &:active {\n    transform: unset;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/Scrollbar.vue",
    "content": "<template>\n  <div>\n    <transition name=\"fade\">\n      <div\n        v-show=\"show\"\n        id=\"scrollbar\"\n        :class=\"{ 'on-drag': isOnDrag }\"\n        @click=\"handleClick\"\n      >\n        <div\n          id=\"thumbContainer\"\n          :class=\"{ active }\"\n          :style=\"thumbStyle\"\n          @mouseenter=\"handleMouseenter\"\n          @mouseleave=\"handleMouseleave\"\n          @mousedown=\"handleDragStart\"\n          @click.stop\n        >\n          <div></div>\n        </div>\n      </div>\n    </transition>\n  </div>\n</template>\n\n<script>\nexport default {\n  name: 'Scrollbar',\n  data() {\n    return {\n      top: 0,\n      thumbHeight: 0,\n      active: false,\n      show: false,\n      hideTimer: null,\n      isOnDrag: false,\n      onDragClientY: 0,\n      positions: {\n        home: { scrollTop: 0, params: {} },\n      },\n    };\n  },\n  computed: {\n    thumbStyle() {\n      return {\n        transform: `translateY(${this.top}px)`,\n        height: `${this.thumbHeight}px`,\n      };\n    },\n    main() {\n      return this.$parent.$refs.main;\n    },\n  },\n\n  created() {\n    this.$router.beforeEach((to, from, next) => {\n      this.show = false;\n      next();\n    });\n  },\n\n  methods: {\n    handleScroll() {\n      const clintHeight = this.main.clientHeight - 128;\n      const scrollHeight = this.main.scrollHeight - 128;\n      const scrollTop = this.main.scrollTop;\n      let top = ~~((scrollTop / scrollHeight) * clintHeight);\n      let thumbHeight = ~~((clintHeight / scrollHeight) * clintHeight);\n\n      if (thumbHeight < 24) thumbHeight = 24;\n      if (top > clintHeight - thumbHeight) {\n        top = clintHeight - thumbHeight;\n      }\n      this.top = top;\n      this.thumbHeight = thumbHeight;\n\n      if (!this.show && clintHeight !== thumbHeight) this.show = true;\n      this.setScrollbarHideTimeout();\n\n      const route = this.$route;\n      if (route.meta.savePosition) {\n        this.positions[route.name] = { scrollTop, params: route.params };\n      }\n    },\n    handleMouseenter() {\n      this.active = true;\n    },\n    handleMouseleave() {\n      this.active = false;\n      this.setScrollbarHideTimeout();\n    },\n    handleDragStart(e) {\n      this.onDragClientY = e.clientY;\n      this.isOnDrag = true;\n      this.$parent.userSelectNone = true;\n      document.addEventListener('mousemove', this.handleDragMove);\n      document.addEventListener('mouseup', this.handleDragEnd);\n    },\n    handleDragMove(e) {\n      if (!this.isOnDrag) return;\n      const clintHeight = this.main.clientHeight - 128;\n      const scrollHeight = this.main.scrollHeight - 128;\n      const clientY = e.clientY;\n      const scrollTop = this.main.scrollTop;\n      const offset = ~~(\n        ((clientY - this.onDragClientY) / clintHeight) *\n        scrollHeight\n      );\n      this.top = ~~((scrollTop / scrollHeight) * clintHeight);\n      this.main.scrollBy(0, offset);\n      this.onDragClientY = clientY;\n    },\n    handleDragEnd() {\n      this.isOnDrag = false;\n      this.$parent.userSelectNone = false;\n      document.removeEventListener('mousemove', this.handleDragMove);\n      document.removeEventListener('mouseup', this.handleDragEnd);\n    },\n    handleClick(e) {\n      let scrollTop;\n      if (e.clientY < this.top + 84) {\n        scrollTop = -256;\n      } else {\n        scrollTop = 256;\n      }\n      this.main.scrollBy({\n        top: scrollTop,\n        behavior: 'smooth',\n      });\n    },\n    setScrollbarHideTimeout() {\n      if (this.hideTimer !== null) clearTimeout(this.hideTimer);\n      this.hideTimer = setTimeout(() => {\n        if (!this.active) this.show = false;\n        this.hideTimer = null;\n      }, 4000);\n    },\n    restorePosition() {\n      const route = this.$route;\n      if (\n        !route.meta.savePosition ||\n        this.positions[route.name] === undefined ||\n        this.main === undefined\n      ) {\n        return;\n      }\n      this.main.scrollTo({ top: this.positions[route.name].scrollTop });\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n#scrollbar {\n  position: fixed;\n  right: 0;\n  top: 0;\n  bottom: 0;\n  width: 16px;\n  z-index: 1000;\n\n  #thumbContainer {\n    margin-top: 64px;\n    div {\n      transition: background 0.4s;\n      position: absolute;\n      right: 2px;\n      width: 8px;\n      height: 100%;\n      border-radius: 4px;\n      background: rgba(128, 128, 128, 0.38);\n    }\n  }\n  #thumbContainer.active div {\n    background: rgba(128, 128, 128, 0.58);\n  }\n}\n\n[data-theme='dark'] {\n  #thumbContainer div {\n    background: var(--color-secondary-bg);\n  }\n}\n\n#scrollbar.on-drag {\n  left: 0;\n  width: auto;\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.2s;\n}\n.fade-enter,\n.fade-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/SvgIcon.vue",
    "content": "<template>\n  <svg :class=\"svgClass\" aria-hidden=\"true\">\n    <use :xlink:href=\"iconName\" />\n  </svg>\n</template>\n\n<script>\nexport default {\n  name: 'SvgIcon',\n  props: {\n    iconClass: {\n      type: String,\n      required: true,\n    },\n    className: {\n      type: String,\n      default: '',\n    },\n  },\n  computed: {\n    iconName() {\n      return `#icon-${this.iconClass}`;\n    },\n    svgClass() {\n      if (this.className) {\n        return 'svg-icon ' + this.className;\n      } else {\n        return 'svg-icon';\n      }\n    },\n  },\n};\n</script>\n\n<style scoped>\n.svg-icon {\n  fill: currentColor;\n}\n</style>\n"
  },
  {
    "path": "src/components/Toast.vue",
    "content": "<template>\n  <transition name=\"fade\">\n    <div v-show=\"toast.show\" class=\"toast\">{{ toast.text }}</div>\n  </transition>\n</template>\n\n<script>\nimport { mapState } from 'vuex';\n\nexport default {\n  name: 'Toast',\n  computed: {\n    ...mapState(['toast']),\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.toast {\n  position: fixed;\n  bottom: 64px;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  font-size: 14px;\n  color: var(--color-text);\n  background: rgba(255, 255, 255, 0.88);\n  box-shadow: 0 6px 12px -4px rgba(0, 0, 0, 0.08);\n  border: 1px solid rgba(0, 0, 0, 0.06);\n  backdrop-filter: blur(12px);\n  border-radius: 8px;\n  box-sizing: border-box;\n  padding: 6px 12px;\n  z-index: 1010;\n}\n\n[data-theme='dark'] {\n  .toast {\n    background: rgba(46, 46, 46, 0.68);\n    backdrop-filter: blur(16px) contrast(120%);\n    border: 1px solid rgba(255, 255, 255, 0.08);\n  }\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.2s;\n}\n.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/TrackList.vue",
    "content": "<template>\n  <div class=\"track-list\">\n    <ContextMenu ref=\"menu\">\n      <div v-show=\"type !== 'cloudDisk'\" class=\"item-info\">\n        <img\n          :src=\"rightClickedTrackComputed.al.picUrl | resizeImage(224)\"\n          loading=\"lazy\"\n        />\n        <div class=\"info\">\n          <div class=\"title\">{{ rightClickedTrackComputed.name }}</div>\n          <div class=\"subtitle\">{{ rightClickedTrackComputed.ar[0].name }}</div>\n        </div>\n      </div>\n      <hr v-show=\"type !== 'cloudDisk'\" />\n      <div class=\"item\" @click=\"play\">{{ $t('contextMenu.play') }}</div>\n      <div class=\"item\" @click=\"addToQueue\">{{\n        $t('contextMenu.addToQueue')\n      }}</div>\n      <div\n        v-if=\"extraContextMenuItem.includes('removeTrackFromQueue')\"\n        class=\"item\"\n        @click=\"removeTrackFromQueue\"\n        >从队列删除</div\n      >\n      <hr v-show=\"type !== 'cloudDisk'\" />\n      <div\n        v-show=\"!isRightClickedTrackLiked && type !== 'cloudDisk'\"\n        class=\"item\"\n        @click=\"like\"\n      >\n        {{ $t('contextMenu.saveToMyLikedSongs') }}\n      </div>\n      <div\n        v-show=\"isRightClickedTrackLiked && type !== 'cloudDisk'\"\n        class=\"item\"\n        @click=\"like\"\n      >\n        {{ $t('contextMenu.removeFromMyLikedSongs') }}\n      </div>\n      <div\n        v-if=\"extraContextMenuItem.includes('removeTrackFromPlaylist')\"\n        class=\"item\"\n        @click=\"removeTrackFromPlaylist\"\n        >从歌单中删除</div\n      >\n      <div\n        v-show=\"type !== 'cloudDisk'\"\n        class=\"item\"\n        @click=\"addTrackToPlaylist\"\n        >{{ $t('contextMenu.addToPlaylist') }}</div\n      >\n      <div v-show=\"type !== 'cloudDisk'\" class=\"item\" @click=\"copyLink\">{{\n        $t('contextMenu.copyUrl')\n      }}</div>\n      <div\n        v-if=\"extraContextMenuItem.includes('removeTrackFromCloudDisk')\"\n        class=\"item\"\n        @click=\"removeTrackFromCloudDisk\"\n        >从云盘中删除</div\n      >\n    </ContextMenu>\n\n    <div :style=\"listStyles\">\n      <TrackListItem\n        v-for=\"(track, index) in tracks\"\n        :key=\"itemKey === 'id' ? track.id : `${track.id}${index}`\"\n        :track-prop=\"track\"\n        :track-no=\"index + 1\"\n        :highlight-playing-track=\"highlightPlayingTrack\"\n        @dblclick.native=\"playThisList(track.id || track.songId)\"\n        @click.right.native=\"openMenu($event, track, index)\"\n      />\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapActions, mapMutations, mapState } from 'vuex';\nimport { addOrRemoveTrackFromPlaylist } from '@/api/playlist';\nimport { cloudDiskTrackDelete } from '@/api/user';\nimport { isAccountLoggedIn } from '@/utils/auth';\n\nimport TrackListItem from '@/components/TrackListItem.vue';\nimport ContextMenu from '@/components/ContextMenu.vue';\nimport locale from '@/locale';\n\nexport default {\n  name: 'TrackList',\n  components: {\n    TrackListItem,\n    ContextMenu,\n  },\n  props: {\n    tracks: {\n      type: Array,\n      default: () => {\n        return [];\n      },\n    },\n    type: {\n      type: String,\n      default: 'tracklist',\n    }, // tracklist | album | playlist | cloudDisk\n    id: {\n      type: Number,\n      default: 0,\n    },\n    dbclickTrackFunc: {\n      type: String,\n      default: 'default',\n    },\n    albumObject: {\n      type: Object,\n      default: () => {\n        return {\n          artist: {\n            name: '',\n          },\n        };\n      },\n    },\n    extraContextMenuItem: {\n      type: Array,\n      default: () => {\n        return [\n          // 'removeTrackFromPlaylist'\n          // 'removeTrackFromQueue'\n          // 'removeTrackFromCloudDisk'\n        ];\n      },\n    },\n    columnNumber: {\n      type: Number,\n      default: 4,\n    },\n    highlightPlayingTrack: {\n      type: Boolean,\n      default: true,\n    },\n    itemKey: {\n      type: String,\n      default: 'id',\n    },\n  },\n  data() {\n    return {\n      rightClickedTrack: {\n        id: 0,\n        name: '',\n        ar: [{ name: '' }],\n        al: { picUrl: '' },\n      },\n      rightClickedTrackIndex: -1,\n      listStyles: {},\n    };\n  },\n  computed: {\n    ...mapState(['liked', 'player']),\n    isRightClickedTrackLiked() {\n      return this.liked.songs.includes(this.rightClickedTrack?.id);\n    },\n    rightClickedTrackComputed() {\n      return this.type === 'cloudDisk'\n        ? {\n            id: 0,\n            name: '',\n            ar: [{ name: '' }],\n            al: { picUrl: '' },\n          }\n        : this.rightClickedTrack;\n    },\n  },\n  created() {\n    if (this.type === 'tracklist') {\n      this.listStyles = {\n        display: 'grid',\n        gap: '4px',\n        gridTemplateColumns: `repeat(${this.columnNumber}, 1fr)`,\n      };\n    }\n  },\n  methods: {\n    ...mapMutations(['updateModal']),\n    ...mapActions(['nextTrack', 'showToast', 'likeATrack']),\n    openMenu(e, track, index = -1) {\n      this.rightClickedTrack = track;\n      this.rightClickedTrackIndex = index;\n      this.$refs.menu.openMenu(e);\n    },\n    closeMenu() {\n      this.rightClickedTrack = {\n        id: 0,\n        name: '',\n        ar: [{ name: '' }],\n        al: { picUrl: '' },\n      };\n      this.rightClickedTrackIndex = -1;\n    },\n    playThisList(trackID) {\n      if (this.dbclickTrackFunc === 'default') {\n        this.playThisListDefault(trackID);\n      } else if (this.dbclickTrackFunc === 'none') {\n        // do nothing\n      } else if (this.dbclickTrackFunc === 'playTrackOnListByID') {\n        this.player.playTrackOnListByID(trackID);\n      } else if (this.dbclickTrackFunc === 'playPlaylistByID') {\n        this.player.playPlaylistByID(this.id, trackID);\n      } else if (this.dbclickTrackFunc === 'playAList') {\n        let trackIDs = this.tracks.map(t => t.id || t.songId);\n        this.player.replacePlaylist(trackIDs, this.id, 'artist', trackID);\n      } else if (this.dbclickTrackFunc === 'dailyTracks') {\n        let trackIDs = this.tracks.map(t => t.id);\n        this.player.replacePlaylist(trackIDs, '/daily/songs', 'url', trackID);\n      } else if (this.dbclickTrackFunc === 'playCloudDisk') {\n        let trackIDs = this.tracks.map(t => t.id || t.songId);\n        this.player.replacePlaylist(trackIDs, this.id, 'cloudDisk', trackID);\n      }\n    },\n    playThisListDefault(trackID) {\n      if (this.type === 'playlist') {\n        this.player.playPlaylistByID(this.id, trackID);\n      } else if (this.type === 'album') {\n        this.player.playAlbumByID(this.id, trackID);\n      } else if (this.type === 'tracklist') {\n        let trackIDs = this.tracks.map(t => t.id);\n        this.player.replacePlaylist(trackIDs, this.id, 'artist', trackID);\n      }\n    },\n    play() {\n      this.player.addTrackToPlayNext(this.rightClickedTrack.id, true);\n    },\n    addToQueue() {\n      this.player.addTrackToPlayNext(this.rightClickedTrack.id);\n    },\n    like() {\n      this.likeATrack(this.rightClickedTrack.id);\n    },\n    addTrackToPlaylist() {\n      if (!isAccountLoggedIn()) {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      this.updateModal({\n        modalName: 'addTrackToPlaylistModal',\n        key: 'show',\n        value: true,\n      });\n      this.updateModal({\n        modalName: 'addTrackToPlaylistModal',\n        key: 'selectedTrackID',\n        value: this.rightClickedTrack.id,\n      });\n    },\n    removeTrackFromPlaylist() {\n      if (!isAccountLoggedIn()) {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      if (confirm(`确定要从歌单删除 ${this.rightClickedTrack.name}？`)) {\n        let trackID = this.rightClickedTrack.id;\n        addOrRemoveTrackFromPlaylist({\n          op: 'del',\n          pid: this.id,\n          tracks: trackID,\n        }).then(data => {\n          this.showToast(\n            data.body.code === 200\n              ? locale.t('toast.removedFromPlaylist')\n              : data.body.message\n          );\n          this.$parent.removeTrack(trackID);\n        });\n      }\n    },\n    copyLink() {\n      this.$copyText(\n        `https://music.163.com/song?id=${this.rightClickedTrack.id}`\n      )\n        .then(() => {\n          this.showToast(locale.t('toast.copied'));\n        })\n        .catch(err => {\n          this.showToast(`${locale.t('toast.copyFailed')}${err}`);\n        });\n    },\n    removeTrackFromQueue() {\n      this.$store.state.player.removeTrackFromQueue(\n        this.rightClickedTrackIndex\n      );\n    },\n    removeTrackFromCloudDisk() {\n      if (confirm(`确定要从云盘删除 ${this.rightClickedTrack.songName}？`)) {\n        let trackID = this.rightClickedTrack.songId;\n        cloudDiskTrackDelete(trackID).then(data => {\n          this.showToast(\n            data.code === 200 ? '已将此歌曲从云盘删除' : data.message\n          );\n          let newCloudDisk = this.liked.cloudDisk.filter(\n            t => t.songId !== trackID\n          );\n          this.$store.commit('updateLikedXXX', {\n            name: 'cloudDisk',\n            data: newCloudDisk,\n          });\n        });\n      }\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped></style>\n"
  },
  {
    "path": "src/components/TrackListItem.vue",
    "content": "<template>\n  <div\n    class=\"track\"\n    :class=\"trackClass\"\n    :style=\"trackStyle\"\n    :title=\"showUnavailableSongInGreyStyle ? track.reason : ''\"\n    @mouseover=\"hover = true\"\n    @mouseleave=\"hover = false\"\n  >\n    <img\n      v-if=\"!isAlbum\"\n      :src=\"imgUrl\"\n      loading=\"lazy\"\n      :class=\"{ hover: focus }\"\n      @click=\"goToAlbum\"\n    />\n    <div v-if=\"showOrderNumber\" class=\"no\">\n      <button v-show=\"focus && playable && !isPlaying\" @click=\"playTrack\">\n        <svg-icon\n          icon-class=\"play\"\n          style=\"height: 14px; width: 14px\"\n        ></svg-icon>\n      </button>\n      <span v-show=\"(!focus || !playable) && !isPlaying\">{{ trackNo }}</span>\n      <button v-show=\"isPlaying\">\n        <svg-icon\n          icon-class=\"volume\"\n          style=\"height: 16px; width: 16px\"\n        ></svg-icon>\n      </button>\n    </div>\n    <div class=\"title-and-artist\">\n      <div class=\"container\">\n        <div class=\"title\">\n          {{ track.name }}\n          <span v-if=\"isSubTitle\" :title=\"subTitle\" class=\"sub-title\">\n            ({{ subTitle }})\n          </span>\n          <span v-if=\"isAlbum\" class=\"featured\">\n            <ArtistsInLine\n              :artists=\"track.ar\"\n              :exclude=\"$parent.albumObject.artist.name\"\n              prefix=\"-\"\n          /></span>\n          <span\n            v-if=\"isAlbum && (track.mark & 1048576) === 1048576\"\n            class=\"explicit-symbol\"\n            ><ExplicitSymbol\n          /></span>\n        </div>\n        <div v-if=\"!isAlbum\" class=\"artist\">\n          <span\n            v-if=\"(track.mark & 1048576) === 1048576\"\n            class=\"explicit-symbol before-artist\"\n            ><ExplicitSymbol :size=\"15\"\n          /></span>\n          <ArtistsInLine :artists=\"artists\" />\n        </div>\n      </div>\n      <div></div>\n    </div>\n\n    <div v-if=\"showAlbumName\" class=\"album\">\n      <router-link v-if=\"album && album.id\" :to=\"`/album/${album.id}`\">{{\n        album.name\n      }}</router-link>\n      <div></div>\n    </div>\n\n    <div v-if=\"showLikeButton\" class=\"actions\">\n      <button @click=\"likeThisSong\">\n        <svg-icon\n          icon-class=\"heart\"\n          :style=\"{\n            visibility: focus && !isLiked ? 'visible' : 'hidden',\n          }\"\n        ></svg-icon>\n        <svg-icon v-show=\"isLiked\" icon-class=\"heart-solid\"></svg-icon>\n      </button>\n    </div>\n    <div v-if=\"showTrackTime\" class=\"time\">\n      {{ track.dt | formatTime }}\n    </div>\n\n    <div v-if=\"track.playCount\" class=\"count\"> {{ track.playCount }}</div>\n  </div>\n</template>\n\n<script>\nimport ArtistsInLine from '@/components/ArtistsInLine.vue';\nimport ExplicitSymbol from '@/components/ExplicitSymbol.vue';\nimport { mapState } from 'vuex';\nimport { isNil } from 'lodash';\n\nexport default {\n  name: 'TrackListItem',\n  components: { ArtistsInLine, ExplicitSymbol },\n\n  props: {\n    trackProp: Object,\n    trackNo: Number,\n    highlightPlayingTrack: {\n      type: Boolean,\n      default: true,\n    },\n  },\n\n  data() {\n    return { hover: false, trackStyle: {} };\n  },\n\n  computed: {\n    ...mapState(['settings']),\n    track() {\n      return this.type === 'cloudDisk'\n        ? this.trackProp.simpleSong\n        : this.trackProp;\n    },\n    playable() {\n      return this.track?.privilege?.pl > 0 || this.track?.playable;\n    },\n    imgUrl() {\n      let image =\n        this.track?.al?.picUrl ??\n        this.track?.album?.picUrl ??\n        'https://p2.music.126.net/UeTuwE7pvjBpypWLudqukA==/3132508627578625.jpg';\n      return image + '?param=224y224';\n    },\n    artists() {\n      const { ar, artists } = this.track;\n      if (!isNil(ar)) return ar;\n      if (!isNil(artists)) return artists;\n      return [];\n    },\n    album() {\n      return this.track.album || this.track.al || this.track?.simpleSong?.al;\n    },\n    subTitle() {\n      let tn = undefined;\n      if (\n        this.track?.tns?.length > 0 &&\n        this.track.name !== this.track.tns[0]\n      ) {\n        tn = this.track.tns[0];\n      }\n\n      //优先显示alia\n      if (this.$store.state.settings.subTitleDefault) {\n        return this.track?.alia?.length > 0 ? this.track.alia[0] : tn;\n      } else {\n        return tn === undefined ? this.track.alia[0] : tn;\n      }\n    },\n    type() {\n      return this.$parent.type;\n    },\n    isAlbum() {\n      return this.type === 'album';\n    },\n    isSubTitle() {\n      return (\n        (this.track?.tns?.length > 0 &&\n          this.track.name !== this.track.tns[0]) ||\n        this.track.alia?.length > 0\n      );\n    },\n    isPlaylist() {\n      return this.type === 'playlist';\n    },\n    isLiked() {\n      return this.$parent.liked.songs.includes(this.track?.id);\n    },\n    isPlaying() {\n      return this.$store.state.player.currentTrack.id === this.track?.id;\n    },\n    trackClass() {\n      let trackClass = [this.type];\n      if (!this.playable && this.showUnavailableSongInGreyStyle)\n        trackClass.push('disable');\n      if (this.isPlaying && this.highlightPlayingTrack)\n        trackClass.push('playing');\n      if (this.focus) trackClass.push('focus');\n      return trackClass;\n    },\n    isMenuOpened() {\n      return this.$parent.rightClickedTrack.id === this.track.id ? true : false;\n    },\n    focus() {\n      return (\n        (this.hover && this.$parent.rightClickedTrack.id === 0) ||\n        this.isMenuOpened\n      );\n    },\n    showUnavailableSongInGreyStyle() {\n      return process.env.IS_ELECTRON\n        ? !this.$store.state.settings.enableUnblockNeteaseMusic\n        : true;\n    },\n    showLikeButton() {\n      return this.type !== 'tracklist' && this.type !== 'cloudDisk';\n    },\n    showOrderNumber() {\n      return this.type === 'album';\n    },\n    showAlbumName() {\n      return this.type !== 'album' && this.type !== 'tracklist';\n    },\n    showTrackTime() {\n      return this.type !== 'tracklist';\n    },\n  },\n\n  methods: {\n    goToAlbum() {\n      if (this.track.al.id === 0) return;\n      this.$router.push({ path: '/album/' + this.track.al.id });\n    },\n    playTrack() {\n      this.$parent.playThisList(this.track.id);\n    },\n    likeThisSong() {\n      this.$parent.likeATrack(this.track.id);\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\nbutton {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 8px;\n  background: transparent;\n  border-radius: 25%;\n  transition: transform 0.2s;\n  .svg-icon {\n    height: 16px;\n    width: 16px;\n    color: var(--color-primary);\n  }\n  &:hover {\n    transform: scale(1.12);\n  }\n  &:active {\n    transform: scale(0.96);\n  }\n}\n\n.track {\n  display: flex;\n  align-items: center;\n  padding: 8px;\n  border-radius: 12px;\n  user-select: none;\n\n  .no {\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    border-radius: 8px;\n    margin: 0 20px 0 10px;\n    width: 12px;\n    color: var(--color-text);\n    cursor: default;\n    span {\n      opacity: 0.58;\n    }\n  }\n\n  .explicit-symbol {\n    opacity: 0.28;\n    color: var(--color-text);\n    .svg-icon {\n      margin-bottom: -3px;\n    }\n  }\n\n  .explicit-symbol.before-artist {\n    .svg-icon {\n      margin-bottom: -3px;\n    }\n  }\n\n  img {\n    border-radius: 8px;\n    height: 46px;\n    width: 46px;\n    margin-right: 20px;\n    border: 1px solid rgba(0, 0, 0, 0.04);\n    cursor: pointer;\n  }\n\n  img.hover {\n    filter: drop-shadow(100 200 0 black);\n  }\n\n  .title-and-artist {\n    flex: 1;\n    display: flex;\n    .container {\n      display: flex;\n      flex-direction: column;\n    }\n    .title {\n      font-size: 18px;\n      font-weight: 600;\n      color: var(--color-text);\n      cursor: default;\n      padding-right: 16px;\n      display: -webkit-box;\n      -webkit-box-orient: vertical;\n      -webkit-line-clamp: 1;\n      overflow: hidden;\n      word-break: break-all;\n      .featured {\n        margin-right: 2px;\n        font-weight: 500;\n        font-size: 14px;\n        opacity: 0.72;\n      }\n      .sub-title {\n        color: #7a7a7a;\n        opacity: 0.7;\n        margin-left: 4px;\n      }\n    }\n    .artist {\n      margin-top: 2px;\n      font-size: 13px;\n      opacity: 0.68;\n      color: var(--color-text);\n      display: -webkit-box;\n      -webkit-box-orient: vertical;\n      -webkit-line-clamp: 1;\n      overflow: hidden;\n      a {\n        span {\n          margin-right: 3px;\n          opacity: 0.8;\n        }\n        &:hover {\n          text-decoration: underline;\n          cursor: pointer;\n        }\n      }\n    }\n  }\n  .album {\n    flex: 1;\n    display: flex;\n    font-size: 16px;\n    opacity: 0.88;\n    color: var(--color-text);\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    overflow: hidden;\n  }\n  .time,\n  .count {\n    font-size: 16px;\n    width: 50px;\n    cursor: default;\n    display: flex;\n    justify-content: flex-end;\n    margin-right: 10px;\n    font-variant-numeric: tabular-nums;\n    opacity: 0.88;\n    color: var(--color-text);\n  }\n  .count {\n    font-weight: bold;\n    font-size: 22px;\n    line-height: 22px;\n  }\n}\n\n.track.focus {\n  transition: all 0.3s;\n  background: var(--color-secondary-bg);\n}\n\n.track.disable {\n  img {\n    filter: grayscale(1) opacity(0.6);\n  }\n  .title,\n  .artist,\n  .album,\n  .time,\n  .no,\n  .featured {\n    opacity: 0.28 !important;\n  }\n  &:hover {\n    background: none;\n  }\n}\n\n.track.tracklist {\n  img {\n    height: 36px;\n    width: 36px;\n    border-radius: 6px;\n    margin-right: 14px;\n    cursor: pointer;\n  }\n  .title {\n    font-size: 16px;\n  }\n  .artist {\n    font-size: 12px;\n  }\n}\n\n.track.album {\n  height: 32px;\n}\n\n.actions {\n  width: 80px;\n  display: flex;\n  justify-content: flex-end;\n}\n\n.track.playing {\n  background: var(--color-primary-bg);\n  color: var(--color-primary);\n  .title,\n  .album,\n  .time,\n  .title-and-artist .sub-title {\n    color: var(--color-primary);\n  }\n  .title .featured,\n  .artist,\n  .explicit-symbol,\n  .count {\n    color: var(--color-primary);\n    opacity: 0.88;\n  }\n  .no span {\n    color: var(--color-primary);\n    opacity: 0.78;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/Win32Titlebar.vue",
    "content": "<template>\n  <div class=\"win32-titlebar\">\n    <div class=\"title\">{{ title }}</div>\n    <div class=\"controls\">\n      <div\n        class=\"button minimize codicon codicon-chrome-minimize\"\n        @click=\"windowMinimize\"\n      ></div>\n      <div\n        class=\"button max-restore codicon\"\n        :class=\"{\n          'codicon-chrome-restore': isMaximized,\n          'codicon-chrome-maximize': !isMaximized,\n        }\"\n        @click=\"windowMaxRestore\"\n      ></div>\n      <div\n        class=\"button close codicon codicon-chrome-close\"\n        @click=\"windowClose\"\n      ></div>\n    </div>\n  </div>\n</template>\n\n<script>\n// icons by https://github.com/microsoft/vscode-codicons\nimport 'vscode-codicons/dist/codicon.css';\n\nimport { mapState } from 'vuex';\n\nconst electron =\n  process.env.IS_ELECTRON === true ? window.require('electron') : null;\nconst ipcRenderer =\n  process.env.IS_ELECTRON === true ? electron.ipcRenderer : null;\n\nexport default {\n  name: 'Win32Titlebar',\n  data() {\n    return {\n      isMaximized: false,\n    };\n  },\n  computed: {\n    ...mapState(['title']),\n  },\n  created() {\n    if (process.env.IS_ELECTRON === true) {\n      ipcRenderer.on('isMaximized', (_, value) => {\n        this.isMaximized = value;\n      });\n    }\n  },\n  methods: {\n    windowMinimize() {\n      ipcRenderer.send('minimize');\n    },\n    windowMaxRestore() {\n      ipcRenderer.send('maximizeOrUnmaximize');\n    },\n    windowClose() {\n      ipcRenderer.send('close');\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.win32-titlebar {\n  color: var(--color-text);\n  position: fixed;\n  left: 0;\n  top: 0;\n  right: 0;\n  -webkit-app-region: drag;\n  display: flex;\n  align-items: center;\n  --hover: #e6e6e6;\n  --active: #cccccc;\n\n  .title {\n    padding: 8px 12px;\n    font-size: 12px;\n    font-family: 'Segoe UI', 'Microsoft YaHei UI', 'Microsoft YaHei', sans-serif;\n  }\n  .controls {\n    height: 32px;\n    margin-left: auto;\n    justify-content: flex-end;\n    display: flex;\n    .button {\n      height: 100%;\n      width: 46px;\n      font-size: 16px;\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      -webkit-app-region: no-drag;\n      &:hover {\n        background: var(--hover);\n      }\n      &:active {\n        background: var(--active);\n      }\n      &.close {\n        &:hover {\n          background: #c42c1b;\n          color: rgba(255, 255, 255, 0.8);\n        }\n        &:active {\n          background: #f1707a;\n          color: #000;\n        }\n      }\n    }\n  }\n}\n[data-theme='dark'] .win32-titlebar {\n  --hover: #191919;\n  --active: #333333;\n}\n</style>\n"
  },
  {
    "path": "src/electron/dockMenu.js",
    "content": "const { Menu } = require('electron');\n\nexport function createDockMenu(win) {\n  return Menu.buildFromTemplate([\n    {\n      label: 'Play',\n      click() {\n        win.webContents.send('play');\n      },\n    },\n    { type: 'separator' },\n    {\n      label: 'Next',\n      click() {\n        win.webContents.send('next');\n      },\n    },\n    {\n      label: 'Previous',\n      click() {\n        win.webContents.send('previous');\n      },\n    },\n  ]);\n}\n"
  },
  {
    "path": "src/electron/globalShortcut.js",
    "content": "import defaultShortcuts from '@/utils/shortcuts';\nconst { globalShortcut } = require('electron');\n\nconst clc = require('cli-color');\nconst log = text => {\n  console.log(`${clc.blueBright('[globalShortcut.js]')} ${text}`);\n};\n\nexport function registerGlobalShortcut(win, store) {\n  log('registerGlobalShortcut');\n  let shortcuts = store.get('settings.shortcuts');\n  if (shortcuts === undefined) {\n    shortcuts = defaultShortcuts;\n  }\n\n  globalShortcut.register(\n    shortcuts.find(s => s.id === 'play').globalShortcut,\n    () => {\n      win.webContents.send('play');\n    }\n  );\n  globalShortcut.register(\n    shortcuts.find(s => s.id === 'next').globalShortcut,\n    () => {\n      win.webContents.send('next');\n    }\n  );\n  globalShortcut.register(\n    shortcuts.find(s => s.id === 'previous').globalShortcut,\n    () => {\n      win.webContents.send('previous');\n    }\n  );\n  globalShortcut.register(\n    shortcuts.find(s => s.id === 'increaseVolume').globalShortcut,\n    () => {\n      win.webContents.send('increaseVolume');\n    }\n  );\n  globalShortcut.register(\n    shortcuts.find(s => s.id === 'decreaseVolume').globalShortcut,\n    () => {\n      win.webContents.send('decreaseVolume');\n    }\n  );\n  globalShortcut.register(\n    shortcuts.find(s => s.id === 'like').globalShortcut,\n    () => {\n      win.webContents.send('like');\n    }\n  );\n  globalShortcut.register(\n    shortcuts.find(s => s.id === 'minimize').globalShortcut,\n    () => {\n      win.isVisible() ? win.hide() : win.show();\n    }\n  );\n}\n"
  },
  {
    "path": "src/electron/ipcMain.js",
    "content": "import { app, dialog, globalShortcut, ipcMain } from 'electron';\nimport UNM from '@unblockneteasemusic/rust-napi';\nimport { registerGlobalShortcut } from '@/electron/globalShortcut';\nimport cloneDeep from 'lodash/cloneDeep';\nimport shortcuts from '@/utils/shortcuts';\nimport { createMenu } from './menu';\nimport { isCreateTray, isMac } from '@/utils/platform';\n\nconst clc = require('cli-color');\nconst log = text => {\n  console.log(`${clc.blueBright('[ipcMain.js]')} ${text}`);\n};\n\nconst exitAsk = (e, win) => {\n  e.preventDefault(); //阻止默认行为\n  dialog\n    .showMessageBox({\n      type: 'info',\n      title: 'Information',\n      cancelId: 2,\n      defaultId: 0,\n      message: '确定要关闭吗？',\n      buttons: ['最小化', '直接退出'],\n    })\n    .then(result => {\n      if (result.response == 0) {\n        e.preventDefault(); //阻止默认行为\n        win.minimize(); //调用 最小化实例方法\n      } else if (result.response == 1) {\n        win = null;\n        //app.quit();\n        app.exit(); //exit()直接关闭客户端，不会执行quit();\n      }\n    })\n    .catch(err => {\n      log(err);\n    });\n};\n\nconst exitAskWithoutMac = (e, win) => {\n  e.preventDefault(); //阻止默认行为\n  dialog\n    .showMessageBox({\n      type: 'info',\n      title: 'Information',\n      cancelId: 2,\n      defaultId: 0,\n      message: '确定要关闭吗？',\n      buttons: ['最小化到托盘', '直接退出'],\n      checkboxLabel: '记住我的选择',\n    })\n    .then(result => {\n      if (result.checkboxChecked && result.response !== 2) {\n        win.webContents.send(\n          'rememberCloseAppOption',\n          result.response === 0 ? 'minimizeToTray' : 'exit'\n        );\n      }\n\n      if (result.response === 0) {\n        e.preventDefault(); //阻止默认行为\n        win.hide(); //调用 最小化实例方法\n      } else if (result.response === 1) {\n        win = null;\n        //app.quit();\n        app.exit(); //exit()直接关闭客户端，不会执行quit();\n      }\n    })\n    .catch(err => {\n      log(err);\n    });\n};\n\nconst client = require('discord-rich-presence')('818936529484906596');\n\n/**\n * Make data a Buffer.\n *\n * @param {?} data The data to convert.\n * @returns {import(\"buffer\").Buffer} The converted data.\n */\nfunction toBuffer(data) {\n  if (data instanceof Buffer) {\n    return data;\n  } else {\n    return Buffer.from(data);\n  }\n}\n\n/**\n * Get the file base64 data from bilivideo.\n *\n * @param {string} url The URL to fetch.\n * @returns {Promise<string>} The file base64 data.\n */\nasync function getBiliVideoFile(url) {\n  const axios = await import('axios').then(m => m.default);\n  const response = await axios.get(url, {\n    headers: {\n      Referer: 'https://www.bilibili.com/',\n      'User-Agent': 'okhttp/3.4.1',\n    },\n    responseType: 'arraybuffer',\n  });\n\n  const buffer = toBuffer(response.data);\n  const encodedData = buffer.toString('base64');\n\n  return encodedData;\n}\n\n/**\n * Parse the source string (`a, b`) to source list `['a', 'b']`.\n *\n * @param {import(\"@unblockneteasemusic/rust-napi\").Executor} executor\n * @param {string} sourceString The source string.\n * @returns {string[]} The source list.\n */\nfunction parseSourceStringToList(executor, sourceString) {\n  const availableSource = executor.list();\n\n  return sourceString\n    .split(',')\n    .map(s => s.trim().toLowerCase())\n    .filter(s => {\n      const isAvailable = availableSource.includes(s);\n\n      if (!isAvailable) {\n        log(`This source is not one of the supported source: ${s}`);\n      }\n\n      return isAvailable;\n    });\n}\n\nexport function initIpcMain(win, store, trayEventEmitter) {\n  // WIP: Do not enable logging as it has some issues in non-blocking I/O environment.\n  // UNM.enableLogging(UNM.LoggingType.ConsoleEnv);\n  const unmExecutor = new UNM.Executor();\n\n  ipcMain.handle(\n    'unblock-music',\n    /**\n     *\n     * @param {*} _\n     * @param {string | null} sourceListString\n     * @param {Record<string, any>} ncmTrack\n     * @param {UNM.Context} context\n     */\n    async (_, sourceListString, ncmTrack, context) => {\n      // Formt the track input\n      // FIXME: Figure out the structure of Track\n      const song = {\n        id: ncmTrack.id && ncmTrack.id.toString(),\n        name: ncmTrack.name,\n        duration: ncmTrack.dt,\n        album: ncmTrack.al && {\n          id: ncmTrack.al.id && ncmTrack.al.id.toString(),\n          name: ncmTrack.al.name,\n        },\n        artists: ncmTrack.ar\n          ? ncmTrack.ar.map(({ id, name }) => ({\n              id: id && id.toString(),\n              name,\n            }))\n          : [],\n      };\n\n      const sourceList =\n        typeof sourceListString === 'string'\n          ? parseSourceStringToList(unmExecutor, sourceListString)\n          : ['ytdl', 'bilibili', 'pyncm', 'kugou'];\n      log(`[UNM] using source: ${sourceList.join(', ')}`);\n      log(`[UNM] using configuration: ${JSON.stringify(context)}`);\n\n      try {\n        // TODO: tell users to install yt-dlp.\n        const matchedAudio = await unmExecutor.search(\n          sourceList,\n          song,\n          context\n        );\n        const retrievedSong = await unmExecutor.retrieve(matchedAudio, context);\n\n        // bilibili's audio file needs some special treatment\n        if (retrievedSong.url.includes('bilivideo.com')) {\n          retrievedSong.url = await getBiliVideoFile(retrievedSong.url);\n        }\n\n        log(`respond with retrieve song…`);\n        log(JSON.stringify(matchedAudio));\n        return retrievedSong;\n      } catch (err) {\n        const errorMessage = err instanceof Error ? `${err.message}` : `${err}`;\n        log(`UnblockNeteaseMusic failed: ${errorMessage}`);\n        return null;\n      }\n    }\n  );\n\n  ipcMain.on('close', e => {\n    if (isMac) {\n      win.hide();\n      exitAsk(e, win);\n    } else {\n      let closeOpt = store.get('settings.closeAppOption');\n      if (closeOpt === 'exit') {\n        win = null;\n        //app.quit();\n        app.exit(); //exit()直接关闭客户端，不会执行quit();\n      } else if (closeOpt === 'minimizeToTray') {\n        e.preventDefault();\n        win.hide();\n      } else {\n        exitAskWithoutMac(e, win);\n      }\n    }\n  });\n\n  ipcMain.on('minimize', () => {\n    win.minimize();\n  });\n\n  ipcMain.on('maximizeOrUnmaximize', () => {\n    win.isMaximized() ? win.unmaximize() : win.maximize();\n  });\n\n  ipcMain.on('settings', (event, options) => {\n    store.set('settings', options);\n    if (options.enableGlobalShortcut) {\n      registerGlobalShortcut(win, store);\n    } else {\n      log('unregister global shortcut');\n      globalShortcut.unregisterAll();\n    }\n  });\n\n  ipcMain.on('playDiscordPresence', (event, track) => {\n    client.updatePresence({\n      details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','),\n      state: track.al.name,\n      endTimestamp: Date.now() + track.dt,\n      largeImageKey: track.al.picUrl,\n      largeImageText: 'Listening ' + track.name,\n      smallImageKey: 'play',\n      smallImageText: 'Playing',\n      instance: true,\n    });\n  });\n\n  ipcMain.on('pauseDiscordPresence', (event, track) => {\n    client.updatePresence({\n      details: track.name + ' - ' + track.ar.map(ar => ar.name).join(','),\n      state: track.al.name,\n      largeImageKey: track.al.picUrl,\n      largeImageText: 'YesPlayMusic',\n      smallImageKey: 'pause',\n      smallImageText: 'Pause',\n      instance: true,\n    });\n  });\n\n  ipcMain.on('setProxy', (event, config) => {\n    const proxyRules = `${config.protocol}://${config.server}:${config.port}`;\n    store.set('proxy', proxyRules);\n    win.webContents.session.setProxy(\n      {\n        proxyRules,\n      },\n      () => {\n        log('finished setProxy');\n      }\n    );\n  });\n\n  ipcMain.on('removeProxy', (event, arg) => {\n    log('removeProxy');\n    win.webContents.session.setProxy({});\n    store.set('proxy', '');\n  });\n\n  ipcMain.on('switchGlobalShortcutStatusTemporary', (e, status) => {\n    log('switchGlobalShortcutStatusTemporary');\n    if (status === 'disable') {\n      globalShortcut.unregisterAll();\n    } else {\n      registerGlobalShortcut(win, store);\n    }\n  });\n\n  ipcMain.on('updateShortcut', (e, { id, type, shortcut }) => {\n    log('updateShortcut');\n    let shortcuts = store.get('settings.shortcuts');\n    let newShortcut = shortcuts.find(s => s.id === id);\n    newShortcut[type] = shortcut;\n    store.set('settings.shortcuts', shortcuts);\n\n    createMenu(win, store);\n    globalShortcut.unregisterAll();\n    registerGlobalShortcut(win, store);\n  });\n\n  ipcMain.on('restoreDefaultShortcuts', () => {\n    log('restoreDefaultShortcuts');\n    store.set('settings.shortcuts', cloneDeep(shortcuts));\n\n    createMenu(win, store);\n    globalShortcut.unregisterAll();\n    registerGlobalShortcut(win, store);\n  });\n\n  if (isCreateTray) {\n    ipcMain.on('updateTrayTooltip', (_, title) => {\n      trayEventEmitter.emit('updateTooltip', title);\n    });\n    ipcMain.on('updateTrayPlayState', (_, isPlaying) => {\n      trayEventEmitter.emit('updatePlayState', isPlaying);\n    });\n    ipcMain.on('updateTrayLikeState', (_, isLiked) => {\n      trayEventEmitter.emit('updateLikeState', isLiked);\n    });\n    ipcMain.on('updateTrayIcon', () => {\n      trayEventEmitter.emit('updateIcon');\n    });\n  }\n}\n"
  },
  {
    "path": "src/electron/ipcRenderer.js",
    "content": "import store from '@/store';\n\nconst player = store.state.player;\n\nexport function ipcRenderer(vueInstance) {\n  const self = vueInstance;\n  // 添加专有的类名\n  document.body.setAttribute('data-electron', 'yes');\n  document.body.setAttribute(\n    'data-electron-os',\n    window.require('os').platform()\n  );\n  // ipc message channel\n  const electron = window.require('electron');\n  const ipcRenderer = electron.ipcRenderer;\n\n  // listens to the main process 'changeRouteTo' event and changes the route from\n  // inside this Vue instance, according to what path the main process requires.\n  // responds to Menu click() events at the main process and changes the route accordingly.\n\n  ipcRenderer.on('changeRouteTo', (event, path) => {\n    self.$router.push(path);\n    if (store.state.showLyrics) {\n      store.commit('toggleLyrics');\n    }\n  });\n\n  ipcRenderer.on('search', () => {\n    // 触发数据响应\n    self.$refs.navbar.$refs.searchInput.focus();\n    self.$refs.navbar.inputFocus = true;\n  });\n\n  ipcRenderer.on('play', () => {\n    player.playOrPause();\n  });\n\n  ipcRenderer.on('next', () => {\n    if (player.isPersonalFM) {\n      player.playNextFMTrack();\n    } else {\n      player.playNextTrack();\n    }\n  });\n\n  ipcRenderer.on('previous', () => {\n    player.playPrevTrack();\n  });\n\n  ipcRenderer.on('increaseVolume', () => {\n    if (player.volume + 0.1 >= 1) {\n      return (player.volume = 1);\n    }\n    player.volume += 0.1;\n  });\n\n  ipcRenderer.on('decreaseVolume', () => {\n    if (player.volume - 0.1 <= 0) {\n      return (player.volume = 0);\n    }\n    player.volume -= 0.1;\n  });\n\n  ipcRenderer.on('like', () => {\n    store.dispatch('likeATrack', player.currentTrack.id);\n  });\n\n  ipcRenderer.on('repeat', () => {\n    player.switchRepeatMode();\n  });\n\n  ipcRenderer.on('shuffle', () => {\n    player.switchShuffle();\n  });\n\n  ipcRenderer.on('routerGo', (event, where) => {\n    self.$refs.navbar.go(where);\n  });\n\n  ipcRenderer.on('nextUp', () => {\n    self.$refs.player.goToNextTracksPage();\n  });\n\n  ipcRenderer.on('rememberCloseAppOption', (event, value) => {\n    store.commit('updateSettings', {\n      key: 'closeAppOption',\n      value,\n    });\n  });\n\n  ipcRenderer.on('setPosition', (event, position) => {\n    player._howler.seek(position);\n  });\n}\n"
  },
  {
    "path": "src/electron/menu.js",
    "content": "import defaultShortcuts from '@/utils/shortcuts';\nconst { app, Menu } = require('electron');\n// import { autoUpdater } from \"electron-updater\"\n// const version = app.getVersion();\n\nconst isMac = process.platform === 'darwin';\n\nexport function createMenu(win, store) {\n  let shortcuts = store.get('settings.shortcuts');\n  if (shortcuts === undefined) {\n    shortcuts = defaultShortcuts;\n  }\n\n  let menu = null;\n  const template = [\n    ...(isMac\n      ? [\n          {\n            label: app.name,\n            submenu: [\n              { role: 'about' },\n              { type: 'separator' },\n              { role: 'services' },\n              { type: 'separator' },\n              { type: 'separator' },\n              {\n                label: 'Preferences...',\n                accelerator: 'CmdOrCtrl+,',\n                click: () => {\n                  win.webContents.send('changeRouteTo', '/settings');\n                },\n                role: 'preferences',\n              },\n              { type: 'separator' },\n              { role: 'hide' },\n              { role: 'hideothers' },\n              { role: 'unhide' },\n              { type: 'separator' },\n              { role: 'quit' },\n            ],\n          },\n        ]\n      : []),\n    {\n      label: 'Edit',\n      submenu: [\n        { role: 'undo' },\n        { role: 'redo' },\n        { type: 'separator' },\n        { role: 'cut' },\n        { role: 'copy' },\n        { role: 'paste' },\n        ...(isMac\n          ? [\n              { role: 'delete' },\n              { role: 'selectAll' },\n              { type: 'separator' },\n              {\n                label: 'Speech',\n                submenu: [{ role: 'startspeaking' }, { role: 'stopspeaking' }],\n              },\n            ]\n          : [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]),\n        {\n          label: 'Search',\n          accelerator: 'CmdOrCtrl+F',\n          click: () => {\n            win.webContents.send('search');\n          },\n        },\n      ],\n    },\n    {\n      label: 'Controls',\n      submenu: [\n        {\n          label: 'Play',\n          accelerator: shortcuts.find(s => s.id === 'play').shortcut,\n          click: () => {\n            win.webContents.send('play');\n          },\n        },\n        {\n          label: 'Next',\n          accelerator: shortcuts.find(s => s.id === 'next').shortcut,\n          click: () => {\n            win.webContents.send('next');\n          },\n        },\n        {\n          label: 'Previous',\n          accelerator: shortcuts.find(s => s.id === 'previous').shortcut,\n          click: () => {\n            win.webContents.send('previous');\n          },\n        },\n        {\n          label: 'Increase Volume',\n          accelerator: shortcuts.find(s => s.id === 'increaseVolume').shortcut,\n          click: () => {\n            win.webContents.send('increaseVolume');\n          },\n        },\n        {\n          label: 'Decrease Volume',\n          accelerator: shortcuts.find(s => s.id === 'decreaseVolume').shortcut,\n          click: () => {\n            win.webContents.send('decreaseVolume');\n          },\n        },\n        {\n          label: 'Like',\n          accelerator: shortcuts.find(s => s.id === 'like').shortcut,\n          click: () => {\n            win.webContents.send('like');\n          },\n        },\n        {\n          label: 'Repeat',\n          accelerator: 'Alt+R',\n          click: () => {\n            win.webContents.send('repeat');\n          },\n        },\n        {\n          label: 'Shuffle',\n          accelerator: 'Alt+S',\n          click: () => {\n            win.webContents.send('shuffle');\n          },\n        },\n      ],\n    },\n    {\n      label: 'Window',\n      submenu: [\n        { role: 'close' },\n        { role: 'minimize' },\n        { role: 'zoom' },\n        { role: 'reload' },\n        { role: 'forcereload' },\n        { role: 'toggledevtools' },\n        { type: 'separator' },\n        { role: 'togglefullscreen' },\n        ...(isMac\n          ? [\n              { type: 'separator' },\n              { role: 'front' },\n              { type: 'separator' },\n              {\n                role: 'window',\n                id: 'window',\n                label: 'YesPlayMusic',\n                type: 'checkbox',\n                checked: true,\n                click: () => {\n                  const current = menu.getMenuItemById('window');\n                  if (current.checked === false) {\n                    win.hide();\n                  } else {\n                    win.show();\n                  }\n                },\n              },\n            ]\n          : [{ role: 'close' }]),\n      ],\n    },\n    {\n      label: 'Help',\n      submenu: [\n        {\n          label: 'GitHub',\n          click: async () => {\n            const { shell } = require('electron');\n            await shell.openExternal('https://github.com/qier222/YesPlayMusic');\n          },\n        },\n        {\n          label: 'Electron',\n          click: async () => {\n            const { shell } = require('electron');\n            await shell.openExternal('https://electronjs.org');\n          },\n        },\n        {\n          label: '开发者工具',\n          accelerator: 'F12',\n          click: () => {\n            win.webContents.openDevTools();\n          },\n        },\n      ],\n    },\n  ];\n  // for window\n  // if (process.platform === \"win32\") {\n  //   template.push({\n  //     label: \"Help\",\n  //     submenu: [\n  //       {\n  //         label: `Current version v${version}`,\n  //         enabled: false,\n  //       },\n  //       {\n  //         label: \"Check for update\",\n  //         accelerator: \"Ctrl+U\",\n  //         click: (item, focusedWindow) => {\n  //           win = focusedWindow;\n  //           updateSource = \"menu\";\n  //           autoUpdater.checkForUpdates();\n  //         },\n  //       },\n  //     ],\n  //   });\n  // }\n\n  menu = Menu.buildFromTemplate(template);\n  Menu.setApplicationMenu(menu);\n}\n"
  },
  {
    "path": "src/electron/mpris.js",
    "content": "import dbus from 'dbus-next';\nimport { ipcMain, app } from 'electron';\n\nexport function createMpris(window) {\n  const Player = require('mpris-service');\n  const renderer = window.webContents;\n\n  const player = Player({\n    name: 'yesplaymusic',\n    identity: 'YesPlayMusic',\n  });\n\n  player.on('next', () => renderer.send('next'));\n  player.on('previous', () => renderer.send('previous'));\n  player.on('playpause', () => renderer.send('play'));\n  player.on('play', () => renderer.send('play'));\n  player.on('pause', () => renderer.send('play'));\n  player.on('quit', () => app.exit());\n  player.on('position', args =>\n    renderer.send('setPosition', args.position / 1000 / 1000)\n  );\n  player.on('loopStatus', () => renderer.send('repeat'));\n  player.on('shuffle', () => renderer.send('shuffle'));\n\n  ipcMain.on('player', (e, { playing }) => {\n    player.playbackStatus = playing\n      ? Player.PLAYBACK_STATUS_PLAYING\n      : Player.PLAYBACK_STATUS_PAUSED;\n  });\n\n  ipcMain.on('metadata', (e, metadata) => {\n    // 更新 Mpris 状态前将位置设为0, 否则 OSDLyrics 获取到的进度是上首音乐切换时的进度\n    player.getPosition = () => 0;\n    player.metadata = {\n      'mpris:trackid': player.objectPath('track/' + metadata.trackId),\n      'mpris:artUrl': metadata.artwork[0].src,\n      'mpris:length': metadata.length * 1000 * 1000,\n      'xesam:title': metadata.title,\n      'xesam:album': metadata.album,\n      'xesam:artist': metadata.artist.split(','),\n      'xesam:url': metadata.url,\n    };\n  });\n\n  ipcMain.on('playerCurrentTrackTime', (e, position) => {\n    player.getPosition = () => position * 1000 * 1000;\n    player.seeked(position * 1000 * 1000);\n  });\n\n  ipcMain.on('seeked', (e, position) => {\n    player.seeked(position * 1000 * 1000);\n  });\n\n  ipcMain.on('switchRepeatMode', (e, mode) => {\n    switch (mode) {\n      case 'off':\n        player.loopStatus = Player.LOOP_STATUS_NONE;\n        break;\n      case 'one':\n        player.loopStatus = Player.LOOP_STATUS_TRACK;\n        break;\n      case 'on':\n        player.loopStatus = Player.LOOP_STATUS_PLAYLIST;\n        break;\n    }\n  });\n\n  ipcMain.on('switchShuffle', (e, shuffle) => {\n    player.shuffle = shuffle;\n  });\n}\n\nexport async function createDbus(window) {\n  const bus = dbus.sessionBus();\n  const Variant = dbus.Variant;\n\n  const osdService = await bus.getProxyObject(\n    'org.osdlyrics.Daemon',\n    '/org/osdlyrics/Lyrics'\n  );\n\n  const osdInterface = osdService.getInterface('org.osdlyrics.Lyrics');\n\n  ipcMain.on('sendLyrics', async (e, { track, lyrics }) => {\n    const metadata = {\n      title: new Variant('s', track.name),\n      artist: new Variant('s', track.ar.map(ar => ar.name).join(', ')),\n    };\n\n    await osdInterface.SetLyricContent(metadata, Buffer.from(lyrics));\n\n    window.webContents.send('saveLyricFinished');\n  });\n}\n"
  },
  {
    "path": "src/electron/services.js",
    "content": "import clc from 'cli-color';\nimport checkAuthToken from '../utils/checkAuthToken';\nimport server from '@neteaseapireborn/api/server';\n\nexport async function startNeteaseMusicApi() {\n  // Let user know that the service is starting\n  console.log(`${clc.redBright('[NetEase API]')} initiating NCM API`);\n\n  // Load the NCM API.\n  await server.serveNcmApi({\n    port: 10754,\n    moduleDefs: require('../ncmModDef'),\n  });\n}\n"
  },
  {
    "path": "src/electron/touchBar.js",
    "content": "const { TouchBar, nativeImage, ipcMain } = require('electron');\nconst { TouchBarButton, TouchBarSpacer } = TouchBar;\nconst path = require('path');\n\nexport function createTouchBar(window) {\n  const renderer = window.webContents;\n\n  // Icon follow touchbar design guideline.\n  // See: https://developer.apple.com/design/human-interface-guidelines/macos/touch-bar/touch-bar-icons-and-images/\n  // Icon Resource: https://devimages-cdn.apple.com/design/resources/\n  function getNativeIcon(name) {\n    return nativeImage.createFromPath(\n      // eslint-disable-next-line no-undef\n      path.join(__static, 'img/touchbar/', name)\n    );\n  }\n\n  const previousPage = new TouchBarButton({\n    click: () => {\n      renderer.send('routerGo', 'back');\n    },\n    icon: getNativeIcon('page_prev.png'),\n  });\n\n  const nextPage = new TouchBarButton({\n    click: () => {\n      renderer.send('routerGo', 'forward');\n    },\n    icon: getNativeIcon('page_next.png'),\n  });\n\n  const searchButton = new TouchBarButton({\n    click: () => {\n      renderer.send('search');\n    },\n    icon: getNativeIcon('search.png'),\n  });\n\n  const playButton = new TouchBarButton({\n    click: () => {\n      renderer.send('play');\n    },\n    icon: getNativeIcon('play.png'),\n  });\n\n  const previousTrackButton = new TouchBarButton({\n    click: () => {\n      renderer.send('previous');\n    },\n    icon: getNativeIcon('backward.png'),\n  });\n\n  const nextTrackButton = new TouchBarButton({\n    click: () => {\n      renderer.send('next');\n    },\n    icon: getNativeIcon('forward.png'),\n  });\n\n  const likeButton = new TouchBarButton({\n    click: () => {\n      renderer.send('like');\n    },\n    icon: getNativeIcon('like.png'),\n  });\n\n  const nextUpButton = new TouchBarButton({\n    click: () => {\n      renderer.send('nextUp');\n    },\n    icon: getNativeIcon('next_up.png'),\n  });\n\n  ipcMain.on('player', (e, { playing, likedCurrentTrack }) => {\n    playButton.icon =\n      playing === true ? getNativeIcon('pause.png') : getNativeIcon('play.png');\n    likeButton.icon = likedCurrentTrack\n      ? getNativeIcon('like_fill.png')\n      : getNativeIcon('like.png');\n  });\n\n  const touchBar = new TouchBar({\n    items: [\n      previousPage,\n      nextPage,\n      searchButton,\n      new TouchBarSpacer({ size: 'flexible' }),\n      previousTrackButton,\n      playButton,\n      nextTrackButton,\n      new TouchBarSpacer({ size: 'flexible' }),\n      likeButton,\n      nextUpButton,\n    ],\n  });\n  return touchBar;\n}\n"
  },
  {
    "path": "src/electron/tray.js",
    "content": "/* global __static */\nimport path from 'path';\nimport { app, nativeImage, Tray, Menu, nativeTheme } from 'electron';\nimport { isLinux } from '@/utils/platform';\n\nfunction createMenuTemplate(win) {\n  return [\n    {\n      label: '播放',\n      icon: nativeImage.createFromPath(\n        path.join(__static, 'img/icons/play.png')\n      ),\n      click: () => {\n        win.webContents.send('play');\n      },\n      id: 'play',\n    },\n    {\n      label: '暂停',\n      icon: nativeImage.createFromPath(\n        path.join(__static, 'img/icons/pause.png')\n      ),\n      click: () => {\n        win.webContents.send('play');\n      },\n      id: 'pause',\n      visible: false,\n    },\n    {\n      label: '上一首',\n      icon: nativeImage.createFromPath(\n        path.join(__static, 'img/icons/left.png')\n      ),\n      accelerator: 'CmdOrCtrl+Left',\n      click: () => {\n        win.webContents.send('previous');\n      },\n    },\n    {\n      label: '下一首',\n      icon: nativeImage.createFromPath(\n        path.join(__static, 'img/icons/right.png')\n      ),\n      accelerator: 'CmdOrCtrl+Right',\n      click: () => {\n        win.webContents.send('next');\n      },\n    },\n    {\n      label: '循环播放',\n      icon: nativeImage.createFromPath(\n        path.join(__static, 'img/icons/repeat.png')\n      ),\n      accelerator: 'Alt+R',\n      click: () => {\n        win.webContents.send('repeat');\n      },\n    },\n    {\n      label: '加入喜欢',\n      icon: nativeImage.createFromPath(\n        path.join(__static, 'img/icons/like.png')\n      ),\n      accelerator: 'CmdOrCtrl+L',\n      click: () => {\n        win.webContents.send('like');\n      },\n      id: 'like',\n    },\n    {\n      label: '取消喜欢',\n      icon: nativeImage.createFromPath(\n        path.join(__static, 'img/icons/unlike.png')\n      ),\n      accelerator: 'CmdOrCtrl+L',\n      click: () => {\n        win.webContents.send('like');\n      },\n      id: 'unlike',\n      visible: false,\n    },\n    {\n      label: '退出',\n      icon: nativeImage.createFromPath(\n        path.join(__static, 'img/icons/exit.png')\n      ),\n      accelerator: 'CmdOrCtrl+W',\n      click: () => {\n        app.exit();\n      },\n    },\n  ];\n}\n\n// linux下托盘的实现方式比较迷惑\n// right-click无法在linux下使用\n// click在默认行为下会弹出一个contextMenu，里面的唯一选项才会调用click事件\n// setContextMenu应该是目前唯一能在linux下使用托盘菜单api\n// 但是无法区分鼠标左右键\n\n// 发现openSUSE KDE环境可以区分鼠标左右键\n// 添加左键支持\n// 2022.05.17\nclass YPMTrayLinuxImpl {\n  constructor(tray, win, emitter, store) {\n    this.tray = tray;\n    this.win = win;\n    this.emitter = emitter;\n    this.store = store;\n    this.template = undefined;\n    this.initTemplate();\n    this.contextMenu = Menu.buildFromTemplate(this.template);\n\n    this.tray.setContextMenu(this.contextMenu);\n    this.handleEvents();\n  }\n\n  initTemplate() {\n    //在linux下，鼠标左右键都会呼出contextMenu\n    //所以此处单独为linux添加一个 显示主面板 选项\n    this.template = [\n      {\n        label: '显示主面板',\n        click: () => {\n          this.win.show();\n        },\n      },\n      {\n        type: 'separator',\n      },\n    ].concat(createMenuTemplate(this.win));\n  }\n\n  handleEvents() {\n    this.tray.on('click', () => {\n      this.win.show();\n    });\n\n    this.emitter.on('updateTooltip', title => this.tray.setToolTip(title));\n    this.emitter.on('updatePlayState', isPlaying => {\n      this.contextMenu.getMenuItemById('play').visible = !isPlaying;\n      this.contextMenu.getMenuItemById('pause').visible = isPlaying;\n      this.tray.setContextMenu(this.contextMenu);\n    });\n    this.emitter.on('updateLikeState', isLiked => {\n      this.contextMenu.getMenuItemById('like').visible = !isLiked;\n      this.contextMenu.getMenuItemById('unlike').visible = isLiked;\n      this.tray.setContextMenu(this.contextMenu);\n    });\n    this.emitter.on('updateIcon', () => {\n      this.updateIcon();\n    });\n  }\n\n  updateIcon() {\n    let trayIconSetting = this.store.get('settings.trayIconTheme') || 'auto';\n    let iconTheme;\n    if (trayIconSetting === 'auto') {\n      iconTheme = nativeTheme.shouldUseDarkColors ? 'light' : 'dark';\n    } else {\n      iconTheme = trayIconSetting;\n    }\n\n    let icon = nativeImage\n      .createFromPath(path.join(__static, `img/icons/menu-${iconTheme}@88.png`))\n      .resize({\n        height: 20,\n        width: 20,\n      });\n\n    this.tray.setImage(icon);\n  }\n}\n\nclass YPMTrayWindowsImpl {\n  constructor(tray, win, emitter, store) {\n    this.tray = tray;\n    this.win = win;\n    this.emitter = emitter;\n    this.store = store;\n    this.template = createMenuTemplate(win);\n    this.contextMenu = Menu.buildFromTemplate(this.template);\n\n    this.isPlaying = false;\n    this.curDisplayPlaying = false;\n\n    this.isLiked = false;\n    this.curDisplayLiked = false;\n\n    this.handleEvents();\n  }\n\n  handleEvents() {\n    this.tray.on('click', () => {\n      this.win.show();\n    });\n\n    this.tray.on('right-click', () => {\n      if (this.isPlaying !== this.curDisplayPlaying) {\n        this.curDisplayPlaying = this.isPlaying;\n        this.contextMenu.getMenuItemById('play').visible = !this.isPlaying;\n        this.contextMenu.getMenuItemById('pause').visible = this.isPlaying;\n      }\n\n      if (this.isLiked !== this.curDisplayLiked) {\n        this.curDisplayLiked = this.isLiked;\n        this.contextMenu.getMenuItemById('like').visible = !this.isLiked;\n        this.contextMenu.getMenuItemById('unlike').visible = this.isLiked;\n      }\n\n      this.tray.popUpContextMenu(this.contextMenu);\n    });\n\n    this.emitter.on('updateTooltip', title => this.tray.setToolTip(title));\n    this.emitter.on(\n      'updatePlayState',\n      isPlaying => (this.isPlaying = isPlaying)\n    );\n    this.emitter.on('updateLikeState', isLiked => (this.isLiked = isLiked));\n    this.emitter.on('updateIcon', () => {\n      this.updateIcon();\n    });\n  }\n\n  updateIcon() {\n    let trayIconSetting = this.store.get('settings.trayIconTheme') || 'auto';\n    let iconTheme;\n    if (trayIconSetting === 'auto') {\n      iconTheme = nativeTheme.shouldUseDarkColors ? 'light' : 'dark';\n    } else {\n      iconTheme = trayIconSetting;\n    }\n\n    let icon = nativeImage\n      .createFromPath(path.join(__static, `img/icons/menu-${iconTheme}@88.png`))\n      .resize({\n        height: 20,\n        width: 20,\n      });\n\n    this.tray.setImage(icon);\n  }\n}\n\nexport function createTray(win, eventEmitter, store) {\n  let trayIconSetting = store.get('settings.trayIconTheme') || 'auto';\n  let iconTheme;\n  if (trayIconSetting === 'auto') {\n    iconTheme = nativeTheme.shouldUseDarkColors ? 'light' : 'dark';\n  } else {\n    iconTheme = trayIconSetting;\n  }\n\n  let icon = nativeImage\n    .createFromPath(path.join(__static, `img/icons/menu-${iconTheme}@88.png`))\n    .resize({\n      height: 20,\n      width: 20,\n    });\n\n  let tray = new Tray(icon);\n  tray.setToolTip('YesPlayMusic');\n\n  return isLinux\n    ? new YPMTrayLinuxImpl(tray, win, eventEmitter, store)\n    : new YPMTrayWindowsImpl(tray, win, eventEmitter, store);\n}\n"
  },
  {
    "path": "src/locale/index.js",
    "content": "import Vue from 'vue';\nimport VueClipboard from 'vue-clipboard2';\nimport VueI18n from 'vue-i18n';\nimport store from '@/store';\n\nimport en from './lang/en.js';\nimport zhCN from './lang/zh-CN.js';\nimport zhTW from './lang/zh-TW.js';\nimport tr from './lang/tr.js';\n\nVue.use(VueClipboard);\nVue.use(VueI18n);\n\nconst i18n = new VueI18n({\n  locale: store.state.settings.lang,\n  messages: {\n    en,\n    'zh-CN': zhCN,\n    'zh-TW': zhTW,\n    tr,\n  },\n  silentTranslationWarn: true,\n});\n\nexport default i18n;\n"
  },
  {
    "path": "src/locale/lang/en.js",
    "content": "export default {\n  common: {\n    play: 'PLAY',\n    songs: 'Songs',\n  },\n  nav: {\n    home: 'Home',\n    explore: 'Explore',\n    library: 'Library',\n    search: 'Search',\n    github: 'GitHub Repo',\n  },\n  footer: {\n    settings: 'Settings',\n  },\n  home: {\n    recommendPlaylist: 'Recommended Playlists',\n    recommendArtist: 'Recommended Artists',\n    newAlbum: 'Latest Albums',\n    seeMore: 'SEE MORE',\n    charts: 'Charts',\n  },\n  library: {\n    sLibrary: \"'s Library\",\n    likedSongs: 'Liked Songs',\n    sLikedSongs: \"'s Liked Songs\",\n    playlists: 'Playlists',\n    albums: 'Albums',\n    artists: 'Artists',\n    mvs: 'MVs',\n    cloudDisk: 'Cloud Disk',\n    newPlayList: 'New Playlist',\n    uploadSongs: 'Upload Songs',\n    playHistory: {\n      title: 'Play History',\n      week: 'Latest Week',\n      all: 'All Time',\n    },\n    userProfileMenu: {\n      settings: 'Settings',\n      logout: 'Logout',\n    },\n  },\n  explore: {\n    explore: 'Explore',\n    loadMore: 'Load More',\n  },\n  artist: {\n    latestRelease: 'Latest Releases',\n    latestMV: 'Latest MV',\n    popularSongs: 'Popular Songs',\n    showMore: 'SHOW MORE',\n    showLess: 'SHOW LESS',\n    EPsSingles: 'EPs & Singles',\n    albums: 'Albums',\n    withAlbums: 'Albums',\n    artist: 'Artist',\n    videos: 'Music Videos',\n    following: 'Following',\n    follow: 'Follow',\n    similarArtists: 'Similar Artists',\n    artistDesc: 'Artist Description',\n  },\n  album: {\n    released: 'Released',\n    albumDesc: 'Album Description',\n  },\n  playlist: {\n    playlist: 'Playlists',\n    updatedAt: 'Updated at',\n    search: 'Search in playlist',\n  },\n  login: {\n    accessToAll: 'Access to all data',\n    loginText: 'Login to Netease',\n    search: 'Search account',\n    readonly: 'Only access to public data',\n    usernameLogin: 'Username Login',\n    searchHolder: 'Your account username',\n    enterTip: \"Press 'enter' to search\",\n    choose: 'Choose your account',\n    confirm: 'Confirm',\n    countryCode: 'Country code',\n    phone: 'Phone',\n    email: 'Email address',\n    password: 'Password',\n    login: 'Login',\n    loginWithEmail: 'Login with Email',\n    loginWithPhone: 'Login with Phone',\n    notice: `YesPlayMusic promises not to save any of your account information to the cloud.<br />\n      Your password will be MD5 encrypted locally and then transmitted to NetEase Music API.<br />\n      YesPlayMusic is not the official website of NetEase Music, please consider carefully before entering account information. You can also go to <a href=\"https://github.com/qier222/YesPlayMusic\">YesPlayMusic's GitHub repository</a> to build and use the self-hosted NetEase Music API.`,\n    noticeElectron: `Your password will be MD5 encrypted locally and then transmitted to NetEase Music API.<br />\n      YesPlayMusic promises not to save any of your account information to the cloud.<br />`,\n  },\n  mv: {\n    moreVideo: 'More Videos',\n  },\n  next: {\n    nowPlaying: 'Now Playing',\n    nextUp: 'Next Up',\n  },\n  player: {\n    like: 'Like',\n    unlike: 'Unlike',\n    previous: 'Previous Song',\n    next: 'Next Song',\n    repeat: 'Repeat',\n    repeatTrack: 'Repeat Track',\n    shuffle: 'Shuffle',\n    reversed: 'Reversed',\n    play: 'Play',\n    pause: 'Pause',\n    mute: 'Mute',\n    nextUp: 'Next Up',\n    translationLyric: 'lyric (trans)',\n    PronunciationLyric: 'lyric (pronounce)',\n  },\n  modal: {\n    close: 'Close',\n  },\n  search: {\n    artist: 'Artists',\n    album: 'Albums',\n    song: 'Songs',\n    mv: 'Music Videos',\n    playlist: 'Playlists',\n    noResult: 'No Results',\n    searchFor: 'Search for',\n  },\n  settings: {\n    settings: 'Settings',\n    logout: 'LOGOUT',\n    language: 'Languages',\n    lyric: 'Lyric',\n    others: 'Others',\n    customization: 'Customization',\n    MusicGenrePreference: {\n      text: 'Music Language Preference',\n      none: 'No preferences',\n      mandarin: 'Mandarin',\n      western: 'Europe & America',\n      korean: 'Korean',\n      japanese: 'Japanese',\n    },\n    musicQuality: {\n      text: 'Music Quality',\n      low: 'Low',\n      medium: 'Medium',\n      high: 'High',\n      lossless: 'Lossless',\n    },\n    cacheLimit: {\n      text: 'Songs Cache limit',\n      none: 'None',\n    },\n    lyricFontSize: {\n      text: 'Lyric Font Size',\n      small: 'Small',\n      medium: 'Medium',\n      large: 'Large (Default)',\n      xlarge: 'X-Large',\n    },\n    deviceSelector: 'Audio Output Device',\n    permissionRequired: 'Microphone Permission Required',\n    appearance: {\n      text: 'Appearance',\n      auto: 'Auto',\n      light: 'Light',\n      dark: 'Dark',\n    },\n    trayIcon: {\n      text: 'Tray Icon Color',\n      auto: 'Auto',\n      light: 'Light',\n      dark: 'Dark',\n    },\n    automaticallyCacheSongs: 'Automatically cache songs',\n    clearSongsCache: 'Clear Songs Cache',\n    cacheCount: 'Cached {song} songs ({size})',\n    showLyricsTranslation: 'Show lyrics translation',\n    showPlaylistsByAppleMusic: 'Show playlists by Apple Music',\n    enableDiscordRichPresence: 'Enable Discord Rich Presence',\n    enableGlobalShortcut: 'Enable Global Shortcut',\n    showLibraryDefault: 'Show Library after App Launched',\n    subTitleDefault: 'Show Alias for Subtitle by default',\n    enableReversedMode: 'Enable Reversed Mode (Experimental)',\n    enableCustomTitlebar: 'Enable custom title bar (Need restart)',\n    showLyricsTime: 'Display current time',\n    lyricsBackground: {\n      text: 'Show Lyrics Background',\n      off: 'Off',\n      on: 'On',\n      dynamic: 'Dynamic (High GPU usage)',\n    },\n    closeAppOption: {\n      text: 'Close App...',\n      ask: 'Ask',\n      exit: 'Exit',\n      minimizeToTray: 'Minimize to tray',\n    },\n    enableOsdlyricsSupport: {\n      title: 'desktop lyrics support',\n      desc1:\n        'Only takes effect under Linux. After enabled, it downloads the lyrics file to the local, and tries to launch OSDLyrics at startup.',\n      desc2:\n        'Please ensure that you have installed OSDLyrics before turning on this.',\n    },\n    unm: {\n      enable: 'Enable',\n      audioSource: {\n        title: 'Audio Sources',\n      },\n      enableFlac: {\n        title: 'Enable FLAC Sources',\n        desc: 'To take effect, it may be required to clear the cache after enabling this function.',\n      },\n      searchMode: {\n        title: 'Audio Search Mode',\n        fast: 'Speed Priority',\n        order: 'Order Priority',\n      },\n      cookie: {\n        joox: 'Cookie for Joox use',\n        qq: 'Cookie for QQ use',\n        desc1: 'Click here for the configuration instruction. ',\n        desc2: 'Leave empty to pick up the default value',\n      },\n      ytdl: 'The youtube-dl Executable File for YtDl',\n      proxy: {\n        title: 'Proxy Server for UNM',\n        desc1:\n          'The proxy server to use for requesting services such as YouTube',\n        desc2: 'Leave empty to pick up the default value',\n      },\n    },\n  },\n  contextMenu: {\n    play: 'Play',\n    addToQueue: 'Add to queue',\n    saveToMyLikedSongs: 'Save to my Liked Songs',\n    removeFromMyLikedSongs: 'Remove from my Liked Songs',\n    saveToLibrary: 'Save to library',\n    removeFromLibrary: 'Remove from library',\n    addToPlaylist: 'Add to playlist',\n    searchInPlaylist: 'Search in playlist',\n    copyUrl: 'Copy URL',\n    openInBrowser: 'Open in Browser',\n    allPlaylists: 'All Playlists',\n    minePlaylists: 'My Playlists',\n    likedPlaylists: 'Liked Playlists',\n    cardiacMode: 'Cardiac Mode',\n    copyLyric: 'Copy Lyric',\n    copyLyricWithTranslation: 'Copy Lyric With Translation',\n  },\n  toast: {\n    savedToPlaylist: 'Saved to playlist',\n    removedFromPlaylist: 'Removed from playlist',\n    savedToMyLikedSongs: 'Saved to my Liked Songs',\n    removedFromMyLikedSongs: 'Removed from my Liked Songs',\n    copied: 'Copied',\n    copyFailed: 'Copy failed: ',\n    needToLogin: 'Need to log into netease account',\n  },\n};\n"
  },
  {
    "path": "src/locale/lang/tr.js",
    "content": "export default {\n  common: {\n    play: 'OYNAT',\n    songs: 'Müzikler',\n  },\n  nav: {\n    home: 'Anasayfa',\n    explore: 'Keşfet',\n    library: 'Kitaplık',\n    search: 'Ara',\n    github: 'GitHub Repo',\n  },\n  footer: {\n    settings: 'Ayarlar',\n  },\n  home: {\n    recommendPlaylist: 'Önerilen Çalma Listeier',\n    recommendArtist: 'Önerilen Sanatçılar',\n    newAlbum: 'Son Çıkan Albümler',\n    seeMore: 'DAHA FAZLASI',\n    charts: 'Listeler',\n  },\n  library: {\n    sLibrary: \"'in Kütüphanesi\",\n    likedSongs: 'Beğenilen Müzikler',\n    sLikedSongs: \"'in Beğendiği Müzikler\",\n    playlists: 'Çalma Listeleri',\n    albums: 'Albümler',\n    artists: 'Sanatçılar',\n    mvs: 'MVs',\n    cloudDisk: 'Cloud Disk',\n    newPlayList: 'Yeni Çalma Listesi',\n    uploadSongs: 'Upload Songs',\n    playHistory: {\n      title: 'Play History',\n      week: 'Latest Week',\n      all: 'All Time',\n    },\n    userProfileMenu: {\n      settings: 'Ayarlar',\n      logout: 'Çıkış Yap',\n    },\n  },\n  explore: {\n    explore: 'Keşfet',\n    loadMore: 'Daha Fazlası',\n  },\n  artist: {\n    latestRelease: 'Son Çıkanlar',\n    popularSongs: 'Popüler Müzikler',\n    showMore: 'Daha Fazlası',\n    showLess: 'Daha Azı',\n    EPsSingles: 'EPs & Singles',\n    albums: 'Albümler',\n    withAlbums: 'Albümler',\n    artist: 'Sanatçı',\n    videos: 'Müzik Videoları',\n    following: 'Takip Ediyor',\n    follow: 'Takip Et',\n  },\n  album: {\n    released: 'Yayınlandı',\n  },\n  playlist: {\n    playlist: 'Çalma Listeleri',\n    updatedAt: 'Tarihinde Güncellendş',\n    search: 'Çalma Listesinde Ara',\n  },\n  login: {\n    accessToAll: 'Tüm verilere eriş',\n    loginText: \"Netease'e giriş yap\",\n    search: 'Hesap ara',\n    readonly: 'Sadece halka açık verilere erişir',\n    usernameLogin: 'Kullanıcı adı giriş',\n    searchHolder: 'Hesabının kullanıcı adı',\n    enterTip: \"Aramak için 'enter'e basınız\",\n    choose: 'Hesabını seç',\n    confirm: 'Onayla',\n    countryCode: 'Ülke kodu',\n    phone: 'Telefon',\n    email: 'Email adresi',\n    password: 'Şifre',\n    login: 'Giriş Yap',\n    loginWithEmail: 'Email ile giriş yap',\n    loginWithPhone: 'Phone ile giriş yap',\n    notice: `YesPlayMusic hesabınızın hiçbir bilgisini kaydetmeyeceğine dair söz veriyor<br />\n      Şifren MD5 şifreleme ile yerel olarak şifrelenir ve daha sonra NetEase Müzik API'sine gönderilir<br />\n      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 <a href=\"https://github.com/qier222/YesPlayMusic\">YesPlayMusic'in GitHub Repo'suna</a> gidebilirsiniz.`,\n    noticeElectron: `YesPlayMusic hesabınızın hiçbir bilgisini kaydetmeyeceğine dair söz veriyor<br />\n      Şifren MD5 şifreleme ile yerel olarak şifrelenir ve daha sonra NetEase Müzik API'sine gönderilir<br />`,\n  },\n  mv: {\n    moreVideo: 'Daha Fazla Video',\n  },\n  next: {\n    nowPlaying: 'Şuan çalıyor',\n    nextUp: 'Sıradaki',\n  },\n  player: {\n    like: 'Beğen',\n    unlike: 'Aksine',\n    previous: 'Önceki Müzik',\n    next: 'Sonraki Müzik',\n    repeat: 'Tekrarla',\n    repeatTrack: 'Parçayı Tekrarla',\n    shuffle: 'Karıştır',\n    play: 'Oynat',\n    pause: 'Durdur',\n    mute: 'Sesi kapat',\n    nextUp: 'Sıradaki',\n    translationLyric: 'şarkı sözleri (çeviri)',\n    PronunciationLyric: 'şarkı sözleri (çeviri)',\n  },\n  modal: {\n    close: 'Kapat',\n  },\n  search: {\n    artist: 'Sanatçılar',\n    album: 'Albümler',\n    song: 'Müzikler',\n    mv: 'Müzik Videoları',\n    playlist: 'Çalma Listeleri',\n    noResult: 'Sonuç Bulunamadı',\n    searchFor: 'Search for',\n  },\n  settings: {\n    settings: 'Ayarlar',\n    logout: 'ÇIKIŞ YAP',\n    language: 'Diller',\n    lyric: 'Şarkı Sözleri',\n    others: 'Diğerleri',\n    customization: 'Özelleştirme',\n    MusicGenrePreference: {\n      text: 'Müzik Dili Tercihi',\n      none: 'Tercih yok',\n      mandarin: 'Çince dili',\n      western: 'Avrupa ve Amerika',\n      korean: 'Korece',\n      japanese: 'Japonca',\n    },\n    musicQuality: {\n      text: 'Müzik Kalitesi',\n      low: 'Düşük',\n      medium: 'Orta',\n      high: 'Yüksek',\n      lossless: 'Kaliteli',\n    },\n    cacheLimit: {\n      text: 'Şarkılar Önbellek sınırı',\n      none: 'Yok',\n    },\n    lyricFontSize: {\n      text: 'Şarkı Sözleri Yazı Boyutu',\n      small: 'Küçük',\n      medium: 'Orta',\n      large: 'Büyük(Varsayılan)',\n      xlarge: 'Çok-Büyük',\n    },\n    deviceSelector: 'Ses Çıkış Cihazı',\n    permissionRequired: 'Mikrofon izni gerekiyor',\n    appearance: {\n      text: 'Görünüş',\n      auto: 'Otomatik',\n      light: 'Aydınlık',\n      dark: 'Karanlık',\n    },\n    trayIcon: {\n      text: 'Tepsi Simgesi Rengi',\n      auto: 'Otomatik',\n      light: 'Aydınlık',\n      dark: 'Karanlık',\n    },\n    automaticallyCacheSongs: 'Müzikleri otomatik çerezle',\n    clearSongsCache: 'Müzik çerezlerini temizle',\n    cacheCount: 'Çerezlenen {song} Müzikler ({size})',\n    showLyricsTranslation: 'Müzik sözlerinin çevirilerini göster',\n    showPlaylistsByAppleMusic: \"Apple Music'in Çalma Listelerini Göster\",\n    enableDiscordRichPresence: 'Discord gösterimini aktifleştir',\n    showLibraryDefault: 'Kitaplık Varsayılanını göster',\n    subTitleDefault: 'Show Alias for Subtitle by default',\n    enableReversedMode: 'Enable Reversed Mode (Experimental)',\n    enableCustomTitlebar: 'Enable custom title bar (Need restart)',\n    lyricsBackground: {\n      text: 'Şarkı Sözleri Arka Planını Göster',\n      off: 'kapalı',\n      on: 'açık',\n      dynamic: 'dinamik(Yüksek GPU kullanımı)',\n    },\n    closeAppOption: {\n      text: 'Close App...',\n      ask: 'Ask',\n      exit: 'Exit',\n      minimizeToTray: 'Küçült',\n    },\n    unm: {\n      enable: 'Enable',\n      audioSource: {\n        title: 'Audio Sources',\n      },\n      enableFlac: {\n        title: 'Enable FLAC Sources',\n        desc: 'To take effect, it may be required to clear the cache after enabling this function.',\n      },\n      searchMode: {\n        title: 'Audio Search Mode',\n        fast: 'Speed Priority',\n        order: 'Order Priority',\n      },\n      cookie: {\n        joox: 'Cookie for Joox use',\n        qq: 'Cookie for QQ use',\n        desc1: 'Click here for the configuration instruction. ',\n        desc2: 'Leave empty to pick up the default value',\n      },\n      ytdl: 'The youtube-dl Executable File for YtDl',\n      proxy: {\n        title: 'Proxy Server for UNM',\n        desc1:\n          'The proxy server to use for requesting services such as YouTube',\n        desc2: 'Leave empty to pick up the default value',\n      },\n    },\n  },\n  contextMenu: {\n    play: 'Oynat',\n    addToQueue: 'Sonrakini Oynat',\n    saveToMyLikedSongs: 'Beğendiğim Müziklere Kaydet',\n    removeFromMyLikedMüzikler: 'Beğendiğim Müziklerden Kaldır',\n    saveToLibrary: 'Save to library',\n    removeFromLibrary: 'Remove from library',\n    addToPlaylist: 'Add to playlist',\n    searchInPlaylist: 'Search in playlist',\n    copyUrl: 'Copy URL',\n    openInBrowser: 'Open in Browser',\n    allPlaylists: 'All Playlists',\n    minePlaylists: 'My Playlists',\n    likedPlaylists: 'Liked Playlists',\n    cardiacMode: 'Cardiac Mode',\n    copyLyric: 'Copy Lyric',\n    copyLyricWithTranslation: 'Copy Lyric With Translation',\n  },\n  toast: {\n    savedToMyLikedSongs: 'Beğendiğim Müziklere Kaydet',\n    removedFromMyLikedSongs: 'Beğendiğim Müziklerden Kaldır',\n  },\n};\n"
  },
  {
    "path": "src/locale/lang/zh-CN.js",
    "content": "export default {\n  common: {\n    play: '播放',\n    songs: '首歌',\n  },\n  nav: {\n    home: '首页',\n    explore: '发现',\n    library: '音乐库',\n    search: '搜索',\n    github: 'GitHub 仓库',\n  },\n  home: {\n    recommendPlaylist: '推荐歌单',\n    recommendArtist: '推荐艺人',\n    newAlbum: '新专速递',\n    seeMore: '查看全部',\n    charts: '排行榜',\n  },\n  library: {\n    sLibrary: '的音乐库',\n    likedSongs: '我喜欢的音乐',\n    sLikedSongs: '喜欢的音乐',\n    playlists: '歌单',\n    albums: '专辑',\n    artists: '艺人',\n    mvs: 'MV',\n    cloudDisk: '云盘',\n    newPlayList: '新建歌单',\n    uploadSongs: '上传歌曲',\n    playHistory: {\n      title: '听歌排行',\n      week: '最近一周',\n      all: '所有时间',\n    },\n    userProfileMenu: {\n      settings: '设置',\n      logout: '登出',\n    },\n  },\n  explore: {\n    explore: '发现',\n    loadMore: '加载更多',\n  },\n  artist: {\n    latestRelease: '最新发布',\n    latestMV: '最新 MV',\n    popularSongs: '热门歌曲',\n    showMore: '显示更多',\n    showLess: '收起',\n    EPsSingles: 'EP 和单曲',\n    albums: '专辑',\n    withAlbums: '张专辑',\n    artist: '艺人',\n    videos: '个 MV',\n    following: '正在关注',\n    follow: '关注',\n    similarArtists: '相似艺人',\n    artistDesc: '艺术家介绍',\n  },\n  album: {\n    released: '发行于',\n    albumDesc: '专辑介绍',\n  },\n  playlist: {\n    playlist: '歌单',\n    updatedAt: '最后更新于',\n    search: '搜索歌单音乐',\n  },\n  login: {\n    accessToAll: '可访问全部数据',\n    loginText: '登录网易云账号',\n    search: '搜索网易云账号',\n    readonly: '只能读取账号公开数据',\n    usernameLogin: '用户名登录',\n    searchHolder: '请输入你的网易云用户名',\n    enterTip: '按 Enter 搜索',\n    choose: '在列表中选中你的账号',\n    confirm: '确认',\n    countryCode: '国际区号',\n    phone: '手机号',\n    email: '邮箱',\n    password: '密码',\n    login: '登录',\n    loginWithEmail: '邮箱登录',\n    loginWithPhone: '手机号登录',\n    notice: `YesPlayMusic 承诺不会保存你的任何账号信息到云端。<br />\n      你的密码会在本地进行 MD5 加密后再传输到网易云 API。<br />\n      YesPlayMusic 并非网易云官方网站，输入账号信息前请慎重考虑。 你也可以前往\n      <a href=\"https://github.com/qier222/YesPlayMusic\"\n        >YesPlayMusic 的 GitHub 源代码仓库</a\n      >\n      自行构建并使用自托管的网易云 API。`,\n    noticeElectron: `你的密码会在本地进行 MD5 加密后再传输到网易云 API。<br />\n      YesPlayMusic 不会传输你的账号数据到任何非网易云音乐官方的服务器。<br />`,\n  },\n  mv: {\n    moreVideo: '更多视频',\n  },\n  next: {\n    nowPlaying: '正在播放',\n    nextUp: '即将播放',\n  },\n  player: {\n    like: '喜欢',\n    unlike: '取消喜欢',\n    previous: '上一首',\n    next: '下一首',\n    repeat: '循环播放',\n    repeatTrack: '单曲循环',\n    shuffle: '随机播放',\n    reversed: '倒序播放',\n    play: '播放',\n    pause: '暂停',\n    mute: '静音',\n    nextUp: '播放列表',\n    translationLyric: '歌词(译)',\n    PronunciationLyric: '歌词(音)',\n  },\n  modal: {\n    close: '关闭',\n  },\n  search: {\n    artist: '艺人',\n    album: '专辑',\n    song: '歌曲',\n    mv: '视频',\n    playlist: '歌单',\n    noResult: '暂无结果',\n    searchFor: '搜索',\n  },\n  settings: {\n    settings: '设置',\n    logout: '登出',\n    language: '语言',\n    lyric: '歌词',\n    others: '其他',\n    customization: '自定义',\n    MusicGenrePreference: {\n      text: '音乐语种偏好',\n      none: '无偏好',\n      mandarin: '华语',\n      western: '欧美',\n      korean: '韩语',\n      japanese: '日语',\n    },\n    musicQuality: {\n      text: '音质选择',\n      low: '普通',\n      medium: '较高',\n      high: '极高',\n      lossless: '无损',\n    },\n    cacheLimit: {\n      text: '歌曲缓存上限',\n      none: '无限制',\n    },\n    lyricFontSize: {\n      text: '歌词字体大小',\n      small: '小',\n      medium: '中',\n      large: '大（默认）',\n      xlarge: '超大',\n    },\n    deviceSelector: '音频输出设备',\n    permissionRequired: '需要麦克风权限',\n    appearance: {\n      text: '外观',\n      auto: '自动',\n      light: '浅色',\n      dark: '深色',\n    },\n    trayIcon: {\n      text: '托盘图标颜色',\n      auto: '自动',\n      light: '浅色',\n      dark: '深色',\n    },\n    automaticallyCacheSongs: '自动缓存歌曲',\n    clearSongsCache: '清除歌曲缓存',\n    cacheCount: '已缓存 {song} 首 ({size})',\n    showLyricsTranslation: '显示歌词翻译',\n    showPlaylistsByAppleMusic: '首页显示来自 Apple Music 的歌单',\n    enableDiscordRichPresence: '启用 Discord Rich Presence',\n    enableGlobalShortcut: '启用全局快捷键',\n    showLibraryDefault: '启动后显示音乐库',\n    subTitleDefault: '副标题使用别名',\n    enableReversedMode: '启用倒序播放功能 (实验性功能)',\n    enableCustomTitlebar: '启用自定义标题栏 (重启后生效)',\n    lyricsBackground: {\n      text: '显示歌词背景',\n      off: '关闭',\n      on: '打开',\n      dynamic: '动态（GPU 占用较高）',\n    },\n    showLyricsTime: '显示当前时间',\n    closeAppOption: {\n      text: '关闭主面板时...',\n      ask: '询问',\n      exit: '退出',\n      minimizeToTray: '最小化到托盘',\n    },\n    enableOsdlyricsSupport: {\n      title: '桌面歌词支持',\n      desc1:\n        '仅 Linux 下生效。启用后会将歌词文件下载到本地，并在开启播放器时尝试拉起 OSDLyrics。',\n      desc2: '请在开启之前确保您已经正确安装了 OSDLyrics。',\n    },\n    unm: {\n      enable: '启用',\n      audioSource: {\n        title: '备选音源',\n      },\n      enableFlac: {\n        title: '启用 FLAC',\n        desc: '启用后需要清除歌曲缓存才能生效',\n      },\n      searchMode: {\n        title: '音源搜索模式',\n        fast: '速度优先',\n        order: '顺序优先',\n      },\n      cookie: {\n        joox: 'Joox 引擎的 Cookie',\n        qq: 'QQ 引擎的 Cookie',\n        desc1: '设置说明请参见此处',\n        desc2: '，留空则不进行相关设置',\n      },\n      ytdl: 'YtDl 引擎要使用的 youtube-dl 可执行文件',\n      proxy: {\n        title: '用于 UNM 的代理服务器',\n        desc1: '请求如 YouTube 音源服务时要使用的代理服务器',\n        desc2: '留空则不进行相关设置',\n      },\n    },\n  },\n  contextMenu: {\n    play: '播放',\n    addToQueue: '添加到队列',\n    saveToMyLikedSongs: '添加到我喜欢的音乐',\n    removeFromMyLikedSongs: '从喜欢的音乐中删除',\n    saveToLibrary: '保存到音乐库',\n    removeFromLibrary: '从音乐库删除',\n    addToPlaylist: '添加到歌单',\n    searchInPlaylist: '歌单内搜索',\n    copyUrl: '复制链接',\n    openInBrowser: '在浏览器中打开',\n    allPlaylists: '全部歌单',\n    minePlaylists: '创建的歌单',\n    likedPlaylists: '收藏的歌单',\n    cardiacMode: '心动模式',\n    copyLyric: '复制歌词',\n    copyLyricWithTranslation: '复制歌词（含翻译）',\n  },\n  toast: {\n    savedToPlaylist: '已添加到歌单',\n    removedFromPlaylist: '已从歌单中删除',\n    savedToMyLikedSongs: '已添加到我喜欢的音乐',\n    removedFromMyLikedSongs: '已从喜欢的音乐中删除',\n    copied: '已复制',\n    copyFailed: '复制失败：',\n    needToLogin: '此操作需要登录网易云帐号',\n  },\n};\n"
  },
  {
    "path": "src/locale/lang/zh-TW.js",
    "content": "export default {\n  common: {\n    play: '播放',\n    songs: '首歌',\n  },\n  nav: {\n    home: '首頁',\n    explore: '發現',\n    library: '音樂庫',\n    search: '搜尋',\n    github: 'GitHub Repo',\n  },\n  home: {\n    recommendPlaylist: '推薦歌單',\n    recommendArtist: '推薦藝人',\n    newAlbum: '新曲上架',\n    seeMore: '查看全部',\n    charts: '排行榜',\n  },\n  library: {\n    sLibrary: '的音樂庫',\n    likedSongs: '我喜歡的音樂',\n    sLikedSongs: '喜歡的音樂',\n    playlists: '歌單',\n    albums: '專輯',\n    artists: '藝人',\n    mvs: 'MV',\n    cloudDisk: '雲端硬碟',\n    newPlayList: '新增歌單',\n    uploadSongs: '上傳音樂',\n    playHistory: {\n      title: '聽歌排行',\n      week: '最近一周',\n      all: '所有時間',\n    },\n    userProfileMenu: {\n      settings: '設定',\n      logout: '登出',\n    },\n  },\n  explore: {\n    explore: '探索',\n    loadMore: '載入更多',\n  },\n  artist: {\n    latestRelease: '最新發佈',\n    popularSongs: '熱門歌曲',\n    showMore: '顯示更多',\n    showLess: '收起',\n    EPsSingles: 'EP 和單曲',\n    albums: '專輯',\n    withAlbums: '張專輯',\n    artist: '藝人',\n    videos: '個 MV',\n    following: '正在追蹤',\n    follow: '追蹤',\n  },\n  album: {\n    released: '發行於',\n  },\n  playlist: {\n    playlist: '歌單',\n    updatedAt: '最後更新於',\n    search: '搜尋歌單內音樂',\n  },\n  login: {\n    accessToAll: '可存取全部資料',\n    loginText: '登入網易雲帳戶',\n    search: '搜尋網易雲帳戶',\n    readonly: '只能讀取帳戶公開資料',\n    usernameLogin: '使用者名稱登入',\n    searchHolder: '請輸入您的網易雲使用者名稱',\n    enterTip: '按 Enter 搜尋',\n    choose: '在選單中選擇你的帳戶',\n    confirm: '確認',\n    countryCode: '國際區碼',\n    phone: '手機號碼',\n    email: 'Email',\n    password: '密碼',\n    login: '登入',\n    loginWithEmail: '信箱登入',\n    loginWithPhone: '手機號碼登入',\n    notice: `YesPlayMusic 承諾不會保存您的任何帳戶資訊到雲端。<br />\n        您的密碼會在本地進行 MD5 加密後再傳輸到網易雲 API。<br />\n        YesPlayMusic 並非網易雲官方網站，輸入帳戶資訊前請慎重考慮。 您也可以前往\n        <a href=\"https://github.com/qier222/YesPlayMusic\"\n          >YesPlayMusic 的 GitHub 原始碼 Repo</a\n        >\n        自行編譯並使用自託管的網易雲 API。`,\n    noticeElectron: `您的密碼會在本地進行 MD5 加密後再傳輸到網易雲 API。<br />\n        YesPlayMusic 不會傳輸你的帳戶資料到任何非網易雲音樂官方的伺服器。<br />`,\n  },\n  mv: {\n    moreVideo: '更多影片',\n  },\n  next: {\n    nowPlaying: '正在播放',\n    nextUp: '即將播放',\n  },\n  player: {\n    like: '喜歡',\n    unlike: '取消喜歡',\n    previous: '上一首',\n    next: '下一首',\n    repeat: '循環播放',\n    repeatTrack: '單曲循環',\n    shuffle: '隨機播放',\n    reversed: '倒序播放',\n    play: '播放',\n    pause: '暫停',\n    mute: '靜音',\n    nextUp: '播放清單',\n    translationLyric: '歌詞(譯)',\n    PronunciationLyric: '歌詞(音)',\n  },\n  modal: {\n    close: '關閉',\n  },\n  search: {\n    artist: '藝人',\n    album: '專輯',\n    song: '歌曲',\n    mv: '影片',\n    playlist: '歌單',\n    noResult: '暫無結果',\n    searchFor: '搜尋',\n  },\n  settings: {\n    settings: '設定',\n    logout: '登出',\n    language: '語言',\n    lyric: '歌詞',\n    others: '其他',\n    customization: '自訂',\n    MusicGenrePreference: {\n      text: '音樂語種偏好',\n      none: '無偏好',\n      mandarin: '華語',\n      western: '歐美',\n      korean: '韓語',\n      japanese: '日語',\n    },\n    musicQuality: {\n      text: '音質選擇',\n      low: '普通',\n      medium: '較高',\n      high: '極高',\n      lossless: '無損',\n    },\n    cacheLimit: {\n      text: '歌曲快取上限',\n      none: '無限制',\n    },\n    lyricFontSize: {\n      text: '歌詞字體大小',\n      small: '小',\n      medium: '中',\n      large: '大（預設）',\n      xlarge: '超大',\n    },\n    deviceSelector: '音訊輸出裝置',\n    permissionRequired: '需要麥克風權限',\n    appearance: {\n      text: '外觀',\n      auto: '自動',\n      light: '淺色',\n      dark: '深色',\n    },\n    trayIcon: {\n      text: '工作列圖示顏色',\n      auto: '自動',\n      light: '淺色',\n      dark: '深色',\n    },\n    automaticallyCacheSongs: '自動快取歌曲',\n    clearSongsCache: '清除歌曲快取',\n    cacheCount: '已快取 {song} 首 ({size})',\n    showLyricsTranslation: '顯示歌詞翻譯',\n    minimizeToTray: '最小化到工作列角落',\n    showPlaylistsByAppleMusic: '首頁顯示來自 Apple Music 的歌單',\n    enableDiscordRichPresence: '啟用 Discord Rich Presence',\n    enableGlobalShortcut: '啟用全域快捷鍵',\n    showLibraryDefault: '啟動後顯示音樂庫',\n    subTitleDefault: '副標題使用別名',\n    enableReversedMode: '啟用倒序播放功能 (實驗性功能)',\n    enableCustomTitlebar: '啟用自訂標題列（重新啟動後生效）',\n    showLyricsTime: '顯示目前時間',\n    lyricsBackground: {\n      text: '顯示歌詞背景',\n      off: '關閉',\n      on: '開啟',\n      dynamic: '動態（GPU 占用較高）',\n    },\n    closeAppOption: {\n      text: '關閉主面板時...',\n      ask: '詢問',\n      exit: '退出',\n      minimizeToTray: '最小化到工作列角落',\n    },\n    enableOsdlyricsSupport: {\n      title: '桌面歌詞支援',\n      desc1:\n        '只在 Linux 環境下生效。啟用後會將歌詞檔案下載至本機位置，並在開啟播放器時嘗試連帶啟動 OSDLyrics。',\n      desc2: '請在開啟之前確保您已經正確安裝了 OSDLyrics。',\n    },\n    unm: {\n      enable: '啟用',\n      audioSource: {\n        title: '備選音源',\n      },\n      enableFlac: {\n        title: '啟用 FLAC',\n        desc: '啟用後需要清除歌曲快取才能生效',\n      },\n      searchMode: {\n        title: '音源搜尋模式',\n        fast: '速度優先',\n        order: '順序優先',\n      },\n      cookie: {\n        joox: 'Joox 引擎的 Cookie',\n        qq: 'QQ 引擎的 Cookie',\n        desc1: '設定說明請參見此處',\n        desc2: '，留空則不進行相關設定',\n      },\n      ytdl: 'YtDl 引擎要使用的 youtube-dl 執行檔',\n      proxy: {\n        title: '用於 UNM 的 Proxy 伺服器',\n        desc1: '請求如 YouTube 音源服務時要使用的 Proxy 伺服器',\n        desc2: '留空則不進行相關設定',\n      },\n    },\n  },\n  contextMenu: {\n    play: '播放',\n    addToQueue: '新增至佇列',\n    saveToMyLikedSongs: '新增至我喜歡的音樂',\n    removeFromMyLikedSongs: '從喜歡的音樂中刪除',\n    saveToLibrary: '新增至音樂庫',\n    removeFromLibrary: '從音樂庫刪除',\n    addToPlaylist: '新增至歌單',\n    searchInPlaylist: '歌單內搜尋',\n    openInBrowser: '在瀏覽器中打開',\n    copyUrl: '複製連結',\n    allPlaylists: '全部歌單',\n    minePlaylists: '我建立的歌單',\n    likedPlaylists: '收藏的歌單',\n    cardiacMode: '心動模式',\n    copyLyric: '複製歌詞',\n    copyLyricWithTranslation: '複製歌詞（含翻譯）',\n  },\n  toast: {\n    savedToPlaylist: '已新增至歌單',\n    removedFromPlaylist: '已從歌單中刪除',\n    savedToMyLikedSongs: '已新增至我喜歡的音樂',\n    removedFromMyLikedSongs: '已從喜歡的音樂中刪除',\n    copied: '已複製',\n    copyFailed: '複製失敗：',\n    needToLogin: '此動作需要登入網易雲帳戶',\n  },\n};\n"
  },
  {
    "path": "src/main.js",
    "content": "import Vue from 'vue';\nimport VueGtag from 'vue-gtag';\nimport App from './App.vue';\nimport router from './router';\nimport store from './store';\nimport i18n from '@/locale';\nimport '@/assets/icons';\nimport '@/utils/filters';\nimport './registerServiceWorker';\nimport { dailyTask } from '@/utils/common';\nimport '@/assets/css/global.scss';\nimport NProgress from 'nprogress';\nimport '@/assets/css/nprogress.css';\n\nwindow.resetApp = () => {\n  localStorage.clear();\n  indexedDB.deleteDatabase('yesplaymusic');\n  document.cookie.split(';').forEach(function (c) {\n    document.cookie = c\n      .replace(/^ +/, '')\n      .replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/');\n  });\n  return '已重置应用，请刷新页面（按Ctrl/Command + R）';\n};\nconsole.log(\n  '如出现问题，可尝试在本页输入 %cresetApp()%c 然后按回车重置应用。',\n  'background: #eaeffd;color:#335eea;padding: 4px 6px;border-radius:3px;',\n  'background:unset;color:unset;'\n);\n\nVue.use(\n  VueGtag,\n  {\n    config: { id: 'G-KMJJCFZDKF' },\n  },\n  router\n);\nVue.config.productionTip = false;\n\nNProgress.configure({ showSpinner: false, trickleSpeed: 100 });\ndailyTask();\n\nnew Vue({\n  i18n,\n  store,\n  router,\n  render: h => h(App),\n}).$mount('#app');\n"
  },
  {
    "path": "src/ncmModDef.js",
    "content": "module.exports = [\n  {\n    identifier: 'user_update',\n    route: '/user/update',\n    module: require('@neteaseapireborn/api/module/user_update'),\n  },\n  {\n    identifier: 'user_subcount',\n    route: '/user/subcount',\n    module: require('@neteaseapireborn/api/module/user_subcount'),\n  },\n  {\n    identifier: 'user_replacephone',\n    route: '/user/replacephone',\n    module: require('@neteaseapireborn/api/module/user_replacephone'),\n  },\n  {\n    identifier: 'user_record',\n    route: '/user/record',\n    module: require('@neteaseapireborn/api/module/user_record'),\n  },\n  {\n    identifier: 'user_playlist',\n    route: '/user/playlist',\n    module: require('@neteaseapireborn/api/module/user_playlist'),\n  },\n  {\n    identifier: 'user_level',\n    route: '/user/level',\n    module: require('@neteaseapireborn/api/module/user_level'),\n  },\n  {\n    identifier: 'user_follows',\n    route: '/user/follows',\n    module: require('@neteaseapireborn/api/module/user_follows'),\n  },\n  {\n    identifier: 'user_followeds',\n    route: '/user/followeds',\n    module: require('@neteaseapireborn/api/module/user_followeds'),\n  },\n  {\n    identifier: 'user_event',\n    route: '/user/event',\n    module: require('@neteaseapireborn/api/module/user_event'),\n  },\n  {\n    identifier: 'user_dj',\n    route: '/user/dj',\n    module: require('@neteaseapireborn/api/module/user_dj'),\n  },\n  {\n    identifier: 'user_detail',\n    route: '/user/detail',\n    module: require('@neteaseapireborn/api/module/user_detail'),\n  },\n  {\n    identifier: 'user_cloud_detail',\n    route: '/user/cloud/detail',\n    module: require('@neteaseapireborn/api/module/user_cloud_detail'),\n  },\n  {\n    identifier: 'user_cloud_del',\n    route: '/user/cloud/del',\n    module: require('@neteaseapireborn/api/module/user_cloud_del'),\n  },\n  {\n    identifier: 'user_cloud',\n    route: '/user/cloud',\n    module: require('@neteaseapireborn/api/module/user_cloud'),\n  },\n  {\n    identifier: 'user_bindingcellphone',\n    route: '/user/bindingcellphone',\n    module: require('@neteaseapireborn/api/module/user_bindingcellphone'),\n  },\n  {\n    identifier: 'user_binding',\n    route: '/user/binding',\n    module: require('@neteaseapireborn/api/module/user_binding'),\n  },\n  {\n    identifier: 'user_audio',\n    route: '/user/audio',\n    module: require('@neteaseapireborn/api/module/user_audio'),\n  },\n  {\n    identifier: 'user_account',\n    route: '/user/account',\n    module: require('@neteaseapireborn/api/module/user_account'),\n  },\n  {\n    identifier: 'toplist_detail',\n    route: '/toplist/detail',\n    module: require('@neteaseapireborn/api/module/toplist_detail'),\n  },\n  {\n    identifier: 'toplist_artist',\n    route: '/toplist/artist',\n    module: require('@neteaseapireborn/api/module/toplist_artist'),\n  },\n  {\n    identifier: 'toplist',\n    route: '/toplist',\n    module: require('@neteaseapireborn/api/module/toplist'),\n  },\n  {\n    identifier: 'topic_sublist',\n    route: '/topic/sublist',\n    module: require('@neteaseapireborn/api/module/topic_sublist'),\n  },\n  {\n    identifier: 'topic_detail_event_hot',\n    route: '/topic/detail/event/hot',\n    module: require('@neteaseapireborn/api/module/topic_detail_event_hot'),\n  },\n  {\n    identifier: 'topic_detail',\n    route: '/topic/detail',\n    module: require('@neteaseapireborn/api/module/topic_detail'),\n  },\n  {\n    identifier: 'top_song',\n    route: '/top/song',\n    module: require('@neteaseapireborn/api/module/top_song'),\n  },\n  {\n    identifier: 'top_playlist_highquality',\n    route: '/top/playlist/highquality',\n    module: require('@neteaseapireborn/api/module/top_playlist_highquality'),\n  },\n  {\n    identifier: 'top_playlist',\n    route: '/top/playlist',\n    module: require('@neteaseapireborn/api/module/top_playlist'),\n  },\n  {\n    identifier: 'top_mv',\n    route: '/top/mv',\n    module: require('@neteaseapireborn/api/module/top_mv'),\n  },\n  {\n    identifier: 'top_list',\n    route: '/top/list',\n    module: require('@neteaseapireborn/api/module/top_list'),\n  },\n  {\n    identifier: 'top_artists',\n    route: '/top/artists',\n    module: require('@neteaseapireborn/api/module/top_artists'),\n  },\n  {\n    identifier: 'top_album',\n    route: '/top/album',\n    module: require('@neteaseapireborn/api/module/top_album'),\n  },\n  {\n    identifier: 'song_url',\n    route: '/song/url',\n    module: require('@neteaseapireborn/api/module/song_url'),\n  },\n  {\n    identifier: 'song_download_url',\n    route: '/song/download/url',\n    module: require('@neteaseapireborn/api/module/song_download_url'),\n  },\n  {\n    identifier: 'song_detail',\n    route: '/song/detail',\n    module: require('@neteaseapireborn/api/module/song_detail'),\n  },\n  {\n    identifier: 'simi_mv',\n    route: '/simi/mv',\n    module: require('@neteaseapireborn/api/module/simi_mv'),\n  },\n  {\n    identifier: 'simi_artist',\n    route: '/simi/artist',\n    module: require('@neteaseapireborn/api/module/simi_artist'),\n  },\n  {\n    identifier: 'search',\n    route: '/search',\n    module: require('@neteaseapireborn/api/module/search'),\n  },\n  {\n    identifier: 'scrobble',\n    route: '/scrobble',\n    module: require('@neteaseapireborn/api/module/scrobble'),\n  },\n  {\n    identifier: 'recommend_songs',\n    route: '/recommend/songs',\n    module: require('@neteaseapireborn/api/module/recommend_songs'),\n  },\n  {\n    identifier: 'recommend_resource',\n    route: '/recommend/resource',\n    module: require('@neteaseapireborn/api/module/recommend_resource'),\n  },\n  {\n    identifier: 'playmode_intelligence_list',\n    route: '/playmode/intelligence/list',\n    module: require('@neteaseapireborn/api/module/playmode_intelligence_list'),\n  },\n  {\n    identifier: 'playlist_video_recent',\n    route: '/playlist/video/recent',\n    module: require('@neteaseapireborn/api/module/playlist_video_recent'),\n  },\n  {\n    identifier: 'playlist_update',\n    route: '/playlist/update',\n    module: require('@neteaseapireborn/api/module/playlist_update'),\n  },\n  {\n    identifier: 'playlist_tracks',\n    route: '/playlist/tracks',\n    module: require('@neteaseapireborn/api/module/playlist_tracks'),\n  },\n  {\n    identifier: 'playlist_track_delete',\n    route: '/playlist/track/delete',\n    module: require('@neteaseapireborn/api/module/playlist_track_delete'),\n  },\n  {\n    identifier: 'playlist_track_all',\n    route: '/playlist/track/all',\n    module: require('@neteaseapireborn/api/module/playlist_track_all'),\n  },\n  {\n    identifier: 'playlist_track_add',\n    route: '/playlist/track/add',\n    module: require('@neteaseapireborn/api/module/playlist_track_add'),\n  },\n  {\n    identifier: 'playlist_tags_update',\n    route: '/playlist/tags/update',\n    module: require('@neteaseapireborn/api/module/playlist_tags_update'),\n  },\n  {\n    identifier: 'playlist_subscribers',\n    route: '/playlist/subscribers',\n    module: require('@neteaseapireborn/api/module/playlist_subscribers'),\n  },\n  {\n    identifier: 'playlist_subscribe',\n    route: '/playlist/subscribe',\n    module: require('@neteaseapireborn/api/module/playlist_subscribe'),\n  },\n  {\n    identifier: 'playlist_privacy',\n    route: '/playlist/privacy',\n    module: require('@neteaseapireborn/api/module/playlist_privacy'),\n  },\n  {\n    identifier: 'playlist_order_update',\n    route: '/playlist/order/update',\n    module: require('@neteaseapireborn/api/module/playlist_order_update'),\n  },\n  {\n    identifier: 'playlist_name_update',\n    route: '/playlist/name/update',\n    module: require('@neteaseapireborn/api/module/playlist_name_update'),\n  },\n  {\n    identifier: 'playlist_mylike',\n    route: '/playlist/mylike',\n    module: require('@neteaseapireborn/api/module/playlist_mylike'),\n  },\n  {\n    identifier: 'playlist_hot',\n    route: '/playlist/hot',\n    module: require('@neteaseapireborn/api/module/playlist_hot'),\n  },\n  {\n    identifier: 'playlist_highquality_tags',\n    route: '/playlist/highquality/tags',\n    module: require('@neteaseapireborn/api/module/playlist_highquality_tags'),\n  },\n  {\n    identifier: 'playlist_detail_dynamic',\n    route: '/playlist/detail/dynamic',\n    module: require('@neteaseapireborn/api/module/playlist_detail_dynamic'),\n  },\n  {\n    identifier: 'playlist_detail',\n    route: '/playlist/detail',\n    module: require('@neteaseapireborn/api/module/playlist_detail'),\n  },\n  {\n    identifier: 'playlist_desc_update',\n    route: '/playlist/desc/update',\n    module: require('@neteaseapireborn/api/module/playlist_desc_update'),\n  },\n  {\n    identifier: 'playlist_delete',\n    route: '/playlist/delete',\n    module: require('@neteaseapireborn/api/module/playlist_delete'),\n  },\n  {\n    identifier: 'playlist_create',\n    route: '/playlist/create',\n    module: require('@neteaseapireborn/api/module/playlist_create'),\n  },\n  {\n    identifier: 'playlist_cover_update',\n    route: '/playlist/cover/update',\n    module: require('@neteaseapireborn/api/module/playlist_cover_update'),\n  },\n  {\n    identifier: 'playlist_catlist',\n    route: '/playlist/catlist',\n    module: require('@neteaseapireborn/api/module/playlist_catlist'),\n  },\n  {\n    identifier: 'personalized',\n    route: '/personalized',\n    module: require('@neteaseapireborn/api/module/personalized'),\n  },\n  {\n    identifier: 'personal_fm',\n    route: '/personal_fm',\n    module: require('@neteaseapireborn/api/module/personal_fm'),\n  },\n  {\n    identifier: 'mv_url',\n    route: '/mv/url',\n    module: require('@neteaseapireborn/api/module/mv_url'),\n  },\n  {\n    identifier: 'mv_sublist',\n    route: '/mv/sublist',\n    module: require('@neteaseapireborn/api/module/mv_sublist'),\n  },\n  {\n    identifier: 'mv_sub',\n    route: '/mv/sub',\n    module: require('@neteaseapireborn/api/module/mv_sub'),\n  },\n  {\n    identifier: 'mv_first',\n    route: '/mv/first',\n    module: require('@neteaseapireborn/api/module/mv_first'),\n  },\n  {\n    identifier: 'mv_exclusive_rcmd',\n    route: '/mv/exclusive/rcmd',\n    module: require('@neteaseapireborn/api/module/mv_exclusive_rcmd'),\n  },\n  {\n    identifier: 'mv_detail_info',\n    route: '/mv/detail/info',\n    module: require('@neteaseapireborn/api/module/mv_detail_info'),\n  },\n  {\n    identifier: 'mv_detail',\n    route: '/mv/detail',\n    module: require('@neteaseapireborn/api/module/mv_detail'),\n  },\n  {\n    identifier: 'mv_all',\n    route: '/mv/all',\n    module: require('@neteaseapireborn/api/module/mv_all'),\n  },\n  {\n    identifier: 'lyric',\n    route: '/lyric',\n    module: require('@neteaseapireborn/api/module/lyric'),\n  },\n  {\n    identifier: 'logout',\n    route: '/logout',\n    module: require('@neteaseapireborn/api/module/logout'),\n  },\n  {\n    identifier: 'login_status',\n    route: '/login/status',\n    module: require('@neteaseapireborn/api/module/login_status'),\n  },\n  {\n    identifier: 'login_refresh',\n    route: '/login/refresh',\n    module: require('@neteaseapireborn/api/module/login_refresh'),\n  },\n  {\n    identifier: 'login_qr_key',\n    route: '/login/qr/key',\n    module: require('@neteaseapireborn/api/module/login_qr_key'),\n  },\n  {\n    identifier: 'login_qr_create',\n    route: '/login/qr/create',\n    module: require('@neteaseapireborn/api/module/login_qr_create'),\n  },\n  {\n    identifier: 'login_qr_check',\n    route: '/login/qr/check',\n    module: require('@neteaseapireborn/api/module/login_qr_check'),\n  },\n  {\n    identifier: 'login_cellphone',\n    route: '/login/cellphone',\n    module: require('@neteaseapireborn/api/module/login_cellphone'),\n  },\n  {\n    identifier: 'login',\n    route: '/login',\n    module: require('@neteaseapireborn/api/module/login'),\n  },\n  {\n    identifier: 'likelist',\n    route: '/likelist',\n    module: require('@neteaseapireborn/api/module/likelist'),\n  },\n  {\n    identifier: 'like',\n    route: '/like',\n    module: require('@neteaseapireborn/api/module/like'),\n  },\n  {\n    identifier: 'follow',\n    route: '/follow',\n    module: require('@neteaseapireborn/api/module/follow'),\n  },\n  {\n    identifier: 'fm_trash',\n    route: '/fm_trash',\n    module: require('@neteaseapireborn/api/module/fm_trash'),\n  },\n  {\n    identifier: 'daily_signin',\n    route: '/daily_signin',\n    module: require('@neteaseapireborn/api/module/daily_signin'),\n  },\n  {\n    identifier: 'cloudsearch',\n    route: '/cloudsearch',\n    module: require('@neteaseapireborn/api/module/cloudsearch'),\n  },\n  {\n    identifier: 'cloud',\n    route: '/cloud',\n    module: require('@neteaseapireborn/api/module/cloud'),\n  },\n  {\n    identifier: 'check_music',\n    route: '/check/music',\n    module: require('@neteaseapireborn/api/module/check_music'),\n  },\n  {\n    identifier: 'cellphone_existence_check',\n    route: '/cellphone/existence/check',\n    module: require('@neteaseapireborn/api/module/cellphone_existence_check'),\n  },\n  {\n    identifier: 'captcha_verify',\n    route: '/captcha/verify',\n    module: require('@neteaseapireborn/api/module/captcha_verify'),\n  },\n  {\n    identifier: 'captcha_sent',\n    route: '/captcha/sent',\n    module: require('@neteaseapireborn/api/module/captcha_sent'),\n  },\n  {\n    identifier: 'calendar',\n    route: '/calendar',\n    module: require('@neteaseapireborn/api/module/calendar'),\n  },\n  {\n    identifier: 'batch',\n    route: '/batch',\n    module: require('@neteaseapireborn/api/module/batch'),\n  },\n  {\n    identifier: 'banner',\n    route: '/banner',\n    module: require('@neteaseapireborn/api/module/banner'),\n  },\n  {\n    identifier: 'avatar_upload',\n    route: '/avatar/upload',\n    module: require('@neteaseapireborn/api/module/avatar_upload'),\n  },\n  {\n    identifier: 'audio_match',\n    route: '/audio/match',\n    module: require('@neteaseapireborn/api/module/audio_match'),\n  },\n  {\n    identifier: 'artists',\n    route: '/artists',\n    module: require('@neteaseapireborn/api/module/artists'),\n  },\n  {\n    identifier: 'artist_video',\n    route: '/artist/video',\n    module: require('@neteaseapireborn/api/module/artist_video'),\n  },\n  {\n    identifier: 'artist_top_song',\n    route: '/artist/top/song',\n    module: require('@neteaseapireborn/api/module/artist_top_song'),\n  },\n  {\n    identifier: 'artist_sublist',\n    route: '/artist/sublist',\n    module: require('@neteaseapireborn/api/module/artist_sublist'),\n  },\n  {\n    identifier: 'artist_sub',\n    route: '/artist/sub',\n    module: require('@neteaseapireborn/api/module/artist_sub'),\n  },\n  {\n    identifier: 'artist_songs',\n    route: '/artist/songs',\n    module: require('@neteaseapireborn/api/module/artist_songs'),\n  },\n  {\n    identifier: 'artist_new_song',\n    route: '/artist/new/song',\n    module: require('@neteaseapireborn/api/module/artist_new_song'),\n  },\n  {\n    identifier: 'artist_new_mv',\n    route: '/artist/new/mv',\n    module: require('@neteaseapireborn/api/module/artist_new_mv'),\n  },\n  {\n    identifier: 'artist_mv',\n    route: '/artist/mv',\n    module: require('@neteaseapireborn/api/module/artist_mv'),\n  },\n  {\n    identifier: 'artist_list',\n    route: '/artist/list',\n    module: require('@neteaseapireborn/api/module/artist_list'),\n  },\n  {\n    identifier: 'artist_fans',\n    route: '/artist/fans',\n    module: require('@neteaseapireborn/api/module/artist_fans'),\n  },\n  {\n    identifier: 'artist_detail',\n    route: '/artist/detail',\n    module: require('@neteaseapireborn/api/module/artist_detail'),\n  },\n  {\n    identifier: 'artist_desc',\n    route: '/artist/desc',\n    module: require('@neteaseapireborn/api/module/artist_desc'),\n  },\n  {\n    identifier: 'artist_album',\n    route: '/artist/album',\n    module: require('@neteaseapireborn/api/module/artist_album'),\n  },\n  {\n    identifier: 'album_sublist',\n    route: '/album/sublist',\n    module: require('@neteaseapireborn/api/module/album_sublist'),\n  },\n  {\n    identifier: 'album_sub',\n    route: '/album/sub',\n    module: require('@neteaseapireborn/api/module/album_sub'),\n  },\n  {\n    identifier: 'album_songsaleboard',\n    route: '/album/songsaleboard',\n    module: require('@neteaseapireborn/api/module/album_songsaleboard'),\n  },\n  {\n    identifier: 'album_newest',\n    route: '/album/newest',\n    module: require('@neteaseapireborn/api/module/album_newest'),\n  },\n  {\n    identifier: 'album_new',\n    route: '/album/new',\n    module: require('@neteaseapireborn/api/module/album_new'),\n  },\n  {\n    identifier: 'album_list_style',\n    route: '/album/list/style',\n    module: require('@neteaseapireborn/api/module/album_list_style'),\n  },\n  {\n    identifier: 'album_list',\n    route: '/album/list',\n    module: require('@neteaseapireborn/api/module/album_list'),\n  },\n  {\n    identifier: 'album_detail_dynamic',\n    route: '/album/detail/dynamic',\n    module: require('@neteaseapireborn/api/module/album_detail_dynamic'),\n  },\n  {\n    identifier: 'album_detail',\n    route: '/album/detail',\n    module: require('@neteaseapireborn/api/module/album_detail'),\n  },\n  {\n    identifier: 'album',\n    route: '/album',\n    module: require('@neteaseapireborn/api/module/album'),\n  },\n  {\n    identifier: 'activate_init_profile',\n    route: '/activate/init/profile',\n    module: require('@neteaseapireborn/api/module/activate_init_profile'),\n  },\n];\n"
  },
  {
    "path": "src/registerServiceWorker.js",
    "content": "/* eslint-disable no-console */\n\nimport { register } from 'register-service-worker';\n\nif (!process.env.IS_ELECTRON) {\n  register(`${process.env.BASE_URL}service-worker.js`, {\n    ready() {\n      // console.log(\n      //   \"App is being served from cache by a service worker.\\n\" +\n      //     \"For more details, visit https://goo.gl/AFskqB\"\n      // );\n    },\n    registered() {\n      // console.log(\"Service worker has been registered.\");\n    },\n    cached() {\n      // console.log(\"Content has been cached for offline use.\");\n    },\n    updatefound() {\n      // console.log(\"New content is downloading.\");\n    },\n    updated() {\n      // console.log(\"New content is available; please refresh.\");\n    },\n    offline() {\n      // console.log(\n      //   \"No internet connection found. App is running in offline mode.\"\n      // );\n    },\n    error(error) {\n      console.error('Error during service worker registration:', error);\n    },\n  });\n}\n"
  },
  {
    "path": "src/router/index.js",
    "content": "import Vue from 'vue';\nimport VueRouter from 'vue-router';\nimport { isLooseLoggedIn, isAccountLoggedIn } from '@/utils/auth';\n\nVue.use(VueRouter);\nconst routes = [\n  {\n    path: '/',\n    name: 'home',\n    component: () => import('@/views/home.vue'),\n    meta: {\n      keepAlive: true,\n      savePosition: true,\n    },\n  },\n  {\n    path: '/login',\n    name: 'login',\n    component: () => import('@/views/login.vue'),\n  },\n  {\n    path: '/login/username',\n    name: 'loginUsername',\n    component: () => import('@/views/loginUsername.vue'),\n  },\n  {\n    path: '/login/account',\n    name: 'loginAccount',\n    component: () => import('@/views/loginAccount.vue'),\n  },\n  {\n    path: '/playlist/:id',\n    name: 'playlist',\n    component: () => import('@/views/playlist.vue'),\n  },\n  {\n    path: '/album/:id',\n    name: 'album',\n    component: () => import('@/views/album.vue'),\n  },\n  {\n    path: '/artist/:id',\n    name: 'artist',\n    component: () => import('@/views/artist.vue'),\n    meta: {\n      keepAlive: true,\n      savePosition: true,\n    },\n  },\n  {\n    path: '/artist/:id/mv',\n    name: 'artistMV',\n    component: () => import('@/views/artistMV.vue'),\n    meta: {\n      keepAlive: true,\n    },\n  },\n  {\n    path: '/mv/:id',\n    name: 'mv',\n    component: () => import('@/views/mv.vue'),\n  },\n  {\n    path: '/next',\n    name: 'next',\n    component: () => import('@/views/next.vue'),\n    meta: {\n      keepAlive: true,\n      savePosition: true,\n    },\n  },\n  {\n    path: '/search/:keywords?',\n    name: 'search',\n    component: () => import('@/views/search.vue'),\n    meta: {\n      keepAlive: true,\n    },\n  },\n  {\n    path: '/search/:keywords/:type',\n    name: 'searchType',\n    component: () => import('@/views/searchType.vue'),\n  },\n  {\n    path: '/new-album',\n    name: 'newAlbum',\n    component: () => import('@/views/newAlbum.vue'),\n  },\n  {\n    path: '/explore',\n    name: 'explore',\n    component: () => import('@/views/explore.vue'),\n    meta: {\n      keepAlive: true,\n      savePosition: true,\n    },\n  },\n  {\n    path: '/library',\n    name: 'library',\n    component: () => import('@/views/library.vue'),\n    meta: {\n      requireLogin: true,\n      keepAlive: true,\n      savePosition: true,\n    },\n  },\n  {\n    path: '/library/liked-songs',\n    name: 'likedSongs',\n    component: () => import('@/views/playlist.vue'),\n    meta: {\n      requireLogin: true,\n    },\n  },\n  {\n    path: '/settings',\n    name: 'settings',\n    component: () => import('@/views/settings.vue'),\n  },\n  {\n    path: '/daily/songs',\n    name: 'dailySongs',\n    component: () => import('@/views/dailyTracks.vue'),\n    meta: {\n      requireAccountLogin: true,\n    },\n  },\n  {\n    path: '/lastfm/callback',\n    name: 'lastfmCallback',\n    component: () => import('@/views/lastfmCallback.vue'),\n  },\n];\n\nconst router = new VueRouter({\n  mode: process.env.IS_ELECTRON ? 'hash' : 'history',\n  routes,\n});\n\nconst originalPush = VueRouter.prototype.push;\nVueRouter.prototype.push = function push(location) {\n  return originalPush.call(this, location).catch(err => err);\n};\n\nrouter.beforeEach((to, from, next) => {\n  // 需要登录的逻辑\n  if (to.meta.requireAccountLogin) {\n    if (isAccountLoggedIn()) {\n      next();\n    } else {\n      next({ path: '/login/account' });\n    }\n  }\n  if (to.meta.requireLogin) {\n    if (isLooseLoggedIn()) {\n      next();\n    } else {\n      if (process.env.IS_ELECTRON === true) {\n        next({ path: '/login/account' });\n      } else {\n        next({ path: '/login' });\n      }\n    }\n  } else {\n    next();\n  }\n});\n\nexport default router;\n"
  },
  {
    "path": "src/store/actions.js",
    "content": "// import store, { state, dispatch, commit } from \"@/store\";\nimport { isAccountLoggedIn, isLooseLoggedIn } from '@/utils/auth';\nimport { likeATrack } from '@/api/track';\nimport { getPlaylistDetail } from '@/api/playlist';\nimport { getTrackDetail } from '@/api/track';\nimport {\n  userPlaylist,\n  userPlayHistory,\n  userLikedSongsIDs,\n  likedAlbums,\n  likedArtists,\n  likedMVs,\n  cloudDisk,\n  userAccount,\n} from '@/api/user';\n\nexport default {\n  showToast({ state, commit }, text) {\n    if (state.toast.timer !== null) {\n      clearTimeout(state.toast.timer);\n      commit('updateToast', { show: false, text: '', timer: null });\n    }\n    commit('updateToast', {\n      show: true,\n      text,\n      timer: setTimeout(() => {\n        commit('updateToast', {\n          show: false,\n          text: state.toast.text,\n          timer: null,\n        });\n      }, 3200),\n    });\n  },\n  likeATrack({ state, commit, dispatch }, id) {\n    if (!isAccountLoggedIn()) {\n      dispatch('showToast', '此操作需要登录网易云账号');\n      return;\n    }\n    let like = true;\n    if (state.liked.songs.includes(id)) like = false;\n    likeATrack({ id, like })\n      .then(() => {\n        if (like === false) {\n          commit('updateLikedXXX', {\n            name: 'songs',\n            data: state.liked.songs.filter(d => d !== id),\n          });\n        } else {\n          let newLikeSongs = state.liked.songs;\n          newLikeSongs.push(id);\n          commit('updateLikedXXX', {\n            name: 'songs',\n            data: newLikeSongs,\n          });\n        }\n        dispatch('fetchLikedSongsWithDetails');\n      })\n      .catch(() => {\n        dispatch('showToast', '操作失败，专辑下架或版权锁定');\n      });\n  },\n  fetchLikedSongs: ({ state, commit }) => {\n    if (!isLooseLoggedIn()) return;\n    if (isAccountLoggedIn()) {\n      return userLikedSongsIDs({ uid: state.data.user.userId }).then(result => {\n        if (result.ids) {\n          commit('updateLikedXXX', {\n            name: 'songs',\n            data: result.ids,\n          });\n        }\n      });\n    } else {\n      // TODO:搜索ID登录的用户\n    }\n  },\n  fetchLikedSongsWithDetails: ({ state, commit }) => {\n    return getPlaylistDetail(state.data.likedSongPlaylistID, true).then(\n      result => {\n        if (result.playlist?.trackIds?.length === 0) {\n          return new Promise(resolve => {\n            resolve();\n          });\n        }\n        return getTrackDetail(\n          result.playlist.trackIds\n            .slice(0, 12)\n            .map(t => t.id)\n            .join(',')\n        ).then(result => {\n          commit('updateLikedXXX', {\n            name: 'songsWithDetails',\n            data: result.songs,\n          });\n        });\n      }\n    );\n  },\n  fetchLikedPlaylist: ({ state, commit }) => {\n    if (!isLooseLoggedIn()) return;\n    if (isAccountLoggedIn()) {\n      return userPlaylist({\n        uid: state.data.user?.userId,\n        limit: 2000, // 最多只加载2000个歌单（等有用户反馈问题再修）\n        timestamp: new Date().getTime(),\n      }).then(result => {\n        if (result.playlist) {\n          commit('updateLikedXXX', {\n            name: 'playlists',\n            data: result.playlist,\n          });\n          // 更新用户”喜欢的歌曲“歌单ID\n          commit('updateData', {\n            key: 'likedSongPlaylistID',\n            value: result.playlist[0].id,\n          });\n        }\n      });\n    } else {\n      // TODO:搜索ID登录的用户\n    }\n  },\n  fetchLikedAlbums: ({ commit }) => {\n    if (!isAccountLoggedIn()) return;\n    return likedAlbums({ limit: 2000 }).then(result => {\n      if (result.data) {\n        commit('updateLikedXXX', {\n          name: 'albums',\n          data: result.data,\n        });\n      }\n    });\n  },\n  fetchLikedArtists: ({ commit }) => {\n    if (!isAccountLoggedIn()) return;\n    return likedArtists({ limit: 2000 }).then(result => {\n      if (result.data) {\n        commit('updateLikedXXX', {\n          name: 'artists',\n          data: result.data,\n        });\n      }\n    });\n  },\n  fetchLikedMVs: ({ commit }) => {\n    if (!isAccountLoggedIn()) return;\n    return likedMVs({ limit: 1000 }).then(result => {\n      if (result.data) {\n        commit('updateLikedXXX', {\n          name: 'mvs',\n          data: result.data,\n        });\n      }\n    });\n  },\n  fetchCloudDisk: ({ commit }) => {\n    if (!isAccountLoggedIn()) return;\n    // FIXME: #1242\n    return cloudDisk({ limit: 1000 }).then(result => {\n      if (result.data) {\n        commit('updateLikedXXX', {\n          name: 'cloudDisk',\n          data: result.data,\n        });\n      }\n    });\n  },\n  fetchPlayHistory: ({ state, commit }) => {\n    if (!isAccountLoggedIn()) return;\n    return Promise.all([\n      userPlayHistory({ uid: state.data.user?.userId, type: 0 }),\n      userPlayHistory({ uid: state.data.user?.userId, type: 1 }),\n    ]).then(result => {\n      const data = {};\n      const dataType = { 0: 'allData', 1: 'weekData' };\n      if (result[0] && result[1]) {\n        for (let i = 0; i < result.length; i++) {\n          const songData = result[i][dataType[i]].map(item => {\n            const song = item.song;\n            song.playCount = item.playCount;\n            return song;\n          });\n          data[[dataType[i]]] = songData;\n        }\n        commit('updateLikedXXX', {\n          name: 'playHistory',\n          data: data,\n        });\n      }\n    });\n  },\n  fetchUserProfile: ({ commit }) => {\n    if (!isAccountLoggedIn()) return;\n    return userAccount().then(result => {\n      if (result.code === 200) {\n        commit('updateData', { key: 'user', value: result.profile });\n      }\n    });\n  },\n};\n"
  },
  {
    "path": "src/store/index.js",
    "content": "import Vue from 'vue';\nimport Vuex from 'vuex';\nimport state from './state';\nimport mutations from './mutations';\nimport actions from './actions';\nimport { changeAppearance } from '@/utils/common';\nimport Player from '@/utils/Player';\n// vuex 自定义插件\nimport saveToLocalStorage from './plugins/localStorage';\nimport { getSendSettingsPlugin } from './plugins/sendSettings';\n\nVue.use(Vuex);\n\nlet plugins = [saveToLocalStorage];\nif (process.env.IS_ELECTRON === true) {\n  let sendSettings = getSendSettingsPlugin();\n  plugins.push(sendSettings);\n}\nconst options = {\n  state,\n  mutations,\n  actions,\n  plugins,\n};\n\nconst store = new Vuex.Store(options);\n\nif ([undefined, null].includes(store.state.settings.lang)) {\n  const defaultLang = 'en';\n  const langMapper = new Map()\n    .set('zh', 'zh-CN')\n    .set('zh-TW', 'zh-TW')\n    .set('en', 'en')\n    .set('tr', 'tr');\n  store.state.settings.lang =\n    langMapper.get(\n      langMapper.has(navigator.language)\n        ? navigator.language\n        : navigator.language.slice(0, 2)\n    ) || defaultLang;\n  localStorage.setItem('settings', JSON.stringify(store.state.settings));\n}\n\nchangeAppearance(store.state.settings.appearance);\n\nwindow\n  .matchMedia('(prefers-color-scheme: dark)')\n  .addEventListener('change', () => {\n    if (store.state.settings.appearance === 'auto') {\n      changeAppearance(store.state.settings.appearance);\n    }\n  });\n\nlet player = new Player();\nplayer = new Proxy(player, {\n  set(target, prop, val) {\n    // console.log({ prop, val });\n    target[prop] = val;\n    if (prop === '_howler') return true;\n    target.saveSelfToLocalStorage();\n    target.sendSelfToIpcMain();\n    return true;\n  },\n});\nstore.state.player = player;\n\nexport default store;\n"
  },
  {
    "path": "src/store/initLocalStorage.js",
    "content": "import { playlistCategories } from '@/utils/staticData';\nimport shortcuts from '@/utils/shortcuts';\n\nconsole.debug('[debug][initLocalStorage.js]');\nconst enabledPlaylistCategories = playlistCategories\n  .filter(c => c.enable)\n  .map(c => c.name);\n\nlet localStorage = {\n  player: {},\n  settings: {\n    lang: null,\n    musicLanguage: 'all',\n    appearance: 'auto',\n    musicQuality: 320000,\n    lyricFontSize: 28,\n    outputDevice: 'default',\n    showPlaylistsByAppleMusic: true,\n    enableUnblockNeteaseMusic: true,\n    automaticallyCacheSongs: true,\n    cacheLimit: 8192,\n    enableReversedMode: false,\n    nyancatStyle: false,\n    showLyricsTranslation: true,\n    lyricsBackground: true,\n    enableOsdlyricsSupport: false,\n    closeAppOption: 'ask',\n    enableDiscordRichPresence: false,\n    enableGlobalShortcut: true,\n    showLibraryDefault: false,\n    subTitleDefault: false,\n    linuxEnableCustomTitlebar: false,\n    trayIconTheme: 'auto',\n    enabledPlaylistCategories,\n    proxyConfig: {\n      protocol: 'noProxy',\n      server: '',\n      port: null,\n    },\n    enableRealIP: false,\n    realIP: null,\n    shortcuts: shortcuts,\n  },\n  data: {\n    user: {},\n    likedSongPlaylistID: 0,\n    lastRefreshCookieDate: 0,\n    loginMode: null,\n  },\n};\n\nif (process.env.IS_ELECTRON === true) {\n  localStorage.settings.automaticallyCacheSongs = true;\n}\n\nexport default localStorage;\n"
  },
  {
    "path": "src/store/mutations.js",
    "content": "import shortcuts from '@/utils/shortcuts';\nimport cloneDeep from 'lodash/cloneDeep';\n\nexport default {\n  updateLikedXXX(state, { name, data }) {\n    state.liked[name] = data;\n    if (name === 'songs') {\n      state.player.sendSelfToIpcMain();\n    }\n  },\n  changeLang(state, lang) {\n    state.settings.lang = lang;\n  },\n  changeMusicQuality(state, value) {\n    state.settings.musicQuality = value;\n  },\n  changeLyricFontSize(state, value) {\n    state.settings.lyricFontSize = value;\n  },\n  changeOutputDevice(state, deviceId) {\n    state.settings.outputDevice = deviceId;\n  },\n  updateSettings(state, { key, value }) {\n    state.settings[key] = value;\n  },\n  updateData(state, { key, value }) {\n    state.data[key] = value;\n  },\n  togglePlaylistCategory(state, name) {\n    const index = state.settings.enabledPlaylistCategories.findIndex(\n      c => c === name\n    );\n    if (index !== -1) {\n      state.settings.enabledPlaylistCategories =\n        state.settings.enabledPlaylistCategories.filter(c => c !== name);\n    } else {\n      state.settings.enabledPlaylistCategories.push(name);\n    }\n  },\n  updateToast(state, toast) {\n    state.toast = toast;\n  },\n  updateModal(state, { modalName, key, value }) {\n    state.modals[modalName][key] = value;\n    if (key === 'show') {\n      // 100ms的延迟是为等待右键菜单blur之后再disableScrolling\n      value === true\n        ? setTimeout(() => (state.enableScrolling = false), 100)\n        : (state.enableScrolling = true);\n    }\n  },\n  toggleLyrics(state) {\n    state.showLyrics = !state.showLyrics;\n  },\n  updateDailyTracks(state, dailyTracks) {\n    state.dailyTracks = dailyTracks;\n  },\n  updateLastfm(state, session) {\n    state.lastfm = session;\n  },\n  updateShortcut(state, { id, type, shortcut }) {\n    let newShortcut = state.settings.shortcuts.find(s => s.id === id);\n    newShortcut[type] = shortcut;\n    state.settings.shortcuts = state.settings.shortcuts.map(s => {\n      if (s.id !== id) return s;\n      return newShortcut;\n    });\n  },\n  restoreDefaultShortcuts(state) {\n    state.settings.shortcuts = cloneDeep(shortcuts);\n  },\n  enableScrolling(state, status = null) {\n    state.enableScrolling = status ? status : !state.enableScrolling;\n  },\n  updateTitle(state, title) {\n    state.title = title;\n  },\n};\n"
  },
  {
    "path": "src/store/plugins/localStorage.js",
    "content": "export default store => {\n  store.subscribe((mutation, state) => {\n    // console.log(mutation);\n    localStorage.setItem('settings', JSON.stringify(state.settings));\n    localStorage.setItem('data', JSON.stringify(state.data));\n  });\n};\n"
  },
  {
    "path": "src/store/plugins/sendSettings.js",
    "content": "export function getSendSettingsPlugin() {\n  const electron = window.require('electron');\n  const ipcRenderer = electron.ipcRenderer;\n  return store => {\n    store.subscribe((mutation, state) => {\n      // console.log(mutation);\n      if (mutation.type !== 'updateSettings') return;\n      ipcRenderer.send('settings', state.settings);\n    });\n  };\n}\n"
  },
  {
    "path": "src/store/state.js",
    "content": "import initLocalStorage from './initLocalStorage';\nimport pkg from '../../package.json';\nimport updateApp from '@/utils/updateApp';\n\nif (localStorage.getItem('appVersion') === null) {\n  localStorage.setItem('settings', JSON.stringify(initLocalStorage.settings));\n  localStorage.setItem('data', JSON.stringify(initLocalStorage.data));\n  localStorage.setItem('appVersion', pkg.version);\n}\n\nupdateApp();\n\nexport default {\n  showLyrics: false,\n  enableScrolling: true,\n  title: 'YesPlayMusic',\n  liked: {\n    songs: [],\n    songsWithDetails: [], // 只有前12首\n    playlists: [],\n    albums: [],\n    artists: [],\n    mvs: [],\n    cloudDisk: [],\n    playHistory: {\n      weekData: [],\n      allData: [],\n    },\n  },\n  contextMenu: {\n    clickObjectID: 0,\n    showMenu: false,\n  },\n  toast: {\n    show: false,\n    text: '',\n    timer: null,\n  },\n  modals: {\n    addTrackToPlaylistModal: {\n      show: false,\n      selectedTrackID: 0,\n    },\n    newPlaylistModal: {\n      show: false,\n      afterCreateAddTrackID: 0,\n    },\n  },\n  dailyTracks: [],\n  lastfm: JSON.parse(localStorage.getItem('lastfm')) || {},\n  player: JSON.parse(localStorage.getItem('player')),\n  settings: JSON.parse(localStorage.getItem('settings')),\n  data: JSON.parse(localStorage.getItem('data')),\n};\n"
  },
  {
    "path": "src/utils/Player.js",
    "content": "import { getAlbum } from '@/api/album';\nimport { getArtist } from '@/api/artist';\nimport { trackScrobble, trackUpdateNowPlaying } from '@/api/lastfm';\nimport { fmTrash, personalFM } from '@/api/others';\nimport { getPlaylistDetail, intelligencePlaylist } from '@/api/playlist';\nimport { getLyric, getMP3, getTrackDetail, scrobble } from '@/api/track';\nimport store from '@/store';\nimport { isAccountLoggedIn } from '@/utils/auth';\nimport { cacheTrackSource, getTrackSource } from '@/utils/db';\nimport { isCreateMpris, isCreateTray } from '@/utils/platform';\nimport { Howl, Howler } from 'howler';\nimport shuffle from 'lodash/shuffle';\nimport { decode as base642Buffer } from '@/utils/base64';\n\nconst PLAY_PAUSE_FADE_DURATION = 200;\n\nconst INDEX_IN_PLAY_NEXT = -1;\n\n/**\n * @readonly\n * @enum {string}\n */\nconst UNPLAYABLE_CONDITION = {\n  PLAY_NEXT_TRACK: 'playNextTrack',\n  PLAY_PREV_TRACK: 'playPrevTrack',\n};\n\nconst electron =\n  process.env.IS_ELECTRON === true ? window.require('electron') : null;\nconst ipcRenderer =\n  process.env.IS_ELECTRON === true ? electron.ipcRenderer : null;\nconst delay = ms =>\n  new Promise(resolve => {\n    setTimeout(() => {\n      resolve('');\n    }, ms);\n  });\nconst excludeSaveKeys = [\n  '_playing',\n  '_personalFMLoading',\n  '_personalFMNextLoading',\n];\n\nfunction setTitle(track) {\n  document.title = track\n    ? `${track.name} · ${track.ar[0].name} - YesPlayMusic`\n    : 'YesPlayMusic';\n  if (isCreateTray) {\n    ipcRenderer?.send('updateTrayTooltip', document.title);\n  }\n  store.commit('updateTitle', document.title);\n}\n\nfunction setTrayLikeState(isLiked) {\n  if (isCreateTray) {\n    ipcRenderer?.send('updateTrayLikeState', isLiked);\n  }\n}\n\nexport default class {\n  constructor() {\n    // 播放器状态\n    this._playing = false; // 是否正在播放中\n    this._progress = 0; // 当前播放歌曲的进度\n    this._enabled = false; // 是否启用Player\n    this._repeatMode = 'off'; // off | on | one\n    this._shuffle = false; // true | false\n    this._reversed = false;\n    this._volume = 1; // 0 to 1\n    this._volumeBeforeMuted = 1; // 用于保存静音前的音量\n    this._personalFMLoading = false; // 是否正在私人FM中加载新的track\n    this._personalFMNextLoading = false; // 是否正在缓存私人FM的下一首歌曲\n\n    // 播放信息\n    this._list = []; // 播放列表\n    this._current = 0; // 当前播放歌曲在播放列表里的index\n    this._shuffledList = []; // 被随机打乱的播放列表，随机播放模式下会使用此播放列表\n    this._shuffledCurrent = 0; // 当前播放歌曲在随机列表里面的index\n    this._playlistSource = { type: 'album', id: 123 }; // 当前播放列表的信息\n    this._currentTrack = { id: 86827685 }; // 当前播放歌曲的详细信息\n    this._playNextList = []; // 当这个list不为空时，会优先播放这个list的歌\n    this._isPersonalFM = false; // 是否是私人FM模式\n    this._personalFMTrack = { id: 0 }; // 私人FM当前歌曲\n    this._personalFMNextTrack = {\n      id: 0,\n    }; // 私人FM下一首歌曲信息（为了快速加载下一首）\n\n    /**\n     * The blob records for cleanup.\n     *\n     * @private\n     * @type {string[]}\n     */\n    this.createdBlobRecords = [];\n\n    // howler (https://github.com/goldfire/howler.js)\n    this._howler = null;\n    Object.defineProperty(this, '_howler', {\n      enumerable: false,\n    });\n\n    // init\n    this._init();\n\n    window.yesplaymusic = {};\n    window.yesplaymusic.player = this;\n  }\n\n  get repeatMode() {\n    return this._repeatMode;\n  }\n  set repeatMode(mode) {\n    if (this._isPersonalFM) return;\n    if (!['off', 'on', 'one'].includes(mode)) {\n      console.warn(\"repeatMode: invalid args, must be 'on' | 'off' | 'one'\");\n      return;\n    }\n    this._repeatMode = mode;\n  }\n  get shuffle() {\n    return this._shuffle;\n  }\n  set shuffle(shuffle) {\n    if (this._isPersonalFM) return;\n    if (shuffle !== true && shuffle !== false) {\n      console.warn('shuffle: invalid args, must be Boolean');\n      return;\n    }\n    this._shuffle = shuffle;\n    if (shuffle) {\n      this._shuffleTheList();\n    }\n    // 同步当前歌曲在列表中的下标\n    this.current = this.list.indexOf(this.currentTrackID);\n  }\n  get reversed() {\n    return this._reversed;\n  }\n  set reversed(reversed) {\n    if (this._isPersonalFM) return;\n    if (reversed !== true && reversed !== false) {\n      console.warn('reversed: invalid args, must be Boolean');\n      return;\n    }\n    console.log('changing reversed to:', reversed);\n    this._reversed = reversed;\n  }\n  get volume() {\n    return this._volume;\n  }\n  set volume(volume) {\n    this._volume = volume;\n    this._howler?.volume(volume);\n  }\n  get list() {\n    return this.shuffle ? this._shuffledList : this._list;\n  }\n  set list(list) {\n    this._list = list;\n  }\n  get current() {\n    return this.shuffle ? this._shuffledCurrent : this._current;\n  }\n  set current(current) {\n    if (this.shuffle) {\n      this._shuffledCurrent = current;\n    } else {\n      this._current = current;\n    }\n  }\n  get enabled() {\n    return this._enabled;\n  }\n  get playing() {\n    return this._playing;\n  }\n  get currentTrack() {\n    return this._currentTrack;\n  }\n  get currentTrackID() {\n    return this._currentTrack?.id ?? 0;\n  }\n  get playlistSource() {\n    return this._playlistSource;\n  }\n  get playNextList() {\n    return this._playNextList;\n  }\n  get isPersonalFM() {\n    return this._isPersonalFM;\n  }\n  get personalFMTrack() {\n    return this._personalFMTrack;\n  }\n  get currentTrackDuration() {\n    const trackDuration = this._currentTrack.dt || 1000;\n    let duration = ~~(trackDuration / 1000);\n    return duration > 1 ? duration - 1 : duration;\n  }\n  get progress() {\n    return this._progress;\n  }\n  set progress(value) {\n    if (this._howler) {\n      this._howler.seek(value);\n      if (isCreateMpris) {\n        ipcRenderer?.send('seeked', this._howler.seek());\n      }\n    }\n  }\n  get isCurrentTrackLiked() {\n    return store.state.liked.songs.includes(this.currentTrack.id);\n  }\n\n  _init() {\n    this._loadSelfFromLocalStorage();\n    this._howler?.volume(this.volume);\n\n    if (this._enabled) {\n      // 恢复当前播放歌曲\n      this._replaceCurrentTrack(this.currentTrackID, false).then(() => {\n        this._howler?.seek(localStorage.getItem('playerCurrentTrackTime') ?? 0);\n      }); // update audio source and init howler\n      this._initMediaSession();\n    }\n\n    this._setIntervals();\n\n    // 初始化私人FM\n    if (\n      this._personalFMTrack.id === 0 ||\n      this._personalFMNextTrack.id === 0 ||\n      this._personalFMTrack.id === this._personalFMNextTrack.id\n    ) {\n      personalFM().then(result => {\n        this._personalFMTrack = result.data[0];\n        this._personalFMNextTrack = result.data[1];\n        return this._personalFMTrack;\n      });\n    }\n  }\n  _setPlaying(isPlaying) {\n    this._playing = isPlaying;\n    if (isCreateTray) {\n      ipcRenderer?.send('updateTrayPlayState', this._playing);\n    }\n  }\n  _setIntervals() {\n    // 同步播放进度\n    // TODO: 如果 _progress 在别的地方被改变了，\n    // 这个定时器会覆盖之前改变的值，是bug\n    setInterval(() => {\n      if (this._howler === null) return;\n      this._progress = this._howler.seek();\n      localStorage.setItem('playerCurrentTrackTime', this._progress);\n      if (isCreateMpris) {\n        ipcRenderer?.send('playerCurrentTrackTime', this._progress);\n      }\n    }, 1000);\n  }\n  _getNextTrack() {\n    const next = this._reversed ? this.current - 1 : this.current + 1;\n\n    if (this._playNextList.length > 0) {\n      let trackID = this._playNextList[0];\n      return [trackID, INDEX_IN_PLAY_NEXT];\n    }\n\n    // 循环模式开启，则重新播放当前模式下的相对的下一首\n    if (this.repeatMode === 'on') {\n      if (this._reversed && this.current === 0) {\n        // 倒序模式，当前歌曲是第一首，则重新播放列表最后一首\n        return [this.list[this.list.length - 1], this.list.length - 1];\n      } else if (this.list.length === this.current + 1) {\n        // 正序模式，当前歌曲是最后一首，则重新播放第一首\n        return [this.list[0], 0];\n      }\n    }\n\n    // 返回 [trackID, index]\n    return [this.list[next], next];\n  }\n  _getPrevTrack() {\n    const next = this._reversed ? this.current + 1 : this.current - 1;\n\n    // 循环模式开启，则重新播放当前模式下的相对的下一首\n    if (this.repeatMode === 'on') {\n      if (this._reversed && this.current === 0) {\n        // 倒序模式，当前歌曲是最后一首，则重新播放列表第一首\n        return [this.list[0], 0];\n      } else if (this.list.length === this.current + 1) {\n        // 正序模式，当前歌曲是第一首，则重新播放列表最后一首\n        return [this.list[this.list.length - 1], this.list.length - 1];\n      }\n    }\n\n    // 返回 [trackID, index]\n    return [this.list[next], next];\n  }\n  async _shuffleTheList(firstTrackID = this.currentTrackID) {\n    let list = this._list.filter(tid => tid !== firstTrackID);\n    if (firstTrackID === 'first') list = this._list;\n    this._shuffledList = shuffle(list);\n    if (firstTrackID !== 'first') this._shuffledList.unshift(firstTrackID);\n  }\n  async _scrobble(track, time, completed = false) {\n    console.debug(\n      `[debug][Player.js] scrobble track 👉 ${track.name} by ${track.ar[0].name} 👉 time:${time} completed: ${completed}`\n    );\n    const trackDuration = ~~(track.dt / 1000);\n    time = completed ? trackDuration : ~~time;\n    scrobble({\n      id: track.id,\n      sourceid: this.playlistSource.id,\n      time,\n    });\n    if (\n      store.state.lastfm.key !== undefined &&\n      (time >= trackDuration / 2 || time >= 240)\n    ) {\n      const timestamp = ~~(new Date().getTime() / 1000) - time;\n      trackScrobble({\n        artist: track.ar[0].name,\n        track: track.name,\n        timestamp,\n        album: track.al.name,\n        trackNumber: track.no,\n        duration: trackDuration,\n      });\n    }\n  }\n  _playAudioSource(source, autoplay = true) {\n    Howler.unload();\n    this._howler = new Howl({\n      src: [source],\n      html5: true,\n      preload: true,\n      format: ['mp3', 'flac'],\n      onend: () => {\n        this._nextTrackCallback();\n      },\n    });\n    this._howler.on('loaderror', (_, errCode) => {\n      // https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code\n      // code 3: MEDIA_ERR_DECODE\n      if (errCode === 3) {\n        this._playNextTrack(this._isPersonalFM);\n      } else if (errCode === 4) {\n        // code 4: MEDIA_ERR_SRC_NOT_SUPPORTED\n        store.dispatch('showToast', `无法播放: 不支持的音频格式`);\n        this._playNextTrack(this._isPersonalFM);\n      } else {\n        const t = this.progress;\n        this._replaceCurrentTrackAudio(this.currentTrack, false, false).then(\n          replaced => {\n            // 如果 replaced 为 false，代表当前的 track 已经不是这里想要替换的track\n            // 此时则不修改当前的歌曲进度\n            if (replaced) {\n              this._howler?.seek(t);\n              this.play();\n            }\n          }\n        );\n      }\n    });\n    if (autoplay) {\n      this.play();\n      if (this._currentTrack.name) {\n        setTitle(this._currentTrack);\n      }\n      setTrayLikeState(store.state.liked.songs.includes(this.currentTrack.id));\n    }\n    this.setOutputDevice();\n  }\n  _getAudioSourceBlobURL(data) {\n    // Create a new object URL.\n    const source = URL.createObjectURL(new Blob([data]));\n\n    // Clean up the previous object URLs since we've created a new one.\n    // Revoke object URLs can release the memory taken by a Blob,\n    // which occupied a large proportion of memory.\n    for (const url in this.createdBlobRecords) {\n      URL.revokeObjectURL(url);\n    }\n\n    // Then, we replace the createBlobRecords with new one with\n    // our newly created object URL.\n    this.createdBlobRecords = [source];\n\n    return source;\n  }\n  _getAudioSourceFromCache(id) {\n    return getTrackSource(id).then(t => {\n      if (!t) return null;\n      return this._getAudioSourceBlobURL(t.source);\n    });\n  }\n  _getAudioSourceFromNetease(track) {\n    if (isAccountLoggedIn()) {\n      return getMP3(track.id).then(result => {\n        if (!result.data[0]) return null;\n        if (!result.data[0].url) return null;\n        if (result.data[0].freeTrialInfo !== null) return null; // 跳过只能试听的歌曲\n        const source = result.data[0].url.replace(/^http:/, 'https:');\n        if (store.state.settings.automaticallyCacheSongs) {\n          cacheTrackSource(track, source, result.data[0].br);\n        }\n        return source;\n      });\n    } else {\n      return new Promise(resolve => {\n        resolve(`https://music.163.com/song/media/outer/url?id=${track.id}`);\n      });\n    }\n  }\n  async _getAudioSourceFromUnblockMusic(track) {\n    console.debug(`[debug][Player.js] _getAudioSourceFromUnblockMusic`);\n\n    if (\n      process.env.IS_ELECTRON !== true ||\n      store.state.settings.enableUnblockNeteaseMusic === false\n    ) {\n      return null;\n    }\n\n    /**\n     *\n     * @param {string=} searchMode\n     * @returns {import(\"@unblockneteasemusic/rust-napi\").SearchMode}\n     */\n    const determineSearchMode = searchMode => {\n      /**\n       * FastFirst = 0\n       * OrderFirst = 1\n       */\n      switch (searchMode) {\n        case 'fast-first':\n          return 0;\n        case 'order-first':\n          return 1;\n        default:\n          return 0;\n      }\n    };\n\n    const retrieveSongInfo = await ipcRenderer.invoke(\n      'unblock-music',\n      store.state.settings.unmSource,\n      track,\n      {\n        enableFlac: store.state.settings.unmEnableFlac || null,\n        proxyUri: store.state.settings.unmProxyUri || null,\n        searchMode: determineSearchMode(store.state.settings.unmSearchMode),\n        config: {\n          'joox:cookie': store.state.settings.unmJooxCookie || null,\n          'qq:cookie': store.state.settings.unmQQCookie || null,\n          'ytdl:exe': store.state.settings.unmYtDlExe || null,\n        },\n      }\n    );\n\n    if (store.state.settings.automaticallyCacheSongs && retrieveSongInfo?.url) {\n      // 对于来自 bilibili 的音源\n      // retrieveSongInfo.url 是音频数据的base64编码\n      // 其他音源为实际url\n      const url =\n        retrieveSongInfo.source === 'bilibili'\n          ? `data:application/octet-stream;base64,${retrieveSongInfo.url}`\n          : retrieveSongInfo.url;\n      cacheTrackSource(track, url, 128000, `unm:${retrieveSongInfo.source}`);\n    }\n\n    if (!retrieveSongInfo) {\n      return null;\n    }\n\n    if (retrieveSongInfo.source !== 'bilibili') {\n      return retrieveSongInfo.url;\n    }\n\n    const buffer = base642Buffer(retrieveSongInfo.url);\n    return this._getAudioSourceBlobURL(buffer);\n  }\n  _getAudioSource(track) {\n    return this._getAudioSourceFromCache(String(track.id))\n      .then(source => {\n        return source ?? this._getAudioSourceFromNetease(track);\n      })\n      .then(source => {\n        return source ?? this._getAudioSourceFromUnblockMusic(track);\n      });\n  }\n  _replaceCurrentTrack(\n    id,\n    autoplay = true,\n    ifUnplayableThen = UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK\n  ) {\n    if (autoplay && this._currentTrack.name) {\n      this._scrobble(this.currentTrack, this._howler?.seek());\n    }\n    return getTrackDetail(id).then(data => {\n      const track = data.songs[0];\n      this._currentTrack = track;\n      this._updateMediaSessionMetaData(track);\n      return this._replaceCurrentTrackAudio(\n        track,\n        autoplay,\n        true,\n        ifUnplayableThen\n      );\n    });\n  }\n  /**\n   * @returns 是否成功加载音频，并使用加载完成的音频替换了howler实例\n   */\n  _replaceCurrentTrackAudio(\n    track,\n    autoplay,\n    isCacheNextTrack,\n    ifUnplayableThen = UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK\n  ) {\n    return this._getAudioSource(track).then(source => {\n      if (source) {\n        let replaced = false;\n        if (track.id === this.currentTrackID) {\n          this._playAudioSource(source, autoplay);\n          replaced = true;\n        }\n        if (isCacheNextTrack) {\n          this._cacheNextTrack();\n        }\n        return replaced;\n      } else {\n        store.dispatch('showToast', `无法播放 ${track.name}`);\n        switch (ifUnplayableThen) {\n          case UNPLAYABLE_CONDITION.PLAY_NEXT_TRACK:\n            this._playNextTrack(this.isPersonalFM);\n            break;\n          case UNPLAYABLE_CONDITION.PLAY_PREV_TRACK:\n            this.playPrevTrack();\n            break;\n          default:\n            store.dispatch(\n              'showToast',\n              `undefined Unplayable condition: ${ifUnplayableThen}`\n            );\n            break;\n        }\n        return false;\n      }\n    });\n  }\n  _cacheNextTrack() {\n    let nextTrackID = this._isPersonalFM\n      ? this._personalFMNextTrack?.id ?? 0\n      : this._getNextTrack()[0];\n    if (!nextTrackID) return;\n    if (this._personalFMTrack.id == nextTrackID) return;\n    getTrackDetail(nextTrackID).then(data => {\n      let track = data.songs[0];\n      this._getAudioSource(track);\n    });\n  }\n  _loadSelfFromLocalStorage() {\n    const player = JSON.parse(localStorage.getItem('player'));\n    if (!player) return;\n    for (const [key, value] of Object.entries(player)) {\n      this[key] = value;\n    }\n  }\n  _initMediaSession() {\n    if ('mediaSession' in navigator) {\n      navigator.mediaSession.setActionHandler('play', () => {\n        this.play();\n      });\n      navigator.mediaSession.setActionHandler('pause', () => {\n        this.pause();\n      });\n      navigator.mediaSession.setActionHandler('previoustrack', () => {\n        this.playPrevTrack();\n      });\n      navigator.mediaSession.setActionHandler('nexttrack', () => {\n        this._playNextTrack(this.isPersonalFM);\n      });\n      navigator.mediaSession.setActionHandler('stop', () => {\n        this.pause();\n      });\n      navigator.mediaSession.setActionHandler('seekto', event => {\n        this.seek(event.seekTime);\n        this._updateMediaSessionPositionState();\n      });\n      navigator.mediaSession.setActionHandler('seekbackward', event => {\n        this.seek(this.seek() - (event.seekOffset || 10));\n        this._updateMediaSessionPositionState();\n      });\n      navigator.mediaSession.setActionHandler('seekforward', event => {\n        this.seek(this.seek() + (event.seekOffset || 10));\n        this._updateMediaSessionPositionState();\n      });\n    }\n  }\n  _updateMediaSessionMetaData(track) {\n    if ('mediaSession' in navigator === false) {\n      return;\n    }\n    let artists = track.ar.map(a => a.name);\n    const metadata = {\n      title: track.name,\n      artist: artists.join(','),\n      album: track.al.name,\n      artwork: [\n        {\n          src: track.al.picUrl + '?param=224y224',\n          type: 'image/jpg',\n          sizes: '224x224',\n        },\n        {\n          src: track.al.picUrl + '?param=512y512',\n          type: 'image/jpg',\n          sizes: '512x512',\n        },\n      ],\n      length: this.currentTrackDuration,\n      trackId: this.current,\n      url: '/trackid/' + track.id,\n    };\n\n    navigator.mediaSession.metadata = new window.MediaMetadata(metadata);\n    if (isCreateMpris) {\n      this._updateMprisState(track, metadata);\n    }\n  }\n  // OSDLyrics 会检测 Mpris 状态并寻找对应歌词文件，所以要在更新 Mpris 状态之前保证歌词下载完成\n  async _updateMprisState(track, metadata) {\n    if (!store.state.settings.enableOsdlyricsSupport) {\n      return ipcRenderer?.send('metadata', metadata);\n    }\n\n    let lyricContent = await getLyric(track.id);\n\n    if (!lyricContent.lrc || !lyricContent.lrc.lyric) {\n      return ipcRenderer?.send('metadata', metadata);\n    }\n\n    ipcRenderer.send('sendLyrics', {\n      track,\n      lyrics: lyricContent.lrc.lyric,\n    });\n\n    ipcRenderer.on('saveLyricFinished', () => {\n      ipcRenderer?.send('metadata', metadata);\n    });\n  }\n  _updateMediaSessionPositionState() {\n    if ('mediaSession' in navigator === false) {\n      return;\n    }\n    if ('setPositionState' in navigator.mediaSession) {\n      navigator.mediaSession.setPositionState({\n        duration: ~~(this.currentTrack.dt / 1000),\n        playbackRate: 1.0,\n        position: this.seek(),\n      });\n    }\n  }\n  _nextTrackCallback() {\n    this._scrobble(this._currentTrack, 0, true);\n    if (!this.isPersonalFM && this.repeatMode === 'one') {\n      this._replaceCurrentTrack(this.currentTrackID);\n    } else {\n      this._playNextTrack(this.isPersonalFM);\n    }\n  }\n  _loadPersonalFMNextTrack() {\n    if (this._personalFMNextLoading) {\n      return [false, undefined];\n    }\n    this._personalFMNextLoading = true;\n    return personalFM()\n      .then(result => {\n        if (!result || !result.data) {\n          this._personalFMNextTrack = undefined;\n        } else {\n          this._personalFMNextTrack = result.data[0];\n          this._cacheNextTrack(); // cache next track\n        }\n        this._personalFMNextLoading = false;\n        return [true, this._personalFMNextTrack];\n      })\n      .catch(() => {\n        this._personalFMNextTrack = undefined;\n        this._personalFMNextLoading = false;\n        return [false, this._personalFMNextTrack];\n      });\n  }\n  _playDiscordPresence(track, seekTime = 0) {\n    if (\n      process.env.IS_ELECTRON !== true ||\n      store.state.settings.enableDiscordRichPresence === false\n    ) {\n      return null;\n    }\n    let copyTrack = { ...track };\n    copyTrack.dt -= seekTime * 1000;\n    ipcRenderer?.send('playDiscordPresence', copyTrack);\n  }\n  _pauseDiscordPresence(track) {\n    if (\n      process.env.IS_ELECTRON !== true ||\n      store.state.settings.enableDiscordRichPresence === false\n    ) {\n      return null;\n    }\n    ipcRenderer?.send('pauseDiscordPresence', track);\n  }\n  _playNextTrack(isPersonal) {\n    if (isPersonal) {\n      this.playNextFMTrack();\n    } else {\n      this.playNextTrack();\n    }\n  }\n\n  appendTrack(trackID) {\n    this.list.append(trackID);\n  }\n  playNextTrack() {\n    // TODO: 切换歌曲时增加加载中的状态\n    const [trackID, index] = this._getNextTrack();\n    if (trackID === undefined) {\n      this._howler?.stop();\n      this._setPlaying(false);\n      return false;\n    }\n    let next = index;\n    if (index === INDEX_IN_PLAY_NEXT) {\n      this._playNextList.shift();\n      next = this.current;\n    }\n    this.current = next;\n    this._replaceCurrentTrack(trackID);\n    return true;\n  }\n  async playNextFMTrack() {\n    if (this._personalFMLoading) {\n      return false;\n    }\n\n    this._isPersonalFM = true;\n    if (!this._personalFMNextTrack) {\n      this._personalFMLoading = true;\n      let result = null;\n      let retryCount = 5;\n      for (; retryCount >= 0; retryCount--) {\n        result = await personalFM().catch(() => null);\n        if (!result) {\n          this._personalFMLoading = false;\n          store.dispatch('showToast', 'personal fm timeout');\n          return false;\n        }\n        if (result.data?.length > 0) {\n          break;\n        } else if (retryCount > 0) {\n          await delay(1000);\n        }\n      }\n      this._personalFMLoading = false;\n\n      if (retryCount < 0) {\n        let content = '获取私人FM数据时重试次数过多，请手动切换下一首';\n        store.dispatch('showToast', content);\n        console.log(content);\n        return false;\n      }\n      // 这里只能拿到一条数据\n      this._personalFMTrack = result.data[0];\n    } else {\n      if (this._personalFMNextTrack.id === this._personalFMTrack.id) {\n        return false;\n      }\n      this._personalFMTrack = this._personalFMNextTrack;\n    }\n    if (this._isPersonalFM) {\n      this._replaceCurrentTrack(this._personalFMTrack.id);\n    }\n    this._loadPersonalFMNextTrack();\n    return true;\n  }\n  playPrevTrack() {\n    const [trackID, index] = this._getPrevTrack();\n    if (trackID === undefined) return false;\n    this.current = index;\n    this._replaceCurrentTrack(\n      trackID,\n      true,\n      UNPLAYABLE_CONDITION.PLAY_PREV_TRACK\n    );\n    return true;\n  }\n  saveSelfToLocalStorage() {\n    let player = {};\n    for (let [key, value] of Object.entries(this)) {\n      if (excludeSaveKeys.includes(key)) continue;\n      player[key] = value;\n    }\n\n    localStorage.setItem('player', JSON.stringify(player));\n  }\n\n  pause() {\n    this._howler?.fade(this.volume, 0, PLAY_PAUSE_FADE_DURATION);\n\n    this._howler?.once('fade', () => {\n      this._howler?.pause();\n      this._setPlaying(false);\n      setTitle(null);\n      this._pauseDiscordPresence(this._currentTrack);\n    });\n  }\n  play() {\n    if (this._howler?.playing()) return;\n\n    this._howler?.play();\n\n    this._howler?.once('play', () => {\n      this._howler?.fade(0, this.volume, PLAY_PAUSE_FADE_DURATION);\n\n      // 播放时确保开启player.\n      // 避免因\"忘记设置\"导致在播放时播放器不显示的Bug\n      this._enabled = true;\n      this._setPlaying(true);\n      if (this._currentTrack.name) {\n        setTitle(this._currentTrack);\n      }\n      this._playDiscordPresence(this._currentTrack, this.seek());\n      if (store.state.lastfm.key !== undefined) {\n        trackUpdateNowPlaying({\n          artist: this.currentTrack.ar[0].name,\n          track: this.currentTrack.name,\n          album: this.currentTrack.al.name,\n          trackNumber: this.currentTrack.no,\n          duration: ~~(this.currentTrack.dt / 1000),\n        });\n      }\n    });\n  }\n  playOrPause() {\n    if (this._howler?.playing()) {\n      this.pause();\n    } else {\n      this.play();\n    }\n  }\n  seek(time = null, sendMpris = true) {\n    if (isCreateMpris && sendMpris && time) {\n      ipcRenderer?.send('seeked', time);\n    }\n    if (time !== null) {\n      this._howler?.seek(time);\n      if (this._playing)\n        this._playDiscordPresence(this._currentTrack, this.seek(null, false));\n    }\n    return this._howler === null ? 0 : this._howler.seek();\n  }\n  mute() {\n    if (this.volume === 0) {\n      this.volume = this._volumeBeforeMuted;\n    } else {\n      this._volumeBeforeMuted = this.volume;\n      this.volume = 0;\n    }\n  }\n  setOutputDevice() {\n    if (this._howler?._sounds.length <= 0 || !this._howler?._sounds[0]._node) {\n      return;\n    }\n    this._howler?._sounds[0]._node.setSinkId(store.state.settings.outputDevice);\n  }\n\n  replacePlaylist(\n    trackIDs,\n    playlistSourceID,\n    playlistSourceType,\n    autoPlayTrackID = 'first'\n  ) {\n    this._isPersonalFM = false;\n    this.list = trackIDs;\n    this.current = 0;\n    this._playlistSource = {\n      type: playlistSourceType,\n      id: playlistSourceID,\n    };\n    if (this.shuffle) this._shuffleTheList(autoPlayTrackID);\n    if (autoPlayTrackID === 'first') {\n      this._replaceCurrentTrack(this.list[0]);\n    } else {\n      this.current = this.list.indexOf(autoPlayTrackID);\n      this._replaceCurrentTrack(autoPlayTrackID);\n    }\n  }\n  playAlbumByID(id, trackID = 'first') {\n    getAlbum(id).then(data => {\n      let trackIDs = data.songs.map(t => t.id);\n      this.replacePlaylist(trackIDs, id, 'album', trackID);\n    });\n  }\n  playPlaylistByID(id, trackID = 'first', noCache = false) {\n    console.debug(\n      `[debug][Player.js] playPlaylistByID 👉 id:${id} trackID:${trackID} noCache:${noCache}`\n    );\n    getPlaylistDetail(id, noCache).then(data => {\n      let trackIDs = data.playlist.trackIds.map(t => t.id);\n      this.replacePlaylist(trackIDs, id, 'playlist', trackID);\n    });\n  }\n  playArtistByID(id, trackID = 'first') {\n    getArtist(id).then(data => {\n      let trackIDs = data.hotSongs.map(t => t.id);\n      this.replacePlaylist(trackIDs, id, 'artist', trackID);\n    });\n  }\n  playTrackOnListByID(id, listName = 'default') {\n    if (listName === 'default') {\n      this._current = this._list.findIndex(t => t === id);\n    }\n    this._replaceCurrentTrack(id);\n  }\n  playIntelligenceListById(id, trackID = 'first', noCache = false) {\n    getPlaylistDetail(id, noCache).then(data => {\n      const randomId = Math.floor(\n        Math.random() * (data.playlist.trackIds.length + 1)\n      );\n      const songId = data.playlist.trackIds[randomId].id;\n      intelligencePlaylist({ id: songId, pid: id }).then(result => {\n        let trackIDs = result.data.map(t => t.id);\n        this.replacePlaylist(trackIDs, id, 'playlist', trackID);\n      });\n    });\n  }\n  addTrackToPlayNext(trackID, playNow = false) {\n    this._playNextList.push(trackID);\n    if (playNow) {\n      this.playNextTrack();\n    }\n  }\n  playPersonalFM() {\n    this._isPersonalFM = true;\n    if (this.currentTrackID !== this._personalFMTrack.id) {\n      this._replaceCurrentTrack(this._personalFMTrack.id, true);\n    } else {\n      this.playOrPause();\n    }\n  }\n  async moveToFMTrash() {\n    this._isPersonalFM = true;\n    let id = this._personalFMTrack.id;\n    if (await this.playNextFMTrack()) {\n      fmTrash(id);\n    }\n  }\n\n  sendSelfToIpcMain() {\n    if (process.env.IS_ELECTRON !== true) return false;\n    let liked = store.state.liked.songs.includes(this.currentTrack.id);\n    ipcRenderer?.send('player', {\n      playing: this.playing,\n      likedCurrentTrack: liked,\n    });\n    setTrayLikeState(liked);\n  }\n\n  switchRepeatMode() {\n    if (this._repeatMode === 'on') {\n      this.repeatMode = 'one';\n    } else if (this._repeatMode === 'one') {\n      this.repeatMode = 'off';\n    } else {\n      this.repeatMode = 'on';\n    }\n    if (isCreateMpris) {\n      ipcRenderer?.send('switchRepeatMode', this.repeatMode);\n    }\n  }\n  switchShuffle() {\n    this.shuffle = !this.shuffle;\n    if (isCreateMpris) {\n      ipcRenderer?.send('switchShuffle', this.shuffle);\n    }\n  }\n  switchReversed() {\n    this.reversed = !this.reversed;\n  }\n\n  clearPlayNextList() {\n    this._playNextList = [];\n  }\n  removeTrackFromQueue(index) {\n    this._playNextList.splice(index, 1);\n  }\n}\n"
  },
  {
    "path": "src/utils/auth.js",
    "content": "import Cookies from 'js-cookie';\nimport { logout } from '@/api/auth';\nimport store from '@/store';\n\nexport function setCookies(string) {\n  const cookies = string.split(';;');\n  cookies.map(cookie => {\n    document.cookie = cookie;\n    const cookieKeyValue = cookie.split(';')[0].split('=');\n    localStorage.setItem(`cookie-${cookieKeyValue[0]}`, cookieKeyValue[1]);\n  });\n}\n\nexport function getCookie(key) {\n  return Cookies.get(key) ?? localStorage.getItem(`cookie-${key}`);\n}\n\nexport function removeCookie(key) {\n  Cookies.remove(key);\n  localStorage.removeItem(`cookie-${key}`);\n}\n\n// MUSIC_U 只有在账户登录的情况下才有\nexport function isLoggedIn() {\n  return getCookie('MUSIC_U') !== undefined;\n}\n\n// 账号登录\nexport function isAccountLoggedIn() {\n  return (\n    getCookie('MUSIC_U') !== undefined &&\n    store.state.data.loginMode === 'account'\n  );\n}\n\n// 用户名搜索（用户数据为只读）\nexport function isUsernameLoggedIn() {\n  return store.state.data.loginMode === 'username';\n}\n\n// 账户登录或者用户名搜索都判断为登录，宽松检查\nexport function isLooseLoggedIn() {\n  return isAccountLoggedIn() || isUsernameLoggedIn();\n}\n\nexport function doLogout() {\n  logout();\n  removeCookie('MUSIC_U');\n  removeCookie('__csrf');\n  // 更新状态仓库中的用户信息\n  store.commit('updateData', { key: 'user', value: {} });\n  // 更新状态仓库中的登录状态\n  store.commit('updateData', { key: 'loginMode', value: null });\n  // 更新状态仓库中的喜欢列表\n  store.commit('updateData', { key: 'likedSongPlaylistID', value: undefined });\n}\n"
  },
  {
    "path": "src/utils/base64.js",
    "content": "// https://github.com/niklasvh/base64-arraybuffer/blob/master/src/index.ts\n// Copyright (c) 2012 Niklas von Hertzen Licensed under the MIT license.\n\nconst chars =\n  'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';\n\n// Use a lookup table to find the index.\nconst lookup = typeof Uint8Array === 'undefined' ? [] : new Uint8Array(256);\nfor (let i = 0; i < chars.length; i++) {\n  lookup[chars.charCodeAt(i)] = i;\n}\n\nexport const encode = arraybuffer => {\n  let bytes = new Uint8Array(arraybuffer),\n    i,\n    len = bytes.length,\n    base64 = '';\n\n  for (i = 0; i < len; i += 3) {\n    base64 += chars[bytes[i] >> 2];\n    base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];\n    base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];\n    base64 += chars[bytes[i + 2] & 63];\n  }\n\n  if (len % 3 === 2) {\n    base64 = base64.substring(0, base64.length - 1) + '=';\n  } else if (len % 3 === 1) {\n    base64 = base64.substring(0, base64.length - 2) + '==';\n  }\n\n  return base64;\n};\n\nexport const decode = base64 => {\n  let bufferLength = base64.length * 0.75,\n    len = base64.length,\n    i,\n    p = 0,\n    encoded1,\n    encoded2,\n    encoded3,\n    encoded4;\n\n  if (base64[base64.length - 1] === '=') {\n    bufferLength--;\n    if (base64[base64.length - 2] === '=') {\n      bufferLength--;\n    }\n  }\n\n  const arraybuffer = new ArrayBuffer(bufferLength),\n    bytes = new Uint8Array(arraybuffer);\n\n  for (i = 0; i < len; i += 4) {\n    encoded1 = lookup[base64.charCodeAt(i)];\n    encoded2 = lookup[base64.charCodeAt(i + 1)];\n    encoded3 = lookup[base64.charCodeAt(i + 2)];\n    encoded4 = lookup[base64.charCodeAt(i + 3)];\n\n    bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);\n    bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);\n    bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);\n  }\n\n  return arraybuffer;\n};\n"
  },
  {
    "path": "src/utils/checkAuthToken.js",
    "content": "import os from 'os';\nimport fs from 'fs';\nimport path from 'path';\n\n// extract from NeteasyCloudMusicAPI/generateConfig.js and avoid bugs in there (generateConfig require main.js but the main.js has bugs)\nif (!fs.existsSync(path.resolve(os.tmpdir(), 'anonymous_token'))) {\n  fs.writeFileSync(path.resolve(os.tmpdir(), 'anonymous_token'), '', 'utf-8');\n}\n"
  },
  {
    "path": "src/utils/common.js",
    "content": "import { isAccountLoggedIn } from './auth';\nimport { refreshCookie } from '@/api/auth';\nimport dayjs from 'dayjs';\nimport store from '@/store';\n\nexport function isTrackPlayable(track) {\n  let result = {\n    playable: true,\n    reason: '',\n  };\n  if (track?.privilege?.pl > 0) {\n    return result;\n  }\n  // cloud storage judgement logic\n  if (isAccountLoggedIn() && track?.privilege?.cs) {\n    return result;\n  }\n  if (track.fee === 1 || track.privilege?.fee === 1) {\n    if (isAccountLoggedIn() && store.state.data.user.vipType === 11) {\n      result.playable = true;\n    } else {\n      result.playable = false;\n      result.reason = 'VIP Only';\n    }\n  } else if (track.fee === 4 || track.privilege?.fee === 4) {\n    result.playable = false;\n    result.reason = '付费专辑';\n  } else if (\n    track.noCopyrightRcmd !== null &&\n    track.noCopyrightRcmd !== undefined\n  ) {\n    result.playable = false;\n    result.reason = '无版权';\n  } else if (track.privilege?.st < 0 && isAccountLoggedIn()) {\n    result.playable = false;\n    result.reason = '已下架';\n  }\n  return result;\n}\n\nexport function mapTrackPlayableStatus(tracks, privileges = []) {\n  if (tracks?.length === undefined) return tracks;\n  return tracks.map(t => {\n    const privilege = privileges.find(item => item.id === t.id) || {};\n    if (t.privilege) {\n      Object.assign(t.privilege, privilege);\n    } else {\n      t.privilege = privilege;\n    }\n    let result = isTrackPlayable(t);\n    t.playable = result.playable;\n    t.reason = result.reason;\n    return t;\n  });\n}\n\nexport function randomNum(minNum, maxNum) {\n  switch (arguments.length) {\n    case 1:\n      return parseInt(Math.random() * minNum + 1, 10);\n    case 2:\n      return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);\n    default:\n      return 0;\n  }\n}\n\nexport function shuffleAList(list) {\n  let sortsList = list.map(t => t.sort);\n  for (let i = 1; i < sortsList.length; i++) {\n    const random = Math.floor(Math.random() * (i + 1));\n    [sortsList[i], sortsList[random]] = [sortsList[random], sortsList[i]];\n  }\n  let newSorts = {};\n  list.map(track => {\n    newSorts[track.id] = sortsList.pop();\n  });\n  return newSorts;\n}\n\nexport function throttle(fn, time) {\n  let isRun = false;\n  return function () {\n    if (isRun) return;\n    isRun = true;\n    fn.apply(this, arguments);\n    setTimeout(() => {\n      isRun = false;\n    }, time);\n  };\n}\n\nexport function updateHttps(url) {\n  if (!url) return '';\n  return url.replace(/^http:/, 'https:');\n}\n\nexport function dailyTask() {\n  let lastDate = store.state.data.lastRefreshCookieDate;\n  if (\n    isAccountLoggedIn() &&\n    (lastDate === undefined || lastDate !== dayjs().date())\n  ) {\n    console.debug('[debug][common.js] execute dailyTask');\n    refreshCookie().then(() => {\n      console.debug('[debug][common.js] 刷新cookie');\n      store.commit('updateData', {\n        key: 'lastRefreshCookieDate',\n        value: dayjs().date(),\n      });\n    });\n  }\n}\n\nexport function changeAppearance(appearance) {\n  if (appearance === 'auto' || appearance === undefined) {\n    appearance = window.matchMedia('(prefers-color-scheme: dark)').matches\n      ? 'dark'\n      : 'light';\n  }\n  document.body.setAttribute('data-theme', appearance);\n  document\n    .querySelector('meta[name=\"theme-color\"]')\n    .setAttribute('content', appearance === 'dark' ? '#222' : '#fff');\n}\n\nexport function splitSoundtrackAlbumTitle(title) {\n  let keywords = [\n    'Music from the Original Motion Picture Score',\n    'The Original Motion Picture Soundtrack',\n    'Original MGM Motion Picture Soundtrack',\n    'Complete Original Motion Picture Score',\n    'Original Music From The Motion Picture',\n    'Music From The Disney+ Original Movie',\n    'Original Music From The Netflix Film',\n    'Original Score to the Motion Picture',\n    'Original Motion Picture Soundtrack',\n    'Soundtrack from the Motion Picture',\n    'Original Television Soundtrack',\n    'Original Motion Picture Score',\n    'Music From the Motion Picture',\n    'Music From The Motion Picture',\n    'Complete Motion Picture Score',\n    'Music from the Motion Picture',\n    'Original Videogame Soundtrack',\n    'La Bande Originale du Film',\n    'Music from the Miniseries',\n    'Bande Originale du Film',\n    'Die Original Filmmusik',\n    'Original Soundtrack',\n    'Complete Score',\n    'Original Score',\n  ];\n  for (let keyword of keywords) {\n    if (title.includes(keyword) === false) continue;\n    return {\n      title: title\n        .replace(`(${keyword})`, '')\n        .replace(`: ${keyword}`, '')\n        .replace(`[${keyword}]`, '')\n        .replace(`- ${keyword}`, '')\n        .replace(`${keyword}`, ''),\n      subtitle: keyword,\n    };\n  }\n  return {\n    title: title,\n    subtitle: '',\n  };\n}\n\nexport function splitAlbumTitle(title) {\n  let keywords = [\n    'Bonus Tracks Edition',\n    'Complete Edition',\n    'Deluxe Edition',\n    'Deluxe Version',\n    'Tour Edition',\n  ];\n  for (let keyword of keywords) {\n    if (title.includes(keyword) === false) continue;\n    return {\n      title: title\n        .replace(`(${keyword})`, '')\n        .replace(`: ${keyword}`, '')\n        .replace(`[${keyword}]`, '')\n        .replace(`- ${keyword}`, '')\n        .replace(`${keyword}`, ''),\n      subtitle: keyword,\n    };\n  }\n  return {\n    title: title,\n    subtitle: '',\n  };\n}\n\nexport function bytesToSize(bytes) {\n  let marker = 1024; // Change to 1000 if required\n  let decimal = 2; // Change as required\n  let kiloBytes = marker;\n  let megaBytes = marker * marker;\n  let gigaBytes = marker * marker * marker;\n\n  let lang = store.state.settings.lang;\n\n  if (bytes < kiloBytes) return bytes + (lang === 'en' ? ' Bytes' : '字节');\n  else if (bytes < megaBytes)\n    return (bytes / kiloBytes).toFixed(decimal) + ' KB';\n  else if (bytes < gigaBytes)\n    return (bytes / megaBytes).toFixed(decimal) + ' MB';\n  else return (bytes / gigaBytes).toFixed(decimal) + ' GB';\n}\n\nexport function formatTrackTime(value) {\n  if (!value) return '';\n  let min = ~~(value / 60);\n  let sec = (~~(value % 60)).toString().padStart(2, '0');\n  return `${min}:${sec}`;\n}\n"
  },
  {
    "path": "src/utils/db.js",
    "content": "import axios from 'axios';\nimport Dexie from 'dexie';\nimport store from '@/store';\n// import pkg from \"../../package.json\";\n\nconst db = new Dexie('yesplaymusic');\n\ndb.version(4).stores({\n  trackDetail: '&id, updateTime',\n  lyric: '&id, updateTime',\n  album: '&id, updateTime',\n});\n\ndb.version(3)\n  .stores({\n    trackSources: '&id, createTime',\n  })\n  .upgrade(tx =>\n    tx\n      .table('trackSources')\n      .toCollection()\n      .modify(\n        track => !track.createTime && (track.createTime = new Date().getTime())\n      )\n  );\n\ndb.version(1).stores({\n  trackSources: '&id',\n});\n\nlet tracksCacheBytes = 0;\n\nasync function deleteExcessCache() {\n  if (\n    store.state.settings.cacheLimit === false ||\n    tracksCacheBytes < store.state.settings.cacheLimit * Math.pow(1024, 2)\n  ) {\n    return;\n  }\n  try {\n    const delCache = await db.trackSources.orderBy('createTime').first();\n    await db.trackSources.delete(delCache.id);\n    tracksCacheBytes -= delCache.source.byteLength;\n    console.debug(\n      `[debug][db.js] deleteExcessCacheSucces, track: ${delCache.name}, size: ${delCache.source.byteLength}, cacheSize:${tracksCacheBytes}`\n    );\n    deleteExcessCache();\n  } catch (error) {\n    console.debug('[debug][db.js] deleteExcessCacheFailed', error);\n  }\n}\n\nexport function cacheTrackSource(trackInfo, url, bitRate, from = 'netease') {\n  if (!process.env.IS_ELECTRON) return;\n  const name = trackInfo.name;\n  const artist =\n    (trackInfo.ar && trackInfo.ar[0]?.name) ||\n    (trackInfo.artists && trackInfo.artists[0]?.name) ||\n    'Unknown';\n  let cover = trackInfo.al.picUrl;\n  if (cover.slice(0, 5) !== 'https') {\n    cover = 'https' + cover.slice(4);\n  }\n  axios.get(`${cover}?param=512y512`);\n  axios.get(`${cover}?param=224y224`);\n  axios.get(`${cover}?param=1024y1024`);\n  return axios\n    .get(url, {\n      responseType: 'arraybuffer',\n    })\n    .then(response => {\n      db.trackSources.put({\n        id: trackInfo.id,\n        source: response.data,\n        bitRate,\n        from,\n        name,\n        artist,\n        createTime: new Date().getTime(),\n      });\n      console.debug(`[debug][db.js] cached track 👉 ${name} by ${artist}`);\n      tracksCacheBytes += response.data.byteLength;\n      deleteExcessCache();\n      return { trackID: trackInfo.id, source: response.data, bitRate };\n    });\n}\n\nexport function getTrackSource(id) {\n  return db.trackSources.get(Number(id)).then(track => {\n    if (!track) return null;\n    console.debug(\n      `[debug][db.js] get track from cache 👉 ${track.name} by ${track.artist}`\n    );\n    return track;\n  });\n}\n\nexport function cacheTrackDetail(track, privileges) {\n  db.trackDetail.put({\n    id: track.id,\n    detail: track,\n    privileges: privileges,\n    updateTime: new Date().getTime(),\n  });\n}\n\nexport function getTrackDetailFromCache(ids) {\n  return db.trackDetail\n    .filter(track => {\n      return ids.includes(String(track.id));\n    })\n    .toArray()\n    .then(tracks => {\n      const result = { songs: [], privileges: [] };\n      ids.map(id => {\n        const one = tracks.find(t => String(t.id) === id);\n        result.songs.push(one?.detail);\n        result.privileges.push(one?.privileges);\n      });\n      if (result.songs.includes(undefined)) {\n        return undefined;\n      }\n      return result;\n    });\n}\n\nexport function cacheLyric(id, lyrics) {\n  db.lyric.put({\n    id,\n    lyrics,\n    updateTime: new Date().getTime(),\n  });\n}\n\nexport function getLyricFromCache(id) {\n  return db.lyric.get(Number(id)).then(result => {\n    if (!result) return undefined;\n    return result.lyrics;\n  });\n}\n\nexport function cacheAlbum(id, album) {\n  db.album.put({\n    id: Number(id),\n    album,\n    updateTime: new Date().getTime(),\n  });\n}\n\nexport function getAlbumFromCache(id) {\n  return db.album.get(Number(id)).then(result => {\n    if (!result) return undefined;\n    return result.album;\n  });\n}\n\nexport function countDBSize() {\n  const trackSizes = [];\n  return db.trackSources\n    .each(track => {\n      trackSizes.push(track.source.byteLength);\n    })\n    .then(() => {\n      const res = {\n        bytes: trackSizes.reduce((s1, s2) => s1 + s2, 0),\n        length: trackSizes.length,\n      };\n      tracksCacheBytes = res.bytes;\n      console.debug(\n        `[debug][db.js] load tracksCacheBytes: ${tracksCacheBytes}`\n      );\n      return res;\n    });\n}\n\nexport function clearDB() {\n  return new Promise(resolve => {\n    db.tables.forEach(function (table) {\n      table.clear();\n    });\n    resolve();\n  });\n}\n"
  },
  {
    "path": "src/utils/filters.js",
    "content": "import Vue from 'vue';\nimport dayjs from 'dayjs';\nimport duration from 'dayjs/plugin/duration';\nimport relativeTime from 'dayjs/plugin/relativeTime';\nimport locale from '@/locale';\n\nVue.filter('formatTime', (Milliseconds, format = 'HH:MM:SS') => {\n  if (!Milliseconds) return '';\n  dayjs.extend(duration);\n  dayjs.extend(relativeTime);\n\n  let time = dayjs.duration(Milliseconds);\n  let hours = time.hours().toString();\n  let mins = time.minutes().toString();\n  let seconds = time.seconds().toString().padStart(2, '0');\n\n  if (format === 'HH:MM:SS') {\n    return hours !== '0'\n      ? `${hours}:${mins.padStart(2, '0')}:${seconds}`\n      : `${mins}:${seconds}`;\n  } else if (format === 'Human') {\n    let hoursUnit, minitesUnit;\n    switch (locale.locale) {\n      case 'zh-CN':\n        hoursUnit = '小时';\n        minitesUnit = '分钟';\n        break;\n      case 'zh-TW':\n        hoursUnit = '小時';\n        minitesUnit = '分鐘';\n        break;\n      default:\n        hoursUnit = 'hr';\n        minitesUnit = 'min';\n        break;\n    }\n    return hours !== '0'\n      ? `${hours} ${hoursUnit} ${mins} ${minitesUnit}`\n      : `${mins} ${minitesUnit}`;\n  }\n});\n\nVue.filter('formatDate', (timestamp, format = 'MMM D, YYYY') => {\n  if (!timestamp) return '';\n  if (locale.locale === 'zh-CN') format = 'YYYY年MM月DD日';\n  else if (locale.locale === 'zh-TW') format = 'YYYY年MM月DD日';\n  return dayjs(timestamp).format(format);\n});\n\nVue.filter('formatAlbumType', (type, album) => {\n  if (!type) return '';\n  if (type === 'EP/Single') {\n    return album.size === 1 ? 'Single' : 'EP';\n  } else if (type === 'Single') {\n    return 'Single';\n  } else if (type === '专辑') {\n    return 'Album';\n  } else {\n    return type;\n  }\n});\n\nVue.filter('resizeImage', (imgUrl, size = 512) => {\n  if (!imgUrl) return '';\n  let httpsImgUrl = imgUrl;\n  if (imgUrl.slice(0, 5) !== 'https') {\n    httpsImgUrl = 'https' + imgUrl.slice(4);\n  }\n  return `${httpsImgUrl}?param=${size}y${size}`;\n});\n\nVue.filter('formatPlayCount', count => {\n  if (!count) return '';\n  if (locale.locale === 'zh-CN') {\n    if (count > 100000000) {\n      return `${Math.floor((count / 100000000) * 100) / 100}亿`; // 2.32 亿\n    }\n    if (count > 100000) {\n      return `${Math.floor((count / 10000) * 10) / 10}万`; // 232.1 万\n    }\n    if (count > 10000) {\n      return `${Math.floor((count / 10000) * 100) / 100}万`; // 2.3 万\n    }\n    return count;\n  } else if (locale.locale === 'zh-TW') {\n    if (count > 100000000) {\n      return `${Math.floor((count / 100000000) * 100) / 100}億`; // 2.32 億\n    }\n    if (count > 100000) {\n      return `${Math.floor((count / 10000) * 10) / 10}萬`; // 232.1 萬\n    }\n    if (count > 10000) {\n      return `${Math.floor((count / 10000) * 100) / 100}萬`; // 2.3 萬\n    }\n    return count;\n  } else {\n    if (count > 10000000) {\n      return `${Math.floor((count / 1000000) * 10) / 10}M`; // 233.2M\n    }\n    if (count > 1000000) {\n      return `${Math.floor((count / 1000000) * 100) / 100}M`; // 2.3M\n    }\n    if (count > 1000) {\n      return `${Math.floor((count / 1000) * 100) / 100}K`; // 233.23K\n    }\n    return count;\n  }\n});\n\nVue.filter('toHttps', url => {\n  if (!url) return '';\n  return url.replace(/^http:/, 'https:');\n});\n"
  },
  {
    "path": "src/utils/lyrics.js",
    "content": "export function lyricParser(lrc) {\n  return {\n    lyric: parseLyric(lrc?.lrc?.lyric || ''),\n    tlyric: parseLyric(lrc?.tlyric?.lyric || ''),\n    romalyric: parseLyric(lrc?.romalrc?.lyric || ''),\n    lyricuser: lrc.lyricUser,\n    transuser: lrc.transUser,\n  };\n}\n\n// regexr.com/6e52n\nconst extractLrcRegex =\n  /^(?<lyricTimestamps>(?:\\[.+?\\])+)(?!\\[)(?<content>.+)$/gm;\nconst extractTimestampRegex =\n  /\\[(?<min>\\d+):(?<sec>\\d+)(?:\\.|:)*(?<ms>\\d+)*\\]/g;\n\n/**\n * @typedef {{time: number, rawTime: string, content: string}} ParsedLyric\n */\n\n/**\n * Parse the lyric string.\n *\n * @param {string} lrc The `lrc` input.\n * @returns {ParsedLyric[]} The parsed lyric.\n * @example parseLyric(\"[00:00.00] Hello, World!\\n[00:00.10] Test\\n\");\n */\nfunction parseLyric(lrc) {\n  /**\n   * A sorted list of parsed lyric and its timestamp.\n   *\n   * @type {ParsedLyric[]}\n   * @see binarySearch\n   */\n  const parsedLyrics = [];\n\n  /**\n   * Find the appropriate index to push our parsed lyric.\n   * @param {ParsedLyric} lyric\n   */\n  const binarySearch = lyric => {\n    let time = lyric.time;\n\n    let low = 0;\n    let high = parsedLyrics.length - 1;\n\n    while (low <= high) {\n      const mid = Math.floor((low + high) / 2);\n      const midTime = parsedLyrics[mid].time;\n      if (midTime === time) {\n        return mid;\n      } else if (midTime < time) {\n        low = mid + 1;\n      } else {\n        high = mid - 1;\n      }\n    }\n\n    return low;\n  };\n\n  for (const line of lrc.trim().matchAll(extractLrcRegex)) {\n    const { lyricTimestamps, content } = line.groups;\n\n    for (const timestamp of lyricTimestamps.matchAll(extractTimestampRegex)) {\n      const { min, sec, ms } = timestamp.groups;\n      const validMs = ms?.slice(0, 2) ?? '00';\n      const rawTime = `[${min}:${sec}.${validMs}]`;\n      const time = Number(min) * 60 + Number(sec) + Number(validMs) * 0.01;\n\n      /** @type {ParsedLyric} */\n      const parsedLyric = { rawTime, time, content: trimContent(content) };\n      parsedLyrics.splice(binarySearch(parsedLyric), 0, parsedLyric);\n    }\n  }\n\n  return parsedLyrics;\n}\n\n/**\n * @param {string} content\n * @returns {string}\n */\nfunction trimContent(content) {\n  let t = content.trim();\n  return t.length < 1 ? content : t;\n}\n\n/**\n * @param {string} lyric\n */\nexport async function copyLyric(lyric) {\n  const textToCopy = lyric;\n  if (navigator.clipboard && navigator.clipboard.writeText) {\n    try {\n      await navigator.clipboard.writeText(textToCopy);\n    } catch (err) {\n      alert('复制失败，请手动复制！');\n    }\n  } else {\n    const tempInput = document.createElement('textarea');\n    tempInput.value = textToCopy;\n    tempInput.style.position = 'absolute';\n    tempInput.style.left = '-9999px';\n    document.body.appendChild(tempInput);\n    tempInput.select();\n    try {\n      document.execCommand('copy');\n    } catch (err) {\n      alert('复制失败，请手动复制！');\n    }\n    document.body.removeChild(tempInput);\n  }\n}\n"
  },
  {
    "path": "src/utils/nativeAlert.js",
    "content": "/**\n * Returns an alert-like function that fits current runtime environment\n *\n * This function is amid to solve a electron bug on Windows, that, when\n * user dismissed a browser alert, <input> elements cannot be focused\n * for further editing unless switching to another window and then back\n *\n * @returns { (message:string) => void }\n * Built-in alert function for browser environment\n * A function wrapping {@link dialog.showMessageBoxSync} for electron environment\n *\n * @see {@link https://github.com/electron/electron/issues/19977} for upstream electron issue\n */\nconst nativeAlert = (() => {\n  if (process.env.IS_ELECTRON === true) {\n    const { dialog } = require('electron');\n    if (dialog) {\n      return message => {\n        var options = {\n          type: 'warning',\n          message,\n        };\n        dialog.showMessageBoxSync(null, options);\n      };\n    }\n  }\n  return alert;\n})();\n\nexport default nativeAlert;\n"
  },
  {
    "path": "src/utils/platform.js",
    "content": "export const isWindows = process.platform === 'win32';\nexport const isMac = process.platform === 'darwin';\nexport const isLinux = process.platform === 'linux';\nexport const isDevelopment = process.env.NODE_ENV === 'development';\n\nexport const isCreateTray = isWindows || isLinux || isDevelopment;\nexport const isCreateMpris = isLinux;\n"
  },
  {
    "path": "src/utils/playList.js",
    "content": "import router from '../router';\nimport state from '../store/state';\nimport {\n  recommendPlaylist,\n  dailyRecommendPlaylist,\n  getPlaylistDetail,\n} from '@/api/playlist';\nimport { isAccountLoggedIn } from '@/utils/auth';\n\nexport function hasListSource() {\n  return !state.player.isPersonalFM && state.player.playlistSource.id !== 0;\n}\n\nexport function goToListSource() {\n  router.push({ path: getListSourcePath() });\n}\n\nexport function getListSourcePath() {\n  if (state.player.playlistSource.id === state.data.likedSongPlaylistID) {\n    return '/library/liked-songs';\n  } else if (state.player.playlistSource.type === 'url') {\n    return state.player.playlistSource.id;\n  } else if (state.player.playlistSource.type === 'cloudDisk') {\n    return '/library';\n  } else {\n    return `/${state.player.playlistSource.type}/${state.player.playlistSource.id}`;\n  }\n}\n\nexport async function getRecommendPlayList(limit, removePrivateRecommand) {\n  if (isAccountLoggedIn()) {\n    const playlists = await Promise.all([\n      dailyRecommendPlaylist(),\n      recommendPlaylist({ limit }),\n    ]);\n    let recommend = playlists[0].recommend ?? [];\n    if (recommend.length) {\n      if (removePrivateRecommand) recommend = recommend.slice(1);\n      await replaceRecommendResult(recommend);\n    }\n    return recommend.concat(playlists[1].result).slice(0, limit);\n  } else {\n    const response = await recommendPlaylist({ limit });\n    return response.result;\n  }\n}\n\nasync function replaceRecommendResult(recommend) {\n  for (let r of recommend) {\n    if (specialPlaylist.indexOf(r.id) > -1) {\n      const data = await getPlaylistDetail(r.id, true);\n      const playlist = data.playlist;\n      if (playlist) {\n        r.name = playlist.name;\n        r.picUrl = playlist.coverImgUrl;\n      }\n    }\n  }\n}\n\nconst specialPlaylist = [3136952023, 2829883282, 2829816518, 2829896389];\n"
  },
  {
    "path": "src/utils/request.js",
    "content": "import router from '@/router';\nimport { doLogout, getCookie } from '@/utils/auth';\nimport axios from 'axios';\n\nlet baseURL = '';\n// Web 和 Electron 跑在不同端口避免同时启动时冲突\nif (process.env.IS_ELECTRON) {\n  if (process.env.NODE_ENV === 'production') {\n    baseURL = process.env.VUE_APP_ELECTRON_API_URL;\n  } else {\n    baseURL = process.env.VUE_APP_ELECTRON_API_URL_DEV;\n  }\n} else {\n  baseURL = process.env.VUE_APP_NETEASE_API_URL;\n}\n\nconst service = axios.create({\n  baseURL,\n  withCredentials: true,\n  timeout: 15000,\n});\n\nservice.interceptors.request.use(function (config) {\n  if (!config.params) config.params = {};\n  if (baseURL.length) {\n    if (\n      baseURL[0] !== '/' &&\n      !process.env.IS_ELECTRON &&\n      getCookie('MUSIC_U') !== null\n    ) {\n      config.params.cookie = `MUSIC_U=${getCookie('MUSIC_U')};`;\n    }\n  } else {\n    console.error(\"You must set up the baseURL in the service's config\");\n  }\n\n  if (!process.env.IS_ELECTRON && !config.url.includes('/login')) {\n    config.params.realIP = '211.161.244.70';\n  }\n\n  // Force real_ip\n  const enableRealIP = JSON.parse(\n    localStorage.getItem('settings')\n  ).enableRealIP;\n  const realIP = JSON.parse(localStorage.getItem('settings')).realIP;\n  if (process.env.VUE_APP_REAL_IP) {\n    config.params.realIP = process.env.VUE_APP_REAL_IP;\n  } else if (enableRealIP) {\n    config.params.realIP = realIP;\n  }\n\n  const proxy = JSON.parse(localStorage.getItem('settings')).proxyConfig;\n  if (['HTTP', 'HTTPS'].includes(proxy.protocol)) {\n    config.params.proxy = `${proxy.protocol}://${proxy.server}:${proxy.port}`;\n  }\n\n  return config;\n});\n\nservice.interceptors.response.use(\n  response => {\n    const res = response.data;\n    return res;\n  },\n  async error => {\n    /** @type {import('axios').AxiosResponse | null} */\n    let response;\n    let data;\n    if (error === 'TypeError: baseURL is undefined') {\n      response = error;\n      data = error;\n      console.error(\"You must set up the baseURL in the service's config\");\n    } else if (error.response) {\n      response = error.response;\n      data = response.data;\n    }\n\n    if (\n      response &&\n      typeof data === 'object' &&\n      data.code === 301 &&\n      data.msg === '需要登录'\n    ) {\n      console.warn('Token has expired. Logout now!');\n\n      // 登出帳戶\n      doLogout();\n\n      // 導向登入頁面\n      if (process.env.IS_ELECTRON === true) {\n        router.push({ name: 'loginAccount' });\n      } else {\n        router.push({ name: 'login' });\n      }\n    }\n  }\n);\n\nexport default service;\n"
  },
  {
    "path": "src/utils/shortcuts.js",
    "content": "// default shortcuts\n// for more info, check https://www.electronjs.org/docs/api/accelerator\n\nexport default [\n  {\n    id: 'play',\n    name: '播放/暂停',\n    shortcut: 'CommandOrControl+P',\n    globalShortcut: 'Alt+CommandOrControl+P',\n  },\n  {\n    id: 'next',\n    name: '下一首',\n    shortcut: 'CommandOrControl+Right',\n    globalShortcut: 'Alt+CommandOrControl+Right',\n  },\n  {\n    id: 'previous',\n    name: '上一首',\n    shortcut: 'CommandOrControl+Left',\n    globalShortcut: 'Alt+CommandOrControl+Left',\n  },\n  {\n    id: 'increaseVolume',\n    name: '增加音量',\n    shortcut: 'CommandOrControl+Up',\n    globalShortcut: 'Alt+CommandOrControl+Up',\n  },\n  {\n    id: 'decreaseVolume',\n    name: '减少音量',\n    shortcut: 'CommandOrControl+Down',\n    globalShortcut: 'Alt+CommandOrControl+Down',\n  },\n  {\n    id: 'like',\n    name: '喜欢歌曲',\n    shortcut: 'CommandOrControl+L',\n    globalShortcut: 'Alt+CommandOrControl+L',\n  },\n  {\n    id: 'minimize',\n    name: '隐藏/显示播放器',\n    shortcut: 'CommandOrControl+M',\n    globalShortcut: 'Alt+CommandOrControl+M',\n  },\n];\n"
  },
  {
    "path": "src/utils/staticData.js",
    "content": "export const byAppleMusic = [\n  {\n    coverImgUrl:\n      'https://p2.music.126.net/GvYQoflE99eoeGi9jG4Bsw==/109951165375336156.jpg',\n    name: 'Happy Hits',\n    id: 5278068783,\n  },\n  {\n    coverImgUrl:\n      'https://p2.music.126.net/5CJeYN35LnzRDsv5Lcs0-Q==/109951165374966765.jpg',\n    name: '\\u4e2d\\u563b\\u5408\\u74a7',\n    id: 5277771961,\n  },\n  {\n    coverImgUrl:\n      'https://p1.music.126.net/cPaBXr1wZSg86ddl47AK7Q==/109951165375130918.jpg',\n    name: 'Heartbreak Pop',\n    id: 5277965913,\n  },\n  {\n    coverImgUrl:\n      'https://p2.music.126.net/FDtX55P2NjccDna-LBj9PA==/109951165375065973.jpg',\n    name: 'Festival Bangers',\n    id: 5277969451,\n  },\n  {\n    coverImgUrl:\n      'https://p2.music.126.net/hC0q2dGbOWHVfg4nkhIXPg==/109951165374881177.jpg',\n    name: 'Bedtime Beats',\n    id: 5277778542,\n  },\n];\n\nexport const playlistCategories = [\n  {\n    name: '全部',\n    enable: true,\n    bigCat: 'static',\n  },\n  // {\n  //   name: \"For You\",\n  //   enable: true,\n  //   bigCat: \"static\",\n  // },\n  {\n    name: '推荐歌单',\n    enable: true,\n    bigCat: 'static',\n  },\n  // {\n  //   name: \"最新专辑\",\n  //   enable: false,\n  //   bigCat: \"static\",\n  // },\n  {\n    name: '精品歌单',\n    enable: true,\n    bigCat: 'static',\n  },\n  {\n    name: '官方',\n    enable: true,\n    bigCat: 'static',\n  },\n  {\n    name: '排行榜',\n    enable: true,\n    bigCat: 'static',\n  },\n  {\n    name: '华语',\n    enable: false,\n    bigCat: '语种',\n  },\n  {\n    name: '欧美',\n    enable: true,\n    bigCat: '语种',\n  },\n  {\n    name: '日语',\n    enable: false,\n    bigCat: '语种',\n  },\n  {\n    name: '韩语',\n    enable: false,\n    bigCat: '语种',\n  },\n  {\n    name: '粤语',\n    enable: false,\n    bigCat: '语种',\n  },\n  {\n    name: '流行',\n    enable: true,\n    bigCat: '风格',\n  },\n  {\n    name: '摇滚',\n    enable: true,\n    bigCat: '风格',\n  },\n  {\n    name: '民谣',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '电子',\n    enable: true,\n    bigCat: '风格',\n  },\n  {\n    name: '舞曲',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '说唱',\n    enable: true,\n    bigCat: '风格',\n  },\n  {\n    name: '轻音乐',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '爵士',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '乡村',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: 'R&B/Soul',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '古典',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '民族',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '英伦',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '金属',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '朋克',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '蓝调',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '雷鬼',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '世界音乐',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '拉丁',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: 'New Age',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '古风',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '后摇',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: 'Bossa Nova',\n    enable: false,\n    bigCat: '风格',\n  },\n  {\n    name: '清晨',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '夜晚',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '学习',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '工作',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '午休',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '下午茶',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '地铁',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '驾车',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '运动',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '旅行',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '散步',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '酒吧',\n    enable: false,\n    bigCat: '场景',\n  },\n  {\n    name: '怀旧',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '清新',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '浪漫',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '伤感',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '治愈',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '放松',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '孤独',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '感动',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '兴奋',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '快乐',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '安静',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '思念',\n    enable: false,\n    bigCat: '情感',\n  },\n  {\n    name: '综艺',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '影视原声',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: 'ACG',\n    enable: true,\n    bigCat: '主题',\n  },\n  {\n    name: '儿童',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '校园',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '游戏',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '70后',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '80后',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '90后',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '网络歌曲',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: 'KTV',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '经典',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '翻唱',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '吉他',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '钢琴',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '器乐',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '榜单',\n    enable: false,\n    bigCat: '主题',\n  },\n  {\n    name: '00后',\n    enable: false,\n    bigCat: '主题',\n  },\n];\n"
  },
  {
    "path": "src/utils/updateApp.js",
    "content": "import initLocalStorage from '@/store/initLocalStorage.js';\nimport pkg from '../../package.json';\n\nconst updateSetting = () => {\n  const parsedSettings = JSON.parse(localStorage.getItem('settings'));\n  const settings = {\n    ...initLocalStorage.settings,\n    ...parsedSettings,\n  };\n\n  if (\n    settings.shortcuts.length !== initLocalStorage.settings.shortcuts.length\n  ) {\n    // 当新增 shortcuts 时\n    const oldShortcutsId = settings.shortcuts.map(s => s.id);\n    const newShortcutsId = initLocalStorage.settings.shortcuts.filter(\n      s => oldShortcutsId.includes(s.id) === false\n    );\n    newShortcutsId.map(id => {\n      settings.shortcuts.push(\n        initLocalStorage.settings.shortcuts.find(s => s.id === id)\n      );\n    });\n  }\n\n  if (localStorage.getItem('appVersion') === '\"0.3.9\"') {\n    settings.lyricsBackground = true;\n  }\n\n  localStorage.setItem('settings', JSON.stringify(settings));\n};\n\nconst updateData = () => {\n  const parsedData = JSON.parse(localStorage.getItem('data'));\n  const data = {\n    ...parsedData,\n  };\n  localStorage.setItem('data', JSON.stringify(data));\n};\n\nconst updatePlayer = () => {\n  let parsedData = JSON.parse(localStorage.getItem('player'));\n  let appVersion = localStorage.getItem('appVersion');\n  if (appVersion === `\"0.2.5\"`) parsedData = {}; // 0.2.6版本重构了player\n  const data = {\n    ...parsedData,\n  };\n  localStorage.setItem('player', JSON.stringify(data));\n};\n\nconst removeOldStuff = () => {\n  // remove old indexedDB databases created by localforage\n  indexedDB.deleteDatabase('tracks');\n};\n\nexport default function () {\n  updateSetting();\n  updateData();\n  updatePlayer();\n  removeOldStuff();\n  localStorage.setItem('appVersion', JSON.stringify(pkg.version));\n}\n"
  },
  {
    "path": "src/views/album.vue",
    "content": "<template>\n  <div v-show=\"show\" class=\"album-page\">\n    <div class=\"playlist-info\">\n      <Cover\n        :id=\"album.id\"\n        :image-url=\"album.picUrl | resizeImage(1024)\"\n        :show-play-button=\"true\"\n        :always-show-shadow=\"true\"\n        :click-cover-to-play=\"true\"\n        :fixed-size=\"288\"\n        type=\"album\"\n        :cover-hover=\"false\"\n        :play-button-size=\"18\"\n        @click.right.native=\"openMenu\"\n      />\n      <div class=\"info\">\n        <div class=\"title\" @click.right=\"openMenu\"> {{ title }}</div>\n        <div v-if=\"subtitle !== ''\" class=\"subtitle\" @click.right=\"openMenu\">{{\n          subtitle\n        }}</div>\n        <div class=\"artist\">\n          <span v-if=\"album.artist.id !== 104700\">\n            <span>{{ album.type | formatAlbumType(album) }} by </span\n            ><router-link :to=\"`/artist/${album.artist.id}`\">{{\n              album.artist.name\n            }}</router-link></span\n          >\n          <span v-else>Compilation by Various Artists</span>\n        </div>\n        <div class=\"date-and-count\">\n          <span\n            v-if=\"(album.mark & 1048576) === 1048576\"\n            class=\"explicit-symbol\"\n            ><ExplicitSymbol\n          /></span>\n          <span :title=\"album.publishTime | formatDate\">{{\n            new Date(album.publishTime).getFullYear()\n          }}</span>\n          <span> · {{ album.size }} {{ $t('common.songs') }}</span\n          >,\n          {{ albumTime | formatTime('Human') }}\n        </div>\n        <div class=\"description\" @click=\"toggleFullDescription\">\n          {{ album.description }}\n        </div>\n        <div class=\"buttons\" style=\"margin-top: 32px\">\n          <ButtonTwoTone\n            icon-class=\"play\"\n            @click.native=\"playAlbumByID(album.id)\"\n          >\n            {{ $t('common.play') }}\n          </ButtonTwoTone>\n          <ButtonTwoTone\n            :icon-class=\"dynamicDetail.isSub ? 'heart-solid' : 'heart'\"\n            :icon-button=\"true\"\n            :horizontal-padding=\"0\"\n            :color=\"dynamicDetail.isSub ? 'blue' : 'grey'\"\n            :text-color=\"dynamicDetail.isSub ? '#335eea' : ''\"\n            :background-color=\"\n              dynamicDetail.isSub ? 'var(--color-secondary-bg)' : ''\n            \"\n            @click.native=\"likeAlbum\"\n          >\n          </ButtonTwoTone>\n          <ButtonTwoTone\n            icon-class=\"more\"\n            :icon-button=\"true\"\n            :horizontal-padding=\"0\"\n            color=\"grey\"\n            @click.native=\"openMenu\"\n          >\n          </ButtonTwoTone>\n        </div>\n      </div>\n    </div>\n    <div v-if=\"tracksByDisc.length > 1\">\n      <div v-for=\"item in tracksByDisc\" :key=\"item.disc\">\n        <h2 class=\"disc\">Disc {{ item.disc }}</h2>\n        <TrackList\n          :id=\"album.id\"\n          :tracks=\"item.tracks\"\n          :type=\"'album'\"\n          :album-object=\"album\"\n        />\n      </div>\n    </div>\n    <div v-else>\n      <TrackList\n        :id=\"album.id\"\n        :tracks=\"tracks\"\n        :type=\"'album'\"\n        :album-object=\"album\"\n      />\n    </div>\n    <div class=\"extra-info\">\n      <div class=\"album-time\"></div>\n      <div class=\"release-date\">\n        {{ $t('album.released') }}\n        {{ album.publishTime | formatDate('MMMM D, YYYY') }}\n      </div>\n      <div v-if=\"album.company\" class=\"copyright\"> © {{ album.company }} </div>\n    </div>\n    <div v-if=\"filteredMoreAlbums.length !== 0\" class=\"more-by\">\n      <div class=\"section-title\">\n        More by\n        <router-link :to=\"`/artist/${album.artist.id}`\"\n          >{{ album.artist.name }}\n        </router-link>\n      </div>\n      <div>\n        <CoverRow\n          type=\"album\"\n          :items=\"filteredMoreAlbums\"\n          sub-text=\"albumType+releaseYear\"\n        />\n      </div>\n    </div>\n    <Modal\n      :show=\"showFullDescription\"\n      :close=\"toggleFullDescription\"\n      :show-footer=\"false\"\n      :click-outside-hide=\"true\"\n      :title=\"$t('album.albumDesc')\"\n    >\n      <p class=\"description-fulltext\">\n        {{ album.description }}\n      </p>\n    </Modal>\n    <ContextMenu ref=\"albumMenu\">\n      <!-- <div class=\"item\">{{ $t('contextMenu.addToQueue') }}</div> -->\n      <div class=\"item\" @click=\"likeAlbum(true)\">{{\n        dynamicDetail.isSub\n          ? $t('contextMenu.removeFromLibrary')\n          : $t('contextMenu.saveToLibrary')\n      }}</div>\n      <div class=\"item\">{{ $t('contextMenu.addToPlaylist') }}</div>\n      <div class=\"item\" @click=\"copyUrl(album.id)\">{{\n        $t('contextMenu.copyUrl')\n      }}</div>\n      <div class=\"item\" @click=\"openInBrowser(album.id)\">{{\n        $t('contextMenu.openInBrowser')\n      }}</div>\n    </ContextMenu>\n  </div>\n</template>\n\n<script>\nimport { mapMutations, mapActions, mapState } from 'vuex';\nimport { getArtistAlbum } from '@/api/artist';\nimport { getTrackDetail } from '@/api/track';\nimport { getAlbum, albumDynamicDetail, likeAAlbum } from '@/api/album';\nimport locale from '@/locale';\nimport { splitSoundtrackAlbumTitle, splitAlbumTitle } from '@/utils/common';\nimport NProgress from 'nprogress';\nimport { isAccountLoggedIn } from '@/utils/auth';\nimport { groupBy, toPairs, sortBy } from 'lodash';\n\nimport ExplicitSymbol from '@/components/ExplicitSymbol.vue';\nimport ButtonTwoTone from '@/components/ButtonTwoTone.vue';\nimport ContextMenu from '@/components/ContextMenu.vue';\nimport TrackList from '@/components/TrackList.vue';\nimport CoverRow from '@/components/CoverRow.vue';\nimport Cover from '@/components/Cover.vue';\nimport Modal from '@/components/Modal.vue';\n\nexport default {\n  name: 'Album',\n  components: {\n    Cover,\n    ButtonTwoTone,\n    TrackList,\n    ExplicitSymbol,\n    CoverRow,\n    Modal,\n    ContextMenu,\n  },\n  beforeRouteUpdate(to, from, next) {\n    this.show = false;\n    this.loadData(to.params.id);\n    next();\n  },\n  data() {\n    return {\n      show: false,\n      album: {\n        id: 0,\n        picUrl: '',\n        artist: {\n          id: 0,\n        },\n      },\n      tracks: [],\n      showFullDescription: false,\n      moreAlbums: [],\n      dynamicDetail: {},\n      subtitle: '',\n      title: '',\n    };\n  },\n  computed: {\n    ...mapState(['player', 'data']),\n    albumTime() {\n      let time = 0;\n      this.tracks.map(t => (time = time + t.dt));\n      return time;\n    },\n    filteredMoreAlbums() {\n      let moreAlbums = this.moreAlbums.filter(a => a.id !== this.album.id);\n      let realAlbums = moreAlbums.filter(a => a.type === '专辑');\n      let eps = moreAlbums.filter(\n        a => a.type === 'EP' || (a.type === 'EP/Single' && a.size > 1)\n      );\n      let restItems = moreAlbums.filter(\n        a =>\n          realAlbums.find(a1 => a1.id === a.id) === undefined &&\n          eps.find(a1 => a1.id === a.id) === undefined\n      );\n      if (realAlbums.length === 0) {\n        return [...realAlbums, ...eps, ...restItems].slice(0, 5);\n      } else {\n        return [...realAlbums, ...restItems].slice(0, 5);\n      }\n    },\n    tracksByDisc() {\n      if (this.tracks.length <= 1) return [];\n      const pairs = toPairs(groupBy(this.tracks, 'cd'));\n      return sortBy(pairs, p => p[0]).map(items => ({\n        disc: items[0],\n        tracks: items[1],\n      }));\n    },\n  },\n  created() {\n    this.loadData(this.$route.params.id);\n  },\n  methods: {\n    ...mapMutations(['appendTrackToPlayerList']),\n    ...mapActions(['playFirstTrackOnList', 'playTrackOnListByID', 'showToast']),\n    playAlbumByID(id, trackID = 'first') {\n      this.$store.state.player.playAlbumByID(id, trackID);\n    },\n    likeAlbum(toast = false) {\n      if (!isAccountLoggedIn()) {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      likeAAlbum({\n        id: this.album.id,\n        t: this.dynamicDetail.isSub ? 0 : 1,\n      })\n        .then(data => {\n          if (data.code === 200) {\n            this.dynamicDetail.isSub = !this.dynamicDetail.isSub;\n            if (toast === true)\n              this.showToast(\n                this.dynamicDetail.isSub ? '已保存到音乐库' : '已从音乐库删除'\n              );\n          }\n        })\n        .catch(error => {\n          this.showToast(`${error.response.data.message || error}`);\n        });\n    },\n    formatTitle() {\n      let splitTitle = splitSoundtrackAlbumTitle(this.album.name);\n      let splitTitle2 = splitAlbumTitle(splitTitle.title);\n      this.title = splitTitle2.title;\n      if (splitTitle.subtitle !== '' && splitTitle2.subtitle !== '') {\n        this.subtitle = splitTitle.subtitle + ' · ' + splitTitle2.subtitle;\n      } else {\n        this.subtitle =\n          splitTitle.subtitle === ''\n            ? splitTitle2.subtitle\n            : splitTitle.subtitle;\n      }\n    },\n    loadData(id) {\n      setTimeout(() => {\n        if (!this.show) NProgress.start();\n      }, 1000);\n      getAlbum(id).then(data => {\n        this.album = data.album;\n        this.tracks = data.songs;\n        this.formatTitle();\n        NProgress.done();\n        this.show = true;\n\n        // to get explicit mark\n        let trackIDs = this.tracks.map(t => t.id);\n        getTrackDetail(trackIDs.join(',')).then(data => {\n          this.tracks = data.songs;\n        });\n\n        // get more album by this artist\n        getArtistAlbum({ id: this.album.artist.id, limit: 100 }).then(data => {\n          this.moreAlbums = data.hotAlbums;\n        });\n      });\n      albumDynamicDetail(id).then(data => {\n        this.dynamicDetail = data;\n      });\n    },\n    toggleFullDescription() {\n      this.showFullDescription = !this.showFullDescription;\n      if (this.showFullDescription) {\n        this.$store.commit('enableScrolling', false);\n      } else {\n        this.$store.commit('enableScrolling', true);\n      }\n    },\n    openMenu(e) {\n      this.$refs.albumMenu.openMenu(e);\n    },\n    copyUrl(id) {\n      let showToast = this.showToast;\n      this.$copyText(`https://music.163.com/#/album?id=${id}`)\n        .then(function () {\n          showToast(locale.t('toast.copied'));\n        })\n        .catch(error => {\n          showToast(`${locale.t('toast.copyFailed')}${error}`);\n        });\n    },\n    openInBrowser(id) {\n      const url = `https://music.163.com/#/album?id=${id}`;\n      window.open(url);\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.album-page {\n  margin-top: 32px;\n}\n.playlist-info {\n  display: flex;\n  width: 78vw;\n  margin-bottom: 72px;\n  .info {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    flex: 1;\n    margin-left: 56px;\n    color: var(--color-text);\n    .title {\n      font-size: 56px;\n      font-weight: 700;\n    }\n    .subtitle {\n      font-size: 22px;\n      font-weight: 600;\n    }\n    .artist {\n      font-size: 18px;\n      opacity: 0.88;\n      margin-top: 24px;\n      a {\n        font-weight: 600;\n      }\n    }\n    .date-and-count {\n      font-size: 14px;\n      opacity: 0.68;\n      margin-top: 2px;\n    }\n    .description {\n      user-select: none;\n      font-size: 14px;\n      opacity: 0.68;\n      margin-top: 24px;\n      display: -webkit-box;\n      -webkit-box-orient: vertical;\n      -webkit-line-clamp: 3;\n      overflow: hidden;\n      cursor: pointer;\n      white-space: pre-line;\n      &:hover {\n        transition: opacity 0.3s;\n        opacity: 0.88;\n      }\n    }\n    .buttons {\n      margin-top: 32px;\n      display: flex;\n      button {\n        margin-right: 16px;\n      }\n    }\n  }\n}\n.disc {\n  color: var(--color-text);\n}\n\n.explicit-symbol {\n  opacity: 0.28;\n  color: var(--color-text);\n  margin-right: 4px;\n  .svg-icon {\n    margin-bottom: -3px;\n  }\n}\n\n.extra-info {\n  margin-top: 36px;\n  margin-bottom: 36px;\n  font-size: 12px;\n  opacity: 0.48;\n  color: var(--color-text);\n  div {\n    margin-bottom: 4px;\n  }\n  .album-time {\n    opacity: 0.68;\n  }\n}\n\n.more-by {\n  border-top: 1px solid rgba(128, 128, 128, 0.18);\n\n  padding-top: 22px;\n  .section-title {\n    font-size: 22px;\n    font-weight: 600;\n    opacity: 0.88;\n    color: var(--color-text);\n    margin-bottom: 20px;\n  }\n}\n.description-fulltext {\n  font-size: 16px;\n  margin-top: 24px;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n  white-space: pre-line;\n}\n</style>\n"
  },
  {
    "path": "src/views/artist.vue",
    "content": "<template>\n  <div v-show=\"show\" class=\"artist-page\">\n    <div class=\"artist-info\">\n      <div class=\"head\">\n        <img :src=\"artist.img1v1Url | resizeImage(1024)\" loading=\"lazy\" />\n      </div>\n      <div>\n        <div class=\"name\">{{ artist.name }}</div>\n        <div class=\"artist\">{{ $t('artist.artist') }}</div>\n        <div class=\"statistics\">\n          <a @click=\"scrollTo('popularTracks')\"\n            >{{ artist.musicSize }} {{ $t('common.songs') }}</a\n          >\n          ·\n          <a @click=\"scrollTo('seeMore', 'start')\"\n            >{{ artist.albumSize }} {{ $t('artist.withAlbums') }}</a\n          >\n          ·\n          <a @click=\"scrollTo('mvs')\"\n            >{{ artist.mvSize }} {{ $t('artist.videos') }}</a\n          >\n        </div>\n        <div class=\"description\" @click=\"toggleFullDescription\">\n          {{ artist.briefDesc }}\n        </div>\n        <div class=\"buttons\">\n          <ButtonTwoTone icon-class=\"play\" @click.native=\"playPopularSongs()\">\n            {{ $t('common.play') }}\n          </ButtonTwoTone>\n          <ButtonTwoTone color=\"grey\" @click.native=\"followArtist\">\n            <span v-if=\"artist.followed\">{{ $t('artist.following') }}</span>\n            <span v-else>{{ $t('artist.follow') }}</span>\n          </ButtonTwoTone>\n          <ButtonTwoTone\n            icon-class=\"more\"\n            :icon-button=\"true\"\n            :horizontal-padding=\"0\"\n            color=\"grey\"\n            @click.native=\"openMenu\"\n          >\n          </ButtonTwoTone>\n        </div>\n      </div>\n    </div>\n    <div v-if=\"latestRelease !== undefined\" class=\"latest-release\">\n      <div class=\"section-title\">{{ $t('artist.latestRelease') }}</div>\n      <div class=\"release\">\n        <div class=\"container\">\n          <Cover\n            :id=\"latestRelease.id\"\n            :image-url=\"latestRelease.picUrl | resizeImage\"\n            type=\"album\"\n            :fixed-size=\"128\"\n            :play-button-size=\"30\"\n          />\n          <div class=\"info\">\n            <div class=\"name\">\n              <router-link :to=\"`/album/${latestRelease.id}`\">{{\n                latestRelease.name\n              }}</router-link>\n            </div>\n            <div class=\"date\">\n              {{ latestRelease.publishTime | formatDate }}\n            </div>\n            <div class=\"type\">\n              {{ latestRelease.type | formatAlbumType(latestRelease) }} ·\n              {{ latestRelease.size }} {{ $t('common.songs') }}\n            </div>\n          </div>\n        </div>\n        <div v-show=\"latestMV.id\" class=\"container latest-mv\">\n          <div\n            class=\"cover\"\n            @mouseover=\"mvHover = true\"\n            @mouseleave=\"mvHover = false\"\n            @click=\"goToMv(latestMV.id)\"\n          >\n            <img :src=\"latestMV.coverUrl\" loading=\"lazy\" />\n            <transition name=\"fade\">\n              <div\n                v-show=\"mvHover\"\n                class=\"shadow\"\n                :style=\"{\n                  background: 'url(' + latestMV.coverUrl + ')',\n                }\"\n              ></div>\n            </transition>\n          </div>\n          <div class=\"info\">\n            <div class=\"name\">\n              <router-link :to=\"'/mv/' + latestMV.id\">{{\n                latestMV.name\n              }}</router-link>\n            </div>\n            <div class=\"date\">\n              {{ latestMV.publishTime | formatDate }}\n            </div>\n            <div class=\"type\">{{ $t('artist.latestMV') }}</div>\n          </div>\n        </div>\n        <div v-show=\"!latestMV.id\"></div>\n      </div>\n    </div>\n    <div id=\"popularTracks\" class=\"popular-tracks\">\n      <div class=\"section-title\">{{ $t('artist.popularSongs') }}</div>\n      <TrackList\n        :tracks=\"popularTracks.slice(0, showMorePopTracks ? 24 : 12)\"\n        :type=\"'tracklist'\"\n      />\n\n      <div id=\"seeMore\" class=\"show-more\">\n        <button @click=\"showMorePopTracks = !showMorePopTracks\">\n          <span v-show=\"!showMorePopTracks\">{{ $t('artist.showMore') }}</span>\n          <span v-show=\"showMorePopTracks\">{{ $t('artist.showLess') }}</span>\n        </button>\n      </div>\n    </div>\n    <div v-if=\"albums.length !== 0\" id=\"albums\" class=\"albums\">\n      <div class=\"section-title\">{{ $t('artist.albums') }}</div>\n      <CoverRow\n        :type=\"'album'\"\n        :items=\"albums\"\n        :sub-text=\"'releaseYear'\"\n        :show-play-button=\"true\"\n      />\n    </div>\n    <div v-if=\"mvs.length !== 0\" id=\"mvs\" class=\"mvs\">\n      <div class=\"section-title\"\n        >MVs\n        <router-link v-show=\"hasMoreMV\" :to=\"`/artist/${artist.id}/mv`\">{{\n          $t('home.seeMore')\n        }}</router-link>\n      </div>\n      <MvRow :mvs=\"mvs\" subtitle=\"publishTime\" />\n    </div>\n    <div v-if=\"eps.length !== 0\" class=\"eps\">\n      <div class=\"section-title\">{{ $t('artist.EPsSingles') }}</div>\n      <CoverRow\n        :type=\"'album'\"\n        :items=\"eps\"\n        :sub-text=\"'albumType+releaseYear'\"\n        :show-play-button=\"true\"\n      />\n    </div>\n\n    <div v-if=\"similarArtists.length !== 0\" class=\"similar-artists\">\n      <div class=\"section-title\">{{ $t('artist.similarArtists') }}</div>\n      <CoverRow\n        type=\"artist\"\n        :column-number=\"6\"\n        gap=\"36px 28px\"\n        :items=\"similarArtists.slice(0, 12)\"\n      />\n    </div>\n\n    <Modal\n      :show=\"showFullDescription\"\n      :close=\"toggleFullDescription\"\n      :show-footer=\"false\"\n      :click-outside-hide=\"true\"\n      :title=\"$t('artist.artistDesc')\"\n    >\n      <p class=\"description-fulltext\">\n        {{ artist.briefDesc }}\n      </p>\n    </Modal>\n\n    <ContextMenu ref=\"artistMenu\">\n      <div class=\"item\" @click=\"copyUrl(artist.id)\">{{\n        $t('contextMenu.copyUrl')\n      }}</div>\n      <div class=\"item\" @click=\"openInBrowser(artist.id)\">{{\n        $t('contextMenu.openInBrowser')\n      }}</div>\n    </ContextMenu>\n  </div>\n</template>\n\n<script>\nimport { mapMutations, mapActions, mapState } from 'vuex';\nimport {\n  getArtist,\n  getArtistAlbum,\n  artistMv,\n  followAArtist,\n  similarArtists,\n} from '@/api/artist';\nimport { getTrackDetail } from '@/api/track';\nimport locale from '@/locale';\nimport { isAccountLoggedIn } from '@/utils/auth';\nimport NProgress from 'nprogress';\n\nimport ButtonTwoTone from '@/components/ButtonTwoTone.vue';\nimport ContextMenu from '@/components/ContextMenu.vue';\nimport TrackList from '@/components/TrackList.vue';\nimport CoverRow from '@/components/CoverRow.vue';\nimport Cover from '@/components/Cover.vue';\nimport MvRow from '@/components/MvRow.vue';\nimport Modal from '@/components/Modal.vue';\n\nexport default {\n  name: 'Artist',\n  components: {\n    Cover,\n    ButtonTwoTone,\n    TrackList,\n    CoverRow,\n    MvRow,\n    Modal,\n    ContextMenu,\n  },\n  beforeRouteUpdate(to, from, next) {\n    this.artist.img1v1Url =\n      'https://p1.music.126.net/VnZiScyynLG7atLIZ2YPkw==/18686200114669622.jpg';\n    this.loadData(to.params.id, next);\n  },\n  data() {\n    return {\n      show: false,\n      artist: {\n        img1v1Url:\n          'https://p1.music.126.net/VnZiScyynLG7atLIZ2YPkw==/18686200114669622.jpg',\n      },\n      popularTracks: [],\n      albumsData: [],\n      latestRelease: {\n        picUrl: '',\n        publishTime: 0,\n        id: 0,\n        name: '',\n        type: '',\n        size: '',\n      },\n      showMorePopTracks: false,\n      showFullDescription: false,\n      mvs: [],\n      hasMoreMV: false,\n      similarArtists: [],\n      mvHover: false,\n    };\n  },\n  computed: {\n    ...mapState(['player']),\n    albums() {\n      return this.albumsData.filter(\n        a => a.type === '专辑' || a.type === '精选集'\n      );\n    },\n    eps() {\n      return this.albumsData.filter(a =>\n        ['EP/Single', 'EP', 'Single'].includes(a.type)\n      );\n    },\n    latestMV() {\n      const mv = this.mvs[0] || {};\n      return {\n        id: mv.id || mv.vid,\n        name: mv.name || mv.title,\n        coverUrl: `${mv.imgurl16v9 || mv.cover || mv.coverUrl}?param=464y260`,\n        publishTime: mv.publishTime,\n      };\n    },\n  },\n  activated() {\n    if (this.artist?.id?.toString() !== this.$route.params.id) {\n      this.loadData(this.$route.params.id);\n    } else {\n      this.$parent.$refs.scrollbar.restorePosition();\n    }\n  },\n  methods: {\n    ...mapMutations(['appendTrackToPlayerList']),\n    ...mapActions(['playFirstTrackOnList', 'playTrackOnListByID', 'showToast']),\n    loadData(id, next = undefined) {\n      setTimeout(() => {\n        if (!this.show) NProgress.start();\n      }, 1000);\n      this.show = false;\n      this.$parent.$refs.main.scrollTo({ top: 0 });\n      getArtist(id).then(data => {\n        this.artist = data.artist;\n        this.setPopularTracks(data.hotSongs);\n        if (next !== undefined) next();\n        NProgress.done();\n        this.show = true;\n      });\n      getArtistAlbum({ id: id, limit: 200 }).then(data => {\n        this.albumsData = data.hotAlbums;\n        this.latestRelease = data.hotAlbums[0];\n      });\n      artistMv({ id }).then(data => {\n        this.mvs = data.mvs;\n        this.hasMoreMV = data.hasMore;\n      });\n      if (isAccountLoggedIn()) {\n        similarArtists(id).then(data => {\n          this.similarArtists = data.artists;\n        });\n      }\n    },\n    setPopularTracks(hotSongs) {\n      const trackIDs = hotSongs.map(t => t.id);\n      getTrackDetail(trackIDs.join(',')).then(data => {\n        this.popularTracks = data.songs;\n      });\n    },\n    goToAlbum(id) {\n      this.$router.push({\n        name: 'album',\n        params: { id },\n      });\n    },\n    goToMv(id) {\n      this.$router.push({ path: '/mv/' + id });\n    },\n    playPopularSongs(trackID = 'first') {\n      let trackIDs = this.popularTracks.map(t => t.id);\n      this.$store.state.player.replacePlaylist(\n        trackIDs,\n        this.artist.id,\n        'artist',\n        trackID\n      );\n    },\n    followArtist() {\n      if (!isAccountLoggedIn()) {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      followAArtist({\n        id: this.artist.id,\n        t: this.artist.followed ? 0 : 1,\n      }).then(data => {\n        if (data.code === 200) this.artist.followed = !this.artist.followed;\n      });\n    },\n    scrollTo(div, block = 'center') {\n      document.getElementById(div).scrollIntoView({\n        behavior: 'smooth',\n        block,\n      });\n    },\n    toggleFullDescription() {\n      this.showFullDescription = !this.showFullDescription;\n      if (this.showFullDescription) {\n        this.$store.commit('enableScrolling', false);\n      } else {\n        this.$store.commit('enableScrolling', true);\n      }\n    },\n    openMenu(e) {\n      this.$refs.artistMenu.openMenu(e);\n    },\n    copyUrl(id) {\n      let showToast = this.showToast;\n      this.$copyText(`https://music.163.com/#/artist?id=${id}`)\n        .then(function () {\n          showToast(locale.t('toast.copied'));\n        })\n        .catch(error => {\n          showToast(`${locale.t('toast.copyFailed')}${error}`);\n        });\n    },\n    openInBrowser(id) {\n      const url = `https://music.163.com/#/artist?id=${id}`;\n      window.open(url);\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.artist-page {\n  margin-top: 32px;\n}\n\n.artist-info {\n  display: flex;\n  align-items: center;\n  margin-bottom: 26px;\n  color: var(--color-text);\n  img {\n    height: 248px;\n    width: 248px;\n    border-radius: 50%;\n    margin-right: 56px;\n    box-shadow: rgba(0, 0, 0, 0.2) 0px 12px 16px -8px;\n  }\n  .name {\n    font-size: 56px;\n    font-weight: 700;\n  }\n\n  .artist {\n    font-size: 18px;\n    opacity: 0.88;\n    margin-top: 24px;\n  }\n\n  .statistics {\n    font-size: 14px;\n    opacity: 0.68;\n    margin-top: 2px;\n  }\n\n  .buttons {\n    margin-top: 26px;\n    display: flex;\n    .shuffle {\n      padding: 8px 11px;\n      .svg-icon {\n        margin: 0;\n      }\n    }\n  }\n\n  .description {\n    user-select: none;\n    font-size: 14px;\n    opacity: 0.68;\n    margin-top: 24px;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 2;\n    overflow: hidden;\n    cursor: pointer;\n    white-space: pre-line;\n    &:hover {\n      transition: opacity 0.3s;\n      opacity: 0.88;\n    }\n  }\n}\n\n.section-title {\n  font-weight: 600;\n  font-size: 22px;\n  opacity: 0.88;\n  color: var(--color-text);\n  margin-bottom: 16px;\n  padding-top: 46px;\n\n  display: flex;\n  justify-content: space-between;\n  align-items: flex-end;\n  a {\n    font-size: 13px;\n    font-weight: 600;\n    opacity: 0.68;\n  }\n}\n\n.latest-release {\n  color: var(--color-text);\n  .release {\n    display: flex;\n  }\n  .container {\n    display: flex;\n    flex: 1;\n    align-items: center;\n    border-radius: 12px;\n  }\n  img {\n    height: 96px;\n    border-radius: 8px;\n  }\n  .info {\n    margin-left: 24px;\n  }\n  .name {\n    font-size: 18px;\n    font-weight: 600;\n    margin-bottom: 8px;\n  }\n  .date {\n    font-size: 14px;\n    opacity: 0.78;\n  }\n  .type {\n    margin-top: 2px;\n    font-size: 12px;\n    opacity: 0.68;\n  }\n}\n\n.popular-tracks {\n  .show-more {\n    display: flex;\n\n    button {\n      padding: 4px 8px;\n      margin-top: 8px;\n      border-radius: 6px;\n      font-size: 12px;\n      opacity: 0.78;\n      color: var(--color-secondary);\n      font-weight: 600;\n      &:hover {\n        opacity: 1;\n      }\n    }\n  }\n}\n\n.similar-artists {\n  .section-title {\n    margin-bottom: 24px;\n  }\n}\n\n.latest-mv {\n  .cover {\n    position: relative;\n    transition: transform 0.3s;\n    &:hover {\n      cursor: pointer;\n    }\n  }\n  img {\n    border-radius: 0.75em;\n    height: 128px;\n    object-fit: cover;\n    user-select: none;\n  }\n\n  .shadow {\n    position: absolute;\n    top: 6px;\n    height: 100%;\n    width: 100%;\n    filter: blur(16px) opacity(0.4);\n    transform: scale(0.9, 0.9);\n    z-index: -1;\n    background-size: cover;\n    border-radius: 0.75em;\n  }\n\n  .fade-enter-active,\n  .fade-leave-active {\n    transition: opacity 0.3s;\n  }\n  .fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {\n    opacity: 0;\n  }\n}\n\n.description-fulltext {\n  font-size: 16px;\n  margin-top: 24px;\n  display: -webkit-box;\n  -webkit-box-orient: vertical;\n  overflow: hidden;\n  white-space: pre-line;\n}\n</style>\n"
  },
  {
    "path": "src/views/artistMV.vue",
    "content": "<template>\n  <div v-show=\"show\">\n    <h1>\n      <img\n        class=\"avatar\"\n        :src=\"artist.img1v1Url | resizeImage(1024)\"\n        loading=\"lazy\"\n      />{{ artist.name }}'s Music Videos\n    </h1>\n    <MvRow :mvs=\"mvs\" subtitle=\"publishTime\" />\n    <div class=\"load-more\">\n      <ButtonTwoTone v-show=\"hasMore\" color=\"grey\" @click.native=\"loadMVs\">{{\n        $t('explore.loadMore')\n      }}</ButtonTwoTone>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { artistMv, getArtist } from '@/api/artist';\nimport NProgress from 'nprogress';\n\nimport ButtonTwoTone from '@/components/ButtonTwoTone.vue';\nimport MvRow from '@/components/MvRow.vue';\n\nexport default {\n  name: 'ArtistMV',\n  components: {\n    MvRow,\n    ButtonTwoTone,\n  },\n  beforeRouteUpdate(to, from, next) {\n    this.id = to.params.id;\n    this.loadData();\n    next();\n  },\n  data() {\n    return {\n      id: 0,\n      show: false,\n      hasMore: true,\n      artist: {},\n      mvs: [],\n    };\n  },\n  created() {\n    this.id = this.$route.params.id;\n    this.loadData();\n  },\n  activated() {\n    if (this.$route.params.id !== this.id) {\n      this.id = this.$route.params.id;\n      this.mvs = [];\n      this.artist = {};\n      this.show = false;\n      this.hasMore = true;\n      this.loadData();\n    }\n  },\n  methods: {\n    loadData() {\n      setTimeout(() => {\n        if (!this.show) NProgress.start();\n      }, 1000);\n      getArtist(this.id).then(data => {\n        this.artist = data.artist;\n      });\n      this.loadMVs();\n    },\n    loadMVs() {\n      artistMv({ id: this.id, limit: 100, offset: this.mvs.length }).then(\n        data => {\n          this.mvs.push(...data.mvs);\n          this.hasMore = data.hasMore;\n          NProgress.done();\n          this.show = true;\n        }\n      );\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\nh1 {\n  font-size: 42px;\n  color: var(--color-text);\n  .avatar {\n    height: 44px;\n    margin-right: 12px;\n    vertical-align: -7px;\n    border-radius: 50%;\n    border: rgba(0, 0, 0, 0.2);\n  }\n}\n.load-more {\n  display: flex;\n  justify-content: center;\n}\n</style>\n"
  },
  {
    "path": "src/views/dailyTracks.vue",
    "content": "<template>\n  <div v-show=\"show\">\n    <div class=\"special-playlist\">\n      <div class=\"title gradient\"> 每日歌曲推荐 </div>\n      <div class=\"subtitle\">根据你的音乐口味生成 · 每天6:00更新</div>\n    </div>\n\n    <TrackList\n      :tracks=\"dailyTracks\"\n      type=\"playlist\"\n      dbclick-track-func=\"dailyTracks\"\n    />\n  </div>\n</template>\n\n<script>\nimport { mapMutations, mapState } from 'vuex';\nimport NProgress from 'nprogress';\nimport { dailyRecommendTracks } from '@/api/playlist';\n\nimport TrackList from '@/components/TrackList.vue';\n\nexport default {\n  name: 'DailyTracks',\n  components: {\n    TrackList,\n  },\n  data() {\n    return {\n      show: false,\n    };\n  },\n  computed: {\n    ...mapState(['player', 'data', 'dailyTracks']),\n  },\n  created() {\n    if (this.dailyTracks.length === 0) {\n      setTimeout(() => {\n        if (!this.show) NProgress.start();\n      }, 1000);\n      this.loadDailyTracks();\n    } else {\n      this.show = true;\n    }\n    this.$parent.$refs.main.scrollTo(0, 0);\n  },\n  methods: {\n    ...mapMutations(['updateDailyTracks']),\n    loadDailyTracks() {\n      dailyRecommendTracks().then(result => {\n        this.updateDailyTracks(result.data.dailySongs);\n        NProgress.done();\n        this.show = true;\n      });\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.special-playlist {\n  margin-top: 192px;\n  margin-bottom: 128px;\n  border-radius: 1.25em;\n  text-align: center;\n\n  @keyframes letterSpacing4 {\n    from {\n      letter-spacing: 0px;\n    }\n\n    to {\n      letter-spacing: 4px;\n    }\n  }\n\n  @keyframes letterSpacing1 {\n    from {\n      letter-spacing: 0px;\n    }\n\n    to {\n      letter-spacing: 1px;\n    }\n  }\n\n  .title {\n    font-size: 84px;\n    line-height: 1.05;\n    font-weight: 700;\n    text-transform: uppercase;\n\n    letter-spacing: 4px;\n    animation-duration: 0.8s;\n    animation-name: letterSpacing4;\n    -webkit-text-fill-color: transparent;\n    background-clip: text;\n    // background-image: linear-gradient(\n    //   225deg,\n    //   var(--color-primary),\n    //   var(--color-primary)\n    // );\n\n    img {\n      height: 78px;\n      border-radius: 0.125em;\n      margin-right: 24px;\n    }\n  }\n  .subtitle {\n    font-size: 18px;\n    letter-spacing: 1px;\n    margin: 28px 0 54px 0;\n    animation-duration: 0.8s;\n    animation-name: letterSpacing1;\n    text-transform: uppercase;\n    color: var(--color-text);\n  }\n  .buttons {\n    margin-top: 32px;\n    display: flex;\n    justify-content: center;\n    button {\n      margin-right: 16px;\n    }\n  }\n}\n\n.gradient {\n  background: linear-gradient(to left, #dd2476, #ff512f);\n}\n</style>\n"
  },
  {
    "path": "src/views/explore.vue",
    "content": "<template>\n  <div class=\"explore-page\">\n    <h1>{{ $t('explore.explore') }}</h1>\n    <div class=\"buttons\">\n      <div\n        v-for=\"category in settings.enabledPlaylistCategories\"\n        :key=\"category\"\n        class=\"button\"\n        :class=\"{ active: category === activeCategory && !showCatOptions }\"\n        @click=\"goToCategory(category)\"\n      >\n        {{ category }}\n      </div>\n      <div\n        class=\"button more\"\n        :class=\"{ active: showCatOptions }\"\n        @click=\"showCatOptions = !showCatOptions\"\n      >\n        <svg-icon icon-class=\"more\"></svg-icon>\n      </div>\n    </div>\n\n    <div v-show=\"showCatOptions\" class=\"panel\">\n      <div v-for=\"bigCat in allBigCats\" :key=\"bigCat\" class=\"big-cat\">\n        <div class=\"name\">{{ bigCat }}</div>\n        <div class=\"cats\">\n          <div\n            v-for=\"cat in getCatsByBigCat(bigCat)\"\n            :key=\"cat.name\"\n            class=\"cat\"\n            :class=\"{\n              active: settings.enabledPlaylistCategories.includes(cat.name),\n            }\"\n            @click=\"toggleCat(cat.name)\"\n            ><span>{{ cat.name }}</span></div\n          >\n        </div>\n      </div>\n    </div>\n\n    <div class=\"playlists\">\n      <CoverRow\n        type=\"playlist\"\n        :items=\"playlists\"\n        :sub-text=\"subText\"\n        :show-play-button=\"true\"\n        :show-play-count=\"activeCategory !== '排行榜' ? true : false\"\n        :image-size=\"activeCategory !== '排行榜' ? 512 : 1024\"\n      />\n    </div>\n    <div\n      v-show=\"['推荐歌单', '排行榜'].includes(activeCategory) === false\"\n      class=\"load-more\"\n    >\n      <ButtonTwoTone\n        v-show=\"showLoadMoreButton && hasMore\"\n        color=\"grey\"\n        :loading=\"loadingMore\"\n        @click.native=\"getPlaylist\"\n        >{{ $t('explore.loadMore') }}</ButtonTwoTone\n      >\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapState, mapMutations } from 'vuex';\nimport NProgress from 'nprogress';\nimport { topPlaylist, highQualityPlaylist, toplists } from '@/api/playlist';\nimport { playlistCategories } from '@/utils/staticData';\nimport { getRecommendPlayList } from '@/utils/playList';\n\nimport ButtonTwoTone from '@/components/ButtonTwoTone.vue';\nimport CoverRow from '@/components/CoverRow.vue';\nimport SvgIcon from '@/components/SvgIcon.vue';\n\nexport default {\n  name: 'Explore',\n  components: {\n    CoverRow,\n    ButtonTwoTone,\n    SvgIcon,\n  },\n  beforeRouteUpdate(to, from, next) {\n    this.showLoadMoreButton = false;\n    this.hasMore = true;\n    this.playlists = [];\n    this.offset = 1;\n    this.activeCategory = to.query.category;\n    this.getPlaylist();\n    next();\n  },\n  data() {\n    return {\n      show: false,\n      playlists: [],\n      activeCategory: '全部',\n      loadingMore: false,\n      showLoadMoreButton: false,\n      hasMore: true,\n      allBigCats: ['语种', '风格', '场景', '情感', '主题'],\n      showCatOptions: false,\n    };\n  },\n  computed: {\n    ...mapState(['settings']),\n    subText() {\n      if (this.activeCategory === '排行榜') return 'updateFrequency';\n      if (this.activeCategory === '推荐歌单') return 'copywriter';\n      return 'none';\n    },\n  },\n  activated() {\n    this.loadData();\n    this.$parent.$refs.scrollbar.restorePosition();\n  },\n  methods: {\n    ...mapMutations(['togglePlaylistCategory']),\n    loadData() {\n      setTimeout(() => {\n        if (!this.show) NProgress.start();\n      }, 1000);\n      const queryCategory = this.$route.query.category;\n      if (queryCategory === undefined) {\n        this.playlists = [];\n        this.activeCategory = '全部';\n      } else {\n        this.activeCategory = queryCategory;\n      }\n      this.getPlaylist();\n    },\n    goToCategory(Category) {\n      this.showCatOptions = false;\n      this.$router.push({ name: 'explore', query: { category: Category } });\n    },\n    updatePlaylist(playlists) {\n      this.playlists.push(...playlists);\n      this.loadingMore = false;\n      this.showLoadMoreButton = true;\n      NProgress.done();\n      this.show = true;\n    },\n    getPlaylist() {\n      this.loadingMore = true;\n      if (this.activeCategory === '推荐歌单') {\n        return this.getRecommendPlayList();\n      }\n      if (this.activeCategory === '精品歌单') {\n        return this.getHighQualityPlaylist();\n      }\n      if (this.activeCategory === '排行榜') {\n        return this.getTopLists();\n      }\n      return this.getTopPlayList();\n    },\n    getRecommendPlayList() {\n      getRecommendPlayList(100, true).then(list => {\n        this.playlists = [];\n        this.updatePlaylist(list);\n      });\n    },\n    getHighQualityPlaylist() {\n      let playlists = this.playlists;\n      let before =\n        playlists.length !== 0 ? playlists[playlists.length - 1].updateTime : 0;\n      highQualityPlaylist({ limit: 50, before }).then(data => {\n        this.updatePlaylist(data.playlists);\n        this.hasMore = data.more;\n      });\n    },\n    getTopLists() {\n      toplists().then(data => {\n        this.playlists = [];\n        this.updatePlaylist(data.list);\n      });\n    },\n    getTopPlayList() {\n      topPlaylist({\n        cat: this.activeCategory,\n        offset: this.playlists.length,\n      }).then(data => {\n        this.updatePlaylist(data.playlists);\n        this.hasMore = data.more;\n      });\n    },\n    getCatsByBigCat(name) {\n      return playlistCategories.filter(c => c.bigCat === name);\n    },\n    toggleCat(name) {\n      this.togglePlaylistCategory(name);\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\nh1 {\n  color: var(--color-text);\n  font-size: 56px;\n}\n.buttons {\n  display: flex;\n  flex-wrap: wrap;\n}\n.button {\n  user-select: none;\n  cursor: pointer;\n  padding: 8px 16px;\n  margin: 10px 16px 6px 0;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  font-weight: 600;\n  font-size: 18px;\n  border-radius: 10px;\n  background-color: var(--color-secondary-bg);\n  color: var(--color-secondary);\n  transition: 0.2s;\n\n  &:hover {\n    background-color: var(--color-primary-bg);\n    color: var(--color-primary);\n  }\n}\n.button.active {\n  background-color: var(--color-primary-bg);\n  color: var(--color-primary);\n}\n.panel {\n  margin-top: 10px;\n  background: var(--color-secondary-bg);\n  border-radius: 10px;\n  padding: 8px;\n  color: var(--color-text);\n\n  .big-cat {\n    display: flex;\n    margin-bottom: 32px;\n  }\n\n  .name {\n    font-size: 24px;\n    font-weight: 700;\n    opacity: 0.68;\n    margin-left: 24px;\n    min-width: 54px;\n    height: 26px;\n    margin-top: 8px;\n  }\n  .cats {\n    margin-left: 24px;\n    display: flex;\n    flex-wrap: wrap;\n  }\n  .cat {\n    user-select: none;\n    margin: 4px 0px 0 0;\n    display: flex;\n    // justify-content: center;\n    align-items: center;\n    font-weight: 500;\n    font-size: 16px;\n    transition: 0.2s;\n    min-width: 98px;\n\n    span {\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      cursor: pointer;\n      padding: 6px 12px;\n      height: 26px;\n      border-radius: 10px;\n      opacity: 0.88;\n      &:hover {\n        opacity: 1;\n        background-color: var(--color-primary-bg);\n        color: var(--color-primary);\n      }\n    }\n  }\n  .cat.active {\n    color: var(--color-primary);\n  }\n}\n\n.playlists {\n  margin-top: 24px;\n}\n\n.load-more {\n  display: flex;\n  justify-content: center;\n  margin-top: 32px;\n}\n\n.button.more {\n  .svg-icon {\n    height: 24px;\n    width: 24px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/views/home.vue",
    "content": "<template>\n  <div v-show=\"show\" class=\"home\">\n    <div\n      v-if=\"settings.showPlaylistsByAppleMusic !== false\"\n      class=\"index-row first-row\"\n    >\n      <div class=\"title\"> by Apple Music </div>\n      <CoverRow\n        :type=\"'playlist'\"\n        :items=\"byAppleMusic\"\n        sub-text=\"appleMusic\"\n        :image-size=\"1024\"\n      />\n    </div>\n    <div class=\"index-row\">\n      <div class=\"title\">\n        {{ $t('home.recommendPlaylist') }}\n        <router-link to=\"/explore?category=推荐歌单\">{{\n          $t('home.seeMore')\n        }}</router-link>\n      </div>\n      <CoverRow\n        :type=\"'playlist'\"\n        :items=\"recommendPlaylist.items\"\n        sub-text=\"copywriter\"\n      />\n    </div>\n    <div class=\"index-row\">\n      <div class=\"title\"> For You </div>\n      <div class=\"for-you-row\">\n        <DailyTracksCard ref=\"DailyTracksCard\" />\n        <FMCard />\n      </div>\n    </div>\n    <div class=\"index-row\">\n      <div class=\"title\">{{ $t('home.recommendArtist') }}</div>\n      <CoverRow\n        type=\"artist\"\n        :column-number=\"6\"\n        :items=\"recommendArtists.items\"\n      />\n    </div>\n    <div class=\"index-row\">\n      <div class=\"title\">\n        {{ $t('home.newAlbum') }}\n        <router-link to=\"/new-album\">{{ $t('home.seeMore') }}</router-link>\n      </div>\n      <CoverRow\n        type=\"album\"\n        :items=\"newReleasesAlbum.items\"\n        sub-text=\"artist\"\n      />\n    </div>\n    <div class=\"index-row\">\n      <div class=\"title\">\n        {{ $t('home.charts') }}\n        <router-link to=\"/explore?category=排行榜\">{{\n          $t('home.seeMore')\n        }}</router-link>\n      </div>\n      <CoverRow\n        type=\"playlist\"\n        :items=\"topList.items\"\n        sub-text=\"updateFrequency\"\n        :image-size=\"1024\"\n      />\n    </div>\n  </div>\n</template>\n\n<script>\nimport { toplists } from '@/api/playlist';\nimport { toplistOfArtists } from '@/api/artist';\nimport { newAlbums } from '@/api/album';\nimport { byAppleMusic } from '@/utils/staticData';\nimport { getRecommendPlayList } from '@/utils/playList';\nimport NProgress from 'nprogress';\nimport { mapState } from 'vuex';\nimport CoverRow from '@/components/CoverRow.vue';\nimport FMCard from '@/components/FMCard.vue';\nimport DailyTracksCard from '@/components/DailyTracksCard.vue';\n\nexport default {\n  name: 'Home',\n  components: { CoverRow, FMCard, DailyTracksCard },\n  data() {\n    return {\n      show: false,\n      recommendPlaylist: { items: [] },\n      newReleasesAlbum: { items: [] },\n      topList: {\n        items: [],\n        ids: [19723756, 180106, 60198, 3812895, 60131],\n      },\n      recommendArtists: {\n        items: [],\n        indexs: [],\n      },\n    };\n  },\n  computed: {\n    ...mapState(['settings']),\n    byAppleMusic() {\n      return byAppleMusic;\n    },\n  },\n  activated() {\n    this.loadData();\n    this.$parent.$refs.scrollbar.restorePosition();\n  },\n  methods: {\n    loadData() {\n      setTimeout(() => {\n        if (!this.show) NProgress.start();\n      }, 1000);\n      getRecommendPlayList(10, false).then(items => {\n        this.recommendPlaylist.items = items;\n        NProgress.done();\n        this.show = true;\n      });\n      newAlbums({\n        area: this.settings.musicLanguage ?? 'ALL',\n        limit: 10,\n      }).then(data => {\n        this.newReleasesAlbum.items = data.albums;\n      });\n\n      const toplistOfArtistsAreaTable = {\n        all: null,\n        zh: 1,\n        ea: 2,\n        jp: 4,\n        kr: 3,\n      };\n      toplistOfArtists(\n        toplistOfArtistsAreaTable[this.settings.musicLanguage ?? 'all']\n      ).then(data => {\n        let indexs = [];\n        while (indexs.length < 6) {\n          let tmp = ~~(Math.random() * 100);\n          if (!indexs.includes(tmp)) indexs.push(tmp);\n        }\n        this.recommendArtists.indexs = indexs;\n        this.recommendArtists.items = data.list.artists.filter((l, index) =>\n          indexs.includes(index)\n        );\n      });\n      toplists().then(data => {\n        this.topList.items = data.list.filter(l =>\n          this.topList.ids.includes(l.id)\n        );\n      });\n      this.$refs.DailyTracksCard.loadDailyTracks();\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.index-row {\n  margin-top: 54px;\n}\n.index-row.first-row {\n  margin-top: 32px;\n}\n.playlists {\n  display: flex;\n  flex-wrap: wrap;\n  margin: {\n    right: -12px;\n    left: -12px;\n  }\n  .index-playlist {\n    margin: 12px 12px 24px 12px;\n  }\n}\n\n.title {\n  display: flex;\n  justify-content: space-between;\n  align-items: flex-end;\n  margin-bottom: 20px;\n  font-size: 28px;\n  font-weight: 700;\n  color: var(--color-text);\n  a {\n    font-size: 13px;\n    font-weight: 600;\n    opacity: 0.68;\n  }\n}\n\nfooter {\n  display: flex;\n  justify-content: center;\n  margin-top: 48px;\n}\n\n.for-you-row {\n  display: grid;\n  grid-template-columns: repeat(2, 1fr);\n  gap: 24px;\n  margin-bottom: 78px;\n}\n</style>\n"
  },
  {
    "path": "src/views/lastfmCallback.vue",
    "content": "<template>\n  <div class=\"lastfm-callback\">\n    <div class=\"section-1\">\n      <img src=\"/img/logos/yesplaymusic.png\" />\n      <svg-icon icon-class=\"x\"></svg-icon>\n      <img src=\"/img/logos/lastfm.png\" />\n    </div>\n    <div class=\"message\">{{ message }}</div>\n    <button v-show=\"done\" @click=\"close\"> 完成 </button>\n  </div>\n</template>\n\n<script>\nimport { authGetSession } from '@/api/lastfm';\n\nexport default {\n  name: 'LastfmCallback',\n  data() {\n    return { message: '请稍等...', done: false };\n  },\n  created() {\n    const token = new URLSearchParams(window.location.search).get('token');\n    if (!token) {\n      this.message = '连接失败，请重试或联系开发者（无Token）';\n      this.done = true;\n      return;\n    }\n    authGetSession(token).then(result => {\n      if (!result.data.session) {\n        this.message = '连接失败，请重试或联系开发者（无Session）';\n        this.done = true;\n        return;\n      }\n      localStorage.setItem('lastfm', JSON.stringify(result.data.session));\n      this.$store.commit('updateLastfm', result.data.session);\n      this.message = '已成功连接到 Last.fm';\n      this.done = true;\n    });\n  },\n  methods: {\n    close() {\n      window.close();\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.lastfm-callback {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  height: calc(100vh - 192px);\n}\n.section-1 {\n  margin-bottom: 16px;\n  display: flex;\n  align-items: center;\n  img {\n    height: 64px;\n    margin: 20px;\n  }\n  .svg-icon {\n    height: 24px;\n    width: 24px;\n    color: rgba(82, 82, 82, 0.28);\n  }\n}\n\n.message {\n  font-size: 1.4rem;\n  font-weight: 500;\n  color: var(--color-text);\n}\n\nbutton {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 16px;\n  font-weight: 600;\n  background-color: var(--color-primary-bg);\n  color: var(--color-primary);\n  border-radius: 8px;\n  margin-top: 24px;\n  transition: 0.2s;\n  padding: 8px 16px;\n  &:hover {\n    transform: scale(1.06);\n  }\n  &:active {\n    transform: scale(0.94);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/views/library.vue",
    "content": "<template>\n  <div v-show=\"show\" ref=\"library\">\n    <h1>\n      <img\n        class=\"avatar\"\n        :src=\"data.user.avatarUrl | resizeImage\"\n        loading=\"lazy\"\n      />{{ data.user.nickname }}{{ $t('library.sLibrary') }}\n    </h1>\n    <div class=\"section-one\">\n      <div class=\"liked-songs\" @click=\"goToLikedSongsList\">\n        <div class=\"top\">\n          <p>\n            <span\n              v-for=\"(line, index) in pickedLyric\"\n              v-show=\"line !== ''\"\n              :key=\"`${line}${index}`\"\n              >{{ line }}<br\n            /></span>\n          </p>\n        </div>\n        <div class=\"bottom\">\n          <div class=\"titles\">\n            <div class=\"title\">{{ $t('library.likedSongs') }}</div>\n            <div class=\"sub-title\">\n              {{ liked.songs.length }} {{ $t('common.songs') }}\n            </div>\n          </div>\n          <button @click.stop=\"openPlayModeTabMenu\">\n            <svg-icon icon-class=\"play\" />\n          </button>\n        </div>\n      </div>\n      <div class=\"songs\">\n        <TrackList\n          :id=\"liked.playlists.length > 0 ? liked.playlists[0].id : 0\"\n          :tracks=\"liked.songsWithDetails\"\n          :column-number=\"3\"\n          type=\"tracklist\"\n          dbclick-track-func=\"playPlaylistByID\"\n        />\n      </div>\n    </div>\n\n    <div class=\"section-two\">\n      <div class=\"tabs-row\">\n        <div class=\"tabs\">\n          <div\n            class=\"tab dropdown\"\n            :class=\"{ active: currentTab === 'playlists' }\"\n            @click=\"updateCurrentTab('playlists')\"\n          >\n            <span class=\"text\">{{\n              {\n                all: $t('contextMenu.allPlaylists'),\n                mine: $t('contextMenu.minePlaylists'),\n                liked: $t('contextMenu.likedPlaylists'),\n              }[playlistFilter]\n            }}</span>\n            <span class=\"icon\" @click.stop=\"openPlaylistTabMenu\"\n              ><svg-icon icon-class=\"dropdown\"\n            /></span>\n          </div>\n          <div\n            class=\"tab\"\n            :class=\"{ active: currentTab === 'albums' }\"\n            @click=\"updateCurrentTab('albums')\"\n          >\n            {{ $t('library.albums') }}\n          </div>\n          <div\n            class=\"tab\"\n            :class=\"{ active: currentTab === 'artists' }\"\n            @click=\"updateCurrentTab('artists')\"\n          >\n            {{ $t('library.artists') }}\n          </div>\n          <div\n            class=\"tab\"\n            :class=\"{ active: currentTab === 'mvs' }\"\n            @click=\"updateCurrentTab('mvs')\"\n          >\n            {{ $t('library.mvs') }}\n          </div>\n          <div\n            class=\"tab\"\n            :class=\"{ active: currentTab === 'cloudDisk' }\"\n            @click=\"updateCurrentTab('cloudDisk')\"\n          >\n            {{ $t('library.cloudDisk') }}\n          </div>\n          <div\n            class=\"tab\"\n            :class=\"{ active: currentTab === 'playHistory' }\"\n            @click=\"updateCurrentTab('playHistory')\"\n          >\n            {{ $t('library.playHistory.title') }}\n          </div>\n        </div>\n        <button\n          v-show=\"currentTab === 'playlists'\"\n          class=\"tab-button\"\n          @click=\"openAddPlaylistModal\"\n          ><svg-icon icon-class=\"plus\" />{{ $t('library.newPlayList') }}\n        </button>\n        <button\n          v-show=\"currentTab === 'cloudDisk'\"\n          class=\"tab-button\"\n          @click=\"selectUploadFiles\"\n          ><svg-icon icon-class=\"arrow-up-alt\" />{{ $t('library.uploadSongs') }}\n        </button>\n      </div>\n\n      <div v-show=\"currentTab === 'playlists'\">\n        <div v-if=\"liked.playlists.length > 1\">\n          <CoverRow\n            :items=\"filterPlaylists\"\n            type=\"playlist\"\n            sub-text=\"creator\"\n            :show-play-button=\"true\"\n          />\n        </div>\n      </div>\n\n      <div v-show=\"currentTab === 'albums'\">\n        <CoverRow\n          :items=\"liked.albums\"\n          type=\"album\"\n          sub-text=\"artist\"\n          :show-play-button=\"true\"\n        />\n      </div>\n\n      <div v-show=\"currentTab === 'artists'\">\n        <CoverRow\n          :items=\"liked.artists\"\n          type=\"artist\"\n          :show-play-button=\"true\"\n        />\n      </div>\n\n      <div v-show=\"currentTab === 'mvs'\">\n        <MvRow :mvs=\"liked.mvs\" />\n      </div>\n\n      <div v-show=\"currentTab === 'cloudDisk'\">\n        <TrackList\n          :id=\"-8\"\n          :tracks=\"liked.cloudDisk\"\n          :column-number=\"3\"\n          type=\"cloudDisk\"\n          dbclick-track-func=\"playCloudDisk\"\n          :extra-context-menu-item=\"['removeTrackFromCloudDisk']\"\n        />\n      </div>\n\n      <div v-show=\"currentTab === 'playHistory'\">\n        <button\n          :class=\"{\n            'playHistory-button': true,\n            'playHistory-button--selected': playHistoryMode === 'week',\n          }\"\n          @click=\"playHistoryMode = 'week'\"\n        >\n          {{ $t('library.playHistory.week') }}\n        </button>\n        <button\n          :class=\"{\n            'playHistory-button': true,\n            'playHistory-button--selected': playHistoryMode === 'all',\n          }\"\n          @click=\"playHistoryMode = 'all'\"\n        >\n          {{ $t('library.playHistory.all') }}\n        </button>\n        <TrackList\n          :tracks=\"playHistoryList\"\n          :column-number=\"1\"\n          type=\"tracklist\"\n        />\n      </div>\n    </div>\n\n    <input\n      ref=\"cloudDiskUploadInput\"\n      type=\"file\"\n      style=\"display: none\"\n      @change=\"uploadSongToCloudDisk\"\n    />\n\n    <ContextMenu ref=\"playlistTabMenu\">\n      <div class=\"item\" @click=\"changePlaylistFilter('all')\">{{\n        $t('contextMenu.allPlaylists')\n      }}</div>\n      <hr />\n      <div class=\"item\" @click=\"changePlaylistFilter('mine')\">{{\n        $t('contextMenu.minePlaylists')\n      }}</div>\n      <div class=\"item\" @click=\"changePlaylistFilter('liked')\">{{\n        $t('contextMenu.likedPlaylists')\n      }}</div>\n    </ContextMenu>\n\n    <ContextMenu ref=\"playModeTabMenu\">\n      <div class=\"item\" @click=\"playLikedSongs\">{{\n        $t('library.likedSongs')\n      }}</div>\n      <hr />\n      <div class=\"item\" @click=\"playIntelligenceList\">{{\n        $t('contextMenu.cardiacMode')\n      }}</div>\n    </ContextMenu>\n  </div>\n</template>\n\n<script>\nimport { mapActions, mapMutations, mapState } from 'vuex';\nimport { randomNum, dailyTask } from '@/utils/common';\nimport { isAccountLoggedIn } from '@/utils/auth';\nimport { uploadSong } from '@/api/user';\nimport { getLyric } from '@/api/track';\nimport NProgress from 'nprogress';\nimport locale from '@/locale';\n\nimport ContextMenu from '@/components/ContextMenu.vue';\nimport TrackList from '@/components/TrackList.vue';\nimport CoverRow from '@/components/CoverRow.vue';\nimport SvgIcon from '@/components/SvgIcon.vue';\nimport MvRow from '@/components/MvRow.vue';\n\n/**\n * Pick the lyric part from a string formed in `[timecode] lyric`.\n *\n * @param {string} rawLyric The raw lyric string formed in `[timecode] lyric`\n * @returns {string} The lyric part\n */\nfunction extractLyricPart(rawLyric) {\n  return rawLyric.split(']').pop().trim();\n}\n\nexport default {\n  name: 'Library',\n  components: { SvgIcon, CoverRow, TrackList, MvRow, ContextMenu },\n  data() {\n    return {\n      show: false,\n      likedSongs: [],\n      lyric: undefined,\n      currentTab: 'playlists',\n      playHistoryMode: 'week',\n    };\n  },\n  computed: {\n    ...mapState(['data', 'liked']),\n    /**\n     * @returns {string[]}\n     */\n    pickedLyric() {\n      /** @type {string?} */\n      const lyric = this.lyric;\n\n      // Returns [] if we got no lyrics.\n      if (!lyric) return [];\n\n      const lyricLine = lyric\n        .split('\\n')\n        .filter(line => !line.includes('作词') && !line.includes('作曲'));\n\n      // Pick 3 or fewer lyrics based on the lyric lines.\n      const lyricsToPick = Math.min(lyricLine.length, 3);\n\n      // The upperBound of the lyric line to pick\n      const randomUpperBound = lyricLine.length - lyricsToPick;\n      const startLyricLineIndex = randomNum(0, randomUpperBound - 1);\n\n      // Pick lyric lines to render.\n      return lyricLine\n        .slice(startLyricLineIndex, startLyricLineIndex + lyricsToPick)\n        .map(extractLyricPart);\n    },\n    playlistFilter() {\n      return this.data.libraryPlaylistFilter || 'all';\n    },\n    filterPlaylists() {\n      const playlists = this.liked.playlists.slice(1);\n      const userId = this.data.user.userId;\n      if (this.playlistFilter === 'mine') {\n        return playlists.filter(p => p.creator.userId === userId);\n      } else if (this.playlistFilter === 'liked') {\n        return playlists.filter(p => p.creator.userId !== userId);\n      }\n      return playlists;\n    },\n    playHistoryList() {\n      if (this.show && this.playHistoryMode === 'week') {\n        return this.liked.playHistory.weekData;\n      }\n      if (this.show && this.playHistoryMode === 'all') {\n        return this.liked.playHistory.allData;\n      }\n      return [];\n    },\n  },\n  created() {\n    setTimeout(() => {\n      if (!this.show) NProgress.start();\n    }, 1000);\n    this.loadData();\n  },\n  activated() {\n    this.$parent.$refs.scrollbar.restorePosition();\n    this.loadData();\n    dailyTask();\n  },\n  methods: {\n    ...mapActions(['showToast']),\n    ...mapMutations(['updateModal', 'updateData']),\n    loadData() {\n      if (this.liked.songsWithDetails.length > 0) {\n        NProgress.done();\n        this.show = true;\n        this.$store.dispatch('fetchLikedSongsWithDetails');\n        this.getRandomLyric();\n      } else {\n        this.$store.dispatch('fetchLikedSongsWithDetails').then(() => {\n          NProgress.done();\n          this.show = true;\n          this.getRandomLyric();\n        });\n      }\n      this.$store.dispatch('fetchLikedSongs');\n      this.$store.dispatch('fetchLikedPlaylist');\n      this.$store.dispatch('fetchLikedAlbums');\n      this.$store.dispatch('fetchLikedArtists');\n      this.$store.dispatch('fetchLikedMVs');\n      this.$store.dispatch('fetchCloudDisk');\n      this.$store.dispatch('fetchPlayHistory');\n    },\n    playLikedSongs() {\n      this.$store.state.player.playPlaylistByID(\n        this.liked.playlists[0].id,\n        'first',\n        true\n      );\n    },\n    playIntelligenceList() {\n      this.$store.state.player.playIntelligenceListById(\n        this.liked.playlists[0].id,\n        'first',\n        true\n      );\n    },\n    updateCurrentTab(tab) {\n      if (!isAccountLoggedIn() && tab !== 'playlists') {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      this.currentTab = tab;\n      this.$parent.$refs.main.scrollTo({ top: 375, behavior: 'smooth' });\n    },\n    goToLikedSongsList() {\n      this.$router.push({ path: '/library/liked-songs' });\n    },\n    getRandomLyric() {\n      if (this.liked.songs.length === 0) return;\n      getLyric(\n        this.liked.songs[randomNum(0, this.liked.songs.length - 1)]\n      ).then(data => {\n        if (data.lrc !== undefined) {\n          const isInstrumental = data.lrc.lyric\n            .split('\\n')\n            .filter(l => l.includes('纯音乐，请欣赏'));\n          if (isInstrumental.length === 0) {\n            this.lyric = data.lrc.lyric;\n          }\n        }\n      });\n    },\n    openAddPlaylistModal() {\n      if (!isAccountLoggedIn()) {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      this.updateModal({\n        modalName: 'newPlaylistModal',\n        key: 'show',\n        value: true,\n      });\n    },\n    openPlaylistTabMenu(e) {\n      this.$refs.playlistTabMenu.openMenu(e);\n    },\n    openPlayModeTabMenu(e) {\n      this.$refs.playModeTabMenu.openMenu(e);\n    },\n    changePlaylistFilter(type) {\n      this.updateData({ key: 'libraryPlaylistFilter', value: type });\n      window.scrollTo({ top: 375, behavior: 'smooth' });\n    },\n    selectUploadFiles() {\n      this.$refs.cloudDiskUploadInput.click();\n    },\n    uploadSongToCloudDisk(e) {\n      const files = e.target.files;\n      uploadSong(files[0]).then(result => {\n        if (result.code === 200) {\n          let newCloudDisk = this.liked.cloudDisk;\n          newCloudDisk.unshift(result.privateCloud);\n          this.$store.commit('updateLikedXXX', {\n            name: 'cloudDisk',\n            data: newCloudDisk,\n          });\n        }\n      });\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\nh1 {\n  font-size: 42px;\n  color: var(--color-text);\n  display: flex;\n  align-items: center;\n  .avatar {\n    height: 44px;\n    margin-right: 12px;\n    vertical-align: -7px;\n    border-radius: 50%;\n    border: rgba(0, 0, 0, 0.2);\n  }\n}\n\n.section-one {\n  display: flex;\n  margin-top: 24px;\n  .songs {\n    flex: 7;\n    margin-top: 8px;\n    margin-left: 36px;\n    overflow: hidden;\n  }\n}\n\n.liked-songs {\n  flex: 3;\n  margin-top: 8px;\n  cursor: pointer;\n  border-radius: 16px;\n  padding: 18px 24px;\n  display: flex;\n  flex-direction: column;\n  transition: all 0.4s;\n  box-sizing: border-box;\n\n  background: var(--color-primary-bg);\n\n  .bottom {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    color: var(--color-primary);\n\n    .title {\n      font-size: 24px;\n      font-weight: 700;\n    }\n    .sub-title {\n      font-size: 15px;\n      margin-top: 2px;\n    }\n\n    button {\n      margin-bottom: 2px;\n      display: flex;\n      justify-content: center;\n      align-items: center;\n      height: 44px;\n      width: 44px;\n      background: var(--color-primary);\n      border-radius: 50%;\n      transition: 0.2s;\n      box-shadow: 0 6px 12px -4px rgba(0, 0, 0, 0.2);\n      cursor: default;\n\n      .svg-icon {\n        color: var(--color-primary-bg);\n        margin-left: 4px;\n        height: 16px;\n        width: 16px;\n      }\n      &:hover {\n        transform: scale(1.06);\n        box-shadow: 0 6px 12px -4px rgba(0, 0, 0, 0.4);\n      }\n      &:active {\n        transform: scale(0.94);\n      }\n    }\n  }\n\n  .top {\n    flex: 1;\n    display: flex;\n    flex-wrap: wrap;\n    font-size: 14px;\n    opacity: 0.88;\n    color: var(--color-primary);\n    p {\n      margin-top: 2px;\n    }\n  }\n}\n\n.section-two {\n  margin-top: 54px;\n  min-height: calc(100vh - 182px);\n}\n\n.tabs-row {\n  display: flex;\n  justify-content: space-between;\n  margin-bottom: 24px;\n}\n\n.tabs {\n  display: flex;\n  flex-wrap: wrap;\n  font-size: 18px;\n  color: var(--color-text);\n  .tab {\n    font-weight: 600;\n    padding: 8px 14px;\n    margin-right: 14px;\n    border-radius: 8px;\n    cursor: pointer;\n    user-select: none;\n    transition: 0.2s;\n    opacity: 0.68;\n    &:hover {\n      opacity: 0.88;\n      background-color: var(--color-secondary-bg);\n    }\n  }\n  .tab.active {\n    opacity: 0.88;\n    background-color: var(--color-secondary-bg);\n  }\n  .tab.dropdown {\n    display: flex;\n    align-items: center;\n    padding: 0;\n    overflow: hidden;\n    .text {\n      padding: 8px 3px 8px 14px;\n    }\n    .icon {\n      height: 100%;\n      display: flex;\n      align-items: center;\n      padding: 0 8px 0 3px;\n      .svg-icon {\n        height: 16px;\n        width: 16px;\n      }\n    }\n  }\n}\n\nbutton.tab-button {\n  color: var(--color-text);\n  border-radius: 8px;\n  padding: 0 14px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  transition: 0.2s;\n  opacity: 0.68;\n  font-weight: 500;\n  .svg-icon {\n    width: 14px;\n    height: 14px;\n    margin-right: 8px;\n  }\n  &:hover {\n    opacity: 1;\n    background: var(--color-secondary-bg);\n  }\n  &:active {\n    opacity: 1;\n    transform: scale(0.92);\n  }\n}\n\nbutton.playHistory-button {\n  color: var(--color-text);\n  border-radius: 8px;\n  padding: 6px 8px;\n  margin-bottom: 12px;\n  margin-right: 4px;\n  transition: 0.2s;\n  opacity: 0.68;\n  font-weight: 500;\n  cursor: pointer;\n  &:hover {\n    opacity: 1;\n    background: var(--color-secondary-bg);\n  }\n  &:active {\n    transform: scale(0.95);\n  }\n}\n\nbutton.playHistory-button--selected {\n  color: var(--color-text);\n  background: var(--color-secondary-bg);\n  opacity: 1;\n  font-weight: 700;\n  &:active {\n    transform: none;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/views/login.vue",
    "content": "<template>\n  <div class=\"login\">\n    <div class=\"section-1\">\n      <img src=\"/img/logos/yesplaymusic.png\" />\n      <svg-icon icon-class=\"x\"></svg-icon>\n      <img src=\"/img/logos/netease-music.png\" />\n    </div>\n    <div class=\"section-2\">\n      <div\n        class=\"card\"\n        @mouseover=\"activeCard = 1\"\n        @mouseleave=\"activeCard = 0\"\n        @click=\"goTo('account')\"\n      >\n        <div class=\"container\" :class=\"{ active: activeCard === 1 }\">\n          <div class=\"title-info\">\n            <div class=\"title\">{{ $t('login.loginText') }}</div>\n            <div class=\"info\">{{ $t('login.accessToAll') }}</div>\n          </div>\n          <svg-icon icon-class=\"arrow-right\"></svg-icon>\n        </div>\n      </div>\n      <div\n        class=\"card\"\n        @mouseover=\"activeCard = 2\"\n        @mouseleave=\"activeCard = 0\"\n        @click=\"goTo('username')\"\n      >\n        <div class=\"container\" :class=\"{ active: activeCard === 2 }\">\n          <div class=\"title-info\">\n            <div class=\"title\">{{ $t('login.search') }}</div>\n            <div class=\"info\">{{ $t('login.readonly') }}</div>\n          </div>\n          <svg-icon icon-class=\"arrow-right\"></svg-icon>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport NProgress from 'nprogress';\n\nimport SvgIcon from '@/components/SvgIcon.vue';\n\nexport default {\n  name: 'Login',\n  components: {\n    SvgIcon,\n  },\n  data() {\n    return {\n      activeCard: 0,\n    };\n  },\n  created() {\n    NProgress.done();\n  },\n  methods: {\n    goTo(path) {\n      this.$router.push({ path: '/login/' + path });\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.login {\n  display: flex;\n  flex-direction: column;\n  justify-content: center;\n  align-items: center;\n  height: calc(100vh - 192px);\n}\n\n.section-1 {\n  margin-bottom: 16px;\n  display: flex;\n  align-items: center;\n  img {\n    height: 64px;\n    margin: 20px;\n  }\n  .svg-icon {\n    height: 24px;\n    width: 24px;\n    color: rgba(82, 82, 82, 0.28);\n  }\n}\n\n.section-2 {\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n}\n.card {\n  cursor: pointer;\n  margin-top: 14px;\n  margin-bottom: 14px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  background: #eaeffd;\n  border-radius: 8px;\n  height: 128px;\n  width: 300px;\n  transition: all 0.3s;\n  padding-left: 22px;\n  box-sizing: border-box;\n\n  .active {\n    .title-info {\n      transform: translateX(-8px);\n    }\n    .svg-icon {\n      opacity: 1;\n      visibility: visible;\n      transform: translateX(8px);\n    }\n  }\n\n  .container {\n    display: flex;\n    // justify-content: space-around;\n    align-items: center;\n\n    color: #335eea;\n  }\n\n  .title-info {\n    transition: all 0.3s;\n  }\n\n  .title {\n    font-size: 24px;\n    font-weight: 600;\n  }\n  .info {\n    margin-top: 2px;\n    font-size: 14px;\n    color: rgba(51, 94, 234, 0.78);\n  }\n  .svg-icon {\n    opacity: 0;\n    height: 24px;\n    width: 24px;\n    margin-left: 16px;\n    transition: all 0.3s;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/views/loginAccount.vue",
    "content": "<template>\n  <div class=\"login\">\n    <div class=\"login-container\">\n      <div class=\"section-1\">\n        <img src=\"/img/logos/netease-music.png\" />\n      </div>\n      <div class=\"title\">{{ $t('login.loginText') }}</div>\n      <div class=\"section-2\">\n        <div v-show=\"mode === 'phone'\" class=\"input-box\">\n          <div\n            class=\"container\"\n            :class=\"{ active: ['phone', 'countryCode'].includes(inputFocus) }\"\n          >\n            <svg-icon icon-class=\"mobile\" />\n            <div class=\"inputs\">\n              <input\n                id=\"countryCode\"\n                v-model=\"countryCode\"\n                :placeholder=\"\n                  inputFocus === 'countryCode' ? '' : $t('login.countryCode')\n                \"\n                @focus=\"inputFocus = 'countryCode'\"\n                @blur=\"inputFocus = ''\"\n                @keyup.enter=\"login\"\n              />\n              <input\n                id=\"phoneNumber\"\n                v-model=\"phoneNumber\"\n                :placeholder=\"inputFocus === 'phone' ? '' : $t('login.phone')\"\n                @focus=\"inputFocus = 'phone'\"\n                @blur=\"inputFocus = ''\"\n                @keyup.enter=\"login\"\n              />\n            </div>\n          </div>\n        </div>\n\n        <div v-show=\"mode === 'email'\" class=\"input-box\">\n          <div class=\"container\" :class=\"{ active: inputFocus === 'email' }\">\n            <svg-icon icon-class=\"mail\" />\n            <div class=\"inputs\">\n              <input\n                id=\"email\"\n                v-model=\"email\"\n                type=\"email\"\n                :placeholder=\"inputFocus === 'email' ? '' : $t('login.email')\"\n                @focus=\"inputFocus = 'email'\"\n                @blur=\"inputFocus = ''\"\n                @keyup.enter=\"login\"\n              />\n            </div>\n          </div>\n        </div>\n        <div v-show=\"mode !== 'qrCode'\" class=\"input-box\">\n          <div class=\"container\" :class=\"{ active: inputFocus === 'password' }\">\n            <svg-icon icon-class=\"lock\" />\n            <div class=\"inputs\">\n              <input\n                id=\"password\"\n                v-model=\"password\"\n                type=\"password\"\n                :placeholder=\"\n                  inputFocus === 'password' ? '' : $t('login.password')\n                \"\n                @focus=\"inputFocus = 'password'\"\n                @blur=\"inputFocus = ''\"\n                @keyup.enter=\"login\"\n              />\n            </div>\n          </div>\n        </div>\n\n        <div v-show=\"mode == 'qrCode'\">\n          <div v-show=\"qrCodeSvg\" class=\"qr-code-container\">\n            <img :src=\"qrCodeSvg\" loading=\"lazy\" />\n          </div>\n          <div class=\"qr-code-info\">\n            {{ qrCodeInformation }}\n          </div>\n        </div>\n      </div>\n      <div v-show=\"mode !== 'qrCode'\" class=\"confirm\">\n        <button v-show=\"!processing\" @click=\"login\">\n          {{ $t('login.login') }}\n        </button>\n        <button v-show=\"processing\" class=\"loading\" disabled>\n          <span></span>\n          <span></span>\n          <span></span>\n        </button>\n      </div>\n      <div class=\"other-login\">\n        <a v-show=\"mode !== 'email'\" @click=\"changeMode('email')\">{{\n          $t('login.loginWithEmail')\n        }}</a>\n        <span v-show=\"mode === 'qrCode'\">|</span>\n        <a v-show=\"mode !== 'phone'\" @click=\"changeMode('phone')\">{{\n          $t('login.loginWithPhone')\n        }}</a>\n        <span v-show=\"mode !== 'qrCode'\">|</span>\n        <a v-show=\"mode !== 'qrCode'\" @click=\"changeMode('qrCode')\">\n          二维码登录\n        </a>\n      </div>\n      <div\n        v-show=\"mode !== 'qrCode'\"\n        class=\"notice\"\n        v-html=\"isElectron ? $t('login.noticeElectron') : $t('login.notice')\"\n      ></div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport QRCode from 'qrcode';\nimport md5 from 'crypto-js/md5';\nimport NProgress from 'nprogress';\nimport { mapMutations } from 'vuex';\nimport { setCookies } from '@/utils/auth';\nimport nativeAlert from '@/utils/nativeAlert';\nimport {\n  loginWithPhone,\n  loginWithEmail,\n  loginQrCodeKey,\n  loginQrCodeCheck,\n} from '@/api/auth';\n\nexport default {\n  name: 'Login',\n  data() {\n    return {\n      processing: false,\n      mode: 'qrCode',\n      countryCode: '+86',\n      phoneNumber: '',\n      email: '',\n      password: '',\n      smsCode: '',\n      inputFocus: '',\n      qrCodeKey: '',\n      qrCodeSvg: '',\n      qrCodeCheckInterval: null,\n      qrCodeInformation: '打开网易云音乐APP扫码登录',\n    };\n  },\n  computed: {\n    isElectron() {\n      return process.env.IS_ELECTRON;\n    },\n  },\n  created() {\n    if (['phone', 'email', 'qrCode'].includes(this.$route.query.mode)) {\n      this.mode = this.$route.query.mode;\n    }\n    this.getQrCodeKey();\n  },\n  beforeDestroy() {\n    clearInterval(this.qrCodeCheckInterval);\n  },\n  methods: {\n    ...mapMutations(['updateData']),\n    validatePhone() {\n      if (\n        this.countryCode === '' ||\n        this.phone === '' ||\n        this.password === ''\n      ) {\n        nativeAlert('国家区号或手机号不正确');\n        this.processing = false;\n        return false;\n      }\n      return true;\n    },\n    validateEmail() {\n      const emailReg =\n        /^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;\n      if (\n        this.email === '' ||\n        this.password === '' ||\n        !emailReg.test(this.email)\n      ) {\n        nativeAlert('邮箱不正确');\n        return false;\n      }\n      return true;\n    },\n    login() {\n      if (this.mode === 'phone') {\n        this.processing = this.validatePhone();\n        if (!this.processing) return;\n        loginWithPhone({\n          countrycode: this.countryCode.replace('+', '').replace(/\\s/g, ''),\n          phone: this.phoneNumber.replace(/\\s/g, ''),\n          password: 'fakePassword',\n          md5_password: md5(this.password).toString(),\n        })\n          .then(this.handleLoginResponse)\n          .catch(error => {\n            this.processing = false;\n            nativeAlert(`发生错误，请检查你的账号密码是否正确\\n${error}`);\n          });\n      } else {\n        this.processing = this.validateEmail();\n        if (!this.processing) return;\n        loginWithEmail({\n          email: this.email.replace(/\\s/g, ''),\n          password: 'fakePassword',\n          md5_password: md5(this.password).toString(),\n        })\n          .then(this.handleLoginResponse)\n          .catch(error => {\n            this.processing = false;\n            nativeAlert(`发生错误，请检查你的账号密码是否正确\\n${error}`);\n          });\n      }\n    },\n    handleLoginResponse(data) {\n      if (!data) {\n        this.processing = false;\n        return;\n      }\n      if (data.code === 200) {\n        setCookies(data.cookie);\n        this.updateData({ key: 'loginMode', value: 'account' });\n        this.$store.dispatch('fetchUserProfile').then(() => {\n          this.$store.dispatch('fetchLikedPlaylist').then(() => {\n            this.$router.push({ path: '/library' });\n          });\n        });\n      } else {\n        this.processing = false;\n        nativeAlert(data.msg ?? data.message ?? '账号或密码错误，请检查');\n      }\n    },\n    getQrCodeKey() {\n      return loginQrCodeKey().then(result => {\n        if (result.code === 200) {\n          this.qrCodeKey = result.data.unikey;\n          QRCode.toString(\n            `https://music.163.com/login?codekey=${this.qrCodeKey}`,\n            {\n              width: 192,\n              margin: 0,\n              color: {\n                dark: '#335eea',\n                light: '#00000000',\n              },\n              type: 'svg',\n            }\n          )\n            .then(svg => {\n              this.qrCodeSvg = `data:image/svg+xml;utf8,${encodeURIComponent(\n                svg\n              )}`;\n            })\n            .catch(err => {\n              console.error(err);\n            })\n            .finally(() => {\n              NProgress.done();\n            });\n        }\n        this.checkQrCodeLogin();\n      });\n    },\n    checkQrCodeLogin() {\n      // 清除二维码检测\n      clearInterval(this.qrCodeCheckInterval);\n      this.qrCodeCheckInterval = setInterval(() => {\n        if (this.qrCodeKey === '') return;\n        loginQrCodeCheck(this.qrCodeKey).then(result => {\n          if (result.code === 800) {\n            this.getQrCodeKey(); // 重新生成QrCode\n            this.qrCodeInformation = '二维码已失效，请重新扫码';\n          } else if (result.code === 802) {\n            this.qrCodeInformation = '扫描成功，请在手机上确认登录';\n          } else if (result.code === 801) {\n            this.qrCodeInformation = '打开网易云音乐APP扫码登录';\n          } else if (result.code === 803) {\n            clearInterval(this.qrCodeCheckInterval);\n            this.qrCodeInformation = '登录成功，请稍等...';\n            result.code = 200;\n            result.cookie = result.cookie.replaceAll(' HTTPOnly', '');\n            this.handleLoginResponse(result);\n          }\n        });\n      }, 1000);\n    },\n    changeMode(mode) {\n      this.mode = mode;\n      if (mode === 'qrCode') {\n        this.checkQrCodeLogin();\n      } else {\n        clearInterval(this.qrCodeCheckInterval);\n      }\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.login {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  margin-top: 32px;\n}\n\n.login-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n}\n\n.title {\n  font-size: 24px;\n  font-weight: 700;\n  margin-bottom: 48px;\n  color: var(--color-text);\n}\n\n.section-1 {\n  margin-bottom: 16px;\n  display: flex;\n  align-items: center;\n  img {\n    height: 64px;\n    margin: 20px;\n    user-select: none;\n  }\n}\n\n.section-2 {\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n}\n\n.input-box {\n  display: flex;\n  justify-content: flex-end;\n  margin-bottom: 16px;\n  color: var(--color-text);\n\n  .container {\n    display: flex;\n    align-items: center;\n    height: 46px;\n    background: var(--color-secondary-bg);\n    border-radius: 8px;\n    width: 300px;\n  }\n\n  .svg-icon {\n    height: 18px;\n    width: 18px;\n    color: #aaaaaa;\n    margin: {\n      left: 12px;\n      right: 6px;\n    }\n  }\n\n  .inputs {\n    display: flex;\n    width: 85%;\n  }\n\n  input {\n    font-size: 20px;\n    border: none;\n    background: transparent;\n    width: 100%;\n    font-weight: 600;\n    margin-top: -1px;\n    color: var(--color-text);\n  }\n\n  input::placeholder {\n    color: var(--color-text);\n    opacity: 0.38;\n  }\n\n  input#countryCode {\n    flex: 3;\n  }\n  input#phoneNumber {\n    flex: 12;\n  }\n\n  .active {\n    background: var(--color-primary-bg);\n    input,\n    .svg-icon {\n      color: var(--color-primary);\n    }\n  }\n}\n\n.confirm button {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  font-size: 20px;\n  font-weight: 600;\n  background-color: var(--color-primary-bg);\n  color: var(--color-primary);\n  border-radius: 8px;\n  margin-top: 24px;\n  transition: 0.2s;\n  padding: 8px;\n  width: 100%;\n  width: 300px;\n  &:hover {\n    transform: scale(1.06);\n  }\n  &:active {\n    transform: scale(0.94);\n  }\n}\n\n.other-login {\n  margin-top: 24px;\n  font-size: 13px;\n  color: var(--color-text);\n  opacity: 0.68;\n  a {\n    padding: 0 8px;\n  }\n}\n\n.notice {\n  width: 300px;\n  border-top: 1px solid rgba(128, 128, 128);\n  margin-top: 48px;\n  padding-top: 12px;\n  font-size: 12px;\n  color: var(--color-text);\n  opacity: 0.48;\n}\n\n@keyframes loading {\n  0% {\n    opacity: 0.2;\n  }\n  20% {\n    opacity: 1;\n  }\n  100% {\n    opacity: 0.2;\n  }\n}\n\nbutton.loading {\n  height: 44px;\n  cursor: unset;\n  &:hover {\n    transform: none;\n  }\n}\n.loading span {\n  width: 6px;\n  height: 6px;\n  background-color: var(--color-primary);\n  border-radius: 50%;\n  margin: 0 2px;\n  animation: loading 1.4s infinite both;\n}\n\n.loading span:nth-child(2) {\n  animation-delay: 0.2s;\n}\n\n.loading span:nth-child(3) {\n  animation-delay: 0.4s;\n}\n\n.qr-code-container {\n  background-color: var(--color-primary-bg);\n  padding: 24px 24px 21px 24px;\n  border-radius: 1.25rem;\n  margin-bottom: 12px;\n}\n.qr-code-info {\n  color: var(--color-text);\n  text-align: center;\n  margin-bottom: 28px;\n}\n</style>\n"
  },
  {
    "path": "src/views/loginUsername.vue",
    "content": "<template>\n  <div class=\"login\">\n    <div>\n      <div class=\"title\">{{ $t('login.usernameLogin') }}</div>\n      <div class=\"section\">\n        <div class=\"search-box\">\n          <div class=\"container\">\n            <svg-icon icon-class=\"search\" />\n            <div class=\"input\">\n              <input\n                v-model=\"keyword\"\n                :placeholder=\"$t('login.searchHolder')\"\n                @keydown.enter=\"throttleSearch\"\n              />\n            </div>\n          </div>\n        </div>\n      </div>\n      <div class=\"sestion\">\n        <div v-show=\"activeUser.nickname === undefined\" class=\"name\">\n          {{ $t('login.enterTip') }}\n        </div>\n        <div v-show=\"activeUser.nickname !== undefined\" class=\"name\">\n          {{ $t('login.choose') }}\n        </div>\n        <div class=\"user-list\">\n          <div\n            v-for=\"user in result\"\n            :key=\"user.id\"\n            class=\"user\"\n            :class=\"{ active: user.nickname === activeUser.nickname }\"\n            @click=\"activeUser = user\"\n          >\n            <img\n              class=\"head\"\n              :src=\"user.avatarUrl | resizeImage\"\n              loading=\"lazy\"\n            />\n            <div class=\"nickname\">\n              {{ user.nickname }}\n            </div>\n          </div>\n        </div>\n      </div>\n      <ButtonTwoTone\n        v-show=\"activeUser.nickname !== undefined\"\n        @click.native=\"confirm\"\n      >\n        {{ $t('login.confirm') }}\n      </ButtonTwoTone>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapMutations } from 'vuex';\nimport NProgress from 'nprogress';\nimport { search } from '@/api/others';\nimport { userPlaylist } from '@/api/user';\nimport { throttle } from '@/utils/common';\n\nimport ButtonTwoTone from '@/components/ButtonTwoTone.vue';\n\nexport default {\n  name: 'LoginUsername',\n  components: {\n    ButtonTwoTone,\n  },\n  data() {\n    return {\n      keyword: '',\n      result: [],\n      activeUser: {},\n    };\n  },\n  created() {\n    NProgress.done();\n  },\n  methods: {\n    ...mapMutations(['updateData']),\n    search() {\n      if (!this.keyword) return;\n      search({ keywords: this.keyword, limit: 9, type: 1002 }).then(data => {\n        this.result = data.result.userprofiles;\n        this.activeUser = this.result[0];\n      });\n    },\n    confirm() {\n      this.updateData({ key: 'user', value: this.activeUser });\n      this.updateData({ key: 'loginMode', value: 'username' });\n      userPlaylist({\n        uid: this.activeUser.userId,\n        limit: 1,\n      }).then(data => {\n        this.updateData({\n          key: 'likedSongPlaylistID',\n          value: data.playlist[0].id,\n        });\n        this.$router.push({ path: '/library' });\n      });\n    },\n    throttleSearch: throttle(function () {\n      this.search();\n    }, 500),\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.login {\n  display: flex;\n  color: var(--color-text);\n}\n\n.title {\n  font-size: 42px;\n  font-weight: 700;\n  margin-bottom: 48px;\n}\n\n.sestion {\n  margin-top: 18px;\n  .name {\n    font-size: 14px;\n    font-weight: 500;\n    margin-bottom: 8px;\n    opacity: 0.78;\n  }\n}\n\n.search-box {\n  .container {\n    display: flex;\n    align-items: center;\n    height: 48px;\n    border-radius: 11px;\n    width: 326px;\n    background: var(--color-primary-bg);\n  }\n\n  .svg-icon {\n    height: 22px;\n    width: 22px;\n    color: var(--color-primary);\n    margin: {\n      left: 12px;\n      right: 8px;\n    }\n  }\n\n  input {\n    flex: 1;\n    font-size: 22px;\n    border: none;\n    background: transparent;\n    width: 115%;\n    font-weight: 600;\n    margin-top: -1px;\n    color: var(--color-primary);\n    &::placeholder {\n      color: var(--color-primary);\n      opacity: 0.78;\n    }\n  }\n}\n\n.user-list {\n  display: flex;\n  flex-wrap: wrap;\n  margin-top: 24px;\n  margin-bottom: 24px;\n}\n\n.user {\n  margin-right: 16px;\n  margin-bottom: 16px;\n  display: flex;\n  align-items: center;\n  padding: 12px 12px 12px 16px;\n  border-radius: 8px;\n  width: 256px;\n  transition: 0.2s;\n  user-select: none;\n  .head {\n    border-radius: 50%;\n    height: 44px;\n    width: 44px;\n  }\n  .nickname {\n    font-size: 18px;\n    margin-left: 12px;\n  }\n  &:hover {\n    background: var(--color-secondary-bg);\n  }\n}\n\n.user.active {\n  transition: 0.2s;\n  background: var(--color-primary-bg);\n  .nickname {\n    color: var(--color-primary);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/views/lyrics.vue",
    "content": "<template>\n  <transition name=\"slide-up\">\n    <div\n      class=\"lyrics-page\"\n      :class=\"{ 'no-lyric': noLyric }\"\n      :data-theme=\"theme\"\n    >\n      <div\n        v-if=\"\n          (settings.lyricsBackground === 'blur') |\n            (settings.lyricsBackground === 'dynamic')\n        \"\n        class=\"lyrics-background\"\n        :class=\"{\n          'dynamic-background': settings.lyricsBackground === 'dynamic',\n        }\"\n      >\n        <div\n          class=\"top-right\"\n          :style=\"{ backgroundImage: `url(${bgImageUrl})` }\"\n        />\n        <div\n          class=\"bottom-left\"\n          :style=\"{ backgroundImage: `url(${bgImageUrl})` }\"\n        />\n      </div>\n      <div\n        v-if=\"settings.lyricsBackground === true\"\n        class=\"gradient-background\"\n        :style=\"{ background }\"\n      ></div>\n\n      <div class=\"left-side\">\n        <div>\n          <div v-if=\"settings.showLyricsTime\" class=\"date\">\n            {{ date }}\n          </div>\n          <div class=\"cover\">\n            <div class=\"cover-container\">\n              <img :src=\"imageUrl\" loading=\"lazy\" />\n              <div\n                class=\"shadow\"\n                :style=\"{ backgroundImage: `url(${imageUrl})` }\"\n              ></div>\n            </div>\n          </div>\n          <div class=\"controls\">\n            <div class=\"top-part\">\n              <div class=\"track-info\">\n                <div class=\"title\" :title=\"currentTrack.name\">\n                  <router-link\n                    v-if=\"hasList()\"\n                    :to=\"`${getListPath()}`\"\n                    @click.native=\"toggleLyrics\"\n                    >{{ currentTrack.name }}\n                  </router-link>\n                  <span v-else>\n                    {{ currentTrack.name }}\n                  </span>\n                </div>\n                <div class=\"subtitle\">\n                  <router-link\n                    v-if=\"artist.id !== 0\"\n                    :to=\"`/artist/${artist.id}`\"\n                    @click.native=\"toggleLyrics\"\n                    >{{ artist.name }}\n                  </router-link>\n                  <span v-else>\n                    {{ artist.name }}\n                  </span>\n                  <span v-if=\"album.id !== 0\">\n                    -\n                    <router-link\n                      :to=\"`/album/${album.id}`\"\n                      :title=\"album.name\"\n                      @click.native=\"toggleLyrics\"\n                      >{{ album.name }}\n                    </router-link>\n                  </span>\n                </div>\n              </div>\n              <div class=\"top-right\">\n                <div class=\"volume-control\">\n                  <button-icon :title=\"$t('player.mute')\" @click.native=\"mute\">\n                    <svg-icon v-show=\"volume > 0.5\" icon-class=\"volume\" />\n                    <svg-icon v-show=\"volume === 0\" icon-class=\"volume-mute\" />\n                    <svg-icon\n                      v-show=\"volume <= 0.5 && volume !== 0\"\n                      icon-class=\"volume-half\"\n                    />\n                  </button-icon>\n                  <div class=\"volume-bar\">\n                    <vue-slider\n                      v-model=\"volume\"\n                      :min=\"0\"\n                      :max=\"1\"\n                      :interval=\"0.01\"\n                      :drag-on-click=\"true\"\n                      :duration=\"0\"\n                      tooltip=\"none\"\n                      :dot-size=\"12\"\n                    ></vue-slider>\n                  </div>\n                </div>\n                <div class=\"buttons\">\n                  <button-icon\n                    :title=\"$t('player.like')\"\n                    @click.native=\"likeATrack(player.currentTrack.id)\"\n                  >\n                    <svg-icon\n                      :icon-class=\"\n                        player.isCurrentTrackLiked ? 'heart-solid' : 'heart'\n                      \"\n                    />\n                  </button-icon>\n                  <button-icon\n                    :title=\"$t('contextMenu.addToPlaylist')\"\n                    @click.native=\"addToPlaylist\"\n                  >\n                    <svg-icon icon-class=\"plus\" />\n                  </button-icon>\n                  <!-- <button-icon @click.native=\"openMenu\" title=\"Menu\"\n                    ><svg-icon icon-class=\"more\"\n                  /></button-icon> -->\n                </div>\n              </div>\n            </div>\n            <div class=\"progress-bar\">\n              <span>{{ formatTrackTime(player.progress) || '0:00' }}</span>\n              <div class=\"slider\">\n                <vue-slider\n                  v-model=\"player.progress\"\n                  :min=\"0\"\n                  :max=\"player.currentTrackDuration\"\n                  :interval=\"1\"\n                  :drag-on-click=\"true\"\n                  :duration=\"0\"\n                  :dot-size=\"12\"\n                  :height=\"2\"\n                  :tooltip-formatter=\"formatTrackTime\"\n                  :lazy=\"true\"\n                  :silent=\"true\"\n                ></vue-slider>\n              </div>\n              <span>{{ formatTrackTime(player.currentTrackDuration) }}</span>\n            </div>\n            <div class=\"media-controls\">\n              <button-icon\n                v-show=\"!player.isPersonalFM\"\n                :title=\"\n                  player.repeatMode === 'one'\n                    ? $t('player.repeatTrack')\n                    : $t('player.repeat')\n                \"\n                :class=\"{ active: player.repeatMode !== 'off' }\"\n                @click.native=\"switchRepeatMode\"\n              >\n                <svg-icon\n                  v-show=\"player.repeatMode !== 'one'\"\n                  icon-class=\"repeat\"\n                />\n                <svg-icon\n                  v-show=\"player.repeatMode === 'one'\"\n                  icon-class=\"repeat-1\"\n                />\n              </button-icon>\n              <div class=\"middle\">\n                <button-icon\n                  v-show=\"!player.isPersonalFM\"\n                  :title=\"$t('player.previous')\"\n                  @click.native=\"playPrevTrack\"\n                >\n                  <svg-icon icon-class=\"previous\" />\n                </button-icon>\n                <button-icon\n                  v-show=\"player.isPersonalFM\"\n                  title=\"不喜欢\"\n                  @click.native=\"moveToFMTrash\"\n                >\n                  <svg-icon icon-class=\"thumbs-down\" />\n                </button-icon>\n                <button-icon\n                  id=\"play\"\n                  :title=\"$t(player.playing ? 'player.pause' : 'player.play')\"\n                  @click.native=\"playOrPause\"\n                >\n                  <svg-icon :icon-class=\"player.playing ? 'pause' : 'play'\" />\n                </button-icon>\n                <button-icon\n                  :title=\"$t('player.next')\"\n                  @click.native=\"playNextTrack\"\n                >\n                  <svg-icon icon-class=\"next\" />\n                </button-icon>\n              </div>\n              <button-icon\n                v-show=\"!player.isPersonalFM\"\n                :title=\"$t('player.shuffle')\"\n                :class=\"{ active: player.shuffle }\"\n                @click.native=\"switchShuffle\"\n              >\n                <svg-icon icon-class=\"shuffle\" />\n              </button-icon>\n              <button-icon\n                v-show=\"\n                  isShowLyricTypeSwitch &&\n                  $store.state.settings.showLyricsTranslation &&\n                  lyricType === 'translation'\n                \"\n                :title=\"$t('player.translationLyric')\"\n                @click.native=\"switchLyricType\"\n              >\n                <span class=\"lyric-switch-icon\">译</span>\n              </button-icon>\n              <button-icon\n                v-show=\"\n                  isShowLyricTypeSwitch &&\n                  $store.state.settings.showLyricsTranslation &&\n                  lyricType === 'romaPronunciation'\n                \"\n                :title=\"$t('player.PronunciationLyric')\"\n                @click.native=\"switchLyricType\"\n              >\n                <span class=\"lyric-switch-icon\">音</span>\n              </button-icon>\n            </div>\n          </div>\n        </div>\n      </div>\n      <div class=\"right-side\">\n        <transition name=\"slide-fade\">\n          <div\n            v-show=\"!noLyric\"\n            ref=\"lyricsContainer\"\n            class=\"lyrics-container\"\n            :style=\"lyricFontSize\"\n          >\n            <div id=\"line-1\" class=\"line\"></div>\n            <div\n              v-for=\"(line, index) in lyricToShow\"\n              :id=\"`line${index}`\"\n              :key=\"index\"\n              class=\"line\"\n              :class=\"{\n                highlight: highlightLyricIndex === index,\n              }\"\n              @click=\"clickLyricLine(line.time)\"\n              @dblclick=\"clickLyricLine(line.time, true)\"\n            >\n              <div class=\"content\">\n                <span\n                  v-if=\"line.contents[0]\"\n                  @click.right=\"openLyricMenu($event, line, 0)\"\n                  >{{ line.contents[0] }}</span\n                >\n                <br />\n                <span\n                  v-if=\"\n                    line.contents[1] &&\n                    $store.state.settings.showLyricsTranslation\n                  \"\n                  class=\"translation\"\n                  @click.right=\"openLyricMenu($event, line, 1)\"\n                  >{{ line.contents[1] }}</span\n                >\n              </div>\n            </div>\n            <ContextMenu v-if=\"!noLyric\" ref=\"lyricMenu\">\n              <div class=\"item\" @click=\"copyLyric(false)\">{{\n                $t('contextMenu.copyLyric')\n              }}</div>\n              <div\n                v-if=\"\n                  rightClickLyric &&\n                  rightClickLyric.contents[1] &&\n                  $store.state.settings.showLyricsTranslation\n                \"\n                class=\"item\"\n                @click=\"copyLyric(true)\"\n                >{{ $t('contextMenu.copyLyricWithTranslation') }}</div\n              >\n            </ContextMenu>\n          </div>\n        </transition>\n      </div>\n      <div class=\"close-button\" @click=\"toggleLyrics\">\n        <button>\n          <svg-icon icon-class=\"arrow-down\" />\n        </button>\n      </div>\n      <div class=\"close-button\" style=\"left: 24px\" @click=\"fullscreen\">\n        <button>\n          <svg-icon v-if=\"isFullscreen\" icon-class=\"fullscreen-exit\" />\n          <svg-icon v-else icon-class=\"fullscreen\" />\n        </button>\n      </div>\n    </div>\n  </transition>\n</template>\n\n<script>\n// The lyrics page of Apple Music is so gorgeous, so I copy the design.\n// Some of the codes are from https://github.com/sl1673495/vue-netease-music\n\nimport { mapState, mapMutations, mapActions } from 'vuex';\nimport VueSlider from 'vue-slider-component';\nimport ContextMenu from '@/components/ContextMenu.vue';\nimport { formatTrackTime } from '@/utils/common';\nimport { getLyric } from '@/api/track';\nimport { lyricParser, copyLyric } from '@/utils/lyrics';\nimport ButtonIcon from '@/components/ButtonIcon.vue';\nimport * as Vibrant from 'node-vibrant/dist/vibrant.worker.min.js';\nimport Color from 'color';\nimport { isAccountLoggedIn } from '@/utils/auth';\nimport { hasListSource, getListSourcePath } from '@/utils/playList';\nimport locale from '@/locale';\n\nexport default {\n  name: 'Lyrics',\n  components: {\n    VueSlider,\n    ButtonIcon,\n    ContextMenu,\n  },\n  data() {\n    return {\n      lyricsInterval: null,\n      lyric: [],\n      tlyric: [],\n      romalyric: [],\n      lyricType: 'translation', // or 'romaPronunciation'\n      highlightLyricIndex: -1,\n      minimize: true,\n      background: '',\n      date: this.formatTime(new Date()),\n      isFullscreen: !!document.fullscreenElement,\n      rightClickLyric: null,\n    };\n  },\n  computed: {\n    ...mapState(['player', 'settings', 'showLyrics']),\n    currentTrack() {\n      return this.player.currentTrack;\n    },\n    volume: {\n      get() {\n        return this.player.volume;\n      },\n      set(value) {\n        this.player.volume = value;\n      },\n    },\n    imageUrl() {\n      return this.player.currentTrack?.al?.picUrl + '?param=1024y1024';\n    },\n    bgImageUrl() {\n      return this.player.currentTrack?.al?.picUrl + '?param=512y512';\n    },\n    isShowLyricTypeSwitch() {\n      return this.romalyric.length > 0 && this.tlyric.length > 0;\n    },\n    lyricToShow() {\n      return this.lyricType === 'translation'\n        ? this.lyricWithTranslation\n        : this.lyricWithRomaPronunciation;\n    },\n    lyricWithTranslation() {\n      let ret = [];\n      // 空内容的去除\n      const lyricFiltered = this.lyric.filter(({ content }) =>\n        Boolean(content)\n      );\n      // content统一转换数组形式\n      if (lyricFiltered.length) {\n        lyricFiltered.forEach(l => {\n          const { rawTime, time, content } = l;\n          const lyricItem = { time, content, contents: [content] };\n          const sameTimeTLyric = this.tlyric.find(\n            ({ rawTime: tLyricRawTime }) => tLyricRawTime === rawTime\n          );\n          if (sameTimeTLyric) {\n            const { content: tLyricContent } = sameTimeTLyric;\n            if (content) {\n              lyricItem.contents.push(tLyricContent);\n            }\n          }\n          ret.push(lyricItem);\n        });\n      } else {\n        ret = lyricFiltered.map(({ time, content }) => ({\n          time,\n          content,\n          contents: [content],\n        }));\n      }\n      return ret;\n    },\n    lyricWithRomaPronunciation() {\n      let ret = [];\n      // 空内容的去除\n      const lyricFiltered = this.lyric.filter(({ content }) =>\n        Boolean(content)\n      );\n      // content统一转换数组形式\n      if (lyricFiltered.length) {\n        lyricFiltered.forEach(l => {\n          const { rawTime, time, content } = l;\n          const lyricItem = { time, content, contents: [content] };\n          const sameTimeRomaLyric = this.romalyric.find(\n            ({ rawTime: tLyricRawTime }) => tLyricRawTime === rawTime\n          );\n          if (sameTimeRomaLyric) {\n            const { content: romaLyricContent } = sameTimeRomaLyric;\n            if (content) {\n              lyricItem.contents.push(romaLyricContent);\n            }\n          }\n          ret.push(lyricItem);\n        });\n      } else {\n        ret = lyricFiltered.map(({ time, content }) => ({\n          time,\n          content,\n          contents: [content],\n        }));\n      }\n      return ret;\n    },\n    lyricFontSize() {\n      return {\n        fontSize: `${this.$store.state.settings.lyricFontSize || 28}px`,\n      };\n    },\n    noLyric() {\n      return this.lyric.length == 0;\n    },\n    artist() {\n      return this.currentTrack?.ar\n        ? this.currentTrack.ar[0]\n        : { id: 0, name: 'unknown' };\n    },\n    album() {\n      return this.currentTrack?.al || { id: 0, name: 'unknown' };\n    },\n    theme() {\n      return this.settings.lyricsBackground === true ? 'dark' : 'auto';\n    },\n  },\n  watch: {\n    currentTrack() {\n      this.getLyric();\n      this.getCoverColor();\n    },\n    showLyrics(show) {\n      if (show) {\n        this.setLyricsInterval();\n        this.$store.commit('enableScrolling', false);\n      } else {\n        clearInterval(this.lyricsInterval);\n        this.$store.commit('enableScrolling', true);\n      }\n    },\n  },\n  created() {\n    this.getLyric();\n    this.getCoverColor();\n    this.initDate();\n    document.addEventListener('keydown', e => {\n      if (e.key === 'F11') {\n        e.preventDefault();\n        this.fullscreen();\n      }\n    });\n    document.addEventListener('fullscreenchange', () => {\n      this.isFullscreen = !!document.fullscreenElement;\n    });\n  },\n  beforeDestroy: function () {\n    if (this.timer) {\n      clearInterval(this.timer);\n    }\n  },\n  destroyed() {\n    clearInterval(this.lyricsInterval);\n  },\n  methods: {\n    ...mapMutations(['toggleLyrics', 'updateModal']),\n    ...mapActions(['likeATrack']),\n    initDate() {\n      var _this = this;\n      clearInterval(this.timer);\n      this.timer = setInterval(function () {\n        _this.date = _this.formatTime(new Date());\n      }, 1000);\n    },\n    formatTime(value) {\n      let hour = value.getHours().toString();\n      let minute = value.getMinutes().toString();\n      let second = value.getSeconds().toString();\n      return (\n        hour.padStart(2, '0') +\n        ':' +\n        minute.padStart(2, '0') +\n        ':' +\n        second.padStart(2, '0')\n      );\n    },\n    fullscreen() {\n      if (document.fullscreenElement) {\n        document.exitFullscreen();\n      } else {\n        document.documentElement.requestFullscreen();\n      }\n    },\n    addToPlaylist() {\n      if (!isAccountLoggedIn()) {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      this.$store.dispatch('fetchLikedPlaylist');\n      this.updateModal({\n        modalName: 'addTrackToPlaylistModal',\n        key: 'show',\n        value: true,\n      });\n      this.updateModal({\n        modalName: 'addTrackToPlaylistModal',\n        key: 'selectedTrackID',\n        value: this.currentTrack?.id,\n      });\n    },\n    playPrevTrack() {\n      this.player.playPrevTrack();\n    },\n    playOrPause() {\n      this.player.playOrPause();\n    },\n    playNextTrack() {\n      if (this.player.isPersonalFM) {\n        this.player.playNextFMTrack();\n      } else {\n        this.player.playNextTrack();\n      }\n    },\n    getLyric() {\n      if (!this.currentTrack.id) return;\n      return getLyric(this.currentTrack.id).then(data => {\n        if (!data?.lrc?.lyric) {\n          this.lyric = [];\n          this.tlyric = [];\n          this.romalyric = [];\n          return false;\n        } else {\n          let { lyric, tlyric, romalyric } = lyricParser(data);\n          lyric = lyric.filter(\n            l => !/^作(词|曲)\\s*(:|：)\\s*无$/.exec(l.content)\n          );\n          let includeAM =\n            lyric.length <= 10 &&\n            lyric.map(l => l.content).includes('纯音乐，请欣赏');\n          if (includeAM) {\n            let reg = /^作(词|曲)\\s*(:|：)\\s*/;\n            let author = this.currentTrack?.ar[0]?.name;\n            lyric = lyric.filter(l => {\n              let regExpArr = l.content.match(reg);\n              return (\n                !regExpArr || l.content.replace(regExpArr[0], '') !== author\n              );\n            });\n          }\n          if (lyric.length === 1 && includeAM) {\n            this.lyric = [];\n            this.tlyric = [];\n            this.romalyric = [];\n            return false;\n          } else {\n            this.lyric = lyric;\n            this.tlyric = tlyric;\n            this.romalyric = romalyric;\n            if (tlyric.length * romalyric.length > 0) {\n              this.lyricType = 'translation';\n            } else {\n              this.lyricType =\n                lyric.length > 0 ? 'translation' : 'romaPronunciation';\n            }\n            return true;\n          }\n        }\n      });\n    },\n    switchLyricType() {\n      this.lyricType =\n        this.lyricType === 'translation' ? 'romaPronunciation' : 'translation';\n    },\n    formatTrackTime(value) {\n      return formatTrackTime(value);\n    },\n    clickLyricLine(value, startPlay = false) {\n      // TODO: 双击选择还会选中文字，考虑搞个右键菜单复制歌词\n      let jumpFlag = false;\n      this.lyric.filter(function (item) {\n        if (item.content == '纯音乐，请欣赏') {\n          jumpFlag = true;\n        }\n      });\n      if (window.getSelection().toString().length === 0 && !jumpFlag) {\n        this.player.seek(value);\n      }\n      if (startPlay === true) {\n        this.player.play();\n      }\n    },\n    openLyricMenu(e, lyric, idx) {\n      this.rightClickLyric = { ...lyric, idx };\n      this.$refs.lyricMenu.openMenu(e);\n      e.preventDefault();\n    },\n    copyLyric(withTranslation) {\n      if (this.rightClickLyric) {\n        const idx = this.rightClickLyric.idx;\n        if (!withTranslation) {\n          copyLyric(this.rightClickLyric.contents[idx]);\n        } else {\n          copyLyric(this.rightClickLyric.contents.join(' '));\n        }\n      }\n    },\n    setLyricsInterval() {\n      this.lyricsInterval = setInterval(() => {\n        const progress = this.player.seek(null, false) ?? 0;\n        let oldHighlightLyricIndex = this.highlightLyricIndex;\n        this.highlightLyricIndex = this.lyric.findIndex((l, index) => {\n          const nextLyric = this.lyric[index + 1];\n          return (\n            progress >= l.time && (nextLyric ? progress < nextLyric.time : true)\n          );\n        });\n        if (oldHighlightLyricIndex !== this.highlightLyricIndex) {\n          const el = document.getElementById(`line${this.highlightLyricIndex}`);\n          if (el)\n            el.scrollIntoView({\n              behavior: 'smooth',\n              block: 'center',\n            });\n        }\n      }, 50);\n    },\n    moveToFMTrash() {\n      this.player.moveToFMTrash();\n    },\n    switchRepeatMode() {\n      this.player.switchRepeatMode();\n    },\n    switchShuffle() {\n      this.player.switchShuffle();\n    },\n    getCoverColor() {\n      if (this.settings.lyricsBackground !== true) return;\n      const cover = this.currentTrack.al?.picUrl + '?param=256y256';\n      Vibrant.from(cover, { colorCount: 1 })\n        .getPalette()\n        .then(palette => {\n          const originColor = Color.rgb(palette.DarkMuted._rgb);\n          const color = originColor.darken(0.1).rgb().string();\n          const color2 = originColor.lighten(0.28).rotate(-30).rgb().string();\n          this.background = `linear-gradient(to top left, ${color}, ${color2})`;\n        });\n    },\n    hasList() {\n      return hasListSource();\n    },\n    getListPath() {\n      return getListSourcePath();\n    },\n    mute() {\n      this.player.mute();\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.lyrics-page {\n  position: fixed;\n  top: 0;\n  right: 0;\n  left: 0;\n  bottom: 0;\n  z-index: 200;\n  background: var(--color-body-bg);\n  display: flex;\n  clip: rect(auto, auto, auto, auto);\n}\n\n.lyrics-background {\n  --contrast-lyrics-background: 75%;\n  --brightness-lyrics-background: 150%;\n}\n\n[data-theme='dark'] .lyrics-background {\n  --contrast-lyrics-background: 125%;\n  --brightness-lyrics-background: 50%;\n}\n\n.lyrics-background {\n  filter: blur(50px) contrast(var(--contrast-lyrics-background))\n    brightness(var(--brightness-lyrics-background));\n  position: absolute;\n  height: 100vh;\n  width: 100vw;\n  .top-right,\n  .bottom-left {\n    z-index: 0;\n    width: 140vw;\n    height: 140vw;\n    opacity: 0.6;\n    position: absolute;\n    background-size: cover;\n  }\n\n  .top-right {\n    right: 0;\n    top: 0;\n    mix-blend-mode: luminosity;\n  }\n\n  .bottom-left {\n    left: 0;\n    bottom: 0;\n    animation-direction: reverse;\n    animation-delay: 10s;\n  }\n}\n\n.dynamic-background > div {\n  animation: rotate 150s linear infinite;\n}\n\n@keyframes rotate {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(360deg);\n  }\n}\n\n.gradient-background {\n  position: absolute;\n  height: 100vh;\n  width: 100vw;\n}\n\n.left-side {\n  flex: 1;\n  display: flex;\n  justify-content: flex-end;\n  margin-right: 32px;\n  margin-top: 24px;\n  align-items: center;\n  transition: all 0.5s;\n\n  z-index: 1;\n\n  .date {\n    max-width: 54vh;\n    margin: 24px 0;\n    color: var(--color-text);\n    text-align: center;\n    font-size: 4rem;\n    font-weight: 600;\n    opacity: 0.88;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 1;\n    overflow: hidden;\n  }\n\n  .controls {\n    max-width: 54vh;\n    margin-top: 24px;\n    color: var(--color-text);\n\n    .title {\n      margin-top: 8px;\n      font-size: 1.4rem;\n      font-weight: 600;\n      opacity: 0.88;\n      display: -webkit-box;\n      -webkit-box-orient: vertical;\n      -webkit-line-clamp: 1;\n      overflow: hidden;\n    }\n\n    .subtitle {\n      margin-top: 4px;\n      font-size: 1rem;\n      opacity: 0.58;\n      display: -webkit-box;\n      -webkit-box-orient: vertical;\n      -webkit-line-clamp: 1;\n      overflow: hidden;\n    }\n\n    .top-part {\n      display: flex;\n      justify-content: space-between;\n\n      .top-right {\n        display: flex;\n        justify-content: space-between;\n\n        .volume-control {\n          margin: 0 10px;\n          display: flex;\n          align-items: center;\n          .volume-bar {\n            width: 84px;\n          }\n        }\n\n        .buttons {\n          display: flex;\n          align-items: center;\n\n          button {\n            margin: 0 0 0 4px;\n          }\n\n          .svg-icon {\n            height: 18px;\n            width: 18px;\n          }\n        }\n      }\n    }\n\n    .progress-bar {\n      margin-top: 22px;\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n\n      .slider {\n        width: 100%;\n        flex-grow: grow;\n        padding: 0 10px;\n      }\n\n      span {\n        font-size: 15px;\n        opacity: 0.58;\n        min-width: 28px;\n      }\n    }\n\n    .media-controls {\n      display: flex;\n      justify-content: center;\n      margin-top: 18px;\n      align-items: center;\n\n      button {\n        margin: 0;\n      }\n\n      .svg-icon {\n        opacity: 0.38;\n        height: 14px;\n        width: 14px;\n      }\n\n      .active .svg-icon {\n        opacity: 0.88;\n      }\n\n      .middle {\n        padding: 0 16px;\n        display: flex;\n        align-items: center;\n\n        button {\n          margin: 0 8px;\n        }\n\n        button#play .svg-icon {\n          height: 28px;\n          width: 28px;\n          padding: 2px;\n        }\n\n        .svg-icon {\n          opacity: 0.88;\n          height: 22px;\n          width: 22px;\n        }\n      }\n      .lyric-switch-icon {\n        color: var(--color-text);\n        font-size: 14px;\n        line-height: 14px;\n        opacity: 0.88;\n      }\n    }\n  }\n}\n\n.cover {\n  position: relative;\n\n  .cover-container {\n    position: relative;\n  }\n\n  img {\n    border-radius: 0.75em;\n    width: 54vh;\n    height: 54vh;\n    user-select: none;\n    object-fit: cover;\n  }\n\n  .shadow {\n    position: absolute;\n    top: 12px;\n    height: 54vh;\n    width: 54vh;\n    filter: blur(16px) opacity(0.6);\n    transform: scale(0.92, 0.96);\n    z-index: -1;\n    background-size: cover;\n    border-radius: 0.75em;\n  }\n}\n\n.right-side {\n  flex: 1;\n  font-weight: 600;\n  color: var(--color-text);\n  margin-right: 24px;\n  z-index: 0;\n\n  .lyrics-container {\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n    padding-left: 78px;\n    max-width: 460px;\n    overflow-y: auto;\n    transition: 0.5s;\n    scrollbar-width: none; // firefox\n\n    .line {\n      margin: 2px 0;\n      padding: 12px 18px;\n      transition: 0.5s;\n      border-radius: 12px;\n\n      &:hover {\n        background: var(--color-secondary-bg-for-transparent);\n      }\n\n      .content {\n        transform-origin: center left;\n        transform: scale(0.95);\n        transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n        user-select: none;\n\n        span {\n          opacity: 0.28;\n          cursor: default;\n          font-size: 1em;\n          transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n        }\n\n        span.translation {\n          opacity: 0.2;\n          font-size: 0.925em;\n        }\n      }\n    }\n\n    .line#line-1:hover {\n      background: unset;\n    }\n\n    .translation {\n      margin-top: 0.1em;\n    }\n\n    .highlight div.content {\n      transform: scale(1);\n      span {\n        opacity: 0.98;\n        display: inline-block;\n      }\n\n      span.translation {\n        opacity: 0.65;\n      }\n    }\n  }\n\n  ::-webkit-scrollbar {\n    display: none;\n  }\n\n  .lyrics-container .line:first-child {\n    margin-top: 50vh;\n  }\n\n  .lyrics-container .line:last-child {\n    margin-bottom: calc(50vh - 128px);\n  }\n}\n\n.close-button {\n  position: fixed;\n  top: 24px;\n  right: 24px;\n  z-index: 300;\n  border-radius: 0.75rem;\n  height: 44px;\n  width: 44px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  opacity: 0.28;\n  transition: 0.2s;\n  -webkit-app-region: no-drag;\n\n  .svg-icon {\n    color: var(--color-text);\n    padding-top: 5px;\n    height: 22px;\n    width: 22px;\n  }\n\n  &:hover {\n    background: var(--color-secondary-bg-for-transparent);\n    opacity: 0.88;\n  }\n}\n\n.lyrics-page.no-lyric {\n  .left-side {\n    transition: all 0.5s;\n    transform: translateX(27vh);\n    margin-right: 0;\n  }\n}\n\n@media (max-aspect-ratio: 10/9) {\n  .left-side {\n    display: none;\n  }\n  .right-side .lyrics-container {\n    max-width: 100%;\n  }\n}\n\n@media screen and (min-width: 1200px) {\n  .right-side .lyrics-container {\n    max-width: 600px;\n  }\n}\n\n.slide-up-enter-active,\n.slide-up-leave-active {\n  transition: all 0.4s;\n}\n\n.slide-up-enter, .slide-up-leave-to /* .fade-leave-active below version 2.1.8 */ {\n  transform: translateY(100%);\n}\n\n.slide-fade-enter-active {\n  transition: all 0.5s ease;\n}\n\n.slide-fade-leave-active {\n  transition: all 0.5s cubic-bezier(0.2, 0.2, 0, 1);\n}\n\n.slide-fade-enter,\n.slide-fade-leave-to {\n  transform: translateX(27vh);\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/views/mv.vue",
    "content": "<template>\n  <div class=\"mv-page\">\n    <div class=\"current-video\">\n      <div class=\"video\">\n        <video ref=\"videoPlayer\" class=\"plyr\"></video>\n      </div>\n      <div class=\"video-info\">\n        <div class=\"title\">\n          <router-link :to=\"'/artist/' + mv.data.artistId\">{{\n            mv.data.artistName\n          }}</router-link>\n          -\n          {{ mv.data.name }}\n          <div class=\"buttons\">\n            <button-icon class=\"button\" @click.native=\"likeMV\">\n              <svg-icon v-if=\"mv.subed\" icon-class=\"heart-solid\"></svg-icon>\n              <svg-icon v-else icon-class=\"heart\"></svg-icon>\n            </button-icon>\n            <button-icon class=\"button\" @click.native=\"openMenu\">\n              <svg-icon icon-class=\"more\"></svg-icon>\n            </button-icon>\n          </div>\n        </div>\n        <div class=\"info\">\n          {{ mv.data.playCount | formatPlayCount }} Views ·\n          {{ mv.data.publishTime }}\n        </div>\n      </div>\n    </div>\n    <div class=\"more-video\">\n      <div class=\"section-title\">{{ $t('mv.moreVideo') }}</div>\n      <MvRow :mvs=\"simiMvs\" />\n    </div>\n    <ContextMenu ref=\"mvMenu\">\n      <div class=\"item\" @click=\"copyUrl(mv.data.id)\">{{\n        $t('contextMenu.copyUrl')\n      }}</div>\n      <div class=\"item\" @click=\"openInBrowser(mv.data.id)\">{{\n        $t('contextMenu.openInBrowser')\n      }}</div>\n    </ContextMenu>\n  </div>\n</template>\n\n<script>\nimport { mvDetail, mvUrl, simiMv, likeAMV } from '@/api/mv';\nimport { isAccountLoggedIn } from '@/utils/auth';\nimport NProgress from 'nprogress';\nimport locale from '@/locale';\nimport '@/assets/css/plyr.css';\nimport Plyr from 'plyr';\n\nimport ButtonIcon from '@/components/ButtonIcon.vue';\nimport ContextMenu from '@/components/ContextMenu.vue';\nimport MvRow from '@/components/MvRow.vue';\nimport { mapActions } from 'vuex';\n\nexport default {\n  name: 'mv',\n  components: {\n    MvRow,\n    ButtonIcon,\n    ContextMenu,\n  },\n  beforeRouteUpdate(to, from, next) {\n    this.getData(to.params.id);\n    next();\n  },\n  data() {\n    return {\n      mv: {\n        url: '',\n        data: {\n          name: '',\n          artistName: '',\n          playCount: '',\n          publishTime: '',\n        },\n      },\n      player: null,\n      simiMvs: [],\n    };\n  },\n  mounted() {\n    let videoOptions = {\n      settings: ['quality'],\n      autoplay: false,\n      quality: {\n        default: 1080,\n        options: [1080, 720, 480, 240],\n      },\n    };\n    if (this.$route.query.autoplay === 'true') videoOptions.autoplay = true;\n    this.player = new Plyr(this.$refs.videoPlayer, videoOptions);\n    this.player.volume = this.$store.state.player.volume;\n    this.player.on('playing', () => {\n      this.$store.state.player.pause();\n    });\n    this.getData(this.$route.params.id);\n    console.log('网易云你这mv音频码率也太糊了吧🙄');\n  },\n  methods: {\n    ...mapActions(['showToast']),\n    getData(id) {\n      mvDetail(id).then(data => {\n        this.mv = data;\n        let requests = data.data.brs.map(br => {\n          return mvUrl({ id, r: br.br });\n        });\n        Promise.all(requests).then(results => {\n          let sources = results.map(result => {\n            return {\n              src: result.data.url.replace(/^http:/, 'https:'),\n              type: 'video/mp4',\n              size: result.data.r,\n            };\n          });\n          this.player.source = {\n            type: 'video',\n            title: this.mv.data.name,\n            sources: sources,\n            poster: this.mv.data.cover.replace(/^http:/, 'https:'),\n          };\n          NProgress.done();\n        });\n      });\n      simiMv(id).then(data => {\n        this.simiMvs = data.mvs;\n      });\n    },\n    likeMV() {\n      if (!isAccountLoggedIn()) {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      likeAMV({\n        mvid: this.mv.data.id,\n        t: this.mv.subed ? 0 : 1,\n      }).then(data => {\n        if (data.code === 200) this.mv.subed = !this.mv.subed;\n      });\n    },\n    openMenu(e) {\n      this.$refs.mvMenu.openMenu(e);\n    },\n    copyUrl(id) {\n      let showToast = this.showToast;\n      this.$copyText(`https://music.163.com/#/mv?id=${id}`)\n        .then(function () {\n          showToast(locale.t('toast.copied'));\n        })\n        .catch(error => {\n          showToast(`${locale.t('toast.copyFailed')}${error}`);\n        });\n    },\n    openInBrowser(id) {\n      const url = `https://music.163.com/#/mv?id=${id}`;\n      window.open(url);\n    },\n  },\n};\n</script>\n<style lang=\"scss\" scoped>\n.video {\n  --plyr-color-main: #335eea;\n  --plyr-control-radius: 8px;\n}\n\n.mv-page {\n  width: 100%;\n  margin-top: 32px;\n}\n.current-video {\n  width: 100%;\n}\n.video {\n  border-radius: 16px;\n  background: transparent;\n  overflow: hidden;\n  max-height: 100vh;\n}\n\n.video-info {\n  margin-top: 12px;\n  color: var(--color-text);\n  .title {\n    font-size: 24px;\n    font-weight: 600;\n  }\n  .artist {\n    font-size: 14px;\n    opacity: 0.88;\n    margin-top: 2px;\n    font-weight: 600;\n  }\n  .info {\n    font-size: 12px;\n    opacity: 0.68;\n    margin-top: 12px;\n  }\n}\n\n.more-video {\n  margin-top: 48px;\n  .section-title {\n    font-size: 18px;\n    font-weight: 600;\n    color: var(--color-text);\n    opacity: 0.88;\n    margin-bottom: 12px;\n  }\n}\n\n.buttons {\n  display: inline-block;\n  .button {\n    display: inline-block;\n  }\n  .svg-icon {\n    height: 18px;\n    width: 18px;\n    color: var(--color-primary);\n  }\n}\n</style>\n"
  },
  {
    "path": "src/views/newAlbum.vue",
    "content": "<template>\n  <div class=\"newAlbum\">\n    <h1>{{ $t('home.newAlbum') }}</h1>\n    <div class=\"playlist-row\">\n      <div class=\"playlists\">\n        <CoverRow\n          type=\"album\"\n          :items=\"albums\"\n          sub-text=\"artist\"\n          :show-play-button=\"true\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { newAlbums } from '@/api/album';\nimport NProgress from 'nprogress';\n\nimport CoverRow from '@/components/CoverRow.vue';\n\nexport default {\n  components: {\n    CoverRow,\n  },\n  data() {\n    return {\n      albums: [],\n    };\n  },\n  created() {\n    newAlbums({\n      area: 'EA',\n      limit: 100,\n    }).then(data => {\n      this.albums = data.albums;\n      NProgress.done();\n    });\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\nh1 {\n  color: var(--color-text);\n  font-size: 56px;\n}\n</style>\n"
  },
  {
    "path": "src/views/next.vue",
    "content": "<template>\n  <div class=\"next-tracks\">\n    <h1>{{ $t('next.nowPlaying') }}</h1>\n    <TrackList\n      :tracks=\"[currentTrack]\"\n      type=\"playlist\"\n      dbclick-track-func=\"none\"\n    />\n    <h1 v-show=\"playNextList.length > 0\"\n      >插队播放\n      <button @click=\"player.clearPlayNextList()\">清除队列</button>\n    </h1>\n    <TrackList\n      v-show=\"playNextList.length > 0\"\n      :tracks=\"playNextTracks\"\n      type=\"playlist\"\n      :highlight-playing-track=\"false\"\n      dbclick-track-func=\"playTrackOnListByID\"\n      item-key=\"id+index\"\n      :extra-context-menu-item=\"['removeTrackFromQueue']\"\n    />\n    <h1>{{ $t('next.nextUp') }}</h1>\n    <TrackList\n      :tracks=\"filteredTracks\"\n      type=\"playlist\"\n      :highlight-playing-track=\"false\"\n      dbclick-track-func=\"playTrackOnListByID\"\n    />\n  </div>\n</template>\n\n<script>\nimport { mapState, mapActions } from 'vuex';\nimport { getTrackDetail } from '@/api/track';\nimport TrackList from '@/components/TrackList.vue';\n\nexport default {\n  name: 'Next',\n  components: {\n    TrackList,\n  },\n  data() {\n    return {\n      tracks: [],\n    };\n  },\n  computed: {\n    ...mapState(['player']),\n    currentTrack() {\n      return this.player.currentTrack;\n    },\n    playerShuffle() {\n      return this.player.shuffle;\n    },\n    filteredTracks() {\n      let trackIDs = this.player.list.slice(\n        this.player.current + 1,\n        this.player.current + 100\n      );\n      return trackIDs\n        .map(tid => this.tracks.find(t => t.id === tid))\n        .filter(t => t);\n    },\n    playNextList() {\n      return this.player.playNextList;\n    },\n    playNextTracks() {\n      return this.playNextList.map(tid => {\n        return this.tracks.find(t => t.id === tid);\n      });\n    },\n  },\n  watch: {\n    currentTrack() {\n      this.loadTracks();\n    },\n    playerShuffle() {\n      this.loadTracks();\n    },\n    playNextList() {\n      this.loadTracks();\n    },\n  },\n  activated() {\n    this.loadTracks();\n    this.$parent.$refs.scrollbar.restorePosition();\n  },\n  methods: {\n    ...mapActions(['playTrackOnListByID']),\n    loadTracks() {\n      // 获取播放列表当前歌曲后100首歌\n      let trackIDs = this.player.list.slice(\n        this.player.current + 1,\n        this.player.current + 100\n      );\n\n      // 将playNextList的歌曲加进trackIDs\n      trackIDs.push(...this.playNextList);\n\n      // 获取已经加载了的歌曲\n      let loadedTrackIDs = this.tracks.map(t => t.id);\n\n      if (trackIDs.length > 0) {\n        getTrackDetail(trackIDs.join(',')).then(data => {\n          let newTracks = data.songs.filter(\n            t => !loadedTrackIDs.includes(t.id)\n          );\n          this.tracks.push(...newTracks);\n        });\n      }\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\nh1 {\n  margin-top: 36px;\n  margin-bottom: 18px;\n  cursor: default;\n  color: var(--color-text);\n  display: flex;\n  justify-content: space-between;\n  button {\n    color: var(--color-text);\n    border-radius: 8px;\n    padding: 0 14px;\n    display: flex;\n    justify-content: center;\n    align-items: center;\n    transition: 0.2s;\n    opacity: 0.68;\n    font-weight: 500;\n    &:hover {\n      opacity: 1;\n      background: var(--color-secondary-bg);\n    }\n    &:active {\n      opacity: 1;\n      transform: scale(0.92);\n    }\n  }\n}\n</style>\n"
  },
  {
    "path": "src/views/playlist.vue",
    "content": "<template>\n  <div v-show=\"show\" class=\"playlist\">\n    <div\n      v-if=\"specialPlaylistInfo === undefined && !isLikeSongsPage\"\n      class=\"playlist-info\"\n    >\n      <Cover\n        :id=\"playlist.id\"\n        :image-url=\"playlist.coverImgUrl | resizeImage(1024)\"\n        :show-play-button=\"true\"\n        :always-show-shadow=\"true\"\n        :click-cover-to-play=\"true\"\n        :fixed-size=\"288\"\n        type=\"playlist\"\n        :cover-hover=\"false\"\n        :play-button-size=\"18\"\n        @click.right.native=\"openMenu\"\n      />\n      <div class=\"info\">\n        <div class=\"title\" @click.right=\"openMenu\"\n          ><span v-if=\"playlist.privacy === 10\" class=\"lock-icon\">\n            <svg-icon icon-class=\"lock\" /></span\n          >{{ playlist.name }}</div\n        >\n        <div class=\"artist\">\n          Playlist by\n          <span\n            v-if=\"\n              [\n                5277771961, 5277965913, 5277969451, 5277778542, 5278068783,\n              ].includes(playlist.id)\n            \"\n            style=\"font-weight: 600\"\n            >Apple Music</span\n          >\n          <a\n            v-else\n            :href=\"`https://music.163.com/#/user/home?id=${playlist.creator.userId}`\"\n            target=\"blank\"\n            >{{ playlist.creator.nickname }}</a\n          >\n        </div>\n        <div class=\"date-and-count\">\n          {{ $t('playlist.updatedAt') }}\n          {{ playlist.updateTime | formatDate }} · {{ playlist.trackCount }}\n          {{ $t('common.songs') }}\n        </div>\n        <div class=\"description\" @click=\"toggleFullDescription\">\n          {{ playlist.description }}\n        </div>\n        <div class=\"buttons\">\n          <ButtonTwoTone icon-class=\"play\" @click.native=\"playPlaylistByID()\">\n            {{ $t('common.play') }}\n          </ButtonTwoTone>\n          <ButtonTwoTone\n            v-if=\"playlist.creator.userId !== data.user.userId\"\n            :icon-class=\"playlist.subscribed ? 'heart-solid' : 'heart'\"\n            :icon-button=\"true\"\n            :horizontal-padding=\"0\"\n            :color=\"playlist.subscribed ? 'blue' : 'grey'\"\n            :text-color=\"playlist.subscribed ? '#335eea' : ''\"\n            :background-color=\"\n              playlist.subscribed ? 'var(--color-secondary-bg)' : ''\n            \"\n            @click.native=\"likePlaylist\"\n          >\n          </ButtonTwoTone>\n          <ButtonTwoTone\n            icon-class=\"more\"\n            :icon-button=\"true\"\n            :horizontal-padding=\"0\"\n            color=\"grey\"\n            @click.native=\"openMenu\"\n          >\n          </ButtonTwoTone>\n        </div>\n      </div>\n      <div v-if=\"displaySearchInPlaylist\" class=\"search-box\">\n        <div class=\"container\" :class=\"{ active: inputFocus }\">\n          <svg-icon icon-class=\"search\" />\n          <div class=\"input\">\n            <input\n              v-model.trim=\"inputSearchKeyWords\"\n              v-focus\n              :placeholder=\"inputFocus ? '' : $t('playlist.search')\"\n              @input=\"inputDebounce()\"\n              @focus=\"inputFocus = true\"\n              @blur=\"inputFocus = false\"\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n    <div v-if=\"specialPlaylistInfo !== undefined\" class=\"special-playlist\">\n      <div\n        class=\"title\"\n        :class=\"specialPlaylistInfo.gradient\"\n        @click.right=\"openMenu\"\n      >\n        <!-- <img :src=\"playlist.coverImgUrl | resizeImage\" /> -->\n        {{ specialPlaylistInfo.name }}\n      </div>\n      <div class=\"subtitle\"\n        >{{ playlist.englishTitle }} · {{ playlist.updateFrequency }}\n      </div>\n\n      <div class=\"buttons\">\n        <ButtonTwoTone\n          class=\"play-button\"\n          icon-class=\"play\"\n          color=\"grey\"\n          @click.native=\"playPlaylistByID()\"\n        >\n          {{ $t('common.play') }}\n        </ButtonTwoTone>\n        <ButtonTwoTone\n          v-if=\"playlist.creator.userId !== data.user.userId\"\n          :icon-class=\"playlist.subscribed ? 'heart-solid' : 'heart'\"\n          :icon-button=\"true\"\n          :horizontal-padding=\"0\"\n          :color=\"playlist.subscribed ? 'blue' : 'grey'\"\n          :text-color=\"playlist.subscribed ? '#335eea' : ''\"\n          :background-color=\"\n            playlist.subscribed ? 'var(--color-secondary-bg)' : ''\n          \"\n          @click.native=\"likePlaylist\"\n        >\n        </ButtonTwoTone>\n        <ButtonTwoTone\n          icon-class=\"more\"\n          :icon-button=\"true\"\n          :horizontal-padding=\"0\"\n          color=\"grey\"\n          @click.native=\"openMenu\"\n        >\n        </ButtonTwoTone>\n      </div>\n    </div>\n\n    <div v-if=\"isLikeSongsPage\" class=\"user-info\">\n      <h1>\n        <img\n          class=\"avatar\"\n          :src=\"data.user.avatarUrl | resizeImage\"\n          loading=\"lazy\"\n        />\n        {{ data.user.nickname }}{{ $t('library.sLikedSongs') }}\n      </h1>\n      <div class=\"search-box-likepage\" @click=\"searchInPlaylist()\">\n        <div class=\"container\" :class=\"{ active: inputFocus }\">\n          <svg-icon icon-class=\"search\" />\n          <div class=\"input\" :style=\"{ width: searchInputWidth }\">\n            <input\n              v-if=\"displaySearchInPlaylist\"\n              v-model.trim=\"inputSearchKeyWords\"\n              v-focus\n              :placeholder=\"inputFocus ? '' : $t('playlist.search')\"\n              @input=\"inputDebounce()\"\n              @focus=\"inputFocus = true\"\n              @blur=\"inputFocus = false\"\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <TrackList\n      :id=\"playlist.id\"\n      :tracks=\"filteredTracks\"\n      type=\"playlist\"\n      :extra-context-menu-item=\"\n        isUserOwnPlaylist ? ['removeTrackFromPlaylist'] : []\n      \"\n    />\n\n    <div class=\"load-more\">\n      <ButtonTwoTone\n        v-show=\"hasMore\"\n        color=\"grey\"\n        :loading=\"loadingMore\"\n        @click.native=\"loadMore(100)\"\n        >{{ $t('explore.loadMore') }}</ButtonTwoTone\n      >\n    </div>\n\n    <Modal\n      :show=\"showFullDescription\"\n      :close=\"toggleFullDescription\"\n      :show-footer=\"false\"\n      :click-outside-hide=\"true\"\n      title=\"歌单介绍\"\n      >{{ playlist.description }}</Modal\n    >\n\n    <ContextMenu ref=\"playlistMenu\">\n      <!-- <div class=\"item\">{{ $t('contextMenu.addToQueue') }}</div> -->\n      <div class=\"item\" @click=\"likePlaylist(true)\">{{\n        playlist.subscribed\n          ? $t('contextMenu.removeFromLibrary')\n          : $t('contextMenu.saveToLibrary')\n      }}</div>\n      <div class=\"item\" @click=\"searchInPlaylist()\">{{\n        $t('contextMenu.searchInPlaylist')\n      }}</div>\n      <div\n        v-if=\"playlist.creator.userId === data.user.userId\"\n        class=\"item\"\n        @click=\"editPlaylist\"\n        >编辑歌单信息</div\n      >\n      <div\n        v-if=\"playlist.creator.userId === data.user.userId\"\n        class=\"item\"\n        @click=\"deletePlaylist\"\n        >删除歌单</div\n      >\n    </ContextMenu>\n  </div>\n</template>\n\n<script>\nimport { mapMutations, mapActions, mapState } from 'vuex';\nimport NProgress from 'nprogress';\nimport {\n  getPlaylistDetail,\n  subscribePlaylist,\n  deletePlaylist,\n} from '@/api/playlist';\nimport { getTrackDetail } from '@/api/track';\nimport { isAccountLoggedIn } from '@/utils/auth';\nimport nativeAlert from '@/utils/nativeAlert';\nimport locale from '@/locale';\n\nimport ButtonTwoTone from '@/components/ButtonTwoTone.vue';\nimport ContextMenu from '@/components/ContextMenu.vue';\nimport TrackList from '@/components/TrackList.vue';\nimport Cover from '@/components/Cover.vue';\nimport Modal from '@/components/Modal.vue';\n\nconst specialPlaylist = {\n  2829816518: {\n    name: '欧美私人订制',\n    gradient: 'gradient-pink-purple-blue',\n  },\n  2890490211: {\n    name: '助眠鸟鸣声',\n    gradient: 'gradient-green',\n  },\n  5089855855: {\n    name: '夜的胡思乱想',\n    gradient: 'gradient-moonstone-blue',\n  },\n  2888212971: {\n    name: '全球百大DJ',\n    gradient: 'gradient-orange-red',\n  },\n  2829733864: {\n    name: '睡眠伴侣',\n    gradient: 'gradient-midnight-blue',\n  },\n  2829844572: {\n    name: '洗澡时听的歌',\n    gradient: 'gradient-yellow',\n  },\n  2920647537: {\n    name: '还是会想你',\n    gradient: 'gradient-dark-blue-midnight-blue',\n  },\n  2890501416: {\n    name: '助眠白噪声',\n    gradient: 'gradient-sky-blue',\n  },\n  5217150082: {\n    name: '摇滚唱片行',\n    gradient: 'gradient-yellow-red',\n  },\n  2829961453: {\n    name: '古风音乐大赏',\n    gradient: 'gradient-fog',\n  },\n  4923261701: {\n    name: 'Trance',\n    gradient: 'gradient-light-red-light-blue ',\n  },\n  5212729721: {\n    name: '欧美点唱机',\n    gradient: 'gradient-indigo-pink-yellow',\n  },\n  3103434282: {\n    name: '甜蜜少女心',\n    gradient: 'gradient-pink',\n  },\n  2829896389: {\n    name: '日系私人订制',\n    gradient: 'gradient-yellow-pink',\n  },\n  2829779628: {\n    name: '运动随身听',\n    gradient: 'gradient-orange-red',\n  },\n  2860654884: {\n    name: '独立女声精选',\n    gradient: 'gradient-sharp-blue',\n  },\n  898150: {\n    name: '浪漫婚礼专用',\n    gradient: 'gradient-pink',\n  },\n  2638104052: {\n    name: '牛奶泡泡浴',\n    gradient: 'gradient-fog',\n  },\n  5317236517: {\n    name: '后朋克精选',\n    gradient: 'gradient-pink-purple-blue',\n  },\n  2821115454: {\n    name: '一周原创发现',\n    gradient: 'gradient-blue-purple',\n  },\n  2829883282: {\n    name: '华语私人雷达',\n    gradient: 'gradient-yellow-red',\n  },\n  3136952023: {\n    name: '私人雷达',\n    gradient: 'gradient-radar',\n  },\n};\n\nexport default {\n  name: 'Playlist',\n  components: {\n    Cover,\n    ButtonTwoTone,\n    TrackList,\n    Modal,\n    ContextMenu,\n  },\n  directives: {\n    focus: {\n      inserted: function (el) {\n        el.focus();\n      },\n    },\n  },\n  data() {\n    return {\n      show: false,\n      playlist: {\n        id: 0,\n        coverImgUrl: '',\n        creator: {\n          userId: '',\n        },\n        trackIds: [],\n      },\n      showFullDescription: false,\n      tracks: [],\n      loadingMore: false,\n      hasMore: false,\n      lastLoadedTrackIndex: 9,\n      displaySearchInPlaylist: false, // 是否显示搜索框\n      searchKeyWords: '', // 搜索使用的关键字\n      inputSearchKeyWords: '', // 搜索框中正在输入的关键字\n      inputFocus: false,\n      debounceTimeout: null,\n      searchInputWidth: '0px', // 搜索框宽度\n    };\n  },\n  computed: {\n    ...mapState(['player', 'data']),\n    isLikeSongsPage() {\n      return this.$route.name === 'likedSongs';\n    },\n    specialPlaylistInfo() {\n      return specialPlaylist[this.playlist.id];\n    },\n    isUserOwnPlaylist() {\n      return (\n        this.playlist.creator.userId === this.data.user.userId &&\n        this.playlist.id !== this.data.likedSongPlaylistID\n      );\n    },\n    filteredTracks() {\n      return this.tracks.filter(\n        track =>\n          (track.name &&\n            track.name\n              .toLowerCase()\n              .includes(this.searchKeyWords.toLowerCase())) ||\n          (track.al.name &&\n            track.al.name\n              .toLowerCase()\n              .includes(this.searchKeyWords.toLowerCase())) ||\n          track.ar.find(\n            artist =>\n              artist.name &&\n              artist.name\n                .toLowerCase()\n                .includes(this.searchKeyWords.toLowerCase())\n          )\n      );\n    },\n  },\n  created() {\n    if (this.$route.name === 'likedSongs') {\n      this.loadData(this.data.likedSongPlaylistID);\n    } else {\n      this.loadData(this.$route.params.id);\n    }\n    setTimeout(() => {\n      if (!this.show) NProgress.start();\n    }, 1000);\n  },\n  methods: {\n    ...mapMutations(['appendTrackToPlayerList']),\n    ...mapActions(['playFirstTrackOnList', 'playTrackOnListByID', 'showToast']),\n    playPlaylistByID(trackID = 'first') {\n      let trackIDs = this.playlist.trackIds.map(t => t.id);\n      this.$store.state.player.replacePlaylist(\n        trackIDs,\n        this.playlist.id,\n        'playlist',\n        trackID\n      );\n    },\n    likePlaylist(toast = false) {\n      if (!isAccountLoggedIn()) {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      subscribePlaylist({\n        id: this.playlist.id,\n        t: this.playlist.subscribed ? 2 : 1,\n      }).then(data => {\n        if (data.code === 200) {\n          this.playlist.subscribed = !this.playlist.subscribed;\n          if (toast === true)\n            this.showToast(\n              this.playlist.subscribed ? '已保存到音乐库' : '已从音乐库删除'\n            );\n        }\n        getPlaylistDetail(this.id, true).then(data => {\n          this.playlist = data.playlist;\n        });\n      });\n    },\n    loadData(id, next = undefined) {\n      this.id = id;\n      getPlaylistDetail(this.id, true)\n        .then(data => {\n          this.playlist = data.playlist;\n          this.tracks = data.playlist.tracks;\n          NProgress.done();\n          if (next !== undefined) next();\n          this.show = true;\n          this.lastLoadedTrackIndex = data.playlist.tracks.length - 1;\n          return data;\n        })\n        .then(() => {\n          if (this.playlist.trackCount > this.tracks.length) {\n            this.loadingMore = true;\n            this.loadMore();\n          }\n        });\n    },\n    loadMore(loadNum = 100) {\n      let trackIDs = this.playlist.trackIds.filter((t, index) => {\n        if (\n          index > this.lastLoadedTrackIndex &&\n          index <= this.lastLoadedTrackIndex + loadNum\n        ) {\n          return t;\n        }\n      });\n      trackIDs = trackIDs.map(t => t.id);\n      getTrackDetail(trackIDs.join(',')).then(data => {\n        this.tracks.push(...data.songs);\n        this.lastLoadedTrackIndex += trackIDs.length;\n        this.loadingMore = false;\n        if (this.lastLoadedTrackIndex + 1 === this.playlist.trackIds.length) {\n          this.hasMore = false;\n        } else {\n          this.hasMore = true;\n        }\n      });\n    },\n    openMenu(e) {\n      this.$refs.playlistMenu.openMenu(e);\n    },\n    deletePlaylist() {\n      if (!isAccountLoggedIn()) {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      let confirmation = confirm(`确定要删除歌单 ${this.playlist.name}？`);\n      if (confirmation === true) {\n        deletePlaylist(this.playlist.id).then(data => {\n          if (data.code === 200) {\n            nativeAlert(`已删除歌单 ${this.playlist.name}`);\n            this.$router.go(-1);\n          } else {\n            nativeAlert('发生错误');\n          }\n        });\n      }\n    },\n    editPlaylist() {\n      nativeAlert('此功能开发中');\n    },\n    searchInPlaylist() {\n      this.displaySearchInPlaylist =\n        !this.displaySearchInPlaylist || this.isLikeSongsPage;\n      if (this.displaySearchInPlaylist == false) {\n        this.searchKeyWords = '';\n        this.inputSearchKeyWords = '';\n      } else {\n        this.searchInputWidth = '172px';\n        this.loadMore(500);\n      }\n    },\n    removeTrack(trackID) {\n      if (!isAccountLoggedIn()) {\n        this.showToast(locale.t('toast.needToLogin'));\n        return;\n      }\n      this.tracks = this.tracks.filter(t => t.id !== trackID);\n    },\n    inputDebounce() {\n      if (this.debounceTimeout) clearTimeout(this.debounceTimeout);\n      this.debounceTimeout = setTimeout(() => {\n        this.searchKeyWords = this.inputSearchKeyWords;\n      }, 600);\n    },\n    toggleFullDescription() {\n      this.showFullDescription = !this.showFullDescription;\n      if (this.showFullDescription) {\n        this.$store.commit('enableScrolling', false);\n      } else {\n        this.$store.commit('enableScrolling', true);\n      }\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.playlist {\n  margin-top: 32px;\n}\n.playlist-info {\n  display: flex;\n  margin-bottom: 72px;\n  position: relative;\n  .info {\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    flex: 1;\n    margin-left: 56px;\n    .title {\n      font-size: 36px;\n      font-weight: 700;\n      color: var(--color-text);\n\n      .lock-icon {\n        opacity: 0.28;\n        color: var(--color-text);\n        margin-right: 8px;\n        .svg-icon {\n          height: 26px;\n          width: 26px;\n        }\n      }\n    }\n    .artist {\n      font-size: 18px;\n      opacity: 0.88;\n      color: var(--color-text);\n      margin-top: 24px;\n    }\n    .date-and-count {\n      font-size: 14px;\n      opacity: 0.68;\n      color: var(--color-text);\n      margin-top: 2px;\n    }\n    .description {\n      font-size: 14px;\n      opacity: 0.68;\n      color: var(--color-text);\n      margin-top: 24px;\n      display: -webkit-box;\n      -webkit-box-orient: vertical;\n      -webkit-line-clamp: 3;\n      overflow: hidden;\n      cursor: pointer;\n      &:hover {\n        transition: opacity 0.3s;\n        opacity: 0.88;\n      }\n    }\n    .buttons {\n      margin-top: 32px;\n      display: flex;\n      button {\n        margin-right: 16px;\n      }\n    }\n  }\n}\n\n.special-playlist {\n  margin-top: 192px;\n  margin-bottom: 128px;\n  border-radius: 1.25em;\n  text-align: center;\n\n  @keyframes letterSpacing4 {\n    from {\n      letter-spacing: 0px;\n    }\n\n    to {\n      letter-spacing: 4px;\n    }\n  }\n\n  @keyframes letterSpacing1 {\n    from {\n      letter-spacing: 0px;\n    }\n\n    to {\n      letter-spacing: 1px;\n    }\n  }\n\n  .title {\n    font-size: 84px;\n    line-height: 1.05;\n    font-weight: 700;\n    text-transform: uppercase;\n\n    letter-spacing: 4px;\n    animation-duration: 0.8s;\n    animation-name: letterSpacing4;\n    -webkit-text-fill-color: transparent;\n    background-clip: text;\n    // background-image: linear-gradient(\n    //   225deg,\n    //   var(--color-primary),\n    //   var(--color-primary)\n    // );\n\n    img {\n      height: 78px;\n      border-radius: 0.125em;\n      margin-right: 24px;\n    }\n  }\n  .subtitle {\n    font-size: 18px;\n    letter-spacing: 1px;\n    margin: 28px 0 54px 0;\n    animation-duration: 0.8s;\n    animation-name: letterSpacing1;\n    text-transform: uppercase;\n    color: var(--color-text);\n  }\n  .buttons {\n    margin-top: 32px;\n    display: flex;\n    justify-content: center;\n    button {\n      margin-right: 16px;\n    }\n  }\n}\n\n.gradient-test {\n  background-image: linear-gradient(to left, #92fe9d 0%, #00c9ff 100%);\n}\n\n[data-theme='dark'] {\n  .gradient-radar {\n    background-image: linear-gradient(to left, #92fe9d 0%, #00c9ff 100%);\n  }\n}\n\n.gradient-radar {\n  background-image: linear-gradient(to left, #0ba360 0%, #3cba92 100%);\n}\n\n.gradient-blue-purple {\n  background-image: linear-gradient(\n    45deg,\n    #89c4f5 0%,\n    #6284ff 42%,\n    #ff0000 100%\n  );\n}\n\n.gradient-sharp-blue {\n  background-image: linear-gradient(45deg, #00c6fb 0%, #005bea 100%);\n}\n\n.gradient-yellow-pink {\n  background-image: linear-gradient(45deg, #f6d365 0%, #fda085 100%);\n}\n\n.gradient-pink {\n  background-image: linear-gradient(45deg, #ee9ca7 0%, #ffdde1 100%);\n}\n\n.gradient-indigo-pink-yellow {\n  background-image: linear-gradient(\n    43deg,\n    #4158d0 0%,\n    #c850c0 46%,\n    #ffcc70 100%\n  );\n}\n\n.gradient-light-red-light-blue {\n  background-image: linear-gradient(\n    225deg,\n    hsl(190, 30%, 50%) 0%,\n    #081abb 38%,\n    #ec3841 58%,\n    hsl(13, 99%, 49%) 100%\n  );\n}\n\n.gradient-fog {\n  background: linear-gradient(-180deg, #bcc5ce 0%, #929ead 98%),\n    radial-gradient(\n      at top left,\n      rgba(255, 255, 255, 0.3) 0%,\n      rgba(0, 0, 0, 0.3) 100%\n    );\n  background-blend-mode: screen;\n}\n\n.gradient-red {\n  background-image: linear-gradient(213deg, #ff0844 0%, #ffb199 100%);\n}\n\n.gradient-sky-blue {\n  background-image: linear-gradient(147deg, #48c6ef 0%, #6f86d6 100%);\n}\n\n.gradient-dark-blue-midnight-blue {\n  background-image: linear-gradient(213deg, #09203f 0%, #537895 100%);\n}\n\n.gradient-yellow-red {\n  background: linear-gradient(147deg, #fec867 0%, #f72c61 100%);\n}\n\n.gradient-yellow {\n  background: linear-gradient(147deg, #fceb02 0%, #fec401 100%);\n}\n\n.gradient-midnight-blue {\n  background-image: linear-gradient(-20deg, #2b5876 0%, #4e4376 100%);\n}\n\n.gradient-orange-red {\n  background-image: linear-gradient(147deg, #ffe53b 0%, #ff2525 74%);\n}\n\n.gradient-moonstone-blue {\n  background-image: linear-gradient(\n    147deg,\n    hsl(200, 34%, 8%) 0%,\n    hsl(204, 35%, 38%) 50%,\n    hsl(200, 34%, 18%) 100%\n  );\n}\n\n.gradient-pink-purple-blue {\n  background-image: linear-gradient(\n    to right,\n    #ff3cac 0%,\n    #784ba0 50%,\n    #2b86c5 100%\n  ) !important;\n}\n\n.gradient-green {\n  background-image: linear-gradient(\n    90deg,\n    #c6f6d5,\n    #68d391,\n    #38b2ac\n  ) !important;\n}\n\n.user-info {\n  h1 {\n    font-size: 42px;\n    position: relative;\n    color: var(--color-text);\n    .avatar {\n      height: 44px;\n      margin-right: 12px;\n      vertical-align: -7px;\n      border-radius: 50%;\n      border: rgba(0, 0, 0, 0.2);\n    }\n  }\n}\n\n.search-box {\n  display: flex;\n  position: absolute;\n  right: 20px;\n  bottom: -55px;\n  justify-content: flex-end;\n  -webkit-app-region: no-drag;\n\n  .container {\n    display: flex;\n    align-items: center;\n    height: 32px;\n    background: var(--color-secondary-bg-for-transparent);\n    border-radius: 8px;\n    width: 200px;\n  }\n\n  .svg-icon {\n    height: 15px;\n    width: 15px;\n    color: var(--color-text);\n    opacity: 0.28;\n    margin: {\n      left: 8px;\n      right: 4px;\n    }\n  }\n\n  input {\n    font-size: 16px;\n    border: none;\n    background: transparent;\n    width: 96%;\n    font-weight: 600;\n    margin-top: -1px;\n    color: var(--color-text);\n  }\n\n  .active {\n    background: var(--color-primary-bg-for-transparent);\n    input,\n    .svg-icon {\n      opacity: 1;\n      color: var(--color-primary);\n    }\n  }\n}\n\n[data-theme='dark'] {\n  .search-box {\n    .active {\n      input,\n      .svg-icon {\n        color: var(--color-text);\n      }\n    }\n  }\n}\n\n.search-box-likepage {\n  display: flex;\n  position: absolute;\n  right: 12vw;\n  top: 95px;\n  justify-content: flex-end;\n  -webkit-app-region: no-drag;\n\n  .input {\n    transition: all 0.5s;\n  }\n\n  .container {\n    display: flex;\n    align-items: center;\n    height: 32px;\n    background: var(--color-secondary-bg-for-transparent);\n    border-radius: 8px;\n  }\n\n  .svg-icon {\n    height: 15px;\n    width: 15px;\n    color: var(--color-text);\n    opacity: 0.28;\n    margin: {\n      left: 8px;\n      right: 8px;\n    }\n  }\n\n  input {\n    font-size: 16px;\n    border: none;\n    background: transparent;\n    width: 96%;\n    font-weight: 600;\n    margin-top: -1px;\n    color: var(--color-text);\n  }\n\n  .active {\n    background: var(--color-primary-bg-for-transparent);\n    input,\n    .svg-icon {\n      opacity: 1;\n      color: var(--color-primary);\n    }\n  }\n}\n\n[data-theme='dark'] {\n  .search-box-likepage {\n    .active {\n      input,\n      .svg-icon {\n        color: var(--color-text);\n      }\n    }\n  }\n}\n\n@media (max-width: 1336px) {\n  .search-box-likepage {\n    right: 8vw;\n  }\n}\n\n.load-more {\n  display: flex;\n  justify-content: center;\n  margin-top: 32px;\n}\n</style>\n"
  },
  {
    "path": "src/views/search.vue",
    "content": "<template>\n  <div v-show=\"show\" class=\"search-page\">\n    <div v-show=\"artists.length > 0 || albums.length > 0\" class=\"row\">\n      <div v-show=\"artists.length > 0\" class=\"artists\">\n        <div v-show=\"artists.length > 0\" class=\"section-title\"\n          >{{ $t('search.artist')\n          }}<router-link :to=\"`/search/${keywords}/artists`\">{{\n            $t('home.seeMore')\n          }}</router-link></div\n        >\n        <CoverRow\n          type=\"artist\"\n          :column-number=\"3\"\n          :items=\"artists.slice(0, 3)\"\n          gap=\"34px 24px\"\n        />\n      </div>\n\n      <div class=\"albums\">\n        <div v-show=\"albums.length > 0\" class=\"section-title\"\n          >{{ $t('search.album')\n          }}<router-link :to=\"`/search/${keywords}/albums`\">{{\n            $t('home.seeMore')\n          }}</router-link></div\n        >\n        <CoverRow\n          type=\"album\"\n          :items=\"albums.slice(0, 3)\"\n          sub-text=\"artist\"\n          :column-number=\"3\"\n          sub-text-font-size=\"14px\"\n          gap=\"34px 24px\"\n          :play-button-size=\"26\"\n        />\n      </div>\n    </div>\n\n    <div v-show=\"tracks.length > 0\" class=\"tracks\">\n      <div class=\"section-title\"\n        >{{ $t('search.song')\n        }}<router-link :to=\"`/search/${keywords}/tracks`\">{{\n          $t('home.seeMore')\n        }}</router-link></div\n      >\n      <TrackList :tracks=\"tracks\" type=\"tracklist\" />\n    </div>\n\n    <div v-show=\"musicVideos.length > 0\" class=\"music-videos\">\n      <div class=\"section-title\"\n        >{{ $t('search.mv')\n        }}<router-link :to=\"`/search/${keywords}/music-videos`\">{{\n          $t('home.seeMore')\n        }}</router-link></div\n      >\n      <MvRow :mvs=\"musicVideos.slice(0, 5)\" />\n    </div>\n\n    <div v-show=\"playlists.length > 0\" class=\"playlists\">\n      <div class=\"section-title\"\n        >{{ $t('search.playlist')\n        }}<router-link :to=\"`/search/${keywords}/playlists`\">{{\n          $t('home.seeMore')\n        }}</router-link></div\n      >\n      <CoverRow\n        type=\"playlist\"\n        :items=\"playlists.slice(0, 12)\"\n        sub-text=\"title\"\n        :column-number=\"6\"\n        sub-text-font-size=\"14px\"\n        gap=\"34px 24px\"\n        :play-button-size=\"26\"\n      />\n    </div>\n\n    <div v-show=\"!haveResult\" class=\"no-results\">\n      <div\n        ><svg-icon icon-class=\"search\" />\n        {{\n          keywords.length === 0 ? '输入关键字搜索' : $t('search.noResult')\n        }}</div\n      >\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapActions } from 'vuex';\nimport { getTrackDetail } from '@/api/track';\nimport { search } from '@/api/others';\nimport NProgress from 'nprogress';\n\nimport TrackList from '@/components/TrackList.vue';\nimport MvRow from '@/components/MvRow.vue';\nimport CoverRow from '@/components/CoverRow.vue';\n\nexport default {\n  name: 'Search',\n  components: {\n    TrackList,\n    MvRow,\n    CoverRow,\n  },\n  data() {\n    return {\n      show: false,\n      tracks: [],\n      artists: [],\n      albums: [],\n      playlists: [],\n      musicVideos: [],\n    };\n  },\n  computed: {\n    keywords() {\n      return this.$route.params.keywords ?? '';\n    },\n    haveResult() {\n      return (\n        this.tracks.length +\n          this.artists.length +\n          this.albums.length +\n          this.playlists.length +\n          this.musicVideos.length >\n        0\n      );\n    },\n  },\n  watch: {\n    keywords: function (newKeywords) {\n      if (newKeywords.length === 0) return;\n      this.getData();\n    },\n  },\n  created() {\n    this.getData();\n  },\n  methods: {\n    ...mapActions(['showToast']),\n    playTrackInSearchResult(id) {\n      let track = this.tracks.find(t => t.id === id);\n      this.$store.state.player.appendTrackToPlayerList(track, true);\n    },\n    search(type = 'all') {\n      let showToast = this.showToast;\n      const typeTable = {\n        all: 1018,\n        musicVideos: 1004,\n        tracks: 1,\n        albums: 10,\n        artists: 100,\n        playlists: 1000,\n      };\n      return search({\n        keywords: this.keywords,\n        type: typeTable[type],\n        limit: 16,\n      })\n        .then(result => {\n          return { result: result.result, type };\n        })\n        .catch(err => {\n          showToast(err.response.data.msg || err.response.data.message);\n        });\n    },\n    getData() {\n      setTimeout(() => {\n        if (!this.show) NProgress.start();\n      }, 1000);\n      this.show = false;\n\n      const requestAll = requests => {\n        const keywords = this.keywords;\n        Promise.all(requests).then(results => {\n          if (keywords != this.keywords) return;\n          results.map(result => {\n            const searchType = result.type;\n            if (result.result === undefined) return;\n            result = result.result;\n            switch (searchType) {\n              case 'all':\n                this.result = result;\n                break;\n              case 'musicVideos':\n                this.musicVideos = result.mvs ?? [];\n                break;\n              case 'artists':\n                this.artists = result.artists ?? [];\n                break;\n              case 'albums':\n                this.albums = result.albums ?? [];\n                break;\n              case 'tracks':\n                this.tracks = result.songs ?? [];\n                this.getTracksDetail();\n                break;\n              case 'playlists':\n                this.playlists = result.playlists ?? [];\n                break;\n            }\n          });\n          NProgress.done();\n          this.show = true;\n        });\n      };\n\n      const requests = [\n        this.search('artists'),\n        this.search('albums'),\n        this.search('tracks'),\n      ];\n      const requests2 = [this.search('musicVideos'), this.search('playlists')];\n\n      requestAll(requests);\n      requestAll(requests2);\n    },\n    getTracksDetail() {\n      const trackIDs = this.tracks.map(t => t.id);\n      if (trackIDs.length === 0) return;\n      getTrackDetail(trackIDs.join(',')).then(result => {\n        this.tracks = result.songs;\n      });\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.section-title {\n  font-weight: 600;\n  font-size: 22px;\n  opacity: 0.88;\n  color: var(--color-text);\n  margin-bottom: 16px;\n\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  a {\n    font-size: 13px;\n    font-weight: 600;\n    opacity: 0.68;\n  }\n}\n\n.row {\n  display: flex;\n  flex-wrap: wrap;\n  margin-top: 32px;\n\n  .artists {\n    flex: 1;\n    margin-right: 8rem;\n  }\n  .albums {\n    flex: 1;\n  }\n}\n\n.tracks,\n.music-videos,\n.playlists {\n  margin-top: 46px;\n}\n\n.no-results {\n  position: absolute;\n  top: 64px;\n  right: 0;\n  left: 0;\n  bottom: 64px;\n  font-size: 24px;\n  color: var(--color-text);\n  opacity: 0.38;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  div {\n    display: flex;\n    align-items: center;\n  }\n  .svg-icon {\n    height: 24px;\n    width: 24px;\n    margin-right: 16px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/views/searchType.vue",
    "content": "<template>\n  <div v-show=\"show\" class=\"search\">\n    <h1>\n      <span>{{ $t('search.searchFor') }} {{ typeNameTable[type] }}</span> \"{{\n        keywords\n      }}\"\n    </h1>\n\n    <div v-if=\"type === 'artists'\">\n      <CoverRow type=\"artist\" :items=\"result\" :column-number=\"6\" />\n    </div>\n    <div v-if=\"type === 'albums'\">\n      <CoverRow\n        type=\"album\"\n        :items=\"result\"\n        sub-text=\"artist\"\n        sub-text-font-size=\"14px\"\n      />\n    </div>\n    <div v-if=\"type === 'tracks'\">\n      <TrackList\n        :tracks=\"result\"\n        type=\"playlist\"\n        dbclick-track-func=\"playAList\"\n      />\n    </div>\n    <div v-if=\"type === 'musicVideos'\">\n      <MvRow :mvs=\"result\" />\n    </div>\n    <div v-if=\"type === 'playlists'\">\n      <CoverRow type=\"playlist\" :items=\"result\" sub-text=\"title\" />\n    </div>\n\n    <div class=\"load-more\">\n      <ButtonTwoTone v-show=\"hasMore\" color=\"grey\" @click.native=\"fetchData\">{{\n        $t('explore.loadMore')\n      }}</ButtonTwoTone>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { getTrackDetail } from '@/api/track';\nimport { search } from '@/api/others';\nimport locale from '@/locale';\nimport { camelCase } from 'change-case';\nimport NProgress from 'nprogress';\n\nimport TrackList from '@/components/TrackList.vue';\nimport MvRow from '@/components/MvRow.vue';\nimport CoverRow from '@/components/CoverRow.vue';\nimport ButtonTwoTone from '@/components/ButtonTwoTone.vue';\n\nexport default {\n  name: 'Search',\n  components: {\n    TrackList,\n    MvRow,\n    CoverRow,\n    ButtonTwoTone,\n  },\n  data() {\n    return { show: false, result: [], hasMore: true };\n  },\n  computed: {\n    keywords() {\n      return this.$route.params.keywords;\n    },\n    type() {\n      return camelCase(this.$route.params.type);\n    },\n    typeNameTable() {\n      return {\n        musicVideos: locale.t('search.mv'),\n        tracks: locale.t('search.song'),\n        albums: locale.t('search.album'),\n        artists: locale.t('search.artist'),\n        playlists: locale.t('search.playlist'),\n      };\n    },\n  },\n  created() {\n    this.fetchData();\n  },\n  methods: {\n    fetchData() {\n      const typeTable = {\n        musicVideos: 1004,\n        tracks: 1,\n        albums: 10,\n        artists: 100,\n        playlists: 1000,\n      };\n      return search({\n        keywords: this.keywords,\n        type: typeTable[this.type],\n        offset: this.result.length,\n      }).then(result => {\n        result = result.result;\n        this.hasMore = result.hasMore ?? true;\n        switch (this.type) {\n          case 'musicVideos':\n            this.result.push(...result.mvs);\n            if (result.mvCount <= this.result.length) {\n              this.hasMore = false;\n            }\n            break;\n          case 'artists':\n            this.result.push(...result.artists);\n            break;\n          case 'albums':\n            this.result.push(...result.albums);\n            if (result.albumCount <= this.result.length) {\n              this.hasMore = false;\n            }\n            break;\n          case 'tracks':\n            this.result.push(...result.songs);\n            this.getTracksDetail();\n            break;\n          case 'playlists':\n            this.result.push(...result.playlists);\n            break;\n        }\n        NProgress.done();\n        this.show = true;\n      });\n    },\n    getTracksDetail() {\n      const trackIDs = this.result.map(t => t.id);\n      if (trackIDs.length === 0) return;\n      getTrackDetail(trackIDs.join(',')).then(result => {\n        this.result = result.songs;\n      });\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\nh1 {\n  margin-top: 32px;\n  margin-bottom: 28px;\n  color: var(--color-text);\n  span {\n    opacity: 0.58;\n  }\n}\n.load-more {\n  display: flex;\n  justify-content: center;\n  margin-top: 32px;\n}\n\n.button.more {\n  .svg-icon {\n    height: 24px;\n    width: 24px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/views/settings.vue",
    "content": "<template>\n  <div class=\"settings-page\" @click=\"clickOutside\">\n    <div class=\"container\">\n      <div v-if=\"showUserInfo\" class=\"user\">\n        <div class=\"left\">\n          <img class=\"avatar\" :src=\"data.user.avatarUrl\" loading=\"lazy\" />\n          <div class=\"info\">\n            <div class=\"nickname\">{{ data.user.nickname }}</div>\n            <div class=\"extra-info\">\n              <span v-if=\"data.user.vipType !== 0\" class=\"vip\"\n                ><img\n                  class=\"cvip\"\n                  src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHIAAAA8CAYAAAC6j+5hAAAQK0lEQVR4AXzNh5WDMAwA0Dv3Su+wIfuxC3MwgCMUOz3xe1/N7e/X0lovhJCVUroR8r9DfVBKAuQAM8QYQ4815wlHQqQsIh6kFEA+USpRCP4H92yMfmCCtScL7rVzd967Fz5kmcf6zHmeJdDf66LIowJzWd5zUlUlqmsU6wo1TVI/adsmutZd1z7p+6Q7HePY7WCbpmGd53kBF87L4yiTMAaiM+u9N2NTIpB1CZEHuZAGHLFS8T9UXdJqzeHRw5VX3Z8YAIAPwf5Ii8k6Hsfx0nBxgEQwcWQIDKGPEZolAhIRGLg8hCaJUEuEVwhFIN8QMkOgfXsCApNESBLj+yNCEYjEg0iRicB7mdP05T7n+eulcbzv+2IMAHyAF/HI5J2pwBGBpIA4iCZqGwF5yKSJ4AJpIm1EoCfytJWAwKqN8MZRmYEIpI0IJCuJtUD/VoGIQ6aL01Yi8OuBu+95nlzo2bIsR8bggPxikn6ZwGuXiEhS2+iJQBKJEEJpIm1Epksr2ggiEanIRGDRRhCJuY1Znjaxm9R3CCRTIxHZtTHJI0MkbUQqMq+2bfllDMAHTbwax0HlZYGBymRWaaOIDIFQy/SkjaBtlFlFpgjs2whlE0nEQddGEonN24hAaWaSSQOjic5EwhXNpJH+JrrJw5yWbQQRiEQE0kJLREobEcmcIhGB8i7KpCIUkQhEome0MLJ5G7PAto2Q55TvaGHTxlqivItdG0PksszOGW/m4D/8sGFOQ55KzE0ko4UqE4nayHypIq6eVARGC5V+UmuBKjLkBe2kCv2kaiMRWM+qg0RQgZ7LMgm2pseHRR0247ITmY8cBPazqu+iytRGqlBE5neRpIX9rML/zCqJRJWZGwkqEJAY6QL7WSWRKDJppH9f+r8mLvJ7SASuVEQmiWRqIdBEMq7U30+qkie1eRdFHDKZVY6bflIVJEL9LqYWAgJJmthMqkITSZfnIpHoua53Mm1dv7vIk9RGoZeISEAc06qNdLSFJKhAeEGmS5VUoSGwnlZklm+jkJv4vrtUmVJ5H2li9zaCCtRGIhKZiNy2+WQweachEZDYzik0bcxXKvRtVImAxPrASXPqQvsDp34j2ybWIj8mEAdVG0kOHG0jTEATaSNprKcu8vxPVyoJWSIp72N55HCx1lcqqZNKBkh0uFJJlRm8kXntr9TyfYQkkfRG6vuYr1Tex6KJJDKrIwehNNJYPM+HelZDHO8jLSSdW1rOAci5bYnCeSprmLHtubbte8fXtm3btm3btm3bxq/9TqfeqtpZ0+fszrs5VbUqU+Pkq9W9GzsCjAUnAmJ1Nus2mZpwKy29FOfGHLhrzz7duU8+SNQN553NuREdHF++E0O/k0GGvp9zIz5v1q9vv+befewhd+9Vl7s9t9vaDfX3CjA+qSpOzMblRoEIkC7DAFmAyG7kniogwo1rrriCe+T6a9zsj9/PPZGvX3rO1VZX+zBF8jn5WvCF2GhyDDD1vEgK/D7qq4ZBUngNwwto1kfvuUtPOdEN9PVwucGhFW5kmJCUIADJYTW5gxNX/IuWX2Jx99wdt6r//LVnn6EW/2uvuUbwiX//6kuupamRa0bOkciLZpAIp4Hv51IjDMuoX956za0/PqrmRg6nDJBBAiLlREgrN/7DbszlsWP328fNSf7HI2ir84RDJJCDT/rOyy4OuhGh1Q7S5kguN+ywwpKotc8O29MJFQLE/NwIIbxmeMIh0ro3eOR2nLgxGyXwJ2+5MfgPI8TW1VTjgAPJ50whdusN1wNMbd5odiSfUI0gi+tIgrnBxCi14UheyQEnQhkPIh1wfKDxJ9Wy0lKEUrOuOycXYnlobAqxP73xiutqb6cuDp1SCwNpciSfVIsNEmF2aKBPYHITAADJkR5Ia2Oc2nAicYbZiax11lpDAHJP1RRiH7z2KgHHDQAopRwpANMDCV16yknkyGrfjb4TPZi1cCTgadP/eDcef8B+2j9jDrH1tbU8ppLPmULsLltuFjemsoJEWDWD9GGmARGn2bkGByi0JrmRQHLxDyeKGKBoyYUXQmkR1IwP3sk5bYPodNbf3eXK5UUpFZWoM0dxa+h3/vbOG26wr0eFmUKO9N1oduRnzz3ltlh/Hdff2xWpO/p4Xflc8Of22n4bv4vDAEV6jgTAUE/VB/rqfXeZnsyN553jujva1U4OQqrXS0Vz3BRin7j5BoADSCn0LSC5DWd1JDo4Jogd7S1S7Od1cro624Iw77v6coDk3KhCrK+PHOkfbPDoO1Fz5GrLLWs6he213dYo/rkVR06cDrOhzhZi991xe3VEZQeZjiPFiRhVcStuyw3WTfpZ6QAlFv8C04coUnOk1orzYErHJvhE9tx2a2W9EY88+dd3cdZZa83g3/nzvbfcvMODfk81FZCAaD3s9PV0+U7Ma44P9HUH2nmvx9SNeQccypGASNJqRlF9bY0hnJ4NgDzhiHMjT/5RK5pC7PN33hbBKMGIKo3QSpONIEjJizzhgKQtFyxDuGZEbqSQKhDhyPCoCk4UbTg+FjzYSE7k5jitccTuqQIgmuON9fWmEHvYnrv5k400cqQ33TCHVlHBofW9xx/i5jhcySA5R8aXGzxnvOTk4xP/CXEQb8RBbSWl7soFFnKfrriySD6Wz8W6EUX/uiNrmk7Giy4wnxlkaWlBIOFEE0gcdjo7WqdB7OpsNxx2rvDdGIIYqU5AMsT4/Ch66tbkBsAG4yPiRjqlCsQS983Kq7lZa4z4ks8BproBgML/+nPPCr54r91/j7zIZkdi6p9GaAVMcZ+UHpIX5WNL+bH3DtvEnlIRXhFSIYAUEcD8HIlB8fuPP5Kc5Lu6ABESmOI+hgjJ12K34qCmhgb3zcvPB1+E4w/cvwCQJWaQvBWXZkNg7qFBdcIB4aBDIP+plBsifdlYTlSJIaukhPOj5EUJpbEgP1tpZUAEUHUrbr3REdMLsfSiCxvni/bQynuqaYG87NSTqOSoCUJsaJDQ6hf/BJDyo0hOVMmHgtJSbQ8nAHKVWIAkU4h959EHzYNi68Sfd1TTaprPNdTvQ4T4pKqDFGlb4yK+FvfWw/cXFFrhyCsXWDAQWnnFUQVqDrEp5EiBia24VMZYG06O8SEHEBmmp7qcMur9Rs+FDFImD6HDjlcv4lEONLGHnfbSMnZjTgO93dqYyhRirY40zhd5M67YEKVDpdaMHFbhSDgRyuQ3xmn1X1lvlD0Tw6xRxOuNavnRXoryI38rTnT7JRcKNED0B8fBEGsHaXIkrzYWNZyKE7nUYKAAqIVVP0f6YoD+jSpTQ6Cns523xRPvNwo0rh2H+/vdzA/fjcLocxJOARBFv+zvBEJsUXMk398o0vLVSW54sE8g+opx5LRwio/hSMDzICq5EarKVsgLHJx4xF8Zt12Ju+eKS/H7xH0CkmHKWOxvgERYNYGkPdWwI2UH5+4rLnEfPvloNHJ7XU770gyXyYaMqaISY4CHxtxP5ZOqyIdJoZUmHH7JAfGi8QPXXBkuarffBj1VBaAOE2H1/OOPnvb71h8bQVM8D+YN56khttjrjbRoHAbJq43+1F/ZACCIITcqOZLcCKluBMixVVc2jrG2ITcq9xsppB6z397q75Mw2tzYQNvi5hAb2MGxO9IOEvcb4y7jVAMiL1R5j8iN+e04htjYWA+Q8SEVjuT7G4/fdL15sNzb1eE7Ug2r3R0dcriJ/T0Isdp1uA2Qt+3iG1UFOjKYIwFOcyQ7kdwYLP4prNYDJOVIAklhFTBl1cP0guEAdN05Z+a2xQd6elylHBrKyyLAndHnxuXaQCjv+iHWv0kFybWC/8eRVpCAiEcrSEA0bsWFW3GcH6FM+A5H/P3GUw49iJ5A+jrugH3V+42tzU3GEAuQhS0c+/cbs9kgSADE0jEgJk3+qTEuwIIhlUIrhVUA1K7G+beMJVTeeyVOl+nrxvPPATwGiRCbYo4UiObQGroax4NjiJ0IoBxa40H6SoLIN6qy0ZN648H7UgWIRSt5IQNv4IAQW+yFY1yHM4NkiOEDTtz0H1Lc6OfIuHfh8E+peIT4fqPAvPfKy1KDKDtCjQ31gJh4v7GtpRkhtphbcXRJ1Q5SoOExfr1Rg8nlRh2UBxPKBJzofUxupOm/nECLnTP/ev9tt99O26sA8cgwRRtOjJmXqSALSH8rLgzSfr8RUpxIboTqFZBKbiSgAAiY6h4OCn85zUoYDIMKT/sXnm8e1IxBN7ICIVbgFRhaAbEgR1JeFMXu4XAHh5vjihuhBpcBRN2RajhVt+L47v/E6qvKpMRUVkBzIj15yw1Ro3yt8Ds3Bt7cqL21JSnEem4sNQ2KPTdaHQl4BDAUVuHC+HCKvAg1Nf0PpPYOHPwuVfFujN8aF9VGT2KTqUl3+WknuYevv9q9/cgDuY6/7KN+9eIz7qV77owWuk50O22+qetsa7W+Jw5JvfMvNaoxR9pDK94Lxx5aATHgxsAhh2GyMv9t7WxS2wiiINw7ZxOyT/w3YPBtdBGDL+R76C66hrWVIO8FCgq+rtYgsvimtS/qve7XM6US7MwBgDkSvbF5gAPtN6Y39wYbsaT+WubFuZiMUkFeHN6Ms2sqpFOlYKMC47j1FEdizu8bGwrI3apKartx257PowQ7lYjY4HAI8MMdaXAwznEcRWQU59SJWnF+FBQQUWOzdCrgAjJuTALKkauYsQZDAIgouEvFgNwEpCNLyOLl1I48tmgG+uL8+43GX/8PjuTtRkioEkipWuWo7gz+25/eSAGXOaruROFOhBvDq422copDAZ8bE/PlOEqkjxDDieNG4WKG0L+b943ThCriQu51Y44aW6cau4hCgsKNtSY3akWAA/qjCQg3srQmN6q0vnz8yy2vHnmZhf7xRePbeXFaeU1FN2YxZ72RrKPG5EQK6Hh/VFmVA11IwbJKMfmlMXeo7JGroTg2N94jL29vb4+jHqPE+rIjR3Rj/UY55feNdKNhIv5s1jGc+3vjMvjPmAXFe2qjpVPBjchTDVlxcGMex/2ZnRlttd6Y3fhVjNGPDt4t8b6xW4WAKYIzlVQ48u4IznBmDBmqaYOTs1QpplwJAdMmR3he3NSRTiqpBgSsVSJ+v7+//y7G6EdRYj4cypVXllXvi3Ckwe8b2Rc99T86EvyHshr6I3qkC5i+b8TNfyir6Ita3YUU83ElpAt63bbtUIymH6L75WcJeGUMpztxUVJ53AiZOLsF1JpSjbWClGrMGE4mNzIwPvRFzFKdUFLhRo7hcE3FvngtPosh+uF0mT2UcN/p0zjsVPlmnASEI3luBBDwnt7o0Ik5ML6RcN4fPfxvvVOlgKvXOPJV1VMcjnc53banQzGcfoDumSXiV3FBOb3tRogoPA+HAQ47yylEnE9yBFONU7qxYDkNbpSgdCNEnLpRKyY4YaZ66Y2NeqKjHhnpo0kJ9VGCHuv3qTi7oL7Jsb6oFX0x/5kKd6vBifYbTrzVHwV6Yq3crXKXylEcd6la8VYcR3GY3mgV59fXp1Nx7HNiHzGKkfgLQfHe2MpsYnIAAAAASUVORK5CYII=\"\n                  loading=\"lazy\"\n                />\n                <span class=\"text\">黑胶VIP</span>\n              </span>\n              <span v-else class=\"text\">{{ data.user.signature }}</span>\n            </div>\n          </div>\n        </div>\n        <div class=\"right\">\n          <button @click=\"logout\">\n            <svg-icon icon-class=\"logout\" />\n            {{ $t('settings.logout') }}\n          </button>\n        </div>\n      </div>\n\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\"> {{ $t('settings.language') }} </div>\n        </div>\n        <div class=\"right\">\n          <select v-model=\"lang\">\n            <option value=\"en\">🇬🇧 English</option>\n            <option value=\"tr\">🇹🇷 Türkçe</option>\n            <option value=\"zh-CN\">🇨🇳 简体中文</option>\n            <option value=\"zh-TW\">繁體中文</option>\n          </select>\n        </div>\n      </div>\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\"> {{ $t('settings.appearance.text') }} </div>\n        </div>\n        <div class=\"right\">\n          <select v-model=\"appearance\">\n            <option value=\"auto\">{{ $t('settings.appearance.auto') }}</option>\n            <option value=\"light\"\n              >🌞 {{ $t('settings.appearance.light') }}</option\n            >\n            <option value=\"dark\"\n              >🌚 {{ $t('settings.appearance.dark') }}</option\n            >\n          </select>\n        </div>\n      </div>\n      <div v-if=\"isElectron\" class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\"> {{ $t('settings.trayIcon.text') }} </div>\n        </div>\n        <div class=\"right\">\n          <select v-model=\"trayIconTheme\">\n            <option value=\"auto\">{{ $t('settings.trayIcon.auto') }}</option>\n            <option value=\"light\">{{ $t('settings.trayIcon.light') }}</option>\n            <option value=\"dark\">{{ $t('settings.trayIcon.dark') }}</option>\n          </select>\n        </div>\n      </div>\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\">\n            {{ $t('settings.MusicGenrePreference.text') }}\n          </div>\n        </div>\n        <div class=\"right\">\n          <select v-model=\"musicLanguage\">\n            <option value=\"all\">{{\n              $t('settings.MusicGenrePreference.none')\n            }}</option>\n            <option value=\"zh\">{{\n              $t('settings.MusicGenrePreference.mandarin')\n            }}</option>\n            <option value=\"ea\">{{\n              $t('settings.MusicGenrePreference.western')\n            }}</option>\n            <option value=\"jp\">{{\n              $t('settings.MusicGenrePreference.japanese')\n            }}</option>\n            <option value=\"kr\">{{\n              $t('settings.MusicGenrePreference.korean')\n            }}</option>\n          </select>\n        </div>\n      </div>\n\n      <!-- <h3>音质</h3> -->\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\"> {{ $t('settings.musicQuality.text') }} </div>\n        </div>\n        <div class=\"right\">\n          <select v-model=\"musicQuality\">\n            <option value=\"128000\">\n              {{ $t('settings.musicQuality.low') }} - 128Kbps\n            </option>\n            <option value=\"192000\">\n              {{ $t('settings.musicQuality.medium') }} - 192Kbps\n            </option>\n            <option value=\"320000\">\n              {{ $t('settings.musicQuality.high') }} - 320Kbps\n            </option>\n            <option value=\"flac\">\n              {{ $t('settings.musicQuality.lossless') }} - FLAC\n            </option>\n            <option value=\"999000\">Hi-Res</option>\n          </select>\n        </div>\n      </div>\n      <div v-if=\"isElectron\" class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\"> {{ $t('settings.deviceSelector') }} </div>\n        </div>\n        <div class=\"right\">\n          <select v-model=\"outputDevice\">\n            <option\n              v-for=\"device in allOutputDevices\"\n              :key=\"device.deviceId\"\n              :value=\"device.deviceId\"\n              :selected=\"device.deviceId == outputDevice\"\n            >\n              {{ $t(device.label) }}\n            </option>\n          </select>\n        </div>\n      </div>\n\n      <h3 v-if=\"isElectron\">缓存</h3>\n      <div v-if=\"isElectron\" class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\">\n            {{ $t('settings.automaticallyCacheSongs') }}\n          </div>\n        </div>\n        <div class=\"right\">\n          <div class=\"toggle\">\n            <input\n              id=\"automatically-cache-songs\"\n              v-model=\"automaticallyCacheSongs\"\n              type=\"checkbox\"\n              name=\"automatically-cache-songs\"\n            />\n            <label for=\"automatically-cache-songs\"></label>\n          </div>\n        </div>\n      </div>\n      <div v-if=\"isElectron\" class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\"> {{ $t('settings.cacheLimit.text') }} </div>\n        </div>\n        <div class=\"right\">\n          <select v-model=\"cacheLimit\">\n            <option :value=\"false\">\n              {{ $t('settings.cacheLimit.none') }}\n            </option>\n            <option :value=\"512\"> 500MB </option>\n            <option :value=\"1024\"> 1GB </option>\n            <option :value=\"2048\"> 2GB </option>\n            <option :value=\"4096\"> 4GB </option>\n            <option :value=\"8192\"> 8GB </option>\n          </select>\n        </div>\n      </div>\n      <div v-if=\"isElectron\" class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\">\n            {{\n              $t('settings.cacheCount', {\n                song: tracksCache.length,\n                size: tracksCache.size,\n              })\n            }}</div\n          >\n        </div>\n        <div class=\"right\">\n          <button @click=\"clearCache()\">\n            {{ $t('settings.clearSongsCache') }}\n          </button>\n        </div>\n      </div>\n\n      <h3>{{ $t('settings.lyric') }}</h3>\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\">{{ $t('settings.showLyricsTranslation') }}</div>\n        </div>\n        <div class=\"right\">\n          <div class=\"toggle\">\n            <input\n              id=\"show-lyrics-translation\"\n              v-model=\"showLyricsTranslation\"\n              type=\"checkbox\"\n              name=\"show-lyrics-translation\"\n            />\n            <label for=\"show-lyrics-translation\"></label>\n          </div>\n        </div>\n      </div>\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\">{{ $t('settings.lyricsBackground.text') }}</div>\n        </div>\n        <div class=\"right\">\n          <select v-model=\"lyricsBackground\">\n            <option :value=\"false\">\n              {{ $t('settings.lyricsBackground.off') }}\n            </option>\n            <option :value=\"true\">\n              {{ $t('settings.lyricsBackground.on') }}\n            </option>\n            <option value=\"blur\"> 模糊封面 </option>\n            <option value=\"dynamic\">\n              {{ $t('settings.lyricsBackground.dynamic') }}\n            </option>\n          </select>\n        </div>\n      </div>\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\"> {{ $t('settings.showLyricsTime') }} </div>\n        </div>\n        <div class=\"right\">\n          <div class=\"toggle\">\n            <input\n              id=\"show-lyrics-time\"\n              v-model=\"showLyricsTime\"\n              type=\"checkbox\"\n              name=\"show-lyrics-time\"\n            />\n            <label for=\"show-lyrics-time\"></label>\n          </div>\n        </div>\n      </div>\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\"> {{ $t('settings.lyricFontSize.text') }} </div>\n        </div>\n        <div class=\"right\">\n          <select v-model=\"lyricFontSize\">\n            <option value=\"16\">\n              {{ $t('settings.lyricFontSize.small') }} - 16px\n            </option>\n            <option value=\"22\">\n              {{ $t('settings.lyricFontSize.medium') }} - 22px\n            </option>\n            <option value=\"28\">\n              {{ $t('settings.lyricFontSize.large') }} - 28px\n            </option>\n            <option value=\"36\">\n              {{ $t('settings.lyricFontSize.xlarge') }} - 36px\n            </option>\n          </select>\n        </div>\n      </div>\n      <div v-if=\"isElectron && isLinux\" class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\">\n            {{ $t('settings.unm.enable') }}\n            <a target=\"_blank\" href=\"https://github.com/osdlyrics/osdlyrics\"\n              >OSDLyrics</a\n            >\n            {{ $t('settings.enableOsdlyricsSupport.title') }}\n          </div>\n          <div class=\"description\">\n            {{ $t('settings.enableOsdlyricsSupport.desc1') }}\n            <br />\n            {{ $t('settings.enableOsdlyricsSupport.desc2') }}\n          </div>\n        </div>\n        <div class=\"right\">\n          <div class=\"toggle\">\n            <input\n              id=\"enable-osdlyrics-support\"\n              v-model=\"enableOsdlyricsSupport\"\n              type=\"checkbox\"\n              name=\"enable-osdlyrics-support\"\n            />\n            <label for=\"enable-osdlyrics-support\"></label>\n          </div>\n        </div>\n      </div>\n\n      <section v-if=\"isElectron\" class=\"unm-configuration\">\n        <h3>UnblockNeteaseMusic</h3>\n        <div class=\"item\">\n          <div class=\"left\">\n            <div class=\"title\"\n              >{{ $t('settings.unm.enable') }}\n              <a\n                href=\"https://github.com/UnblockNeteaseMusic/server\"\n                target=\"blank\"\n                >UnblockNeteaseMusic</a\n              ></div\n            >\n          </div>\n          <div class=\"right\">\n            <div class=\"toggle\">\n              <input\n                id=\"enable-unblock-netease-music\"\n                v-model=\"enableUnblockNeteaseMusic\"\n                type=\"checkbox\"\n                name=\"enable-unblock-netease-music\"\n              />\n              <label for=\"enable-unblock-netease-music\"></label>\n            </div>\n          </div>\n        </div>\n\n        <div class=\"item\">\n          <div class=\"left\">\n            <div class=\"title\">\n              {{ $t('settings.unm.audioSource.title') }}\n            </div>\n            <div class=\"description\">\n              音源的具体代号\n              <a\n                href=\"https://github.com/UnblockNeteaseMusic/server-rust/blob/main/README.md#支援的所有引擎\"\n                target=\"_blank\"\n              >\n                可以点此到 UNM 的说明页面查询。 </a\n              ><br />\n              多个音源请用 <code>,</code> 逗号分隔。<br />\n              留空则使用 UNM 内置的默认值。\n            </div>\n          </div>\n          <div class=\"right\">\n            <input\n              v-model=\"unmSource\"\n              class=\"text-input margin-right-0\"\n              placeholder=\"例 bilibili, kuwo\"\n            />\n          </div>\n        </div>\n\n        <div class=\"item\">\n          <div class=\"left\">\n            <div class=\"title\"> {{ $t('settings.unm.enableFlac.title') }} </div>\n            <div class=\"description\">\n              {{ $t('settings.unm.enableFlac.desc') }}\n            </div>\n          </div>\n          <div class=\"right\">\n            <div class=\"toggle\">\n              <input\n                id=\"unm-enable-flac\"\n                v-model=\"unmEnableFlac\"\n                type=\"checkbox\"\n              />\n              <label for=\"unm-enable-flac\" />\n            </div>\n          </div>\n        </div>\n\n        <div class=\"item\">\n          <div class=\"left\">\n            <div class=\"title\"> {{ $t('settings.unm.searchMode.title') }} </div>\n          </div>\n          <div class=\"right\">\n            <select v-model=\"unmSearchMode\">\n              <option value=\"fast-first\">\n                {{ $t('settings.unm.searchMode.fast') }}\n              </option>\n              <option value=\"order-first\">\n                {{ $t('settings.unm.searchMode.order') }}\n              </option>\n            </select>\n          </div>\n        </div>\n\n        <div class=\"item\">\n          <div class=\"left\">\n            <div class=\"title\">{{ $t('settings.unm.cookie.joox') }}</div>\n            <div class=\"description\">\n              <a\n                href=\"https://github.com/UnblockNeteaseMusic/server-rust/tree/main/engines#joox-cookie-設定說明\"\n                target=\"_blank\"\n                >{{ $t('settings.unm.cookie.desc1') }}\n              </a>\n              {{ $t('settings.unm.cookie.desc2') }}\n            </div>\n          </div>\n          <div class=\"right\">\n            <input\n              v-model=\"unmJooxCookie\"\n              class=\"text-input margin-right-0\"\n              placeholder=\"wmid=..; session_key=..\"\n            />\n          </div>\n        </div>\n\n        <div class=\"item\">\n          <div class=\"left\">\n            <div class=\"title\"> {{ $t('settings.unm.cookie.qq') }} </div>\n            <div class=\"description\">\n              <a\n                href=\"https://github.com/UnblockNeteaseMusic/server-rust/tree/main/engines#qq-cookie-設定說明\"\n                target=\"_blank\"\n                >{{ $t('settings.unm.cookie.desc1') }}\n              </a>\n              {{ $t('settings.unm.cookie.desc2') }}\n            </div>\n          </div>\n          <div class=\"right\">\n            <input\n              v-model=\"unmQQCookie\"\n              class=\"text-input margin-right-0\"\n              placeholder=\"uin=..; qm_keyst=..;\"\n            />\n          </div>\n        </div>\n\n        <div class=\"item\">\n          <div class=\"left\">\n            <div class=\"title\"> {{ $t('settings.unm.ytdl') }} </div>\n            <div class=\"description\">\n              <a\n                href=\"https://github.com/UnblockNeteaseMusic/server-rust/tree/main/engines#ytdlexe-設定說明\"\n                target=\"_blank\"\n                >{{ $t('settings.unm.cookie.desc1') }}\n              </a>\n              {{ $t('settings.unm.cookie.desc2') }}\n            </div>\n          </div>\n          <div class=\"right\">\n            <input\n              v-model=\"unmYtDlExe\"\n              class=\"text-input margin-right-0\"\n              placeholder=\"ex. youtube-dl\"\n            />\n          </div>\n        </div>\n\n        <div class=\"item\">\n          <div class=\"left\">\n            <div class=\"title\"> {{ $t('settings.unm.proxy.title') }} </div>\n            <div class=\"description\">\n              {{ $t('settings.unm.proxy.desc1') }}<br />\n              {{ $t('settings.unm.proxy.desc2') }}\n            </div>\n          </div>\n          <div class=\"right\">\n            <input\n              v-model=\"unmProxyUri\"\n              class=\"text-input margin-right-0\"\n              placeholder=\"ex. https://192.168.11.45\"\n            />\n          </div>\n        </div>\n      </section>\n\n      <h3>{{ $t('settings.customization') }}</h3>\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\">\n            {{\n              isLastfmConnected\n                ? `已连接到 Last.fm (${lastfm.name})`\n                : '连接 Last.fm '\n            }}</div\n          >\n        </div>\n        <div class=\"right\">\n          <button v-if=\"isLastfmConnected\" @click=\"lastfmDisconnect()\"\n            >断开连接\n          </button>\n          <button v-else @click=\"lastfmConnect()\"> 授权连接 </button>\n        </div>\n      </div>\n      <div v-if=\"isElectron\" class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\">\n            {{ $t('settings.enableDiscordRichPresence') }}</div\n          >\n        </div>\n        <div class=\"right\">\n          <div class=\"toggle\">\n            <input\n              id=\"enable-discord-rich-presence\"\n              v-model=\"enableDiscordRichPresence\"\n              type=\"checkbox\"\n              name=\"enable-discord-rich-presence\"\n            />\n            <label for=\"enable-discord-rich-presence\"></label>\n          </div>\n        </div>\n      </div>\n\n      <h3>{{ $t('settings.others') }}</h3>\n      <div v-if=\"isElectron && !isMac\" class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\"> {{ $t('settings.closeAppOption.text') }} </div>\n        </div>\n        <div class=\"right\">\n          <select v-model=\"closeAppOption\">\n            <option value=\"ask\">\n              {{ $t('settings.closeAppOption.ask') }}\n            </option>\n            <option value=\"exit\">\n              {{ $t('settings.closeAppOption.exit') }}\n            </option>\n            <option value=\"minimizeToTray\">\n              {{ $t('settings.closeAppOption.minimizeToTray') }}\n            </option>\n          </select>\n        </div>\n      </div>\n\n      <div v-if=\"isElectron && isLinux\" class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\"> {{ $t('settings.enableCustomTitlebar') }} </div>\n        </div>\n        <div class=\"right\">\n          <div class=\"toggle\">\n            <input\n              id=\"enable-custom-titlebar\"\n              v-model=\"enableCustomTitlebar\"\n              type=\"checkbox\"\n              name=\"enable-custom-titlebar\"\n            />\n            <label for=\"enable-custom-titlebar\"></label>\n          </div>\n        </div>\n      </div>\n\n      <div v-if=\"isElectron\" class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\"> {{ $t('settings.showLibraryDefault') }}</div>\n        </div>\n        <div class=\"right\">\n          <div class=\"toggle\">\n            <input\n              id=\"show-library-default\"\n              v-model=\"showLibraryDefault\"\n              type=\"checkbox\"\n              name=\"show-library-default\"\n            />\n            <label for=\"show-library-default\"></label>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\">\n            {{ $t('settings.showPlaylistsByAppleMusic') }}</div\n          >\n        </div>\n        <div class=\"right\">\n          <div class=\"toggle\">\n            <input\n              id=\"show-playlists-by-apple-music\"\n              v-model=\"showPlaylistsByAppleMusic\"\n              type=\"checkbox\"\n              name=\"show-playlists-by-apple-music\"\n            />\n            <label for=\"show-playlists-by-apple-music\"></label>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\">{{ $t('settings.subTitleDefault') }}</div>\n        </div>\n        <div class=\"right\">\n          <div class=\"toggle\">\n            <input\n              id=\"sub-title-default\"\n              v-model=\"subTitleDefault\"\n              type=\"checkbox\"\n              name=\"sub-title-default\"\n            />\n            <label for=\"sub-title-default\"></label>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\">{{ $t('settings.enableReversedMode') }}</div>\n        </div>\n        <div class=\"right\">\n          <div class=\"toggle\">\n            <input\n              id=\"enable-reversed-mode\"\n              v-model=\"enableReversedMode\"\n              type=\"checkbox\"\n              name=\"enable-reversed-mode\"\n            />\n            <label for=\"enable-reversed-mode\"></label>\n          </div>\n        </div>\n      </div>\n\n      <div class=\"item\">\n        <div class=\"left\">\n          <div class=\"title\" style=\"transform: scaleX(-1)\">🐈️ 🏳️‍🌈</div>\n        </div>\n        <div class=\"right\">\n          <div class=\"toggle\">\n            <input\n              id=\"nyancat-style\"\n              v-model=\"nyancatStyle\"\n              type=\"checkbox\"\n              name=\"nyancat-style\"\n            />\n            <label for=\"nyancat-style\"></label>\n          </div>\n        </div>\n      </div>\n\n      <div v-if=\"isElectron\">\n        <h3>代理</h3>\n        <div class=\"item\">\n          <div class=\"left\">\n            <div class=\"title\"> 代理协议 </div>\n          </div>\n          <div class=\"right\">\n            <select v-model=\"proxyProtocol\">\n              <option value=\"noProxy\"> 关闭代理 </option>\n              <option value=\"HTTP\"> HTTP 代理 </option>\n              <option value=\"HTTPS\"> HTTPS 代理 </option>\n              <!-- <option value=\"SOCKS\"> SOCKS 代理 </option> -->\n            </select>\n          </div>\n        </div>\n        <div id=\"proxy-form\" :class=\"{ disabled: proxyProtocol === 'noProxy' }\">\n          <input\n            v-model=\"proxyServer\"\n            class=\"text-input\"\n            placeholder=\"服务器地址\"\n            :disabled=\"proxyProtocol === 'noProxy'\"\n          /><input\n            v-model=\"proxyPort\"\n            class=\"text-input\"\n            placeholder=\"端口\"\n            type=\"number\"\n            min=\"1\"\n            max=\"65535\"\n            :disabled=\"proxyProtocol === 'noProxy'\"\n          />\n          <button @click=\"sendProxyConfig\">更新代理</button>\n        </div>\n      </div>\n      <div v-if=\"isElectron\">\n        <h3>Real IP</h3>\n        <div class=\"item\">\n          <div class=\"left\">\n            <div class=\"title\"> Real IP </div>\n          </div>\n          <div class=\"right\">\n            <div class=\"toggle\">\n              <input\n                id=\"enable-real-ip\"\n                v-model=\"enableRealIP\"\n                type=\"checkbox\"\n                name=\"enable-real-ip\"\n              />\n              <label for=\"enable-real-ip\"></label>\n            </div>\n          </div>\n        </div>\n        <div id=\"real-ip\" :class=\"{ disabled: !enableRealIP }\">\n          <input\n            v-model=\"realIP\"\n            class=\"text-input\"\n            placeholder=\"IP地址\"\n            :disabled=\"!enableRealIP\"\n          />\n        </div>\n      </div>\n\n      <div v-if=\"isElectron\">\n        <h3>快捷键</h3>\n        <div class=\"item\">\n          <div class=\"left\">\n            <div class=\"title\"> {{ $t('settings.enableGlobalShortcut') }}</div>\n          </div>\n          <div class=\"right\">\n            <div class=\"toggle\">\n              <input\n                id=\"enable-enable-global-shortcut\"\n                v-model=\"enableGlobalShortcut\"\n                type=\"checkbox\"\n                name=\"enable-enable-global-shortcut\"\n              />\n              <label for=\"enable-enable-global-shortcut\"></label>\n            </div>\n          </div>\n        </div>\n        <div\n          id=\"shortcut-table\"\n          :class=\"{ 'global-disabled': !enableGlobalShortcut }\"\n          tabindex=\"0\"\n          @keydown=\"handleShortcutKeydown\"\n        >\n          <div class=\"row row-head\">\n            <div class=\"col\">功能</div>\n            <div class=\"col\">快捷键</div>\n            <div class=\"col\">全局快捷键</div>\n          </div>\n          <div\n            v-for=\"shortcut in settings.shortcuts\"\n            :key=\"shortcut.id\"\n            class=\"row\"\n          >\n            <div class=\"col\">{{ shortcut.name }}</div>\n            <div class=\"col\">\n              <div\n                class=\"keyboard-input\"\n                :class=\"{\n                  active:\n                    shortcutInput.id === shortcut.id &&\n                    shortcutInput.type === 'shortcut',\n                }\"\n                @click.stop=\"readyToRecordShortcut(shortcut.id, 'shortcut')\"\n              >\n                {{\n                  shortcutInput.id === shortcut.id &&\n                  shortcutInput.type === 'shortcut' &&\n                  recordedShortcutComputed !== ''\n                    ? formatShortcut(recordedShortcutComputed)\n                    : formatShortcut(shortcut.shortcut)\n                }}\n              </div>\n            </div>\n            <div class=\"col\">\n              <div\n                class=\"keyboard-input\"\n                :class=\"{\n                  active:\n                    shortcutInput.id === shortcut.id &&\n                    shortcutInput.type === 'globalShortcut' &&\n                    enableGlobalShortcut,\n                }\"\n                @click.stop=\"\n                  readyToRecordShortcut(shortcut.id, 'globalShortcut')\n                \"\n                >{{\n                  shortcutInput.id === shortcut.id &&\n                  shortcutInput.type === 'globalShortcut' &&\n                  recordedShortcutComputed !== ''\n                    ? formatShortcut(recordedShortcutComputed)\n                    : formatShortcut(shortcut.globalShortcut)\n                }}</div\n              >\n            </div>\n          </div>\n          <button\n            class=\"restore-default-shortcut\"\n            @click=\"restoreDefaultShortcuts\"\n            >恢复默认快捷键</button\n          >\n        </div>\n      </div>\n\n      <div class=\"footer\">\n        <p class=\"author\"\n          >MADE BY\n          <a href=\"http://github.com/qier222\" target=\"_blank\">QIER222</a></p\n        >\n        <p class=\"version\">v{{ version }}</p>\n\n        <a\n          v-if=\"!isElectron\"\n          href=\"https://vercel.com/?utm_source=ohmusic&utm_campaign=oss\"\n        >\n          <img\n            height=\"36\"\n            src=\"https://www.datocms-assets.com/31049/1618983297-powered-by-vercel.svg\"\n          />\n        </a>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapState, mapActions } from 'vuex';\nimport { isLooseLoggedIn, doLogout } from '@/utils/auth';\nimport { auth as lastfmAuth } from '@/api/lastfm';\nimport { changeAppearance, bytesToSize } from '@/utils/common';\nimport { countDBSize, clearDB } from '@/utils/db';\nimport pkg from '../../package.json';\n\nconst electron =\n  process.env.IS_ELECTRON === true ? window.require('electron') : null;\nconst ipcRenderer =\n  process.env.IS_ELECTRON === true ? electron.ipcRenderer : null;\n\nconst validShortcutCodes = ['=', '-', '~', '[', ']', ';', \"'\", ',', '.', '/'];\n\nexport default {\n  name: 'Settings',\n  data() {\n    return {\n      tracksCache: {\n        size: '0KB',\n        length: 0,\n      },\n      allOutputDevices: [\n        {\n          deviceId: 'default',\n          label: 'settings.permissionRequired',\n        },\n      ],\n      shortcutInput: {\n        id: '',\n        type: '',\n        recording: false,\n      },\n      recordedShortcut: [],\n    };\n  },\n  computed: {\n    ...mapState(['player', 'settings', 'data', 'lastfm']),\n    isElectron() {\n      return process.env.IS_ELECTRON;\n    },\n    isMac() {\n      return /macintosh|mac os x/i.test(navigator.userAgent);\n    },\n    isLinux() {\n      return process.platform === 'linux';\n    },\n    version() {\n      return pkg.version;\n    },\n    showUserInfo() {\n      return isLooseLoggedIn() && this.data.user.nickname;\n    },\n    recordedShortcutComputed() {\n      let shortcut = [];\n      this.recordedShortcut.map(e => {\n        if (e.keyCode >= 65 && e.keyCode <= 90) {\n          // A-Z\n          shortcut.push(e.code.replace('Key', ''));\n        } else if (e.key === 'Meta') {\n          // ⌘ Command on macOS\n          shortcut.push('Command');\n        } else if (['Alt', 'Control', 'Shift'].includes(e.key)) {\n          shortcut.push(e.key);\n        } else if (e.keyCode >= 48 && e.keyCode <= 57) {\n          // 0-9\n          shortcut.push(e.code.replace('Digit', ''));\n        } else if (e.keyCode >= 112 && e.keyCode <= 123) {\n          // F1-F12\n          shortcut.push(e.code);\n        } else if (\n          ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(e.key)\n        ) {\n          // Arrows\n          shortcut.push(e.code.replace('Arrow', ''));\n        } else if (validShortcutCodes.includes(e.key)) {\n          shortcut.push(e.key);\n        }\n      });\n      const sortTable = {\n        Control: 1,\n        Shift: 2,\n        Alt: 3,\n        Command: 4,\n      };\n      shortcut = shortcut.sort((a, b) => {\n        if (!sortTable[a] || !sortTable[b]) return 0;\n        if (sortTable[a] - sortTable[b] <= -1) {\n          return -1;\n        } else if (sortTable[a] - sortTable[b] >= 1) {\n          return 1;\n        } else {\n          return 0;\n        }\n      });\n      shortcut = shortcut.join('+');\n      return shortcut;\n    },\n\n    lang: {\n      get() {\n        return this.settings.lang;\n      },\n      set(lang) {\n        this.$i18n.locale = lang;\n        this.$store.commit('changeLang', lang);\n      },\n    },\n    musicLanguage: {\n      get() {\n        return this.settings.musicLanguage ?? 'all';\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'musicLanguage',\n          value,\n        });\n      },\n    },\n    appearance: {\n      get() {\n        if (this.settings.appearance === undefined) return 'auto';\n        return this.settings.appearance;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'appearance',\n          value,\n        });\n        changeAppearance(value);\n      },\n    },\n    trayIconTheme: {\n      get() {\n        if (this.settings.trayIconTheme === undefined) return 'auto';\n        return this.settings.trayIconTheme;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'trayIconTheme',\n          value,\n        });\n        if (this.isElectron) {\n          ipcRenderer.send('updateTrayIcon', value);\n        }\n      },\n    },\n    musicQuality: {\n      get() {\n        return this.settings.musicQuality ?? 320000;\n      },\n      set(value) {\n        if (value === this.settings.musicQuality) return;\n        this.$store.commit('changeMusicQuality', value);\n        this.clearCache();\n      },\n    },\n    lyricFontSize: {\n      get() {\n        if (this.settings.lyricFontSize === undefined) return 28;\n        return this.settings.lyricFontSize;\n      },\n      set(value) {\n        this.$store.commit('changeLyricFontSize', value);\n      },\n    },\n    outputDevice: {\n      get() {\n        const isValidDevice = this.allOutputDevices.find(\n          device => device.deviceId === this.settings.outputDevice\n        );\n        if (\n          this.settings.outputDevice === undefined ||\n          isValidDevice === undefined\n        )\n          return 'default'; // Default deviceId\n        return this.settings.outputDevice;\n      },\n      set(deviceId) {\n        if (deviceId === this.settings.outputDevice || deviceId === undefined)\n          return;\n        this.$store.commit('changeOutputDevice', deviceId);\n        this.player.setOutputDevice();\n      },\n    },\n    enableUnblockNeteaseMusic: {\n      get() {\n        const value = this.settings.enableUnblockNeteaseMusic;\n        return value !== undefined ? value : true;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'enableUnblockNeteaseMusic',\n          value,\n        });\n      },\n    },\n    showPlaylistsByAppleMusic: {\n      get() {\n        if (this.settings.showPlaylistsByAppleMusic === undefined) return true;\n        return this.settings.showPlaylistsByAppleMusic;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'showPlaylistsByAppleMusic',\n          value,\n        });\n      },\n    },\n    nyancatStyle: {\n      get() {\n        if (this.settings.nyancatStyle === undefined) return false;\n        return this.settings.nyancatStyle;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'nyancatStyle',\n          value,\n        });\n      },\n    },\n    automaticallyCacheSongs: {\n      get() {\n        if (this.settings.automaticallyCacheSongs === undefined) return false;\n        return this.settings.automaticallyCacheSongs;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'automaticallyCacheSongs',\n          value,\n        });\n        if (value === false) {\n          this.clearCache();\n        }\n      },\n    },\n    showLyricsTranslation: {\n      get() {\n        return this.settings.showLyricsTranslation;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'showLyricsTranslation',\n          value,\n        });\n      },\n    },\n    lyricsBackground: {\n      get() {\n        return this.settings.lyricsBackground || false;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'lyricsBackground',\n          value,\n        });\n      },\n    },\n    showLyricsTime: {\n      get() {\n        return this.settings.showLyricsTime;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'showLyricsTime',\n          value,\n        });\n      },\n    },\n    enableOsdlyricsSupport: {\n      get() {\n        return this.settings.enableOsdlyricsSupport;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'enableOsdlyricsSupport',\n          value,\n        });\n      },\n    },\n    closeAppOption: {\n      get() {\n        return this.settings.closeAppOption;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'closeAppOption',\n          value,\n        });\n      },\n    },\n    enableDiscordRichPresence: {\n      get() {\n        return this.settings.enableDiscordRichPresence;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'enableDiscordRichPresence',\n          value,\n        });\n      },\n    },\n    subTitleDefault: {\n      get() {\n        return this.settings.subTitleDefault;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'subTitleDefault',\n          value,\n        });\n      },\n    },\n    enableReversedMode: {\n      get() {\n        if (this.settings.enableReversedMode === undefined) return false;\n        return this.settings.enableReversedMode;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'enableReversedMode',\n          value,\n        });\n        if (value === false) {\n          this.$store.state.player.reversed = false;\n        }\n      },\n    },\n    enableGlobalShortcut: {\n      get() {\n        return this.settings.enableGlobalShortcut;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'enableGlobalShortcut',\n          value,\n        });\n      },\n    },\n    showLibraryDefault: {\n      get() {\n        return this.settings.showLibraryDefault || false;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'showLibraryDefault',\n          value,\n        });\n      },\n    },\n    cacheLimit: {\n      get() {\n        return this.settings.cacheLimit || false;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'cacheLimit',\n          value,\n        });\n      },\n    },\n    proxyProtocol: {\n      get() {\n        return this.settings.proxyConfig?.protocol || 'noProxy';\n      },\n      set(value) {\n        let config = this.settings.proxyConfig || {};\n        config.protocol = value;\n        if (value === 'noProxy') {\n          ipcRenderer.send('removeProxy');\n          this.showToast('已关闭代理');\n        }\n        this.$store.commit('updateSettings', {\n          key: 'proxyConfig',\n          value: config,\n        });\n      },\n    },\n    proxyServer: {\n      get() {\n        return this.settings.proxyConfig?.server || '';\n      },\n      set(value) {\n        let config = this.settings.proxyConfig || {};\n        config.server = value;\n        this.$store.commit('updateSettings', {\n          key: 'proxyConfig',\n          value: config,\n        });\n      },\n    },\n    enableRealIP: {\n      get() {\n        return this.settings.enableRealIP || false;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'enableRealIP',\n          value: value,\n        });\n      },\n    },\n    realIP: {\n      get() {\n        return this.settings.realIP || '';\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'realIP',\n          value: value,\n        });\n      },\n    },\n    proxyPort: {\n      get() {\n        return this.settings.proxyConfig?.port || '';\n      },\n      set(value) {\n        let config = this.settings.proxyConfig || {};\n        config.port = value;\n        this.$store.commit('updateSettings', {\n          key: 'proxyConfig',\n          value: config,\n        });\n      },\n    },\n    unmSource: {\n      /**\n       * @returns {string}\n       */\n      get() {\n        return this.settings.unmSource || '';\n      },\n      /** @param {string?} value */\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'unmSource',\n          value: value.length && value,\n        });\n      },\n    },\n    unmSearchMode: {\n      get() {\n        return this.settings.unmSearchMode || 'fast-first';\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'unmSearchMode',\n          value: value,\n        });\n      },\n    },\n    unmEnableFlac: {\n      get() {\n        return this.settings.unmEnableFlac || false;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'unmEnableFlac',\n          value: value || false,\n        });\n      },\n    },\n    unmProxyUri: {\n      get() {\n        return this.settings.unmProxyUri || '';\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'unmProxyUri',\n          value: value.length && value,\n        });\n      },\n    },\n    unmJooxCookie: {\n      get() {\n        return this.settings.unmJooxCookie || '';\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'unmJooxCookie',\n          value: value.length && value,\n        });\n      },\n    },\n    unmQQCookie: {\n      get() {\n        return this.settings.unmQQCookie || '';\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'unmQQCookie',\n          value: value.length && value,\n        });\n      },\n    },\n    unmYtDlExe: {\n      get() {\n        return this.settings.unmYtDlExe || '';\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'unmYtDlExe',\n          value: value.length && value,\n        });\n      },\n    },\n    enableCustomTitlebar: {\n      get() {\n        return this.settings.linuxEnableCustomTitlebar;\n      },\n      set(value) {\n        this.$store.commit('updateSettings', {\n          key: 'linuxEnableCustomTitlebar',\n          value,\n        });\n      },\n    },\n    isLastfmConnected() {\n      return this.lastfm.key !== undefined;\n    },\n  },\n  created() {\n    this.countDBSize('tracks');\n    if (process.env.IS_ELECTRON) this.getAllOutputDevices();\n  },\n  activated() {\n    this.countDBSize('tracks');\n    if (process.env.IS_ELECTRON) this.getAllOutputDevices();\n  },\n  methods: {\n    ...mapActions(['showToast']),\n    getAllOutputDevices() {\n      navigator.mediaDevices.enumerateDevices().then(devices => {\n        this.allOutputDevices = devices.filter(device => {\n          return device.kind == 'audiooutput';\n        });\n        if (\n          this.allOutputDevices.length > 0 &&\n          this.allOutputDevices[0].label !== ''\n        ) {\n          this.withoutAudioPriviledge = false;\n        } else {\n          this.allOutputDevices = [\n            {\n              deviceId: 'default',\n              label: 'settings.permissionRequired',\n            },\n          ];\n        }\n      });\n    },\n    logout() {\n      doLogout();\n      this.$router.push({ name: 'home' });\n    },\n    countDBSize() {\n      countDBSize().then(data => {\n        if (data === undefined) {\n          this.tracksCache = {\n            size: '0KB',\n            length: 0,\n          };\n          return;\n        }\n        this.tracksCache.size = bytesToSize(data.bytes);\n        this.tracksCache.length = data.length;\n      });\n    },\n    clearCache() {\n      clearDB().then(() => {\n        this.countDBSize();\n      });\n    },\n    lastfmConnect() {\n      lastfmAuth();\n      let lastfmChecker = setInterval(() => {\n        const session = localStorage.getItem('lastfm');\n        if (session) {\n          this.$store.commit('updateLastfm', JSON.parse(session));\n          clearInterval(lastfmChecker);\n        }\n      }, 1000);\n    },\n    lastfmDisconnect() {\n      localStorage.removeItem('lastfm');\n      this.$store.commit('updateLastfm', {});\n    },\n    sendProxyConfig() {\n      if (this.proxyProtocol === 'noProxy') return;\n      const config = this.settings.proxyConfig;\n      if (\n        config.server === '' ||\n        !config.port ||\n        config.protocol === 'noProxy'\n      ) {\n        ipcRenderer.send('removeProxy');\n      } else {\n        ipcRenderer.send('setProxy', config);\n      }\n      this.showToast('已更新代理设置');\n    },\n    clickOutside() {\n      this.exitRecordShortcut();\n    },\n    formatShortcut(shortcut) {\n      shortcut = shortcut\n        .replaceAll('+', ' + ')\n        .replace('Up', '↑')\n        .replace('Down', '↓')\n        .replace('Right', '→')\n        .replace('Left', '←');\n      if (this.settings.lang === 'zh-CN') {\n        shortcut = shortcut.replace('Space', '空格');\n      } else if (this.settings.lang === 'zh-TW') {\n        shortcut = shortcut.replace('Space', '空白鍵');\n      }\n      if (process.platform === 'darwin') {\n        return shortcut\n          .replace('CommandOrControl', '⌘')\n          .replace('Command', '⌘')\n          .replace('Alt', '⌥')\n          .replace('Control', '⌃')\n          .replace('Shift', '⇧');\n      }\n      return shortcut.replace('CommandOrControl', 'Ctrl');\n    },\n    readyToRecordShortcut(id, type) {\n      if (type === 'globalShortcut' && this.enableGlobalShortcut === false) {\n        return;\n      }\n      this.shortcutInput = { id, type, recording: true };\n      this.recordedShortcut = [];\n      ipcRenderer.send('switchGlobalShortcutStatusTemporary', 'disable');\n    },\n    handleShortcutKeydown(e) {\n      if (this.shortcutInput.recording === false) return;\n      e.preventDefault();\n      if (this.recordedShortcut.find(s => s.keyCode === e.keyCode)) return;\n      this.recordedShortcut.push(e);\n      if (\n        (e.keyCode >= 65 && e.keyCode <= 90) || // A-Z\n        (e.keyCode >= 48 && e.keyCode <= 57) || // 0-9\n        (e.keyCode >= 112 && e.keyCode <= 123) || // F1-F12\n        ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(e.key) || // Arrows\n        validShortcutCodes.includes(e.key)\n      ) {\n        this.saveShortcut();\n      }\n    },\n    handleShortcutKeyup(e) {\n      if (this.recordedShortcut.find(s => s.keyCode === e.keyCode)) {\n        this.recordedShortcut = this.recordedShortcut.filter(\n          s => s.keyCode !== e.keyCode\n        );\n      }\n    },\n    saveShortcut() {\n      const { id, type } = this.shortcutInput;\n      const payload = {\n        id,\n        type,\n        shortcut: this.recordedShortcutComputed,\n      };\n      this.$store.commit('updateShortcut', payload);\n      ipcRenderer.send('updateShortcut', payload);\n      this.showToast('快捷键已保存');\n      this.recordedShortcut = [];\n    },\n    exitRecordShortcut() {\n      if (this.shortcutInput.recording === false) return;\n      this.shortcutInput = { id: '', type: '', recording: false };\n      this.recordedShortcut = [];\n      ipcRenderer.send('switchGlobalShortcutStatusTemporary', 'enable');\n    },\n    restoreDefaultShortcuts() {\n      this.$store.commit('restoreDefaultShortcuts');\n      ipcRenderer.send('restoreDefaultShortcuts');\n    },\n  },\n};\n</script>\n\n<style lang=\"scss\" scoped>\n.settings-page {\n  display: flex;\n  justify-content: center;\n  margin-top: 32px;\n}\n.container {\n  margin-top: 24px;\n  width: 720px;\n}\nh2 {\n  margin-top: 48px;\n  font-size: 36px;\n  color: var(--color-text);\n}\n\nh3 {\n  margin-top: 48px;\n  padding-bottom: 12px;\n  font-size: 26px;\n  color: var(--color-text);\n  border-bottom: 1px solid rgba(128, 128, 128, 0.18);\n}\n\n.user {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  background: var(--color-secondary-bg);\n  color: var(--color-text);\n  padding: 16px 20px;\n  border-radius: 16px;\n  margin-bottom: 48px;\n  img.avatar {\n    border-radius: 50%;\n    height: 64px;\n    width: 64px;\n  }\n  img.cvip {\n    height: 13px;\n    margin-right: 4px;\n  }\n  .left {\n    display: flex;\n    align-items: center;\n    .info {\n      margin-left: 24px;\n    }\n    .nickname {\n      font-size: 20px;\n      font-weight: 600;\n      margin-bottom: 2px;\n    }\n    .extra-info {\n      font-size: 13px;\n      .text {\n        opacity: 0.68;\n      }\n      .vip {\n        display: flex;\n        align-items: center;\n      }\n    }\n  }\n  .right {\n    .svg-icon {\n      height: 18px;\n      width: 18px;\n      margin-right: 4px;\n    }\n    button {\n      display: flex;\n      align-items: center;\n      font-size: 18px;\n      font-weight: 600;\n      text-decoration: none;\n      border-radius: 10px;\n      padding: 8px 12px;\n      opacity: 0.68;\n      color: var(--color-text);\n      transition: 0.2s;\n      margin: {\n        right: 12px;\n        left: 12px;\n      }\n      &:hover {\n        opacity: 1;\n        background: #eaeffd;\n        color: #335eea;\n      }\n      &:active {\n        opacity: 1;\n        transform: scale(0.92);\n        transition: 0.2s;\n      }\n    }\n  }\n}\n\n.item {\n  margin: 24px 0;\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  color: var(--color-text);\n\n  .title {\n    font-size: 16px;\n    font-weight: 500;\n    opacity: 0.78;\n  }\n\n  .description {\n    font-size: 14px;\n    margin-top: 0.5em;\n    opacity: 0.7;\n  }\n}\n\nselect {\n  min-width: 192px;\n  max-width: 600px;\n  font-weight: 600;\n  border: none;\n  padding: 8px 12px 8px 12px;\n  border-radius: 8px;\n  color: var(--color-text);\n  background: var(--color-secondary-bg);\n  appearance: none;\n  &:focus {\n    outline: none;\n    color: var(--color-primary);\n    background: var(--color-primary-bg);\n  }\n}\n\nbutton {\n  color: var(--color-text);\n  background: var(--color-secondary-bg);\n  padding: 8px 12px 8px 12px;\n  font-weight: 600;\n  border-radius: 8px;\n  transition: 0.2s;\n  &:hover {\n    transform: scale(1.06);\n  }\n  &:active {\n    transform: scale(0.94);\n  }\n}\n\ninput.text-input.margin-right-0 {\n  margin-right: 0;\n}\ninput.text-input {\n  background: var(--color-secondary-bg);\n  border: none;\n  margin-right: 22px;\n  padding: 8px 12px 8px 12px;\n  border-radius: 8px;\n  color: var(--color-text);\n  font-weight: 600;\n  font-size: 16px;\n}\ninput::-webkit-outer-spin-button,\ninput::-webkit-inner-spin-button {\n  -webkit-appearance: none;\n}\ninput[type='number'] {\n  -moz-appearance: textfield;\n}\n\n#proxy-form,\n#real-ip {\n  display: flex;\n  align-items: center;\n}\n#proxy-form.disabled,\n#real-ip.disabled {\n  opacity: 0.47;\n  button:hover {\n    transform: unset;\n  }\n}\n\n#shortcut-table {\n  font-size: 14px;\n  /* border: 1px solid black; */\n  user-select: none;\n  color: var(--color-text);\n  .row {\n    display: flex;\n  }\n  .row.row-head {\n    opacity: 0.58;\n    font-size: 13px;\n    font-weight: 500;\n  }\n  .col {\n    min-width: 192px;\n    padding: 8px;\n    display: flex;\n    align-items: center;\n    /* border: 1px solid red; */\n    &:first-of-type {\n      padding-left: 0;\n      min-width: 128px;\n    }\n  }\n  .keyboard-input {\n    font-weight: 600;\n    background-color: var(--color-secondary-bg);\n    padding: 8px 12px 8px 12px;\n    border-radius: 0.5rem;\n    min-width: 146px;\n    min-height: 34px;\n    box-sizing: border-box;\n    &.active {\n      color: var(--color-primary);\n      background-color: var(--color-primary-bg);\n    }\n  }\n  .restore-default-shortcut {\n    margin-top: 12px;\n  }\n  &.global-disabled {\n    .row .col:last-child {\n      opacity: 0.48;\n    }\n    .row.row-head .col:last-child {\n      opacity: 1;\n    }\n  }\n  &:focus {\n    outline: none;\n  }\n}\n\n.footer {\n  text-align: center;\n  margin-top: 6rem;\n  color: var(--color-text);\n  font-weight: 600;\n  .author {\n    font-size: 0.9rem;\n  }\n  .version {\n    font-size: 0.88rem;\n    opacity: 0.58;\n    margin-top: -10px;\n  }\n}\n\n.beforeAnimation {\n  -webkit-transition: 0.2s cubic-bezier(0.24, 0, 0.5, 1);\n  transition: 0.2s cubic-bezier(0.24, 0, 0.5, 1);\n}\n.afterAnimation {\n  box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.1), 0 4px 0px 0 hsla(0, 0%, 0%, 0.04),\n    0 4px 9px hsla(0, 0%, 0%, 0.13), 0 3px 3px hsla(0, 0%, 0%, 0.05);\n  -webkit-transition: 0.35s cubic-bezier(0.54, 1.6, 0.5, 1);\n  transition: 0.35s cubic-bezier(0.54, 1.6, 0.5, 1);\n}\n.toggle {\n  margin: auto;\n}\n.toggle input {\n  opacity: 0;\n  position: absolute;\n}\n.toggle input + label {\n  position: relative;\n  display: inline-block;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  -webkit-transition: 0.4s ease;\n  transition: 0.4s ease;\n  height: 32px;\n  width: 52px;\n  background: var(--color-secondary-bg);\n  border-radius: 8px;\n}\n.toggle input + label:before {\n  content: '';\n  position: absolute;\n  display: block;\n  -webkit-transition: 0.2s cubic-bezier(0.24, 0, 0.5, 1);\n  transition: 0.2s cubic-bezier(0.24, 0, 0.5, 1);\n  height: 32px;\n  width: 52px;\n  top: 0;\n  left: 0;\n  border-radius: 8px;\n}\n.toggle input + label:after {\n  content: '';\n  position: absolute;\n  display: block;\n  box-shadow: 0 0 0 1px hsla(0, 0%, 0%, 0.02), 0 4px 0px 0 hsla(0, 0%, 0%, 0.01),\n    0 4px 9px hsla(0, 0%, 0%, 0.08), 0 3px 3px hsla(0, 0%, 0%, 0.03);\n  -webkit-transition: 0.35s cubic-bezier(0.54, 1.6, 0.5, 1);\n  transition: 0.35s cubic-bezier(0.54, 1.6, 0.5, 1);\n  background: #fff;\n  height: 20px;\n  width: 20px;\n  top: 6px;\n  left: 6px;\n  border-radius: 6px;\n}\n.toggle input:checked + label:before {\n  background: var(--color-primary);\n  -webkit-transition: width 0.2s cubic-bezier(0, 0, 0, 0.1);\n  transition: width 0.2s cubic-bezier(0, 0, 0, 0.1);\n}\n.toggle input:checked + label:after {\n  left: 26px;\n}\n</style>\n"
  },
  {
    "path": "vercel.example.json",
    "content": "{\n  \"rewrites\": [\n    {\n      \"source\": \"/api/:match*\",\n      \"destination\": \"https://your-netease-api.example.com/:match*\"\n    }\n  ]\n}\n"
  },
  {
    "path": "vue.config.js",
    "content": "const webpack = require('webpack');\nconst path = require('path');\nfunction resolve(dir) {\n  return path.join(__dirname, dir);\n}\n\nmodule.exports = {\n  // 生产环境打包不输出 map\n  productionSourceMap: false,\n  devServer: {\n    disableHostCheck: true,\n    port: process.env.DEV_SERVER_PORT || 8080,\n    proxy: {\n      '^/api': {\n        target: 'http://localhost:3000',\n        changeOrigin: true,\n        pathRewrite: {\n          '^/api': '/',\n        },\n      },\n    },\n  },\n  pwa: {\n    name: 'YesPlayMusic',\n    iconPaths: {\n      favicon32: 'img/icons/favicon-32x32.png',\n    },\n    themeColor: '#ffffff00',\n    manifestOptions: {\n      background_color: '#335eea',\n    },\n    // workboxOptions: {\n    //   swSrc: \"dev/sw.js\",\n    // },\n  },\n  pages: {\n    index: {\n      entry: 'src/main.js',\n      template: 'public/index.html',\n      filename: 'index.html',\n      title: 'YesPlayMusic',\n      chunks: ['main', 'chunk-vendors', 'chunk-common', 'index'],\n    },\n  },\n  chainWebpack(config) {\n    config.module.rules.delete('svg');\n    config.module.rule('svg').exclude.add(resolve('src/assets/icons')).end();\n    config.module\n      .rule('icons')\n      .test(/\\.svg$/)\n      .include.add(resolve('src/assets/icons'))\n      .end()\n      .use('svg-sprite-loader')\n      .loader('svg-sprite-loader')\n      .options({\n        symbolId: 'icon-[name]',\n      })\n      .end();\n    config.module\n      .rule('napi')\n      .test(/\\.node$/)\n      .use('node-loader')\n      .loader('node-loader')\n      .end();\n\n    config.module\n      .rule('webpack4_es_fallback')\n      .test(/\\.js$/)\n      .include.add(/node_modules/)\n      .end()\n      .use('esbuild-loader')\n      .loader('esbuild-loader')\n      .options({ target: 'es2015', format: \"cjs\" })\n      .end();\n\n    // LimitChunkCountPlugin 可以通过合并块来对块进行后期处理。用以解决 chunk 包太多的问题\n    config.plugin('chunkPlugin').use(webpack.optimize.LimitChunkCountPlugin, [\n      {\n        maxChunks: 3,\n        minChunkSize: 10_000,\n      },\n    ]);\n  },\n  // 添加插件的配置\n  pluginOptions: {\n    // electron-builder的配置文件\n    electronBuilder: {\n      nodeIntegration: true,\n      externals: ['@unblockneteasemusic/rust-napi'],\n      builderOptions: {\n        productName: 'YesPlayMusic',\n        copyright: 'Copyright © YesPlayMusic',\n        // compression: \"maximum\", // 机器好的可以打开，配置压缩，开启后会让 .AppImage 格式的客户端启动缓慢\n        asar: true,\n        publish: [\n          {\n            provider: 'github',\n            owner: 'qier222',\n            repo: 'YesPlayMusic',\n            vPrefixedTagName: true,\n            releaseType: 'draft',\n          },\n        ],\n        directories: {\n          output: 'dist_electron',\n        },\n        mac: {\n          target: [\n            {\n              target: 'dmg',\n              arch: ['x64', 'arm64', 'universal'],\n            },\n          ],\n          artifactName: '${productName}-${os}-${version}-${arch}.${ext}',\n          category: 'public.app-category.music',\n          darkModeSupport: true,\n        },\n        win: {\n          target: [\n            {\n              target: 'portable',\n              arch: ['x64'],\n            },\n            {\n              target: 'nsis',\n              arch: ['x64'],\n            },\n          ],\n          publisherName: 'YesPlayMusic',\n          icon: 'build/icons/icon.ico',\n          publish: ['github'],\n        },\n        linux: {\n          target: [\n            {\n              target: 'AppImage',\n              arch: ['x64'],\n            },\n            {\n              target: 'tar.gz',\n              arch: ['x64', 'arm64'],\n            },\n            {\n              target: 'deb',\n              arch: ['x64', 'armv7l', 'arm64'],\n            },\n            {\n              target: 'rpm',\n              arch: ['x64'],\n            },\n            {\n              target: 'snap',\n              arch: ['x64'],\n            },\n            {\n              target: 'pacman',\n              arch: ['x64'],\n            },\n          ],\n          category: 'Music',\n          icon: './build/icon.icns',\n        },\n        dmg: {\n          icon: 'build/icons/icon.icns',\n        },\n        nsis: {\n          oneClick: true,\n          perMachine: true,\n          deleteAppDataOnUninstall: true,\n        },\n      },\n      // 主线程的配置文件\n      chainWebpackMainProcess: config => {\n        config.plugin('define').tap(args => {\n          args[0]['IS_ELECTRON'] = true;\n          return args;\n        });\n        config.resolve.alias.set(\n          'jsbi',\n          path.join(__dirname, 'node_modules/jsbi/dist/jsbi-cjs.js')\n        );\n\n        config.module\n          .rule('webpack4_es_fallback')\n          .test(/\\.js$/)\n          .include.add(/node_modules/)\n          .end()\n          .use('esbuild-loader')\n          .loader('esbuild-loader')\n          .options({ target: 'es2015', format: \"cjs\" })\n          .end();\n      },\n      // 渲染线程的配置文件\n      chainWebpackRendererProcess: config => {\n        // 渲染线程的一些其他配置\n        // Chain webpack config for electron renderer process only\n        // The following example will set IS_ELECTRON to true in your app\n        config.plugin('define').tap(args => {\n          args[0]['IS_ELECTRON'] = true;\n          return args;\n        });\n      },\n      // 主入口文件\n      // mainProcessFile: 'src/main.js',\n      // mainProcessArgs: []\n    },\n  },\n};\n"
  }
]