mirror of
https://github.com/tcsenpai/swingmusic.git
synced 2025-07-29 14:12:21 +00:00
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:
parent
da852e72f3
commit
4a49d48011
51
src/components/AlbumView/ArtistAlbums.vue
Normal file
51
src/components/AlbumView/ArtistAlbums.vue
Normal 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>
|
50
src/components/AlbumView/GenreBanner.vue
Normal file
50
src/components/AlbumView/GenreBanner.vue
Normal 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>
|
@ -49,6 +49,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -59,7 +60,7 @@ 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 { getButtonColor, isLight } from "../../composables/colors/album";
|
import { isLight } from "../../composables/colors/album";
|
||||||
import { playSources } from "../../composables/enums";
|
import { playSources } from "../../composables/enums";
|
||||||
import { AlbumInfo } from "../../interfaces";
|
import { AlbumInfo } from "../../interfaces";
|
||||||
|
|
||||||
@ -73,6 +74,7 @@ const albumheaderthing = ref<any>(null);
|
|||||||
const imguri = paths.images;
|
const imguri = paths.images;
|
||||||
const nav = useNavStore();
|
const nav = useNavStore();
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls the `toggleShowPlay` method which toggles the play button in the nav.
|
* Calls the `toggleShowPlay` method which toggles the play button in the nav.
|
||||||
* Emits the `resetBottomPadding` event to reset the album page content bottom padding.
|
* Emits the `resetBottomPadding` event to reset the album page content bottom padding.
|
||||||
@ -87,6 +89,8 @@ useVisibility(albumheaderthing, handleVisibilityState);
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
||||||
|
|
||||||
.a-header {
|
.a-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: max-content 1fr;
|
grid-template-columns: max-content 1fr;
|
||||||
|
@ -40,7 +40,7 @@ function getSource() {
|
|||||||
location: {
|
location: {
|
||||||
name: Routes.album,
|
name: Routes.album,
|
||||||
params: {
|
params: {
|
||||||
hash: source.hash,
|
hash: source.albumhash,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'AlbumView',
|
name: 'AlbumView',
|
||||||
params: { hash: album.hash },
|
params: { hash: album.albumhash },
|
||||||
}"
|
}"
|
||||||
class="result-item"
|
class="result-item"
|
||||||
>
|
>
|
||||||
|
@ -149,6 +149,8 @@ function showMenu(e: MouseEvent) {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 3rem;
|
height: 3rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import { paths } from "@/config";
|
import { paths } from "@/config";
|
||||||
import state from "../state";
|
import { NotifType, useNotifStore } from "@/stores/notification";
|
||||||
import { AlbumInfo, Track } from "../../interfaces";
|
import { AlbumInfo, Track } from "../../interfaces";
|
||||||
import useAxios from "./useAxios";
|
import useAxios from "./useAxios";
|
||||||
import { NotifType, useNotifStore } from "@/stores/notification";
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
album: albumUrl,
|
album: albumUrl,
|
||||||
albumartists: albumArtistsUrl,
|
albumartists: albumArtistsUrl,
|
||||||
albumbio: albumBioUrl
|
albumbio: albumBioUrl,
|
||||||
} = paths.api
|
albumsByArtistUrl,
|
||||||
|
} = paths.api;
|
||||||
|
|
||||||
const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => {
|
const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => {
|
||||||
interface AlbumData {
|
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,
|
||||||
|
};
|
||||||
|
@ -34,7 +34,7 @@ export default function play(
|
|||||||
useQueue.playFromAlbum(
|
useQueue.playFromAlbum(
|
||||||
a_store.info.title,
|
a_store.info.title,
|
||||||
a_store.info.albumartist,
|
a_store.info.albumartist,
|
||||||
a_store.info.hash,
|
a_store.info.albumhash,
|
||||||
a_store.allTracks
|
a_store.allTracks
|
||||||
);
|
);
|
||||||
useQueue.play();
|
useQueue.play();
|
||||||
|
@ -44,6 +44,9 @@ const paths = {
|
|||||||
get albumbio() {
|
get albumbio() {
|
||||||
return this.album + "/bio";
|
return this.album + "/bio";
|
||||||
},
|
},
|
||||||
|
get albumsByArtistUrl(){
|
||||||
|
return this.album + "/from-artist"
|
||||||
|
},
|
||||||
folder: baseApiUrl + "/folder",
|
folder: baseApiUrl + "/folder",
|
||||||
playlist: {
|
playlist: {
|
||||||
base: baseApiUrl + "/playlist",
|
base: baseApiUrl + "/playlist",
|
||||||
@ -85,4 +88,4 @@ const paths = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export { paths, toggleMode };
|
export { paths, toggleMode };
|
||||||
|
@ -43,7 +43,7 @@ export interface AlbumInfo {
|
|||||||
date: string;
|
date: string;
|
||||||
image: string;
|
image: string;
|
||||||
artistimg: string;
|
artistimg: string;
|
||||||
hash: string;
|
albumhash: string;
|
||||||
colors: string[];
|
colors: string[];
|
||||||
copyright?: string;
|
copyright?: string;
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ export interface fromFolder {
|
|||||||
export interface fromAlbum {
|
export interface fromAlbum {
|
||||||
type: FromOptions.album;
|
type: FromOptions.album;
|
||||||
name: string;
|
name: string;
|
||||||
hash: string;
|
albumhash: string;
|
||||||
albumartist: string;
|
albumartist: string;
|
||||||
}
|
}
|
||||||
export interface fromPlaylist {
|
export interface fromPlaylist {
|
||||||
|
@ -4,8 +4,12 @@ import { ComputedRef } from "vue";
|
|||||||
import { AlbumDisc } from "./../../interfaces";
|
import { AlbumDisc } from "./../../interfaces";
|
||||||
|
|
||||||
import { FuseTrackOptions } from "@/composables/enums";
|
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 { AlbumInfo, Artist, FuseResult, Track } from "../../interfaces";
|
||||||
import { useNotifStore } from "../notification";
|
import { useNotifStore } from "../notification";
|
||||||
|
|
||||||
@ -49,6 +53,7 @@ export default defineStore("album", {
|
|||||||
info: <AlbumInfo>{},
|
info: <AlbumInfo>{},
|
||||||
rawTracks: <Track[]>[],
|
rawTracks: <Track[]>[],
|
||||||
artists: <Artist[]>[],
|
artists: <Artist[]>[],
|
||||||
|
albumArtists: <{ artist: string; albums: AlbumInfo[] }[]>[],
|
||||||
bio: null,
|
bio: null,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
@ -62,9 +67,23 @@ export default defineStore("album", {
|
|||||||
this.rawTracks = album.tracks;
|
this.rawTracks = album.tracks;
|
||||||
this.info = album.info;
|
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() {
|
resetQuery() {
|
||||||
this.query = "";
|
this.query = "";
|
||||||
},
|
},
|
||||||
|
resetAlbumArtists() {
|
||||||
|
this.albumArtists = [];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
discs(): Disc {
|
discs(): Disc {
|
||||||
|
@ -161,7 +161,7 @@ export default defineStore("Queue", {
|
|||||||
this.from = <fromAlbum>{
|
this.from = <fromAlbum>{
|
||||||
type: FromOptions.album,
|
type: FromOptions.album,
|
||||||
name: aname,
|
name: aname,
|
||||||
hash: albumhash,
|
albumhash: albumhash,
|
||||||
albumartist: albumartist,
|
albumartist: albumartist,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
<component
|
<component
|
||||||
:is="item.component"
|
:is="item.component"
|
||||||
v-bind="item.props"
|
v-bind="item.props"
|
||||||
|
:style="{ maxHeight: `${item.size}px` }"
|
||||||
@playThis="
|
@playThis="
|
||||||
playFromAlbum(item.props.track.index - item.props.track.disc)
|
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 useQueueStore from "@/stores/queue";
|
||||||
|
|
||||||
import AlbumDiscBar from "@/components/AlbumView/AlbumDiscBar.vue";
|
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 Header from "@/components/AlbumView/Header.vue";
|
||||||
import SongItem from "@/components/shared/SongItem.vue";
|
import SongItem from "@/components/shared/SongItem.vue";
|
||||||
|
|
||||||
import { isSmall } from "@/stores/content-width";
|
import { isSmall } from "@/stores/content-width";
|
||||||
|
|
||||||
const album = useAlbumStore();
|
const album = useAlbumStore();
|
||||||
@ -42,8 +46,12 @@ const queue = useQueueStore();
|
|||||||
|
|
||||||
interface ScrollerItem {
|
interface ScrollerItem {
|
||||||
id: string;
|
id: string;
|
||||||
component: typeof Header | typeof SongItem;
|
component:
|
||||||
props: any;
|
| typeof Header
|
||||||
|
| typeof SongItem
|
||||||
|
| typeof GenreBanner
|
||||||
|
| typeof ArtistAlbums;
|
||||||
|
props?: any;
|
||||||
size: number;
|
size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -62,12 +70,29 @@ class songItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const genreBanner: ScrollerItem = {
|
||||||
|
id: "genre-banner",
|
||||||
|
component: GenreBanner,
|
||||||
|
size: 64,
|
||||||
|
};
|
||||||
|
|
||||||
function getSongItems() {
|
function getSongItems() {
|
||||||
return album.tracks.map((track) => {
|
return album.tracks.map((track) => {
|
||||||
return new songItem(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 scrollerItems = computed(() => {
|
||||||
const header: ScrollerItem = {
|
const header: ScrollerItem = {
|
||||||
id: "album-header",
|
id: "album-header",
|
||||||
@ -75,22 +100,29 @@ const scrollerItems = computed(() => {
|
|||||||
props: {
|
props: {
|
||||||
album: album.info,
|
album: album.info,
|
||||||
},
|
},
|
||||||
size: 18 * 16,
|
size: 19 * 16,
|
||||||
};
|
};
|
||||||
|
|
||||||
return [header, ...getSongItems()];
|
return [
|
||||||
|
header,
|
||||||
|
...getSongItems(),
|
||||||
|
genreBanner,
|
||||||
|
...getArtistAlbumComponents(),
|
||||||
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
function playFromAlbum(index: number) {
|
function playFromAlbum(index: number) {
|
||||||
const { title, artist, hash } = album.info;
|
const { title, albumartist, albumhash } = album.info;
|
||||||
queue.playFromAlbum(title, artist, hash, album.allTracks);
|
queue.playFromAlbum(title, albumartist, albumhash, album.allTracks);
|
||||||
queue.play(index);
|
queue.play(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeRouteUpdate(async (to: RouteLocationNormalized) => {
|
onBeforeRouteUpdate(async (to: RouteLocationNormalized) => {
|
||||||
await album
|
await album.fetchTracksAndArtists(to.params.hash.toString()).then(() => {
|
||||||
.fetchTracksAndArtists(to.params.hash.toString())
|
album.resetQuery();
|
||||||
.then(() => album.resetQuery());
|
album.resetAlbumArtists();
|
||||||
|
album.fetchArtistAlbums();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeRouteLeave(() => {
|
onBeforeRouteLeave(() => {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="search-albums-view grid-page" v-auto-animate>
|
<div class="search-albums-view grid-page" v-auto-animate>
|
||||||
<AlbumCard
|
<AlbumCard
|
||||||
v-for="album in search.albums.value"
|
v-for="album in search.albums.value"
|
||||||
:key="album.hash"
|
:key="album.albumhash"
|
||||||
:album="album"
|
:album="album"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user