mirror of
https://git.unlock-music.dev/um/web.git
synced 2024-12-28 17:05:02 +08:00
refactor: component/*.vue
This commit is contained in:
parent
ca4ed149b2
commit
c7e5dfb4c4
94
package-lock.json
generated
94
package-lock.json
generated
@ -5820,6 +5820,12 @@
|
|||||||
"estraverse": "^4.1.1"
|
"estraverse": "^4.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"esm": {
|
||||||
|
"version": "3.2.25",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/esm/-/esm-3.2.25.tgz",
|
||||||
|
"integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"esprima": {
|
"esprima": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||||
@ -7822,6 +7828,11 @@
|
|||||||
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
|
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"is-observable": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/is-observable/-/is-observable-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-DailKdLb0WU+xX8K5w7VsJhapwHLZ9jjmazqCJq4X12CTgqq73TKnbRcnSLuXYPOoLQgV5IrD7ePiX/h1vnkBw=="
|
||||||
|
},
|
||||||
"is-path-cwd": {
|
"is-path-cwd": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz",
|
||||||
@ -9305,6 +9316,11 @@
|
|||||||
"has": "^1.0.3"
|
"has": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"observable-fns": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/observable-fns/-/observable-fns-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-wf7g4Jpo1Wt2KIqZKLGeiuLOEMqpaOZ5gJn7DmSdqXgTdxRwSdBhWegQQpPteQ2gZvzCKqNNpwb853wcpA0j7A=="
|
||||||
|
},
|
||||||
"obuf": {
|
"obuf": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
|
||||||
@ -12243,6 +12259,56 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"threads": {
|
||||||
|
"version": "1.6.4",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/threads/-/threads-1.6.4.tgz",
|
||||||
|
"integrity": "sha512-A+9MQFAUha9W8MjIPmrvETy98qVmZFr5Unox9D95y7kvz3fBpGiFS7JOZs07B2KvTHoRNI5MrGudRVeCmv4Alw==",
|
||||||
|
"requires": {
|
||||||
|
"callsites": "^3.1.0",
|
||||||
|
"debug": "^4.2.0",
|
||||||
|
"is-observable": "^2.1.0",
|
||||||
|
"observable-fns": "^0.5.1",
|
||||||
|
"tiny-worker": ">= 2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"callsites": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://mirrors.tencent.com/npm/callsites/-/callsites-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"threads-plugin": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/threads-plugin/-/threads-plugin-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lQENPueZLsD+6Cvxvj/QaQyUskwnFZO+2ZGDMnPIvtytSeywWvYzete8paZ9L+5IR4v8jnSYNZPlIQrEhSK1EA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"loader-utils": "^1.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"json5": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/json5/-/json5-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"minimist": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loader-utils": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/loader-utils/-/loader-utils-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"big.js": "^5.2.2",
|
||||||
|
"emojis-list": "^3.0.0",
|
||||||
|
"json5": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"throttle-debounce": {
|
"throttle-debounce": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-1.1.0.tgz",
|
||||||
@ -12284,6 +12350,15 @@
|
|||||||
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
|
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"tiny-worker": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "http://mirrors.cloud.tencent.com/npm/tiny-worker/-/tiny-worker-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"esm": "^3.2.25"
|
||||||
|
}
|
||||||
|
},
|
||||||
"tinycolor2": {
|
"tinycolor2": {
|
||||||
"version": "1.4.2",
|
"version": "1.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz",
|
||||||
@ -13009,11 +13084,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.12.tgz",
|
||||||
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
"integrity": "sha512-uhmLFETqPPNyuLLbsKz6ioJ4q7AZHzD8ZVFNATNyICSZouqP2Sz0rotWQC8UNBF6VGSCs5abnKJoStA6JbCbfg=="
|
||||||
},
|
},
|
||||||
"vue-class-component": {
|
|
||||||
"version": "7.2.6",
|
|
||||||
"resolved": "http://mirrors.cloud.tencent.com/npm/vue-class-component/-/vue-class-component-7.2.6.tgz",
|
|
||||||
"integrity": "sha512-+eaQXVrAm/LldalI272PpDe3+i4mPis0ORiMYxF6Ae4hyuCh15W8Idet7wPUEs4N4YptgFHGys4UrgNQOMyO6w=="
|
|
||||||
},
|
|
||||||
"vue-cli-plugin-element": {
|
"vue-cli-plugin-element": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/vue-cli-plugin-element/-/vue-cli-plugin-element-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-cli-plugin-element/-/vue-cli-plugin-element-1.0.1.tgz",
|
||||||
@ -13136,11 +13206,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue-property-decorator": {
|
|
||||||
"version": "9.1.2",
|
|
||||||
"resolved": "http://mirrors.cloud.tencent.com/npm/vue-property-decorator/-/vue-property-decorator-9.1.2.tgz",
|
|
||||||
"integrity": "sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ=="
|
|
||||||
},
|
|
||||||
"vue-style-loader": {
|
"vue-style-loader": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz",
|
||||||
@ -13957,15 +14022,6 @@
|
|||||||
"microevent.ts": "~0.1.1"
|
"microevent.ts": "~0.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"workerize-loader": {
|
|
||||||
"version": "1.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/workerize-loader/-/workerize-loader-1.3.0.tgz",
|
|
||||||
"integrity": "sha512-utWDc8K6embcICmRBUUkzanPgKBb8yM1OHfh6siZfiMsswE8wLCa9CWS+L7AARz0+Th4KH4ZySrqer/OJ9WuWw==",
|
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
|
||||||
"loader-utils": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"wrap-ansi": {
|
"wrap-ansi": {
|
||||||
"version": "5.1.0",
|
"version": "5.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
|
||||||
|
@ -26,9 +26,8 @@
|
|||||||
"metaflac-js": "^1.0.5",
|
"metaflac-js": "^1.0.5",
|
||||||
"music-metadata-browser": "^2.2.6",
|
"music-metadata-browser": "^2.2.6",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"vue": "^2.6.12",
|
"threads": "^1.6.4",
|
||||||
"vue-class-component": "^7.2.3",
|
"vue": "^2.6.12"
|
||||||
"vue-property-decorator": "^9.1.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/crypto-js": "^4.0.1",
|
"@types/crypto-js": "^4.0.1",
|
||||||
@ -40,9 +39,9 @@
|
|||||||
"node-sass": "^5.0.0",
|
"node-sass": "^5.0.0",
|
||||||
"sass-loader": "^10.2.0",
|
"sass-loader": "^10.2.0",
|
||||||
"semver": "^7.3.5",
|
"semver": "^7.3.5",
|
||||||
|
"threads-plugin": "^1.4.0",
|
||||||
"typescript": "~4.1.5",
|
"typescript": "~4.1.5",
|
||||||
"vue-cli-plugin-element": "^1.0.1",
|
"vue-cli-plugin-element": "^1.0.1",
|
||||||
"vue-template-compiler": "^2.6.12",
|
"vue-template-compiler": "^2.6.12"
|
||||||
"workerize-loader": "^1.3.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
226
src/App.vue
226
src/App.vue
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<el-container id="app">
|
<el-container id="app">
|
||||||
<el-main>
|
<el-main>
|
||||||
<x-upload v-on:handle_error="showFail" v-on:handle_finish="showSuccess"></x-upload>
|
<file-selector @error="showFail" @success="showSuccess"/>
|
||||||
|
|
||||||
<div id="app-control">
|
<div id="app-control">
|
||||||
<el-row class="mb-3">
|
<el-row class="mb-3">
|
||||||
@ -28,14 +28,14 @@
|
|||||||
|
|
||||||
<audio :autoplay="playing_auto" :src="playing_url" controls/>
|
<audio :autoplay="playing_auto" :src="playing_url" controls/>
|
||||||
|
|
||||||
<x-preview :download_format="download_format" :table-data="tableData"
|
<PreviewTable :filename_format="download_format" :table-data="tableData"
|
||||||
v-on:music_changed="changePlaying"></x-preview>
|
@music_changed="changePlaying"></PreviewTable>
|
||||||
|
|
||||||
</el-main>
|
</el-main>
|
||||||
<el-footer id="app-footer">
|
<el-footer id="app-footer">
|
||||||
<el-row>
|
<el-row>
|
||||||
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>(v<span
|
<a href="https://github.com/ix64/unlock-music" target="_blank">音乐解锁</a>
|
||||||
v-text="version"></span>):移除已购音乐的加密保护。
|
(v{{ 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>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
@ -44,7 +44,7 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上-->
|
<!--如果进行二次开发,此行版权信息不得移除且应明显地标注于页面上-->
|
||||||
<span>Copyright © 2019-</span><span v-text="(new Date()).getFullYear()"></span> MengYX
|
<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>
|
||||||
开放源代码
|
开放源代码
|
||||||
@ -56,50 +56,50 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
import upload from "./component/upload"
|
import FileSelector from "./component/FileSelector"
|
||||||
import preview from "./component/preview"
|
import PreviewTable from "./component/PreviewTable"
|
||||||
import {DownloadBlobMusic, RemoveBlobMusic} from "./component/util"
|
import {DownloadBlobMusic, RemoveBlobMusic} from "./component/utils"
|
||||||
import config from "../package"
|
import config from "../package"
|
||||||
import {IXAREA_API_ENDPOINT} from "@/decrypt/utils";
|
import {IXAREA_API_ENDPOINT} from "@/decrypt/utils";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'app',
|
name: 'app',
|
||||||
components: {
|
components: {
|
||||||
xUpload: upload,
|
FileSelector,
|
||||||
xPreview: preview
|
PreviewTable
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
version: config.version,
|
version: config.version,
|
||||||
activeIndex: '1',
|
activeIndex: '1',
|
||||||
tableData: [],
|
tableData: [],
|
||||||
playing_url: "",
|
playing_url: "",
|
||||||
playing_auto: false,
|
playing_auto: false,
|
||||||
download_format: '1',
|
download_format: '1',
|
||||||
instant_download: false,
|
instant_download: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$nextTick(function () {
|
||||||
|
this.finishLoad();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async finishLoad() {
|
||||||
|
const mask = document.getElementById("loader-mask");
|
||||||
|
if (!!mask) mask.remove();
|
||||||
|
let updateInfo;
|
||||||
|
try {
|
||||||
|
const resp = await fetch(IXAREA_API_ENDPOINT + "/music/app-version", {
|
||||||
|
method: "POST", headers: {"Content-Type": "application/json"},
|
||||||
|
body: JSON.stringify({"Version": this.version})
|
||||||
|
});
|
||||||
|
updateInfo = await resp.json();
|
||||||
|
} catch (e) {
|
||||||
}
|
}
|
||||||
},
|
if ((!!updateInfo && process.env.NODE_ENV === 'production') && (!!updateInfo.HttpsFound ||
|
||||||
created() {
|
(!!updateInfo.Found && window.location.protocol !== "https:"))) {
|
||||||
this.$nextTick(function () {
|
this.$notify.warning({
|
||||||
this.finishLoad();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async finishLoad() {
|
|
||||||
const mask = document.getElementById("loader-mask");
|
|
||||||
if (!!mask) mask.remove();
|
|
||||||
let updateInfo;
|
|
||||||
try {
|
|
||||||
const resp = await fetch(IXAREA_API_ENDPOINT + "/music/app-version", {
|
|
||||||
method: "POST", headers: {"Content-Type": "application/json"},
|
|
||||||
body: JSON.stringify({"Version": this.version})
|
|
||||||
});
|
|
||||||
updateInfo = await resp.json();
|
|
||||||
} catch (e) {
|
|
||||||
}
|
|
||||||
if ((!!updateInfo && process.env.NODE_ENV === 'production') && (!!updateInfo.HttpsFound ||
|
|
||||||
(!!updateInfo.Found && window.location.protocol !== "https:"))) {
|
|
||||||
this.$notify.warning({
|
|
||||||
title: '发现更新',
|
title: '发现更新',
|
||||||
message: '发现新版本 v' + updateInfo.Version +
|
message: '发现新版本 v' + updateInfo.Version +
|
||||||
'<br/>更新详情:' + updateInfo.Detail +
|
'<br/>更新详情:' + updateInfo.Detail +
|
||||||
@ -107,78 +107,74 @@
|
|||||||
dangerouslyUseHTMLString: true,
|
dangerouslyUseHTMLString: true,
|
||||||
duration: 15000,
|
duration: 15000,
|
||||||
position: 'top-left'
|
position: 'top-left'
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.$notify.info({
|
this.$notify.info({
|
||||||
title: '离线使用',
|
title: '离线使用',
|
||||||
message: '我们使用PWA技术,无网络也能使用' +
|
message: '我们使用PWA技术,无网络也能使用' +
|
||||||
'<br/>最近更新:' + config.updateInfo +
|
'<br/>最近更新:' + config.updateInfo +
|
||||||
'<br/><a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
|
'<br/><a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
|
||||||
dangerouslyUseHTMLString: true,
|
|
||||||
duration: 10000,
|
|
||||||
position: 'top-left'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showSuccess(data) {
|
|
||||||
if (data.status) {
|
|
||||||
if (this.instant_download) {
|
|
||||||
DownloadBlobMusic(data, this.download_format);
|
|
||||||
RemoveBlobMusic(data);
|
|
||||||
} else {
|
|
||||||
this.tableData.push(data);
|
|
||||||
this.$notify.success({
|
|
||||||
title: '解锁成功',
|
|
||||||
message: '成功解锁 ' + data.title,
|
|
||||||
duration: 3000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
let _rp_data = [data.title, data.artist, data.album];
|
|
||||||
window._paq.push(["trackEvent", "Unlock", data.rawExt + "," + data.mime, JSON.stringify(_rp_data)]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.showFail(data.message, data.rawFilename + "." + data.rawExt)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showFail(errInfo, filename) {
|
|
||||||
this.$notify.error({
|
|
||||||
title: '出现问题',
|
|
||||||
message: errInfo + "," + filename +
|
|
||||||
',参考<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
|
|
||||||
dangerouslyUseHTMLString: true,
|
dangerouslyUseHTMLString: true,
|
||||||
duration: 6000
|
duration: 10000,
|
||||||
|
position: 'top-left'
|
||||||
});
|
});
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
window._paq.push(["trackEvent", "Error", errInfo, filename]);
|
|
||||||
}
|
|
||||||
console.error(errInfo, filename);
|
|
||||||
},
|
|
||||||
changePlaying(url) {
|
|
||||||
this.playing_url = url;
|
|
||||||
this.playing_auto = true;
|
|
||||||
},
|
|
||||||
handleDeleteAll() {
|
|
||||||
this.tableData.forEach(value => {
|
|
||||||
RemoveBlobMusic(value);
|
|
||||||
});
|
|
||||||
this.tableData = [];
|
|
||||||
},
|
|
||||||
handleDownloadAll() {
|
|
||||||
let index = 0;
|
|
||||||
let c = setInterval(() => {
|
|
||||||
if (index < this.tableData.length) {
|
|
||||||
DownloadBlobMusic(this.tableData[index], this.download_format);
|
|
||||||
index++;
|
|
||||||
} else {
|
|
||||||
clearInterval(c);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
showSuccess(data) {
|
||||||
|
if (this.instant_download) {
|
||||||
|
DownloadBlobMusic(data, this.download_format);
|
||||||
|
RemoveBlobMusic(data);
|
||||||
|
} else {
|
||||||
|
this.tableData.push(data);
|
||||||
|
this.$notify.success({
|
||||||
|
title: '解锁成功',
|
||||||
|
message: '成功解锁 ' + data.title,
|
||||||
|
duration: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
let _rp_data = [data.title, data.artist, data.album];
|
||||||
|
window._paq.push(["trackEvent", "Unlock", data.rawExt + "," + data.mime, JSON.stringify(_rp_data)]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showFail(errInfo, filename) {
|
||||||
|
this.$notify.error({
|
||||||
|
title: '出现问题',
|
||||||
|
message: errInfo + "," + filename +
|
||||||
|
',参考<a target="_blank" href="https://github.com/ix64/unlock-music/wiki/使用提示">使用提示</a>',
|
||||||
|
dangerouslyUseHTMLString: true,
|
||||||
|
duration: 6000
|
||||||
|
});
|
||||||
|
if (process.env.NODE_ENV === 'production') {
|
||||||
|
window._paq.push(["trackEvent", "Error", errInfo, filename]);
|
||||||
|
}
|
||||||
|
console.error(errInfo, filename);
|
||||||
|
},
|
||||||
|
changePlaying(url) {
|
||||||
|
this.playing_url = url;
|
||||||
|
this.playing_auto = true;
|
||||||
|
},
|
||||||
|
handleDeleteAll() {
|
||||||
|
this.tableData.forEach(value => {
|
||||||
|
RemoveBlobMusic(value);
|
||||||
|
});
|
||||||
|
this.tableData = [];
|
||||||
|
},
|
||||||
|
handleDownloadAll() {
|
||||||
|
let index = 0;
|
||||||
|
let c = setInterval(() => {
|
||||||
|
if (index < this.tableData.length) {
|
||||||
|
DownloadBlobMusic(this.tableData[index], this.download_format);
|
||||||
|
index++;
|
||||||
|
} else {
|
||||||
|
clearInterval(c);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import "scss/unlock-music";
|
@import "scss/unlock-music";
|
||||||
</style>
|
</style>
|
||||||
|
77
src/component/FileSelector.vue
Normal file
77
src/component/FileSelector.vue
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<el-upload
|
||||||
|
:auto-upload="false"
|
||||||
|
:on-change="addFile"
|
||||||
|
:show-file-list="false"
|
||||||
|
action=""
|
||||||
|
drag
|
||||||
|
multiple>
|
||||||
|
<i class="el-icon-upload"/>
|
||||||
|
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
|
||||||
|
<div slot="tip" class="el-upload__tip">本工具仅在浏览器内对文件进行解锁,无需消耗流量</div>
|
||||||
|
<transition name="el-fade-in"><!--todo: add delay to animation-->
|
||||||
|
<el-progress
|
||||||
|
v-show="progress_show" :format="progress_string" :percentage="progress_value"
|
||||||
|
:stroke-width="16" :text-inside="true"
|
||||||
|
style="margin: 16px 6px 0 6px"
|
||||||
|
></el-progress>
|
||||||
|
</transition>
|
||||||
|
</el-upload>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {spawn, Worker, Pool} from "threads"
|
||||||
|
import {CommonDecrypt} from "@/decrypt/common.ts";
|
||||||
|
import {DecryptQueue} from "@/component/utils";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "FileSelector",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
task_all: 0,
|
||||||
|
task_finished: 0,
|
||||||
|
queue: new DecryptQueue() // for http or file protocol
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
progress_value() {
|
||||||
|
return this.task_all ? this.task_finished / this.task_all * 100 : 0
|
||||||
|
},
|
||||||
|
progress_show() {
|
||||||
|
return this.task_all !== this.task_finished
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (window.Worker) {
|
||||||
|
console.log("Using Worker Pool")
|
||||||
|
this.queue = Pool(
|
||||||
|
() => spawn(new Worker('@/component/worker.ts')),
|
||||||
|
navigator.hardwareConcurrency || 1
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.log("Using Queue in Main Thread")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
progress_string() {
|
||||||
|
return `${this.task_finished} / ${this.task_all}`
|
||||||
|
},
|
||||||
|
async addFile(file) {
|
||||||
|
this.task_all++
|
||||||
|
this.queue.queue(async (dec = CommonDecrypt) => {
|
||||||
|
console.log("start handling", file.name)
|
||||||
|
try {
|
||||||
|
this.$emit("success", await dec(file));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
this.$emit("error", file)
|
||||||
|
} finally {
|
||||||
|
this.task_finished++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
@ -4,7 +4,7 @@
|
|||||||
<el-table-column label="封面">
|
<el-table-column label="封面">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
|
<el-image :src="scope.row.picture" style="width: 100px; height: 100px">
|
||||||
<div class="image-slot el-image__error" slot="error">
|
<div slot="error" class="image-slot el-image__error">
|
||||||
暂无封面
|
暂无封面
|
||||||
</div>
|
</div>
|
||||||
</el-image>
|
</el-image>
|
||||||
@ -27,14 +27,14 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作">
|
<el-table-column label="操作">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button @click="handlePlay(scope.$index, scope.row)"
|
<el-button circle
|
||||||
circle icon="el-icon-video-play" type="success">
|
icon="el-icon-video-play" type="success" @click="handlePlay(scope.$index, scope.row)">
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="handleDownload(scope.row)"
|
<el-button circle
|
||||||
circle icon="el-icon-download">
|
icon="el-icon-download" @click="handleDownload(scope.row)">
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button @click="handleDelete(scope.$index, scope.row)"
|
<el-button circle
|
||||||
circle icon="el-icon-delete" type="danger">
|
icon="el-icon-delete" type="danger" @click="handleDelete(scope.$index, scope.row)">
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@ -42,28 +42,28 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {DownloadBlobMusic, RemoveBlobMusic} from './util'
|
import {DownloadBlobMusic, RemoveBlobMusic} from '@/component/utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "preview",
|
name: "PreviewTable",
|
||||||
props: {
|
props: {
|
||||||
tableData: {type: Array, required: true},
|
tableData: {type: Array, required: true},
|
||||||
download_format: {type: String, required: true}
|
filename_format: {type: String, required: true}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
handlePlay(index, row) {
|
||||||
|
this.$emit("music_changed", row.file);
|
||||||
|
},
|
||||||
|
handleDelete(index, row) {
|
||||||
|
RemoveBlobMusic(row);
|
||||||
|
this.tableData.splice(index, 1);
|
||||||
|
},
|
||||||
|
handleDownload(row) {
|
||||||
|
DownloadBlobMusic(row, this.download_format)
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
|
||||||
handlePlay(index, row) {
|
|
||||||
this.$emit("music_changed", row.file);
|
|
||||||
},
|
|
||||||
handleDelete(index, row) {
|
|
||||||
RemoveBlobMusic(row);
|
|
||||||
this.tableData.splice(index, 1);
|
|
||||||
},
|
|
||||||
handleDownload(row) {
|
|
||||||
DownloadBlobMusic(row, this.download_format)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
@ -1,120 +0,0 @@
|
|||||||
<template>
|
|
||||||
<el-upload
|
|
||||||
:auto-upload="false"
|
|
||||||
:on-change="handleFile"
|
|
||||||
:show-file-list="false"
|
|
||||||
action=""
|
|
||||||
drag
|
|
||||||
multiple>
|
|
||||||
<i class="el-icon-upload"/>
|
|
||||||
<div class="el-upload__text">将文件拖到此处,或<em>点击选择</em></div>
|
|
||||||
<div class="el-upload__tip" slot="tip">本工具仅在浏览器内对文件进行解锁,无需消耗流量</div>
|
|
||||||
<transition name="el-fade-in">
|
|
||||||
<el-progress
|
|
||||||
:format="progressFormat" :percentage="progress_percent" :stroke-width="16"
|
|
||||||
:text-inside="true" style="margin: 16px 6px 0 6px"
|
|
||||||
v-show="progress_show"
|
|
||||||
></el-progress>
|
|
||||||
</transition>
|
|
||||||
</el-upload>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
"use strict";// 严格模式 用于尾调用优化
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: "upload",
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
cacheQueue: [],
|
|
||||||
workers: [],
|
|
||||||
idle_workers: [],
|
|
||||||
thread_num: 1,
|
|
||||||
|
|
||||||
progress_show: false,
|
|
||||||
|
|
||||||
progress_finished: 0,
|
|
||||||
progress_all: 0,
|
|
||||||
progress_percent: 0,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (document.location.host !== "" && process.env.NODE_ENV === 'production') {
|
|
||||||
this.thread_num = navigator.hardwareConcurrency || 1;
|
|
||||||
const worker = require("workerize-loader!../decrypt/common");
|
|
||||||
// noinspection JSValidateTypes,JSUnresolvedVariable
|
|
||||||
this.workers.push(worker().CommonDecrypt);
|
|
||||||
this.idle_workers.push(0);
|
|
||||||
// delay to optimize for first loading
|
|
||||||
setTimeout(() => {
|
|
||||||
for (let i = 1; i < this.thread_num; i++) {
|
|
||||||
// noinspection JSValidateTypes,JSUnresolvedVariable
|
|
||||||
this.workers.push(worker().CommonDecrypt);
|
|
||||||
this.idle_workers.push(i);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
} else {
|
|
||||||
const dec = require('../decrypt/common');
|
|
||||||
this.workers.push(dec.CommonDecrypt);
|
|
||||||
this.idle_workers.push(0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
progressFormat() {
|
|
||||||
return this.progress_finished + "/" + (this.progress_all)
|
|
||||||
},
|
|
||||||
progressChange(finish, all) {
|
|
||||||
this.progress_all += all;
|
|
||||||
this.progress_finished += finish;
|
|
||||||
this.progress_percent = Math.round(this.progress_finished / this.progress_all * 100);
|
|
||||||
if (this.progress_finished === this.progress_all) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.progress_show = false;
|
|
||||||
this.progress_finished = 0;
|
|
||||||
this.progress_all = 0;
|
|
||||||
}, 3000);
|
|
||||||
} else {
|
|
||||||
this.progress_show = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleFile(file) {
|
|
||||||
this.progressChange(0, +1);
|
|
||||||
// 有空闲worker 立刻处理文件
|
|
||||||
if (this.idle_workers.length > 0) {
|
|
||||||
this.handleDoFile(file, this.idle_workers.shift());
|
|
||||||
}
|
|
||||||
// 无空闲worker 则放入缓存队列
|
|
||||||
else {
|
|
||||||
this.cacheQueue.push(file);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleCacheQueue(worker_id) {
|
|
||||||
// 调用方法消费缓存队列中的数据
|
|
||||||
if (this.cacheQueue.length === 0) {
|
|
||||||
this.idle_workers.push(worker_id);
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.handleDoFile(this.cacheQueue.shift(), worker_id);
|
|
||||||
},
|
|
||||||
handleDoFile(file, worker_id) {
|
|
||||||
this.workers[worker_id](file).then(data => {
|
|
||||||
this.$emit("handle_finish", data);
|
|
||||||
// 完成之后 执行新任务 todo: 可能导致call stack过长
|
|
||||||
this.handleCacheQueue(worker_id);
|
|
||||||
this.progressChange(+1, 0);
|
|
||||||
}).catch(err => {
|
|
||||||
this.$emit("handle_error", err, file.name);
|
|
||||||
this.handleCacheQueue(worker_id);
|
|
||||||
this.progressChange(+1, 0);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/*noinspection CssUnusedSymbol*/
|
|
||||||
.el-upload-dragger {
|
|
||||||
width: 80vw !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -1,4 +1,6 @@
|
|||||||
export function DownloadBlobMusic(data, format) {
|
import {DecryptResult} from "@/decrypt/entity";
|
||||||
|
|
||||||
|
export function DownloadBlobMusic(data: DecryptResult, format: string) {//todo: use enum
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = data.file;
|
a.href = data.file;
|
||||||
switch (format) {
|
switch (format) {
|
||||||
@ -21,10 +23,27 @@ export function DownloadBlobMusic(data, format) {
|
|||||||
a.remove();
|
a.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RemoveBlobMusic(data) {
|
export function RemoveBlobMusic(data: DecryptResult) {
|
||||||
URL.revokeObjectURL(data.file);
|
URL.revokeObjectURL(data.file);
|
||||||
if (data.picture.startsWith("blob:")) {
|
if (data.picture?.startsWith("blob:")) {
|
||||||
URL.revokeObjectURL(data.picture);
|
URL.revokeObjectURL(data.picture);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DecryptQueue {
|
||||||
|
private readonly pending: (() => Promise<void>)[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.pending = []
|
||||||
|
}
|
||||||
|
|
||||||
|
queue(fn: () => Promise<void>) {
|
||||||
|
this.pending.push(fn)
|
||||||
|
this.consume()
|
||||||
|
}
|
||||||
|
|
||||||
|
private consume() {
|
||||||
|
const fn = this.pending.shift()
|
||||||
|
if (fn) fn().then(() => this.consume).catch(console.error)
|
||||||
|
}
|
||||||
}
|
}
|
4
src/component/worker.ts
Normal file
4
src/component/worker.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import {expose} from "threads/worker";
|
||||||
|
import {CommonDecrypt} from "@/decrypt/common";
|
||||||
|
|
||||||
|
expose(CommonDecrypt)
|
@ -1,21 +1,11 @@
|
|||||||
import {Decrypt as NcmDecrypt} from "./ncm";
|
import {Decrypt as NcmDecrypt} from "@/decrypt/ncm";
|
||||||
import {Decrypt as XmDecrypt} from "./xm";
|
import {Decrypt as XmDecrypt} from "@/decrypt/xm";
|
||||||
import {Decrypt as QmcDecrypt} from "./qmc";
|
import {Decrypt as QmcDecrypt} from "@/decrypt/qmc";
|
||||||
import {Decrypt as KgmDecrypt} from "./kgm";
|
import {Decrypt as KgmDecrypt} from "@/decrypt/kgm";
|
||||||
import {Decrypt as KwmDecrypt} from "@/decrypt/kwm";
|
import {Decrypt as KwmDecrypt} from "@/decrypt/kwm";
|
||||||
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
|
import {Decrypt as RawDecrypt} from "@/decrypt/raw";
|
||||||
import {Decrypt as TmDecrypt} from "@/decrypt/tm";
|
import {Decrypt as TmDecrypt} from "@/decrypt/tm";
|
||||||
import {DecryptResult} from "@/decrypt/entity";
|
import {DecryptResult, FileInfo} from "@/decrypt/entity";
|
||||||
|
|
||||||
|
|
||||||
interface FileInfo {
|
|
||||||
status: string
|
|
||||||
name: string,
|
|
||||||
size: number,
|
|
||||||
percentage: number,
|
|
||||||
uid: number,
|
|
||||||
raw: File
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
|
export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
|
||||||
@ -78,3 +68,4 @@ export async function CommonDecrypt(file: FileInfo): Promise<DecryptResult> {
|
|||||||
console.log(rt_data);
|
console.log(rt_data);
|
||||||
return rt_data;
|
return rt_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,3 +14,12 @@ export interface DecryptResult {
|
|||||||
rawFilename?: string
|
rawFilename?: string
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
status: string
|
||||||
|
name: string,
|
||||||
|
size: number,
|
||||||
|
percentage: number,
|
||||||
|
uid: number,
|
||||||
|
raw: File
|
||||||
|
}
|
||||||
|
@ -136,7 +136,7 @@ async function queryKeyInfo(keyData: Uint8Array, filename: string, format: strin
|
|||||||
let data = await resp.json();
|
let data = await resp.json();
|
||||||
return new QmcMask(Base64Decode(data.Matrix44));
|
return new QmcMask(Base64Decode(data.Matrix44));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ async function queryAlbumCoverImage(title: string, artist?: string, album?: stri
|
|||||||
return song_query_url + "/" + data.Type + "/" + data.Id
|
return song_query_url + "/" + data.Type + "/" + data.Id
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e);
|
console.warn(e);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,6 @@ export function QmcMaskDetectMgg(data: Uint8Array) {
|
|||||||
|
|
||||||
|
|
||||||
function calcMaskFromConfidence(confidence: { [key: number]: number }) {
|
function calcMaskFromConfidence(confidence: { [key: number]: number }) {
|
||||||
console.log(confidence)
|
|
||||||
const count = Object.keys(confidence).length
|
const count = Object.keys(confidence).length
|
||||||
if (count === 0) throw "can not match at least one key";
|
if (count === 0) throw "can not match at least one key";
|
||||||
if (count > 1) console.warn("There are 2 potential value for the mask!")
|
if (count > 1) console.warn("There are 2 potential value for the mask!")
|
||||||
|
@ -42,7 +42,7 @@ export async function Decrypt(file: File, raw_filename: string, raw_ext: string)
|
|||||||
const musicMeta = await metaParseBlob(musicBlob);
|
const musicMeta = await metaParseBlob(musicBlob);
|
||||||
if (ext === "wav") {
|
if (ext === "wav") {
|
||||||
//todo:未知的编码方式
|
//todo:未知的编码方式
|
||||||
console.log(musicMeta.common)
|
console.info(musicMeta.common)
|
||||||
musicMeta.common.album = "";
|
musicMeta.common.album = "";
|
||||||
musicMeta.common.artist = "";
|
musicMeta.common.artist = "";
|
||||||
musicMeta.common.title = "";
|
musicMeta.common.title = "";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import App from './App.vue'
|
import App from '@/App.vue'
|
||||||
import './registerServiceWorker'
|
import '@/registerServiceWorker'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "esnext",
|
"target": "es5",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
const ThreadsPlugin = require('threads-plugin');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
publicPath: '',
|
publicPath: '',
|
||||||
productionSourceMap: false,
|
productionSourceMap: false,
|
||||||
@ -35,5 +36,8 @@ module.exports = {
|
|||||||
workboxOptions: {
|
workboxOptions: {
|
||||||
skipWaiting: true
|
skipWaiting: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
configureWebpack: {
|
||||||
|
plugins: [new ThreadsPlugin()]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user