mirror of
https://codeberg.org/Hyperpipe/Hyperpipe
synced 2025-06-27 20:58:01 +02:00
505 lines
12 KiB
Vue
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>
|