v2.0.0
This commit is contained in:
@@ -1,568 +0,0 @@
|
||||
<template lang="pug">
|
||||
div(:class="$style.download")
|
||||
//- transition
|
||||
div(:class="$style.header")
|
||||
base-tab(:class="$style.tab" :list="tabs" align="left" item-key="id" item-name="name" @change="handleTabChange" v-model="tabId")
|
||||
div(:class="$style.content" v-if="downloadList.length")
|
||||
div.thead(:class="$style.thead")
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th.nobreak.center(style="width: 5%;") #
|
||||
th.nobreak {{$t('music_name')}}
|
||||
th.nobreak(style="width: 20%;") {{$t('download__progress')}}
|
||||
th.nobreak(style="width: 22%;") {{$t('download__status')}}
|
||||
th.nobreak(style="width: 10%;") {{$t('download__quality')}}
|
||||
th.nobreak(style="width: 13%;") {{$t('action')}}
|
||||
div(v-if="downloadList.length" :class="$style.content" ref="dom_listContent")
|
||||
base-virtualized-list(:list="showList" key-name="key" ref="list" :item-height="listItemHeight" #default="{ item, index }"
|
||||
containerClass="scroll" contentClass="list")
|
||||
div.list-item(@click="handleDoubleClick($event, index)" @contextmenu="handleListItemRigthClick($event, index)"
|
||||
:class="[{[$style.active]: playListIndex == index }, { selected: selectedIndex == index }, { active: selectedData.includes(item) }]")
|
||||
div.list-item-cell.nobreak.center(style="width: 5%; padding-left: 3px; padding-right: 3px;" @click.stop) {{index + 1}}
|
||||
div.list-item-cell.auto(:aria-label="item.name")
|
||||
span.select {{item.name}}
|
||||
div.list-item-cell(style="width: 20%;") {{item.progress.progress}}%
|
||||
div.list-item-cell(style="width: 22%;" :aria-label="item.statusText") {{item.statusText}}
|
||||
div.list-item-cell(style="width: 10%;") {{getTypeName(item.metadata.type)}}
|
||||
div.list-item-cell(style="width: 13%; padding-left: 0; padding-right: 0;")
|
||||
material-list-buttons(:index="index" :download-btn="false" :file-btn="item.status != downloadStatus.ERROR" remove-btn
|
||||
:start-btn="!item.isComplate && item.status != downloadStatus.WAITING && (item.status != downloadStatus.RUN)"
|
||||
:pause-btn="!item.isComplate && (item.status == downloadStatus.RUN || item.status == downloadStatus.WAITING)" :list-add-btn="false"
|
||||
:play-btn="item.status == downloadStatus.COMPLETED" :search-btn="item.status == downloadStatus.ERROR" @btn-click="handleListBtnClick")
|
||||
base-menu(:menus="listItemMenu" :location="listMenu.menuLocation" item-name="name" :isShow="listMenu.isShowItemMenu" @menu-click="handleListItemMenuClick")
|
||||
div(:class="$style.noItem" v-else)
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { checkPath, openDirInExplorer, openUrl, getFontSizeWithScreen } from '@renderer/utils'
|
||||
import musicSdk from '@renderer/utils/music'
|
||||
import path from 'path'
|
||||
import { windowSizeList, isFullscreen } from '@renderer/core/share'
|
||||
import { playInfo, playMusicInfo } from '@renderer/core/share/player'
|
||||
import { downloadList, downloadStatus } from '@renderer/core/share/download'
|
||||
|
||||
export default {
|
||||
name: 'Download',
|
||||
setup() {
|
||||
return {
|
||||
playMusicInfo,
|
||||
playInfo,
|
||||
downloadList,
|
||||
downloadStatus,
|
||||
isFullscreen,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
clickTime: window.performance.now(),
|
||||
clickIndex: -1,
|
||||
selectedData: [],
|
||||
isShowDownloadMultiple: false,
|
||||
tabId: 'all',
|
||||
keyEvent: {
|
||||
isShiftDown: false,
|
||||
isModDown: false,
|
||||
},
|
||||
selectedIndex: -1,
|
||||
lastSelectIndex: 0,
|
||||
listMenu: {
|
||||
rightClickItemIndex: -1,
|
||||
isShowItemMenu: false,
|
||||
itemMenuControl: {
|
||||
play: true,
|
||||
start: true,
|
||||
pause: true,
|
||||
playLater: true,
|
||||
file: true,
|
||||
search: true,
|
||||
remove: true,
|
||||
sourceDetail: true,
|
||||
},
|
||||
menuLocation: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['setting']),
|
||||
isPlayList() {
|
||||
return this.playMusicInfo.listId == 'download'
|
||||
},
|
||||
playListIndex() {
|
||||
if (this.playMusicInfo.listId != 'download' || !this.downloadList.length) return -1
|
||||
let info = this.downloadList[this.playInfo.playIndex]
|
||||
if (!info) return -1
|
||||
let key = info.key
|
||||
return this.showList.findIndex(i => i.key == key)
|
||||
},
|
||||
showList() {
|
||||
switch (this.tabId) {
|
||||
case 'runing':
|
||||
return this.downloadList.filter(i => i.status == this.downloadStatus.RUN || i.status == this.downloadStatus.WAITING)
|
||||
case 'paused':
|
||||
return this.downloadList.filter(i => i.status == this.downloadStatus.PAUSE)
|
||||
case 'error':
|
||||
return this.downloadList.filter(i => i.status == this.downloadStatus.ERROR)
|
||||
case 'finished':
|
||||
return this.downloadList.filter(i => i.status == this.downloadStatus.COMPLETED)
|
||||
default:
|
||||
return this.downloadList
|
||||
}
|
||||
},
|
||||
tabs() {
|
||||
return [
|
||||
{
|
||||
name: this.$t('download__all'),
|
||||
id: 'all',
|
||||
},
|
||||
{
|
||||
name: this.$t('download__runing'),
|
||||
id: 'runing',
|
||||
},
|
||||
{
|
||||
name: this.$t('download__paused'),
|
||||
id: 'paused',
|
||||
},
|
||||
{
|
||||
name: this.$t('download__error'),
|
||||
id: 'error',
|
||||
},
|
||||
{
|
||||
name: this.$t('download__finished'),
|
||||
id: 'finished',
|
||||
},
|
||||
]
|
||||
},
|
||||
listItemMenu() {
|
||||
return [
|
||||
{
|
||||
name: this.$t('list__play'),
|
||||
action: 'play',
|
||||
hide: !this.listMenu.itemMenuControl.play,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__start'),
|
||||
action: 'start',
|
||||
hide: !this.listMenu.itemMenuControl.start,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__pause'),
|
||||
action: 'pause',
|
||||
hide: !this.listMenu.itemMenuControl.pause,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__play_later'),
|
||||
action: 'playLater',
|
||||
hide: !this.listMenu.itemMenuControl.playLater,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__file'),
|
||||
action: 'file',
|
||||
hide: !this.listMenu.itemMenuControl.file,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__source_detail'),
|
||||
action: 'sourceDetail',
|
||||
disabled: !this.listMenu.itemMenuControl.sourceDetail,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__search'),
|
||||
action: 'search',
|
||||
hide: !this.listMenu.itemMenuControl.search,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__remove'),
|
||||
action: 'remove',
|
||||
hide: !this.listMenu.itemMenuControl.remove,
|
||||
},
|
||||
]
|
||||
},
|
||||
listItemHeight() {
|
||||
return Math.ceil((this.isFullscreen ? getFontSizeWithScreen() : parseInt(windowSizeList.find(item => item.id == this.setting.windowSizeId).fontSize)) * 2.3)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
list() {
|
||||
this.removeAllSelect()
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.listenEvent()
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.unlistenEvent()
|
||||
},
|
||||
methods: {
|
||||
...mapActions('download', ['removeTask', 'removeTasks', 'startTask', 'startTasks', 'pauseTask', 'pauseTasks']),
|
||||
...mapMutations('player', ['setList', 'setTempPlayList']),
|
||||
listenEvent() {
|
||||
window.eventHub.on('key_shift_down', this.handle_key_shift_down)
|
||||
window.eventHub.on('key_shift_up', this.handle_key_shift_up)
|
||||
window.eventHub.on('key_mod_down', this.handle_key_mod_down)
|
||||
window.eventHub.on('key_mod_up', this.handle_key_mod_up)
|
||||
window.eventHub.on('key_mod+a_down', this.handle_key_mod_a_down)
|
||||
},
|
||||
unlistenEvent() {
|
||||
window.eventHub.off('key_shift_down', this.handle_key_shift_down)
|
||||
window.eventHub.off('key_shift_up', this.handle_key_shift_up)
|
||||
window.eventHub.off('key_mod_down', this.handle_key_mod_down)
|
||||
window.eventHub.off('key_mod_up', this.handle_key_mod_up)
|
||||
window.eventHub.off('key_mod+a_down', this.handle_key_mod_a_down)
|
||||
},
|
||||
handle_key_shift_down() {
|
||||
if (!this.keyEvent.isShiftDown) this.keyEvent.isShiftDown = true
|
||||
},
|
||||
handle_key_shift_up() {
|
||||
if (this.keyEvent.isShiftDown) this.keyEvent.isShiftDown = false
|
||||
},
|
||||
handle_key_mod_down() {
|
||||
if (!this.keyEvent.isModDown) this.keyEvent.isModDown = true
|
||||
},
|
||||
handle_key_mod_up() {
|
||||
if (this.keyEvent.isModDown) this.keyEvent.isModDown = false
|
||||
},
|
||||
handle_key_mod_a_down({ event }) {
|
||||
if (event.target.tagName == 'INPUT') return
|
||||
event.preventDefault()
|
||||
if (event.repeat) return
|
||||
this.keyEvent.isModDown = false
|
||||
this.handleSelectAllData()
|
||||
},
|
||||
handleDoubleClick(event, index) {
|
||||
if (this.listMenu.rightClickItemIndex > -1) return
|
||||
|
||||
this.handleSelectData(event, index)
|
||||
|
||||
if (
|
||||
window.performance.now() - this.clickTime > 400 ||
|
||||
this.clickIndex !== index
|
||||
) {
|
||||
this.clickTime = window.performance.now()
|
||||
this.clickIndex = index
|
||||
return
|
||||
}
|
||||
this.handleClick(index)
|
||||
this.clickTime = 0
|
||||
this.clickIndex = -1
|
||||
},
|
||||
handleSelectData(event, clickIndex) {
|
||||
if (this.keyEvent.isShiftDown) {
|
||||
if (this.selectedData.length) {
|
||||
let lastSelectIndex = this.lastSelectIndex
|
||||
this.removeAllSelect()
|
||||
if (lastSelectIndex != clickIndex) {
|
||||
let isNeedReverse = false
|
||||
if (clickIndex < lastSelectIndex) {
|
||||
let temp = lastSelectIndex
|
||||
lastSelectIndex = clickIndex
|
||||
clickIndex = temp
|
||||
isNeedReverse = true
|
||||
}
|
||||
this.selectedData = this.showList.slice(lastSelectIndex, clickIndex + 1)
|
||||
if (isNeedReverse) this.selectedData.reverse()
|
||||
}
|
||||
} else {
|
||||
this.selectedData.push(this.showList[clickIndex])
|
||||
this.lastSelectIndex = clickIndex
|
||||
}
|
||||
} else if (this.keyEvent.isModDown) {
|
||||
this.lastSelectIndex = clickIndex
|
||||
let item = this.showList[clickIndex]
|
||||
let index = this.selectedData.indexOf(item)
|
||||
if (index < 0) {
|
||||
this.selectedData.push(item)
|
||||
} else {
|
||||
this.selectedData.splice(index, 1)
|
||||
}
|
||||
} else if (this.selectedData.length) this.removeAllSelect()
|
||||
},
|
||||
removeAllSelect() {
|
||||
this.selectedData = []
|
||||
},
|
||||
handleClick(index) {
|
||||
const key = this.showList[index].key
|
||||
index = this.downloadList.findIndex(i => i.key === key)
|
||||
let info = this.downloadList[index]
|
||||
if (info.isComplate) {
|
||||
this.handlePlay(info)
|
||||
} else if (info.status === this.downloadStatus.RUN) {
|
||||
this.handlePauseTask(index)
|
||||
} else {
|
||||
this.handleStartTask(index)
|
||||
}
|
||||
},
|
||||
async handlePlay(targetSong) {
|
||||
if (!await checkPath(path.join(this.setting.download.savePath, targetSong.metadata.fileName))) return
|
||||
this.setList({ listId: 'download', index: this.downloadList.findIndex(i => i.key === targetSong.key) })
|
||||
},
|
||||
handleListBtnClick(info) {
|
||||
let item = this.showList[info.index]
|
||||
switch (info.action) {
|
||||
case 'play':
|
||||
this.handlePlay(item)
|
||||
break
|
||||
case 'start':
|
||||
this.startTask(item)
|
||||
break
|
||||
case 'pause':
|
||||
this.pauseTask(item)
|
||||
break
|
||||
case 'remove':
|
||||
this.removeTask(item)
|
||||
break
|
||||
case 'file':
|
||||
this.handleOpenFolder(item.metadata.filePath)
|
||||
break
|
||||
case 'search':
|
||||
this.handleSearch(item.metadata.musicInfo)
|
||||
break
|
||||
}
|
||||
},
|
||||
handleSelectAllData() {
|
||||
this.removeAllSelect()
|
||||
this.selectedData = [...this.showList]
|
||||
},
|
||||
// async handleFlowBtnClick(action) {
|
||||
// let selectedData = [...this.selectedData]
|
||||
// this.removeAllSelect()
|
||||
// await this.$nextTick()
|
||||
|
||||
// switch (action) {
|
||||
// case 'start':
|
||||
// this.startTasks(selectedData)
|
||||
// break
|
||||
// case 'pause':
|
||||
// this.pauseTasks(selectedData)
|
||||
// break
|
||||
// case 'remove':
|
||||
// this.removeTasks(selectedData)
|
||||
// break
|
||||
// }
|
||||
// },
|
||||
async handleOpenFolder(filePath) {
|
||||
if (!await checkPath(filePath)) return
|
||||
openDirInExplorer(filePath)
|
||||
},
|
||||
handleSearch(musicInfo) {
|
||||
this.$router.push({
|
||||
path: 'search',
|
||||
query: {
|
||||
text: `${musicInfo.name} ${musicInfo.singer}`,
|
||||
},
|
||||
})
|
||||
},
|
||||
handleTabChange() {
|
||||
this.selectedData = []
|
||||
},
|
||||
handleListItemRigthClick(event, index) {
|
||||
const downloadInfo = this.showList[index]
|
||||
this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[downloadInfo.metadata.musicInfo.source].getMusicDetailPageUrl
|
||||
let dom_container = event.target.closest('.' + this.$style.download)
|
||||
const getOffsetValue = (target, x = 0, y = 0) => {
|
||||
if (target === dom_container) return { x, y }
|
||||
if (!target) return { x: 0, y: 0 }
|
||||
x += target.offsetLeft
|
||||
y += target.offsetTop
|
||||
return getOffsetValue(target.offsetParent, x, y)
|
||||
}
|
||||
this.listMenu.rightClickItemIndex = index
|
||||
this.selectedIndex = index
|
||||
let { x, y } = getOffsetValue(event.target)
|
||||
this.listMenu.menuLocation.x = x + event.offsetX
|
||||
this.listMenu.menuLocation.y = y + event.offsetY - this.$refs.list.getScrollTop()
|
||||
|
||||
let item = this.showList[index]
|
||||
if (item.isComplate) {
|
||||
this.listMenu.itemMenuControl.play =
|
||||
this.listMenu.itemMenuControl.playLater =
|
||||
this.listMenu.itemMenuControl.file = true
|
||||
this.listMenu.itemMenuControl.start =
|
||||
this.listMenu.itemMenuControl.pause = false
|
||||
} else if (item.status === this.downloadStatus.ERROR || item.status === this.downloadStatus.PAUSE) {
|
||||
this.listMenu.itemMenuControl.play =
|
||||
this.listMenu.itemMenuControl.playLater =
|
||||
this.listMenu.itemMenuControl.pause =
|
||||
this.listMenu.itemMenuControl.file = false
|
||||
this.listMenu.itemMenuControl.start = true
|
||||
} else {
|
||||
this.listMenu.itemMenuControl.play =
|
||||
this.listMenu.itemMenuControl.playLater =
|
||||
this.listMenu.itemMenuControl.start =
|
||||
this.listMenu.itemMenuControl.file = false
|
||||
this.listMenu.itemMenuControl.pause = true
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.listMenu.isShowItemMenu = true
|
||||
})
|
||||
},
|
||||
hideListMenu() {
|
||||
this.selectedIndex = -1
|
||||
this.listMenu.isShowItemMenu = false
|
||||
this.listMenu.rightClickItemIndex = -1
|
||||
},
|
||||
handleListItemMenuClick(action) {
|
||||
// console.log(action)
|
||||
let index = this.listMenu.rightClickItemIndex
|
||||
this.hideListMenu()
|
||||
// let key
|
||||
let item
|
||||
let url
|
||||
|
||||
switch (action && action.action) {
|
||||
case 'play':
|
||||
item = this.showList[index]
|
||||
if (item) this.handlePlay(item)
|
||||
break
|
||||
case 'start':
|
||||
if (this.selectedData.length) {
|
||||
let selectedData = [...this.selectedData]
|
||||
this.removeAllSelect()
|
||||
this.startTasks(selectedData)
|
||||
} else {
|
||||
item = this.showList[index]
|
||||
if (item) this.startTask(item)
|
||||
this.$nextTick(() => {
|
||||
this.isShowDownload = true
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'pause':
|
||||
if (this.selectedData.length) {
|
||||
let selectedData = [...this.selectedData]
|
||||
this.removeAllSelect()
|
||||
this.pauseTasks(selectedData)
|
||||
} else {
|
||||
item = this.showList[index]
|
||||
if (item) this.pauseTask(item)
|
||||
this.$nextTick(() => {
|
||||
this.isShowDownload = true
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'file':
|
||||
item = this.showList[index]
|
||||
// key = item.key
|
||||
// index = this.list.findIndex(i => i.key === key)
|
||||
if (item) this.handleOpenFolder(item.metadata.filePath)
|
||||
break
|
||||
case 'search':
|
||||
item = this.showList[index]
|
||||
if (item) this.handleSearch(item.metadata.musicInfo)
|
||||
break
|
||||
case 'remove':
|
||||
if (this.selectedData.length) {
|
||||
let selectedData = [...this.selectedData]
|
||||
this.removeAllSelect()
|
||||
this.removeTasks(selectedData)
|
||||
} else {
|
||||
item = this.showList[index]
|
||||
if (item) this.removeTask(item)
|
||||
this.$nextTick(() => {
|
||||
this.isShowDownload = true
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'playLater':
|
||||
if (this.selectedData.length) {
|
||||
this.setTempPlayList(this.selectedData.map(s => ({ listId: 'download', musicInfo: s })))
|
||||
this.removeAllSelect()
|
||||
} else {
|
||||
this.setTempPlayList([{ listId: 'download', musicInfo: this.showList[index] }])
|
||||
}
|
||||
break
|
||||
case 'sourceDetail':
|
||||
item = this.showList[index].metadata.musicInfo
|
||||
url = musicSdk[item.source].getMusicDetailPageUrl(item)
|
||||
if (!url) return
|
||||
openUrl(url)
|
||||
}
|
||||
},
|
||||
getTypeName(type) {
|
||||
return type == 'flac32bit' ? 'FLAC Hires' : type?.toUpperCase()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '../assets/styles/layout.less';
|
||||
|
||||
.download {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
// .noItem {
|
||||
|
||||
// }
|
||||
}
|
||||
.header {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
flex: auto;
|
||||
overflow: hidden;
|
||||
// table {
|
||||
// position: relative;
|
||||
// thead {
|
||||
// position: fixed;
|
||||
// width: 100%;
|
||||
// th {
|
||||
// width: 100%;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
.thead {
|
||||
flex: none;
|
||||
tr > th:first-child {
|
||||
color: @color-theme_2-font-label;
|
||||
// padding-left: 10px;
|
||||
}
|
||||
}
|
||||
:global(.list) {
|
||||
flex: auto;
|
||||
overflow-y: auto;
|
||||
:global(.list-item-cell) {
|
||||
font-size: 12px !important;
|
||||
&:first-child {
|
||||
// padding-left: 10px;
|
||||
font-size: 11px !important;
|
||||
color: @color-theme_2-font-label !important;
|
||||
}
|
||||
}
|
||||
:global(.list-item) {
|
||||
&.active {
|
||||
color: @color-btn;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
:global(.list) {
|
||||
:global(.list-item-cell) {
|
||||
&:first-child {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
:global(.list-item) {
|
||||
&.active {
|
||||
color: ~'@{color-@{value}-btn}' !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</style>
|
||||
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div :class="$style.download">
|
||||
<div :class="$style.header">
|
||||
<base-tab v-model="activeTab" :class="$style.tab" :list="tabs" />
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<div class="thead" :class="$style.thead">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="num" style="width: 5%;">#</th>
|
||||
<th class="nobreak">{{ $t('music_name') }}</th>
|
||||
<th class="nobreak" style="width: 20%;">{{ $t('download__progress') }}</th>
|
||||
<th class="nobreak" style="width: 22%;">{{ $t('download__status') }}</th>
|
||||
<th class="nobreak" style="width: 10%;">{{ $t('download__quality') }}</th>
|
||||
<th class="nobreak" style="width: 13%;">{{ $t('action') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div v-if="list.length" ref="dom_listContent" :class="$style.content">
|
||||
<base-virtualized-list
|
||||
v-slot="{ item, index }" :list="list" key-name="key" :item-height="listItemHeight"
|
||||
container-class="scroll" content-class="list"
|
||||
>
|
||||
<div
|
||||
class="list-item"
|
||||
:class="[{[$style.active]: playTaskId == item.id }, { selected: rightClickSelectedIndex == index }, { active: selectedList.includes(item) }]"
|
||||
@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
|
||||
>
|
||||
<div class="list-item-cell no-select num" style="flex: 0 0 5%;" @click.stop>{{ index + 1 }}</div>
|
||||
<div class="list-item-cell auto name">
|
||||
<span class="select name" :aria-label="getName(item)">{{ getName(item) }}</span>
|
||||
</div>
|
||||
<div class="list-item-cell" style="flex: 0 0 20%;">{{ item.progress }}%<span v-if="item.status == downloadStatus.RUN && item.speed"> - {{ item.speed }}/s</span></div>
|
||||
<div class="list-item-cell" style="flex: 0 0 22%;" :aria-label="item.statusText">{{ item.statusText }}</div>
|
||||
<div class="list-item-cell" style="flex: 0 0 10%;">{{ getTypeName(item.metadata.quality) }}</div>
|
||||
<div class="list-item-cell" style="flex: 0 0 13%; padding-left: 0; padding-right: 0;">
|
||||
<material-list-buttons
|
||||
:index="index" :download-btn="false" :file-btn="item.status != downloadStatus.ERROR" remove-btn="remove-btn"
|
||||
:start-btn="!item.isComplate && item.status != downloadStatus.WAITING && (item.status != downloadStatus.RUN)"
|
||||
:pause-btn="!item.isComplate && (item.status == downloadStatus.RUN || item.status == downloadStatus.WAITING)"
|
||||
:list-add-btn="false" :play-btn="item.status == downloadStatus.COMPLETED"
|
||||
:search-btn="item.status == downloadStatus.ERROR" @btn-click="handleListBtnClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</base-virtualized-list>
|
||||
</div>
|
||||
<div v-else :class="$style.noItem">
|
||||
<p v-text="$t('no_item')" />
|
||||
</div>
|
||||
<base-menu v-model="isShowItemMenu" :menus="menus" :xy="menuLocation" item-name="name" @menu-click="handleMenuClick" />
|
||||
<!-- <base-menu :menus="listItemMenu" :location="listMenu.menuLocation" item-name="name" :is-show="listMenu.isShowItemMenu" @menu-click="handleListItemMenuClick" /> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import { checkPath, openDirInExplorer, openUrl } from '@common/utils/electron'
|
||||
|
||||
import useListInfo from './useListInfo'
|
||||
import useList from './useList'
|
||||
import useTab from './useTab'
|
||||
import useMenu from './useMenu'
|
||||
import usePlay from './usePlay'
|
||||
import useTaskActions from './useTaskActions'
|
||||
import { downloadStatus } from '@renderer/store/download/state'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
|
||||
export default {
|
||||
name: 'Download',
|
||||
setup() {
|
||||
const { tabs, activeTab } = useTab()
|
||||
|
||||
const {
|
||||
rightClickSelectedIndex,
|
||||
dom_listContent,
|
||||
listAll,
|
||||
list,
|
||||
playTaskId,
|
||||
} = useListInfo(activeTab)
|
||||
|
||||
const {
|
||||
selectedList,
|
||||
listItemHeight,
|
||||
removeAllSelect,
|
||||
handleSelectData,
|
||||
} = useList({ list, listAll })
|
||||
|
||||
const {
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
} = usePlay({ selectedList, list, listAll, removeAllSelect })
|
||||
|
||||
const {
|
||||
handleSearch,
|
||||
handleOpenMusicDetail,
|
||||
handleStartTask,
|
||||
handlePauseTask,
|
||||
handleRemoveTask,
|
||||
handleOpenFile,
|
||||
} = useTaskActions({ list, removeAllSelect, selectedList })
|
||||
|
||||
const {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowItemMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
} = useMenu({
|
||||
handleStartTask,
|
||||
handlePauseTask,
|
||||
handleRemoveTask,
|
||||
handleOpenFile,
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
handleSearch,
|
||||
handleOpenMusicDetail,
|
||||
})
|
||||
|
||||
let clickTime = 0
|
||||
let clickIndex = -1
|
||||
const doubleClickPlay = index => {
|
||||
if (
|
||||
window.performance.now() - clickTime > 400 ||
|
||||
clickIndex !== index
|
||||
) {
|
||||
clickTime = window.performance.now()
|
||||
clickIndex = index
|
||||
return
|
||||
}
|
||||
const task = list.value[index]
|
||||
if (task.isComplate) {
|
||||
handlePlayMusic(list.value.indexOf(task), true)
|
||||
} else if (task.status === downloadStatus.RUN || task.status === downloadStatus.WAITING) {
|
||||
handlePauseTask(index, true)
|
||||
} else {
|
||||
handleStartTask(index, true)
|
||||
}
|
||||
clickTime = 0
|
||||
clickIndex = -1
|
||||
}
|
||||
|
||||
const handleListItemClick = (event, index) => {
|
||||
if (rightClickSelectedIndex.value > -1) return
|
||||
handleSelectData(index)
|
||||
doubleClickPlay(index)
|
||||
}
|
||||
const handleListItemRightClick = (event, index) => {
|
||||
rightClickSelectedIndex.value = index
|
||||
showMenu(event, list.value[index], index)
|
||||
}
|
||||
const handleMenuClick = (action) => {
|
||||
let index = rightClickSelectedIndex.value
|
||||
rightClickSelectedIndex.value = -1
|
||||
menuClick(action, index)
|
||||
}
|
||||
|
||||
const handleListBtnClick = ({ action, index }) => {
|
||||
switch (action) {
|
||||
case 'play':
|
||||
handlePlayMusic(index, true)
|
||||
break
|
||||
case 'start':
|
||||
handleStartTask(index, true)
|
||||
break
|
||||
case 'pause':
|
||||
handlePauseTask(index, true)
|
||||
break
|
||||
case 'remove':
|
||||
handleRemoveTask(index, true)
|
||||
break
|
||||
case 'file':
|
||||
handleOpenFile(index)
|
||||
break
|
||||
case 'search':
|
||||
handleSearch(index)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const getName = (downloadInfo) => {
|
||||
return appSetting['download.fileName'].replace('歌名', downloadInfo.metadata.musicInfo.name).replace('歌手', downloadInfo.metadata.musicInfo.singer)
|
||||
}
|
||||
const getTypeName = (quality) => {
|
||||
return quality == 'flac24bit' ? 'FLAC Hires' : quality?.toUpperCase()
|
||||
}
|
||||
return {
|
||||
list,
|
||||
downloadStatus,
|
||||
rightClickSelectedIndex,
|
||||
dom_listContent,
|
||||
tabs,
|
||||
activeTab,
|
||||
selectedList,
|
||||
listItemHeight,
|
||||
playTaskId,
|
||||
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowItemMenu,
|
||||
|
||||
handleListItemClick,
|
||||
handleListItemRightClick,
|
||||
handleMenuClick,
|
||||
handleListBtnClick,
|
||||
|
||||
getName,
|
||||
getTypeName,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.download {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
:global(.list-item) {
|
||||
&.active {
|
||||
color: var(--color-button-font);
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
min-height: 0;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.no-item {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { computed, watch, ref, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { isFullscreen } from '@renderer/store'
|
||||
import { getFontSizeWithScreen } from '@renderer/utils'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
|
||||
const useKeyEvent = ({ handleSelectAllData }) => {
|
||||
const keyEvent = {
|
||||
isShiftDown: false,
|
||||
isModDown: false,
|
||||
}
|
||||
|
||||
const handle_key_shift_down = () => {
|
||||
if (!keyEvent.isShiftDown) keyEvent.isShiftDown = true
|
||||
}
|
||||
const handle_key_shift_up = () => {
|
||||
if (keyEvent.isShiftDown) keyEvent.isShiftDown = false
|
||||
}
|
||||
const handle_key_mod_down = () => {
|
||||
if (!keyEvent.isModDown) keyEvent.isModDown = true
|
||||
}
|
||||
const handle_key_mod_up = () => {
|
||||
if (keyEvent.isModDown) keyEvent.isModDown = false
|
||||
}
|
||||
const handle_key_mod_a_down = ({ event }) => {
|
||||
if (event.target.tagName == 'INPUT') return
|
||||
event.preventDefault()
|
||||
if (event.repeat) return
|
||||
keyEvent.isModDown = false
|
||||
handleSelectAllData()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.key_event.off('key_shift_down', handle_key_shift_down)
|
||||
window.key_event.off('key_shift_up', handle_key_shift_up)
|
||||
window.key_event.off('key_mod_down', handle_key_mod_down)
|
||||
window.key_event.off('key_mod_up', handle_key_mod_up)
|
||||
window.key_event.off('key_mod+a_down', handle_key_mod_a_down)
|
||||
})
|
||||
window.key_event.on('key_shift_down', handle_key_shift_down)
|
||||
window.key_event.on('key_shift_up', handle_key_shift_up)
|
||||
window.key_event.on('key_mod_down', handle_key_mod_down)
|
||||
window.key_event.on('key_mod_up', handle_key_mod_up)
|
||||
window.key_event.on('key_mod+a_down', handle_key_mod_a_down)
|
||||
|
||||
return keyEvent
|
||||
}
|
||||
|
||||
export default ({ list, listAll }) => {
|
||||
const selectedList = ref([])
|
||||
|
||||
let lastSelectIndex = -1
|
||||
const listItemHeight = computed(() => {
|
||||
return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : appSetting['common.fontSize']) * 2.3)
|
||||
})
|
||||
|
||||
const removeAllSelect = () => {
|
||||
selectedList.value = []
|
||||
}
|
||||
const handleSelectAllData = () => {
|
||||
removeAllSelect()
|
||||
selectedList.value = [...list.value]
|
||||
}
|
||||
const keyEvent = useKeyEvent({ handleSelectAllData })
|
||||
|
||||
const handleSelectData = clickIndex => {
|
||||
if (keyEvent.isShiftDown) {
|
||||
if (selectedList.value.length) {
|
||||
removeAllSelect()
|
||||
if (lastSelectIndex != clickIndex) {
|
||||
let isNeedReverse = false
|
||||
if (clickIndex < lastSelectIndex) {
|
||||
let temp = lastSelectIndex
|
||||
lastSelectIndex = clickIndex
|
||||
clickIndex = temp
|
||||
isNeedReverse = true
|
||||
}
|
||||
selectedList.value = list.value.slice(lastSelectIndex, clickIndex + 1)
|
||||
if (isNeedReverse) selectedList.value.reverse()
|
||||
}
|
||||
} else {
|
||||
selectedList.value.push(list.value[clickIndex])
|
||||
lastSelectIndex = clickIndex
|
||||
}
|
||||
} else if (keyEvent.isModDown) {
|
||||
lastSelectIndex = clickIndex
|
||||
let item = list.value[clickIndex]
|
||||
let index = selectedList.value.indexOf(item)
|
||||
if (index < 0) {
|
||||
selectedList.value.push(item)
|
||||
} else {
|
||||
selectedList.value.splice(index, 1)
|
||||
}
|
||||
} else if (selectedList.value.length) {
|
||||
removeAllSelect()
|
||||
}
|
||||
}
|
||||
|
||||
watch(listAll, removeAllSelect)
|
||||
|
||||
return {
|
||||
selectedList,
|
||||
listItemHeight,
|
||||
removeAllSelect,
|
||||
handleSelectData,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { ref, computed } from '@common/utils/vueTools'
|
||||
import { playMusicInfo, playInfo } from '@renderer/store/player/state'
|
||||
import { downloadStatus } from '@renderer/store/download/state'
|
||||
import { getDownloadList } from '@renderer/store/download/action'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
|
||||
|
||||
export default (activeTab) => {
|
||||
const rightClickSelectedIndex = ref(-1)
|
||||
const dom_listContent = ref(null)
|
||||
|
||||
const listAll = ref([])
|
||||
getDownloadList().then(l => {
|
||||
listAll.value = l
|
||||
})
|
||||
|
||||
const list = computed(() => {
|
||||
switch (activeTab.value) {
|
||||
case 'runing':
|
||||
return listAll.value.filter(i => i.status == downloadStatus.RUN || i.status == downloadStatus.WAITING)
|
||||
case 'paused':
|
||||
return listAll.value.filter(i => i.status == downloadStatus.PAUSE)
|
||||
case 'error':
|
||||
return listAll.value.filter(i => i.status == downloadStatus.ERROR)
|
||||
case 'finished':
|
||||
return listAll.value.filter(i => i.status == downloadStatus.COMPLETED)
|
||||
default:
|
||||
return listAll.value
|
||||
}
|
||||
})
|
||||
|
||||
const playTaskId = computed(() => playMusicInfo.listId == LIST_IDS.DOWNLOAD ? listAll.value[playInfo.playIndex]?.id : '')
|
||||
|
||||
|
||||
return {
|
||||
rightClickSelectedIndex,
|
||||
dom_listContent,
|
||||
listAll,
|
||||
list,
|
||||
playTaskId,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import { computed, ref, shallowReactive, reactive, nextTick } from '@common/utils/vueTools'
|
||||
import musicSdk from '@renderer/utils/musicSdk'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { DOWNLOAD_STATUS } from '@common/constants'
|
||||
|
||||
export default ({
|
||||
handleStartTask,
|
||||
handlePauseTask,
|
||||
handleRemoveTask,
|
||||
handleOpenFile,
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
handleSearch,
|
||||
handleOpenMusicDetail,
|
||||
}) => {
|
||||
const itemMenuControl = reactive({
|
||||
play: true,
|
||||
start: true,
|
||||
pause: true,
|
||||
playLater: true,
|
||||
file: true,
|
||||
sourceDetail: true,
|
||||
search: true,
|
||||
remove: true,
|
||||
})
|
||||
const t = useI18n()
|
||||
const menuLocation = shallowReactive({ x: 0, y: 0 })
|
||||
const isShowItemMenu = ref(false)
|
||||
|
||||
const menus = computed(() => {
|
||||
return [
|
||||
{
|
||||
name: t('list__play'),
|
||||
action: 'play',
|
||||
hide: !itemMenuControl.play,
|
||||
},
|
||||
{
|
||||
name: t('list__start'),
|
||||
action: 'start',
|
||||
hide: !itemMenuControl.start,
|
||||
},
|
||||
{
|
||||
name: t('list__pause'),
|
||||
action: 'pause',
|
||||
hide: !itemMenuControl.pause,
|
||||
},
|
||||
{
|
||||
name: t('list__play_later'),
|
||||
action: 'playLater',
|
||||
hide: !itemMenuControl.playLater,
|
||||
},
|
||||
{
|
||||
name: t('list__file'),
|
||||
action: 'file',
|
||||
hide: !itemMenuControl.file,
|
||||
},
|
||||
{
|
||||
name: t('list__source_detail'),
|
||||
action: 'sourceDetail',
|
||||
disabled: !itemMenuControl.sourceDetail,
|
||||
},
|
||||
{
|
||||
name: t('list__search'),
|
||||
action: 'search',
|
||||
hide: !itemMenuControl.search,
|
||||
},
|
||||
{
|
||||
name: t('list__remove'),
|
||||
action: 'remove',
|
||||
hide: !itemMenuControl.remove,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const showMenu = (event, taskInfo) => {
|
||||
itemMenuControl.sourceDetail = !!musicSdk[taskInfo.metadata.musicInfo.source]?.getMusicDetailPageUrl
|
||||
|
||||
if (taskInfo.isComplate) {
|
||||
itemMenuControl.play =
|
||||
itemMenuControl.playLater =
|
||||
itemMenuControl.file = true
|
||||
itemMenuControl.start =
|
||||
itemMenuControl.pause = false
|
||||
} else if (taskInfo.status === DOWNLOAD_STATUS.ERROR || taskInfo.status === DOWNLOAD_STATUS.PAUSE) {
|
||||
itemMenuControl.play =
|
||||
itemMenuControl.playLater =
|
||||
itemMenuControl.pause =
|
||||
itemMenuControl.file = false
|
||||
itemMenuControl.start = true
|
||||
} else {
|
||||
itemMenuControl.play =
|
||||
itemMenuControl.playLater =
|
||||
itemMenuControl.start =
|
||||
itemMenuControl.file = false
|
||||
itemMenuControl.pause = true
|
||||
}
|
||||
|
||||
menuLocation.x = event.pageX
|
||||
menuLocation.y = event.pageY
|
||||
|
||||
if (isShowItemMenu.value) return
|
||||
|
||||
nextTick(() => {
|
||||
isShowItemMenu.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const hideMenu = () => {
|
||||
isShowItemMenu.value = false
|
||||
}
|
||||
|
||||
const menuClick = (action, index) => {
|
||||
// console.log(action)
|
||||
hideMenu()
|
||||
if (!action) return
|
||||
switch (action.action) {
|
||||
case 'start':
|
||||
handleStartTask(index)
|
||||
break
|
||||
case 'pause':
|
||||
handlePauseTask(index)
|
||||
break
|
||||
case 'file':
|
||||
handleOpenFile(index)
|
||||
break
|
||||
case 'play':
|
||||
handlePlayMusic(index)
|
||||
break
|
||||
case 'playLater':
|
||||
handlePlayMusicLater(index)
|
||||
break
|
||||
case 'search':
|
||||
handleSearch(index)
|
||||
break
|
||||
case 'remove':
|
||||
handleRemoveTask(index)
|
||||
break
|
||||
case 'sourceDetail':
|
||||
handleOpenMusicDetail(index)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowItemMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { addTempPlayList } from '@renderer/store/player/action'
|
||||
import { playList } from '@renderer/core/player'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
|
||||
export default ({ selectedList, list, listAll, removeAllSelect }) => {
|
||||
const handlePlayMusic = (index) => {
|
||||
playList(LIST_IDS.DOWNLOAD, listAll.value.indexOf(list.value[index]))
|
||||
}
|
||||
|
||||
const handlePlayMusicLater = (index, single) => {
|
||||
if (selectedList.value.length && !single) {
|
||||
addTempPlayList(selectedList.value.map(s => ({ listId: LIST_IDS.DOWNLOAD, musicInfo: s })))
|
||||
removeAllSelect()
|
||||
} else {
|
||||
addTempPlayList([{ listId: LIST_IDS.DOWNLOAD, musicInfo: list.value[index] }])
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { computed, ref } from '@common/utils/vueTools'
|
||||
|
||||
export default () => {
|
||||
const tabs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: window.i18n.t('download__all'),
|
||||
id: 'all',
|
||||
},
|
||||
{
|
||||
label: window.i18n.t('download__runing'),
|
||||
id: 'runing',
|
||||
},
|
||||
{
|
||||
label: window.i18n.t('download__paused'),
|
||||
id: 'paused',
|
||||
},
|
||||
{
|
||||
label: window.i18n.t('download__error'),
|
||||
id: 'error',
|
||||
},
|
||||
{
|
||||
label: window.i18n.t('download__finished'),
|
||||
id: 'finished',
|
||||
},
|
||||
]
|
||||
})
|
||||
const activeTab = ref('all')
|
||||
|
||||
// const setActiveTab = (tab) => {
|
||||
// activeTab.value = tab
|
||||
// }
|
||||
|
||||
return {
|
||||
tabs,
|
||||
activeTab,
|
||||
// setActiveTab,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useRouter } from '@common/utils/vueRouter'
|
||||
import musicSdk from '@renderer/utils/musicSdk'
|
||||
import { openUrl, openDirInExplorer } from '@common/utils/electron'
|
||||
import { checkPath } from '@common/utils/nodejs'
|
||||
// import { dialog } from '@renderer/plugins/Dialog'
|
||||
// import { useI18n } from '@renderer/plugins/i18n'
|
||||
// import { appSetting } from '@renderer/store/setting'
|
||||
import { toOldMusicInfo } from '@renderer/utils/index'
|
||||
import { startDownloadTasks, pauseDownloadTasks, removeDownloadTasks } from '@renderer/store/download/action'
|
||||
|
||||
export default ({ list, selectedList, removeAllSelect }) => {
|
||||
const router = useRouter()
|
||||
// const t = useI18n()
|
||||
|
||||
const handleSearch = index => {
|
||||
const info = list.value[index].metadata.musicInfo
|
||||
router.push({
|
||||
path: '/search',
|
||||
query: {
|
||||
text: `${info.name} ${info.singer}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpenMusicDetail = index => {
|
||||
const task = list.value[index]
|
||||
const mInfo = toOldMusicInfo(task.metadata.musicInfo)
|
||||
const url = musicSdk[mInfo.source].getMusicDetailPageUrl(mInfo)
|
||||
if (!url) return
|
||||
openUrl(url)
|
||||
}
|
||||
|
||||
const handleStartTask = async(index, single) => {
|
||||
if (selectedList.value.length && !single) {
|
||||
startDownloadTasks([...selectedList.value])
|
||||
removeAllSelect()
|
||||
} else {
|
||||
startDownloadTasks([list.value[index]])
|
||||
}
|
||||
}
|
||||
|
||||
const handlePauseTask = async(index, single) => {
|
||||
if (selectedList.value.length && !single) {
|
||||
pauseDownloadTasks([...selectedList.value])
|
||||
removeAllSelect()
|
||||
} else {
|
||||
pauseDownloadTasks([list.value[index]])
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveTask = async(index, single) => {
|
||||
if (selectedList.value.length && !single) {
|
||||
// const confirm = await (selectedList.value.length > 1
|
||||
// ? dialog.confirm({
|
||||
// message: t('lists__remove music_tip', { len: selectedList.value.length }),
|
||||
// confirmButtonText: t('lists__remove_tip_button'),
|
||||
// })
|
||||
// : Promise.resolve(true)
|
||||
// )
|
||||
// if (!confirm) return
|
||||
removeDownloadTasks(selectedList.value.map(m => m.id))
|
||||
removeAllSelect()
|
||||
} else {
|
||||
removeDownloadTasks([list.value[index].id])
|
||||
}
|
||||
}
|
||||
|
||||
const handleOpenFile = async(index) => {
|
||||
const task = list.value[index]
|
||||
if (!checkPath(task.metadata.filePath)) return
|
||||
openDirInExplorer(task.metadata.filePath)
|
||||
}
|
||||
|
||||
return {
|
||||
handleSearch,
|
||||
handleOpenMusicDetail,
|
||||
handleStartTask,
|
||||
handlePauseTask,
|
||||
handleRemoveTask,
|
||||
handleOpenFile,
|
||||
}
|
||||
}
|
||||
@@ -1,446 +0,0 @@
|
||||
<template>
|
||||
<div :class="$style.leaderboard">
|
||||
<div :class="$style.lists" ref="dom_lists">
|
||||
<div :class="$style.listsSelect">
|
||||
<base-selection :class="$style.select" :list="sources" item-key="id" item-name="name" v-model="source" />
|
||||
</div>
|
||||
<ul class="scroll" :class="$style.listsContent" ref="dom_lists_list">
|
||||
<li :class="[$style.listsItem, { [$style.active]: item.id == tabId }, { [$style.clicked]: boardListData.rightClickItemIndex == index }]"
|
||||
:aria-label="item.name" v-for="(item, index) in boardList" :key="item.id"
|
||||
@click="handleToggleList(item.id)" @contextmenu="handleListsItemRigthClick($event, index)">
|
||||
<span :class="$style.listsLabel">{{item.name}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div :class="$style.list">
|
||||
<material-online-list
|
||||
ref="songList"
|
||||
@show-menu="hideListsMenu"
|
||||
@play-list="handlePlayList"
|
||||
@toggle-page="handleGetList"
|
||||
:rowWidth="{r1: '5%', r2: 'auto', r3: '22%', r4: '22%', r5: '9%', r6: '15%'}"
|
||||
:page="page"
|
||||
:limit="listInfo.limit"
|
||||
:total="listInfo.total"
|
||||
:noItem="noItemLabel"
|
||||
:list="list" />
|
||||
</div>
|
||||
<base-menu
|
||||
:menus="listsItemMenu"
|
||||
:location="boardListData.menuLocation"
|
||||
item-name="name"
|
||||
:isShow="boardListData.isShowItemMenu"
|
||||
@menu-click="handleListsItemMenuClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations, mapActions } from 'vuex'
|
||||
import { nextTick } from '@renderer/utils/vueTools'
|
||||
import { tempList } from '@renderer/core/share/list'
|
||||
export default {
|
||||
name: 'Leaderboard',
|
||||
data() {
|
||||
return {
|
||||
tabId: null,
|
||||
source: null,
|
||||
page: 1,
|
||||
isShowDownload: false,
|
||||
musicInfo: null,
|
||||
isShowDownloadMultiple: false,
|
||||
isShowListAdd: false,
|
||||
isShowListAddMultiple: false,
|
||||
boardListData: {
|
||||
isShowItemMenu: false,
|
||||
itemMenuControl: {
|
||||
rename: true,
|
||||
sync: false,
|
||||
moveup: true,
|
||||
movedown: true,
|
||||
remove: true,
|
||||
},
|
||||
rightClickItemIndex: -1,
|
||||
menuLocation: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
listInfo: {
|
||||
list: [],
|
||||
total: 0,
|
||||
page: 1,
|
||||
limit: 30,
|
||||
key: null,
|
||||
},
|
||||
loadId: null,
|
||||
loadError: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['setting']),
|
||||
...mapGetters('leaderboard', ['sources', 'sourceIds', 'boards', 'info']),
|
||||
boardList() {
|
||||
return this.source && this.boards[this.source] ? this.boards[this.source] : []
|
||||
},
|
||||
listsItemMenu() {
|
||||
return [
|
||||
{
|
||||
name: this.$t('list__play'),
|
||||
action: 'play',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__collect'),
|
||||
action: 'collect',
|
||||
disabled: false,
|
||||
},
|
||||
]
|
||||
},
|
||||
list() {
|
||||
return this.listInfo.list
|
||||
},
|
||||
noItemLabel() {
|
||||
return this.loadId
|
||||
? this.$t('list__loading')
|
||||
: this.loadError
|
||||
? this.$t('list__load_failed')
|
||||
: this.listInfo.list.length
|
||||
? this.$t('list__loading')
|
||||
: this.$t('no_item')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
tabId(n, o) {
|
||||
this.setLeaderboard({ tabId: n })
|
||||
if (!n || (!o && this.page !== 1)) return
|
||||
this.listInfo.list = []
|
||||
this.handleGetList(1)
|
||||
},
|
||||
source(n, o) {
|
||||
this.setLeaderboard({ source: n })
|
||||
if (o) this.tabId = null
|
||||
this.getBoardsList().then(() => {
|
||||
if (this.tabId != null) return
|
||||
nextTick(() => {
|
||||
this.tabId = this.boardList[0] && this.boardList[0].id
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.source = this.setting.leaderboard.source
|
||||
if (!this.sourceIds.includes(this.source)) this.source = this.sourceIds[0]
|
||||
this.tabId = this.setting.leaderboard.tabId
|
||||
this.page = this.listInfo.page
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['setLeaderboard']),
|
||||
...mapActions('leaderboard', ['getBoardsList', 'getList', 'getListAll']),
|
||||
...mapMutations('list', ['createUserList']),
|
||||
...mapMutations('player', ['setTempList', 'updateTempList']),
|
||||
handleGetList(page) {
|
||||
const loadId = `${this.source}${this.tabId}${page}`
|
||||
this.loadError = false
|
||||
this.loadId = loadId
|
||||
this.getList(page).then(listInfo => {
|
||||
if (this.loadId != loadId) return
|
||||
this.listInfo.list = listInfo.list
|
||||
this.listInfo.total = listInfo.total
|
||||
this.listInfo.limit = listInfo.limit
|
||||
this.listInfo.page = listInfo.page
|
||||
this.listInfo.key = listInfo.key
|
||||
this.page = listInfo.page
|
||||
nextTick(() => {
|
||||
this.$refs.songList?.scrollToTop()
|
||||
})
|
||||
}).catch(() => {
|
||||
if (this.loadId != loadId) return
|
||||
this.loadError = true
|
||||
}).finally(() => {
|
||||
if (this.loadId != loadId) return
|
||||
this.loadId = null
|
||||
})
|
||||
},
|
||||
handleToggleList(id) {
|
||||
if (this.tabId == id) return
|
||||
this.tabId = id
|
||||
},
|
||||
handleListsItemRigthClick(event, index) {
|
||||
// const board = this.boardList[index]
|
||||
// this.boardListData.itemMenuControl.sync = !!source && !!musicSdk[source].songList
|
||||
// this.boardListData.itemMenuControl.moveup = index > 0
|
||||
// this.boardListData.itemMenuControl.movedown = index < this.userList.length - 1
|
||||
this.boardListData.rightClickItemIndex = index
|
||||
this.boardListData.menuLocation.x = event.currentTarget.offsetLeft + event.offsetX
|
||||
this.boardListData.menuLocation.y = event.currentTarget.offsetTop + event.offsetY - this.$refs.dom_lists_list.scrollTop
|
||||
// this.hideListsMenu()
|
||||
this.$refs.songList.hideMenu()
|
||||
this.$nextTick(() => {
|
||||
this.boardListData.isShowItemMenu = true
|
||||
})
|
||||
},
|
||||
hideListsMenu() {
|
||||
this.boardListData.isShowItemMenu = false
|
||||
this.boardListData.rightClickItemIndex = -1
|
||||
},
|
||||
async handleListsItemMenuClick(action) {
|
||||
let index = this.boardListData.rightClickItemIndex
|
||||
this.hideListsMenu()
|
||||
this.boardListData.isShowItemMenu = false
|
||||
|
||||
if (action) {
|
||||
const board = this.boardList[index]
|
||||
const id = `board__${this.source}__${board.id}`
|
||||
switch (action && action.action) {
|
||||
case 'play':
|
||||
this.playSongListDetail({
|
||||
boardId: board.id,
|
||||
id,
|
||||
})
|
||||
break
|
||||
case 'collect':
|
||||
this.addSongListDetail({
|
||||
boardId: board.id,
|
||||
boardName: board.name,
|
||||
source: this.source,
|
||||
id,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
async addSongListDetail({ boardId, boardName, source, id }) {
|
||||
// console.log(this.listDetail.info)
|
||||
// if (!this.listDetail.info.name) return
|
||||
const list = await this.getListAll({ id: boardId })
|
||||
this.createUserList({
|
||||
name: boardName,
|
||||
id,
|
||||
list,
|
||||
source,
|
||||
sourceListId: `board__${boardId}`,
|
||||
})
|
||||
},
|
||||
async playSongListDetail({ boardId, id, index = 0 }) {
|
||||
let isPlayingList = false
|
||||
const list = this.tabId == boardId ? [...this.list] : null
|
||||
if (list?.length) {
|
||||
this.setTempList({
|
||||
list,
|
||||
index,
|
||||
id,
|
||||
})
|
||||
isPlayingList = true
|
||||
}
|
||||
const fullList = await this.getListAll({ id: boardId })
|
||||
if (!fullList.length) return
|
||||
if (isPlayingList) {
|
||||
if (tempList.meta.id == id) {
|
||||
this.updateTempList({
|
||||
list: fullList,
|
||||
id,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.setTempList({
|
||||
list: fullList,
|
||||
index,
|
||||
id,
|
||||
})
|
||||
}
|
||||
},
|
||||
handlePlayList(index) {
|
||||
this.playSongListDetail({
|
||||
boardId: this.tabId,
|
||||
id: `board__${this.source}__${this.tabId}`,
|
||||
index,
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '../assets/styles/layout.less';
|
||||
|
||||
.leaderboard {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
.header {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
}
|
||||
.tab {
|
||||
flex: auto;
|
||||
}
|
||||
.select {
|
||||
flex: none;
|
||||
width: 80px;
|
||||
}
|
||||
.content {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.lists {
|
||||
flex: none;
|
||||
width: 14.8%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.listsHeader {
|
||||
position: relative;
|
||||
}
|
||||
.listsSelect {
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
:global(.icon) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
>:global(.content) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
:global(.label-content) {
|
||||
background-color: transparent !important;
|
||||
line-height: 38px;
|
||||
height: 38px;
|
||||
border-radius: 0;
|
||||
&:hover {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
:global(.label) {
|
||||
color: @color-theme_2-font !important;
|
||||
}
|
||||
:global(.icon) {
|
||||
opacity: .6;
|
||||
transition: opacity .3s ease;
|
||||
}
|
||||
|
||||
:global(.selection-list) {
|
||||
max-height: 500px;
|
||||
box-shadow: 0 1px 8px 0 rgba(0,0,0,.2);
|
||||
li {
|
||||
background-color: @color-theme_2-background_2;
|
||||
line-height: 38px;
|
||||
font-size: 13px;
|
||||
&:hover {
|
||||
background-color: @color-btn-hover;
|
||||
}
|
||||
&:active {
|
||||
background-color: @color-btn-active;
|
||||
}
|
||||
}
|
||||
}
|
||||
// line-height: 38px;
|
||||
// padding: 0 10px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
flex: none;
|
||||
}
|
||||
.listsContent {
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
overflow-y: scroll !important;
|
||||
// border-right: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.listsItem {
|
||||
position: relative;
|
||||
transition: .3s ease;
|
||||
transition-property: color, background-color;
|
||||
background-color: transparent;
|
||||
&:hover:not(.active) {
|
||||
background-color: @color-theme_2-hover;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.active {
|
||||
// background-color:
|
||||
color: @color-theme;
|
||||
}
|
||||
&.selected {
|
||||
background-color: @color-theme_2-active;
|
||||
}
|
||||
&.clicked {
|
||||
background-color: @color-theme_2-hover;
|
||||
}
|
||||
&.editing {
|
||||
padding: 0 10px;
|
||||
background-color: @color-theme_2-hover;
|
||||
.listsLabel {
|
||||
display: none;
|
||||
}
|
||||
.listsInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsLabel {
|
||||
display: block;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
line-height: 36px;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
|
||||
.list {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
// .noItem {
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.listsSelect {
|
||||
:global(.label) {
|
||||
color: ~'@{color-@{value}-theme_2-font}' !important;
|
||||
}
|
||||
:global(.selection-list) {
|
||||
li {
|
||||
background-color: ~'@{color-@{value}-theme_2-background_2}';
|
||||
&:hover {
|
||||
background-color: ~'@{color-@{value}-btn-hover}';
|
||||
}
|
||||
&:active {
|
||||
background-color: ~'@{color-@{value}-btn-active}';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsItem {
|
||||
&:hover:not(.active) {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
&.active {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
&.select {
|
||||
background-color: ~'@{color-@{value}-theme_2-active}';
|
||||
}
|
||||
&.clicked {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
&.editing {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<ul ref="dom_lists_list" class="scroll" :class="$style.listsContent">
|
||||
<li
|
||||
v-for="(item, index) in list"
|
||||
:key="item.id" :class="[$style.listsItem, { [$style.active]: item.id == boardId }, { [$style.clicked]: rightClickItemIndex == index }]"
|
||||
:aria-label="item.name" @click="handleToggleList(item.id)" @contextmenu="handleRigthClick($event, index)"
|
||||
>
|
||||
<span :class="$style.listsLabel">{{ item.name }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<base-menu
|
||||
v-model="isShowMenu"
|
||||
:menus="menus"
|
||||
:xy="menuLocation"
|
||||
item-name="name"
|
||||
@menu-click="handleMenuClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, shallowReactive, ref, defineExpose } from '@common/utils/vueTools'
|
||||
import { getBoardsList, setBoard } from '@renderer/store/leaderboard/action'
|
||||
import { boards, Board } from '@renderer/store/leaderboard/state'
|
||||
import useMenu from './useMenu'
|
||||
import LX from '@renderer/types/lx'
|
||||
import { useRouter, useRoute } from '@common/utils/vueRouter'
|
||||
|
||||
const props = defineProps<{
|
||||
source: LX.OnlineSource
|
||||
boardId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['show-menu'])
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const list = shallowReactive<Board['list']>([])
|
||||
const rightClickItemIndex = ref(-1)
|
||||
|
||||
const handleToggleList = (id: string) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
source: props.source,
|
||||
boardId: id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
} = useMenu({ emit, list })
|
||||
|
||||
const handleRigthClick = (event: MouseEvent, index: number) => {
|
||||
rightClickItemIndex.value = index
|
||||
showMenu(event, index)
|
||||
}
|
||||
const handleMenuClick = (action?: { action: string}) => {
|
||||
if (rightClickItemIndex.value < 0) return
|
||||
let index = rightClickItemIndex.value
|
||||
rightClickItemIndex.value = -1
|
||||
menuClick(action, index, props.source)
|
||||
}
|
||||
|
||||
|
||||
watch(() => props.source, async(source) => {
|
||||
// const source = (await getLeaderboardSetting()).source as LX.OnlineSource
|
||||
let boardList = boards[source]
|
||||
if (boardList == null) setBoard(boardList = await getBoardsList(source), source)
|
||||
list.splice(0, list.length, ...boardList.list)
|
||||
if (!props.boardId && boardList.list.length) handleToggleList(boardList.list[0].id)
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
defineExpose({ hideMenu: handleMenuClick })
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.listsContent {
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
overflow-y: scroll;
|
||||
// overflow-y: scroll !important;
|
||||
// border-right: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.listsItem {
|
||||
position: relative;
|
||||
transition: .3s ease;
|
||||
transition-property: color, background-color;
|
||||
background-color: transparent;
|
||||
&:hover:not(.active) {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
&.active {
|
||||
// background-color:
|
||||
color: var(--color-primary);
|
||||
}
|
||||
&.selected {
|
||||
background-color: var(--color-primary-font-active);
|
||||
}
|
||||
&.clicked {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
&.editing {
|
||||
padding: 0 10px;
|
||||
background-color: var(--color-primary-background-hover);
|
||||
.listsLabel {
|
||||
display: none;
|
||||
}
|
||||
.listsInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsLabel {
|
||||
display: block;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
line-height: 36px;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { computed, ref, reactive, nextTick } from '@common/utils/vueTools'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { addSongListDetail, playSongListDetail } from '../action'
|
||||
|
||||
export default ({
|
||||
emit,
|
||||
list,
|
||||
}) => {
|
||||
// const menuControl = reactive({
|
||||
// play: true,
|
||||
// collect: true,
|
||||
// })
|
||||
const t = useI18n()
|
||||
const menuLocation = reactive({ x: 0, y: 0 })
|
||||
const isShowMenu = ref(false)
|
||||
|
||||
const menus = computed(() => {
|
||||
return [
|
||||
{
|
||||
name: t('list__play'),
|
||||
action: 'play',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: t('list__collect'),
|
||||
action: 'collect',
|
||||
disabled: false,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
const showMenu = (event, index) => {
|
||||
menuLocation.x = event.pageX
|
||||
menuLocation.y = event.pageY
|
||||
|
||||
if (isShowMenu.value) return
|
||||
emit('show-menu')
|
||||
nextTick(() => {
|
||||
isShowMenu.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const hideMenu = () => {
|
||||
isShowMenu.value = false
|
||||
}
|
||||
|
||||
|
||||
const menuClick = (action, index, source) => {
|
||||
// console.log(action)
|
||||
hideMenu()
|
||||
if (!action) return
|
||||
// const id = `board__${this.source}__${board.id}`
|
||||
const board = list[index]
|
||||
switch (action.action) {
|
||||
case 'play':
|
||||
playSongListDetail(board.id)
|
||||
break
|
||||
case 'collect':
|
||||
addSongListDetail(board.id, board.name, source)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<material-online-list
|
||||
ref="listRef"
|
||||
:page="listDetailInfo.page"
|
||||
:limit="listDetailInfo.limit"
|
||||
:total="listDetailInfo.total"
|
||||
:list="listDetailInfo.list"
|
||||
:no-item="listDetailInfo.noItemLabel"
|
||||
@show-menu="hideListsMenu"
|
||||
@play-list="handlePlayList"
|
||||
@toggle-page="togglePage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, defineProps, defineExpose } from '@common/utils/vueTools'
|
||||
import LX from '@renderer/types/lx'
|
||||
import useList from './useList'
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
source: LX.OnlineSource
|
||||
boardId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['show-menu'])
|
||||
|
||||
const {
|
||||
listRef,
|
||||
listDetailInfo,
|
||||
getList,
|
||||
handlePlayList,
|
||||
} = useList()
|
||||
|
||||
watch(() => props.boardId, (boardId) => {
|
||||
if (!boardId) return
|
||||
getList(boardId, 1)
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
|
||||
const hideListsMenu = () => {
|
||||
emit('show-menu')
|
||||
}
|
||||
|
||||
const togglePage = (page: number) => {
|
||||
getList(listDetailInfo.id, page)
|
||||
}
|
||||
|
||||
const hideMenu = () => {
|
||||
listRef.value.handleMenuClick()
|
||||
}
|
||||
|
||||
defineExpose({ hideMenu })
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
.container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.list {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
// import { useI18n } from '@renderer/plugins/i18n'
|
||||
// import { } from '@renderer/store/search/state'
|
||||
import { getAndSetListDetail } from '@renderer/store/leaderboard/action'
|
||||
import { listDetailInfo } from '@renderer/store/leaderboard/state'
|
||||
import { playSongListDetail } from '../action'
|
||||
|
||||
export default () => {
|
||||
const listRef = ref<any>(null)
|
||||
|
||||
const handlePlayList = (index: number) => {
|
||||
void playSongListDetail(listDetailInfo.id, listDetailInfo.list, index)
|
||||
}
|
||||
|
||||
const getList = (id: string, page: number) => {
|
||||
void getAndSetListDetail(id, page).then(() => {
|
||||
setTimeout(() => {
|
||||
if (listRef.value) listRef.value.scrollToTop()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
listRef,
|
||||
listDetailInfo,
|
||||
getList,
|
||||
handlePlayList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { tempListMeta, userLists } from '@renderer/store/list/state'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
import syncSourceList from '@renderer/store/list/syncSourceList'
|
||||
import { getListDetail, getListDetailAll } from '@renderer/store/leaderboard/action'
|
||||
import { createUserList, setTempList } from '@renderer/store/list/action'
|
||||
import { playList } from '@renderer/core/player/action'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
|
||||
const getListId = (id: string) => `board__${id}`
|
||||
|
||||
export const addSongListDetail = async(id: string, name: string, source: LX.OnlineSource) => {
|
||||
// console.log(this.listDetail.info)
|
||||
// if (!this.listDetail.info.name) return
|
||||
const listId = getListId(id)
|
||||
const targetList = userLists.find(l => l.id == listId)
|
||||
if (targetList) {
|
||||
const confirm = await dialog.confirm({
|
||||
message: window.i18n.t('duplicate_list_tip', { name: targetList.name }),
|
||||
cancelButtonText: window.i18n.t('lists__import_part_button_cancel'),
|
||||
confirmButtonText: window.i18n.t('confirm_button_text'),
|
||||
})
|
||||
if (!confirm) return
|
||||
void syncSourceList(targetList)
|
||||
return
|
||||
}
|
||||
|
||||
const list = await getListDetailAll(id)
|
||||
await createUserList({
|
||||
name,
|
||||
id: listId,
|
||||
list,
|
||||
source,
|
||||
sourceListId: listId,
|
||||
})
|
||||
}
|
||||
|
||||
export const playSongListDetail = async(id: string, list?: LX.Music.MusicInfoOnline[], index: number = 0) => {
|
||||
let isPlayingList = false
|
||||
console.log(list)
|
||||
const listId = getListId(id)
|
||||
if (!list?.length) list = (await getListDetail(id, 1)).list
|
||||
if (list?.length) {
|
||||
await setTempList(listId, [...list])
|
||||
playList(LIST_IDS.TEMP, index)
|
||||
isPlayingList = true
|
||||
}
|
||||
const fullList = await getListDetailAll(id)
|
||||
if (!fullList.length) return
|
||||
if (isPlayingList) {
|
||||
if (tempListMeta.id == listId) {
|
||||
await setTempList(listId, [...fullList])
|
||||
}
|
||||
} else {
|
||||
await setTempList(listId, [...fullList])
|
||||
playList(LIST_IDS.TEMP, index)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div :class="$style.leaderboard">
|
||||
<div :class="$style.lists">
|
||||
<div :class="$style.listsSelect">
|
||||
<base-selection :model-value="source" :class="$style.select" :list="sourceList" item-key="id" item-name="name" @update:model-value="handleToggleSource" />
|
||||
</div>
|
||||
<BoardList ref="boardListRef" :board-id="boardId" :source="source" @show-menu="$refs.musicListRef?.hideMenu()" />
|
||||
</div>
|
||||
<div :class="$style.list">
|
||||
<MusicList ref="musicListRef" :source="source" :board-id="boardId" @show-menu="$refs.boardListRef?.hideMenu()" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, ref } from '@common/utils/vueTools'
|
||||
import { getLeaderboardSetting, setLeaderboardSetting } from '@renderer/utils/data'
|
||||
import BoardList from './BoardList/index.vue'
|
||||
import MusicList from './MusicList/index.vue'
|
||||
import { sources } from '@renderer/store/leaderboard/state'
|
||||
import { sourceNames } from '@renderer/store'
|
||||
import { useRoute, useRouter } from '@common/utils/vueRouter'
|
||||
|
||||
|
||||
const source = ref('')
|
||||
const boardId = ref(null)
|
||||
|
||||
const verifyQueryParams = async function(to, from, next) {
|
||||
let _source = to.query.source
|
||||
let _boardId = to.query.boardId
|
||||
|
||||
if (_source == null) {
|
||||
const setting = await getLeaderboardSetting()
|
||||
if (_source == null) {
|
||||
_source = setting.source
|
||||
_boardId = setting.boardId
|
||||
}
|
||||
next({
|
||||
path: to.path,
|
||||
query: { ...to.query, source: _source, boardId: _boardId },
|
||||
})
|
||||
return
|
||||
}
|
||||
next()
|
||||
source.value = _source
|
||||
boardId.value = _boardId
|
||||
setLeaderboardSetting({ source: _source, boardId: _boardId })
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BoardList,
|
||||
MusicList,
|
||||
},
|
||||
beforeRouteEnter: verifyQueryParams,
|
||||
beforeRouteUpdate: verifyQueryParams,
|
||||
setup() {
|
||||
const musicListRef = ref(null)
|
||||
const boardListRef = ref(null)
|
||||
const sourceList = computed(() => {
|
||||
return sources.map(s => ({ id: s, name: sourceNames.value[s] }))
|
||||
})
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const handleToggleSource = (id) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
source: id,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
source,
|
||||
boardId,
|
||||
sourceList,
|
||||
handleToggleSource,
|
||||
musicListRef,
|
||||
boardListRef,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.leaderboard {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
.header {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
}
|
||||
.tab {
|
||||
flex: auto;
|
||||
}
|
||||
.select {
|
||||
flex: none;
|
||||
width: 80px;
|
||||
}
|
||||
.content {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.lists {
|
||||
flex: none;
|
||||
width: 14.8%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.listsHeader {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.listsSelect {
|
||||
font-size: 12px;
|
||||
|
||||
&:hover {
|
||||
:global(.icon) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
>:global(.content) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
:global(.label-content) {
|
||||
background-color: transparent !important;
|
||||
line-height: 38px;
|
||||
height: 38px;
|
||||
border-radius: 0;
|
||||
&:hover {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
:global(.label) {
|
||||
color: var(--color-font) !important;
|
||||
}
|
||||
:global(.icon) {
|
||||
opacity: .6;
|
||||
transition: opacity .3s ease;
|
||||
}
|
||||
|
||||
:global(.selection-list) {
|
||||
max-height: 500px;
|
||||
box-shadow: 0 1px 8px 0 rgba(0,0,0,.2);
|
||||
li {
|
||||
// background-color: var(--color-main-background);
|
||||
line-height: 38px;
|
||||
font-size: 13px;
|
||||
&:hover {
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-button-background-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
// line-height: 38px;
|
||||
// padding: 0 10px;
|
||||
border-bottom: var(--color-list-header-border-bottom);
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.list {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
// .noItem {
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
</style>
|
||||
+17
-24
@@ -1,18 +1,20 @@
|
||||
<template>
|
||||
<material-modal :show="show" @close="handleClose" @after-enter="$refs.input.focus()" teleport="#view">
|
||||
<main :class="$style.main">
|
||||
<h2>{{selectedNum > 0 ? $t('music_sort__title_multiple', { num: selectedNum }) : $t('music_sort__title', { name: musicInfo ? musicInfo.name : '' })}}</h2>
|
||||
<base-input :class="$style.input"
|
||||
type="number"
|
||||
v-model="sortNum"
|
||||
ref="input"
|
||||
@submit="handleSubmit"
|
||||
@blur="verify" :placeholder="$t('music_sort__input_tip')" />
|
||||
<div :class="$style.footer">
|
||||
<base-btn :class="$style.btn" @click="handleSubmit">{{$t('music_sort__btn_confirm')}}</base-btn>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
<material-modal :show="show" teleport="#view" @close="handleClose" @after-enter="$refs.input.focus()">
|
||||
<main :class="$style.main">
|
||||
<h2>{{ selectedNum > 0 ? $t('music_sort__title_multiple', { num: selectedNum }) : $t('music_sort__title', { name: musicInfo ? musicInfo.name : '' }) }}</h2>
|
||||
<base-input
|
||||
ref="input"
|
||||
v-model="sortNum"
|
||||
:class="$style.input"
|
||||
type="number"
|
||||
:placeholder="$t('music_sort__input_tip')"
|
||||
@submit="handleSubmit" @blur="verify"
|
||||
/>
|
||||
<div :class="$style.footer">
|
||||
<base-btn :class="$style.btn" @click="handleSubmit">{{ $t('btn_confirm') }}</base-btn>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -81,7 +83,7 @@ export default {
|
||||
// overflow: hidden;
|
||||
h2 {
|
||||
font-size: 13px;
|
||||
color: @color-theme_2-font;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
word-break: break-all;
|
||||
// text-align: center;
|
||||
@@ -112,14 +114,5 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.main {
|
||||
h2 {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
+68
-151
@@ -1,98 +1,45 @@
|
||||
<template lang="pug">
|
||||
teleport(to="#view")
|
||||
div(:class="$style.container" ref="dom_container" v-show="isShow")
|
||||
transition(enter-active-class="animated-fast zoomIn" leave-active-class="animated zoomOut" @after-leave="handleAnimated")
|
||||
div(:class="$style.search" v-show="visible")
|
||||
div(:class="$style.form")
|
||||
input.ignore-esc(:placeholder="placeholder" v-model.trim="text" ref="dom_input"
|
||||
@input="handleDelaySearch"
|
||||
@keyup.enter="handleTemplistClick(selectIndex)"
|
||||
@keyup.arrow-down.prevent="handleKeyDown"
|
||||
@keyup.arrow-up.prevent="handleKeyUp"
|
||||
@keyup.escape.prevent="handleKeyEsc"
|
||||
@keydown.control.prevent="handle_key_mod_down"
|
||||
@keydown.meta.prevent="handle_key_mod_down"
|
||||
@keyup.control.prevent="handle_key_mod_up"
|
||||
@keyup.meta.prevent="handle_key_mod_up"
|
||||
@contextmenu="handleContextMenu")
|
||||
button(type="button" @click="handleHide")
|
||||
slot
|
||||
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 212.982 212.982' space='preserve')
|
||||
use(xlink:href='#icon-delete')
|
||||
div.scroll(v-if="resultList" :class="$style.list" :style="listStyle" ref="dom_scrollContainer")
|
||||
ul(ref="dom_list")
|
||||
li(v-for="(item, index) in resultList" :key="item.songmid" :class="selectIndex === index ? $style.select : null" @mouseenter="selectIndex = index" @click="handleTemplistClick(index)")
|
||||
div(:class="$style.img")
|
||||
div(:class="$style.text")
|
||||
h3(:class="$style.text") {{item.name}} - {{item.singer}}
|
||||
h3(v-if="item.albumName" :class="[$style.text, $style.albumName]") {{item.albumName}}
|
||||
div(:class="$style.source") {{item.source}}
|
||||
<template>
|
||||
<teleport to="#view">
|
||||
<div v-show="isShow" ref="dom_container" :class="$style.container">
|
||||
<transition enter-active-class="animated-fast zoomIn" leave-active-class="animated zoomOut" @after-leave="handleAnimated">
|
||||
<div v-show="visible" :class="$style.search">
|
||||
<div :class="$style.form">
|
||||
<input
|
||||
ref="dom_input" v-model.trim="text" class="ignore-esc" :placeholder="placeholder" @input="handleDelaySearch"
|
||||
@keyup.enter="handleTemplistClick(selectIndex)" @keyup.arrow-down.prevent.exact="handleKeyDown" @keyup.arrow-up.prevent.exact="handleKeyUp"
|
||||
@keyup.escape.prevent.exact="handleKeyEsc" @keydown.control.prevent="handle_key_mod_down" @keydown.meta.prevent="handle_key_mod_down"
|
||||
@keyup.control.prevent="handle_key_mod_up" @keyup.meta.prevent="handle_key_mod_up" @contextmenu="handleContextMenu"
|
||||
>
|
||||
<button type="button" @click="handleHide">
|
||||
<slot>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 212.982 212.982" space="preserve">
|
||||
<use xlink:href="#icon-delete" />
|
||||
</svg>
|
||||
</slot>
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="resultList" ref="dom_scrollContainer" class="scroll" :class="$style.list" :style="listStyle">
|
||||
<ul ref="dom_list">
|
||||
<li v-for="(item, index) in resultList" :key="item.songmid" :class="selectIndex === index ? $style.select : null" @mouseenter="selectIndex = index" @click="handleTemplistClick(index)">
|
||||
<div :class="$style.img" />
|
||||
<div :class="$style.text">
|
||||
<h3 :class="$style.text">{{ item.name }} - {{ item.singer }}</h3>
|
||||
<h3 v-if="item.meta.albumName" :class="[$style.text, $style.albumName]">{{ item.meta.albumName }}</h3>
|
||||
</div>
|
||||
<div :class="$style.source">{{ item.source }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { clipboardReadText, debounce } from '@renderer/utils'
|
||||
|
||||
|
||||
// https://blog.csdn.net/xcxy2015/article/details/77164126#comments
|
||||
const similar = (a, b) => {
|
||||
if (!a || !b) return 0
|
||||
if (a.length > b.length) { // 保证 a <= b
|
||||
let t = b
|
||||
b = a
|
||||
a = t
|
||||
}
|
||||
let al = a.length
|
||||
let bl = b.length
|
||||
let mp = [] // 一个表
|
||||
let i, j, ai, lt, tmp // ai:字符串a的第i个字符。 lt:左上角的值。 tmp:暂存新的值。
|
||||
for (i = 0; i <= bl; i++) mp[i] = i
|
||||
for (i = 1; i <= al; i++) {
|
||||
ai = a.charAt(i - 1)
|
||||
lt = mp[0]
|
||||
mp[0] = mp[0] + 1
|
||||
for (j = 1; j <= bl; j++) {
|
||||
tmp = Math.min(mp[j] + 1, mp[j - 1] + 1, lt + (ai == b.charAt(j - 1) ? 0 : 1))
|
||||
lt = mp[j]
|
||||
mp[j] = tmp
|
||||
}
|
||||
}
|
||||
return 1 - (mp[bl] / bl)
|
||||
}
|
||||
|
||||
const sortInsert = (arr, data) => {
|
||||
let key = data.num
|
||||
let left = 0
|
||||
let right = arr.length - 1
|
||||
|
||||
while (left <= right) {
|
||||
let middle = parseInt((left + right) / 2)
|
||||
if (key == arr[middle]) {
|
||||
left = middle
|
||||
break
|
||||
} else if (key < arr[middle].num) {
|
||||
right = middle - 1
|
||||
} else {
|
||||
left = middle + 1
|
||||
}
|
||||
}
|
||||
while (left > 0) {
|
||||
if (arr[left - 1].num != key) break
|
||||
left--
|
||||
}
|
||||
|
||||
arr.splice(left, 0, data)
|
||||
}
|
||||
|
||||
const handleSortList = (list, keyword) => {
|
||||
let arr = []
|
||||
for (const item of list) {
|
||||
sortInsert(arr, {
|
||||
num: similar(keyword, `${item.name} ${item.singer} ${item.albumName || ''}`),
|
||||
data: item,
|
||||
})
|
||||
}
|
||||
return arr.map(item => item.data).reverse()
|
||||
}
|
||||
import { debounce } from '@common/utils'
|
||||
import { clipboardReadText } from '@common/utils/electron'
|
||||
import { toRaw } from '@common/utils/vueTools'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -119,6 +66,7 @@ export default {
|
||||
listStyle: {
|
||||
height: 0,
|
||||
maxHeight: 0,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
maxHeight: 0,
|
||||
resultList: [],
|
||||
@@ -130,7 +78,14 @@ export default {
|
||||
resultList(n) {
|
||||
if (this.selectIndex > -1) this.selectIndex = -1
|
||||
this.$nextTick(() => {
|
||||
this.listStyle.height = Math.min(this.$refs.dom_list.scrollHeight, this.maxHeight) + 'px'
|
||||
const height = this.$refs.dom_list.scrollHeight
|
||||
if (height > this.maxHeight) {
|
||||
this.listStyle.height = this.maxHeight + 'px'
|
||||
this.listStyle.overflow = 'auto'
|
||||
} else {
|
||||
this.listStyle.height = height + 'px'
|
||||
this.listStyle.overflow = 'hidden'
|
||||
}
|
||||
})
|
||||
},
|
||||
list(n) {
|
||||
@@ -151,14 +106,14 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
// window.eventHub.on('key_mod_down', this.handle_key_mod_down)
|
||||
// window.eventHub.on('key_mod_up', this.handle_key_mod_up)
|
||||
window.eventHub.on('key_mod+f_down', this.handle_key_mod_f_down)
|
||||
// window.key_event.on('key_mod_down', this.handle_key_mod_down)
|
||||
// window.key_event.on('key_mod_up', this.handle_key_mod_up)
|
||||
window.key_event.on('key_mod+f_down', this.handle_key_mod_f_down)
|
||||
},
|
||||
beforeUnmount() {
|
||||
// window.eventHub.off('key_mod_down', this.handle_key_mod_down)
|
||||
// window.eventHub.off('key_mod_up', this.handle_key_mod_up)
|
||||
window.eventHub.off('key_mod+f_down', this.handle_key_mod_f_down)
|
||||
// window.key_event.off('key_mod_down', this.handle_key_mod_down)
|
||||
// window.key_event.off('key_mod_up', this.handle_key_mod_up)
|
||||
window.key_event.off('key_mod+f_down', this.handle_key_mod_f_down)
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
@@ -181,6 +136,7 @@ export default {
|
||||
}
|
||||
},
|
||||
handle_key_mod_down() {
|
||||
console.log('handle_key_mod_down')
|
||||
if (!this.isModDown) this.isModDown = true
|
||||
},
|
||||
handle_key_mod_up() {
|
||||
@@ -195,8 +151,9 @@ export default {
|
||||
},
|
||||
handleTemplistClick(index) {
|
||||
if (index < 0) return
|
||||
const id = this.resultList[index].id
|
||||
this.sendEvent('listClick', {
|
||||
index: this.list.indexOf(this.resultList[index]),
|
||||
index: this.list.findIndex(m => m.id == id),
|
||||
isPlay: this.isModDown,
|
||||
})
|
||||
},
|
||||
@@ -239,14 +196,9 @@ export default {
|
||||
let dom_input = this.$refs.dom_input
|
||||
this.text = `${this.text.substring(0, dom_input.selectionStart)}${str}${this.text.substring(dom_input.selectionEnd, this.text.length)}`
|
||||
},
|
||||
handleSearch() {
|
||||
async handleSearch() {
|
||||
if (!this.text.length) return this.resultList = []
|
||||
let list = []
|
||||
let rxp = new RegExp(this.text.split('').map(s => s.replace(/[.*+?^${}()|[\]\\]/, '\\$&')).join('.*') + '.*', 'i')
|
||||
for (const item of this.list) {
|
||||
if (rxp.test(`${item.name}${item.singer}${item.albumName ? item.albumName : ''}`)) list.push(item)
|
||||
}
|
||||
this.resultList = handleSortList(list, this.text)
|
||||
this.resultList = await window.lx.worker.main.searchListMusic(toRaw(this.list), this.text)
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -270,10 +222,10 @@ export default {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
transition: box-shadow .4s ease, background-color @transition-theme;
|
||||
transition: box-shadow .4s ease, background-color @transition-normal;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
background-color: @color-search-form-background;
|
||||
background-color: var(--color-primary-light-600-alpha-100);
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.07),
|
||||
0 2px 4px rgba(0,0,0,0.07),
|
||||
0 4px 8px rgba(0,0,0,0.07),
|
||||
@@ -302,8 +254,8 @@ export default {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: transparent;
|
||||
// border-bottom: 2px solid @color-theme;
|
||||
// border-color: @color-theme;
|
||||
// border-bottom: 2px solid var(--color-primary);
|
||||
// border-color: var(--color-primary);
|
||||
border: none;
|
||||
|
||||
outline: none;
|
||||
@@ -313,7 +265,7 @@ export default {
|
||||
font-size: 13.5px;
|
||||
line-height: @height-toolbar * 0.52 + 5px;
|
||||
&::placeholder {
|
||||
color: @color-btn;
|
||||
color: var(--color-button-font);
|
||||
}
|
||||
}
|
||||
button {
|
||||
@@ -327,15 +279,15 @@ export default {
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
padding: 6px 7px;
|
||||
color: @color-btn;
|
||||
color: var(--color-button-font);
|
||||
transition: background-color .2s ease;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
background-color: @color-theme-hover;
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: @color-theme-active;
|
||||
background-color: var(--color-button-background-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,7 +311,7 @@ export default {
|
||||
flex-flow: row nowrap;
|
||||
|
||||
&.select {
|
||||
background-color: @color-search-list-hover;
|
||||
background-color: var(--color-primary-dark-100-alpha-700);
|
||||
}
|
||||
border-radius: 4px;
|
||||
// &:last-child {
|
||||
@@ -393,39 +345,4 @@ export default {
|
||||
// background-color:
|
||||
}
|
||||
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
|
||||
.search {
|
||||
background-color: ~'@{color-@{value}-search-form-background}';
|
||||
|
||||
.form {
|
||||
input {
|
||||
&::placeholder {
|
||||
color: ~'@{color-@{value}-btn}';
|
||||
}
|
||||
}
|
||||
button {
|
||||
color: ~'@{color-@{value}-btn}';
|
||||
|
||||
&:hover {
|
||||
background-color: ~'@{color-@{value}-theme-hover}';
|
||||
}
|
||||
&:active {
|
||||
background-color: ~'@{color-@{value}-theme-active}';
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
li {
|
||||
&.select {
|
||||
background-color: ~'@{color-@{value}-search-list-hover}';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,375 @@
|
||||
<template>
|
||||
<div :class="$style.list">
|
||||
<div class="thead">
|
||||
<table>
|
||||
<thead>
|
||||
<tr v-if="actionButtonsVisible">
|
||||
<th class="num" style="width: 5%;">#</th>
|
||||
<th class="nobreak">{{ $t('music_name') }}</th>
|
||||
<th class="nobreak" style="width: 22%;">{{ $t('music_singer') }}</th>
|
||||
<th class="nobreak" style="width: 22%;">{{ $t('music_album') }}</th>
|
||||
<th class="nobreak" style="width: 9%;">{{ $t('music_time') }}</th>
|
||||
<th class="nobreak" style="width: 16%;">{{ $t('action') }}</th>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<th class="num" style="width: 5%;">#</th>
|
||||
<th class="nobreak">{{ $t('music_name') }}</th>
|
||||
<th class="nobreak" style="width: 25%;">{{ $t('music_singer') }}</th>
|
||||
<th class="nobreak" style="width: 28%;">{{ $t('music_album') }}</th>
|
||||
<th class="nobreak" style="width: 10%;">{{ $t('music_time') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div v-show="list.length" ref="dom_listContent" :class="$style.content">
|
||||
<base-virtualized-list
|
||||
v-if="actionButtonsVisible" ref="listRef" v-slot="{ item, index }" :list="list" key-name="id"
|
||||
:item-height="listItemHeight" container-class="scroll" content-class="list"
|
||||
@scroll="saveListPosition" @contextmenu.capture="handleListRightClick"
|
||||
>
|
||||
<div
|
||||
class="list-item" :class="[{ [$style.active]: playerInfo.isPlayList && playerInfo.playIndex === index }, { selected: selectedIndex == index || rightClickSelectedIndex == index }, { active: selectedList.includes(item) }, { disabled: !assertApiSupport(item.source) }]"
|
||||
@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
|
||||
>
|
||||
<div class="list-item-cell no-select num" style="flex: 0 0 5%;" @click.stop>{{ index + 1 }}</div>
|
||||
<div class="list-item-cell auto name" :aria-label="item.name">
|
||||
<span class="select name">{{ item.name }}</span>
|
||||
<span v-if="isShowSource" class="no-select label-source">{{ item.source }}</span>
|
||||
</div>
|
||||
<div class="list-item-cell" style="flex: 0 0 22%;"><span class="select" :aria-label="item.singer">{{ item.singer }}</span></div>
|
||||
<div class="list-item-cell" style="flex: 0 0 22%;"><span class="select" :aria-label="item.meta.albumName">{{ item.meta.albumName }}</span></div>
|
||||
<div class="list-item-cell" style="flex: 0 0 9%;"><span class="no-select">{{ item.interval || '--/--' }}</span></div>
|
||||
<div class="list-item-cell" style="flex: 0 0 16%; padding-left: 0; padding-right: 0;">
|
||||
<material-list-buttons :index="index" :download-btn="assertApiSupport(item.source) && item.source != 'local'" @btn-click="handleListBtnClick" />
|
||||
</div>
|
||||
</div>
|
||||
</base-virtualized-list>
|
||||
<base-virtualized-list
|
||||
v-else ref="listRef" v-slot="{ item, index }" :list="list" key-name="id"
|
||||
:item-height="listItemHeight" container-class="scroll" content-class="list"
|
||||
@scroll="saveListPosition" @contextmenu.capture="handleListRightClick"
|
||||
>
|
||||
<div
|
||||
class="list-item"
|
||||
:class="[{ [$style.active]: playerInfo.isPlayList && playerInfo.playIndex === index }, { selected: selectedIndex == index || rightClickSelectedIndex == index }, { active: selectedList.includes(item) }, { disabled: !assertApiSupport(item.source) }]"
|
||||
@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
|
||||
>
|
||||
<div class="list-item-cell no-select num" style="flex: 0 0 5%;" @click.stop>{{ index + 1 }}</div>
|
||||
<div class="list-item-cell auto name">
|
||||
<span class="select name" :aria-label="item.name">{{ item.name }}</span>
|
||||
<span v-if="isShowSource" class="no-select label-source">{{ item.source }}</span>
|
||||
</div>
|
||||
<div class="list-item-cell" style="flex: 0 0 25%;"><span class="select" :aria-label="item.singer">{{ item.singer }}</span></div>
|
||||
<div class="list-item-cell" style="flex: 0 0 28%;"><span class="select" :aria-label="item.meta.albumName">{{ item.meta.albumName }}</span></div>
|
||||
<div class="list-item-cell" style="flex: 0 0 10%;"><span class="no-select">{{ item.interval || '--/--' }}</span></div>
|
||||
</div>
|
||||
</base-virtualized-list>
|
||||
</div>
|
||||
<div v-show="!list.length" :class="$style.noItem">
|
||||
<p v-text="$t('no_item')" />
|
||||
</div>
|
||||
<common-list-add-modal
|
||||
v-model:show="isShowListAdd" :is-move="isMove" :from-list-id="listId"
|
||||
:music-info="selectedAddMusicInfo" :exclude-list-id="excludeListIds" teleport="#view"
|
||||
/>
|
||||
<common-list-add-multiple-modal
|
||||
v-model:show="isShowListAddMultiple" :from-list-id="listId"
|
||||
:is-move="isMoveMultiple" :music-list="selectedList" :exclude-list-id="excludeListIds" teleport="#view" @confirm="removeAllSelect"
|
||||
/>
|
||||
<common-download-modal v-model:show="isShowDownload" :music-info="selectedDownloadMusicInfo" teleport="#view" />
|
||||
<common-download-multiple-modal v-model:show="isShowDownloadMultiple" :list="selectedList" teleport="#view" @confirm="removeAllSelect" />
|
||||
<search-list :list="list" :visible="isShowSearchBar" @action="handleMusicSearchAction" />
|
||||
<music-sort-modal v-model:show="isShowMusicSortModal" :music-info="selectedSortMusicInfo" :selected-num="selectedNum" @confirm="sortMusic" />
|
||||
<base-menu v-model="isShowItemMenu" :menus="menus" :xy="menuLocation" item-name="name" @menu-click="handleMenuClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { clipboardWriteText } from '@common/utils/electron'
|
||||
import { assertApiSupport } from '@renderer/store/utils'
|
||||
import SearchList from './components/SearchList'
|
||||
import MusicSortModal from './components/MusicSortModal'
|
||||
import useListInfo from './useListInfo'
|
||||
import useList from './useList'
|
||||
import useMenu from './useMenu'
|
||||
import usePlay from './usePlay'
|
||||
import useMusicDownload from './useMusicDownload'
|
||||
import useMusicAdd from './useMusicAdd'
|
||||
import useSort from './useSort'
|
||||
import useMusicActions from './useMusicActions'
|
||||
import useSearch from './useSearch'
|
||||
import useListScroll from './useListScroll'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
export default {
|
||||
name: 'MusicList',
|
||||
components: {
|
||||
SearchList,
|
||||
MusicSortModal,
|
||||
},
|
||||
props: {
|
||||
listId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['show-menu'],
|
||||
setup(props, { emit }) {
|
||||
const actionButtonsVisible = appSetting['list.actionButtonsVisible']
|
||||
|
||||
let scrollIndex = null
|
||||
let isAnimation = false
|
||||
const handleRestoreScroll = (_scrollIndex, _isAnimation) => {
|
||||
scrollIndex = _scrollIndex
|
||||
isAnimation = _isAnimation
|
||||
if (isAnimation) restoreScroll(scrollIndex, isAnimation)
|
||||
console.log('handleRestoreScroll', scrollIndex, isAnimation)
|
||||
}
|
||||
const onLoadedList = () => {
|
||||
console.log('restoreScroll', scrollIndex, isAnimation)
|
||||
restoreScroll(scrollIndex, isAnimation)
|
||||
}
|
||||
|
||||
const {
|
||||
rightClickSelectedIndex,
|
||||
selectedIndex,
|
||||
dom_listContent,
|
||||
listRef,
|
||||
list,
|
||||
playerInfo,
|
||||
setSelectedIndex,
|
||||
isShowSource,
|
||||
excludeListIds,
|
||||
} = useListInfo({ props, onLoadedList })
|
||||
|
||||
const {
|
||||
selectedList,
|
||||
listItemHeight,
|
||||
handleSelectData,
|
||||
removeAllSelect,
|
||||
} = useList({ list })
|
||||
|
||||
const {
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
doubleClickPlay,
|
||||
} = usePlay({ props, selectedList, list, removeAllSelect })
|
||||
|
||||
const {
|
||||
isShowListAdd,
|
||||
isMove,
|
||||
isShowListAddMultiple,
|
||||
isMoveMultiple,
|
||||
selectedAddMusicInfo,
|
||||
handleShowMusicAddModal,
|
||||
handleShowMusicMoveModal,
|
||||
} = useMusicAdd({ selectedList, list })
|
||||
|
||||
const {
|
||||
isShowDownload,
|
||||
isShowDownloadMultiple,
|
||||
selectedDownloadMusicInfo,
|
||||
handleShowDownloadModal,
|
||||
} = useMusicDownload({ selectedList, list })
|
||||
|
||||
const {
|
||||
isShowMusicSortModal,
|
||||
selectedNum,
|
||||
selectedSortMusicInfo,
|
||||
handleShowSortModal,
|
||||
sortMusic,
|
||||
} = useSort({ props, list, selectedList, removeAllSelect })
|
||||
|
||||
const {
|
||||
handleSearch,
|
||||
handleOpenMusicDetail,
|
||||
handleCopyName,
|
||||
handleRemoveMusic,
|
||||
} = useMusicActions({ props, list, removeAllSelect, selectedList })
|
||||
|
||||
const {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowItemMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
} = useMenu({
|
||||
assertApiSupport,
|
||||
emit,
|
||||
|
||||
handleShowDownloadModal,
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
handleSearch,
|
||||
handleShowMusicAddModal,
|
||||
handleShowMusicMoveModal,
|
||||
handleShowSortModal,
|
||||
handleOpenMusicDetail,
|
||||
handleCopyName,
|
||||
handleRemoveMusic,
|
||||
})
|
||||
|
||||
const {
|
||||
isShowSearchBar,
|
||||
searchList,
|
||||
handleMusicSearchAction,
|
||||
} = useSearch({
|
||||
setSelectedIndex,
|
||||
handlePlayMusic,
|
||||
listRef,
|
||||
})
|
||||
|
||||
const { saveListPosition, restoreScroll } = useListScroll({ props, listRef, list, handleRestoreScroll })
|
||||
|
||||
|
||||
const handleListItemClick = (event, index) => {
|
||||
if (rightClickSelectedIndex.value > -1) return
|
||||
handleSelectData(index)
|
||||
doubleClickPlay(index)
|
||||
}
|
||||
const handleListItemRightClick = (event, index) => {
|
||||
rightClickSelectedIndex.value = index
|
||||
showMenu(event, list.value[index], index)
|
||||
}
|
||||
const handleMenuClick = (action) => {
|
||||
let index = rightClickSelectedIndex.value
|
||||
rightClickSelectedIndex.value = -1
|
||||
menuClick(action, index)
|
||||
}
|
||||
const handleListRightClick = (event) => {
|
||||
if (!event.target.classList.contains('select')) return
|
||||
event.stopImmediatePropagation()
|
||||
let classList = dom_listContent.value.classList
|
||||
classList.add('copying')
|
||||
window.requestAnimationFrame(() => {
|
||||
let str = window.getSelection().toString()
|
||||
classList.remove('copying')
|
||||
str = str.split(/\n\n/).map(s => s.replace(/\n/g, ' ')).join('\n').trim()
|
||||
if (!str.length) return
|
||||
clipboardWriteText(str)
|
||||
})
|
||||
}
|
||||
const handleListBtnClick = ({ action, index }) => {
|
||||
switch (action) {
|
||||
case 'download':
|
||||
handleShowDownloadModal(index, true)
|
||||
break
|
||||
case 'play':
|
||||
handlePlayMusic(index, true)
|
||||
break
|
||||
case 'search':
|
||||
handleSearch(index)
|
||||
break
|
||||
case 'listAdd':
|
||||
handleShowMusicAddModal(index, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
const scrollToTop = () => {
|
||||
listRef.value.scrollTo(0, true)
|
||||
}
|
||||
|
||||
return {
|
||||
listItemHeight,
|
||||
handleListItemClick,
|
||||
selectedList,
|
||||
handleListItemRightClick,
|
||||
removeAllSelect,
|
||||
handleListBtnClick,
|
||||
rightClickSelectedIndex,
|
||||
selectedIndex,
|
||||
dom_listContent,
|
||||
listRef,
|
||||
excludeListIds,
|
||||
|
||||
menus,
|
||||
isShowItemMenu,
|
||||
menuLocation,
|
||||
handleMenuClick,
|
||||
|
||||
handleListRightClick,
|
||||
assertApiSupport,
|
||||
|
||||
isShowListAdd,
|
||||
isMove,
|
||||
isShowListAddMultiple,
|
||||
isMoveMultiple,
|
||||
selectedAddMusicInfo,
|
||||
|
||||
isShowMusicSortModal,
|
||||
selectedNum,
|
||||
selectedSortMusicInfo,
|
||||
sortMusic,
|
||||
|
||||
isShowDownload,
|
||||
isShowDownloadMultiple,
|
||||
selectedDownloadMusicInfo,
|
||||
|
||||
scrollToTop,
|
||||
|
||||
isShowSearchBar,
|
||||
searchList,
|
||||
handleMusicSearchAction,
|
||||
|
||||
list,
|
||||
playerInfo,
|
||||
|
||||
saveListPosition,
|
||||
isShowSource,
|
||||
handleRestoreScroll,
|
||||
|
||||
actionButtonsVisible,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.list {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
:global(.list-item) {
|
||||
&.active {
|
||||
color: var(--color-button-font);
|
||||
}
|
||||
}
|
||||
:global {
|
||||
.label-source {
|
||||
color: var(--color-primary);
|
||||
padding: 5px;
|
||||
font-size: .8em;
|
||||
line-height: 1;
|
||||
opacity: .75;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
min-height: 0;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.no-item {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
+15
-14
@@ -1,6 +1,7 @@
|
||||
import { computed, watch, ref, onBeforeUnmount } from '@renderer/utils/vueTools'
|
||||
import { windowSizeList, isFullscreen } from '@renderer/core/share'
|
||||
import { computed, watch, ref, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { isFullscreen } from '@renderer/store'
|
||||
import { getFontSizeWithScreen } from '@renderer/utils'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
|
||||
const useKeyEvent = ({ handleSelectAllData }) => {
|
||||
const keyEvent = {
|
||||
@@ -29,27 +30,27 @@ const useKeyEvent = ({ handleSelectAllData }) => {
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.eventHub.off('key_shift_down', handle_key_shift_down)
|
||||
window.eventHub.off('key_shift_up', handle_key_shift_up)
|
||||
window.eventHub.off('key_mod_down', handle_key_mod_down)
|
||||
window.eventHub.off('key_mod_up', handle_key_mod_up)
|
||||
window.eventHub.off('key_mod+a_down', handle_key_mod_a_down)
|
||||
window.key_event.off('key_shift_down', handle_key_shift_down)
|
||||
window.key_event.off('key_shift_up', handle_key_shift_up)
|
||||
window.key_event.off('key_mod_down', handle_key_mod_down)
|
||||
window.key_event.off('key_mod_up', handle_key_mod_up)
|
||||
window.key_event.off('key_mod+a_down', handle_key_mod_a_down)
|
||||
})
|
||||
window.eventHub.on('key_shift_down', handle_key_shift_down)
|
||||
window.eventHub.on('key_shift_up', handle_key_shift_up)
|
||||
window.eventHub.on('key_mod_down', handle_key_mod_down)
|
||||
window.eventHub.on('key_mod_up', handle_key_mod_up)
|
||||
window.eventHub.on('key_mod+a_down', handle_key_mod_a_down)
|
||||
window.key_event.on('key_shift_down', handle_key_shift_down)
|
||||
window.key_event.on('key_shift_up', handle_key_shift_up)
|
||||
window.key_event.on('key_mod_down', handle_key_mod_down)
|
||||
window.key_event.on('key_mod_up', handle_key_mod_up)
|
||||
window.key_event.on('key_mod+a_down', handle_key_mod_a_down)
|
||||
|
||||
return keyEvent
|
||||
}
|
||||
|
||||
export default ({ list, setting }) => {
|
||||
export default ({ list }) => {
|
||||
const selectedList = ref([])
|
||||
|
||||
let lastSelectIndex = -1
|
||||
const listItemHeight = computed(() => {
|
||||
return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : parseInt(windowSizeList.find(item => item.id == setting.value.windowSizeId).fontSize)) * 2.3)
|
||||
return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : appSetting['common.fontSize']) * 2.3)
|
||||
})
|
||||
|
||||
const removeAllSelect = () => {
|
||||
+14
-10
@@ -1,11 +1,10 @@
|
||||
import { ref, watch, useRefGetter, computed } from '@renderer/utils/vueTools'
|
||||
import { playMusicInfo, playInfo } from '@renderer/core/share/player'
|
||||
import { getList } from '@renderer/core/share/utils'
|
||||
import { ref, watch, computed } from '@common/utils/vueTools'
|
||||
import { playMusicInfo, playInfo } from '@renderer/store/player/state'
|
||||
import { getListMusics } from '@renderer/store/list/action'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
|
||||
|
||||
export default ({ props }) => {
|
||||
const setting = useRefGetter('setting')
|
||||
|
||||
export default ({ props, onLoadedList }) => {
|
||||
const rightClickSelectedIndex = ref(-1)
|
||||
const selectedIndex = ref(-1)
|
||||
const dom_listContent = ref(null)
|
||||
@@ -14,9 +13,15 @@ export default ({ props }) => {
|
||||
const excludeListIds = computed(() => ([props.listId]))
|
||||
|
||||
|
||||
const list = ref(getList(props.listId))
|
||||
const list = ref([])
|
||||
watch(() => props.listId, id => {
|
||||
list.value = getList(id)
|
||||
getListMusics(id).then(l => {
|
||||
list.value = l
|
||||
if (id != props.listId) return
|
||||
onLoadedList()
|
||||
})
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
const playerInfo = computed(() => ({
|
||||
@@ -28,7 +33,7 @@ export default ({ props }) => {
|
||||
selectedIndex.value = index
|
||||
}
|
||||
|
||||
const isShowSource = computed(() => setting.value.list.isShowSource)
|
||||
const isShowSource = computed(() => appSetting['list.isShowSource'])
|
||||
|
||||
return {
|
||||
rightClickSelectedIndex,
|
||||
@@ -38,7 +43,6 @@ export default ({ props }) => {
|
||||
list,
|
||||
playerInfo,
|
||||
setSelectedIndex,
|
||||
setting,
|
||||
isShowSource,
|
||||
excludeListIds,
|
||||
}
|
||||
+11
-9
@@ -1,7 +1,9 @@
|
||||
import { useRoute, useRouter, onMounted, onBeforeUnmount } from '@renderer/utils/vueTools'
|
||||
import { onMounted, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { useRoute, useRouter } from '@common/utils/vueRouter'
|
||||
import { setListPosition, getListPosition } from '@renderer/utils/data'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
|
||||
export default ({ props, listRef, list, setting }) => {
|
||||
export default ({ props, listRef, list, handleRestoreScroll }) => {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -10,16 +12,16 @@ export default ({ props, listRef, list, setting }) => {
|
||||
}
|
||||
|
||||
const handleScrollList = (index, isAnimation, callback = () => {}) => {
|
||||
listRef.value.scrollToIndex(index, -150, isAnimation).then(callback)
|
||||
listRef.value.scrollToIndex(index, -150, isAnimation, callback)
|
||||
}
|
||||
|
||||
const restoreScroll = (index, isAnimation) => {
|
||||
const restoreScroll = async(index, isAnimation) => {
|
||||
// console.log(index, isAnimation)
|
||||
if (!list.value.length) return
|
||||
if (index == null) {
|
||||
let location = getListPosition(props.listId) || 0
|
||||
if (setting.value.list.isSaveScrollLocation && location != null) {
|
||||
listRef.value.scrollTo(location)
|
||||
let location = await getListPosition(props.listId) || 0
|
||||
if (appSetting['list.isSaveScrollLocation'] && location != null) {
|
||||
listRef.value?.scrollTo(location)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -28,10 +30,10 @@ export default ({ props, listRef, list, setting }) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
restoreScroll(route.query.scrollIndex, false)
|
||||
handleRestoreScroll(route.query.scrollIndex, false)
|
||||
if (route.query.scrollIndex != null) {
|
||||
router.replace({
|
||||
path: 'list',
|
||||
path: '/list',
|
||||
query: {
|
||||
id: props.listId,
|
||||
updated: true,
|
||||
+12
-18
@@ -1,8 +1,8 @@
|
||||
import { computed, ref, reactive, useI18n, nextTick } from '@renderer/utils/vueTools'
|
||||
import musicSdk from '@renderer/utils/music'
|
||||
import { computed, ref, shallowReactive, reactive, nextTick } from '@common/utils/vueTools'
|
||||
import musicSdk from '@renderer/utils/musicSdk'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
|
||||
export default ({
|
||||
listRef,
|
||||
assertApiSupport,
|
||||
emit,
|
||||
|
||||
@@ -29,8 +29,8 @@ export default ({
|
||||
remove: true,
|
||||
sourceDetail: true,
|
||||
})
|
||||
const { t } = useI18n()
|
||||
const menuLocation = ref({ x: 0, y: 0 })
|
||||
const t = useI18n()
|
||||
const menuLocation = shallowReactive({ x: 0, y: 0 })
|
||||
const isShowItemMenu = ref(false)
|
||||
|
||||
const menus = computed(() => {
|
||||
@@ -89,22 +89,16 @@ export default ({
|
||||
})
|
||||
|
||||
const showMenu = (event, musicInfo) => {
|
||||
itemMenuControl.sourceDetail = !!musicSdk[musicInfo.source].getMusicDetailPageUrl
|
||||
itemMenuControl.sourceDetail = !!musicSdk[musicInfo.source]?.getMusicDetailPageUrl
|
||||
// itemMenuControl.play =
|
||||
// itemMenuControl.playLater =
|
||||
itemMenuControl.download = assertApiSupport(musicInfo.source)
|
||||
itemMenuControl.download = assertApiSupport(musicInfo.source) && musicInfo.source != 'local'
|
||||
|
||||
menuLocation.x = event.pageX
|
||||
menuLocation.y = event.pageY
|
||||
|
||||
if (isShowItemMenu.value) return
|
||||
|
||||
let dom_container = event.target.closest('#my-list')
|
||||
const getOffsetValue = (target, x = 0, y = 0) => {
|
||||
if (target === dom_container) return { x, y }
|
||||
if (!target) return { x: 0, y: 0 }
|
||||
x += target.offsetLeft
|
||||
y += target.offsetTop
|
||||
return getOffsetValue(target.offsetParent, x, y)
|
||||
}
|
||||
let { x, y } = getOffsetValue(event.target)
|
||||
menuLocation.value.x = x + event.offsetX
|
||||
menuLocation.value.y = y + event.offsetY - listRef.value.getScrollTop()
|
||||
emit('show-menu')
|
||||
nextTick(() => {
|
||||
isShowItemMenu.value = true
|
||||
+14
-13
@@ -1,20 +1,21 @@
|
||||
import { useCommit, useRouter, useI18n } from '@renderer/utils/vueTools'
|
||||
import musicSdk from '@renderer/utils/music'
|
||||
import { openUrl, clipboardWriteText } from '@renderer/utils'
|
||||
import { useRouter } from '@common/utils/vueRouter'
|
||||
import musicSdk from '@renderer/utils/musicSdk'
|
||||
import { openUrl, clipboardWriteText } from '@common/utils/electron'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { removeListMusics } from '@renderer/store/list/action'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { toOldMusicInfo } from '@renderer/utils/index'
|
||||
|
||||
|
||||
export default ({ props, list, setting, selectedList, removeAllSelect }) => {
|
||||
export default ({ props, list, selectedList, removeAllSelect }) => {
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const listRemove = useCommit('list', 'listRemove')
|
||||
const listRemoveMultiple = useCommit('list', 'listRemoveMultiple')
|
||||
const t = useI18n()
|
||||
|
||||
const handleSearch = index => {
|
||||
const info = list.value[index]
|
||||
router.push({
|
||||
path: 'search',
|
||||
path: '/search',
|
||||
query: {
|
||||
text: `${info.name} ${info.singer}`,
|
||||
},
|
||||
@@ -23,14 +24,14 @@ export default ({ props, list, setting, selectedList, removeAllSelect }) => {
|
||||
|
||||
const handleOpenMusicDetail = index => {
|
||||
const minfo = list.value[index]
|
||||
const url = musicSdk[minfo.source].getMusicDetailPageUrl(minfo)
|
||||
const url = musicSdk[minfo.source].getMusicDetailPageUrl(toOldMusicInfo(minfo))
|
||||
if (!url) return
|
||||
openUrl(url)
|
||||
}
|
||||
|
||||
const handleCopyName = index => {
|
||||
const minfo = list.value[index]
|
||||
clipboardWriteText(setting.value.download.fileName.replace('歌名', minfo.name).replace('歌手', minfo.singer))
|
||||
clipboardWriteText(appSetting['download.fileName'].replace('歌名', minfo.name).replace('歌手', minfo.singer))
|
||||
}
|
||||
|
||||
const handleRemoveMusic = async(index, single) => {
|
||||
@@ -43,10 +44,10 @@ export default ({ props, list, setting, selectedList, removeAllSelect }) => {
|
||||
: Promise.resolve(true)
|
||||
)
|
||||
if (!confirm) return
|
||||
listRemoveMultiple({ listId: props.listId, ids: selectedList.value.map(m => m.songmid) })
|
||||
removeListMusics({ listId: props.listId, ids: selectedList.value.map(m => m.id) })
|
||||
removeAllSelect()
|
||||
} else {
|
||||
listRemove({ listId: props.listId, id: list.value[index].songmid })
|
||||
removeListMusics({ listId: props.listId, ids: [list.value[index].id] })
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ref, nextTick } from '@renderer/utils/vueTools'
|
||||
import { ref, nextTick } from '@common/utils/vueTools'
|
||||
|
||||
export default ({ selectedList, list }) => {
|
||||
const isShowListAdd = ref(false)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { ref, nextTick } from '@renderer/utils/vueTools'
|
||||
import { ref, nextTick } from '@common/utils/vueTools'
|
||||
|
||||
export default ({ selectedList, list }) => {
|
||||
const isShowDownload = ref(false)
|
||||
+5
-10
@@ -1,25 +1,20 @@
|
||||
import { useCommit } from '@renderer/utils/vueTools'
|
||||
import { addTempPlayList } from '@renderer/store/player/action'
|
||||
import { playList } from '@renderer/core/player'
|
||||
|
||||
export default ({ props, selectedList, list, removeAllSelect }) => {
|
||||
let clickTime = 0
|
||||
let clickIndex = -1
|
||||
|
||||
const setList = useCommit('player', 'setList')
|
||||
const setTempPlayList = useCommit('player', 'setTempPlayList')
|
||||
|
||||
const handlePlayMusic = (index) => {
|
||||
setList({
|
||||
listId: props.listId,
|
||||
index,
|
||||
})
|
||||
playList(props.listId, index)
|
||||
}
|
||||
|
||||
const handlePlayMusicLater = (index, single) => {
|
||||
if (selectedList.value.length && !single) {
|
||||
setTempPlayList(selectedList.value.map(s => ({ listId: props.listId, musicInfo: s })))
|
||||
addTempPlayList(selectedList.value.map(s => ({ listId: props.listId, musicInfo: s })))
|
||||
removeAllSelect()
|
||||
} else {
|
||||
setTempPlayList([{ listId: props.listId, musicInfo: list.value[index] }])
|
||||
addTempPlayList([{ listId: props.listId, musicInfo: list.value[index] }])
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { ref, onBeforeUnmount } from '@renderer/utils/vueTools'
|
||||
import { ref, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
|
||||
export default ({ setSelectedIndex, handlePlayMusic, listRef }) => {
|
||||
const isShowSearchBar = ref(false)
|
||||
@@ -13,7 +13,7 @@ export default ({ setSelectedIndex, handlePlayMusic, listRef }) => {
|
||||
switch (action) {
|
||||
case 'listClick':
|
||||
if (index < 0) return
|
||||
listRef.value.scrollToIndex(index, -150, true).then(() => {
|
||||
listRef.value.scrollToIndex(index, -150, true, () => {
|
||||
setSelectedIndex(index)
|
||||
setTimeout(() => {
|
||||
setSelectedIndex(-1)
|
||||
@@ -24,10 +24,10 @@ export default ({ setSelectedIndex, handlePlayMusic, listRef }) => {
|
||||
}
|
||||
}
|
||||
|
||||
window.eventHub.on('key_mod+f_down', handleShowSearchBar)
|
||||
window.key_event.on('key_mod+f_down', handleShowSearchBar)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.eventHub.off('key_mod+f_down', handleShowSearchBar)
|
||||
window.key_event.off('key_mod+f_down', handleShowSearchBar)
|
||||
})
|
||||
|
||||
return {
|
||||
+6
-7
@@ -1,12 +1,11 @@
|
||||
import { ref, nextTick, useCommit } from '@renderer/utils/vueTools'
|
||||
import { updateListMusicsPosition } from '@renderer/store/list/action'
|
||||
import { ref, nextTick } from '@common/utils/vueTools'
|
||||
|
||||
export default ({ props, list, selectedList, removeAllSelect }) => {
|
||||
const isShowMusicSortModal = ref(false)
|
||||
const selectedNum = ref(0)
|
||||
const musicInfo = ref(null)
|
||||
|
||||
const setMusicPosition = useCommit('list', 'setMusicPosition')
|
||||
|
||||
const handleShowSortModal = (index, single) => {
|
||||
if (selectedList.value.length && !single) {
|
||||
selectedNum.value = selectedList.value.length
|
||||
@@ -21,10 +20,10 @@ export default ({ props, list, selectedList, removeAllSelect }) => {
|
||||
|
||||
const sortMusic = num => {
|
||||
num = Math.min(num, list.value.length)
|
||||
setMusicPosition({
|
||||
id: props.listId,
|
||||
position: num,
|
||||
list: selectedNum.value ? [...selectedList.value] : [musicInfo.value],
|
||||
updateListMusicsPosition({
|
||||
listId: props.listId,
|
||||
position: num - 1,
|
||||
ids: (selectedNum.value ? [...selectedList.value] : [musicInfo.value]).map(m => m.id),
|
||||
})
|
||||
removeAllSelect()
|
||||
isShowMusicSortModal.value = false
|
||||
@@ -0,0 +1,28 @@
|
||||
import { addListMusics, setFetchingListStatus } from '@renderer/store/list/action'
|
||||
import { showSelectDialog } from '@renderer/utils/ipc'
|
||||
|
||||
|
||||
const handleAddMusics = async(listId: string, filePaths: string[], index: number = -1) => {
|
||||
// console.log(index + 1, index + 201)
|
||||
const paths = filePaths.slice(index + 1, index + 201)
|
||||
const musicInfos = await window.lx.worker.main.createLocalMusicInfos(paths)
|
||||
if (musicInfos.length) await addListMusics(listId, musicInfos)
|
||||
index += 200
|
||||
if (filePaths.length - 1 > index) await handleAddMusics(listId, filePaths, index)
|
||||
}
|
||||
export const addLocalFile = async(listInfo: LX.List.MyListInfo) => {
|
||||
const { canceled, filePaths } = await showSelectDialog({
|
||||
title: window.i18n.t('lists__add_local_file_desc'),
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: [
|
||||
{ name: 'Media File', extensions: ['mp3', 'flac', 'ogg', 'wav'] },
|
||||
// { name: 'All Files', extensions: ['*'] },
|
||||
],
|
||||
})
|
||||
if (canceled || !filePaths.length) return
|
||||
|
||||
console.log(filePaths)
|
||||
setFetchingListStatus(listInfo.id, true)
|
||||
await handleAddMusics(listInfo.id, filePaths)
|
||||
setFetchingListStatus(listInfo.id, false)
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<material-modal :show="visible" bg-close teleport="#view" width="60%" max-width="900px" @close="$emit('update:visible', false)">
|
||||
<div :class="$style.header">
|
||||
<h2>{{ listInfo.name }}</h2>
|
||||
</div>
|
||||
<base-virtualized-list
|
||||
v-if="duplicateList.length" v-slot="{ item, index }" :list="duplicateList" key-name="id" :class="$style.list" style="contain: none;"
|
||||
:item-height="listItemHeight" container-class="scroll" content-class="list"
|
||||
>
|
||||
<div :class="$style.listItem">
|
||||
<div :class="$style.num">{{ item.index + 1 }}</div>
|
||||
<div :class="$style.textContent">
|
||||
<h3 :class="$style.text" :aria-label="`${item.musicInfo.name} - ${item.musicInfo.singer}`">{{ item.musicInfo.name }} - {{ item.musicInfo.singer }}</h3>
|
||||
<h3 v-if="item.musicInfo.meta.albumName" :class="[$style.text, $style.albumName]" :aria-label="item.musicInfo.meta.albumName">{{ item.musicInfo.meta.albumName }}</h3>
|
||||
</div>
|
||||
<div :class="$style.label">{{ item.musicInfo.source }}</div>
|
||||
<div :class="$style.label">{{ item.musicInfo.interval }}</div>
|
||||
<div :class="$style.btns">
|
||||
<button type="button" :class="$style.btn" @click="handlePlay(index)">
|
||||
<svg v-once version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="50%" viewBox="0 0 287.386 287.386" space="preserve">
|
||||
<use xlink:href="#icon-testPlay" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" :class="$style.btn" @click="handleRemove(index)">
|
||||
<svg v-once version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="50%" viewBox="0 0 212.982 212.982" space="preserve">
|
||||
<use xlink:href="#icon-delete" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</base-virtualized-list>
|
||||
<div v-else :class="$style.noItem">
|
||||
<p v-text="$t('no_item')" />
|
||||
</div>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { shallowReactive, watch, toRaw, computed } from '@common/utils/vueTools'
|
||||
import { playList } from '@renderer/core/player'
|
||||
import { getListMusics, removeListMusics } from '@renderer/store/list/action'
|
||||
import { isFullscreen } from '@renderer/store'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { getFontSizeWithScreen } from '@renderer/utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
listInfo: { // { id: '', name: '' }
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:visible'],
|
||||
setup(props) {
|
||||
const duplicateList = shallowReactive([])
|
||||
const listItemHeight = computed(() => {
|
||||
return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : appSetting['common.fontSize']) * 3.2)
|
||||
})
|
||||
|
||||
const handlePlay = (index) => {
|
||||
const { index: musicInfoIndex } = duplicateList[index]
|
||||
playList(props.listInfo.id, musicInfoIndex)
|
||||
}
|
||||
const handleFilterList = async() => {
|
||||
// console.time('filter')
|
||||
duplicateList.splice(
|
||||
0,
|
||||
duplicateList.length,
|
||||
...(await window.lx.worker.main.filterDuplicateMusic(toRaw(await getListMusics(props.listInfo.id)))))
|
||||
|
||||
// console.timeEnd('filter')
|
||||
}
|
||||
const handleRemove = async(index) => {
|
||||
const { musicInfo: targetMusicInfo } = duplicateList.splice(index, 1)[0]
|
||||
await removeListMusics({ listId: props.listInfo.id, ids: [targetMusicInfo.id] })
|
||||
await handleFilterList()
|
||||
}
|
||||
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
if (duplicateList.length) duplicateList.splice(0, duplicateList.length)
|
||||
handleFilterList()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
listItemHeight,
|
||||
duplicateList,
|
||||
handleFilterList,
|
||||
handleRemove,
|
||||
handlePlay,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.header {
|
||||
flex: none;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
h2 {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
.main {
|
||||
min-height: 200px;
|
||||
min-width: 460px;
|
||||
// display: flex;
|
||||
// flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.list {
|
||||
min-height: 200px;
|
||||
min-width: 460px;
|
||||
// background-color: @color-search-form-background;
|
||||
font-size: 13px;
|
||||
transition-property: height;
|
||||
// position: relative;
|
||||
.listItem {
|
||||
position: relative;
|
||||
padding: 0 5px;
|
||||
transition: background-color .2s ease;
|
||||
line-height: 1.4;
|
||||
height: 100%;
|
||||
// overflow: hidden;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
// border-radius: 4px;
|
||||
// &:last-child {
|
||||
// border-bottom-left-radius: 4px;
|
||||
// border-bottom-right-radius: 4px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
.num {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
|
||||
.textContent {
|
||||
flex: auto;
|
||||
padding-left: 5px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
}
|
||||
.text {
|
||||
max-width: 100%;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.albumName {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
// .mixin-ellipsis-1;
|
||||
}
|
||||
.label {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// transform: rotate(45deg);
|
||||
// background-color:
|
||||
}
|
||||
.btns {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: @form-radius;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
padding: 4px 7px;
|
||||
color: var(--color-button-font);
|
||||
outline: none;
|
||||
transition: background-color 0.2s ease;
|
||||
line-height: 0;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-primary-font-active);
|
||||
}
|
||||
}
|
||||
|
||||
.no-item {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<material-modal :show="visible" teleport="#view" bg-close @close="closeModal" @after-leave="handleAfterLeave">
|
||||
<main :class="$style.main">
|
||||
<div :class="$style.header">
|
||||
<h2>{{ listInfo.name }}</h2>
|
||||
</div>
|
||||
<section>
|
||||
<h3 :class="$style.title">{{ $t('list_sort_modal_by_field') }}</h3>
|
||||
<ul :class="$style.list">
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_field_name" v-model="sortField" name="list_sort_modal_field" :aria-label="$t('list_sort_modal_by_name')"
|
||||
need="need" value="name" :label="$t('list_sort_modal_by_name')"
|
||||
/>
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_field_singer" v-model="sortField" name="list_sort_modal_field"
|
||||
need="need" value="singer" :label="$t('list_sort_modal_by_singer')"
|
||||
/>
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_field_album" v-model="sortField" name="list_sort_modal_field"
|
||||
need="need" value="album" :label="$t('list_sort_modal_by_album')"
|
||||
/>
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_field_time" v-model="sortField" name="list_sort_modal_field"
|
||||
need="need" value="interval" :label="$t('list_sort_modal_by_time')"
|
||||
/>
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_field_source" v-model="sortField" name="list_sort_modal_field"
|
||||
need="need" value="source" :label="$t('list_sort_modal_by_source')"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h3 :class="$style.title">{{ $t('list_sort_modal_by_type') }}</h3>
|
||||
<ul :class="$style.list">
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_type_up" v-model="sortType" name="list_sort_modal_type"
|
||||
need="need" value="up" :label="$t('list_sort_modal_by_up')"
|
||||
/>
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_type_down" v-model="sortType" name="list_sort_modal_type"
|
||||
need="need" value="down" :label="$t('list_sort_modal_by_down')"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div :class="$style.footer">
|
||||
<base-btn :class="$style.btn" @click="closeModal">{{ $t('btn_cancel') }}</base-btn>
|
||||
<base-btn :class="$style.btn" @click="handleSort">{{ $t('btn_confirm') }}</base-btn>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
// import { dialog } from '@renderer/plugins/Dialog'
|
||||
import { getListMusics, updateListMusicsPosition } from '@renderer/store/list/action'
|
||||
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
listInfo: { // { id: '', name: '' }
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:visible'],
|
||||
setup(props, { emit }) {
|
||||
// const { t } = useI18n()
|
||||
const sortField = ref('')
|
||||
const sortType = ref('')
|
||||
const closeModal = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
const handleAfterLeave = () => {
|
||||
sortField.value = ''
|
||||
sortType.value = ''
|
||||
}
|
||||
const verify = () => {
|
||||
return !!sortField.value && !!sortType.value
|
||||
}
|
||||
const handleSort = async() => {
|
||||
if (!verify()) return
|
||||
// if (!await dialog.confirm({
|
||||
// message: t('list_sort_modal_tip_confirm'),
|
||||
// cancelButtonText: t('cancel_button_text'),
|
||||
// confirmButtonText: t('confirm_button_text'),
|
||||
// })) return
|
||||
|
||||
let list = [...(await getListMusics(props.listInfo.id))]
|
||||
list = await window.lx.worker.main.sortListMusicInfo(list, sortType.value, sortField.value, window.i18n.locale)
|
||||
console.log(sortType.value, sortField.value)
|
||||
|
||||
closeModal()
|
||||
updateListMusicsPosition({ listId: props.listInfo.id, position: 0, ids: list.map(m => m.id) })
|
||||
}
|
||||
return {
|
||||
sortField,
|
||||
sortType,
|
||||
closeModal,
|
||||
handleSort,
|
||||
handleAfterLeave,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.header {
|
||||
flex: none;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
h2 {
|
||||
color: var(--color-font);
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 0 15px;
|
||||
width: 320px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-height: 0;
|
||||
// max-height: 100%;
|
||||
// overflow: hidden;
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: var(--color-font-label);
|
||||
padding: 10px 0 8px;
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
.listItem {
|
||||
width: (100% / 3);
|
||||
padding-left: 10px;
|
||||
margin-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.footer {
|
||||
margin: 20px 0 15px auto;
|
||||
}
|
||||
.btn {
|
||||
// box-sizing: border-box;
|
||||
// margin-left: 15px;
|
||||
// margin-bottom: 15px;
|
||||
// height: 36px;
|
||||
// line-height: 36px;
|
||||
// padding: 0 10px !important;
|
||||
min-width: 70px;
|
||||
// .mixin-ellipsis-1;
|
||||
|
||||
+.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<material-modal :show="visible" bg-close teleport="#view" @close="$emit('update:visible', false)">
|
||||
<div :class="$style.header">
|
||||
<h2>{{ $t('list_update_modal__title') }}</h2>
|
||||
</div>
|
||||
<main class="scroll" :class="$style.main">
|
||||
<ul v-if="lists.length" ref="dom_list" :class="$style.list">
|
||||
<li v-for="list in lists" :key="list.id" :class="[$style.listItem, {[$style.fetching]: fetchingListStatus[list.id]}]">
|
||||
<div :class="$style.listLeft">
|
||||
<h3 :class="$style.text">{{ list.name }} <span :class="$style.label">{{ list.source }}</span></h3>
|
||||
<div>
|
||||
<base-checkbox
|
||||
:id="`list_auto_update_${list.id}`" :model-value="updateInfo[list.id]?.isAutoUpdate == true"
|
||||
:class="$style.checkbox" :label="$t('list_update_modal__auto_update')" @change="handleChangeAutoUpdate(list, $event)"
|
||||
/>
|
||||
<span :class="$style.label" style="vertical-align: text-top;">{{ listUpdateTimes[list.id] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.btns">
|
||||
<button :class="$style.btn" :disabled="fetchingListStatus[list.id]" outline="outline" :aria-label="$t('list_update_modal__update')" @click.stop="handleUpdate(list)">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" style="transform: rotate(45deg);" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-refresh" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else :class="$style.noItem">
|
||||
<p v-text="$t('no_item')" />
|
||||
</div>
|
||||
</main>
|
||||
<div :class="$style.footer">
|
||||
<div :class="$style.tips">{{ $t('list_update_modal__tips') }}</div>
|
||||
</div>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, ref } from '@common/utils/vueTools'
|
||||
import { userLists, fetchingListStatus, listUpdateTimes } from '@renderer/store/list/state'
|
||||
import handleSyncSourceList from '@renderer/store/list/syncSourceList'
|
||||
import musicSdk from '@renderer/utils/musicSdk'
|
||||
// import { dateFormat } from '@common/utils/renderer'
|
||||
import { getListUpdateInfo, setListAutoUpdate } from '@renderer/utils/data'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:visible'],
|
||||
setup() {
|
||||
const lists = computed(() => userLists.filter(l => !!l.source && !!musicSdk[l.source]?.songList))
|
||||
const updateInfo = ref({})
|
||||
// const updateTimes = ref({})
|
||||
|
||||
getListUpdateInfo().then((listUpdateInfo) => {
|
||||
updateInfo.value = listUpdateInfo
|
||||
// if (listUpdateTimes._inited) {
|
||||
// for (const [id, value] of Object.entries(info)) {
|
||||
// autoUpdate[id] = value.isAutoUpdate == true
|
||||
// }
|
||||
// } else {
|
||||
// for (const [id, value] of Object.entries(info)) {
|
||||
// autoUpdate[id] = value.isAutoUpdate == true
|
||||
// listUpdateTimes[id] = value.updateTime ? dateFormat(value.updateTime) : ''
|
||||
// }
|
||||
// }
|
||||
// listUpdateTimes._inited = true
|
||||
})
|
||||
|
||||
const handleUpdate = (targetListInfo) => {
|
||||
handleSyncSourceList(targetListInfo)
|
||||
// console.log(targetListInfo.list.length, list.length)
|
||||
}
|
||||
|
||||
const handleChangeAutoUpdate = (list, enable) => {
|
||||
setListAutoUpdate(list.id, enable)
|
||||
}
|
||||
|
||||
return {
|
||||
lists,
|
||||
updateInfo,
|
||||
fetchingListStatus,
|
||||
handleUpdate,
|
||||
handleChangeAutoUpdate,
|
||||
listUpdateTimes,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
@width: 460px;
|
||||
|
||||
.header {
|
||||
flex: none;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
h2 {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
.main {
|
||||
min-height: 200px;
|
||||
width: @width;
|
||||
}
|
||||
|
||||
.list {
|
||||
// background-color: @color-search-form-background;
|
||||
font-size: 13px;
|
||||
transition-property: height;
|
||||
position: relative;
|
||||
.listItem {
|
||||
position: relative;
|
||||
padding: 15px 10px 15px 15px;
|
||||
transition: .3s ease;
|
||||
transition-property: background-color, opacity;
|
||||
line-height: 1.3;
|
||||
// overflow: hidden;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
// border-radius: 4px;
|
||||
// &:last-child {
|
||||
// border-bottom-left-radius: 4px;
|
||||
// border-bottom-right-radius: 4px;
|
||||
// }
|
||||
&.fetching {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listLeft {
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: auto;
|
||||
margin-bottom: 2px;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.checkbox {
|
||||
margin-top: 3px;
|
||||
font-size: 14px;
|
||||
opacity: .86;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
padding: 0 10px;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// transform: rotate(45deg);
|
||||
// background-color:
|
||||
}
|
||||
.btns {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: @form-radius;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
padding: 4px 7px;
|
||||
color: var(--color-button-font);
|
||||
outline: none;
|
||||
transition: background-color 0.2s ease;
|
||||
line-height: 0;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-primary-font-active);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: @width;
|
||||
}
|
||||
.tips {
|
||||
padding: 8px 15px;
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
color: var(--color-font);
|
||||
}
|
||||
|
||||
.no-item {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,259 @@
|
||||
<!-- <template>
|
||||
<teleport to="#root">
|
||||
<div ref="dom_menu" :class="$style.container" :style="menuStyles" :aria-hidden="!modelValue">
|
||||
<div :class="$style.group">
|
||||
<div :class="$style.subGroup">
|
||||
<button :class="$style.btn" :disabled="!itemMenuControl.rename" ignore-tip :aria-label="$t('lists__rename')" @click="menuClick('rename')">{{ $t('lists__rename') }}</button>
|
||||
<button :class="$style.btn" :disabled="offsetDisabled" ignore-tip :aria-label="$t('lists__sync')" @click="menuClick('sync')">{{ $t('lists__sync') }}</button>
|
||||
</div>
|
||||
<div :class="$style.subGroup">
|
||||
<button :class="$style.btn" :disabled="offsetDisabled" ignore-tip :aria-label="$t('lists__sort_list')" @click="menuClick('sort')">{{ $t('lists__sort_list') }}</button>
|
||||
<button :class="$style.btn" :disabled="offsetDisabled" ignore-tip :aria-label="$t('lyric_menu__offset_dec_100')" @click="menuClick(-100)">- 100ms</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, useRefGetter, ref, useCommit, watch } from '@common/utils/vueTools'
|
||||
import useMenuLocation from '@renderer/utils/compositions/useMenuLocation'
|
||||
import { setLyricEdited, removeLyricEdited, debounce } from '@renderer/utils'
|
||||
|
||||
const offsetTagRxp = /(?:^|\n)\s*\[offset:\s*(\S+(?:\d+)*)\s*\]/
|
||||
const offsetTagAllRxp = /(?:^|\n)\s*\[offset:\s*(\S+(?:\d+)*)\s*\]/g
|
||||
|
||||
const saveLyric = debounce((musicInfo, lyricInfo) => {
|
||||
setLyricEdited(musicInfo, lyricInfo)
|
||||
})
|
||||
const removeLyric = debounce(musicInfo => {
|
||||
removeLyricEdited(musicInfo)
|
||||
})
|
||||
|
||||
const getOffset = lrc => {
|
||||
let offset = offsetTagRxp.exec(lrc)
|
||||
if (offset) {
|
||||
offset = parseInt(offset[1])
|
||||
if (Number.isNaN(offset)) offset = 0
|
||||
} else offset = 0
|
||||
return offset
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'LyricMenu',
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
xy: Object,
|
||||
lyricInfo: Object,
|
||||
},
|
||||
emits: ['updateLyric', 'update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const setting = useRefGetter('setting')
|
||||
const playDetailSetting = useRefGetter('playDetailSetting')
|
||||
const setPlayDetailLyricAlign = useCommit('setPlayDetailLyricAlign')
|
||||
const setPlayDetailLyricFont = useCommit('setPlayDetailLyricFont')
|
||||
|
||||
const offset = ref(0)
|
||||
const offsetDisabled = ref(true)
|
||||
const originOffset = ref(0)
|
||||
|
||||
const visible = computed(() => props.modelValue)
|
||||
const musicInfo = computed(() => props.lyricInfo.musicInfo)
|
||||
const location = computed(() => props.xy)
|
||||
|
||||
const onHide = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
const setFontAlign = val => {
|
||||
if (playDetailSetting.value.style.align == val) return
|
||||
setPlayDetailLyricAlign(val)
|
||||
}
|
||||
|
||||
const fontSizeUp = step => {
|
||||
if (setting.value.playDetail.style.fontSize >= 200) return
|
||||
setPlayDetailLyricFont(Math.min(setting.value.playDetail.style.fontSize + step, 200))
|
||||
}
|
||||
const fontSizeDown = step => {
|
||||
if (setting.value.playDetail.style.fontSize <= 70) return
|
||||
setPlayDetailLyricFont(Math.max(setting.value.playDetail.style.fontSize - step, 70))
|
||||
}
|
||||
const fontSizeReset = () => {
|
||||
setPlayDetailLyricFont(100)
|
||||
}
|
||||
|
||||
const updateLyric = offset => {
|
||||
let lyric = props.lyricInfo.lyric
|
||||
let tlyric = props.lyricInfo.tlyric
|
||||
let rlyric = props.lyricInfo.rlyric
|
||||
let lxlyric = props.lyricInfo.lxlyric
|
||||
if (offsetTagRxp.test(lyric)) {
|
||||
lyric = lyric.replace(offsetTagAllRxp, `[offset:${offset}]`)
|
||||
if (tlyric) tlyric = tlyric.replace(offsetTagAllRxp, `[offset:${offset}]`)
|
||||
if (lxlyric) lxlyric = lxlyric.replace(offsetTagAllRxp, `[offset:${offset}]`)
|
||||
if (rlyric) rlyric = rlyric.replace(offsetTagAllRxp, `[offset:${offset}]`)
|
||||
} else {
|
||||
lyric = `[offset:${offset}]\n` + lyric
|
||||
if (tlyric) tlyric = `[offset:${offset}]\n` + tlyric
|
||||
if (lxlyric) lxlyric = `[offset:${offset}]\n` + lxlyric
|
||||
if (rlyric) rlyric = `[offset:${offset}]\n` + rlyric
|
||||
}
|
||||
|
||||
if (offset == originOffset.value) {
|
||||
removeLyric(props.lyricInfo.musicInfo)
|
||||
} else {
|
||||
saveLyric(props.lyricInfo.musicInfo, {
|
||||
lyric,
|
||||
tlyric,
|
||||
rlyric,
|
||||
lxlyric,
|
||||
})
|
||||
}
|
||||
|
||||
emit('updateLyric', {
|
||||
lyric,
|
||||
tlyric,
|
||||
rlyric,
|
||||
lxlyric,
|
||||
offset,
|
||||
})
|
||||
}
|
||||
const setOffset = step => {
|
||||
offset.value += step
|
||||
updateLyric(offset.value)
|
||||
}
|
||||
const offsetReset = () => {
|
||||
if (offset.value == originOffset.value) return
|
||||
offset.value = originOffset.value
|
||||
updateLyric(originOffset.value)
|
||||
}
|
||||
|
||||
const parseLrcOffset = () => {
|
||||
offset.value = getOffset(props.lyricInfo.lyric)
|
||||
originOffset.value = getOffset(props.lyricInfo.rawlyric)
|
||||
offsetDisabled.value = !props.lyricInfo.lyric
|
||||
}
|
||||
|
||||
|
||||
const { dom_menu, menuStyles } = useMenuLocation({
|
||||
visible,
|
||||
location,
|
||||
onHide,
|
||||
})
|
||||
|
||||
watch(musicInfo, () => {
|
||||
if (!props.modelValue) return
|
||||
parseLrcOffset()
|
||||
})
|
||||
watch(visible, val => {
|
||||
if (!val) return
|
||||
parseLrcOffset()
|
||||
})
|
||||
|
||||
return {
|
||||
dom_menu,
|
||||
menuStyles,
|
||||
playDetailSetting,
|
||||
offset,
|
||||
originOffset,
|
||||
fontSizeUp,
|
||||
fontSizeDown,
|
||||
fontSizeReset,
|
||||
setOffset,
|
||||
offsetReset,
|
||||
setFontAlign,
|
||||
offsetDisabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.container {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transform-origin: 0 0 0;
|
||||
transition: .25s ease;
|
||||
transition-property: transform, opacity;
|
||||
border-radius: @radius-border;
|
||||
background-color: var(--color-main-background);
|
||||
box-shadow: 0 1px 8px 0 rgba(0,0,0,.2);
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.title {
|
||||
flex: auto;
|
||||
padding: 10px 0 10px 10px;
|
||||
color: var(--color-font-label);
|
||||
white-space: nowrap;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.subGroup {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: auto;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
min-width: 60px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// color: var(--color-button-font);
|
||||
padding: 0 10px;
|
||||
outline: none;
|
||||
transition: @transition-normal;
|
||||
transition-property: background-color, opacity;
|
||||
box-sizing: border-box;
|
||||
.mixin-ellipsis-1;
|
||||
background-color: var(--color-main-background);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
opacity: .7;
|
||||
background-color: @color-theme_2-hover;
|
||||
}
|
||||
&:active {
|
||||
background-color: @color-theme_2-active;
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-main-background);
|
||||
color: var(--color-button-background-active);
|
||||
cursor: default;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
cursor: default;
|
||||
opacity: .4;
|
||||
&:hover {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.titleBtn {
|
||||
flex: none;
|
||||
padding: 0 10;
|
||||
min-width: 40px;
|
||||
opacity: .7;
|
||||
|
||||
&[disabled] {
|
||||
opacity: .3;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
-->
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<material-modal :show="show" teleport="#view" @close="handleClose" @after-enter="$refs.input.focus()">
|
||||
<main :class="$style.main">
|
||||
<h2>{{ selectedNum > 0 ? $t('music_sort__title_multiple', { num: selectedNum }) : $t('music_sort__title', { name: musicInfo ? musicInfo.name : '' }) }}</h2>
|
||||
<base-input
|
||||
ref="input"
|
||||
v-model="sortNum"
|
||||
:class="$style.input"
|
||||
type="number"
|
||||
:placeholder="$t('music_sort__input_tip')"
|
||||
@submit="handleSubmit" @blur="verify"
|
||||
/>
|
||||
<div :class="$style.footer">
|
||||
<base-btn :class="$style.btn" @click="handleSubmit">{{ $t('btn_confirm') }}</base-btn>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
musicInfo: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
selectedNum: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
emits: ['update:show', 'confirm'],
|
||||
data() {
|
||||
return {
|
||||
sortNum: '',
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(n) {
|
||||
if (n) {
|
||||
this.sortNum = ''
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:show', false)
|
||||
},
|
||||
verify() {
|
||||
let num = /^[1-9]\d*/.exec(this.sortNum)
|
||||
num = num ? parseInt(num[0]) : ''
|
||||
this.sortNum = num.toString()
|
||||
return num
|
||||
},
|
||||
handleSubmit() {
|
||||
let num = this.verify()
|
||||
if (this.sortNum == '') return
|
||||
this.handleClose()
|
||||
this.$emit('confirm', num)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.main {
|
||||
padding: 0 15px;
|
||||
max-width: 530px;
|
||||
min-width: 280px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-height: 0;
|
||||
// max-height: 100%;
|
||||
// overflow: hidden;
|
||||
h2 {
|
||||
font-size: 13px;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
word-break: break-all;
|
||||
// text-align: center;
|
||||
padding: 15px 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
// width: 100%;
|
||||
// height: 26px;
|
||||
padding: 8px 8px;
|
||||
}
|
||||
.footer {
|
||||
margin: 20px 0 15px auto;
|
||||
}
|
||||
.btn {
|
||||
// box-sizing: border-box;
|
||||
// margin-left: 15px;
|
||||
// margin-bottom: 15px;
|
||||
// height: 36px;
|
||||
// line-height: 36px;
|
||||
// padding: 0 10px !important;
|
||||
min-width: 70px;
|
||||
// .mixin-ellipsis-1;
|
||||
|
||||
+.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div ref="dom_lists" :class="$style.lists">
|
||||
<div :class="$style.listHeader">
|
||||
<h2 :class="$style.listsTitle">{{ $t('my_list') }}</h2>
|
||||
<div :class="$style.headerBtns">
|
||||
<button :class="$style.listsAdd" :aria-label="$t('lists__new_list_btn')" @click="isShowNewList = true">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="70%" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-list-add" />
|
||||
</svg>
|
||||
</button>
|
||||
<button :class="$style.listsAdd" :aria-label="$t('list_update_modal__title')" @click="isShowListUpdateModal = true">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" style="transform: rotate(45deg);" height="70%" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-refresh" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul ref="dom_lists_list" class="scroll" :class="[$style.listsContent, { [$style.sortable]: isModDown }]">
|
||||
<li
|
||||
class="default-list" :class="[$style.listsItem, {[$style.active]: defaultList.id == listId}, {[$style.clicked]: rightClickItemIndex == -2}, {[$style.fetching]: fetchingListStatus[defaultList.id]}]"
|
||||
:aria-label="defaultList.name" :aria-selected="defaultList.id == listId"
|
||||
@contextmenu="handleListsItemRigthClick($event, -2)" @click="handleListToggle(defaultList.id)"
|
||||
>
|
||||
<span :class="$style.listsLabel">{{ defaultList.name }}</span>
|
||||
</li>
|
||||
<li
|
||||
class="default-list" :class="[$style.listsItem, {[$style.active]: loveList.id == listId}, {[$style.clicked]: rightClickItemIndex == -1}, {[$style.fetching]: fetchingListStatus[loveList.id]}]"
|
||||
:aria-label="loveList.name" :aria-selected="loveList.id == listId"
|
||||
@contextmenu="handleListsItemRigthClick($event, -1)" @click="handleListToggle(loveList.id)"
|
||||
>
|
||||
<span :class="$style.listsLabel">{{ loveList.name }}</span>
|
||||
</li>
|
||||
<li
|
||||
v-for="(item, index) in userLists"
|
||||
:key="item.id" class="user-list"
|
||||
:class="[$style.listsItem, {[$style.active]: item.id == listId}, {[$style.clicked]: rightClickItemIndex == index}, {[$style.fetching]: fetchingListStatus[item.id]}]"
|
||||
:data-index="index" :aria-label="item.name" :aria-selected="defaultList.id == listId" @contextmenu="handleListsItemRigthClick($event, index)"
|
||||
>
|
||||
<span :class="$style.listsLabel" @click="handleListToggle(item.id, index + 2)">{{ item.name }}</span>
|
||||
<input
|
||||
:class="$style.listsInput" type="text" :value="item.name"
|
||||
:placeholder="item.name" @contextmenu.stop @keyup.enter="handleSaveListName(index, $event)" @blur="handleSaveListName(index, $event)"
|
||||
>
|
||||
</li>
|
||||
<transition enter-active-class="animated-fast slideInLeft" leave-active-class="animated-fast fadeOut" @after-leave="isNewListLeave = false" @after-enter="$refs.dom_listsNewInput.focus()">
|
||||
<li v-if="isShowNewList" :class="[$style.listsItem, $style.listsNew, {[$style.newLeave]: isNewListLeave}]">
|
||||
<input
|
||||
ref="dom_listsNewInput" :class="$style.listsInput" type="text" :placeholder="$t('lists__new_list_input')" @contextmenu.stop
|
||||
@keyup.enter="handleCreateList" @blur="handleCreateList"
|
||||
>
|
||||
</li>
|
||||
</transition>
|
||||
</ul>
|
||||
<base-menu v-model="isShowMenu" :menus="menus" :xy="menuLocation" item-name="name" @menu-click="handleMenuClick" />
|
||||
<DuplicateMusicModal v-model:visible="isShowDuplicateMusicModal" :list-info="duplicateListInfo" />
|
||||
<ListSortModal v-model:visible="isShowListSortModal" :list-info="sortListInfo" />
|
||||
<ListUpdateModal v-model:visible="isShowListUpdateModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { openUrl } from '@common/utils/electron'
|
||||
|
||||
import musicSdk from '@renderer/utils/musicSdk'
|
||||
import DuplicateMusicModal from './components/DuplicateMusicModal'
|
||||
import ListSortModal from './components/ListSortModal'
|
||||
import ListUpdateModal from './components/ListUpdateModal'
|
||||
|
||||
import { defaultList, loveList, userLists, fetchingListStatus } from '@renderer/store/list/state'
|
||||
import { removeUserList } from '@renderer/store/list/action'
|
||||
|
||||
import { ref, watch } from '@common/utils/vueTools'
|
||||
import { useRouter } from '@common/utils/vueRouter'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
|
||||
import { saveListPrevSelectId } from '@renderer/utils/data'
|
||||
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
|
||||
|
||||
import useShare from './useShare'
|
||||
import useMenu from './useMenu'
|
||||
import useListUpdate from './useListUpdate'
|
||||
import useSort from './useSort'
|
||||
import useDarg from './useDarg'
|
||||
import useEditList from './useEditList'
|
||||
import useListScroll from './useListScroll'
|
||||
import useDuplicate from './useDuplicate'
|
||||
|
||||
export default {
|
||||
name: 'MyLists',
|
||||
components: {
|
||||
DuplicateMusicModal,
|
||||
ListSortModal,
|
||||
ListUpdateModal,
|
||||
},
|
||||
props: {
|
||||
listId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['show-menu'],
|
||||
setup(props, { emit }) {
|
||||
const router = useRouter()
|
||||
const t = useI18n()
|
||||
|
||||
const dom_lists_list = ref(null)
|
||||
const rightClickItemIndex = ref(-10)
|
||||
|
||||
const { handleImportList, handleExportList } = useShare()
|
||||
const { isShowListUpdateModal, handleUpdateSourceList } = useListUpdate()
|
||||
const { isShowListSortModal, sortListInfo, handleSortList } = useSort()
|
||||
const { isShowDuplicateMusicModal, duplicateListInfo, handleDuplicateList } = useDuplicate()
|
||||
const { handleRename, handleSaveListName, isShowNewList, isNewListLeave, handleCreateList } = useEditList({ dom_lists_list })
|
||||
useListScroll({ dom_lists_list })
|
||||
|
||||
const handleOpenSourceDetailPage = async(listInfo) => {
|
||||
const { source, sourceListId } = listInfo
|
||||
if (!sourceListId) return
|
||||
let url
|
||||
if (/board__/.test(sourceListId)) {
|
||||
const id = sourceListId.replace(/board__/, '')
|
||||
url = musicSdk[source].leaderboard.getDetailPageUrl(id)
|
||||
} else if (musicSdk[source].songList?.getDetailPageUrl) {
|
||||
url = await musicSdk[source].songList.getDetailPageUrl(sourceListId)
|
||||
}
|
||||
if (!url) return
|
||||
openUrl(url)
|
||||
}
|
||||
|
||||
const handleRemove = (listInfo) => {
|
||||
dialog.confirm({
|
||||
message: t('lists__remove_tip', { name: listInfo.name }),
|
||||
confirmButtonText: t('lists__remove_tip_button'),
|
||||
}).then(isRemove => {
|
||||
if (!isRemove) return
|
||||
removeUserList([listInfo.id])
|
||||
if (props.listId == listInfo.id) {
|
||||
handleListToggle(LIST_IDS.DEFAULT)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
} = useMenu({
|
||||
emit,
|
||||
|
||||
handleImportList,
|
||||
handleExportList,
|
||||
handleUpdateSourceList,
|
||||
handleOpenSourceDetailPage,
|
||||
handleSortList,
|
||||
handleDuplicateList,
|
||||
handleRename,
|
||||
handleRemove,
|
||||
})
|
||||
|
||||
const handleListsItemRigthClick = (event, index) => {
|
||||
rightClickItemIndex.value = index
|
||||
showMenu(event, index)
|
||||
}
|
||||
|
||||
const handleListToggle = (id) => {
|
||||
if (id == props.listId) return
|
||||
router.replace({
|
||||
path: '/list',
|
||||
query: { id },
|
||||
}).catch(_ => _)
|
||||
}
|
||||
|
||||
const handleMenuClick = (action) => {
|
||||
if (rightClickItemIndex.value < -2) return
|
||||
let index = rightClickItemIndex.value
|
||||
rightClickItemIndex.value = -10
|
||||
menuClick(action, index)
|
||||
}
|
||||
|
||||
const { isModDown } = useDarg({ dom_lists_list, handleMenuClick, handleSaveListName })
|
||||
|
||||
|
||||
watch(() => props.listId, (listId) => {
|
||||
saveListPrevSelectId(listId)
|
||||
})
|
||||
|
||||
watch(() => userLists, (lists) => {
|
||||
if (lists.some(l => l.id == props.listId)) return
|
||||
router.replace({
|
||||
path: '/list',
|
||||
query: {
|
||||
id: defaultList.id,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
rightClickItemIndex,
|
||||
defaultList,
|
||||
loveList,
|
||||
userLists,
|
||||
fetchingListStatus,
|
||||
dom_lists_list,
|
||||
isShowListUpdateModal,
|
||||
isShowListSortModal,
|
||||
sortListInfo,
|
||||
isShowDuplicateMusicModal,
|
||||
duplicateListInfo,
|
||||
handleSaveListName,
|
||||
isShowNewList,
|
||||
isNewListLeave,
|
||||
handleCreateList,
|
||||
handleListsItemRigthClick,
|
||||
isShowMenu,
|
||||
handleMenuClick,
|
||||
menus,
|
||||
menuLocation,
|
||||
handleListToggle,
|
||||
isModDown,
|
||||
hideMenu: handleMenuClick,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
@lists-item-height: 36px;
|
||||
.lists {
|
||||
flex: none;
|
||||
width: 16%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.listHeader {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
border-bottom: var(--color-list-header-border-bottom);
|
||||
&:hover {
|
||||
.listsAdd {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsTitle {
|
||||
flex: auto;
|
||||
font-size: 12px;
|
||||
line-height: 38px;
|
||||
padding: 0 10px;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.headerBtns {
|
||||
flex: none;
|
||||
display: flex;
|
||||
}
|
||||
.listsAdd {
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
margin-top: 6px;
|
||||
background: none;
|
||||
height: 30px;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: @radius-border;
|
||||
cursor: pointer;
|
||||
opacity: .1;
|
||||
transition: opacity @transition-normal;
|
||||
color: var(--color-button-font);
|
||||
svg {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
&:active {
|
||||
opacity: .7 !important;
|
||||
}
|
||||
&:hover {
|
||||
opacity: .6 !important;
|
||||
}
|
||||
}
|
||||
.listsContent {
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
overflow-y: scroll !important;
|
||||
// border-right: 1px solid rgba(0, 0, 0, 0.12);
|
||||
|
||||
&.sortable {
|
||||
* {
|
||||
-webkit-user-drag: element;
|
||||
}
|
||||
|
||||
.listsItem {
|
||||
&:hover, &.active, &.selected, &.clicked {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
&.dragingItem {
|
||||
background-color: var(--color-primary-background-hover) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsItem {
|
||||
position: relative;
|
||||
transition: .3s ease;
|
||||
transition-property: color, background-color, opacity;
|
||||
background-color: transparent;
|
||||
&:not(.active) {
|
||||
&:hover {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
// background-color:
|
||||
color: var(--color-primary);
|
||||
}
|
||||
&.selected {
|
||||
background-color: var(--color-primary-font-active);
|
||||
}
|
||||
&.clicked {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
&.fetching {
|
||||
opacity: .5;
|
||||
}
|
||||
&.editing {
|
||||
padding: 0 10px;
|
||||
background-color: var(--color-primary-background-hover);
|
||||
.listsLabel {
|
||||
display: none;
|
||||
}
|
||||
.listsInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsLabel {
|
||||
display: block;
|
||||
height: @lists-item-height;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
line-height: @lists-item-height;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.listsInput {
|
||||
width: 100%;
|
||||
height: @lists-item-height;
|
||||
border: none;
|
||||
padding: 0;
|
||||
// padding-bottom: 1px;
|
||||
line-height: @lists-item-height;
|
||||
background: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.listsNew {
|
||||
padding: 0 10px;
|
||||
background-color: var(--color-primary-background-hover);
|
||||
.listsInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.newLeave {
|
||||
margin-top: -@lists-item-height;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
import { onBeforeUnmount, ref, Ref, useCssModule } from '@common/utils/vueTools'
|
||||
import { updateUserListPosition } from '@renderer/store/list/action'
|
||||
import { userLists } from '@renderer/store/list/state'
|
||||
import useDarg from '@renderer/utils/compositions/useDrag'
|
||||
|
||||
|
||||
export default ({ dom_lists_list, handleSaveListName, handleMenuClick }: {
|
||||
dom_lists_list: Ref<HTMLElement | null>
|
||||
handleSaveListName: () => void
|
||||
handleMenuClick: () => void
|
||||
}) => {
|
||||
const isModDown = ref(false)
|
||||
const styles = useCssModule()
|
||||
|
||||
const { setDisabled } = useDarg({
|
||||
dom_list: dom_lists_list,
|
||||
dragingItemClassName: styles.dragingItem,
|
||||
filter: 'default-list',
|
||||
onUpdate(newIndex: number, oldIndex: number) {
|
||||
void updateUserListPosition({ ids: [userLists[oldIndex - 2].id], position: newIndex - 2 })
|
||||
},
|
||||
})
|
||||
|
||||
const handle_key_mod_down = ({ event }: LX.KeyDownEevent) => {
|
||||
if (!isModDown.value) {
|
||||
// console.log(event)
|
||||
switch ((event!.target as HTMLElement).tagName) {
|
||||
case 'INPUT':
|
||||
case 'SELECT':
|
||||
case 'TEXTAREA':
|
||||
return
|
||||
default: if ((event!.target as HTMLElement).isContentEditable) return
|
||||
}
|
||||
|
||||
isModDown.value = true
|
||||
setDisabled(false)
|
||||
handleSaveListName()
|
||||
}
|
||||
handleMenuClick()
|
||||
}
|
||||
const handle_key_mod_up = () => {
|
||||
if (isModDown.value) {
|
||||
isModDown.value = false
|
||||
setDisabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
window.key_event.on('key_mod_down', handle_key_mod_down)
|
||||
window.key_event.on('key_mod_up', handle_key_mod_up)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.key_event.off('key_mod_down', handle_key_mod_down)
|
||||
window.key_event.off('key_mod_up', handle_key_mod_up)
|
||||
})
|
||||
|
||||
return {
|
||||
isModDown,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ref, reactive } from '@common/utils/vueTools'
|
||||
|
||||
|
||||
export default () => {
|
||||
const isShowDuplicateMusicModal = ref(false)
|
||||
const duplicateListInfo = reactive({ id: '', name: '' })
|
||||
|
||||
const handleDuplicateList = (listInfo: LX.List.MyListInfo) => {
|
||||
duplicateListInfo.id = listInfo.id
|
||||
duplicateListInfo.name = listInfo.name
|
||||
isShowDuplicateMusicModal.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
isShowDuplicateMusicModal,
|
||||
duplicateListInfo,
|
||||
handleDuplicateList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { ref, nextTick, useCssModule, Ref } from '@common/utils/vueTools'
|
||||
import { userLists } from '@renderer/store/list/state'
|
||||
import { updateUserList, createUserList } from '@renderer/store/list/action'
|
||||
|
||||
export default ({ dom_lists_list }: {
|
||||
dom_lists_list: Ref<HTMLElement | null>
|
||||
}) => {
|
||||
const isShowNewList = ref(false)
|
||||
const isNewListLeave = ref(false)
|
||||
const editIndex = ref(-1)
|
||||
const styles = useCssModule()
|
||||
|
||||
const handleRename = (index: number) => {
|
||||
console.log(index)
|
||||
const dom = dom_lists_list.value?.querySelectorAll('.user-list')[index]
|
||||
if (!dom) return
|
||||
void nextTick(() => {
|
||||
dom.classList.add(styles.editing)
|
||||
dom.querySelector('input')?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveListName = async() => {
|
||||
let dom_target = dom_lists_list.value?.querySelector('.' + styles.editing) as HTMLElement
|
||||
if (!dom_target) return
|
||||
const dom_input = dom_target.querySelector('.' + styles.listsInput) as HTMLInputElement
|
||||
if (!dom_input) return
|
||||
let name = dom_input.value.trim()
|
||||
if (dom_target.dataset.index == null) return
|
||||
const targetList = userLists[parseInt(dom_target.dataset.index)]
|
||||
if (name.length) {
|
||||
await updateUserList([{ ...targetList, name }])
|
||||
dom_target.classList.remove(styles.editing)
|
||||
}
|
||||
dom_input.value = targetList.name
|
||||
}
|
||||
|
||||
const handleCreateList = async(event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.readOnly) return
|
||||
let name = target.value.trim()
|
||||
target.readOnly = true
|
||||
|
||||
if (name == '') {
|
||||
isShowNewList.value = false
|
||||
return
|
||||
}
|
||||
|
||||
await createUserList({ name })
|
||||
isNewListLeave.value = true
|
||||
void nextTick(() => {
|
||||
isShowNewList.value = false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
isShowNewList,
|
||||
isNewListLeave,
|
||||
editIndex,
|
||||
handleRename,
|
||||
handleSaveListName,
|
||||
handleCreateList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { onMounted, useCssModule, Ref } from '@common/utils/vueTools'
|
||||
|
||||
|
||||
export default ({ dom_lists_list }: {
|
||||
dom_lists_list: Ref<HTMLElement | null>
|
||||
}) => {
|
||||
const styles = useCssModule()
|
||||
|
||||
const setListsScroll = () => {
|
||||
if (!dom_lists_list.value) return
|
||||
let target = dom_lists_list.value.querySelector('.' + styles.active) as HTMLElement
|
||||
if (!target) return
|
||||
let offsetTop = target.offsetTop
|
||||
let location = offsetTop - 150
|
||||
if (location > 0) dom_lists_list.value.scrollTop = location
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setListsScroll()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
import syncSourceList from '@renderer/store/list/syncSourceList'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
|
||||
|
||||
export default () => {
|
||||
const isShowListUpdateModal = ref(false)
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const handleUpdateSourceList = (listInfo: LX.List.UserListInfo) => {
|
||||
void dialog.confirm({
|
||||
message: t('lists__sync_confirm_tip', { name: listInfo.name }),
|
||||
confirmButtonText: t('lists__remove_tip_button'),
|
||||
}).then(isSync => {
|
||||
if (!isSync) return
|
||||
void syncSourceList(listInfo)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
isShowListUpdateModal,
|
||||
handleUpdateSourceList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { computed, ref, reactive, nextTick } from '@common/utils/vueTools'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { userLists, defaultList, loveList } from '@renderer/store/list/state'
|
||||
import musicSdk from '@renderer/utils/musicSdk'
|
||||
import { addLocalFile } from './actions'
|
||||
|
||||
export default ({
|
||||
emit,
|
||||
|
||||
handleRename,
|
||||
handleDuplicateList,
|
||||
handleSortList,
|
||||
handleOpenSourceDetailPage,
|
||||
handleImportList,
|
||||
handleExportList,
|
||||
handleUpdateSourceList,
|
||||
handleRemove,
|
||||
}) => {
|
||||
const menuControl = reactive({
|
||||
rename: true,
|
||||
duplicate: true,
|
||||
sort: true,
|
||||
local_file: true,
|
||||
sourceDetail: true,
|
||||
import: true,
|
||||
export: true,
|
||||
sync: false,
|
||||
remove: true,
|
||||
})
|
||||
const t = useI18n()
|
||||
const menuLocation = reactive({ x: 0, y: 0 })
|
||||
const isShowMenu = ref(false)
|
||||
|
||||
const menus = computed(() => {
|
||||
return [
|
||||
{
|
||||
name: t('lists__rename'),
|
||||
action: 'rename',
|
||||
disabled: !menuControl.rename,
|
||||
},
|
||||
{
|
||||
name: t('lists__sort_list'),
|
||||
action: 'sort',
|
||||
disabled: !menuControl.sort,
|
||||
},
|
||||
{
|
||||
name: t('lists__duplicate'),
|
||||
action: 'duplicate',
|
||||
disabled: !menuControl.duplicate,
|
||||
},
|
||||
{
|
||||
name: t('lists__select_local_file'),
|
||||
action: 'local_file',
|
||||
disabled: !menuControl.local_file,
|
||||
},
|
||||
{
|
||||
name: t('lists__sync'),
|
||||
action: 'sync',
|
||||
disabled: !menuControl.sync,
|
||||
},
|
||||
{
|
||||
name: t('lists__source_detail'),
|
||||
action: 'sourceDetail',
|
||||
disabled: !menuControl.sourceDetail,
|
||||
},
|
||||
{
|
||||
name: t('lists__import'),
|
||||
action: 'import',
|
||||
disabled: !menuControl.export,
|
||||
},
|
||||
{
|
||||
name: t('lists__export'),
|
||||
action: 'export',
|
||||
disabled: !menuControl.export,
|
||||
},
|
||||
{
|
||||
name: t('lists__remove'),
|
||||
action: 'remove',
|
||||
disabled: !menuControl.remove,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const assertSupportDetail = (source, index) => {
|
||||
if (source) {
|
||||
const { sourceListId } = userLists[index]
|
||||
if (sourceListId) {
|
||||
if (/board__/.test(sourceListId)) {
|
||||
// const id = sourceListId.replace(/board__/, '')
|
||||
return !!musicSdk[source]?.leaderboard?.getDetailPageUrl
|
||||
} else {
|
||||
return !!musicSdk[source]?.songList?.getDetailPageUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const showMenu = (event, index) => {
|
||||
let source
|
||||
switch (index) {
|
||||
case -1:
|
||||
case -2:
|
||||
menuControl.rename = false
|
||||
menuControl.remove = false
|
||||
menuControl.sync = false
|
||||
break
|
||||
default:
|
||||
menuControl.rename = true
|
||||
menuControl.remove = true
|
||||
source = userLists[index].source
|
||||
menuControl.sync = !!source && !!musicSdk[source]?.songList
|
||||
break
|
||||
}
|
||||
// menuControl.sort = !!getList(this.getTargetListInfo(index)?.id).length
|
||||
menuControl.sourceDetail = assertSupportDetail(source, index)
|
||||
|
||||
menuLocation.x = event.pageX
|
||||
menuLocation.y = event.pageY
|
||||
|
||||
if (isShowMenu.value) return
|
||||
emit('show-menu')
|
||||
nextTick(() => {
|
||||
isShowMenu.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const hideMenu = () => {
|
||||
isShowMenu.value = false
|
||||
}
|
||||
|
||||
const getListInfo = (index) => {
|
||||
let list
|
||||
switch (index) {
|
||||
case -2:
|
||||
list = defaultList
|
||||
break
|
||||
case -1:
|
||||
list = loveList
|
||||
break
|
||||
default:
|
||||
list = userLists[index]
|
||||
if (!list) return null
|
||||
break
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
const menuClick = (action, index) => {
|
||||
// console.log(action)
|
||||
hideMenu()
|
||||
if (!action) return
|
||||
const listInfo = getListInfo(index)
|
||||
switch (action.action) {
|
||||
case 'rename':
|
||||
handleRename(index)
|
||||
break
|
||||
case 'duplicate':
|
||||
handleDuplicateList(listInfo)
|
||||
break
|
||||
case 'sort':
|
||||
handleSortList(listInfo)
|
||||
break
|
||||
case 'local_file':
|
||||
addLocalFile(listInfo)
|
||||
break
|
||||
case 'sourceDetail':
|
||||
handleOpenSourceDetailPage(listInfo)
|
||||
break
|
||||
case 'import':
|
||||
handleImportList(listInfo, index)
|
||||
break
|
||||
case 'export':
|
||||
handleExportList(listInfo)
|
||||
break
|
||||
case 'sync':
|
||||
handleUpdateSourceList(listInfo)
|
||||
break
|
||||
case 'remove':
|
||||
handleRemove(listInfo)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { toRaw } from '@common/utils/vueTools'
|
||||
import { openSaveDir, showSelectDialog } from '@renderer/utils/ipc'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { filterFileName, toNewMusicInfo, fixNewMusicInfoQuality } from '@renderer/utils'
|
||||
import { getListMusics, updateUserList, addListMusics, overwriteListMusics, createUserList } from '@renderer/store/list/action'
|
||||
import { defaultList, loveList, userLists } from '@renderer/store/list/state'
|
||||
import useImportTip from '@renderer/utils/compositions/useImportTip'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
|
||||
|
||||
export default () => {
|
||||
const t = useI18n()
|
||||
const showImportTip = useImportTip()
|
||||
|
||||
const handleExportList = (listInfo: LX.List.MyListInfo) => {
|
||||
if (!listInfo) return
|
||||
void openSaveDir({
|
||||
title: t('lists__export_part_desc'),
|
||||
defaultPath: `lx_list_part_${filterFileName(listInfo.name)}.lxmc`,
|
||||
}).then(async result => {
|
||||
if (result.canceled || !result.filePath) return
|
||||
void window.lx.worker.main.saveLxConfigFile(result.filePath, {
|
||||
type: 'playListPart_v2',
|
||||
data: { ...toRaw(listInfo), list: toRaw(await getListMusics(listInfo.id)) },
|
||||
})
|
||||
})
|
||||
}
|
||||
const handleImportList = (listInfo: LX.List.MyListInfo, index: number) => {
|
||||
void showSelectDialog({
|
||||
title: t('lists__import_part_desc'),
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'Play List Part', extensions: ['json', 'lxmc'] },
|
||||
{ name: 'All Files', extensions: ['*'] },
|
||||
],
|
||||
}).then(async result => {
|
||||
if (result.canceled) return
|
||||
const filePath = result.filePaths[0]
|
||||
if (!filePath) return
|
||||
let configData: any
|
||||
try {
|
||||
configData = await window.lx.worker.main.readLxConfigFile(filePath)
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
let listData: LX.ConfigFile.MyListInfoPart['data']
|
||||
switch (configData.type) {
|
||||
case 'playListPart':
|
||||
listData = configData.data
|
||||
listData.list = listData.list.map(m => toNewMusicInfo(m))
|
||||
break
|
||||
case 'playListPart_v2':
|
||||
listData = configData.data
|
||||
break
|
||||
default: return showImportTip(configData.type)
|
||||
}
|
||||
|
||||
const targetList = [defaultList, loveList, ...userLists].find(l => l.id == listData.id)
|
||||
if (targetList) {
|
||||
const confirm = await dialog.confirm({
|
||||
message: t('lists__import_part_confirm', { importName: listData.name, localName: targetList.name }),
|
||||
cancelButtonText: t('lists__import_part_button_cancel'),
|
||||
confirmButtonText: t('lists__import_part_button_confirm'),
|
||||
})
|
||||
if (confirm) {
|
||||
listData.name = targetList.name
|
||||
switch (listData.id) {
|
||||
case defaultList.id:
|
||||
case loveList.id:
|
||||
break
|
||||
default:
|
||||
void updateUserList([
|
||||
{
|
||||
name: listData.name,
|
||||
id: listData.id,
|
||||
source: (listData as LX.List.UserListInfo).source,
|
||||
sourceListId: (listData as LX.List.UserListInfo).sourceListId,
|
||||
locationUpdateTime: (targetList as LX.List.UserListInfo).locationUpdateTime,
|
||||
},
|
||||
])
|
||||
break
|
||||
}
|
||||
void overwriteListMusics({
|
||||
listId: listData.id,
|
||||
musicInfos: listData.list.map(m => fixNewMusicInfoQuality(m)),
|
||||
})
|
||||
return
|
||||
}
|
||||
listData.id += `__${Date.now()}`
|
||||
}
|
||||
void createUserList({
|
||||
position: index,
|
||||
name: listData.name,
|
||||
id: listData.id,
|
||||
source: (listData as LX.List.UserListInfo).source,
|
||||
sourceListId: (listData as LX.List.UserListInfo).sourceListId,
|
||||
})
|
||||
void addListMusics(listData.id, listData.list.map(m => fixNewMusicInfoQuality(m)))
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
handleExportList,
|
||||
handleImportList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ref, reactive } from '@common/utils/vueTools'
|
||||
|
||||
|
||||
export default () => {
|
||||
const isShowListSortModal = ref(false)
|
||||
const sortListInfo = reactive({ id: '', name: '' })
|
||||
|
||||
const handleSortList = (listInfo) => {
|
||||
sortListInfo.id = listInfo.id
|
||||
sortListInfo.name = listInfo.name
|
||||
isShowListSortModal.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
isShowListSortModal,
|
||||
sortListInfo,
|
||||
handleSortList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div id="my-list" :class="$style.container" @click="handleContainerClick">
|
||||
<MyList ref="myList" :list-id="listId" @show-menu="$refs.musicList.handleMenuClick()" />
|
||||
<MusicList ref="musicList" :list-id="listId" @show-menu="$refs.myList.handleMenuClick()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getListPrevSelectId } from '@renderer/utils/data'
|
||||
|
||||
import MyList from './MyList'
|
||||
import MusicList from './MusicList'
|
||||
|
||||
export default {
|
||||
name: 'List',
|
||||
components: {
|
||||
MyList,
|
||||
MusicList,
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
let id = to.query.id
|
||||
if (!id) {
|
||||
id = await getListPrevSelectId()
|
||||
next({
|
||||
path: to.path,
|
||||
query: { id },
|
||||
})
|
||||
} else next()
|
||||
},
|
||||
beforeRouteUpdate(to, from) {
|
||||
// console.log(to, from)
|
||||
if (to.query.updated) return
|
||||
let id = to.query.id
|
||||
if (id == null) return
|
||||
// if (!getList(id)) {
|
||||
// id = defaultList.id
|
||||
// }
|
||||
this.listId = id
|
||||
const scrollIndex = to.query.scrollIndex
|
||||
const isAnimation = from.query.id == to.query.id
|
||||
this.$refs.musicList?.handleRestoreScroll(scrollIndex, isAnimation)
|
||||
|
||||
return {
|
||||
path: '/list',
|
||||
query: { id, updated: true },
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(to, from) {
|
||||
this.$refs.musicList?.saveListPosition()
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
listId: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.listId = this.$route.query.id
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.container {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,753 +0,0 @@
|
||||
<template lang="pug">
|
||||
div(:class="$style.search")
|
||||
//- transition
|
||||
div(:class="$style.header")
|
||||
base-tab(:class="$style.tab" :list="sources" align="left" item-key="id" item-name="name" @change="handleSourceChange" v-model="searchSourceId")
|
||||
div(:class="$style.main")
|
||||
div(:class="$style.list" v-show="isLoading || listInfo.list.length")
|
||||
div.thead(:class="$style.thead")
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th.nobreak.center(style="width: 5%;") #
|
||||
th.nobreak {{$t('music_name')}}
|
||||
th.nobreak(style="width: 22%;") {{$t('music_singer')}}
|
||||
th.nobreak(style="width: 22%;") {{$t('music_album')}}
|
||||
th.nobreak(style="width: 8%;") {{$t('music_time')}}
|
||||
th.nobreak(style="width: 13%;") {{$t('action')}}
|
||||
div.scroll(:class="$style.tbody" ref="dom_scrollContent")
|
||||
table
|
||||
tbody(@contextmenu.capture="handleContextMenu" ref="dom_tbody")
|
||||
tr(v-for='(item, index) in listInfo.list' :key='item.songmid' @contextmenu="handleListItemRigthClick($event, index)" @click="handleDoubleClick($event, index)")
|
||||
td.nobreak.center(style="width: 5%; padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{index + 1}}
|
||||
td.break
|
||||
span.select {{item.name}}
|
||||
span.badge.badge-theme-success(:class="[$style.labelQuality, $style.noSelect]" v-if="item._types.flac32bit") {{$t('tag__lossless_24bit')}}
|
||||
span.badge.badge-theme-success(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item._types.ape || item._types.flac || item._types.wav") {{$t('tag__lossless')}}
|
||||
span.badge.badge-theme-info(:class="[$style.labelQuality, $style.noSelect]" v-else-if="item._types['320k']") {{$t('tag__high_quality')}}
|
||||
span(:class="[$style.labelSource, $style.noSelect]" v-if="searchSourceId == 'all'") {{item.source}}
|
||||
td.break(style="width: 22%;")
|
||||
span.select {{item.singer}}
|
||||
td.break(style="width: 22%;")
|
||||
span.select {{item.albumName}}
|
||||
td(style="width: 8%;")
|
||||
span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}}
|
||||
td(style="width: 13%; padding-left: 0; padding-right: 0;")
|
||||
material-list-buttons(:index="index" :remove-btn="false" :class="$style.listBtn"
|
||||
:play-btn="assertApiSupport(item.source)"
|
||||
:download-btn="assertApiSupport(item.source)"
|
||||
@btn-click="handleListBtnClick")
|
||||
div(:class="$style.pagination")
|
||||
material-pagination(:max-page="listInfo.allPage" :limit="listInfo.limit" :page="page" @btn-click="handleTogglePage")
|
||||
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated fadeOut")
|
||||
div(v-show="isLoading" :class="$style.loading")
|
||||
p {{$t('list__loading')}}
|
||||
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
|
||||
div(v-show="!isLoading && !listInfo.list.length" :class="$style.noitem")
|
||||
div.scroll(:class="$style.noitemListContainer" v-if="setting.search.isShowHotSearch || (setting.search.isShowHistorySearch && historyList.length)")
|
||||
dl(:class="[$style.noitemList, $style.noitemHotSearchList]" v-if="setting.search.isShowHotSearch")
|
||||
dt(:class="$style.noitemListTitle") {{$t('search__hot_search')}}
|
||||
dd(:class="$style.noitemListItem" @click="handleNoitemSearch(item)" v-for="item in hotSearchList") {{item}}
|
||||
dl(:class="$style.noitemList" v-if="setting.search.isShowHistorySearch && historyList.length")
|
||||
dt(:class="$style.noitemListTitle")
|
||||
span {{$t('history_search')}}
|
||||
span(:class="$style.historyClearBtn" @click="clearHistory" :aria-label="$t('history_clear')")
|
||||
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 512 512' space='preserve')
|
||||
use(xlink:href='#icon-eraser')
|
||||
dd(:class="$style.noitemListItem" v-for="(item, index) in historyList" @contextmenu="removeHistory(index)" :key="index + item" @click="handleNoitemSearch(item)" :aria-label="$t('history_remove')") {{item}}
|
||||
div(v-else :class="$style.noitem_list")
|
||||
p {{$t('search__welcome')}}
|
||||
//- common-flow-btn(:show="isShowEditBtn && (searchSourceId == 'all' || assertApiSupport(searchSourceId))" :remove-btn="false" @btn-click="handleFlowBtnClick")
|
||||
common-download-modal(v-model:show="isShowDownload" :musicInfo="musicInfo" teleport="#view")
|
||||
common-download-multiple-modal(v-model:show="isShowDownloadMultiple" :list="selectedData" @confirm="removeAllSelect" teleport="#view")
|
||||
common-list-add-modal(v-model:show="isShowListAdd" :musicInfo="musicInfo" teleport="#view")
|
||||
common-list-add-multiple-modal(v-model:show="isShowListAddMultiple" :musicList="selectedData" @confirm="removeAllSelect" teleport="#view")
|
||||
base-menu(:menus="listItemMenu" :location="listMenu.menuLocation" item-name="name" :isShow="listMenu.isShowItemMenu" @menu-click="handleListItemMenuClick")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions, mapMutations } from 'vuex'
|
||||
import { clipboardWriteText, assertApiSupport, openUrl } from '@renderer/utils'
|
||||
import musicSdk from '@renderer/utils/music'
|
||||
import { defaultList } from '@renderer/core/share/list'
|
||||
import { getList } from '@renderer/core/share/utils'
|
||||
|
||||
export default {
|
||||
name: 'Search',
|
||||
data() {
|
||||
return {
|
||||
page: 0,
|
||||
text: '',
|
||||
clickTime: 0,
|
||||
clickIndex: -1,
|
||||
isShowDownload: false,
|
||||
musicInfo: null,
|
||||
selectedData: [],
|
||||
// isShowEditBtn: false,
|
||||
isShowDownloadMultiple: false,
|
||||
searchSourceId: null,
|
||||
isShowListAdd: false,
|
||||
isShowListAddMultiple: false,
|
||||
keyEvent: {
|
||||
isShiftDown: false,
|
||||
isModDown: false,
|
||||
},
|
||||
lastSelectIndex: 0,
|
||||
listMenu: {
|
||||
isShowItemMenu: false,
|
||||
itemMenuControl: {
|
||||
play: true,
|
||||
playLater: true,
|
||||
addTo: true,
|
||||
download: true,
|
||||
sourceDetail: true,
|
||||
},
|
||||
menuLocation: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
isLoading: false,
|
||||
searchId: null,
|
||||
}
|
||||
},
|
||||
beforeRouteUpdate(to, from) {
|
||||
if (to.query.source && (this.sourceList[to.query.source] || to.query.source == 'all')) {
|
||||
if (this.setting.search.searchSource != to.query.source) {
|
||||
this.setSearchSource({
|
||||
searchSource: to.query.source,
|
||||
})
|
||||
}
|
||||
if (this.searchSourceId != to.query.source) {
|
||||
this.searchSourceId = to.query.source
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.handleGetHotSearch()
|
||||
})
|
||||
}
|
||||
if (to.query.text != null && this.text != to.query.text) {
|
||||
this.text = to.query.text
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.page = 1
|
||||
this.handleSearch(this.text, this.page)
|
||||
})
|
||||
},
|
||||
created() {
|
||||
this.listenEvent()
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.unlistenEvent()
|
||||
},
|
||||
mounted() {
|
||||
// console.log('mounted')
|
||||
if (this.$route.query.source && (this.sourceList[this.$route.query.source] || this.$route.query.source == 'all')) {
|
||||
this.setSearchSource({
|
||||
searchSource: this.$route.query.source,
|
||||
})
|
||||
} else if (!this.sourceList[this.setting.search.searchSource] && this.setting.search.searchSource != 'all') { // 处理搜索源不存在时页面报错的问题
|
||||
this.setSearchSource({
|
||||
searchSource: 'kw',
|
||||
})
|
||||
}
|
||||
this.searchSourceId = this.setting.search.searchSource
|
||||
if (this.$route.query.text == null) {
|
||||
this.text = this.$store.getters['search/searchText']
|
||||
this.page = this.listInfo.page
|
||||
} else if (this.$route.query.text == '') {
|
||||
this.clearList()
|
||||
} else {
|
||||
this.text = this.$route.query.text
|
||||
this.page = 1
|
||||
this.handleSearch(this.text, this.page)
|
||||
}
|
||||
this.handleGetHotSearch()
|
||||
},
|
||||
watch: {
|
||||
// selectedData(n) {
|
||||
// const len = n.length
|
||||
// if (len) {
|
||||
// this.isShowEditBtn = true
|
||||
// } else {
|
||||
// this.isShowEditBtn = false
|
||||
// }
|
||||
// },
|
||||
'listInfo.list'() {
|
||||
this.removeAllSelect()
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['userInfo', 'setting']),
|
||||
...mapGetters('search', ['sourceList', 'allList', 'sources', 'historyList']),
|
||||
listItemMenu() {
|
||||
return [
|
||||
{
|
||||
name: this.$t('list__play'),
|
||||
action: 'play',
|
||||
disabled: !this.listMenu.itemMenuControl.play,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__download'),
|
||||
action: 'download',
|
||||
disabled: !this.listMenu.itemMenuControl.download,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__play_later'),
|
||||
action: 'playLater',
|
||||
disabled: !this.listMenu.itemMenuControl.playLater,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__add_to'),
|
||||
action: 'addTo',
|
||||
disabled: !this.listMenu.itemMenuControl.addTo,
|
||||
},
|
||||
{
|
||||
name: this.$t('list__source_detail'),
|
||||
action: 'sourceDetail',
|
||||
disabled: !this.listMenu.itemMenuControl.sourceDetail,
|
||||
},
|
||||
]
|
||||
},
|
||||
listInfo() {
|
||||
return this.setting.search.searchSource == 'all' ? this.allList : this.sourceList[this.setting.search.searchSource]
|
||||
},
|
||||
hotSearchList() {
|
||||
return this.$store.getters['hotSearch/list'][this.setting.search.searchSource] || []
|
||||
},
|
||||
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['setSearchSource']),
|
||||
...mapActions('search', ['search']),
|
||||
...mapActions('download', ['createDownload', 'createDownloadMultiple']),
|
||||
...mapMutations('search', ['clearList', 'setPage', 'removeHistory', 'clearHistory']),
|
||||
...mapMutations('list', ['listAdd', 'listAddMultiple']),
|
||||
...mapMutations('player', ['setList', 'setTempPlayList']),
|
||||
...mapActions('hotSearch', {
|
||||
getHotSearch: 'getList',
|
||||
}),
|
||||
listenEvent() {
|
||||
window.eventHub.on('key_shift_down', this.handle_key_shift_down)
|
||||
window.eventHub.on('key_shift_up', this.handle_key_shift_up)
|
||||
window.eventHub.on('key_mod_down', this.handle_key_mod_down)
|
||||
window.eventHub.on('key_mod_up', this.handle_key_mod_up)
|
||||
window.eventHub.on('key_mod+a_down', this.handle_key_mod_a_down)
|
||||
},
|
||||
unlistenEvent() {
|
||||
window.eventHub.off('key_shift_down', this.handle_key_shift_down)
|
||||
window.eventHub.off('key_shift_up', this.handle_key_shift_up)
|
||||
window.eventHub.off('key_mod_down', this.handle_key_mod_down)
|
||||
window.eventHub.off('key_mod_up', this.handle_key_mod_up)
|
||||
window.eventHub.off('key_mod+a_down', this.handle_key_mod_a_down)
|
||||
},
|
||||
handle_key_shift_down() {
|
||||
if (!this.keyEvent.isShiftDown) this.keyEvent.isShiftDown = true
|
||||
},
|
||||
handle_key_shift_up() {
|
||||
if (this.keyEvent.isShiftDown) this.keyEvent.isShiftDown = false
|
||||
},
|
||||
handle_key_mod_down() {
|
||||
if (!this.keyEvent.isModDown) this.keyEvent.isModDown = true
|
||||
},
|
||||
handle_key_mod_up() {
|
||||
if (this.keyEvent.isModDown) this.keyEvent.isModDown = false
|
||||
},
|
||||
handle_key_mod_a_down({ event }) {
|
||||
if (event.target.tagName == 'INPUT') return
|
||||
event.preventDefault()
|
||||
if (event.repeat) return
|
||||
this.keyEvent.isModDown = false
|
||||
this.handleSelectAllData()
|
||||
},
|
||||
handleSearch(text, page) {
|
||||
const searchId = this.searchId = `${this.searchSourceId}__${page}__${text}`
|
||||
if (text === '') {
|
||||
this.isLoading = false
|
||||
return this.clearList()
|
||||
}
|
||||
this.isLoading = true
|
||||
this.search({ text, page, limit: this.listInfo.limit }).then(data => {
|
||||
if (this.searchId != searchId) return
|
||||
this.page = page
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dom_scrollContent.scrollTo(0, 0)
|
||||
})
|
||||
}).finally(() => {
|
||||
if (this.searchId != searchId) return
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
handleDoubleClick(event, index) {
|
||||
if (event.target.classList.contains('select')) return
|
||||
|
||||
this.handleSelectData(event, index)
|
||||
|
||||
if (
|
||||
window.performance.now() - this.clickTime > 400 ||
|
||||
this.clickIndex !== index
|
||||
) {
|
||||
this.clickTime = window.performance.now()
|
||||
this.clickIndex = index
|
||||
return
|
||||
}
|
||||
if (this.assertApiSupport(this.listInfo.list[index].source)) this.testPlay(index)
|
||||
this.clickTime = 0
|
||||
this.clickIndex = -1
|
||||
},
|
||||
handleListBtnClick(info) {
|
||||
switch (info.action) {
|
||||
case 'download':
|
||||
this.musicInfo = this.listInfo.list[info.index]
|
||||
this.$nextTick(() => {
|
||||
this.isShowDownload = true
|
||||
})
|
||||
break
|
||||
case 'play':
|
||||
this.testPlay(info.index)
|
||||
break
|
||||
case 'listAdd':
|
||||
this.musicInfo = this.listInfo.list[info.index]
|
||||
this.$nextTick(() => {
|
||||
this.isShowListAdd = true
|
||||
})
|
||||
break
|
||||
}
|
||||
},
|
||||
handleSelectData(event, clickIndex) {
|
||||
if (this.keyEvent.isShiftDown) {
|
||||
if (this.selectedData.length) {
|
||||
let lastSelectIndex = this.lastSelectIndex
|
||||
this.removeAllSelect()
|
||||
if (lastSelectIndex != clickIndex) {
|
||||
let isNeedReverse = false
|
||||
if (clickIndex < lastSelectIndex) {
|
||||
let temp = lastSelectIndex
|
||||
lastSelectIndex = clickIndex
|
||||
clickIndex = temp
|
||||
isNeedReverse = true
|
||||
}
|
||||
this.selectedData = this.listInfo.list.slice(lastSelectIndex, clickIndex + 1)
|
||||
if (isNeedReverse) this.selectedData.reverse()
|
||||
let nodes = Array.from(this.$refs.dom_tbody.childNodes).filter(n => n.nodeType === 1)
|
||||
do {
|
||||
const node = nodes[lastSelectIndex]
|
||||
if (node.tagName == 'TR') {
|
||||
node.classList.add('active')
|
||||
}
|
||||
lastSelectIndex++
|
||||
} while (lastSelectIndex <= clickIndex)
|
||||
}
|
||||
} else {
|
||||
event.currentTarget.classList.add('active')
|
||||
this.selectedData.push(this.listInfo.list[clickIndex])
|
||||
this.lastSelectIndex = clickIndex
|
||||
}
|
||||
} else if (this.keyEvent.isModDown) {
|
||||
this.lastSelectIndex = clickIndex
|
||||
let item = this.listInfo.list[clickIndex]
|
||||
let index = this.selectedData.indexOf(item)
|
||||
if (index < 0) {
|
||||
this.selectedData.push(item)
|
||||
event.currentTarget.classList.add('active')
|
||||
} else {
|
||||
this.selectedData.splice(index, 1)
|
||||
event.currentTarget.classList.remove('active')
|
||||
}
|
||||
} else if (this.selectedData.length) this.removeAllSelect()
|
||||
},
|
||||
removeAllSelect() {
|
||||
this.selectedData = []
|
||||
let dom_tbody = this.$refs.dom_tbody
|
||||
if (!dom_tbody) return
|
||||
let nodes = dom_tbody.querySelectorAll('.active')
|
||||
for (const node of nodes) {
|
||||
if (node.parentNode == dom_tbody) node.classList.remove('active')
|
||||
}
|
||||
},
|
||||
testPlay(index) {
|
||||
let targetSong = this.listInfo.list[index]
|
||||
// if (!targetSong || !this.assertApiSupport(targetSong.source)) return
|
||||
this.listAdd({ id: 'default', musicInfo: targetSong })
|
||||
let targetIndex = getList(defaultList.id).findIndex(
|
||||
s => s.songmid === targetSong.songmid,
|
||||
)
|
||||
if (targetIndex > -1) {
|
||||
this.setList({
|
||||
listId: defaultList.id,
|
||||
index: targetIndex,
|
||||
})
|
||||
}
|
||||
},
|
||||
handleTogglePage(page) {
|
||||
this.handleSearch(this.text, page)
|
||||
},
|
||||
handleSelectAllData() {
|
||||
this.removeAllSelect()
|
||||
this.selectedData = [...this.listInfo.list]
|
||||
let nodes = this.$refs.dom_tbody.childNodes
|
||||
// console.log(nodes)
|
||||
for (const node of nodes) {
|
||||
if (node.tagName != 'TR') continue
|
||||
node.classList.add('active')
|
||||
}
|
||||
},
|
||||
// handleFlowBtnClick(action) {
|
||||
// switch (action) {
|
||||
// case 'download':
|
||||
// this.isShowDownloadMultiple = true
|
||||
// break
|
||||
// case 'play':
|
||||
// this.testPlay()
|
||||
// this.removeAllSelect()
|
||||
// break
|
||||
// case 'add':
|
||||
// this.isShowListAddMultiple = true
|
||||
// break
|
||||
// }
|
||||
// },
|
||||
filterList(list) {
|
||||
return this.searchSourceId == 'all'
|
||||
? list.filter(s => this.assertApiSupport(s.source))
|
||||
: this.assertApiSupport(this.searchSourceId)
|
||||
? [...list]
|
||||
: []
|
||||
},
|
||||
handleContextMenu(event) {
|
||||
if (!event.target.classList.contains('select')) return
|
||||
event.stopImmediatePropagation()
|
||||
let classList = this.$refs.dom_scrollContent.classList
|
||||
classList.add(this.$style.copying)
|
||||
window.requestAnimationFrame(() => {
|
||||
let str = window.getSelection().toString()
|
||||
classList.remove(this.$style.copying)
|
||||
str = str.trim()
|
||||
if (!str.length) return
|
||||
clipboardWriteText(str)
|
||||
})
|
||||
},
|
||||
handleGetHotSearch() {
|
||||
if (this.hotSearchList.length || !this.setting.search.isShowHotSearch) return
|
||||
this.getHotSearch(this.setting.search.searchSource)
|
||||
},
|
||||
handleNoitemSearch(text) {
|
||||
this.$router.replace({
|
||||
path: 'search',
|
||||
query: {
|
||||
text,
|
||||
},
|
||||
})
|
||||
},
|
||||
assertApiSupport(source) {
|
||||
return assertApiSupport(source)
|
||||
},
|
||||
handleListItemRigthClick(event, index) {
|
||||
this.listMenu.itemMenuControl.sourceDetail = !!musicSdk[this.listInfo.list[index].source].getMusicDetailPageUrl
|
||||
this.listMenu.itemMenuControl.play =
|
||||
this.listMenu.itemMenuControl.playLater =
|
||||
this.listMenu.itemMenuControl.download =
|
||||
this.assertApiSupport(this.listInfo.list[index].source)
|
||||
let dom_selected = this.$refs.dom_tbody.querySelector('tr.selected')
|
||||
if (dom_selected) dom_selected.classList.remove('selected')
|
||||
this.$refs.dom_tbody.querySelectorAll('tr')[index].classList.add('selected')
|
||||
this.listMenu.rightClickItemIndex = index
|
||||
let dom_container = event.target.closest('.' + this.$style.search)
|
||||
const getOffsetValue = (target, x = 0, y = 0) => {
|
||||
if (target === dom_container) return { x, y }
|
||||
if (!target) return { x: 0, y: 0 }
|
||||
x += target.offsetLeft
|
||||
y += target.offsetTop
|
||||
return getOffsetValue(target.offsetParent, x, y)
|
||||
}
|
||||
let { x, y } = getOffsetValue(event.target)
|
||||
this.listMenu.menuLocation.x = x + event.offsetX
|
||||
this.listMenu.menuLocation.y = y + event.offsetY - this.$refs.dom_scrollContent.scrollTop
|
||||
this.$nextTick(() => {
|
||||
this.listMenu.isShowItemMenu = true
|
||||
})
|
||||
},
|
||||
hideListMenu() {
|
||||
let dom_selected = this.$refs.dom_tbody && this.$refs.dom_tbody.querySelector('tr.selected')
|
||||
if (dom_selected) dom_selected.classList.remove('selected')
|
||||
this.listMenu.isShowItemMenu = false
|
||||
this.listMenu.rightClickItemIndex = -1
|
||||
},
|
||||
handleListItemMenuClick(action) {
|
||||
// console.log(action)
|
||||
let index = this.listMenu.rightClickItemIndex
|
||||
this.hideListMenu()
|
||||
let minfo
|
||||
let url
|
||||
switch (action && action.action) {
|
||||
case 'play':
|
||||
if (this.selectedData.length) {
|
||||
this.listAddMultiple({ id: 'default', list: this.filterList(this.selectedData) })
|
||||
this.removeAllSelect()
|
||||
}
|
||||
this.testPlay(index)
|
||||
break
|
||||
case 'playLater':
|
||||
if (this.selectedData.length) {
|
||||
this.setTempPlayList(this.selectedData.map(s => ({ listId: '__temp__', musicInfo: s })))
|
||||
this.resetSelect()
|
||||
} else {
|
||||
this.setTempPlayList([{ listId: '__temp__', musicInfo: this.listInfo.list[index] }])
|
||||
}
|
||||
break
|
||||
case 'addTo':
|
||||
if (this.selectedData.length) {
|
||||
this.$nextTick(() => {
|
||||
this.isShowListAddMultiple = true
|
||||
})
|
||||
} else {
|
||||
this.musicInfo = this.listInfo.list[index]
|
||||
this.$nextTick(() => {
|
||||
this.isShowListAdd = true
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'download':
|
||||
if (this.selectedData.length) {
|
||||
this.isShowDownloadMultiple = true
|
||||
} else {
|
||||
minfo = this.listInfo.list[index]
|
||||
if (!this.assertApiSupport(minfo.source)) return
|
||||
this.musicInfo = minfo
|
||||
this.$nextTick(() => {
|
||||
this.isShowDownload = true
|
||||
})
|
||||
}
|
||||
break
|
||||
case 'sourceDetail':
|
||||
minfo = this.listInfo.list[index]
|
||||
url = musicSdk[minfo.source].getMusicDetailPageUrl(minfo)
|
||||
if (!url) return
|
||||
openUrl(url)
|
||||
break
|
||||
}
|
||||
},
|
||||
handleSourceChange(source) {
|
||||
this.$router.replace({
|
||||
path: 'search',
|
||||
query: {
|
||||
text: this.text,
|
||||
source,
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '../assets/styles/layout.less';
|
||||
|
||||
.search {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.header {
|
||||
flex: none;
|
||||
}
|
||||
.main {
|
||||
flex: auto;
|
||||
min-height: 0;
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
.list {
|
||||
// position: relative;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
flex: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
.thead {
|
||||
flex: none;
|
||||
tr > th:first-child {
|
||||
color: @color-theme_2-font-label;
|
||||
// padding-left: 10px;
|
||||
}
|
||||
}
|
||||
.tbody {
|
||||
flex: auto;
|
||||
overflow-y: auto;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
td {
|
||||
font-size: 12px;
|
||||
:global(.badge) {
|
||||
margin-left: 3px;
|
||||
}
|
||||
&:first-child {
|
||||
// padding-left: 10px;
|
||||
font-size: 11px;
|
||||
color: @color-theme_2-font-label;
|
||||
}
|
||||
}
|
||||
:global(.badge) {
|
||||
opacity: .85;
|
||||
}
|
||||
|
||||
&.copying {
|
||||
.no-select {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listBtn {
|
||||
min-height: 24px;
|
||||
}
|
||||
.labelSource {
|
||||
color: @color-theme;
|
||||
padding: 5px;
|
||||
font-size: .8em;
|
||||
line-height: 1;
|
||||
opacity: .75;
|
||||
}
|
||||
.pagination {
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
// left: 50%;
|
||||
// transform: translateX(-50%);
|
||||
}
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: @color-theme_2;
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: @color-theme_2-font-label;
|
||||
}
|
||||
}
|
||||
.noitem {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: @color-theme_2-font-label;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.noitem-list-container {
|
||||
padding: 0 15px;
|
||||
margin-top: -20px;
|
||||
min-height: 300px;
|
||||
max-height: 94.7%;
|
||||
}
|
||||
.noitem-list {
|
||||
+.noitem-list {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
.noitem-hot-search-list {
|
||||
min-height: 106px;
|
||||
}
|
||||
.noitem-list-title {
|
||||
color: @color-theme_2-font-label;
|
||||
padding: 5px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.noitem-list-item {
|
||||
display: inline-block;
|
||||
margin: 3px 5px;
|
||||
background-color: @color-btn-background;
|
||||
padding: 7px 10px;
|
||||
border-radius: @radius-progress-border;
|
||||
transition: background-color @transition-theme;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
color: @color-btn;
|
||||
.mixin-ellipsis-1;
|
||||
max-width: 150px;
|
||||
&:hover {
|
||||
background-color: @color-btn-hover;
|
||||
}
|
||||
&:active {
|
||||
background-color: @color-btn-active;
|
||||
}
|
||||
}
|
||||
.history-clear-btn {
|
||||
padding: 0 5px;
|
||||
margin-left: 5px;
|
||||
color: @color-theme_2-font-label;
|
||||
cursor: pointer;
|
||||
transition: color @transition-theme;
|
||||
&:hover {
|
||||
color: @color-theme-hover;
|
||||
}
|
||||
&:active {
|
||||
color: @color-theme-active;
|
||||
}
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.labelSource {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
.loading {
|
||||
background-color: ~'@{color-@{value}-theme_2}';
|
||||
}
|
||||
.noitem {
|
||||
p {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
.noitem-list-title {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
.noitem-list-item {
|
||||
color: ~'@{color-@{value}-btn}';
|
||||
background-color: ~'@{color-@{value}-btn-background}';
|
||||
&:hover {
|
||||
background-color: ~'@{color-@{value}-btn-hover}';
|
||||
}
|
||||
&:active {
|
||||
background-color: ~'@{color-@{value}-btn-active}';
|
||||
}
|
||||
}
|
||||
.tbody {
|
||||
td {
|
||||
&:first-child {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
}
|
||||
.history-clear-btn {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
&:hover {
|
||||
color: ~'@{color-@{value}-theme-hover}';
|
||||
}
|
||||
&:active {
|
||||
color: ~'@{color-@{value}-theme-active}';
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<material-online-list
|
||||
ref="listRef"
|
||||
:page="listInfo.page"
|
||||
:limit="listInfo.limit"
|
||||
:total="listInfo.total"
|
||||
:list="listInfo.list"
|
||||
:no-item="listInfo.noItemLabel"
|
||||
:source-tag="sourceId == 'all'"
|
||||
check-api-source
|
||||
@toggle-page="handleTogglePage"
|
||||
@play-list="handlePlayList"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, defineProps } from '@common/utils/vueTools'
|
||||
import { searchText } from '@renderer/store/search/state'
|
||||
import { useRouter, useRoute } from '@common/utils/vueRouter'
|
||||
import useList, { SearchSource } from './useList'
|
||||
|
||||
interface Props {
|
||||
sourceId: SearchSource
|
||||
page: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const {
|
||||
listRef,
|
||||
listInfo,
|
||||
search,
|
||||
handlePlayList,
|
||||
} = useList()
|
||||
|
||||
watch(() => [props.sourceId, props.page], ([sourceId, page]) => {
|
||||
setTimeout(() => {
|
||||
search(searchText.value, sourceId as SearchSource, page as number || 1)
|
||||
})
|
||||
})
|
||||
watch(searchText, (searchText) => {
|
||||
setTimeout(() => {
|
||||
search(searchText, props.sourceId, props.page)
|
||||
})
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
const handleTogglePage = (page: number) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
page,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
.container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.list {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { playList } from '@renderer/core/player/action'
|
||||
import { getListMusics, addListMusics } from '@renderer/store/list/action'
|
||||
import { addHistoryWord } from '@renderer/store/search/action'
|
||||
// import { useI18n } from '@renderer/plugins/i18n'
|
||||
// import { } from '@renderer/store/search/state'
|
||||
import { search as searchMusic, listInfos, ListInfo } from '@renderer/store/search/music'
|
||||
import { assertApiSupport } from '@renderer/store/utils'
|
||||
|
||||
export declare type SearchSource = LX.OnlineSource | 'all'
|
||||
|
||||
export default () => {
|
||||
const listRef = ref<any>(null)
|
||||
|
||||
const listInfo = ref<ListInfo>({
|
||||
page: 1,
|
||||
maxPage: 0,
|
||||
limit: 30,
|
||||
total: 0,
|
||||
list: [],
|
||||
key: null,
|
||||
noItemLabel: '',
|
||||
})
|
||||
|
||||
const search = (text: string, source: SearchSource, page: number) => {
|
||||
listInfo.value = listInfos[source] as ListInfo
|
||||
if (text.length) void addHistoryWord(text)
|
||||
void searchMusic(text, page, source).then((list: LX.Music.MusicInfo[]) => {
|
||||
if (list.length) {
|
||||
setTimeout(() => {
|
||||
if (listRef.value) listRef.value.scrollToTop()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handlePlayList = async(index: number) => {
|
||||
let targetSong = listInfo.value.list[index]
|
||||
|
||||
if (!assertApiSupport(targetSong.source)) return
|
||||
|
||||
const defaultListMusics = await getListMusics(LIST_IDS.DEFAULT)
|
||||
|
||||
await addListMusics(LIST_IDS.DEFAULT, [targetSong])
|
||||
|
||||
let targetIndex = defaultListMusics.findIndex(s => s.id === targetSong.id)
|
||||
if (targetIndex > -1) playList(LIST_IDS.DEFAULT, targetIndex)
|
||||
}
|
||||
|
||||
return {
|
||||
listRef,
|
||||
listInfo,
|
||||
search,
|
||||
handlePlayList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="scroll" :class="$style.container">
|
||||
<SongList ref="listRef" :list-info="listInfo" @toggle-page="togglePage" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, defineProps } from '@common/utils/vueTools'
|
||||
import { searchText } from '@renderer/store/search/state'
|
||||
import { useRouter, useRoute } from '@common/utils/vueRouter'
|
||||
import useList, { SearchSource } from './useList'
|
||||
import SongList from '@renderer/views/songList/List/components/SongList.vue'
|
||||
|
||||
interface Props {
|
||||
sourceId: SearchSource
|
||||
page: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const {
|
||||
listRef,
|
||||
listInfo,
|
||||
search,
|
||||
} = useList()
|
||||
|
||||
watch(() => [props.sourceId, props.page], ([sourceId, page]) => {
|
||||
setTimeout(() => {
|
||||
search(searchText.value, sourceId as SearchSource, page as number || 1)
|
||||
})
|
||||
})
|
||||
watch(searchText, (searchText) => {
|
||||
setTimeout(() => {
|
||||
search(searchText, props.sourceId, props.page)
|
||||
})
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
const togglePage = (page: number) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
page,
|
||||
},
|
||||
})
|
||||
// search(searchText.value, props.sourceId, page)
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
.container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// .list {
|
||||
// overflow: hidden;
|
||||
// height: 100%;
|
||||
// flex: auto;
|
||||
// }
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
import { onBeforeRouteLeave } from '@common/utils/vueRouter'
|
||||
import { ref, nextTick } from '@common/utils/vueTools'
|
||||
import { addHistoryWord } from '@renderer/store/search/action'
|
||||
// import { useI18n } from '@renderer/plugins/i18n'
|
||||
// import { } from '@renderer/store/search/state'
|
||||
import { search as searchSongList, listInfos, SearchListInfo, ListInfoItem } from '@renderer/store/search/songlist'
|
||||
|
||||
export declare type SearchSource = LX.OnlineSource | 'all'
|
||||
|
||||
export default () => {
|
||||
const listRef = ref<any>(null)
|
||||
|
||||
const listInfo = ref<SearchListInfo>({
|
||||
page: 1,
|
||||
limit: 30,
|
||||
total: 0,
|
||||
list: [],
|
||||
key: null,
|
||||
noItemLabel: '',
|
||||
tagId: '',
|
||||
sortId: '',
|
||||
})
|
||||
|
||||
const search = (text: string, source: SearchSource, page: number) => {
|
||||
// console.log(text, source, page)
|
||||
listInfo.value = listInfos[source] as SearchListInfo
|
||||
if (text.length) void addHistoryWord(text)
|
||||
void searchSongList(text, page, source).then((list: ListInfoItem[]) => {
|
||||
// console.log(list)
|
||||
if (listInfo.value.key == window.lx.songListInfo.searchKey && window.lx.songListInfo.searchPosition) {
|
||||
void nextTick(() => {
|
||||
listRef.value?.scrollTo(window.lx.songListInfo.searchPosition)
|
||||
})
|
||||
} else if (list.length && listRef.value) {
|
||||
window.lx.songListInfo.searchKey = null
|
||||
void nextTick(() => {
|
||||
listRef.value.scrollTo(0)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
window.lx.songListInfo.searchKey = listInfo.value.key
|
||||
if (listRef.value) window.lx.songListInfo.searchPosition = listRef.value.getScrollTop()
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
listRef,
|
||||
listInfo,
|
||||
search,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<transition enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut">
|
||||
<div v-show="props.visible" :class="$style.noitem">
|
||||
<div v-if="appSetting['search.isShowHotSearch'] || (appSetting['search.isShowHistorySearch'] && historyList.length)" class="scroll" :class="$style.noitemListContainer">
|
||||
<dl v-if="appSetting['search.isShowHotSearch']" :class="[$style.noitemList, $style.noitemHotSearchList]">
|
||||
<dt :class="$style.noitemListTitle">{{ $t('search__hot_search') }}</dt>
|
||||
<dd v-for="(item, index) in hotSearchList" :key="index" :class="$style.noitemListItem" @click="handleSearch(item)">{{ item }}</dd>
|
||||
</dl>
|
||||
<dl v-if="appSetting['search.isShowHistorySearch'] && historyList.length" :class="$style.noitemList">
|
||||
<dt :class="$style.noitemListTitle">
|
||||
<span>{{ $t('history_search') }}</span><span :class="$style.historyClearBtn" :aria-label="$t('history_clear')" @click="clearHistoryList">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 512 512" space="preserve">
|
||||
<use xlink:href="#icon-eraser" />
|
||||
</svg></span>
|
||||
</dt>
|
||||
<dd v-for="(item, index) in historyList" :key="index + item" :class="$style.noitemListItem" :aria-label="$t('history_remove')" @contextmenu="removeHistoryWord(index)" @click="handleSearch(item)">{{ item }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div v-else :class="$style.noitem_label">
|
||||
<p>{{ $t('search__welcome') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch, shallowRef } from '@common/utils/vueTools'
|
||||
import { historyList } from '@renderer/store/search/state'
|
||||
import { getHistoryList, removeHistoryWord, clearHistoryList } from '@renderer/store/search/action'
|
||||
import { getList } from '@renderer/store/hotSearch'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { useRouter } from '@common/utils/vueRouter'
|
||||
|
||||
const props = defineProps({
|
||||
visible: Boolean,
|
||||
source: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const hotSearchList = shallowRef([])
|
||||
|
||||
if (appSetting['search.isShowHotSearch']) {
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (!visible) return
|
||||
getList(props.source).then(list => {
|
||||
hotSearchList.value = list
|
||||
})
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
watch(() => props.source, (source) => {
|
||||
if (!props.visible) return
|
||||
getList(source).then(list => {
|
||||
hotSearchList.value = list
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (appSetting['search.isShowHistorySearch']) {
|
||||
getHistoryList()
|
||||
}
|
||||
|
||||
const router = useRouter()
|
||||
const handleSearch = (text) => {
|
||||
router.replace({
|
||||
path: 'search',
|
||||
query: {
|
||||
text,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.noitem {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
// justify-content: center;
|
||||
}
|
||||
.noitem-list-container {
|
||||
padding: 3% 15px 15px;
|
||||
// margin-top: -20px;
|
||||
min-height: 300px;
|
||||
max-height: 94.7%;
|
||||
}
|
||||
.noitem-list {
|
||||
+.noitem-list {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
.noitem-hot-search-list {
|
||||
min-height: 106px;
|
||||
}
|
||||
.noitem-list-title {
|
||||
color: var(--color-font);
|
||||
padding: 5px 5px 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.noitem-list-item {
|
||||
display: inline-block;
|
||||
margin: 3px 5px;
|
||||
background-color: var(--color-button-background);
|
||||
padding: 7px 10px;
|
||||
border-radius: @radius-progress-border;
|
||||
transition: background-color @transition-normal;
|
||||
cursor: pointer;
|
||||
color: var(--color-button-font);
|
||||
.mixin-ellipsis-1;
|
||||
max-width: 150px;
|
||||
font-size: 13px;
|
||||
&:hover {
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-button-background-active);
|
||||
}
|
||||
}
|
||||
.history-clear-btn {
|
||||
padding: 0 5px;
|
||||
margin-left: 5px;
|
||||
color: var(--color-font-label);
|
||||
cursor: pointer;
|
||||
transition: @transition-normal;
|
||||
transition-property: color, opacity;
|
||||
opacity: .3;
|
||||
&:hover {
|
||||
color: var(--color-primary-font-hover);
|
||||
opacity: .8;
|
||||
}
|
||||
&:active {
|
||||
color: var(--color-primary-font-active);
|
||||
opacity: 1;
|
||||
}
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.noitem_label {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: var(--color-font-label);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.header">
|
||||
<base-tab v-model="source" :class="$style.tab" :list="sources" @change="handleSourceChange" />
|
||||
<base-tab v-model="searchType" :class="$style.tab" :list="searchTypes" @change="handleTypeChange" />
|
||||
</div>
|
||||
<div :class="$style.main">
|
||||
<song-list-list v-if="searchType == 'songlist'" v-show="searchText" :page="page" :source-id="source" />
|
||||
<music-list v-else v-show="searchText" :page="page" :source-id="source" />
|
||||
<blank-view :visible="!searchText" :source="source" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useRoute, useRouter } from '@common/utils/vueRouter'
|
||||
import { searchText } from '@renderer/store/search/state'
|
||||
import { getSearchSetting, setSearchSetting } from '@renderer/utils/data'
|
||||
import { sources as _sources } from '@renderer/store/search/music'
|
||||
|
||||
import MusicList from './MusicList/index.vue'
|
||||
import SongListList from './SongListList/index.vue'
|
||||
import BlankView from './components/BlankView.vue'
|
||||
import { computed, ref } from '@common/utils/vueTools'
|
||||
import { sourceNames } from '@renderer/store'
|
||||
|
||||
const source = ref('kw')
|
||||
const searchType = ref(null)
|
||||
const page = ref(1)
|
||||
|
||||
const verifyQueryParams = async(to, from, next) => {
|
||||
let _source = to.query.source
|
||||
let _type = to.query.type
|
||||
let _page = to.query.page
|
||||
|
||||
if (_source == null || _type == null) {
|
||||
const setting = await getSearchSetting()
|
||||
if (!_source) _source = setting.source
|
||||
if (!_type) _type = setting.type
|
||||
|
||||
next({
|
||||
path: to.path,
|
||||
query: { ...to.query, source: _source, type: _type, page: _page },
|
||||
})
|
||||
return
|
||||
}
|
||||
source.value = _source
|
||||
searchType.value = _type
|
||||
|
||||
if (_page) page.value = parseInt(_page)
|
||||
|
||||
if (to.query.text != null) {
|
||||
searchText.value = to.query.text
|
||||
if (!_page) page.value = 1
|
||||
}
|
||||
next()
|
||||
setSearchSetting({ source: _source, type: _type })
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MusicList,
|
||||
SongListList,
|
||||
BlankView,
|
||||
},
|
||||
beforeRouteEnter: verifyQueryParams,
|
||||
beforeRouteUpdate: verifyQueryParams,
|
||||
setup() {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const sources = _sources.map(id => {
|
||||
return {
|
||||
id,
|
||||
label: sourceNames.value[id],
|
||||
}
|
||||
})
|
||||
const handleSourceChange = (id) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
source: id,
|
||||
page: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const searchTypes = computed(() => {
|
||||
return [
|
||||
{ label: window.i18n.t('search__type_music'), id: 'music' },
|
||||
{ label: window.i18n.t('search__type_songlist'), id: 'songlist' },
|
||||
]
|
||||
})
|
||||
const handleTypeChange = (type) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
type,
|
||||
page: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
sources,
|
||||
source,
|
||||
handleSourceChange,
|
||||
searchTypes,
|
||||
searchType,
|
||||
handleTypeChange,
|
||||
page,
|
||||
searchText,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
.container {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.header {
|
||||
// padding: 5px 0;
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.main {
|
||||
position: relative;
|
||||
flex: auto;
|
||||
// min-height: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,329 +0,0 @@
|
||||
<template>
|
||||
<div id="my-list" :class="$style.container" @click="handleContainerClick" v-if="isInitedList">
|
||||
<MyLists :list-id="listId" @show-menu="$refs.musicList.handleMenuClick()" ref="lists" />
|
||||
<MusicList :list-id="listId" @show-menu="$refs.lists.hideListsMenu()" ref="musicList" />
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getListPrevSelectId } from '@renderer/utils/data'
|
||||
import { isInitedList, defaultList } from '@renderer/core/share/list'
|
||||
import { getList } from '@renderer/core/share/utils'
|
||||
|
||||
import MyLists from './components/MyLists'
|
||||
import MusicList from './components/MusicList'
|
||||
|
||||
export default {
|
||||
name: 'List',
|
||||
components: {
|
||||
MyLists,
|
||||
MusicList,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
isInitedList,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
listId: null,
|
||||
focusTarget: 'listDetail',
|
||||
}
|
||||
},
|
||||
beforeRouteEnter(to, from) {
|
||||
let id = to.query.id
|
||||
if (!id) {
|
||||
id = getListPrevSelectId() || defaultList.id
|
||||
return {
|
||||
path: '/list',
|
||||
query: { id },
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeRouteUpdate(to, from) {
|
||||
// console.log(to, from)
|
||||
if (to.query.updated) return
|
||||
let id = to.query.id
|
||||
if (id == null) return
|
||||
if (!getList(id)) {
|
||||
id = defaultList.id
|
||||
}
|
||||
this.listId = id
|
||||
const scrollIndex = to.query.scrollIndex
|
||||
const isAnimation = from.query.id == to.query.id
|
||||
this.$nextTick(() => {
|
||||
this.$refs.musicList?.restoreScroll(scrollIndex, isAnimation)
|
||||
})
|
||||
return {
|
||||
path: '/list',
|
||||
query: { id, updated: true },
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(to, from) {
|
||||
this.$refs.musicList?.saveListPosition()
|
||||
},
|
||||
created() {
|
||||
this.listId = this.$route.query.id
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.container {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@lists-item-height: 36px;
|
||||
.lists {
|
||||
flex: none;
|
||||
width: 16%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.listHeader {
|
||||
position: relative;
|
||||
&:hover {
|
||||
.listsAdd {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsTitle {
|
||||
font-size: 12px;
|
||||
line-height: 38px;
|
||||
padding: 0 10px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
flex: none;
|
||||
}
|
||||
.listsAdd {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 8px;
|
||||
background: none;
|
||||
height: 30px;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: @radius-border;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity @transition-theme;
|
||||
color: @color-btn;
|
||||
svg {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
&:active {
|
||||
opacity: .7 !important;
|
||||
}
|
||||
}
|
||||
.listsContent {
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
// border-right: 1px solid rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
.listsItem {
|
||||
position: relative;
|
||||
transition: .3s ease;
|
||||
transition-property: color, background-color, opacity;
|
||||
background-color: transparent;
|
||||
&:hover:not(.active) {
|
||||
background-color: @color-theme_2-hover;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.active {
|
||||
// background-color:
|
||||
color: @color-theme;
|
||||
}
|
||||
&.selected {
|
||||
background-color: @color-theme_2-active;
|
||||
}
|
||||
&.clicked {
|
||||
background-color: @color-theme_2-hover;
|
||||
}
|
||||
&.fetching {
|
||||
opacity: .5;
|
||||
}
|
||||
&.editing {
|
||||
padding: 0 10px;
|
||||
background-color: @color-theme_2-hover;
|
||||
.listsLabel {
|
||||
display: none;
|
||||
}
|
||||
.listsInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsLabel {
|
||||
display: block;
|
||||
height: @lists-item-height;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
line-height: @lists-item-height;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.listsInput {
|
||||
width: 100%;
|
||||
height: @lists-item-height;
|
||||
border: none;
|
||||
padding: 0;
|
||||
// padding-bottom: 1px;
|
||||
line-height: @lists-item-height;
|
||||
background: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.listsNew {
|
||||
padding: 0 10px;
|
||||
background-color: @color-theme_2-hover;
|
||||
.listsInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.newLeave {
|
||||
margin-top: -@lists-item-height;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.list {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
// .noItem {
|
||||
|
||||
// }
|
||||
}
|
||||
.content {
|
||||
min-height: 0;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
// table {
|
||||
// position: relative;
|
||||
// thead {
|
||||
// position: fixed;
|
||||
// width: 100%;
|
||||
// th {
|
||||
// width: 100%;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
&.copying {
|
||||
.no-select {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.thead {
|
||||
flex: none;
|
||||
tr > th:first-child {
|
||||
color: @color-theme_2-font-label;
|
||||
// padding-left: 10px;
|
||||
}
|
||||
}
|
||||
:global(.list) {
|
||||
flex: auto;
|
||||
:global(.list-item) {
|
||||
&.active {
|
||||
color: @color-btn;
|
||||
}
|
||||
}
|
||||
:global(.list-item-cell) {
|
||||
font-size: 12px !important;
|
||||
|
||||
&:first-child {
|
||||
// padding-left: 10px;
|
||||
font-size: 11px !important;
|
||||
color: @color-theme_2-font-label !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.labelSource {
|
||||
color: @color-theme;
|
||||
padding: 5px;
|
||||
font-size: .8em;
|
||||
line-height: 1;
|
||||
opacity: .75;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.no-item {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: @color-theme_2-font-label;
|
||||
}
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.listsAdd {
|
||||
color: ~'@{color-@{value}-btn}';
|
||||
}
|
||||
.listsItem {
|
||||
&:hover:not(.active) {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
&.active {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
&.select {
|
||||
background-color: ~'@{color-@{value}-theme_2-active}';
|
||||
}
|
||||
&.clicked {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
&.editing {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
}
|
||||
.listsNew {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
:global(.list) {
|
||||
:global(.list-item) {
|
||||
&.active {
|
||||
color: ~'@{color-@{value}-btn}';
|
||||
}
|
||||
}
|
||||
:global(.list-item-cell) {
|
||||
&:first-child {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}' !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.labelSource {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
.no-item {
|
||||
p {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
@@ -1,262 +0,0 @@
|
||||
<template>
|
||||
<material-modal :show="visible" @close="$emit('update:visible', false)" bg-close teleport="#view">
|
||||
<div :class="$style.header">
|
||||
<h2>{{listInfo.name}}</h2>
|
||||
</div>
|
||||
<main class="scroll" :class="$style.main">
|
||||
<ul ref="dom_list" v-if="duplicateList.length" :class="$style.list">
|
||||
<li v-for="(item, index) in duplicateList" :key="item.songmid" :class="$style.listItem">
|
||||
<div :class="$style.num">{{item.index + 1}}</div>
|
||||
<div :class="$style.text">
|
||||
<h3 :class="$style.text">{{item.musicInfo.name}} - {{item.musicInfo.singer}}</h3>
|
||||
<h3 v-if="item.musicInfo.albumName" :class="[$style.text, $style.albumName]">{{item.musicInfo.albumName}}</h3>
|
||||
</div>
|
||||
<div :class="$style.label">{{item.musicInfo.source}}</div>
|
||||
<div :class="$style.label">{{item.musicInfo.interval}}</div>
|
||||
<div :class="$style.btns">
|
||||
<button type="button" @click="handlePlay(index)" :class="$style.btn">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="50%" viewBox="0 0 287.386 287.386" space="preserve" v-once>
|
||||
<use xlink:href="#icon-testPlay"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" @click="handleRemove(index)" :class="$style.btn">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="50%" viewBox="0 0 212.982 212.982" space="preserve" v-once>
|
||||
<use xlink:href="#icon-delete"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div :class="$style.noItem" v-else>
|
||||
<p v-text="$t('no_item')"></p>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
listInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(n) {
|
||||
if (n) this.handleFilterList()
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
duplicateList: [],
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.listInfo.list) this.handleFilterList()
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('list', [
|
||||
'listRemove',
|
||||
]),
|
||||
...mapMutations('player', {
|
||||
setPlayList: 'setList',
|
||||
}),
|
||||
handleFilterList() {
|
||||
const listMap = new Map()
|
||||
const duplicateList = []
|
||||
this.listInfo.list.forEach((musicInfo, index) => {
|
||||
const musicInfoName = musicInfo.name.toLowerCase().trim()
|
||||
if (listMap.has(musicInfoName)) {
|
||||
const targetMusicInfo = listMap.get(musicInfoName)
|
||||
duplicateList.push({
|
||||
index: this.listInfo.list.indexOf(targetMusicInfo),
|
||||
musicInfo: targetMusicInfo,
|
||||
}, {
|
||||
index,
|
||||
musicInfo,
|
||||
})
|
||||
} else {
|
||||
listMap.set(musicInfoName, musicInfo)
|
||||
}
|
||||
})
|
||||
|
||||
this.duplicateList = duplicateList
|
||||
},
|
||||
handleRemove(index) {
|
||||
const { musicInfo: targetMusicInfo } = this.duplicateList.splice(index, 1)[0]
|
||||
// let duplicates = []
|
||||
// for (let index = 0; index < this.duplicateList.length; index++) {
|
||||
// const { musicInfo } = this.duplicateList[index]
|
||||
// if (musicInfo.name == targetMusicInfo.name) {
|
||||
// duplicates.push(index)
|
||||
// if (duplicates.length > 1) break
|
||||
// }
|
||||
// }
|
||||
// console.log(duplicates)
|
||||
// if (duplicates.length < 2) this.duplicateList.splice(duplicates[0], 1)
|
||||
|
||||
this.listRemove({ listId: this.listInfo.id, id: targetMusicInfo.songmid })
|
||||
|
||||
this.handleFilterList()
|
||||
},
|
||||
handlePlay(index) {
|
||||
const { index: musicInfoIndex } = this.duplicateList[index]
|
||||
this.setPlayList({ listId: this.listInfo.id, index: musicInfoIndex })
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.header {
|
||||
flex: none;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
h2 {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
.main {
|
||||
min-height: 200px;
|
||||
width: 460px;
|
||||
}
|
||||
|
||||
.list {
|
||||
// background-color: @color-search-form-background;
|
||||
font-size: 13px;
|
||||
transition-property: height;
|
||||
position: relative;
|
||||
.listItem {
|
||||
position: relative;
|
||||
padding: 8px 5px;
|
||||
transition: background-color .2s ease;
|
||||
line-height: 1.3;
|
||||
// overflow: hidden;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: @color-theme_2-hover;
|
||||
}
|
||||
// border-radius: 4px;
|
||||
// &:last-child {
|
||||
// border-bottom-left-radius: 4px;
|
||||
// border-bottom-right-radius: 4px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
.num {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
color: @color-theme_2-font-label;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: auto;
|
||||
padding-left: 5px;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.albumName {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.label {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// transform: rotate(45deg);
|
||||
// background-color:
|
||||
}
|
||||
.btns {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: @form-radius;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
padding: 4px 7px;
|
||||
color: @color-btn;
|
||||
outline: none;
|
||||
transition: background-color 0.2s ease;
|
||||
line-height: 0;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @color-theme_2-hover;
|
||||
}
|
||||
&:active {
|
||||
background-color: @color-theme_2-active;
|
||||
}
|
||||
}
|
||||
|
||||
.no-item {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: @color-theme_2-font-label;
|
||||
}
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.listItem {
|
||||
&:hover {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
}
|
||||
.num {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
.btn {
|
||||
color: ~'@{color-@{value}-btn}';
|
||||
&:hover {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
&:active {
|
||||
background-color: ~'@{color-@{value}-theme_2-active}';
|
||||
}
|
||||
}
|
||||
.no-item {
|
||||
p {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
@@ -1,238 +0,0 @@
|
||||
<template>
|
||||
<material-modal :show="visible" @close="closeModal" teleport="#view" bg-close @after-leave="handleAfterLeave">
|
||||
<main :class="$style.main">
|
||||
<div :class="$style.header">
|
||||
<h2>{{listInfo.name}}</h2>
|
||||
</div>
|
||||
<section>
|
||||
<h3 :class="$style.title">{{$t('list_sort_modal_by_field')}}</h3>
|
||||
<ul :class="$style.list">
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox id="list_sort_modal_field_name" name="list_sort_modal_field" need="need"
|
||||
v-model="sortField" value="name" :label="$t('list_sort_modal_by_name')" />
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox id="list_sort_modal_field_singer" name="list_sort_modal_field" need="need"
|
||||
v-model="sortField" value="singer" :label="$t('list_sort_modal_by_singer')" />
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox id="list_sort_modal_field_album" name="list_sort_modal_field" need="need"
|
||||
v-model="sortField" value="album" :label="$t('list_sort_modal_by_album')" />
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox id="list_sort_modal_field_time" name="list_sort_modal_field" need="need"
|
||||
v-model="sortField" value="time" :label="$t('list_sort_modal_by_time')" />
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox id="list_sort_modal_field_source" name="list_sort_modal_field" need="need"
|
||||
v-model="sortField" value="source" :label="$t('list_sort_modal_by_source')" />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h3 :class="$style.title">{{$t('list_sort_modal_by_type')}}</h3>
|
||||
<ul :class="$style.list">
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox id="list_sort_modal_type_up" name="list_sort_modal_type" need="need"
|
||||
v-model="sortType" value="up" :label="$t('list_sort_modal_by_up')" />
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox id="list_sort_modal_type_down" name="list_sort_modal_type" need="need"
|
||||
v-model="sortType" value="down" :label="$t('list_sort_modal_by_down')" />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div :class="$style.footer">
|
||||
<base-btn :class="$style.btn" @click="closeModal">{{$t('music_sort__btn_cancel')}}</base-btn>
|
||||
<base-btn :class="$style.btn" @click="handleSort">{{$t('music_sort__btn_confirm')}}</base-btn>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, useCommit } from '@renderer/utils/vueTools'
|
||||
// import { dialog } from '@renderer/plugins/Dialog'
|
||||
import { getList } from '@renderer/core/share/utils'
|
||||
|
||||
const getIntv = (musicInfo) => {
|
||||
if (!musicInfo.interval) return 0
|
||||
if (musicInfo._interval) return musicInfo._interval
|
||||
let intvArr = musicInfo.interval.split(':')
|
||||
let intv = 0
|
||||
let unit = 1
|
||||
while (intvArr.length) {
|
||||
intv += (intvArr.pop()) * unit
|
||||
unit *= 60
|
||||
}
|
||||
return parseInt(intv)
|
||||
}
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
listInfo: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:visible'],
|
||||
setup(props, { emit }) {
|
||||
// const { t } = useI18n()
|
||||
const sortField = ref(null)
|
||||
const sortType = ref(null)
|
||||
const setList = useCommit('list', 'setList')
|
||||
const closeModal = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
const handleAfterLeave = () => {
|
||||
sortField.value = null
|
||||
sortType.value = null
|
||||
}
|
||||
const verify = () => {
|
||||
return !!sortField.value && !!sortType.value
|
||||
}
|
||||
const handleSort = async() => {
|
||||
if (!verify()) return
|
||||
// if (!await dialog.confirm({
|
||||
// message: t('list_sort_modal_tip_confirm'),
|
||||
// cancelButtonText: t('cancel_button_text'),
|
||||
// confirmButtonText: t('confirm_button_text'),
|
||||
// })) return
|
||||
|
||||
const list = [...getList(props.listInfo.id)]
|
||||
let fieldName
|
||||
switch (sortField.value) {
|
||||
case 'name':
|
||||
fieldName = 'name'
|
||||
break
|
||||
case 'singer':
|
||||
fieldName = 'singer'
|
||||
break
|
||||
case 'album':
|
||||
fieldName = 'albumName'
|
||||
break
|
||||
case 'time':
|
||||
fieldName = 'interval'
|
||||
break
|
||||
case 'source':
|
||||
fieldName = 'source'
|
||||
break
|
||||
}
|
||||
console.log(sortType.value, sortField.value)
|
||||
if (sortType.value == 'up') {
|
||||
if (fieldName == 'interval') {
|
||||
list.sort((a, b) => {
|
||||
if (a.interval == null) {
|
||||
return b.interval == null ? 0 : -1
|
||||
} else return b.interval == null ? 1 : getIntv(a) - getIntv(b)
|
||||
})
|
||||
} else {
|
||||
list.sort((a, b) => {
|
||||
if (a[fieldName] == null) {
|
||||
return b[fieldName] == null ? 0 : -1
|
||||
} else return b[fieldName] == null ? 1 : a[fieldName].localeCompare(b[fieldName])
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if (fieldName == 'interval') {
|
||||
list.sort((a, b) => {
|
||||
if (a.interval == null) {
|
||||
return b.interval == null ? 0 : 1
|
||||
} else return b.interval == null ? -1 : getIntv(b) - getIntv(a)
|
||||
})
|
||||
} else {
|
||||
list.sort((a, b) => {
|
||||
if (a[fieldName] == null) {
|
||||
return b[fieldName] == null ? 0 : 1
|
||||
} else return b[fieldName] == null ? -1 : b[fieldName].localeCompare(a[fieldName])
|
||||
})
|
||||
}
|
||||
}
|
||||
closeModal()
|
||||
setList({ ...props.listInfo, list })
|
||||
}
|
||||
return {
|
||||
sortField,
|
||||
sortType,
|
||||
closeModal,
|
||||
handleSort,
|
||||
handleAfterLeave,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.header {
|
||||
flex: none;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
h2 {
|
||||
color: @color-theme_2-font;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 0 15px;
|
||||
width: 320px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-height: 0;
|
||||
// max-height: 100%;
|
||||
// overflow: hidden;
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: @color-theme_2-font-label;
|
||||
padding: 10px 0 8px;
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
.listItem {
|
||||
width: (100% / 3);
|
||||
padding-left: 10px;
|
||||
margin-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.footer {
|
||||
margin: 20px 0 15px auto;
|
||||
}
|
||||
.btn {
|
||||
// box-sizing: border-box;
|
||||
// margin-left: 15px;
|
||||
// margin-bottom: 15px;
|
||||
// height: 36px;
|
||||
// line-height: 36px;
|
||||
// padding: 0 10px !important;
|
||||
min-width: 70px;
|
||||
// .mixin-ellipsis-1;
|
||||
|
||||
+.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.header {
|
||||
h2 {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
}
|
||||
.title {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
@@ -1,239 +0,0 @@
|
||||
<template>
|
||||
<material-modal :show="visible" @close="$emit('update:visible', false)" bg-close teleport="#view">
|
||||
<div :class="$style.header">
|
||||
<h2>{{$t('list_update_modal__title')}}</h2>
|
||||
</div>
|
||||
<main class="scroll" :class="$style.main">
|
||||
<ul ref="dom_list" v-if="lists.length" :class="$style.list">
|
||||
<li v-for="(list, index) in lists" :key="list.id" :class="[$style.listItem, {[$style.fetching]: fetchingListStatus[list.id]}]">
|
||||
<div :class="$style.listLeft">
|
||||
<h3 :class="$style.text">{{list.name}} <span :class="$style.label">{{list.source}}</span></h3>
|
||||
<div>
|
||||
<base-checkbox :class="$style.checkbox" :id="`list_auto_update_${list.id}`" :modelValue="autoUpdate[list.id] == true"
|
||||
@change="handleChangeAutoUpdate(list, $event)" :label="$t('list_update_modal__auto_update')" />
|
||||
<span :class="$style.label" style="vertical-align: text-top;">{{listUpdateTimes[list.id]}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.btns">
|
||||
<button :class="$style.btn" :disabled="fetchingListStatus[list.id]" outline="outline" :aria-label="$t('list_update_modal__update')" @click.stop="handleUpdate(index)">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" style="transform: rotate(45deg);" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-refresh" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div :class="$style.noItem" v-else>
|
||||
<p v-text="$t('no_item')"></p>
|
||||
</div>
|
||||
</main>
|
||||
<div :class="$style.footer">
|
||||
<div :class="$style.tips">{{$t('list_update_modal__tips')}}</div>
|
||||
</div>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, reactive } from '@renderer/utils/vueTools'
|
||||
import { userLists, fetchingListStatus } from '@renderer/core/share/list'
|
||||
import musicSdk from '@renderer/utils/music'
|
||||
import { getListUpdateInfo, setListAutoUpdate } from '@renderer/utils/data'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
listUpdateTimes: Object,
|
||||
},
|
||||
emits: ['update-list', 'update:visible'],
|
||||
setup(props, { emit }) {
|
||||
const lists = computed(() => userLists.filter(l => !!l.source && !!musicSdk[l.source]?.songList))
|
||||
const autoUpdate = reactive({})
|
||||
for (const [id, value] of Object.entries(getListUpdateInfo())) {
|
||||
autoUpdate[id] = value.isAutoUpdate == true
|
||||
}
|
||||
|
||||
const handleChangeAutoUpdate = (list, enable) => {
|
||||
setListAutoUpdate(list.id, enable)
|
||||
autoUpdate[list.id] = enable
|
||||
}
|
||||
const handleUpdate = index => {
|
||||
emit('update-list', lists.value[index])
|
||||
}
|
||||
return {
|
||||
lists,
|
||||
autoUpdate,
|
||||
fetchingListStatus,
|
||||
handleUpdate,
|
||||
handleChangeAutoUpdate,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
@width: 460px;
|
||||
|
||||
.header {
|
||||
flex: none;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
h2 {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
.main {
|
||||
min-height: 200px;
|
||||
width: @width;
|
||||
}
|
||||
|
||||
.list {
|
||||
// background-color: @color-search-form-background;
|
||||
font-size: 13px;
|
||||
transition-property: height;
|
||||
position: relative;
|
||||
.listItem {
|
||||
position: relative;
|
||||
padding: 15px 10px 15px 15px;
|
||||
transition: .3s ease;
|
||||
transition-property: background-color, opacity;
|
||||
line-height: 1.3;
|
||||
// overflow: hidden;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: @color-theme_2-hover;
|
||||
}
|
||||
// border-radius: 4px;
|
||||
// &:last-child {
|
||||
// border-bottom-left-radius: 4px;
|
||||
// border-bottom-right-radius: 4px;
|
||||
// }
|
||||
&.fetching {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listLeft {
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: auto;
|
||||
margin-bottom: 2px;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.checkbox {
|
||||
margin-top: 3px;
|
||||
font-size: 14px;
|
||||
opacity: .86;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
padding: 0 10px;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// transform: rotate(45deg);
|
||||
// background-color:
|
||||
}
|
||||
.btns {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: @form-radius;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
padding: 4px 7px;
|
||||
color: @color-btn;
|
||||
outline: none;
|
||||
transition: background-color 0.2s ease;
|
||||
line-height: 0;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: @color-theme_2-hover;
|
||||
}
|
||||
&:active {
|
||||
background-color: @color-theme_2-active;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: @width;
|
||||
}
|
||||
.tips {
|
||||
padding: 8px 15px;
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
color: @color-theme_2-font;
|
||||
}
|
||||
|
||||
.no-item {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: @color-theme_2-font-label;
|
||||
}
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.listItem {
|
||||
&:hover {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
color: ~'@{color-@{value}-btn}';
|
||||
&:hover {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
&:active {
|
||||
background-color: ~'@{color-@{value}-theme_2-active}';
|
||||
}
|
||||
}
|
||||
.note {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
.no-item {
|
||||
p {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
@@ -1,368 +0,0 @@
|
||||
<template lang="pug">
|
||||
div(:class="$style.list")
|
||||
//- transition
|
||||
div.thead(:class="$style.thead")
|
||||
table
|
||||
thead
|
||||
tr
|
||||
th.nobreak.center(:class="$style.th" style="width: 5%;") #
|
||||
th.nobreak(:class="$style.th") {{$t('music_name')}}
|
||||
th.nobreak(:class="$style.th" style="width: 22%;") {{$t('music_singer')}}
|
||||
th.nobreak(:class="$style.th" style="width: 22%;") {{$t('music_album')}}
|
||||
th.nobreak(:class="$style.th" style="width: 9%;") {{$t('music_time')}}
|
||||
th.nobreak(:class="$style.th" style="width: 15%;") {{$t('action')}}
|
||||
div(v-show="list.length" :class="$style.content" ref="dom_listContent")
|
||||
base-virtualized-list(:list="list" key-name="songmid" ref="listRef" #default="{ item, index }" :item-height="listItemHeight"
|
||||
@scroll="saveListPosition" containerClass="scroll" contentClass="list" @contextmenu.capture="handleListRightClick")
|
||||
div.list-item(@click="handleListItemClick($event, index)"
|
||||
:class="[{ [$style.active]: playerInfo.isPlayList && playerInfo.playIndex === index }, { selected: selectedIndex == index || rightClickSelectedIndex == index }, { active: selectedList.includes(item) }, { [$style.disabled]: !assertApiSupport(item.source) }]"
|
||||
@contextmenu="handleListItemRightClick($event, index)")
|
||||
div.list-item-cell.nobreak.center(style="flex: 0 0 5%; padding-left: 3px; padding-right: 3px;" :class="$style.noSelect" @click.stop) {{index + 1}}
|
||||
div.list-item-cell.auto(:aria-label="item.name + (isShowSource ? ` - ${item.source}` : '')")
|
||||
span.select {{item.name}}
|
||||
span(:class="[$style.labelSource, $style.noSelect]" v-if="isShowSource") {{item.source}}
|
||||
div.list-item-cell(style="flex: 0 0 22%;" :aria-label="item.singer")
|
||||
span.select {{item.singer}}
|
||||
div.list-item-cell(style="flex: 0 0 22%;" :aria-label="item.albumName")
|
||||
span.select {{item.albumName}}
|
||||
div.list-item-cell(style="flex: 0 0 9%;")
|
||||
span(:class="[$style.time, $style.noSelect]") {{item.interval || '--/--'}}
|
||||
div.list-item-cell(style="flex: 0 0 15%; padding-left: 0; padding-right: 0;")
|
||||
material-list-buttons(:index="index" @btn-click="handleListBtnClick" :download-btn="assertApiSupport(item.source)")
|
||||
div(:class="$style.noItem" v-show="!list.length")
|
||||
p(v-text="$t('no_item')")
|
||||
//- material-flow-btn(:show="isShowEditBtn && assertApiSupport(source)" :remove-btn="false" @btn-click="handleFlowBtnClick")
|
||||
common-download-modal(v-model:show="isShowDownload" :musicInfo="selectedDownloadMusicInfo" teleport="#view")
|
||||
common-download-multiple-modal(v-model:show="isShowDownloadMultiple" :list="selectedList" @confirm="removeAllSelect" teleport="#view")
|
||||
common-list-add-modal(v-model:show="isShowListAdd" :is-move="isMove" :from-list-id="listId" :musicInfo="selectedAddMusicInfo" :exclude-list-id="excludeListIds" teleport="#view")
|
||||
common-list-add-multiple-modal(v-model:show="isShowListAddMultiple" :from-list-id="listId" :is-move="isMoveMultiple" :musicList="selectedList" @confirm="removeAllSelect" :exclude-list-id="excludeListIds" teleport="#view")
|
||||
search-list(:list="list" @action="handleMusicSearchAction" :visible="isShowSearchBar")
|
||||
music-sort-modal(v-model:show="isShowMusicSortModal" :music-info="selectedSortMusicInfo" :selected-num="selectedNum" @confirm="sortMusic")
|
||||
base-menu(:menus="menus" :location="menuLocation" item-name="name" :isShow="isShowItemMenu" @menu-click="handleMenuClick")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { clipboardWriteText, assertApiSupport } from '@renderer/utils'
|
||||
import { useCssModule } from '@renderer/utils/vueTools'
|
||||
import SearchList from '../SearchList'
|
||||
import MusicSortModal from '../MusicSortModal'
|
||||
import useListInfo from './useListInfo'
|
||||
import useList from './useList'
|
||||
import useMenu from './useMenu'
|
||||
import usePlay from './usePlay'
|
||||
import useMusicDownload from './useMusicDownload'
|
||||
import useMusicAdd from './useMusicAdd'
|
||||
import useSort from './useSort'
|
||||
import useMusicActions from './useMusicActions'
|
||||
import useSearch from './useSearch'
|
||||
import useListScroll from './useListScroll'
|
||||
export default {
|
||||
name: 'MusicList',
|
||||
props: {
|
||||
listId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['show-menu'],
|
||||
components: {
|
||||
SearchList,
|
||||
MusicSortModal,
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const styles = useCssModule()
|
||||
|
||||
const {
|
||||
rightClickSelectedIndex,
|
||||
selectedIndex,
|
||||
dom_listContent,
|
||||
listRef,
|
||||
list,
|
||||
playerInfo,
|
||||
setSelectedIndex,
|
||||
isShowSource,
|
||||
setting,
|
||||
excludeListIds,
|
||||
} = useListInfo({ props })
|
||||
|
||||
const {
|
||||
selectedList,
|
||||
listItemHeight,
|
||||
handleSelectData,
|
||||
removeAllSelect,
|
||||
} = useList({ list, setting })
|
||||
|
||||
const {
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
doubleClickPlay,
|
||||
} = usePlay({ props, selectedList, list, removeAllSelect })
|
||||
|
||||
const {
|
||||
isShowListAdd,
|
||||
isMove,
|
||||
isShowListAddMultiple,
|
||||
isMoveMultiple,
|
||||
selectedAddMusicInfo,
|
||||
handleShowMusicAddModal,
|
||||
handleShowMusicMoveModal,
|
||||
} = useMusicAdd({ selectedList, list })
|
||||
|
||||
const {
|
||||
isShowDownload,
|
||||
isShowDownloadMultiple,
|
||||
selectedDownloadMusicInfo,
|
||||
handleShowDownloadModal,
|
||||
} = useMusicDownload({ selectedList, list })
|
||||
|
||||
const {
|
||||
isShowMusicSortModal,
|
||||
selectedNum,
|
||||
selectedSortMusicInfo,
|
||||
handleShowSortModal,
|
||||
sortMusic,
|
||||
} = useSort({ props, list, selectedList, removeAllSelect })
|
||||
|
||||
const {
|
||||
handleSearch,
|
||||
handleOpenMusicDetail,
|
||||
handleCopyName,
|
||||
handleRemoveMusic,
|
||||
} = useMusicActions({ props, list, setting, removeAllSelect, selectedList })
|
||||
|
||||
const {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowItemMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
} = useMenu({
|
||||
listRef,
|
||||
assertApiSupport,
|
||||
emit,
|
||||
|
||||
handleShowDownloadModal,
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
handleSearch,
|
||||
handleShowMusicAddModal,
|
||||
handleShowMusicMoveModal,
|
||||
handleShowSortModal,
|
||||
handleOpenMusicDetail,
|
||||
handleCopyName,
|
||||
handleRemoveMusic,
|
||||
})
|
||||
|
||||
const {
|
||||
isShowSearchBar,
|
||||
searchList,
|
||||
handleMusicSearchAction,
|
||||
} = useSearch({
|
||||
setSelectedIndex,
|
||||
handlePlayMusic,
|
||||
listRef,
|
||||
})
|
||||
|
||||
const { saveListPosition, restoreScroll } = useListScroll({ props, listRef, list, setting })
|
||||
|
||||
const handleListItemClick = (event, index) => {
|
||||
if (rightClickSelectedIndex.value > -1) return
|
||||
handleSelectData(index)
|
||||
doubleClickPlay(index)
|
||||
}
|
||||
const handleListItemRightClick = (event, index) => {
|
||||
rightClickSelectedIndex.value = index
|
||||
showMenu(event, list.value[index], index)
|
||||
}
|
||||
const handleMenuClick = (action) => {
|
||||
let index = rightClickSelectedIndex.value
|
||||
rightClickSelectedIndex.value = -1
|
||||
menuClick(action, index)
|
||||
}
|
||||
const handleListRightClick = (event) => {
|
||||
if (!event.target.classList.contains('select')) return
|
||||
event.stopImmediatePropagation()
|
||||
let classList = dom_listContent.value.classList
|
||||
classList.add(styles.copying)
|
||||
window.requestAnimationFrame(() => {
|
||||
let str = window.getSelection().toString()
|
||||
classList.remove(styles.copying)
|
||||
str = str.split(/\n\n/).map(s => s.replace(/\n/g, ' ')).join('\n').trim()
|
||||
if (!str.length) return
|
||||
clipboardWriteText(str)
|
||||
})
|
||||
}
|
||||
const handleListBtnClick = ({ action, index }) => {
|
||||
switch (action) {
|
||||
case 'download':
|
||||
handleShowDownloadModal(index, true)
|
||||
break
|
||||
case 'play':
|
||||
handlePlayMusic(index, true)
|
||||
break
|
||||
case 'search':
|
||||
handleSearch(index)
|
||||
break
|
||||
case 'listAdd':
|
||||
handleShowMusicAddModal(index, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
const scrollToTop = () => {
|
||||
listRef.value.scrollTo(0, true)
|
||||
}
|
||||
|
||||
return {
|
||||
listItemHeight,
|
||||
handleListItemClick,
|
||||
selectedList,
|
||||
handleListItemRightClick,
|
||||
removeAllSelect,
|
||||
handleListBtnClick,
|
||||
rightClickSelectedIndex,
|
||||
selectedIndex,
|
||||
dom_listContent,
|
||||
listRef,
|
||||
excludeListIds,
|
||||
|
||||
menus,
|
||||
isShowItemMenu,
|
||||
menuLocation,
|
||||
handleMenuClick,
|
||||
|
||||
handleListRightClick,
|
||||
assertApiSupport,
|
||||
|
||||
isShowListAdd,
|
||||
isMove,
|
||||
isShowListAddMultiple,
|
||||
isMoveMultiple,
|
||||
selectedAddMusicInfo,
|
||||
|
||||
isShowMusicSortModal,
|
||||
selectedNum,
|
||||
selectedSortMusicInfo,
|
||||
sortMusic,
|
||||
|
||||
isShowDownload,
|
||||
isShowDownloadMultiple,
|
||||
selectedDownloadMusicInfo,
|
||||
|
||||
scrollToTop,
|
||||
|
||||
isShowSearchBar,
|
||||
searchList,
|
||||
handleMusicSearchAction,
|
||||
|
||||
list,
|
||||
playerInfo,
|
||||
|
||||
saveListPosition,
|
||||
restoreScroll,
|
||||
isShowSource,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.list {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.content {
|
||||
min-height: 0;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
flex: auto;
|
||||
&.copying {
|
||||
.no-select {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.thead {
|
||||
flex: none;
|
||||
}
|
||||
.th:first-child {
|
||||
color: @color-theme_2-font-label;
|
||||
// padding-left: 10px;
|
||||
}
|
||||
|
||||
:global(.list) {
|
||||
flex: auto;
|
||||
:global(.list-item) {
|
||||
&.active {
|
||||
color: @color-btn;
|
||||
}
|
||||
}
|
||||
:global(.list-item-cell) {
|
||||
font-size: 12px !important;
|
||||
|
||||
&:first-child {
|
||||
// padding-left: 10px;
|
||||
font-size: 11px !important;
|
||||
color: @color-theme_2-font-label !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.labelSource {
|
||||
color: @color-theme;
|
||||
padding: 5px;
|
||||
font-size: .8em;
|
||||
line-height: 1;
|
||||
opacity: .75;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
.no-item {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: @color-theme_2-font-label;
|
||||
}
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.th:first-child {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}' !important;
|
||||
}
|
||||
:global(.list) {
|
||||
:global(.list-item) {
|
||||
&.active {
|
||||
color: ~'@{color-@{value}-btn}';
|
||||
}
|
||||
}
|
||||
}
|
||||
.labelSource {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
.no-item {
|
||||
p {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
@@ -1,677 +0,0 @@
|
||||
<template>
|
||||
<div :class="$style.lists" ref="dom_lists">
|
||||
<div :class="$style.listHeader">
|
||||
<h2 :class="$style.listsTitle">{{$t('my_list')}}</h2>
|
||||
<div :class="$style.headerBtns">
|
||||
<button :class="$style.listsAdd" @click="handleShowNewList" :aria-label="$t('lists__new_list_btn')">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="70%" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-list-add"></use>
|
||||
</svg>
|
||||
</button>
|
||||
<button :class="$style.listsAdd" @click="isShowListUpdateModal = true" :aria-label="$t('list_update_modal__title')">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" style="transform: rotate(45deg);" height="70%" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-refresh"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="scroll" :class="[$style.listsContent, { [$style.sortable]: keyEvent.isModDown }]" ref="dom_lists_list">
|
||||
<li class="default-list" :class="[$style.listsItem, {[$style.active]: defaultList.id == listId}]" :aria-label="defaultList.name" :aria-selected="defaultList.id == listId"
|
||||
@contextmenu="handleListsItemRigthClick($event, -2)" @click="handleListToggle(defaultList.id)"
|
||||
>
|
||||
<span :class="$style.listsLabel">{{defaultList.name}}</span>
|
||||
</li>
|
||||
<li class="default-list" :class="[$style.listsItem, {[$style.active]: loveList.id == listId}]" :aria-label="loveList.name" :aria-selected="loveList.id == listId"
|
||||
@contextmenu="handleListsItemRigthClick($event, -1)" @click="handleListToggle(loveList.id)">
|
||||
<span :class="$style.listsLabel">{{loveList.name}}</span>
|
||||
</li>
|
||||
<li class="user-list"
|
||||
:class="[$style.listsItem, {[$style.active]: item.id == listId}, {[$style.clicked]: listsData.rightClickItemIndex == index}, {[$style.fetching]: fetchingListStatus[item.id]}]" :data-index="index"
|
||||
@contextmenu="handleListsItemRigthClick($event, index)" :aria-label="item.name" v-for="(item, index) in userLists" :key="item.id" :aria-selected="defaultList.id == listId"
|
||||
>
|
||||
<span :class="$style.listsLabel" @click="handleListToggle(item.id, index + 2)">{{item.name}}</span>
|
||||
<input :class="$style.listsInput" @contextmenu.stop type="text"
|
||||
@keyup.enter="handleListsSave(index, $event)" @blur="handleListsSave(index, $event)" :value="item.name" :placeholder="item.name"/>
|
||||
</li>
|
||||
<transition enter-active-class="animated-fast slideInLeft" leave-active-class="animated-fast fadeOut" @after-leave="handleListsNewAfterLeave" @after-enter="$refs.dom_listsNewInput.focus()">
|
||||
<li :class="[$style.listsItem, $style.listsNew, listsData.isNewLeave ? $style.newLeave : null]" v-if="listsData.isShowNewList">
|
||||
<input :class="$style.listsInput" @contextmenu.stop ref="dom_listsNewInput" type="text" @keyup.enter="handleListsCreate"
|
||||
@blur="handleListsCreate" :placeholder="$t('lists__new_list_input')"/>
|
||||
</li>
|
||||
</transition>
|
||||
</ul>
|
||||
<base-menu :menus="listsItemMenu" :location="listsData.menuLocation" item-name="name" :isShow="listsData.isShowItemMenu" @menu-click="handleListsItemMenuClick" />
|
||||
<DuplicateMusicModal v-model:visible="isShowDuplicateMusicModal" :list-info="selectedDuplicateListInfo" />
|
||||
<ListSortModal v-model:visible="isShowListSortModal" :list-info="selectedSortListInfo" />
|
||||
<ListUpdateModal v-model:visible="isShowListUpdateModal" :list-update-times="listUpdateTimes" @update-list="handleSyncSourceList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapMutations } from 'vuex'
|
||||
import { openSaveDir, saveLxConfigFile, selectDir, readLxConfigFile, filterFileName, openUrl, dateFormat } from '@renderer/utils'
|
||||
import musicSdk from '@renderer/utils/music'
|
||||
import DuplicateMusicModal from './DuplicateMusicModal'
|
||||
import ListSortModal from './ListSortModal'
|
||||
import ListUpdateModal from './ListUpdateModal'
|
||||
import { defaultList, loveList, userLists, fetchingListStatus } from '@renderer/core/share/list'
|
||||
import { ref, computed, useCommit, useCssModule } from '@renderer/utils/vueTools'
|
||||
import { getList } from '@renderer/core/share/utils'
|
||||
import useDarg from '@renderer/utils/compositions/useDrag'
|
||||
import { getListUpdateInfo } from '@renderer/utils/data'
|
||||
import useSyncSourceList from '@renderer/utils/compositions/useSyncSourceList'
|
||||
import useImportTip from '@renderer/utils/compositions/useImportTip'
|
||||
|
||||
export default {
|
||||
name: 'MyLists',
|
||||
props: {
|
||||
listId: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
DuplicateMusicModal,
|
||||
ListSortModal,
|
||||
ListUpdateModal,
|
||||
},
|
||||
setup() {
|
||||
const dom_lists_list = ref(null)
|
||||
const lists = computed(() => [defaultList, loveList, ...userLists])
|
||||
const setUserListPosition = useCommit('list', 'setUserListPosition')
|
||||
const showImportTip = useImportTip()
|
||||
|
||||
const syncSourceList = useSyncSourceList()
|
||||
|
||||
const styles = useCssModule()
|
||||
const { setDisabled } = useDarg({
|
||||
dom_list: dom_lists_list,
|
||||
dragingItemClassName: styles.dragingItem,
|
||||
filter: 'default-list',
|
||||
onUpdate(newIndex, oldIndex) {
|
||||
setUserListPosition({ id: lists.value[oldIndex].id, position: newIndex - 2 })
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
defaultList,
|
||||
loveList,
|
||||
userLists,
|
||||
lists,
|
||||
fetchingListStatus,
|
||||
dom_lists_list,
|
||||
setDisabledSort: setDisabled,
|
||||
syncSourceList,
|
||||
showImportTip,
|
||||
}
|
||||
},
|
||||
emits: ['show-menu'],
|
||||
data() {
|
||||
return {
|
||||
isShowDuplicateMusicModal: false,
|
||||
isShowListSortModal: false,
|
||||
isShowListUpdateModal: false,
|
||||
listsData: {
|
||||
isShowItemMenu: false,
|
||||
itemMenuControl: {
|
||||
rename: true,
|
||||
duplicate: true,
|
||||
sort: true,
|
||||
sourceDetail: true,
|
||||
import: true,
|
||||
export: true,
|
||||
sync: false,
|
||||
remove: true,
|
||||
},
|
||||
rightClickItemIndex: -1,
|
||||
menuLocation: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
isShowNewList: false,
|
||||
isNewLeave: false,
|
||||
},
|
||||
selectedDuplicateListInfo: {},
|
||||
selectedSortListInfo: {},
|
||||
listUpdateTimes: {},
|
||||
keyEvent: {
|
||||
isModDown: false,
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
listsItemMenu() {
|
||||
return [
|
||||
{
|
||||
name: this.$t('lists__rename'),
|
||||
action: 'rename',
|
||||
disabled: !this.listsData.itemMenuControl.rename,
|
||||
},
|
||||
{
|
||||
name: this.$t('lists__sync'),
|
||||
action: 'sync',
|
||||
disabled: !this.listsData.itemMenuControl.sync,
|
||||
},
|
||||
{
|
||||
name: this.$t('lists__sort_list'),
|
||||
action: 'sort',
|
||||
disabled: !this.listsData.itemMenuControl.sort,
|
||||
},
|
||||
{
|
||||
name: this.$t('lists__duplicate'),
|
||||
action: 'duplicate',
|
||||
disabled: !this.listsData.itemMenuControl.duplicate,
|
||||
},
|
||||
{
|
||||
name: this.$t('lists__source_detail'),
|
||||
action: 'sourceDetail',
|
||||
disabled: !this.listsData.itemMenuControl.sourceDetail,
|
||||
},
|
||||
{
|
||||
name: this.$t('lists__import'),
|
||||
action: 'import',
|
||||
disabled: !this.listsData.itemMenuControl.export,
|
||||
},
|
||||
{
|
||||
name: this.$t('lists__export'),
|
||||
action: 'export',
|
||||
disabled: !this.listsData.itemMenuControl.export,
|
||||
},
|
||||
{
|
||||
name: this.$t('lists__remove'),
|
||||
action: 'remove',
|
||||
disabled: !this.listsData.itemMenuControl.remove,
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
listId(id) {
|
||||
this.setPrevSelectListId(id)
|
||||
},
|
||||
lists(lists) {
|
||||
if (lists.some(l => l.id == this.listId)) return
|
||||
this.$router.replace({
|
||||
path: 'list',
|
||||
query: {
|
||||
id: this.defaultList.id,
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.setPrevSelectListId(this.listId)
|
||||
},
|
||||
mounted() {
|
||||
this.setListsScroll()
|
||||
window.eventHub.on('key_mod_down', this.handle_key_mod_down)
|
||||
window.eventHub.on('key_mod_up', this.handle_key_mod_up)
|
||||
for (const [id, value] of Object.entries(getListUpdateInfo())) {
|
||||
this.listUpdateTimes[id] = value.updateTime ? dateFormat(value.updateTime) : ''
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.eventHub.off('key_mod_down', this.handle_key_mod_down)
|
||||
window.eventHub.off('key_mod_up', this.handle_key_mod_up)
|
||||
},
|
||||
methods: {
|
||||
...mapMutations('list', [
|
||||
'setUserListName',
|
||||
'createUserList',
|
||||
'removeUserList',
|
||||
'setPrevSelectListId',
|
||||
'setList',
|
||||
]),
|
||||
handle_key_mod_down(event) {
|
||||
if (!this.keyEvent.isModDown) {
|
||||
// console.log(event)
|
||||
switch (event.event.target.tagName) {
|
||||
case 'INPUT':
|
||||
case 'SELECT':
|
||||
case 'TEXTAREA':
|
||||
return
|
||||
default: if (event.event.target.isContentEditable) return
|
||||
}
|
||||
|
||||
this.keyEvent.isModDown = true
|
||||
this.setDisabledSort(false)
|
||||
const dom_target = this.dom_lists_list.querySelector('.' + this.$style.editing)
|
||||
if (dom_target) this.handleListsSave(dom_target.dataset.index)
|
||||
}
|
||||
if (this.listsData.isShowItemMenu) this.hideListsMenu()
|
||||
},
|
||||
handle_key_mod_up() {
|
||||
if (this.keyEvent.isModDown) {
|
||||
this.keyEvent.isModDown = false
|
||||
this.setDisabledSort(true)
|
||||
}
|
||||
},
|
||||
setListsScroll() {
|
||||
let target = this.dom_lists_list.querySelector('.' + this.$style.active)
|
||||
if (!target) return
|
||||
let offsetTop = target.offsetTop
|
||||
let location = offsetTop - 150
|
||||
if (location > 0) this.dom_lists_list.scrollTop = location
|
||||
},
|
||||
handleListsSave(index) {
|
||||
let dom_target = this.dom_lists_list.querySelector('.' + this.$style.editing)
|
||||
if (!dom_target) return
|
||||
dom_target.classList.remove(this.$style.editing)
|
||||
const dom_input = dom_target.querySelector('.' + this.$style.listsInput)
|
||||
let name = dom_input.value.trim()
|
||||
const targetList = userLists[index]
|
||||
if (name.length) return this.setUserListName({ id: targetList.id, name })
|
||||
dom_input.value = targetList.name
|
||||
},
|
||||
handleListsCreate(event) {
|
||||
if (event.target.readonly) return
|
||||
let name = event.target.value.trim()
|
||||
event.target.readonly = true
|
||||
|
||||
if (name == '') {
|
||||
this.listsData.isShowNewList = false
|
||||
return
|
||||
}
|
||||
|
||||
this.listsData.isNewLeave = true
|
||||
this.$nextTick(() => {
|
||||
this.listsData.isShowNewList = false
|
||||
})
|
||||
|
||||
this.createUserList({ name })
|
||||
},
|
||||
handleShowNewList() {
|
||||
this.listsData.isShowNewList = true
|
||||
},
|
||||
handleListsNewAfterLeave() {
|
||||
this.listsData.isNewLeave = false
|
||||
},
|
||||
handleListToggle(id) {
|
||||
if (id == this.listId) return
|
||||
this.$router.replace({
|
||||
path: 'list',
|
||||
query: { id },
|
||||
}).catch(_ => _)
|
||||
},
|
||||
handleListsItemRigthClick(event, index) {
|
||||
let source
|
||||
switch (index) {
|
||||
case -1:
|
||||
case -2:
|
||||
this.listsData.itemMenuControl.rename = false
|
||||
this.listsData.itemMenuControl.remove = false
|
||||
this.listsData.itemMenuControl.sync = false
|
||||
break
|
||||
default:
|
||||
this.listsData.itemMenuControl.rename = true
|
||||
this.listsData.itemMenuControl.remove = true
|
||||
source = userLists[index].source
|
||||
this.listsData.itemMenuControl.sync = !!source && !!musicSdk[source]?.songList
|
||||
break
|
||||
}
|
||||
this.listsData.itemMenuControl.sort = !!getList(this.getTargetListInfo(index)?.id).length
|
||||
this.listsData.itemMenuControl.sourceDetail = this.assertSupportDetail(source, index)
|
||||
this.listsData.rightClickItemIndex = index
|
||||
this.listsData.menuLocation.x = event.currentTarget.offsetLeft + event.offsetX
|
||||
this.listsData.menuLocation.y = event.currentTarget.offsetTop + event.offsetY - this.dom_lists_list.scrollTop
|
||||
this.$emit('show-menu')
|
||||
this.$nextTick(() => {
|
||||
this.listsData.isShowItemMenu = true
|
||||
})
|
||||
},
|
||||
hideListsMenu() {
|
||||
this.listsData.isShowItemMenu = false
|
||||
this.listsData.rightClickItemIndex = -1
|
||||
},
|
||||
handleListsItemMenuClick(action) {
|
||||
// console.log(action)
|
||||
let index = this.listsData.rightClickItemIndex
|
||||
this.hideListsMenu()
|
||||
this.listsData.isShowItemMenu = false
|
||||
let dom
|
||||
switch (action && action.action) {
|
||||
case 'rename':
|
||||
dom = this.dom_lists_list.querySelectorAll('.user-list')[index]
|
||||
this.$nextTick(() => {
|
||||
dom.classList.add(this.$style.editing)
|
||||
dom.querySelector('input').focus()
|
||||
})
|
||||
break
|
||||
case 'duplicate':
|
||||
this.selectedDuplicateListInfo = this.getTargetListInfo(index)
|
||||
this.isShowDuplicateMusicModal = true
|
||||
break
|
||||
case 'sort':
|
||||
this.selectedSortListInfo = this.getTargetListInfo(index)
|
||||
this.isShowListSortModal = true
|
||||
break
|
||||
case 'sourceDetail':
|
||||
this.openSourceDetailPage(index)
|
||||
break
|
||||
case 'import':
|
||||
this.handleImportList(index)
|
||||
break
|
||||
case 'export':
|
||||
this.handleExportList(index)
|
||||
break
|
||||
case 'sync':
|
||||
this.$dialog.confirm({
|
||||
message: this.$t('lists__sync_confirm_tip', { name: userLists[index].name }),
|
||||
confirmButtonText: this.$t('lists__remove_tip_button'),
|
||||
}).then(isSync => {
|
||||
if (!isSync) return
|
||||
this.handleSyncSourceList(userLists[index])
|
||||
})
|
||||
break
|
||||
case 'remove':
|
||||
this.$dialog.confirm({
|
||||
message: this.$t('lists__remove_tip', { name: userLists[index].name }),
|
||||
confirmButtonText: this.$t('lists__remove_tip_button'),
|
||||
}).then(isRemove => {
|
||||
if (!isRemove) return
|
||||
this.removeUserList({ id: userLists[index].id })
|
||||
})
|
||||
break
|
||||
}
|
||||
},
|
||||
async handleSyncSourceList(targetListInfo) {
|
||||
await this.syncSourceList(targetListInfo)
|
||||
// console.log(targetListInfo.list.length, list.length)
|
||||
this.listUpdateTimes[targetListInfo.id] = dateFormat(Date.now())
|
||||
},
|
||||
getTargetListInfo(index) {
|
||||
let list
|
||||
switch (index) {
|
||||
case -2:
|
||||
list = { ...this.defaultList, list: getList(this.defaultList.id) }
|
||||
break
|
||||
case -1:
|
||||
list = { ...this.loveList, list: getList(this.loveList.id) }
|
||||
break
|
||||
default:
|
||||
list = this.userLists[index]
|
||||
if (!list) return null
|
||||
list = { ...list, list: getList(list.id) }
|
||||
break
|
||||
}
|
||||
return list
|
||||
},
|
||||
assertSupportDetail(source, index) {
|
||||
if (source) {
|
||||
const { sourceListId } = this.userLists[index]
|
||||
if (sourceListId) {
|
||||
if (/board__/.test(sourceListId)) {
|
||||
// const id = sourceListId.replace(/board__/, '')
|
||||
return !!musicSdk[source]?.leaderboard?.getDetailPageUrl
|
||||
} else {
|
||||
return !!musicSdk[source]?.songList?.getDetailPageUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
async openSourceDetailPage(index) {
|
||||
const { source, sourceListId } = this.userLists[index]
|
||||
if (!sourceListId) return
|
||||
let url
|
||||
if (/board__/.test(sourceListId)) {
|
||||
const id = sourceListId.replace(/board__/, '')
|
||||
url = musicSdk[source].leaderboard.getDetailPageUrl(id)
|
||||
} else if (musicSdk[source].songList?.getDetailPageUrl) {
|
||||
url = await musicSdk[source].songList.getDetailPageUrl(sourceListId)
|
||||
}
|
||||
if (!url) return
|
||||
openUrl(url)
|
||||
},
|
||||
handleExportList(index) {
|
||||
const list = this.getTargetListInfo(index)
|
||||
if (!list) return
|
||||
openSaveDir({
|
||||
title: this.$t('lists__export_part_desc'),
|
||||
defaultPath: `lx_list_part_${filterFileName(list.name)}.lxmc`,
|
||||
}).then(async result => {
|
||||
if (result.canceled) return
|
||||
const data = JSON.parse(JSON.stringify({
|
||||
type: 'playListPart',
|
||||
data: list,
|
||||
}))
|
||||
for await (const item of data.data.list) {
|
||||
if (item.otherSource) delete item.otherSource
|
||||
if (item.lrc) delete item.lrc
|
||||
}
|
||||
saveLxConfigFile(result.filePath, data)
|
||||
})
|
||||
},
|
||||
handleImportList(index) {
|
||||
selectDir({
|
||||
title: this.$t('lists__import_part_desc'),
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'Play List Part', extensions: ['json', 'lxmc'] },
|
||||
{ name: 'All Files', extensions: ['*'] },
|
||||
],
|
||||
}).then(async result => {
|
||||
if (result.canceled) return
|
||||
let listData
|
||||
try {
|
||||
listData = await readLxConfigFile(result.filePaths[0])
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
if (listData.type !== 'playListPart') return this.showImportTip(listData.type)
|
||||
const targetList = this.lists.find(l => l.id == listData.data.id)
|
||||
if (targetList) {
|
||||
const confirm = await this.$dialog.confirm({
|
||||
message: this.$t('lists__import_part_confirm', { importName: listData.data.name, localName: targetList.name }),
|
||||
cancelButtonText: this.$t('lists__import_part_button_cancel'),
|
||||
confirmButtonText: this.$t('lists__import_part_button_confirm'),
|
||||
})
|
||||
if (confirm) {
|
||||
listData.data.name = targetList.name
|
||||
this.setList({
|
||||
name: listData.data.name,
|
||||
id: listData.data.id,
|
||||
list: listData.data.list,
|
||||
source: listData.data.source,
|
||||
sourceListId: listData.data.sourceListId,
|
||||
})
|
||||
return
|
||||
}
|
||||
listData.data.id += `__${Date.now()}`
|
||||
}
|
||||
this.createUserList({
|
||||
name: listData.data.name,
|
||||
id: listData.data.id,
|
||||
list: listData.data.list,
|
||||
source: listData.data.source,
|
||||
sourceListId: listData.data.sourceListId,
|
||||
position: Math.max(index, -1),
|
||||
})
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
@lists-item-height: 36px;
|
||||
.lists {
|
||||
flex: none;
|
||||
width: 16%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.listHeader {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
&:hover {
|
||||
.listsAdd {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsTitle {
|
||||
flex: auto;
|
||||
font-size: 12px;
|
||||
line-height: 38px;
|
||||
padding: 0 10px;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.headerBtns {
|
||||
flex: none;
|
||||
display: flex;
|
||||
}
|
||||
.listsAdd {
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
margin-top: 6px;
|
||||
background: none;
|
||||
height: 30px;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: @radius-border;
|
||||
cursor: pointer;
|
||||
opacity: .1;
|
||||
transition: opacity @transition-theme;
|
||||
color: @color-btn;
|
||||
svg {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
&:active {
|
||||
opacity: .7 !important;
|
||||
}
|
||||
&:hover {
|
||||
opacity: .6 !important;
|
||||
}
|
||||
}
|
||||
.listsContent {
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
overflow-y: scroll !important;
|
||||
// border-right: 1px solid rgba(0, 0, 0, 0.12);
|
||||
|
||||
&.sortable {
|
||||
* {
|
||||
-webkit-user-drag: element;
|
||||
}
|
||||
|
||||
.listsItem {
|
||||
&:hover, &.active, &.selected, &.clicked {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
&.dragingItem {
|
||||
background-color: @color-theme_2-hover !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsItem {
|
||||
position: relative;
|
||||
transition: .3s ease;
|
||||
transition-property: color, background-color, opacity;
|
||||
background-color: transparent;
|
||||
&:hover:not(.active) {
|
||||
background-color: @color-theme_2-hover;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.active {
|
||||
// background-color:
|
||||
color: @color-theme;
|
||||
}
|
||||
&.selected {
|
||||
background-color: @color-theme_2-active;
|
||||
}
|
||||
&.clicked {
|
||||
background-color: @color-theme_2-hover;
|
||||
}
|
||||
&.fetching {
|
||||
opacity: .5;
|
||||
}
|
||||
&.editing {
|
||||
padding: 0 10px;
|
||||
background-color: @color-theme_2-hover;
|
||||
.listsLabel {
|
||||
display: none;
|
||||
}
|
||||
.listsInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsLabel {
|
||||
display: block;
|
||||
height: @lists-item-height;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
line-height: @lists-item-height;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.listsInput {
|
||||
width: 100%;
|
||||
height: @lists-item-height;
|
||||
border: none;
|
||||
padding: 0;
|
||||
// padding-bottom: 1px;
|
||||
line-height: @lists-item-height;
|
||||
background: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.listsNew {
|
||||
padding: 0 10px;
|
||||
background-color: @color-theme_2-hover;
|
||||
.listsInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.newLeave {
|
||||
margin-top: -@lists-item-height;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.listsAdd {
|
||||
color: ~'@{color-@{value}-btn}';
|
||||
}
|
||||
.listsItem {
|
||||
&:hover:not(.active) {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
&.active {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
&.select {
|
||||
background-color: ~'@{color-@{value}-theme_2-active}';
|
||||
}
|
||||
&.clicked {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
&.editing {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
}
|
||||
.listsContent {
|
||||
&.sortable {
|
||||
.listsItem {
|
||||
&.dragingItem {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}' !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsNew {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
@@ -1,329 +0,0 @@
|
||||
<template>
|
||||
<div :class="$style.main">
|
||||
<!-- <div class="scroll" :class="$style.toc">
|
||||
<ul :class="$style.tocList">
|
||||
<li :class="$style.tocListItem" v-for="h2 in toc.list" :key="h2.id">
|
||||
<h2 :class="[$style.tocH2, toc.activeId == h2.id ? $style.active : null]" :aria-label="h2.title">
|
||||
<a :href="'#' + h2.id" @click.stop="toc.activeId = h2.id">{{h2.title}}</a>
|
||||
</h2>
|
||||
<ul :class="$style.tocList" v-if="h2.children.length">
|
||||
<li :class="$style.tocSubListItem" v-for="h3 in h2.children" :key="h3.id">
|
||||
<h3 :class="[$style.tocH3, toc.activeId == h3.id ? $style.active : null]" :aria-label="h3.title">
|
||||
<a :href="'#' + h3.id" @click.stop="toc.activeId = h3.id">{{h3.title}}</a>
|
||||
</h3>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div> -->
|
||||
<div class="scroll" :class="$style.setting" ref="dom_setting">
|
||||
<dl ref="dom_setting_list">
|
||||
<SettingBasic />
|
||||
<SettingPlay />
|
||||
<SettingPlayDetail />
|
||||
<SettingDesktopLyric />
|
||||
<SettingSearch />
|
||||
<SettingList />
|
||||
<SettingDownload />
|
||||
<SettingSync />
|
||||
<SettingHotKey />
|
||||
<SettingNetwork />
|
||||
<SettingOdc />
|
||||
<SettingBackup />
|
||||
<SettingOther />
|
||||
<SettingUpdate />
|
||||
<SettingAbout />
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useRefGetter, watch, useCommit, toRaw } from '@renderer/utils/vueTools'
|
||||
import { currentStting } from './setting'
|
||||
|
||||
import SettingBasic from './components/SettingBasic'
|
||||
import SettingPlay from './components/SettingPlay'
|
||||
import SettingPlayDetail from './components/SettingPlayDetail'
|
||||
import SettingDesktopLyric from './components/SettingDesktopLyric'
|
||||
import SettingSearch from './components/SettingSearch'
|
||||
import SettingList from './components/SettingList'
|
||||
import SettingDownload from './components/SettingDownload'
|
||||
import SettingSync from './components/SettingSync'
|
||||
import SettingHotKey from './components/SettingHotKey'
|
||||
import SettingNetwork from './components/SettingNetwork'
|
||||
import SettingOdc from './components/SettingOdc'
|
||||
import SettingBackup from './components/SettingBackup'
|
||||
import SettingOther from './components/SettingOther'
|
||||
import SettingUpdate from './components/SettingUpdate'
|
||||
import SettingAbout from './components/SettingAbout'
|
||||
|
||||
export default {
|
||||
name: 'Setting',
|
||||
components: {
|
||||
SettingBasic,
|
||||
SettingPlay,
|
||||
SettingPlayDetail,
|
||||
SettingDesktopLyric,
|
||||
SettingSearch,
|
||||
SettingList,
|
||||
SettingDownload,
|
||||
SettingSync,
|
||||
SettingHotKey,
|
||||
SettingNetwork,
|
||||
SettingOdc,
|
||||
SettingBackup,
|
||||
SettingOther,
|
||||
SettingUpdate,
|
||||
SettingAbout,
|
||||
},
|
||||
setup() {
|
||||
const setting = useRefGetter('setting')
|
||||
|
||||
const setSetting = useCommit('setSetting')
|
||||
currentStting.value = JSON.parse(JSON.stringify(toRaw(setting.value)))
|
||||
watch(currentStting, newSetting => {
|
||||
const newSettingStr = JSON.stringify(newSetting)
|
||||
if (newSettingStr === JSON.stringify(setting.value)) return
|
||||
// console.log(newSetting)
|
||||
setSetting(JSON.parse(newSettingStr))
|
||||
}, {
|
||||
deep: true,
|
||||
})
|
||||
|
||||
watch(() => setting.value.isAgreePact, val => {
|
||||
currentStting.value.isAgreePact = val
|
||||
})
|
||||
watch(() => setting.value.player.mediaDeviceId, val => {
|
||||
currentStting.value.player.mediaDeviceId = val
|
||||
})
|
||||
watch(() => setting.value.playDetail, val => {
|
||||
currentStting.value.playDetail = JSON.parse(JSON.stringify(val))
|
||||
}, { deep: true })
|
||||
watch(() => setting.value.player.isMute, val => {
|
||||
currentStting.value.player.isMute = val
|
||||
})
|
||||
watch(() => setting.value.apiSource, val => {
|
||||
currentStting.value.apiSource = val
|
||||
})
|
||||
watch(() => setting.value.desktopLyric.enable, val => {
|
||||
currentStting.value.desktopLyric.enable = val
|
||||
})
|
||||
watch(() => setting.value.desktopLyric.isLock, val => {
|
||||
currentStting.value.desktopLyric.isLock = val
|
||||
})
|
||||
watch(() => setting.value.player.togglePlayMethod, val => {
|
||||
currentStting.value.player.togglePlayMethod = val
|
||||
})
|
||||
watch(() => setting.value.player.audioVisualization, val => {
|
||||
currentStting.value.player.audioVisualization = val
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
toc: {
|
||||
list: [],
|
||||
activeId: '',
|
||||
},
|
||||
}
|
||||
},
|
||||
// mounted() {
|
||||
// this.initTOC()
|
||||
// },
|
||||
// methods: {
|
||||
// initTOC() {
|
||||
// const list = this.$refs.dom_setting_list.children
|
||||
// const toc = []
|
||||
// let prevTitle
|
||||
// for (const item of list) {
|
||||
// if (item.tagName == 'DT') {
|
||||
// prevTitle = {
|
||||
// title: item.innerText.replace(/[((].+?[))]/, ''),
|
||||
// id: item.getAttribute('id'),
|
||||
// dom: item,
|
||||
// children: [],
|
||||
// }
|
||||
// toc.push(prevTitle)
|
||||
// continue
|
||||
// }
|
||||
// const h3 = item.querySelector('h3')
|
||||
// if (h3) {
|
||||
// prevTitle.children.push({
|
||||
// title: h3.innerText.replace(/[((].+?[))]/, ''),
|
||||
// id: h3.getAttribute('id'),
|
||||
// dom: h3,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// console.log(toc)
|
||||
// this.toc.list = toc
|
||||
// },
|
||||
// handleListScroll(event) {
|
||||
// // console.log(event.target.scrollTop)
|
||||
// },
|
||||
// },
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
height: 100%;
|
||||
// border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
// .toc {
|
||||
// flex: 0 0 15%;
|
||||
// overflow-y: scroll;
|
||||
// padding: 10px;
|
||||
// a {
|
||||
// text-decoration: none;
|
||||
// display: block;
|
||||
// line-height: 1.5;
|
||||
// .mixin-ellipsis-1;
|
||||
// }
|
||||
// }
|
||||
// .tocH2 {
|
||||
// font-size: 14px;
|
||||
// a {
|
||||
// color: @color-theme;
|
||||
// }
|
||||
// &.active {
|
||||
// a {
|
||||
// color: @color-theme;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .tocH3 {
|
||||
// font-size: 13px;
|
||||
// opacity: .8;
|
||||
// }
|
||||
|
||||
// .tocList {
|
||||
// .tocList {
|
||||
// padding-left: 15px;
|
||||
// }
|
||||
// }
|
||||
// .tocListItem {
|
||||
// +.tocListItem {
|
||||
// padding-top: 10px;
|
||||
// }
|
||||
// }
|
||||
// .tocSubListItem {
|
||||
// padding-top: 10px;
|
||||
// }
|
||||
|
||||
.setting {
|
||||
padding: 0 15px 30px 15px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
:global {
|
||||
dt {
|
||||
border-left: 5px solid @color-theme;
|
||||
padding: 3px 7px;
|
||||
margin: 30px 0 15px;
|
||||
|
||||
+ dd h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-left: 15px;
|
||||
// font-size: 13px;
|
||||
> div {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
}
|
||||
h3 {
|
||||
font-size: 12px;
|
||||
margin: 25px 0 15px;
|
||||
}
|
||||
p {
|
||||
padding: 3px 0;
|
||||
line-height: 1.3;
|
||||
.btn {
|
||||
+ .btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.help-btn {
|
||||
padding: 0;
|
||||
margin: 0 0.4em;
|
||||
border: none;
|
||||
background: none;
|
||||
color: @color-theme;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
.help-icon {
|
||||
margin: 0 0.4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-content {
|
||||
display: inline-block;
|
||||
transition: @transition-theme;
|
||||
transition-property: opacity, transform;
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
|
||||
&.hide {
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// :global(dt):target, :global(h3):target {
|
||||
// animation: highlight 1s ease;
|
||||
// }
|
||||
|
||||
// @keyframes highlight {
|
||||
// from { background: yellow; }
|
||||
// to { background: transparent; }
|
||||
// }
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.tocH2 {
|
||||
a {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
}
|
||||
.tbody {
|
||||
tr {
|
||||
&.active {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
}
|
||||
}
|
||||
.setting {
|
||||
dt {
|
||||
border-left-color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
|
||||
:global {
|
||||
.help-btn {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</style>
|
||||
@@ -7,7 +7,7 @@ material-modal(:show="modelValue" bg-close @close="handleCloseModal" @after-ente
|
||||
base-input(:class="$style.input" ref="dom_input" v-model="time" type="number")
|
||||
p(:class="$style.inputLabel") {{$t('play_timeout_unit')}}
|
||||
div(:class="$style.row")
|
||||
base-checkbox(id="play_timeout_end" v-model="currentStting.player.waitPlayEndStop" :label="$t('play_timeout_end')")
|
||||
base-checkbox(id="play_timeout_end" :modelValue="appSetting['player.waitPlayEndStop']" @update:modelValue="updateSetting({'player.waitPlayEndStop': $event})" :label="$t('play_timeout_end')")
|
||||
div(:class="[$style.row, $style.tip, { [$style.show]: !!timeLabel }]")
|
||||
p {{$t('play_timeout_tip', { time: timeLabel })}}
|
||||
div(:class="$style.footer")
|
||||
@@ -16,9 +16,9 @@ material-modal(:show="modelValue" bg-close @close="handleCloseModal" @after-ente
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { currentStting } from '../setting'
|
||||
import { useTimeout, startTimeoutStop, stopTimeoutStop } from '@renderer/utils/timeoutStop'
|
||||
import { ref } from '@renderer/utils/vueTools'
|
||||
import { useTimeout, startTimeoutStop, stopTimeoutStop } from '@renderer/core/player/timeoutStop'
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
const MAX_MIN = 1440
|
||||
|
||||
@@ -31,9 +31,10 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const { timeLabel } = useTimeout()
|
||||
const time = ref(currentStting.value.player.waitPlayEndStopTime)
|
||||
const time = ref(appSetting['player.waitPlayEndStopTime'])
|
||||
|
||||
const handleCloseModal = () => {
|
||||
emit('update:modelValue', false)
|
||||
@@ -62,12 +63,13 @@ export default {
|
||||
const handleConfirm = () => {
|
||||
let time = verify()
|
||||
if (time == '') return
|
||||
currentStting.value.player.waitPlayEndStopTime = time
|
||||
if (appSetting['player.waitPlayEndStopTime'] != time) updateSetting({ 'player.waitPlayEndStopTime': time })
|
||||
startTimeoutStop(time * 60)
|
||||
handleCloseModal()
|
||||
}
|
||||
return {
|
||||
currentStting,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
timeLabel,
|
||||
time,
|
||||
handleCloseModal,
|
||||
@@ -94,7 +96,7 @@ export default {
|
||||
// overflow: hidden;
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
color: @color-theme_2-font;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -144,32 +146,4 @@ export default {
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.main {
|
||||
h2 {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
}
|
||||
.listItem {
|
||||
&:hover {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
&.active {
|
||||
background-color: ~'@{color-@{value}-theme_2-active}';
|
||||
}
|
||||
h3 {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
p {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
.noitem {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
|
||||
@@ -33,7 +33,7 @@ dd
|
||||
p
|
||||
| 可以加入测试企鹅群
|
||||
span.hover(:aria-label="$t('setting__click_open')" @click="openUrl('https://qm.qq.com/cgi-bin/qm/qr?k=zR6aYosQoKb07g4FGFZdO9n9zL1dhFpE&jump_from=webapi')") 768786588
|
||||
| ,注意:测试版的功可能会不稳定,
|
||||
| ,注意:测试版的功能可能会不稳定,
|
||||
strong 打算潜水的勿加
|
||||
|
||||
br
|
||||
@@ -53,10 +53,9 @@ dd
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import { ref, onBeforeUnmount } from '@renderer/utils/vueTools'
|
||||
import { isShowPact } from '@renderer/core/share'
|
||||
import { openUrl, clipboardWriteText } from '@renderer/utils'
|
||||
import { currentStting } from '../setting'
|
||||
// import { ref, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { isShowPact } from '@renderer/store'
|
||||
import { openUrl, clipboardWriteText } from '@common/utils/electron'
|
||||
|
||||
export default {
|
||||
name: 'SettingAbout',
|
||||
@@ -65,7 +64,6 @@ export default {
|
||||
isShowPact.value = true
|
||||
}
|
||||
return {
|
||||
currentStting,
|
||||
openUrl,
|
||||
clipboardWriteText,
|
||||
handleShowPact,
|
||||
|
||||
@@ -20,70 +20,128 @@ dd
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { useCommit, useI18n, onBeforeUnmount, toRaw, useRefGetter } from '@renderer/utils/vueTools'
|
||||
import { mergeSetting } from '@common/utils'
|
||||
import { base as eventBaseName } from '@renderer/event/names'
|
||||
import { defaultList, loveList, userLists } from '@renderer/core/share/list'
|
||||
import { toRaw } from '@common/utils/vueTools'
|
||||
// import { mergeSetting } from '@common/utils'
|
||||
// import { base as eventBaseName } from '@renderer/event/names'
|
||||
// import { defaultList, loveList, userLists } from '@renderer/core/share/list'
|
||||
import {
|
||||
getSetting,
|
||||
saveSetting,
|
||||
saveLxConfigFile,
|
||||
readLxConfigFile,
|
||||
selectDir,
|
||||
openSaveDir,
|
||||
filterFileName,
|
||||
saveStrToFile,
|
||||
toNewMusicInfo,
|
||||
// toOldMusicInfo,
|
||||
fixNewMusicInfoQuality,
|
||||
} from '@renderer/utils'
|
||||
import { currentStting } from '../setting'
|
||||
import { getList } from '@renderer/core/share/utils'
|
||||
import path from 'path'
|
||||
import {
|
||||
showSelectDialog,
|
||||
openSaveDir,
|
||||
} from '@renderer/utils/ipc'
|
||||
// import { currentStting } from '../setting'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
import iconv from 'iconv-lite'
|
||||
import useImportTip from '@renderer/utils/compositions/useImportTip'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { createUserList, getListMusics, overwriteListMusics } from '@renderer/store/list/action'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
import { defaultList, loveList, userLists } from '@renderer/store/list/state'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
import migrateSetting from '@common/utils/migrateSetting'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'SettingUpdate',
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const setting = useRefGetter('setting')
|
||||
const settingVersion = useRefGetter('settingVersion')
|
||||
const setSettingVersion = useCommit('setSettingVersion')
|
||||
const setList = useCommit('list', 'setList')
|
||||
const t = useI18n()
|
||||
// const setting = useRefGetter('setting')
|
||||
// const settingVersion = useRefGetter('settingVersion')
|
||||
// const setSettingVersion = useCommit('setSettingVersion')
|
||||
// const setList = useCommit('list', 'setList')
|
||||
const showImportTip = useImportTip()
|
||||
|
||||
const handleUpdateSetting = (config) => {
|
||||
currentStting.value = JSON.parse(JSON.stringify(config))
|
||||
const getAllLists = async() => {
|
||||
const lists = []
|
||||
lists.push(await getListMusics(defaultList.id).then(musics => ({ ...defaultList, list: toRaw(musics) })))
|
||||
lists.push(await getListMusics(loveList.id).then(musics => ({ ...loveList, list: toRaw(musics) })))
|
||||
|
||||
for await (const list of userLists) {
|
||||
lists.push(await getListMusics(list.id).then(musics => ({ ...toRaw(list), list: toRaw(musics) })))
|
||||
}
|
||||
|
||||
return lists
|
||||
}
|
||||
const refreshSetting = async(newSetting, newVersion) => {
|
||||
await saveSetting(toRaw(newSetting))
|
||||
const { setting, version } = await getSetting()
|
||||
currentStting.value = setting
|
||||
setSettingVersion(version)
|
||||
|
||||
const importOldListData = async(lists) => {
|
||||
for await (const list of lists) {
|
||||
try {
|
||||
if (userLists.some(l => l.id == list.id) || list.id == LIST_IDS.DEFAULT || list.id == LIST_IDS.LOVE) {
|
||||
await overwriteListMusics({ listId: list.id, musicInfos: list.list.map(m => toNewMusicInfo(m)) })
|
||||
} else {
|
||||
await createUserList({
|
||||
name: list.name,
|
||||
id: list.id,
|
||||
list: list.list.map(m => toNewMusicInfo(m)),
|
||||
position: list.position,
|
||||
source: list.source,
|
||||
sourceListId: list.sourceListId,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
const importNewListData = async(lists) => {
|
||||
for await (const list of lists) {
|
||||
try {
|
||||
if (userLists.some(l => l.id == list.id) || list.id == LIST_IDS.DEFAULT || list.id == LIST_IDS.LOVE) {
|
||||
await overwriteListMusics({ listId: list.id, musicInfos: list.list.map(m => fixNewMusicInfoQuality(m)) })
|
||||
} else {
|
||||
await createUserList({
|
||||
name: list.name,
|
||||
id: list.id,
|
||||
list: list.list.map(m => fixNewMusicInfoQuality(m)),
|
||||
position: list.position,
|
||||
source: list.source,
|
||||
sourceListId: list.sourceListId,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
const importOldSettingData = (setting) => {
|
||||
console.log(setting)
|
||||
setting = migrateSetting(setting)
|
||||
setting['common.isAgreePact'] = false
|
||||
updateSetting(setting)
|
||||
}
|
||||
const importNewSettingData = (setting) => {
|
||||
setting['common.isAgreePact'] = false
|
||||
updateSetting(setting)
|
||||
}
|
||||
|
||||
|
||||
const importAllData = async(path) => {
|
||||
let allData
|
||||
try {
|
||||
allData = await readLxConfigFile(path)
|
||||
allData = await window.lx.worker.main.readLxConfigFile(path)
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
if (allData.type !== 'allData') return showImportTip(allData.type)
|
||||
|
||||
// 兼容0.6.2及以前版本的列表数据
|
||||
if (allData.defaultList) return setList({ id: 'default', list: allData.defaultList.list, name: '试听列表' })
|
||||
|
||||
for (const list of allData.playList) {
|
||||
setList(list)
|
||||
switch (allData.type) {
|
||||
case 'allData':
|
||||
// 兼容0.6.2及以前版本的列表数据
|
||||
if (allData.defaultList) await overwriteListMusics({ listId: LIST_IDS.DEFAULT, musicInfos: allData.defaultList.list.map(m => toNewMusicInfo(m)) })
|
||||
else await importOldListData(allData.playList)
|
||||
importOldSettingData(allData.setting)
|
||||
break
|
||||
case 'allData_v2':
|
||||
await importNewListData(allData.playList)
|
||||
importNewSettingData(allData.setting)
|
||||
break
|
||||
default: return showImportTip(allData.type)
|
||||
}
|
||||
|
||||
const { version: settingVersion, setting } = mergeSetting(allData.setting)
|
||||
setting.isAgreePact = false
|
||||
|
||||
await refreshSetting(setting, settingVersion)
|
||||
}
|
||||
const handleImportAllData = () => {
|
||||
selectDir({
|
||||
showSelectDialog({
|
||||
title: t('setting__backup_all_import_desc'),
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
@@ -92,31 +150,29 @@ export default {
|
||||
],
|
||||
}).then(result => {
|
||||
if (result.canceled) return
|
||||
importAllData(result.filePaths[0])
|
||||
dialog.confirm({
|
||||
message: t('setting__backup_part_import_list_confirm'),
|
||||
cancelButtonText: t('cancel_button_text'),
|
||||
confirmButtonText: t('confirm_button_text'),
|
||||
}).then(confirm => {
|
||||
if (!confirm) return
|
||||
importAllData(result.filePaths[0])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const exportAllData = async(path) => {
|
||||
let allData = JSON.parse(JSON.stringify({
|
||||
type: 'allData',
|
||||
setting: Object.assign({ version: settingVersion.value }, toRaw(setting.value)),
|
||||
playList: [
|
||||
{ ...toRaw(defaultList), list: toRaw(getList(defaultList.id)) },
|
||||
{ ...toRaw(loveList), list: toRaw(getList(loveList.id)) },
|
||||
...userLists.map(l => ({ ...toRaw(l), list: toRaw(getList(l.id)) })),
|
||||
],
|
||||
}))
|
||||
for await (const list of allData.playList) {
|
||||
for await (const item of list.list) {
|
||||
if (item.otherSource) delete item.otherSource
|
||||
}
|
||||
let allData = {
|
||||
type: 'allData_v2',
|
||||
setting: { ...appSetting },
|
||||
playList: await getAllLists(),
|
||||
}
|
||||
saveLxConfigFile(path, allData)
|
||||
window.lx.worker.main.saveLxConfigFile(path, allData)
|
||||
}
|
||||
const handleExportAllData = () => {
|
||||
openSaveDir({
|
||||
title: t('setting__backup_all_export_desc'),
|
||||
defaultPath: 'lx_datas.lxmc',
|
||||
defaultPath: 'lx_datas_v2.lxmc',
|
||||
}).then(result => {
|
||||
if (result.canceled) return
|
||||
exportAllData(result.filePath)
|
||||
@@ -125,15 +181,15 @@ export default {
|
||||
|
||||
const exportSetting = (path) => {
|
||||
const data = {
|
||||
type: 'setting',
|
||||
data: Object.assign({ version: settingVersion.value }, toRaw(setting.value)),
|
||||
type: 'setting_v2',
|
||||
data: { ...appSetting },
|
||||
}
|
||||
saveLxConfigFile(path, data)
|
||||
window.lx.worker.main.saveLxConfigFile(path, data)
|
||||
}
|
||||
const handleExportSetting = () => {
|
||||
openSaveDir({
|
||||
title: t('setting__backup_part_export_setting_desc'),
|
||||
defaultPath: 'lx_setting.lxmc',
|
||||
defaultPath: 'lx_setting_v2.lxmc',
|
||||
}).then(result => {
|
||||
if (result.canceled) return
|
||||
exportSetting(result.filePath)
|
||||
@@ -143,17 +199,23 @@ export default {
|
||||
const importSetting = async(path) => {
|
||||
let settingData
|
||||
try {
|
||||
settingData = await readLxConfigFile(path)
|
||||
settingData = await window.lx.worker.main.readLxConfigFile(path)
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
if (settingData.type !== 'setting') return showImportTip(settingData.type)
|
||||
const { version: settingVersion, setting } = mergeSetting(settingData.data)
|
||||
setting.isAgreePact = false
|
||||
refreshSetting(setting, settingVersion)
|
||||
|
||||
switch (settingData.type) {
|
||||
case 'setting':
|
||||
importOldSettingData(settingData.data)
|
||||
break
|
||||
case 'setting_v2':
|
||||
importNewSettingData(settingData.data)
|
||||
break
|
||||
default: return showImportTip(settingData.type)
|
||||
}
|
||||
}
|
||||
const handleImportSetting = () => {
|
||||
selectDir({
|
||||
showSelectDialog({
|
||||
title: t('setting__backup_part_import_setting_desc'),
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
@@ -167,20 +229,11 @@ export default {
|
||||
}
|
||||
|
||||
const exportPlayList = async(path) => {
|
||||
const data = JSON.parse(JSON.stringify({
|
||||
type: 'playList',
|
||||
data: [
|
||||
{ ...toRaw(defaultList), list: toRaw(getList(defaultList.id)) },
|
||||
{ ...toRaw(loveList), list: toRaw(getList(loveList.id)) },
|
||||
...userLists.map(l => ({ ...toRaw(l), list: toRaw(getList(l.id)) })),
|
||||
],
|
||||
}))
|
||||
for await (const list of data.data) {
|
||||
for await (const item of list.list) {
|
||||
if (item.otherSource) delete item.otherSource
|
||||
}
|
||||
const data = {
|
||||
type: 'playList_v2',
|
||||
data: await getAllLists(),
|
||||
}
|
||||
saveLxConfigFile(path, data)
|
||||
window.lx.worker.main.saveLxConfigFile(path, data)
|
||||
}
|
||||
const handleExportPlayList = () => {
|
||||
openSaveDir({
|
||||
@@ -195,25 +248,27 @@ export default {
|
||||
const importPlayList = async(path) => {
|
||||
let listData
|
||||
try {
|
||||
listData = await readLxConfigFile(path)
|
||||
listData = await window.lx.worker.main.readLxConfigFile(path)
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
console.log(listData.type)
|
||||
|
||||
// 兼容0.6.2及以前版本的列表数据
|
||||
if (listData.type === 'defautlList') return setList({ id: 'default', list: listData.data.list, name: '试听列表' })
|
||||
|
||||
if (listData.type !== 'playList') return showImportTip(listData.type)
|
||||
|
||||
for (const list of listData.data) {
|
||||
setList(list)
|
||||
switch (listData.type) {
|
||||
case 'defautlList': // 兼容0.6.2及以前版本的列表数据
|
||||
await overwriteListMusics({ listId: LIST_IDS.DEFAULT, musicInfos: listData.data.list.map(m => toNewMusicInfo(m)) })
|
||||
break
|
||||
case 'playList':
|
||||
await importOldListData(listData.data)
|
||||
break
|
||||
case 'playList_v2':
|
||||
await importNewListData(listData.data)
|
||||
break
|
||||
default: return showImportTip(listData.type)
|
||||
}
|
||||
|
||||
await refreshSetting(setting.value, settingVersion.value)
|
||||
}
|
||||
const handleImportPlayList = () => {
|
||||
selectDir({
|
||||
showSelectDialog({
|
||||
title: t('setting__backup_part_import_list_desc'),
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
@@ -222,28 +277,20 @@ export default {
|
||||
],
|
||||
}).then(result => {
|
||||
if (result.canceled) return
|
||||
importPlayList(result.filePaths[0])
|
||||
dialog.confirm({
|
||||
message: t('setting__backup_part_import_list_confirm'),
|
||||
cancelButtonText: t('cancel_button_text'),
|
||||
confirmButtonText: t('confirm_button_text'),
|
||||
}).then(confirm => {
|
||||
if (!confirm) return
|
||||
importPlayList(result.filePaths[0])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const exportPlayListToText = async(savePath, isMerge) => {
|
||||
const lists = JSON.parse(JSON.stringify([
|
||||
{ ...toRaw(defaultList), list: toRaw(getList(defaultList.id)) },
|
||||
{ ...toRaw(loveList), list: toRaw(getList(loveList.id)) },
|
||||
...userLists.map(l => ({ ...toRaw(l), list: toRaw(getList(l.id)) })),
|
||||
]))
|
||||
for await (const list of lists) {
|
||||
for await (const item of list.list) {
|
||||
if (item.otherSource) delete item.otherSource
|
||||
}
|
||||
}
|
||||
if (isMerge) {
|
||||
saveStrToFile(savePath, iconv.encode(lists.map(l => l.list.map(m => `${m.name} ${m.singer} ${m.albumName}`).join('\n')).join('\n\n'), 'utf8', { addBOM: true }))
|
||||
} else {
|
||||
for await (const list of lists) {
|
||||
await saveStrToFile(path.join(savePath, `lx_list_${filterFileName(list.name)}.txt`), iconv.encode(list.list.map(m => `${m.name} ${m.singer} ${m.albumName}`).join('\n'), 'utf8', { addBOM: true }))
|
||||
}
|
||||
}
|
||||
const lists = await getAllLists()
|
||||
await window.lx.worker.main.exportPlayListToText(savePath, lists, isMerge)
|
||||
}
|
||||
const handleExportPlayListToText = async() => {
|
||||
const confirm = await dialog.confirm({
|
||||
@@ -262,9 +309,9 @@ export default {
|
||||
exportPlayListToText(path, true)
|
||||
})
|
||||
} else {
|
||||
selectDir({
|
||||
showSelectDialog({
|
||||
title: t('setting__backup_other_export_dir'),
|
||||
defaultPath: currentStting.value.download.savePath,
|
||||
// defaultPath: currentStting.value.download.savePath,
|
||||
properties: ['openDirectory'],
|
||||
}).then(result => {
|
||||
if (result.canceled) return
|
||||
@@ -274,30 +321,8 @@ export default {
|
||||
}
|
||||
|
||||
const exportPlayListToCsv = async(savePath, isMerge) => {
|
||||
const lists = JSON.parse(JSON.stringify([
|
||||
{ ...toRaw(defaultList), list: toRaw(getList(defaultList.id)) },
|
||||
{ ...toRaw(loveList), list: toRaw(getList(loveList.id)) },
|
||||
...userLists.map(l => ({ ...toRaw(l), list: toRaw(getList(l.id)) })),
|
||||
]))
|
||||
for await (const list of lists) {
|
||||
for await (const item of list.list) {
|
||||
if (item.otherSource) delete item.otherSource
|
||||
}
|
||||
}
|
||||
const filterStr = str => {
|
||||
if (!str) return ''
|
||||
str = str.replace(/"/g, '""')
|
||||
if (/,/.test(str)) str = `"${str}"`
|
||||
return str
|
||||
}
|
||||
const header = `${t('music_name')},${t('music_singer')},${t('music_album')}\n`
|
||||
if (isMerge) {
|
||||
saveStrToFile(savePath, iconv.encode(header + lists.map(l => l.list.map(m => `${filterStr(m.name)},${filterStr(m.singer)},${filterStr(m.albumName)}`).join('\n')).join('\n'), 'utf8', { addBOM: true }))
|
||||
} else {
|
||||
for await (const list of lists) {
|
||||
await saveStrToFile(path.join(savePath, `lx_list_${filterFileName(list.name)}.csv`), iconv.encode(header + list.list.map(m => `${filterStr(m.name)},${filterStr(m.singer)},${filterStr(m.albumName)}`).join('\n'), 'utf8', { addBOM: true }))
|
||||
}
|
||||
}
|
||||
const lists = await getAllLists()
|
||||
await window.lx.worker.main.exportPlayListToCSV(savePath, lists, isMerge, `${t('music_name')},${t('music_singer')},${t('music_album')}\n`)
|
||||
}
|
||||
const handleExportPlayListToCsv = async() => {
|
||||
const confirm = await dialog.confirm({
|
||||
@@ -316,9 +341,9 @@ export default {
|
||||
exportPlayListToCsv(path, true)
|
||||
})
|
||||
} else {
|
||||
selectDir({
|
||||
showSelectDialog({
|
||||
title: t('setting__backup_other_export_dir'),
|
||||
defaultPath: currentStting.value.download.savePath,
|
||||
// defaultPath: currentStting.value.download.savePath,
|
||||
properties: ['openDirectory'],
|
||||
}).then(result => {
|
||||
if (result.canceled) return
|
||||
@@ -327,14 +352,14 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
window.eventHub.on(eventBaseName.set_config, handleUpdateSetting)
|
||||
// window.eventHub.on(eventBaseName.set_config, handleUpdateSetting)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.eventHub.off(eventBaseName.set_config, handleUpdateSetting)
|
||||
})
|
||||
// onBeforeUnmount(() => {
|
||||
// window.eventHub.off(eventBaseName.set_config, handleUpdateSetting)
|
||||
// })
|
||||
|
||||
return {
|
||||
currentStting,
|
||||
// currentStting,
|
||||
handleExportPlayList,
|
||||
handleImportPlayList,
|
||||
handleExportSetting,
|
||||
|
||||
@@ -4,109 +4,197 @@ dd
|
||||
h3#basic_theme {{$t('setting__basic_theme')}}
|
||||
div
|
||||
ul(:class="$style.theme")
|
||||
li(v-for="theme in themes.list" :key="theme.id" :aria-label="$t('theme_' + theme.className)" @click="currentStting.theme.id = theme.id" :class="[theme.className, {[$style.active]: themes.active == theme.id}]")
|
||||
li(v-for="theme in defaultThemes" :key="theme.id" :aria-label="$t('theme_' + theme.id)" @click="toggleTheme(theme)" :style="theme.styles" :class="[$style.themeItem, {[$style.active]: themeId == theme.id}]")
|
||||
div(:class="$style.bg")
|
||||
label {{$t('theme_' + theme.className)}}
|
||||
li(:aria-label="$t('theme_auto_tip')" @click="handleSetThemeAuto" @contextmenu="isShowThemeSelectorModal = true" :class="[$style.auto, themeClassName, {[$style.active]: themes.active == 'auto'}]")
|
||||
span(:class="$style.label") {{$t('theme_' + theme.id)}}
|
||||
li(v-for="theme in userThemes" :key="theme.id" :aria-label="theme.name" @click="toggleTheme(theme)" @contextmenu="handleEditTheme(theme)" :style="theme.styles" :class="[$style.themeItem, {[$style.active]: themeId == theme.id}]")
|
||||
div(:class="$style.bg")
|
||||
span(:class="$style.label") {{theme.name}}
|
||||
li(:aria-label="$t('theme_auto_tip')" @click="handleSetThemeAuto" @contextmenu="isShowThemeSelectorModal = true" :style="autoTheme" :class="[$style.themeItem, $style.auto, {[$style.active]: themeId == 'auto'}]")
|
||||
div(:class="$style.bg")
|
||||
div(:class="$style.bgContent")
|
||||
div(:class="[$style.light, themes.lightTheme.className]")
|
||||
div(:class="[$style.dark, themes.darkTheme.className]")
|
||||
label {{$t('theme_auto')}}
|
||||
div(:class="$style.light")
|
||||
div(:class="$style.dark")
|
||||
span(:class="$style.label") {{$t('theme_auto')}}
|
||||
li(:aria-label="$t('theme_add')" @click="handleEditTheme()" :class="[$style.themeItem, $style.add]")
|
||||
div(:class="$style.bg")
|
||||
div(:class="$style.bgContent")
|
||||
svg-icon(:class="$style.icon" name="plus")
|
||||
span(:class="$style.label") {{$t('theme_add')}}
|
||||
|
||||
dd
|
||||
div
|
||||
.gap-top.top
|
||||
base-checkbox(id="setting_show_animate" v-model="currentStting.isShowAnimation" :label="$t('setting__basic_show_animation')")
|
||||
base-checkbox(id="setting_show_animate" :modelValue="appSetting['common.isShowAnimation']" @update:modelValue="updateSetting({'common.isShowAnimation': $event})" :label="$t('setting__basic_show_animation')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_animate" v-model="currentStting.randomAnimate" :label="$t('setting__basic_animation')")
|
||||
base-checkbox(id="setting_animate" :modelValue="appSetting['common.randomAnimate']" @update:modelValue="updateSetting({'common.randomAnimate': $event})" :label="$t('setting__basic_animation')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_start_in_fullscreen" v-model="currentStting.startInFullscreen" :label="$t('setting__basic_start_in_fullscreen')")
|
||||
base-checkbox(id="setting_start_in_fullscreen" :modelValue="appSetting['common.startInFullscreen']" @update:modelValue="updateSetting({'common.startInFullscreen': $event})" :label="$t('setting__basic_start_in_fullscreen')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_to_tray" v-model="currentStting.tray.isShow" :label="$t('setting__basic_to_tray')")
|
||||
base-checkbox(id="setting_to_tray" :modelValue="appSetting['tray.enable']" @update:modelValue="updateSetting({'tray.enable': $event})" :label="$t('setting__basic_to_tray')")
|
||||
p.gap-top
|
||||
base-btn.btn(min @click="isShowPlayTimeoutModal = true") {{$t('setting__play_timeout')}} {{ timeLabel ? ` (${timeLabel})` : '' }}
|
||||
|
||||
dd(:aria-label="$t('setting__basic_source_title')")
|
||||
dd
|
||||
h3#basic_source {{$t('setting__basic_source')}}
|
||||
div
|
||||
.gap-top(v-for="item in apiSources" :key="item.id")
|
||||
base-checkbox(:id="`setting_api_source_${item.id}`" name="setting_api_source"
|
||||
need v-model="currentStting.apiSource" :disabled="item.disabled" :value="item.id" :label="item.label")
|
||||
need :modelValue="appSetting['common.apiSource']" @update:modelValue="updateSetting({'common.apiSource': $event})" :disabled="item.disabled" :value="item.id" :label="item.label")
|
||||
p.gap-top
|
||||
base-btn.btn(min @click="isShowUserApiModal = true") {{$t('setting__basic_source_user_api_btn')}}
|
||||
|
||||
dd(:aria-label="$t('setting__basic_window_size_title')")
|
||||
dd
|
||||
h3#basic_window_size {{$t('setting__basic_window_size')}}
|
||||
div
|
||||
base-checkbox.gap-left(v-for="(item, index) in windowSizeList" :id="`setting_window_size_${item.id}`" name="setting_window_size"
|
||||
need v-model="currentStting.windowSizeId" :disabled="isFullscreen" :value="item.id" :label="$t('setting__basic_window_size_' + item.name)" :key="item.id")
|
||||
need :modelValue="appSetting['common.windowSizeId']" @update:modelValue="updateSetting({'common.windowSizeId': $event})" :disabled="isFullscreen" :value="item.id" :label="$t('setting__basic_window_size_' + item.name)" :key="item.id")
|
||||
|
||||
dd(:aria-label="$t('setting__basic_lang_title')")
|
||||
dd
|
||||
h3#basic_font_size {{$t('setting__basic_font_size')}}
|
||||
div
|
||||
//- base-selection.gap-teft(:list="fontSizeList" :modelValue="appSetting['common.fontSize']" @update:modelValue="updateSetting({'common.fontSize': $event})")
|
||||
base-checkbox.gap-left(v-for="item in fontSizeList" :key="item.id" :id="`setting_basic_font_size_${item.id}`"
|
||||
name="setting_basic_font_size" need :modelValue="appSetting['common.fontSize']" @update:modelValue="updateSetting({'common.fontSize': $event})"
|
||||
:value="item.id" :label="item.label" :disabled="isFullscreen")
|
||||
|
||||
dd
|
||||
h3#basic_font {{$t('setting__basic_font')}}
|
||||
div
|
||||
base-selection.gap-teft(:list="fontList" :modelValue="appSetting['common.font']" @update:modelValue="updateSetting({'common.font': $event})" item-key="id" item-name="label")
|
||||
|
||||
dd
|
||||
h3#basic_lang {{$t('setting__basic_lang')}}
|
||||
div
|
||||
base-checkbox.gap-left(v-for="item in langList" :key="item.locale" :id="`setting_lang_${item.locale}`" name="setting_lang"
|
||||
need v-model="currentStting.langId" :value="item.locale" :label="item.name")
|
||||
need :modelValue="appSetting['common.langId']" @update:modelValue="updateSetting({'common.langId': $event})" :value="item.locale" :label="item.name")
|
||||
|
||||
dd(:aria-label="$t('setting__basic_sourcename_title')")
|
||||
dd
|
||||
h3#basic_sourcename {{$t('setting__basic_sourcename')}}
|
||||
div
|
||||
base-checkbox.gap-left(v-for="item in sourceNameTypes" :key="item.id" :id="`setting_abasic_sourcename_${item.id}`"
|
||||
name="setting_basic_sourcename" need v-model="currentStting.sourceNameType" :value="item.id" :label="item.label")
|
||||
name="setting_basic_sourcename" need :modelValue="appSetting['common.sourceNameType']" @update:modelValue="updateSetting({'common.sourceNameType': $event})" :value="item.id" :label="item.label")
|
||||
dd
|
||||
h3#basic_control_btn_position {{$t('setting__basic_control_btn_position')}}
|
||||
div
|
||||
base-checkbox.gap-left(v-for="item in controlBtnPositionList" :key="item.id" :id="`setting_basic_control_btn_position_${item.id}`"
|
||||
name="setting_basic_control_btn_position" need v-model="currentStting.controlBtnPosition" :value="item.id" :label="item.name")
|
||||
dd
|
||||
h3#basic_font {{$t('setting__basic_font')}}
|
||||
div
|
||||
base-selection.gap-teft(:list="fontList" v-model="currentStting.font" item-key="id" item-name="label")
|
||||
name="setting_basic_control_btn_position" need :modelValue="appSetting['common.controlBtnPosition']" @update:modelValue="updateSetting({'common.controlBtnPosition': $event})" :value="item.id" :label="item.name")
|
||||
|
||||
ThemeSelectorModal(v-model="isShowThemeSelectorModal")
|
||||
ThemeEditModal(v-model="isShowThemeEditModal" :theme-id="editThemeId" @submit="handleRefreshTheme")
|
||||
play-timeout-modal(v-model="isShowPlayTimeoutModal")
|
||||
user-api-modal(v-model="isShowUserApiModal")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, ref, useI18n, watch, useRefGetter } from '@renderer/utils/vueTools'
|
||||
import { themes as themeList, windowSizeList, apiSource, userApi, isFullscreen } from '@renderer/core/share'
|
||||
import { langList } from '@/lang'
|
||||
import { currentStting } from '../setting'
|
||||
import { setWindowSize } from '@renderer/utils'
|
||||
import apiSourceInfo from '@renderer/utils/music/api-source-info'
|
||||
import { useTimeout } from '@renderer/utils/timeoutStop'
|
||||
import { getSystemFonts } from '@renderer/utils/tools'
|
||||
import { computed, ref, watch, reactive, shallowReactive } from '@common/utils/vueTools'
|
||||
import { windowSizeList, userApi, isFullscreen, themeId } from '@renderer/store'
|
||||
import { langList, useI18n } from '@/lang'
|
||||
import { getSystemFonts } from '@renderer/utils/ipc'
|
||||
import apiSourceInfo from '@renderer/utils/musicSdk/api-source-info'
|
||||
import { useTimeout } from '@renderer/core/player/timeoutStop'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
|
||||
import ThemeSelectorModal from './ThemeSelectorModal'
|
||||
import ThemeEditModal from './ThemeEditModal'
|
||||
import PlayTimeoutModal from './PlayTimeoutModal'
|
||||
import UserApiModal from './UserApiModal'
|
||||
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
import { getThemes, applyTheme, findTheme } from '@renderer/store/utils'
|
||||
import { joinPath } from '@common/utils/nodejs'
|
||||
|
||||
export default {
|
||||
name: 'SettingBasic',
|
||||
components: {
|
||||
ThemeSelectorModal,
|
||||
ThemeEditModal,
|
||||
PlayTimeoutModal,
|
||||
UserApiModal,
|
||||
},
|
||||
setup() {
|
||||
const { t, locale } = useI18n()
|
||||
const t = useI18n()
|
||||
|
||||
const themeClassName = useRefGetter('theme')
|
||||
const themes = computed(() => {
|
||||
return {
|
||||
active: currentStting.value.theme.id,
|
||||
lightTheme: themeList.find(t => t.id == currentStting.value.theme.lightId) ?? themeList[0],
|
||||
darkTheme: themeList.find(t => t.id == currentStting.value.theme.darkId) ?? themeList[0],
|
||||
list: themeList,
|
||||
const defaultThemes = shallowReactive([])
|
||||
const userThemes = shallowReactive([])
|
||||
const autoTheme = reactive({})
|
||||
const updateAutoTheme = (info) => {
|
||||
let light = findTheme(info, appSetting['theme.lightId'])
|
||||
if (!light) light = info.themes.find(theme => theme.id == 'green')
|
||||
let dark = findTheme(info, appSetting['theme.darkId'])
|
||||
if (!dark) dark = info.themes.find(theme => theme.id == 'black')
|
||||
autoTheme['--color-primary-theme-light'] = light.config.themeColors['--color-theme']
|
||||
autoTheme['--background-image-theme-light'] = light.isCustom
|
||||
? light.config.extInfo['--background-image'] == 'none'
|
||||
? 'none'
|
||||
: `url(file:///${joinPath(info.dataPath, light.config.extInfo['--background-image']).replaceAll('\\', '/')})`
|
||||
: light.config.extInfo['--background-image']
|
||||
autoTheme['--color-primary-theme-dark'] = dark.config.themeColors['--color-theme']
|
||||
autoTheme['--background-image-theme-dark'] = dark.isCustom
|
||||
? dark.config.extInfo['--background-image'] == 'none'
|
||||
? 'none'
|
||||
: `url(file:///${joinPath(info.dataPath, dark.config.extInfo['--background-image']).replaceAll('\\', '/')})`
|
||||
: dark.config.extInfo['--background-image']
|
||||
}
|
||||
|
||||
let dataPath = ''
|
||||
const init = () => {
|
||||
getThemes((info) => {
|
||||
// console.log(info)
|
||||
dataPath = info.dataPath
|
||||
defaultThemes.splice(0, defaultThemes.length, ...info.themes.map(t => {
|
||||
return {
|
||||
id: t.id,
|
||||
styles: {
|
||||
'--color-primary-theme': t.config.themeColors['--color-theme'],
|
||||
'--background-image-theme': t.config.extInfo['--background-image'],
|
||||
},
|
||||
}
|
||||
}))
|
||||
userThemes.splice(0, userThemes.length, ...info.userThemes.map(t => {
|
||||
return {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
styles: {
|
||||
'--color-primary-theme': t.config.themeColors['--color-theme'],
|
||||
'--background-image-theme': t.config.extInfo['--background-image'] == 'none'
|
||||
? 'none'
|
||||
: `url(file:///${joinPath(info.dataPath, t.config.extInfo['--background-image']).replaceAll('\\', '/')})`,
|
||||
},
|
||||
}
|
||||
}))
|
||||
updateAutoTheme(info)
|
||||
})
|
||||
}
|
||||
const editThemeId = ref('')
|
||||
const handleEditTheme = (theme) => {
|
||||
// console.log(theme)
|
||||
if (!theme && userThemes.length >= 10) {
|
||||
dialog({
|
||||
message: t('theme_max_tip'),
|
||||
confirmButtonText: t('alert_button_text'),
|
||||
})
|
||||
return
|
||||
}
|
||||
editThemeId.value = theme ? theme.id : ''
|
||||
isShowThemeEditModal.value = true
|
||||
}
|
||||
const handleRefreshTheme = () => {
|
||||
init()
|
||||
}
|
||||
init()
|
||||
const toggleTheme = (theme) => {
|
||||
if (themeId.value == theme.id) return
|
||||
themeId.value = theme.id
|
||||
applyTheme(theme.id, appSetting['theme.lightId'], appSetting['theme.darkId'], dataPath)
|
||||
updateSetting({ 'theme.id': theme.id })
|
||||
}
|
||||
|
||||
watch(() => [appSetting['theme.lightId'], appSetting['theme.darkId']], () => {
|
||||
getThemes(updateAutoTheme)
|
||||
})
|
||||
const isShowThemeSelectorModal = ref(false)
|
||||
const handleSetThemeAuto = () => {
|
||||
if (currentStting.value.theme.id == 'auto') return
|
||||
currentStting.value.theme.id = 'auto'
|
||||
if (themeId.value == 'auto') return
|
||||
if (window.localStorage.getItem('theme-auto-tip') != 'true') {
|
||||
window.localStorage.setItem('theme-auto-tip', 'true')
|
||||
dialog({
|
||||
@@ -114,11 +202,9 @@ export default {
|
||||
confirmButtonText: t('ok'),
|
||||
})
|
||||
}
|
||||
toggleTheme({ id: 'auto' })
|
||||
}
|
||||
|
||||
watch(() => currentStting.value.apiSource, visible => {
|
||||
apiSource.value = visible
|
||||
})
|
||||
const isShowThemeEditModal = ref(false)
|
||||
|
||||
const isShowPlayTimeoutModal = ref(false)
|
||||
const { timeLabel } = useTimeout()
|
||||
@@ -141,7 +227,7 @@ export default {
|
||||
})),
|
||||
...userApi.list.map(api => ({
|
||||
id: api.id,
|
||||
label: `${api.name}${api.description ? `(${api.description})` : ''}${api.id == currentStting.value.apiSource ? `[${getApiStatus()}]` : ''}`,
|
||||
label: `${api.name}${api.description ? `(${api.description})` : ''}${api.id == appSetting['common.apiSource'] ? `[${getApiStatus()}]` : ''}`,
|
||||
status: api.status,
|
||||
message: api.message,
|
||||
disabled: false,
|
||||
@@ -149,18 +235,6 @@ export default {
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
watch(() => currentStting.value.windowSizeId, (index) => {
|
||||
let info = index == null ? windowSizeList[2] : windowSizeList[index]
|
||||
setWindowSize(info.width, info.height)
|
||||
})
|
||||
|
||||
|
||||
watch(() => currentStting.value.langId, (id) => {
|
||||
locale.value = id
|
||||
})
|
||||
|
||||
|
||||
const sourceNameTypes = computed(() => {
|
||||
return [
|
||||
{ id: 'real', label: t('setting__basic_sourcename_real') },
|
||||
@@ -184,11 +258,29 @@ export default {
|
||||
systemFontList.value = fonts.map(f => ({ id: f, label: f.replace(/(^"|"$)/g, '') }))
|
||||
})
|
||||
|
||||
const fontSizeList = computed(() => {
|
||||
return [
|
||||
{ id: 14, label: t('setting__basic_font_size_14px') },
|
||||
{ id: 15, label: t('setting__basic_font_size_15px') },
|
||||
{ id: 16, label: t('setting__basic_font_size_16px') },
|
||||
{ id: 17, label: t('setting__basic_font_size_17px') },
|
||||
{ id: 18, label: t('setting__basic_font_size_18px') },
|
||||
{ id: 19, label: t('setting__basic_font_size_19px') },
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
currentStting,
|
||||
themes,
|
||||
themeClassName,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
defaultThemes,
|
||||
userThemes,
|
||||
autoTheme,
|
||||
// currentStting,
|
||||
// themes,
|
||||
// themeClassName,
|
||||
isShowThemeSelectorModal,
|
||||
isShowThemeEditModal,
|
||||
handleSetThemeAuto,
|
||||
isShowPlayTimeoutModal,
|
||||
timeLabel,
|
||||
@@ -200,6 +292,12 @@ export default {
|
||||
controlBtnPositionList,
|
||||
fontList,
|
||||
isFullscreen,
|
||||
toggleTheme,
|
||||
themeId,
|
||||
handleRefreshTheme,
|
||||
editThemeId,
|
||||
handleEditTheme,
|
||||
fontSizeList,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -214,28 +312,44 @@ export default {
|
||||
// padding: 0 15px;
|
||||
margin-bottom: -20px;
|
||||
|
||||
li {
|
||||
.themeItem {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
// color: @color-theme;
|
||||
// color: var(--color-primary);
|
||||
margin-right: 30px;
|
||||
transition: color .3s ease;
|
||||
transition: .3s ease;
|
||||
transition-property: color, opacity;
|
||||
margin-bottom: 18px;
|
||||
width: 56px;
|
||||
|
||||
&:hover {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: var(--color-primary-font-active);
|
||||
.bg {
|
||||
border-color: var(--color-primary-font-active);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.bg {
|
||||
display: block;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 5px;
|
||||
border: 2px solid transparent;
|
||||
padding: 2px;
|
||||
border: 2Px solid transparent;
|
||||
padding: 2Px;
|
||||
transition: border-color .3s ease;
|
||||
border-radius: 5px;
|
||||
&:after {
|
||||
@@ -247,28 +361,27 @@ export default {
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-color: var(--color-primary-theme);
|
||||
background-image: var(--background-image-theme);
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
.label {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
height: 1.2em;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
&:global(.@{value}) {
|
||||
&.auto {
|
||||
|
||||
&.active {
|
||||
color: var(--color-primary-font-active);
|
||||
.bg {
|
||||
&:after {
|
||||
background-color: ~'@{color-@{value}-theme}';
|
||||
background-image: ~'@{color-@{value}-theme-bgimg}';
|
||||
}
|
||||
border-color: var(--color-primary-font-active);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
&.auto {
|
||||
>.bg {
|
||||
&:after {
|
||||
content: none;
|
||||
@@ -300,53 +413,59 @@ export default {
|
||||
&:after {
|
||||
clip-path: polygon(0 0, 100% 0, 0 100%);
|
||||
}
|
||||
each(@themes, {
|
||||
&:global(.@{value}) {
|
||||
svg {
|
||||
fill: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
&:after {
|
||||
background-color: ~'@{color-@{value}-theme}';
|
||||
background-image: ~'@{color-@{value}-theme-bgimg}';
|
||||
}
|
||||
}
|
||||
})
|
||||
svg {
|
||||
fill: var(--color-primary-theme-light);
|
||||
}
|
||||
&:after {
|
||||
background-color: var(--color-primary-theme-light);
|
||||
background-image: var(--background-image-theme-light);
|
||||
}
|
||||
}
|
||||
.dark {
|
||||
&:after {
|
||||
clip-path: polygon(0 100%, 100% 0, 100% 100%);
|
||||
}
|
||||
each(@themes, {
|
||||
&:global(.@{value}) {
|
||||
svg {
|
||||
fill: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
&:after {
|
||||
background-color: ~'@{color-@{value}-theme}';
|
||||
background-image: ~'@{color-@{value}-theme-bgimg}';
|
||||
}
|
||||
}
|
||||
})
|
||||
svg {
|
||||
fill: var(--color-primary-theme-dark);
|
||||
}
|
||||
&:after {
|
||||
background-color: var(--color-primary-theme-dark);
|
||||
background-image: var(--background-image-theme-dark);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.add {
|
||||
>.bg {
|
||||
&:after {
|
||||
content: none;
|
||||
}
|
||||
.bgContent {
|
||||
transition: .3s ease;
|
||||
transition-property: border, color;
|
||||
box-sizing: border-box;
|
||||
border: 1Px dashed var(--color-primary-light-100-alpha-300);
|
||||
color: var(--color-primary-light-100-alpha-300);
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.icon {
|
||||
// position: absolute;
|
||||
// font-size: 16px;
|
||||
width: 66%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
.label {
|
||||
color: var(--color-primary-dark-100-alpha-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.theme {
|
||||
li {
|
||||
&.active {
|
||||
&:global(.@{value}) {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
.bg {
|
||||
border-color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
|
||||
@@ -2,35 +2,214 @@
|
||||
dt#desktop_lyric {{$t('setting__desktop_lyric')}}
|
||||
dd
|
||||
.gap-top
|
||||
base-checkbox(id="setting_desktop_lyric_enable" v-model="currentStting.desktopLyric.enable" :label="$t('setting__desktop_lyric_enable')")
|
||||
base-checkbox(id="setting_desktop_lyric_enable" :modelValue="appSetting['desktopLyric.enable']" @update:modelValue="updateSetting({ 'desktopLyric.enable': $event })" :label="$t('setting__desktop_lyric_enable')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_desktop_lyric_lock" v-model="currentStting.desktopLyric.isLock" :label="$t('setting__desktop_lyric_lock')")
|
||||
base-checkbox(id="setting_desktop_lyric_lock" :modelValue="appSetting['desktopLyric.isLock']" @update:modelValue="updateSetting({ 'desktopLyric.isLock': $event })" :label="$t('setting__desktop_lyric_lock')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_desktop_lyric_delayScroll" v-model="currentStting.desktopLyric.isDelayScroll" :label="$t('setting__desktop_lyric_delay_scroll')")
|
||||
base-checkbox(id="setting_desktop_lyric_audio_visualization" :modelValue="appSetting['desktopLyric.audioVisualization']" @update:modelValue="updateSetting({ 'desktopLyric.audioVisualization': $event })" :label="$t('setting__desktop_lyric_audio_visualization')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_desktop_lyric_alwaysOnTop" v-model="currentStting.desktopLyric.isAlwaysOnTop" :label="$t('setting__desktop_lyric_always_on_top')")
|
||||
base-checkbox(id="setting_desktop_lyric_delayScroll" :modelValue="appSetting['desktopLyric.isDelayScroll']" @update:modelValue="updateSetting({ 'desktopLyric.isDelayScroll': $event })" :label="$t('setting__desktop_lyric_delay_scroll')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_desktop_lyric_alwaysOnTopLoop" v-model="currentStting.desktopLyric.isAlwaysOnTopLoop" :label="$t('setting__desktop_lyric_always_on_top_loop')")
|
||||
base-checkbox(id="setting_desktop_lyric_alwaysOnTop" :modelValue="appSetting['desktopLyric.isAlwaysOnTop']" @update:modelValue="updateSetting({ 'desktopLyric.isAlwaysOnTop': $event })" :label="$t('setting__desktop_lyric_always_on_top')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_desktop_lyric_lockScreen" v-model="currentStting.desktopLyric.isLockScreen" :label="$t('setting__desktop_lyric_lock_screen')")
|
||||
base-checkbox(id="setting_desktop_lyric_alwaysOnTopLoop" :modelValue="appSetting['desktopLyric.isAlwaysOnTopLoop']" @update:modelValue="updateSetting({ 'desktopLyric.isAlwaysOnTopLoop': $event })" :label="$t('setting__desktop_lyric_always_on_top_loop')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_desktop_lyric_lockScreen" :modelValue="appSetting['desktopLyric.isLockScreen']" @update:modelValue="updateSetting({ 'desktopLyric.isLockScreen': $event })" :label="$t('setting__desktop_lyric_lock_screen')")
|
||||
.gap-top(v-if="!isLinux")
|
||||
base-checkbox(id="setting_desktop_lyric_hoverHide" v-model="currentStting.desktopLyric.isHoverHide" :label="$t('setting__desktop_lyric_hover_hide')")
|
||||
base-checkbox(id="setting_desktop_lyric_hoverHide" :modelValue="appSetting['desktopLyric.isHoverHide']" @update:modelValue="updateSetting({ 'desktopLyric.isHoverHide': $event })" :label="$t('setting__desktop_lyric_hover_hide')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_desktop_lyric_ellipsis" :modelValue="appSetting['desktopLyric.style.ellipsis']" @update:modelValue="updateSetting({ 'desktopLyric.style.ellipsis': $event })" :label="$t('setting__desktop_lyric_ellipsis')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_desktop_lyric_zoom" :modelValue="appSetting['desktopLyric.style.isZoomActiveLrc']" @update:modelValue="updateSetting({ 'desktopLyric.style.isZoomActiveLrc': $event })" :label="$t('desktop_lyric__lrc_active_zoom_on')")
|
||||
//- .gap-top
|
||||
base-checkbox(id="setting_desktop_lyric_fontWeight" :modelValue="appSetting['desktopLyric.style.fontWeight']" @update:modelValue="updateSetting({ 'desktopLyric.style.fontWeight': $event })" :label="$t('setting__desktop_lyric_font_weight')")
|
||||
|
||||
dd
|
||||
h3#desktop_lyric_direction {{$t('setting__desktop_lyric_direction')}}
|
||||
div
|
||||
base-checkbox.gap-left(id="setting_desktop_lyric_direction_horizontal" :modelValue="appSetting['desktopLyric.direction']" @update:modelValue="updateSetting({ 'desktopLyric.direction': $event })" need value="horizontal" :label="$t('setting__desktop_lyric_direction_horizontal')")
|
||||
base-checkbox.gap-left(id="setting_desktop_lyric_direction_vertical" :modelValue="appSetting['desktopLyric.direction']" @update:modelValue="updateSetting({ 'desktopLyric.direction': $event })" need value="vertical" :label="$t('setting__desktop_lyric_direction_vertical')")
|
||||
|
||||
dd
|
||||
h3#desktop_lyric_align {{$t('setting__desktop_lyric_align')}}
|
||||
div
|
||||
base-checkbox.gap-left(id="setting_desktop_lyric_align_left" :modelValue="appSetting['desktopLyric.style.align']" @update:modelValue="updateSetting({ 'desktopLyric.style.align': $event })" need value="left" :label="$t('setting__desktop_lyric_align_left')")
|
||||
base-checkbox.gap-left(id="setting_desktop_lyric_align_center" :modelValue="appSetting['desktopLyric.style.align']" @update:modelValue="updateSetting({ 'desktopLyric.style.align': $event })" need value="center" :label="$t('setting__desktop_lyric_align_center')")
|
||||
base-checkbox.gap-left(id="setting_desktop_lyric_align_right" :modelValue="appSetting['desktopLyric.style.align']" @update:modelValue="updateSetting({ 'desktopLyric.style.align': $event })" need value="right" :label="$t('setting__desktop_lyric_align_right')")
|
||||
|
||||
dd
|
||||
h3#desktop_lyric_color {{$t('setting__desktop_lyric_color')}}
|
||||
div
|
||||
p.gap-top
|
||||
div(:class="$style.groupContent")
|
||||
div(:class="$style.item")
|
||||
div(ref="lyric_unplay_color_ref", :class="$style.color")
|
||||
div(:class="$style.label") {{ $t('setting__desktop_lyric_unplay_color') }}
|
||||
div(:class="$style.item")
|
||||
div(ref="lyric_played_color_ref", :class="$style.color")
|
||||
div(:class="$style.label") {{ $t('setting__desktop_lyric_played_color') }}
|
||||
div(:class="$style.item")
|
||||
div(ref="lyric_shadow_color_ref", :class="$style.color")
|
||||
div(:class="$style.label") {{ $t('setting__desktop_lyric_shadow_color') }}
|
||||
p.gap-top
|
||||
base-btn.btn(min @click="resetColor") {{$t('setting__desktop_lyric_color_reset')}}
|
||||
dd
|
||||
h3#desktop_lyric_font {{$t('setting__desktop_lyric_font')}}
|
||||
div
|
||||
base-selection.gap-teft(:list="fontList" v-model="currentStting.desktopLyric.style.font" item-key="id" item-name="label")
|
||||
base-selection.gap-teft(:list="fontList" :modelValue="appSetting['desktopLyric.style.font']" @update:modelValue="updateSetting({ 'desktopLyric.style.font': $event })" item-key="id" item-name="label")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, useI18n } from '@renderer/utils/vueTools'
|
||||
import { getSystemFonts } from '@renderer/utils/tools'
|
||||
import { currentStting } from '../setting'
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { getSystemFonts } from '@renderer/utils/ipc'
|
||||
import { isLinux } from '@common/utils'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { pickrTools } from '@renderer/utils/pickrTools'
|
||||
|
||||
const useLyricUnplayColor = () => {
|
||||
const lyric_unplay_color_ref = ref(null)
|
||||
let tools
|
||||
|
||||
const initLyricUnplayColor = (color, changed, reset) => {
|
||||
if (!lyric_unplay_color_ref.value) return
|
||||
tools = pickrTools.create(lyric_unplay_color_ref.value, color, null, changed, reset)
|
||||
}
|
||||
const destroyLyricUnplayColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setLyricUnplayColor = (color) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
lyric_unplay_color_ref,
|
||||
initLyricUnplayColor,
|
||||
destroyLyricUnplayColor,
|
||||
setLyricUnplayColor,
|
||||
}
|
||||
}
|
||||
|
||||
const useLyricPlayedColor = () => {
|
||||
const lyric_played_color_ref = ref(null)
|
||||
let tools
|
||||
|
||||
const initLyricPlayedColor = (color, changed, reset) => {
|
||||
if (!lyric_played_color_ref.value) return
|
||||
tools = pickrTools.create(lyric_played_color_ref.value, color, [
|
||||
'rgba(7, 197, 86, 1)',
|
||||
'rgba(255, 250, 18, 1)',
|
||||
'rgba(25, 181, 254, 1)',
|
||||
'rgba(255, 18, 34, 1)',
|
||||
], changed, reset)
|
||||
}
|
||||
const destroyLyricPlayedColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setLyricPlayedColor = (color) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
lyric_played_color_ref,
|
||||
initLyricPlayedColor,
|
||||
destroyLyricPlayedColor,
|
||||
setLyricPlayedColor,
|
||||
}
|
||||
}
|
||||
|
||||
const useLyricShadowColor = () => {
|
||||
const lyric_shadow_color_ref = ref(null)
|
||||
let tools
|
||||
|
||||
const initLyricShadowColor = (color, changed, reset) => {
|
||||
if (!lyric_shadow_color_ref.value) return
|
||||
tools = pickrTools.create(lyric_shadow_color_ref.value, color, null, changed, reset)
|
||||
}
|
||||
const destroyLyricShadowColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setLyricShadowColor = (color) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
lyric_shadow_color_ref,
|
||||
initLyricShadowColor,
|
||||
destroyLyricShadowColor,
|
||||
setLyricShadowColor,
|
||||
}
|
||||
}
|
||||
|
||||
const useLyricColor = () => {
|
||||
const { lyric_unplay_color_ref, initLyricUnplayColor, destroyLyricUnplayColor, setLyricUnplayColor } = useLyricUnplayColor()
|
||||
const { lyric_played_color_ref, initLyricPlayedColor, destroyLyricPlayedColor, setLyricPlayedColor } = useLyricPlayedColor()
|
||||
const { lyric_shadow_color_ref, initLyricShadowColor, destroyLyricShadowColor, setLyricShadowColor } = useLyricShadowColor()
|
||||
|
||||
const initColors = () => {
|
||||
initLyricUnplayColor(appSetting['desktopLyric.style.lyricUnplayColor'], (color) => {
|
||||
updateSetting({ 'desktopLyric.style.lyricUnplayColor': color })
|
||||
})
|
||||
initLyricPlayedColor(appSetting['desktopLyric.style.lyricPlayedColor'], (color) => {
|
||||
updateSetting({ 'desktopLyric.style.lyricPlayedColor': color })
|
||||
})
|
||||
initLyricShadowColor(appSetting['desktopLyric.style.lyricShadowColor'], (color) => {
|
||||
updateSetting({ 'desktopLyric.style.lyricShadowColor': color })
|
||||
})
|
||||
}
|
||||
|
||||
const destroyColors = () => {
|
||||
destroyLyricUnplayColor()
|
||||
destroyLyricPlayedColor()
|
||||
destroyLyricShadowColor()
|
||||
}
|
||||
|
||||
const resetColor = () => {
|
||||
const defaultSetting = {
|
||||
'desktopLyric.style.lyricUnplayColor': 'rgba(255, 255, 255, 1)',
|
||||
'desktopLyric.style.lyricPlayedColor': 'rgba(7, 197, 86, 1)',
|
||||
'desktopLyric.style.lyricShadowColor': 'rgba(0, 0, 0, 0.14)',
|
||||
}
|
||||
updateSetting(defaultSetting)
|
||||
setLyricUnplayColor(defaultSetting['desktopLyric.style.lyricUnplayColor'])
|
||||
setLyricPlayedColor(defaultSetting['desktopLyric.style.lyricPlayedColor'])
|
||||
setLyricShadowColor(defaultSetting['desktopLyric.style.lyricShadowColor'])
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initColors()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
destroyColors()
|
||||
})
|
||||
|
||||
return {
|
||||
lyric_unplay_color_ref,
|
||||
lyric_played_color_ref,
|
||||
lyric_shadow_color_ref,
|
||||
resetColor,
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'SettingDesktopLyric',
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const t = useI18n()
|
||||
|
||||
|
||||
const {
|
||||
lyric_unplay_color_ref,
|
||||
lyric_played_color_ref,
|
||||
lyric_shadow_color_ref,
|
||||
resetColor,
|
||||
} = useLyricColor()
|
||||
|
||||
const systemFontList = ref([])
|
||||
const fontList = computed(() => {
|
||||
return [{ id: '', label: t('setting__desktop_lyric_font_default') }, ...systemFontList.value]
|
||||
@@ -40,10 +219,52 @@ export default {
|
||||
})
|
||||
|
||||
return {
|
||||
currentStting,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
lyric_unplay_color_ref,
|
||||
lyric_played_color_ref,
|
||||
lyric_shadow_color_ref,
|
||||
resetColor,
|
||||
|
||||
fontList,
|
||||
isLinux,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.groupContent {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.item {
|
||||
padding-right: 40px;
|
||||
width: 70px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
.color {
|
||||
width: 80%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-color: var(--pcr-color);
|
||||
border-radius: @radius-border;
|
||||
cursor: pointer;
|
||||
transition: @transition-fast !important;
|
||||
transition-property: background-color, opacity !important;
|
||||
box-shadow: 0 0 3px var(--color-primary-light-100-alpha-300);
|
||||
&:hover {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
.label {
|
||||
.mixin-ellipsis-2;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template lang="pug">
|
||||
dt#download {{$t('setting__download')}}
|
||||
dd
|
||||
base-checkbox(id="setting_download_enable" v-model="currentStting.download.enable" :label="$t('setting__download_enable')")
|
||||
base-checkbox(id="setting_download_enable" :modelValue="appSetting['download.enable']" @update:modelValue="updateSetting({'download.enable': $event})" :label="$t('setting__download_enable')")
|
||||
dd(:aria-label="$t('setting__download_path_title')")
|
||||
h3#download_path {{$t('setting__download_path')}}
|
||||
div
|
||||
p
|
||||
| {{$t('setting__download_path_label')}}
|
||||
span.auto-hidden.hover(:class="$style.savePath" @click="openDirInExplorer(currentStting.download.savePath)" :aria-label="$t('setting__download_path_open_label')") {{currentStting.download.savePath}}
|
||||
span.auto-hidden.hover(:class="$style.savePath" @click="openDirInExplorer(appSetting['download.savePath'])" :aria-label="$t('setting__download_path_open_label')") {{appSetting['download.savePath']}}
|
||||
p
|
||||
base-btn.btn(min @click="handleChangeSavePath") {{$t('setting__download_path_change_btn')}}
|
||||
dd
|
||||
@@ -15,49 +15,55 @@ dd
|
||||
| {{$t('setting__download_use_other_source')}}
|
||||
svg-icon(class="help-icon" name="help-circle-outline" :aria-label="$t('setting__download_use_other_source_tip')")
|
||||
div
|
||||
base-checkbox(id="setting_download_isUseOtherSource" v-model="currentStting.download.isUseOtherSource" :label="$t('setting__is_enable')")
|
||||
base-checkbox(id="setting_download_isUseOtherSource" :modelValue="appSetting['download.isUseOtherSource']" @update:modelValue="updateSetting({'download.isUseOtherSource': $event})" :label="$t('setting__is_enable')")
|
||||
div
|
||||
dd(:aria-label="$t('setting__download_name_title')")
|
||||
h3#download_name {{$t('setting__download_name')}}
|
||||
div
|
||||
base-checkbox.gap-left(:id="`setting_download_musicName_${item.value}`" name="setting_download_musicName" :value="item.value" :key="item.value" need
|
||||
v-model="currentStting.download.fileName" v-for="item in musicNames" :label="item.name")
|
||||
:modelValue="appSetting['download.fileName']" @update:modelValue="updateSetting({'download.fileName': $event})" v-for="item in musicNames" :label="item.name")
|
||||
dd
|
||||
h3#download_data_embed {{$t('setting__download_data_embed')}}
|
||||
.gap-top
|
||||
base-checkbox(id="setting_download_isEmbedPic" v-model="currentStting.download.isEmbedPic" :label="$t('setting__download_embed_pic')")
|
||||
base-checkbox(id="setting_download_isEmbedPic" :modelValue="appSetting['download.isEmbedPic']" @update:modelValue="updateSetting({'download.isEmbedPic': $event})" :label="$t('setting__download_embed_pic')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_download_isEmbedLyric" v-model="currentStting.download.isEmbedLyric" :label="$t('setting__download_embed_lyric')")
|
||||
base-checkbox(id="setting_download_isEmbedLyric" :modelValue="appSetting['download.isEmbedLyric']" @update:modelValue="updateSetting({'download.isEmbedLyric': $event})" :label="$t('setting__download_embed_lyric')")
|
||||
dd(:aria-label="$t('setting__download_lyric_title')")
|
||||
h3#download_lyric {{$t('setting__download_lyric')}}
|
||||
div
|
||||
base-checkbox(id="setting_download_isDownloadLrc" v-model="currentStting.download.isDownloadLrc" :label="$t('setting__is_enable')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_download_isDownloadLrc" :modelValue="appSetting['download.isDownloadLrc']" @update:modelValue="updateSetting({'download.isDownloadLrc': $event})" :label="$t('setting__is_enable')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_download_isDownloadTLrc" :disabled="!appSetting['download.isDownloadLrc']" :modelValue="appSetting['download.isDownloadTLrc']" @update:modelValue="updateSetting({'download.isDownloadTLrc': $event})" :label="$t('setting__download_tlyric')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_download_isDownloadRLrc" :disabled="!appSetting['download.isDownloadLrc']" :modelValue="appSetting['download.isDownloadRLrc']" @update:modelValue="updateSetting({'download.isDownloadRLrc': $event})" :label="$t('setting__download_rlyric')")
|
||||
dd
|
||||
h3#download_lyric_format {{$t('setting__download_lyric_format')}}
|
||||
div
|
||||
base-checkbox.gap-left(v-for="item in lrcFormatList" :key="item.id" :id="`setting_download_lrcFormat_${item.id}`"
|
||||
name="setting_download_lrcFormat" need v-model="currentStting.download.lrcFormat" :value="item.id" :label="item.name")
|
||||
name="setting_download_lrcFormat" need :modelValue="appSetting['download.lrcFormat']" @update:modelValue="updateSetting({'download.lrcFormat': $event})" :value="item.id" :label="item.name")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, useI18n } from '@renderer/utils/vueTools'
|
||||
import { computed } from '@common/utils/vueTools'
|
||||
// import { getSystemFonts } from '@renderer/utils/tools'
|
||||
import { openDirInExplorer, selectDir } from '@renderer/utils'
|
||||
import { currentStting } from '../setting'
|
||||
import { openDirInExplorer } from '@common/utils/electron'
|
||||
import { showSelectDialog } from '@renderer/utils/ipc'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
export default {
|
||||
name: 'SettingDownload',
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const t = useI18n()
|
||||
|
||||
const handleChangeSavePath = () => {
|
||||
selectDir({
|
||||
showSelectDialog({
|
||||
title: t('setting__download_select_save_path'),
|
||||
defaultPath: currentStting.value.download.savePath,
|
||||
defaultPath: appSetting['download.savePath'],
|
||||
properties: ['openDirectory'],
|
||||
}).then(result => {
|
||||
if (result.canceled) return
|
||||
currentStting.value.download.savePath = result.filePaths[0]
|
||||
updateSetting({ 'download.savePath': result.filePaths[0] })
|
||||
})
|
||||
}
|
||||
|
||||
@@ -77,7 +83,8 @@ export default {
|
||||
})
|
||||
|
||||
return {
|
||||
currentStting,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
openDirInExplorer,
|
||||
handleChangeSavePath,
|
||||
musicNames,
|
||||
|
||||
@@ -25,13 +25,11 @@ dd
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, useI18n, onBeforeUnmount, toRaw } from '@renderer/utils/vueTools'
|
||||
import { allHotKeys, hotKeySetEnable, hotKeySetConfig, hotKeyGetStatus } from '@renderer/utils/tools'
|
||||
import { ref, onBeforeUnmount, toRaw, shallowReactive } from '@common/utils/vueTools'
|
||||
import { allHotKeys, hotKeySetEnable, hotKeySetConfig, hotKeyGetStatus } from '@renderer/utils/ipc'
|
||||
import { isMac } from '@common/utils'
|
||||
import { base as eventBaseName } from '@renderer/event/names'
|
||||
import { mainWindow as eventsNameMainWindow } from '@main/events/_name'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
|
||||
import { currentStting } from '../setting'
|
||||
|
||||
const formatHotKeyName = (name) => {
|
||||
if (name.includes('arrow')) {
|
||||
@@ -53,7 +51,7 @@ const formatHotKeyName = (name) => {
|
||||
export default {
|
||||
name: 'SettingHotKey',
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const t = useI18n()
|
||||
const current_hot_key = ref({
|
||||
local: {
|
||||
enable: false,
|
||||
@@ -74,17 +72,18 @@ export default {
|
||||
let isEditHotKey = false
|
||||
let hotKeyTargetInput
|
||||
let newHotKey
|
||||
let tip
|
||||
|
||||
const initHotKeyConfig = () => {
|
||||
let config = {}
|
||||
for (const [type, typeInfo] of Object.entries(current_hot_key.value)) {
|
||||
let configInfo = config[type] = {}
|
||||
for (const [key, info] of Object.entries(typeInfo.keys)) {
|
||||
if (info.name == null) continue
|
||||
configInfo[info.name] = {
|
||||
if (!info.name) continue
|
||||
configInfo[info.name] = shallowReactive({
|
||||
key,
|
||||
info,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
hotKeyConfig.value = config
|
||||
@@ -93,23 +92,23 @@ export default {
|
||||
const handleHotKeyFocus = (event, info, type) => {
|
||||
setTimeout(async() => {
|
||||
await hotKeySetEnable(false)
|
||||
window.isEditingHotKey = true
|
||||
window.lx.isEditingHotKey = true
|
||||
isEditHotKey = true
|
||||
let config = hotKeyConfig.value[type][info.name]
|
||||
newHotKey = config?.key
|
||||
hotKeyTargetInput = event.target
|
||||
event.target.value = t('setting__hot_key_tip_input')
|
||||
event.target.value = tip = t('setting__hot_key_tip_input')
|
||||
})
|
||||
}
|
||||
|
||||
const handleHotKeyBlur = (event, info, type) => {
|
||||
setTimeout(async() => {
|
||||
await hotKeySetEnable(true)
|
||||
window.isEditingHotKey = false
|
||||
window.lx.isEditingHotKey = false
|
||||
isEditHotKey = false
|
||||
const prevInput = hotKeyTargetInput
|
||||
hotKeyTargetInput = null
|
||||
if (prevInput?.value == t('setting__hot_key_tip_input')) {
|
||||
if (prevInput?.value == tip) {
|
||||
prevInput.value = newHotKey ? formatHotKeyName(newHotKey) : ''
|
||||
return
|
||||
}
|
||||
@@ -180,26 +179,23 @@ export default {
|
||||
// console.log(keys, key, type)
|
||||
newHotKey = key
|
||||
}
|
||||
const handleUpdateHotKeyConfig = (config) => {
|
||||
// console.log(config)
|
||||
for (const [type, info] of Object.entries(config)) {
|
||||
current_hot_key.value[type] = info
|
||||
}
|
||||
}
|
||||
// const handleUpdateHotKeyConfig = (config) => {
|
||||
// // console.log(config)
|
||||
// for (const [type, info] of Object.entries(config)) {
|
||||
// current_hot_key.value[type] = info
|
||||
// }
|
||||
// }
|
||||
const handleHotKeySaveConfig = async() => {
|
||||
// console.log(this.current_hot_key)
|
||||
window.appHotKeyConfig = current_hot_key.value
|
||||
await hotKeySetConfig({
|
||||
action: 'config',
|
||||
data: toRaw(current_hot_key.value),
|
||||
source: eventsNameMainWindow.name,
|
||||
})
|
||||
}
|
||||
const handleEnableHotKey = async() => {
|
||||
await hotKeySetConfig({
|
||||
action: 'enable',
|
||||
data: current_hot_key.value.global.enable,
|
||||
source: eventsNameMainWindow.name,
|
||||
})
|
||||
await handleHotKeySaveConfig()
|
||||
await getHotKeyStatus()
|
||||
@@ -212,20 +208,18 @@ export default {
|
||||
})
|
||||
}
|
||||
|
||||
current_hot_key.value = window.appHotKeyConfig
|
||||
current_hot_key.value = window.lx.appHotKeyConfig
|
||||
initHotKeyConfig()
|
||||
getHotKeyStatus()
|
||||
|
||||
window.eventHub.on(eventBaseName.set_hot_key_config, handleUpdateHotKeyConfig)
|
||||
window.eventHub.on(eventBaseName.key_down, handleKeyDown)
|
||||
window.app_event.on('keyDown', handleKeyDown)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.eventHub.off(eventBaseName.set_hot_key_config, handleUpdateHotKeyConfig)
|
||||
window.eventHub.off(eventBaseName.key_down, handleKeyDown)
|
||||
window.app_event.off('keyDown', handleKeyDown)
|
||||
})
|
||||
|
||||
return {
|
||||
currentStting,
|
||||
// appSetting,
|
||||
allHotKeys,
|
||||
current_hot_key,
|
||||
hotKeyConfig,
|
||||
@@ -248,7 +242,7 @@ export default {
|
||||
flex-flow: row wrap;
|
||||
// margin-top: -15px;
|
||||
margin-bottom: 15px;
|
||||
transition: opacity @transition-theme;
|
||||
transition: opacity @transition-normal;
|
||||
}
|
||||
.hotKeyItem {
|
||||
width: 30%;
|
||||
@@ -259,7 +253,7 @@ export default {
|
||||
.hotKeyItemTitle {
|
||||
.mixin-ellipsis-1;
|
||||
padding-bottom: 5px;
|
||||
color: @color-theme_2-font-label;
|
||||
color: var(--color-font-label);
|
||||
font-size: 12px;
|
||||
}
|
||||
.hotKeyItemInput {
|
||||
@@ -267,7 +261,7 @@ export default {
|
||||
box-sizing: border-box;
|
||||
// font-family: monospace;
|
||||
&:focus {
|
||||
background-color: @color-theme_2-active;
|
||||
background-color: var(--color-primary-background-active);
|
||||
text-decoration: none;
|
||||
}
|
||||
&::placeholder {
|
||||
@@ -308,11 +302,4 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.hotKeyItemTitle {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
})
|
||||
</style>
|
||||
|
||||
@@ -2,30 +2,33 @@
|
||||
dt#list {{$t('setting__list')}}
|
||||
dd
|
||||
.gap-top
|
||||
base-checkbox(id="setting_list_showSource_enable" v-model="currentStting.list.isShowSource" :label="$t('setting__list_source')")
|
||||
base-checkbox(id="setting_list_actionButtonsVisible_enable" :modelValue="appSetting['list.actionButtonsVisible']" @update:modelValue="updateSetting({'list.actionButtonsVisible': $event})" :label="$t('setting__list_action_btn')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_list_scroll_enable" v-model="currentStting.list.isSaveScrollLocation" :label="$t('setting__list_scroll')")
|
||||
base-checkbox(id="setting_list_showSource_enable" :modelValue="appSetting['list.isShowSource']" @update:modelValue="updateSetting({'list.isShowSource': $event})" :label="$t('setting__list_source')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_list_clickAction_enable" v-model="currentStting.list.isClickPlayList" :label="$t('setting__list_click_action')")
|
||||
base-checkbox(id="setting_list_scroll_enable" :modelValue="appSetting['list.isSaveScrollLocation']" @update:modelValue="updateSetting({'list.isSaveScrollLocation': $event})" :label="$t('setting__list_scroll')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_list_clickAction_enable" :modelValue="appSetting['list.isClickPlayList']" @update:modelValue="updateSetting({'list.isClickPlayList': $event})" :label="$t('setting__list_click_action')")
|
||||
dd(:aria-label="$t('setting__basic_sourcename_title')")
|
||||
h3#list_addMusicLocationType {{$t('setting__list_add_music_location_type')}}
|
||||
div
|
||||
base-checkbox.gap-left(id="setting_list_add_music_location_type_top"
|
||||
name="setting_list_add_music_location_type" need v-model="currentStting.list.addMusicLocationType" value="top" :label="$t('setting__list_add_music_location_type_top')")
|
||||
name="setting_list_add_music_location_type" need :modelValue="appSetting['list.addMusicLocationType']" @update:modelValue="updateSetting({'list.addMusicLocationType': $event})" value="top" :label="$t('setting__list_add_music_location_type_top')")
|
||||
base-checkbox.gap-left(id="setting_list_add_music_location_type_bottom"
|
||||
name="setting_list_add_music_location_type" need v-model="currentStting.list.addMusicLocationType" value="bottom" :label="$t('setting__list_add_music_location_type_bottom')")
|
||||
name="setting_list_add_music_location_type" need :modelValue="appSetting['list.addMusicLocationType']" @update:modelValue="updateSetting({'list.addMusicLocationType': $event})" value="bottom" :label="$t('setting__list_add_music_location_type_bottom')")
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import { ref, onBeforeUnmount } from '@renderer/utils/vueTools'
|
||||
import { currentStting } from '../setting'
|
||||
// import { ref, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
export default {
|
||||
name: 'SettingList',
|
||||
setup() {
|
||||
return {
|
||||
currentStting,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,53 +4,50 @@ dd
|
||||
h3#network_proxy_title {{$t('setting__network_proxy_title')}}
|
||||
div
|
||||
p
|
||||
base-checkbox(id="setting_network_proxy_enable" v-model="currentStting.network.proxy.enable" :label="$t('setting__is_enable')")
|
||||
base-checkbox(id="setting_network_proxy_enable" :modelValue="appSetting['network.proxy.enable']" @update:modelValue="updateSetting({'network.proxy.enable': $event})" :label="$t('setting__is_enable')")
|
||||
p
|
||||
base-input.gap-left(v-model.trim="currentStting.network.proxy.host" :placeholder="proxy.envProxy ? proxy.envProxy.host : $t('setting__network_proxy_host')")
|
||||
base-input.gap-left(v-model.trim="currentStting.network.proxy.port" :placeholder="proxy.envProxy ? proxy.envProxy.port : $t('setting__network_proxy_port')")
|
||||
base-input.gap-left(:modelValue="appSetting['network.proxy.host']" @update:modelValue="setHost" :placeholder="proxy.envProxy ? proxy.envProxy.host : $t('setting__network_proxy_host')")
|
||||
base-input.gap-left(:modelValue="appSetting['network.proxy.port']" @update:modelValue="setPort" :placeholder="proxy.envProxy ? proxy.envProxy.port : $t('setting__network_proxy_port')")
|
||||
p
|
||||
base-input.gap-left(v-model.trim="currentStting.network.proxy.username" :placeholder="$t('setting__network_proxy_username')")
|
||||
base-input.gap-left(v-model.trim="currentStting.network.proxy.password" type="password" :placeholder="$t('setting__network_proxy_password')")
|
||||
base-input.gap-left(:modelValue="appSetting['network.proxy.username']" @update:modelValue="setUserName" :placeholder="$t('setting__network_proxy_username')")
|
||||
base-input.gap-left(:modelValue="appSetting['network.proxy.password']" @update:modelValue="setPassword" type="password" :placeholder="$t('setting__network_proxy_password')")
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { watch, onBeforeUnmount } from '@renderer/utils/vueTools'
|
||||
import { proxy } from '@renderer/core/share'
|
||||
import { debounce } from '@renderer/utils'
|
||||
import { onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { proxy } from '@renderer/store'
|
||||
import { debounce } from '@common/utils'
|
||||
|
||||
import { currentStting } from '../setting'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
export default {
|
||||
name: 'SettingNetwork',
|
||||
setup() {
|
||||
watch(() => currentStting.value.network.proxy.enable, enable => {
|
||||
proxy.enable = enable
|
||||
})
|
||||
|
||||
const setHost = debounce(host => {
|
||||
proxy.host = host
|
||||
updateSetting({ 'network.proxy.host': host.trim() })
|
||||
}, 500)
|
||||
watch(() => currentStting.value.network.proxy.host, setHost)
|
||||
const setPort = debounce(port => {
|
||||
proxy.port = port
|
||||
updateSetting({ 'network.proxy.port': port.trim() })
|
||||
}, 500)
|
||||
watch(() => currentStting.value.network.proxy.port, setPort)
|
||||
const setUserName = debounce(username => {
|
||||
proxy.username = username
|
||||
updateSetting({ 'network.proxy.username': username.trim() })
|
||||
}, 500)
|
||||
watch(() => currentStting.value.network.proxy.username, setUserName)
|
||||
const setPassword = debounce(password => {
|
||||
proxy.password = password
|
||||
updateSetting({ 'network.proxy.password': password.trim() })
|
||||
}, 500)
|
||||
watch(() => currentStting.value.network.proxy.password, setPassword)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (currentStting.value.network.proxy.enable && !currentStting.value.network.proxy.host) proxy.enable = false
|
||||
if (appSetting['network.proxy.enable'] && !appSetting['network.proxy.host']) proxy.enable = false
|
||||
})
|
||||
|
||||
return {
|
||||
currentStting,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
setHost,
|
||||
setPort,
|
||||
setUserName,
|
||||
setPassword,
|
||||
proxy,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -2,20 +2,21 @@
|
||||
dt#odc {{$t('setting__odc')}}
|
||||
dd
|
||||
.gap-top
|
||||
base-checkbox(id="setting_odc_isAutoClearSearchInput" v-model="currentStting.odc.isAutoClearSearchInput" :label="$t('setting__odc_clear_search_input')")
|
||||
base-checkbox(id="setting_odc_isAutoClearSearchInput" :modelValue="appSetting['odc.isAutoClearSearchInput']" @update:modelValue="updateSetting({'odc.isAutoClearSearchInput': $event})" :label="$t('setting__odc_clear_search_input')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_odc_isAutoClearSearchList" v-model="currentStting.odc.isAutoClearSearchList" :label="$t('setting__odc_clear_search_list')")
|
||||
base-checkbox(id="setting_odc_isAutoClearSearchList" :modelValue="appSetting['odc.isAutoClearSearchList']" @update:modelValue="updateSetting({'odc.isAutoClearSearchList': $event})" :label="$t('setting__odc_clear_search_list')")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import { ref, onBeforeUnmount } from '@renderer/utils/vueTools'
|
||||
import { currentStting } from '../setting'
|
||||
// import { ref, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
export default {
|
||||
name: 'SettingOdc',
|
||||
setup() {
|
||||
return {
|
||||
currentStting,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ dt#other {{$t('setting__other')}}
|
||||
dd
|
||||
h3#other_tray_theme {{$t('setting__other_tray_theme')}}
|
||||
div
|
||||
base-checkbox.gap-left(:id="'setting_tray_theme_' + item.id" v-model="currentStting.tray.themeId" name="setting_tray_theme" need
|
||||
base-checkbox.gap-left(:id="'setting_tray_theme_' + item.id" :modelValue="appSetting['tray.themeId']" @update:modelValue="updateSetting({'tray.themeId': $event})" name="setting_tray_theme" need
|
||||
:label="item.label" :key="item.id" :value="item.id" v-for="item in trayThemeList")
|
||||
dd
|
||||
h3#other_resource_cache
|
||||
@@ -15,26 +15,53 @@ dd
|
||||
span.auto-hidden {{cacheSize}}
|
||||
p
|
||||
base-btn.btn(min :disabled="isDisabledResourceCacheClear" @click="clearResourceCache") {{$t('setting__other_resource_cache_clear_btn')}}
|
||||
dd
|
||||
h3#other_play_list_cache
|
||||
| {{$t('setting__other_play_list_cache')}}
|
||||
svg-icon(class="help-icon" name="help-circle-outline" :aria-label="$t('setting__other_play_list_cache_tip')")
|
||||
|
||||
dd
|
||||
h3#other_other_source {{$t('setting__other_other_cache')}}
|
||||
div
|
||||
base-btn.btn(min :disabled="isDisabledListCacheClear" @click="clearListCache") {{$t('setting__other_play_list_cache_clear_btn')}}
|
||||
p
|
||||
| {{$t('setting__other_other_source_label')}}
|
||||
span.auto-hidden {{otherSourceCount}}
|
||||
p
|
||||
| {{$t('setting__other_music_url_label')}}
|
||||
span.auto-hidden {{musicUrlCount}}
|
||||
p
|
||||
| {{$t('setting__other_lyric_raw_label')}}
|
||||
span.auto-hidden {{lyricRawCount}}
|
||||
p
|
||||
base-btn.btn(min :disabled="isDisabledOtherSourceCacheClear" @click="handleClearOtherSourceCache") {{$t('setting__other_other_source_clear_btn')}}
|
||||
base-btn.btn(min :disabled="isDisabledMusicUrlCacheClear" @click="handleClearMusicUrlCache") {{$t('setting__other_music_url_clear_btn')}}
|
||||
base-btn.btn(min :disabled="isDisabledLyricRawCacheClear" @click="handleClearLyricRawCache") {{$t('setting__other_lyric_raw_clear_btn')}}
|
||||
|
||||
dd
|
||||
h3#other_lyric_edited {{$t('setting__other_lyric_edited_cache')}}
|
||||
div
|
||||
p
|
||||
| {{$t('setting__other_lyric_edited_label')}}
|
||||
span.auto-hidden {{lyricEditedCount}}
|
||||
p
|
||||
base-btn.btn(min :disabled="isDisabledLyricEditedCacheClear" @click="handleClearLyricEditedCache") {{$t('setting__other_lyric_edited_clear_btn')}}
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, useI18n, useCommit } from '@renderer/utils/vueTools'
|
||||
import { sizeFormate, clearCache, getCacheSize } from '@renderer/utils'
|
||||
import { ref, computed } from '@common/utils/vueTools'
|
||||
import {
|
||||
clearCache, getCacheSize,
|
||||
getOtherSourceCount, clearOtherSource,
|
||||
getMusicUrlCount, clearMusicUrl,
|
||||
getLyricRawCount, clearLyricRaw,
|
||||
getLyricEditedCount, clearLyricEdited,
|
||||
} from '@renderer/utils/ipc'
|
||||
import { sizeFormate } from '@common/utils/common'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
|
||||
import { currentStting } from '../setting'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
export default {
|
||||
name: 'SettingOther',
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const t = useI18n()
|
||||
|
||||
const trayThemeList = computed(() => {
|
||||
return [
|
||||
@@ -46,7 +73,7 @@ export default {
|
||||
|
||||
const cacheSize = ref('0 B')
|
||||
const isDisabledResourceCacheClear = ref(false)
|
||||
const isDisabledListCacheClear = ref(false)
|
||||
// const isDisabledListCacheClear = ref(false)
|
||||
const refreshCacheSize = () => {
|
||||
getCacheSize().then(size => {
|
||||
cacheSize.value = sizeFormate(size)
|
||||
@@ -66,27 +93,102 @@ export default {
|
||||
}
|
||||
refreshCacheSize()
|
||||
|
||||
const clearMyListCache = useCommit('list', 'clearCache')
|
||||
const clearListCache = async() => {
|
||||
|
||||
const otherSourceCount = ref(0)
|
||||
const isDisabledOtherSourceCacheClear = ref(false)
|
||||
const refreshOtherSourceCount = () => {
|
||||
getOtherSourceCount().then(count => {
|
||||
otherSourceCount.value = count
|
||||
})
|
||||
}
|
||||
const handleClearOtherSourceCache = async() => {
|
||||
isDisabledOtherSourceCacheClear.value = true
|
||||
clearOtherSource().then(() => {
|
||||
refreshOtherSourceCount()
|
||||
isDisabledOtherSourceCacheClear.value = false
|
||||
})
|
||||
}
|
||||
refreshOtherSourceCount()
|
||||
|
||||
|
||||
const musicUrlCount = ref(0)
|
||||
const isDisabledMusicUrlCacheClear = ref(false)
|
||||
const refreshMusicUrlCount = () => {
|
||||
getMusicUrlCount().then(count => {
|
||||
musicUrlCount.value = count
|
||||
})
|
||||
}
|
||||
const handleClearMusicUrlCache = async() => {
|
||||
isDisabledMusicUrlCacheClear.value = true
|
||||
clearMusicUrl().then(() => {
|
||||
refreshMusicUrlCount()
|
||||
isDisabledMusicUrlCacheClear.value = false
|
||||
})
|
||||
}
|
||||
refreshMusicUrlCount()
|
||||
|
||||
|
||||
const lyricRawCount = ref(0)
|
||||
const isDisabledLyricRawCacheClear = ref(false)
|
||||
const refreshLyricRawCount = () => {
|
||||
getLyricRawCount().then(count => {
|
||||
lyricRawCount.value = count
|
||||
})
|
||||
}
|
||||
const handleClearLyricRawCache = async() => {
|
||||
isDisabledLyricRawCacheClear.value = true
|
||||
clearLyricRaw().then(() => {
|
||||
refreshLyricRawCount()
|
||||
isDisabledLyricRawCacheClear.value = false
|
||||
})
|
||||
}
|
||||
refreshLyricRawCount()
|
||||
|
||||
|
||||
const lyricEditedCount = ref(0)
|
||||
const isDisabledLyricEditedCacheClear = ref(false)
|
||||
const refreshLyricEditedCount = () => {
|
||||
getLyricEditedCount().then(count => {
|
||||
lyricEditedCount.value = count
|
||||
})
|
||||
}
|
||||
const handleClearLyricEditedCache = async() => {
|
||||
if (!await dialog.confirm({
|
||||
message: t('setting__other_play_list_cache_tip_confirm'),
|
||||
message: t('setting__other_lyric_edited_clear_tip_confirm'),
|
||||
cancelButtonText: t('cancel_button_text'),
|
||||
confirmButtonText: t('setting__other_resource_cache_confirm'),
|
||||
})) return
|
||||
|
||||
isDisabledListCacheClear.value = true
|
||||
clearMyListCache()
|
||||
isDisabledListCacheClear.value = false
|
||||
isDisabledLyricEditedCacheClear.value = true
|
||||
clearLyricEdited().then(() => {
|
||||
refreshLyricEditedCount()
|
||||
isDisabledLyricEditedCacheClear.value = false
|
||||
})
|
||||
}
|
||||
refreshLyricEditedCount()
|
||||
|
||||
return {
|
||||
currentStting,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
trayThemeList,
|
||||
cacheSize,
|
||||
isDisabledResourceCacheClear,
|
||||
isDisabledListCacheClear,
|
||||
clearResourceCache,
|
||||
clearListCache,
|
||||
|
||||
otherSourceCount,
|
||||
isDisabledOtherSourceCacheClear,
|
||||
handleClearOtherSourceCache,
|
||||
|
||||
musicUrlCount,
|
||||
isDisabledMusicUrlCacheClear,
|
||||
handleClearMusicUrlCache,
|
||||
|
||||
lyricRawCount,
|
||||
isDisabledLyricRawCacheClear,
|
||||
handleClearLyricRawCache,
|
||||
|
||||
lyricEditedCount,
|
||||
isDisabledLyricEditedCacheClear,
|
||||
handleClearLyricEditedCache,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,23 +2,25 @@
|
||||
dt#play {{$t('setting__play')}}
|
||||
dd
|
||||
.gap-top
|
||||
base-checkbox(id="setting_player_save_play_time" v-model="currentStting.player.isSavePlayTime" :label="$t('setting__play_save_play_time')")
|
||||
base-checkbox(id="setting_player_startup_auto_play" :modelValue="appSetting['player.startupAutoPlay']" @update:modelValue="updateSetting({'player.startupAutoPlay': $event})" :label="$t('setting__play_startup_auto_play')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_player_lyric_transition" v-model="currentStting.player.isShowLyricTranslation" :label="$t('setting__play_lyric_transition')")
|
||||
base-checkbox(id="setting_player_save_play_time" :modelValue="appSetting['player.isSavePlayTime']" @update:modelValue="updateSetting({'player.isSavePlayTime': $event})" :label="$t('setting__play_save_play_time')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_player_lyric_roma" v-model="currentStting.player.isShowLyricRoma" :label="$t('setting__play_lyric_roma')")
|
||||
base-checkbox(id="setting_player_lyric_transition" :modelValue="appSetting['player.isShowLyricTranslation']" @update:modelValue="updateSetting({'player.isShowLyricTranslation': $event})" :label="$t('setting__play_lyric_transition')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_player_auto_skip_on_error" v-model="currentStting.player.autoSkipOnError" :label="$t('setting__play_auto_skip_on_error')")
|
||||
base-checkbox(id="setting_player_lyric_roma" :modelValue="appSetting['player.isShowLyricRoma']" @update:modelValue="updateSetting({'player.isShowLyricRoma': $event})" :label="$t('setting__play_lyric_roma')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_player_lyric_s2t" v-model="currentStting.player.isS2t" :label="$t('setting__play_lyric_s2t')")
|
||||
base-checkbox(id="setting_player_auto_skip_on_error" :modelValue="appSetting['player.autoSkipOnError']" @update:modelValue="updateSetting({'player.autoSkipOnError': $event})" :label="$t('setting__play_auto_skip_on_error')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_player_lyric_play_lxlrc" v-model="currentStting.player.isPlayLxlrc" :label="$t('setting__play_lyric_lxlrc')")
|
||||
base-checkbox(id="setting_player_lyric_s2t" :modelValue="appSetting['player.isS2t']" @update:modelValue="updateSetting({'player.isS2t': $event})" :label="$t('setting__play_lyric_s2t')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_player_highQuality" v-model="currentStting.player.highQuality" :label="$t('setting__play_quality')")
|
||||
base-checkbox(id="setting_player_lyric_play_lxlrc" :modelValue="appSetting['player.isPlayLxlrc']" @update:modelValue="updateSetting({'player.isPlayLxlrc': $event})" :label="$t('setting__play_lyric_lxlrc')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_player_showTaskProgess" v-model="currentStting.player.isShowTaskProgess" :label="$t('setting__play_task_bar')")
|
||||
base-checkbox(id="setting_player_highQuality" :modelValue="appSetting['player.highQuality']" @update:modelValue="updateSetting({'player.highQuality': $event})" :label="$t('setting__play_quality')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_player_isMediaDeviceRemovedStopPlay" v-model="currentStting.player.isMediaDeviceRemovedStopPlay" :label="$t('setting__play_mediaDevice_remove_stop_play')")
|
||||
base-checkbox(id="setting_player_showTaskProgess" :modelValue="appSetting['player.isShowTaskProgess']" @update:modelValue="updateSetting({'player.isShowTaskProgess': $event})" :label="$t('setting__play_task_bar')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_player_isMediaDeviceRemovedStopPlay" :modelValue="appSetting['player.isMediaDeviceRemovedStopPlay']" @update:modelValue="updateSetting({'player.isMediaDeviceRemovedStopPlay': $event})" :label="$t('setting__play_mediaDevice_remove_stop_play')")
|
||||
dd(:aria-label="$t('setting__play_mediaDevice_title')")
|
||||
h3#play_mediaDevice {{$t('setting__play_mediaDevice')}}
|
||||
div
|
||||
@@ -26,17 +28,17 @@ dd(:aria-label="$t('setting__play_mediaDevice_title')")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onBeforeUnmount, watch, nextTick, useI18n } from '@renderer/utils/vueTools'
|
||||
import { setTaskBarProgress } from '@renderer/utils/tools'
|
||||
import { ref, onBeforeUnmount, watch } from '@common/utils/vueTools'
|
||||
import { hasInitedAnalyser } from '@renderer/plugins/player'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
import { currentStting } from '../setting'
|
||||
|
||||
export default {
|
||||
name: 'SettingPlay',
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const t = useI18n()
|
||||
|
||||
const mediaDevices = ref([])
|
||||
const getMediaDevice = async() => {
|
||||
@@ -52,44 +54,38 @@ export default {
|
||||
navigator.mediaDevices.removeEventListener('devicechange', getMediaDevice)
|
||||
})
|
||||
|
||||
watch(() => currentStting.value.player.isShowTaskProgess, val => {
|
||||
if (val) return
|
||||
nextTick(() => {
|
||||
setTaskBarProgress(-1, 'normal')
|
||||
})
|
||||
})
|
||||
|
||||
const mediaDeviceId = ref(currentStting.value.player.mediaDeviceId)
|
||||
const mediaDeviceId = ref(appSetting['player.mediaDeviceId'])
|
||||
const handleMediaDeviceIdChnage = async() => {
|
||||
if (hasInitedAnalyser()) {
|
||||
await dialog({
|
||||
message: t('setting__play_media_device_error_tip'),
|
||||
confirmButtonText: t('alert_button_text'),
|
||||
})
|
||||
mediaDeviceId.value = currentStting.value.player.mediaDeviceId
|
||||
} else if (currentStting.value.player.audioVisualization) {
|
||||
mediaDeviceId.value = appSetting['player.mediaDeviceId']
|
||||
} else if (appSetting['player.audioVisualization']) {
|
||||
const confirm = await dialog.confirm({
|
||||
message: t('setting__play_media_device_tip'),
|
||||
cancelButtonText: t('cancel_button_text'),
|
||||
confirmButtonText: t('confirm_button_text'),
|
||||
})
|
||||
if (confirm) {
|
||||
currentStting.value.player.audioVisualization = false
|
||||
currentStting.value.player.mediaDeviceId = mediaDeviceId.value
|
||||
appSetting['player.audioVisualization'] = false
|
||||
appSetting['player.mediaDeviceId'] = mediaDeviceId.value
|
||||
} else {
|
||||
mediaDeviceId.value = currentStting.value.player.mediaDeviceId
|
||||
mediaDeviceId.value = appSetting['player.mediaDeviceId']
|
||||
}
|
||||
} else {
|
||||
currentStting.value.player.mediaDeviceId = mediaDeviceId.value
|
||||
appSetting['player.mediaDeviceId'] = mediaDeviceId.value
|
||||
}
|
||||
}
|
||||
watch(() => currentStting.value.player.mediaDeviceId, val => {
|
||||
watch(() => appSetting['player.mediaDeviceId'], val => {
|
||||
mediaDeviceId.value = val
|
||||
})
|
||||
|
||||
|
||||
return {
|
||||
currentStting,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
mediaDevices,
|
||||
mediaDeviceId,
|
||||
handleMediaDeviceIdChnage,
|
||||
|
||||
@@ -2,28 +2,29 @@
|
||||
dt#play_detail {{$t('setting__play_detail')}}
|
||||
dd
|
||||
.gap-top
|
||||
base-checkbox(id="setting_play_detail_font_zoom_enable" v-model="currentStting.playDetail.isZoomActiveLrc" :label="$t('setting__play_detail_font_zoom')")
|
||||
base-checkbox(id="setting_play_detail_font_zoom_enable" :modelValue="appSetting['playDetail.isZoomActiveLrc']" @update:modelValue="updateSetting({'playDetail.isZoomActiveLrc': $event})" :label="$t('setting__play_detail_font_zoom')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_play_detail_lyric_progress_enable" v-model="currentStting.playDetail.isShowLyricProgressSetting" :label="$t('setting__play_detail_lyric_progress')")
|
||||
base-checkbox(id="setting_play_detail_lyric_progress_enable" :modelValue="appSetting['playDetail.isShowLyricProgressSetting']" @update:modelValue="updateSetting({'playDetail.isShowLyricProgressSetting': $event})" :label="$t('setting__play_detail_lyric_progress')")
|
||||
|
||||
dd
|
||||
h3#play_detail_align {{$t('setting__play_detail_align')}}
|
||||
div
|
||||
base-checkbox.gap-left(id="setting_play_detail_align_left" v-model="currentStting.playDetail.style.align" value="left" :label="$t('setting__play_detail_align_left')")
|
||||
base-checkbox.gap-left(id="setting_play_detail_align_center" v-model="currentStting.playDetail.style.align" value="center" :label="$t('setting__play_detail_align_center')")
|
||||
//- base-checkbox.gap-left(id="setting_play_detail_align_right" v-model="currentStting.playDetail.style.align" value="right" :label="$t('setting__play_detail_align_right')")
|
||||
base-checkbox.gap-left(id="setting_play_detail_align_left" :modelValue="appSetting['playDetail.style.align']" @update:modelValue="updateSetting({ 'playDetail.style.align': $event })" need value="left" :label="$t('setting__play_detail_align_left')")
|
||||
base-checkbox.gap-left(id="setting_play_detail_align_center" :modelValue="appSetting['playDetail.style.align']" @update:modelValue="updateSetting({ 'playDetail.style.align': $event })" need value="center" :label="$t('setting__play_detail_align_center')")
|
||||
base-checkbox.gap-left(id="setting_play_detail_align_right" :modelValue="appSetting['playDetail.style.align']" @update:modelValue="updateSetting({ 'playDetail.style.align': $event })" need value="right" :label="$t('setting__play_detail_align_right')")
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import { ref, onBeforeUnmount } from '@renderer/utils/vueTools'
|
||||
import { currentStting } from '../setting'
|
||||
// import { ref, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
export default {
|
||||
name: 'SettingPlayDetail',
|
||||
setup() {
|
||||
return {
|
||||
currentStting,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,23 +2,24 @@
|
||||
dt#search {{$t('setting__search')}}
|
||||
dd
|
||||
.gap-top
|
||||
base-checkbox(id="setting_search_showHot_enable" v-model="currentStting.search.isShowHotSearch" :label="$t('setting__search_hot')")
|
||||
base-checkbox(id="setting_search_showHot_enable" :modelValue="appSetting['search.isShowHotSearch']" @update:modelValue="updateSetting({'search.isShowHotSearch': $event})" :label="$t('setting__search_hot')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_search_showHistory_enable" v-model="currentStting.search.isShowHistorySearch" :label="$t('setting__search_history')")
|
||||
base-checkbox(id="setting_search_showHistory_enable" :modelValue="appSetting['search.isShowHistorySearch']" @update:modelValue="updateSetting({'search.isShowHistorySearch': $event})" :label="$t('setting__search_history')")
|
||||
.gap-top
|
||||
base-checkbox(id="setting_search_focusSearchBox_enable" v-model="currentStting.search.isFocusSearchBox" :label="$t('setting__search_focus_search_box')")
|
||||
base-checkbox(id="setting_search_focusSearchBox_enable" :modelValue="appSetting['search.isFocusSearchBox']" @update:modelValue="updateSetting({'search.isFocusSearchBox': $event})" :label="$t('setting__search_focus_search_box')")
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import { ref, onBeforeUnmount } from '@renderer/utils/vueTools'
|
||||
import { currentStting } from '../setting'
|
||||
// import { ref, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
export default {
|
||||
name: 'SettingSearch',
|
||||
setup() {
|
||||
return {
|
||||
currentStting,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ dt#sync
|
||||
button(class="help-btn" @click="openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq/sync')" :aria-label="$t('setting__sync_tip')")
|
||||
svg-icon(name="help-circle-outline")
|
||||
dd
|
||||
base-checkbox(id="setting_sync_enable" v-model="currentStting.sync.enable" :label="syncEnableTitle")
|
||||
base-checkbox(id="setting_sync_enable" :modelValue="appSetting['sync.enable']" @update:modelValue="updateSetting({ 'sync.enable': $event })" :label="syncEnableTitle")
|
||||
div
|
||||
p.small {{$t('setting__sync_auth_code', { code: sync.status.code || '' })}}
|
||||
p.small {{$t('setting__sync_address', { address: sync.status.address.join(', ') || '' })}}
|
||||
@@ -15,20 +15,22 @@ dd
|
||||
h3#sync_port {{$t('setting__sync_port')}}
|
||||
div
|
||||
p
|
||||
base-input.gap-left(v-model.trim="currentStting.sync.port" :placeholder="$t('setting__sync_port_tip')")
|
||||
base-input.gap-left(:modelValue="appSetting['sync.port']" @update:modelValue="setSyncPort" :placeholder="$t('setting__sync_port_tip')")
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, useI18n, watch } from '@renderer/utils/vueTools'
|
||||
import { sync } from '@renderer/core/share'
|
||||
import { refreshSyncCode, syncEnable } from '@renderer/utils/tools'
|
||||
import { debounce, openUrl } from '@renderer/utils'
|
||||
import { currentStting } from '../setting'
|
||||
import { computed } from '@common/utils/vueTools'
|
||||
import { sync } from '@renderer/store'
|
||||
import { sendSyncAction } from '@renderer/utils/ipc'
|
||||
import { openUrl } from '@common/utils/electron'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
import { debounce } from '@common/utils/common'
|
||||
|
||||
export default {
|
||||
name: 'SettingSync',
|
||||
setup() {
|
||||
const { t } = useI18n()
|
||||
const t = useI18n()
|
||||
|
||||
|
||||
const syncEnableTitle = computed(() => {
|
||||
@@ -47,28 +49,21 @@ export default {
|
||||
: ''
|
||||
})
|
||||
|
||||
watch(() => currentStting.value.sync.enable, enable => {
|
||||
syncEnable({
|
||||
enable,
|
||||
port: currentStting.value.sync.port,
|
||||
})
|
||||
sync.enable = enable
|
||||
})
|
||||
const refreshSyncCode = () => {
|
||||
sendSyncAction({ action: 'generate_code' })
|
||||
}
|
||||
|
||||
const setPort = debounce(port => {
|
||||
syncEnable({
|
||||
enable: currentStting.value.sync.enable,
|
||||
port,
|
||||
})
|
||||
sync.port = port
|
||||
const setSyncPort = debounce(port => {
|
||||
updateSetting({ 'sync.port': port.trim() })
|
||||
}, 500)
|
||||
watch(() => currentStting.value.sync.port, setPort)
|
||||
|
||||
|
||||
return {
|
||||
currentStting,
|
||||
appSetting,
|
||||
updateSetting,
|
||||
sync,
|
||||
syncEnableTitle,
|
||||
setSyncPort,
|
||||
syncDevices,
|
||||
refreshSyncCode,
|
||||
openUrl,
|
||||
|
||||
@@ -15,12 +15,12 @@ dd
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, useI18n } from '@renderer/utils/vueTools'
|
||||
import { versionInfo } from '@renderer/core/share'
|
||||
import { sizeFormate } from '@renderer/utils/tools'
|
||||
import { computed } from '@common/utils/vueTools'
|
||||
import { versionInfo } from '@renderer/store'
|
||||
import { sizeFormate } from '@common/utils/common'
|
||||
// import { openDirInExplorer, selectDir } from '@renderer/utils'
|
||||
import { currentStting } from '../setting'
|
||||
import { rendererSend, NAMES } from '@common/ipc'
|
||||
import { openDevTools } from '@renderer/utils/ipc'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
|
||||
export default {
|
||||
name: 'SettingUpdate',
|
||||
@@ -28,14 +28,14 @@ export default {
|
||||
let lastClickTime = 0
|
||||
let clickNum = 0
|
||||
|
||||
const { t } = useI18n()
|
||||
const t = useI18n()
|
||||
|
||||
const handleOPenDevTools = () => {
|
||||
if (window.performance.now() - lastClickTime > 1000) {
|
||||
if (clickNum > 0) clickNum = 0
|
||||
} else {
|
||||
if (clickNum > 6) {
|
||||
rendererSend(NAMES.mainWindow.open_dev_tools)
|
||||
if (clickNum > 4) {
|
||||
openDevTools()
|
||||
clickNum = 0
|
||||
return
|
||||
}
|
||||
@@ -55,7 +55,6 @@ export default {
|
||||
}
|
||||
|
||||
return {
|
||||
currentStting,
|
||||
versionInfo,
|
||||
downloadProgress,
|
||||
handleOPenDevTools,
|
||||
|
||||
@@ -0,0 +1,768 @@
|
||||
<template>
|
||||
<material-modal :show="modelValue" max-height="90%" teleport="#view" @close="handleCancel">
|
||||
<main :class="$style.main">
|
||||
<h2>{{ $t('theme_edit_modal__title') }}</h2>
|
||||
<div class="scroll" :class="$style.content">
|
||||
<div :class="[$style.group, $style.base]">
|
||||
<div :class="$style.groupContent">
|
||||
<div :class="$style.item">
|
||||
<div ref="primary_color_ref" :class="$style.color" />
|
||||
<div :class="$style.label">{{ $t('theme_edit_modal__primary') }}</div>
|
||||
</div>
|
||||
<div :class="$style.item">
|
||||
<div ref="app_bg_color_ref" :class="$style.color" />
|
||||
<div :class="$style.label">{{ $t('theme_edit_modal__app_bg') }}</div>
|
||||
</div>
|
||||
<div :class="$style.item">
|
||||
<div ref="aside_font_color_ref" :class="$style.color" />
|
||||
<div :class="$style.label">{{ $t('theme_edit_modal__aside_color') }}</div>
|
||||
</div>
|
||||
<div :class="$style.item">
|
||||
<div ref="main_bg_color_ref" :class="$style.color" />
|
||||
<div :class="$style.label">{{ $t('theme_edit_modal__main_bg') }}</div>
|
||||
</div>
|
||||
<div :class="[$style.item, $style.bg]">
|
||||
<div :class="[$style.bgImg, {[$style.hasBg]: !!bgImg}]" @click="selectBgImg">
|
||||
<img v-if="bgImg" loading="lazy" decoding="async" :class="$style.img" :src="bgImg" alt="Background Image">
|
||||
<svg-icon v-else :class="$style.icon" name="plus" />
|
||||
<button :class="$style.removeBtn" type="button" @click.stop="removeBgImg">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 212.982 212.982" space="preserve">
|
||||
<use xlink:href="#icon-delete" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div :class="$style.label">{{ $t('theme_edit_modal__bg_image') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.groupContent">
|
||||
<div :class="$style.group">
|
||||
<div :class="$style.groupTitle">
|
||||
<span :class="$style.title">{{ $t('theme_edit_modal__badge') }}</span>
|
||||
<span class="badge badge-theme-primary">{{ $t('tag__lossless') }}</span>
|
||||
<span class="badge badge-theme-secondary">{{ $t('tag__high_quality') }}</span>
|
||||
<span class="badge badge-theme-tertiary">kw</span>
|
||||
</div>
|
||||
<div :class="$style.groupContent">
|
||||
<div :class="$style.item">
|
||||
<div ref="badge_primary_color_ref" :class="$style.color" />
|
||||
<div :class="$style.label">{{ $t('theme_edit_modal__badge_primary') }}</div>
|
||||
</div>
|
||||
<div :class="$style.item">
|
||||
<div ref="badge_secondary_color_ref" :class="$style.color" />
|
||||
<div :class="$style.label">{{ $t('theme_edit_modal__badge_secondary') }}</div>
|
||||
</div>
|
||||
<div :class="$style.item">
|
||||
<div ref="badge_tertiary_color_ref" :class="$style.color" />
|
||||
<div :class="$style.label">{{ $t('theme_edit_modal__badge_tertiary') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.group">
|
||||
<div :class="$style.groupTitle">
|
||||
<span>{{ $t('theme_edit_modal__control_btn') }}</span>
|
||||
<div :class="$style.controlBtn">
|
||||
<button type="button" :class="$style.hide">
|
||||
<svg :class="$style.controlBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="80%" viewBox="0 0 30.727 30.727" space="preserve">
|
||||
<use xlink:href="#icon-window-hide" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" :class="$style.min">
|
||||
<svg :class="$style.controlBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-window-minimize" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" :class="$style.close">
|
||||
<svg :class="$style.controlBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-window-close" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.groupContent">
|
||||
<div :class="$style.item">
|
||||
<div ref="close_btn_color_ref" :class="$style.color" />
|
||||
<div :class="$style.label">{{ $t('theme_edit_modal__close_btn') }}</div>
|
||||
</div>
|
||||
<div :class="$style.item">
|
||||
<div ref="min_btn_color_ref" :class="$style.color" />
|
||||
<div :class="$style.label">{{ $t('theme_edit_modal__min_btn') }}</div>
|
||||
</div>
|
||||
<div :class="$style.item">
|
||||
<div ref="hide_btn_color_ref" :class="$style.color" />
|
||||
<div :class="$style.label">{{ $t('theme_edit_modal__hide_btn') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div :class="$style.note">
|
||||
<p>{{ $t('theme_selector_modal__title_tip') }}</p>
|
||||
</div> -->
|
||||
<div :class="$style.footer">
|
||||
<div :class="$style.subContent">
|
||||
<base-input v-model="themeName" :class="$style.input" :placeholder="$t('theme_selector_modal__theme_name')" />
|
||||
<base-checkbox id="theme_edit_modal__dark" v-model="isDark" :class="$style.checkbox" :label="$t('theme_edit_modal__dark')" @change="handleDark" />
|
||||
<base-checkbox id="theme_edit_modal__preview" v-model="preview" :class="$style.checkbox" :label="$t('theme_edit_modal__preview')" @change="handlePreview" />
|
||||
</div>
|
||||
<div :class="$style.subContent">
|
||||
<base-btn v-if="themeId" :class="$style.btn" @click="handleRemove">{{ $t('theme_edit_modal__remove') }}</base-btn>
|
||||
<base-btn v-if="themeId" :class="$style.btn" @click="handleSaveNew">{{ $t('theme_edit_modal__save_new') }}</base-btn>
|
||||
<!-- <base-btn :class="$style.btn" @click="handleCancel">{{ $t('btn_cancel') }}</base-btn> -->
|
||||
<base-btn :class="$style.btn" @click="handleSubmit">{{ $t('btn_save') }}</base-btn>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { joinPath, extname, copyFile, checkPath, createDir, removeFile, moveFile, basename } from '@common/utils/nodejs'
|
||||
import { nextTick, ref, watch } from '@common/utils/vueTools'
|
||||
import { applyTheme, buildThemeColors, getThemes, copyTheme } from '@renderer/store/utils'
|
||||
// import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
// import { applyTheme, getThemes } from '@renderer/store/utils'
|
||||
import { createThemeColors } from '@common/theme/utils'
|
||||
import useMainCoolor from './useMainCoolor'
|
||||
import useAppBgColor from './useAppBgColor'
|
||||
import useMainBgColor from './useMainBgColor'
|
||||
import useAsideFontColor from './useAsideFontColor'
|
||||
import useBadgePrimaryColor from './useBadgePrimaryColor'
|
||||
import useBadgeSecondaryColor from './useBadgeSecondaryColor'
|
||||
import useBadgeTertiaryColor from './useBadgeTertiaryColor'
|
||||
import useCloseBtnColor from './useCloseBtnColor'
|
||||
import useMinBtnColor from './useMinBtnColor'
|
||||
import useHideBtnColor from './useHideBtnColor'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
import { removeTheme, saveTheme, showSelectDialog } from '@renderer/utils/ipc'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
import { themeInfo } from '@renderer/store'
|
||||
|
||||
|
||||
export default {
|
||||
name: 'ThemeSelectorModal',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
themeId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'submit'],
|
||||
setup(props, { emit }) {
|
||||
const themeName = ref('')
|
||||
const isDark = ref(false)
|
||||
const preview = ref(false)
|
||||
const bgImg = ref('')
|
||||
let originBgName = ''
|
||||
let currentBgPath = ''
|
||||
|
||||
let theme
|
||||
|
||||
const getColor = (color, theme) => {
|
||||
return color.startsWith('var')
|
||||
? theme.config.themeColors[color.replace(/var\((.+)\)/, '$1')]
|
||||
: color
|
||||
}
|
||||
|
||||
const createPreview = () => {
|
||||
if (!preview.value) return
|
||||
window.setTheme(buildThemeColors(theme, themeInfo.dataPath))
|
||||
}
|
||||
|
||||
// '--color-app-background': string
|
||||
// '--color-main-background': string
|
||||
// '--color-nav-font': string
|
||||
// '--background-image': string
|
||||
// '--background-image-position': string
|
||||
// '--background-image-size': string
|
||||
|
||||
// // 关闭按钮颜色
|
||||
// '--color-btn-hide': string
|
||||
// '--color-btn-min': string
|
||||
// '--color-btn-close': string
|
||||
|
||||
// // 徽章颜色
|
||||
// '--color-badge-primary': string
|
||||
// '--color-badge-secondary': string
|
||||
// '--color-badge-tertiary': string
|
||||
const { primary_color_ref, initMainColor, destroyMainColor } = useMainCoolor()
|
||||
const { app_bg_color_ref, initAppBgColor, destroyAppBgColor, setAppBgColor } = useAppBgColor()
|
||||
const { aside_font_color_ref, initAsideFontColor, destroyAsideFontColor, setAsideFontColor } = useAsideFontColor()
|
||||
const { main_bg_color_ref, initMainBgColor, destroyMainBgColor, setMainBgColor } = useMainBgColor()
|
||||
const { badge_primary_color_ref, initBadgePrimaryColor, destroyBadgePrimaryColor, setBadgePrimaryColor } = useBadgePrimaryColor()
|
||||
const { badge_secondary_color_ref, initBadgeSecondaryColor, destroyBadgeSecondaryColor, setBadgeSecondaryColor } = useBadgeSecondaryColor()
|
||||
const { badge_tertiary_color_ref, initBadgeTertiaryColor, destroyBadgeTertiaryColor, setBadgeTertiaryColor } = useBadgeTertiaryColor()
|
||||
const { close_btn_color_ref, initCloseBtnColor, destroyCloseBtnColor, setCloseBtnColor } = useCloseBtnColor()
|
||||
const { min_btn_color_ref, initMinBtnColor, destroyMinBtnColor, setMinBtnColor } = useMinBtnColor()
|
||||
const { hide_btn_color_ref, initHideBtnColor, destroyHideBtnColor, setHideBtnColor } = useHideBtnColor()
|
||||
|
||||
let appBgColorOrigin
|
||||
let appBgColor
|
||||
let asideFontColorOrigin
|
||||
let asideFontColor
|
||||
let mainBgColorOrigin
|
||||
let mainBgColor
|
||||
let badgePrimaryColorOrigin
|
||||
let badgePrimaryColor
|
||||
let badgeSecondaryColorOrigin
|
||||
let badgeSecondaryColor
|
||||
let badgeTertiaryColorOrigin
|
||||
let badgeTertiaryColor
|
||||
let closeBtnColorOrigin
|
||||
let closeBtnColor
|
||||
let minBtnColorOrigin
|
||||
let minBtnColor
|
||||
let hideBtnColorOrigin
|
||||
let hideBtnColor
|
||||
|
||||
const applyPrimaryColor = (color, isDark) => {
|
||||
theme.config.themeColors = createThemeColors(color, isDark)
|
||||
if (theme.config.extInfo['--color-app-background'].startsWith('var')) setAppBgColor(getColor(appBgColorOrigin, theme))
|
||||
if (theme.config.extInfo['--color-nav-font'].startsWith('var')) setAsideFontColor(getColor(asideFontColorOrigin, theme))
|
||||
if (theme.config.extInfo['--color-main-background'].startsWith('var')) setMainBgColor(getColor(mainBgColorOrigin, theme))
|
||||
if (theme.config.extInfo['--color-badge-primary'].startsWith('var')) setBadgePrimaryColor(getColor(badgePrimaryColorOrigin, theme))
|
||||
if (theme.config.extInfo['--color-badge-secondary'].startsWith('var')) setBadgeSecondaryColor(getColor(badgeSecondaryColorOrigin, theme))
|
||||
if (theme.config.extInfo['--color-badge-tertiary'].startsWith('var')) setBadgeTertiaryColor(getColor(badgeTertiaryColorOrigin, theme))
|
||||
if (theme.config.extInfo['--color-btn-close'].startsWith('var')) setCloseBtnColor(getColor(closeBtnColorOrigin, theme))
|
||||
if (theme.config.extInfo['--color-btn-min'].startsWith('var')) setMinBtnColor(getColor(minBtnColorOrigin, theme))
|
||||
if (theme.config.extInfo['--color-btn-hide'].startsWith('var')) setHideBtnColor(getColor(hideBtnColorOrigin, theme))
|
||||
|
||||
createPreview()
|
||||
}
|
||||
|
||||
const initColors = (_theme) => {
|
||||
theme = _theme
|
||||
// console.log(theme)
|
||||
themeName.value = theme.name
|
||||
isDark.value = theme.isDark
|
||||
currentBgPath = ''
|
||||
if (theme.config.extInfo['--background-image'] == 'none') {
|
||||
bgImg.value = ''
|
||||
originBgName = ''
|
||||
} else {
|
||||
bgImg.value = joinPath(themeInfo.dataPath, theme.config.extInfo['--background-image'])
|
||||
originBgName = theme.config.extInfo['--background-image']
|
||||
}
|
||||
appBgColorOrigin = theme.config.extInfo['--color-app-background']
|
||||
appBgColor = getColor(appBgColorOrigin, theme)
|
||||
asideFontColorOrigin = theme.config.extInfo['--color-nav-font']
|
||||
asideFontColor = getColor(asideFontColorOrigin, theme)
|
||||
mainBgColorOrigin = theme.config.extInfo['--color-main-background']
|
||||
mainBgColor = getColor(mainBgColorOrigin, theme)
|
||||
badgePrimaryColorOrigin = theme.config.extInfo['--color-badge-primary']
|
||||
badgePrimaryColor = getColor(badgePrimaryColorOrigin, theme)
|
||||
badgeSecondaryColorOrigin = theme.config.extInfo['--color-badge-secondary']
|
||||
badgeSecondaryColor = getColor(badgeSecondaryColorOrigin, theme)
|
||||
badgeTertiaryColorOrigin = theme.config.extInfo['--color-badge-tertiary']
|
||||
badgeTertiaryColor = getColor(badgeTertiaryColorOrigin, theme)
|
||||
closeBtnColorOrigin = theme.config.extInfo['--color-btn-close']
|
||||
closeBtnColor = getColor(closeBtnColorOrigin, theme)
|
||||
minBtnColorOrigin = theme.config.extInfo['--color-btn-min']
|
||||
minBtnColor = getColor(minBtnColorOrigin, theme)
|
||||
hideBtnColorOrigin = theme.config.extInfo['--color-btn-hide']
|
||||
hideBtnColor = getColor(hideBtnColorOrigin, theme)
|
||||
|
||||
initMainColor(theme.config.themeColors['--color-primary'], (color) => {
|
||||
// console.log(color)
|
||||
applyPrimaryColor(color, theme.isDark)
|
||||
})
|
||||
initAppBgColor(appBgColor, (color) => {
|
||||
// console.log('appBgColor', color)
|
||||
theme.config.extInfo['--color-app-background'] = color == appBgColor ? appBgColorOrigin : color
|
||||
createPreview()
|
||||
}, () => setAppBgColor(getColor(appBgColorOrigin, theme)))
|
||||
initAsideFontColor(asideFontColor, (color) => {
|
||||
theme.config.extInfo['--color-nav-font'] = color == asideFontColor ? asideFontColorOrigin : color
|
||||
createPreview()
|
||||
}, () => setAsideFontColor(getColor(asideFontColorOrigin, theme)))
|
||||
initMainBgColor(mainBgColor, (color) => {
|
||||
theme.config.extInfo['--color-main-background'] = color == mainBgColor ? mainBgColorOrigin : color
|
||||
createPreview()
|
||||
}, () => setMainBgColor(getColor(mainBgColorOrigin, theme)))
|
||||
initBadgePrimaryColor(badgePrimaryColor, (color) => {
|
||||
theme.config.extInfo['--color-badge-primary'] = color == badgePrimaryColor ? badgePrimaryColorOrigin : color
|
||||
createPreview()
|
||||
}, () => setBadgePrimaryColor(getColor(badgePrimaryColorOrigin, theme)))
|
||||
initBadgeSecondaryColor(badgeSecondaryColor, (color) => {
|
||||
theme.config.extInfo['--color-badge-secondary'] = color == badgeSecondaryColor ? badgeSecondaryColorOrigin : color
|
||||
createPreview()
|
||||
}, () => setBadgeSecondaryColor(getColor(badgeSecondaryColorOrigin, theme)))
|
||||
initBadgeTertiaryColor(badgeTertiaryColor, (color) => {
|
||||
theme.config.extInfo['--color-badge-tertiary'] = color == badgeTertiaryColor ? badgeTertiaryColorOrigin : color
|
||||
createPreview()
|
||||
}, () => setBadgeTertiaryColor(getColor(badgeTertiaryColorOrigin, theme)))
|
||||
initCloseBtnColor(closeBtnColor, (color) => {
|
||||
theme.config.extInfo['--color-btn-close'] = color == closeBtnColor ? closeBtnColorOrigin : color
|
||||
createPreview()
|
||||
}, () => setCloseBtnColor(getColor(closeBtnColorOrigin, theme)))
|
||||
initMinBtnColor(minBtnColor, (color) => {
|
||||
theme.config.extInfo['--color-btn-min'] = color == minBtnColor ? minBtnColorOrigin : color
|
||||
createPreview()
|
||||
}, () => setMinBtnColor(getColor(minBtnColorOrigin, theme)))
|
||||
initHideBtnColor(hideBtnColor, (color) => {
|
||||
theme.config.extInfo['--color-btn-hide'] = color == hideBtnColor ? hideBtnColorOrigin : color
|
||||
createPreview()
|
||||
}, () => setHideBtnColor(getColor(hideBtnColorOrigin, theme)))
|
||||
|
||||
createPreview()
|
||||
}
|
||||
const destroyColors = () => {
|
||||
destroyMainColor()
|
||||
destroyAppBgColor()
|
||||
destroyAsideFontColor()
|
||||
destroyMainBgColor()
|
||||
destroyBadgePrimaryColor()
|
||||
destroyBadgeSecondaryColor()
|
||||
destroyBadgeTertiaryColor()
|
||||
destroyCloseBtnColor()
|
||||
destroyMinBtnColor()
|
||||
destroyHideBtnColor()
|
||||
}
|
||||
|
||||
watch(() => props.modelValue, (visible) => {
|
||||
nextTick(() => {
|
||||
getThemes(({ themes, userThemes }) => {
|
||||
if (visible) {
|
||||
if (props.themeId) {
|
||||
const theme = userThemes.find(t => t.id == props.themeId)
|
||||
if (theme) {
|
||||
initColors(copyTheme(theme))
|
||||
return
|
||||
}
|
||||
}
|
||||
const theme = copyTheme(themes[0])
|
||||
theme.id = 'user_theme_' + Date.now()
|
||||
theme.name = ''
|
||||
theme.isCustom = true
|
||||
initColors(theme)
|
||||
} else {
|
||||
destroyColors()
|
||||
// 移除临时保存的背景
|
||||
if (currentBgPath) removeFile(currentBgPath).catch(_ => _)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const selectBgImg = async() => {
|
||||
const result = await showSelectDialog({
|
||||
title: window.i18n.t('theme_edit_modal__select_bg_file'),
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: 'Image File',
|
||||
extensions: [
|
||||
'jpg', 'jpeg', 'jfif', 'pjpeg',
|
||||
'pjp', 'png', 'apng', 'avif', 'gif', 'svg',
|
||||
'webp', 'bmp'],
|
||||
},
|
||||
],
|
||||
})
|
||||
if (result.canceled) return
|
||||
const path = result.filePaths[0]
|
||||
const fileName = `${theme.id}_${Date.now()}${extname(path)}`
|
||||
const tempDir = joinPath(themeInfo.dataPath, 'temp')
|
||||
const bgPath = joinPath(tempDir, fileName)
|
||||
if (!await checkPath(tempDir)) await createDir(tempDir)
|
||||
await copyFile(path, bgPath)
|
||||
currentBgPath = bgImg.value = bgPath
|
||||
theme.config.extInfo['--background-image'] = 'temp/' + fileName
|
||||
|
||||
createPreview()
|
||||
}
|
||||
const removeBgImg = async() => {
|
||||
if (currentBgPath) {
|
||||
removeFile(currentBgPath)
|
||||
currentBgPath = ''
|
||||
}
|
||||
bgImg.value = ''
|
||||
theme.config.extInfo['--background-image'] = 'none'
|
||||
createPreview()
|
||||
}
|
||||
const handleDark = (val) => {
|
||||
theme.isDark = val
|
||||
applyPrimaryColor(theme.config.themeColors['--color-primary'], theme.isDark)
|
||||
}
|
||||
const handlePreview = (val) => {
|
||||
if (val) {
|
||||
createPreview()
|
||||
} else {
|
||||
applyTheme(appSetting['theme.id'], appSetting['theme.lightId'], appSetting['theme.darkId'], themeInfo.dataPath)
|
||||
}
|
||||
}
|
||||
const handleCancel = () => {
|
||||
handlePreview(false)
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
const handleSubmit = async() => {
|
||||
if (!themeName.value) return
|
||||
theme.name = themeName.value.substring(0, 20)
|
||||
// 保存新背景
|
||||
if (currentBgPath) {
|
||||
const name = basename(currentBgPath)
|
||||
await moveFile(currentBgPath, joinPath(themeInfo.dataPath, name))
|
||||
theme.config.extInfo['--background-image'] = name
|
||||
}
|
||||
// 移除旧背景
|
||||
if (originBgName && theme.config.extInfo['--background-image'] != originBgName) removeFile(joinPath(themeInfo.dataPath, originBgName))
|
||||
if (props.themeId) {
|
||||
const index = themeInfo.userThemes.findIndex(t => t.id == theme.id)
|
||||
if (index > -1) themeInfo.userThemes.splice(index, 1, theme)
|
||||
} else themeInfo.userThemes.push(theme)
|
||||
handlePreview(false)
|
||||
await saveTheme(theme)
|
||||
emit('submit')
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
const handleRemove = async() => {
|
||||
const confirm = await dialog.confirm({
|
||||
message: window.i18n.t('theme_edit_modal__remove_tip'),
|
||||
cancelButtonText: window.i18n.t('cancel_button_text'),
|
||||
confirmButtonText: window.i18n.t('confirm_button_text'),
|
||||
})
|
||||
if (!confirm) return
|
||||
let isRequireUpdateSetting = false
|
||||
const newSetting = {}
|
||||
if (appSetting['theme.id'] == props.themeId) {
|
||||
newSetting['theme.id'] = 'green'
|
||||
isRequireUpdateSetting = true
|
||||
}
|
||||
if (theme.isDark) {
|
||||
if (appSetting['theme.darkId'] == props.themeId) {
|
||||
newSetting['theme.darkId'] = 'black'
|
||||
isRequireUpdateSetting = true
|
||||
}
|
||||
} else {
|
||||
if (appSetting['theme.lightId'] == props.themeId) {
|
||||
newSetting['theme.lightId'] = 'green'
|
||||
isRequireUpdateSetting = true
|
||||
}
|
||||
}
|
||||
if (isRequireUpdateSetting) updateSetting(newSetting)
|
||||
if (originBgName) removeFile(joinPath(themeInfo.dataPath, originBgName))
|
||||
await removeTheme(props.themeId)
|
||||
const index = themeInfo.userThemes.findIndex(t => t.id == theme.id)
|
||||
console.log(index)
|
||||
if (index > -1) themeInfo.userThemes.splice(index, 1)
|
||||
handlePreview(false)
|
||||
emit('submit')
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
const handleSaveNew = async() => {
|
||||
if (!themeName.value) return
|
||||
theme.name = themeName.value.substring(0, 20)
|
||||
theme.id = 'user_theme_' + Date.now()
|
||||
// 保存新背景
|
||||
if (currentBgPath) {
|
||||
const name = basename(currentBgPath)
|
||||
await moveFile(currentBgPath, joinPath(themeInfo.dataPath, name))
|
||||
theme.config.extInfo['--background-image'] = name
|
||||
} else if (bgImg.value) {
|
||||
const fileName = `${theme.id}_${Date.now()}${extname(bgImg.value)}`
|
||||
await copyFile(bgImg.value, joinPath(themeInfo.dataPath, fileName))
|
||||
theme.config.extInfo['--background-image'] = fileName
|
||||
}
|
||||
themeInfo.userThemes.push(theme)
|
||||
handlePreview(false)
|
||||
await saveTheme(theme)
|
||||
emit('submit')
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
return {
|
||||
themeName,
|
||||
bgImg,
|
||||
isDark,
|
||||
handleDark,
|
||||
preview,
|
||||
handlePreview,
|
||||
handleCancel,
|
||||
handleSubmit,
|
||||
handleRemove,
|
||||
handleSaveNew,
|
||||
selectBgImg,
|
||||
removeBgImg,
|
||||
|
||||
primary_color_ref,
|
||||
app_bg_color_ref,
|
||||
main_bg_color_ref,
|
||||
aside_font_color_ref,
|
||||
badge_primary_color_ref,
|
||||
badge_secondary_color_ref,
|
||||
badge_tertiary_color_ref,
|
||||
close_btn_color_ref,
|
||||
min_btn_color_ref,
|
||||
hide_btn_color_ref,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
.main {
|
||||
// padding: 15px;
|
||||
// max-width: 400px;
|
||||
min-width: 300px;
|
||||
min-height: 0;
|
||||
// max-height: 100%;
|
||||
// overflow: hidden;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
h2 {
|
||||
flex: none;
|
||||
font-size: 16px;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
padding-bottom: 15px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.content {
|
||||
flex: auto;
|
||||
padding: 15px 0;
|
||||
font-size: 14px;
|
||||
gap: 5px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.group {
|
||||
|
||||
}
|
||||
.groupTitle {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
|
||||
.title {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
.groupContent {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.item {
|
||||
padding: 0 20px;
|
||||
width: 60px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
.base {
|
||||
.color {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.color {
|
||||
width: 80%;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-color: var(--pcr-color);
|
||||
border-radius: @radius-border;
|
||||
cursor: pointer;
|
||||
transition: @transition-fast !important;
|
||||
transition-property: background-color, opacity !important;
|
||||
box-shadow: 0 0 3px var(--color-primary-light-100-alpha-300);
|
||||
&:hover {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
.label {
|
||||
.mixin-ellipsis-2;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.bg {
|
||||
width: 0;
|
||||
flex: 1 1 auto;
|
||||
// width: 180px;
|
||||
min-width: 60px;
|
||||
max-width: 200px;
|
||||
}
|
||||
.bgImg {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
border: 1Px dashed var(--color-primary-light-100-alpha-300);
|
||||
transition: .3s ease;
|
||||
transition-property: border, color;
|
||||
box-sizing: border-box;
|
||||
border: 1px dashed var(--color-primary-light-100-alpha-300);
|
||||
color: var(--color-primary-light-100-alpha-300);
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: @transition-fast !important;
|
||||
transition-property: background-color, opacity !important;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
&.hasBg {
|
||||
border: none;
|
||||
|
||||
.removeBtn {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.removeBtn {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 6px 8px;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
outline: none;
|
||||
transition: background-color 0.2s ease;
|
||||
line-height: 0;
|
||||
display: none;
|
||||
|
||||
svg {
|
||||
height: .7em;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
&:active {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
}
|
||||
.icon {
|
||||
// position: absolute;
|
||||
// font-size: 16px;
|
||||
width: auto;
|
||||
height: 66%;
|
||||
}
|
||||
}
|
||||
|
||||
@control-btn-width: @height-toolbar * .26;
|
||||
.controlBtn {
|
||||
display: flex;
|
||||
-webkit-app-region: no-drag;
|
||||
align-items: center;
|
||||
padding: 0 @control-btn-width;
|
||||
flex-direction: row-reverse;
|
||||
// height: @height-toolbar * .7;
|
||||
transition: opacity @transition-normal;
|
||||
opacity: .5;
|
||||
&:hover {
|
||||
opacity: .8;
|
||||
.controlBtnIcon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
position: relative;
|
||||
background: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
padding: 1px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: @control-btn-width;
|
||||
height: @control-btn-width;
|
||||
border-radius: 50%;
|
||||
color: var(--color-font);
|
||||
+ button {
|
||||
margin-right: (@control-btn-width / 2);
|
||||
}
|
||||
|
||||
&.hide {
|
||||
background-color: var(--color-btn-hide);
|
||||
}
|
||||
&.min, &.fullscreenExit {
|
||||
background-color: var(--color-btn-min);
|
||||
}
|
||||
// &.max {
|
||||
// background-color: var(--color-btn-max);
|
||||
// }
|
||||
&.close {
|
||||
background-color: var(--color-btn-close);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controlBtnIcon {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.note {
|
||||
padding: 8px 15px;
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
color: var(--color-font);
|
||||
// p {
|
||||
// + p {
|
||||
// margin-top: 5px;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
.footer {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 15px;
|
||||
font-size: 14px;
|
||||
.subContent {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
flex: none;
|
||||
}
|
||||
.input {
|
||||
min-width: 0;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.btn {
|
||||
// box-sizing: border-box;
|
||||
// margin-left: 15px;
|
||||
// margin-bottom: 15px;
|
||||
// height: 36px;
|
||||
// line-height: 36px;
|
||||
// padding: 0 10px !important;
|
||||
min-width: 70px;
|
||||
// .mixin-ellipsis-1;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
|
||||
|
||||
export default () => {
|
||||
const app_bg_color_ref = ref(null)
|
||||
let tools: PickrTools | null
|
||||
|
||||
const initAppBgColor = (color: string, changed: (color: string) => void, reset: () => void) => {
|
||||
if (!app_bg_color_ref.value) return
|
||||
tools = pickrTools.create(app_bg_color_ref.value, color, [
|
||||
'rgba(255, 255, 255, 0)',
|
||||
'rgba(255, 255, 255, 0.15)',
|
||||
'rgba(255, 255, 255, 0.15)',
|
||||
'rgba(21.34, 18.92, 44.61, 0.81)',
|
||||
], changed, reset)
|
||||
}
|
||||
const destroyAppBgColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setAppBgColor = (color: string) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
app_bg_color_ref,
|
||||
initAppBgColor,
|
||||
destroyAppBgColor,
|
||||
setAppBgColor,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
|
||||
|
||||
export default () => {
|
||||
const aside_font_color_ref = ref(null)
|
||||
let tools: PickrTools | null
|
||||
|
||||
const initAsideFontColor = (color: string, changed: (color: string) => void, reset: () => void) => {
|
||||
if (!aside_font_color_ref.value) return
|
||||
tools = pickrTools.create(aside_font_color_ref.value, color, null, changed, reset)
|
||||
}
|
||||
const destroyAsideFontColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setAsideFontColor = (color: string) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
aside_font_color_ref,
|
||||
initAsideFontColor,
|
||||
destroyAsideFontColor,
|
||||
setAsideFontColor,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
|
||||
|
||||
export default () => {
|
||||
const badge_primary_color_ref = ref(null)
|
||||
let tools: PickrTools | null
|
||||
|
||||
const initBadgePrimaryColor = (color: string, changed: (color: string) => void, reset: () => void) => {
|
||||
if (!badge_primary_color_ref.value) return
|
||||
tools = pickrTools.create(badge_primary_color_ref.value, color, null, changed, reset)
|
||||
}
|
||||
const destroyBadgePrimaryColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setBadgePrimaryColor = (color: string) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
badge_primary_color_ref,
|
||||
initBadgePrimaryColor,
|
||||
destroyBadgePrimaryColor,
|
||||
setBadgePrimaryColor,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
|
||||
|
||||
export default () => {
|
||||
const badge_secondary_color_ref = ref(null)
|
||||
let tools: PickrTools | null
|
||||
|
||||
const initBadgeSecondaryColor = (color: string, changed: (color: string) => void, reset: () => void) => {
|
||||
if (!badge_secondary_color_ref.value) return
|
||||
tools = pickrTools.create(badge_secondary_color_ref.value, color, [
|
||||
'rgba(75, 174, 213, 1)',
|
||||
'rgba(92, 191, 155, 1)',
|
||||
'rgba(66, 6, 150, 7, 171, 1)',
|
||||
'rgba(158, 212, 88, 1)',
|
||||
'rgba(223, 187, 107, 1)',
|
||||
'rgba(245, 182, 132, 1)',
|
||||
'rgba(229, 163, 159, 1)',
|
||||
'rgba(177, 155, 159, 1)',
|
||||
'rgba(99, 118, 162, 1)',
|
||||
'rgba(176, 128, 219, 1)',
|
||||
'rgba(175, 148, 121, 1)',
|
||||
'rgba(223, 187, 107, 1)',
|
||||
'rgba(53.08, 107.67, 129.18, 1)',
|
||||
], changed, reset)
|
||||
}
|
||||
const destroyBadgeSecondaryColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setBadgeSecondaryColor = (color: string) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
badge_secondary_color_ref,
|
||||
initBadgeSecondaryColor,
|
||||
destroyBadgeSecondaryColor,
|
||||
setBadgeSecondaryColor,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
|
||||
|
||||
export default () => {
|
||||
const badge_tertiary_color_ref = ref(null)
|
||||
let tools: PickrTools | null
|
||||
|
||||
const initBadgeTertiaryColor = (color: string, changed: (color: string) => void, reset: () => void) => {
|
||||
if (!badge_tertiary_color_ref.value) return
|
||||
tools = pickrTools.create(badge_tertiary_color_ref.value, color, [
|
||||
'rgba(231, 170, 54, 1)',
|
||||
'rgba(92, 191, 155, 1)',
|
||||
'rgba(54, 196, 231, 1)',
|
||||
'rgba(158, 212, 88, 1)',
|
||||
'rgba(223, 187, 107, 1)',
|
||||
'rgba(245, 182, 132, 1)',
|
||||
'rgba(229, 163, 159, 1)',
|
||||
'rgba(177, 155, 159, 1)',
|
||||
'rgba(99, 118, 162, 1)',
|
||||
'rgba(176, 128, 219, 1)',
|
||||
'rgba(175, 148, 121, 1)',
|
||||
'rgba(223, 187, 107, 1)',
|
||||
'rgba(49.11, 161.05, 135.64, 1)',
|
||||
], changed, reset)
|
||||
}
|
||||
const destroyBadgeTertiaryColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setBadgeTertiaryColor = (color: string) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
badge_tertiary_color_ref,
|
||||
initBadgeTertiaryColor,
|
||||
destroyBadgeTertiaryColor,
|
||||
setBadgeTertiaryColor,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
|
||||
|
||||
export default () => {
|
||||
const close_btn_color_ref = ref(null)
|
||||
let tools: PickrTools | null
|
||||
|
||||
const initCloseBtnColor = (color: string, changed: (color: string) => void, reset: () => void) => {
|
||||
if (!close_btn_color_ref.value) return
|
||||
tools = pickrTools.create(close_btn_color_ref.value, color, [
|
||||
'rgba(59, 194, 178, 1)',
|
||||
'rgba(133, 196, 59, 1)',
|
||||
'rgba(250, 180, 160, 1)',
|
||||
'rgba(104.51, 72.55, 107.98, 1)',
|
||||
], changed, reset)
|
||||
}
|
||||
const destroyCloseBtnColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setCloseBtnColor = (color: string) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
close_btn_color_ref,
|
||||
initCloseBtnColor,
|
||||
destroyCloseBtnColor,
|
||||
setCloseBtnColor,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
|
||||
|
||||
export default () => {
|
||||
const hide_btn_color_ref = ref(null)
|
||||
let tools: PickrTools | null
|
||||
|
||||
const initHideBtnColor = (color: string, changed: (color: string) => void, reset: () => void) => {
|
||||
if (!hide_btn_color_ref.value) return
|
||||
tools = pickrTools.create(hide_btn_color_ref.value, color, [
|
||||
'rgba(59, 194, 178, 1)',
|
||||
'rgba(133, 196, 59, 1)',
|
||||
'rgba(250, 180, 160, 1)',
|
||||
'rgba(77.46, 103.73, 151.81, 1)',
|
||||
], changed, reset)
|
||||
}
|
||||
const destroyHideBtnColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setHideBtnColor = (color: string) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
hide_btn_color_ref,
|
||||
initHideBtnColor,
|
||||
destroyHideBtnColor,
|
||||
setHideBtnColor,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
|
||||
|
||||
export default () => {
|
||||
const main_bg_color_ref = ref(null)
|
||||
let tools: PickrTools | null
|
||||
|
||||
const initMainBgColor = (color: string, changed: (color: string) => void, reset: () => void) => {
|
||||
if (!main_bg_color_ref.value) return
|
||||
tools = pickrTools.create(main_bg_color_ref.value, color, [
|
||||
'rgba(255, 255, 255, 1)',
|
||||
'rgba(19, 19, 19, 0.9)',
|
||||
'rgba(255, 255, 255, 0.9)',
|
||||
'rgba(255, 255, 255, 0.8)',
|
||||
'rgba(25.82, 23.65, 46.6, 0.54)',
|
||||
], changed, reset)
|
||||
}
|
||||
const destroyMainBgColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setMainBgColor = (color: string) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
main_bg_color_ref,
|
||||
initMainBgColor,
|
||||
destroyMainBgColor,
|
||||
setMainBgColor,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
|
||||
|
||||
export default () => {
|
||||
const primary_color_ref = ref(null)
|
||||
let tools: PickrTools | null
|
||||
|
||||
const initMainColor = (color: string, changed: (color: string) => void) => {
|
||||
if (!primary_color_ref.value) return
|
||||
tools = pickrTools.create(primary_color_ref.value, color, [
|
||||
'rgba(77, 175, 124, 1)',
|
||||
'rgba(52, 152, 219, 1)',
|
||||
'rgba(77, 131, 175, 1)',
|
||||
'rgba(245, 171, 53, 1)',
|
||||
'rgba(214, 69, 65, 1)',
|
||||
'rgba(241, 130, 141, 1)',
|
||||
'rgba(155, 89, 182, 1)',
|
||||
'rgba(108, 122, 137, 1)',
|
||||
'rgba(51, 110, 123, 1)',
|
||||
'rgba(79, 98, 208, 1)',
|
||||
'rgba(150, 150, 150, 1)',
|
||||
'rgba(74, 55, 82, 1)',
|
||||
'rgba(87, 144, 167, 1)',
|
||||
'rgba(192, 57, 43, 1)',
|
||||
'rgba(113.52, 107.21, 166.13, 1)',
|
||||
], changed, () => {})
|
||||
}
|
||||
const destroyMainColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
|
||||
return {
|
||||
primary_color_ref,
|
||||
initMainColor,
|
||||
destroyMainColor,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
|
||||
|
||||
export default () => {
|
||||
const min_btn_color_ref = ref(null)
|
||||
let tools: PickrTools | null
|
||||
|
||||
const initMinBtnColor = (color: string, changed: (color: string) => void, reset: () => void) => {
|
||||
if (!min_btn_color_ref.value) return
|
||||
tools = pickrTools.create(min_btn_color_ref.value, color, [
|
||||
'rgba(59, 194, 178, 1)',
|
||||
'rgba(133, 196, 59, 1)',
|
||||
'rgba(250, 180, 160, 1)',
|
||||
'rgba(116, 87, 152, 1)',
|
||||
], changed, reset)
|
||||
}
|
||||
const destroyMinBtnColor = () => {
|
||||
if (!tools) return
|
||||
tools.destroy()
|
||||
tools = null
|
||||
}
|
||||
const setMinBtnColor = (color: string) => {
|
||||
tools?.setColor(color)
|
||||
}
|
||||
|
||||
return {
|
||||
min_btn_color_ref,
|
||||
initMinBtnColor,
|
||||
destroyMinBtnColor,
|
||||
setMinBtnColor,
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,47 @@
|
||||
<template lang="pug">
|
||||
material-modal(:show="modelValue" bg-close @close="$emit('update:modelValue', false)" teleport="#view")
|
||||
main(:class="$style.main")
|
||||
h2 {{$t('theme_selector_modal__title')}}
|
||||
div.scroll(:class="$style.content")
|
||||
div
|
||||
h3 {{$t('theme_selector_modal__light_title')}}
|
||||
ul(:class="$style.theme")
|
||||
li(v-for="theme in themeLights" :key="theme.id" :aria-label="$t('theme_' + theme.className)" @click="currentStting.theme.lightId = theme.id" :class="[theme.className, {[$style.active]: lightId == theme.id}]")
|
||||
span
|
||||
label {{$t('theme_' + theme.className)}}
|
||||
div
|
||||
h3 {{$t('theme_selector_modal__dark_title')}}
|
||||
ul(:class="$style.theme")
|
||||
li(v-for="theme in themeDarks" :key="theme.id" :aria-label="$t('theme_' + theme.className)" @click="currentStting.theme.darkId = theme.id" :class="[theme.className, {[$style.active]: darkId == theme.id}]")
|
||||
span
|
||||
label {{$t('theme_' + theme.className)}}
|
||||
div(:class="$style.note")
|
||||
p {{$t('theme_selector_modal__title_tip')}}
|
||||
<template>
|
||||
<material-modal :show="modelValue" bg-close="bg-close" teleport="#view" @close="$emit('update:modelValue', false)">
|
||||
<main :class="$style.main">
|
||||
<h2>{{ $t('theme_selector_modal__title') }}</h2>
|
||||
<div class="scroll" :class="$style.content">
|
||||
<div>
|
||||
<h3>{{ $t('theme_selector_modal__light_title') }}</h3>
|
||||
<ul :class="$style.theme">
|
||||
<li
|
||||
v-for="theme in themeInfo.themeLights" :key="theme.id"
|
||||
:style="theme.styles" :aria-label="theme.name"
|
||||
:class="[{[$style.active]: appSetting['theme.lightId'] == theme.id}]" @click="setLightId(theme.id)"
|
||||
>
|
||||
<span :class="$style.bg" />
|
||||
<label>{{ theme.name }}</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h3>{{ $t('theme_selector_modal__dark_title') }}</h3>
|
||||
<ul :class="$style.theme">
|
||||
<li
|
||||
v-for="theme in themeInfo.themeDarks" :key="theme.id"
|
||||
:style="theme.styles" :aria-label="theme.name"
|
||||
:class="[{[$style.active]: appSetting['theme.darkId'] == theme.id}]" @click="setDarkId(theme.id)"
|
||||
>
|
||||
<span :class="$style.bg" />
|
||||
<label>{{ theme.name }}</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.note">
|
||||
<p>{{ $t('theme_selector_modal__title_tip') }}</p>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed } from '@renderer/utils/vueTools'
|
||||
import { themeLights, themeDarks } from '@renderer/core/share'
|
||||
import { currentStting } from '../setting'
|
||||
import { joinPath } from '@common/utils/nodejs'
|
||||
import { markRaw, reactive, watch } from '@common/utils/vueTools'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
import { applyTheme, getThemes } from '@renderer/store/utils'
|
||||
|
||||
export default {
|
||||
name: 'ThemeSelectorModal',
|
||||
@@ -32,19 +51,74 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const lightId = computed(() => {
|
||||
return currentStting.value.theme.lightId
|
||||
emits: ['update:modelValue'],
|
||||
setup(props) {
|
||||
const themeInfo = reactive({
|
||||
themeLights: [],
|
||||
themeDarks: [],
|
||||
})
|
||||
const darkId = computed(() => {
|
||||
return currentStting.value.theme.darkId
|
||||
let dataPath = ''
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (!val) return
|
||||
getThemes((info) => {
|
||||
// console.log(info)
|
||||
const themes = [...info.themes, ...info.userThemes]
|
||||
const lights = []
|
||||
const darks = themes.filter(t => {
|
||||
if (t.isDark) return true
|
||||
lights.push(t)
|
||||
return false
|
||||
})
|
||||
dataPath = info.dataPath
|
||||
themeInfo.themeLights = lights.map(t => {
|
||||
return {
|
||||
id: t.id,
|
||||
// @ts-expect-error
|
||||
name: t.isCustom ? t.name : window.i18n.t('theme_' + t.id),
|
||||
styles: {
|
||||
'--color-primary-theme': t.config.themeColors['--color-theme'],
|
||||
'--background-image-theme': t.isCustom
|
||||
? t.config.extInfo['--background-image'] == 'none'
|
||||
? 'none'
|
||||
: `url(file:///${joinPath(info.dataPath, t.config.extInfo['--background-image']).replaceAll('\\', '/')})`
|
||||
: t.config.extInfo['--background-image'],
|
||||
},
|
||||
}
|
||||
})
|
||||
themeInfo.themeDarks = markRaw(darks.map(t => {
|
||||
return {
|
||||
id: t.id,
|
||||
// @ts-expect-error
|
||||
name: t.isCustom ? t.name : window.i18n.t('theme_' + t.id),
|
||||
styles: {
|
||||
'--color-primary-theme': t.config.themeColors['--color-theme'],
|
||||
'--background-image-theme': t.isCustom
|
||||
? t.config.extInfo['--background-image'] == 'none'
|
||||
? 'none'
|
||||
: `url(file:///${joinPath(info.dataPath, t.config.extInfo['--background-image']).replaceAll('\\', '/')})`
|
||||
: t.config.extInfo['--background-image'],
|
||||
},
|
||||
}
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
const setLightId = (id) => {
|
||||
if (appSetting['theme.lightId'] == id) return
|
||||
updateSetting({ 'theme.lightId': id })
|
||||
if (appSetting['theme.id'] == 'auto') applyTheme('auto', id, appSetting['theme.darkId'], dataPath)
|
||||
}
|
||||
const setDarkId = (id) => {
|
||||
if (appSetting['theme.darkId'] == id) return
|
||||
updateSetting({ 'theme.darkId': id })
|
||||
if (appSetting['theme.id'] == 'auto') applyTheme('auto', appSetting['theme.lightId'], id, dataPath)
|
||||
}
|
||||
return {
|
||||
currentStting,
|
||||
themeLights,
|
||||
themeDarks,
|
||||
lightId,
|
||||
darkId,
|
||||
appSetting,
|
||||
themeInfo,
|
||||
setLightId,
|
||||
setDarkId,
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -66,14 +140,14 @@ export default {
|
||||
h2 {
|
||||
flex: none;
|
||||
font-size: 16px;
|
||||
color: @color-theme_2-font;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
color: @color-theme_2-font;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
padding-bottom: 15px;
|
||||
font-size: 15px;
|
||||
@@ -96,7 +170,7 @@ export default {
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
// color: @color-theme;
|
||||
// color: var(--color-primary);
|
||||
margin-right: 30px;
|
||||
transition: color .3s ease;
|
||||
margin-bottom: 15px;
|
||||
@@ -106,7 +180,14 @@ export default {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
&.active {
|
||||
color: var(--color-primary-theme);
|
||||
.bg {
|
||||
border-color: var(--color-primary-theme);
|
||||
}
|
||||
}
|
||||
|
||||
.bg {
|
||||
display: block;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@@ -124,6 +205,8 @@ export default {
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-color: var(--color-primary-theme);
|
||||
background-image: var(--background-image-theme);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,17 +217,6 @@ export default {
|
||||
font-size: 14px;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
&:global(.@{value}) {
|
||||
span {
|
||||
&:after {
|
||||
background-color: ~'@{color-@{value}-theme}';
|
||||
background-image: ~'@{color-@{value}-theme-bgimg}';
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +224,7 @@ export default {
|
||||
padding: 8px 15px;
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
color: @color-theme_2-font;
|
||||
color: var(--color-font);
|
||||
// p {
|
||||
// + p {
|
||||
// margin-top: 5px;
|
||||
@@ -160,22 +232,4 @@ export default {
|
||||
// }
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
.theme {
|
||||
li {
|
||||
&:global(.@{value}) {
|
||||
&.active {
|
||||
color: ~'@{color-@{value}-theme}';
|
||||
span {
|
||||
border-color: ~'@{color-@{value}-theme}';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.note {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@ material-modal(:show="modelValue" bg-close @close="handleClose" teleport="#view"
|
||||
main(:class="$style.main")
|
||||
h2 {{$t('user_api__title')}}
|
||||
ul.scroll(v-if="apiList.length" :class="$style.content")
|
||||
li(:class="[$style.listItem, {[$style.active]: setting.apiSource == api.id}]" v-for="(api, index) in apiList" :key="api.id")
|
||||
li(:class="[$style.listItem, {[$style.active]: appSetting['common.apiSource'] == api.id}]" v-for="(api, index) in apiList" :key="api.id")
|
||||
div(:class="$style.listLeft")
|
||||
h3 {{api.name}}
|
||||
p {{api.description}}
|
||||
@@ -25,13 +25,12 @@ material-modal(:show="modelValue" bg-close @close="handleClose" teleport="#view"
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import { rendererInvoke, NAMES } from '@common/ipc'
|
||||
import { promises as fsPromises } from 'fs'
|
||||
import { selectDir, openUrl } from '@renderer/utils'
|
||||
import apiSourceInfo from '@renderer/utils/music/api-source-info'
|
||||
import { userApi, apiSource } from '@renderer/core/share'
|
||||
import { setAllowShowUserApiUpdateAlert } from '@renderer/utils/tools'
|
||||
import { importUserApi, removeUserApi, showSelectDialog, setAllowShowUserApiUpdateAlert } from '@renderer/utils/ipc'
|
||||
import { readFile } from '@common/utils/nodejs'
|
||||
import { openUrl } from '@common/utils/electron'
|
||||
import apiSourceInfo from '@renderer/utils/musicSdk/api-source-info'
|
||||
import { userApi } from '@renderer/store'
|
||||
import { appSetting, updateSetting } from '@renderer/store/setting'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -40,13 +39,14 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup() {
|
||||
return {
|
||||
userApi,
|
||||
appSetting,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['setting']),
|
||||
apiList() {
|
||||
return this.userApi.list
|
||||
},
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
})
|
||||
return
|
||||
}
|
||||
selectDir({
|
||||
showSelectDialog({
|
||||
title: this.$t('user_api__import_file'),
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
@@ -69,8 +69,8 @@ export default {
|
||||
],
|
||||
}).then(result => {
|
||||
if (result.canceled) return
|
||||
return fsPromises.readFile(result.filePaths[0]).then(data => {
|
||||
return rendererInvoke(NAMES.mainWindow.import_user_api, data.toString()).then(({ apiList }) => {
|
||||
return readFile(result.filePaths[0]).then(data => {
|
||||
return importUserApi(data.toString()).then(({ apiList }) => {
|
||||
userApi.list = apiList
|
||||
})
|
||||
})
|
||||
@@ -82,11 +82,11 @@ export default {
|
||||
async handleRemove(index) {
|
||||
const api = this.apiList[index]
|
||||
if (!api) return
|
||||
if (this.setting.apiSource == api.id) {
|
||||
if (appSetting['common.apiSource'] == api.id) {
|
||||
let backApi = apiSourceInfo.find(api => !api.disabled)
|
||||
if (backApi) apiSource.value = backApi.id
|
||||
if (backApi) updateSetting({ 'common.apiSource': backApi.id })
|
||||
}
|
||||
userApi.list = await rendererInvoke(NAMES.mainWindow.remove_user_api, [api.id])
|
||||
userApi.list = await removeUserApi([api.id])
|
||||
},
|
||||
handleClose() {
|
||||
this.$emit('update:modelValue', false)
|
||||
@@ -117,14 +117,14 @@ export default {
|
||||
// overflow: hidden;
|
||||
h2 {
|
||||
font-size: 16px;
|
||||
color: @color-theme_2-font;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
color: @color-theme;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
@@ -147,27 +147,27 @@ export default {
|
||||
padding: 10px;
|
||||
border-radius: @radius-border;
|
||||
&:hover {
|
||||
background-color: @color-theme_2-hover;
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
&.active {
|
||||
background-color: @color-theme_2-active;
|
||||
background-color: var(--color-primary-background-active);
|
||||
}
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
color: @color-theme_2-font;
|
||||
color: var(--color-font);
|
||||
word-break: break-all;
|
||||
}
|
||||
p {
|
||||
margin-top: 5px;
|
||||
font-size: 14px;
|
||||
color: @color-theme_2-font-label;
|
||||
color: var(--color-font-label);
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
.noitem {
|
||||
height: 100px;
|
||||
font-size: 18px;
|
||||
color: @color-theme_2-font-label;
|
||||
color: var(--color-font-label);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -195,7 +195,7 @@ export default {
|
||||
margin-top: 15px;
|
||||
font-size: 12px;
|
||||
line-height: 1.25;
|
||||
color: @color-theme_2-font;
|
||||
color: var(--color-font);
|
||||
p {
|
||||
+ p {
|
||||
margin-top: 5px;
|
||||
@@ -222,36 +222,4 @@ export default {
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.main {
|
||||
h2 {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
}
|
||||
.listItem {
|
||||
&:hover {
|
||||
background-color: ~'@{color-@{value}-theme_2-hover}';
|
||||
}
|
||||
&.active {
|
||||
background-color: ~'@{color-@{value}-theme_2-active}';
|
||||
}
|
||||
h3 {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
p {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
.noitem {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
|
||||
.note {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
<template>
|
||||
<div :class="$style.main">
|
||||
<div class="scroll" :class="$style.toc">
|
||||
<ul :class="$style.tocList" role="toolbar">
|
||||
<li v-for="h2 in tocList" :key="h2.id" :class="$style.tocListItem" role="presentation">
|
||||
<h2
|
||||
:class="[$style.tocH2, {[$style.active]: avtiveComponentName == h2.id }]"
|
||||
role="tab" :aria-selected="avtiveComponentName == h2.id"
|
||||
:aria-label="h2.title" ignore-tip @click="toggleTab(h2.id)"
|
||||
>
|
||||
{{ h2.title }}
|
||||
</h2>
|
||||
<!-- <ul v-if="h2.children.length" :class="$style.tocList">
|
||||
<li v-for="h3 in h2.children" :key="h3.id" :class="$style.tocSubListItem">
|
||||
<h3 :class="[$style.tocH3, toc.activeId == h3.id ? $style.active : null]" :aria-label="h3.title">
|
||||
<a :href="'#' + h3.id" @click.stop="toc.activeId = h3.id">{{ h3.title }}</a>
|
||||
</h3>
|
||||
</li>
|
||||
</ul> -->
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div ref="dom_content_ref" class="scroll" :class="$style.setting">
|
||||
<dl>
|
||||
<component :is="avtiveComponentName" />
|
||||
<!-- <SettingBasic />
|
||||
<SettingPlay />
|
||||
<SettingPlayDetail />
|
||||
<SettingDesktopLyric />
|
||||
<SettingSearch />
|
||||
<SettingList />
|
||||
<SettingDownload />
|
||||
<SettingSync />
|
||||
<SettingHotKey />
|
||||
<SettingNetwork />
|
||||
<SettingOdc />
|
||||
<SettingBackup />
|
||||
<SettingOther />
|
||||
<SettingUpdate />
|
||||
<SettingAbout /> -->
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, computed, nextTick } from '@common/utils/vueTools'
|
||||
// import { currentStting } from './setting'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { useRoute } from '@common/utils/vueRouter'
|
||||
|
||||
import SettingBasic from './components/SettingBasic'
|
||||
import SettingPlay from './components/SettingPlay'
|
||||
import SettingPlayDetail from './components/SettingPlayDetail'
|
||||
import SettingDesktopLyric from './components/SettingDesktopLyric'
|
||||
import SettingSearch from './components/SettingSearch'
|
||||
import SettingList from './components/SettingList'
|
||||
import SettingDownload from './components/SettingDownload'
|
||||
import SettingSync from './components/SettingSync'
|
||||
import SettingHotKey from './components/SettingHotKey'
|
||||
import SettingNetwork from './components/SettingNetwork'
|
||||
import SettingOdc from './components/SettingOdc'
|
||||
import SettingBackup from './components/SettingBackup'
|
||||
import SettingOther from './components/SettingOther'
|
||||
import SettingUpdate from './components/SettingUpdate'
|
||||
import SettingAbout from './components/SettingAbout'
|
||||
|
||||
export default {
|
||||
name: 'Setting',
|
||||
components: {
|
||||
SettingBasic,
|
||||
SettingPlay,
|
||||
SettingPlayDetail,
|
||||
SettingDesktopLyric,
|
||||
SettingSearch,
|
||||
SettingList,
|
||||
SettingDownload,
|
||||
SettingSync,
|
||||
SettingHotKey,
|
||||
SettingNetwork,
|
||||
SettingOdc,
|
||||
SettingBackup,
|
||||
SettingOther,
|
||||
SettingUpdate,
|
||||
SettingAbout,
|
||||
},
|
||||
setup() {
|
||||
const t = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
const dom_content_ref = ref(null)
|
||||
|
||||
const tocList = computed(() => {
|
||||
return [
|
||||
{ id: 'SettingBasic', title: t('setting__basic') },
|
||||
{ id: 'SettingPlay', title: t('setting__play') },
|
||||
{ id: 'SettingPlayDetail', title: t('setting__play_detail') },
|
||||
{ id: 'SettingDesktopLyric', title: t('setting__desktop_lyric') },
|
||||
{ id: 'SettingSearch', title: t('setting__search') },
|
||||
{ id: 'SettingList', title: t('setting__list') },
|
||||
{ id: 'SettingDownload', title: t('setting__download') },
|
||||
{ id: 'SettingSync', title: t('setting__sync') },
|
||||
{ id: 'SettingHotKey', title: t('setting__hot_key') },
|
||||
{ id: 'SettingNetwork', title: t('setting__network') },
|
||||
{ id: 'SettingOdc', title: t('setting__odc') },
|
||||
{ id: 'SettingBackup', title: t('setting__backup') },
|
||||
{ id: 'SettingOther', title: t('setting__other') },
|
||||
{ id: 'SettingUpdate', title: t('setting__update') },
|
||||
{ id: 'SettingAbout', title: t('setting__about') },
|
||||
]
|
||||
})
|
||||
|
||||
const avtiveComponentName = ref(route.query.name && tocList.value.some(t => t.id == route.query.name)
|
||||
? route.query.name
|
||||
: tocList.value[0].id)
|
||||
|
||||
const toggleTab = id => {
|
||||
avtiveComponentName.value = id
|
||||
nextTick(() => {
|
||||
dom_content_ref.value?.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
tocList,
|
||||
avtiveComponentName,
|
||||
dom_content_ref,
|
||||
toggleTab,
|
||||
}
|
||||
},
|
||||
// mounted() {
|
||||
// this.initTOC()
|
||||
// },
|
||||
// methods: {
|
||||
// initTOC() {
|
||||
// const list = this.$refs.dom_setting_list.children
|
||||
// const toc = []
|
||||
// let prevTitle
|
||||
// for (const item of list) {
|
||||
// if (item.tagName == 'DT') {
|
||||
// prevTitle = {
|
||||
// title: item.innerText.replace(/[((].+?[))]/, ''),
|
||||
// id: item.getAttribute('id'),
|
||||
// dom: item,
|
||||
// children: [],
|
||||
// }
|
||||
// toc.push(prevTitle)
|
||||
// continue
|
||||
// }
|
||||
// const h3 = item.querySelector('h3')
|
||||
// if (h3) {
|
||||
// prevTitle.children.push({
|
||||
// title: h3.innerText.replace(/[((].+?[))]/, ''),
|
||||
// id: h3.getAttribute('id'),
|
||||
// dom: h3,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// console.log(toc)
|
||||
// this.toc.list = toc
|
||||
// },
|
||||
// handleListScroll(event) {
|
||||
// // console.log(event.target.scrollTop)
|
||||
// },
|
||||
// },
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.main {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
height: 100%;
|
||||
border-top: var(--color-list-header-border-bottom);
|
||||
}
|
||||
|
||||
.toc {
|
||||
flex: 0 0 16%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.tocH2 {
|
||||
line-height: 1.5;
|
||||
.mixin-ellipsis-1;
|
||||
font-size: 13px;
|
||||
color: var(--color-font);
|
||||
padding: 8px 10px;
|
||||
transition: @transition-fast;
|
||||
transition-property: background-color, color;
|
||||
|
||||
&:not(.active) {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
// .tocH3 {
|
||||
// font-size: 13px;
|
||||
// opacity: .8;
|
||||
// }
|
||||
|
||||
// .tocList {
|
||||
// .tocList {
|
||||
// // padding-left: 15px;
|
||||
// }
|
||||
// }
|
||||
// .tocSubListItem {
|
||||
// padding-top: 10px;
|
||||
// }
|
||||
|
||||
.setting {
|
||||
padding: 0 15px 15px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
:global {
|
||||
dt {
|
||||
border-left: 5px solid var(--color-primary-alpha-700);
|
||||
padding: 3px 7px;
|
||||
margin: 15px 0;
|
||||
|
||||
+ dd h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
dd {
|
||||
// margin-left: 15px;
|
||||
// font-size: 13px;
|
||||
> div {
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
}
|
||||
h3 {
|
||||
font-size: 12px;
|
||||
margin: 25px 0 15px;
|
||||
}
|
||||
p {
|
||||
padding: 3px 0;
|
||||
line-height: 1.3;
|
||||
.btn {
|
||||
+ .btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.help-btn {
|
||||
padding: 0;
|
||||
margin: 0 0.4em;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--color-button-font);
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
.help-icon {
|
||||
margin: 0 0.4em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// .btn-content {
|
||||
// display: inline-block;
|
||||
// transition: @transition-theme;
|
||||
// transition-property: opacity, transform;
|
||||
// opacity: 1;
|
||||
// transform: scale(1);
|
||||
|
||||
// &.hide {
|
||||
// opacity: 0;
|
||||
// transform: scale(0);
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// :global(dt):target, :global(h3):target {
|
||||
// animation: highlight 1s ease;
|
||||
// }
|
||||
|
||||
// @keyframes highlight {
|
||||
// from { background: yellow; }
|
||||
// to { background: transparent; }
|
||||
// }
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
import { ref } from '@renderer/utils/vueTools'
|
||||
|
||||
export const currentStting = ref({
|
||||
player: {
|
||||
togglePlayMethod: 'listLoop',
|
||||
highQuality: false,
|
||||
isShowTaskProgess: true,
|
||||
volume: 1,
|
||||
isMute: false,
|
||||
mediaDeviceId: 'default',
|
||||
isMediaDeviceRemovedStopPlay: false,
|
||||
isShowLyricTranslation: false,
|
||||
isShowLyricRoma: false,
|
||||
isS2t: false, // 是否将歌词从简体转换为繁体
|
||||
isPlayLxlrc: true,
|
||||
isSavePlayTime: false,
|
||||
audioVisualization: false,
|
||||
waitPlayEndStop: true,
|
||||
waitPlayEndStopTime: '',
|
||||
autoSkipOnError: true,
|
||||
},
|
||||
playDetail: {
|
||||
isZoomActiveLrc: true,
|
||||
style: {
|
||||
fontSize: 100,
|
||||
align: 'center',
|
||||
},
|
||||
},
|
||||
desktopLyric: {
|
||||
enable: false,
|
||||
isLock: false,
|
||||
width: 600,
|
||||
height: 700,
|
||||
x: -1,
|
||||
y: -1,
|
||||
theme: '',
|
||||
isLockScreen: true,
|
||||
isDelayScroll: true,
|
||||
isHoverHide: false,
|
||||
style: {
|
||||
font: '',
|
||||
fontSize: 125,
|
||||
opacity: 80,
|
||||
isZoomActiveLrc: true,
|
||||
},
|
||||
},
|
||||
list: {
|
||||
isShowAlbumName: true,
|
||||
isShowSource: true,
|
||||
isSaveScrollLocation: true,
|
||||
},
|
||||
search: {
|
||||
searchSource: 'kw',
|
||||
tempSearchSource: 'kw',
|
||||
isShowHotSearch: false,
|
||||
isShowHistorySearch: false,
|
||||
isFocusSearchBox: false,
|
||||
},
|
||||
download: {
|
||||
enable: false,
|
||||
savePath: '',
|
||||
fileName: '歌名 - 歌手',
|
||||
isDownloadLrc: false,
|
||||
lrcFormat: 'utf8',
|
||||
isEmbedPic: true,
|
||||
isEmbedLyric: true,
|
||||
},
|
||||
network: {
|
||||
proxy: {
|
||||
enable: false,
|
||||
host: '',
|
||||
port: '',
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
odc: {
|
||||
isAutoClearSearchInput: false,
|
||||
isAutoClearSearchList: false,
|
||||
},
|
||||
tray: {
|
||||
isShow: false,
|
||||
isToTray: false,
|
||||
themeId: 0,
|
||||
},
|
||||
sync: {
|
||||
enable: false,
|
||||
port: '23332',
|
||||
},
|
||||
windowSizeId: 1,
|
||||
startInFullscreen: false,
|
||||
langId: 'cns',
|
||||
theme: {
|
||||
id: 0,
|
||||
lightId: 0,
|
||||
darkId: 13,
|
||||
},
|
||||
sourceId: 0,
|
||||
font: '',
|
||||
isShowAnimation: true,
|
||||
randomAnimate: true,
|
||||
isAgreePact: false,
|
||||
controlBtnPosition: 'left',
|
||||
apiSource: 'temp',
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
import { tempListMeta, userLists } from '@renderer/store/list/state'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
import syncSourceList from '@renderer/store/list/syncSourceList'
|
||||
import { getListDetail, getListDetailAll } from '@renderer/store/songList/action'
|
||||
import { createUserList, setTempList } from '@renderer/store/list/action'
|
||||
import { playList } from '@renderer/core/player/action'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
|
||||
const getListId = (id: string, source: LX.OnlineSource) => `${source}__${id}`
|
||||
|
||||
export const addSongListDetail = async(id: string, source: LX.OnlineSource, name?: string) => {
|
||||
// console.log(this.listDetail.info)
|
||||
// if (!this.listDetail.info.name) return
|
||||
const listId = getListId(id, source)
|
||||
const targetList = userLists.find(l => l.id == listId)
|
||||
if (targetList) {
|
||||
const confirm = await dialog.confirm({
|
||||
message: window.i18n.t('duplicate_list_tip', { name: targetList.name }),
|
||||
cancelButtonText: window.i18n.t('lists__import_part_button_cancel'),
|
||||
confirmButtonText: window.i18n.t('confirm_button_text'),
|
||||
})
|
||||
if (!confirm) return
|
||||
void syncSourceList(targetList)
|
||||
return
|
||||
}
|
||||
|
||||
const list = await getListDetailAll(id, source)
|
||||
await createUserList({
|
||||
name,
|
||||
id: listId,
|
||||
list,
|
||||
source,
|
||||
sourceListId: id,
|
||||
})
|
||||
}
|
||||
|
||||
export const playSongListDetail = async(id: string, source: LX.OnlineSource, list?: LX.Music.MusicInfoOnline[], index: number = 0) => {
|
||||
let isPlayingList = false
|
||||
console.log(list)
|
||||
const listId = getListId(id, source)
|
||||
if (!list?.length) list = (await getListDetail(id, source, 1)).list
|
||||
if (list?.length) {
|
||||
await setTempList(listId, [...list])
|
||||
playList(LIST_IDS.TEMP, index)
|
||||
isPlayingList = true
|
||||
}
|
||||
const fullList = await getListDetailAll(id, source)
|
||||
if (!fullList.length) return
|
||||
if (isPlayingList) {
|
||||
if (tempListMeta.id == listId) {
|
||||
await setTempList(listId, [...fullList])
|
||||
}
|
||||
} else {
|
||||
await setTempList(listId, [...fullList])
|
||||
playList(LIST_IDS.TEMP, index)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.songListHeader">
|
||||
<div :class="$style.songListHeaderLeft" :style="{ backgroundImage: 'url('+(picUrl || listDetailInfo.info.img)+')' }">
|
||||
<!-- <span v-if="listDetailInfo.info.play_count" :class="$style.playNum">{{ listDetailInfo.info.play_count }}</span> -->
|
||||
</div>
|
||||
<div :class="$style.songListHeaderMiddle">
|
||||
<h3 :title="listDetailInfo.info.name">{{ listDetailInfo.info.name }}</h3>
|
||||
<p :title="listDetailInfo.info.desc">{{ listDetailInfo.info.desc }}</p>
|
||||
</div>
|
||||
<div :class="$style.songListHeaderRight">
|
||||
<base-btn
|
||||
:class="$style.headerRightBtn"
|
||||
:disabled="!!listDetailInfo.noItemLabel"
|
||||
@click="playSongListDetail(listDetailInfo.id, listDetailInfo.source, listDetailInfo.list)"
|
||||
>
|
||||
{{ $t('list__play') }}
|
||||
</base-btn>
|
||||
<base-btn
|
||||
:class="$style.headerRightBtn"
|
||||
:disabled="!!listDetailInfo.noItemLabel"
|
||||
@click="addSongListDetail(listDetailInfo.id, listDetailInfo.source, listDetailInfo.info.name)"
|
||||
>
|
||||
{{ $t('list__collect') }}
|
||||
</base-btn>
|
||||
<base-btn :class="$style.headerRightBtn" @click="handleBack">{{ $t('back') }}</base-btn>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.list">
|
||||
<material-online-list
|
||||
ref="listRef"
|
||||
:page="listDetailInfo.page"
|
||||
:limit="listDetailInfo.limit"
|
||||
:total="listDetailInfo.total"
|
||||
:list="listDetailInfo.list"
|
||||
:no-item="listDetailInfo.noItemLabel"
|
||||
@play-list="handlePlayList"
|
||||
@toggle-page="togglePage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref, watch } from '@common/utils/vueTools'
|
||||
import { listDetailInfo } from '@renderer/store/songList/state'
|
||||
import { setVisibleListDetail } from '@renderer/store/songList/action'
|
||||
import LX from '@renderer/types/lx'
|
||||
import { useRouter } from '@common/utils/vueRouter'
|
||||
import { addSongListDetail, playSongListDetail } from './action'
|
||||
import useList from './useList'
|
||||
import useKeyBack from './useKeyBack'
|
||||
|
||||
|
||||
const source = ref<LX.OnlineSource>('kw')
|
||||
const id = ref<string>('')
|
||||
const page = ref<number>(1)
|
||||
const picUrl = ref<string>('')
|
||||
const refresh = ref<boolean>(false)
|
||||
|
||||
|
||||
interface Query {
|
||||
source?: string
|
||||
id?: string
|
||||
page?: string
|
||||
picUrl?: string
|
||||
refresh?: 'true'
|
||||
fromName?: string
|
||||
}
|
||||
|
||||
const verifyQueryParams = async function(this: any, to: { query: Query, path: string}, from: any, next: (route?: { path: string; query: Query }) => void) {
|
||||
let _source = to.query.source
|
||||
let _id = to.query.id
|
||||
let _page: string | undefined = to.query.page
|
||||
let _picUrl: string | undefined = to.query.picUrl
|
||||
let _refresh: 'true' | undefined = to.query.refresh
|
||||
|
||||
if (_source == null || _id == null) {
|
||||
if (listDetailInfo.key) {
|
||||
_source = listDetailInfo.source
|
||||
_id = listDetailInfo.id
|
||||
_page = listDetailInfo.page.toString()
|
||||
_picUrl = listDetailInfo.info.img
|
||||
} else {
|
||||
setVisibleListDetail(false)
|
||||
next({ path: '/songList/list', query: {} })
|
||||
return
|
||||
}
|
||||
|
||||
next({
|
||||
path: to.path,
|
||||
query: { ...to.query, source: _source, id: _id, page: _page, picUrl: _picUrl, refresh: _refresh },
|
||||
})
|
||||
return
|
||||
}
|
||||
next()
|
||||
setVisibleListDetail(true)
|
||||
source.value = _source as LX.OnlineSource
|
||||
id.value = _id
|
||||
page.value = _page ? parseInt(_page) : 1
|
||||
picUrl.value = _picUrl ?? ''
|
||||
refresh.value = _refresh ? _refresh == 'true' : false
|
||||
if (to.query.fromName) window.lx.songListInfo.fromName = to.query.fromName
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
beforeRouteEnter: verifyQueryParams,
|
||||
beforeRouteUpdate: verifyQueryParams,
|
||||
setup() {
|
||||
const router = useRouter()
|
||||
|
||||
const {
|
||||
listRef,
|
||||
listDetailInfo,
|
||||
getListData,
|
||||
handlePlayList,
|
||||
} = useList()
|
||||
|
||||
|
||||
const togglePage = (page: number) => {
|
||||
getListData(source.value, id.value, page, refresh.value)
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
setVisibleListDetail(false)
|
||||
if (window.lx.songListInfo.fromName) router.replace({ name: window.lx.songListInfo.fromName })
|
||||
else router.back()
|
||||
}
|
||||
|
||||
useKeyBack(handleBack)
|
||||
|
||||
watch([source, id, page, refresh], ([_source, _id, _page, _refresh]) => {
|
||||
if (!_source || !_id) return router.replace({ path: '/songList/list' })
|
||||
// console.log(_source, _id, _page, _refresh, picUrl.value)
|
||||
// source.value = _source
|
||||
// id.value = _id
|
||||
// refresh.value = _refresh
|
||||
// page.value = _page ?? 1
|
||||
getListData(_source, _id, _page, _refresh)
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
return {
|
||||
source,
|
||||
id,
|
||||
page,
|
||||
picUrl,
|
||||
listDetailInfo,
|
||||
listRef,
|
||||
togglePage,
|
||||
addSongListDetail,
|
||||
playSongListDetail,
|
||||
handlePlayList,
|
||||
handleBack,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.container {
|
||||
// position: absolute;
|
||||
// left: 0;
|
||||
// top: 0;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.song-list-header {
|
||||
flex: none;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
height: 80px;
|
||||
}
|
||||
.song-list-header-left {
|
||||
flex: none;
|
||||
margin-left: 15px;
|
||||
height: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
opacity: .9;
|
||||
box-shadow: 0 0 2px 0 rgba(0,0,0,.2);
|
||||
}
|
||||
.play-num {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 4px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
|
||||
.song-list-header-middle {
|
||||
flex: auto;
|
||||
padding: 2px 7px;
|
||||
h3 {
|
||||
.mixin-ellipsis-1;
|
||||
line-height: 1.2;
|
||||
padding-bottom: 5px;
|
||||
color: var(--color-font);
|
||||
}
|
||||
p {
|
||||
.mixin-ellipsis(3);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
}
|
||||
.song-list-header-right {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 15px;
|
||||
|
||||
.header-right-btn {
|
||||
border-radius: 0;
|
||||
&:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
&:last-child {
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
flex: auto;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { onBeforeUnmount, onMounted } from '@common/utils/vueTools'
|
||||
|
||||
export default (handleBack: () => void) => {
|
||||
const handle_key_backspace_down = (event: LX.KeyDownEevent) => {
|
||||
if (event.event && (event.event.repeat || (event.event.target as HTMLElement).classList.contains('key-bind'))) return
|
||||
handleBack()
|
||||
}
|
||||
onMounted(() => {
|
||||
window.key_event.on('key_backspace_down', handle_key_backspace_down)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.key_event.off('key_backspace_down', handle_key_backspace_down)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
// import { useI18n } from '@renderer/plugins/i18n'
|
||||
// import { } from '@renderer/store/search/state'
|
||||
import { getAndSetListDetail } from '@renderer/store/songList/action'
|
||||
import { listDetailInfo } from '@renderer/store/songList/state'
|
||||
import { playSongListDetail } from './action'
|
||||
|
||||
export default () => {
|
||||
const listRef = ref<any>(null)
|
||||
|
||||
const getListData = async(source: LX.OnlineSource, id: string, page: number, refresh: boolean) => {
|
||||
await getAndSetListDetail(id, source, page, refresh).then(() => {
|
||||
setTimeout(() => {
|
||||
if (listRef.value) listRef.value.scrollToTop()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handlePlayList = (index: number) => {
|
||||
void playSongListDetail(listDetailInfo.id, listDetailInfo.source, listDetailInfo.list, index)
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
listRef,
|
||||
listDetailInfo,
|
||||
getListData,
|
||||
handlePlayList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<SongList ref="list_ref" :list-info="listInfo" @toggle-page="togglePage" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, ref, nextTick } from '@common/utils/vueTools'
|
||||
import { listInfo } from '@renderer/store/songList/state'
|
||||
import { getAndSetList } from '@renderer/store/songList/action'
|
||||
import { useRouter, useRoute, onBeforeRouteLeave } from '@common/utils/vueRouter'
|
||||
import LX from '@renderer/types/lx'
|
||||
import SongList from './components/SongList.vue'
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
source: LX.OnlineSource
|
||||
tagId: string
|
||||
sortId?: string
|
||||
page: number
|
||||
}>()
|
||||
|
||||
|
||||
const list_ref = ref<any>(null)
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const getListData = async(source: LX.OnlineSource, tabId: string, sortId: string, page: number) => {
|
||||
// console.log(source, tabId, sortId, page)
|
||||
await getAndSetList(source, tabId, sortId, page).then(() => {
|
||||
if (listInfo.key == window.lx.songListInfo.songlistKey && window.lx.songListInfo.songlistPosition) {
|
||||
nextTick(() => {
|
||||
list_ref.value?.scrollTo(window.lx.songListInfo.songlistPosition)
|
||||
})
|
||||
} else if (list_ref.value) {
|
||||
window.lx.songListInfo.songlistKey = null
|
||||
nextTick(() => {
|
||||
list_ref.value.scrollTo(0)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const togglePage = (page: number) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
...route.query,
|
||||
sortId: props.sortId ?? '',
|
||||
page,
|
||||
},
|
||||
})
|
||||
// getListData(props.source, props.tagId, props.sortId ?? '', page)
|
||||
}
|
||||
|
||||
watch(() => [props.source, props.tagId, props.sortId, props.page], ([source, tagId, sortId, page]) => {
|
||||
// const source = (await getLeaderboardSetting()).source as LX.OnlineSource
|
||||
// console.log(source, tagId, sortId)
|
||||
if (!source || !sortId) return
|
||||
// console.log(source, tagId, sortId, page)
|
||||
getListData(source as LX.OnlineSource, tagId as string, sortId as string, page as number)
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
window.lx.songListInfo.songlistKey = listInfo.key
|
||||
if (list_ref.value) window.lx.songListInfo.songlistPosition = list_ref.value.getScrollTop()
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
.container {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.listContent {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 15px;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
.item {
|
||||
width: 32%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
margin-top: 15px;
|
||||
cursor: pointer;
|
||||
transition: opacity @transition-normal;
|
||||
&:hover {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
.left {
|
||||
flex: none;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
display: flex;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
opacity: .9;
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
}
|
||||
// box-shadow: 0 0 2px 0 rgba(0,0,0,.2);
|
||||
}
|
||||
.right {
|
||||
flex: auto;
|
||||
padding: 3px 15px 5px 7px;
|
||||
overflow: hidden;
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
height: 2.6em;
|
||||
text-align: justify;
|
||||
line-height: 1.3;
|
||||
.mixin-ellipsis-2;
|
||||
}
|
||||
}
|
||||
.songlist_info {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
gap: 15px;
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
.mixin-ellipsis-1;
|
||||
text-align: justify;
|
||||
line-height: 1.2;
|
||||
// text-indent: 24px;
|
||||
color: var(--color-font-label);
|
||||
svg {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
.author {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
.mixin-ellipsis-1;
|
||||
text-align: justify;
|
||||
line-height: 1.2;
|
||||
// text-indent: 24px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
.pagination {
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
// left: 50%;
|
||||
// transform: translateX(-50%);
|
||||
}
|
||||
.noitem {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// background-color: var(--color-000);
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<material-modal :show="props.modelValue" teleport="#view" width="60%" @close="emit('update:modelValue', $event)" @after-enter="$refs.input.focus()">
|
||||
<main :class="$style.main">
|
||||
<h2>{{ $t('songlist__import_input_title') }}</h2>
|
||||
<div :class="$style.inputContent">
|
||||
<base-selection v-model="source" :class="$style.select" :list="props.sourceList" item-key="id" item-name="name" />
|
||||
<base-input
|
||||
ref="input"
|
||||
v-model.trim="text"
|
||||
:class="$style.input"
|
||||
:placeholder="$t('songlist__import_input_tip')"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.footer">
|
||||
<div :class="$style.tips">
|
||||
<ul>
|
||||
<li>{{ $t('songlist__import_input_tip_1') }}</li>
|
||||
<li>{{ $t('songlist__import_input_tip_2') }}</li>
|
||||
<li>{{ $t('songlist__import_input_tip_3') }}</li>
|
||||
<li>
|
||||
{{ $t('songlist__import_input_tip_4') }}
|
||||
<span
|
||||
class="hover underline"
|
||||
aria-label="https://lyswhut.github.io/lx-music-doc/desktop/faq/cannot-open-songlist"
|
||||
@click="openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq/cannot-open-songlist')"
|
||||
>FAQ</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<base-btn :class="$style.btn" @click="handleSubmit">{{ $t('songlist__import_input_btn_confirm') }}</base-btn>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { openSongListInputInfo } from '@renderer/store/songList/state'
|
||||
import { setOpenSongListInputInfo } from '@renderer/store/songList/action'
|
||||
import { ref, watch } from '@common/utils/vueTools'
|
||||
import { useRoute, useRouter } from '@common/utils/vueRouter'
|
||||
import { openUrl } from '@common/utils/electron'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
sourceList: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const text = ref('')
|
||||
const source = ref('')
|
||||
|
||||
watch(() => props.modelValue, (visible) => {
|
||||
if (!visible) return
|
||||
source.value = openSongListInputInfo.source || route.query.source
|
||||
// text.value = openSongListInputInfo.text
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!text.value.length) return
|
||||
setOpenSongListInputInfo(text.value, source.value)
|
||||
router.push({
|
||||
path: '/songList/detail',
|
||||
query: {
|
||||
source: source.value,
|
||||
id: text.value,
|
||||
refresh: 'true',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.main {
|
||||
padding: 0 15px;
|
||||
// max-width: 530px;
|
||||
// min-width: 300px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-height: 0;
|
||||
// max-height: 100%;
|
||||
// overflow: hidden;
|
||||
h2 {
|
||||
font-size: 14px;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
word-break: break-all;
|
||||
// text-align: center;
|
||||
padding: 15px 0 8px;
|
||||
}
|
||||
}
|
||||
.inputContent {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
.select {
|
||||
width: auto;
|
||||
:global {
|
||||
.label-content {
|
||||
height: 100%;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.selection-list {
|
||||
li {
|
||||
// background-color: var(--color-main-background);
|
||||
text-align: center;
|
||||
line-height: 32px;
|
||||
font-size: 13px;
|
||||
&:hover {
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-button-background-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.input {
|
||||
flex: auto;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
// width: 100%;
|
||||
// height: 26px;
|
||||
padding: 8px 8px;
|
||||
color: var(--color-font);
|
||||
}
|
||||
.footer {
|
||||
margin: 50px 0 15px;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.tips {
|
||||
flex: auto;
|
||||
font-size: 12px;
|
||||
color: var(--color-font);
|
||||
line-height: 1.5;
|
||||
ul {
|
||||
list-style: decimal;
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
// box-sizing: border-box;
|
||||
// margin-left: 15px;
|
||||
// margin-bottom: 15px;
|
||||
// height: 36px;
|
||||
// line-height: 36px;
|
||||
// padding: 0 10px !important;
|
||||
min-width: 80px;
|
||||
// .mixin-ellipsis-1;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div v-show="!props.listInfo.noItemLabel" ref="dom_list_ref" :class="$style.listContent" class="scroll">
|
||||
<ul>
|
||||
<li v-for="item in props.listInfo.list" :key="item.id" :class="$style.item" @click="toDetail(item)">
|
||||
<div :class="$style.image">
|
||||
<img :class="$style.img" loading="lazy" decoding="async" :src="item.img">
|
||||
</div>
|
||||
<div :class="$style.desc">
|
||||
<h4>{{ item.name }}</h4>
|
||||
<div>
|
||||
<div :class="$style.songlist_info">
|
||||
<span v-if="item.total != null"><svg-icon name="music" />{{ item.total }}</span>
|
||||
<span v-if="item.play_count != null"><svg-icon name="headphones" />{{ item.play_count }}</span>
|
||||
</div>
|
||||
<p :class="$style.author">{{ item.author }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li v-for="(i, index) in 6" :key="index" :class="$style.item" style="margin-bottom: 0;height: 0;" />
|
||||
</ul>
|
||||
<div :class="$style.pagination">
|
||||
<material-pagination :count="props.listInfo.total" :limit="props.listInfo.limit" :page="props.listInfo.page" @btn-click="togglePage" />
|
||||
</div>
|
||||
</div>
|
||||
<transition enter-active-class="animated fadeIn" leave-active-class="animated fadeOut">
|
||||
<div v-show="props.listInfo.noItemLabel" :class="$style.noitem">
|
||||
<p v-text="props.listInfo.noItemLabel" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, defineEmits, defineExpose } from '@common/utils/vueTools'
|
||||
import { ListInfo, ListInfoItem } from '@renderer/store/songList/state'
|
||||
// import LX from '@renderer/types/lx'
|
||||
import { useRoute, useRouter } from '@common/utils/vueRouter'
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
listInfo: ListInfo
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const dom_list_ref = ref<HTMLElement | null>(null)
|
||||
|
||||
const emit = defineEmits(['toggle-page'])
|
||||
|
||||
|
||||
const togglePage = (page: number) => {
|
||||
emit('toggle-page', page)
|
||||
}
|
||||
|
||||
const toDetail = (info: ListInfoItem) => {
|
||||
router.push({
|
||||
path: '/songList/detail',
|
||||
query: {
|
||||
source: info.source,
|
||||
id: info.id,
|
||||
picUrl: info.img,
|
||||
fromName: route.name as string,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
scrollTo(top: number) {
|
||||
dom_list_ref.value?.scrollTo({
|
||||
top,
|
||||
// behavior: 'smooth',
|
||||
})
|
||||
},
|
||||
getScrollTop() {
|
||||
return dom_list_ref.value?.scrollTop ?? 0
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
.container {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.listContent {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
padding: 15px 15px 0;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
.item {
|
||||
max-width: 360px;
|
||||
width: 32%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
// flex-flow: column nowrap;
|
||||
// padding: 10px;
|
||||
margin-bottom: 20px;
|
||||
cursor: pointer;
|
||||
transition: opacity @transition-normal;
|
||||
&:hover {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
.image {
|
||||
flex: none;
|
||||
width: 40%;
|
||||
display: flex;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
opacity: .9;
|
||||
aspect-ratio: 1 / 1;
|
||||
|
||||
box-shadow: 0 0 2px 0 rgba(0,0,0,.2);
|
||||
}
|
||||
.img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.desc {
|
||||
flex: auto;
|
||||
padding: 2px 15px 2px 7px;
|
||||
overflow: hidden;
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
// height: 2.6em;
|
||||
text-align: justify;
|
||||
line-height: 1.3;
|
||||
.mixin-ellipsis-2;
|
||||
}
|
||||
}
|
||||
.songlist_info {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
gap: 15px;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
.mixin-ellipsis-1;
|
||||
text-align: justify;
|
||||
line-height: 1.2;
|
||||
// text-indent: 24px;
|
||||
color: var(--color-font-label);
|
||||
svg {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
.author {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
.mixin-ellipsis-1;
|
||||
text-align: justify;
|
||||
line-height: 1.2;
|
||||
// text-indent: 24px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
.pagination {
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
// left: 50%;
|
||||
// transform: translateX(-50%);
|
||||
}
|
||||
.noitem {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// background-color: var(--color-000);
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<base-tab :model-value="sortId" :class="$style.tab" :list="list" item-label="name" @change="handleToggle" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { watch, shallowReactive } from '@common/utils/vueTools'
|
||||
import { sortList } from '@renderer/store/songList/state'
|
||||
import { useRouter, useRoute } from '@common/utils/vueRouter'
|
||||
|
||||
const props = defineProps({
|
||||
source: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
tagId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sortId: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const list = shallowReactive([])
|
||||
|
||||
|
||||
const handleToggle = (id) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
source: props.source,
|
||||
tagId: props.tagId,
|
||||
sortId: id,
|
||||
},
|
||||
})
|
||||
}
|
||||
watch(() => props.source, async(source) => {
|
||||
// const source = (await getLeaderboardSetting()).source as LX.OnlineSource
|
||||
if (!source) return
|
||||
let _list = sortList[source] ?? []
|
||||
list.splice(0, list.length, ..._list)
|
||||
if (!props.sortId && list.length) handleToggle(list[0].id)
|
||||
// console.log(list)
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.tag-list {
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
.label {
|
||||
.icon {
|
||||
svg{
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
opacity: 1;
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 8px 15px;
|
||||
// background-color: var(--color-button-background);
|
||||
transition: background-color @transition-normal;
|
||||
// border-top: 2px solid @color-tab-border-bottom;
|
||||
// border-left: 2px solid @color-tab-border-bottom;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
// border-top-left-radius: 3px;
|
||||
color: var(--color-button-font);
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
flex: auto;
|
||||
}
|
||||
.icon {
|
||||
flex: none;
|
||||
margin-left: 7px;
|
||||
line-height: 0;
|
||||
svg {
|
||||
width: .9em;
|
||||
transition: transform .2s ease;
|
||||
transform: rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-button-background-active);
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 645px;
|
||||
left: 0;
|
||||
// border-bottom: 2px solid @color-tab-border-bottom;
|
||||
// border-right: 2px solid @color-tab-border-bottom;
|
||||
border-bottom-right-radius: 5px;
|
||||
background-color: var(--color-main-background);
|
||||
opacity: 0;
|
||||
transform: scaleY(0);
|
||||
overflow-y: auto;
|
||||
transform-origin: 0 0 0;
|
||||
max-height: 250px;
|
||||
transition: .25s ease;
|
||||
transition-property: transform, opacity;
|
||||
z-index: 10;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
|
||||
li {
|
||||
cursor: pointer;
|
||||
padding: 8px 15px;
|
||||
// color: var(--color-button-font);
|
||||
text-align: center;
|
||||
outline: none;
|
||||
transition: background-color @transition-normal;
|
||||
background-color: var(--color-button-background);
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-button-background-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.type {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 3px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
background-color: var(--color-button-background);
|
||||
padding: 8px 10px;
|
||||
border-radius: @radius-progress-border;
|
||||
transition: background-color @transition-normal;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-button-background-active);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<div :class="[$style.tagList, {[$style.active]: popupVisible}]">
|
||||
<div ref="dom_btn" :class="$style.label" @click="handleShow">
|
||||
<span>{{ tagName }}</span>
|
||||
<div :class="$style.icon">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 451.847 451.847" space="preserve">
|
||||
<use xlink:href="#icon-down" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.popup" :style="popupStyle" :aria-hidden="!popupVisible" @click.stop>
|
||||
<div :class="$style.list" class="scroll">
|
||||
<div :class="$style.tag" @click="handleToggleTag('')">{{ $t('default') }}</div>
|
||||
<dl v-for="tagInfo in list" :key="tagInfo.name">
|
||||
<dt :class="$style.type">{{ tagInfo.name }}</dt>
|
||||
<dd v-for="tag in tagInfo.list" :key="tag.id" :class="$style.tag" @click="handleToggleTag(tag.id)">{{ tag.name }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { watch, shallowReactive, ref, onMounted, onBeforeUnmount, computed, reactive } from '@common/utils/vueTools'
|
||||
import { setTags, getTags } from '@renderer/store/songList/action'
|
||||
import { tags, TagInfoTypeItem } from '@renderer/store/songList/state'
|
||||
import LX from '@renderer/types/lx'
|
||||
import { useRouter, useRoute } from '@common/utils/vueRouter'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
|
||||
const props = defineProps<{
|
||||
source: LX.OnlineSource
|
||||
tagId: string
|
||||
sortId?: string
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const t = useI18n()
|
||||
|
||||
const list = shallowReactive<TagInfoTypeItem[]>([])
|
||||
const handleToggleTag = (id: string) => {
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
source: props.source,
|
||||
tagId: id,
|
||||
sortId: props.sortId,
|
||||
},
|
||||
})
|
||||
handleHide()
|
||||
}
|
||||
watch(() => props.source, async(source) => {
|
||||
if (!source) return
|
||||
// const source = (await getLeaderboardSetting()).source as LX.OnlineSource
|
||||
let tagInfo = tags[source]
|
||||
// console.log(await getTags(source))
|
||||
if (tagInfo == null) setTags(tagInfo = await getTags(source), source)
|
||||
|
||||
list.splice(0, list.length, ...[{ name: window.i18n.t('songlist__tag_info_hot_tag'), list: [...tagInfo.hotTag] }, ...tagInfo.tags])
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
const tagName = computed(() => {
|
||||
if (!props.tagId) return t('default')
|
||||
for (const tags of list) {
|
||||
const tag = tags.list.find(t => t.id == props.tagId)
|
||||
if (tag) return tag.name
|
||||
}
|
||||
return props.tagId
|
||||
})
|
||||
|
||||
const popupStyle = reactive({
|
||||
width: '645px',
|
||||
maxHeight: '250px',
|
||||
})
|
||||
|
||||
const setTagPopupWidth = () => {
|
||||
window.setTimeout(() => {
|
||||
const dom_view = document.getElementById('view')
|
||||
popupStyle.width = dom_view!.clientWidth * 0.96 + 'px'
|
||||
popupStyle.maxHeight = dom_view!.clientHeight * 0.65 + 'px'
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const dom_btn = ref<HTMLElement | null>(null)
|
||||
const popupVisible = ref(false)
|
||||
const handleShow = () => popupVisible.value = !popupVisible.value
|
||||
const handleHide = (evt?: MouseEvent) => {
|
||||
// if (e && e.target.parentNode != this.$refs.dom_popup && this.show) return this.show = false
|
||||
// console.log(this.$refs)
|
||||
if (evt && (evt.target == dom_btn.value || dom_btn.value?.contains(evt.target as HTMLElement))) return
|
||||
setTimeout(() => {
|
||||
popupVisible.value = false
|
||||
}, 50)
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
setTagPopupWidth()
|
||||
document.addEventListener('click', handleHide)
|
||||
window.addEventListener('resize', setTagPopupWidth)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', handleHide)
|
||||
window.removeEventListener('resize', setTagPopupWidth)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.tag-list {
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
.label {
|
||||
.icon {
|
||||
svg{
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
.popup {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
pointer-events: initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 8px 15px;
|
||||
// background-color: var(--color-button-background);
|
||||
transition: color @transition-normal;
|
||||
// border-top: 2px solid @color-tab-border-bottom;
|
||||
// border-left: 2px solid @color-tab-border-bottom;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
// border-top-left-radius: 3px;
|
||||
color: var(--color-font);
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
|
||||
span {
|
||||
flex: auto;
|
||||
}
|
||||
.icon {
|
||||
flex: none;
|
||||
margin-left: 7px;
|
||||
line-height: 0;
|
||||
svg {
|
||||
width: .8em;
|
||||
transition: transform .2s ease;
|
||||
transform: rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary-font-hover);
|
||||
}
|
||||
&:active {
|
||||
color: var(--color-primary-font-active);
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
width: 645px;
|
||||
left: 8px;
|
||||
margin-top: 12px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--color-content-background);
|
||||
opacity: 0;
|
||||
transform: scale(.95, .8);
|
||||
transform-origin: 0 0 0;
|
||||
transition: .25s ease;
|
||||
transition-property: transform, opacity;
|
||||
max-height: 250px;
|
||||
z-index: 10;
|
||||
pointer-events: none;
|
||||
filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, .15));
|
||||
display: flex;
|
||||
|
||||
&:before {
|
||||
content: " ";
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: 20px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 8px solid transparent;
|
||||
border-right: 8px solid transparent;
|
||||
border-bottom: 8px solid var(--color-content-background);
|
||||
}
|
||||
}
|
||||
.list {
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
// box-shadow: 0 0 4px rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.type {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 3px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-block;
|
||||
margin: 5px;
|
||||
background-color: var(--color-button-background);
|
||||
padding: 8px 10px;
|
||||
border-radius: @radius-progress-border;
|
||||
transition: background-color @transition-normal;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-button-background-active);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.header">
|
||||
<div :class="$style.left">
|
||||
<tag-list :source="source" :tag-id="tagId" :sort-id="sortId" />
|
||||
<sort-tab :source="source" :tag-id="tagId" :sort-id="sortId" />
|
||||
</div>
|
||||
<base-btn :class="$style.btn" outline min @click="visibleOpenSongListModal = true">{{ $t('songlist__import_input_show_btn') }}</base-btn>
|
||||
<base-selection :model-value="source" :class="$style.select" :list="sourceList" item-key="id" item-name="name" @update:model-value="handleToggleSource" />
|
||||
</div>
|
||||
<list-view :source="source" :tag-id="tagId" :sort-id="sortId" :page="page" />
|
||||
<open-list-modal v-model="visibleOpenSongListModal" :source-list="sourceList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, ref } from '@common/utils/vueTools'
|
||||
import { getSongListSetting, setSongListSetting } from '@renderer/utils/data'
|
||||
import TagList from './components/TagList.vue'
|
||||
import SortTab from './components/SortTab.vue'
|
||||
import OpenListModal from './components/OpenListModal.vue'
|
||||
import ListView from './ListView.vue'
|
||||
import { sources, listInfo, isVisibleListDetail } from '@renderer/store/songList/state'
|
||||
import { sourceNames } from '@renderer/store'
|
||||
import LX from '@renderer/types/lx'
|
||||
import { useRoute, useRouter } from '@common/utils/vueRouter'
|
||||
|
||||
const source = ref<LX.OnlineSource>('kw')
|
||||
const tagId = ref<string>('')
|
||||
const sortId = ref<string>('')
|
||||
const page = ref<number>(1)
|
||||
|
||||
|
||||
interface Query {
|
||||
source?: string
|
||||
tagId?: string
|
||||
sortId?: string
|
||||
page?: string
|
||||
}
|
||||
|
||||
const verifyQueryParams = async function(this: any, to: { query: Query, path: string}, from: any, next: (route?: { path: string; query: Query }) => void) {
|
||||
let _source = to.query.source
|
||||
let _tagId = to.query.tagId
|
||||
let _sortId = to.query.sortId
|
||||
let _page: string | undefined = to.query.page
|
||||
|
||||
if (isVisibleListDetail.value) {
|
||||
next({ path: '/songList/detail', query: {} })
|
||||
return
|
||||
} else if (_source == null) {
|
||||
if (listInfo.key) {
|
||||
_source = listInfo.source
|
||||
_tagId = listInfo.tagId
|
||||
_sortId = listInfo.sortId
|
||||
_page = listInfo.page.toString()
|
||||
} else {
|
||||
const setting = await getSongListSetting()
|
||||
_source = setting.source
|
||||
_tagId = setting.tagId
|
||||
_sortId = setting.sortId
|
||||
_page = '1'
|
||||
}
|
||||
|
||||
next({
|
||||
path: to.path,
|
||||
query: { ...to.query, source: _source, tagId: _tagId, sortId: _sortId, page: _page },
|
||||
})
|
||||
return
|
||||
}
|
||||
next()
|
||||
source.value = _source as LX.OnlineSource
|
||||
tagId.value = _tagId ?? ''
|
||||
sortId.value = _sortId ?? ''
|
||||
page.value = _page ? parseInt(_page) : 1
|
||||
setSongListSetting({ source: _source, tagId: _tagId, sortId: _sortId })
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TagList,
|
||||
SortTab,
|
||||
ListView,
|
||||
OpenListModal,
|
||||
},
|
||||
beforeRouteEnter: verifyQueryParams,
|
||||
beforeRouteUpdate: verifyQueryParams,
|
||||
setup() {
|
||||
const visibleOpenSongListModal = ref(false)
|
||||
|
||||
const sourceList = computed(() => {
|
||||
return sources.map(s => ({ id: s, name: sourceNames.value[s] }))
|
||||
})
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const handleToggleSource = (id: LX.OnlineSource) => {
|
||||
if (id == source.value) return
|
||||
router.replace({
|
||||
path: route.path,
|
||||
query: {
|
||||
source: id,
|
||||
tagId: '',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
source,
|
||||
tagId,
|
||||
sortId,
|
||||
page,
|
||||
sourceList,
|
||||
handleToggleSource,
|
||||
visibleOpenSongListModal,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
position: relative;
|
||||
}
|
||||
.header {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
// padding-right: 5px;
|
||||
// box-sizing: border-box;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.left {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
color: var(--color-font);
|
||||
transition: color @transition-fast;
|
||||
background: none !important;
|
||||
&:hover {
|
||||
color: var(--color-primary-font-hover);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.select {
|
||||
font-size: 12px;
|
||||
width: auto;
|
||||
flex: none;
|
||||
padding: 0 5px;
|
||||
|
||||
&:hover {
|
||||
:global(.icon) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:global {
|
||||
.label-content {
|
||||
background-color: transparent !important;
|
||||
transition: color @transition-fast;
|
||||
color: var(--color-font);
|
||||
// line-height: 38px;
|
||||
// height: 38px;
|
||||
border-radius: 0;
|
||||
&:hover {
|
||||
// background: none !important;
|
||||
color: var(--color-primary-font-hover);
|
||||
.icon {
|
||||
opacity: 1;
|
||||
// color: var(--color-primary-font-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
// .label {
|
||||
// color: var(--color-font) !important;
|
||||
// }
|
||||
.icon {
|
||||
svg {
|
||||
width: .8em;
|
||||
}
|
||||
// opacity: .6;
|
||||
// transition: color @transition-fast;
|
||||
// color: var(--color-font-label);
|
||||
}
|
||||
|
||||
.selection-list {
|
||||
max-height: 500px;
|
||||
box-shadow: 0 1px 4px 0 rgba(0,0,0,.2);
|
||||
li {
|
||||
// background-color: var(--color-main-background);
|
||||
text-align: center;
|
||||
line-height: 38px;
|
||||
font-size: 13px;
|
||||
&:hover {
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-button-background-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,619 +0,0 @@
|
||||
<template lang="pug">
|
||||
div(:class="$style.container")
|
||||
//- 歌曲列表页放在底层,在隐藏列表页时不会出现列表项的间隔线突兀感
|
||||
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
|
||||
div(:class="$style.songListDetailContent" v-if="isVisibleListDetail")
|
||||
div(:class="$style.songListHeader")
|
||||
div(:class="$style.songListHeaderLeft" :style="{ backgroundImage: 'url('+(selectListInfo.img || listDetail.info.img)+')' }")
|
||||
//- img(:src="listDetail.info.img || selectListInfo.img")
|
||||
span(:class="$style.playNum" v-if="listDetail.info.play_count || selectListInfo.play_count") {{listDetail.info.play_count || selectListInfo.play_count}}
|
||||
div(:class="$style.songListHeaderMiddle")
|
||||
h3(:title="listDetail.info.name || selectListInfo.name") {{listDetail.info.name || selectListInfo.name}}
|
||||
p(:title="listDetail.info.desc || selectListInfo.desc") {{listDetail.info.desc || selectListInfo.desc}}
|
||||
div(:class="$style.songListHeaderRight")
|
||||
base-btn(:class="$style.headerRightBtn" :disabled="detailLoading" @click="playSongListDetail()") {{$t('list__play')}}
|
||||
base-btn(:class="$style.headerRightBtn" :disabled="detailLoading" @click="addSongListDetail") {{$t('list__collect')}}
|
||||
base-btn(:class="$style.headerRightBtn" @click="hideListDetail") {{$t('back')}}
|
||||
material-online-list(ref="songList" @play-list="playSongListDetail" @toggle-page="handleToggleListDetailPage" :page="listDetail.page" :limit="listDetail.limit" :total="listDetail.total"
|
||||
:list="listDetail.list" :noItem="isGetDetailFailed ? $t('list__load_failed') : $t('list__loading')")
|
||||
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
|
||||
div(:class="$style.songListContainer" v-show="!isVisibleListDetail")
|
||||
div(:class="$style.header")
|
||||
tag-list(:class="$style.tagList" :list-width="listWidth" ref="tagList" :list="tagList" v-model="tagInfo")
|
||||
base-tab(:class="$style.tab" :list="sorts" item-key="id" ref="tab" item-name="name" v-model="sortId")
|
||||
base-select(:class="$style.select" :list="sourceInfo.sources" item-key="id" item-name="name" v-model="source")
|
||||
div(:class="$style.songListContent")
|
||||
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
|
||||
div(:class="$style.songListContent")
|
||||
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
|
||||
div.scroll(:class="$style.songList" v-if="sortId !== 'importSongList'" ref="dom_scrollContent")
|
||||
ul
|
||||
li(:class="$style.item" v-for="(item, index) in listData.list" @click="handleItemClick(index)")
|
||||
div(:class="$style.left" :style="{ backgroundImage: 'url('+item.img+')' }")
|
||||
//- img(:src="item.img")
|
||||
div(:class="$style.right" :src="item.img")
|
||||
h4 {{item.name}}
|
||||
div(:class="$style.songlist_info")
|
||||
span(v-if="item.total != null")
|
||||
svg-icon(name="music")
|
||||
| {{item.total}}
|
||||
span
|
||||
svg-icon(name="headphones")
|
||||
| {{item.play_count}}
|
||||
p(:class="$style.author") {{item.author}}
|
||||
li(:class="$style.item" style="cursor: default;" v-for="i in spaceNum")
|
||||
div(:class="$style.pagination")
|
||||
material-pagination(:count="listData.total" :limit="listData.limit" :page="listData.page" @btn-click="handleToggleListPage")
|
||||
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
|
||||
div(:class="$style.importSongListContent" v-show="sortId === 'importSongList'")
|
||||
div(:style="{ width: '50%' }")
|
||||
material-search-input(:class="$style.searchInput" v-model="importSongListText" @event="handleImportSongListEvent" big :placeholder="$t('songlist__import_input_tip')")
|
||||
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='100%' viewBox='0 0 451.846 451.847' space='preserve')
|
||||
use(xlink:href='#icon-right')
|
||||
div(:class="$style.tips")
|
||||
ul
|
||||
li {{$t('songlist__import_input_tip_1')}}
|
||||
li {{$t('songlist__import_input_tip_2')}}
|
||||
li {{$t('songlist__import_input_tip_3')}}
|
||||
transition(enter-active-class="animated-fast fadeIn" leave-active-class="animated-fast fadeOut")
|
||||
div(v-show="!listData.list.length && sortId !== 'importSongList'" :class="$style.noitem")
|
||||
p {{$t('list__loading')}}
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapMutations, mapActions } from 'vuex'
|
||||
import TagList from './components/TagList'
|
||||
import { tempList } from '@renderer/core/share/list'
|
||||
export default {
|
||||
name: 'SongList',
|
||||
components: {
|
||||
TagList,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tagInfo: {
|
||||
name: this.$t('default'),
|
||||
id: null,
|
||||
},
|
||||
sortId: undefined,
|
||||
source: null,
|
||||
isShowDownload: false,
|
||||
musicInfo: null,
|
||||
isShowDownloadMultiple: false,
|
||||
isToggleSource: false,
|
||||
isShowListAdd: false,
|
||||
isShowListAddMultiple: false,
|
||||
importSongListText: '',
|
||||
listWidth: 645,
|
||||
isGetDetailFailed: false,
|
||||
detailLoading: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['setting']),
|
||||
...mapGetters('songList', ['sourceInfo', 'tags', 'listData', 'isVisibleListDetail', 'selectListInfo', 'listDetail']),
|
||||
sorts() {
|
||||
let list
|
||||
list = this.source ? [...this.sourceInfo.sortList[this.source]] : []
|
||||
switch (this.source) {
|
||||
case 'wy':
|
||||
case 'kw':
|
||||
// case 'bd':
|
||||
case 'tx':
|
||||
case 'mg':
|
||||
case 'kg':
|
||||
// case 'xm':
|
||||
list.push({
|
||||
name: this.$t('songlist__open_list', { name: this.sourceInfo.sources.find(s => s.id == this.source).name }),
|
||||
id: 'importSongList',
|
||||
})
|
||||
}
|
||||
return list
|
||||
},
|
||||
tagList() {
|
||||
let tagInfo = this.tags[this.source]
|
||||
return tagInfo ? [{ name: '热门标签', list: [...tagInfo.hotTag] }, ...tagInfo.tags] : []
|
||||
},
|
||||
spaceNum() {
|
||||
let num = this.listData.list ? this.listData.list.length % 3 : 0
|
||||
return num > 0 ? 3 - num : 0
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
sortId(n, o) {
|
||||
this.setSongList({ sortId: n })
|
||||
if (o === undefined && this.listData.page !== 1) return
|
||||
if (n == 'importSongList') return
|
||||
this.$nextTick(() => {
|
||||
this.getList(1).then(() => {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dom_scrollContent.scrollTo(0, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
// if (this.isVisibleListDetail) this.setVisibleListDetail(false)
|
||||
},
|
||||
tagInfo(n, o) {
|
||||
this.setSongList({ tagInfo: n })
|
||||
if (!o && this.listData.page !== 1) return
|
||||
if (this.sortId == 'importSongList') this.sortId = this.sorts[0] && this.sorts[0].id
|
||||
if (this.isToggleSource) {
|
||||
this.isToggleSource = false
|
||||
return
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.getList(1).then(() => {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dom_scrollContent.scrollTo(0, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
// if (this.isVisibleListDetail) this.setVisibleListDetail(false)
|
||||
},
|
||||
source(n, o) {
|
||||
this.setSongList({ source: n })
|
||||
if (!this.tags[n]) this.getTags()
|
||||
this.importSongListText = ''
|
||||
if (o) {
|
||||
this.isToggleSource = true
|
||||
this.tagInfo = {
|
||||
name: '默认',
|
||||
id: null,
|
||||
}
|
||||
this.sortId = this.sorts[0] && this.sorts[0].id
|
||||
}
|
||||
},
|
||||
'setting.theme.id'() {
|
||||
this.setTagListWidth()
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.source = this.setting.songList.source
|
||||
if (!this.sourceInfo.sourceIds.includes(this.source)) this.source = this.sourceInfo.sourceIds[0]
|
||||
this.isToggleSource = true
|
||||
this.tagInfo = this.setting.songList.tagInfo
|
||||
this.sortId = this.setting.songList.sortId
|
||||
if (!this.isVisibleListDetail) this.setTagListWidth()
|
||||
this.listenEvent()
|
||||
|
||||
if (this.$route.query.source && (this.$route.query.id || this.$route.query.url)) {
|
||||
this.handleRouteParams(this.$route.query.id, this.$route.query.url, this.$route.query.source)
|
||||
this.$router.replace({
|
||||
path: '/songList',
|
||||
})
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.unlistenEvent()
|
||||
},
|
||||
beforeRouteUpdate(to) {
|
||||
if (to.query.source && (to.query.id || to.query.url)) {
|
||||
this.handleRouteParams(to.query.id, to.query.url, to.query.source)
|
||||
return {
|
||||
path: '/songList',
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapMutations(['setSongList']),
|
||||
...mapActions('songList', ['getTags', 'getList', 'getListDetail', 'getListDetailAll']),
|
||||
...mapMutations('songList', ['setVisibleListDetail', 'setSelectListInfo']),
|
||||
...mapActions('download', ['createDownload', 'createDownloadMultiple']),
|
||||
...mapMutations('list', ['listAdd', 'listAddMultiple', 'createUserList']),
|
||||
...mapMutations('player', {
|
||||
setTempList: 'setTempList',
|
||||
updateTempList: 'updateTempList',
|
||||
setTempPlayList: 'setTempPlayList',
|
||||
}),
|
||||
listenEvent() {
|
||||
window.eventHub.on('key_backspace_down', this.handle_key_backspace_down)
|
||||
window.addEventListener('resize', this.handleSetTagWidth)
|
||||
},
|
||||
unlistenEvent() {
|
||||
window.eventHub.off('key_backspace_down', this.handle_key_backspace_down)
|
||||
window.removeEventListener('resize', this.handleSetTagWidth)
|
||||
},
|
||||
handleSetTagWidth() {
|
||||
setTimeout(() => {
|
||||
this.setTagListWidth()
|
||||
setTimeout(this.setTagListWidth, 100)
|
||||
})
|
||||
},
|
||||
handle_key_backspace_down({ event }) {
|
||||
if (!this.isVisibleListDetail ||
|
||||
event.repeat ||
|
||||
this.isShowDownload ||
|
||||
this.isShowDownloadMultiple ||
|
||||
this.isShowListAdd ||
|
||||
this.isShowListAddMultiple ||
|
||||
event.target.classList.contains('key-bind')) return
|
||||
this.hideListDetail()
|
||||
},
|
||||
handleRouteParams(id, url, source) {
|
||||
if (!id) id = decodeURIComponent(url)
|
||||
this.handleGetSongListDetail(id, source)
|
||||
},
|
||||
handleToggleListPage(page) {
|
||||
this.getList(page).then(() => {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.dom_scrollContent.scrollTo(0, 0)
|
||||
})
|
||||
})
|
||||
},
|
||||
handleToggleListDetailPage(page) {
|
||||
this.handleGetListDetail(this.selectListInfo.id, this.selectListInfo.source, page).then(() => {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.songList.scrollToTop()
|
||||
})
|
||||
})
|
||||
},
|
||||
handleItemClick(index) {
|
||||
// this.detailLoading = true
|
||||
this.setSelectListInfo(this.listData.list[index])
|
||||
this.setVisibleListDetail(true)
|
||||
this.$nextTick(() => {
|
||||
this.handleGetListDetail(this.selectListInfo.id, this.source, 1)
|
||||
})
|
||||
},
|
||||
// handleFlowBtnClick(action) {
|
||||
// switch (action) {
|
||||
// case 'download':
|
||||
// this.isShowDownloadMultiple = true
|
||||
// break
|
||||
// case 'play':
|
||||
// this.testPlay()
|
||||
// break
|
||||
// case 'add':
|
||||
// this.isShowListAddMultiple = true
|
||||
// break
|
||||
// }
|
||||
// },
|
||||
hideListDetail() {
|
||||
setTimeout(async() => {
|
||||
this.setVisibleListDetail(false)
|
||||
await this.$nextTick()
|
||||
this.setTagListWidth()
|
||||
}, 50)
|
||||
},
|
||||
handleImportSongListEvent({ action }) {
|
||||
switch (action) {
|
||||
case 'submit':
|
||||
this.handleGetSongListDetail(this.importSongListText, this.source)
|
||||
break
|
||||
// case 'blur':
|
||||
// break
|
||||
}
|
||||
},
|
||||
handleGetSongListDetail(id, source) {
|
||||
if (!id.length) return
|
||||
console.log(id, source)
|
||||
this.setSelectListInfo({
|
||||
play_count: null,
|
||||
id,
|
||||
author: '',
|
||||
name: '',
|
||||
img: null,
|
||||
desc: '',
|
||||
source,
|
||||
})
|
||||
this.setVisibleListDetail(true)
|
||||
this.handleGetListDetail(id, source, 1)
|
||||
},
|
||||
setTagListWidth() {
|
||||
if (this.isVisibleListDetail) return
|
||||
this.listWidth = this.$refs.tagList.$el.clientWidth + this.$refs.tab.$el.clientWidth + 2
|
||||
},
|
||||
handleGetListDetail(id, source, page) {
|
||||
this.detailLoading = true
|
||||
this.isGetDetailFailed = false
|
||||
return this.getListDetail({ id, source, page }).catch(err => {
|
||||
this.isGetDetailFailed = true
|
||||
return Promise.reject(err)
|
||||
}).finally(() => {
|
||||
this.detailLoading = false
|
||||
})
|
||||
},
|
||||
async fetchList() {
|
||||
this.detailLoading = true
|
||||
return this.getListDetailAll({ source: this.listDetail.source, id: this.listDetail.id }).finally(() => {
|
||||
this.detailLoading = false
|
||||
})
|
||||
},
|
||||
async addSongListDetail() {
|
||||
// console.log(this.listDetail.info)
|
||||
// if (!this.listDetail.info.name) return
|
||||
const list = await this.fetchList()
|
||||
this.createUserList({
|
||||
name: this.listDetail.info.name || `${this.listDetail.source}-list`,
|
||||
id: `${this.listDetail.source}__${this.listDetail.id}`,
|
||||
list,
|
||||
source: this.listDetail.source,
|
||||
sourceListId: this.listDetail.id,
|
||||
})
|
||||
},
|
||||
async playSongListDetail(index = 0) {
|
||||
if (!this.listDetail.info.name) return
|
||||
const id = `${this.listDetail.source}__${this.listDetail.id}`
|
||||
let isPlayingList = false
|
||||
if (this.listDetail.list?.length) {
|
||||
this.setTempList({
|
||||
list: [...this.listDetail.list],
|
||||
index,
|
||||
id,
|
||||
})
|
||||
isPlayingList = true
|
||||
}
|
||||
const list = await this.fetchList()
|
||||
if (!list.length) return
|
||||
if (isPlayingList) {
|
||||
if (tempList.meta.id == id) {
|
||||
this.updateTempList({
|
||||
list,
|
||||
id,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.setTempList({
|
||||
list,
|
||||
index,
|
||||
id,
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.header {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
.tab {
|
||||
flex: auto;
|
||||
}
|
||||
.select {
|
||||
flex: none;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.songListContainer, .song-list-detail-content, .songListContent, .importSongListContent {
|
||||
// flex: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.songListContainer, .songListContent, .importSongListContent {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
height: 100%;
|
||||
// position: relative;
|
||||
}
|
||||
|
||||
.importSongListContent {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
padding-top: 20%;
|
||||
}
|
||||
.songList {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
.song-list-header {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
height: 60px;
|
||||
}
|
||||
.song-list-header-left {
|
||||
flex: none;
|
||||
margin-left: 5px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
opacity: .9;
|
||||
|
||||
.play-num {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 2px;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.song-list-header-middle {
|
||||
flex: auto;
|
||||
padding: 5px 7px;
|
||||
h3 {
|
||||
.mixin-ellipsis-1;
|
||||
line-height: 1.2;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
p {
|
||||
.mixin-ellipsis-2;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
color: #888;
|
||||
}
|
||||
}
|
||||
.song-list-header-right {
|
||||
flex: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 15px;
|
||||
|
||||
.header-right-btn {
|
||||
border-radius: 0;
|
||||
&:first-child {
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
&:last-child {
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.song-list-detail-content {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.songList {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 0 15px;
|
||||
ul {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
.item {
|
||||
width: 32%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
margin-top: 15px;
|
||||
cursor: pointer;
|
||||
transition: opacity @transition-theme;
|
||||
&:hover {
|
||||
opacity: .7;
|
||||
}
|
||||
}
|
||||
.left {
|
||||
flex: none;
|
||||
width: 88px;
|
||||
height: 88px;
|
||||
display: flex;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
opacity: .9;
|
||||
// box-shadow: 0 0 2px 0 rgba(0,0,0,.2);
|
||||
}
|
||||
.right {
|
||||
flex: auto;
|
||||
padding: 3px 15px 5px 7px;
|
||||
overflow: hidden;
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
height: 2.6em;
|
||||
text-align: justify;
|
||||
line-height: 1.3;
|
||||
.mixin-ellipsis-2;
|
||||
}
|
||||
}
|
||||
.songlist_info {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
gap: 15px;
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
.mixin-ellipsis-1;
|
||||
text-align: justify;
|
||||
line-height: 1.2;
|
||||
// text-indent: 24px;
|
||||
color: @color-theme_2-font-label;
|
||||
svg {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
.author {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
.mixin-ellipsis-1;
|
||||
text-align: justify;
|
||||
line-height: 1.2;
|
||||
// text-indent: 24px;
|
||||
color: @color-theme_2-font-label;
|
||||
}
|
||||
.pagination {
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
// left: 50%;
|
||||
// transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.noitem {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: @color-theme_2-font-label;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
padding: 15px 0;
|
||||
font-size: 12px;
|
||||
color: @color-theme_2-font;
|
||||
line-height: 1.5;
|
||||
ul {
|
||||
list-style: decimal;
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
each(@themes, {
|
||||
:global(#root.@{value}) {
|
||||
.song-list-header-middle {
|
||||
p {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
.right {
|
||||
p {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
.noitem {
|
||||
p {
|
||||
color: ~'@{color-@{value}-theme_2-font-label}';
|
||||
}
|
||||
}
|
||||
.tips {
|
||||
color: ~'@{color-@{value}-theme_2-font}';
|
||||
}
|
||||
}
|
||||
})
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user