This commit is contained in:
lyswhut
2022-10-29 11:36:35 +08:00
parent 66b965d9f7
commit e45cee6596
715 changed files with 57096 additions and 31771 deletions
-568
View File
@@ -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>
+255
View File
@@ -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>
+106
View File
@@ -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,
}
}
+150
View File
@@ -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,
}
}
+23
View File
@@ -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,
}
}
+39
View File
@@ -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,
}
}
-446
View File
@@ -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,
}
}
+57
View File
@@ -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)
}
}
+189
View File
@@ -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>
@@ -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>
@@ -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>
+375
View File
@@ -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>
@@ -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 = () => {
@@ -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,
}
@@ -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,
@@ -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
@@ -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,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,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)
@@ -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] }])
}
}
@@ -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 {
@@ -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
+28
View File
@@ -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>
+379
View File
@@ -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>
+59
View File
@@ -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,
}
}
+192
View File
@@ -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,
}
}
+106
View File
@@ -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,
}
}
+19
View File
@@ -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,
}
}
+72
View File
@@ -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>
-753
View File
@@ -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>
+142
View File
@@ -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>
-329
View File
@@ -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>
-329
View File
@@ -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
| 可以加入测试企鹅群&nbsp;
span.hover(:aria-label="$t('setting__click_open')" @click="openUrl('https://qm.qq.com/cgi-bin/qm/qr?k=zR6aYosQoKb07g4FGFZdO9n9zL1dhFpE&jump_from=webapi')") 768786588
| &nbsp;,注意:测试版的功可能会不稳定,
| &nbsp;,注意:测试版的功能可能会不稳定,
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>
+303
View File
@@ -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>
-105
View File
@@ -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>
+215
View File
@@ -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>
-619
View File
@@ -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