mirror of
https://github.com/tcsenpai/swingmusic.git
synced 2025-06-07 03:35:35 +00:00
rewrite tracks page of the search view
This commit is contained in:
parent
a8dc816d0b
commit
1d03b21caf
@ -5,6 +5,7 @@
|
||||
:tabs="tabs"
|
||||
@switchTab="switchTab"
|
||||
:currentTab="currentTab"
|
||||
:tabContent="true"
|
||||
>
|
||||
<Tab :name="currentTab" :isOnSearchPage="isOnSearchPage" />
|
||||
</TabsWrapper>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div id="right-tabs" class="rounded">
|
||||
<div id="right-tabs" :class="tabContent">
|
||||
<div class="tab-buttons-wrapper">
|
||||
<div class="tabheaders rounded-sm no-scroll">
|
||||
<div
|
||||
class="tab"
|
||||
v-for="tab in tabs"
|
||||
:key="tab"
|
||||
@click="switchTab(tab)"
|
||||
@click="emit('switchTab', tab)"
|
||||
:class="{ activetab: tab === currentTab }"
|
||||
>
|
||||
{{ tab }}
|
||||
@ -14,7 +14,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tab-content" v-auto-animate>
|
||||
<div id="tab-content" v-auto-animate v-if="tabContent">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
@ -24,22 +24,18 @@
|
||||
defineProps<{
|
||||
tabs: string[];
|
||||
currentTab: string;
|
||||
tabContent?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "switchTab", tab: string): void;
|
||||
}>();
|
||||
|
||||
function switchTab(tab: string) {
|
||||
emit("switchTab", tab);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#right-tabs {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: min-content 1fr;
|
||||
|
||||
.tab-buttons-wrapper {
|
||||
display: flex;
|
||||
@ -54,4 +50,8 @@ function switchTab(tab: string) {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
#right-tabs.tabContent {
|
||||
grid-template-rows: min-content 1fr;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,21 +1,43 @@
|
||||
<template>
|
||||
<div class="nav-search-input">
|
||||
<SearchInput :on_nav="true" />
|
||||
<TabsWrapper
|
||||
:tabs="tabs"
|
||||
:currentTab="($route.params.page as string)"
|
||||
@switchTab="(tab: string) => {
|
||||
$router.push({ name: Routes.search, params: { page: tab } });
|
||||
search.switchTab(tab);
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TabsWrapper from "@/components/RightSideBar/Search/TabsWrapper.vue";
|
||||
import SearchInput from "@/components/RightSideBar/SearchInput.vue";
|
||||
import { Routes } from "@/router/routes";
|
||||
|
||||
import useSearchStore from "@/stores/search";
|
||||
const search = useSearchStore();
|
||||
|
||||
const tabs = ["tracks", "albums", "artists"];
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.nav-search-input {
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
gap: 1rem;
|
||||
|
||||
& > div {
|
||||
width: 70%;
|
||||
margin: 0 auto;
|
||||
#right-tabs {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabheaders {
|
||||
height: 2.25rem;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,194 +0,0 @@
|
||||
<template>
|
||||
<!-- 64 is single item height, 24 is gap height -->
|
||||
<div class="header-list-layout">
|
||||
<div
|
||||
id="v-page-scrollable"
|
||||
v-bind="containerProps"
|
||||
style="height: 100%"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div
|
||||
v-bind="wrapperProps"
|
||||
class="v-list"
|
||||
ref="v_list"
|
||||
:class="{
|
||||
isSmall: isSmall,
|
||||
isMedium: isMedium || on_album_page,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
ref="header"
|
||||
class="header rounded"
|
||||
v-if="!no_header"
|
||||
:style="{
|
||||
height:
|
||||
headerHeight +
|
||||
(on_album_page && album.query.length === 0 ? 0 : 24) +
|
||||
'px',
|
||||
}"
|
||||
>
|
||||
<div ref="header_content" class="header-content">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="t in tracks">
|
||||
<AlbumDiscBar
|
||||
v-if="on_album_page && t.data.is_album_disc_number"
|
||||
:album_disc="t.data"
|
||||
/>
|
||||
|
||||
<SongItem
|
||||
v-else
|
||||
style="height: 60px"
|
||||
:key="t.data.id"
|
||||
:track="t.data"
|
||||
:hide_album="on_album_page"
|
||||
:index="
|
||||
on_album_page
|
||||
? t.data.track
|
||||
: t.data.index !== undefined
|
||||
? t.data.index + 1
|
||||
: t.index + 1
|
||||
"
|
||||
@playThis="
|
||||
updateQueue(
|
||||
t.data.index !== undefined
|
||||
? t.data.index - (on_album_page ? t.data.disc : 0)
|
||||
: t.index
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="page-bottom-padding" style="height: 64px"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useElementSize, useVirtualList } from "@vueuse/core";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
|
||||
import { Track } from "@/interfaces";
|
||||
import useAlbumStore from "@/stores/pages/album";
|
||||
import useQStore from "@/stores/queue";
|
||||
|
||||
import AlbumDiscBar from "@/components/AlbumView/AlbumDiscBar.vue";
|
||||
import SongItem from "@/components/shared/SongItem.vue";
|
||||
|
||||
// EMITS & PROPS
|
||||
const emit = defineEmits<{
|
||||
(e: "playFromPage", index: number): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
tracks: Track[];
|
||||
on_album_page?: boolean;
|
||||
no_header?: boolean;
|
||||
}>();
|
||||
|
||||
const queue = useQStore();
|
||||
const album = useAlbumStore();
|
||||
|
||||
function updateQueue(index: number) {
|
||||
emit("playFromPage", index);
|
||||
}
|
||||
|
||||
let scrollable: HTMLElement;
|
||||
const v_list = ref<HTMLElement>();
|
||||
const header_content = ref<HTMLElement>();
|
||||
const { width } = useElementSize(v_list);
|
||||
|
||||
const brk = {
|
||||
sm: 500,
|
||||
md: 800,
|
||||
};
|
||||
|
||||
const isSmall = computed(() => width.value < brk.sm);
|
||||
const isMedium = computed(() => width.value > brk.sm && width.value < brk.md);
|
||||
|
||||
// VIRTUAL LIST
|
||||
const source = computed(() => props.tracks);
|
||||
|
||||
const {
|
||||
list: tracks,
|
||||
containerProps,
|
||||
wrapperProps,
|
||||
} = useVirtualList(source, {
|
||||
itemHeight: 60,
|
||||
overscan: 20,
|
||||
});
|
||||
|
||||
// watch source changes and scroll to top
|
||||
watch(source, () => {
|
||||
scrollable.scroll(0, 0);
|
||||
});
|
||||
|
||||
// HEADER
|
||||
const header = ref<HTMLElement>();
|
||||
const { height: headerHeight } = useElementSize(header_content);
|
||||
|
||||
function handleScroll(e: Event) {
|
||||
const scrollTop = (e.target as HTMLElement).scrollTop;
|
||||
|
||||
if (scrollTop > (header.value?.offsetHeight || 0)) {
|
||||
header.value ? (header.value.style.opacity = "0") : null;
|
||||
} else {
|
||||
header.value ? (header.value.style.opacity = "1") : null;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
scrollable = document.getElementById("v-page-scrollable") as HTMLElement;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.header-list-layout {
|
||||
margin-right: -$medium;
|
||||
height: 100%;
|
||||
|
||||
.v-list {
|
||||
padding-right: calc(1rem - $small + 2px);
|
||||
scrollbar-width: thin;
|
||||
|
||||
.current {
|
||||
background-color: $gray5;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.v-list.isSmall {
|
||||
// hide album and artists columns
|
||||
.songlist-item {
|
||||
grid-template-columns: 1.5rem 2fr 2.5rem 2.5rem;
|
||||
}
|
||||
|
||||
.song-artists,
|
||||
.song-album {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.isSmallArtists {
|
||||
display: unset !important;
|
||||
font-size: small;
|
||||
color: $white;
|
||||
opacity: 0.67;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list.isMedium {
|
||||
// hide album column
|
||||
.songlist-item {
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 2.5rem 2.5rem;
|
||||
}
|
||||
|
||||
.song-album {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,33 +0,0 @@
|
||||
<template>
|
||||
<div id="ap-page">
|
||||
<header class="ap-page-header" ref="apheader">
|
||||
<slot name="header"></slot>
|
||||
</header>
|
||||
<main class="ap-page-content">
|
||||
<slot name="content"></slot>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useVisibility } from "@/utils";
|
||||
import useNavStore from "@/stores/nav";
|
||||
|
||||
const nav = useNavStore();
|
||||
const apheader = ref<any>(null);
|
||||
|
||||
function handleVisibilityState(state: boolean) {
|
||||
nav.toggleShowPlay(state);
|
||||
}
|
||||
|
||||
useVisibility(apheader, handleVisibilityState);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#ap-page {
|
||||
display: grid;
|
||||
grid-template-rows: 23rem 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
</style>
|
@ -36,7 +36,7 @@ export default defineStore("search", () => {
|
||||
const route = useRoute();
|
||||
|
||||
const currentTab = ref("tracks");
|
||||
const RESULT_COUNT = 10;
|
||||
const RESULT_COUNT = 12;
|
||||
|
||||
const loadCounter = reactive({
|
||||
tracks: 0,
|
||||
@ -92,8 +92,6 @@ export default defineStore("search", () => {
|
||||
albums.more = res.more;
|
||||
albums.query = query;
|
||||
});
|
||||
|
||||
console.log("fetched albums");
|
||||
}
|
||||
|
||||
function fetchArtists(query: string) {
|
||||
@ -109,7 +107,7 @@ export default defineStore("search", () => {
|
||||
function loadTracks() {
|
||||
loadCounter.tracks += RESULT_COUNT;
|
||||
|
||||
startLoading();
|
||||
startLoading();
|
||||
loadMoreTracks(loadCounter.tracks)
|
||||
.then((res) => {
|
||||
tracks.value = [...tracks.value, ...res.tracks];
|
||||
@ -139,7 +137,7 @@ export default defineStore("search", () => {
|
||||
function loadArtists() {
|
||||
loadCounter.artists += RESULT_COUNT;
|
||||
|
||||
startLoading();
|
||||
startLoading();
|
||||
loadMoreArtists(loadCounter.artists)
|
||||
.then((res) => {
|
||||
artists.value = [...artists.value, ...res.artists];
|
||||
|
@ -13,18 +13,4 @@ import AlbumCard from "@/components/shared/AlbumCard.vue";
|
||||
import useSearchStore from "@/stores/search";
|
||||
|
||||
const search = useSearchStore();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.search-albums-view.grid-page {
|
||||
// max-height: 100%;
|
||||
// display: grid;
|
||||
// grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
|
||||
// gap: 1.75rem 0;
|
||||
|
||||
// padding-bottom: 4rem;
|
||||
padding-right: $small;
|
||||
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
</script>
|
@ -1,19 +1,5 @@
|
||||
<template>
|
||||
<div class="search-view content-page">
|
||||
<div class="tabs">
|
||||
<button
|
||||
v-for="page in pages"
|
||||
:class="{ 'btn-active': page === $route.params.page }"
|
||||
@click="
|
||||
() => {
|
||||
$router.push({ name: Routes.search, params: { page: page } });
|
||||
search.switchTab(page);
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ page }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="search-view content-page" style="padding-right: 0;">
|
||||
<div ref="page" class="page no-scroll" v-auto-animate>
|
||||
<component :is="component" />
|
||||
</div>
|
||||
@ -152,23 +138,10 @@ onMounted(() => {
|
||||
height: calc(100% - 1rem);
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-rows: max-content 1fr;
|
||||
grid-template-rows: 1fr max-content;
|
||||
|
||||
margin-right: -0.75rem;
|
||||
|
||||
.tabs {
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
& > * {
|
||||
background-color: $gray4;
|
||||
padding: $small 1rem;
|
||||
border-radius: $small;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.page.no-scroll {
|
||||
overflow-x: visible;
|
||||
}
|
||||
@ -181,6 +154,7 @@ onMounted(() => {
|
||||
|
||||
padding-bottom: 4rem;
|
||||
overflow: auto;
|
||||
padding-right: $medium;
|
||||
}
|
||||
|
||||
button.load-more {
|
||||
|
@ -1,19 +1,32 @@
|
||||
<template>
|
||||
<div class="search-tracks-view">
|
||||
<div class="no-scroll">
|
||||
<Layout
|
||||
:no_header="true"
|
||||
:tracks="search.tracks.value"
|
||||
@playFromPage="playFromSearch"
|
||||
/>
|
||||
<div
|
||||
:class="{ isSmall, isMedium }"
|
||||
style="height: 100%"
|
||||
>
|
||||
<RecycleScroller
|
||||
id="songlist-scroller"
|
||||
style="height: 100%"
|
||||
:items="search.tracks.value.map((track) => ({ track, id: Math.random() }))"
|
||||
:item-size="64"
|
||||
key-field="id"
|
||||
v-slot="{ item, index }"
|
||||
>
|
||||
<SongItem
|
||||
:track="item.track"
|
||||
:index="index + 1"
|
||||
@playThis="playFromSearch(index)"
|
||||
/>
|
||||
</RecycleScroller>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Layout from "@/layouts/HeaderAndVList.vue";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import useSearchStore from "@/stores/search";
|
||||
import { isMedium, isSmall } from "@/stores/content-width";
|
||||
import SongItem from "@/components/shared/SongItem.vue";
|
||||
|
||||
const search = useSearchStore();
|
||||
const queue = useQueueStore();
|
||||
@ -32,13 +45,9 @@ function playFromSearch(index: number) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.header-list-layout {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
#songlist-scroller{
|
||||
padding-right: 1rem;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user