use tabs to seperate search results

This commit is contained in:
geoffrey45
2022-05-22 19:29:16 +03:00
parent 6ef725c0ae
commit 6a2b87b48c
19 changed files with 463 additions and 346 deletions
+17 -121
View File
@@ -1,152 +1,48 @@
<template>
<div class="right-search">
<Options />
<div class="scrollable rounded" ref="search_thing">
<TracksGrid
v-if="tracks.tracks.length"
:more="tracks.more"
:tracks="tracks.tracks"
:query="search.query"
@loadMore="loadMoreTracks"
/>
<div class="separator no-border" v-if="tracks.tracks.length"></div>
<AlbumGrid
v-if="albums.albums.length"
:albums="albums.albums"
:more="albums.more"
@loadMore="loadMoreAlbums"
/>
<div class="separator no-border" v-if="albums.albums.length"></div>
<ArtistGrid
v-if="artists.artists.length"
:artists="artists.artists"
:more="artists.more"
@loadMore="loadMoreArtists"
/>
<div
v-if="search.query.trim().length === 0"
class="no-res border rounded"
>
<div class="no-res-text">🦋 Find your music</div>
</div>
<div
v-else-if="
!artists.artists.length &&
!tracks.tracks.length &&
!albums.albums.length
"
class="no-res border rounded"
>
<div class="no-res-text">
No results for
<span class="highlight rounded">{{ search.query }}</span>
</div>
</div>
</div>
<TabsWrapper>
<Tab name="Tracks">
<TracksGrid />
</Tab>
<Tab name="Albums">
<AlbumGrid />
</Tab>
<Tab name="Artists">
<ArtistGrid />
</Tab>
</TabsWrapper>
</div>
</template>
<script setup>
<script setup lang="ts">
import { reactive, ref } from "@vue/reactivity";
import state from "../../composables/state";
import searchMusic from "@/composables/searchMusic.js";
import useDebouncedRef from "@/composables/useDebouncedRef";
import AlbumGrid from "@/components/Search/AlbumGrid.vue";
import ArtistGrid from "@/components/Search/ArtistGrid.vue";
import TracksGrid from "@/components/Search/TracksGrid.vue";
import Options from "@/components/Search/Options.vue";
import loadMore from "../../composables/loadmore";
import useSearchStore from "../../stores/gsearch";
import useTabStore from "../../stores/tabs";
import TabsWrapper from "./TabsWrapper.vue";
import Tab from "./Tab.vue";
import TracksGrid from "./Search/TracksGrid.vue";
import AlbumGrid from "./Search/AlbumGrid.vue";
import "@/assets/css/Search/Search.scss";
import ArtistGrid from "./Search/ArtistGrid.vue";
const search = useSearchStore();
const tabs = useTabStore();
const search_thing = ref(null);
const tracks = reactive({
tracks: [],
more: false,
});
let albums = reactive({
albums: [],
more: false,
});
const artists = reactive({
artists: [],
more: false,
});
function scrollSearchThing() {
search_thing.value.scroll({
top: search_thing.value.scrollTop + 330,
left: 0,
behavior: "smooth",
});
}
function loadMoreTracks(start) {
scrollSearchThing();
loadMore.loadMoreTracks(start).then((response) => {
tracks.tracks = [...tracks.tracks, ...response.tracks];
tracks.more = response.more;
});
}
function loadMoreAlbums(start) {
loadMore.loadMoreAlbums(start).then((response) => {
albums.albums = [...albums.albums, ...response.albums];
albums.more = response.more;
});
}
function loadMoreArtists(start) {
scrollSearchThing();
loadMore.loadMoreArtists(start).then((response) => {
artists.artists = [...artists.artists, ...response.artists];
artists.more = response.more;
});
}
search.$subscribe((mutation, state) => {
if (state.query.trim() == "") {
tracks.tracks = [];
albums.albums = [];
artists.artists = [];
return;
}
searchMusic(state.query).then((res) => {
if (tabs.current !== tabs.tabs.search) {
tabs.switchToSearch();
}
albums.albums = res.albums.albums;
albums.more = res.albums.more;
artists.artists = res.artists.artists;
artists.more = res.artists.more;
tracks.tracks = res.tracks.tracks;
tracks.more = res.tracks.more;
});
});
</script>
<style lang="scss">
.right-search {
position: relative;
display: grid;
grid-template-rows: min-content 1fr;
overflow: hidden;
width: auto;
height: 100%;
padding: $small $small 0 0;
.no-res {
text-align: center;
@@ -1,36 +1,29 @@
<template>
<div class="albums-results border">
<div class="heading">Albums</div>
<div class="grid">
<AlbumCard v-for="album in albums" :key="album" :album="album" />
<AlbumCard
v-for="album in search.albums.value"
:key="album.image"
:album="album"
/>
</div>
<LoadMore v-if="more" @loadMore="loadMore" />
<LoadMore v-if="search.albums.more" @loadMore="loadMore()" />
</div>
</template>
<script>
import AlbumCard from "@/components/shared/AlbumCard.vue";
<script setup lang="ts">
import AlbumCard from "../../shared/AlbumCard.vue";
import LoadMore from "./LoadMore.vue";
import useSearchStore from "../../../stores/search";
export default {
props: ["albums", "more"],
components: {
AlbumCard,
LoadMore,
},
setup(props, { emit }) {
let counter = 0;
const search = useSearchStore();
function loadMore() {
counter += 6;
emit("loadMore", counter);
}
let counter = 0;
return {
loadMore,
};
},
};
function loadMore() {
counter += 6;
search.loadAlbums(counter);
}
</script>
<style lang="scss">
@@ -0,0 +1,45 @@
<template>
<div class="artists-results border">
<div class="grid">
<ArtistCard
v-for="artist in search.artists.value"
:key="artist.image"
:artist="artist"
/>
</div>
<LoadMore v-if="search.artists.more" @loadMore="loadMore" />
</div>
</template>
<script setup lang="ts">
import ArtistCard from "../../shared/ArtistCard.vue";
import LoadMore from "./LoadMore.vue";
import useSearchStore from "../../../stores/search";
const search = useSearchStore();
let counter = 0;
function loadMore() {
counter += 6;
search.loadArtists(counter);
}
</script>
<style lang="scss">
.right-search .artists-results {
border-radius: 0.5rem;
padding: $small;
margin-bottom: $small;
.xartist {
background-color: $gray;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
}
</style>
@@ -1,9 +1,8 @@
<template>
<div class="tracks-results border" v-if="tracks">
<div class="heading">Tracks</div>
<TransitionGroup class="items" name="list">
<div id="tracks-results" v-if="search.tracks.value">
<TransitionGroup name="list">
<TrackItem
v-for="track in tracks"
v-for="track in search.tracks.value"
:key="track.trackid"
:track="track"
:isPlaying="queue.playing"
@@ -12,45 +11,39 @@
@PlayThis="updateQueue"
/>
</TransitionGroup>
<LoadMore v-if="more" @loadMore="loadMore" />
<LoadMore v-if="search.tracks.more" @loadMore="loadMore" />
</div>
</template>
<script setup lang="ts">
import LoadMore from "./LoadMore.vue";
import TrackItem from "../shared/TrackItem.vue";
import useQStore from "../../stores/queue";
import { Track } from "../../interfaces";
import TrackItem from "../../shared/TrackItem.vue";
import useQStore from "../../../stores/queue";
import { Track } from "../../../interfaces";
import useSearchStore from "../../../stores/search";
let counter = 0;
const queue = useQStore();
const props = defineProps<{
tracks: Track[];
more: boolean;
query: string;
}>();
const emit = defineEmits(["loadMore"]);
const search = useSearchStore();
function loadMore() {
counter += 5;
console.log("load more", counter);
emit("loadMore", counter);
search.loadTracks(counter);
}
function updateQueue(track: Track) {
console.log(props.query);
queue.playFromSearch(props.query, props.tracks);
queue.playFromSearch(search.query, search.tracks.value);
queue.play(track);
}
</script>
<style lang="scss">
.right-search .tracks-results {
.right-search #tracks-results {
border-radius: 0.5rem;
padding: $small;
height: 100% !important;
overflow: hidden;
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
+2 -23
View File
@@ -1,6 +1,5 @@
<template>
<div class="gsearch-input">
<Filters :filters="search.filters" @removeFilter="removeFilter" />
<div class="input-loader">
<input
id="search"
@@ -8,36 +7,16 @@
v-model="search.query"
placeholder="Search your library"
type="text"
@keyup.backspace="removeLastFilter"
/>
</div>
</div>
</template>
<script setup>
import Filters from "../Search/Filters.vue";
import Loader from "../shared/Loader.vue";
import useSearchStore from "../../stores/gsearch";
<script setup lang="ts">
import useSearchStore from "../../stores/search";
const search = useSearchStore();
function removeFilter(filter) {
search.removeFilter(filter);
}
let counter = 0;
function removeLastFilter() {
if (search.query === "") {
counter++;
if (counter > 0) {
search.removeLastFilter();
}
} else {
counter = 0;
}
}
</script>
<style lang="scss">
+15
View File
@@ -0,0 +1,15 @@
<template>
<div v-show="name == selectedTab">
<slot />
</div>
</template>
<script setup lang="ts">
import { inject } from "vue";
defineProps<{
name: string;
}>();
const selectedTab = inject("currentTab");
</script>
@@ -0,0 +1,54 @@
<template>
<div id="right-tabs">
<div id="tabheaders">
<div
class="tab rounded"
v-for="slot in $slots.default()"
:key="slot.key"
@click="currentTab = slot.props.name"
>
{{ slot.props.name }}
</div>
</div>
<div id="tab-content">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, provide } from "vue";
const currentTab = ref("Tracks");
provide("currentTab", currentTab);
</script>
<style lang="scss">
#right-tabs {
height: 100%;
margin-right: $small;
display: grid;
grid-template-rows: min-content 1fr;
#tabheaders {
border: solid 1px rgb(0, 68, 255);
display: flex;
gap: $small;
margin: $small 0;
.tab {
background-color: $gray3;
padding: $small;
text-transform: capitalize;
}
}
#tab-content {
height: 100%;
overflow: auto;
border-radius: $small;
background-color: $gray;
// overflow: hidden;
}
}
</style>
-52
View File
@@ -1,52 +0,0 @@
<template>
<div class="artists-results border">
<div class="heading">Artists</div>
<div class="grid">
<ArtistCard v-for="artist in artists" :key="artist" :artist="artist" />
</div>
<LoadMore v-if="more" @loadMore="loadMore" />
</div>
</template>
<script>
import ArtistCard from "@/components/shared/ArtistCard.vue";
import LoadMore from "./LoadMore.vue";
export default {
props: ["artists", "more"],
components: {
ArtistCard,
LoadMore,
},
setup(props, { emit }) {
let counter = 0;
function loadMore() {
counter += 6;
emit("loadMore", counter);
}
return {
loadMore,
};
},
};
</script>
<style lang="scss">
.right-search .artists-results {
border-radius: 0.5rem;
padding: $small;
margin-bottom: $small;
.xartist {
background-color: $gray;
}
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
}
</style>
+1 -1
View File
@@ -17,7 +17,7 @@ const imguri = paths.images.artist;
defineProps<{
artist: any;
color: string;
color?: string;
}>();
</script>
-1
View File
@@ -97,7 +97,6 @@ const playThis = (track: Track) => {
}
.track-item {
width: 26.55rem;
display: flex;
align-items: center;
border-radius: 0.5rem;
-42
View File
@@ -1,42 +0,0 @@
import axios from "axios";
const url = "http://127.0.0.1:9876/search/loadmore";
async function loadMoreTracks(start) {
const response = await axios.get(url, {
params: {
type: "tracks",
start: start,
},
});
return response.data;
}
async function loadMoreAlbums(start) {
const response = await axios.get(url, {
params: {
type: "albums",
start: start,
},
});
return response.data;
}
async function loadMoreArtists(start) {
const response = await axios.get(url, {
params: {
type: "artists",
start: start,
},
});
return response.data;
}
export default {
loadMoreTracks,
loadMoreAlbums,
loadMoreArtists,
};
-27
View File
@@ -1,27 +0,0 @@
import state from "./state";
const base_url = `${state.settings.uri}/search?q=`;
async function search(query) {
state.loading.value = true;
const url = base_url + encodeURIComponent(query.trim());
const res = await fetch(url);
if (!res.ok) {
const message = `An error has occured: ${res.status}`;
throw new Error(message);
}
const data = await res.json();
state.loading.value = false;
return {
tracks: data.data[0],
albums: data.data[1],
artists: data.data[2],
};
}
export default search;
+106
View File
@@ -0,0 +1,106 @@
import state from "./state";
import axios from "axios";
const base_url = `${state.settings.uri}/search`;
const uris = {
tracks: `${base_url}/tracks?q=`,
albums: `${base_url}/albums?q=`,
artists: `${base_url}/artists?q=`,
};
async function search(query: string) {
state.loading.value = true;
const url = base_url + encodeURIComponent(query.trim());
const res = await fetch(url);
if (!res.ok) {
const message = `An error has occured: ${res.status}`;
throw new Error(message);
}
const data = await res.json();
state.loading.value = false;
return {
tracks: data.data[0],
albums: data.data[1],
artists: data.data[2],
};
}
async function searchTracks(query: string) {
const url = uris.tracks + encodeURIComponent(query.trim());
const res = await fetch(url);
if (!res.ok) {
const message = `An error has occured: ${res.status}`;
throw new Error(message);
}
const data = await res.json();
return data;
}
async function searchAlbums(query: string) {
const url = uris.albums + encodeURIComponent(query.trim());
const res = await axios.get(url);
return res.data;
}
async function searchArtists(query: string) {
const url = uris.artists + encodeURIComponent(query.trim());
const res = await axios.get(url);
return res.data;
}
const url = state.settings.uri + "/search/loadmore";
async function loadMoreTracks(index: number) {
const response = await axios.get(url, {
params: {
type: "tracks",
index: index,
},
});
return response.data;
}
async function loadMoreAlbums(index: number) {
const response = await axios.get(url, {
params: {
type: "albums",
index: index,
},
});
return response.data;
}
async function loadMoreArtists(index: number) {
const response = await axios.get(url, {
params: {
type: "artists",
index: index,
},
});
return response.data;
}
export {
searchTracks,
searchAlbums,
searchArtists,
loadMoreTracks,
loadMoreAlbums,
loadMoreArtists,
};
+37 -21
View File
@@ -1,33 +1,49 @@
import {customRef, ref} from 'vue'
import { customRef, ref } from "vue";
/**
* Debounces a function
*
* @param {*} fn The function to debounce
* @param {*} delay The delay in milliseconds
* @param {*} immediate whether to debounce immediately
* @returns {Function} The debounced function
*/
const debounce = (fn, delay = 0, immediate = false) => {
let timeout
let timeout;
return (...args) => {
if (immediate && !timeout) fn(...args)
clearTimeout(timeout)
if (immediate && !timeout) fn(...args);
clearTimeout(timeout);
timeout = setTimeout(() => {
fn(...args)
}, delay)
}
}
fn(...args);
}, delay);
};
};
const useDebouncedRef = (initialValue, delay, immediate) => {
const state = ref(initialValue)
/**
* Emits the ref updated value after the given delay.
*
* @param {*} initialValue The default value of the ref
* @param {*} delay The delay in milliseconds
* @param {*} immediate Whether to call the function immediately
* @returns {Object} The ref and a function to call to update the ref
*/
const useDebouncedRef = (initialValue, delay, immediate = false) => {
const state = ref(initialValue);
return customRef((track, trigger) => ({
get() {
track()
return state.value
track();
return state.value;
},
set: debounce(
value => {
state.value = value
trigger()
},
delay,
immediate
(value) => {
state.value = value;
trigger();
},
delay,
immediate
),
}))
}
}));
};
export default useDebouncedRef
export default useDebouncedRef;
+22 -14
View File
@@ -12,7 +12,8 @@ import {
import notif from "../composables/mediaNotification";
import { FromOptions } from "../composables/enums";
function addQToLocalStorage(
function writeQueue(
from: fromFolder | fromAlbum | fromPlaylist,
tracks: Track[]
) {
@@ -25,11 +26,11 @@ function addQToLocalStorage(
);
}
function addCurrentToLocalStorage(track: Track) {
function writeCurrent(track: Track) {
localStorage.setItem("current", JSON.stringify(track));
}
function readCurrentFromLocalStorage(): Track {
function readCurrent(): Track {
const current = localStorage.getItem("current");
if (current) {
return JSON.parse(current);
@@ -114,7 +115,7 @@ export default defineStore("Queue", {
}
}
},
readQueueFromLocalStorage() {
readQueue() {
const queue = localStorage.getItem("queue");
if (queue) {
@@ -123,7 +124,7 @@ export default defineStore("Queue", {
this.tracks = parsed.tracks;
}
this.updateCurrent(readCurrentFromLocalStorage());
this.updateCurrent(readCurrent());
},
updateCurrent(track: Track) {
this.current = track;
@@ -131,7 +132,7 @@ export default defineStore("Queue", {
this.updateNext(this.current);
this.updatePrev(this.current);
addCurrentToLocalStorage(track);
writeCurrent(track);
},
updateNext(track: Track) {
const index = this.tracks.findIndex(
@@ -161,8 +162,9 @@ export default defineStore("Queue", {
},
setNewQueue(tracklist: Track[]) {
if (this.tracks !== tracklist) {
this.tracks = tracklist;
addQToLocalStorage(this.from, this.tracks);
this.tracks = [];
this.tracks.push(...tracklist);
writeQueue(this.from, this.tracks);
}
},
playFromFolder(fpath: string, tracks: Track[]) {
@@ -201,7 +203,8 @@ export default defineStore("Queue", {
},
addTrackToQueue(track: Track) {
this.tracks.push(track);
addQToLocalStorage(this.from, this.tracks);
writeQueue(this.from, this.tracks);
this.updateNext(this.current);
},
playTrackNext(track: Track) {
const Toast = useNotifStore();
@@ -209,19 +212,24 @@ export default defineStore("Queue", {
(t: Track) => t.trackid === this.current.trackid
);
const next: Track = this.tracks[currentid + 1];
if (currentid == this.tracks.length - 1) {
this.tracks.push(track);
} else {
const next: Track = this.tracks[currentid + 1];
if (next.trackid === track.trackid) {
Toast.showNotification("Track is already queued", NotifType.Info);
return;
if (next.trackid === track.trackid) {
Toast.showNotification("Track is already queued", NotifType.Info);
return;
}
}
this.tracks.splice(currentid + 1, 0, track);
this.updateNext(this.current);
Toast.showNotification(
`Added ${track.title} to queue`,
NotifType.Success
);
addQToLocalStorage(this.from, this.tracks);
writeQueue(this.from, this.tracks);
},
},
});
+134
View File
@@ -0,0 +1,134 @@
import { ref, reactive } from "@vue/reactivity";
import { defineStore } from "pinia";
import { AlbumInfo, Artist, Track } from "../interfaces";
import {
searchTracks,
searchAlbums,
searchArtists,
loadMoreTracks,
loadMoreAlbums,
loadMoreArtists,
} from "../composables/searchMusic";
import { watch } from "vue";
import useDebouncedRef from "../composables/useDebouncedRef";
/**
*
* @param id The id of the element of the div to scroll
* Scrolls on clicking the loadmore button
*/
function scrollOnLoad(id: string) {
const elem = document.getElementById(id);
elem.scroll({
top: elem.scrollHeight,
left: 0,
behavior: "smooth",
});
}
export default defineStore("search", () => {
const query = useDebouncedRef("", 600);
const tracks = reactive({
value: <Track[]>[],
more: false,
});
const albums = reactive({
value: <AlbumInfo[]>[],
more: false,
});
const artists = reactive({
value: <Artist[]>[],
more: false,
});
/**
* Searches for tracks, albums and artists
* @param query query to search for
*/
function search(query: string) {
searchTracks(query).then((res) => {
tracks.value = res.tracks;
tracks.more = res.more;
});
searchAlbums(query).then((res) => {
albums.value = res.albums;
albums.more = res.more;
});
searchArtists(query).then((res) => {
artists.value = res.artists;
artists.more = res.more;
});
}
/**
* Loads more search tracks results
*
* @param index The starting index of the tracks to load
*/
function loadTracks(index: number) {
loadMoreTracks(index)
.then((res) => {
tracks.value = [...tracks.value, ...res.tracks];
tracks.more = res.more;
})
.then(() => {
scrollOnLoad("tab-content");
});
}
/**
* Loads more search albums results
*
* @param index The starting index of the albums to load
*/
function loadAlbums(index: number) {
loadMoreAlbums(index)
.then((res) => {
albums.value = [...albums.value, ...res.albums];
albums.more = res.more;
})
.then(() => {
scrollOnLoad("tab-content");
});
}
/**
* Loads more search artists results
*
* @param index The starting index of the artists to load
*/
function loadArtists(index: number) {
loadMoreArtists(index)
.then((res) => {
artists.value = [...artists.value, ...res.artists];
artists.more = res.more;
})
.then(() => {
scrollOnLoad("tab-content");
});
}
watch(
() => query.value,
(newQuery) => {
search(newQuery);
}
);
return {
tracks,
albums,
artists,
query,
search,
loadTracks,
loadAlbums,
loadArtists,
};
});