chore: refactor and simplify song filtering

This commit is contained in:
Phan An 2024-06-24 21:08:34 +02:00
parent 732e9b0d10
commit bd28da72d1
10 changed files with 62 additions and 56 deletions

View file

@ -61,7 +61,6 @@
<SongList
v-else
ref="songList"
@sort="sort"
@press:enter="onPressEnter"
@scroll-breakpoint="onScrollBreakpoint"
/>

View file

@ -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

View file

@ -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,

View file

@ -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 () => {

View file

@ -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

View file

@ -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)

View file

@ -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' })

View file

@ -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()

View file

@ -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)

View file

@ -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