build artist page

+ connect artist page to backend
~ bugs introduced as there are hashing changes in the backend

[will fix later]
This commit is contained in:
geoffrey45 2022-12-03 16:06:26 +03:00 committed by Mungai Njoroge
parent fff2c53801
commit 075765088f
17 changed files with 382 additions and 82 deletions

View File

@ -15,7 +15,7 @@ $larger: 2rem;
$banner-height: 18rem; $banner-height: 18rem;
$song-item-height: 4rem; $song-item-height: 4rem;
$content-padding-bottom: 4rem; $content-padding-bottom: 2rem;
// apple human design guideline colors // apple human design guideline colors
$black: #181a1c; $black: #181a1c;

View File

@ -1,11 +1,11 @@
<template> <template>
<div class="albums-from-artist"> <div class="albums-from-artist">
<h3> <h3>
<span>More from {{ artist.artist }} </span> <span>{{ title }} </span>
<span class="see-more">SEE ALL</span> <span class="see-more">SEE ALL</span>
</h3> </h3>
<div class="cards"> <div class="cards">
<AlbumCard v-for="a in artist.albums" :album="a" /> <AlbumCard v-for="a in albums" :album="a" />
</div> </div>
</div> </div>
</template> </template>
@ -13,26 +13,25 @@
<script setup lang="ts"> <script setup lang="ts">
import AlbumCard from "../shared/AlbumCard.vue"; import AlbumCard from "../shared/AlbumCard.vue";
import { AlbumInfo } from "@/interfaces"; import { Album } from "@/interfaces";
defineProps<{ defineProps<{
artist: { title: string;
artist: string; albums: Album[];
albums: AlbumInfo[];
};
}>(); }>();
</script> </script>
<style lang="scss"> <style lang="scss">
.albums-from-artist { .albums-from-artist {
overflow: hidden; overflow: hidden;
padding-top: 2rem; padding-top: 1rem;
h3 { h3 {
display: grid; display: grid;
grid-template-columns: 1fr max-content; grid-template-columns: 1fr max-content;
align-items: center; align-items: center;
padding: 0 $medium; padding: 0 $medium;
margin-bottom: $small;
.see-more { .see-more {
font-size: $medium; font-size: $medium;

View File

@ -53,14 +53,21 @@
</div> </div>
</div> </div>
<div class="art" v-if="!albumHeaderSmall"> <div class="art" v-if="!albumHeaderSmall">
<img <RouterLink
v-for="a in album.albumartists" v-for="a in album.albumartists"
:to="{
name: Routes.artist,
params: { hash: a.hash },
}"
>
<img
:src="imguri.artist + a.image" :src="imguri.artist + a.image"
class="shadow-lg circular" class="shadow-lg circular"
loading="lazy" loading="lazy"
:title="a.name" :title="a.name"
:style="{ border: `solid 2px ${album.colors[0]}` }" :style="{ border: `solid 2px ${album.colors[0]}` }"
/> />
</RouterLink>
</div> </div>
</div> </div>
</div> </div>
@ -69,20 +76,20 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import ArtistName from "@/components/shared/ArtistName.vue";
import { paths } from "@/config"; import { paths } from "@/config";
import { albumHeaderSmall } from "@/stores/content-width"; import { albumHeaderSmall } from "@/stores/content-width";
import useNavStore from "@/stores/nav"; import useNavStore from "@/stores/nav";
import useAlbumStore from "@/stores/pages/album"; import useAlbumStore from "@/stores/pages/album";
import { formatSeconds, useVisibility } from "@/utils"; import { formatSeconds, useVisibility } from "@/utils";
import { isLight } from "../../composables/colors/album"; import { isLight } from "../../composables/colors/album";
import { playSources } from "../../composables/enums"; import { playSources, Routes } from "../../composables/enums";
import { AlbumInfo } from "../../interfaces"; import { Album } from "../../interfaces";
import ArtistName from "@/components/shared/ArtistName.vue";
import PlayBtnRect from "../shared/PlayBtnRect.vue"; import PlayBtnRect from "../shared/PlayBtnRect.vue";
const props = defineProps<{ const props = defineProps<{
album: AlbumInfo; album: Album;
}>(); }>();
const albumheaderthing = ref<any>(null); const albumheaderthing = ref<any>(null);

View File

@ -0,0 +1,53 @@
<template>
<div class="albums-list">
<div class="section-title">
<b>
{{ title }}
</b>
<div class="see-all"><b>SEE ALL</b></div>
</div>
<div class="cars">
<AlbumCard
v-for="album in search.albums.value.slice(0, 6)"
:album="album"
:key="Math.random()"
/>
</div>
</div>
</template>
<script setup lang="ts">
import AlbumCard from "../shared/AlbumCard.vue";
import useSearchStore from "@/stores/search";
defineProps<{
// albums: Album[];
title: string;
}>();
const search = useSearchStore();
// TODO: use AlbumView's ArtistAlbums component instead of this
</script>
<style lang="scss">
.albums-list {
.section-title {
margin: 0;
margin-bottom: -$small;
.see-all {
float: right;
font-size: small;
opacity: 0.5;
}
}
.cars {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
// padding: 1rem 0;
// gap: 2rem 0;
}
}
</style>

View File

@ -1,9 +1,119 @@
<template> <template>
<div class="artist-page-header"> <div class="artist-page-header rounded no-scroll">
This is the header <div
class="artist-info"
:class="{
nocontrast: artist.info.colors ? isLight(artist.info.colors[0]) : false,
}"
>
<section class="text">
<div class="card-title">ARTIST</div>
<div class="artist-name">{{ artist.info.name }}</div>
<div class="stats">
{{ artist.info.trackcount }} Tracks
{{ artist.info.albumcount }} Albums
{{ formatSeconds(artist.info.duration, true) }}
</div>
</section>
<PlayBtnRect />
</div>
<div class="artist-img">
<img :src="paths.images.artist + artist.info.image" />
</div>
<div
class="gradient"
:style="{
backgroundImage: `linear-gradient(to left, transparent 10%,
${artist.info.colors[0]} 50%,
${artist.info.colors[0]} 100%)`,
}"
></div>
</div> </div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import useArtistPageStore from "@/stores/pages/artist";
import PlayBtnRect from "../shared/PlayBtnRect.vue";
import formatSeconds from "@/utils/useFormatSeconds";
import { isLight } from "@/composables/colors/album";
import { paths } from "@/config";
<style lang="scss"></style> const artist = useArtistPageStore();
</script>
<style lang="scss">
.artist-page-header {
height: 18rem;
display: grid;
grid-template-columns: 1fr 1fr;
position: relative;
.artist-img {
height: 18rem;
img {
height: 100%;
width: 100%;
object-fit: cover;
object-position: 0% 20%;
}
}
.gradient {
position: absolute;
background-image: linear-gradient(
to left,
transparent 10%,
#434142 50%,
#434142 100%
);
height: 100%;
width: 100%;
}
.artist-info {
z-index: 1;
padding: 1rem;
padding-right: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
gap: 1rem;
.text {
display: flex;
flex-direction: column;
gap: $small;
}
.card-title {
opacity: 0.5;
font-size: small;
}
.artist-name {
font-size: 3rem;
font-weight: bold;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.stats {
font-size: small;
}
.playbtnrect {
border-radius: 2rem;
}
}
.artist-info.nocontrast {
color: $black;
}
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="artist-top-tracks">
<h3 class="section-title">Tracks</h3>
<div class="tracks">
<SongItem
v-for="(song, index) in artist.tracks"
:track="song"
:index="index + 1"
:isCurrent="false"
:isCurrentPlaying="false"
/>
</div>
</div>
</template>
<script setup lang="ts">
import useQueueStore from "@/stores/queue";
import SongItem from "../shared/SongItem.vue";
import useArtistPageStore from "@/stores/pages/artist";
const queue = useQueueStore();
const artist = useArtistPageStore();
</script>
<style lang="scss">
.artist-top-tracks {
// padding-bottom: 2rem;
.section-title {
margin-left: 0;
}
}
</style>

View File

@ -16,11 +16,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { paths } from "../../config"; import { paths } from "../../config";
import { AlbumInfo } from "../../interfaces"; import { Album } from "../../interfaces";
const imguri = paths.images.thumb.large; const imguri = paths.images.thumb.large;
defineProps<{ defineProps<{
album: AlbumInfo; album: Album;
}>(); }>();
</script> </script>

View File

@ -31,6 +31,7 @@ export enum Routes {
playlists = "PlaylistList", playlists = "PlaylistList",
playlist = "PlaylistView", playlist = "PlaylistView",
albums = "AlbumsView", albums = "AlbumsView",
artist = "ArtistView",
album = "AlbumView", album = "AlbumView",
artists = "ArtistsView", artists = "ArtistsView",
settings = "SettingsView", settings = "SettingsView",

View File

@ -1,6 +1,6 @@
import { paths } from "@/config"; import { paths } from "@/config";
import { NotifType, useNotifStore } from "@/stores/notification"; import { NotifType, useNotifStore } from "@/stores/notification";
import { AlbumInfo, Track } from "../../interfaces"; import { Album, Track } from "../../interfaces";
import useAxios from "./useAxios"; import useAxios from "./useAxios";
const { const {
@ -12,7 +12,7 @@ const {
const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => { const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => {
interface AlbumData { interface AlbumData {
info: AlbumInfo; info: Album;
tracks: Track[]; tracks: Track[];
} }
@ -26,7 +26,7 @@ const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => {
if (status == 204) { if (status == 204) {
ToastStore().showNotification("Album not created yet!", NotifType.Error); ToastStore().showNotification("Album not created yet!", NotifType.Error);
return { return {
info: {} as AlbumInfo, info: {} as Album,
tracks: [], tracks: [],
}; };
} }

View File

@ -0,0 +1,26 @@
import { paths } from "@/config";
import useAxios from "./useAxios";
import { Artist, Track, Album } from "@/interfaces";
const getArtistData = async (hash: string) => {
interface ArtistData {
artist: Artist;
albums: Album[];
tracks: Track[];
}
const { data, error } = await useAxios({
get: true,
url: paths.api.artist + `/${hash}`,
});
if (error) {
console.error(error);
}
console.log(data);
return data as ArtistData;
};
export { getArtistData };

View File

@ -38,6 +38,7 @@ const baseApiUrl = domain() + ports.api;
const paths = { const paths = {
api: { api: {
album: baseApiUrl + "/album", album: baseApiUrl + "/album",
artist: baseApiUrl + "/artist",
get albumartists() { get albumartists() {
return this.album + "/artists"; return this.album + "/artists";
}, },
@ -45,7 +46,7 @@ const paths = {
return this.album + "/bio"; return this.album + "/bio";
}, },
get albumsByArtistUrl() { get albumsByArtistUrl() {
return this.album + "/from-artist" return this.album + "/from-artist";
}, },
folder: baseApiUrl + "/folder", folder: baseApiUrl + "/folder",
playlist: { playlist: {

View File

@ -34,7 +34,7 @@ export interface Folder {
is_sym: boolean; is_sym: boolean;
} }
export interface AlbumInfo { export interface Album {
albumid: string; albumid: string;
title: string; title: string;
albumartists: { albumartists: {
@ -61,6 +61,11 @@ export interface AlbumInfo {
export interface Artist { export interface Artist {
name: string; name: string;
image: string; image: string;
artisthash: string;
trackcount: number;
albumcount: number;
duration: number;
colors: string[];
} }
export interface Option { export interface Option {

View File

@ -1,8 +1,9 @@
import state from "@/composables/state"; import state from "@/composables/state";
import useAStore from "@/stores/pages/album"; import useAlbumPageStore from "@/stores/pages/album";
import useFStore from "@/stores/pages/folder"; import useFolderPageStore from "@/stores/pages/folder";
import usePTrackStore from "@/stores/pages/playlist"; import usePlaylistPageStore from "@/stores/pages/playlist";
import usePStore from "@/stores/pages/playlists"; import usePlaylistListPageStore from "@/stores/pages/playlists";
import useArtistPageStore from "@/stores/pages/artist";
const routes = [ const routes = [
{ {
@ -16,7 +17,7 @@ const routes = [
component: () => import("@/views/FolderView.vue"), component: () => import("@/views/FolderView.vue"),
beforeEnter: async (to: any) => { beforeEnter: async (to: any) => {
state.loading.value = true; state.loading.value = true;
await useFStore() await useFolderPageStore()
.fetchAll(to.params.path) .fetchAll(to.params.path)
.then(() => { .then(() => {
state.loading.value = false; state.loading.value = false;
@ -29,7 +30,7 @@ const routes = [
component: () => import("@/views/PlaylistList.vue"), component: () => import("@/views/PlaylistList.vue"),
beforeEnter: async () => { beforeEnter: async () => {
state.loading.value = true; state.loading.value = true;
await usePStore() await usePlaylistListPageStore()
.fetchAll() .fetchAll()
.then(() => { .then(() => {
state.loading.value = false; state.loading.value = false;
@ -42,7 +43,7 @@ const routes = [
component: () => import("@/views/PlaylistView/index.vue"), component: () => import("@/views/PlaylistView/index.vue"),
beforeEnter: async (to: any) => { beforeEnter: async (to: any) => {
state.loading.value = true; state.loading.value = true;
await usePTrackStore() await usePlaylistPageStore()
.fetchAll(to.params.pid) .fetchAll(to.params.pid)
.then(() => { .then(() => {
state.loading.value = false; state.loading.value = false;
@ -60,7 +61,7 @@ const routes = [
component: () => import("@/views/AlbumView/index.vue"), component: () => import("@/views/AlbumView/index.vue"),
beforeEnter: async (to: any) => { beforeEnter: async (to: any) => {
state.loading.value = true; state.loading.value = true;
const store = useAStore(); const store = useAlbumPageStore();
await store.fetchTracksAndArtists(to.params.hash).then(() => { await store.fetchTracksAndArtists(to.params.hash).then(() => {
state.loading.value = false; state.loading.value = false;
@ -76,6 +77,15 @@ const routes = [
path: "/artists/:hash", path: "/artists/:hash",
name: "ArtistView", name: "ArtistView",
component: () => import("@/views/ArtistView"), component: () => import("@/views/ArtistView"),
beforeEnter: async (to: any) => {
state.loading.value = true;
await useArtistPageStore()
.getData(to.params.hash)
.then(() => {
state.loading.value = false;
});
},
}, },
{ {
path: "/settings", path: "/settings",

View File

@ -8,9 +8,9 @@ import { content_width } from "@/stores/content-width";
import { import {
getAlbumsFromArtist, getAlbumsFromArtist,
getAlbumTracks, getAlbumTracks
} from "../../composables/fetch/album"; } from "../../composables/fetch/album";
import { AlbumInfo, Artist, FuseResult, Track } from "../../interfaces"; import { Album, Artist, FuseResult, Track } from "../../interfaces";
import { useNotifStore } from "../notification"; import { useNotifStore } from "../notification";
interface Disc { interface Disc {
@ -27,7 +27,7 @@ function sortByTrackNumber(tracks: Track[]) {
}); });
} }
function albumHasNoDiscs(album: AlbumInfo) { function albumHasNoDiscs(album: Album) {
if (album.is_single) return true; if (album.is_single) return true;
return false; return false;
@ -50,10 +50,10 @@ function createDiscs(tracks: Track[]) {
export default defineStore("album", { export default defineStore("album", {
state: () => ({ state: () => ({
query: "", query: "",
info: <AlbumInfo>{}, info: <Album>{},
rawTracks: <Track[]>[], rawTracks: <Track[]>[],
artists: <Artist[]>[], artists: <Artist[]>[],
albumArtists: <{ artist: string; albums: AlbumInfo[] }[]>[], albumArtists: <{ artist: string; albums: Album[] }[]>[],
bio: null, bio: null,
}), }),
actions: { actions: {

View File

@ -0,0 +1,21 @@
import { defineStore } from "pinia";
import { Artist, Album, Track } from "@/interfaces";
import { getArtistData } from "@/composables/fetch/artists";
export default defineStore("artistPage", {
state: () => ({
info: <Artist>{},
albums: <Album[]>[],
tracks: <Track[]>[],
}),
actions: {
async getData(hash: string) {
const { artist, albums, tracks } = await getArtistData(hash);
this.info = artist;
this.albums = albums;
this.tracks = tracks;
},
},
});

View File

@ -1,20 +1,17 @@
import { Routes } from "./../composables/enums"; import { reactive, ref } from "@vue/reactivity";
import { ref, reactive } from "@vue/reactivity";
import { defineStore } from "pinia";
import { AlbumInfo, Artist, Playlist, Track } from "../interfaces";
import {
searchTracks,
searchAlbums,
searchArtists,
loadMoreTracks,
loadMoreAlbums,
loadMoreArtists,
} from "../composables/fetch/searchMusic";
import { watch } from "vue";
import useTabStore from "./tabs";
import useLoaderStore from "./loader";
import { useRoute } from "vue-router";
import { useDebounce } from "@vueuse/core"; import { useDebounce } from "@vueuse/core";
import { defineStore } from "pinia";
import { watch } from "vue";
import { useRoute } from "vue-router";
import {
loadMoreAlbums,
loadMoreArtists, loadMoreTracks, searchAlbums,
searchArtists, searchTracks
} from "../composables/fetch/searchMusic";
import { Album, Artist, Playlist, Track } from "../interfaces";
import { Routes } from "./../composables/enums";
import useLoaderStore from "./loader";
import useTabStore from "./tabs";
/** /**
* *
* Scrolls on clicking the loadmore button * Scrolls on clicking the loadmore button
@ -56,7 +53,7 @@ export default defineStore("search", () => {
const albums = reactive({ const albums = reactive({
query: "", query: "",
value: <AlbumInfo[]>[], value: <Album[]>[],
more: false, more: false,
}); });

View File

@ -4,16 +4,28 @@
style="height: 100%" style="height: 100%"
:class="{ isSmall, isMedium }" :class="{ isSmall, isMedium }"
> >
<RecycleScroller <DynamicScroller
class="scroller"
:items="scrollerItems" :items="scrollerItems"
:item-size="null" :min-item-size="64"
key-field="id" class="scroller"
v-slot="{ item }"
style="height: 100%" style="height: 100%"
> >
<component :is="item.component" v-bind="item.props" /> <template v-slot="{ item, index, active }">
</RecycleScroller> <DynamicScrollerItem
:item="item"
:active="active"
:size-dependencies="[item.props]"
:data-index="index"
>
<component
:key="index"
:is="item.component"
v-bind="item.props"
></component>
<!-- @playThis="playFromPage(item.props.index - 1)" -->
</DynamicScrollerItem>
</template>
</DynamicScroller>
</div> </div>
</template> </template>
@ -21,29 +33,54 @@
import { isMedium, isSmall } from "@/stores/content-width"; 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 Albums from "@/components/ArtistView/Albums.vue";
import useArtistPageStore from "@/stores/pages/artist";
import ArtistAlbums from "@/components/AlbumView/ArtistAlbums.vue";
import { computed } from "vue"; import { computed } from "vue";
import { onBeforeRouteUpdate } from "vue-router";
const artistStore = useArtistPageStore();
interface ScrollerItem { interface ScrollerItem {
id: string | number; id: string | number;
component: typeof Header; component: any;
// props: Record<string, unknown>; props?: Record<string, unknown>;
size: number; // size: number;
} }
const header: ScrollerItem = { const header: ScrollerItem = {
id: "artist-header", id: "artist-header",
component: Header, component: Header,
size: 19 * 16, // size: 16 * 19,
};
const top_tracks: ScrollerItem = {
id: "artist-top-tracks",
component: TopTracks,
// size: 16 * 25,
};
const artistAlbums: ScrollerItem = {
id: "artist-albums",
component: ArtistAlbums,
// size: 16 * 16,
props: { title: "Albums", albums: artistStore.albums },
}; };
const scrollerItems = computed(() => { const scrollerItems = computed(() => {
return [header]; return [header, top_tracks, artistAlbums];
});
onBeforeRouteUpdate((to, from, next) => {
artistStore.getData(to.params.hash as string);
}); });
</script> </script>
<style lang="scss"> <style lang="scss">
.artist-page { .section-title {
border: solid 1px; margin: 1rem;
padding-left: 1rem;
} }
</style> </style>