mirror of
https://github.com/tcsenpai/swingmusic.git
synced 2025-06-10 13:07: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"
|
:tabs="tabs"
|
||||||
@switchTab="switchTab"
|
@switchTab="switchTab"
|
||||||
:currentTab="currentTab"
|
:currentTab="currentTab"
|
||||||
|
:tabContent="true"
|
||||||
>
|
>
|
||||||
<Tab :name="currentTab" :isOnSearchPage="isOnSearchPage" />
|
<Tab :name="currentTab" :isOnSearchPage="isOnSearchPage" />
|
||||||
</TabsWrapper>
|
</TabsWrapper>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="right-tabs" class="rounded">
|
<div id="right-tabs" :class="tabContent">
|
||||||
<div class="tab-buttons-wrapper">
|
<div class="tab-buttons-wrapper">
|
||||||
<div class="tabheaders rounded-sm no-scroll">
|
<div class="tabheaders rounded-sm no-scroll">
|
||||||
<div
|
<div
|
||||||
class="tab"
|
class="tab"
|
||||||
v-for="tab in tabs"
|
v-for="tab in tabs"
|
||||||
:key="tab"
|
:key="tab"
|
||||||
@click="switchTab(tab)"
|
@click="emit('switchTab', tab)"
|
||||||
:class="{ activetab: tab === currentTab }"
|
:class="{ activetab: tab === currentTab }"
|
||||||
>
|
>
|
||||||
{{ tab }}
|
{{ tab }}
|
||||||
@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tab-content" v-auto-animate>
|
<div id="tab-content" v-auto-animate v-if="tabContent">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -24,22 +24,18 @@
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
tabs: string[];
|
tabs: string[];
|
||||||
currentTab: string;
|
currentTab: string;
|
||||||
|
tabContent?: boolean;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: "switchTab", tab: string): void;
|
(e: "switchTab", tab: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
function switchTab(tab: string) {
|
|
||||||
emit("switchTab", tab);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
#right-tabs {
|
#right-tabs {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: min-content 1fr;
|
|
||||||
|
|
||||||
.tab-buttons-wrapper {
|
.tab-buttons-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -54,4 +50,8 @@ function switchTab(tab: string) {
|
|||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#right-tabs.tabContent {
|
||||||
|
grid-template-rows: min-content 1fr;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,21 +1,43 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="nav-search-input">
|
<div class="nav-search-input">
|
||||||
<SearchInput :on_nav="true" />
|
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import TabsWrapper from "@/components/RightSideBar/Search/TabsWrapper.vue";
|
||||||
import SearchInput from "@/components/RightSideBar/SearchInput.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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.nav-search-input {
|
.nav-search-input {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-evenly;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr max-content;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
& > div {
|
#right-tabs {
|
||||||
width: 70%;
|
display: grid;
|
||||||
margin: 0 auto;
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabheaders {
|
||||||
|
height: 2.25rem;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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 route = useRoute();
|
||||||
|
|
||||||
const currentTab = ref("tracks");
|
const currentTab = ref("tracks");
|
||||||
const RESULT_COUNT = 10;
|
const RESULT_COUNT = 12;
|
||||||
|
|
||||||
const loadCounter = reactive({
|
const loadCounter = reactive({
|
||||||
tracks: 0,
|
tracks: 0,
|
||||||
@ -92,8 +92,6 @@ export default defineStore("search", () => {
|
|||||||
albums.more = res.more;
|
albums.more = res.more;
|
||||||
albums.query = query;
|
albums.query = query;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("fetched albums");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchArtists(query: string) {
|
function fetchArtists(query: string) {
|
||||||
@ -109,7 +107,7 @@ export default defineStore("search", () => {
|
|||||||
function loadTracks() {
|
function loadTracks() {
|
||||||
loadCounter.tracks += RESULT_COUNT;
|
loadCounter.tracks += RESULT_COUNT;
|
||||||
|
|
||||||
startLoading();
|
startLoading();
|
||||||
loadMoreTracks(loadCounter.tracks)
|
loadMoreTracks(loadCounter.tracks)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
tracks.value = [...tracks.value, ...res.tracks];
|
tracks.value = [...tracks.value, ...res.tracks];
|
||||||
@ -139,7 +137,7 @@ export default defineStore("search", () => {
|
|||||||
function loadArtists() {
|
function loadArtists() {
|
||||||
loadCounter.artists += RESULT_COUNT;
|
loadCounter.artists += RESULT_COUNT;
|
||||||
|
|
||||||
startLoading();
|
startLoading();
|
||||||
loadMoreArtists(loadCounter.artists)
|
loadMoreArtists(loadCounter.artists)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
artists.value = [...artists.value, ...res.artists];
|
artists.value = [...artists.value, ...res.artists];
|
||||||
|
@ -13,18 +13,4 @@ import AlbumCard from "@/components/shared/AlbumCard.vue";
|
|||||||
import useSearchStore from "@/stores/search";
|
import useSearchStore from "@/stores/search";
|
||||||
|
|
||||||
const search = useSearchStore();
|
const search = useSearchStore();
|
||||||
</script>
|
</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>
|
|
@ -1,19 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search-view content-page">
|
<div class="search-view content-page" style="padding-right: 0;">
|
||||||
<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 ref="page" class="page no-scroll" v-auto-animate>
|
<div ref="page" class="page no-scroll" v-auto-animate>
|
||||||
<component :is="component" />
|
<component :is="component" />
|
||||||
</div>
|
</div>
|
||||||
@ -152,23 +138,10 @@ onMounted(() => {
|
|||||||
height: calc(100% - 1rem);
|
height: calc(100% - 1rem);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
grid-template-rows: max-content 1fr;
|
grid-template-rows: 1fr max-content;
|
||||||
|
|
||||||
margin-right: -0.75rem;
|
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 {
|
.page.no-scroll {
|
||||||
overflow-x: visible;
|
overflow-x: visible;
|
||||||
}
|
}
|
||||||
@ -181,6 +154,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
padding-bottom: 4rem;
|
padding-bottom: 4rem;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
padding-right: $medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.load-more {
|
button.load-more {
|
||||||
|
@ -1,19 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search-tracks-view">
|
<div class="search-tracks-view">
|
||||||
<div class="no-scroll">
|
<div
|
||||||
<Layout
|
:class="{ isSmall, isMedium }"
|
||||||
:no_header="true"
|
style="height: 100%"
|
||||||
:tracks="search.tracks.value"
|
>
|
||||||
@playFromPage="playFromSearch"
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Layout from "@/layouts/HeaderAndVList.vue";
|
|
||||||
import useQueueStore from "@/stores/queue";
|
import useQueueStore from "@/stores/queue";
|
||||||
import useSearchStore from "@/stores/search";
|
import useSearchStore from "@/stores/search";
|
||||||
|
import { isMedium, isSmall } from "@/stores/content-width";
|
||||||
|
import SongItem from "@/components/shared/SongItem.vue";
|
||||||
|
|
||||||
const search = useSearchStore();
|
const search = useSearchStore();
|
||||||
const queue = useQueueStore();
|
const queue = useQueueStore();
|
||||||
@ -32,13 +45,9 @@ function playFromSearch(index: number) {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-list-layout {
|
#songlist-scroller{
|
||||||
width: 100%;
|
padding-right: 1rem;
|
||||||
}
|
padding-left: 0;
|
||||||
|
|
||||||
button {
|
|
||||||
width: fit-content;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user