migration: song list controls

This commit is contained in:
Phan An 2022-04-21 18:06:45 +02:00
parent 6a06e5ef9b
commit c3880df2bc
No known key found for this signature in database
GPG key ID: A81E4477F0BB6FDC
37 changed files with 201 additions and 215 deletions

View file

@ -1,6 +1,6 @@
import Component from '@/components/screens/album.vue' import Component from '@/components/screens/album.vue'
import SongList from '@/components/song/list.vue' import SongList from '@/components/song/SongList.vue'
import { download, albumInfo as albumInfoService, playback } from '@/services' import { albumInfo as albumInfoService, download } from '@/services'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { mock } from '@/__tests__/__helpers__' import { mock } from '@/__tests__/__helpers__'
import { mount, shallow } from '@/__tests__/adapter' import { mount, shallow } from '@/__tests__/adapter'

View file

@ -1,5 +1,5 @@
import Component from '@/components/screens/all-songs.vue' import Component from '@/components/screens/AllSongsScreen.vue'
import SongList from '@/components/song/list.vue' import SongList from '@/components/song/SongList.vue'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { songStore } from '@/stores' import { songStore } from '@/stores'
import { mount } from '@/__tests__/adapter' import { mount } from '@/__tests__/adapter'

View file

@ -1,6 +1,6 @@
import Component from '@/components/screens/artist.vue' import Component from '@/components/screens/artist.vue'
import SongList from '@/components/song/list.vue' import SongList from '@/components/song/SongList.vue'
import { download, artistInfo as artistInfoService, playback } from '@/services' import { artistInfo as artistInfoService, download } from '@/services'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { mock } from '@/__tests__/__helpers__' import { mock } from '@/__tests__/__helpers__'
import { mount, shallow } from '@/__tests__/adapter' import { mount, shallow } from '@/__tests__/adapter'

View file

@ -1,6 +1,6 @@
import Component from '@/components/screens/favorites.vue' import Component from '@/components/screens/favorites.vue'
import SongList from '@/components/song/list.vue' import SongList from '@/components/song/SongList.vue'
import SongListControls from '@/components/song/list-controls.vue' import SongListControls from '@/components/songSongListControls.vue'
import { download } from '@/services' import { download } from '@/services'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { mock } from '@/__tests__/__helpers__' import { mock } from '@/__tests__/__helpers__'
@ -45,7 +45,7 @@ describe('components/screens/favorites', () => {
shallow(Component, { shallow(Component, {
data: () => ({ data: () => ({
state: { state: {
songs: factory('song', 5), songs: factory('song', 5)
}, },
sharedState: { allowDownload: true }, sharedState: { allowDownload: true },
meta: { meta: {

View file

@ -1,5 +1,5 @@
import Component from '@/components/screens/playlist.vue' import Component from '@/components/screens/playlist.vue'
import SongList from '@/components/song/list.vue' import SongList from '@/components/song/SongList.vue'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { eventBus } from '@/utils' import { eventBus } from '@/utils'
import { playlistStore } from '@/stores' import { playlistStore } from '@/stores'

View file

@ -1,5 +1,5 @@
import Component from '@/components/screens/queue.vue' import Component from '@/components/screens/queue.vue'
import SongList from '@/components/song/list.vue' import SongList from '@/components/song/SongList.vue'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { queueStore, songStore } from '@/stores' import { queueStore, songStore } from '@/stores'
import { playback } from '@/services' import { playback } from '@/services'

View file

@ -1,5 +1,5 @@
import Component from '@/components/screens/recently-played.vue' import Component from '@/components/screens/recently-played.vue'
import SongList from '@/components/song/list.vue' import SongList from '@/components/song/SongList.vue'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { recentlyPlayedStore } from '@/stores' import { recentlyPlayedStore } from '@/stores'
import { eventBus } from '@/utils' import { eventBus } from '@/utils'

View file

@ -1,4 +1,4 @@
import Component from '@/components/song/list-controls.vue' import Component from '@/components/songSongListControls.vue'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { take } from 'lodash' import { take } from 'lodash'
import { shallow, mount } from '@/__tests__/adapter' import { shallow, mount } from '@/__tests__/adapter'

View file

@ -1,5 +1,5 @@
import router from '@/router' import router from '@/router'
import Component from '@/components/song/list.vue' import Component from '@/components/song/SongList.vue'
import factory from '@/__tests__/factory' import factory from '@/__tests__/factory'
import { queueStore } from '@/stores' import { queueStore } from '@/stores'
import { playback } from '@/services' import { playback } from '@/services'

View file

@ -1,4 +1,4 @@
import Component from '@/components/ui/screen-controls-toggler.vue' import Component from '@/components/ui/ScreenControlsToggler.vue'
import isMobile from 'ismobilejs' import isMobile from 'ismobilejs'
import { shallow } from '@/__tests__/adapter' import { shallow } from '@/__tests__/adapter'

View file

@ -37,7 +37,7 @@ import HomeScreen from '@/components/screens/home.vue'
import QueueScreen from '@/components/screens/queue.vue' import QueueScreen from '@/components/screens/queue.vue'
import AlbumListScreen from '@/components/screens/album-list.vue' import AlbumListScreen from '@/components/screens/album-list.vue'
import ArtistListScreen from '@/components/screens/artist-list.vue' import ArtistListScreen from '@/components/screens/artist-list.vue'
import AllSongsScreen from '@/components/screens/all-songs.vue' import AllSongsScreen from '@/components/screens/AllSongsScreen.vue'
import PlaylistScreen from '@/components/screens/playlist.vue' import PlaylistScreen from '@/components/screens/playlist.vue'
import FavoritesScreen from '@/components/screens/favorites.vue' import FavoritesScreen from '@/components/screens/favorites.vue'

View file

@ -10,30 +10,33 @@
<template v-slot:controls> <template v-slot:controls>
<SongListControls <SongListControls
v-if="state.songs.length && (!isPhone || showingControls)" v-if="songs.length && (!isPhone || showingControls)"
@playAll="playAll" @playAll="playAll"
@playSelected="playSelected" @playSelected="playSelected"
:songs="state.songs" :songs="songs"
:config="songListControlConfig" :config="songListControlConfig"
:selectedSongs="selectedSongs" :selectedSongs="selectedSongs"
/> />
</template> </template>
</ScreenHeader> </ScreenHeader>
<SongList :items="state.songs" type="all-songs" ref="songList"/> <SongList :items="songs" type="all-songs" ref="songList"/>
</section> </section>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, toRef } from 'vue'
import { pluralize } from '@/utils' import { pluralize } from '@/utils'
import { songStore } from '@/stores' import { songStore } from '@/stores'
import { useSongList } from '@/composables' import { useSongList } from '@/composables'
import { defineAsyncComponent, reactive } from 'vue'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const { const {
SongList, SongList,
SongListControls, SongListControls,
ControlsToggler, ControlsToggler,
songs,
songList, songList,
meta, meta,
selectedSongs, selectedSongs,
@ -43,8 +46,5 @@ const {
playAll, playAll,
playSelected, playSelected,
toggleControls toggleControls
} = useSongList() } = useSongList(toRef(songStore.state, 'songs'))
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue'))
const state = reactive(songStore.state)
</script> </script>

View file

@ -20,7 +20,7 @@ import { eventBus, limitBy } from '@/utils'
import { albumStore, preferenceStore as preferences } from '@/stores' import { albumStore, preferenceStore as preferences } from '@/stores'
import { useInfiniteScroll } from '@/composables' import { useInfiniteScroll } from '@/composables'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const AlbumCard = defineAsyncComponent(() => import('@/components/album/card.vue')) const AlbumCard = defineAsyncComponent(() => import('@/components/album/card.vue'))
const ViewModeSwitch = defineAsyncComponent(() => import('@/components/ui/view-mode-switch.vue')) const ViewModeSwitch = defineAsyncComponent(() => import('@/components/ui/view-mode-switch.vue'))

View file

@ -9,22 +9,22 @@
</template> </template>
<template v-slot:meta> <template v-slot:meta>
<span v-if="album.songs.length"> <span v-if="songs.length">
by by
<a class="artist" v-if="isNormalArtist" :href="`#!/artist/${album.artist.id}`">{{ album.artist.name }}</a> <a v-if="isNormalArtist" :href="`#!/artist/${album.artist.id}`" class="artist">{{ album.artist.name }}</a>
<span class="nope" v-else>{{ album.artist.name }}</span> <span class="nope" v-else>{{ album.artist.name }}</span>
{{ pluralize(album.songs.length, 'song') }} {{ pluralize(songs.length, 'song') }}
{{ fmtLength }} {{ fmtLength }}
<template v-if="sharedState.useLastfm"> <template v-if="sharedState.useLastfm">
<a class="info" href @click.prevent="showInfo" title="View album's extra information">Info</a> <a class="info" href title="View album's extra information" @click.prevent="showInfo">Info</a>
</template> </template>
<template v-if="sharedState.allowDownload"> <template v-if="sharedState.allowDownload">
<a class="download" href @click.prevent="download" title="Download all songs in album" role="button"> <a class="download" href role="button" title="Download all songs in album" @click.prevent="download">
Download All Download All
</a> </a>
</template> </template>
@ -33,19 +33,19 @@
<template v-slot:controls> <template v-slot:controls>
<SongListControls <SongListControls
v-if="album.songs.length && (!isPhone || showingControls)" v-if="songs.length && (!isPhone || showingControls)"
@playAll="playAll"
@playSelected="playSelected"
:songs="album.songs"
:config="songListControlConfig" :config="songListControlConfig"
:selectedSongs="selectedSongs" :selectedSongs="selectedSongs"
:songs="songs"
@playAll="playAll"
@playSelected="playSelected"
/> />
</template> </template>
</ScreenHeader> </ScreenHeader>
<SongList :items="album.songs" type="album" :config="listConfig" ref="songList"/> <SongList :items="songs" type="album" :config="listConfig" ref="songList"/>
<section class="info-wrapper" v-if="sharedState.useLastfm && showing"> <section v-if="sharedState.useLastfm && showing" class="info-wrapper">
<CloseModalBtn @click="showing = false"/> <CloseModalBtn @click="showing = false"/>
<div class="inner"> <div class="inner">
<div class="loading" v-if="loading"> <div class="loading" v-if="loading">
@ -65,16 +65,20 @@ import { albumInfo as albumInfoService, download as downloadService } from '@/se
import router from '@/router' import router from '@/router'
import { useAlbumAttributes, useSongList } from '@/composables' import { useAlbumAttributes, useSongList } from '@/composables'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const AlbumInfo = defineAsyncComponent(() => import('@/components/album/AlbumInfo.vue')) const AlbumInfo = defineAsyncComponent(() => import('@/components/album/AlbumInfo.vue'))
const SoundBar = defineAsyncComponent(() => import('@/components/ui/sound-bar.vue')) const SoundBar = defineAsyncComponent(() => import('@/components/ui/sound-bar.vue'))
const AlbumThumbnail = defineAsyncComponent(() => import('@/components/ui/AlbumArtistThumbnail.vue')) const AlbumThumbnail = defineAsyncComponent(() => import('@/components/ui/AlbumArtistThumbnail.vue'))
const CloseModalBtn = defineAsyncComponent(() => import('@/components/ui/close-modal-btn.vue')) const CloseModalBtn = defineAsyncComponent(() => import('@/components/ui/close-modal-btn.vue'))
const props = defineProps<{ album: Album }>()
const { album } = toRefs(props)
const { const {
SongList, SongList,
SongListControls, SongListControls,
ControlsToggler, ControlsToggler,
songs,
songList, songList,
selectedSongs, selectedSongs,
showingControls, showingControls,
@ -83,10 +87,7 @@ const {
playAll, playAll,
playSelected, playSelected,
toggleControls toggleControls
} = useSongList() } = useSongList(ref(album.value.songs))
const props = defineProps<{ album: Album }>()
const { album } = toRefs(props)
const { length, fmtLength } = useAlbumAttributes(album.value) const { length, fmtLength } = useAlbumAttributes(album.value)

View file

@ -28,7 +28,7 @@ const {
makeScrollable makeScrollable
} = useInfiniteScroll(9) } = useInfiniteScroll(9)
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ArtistCard = defineAsyncComponent(() => import('@/components/artist/card.vue')) const ArtistCard = defineAsyncComponent(() => import('@/components/artist/card.vue'))
const ViewModeSwitch = defineAsyncComponent(() => import('@/components/ui/view-mode-switch.vue')) const ViewModeSwitch = defineAsyncComponent(() => import('@/components/ui/view-mode-switch.vue'))

View file

@ -9,26 +9,26 @@
</template> </template>
<template v-slot:meta> <template v-slot:meta>
<span v-if="artist.songs.length"> <span v-if="songs.length">
{{ pluralize(artist.albums.length, 'album') }} {{ pluralize(artist.albums.length, 'album') }}
{{ pluralize(artist.songs.length, 'song') }} {{ pluralize(songs.length, 'song') }}
{{ fmtLength }} {{ fmtLength }}
<template v-if="sharedState.useLastfm"> <template v-if="sharedState.useLastfm">
<a class="info" href @click.prevent="showInfo" title="View artist's extra information">Info</a> <a class="info" href title="View artist's extra information" @click.prevent="showInfo">Info</a>
</template> </template>
<template v-if="sharedState.allowDownload"> <template v-if="sharedState.allowDownload">
<a <a
@click.prevent="download"
class="download" class="download"
href href
role="button" role="button"
title="Download all songs by this artist" title="Download all songs by this artist"
@click.prevent="download"
> >
Download All Download All
</a> </a>
@ -38,17 +38,17 @@
<template v-slot:controls> <template v-slot:controls>
<SongListControls <SongListControls
v-if="artist.songs.length && (!isPhone || showingControls)" v-if="songs.length && (!isPhone || showingControls)"
@playAll="playAll"
@playSelected="playSelected"
:songs="artist.songs"
:config="songListControlConfig" :config="songListControlConfig"
:selectedSongs="selectedSongs" :selectedSongs="selectedSongs"
:songs="songs"
@playAll="playAll"
@playSelected="playSelected"
/> />
</template> </template>
</ScreenHeader> </ScreenHeader>
<SongList :items="artist.songs" type="artist" :config="listConfig" ref="songList"/> <SongList :items="songs" type="artist" :config="listConfig" ref="songList"/>
<section class="info-wrapper" v-if="sharedState.useLastfm && showing"> <section class="info-wrapper" v-if="sharedState.useLastfm && showing">
<CloseModalBtn @click="showing = false"/> <CloseModalBtn @click="showing = false"/>
@ -70,12 +70,15 @@ import { artistInfo as artistInfoService, download as downloadService } from '@/
import router from '@/router' import router from '@/router'
import { useArtistAttributes, useSongList } from '@/composables' import { useArtistAttributes, useSongList } from '@/composables'
const props = defineProps<{ artist: Artist }>()
const { artist } = toRefs(props)
const { const {
SongList, SongList,
SongListControls, SongListControls,
ControlsToggler, ControlsToggler,
songList, songList,
state, songs,
meta, meta,
selectedSongs, selectedSongs,
showingControls, showingControls,
@ -84,13 +87,11 @@ const {
playAll, playAll,
playSelected, playSelected,
toggleControls toggleControls
} = useSongList() } = useSongList(ref(artist.value.songs))
const props = defineProps<{ artist: Artist }>()
const { artist } = toRefs(props)
const { length, fmtLength, image } = useArtistAttributes(artist.value) const { length, fmtLength, image } = useArtistAttributes(artist.value)
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ArtistInfo = defineAsyncComponent(() => import('@/components/artist/info.vue')) const ArtistInfo = defineAsyncComponent(() => import('@/components/artist/info.vue'))
const SoundBar = defineAsyncComponent(() => import('@/components/ui/sound-bar.vue')) const SoundBar = defineAsyncComponent(() => import('@/components/ui/sound-bar.vue'))
const ArtistThumbnail = defineAsyncComponent(() => import('@/components/ui/AlbumArtistThumbnail.vue')) const ArtistThumbnail = defineAsyncComponent(() => import('@/components/ui/AlbumArtistThumbnail.vue'))
@ -112,7 +113,6 @@ watch(() => artist.value.albums.length, newAlbumCount => newAlbumCount || router
watch(artist, () => { watch(artist, () => {
showing.value = false showing.value = false
// @ts-ignore
songList.value?.sort() songList.value?.sort()
}) })

View file

@ -9,9 +9,9 @@
{{ pluralize(meta.songCount, 'song') }} {{ pluralize(meta.songCount, 'song') }}
{{ meta.totalLength }} {{ meta.totalLength }}
<template v-if="sharedState.allowDownload && state.songs.length"> <template v-if="allowDownload && songs.length">
<a href @click.prevent="download" class="download" title="Download all songs in playlist" role="button"> <a class="download" href role="button" title="Download all songs in playlist" @click.prevent="download">
Download All Download All
</a> </a>
</template> </template>
@ -20,17 +20,17 @@
<template v-slot:controls> <template v-slot:controls>
<SongListControls <SongListControls
v-if="state.songs.length && (!isPhone || showingControls)" v-if="songs.length && (!isPhone || showingControls)"
@playAll="playAll"
@playSelected="playSelected"
:songs="state.songs"
:config="songListControlConfig" :config="songListControlConfig"
:selectedSongs="selectedSongs" :selectedSongs="selectedSongs"
:songs="songs"
@playAll="playAll"
@playSelected="playSelected"
/> />
</template> </template>
</ScreenHeader> </ScreenHeader>
<SongList v-if="state.songs.length" :items="state.songs" type="favorites" ref="songList"/> <SongList v-if="songs.length" ref="songList" :items="songs" type="favorites"/>
<ScreenPlaceholder v-else> <ScreenPlaceholder v-else>
<template v-slot:icon> <template v-slot:icon>
@ -38,8 +38,8 @@
</template> </template>
No favorites yet. No favorites yet.
<span class="secondary d-block"> <span class="secondary d-block">
Click the Click the&nbsp;
<i class="fa fa-heart-o"></i> <i class="fa fa-heart-o"></i>&nbsp;
icon to mark a song as favorite. icon to mark a song as favorite.
</span> </span>
</ScreenPlaceholder> </ScreenPlaceholder>
@ -51,15 +51,16 @@ import { pluralize } from '@/utils'
import { favoriteStore, sharedStore } from '@/stores' import { favoriteStore, sharedStore } from '@/stores'
import { download as downloadService } from '@/services' import { download as downloadService } from '@/services'
import { useSongList } from '@/composables' import { useSongList } from '@/composables'
import { defineAsyncComponent, reactive } from 'vue' import { defineAsyncComponent, toRef } from 'vue'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue')) const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue'))
const { const {
SongList, SongList,
SongListControls, SongListControls,
ControlsToggler, ControlsToggler,
songs,
songList, songList,
meta, meta,
selectedSongs, selectedSongs,
@ -69,10 +70,9 @@ const {
playAll, playAll,
playSelected, playSelected,
toggleControls toggleControls
} = useSongList() } = useSongList(toRef(favoriteStore.state, 'songs'))
const state = reactive(favoriteStore.state) const allowDownload = toRef(sharedStore.state, 'allowDownload')
const sharedState = reactive(sharedStore.state)
const download = () => downloadService.fromFavorites() const download = () => downloadService.fromFavorites()
</script> </script>

View file

@ -89,7 +89,7 @@ import router from '@/router'
import { useInfiniteScroll } from '@/composables' import { useInfiniteScroll } from '@/composables'
import { computed, defineAsyncComponent, reactive, ref } from 'vue' import { computed, defineAsyncComponent, reactive, ref } from 'vue'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const AlbumCard = defineAsyncComponent(() => import('@/components/album/card.vue')) const AlbumCard = defineAsyncComponent(() => import('@/components/album/card.vue'))
const ArtistCard = defineAsyncComponent(() => import('@/components/artist/card.vue')) const ArtistCard = defineAsyncComponent(() => import('@/components/artist/card.vue'))
const SongCard = defineAsyncComponent(() => import('@/components/song/card.vue')) const SongCard = defineAsyncComponent(() => import('@/components/song/card.vue'))

View file

@ -1,5 +1,5 @@
<template> <template>
<section id="playlistWrapper"> <section id="playlistWrapper" v-if="playlist">
<ScreenHeader> <ScreenHeader>
{{ playlist.name }} {{ playlist.name }}
<ControlsToggler v-if="playlist.populated" :showing-controls="showingControls" @toggleControls="toggleControls"/> <ControlsToggler v-if="playlist.populated" :showing-controls="showingControls" @toggleControls="toggleControls"/>
@ -33,11 +33,11 @@
<template v-if="playlist.populated"> <template v-if="playlist.populated">
<SongList <SongList
v-if="playlist.songs.length" v-if="songs.length"
:items="playlist.songs" ref="songList"
:items="songs"
:playlist="playlist" :playlist="playlist"
type="playlist" type="playlist"
ref="songList"
/> />
<ScreenPlaceholder v-else> <ScreenPlaceholder v-else>
@ -62,23 +62,24 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { eventBus } from '@/utils' import { defineAsyncComponent, nextTick, ref } from 'vue'
import { eventBus, pluralize } from '@/utils'
import { playlistStore, sharedStore } from '@/stores' import { playlistStore, sharedStore } from '@/stores'
import { download as downloadService } from '@/services' import { download as downloadService } from '@/services'
import { useSongList } from '@/composables' import { useSongList } from '@/composables'
import { defineAsyncComponent, nextTick, reactive, ref } from 'vue'
import { pluralize } from '@/utils'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue')) const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue'))
const playlist = ref<Playlist>()
const { const {
SongList, SongList,
SongListControls, SongListControls,
ControlsToggler, ControlsToggler,
songs,
songList, songList,
meta, meta,
state,
selectedSongs, selectedSongs,
showingControls, showingControls,
songListControlConfig, songListControlConfig,
@ -86,15 +87,12 @@ const {
playAll, playAll,
playSelected, playSelected,
toggleControls toggleControls
} = useSongList({ } = useSongList(ref(playlist.value?.songs || []), { deletePlaylist: true })
deletePlaylist: true
})
const playlist = ref<Playlist>(playlistStore.stub) const sharedState = sharedStore.state
const sharedState = reactive(sharedStore.state)
const destroy = () => eventBus.emit('PLAYLIST_DELETE', playlist.value) const destroy = () => eventBus.emit('PLAYLIST_DELETE', playlist.value)
const download = () => downloadService.fromPlaylist(playlist.value) const download = () => downloadService.fromPlaylist(playlist.value!)
const editSmartPlaylist = () => eventBus.emit('MODAL_SHOW_EDIT_SMART_PLAYLIST_FORM', playlist.value) const editSmartPlaylist = () => eventBus.emit('MODAL_SHOW_EDIT_SMART_PLAYLIST_FORM', playlist.value)
/** /**
@ -103,9 +101,8 @@ const editSmartPlaylist = () => eventBus.emit('MODAL_SHOW_EDIT_SMART_PLAYLIST_FO
const populate = async (_playlist: Playlist) => { const populate = async (_playlist: Playlist) => {
await playlistStore.fetchSongs(_playlist) await playlistStore.fetchSongs(_playlist)
playlist.value = _playlist playlist.value = _playlist
state.songs = playlist.value.songs songs.value = playlist.value.songs
await nextTick() await nextTick()
// @ts-ignore
songList.value?.sort() songList.value?.sort()
} }
@ -116,7 +113,7 @@ eventBus.on('LOAD_MAIN_CONTENT', (view: MainViewName, _playlist: Playlist): void
if (_playlist.populated) { if (_playlist.populated) {
playlist.value = _playlist playlist.value = _playlist
state.songs = playlist.value.songs songs.value = playlist.value.songs
} else { } else {
populate(_playlist) populate(_playlist)
} }

View file

@ -14,7 +14,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent } from 'vue' import { defineAsyncComponent } from 'vue'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ProfileForm = defineAsyncComponent(() => import('@/components/profile-preferences/profile-form.vue')) const ProfileForm = defineAsyncComponent(() => import('@/components/profile-preferences/profile-form.vue'))
const LastfmIntegration = defineAsyncComponent(() => import('@/components/profile-preferences/lastfm-integration.vue')) const LastfmIntegration = defineAsyncComponent(() => import('@/components/profile-preferences/lastfm-integration.vue'))
const Preferences = defineAsyncComponent(() => import('@/components/profile-preferences/preferences.vue')) const Preferences = defineAsyncComponent(() => import('@/components/profile-preferences/preferences.vue'))

View file

@ -46,19 +46,20 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, reactive, toRef } from 'vue'
import { pluralize } from '@/utils' import { pluralize } from '@/utils'
import { queueStore, songStore } from '@/stores' import { queueStore, songStore } from '@/stores'
import { playback } from '@/services' import { playback } from '@/services'
import { useSongList } from '@/composables' import { useSongList } from '@/composables'
import { computed, defineAsyncComponent, reactive, toRef } from 'vue'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue')) const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue'))
const { const {
SongList, SongList,
SongListControls, SongListControls,
ControlsToggler, ControlsToggler,
songs,
songList, songList,
meta, meta,
selectedSongs, selectedSongs,
@ -67,19 +68,13 @@ const {
isPhone, isPhone,
playSelected, playSelected,
toggleControls toggleControls
} = useSongList({ } = useSongList(toRef(queueStore.state, 'songs'), { clearQueue: true })
clearQueue: true
})
const songs = toRef(queueStore.state, 'songs')
const songState = reactive(songStore.state) const songState = reactive(songStore.state)
const shouldShowShufflingAllLink = computed(() => songState.songs.length > 0) const shouldShowShufflingAllLink = computed(() => songState.songs.length > 0)
const playAll = () => { const playAll = () => playback.queueAndPlay(songs.value.length ? songList.value.getAllSongsWithSort() : songStore.all)
playback.queueAndPlay(songs.value.length ? songList.value.getAllSongsWithSort() : songStore.all)
}
const shuffleAll = async () => await playback.queueAndPlay(songStore.all, true) const shuffleAll = async () => await playback.queueAndPlay(songStore.all, true)
const clearQueue = () => queueStore.clear() const clearQueue = () => queueStore.clear()
</script> </script>

View file

@ -10,26 +10,24 @@
<template v-slot:controls> <template v-slot:controls>
<SongListControls <SongListControls
v-if="state.songs.length && (!isPhone || showingControls)" v-if="songs.length && (!isPhone || showingControls)"
@playAll="playAll"
@playSelected="playSelected"
:songs="state.songs"
:config="songListControlConfig" :config="songListControlConfig"
:selectedSongs="selectedSongs" :selectedSongs="selectedSongs"
:songs="songs"
@playAll="playAll"
@playSelected="playSelected"
/> />
</template> </template>
</ScreenHeader> </ScreenHeader>
<SongList v-if="state.songs.length" :items="state.songs" type="recently-played" :sortable="false"/> <SongList v-if="songs.length" :items="songs" :sortable="false" type="recently-played"/>
<ScreenPlaceholder v-else> <ScreenPlaceholder v-else>
<template v-slot:icon> <template v-slot:icon>
<i class="fa fa-clock-o"></i> <i class="fa fa-clock-o"></i>
</template> </template>
No songs recently played. No songs recently played.
<span class="secondary d-block"> <span class="secondary d-block">Start playing to populate this playlist.</span>
Start playing to populate this playlist.
</span>
</ScreenPlaceholder> </ScreenPlaceholder>
</section> </section>
</template> </template>
@ -38,16 +36,17 @@
import { eventBus, pluralize } from '@/utils' import { eventBus, pluralize } from '@/utils'
import { recentlyPlayedStore } from '@/stores' import { recentlyPlayedStore } from '@/stores'
import { useSongList } from '@/composables' import { useSongList } from '@/composables'
import { defineAsyncComponent, reactive } from 'vue' import { defineAsyncComponent, reactive, toRef } from 'vue'
import { playback } from '@/services' import { playback } from '@/services'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue')) const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue'))
const { const {
SongList, SongList,
SongListControls, SongListControls,
ControlsToggler, ControlsToggler,
songs,
songList, songList,
meta, meta,
selectedSongs, selectedSongs,
@ -56,11 +55,9 @@ const {
isPhone, isPhone,
playSelected, playSelected,
toggleControls toggleControls
} = useSongList() } = useSongList(toRef(recentlyPlayedStore.state, 'songs'))
const state = reactive(recentlyPlayedStore.state) const playAll = () => playback.queueAndPlay(songs.value)
const playAll = () => playback.queueAndPlay(state.songs)
eventBus.on({ eventBus.on({
'LOAD_MAIN_CONTENT': (view: MainViewName) => view === 'RecentlyPlayed' && recentlyPlayedStore.fetchAll() 'LOAD_MAIN_CONTENT': (view: MainViewName) => view === 'RecentlyPlayed' && recentlyPlayedStore.fetchAll()

View file

@ -65,7 +65,7 @@ import { eventBus } from '@/utils'
import { searchStore } from '@/stores' import { searchStore } from '@/stores'
import router from '@/router' import router from '@/router'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue')) const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue'))
const SongCard = defineAsyncComponent(() => import('@/components/song/card.vue')) const SongCard = defineAsyncComponent(() => import('@/components/song/card.vue'))
const ArtistCard = defineAsyncComponent(() => import('@/components/artist/card.vue')) const ArtistCard = defineAsyncComponent(() => import('@/components/artist/card.vue'))

View file

@ -10,34 +10,37 @@
<template v-slot:controls> <template v-slot:controls>
<SongListControls <SongListControls
v-if="state.songs.length && (!isPhone || showingControls)" v-if="songs.length && (!isPhone || showingControls)"
:config="songListControlConfig" :config="songListControlConfig"
:selectedSongs="selectedSongs" :selectedSongs="selectedSongs"
:songs="state.songs" :songs="songs"
@playAll="playAll" @playAll="playAll"
@playSelected="playSelected" @playSelected="playSelected"
/> />
</template> </template>
</ScreenHeader> </ScreenHeader>
<SongList ref="songList" :items="state.songs" type="search-results"/> <SongList ref="songList" :items="songs" type="search-results"/>
</section> </section>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, defineAsyncComponent, toRef, toRefs } from 'vue'
import { searchStore } from '@/stores' import { searchStore } from '@/stores'
import { computed, defineAsyncComponent, reactive, toRefs } from 'vue'
import { useSongList } from '@/composables' import { useSongList } from '@/composables'
import { pluralize } from '@/utils' import { pluralize } from '@/utils'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const props = defineProps<{ q: string }>()
const { q } = toRefs(props)
const { const {
SongList, SongList,
SongListControls, SongListControls,
ControlsToggler, ControlsToggler,
songs,
songList, songList,
state: songListState,
meta, meta,
selectedSongs, selectedSongs,
showingControls, showingControls,
@ -46,12 +49,7 @@ const {
playAll, playAll,
playSelected, playSelected,
toggleControls toggleControls
} = useSongList() } = useSongList(toRef(searchStore.state, 'songs'))
const props = defineProps<{ q: string }>()
const { q } = toRefs(props)
const state = reactive(searchStore.state)
const decodedQ = computed(() => decodeURIComponent(q.value)) const decodedQ = computed(() => decodeURIComponent(q.value))

View file

@ -34,7 +34,7 @@ import { settingStore, sharedStore } from '@/stores'
import { alerts, forceReloadWindow, hideOverlay, parseValidationError, showOverlay } from '@/utils' import { alerts, forceReloadWindow, hideOverlay, parseValidationError, showOverlay } from '@/utils'
import router from '@/router' import router from '@/router'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const Btn = defineAsyncComponent(() => import('@/components/ui/btn.vue')) const Btn = defineAsyncComponent(() => import('@/components/ui/btn.vue'))
const state = settingStore.state const state = settingStore.state

View file

@ -67,10 +67,10 @@ import { UploadFile, validMediaMimeTypes } from '@/config'
import { upload } from '@/services' import { upload } from '@/services'
import UploadItem from '@/components/ui/upload/upload-item.vue' import UploadItem from '@/components/ui/upload/upload-item.vue'
import BtnGroup from '@/components/ui/btn-group.vue' import BtnGroup from '@/components/ui/BtnGroup.vue'
import Btn from '@/components/ui/btn.vue' import Btn from '@/components/ui/btn.vue'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue')) const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue'))
const mediaPath = toRef(settingStore.state, 'media_path') const mediaPath = toRef(settingStore.state, 'media_path')

View file

@ -29,10 +29,10 @@ import isMobile from 'ismobilejs'
import { userStore } from '@/stores' import { userStore } from '@/stores'
import { eventBus } from '@/utils' import { eventBus } from '@/utils'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ControlsToggler = defineAsyncComponent(() => import('@/components/ui/screen-controls-toggler.vue')) const ControlsToggler = defineAsyncComponent(() => import('@/components/ui/ScreenControlsToggler.vue'))
const Btn = defineAsyncComponent(() => import('@/components/ui/btn.vue')) const Btn = defineAsyncComponent(() => import('@/components/ui/btn.vue'))
const BtnGroup = defineAsyncComponent(() => import('@/components/ui/btn-group.vue')) const BtnGroup = defineAsyncComponent(() => import('@/components/ui/BtnGroup.vue'))
const UserCard = defineAsyncComponent(() => import('@/components/user/card.vue')) const UserCard = defineAsyncComponent(() => import('@/components/user/card.vue'))
const state = reactive(userStore.state) const state = reactive(userStore.state)

View file

@ -25,7 +25,7 @@ import createYouTubePlayer from 'youtube-player'
let player: YouTubePlayer|null = null let player: YouTubePlayer|null = null
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue')) const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue')) const ScreenPlaceholder = defineAsyncComponent(() => import('@/components/ui/screen-placeholder.vue'))
const title = ref('YouTube Video') const title = ref('YouTube Video')

View file

@ -72,7 +72,6 @@ import {
getCurrentInstance, getCurrentInstance,
nextTick, nextTick,
onMounted, onMounted,
PropType,
ref, ref,
toRefs, toRefs,
watch watch
@ -182,7 +181,8 @@ const render = () => {
watch(items, () => render()) watch(items, () => render())
watch(selectedSongs, () => eventBus.emit('SET_SELECTED_SONGS', selectedSongs, getCurrentInstance()?.parent)) const vm = getCurrentInstance()
watch(selectedSongs, () => eventBus.emit('SET_SELECTED_SONGS', selectedSongs.value, vm?.parent))
const handleDelete = () => { const handleDelete = () => {
if (!selectedSongs.value.length) { if (!selectedSongs.value.length) {

View file

@ -4,23 +4,23 @@
<template v-if="mergedConfig.play"> <template v-if="mergedConfig.play">
<template v-if="altPressed"> <template v-if="altPressed">
<Btn <Btn
@click.prevent="playAll" v-if="selectedSongs.length < 2 && songs.length"
class="btn-play-all" class="btn-play-all"
data-test="btn-play-all"
orange orange
title="Play all songs" title="Play all songs"
v-if="selectedSongs.length < 2 && songs.length" @click.prevent="playAll"
data-test="btn-play-all"
> >
<i class="fa fa-play"></i> All <i class="fa fa-play"></i> All
</Btn> </Btn>
<Btn <Btn
@click.prevent="playSelected" v-if="selectedSongs.length > 1"
class="btn-play-selected" class="btn-play-selected"
data-test="btn-play-selected"
orange orange
title="Play selected songs" title="Play selected songs"
v-if="selectedSongs.length > 1" @click.prevent="playSelected"
data-test="btn-play-selected"
> >
<i class="fa fa-play"></i> Selected <i class="fa fa-play"></i> Selected
</Btn> </Btn>
@ -28,23 +28,23 @@
<template v-else> <template v-else>
<Btn <Btn
@click.prevent="shuffle" v-if="selectedSongs.length < 2 && songs.length"
class="btn-shuffle-all" class="btn-shuffle-all"
data-test="btn-shuffle-all"
orange orange
title="Shuffle all songs" title="Shuffle all songs"
v-if="selectedSongs.length < 2 && songs.length" @click.prevent="shuffle"
data-test="btn-shuffle-all"
> >
<i class="fa fa-random"></i> All <i class="fa fa-random"></i> All
</Btn> </Btn>
<Btn <Btn
@click.prevent="shuffleSelected" v-if="selectedSongs.length > 1"
class="btn-shuffle-selected" class="btn-shuffle-selected"
data-test="btn-shuffle-selected"
orange orange
title="Shuffle selected songs" title="Shuffle selected songs"
v-if="selectedSongs.length > 1" @click.prevent="shuffleSelected"
data-test="btn-shuffle-selected"
> >
<i class="fa fa-random"></i> Selected <i class="fa fa-random"></i> Selected
</Btn> </Btn>
@ -63,21 +63,21 @@
</Btn> </Btn>
<Btn <Btn
@click.prevent="clearQueue" v-if="showClearQueueButton"
class="btn-clear-queue" class="btn-clear-queue"
red red
v-if="showClearQueueButton"
title="Clear current queue" title="Clear current queue"
@click.prevent="clearQueue"
> >
Clear Clear
</Btn> </Btn>
<Btn <Btn
@click.prevent="deletePlaylist" v-if="showDeletePlaylistButton"
class="del btn-delete-playlist" class="del btn-delete-playlist"
red red
title="Delete this playlist" title="Delete this playlist"
v-if="showDeletePlaylistButton" @click.prevent="deletePlaylist"
> >
<i class="fa fa-times"></i> Playlist <i class="fa fa-times"></i> Playlist
</Btn> </Btn>
@ -85,11 +85,11 @@
</BtnGroup> </BtnGroup>
<AddToMenu <AddToMenu
@closing="closeAddToMenu"
:config="mergedConfig.addTo"
:songs="selectedSongs"
:showing="showingAddToMenu"
v-koel-clickaway="closeAddToMenu" v-koel-clickaway="closeAddToMenu"
:config="mergedConfig.addTo"
:showing="showingAddToMenu"
:songs="selectedSongs"
@closing="closeAddToMenu"
/> />
</div> </div>
</template> </template>
@ -99,17 +99,20 @@ import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref,
const AddToMenu = defineAsyncComponent(() => import('./AddToMenu.vue')) const AddToMenu = defineAsyncComponent(() => import('./AddToMenu.vue'))
const Btn = defineAsyncComponent(() => import('@/components/ui/btn.vue')) const Btn = defineAsyncComponent(() => import('@/components/ui/btn.vue'))
const BtnGroup = defineAsyncComponent(() => import('@/components/ui/btn-group.vue')) const BtnGroup = defineAsyncComponent(() => import('@/components/ui/BtnGroup.vue'))
const props = withDefaults(defineProps<{ songs: Song[], selectedSongs: Song[], config: Partial<SongListControlsConfig> }>(), { const props = withDefaults(
songs: () => [], defineProps<{ songs: Song[], selectedSongs: Song[], config: Partial<SongListControlsConfig> }>(),
selectedSongs: () => [], {
config: () => ({}) songs: () => [],
}) selectedSongs: () => [],
config: () => ({})
}
)
const { config, songs, selectedSongs } = toRefs(props) const { config, songs, selectedSongs } = toRefs(props)
const el = ref(null as unknown as HTMLElement) const el = ref<HTMLElement>()
const showingAddToMenu = ref(false) const showingAddToMenu = ref(false)
const numberOfQueuedSongs = ref(0) const numberOfQueuedSongs = ref(0)
const altPressed = ref(false) const altPressed = ref(false)
@ -124,7 +127,7 @@ const mergedConfig = computed((): SongListControlsConfig => Object.assign({
}, },
clearQueue: false, clearQueue: false,
deletePlaylist: false deletePlaylist: false
}, config) }, config.value)
) )
const showClearQueueButton = computed(() => mergedConfig.value.clearQueue) const showClearQueueButton = computed(() => mergedConfig.value.clearQueue)
@ -151,9 +154,9 @@ const toggleAddToMenu = async () => {
await nextTick() await nextTick()
const btnAddTo = el.value.querySelector<HTMLButtonElement>('.btn-add-to')! const btnAddTo = el.value?.querySelector<HTMLButtonElement>('.btn-add-to')!
const { left: btnLeft, bottom: btnBottom, width: btnWidth } = btnAddTo.getBoundingClientRect() const { left: btnLeft, bottom: btnBottom, width: btnWidth } = btnAddTo.getBoundingClientRect()
const contextMenu = el.value.querySelector<HTMLElement>('.add-to')! const contextMenu = el.value?.querySelector<HTMLElement>('.add-to')!
const menuWidth = contextMenu.getBoundingClientRect().width const menuWidth = contextMenu.getBoundingClientRect().width
contextMenu.style.top = `${btnBottom + 10}px` contextMenu.style.top = `${btnBottom + 10}px`
contextMenu.style.left = `${btnLeft + btnWidth / 2 - menuWidth / 2}px` contextMenu.style.left = `${btnLeft + btnWidth / 2 - menuWidth / 2}px`

View file

@ -14,7 +14,7 @@ import { defineAsyncComponent } from 'vue'
const Btn = defineAsyncComponent(() => import('@/components/ui/btn.vue')) const Btn = defineAsyncComponent(() => import('@/components/ui/btn.vue'))
</script> </script>
<style lang="scss" scoped> <style lang="scss">
.btn-group { .btn-group {
display: flex; display: flex;
position: relative; position: relative;

View file

@ -1,20 +1,20 @@
/** /**
* Add necessary functionalities into a view that contains a song-list component. * Add necessary functionalities into a view that contains a song-list component.
*/ */
import { ComponentInternalInstance, getCurrentInstance, reactive, ref, watchEffect } from 'vue' import { ComponentInternalInstance, getCurrentInstance, reactive, Ref, ref, watchEffect } from 'vue'
import isMobile from 'ismobilejs' import isMobile from 'ismobilejs'
import { playback } from '@/services' import { playback } from '@/services'
import { eventBus } from '@/utils' import { eventBus } from '@/utils'
import ControlsToggler from '@/components/ui/screen-controls-toggler.vue' import ControlsToggler from '@/components/ui/ScreenControlsToggler.vue'
import SongList from '@/components/song/list.vue' import SongList from '@/components/song/SongList.vue'
import SongListControls from '@/components/song/list-controls.vue' import SongListControls from '@/components/song/SongListControls.vue'
import { songStore } from '@/stores' import { songStore } from '@/stores'
export const useSongList = (controlsConfig: Partial<SongListControlsConfig> = {}) => { export const useSongList = (songs: Ref<Song[]>, controlsConfig: Partial<SongListControlsConfig> = {}) => {
const songList = ref<InstanceType<typeof SongList>>() const songList = ref<InstanceType<typeof SongList>>()
const state = reactive<SongListState>({ songs: [] }) const vm = getCurrentInstance()
const meta = reactive<SongListMeta>({ const meta = reactive<SongListMeta>({
songCount: 0, songCount: 0,
@ -27,12 +27,12 @@ export const useSongList = (controlsConfig: Partial<SongListControlsConfig> = {}
const isPhone = isMobile.phone const isPhone = isMobile.phone
watchEffect(() => { watchEffect(() => {
if (!state.songs.length) { if (!songs.value.length) {
return return
} }
meta.songCount = state.songs.length meta.songCount = songs.value.length
meta.totalLength = songStore.getFormattedLength(state.songs) meta.totalLength = songStore.getFormattedLength(songs.value)
}) })
const getSongsToPlay = (): Song[] => songList.value.getAllSongsWithSort() const getSongsToPlay = (): Song[] => songList.value.getAllSongsWithSort()
@ -42,7 +42,7 @@ export const useSongList = (controlsConfig: Partial<SongListControlsConfig> = {}
eventBus.on({ eventBus.on({
SET_SELECTED_SONGS (songs: Song[], target: ComponentInternalInstance) { SET_SELECTED_SONGS (songs: Song[], target: ComponentInternalInstance) {
target === getCurrentInstance() && (selectedSongs.value = songs) target === vm && (selectedSongs.value = songs)
} }
}) })
@ -50,8 +50,8 @@ export const useSongList = (controlsConfig: Partial<SongListControlsConfig> = {}
SongList, SongList,
SongListControls, SongListControls,
ControlsToggler, ControlsToggler,
songs,
songList, songList,
state,
meta, meta,
selectedSongs, selectedSongs,
showingControls, showingControls,

View file

@ -1,15 +1,16 @@
import { difference, union } from 'lodash' import { difference, union } from 'lodash'
import { http } from '@/services' import { http } from '@/services'
import { arrayify } from '@/utils' import { arrayify } from '@/utils'
import { reactive } from 'vue'
export const favoriteStore = { export const favoriteStore = {
state: { state: reactive({
songs: [] as Song[], songs: [] as Song[],
length: 0, length: 0,
fmtLength: '' fmtLength: ''
}, }),
get all (): Song[] { get all () {
return this.state.songs return this.state.songs
}, },
@ -17,7 +18,7 @@ export const favoriteStore = {
this.state.songs = value this.state.songs = value
}, },
async toggleOne (song: Song): Promise<void> { async toggleOne (song: Song) {
// Don't wait for the HTTP response to update the status, just toggle right away. // Don't wait for the HTTP response to update the status, just toggle right away.
// This may cause a minor problem if the request fails somehow, but do we care? // This may cause a minor problem if the request fails somehow, but do we care?
song.liked = !song.liked song.liked = !song.liked
@ -26,17 +27,11 @@ export const favoriteStore = {
await http.post<Song>('interaction/like', { song: song.id }) await http.post<Song>('interaction/like', { song: song.id })
}, },
/** add (songs: Song | Song[]) {
* Add a song/songs into the store.
*/
add (songs: Song | Song[]): void {
this.all = union(this.all, arrayify(songs)) this.all = union(this.all, arrayify(songs))
}, },
/** remove (songs: Song | Song[]) {
* Remove a song/songs from the store.
*/
remove (songs: Song | Song[]): void {
this.all = difference(this.all, arrayify(songs)) this.all = difference(this.all, arrayify(songs))
}, },
@ -44,7 +39,7 @@ export const favoriteStore = {
this.all = [] this.all = []
}, },
async like (songs: Song[]): Promise<void> { async like (songs: Song[]) {
// Don't wait for the HTTP response to update the status, just set them to Liked right away. // Don't wait for the HTTP response to update the status, just set them to Liked right away.
// This may cause a minor problem if the request fails somehow, but do we care? // This may cause a minor problem if the request fails somehow, but do we care?
songs.forEach(song => { song.liked = true }) songs.forEach(song => { song.liked = true })
@ -53,7 +48,7 @@ export const favoriteStore = {
await http.post('interaction/batch/like', { songs: songs.map(song => song.id) }) await http.post('interaction/batch/like', { songs: songs.map(song => song.id) })
}, },
async unlike (songs: Song[]): Promise<void> { async unlike (songs: Song[]) {
songs.forEach(song => { song.liked = false }) songs.forEach(song => { song.liked = false })
this.remove(songs) this.remove(songs)

View file

@ -1,25 +1,26 @@
import { songStore } from '.' import { songStore } from '.'
import { http } from '@/services' import { http } from '@/services'
import { remove } from 'lodash' import { remove } from 'lodash'
import { reactive } from 'vue'
const EXCERPT_COUNT = 7 const EXCERPT_COUNT = 7
export const recentlyPlayedStore = { export const recentlyPlayedStore = {
excerptState: { excerptState: reactive({
songs: [] as Song[] songs: [] as Song[]
}, }),
state: { state: reactive({
songs: [] as Song[] songs: [] as Song[]
}, }),
fetched: false, fetched: false,
initExcerpt (songIds: string[]): void { initExcerpt (songIds: string[]) {
this.excerptState.songs = songStore.byIds(songIds) this.excerptState.songs = songStore.byIds(songIds)
}, },
async fetchAll (): Promise<Song[]> { async fetchAll () {
if (!this.fetched) { if (!this.fetched) {
this.state.songs = songStore.byIds(await http.get<string[]>(`interaction/recently-played`)) this.state.songs = songStore.byIds(await http.get<string[]>(`interaction/recently-played`))
this.fetched = true this.fetched = true
@ -28,8 +29,8 @@ export const recentlyPlayedStore = {
return this.state.songs return this.state.songs
}, },
add (song: Song): void { add (song: Song) {
[this.state, this.excerptState].forEach((state): void => { [this.state, this.excerptState].forEach(state => {
// make sure there's no duplicate // make sure there's no duplicate
remove(state.songs, s => s.id === song.id) remove(state.songs, s => s.id === song.id)
state.songs.unshift(song) state.songs.unshift(song)

View file

@ -2,6 +2,7 @@ import { http } from '@/services'
import { songStore } from '@/stores/song' import { songStore } from '@/stores/song'
import { albumStore } from '@/stores/album' import { albumStore } from '@/stores/album'
import { artistStore } from '@/stores/artist' import { artistStore } from '@/stores/artist'
import { reactive } from 'vue'
interface ExcerptSearchResult { interface ExcerptSearchResult {
songs: Array<string> songs: Array<string>
@ -14,27 +15,25 @@ interface SongSearchResult {
} }
export const searchStore = { export const searchStore = {
state: { state: reactive({
excerpt: { excerpt: {
songs: [] as Song[], songs: [] as Song[],
albums: [] as Album[], albums: [] as Album[],
artists: [] as Artist[] artists: [] as Artist[]
}, },
songs: [] as Song[], songs: [] as Song[]
}),
async excerptSearch (q: string) {
const { results } = await http.get<{ [key: string]: ExcerptSearchResult }>(`search?q=${q}`)
this.state.excerpt.songs = songStore.byIds(results.songs)
this.state.excerpt.albums = albumStore.byIds(results.albums)
this.state.excerpt.artists = artistStore.byIds(results.artists)
}, },
excerptSearch (q: string) { async songSearch (q: string) {
http.get<{ [key: string]: ExcerptSearchResult }>(`search?q=${q}`).then(({ results }) => { const { songs } = await http.get<SongSearchResult>(`search/songs?q=${q}`)
this.state.excerpt.songs = songStore.byIds(results.songs) this.state.songs = this.state.songs.concat(songStore.byIds(songs))
this.state.excerpt.albums = albumStore.byIds(results.albums)
this.state.excerpt.artists = artistStore.byIds(results.artists)
})
},
songSearch (q: string) {
http.get<SongSearchResult>(`search/songs?q=${q}`).then(({ songs }) => {
this.state.songs = this.state.songs.concat(songStore.byIds(songs))
})
}, },
resetSongResultState () { resetSongResultState () {