Files
Melo/src/renderer/views/List/MusicList/index.vue
T
2023-08-07 18:49:14 +08:00

414 lines
13 KiB
Vue

<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" :class="$style.num" style="flex: 0 0 5%;">
<transition name="play-active">
<div v-if="playerInfo.isPlayList && playerInfo.playIndex === index" :class="$style.playIcon">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="50%" viewBox="0 0 512 512" space="preserve">
<use xlink:href="#icon-play-outline" />
</svg>
</div>
<div v-else class="num">{{ index + 1 }}</div>
</transition>
</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" :class="$style.num" style="flex: 0 0 5%;">
<transition name="play-active">
<div v-if="playerInfo.isPlayList && playerInfo.playIndex === index" :class="$style.playIcon">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="50%" viewBox="0 0 512 512" space="preserve">
<use xlink:href="#icon-play-outline" />
</svg>
</div>
<div v-else class="num">{{ index + 1 }}</div>
</transition>
</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.vue'
import MusicSortModal from './components/MusicSortModal.vue'
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;
}
}
}
.num {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.playIcon {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-button-font);
opacity: .7;
}
.content {
min-height: 0;
font-size: 14px;
display: flex;
flex-flow: column nowrap;
flex: auto;
}
.noItem {
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>