优化下载列表流畅度
parent
d8a3466914
commit
015c17e2dc
|
@ -21,13 +21,13 @@ const state = {
|
||||||
|
|
||||||
const dls = {}
|
const dls = {}
|
||||||
const tryNum = {}
|
const tryNum = {}
|
||||||
|
let isRuningActionTask = false
|
||||||
|
|
||||||
const filterFileName = /[\\/:*?#"<>|]/g
|
const filterFileName = /[\\/:*?#"<>|]/g
|
||||||
|
|
||||||
// getters
|
// getters
|
||||||
const getters = {
|
const getters = {
|
||||||
list: state => state.list || [],
|
list: state => state.list || [],
|
||||||
dls: () => dls || {},
|
|
||||||
downloadStatus: state => state.downloadStatus,
|
downloadStatus: state => state.downloadStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,6 +57,8 @@ const getExt = type => {
|
||||||
return 'ape'
|
return 'ape'
|
||||||
case 'flac':
|
case 'flac':
|
||||||
return 'flac'
|
return 'flac'
|
||||||
|
case 'wav':
|
||||||
|
return 'wav'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,15 +71,79 @@ const getStartTask = (list, downloadStatus, maxDownloadNum) => {
|
||||||
return downloadCount < maxDownloadNum && waitList.length > 0 && waitList.shift()
|
return downloadCount < maxDownloadNum && waitList.length > 0 && waitList.shift()
|
||||||
}
|
}
|
||||||
|
|
||||||
const addTask = (list, type, store) => {
|
const awaitRequestAnimationFrame = () => new Promise(resolve => window.requestAnimationFrame(() => resolve()))
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
|
const addTasks = async(store, list, type) => {
|
||||||
|
if (list.length == 0) return
|
||||||
|
let num = 5
|
||||||
|
while (num-- > 0) {
|
||||||
let item = list.shift()
|
let item = list.shift()
|
||||||
store.dispatch('download/createDownload', {
|
if (!item) return
|
||||||
|
await store.dispatch('createDownload', {
|
||||||
musicInfo: item,
|
musicInfo: item,
|
||||||
type: getMusicType(item, type),
|
type: getMusicType(item, type),
|
||||||
})
|
})
|
||||||
if (list.length) addTask(list, type, store)
|
}
|
||||||
})
|
await awaitRequestAnimationFrame()
|
||||||
|
await addTasks(store, list, type)
|
||||||
|
}
|
||||||
|
const removeTasks = async(store, list) => {
|
||||||
|
let num = 50
|
||||||
|
while (num-- > 0) {
|
||||||
|
let item = list.pop()
|
||||||
|
if (!item) return
|
||||||
|
let index = store.state.list.indexOf(item)
|
||||||
|
if (index < 0) continue
|
||||||
|
await store.dispatch('removeTask', index)
|
||||||
|
}
|
||||||
|
await awaitRequestAnimationFrame()
|
||||||
|
await removeTasks(store, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTasks = async(store, list) => {
|
||||||
|
let num = 10
|
||||||
|
while (num-- > 0) {
|
||||||
|
let item = list.shift()
|
||||||
|
if (!item) return
|
||||||
|
if (item.isComplate || item.status == state.downloadStatus.RUN || item.status == state.downloadStatus.WAITING) continue
|
||||||
|
let index = store.state.list.indexOf(item)
|
||||||
|
if (index < 0) continue
|
||||||
|
store.dispatch('startTask', item)
|
||||||
|
}
|
||||||
|
await awaitRequestAnimationFrame()
|
||||||
|
await startTasks(store, list)
|
||||||
|
}
|
||||||
|
|
||||||
|
const pauseTasks = async(store, list, runs = []) => {
|
||||||
|
let num = 10
|
||||||
|
let index
|
||||||
|
let stateList = store.state.list
|
||||||
|
while (num-- > 0) {
|
||||||
|
let item = list.shift()
|
||||||
|
if (item.isComplate) continue
|
||||||
|
if (item) {
|
||||||
|
switch (item.status) {
|
||||||
|
case state.downloadStatus.RUN:
|
||||||
|
runs.push(item)
|
||||||
|
continue
|
||||||
|
case state.downloadStatus.WAITING:
|
||||||
|
index = stateList.indexOf(item)
|
||||||
|
if (index < 0) return
|
||||||
|
await store.dispatch('pauseTask', index)
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const item of runs) {
|
||||||
|
index = stateList.indexOf(item)
|
||||||
|
if (index < 0) return
|
||||||
|
await store.dispatch('pauseTask', index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await awaitRequestAnimationFrame()
|
||||||
|
await pauseTasks(store, list, runs)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUrl = (downloadInfo, isRefresh) => {
|
const getUrl = (downloadInfo, isRefresh) => {
|
||||||
|
@ -142,7 +208,11 @@ const refreshUrl = function(commit, downloadInfo) {
|
||||||
const dl = dls[downloadInfo.key]
|
const dl = dls[downloadInfo.key]
|
||||||
if (!dl) return
|
if (!dl) return
|
||||||
dl.refreshUrl(result.url)
|
dl.refreshUrl(result.url)
|
||||||
dl.start()
|
dl.start().catch(err => {
|
||||||
|
commit('onError', downloadInfo)
|
||||||
|
commit('setStatusText', { downloadInfo, text: err.message })
|
||||||
|
this.dispatch('download/startTask')
|
||||||
|
})
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
// console.log(err)
|
// console.log(err)
|
||||||
commit('onError', downloadInfo)
|
commit('onError', downloadInfo)
|
||||||
|
@ -168,7 +238,7 @@ const deleteFile = path => new Promise((resolve, reject) => {
|
||||||
|
|
||||||
// actions
|
// actions
|
||||||
const actions = {
|
const actions = {
|
||||||
async createDownload({ state, rootState, commit }, { musicInfo, type }) {
|
async createDownload({ state, rootState, commit, dispatch }, { musicInfo, type }) {
|
||||||
let ext = getExt(type)
|
let ext = getExt(type)
|
||||||
if (checkList(state.list, musicInfo, type, ext)) return
|
if (checkList(state.list, musicInfo, type, ext)) return
|
||||||
const downloadInfo = {
|
const downloadInfo = {
|
||||||
|
@ -199,28 +269,21 @@ const actions = {
|
||||||
if (dls[downloadInfo.key]) {
|
if (dls[downloadInfo.key]) {
|
||||||
dls[downloadInfo.key].stop().finally(() => {
|
dls[downloadInfo.key].stop().finally(() => {
|
||||||
delete dls[downloadInfo.key]
|
delete dls[downloadInfo.key]
|
||||||
this.dispatch('download/startTask', downloadInfo)
|
dispatch('startTask', downloadInfo)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// console.log(downloadInfo)
|
// console.log(downloadInfo)
|
||||||
this.dispatch('download/startTask', downloadInfo)
|
dispatch('startTask', downloadInfo)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createDownloadMultiple({ state, rootState }, { list, type }) {
|
async createDownloadMultiple(store, { list, type }) {
|
||||||
if (!list.length) return
|
if (!list.length) return
|
||||||
addTask([...list], type, this)
|
if (isRuningActionTask) return
|
||||||
|
isRuningActionTask = true
|
||||||
|
await addTasks(store, [...list], type)
|
||||||
|
isRuningActionTask = false
|
||||||
},
|
},
|
||||||
async startTask({ commit, state, rootState }, downloadInfo) {
|
async handleStartTask({ commit, dispatch, rootState }, downloadInfo) {
|
||||||
// 检查是否可以开始任务
|
|
||||||
if (downloadInfo && downloadInfo != state.downloadStatus.WAITING) commit('setStatus', { downloadInfo, status: state.downloadStatus.WAITING })
|
|
||||||
let result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
|
|
||||||
if (!result) return
|
|
||||||
if (!downloadInfo) downloadInfo = result
|
|
||||||
commit('updateFilePath', {
|
|
||||||
downloadInfo,
|
|
||||||
filePath: path.join(rootState.setting.download.savePath, downloadInfo.fileName),
|
|
||||||
})
|
|
||||||
|
|
||||||
// 开始任务
|
// 开始任务
|
||||||
commit('onStart', downloadInfo)
|
commit('onStart', downloadInfo)
|
||||||
commit('setStatusText', { downloadInfo, text: '任务初始化中' })
|
commit('setStatusText', { downloadInfo, text: '任务初始化中' })
|
||||||
|
@ -239,10 +302,10 @@ const actions = {
|
||||||
onCompleted() {
|
onCompleted() {
|
||||||
// if (downloadInfo.progress.progress != '100.00') {
|
// if (downloadInfo.progress.progress != '100.00') {
|
||||||
// delete dls[downloadInfo.key]
|
// delete dls[downloadInfo.key]
|
||||||
// return this.dispatch('download/startTask', downloadInfo)
|
// return dispatch('startTask', downloadInfo)
|
||||||
// }
|
// }
|
||||||
commit('onCompleted', downloadInfo)
|
commit('onCompleted', downloadInfo)
|
||||||
_this.dispatch('download/startTask')
|
dispatch('startTask')
|
||||||
|
|
||||||
saveMeta(downloadInfo, downloadInfo.filePath, rootState.setting.download.isEmbedPic)
|
saveMeta(downloadInfo, downloadInfo.filePath, rootState.setting.download.isEmbedPic)
|
||||||
if (rootState.setting.download.isDownloadLrc) downloadLyric(downloadInfo, downloadInfo.filePath)
|
if (rootState.setting.download.isDownloadLrc) downloadLyric(downloadInfo, downloadInfo.filePath)
|
||||||
|
@ -252,7 +315,7 @@ const actions = {
|
||||||
// console.log(tryNum[downloadInfo.key])
|
// console.log(tryNum[downloadInfo.key])
|
||||||
if (++tryNum[downloadInfo.key] > 2) {
|
if (++tryNum[downloadInfo.key] > 2) {
|
||||||
commit('onError', downloadInfo)
|
commit('onError', downloadInfo)
|
||||||
_this.dispatch('download/startTask')
|
dispatch('startTask')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (err.code == 'ENOTFOUND') {
|
if (err.code == 'ENOTFOUND') {
|
||||||
|
@ -266,7 +329,7 @@ const actions = {
|
||||||
onFail(response) {
|
onFail(response) {
|
||||||
if (++tryNum[downloadInfo.key] > 2) {
|
if (++tryNum[downloadInfo.key] > 2) {
|
||||||
commit('onError', downloadInfo)
|
commit('onError', downloadInfo)
|
||||||
_this.dispatch('download/startTask')
|
dispatch('startTask')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch (response.statusCode) {
|
switch (response.statusCode) {
|
||||||
|
@ -286,7 +349,7 @@ const actions = {
|
||||||
},
|
},
|
||||||
onStop() {
|
onStop() {
|
||||||
commit('pauseTask', downloadInfo)
|
commit('pauseTask', downloadInfo)
|
||||||
_this.dispatch('download/startTask')
|
dispatch('startTask')
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
commit('setStatusText', { downloadInfo, text: '获取URL中...' })
|
commit('setStatusText', { downloadInfo, text: '获取URL中...' })
|
||||||
|
@ -302,47 +365,89 @@ const actions = {
|
||||||
// console.log(err.message)
|
// console.log(err.message)
|
||||||
commit('onError', downloadInfo)
|
commit('onError', downloadInfo)
|
||||||
commit('setStatusText', { downloadInfo, text: err.message })
|
commit('setStatusText', { downloadInfo, text: err.message })
|
||||||
this.dispatch('download/startTask')
|
dispatch('startTask')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
// startTaskMultiple({ state, rootState }, list) {
|
async removeTask({ commit, state, dispatch }, index) {
|
||||||
|
|
||||||
// },
|
|
||||||
removeTask({ commit, state }, index) {
|
|
||||||
let info = state.list[index]
|
let info = state.list[index]
|
||||||
if (state.list[index].status == state.downloadStatus.RUN) {
|
|
||||||
if (dls[info.key]) {
|
if (dls[info.key]) {
|
||||||
dls[info.key].stop().finally(() => {
|
if (info.status == state.downloadStatus.RUN) {
|
||||||
|
try {
|
||||||
|
await dls[info.key].stop()
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
delete dls[info.key]
|
delete dls[info.key]
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
commit('removeTask', index)
|
commit('removeTask', index)
|
||||||
if (dls[info.key]) delete dls[info.key]
|
if (info.status != state.downloadStatus.COMPLETED) {
|
||||||
;(info.status != state.downloadStatus.COMPLETED ? deleteFile(info.filePath) : Promise.resolve()).finally(() => {
|
try {
|
||||||
this.dispatch('download/startTask')
|
await deleteFile(info.filePath)
|
||||||
})
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
switch (info.status) {
|
||||||
|
case state.downloadStatus.RUN:
|
||||||
|
case state.downloadStatus.WAITING:
|
||||||
|
await dispatch('startTask')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async removeTaskMultiple({ commit, rootState, state }, list) {
|
async removeTasks(store, list) {
|
||||||
list.forEach(item => {
|
let { rootState, state } = store
|
||||||
let index = state.list.indexOf(item)
|
if (isRuningActionTask) return
|
||||||
if (index < 0) return
|
isRuningActionTask = true
|
||||||
// this.dispatch('download/removeTask', index)
|
await removeTasks(store, [...list])
|
||||||
if (state.list[index].status == state.downloadStatus.RUN) {
|
|
||||||
if (dls[item.key]) {
|
|
||||||
dls[item.key].stop().finally(() => {
|
|
||||||
delete dls[item.key]
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commit('removeTask', index)
|
|
||||||
if (dls[item.key]) delete dls[item.key]
|
|
||||||
})
|
|
||||||
let result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
|
let result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
|
||||||
while (result) {
|
while (result) {
|
||||||
this.dispatch('download/startTask', result)
|
store.dispatch('startTask', result)
|
||||||
result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
|
result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
|
||||||
}
|
}
|
||||||
|
isRuningActionTask = false
|
||||||
|
},
|
||||||
|
async startTask({ state, rootState, commit, dispatch }, downloadInfo) {
|
||||||
|
// 检查是否可以开始任务
|
||||||
|
let result = getStartTask(state.list, state.downloadStatus, rootState.setting.download.maxDownloadNum)
|
||||||
|
if (result) {
|
||||||
|
if (!downloadInfo || downloadInfo.isComplate || downloadInfo.status == state.downloadStatus.RUN) downloadInfo = result
|
||||||
|
} else {
|
||||||
|
if (downloadInfo) commit('setStatus', { downloadInfo, status: state.downloadStatus.WAITING })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let dl = dls[downloadInfo.key]
|
||||||
|
if (dl) {
|
||||||
|
commit('updateFilePath', {
|
||||||
|
downloadInfo,
|
||||||
|
filePath: path.join(rootState.setting.download.savePath, downloadInfo.fileName),
|
||||||
|
})
|
||||||
|
dl.updateSaveInfo(rootState.setting.download.savePath, downloadInfo.fileName)
|
||||||
|
try {
|
||||||
|
await dl.start()
|
||||||
|
} catch (_) {}
|
||||||
|
} else {
|
||||||
|
await dispatch('handleStartTask', downloadInfo)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async startTasks(store, list) {
|
||||||
|
if (isRuningActionTask) return
|
||||||
|
isRuningActionTask = true
|
||||||
|
await startTasks(store, [...list])
|
||||||
|
isRuningActionTask = false
|
||||||
|
},
|
||||||
|
async pauseTask({ state, commit }, index) {
|
||||||
|
let item = state.list[index]
|
||||||
|
if (item.isComplate) return
|
||||||
|
let dl = dls[item.key]
|
||||||
|
if (dl) {
|
||||||
|
try {
|
||||||
|
await dl.stop()
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
commit('pauseTask', item)
|
||||||
|
},
|
||||||
|
async pauseTasks(store, list) {
|
||||||
|
if (isRuningActionTask) return
|
||||||
|
isRuningActionTask = true
|
||||||
|
await pauseTasks(store, [...list])
|
||||||
|
isRuningActionTask = false
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,7 +79,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['setting']),
|
...mapGetters(['setting']),
|
||||||
...mapGetters('download', ['list', 'dls', 'downloadStatus']),
|
...mapGetters('download', ['list', 'downloadStatus']),
|
||||||
...mapGetters('player', ['listId', 'playIndex']),
|
...mapGetters('player', ['listId', 'playIndex']),
|
||||||
isPlayList() {
|
isPlayList() {
|
||||||
return this.listId == 'download'
|
return this.listId == 'download'
|
||||||
|
@ -126,9 +126,8 @@ export default {
|
||||||
this.unlistenEvent()
|
this.unlistenEvent()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('download', ['removeTask', 'removeTaskMultiple', 'startTask']),
|
...mapActions('download', ['removeTask', 'removeTasks', 'startTask', 'startTasks', 'pauseTask', 'pauseTasks']),
|
||||||
...mapMutations('player', ['setList']),
|
...mapMutations('player', ['setList']),
|
||||||
...mapMutations('download', ['pauseTask', 'updateFilePath']),
|
|
||||||
listenEvent() {
|
listenEvent() {
|
||||||
window.eventHub.$on('shift_down', this.handle_shift_down)
|
window.eventHub.$on('shift_down', this.handle_shift_down)
|
||||||
window.eventHub.$on('shift_up', this.handle_shift_up)
|
window.eventHub.$on('shift_up', this.handle_shift_up)
|
||||||
|
@ -166,27 +165,6 @@ export default {
|
||||||
handle_mod_a_up() {
|
handle_mod_a_up() {
|
||||||
if (this.keyEvent.isADown) this.keyEvent.isADown = false
|
if (this.keyEvent.isADown) this.keyEvent.isADown = false
|
||||||
},
|
},
|
||||||
handlePauseTask(index) {
|
|
||||||
let info = this.list[index]
|
|
||||||
let dl = this.dls[info.key]
|
|
||||||
dl ? dl.stop() : this.pauseTask(info)
|
|
||||||
console.log('pause')
|
|
||||||
},
|
|
||||||
handleStartTask(index) {
|
|
||||||
console.log('start')
|
|
||||||
let info = this.list[index]
|
|
||||||
let dl = this.dls[info.key]
|
|
||||||
if (dl) {
|
|
||||||
this.updateFilePath({
|
|
||||||
downloadInfo: info,
|
|
||||||
filePath: path.join(this.setting.download.savePath, info.fileName),
|
|
||||||
})
|
|
||||||
dl.updateSaveInfo(this.setting.download.savePath, info.fileName)
|
|
||||||
dl.start()
|
|
||||||
} else {
|
|
||||||
this.startTask(info)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
handleDoubleClick(event, index) {
|
handleDoubleClick(event, index) {
|
||||||
if (event.target.classList.contains('select')) return
|
if (event.target.classList.contains('select')) return
|
||||||
|
|
||||||
|
@ -268,17 +246,18 @@ export default {
|
||||||
this.setList({ list: this.list, listId: 'download', index: this.list.findIndex(i => i.key === targetSong.key) })
|
this.setList({ list: this.list, listId: 'download', index: this.list.findIndex(i => i.key === targetSong.key) })
|
||||||
},
|
},
|
||||||
handleListBtnClick(info) {
|
handleListBtnClick(info) {
|
||||||
const key = this.showList[info.index].key
|
let item = this.showList[info.index]
|
||||||
|
const key = item.key
|
||||||
let index = this.list.findIndex(i => i.key === key)
|
let index = this.list.findIndex(i => i.key === key)
|
||||||
switch (info.action) {
|
switch (info.action) {
|
||||||
case 'play':
|
case 'play':
|
||||||
this.handlePlay(index)
|
this.handlePlay(index)
|
||||||
break
|
break
|
||||||
case 'start':
|
case 'start':
|
||||||
this.handleStartTask(index)
|
this.startTask(item)
|
||||||
break
|
break
|
||||||
case 'pause':
|
case 'pause':
|
||||||
this.handlePauseTask(index)
|
this.pauseTask(index)
|
||||||
break
|
break
|
||||||
case 'remove':
|
case 'remove':
|
||||||
this.removeTask(index)
|
this.removeTask(index)
|
||||||
|
@ -300,38 +279,22 @@ export default {
|
||||||
node.classList.add('active')
|
node.classList.add('active')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleFlowBtnClick(action) {
|
async handleFlowBtnClick(action) {
|
||||||
|
let selectdData = [...this.selectdData]
|
||||||
|
this.removeAllSelect()
|
||||||
|
await this.$nextTick()
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'start':
|
case 'start':
|
||||||
this.selectdData.forEach(item => {
|
this.startTasks(selectdData)
|
||||||
if (item.isComplate || item.status == this.downloadStatus.RUN) return
|
|
||||||
let index = this.list.indexOf(item)
|
|
||||||
if (index < 0) return
|
|
||||||
this.handleStartTask(index)
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
case 'pause': {
|
case 'pause':
|
||||||
let runs = []
|
this.pauseTasks(selectdData)
|
||||||
this.selectdData.forEach(item => {
|
|
||||||
if (item.isComplate || item.status == this.downloadStatus.PAUSE) return
|
|
||||||
if (item.status == this.downloadStatus.RUN) return runs.push(item)
|
|
||||||
let index = this.list.indexOf(item)
|
|
||||||
if (index < 0) return
|
|
||||||
this.handlePauseTask(index)
|
|
||||||
})
|
|
||||||
runs.forEach(item => {
|
|
||||||
if (item.isComplate || item.status == this.downloadStatus.PAUSE) return
|
|
||||||
let index = this.list.indexOf(item)
|
|
||||||
if (index < 0) return
|
|
||||||
this.handlePauseTask(index)
|
|
||||||
})
|
|
||||||
break
|
break
|
||||||
}
|
|
||||||
case 'remove':
|
case 'remove':
|
||||||
this.removeTaskMultiple(this.selectdData)
|
this.removeTasks(selectdData)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
this.removeAllSelect()
|
|
||||||
},
|
},
|
||||||
async handleOpenFolder(index) {
|
async handleOpenFolder(index) {
|
||||||
let path = this.list[index].filePath
|
let path = this.list[index].filePath
|
||||||
|
|
Loading…
Reference in New Issue