Hyperpipe/src/components/Search.vue
2023-07-16 10:48:52 +00:00

505 lines
12 KiB
Vue

<script setup>
import { ref, watch, onActivated, onUpdated, onDeactivated } from 'vue';
import Btn from './Btn.vue';
import SongItem from './SongItem.vue';
import AlbumItem from './AlbumItem.vue';
import {
getJsonPiped,
getPipedQuery,
useAuthRemovePlaylist,
} from '@/scripts/fetch.js';
import { useVerifyAuth, useRoute, useWrap, useShare } from '@/scripts/util.js';
import { useCreatePlaylist, useRemovePlaylist } from '@/scripts/db.js';
import { useResults, useArtist } from '@/stores/results.js';
import { useData } from '@/stores/player.js';
import { useNav, useI18n } from '@/stores/misc.js';
const { t } = useI18n(),
results = useResults(),
data = useData(),
nav = useNav(),
artist = useArtist();
const emit = defineEmits(['play-urls']),
filters = ['music_songs', 'music_albums', 'music_artists', 'music_playlists'],
filter = ref('music_songs'),
isSearch = ref(/search/.test(location.pathname)),
albumMenu = ref(false);
const shuffleAdd = () => {
const songs = results.items.songs.items.map(i => ({
url: i.url,
title: i.title,
thumbnails: [{ url: i.thumbnail }],
thumbnail: i.thumbnail,
offlineUri: i.offlineUri,
duration: i.duration,
})),
copy = [];
let nos = songs.length;
while (nos) {
const i = Math.floor(Math.random() * nos--);
copy.push(songs[i]);
songs[i] = songs[nos];
delete songs[nos];
}
emit('play-urls', copy);
},
openSong = (song, nxt = false) => {
if (results.items?.songs?.title && !nxt) {
data.state.urls = results.items.songs.items.map(i => ({
url: i.url || '/watch?v=' + song.id,
title: i.title,
thumbnails: [{ url: i.thumbnail }],
thumbnail: i.thumbnail,
offlineUri: i.offlineUri,
duration: i.duration,
}));
song.url = song.url || '/watch?v=' + song.id;
data.play(song);
} else {
emit('play-urls', [
{
url: song.url || '/watch?v=' + song.id,
title: song.title || song.name,
thumbnails: [
{
url:
song.thumbnail ||
song.thumbnails[1]?.url ||
song.thumbnails[0]?.url,
},
],
},
]);
}
},
removeSong = i => {
results.items.songs.items.splice(i, 1);
},
shareAlbum = () => {
const data = {
title: `View ${results.items?.songs?.title} on Hyperpipe`,
url:
location.origin +
(results.album.startsWith('/') ? '' : '/') +
results.album,
};
useShare(data);
},
saveAlbum = () => {
const urls = results.items?.songs?.items?.map(item => ({
url: item.url,
title: item.title,
}));
let title = results.items?.songs?.title;
if (title) {
if (title == 'Songs')
title += ' - ' + results.items.songs.items[0].uploaderName;
useCreatePlaylist(title, urls, () => {
alert('Saved!');
});
}
},
removePlaylist = async id => {
const consent = confirm('Confirm?');
console.log(id, consent);
if (!id || !consent) return;
console.log(id, consent);
if (useVerifyAuth(id)) {
const { message } = await useAuthRemovePlaylist(id);
if (message != 'ok') {
alert(message);
return;
}
} else useRemovePlaylist(id);
useRoute('/library');
nav.state.page = 'library';
},
getSearch = q => {
if (q) {
const pq = useWrap(q);
history.pushState({}, '', `/search/${pq + getPipedQuery()}`);
document.title = 'Search Results for ' + q;
isSearch.value = /search/.test(location.pathname);
getResults(pq);
} else {
results.resetItems();
useRoute('/');
document.title = 'Hyperpipe';
console.log('No Search');
}
},
loading = ref(false),
getNextPage = async () => {
if (
(!isSearch.value && !results.items?.songs?.title) ||
loading.value ||
results.next == 'null' ||
!results.next
)
return;
if (
window.innerHeight + window.scrollY >=
document.body.offsetHeight - window.innerHeight
) {
loading.value = true;
let items, key;
if (isSearch.value) {
const f = filter.value || 'music_songs';
const json = await getJsonPiped(
`/nextpage/search?nextpage=${encodeURIComponent(results.next)}&q=${
nav.state.search
}&filter=${f}`,
);
key = f.split('_')[1];
results.next = json.nextpage;
items = json.items;
} else {
const json = await getJsonPiped(`/nextpage/playlists/${results.next}`);
key = 'songs';
items = json.relatedStreams;
}
results.items[key].items.push(...items);
loading.value = false;
}
},
getResults = async q => {
results.resetItems();
const f = filter.value || 'music_songs',
json = await getJsonPiped(`/search?q=${q}&filter=${f}`),
key = f.split('_')[1];
results.next = json.nextpage;
results.setItem(key, json);
};
watch(
() => nav.state.search,
n => {
if (n) {
n = n.replace(location.search || '', '');
artist.reset();
getSearch(n);
}
},
);
onUpdated(() => {
isSearch.value = /search/.test(location.pathname);
});
onActivated(() => {
window.addEventListener('scroll', getNextPage);
});
onDeactivated(() => {
window.removeEventListener('scroll', getNextPage);
});
</script>
<template>
<div
v-if="results.items.songs && results.items.songs.corrected"
class="text-full">
Did you mean, "<span class="caps">{{
results.items?.songs?.suggestion
}}</span
>"!!
</div>
<div v-if="results.items?.songs?.title" class="text-full flex">
<Btn
@click="
$emit(
'play-urls',
results.items?.songs?.items?.map(item => ({
url: item.url,
title: item.title,
thumbnails: [{ url: item.thumbnail }],
thumbnail: item.thumbnail,
offlineUri: item.offlineUri,
duration: item.duration,
})),
)
" />
<Btn ico="three-dots" @click="albumMenu = !albumMenu">
<template #menu>
<Transition name="fade">
<div v-if="albumMenu" class="alb popup">
<button
class="bi bi-bookmark-plus clickable"
@click="saveAlbum"></button>
<button class="bi bi-share clickable" @click="shareAlbum"></button>
<button
class="bi bi-plus-lg clickable"
@click="
data.state.urls.push(
...results.items.songs.items.map(i => ({
url: i.url,
title: i.title,
thumbnails: [{ url: i.thumbnail }],
})),
)
"></button>
<button
class="bi bi-shuffle clickable"
@click="shuffleAdd"></button>
<button
v-if="results.items?.songs?.items?.[0]?.playlistId"
class="bi bi-trash3 clickable"
@click="
removePlaylist(results.items?.songs?.items?.[0]?.playlistId)
"></button>
</div>
</Transition>
</template>
</Btn>
<span class="ml-auto">{{ results.items?.songs?.title }}</span>
</div>
<div v-if="isSearch" class="filters">
<button
v-for="f in filters"
class="filter caps"
:key="f"
:data-active="f == filter"
@click="
filter = f;
getSearch(nav.state.search);
">
{{ t('title.' + f.split('_')[1]) }}
</button>
</div>
<div
v-if="results.items.songs && results.items.songs.items[0]"
class="search-wrap">
<h2 v-if="!isSearch">{{ t('title.songs') }}</h2>
<div class="grid">
<SongItem
v-for="(song, index) in results.items.songs.items"
:key="song.url || song.id"
:index="index"
:playlistId="song.playlistId"
:author="song.uploaderName || song.artist || song.subtitle"
:title="song.title || song.name"
:channel="
song.uploaderUrl || song.artistUrl || '/channel/' + song.subId
"
:play="song.url || '/watch?v=' + song.id"
:art="
song.thumbnail ||
song.thumbnails?.[1]?.url ||
song.thumbnails?.[0]?.url ||
'/1x1.png'
"
@remove="removeSong"
@open-song="openSong(song)"
@nxt-song="openSong(song, true)" />
</div>
<a
v-if="artist.state.playlistId"
@click.prevent="
results.getAlbum('/playlist?list=' + artist.state.playlistId)
"
class="more"
:href="'/playlist?list=' + artist.state.playlistId"
>{{ t('info.see_all') }}</a
>
</div>
<div
v-if="results.items.albums && results.items.albums.items[0]"
class="search-wrap">
<h2 v-if="!isSearch">{{ t('title.albums') }}</h2>
<div class="grid-3">
<AlbumItem
v-for="album in results.items.albums.items"
:key="album.url || album.id"
:author="album.uploaderName || album.subtitle"
:name="album.name || album.title"
:art="album.thumbnail || album.thumbnails[0].url"
@open-album="
results.getAlbum(album.url || '/playlist?list=' + album.id)
" />
</div>
<a
v-if="results.items.albums.more?.params"
@click.prevent="artist.getArtistNext('albums', results.items.albums.more)"
class="more"
>{{ t('info.see_all') }}</a
>
</div>
<div
v-if="results.items.playlists && results.items.playlists.items[0]"
class="search-wrap">
<h2>{{ t('title.playlists') }}</h2>
<div class="grid-3">
<AlbumItem
v-for="pl in results.items.playlists.items"
:key="pl.url"
:author="pl.videos + ' Songs • ' + pl.uploaderName"
:name="pl.name"
:art="pl.thumbnail"
@open-album="results.getAlbum(pl.url)" />
</div>
</div>
<div
v-if="results.items.singles && results.items.singles.items[0]"
class="search-wrap">
<h2>{{ t('title.singles') }}</h2>
<div class="grid-3">
<AlbumItem
v-for="single in results.items.singles.items"
:key="single.id"
:author="single.subtitle"
:name="single.title"
:art="single.thumbnails[0].url"
@open-album="results.getAlbum('/playlist?list=' + single.id)" />
</div>
<a
v-if="results.items.singles.more?.params"
@click.prevent="
artist.getArtistNext('singles', results.items.singles.more)
"
class="more"
>{{ t('info.see_all') }}</a
>
</div>
<div
v-if="
(results.items.recommendedArtists &&
results.items.recommendedArtists.items[0]) ||
(results.items.artists && results.items.artists.items[0])
"
class="search-wrap">
<h2 v-if="!isSearch">
{{
results.items.artists ? t('title.artists') : t('title.similar_artists')
}}
</h2>
<div class="grid-3 circle">
<AlbumItem
v-for="a in results.items.artists
? results.items.artists.items
: results.items.recommendedArtists.items"
:key="a.id || a.url"
:author="a.subtitle"
:name="a.name || a.title"
:art="a.thumbnail || a.thumbnails[0].url"
@open-album="
artist.getArtist(a.id || a.url.replace('/channel/', ''))
" />
</div>
</div>
</template>
<style scoped>
.search-wrap {
place-items: start center;
margin-bottom: 2rem;
}
.search-wrap h2 {
text-align: center;
}
.circle {
text-align: center;
}
:deep(.bi) {
margin-right: 0;
}
:deep(.bi-play) {
margin-right: 0.75rem;
}
:deep(.ml-auto) {
margin-left: auto;
}
.alb {
bottom: 2.5rem;
border-radius: 0.25rem;
box-shadow: -0.5rem 0.5rem 1rem var(--color-shadow);
}
.alb .bi {
padding: 0.5rem;
}
.filters {
max-width: 100%;
width: max-content;
margin: 0 auto 2rem;
overflow-x: auto;
white-space: nowrap;
}
.filter {
max-width: 200px;
margin: 0 0.25rem;
padding: 0.5rem 1rem;
font-size: 1.25rem;
border-radius: 0.25rem;
}
.filter:hover {
background: var(--color-background-mute);
}
.filter[data-active='true'] {
border-bottom: 0.125rem var(--color-text) solid;
border-radius: 0.25rem 0.25rem 0 0;
}
.text-full {
padding: 1rem;
font-size: 1.5rem;
text-align: center;
margin-bottom: 1rem;
}
.text-right {
text-align: right;
}
.more {
margin: 1.5rem 0.5rem;
font-weight: bold;
font-size: 1rem;
}
</style>