WIP: Vue 3 迁移 #15
|
@ -10,7 +10,7 @@ steps:
|
||||||
- ./scripts/build-wasm.sh
|
- ./scripts/build-wasm.sh
|
||||||
|
|
||||||
- name: build
|
- name: build
|
||||||
image: node:16.18-bullseye
|
image: node:18.12-bullseye
|
||||||
commands:
|
commands:
|
||||||
- apt-get update
|
- apt-get update
|
||||||
- apt-get install -y jq zip
|
- apt-get install -y jq zip
|
||||||
|
@ -18,7 +18,8 @@ steps:
|
||||||
- npm run test
|
- npm run test
|
||||||
- ./scripts/build-and-package.sh legacy
|
- ./scripts/build-and-package.sh legacy
|
||||||
- ./scripts/build-and-package.sh extension
|
- ./scripts/build-and-package.sh extension
|
||||||
- ./scripts/build-and-package.sh modern
|
# FIXME: --modern is not a valid flag. Is this still required?
|
||||||
|
# - ./scripts/build-and-package.sh modern
|
||||||
|
|
||||||
- name: upload artifact
|
- name: upload artifact
|
||||||
image: node:16.18-bullseye
|
image: node:16.18-bullseye
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
insert_final_newline = false
|
|
@ -32,3 +32,7 @@ yarn-error.log*
|
||||||
*.zip
|
*.zip
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
/sha256sum.txt
|
/sha256sum.txt
|
||||||
|
|
||||||
|
# auto generated
|
||||||
|
/auto-imports.d.ts
|
||||||
|
/components.d.ts
|
||||||
|
|
|
@ -3,10 +3,9 @@ cache:
|
||||||
paths:
|
paths:
|
||||||
- node_modules/
|
- node_modules/
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- build
|
- build
|
||||||
|
|
||||||
|
|
||||||
build-job:
|
build-job:
|
||||||
stage: build
|
stage: build
|
||||||
script: |
|
script: |
|
||||||
|
@ -37,7 +36,7 @@ build-job:
|
||||||
sha256sum *.tar.gz *.zip > sha256sum.txt
|
sha256sum *.tar.gz *.zip > sha256sum.txt
|
||||||
|
|
||||||
artifacts:
|
artifacts:
|
||||||
name: "$CI_JOB_NAME"
|
name: '$CI_JOB_NAME'
|
||||||
paths:
|
paths:
|
||||||
- legacy.zip
|
- legacy.zip
|
||||||
- legacy.tar.gz
|
- legacy.tar.gz
|
||||||
|
|
|
@ -1,39 +1,34 @@
|
||||||
---
|
---
|
||||||
name: Bug报告
|
name: Bug报告
|
||||||
about: 报告Bug以帮助改进程序
|
about: 报告Bug以帮助改进程序
|
||||||
title: ''
|
title: ''
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
---
|
||||||
---
|
|
||||||
|
- 请按照此模板填写,否则可能立即被关闭
|
||||||
* 请按照此模板填写,否则可能立即被关闭
|
|
||||||
|
* [x] 我确认已经搜索过 Issue 不存并确认相同的 Issue
|
||||||
- [x] 我确认已经搜索过Issue不存并确认相同的Issue
|
* [x] 我有证据表明这是程序导致的问题(如不确认,可以在[Discussions](https://github.com/ix64/unlock-music/discussions)内提出)
|
||||||
- [x] 我有证据表明这是程序导致的问题(如不确认,可以在[Discussions](https://github.com/ix64/unlock-music/discussions)内提出)
|
|
||||||
|
**Bug 描述**
|
||||||
|
|
||||||
**Bug描述**
|
简要地复述你遇到的 Bug
|
||||||
|
|
||||||
简要地复述你遇到的Bug
|
**复现方法**
|
||||||
|
|
||||||
**复现方法**
|
描述复现方法,必要时请提供样本文件
|
||||||
|
|
||||||
描述复现方法,必要时请提供样本文件
|
**程序截图或者 Console 报错信息**
|
||||||
|
|
||||||
**程序截图或者Console报错信息**
|
如果可以请提供二者之一
|
||||||
|
|
||||||
如果可以请提供二者之一
|
**环境信息:**
|
||||||
|
|
||||||
|
- 操作系统和浏览器:
|
||||||
**环境信息:**
|
- 程序版本:
|
||||||
|
- 获取音乐文件所使用的客户端及其版本信息:
|
||||||
- 操作系统和浏览器:
|
|
||||||
- 程序版本:
|
**附加信息**
|
||||||
- 获取音乐文件所使用的客户端及其版本信息:
|
|
||||||
|
其他能够帮助确认问题的信息
|
||||||
|
|
||||||
**附加信息**
|
|
||||||
|
|
||||||
其他能够帮助确认问题的信息
|
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,22 @@
|
||||||
---
|
---
|
||||||
name: 新功能
|
name: 新功能
|
||||||
about: 对于程序新的想法或建议
|
about: 对于程序新的想法或建议
|
||||||
title: ''
|
title: ''
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
---
|
||||||
---
|
|
||||||
|
- 请按照此模板填写,否则可能立即被关闭
|
||||||
- 请按照此模板填写,否则可能立即被关闭
|
|
||||||
|
**背景和说明**
|
||||||
**背景和说明**
|
|
||||||
|
简要说明产生此想法的背景和此想法的具体内容
|
||||||
简要说明产生此想法的背景和此想法的具体内容
|
|
||||||
|
**实现途径**
|
||||||
|
|
||||||
**实现途径**
|
- 如果没有设计方案,请简要描述实现思路
|
||||||
|
- 如果你没有任何的实现思路,请通过[Discussions](https://github.com/ix64/unlock-music/discussions)或者 Telegram 进行讨论
|
||||||
- 如果没有设计方案,请简要描述实现思路
|
|
||||||
- 如果你没有任何的实现思路,请通过[Discussions](https://github.com/ix64/unlock-music/discussions)或者Telegram进行讨论
|
**附加信息**
|
||||||
|
|
||||||
|
更多你想要表达的内容
|
||||||
**附加信息**
|
|
||||||
|
|
||||||
更多你想要表达的内容
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
src/QmcWasm/
|
||||||
|
src/KgmWasm/
|
17
README.md
|
@ -11,7 +11,7 @@
|
||||||
[授权协议]: https://git.unlock-music.dev/um/web/src/branch/master/LICENSE
|
[授权协议]: https://git.unlock-music.dev/um/web/src/branch/master/LICENSE
|
||||||
[unlock-music/cli]: https://git.unlock-music.dev/um/cli
|
[unlock-music/cli]: https://git.unlock-music.dev/um/cli
|
||||||
[`@unlock_music_chat`]: https://t.me/unlock_music_chat
|
[`@unlock_music_chat`]: https://t.me/unlock_music_chat
|
||||||
[UM-Packages]: https://git.unlock-music.dev/um/-/packages/generic/web-build/
|
[um-packages]: https://git.unlock-music.dev/um/-/packages/generic/web-build/
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
|
|
||||||
### 使用预构建版本
|
### 使用预构建版本
|
||||||
|
|
||||||
- 从 [Release] 或 [CI 构建][UM-Packages] 下载预构建的版本
|
- 从 [Release] 或 [CI 构建][um-packages] 下载预构建的版本
|
||||||
- :warning: 本地使用请下载`legacy版本`(`modern版本`只能通过 **http(s)协议** 访问)
|
- :warning: 本地使用请下载`legacy版本`(`modern版本`只能通过 **http(s)协议** 访问)
|
||||||
- 解压缩后即可部署或本地使用(**请勿直接运行源代码**)
|
- 解压缩后即可部署或本地使用(**请勿直接运行源代码**)
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@
|
||||||
|
|
||||||
### 自行构建
|
### 自行构建
|
||||||
|
|
||||||
#### JS部分
|
#### JS 部分
|
||||||
|
|
||||||
- 环境要求
|
- 环境要求
|
||||||
- nodejs (v16.x)
|
- nodejs (v16.x)
|
||||||
|
@ -77,14 +77,15 @@
|
||||||
npm run make-extension
|
npm run make-extension
|
||||||
```
|
```
|
||||||
|
|
||||||
#### WASM部分
|
#### WASM 部分
|
||||||
|
|
||||||
- 环境要求
|
- 环境要求
|
||||||
|
|
||||||
- Linux
|
- Linux
|
||||||
- python3
|
- python3
|
||||||
|
|
||||||
- 运行此目录下的build-wasm
|
- 运行此目录下的 build-wasm
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
./scripts/build-wasm.sh
|
./scripts/build-wasm.sh
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: ['@vue/app', '@babel/preset-typescript'],
|
||||||
'@vue/app',
|
plugins: [
|
||||||
'@babel/preset-typescript'
|
[
|
||||||
|
'component',
|
||||||
|
{
|
||||||
|
libraryName: 'element-ui',
|
||||||
|
styleLibraryName: 'theme-chalk',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
plugins: [
|
],
|
||||||
["component", {
|
|
||||||
"libraryName": "element-ui",
|
|
||||||
"styleLibraryName": "theme-chalk"
|
|
||||||
}]
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 641 B After Width: | Height: | Size: 641 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,40 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta content="webkit" name="renderer" />
|
||||||
|
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" />
|
||||||
|
<meta content="width=device-width,initial-scale=1.0" name="viewport" />
|
||||||
|
<title>音乐解锁</title>
|
||||||
|
<meta content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密" name="keywords" />
|
||||||
|
<meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="loader-mask">
|
||||||
|
<div id="loader"></div>
|
||||||
|
<noscript>
|
||||||
|
<h3 id="loader-js">请启用JavaScript</h3>
|
||||||
|
</noscript>
|
||||||
|
<h3 id="loader-source">请勿直接运行源代码!</h3>
|
||||||
|
<div id="loader-tips-outdated" hidden>
|
||||||
|
<h2>您可能在使用不受支持的<span style="color: #f00">过时</span>浏览器,这可能导致此应用无法正常工作。</h2>
|
||||||
|
<h3>
|
||||||
|
如果您使用双核浏览器,您可以尝试切换到
|
||||||
|
<span style="color: #f00">“极速模式”</span> 解决此问题。
|
||||||
|
</h3>
|
||||||
|
<h3>或者,您可以尝试更换下方的几个浏览器之一。</h3>
|
||||||
|
</div>
|
||||||
|
<h3 id="loader-tips-timeout" hidden>
|
||||||
|
音乐解锁采用了一些新特性!建议使用
|
||||||
|
<a href="https://www.microsoft.com/zh-cn/edge" target="_blank">Microsoft Edge Chromium</a>
|
||||||
|
<a href="https://www.google.cn/chrome/" target="_blank">Google Chrome</a>
|
||||||
|
<a href="https://www.firefox.com.cn/" target="_blank">Mozilla Firefox</a>
|
||||||
|
|
|
||||||
|
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="./src/loader.js"></script>
|
||||||
|
<script type="module" src="./src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,7 +1,11 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
testPathIgnorePatterns: ['/build/', '/dist/', '/node_modules/'],
|
testPathIgnorePatterns: ['/build/', '/dist/', '/node_modules/'],
|
||||||
setupFilesAfterEnv: ['./src/__test__/setup_jest.js'],
|
setupFilesAfterEnv: ['./src/__test__/setup_jest.js'],
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
'@/(.*)': '<rootDir>/src/$1',
|
'@/(.*)': '<rootDir>/src/$1',
|
||||||
},
|
},
|
||||||
};
|
transform: {
|
||||||
|
'^.+\\.[jt]s$': 'babel-jest',
|
||||||
|
'^.+\\.vue$': '@vue/vue3-jest',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
const fs = require('fs')
|
const fs = require('fs');
|
||||||
const path = require('path')
|
const path = require('path');
|
||||||
const src = __dirname + "/src/extension/"
|
const src = __dirname + '/src/extension/';
|
||||||
const dst = __dirname + "/dist"
|
const dst = __dirname + '/dist';
|
||||||
fs.readdirSync(src).forEach(file => {
|
fs.readdirSync(src).forEach((file) => {
|
||||||
let srcPath = path.join(src, file)
|
let srcPath = path.join(src, file);
|
||||||
let dstPath = path.join(dst, file)
|
let dstPath = path.join(dst, file);
|
||||||
fs.copyFileSync(srcPath, dstPath)
|
fs.copyFileSync(srcPath, dstPath);
|
||||||
console.log(`Copy: ${srcPath} => ${dstPath}`)
|
console.log(`Copy: ${srcPath} => ${dstPath}`);
|
||||||
})
|
});
|
||||||
|
|
||||||
const manifestRaw = fs.readFileSync(__dirname + "/extension-manifest.json", "utf-8")
|
const manifestRaw = fs.readFileSync(__dirname + '/extension-manifest.json', 'utf-8');
|
||||||
const manifest = JSON.parse(manifestRaw)
|
const manifest = JSON.parse(manifestRaw);
|
||||||
|
|
||||||
const pkgRaw = fs.readFileSync(__dirname + "/package.json", "utf-8")
|
const pkgRaw = fs.readFileSync(__dirname + '/package.json', 'utf-8');
|
||||||
const pkg = JSON.parse(pkgRaw)
|
const pkg = JSON.parse(pkgRaw);
|
||||||
|
|
||||||
verExt = pkg["version"]
|
verExt = pkg['version'];
|
||||||
if (verExt.startsWith("v")) verExt = verExt.slice(1)
|
if (verExt.startsWith('v')) verExt = verExt.slice(1);
|
||||||
if (verExt.includes("-")) verExt = verExt.split("-")[0]
|
if (verExt.includes('-')) verExt = verExt.split('-')[0];
|
||||||
manifest["version"] = `${verExt}.${pkg["ext_build"]}`
|
manifest['version'] = `${verExt}.${pkg['ext_build']}`;
|
||||||
manifest["version_name"] = pkg["version"]
|
manifest['version_name'] = pkg['version'];
|
||||||
|
|
||||||
fs.writeFileSync(__dirname + "/dist/manifest.json", JSON.stringify(manifest), "utf-8")
|
fs.writeFileSync(__dirname + '/dist/manifest.json', JSON.stringify(manifest), 'utf-8');
|
||||||
console.log("Write: manifest.json")
|
console.log('Write: manifest.json');
|
||||||
|
|
53
package.json
|
@ -12,8 +12,10 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vite",
|
||||||
"build": "vue-cli-service build",
|
"start": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"build:wasm": "bash ./scripts/build-wasm.sh",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"pretty": "prettier --write src/{**/*,*}.{js,ts,jsx,tsx,vue}",
|
"pretty": "prettier --write src/{**/*,*}.{js,ts,jsx,tsx,vue}",
|
||||||
"pretty:check": "prettier --check src/{**/*,*}.{js,ts,jsx,tsx,vue}",
|
"pretty:check": "prettier --check src/{**/*,*}.{js,ts,jsx,tsx,vue}",
|
||||||
|
@ -21,39 +23,54 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/preset-typescript": "^7.16.5",
|
"@babel/preset-typescript": "^7.16.5",
|
||||||
|
"@element-plus/icons-vue": "^2.0.10",
|
||||||
|
"@element-plus/theme-chalk": "^2.2.16",
|
||||||
"@jixun/kugou-crypto": "^1.0.3",
|
"@jixun/kugou-crypto": "^1.0.3",
|
||||||
"@unlock-music/joox-crypto": "^0.0.1-R5",
|
"@unlock-music/joox-crypto": "^0.0.1-R5",
|
||||||
|
"autoprefixer": "^10.4.13",
|
||||||
"base64-js": "^1.5.1",
|
"base64-js": "^1.5.1",
|
||||||
"browser-id3-writer": "^4.4.0",
|
"browser-id3-writer": "^4.4.0",
|
||||||
"core-js": "^3.16.0",
|
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"element-ui": "^2.15.5",
|
"element-plus": "^2.2.25",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
"jimp": "^0.16.1",
|
|
||||||
"metaflac-js": "^1.0.5",
|
"metaflac-js": "^1.0.5",
|
||||||
"music-metadata": "7.9.0",
|
"music-metadata": "7.9.0",
|
||||||
"music-metadata-browser": "2.2.7",
|
"music-metadata-browser": "2.2.7",
|
||||||
|
"postcss": "^8.4.19",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"threads": "^1.6.5",
|
"threads": "^1.6.5",
|
||||||
"vue": "^2.6.14"
|
"tslib": "^2.4.1",
|
||||||
|
"vue": "^3.2.45"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.20.5",
|
||||||
|
"@esbuild-plugins/node-globals-polyfill": "^0.1.1",
|
||||||
|
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
|
||||||
|
"@rollup/plugin-replace": "^5.0.1",
|
||||||
"@types/crypto-js": "^4.0.2",
|
"@types/crypto-js": "^4.0.2",
|
||||||
"@types/jest": "^27.0.3",
|
"@types/jest": "^29.2.4",
|
||||||
"@vue/cli-plugin-babel": "^4.5.13",
|
"@vitejs/plugin-vue": "^3.2.0",
|
||||||
"@vue/cli-plugin-pwa": "^4.5.13",
|
"@vue/babel-preset-app": "^5.0.8",
|
||||||
"@vue/cli-plugin-typescript": "^4.5.13",
|
"@vue/vue3-jest": "^29.2.1",
|
||||||
"@vue/cli-service": "^4.5.13",
|
"babel-jest": "^29.3.1",
|
||||||
"babel-plugin-component": "^1.1.1",
|
"babel-plugin-component": "^1.1.1",
|
||||||
"jest": "^27.4.5",
|
"buffer": "^6.0.3",
|
||||||
|
"jest": "^29.3.1",
|
||||||
"patch-package": "^6.4.7",
|
"patch-package": "^6.4.7",
|
||||||
"prettier": "2.5.1",
|
"prettier": "^2.5.1",
|
||||||
"sass": "^1.38.1",
|
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||||
"sass-loader": "^10.2.0",
|
"sass": "^1.56.1",
|
||||||
"semver": "^7.3.5",
|
"semver": "^7.3.5",
|
||||||
|
"stream-browserify": "^3.0.0",
|
||||||
"threads-plugin": "^1.4.0",
|
"threads-plugin": "^1.4.0",
|
||||||
"typescript": "^4.5.4",
|
"typescript": "^4.5.4",
|
||||||
"vue-cli-plugin-element": "^1.0.1",
|
"unplugin-auto-import": "^0.12.0",
|
||||||
"vue-template-compiler": "^2.6.14"
|
"unplugin-icons": "^0.14.14",
|
||||||
|
"unplugin-vue-components": "^0.22.11",
|
||||||
|
"util": "^0.12.5",
|
||||||
|
"vite": "^3.2.4",
|
||||||
|
"vite-plugin-commonjs": "^0.5.3",
|
||||||
|
"vite-plugin-pwa": "^0.13.3",
|
||||||
|
"vite-plugin-static-copy": "^0.12.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
autoprefixer: {}
|
autoprefixer: {},
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta content="webkit" name="renderer">
|
|
||||||
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
|
|
||||||
<meta content="width=device-width,initial-scale=1.0" name="viewport">
|
|
||||||
<title>音乐解锁</title>
|
|
||||||
<meta content="音乐,解锁,ncm,qmc,mgg,mflac,qq音乐,网易云音乐,加密" name="keywords"/>
|
|
||||||
<meta content="音乐解锁 - 在任何设备上解锁已购的加密音乐!" name="description"/>
|
|
||||||
<!--@formatter:off-->
|
|
||||||
<style>#loader{position:absolute;left:50%;top:50%;z-index:1010;margin:-75px 0 0 -75px;border:16px solid #f3f3f3;border-radius:50%;border-top:16px solid #1db1ff;width:120px;height:120px;animation:spin 2s linear infinite}@keyframes spin{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}#loader-mask{text-align:center;position:absolute;width:100%;height:100%;bottom:0;left:0;right:0;top:0;z-index:1009;background-color:rgba(242,246,252,.88)}@media (prefers-color-scheme:dark){#loader-mask{color:#fff;background-color:rgba(0,0,0,.85)}#loader-mask a{color:#ddd}#loader-mask a:hover{color:#1db1ff}}#loader-source{font-size:1.5rem}#loader-tips-timeout{font-size:1.2rem}</style>
|
|
||||||
<!--@formatter:on-->
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div id="loader-mask">
|
|
||||||
<div id="loader"></div>
|
|
||||||
<noscript>
|
|
||||||
<h3 id="loader-js">请启用JavaScript</h3>
|
|
||||||
</noscript>
|
|
||||||
<h3 id="loader-source"> 请勿直接运行源代码! </h3>
|
|
||||||
<div id="loader-tips-outdated" hidden>
|
|
||||||
<h2>您可能在使用不受支持的<span style="color:#f00;">过时</span>浏览器,这可能导致此应用无法正常工作。</h2>
|
|
||||||
<h3>如果您使用双核浏览器,您可以尝试切换到 <span style="color:#f00;">“极速模式”</span> 解决此问题。</h3>
|
|
||||||
<h3>或者,您可以尝试更换下方的几个浏览器之一。</h3>
|
|
||||||
</div>
|
|
||||||
<h3 id="loader-tips-timeout" hidden>
|
|
||||||
音乐解锁采用了一些新特性!建议使用
|
|
||||||
<a href="https://www.microsoft.com/zh-cn/edge" target="_blank">Microsoft Edge Chromium</a>
|
|
||||||
<a href="https://www.google.cn/chrome/" target="_blank">Google Chrome</a>
|
|
||||||
<a href="https://www.firefox.com.cn/" target="_blank">Mozilla Firefox</a>
|
|
||||||
| <a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script src="./loader.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,25 +0,0 @@
|
||||||
(function () {
|
|
||||||
setTimeout(function () {
|
|
||||||
var ele = document.getElementById("loader-tips-timeout");
|
|
||||||
if (ele != null) {
|
|
||||||
ele.hidden = false;
|
|
||||||
}
|
|
||||||
}, 2000);
|
|
||||||
|
|
||||||
var ua = navigator && navigator.userAgent;
|
|
||||||
var detected = (function () {
|
|
||||||
var m;
|
|
||||||
if (!ua) return true;
|
|
||||||
if (/MSIE |Trident\//.exec(ua)) return true; // no IE
|
|
||||||
m = /Edge\/([\d.]+)/.exec(ua); // Edge >= 17
|
|
||||||
if (m && Number(m[1]) < 17) return true;
|
|
||||||
m = /Chrome\/([\d.]+)/.exec(ua); // Chrome >= 58
|
|
||||||
if (m && Number(m[1]) < 58) return true;
|
|
||||||
m = /Firefox\/([\d.]+)/.exec(ua); // Firefox >= 45
|
|
||||||
return m && Number(m[1]) < 45;
|
|
||||||
})();
|
|
||||||
if (detected) {
|
|
||||||
document.getElementById('loader-tips-outdated').hidden = false;
|
|
||||||
document.getElementById("loader-tips-timeout").hidden = false;
|
|
||||||
}
|
|
||||||
})();
|
|
33
src/App.vue
|
@ -4,34 +4,38 @@
|
||||||
<Home />
|
<Home />
|
||||||
</el-main>
|
</el-main>
|
||||||
<el-footer id="app-footer">
|
<el-footer id="app-footer">
|
||||||
<el-row>
|
<p>
|
||||||
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>({{ version }})
|
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>({{
|
||||||
:移除已购音乐的加密保护。
|
version
|
||||||
|
}}):移除已购音乐的加密保护。
|
||||||
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
|
<a href="https://github.com/ix64/unlock-music/wiki/使用提示" target="_blank">使用提示</a>
|
||||||
</el-row>
|
</p>
|
||||||
<el-row>
|
<p>
|
||||||
目前支持 网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
|
目前支持 网易云音乐(ncm), QQ音乐(qmc, mflac, mgg), 酷狗音乐(kgm), 虾米音乐(xm), 酷我音乐(.kwm)
|
||||||
<a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>。
|
<a href="https://github.com/ix64/unlock-music/blob/master/README.md" target="_blank">更多</a>。
|
||||||
</el-row>
|
</p>
|
||||||
<el-row>
|
<p>
|
||||||
<!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上-->
|
<!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上-->
|
||||||
<span>Copyright © 2019 - {{ new Date().getFullYear() }} MengYX</span>
|
<span>Copyright © 2019 - {{ new Date().getFullYear() }} MengYX</span>
|
||||||
音乐解锁使用
|
音乐解锁使用
|
||||||
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
|
<a href="https://github.com/ix64/unlock-music/blob/master/LICENSE" target="_blank">MIT许可协议</a>
|
||||||
开放源代码
|
开放源代码
|
||||||
</el-row>
|
</p>
|
||||||
</el-footer>
|
</el-footer>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import './scss/unlock-music.scss';
|
||||||
|
import { ElNotification } from 'element-plus';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
import FileSelector from '@/component/FileSelector';
|
import FileSelector from '@/component/FileSelector';
|
||||||
import PreviewTable from '@/component/PreviewTable';
|
import PreviewTable from '@/component/PreviewTable';
|
||||||
import config from '@/../package.json';
|
import config from '@/../package.json';
|
||||||
import Home from '@/view/Home';
|
import Home from '@/view/Home';
|
||||||
import { checkUpdate } from '@/utils/api';
|
import { checkUpdate } from '@/utils/api';
|
||||||
|
|
||||||
export default {
|
export default defineComponent({
|
||||||
name: 'app',
|
name: 'app',
|
||||||
components: {
|
components: {
|
||||||
FileSelector,
|
FileSelector,
|
||||||
|
@ -49,7 +53,7 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
async finishLoad() {
|
async finishLoad() {
|
||||||
const mask = document.getElementById('loader-mask');
|
const mask = document.getElementById('loader-mask');
|
||||||
if (!!mask) mask.remove();
|
if (mask) mask.remove();
|
||||||
let updateInfo;
|
let updateInfo;
|
||||||
try {
|
try {
|
||||||
updateInfo = await checkUpdate(this.version);
|
updateInfo = await checkUpdate(this.version);
|
||||||
|
@ -69,7 +73,8 @@ export default {
|
||||||
position: 'top-left',
|
position: 'top-left',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$notify.info({
|
ElNotification({
|
||||||
|
type: 'info',
|
||||||
title: '离线使用',
|
title: '离线使用',
|
||||||
message: `<div>
|
message: `<div>
|
||||||
<p>我们使用 PWA 技术,无网络也能使用</p>
|
<p>我们使用 PWA 技术,无网络也能使用</p>
|
||||||
|
@ -86,9 +91,5 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
@import 'scss/unlock-music';
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
interface KgmWasmBundle {
|
||||||
|
(): Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export = KgmWasmBundle;
|
|
@ -0,0 +1,5 @@
|
||||||
|
interface QmcWasmBundle {
|
||||||
|
(): Promise<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export = QmcWasmBundle;
|
|
@ -1,2 +1,2 @@
|
||||||
// Polyfill for node.
|
// Polyfill for node.
|
||||||
global.Blob = global.Blob || require("node:buffer").Blob;
|
global.Blob = global.Blob || require('node:buffer').Blob;
|
||||||
|
|
|
@ -26,7 +26,7 @@ form >>> input {
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog @close="cancel()" title="解密设定" :visible="show" custom-class="um-config-dialog" center>
|
<el-dialog @close="cancel()" title="解密设定" v-model="internalShow" class="um-config-dialog" center>
|
||||||
<el-form ref="form" :rules="rules" status-icon :model="form" label-width="0">
|
<el-form ref="form" :rules="rules" status-icon :model="form" label-width="0">
|
||||||
<section>
|
<section>
|
||||||
<label>
|
<label>
|
||||||
|
@ -49,9 +49,11 @@ form >>> input {
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</el-form>
|
</el-form>
|
||||||
<span slot="footer" class="dialog-footer">
|
<template #footer>
|
||||||
<el-button type="primary" :loading="saving" @click="emitConfirm()">确 定</el-button>
|
<span class="dialog-footer">
|
||||||
</span>
|
<el-button type="primary" :loading="saving" @click="emitConfirm()"> 确 定 </el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -86,9 +88,15 @@ export default {
|
||||||
form: {
|
form: {
|
||||||
jooxUUID: '',
|
jooxUUID: '',
|
||||||
},
|
},
|
||||||
|
internalShow: false,
|
||||||
centerDialogVisible: false,
|
centerDialogVisible: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
show(newValue, oldValue) {
|
||||||
|
this.internalShow = newValue;
|
||||||
|
},
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.resetForm();
|
await this.resetForm();
|
||||||
},
|
},
|
||||||
|
|
|
@ -26,58 +26,67 @@ form >>> input {
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog @close="cancel()" title="音乐标签编辑" :visible="show" custom-class="um-edit-dialog" center>
|
<el-dialog @close="cancel()" title="音乐标签编辑" v-model="internalShow" class="um-edit-dialog" center>
|
||||||
<el-form ref="form" status-icon :model="form" label-width="0">
|
<el-form ref="form" status-icon :model="form" label-width="0">
|
||||||
<section>
|
<section>
|
||||||
<el-image v-show="!editPicture" :src="imgFile.url || picture" style="width: 100px; height: 100px">
|
<el-image v-show="!editPicture" :src="imgFile.url || picture" style="width: 100px; height: 100px">
|
||||||
<div slot="error" class="image-slot el-image__error">暂无封面</div>
|
<template #error class="image-slot el-image__error"> 暂无封面 </template>
|
||||||
</el-image>
|
</el-image>
|
||||||
<el-upload v-show="editPicture" :auto-upload="false" :on-change="addFile" :on-remove="rmvFile" :show-file-list="true" :limit="1" list-type="picture" action="" drag>
|
<el-upload
|
||||||
<i class="el-icon-upload" />
|
v-show="editPicture"
|
||||||
|
:auto-upload="false"
|
||||||
|
:on-change="addFile"
|
||||||
|
:on-remove="rmvFile"
|
||||||
|
:show-file-list="true"
|
||||||
|
:limit="1"
|
||||||
|
list-type="picture"
|
||||||
|
action=""
|
||||||
|
drag
|
||||||
|
>
|
||||||
|
<el-icon><UploadFilled /></el-icon>
|
||||||
<div class="el-upload__text">将新图片拖到此处,或<em>点击选择</em><br />以替换自动匹配的图片</div>
|
<div class="el-upload__text">将新图片拖到此处,或<em>点击选择</em><br />以替换自动匹配的图片</div>
|
||||||
<div slot="tip" class="el-upload__tip">
|
<template #tip class="el-upload__tip"> 新拖到此处的图片将覆盖原始图片 </template>
|
||||||
新拖到此处的图片将覆盖原始图片
|
</el-upload>
|
||||||
</div>
|
|
||||||
</el-upload>
|
|
||||||
<i
|
<i
|
||||||
:class="{'el-icon-edit': !editPicture, 'el-icon-check': editPicture}"
|
:class="{
|
||||||
|
'el-icon-edit': !editPicture,
|
||||||
|
'el-icon-check': editPicture,
|
||||||
|
}"
|
||||||
@click="changeCover"
|
@click="changeCover"
|
||||||
></i><br />
|
></i>
|
||||||
标题:
|
<br />
|
||||||
<span v-show="!editTitle">{{title}}</span>
|
标题:
|
||||||
<el-input v-show="editTitle" v-model="title"></el-input>
|
<span v-show="!editTitle">{{ title }}</span>
|
||||||
|
<!-- <el-input v-show="editTitle" v-model="title"></el-input> -->
|
||||||
|
<i :class="{ 'el-icon-edit': !editTitle, 'el-icon-check': editTitle }" @click="editTitle = !editTitle"></i
|
||||||
|
><br />
|
||||||
|
艺术家:
|
||||||
|
<span v-show="!editArtist">{{ artist }}</span>
|
||||||
|
<!-- <el-input v-show="editArtist" v-model="artist"></el-input> -->
|
||||||
|
<i :class="{ 'el-icon-edit': !editArtist, 'el-icon-check': editArtist }" @click="editArtist = !editArtist"></i
|
||||||
|
><br />
|
||||||
|
专辑:
|
||||||
|
<span v-show="!editAlbum">{{ album }}</span>
|
||||||
|
<!-- <el-input v-show="editAlbum" v-model="album"></el-input> -->
|
||||||
|
<i :class="{ 'el-icon-edit': !editAlbum, 'el-icon-check': editAlbum }" @click="editAlbum = !editAlbum"></i
|
||||||
|
><br />
|
||||||
|
专辑艺术家:
|
||||||
|
<span v-show="!editAlbumartist">{{ albumartist }}</span>
|
||||||
|
<!-- <el-input v-show="editAlbumartist" v-model="albumartist"></el-input> -->
|
||||||
<i
|
<i
|
||||||
:class="{'el-icon-edit': !editTitle, 'el-icon-check': editTitle}"
|
:class="{
|
||||||
@click="editTitle = !editTitle"
|
'el-icon-edit': !editAlbumartist,
|
||||||
></i><br />
|
'el-icon-check': editAlbumartist,
|
||||||
艺术家:
|
}"
|
||||||
<span v-show="!editArtist">{{artist}}</span>
|
|
||||||
<el-input v-show="editArtist" v-model="artist"></el-input>
|
|
||||||
<i
|
|
||||||
:class="{'el-icon-edit': !editArtist, 'el-icon-check': editArtist}"
|
|
||||||
@click="editArtist = !editArtist"
|
|
||||||
></i><br />
|
|
||||||
专辑:
|
|
||||||
<span v-show="!editAlbum">{{album}}</span>
|
|
||||||
<el-input v-show="editAlbum" v-model="album"></el-input>
|
|
||||||
<i
|
|
||||||
:class="{'el-icon-edit': !editAlbum, 'el-icon-check': editAlbum}"
|
|
||||||
@click="editAlbum = !editAlbum"
|
|
||||||
></i><br />
|
|
||||||
专辑艺术家:
|
|
||||||
<span v-show="!editAlbumartist">{{albumartist}}</span>
|
|
||||||
<el-input v-show="editAlbumartist" v-model="albumartist"></el-input>
|
|
||||||
<i
|
|
||||||
:class="{'el-icon-edit': !editAlbumartist, 'el-icon-check': editAlbumartist}"
|
|
||||||
@click="editAlbumartist = !editAlbumartist"
|
@click="editAlbumartist = !editAlbumartist"
|
||||||
></i><br />
|
></i
|
||||||
风格:
|
><br />
|
||||||
<span v-show="!editGenre">{{genre}}</span>
|
风格:
|
||||||
<el-input v-show="editGenre" v-model="genre"></el-input>
|
<span v-show="!editGenre">{{ genre }}</span>
|
||||||
<i
|
<!-- <el-input v-show="editGenre" v-model="genre"></el-input> -->
|
||||||
:class="{'el-icon-edit': !editGenre, 'el-icon-check': editGenre}"
|
<i :class="{ 'el-icon-edit': !editGenre, 'el-icon-check': editGenre }" @click="editGenre = !editGenre"></i
|
||||||
@click="editGenre = !editGenre"
|
><br />
|
||||||
></i><br />
|
|
||||||
|
|
||||||
<p class="item-desc">
|
<p class="item-desc">
|
||||||
为了节省您设备的资源,请在确定前充分检查,避免反复修改。<br />
|
为了节省您设备的资源,请在确定前充分检查,避免反复修改。<br />
|
||||||
|
@ -85,9 +94,11 @@ form >>> input {
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
</el-form>
|
</el-form>
|
||||||
<span slot="footer" class="dialog-footer">
|
<template #footer>
|
||||||
<el-button type="primary" @click="emitConfirm()">确 定</el-button>
|
<span class="dialog-footer">
|
||||||
</span>
|
<el-button type="primary" @click="emitConfirm()">确 定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -100,17 +111,17 @@ export default {
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
show: { type: Boolean, required: true },
|
show: { type: Boolean, required: true },
|
||||||
picture: { type: String | undefined, required: true },
|
picture: { type: String, required: true },
|
||||||
title: { type: String | undefined, required: true },
|
title: { type: String, required: true },
|
||||||
artist: { type: String | undefined, required: true },
|
artist: { type: String, required: true },
|
||||||
album: { type: String | undefined, required: true },
|
album: { type: String, required: true },
|
||||||
albumartist: { type: String | undefined, required: true },
|
albumartist: { type: String, required: true },
|
||||||
genre: { type: String | undefined, required: true },
|
genre: { type: String, required: true },
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
internalShow: false,
|
||||||
},
|
form: {},
|
||||||
imgFile: { tmpblob: undefined, blob: undefined, url: undefined },
|
imgFile: { tmpblob: undefined, blob: undefined, url: undefined },
|
||||||
editPicture: false,
|
editPicture: false,
|
||||||
editTitle: false,
|
editTitle: false,
|
||||||
|
@ -120,6 +131,11 @@ export default {
|
||||||
editGenre: false,
|
editGenre: false,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
show(newValue, oldValue) {
|
||||||
|
this.internalShow = newValue;
|
||||||
|
},
|
||||||
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.refreshForm();
|
this.refreshForm();
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,40 +1,53 @@
|
||||||
<template>
|
<template>
|
||||||
<el-upload :auto-upload="false" :on-change="addFile" :show-file-list="false" action="" drag multiple>
|
<div class="decrypt-file-selector">
|
||||||
<i class="el-icon-upload" />
|
<el-upload :auto-upload="false" :on-change="addFile" :show-file-list="false" action="" drag multiple>
|
||||||
<div class="el-upload__text">将文件拖到此处,或 <em>点击选择</em></div>
|
<el-icon size="80"><UploadFilled /></el-icon>
|
||||||
<div slot="tip" class="el-upload__tip">
|
<div>将文件拖到此处,或 <em>点击选择</em></div>
|
||||||
<div>
|
<template #tip> </template>
|
||||||
仅在浏览器内对文件进行解锁,无需消耗流量
|
<transition name="el-fade-in">
|
||||||
<el-tooltip effect="dark" placement="top-start">
|
<!--todo: add delay to animation-->
|
||||||
<div slot="content">算法在源代码中已经提供,所有运算都发生在本地</div>
|
<el-progress
|
||||||
<i class="el-icon-info" style="font-size: 12px" />
|
v-show="progress_show"
|
||||||
</el-tooltip>
|
:format="progress_string"
|
||||||
</div>
|
:percentage="progress_value"
|
||||||
<div>
|
:stroke-width="16"
|
||||||
工作模式: {{ parallel ? '多线程 Worker' : '单线程 Queue' }}
|
:text-inside="true"
|
||||||
<el-tooltip effect="dark" placement="top-start">
|
style="margin: 16px 6px 0 6px"
|
||||||
<div slot="content">
|
></el-progress>
|
||||||
将此工具部署在HTTPS环境下,可以启用Web Worker特性,<br />
|
</transition>
|
||||||
从而更快的利用并行处理完成解锁
|
</el-upload>
|
||||||
</div>
|
</div>
|
||||||
<i class="el-icon-info" style="font-size: 12px" />
|
<div>
|
||||||
</el-tooltip>
|
仅在浏览器内对文件进行解锁,无需消耗流量
|
||||||
</div>
|
<el-tooltip effect="dark" placement="top-start">
|
||||||
</div>
|
<template #content> 算法在源代码中已经提供,所有运算都发生在本地 </template>
|
||||||
<transition name="el-fade-in"
|
<el-icon size="12">
|
||||||
><!--todo: add delay to animation-->
|
<InfoFilled />
|
||||||
<el-progress
|
</el-icon>
|
||||||
v-show="progress_show"
|
</el-tooltip>
|
||||||
:format="progress_string"
|
</div>
|
||||||
:percentage="progress_value"
|
<div>
|
||||||
:stroke-width="16"
|
工作模式: {{ parallel ? '多线程任务' : '单线程队列' }}
|
||||||
:text-inside="true"
|
<el-tooltip effect="dark" placement="top-start">
|
||||||
style="margin: 16px 6px 0 6px"
|
<template #content>
|
||||||
></el-progress>
|
将此工具部署在 HTTPS 环境下,可以利用 Web Worker 的多线程特性,<br />
|
||||||
</transition>
|
从而更快的利用并行处理完成解锁
|
||||||
</el-upload>
|
</template>
|
||||||
|
<el-icon size="12">
|
||||||
|
<InfoFilled />
|
||||||
|
</el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.decrypt-file-selector {
|
||||||
|
max-width: 360px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { spawn, Worker, Pool } from 'threads';
|
import { spawn, Worker, Pool } from 'threads';
|
||||||
import { Decrypt } from '@/decrypt';
|
import { Decrypt } from '@/decrypt';
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<el-table :data="tableData" style="width: 100%">
|
<el-table :data="tableData" style="width: 100%">
|
||||||
<el-table-column label="封面">
|
<el-table-column label="封面">
|
||||||
<template slot-scope="scope">
|
<template #default="scope">
|
||||||
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
|
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
|
||||||
<div slot="error" class="image-slot el-image__error">暂无封面</div>
|
<template #error class="image-slot el-image__error"> 暂无封面 </template>
|
||||||
</el-image>
|
</el-image>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
@ -24,11 +24,23 @@
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作">
|
<el-table-column label="操作">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button circle icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)">
|
<el-button circle type="success" @click="handlePlay(scope.$index, scope.row)">
|
||||||
|
<el-icon size="20" style="vertical-align: middle">
|
||||||
|
<VideoPlay />
|
||||||
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button circle icon="el-icon-download" @click="handleDownload(scope.row)"></el-button>
|
<el-button circle @click="handleDownload(scope.row)">
|
||||||
<el-button circle icon="el-icon-edit" @click="handleEdit(scope.row)"></el-button>
|
<el-icon size="20" style="vertical-align: middle">
|
||||||
<el-button circle icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
|
<Download />
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button circle @click="handleEdit(scope.row)">
|
||||||
|
<el-icon size="20" style="vertical-align: middle"><Edit /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button circle type="danger" @click="handleDelete(scope.$index, scope.row)">
|
||||||
|
<el-icon size="20" style="vertical-align: middle">
|
||||||
|
<Delete />
|
||||||
|
</el-icon>
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
|
@ -71,7 +71,6 @@ export async function Decrypt(file: FileInfo, config: Record<string, any>): Prom
|
||||||
case 'mggl': //QQ Music Mac
|
case 'mggl': //QQ Music Mac
|
||||||
case 'mflac': //QQ Music New Flac
|
case 'mflac': //QQ Music New Flac
|
||||||
case 'mflac0': //QQ Music New Flac
|
case 'mflac0': //QQ Music New Flac
|
||||||
case 'mflach': //QQ Music New Flac
|
|
||||||
case 'mgg': //QQ Music New Ogg
|
case 'mgg': //QQ Music New Ogg
|
||||||
case 'mgg1': //QQ Music New Ogg
|
case 'mgg1': //QQ Music New Ogg
|
||||||
case 'mgg0':
|
case 'mgg0':
|
||||||
|
@ -102,8 +101,8 @@ export async function Decrypt(file: FileInfo, config: Record<string, any>): Prom
|
||||||
case 'x3m':
|
case 'x3m':
|
||||||
rt_data = await XimalayaDecrypt(file.raw, raw.name, raw.ext);
|
rt_data = await XimalayaDecrypt(file.raw, raw.name, raw.ext);
|
||||||
break;
|
break;
|
||||||
case 'mflach': //QQ Music New Flac
|
case 'mflach': //QQ Music (Mac)
|
||||||
throw '网页版无法解锁,请使用<a target="_blank" href="https://git.unlock-music.dev/um/cli">CLI版本</a>'
|
throw '网页版无法解锁,请使用<a target="_blank" href="https://git.unlock-music.dev/um/cli">CLI版本</a>';
|
||||||
default:
|
default:
|
||||||
throw '不支持此文件格式';
|
throw '不支持此文件格式';
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,11 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
||||||
const mime = AudioMimeType[ext];
|
const mime = AudioMimeType[ext];
|
||||||
let musicBlob = new Blob([musicDecoded], { type: mime });
|
let musicBlob = new Blob([musicDecoded], { type: mime });
|
||||||
const musicMeta = await metaParseBlob(musicBlob);
|
const musicMeta = await metaParseBlob(musicBlob);
|
||||||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || ""));
|
const { title, artist } = GetMetaFromFile(
|
||||||
|
raw_filename,
|
||||||
|
musicMeta.common.title,
|
||||||
|
String(musicMeta.common.artists || musicMeta.common.artist || ''),
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
album: musicMeta.common.album,
|
album: musicMeta.common.album,
|
||||||
picture: GetCoverFromFile(musicMeta),
|
picture: GetCoverFromFile(musicMeta),
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import KgmCryptoModule from '@/KgmWasm/KgmWasmBundle';
|
import * as KgmCryptoModule from '@/KgmWasm/KgmWasmBundle';
|
||||||
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
||||||
|
|
||||||
// 每次处理 2M 的数据
|
// 每次处理 2M 的数据
|
||||||
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
|
const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
|
||||||
|
|
||||||
export interface KGMDecryptionResult {
|
export interface KGMDecryptionResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
@ -17,7 +17,11 @@ export interface KGMDecryptionResult {
|
||||||
* @param {ArrayBuffer} kgmBlob 读入的文件 Blob
|
* @param {ArrayBuffer} kgmBlob 读入的文件 Blob
|
||||||
*/
|
*/
|
||||||
export async function DecryptKgmWasm(kgmBlob: ArrayBuffer, ext: string): Promise<KGMDecryptionResult> {
|
export async function DecryptKgmWasm(kgmBlob: ArrayBuffer, ext: string): Promise<KGMDecryptionResult> {
|
||||||
const result: KGMDecryptionResult = { success: false, data: new Uint8Array(), error: '' };
|
const result: KGMDecryptionResult = {
|
||||||
|
success: false,
|
||||||
|
data: new Uint8Array(),
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化模组
|
// 初始化模组
|
||||||
let KgmCrypto: any;
|
let KgmCrypto: any;
|
||||||
|
|
|
@ -42,7 +42,11 @@ export async function Decrypt(file: File, raw_filename: string, _: string): Prom
|
||||||
let musicBlob = new Blob([audioData], { type: mime });
|
let musicBlob = new Blob([audioData], { type: mime });
|
||||||
|
|
||||||
const musicMeta = await metaParseBlob(musicBlob);
|
const musicMeta = await metaParseBlob(musicBlob);
|
||||||
const { title, artist } = GetMetaFromFile(raw_filename, musicMeta.common.title, String(musicMeta.common.artists || musicMeta.common.artist || ""));
|
const { title, artist } = GetMetaFromFile(
|
||||||
|
raw_filename,
|
||||||
|
musicMeta.common.title,
|
||||||
|
String(musicMeta.common.artists || musicMeta.common.artist || ''),
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
album: musicMeta.common.album,
|
album: musicMeta.common.album,
|
||||||
picture: GetCoverFromFile(musicMeta),
|
picture: GetCoverFromFile(musicMeta),
|
||||||
|
|
|
@ -5,24 +5,24 @@ import { DecryptResult } from '@/decrypt/entity';
|
||||||
const segmentSize = 0x20;
|
const segmentSize = 0x20;
|
||||||
|
|
||||||
function isPrintableAsciiChar(ch: number) {
|
function isPrintableAsciiChar(ch: number) {
|
||||||
return ch >= 0x20 && ch <= 0x7E;
|
return ch >= 0x20 && ch <= 0x7e;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUpperHexChar(ch: number) {
|
function isUpperHexChar(ch: number) {
|
||||||
return (ch >= 0x30 && ch <= 0x39) || (ch >= 0x41 && ch <= 0x46);
|
return (ch >= 0x30 && ch <= 0x39) || (ch >= 0x41 && ch <= 0x46);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Buffer} data
|
* @param {Buffer} data
|
||||||
* @param {Buffer} key
|
* @param {Buffer} key
|
||||||
* @param {boolean} copy
|
* @param {boolean} copy
|
||||||
* @returns Buffer
|
* @returns Buffer
|
||||||
*/
|
*/
|
||||||
function decryptSegment(data: Uint8Array, key: Uint8Array) {
|
function decryptSegment(data: Uint8Array, key: Uint8Array) {
|
||||||
for (let i = 0; i < data.byteLength; i++) {
|
for (let i = 0; i < data.byteLength; i++) {
|
||||||
data[i] -= key[i % segmentSize];
|
data[i] -= key[i % segmentSize];
|
||||||
}
|
}
|
||||||
return Buffer.from(data);
|
return Buffer.from(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> {
|
export async function Decrypt(file: File, raw_filename: string): Promise<DecryptResult> {
|
||||||
|
@ -33,37 +33,37 @@ export async function Decrypt(file: File, raw_filename: string): Promise<Decrypt
|
||||||
const bytesRIFF = Buffer.from('RIFF', 'ascii');
|
const bytesRIFF = Buffer.from('RIFF', 'ascii');
|
||||||
const bytesWaveFormat = Buffer.from('WAVEfmt ', 'ascii');
|
const bytesWaveFormat = Buffer.from('WAVEfmt ', 'ascii');
|
||||||
const possibleKeys = [];
|
const possibleKeys = [];
|
||||||
|
|
||||||
for (let i = segmentSize; i < segmentSize * 20; i += segmentSize) {
|
for (let i = segmentSize; i < segmentSize * 20; i += segmentSize) {
|
||||||
const possibleKey = buf.slice(i, i + segmentSize);
|
const possibleKey = buf.slice(i, i + segmentSize);
|
||||||
if (!possibleKey.every(isUpperHexChar)) continue;
|
if (!possibleKey.every(isUpperHexChar)) continue;
|
||||||
|
|
||||||
const tempHeader = decryptSegment(header, possibleKey);
|
const tempHeader = decryptSegment(header, possibleKey);
|
||||||
if (tempHeader.slice(0, 4).compare(bytesRIFF)) continue;
|
if (tempHeader.slice(0, 4).compare(bytesRIFF)) continue;
|
||||||
if (tempHeader.slice(8, 16).compare(bytesWaveFormat)) continue;
|
if (tempHeader.slice(8, 16).compare(bytesWaveFormat)) continue;
|
||||||
|
|
||||||
// fmt chunk 大小可以是 16 / 18 / 40。
|
// fmt chunk 大小可以是 16 / 18 / 40。
|
||||||
const fmtChunkSize = tempHeader.readUInt32LE(0x10);
|
const fmtChunkSize = tempHeader.readUInt32LE(0x10);
|
||||||
if (![16, 18, 40].includes(fmtChunkSize)) continue;
|
if (![16, 18, 40].includes(fmtChunkSize)) continue;
|
||||||
|
|
||||||
// 下一个 chunk
|
// 下一个 chunk
|
||||||
const firstDataChunkOffset = 0x14 + fmtChunkSize;
|
const firstDataChunkOffset = 0x14 + fmtChunkSize;
|
||||||
const chunkName = tempHeader.slice(firstDataChunkOffset, firstDataChunkOffset + 4);
|
const chunkName = tempHeader.slice(firstDataChunkOffset, firstDataChunkOffset + 4);
|
||||||
if (!chunkName.every(isPrintableAsciiChar)) continue;
|
if (!chunkName.every(isPrintableAsciiChar)) continue;
|
||||||
|
|
||||||
const secondDataChunkOffset = firstDataChunkOffset + 8 + tempHeader.readUInt32LE(firstDataChunkOffset + 4);
|
const secondDataChunkOffset = firstDataChunkOffset + 8 + tempHeader.readUInt32LE(firstDataChunkOffset + 4);
|
||||||
if (secondDataChunkOffset <= header.byteLength) {
|
if (secondDataChunkOffset <= header.byteLength) {
|
||||||
const secondChunkName = tempHeader.slice(secondDataChunkOffset, secondDataChunkOffset + 4);
|
const secondChunkName = tempHeader.slice(secondDataChunkOffset, secondDataChunkOffset + 4);
|
||||||
if (!secondChunkName.every(isPrintableAsciiChar)) continue;
|
if (!secondChunkName.every(isPrintableAsciiChar)) continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
possibleKeys.push(Buffer.from(possibleKey).toString('ascii'));
|
possibleKeys.push(Buffer.from(possibleKey).toString('ascii'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (possibleKeys.length <= 0) {
|
if (possibleKeys.length <= 0) {
|
||||||
throw new Error(`ERROR: no suitable key discovered`);
|
throw new Error(`ERROR: no suitable key discovered`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptionKey = Buffer.from(possibleKeys[0], 'ascii');
|
const decryptionKey = Buffer.from(possibleKeys[0], 'ascii');
|
||||||
decryptSegment(buf, decryptionKey);
|
decryptSegment(buf, decryptionKey);
|
||||||
const musicData = new Blob([buf], { type: 'audio/x-wav' });
|
const musicData = new Blob([buf], { type: 'audio/x-wav' });
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
WriteMetaToMp3,
|
WriteMetaToMp3,
|
||||||
} from '@/decrypt/utils';
|
} from '@/decrypt/utils';
|
||||||
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
import { parseBlob as metaParseBlob } from 'music-metadata-browser';
|
||||||
import jimp from 'jimp';
|
// import jimp from 'jimp';
|
||||||
|
|
||||||
import AES from 'crypto-js/aes';
|
import AES from 'crypto-js/aes';
|
||||||
import PKCS7 from 'crypto-js/pad-pkcs7';
|
import PKCS7 from 'crypto-js/pad-pkcs7';
|
||||||
|
@ -175,15 +175,20 @@ class NcmDecrypt {
|
||||||
try {
|
try {
|
||||||
this.image = await GetImageFromURL(this.oriMeta.albumPic);
|
this.image = await GetImageFromURL(this.oriMeta.albumPic);
|
||||||
while (this.image && this.image.buffer.byteLength >= 1 << 24) {
|
while (this.image && this.image.buffer.byteLength >= 1 << 24) {
|
||||||
let img = await jimp.read(Buffer.from(this.image.buffer));
|
// let img = await jimp.read(Buffer.from(this.image.buffer));
|
||||||
await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO);
|
// await img.resize(Math.round(img.getHeight() / 2), jimp.AUTO);
|
||||||
this.image.buffer = await img.getBufferAsync('image/jpeg');
|
// this.image.buffer = await img.getBufferAsync('image/jpeg');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('get cover image failed', e);
|
console.log('get cover image failed', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.newMeta = { title: info.title, artists, album: this.oriMeta.album, picture: this.image?.buffer };
|
this.newMeta = {
|
||||||
|
title: info.title,
|
||||||
|
artists,
|
||||||
|
album: this.oriMeta.album,
|
||||||
|
picture: this.image?.buffer,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async _writeMeta() {
|
async _writeMeta() {
|
||||||
|
|
|
@ -13,7 +13,11 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
||||||
const ext = SniffAudioExt(buffer, raw_ext);
|
const ext = SniffAudioExt(buffer, raw_ext);
|
||||||
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
|
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
|
||||||
const tag = await metaParseBlob(file);
|
const tag = await metaParseBlob(file);
|
||||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ""));
|
const { title, artist } = GetMetaFromFile(
|
||||||
|
raw_filename,
|
||||||
|
tag.common.title,
|
||||||
|
String(tag.common.artists || tag.common.artist || ''),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|
|
@ -34,11 +34,14 @@ export function simpleMakeKey(salt: number, length: number): number[] {
|
||||||
return keyBuf;
|
return keyBuf;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mixKey1: Uint8Array = new Uint8Array([ 0x33, 0x38, 0x36, 0x5A, 0x4A, 0x59, 0x21, 0x40, 0x23, 0x2A, 0x24, 0x25, 0x5E, 0x26, 0x29, 0x28 ])
|
const mixKey1: Uint8Array = new Uint8Array([
|
||||||
const mixKey2: Uint8Array = new Uint8Array([ 0x2A, 0x2A, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5E, 0x61, 0x31, 0x63, 0x5A, 0x2C, 0x54 ])
|
0x33, 0x38, 0x36, 0x5a, 0x4a, 0x59, 0x21, 0x40, 0x23, 0x2a, 0x24, 0x25, 0x5e, 0x26, 0x29, 0x28,
|
||||||
|
]);
|
||||||
|
const mixKey2: Uint8Array = new Uint8Array([
|
||||||
|
0x2a, 0x2a, 0x23, 0x21, 0x28, 0x23, 0x24, 0x25, 0x26, 0x5e, 0x61, 0x31, 0x63, 0x5a, 0x2c, 0x54,
|
||||||
|
]);
|
||||||
|
|
||||||
function decryptV2Key(key: Buffer): Buffer
|
function decryptV2Key(key: Buffer): Buffer {
|
||||||
{
|
|
||||||
const textEnc = new TextDecoder();
|
const textEnc = new TextDecoder();
|
||||||
if (key.length < 18 || textEnc.decode(key.slice(0, 18)) !== 'QQMusic EncV2,Key:') {
|
if (key.length < 18 || textEnc.decode(key.slice(0, 18)) !== 'QQMusic EncV2,Key:') {
|
||||||
return key;
|
return key;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import QmcCryptoModule from '@/QmcWasm/QmcWasmBundle';
|
|
||||||
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
import { MergeUint8Array } from '@/utils/MergeUint8Array';
|
||||||
|
import * as QmcCryptoModule from '@/QmcWasm/QmcWasmBundle';
|
||||||
|
|
||||||
// 每次处理 2M 的数据
|
// 每次处理 2M 的数据
|
||||||
const DECRYPTION_BUF_SIZE = 2 *1024 * 1024;
|
const DECRYPTION_BUF_SIZE = 2 * 1024 * 1024;
|
||||||
|
|
||||||
export interface QMCDecryptionResult {
|
export interface QMCDecryptionResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
@ -18,7 +18,12 @@ export interface QMCDecryptionResult {
|
||||||
* @param {ArrayBuffer} qmcBlob 读入的文件 Blob
|
* @param {ArrayBuffer} qmcBlob 读入的文件 Blob
|
||||||
*/
|
*/
|
||||||
export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise<QMCDecryptionResult> {
|
export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise<QMCDecryptionResult> {
|
||||||
const result: QMCDecryptionResult = { success: false, data: new Uint8Array(), songId: 0, error: '' };
|
const result: QMCDecryptionResult = {
|
||||||
|
success: false,
|
||||||
|
data: new Uint8Array(),
|
||||||
|
songId: 0,
|
||||||
|
error: '',
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化模组
|
// 初始化模组
|
||||||
let QmcCrypto: any;
|
let QmcCrypto: any;
|
||||||
|
@ -47,7 +52,7 @@ export async function DecryptQmcWasm(qmcBlob: ArrayBuffer, ext: string): Promise
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
result.songId = QmcCrypto.getSongId();
|
result.songId = QmcCrypto.getSongId();
|
||||||
result.songId = result.songId == "0" ? 0 : result.songId;
|
result.songId = result.songId == '0' ? 0 : result.songId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedParts = [];
|
const decryptedParts = [];
|
||||||
|
|
|
@ -52,7 +52,11 @@ export async function Decrypt(file: Blob, raw_filename: string, raw_ext: string)
|
||||||
throw '不支持的QQ音乐缓存格式';
|
throw '不支持的QQ音乐缓存格式';
|
||||||
}
|
}
|
||||||
const tag = await metaParseBlob(audioBlob);
|
const tag = await metaParseBlob(audioBlob);
|
||||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ""));
|
const { title, artist } = GetMetaFromFile(
|
||||||
|
raw_filename,
|
||||||
|
tag.common.title,
|
||||||
|
String(tag.common.artists || tag.common.artist || ''),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|
|
@ -17,7 +17,11 @@ export async function Decrypt(
|
||||||
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
|
if (ext !== raw_ext) file = new Blob([buffer], { type: AudioMimeType[ext] });
|
||||||
}
|
}
|
||||||
const tag = await metaParseBlob(file);
|
const tag = await metaParseBlob(file);
|
||||||
const { title, artist } = GetMetaFromFile(raw_filename, tag.common.title, String(tag.common.artists || tag.common.artist || ''));
|
const { title, artist } = GetMetaFromFile(
|
||||||
|
raw_filename,
|
||||||
|
tag.common.title,
|
||||||
|
String(tag.common.artists || tag.common.artist || ''),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|
|
@ -94,7 +94,8 @@ export function GetMetaFromFile(
|
||||||
const items = filename.split(separator);
|
const items = filename.split(separator);
|
||||||
if (items.length > 1) {
|
if (items.length > 1) {
|
||||||
//由文件名和原metadata共同决定歌手tag(有时从文件名看有多个歌手,而metadata只有一个)
|
//由文件名和原metadata共同决定歌手tag(有时从文件名看有多个歌手,而metadata只有一个)
|
||||||
if (!meta.artist || meta.artist.split(split_regex).length < items[0].trim().split(split_regex).length) meta.artist = items[0].trim();
|
if (!meta.artist || meta.artist.split(split_regex).length < items[0].trim().split(split_regex).length)
|
||||||
|
meta.artist = items[0].trim();
|
||||||
if (!meta.title) meta.title = items[1].trim();
|
if (!meta.title) meta.title = items[1].trim();
|
||||||
} else if (items.length === 1) {
|
} else if (items.length === 1) {
|
||||||
if (!meta.title) meta.title = items[0].trim();
|
if (!meta.title) meta.title = items[0].trim();
|
||||||
|
@ -182,11 +183,12 @@ export function RewriteMetaToMp3(audioData: Buffer, info: IMusicMeta, original:
|
||||||
// preserve original data
|
// preserve original data
|
||||||
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [];
|
const frames = original.native['ID3v2.4'] || original.native['ID3v2.3'] || original.native['ID3v2.2'] || [];
|
||||||
frames.forEach((frame) => {
|
frames.forEach((frame) => {
|
||||||
if (frame.id !== 'TPE1'
|
if (
|
||||||
&& frame.id !== 'TIT2'
|
frame.id !== 'TPE1' &&
|
||||||
&& frame.id !== 'TALB'
|
frame.id !== 'TIT2' &&
|
||||||
&& frame.id !== 'TPE2'
|
frame.id !== 'TALB' &&
|
||||||
&& frame.id !== 'TCON'
|
frame.id !== 'TPE2' &&
|
||||||
|
frame.id !== 'TCON'
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
writer.setFrame(frame.id, frame.value);
|
writer.setFrame(frame.id, frame.value);
|
||||||
|
|
|
@ -49,7 +49,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
||||||
const { title, artist } = GetMetaFromFile(
|
const { title, artist } = GetMetaFromFile(
|
||||||
raw_filename,
|
raw_filename,
|
||||||
musicMeta.common.title,
|
musicMeta.common.title,
|
||||||
String(musicMeta.common.artists || musicMeta.common.artist || ""),
|
String(musicMeta.common.artists || musicMeta.common.artist || ''),
|
||||||
raw_filename.indexOf('_') === -1 ? '-' : '_',
|
raw_filename.indexOf('_') === -1 ? '-' : '_',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="./popup.js"></script>
|
<script src="./popup.js"></script>
|
||||||
<a href="./index.html" target="_blank">
|
<a href="./index.html" target="_blank">
|
||||||
<button>立即使用</button>
|
<button>立即使用</button>
|
||||||
</a>
|
</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
declare module 'virtual:pwa-register/vue' {
|
||||||
|
// @ts-expect-error ignore when vue is not installed
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
export interface RegisterSWOptions {
|
||||||
|
immediate?: boolean;
|
||||||
|
onNeedRefresh?: () => void;
|
||||||
|
onOfflineReady?: () => void;
|
||||||
|
/**
|
||||||
|
* Called only if `onRegisteredSW` is not provided.
|
||||||
|
*
|
||||||
|
* @deprecated Use `onRegisteredSW` instead.
|
||||||
|
* @param registration The service worker registration if available.
|
||||||
|
*/
|
||||||
|
onRegistered?: (registration: ServiceWorkerRegistration | undefined) => void;
|
||||||
|
/**
|
||||||
|
* Called once the service worker is registered (requires version `0.12.8+`).
|
||||||
|
*
|
||||||
|
* @param swScriptUrl The service worker script url.
|
||||||
|
* @param registration The service worker registration if available.
|
||||||
|
*/
|
||||||
|
onRegisteredSW?: (swScriptUrl: string, registration: ServiceWorkerRegistration | undefined) => void;
|
||||||
|
onRegisterError?: (error: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRegisterSW(options?: RegisterSWOptions): {
|
||||||
|
needRefresh: Ref<boolean>;
|
||||||
|
offlineReady: Ref<boolean>;
|
||||||
|
updateServiceWorker: (reloadPage?: boolean) => Promise<void>;
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
#loader {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
z-index: 1010;
|
||||||
|
margin: -75px 0 0 -75px;
|
||||||
|
border: 16px solid #f3f3f3;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 16px solid #1db1ff;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#loader-mask {
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1009;
|
||||||
|
background-color: rgba(242, 246, 252, 0.88);
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
#loader-mask {
|
||||||
|
color: #fff;
|
||||||
|
background-color: rgba(0, 0, 0, 0.85);
|
||||||
|
}
|
||||||
|
#loader-mask a {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
#loader-mask a:hover {
|
||||||
|
color: #1db1ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#loader-source {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
#loader-tips-timeout {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import './loader.css';
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
setTimeout(function () {
|
||||||
|
var ele = document.getElementById('loader-tips-timeout');
|
||||||
|
if (ele != null) {
|
||||||
|
ele.hidden = false;
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
var ua = navigator && navigator.userAgent;
|
||||||
|
var detected = (function () {
|
||||||
|
var m;
|
||||||
|
if (!ua) return true;
|
||||||
|
if (/MSIE |Trident\//.exec(ua)) return true; // no IE
|
||||||
|
m = /Edge\/([\d.]+)/.exec(ua); // Edge >= 17
|
||||||
|
if (m && Number(m[1]) < 17) return true;
|
||||||
|
m = /Chrome\/([\d.]+)/.exec(ua); // Chrome >= 58
|
||||||
|
if (m && Number(m[1]) < 58) return true;
|
||||||
|
m = /Firefox\/([\d.]+)/.exec(ua); // Firefox >= 45
|
||||||
|
return m && Number(m[1]) < 45;
|
||||||
|
})();
|
||||||
|
if (detected) {
|
||||||
|
document.getElementById('loader-tips-outdated').hidden = false;
|
||||||
|
document.getElementById('loader-tips-timeout').hidden = false;
|
||||||
|
}
|
||||||
|
})();
|
61
src/main.ts
|
@ -1,56 +1,11 @@
|
||||||
import Vue from 'vue';
|
import { createApp } from 'vue';
|
||||||
import App from '@/App.vue';
|
import App from '@/App.vue';
|
||||||
import '@/registerServiceWorker';
|
import '@/registerServiceWorker';
|
||||||
import {
|
import '@element-plus/theme-chalk/dist/index.css';
|
||||||
Button,
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue';
|
||||||
Checkbox,
|
|
||||||
Col,
|
|
||||||
Container,
|
|
||||||
Dialog,
|
|
||||||
Form,
|
|
||||||
FormItem,
|
|
||||||
Footer,
|
|
||||||
Icon,
|
|
||||||
Image,
|
|
||||||
Input,
|
|
||||||
Link,
|
|
||||||
Main,
|
|
||||||
Notification,
|
|
||||||
Progress,
|
|
||||||
Radio,
|
|
||||||
Row,
|
|
||||||
Table,
|
|
||||||
TableColumn,
|
|
||||||
Tooltip,
|
|
||||||
Upload,
|
|
||||||
MessageBox,
|
|
||||||
} from 'element-ui';
|
|
||||||
import 'element-ui/lib/theme-chalk/base.css';
|
|
||||||
|
|
||||||
Vue.use(Link);
|
const app = createApp(App);
|
||||||
Vue.use(Image);
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
Vue.use(Button);
|
app.component(key, component);
|
||||||
Vue.use(Dialog);
|
}
|
||||||
Vue.use(Form);
|
app.mount('#app');
|
||||||
Vue.use(FormItem);
|
|
||||||
Vue.use(Input);
|
|
||||||
Vue.use(Table);
|
|
||||||
Vue.use(TableColumn);
|
|
||||||
Vue.use(Main);
|
|
||||||
Vue.use(Footer);
|
|
||||||
Vue.use(Container);
|
|
||||||
Vue.use(Icon);
|
|
||||||
Vue.use(Row);
|
|
||||||
Vue.use(Col);
|
|
||||||
Vue.use(Upload);
|
|
||||||
Vue.use(Checkbox);
|
|
||||||
Vue.use(Radio);
|
|
||||||
Vue.use(Tooltip);
|
|
||||||
Vue.use(Progress);
|
|
||||||
Vue.prototype.$notify = Notification;
|
|
||||||
Vue.prototype.$confirm = MessageBox.confirm;
|
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
|
||||||
new Vue({
|
|
||||||
render: (h) => h(App),
|
|
||||||
}).$mount('#app');
|
|
||||||
|
|
|
@ -1,30 +1,3 @@
|
||||||
/* eslint-disable no-console */
|
import { useRegisterSW } from 'virtual:pwa-register/vue';
|
||||||
|
|
||||||
import { register } from 'register-service-worker';
|
useRegisterSW();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production' && window.location.protocol === 'https:') {
|
|
||||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
|
||||||
ready() {
|
|
||||||
console.log('App is being served from cache by a service worker.');
|
|
||||||
},
|
|
||||||
registered() {
|
|
||||||
console.log('Service worker has been registered.');
|
|
||||||
},
|
|
||||||
cached() {
|
|
||||||
console.log('Content has been cached for offline use.');
|
|
||||||
},
|
|
||||||
updatefound() {
|
|
||||||
console.log('New content is downloading.');
|
|
||||||
},
|
|
||||||
updated() {
|
|
||||||
console.log('New content is available.');
|
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
offline() {
|
|
||||||
console.log('No internet connection found. App is running in offline mode.');
|
|
||||||
},
|
|
||||||
error(error) {
|
|
||||||
console.error('Error during service worker registration:', error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,85 +3,83 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
#app{
|
#app {
|
||||||
color: $dark-text-info;
|
color: $dark-text-info;
|
||||||
}
|
}
|
||||||
body{
|
body {
|
||||||
background-color: $dark-bg;
|
background-color: $dark-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FORM
|
// FORM
|
||||||
.el-radio{
|
.el-radio {
|
||||||
&__label{
|
&__label {
|
||||||
color: $dark-text-main;
|
color: $dark-text-main;
|
||||||
}
|
}
|
||||||
&__input{
|
&__input {
|
||||||
color: $dark-text-info;
|
color: $dark-text-info;
|
||||||
.el-radio__inner{
|
.el-radio__inner {
|
||||||
border-color: $dark-border;
|
border-color: $dark-border;
|
||||||
background-color: $dark-btn-bg;
|
background-color: $dark-btn-bg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-checked{
|
&.is-checked {
|
||||||
.el-radio__inner{
|
.el-radio__inner {
|
||||||
background-color: $blue;
|
background-color: $blue;
|
||||||
}
|
}
|
||||||
.el-radio__label{
|
.el-radio__label {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-checkbox.is-bordered{
|
.el-checkbox.is-bordered {
|
||||||
border-color: $dark-border;
|
border-color: $dark-border;
|
||||||
color: $dark-text-main;
|
color: $dark-text-main;
|
||||||
background-color: $dark-btn-bg;
|
background-color: $dark-btn-bg;
|
||||||
.el-checkbox__inner{
|
.el-checkbox__inner {
|
||||||
background-color: $dark-btn-bg-highlight;
|
background-color: $dark-btn-bg-highlight;
|
||||||
border-color: $dark-border-highlight;
|
border-color: $dark-border-highlight;
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover {
|
||||||
border-color: $dark-border-highlight;
|
border-color: $dark-border-highlight;
|
||||||
.el-checkbox__inner{
|
.el-checkbox__inner {
|
||||||
background-color: $dark-btn-bg-highlight;
|
background-color: $dark-btn-bg-highlight;
|
||||||
border-color: $dark-border-highlight;
|
border-color: $dark-border-highlight;
|
||||||
}
|
}
|
||||||
.el-checkbox__label{
|
.el-checkbox__label {
|
||||||
color: $dark-text-info;
|
color: $dark-text-info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.is-checked{
|
&.is-checked {
|
||||||
background-color: $blue;
|
background-color: $blue;
|
||||||
.el-checkbox__inner{
|
.el-checkbox__inner {
|
||||||
border-color: white;
|
border-color: white;
|
||||||
}
|
}
|
||||||
.el-checkbox__label{
|
.el-checkbox__label {
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover {
|
||||||
border-color: $blue;
|
border-color: $blue;
|
||||||
.el-checkbox__inner{
|
.el-checkbox__inner {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// BUTTON
|
// BUTTON
|
||||||
.el-button{
|
.el-button {
|
||||||
background-color: $dark-btn-bg;
|
background-color: $dark-btn-bg;
|
||||||
border-color: $dark-border;
|
border-color: $dark-border;
|
||||||
color: $dark-text-main;
|
color: $dark-text-main;
|
||||||
|
|
||||||
&:active{
|
&:active {
|
||||||
transform: translateY(2px);
|
transform: translateY(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--default{
|
&--default {
|
||||||
&.is-plain {
|
&.is-plain {
|
||||||
background-color: $dark-btn-bg;
|
background-color: $dark-btn-bg;
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -101,7 +99,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--success{
|
&--success {
|
||||||
&.is-plain {
|
&.is-plain {
|
||||||
background-color: $dark-btn-bg;
|
background-color: $dark-btn-bg;
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -120,11 +118,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&--danger{
|
&--danger {
|
||||||
&.is-plain{
|
&.is-plain {
|
||||||
border-color: $dark-border;
|
border-color: $dark-border;
|
||||||
background-color: $dark-btn-bg;
|
background-color: $dark-btn-bg;
|
||||||
&:hover{
|
&:hover {
|
||||||
background-color: $red;
|
background-color: $red;
|
||||||
border-color: $red;
|
border-color: $red;
|
||||||
}
|
}
|
||||||
|
@ -142,44 +140,45 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// 文件拖放区
|
// 文件拖放区
|
||||||
.el-upload__tip{
|
.el-upload__tip {
|
||||||
color: $dark-text-info;
|
color: $dark-text-info;
|
||||||
}
|
}
|
||||||
.el-upload-dragger{
|
.el-upload-dragger {
|
||||||
background-color: $dark-uploader-bg;
|
background-color: $dark-uploader-bg;
|
||||||
border-color: $dark-border;
|
border-color: $dark-border;
|
||||||
.el-upload__text{
|
.el-upload__text {
|
||||||
color: $dark-text-info;
|
color: $dark-text-info;
|
||||||
}
|
}
|
||||||
&:hover{
|
&:hover {
|
||||||
background: $dark-uploader-bg-highlight;
|
background: $dark-uploader-bg-highlight;
|
||||||
border-color: $dark-border-highlight;
|
border-color: $dark-border-highlight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TABLE
|
// TABLE
|
||||||
.el-table{
|
.el-table {
|
||||||
background-color: $dark-bg-td;
|
background-color: $dark-bg-td;
|
||||||
&:before{ // 去除表格末尾的横线
|
&:before {
|
||||||
|
// 去除表格末尾的横线
|
||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
&__header{
|
&__header {
|
||||||
th{
|
th {
|
||||||
border-bottom-color: $dark-border !important;
|
border-bottom-color: $dark-border !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
th.el-table__cell{
|
th.el-table__cell {
|
||||||
background-color: $dark-bg-th;
|
background-color: $dark-bg-th;
|
||||||
color: $dark-text-info;
|
color: $dark-text-info;
|
||||||
}
|
}
|
||||||
td{
|
td {
|
||||||
border-bottom-color: $dark-border !important;
|
border-bottom-color: $dark-border !important;
|
||||||
}
|
}
|
||||||
tr{
|
tr {
|
||||||
background-color: $dark-bg-td;
|
background-color: $dark-bg-td;
|
||||||
color: $dark-text-main;
|
color: $dark-text-main;
|
||||||
&:hover{
|
&:hover {
|
||||||
td{
|
td {
|
||||||
background-color: $dark-bg-th !important;
|
background-color: $dark-bg-th !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,68 +186,66 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// LINKS
|
// LINKS
|
||||||
a{
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: darken($dark-color-link, 15%);
|
color: darken($dark-color-link, 15%);
|
||||||
&:hover{
|
&:hover {
|
||||||
color: $dark-color-link;
|
color: $dark-color-link;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ALERT
|
// ALERT
|
||||||
.el-notification{
|
.el-notification {
|
||||||
background-color: $dark-btn-bg-highlight;
|
background-color: $dark-btn-bg-highlight;
|
||||||
border-color: $dark-border;
|
border-color: $dark-border;
|
||||||
&__title{
|
&__title {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
&__content{
|
&__content {
|
||||||
color: $dark-text-info;
|
color: $dark-text-info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DIALOG
|
// DIALOG
|
||||||
.el-dialog{
|
.el-dialog {
|
||||||
background-color: $dark-dialog-bg;
|
background-color: $dark-dialog-bg;
|
||||||
.el-dialog__header{
|
.el-dialog__header {
|
||||||
.el-dialog__title{
|
.el-dialog__title {
|
||||||
color: $dark-text-main;
|
color: $dark-text-main;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.el-dialog__body{
|
.el-dialog__body {
|
||||||
color: $dark-text-main;
|
color: $dark-text-main;
|
||||||
.el-input{
|
.el-input {
|
||||||
.el-input__inner{
|
.el-input__inner {
|
||||||
color: $dark-text-main;
|
color: $dark-text-main;
|
||||||
background-color: $dark-btn-bg;
|
background-color: $dark-btn-bg;
|
||||||
}
|
}
|
||||||
.el-input__suffix{
|
.el-input__suffix {
|
||||||
.el-input__suffix-inner{
|
.el-input__suffix-inner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.el-input__count{
|
.el-input__count {
|
||||||
.el-input__count-inner{
|
.el-input__count-inner {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.item-desc{
|
.item-desc {
|
||||||
color: $dark-text-info;
|
color: $dark-text-info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 自定义样式
|
// 自定义样式
|
||||||
// 首页弹窗提示信息的 更新信息 面板
|
// 首页弹窗提示信息的 更新信息 面板
|
||||||
.update-info{
|
.update-info {
|
||||||
border: 1px solid $dark-btn-bg !important;
|
border: 1px solid $dark-btn-bg !important;
|
||||||
.update-title{
|
.update-title {
|
||||||
color: $dark-text-main;
|
color: $dark-text-main;
|
||||||
background-color: $dark-btn-bg !important;
|
background-color: $dark-btn-bg !important;
|
||||||
}
|
}
|
||||||
.update-content{
|
.update-content {
|
||||||
color: $dark-text-info;
|
color: $dark-text-info;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
$color-checkbox: $blue;
|
$color-checkbox: $blue;
|
||||||
$color-border-el: #DCDFE6;
|
$color-border-el: #dcdfe6;
|
||||||
|
|
||||||
$btn-radius: 6px;
|
$btn-radius: 6px;
|
||||||
|
|
||||||
/* FORM */
|
/* FORM */
|
||||||
// checkbox
|
// checkbox
|
||||||
.el-checkbox.is-bordered{
|
.el-checkbox.is-bordered {
|
||||||
@include border-radius($btn-radius) ;
|
@include border-radius($btn-radius);
|
||||||
&:hover{
|
&:hover {
|
||||||
border-color: $color-checkbox;
|
border-color: $color-checkbox;
|
||||||
.el-checkbox__label{
|
.el-checkbox__label {
|
||||||
color: $color-checkbox;
|
color: $color-checkbox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.el-checkbox__input.is-focus{
|
.el-checkbox__input.is-focus {
|
||||||
.el-checkbox__inner{
|
.el-checkbox__inner {
|
||||||
border-color: $color-border-el;
|
border-color: $color-border-el;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.is-checked{
|
&.is-checked {
|
||||||
background-color: $color-checkbox;
|
background-color: $color-checkbox;
|
||||||
.el-checkbox__label{
|
.el-checkbox__label {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.el-checkbox__inner{
|
.el-checkbox__inner {
|
||||||
border-color: white;
|
border-color: white;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
&:after{
|
&:after {
|
||||||
border-color: $color-checkbox;
|
border-color: $color-checkbox;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,6 @@ $btn-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// el-button
|
// el-button
|
||||||
.el-button{
|
.el-button {
|
||||||
@include border-radius($btn-radius) ;
|
@include border-radius($btn-radius);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,35 @@
|
||||||
|
|
||||||
$gap: 5px;
|
$gap: 5px;
|
||||||
@for $item from 0 through 8 {
|
@for $item from 0 through 8 {
|
||||||
.mt-#{$item} { margin-top : $gap * $item !important;}
|
.mt-#{$item} {
|
||||||
.mb-#{$item} { margin-bottom : $gap * $item !important;}
|
margin-top: $gap * $item !important;
|
||||||
.ml-#{$item} { margin-left : $gap * $item !important;}
|
}
|
||||||
.mr-#{$item} { margin-right : $gap * $item !important;}
|
.mb-#{$item} {
|
||||||
.m-#{$item} { margin : $gap * $item !important;}
|
margin-bottom: $gap * $item !important;
|
||||||
|
}
|
||||||
|
.ml-#{$item} {
|
||||||
|
margin-left: $gap * $item !important;
|
||||||
|
}
|
||||||
|
.mr-#{$item} {
|
||||||
|
margin-right: $gap * $item !important;
|
||||||
|
}
|
||||||
|
.m-#{$item} {
|
||||||
|
margin: $gap * $item !important;
|
||||||
|
}
|
||||||
|
|
||||||
.pt-#{$item} { padding-top : $gap * $item !important;}
|
.pt-#{$item} {
|
||||||
.pb-#{$item} { padding-bottom : $gap * $item !important;}
|
padding-top: $gap * $item !important;
|
||||||
.pl-#{$item} { padding-left : $gap * $item !important;}
|
}
|
||||||
.pr-#{$item} { padding-right : $gap * $item !important;}
|
.pb-#{$item} {
|
||||||
.p-#{$item} { padding : $gap * $item !important;}
|
padding-bottom: $gap * $item !important;
|
||||||
|
}
|
||||||
|
.pl-#{$item} {
|
||||||
|
padding-left: $gap * $item !important;
|
||||||
|
}
|
||||||
|
.pr-#{$item} {
|
||||||
|
padding-right: $gap * $item !important;
|
||||||
|
}
|
||||||
|
.p-#{$item} {
|
||||||
|
padding: $gap * $item !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
body{
|
body {
|
||||||
font-family: $font-family;
|
font-family: $font-family;
|
||||||
font-size: $fz-main;
|
font-size: $fz-main;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
|
@ -11,14 +11,18 @@ body{
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#app-footer a {
|
|
||||||
padding-left: 0.2em;
|
|
||||||
padding-right: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app-footer {
|
#app-footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: small;
|
font-size: small;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
padding-left: 0.2em;
|
||||||
|
padding-right: 0.2em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#app-control {
|
#app-control {
|
||||||
|
@ -26,13 +30,13 @@ body{
|
||||||
padding-bottom: 1em;
|
padding-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
audio{
|
audio {
|
||||||
margin-bottom: 15px; // 播放控件与表格间隔
|
margin-bottom: 15px; // 播放控件与表格间隔
|
||||||
}
|
}
|
||||||
|
|
||||||
a{
|
a {
|
||||||
color: darken($color-link, 15%);
|
color: darken($color-link, 15%);
|
||||||
&:hover{
|
&:hover {
|
||||||
color: $color-link;
|
color: $color-link;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
// box-shadow
|
// box-shadow
|
||||||
@mixin box-shadow($value...){
|
@mixin box-shadow($value...) {
|
||||||
-webkit-box-shadow: $value;
|
-webkit-box-shadow: $value;
|
||||||
-moz-box-shadow: $value;
|
-moz-box-shadow: $value;
|
||||||
box-shadow: $value;
|
box-shadow: $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// border-radius
|
// border-radius
|
||||||
@mixin border-radius($corner...){
|
@mixin border-radius($corner...) {
|
||||||
-webkit-border-radius: $corner;
|
-webkit-border-radius: $corner;
|
||||||
-moz-border-radius: $corner;
|
-moz-border-radius: $corner;
|
||||||
border-radius: $corner;
|
border-radius: $corner;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin clearfix(){
|
@mixin clearfix() {
|
||||||
&:after{
|
&:after {
|
||||||
content: '';
|
content: '';
|
||||||
display: block;
|
display: block;
|
||||||
clear: both;
|
clear: both;
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin transform($value){
|
@mixin transform($value) {
|
||||||
-webkit-transform: $value;
|
-webkit-transform: $value;
|
||||||
-moz-transform: $value;
|
-moz-transform: $value;
|
||||||
-ms-transform: $value;
|
-ms-transform: $value;
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
transform: $value;
|
transform: $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin transition($value...){
|
@mixin transition($value...) {
|
||||||
-webkit-transition: $value;
|
-webkit-transition: $value;
|
||||||
-moz-transition: $value;
|
-moz-transition: $value;
|
||||||
-ms-transition: $value;
|
-ms-transition: $value;
|
||||||
|
@ -37,23 +37,22 @@
|
||||||
transition: $value;
|
transition: $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin animation($value){
|
@mixin animation($value) {
|
||||||
animation: $value;
|
animation: $value;
|
||||||
-webkit-animation: $value;
|
-webkit-animation: $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin linear-gradient($direct, $colors){
|
@mixin linear-gradient($direct, $colors) {
|
||||||
background: linear-gradient($direct, $colors);
|
background: linear-gradient($direct, $colors);
|
||||||
background: -webkit-linear-gradient($direct, $colors);
|
background: -webkit-linear-gradient($direct, $colors);
|
||||||
background: -moz-linear-gradient($direct, $colors);
|
background: -moz-linear-gradient($direct, $colors);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin backdrop-filter($value){
|
@mixin backdrop-filter($value) {
|
||||||
backdrop-filter: $value ;
|
backdrop-filter: $value;
|
||||||
-webkit-backdrop-filter: $value;
|
-webkit-backdrop-filter: $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Extension
|
Extension
|
||||||
*/
|
*/
|
||||||
|
@ -65,8 +64,8 @@
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-like{
|
.btn-like {
|
||||||
&:active{
|
&:active {
|
||||||
@include transform(translateY(2px))
|
@include transform(translateY(2px));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,34 @@
|
||||||
// COLORS
|
// COLORS
|
||||||
$blue : #409EFF;
|
$blue: #409eff;
|
||||||
$red : #F56C6C;
|
$red: #f56c6c;
|
||||||
$green : #85ce61;
|
$green: #85ce61;
|
||||||
|
|
||||||
// TEXT
|
// TEXT
|
||||||
$text-main : #2C3E50;
|
$text-main: #2c3e50;
|
||||||
$color-link: $blue;
|
$color-link: $blue;
|
||||||
|
|
||||||
$fz-main: 14px;
|
$fz-main: 14px;
|
||||||
$fz-mini-title: 13px;
|
$fz-mini-title: 13px;
|
||||||
$fz-mini-content: 12px;
|
$fz-mini-content: 12px;
|
||||||
|
|
||||||
$font-family: "Helvetica Neue", Helvetica, "PingFang SC",
|
$font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', '微软雅黑', Arial,
|
||||||
"Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
sans-serif;
|
||||||
|
|
||||||
|
|
||||||
// DARK MODE
|
// DARK MODE
|
||||||
$dark-border : lighten(black, 25%);
|
$dark-border: lighten(black, 25%);
|
||||||
$dark-border-highlight : lighten(black, 55%);
|
$dark-border-highlight: lighten(black, 55%);
|
||||||
$dark-bg : lighten(black, 10%);
|
$dark-bg: lighten(black, 10%);
|
||||||
$dark-text-main : lighten(black, 90%);
|
$dark-text-main: lighten(black, 90%);
|
||||||
$dark-text-info : lighten(black, 60%);
|
$dark-text-info: lighten(black, 60%);
|
||||||
$dark-uploader-bg : lighten(black, 13%);
|
$dark-uploader-bg: lighten(black, 13%);
|
||||||
$dark-dialog-bg : lighten(black, 15%);
|
$dark-dialog-bg: lighten(black, 15%);
|
||||||
$dark-uploader-bg-highlight : lighten(black, 18%);
|
$dark-uploader-bg-highlight: lighten(black, 18%);
|
||||||
$dark-btn-bg : lighten(black, 20%);
|
$dark-btn-bg: lighten(black, 20%);
|
||||||
$dark-btn-bg-highlight : lighten(black, 30%);
|
$dark-btn-bg-highlight: lighten(black, 30%);
|
||||||
$dark-bg-th : lighten(black, 18%);
|
$dark-bg-th: lighten(black, 18%);
|
||||||
$dark-bg-td : lighten(black, 13%);
|
$dark-bg-td: lighten(black, 13%);
|
||||||
$dark-color-link : $green;
|
$dark-color-link: $green;
|
||||||
|
|
||||||
$dark-blue : darken(desaturate($blue, 40%), 30%);
|
$dark-blue: darken(desaturate($blue, 40%), 30%);
|
||||||
$dark-red : darken(desaturate($red, 50%), 30%);
|
$dark-red: darken(desaturate($red, 50%), 30%);
|
||||||
$dark-green : darken(desaturate($green, 30%), 30%);
|
$dark-green: darken(desaturate($green, 30%), 30%);
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
@import "variables";
|
@import 'variables';
|
||||||
@import "utility";
|
@import 'utility';
|
||||||
@import "gaps";
|
@import 'gaps';
|
||||||
@import "element-ui-overrite";
|
@import 'element-ui-overrite';
|
||||||
|
|
||||||
@import "normal";
|
|
||||||
@import "dark-mode"; // dark-mode 放在 normal 后面,以获得更高优先级
|
|
||||||
|
|
||||||
|
@import 'normal';
|
||||||
|
@import 'dark-mode'; // dark-mode 放在 normal 后面,以获得更高优先级
|
||||||
|
|
||||||
// 首页弹窗提示信息的 更新信息 面板
|
// 首页弹窗提示信息的 更新信息 面板
|
||||||
.update-info{
|
.update-info {
|
||||||
@include border-radius(8px);
|
@include border-radius(8px);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border: 1px solid $color-border-el;
|
border: 1px solid $color-border-el;
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
.update-title{
|
.update-title {
|
||||||
font-size: $fz-mini-title;
|
font-size: $fz-mini-title;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
background-color: $color-border-el;
|
background-color: $color-border-el;
|
||||||
}
|
}
|
||||||
.update-content{
|
.update-content {
|
||||||
font-size: $fz-mini-content;
|
font-size: $fz-mini-content;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
declare module '*.vue' {
|
declare module '*.vue' {
|
||||||
import Vue from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
export default Vue;
|
const Component: ReturnType<typeof defineComponent>;
|
||||||
|
export default Component;
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,8 +44,7 @@ export async function extractQQMusicMeta(
|
||||||
musicMeta.common.artist = '';
|
musicMeta.common.artist = '';
|
||||||
if (!musicMeta.common.artists) {
|
if (!musicMeta.common.artists) {
|
||||||
musicMeta.common.artist = fromGBK(musicMeta.common.artist);
|
musicMeta.common.artist = fromGBK(musicMeta.common.artist);
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
musicMeta.common.artist = musicMeta.common.artists.map(fromGBK).join();
|
musicMeta.common.artist = musicMeta.common.artists.map(fromGBK).join();
|
||||||
}
|
}
|
||||||
musicMeta.common.title = fromGBK(musicMeta.common.title);
|
musicMeta.common.title = fromGBK(musicMeta.common.title);
|
||||||
|
|
|
@ -3,13 +3,13 @@
|
||||||
<file-selector @error="showFail" @success="showSuccess" />
|
<file-selector @error="showFail" @success="showSuccess" />
|
||||||
|
|
||||||
<div id="app-control">
|
<div id="app-control">
|
||||||
<el-row class="mb-3">
|
<el-row class="mb-3" justify="center" align="middle">
|
||||||
<span>歌曲命名格式:</span>
|
<span>歌曲命名格式:</span>
|
||||||
<el-radio v-for="k in FilenamePolicies" :key="k.key" v-model="filename_policy" :label="k.key">
|
<el-radio v-for="k in FilenamePolicies" :key="k.key" v-model="filename_policy" :label="k.key">
|
||||||
{{ k.text }}
|
{{ k.text }}
|
||||||
</el-radio>
|
</el-radio>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row justify="center" align="middle">
|
||||||
<edit-dialog
|
<edit-dialog
|
||||||
:show="showEditDialog"
|
:show="showEditDialog"
|
||||||
:picture="editing_data.picture"
|
:picture="editing_data.picture"
|
||||||
|
@ -18,37 +18,52 @@
|
||||||
:album="editing_data.album"
|
:album="editing_data.album"
|
||||||
:albumartist="editing_data.albumartist"
|
:albumartist="editing_data.albumartist"
|
||||||
:genre="editing_data.genre"
|
:genre="editing_data.genre"
|
||||||
@cancel="showEditDialog = false" @ok="handleEdit"></edit-dialog>
|
@cancel="showEditDialog = false"
|
||||||
|
@ok="handleEdit"
|
||||||
|
></edit-dialog>
|
||||||
<config-dialog :show="showConfigDialog" @done="showConfigDialog = false"></config-dialog>
|
<config-dialog :show="showConfigDialog" @done="showConfigDialog = false"></config-dialog>
|
||||||
<el-tooltip class="item" effect="dark" placement="top">
|
<el-tooltip class="item" effect="dark" placement="top">
|
||||||
<div slot="content">
|
<template #content>
|
||||||
<span> 部分解密方案需要设定解密参数。 </span>
|
<span> 部分解密方案需要设定解密参数。 </span>
|
||||||
</div>
|
</template>
|
||||||
<el-button icon="el-icon-s-tools" plain @click="showConfigDialog = true">解密设定</el-button>
|
<el-button plain @click="showConfigDialog = true">
|
||||||
|
<el-icon><Tools /></el-icon> 解密设定
|
||||||
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-button icon="el-icon-download" plain @click="handleDownloadAll">下载全部</el-button>
|
<el-button plain @click="handleDownloadAll">
|
||||||
<el-button icon="el-icon-delete" plain type="danger" @click="handleDeleteAll">清除全部</el-button>
|
<el-icon><Download /></el-icon> 下载全部
|
||||||
|
</el-button>
|
||||||
|
<el-button plain type="danger" @click="handleDeleteAll">
|
||||||
|
<el-icon><Delete /></el-icon> 清除全部
|
||||||
|
</el-button>
|
||||||
|
|
||||||
<el-tooltip class="item" effect="dark" placement="top-start">
|
<el-tooltip class="item" effect="dark" placement="top-start">
|
||||||
<div slot="content">
|
<template #content>
|
||||||
<span v-if="instant_save">工作模式: {{ dir ? '写入本地文件系统' : '调用浏览器下载' }}</span>
|
<span v-if="instant_save"> 工作模式: {{ dir ? '写入本地文件系统' : '调用浏览器下载' }} </span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
当您使用此工具进行大量文件解锁的时候,建议开启此选项。<br />
|
当您使用此工具进行大量文件解锁的时候,建议开启此选项。<br />
|
||||||
开启后,解锁结果将不会存留于浏览器中,防止内存不足。
|
开启后,解锁结果将不会存留于浏览器中,防止内存不足。
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</template>
|
||||||
<el-checkbox v-model="instant_save" type="success" border class="ml-2">立即保存</el-checkbox>
|
<el-checkbox v-model="instant_save" type="success" border class="ml-2"> 立即保存 </el-checkbox>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<audio :autoplay="playing_auto" :src="playing_url" controls />
|
<audio :autoplay="playing_auto" :src="playing_url" controls />
|
||||||
|
|
||||||
<PreviewTable :policy="filename_policy" :table-data="tableData" @download="saveFile" @edit="editFile" @play="changePlaying" />
|
<PreviewTable
|
||||||
|
:policy="filename_policy"
|
||||||
|
:table-data="tableData"
|
||||||
|
@download="saveFile"
|
||||||
|
@edit="editFile"
|
||||||
|
@play="changePlaying"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { ElNotification, ElMessage, ElMessageBox } from 'element-plus';
|
||||||
import FileSelector from '@/component/FileSelector';
|
import FileSelector from '@/component/FileSelector';
|
||||||
import PreviewTable from '@/component/PreviewTable';
|
import PreviewTable from '@/component/PreviewTable';
|
||||||
import ConfigDialog from '@/component/ConfigDialog';
|
import ConfigDialog from '@/component/ConfigDialog';
|
||||||
|
@ -70,7 +85,14 @@ export default {
|
||||||
return {
|
return {
|
||||||
showConfigDialog: false,
|
showConfigDialog: false,
|
||||||
showEditDialog: false,
|
showEditDialog: false,
|
||||||
editing_data: { picture: '', title: '', artist: '', album: '', albumartist: '', genre: '', },
|
editing_data: {
|
||||||
|
picture: '',
|
||||||
|
title: '',
|
||||||
|
artist: '',
|
||||||
|
album: '',
|
||||||
|
albumartist: '',
|
||||||
|
genre: '',
|
||||||
|
},
|
||||||
tableData: [],
|
tableData: [],
|
||||||
playing_url: '',
|
playing_url: '',
|
||||||
playing_auto: false,
|
playing_auto: false,
|
||||||
|
@ -92,7 +114,8 @@ export default {
|
||||||
RemoveBlobMusic(data);
|
RemoveBlobMusic(data);
|
||||||
} else {
|
} else {
|
||||||
this.tableData.push(data);
|
this.tableData.push(data);
|
||||||
this.$notify.success({
|
ElNotification({
|
||||||
|
type: 'success',
|
||||||
title: '解锁成功',
|
title: '解锁成功',
|
||||||
message: '成功解锁 ' + data.title,
|
message: '成功解锁 ' + data.title,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
|
@ -105,7 +128,8 @@ export default {
|
||||||
},
|
},
|
||||||
showFail(errInfo, filename) {
|
showFail(errInfo, filename) {
|
||||||
console.error(errInfo, filename);
|
console.error(errInfo, filename);
|
||||||
this.$notify.error({
|
ElNotification({
|
||||||
|
type: 'error',
|
||||||
title: '出现问题',
|
title: '出现问题',
|
||||||
message:
|
message:
|
||||||
errInfo +
|
errInfo +
|
||||||
|
@ -164,12 +188,13 @@ export default {
|
||||||
console.warn('获取图像失败', this.editing_data.picture);
|
console.warn('获取图像失败', this.editing_data.picture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newMeta = { picture: imageInfo?.buffer,
|
const newMeta = {
|
||||||
|
picture: imageInfo?.buffer,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
artists: data.artist.split(split_regex),
|
artists: data.artist.split(split_regex),
|
||||||
album: data.album,
|
album: data.album,
|
||||||
albumartist: data.albumartist,
|
albumartist: data.albumartist,
|
||||||
genre: data.genre.split(split_regex)
|
genre: data.genre.split(split_regex),
|
||||||
};
|
};
|
||||||
const buffer = Buffer.from(await this.editing_data.blob.arrayBuffer());
|
const buffer = Buffer.from(await this.editing_data.blob.arrayBuffer());
|
||||||
const mime = AudioMimeType[this.editing_data.ext] || AudioMimeType.mp3;
|
const mime = AudioMimeType[this.editing_data.ext] || AudioMimeType.mp3;
|
||||||
|
@ -187,19 +212,22 @@ export default {
|
||||||
}
|
}
|
||||||
this.editing_data.file = URL.createObjectURL(this.editing_data.blob);
|
this.editing_data.file = URL.createObjectURL(this.editing_data.blob);
|
||||||
if (writeSuccess === true) {
|
if (writeSuccess === true) {
|
||||||
this.$notify.success({
|
ElNotification({
|
||||||
|
type: 'success',
|
||||||
title: '修改成功',
|
title: '修改成功',
|
||||||
message: notifyMsg,
|
message: notifyMsg,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
} else if (writeSuccess === false) {
|
} else if (writeSuccess === false) {
|
||||||
this.$notify.error({
|
ElNotification({
|
||||||
|
type: 'error',
|
||||||
title: '修改失败',
|
title: '修改失败',
|
||||||
message: notifyMsg,
|
message: notifyMsg,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$notify.warning({
|
ElNotification({
|
||||||
|
type: 'warning',
|
||||||
title: '修改取消',
|
title: '修改取消',
|
||||||
message: notifyMsg,
|
message: notifyMsg,
|
||||||
duration: 3000,
|
duration: 3000,
|
||||||
|
@ -217,7 +245,7 @@ export default {
|
||||||
async saveFile(data) {
|
async saveFile(data) {
|
||||||
if (this.dir) {
|
if (this.dir) {
|
||||||
await DirectlyWriteFile(data, this.filename_policy, this.dir);
|
await DirectlyWriteFile(data, this.filename_policy, this.dir);
|
||||||
this.$notify({
|
ElNotification({
|
||||||
title: '保存成功',
|
title: '保存成功',
|
||||||
message: data.title,
|
message: data.title,
|
||||||
position: 'top-left',
|
position: 'top-left',
|
||||||
|
@ -231,7 +259,7 @@ export default {
|
||||||
async showDirectlySave() {
|
async showDirectlySave() {
|
||||||
if (!window.showDirectoryPicker) return;
|
if (!window.showDirectoryPicker) return;
|
||||||
try {
|
try {
|
||||||
await this.$confirm('您的浏览器支持文件直接保存到磁盘,是否使用?', '新特性提示', {
|
await ElMessageBox.confirm('您的浏览器支持文件直接保存到磁盘,是否使用?', '新特性提示', {
|
||||||
confirmButtonText: '使用',
|
confirmButtonText: '使用',
|
||||||
cancelButtonText: '不使用',
|
cancelButtonText: '不使用',
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"types": [
|
"types": [
|
||||||
"webpack-env",
|
|
||||||
"jest"
|
"jest"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
|
@ -40,4 +39,4 @@
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import AutoImport from 'unplugin-auto-import/vite';
|
||||||
|
import Components from 'unplugin-vue-components/vite';
|
||||||
|
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
import GlobalsPolyfills from '@esbuild-plugins/node-globals-polyfill';
|
||||||
|
import NodeModulesPolyfills from '@esbuild-plugins/node-modules-polyfill';
|
||||||
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
|
import commonjs from 'vite-plugin-commonjs';
|
||||||
|
import replace from '@rollup/plugin-replace';
|
||||||
|
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
optimizeDeps: {
|
||||||
|
esbuildOptions: {
|
||||||
|
define: {
|
||||||
|
global: 'globalThis',
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
GlobalsPolyfills({
|
||||||
|
process: true,
|
||||||
|
buffer: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
|
||||||
|
process: 'rollup-plugin-node-polyfills/polyfills/process-es6',
|
||||||
|
stream: 'rollup-plugin-node-polyfills/polyfills/stream',
|
||||||
|
util: 'rollup-plugin-node-polyfills/polyfills/util',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
commonjs(),
|
||||||
|
NodeModulesPolyfills(),
|
||||||
|
vue(),
|
||||||
|
AutoImport({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [ElementPlusResolver()],
|
||||||
|
}),
|
||||||
|
VitePWA({
|
||||||
|
registerType: 'autoUpdate',
|
||||||
|
manifest: {
|
||||||
|
name: '音乐解锁',
|
||||||
|
theme_color: '#4DBA87',
|
||||||
|
description: '在任何设备上解锁已购的加密音乐!',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: 'assets/img/icons/android-chrome-192x192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: 'assets/img/icons/android-chrome-512x512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
replace({
|
||||||
|
preventAssignment: false,
|
||||||
|
values: {
|
||||||
|
'global.': 'window.',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
viteStaticCopy({
|
||||||
|
targets: [
|
||||||
|
{
|
||||||
|
src: 'img',
|
||||||
|
dest: 'assets',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
|
@ -1,43 +1,43 @@
|
||||||
const ThreadsPlugin = require('threads-plugin');
|
const ThreadsPlugin = require('threads-plugin');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
publicPath: '',
|
publicPath: '',
|
||||||
productionSourceMap: false,
|
productionSourceMap: false,
|
||||||
pwa: {
|
pwa: {
|
||||||
manifestPath: "web-manifest.json",
|
manifestPath: 'web-manifest.json',
|
||||||
name: "音乐解锁",
|
name: '音乐解锁',
|
||||||
themeColor: "#4DBA87",
|
themeColor: '#4DBA87',
|
||||||
msTileColor: "#000000",
|
msTileColor: '#000000',
|
||||||
manifestOptions: {
|
manifestOptions: {
|
||||||
start_url: "./index.html",
|
start_url: './index.html',
|
||||||
description: "在任何设备上解锁已购的加密音乐!",
|
description: '在任何设备上解锁已购的加密音乐!',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
'src': './img/icons/android-chrome-192x192.png',
|
src: './img/icons/android-chrome-192x192.png',
|
||||||
'sizes': '192x192',
|
sizes: '192x192',
|
||||||
'type': 'image/png'
|
type: 'image/png',
|
||||||
},
|
|
||||||
{
|
|
||||||
'src': './img/icons/android-chrome-512x512.png',
|
|
||||||
'sizes': '512x512',
|
|
||||||
'type': 'image/png'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
appleMobileWebAppCapable: 'yes',
|
{
|
||||||
iconPaths: {
|
src: './img/icons/android-chrome-512x512.png',
|
||||||
faviconSVG: './img/icons/safari-pinned-tab.svg',
|
sizes: '512x512',
|
||||||
favicon32: './img/icons/favicon-32x32.png',
|
type: 'image/png',
|
||||||
favicon16: './img/icons/favicon-16x16.png',
|
|
||||||
appleTouchIcon: './img/icons/apple-touch-icon-152x152.png',
|
|
||||||
maskIcon: './img/icons/safari-pinned-tab.svg',
|
|
||||||
msTileImage: './img/icons/msapplication-icon-144x144.png'
|
|
||||||
},
|
},
|
||||||
workboxPluginMode: "GenerateSW",
|
],
|
||||||
workboxOptions: {
|
|
||||||
skipWaiting: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
configureWebpack: {
|
appleMobileWebAppCapable: 'yes',
|
||||||
plugins: [new ThreadsPlugin()]
|
iconPaths: {
|
||||||
}
|
faviconSVG: './img/icons/safari-pinned-tab.svg',
|
||||||
|
favicon32: './img/icons/favicon-32x32.png',
|
||||||
|
favicon16: './img/icons/favicon-16x16.png',
|
||||||
|
appleTouchIcon: './img/icons/apple-touch-icon-152x152.png',
|
||||||
|
maskIcon: './img/icons/safari-pinned-tab.svg',
|
||||||
|
msTileImage: './img/icons/msapplication-icon-144x144.png',
|
||||||
|
},
|
||||||
|
workboxPluginMode: 'GenerateSW',
|
||||||
|
workboxOptions: {
|
||||||
|
skipWaiting: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configureWebpack: {
|
||||||
|
plugins: [new ThreadsPlugin()],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|