mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
chore: refactor and simplify song filtering
This commit is contained in:
parent
732e9b0d10
commit
bd28da72d1
10 changed files with 62 additions and 56 deletions
|
@ -61,7 +61,6 @@
|
|||
<SongList
|
||||
v-else
|
||||
ref="songList"
|
||||
@sort="sort"
|
||||
@press:enter="onPressEnter"
|
||||
@scroll-breakpoint="onScrollBreakpoint"
|
||||
/>
|
||||
|
|
|
@ -97,7 +97,7 @@ const {
|
|||
onPressEnter,
|
||||
playSelected,
|
||||
onScrollBreakpoint
|
||||
} = useSongList(toRef(songStore.state, 'songs'), { type: 'Songs' }, { sortable: true })
|
||||
} = useSongList(toRef(songStore.state, 'songs'), { type: 'Songs' }, { filterable: false, sortable: true })
|
||||
|
||||
const { SongListControls, config } = useSongListControls('Songs')
|
||||
|
||||
|
@ -107,7 +107,7 @@ const { get: lsGet, set: lsSet } = useLocalStorage()
|
|||
|
||||
let initialized = false
|
||||
const loading = ref(false)
|
||||
let sortField: PlayableListSortField = 'title' // @todo get from query string
|
||||
let sortField: MaybeArray<PlayableListSortField> = 'title' // @todo get from query string
|
||||
let sortOrder: SortOrder = 'asc'
|
||||
|
||||
const page = ref<number | null>(1)
|
||||
|
@ -124,7 +124,7 @@ watch(ownSongsOnly, async value => {
|
|||
await fetchSongs()
|
||||
})
|
||||
|
||||
const sort = async (field: PlayableListSortField, order: SortOrder) => {
|
||||
const sort = async (field: MaybeArray<PlayableListSortField>, order: SortOrder) => {
|
||||
page.value = 1
|
||||
songStore.state.songs = []
|
||||
sortField = field
|
||||
|
|
|
@ -60,7 +60,6 @@
|
|||
<SongList
|
||||
v-else
|
||||
ref="songList"
|
||||
@sort="sort"
|
||||
@press:enter="onPressEnter"
|
||||
@scroll-breakpoint="onScrollBreakpoint"
|
||||
/>
|
||||
|
@ -123,7 +122,6 @@ const {
|
|||
isPhone,
|
||||
context,
|
||||
duration,
|
||||
sort,
|
||||
onPressEnter,
|
||||
playAll,
|
||||
playSelected,
|
||||
|
|
|
@ -41,7 +41,6 @@
|
|||
v-if="songs.length"
|
||||
ref="songList"
|
||||
class="-m-6"
|
||||
@sort="sort"
|
||||
@press:delete="removeSelected"
|
||||
@press:enter="onPressEnter"
|
||||
@scroll-breakpoint="onScrollBreakpoint"
|
||||
|
@ -92,7 +91,6 @@ const {
|
|||
playSelected,
|
||||
applyFilter,
|
||||
onScrollBreakpoint,
|
||||
sort
|
||||
} = useSongList(toRef(favoriteStore.state, 'playables'), { type: 'Favorites' })
|
||||
|
||||
const { SongListControls, config } = useSongListControls('Favorites')
|
||||
|
@ -109,8 +107,6 @@ const fetchSongs = async () => {
|
|||
loading.value = true
|
||||
await favoriteStore.fetch()
|
||||
loading.value = false
|
||||
await nextTick()
|
||||
sort()
|
||||
}
|
||||
|
||||
useRouter().onScreenActivated('Favorites', async () => {
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
v-else
|
||||
ref="songList"
|
||||
class="-m-6"
|
||||
@sort="sort"
|
||||
@sort="fetchWithSort"
|
||||
@press:enter="onPressEnter"
|
||||
@scroll-breakpoint="onScrollBreakpoint"
|
||||
@scrolled-to-end="fetch"
|
||||
|
@ -74,7 +74,7 @@ const {
|
|||
onPressEnter,
|
||||
playSelected,
|
||||
onScrollBreakpoint
|
||||
} = useSongList(ref<Song[]>([]), { type: 'Genre' })
|
||||
} = useSongList(ref<Song[]>([]), { type: 'Genre' }, { sortable: true, filterable: false })
|
||||
|
||||
const { SongListControls, config } = useSongListControls('Genre')
|
||||
|
||||
|
@ -93,7 +93,7 @@ const moreSongsAvailable = computed(() => page.value !== null)
|
|||
const showSkeletons = computed(() => loading.value && songs.value.length === 0)
|
||||
const duration = computed(() => secondsToHumanReadable(genre.value?.length ?? 0))
|
||||
|
||||
const sort = async (field: MaybeArray<PlayableListSortField>, order: SortOrder) => {
|
||||
const fetchWithSort = async (field: MaybeArray<PlayableListSortField>, order: SortOrder) => {
|
||||
page.value = 1
|
||||
songs.value = []
|
||||
sortField = field
|
||||
|
|
|
@ -83,7 +83,11 @@ const init = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const podcasts = computed(() => orderBy(fuzzy.search(keywords.value), sortParams.field, sortParams.order))
|
||||
const podcasts = computed(() => orderBy(
|
||||
keywords.value ? fuzzy.search(keywords.value) : podcastStore.state.podcasts,
|
||||
sortParams.field,
|
||||
sortParams.order
|
||||
))
|
||||
|
||||
const onFilterChanged = (q: string) => (keywords.value = q)
|
||||
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
v-else
|
||||
ref="songList"
|
||||
class="-m-6"
|
||||
@sort="sort"
|
||||
@press:enter="onPressEnter"
|
||||
@scroll-breakpoint="onScrollBreakpoint"
|
||||
/>
|
||||
|
@ -66,7 +65,6 @@ const {
|
|||
playAll,
|
||||
playSelected,
|
||||
applyFilter,
|
||||
sort,
|
||||
onScrollBreakpoint
|
||||
} = useSongList(toRef(searchStore.state, 'playables'), { type: 'Search.Songs' })
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@
|
|||
<VirtualScroller
|
||||
v-slot="{ item }: { item: PlayableRow }"
|
||||
:item-height="64"
|
||||
:items="filteredRows"
|
||||
:items="rows"
|
||||
@scroll="onScroll"
|
||||
@scrolled-to-end="$emit('scrolled-to-end')"
|
||||
>
|
||||
|
@ -108,13 +108,13 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { findIndex, findLastIndex, throttle } from 'lodash'
|
||||
import { findIndex, findLastIndex, sortBy, throttle } from 'lodash'
|
||||
import isMobile from 'ismobilejs'
|
||||
import { faCaretDown, faCaretUp } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, nextTick, onMounted, Ref, ref, watch } from 'vue'
|
||||
import { arrayify, eventBus, getPlayableCollectionContentType, requireInjection } from '@/utils'
|
||||
import { preferenceStore as preferences, queueStore } from '@/stores'
|
||||
import { useDraggable, useDroppable, useFuzzySearch } from '@/composables'
|
||||
import { useDraggable, useDroppable } from '@/composables'
|
||||
import { playbackService } from '@/services'
|
||||
import {
|
||||
PlayableListConfigKey,
|
||||
|
@ -122,7 +122,6 @@ import {
|
|||
PlayableListSortFieldKey,
|
||||
PlayablesKey,
|
||||
SelectedPlayablesKey,
|
||||
SongListFilterKeywordsKey,
|
||||
SongListSortOrderKey
|
||||
} from '@/symbols'
|
||||
|
||||
|
@ -142,29 +141,18 @@ const emit = defineEmits<{
|
|||
(e: 'scrolled-to-end'): void,
|
||||
}>()
|
||||
|
||||
const [items] = requireInjection<[Ref<Playable[]>]>(PlayablesKey)
|
||||
const [playables] = requireInjection<[Ref<Playable[]>]>(PlayablesKey)
|
||||
const [selectedPlayables, setSelectedPlayables] = requireInjection<[Ref<Playable[]>, Closure]>(SelectedPlayablesKey)
|
||||
const [sortField, setSortField] = requireInjection<[Ref<MaybeArray<PlayableListSortField>>, Closure]>(PlayableListSortFieldKey)
|
||||
const [sortOrder, setSortOrder] = requireInjection<[Ref<SortOrder>, Closure]>(SongListSortOrderKey)
|
||||
const [config] = requireInjection<[Partial<PlayableListConfig>]>(PlayableListConfigKey, [{}])
|
||||
const [context] = requireInjection<[PlayableListContext]>(PlayableListContextKey)
|
||||
|
||||
const filterKeywords = requireInjection(SongListFilterKeywordsKey, ref(''))
|
||||
|
||||
const wrapper = ref<HTMLElement>()
|
||||
const lastSelectedRow = ref<PlayableRow>()
|
||||
const sortFields = ref<PlayableListSortField[]>([])
|
||||
const rows = ref<PlayableRow[]>([])
|
||||
|
||||
const { search } = useFuzzySearch(rows, [
|
||||
'playable.title',
|
||||
'playable.artist_name',
|
||||
'playable.album_name',
|
||||
'playable.podcast_title',
|
||||
'playable.podcast_author',
|
||||
'playable.episode_description'
|
||||
])
|
||||
|
||||
const shouldTriggerContinuousPlayback = computed(() => {
|
||||
return preferences.continuous_playback
|
||||
&& typeof context.type !== 'undefined'
|
||||
|
@ -184,8 +172,6 @@ watch(
|
|||
{ deep: true }
|
||||
)
|
||||
|
||||
const filteredRows = computed(() => search(filterKeywords.value))
|
||||
|
||||
let lastScrollTop = 0
|
||||
|
||||
const onScroll = (e: Event) => {
|
||||
|
@ -210,7 +196,7 @@ const generateRows = () => {
|
|||
// selected playable manually.
|
||||
const selectedIds = selectedPlayables.value.map(playable => playable.id)
|
||||
|
||||
return items.value.map<PlayableRow>(playable => ({
|
||||
return playables.value.map<PlayableRow>(playable => ({
|
||||
playable,
|
||||
selected: selectedIds.includes(playable.id)
|
||||
}))
|
||||
|
@ -233,7 +219,7 @@ const render = () => {
|
|||
rows.value = generateRows()
|
||||
}
|
||||
|
||||
watch(items, () => render(), { deep: true })
|
||||
watch(playables, () => render(), { deep: true })
|
||||
|
||||
const handleDelete = () => {
|
||||
emit('press:delete')
|
||||
|
@ -245,9 +231,6 @@ const handleEnter = (event: KeyboardEvent) => {
|
|||
clearSelection()
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all (filtered) rows in the current list.
|
||||
*/
|
||||
const selectAllRows = () => rows.value.forEach(row => (row.selected = true))
|
||||
const clearSelection = () => rows.value.forEach(row => (row.selected = false))
|
||||
const handleA = (event: KeyboardEvent) => (event.ctrlKey || event.metaKey) && selectAllRows()
|
||||
|
|
|
@ -4,7 +4,7 @@ import { computed, provide, reactive, Ref, ref } from 'vue'
|
|||
import { playbackService } from '@/services'
|
||||
import { queueStore, songStore } from '@/stores'
|
||||
import { arrayify, eventBus, getPlayableProp, provideReadonly } from '@/utils'
|
||||
import { useRouter } from '@/composables'
|
||||
import { useFuzzySearch, useRouter } from '@/composables'
|
||||
|
||||
import {
|
||||
PlayableListConfigKey,
|
||||
|
@ -24,6 +24,7 @@ export const useSongList = (
|
|||
playables: Ref<Playable[]>,
|
||||
context: PlayableListContext = {},
|
||||
config: Partial<PlayableListConfig> = {
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
reorderable: false,
|
||||
collaborative: false,
|
||||
|
@ -33,8 +34,18 @@ export const useSongList = (
|
|||
const filterKeywords = ref('')
|
||||
config = reactive(config)
|
||||
context = reactive(context)
|
||||
|
||||
const { isCurrentScreen, go } = useRouter()
|
||||
|
||||
const fuzzy = config.filterable ? useFuzzySearch(playables, [
|
||||
'title',
|
||||
'artist_name',
|
||||
'album_name',
|
||||
'podcast_title',
|
||||
'podcast_author',
|
||||
'episode_description'
|
||||
]) : null
|
||||
|
||||
const songList = ref<InstanceType<typeof SongList>>()
|
||||
|
||||
const isPhone = isMobile.phone
|
||||
|
@ -68,6 +79,35 @@ export const useSongList = (
|
|||
|
||||
const applyFilter = throttle((keywords: string) => (filterKeywords.value = keywords), 200)
|
||||
|
||||
const filteredPlayables = computed(() => {
|
||||
if (!fuzzy) return playables.value
|
||||
|
||||
return sortField.value
|
||||
? orderBy(fuzzy.search(filterKeywords.value), extendedSortFields.value!, sortOrder.value)
|
||||
: fuzzy.search(filterKeywords.value)
|
||||
})
|
||||
|
||||
/**
|
||||
* Extends the sort fields based on the current field(s) to cater to relevant fields.
|
||||
* For example, sorting by track should take into account the disc number and the title.
|
||||
* Similarly, sorting by album name should also include the artist name, disc number, track number, and title, etc.
|
||||
*/
|
||||
const extendedSortFields = computed(() => {
|
||||
if (!sortField.value) return null
|
||||
|
||||
let extended: PlayableListSortField[] = arrayify(sortField.value)
|
||||
|
||||
if (sortField.value === 'track') {
|
||||
extended = ['disc', 'track', 'title']
|
||||
} else if (sortField.value.includes('album_name') && !sortField.value.includes('disc')) {
|
||||
extended.push('artist_name', 'disc', 'track', 'title')
|
||||
} else if (sortField.value.includes('artist_name') && !sortField.value.includes('disc')) {
|
||||
extended.push('album_name', 'disc', 'track', 'title')
|
||||
}
|
||||
|
||||
return extended
|
||||
})
|
||||
|
||||
const onPressEnter = async (event: KeyboardEvent) => {
|
||||
if (selectedPlayables.value.length === 1) {
|
||||
await playbackService.play(selectedPlayables.value[0])
|
||||
|
@ -97,28 +137,15 @@ export const useSongList = (
|
|||
const sortOrder = ref<SortOrder>('asc')
|
||||
|
||||
const sort = (by: MaybeArray<PlayableListSortField> | null = sortField.value, order: SortOrder = sortOrder.value) => {
|
||||
if (!config.sortable) return
|
||||
if (!by) return
|
||||
|
||||
// To sort a song list, we simply set the sort field and order.
|
||||
// The list will be sorted automatically by the computed property.
|
||||
sortField.value = by
|
||||
sortOrder.value = order
|
||||
|
||||
let sortFields: PlayableListSortField[] = arrayify(by)
|
||||
|
||||
if (by === 'track') {
|
||||
sortFields = ['disc', 'track', 'title']
|
||||
} else if (sortFields.includes('album_name') && !sortFields.includes('disc')) {
|
||||
sortFields.push('artist_name', 'disc', 'track', 'title')
|
||||
} else if (sortFields.includes('artist_name') && !sortFields.includes('disc')) {
|
||||
sortFields.push('album_name', 'disc', 'track', 'title')
|
||||
}
|
||||
|
||||
playables.value = orderBy(playables.value, sortFields, order)
|
||||
}
|
||||
|
||||
eventBus.on('SONGS_DELETED', deletedSongs => (playables.value = differenceBy(playables.value, deletedSongs, 'id')))
|
||||
|
||||
provideReadonly(PlayablesKey, playables, false)
|
||||
provideReadonly(PlayablesKey, filteredPlayables, false)
|
||||
provideReadonly(SelectedPlayablesKey, selectedPlayables, false)
|
||||
provideReadonly(PlayableListConfigKey, config)
|
||||
provideReadonly(PlayableListContextKey, context)
|
||||
|
|
1
resources/assets/js/types.d.ts
vendored
1
resources/assets/js/types.d.ts
vendored
|
@ -455,6 +455,7 @@ type ArtistAlbumViewMode = 'list' | 'thumbnails'
|
|||
type RepeatMode = 'NO_REPEAT' | 'REPEAT_ALL' | 'REPEAT_ONE'
|
||||
|
||||
interface PlayableListConfig {
|
||||
filterable: boolean
|
||||
sortable: boolean
|
||||
reorderable: boolean
|
||||
collaborative: boolean
|
||||
|
|
Loading…
Reference in a new issue