connect favorites data to favorites page

+ detach isSmall and isMedium classes from the v-scroll-page class
+ customize the TopTracks component to be usable with the favorite tracks page
+ add queue methods to play tracks from favorites page
+ handle playing from artist top tracks in parent component
This commit is contained in:
geoffrey45
2022-12-28 14:49:02 +03:00
committed by Mungai Njoroge
parent 62fb70d26c
commit 905fff04b4
16 changed files with 254 additions and 204 deletions
+2 -2
View File
@@ -110,7 +110,7 @@ $g-border: solid 1px $gray5;
} }
} }
.v-scroll-page.isSmall { .isSmall {
.songlist-item { .songlist-item {
grid-template-columns: 1.75rem 2fr 2.5rem 2.5rem; grid-template-columns: 1.75rem 2fr 2.5rem 2.5rem;
} }
@@ -128,7 +128,7 @@ $g-border: solid 1px $gray5;
} }
} }
.v-scroll-page.isMedium { .isMedium {
// hide album column // hide album column
.songlist-item { .songlist-item {
grid-template-columns: 1.75rem 1.5fr 1fr 2.5rem 2.5rem; grid-template-columns: 1.75rem 1.5fr 1fr 2.5rem 2.5rem;
+10 -17
View File
@@ -3,17 +3,13 @@
<h3> <h3>
<span>{{ title }} </span> <span>{{ title }} </span>
<span <span
class="see-more" class="see-all"
v-if="maxAbumCards <= albums.length" v-if="maxAbumCards <= albums.length"
@click="store.setPage(albumType)" @click="
> !favorites ? useArtistDiscographyStore().setPage(albumType) : null
<RouterLink "
:to="{
name: Routes.artistDiscography,
params: { hash: artisthash },
}"
>SEE ALL</RouterLink
> >
<RouterLink :to="route">SEE ALL</RouterLink>
</span> </span>
</h3> </h3>
<div class="cards"> <div class="cards">
@@ -23,22 +19,19 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import AlbumCard from "../shared/AlbumCard.vue";
import { Album } from "@/interfaces"; import { Album } from "@/interfaces";
import { maxAbumCards } from "@/stores/content-width"; import { maxAbumCards } from "@/stores/content-width";
import { Routes } from "@/router/routes";
import { discographyAlbumTypes } from "@/composables/enums"; import { discographyAlbumTypes } from "@/composables/enums";
import useArtistDiscographyStore from "@/stores/pages/artistDiscog"; import useArtistDiscographyStore from "@/stores/pages/artistDiscog";
const store = useArtistDiscographyStore(); import AlbumCard from "../shared/AlbumCard.vue";
defineProps<{ defineProps<{
title: string; title: string;
artisthash: string;
albums: Album[]; albums: Album[];
albumType: discographyAlbumTypes; albumType?: discographyAlbumTypes;
favorites?: boolean;
route: string;
}>(); }>();
</script> </script>
@@ -53,7 +46,7 @@ defineProps<{
padding: 0 $medium; padding: 0 $medium;
margin-bottom: $small; margin-bottom: $small;
.see-more { .see-all {
font-size: $medium; font-size: $medium;
a:hover { a:hover {
+17 -41
View File
@@ -1,59 +1,35 @@
<template> <template>
<div class="artist-top-tracks"> <div class="artist-top-tracks">
<h3 class="section-title"> <h3 class="section-title">
Tracks {{ title }}
<span class="see-more"> <span class="see-all">
<RouterLink
:to="{ <RouterLink :to="route">SEE ALL</RouterLink>
name: Routes.artistTracks,
params: {
hash: artist.info.artisthash,
},
query: {
artist: artist.info.name,
},
}"
>SEE ALL</RouterLink
>
</span> </span>
</h3> </h3>
<div class="tracks"> <div class="tracks" :class="{ isSmall, isMedium }">
<SongItem <SongItem
v-for="(song, index) in artist.tracks" v-for="(song, index) in tracks"
:track="song" :track="song"
:index="index + 1" :index="index + 1"
@playThis="playFromPage(index)" @playThis="playHandler(index)"
/> />
</div> </div>
<div class="error" v-if="!artist.tracks.length">No tracks</div> <div class="error" v-if="!tracks.length">No tracks</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import SongItem from "../shared/SongItem.vue"; import SongItem from "../shared/SongItem.vue";
import useArtistPageStore from "@/stores/pages/artist"; import { Track } from "@/interfaces";
import useQueueStore from "@/stores/queue"; import { isMedium, isSmall } from "@/stores/content-width";
import { FromOptions, playSources } from "@/composables/enums";
import { getArtistTracks } from "@/composables/fetch/artists"; defineProps<{
import { Routes } from "@/router/routes"; tracks: Track[];
route: string;
const artist = useArtistPageStore(); title: string;
const queue = useQueueStore(); playHandler: (index: number) => void;
}>();
async function playFromPage(index: number) {
if (
queue.from.type == FromOptions.artist &&
queue.from.artisthash == artist.info.artisthash
) {
queue.play(index);
return;
}
const tracks = await getArtistTracks(artist.info.artisthash);
queue.playFromArtist(artist.info.artisthash, artist.info.name, tracks);
queue.play(index);
}
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -74,7 +50,7 @@ async function playFromPage(index: number) {
justify-content: space-between; justify-content: space-between;
} }
.see-more { .see-all {
font-size: $medium; font-size: $medium;
a:hover { a:hover {
@@ -0,0 +1,42 @@
<template>
<div class="f-artists">
<h3>{{ title }} <span class="see-all">SEE ALL</span></h3>
<div class="artist-list">
<ArtistCard
v-for="artist in artists.slice(0, maxAbumCards)"
:key="artist.image"
:artist="artist"
/>
</div>
</div>
</template>
<script setup lang="ts">
import ArtistCard from "@/components/shared/ArtistCard.vue";
import { Artist } from "@/interfaces";
import { maxAbumCards } from "@/stores/content-width";
defineProps<{
artists: Artist[];
title: string;
}>();
</script>
<style lang="scss">
.f-artists {
h3 {
display: flex;
justify-content: space-between;
padding-left: $medium;
.see-all {
font-size: $medium;
}
}
.artist-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
}
}
</style>
@@ -1,112 +0,0 @@
<template>
<div class="f-artists">
<div class="header">
<div class="headin">Featured Artists</div>
<div class="xcontrols">
<div class="prev icon" @click="scrollLeft()"><ArrowSvg /></div>
<div class="next icon" @click="scrollRight()"><ArrowSvg /></div>
</div>
</div>
<div class="separator no-border"></div>
<div class="artists" ref="artists_dom">
<ArtistCard
v-for="artist in artists"
:key="artist.image"
:artist="artist"
:color="'ffffff00'"
/>
</div>
</div>
</template>
<script setup lang="ts">
import ArtistCard from "@/components/shared/ArtistCard.vue";
import { Artist } from "@/interfaces";
import { ref } from "@vue/reactivity";
import ArrowSvg from "../../assets/icons/right-arrow.svg";
defineProps<{
artists: Artist[];
}>();
const artists_dom = ref(null);
const scrollLeft = () => {
const dom = artists_dom.value;
dom.scrollBy({
left: -700,
behavior: "smooth",
});
};
const scrollRight = () => {
const dom = artists_dom.value;
dom.scrollBy({
left: 700,
behavior: "smooth",
});
};
</script>
<style lang="scss">
.f-artists {
width: 100%;
padding: 0 $small;
border-radius: $small;
user-select: none;
position: relative;
.header {
display: flex;
align-items: center;
position: relative;
.headin {
font-size: 1.5rem;
font-weight: 900;
margin-left: $small;
}
}
}
.f-artists .xcontrols {
z-index: 1;
position: absolute;
top: 0;
right: 0;
display: flex;
gap: 1rem;
.prev {
transform: rotate(180deg);
}
.icon {
border-radius: $small;
transition: all 0.5s ease;
background-color: rgb(51, 51, 51);
padding: $smaller;
svg {
display: flex;
}
&:hover {
background-color: $accent;
transition: all 0.5s ease;
}
}
}
.f-artists > .artists {
display: flex;
align-items: flex-end;
flex-wrap: nowrap;
overflow-x: scroll;
gap: $small;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
</style>
+10
View File
@@ -27,6 +27,7 @@ import SearchSvg from "@/assets/icons/search.svg";
import ArtistSvg from "@/assets/icons/artist.svg"; import ArtistSvg from "@/assets/icons/artist.svg";
import { RouteLocationRaw } from "vue-router"; import { RouteLocationRaw } from "vue-router";
import HeartSvg from "@/assets/icons/heart.fill.svg";
const queue = useQueueStore(); const queue = useQueueStore();
@@ -95,6 +96,15 @@ function getSource() {
}, },
}; };
case FromOptions.favorite:
return {
name: "Favorite tracks",
icon: HeartSvg,
location: {
name: Routes.favorites,
},
};
default: default:
return { name: "👻 No source", location: {} }; return { name: "👻 No source", location: {} };
} }
+2 -12
View File
@@ -7,16 +7,8 @@
}, },
}" }"
> >
<div <div class="artist-card">
class="artist-card" <img class="artist-image circular" :src="imguri + artist.image" />
:class="{ _is_on_sidebar: alt }"
:style="{ backgroundColor: `${artist.colors[0]}` }"
>
<img
class="artist-image circular"
:src="imguri + artist.image"
loading="lazy"
/>
<div class="artist-name t-center"> <div class="artist-name t-center">
{{ artist.name }} {{ artist.name }}
</div> </div>
@@ -33,7 +25,6 @@ const imguri = paths.images.artist.large;
defineProps<{ defineProps<{
artist: Artist; artist: Artist;
alt?: boolean;
}>(); }>();
</script> </script>
@@ -58,7 +49,6 @@ defineProps<{
.artist-image { .artist-image {
width: 100%; width: 100%;
// margin-bottom: $small;
transition: all 0.5s ease-in-out; transition: all 0.5s ease-in-out;
object-fit: cover; object-fit: cover;
} }
+2 -1
View File
@@ -4,7 +4,7 @@ export enum playSources {
search, search,
folder, folder,
artist, artist,
albumCard, favorite,
} }
export enum NotifType { export enum NotifType {
@@ -22,6 +22,7 @@ export enum FromOptions {
search = "search", search = "search",
artist = "artist", artist = "artist",
albumCard = "albumCard", albumCard = "albumCard",
favorite = "favorite",
} }
export enum ContextSrc { export enum ContextSrc {
+1
View File
@@ -8,6 +8,7 @@ const {
albumartists: albumArtistsUrl, albumartists: albumArtistsUrl,
albumbio: albumBioUrl, albumbio: albumBioUrl,
albumsByArtistUrl, albumsByArtistUrl,
favAlbums,
} = paths.api; } = paths.api;
const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => { const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => {
+28
View File
@@ -3,6 +3,7 @@ import { paths } from "@/config";
import { favType, NotifType } from "@/composables/enums"; import { favType, NotifType } from "@/composables/enums";
import { useNotifStore as notif } from "@/stores/notification"; import { useNotifStore as notif } from "@/stores/notification";
import { Album, Artist, Track } from "@/interfaces";
export async function addFavorite(favtype: favType, itemhash: string) { export async function addFavorite(favtype: favType, itemhash: string) {
const { data, error } = await useAxios({ const { data, error } = await useAxios({
@@ -45,3 +46,30 @@ export async function removeFavorite(favtype: favType, itemhash: string) {
return true; return true;
} }
export async function getFavAlbums(limit = 6) {
const { data } = await useAxios({
url: paths.api.favAlbums + `?limit=${limit}`,
get: true,
});
return data.albums as Album[];
}
export async function getFavTracks(limit = 5) {
const { data } = await useAxios({
url: paths.api.favTracks + `?limit=${limit}`,
get: true,
});
return data.tracks as Track[];
}
export async function getFavArtists(limit = 6) {
const { data } = await useAxios({
url: paths.api.favArtists + `?limit=${limit}`,
get: true,
});
return data.artists as Artist[];
}
+3
View File
@@ -35,6 +35,9 @@ const baseImgUrl = baseApiUrl + "/img";
const paths = { const paths = {
api: { api: {
album: baseApiUrl + "/album", album: baseApiUrl + "/album",
favAlbums: baseApiUrl + "/albums/favorite",
favTracks: baseApiUrl + "/tracks/favorite",
favArtists: baseApiUrl + "/artists/favorite",
artist: baseApiUrl + "/artist", artist: baseApiUrl + "/artist",
favorite: baseApiUrl + "/favorite", favorite: baseApiUrl + "/favorite",
get removeFavorite() { get removeFavorite() {
+4
View File
@@ -121,6 +121,10 @@ export interface fromArtist {
artistname: string; artistname: string;
} }
export interface fromFav {
type: FromOptions.favorite;
}
export interface subPath { export interface subPath {
name: string; name: string;
path: string; path: string;
+2 -1
View File
@@ -39,7 +39,8 @@ export default defineStore("artistDiscography", {
); );
} }
}, },
setPage(page: discographyAlbumTypes) { setPage(page: discographyAlbumTypes | undefined) {
// @ts-ignore
this.page = page; this.page = page;
}, },
fetchAlbums(artisthash: string) { fetchAlbums(artisthash: string) {
+15 -1
View File
@@ -9,6 +9,7 @@ import updateMediaNotif from "../composables/mediaNotification";
import { import {
fromAlbum, fromAlbum,
fromArtist, fromArtist,
fromFav,
fromFolder, fromFolder,
fromPlaylist, fromPlaylist,
fromSearch, fromSearch,
@@ -24,7 +25,13 @@ function shuffle(tracks: Track[]) {
return shuffled; return shuffled;
} }
type From = fromFolder | fromAlbum | fromPlaylist | fromSearch | fromArtist; type From =
| fromFolder
| fromAlbum
| fromPlaylist
| fromSearch
| fromArtist
| fromFav;
let audio = new Audio(); let audio = new Audio();
audio.autoplay = false; audio.autoplay = false;
@@ -189,6 +196,13 @@ export default defineStore("Queue", {
this.setNewQueue(tracks); this.setNewQueue(tracks);
}, },
playFromFav(tracks: Track[]) {
this.from = <fromFav>{
type: FromOptions.favorite,
};
this.setNewQueue(tracks);
},
addTrackToQueue(track: Track) { addTrackToQueue(track: Track) {
this.tracklist.push(track); this.tracklist.push(track);
}, },
+34 -14
View File
@@ -1,9 +1,5 @@
<template> <template>
<div <div class="artist-page v-scroll-page" style="height: 100%">
class="artist-page v-scroll-page"
style="height: 100%"
:class="{ isSmall, isMedium }"
>
<DynamicScroller <DynamicScroller
:items="scrollerItems" :items="scrollerItems"
:min-item-size="64" :min-item-size="64"
@@ -29,8 +25,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { isMedium, isSmall } from "@/stores/content-width";
import Header from "@/components/ArtistView/Header.vue"; import Header from "@/components/ArtistView/Header.vue";
import TopTracks from "@/components/ArtistView/TopTracks.vue"; import TopTracks from "@/components/ArtistView/TopTracks.vue";
import useArtistPageStore from "@/stores/pages/artist"; import useArtistPageStore from "@/stores/pages/artist";
@@ -39,9 +33,12 @@ import ArtistAlbumsFetcher from "@/components/ArtistView/ArtistAlbumsFetcher.vue
import { computed } from "vue"; import { computed } from "vue";
import { onBeforeRouteLeave, onBeforeRouteUpdate, useRoute } from "vue-router"; import { onBeforeRouteLeave, onBeforeRouteUpdate, useRoute } from "vue-router";
import { Album } from "@/interfaces"; import { Album } from "@/interfaces";
import { discographyAlbumTypes } from "@/composables/enums"; import { discographyAlbumTypes, FromOptions } from "@/composables/enums";
import useQueueStore from "@/stores/queue";
import { getArtistTracks } from "@/composables/fetch/artists";
const store = useArtistPageStore(); const store = useArtistPageStore();
const queue = useQueueStore();
const route = useRoute(); const route = useRoute();
interface ScrollerItem { interface ScrollerItem {
@@ -55,11 +52,6 @@ const header: ScrollerItem = {
component: Header, component: Header,
}; };
const top_tracks: ScrollerItem = {
id: "artist-top-tracks",
component: TopTracks,
};
const artist_albums_fetcher: ScrollerItem = { const artist_albums_fetcher: ScrollerItem = {
id: "artist-albums-fetcher", id: "artist-albums-fetcher",
component: ArtistAlbumsFetcher, component: ArtistAlbumsFetcher,
@@ -100,6 +92,20 @@ function createAbumComponent(title: AlbumType, albums: Album[]) {
albums, albums,
title, title,
artisthash: route.params.hash, artisthash: route.params.hash,
route: `/artists/${store.info.artisthash}/discography?artist=${store.info.name}`,
},
};
}
function getTopTracksComponent(): ScrollerItem {
return {
id: "artist-top-tracks",
component: TopTracks,
props: {
tracks: store.tracks,
title: "Tracks",
route: `/artists/${store.info.artisthash}/tracks?artist=${store.info.name}`,
playHandler: handlePlay,
}, },
}; };
} }
@@ -108,7 +114,7 @@ const scrollerItems = computed(() => {
let components = [header]; let components = [header];
if (store.tracks.length > 0) { if (store.tracks.length > 0) {
components.push(top_tracks); components.push(getTopTracksComponent());
} }
components = [...components, artist_albums_fetcher]; components = [...components, artist_albums_fetcher];
@@ -139,6 +145,20 @@ const scrollerItems = computed(() => {
return components; return components;
}); });
async function handlePlay(index: number) {
if (
queue.from.type == FromOptions.artist &&
queue.from.artisthash == store.info.artisthash
) {
queue.play(index);
return;
}
const tracks = await getArtistTracks(store.info.artisthash);
queue.playFromArtist(store.info.artisthash, store.info.name, tracks);
queue.play(index);
}
onBeforeRouteUpdate(async (to) => { onBeforeRouteUpdate(async (to) => {
await store.getData(to.params.hash as string); await store.getData(to.params.hash as string);
}); });
+82 -3
View File
@@ -6,13 +6,66 @@
<div class="artists">Artists</div> <div class="artists">Artists</div>
<div class="folders">Folders</div> <div class="folders">Folders</div>
</div> </div>
<div class="fav-tracks">
<TopTracks
:tracks="favTracks"
:route="'/home'"
:title="'Favorite tracks'"
:playHandler="handlePlay"
/>
</div>
<div class="fav-albums">
<ArtistAlbums
:albums="favAlbums"
:albumType="discographyAlbumTypes.albums"
:title="'Favorite albums'"
:route="'some'"
/>
</div>
<div class="fav-artists">
<FeaturedArtists :artists="favArtists" :title="'Favorite artists'" />
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import AlbumSvg from "@/assets/icons/album.svg"; import { onMounted, Ref, ref } from "vue";
import ArtistSvg from "@/assets/icons/artist.svg";
import TrackSvg from "@/assets/icons/artist.svg"; import ArtistAlbums from "@/components/AlbumView/ArtistAlbums.vue";
import TopTracks from "@/components/ArtistView/TopTracks.vue";
import FeaturedArtists from "@/components/PlaylistView/ArtistsList.vue";
import { discographyAlbumTypes } from "@/composables/enums";
import {
getFavAlbums,
getFavArtists,
getFavTracks,
} from "@/composables/fetch/favorite";
import { Album, Artist, Track } from "@/interfaces";
import useQueueStore from "@/stores/queue";
import { maxAbumCards } from "@/stores/content-width";
const queue = useQueueStore();
const favAlbums: Ref<Album[]> = ref([]);
const favTracks: Ref<Track[]> = ref([]);
const favArtists: Ref<Artist[]> = ref([]);
onMounted(() => {
getFavTracks().then((tracks) => (favTracks.value = tracks));
getFavAlbums(maxAbumCards.value).then((albums) => (favAlbums.value = albums));
getFavArtists(maxAbumCards.value).then(
(artists) => (favArtists.value = artists)
);
});
async function handlePlay(index: number) {
console.log(index);
const tracks = await getFavTracks(0);
queue.playFromFav(tracks);
queue.play(index);
}
</script> </script>
<style lang="scss"> <style lang="scss">
@@ -23,6 +76,7 @@ $artistsbg: rgb(0, 255, 21);
.favorites-page { .favorites-page {
height: 100%; height: 100%;
overflow: scroll; overflow: scroll;
padding-bottom: 4rem;
.header > * { .header > * {
padding: 1rem; padding: 1rem;
@@ -54,5 +108,30 @@ $artistsbg: rgb(0, 255, 21);
background-color: $gray2; background-color: $gray2;
} }
} }
.fav-tracks {
h3 {
padding-left: 2rem;
display: flex;
justify-content: space-between;
.see-all {
font-size: $medium;
}
}
margin: 1rem 0;
}
.fav-albums {
// margin-top: 3rem;
.album-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
}
}
.fav-artists {
margin-top: 3rem;
}
} }
</style> </style>