From 2951fa3ddbd29759d98be532bf27bc01788c4b7b Mon Sep 17 00:00:00 2001 From: Phan An Date: Sat, 30 Jul 2022 17:08:20 +0200 Subject: [PATCH] feat: add loading skeletons --- .../js/components/screens/AlbumListScreen.vue | 23 ++-- .../js/components/screens/AlbumScreen.vue | 46 +++++--- .../js/components/screens/AllSongsScreen.vue | 12 ++- .../components/screens/ArtistListScreen.vue | 23 ++-- .../js/components/screens/ArtistScreen.vue | 46 +++++--- .../js/components/screens/FavoritesScreen.vue | 7 +- .../js/components/screens/PlaylistScreen.vue | 4 +- .../js/components/screens/QueueScreen.vue | 7 +- .../screens/RecentlyPlayedScreen.vue | 7 +- .../screens/home/MostPlayedAlbums.vue | 19 +++- .../screens/home/MostPlayedArtists.vue | 18 +++- .../screens/home/MostPlayedSongs.vue | 17 ++- .../screens/home/RecentlyAddedAlbums.vue | 19 +++- .../screens/home/RecentlyAddedSongs.vue | 17 ++- .../screens/home/RecentlyPlayedSongs.vue | 20 ++-- .../assets/js/components/song/SongCard.vue | 48 ++++----- .../assets/js/components/ui/ScreenHeader.vue | 12 +-- .../ui/skeletons/ArtistAlbumCardSkeleton.vue | 65 +++++++++++ .../ui/skeletons/ScreenHeaderSkeleton.vue | 53 +++++++++ .../ui/skeletons/SongCardSkeleton.vue | 51 +++++++++ .../ui/skeletons/SongListSkeleton.vue | 102 ++++++++++++++++++ resources/assets/js/router.ts | 6 +- resources/assets/js/stores/overviewStore.ts | 6 +- resources/assets/js/stores/songStore.ts | 16 +-- resources/assets/sass/app.scss | 2 +- resources/assets/sass/partials/_skeleton.scss | 16 +++ 26 files changed, 545 insertions(+), 117 deletions(-) create mode 100644 resources/assets/js/components/ui/skeletons/ArtistAlbumCardSkeleton.vue create mode 100644 resources/assets/js/components/ui/skeletons/ScreenHeaderSkeleton.vue create mode 100644 resources/assets/js/components/ui/skeletons/SongCardSkeleton.vue create mode 100644 resources/assets/js/components/ui/skeletons/SongListSkeleton.vue create mode 100644 resources/assets/sass/partials/_skeleton.scss diff --git a/resources/assets/js/components/screens/AlbumListScreen.vue b/resources/assets/js/components/screens/AlbumListScreen.vue index e84443f6..f0db1ce3 100644 --- a/resources/assets/js/components/screens/AlbumListScreen.vue +++ b/resources/assets/js/components/screens/AlbumListScreen.vue @@ -14,8 +14,13 @@ data-testid="album-list" @scroll="scrolling" > - - + + @@ -27,6 +32,7 @@ import { albumStore, preferenceStore as preferences } from '@/stores' import { useInfiniteScroll } from '@/composables' import AlbumCard from '@/components/album/AlbumCard.vue' +import AlbumCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue' import ScreenHeader from '@/components/ui/ScreenHeader.vue' import ViewModeSwitch from '@/components/ui/ViewModeSwitch.vue' @@ -40,21 +46,22 @@ const { makeScrollable } = useInfiniteScroll(async () => await fetchAlbums()) -const itemLayout = computed(() => viewMode.value === 'thumbnails' ? 'full' : 'compact') - watch(viewMode, () => (preferences.albumsViewMode = viewMode.value)) let initialized = false -let loading = false +const loading = ref(false) const page = ref(1) + +const itemLayout = computed(() => viewMode.value === 'thumbnails' ? 'full' : 'compact') const moreAlbumsAvailable = computed(() => page.value !== null) +const showSkeletons = computed(() => loading.value && albums.value.length === 0) const fetchAlbums = async () => { - if (loading || !moreAlbumsAvailable.value) return + if (loading.value || !moreAlbumsAvailable.value) return - loading = true + loading.value = true page.value = await albumStore.paginate(page.value!) - loading = false + loading.value = false } eventBus.on('LOAD_MAIN_CONTENT', async (view: MainViewName) => { diff --git a/resources/assets/js/components/screens/AlbumScreen.vue b/resources/assets/js/components/screens/AlbumScreen.vue index c32785e5..febd9a84 100644 --- a/resources/assets/js/components/screens/AlbumScreen.vue +++ b/resources/assets/js/components/screens/AlbumScreen.vue @@ -1,6 +1,8 @@ - + + -
+
@@ -48,29 +51,36 @@ diff --git a/resources/assets/js/components/screens/AllSongsScreen.vue b/resources/assets/js/components/screens/AllSongsScreen.vue index 9a71364b..788390f8 100644 --- a/resources/assets/js/components/screens/AllSongsScreen.vue +++ b/resources/assets/js/components/screens/AllSongsScreen.vue @@ -22,7 +22,9 @@ + secondsToHis(commonStore.state.song_length)) @@ -64,12 +67,13 @@ const { } = useSongList(toRef(songStore.state, 'songs'), 'all-songs') let initialized = false -let loading = false +const loading = ref(false) let sortField: SongListSortField = 'title' // @todo get from query string let sortOrder: SortOrder = 'asc' const page = ref(1) const moreSongsAvailable = computed(() => page.value !== null) +const showSkeletons = computed(() => loading.value && songs.value.length === 0) const sort = async (field: SongListSortField, order: SortOrder) => { page.value = 1 @@ -81,11 +85,11 @@ const sort = async (field: SongListSortField, order: SortOrder) => { } const fetchSongs = async () => { - if (!moreSongsAvailable.value || loading) return + if (!moreSongsAvailable.value || loading.value) return - loading = true + loading.value = true page.value = await songStore.paginate(sortField, sortOrder, page.value!) - loading = false + loading.value = false } const playAll = async (shuffle: boolean) => { diff --git a/resources/assets/js/components/screens/ArtistListScreen.vue b/resources/assets/js/components/screens/ArtistListScreen.vue index 32a80d7e..25fbd522 100644 --- a/resources/assets/js/components/screens/ArtistListScreen.vue +++ b/resources/assets/js/components/screens/ArtistListScreen.vue @@ -14,8 +14,13 @@ data-testid="artist-list" @scroll="scrolling" > - - + +
@@ -27,6 +32,7 @@ import { artistStore, preferenceStore as preferences } from '@/stores' import { useInfiniteScroll } from '@/composables' import ArtistCard from '@/components/artist/ArtistCard.vue' +import ArtistCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue' import ScreenHeader from '@/components/ui/ScreenHeader.vue' import ViewModeSwitch from '@/components/ui/ViewModeSwitch.vue' @@ -40,21 +46,22 @@ const { makeScrollable } = useInfiniteScroll(async () => await fetchArtists()) -const itemLayout = computed(() => viewMode.value === 'thumbnails' ? 'full' : 'compact') - watch(viewMode, () => preferences.artistsViewMode = viewMode.value) let initialized = false -let loading = false +const loading = ref(false) const page = ref(1) + +const itemLayout = computed(() => viewMode.value === 'thumbnails' ? 'full' : 'compact') const moreArtistsAvailable = computed(() => page.value !== null) +const showSkeletons = computed(() => loading.value && artists.value.length === 0) const fetchArtists = async () => { - if (loading || !moreArtistsAvailable.value) return + if (loading.value || !moreArtistsAvailable.value) return - loading = true + loading.value = true page.value = await artistStore.paginate(page.value!) - loading = false + loading.value = false } eventBus.on('LOAD_MAIN_CONTENT', async (view: MainViewName) => { diff --git a/resources/assets/js/components/screens/ArtistScreen.vue b/resources/assets/js/components/screens/ArtistScreen.vue index 2e19b91f..eb5ea99c 100644 --- a/resources/assets/js/components/screens/ArtistScreen.vue +++ b/resources/assets/js/components/screens/ArtistScreen.vue @@ -1,6 +1,8 @@ - + + -
+
@@ -48,19 +51,27 @@ diff --git a/resources/assets/js/components/screens/FavoritesScreen.vue b/resources/assets/js/components/screens/FavoritesScreen.vue index 5decf0d8..4c81cf34 100644 --- a/resources/assets/js/components/screens/FavoritesScreen.vue +++ b/resources/assets/js/components/screens/FavoritesScreen.vue @@ -33,6 +33,7 @@ + downloadService.fromFavorites() const removeSelected = () => selectedSongs.value.length && favoriteStore.unlike(selectedSongs.value) let initialized = false +const loading = ref(false) const fetchSongs = async () => { + loading.value = true await favoriteStore.fetch() + loading.value = false await nextTick() sort() } diff --git a/resources/assets/js/components/screens/PlaylistScreen.vue b/resources/assets/js/components/screens/PlaylistScreen.vue index bc3fa117..72ce7be3 100644 --- a/resources/assets/js/components/screens/PlaylistScreen.vue +++ b/resources/assets/js/components/screens/PlaylistScreen.vue @@ -33,8 +33,9 @@ + () diff --git a/resources/assets/js/components/screens/QueueScreen.vue b/resources/assets/js/components/screens/QueueScreen.vue index 61fd3ffc..7b8ff2f4 100644 --- a/resources/assets/js/components/screens/QueueScreen.vue +++ b/resources/assets/js/components/screens/QueueScreen.vue @@ -24,6 +24,7 @@ + import { faCoffee } from '@fortawesome/free-solid-svg-icons' -import { computed, toRef } from 'vue' +import { computed, ref, toRef } from 'vue' import { logger, pluralize, requireInjection } from '@/utils' import { commonStore, queueStore } from '@/stores' import { playbackService } from '@/services' @@ -58,6 +59,7 @@ import { DialogBoxKey } from '@/symbols' import ScreenHeader from '@/components/ui/ScreenHeader.vue' import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue' +import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue' const dialog = requireInjection(DialogBoxKey) const controlConfig: Partial = { clearQueue: true } @@ -80,13 +82,16 @@ const { onScrollBreakpoint } = useSongList(toRef(queueStore.state, 'songs'), 'queue', { sortable: false }) +const fetchingRandomSongs = ref(false) const libraryNotEmpty = computed(() => commonStore.state.song_count > 0) const playAll = (shuffle = true) => playbackService.queueAndPlay(songs.value, shuffle) const shuffleSome = async () => { try { + fetchingRandomSongs.value = true await queueStore.fetchRandom() + fetchingRandomSongs.value = false await playbackService.playFirstInQueue() } catch (e) { dialog.value.error('Failed to fetch songs to play. Please try again.', 'Error') diff --git a/resources/assets/js/components/screens/RecentlyPlayedScreen.vue b/resources/assets/js/components/screens/RecentlyPlayedScreen.vue index 9b41516e..946c1624 100644 --- a/resources/assets/js/components/screens/RecentlyPlayedScreen.vue +++ b/resources/assets/js/components/screens/RecentlyPlayedScreen.vue @@ -22,6 +22,7 @@ + @@ -39,10 +40,11 @@ import { faClock } from '@fortawesome/free-regular-svg-icons' import { eventBus, pluralize } from '@/utils' import { recentlyPlayedStore } from '@/stores' import { useSongList } from '@/composables' -import { toRef } from 'vue' +import { ref, toRef } from 'vue' import ScreenHeader from '@/components/ui/ScreenHeader.vue' import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue' +import SongListSkeleton from '@/components/ui/skeletons/SongListSkeleton.vue' const recentlyPlayedSongs = toRef(recentlyPlayedStore.state, 'songs') @@ -66,11 +68,14 @@ const { } = useSongList(recentlyPlayedSongs, 'recently-played', { sortable: false }) let initialized = false +let loading = ref(false) eventBus.on({ 'LOAD_MAIN_CONTENT': async (view: MainViewName) => { if (view === 'RecentlyPlayed' && !initialized) { + loading.value = true await recentlyPlayedStore.fetch() + loading.value = false initialized = true } } diff --git a/resources/assets/js/components/screens/home/MostPlayedAlbums.vue b/resources/assets/js/components/screens/home/MostPlayedAlbums.vue index c0ac0170..c11c4de5 100644 --- a/resources/assets/js/components/screens/home/MostPlayedAlbums.vue +++ b/resources/assets/js/components/screens/home/MostPlayedAlbums.vue @@ -1,19 +1,30 @@ diff --git a/resources/assets/js/components/screens/home/MostPlayedArtists.vue b/resources/assets/js/components/screens/home/MostPlayedArtists.vue index 5b574a6b..14e6f034 100644 --- a/resources/assets/js/components/screens/home/MostPlayedArtists.vue +++ b/resources/assets/js/components/screens/home/MostPlayedArtists.vue @@ -1,12 +1,20 @@ @@ -15,6 +23,8 @@ import { toRef } from 'vue' import { overviewStore } from '@/stores' import ArtistCard from '@/components/artist/ArtistCard.vue' +import ArtistCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue' const artists = toRef(overviewStore.state, 'mostPlayedArtists') +const loading = toRef(overviewStore.state, 'loading') diff --git a/resources/assets/js/components/screens/home/MostPlayedSongs.vue b/resources/assets/js/components/screens/home/MostPlayedSongs.vue index 0100f5b7..738977b3 100644 --- a/resources/assets/js/components/screens/home/MostPlayedSongs.vue +++ b/resources/assets/js/components/screens/home/MostPlayedSongs.vue @@ -1,12 +1,19 @@ @@ -15,6 +22,8 @@ import { toRef } from 'vue' import { overviewStore } from '@/stores' import SongCard from '@/components/song/SongCard.vue' +import SongCardSkeleton from '@/components/ui/skeletons/SongCardSkeleton.vue' const songs = toRef(overviewStore.state, 'mostPlayedSongs') +const loading = toRef(overviewStore.state, 'loading') diff --git a/resources/assets/js/components/screens/home/RecentlyAddedAlbums.vue b/resources/assets/js/components/screens/home/RecentlyAddedAlbums.vue index af2de20e..91cc6671 100644 --- a/resources/assets/js/components/screens/home/RecentlyAddedAlbums.vue +++ b/resources/assets/js/components/screens/home/RecentlyAddedAlbums.vue @@ -1,13 +1,20 @@ @@ -16,6 +23,8 @@ import { toRef } from 'vue' import { overviewStore } from '@/stores' import AlbumCard from '@/components/album/AlbumCard.vue' +import AlbumCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue' const albums = toRef(overviewStore.state, 'recentlyAddedAlbums') +const loading = toRef(overviewStore.state, 'loading') diff --git a/resources/assets/js/components/screens/home/RecentlyAddedSongs.vue b/resources/assets/js/components/screens/home/RecentlyAddedSongs.vue index f2893b63..95ff0b71 100644 --- a/resources/assets/js/components/screens/home/RecentlyAddedSongs.vue +++ b/resources/assets/js/components/screens/home/RecentlyAddedSongs.vue @@ -1,12 +1,19 @@ @@ -15,6 +22,8 @@ import { toRef } from 'vue' import { overviewStore } from '@/stores' import SongCard from '@/components/song/SongCard.vue' +import SongCardSkeleton from '@/components/ui/skeletons/SongCardSkeleton.vue' const songs = toRef(overviewStore.state, 'recentlyAddedSongs') +const loading = toRef(overviewStore.state, 'loading') diff --git a/resources/assets/js/components/screens/home/RecentlyPlayedSongs.vue b/resources/assets/js/components/screens/home/RecentlyPlayedSongs.vue index 8e39299e..5bd9031f 100644 --- a/resources/assets/js/components/screens/home/RecentlyPlayedSongs.vue +++ b/resources/assets/js/components/screens/home/RecentlyPlayedSongs.vue @@ -14,25 +14,33 @@ -
    -
  1. - +
      +
    1. +
    - -

    No songs played as of late.

    +
diff --git a/resources/assets/js/components/song/SongCard.vue b/resources/assets/js/components/song/SongCard.vue index 253d565f..bb17fcf0 100644 --- a/resources/assets/js/components/song/SongCard.vue +++ b/resources/assets/js/components/song/SongCard.vue @@ -8,40 +8,35 @@ @contextmenu.prevent="requestContextMenu" @dblclick.prevent="play" > - + +
+
+

{{ song.title }}

+

{{ song.artist_name }} - - - - - - - + - {{ pluralize(song.play_count, 'play') }} +

+
+ +
+ + diff --git a/resources/assets/js/components/ui/skeletons/ScreenHeaderSkeleton.vue b/resources/assets/js/components/ui/skeletons/ScreenHeaderSkeleton.vue new file mode 100644 index 00000000..6562e867 --- /dev/null +++ b/resources/assets/js/components/ui/skeletons/ScreenHeaderSkeleton.vue @@ -0,0 +1,53 @@ + + + diff --git a/resources/assets/js/components/ui/skeletons/SongCardSkeleton.vue b/resources/assets/js/components/ui/skeletons/SongCardSkeleton.vue new file mode 100644 index 00000000..dbb19bc0 --- /dev/null +++ b/resources/assets/js/components/ui/skeletons/SongCardSkeleton.vue @@ -0,0 +1,51 @@ + + + diff --git a/resources/assets/js/components/ui/skeletons/SongListSkeleton.vue b/resources/assets/js/components/ui/skeletons/SongListSkeleton.vue new file mode 100644 index 00000000..93a7f495 --- /dev/null +++ b/resources/assets/js/components/ui/skeletons/SongListSkeleton.vue @@ -0,0 +1,102 @@ + + + diff --git a/resources/assets/js/router.ts b/resources/assets/js/router.ts index e7585fe5..730fb726 100644 --- a/resources/assets/js/router.ts +++ b/resources/assets/js/router.ts @@ -1,6 +1,6 @@ import isMobile from 'ismobilejs' import { loadMainView, use } from '@/utils' -import { albumStore, artistStore, playlistStore, queueStore, songStore, userStore } from '@/stores' +import { playlistStore, queueStore, songStore, userStore } from '@/stores' import { playbackService } from '@/services' class Router { @@ -23,8 +23,8 @@ class Router { '/youtube': () => loadMainView('YouTube'), '/visualizer': () => loadMainView('Visualizer'), '/profile': () => loadMainView('Profile'), - '/album/(\\d+)': async (id: number) => loadMainView('Album', await albumStore.resolve(id)), - '/artist/(\\d+)': async (id: number) => loadMainView('Artist', await artistStore.resolve(id)), + '/album/(\\d+)': async (id: string) => loadMainView('Album', parseInt(id)), + '/artist/(\\d+)': async (id: string) => loadMainView('Artist', parseInt(id)), '/playlist/(\\d+)': (id: number) => use(playlistStore.byId(~~id), playlist => loadMainView('Playlist', playlist)), '/song/([a-z0-9]{32})': async (id: string) => { const song = await songStore.resolve(id) diff --git a/resources/assets/js/stores/overviewStore.ts b/resources/assets/js/stores/overviewStore.ts index ae0c1fc4..eea482d9 100644 --- a/resources/assets/js/stores/overviewStore.ts +++ b/resources/assets/js/stores/overviewStore.ts @@ -12,10 +12,13 @@ export const overviewStore = { recentlyAddedAlbums: [], mostPlayedSongs: [], mostPlayedAlbums: [], - mostPlayedArtists: [] + mostPlayedArtists: [], + loading: false }), async init () { + this.state.loading = true + const resource = await httpService.get<{ most_played_songs: Song[], most_played_albums: Album[], @@ -32,6 +35,7 @@ export const overviewStore = { recentlyPlayedStore.excerptState.songs = songStore.syncWithVault(resource.recently_played_songs) this.refresh() + this.state.loading = false }, refresh () { diff --git a/resources/assets/js/stores/songStore.ts b/resources/assets/js/stores/songStore.ts index 53235570..a97e158e 100644 --- a/resources/assets/js/stores/songStore.ts +++ b/resources/assets/js/stores/songStore.ts @@ -154,17 +154,21 @@ export const songStore = { }) }, - async fetchForAlbum (album: Album) { + async fetchForAlbum (album: Album | number) { + const id = typeof album === 'number' ? album : album.id + return await cache.remember( - [`album.songs`, album.id], - async () => this.syncWithVault(await httpService.get(`albums/${album.id}/songs`)) + [`album.songs`, id], + async () => this.syncWithVault(await httpService.get(`albums/${id}/songs`)) ) }, - async fetchForArtist (artist: Artist) { + async fetchForArtist (artist: Artist | number) { + const id = typeof artist === 'number' ? artist : artist.id + return await cache.remember( - ['artist.songs', artist.id], - async () => this.syncWithVault(await httpService.get(`artists/${artist.id}/songs`)) + ['artist.songs', id], + async () => this.syncWithVault(await httpService.get(`artists/${id}/songs`)) ) }, diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index 095c68c2..2a0836f8 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -9,6 +9,6 @@ @import "#/vendor/_plyr.scss"; @import "#/vendor/_nprogress.scss"; -@import "#/vendor/_alertify.scss"; +@import "#/partials/_skeleton.scss"; @import "#/partials/_shared.scss"; diff --git a/resources/assets/sass/partials/_skeleton.scss b/resources/assets/sass/partials/_skeleton.scss new file mode 100644 index 00000000..38ff3326 --- /dev/null +++ b/resources/assets/sass/partials/_skeleton.scss @@ -0,0 +1,16 @@ +.skeleton { + .pulse { + animation: skeleton-pulse 2s infinite; + background-color: rgba(255, 255, 255, .05); + } + + @keyframes skeleton-pulse { + 0%, 100% { + opacity: 0; + } + + 50% { + opacity: 1; + } + } +}