2022-12-17 12:09:22 +00:00
|
|
|
import { differenceBy, orderBy, sampleSize, take, throttle } from 'lodash'
|
2022-07-07 18:05:46 +00:00
|
|
|
import isMobile from 'ismobilejs'
|
2024-10-13 17:37:01 +00:00
|
|
|
import type { Ref } from 'vue'
|
|
|
|
import { computed, provide, reactive, ref } from 'vue'
|
2022-04-24 08:50:45 +00:00
|
|
|
import { playbackService } from '@/services'
|
2024-08-29 17:54:58 +00:00
|
|
|
import { commonStore, queueStore, songStore } from '@/stores'
|
2024-05-19 05:49:42 +00:00
|
|
|
import { arrayify, eventBus, getPlayableProp, provideReadonly } from '@/utils'
|
2024-06-24 19:08:34 +00:00
|
|
|
import { useFuzzySearch, useRouter } from '@/composables'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-12-17 12:09:22 +00:00
|
|
|
import {
|
2024-05-19 05:49:42 +00:00
|
|
|
PlayableListConfigKey,
|
|
|
|
PlayableListContextKey,
|
|
|
|
PlayableListSortFieldKey,
|
2024-06-03 07:16:29 +00:00
|
|
|
PlayablesKey,
|
|
|
|
SelectedPlayablesKey,
|
|
|
|
SongListFilterKeywordsKey,
|
2024-10-13 17:37:01 +00:00
|
|
|
SongListSortOrderKey,
|
2022-12-17 12:09:22 +00:00
|
|
|
} from '@/symbols'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-07-07 18:05:46 +00:00
|
|
|
import ControlsToggle from '@/components/ui/ScreenControlsToggle.vue'
|
|
|
|
import SongList from '@/components/song/SongList.vue'
|
2022-07-16 09:52:39 +00:00
|
|
|
import ThumbnailStack from '@/components/ui/ThumbnailStack.vue'
|
2022-07-07 18:05:46 +00:00
|
|
|
|
2022-12-07 00:44:42 +00:00
|
|
|
export const useSongList = (
|
2024-05-19 05:49:42 +00:00
|
|
|
playables: Ref<Playable[]>,
|
|
|
|
context: PlayableListContext = {},
|
|
|
|
config: Partial<PlayableListConfig> = {
|
2024-06-24 19:08:34 +00:00
|
|
|
filterable: true,
|
2024-05-19 05:49:42 +00:00
|
|
|
sortable: true,
|
|
|
|
reorderable: false,
|
|
|
|
collaborative: false,
|
2024-10-13 17:37:01 +00:00
|
|
|
hasCustomOrderSort: false,
|
|
|
|
},
|
2022-12-07 00:44:42 +00:00
|
|
|
) => {
|
2022-12-17 12:09:22 +00:00
|
|
|
const filterKeywords = ref('')
|
2022-11-17 16:30:38 +00:00
|
|
|
config = reactive(config)
|
2024-03-25 22:59:38 +00:00
|
|
|
context = reactive(context)
|
2024-06-24 19:08:34 +00:00
|
|
|
|
2024-01-22 23:11:13 +00:00
|
|
|
const { isCurrentScreen, go } = useRouter()
|
2022-10-08 10:54:25 +00:00
|
|
|
|
2024-10-13 17:37:01 +00:00
|
|
|
const fuzzy = config.filterable
|
|
|
|
? useFuzzySearch(playables, [
|
2024-06-24 19:08:34 +00:00
|
|
|
'title',
|
|
|
|
'artist_name',
|
|
|
|
'album_name',
|
|
|
|
'podcast_title',
|
|
|
|
'podcast_author',
|
2024-10-13 17:37:01 +00:00
|
|
|
'episode_description',
|
|
|
|
])
|
|
|
|
: null
|
2024-06-24 19:08:34 +00:00
|
|
|
|
2022-04-23 21:24:02 +00:00
|
|
|
const songList = ref<InstanceType<typeof SongList>>()
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-04-23 21:24:02 +00:00
|
|
|
const isPhone = isMobile.phone
|
2024-05-19 05:49:42 +00:00
|
|
|
const selectedPlayables = ref<Playable[]>([])
|
2022-04-15 14:24:30 +00:00
|
|
|
const showingControls = ref(false)
|
2022-07-17 09:07:46 +00:00
|
|
|
const headerLayout = ref<ScreenHeaderLayout>('expanded')
|
2022-07-16 09:52:39 +00:00
|
|
|
|
2024-10-13 17:37:01 +00:00
|
|
|
const sortField = ref<MaybeArray<PlayableListSortField> | null>((() => {
|
|
|
|
if (!config.sortable) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
if (isCurrentScreen('Artist', 'Album')) {
|
|
|
|
return 'track'
|
|
|
|
}
|
|
|
|
if (isCurrentScreen('Search.Songs', 'Queue', 'RecentlyPlayed')) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
return 'title'
|
|
|
|
})())
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 sortOrder = ref<SortOrder>('asc')
|
|
|
|
|
2022-07-16 09:52:39 +00:00
|
|
|
const onScrollBreakpoint = (direction: 'up' | 'down') => {
|
|
|
|
headerLayout.value = direction === 'down' ? 'collapsed' : 'expanded'
|
|
|
|
}
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
const duration = computed(() => songStore.getFormattedLength(playables.value))
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2024-08-29 17:54:58 +00:00
|
|
|
const downloadable = computed(() => {
|
2024-10-13 17:37:01 +00:00
|
|
|
if (!commonStore.state.allows_download) {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if (playables.value.length === 0) {
|
|
|
|
return false
|
|
|
|
}
|
2024-08-29 17:54:58 +00:00
|
|
|
return playables.value.length === 1 || commonStore.state.supports_batch_downloading
|
|
|
|
})
|
|
|
|
|
2022-07-16 09:52:39 +00:00
|
|
|
const thumbnails = computed(() => {
|
2024-05-19 05:49:42 +00:00
|
|
|
const playablesWithCover = playables.value.filter(p => getPlayableProp<string>(p, 'album_cover', 'episode_image'))
|
|
|
|
|
|
|
|
const sampleCovers = sampleSize(playablesWithCover, 20)
|
|
|
|
.map(p => getPlayableProp<string>(p, 'album_cover', 'episode_image'))
|
|
|
|
|
2022-07-16 09:52:39 +00:00
|
|
|
return take(Array.from(new Set(sampleCovers)), 4)
|
|
|
|
})
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
const getPlayablesToPlay = () => songList.value!.getAllPlayablesWithSort()
|
2022-10-21 20:06:43 +00:00
|
|
|
|
|
|
|
const playAll = (shuffle: boolean) => {
|
2024-05-19 05:49:42 +00:00
|
|
|
playbackService.queueAndPlay(getPlayablesToPlay(), shuffle)
|
2022-11-18 18:44:20 +00:00
|
|
|
go('queue')
|
2022-10-21 20:06:43 +00:00
|
|
|
}
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
const playSelected = (shuffle: boolean) => playbackService.queueAndPlay(selectedPlayables.value, shuffle)
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-12-17 12:09:22 +00:00
|
|
|
const applyFilter = throttle((keywords: string) => (filterKeywords.value = keywords), 200)
|
|
|
|
|
2024-06-24 19:08:34 +00:00
|
|
|
const filteredPlayables = computed(() => {
|
2024-10-13 17:37:01 +00:00
|
|
|
if (!fuzzy) {
|
|
|
|
return playables.value
|
|
|
|
}
|
2024-06-24 19:08:34 +00:00
|
|
|
|
|
|
|
return sortField.value
|
|
|
|
? orderBy(fuzzy.search(filterKeywords.value), extendedSortFields.value!, sortOrder.value)
|
|
|
|
: fuzzy.search(filterKeywords.value)
|
|
|
|
})
|
|
|
|
|
2022-04-21 18:12:11 +00:00
|
|
|
const onPressEnter = async (event: KeyboardEvent) => {
|
2024-05-19 05:49:42 +00:00
|
|
|
if (selectedPlayables.value.length === 1) {
|
|
|
|
await playbackService.play(selectedPlayables.value[0])
|
2022-04-21 18:12:11 +00:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
// • Only Enter: Queue to bottom
|
|
|
|
// • Shift+Enter: Queues to top
|
|
|
|
// • Cmd/Ctrl+Enter: Queues to bottom and play the first selected item
|
|
|
|
// • Cmd/Ctrl+Shift+Enter: Queue to top and play the first queued item
|
|
|
|
event.shiftKey ? queueStore.queueToTop(selectedPlayables.value) : queueStore.queue(selectedPlayables.value)
|
2022-04-21 18:12:11 +00:00
|
|
|
|
|
|
|
if (event.ctrlKey || event.metaKey) {
|
2024-05-19 05:49:42 +00:00
|
|
|
await playbackService.play(selectedPlayables.value[0])
|
2022-04-21 18:12:11 +00:00
|
|
|
}
|
|
|
|
|
2022-11-18 18:44:20 +00:00
|
|
|
go('queue')
|
2022-04-21 18:12:11 +00:00
|
|
|
}
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
const sort = (by: MaybeArray<PlayableListSortField> | null = sortField.value, order: SortOrder = sortOrder.value) => {
|
2024-06-24 19:08:34 +00:00
|
|
|
// To sort a song list, we simply set the sort field and order.
|
|
|
|
// The list will be sorted automatically by the computed property.
|
2022-07-05 15:09:20 +00:00
|
|
|
sortField.value = by
|
|
|
|
sortOrder.value = order
|
2022-06-10 10:47:46 +00:00
|
|
|
}
|
|
|
|
|
2024-05-19 05:49:42 +00:00
|
|
|
eventBus.on('SONGS_DELETED', deletedSongs => (playables.value = differenceBy(playables.value, deletedSongs, 'id')))
|
2022-09-15 09:07:25 +00:00
|
|
|
|
2024-06-24 19:08:34 +00:00
|
|
|
provideReadonly(PlayablesKey, filteredPlayables, false)
|
2024-05-19 05:49:42 +00:00
|
|
|
provideReadonly(SelectedPlayablesKey, selectedPlayables, false)
|
|
|
|
provideReadonly(PlayableListConfigKey, config)
|
|
|
|
provideReadonly(PlayableListContextKey, context)
|
|
|
|
provideReadonly(PlayableListSortFieldKey, sortField)
|
2022-07-20 08:00:02 +00:00
|
|
|
provideReadonly(SongListSortOrderKey, sortOrder)
|
2022-06-10 10:47:46 +00:00
|
|
|
|
2022-12-17 12:09:22 +00:00
|
|
|
provide(SongListFilterKeywordsKey, filterKeywords)
|
|
|
|
|
2022-04-15 14:24:30 +00:00
|
|
|
return {
|
|
|
|
SongList,
|
2022-06-10 10:47:46 +00:00
|
|
|
ControlsToggle,
|
2022-07-16 09:52:39 +00:00
|
|
|
ThumbnailStack,
|
2024-05-19 05:49:42 +00:00
|
|
|
songs: playables,
|
2024-01-18 11:13:05 +00:00
|
|
|
config,
|
2024-03-25 22:59:38 +00:00
|
|
|
context,
|
2024-08-29 17:54:58 +00:00
|
|
|
downloadable,
|
2022-07-16 09:52:39 +00:00
|
|
|
headerLayout,
|
2022-07-05 15:09:20 +00:00
|
|
|
sortField,
|
|
|
|
sortOrder,
|
2022-04-23 21:24:02 +00:00
|
|
|
duration,
|
2022-07-16 09:52:39 +00:00
|
|
|
thumbnails,
|
2022-04-15 14:24:30 +00:00
|
|
|
songList,
|
2024-05-19 05:49:42 +00:00
|
|
|
selectedPlayables,
|
2022-04-15 14:24:30 +00:00
|
|
|
showingControls,
|
|
|
|
isPhone,
|
2022-04-21 18:12:11 +00:00
|
|
|
onPressEnter,
|
2022-04-15 14:24:30 +00:00
|
|
|
playAll,
|
|
|
|
playSelected,
|
2022-12-17 12:09:22 +00:00
|
|
|
applyFilter,
|
2022-07-16 09:52:39 +00:00
|
|
|
onScrollBreakpoint,
|
2024-10-13 17:37:01 +00:00
|
|
|
sort,
|
2022-04-15 14:24:30 +00:00
|
|
|
}
|
|
|
|
}
|