mirror of
https://codeberg.org/Hyperpipe/Hyperpipe
synced 2025-06-27 20:58:01 +02:00
Playlists, Play Next, Code Refactor
This commit is contained in:
parent
5486be7613
commit
51c56abe16
17 changed files with 664 additions and 204 deletions
15
README.md
15
README.md
|
@ -2,13 +2,15 @@
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## 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
|
|
@ -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
9
public/manifest.json
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"name": "Hyperpipe",
|
||||||
|
"short_name": "Hyperpipe",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#fff",
|
||||||
|
"description": "Privacy respecting YouTube Music Frontend.",
|
||||||
|
|
||||||
|
}
|
127
src/App.vue
127
src/App.vue
|
@ -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;
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -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
104
src/components/Modal.vue
Normal 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>
|
|
@ -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>
|
||||||
|
|
90
src/components/NewPlaylist.vue
Normal file
90
src/components/NewPlaylist.vue
Normal 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>
|
|
@ -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 {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
16
src/scripts/colors.js
Normal 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
98
src/scripts/db.js
Normal 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
33
src/scripts/util.js
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue