show albums from artist at the bottom of album page

+ add a testing genres strip in album page
+ misc refactors
This commit is contained in:
geoffrey45 2022-11-24 12:01:39 +03:00 committed by Mungai Njoroge
parent da852e72f3
commit 4a49d48011
14 changed files with 211 additions and 24 deletions

View File

@ -0,0 +1,51 @@
<template>
<div class="albums-from-artist">
<h3>
<span>More from {{ artist.artist }} </span>
<span class="see-more">SEE ALL</span>
</h3>
<div class="cards">
<AlbumCard v-for="a in artist.albums" :album="a" />
</div>
</div>
</template>
<script setup lang="ts">
import AlbumCard from "../shared/AlbumCard.vue";
import useAlbumStore from "@/stores/pages/album";
import { content_width } from "@/stores/content-width";
import { computed, onBeforeMount } from "vue";
import { AlbumInfo } from "@/interfaces";
defineProps<{
artist: {
artist: string;
albums: AlbumInfo[];
};
}>();
</script>
<style lang="scss">
.albums-from-artist {
overflow: hidden;
padding-top: 1rem;
h3 {
display: grid;
grid-template-columns: 1fr max-content;
align-items: center;
padding: 0 $medium;
.see-more {
font-size: $medium;
}
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
gap: 2rem 0
}
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<div class="genres-banner">
<div v-for="genre in genres" class="rounded pad-sm">{{ genre }}</div>
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import useAlbumStore from "@/stores/pages/album";
const genres = ["Genres", "RNB", "Alternative", "Genres", "RNB", "Alternative"];
onMounted(() => {
// onMounted, fetch data to be used in the component below this one.
const album = useAlbumStore();
album.fetchArtistAlbums();
});
</script>
<style lang="scss">
.genres-banner {
display: flex;
gap: 1rem;
margin-top: 1rem;
font-size: 0.9rem;
padding-left: $medium;
text-transform: capitalize;
user-select: none;
div {
background-color: $gray5;
min-width: 4rem;
text-align: center;
outline: solid 1px $gray3;
padding: $small 1rem;
&:first-child {
background-color: white;
color: black;
outline-color: white;
pointer-events: none;
}
&:hover {
background-color: $darkestblue;
outline-color: $darkestblue;
}
}
}
</style>

View File

@ -49,6 +49,7 @@
</div>
</div>
</div>
</template>
<script setup lang="ts">
@ -59,7 +60,7 @@ import { albumHeaderSmall } from "@/stores/content-width";
import useNavStore from "@/stores/nav";
import useAlbumStore from "@/stores/pages/album";
import { formatSeconds, useVisibility } from "@/utils";
import { getButtonColor, isLight } from "../../composables/colors/album";
import { isLight } from "../../composables/colors/album";
import { playSources } from "../../composables/enums";
import { AlbumInfo } from "../../interfaces";
@ -73,6 +74,7 @@ const albumheaderthing = ref<any>(null);
const imguri = paths.images;
const nav = useNavStore();
/**
* Calls the `toggleShowPlay` method which toggles the play button in the nav.
* Emits the `resetBottomPadding` event to reset the album page content bottom padding.
@ -87,6 +89,8 @@ useVisibility(albumheaderthing, handleVisibilityState);
</script>
<style lang="scss">
.a-header {
display: grid;
grid-template-columns: max-content 1fr;

View File

@ -40,7 +40,7 @@ function getSource() {
location: {
name: Routes.album,
params: {
hash: source.hash,
hash: source.albumhash,
},
},
};

View File

@ -2,7 +2,7 @@
<router-link
:to="{
name: 'AlbumView',
params: { hash: album.hash },
params: { hash: album.albumhash },
}"
class="result-item"
>

View File

@ -149,6 +149,8 @@ function showMenu(e: MouseEvent) {
width: 100%;
position: relative;
height: 3rem;
display: flex;
justify-content: center;
.text {
opacity: 1;

View File

@ -1,14 +1,14 @@
import { paths } from "@/config";
import state from "../state";
import { NotifType, useNotifStore } from "@/stores/notification";
import { AlbumInfo, Track } from "../../interfaces";
import useAxios from "./useAxios";
import { NotifType, useNotifStore } from "@/stores/notification";
const {
album: albumUrl,
albumartists: albumArtistsUrl,
albumbio: albumBioUrl
} = paths.api
albumbio: albumBioUrl,
albumsByArtistUrl,
} = paths.api;
const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => {
interface AlbumData {
@ -66,4 +66,30 @@ const getAlbumBio = async (hash: string) => {
}
};
export { getAlbumData as getAlbumTracks, getAlbumArtists, getAlbumBio };
const getAlbumsFromArtist = async (
albumartist: string,
limit: number = 2,
exclude: string
) => {
const { data } = await useAxios({
url: albumsByArtistUrl,
props: {
albumartist: albumartist,
limit: limit,
exclude: exclude,
},
});
if (data) {
return data.data;
}
return [];
};
export {
getAlbumData as getAlbumTracks,
getAlbumArtists,
getAlbumBio,
getAlbumsFromArtist,
};

View File

@ -34,7 +34,7 @@ export default function play(
useQueue.playFromAlbum(
a_store.info.title,
a_store.info.albumartist,
a_store.info.hash,
a_store.info.albumhash,
a_store.allTracks
);
useQueue.play();

View File

@ -44,6 +44,9 @@ const paths = {
get albumbio() {
return this.album + "/bio";
},
get albumsByArtistUrl(){
return this.album + "/from-artist"
},
folder: baseApiUrl + "/folder",
playlist: {
base: baseApiUrl + "/playlist",
@ -85,4 +88,4 @@ const paths = {
},
};
export { paths, toggleMode };
export { paths, toggleMode };

View File

@ -43,7 +43,7 @@ export interface AlbumInfo {
date: string;
image: string;
artistimg: string;
hash: string;
albumhash: string;
colors: string[];
copyright?: string;
@ -91,7 +91,7 @@ export interface fromFolder {
export interface fromAlbum {
type: FromOptions.album;
name: string;
hash: string;
albumhash: string;
albumartist: string;
}
export interface fromPlaylist {

View File

@ -4,8 +4,12 @@ import { ComputedRef } from "vue";
import { AlbumDisc } from "./../../interfaces";
import { FuseTrackOptions } from "@/composables/enums";
import { content_width } from "@/stores/content-width";
import { getAlbumTracks } from "../../composables/fetch/album";
import {
getAlbumsFromArtist,
getAlbumTracks,
} from "../../composables/fetch/album";
import { AlbumInfo, Artist, FuseResult, Track } from "../../interfaces";
import { useNotifStore } from "../notification";
@ -49,6 +53,7 @@ export default defineStore("album", {
info: <AlbumInfo>{},
rawTracks: <Track[]>[],
artists: <Artist[]>[],
albumArtists: <{ artist: string; albums: AlbumInfo[] }[]>[],
bio: null,
}),
actions: {
@ -62,9 +67,23 @@ export default defineStore("album", {
this.rawTracks = album.tracks;
this.info = album.info;
},
async fetchArtistAlbums() {
const albumartist = this.info.albumartist;
const cardWidth = 8 * 16;
const visible_cards = Math.floor(content_width.value / cardWidth);
this.albumArtists = await getAlbumsFromArtist(
albumartist,
visible_cards,
this.info.albumhash
);
},
resetQuery() {
this.query = "";
},
resetAlbumArtists() {
this.albumArtists = [];
},
},
getters: {
discs(): Disc {

View File

@ -161,7 +161,7 @@ export default defineStore("Queue", {
this.from = <fromAlbum>{
type: FromOptions.album,
name: aname,
hash: albumhash,
albumhash: albumhash,
albumartist: albumartist,
};

View File

@ -10,6 +10,7 @@
<component
:is="item.component"
v-bind="item.props"
:style="{ maxHeight: `${item.size}px` }"
@playThis="
playFromAlbum(item.props.track.index - item.props.track.disc)
"
@ -33,8 +34,11 @@ import useAlbumStore from "@/stores/pages/album";
import useQueueStore from "@/stores/queue";
import AlbumDiscBar from "@/components/AlbumView/AlbumDiscBar.vue";
import ArtistAlbums from "@/components/AlbumView/ArtistAlbums.vue";
import GenreBanner from "@/components/AlbumView/GenreBanner.vue";
import Header from "@/components/AlbumView/Header.vue";
import SongItem from "@/components/shared/SongItem.vue";
import { isSmall } from "@/stores/content-width";
const album = useAlbumStore();
@ -42,8 +46,12 @@ const queue = useQueueStore();
interface ScrollerItem {
id: string;
component: typeof Header | typeof SongItem;
props: any;
component:
| typeof Header
| typeof SongItem
| typeof GenreBanner
| typeof ArtistAlbums;
props?: any;
size: number;
}
@ -62,12 +70,29 @@ class songItem {
}
}
const genreBanner: ScrollerItem = {
id: "genre-banner",
component: GenreBanner,
size: 64,
};
function getSongItems() {
return album.tracks.map((track) => {
return new songItem(track);
});
}
function getArtistAlbumComponents(): ScrollerItem[] {
return album.albumArtists.map((artist) => {
return {
id: Math.random().toString(),
component: ArtistAlbums,
props: { artist },
size: 20 * 16,
};
});
}
const scrollerItems = computed(() => {
const header: ScrollerItem = {
id: "album-header",
@ -75,22 +100,29 @@ const scrollerItems = computed(() => {
props: {
album: album.info,
},
size: 18 * 16,
size: 19 * 16,
};
return [header, ...getSongItems()];
return [
header,
...getSongItems(),
genreBanner,
...getArtistAlbumComponents(),
];
});
function playFromAlbum(index: number) {
const { title, artist, hash } = album.info;
queue.playFromAlbum(title, artist, hash, album.allTracks);
const { title, albumartist, albumhash } = album.info;
queue.playFromAlbum(title, albumartist, albumhash, album.allTracks);
queue.play(index);
}
onBeforeRouteUpdate(async (to: RouteLocationNormalized) => {
await album
.fetchTracksAndArtists(to.params.hash.toString())
.then(() => album.resetQuery());
await album.fetchTracksAndArtists(to.params.hash.toString()).then(() => {
album.resetQuery();
album.resetAlbumArtists();
album.fetchArtistAlbums();
});
});
onBeforeRouteLeave(() => {

View File

@ -2,7 +2,7 @@
<div class="search-albums-view grid-page" v-auto-animate>
<AlbumCard
v-for="album in search.albums.value"
:key="album.hash"
:key="album.albumhash"
:album="album"
/>
</div>