v2.0.0
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<material-modal :show="show" teleport="#view" @close="handleClose" @after-enter="$refs.input.focus()">
|
||||
<main :class="$style.main">
|
||||
<h2>{{ selectedNum > 0 ? $t('music_sort__title_multiple', { num: selectedNum }) : $t('music_sort__title', { name: musicInfo ? musicInfo.name : '' }) }}</h2>
|
||||
<base-input
|
||||
ref="input"
|
||||
v-model="sortNum"
|
||||
:class="$style.input"
|
||||
type="number"
|
||||
:placeholder="$t('music_sort__input_tip')"
|
||||
@submit="handleSubmit" @blur="verify"
|
||||
/>
|
||||
<div :class="$style.footer">
|
||||
<base-btn :class="$style.btn" @click="handleSubmit">{{ $t('btn_confirm') }}</base-btn>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
musicInfo: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
selectedNum: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
emits: ['update:show', 'confirm'],
|
||||
data() {
|
||||
return {
|
||||
sortNum: '',
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(n) {
|
||||
if (n) {
|
||||
this.sortNum = ''
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:show', false)
|
||||
},
|
||||
verify() {
|
||||
let num = /^[1-9]\d*/.exec(this.sortNum)
|
||||
num = num ? parseInt(num[0]) : ''
|
||||
this.sortNum = num.toString()
|
||||
return num
|
||||
},
|
||||
handleSubmit() {
|
||||
let num = this.verify()
|
||||
if (this.sortNum == '') return
|
||||
this.handleClose()
|
||||
this.$emit('confirm', num)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.main {
|
||||
padding: 0 15px;
|
||||
max-width: 530px;
|
||||
min-width: 280px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-height: 0;
|
||||
// max-height: 100%;
|
||||
// overflow: hidden;
|
||||
h2 {
|
||||
font-size: 13px;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
word-break: break-all;
|
||||
// text-align: center;
|
||||
padding: 15px 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
// width: 100%;
|
||||
// height: 26px;
|
||||
padding: 8px 8px;
|
||||
}
|
||||
.footer {
|
||||
margin: 20px 0 15px auto;
|
||||
}
|
||||
.btn {
|
||||
// box-sizing: border-box;
|
||||
// margin-left: 15px;
|
||||
// margin-bottom: 15px;
|
||||
// height: 36px;
|
||||
// line-height: 36px;
|
||||
// padding: 0 10px !important;
|
||||
min-width: 70px;
|
||||
// .mixin-ellipsis-1;
|
||||
|
||||
+.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,348 @@
|
||||
<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 { debounce } from '@common/utils'
|
||||
import { clipboardReadText } from '@common/utils/electron'
|
||||
import { toRaw } from '@common/utils/vueTools'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: 'Find for something...',
|
||||
},
|
||||
list: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['action'],
|
||||
data() {
|
||||
return {
|
||||
text: '',
|
||||
selectIndex: -1,
|
||||
listStyle: {
|
||||
height: 0,
|
||||
maxHeight: 0,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
maxHeight: 0,
|
||||
resultList: [],
|
||||
isModDown: false,
|
||||
isShow: false,
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
resultList(n) {
|
||||
if (this.selectIndex > -1) this.selectIndex = -1
|
||||
this.$nextTick(() => {
|
||||
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) {
|
||||
if (!this.visible) return
|
||||
this.handleDelaySearch()
|
||||
},
|
||||
visible(n) {
|
||||
if (!n) return
|
||||
this.isShow = true
|
||||
this.init()
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.handleDelaySearch = debounce(() => {
|
||||
this.handleSearch()
|
||||
})
|
||||
if (this.visible) this.isShow = true
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
// 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.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() {
|
||||
if (!this.visible) return
|
||||
this.handleSearch()
|
||||
this.$nextTick(() => {
|
||||
if (!this.listStyle.maxHeight) {
|
||||
this.maxHeight = this.$refs.dom_container.offsetParent.clientHeight - this.$refs.dom_list.offsetTop - 70
|
||||
this.listStyle.maxHeight = this.maxHeight + 'px'
|
||||
}
|
||||
this.$refs.dom_input.focus()
|
||||
})
|
||||
},
|
||||
handleKeyEsc() {
|
||||
if (this.text.length > 0) {
|
||||
this.text = ''
|
||||
this.resultList = []
|
||||
} else {
|
||||
this.handleHide()
|
||||
}
|
||||
},
|
||||
handle_key_mod_down() {
|
||||
console.log('handle_key_mod_down')
|
||||
if (!this.isModDown) this.isModDown = true
|
||||
},
|
||||
handle_key_mod_up() {
|
||||
if (this.isModDown) this.isModDown = false
|
||||
},
|
||||
handle_key_mod_f_down() {
|
||||
if (this.visible) this.$refs.dom_input.focus()
|
||||
},
|
||||
handleAnimated() {
|
||||
if (this.visible) return
|
||||
this.isShow = false
|
||||
},
|
||||
handleTemplistClick(index) {
|
||||
if (index < 0) return
|
||||
const id = this.resultList[index].id
|
||||
this.sendEvent('listClick', {
|
||||
index: this.list.findIndex(m => m.id == id),
|
||||
isPlay: this.isModDown,
|
||||
})
|
||||
},
|
||||
handleHide() {
|
||||
this.sendEvent('hide')
|
||||
},
|
||||
sendEvent(action, data) {
|
||||
this.$emit('action', {
|
||||
action,
|
||||
data,
|
||||
})
|
||||
},
|
||||
handleKeyDown() {
|
||||
if (!this.resultList.length) return
|
||||
this.selectIndex = this.selectIndex + 1 < this.resultList.length ? this.selectIndex + 1 : 0
|
||||
this.handleScrollList()
|
||||
},
|
||||
handleKeyUp() {
|
||||
if (!this.resultList.length) return
|
||||
this.selectIndex = this.selectIndex - 1 < -1 ? this.resultList.length - 1 : this.selectIndex - 1
|
||||
this.handleScrollList()
|
||||
},
|
||||
handleScrollList() {
|
||||
if (this.selectIndex < 0) return
|
||||
let dom = this.$refs.dom_list.children[this.selectIndex]
|
||||
let offsetTop = dom.offsetTop
|
||||
let scrollTop = this.$refs.dom_scrollContainer.scrollTop
|
||||
let top
|
||||
if (offsetTop < scrollTop) {
|
||||
top = offsetTop
|
||||
} else if (offsetTop + dom.clientHeight > this.$refs.dom_scrollContainer.clientHeight + scrollTop) {
|
||||
top = offsetTop + dom.clientHeight - this.$refs.dom_scrollContainer.clientHeight
|
||||
} else return
|
||||
this.$refs.dom_scrollContainer.scrollTo(0, top)
|
||||
},
|
||||
handleContextMenu() {
|
||||
let str = clipboardReadText()
|
||||
str = str.replace(/\t|\r\n|\n|\r/g, ' ')
|
||||
str = str.replace(/\s+/g, ' ')
|
||||
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)}`
|
||||
},
|
||||
async handleSearch() {
|
||||
if (!this.text.length) return this.resultList = []
|
||||
this.resultList = await window.lx.worker.main.searchListMusic(toRaw(this.list), this.text)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
top: 20px;
|
||||
width: 45%;
|
||||
height: @height-toolbar * 0.52;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.search {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
transition: box-shadow .4s ease, background-color @transition-normal;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
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),
|
||||
0 8px 16px rgba(0,0,0,0.07),
|
||||
0 16px 32px rgba(0,0,0,0.07),
|
||||
0 32px 64px rgba(0,0,0,0.07);
|
||||
|
||||
&.active {
|
||||
.form {
|
||||
input {
|
||||
border-bottom-left-radius: 0;
|
||||
|
||||
}
|
||||
button {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.form {
|
||||
display: flex;
|
||||
height: @height-toolbar * 0.52;
|
||||
position: relative;
|
||||
input {
|
||||
flex: auto;
|
||||
// border: 1px solid;
|
||||
border-top-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
background-color: transparent;
|
||||
// border-bottom: 2px solid var(--color-primary);
|
||||
// border-color: var(--color-primary);
|
||||
border: none;
|
||||
|
||||
outline: none;
|
||||
// height: @height-toolbar * .7;
|
||||
padding: 0 5px;
|
||||
overflow: hidden;
|
||||
font-size: 13.5px;
|
||||
line-height: @height-toolbar * 0.52 + 5px;
|
||||
&::placeholder {
|
||||
color: var(--color-button-font);
|
||||
}
|
||||
}
|
||||
button {
|
||||
flex: none;
|
||||
border: none;
|
||||
// background-color: @color-search-form-background;
|
||||
background-color: transparent;
|
||||
outline: none;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
padding: 6px 7px;
|
||||
color: var(--color-button-font);
|
||||
transition: background-color .2s ease;
|
||||
opacity: 0.8;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-button-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-button-background-active);
|
||||
}
|
||||
}
|
||||
}
|
||||
.list {
|
||||
// background-color: @color-search-form-background;
|
||||
font-size: 13px;
|
||||
transition: .3s ease;
|
||||
height: 0;
|
||||
transition-property: height;
|
||||
position: relative;
|
||||
scroll-behavior: smooth;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
padding: 8px 5px;
|
||||
transition: background-color .2s ease;
|
||||
line-height: 1.3;
|
||||
// overflow: hidden;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
|
||||
&.select {
|
||||
background-color: var(--color-primary-dark-100-alpha-700);
|
||||
}
|
||||
border-radius: 4px;
|
||||
// &:last-child {
|
||||
// border-bottom-left-radius: 4px;
|
||||
// border-bottom-right-radius: 4px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.img {
|
||||
flex: none;
|
||||
}
|
||||
.text {
|
||||
flex: auto;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.albumName {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.source {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// transform: rotate(45deg);
|
||||
// background-color:
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,375 @@
|
||||
<template>
|
||||
<div :class="$style.list">
|
||||
<div class="thead">
|
||||
<table>
|
||||
<thead>
|
||||
<tr v-if="actionButtonsVisible">
|
||||
<th class="num" style="width: 5%;">#</th>
|
||||
<th class="nobreak">{{ $t('music_name') }}</th>
|
||||
<th class="nobreak" style="width: 22%;">{{ $t('music_singer') }}</th>
|
||||
<th class="nobreak" style="width: 22%;">{{ $t('music_album') }}</th>
|
||||
<th class="nobreak" style="width: 9%;">{{ $t('music_time') }}</th>
|
||||
<th class="nobreak" style="width: 16%;">{{ $t('action') }}</th>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<th class="num" style="width: 5%;">#</th>
|
||||
<th class="nobreak">{{ $t('music_name') }}</th>
|
||||
<th class="nobreak" style="width: 25%;">{{ $t('music_singer') }}</th>
|
||||
<th class="nobreak" style="width: 28%;">{{ $t('music_album') }}</th>
|
||||
<th class="nobreak" style="width: 10%;">{{ $t('music_time') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div v-show="list.length" ref="dom_listContent" :class="$style.content">
|
||||
<base-virtualized-list
|
||||
v-if="actionButtonsVisible" ref="listRef" v-slot="{ item, index }" :list="list" key-name="id"
|
||||
:item-height="listItemHeight" container-class="scroll" content-class="list"
|
||||
@scroll="saveListPosition" @contextmenu.capture="handleListRightClick"
|
||||
>
|
||||
<div
|
||||
class="list-item" :class="[{ [$style.active]: playerInfo.isPlayList && playerInfo.playIndex === index }, { selected: selectedIndex == index || rightClickSelectedIndex == index }, { active: selectedList.includes(item) }, { disabled: !assertApiSupport(item.source) }]"
|
||||
@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
|
||||
>
|
||||
<div class="list-item-cell no-select num" style="flex: 0 0 5%;" @click.stop>{{ index + 1 }}</div>
|
||||
<div class="list-item-cell auto name" :aria-label="item.name">
|
||||
<span class="select name">{{ item.name }}</span>
|
||||
<span v-if="isShowSource" class="no-select label-source">{{ item.source }}</span>
|
||||
</div>
|
||||
<div class="list-item-cell" style="flex: 0 0 22%;"><span class="select" :aria-label="item.singer">{{ item.singer }}</span></div>
|
||||
<div class="list-item-cell" style="flex: 0 0 22%;"><span class="select" :aria-label="item.meta.albumName">{{ item.meta.albumName }}</span></div>
|
||||
<div class="list-item-cell" style="flex: 0 0 9%;"><span class="no-select">{{ item.interval || '--/--' }}</span></div>
|
||||
<div class="list-item-cell" style="flex: 0 0 16%; padding-left: 0; padding-right: 0;">
|
||||
<material-list-buttons :index="index" :download-btn="assertApiSupport(item.source) && item.source != 'local'" @btn-click="handleListBtnClick" />
|
||||
</div>
|
||||
</div>
|
||||
</base-virtualized-list>
|
||||
<base-virtualized-list
|
||||
v-else ref="listRef" v-slot="{ item, index }" :list="list" key-name="id"
|
||||
:item-height="listItemHeight" container-class="scroll" content-class="list"
|
||||
@scroll="saveListPosition" @contextmenu.capture="handleListRightClick"
|
||||
>
|
||||
<div
|
||||
class="list-item"
|
||||
:class="[{ [$style.active]: playerInfo.isPlayList && playerInfo.playIndex === index }, { selected: selectedIndex == index || rightClickSelectedIndex == index }, { active: selectedList.includes(item) }, { disabled: !assertApiSupport(item.source) }]"
|
||||
@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
|
||||
>
|
||||
<div class="list-item-cell no-select num" style="flex: 0 0 5%;" @click.stop>{{ index + 1 }}</div>
|
||||
<div class="list-item-cell auto name">
|
||||
<span class="select name" :aria-label="item.name">{{ item.name }}</span>
|
||||
<span v-if="isShowSource" class="no-select label-source">{{ item.source }}</span>
|
||||
</div>
|
||||
<div class="list-item-cell" style="flex: 0 0 25%;"><span class="select" :aria-label="item.singer">{{ item.singer }}</span></div>
|
||||
<div class="list-item-cell" style="flex: 0 0 28%;"><span class="select" :aria-label="item.meta.albumName">{{ item.meta.albumName }}</span></div>
|
||||
<div class="list-item-cell" style="flex: 0 0 10%;"><span class="no-select">{{ item.interval || '--/--' }}</span></div>
|
||||
</div>
|
||||
</base-virtualized-list>
|
||||
</div>
|
||||
<div v-show="!list.length" :class="$style.noItem">
|
||||
<p v-text="$t('no_item')" />
|
||||
</div>
|
||||
<common-list-add-modal
|
||||
v-model:show="isShowListAdd" :is-move="isMove" :from-list-id="listId"
|
||||
:music-info="selectedAddMusicInfo" :exclude-list-id="excludeListIds" teleport="#view"
|
||||
/>
|
||||
<common-list-add-multiple-modal
|
||||
v-model:show="isShowListAddMultiple" :from-list-id="listId"
|
||||
:is-move="isMoveMultiple" :music-list="selectedList" :exclude-list-id="excludeListIds" teleport="#view" @confirm="removeAllSelect"
|
||||
/>
|
||||
<common-download-modal v-model:show="isShowDownload" :music-info="selectedDownloadMusicInfo" teleport="#view" />
|
||||
<common-download-multiple-modal v-model:show="isShowDownloadMultiple" :list="selectedList" teleport="#view" @confirm="removeAllSelect" />
|
||||
<search-list :list="list" :visible="isShowSearchBar" @action="handleMusicSearchAction" />
|
||||
<music-sort-modal v-model:show="isShowMusicSortModal" :music-info="selectedSortMusicInfo" :selected-num="selectedNum" @confirm="sortMusic" />
|
||||
<base-menu v-model="isShowItemMenu" :menus="menus" :xy="menuLocation" item-name="name" @menu-click="handleMenuClick" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { clipboardWriteText } from '@common/utils/electron'
|
||||
import { assertApiSupport } from '@renderer/store/utils'
|
||||
import SearchList from './components/SearchList'
|
||||
import MusicSortModal from './components/MusicSortModal'
|
||||
import useListInfo from './useListInfo'
|
||||
import useList from './useList'
|
||||
import useMenu from './useMenu'
|
||||
import usePlay from './usePlay'
|
||||
import useMusicDownload from './useMusicDownload'
|
||||
import useMusicAdd from './useMusicAdd'
|
||||
import useSort from './useSort'
|
||||
import useMusicActions from './useMusicActions'
|
||||
import useSearch from './useSearch'
|
||||
import useListScroll from './useListScroll'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
export default {
|
||||
name: 'MusicList',
|
||||
components: {
|
||||
SearchList,
|
||||
MusicSortModal,
|
||||
},
|
||||
props: {
|
||||
listId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['show-menu'],
|
||||
setup(props, { emit }) {
|
||||
const actionButtonsVisible = appSetting['list.actionButtonsVisible']
|
||||
|
||||
let scrollIndex = null
|
||||
let isAnimation = false
|
||||
const handleRestoreScroll = (_scrollIndex, _isAnimation) => {
|
||||
scrollIndex = _scrollIndex
|
||||
isAnimation = _isAnimation
|
||||
if (isAnimation) restoreScroll(scrollIndex, isAnimation)
|
||||
console.log('handleRestoreScroll', scrollIndex, isAnimation)
|
||||
}
|
||||
const onLoadedList = () => {
|
||||
console.log('restoreScroll', scrollIndex, isAnimation)
|
||||
restoreScroll(scrollIndex, isAnimation)
|
||||
}
|
||||
|
||||
const {
|
||||
rightClickSelectedIndex,
|
||||
selectedIndex,
|
||||
dom_listContent,
|
||||
listRef,
|
||||
list,
|
||||
playerInfo,
|
||||
setSelectedIndex,
|
||||
isShowSource,
|
||||
excludeListIds,
|
||||
} = useListInfo({ props, onLoadedList })
|
||||
|
||||
const {
|
||||
selectedList,
|
||||
listItemHeight,
|
||||
handleSelectData,
|
||||
removeAllSelect,
|
||||
} = useList({ list })
|
||||
|
||||
const {
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
doubleClickPlay,
|
||||
} = usePlay({ props, selectedList, list, removeAllSelect })
|
||||
|
||||
const {
|
||||
isShowListAdd,
|
||||
isMove,
|
||||
isShowListAddMultiple,
|
||||
isMoveMultiple,
|
||||
selectedAddMusicInfo,
|
||||
handleShowMusicAddModal,
|
||||
handleShowMusicMoveModal,
|
||||
} = useMusicAdd({ selectedList, list })
|
||||
|
||||
const {
|
||||
isShowDownload,
|
||||
isShowDownloadMultiple,
|
||||
selectedDownloadMusicInfo,
|
||||
handleShowDownloadModal,
|
||||
} = useMusicDownload({ selectedList, list })
|
||||
|
||||
const {
|
||||
isShowMusicSortModal,
|
||||
selectedNum,
|
||||
selectedSortMusicInfo,
|
||||
handleShowSortModal,
|
||||
sortMusic,
|
||||
} = useSort({ props, list, selectedList, removeAllSelect })
|
||||
|
||||
const {
|
||||
handleSearch,
|
||||
handleOpenMusicDetail,
|
||||
handleCopyName,
|
||||
handleRemoveMusic,
|
||||
} = useMusicActions({ props, list, removeAllSelect, selectedList })
|
||||
|
||||
const {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowItemMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
} = useMenu({
|
||||
assertApiSupport,
|
||||
emit,
|
||||
|
||||
handleShowDownloadModal,
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
handleSearch,
|
||||
handleShowMusicAddModal,
|
||||
handleShowMusicMoveModal,
|
||||
handleShowSortModal,
|
||||
handleOpenMusicDetail,
|
||||
handleCopyName,
|
||||
handleRemoveMusic,
|
||||
})
|
||||
|
||||
const {
|
||||
isShowSearchBar,
|
||||
searchList,
|
||||
handleMusicSearchAction,
|
||||
} = useSearch({
|
||||
setSelectedIndex,
|
||||
handlePlayMusic,
|
||||
listRef,
|
||||
})
|
||||
|
||||
const { saveListPosition, restoreScroll } = useListScroll({ props, listRef, list, handleRestoreScroll })
|
||||
|
||||
|
||||
const handleListItemClick = (event, index) => {
|
||||
if (rightClickSelectedIndex.value > -1) return
|
||||
handleSelectData(index)
|
||||
doubleClickPlay(index)
|
||||
}
|
||||
const handleListItemRightClick = (event, index) => {
|
||||
rightClickSelectedIndex.value = index
|
||||
showMenu(event, list.value[index], index)
|
||||
}
|
||||
const handleMenuClick = (action) => {
|
||||
let index = rightClickSelectedIndex.value
|
||||
rightClickSelectedIndex.value = -1
|
||||
menuClick(action, index)
|
||||
}
|
||||
const handleListRightClick = (event) => {
|
||||
if (!event.target.classList.contains('select')) return
|
||||
event.stopImmediatePropagation()
|
||||
let classList = dom_listContent.value.classList
|
||||
classList.add('copying')
|
||||
window.requestAnimationFrame(() => {
|
||||
let str = window.getSelection().toString()
|
||||
classList.remove('copying')
|
||||
str = str.split(/\n\n/).map(s => s.replace(/\n/g, ' ')).join('\n').trim()
|
||||
if (!str.length) return
|
||||
clipboardWriteText(str)
|
||||
})
|
||||
}
|
||||
const handleListBtnClick = ({ action, index }) => {
|
||||
switch (action) {
|
||||
case 'download':
|
||||
handleShowDownloadModal(index, true)
|
||||
break
|
||||
case 'play':
|
||||
handlePlayMusic(index, true)
|
||||
break
|
||||
case 'search':
|
||||
handleSearch(index)
|
||||
break
|
||||
case 'listAdd':
|
||||
handleShowMusicAddModal(index, true)
|
||||
break
|
||||
}
|
||||
}
|
||||
const scrollToTop = () => {
|
||||
listRef.value.scrollTo(0, true)
|
||||
}
|
||||
|
||||
return {
|
||||
listItemHeight,
|
||||
handleListItemClick,
|
||||
selectedList,
|
||||
handleListItemRightClick,
|
||||
removeAllSelect,
|
||||
handleListBtnClick,
|
||||
rightClickSelectedIndex,
|
||||
selectedIndex,
|
||||
dom_listContent,
|
||||
listRef,
|
||||
excludeListIds,
|
||||
|
||||
menus,
|
||||
isShowItemMenu,
|
||||
menuLocation,
|
||||
handleMenuClick,
|
||||
|
||||
handleListRightClick,
|
||||
assertApiSupport,
|
||||
|
||||
isShowListAdd,
|
||||
isMove,
|
||||
isShowListAddMultiple,
|
||||
isMoveMultiple,
|
||||
selectedAddMusicInfo,
|
||||
|
||||
isShowMusicSortModal,
|
||||
selectedNum,
|
||||
selectedSortMusicInfo,
|
||||
sortMusic,
|
||||
|
||||
isShowDownload,
|
||||
isShowDownloadMultiple,
|
||||
selectedDownloadMusicInfo,
|
||||
|
||||
scrollToTop,
|
||||
|
||||
isShowSearchBar,
|
||||
searchList,
|
||||
handleMusicSearchAction,
|
||||
|
||||
list,
|
||||
playerInfo,
|
||||
|
||||
saveListPosition,
|
||||
isShowSource,
|
||||
handleRestoreScroll,
|
||||
|
||||
actionButtonsVisible,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.list {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
:global(.list-item) {
|
||||
&.active {
|
||||
color: var(--color-button-font);
|
||||
}
|
||||
}
|
||||
:global {
|
||||
.label-source {
|
||||
color: var(--color-primary);
|
||||
padding: 5px;
|
||||
font-size: .8em;
|
||||
line-height: 1;
|
||||
opacity: .75;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.content {
|
||||
min-height: 0;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.no-item {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -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 }) => {
|
||||
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(list, removeAllSelect)
|
||||
|
||||
return {
|
||||
selectedList,
|
||||
listItemHeight,
|
||||
removeAllSelect,
|
||||
handleSelectData,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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, onLoadedList }) => {
|
||||
const rightClickSelectedIndex = ref(-1)
|
||||
const selectedIndex = ref(-1)
|
||||
const dom_listContent = ref(null)
|
||||
const listRef = ref(null)
|
||||
|
||||
const excludeListIds = computed(() => ([props.listId]))
|
||||
|
||||
|
||||
const list = ref([])
|
||||
watch(() => props.listId, id => {
|
||||
getListMusics(id).then(l => {
|
||||
list.value = l
|
||||
if (id != props.listId) return
|
||||
onLoadedList()
|
||||
})
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
const playerInfo = computed(() => ({
|
||||
isPlayList: playMusicInfo.listId == props.listId,
|
||||
playIndex: playInfo.playIndex,
|
||||
}))
|
||||
|
||||
const setSelectedIndex = index => {
|
||||
selectedIndex.value = index
|
||||
}
|
||||
|
||||
const isShowSource = computed(() => appSetting['list.isShowSource'])
|
||||
|
||||
return {
|
||||
rightClickSelectedIndex,
|
||||
selectedIndex,
|
||||
dom_listContent,
|
||||
listRef,
|
||||
list,
|
||||
playerInfo,
|
||||
setSelectedIndex,
|
||||
isShowSource,
|
||||
excludeListIds,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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, handleRestoreScroll }) => {
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const saveListPosition = () => {
|
||||
setListPosition(props.listId, listRef.value?.getScrollTop() || 0)
|
||||
}
|
||||
|
||||
const handleScrollList = (index, isAnimation, callback = () => {}) => {
|
||||
listRef.value.scrollToIndex(index, -150, isAnimation, callback)
|
||||
}
|
||||
|
||||
const restoreScroll = async(index, isAnimation) => {
|
||||
// console.log(index, isAnimation)
|
||||
if (!list.value.length) return
|
||||
if (index == null) {
|
||||
let location = await getListPosition(props.listId) || 0
|
||||
if (appSetting['list.isSaveScrollLocation'] && location != null) {
|
||||
listRef.value?.scrollTo(location)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
handleScrollList(index, isAnimation)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleRestoreScroll(route.query.scrollIndex, false)
|
||||
if (route.query.scrollIndex != null) {
|
||||
router.replace({
|
||||
path: '/list',
|
||||
query: {
|
||||
id: props.listId,
|
||||
updated: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
saveListPosition()
|
||||
})
|
||||
|
||||
return {
|
||||
saveListPosition,
|
||||
restoreScroll,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import { computed, ref, shallowReactive, reactive, nextTick } from '@common/utils/vueTools'
|
||||
import musicSdk from '@renderer/utils/musicSdk'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
|
||||
export default ({
|
||||
assertApiSupport,
|
||||
emit,
|
||||
|
||||
handleShowDownloadModal,
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
handleSearch,
|
||||
handleShowMusicAddModal,
|
||||
handleShowMusicMoveModal,
|
||||
handleShowSortModal,
|
||||
handleOpenMusicDetail,
|
||||
handleCopyName,
|
||||
handleRemoveMusic,
|
||||
}) => {
|
||||
const itemMenuControl = reactive({
|
||||
play: true,
|
||||
playLater: true,
|
||||
copyName: true,
|
||||
addTo: true,
|
||||
moveTo: true,
|
||||
sort: true,
|
||||
download: true,
|
||||
search: true,
|
||||
remove: true,
|
||||
sourceDetail: 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',
|
||||
disabled: !itemMenuControl.play,
|
||||
},
|
||||
{
|
||||
name: t('list__download'),
|
||||
action: 'download',
|
||||
disabled: !itemMenuControl.download,
|
||||
},
|
||||
{
|
||||
name: t('list__play_later'),
|
||||
action: 'playLater',
|
||||
disabled: !itemMenuControl.playLater,
|
||||
},
|
||||
{
|
||||
name: t('list__add_to'),
|
||||
action: 'addTo',
|
||||
disabled: !itemMenuControl.addTo,
|
||||
},
|
||||
{
|
||||
name: t('list__move_to'),
|
||||
action: 'moveTo',
|
||||
disabled: !itemMenuControl.moveTo,
|
||||
},
|
||||
{
|
||||
name: t('list__sort'),
|
||||
action: 'sort',
|
||||
disabled: !itemMenuControl.sort,
|
||||
},
|
||||
{
|
||||
name: t('list__copy_name'),
|
||||
action: 'copyName',
|
||||
disabled: !itemMenuControl.copyName,
|
||||
},
|
||||
{
|
||||
name: t('list__source_detail'),
|
||||
action: 'sourceDetail',
|
||||
disabled: !itemMenuControl.sourceDetail,
|
||||
},
|
||||
{
|
||||
name: t('list__search'),
|
||||
action: 'search',
|
||||
disabled: !itemMenuControl.search,
|
||||
},
|
||||
{
|
||||
name: t('list__remove'),
|
||||
action: 'remove',
|
||||
disabled: !itemMenuControl.remove,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const showMenu = (event, musicInfo) => {
|
||||
itemMenuControl.sourceDetail = !!musicSdk[musicInfo.source]?.getMusicDetailPageUrl
|
||||
// itemMenuControl.play =
|
||||
// itemMenuControl.playLater =
|
||||
itemMenuControl.download = assertApiSupport(musicInfo.source) && musicInfo.source != 'local'
|
||||
|
||||
menuLocation.x = event.pageX
|
||||
menuLocation.y = event.pageY
|
||||
|
||||
if (isShowItemMenu.value) return
|
||||
|
||||
emit('show-menu')
|
||||
nextTick(() => {
|
||||
isShowItemMenu.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const hideMenu = () => {
|
||||
isShowItemMenu.value = false
|
||||
}
|
||||
|
||||
const menuClick = (action, index) => {
|
||||
// console.log(action)
|
||||
hideMenu()
|
||||
if (!action) return
|
||||
switch (action.action) {
|
||||
case 'play':
|
||||
handlePlayMusic(index)
|
||||
break
|
||||
case 'playLater':
|
||||
handlePlayMusicLater(index)
|
||||
break
|
||||
case 'copyName':
|
||||
handleCopyName(index)
|
||||
break
|
||||
case 'addTo':
|
||||
handleShowMusicAddModal(index)
|
||||
break
|
||||
case 'moveTo':
|
||||
handleShowMusicMoveModal(index)
|
||||
break
|
||||
case 'sort':
|
||||
handleShowSortModal(index)
|
||||
break
|
||||
case 'download':
|
||||
handleShowDownloadModal(index)
|
||||
break
|
||||
case 'search':
|
||||
handleSearch(index)
|
||||
break
|
||||
case 'remove':
|
||||
handleRemoveMusic(index)
|
||||
break
|
||||
case 'sourceDetail':
|
||||
handleOpenMusicDetail(index)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowItemMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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, selectedList, removeAllSelect }) => {
|
||||
const router = useRouter()
|
||||
const t = useI18n()
|
||||
|
||||
const handleSearch = index => {
|
||||
const info = list.value[index]
|
||||
router.push({
|
||||
path: '/search',
|
||||
query: {
|
||||
text: `${info.name} ${info.singer}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpenMusicDetail = index => {
|
||||
const minfo = list.value[index]
|
||||
const url = musicSdk[minfo.source].getMusicDetailPageUrl(toOldMusicInfo(minfo))
|
||||
if (!url) return
|
||||
openUrl(url)
|
||||
}
|
||||
|
||||
const handleCopyName = index => {
|
||||
const minfo = list.value[index]
|
||||
clipboardWriteText(appSetting['download.fileName'].replace('歌名', minfo.name).replace('歌手', minfo.singer))
|
||||
}
|
||||
|
||||
const handleRemoveMusic = 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
|
||||
removeListMusics({ listId: props.listId, ids: selectedList.value.map(m => m.id) })
|
||||
removeAllSelect()
|
||||
} else {
|
||||
removeListMusics({ listId: props.listId, ids: [list.value[index].id] })
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleSearch,
|
||||
handleOpenMusicDetail,
|
||||
handleCopyName,
|
||||
handleRemoveMusic,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { ref, nextTick } from '@common/utils/vueTools'
|
||||
|
||||
export default ({ selectedList, list }) => {
|
||||
const isShowListAdd = ref(false)
|
||||
const isMove = ref(false)
|
||||
const isMoveMultiple = ref(false)
|
||||
const isShowListAddMultiple = ref(false)
|
||||
const selectedAddMusicInfo = ref(null)
|
||||
|
||||
const handleShowMusicAddModal = (index, single) => {
|
||||
if (selectedList.value.length && !single) {
|
||||
isMoveMultiple.value = false
|
||||
isShowListAddMultiple.value = true
|
||||
} else {
|
||||
isMove.value = false
|
||||
selectedAddMusicInfo.value = list.value[index]
|
||||
nextTick(() => {
|
||||
isShowListAdd.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowMusicMoveModal = (index, single) => {
|
||||
if (selectedList.value.length && !single) {
|
||||
isMoveMultiple.value = true
|
||||
isShowListAddMultiple.value = true
|
||||
} else {
|
||||
isMove.value = true
|
||||
selectedAddMusicInfo.value = list.value[index]
|
||||
nextTick(() => {
|
||||
isShowListAdd.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isShowListAdd,
|
||||
isMove,
|
||||
isMoveMultiple,
|
||||
isShowListAddMultiple,
|
||||
selectedAddMusicInfo,
|
||||
handleShowMusicAddModal,
|
||||
handleShowMusicMoveModal,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ref, nextTick } from '@common/utils/vueTools'
|
||||
|
||||
export default ({ selectedList, list }) => {
|
||||
const isShowDownload = ref(false)
|
||||
const isShowDownloadMultiple = ref(false)
|
||||
const musicInfo = ref(null)
|
||||
|
||||
const handleShowDownloadModal = (index, single) => {
|
||||
if (selectedList.value.length && !single) {
|
||||
isShowDownloadMultiple.value = true
|
||||
} else {
|
||||
musicInfo.value = list.value[index]
|
||||
nextTick(() => {
|
||||
isShowDownload.value = true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isShowDownload,
|
||||
isShowDownloadMultiple,
|
||||
selectedDownloadMusicInfo: musicInfo,
|
||||
handleShowDownloadModal,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
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 handlePlayMusic = (index) => {
|
||||
playList(props.listId, index)
|
||||
}
|
||||
|
||||
const handlePlayMusicLater = (index, single) => {
|
||||
if (selectedList.value.length && !single) {
|
||||
addTempPlayList(selectedList.value.map(s => ({ listId: props.listId, musicInfo: s })))
|
||||
removeAllSelect()
|
||||
} else {
|
||||
addTempPlayList([{ listId: props.listId, musicInfo: list.value[index] }])
|
||||
}
|
||||
}
|
||||
|
||||
const doubleClickPlay = index => {
|
||||
if (
|
||||
window.performance.now() - clickTime > 400 ||
|
||||
clickIndex !== index
|
||||
) {
|
||||
clickTime = window.performance.now()
|
||||
clickIndex = index
|
||||
return
|
||||
}
|
||||
handlePlayMusic(index, true)
|
||||
clickTime = 0
|
||||
clickIndex = -1
|
||||
}
|
||||
|
||||
return {
|
||||
handlePlayMusic,
|
||||
handlePlayMusicLater,
|
||||
doubleClickPlay,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { ref, onBeforeUnmount } from '@common/utils/vueTools'
|
||||
|
||||
export default ({ setSelectedIndex, handlePlayMusic, listRef }) => {
|
||||
const isShowSearchBar = ref(false)
|
||||
const searchList = ref([])
|
||||
|
||||
const handleShowSearchBar = () => {
|
||||
isShowSearchBar.value = true
|
||||
}
|
||||
|
||||
const handleMusicSearchAction = ({ action, data: { index, isPlay } = {} }) => {
|
||||
isShowSearchBar.value = false
|
||||
switch (action) {
|
||||
case 'listClick':
|
||||
if (index < 0) return
|
||||
listRef.value.scrollToIndex(index, -150, true, () => {
|
||||
setSelectedIndex(index)
|
||||
setTimeout(() => {
|
||||
setSelectedIndex(-1)
|
||||
if (isPlay) handlePlayMusic(index)
|
||||
}, 600)
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
window.key_event.on('key_mod+f_down', handleShowSearchBar)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.key_event.off('key_mod+f_down', handleShowSearchBar)
|
||||
})
|
||||
|
||||
return {
|
||||
isShowSearchBar,
|
||||
searchList,
|
||||
handleMusicSearchAction,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
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 handleShowSortModal = (index, single) => {
|
||||
if (selectedList.value.length && !single) {
|
||||
selectedNum.value = selectedList.value.length
|
||||
} else {
|
||||
selectedNum.value = 0
|
||||
musicInfo.value = list.value[index]
|
||||
}
|
||||
nextTick(() => {
|
||||
isShowMusicSortModal.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const sortMusic = num => {
|
||||
num = Math.min(num, list.value.length)
|
||||
updateListMusicsPosition({
|
||||
listId: props.listId,
|
||||
position: num - 1,
|
||||
ids: (selectedNum.value ? [...selectedList.value] : [musicInfo.value]).map(m => m.id),
|
||||
})
|
||||
removeAllSelect()
|
||||
isShowMusicSortModal.value = false
|
||||
}
|
||||
|
||||
return {
|
||||
isShowMusicSortModal,
|
||||
selectedNum,
|
||||
selectedSortMusicInfo: musicInfo,
|
||||
handleShowSortModal,
|
||||
sortMusic,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { addListMusics, setFetchingListStatus } from '@renderer/store/list/action'
|
||||
import { showSelectDialog } from '@renderer/utils/ipc'
|
||||
|
||||
|
||||
const handleAddMusics = async(listId: string, filePaths: string[], index: number = -1) => {
|
||||
// console.log(index + 1, index + 201)
|
||||
const paths = filePaths.slice(index + 1, index + 201)
|
||||
const musicInfos = await window.lx.worker.main.createLocalMusicInfos(paths)
|
||||
if (musicInfos.length) await addListMusics(listId, musicInfos)
|
||||
index += 200
|
||||
if (filePaths.length - 1 > index) await handleAddMusics(listId, filePaths, index)
|
||||
}
|
||||
export const addLocalFile = async(listInfo: LX.List.MyListInfo) => {
|
||||
const { canceled, filePaths } = await showSelectDialog({
|
||||
title: window.i18n.t('lists__add_local_file_desc'),
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: [
|
||||
{ name: 'Media File', extensions: ['mp3', 'flac', 'ogg', 'wav'] },
|
||||
// { name: 'All Files', extensions: ['*'] },
|
||||
],
|
||||
})
|
||||
if (canceled || !filePaths.length) return
|
||||
|
||||
console.log(filePaths)
|
||||
setFetchingListStatus(listInfo.id, true)
|
||||
await handleAddMusics(listInfo.id, filePaths)
|
||||
setFetchingListStatus(listInfo.id, false)
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<template>
|
||||
<material-modal :show="visible" bg-close teleport="#view" width="60%" max-width="900px" @close="$emit('update:visible', false)">
|
||||
<div :class="$style.header">
|
||||
<h2>{{ listInfo.name }}</h2>
|
||||
</div>
|
||||
<base-virtualized-list
|
||||
v-if="duplicateList.length" v-slot="{ item, index }" :list="duplicateList" key-name="id" :class="$style.list" style="contain: none;"
|
||||
:item-height="listItemHeight" container-class="scroll" content-class="list"
|
||||
>
|
||||
<div :class="$style.listItem">
|
||||
<div :class="$style.num">{{ item.index + 1 }}</div>
|
||||
<div :class="$style.textContent">
|
||||
<h3 :class="$style.text" :aria-label="`${item.musicInfo.name} - ${item.musicInfo.singer}`">{{ item.musicInfo.name }} - {{ item.musicInfo.singer }}</h3>
|
||||
<h3 v-if="item.musicInfo.meta.albumName" :class="[$style.text, $style.albumName]" :aria-label="item.musicInfo.meta.albumName">{{ item.musicInfo.meta.albumName }}</h3>
|
||||
</div>
|
||||
<div :class="$style.label">{{ item.musicInfo.source }}</div>
|
||||
<div :class="$style.label">{{ item.musicInfo.interval }}</div>
|
||||
<div :class="$style.btns">
|
||||
<button type="button" :class="$style.btn" @click="handlePlay(index)">
|
||||
<svg v-once version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="50%" viewBox="0 0 287.386 287.386" space="preserve">
|
||||
<use xlink:href="#icon-testPlay" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" :class="$style.btn" @click="handleRemove(index)">
|
||||
<svg v-once version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="50%" viewBox="0 0 212.982 212.982" space="preserve">
|
||||
<use xlink:href="#icon-delete" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</base-virtualized-list>
|
||||
<div v-else :class="$style.noItem">
|
||||
<p v-text="$t('no_item')" />
|
||||
</div>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { shallowReactive, watch, toRaw, computed } from '@common/utils/vueTools'
|
||||
import { playList } from '@renderer/core/player'
|
||||
import { getListMusics, removeListMusics } from '@renderer/store/list/action'
|
||||
import { isFullscreen } from '@renderer/store'
|
||||
import { appSetting } from '@renderer/store/setting'
|
||||
import { getFontSizeWithScreen } from '@renderer/utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
listInfo: { // { id: '', name: '' }
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:visible'],
|
||||
setup(props) {
|
||||
const duplicateList = shallowReactive([])
|
||||
const listItemHeight = computed(() => {
|
||||
return Math.ceil((isFullscreen.value ? getFontSizeWithScreen() : appSetting['common.fontSize']) * 3.2)
|
||||
})
|
||||
|
||||
const handlePlay = (index) => {
|
||||
const { index: musicInfoIndex } = duplicateList[index]
|
||||
playList(props.listInfo.id, musicInfoIndex)
|
||||
}
|
||||
const handleFilterList = async() => {
|
||||
// console.time('filter')
|
||||
duplicateList.splice(
|
||||
0,
|
||||
duplicateList.length,
|
||||
...(await window.lx.worker.main.filterDuplicateMusic(toRaw(await getListMusics(props.listInfo.id)))))
|
||||
|
||||
// console.timeEnd('filter')
|
||||
}
|
||||
const handleRemove = async(index) => {
|
||||
const { musicInfo: targetMusicInfo } = duplicateList.splice(index, 1)[0]
|
||||
await removeListMusics({ listId: props.listInfo.id, ids: [targetMusicInfo.id] })
|
||||
await handleFilterList()
|
||||
}
|
||||
|
||||
watch(() => props.visible, (visible) => {
|
||||
if (visible) {
|
||||
if (duplicateList.length) duplicateList.splice(0, duplicateList.length)
|
||||
handleFilterList()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
listItemHeight,
|
||||
duplicateList,
|
||||
handleFilterList,
|
||||
handleRemove,
|
||||
handlePlay,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.header {
|
||||
flex: none;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
h2 {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
.main {
|
||||
min-height: 200px;
|
||||
min-width: 460px;
|
||||
// display: flex;
|
||||
// flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.list {
|
||||
min-height: 200px;
|
||||
min-width: 460px;
|
||||
// background-color: @color-search-form-background;
|
||||
font-size: 13px;
|
||||
transition-property: height;
|
||||
// position: relative;
|
||||
.listItem {
|
||||
position: relative;
|
||||
padding: 0 5px;
|
||||
transition: background-color .2s ease;
|
||||
line-height: 1.4;
|
||||
height: 100%;
|
||||
// overflow: hidden;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
// border-radius: 4px;
|
||||
// &:last-child {
|
||||
// border-bottom-left-radius: 4px;
|
||||
// border-bottom-right-radius: 4px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
.num {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
|
||||
.textContent {
|
||||
flex: auto;
|
||||
padding-left: 5px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
}
|
||||
.text {
|
||||
max-width: 100%;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.albumName {
|
||||
font-size: 12px;
|
||||
opacity: 0.6;
|
||||
// .mixin-ellipsis-1;
|
||||
}
|
||||
.label {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
// transform: rotate(45deg);
|
||||
// background-color:
|
||||
}
|
||||
.btns {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: @form-radius;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
padding: 4px 7px;
|
||||
color: var(--color-button-font);
|
||||
outline: none;
|
||||
transition: background-color 0.2s ease;
|
||||
line-height: 0;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-primary-font-active);
|
||||
}
|
||||
}
|
||||
|
||||
.no-item {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<material-modal :show="visible" teleport="#view" bg-close @close="closeModal" @after-leave="handleAfterLeave">
|
||||
<main :class="$style.main">
|
||||
<div :class="$style.header">
|
||||
<h2>{{ listInfo.name }}</h2>
|
||||
</div>
|
||||
<section>
|
||||
<h3 :class="$style.title">{{ $t('list_sort_modal_by_field') }}</h3>
|
||||
<ul :class="$style.list">
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_field_name" v-model="sortField" name="list_sort_modal_field" :aria-label="$t('list_sort_modal_by_name')"
|
||||
need="need" value="name" :label="$t('list_sort_modal_by_name')"
|
||||
/>
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_field_singer" v-model="sortField" name="list_sort_modal_field"
|
||||
need="need" value="singer" :label="$t('list_sort_modal_by_singer')"
|
||||
/>
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_field_album" v-model="sortField" name="list_sort_modal_field"
|
||||
need="need" value="album" :label="$t('list_sort_modal_by_album')"
|
||||
/>
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_field_time" v-model="sortField" name="list_sort_modal_field"
|
||||
need="need" value="interval" :label="$t('list_sort_modal_by_time')"
|
||||
/>
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_field_source" v-model="sortField" name="list_sort_modal_field"
|
||||
need="need" value="source" :label="$t('list_sort_modal_by_source')"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h3 :class="$style.title">{{ $t('list_sort_modal_by_type') }}</h3>
|
||||
<ul :class="$style.list">
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_type_up" v-model="sortType" name="list_sort_modal_type"
|
||||
need="need" value="up" :label="$t('list_sort_modal_by_up')"
|
||||
/>
|
||||
</li>
|
||||
<li :class="$style.listItem">
|
||||
<base-checkbox
|
||||
id="list_sort_modal_type_down" v-model="sortType" name="list_sort_modal_type"
|
||||
need="need" value="down" :label="$t('list_sort_modal_by_down')"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div :class="$style.footer">
|
||||
<base-btn :class="$style.btn" @click="closeModal">{{ $t('btn_cancel') }}</base-btn>
|
||||
<base-btn :class="$style.btn" @click="handleSort">{{ $t('btn_confirm') }}</base-btn>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
// import { dialog } from '@renderer/plugins/Dialog'
|
||||
import { getListMusics, updateListMusicsPosition } from '@renderer/store/list/action'
|
||||
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
listInfo: { // { id: '', name: '' }
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:visible'],
|
||||
setup(props, { emit }) {
|
||||
// const { t } = useI18n()
|
||||
const sortField = ref('')
|
||||
const sortType = ref('')
|
||||
const closeModal = () => {
|
||||
emit('update:visible', false)
|
||||
}
|
||||
const handleAfterLeave = () => {
|
||||
sortField.value = ''
|
||||
sortType.value = ''
|
||||
}
|
||||
const verify = () => {
|
||||
return !!sortField.value && !!sortType.value
|
||||
}
|
||||
const handleSort = async() => {
|
||||
if (!verify()) return
|
||||
// if (!await dialog.confirm({
|
||||
// message: t('list_sort_modal_tip_confirm'),
|
||||
// cancelButtonText: t('cancel_button_text'),
|
||||
// confirmButtonText: t('confirm_button_text'),
|
||||
// })) return
|
||||
|
||||
let list = [...(await getListMusics(props.listInfo.id))]
|
||||
list = await window.lx.worker.main.sortListMusicInfo(list, sortType.value, sortField.value, window.i18n.locale)
|
||||
console.log(sortType.value, sortField.value)
|
||||
|
||||
closeModal()
|
||||
updateListMusicsPosition({ listId: props.listInfo.id, position: 0, ids: list.map(m => m.id) })
|
||||
}
|
||||
return {
|
||||
sortField,
|
||||
sortType,
|
||||
closeModal,
|
||||
handleSort,
|
||||
handleAfterLeave,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.header {
|
||||
flex: none;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
h2 {
|
||||
color: var(--color-font);
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 0 15px;
|
||||
width: 320px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-height: 0;
|
||||
// max-height: 100%;
|
||||
// overflow: hidden;
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: var(--color-font-label);
|
||||
padding: 10px 0 8px;
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
font-size: 14px;
|
||||
}
|
||||
.listItem {
|
||||
width: (100% / 3);
|
||||
padding-left: 10px;
|
||||
margin-bottom: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.footer {
|
||||
margin: 20px 0 15px auto;
|
||||
}
|
||||
.btn {
|
||||
// box-sizing: border-box;
|
||||
// margin-left: 15px;
|
||||
// margin-bottom: 15px;
|
||||
// height: 36px;
|
||||
// line-height: 36px;
|
||||
// padding: 0 10px !important;
|
||||
min-width: 70px;
|
||||
// .mixin-ellipsis-1;
|
||||
|
||||
+.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<material-modal :show="visible" bg-close teleport="#view" @close="$emit('update:visible', false)">
|
||||
<div :class="$style.header">
|
||||
<h2>{{ $t('list_update_modal__title') }}</h2>
|
||||
</div>
|
||||
<main class="scroll" :class="$style.main">
|
||||
<ul v-if="lists.length" ref="dom_list" :class="$style.list">
|
||||
<li v-for="list in lists" :key="list.id" :class="[$style.listItem, {[$style.fetching]: fetchingListStatus[list.id]}]">
|
||||
<div :class="$style.listLeft">
|
||||
<h3 :class="$style.text">{{ list.name }} <span :class="$style.label">{{ list.source }}</span></h3>
|
||||
<div>
|
||||
<base-checkbox
|
||||
:id="`list_auto_update_${list.id}`" :model-value="updateInfo[list.id]?.isAutoUpdate == true"
|
||||
:class="$style.checkbox" :label="$t('list_update_modal__auto_update')" @change="handleChangeAutoUpdate(list, $event)"
|
||||
/>
|
||||
<span :class="$style.label" style="vertical-align: text-top;">{{ listUpdateTimes[list.id] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.btns">
|
||||
<button :class="$style.btn" :disabled="fetchingListStatus[list.id]" outline="outline" :aria-label="$t('list_update_modal__update')" @click.stop="handleUpdate(list)">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" style="transform: rotate(45deg);" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-refresh" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else :class="$style.noItem">
|
||||
<p v-text="$t('no_item')" />
|
||||
</div>
|
||||
</main>
|
||||
<div :class="$style.footer">
|
||||
<div :class="$style.tips">{{ $t('list_update_modal__tips') }}</div>
|
||||
</div>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, ref } from '@common/utils/vueTools'
|
||||
import { userLists, fetchingListStatus, listUpdateTimes } from '@renderer/store/list/state'
|
||||
import handleSyncSourceList from '@renderer/store/list/syncSourceList'
|
||||
import musicSdk from '@renderer/utils/musicSdk'
|
||||
// import { dateFormat } from '@common/utils/renderer'
|
||||
import { getListUpdateInfo, setListAutoUpdate } from '@renderer/utils/data'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ['update:visible'],
|
||||
setup() {
|
||||
const lists = computed(() => userLists.filter(l => !!l.source && !!musicSdk[l.source]?.songList))
|
||||
const updateInfo = ref({})
|
||||
// const updateTimes = ref({})
|
||||
|
||||
getListUpdateInfo().then((listUpdateInfo) => {
|
||||
updateInfo.value = listUpdateInfo
|
||||
// if (listUpdateTimes._inited) {
|
||||
// for (const [id, value] of Object.entries(info)) {
|
||||
// autoUpdate[id] = value.isAutoUpdate == true
|
||||
// }
|
||||
// } else {
|
||||
// for (const [id, value] of Object.entries(info)) {
|
||||
// autoUpdate[id] = value.isAutoUpdate == true
|
||||
// listUpdateTimes[id] = value.updateTime ? dateFormat(value.updateTime) : ''
|
||||
// }
|
||||
// }
|
||||
// listUpdateTimes._inited = true
|
||||
})
|
||||
|
||||
const handleUpdate = (targetListInfo) => {
|
||||
handleSyncSourceList(targetListInfo)
|
||||
// console.log(targetListInfo.list.length, list.length)
|
||||
}
|
||||
|
||||
const handleChangeAutoUpdate = (list, enable) => {
|
||||
setListAutoUpdate(list.id, enable)
|
||||
}
|
||||
|
||||
return {
|
||||
lists,
|
||||
updateInfo,
|
||||
fetchingListStatus,
|
||||
handleUpdate,
|
||||
handleChangeAutoUpdate,
|
||||
listUpdateTimes,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
@width: 460px;
|
||||
|
||||
.header {
|
||||
flex: none;
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
h2 {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
.main {
|
||||
min-height: 200px;
|
||||
width: @width;
|
||||
}
|
||||
|
||||
.list {
|
||||
// background-color: @color-search-form-background;
|
||||
font-size: 13px;
|
||||
transition-property: height;
|
||||
position: relative;
|
||||
.listItem {
|
||||
position: relative;
|
||||
padding: 15px 10px 15px 15px;
|
||||
transition: .3s ease;
|
||||
transition-property: background-color, opacity;
|
||||
line-height: 1.3;
|
||||
// overflow: hidden;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
// border-radius: 4px;
|
||||
// &:last-child {
|
||||
// border-bottom-left-radius: 4px;
|
||||
// border-bottom-right-radius: 4px;
|
||||
// }
|
||||
&.fetching {
|
||||
opacity: .5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listLeft {
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.text {
|
||||
flex: auto;
|
||||
margin-bottom: 2px;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.checkbox {
|
||||
margin-top: 3px;
|
||||
font-size: 14px;
|
||||
opacity: .86;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
padding: 0 10px;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// transform: rotate(45deg);
|
||||
// background-color:
|
||||
}
|
||||
.btns {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
padding: 0 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.btn {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: @form-radius;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
padding: 4px 7px;
|
||||
color: var(--color-button-font);
|
||||
outline: none;
|
||||
transition: background-color 0.2s ease;
|
||||
line-height: 0;
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
&:active {
|
||||
background-color: var(--color-primary-font-active);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: @width;
|
||||
}
|
||||
.tips {
|
||||
padding: 8px 15px;
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
color: var(--color-font);
|
||||
}
|
||||
|
||||
.no-item {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
p {
|
||||
font-size: 16px;
|
||||
color: var(--color-font-label);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,259 @@
|
||||
<!-- <template>
|
||||
<teleport to="#root">
|
||||
<div ref="dom_menu" :class="$style.container" :style="menuStyles" :aria-hidden="!modelValue">
|
||||
<div :class="$style.group">
|
||||
<div :class="$style.subGroup">
|
||||
<button :class="$style.btn" :disabled="!itemMenuControl.rename" ignore-tip :aria-label="$t('lists__rename')" @click="menuClick('rename')">{{ $t('lists__rename') }}</button>
|
||||
<button :class="$style.btn" :disabled="offsetDisabled" ignore-tip :aria-label="$t('lists__sync')" @click="menuClick('sync')">{{ $t('lists__sync') }}</button>
|
||||
</div>
|
||||
<div :class="$style.subGroup">
|
||||
<button :class="$style.btn" :disabled="offsetDisabled" ignore-tip :aria-label="$t('lists__sort_list')" @click="menuClick('sort')">{{ $t('lists__sort_list') }}</button>
|
||||
<button :class="$style.btn" :disabled="offsetDisabled" ignore-tip :aria-label="$t('lyric_menu__offset_dec_100')" @click="menuClick(-100)">- 100ms</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { computed, useRefGetter, ref, useCommit, watch } from '@common/utils/vueTools'
|
||||
import useMenuLocation from '@renderer/utils/compositions/useMenuLocation'
|
||||
import { setLyricEdited, removeLyricEdited, debounce } from '@renderer/utils'
|
||||
|
||||
const offsetTagRxp = /(?:^|\n)\s*\[offset:\s*(\S+(?:\d+)*)\s*\]/
|
||||
const offsetTagAllRxp = /(?:^|\n)\s*\[offset:\s*(\S+(?:\d+)*)\s*\]/g
|
||||
|
||||
const saveLyric = debounce((musicInfo, lyricInfo) => {
|
||||
setLyricEdited(musicInfo, lyricInfo)
|
||||
})
|
||||
const removeLyric = debounce(musicInfo => {
|
||||
removeLyricEdited(musicInfo)
|
||||
})
|
||||
|
||||
const getOffset = lrc => {
|
||||
let offset = offsetTagRxp.exec(lrc)
|
||||
if (offset) {
|
||||
offset = parseInt(offset[1])
|
||||
if (Number.isNaN(offset)) offset = 0
|
||||
} else offset = 0
|
||||
return offset
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'LyricMenu',
|
||||
props: {
|
||||
modelValue: Boolean,
|
||||
xy: Object,
|
||||
lyricInfo: Object,
|
||||
},
|
||||
emits: ['updateLyric', 'update:modelValue'],
|
||||
setup(props, { emit }) {
|
||||
const setting = useRefGetter('setting')
|
||||
const playDetailSetting = useRefGetter('playDetailSetting')
|
||||
const setPlayDetailLyricAlign = useCommit('setPlayDetailLyricAlign')
|
||||
const setPlayDetailLyricFont = useCommit('setPlayDetailLyricFont')
|
||||
|
||||
const offset = ref(0)
|
||||
const offsetDisabled = ref(true)
|
||||
const originOffset = ref(0)
|
||||
|
||||
const visible = computed(() => props.modelValue)
|
||||
const musicInfo = computed(() => props.lyricInfo.musicInfo)
|
||||
const location = computed(() => props.xy)
|
||||
|
||||
const onHide = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
const setFontAlign = val => {
|
||||
if (playDetailSetting.value.style.align == val) return
|
||||
setPlayDetailLyricAlign(val)
|
||||
}
|
||||
|
||||
const fontSizeUp = step => {
|
||||
if (setting.value.playDetail.style.fontSize >= 200) return
|
||||
setPlayDetailLyricFont(Math.min(setting.value.playDetail.style.fontSize + step, 200))
|
||||
}
|
||||
const fontSizeDown = step => {
|
||||
if (setting.value.playDetail.style.fontSize <= 70) return
|
||||
setPlayDetailLyricFont(Math.max(setting.value.playDetail.style.fontSize - step, 70))
|
||||
}
|
||||
const fontSizeReset = () => {
|
||||
setPlayDetailLyricFont(100)
|
||||
}
|
||||
|
||||
const updateLyric = offset => {
|
||||
let lyric = props.lyricInfo.lyric
|
||||
let tlyric = props.lyricInfo.tlyric
|
||||
let rlyric = props.lyricInfo.rlyric
|
||||
let lxlyric = props.lyricInfo.lxlyric
|
||||
if (offsetTagRxp.test(lyric)) {
|
||||
lyric = lyric.replace(offsetTagAllRxp, `[offset:${offset}]`)
|
||||
if (tlyric) tlyric = tlyric.replace(offsetTagAllRxp, `[offset:${offset}]`)
|
||||
if (lxlyric) lxlyric = lxlyric.replace(offsetTagAllRxp, `[offset:${offset}]`)
|
||||
if (rlyric) rlyric = rlyric.replace(offsetTagAllRxp, `[offset:${offset}]`)
|
||||
} else {
|
||||
lyric = `[offset:${offset}]\n` + lyric
|
||||
if (tlyric) tlyric = `[offset:${offset}]\n` + tlyric
|
||||
if (lxlyric) lxlyric = `[offset:${offset}]\n` + lxlyric
|
||||
if (rlyric) rlyric = `[offset:${offset}]\n` + rlyric
|
||||
}
|
||||
|
||||
if (offset == originOffset.value) {
|
||||
removeLyric(props.lyricInfo.musicInfo)
|
||||
} else {
|
||||
saveLyric(props.lyricInfo.musicInfo, {
|
||||
lyric,
|
||||
tlyric,
|
||||
rlyric,
|
||||
lxlyric,
|
||||
})
|
||||
}
|
||||
|
||||
emit('updateLyric', {
|
||||
lyric,
|
||||
tlyric,
|
||||
rlyric,
|
||||
lxlyric,
|
||||
offset,
|
||||
})
|
||||
}
|
||||
const setOffset = step => {
|
||||
offset.value += step
|
||||
updateLyric(offset.value)
|
||||
}
|
||||
const offsetReset = () => {
|
||||
if (offset.value == originOffset.value) return
|
||||
offset.value = originOffset.value
|
||||
updateLyric(originOffset.value)
|
||||
}
|
||||
|
||||
const parseLrcOffset = () => {
|
||||
offset.value = getOffset(props.lyricInfo.lyric)
|
||||
originOffset.value = getOffset(props.lyricInfo.rawlyric)
|
||||
offsetDisabled.value = !props.lyricInfo.lyric
|
||||
}
|
||||
|
||||
|
||||
const { dom_menu, menuStyles } = useMenuLocation({
|
||||
visible,
|
||||
location,
|
||||
onHide,
|
||||
})
|
||||
|
||||
watch(musicInfo, () => {
|
||||
if (!props.modelValue) return
|
||||
parseLrcOffset()
|
||||
})
|
||||
watch(visible, val => {
|
||||
if (!val) return
|
||||
parseLrcOffset()
|
||||
})
|
||||
|
||||
return {
|
||||
dom_menu,
|
||||
menuStyles,
|
||||
playDetailSetting,
|
||||
offset,
|
||||
originOffset,
|
||||
fontSizeUp,
|
||||
fontSizeDown,
|
||||
fontSizeReset,
|
||||
setOffset,
|
||||
offsetReset,
|
||||
setFontAlign,
|
||||
offsetDisabled,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.container {
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transform: scale(0);
|
||||
transform-origin: 0 0 0;
|
||||
transition: .25s ease;
|
||||
transition-property: transform, opacity;
|
||||
border-radius: @radius-border;
|
||||
background-color: var(--color-main-background);
|
||||
box-shadow: 0 1px 8px 0 rgba(0,0,0,.2);
|
||||
z-index: 10;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.title {
|
||||
flex: auto;
|
||||
padding: 10px 0 10px 10px;
|
||||
color: var(--color-font-label);
|
||||
white-space: nowrap;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.subGroup {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: auto;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
min-width: 60px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
// color: var(--color-button-font);
|
||||
padding: 0 10px;
|
||||
outline: none;
|
||||
transition: @transition-normal;
|
||||
transition-property: background-color, opacity;
|
||||
box-sizing: border-box;
|
||||
.mixin-ellipsis-1;
|
||||
background-color: var(--color-main-background);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
opacity: .7;
|
||||
background-color: @color-theme_2-hover;
|
||||
}
|
||||
&:active {
|
||||
background-color: @color-theme_2-active;
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-main-background);
|
||||
color: var(--color-button-background-active);
|
||||
cursor: default;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
cursor: default;
|
||||
opacity: .4;
|
||||
&:hover {
|
||||
background: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.titleBtn {
|
||||
flex: none;
|
||||
padding: 0 10;
|
||||
min-width: 40px;
|
||||
opacity: .7;
|
||||
|
||||
&[disabled] {
|
||||
opacity: .3;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
-->
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<material-modal :show="show" teleport="#view" @close="handleClose" @after-enter="$refs.input.focus()">
|
||||
<main :class="$style.main">
|
||||
<h2>{{ selectedNum > 0 ? $t('music_sort__title_multiple', { num: selectedNum }) : $t('music_sort__title', { name: musicInfo ? musicInfo.name : '' }) }}</h2>
|
||||
<base-input
|
||||
ref="input"
|
||||
v-model="sortNum"
|
||||
:class="$style.input"
|
||||
type="number"
|
||||
:placeholder="$t('music_sort__input_tip')"
|
||||
@submit="handleSubmit" @blur="verify"
|
||||
/>
|
||||
<div :class="$style.footer">
|
||||
<base-btn :class="$style.btn" @click="handleSubmit">{{ $t('btn_confirm') }}</base-btn>
|
||||
</div>
|
||||
</main>
|
||||
</material-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
musicInfo: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
selectedNum: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
emits: ['update:show', 'confirm'],
|
||||
data() {
|
||||
return {
|
||||
sortNum: '',
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show(n) {
|
||||
if (n) {
|
||||
this.sortNum = ''
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.$emit('update:show', false)
|
||||
},
|
||||
verify() {
|
||||
let num = /^[1-9]\d*/.exec(this.sortNum)
|
||||
num = num ? parseInt(num[0]) : ''
|
||||
this.sortNum = num.toString()
|
||||
return num
|
||||
},
|
||||
handleSubmit() {
|
||||
let num = this.verify()
|
||||
if (this.sortNum == '') return
|
||||
this.handleClose()
|
||||
this.$emit('confirm', num)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.main {
|
||||
padding: 0 15px;
|
||||
max-width: 530px;
|
||||
min-width: 280px;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
min-height: 0;
|
||||
// max-height: 100%;
|
||||
// overflow: hidden;
|
||||
h2 {
|
||||
font-size: 13px;
|
||||
color: var(--color-font);
|
||||
line-height: 1.3;
|
||||
word-break: break-all;
|
||||
// text-align: center;
|
||||
padding: 15px 0 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
// width: 100%;
|
||||
// height: 26px;
|
||||
padding: 8px 8px;
|
||||
}
|
||||
.footer {
|
||||
margin: 20px 0 15px auto;
|
||||
}
|
||||
.btn {
|
||||
// box-sizing: border-box;
|
||||
// margin-left: 15px;
|
||||
// margin-bottom: 15px;
|
||||
// height: 36px;
|
||||
// line-height: 36px;
|
||||
// padding: 0 10px !important;
|
||||
min-width: 70px;
|
||||
// .mixin-ellipsis-1;
|
||||
|
||||
+.btn {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,379 @@
|
||||
<template>
|
||||
<div ref="dom_lists" :class="$style.lists">
|
||||
<div :class="$style.listHeader">
|
||||
<h2 :class="$style.listsTitle">{{ $t('my_list') }}</h2>
|
||||
<div :class="$style.headerBtns">
|
||||
<button :class="$style.listsAdd" :aria-label="$t('lists__new_list_btn')" @click="isShowNewList = true">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="70%" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-list-add" />
|
||||
</svg>
|
||||
</button>
|
||||
<button :class="$style.listsAdd" :aria-label="$t('list_update_modal__title')" @click="isShowListUpdateModal = true">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" style="transform: rotate(45deg);" height="70%" viewBox="0 0 24 24" space="preserve">
|
||||
<use xlink:href="#icon-refresh" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul ref="dom_lists_list" class="scroll" :class="[$style.listsContent, { [$style.sortable]: isModDown }]">
|
||||
<li
|
||||
class="default-list" :class="[$style.listsItem, {[$style.active]: defaultList.id == listId}, {[$style.clicked]: rightClickItemIndex == -2}, {[$style.fetching]: fetchingListStatus[defaultList.id]}]"
|
||||
:aria-label="defaultList.name" :aria-selected="defaultList.id == listId"
|
||||
@contextmenu="handleListsItemRigthClick($event, -2)" @click="handleListToggle(defaultList.id)"
|
||||
>
|
||||
<span :class="$style.listsLabel">{{ defaultList.name }}</span>
|
||||
</li>
|
||||
<li
|
||||
class="default-list" :class="[$style.listsItem, {[$style.active]: loveList.id == listId}, {[$style.clicked]: rightClickItemIndex == -1}, {[$style.fetching]: fetchingListStatus[loveList.id]}]"
|
||||
:aria-label="loveList.name" :aria-selected="loveList.id == listId"
|
||||
@contextmenu="handleListsItemRigthClick($event, -1)" @click="handleListToggle(loveList.id)"
|
||||
>
|
||||
<span :class="$style.listsLabel">{{ loveList.name }}</span>
|
||||
</li>
|
||||
<li
|
||||
v-for="(item, index) in userLists"
|
||||
:key="item.id" class="user-list"
|
||||
:class="[$style.listsItem, {[$style.active]: item.id == listId}, {[$style.clicked]: rightClickItemIndex == index}, {[$style.fetching]: fetchingListStatus[item.id]}]"
|
||||
:data-index="index" :aria-label="item.name" :aria-selected="defaultList.id == listId" @contextmenu="handleListsItemRigthClick($event, index)"
|
||||
>
|
||||
<span :class="$style.listsLabel" @click="handleListToggle(item.id, index + 2)">{{ item.name }}</span>
|
||||
<input
|
||||
:class="$style.listsInput" type="text" :value="item.name"
|
||||
:placeholder="item.name" @contextmenu.stop @keyup.enter="handleSaveListName(index, $event)" @blur="handleSaveListName(index, $event)"
|
||||
>
|
||||
</li>
|
||||
<transition enter-active-class="animated-fast slideInLeft" leave-active-class="animated-fast fadeOut" @after-leave="isNewListLeave = false" @after-enter="$refs.dom_listsNewInput.focus()">
|
||||
<li v-if="isShowNewList" :class="[$style.listsItem, $style.listsNew, {[$style.newLeave]: isNewListLeave}]">
|
||||
<input
|
||||
ref="dom_listsNewInput" :class="$style.listsInput" type="text" :placeholder="$t('lists__new_list_input')" @contextmenu.stop
|
||||
@keyup.enter="handleCreateList" @blur="handleCreateList"
|
||||
>
|
||||
</li>
|
||||
</transition>
|
||||
</ul>
|
||||
<base-menu v-model="isShowMenu" :menus="menus" :xy="menuLocation" item-name="name" @menu-click="handleMenuClick" />
|
||||
<DuplicateMusicModal v-model:visible="isShowDuplicateMusicModal" :list-info="duplicateListInfo" />
|
||||
<ListSortModal v-model:visible="isShowListSortModal" :list-info="sortListInfo" />
|
||||
<ListUpdateModal v-model:visible="isShowListUpdateModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { openUrl } from '@common/utils/electron'
|
||||
|
||||
import musicSdk from '@renderer/utils/musicSdk'
|
||||
import DuplicateMusicModal from './components/DuplicateMusicModal'
|
||||
import ListSortModal from './components/ListSortModal'
|
||||
import ListUpdateModal from './components/ListUpdateModal'
|
||||
|
||||
import { defaultList, loveList, userLists, fetchingListStatus } from '@renderer/store/list/state'
|
||||
import { removeUserList } from '@renderer/store/list/action'
|
||||
|
||||
import { ref, watch } from '@common/utils/vueTools'
|
||||
import { useRouter } from '@common/utils/vueRouter'
|
||||
import { LIST_IDS } from '@common/constants'
|
||||
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
|
||||
import { saveListPrevSelectId } from '@renderer/utils/data'
|
||||
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
|
||||
|
||||
import useShare from './useShare'
|
||||
import useMenu from './useMenu'
|
||||
import useListUpdate from './useListUpdate'
|
||||
import useSort from './useSort'
|
||||
import useDarg from './useDarg'
|
||||
import useEditList from './useEditList'
|
||||
import useListScroll from './useListScroll'
|
||||
import useDuplicate from './useDuplicate'
|
||||
|
||||
export default {
|
||||
name: 'MyLists',
|
||||
components: {
|
||||
DuplicateMusicModal,
|
||||
ListSortModal,
|
||||
ListUpdateModal,
|
||||
},
|
||||
props: {
|
||||
listId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['show-menu'],
|
||||
setup(props, { emit }) {
|
||||
const router = useRouter()
|
||||
const t = useI18n()
|
||||
|
||||
const dom_lists_list = ref(null)
|
||||
const rightClickItemIndex = ref(-10)
|
||||
|
||||
const { handleImportList, handleExportList } = useShare()
|
||||
const { isShowListUpdateModal, handleUpdateSourceList } = useListUpdate()
|
||||
const { isShowListSortModal, sortListInfo, handleSortList } = useSort()
|
||||
const { isShowDuplicateMusicModal, duplicateListInfo, handleDuplicateList } = useDuplicate()
|
||||
const { handleRename, handleSaveListName, isShowNewList, isNewListLeave, handleCreateList } = useEditList({ dom_lists_list })
|
||||
useListScroll({ dom_lists_list })
|
||||
|
||||
const handleOpenSourceDetailPage = async(listInfo) => {
|
||||
const { source, sourceListId } = listInfo
|
||||
if (!sourceListId) return
|
||||
let url
|
||||
if (/board__/.test(sourceListId)) {
|
||||
const id = sourceListId.replace(/board__/, '')
|
||||
url = musicSdk[source].leaderboard.getDetailPageUrl(id)
|
||||
} else if (musicSdk[source].songList?.getDetailPageUrl) {
|
||||
url = await musicSdk[source].songList.getDetailPageUrl(sourceListId)
|
||||
}
|
||||
if (!url) return
|
||||
openUrl(url)
|
||||
}
|
||||
|
||||
const handleRemove = (listInfo) => {
|
||||
dialog.confirm({
|
||||
message: t('lists__remove_tip', { name: listInfo.name }),
|
||||
confirmButtonText: t('lists__remove_tip_button'),
|
||||
}).then(isRemove => {
|
||||
if (!isRemove) return
|
||||
removeUserList([listInfo.id])
|
||||
if (props.listId == listInfo.id) {
|
||||
handleListToggle(LIST_IDS.DEFAULT)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
} = useMenu({
|
||||
emit,
|
||||
|
||||
handleImportList,
|
||||
handleExportList,
|
||||
handleUpdateSourceList,
|
||||
handleOpenSourceDetailPage,
|
||||
handleSortList,
|
||||
handleDuplicateList,
|
||||
handleRename,
|
||||
handleRemove,
|
||||
})
|
||||
|
||||
const handleListsItemRigthClick = (event, index) => {
|
||||
rightClickItemIndex.value = index
|
||||
showMenu(event, index)
|
||||
}
|
||||
|
||||
const handleListToggle = (id) => {
|
||||
if (id == props.listId) return
|
||||
router.replace({
|
||||
path: '/list',
|
||||
query: { id },
|
||||
}).catch(_ => _)
|
||||
}
|
||||
|
||||
const handleMenuClick = (action) => {
|
||||
if (rightClickItemIndex.value < -2) return
|
||||
let index = rightClickItemIndex.value
|
||||
rightClickItemIndex.value = -10
|
||||
menuClick(action, index)
|
||||
}
|
||||
|
||||
const { isModDown } = useDarg({ dom_lists_list, handleMenuClick, handleSaveListName })
|
||||
|
||||
|
||||
watch(() => props.listId, (listId) => {
|
||||
saveListPrevSelectId(listId)
|
||||
})
|
||||
|
||||
watch(() => userLists, (lists) => {
|
||||
if (lists.some(l => l.id == props.listId)) return
|
||||
router.replace({
|
||||
path: '/list',
|
||||
query: {
|
||||
id: defaultList.id,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
rightClickItemIndex,
|
||||
defaultList,
|
||||
loveList,
|
||||
userLists,
|
||||
fetchingListStatus,
|
||||
dom_lists_list,
|
||||
isShowListUpdateModal,
|
||||
isShowListSortModal,
|
||||
sortListInfo,
|
||||
isShowDuplicateMusicModal,
|
||||
duplicateListInfo,
|
||||
handleSaveListName,
|
||||
isShowNewList,
|
||||
isNewListLeave,
|
||||
handleCreateList,
|
||||
handleListsItemRigthClick,
|
||||
isShowMenu,
|
||||
handleMenuClick,
|
||||
menus,
|
||||
menuLocation,
|
||||
handleListToggle,
|
||||
isModDown,
|
||||
hideMenu: handleMenuClick,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
@lists-item-height: 36px;
|
||||
.lists {
|
||||
flex: none;
|
||||
width: 16%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.listHeader {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
border-bottom: var(--color-list-header-border-bottom);
|
||||
&:hover {
|
||||
.listsAdd {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsTitle {
|
||||
flex: auto;
|
||||
font-size: 12px;
|
||||
line-height: 38px;
|
||||
padding: 0 10px;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.headerBtns {
|
||||
flex: none;
|
||||
display: flex;
|
||||
}
|
||||
.listsAdd {
|
||||
// position: absolute;
|
||||
// right: 0;
|
||||
margin-top: 6px;
|
||||
background: none;
|
||||
height: 30px;
|
||||
border: none;
|
||||
outline: none;
|
||||
border-radius: @radius-border;
|
||||
cursor: pointer;
|
||||
opacity: .1;
|
||||
transition: opacity @transition-normal;
|
||||
color: var(--color-button-font);
|
||||
svg {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
&:active {
|
||||
opacity: .7 !important;
|
||||
}
|
||||
&:hover {
|
||||
opacity: .6 !important;
|
||||
}
|
||||
}
|
||||
.listsContent {
|
||||
flex: auto;
|
||||
min-width: 0;
|
||||
overflow-y: scroll !important;
|
||||
// border-right: 1px solid rgba(0, 0, 0, 0.12);
|
||||
|
||||
&.sortable {
|
||||
* {
|
||||
-webkit-user-drag: element;
|
||||
}
|
||||
|
||||
.listsItem {
|
||||
&:hover, &.active, &.selected, &.clicked {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
&.dragingItem {
|
||||
background-color: var(--color-primary-background-hover) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsItem {
|
||||
position: relative;
|
||||
transition: .3s ease;
|
||||
transition-property: color, background-color, opacity;
|
||||
background-color: transparent;
|
||||
&:not(.active) {
|
||||
&:hover {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
// background-color:
|
||||
color: var(--color-primary);
|
||||
}
|
||||
&.selected {
|
||||
background-color: var(--color-primary-font-active);
|
||||
}
|
||||
&.clicked {
|
||||
background-color: var(--color-primary-background-hover);
|
||||
}
|
||||
&.fetching {
|
||||
opacity: .5;
|
||||
}
|
||||
&.editing {
|
||||
padding: 0 10px;
|
||||
background-color: var(--color-primary-background-hover);
|
||||
.listsLabel {
|
||||
display: none;
|
||||
}
|
||||
.listsInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
.listsLabel {
|
||||
display: block;
|
||||
height: @lists-item-height;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
line-height: @lists-item-height;
|
||||
.mixin-ellipsis-1;
|
||||
}
|
||||
.listsInput {
|
||||
width: 100%;
|
||||
height: @lists-item-height;
|
||||
border: none;
|
||||
padding: 0;
|
||||
// padding-bottom: 1px;
|
||||
line-height: @lists-item-height;
|
||||
background: none;
|
||||
outline: none;
|
||||
font-size: 13px;
|
||||
display: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.listsNew {
|
||||
padding: 0 10px;
|
||||
background-color: var(--color-primary-background-hover);
|
||||
.listsInput {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.newLeave {
|
||||
margin-top: -@lists-item-height;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
import { onBeforeUnmount, ref, Ref, useCssModule } from '@common/utils/vueTools'
|
||||
import { updateUserListPosition } from '@renderer/store/list/action'
|
||||
import { userLists } from '@renderer/store/list/state'
|
||||
import useDarg from '@renderer/utils/compositions/useDrag'
|
||||
|
||||
|
||||
export default ({ dom_lists_list, handleSaveListName, handleMenuClick }: {
|
||||
dom_lists_list: Ref<HTMLElement | null>
|
||||
handleSaveListName: () => void
|
||||
handleMenuClick: () => void
|
||||
}) => {
|
||||
const isModDown = ref(false)
|
||||
const styles = useCssModule()
|
||||
|
||||
const { setDisabled } = useDarg({
|
||||
dom_list: dom_lists_list,
|
||||
dragingItemClassName: styles.dragingItem,
|
||||
filter: 'default-list',
|
||||
onUpdate(newIndex: number, oldIndex: number) {
|
||||
void updateUserListPosition({ ids: [userLists[oldIndex - 2].id], position: newIndex - 2 })
|
||||
},
|
||||
})
|
||||
|
||||
const handle_key_mod_down = ({ event }: LX.KeyDownEevent) => {
|
||||
if (!isModDown.value) {
|
||||
// console.log(event)
|
||||
switch ((event!.target as HTMLElement).tagName) {
|
||||
case 'INPUT':
|
||||
case 'SELECT':
|
||||
case 'TEXTAREA':
|
||||
return
|
||||
default: if ((event!.target as HTMLElement).isContentEditable) return
|
||||
}
|
||||
|
||||
isModDown.value = true
|
||||
setDisabled(false)
|
||||
handleSaveListName()
|
||||
}
|
||||
handleMenuClick()
|
||||
}
|
||||
const handle_key_mod_up = () => {
|
||||
if (isModDown.value) {
|
||||
isModDown.value = false
|
||||
setDisabled(true)
|
||||
}
|
||||
}
|
||||
|
||||
window.key_event.on('key_mod_down', handle_key_mod_down)
|
||||
window.key_event.on('key_mod_up', handle_key_mod_up)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.key_event.off('key_mod_down', handle_key_mod_down)
|
||||
window.key_event.off('key_mod_up', handle_key_mod_up)
|
||||
})
|
||||
|
||||
return {
|
||||
isModDown,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ref, reactive } from '@common/utils/vueTools'
|
||||
|
||||
|
||||
export default () => {
|
||||
const isShowDuplicateMusicModal = ref(false)
|
||||
const duplicateListInfo = reactive({ id: '', name: '' })
|
||||
|
||||
const handleDuplicateList = (listInfo: LX.List.MyListInfo) => {
|
||||
duplicateListInfo.id = listInfo.id
|
||||
duplicateListInfo.name = listInfo.name
|
||||
isShowDuplicateMusicModal.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
isShowDuplicateMusicModal,
|
||||
duplicateListInfo,
|
||||
handleDuplicateList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { ref, nextTick, useCssModule, Ref } from '@common/utils/vueTools'
|
||||
import { userLists } from '@renderer/store/list/state'
|
||||
import { updateUserList, createUserList } from '@renderer/store/list/action'
|
||||
|
||||
export default ({ dom_lists_list }: {
|
||||
dom_lists_list: Ref<HTMLElement | null>
|
||||
}) => {
|
||||
const isShowNewList = ref(false)
|
||||
const isNewListLeave = ref(false)
|
||||
const editIndex = ref(-1)
|
||||
const styles = useCssModule()
|
||||
|
||||
const handleRename = (index: number) => {
|
||||
console.log(index)
|
||||
const dom = dom_lists_list.value?.querySelectorAll('.user-list')[index]
|
||||
if (!dom) return
|
||||
void nextTick(() => {
|
||||
dom.classList.add(styles.editing)
|
||||
dom.querySelector('input')?.focus()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveListName = async() => {
|
||||
let dom_target = dom_lists_list.value?.querySelector('.' + styles.editing) as HTMLElement
|
||||
if (!dom_target) return
|
||||
const dom_input = dom_target.querySelector('.' + styles.listsInput) as HTMLInputElement
|
||||
if (!dom_input) return
|
||||
let name = dom_input.value.trim()
|
||||
if (dom_target.dataset.index == null) return
|
||||
const targetList = userLists[parseInt(dom_target.dataset.index)]
|
||||
if (name.length) {
|
||||
await updateUserList([{ ...targetList, name }])
|
||||
dom_target.classList.remove(styles.editing)
|
||||
}
|
||||
dom_input.value = targetList.name
|
||||
}
|
||||
|
||||
const handleCreateList = async(event: Event) => {
|
||||
const target = event.target as HTMLInputElement
|
||||
if (target.readOnly) return
|
||||
let name = target.value.trim()
|
||||
target.readOnly = true
|
||||
|
||||
if (name == '') {
|
||||
isShowNewList.value = false
|
||||
return
|
||||
}
|
||||
|
||||
await createUserList({ name })
|
||||
isNewListLeave.value = true
|
||||
void nextTick(() => {
|
||||
isShowNewList.value = false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
isShowNewList,
|
||||
isNewListLeave,
|
||||
editIndex,
|
||||
handleRename,
|
||||
handleSaveListName,
|
||||
handleCreateList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { onMounted, useCssModule, Ref } from '@common/utils/vueTools'
|
||||
|
||||
|
||||
export default ({ dom_lists_list }: {
|
||||
dom_lists_list: Ref<HTMLElement | null>
|
||||
}) => {
|
||||
const styles = useCssModule()
|
||||
|
||||
const setListsScroll = () => {
|
||||
if (!dom_lists_list.value) return
|
||||
let target = dom_lists_list.value.querySelector('.' + styles.active) as HTMLElement
|
||||
if (!target) return
|
||||
let offsetTop = target.offsetTop
|
||||
let location = offsetTop - 150
|
||||
if (location > 0) dom_lists_list.value.scrollTop = location
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setListsScroll()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ref } from '@common/utils/vueTools'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
import syncSourceList from '@renderer/store/list/syncSourceList'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
|
||||
|
||||
export default () => {
|
||||
const isShowListUpdateModal = ref(false)
|
||||
|
||||
const t = useI18n()
|
||||
|
||||
const handleUpdateSourceList = (listInfo: LX.List.UserListInfo) => {
|
||||
void dialog.confirm({
|
||||
message: t('lists__sync_confirm_tip', { name: listInfo.name }),
|
||||
confirmButtonText: t('lists__remove_tip_button'),
|
||||
}).then(isSync => {
|
||||
if (!isSync) return
|
||||
void syncSourceList(listInfo)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
isShowListUpdateModal,
|
||||
handleUpdateSourceList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import { computed, ref, reactive, nextTick } from '@common/utils/vueTools'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { userLists, defaultList, loveList } from '@renderer/store/list/state'
|
||||
import musicSdk from '@renderer/utils/musicSdk'
|
||||
import { addLocalFile } from './actions'
|
||||
|
||||
export default ({
|
||||
emit,
|
||||
|
||||
handleRename,
|
||||
handleDuplicateList,
|
||||
handleSortList,
|
||||
handleOpenSourceDetailPage,
|
||||
handleImportList,
|
||||
handleExportList,
|
||||
handleUpdateSourceList,
|
||||
handleRemove,
|
||||
}) => {
|
||||
const menuControl = reactive({
|
||||
rename: true,
|
||||
duplicate: true,
|
||||
sort: true,
|
||||
local_file: true,
|
||||
sourceDetail: true,
|
||||
import: true,
|
||||
export: true,
|
||||
sync: false,
|
||||
remove: true,
|
||||
})
|
||||
const t = useI18n()
|
||||
const menuLocation = reactive({ x: 0, y: 0 })
|
||||
const isShowMenu = ref(false)
|
||||
|
||||
const menus = computed(() => {
|
||||
return [
|
||||
{
|
||||
name: t('lists__rename'),
|
||||
action: 'rename',
|
||||
disabled: !menuControl.rename,
|
||||
},
|
||||
{
|
||||
name: t('lists__sort_list'),
|
||||
action: 'sort',
|
||||
disabled: !menuControl.sort,
|
||||
},
|
||||
{
|
||||
name: t('lists__duplicate'),
|
||||
action: 'duplicate',
|
||||
disabled: !menuControl.duplicate,
|
||||
},
|
||||
{
|
||||
name: t('lists__select_local_file'),
|
||||
action: 'local_file',
|
||||
disabled: !menuControl.local_file,
|
||||
},
|
||||
{
|
||||
name: t('lists__sync'),
|
||||
action: 'sync',
|
||||
disabled: !menuControl.sync,
|
||||
},
|
||||
{
|
||||
name: t('lists__source_detail'),
|
||||
action: 'sourceDetail',
|
||||
disabled: !menuControl.sourceDetail,
|
||||
},
|
||||
{
|
||||
name: t('lists__import'),
|
||||
action: 'import',
|
||||
disabled: !menuControl.export,
|
||||
},
|
||||
{
|
||||
name: t('lists__export'),
|
||||
action: 'export',
|
||||
disabled: !menuControl.export,
|
||||
},
|
||||
{
|
||||
name: t('lists__remove'),
|
||||
action: 'remove',
|
||||
disabled: !menuControl.remove,
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const assertSupportDetail = (source, index) => {
|
||||
if (source) {
|
||||
const { sourceListId } = userLists[index]
|
||||
if (sourceListId) {
|
||||
if (/board__/.test(sourceListId)) {
|
||||
// const id = sourceListId.replace(/board__/, '')
|
||||
return !!musicSdk[source]?.leaderboard?.getDetailPageUrl
|
||||
} else {
|
||||
return !!musicSdk[source]?.songList?.getDetailPageUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const showMenu = (event, index) => {
|
||||
let source
|
||||
switch (index) {
|
||||
case -1:
|
||||
case -2:
|
||||
menuControl.rename = false
|
||||
menuControl.remove = false
|
||||
menuControl.sync = false
|
||||
break
|
||||
default:
|
||||
menuControl.rename = true
|
||||
menuControl.remove = true
|
||||
source = userLists[index].source
|
||||
menuControl.sync = !!source && !!musicSdk[source]?.songList
|
||||
break
|
||||
}
|
||||
// menuControl.sort = !!getList(this.getTargetListInfo(index)?.id).length
|
||||
menuControl.sourceDetail = assertSupportDetail(source, index)
|
||||
|
||||
menuLocation.x = event.pageX
|
||||
menuLocation.y = event.pageY
|
||||
|
||||
if (isShowMenu.value) return
|
||||
emit('show-menu')
|
||||
nextTick(() => {
|
||||
isShowMenu.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const hideMenu = () => {
|
||||
isShowMenu.value = false
|
||||
}
|
||||
|
||||
const getListInfo = (index) => {
|
||||
let list
|
||||
switch (index) {
|
||||
case -2:
|
||||
list = defaultList
|
||||
break
|
||||
case -1:
|
||||
list = loveList
|
||||
break
|
||||
default:
|
||||
list = userLists[index]
|
||||
if (!list) return null
|
||||
break
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
const menuClick = (action, index) => {
|
||||
// console.log(action)
|
||||
hideMenu()
|
||||
if (!action) return
|
||||
const listInfo = getListInfo(index)
|
||||
switch (action.action) {
|
||||
case 'rename':
|
||||
handleRename(index)
|
||||
break
|
||||
case 'duplicate':
|
||||
handleDuplicateList(listInfo)
|
||||
break
|
||||
case 'sort':
|
||||
handleSortList(listInfo)
|
||||
break
|
||||
case 'local_file':
|
||||
addLocalFile(listInfo)
|
||||
break
|
||||
case 'sourceDetail':
|
||||
handleOpenSourceDetailPage(listInfo)
|
||||
break
|
||||
case 'import':
|
||||
handleImportList(listInfo, index)
|
||||
break
|
||||
case 'export':
|
||||
handleExportList(listInfo)
|
||||
break
|
||||
case 'sync':
|
||||
handleUpdateSourceList(listInfo)
|
||||
break
|
||||
case 'remove':
|
||||
handleRemove(listInfo)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
menus,
|
||||
menuLocation,
|
||||
isShowMenu,
|
||||
showMenu,
|
||||
menuClick,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { toRaw } from '@common/utils/vueTools'
|
||||
import { openSaveDir, showSelectDialog } from '@renderer/utils/ipc'
|
||||
import { useI18n } from '@renderer/plugins/i18n'
|
||||
import { filterFileName, toNewMusicInfo, fixNewMusicInfoQuality } from '@renderer/utils'
|
||||
import { getListMusics, updateUserList, addListMusics, overwriteListMusics, createUserList } from '@renderer/store/list/action'
|
||||
import { defaultList, loveList, userLists } from '@renderer/store/list/state'
|
||||
import useImportTip from '@renderer/utils/compositions/useImportTip'
|
||||
import { dialog } from '@renderer/plugins/Dialog'
|
||||
|
||||
|
||||
export default () => {
|
||||
const t = useI18n()
|
||||
const showImportTip = useImportTip()
|
||||
|
||||
const handleExportList = (listInfo: LX.List.MyListInfo) => {
|
||||
if (!listInfo) return
|
||||
void openSaveDir({
|
||||
title: t('lists__export_part_desc'),
|
||||
defaultPath: `lx_list_part_${filterFileName(listInfo.name)}.lxmc`,
|
||||
}).then(async result => {
|
||||
if (result.canceled || !result.filePath) return
|
||||
void window.lx.worker.main.saveLxConfigFile(result.filePath, {
|
||||
type: 'playListPart_v2',
|
||||
data: { ...toRaw(listInfo), list: toRaw(await getListMusics(listInfo.id)) },
|
||||
})
|
||||
})
|
||||
}
|
||||
const handleImportList = (listInfo: LX.List.MyListInfo, index: number) => {
|
||||
void showSelectDialog({
|
||||
title: t('lists__import_part_desc'),
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{ name: 'Play List Part', extensions: ['json', 'lxmc'] },
|
||||
{ name: 'All Files', extensions: ['*'] },
|
||||
],
|
||||
}).then(async result => {
|
||||
if (result.canceled) return
|
||||
const filePath = result.filePaths[0]
|
||||
if (!filePath) return
|
||||
let configData: any
|
||||
try {
|
||||
configData = await window.lx.worker.main.readLxConfigFile(filePath)
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
let listData: LX.ConfigFile.MyListInfoPart['data']
|
||||
switch (configData.type) {
|
||||
case 'playListPart':
|
||||
listData = configData.data
|
||||
listData.list = listData.list.map(m => toNewMusicInfo(m))
|
||||
break
|
||||
case 'playListPart_v2':
|
||||
listData = configData.data
|
||||
break
|
||||
default: return showImportTip(configData.type)
|
||||
}
|
||||
|
||||
const targetList = [defaultList, loveList, ...userLists].find(l => l.id == listData.id)
|
||||
if (targetList) {
|
||||
const confirm = await dialog.confirm({
|
||||
message: t('lists__import_part_confirm', { importName: listData.name, localName: targetList.name }),
|
||||
cancelButtonText: t('lists__import_part_button_cancel'),
|
||||
confirmButtonText: t('lists__import_part_button_confirm'),
|
||||
})
|
||||
if (confirm) {
|
||||
listData.name = targetList.name
|
||||
switch (listData.id) {
|
||||
case defaultList.id:
|
||||
case loveList.id:
|
||||
break
|
||||
default:
|
||||
void updateUserList([
|
||||
{
|
||||
name: listData.name,
|
||||
id: listData.id,
|
||||
source: (listData as LX.List.UserListInfo).source,
|
||||
sourceListId: (listData as LX.List.UserListInfo).sourceListId,
|
||||
locationUpdateTime: (targetList as LX.List.UserListInfo).locationUpdateTime,
|
||||
},
|
||||
])
|
||||
break
|
||||
}
|
||||
void overwriteListMusics({
|
||||
listId: listData.id,
|
||||
musicInfos: listData.list.map(m => fixNewMusicInfoQuality(m)),
|
||||
})
|
||||
return
|
||||
}
|
||||
listData.id += `__${Date.now()}`
|
||||
}
|
||||
void createUserList({
|
||||
position: index,
|
||||
name: listData.name,
|
||||
id: listData.id,
|
||||
source: (listData as LX.List.UserListInfo).source,
|
||||
sourceListId: (listData as LX.List.UserListInfo).sourceListId,
|
||||
})
|
||||
void addListMusics(listData.id, listData.list.map(m => fixNewMusicInfoQuality(m)))
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
handleExportList,
|
||||
handleImportList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ref, reactive } from '@common/utils/vueTools'
|
||||
|
||||
|
||||
export default () => {
|
||||
const isShowListSortModal = ref(false)
|
||||
const sortListInfo = reactive({ id: '', name: '' })
|
||||
|
||||
const handleSortList = (listInfo) => {
|
||||
sortListInfo.id = listInfo.id
|
||||
sortListInfo.name = listInfo.name
|
||||
isShowListSortModal.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
isShowListSortModal,
|
||||
sortListInfo,
|
||||
handleSortList,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div id="my-list" :class="$style.container" @click="handleContainerClick">
|
||||
<MyList ref="myList" :list-id="listId" @show-menu="$refs.musicList.handleMenuClick()" />
|
||||
<MusicList ref="musicList" :list-id="listId" @show-menu="$refs.myList.handleMenuClick()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getListPrevSelectId } from '@renderer/utils/data'
|
||||
|
||||
import MyList from './MyList'
|
||||
import MusicList from './MusicList'
|
||||
|
||||
export default {
|
||||
name: 'List',
|
||||
components: {
|
||||
MyList,
|
||||
MusicList,
|
||||
},
|
||||
async beforeRouteEnter(to, from, next) {
|
||||
let id = to.query.id
|
||||
if (!id) {
|
||||
id = await getListPrevSelectId()
|
||||
next({
|
||||
path: to.path,
|
||||
query: { id },
|
||||
})
|
||||
} else next()
|
||||
},
|
||||
beforeRouteUpdate(to, from) {
|
||||
// console.log(to, from)
|
||||
if (to.query.updated) return
|
||||
let id = to.query.id
|
||||
if (id == null) return
|
||||
// if (!getList(id)) {
|
||||
// id = defaultList.id
|
||||
// }
|
||||
this.listId = id
|
||||
const scrollIndex = to.query.scrollIndex
|
||||
const isAnimation = from.query.id == to.query.id
|
||||
this.$refs.musicList?.handleRestoreScroll(scrollIndex, isAnimation)
|
||||
|
||||
return {
|
||||
path: '/list',
|
||||
query: { id, updated: true },
|
||||
}
|
||||
},
|
||||
beforeRouteLeave(to, from) {
|
||||
this.$refs.musicList?.saveListPosition()
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
listId: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.listId = this.$route.query.id
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" module>
|
||||
@import '@renderer/assets/styles/layout.less';
|
||||
|
||||
.container {
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user