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 SongList from '@/components/song/list.vue'
import { download, albumInfo as albumInfoService, playback } from '@/services'
import SongList from '@/components/song/SongList.vue'
import { albumInfo as albumInfoService, download } from '@/services'
import factory from '@/__tests__/factory'
import { mock } from '@/__tests__/__helpers__'
import { mount, shallow } from '@/__tests__/adapter'

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
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 { eventBus } from '@/utils'
import { playlistStore } from '@/stores'

View file

@ -1,5 +1,5 @@
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 { queueStore, songStore } from '@/stores'
import { playback } from '@/services'

View file

@ -1,5 +1,5 @@
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 { recentlyPlayedStore } from '@/stores'
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 { take } from 'lodash'
import { shallow, mount } from '@/__tests__/adapter'

View file

@ -1,5 +1,5 @@
import router from '@/router'
import Component from '@/components/song/list.vue'
import Component from '@/components/song/SongList.vue'
import factory from '@/__tests__/factory'
import { queueStore } from '@/stores'
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 { 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 AlbumListScreen from '@/components/screens/album-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 FavoritesScreen from '@/components/screens/favorites.vue'

View file

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

View file

@ -20,7 +20,7 @@ import { eventBus, limitBy } from '@/utils'
import { albumStore, preferenceStore as preferences } from '@/stores'
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 ViewModeSwitch = defineAsyncComponent(() => import('@/components/ui/view-mode-switch.vue'))

View file

@ -9,22 +9,22 @@
</template>
<template v-slot:meta>
<span v-if="album.songs.length">
<span v-if="songs.length">
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>
{{ pluralize(album.songs.length, 'song') }}
{{ pluralize(songs.length, 'song') }}
{{ fmtLength }}
<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 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
</a>
</template>
@ -33,19 +33,19 @@
<template v-slot:controls>
<SongListControls
v-if="album.songs.length && (!isPhone || showingControls)"
@playAll="playAll"
@playSelected="playSelected"
:songs="album.songs"
v-if="songs.length && (!isPhone || showingControls)"
:config="songListControlConfig"
:selectedSongs="selectedSongs"
:songs="songs"
@playAll="playAll"
@playSelected="playSelected"
/>
</template>
</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"/>
<div class="inner">
<div class="loading" v-if="loading">
@ -65,16 +65,20 @@ import { albumInfo as albumInfoService, download as downloadService } from '@/se
import router from '@/router'
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 SoundBar = defineAsyncComponent(() => import('@/components/ui/sound-bar.vue'))
const AlbumThumbnail = defineAsyncComponent(() => import('@/components/ui/AlbumArtistThumbnail.vue'))
const CloseModalBtn = defineAsyncComponent(() => import('@/components/ui/close-modal-btn.vue'))
const props = defineProps<{ album: Album }>()
const { album } = toRefs(props)
const {
SongList,
SongListControls,
ControlsToggler,
songs,
songList,
selectedSongs,
showingControls,
@ -83,10 +87,7 @@ const {
playAll,
playSelected,
toggleControls
} = useSongList()
const props = defineProps<{ album: Album }>()
const { album } = toRefs(props)
} = useSongList(ref(album.value.songs))
const { length, fmtLength } = useAlbumAttributes(album.value)

View file

@ -28,7 +28,7 @@ const {
makeScrollable
} = 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 ViewModeSwitch = defineAsyncComponent(() => import('@/components/ui/view-mode-switch.vue'))

View file

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

View file

@ -9,9 +9,9 @@
{{ pluralize(meta.songCount, 'song') }}
{{ 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
</a>
</template>
@ -20,17 +20,17 @@
<template v-slot:controls>
<SongListControls
v-if="state.songs.length && (!isPhone || showingControls)"
@playAll="playAll"
@playSelected="playSelected"
:songs="state.songs"
v-if="songs.length && (!isPhone || showingControls)"
:config="songListControlConfig"
:selectedSongs="selectedSongs"
:songs="songs"
@playAll="playAll"
@playSelected="playSelected"
/>
</template>
</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>
<template v-slot:icon>
@ -38,8 +38,8 @@
</template>
No favorites yet.
<span class="secondary d-block">
Click the
<i class="fa fa-heart-o"></i>
Click the&nbsp;
<i class="fa fa-heart-o"></i>&nbsp;
icon to mark a song as favorite.
</span>
</ScreenPlaceholder>
@ -51,15 +51,16 @@ import { pluralize } from '@/utils'
import { favoriteStore, sharedStore } from '@/stores'
import { download as downloadService } from '@/services'
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 {
SongList,
SongListControls,
ControlsToggler,
songs,
songList,
meta,
selectedSongs,
@ -69,10 +70,9 @@ const {
playAll,
playSelected,
toggleControls
} = useSongList()
} = useSongList(toRef(favoriteStore.state, 'songs'))
const state = reactive(favoriteStore.state)
const sharedState = reactive(sharedStore.state)
const allowDownload = toRef(sharedStore.state, 'allowDownload')
const download = () => downloadService.fromFavorites()
</script>

View file

@ -89,7 +89,7 @@ import router from '@/router'
import { useInfiniteScroll } from '@/composables'
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 ArtistCard = defineAsyncComponent(() => import('@/components/artist/card.vue'))
const SongCard = defineAsyncComponent(() => import('@/components/song/card.vue'))

View file

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

View file

@ -14,7 +14,7 @@
<script lang="ts" setup>
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 LastfmIntegration = defineAsyncComponent(() => import('@/components/profile-preferences/lastfm-integration.vue'))
const Preferences = defineAsyncComponent(() => import('@/components/profile-preferences/preferences.vue'))

View file

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

View file

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

View file

@ -65,7 +65,7 @@ import { eventBus } from '@/utils'
import { searchStore } from '@/stores'
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 SongCard = defineAsyncComponent(() => import('@/components/song/card.vue'))
const ArtistCard = defineAsyncComponent(() => import('@/components/artist/card.vue'))

View file

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

View file

@ -67,10 +67,10 @@ import { UploadFile, validMediaMimeTypes } from '@/config'
import { upload } from '@/services'
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'
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 mediaPath = toRef(settingStore.state, 'media_path')

View file

@ -29,10 +29,10 @@ import isMobile from 'ismobilejs'
import { userStore } from '@/stores'
import { eventBus } from '@/utils'
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/screen-header.vue'))
const ControlsToggler = defineAsyncComponent(() => import('@/components/ui/screen-controls-toggler.vue'))
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ControlsToggler = defineAsyncComponent(() => import('@/components/ui/ScreenControlsToggler.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 state = reactive(userStore.state)

View file

@ -25,7 +25,7 @@ import createYouTubePlayer from 'youtube-player'
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 title = ref('YouTube Video')

View file

@ -72,7 +72,6 @@ import {
getCurrentInstance,
nextTick,
onMounted,
PropType,
ref,
toRefs,
watch
@ -182,7 +181,8 @@ const 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 = () => {
if (!selectedSongs.value.length) {

View file

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

View file

@ -1,20 +1,20 @@
/**
* 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 { playback } from '@/services'
import { eventBus } from '@/utils'
import ControlsToggler from '@/components/ui/screen-controls-toggler.vue'
import SongList from '@/components/song/list.vue'
import SongListControls from '@/components/song/list-controls.vue'
import ControlsToggler from '@/components/ui/ScreenControlsToggler.vue'
import SongList from '@/components/song/SongList.vue'
import SongListControls from '@/components/song/SongListControls.vue'
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 state = reactive<SongListState>({ songs: [] })
const vm = getCurrentInstance()
const meta = reactive<SongListMeta>({
songCount: 0,
@ -27,12 +27,12 @@ export const useSongList = (controlsConfig: Partial<SongListControlsConfig> = {}
const isPhone = isMobile.phone
watchEffect(() => {
if (!state.songs.length) {
if (!songs.value.length) {
return
}
meta.songCount = state.songs.length
meta.totalLength = songStore.getFormattedLength(state.songs)
meta.songCount = songs.value.length
meta.totalLength = songStore.getFormattedLength(songs.value)
})
const getSongsToPlay = (): Song[] => songList.value.getAllSongsWithSort()
@ -42,7 +42,7 @@ export const useSongList = (controlsConfig: Partial<SongListControlsConfig> = {}
eventBus.on({
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,
SongListControls,
ControlsToggler,
songs,
songList,
state,
meta,
selectedSongs,
showingControls,

View file

@ -1,15 +1,16 @@
import { difference, union } from 'lodash'
import { http } from '@/services'
import { arrayify } from '@/utils'
import { reactive } from 'vue'
export const favoriteStore = {
state: {
state: reactive({
songs: [] as Song[],
length: 0,
fmtLength: ''
},
}),
get all (): Song[] {
get all () {
return this.state.songs
},
@ -17,7 +18,7 @@ export const favoriteStore = {
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.
// This may cause a minor problem if the request fails somehow, but do we care?
song.liked = !song.liked
@ -26,17 +27,11 @@ export const favoriteStore = {
await http.post<Song>('interaction/like', { song: song.id })
},
/**
* Add a song/songs into the store.
*/
add (songs: Song | Song[]): void {
add (songs: Song | Song[]) {
this.all = union(this.all, arrayify(songs))
},
/**
* Remove a song/songs from the store.
*/
remove (songs: Song | Song[]): void {
remove (songs: Song | Song[]) {
this.all = difference(this.all, arrayify(songs))
},
@ -44,7 +39,7 @@ export const favoriteStore = {
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.
// This may cause a minor problem if the request fails somehow, but do we care?
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) })
},
async unlike (songs: Song[]): Promise<void> {
async unlike (songs: Song[]) {
songs.forEach(song => { song.liked = false })
this.remove(songs)

View file

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

View file

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