Playlists, Play Next, Code Refactor

This commit is contained in:
Shiny Nematoda 2022-04-21 18:38:21 +05:30
parent 5486be7613
commit 51c56abe16
17 changed files with 664 additions and 204 deletions

View file

@ -2,13 +2,15 @@
![On Codeberg](https://codeberg.org/Hyperpipe/static/raw/branch/master/on-codeberg.svg) ![On Codeberg](https://codeberg.org/Hyperpipe/static/raw/branch/master/on-codeberg.svg)
A Privacy Respecting Frontend for YouTube Music inspired and built with [Piped][piped] ( and a tiny bit of messy custom scrapers ). A Privacy Respecting Frontend for YouTube Music inspired and built with the help [Piped][piped] and YouTube's innertube API.
![YouTube Music](https://img.shields.io/badge/youtube-music-red?style=for-the-badge&logo=youtube) ![YouTube Music](https://img.shields.io/badge/youtube-music-red?style=for-the-badge&logo=youtube)
![Website](https://img.shields.io/website?down_color=red&down_message=offline&label=status&style=for-the-badge&up_color=cornflowerblue&up_message=online&url=https%3A%2F%2Fhyperpipe.surge.sh) ![Website](https://img.shields.io/website?down_color=red&down_message=offline&label=status&style=for-the-badge&up_color=cornflowerblue&up_message=online&url=https%3A%2F%2Fhyperpipe.surge.sh)
## Usage ## Usage
***HYPERPIPE IS ONLY BEING DEVELOPED, EXPECT BUGS AND MESSY CODE***
```sh ```sh
npm install npm install
``` ```
@ -35,15 +37,15 @@ npm run build
## Instances ## Instances
| Instance | Country | Country (Backend) | Offical | | Instance | Country (Backend) |
| ---------------------------- | ------- | ----------------- | ------- | | :--------------------------: | :---------------: |
| [hyperpipe.surge.sh][hypipe] | 🇺🇸 | 🇩🇪 | ✅ | | [hyperpipe.surge.sh][hypipe] | 🇩🇪 |
## LICENSE ## LICENSE
### GPL v3 Only ### GPL v3 Only
Please refer to LICENSE. Please refer to [LICENSE][LICENSE].
You can reach out to me on <TODO> You can reach out to me on <TODO>
@ -58,9 +60,10 @@ You can reach out to me on <TODO>
[hypipe]: https://hyperpipe.surge.sh [hypipe]: https://hyperpipe.surge.sh
[piped]: https://piped.kavin.rocks [piped]: https://piped.kavin.rocks
[LICENSE]: https://codeberg.org/Hyperpipe/Hyperpipe/src/branch/main/LICENSE.txt
[vue]: https://github.com/vuejs/core/blob/main/LICENSE [vue]: https://github.com/vuejs/core/blob/main/LICENSE
[vite]: https://github.com/vitejs/vite/blob/main/LICENSE [vite]: https://github.com/vitejs/vite/blob/main/LICENSE
[bi]: https://github.com/twbs/icons/blob/main/LICENSE.md [bi]: https://github.com/twbs/icons/blob/main/LICENSE.md
[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

View file

@ -7,6 +7,8 @@
<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" />
<link rel="dns-prefetch" href="https://hyperpipe-proxy.onrender.com" /> <link rel="dns-prefetch" href="https://hyperpipe-proxy.onrender.com" />
<link rel="manifest" href="/manifest.json">
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hyperpipe</title> <title>Hyperpipe</title>

9
public/manifest.json Normal file
View file

@ -0,0 +1,9 @@
{
"name": "Hyperpipe",
"short_name": "Hyperpipe",
"start_url": "/",
"display": "standalone",
"background_color": "#fff",
"description": "Privacy respecting YouTube Music Frontend.",
}

View file

@ -5,10 +5,13 @@ 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 Search from './components/Search.vue'; import Search from './components/Search.vue';
import NewPlaylist from './components/NewPlaylist.vue';
import Playlists from './components/Playlists.vue'; import Playlists from './components/Playlists.vue';
import Artist from './components/Artist.vue'; import Artist from './components/Artist.vue';
import { getJson, getJsonPiped } from './scripts/fetch.js'; import { getJson, getJsonPiped } from './scripts/fetch.js';
import { useLazyLoad } from './scripts/util.js';
import { useUpdatePlaylist } from './scripts/db.js'
const data = reactive({ const data = reactive({
artUrl: '', artUrl: '',
@ -36,7 +39,7 @@ const artist = reactive({
thumbnails: [], thumbnails: [],
}); });
const search = ref(''); const search = ref(''), page = ref('home');
const audio = ref(null); const audio = ref(null);
@ -47,6 +50,9 @@ function parseUrl() {
switch (loc[3].replace(location.search, '')) { switch (loc[3].replace(location.search, '')) {
case '': case '':
search.value = ''
page.value = 'home'
break;
case 'search': case 'search':
search.value = loc[4]; search.value = loc[4];
console.log(search.value); console.log(search.value);
@ -69,17 +75,7 @@ function parseUrl() {
function Toggle(e) { function Toggle(e) {
console.log(e, data[e]); console.log(e, data[e]);
data[e] = !data[e]
if (data[e]) {
data[e] = false;
} else {
data[e] = true;
}
}
function Update(e) {
search.value = e;
console.log('update');
} }
function timeUpdate(t) { function timeUpdate(t) {
@ -126,7 +122,7 @@ function playNext(u) {
audio.value.src = ''; audio.value.src = '';
const i = data.urls.map((s) => s.url).indexOf(data.url), const i = data.urls.map((s) => s.url).indexOf(data.url),
next = data.urls[i + 1]; next = data.urls[i + 1];
console.log('Index: ' + i); console.log('Index: ' + i);
console.log(data.url, data.urls, next); console.log(data.url, data.urls, next);
@ -134,6 +130,7 @@ function playNext(u) {
if (data.urls.length > i && data.urls.length != 0 && next) { if (data.urls.length > i && data.urls.length != 0 && next) {
getSong(next.url); getSong(next.url);
} else if (data.loop) { } else if (data.loop) {
console.log(data.url, data.urls[0]);
data.url = data.urls[0].url; data.url = data.urls[0].url;
getSong(data.urls[0].url); getSong(data.urls[0].url);
} else { } else {
@ -203,7 +200,7 @@ async function getArtist(e) {
} }
async function getNext(hash) { async function getNext(hash) {
if (!data.urls || data.urls.map((s) => s.url).indexOf(data.url) <= 0) { if (!data.urls || data.urls.map((s) => s.url).indexOf(data.url) < 0 || data.urls.length == 1) {
const json = await getJson( const json = await getJson(
'https://hyperpipeapi.onrender.com/next/' + hash, 'https://hyperpipeapi.onrender.com/next/' + hash,
); );
@ -257,7 +254,7 @@ function Stream(res) {
} }
function audioCanPlay() { function audioCanPlay() {
lazyLoad(); useLazyLoad();
audio.value.play(); audio.value.play();
data.state = 'pause'; data.state = 'pause';
@ -268,12 +265,24 @@ function audioCanPlay() {
document.title = `Playing: ${data.nowtitle} by ${data.nowartist}`; document.title = `Playing: ${data.nowtitle} by ${data.nowartist}`;
} }
function SaveTrack(e) {
useUpdatePlaylist(e, {
url: data.url,
title: data.nowtitle
}, (e) => {
if (e === true) {
console.log('Added Song To '+ e)
}
})
}
function setMetadata() { function setMetadata() {
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
const i = data.urls.map((u) => u.url).indexOf(data.url); const i = data.urls.map((u) => u.url).indexOf(data.url);
let artwork = [], let artwork = [],
album = undefined; album = undefined;
console.log(i); console.log(i);
if (i >= 0) { if (i >= 0) {
@ -303,43 +312,19 @@ function setMetadata() {
} }
} }
function lazyLoad() {
let lazyElems;
if ('IntersectionObserver' in window) {
lazyElems = document.querySelectorAll('.bg-img:not(.lazy)');
let imgObs = new IntersectionObserver((elems, obs) => {
elems.forEach((elem) => {
setTimeout(() => {
if (elem.isIntersecting) {
let ele = elem.target;
ele.classList.add('lazy');
imgObs.unobserve(ele);
}
}, 20);
});
});
lazyElems.forEach((img) => {
imgObs.observe(img);
});
} else {
console.log('Failed');
}
}
onMounted(() => { onMounted(() => {
lazyLoad(); useLazyLoad();
document.addEventListener('scroll', lazyLoad); document.addEventListener('scroll', useLazyLoad);
document.addEventListener('resize', lazyLoad); document.addEventListener('resize', useLazyLoad);
document.addEventListener('orientationChange', lazyLoad); document.addEventListener('orientationChange', useLazyLoad);
window.addEventListener('popstate', parseUrl); window.addEventListener('popstate', parseUrl);
window.onbeforeunload = () => { window.onbeforeunload = () => {
return 'Are you Sure?'; if (data.url) {
return 'Are you Sure?';
}
}; };
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
@ -359,6 +344,33 @@ onMounted(() => {
}); });
} }
if ('indexedDB' in window) {
const req = indexedDB.open('hyperpipedb', 1)
req.onupgradeneeded = e => {
const db = e.target.result;
console.log(db)
if (!db.objectStoreNames.contains("playlist")) {
const store = db.createObjectStore("playlist", { keyPath: 'name' })
store.createIndex('urls', 'urls', { unique: false })
}
}
req.onerror = e => {
console.log("Please let me use indexedDB!!")
console.log(e)
}
req.onsuccess = e => {
window.db = e.target.result
}
}
parseUrl(); parseUrl();
console.log('Mounted <App>!'); console.log('Mounted <App>!');
@ -372,6 +384,7 @@ onMounted(() => {
search = e; search = e;
} }
" "
@update-page="(e) => { page = e }"
:search="search" /> :search="search" />
<template v-if="artist"> <template v-if="artist">
@ -388,20 +401,27 @@ onMounted(() => {
<div v-if="data.cover" class="art bg-img" :style="data.cover"></div> <div v-if="data.cover" class="art bg-img" :style="data.cover"></div>
<div class="wrapper"> <div class="wrapper">
<NowPlaying :title="data.nowtitle" :artist="data.nowartist" /> <NowPlaying
:title="data.nowtitle"
:artist="data.nowartist" />
</div> </div>
</header> </header>
<main class="placeholder"> <main class="placeholder">
<Search <template v-if="page == 'home'">
<Search
@get-album="getAlbum" @get-album="getAlbum"
@get-artist="getArtist" @get-artist="getArtist"
@lazy="lazyLoad"
@play-urls="playList" @play-urls="playList"
@add-song="addSong" @add-song="addSong"
:items="data.items" :items="data.items"
:songItems="data.songItems" :songItems="data.songItems"
:search="search" /> :search="search" />
</template>
<template v-if="page == 'playlist'">
<NewPlaylist @play-urls="playList" />
</template>
</main> </main>
<Playlists <Playlists
@ -415,6 +435,7 @@ onMounted(() => {
@vol="setVolume" @vol="setVolume"
@list="Toggle" @list="Toggle"
@loop="Toggle" @loop="Toggle"
@save="SaveTrack"
@change-time="setTime" @change-time="setTime"
:state="data.state" :state="data.state"
:time="data.time" :time="data.time"
@ -492,10 +513,6 @@ a,
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
main .grid {
display: grid;
grid-template-columns: 1fr 1fr;
}
header { header {
margin: auto; margin: auto;
display: flex; display: flex;

View file

@ -141,6 +141,17 @@ button {
text-transform: capitalize; text-transform: capitalize;
} }
.textbox {
padding: .5rem 1rem;
color: var(--color-text);
background: var(--color-background-mute);
border-radius: .25rem;
font-size: 1rem;
border: none;
appearence: none;
outline: none;
}
.bg-img { .bg-img {
background-image: linear-gradient(45deg, #88c0d0, #5e81ac); background-image: linear-gradient(45deg, #88c0d0, #5e81ac);
background-position: center; background-position: center;
@ -175,25 +186,38 @@ button {
} }
.popup-wrap { .popup-wrap {
--display: none;
position: relative; position: relative;
} }
.popup-wrap:hover,
.popup-wrap:focus,
.popup:focus,
.popup:active {
--display: flex;
}
.popup { .popup {
position: absolute; position: absolute;
display: var(--display); display: flex;
background-color: var(--color-background); background-color: var(--color-background);
padding: 0.5rem; padding: 0.5rem;
border-radius: 0.125rem; border-radius: 0.125rem;
z-index: 999; z-index: 999;
bottom: 1.25rem; bottom: 1.25rem;
box-shadow: 0 0 0.5rem var(--color-border); box-shadow: 0 0 0.5rem var(--color-border);
animation: fade 0.4s ease; }
.grid-3 {
display: grid;
grid-template-columns: 1fr;
}
@media (min-width: 530px) {
.grid-3 {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
@media (min-width: 1024px) {
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
}
.grid-3 {
grid-template-columns: 1fr 1fr 1fr;
}
} }
.bars-wrap { .bars-wrap {
@ -222,22 +246,15 @@ button {
} }
/* Animations */ /* Animations */
@keyframes fade { .fade-enter-active,
from { .fade-leave-active {
opacity: 0; transition: opacity 0.5s ease;
}
to {
opacity: 1;
}
} }
@keyframes fill { .fade-enter-from,
from { .fade-leave-to {
width: 0; opacity: 0;
}
to {
width: var(--width);
}
} }
@keyframes heightc { @keyframes heightc {
50% { 50% {
height: 100%; height: 100%;

View file

@ -1,23 +1,23 @@
<script setup> <script setup>
defineProps({ defineProps({
name: String, name: String,
author: String, author: {
type: String,
default: ''
},
grad: String,
art: { art: {
type: String, type: String,
default: '--art: linear-gradient(45deg, #88c0d0, #5e81ac);', default: 'linear-gradient(45deg, #88c0d0, #5e81ac)',
}, },
}); });
const emit = defineEmits(['open-album']); defineEmits(['open-album']);
function onClick() {
emit('open-album');
}
</script> </script>
<template> <template>
<div class="album card pop" @click="onClick"> <div class="album card pop" @click="$emit('open-album')">
<div class="card-bg bg-img pop-2" :style="art"></div> <div class="card-bg bg-img pop-2"></div>
<div class="card-text"> <div class="card-text">
<h4>{{ name }}</h4> <h4>{{ name }}</h4>
@ -38,6 +38,8 @@ function onClick() {
background-color: var(--color-background); background-color: var(--color-background);
} }
.card-bg { .card-bg {
--art: v-bind('grad || art');
background: v-bind('grad');
height: 13rem; height: 13rem;
width: 13rem; width: 13rem;
} }

104
src/components/Modal.vue Normal file
View file

@ -0,0 +1,104 @@
<script setup>
import { ref, watch } from 'vue'
const props = defineProps(['display', 'title', 'n']),
emit = defineEmits(['show']),
show = ref(props.display);
watch(() => props.display, (n) => {
console.log(n, props.display)
show.value = n
})
watch(show, (n) => {
emit('show', show.value)
})
</script>
<template>
<Transition name="fade">
<div class="modal" v-if="show">
<span class="bi bi-x modal-close" @click="show = false"></span>
<div class="modal-box">
<div class="modal-title">{{ title }}</div>
<div class="modal-content">
<slot name="content"></slot>
</div>
<div class="modal-buttons">
<slot name="buttons"></slot>
</div>
</div>
</div>
</Transition>
</template>
<style>
.modal {
display: flex;
position: fixed;
align-items: center;
justify-content: center;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: #00000066;
z-index: 9999;
}
.modal-box {
width: 50vw;
border-radius: .5rem;
background-color: var(--color-background-soft);
}
.modal-title {
font-size: 1.25rem;
padding: 1rem 2rem;
border-bottom: 1px solid var(--color-shadow);
}
.modal-content {
padding: 1rem;
}
.modal-content * {
width: 100%;
padding: 1rem;
}
.modal-close {
color: var(--color-background);
font-size: 2rem;
position: fixed;
top: 1rem;
right: 1rem;
}
.modal-buttons {
width: 100%;
border-top: 1px solid var(--color-shadow);
}
.modal-buttons button {
padding: 1rem 2rem;
color: var(--color-foreground);
width: calc(100% / v-bind('n'));
font-size: 1rem;
font-weight: bold;
border-right: 1px solid var(--color-shadow);
}
.modal-buttons button:first-child {
color: indianred;
border-bottom-left-radius: .5rem;
}
.modal-buttons button:last-child {
border: none;
border-bottom-right-radius: .5rem;
}
.modal-buttons button:hover {
background-color: var(--color-background-mute);
}
@media (max-width: 530px) {
.modal-box {
width: 80vw;
}
}
@media (min-width: 1024px) {
.modal-box {
width: 40vw;
}
}
</style>

View file

@ -1,12 +1,35 @@
<script setup> <script setup>
import { reactive } from 'vue'
import SearchBar from '../components/SearchBar.vue'; import SearchBar from '../components/SearchBar.vue';
defineEmits(['update-search']); const emit = defineEmits(['update-page', 'update-search']);
const page = reactive({
home: true,
playlist: false
})
function Toggle(p) {
for (let pg in page) {
page[pg] = false
}
page[p] = true
emit('update-page', p);
}
function home() {
history.pushState('', {}, '/')
}
</script> </script>
<template> <template>
<nav> <nav>
<h1>Hyperpipe</h1> <h1 class="bi bi-vinyl" @click="home"></h1>
<div class="wrap">
<span :class="'nav-ico bi bi-house ' + page.home" @click="Toggle('home')"></span>
<span :class="'nav-ico bi bi-collection ' + page.playlist" @click="Toggle('playlist')"></span>
</div>
<div class="wrap"> <div class="wrap">
<SearchBar <SearchBar
@ -26,10 +49,18 @@ nav {
display: flex; display: flex;
align-items: center; align-items: center;
} }
h1 { h1.bi {
font-size: 2rem; font-size: calc(1.75rem + 1.5vw);
}
.bi {
font-size: calc(1rem + 1vw);
} }
.wrap { .wrap {
text-align: center;
margin-left: auto; margin-left: auto;
margin-right: .5rem;
}
.nav-ico {
margin: 0 .5rem;
} }
</style> </style>

View file

@ -0,0 +1,90 @@
<script setup>
import { ref, onMounted } from 'vue'
import AlbumItem from './AlbumItem.vue'
import Modal from './Modal.vue'
import { useRand } from '../scripts/colors.js'
import { useListPlaylists, useGetPlaylist, useCreatePlaylist } from '../scripts/db.js'
const emit = defineEmits(['play-urls'])
const list = ref([]),
show = ref(false),
text = ref('');
function Play(key) {
console.log(key);
useGetPlaylist(key, res => {
console.log(res);
emit('play-urls', res.urls)
})
}
function List() {
useListPlaylists((res) => {
list.value = res
})
}
function Create() {
if (text.value) {
useCreatePlaylist(text.value, [], () => {
List()
show.value = false
})
}
}
onMounted(() => {
List()
})
</script>
<template>
<div class="npl-wrap">
<Modal n="2" :display="show" title="Create a new Playlist..." @show="(e) => { show = e }" >
<template #content>
<input type="text" placeholder="Playlist name..." class="textbox" v-model="text" />
</template>
<template #buttons>
<button @click="show = false">Cancel</button>
<button @click="Create">Create</button>
</template>
</Modal>
<div class="npl-box bi bi-plus-lg pop" @click="show = true"></div>
<div class="grid-3">
<template v-for="i in list">
<AlbumItem :name="i.name" :grad="useRand()" @open-album="Play(i.name)" />
</template>
</div>
</div>
</template>
<style scoped>
.npl-wrap {
padding-bottom: 5rem;
}
.npl-box {
margin: 0 auto 5rem auto;
border-radius: .5rem;
background-color: var(--color-background-mute);
padding: 2rem 3rem;
font-size: 4rem;
width: 10rem;
}
.npl-box:hover {
background-color: var(--color-background-soft);
}
.npl-round {
float: left;
display: inline-block;
height: 5rem;
width: 5rem;
border-radius: 50%;
background: v-bind('bg');
}
.text-box {
padding: 2rem;
}
</style>

View file

@ -8,22 +8,24 @@ defineEmits(['playthis']);
</script> </script>
<template> <template>
<div v-if="show" class="modal placeholder"> <Transition name="fade">
<template v-for="plurl in urls"> <div v-if="show" class="pl-modal placeholder">
<div class="pl-item" @click="$emit('playthis', plurl)"> <template v-for="plurl in urls">
<span v-if="url == plurl.url" class="bars-wrap"> <div class="pl-item" @click="$emit('playthis', plurl)">
<div class="bars"></div> <span v-if="url == plurl.url" class="bars-wrap">
<div class="bars"></div> <div class="bars"></div>
<div class="bars"></div> <div class="bars"></div>
</span> <div class="bars"></div>
<span class="pl-main caps">{{ plurl.title }}</span> </span>
</div> <span class="pl-main caps">{{ plurl.title }}</span>
</template> </div>
</div> </template>
</div>
</Transition>
</template> </template>
<style scoped> <style scoped>
.modal { .pl-modal {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: fixed; position: fixed;
@ -37,8 +39,6 @@ defineEmits(['playthis']);
z-index: 99999; z-index: 99999;
box-shadow: 0.1rem 0.1rem 1rem var(--color-shadow); box-shadow: 0.1rem 0.1rem 1rem var(--color-shadow);
padding: 1rem; padding: 1rem;
transition: width 0.4s ease;
animation: fade 0.4s ease;
overflow-y: auto; overflow-y: auto;
} }
.placeholder:empty:before { .placeholder:empty:before {

View file

@ -5,16 +5,11 @@ 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';
const props = defineProps(['search', 'songItems', 'items']); const props = defineProps(['search', 'songItems', 'items']);
const emit = defineEmits([ const emit = defineEmits(['get-album', 'get-artist', 'play-urls', 'add-song']);
'get-album',
'get-artist',
'lazy',
'play-urls',
'add-song',
]);
const data = reactive({ const data = reactive({
notes: null, notes: null,
@ -47,7 +42,7 @@ function getSearch(q) {
document.title = 'Search Results for ' + q; document.title = 'Search Results for ' + q;
getResults(pq); getResults(pq);
emit('lazy'); useLazyLoad();
} else { } else {
Reset(); Reset();
@ -103,7 +98,7 @@ watch(
data[i] = {}; data[i] = {};
data[i].items = itms[i]; data[i].items = itms[i];
console.log(i + ': ' + data[i]); console.log(i, data[i]);
} }
}, },
); );
@ -111,7 +106,7 @@ watch(
<template> <template>
<div v-if="data.songs && data.songs.corrected" class="text-full"> <div v-if="data.songs && data.songs.corrected" class="text-full">
I Fixed your Typo, "<span class="caps">{{ data.songs.suggestion }}</span Did you mean, "<span class="caps">{{ data.songs.suggestion }}</span
>"!! >"!!
</div> </div>
@ -129,11 +124,12 @@ watch(
:title="song.title || song.name" :title="song.title || song.name"
:channel="song.uploaderUrl || ''" :channel="song.uploaderUrl || ''"
:play="song.url || '/watch?v=' + song.id" :play="song.url || '/watch?v=' + song.id"
:art="'url(' + (song.thumbnail || song.thumbnails[1].url) + ')'"
@open-song=" @open-song="
$emit('play-urls', [ $emit('play-urls', [
{ {
url: song.url || '/watch?v=' + song.id, url: song.url || '/watch?v=' + song.id,
title: song.title || song.name, title: (song.title || song.name),
}, },
]) ])
" "
@ -141,15 +137,7 @@ watch(
(e) => { (e) => {
$emit('get-artist', e); $emit('get-artist', e);
} }
"> " />
<template #art>
<div
:style="`--art: url(${
song.thumbnail || song.thumbnails[1].url
});`"
class="pop-2 bg-img song-bg"></div>
</template>
</SongItem>
</template> </template>
</div> </div>
<a <a
@ -168,9 +156,7 @@ watch(
<AlbumItem <AlbumItem
:author="album.uploaderName || album.subtitle" :author="album.uploaderName || album.subtitle"
:name="album.name || album.title" :name="album.name || album.title"
:art=" :art="'url(' + (album.thumbnail || album.thumbnails[0].url) + ')'"
'--art: url(' + (album.thumbnail || album.thumbnails[0].url) + ');'
"
@open-album=" @open-album="
$emit('get-album', album.url || '/playlist?list=' + album.id) $emit('get-album', album.url || '/playlist?list=' + album.id)
" /> " />
@ -185,7 +171,7 @@ watch(
<AlbumItem <AlbumItem
:author="single.subtitle" :author="single.subtitle"
:name="single.title" :name="single.title"
:art="'--art: url(' + single.thumbnails[0].url + ');'" :art="'url(' + single.thumbnails[0].url + ')'"
@open-album="$emit('get-album', '/playlist?list=' + single.id)" /> @open-album="$emit('get-album', '/playlist?list=' + single.id)" />
</template> </template>
</div> </div>
@ -200,7 +186,7 @@ watch(
<AlbumItem <AlbumItem
:author="artist.subtitle" :author="artist.subtitle"
:name="artist.title" :name="artist.title"
:art="'--art: url(' + artist.thumbnails[0].url + ');'" :art="'url(' + artist.thumbnails[0].url + ')'"
@open-album="$emit('get-artist', artist.id)" /> @open-album="$emit('get-artist', artist.id)" />
</template> </template>
</div> </div>
@ -219,11 +205,6 @@ watch(
.search-albums h2 { .search-albums h2 {
text-align: center; text-align: center;
} }
.search-albums .grid-3,
.search-artists .grid-3 {
display: grid;
grid-template-columns: 1fr;
}
.search-artists { .search-artists {
text-align: center; text-align: center;
} }
@ -236,26 +217,9 @@ watch(
.text-right { .text-right {
text-align: right; text-align: right;
} }
.song-bg {
width: 120px;
height: 120px;
}
.more { .more {
margin: 1.5rem 0.5rem; margin: 1.5rem 0.5rem;
font-weight: bold; font-weight: bold;
font-size: 1rem; font-size: 1rem;
} }
@media (min-width: 530px) {
.search-albums .grid-3,
.search-artists .grid-3 {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
@media (min-width: 1024px) {
.search-albums .grid-3,
.search-artists .grid-3 {
grid-template-columns: 1fr 1fr 1fr;
}
}
</style> </style>

View file

@ -1,17 +1,26 @@
<script setup> <script setup>
import { ref } from 'vue';
defineProps(['search']); defineProps(['search']);
defineEmits(['update-search']); defineEmits(['update-search']);
const show = ref(false);
</script> </script>
<template> <template>
<button class="bi bi-search popup-wrap"> <button
<div class="popup"> class="bi bi-search popup-wrap"
<input @mouseenter="show = true"
type="text" @mouseleave="show = false">
placeholder="Search..." <Transition name="fade">
@change="$emit('update-search', $event.target.value)" <div v-if="show" class="popup">
:value="search" /> <input
</div> type="text"
placeholder="Search..."
@change="$emit('update-search', $event.target.value)"
:value="search" />
</div>
</Transition>
</button> </button>
</template> </template>
@ -24,8 +33,7 @@ defineEmits(['update-search']);
} }
.popup input { .popup input {
color: var(--color-text); color: var(--color-text);
--width: calc(100vw - 4rem); width: calc(100vw - 4rem);
width: 1.5rem;
max-width: 600px; max-width: 600px;
font-size: 1rem; font-size: 1rem;
border: none; border: none;
@ -33,10 +41,5 @@ defineEmits(['update-search']);
background: var(--color-background-mute); background: var(--color-background-mute);
outline: none; outline: none;
text-align: center; text-align: center;
animation: fill 0.4s ease;
transform: width 0.4s ease;
}
.popup input:hover {
width: var(--width);
} }
</style> </style>

View file

@ -1,13 +1,18 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue';
const props = defineProps({ const props = defineProps({
author: String, author: String,
title: String, title: String,
channel: String, channel: String,
play: String, play: String,
art: String,
}); });
const emit = defineEmits(['get-artist', 'open-song']); const emit = defineEmits(['get-artist', 'open-song']);
const show = ref(false);
function openSong(el) { function openSong(el) {
if (!el.classList.contains('ign')) { if (!el.classList.contains('ign')) {
emit('open-song', props.play); emit('open-song', props.play);
@ -38,10 +43,14 @@ async function Share() {
); );
} }
} }
onMounted(() => {
console.log(props)
})
</script> </script>
<template> <template>
<div class="song card flex pop" @click="openSong($event.target)"> <div class="song card flex pop" @click="openSong($event.target)">
<slot name="art"></slot> <div class="pop-2 bg-img song-bg"></div>
<span class="flex content"> <span class="flex content">
<h4>{{ title }}</h4> <h4>{{ title }}</h4>
@ -53,16 +62,21 @@ async function Share() {
</a> </a>
</span> </span>
<span class="bi bi-three-dots-vertical popup-wrap ign"> <span
<div class="popup ign"> class="bi bi-three-dots-vertical popup-wrap ign"
<span @mouseenter="show = true"
class="bi bi-plus-lg ign" @mouseleave="show = false">
@click=" <Transition name="fade">
$parent.$emit('add-song', { url: play, title: title }) <div v-if="show" class="popup ign">
"></span> <span
class="bi bi-plus-lg ign"
@click="
$parent.$emit('add-song', { url: play, title: title })
"></span>
<span class="bi bi-share ign" @click="Share"></span> <span class="bi bi-share ign" @click="Share"></span>
</div> </div>
</Transition>
</span> </span>
</div> </div>
</template> </template>
@ -92,4 +106,9 @@ span.bi-three-dots-vertical {
.popup span { .popup span {
padding: 0.5rem; padding: 0.5rem;
} }
.song-bg {
--art: v-bind('art');
width: 120px;
height: 120px;
}
</style> </style>

View file

@ -1,13 +1,44 @@
<script setup> <script setup>
import { ref, watch } from 'vue';
import Modal from './Modal.vue'
import { useListPlaylists } from '../scripts/db.js'
defineProps({ defineProps({
state: String, state: String,
time: Number, time: Number,
show: Boolean, show: Boolean,
loop: Boolean, loop: Boolean,
}); });
defineEmits(['vol', 'play', 'list', 'loop', 'change-time']);
const emit = defineEmits(['vol', 'play', 'list', 'loop', 'save', 'change-time']),
showVol = ref(false), vol = ref(1), showmenu = ref(false), showpl = ref(false), pl = ref(''), list = ref([]);
function Save() {
showpl.value = true;
useListPlaylists((res) => {
console.log(res);
list.value = res;
showmenu.value = false;
})
}
</script> </script>
<template> <template>
<Teleport to="body">
<Transition name="fade">
<Modal n="2" :display="showpl" title="Select Playlist to Add" @show="(e) => { showpl = e }">
<template #content>
<template v-for="i in list">
<div class="flex" @click="pl = i.name"><span>{{ i.name }}</span><span class="ml-auto">{{ i.urls.length || '' }}</span></div>
</template>
</template>
<template #buttons>
<button @click="showpl = false">Cancel</button>
<button @click="if (pl) $emit('save', pl); showpl = false">Add</button>
</template>
</Modal>
</Transition>
</Teleport>
<div id="statusbar" class="flex"> <div id="statusbar" class="flex">
<div class="flex statusbar-left"> <div class="flex statusbar-left">
<button <button
@ -29,25 +60,36 @@ defineEmits(['vol', 'play', 'list', 'loop', 'change-time']);
</div> </div>
<div class="flex statusbar-right"> <div class="flex statusbar-right">
<button id="vol-btn" class="popup-wrap bi bi-volume-up"> <button
<div id="vol" class="popup"> id="vol-btn"
<input @click="showVol = !showVol"
id="vol-input" class="popup-wrap bi bi-volume-up">
type="range" <Transition name="fade">
value="1" <div v-if="showVol" id="vol" class="popup">
max="1" <input
step=".01" id="vol-input"
@input="$emit('vol', $event.target.value)" /> type="range"
</div> :value="vol"
max="1"
step=".01"
@input="$emit('vol', $event.target.value); vol = $event.target.value" />
</div>
</Transition>
</button> </button>
<button <button class="bi bi-three-dots" @click="showmenu = !showmenu; if (show) $emit('list', 'showplaylist')"></button>
id="list-btn" <div id="menu" v-if="showmenu" class="popup">
:class="'bi bi-music-note-list ' + show" <button id="addToPlaylist" title="Add Current Song to a Playlist" class="bi bi-collection" @click="Save"></button>
@click="$emit('list', 'showplaylist')"></button> <button
<button id="list-btn"
id="loop-btn" title="Current Playlist"
:class="'bi bi-infinity ' + loop" :class="'bi bi-music-note-list ' + show"
@click="$emit('loop', 'loop')"></button> @click="$emit('list', 'showplaylist')"></button>
<button
id="loop-btn"
title="Loop"
:class="'bi bi-infinity ' + loop"
@click="$emit('loop', 'loop')"></button>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -80,9 +122,19 @@ defineEmits(['vol', 'play', 'list', 'loop', 'change-time']);
.bi-infinity { .bi-infinity {
font-size: 1.75rem !important; font-size: 1.75rem !important;
} }
.popup { .ml-auto {
margin-left: auto;
}
#menu {
bottom: 1.5rem;
left: -1.75rem;
box-shadow: .5rem .5rem 2rem var(--color-shadow);
}
#vol {
--h: 6.5rem; --h: 6.5rem;
--w: 1rem; --w: 1rem;
display: flex;
box-shadow: -.5rem -.5rem 2rem var(--color-shadow);
transform: rotateZ(270deg) translateX(calc(calc(var(--h) / 2) - 0.5rem)) transform: rotateZ(270deg) translateX(calc(calc(var(--h) / 2) - 0.5rem))
translateY(calc(calc(var(--w) + 2rem) * -1)); translateY(calc(calc(var(--w) + 2rem) * -1));
} }

16
src/scripts/colors.js Normal file
View file

@ -0,0 +1,16 @@
const c = [
"linear-gradient(45deg, #88c0d0, #5e81ac)",
"linear-gradient(45deg, #5e81ac, #b48ead)",
"linear-gradient(45deg, #a3be8c, #88c0d0)",
"linear-gradient(45deg, #ebcb8b, #a3be8c)",
"linear-gradient(45deg, #d08770, #bf616a)"
];
export function useColors() {
return c
}
export function useRand() {
const i = Math.floor(Math.random() * c.length);
return c[i]
}

98
src/scripts/db.js Normal file
View file

@ -0,0 +1,98 @@
export function useUpdatePlaylist(key, obj, cb = () => null) {
if (window.db && key) {
const store = window.db.transaction(['playlist'], "readwrite").objectStore('playlist'),
req = store.get(key);
req.onerror = (e) => {
console.log('Error!!', e)
}
req.onsuccess = e => {
const itm = e.target.result;
if (itm) {
itm.urls.push(obj)
store.put(itm)
cb(true)
}
}
} else alert('No indexedDB Created')
}
export function useCreatePlaylist(key, obj, cb = () => null ) {
if (window.db && key && obj) {
const store = window.db.transaction(['playlist'], "readwrite").objectStore('playlist'),
req = store.get(key);
req.onerror = (e) => {
console.log('Error!!', e)
}
req.onsuccess = (e) => {
const res = e.target.result;
if (!res) {
store.add({
name: key, urls: obj
});
cb(res);
} else {
console.log(e.target.result);
alert(`Error: Playlist with name ${key} exists`)
}
}
} else alert('No indexedDB Created')
}
export function useGetPlaylist(key, cb = () => null ) {
if (window.db && key) {
const store = window.db.transaction(['playlist']).objectStore('playlist'),
req = store.get(key);
req.onerror = (e) => {
console.log('Error!!', e)
}
req.onsuccess = e => {
const res = e.target.result;
if (res) {
cb(res)
}
}
} else alert('No indexedDB Created')
}
export function useListPlaylists(cb = () => null) {
if (window.db) {
let pls = [];
const store = window.db.transaction(['playlist']).objectStore('playlist'),
cursor = store.openCursor();
cursor.onsuccess = (e) => {
const pl = e.target.result;
if (pl) {
pls.push(pl.value)
pl.continue()
} else {
cb(pls)
}
}
}
}

33
src/scripts/util.js Normal file
View file

@ -0,0 +1,33 @@
export function usePrefs(key) {
if (localStorage) {
if (localStorage.get(key)) return true
else return false
} else return false
}
export function useLazyLoad() {
let lazyElems;
if ('IntersectionObserver' in window) {
lazyElems = document.querySelectorAll('.bg-img:not(.lazy)');
let imgObs = new IntersectionObserver((elems, obs) => {
elems.forEach((elem) => {
setTimeout(() => {
if (elem.isIntersecting) {
let ele = elem.target;
ele.classList.add('lazy');
imgObs.unobserve(ele);
}
}, 20);
});
});
lazyElems.forEach((img) => {
imgObs.observe(img);
});
} else {
console.log('Failed');
}
}