This commit is contained in:
lyswhut
2022-10-29 11:36:35 +08:00
parent 66b965d9f7
commit e45cee6596
715 changed files with 57096 additions and 31771 deletions
@@ -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>
+375
View File
@@ -0,0 +1,375 @@
<template>
<div :class="$style.list">
<div class="thead">
<table>
<thead>
<tr v-if="actionButtonsVisible">
<th class="num" style="width: 5%;">#</th>
<th class="nobreak">{{ $t('music_name') }}</th>
<th class="nobreak" style="width: 22%;">{{ $t('music_singer') }}</th>
<th class="nobreak" style="width: 22%;">{{ $t('music_album') }}</th>
<th class="nobreak" style="width: 9%;">{{ $t('music_time') }}</th>
<th class="nobreak" style="width: 16%;">{{ $t('action') }}</th>
</tr>
<tr v-else>
<th class="num" style="width: 5%;">#</th>
<th class="nobreak">{{ $t('music_name') }}</th>
<th class="nobreak" style="width: 25%;">{{ $t('music_singer') }}</th>
<th class="nobreak" style="width: 28%;">{{ $t('music_album') }}</th>
<th class="nobreak" style="width: 10%;">{{ $t('music_time') }}</th>
</tr>
</thead>
</table>
</div>
<div v-show="list.length" ref="dom_listContent" :class="$style.content">
<base-virtualized-list
v-if="actionButtonsVisible" ref="listRef" v-slot="{ item, index }" :list="list" key-name="id"
:item-height="listItemHeight" container-class="scroll" content-class="list"
@scroll="saveListPosition" @contextmenu.capture="handleListRightClick"
>
<div
class="list-item" :class="[{ [$style.active]: playerInfo.isPlayList && playerInfo.playIndex === index }, { selected: selectedIndex == index || rightClickSelectedIndex == index }, { active: selectedList.includes(item) }, { disabled: !assertApiSupport(item.source) }]"
@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
>
<div class="list-item-cell no-select num" style="flex: 0 0 5%;" @click.stop>{{ index + 1 }}</div>
<div class="list-item-cell auto name" :aria-label="item.name">
<span class="select name">{{ item.name }}</span>
<span v-if="isShowSource" class="no-select label-source">{{ item.source }}</span>
</div>
<div class="list-item-cell" style="flex: 0 0 22%;"><span class="select" :aria-label="item.singer">{{ item.singer }}</span></div>
<div class="list-item-cell" style="flex: 0 0 22%;"><span class="select" :aria-label="item.meta.albumName">{{ item.meta.albumName }}</span></div>
<div class="list-item-cell" style="flex: 0 0 9%;"><span class="no-select">{{ item.interval || '--/--' }}</span></div>
<div class="list-item-cell" style="flex: 0 0 16%; padding-left: 0; padding-right: 0;">
<material-list-buttons :index="index" :download-btn="assertApiSupport(item.source) && item.source != 'local'" @btn-click="handleListBtnClick" />
</div>
</div>
</base-virtualized-list>
<base-virtualized-list
v-else ref="listRef" v-slot="{ item, index }" :list="list" key-name="id"
:item-height="listItemHeight" container-class="scroll" content-class="list"
@scroll="saveListPosition" @contextmenu.capture="handleListRightClick"
>
<div
class="list-item"
:class="[{ [$style.active]: playerInfo.isPlayList && playerInfo.playIndex === index }, { selected: selectedIndex == index || rightClickSelectedIndex == index }, { active: selectedList.includes(item) }, { disabled: !assertApiSupport(item.source) }]"
@click="handleListItemClick($event, index)" @contextmenu="handleListItemRightClick($event, index)"
>
<div class="list-item-cell no-select num" style="flex: 0 0 5%;" @click.stop>{{ index + 1 }}</div>
<div class="list-item-cell auto name">
<span class="select name" :aria-label="item.name">{{ item.name }}</span>
<span v-if="isShowSource" class="no-select label-source">{{ item.source }}</span>
</div>
<div class="list-item-cell" style="flex: 0 0 25%;"><span class="select" :aria-label="item.singer">{{ item.singer }}</span></div>
<div class="list-item-cell" style="flex: 0 0 28%;"><span class="select" :aria-label="item.meta.albumName">{{ item.meta.albumName }}</span></div>
<div class="list-item-cell" style="flex: 0 0 10%;"><span class="no-select">{{ item.interval || '--/--' }}</span></div>
</div>
</base-virtualized-list>
</div>
<div v-show="!list.length" :class="$style.noItem">
<p v-text="$t('no_item')" />
</div>
<common-list-add-modal
v-model:show="isShowListAdd" :is-move="isMove" :from-list-id="listId"
:music-info="selectedAddMusicInfo" :exclude-list-id="excludeListIds" teleport="#view"
/>
<common-list-add-multiple-modal
v-model:show="isShowListAddMultiple" :from-list-id="listId"
:is-move="isMoveMultiple" :music-list="selectedList" :exclude-list-id="excludeListIds" teleport="#view" @confirm="removeAllSelect"
/>
<common-download-modal v-model:show="isShowDownload" :music-info="selectedDownloadMusicInfo" teleport="#view" />
<common-download-multiple-modal v-model:show="isShowDownloadMultiple" :list="selectedList" teleport="#view" @confirm="removeAllSelect" />
<search-list :list="list" :visible="isShowSearchBar" @action="handleMusicSearchAction" />
<music-sort-modal v-model:show="isShowMusicSortModal" :music-info="selectedSortMusicInfo" :selected-num="selectedNum" @confirm="sortMusic" />
<base-menu v-model="isShowItemMenu" :menus="menus" :xy="menuLocation" item-name="name" @menu-click="handleMenuClick" />
</div>
</template>
<script>
import { clipboardWriteText } from '@common/utils/electron'
import { assertApiSupport } from '@renderer/store/utils'
import SearchList from './components/SearchList'
import MusicSortModal from './components/MusicSortModal'
import useListInfo from './useListInfo'
import useList from './useList'
import useMenu from './useMenu'
import usePlay from './usePlay'
import useMusicDownload from './useMusicDownload'
import useMusicAdd from './useMusicAdd'
import useSort from './useSort'
import useMusicActions from './useMusicActions'
import useSearch from './useSearch'
import useListScroll from './useListScroll'
import { appSetting } from '@renderer/store/setting'
export default {
name: 'MusicList',
components: {
SearchList,
MusicSortModal,
},
props: {
listId: {
type: String,
required: true,
},
},
emits: ['show-menu'],
setup(props, { emit }) {
const actionButtonsVisible = appSetting['list.actionButtonsVisible']
let scrollIndex = null
let isAnimation = false
const handleRestoreScroll = (_scrollIndex, _isAnimation) => {
scrollIndex = _scrollIndex
isAnimation = _isAnimation
if (isAnimation) restoreScroll(scrollIndex, isAnimation)
console.log('handleRestoreScroll', scrollIndex, isAnimation)
}
const onLoadedList = () => {
console.log('restoreScroll', scrollIndex, isAnimation)
restoreScroll(scrollIndex, isAnimation)
}
const {
rightClickSelectedIndex,
selectedIndex,
dom_listContent,
listRef,
list,
playerInfo,
setSelectedIndex,
isShowSource,
excludeListIds,
} = useListInfo({ props, onLoadedList })
const {
selectedList,
listItemHeight,
handleSelectData,
removeAllSelect,
} = useList({ list })
const {
handlePlayMusic,
handlePlayMusicLater,
doubleClickPlay,
} = usePlay({ props, selectedList, list, removeAllSelect })
const {
isShowListAdd,
isMove,
isShowListAddMultiple,
isMoveMultiple,
selectedAddMusicInfo,
handleShowMusicAddModal,
handleShowMusicMoveModal,
} = useMusicAdd({ selectedList, list })
const {
isShowDownload,
isShowDownloadMultiple,
selectedDownloadMusicInfo,
handleShowDownloadModal,
} = useMusicDownload({ selectedList, list })
const {
isShowMusicSortModal,
selectedNum,
selectedSortMusicInfo,
handleShowSortModal,
sortMusic,
} = useSort({ props, list, selectedList, removeAllSelect })
const {
handleSearch,
handleOpenMusicDetail,
handleCopyName,
handleRemoveMusic,
} = useMusicActions({ props, list, removeAllSelect, selectedList })
const {
menus,
menuLocation,
isShowItemMenu,
showMenu,
menuClick,
} = useMenu({
assertApiSupport,
emit,
handleShowDownloadModal,
handlePlayMusic,
handlePlayMusicLater,
handleSearch,
handleShowMusicAddModal,
handleShowMusicMoveModal,
handleShowSortModal,
handleOpenMusicDetail,
handleCopyName,
handleRemoveMusic,
})
const {
isShowSearchBar,
searchList,
handleMusicSearchAction,
} = useSearch({
setSelectedIndex,
handlePlayMusic,
listRef,
})
const { saveListPosition, restoreScroll } = useListScroll({ props, listRef, list, handleRestoreScroll })
const handleListItemClick = (event, index) => {
if (rightClickSelectedIndex.value > -1) return
handleSelectData(index)
doubleClickPlay(index)
}
const handleListItemRightClick = (event, index) => {
rightClickSelectedIndex.value = index
showMenu(event, list.value[index], index)
}
const handleMenuClick = (action) => {
let index = rightClickSelectedIndex.value
rightClickSelectedIndex.value = -1
menuClick(action, index)
}
const handleListRightClick = (event) => {
if (!event.target.classList.contains('select')) return
event.stopImmediatePropagation()
let classList = dom_listContent.value.classList
classList.add('copying')
window.requestAnimationFrame(() => {
let str = window.getSelection().toString()
classList.remove('copying')
str = str.split(/\n\n/).map(s => s.replace(/\n/g, ' ')).join('\n').trim()
if (!str.length) return
clipboardWriteText(str)
})
}
const handleListBtnClick = ({ action, index }) => {
switch (action) {
case 'download':
handleShowDownloadModal(index, true)
break
case 'play':
handlePlayMusic(index, true)
break
case 'search':
handleSearch(index)
break
case 'listAdd':
handleShowMusicAddModal(index, true)
break
}
}
const scrollToTop = () => {
listRef.value.scrollTo(0, true)
}
return {
listItemHeight,
handleListItemClick,
selectedList,
handleListItemRightClick,
removeAllSelect,
handleListBtnClick,
rightClickSelectedIndex,
selectedIndex,
dom_listContent,
listRef,
excludeListIds,
menus,
isShowItemMenu,
menuLocation,
handleMenuClick,
handleListRightClick,
assertApiSupport,
isShowListAdd,
isMove,
isShowListAddMultiple,
isMoveMultiple,
selectedAddMusicInfo,
isShowMusicSortModal,
selectedNum,
selectedSortMusicInfo,
sortMusic,
isShowDownload,
isShowDownloadMultiple,
selectedDownloadMusicInfo,
scrollToTop,
isShowSearchBar,
searchList,
handleMusicSearchAction,
list,
playerInfo,
saveListPosition,
isShowSource,
handleRestoreScroll,
actionButtonsVisible,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.list {
overflow: hidden;
height: 100%;
flex: auto;
display: flex;
flex-flow: column nowrap;
:global(.list-item) {
&.active {
color: var(--color-button-font);
}
}
:global {
.label-source {
color: var(--color-primary);
padding: 5px;
font-size: .8em;
line-height: 1;
opacity: .75;
display: inline-block;
}
}
}
.content {
min-height: 0;
font-size: 14px;
display: flex;
flex-flow: column nowrap;
flex: auto;
}
.no-item {
position: relative;
height: 100%;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
p {
font-size: 24px;
color: var(--color-font-label);
}
}
</style>
@@ -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,
}
}