Switched to Pinia, Added Save for Albums

This commit is contained in:
Shiny Nematoda 2022-07-17 08:06:17 +00:00
parent a43b0907e2
commit f303f91108
No known key found for this signature in database
GPG key ID: 6506D50F5613A42D
25 changed files with 945 additions and 799 deletions

View file

@ -75,6 +75,7 @@ You can reach out to me personally on:
- PeerJS -> [MIT][peer] - PeerJS -> [MIT][peer]
- Bootstrap Icons -> [MIT][bi] - Bootstrap Icons -> [MIT][bi]
- VueJS theme -> [MIT][vuetheme] - VueJS theme -> [MIT][vuetheme]
- Dracula theme -> [MIT][dracula]
- Nord theme -> [MIT][nord] - Nord theme -> [MIT][nord]
### Similar Projects ### Similar Projects
@ -95,3 +96,4 @@ You can reach out to me personally on:
[hls]: https://github.com/video-dev/hls.js/blob/master/LICENSE [hls]: https://github.com/video-dev/hls.js/blob/master/LICENSE
[nord]: https://github.com/arcticicestudio/nord/blob/develop/LICENSE.md [nord]: https://github.com/arcticicestudio/nord/blob/develop/LICENSE.md
[vuetheme]: https://github.com/vuejs/theme/blob/main/LICENSE [vuetheme]: https://github.com/vuejs/theme/blob/main/LICENSE
[dracula]: https://github.com/dracula/dracula-theme/blob/master/LICENSE

View file

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" /> <link rel="icon" href="/favicon.svg" />
<link rel="preconnect" href="https://pipedapi.kavin.rocks" /> <link rel="preconnect" href="https://pipedapi.kavin.rocks" />
<link rel="preconnect" href="https://cdn.jsdelivr.net" /> <link rel="preconnect" href="https://cdn.jsdelivr.net" />
<link rel="preconnect" href="https://hyperpipeapi.onrender.com" /> <link rel="preconnect" href="https://hyperpipeapi.onrender.com" />

782
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"hls.js": "^1.1.5", "hls.js": "^1.1.5",
"pinia": "^2.0.16",
"vue": "^3.2.31" "vue": "^3.2.31"
}, },
"devDependencies": { "devDependencies": {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

5
public/favicon.svg Normal file
View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#ebebeba3" class="bi bi-vinyl" viewBox="0 0 16 16">
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/>
<path d="M8 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4zM4 8a4 4 0 1 1 8 0 4 4 0 0 1-8 0z"/>
<path d="M9 8a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 340 B

View file

@ -3,6 +3,12 @@
"short_name": "Hyperpipe", "short_name": "Hyperpipe",
"start_url": "/", "start_url": "/",
"display": "standalone", "display": "standalone",
"background_color": "#fff", "background_color": "#000",
"description": "Privacy respecting YouTube Music Frontend." "description": "Privacy respecting YouTube Music Frontend.",
"icons": [
{
"src": "/favicon.svg",
"sizes": "any"
}
]
} }

View file

@ -1,60 +1,39 @@
<script setup> <script setup>
/* Imports */ /* Imports */
import Hls from 'hls.js'; import Hls from 'hls.js';
import { ref, reactive, onBeforeMount, onMounted } from 'vue'; import { ref, watch, reactive, onBeforeMount, onMounted } from 'vue';
/* Components */ /* Components */
import NavBar from './components/NavBar.vue'; import NavBar from '@/components/NavBar.vue';
import StatusBar from './components/StatusBar.vue'; import StatusBar from '@/components/StatusBar.vue';
import NowPlaying from './components/NowPlaying.vue'; import NowPlaying from '@/components/NowPlaying.vue';
import Genres from './components/Genres.vue'; import Genres from '@/components/Genres.vue';
import Search from './components/Search.vue'; import Search from '@/components/Search.vue';
import NewPlaylist from './components/NewPlaylist.vue'; import NewPlaylist from '@/components/NewPlaylist.vue';
import Playlists from './components/Playlists.vue'; import Playlists from '@/components/Playlists.vue';
import Lyrics from './components/Lyrics.vue'; import Lyrics from '@/components/Lyrics.vue';
import Info from './components/Info.vue'; import Info from '@/components/Info.vue';
import Artist from './components/Artist.vue'; import Artist from '@/components/Artist.vue';
import Prefs from './components/Prefs.vue'; import Prefs from '@/components/Prefs.vue';
/* Composables */ /* Composables */
import { getJson, getJsonPiped } from './scripts/fetch.js'; import { getJson, getJsonPiped } from '@/scripts/fetch.js';
import { useLazyLoad, useStore } from './scripts/util.js'; import { useLazyLoad, useStore, useRoute } from '@/scripts/util.js';
import { useSetupDB, useUpdatePlaylist } from './scripts/db.js'; import { useSetupDB, useUpdatePlaylist } from '@/scripts/db.js';
/* Reactivity */ /* Stores */
const data = reactive({ import { useData, usePlayer } from '@/stores/player.js';
artUrl: '', import { useResults, useArtist } from '@/stores/results.js';
cover: '', import { useNav } from '@/stores/misc.js';
audioSrc: [],
url: '',
urls: [],
songItems: null,
items: {},
title: '',
description: '',
artist: '',
artistUrl: '',
state: 'play',
duration: 0,
time: 0,
showplaylist: false,
showlyrics: false,
showinfo: false,
loop: false,
lyrics: '',
});
const artist = reactive({ const store = useStore(),
playlistId: null, data = useData(),
title: null, player = usePlayer(),
description: null, results = useResults(),
subscriberCount: 0, artist = useArtist(),
thumbnails: [], nav = useNav();
});
const search = ref(''), const genreid = ref(''),
genreid = ref(''),
page = ref('home'),
path = ref(location.pathname); path = ref(location.pathname);
const audio = ref(null); const audio = ref(null);
@ -73,8 +52,8 @@ function parseUrl() {
getExplore(); getExplore();
break; break;
case 'search': case 'search':
search.value = loc[2]; nav.state.search = loc[2];
console.log(search.value); console.log(nav.state.search);
break; break;
case 'watch': case 'watch':
getSong(loc[1] + location.search); getSong(loc[1] + location.search);
@ -85,56 +64,44 @@ function parseUrl() {
console.log(loc[1]); console.log(loc[1]);
break; break;
case 'channel': case 'channel':
getArtist(loc[1]); getArtist(loc[2]);
console.log(loc[1]); console.log(loc[2]);
break; break;
case 'explore': case 'explore':
genreid.value = loc[2]; genreid.value = loc[2];
page.value = 'genres'; nav.state.page = 'genres';
default: default:
console.log(loc); console.log(loc);
} }
} }
function Toggle(e) {
console.log(e, data[e]);
data[e] = !data[e];
}
function timeUpdate(t) {
data.time = Math.floor((t / data.duration) * 100);
}
function setTime(t) { function setTime(t) {
audio.value.currentTime = (t / 100) * data.duration; audio.value.currentTime = (t / 100) * player.state.duration;
} }
function addSong(s) { function addSong(s) {
data.urls.push(s); data.state.urls.push(s);
const index = data.urls.map(s => s.url).indexOf(data.url); const index = data.state.urls.map(s => s.url).indexOf(data.state.url);
if ( if (
(index == data.urls.length - 1 && data.time > 98) || (index == data.state.urls.length - 1 && player.state.time > 98) ||
data.urls.length == 1 data.state.urls.length == 1
) { ) {
console.log(true);
playNext(); playNext();
} else {
console.log(false);
} }
console.log(s, data.urls); console.log(s, data.state.urls);
} }
function playThis(t) { function playThis(t) {
const i = data.urls.indexOf(t); const i = data.state.urls.indexOf(t);
getSong(data.urls[i].url); getSong(data.state.urls[i].url);
} }
function playList(a) { function playList(a) {
data.urls = a; data.state.urls = a;
getSong(data.urls[0].url); getSong(data.state.urls[0].url);
} }
function playNext(u) { function playNext(u) {
@ -144,21 +111,21 @@ function playNext(u) {
audio.value.src = ''; audio.value.src = '';
const now = data.urls.filter(s => s.url === data.url)[0], const now = data.state.urls.filter(s => s.url === data.state.url)[0],
i = data.urls.indexOf(now), i = data.state.urls.indexOf(now),
next = data.urls[i + 1]; next = data.state.urls[i + 1];
console.log('Index: ' + i); console.log('Index: ' + i);
console.log(data.url, data.urls, next); console.log(data.state.url, data.state.urls, next);
if (data.urls.length > i && data.urls.length != 0 && next) { if (data.state.urls.length > i && data.state.urls.length != 0 && next) {
getSong(next.url); getSong(next.url);
} else if (data.loop) { } else if (player.state.loop) {
console.log(data.url, data.urls[0]); console.log(data.state.url, data.state.urls[0]);
data.url = data.urls[0].url; data.state.url = data.state.urls[0].url;
getSong(data.urls[0].url); getSong(data.state.urls[0].url);
} else { } else {
data.urls = []; data.state.urls = [];
} }
} }
@ -167,9 +134,9 @@ async function getExplore() {
console.log(json); console.log(json);
data.items = {}; results.items.value = {};
data.items = { results.items.value = {
songs: json.trending, songs: json.trending,
albums: json.albums_and_singles, albums: json.albums_and_singles,
}; };
@ -183,14 +150,13 @@ async function getSong(e) {
console.log(json); console.log(json);
data.artUrl = json.thumbnailUrl; data.state.art = json.thumbnailUrl;
data.description = json.description; data.state.description = json.description;
data.cover = `--art: url(${json.thumbnailUrl});`; data.state.title = json.title;
data.nowtitle = json.title; data.state.artist = json.uploader.replace(' - Topic', '');
data.nowartist = json.uploader.split(' - ')[0]; data.state.artistUrl = json.uploaderUrl;
data.artistUrl = json.uploaderUrl; player.state.duration = json.duration;
data.duration = json.duration; data.state.url = e;
data.url = e;
await getNext(hash); await getNext(hash);
@ -208,16 +174,15 @@ async function getAlbum(e) {
console.log(json, json.relatedStreams); console.log(json, json.relatedStreams);
data.songItems = { results.resetItems();
results.setItem('songs', {
items: json.relatedStreams, items: json.relatedStreams,
title: json.name, title: json.name,
}; });
history.pushState({}, '', e); useRoute(e);
for (let i in artist) { artist.reset();
artist[i] = null;
}
} }
async function getArtist(e) { async function getArtist(e) {
@ -229,36 +194,40 @@ async function getArtist(e) {
console.log(json); console.log(json);
data.items = json.items; for (let i in json.items) {
data.items.notes = json.playlistId; results.setItem(i, { items: json.items[i] });
json.items = null;
for (let i in json) {
artist[i] = json[i];
} }
history.pushState({}, '', '/channel/' + e); console.log(results.items);
json.items = undefined;
artist.reset();
artist.set(json);
useRoute('/channel/' + e);
} }
async function getNext(hash) { async function getNext(hash) {
if ( if (
useStore().getItem('next') !== 'false' && store.getItem('next') !== 'false' &&
(!data.urls || (!data.state.urls ||
!data.urls.filter(s => s.url == data.url)[0] || !data.state.urls.filter(s => s.url == data.state.url)[0] ||
data.urls.length == 1) data.state.urls.length == 1)
) { ) {
const json = await getJson( const json = await getJson(
'https://hyperpipeapi.onrender.com/next/' + hash, 'https://hyperpipeapi.onrender.com/next/' + hash,
); );
data.lyrics = json.lyricsId; data.state.lyrics = json.lyricsId;
data.url = json.songs[0] data.state.url = json.songs[0]
? '/watch?v=' + json.songs[0].id ? '/watch?v=' + json.songs[0].id
: '/watch?v=' + hash; : '/watch?v=' + hash;
console.log(json); console.log(json);
data.urls = data.state.urls =
json.songs.length > 0 json.songs.length > 0
? json.songs.map(i => ({ ? json.songs.map(i => ({
...i, ...i,
@ -267,43 +236,29 @@ async function getNext(hash) {
id: undefined, id: undefined,
}, },
})) }))
: data.urls; : data.state.urls;
setMetadata(); setMetadata();
console.log(data.urls); console.log(data.state.urls);
} else { } else {
setMetadata(); setMetadata();
if (data.urls.length == 0) { if (data.state.urls.length == 0) {
data.urls = [ data.state.urls = [
{ {
title: nowtitle, title: nowtitle,
url: data.url, url: data.state.url,
}, },
]; ];
} }
} }
} }
function setVolume(vol) {
audio.value.volume = vol;
}
function playPause() {
if (audio.value.paused) {
audio.value.play();
data.state = 'pause';
} else {
audio.value.pause();
data.state = 'play';
}
}
function Stream(res) { function Stream(res) {
console.log(res); console.log(res);
if (Hls.isSupported() && useStore().hls !== 'false') { if (Hls.isSupported() && store.hls !== 'false') {
window.hls = new Hls(); window.hls = new Hls();
window.hls.attachMedia(audio.value); window.hls.attachMedia(audio.value);
@ -312,7 +267,7 @@ function Stream(res) {
window.hls.loadSource(res.hls); window.hls.loadSource(res.hls);
}); });
} else { } else {
data.audioSrc = res.stream; data.state.src = res.stream;
audio.value.load(); audio.value.load();
} }
} }
@ -320,24 +275,23 @@ function Stream(res) {
function audioCanPlay() { function audioCanPlay() {
useLazyLoad(); useLazyLoad();
audio.value.play().catch(err => { if (audio.value.paused) {
alert(err); player.toggle('play');
});
data.state = 'pause';
if (location.pathname != '/playlist') {
history.pushState({}, '', data.url);
} }
document.title = `Playing: ${data.nowtitle} by ${data.nowartist}`; if (location.pathname != '/playlist') {
useRoute(data.state.url);
}
document.title = `Playing: ${data.state.title} by ${data.state.artist}`;
} }
function SaveTrack(e) { function SaveTrack(e) {
useUpdatePlaylist( useUpdatePlaylist(
e, e,
{ {
url: data.url, url: data.state.url,
title: data.nowtitle, title: data.state.title,
}, },
e => { e => {
if (e === true) { if (e === true) {
@ -349,7 +303,7 @@ function SaveTrack(e) {
function setMetadata() { function setMetadata() {
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
const now = data.urls.filter(u => u.url === data.url)[0]; const now = data.state.urls.filter(u => u.url === data.state.url)[0];
let artwork = [], let artwork = [],
album = undefined; album = undefined;
@ -366,24 +320,44 @@ function setMetadata() {
}; };
}); });
} else { } else {
artwork = [{ src: data.artUrl, type: 'image/webp' }]; artwork = [{ src: data.state.art, type: 'image/webp' }];
} }
console.log(album, artwork); console.log(album, artwork);
} }
navigator.mediaSession.metadata = new MediaMetadata({ navigator.mediaSession.metadata = new MediaMetadata({
title: data.nowtitle, title: data.state.title,
artist: data.nowartist, artist: data.state.artist,
album: album, album: album,
artwork: artwork, artwork: artwork,
}); });
} }
} }
watch(
() => player.state.play,
() => {
if (audio.value.paused) {
audio.value
.play()
.then(() => {
player.state.state = 'pause';
})
.catch(err => {
alert(err);
player.state.state = 'play';
});
} else {
player.state.state = 'play';
audio.value.pause();
}
},
);
onBeforeMount(() => { onBeforeMount(() => {
if (useStore().theme) { if (store.theme) {
document.body.setAttribute('data-theme', useStore().theme); document.body.setAttribute('data-theme', store.theme);
} }
}); });
@ -404,25 +378,31 @@ onMounted(() => {
/* Alert User on close if url is present */ /* Alert User on close if url is present */
window.onbeforeunload = () => { window.onbeforeunload = () => {
if (data.url) { if (data.state.url) {
return 'Are you Sure?'; return 'Are you Sure?';
} }
}; };
/* Media Session Controls */ /* Media Session Controls */
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('play', playPause); navigator.mediaSession.setActionHandler('play', () => {
navigator.mediaSession.setActionHandler('pause', playPause); player.state.state = 'play';
});
navigator.mediaSession.setActionHandler('pause', () => {
player.state.state = 'pause';
});
navigator.mediaSession.setActionHandler('previoustrack', () => { navigator.mediaSession.setActionHandler('previoustrack', () => {
if (data.urls.length > 2) { if (data.state.urls.length > 2) {
const i = data.urls.map(s => s.url).indexOf(data.url); const i = data.state.urls.map(s => s.url).indexOf(data.state.url);
getSong(data.urls[i - 1].url); getSong(data.state.urls[i - 1].url);
} }
}); });
navigator.mediaSession.setActionHandler('nexttrack', () => { navigator.mediaSession.setActionHandler('nexttrack', () => {
if (data.urls.length > 2) { if (data.state.urls.length > 2) {
const i = data.urls.map(s => s.url).indexOf(data.url); const i = data.state.urls.map(s => s.url).indexOf(data.state.url);
getSong(data.urls[i + 1].url); getSong(data.state.urls[i + 1].url);
} }
}); });
} }
@ -437,108 +417,66 @@ onMounted(() => {
</script> </script>
<template> <template>
<NavBar <NavBar />
@update-search="
e => {
search = e;
}
"
@update-page="
e => {
page = e;
}
"
:search="search" />
<template v-if="artist && page == 'home'"> <template v-if="artist.state.title && nav.state.page == 'home'">
<Artist <Artist @playall="getAlbum" />
@playall="getAlbum"
:title="artist.title"
:desc="artist.description"
:subs="artist.subscriberCount"
:thumbs="artist.thumbnails"
:play="artist.playlistId" />
</template> </template>
<header v-if="!artist.title"> <header v-if="!artist.state.title">
<div v-if="data.cover" class="art bg-img" :style="data.cover"></div> <div v-show="data.state.art" class="art bg-img"></div>
<div class="wrapper"> <div class="wrapper">
<NowPlaying <NowPlaying @get-artist="getArtist" />
@get-artist="getArtist"
:title="data.nowtitle"
:artist="data.nowartist"
:artistUrl="data.artistUrl" />
</div> </div>
</header> </header>
<main class="placeholder"> <main class="placeholder">
<KeepAlive> <KeepAlive>
<Search <Search
v-if="page == 'home'" v-if="nav.state.page == 'home'"
@get-album="getAlbum" @get-album="getAlbum"
@get-artist="getArtist" @get-artist="getArtist"
@play-urls="playList" @play-urls="playList"
@add-song="addSong" @add-song="addSong" />
:items="data.items"
:songItems="data.songItems"
:search="search" />
</KeepAlive> </KeepAlive>
<KeepAlive> <KeepAlive>
<Genres <Genres
v-if="page == 'genres'" v-if="nav.state.page == 'genres'"
:id="genreid" :id="genreid"
@get-album=" @get-album="
e => { e => {
getAlbum(e); getAlbum(e);
page = 'home'; nav.state.page = 'home';
} }
" /> " />
</KeepAlive> </KeepAlive>
<NewPlaylist v-if="page == 'playlist'" @play-urls="playList" /> <NewPlaylist v-if="nav.state.page == 'playlist'" @play-urls="playList" />
<Prefs v-if="page == 'prefs'" /> <Prefs v-if="nav.state.page == 'prefs'" />
</main> </main>
<Playlists <Playlists v-if="player.state.playlist" @playthis="playThis" />
@playthis="playThis"
:url="data.url"
:urls="data.urls"
:show="data.showplaylist" />
<Lyrics <Lyrics v-if="player.state.lyrics" />
v-if="data.showlyrics"
:id="data.lyrics"
:curl="data.url"
:iniurl="data.urls[0]?.url" />
<Info v-if="data.showinfo" :text="data.description" /> <Info v-if="player.state.info" :text="data.state.description" />
<StatusBar <StatusBar @save="SaveTrack" @change-time="setTime" />
@play="playPause"
@vol="setVolume"
@toggle="Toggle"
@save="SaveTrack"
@change-time="setTime"
:state="data.state"
:time="data.time"
:show="data.showplaylist"
:lyrics="data.showlyrics"
:loop="data.loop" />
<audio <audio
id="audio" id="audio"
ref="audio" ref="audio"
:volume="useStore().vol ? useStore().vol / 100 : 1" :volume="player.state.vol"
@canplay="audioCanPlay" @canplay="audioCanPlay"
@timeupdate="timeUpdate($event.target.currentTime)" @timeupdate="player.setTime($event.target.currentTime)"
@ended="playNext" @ended="playNext"
autoplay> autoplay>
<source <source
v-if="useStore().getItem('hls') != 'false'" v-if="store.getItem('hls') != 'false'"
v-for="src in data.audioSrc" v-for="src in data.state.audioSrc"
:key="src.url" :key="src.url"
:src="src.url" :src="src.url"
:type="src.mimeType" /> :type="src.mimeType" />
@ -573,7 +511,9 @@ header {
height: 175px; height: 175px;
width: 175px; width: 175px;
} }
.bg-img {
--art: v-bind(`url(${data.state.art}) `);
}
img, img,
.card, .card,
.card-bg { .card-bg {

View file

@ -70,6 +70,22 @@ body[data-theme='nord'] {
--color-text: #d8dee9; --color-text: #d8dee9;
} }
body[data-theme='dracula'] {
--color-foreground: #bd93f9;
--color-background: #282a36;
--color-background-soft: #44475a;
--color-background-mute: #44475a;
--color-border: #44475a;
--color-border-hover: #44475a;
--color-shadow: #6272a4;
--color-scrollbar: #bd93f9;
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
*, *,
*::before, *::before,
*::after { *::after {

View file

@ -2,7 +2,10 @@
import { ref, onUpdated } from 'vue'; import { ref, onUpdated } from 'vue';
import PlayBtn from './PlayBtn.vue'; import PlayBtn from './PlayBtn.vue';
defineProps(['title', 'desc', 'subs', 'thumbs', 'play']); import { useArtist } from '@/stores/results.js';
const artist = useArtist();
defineEmits(['playall']); defineEmits(['playall']);
const show = ref(-1); const show = ref(-1);
@ -14,14 +17,21 @@ onUpdated(() => {
</script> </script>
<template> <template>
<div v-if="show == 0 && title" class="us-wrap"> <div v-if="show == 0 && artist.state.title" class="us-wrap">
<div class="bg-imgfill" :style="'--art: url(' + thumbs[1].url + ');'"></div> <div
class="bg-imgfill"
:style="'--art: url(' + artist.state.thumbnails[1].url + ');'"></div>
<div class="us-main"> <div class="us-main">
<h2>{{ title }}</h2> <h2>{{ artist.state.title }}</h2>
<p @click="$event.target.classList.toggle('more')">{{ desc }}</p> <p @click="$event.target.classList.toggle('more')">
{{ artist.state.description }}
</p>
<div class="us-playwrap"> <div class="us-playwrap">
<PlayBtn @click="$emit('playall', '/playlist?list=' + play)" /> <PlayBtn
<span class="us-box subs">{{ subs || 0 }}</span> @click="
$emit('playall', '/playlist?list=' + artist.state.playlistId)
" />
<span class="us-box subs">{{ artist.state.subscriberCount || 0 }}</span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -132,9 +132,9 @@ onMounted(get);
@media (min-width: 1024px) { @media (min-width: 1024px) {
.btn-grid { .btn-grid {
grid-template-columns: calc(100% / 5) calc(100% / 5) calc(100% / 5) calc( grid-template-columns:
100% / 5 calc(100% / 5) calc(100% / 5) calc(100% / 5) calc(100% / 5)
) calc(100% / 5); calc(100% / 5);
} }
} }
</style> </style>

View file

@ -10,7 +10,18 @@ const parse = d =>
<template> <template>
<TextModal> <TextModal>
<template #content> <template #content>
<pre>{{ parse(text.replaceAll('<br>', '\n')) }}</pre> <pre class="placeholder">{{
text ? parse(text.replaceAll('<br>', '\n')) : ''
}}</pre>
</template> </template>
</TextModal> </TextModal>
</template> </template>
<style scoped>
.placeholder:empty::before {
--ico: '\F3B9';
}
.placeholder:empty::after {
--text: 'No Information Available...';
}
</style>

View file

@ -1,38 +1,31 @@
<script setup> <script setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { getJson } from '../scripts/fetch.js'; import { getJson } from '@/scripts/fetch.js';
import { useData } from '@/stores/player.js';
import TextModal from './TextModal.vue'; import TextModal from './TextModal.vue';
const props = defineProps({ const data = useData(),
id: String,
curl: String,
iniurl: String,
}),
text = ref(''), text = ref(''),
source = ref(''), source = ref(''),
status = ref(false); status = ref(false);
console.log(props);
function get() { function get() {
status.value = false; status.value = false;
if (props.id && props.curl === props.iniurl) { if (data.state.lyrics && data.state.urls === data.state.urls[0]?.url) {
console.log(props.id); getJson(
'https://hyperpipeapi.onrender.com/browse/' + data.state.lyrics,
getJson('https://hyperpipeapi.onrender.com/browse/' + props.id).then( ).then(res => {
res => {
text.value = res.text; text.value = res.text;
source.value = res.source; source.value = res.source;
status.value = true; status.value = true;
}, });
); } else if (data.state.urls[0]?.url) {
} else if (props.curl) {
getJson( getJson(
'https://hyperpipeapi.onrender.com/next/' + 'https://hyperpipeapi.onrender.com/next/' +
props.curl.replace('/watch?v=', ''), data.state.urls[0]?.url.replace('/watch?v=', ''),
).then(next => { ).then(next => {
if (next.lyricsId) { if (next.lyricsId) {
getJson( getJson(
@ -50,7 +43,7 @@ function get() {
get(); get();
watch( watch(
() => props.curl, () => data.state.urls[0]?.url,
() => { () => {
get(); get();
}, },
@ -60,22 +53,24 @@ watch(
<template> <template>
<TextModal> <TextModal>
<template #content> <template #content>
<pre class="placeholder" :data-loaded="curl ? status : true">{{ <pre
text class="placeholder"
}}</pre> :data-loaded="data.state.urls[0]?.url ? status : true"
>{{ text }}</pre
>
<div>{{ source }}</div> <div>{{ source }}</div>
</template> </template>
</TextModal> </TextModal>
</template> </template>
<style scoped> <style scoped>
pre:empty::before { .placeholder:empty::before {
--ico: '\f3a5'; --ico: '\f3a5';
} }
pre[data-loaded='false']:empty::after { .placeholder[data-loaded='false']:empty::after {
--text: 'Fetching Lyrics...'; --text: 'Fetching Lyrics...';
} }
pre[data-loaded='true']:empty::after { .placeholder[data-loaded='true']:empty::after {
--text: 'No Lyrics...'; --text: 'No Lyrics...';
} }
</style> </style>

View file

@ -2,30 +2,9 @@
import { reactive } from 'vue'; import { reactive } from 'vue';
import SearchBar from '../components/SearchBar.vue'; import SearchBar from '../components/SearchBar.vue';
defineProps({ import { useNav } from '@/stores/misc.js';
search: String,
});
const emit = defineEmits(['update-page', 'update-search']), const nav = useNav();
page = reactive({
home: true,
playlist: false,
prefs: false,
genres: false,
});
const Toggle = p => {
for (let pg in page) {
page[pg] = false;
}
page[p] = true;
emit('update-page', p);
console.log(page[p], p);
},
home = () => {
history.pushState('', {}, '/');
};
</script> </script>
<template> <template>
@ -35,30 +14,24 @@ const Toggle = p => {
<div class="wrap"> <div class="wrap">
<span <span
class="nav-ico bi bi-house" class="nav-ico bi bi-house"
:data-active="page.home" :data-active="nav.state.page == 'home'"
@click="Toggle('home')"></span> @click="nav.state.page = 'home'"></span>
<span <span
class="nav-ico bi bi-compass" class="nav-ico bi bi-compass"
:data-active="page.genres" :data-active="nav.state.page == 'genres'"
@click="Toggle('genres')"></span> @click="nav.state.page = 'genres'"></span>
<span <span
class="nav-ico bi bi-collection" class="nav-ico bi bi-collection"
:data-active="page.playlist" :data-active="nav.state.page == 'playlist'"
@click="Toggle('playlist')"></span> @click="nav.state.page = 'playlist'"></span>
<span <span
class="nav-ico bi bi-gear" class="nav-ico bi bi-gear"
:data-active="page.prefs" :data-active="nav.state.page == 'prefs'"
@click="Toggle('prefs')"></span> @click="nav.state.page = 'prefs'"></span>
</div> </div>
<div class="wrap"> <div class="wrap">
<SearchBar <SearchBar />
:search="search"
@update-search="
e => {
$emit('update-search', e);
}
" />
</div> </div>
</nav> </nav>
</template> </template>

View file

@ -1,28 +1,20 @@
<script setup> <script setup>
defineProps({ import { useData } from '@/stores/player.js';
title: {
type: String, const data = useData();
default: '',
},
artist: {
type: String,
default: '',
},
artistUrl: {
type: String,
default: '',
},
});
defineEmits(['get-artist']); defineEmits(['get-artist']);
</script> </script>
<template> <template>
<div v-if="title && artist" class="wrap"> <div v-if="data.state.title && data.state.artist" class="wrap">
<h1>{{ title }}</h1> <h1>{{ data.state.title }}</h1>
<h3> <h3>
<a :href="artistUrl" @click.prevent="$emit('get-artist', artistUrl)">{{ <a
artist :href="data.state.artistUrl"
}}</a> @click.prevent="$emit('get-artist', data.state.artistUrl)"
>{{ data.state.artist }}</a
>
</h3> </h3>
</div> </div>
</template> </template>

View file

@ -1,13 +1,16 @@
<script setup> <script setup>
defineProps(['ico']);
defineEmits(['click']); defineEmits(['click']);
</script> </script>
<template> <template>
<button class="bi bi-play" @click="$emit('click')"></button> <button
:class="'bi bi-' + (ico ? ico : 'play')"
@click="$emit('click')"></button>
</template> </template>
<style scoped> <style scoped>
button { .bi {
height: 4rem; height: 4rem;
width: 4rem; width: 4rem;
font-size: 4rem; font-size: 4rem;
@ -21,10 +24,14 @@ button {
transition: background 0.4s ease; transition: background 0.4s ease;
margin-right: auto; margin-right: auto;
} }
button:before { .bi-play:before {
padding-left: 0.2rem; padding-left: 0.2rem;
} }
button:hover { .bi:hover,
.bi:not(.bi-play) {
background: transparent; background: transparent;
} }
.bi:not(.bi-play) {
font-size: 3rem;
}
</style> </style>

View file

@ -1,19 +1,18 @@
<script setup> <script setup>
defineProps({ import { useData, usePlayer } from '@/stores/player.js';
url: String,
urls: Array, const player = usePlayer(),
show: Boolean, data = useData();
});
defineEmits(['playthis']); defineEmits(['playthis']);
</script> </script>
<template> <template>
<Transition name="fade"> <Transition name="fade">
<div v-if="show" class="pl-modal placeholder"> <div class="pl-modal placeholder">
<template v-for="plurl in urls"> <template v-for="plurl in data.state.urls">
<div class="pl-item" @click="$emit('playthis', plurl)"> <div class="pl-item" @click="$emit('playthis', plurl)">
<span v-if="url == plurl.url" class="bars-wrap"> <span v-if="data.state.url == plurl.url" class="bars-wrap">
<div class="bars"></div> <div class="bars"></div>
<div class="bars"></div> <div class="bars"></div>
<div class="bars"></div> <div class="bars"></div>
@ -22,7 +21,8 @@ defineEmits(['playthis']);
<img <img
:src="plurl.thumbnails[0].url" :src="plurl.thumbnails[0].url"
:height="plurl.thumbnails[0].height" :height="plurl.thumbnails[0].height"
:width="plurl.thumbnails[0].width" /> :width="plurl.thumbnails[0].width"
loading="lazy" />
</div> </div>
<span class="pl-main caps">{{ plurl.title }}</span> <span class="pl-main caps">{{ plurl.title }}</span>
</div> </div>

View file

@ -63,6 +63,7 @@ onMounted(() => {
@change="setTheme($event.target.value)"> @change="setTheme($event.target.value)">
<option value="dark">Dark (Default)</option> <option value="dark">Dark (Default)</option>
<option value="light">Light</option> <option value="light">Light</option>
<option value="dracula">Dracula</option>
<option value="nord">Nord</option> <option value="nord">Nord</option>
</select> </select>

View file

@ -1,40 +1,45 @@
<script setup> <script setup>
import { ref, reactive, watch } from 'vue'; import { ref, reactive, watch, onUpdated } from 'vue';
import PlayBtn from './PlayBtn.vue'; import PlayBtn from './PlayBtn.vue';
import SongItem from './SongItem.vue'; import SongItem from './SongItem.vue';
import AlbumItem from './AlbumItem.vue'; import AlbumItem from './AlbumItem.vue';
import { getJsonPiped, getPipedQuery } from '../scripts/fetch.js'; import { getJsonPiped, getPipedQuery } from '../scripts/fetch.js';
import { useLazyLoad } from '../scripts/util.js'; import { useLazyLoad, useRoute } from '../scripts/util.js';
import { useCreatePlaylist } from '../scripts/db.js';
const props = defineProps(['search', 'songItems', 'items']), import { useResults, useArtist } from '@/stores/results.js';
emit = defineEmits(['get-album', 'get-artist', 'play-urls', 'add-song']), import { useNav } from '@/stores/misc.js';
const results = useResults(),
nav = useNav(),
artist = useArtist();
const emit = defineEmits(['get-album', 'get-artist', 'play-urls', 'add-song']),
filters = ['music_songs', 'music_albums', 'music_artists'], filters = ['music_songs', 'music_albums', 'music_artists'],
filter = ref('music_songs'), filter = ref('music_songs'),
isSearch = ref(/search/.test(location.pathname)), isSearch = ref(/search/.test(location.pathname));
data = reactive({
notes: null,
albums: null,
albumTitle: null,
songs: null,
artists: null,
recommendedArtists: null,
});
const Reset = () => { const playAlbum = () => {
isSearch.value = /search/.test(location.pathname); const urls = results.items?.songs?.items?.map(item => {
for (let i in data) {
data[i] = null;
}
},
playAlbum = () => {
const urls = data.songs.items.map(item => {
return { url: item.url, title: item.title }; return { url: item.url, title: item.title };
}); });
emit('play-urls', urls); emit('play-urls', urls);
}, },
saveAlbum = () => {
const urls = results.items?.songs?.items?.map(item => {
return { url: item.url, title: item.title };
}),
title = results.items?.songs?.title;
if (title) {
useCreatePlaylist(title, urls, () => {
alert('Saved!');
});
}
},
getSearch = q => { getSearch = q => {
if (q) { if (q) {
const pq = q.split(' ').join('+'); const pq = q.split(' ').join('+');
@ -47,74 +52,60 @@ const Reset = () => {
getResults(pq); getResults(pq);
useLazyLoad(); useLazyLoad();
} else { } else {
Reset(); results.resetItems();
history.pushState({}, '', '/'); useRoute('/');
document.title = 'Hyperpipe'; document.title = 'Hyperpipe';
console.log('No Search'); console.log('No Search');
} }
}, },
getResults = async q => { getResults = async q => {
const f = filter.value || 'music_songs', results.resetItems();
json = await getJsonPiped(`/search?q=${q}&filter=${f}`);
data[f.split('_')[1]] = json; const f = filter.value || 'music_songs',
console.log(json, data); json = await getJsonPiped(`/search?q=${q}&filter=${f}`),
key = f.split('_')[1];
results.setItem(key, json);
console.log(json, key);
}; };
watch( watch(
() => props.search, () => nav.state.search,
n => { n => {
if (n) { if (n) {
Reset();
n = n.replace(location.search || '', ''); n = n.replace(location.search || '', '');
console.log(n); console.log(n);
artist.reset();
getSearch(n); getSearch(n);
} }
}, },
); );
watch( onUpdated(() => {
() => props.songItems, isSearch.value = /search/.test(location.pathname);
i => { });
console.log(i);
Reset();
data.songs = i;
data.albumTitle = i.title;
},
);
watch(
() => props.items,
itms => {
Reset();
console.log(itms);
for (let i in itms) {
data[i] = {};
data[i].items = itms[i];
console.log(i, data[i]);
}
},
);
</script> </script>
<template> <template>
<div v-if="data.songs && data.songs.corrected" class="text-full"> <div
Did you mean, "<span class="caps">{{ data.songs.suggestion }}</span 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>
<div v-if="data.albumTitle" class="text-full flex"> <div v-if="results.items?.songs?.title" class="text-full flex">
<PlayBtn @click="playAlbum" /> <PlayBtn @click="playAlbum" />
<span>{{ data.albumTitle }}</span> <PlayBtn ico="plus" @click="saveAlbum" />
<span>{{ results.items?.songs?.title }}</span>
</div> </div>
<div v-if="isSearch" class="filters"> <div v-if="isSearch" class="filters">
@ -123,18 +114,19 @@ watch(
class="filter caps" class="filter caps"
@click=" @click="
filter = f; filter = f;
Reset(); getSearch(nav.state.search);
getSearch(search);
" "
:data-active="f == filter"> :data-active="f == filter">
{{ f.split('_')[1] }} {{ f.split('_')[1] }}
</button> </button>
</div> </div>
<div v-if="data.songs && data.songs.items[0]" class="search-songs"> <div
v-if="results.items.songs && results.items.songs.items[0]"
class="search-songs">
<h2>Songs</h2> <h2>Songs</h2>
<div class="grid"> <div class="grid">
<template v-for="song in data.songs.items"> <template v-for="song in results.items.songs.items">
<SongItem <SongItem
:author="song.uploaderName || song.subtitle" :author="song.uploaderName || song.subtitle"
:title="song.title || song.name" :title="song.title || song.name"
@ -163,18 +155,22 @@ watch(
</template> </template>
</div> </div>
<a <a
v-if="data.notes" v-if="results.items.notes"
@click.prevent="$emit('get-album', '/playlist?list=' + data.notes.items)" @click.prevent="
$emit('get-album', '/playlist?list=' + results.items.notes.items)
"
class="more" class="more"
:href="'/playlist?list=' + data.notes.items" :href="'/playlist?list=' + results.items.notes.items"
>See All</a >See All</a
> >
</div> </div>
<div v-if="data.albums && data.albums.items[0]" class="search-albums"> <div
v-if="results.items.albums && results.items.albums.items[0]"
class="search-albums">
<h2>Albums</h2> <h2>Albums</h2>
<div class="grid-3"> <div class="grid-3">
<template v-for="album in data.albums.items"> <template v-for="album in results.items.albums.items">
<AlbumItem <AlbumItem
:author="album.uploaderName || album.subtitle" :author="album.uploaderName || album.subtitle"
:name="album.name || album.title" :name="album.name || album.title"
@ -186,10 +182,12 @@ watch(
</div> </div>
</div> </div>
<div v-if="data.singles && data.singles.items[0]" class="search-albums"> <div
v-if="results.items.singles && results.items.singles.items[0]"
class="search-albums">
<h2>Singles</h2> <h2>Singles</h2>
<div class="grid-3"> <div class="grid-3">
<template v-for="single in data.singles.items"> <template v-for="single in results.items.singles.items">
<AlbumItem <AlbumItem
:author="single.subtitle" :author="single.subtitle"
:name="single.title" :name="single.title"
@ -201,16 +199,17 @@ watch(
<div <div
v-if=" v-if="
(data.recommendedArtists && data.recommendedArtists.items[0]) || (results.items.recommendedArtists &&
(data.artists && data.artists.items[0]) results.items.recommendedArtists.items[0]) ||
(results.items.artists && results.items.artists.items[0])
" "
class="search-artists"> class="search-artists">
<h2>{{ data.artists ? 'Artists' : 'Similar Artists' }}</h2> <h2>{{ results.items.artists ? 'Artists' : 'Similar Artists' }}</h2>
<div class="grid-3 circle"> <div class="grid-3 circle">
<template <template
v-for="artist in data.artists v-for="artist in results.items.artists
? data.artists.items ? results.items.artists.items
: data.recommendedArtists.items"> : results.items.recommendedArtists.items">
<AlbumItem <AlbumItem
:author="artist.subtitle" :author="artist.subtitle"
:name="artist.name || artist.title" :name="artist.name || artist.title"
@ -241,6 +240,9 @@ watch(
.search-artists { .search-artists {
text-align: center; text-align: center;
} }
:deep(.bi-play) {
margin-right: 0.75rem;
}
.filters { .filters {
display: flex; display: flex;
width: 100%; width: 100%;

View file

@ -1,13 +1,9 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useNav } from '@/stores/misc.js';
defineProps({ const show = ref(false),
search: String, nav = useNav();
});
defineEmits(['update-search']);
const show = ref(false);
</script> </script>
<template> <template>
@ -22,8 +18,8 @@ const show = ref(false);
type="text" type="text"
aria-label="Search Input" aria-label="Search Input"
placeholder="Search..." placeholder="Search..."
@change="$emit('update-search', $event.target.value)" @change="nav.state.search = $event.target.value"
:value="search" /> :value="nav.state.search" />
</div> </div>
</Transition> </Transition>
</button> </button>

View file

@ -6,16 +6,12 @@ import Modal from './Modal.vue';
import { useStore } from '../scripts/util.js'; import { useStore } from '../scripts/util.js';
import { useListPlaylists } from '../scripts/db.js'; import { useListPlaylists } from '../scripts/db.js';
defineProps({ import { usePlayer } from '../stores/player.js';
state: String,
time: Number,
show: Boolean,
loop: Boolean,
lyrics: Boolean,
});
const emit = defineEmits(['vol', 'play', 'toggle', 'save', 'change-time']), const player = usePlayer(),
vol = ref(useStore().vol / 100 || 1), store = useStore();
const emit = defineEmits(['vol', 'save', 'change-time']),
showme = reactive({ showme = reactive({
menu: false, menu: false,
pl: false, pl: false,
@ -76,13 +72,13 @@ function Save() {
<button <button
id="btn-play-pause" id="btn-play-pause"
aria-label="Play or Pause" aria-label="Play or Pause"
:class="'bi bi-' + state" :class="'bi bi-' + player.state.status"
@click="$emit('play')"></button> @click="player.toggle('play')"></button>
<div id="statusbar-progress" class="range-wrap"> <div id="statusbar-progress" class="range-wrap">
<input <input
aria-label="Change Time" aria-label="Change Time"
:value="time" :value="player.state.time"
type="range" type="range"
name="statusbar-progress" name="statusbar-progress"
max="100" max="100"
@ -102,13 +98,10 @@ function Save() {
id="vol-input" id="vol-input"
aria-label="Volume Input" aria-label="Volume Input"
type="range" type="range"
:value="vol" :value="player.state.vol"
max="1" max="1"
step=".01" step=".01"
@input=" @input="player.state.vol = $event.target.value" />
$emit('vol', $event.target.value);
vol = $event.target.value;
" />
</div> </div>
</Transition> </Transition>
</button> </button>
@ -117,15 +110,15 @@ function Save() {
aria-label="More Controls" aria-label="More Controls"
@click=" @click="
showme.menu = !showme.menu; showme.menu = !showme.menu;
show ? $emit('toggle', 'showplaylist') : ''; player.state.playlist ? player.toggle('playlist') : '';
lyrics ? $emit('toggle', 'showlyrics') : ''; player.state.lyrics ? player.toggle('lyrics') : '';
"></button> "></button>
<div id="menu" v-if="showme.menu" class="popup"> <div id="menu" v-if="showme.menu" class="popup">
<button <button
id="info-btn" id="info-btn"
class="bi bi-info-circle" class="bi bi-info-circle"
aria-label="Show Information About Song" aria-label="Show Information About Song"
@click="$emit('toggle', 'showinfo')"></button> @click="player.toggle('info')"></button>
<button <button
id="addToPlaylist" id="addToPlaylist"
title="Add Current Song to a Playlist" title="Add Current Song to a Playlist"
@ -136,18 +129,21 @@ function Save() {
id="list-btn" id="list-btn"
title="Current Playlist" title="Current Playlist"
aria-label="Current Playlist" aria-label="Current Playlist"
:class="'bi bi-music-note-list ' + show" class="bi bi-music-note-list"
@click="$emit('toggle', 'showplaylist')"></button> :data-active="player.state.playlist"
@click="player.toggle('playlist')"></button>
<button <button
id="btn-lyrics" id="btn-lyrics"
:class="'bi bi-file-music ' + lyrics" class="bi bi-file-music"
@click="$emit('toggle', 'showlyrics')"></button> :data-active="player.state.lyrics"
@click="player.toggle('lyrics')"></button>
<button <button
id="loop-btn" id="loop-btn"
title="Loop" title="Loop"
aria-label="Loop" aria-label="Loop"
:class="'bi bi-infinity ' + loop" class="bi bi-infinity"
@click="$emit('toggle', 'loop')"></button> :data-active="player.state.loop"
@click="player.toggle('loop')"></button>
</div> </div>
</div> </div>
</div> </div>
@ -302,7 +298,7 @@ input[type='range']::-moz-range-track {
top: 0; top: 0;
} }
#statusbar-progress { #statusbar-progress {
--fw: v-bind('time + "%"'); --fw: v-bind('player.state.time + "%"');
width: 50vw; width: 50vw;
min-width: 200px; min-width: 200px;
max-width: 500px; max-width: 500px;

View file

@ -1,6 +1,10 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue'; import App from './App.vue';
const app = createApp(App); const pinia = createPinia(),
app = createApp(App);
app.use(pinia);
app.mount('#app'); app.mount('#app');

11
src/stores/misc.js Normal file
View file

@ -0,0 +1,11 @@
import { reactive } from 'vue';
import { defineStore } from 'pinia';
export const useNav = defineStore('nav', () => {
const state = reactive({
search: '',
page: 'home',
});
return { state };
});

50
src/stores/player.js Normal file
View file

@ -0,0 +1,50 @@
import { reactive } from 'vue';
import { defineStore } from 'pinia';
import { useStore } from '../scripts/util.js';
const store = useStore();
export const useData = defineStore('data', () => {
const state = reactive({
title: '',
description: '',
artist: '',
art: '',
url: '',
artistUrl: '',
lyrics: '',
src: [],
urls: [],
});
return { state };
});
export const usePlayer = defineStore('player', () => {
const state = reactive({
loop: false,
play: false,
status: 'play',
duration: 0,
time: 0,
playlist: false,
lyrics: false,
info: false,
vol: store.vol ? store.vol / 100 : 1,
});
function toggle(i) {
console.log(i, state[i]);
if (typeof state[i] == 'boolean') {
state[i] = !state[i];
}
console.log(i, state[i]);
}
function setTime(t) {
state.time = Math.floor((t / state.duration) * 100);
}
return { state, toggle, setTime };
});

44
src/stores/results.js Normal file
View file

@ -0,0 +1,44 @@
import { ref, reactive } from 'vue';
import { defineStore } from 'pinia';
export const useResults = defineStore('results', () => {
const items = ref({}),
search = ref('');
function setItem(key, val) {
items.value[key] = val;
console.log(items.value);
}
function resetItems() {
for (let i in items.value) {
items.value[i] = undefined;
}
}
return { items, search, setItem, resetItems };
});
export const useArtist = defineStore('artist', () => {
const state = reactive({
playlistId: null,
title: null,
description: null,
subscriberCount: 0,
thumbnails: [],
});
function reset() {
for (let i in state) {
state[i] = undefined;
}
}
function set(obj) {
for (let i in obj) {
state[i] = obj[i];
}
}
return { state, set, reset };
});