mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-05 04:53:01 +00:00
make albums on artist page reactive
+ show artist name on artist album component on album page + attach artist page link to artist card + use small artist page on album header + use album color on genre banner on album page
This commit is contained in:
committed by
Mungai Njoroge
parent
075765088f
commit
e54fea2d4d
@@ -24,7 +24,6 @@ defineProps<{
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.albums-from-artist {
|
.albums-from-artist {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-top: 1rem;
|
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="genres-banner">
|
<div
|
||||||
|
class="genres-banner"
|
||||||
|
:class="{
|
||||||
|
nocontrast: album.info.colors ? isLight(album.info.colors[0]) : false,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div class="rounded pad-sm">
|
<div class="rounded pad-sm">
|
||||||
{{ album.info.genres.length ? "Genres" : "No genres" }}
|
{{ album.info.genres.length ? "Genres" : "No genres" }}
|
||||||
</div>
|
</div>
|
||||||
<div v-for="genre in album.info.genres" class="rounded pad-sm">
|
<div
|
||||||
|
v-for="genre in album.info.genres"
|
||||||
|
class="rounded pad-sm"
|
||||||
|
:style="{ backgroundColor: album.info.colors[0] }"
|
||||||
|
>
|
||||||
{{ genre }}
|
{{ genre }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -12,6 +21,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
import useAlbumStore from "@/stores/pages/album";
|
import useAlbumStore from "@/stores/pages/album";
|
||||||
|
import { isLight } from "@/composables/colors/album";
|
||||||
|
|
||||||
const album = useAlbumStore();
|
const album = useAlbumStore();
|
||||||
|
|
||||||
@@ -23,6 +33,9 @@ onMounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.genres-banner.nocontrast {
|
||||||
|
color: $black;
|
||||||
|
}
|
||||||
.genres-banner {
|
.genres-banner {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -36,7 +49,7 @@ onMounted(() => {
|
|||||||
background-color: $gray5;
|
background-color: $gray5;
|
||||||
min-width: 4rem;
|
min-width: 4rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
outline: solid 1px $gray3;
|
outline: solid 1px $gray;
|
||||||
padding: $small 1rem;
|
padding: $small 1rem;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
@@ -47,8 +60,9 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $darkestblue;
|
background-color: $darkestblue !important;
|
||||||
outline-color: $darkestblue;
|
outline-color: $darkestblue;
|
||||||
|
color: $white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="imguri.artist + a.image"
|
:src="imguri.artist.small + a.image"
|
||||||
class="shadow-lg circular"
|
class="shadow-lg circular"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
:title="a.name"
|
:title="a.name"
|
||||||
@@ -164,27 +164,21 @@ useVisibility(albumheaderthing, handleVisibilityState);
|
|||||||
|
|
||||||
.art {
|
.art {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
// align-items: center;
|
||||||
flex-direction: row-reverse;
|
gap: $small;
|
||||||
// background-color: red;
|
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 4rem;
|
height: 3rem;
|
||||||
background-color: $gray;
|
background-color: $gray;
|
||||||
border: solid 2px $white;
|
border: solid 2px $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
img:last-child {
|
a {
|
||||||
height: 4rem;
|
transition: all 0.25s ease-in-out;
|
||||||
margin-top: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img:not(:last-child) {
|
a:hover {
|
||||||
margin-left: -2rem;
|
transform: scale(1.4);
|
||||||
}
|
|
||||||
|
|
||||||
img:hover {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,15 +10,20 @@
|
|||||||
<div class="card-title">ARTIST</div>
|
<div class="card-title">ARTIST</div>
|
||||||
<div class="artist-name">{{ artist.info.name }}</div>
|
<div class="artist-name">{{ artist.info.name }}</div>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
{{ artist.info.trackcount }} Tracks •
|
{{ artist.info.trackcount }} Track{{
|
||||||
{{ artist.info.albumcount }} Albums •
|
`${artist.info.trackcount == 1 ? "" : "s"}`
|
||||||
|
}}
|
||||||
|
• {{ artist.info.albumcount }} Album{{
|
||||||
|
`${artist.info.albumcount == 1 ? "" : "s"}`
|
||||||
|
}}
|
||||||
|
•
|
||||||
{{ formatSeconds(artist.info.duration, true) }}
|
{{ formatSeconds(artist.info.duration, true) }}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<PlayBtnRect />
|
<PlayBtnRect />
|
||||||
</div>
|
</div>
|
||||||
<div class="artist-img">
|
<div class="artist-img no-select">
|
||||||
<img :src="paths.images.artist + artist.info.image" />
|
<img :src="paths.images.artist.large + artist.info.image" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="gradient"
|
class="gradient"
|
||||||
@@ -63,8 +68,8 @@ const artist = useArtistPageStore();
|
|||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
to left,
|
to left,
|
||||||
transparent 10%,
|
transparent 10%,
|
||||||
#434142 50%,
|
$gray2 50%,
|
||||||
#434142 100%
|
$gray2 100%
|
||||||
);
|
);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -106,10 +111,6 @@ const artist = useArtistPageStore();
|
|||||||
.stats {
|
.stats {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playbtnrect {
|
|
||||||
border-radius: 2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.artist-info.nocontrast {
|
.artist-info.nocontrast {
|
||||||
|
|||||||
@@ -10,24 +10,28 @@
|
|||||||
:isCurrentPlaying="false"
|
:isCurrentPlaying="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="error" v-if="!artist.tracks.length">No tracks</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import useQueueStore from "@/stores/queue";
|
|
||||||
import SongItem from "../shared/SongItem.vue";
|
import SongItem from "../shared/SongItem.vue";
|
||||||
import useArtistPageStore from "@/stores/pages/artist";
|
import useArtistPageStore from "@/stores/pages/artist";
|
||||||
|
|
||||||
const queue = useQueueStore();
|
|
||||||
const artist = useArtistPageStore();
|
const artist = useArtistPageStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.artist-top-tracks {
|
.artist-top-tracks {
|
||||||
// padding-bottom: 2rem;
|
margin-bottom: 1rem;
|
||||||
|
margin-top: 2rem;
|
||||||
.section-title {
|
.section-title {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
padding-left: 1rem;
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="artist-card" :class="{ _is_on_sidebar: alt }">
|
<RouterLink
|
||||||
<img class="artist-image circular" :src="imguri + artist.image" loading="lazy" />
|
:to="{
|
||||||
|
name: Routes.artist,
|
||||||
|
params: {
|
||||||
|
hash: artist.artisthash,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="artist-card"
|
||||||
|
: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>
|
||||||
</div>
|
</div>
|
||||||
|
</RouterLink>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Routes } from "@/composables/enums";
|
||||||
import { Artist } from "@/interfaces";
|
import { Artist } from "@/interfaces";
|
||||||
import { paths } from "../../config";
|
import { paths } from "../../config";
|
||||||
|
|
||||||
const imguri = paths.images.artist;
|
const imguri = paths.images.artist.large;
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
artist: Artist;
|
artist: Artist;
|
||||||
|
|||||||
@@ -40,5 +40,6 @@ defineProps<{
|
|||||||
transition: all 0.5s ease-in-out;
|
transition: all 0.5s ease-in-out;
|
||||||
color: $white;
|
color: $white;
|
||||||
background: $darkestblue !important;
|
background: $darkestblue !important;
|
||||||
|
border-radius: 2rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export function isLight(rgb: string): boolean {
|
|||||||
const [r, g, b] = rgb.match(/\d+/g)!.map(Number);
|
const [r, g, b] = rgb.match(/\d+/g)!.map(Number);
|
||||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||||
|
|
||||||
return brightness > 170;
|
return brightness > 165;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BtnColor {
|
interface BtnColor {
|
||||||
@@ -18,7 +18,6 @@ interface BtnColor {
|
|||||||
isDark: boolean;
|
isDark: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the luminance of a color.
|
* Returns the luminance of a color.
|
||||||
* @param r The red value of the color.
|
* @param r The red value of the color.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const getArtistData = async (hash: string) => {
|
|||||||
|
|
||||||
const { data, error } = await useAxios({
|
const { data, error } = await useAxios({
|
||||||
get: true,
|
get: true,
|
||||||
url: paths.api.artist + `/${hash}`,
|
url: paths.api.artist + `/${hash}?limit=6`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|||||||
+9
-2
@@ -20,7 +20,11 @@ const imageRoutes = {
|
|||||||
large: "/t/",
|
large: "/t/",
|
||||||
small: "/t/s/",
|
small: "/t/s/",
|
||||||
},
|
},
|
||||||
artist: "/a/",
|
// artist: "/a/",
|
||||||
|
artist: {
|
||||||
|
large: "/a/",
|
||||||
|
small: "/a/s/",
|
||||||
|
},
|
||||||
playlist: "/p/",
|
playlist: "/p/",
|
||||||
raw: "/raw/",
|
raw: "/raw/",
|
||||||
};
|
};
|
||||||
@@ -83,7 +87,10 @@ const paths = {
|
|||||||
small: baseImgUrl + imageRoutes.thumb.small,
|
small: baseImgUrl + imageRoutes.thumb.small,
|
||||||
large: baseImgUrl + imageRoutes.thumb.large,
|
large: baseImgUrl + imageRoutes.thumb.large,
|
||||||
},
|
},
|
||||||
artist: baseImgUrl + imageRoutes.artist,
|
artist: {
|
||||||
|
small: baseImgUrl + imageRoutes.artist.small,
|
||||||
|
large: baseImgUrl + imageRoutes.artist.large,
|
||||||
|
},
|
||||||
playlist: baseImgUrl + imageRoutes.playlist,
|
playlist: baseImgUrl + imageRoutes.playlist,
|
||||||
raw: baseImgUrl + imageRoutes.raw,
|
raw: baseImgUrl + imageRoutes.raw,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -52,8 +52,7 @@ export default defineStore("album", {
|
|||||||
query: "",
|
query: "",
|
||||||
info: <Album>{},
|
info: <Album>{},
|
||||||
rawTracks: <Track[]>[],
|
rawTracks: <Track[]>[],
|
||||||
artists: <Artist[]>[],
|
albumArtists: <{ artisthash: string; albums: Album[] }[]>[],
|
||||||
albumArtists: <{ artist: string; albums: Album[] }[]>[],
|
|
||||||
bio: null,
|
bio: null,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export default defineStore("artistPage", {
|
|||||||
const { artist, albums, tracks } = await getArtistData(hash);
|
const { artist, albums, tracks } = await getArtistData(hash);
|
||||||
|
|
||||||
this.info = artist;
|
this.info = artist;
|
||||||
this.albums = albums;
|
|
||||||
this.tracks = tracks;
|
this.tracks = tracks;
|
||||||
|
this.albums = albums;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -85,10 +85,13 @@ function getSongItems() {
|
|||||||
|
|
||||||
function getArtistAlbumComponents(): ScrollerItem[] {
|
function getArtistAlbumComponents(): ScrollerItem[] {
|
||||||
return album.albumArtists.map((artist) => {
|
return album.albumArtists.map((artist) => {
|
||||||
|
const artist_name = album.info.albumartists.find(
|
||||||
|
(a) => a.hash === artist.artisthash
|
||||||
|
)?.name;
|
||||||
return {
|
return {
|
||||||
id: Math.random().toString(),
|
id: Math.random().toString(),
|
||||||
component: ArtistAlbums,
|
component: ArtistAlbums,
|
||||||
props: { artist },
|
props: { title: `More from ${artist_name}`, albums: artist.albums },
|
||||||
size: 20 * 16,
|
size: 20 * 16,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -113,7 +116,6 @@ const scrollerItems = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function playFromAlbum(index: number) {
|
function playFromAlbum(index: number) {
|
||||||
|
|
||||||
const { title, albumartists, albumhash } = album.info;
|
const { title, albumartists, albumhash } = album.info;
|
||||||
queue.playFromAlbum(title, albumhash, album.allTracks);
|
queue.playFromAlbum(title, albumhash, album.allTracks);
|
||||||
queue.play(index);
|
queue.play(index);
|
||||||
|
|||||||
@@ -47,30 +47,36 @@ interface ScrollerItem {
|
|||||||
id: string | number;
|
id: string | number;
|
||||||
component: any;
|
component: any;
|
||||||
props?: Record<string, unknown>;
|
props?: Record<string, unknown>;
|
||||||
// size: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const header: ScrollerItem = {
|
const header: ScrollerItem = {
|
||||||
id: "artist-header",
|
id: "artist-header",
|
||||||
component: Header,
|
component: Header,
|
||||||
// size: 16 * 19,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const top_tracks: ScrollerItem = {
|
const top_tracks: ScrollerItem = {
|
||||||
id: "artist-top-tracks",
|
id: "artist-top-tracks",
|
||||||
component: TopTracks,
|
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, top_tracks, artistAlbums];
|
let components = [header];
|
||||||
|
|
||||||
|
if (artistStore.tracks.length > 0) {
|
||||||
|
components.push(top_tracks);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artistStore.albums.length > 0) {
|
||||||
|
const artistAlbums: ScrollerItem = {
|
||||||
|
id: "artist-albums",
|
||||||
|
component: ArtistAlbums,
|
||||||
|
props: { title: "Albums", albums: artistStore.albums },
|
||||||
|
};
|
||||||
|
|
||||||
|
components.push(artistAlbums);
|
||||||
|
}
|
||||||
|
|
||||||
|
return components;
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeRouteUpdate((to, from, next) => {
|
onBeforeRouteUpdate((to, from, next) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user