rebuild search route with custom pages for tracks, album, and artists

This commit is contained in:
geoffrey45 2022-10-01 01:30:14 +03:00 committed by Mungai Njoroge
parent 264406aef4
commit 8e258eaf24
20 changed files with 240 additions and 60 deletions

View File

@ -86,17 +86,17 @@ button {
} }
} }
.btn-active { .btn-active {
background-image: linear-gradient($darkestblue, $darkblue); background-image: linear-gradient($darkestblue, $darkblue);
} }
.btn-disabled {
pointer-events: none;
opacity: 0.5;
}
.btn-more { .btn-more {
width: 2.5rem; width: 2.5rem;
svg {
transform: scale(1.25);
}
} }
.separator { .separator {

View File

@ -6,11 +6,13 @@
padding-right: 1rem; padding-right: 1rem;
} }
// applies to playlist list page
.content-page { .content-page {
margin-right: calc(0rem - ($medium + 4px)); margin-right: calc(0rem - ($medium + 4px));
padding-right: calc(1rem - 3px) !important; padding-right: calc(1rem - 3px) !important;
} }
// virtual scroller pages: folder, playlist, album
.header-list-layout { .header-list-layout {
margin-right: calc(0rem - ($medium + 4px)) !important; margin-right: calc(0rem - ($medium + 4px)) !important;
@ -22,4 +24,8 @@
#app-grid.noSidebar > #acontent { #app-grid.noSidebar > #acontent {
padding-right: 1rem !important; padding-right: 1rem !important;
} }
.search-view {
margin-right: -1rem !important;
}
} }

View File

@ -57,7 +57,7 @@ const menus = [
{ {
name: "search", name: "search",
route_name: Routes.search, route_name: Routes.search,
params: { page: "top" }, params: { page: "tracks" },
icon: SearchSvg, icon: SearchSvg,
}, },
{ {

View File

@ -1,14 +1,12 @@
<template> <template>
<div class="artists-results" v-auto-animate> <div class="artists-results" v-auto-animate>
<div <div
v-auto-animate
class="search-results-grid" class="search-results-grid"
v-if="album_grid == true && search.albums.value.length" v-if="album_grid == true && search.albums.value.length"
> >
<AlbumCard v-for="a in search.albums.value" :key="a.albumid" :album="a" /> <AlbumCard v-for="a in search.albums.value" :key="a.albumid" :album="a" />
</div> </div>
<div <div
v-auto-animate
class="search-results-grid" class="search-results-grid"
v-else-if="!album_grid && search.artists.value.length" v-else-if="!album_grid && search.artists.value.length"
> >

View File

@ -1,6 +1,6 @@
<template> <template>
<div id="tracks-results" class="noscroll"> <div id="tracks-results" class="noscroll">
<div v-if="search.tracks.value.length" v-auto-animate> <div v-if="search.tracks.value.length">
<TrackComponent <TrackComponent
v-for="(track, index) in search.tracks.value" v-for="(track, index) in search.tracks.value"
:key="track.trackid" :key="track.trackid"
@ -18,7 +18,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from "vue"; import { computed, onMounted } from "vue";
import SongItem from "@/components/shared/SongItem.vue"; import SongItem from "@/components/shared/SongItem.vue";
import TrackItem from "@/components/shared/TrackItem.vue"; import TrackItem from "@/components/shared/TrackItem.vue";
@ -51,4 +51,8 @@ let use_song_item: boolean = false;
if (props.isOnSearchPage) { if (props.isOnSearchPage) {
use_song_item = true; use_song_item = true;
} }
onMounted(() => {
search.switchTab("tracks");
});
</script> </script>

View File

@ -36,7 +36,7 @@
import SearchInput from "@/components/shared/NavSearchInput.vue"; import SearchInput from "@/components/shared/NavSearchInput.vue";
import { Routes } from "@/composables/enums"; import { Routes } from "@/composables/enums";
import { subPath } from "@/interfaces"; import { subPath } from "@/interfaces";
import { focusElem } from "@/utils"; import { focusElemByClass } from "@/utils";
import { onUpdated } from "vue"; import { onUpdated } from "vue";
defineProps<{ defineProps<{
@ -44,7 +44,7 @@ defineProps<{
}>(); }>();
onUpdated(() => { onUpdated(() => {
focusElem("inthisfolder"); focusElemByClass("inthisfolder");
}); });
</script> </script>

View File

@ -30,6 +30,7 @@ defineProps<{
gap: $small; gap: $small;
padding: $medium; padding: $medium;
border-radius: 1rem; border-radius: 1rem;
height: fit-content;
&:hover { &:hover {
background-color: $gray4; background-color: $gray4;

View File

@ -32,6 +32,7 @@ defineProps<{
padding: 1.2rem 1rem !important; padding: 1.2rem 1rem !important;
font-size: 0.9rem; font-size: 0.9rem;
font-weight: bolder; font-weight: bolder;
height: fit-content;
&:hover { &:hover {
background-color: $gray4; background-color: $gray4;

View File

@ -49,11 +49,12 @@ async function loadMoreTracks(index: number) {
return response.data; return response.data;
} }
async function loadMoreAlbums(index: number) { async function loadMoreAlbums(index: number, count: number) {
const response = await axios.get(loadMoreUrl, { const response = await axios.get(loadMoreUrl, {
params: { params: {
type: "albums", type: "albums",
index: index, index: index,
count: count,
}, },
}); });

View File

@ -1,5 +1,5 @@
// "local" | "remote" // "local" | "remote"
let mode = "local"; let mode = "remote";
export interface D<T = string> { export interface D<T = string> {
[key: string]: T; [key: string]: T;
@ -7,7 +7,7 @@ export interface D<T = string> {
const domains: D = { const domains: D = {
local: "http://localhost:", local: "http://localhost:",
remote: "http://10.5.71.115:", remote: "http://10.16.22.240:",
}; };
const ports = { const ports = {

View File

@ -46,7 +46,7 @@
updateQueue(t.data.index !== undefined ? t.data.index : t.index) updateQueue(t.data.index !== undefined ? t.data.index : t.index)
" "
/> />
<div class="bottom-padding" style="height: 64px"></div> <div class="page-bottom-padding" style="height: 64px"></div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -22,6 +22,8 @@ import { useDebounce } from "@vueuse/core";
function scrollOnLoad() { function scrollOnLoad() {
const elem = document.getElementById("tab-content") as HTMLElement; const elem = document.getElementById("tab-content") as HTMLElement;
if (elem === null) return;
elem.scroll({ elem.scroll({
top: elem.scrollHeight, top: elem.scrollHeight,
left: 0, left: 0,
@ -85,6 +87,7 @@ export default defineStore("search", () => {
} }
function fetchAlbums(query: string) { function fetchAlbums(query: string) {
console.log("fetching albums");
if (!query) return; if (!query) return;
searchAlbums(query).then((res) => { searchAlbums(query).then((res) => {
@ -92,6 +95,8 @@ 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) {
@ -117,11 +122,11 @@ export default defineStore("search", () => {
.then(() => scrollOnLoad()); .then(() => scrollOnLoad());
} }
function loadAlbums() { function loadAlbums(count = 6) {
loadCounter.albums += RESULT_COUNT; loadCounter.albums += count;
startLoading(); startLoading();
loadMoreAlbums(loadCounter.albums) loadMoreAlbums(loadCounter.albums, count)
.then((res) => { .then((res) => {
albums.value = [...albums.value, ...res.albums]; albums.value = [...albums.value, ...res.albums];
albums.more = res.more; albums.more = res.more;

View File

@ -1,5 +1,5 @@
import { focusElemByClass } from "@/utils";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { focusElem } from "@/utils";
const tablist = { const tablist = {
home: "home", home: "home",
@ -16,7 +16,7 @@ export default defineStore("tabs", {
changeTab(tab: string) { changeTab(tab: string) {
if (tab === this.tabs.queue) { if (tab === this.tabs.queue) {
setTimeout(() => { setTimeout(() => {
focusElem("currentInQueue"); focusElemByClass("currentInQueue");
}, 500); }, 500);
} }
this.current = tab; this.current = tab;

View File

@ -1,18 +1,19 @@
import focusElem from "./useFocusElem"; import createSubPaths from "./useCreateSubPaths";
import focusElemByClass from "./useFocusElem";
import formatSeconds from "./useFormatSeconds";
import useFuse from "./useFuse";
import { readLocalStorage, writeLocalStorage } from "./useLocalStorage";
import putCommas from "./usePutCommas"; import putCommas from "./usePutCommas";
import useVisibility from "./useVisibility"; import useVisibility from "./useVisibility";
import formatSeconds from "./useFormatSeconds";
import createSubPaths from "./useCreateSubPaths";
import { readLocalStorage, writeLocalStorage } from "./useLocalStorage";
import useFuse from "./useFuse";
export { export {
readLocalStorage, readLocalStorage,
writeLocalStorage, writeLocalStorage,
createSubPaths, createSubPaths,
focusElem, focusElemByClass,
useVisibility, useVisibility,
formatSeconds, formatSeconds,
putCommas, putCommas,
useFuse, useFuse,
}; };

View File

@ -0,0 +1,6 @@
const itemWidth = 160;
const itemMarginBottom = 24;
export default (containerWidth = 0, containerHeight = 0) => {
return Math.floor(containerWidth / itemWidth);
};

View File

@ -4,7 +4,7 @@
* @param delay Delay in milliseconds * @param delay Delay in milliseconds
* @param pos Positioning of the focus element * @param pos Positioning of the focus element
*/ */
export default function focusElem( export default function focusElemByClass(
className: string, className: string,
delay?: number, delay?: number,
pos?: any pos?: any

View File

@ -1,11 +1,30 @@
<template> <template>
<div class="search-albums-view"></div> <div class="search-albums-view grid-page" v-auto-animate>
<AlbumCard
v-for="album in search.albums.value"
:key="album.hash"
:album="album"
/>
</div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import AlbumCard from "@/components/shared/AlbumCard.vue";
import useSearchStore from "@/stores/search";
const search = useSearchStore();
</script>
<style lang="scss"> <style lang="scss">
.search-albums-view { // .search-albums-view.grid-page {
height: 100%; // max-height: 100%;
} // display: grid;
// grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
// gap: 1.75rem 0;
// padding-bottom: 4rem;
// padding-right: $medium;
// overflow: auto;
// }
</style> </style>

View File

@ -1,11 +1,19 @@
<template> <template>
<div class="search-artists-view"></div> <div v-auto-animate class="search-artists-view grid-page">
<ArtistCard
v-for="artist in search.artists.value"
:key="artist.image"
:artist="artist"
/>
</div>
</template> </template>
<script setup lang="ts"></script> <script setup lang="ts">
import ArtistCard from "@/components/shared/ArtistCard.vue";
import useSearchStore from "@/stores/search";
const search = useSearchStore();
</script>
<style lang="scss"> <style lang="scss">
.search-artists-view {
height: 100%;
}
</style> </style>

View File

@ -1,20 +1,48 @@
<template> <template>
<div class="search-view"> <div class="search-view">
<div class="tabs"> <div class="tabs">
<button v-for="page in pages">{{ page }}</button> <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>
<div class="noscroll"> <div ref="page" class="page noscroll" v-auto-animate>
<component :is="getComponent()" /> <component :is="component" />
</div> </div>
<button
class="load-more"
:class="{ 'btn-disabled': !canLoadMore }"
@click="loadMore"
>
Load more
</button>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { computed, onMounted, ref } from "vue";
import TracksPage from "./tracks.vue"; import TracksPage from "./tracks.vue";
import AlbumPage from "./albums.vue"; import AlbumPage from "./albums.vue";
import ArtistPage from "./artists.vue"; import ArtistPage from "./artists.vue";
import { Routes } from "@/composables/enums";
import useSearchStore from "@/stores/search";
import { focusElemByClass } from "@/utils";
// width of album and artist cards
const defaultItemCount = 6;
const gridItemWidth = 160;
const page = ref<HTMLElement>();
const search = useSearchStore();
enum pages { enum pages {
tracks = "tracks", tracks = "tracks",
@ -23,10 +51,9 @@ enum pages {
} }
const route = useRoute(); const route = useRoute();
const page = route.params.page as string;
function getComponent() { const component = computed(() => {
switch (page) { switch (route.params.page) {
case pages.tracks: case pages.tracks:
return TracksPage; return TracksPage;
case pages.albums: case pages.albums:
@ -38,22 +65,101 @@ function getComponent() {
default: default:
return TracksPage; return TracksPage;
} }
});
function loadTracks() {
search.loadTracks();
focusElemByClass("page-bottom-padding", 100);
} }
function getGridRowItemCount() {
if (page.value?.offsetWidth === undefined) return defaultItemCount;
const page_width = page.value?.offsetWidth - 16;
return Math.floor(page_width / gridItemWidth);
}
function scrollToGridPageBottom() {
const elem = document.getElementsByClassName("grid-page")[0] as HTMLElement;
setTimeout(() => {
elem.scrollTo(0, elem.scrollHeight);
}, 250);
// const elemWidth = elem.offsetWidth;
// console.log(Math.floor(elemWidth / 160));
// elem.scroll({
// top: elem.scrollHeight,
// behavior: "smooth",
// });
}
function loadAlbums() {
scrollToGridPageBottom();
setTimeout(() => {
// search.loadAlbums();
const itemCount = getGridRowItemCount();
search.loadAlbums(itemCount);
scrollToGridPageBottom();
}, 250);
}
function loadArtists() {
// const itemCount = getGridRowItemCount();
search.loadArtists();
scrollToGridPageBottom();
}
function loadMore() {
switch (route.params.page) {
case pages.tracks:
loadTracks();
break;
case pages.albums:
loadAlbums();
break;
case pages.artists:
loadArtists();
break;
default:
break;
}
}
const canLoadMore = computed(() => {
switch (route.params.page) {
case pages.tracks:
return search.tracks.more;
case pages.albums:
return search.albums.more;
case pages.artists:
return search.artists.more;
default:
false;
}
});
onMounted(() => {
search.switchTab(route.params.page as string);
});
</script> </script>
<style lang="scss"> <style lang="scss">
.search-view { .search-view {
height: calc(100% - 1rem); height: calc(100% - 1rem);
width: calc(100% - $small);
display: grid; display: grid;
gap: 1rem;
grid-template-rows: max-content 1fr; grid-template-rows: max-content 1fr;
margin-right: -0.75rem;
.tabs { .tabs {
width: fit-content; width: fit-content;
display: flex; display: flex;
gap: 1rem; gap: 1rem;
// margin: 0 auto;
margin-bottom: 1rem;
& > * { & > * {
background-color: $gray4; background-color: $gray4;
@ -62,5 +168,26 @@ function getComponent() {
text-transform: capitalize; text-transform: capitalize;
} }
} }
.page.noscroll {
overflow-x: visible;
}
.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: 1rem;
overflow: auto;
}
button.load-more {
width: 10rem;
margin: 0 auto;
}
} }
</style> </style>

View File

@ -3,7 +3,6 @@
<div class="noscroll"> <div class="noscroll">
<Layout :no_header="true" :tracks="search.tracks.value" /> <Layout :no_header="true" :tracks="search.tracks.value" />
</div> </div>
<button @click.prevent="search.loadTracks">Load More</button>
</div> </div>
</template> </template>
@ -18,9 +17,13 @@ const search = useSearchStore();
.search-tracks-view { .search-tracks-view {
height: 100%; height: 100%;
display: grid; .noscroll {
grid-template-rows: 1fr max-content; height: 100%;
gap: 1rem; }
.header-list-layout {
width: 100%;
}
button { button {
width: fit-content; width: fit-content;