Initial Commit

This commit is contained in:
shiny_nematoda 2022-04-07 02:12:25 +05:30
commit 98d558ad6f
27 changed files with 3097 additions and 0 deletions

529
src/App.vue Normal file
View file

@ -0,0 +1,529 @@
<script setup>
import { ref } from 'vue';
import NavBar from './components/NavBar.vue';
import StatusBar from './components/StatusBar.vue';
import NowPlaying from './components/NowPlaying.vue';
import Search from './components/Search.vue';
import Playlists from './components/Playlists.vue';
import Artist from './components/Artist.vue';
let search = ref('');
</script>
<template>
<NavBar
@update-search="
(e) => {
search = e;
}
"
:search="search" />
<Artist
@playall="getAlbum"
:title="artistTitle"
:desc="artistDesc"
:subs="artistSubs"
:thumbs="artistThumbs"
:play="artistPlay" />
<header v-if="!artistTitle">
<div v-if="cover" class="art bg-img" :style="cover"></div>
<div class="wrapper">
<NowPlaying :title="title" :artist="artist" />
</div>
</header>
<main class="placeholder">
<Search
@get-song="setSong"
@get-album="getAlbum"
@get-artist="getArtist"
@lazy="lazyLoad"
@play-urls="playList"
@add-song="addSong"
:items="items"
:songItems="songItems"
:search="search" />
</main>
<Playlists
@playthis="playThis"
:url="url"
:urls="urls"
:show="showplaylist" />
<StatusBar
@play="playPause"
@vol="setVolume"
@list="Toggle"
@loop="Toggle"
:state="state"
:time="time"
:show="showplaylist"
:loop="loop" />
<audio
id="audio"
ref="audio"
:src="audioSrc"
@canplay="audioCanPlay"
@timeupdate="timeUpdate($event.target.currentTime)"
@ended="playNext"
autoplay></audio>
</template>
<script>
export default {
data() {
return {
artUrl: '',
cover: '',
title: '',
artist: '',
state: 'play',
audioSrc: '',
duration: 0,
time: 0,
url: '',
urls: [],
songItems: null,
showplaylist: false,
loop: false,
hls: null,
artistTitle: null,
artistDesc: null,
artistSubs: 0,
artistPlay: null,
artistThumbs: [],
items: {},
};
},
mounted() {
this.lazyLoad();
document.addEventListener('scroll', this.lazyLoad);
document.addEventListener('resize', this.lazyLoad);
document.addEventListener('orientationChange', this.lazyLoad);
window.addEventListener('popstate', this.parseUrl);
window.onbeforeunload = () => {
return 'Are you Sure?';
};
this.parseUrl();
console.log('Mounted <App>!');
},
methods: {
parseUrl() {
const loc = location.href.split('/');
console.log(loc);
switch (loc[3].replace(location.search, '')) {
case '':
case 'search':
this.search = loc[4];
console.log(loc[4], this.search);
break;
case 'watch':
this.getSong(loc[3]);
console.log(loc[3]);
break;
case 'playlist':
this.getAlbum(loc[3]);
console.log(loc[3]);
break;
case 'channel':
this.getArtist(loc[4]);
console.log(loc[4]);
default:
console.log(loc);
}
},
Toggle(e) {
console.log(this[e]);
if (this[e]) {
this[e] = false;
} else {
this[e] = true;
}
},
Update(e) {
this.search = e;
console.log('update');
},
timeUpdate(t) {
this.time = Math.floor((t / this.duration) * 100);
},
getJson(url) {
return fetch(url).then((res) => res.json());
},
setSong(s) {
this.urls = [s];
this.playNext();
},
addSong(s) {
this.urls.push(s);
const index = this.urls.map((s) => s.url).indexOf(this.url);
if (
(index == this.urls.length - 1 && this.time > 98) ||
this.urls.length == 1
) {
console.log(true);
this.playNext();
} else {
console.log(false);
}
console.log(s, this.urls);
},
playThis(t) {
const i = this.urls.indexOf(t);
this.getSong(this.urls[i].url);
},
playList(a) {
this.urls = a;
this.getSong(this.urls[0].url);
},
playNext(u) {
if (this.hls) {
this.hls.destroy();
}
const i = this.urls.map((s) => s.url).indexOf(this.url),
next = this.urls[i + 1];
console.log('Index: ' + i);
console.log(this.url, this.urls, next);
if (this.urls.length > i && this.urls.length != 0 && next) {
this.getSong(next.url);
} else if (this.loop) {
this.url = this.urls[0].url;
this.getSong(this.urls[0].url);
} else {
this.urls = [];
}
},
async getSong(e) {
console.log(e);
const hash = e.split('?v=').pop(),
json = await this.getJson(
'https://pipedapi.kavin.rocks/streams/' + hash,
);
console.log(json);
this.Stream({
art: json.thumbnailUrl,
artist: json.uploader,
time: json.duration,
hls: json.hls,
stream: json.audioStreams[0].url,
title: json.title,
url: e,
});
},
async getAlbum(e) {
console.log('Album: ', e);
const hash = e.split('?list=').pop(),
json = await this.getJson(
'https://pipedapi.kavin.rocks/playlists/' + hash,
);
console.log(json, json.relatedStreams);
this.songItems = {
items: json.relatedStreams,
title: json.name,
};
history.pushState({}, '', e);
},
async getArtist(e) {
console.log(e);
const json = await this.getJson(
'https://hypipeapi.onrender.com/browse/' + e,
);
console.log(json);
this.artistTitle = json.title;
this.artistDesc = json.description;
this.artistPlay = json.playlistId;
this.artistSubs = json.subscriberCount;
this.artistThumbs = json.thumbnails;
this.items = json.items;
this.items.notes = json.playlistId;
history.pushState({}, '', '/channel/' + e);
},
setVolume(vol) {
this.$refs.audio.volume = vol;
},
playPause() {
if (this.$refs.audio.paused) {
this.$refs.audio.play();
this.state = 'pause';
} else {
this.$refs.audio.pause();
this.state = 'play';
}
},
Stream(res) {
console.log(res);
this.art = res.art;
this.cover = `--art: url(${res.art});`;
this.title = res.title;
this.artist = res.artist.split(' - ')[0];
this.duration = res.time;
this.url = res.url;
if (!!Hls && Hls.isSupported()) {
this.hls = new Hls();
console.log(this.hls.levels);
this.hls.loadSource(res.hls);
this.hls.attachMedia(this.$refs.audio);
} else {
this.audioSrc = res.stream;
}
},
audioCanPlay() {
this.lazyLoad();
this.$refs.audio.play();
this.state = 'pause';
if (location.pathname != '/playlist') {
history.pushState({}, '', this.url);
}
document.title = `Playing: ${this.title} by ${this.artist}`;
this.setMetadata(this.art);
},
setMetadata(art) {
if (navigator.mediaSession) {
navigator.mediaSession.metadata = new MediaMetadata({
title: this.title,
artist: this.artist,
artwork: [
{
src: art,
type: 'image/png',
},
],
});
}
},
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');
}
},
},
};
</script>
<style>
@import './assets/base.css';
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
padding-top: 1rem;
font-weight: normal;
margin-bottom: 10rem;
}
main {
display: flex;
flex-direction: column;
}
header {
line-height: 1.5;
padding-bottom: 2.5rem;
}
.art {
display: block;
margin: 0 auto 2rem;
height: 175px;
width: 175px;
}
.bg-img {
background-image: linear-gradient(45deg, #88c0d0, #5e81ac);
background-position: center;
background-size: cover;
border-radius: 0.25rem;
}
.bg-img.lazy {
background-image: var(--art);
}
img,
.card,
.card-bg {
border-radius: 0.25rem;
border-radius: 0.25rem;
}
h4 {
font-weight: bold;
}
a,
.green {
text-decoration: none;
color: var(--color-foreground);
transition: 0.4s;
}
button {
border: none;
background: transparent;
color: var(--color-text);
appearence: none;
}
.bi {
color: var(--color-text);
font-size: 1.25rem;
transition: color 0.3s ease;
}
.bi:hover,
.bi.true {
color: var(--color-foreground);
}
.flex {
display: flex;
align-items: center;
justify-content: center;
}
.center {
margin-left: auto;
margin-right: auto;
}
.flex .bi {
line-height: 0;
}
.caps {
text-transform: capitalize;
}
.pop {
--shadow: none;
--translate: 0;
}
.pop,
.pop-2 {
transition: box-shadow 0.4s ease, transform 0.4s ease;
}
.pop:hover {
--shadow: 0.5rem 0.5rem 1rem var(--color-shadow);
--translate: -1.25rem;
box-shadow: var(--shadow);
transform: translateX(calc(var(--translate) / 2))
translateY(calc(var(--translate) / 2));
}
.pop-2 {
transform: translateX(var(--translate)) translateY(var(--translate));
box-shadow: var(--shadow);
}
.popup-wrap {
--display: none;
position: relative;
}
.popup-wrap:hover,
.popup-wrap:focus,
.popup:focus,
.popup:active {
--display: flex;
}
.popup {
position: absolute;
display: var(--display);
background-color: var(--color-background);
padding: 0.5rem;
border-radius: 0.125rem;
z-index: 999;
bottom: 1.25rem;
box-shadow: 0 0 0.5rem var(--color-border);
animation: fade 0.4s ease;
}
@media (hover: hover) {
a:hover {
background-color: var(--color-border);
}
}
@media (min-width: 1024px) {
#app {
display: flex;
place-items: center;
flex-direction: column;
}
main .grid {
display: grid;
grid-template-columns: 1fr 1fr;
}
header {
display: flex;
place-items: center;
}
header .wrapper {
display: flex;
place-items: flex-start;
flex-wrap: wrap;
}
.art {
margin: 0 2rem 0 0;
}
}
@keyframes fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fill {
from {
width: 0;
}
to {
width: var(--width);
}
}
</style>

115
src/assets/base.css Normal file
View file

@ -0,0 +1,115 @@
/* color palette from <https://github.com/vuejs/theme> and <https://nordtheme.com> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-blue: #88c0d0;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
:root {
--color-foreground: cornflowerblue;
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-shadow: #ccc;
--color-scrollbar: var(--color-shadow);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-foreground: var(--vt-c-blue);
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-shadow: #000;
--color-scrollbar: var(--vt-c-blue);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition: color 0.5s, background-color 0.5s;
line-height: 1.6;
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.placeholder:empty:before {
--url: url('./bg_music.svg');
content: '';
background-image: var(--url);
background-size: contain;
background-position: center;
background-repeat: no-repeat;
padding: 10rem;
}
.placeholder:empty:after {
--text: 'Start Searching...';
content: var(--text);
position: block;
margin-top: 1rem;
font-size: 1.5rem;
opacity: 0.6;
text-align: center;
}
::-webkit-scrollbar {
width: 0.35rem;
height: 0.35rem;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--color-scrollbar);
border-radius: 0.3rem;
}
::-webkit-scrollbar-button:single-button {
height: 0.75rem;
}

1
src/assets/bg_music.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -0,0 +1,37 @@
<svg xmlns="http://www.w3.org/2000/svg" width="757.964" height="743.732" viewBox="0 0 757.964 743.732" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Group_4" data-name="Group 4" transform="translate(-221 -78)">
<path id="Path_31-33" data-name="Path 31" d="M312.471,78.134a32.036,32.036,0,0,0-32,32V788.562a32.036,32.036,0,0,0,32,32h297a32.037,32.037,0,0,0,32-32V110.134a32.036,32.036,0,0,0-32-32Z" transform="translate(-0.018 -0.134)" fill="#e6e6e6"/>
<path id="Path_32-34" data-name="Path 32" d="M621.676,235.116v-54.44a125.247,125.247,0,0,1-80.859-60.189h0a23.789,23.789,0,0,1-14.22,4.68H483.368A178.549,178.549,0,0,0,621.676,235.116Z" transform="translate(-0.018 -0.134)" fill="#fff"/>
<path id="Path_33-35" data-name="Path 33" d="M621.676,177.556v-52.3a29.121,29.121,0,0,0-29.13-29.13h-41.97v5.05a23.917,23.917,0,0,1-7.4,17.329,122.3,122.3,0,0,0,78.5,59.049Z" transform="translate(-0.018 -0.134)" fill="#fff"/>
<path id="Path_34-36" data-name="Path 34" d="M419.787,125.168H392.756a23.987,23.987,0,0,1-23.98-23.99v-5.05H329.4a29.128,29.128,0,0,0-29.13,29.132v648.2a29.079,29.079,0,0,0,29.13,29.11h263.15a28.362,28.362,0,0,0,3.59-.22,29.146,29.146,0,0,0,25.54-28.89V296.286C525.967,285.2,446.218,216.907,419.787,125.168Z" transform="translate(-0.018 -0.134)" fill="#fff"/>
<path id="Path_35-37" data-name="Path 35" d="M480.088,125.168h-57.14c26.3,90.039,104.68,157.028,198.729,168.068v-55.02A181.668,181.668,0,0,1,480.087,125.168Z" transform="translate(-0.018 -0.134)" fill="#fff"/>
<path id="Path_36-38" data-name="Path 36" d="M601.63,610.915h-270a5.006,5.006,0,0,1-5-5V538.94a5.006,5.006,0,0,1,5-5h270a5.006,5.006,0,0,1,5,5v66.976A5.006,5.006,0,0,1,601.63,610.915Zm-270-74.976a3,3,0,0,0-3,3v66.976a3,3,0,0,0,3,3h270a3,3,0,0,0,3-3V538.94a3,3,0,0,0-3-3Z" transform="translate(-0.018 -0.134)" fill="#e6e6e6"/>
<circle id="Ellipse_4" data-name="Ellipse 4" cx="21" cy="21" r="21" transform="translate(345.611 551.293)" fill="#3f3d56"/>
<path id="Path_37-39" data-name="Path 37" d="M415.129,558.427a3.5,3.5,0,0,0,0,7h165a3.5,3.5,0,1,0,0-7Z" transform="translate(-0.018 -0.134)" fill="#e6e6e6"/>
<path id="Path_38-40" data-name="Path 38" d="M415.129,579.427a3.5,3.5,0,0,0,0,7h165a3.5,3.5,0,1,0,0-7Z" transform="translate(-0.018 -0.134)" fill="#e6e6e6"/>
<path id="Path_39-41" data-name="Path 39" d="M601.63,722.915h-270a5.006,5.006,0,0,1-5-5V650.94a5.006,5.006,0,0,1,5-5h270a5.006,5.006,0,0,1,5,5v66.976A5.006,5.006,0,0,1,601.63,722.915Zm-270-74.976a3,3,0,0,0-3,3v66.976a3,3,0,0,0,3,3h270a3,3,0,0,0,3-3V650.94a3,3,0,0,0-3-3Z" transform="translate(-0.018 -0.134)" fill="#e6e6e6"/>
<circle id="Ellipse_5" data-name="Ellipse 5" cx="21" cy="21" r="21" transform="translate(345.611 663.293)" fill="#3f3d56"/>
<path id="Path_40-42" data-name="Path 40" d="M415.129,670.427a3.5,3.5,0,0,0,0,7h165a3.5,3.5,0,1,0,0-7Z" transform="translate(-0.018 -0.134)" fill="#e6e6e6"/>
<path id="Path_41-43" data-name="Path 41" d="M415.129,691.427a3.5,3.5,0,0,0,0,7h165a3.5,3.5,0,1,0,0-7Z" transform="translate(-0.018 -0.134)" fill="#e6e6e6"/>
<path id="Path_42-44" data-name="Path 42" d="M460.947,471.93a94.96,94.96,0,0,1-95-95c0-.2,0-.408.012-.607.291-52.025,42.9-94.393,94.988-94.393a95,95,0,1,1,0,190Zm0-188a93.2,93.2,0,0,0-92.99,92.456c-.011.212-.01.383-.01.544a93.012,93.012,0,1,0,93-93Z" transform="translate(-0.018 0.07)" fill="#3f3d56"/>
<path id="Path_43-45" data-name="Path 43" d="M503.97,381.529l-65.022-37.541a2,2,0,0,0-3,1.732V420.8a2,2,0,0,0,3,1.732l65.022-37.541a2,2,0,0,0,0-3.464l-65.022-37.541a2,2,0,0,0-3,1.732V420.8a2,2,0,0,0,3,1.732l65.022-37.541a2,2,0,0,0,0-3.464Z" transform="translate(-0.018 -6.721)" fill="#6c63ff"/>
<path id="Path_54-46" data-name="Path 54" d="M757.569,743.732H0v-2.181H757.964Z" transform="translate(221 78)" fill="#3f3d56"/>
<g id="Group_1" data-name="Group 1" transform="translate(-869.284 495.958)">
<circle id="Ellipse_3" data-name="Ellipse 3" cx="27.936" cy="27.936" r="27.936" transform="translate(1653.026 -107.758)" fill="#ffb8b8"/>
<path id="Path_21-47" data-name="Path 21" d="M812.723,631a12.514,12.514,0,0,1,9.466-16.1,11.893,11.893,0,0,1,1.66-.2l29.427-47.229L826.4,541.915A10.728,10.728,0,1,1,841.32,526.5l37.113,36.6.075.091a9.719,9.719,0,0,1-.676,11.584L836.6,623.534a11.733,11.733,0,0,1,.307,1.19,12.514,12.514,0,0,1-11.232,14.918q-.533.047-1.06.047A12.553,12.553,0,0,1,812.723,631Z" transform="translate(866.433 -554.209)" fill="#ffb8b8"/>
<path id="Path_22-48" data-name="Path 22" d="M589.772,539.948H575.827l-6.633-53.786,20.58,0Z" transform="translate(1045.18 -230.972)" fill="#ffb8b8"/>
<path id="Path_23-49" data-name="Path 23" d="M812.885,719.943l-46.1,0V718.8a18.069,18.069,0,0,1,18.07-18.069h28.031Z" transform="translate(826.191 -396.88)" fill="#2f2e41"/>
<path id="Path_24-50" data-name="Path 24" d="M674.777,526.174l-11.982,7.135-33.217-42.82,17.685-10.529Z" transform="translate(1098.774 -236.477)" fill="#ffb8b8"/>
<path id="Path_25-51" data-name="Path 25" d="M848.632,729.873l-.582-.977a18.07,18.07,0,0,1,6.281-24.77l24.085-14.341,9.826,16.5Z" transform="translate(896.059 -406.599)" fill="#2f2e41"/>
<path id="Path_26-52" data-name="Path 26" d="M780.531,810.423c-9.341-109.994-14.9-212.178,19.25-253.862l.264-.323,57.468,22.988.095.205c.194.422,19.306,42.463,14.848,70.741l14.175,65.206,46.219,77.391a5.12,5.12,0,0,1-2.333,7.311l-20.086,8.837a5.142,5.142,0,0,1-6.424-2.008L853.73,724.923l-28.4-62.883a1.706,1.706,0,0,0-3.252.522L806.338,810.535a5.109,5.109,0,0,1-5.089,4.577H785.633A5.153,5.153,0,0,1,780.531,810.423Z" transform="translate(832.083 -525.133)" fill="#2f2e41"/>
<path id="Path_27-53" data-name="Path 27" d="M787.986,592.013l-.274-.131-.043-.3c-2.147-15.025.394-31.72,7.552-49.62a39.4,39.4,0,0,1,45.726-23.594h0a39.348,39.348,0,0,1,25.092,19.295,38.923,38.923,0,0,1,2.7,31.193c-9.024,26.388-20.73,51.078-20.848,51.324l-.245.515Z" transform="translate(844.072 -559.713)" fill="#6c63ff"/>
<path id="Path_28-54" data-name="Path 28" d="M765.634,647.248a12.776,12.776,0,0,1,9.16-13.935l53.739-103.171a10.3,10.3,0,1,1,17.522,10.817L791.046,643.411a12.419,12.419,0,0,1,.2,1.888,12.861,12.861,0,0,1-13.033,13.208h0a12.873,12.873,0,0,1-9.87-4.834,12.713,12.713,0,0,1-2.714-6.425Z" transform="translate(825.073 -552.635)" fill="#ffb8b8"/>
<path id="Path_29-55" data-name="Path 29" d="M795.454,500.188h44.359V480.852c-9.736-3.868-19.264-7.158-25.023,0a19.336,19.336,0,0,0-19.336,19.336Z" transform="translate(851.636 -595.791)" fill="#2f2e41"/>
<path id="Path_30-56" data-name="Path 30" d="M836.215,477.059c26.519,0,33.941,33.24,33.941,51.993,0,10.458-4.729,14.2-12.162,15.464l-2.625-14-6.147,14.6c-2.088.01-4.281-.03-6.555-.072l-2.084-4.292-4.648,4.215c-18.616.028-33.661,2.741-33.661-15.917C802.274,510.3,808.784,477.059,836.215,477.059Z" transform="translate(857.69 -595.41)" fill="#2f2e41"/>
</g>
<g id="Group_2" data-name="Group 2" transform="translate(684 38)">
<path id="Path_53-57" data-name="Path 53" d="M820.737,295.13V269.263a2.333,2.333,0,0,0-2.333-2.333h-4.666a2.333,2.333,0,0,0-2.333,2.333V293.57a14.745,14.745,0,1,0,9.331,1.56Z" transform="translate(-688.018 71.866)" fill="#6c63ff"/>
<circle id="Ellipse_8" data-name="Ellipse 8" cx="6.998" cy="6.998" r="6.998" transform="translate(119.5 373.01)" fill="#fff"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.2 KiB

1
src/assets/logo.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69" xmlns:v="https://vecta.io/nano"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View file

@ -0,0 +1,48 @@
<script setup>
defineProps({
name: String,
author: String,
art: {
type: String,
default: '--art: linear-gradient(45deg, #88c0d0, #5e81ac);',
},
});
</script>
<template>
<div class="album card pop" @click="onClick">
<div class="card-bg bg-img pop-2" :style="art"></div>
<div class="card-text">
<h4>{{ name }}</h4>
<i>{{ author }}</i>
</div>
</div>
</template>
<script>
export default {
methods: {
onClick() {
this.$emit('open-album');
},
},
};
</script>
<style scoped>
.card {
min-height: 17rem;
width: 15rem;
border-radius: 0.5rem;
padding: 1rem;
margin: auto;
}
.card:hover {
background-color: var(--color-background);
}
.card-bg {
height: 13rem;
width: 13rem;
}
</style>

86
src/components/Artist.vue Normal file
View file

@ -0,0 +1,86 @@
<script setup>
import { ref } from 'vue';
import PlayBtn from './PlayBtn.vue';
defineProps(['title', 'desc', 'subs', 'thumbs', 'play']);
defineEmits(['playall']);
</script>
<template>
<div v-if="title" class="us-wrap">
<div class="bg-imgfill" :style="'--art: url(' + thumbs[1].url + ');'"></div>
<div class="us-main">
<h2>{{ title }}</h2>
<p @click="$event.target.classList.toggle('more')">{{ desc }}</p>
<div class="us-playwrap">
<PlayBtn @click="$emit('playall', '?list=' + play)" />
<span class="us-box subs">{{ subs || 0 }}</span>
</div>
</div>
</div>
</template>
<style scoped>
.us-wrap {
min-height: 5rem;
margin-bottom: 5rem;
}
.bg-imgfill {
position: absolute;
width: 100%;
height: 100%;
background-image: var(--art);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
filter: blur(5px) opacity(50%);
}
.us-main {
color: #fff;
border-radius: 0.5rem;
display: flex;
flex-direction: column;
padding: 1rem;
box-shadow: inset 0 0 10rem #000;
}
h2 {
padding: 1rem;
font-size: 2rem;
}
p {
--line: 3;
display: -webkit-box;
-webkit-line-clamp: var(--line);
-webkit-box-orient: vertical;
hyphens: auto;
margin: 1rem;
font-size: 1rem;
line-height: 1.25rem;
overflow: hidden;
}
p.more {
--line: auto;
}
.us-playwrap {
padding: 1rem;
}
.us-box {
border-radius: 0.25rem;
font-weight: bold;
margin-left: 2rem;
padding: 0.5rem;
border: 0.125rem solid var(--color-foreground);
background: var(--color-background-mute);
color: var(--color-foreground);
box-shadow: 0 0 1rem var(--color-background-mute);
}
.subs:after {
content: ' Subscribers';
font-weight: bold;
}
@media (max-width: 400px) {
.subs:after {
content: ' Subs';
}
}
</style>

34
src/components/NavBar.vue Normal file
View file

@ -0,0 +1,34 @@
<script setup>
defineEmits(['update-search']);
import SearchBar from '../components/SearchBar.vue';
</script>
<template>
<nav>
<h1>Hyperpipe</h1>
<div class="wrap">
<SearchBar
@update-search="
(e) => {
$emit('update-search', e);
}
" />
</div>
</nav>
</template>
<style scoped>
nav {
width: 100%;
margin-bottom: 2rem;
display: flex;
align-items: center;
}
h1 {
font-size: 2rem;
}
.wrap {
margin-left: auto;
}
</style>

View file

@ -0,0 +1,39 @@
<script setup>
defineProps({
title: {
type: String,
default: '',
},
artist: {
type: String,
default: '',
},
});
</script>
<template>
<div v-if="title && artist" class="wrap">
<h1>{{ title }}</h1>
<h3>{{ artist }}</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.wrap {
text-align: center;
}
@media (min-width: 1024px) {
.wrap {
text-align: left;
}
}
</style>

View file

@ -0,0 +1,30 @@
<script setup>
defineEmits(['click']);
</script>
<template>
<button class="bi bi-play" @click="$emit('click')"></button>
</template>
<style scoped>
button {
height: 4rem;
width: 4rem;
font-size: 4rem;
color: #fff;
padding: 0;
line-height: 0;
background: var(--color-foreground);
border-radius: 50%;
vertical-align: -1rem;
text-align: center;
transition: background 0.4s ease;
margin-right: auto;
}
button:before {
padding-left: 0.2rem;
}
button:hover {
background: transparent;
}
</style>

View file

@ -0,0 +1,89 @@
<script setup>
defineProps(['url', 'urls', 'show']);
defineEmits(['playthis']);
</script>
<template>
<div v-if="show" class="modal placeholder">
<template v-for="plurl in urls">
<div class="pl-item" @click="$emit('playthis', plurl)">
<span v-if="url == plurl.url" class="bars-wrap">
<div class="bars"></div>
<div class="bars"></div>
<div class="bars"></div>
</span>
<span class="pl-main caps">{{ plurl.title }}</span>
</div>
</template>
</div>
</template>
<style scoped>
.modal {
display: flex;
flex-direction: column;
position: fixed;
top: 2rem;
bottom: 5rem;
right: 1rem;
width: 30rem;
max-width: calc(100% - 2rem);
background: var(--color-background-mute);
border-radius: 0.5rem;
z-index: 99999;
box-shadow: 0.1rem 0.1rem 1rem var(--color-shadow);
padding: 1rem;
transition: width 0.4s ease;
animation: fade 0.4s ease;
overflow-y: auto;
}
.placeholder:empty:before {
--url: url('../assets/bg_playlist.svg');
}
.placeholder:empty:after {
--text: 'Add Songs to Your Playlist...';
padding-top: 2rem;
}
.pl-item {
padding: 1rem;
margin: 0.125rem;
border-radius: 0.25rem;
background: var(--color-background);
}
.pl-item:hover {
background: var(--color-background-soft);
}
.pl-main {
padding-left: 3rem;
}
.bars-wrap {
position: absolute;
height: 1.5rem;
width: 2rem;
transform: rotateZ(180deg);
}
.bars {
position: relative;
height: 15%;
width: calc(calc(100% / 3) - 0.2rem);
margin-left: 0.1rem;
background: var(--color-foreground);
float: left;
animation: heightc 1s ease infinite;
}
.bars:first-child {
animation-delay: 0.25s;
}
.bars:nth-child(2) {
animation-delay: 0.5s;
}
.bars:last-child {
margin-left: none;
}
@keyframes heightc {
50% {
height: 100%;
}
}
</style>

234
src/components/Search.vue Normal file
View file

@ -0,0 +1,234 @@
<script setup>
defineProps(['search', 'songItems', 'items']);
defineEmits([
'get-song',
'get-album',
'get-artist',
'lazy',
'play-urls',
'add-song',
]);
</script>
<template>
<div v-if="songs && songs.corrected" class="text-full">
I Fixed your Typo, "<span class="caps">{{ songs.suggestion }}</span
>"!!
</div>
<div v-if="albumTitle" class="text-full flex">
<PlayBtn @click="playAlbum" />
<span>{{ albumTitle }}</span>
</div>
<div v-if="songs && songs.items[0]" class="search-songs">
<h2>Top Songs</h2>
<div class="grid">
<template v-for="song in songs.items">
<SongItem
:author="song.uploaderName || ''"
:title="song.title || song.name"
:channel="song.uploaderUrl || ''"
:play="song.url || song.watchId"
@open-song="
$emit('get-song', {
url: song.url || song.watchId,
title: song.title,
})
"
@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>
</div>
<a
v-if="this.notes"
@click.prevent="$emit('get-album', '/playlist?list=' + this.notes.items)"
class="more"
>See All</a
>
</div>
<div v-if="albums && albums.items[0]" class="search-albums">
<h2>Albums</h2>
<div class="grid-3">
<template v-for="album in albums.items">
<AlbumItem
:author="album.uploaderName || album.subtitle"
:name="album.name || album.title"
:art="
'--art: url(' + (album.thumbnail || album.thumbnails[0].url) + ');'
"
@open-album="
$emit(
'get-album',
album.url || '/playlist?list=' + album.playlistId,
)
" />
</template>
</div>
</div>
<div
v-if="recommendedArtists && recommendedArtists.items[0]"
class="search-artists">
<h2>Similar Artists</h2>
<div class="grid-3">
<template v-for="artist in recommendedArtists.items">
<AlbumItem
:author="artist.subtitle"
:name="artist.title"
:art="'--art: url(' + artist.thumbnails[0].url + ');'"
@open-album="$emit('get-artist', artist.artistId)" />
</template>
</div>
</div>
</template>
<script>
import PlayBtn from './PlayBtn.vue';
import SongItem from './SongItem.vue';
import AlbumItem from './AlbumItem.vue';
export default {
components: {
PlayBtn,
SongItem,
AlbumItem,
},
data() {
return {
songs: null,
albums: null,
recommendedArtists: null,
albumTitle: null,
notes: null,
};
},
watch: {
search(NewSearch) {
console.log(NewSearch);
this.getSearch(NewSearch);
},
songItems(i) {
console.log(i);
this.Reset();
this.songs = {};
this.songs.items = i.items;
this.albumTitle = i.title;
},
items(itms) {
this.Reset();
for (let i in itms) {
this[i] = {};
this[i].items = itms[i];
console.log(this[i]);
}
},
},
methods: {
Reset() {
this.notes = null;
this.albums = null;
this.albumTitle = null;
this.songs = null;
this.recommendedArtists = null;
},
playAlbum() {
const urls = this.songs.items.map((item) => {
return { url: item.url, title: item.title };
});
this.$emit('play-urls', urls);
},
getSearch(q) {
if (q) {
history.pushState({}, '', `/search/${q}`);
document.title = 'Search Results for ' + q;
this.getResults(q.split(' ').join('+'));
this.$emit('lazy');
} else {
this.Reset();
history.pushState({}, '', '/');
document.title = 'Hyperpipe';
console.log('No Search');
}
},
getResults(q) {
const filters = ['music_songs', 'music_albums'];
for (let filter of filters) {
fetch(
`https://pipedapi.kavin.rocks/search?q=${q}&filter=${filter}`,
).then((res) => {
res.json().then((json) => {
this[filter.split('_')[1]] = json;
console.log(json);
});
});
}
},
},
};
</script>
<style scoped>
.search-albums,
.search-songs,
.search-artists {
place-items: start center;
margin-bottom: 2rem;
}
.search-albums .grid-3,
.search-artists .grid-3 {
display: grid;
grid-template-columns: 1fr;
}
.text-full {
padding: 1rem;
font-size: 1.5rem;
text-align: center;
margin-bottom: 1rem;
}
.text-right {
text-align: right;
}
.song-bg {
width: 120px;
height: 120px;
}
.more {
margin: 1.5rem 0.5rem;
font-weight: bold;
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>

View file

@ -0,0 +1,42 @@
<script setup>
defineProps(['search']);
defineEmits(['update-search']);
</script>
<template>
<button class="bi bi-search popup-wrap">
<div class="popup">
<input
type="text"
placeholder="Search..."
@change="$emit('update-search', $event.target.value)"
:value="search" />
</div>
</button>
</template>
<style scoped>
.popup {
right: 0;
top: -0.5rem;
bottom: -0.5rem;
padding: 0;
}
.popup input {
color: var(--color-text);
--width: calc(100vw - 4rem);
width: 1.5rem;
max-width: 600px;
font-size: 1rem;
border: none;
border-radius: inherit;
background: var(--color-background-mute);
outline: none;
text-align: center;
animation: fill 0.4s ease;
transform: width 0.4s ease;
}
.popup input:hover {
width: var(--width);
}
</style>

View file

@ -0,0 +1,96 @@
<script setup>
defineProps({
author: String,
title: String,
channel: String,
play: String,
});
defineEmits(['get-artist']);
</script>
<template>
<div class="song card flex pop" @click="openSong($event.target)">
<slot name="art"></slot>
<span class="flex content">
<h4>{{ title }}</h4>
<a
:href="channel"
@click.prevent="$emit('get-artist', channel.split('/')[2])"
class="ign"
><i class="ign">{{ author }}</i></a
>
</span>
<span class="bi bi-three-dots-vertical popup-wrap ign">
<div class="popup ign">
<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>
</div>
</span>
</div>
</template>
<script>
export default {
methods: {
openSong(el) {
if (!el.classList.contains('ign')) {
this.$emit('open-song', this.play);
}
},
async Share() {
if ('share' in navigator) {
const data = {
title: `Listen to ${this.title} by ${this.author} on Hyperpipe`,
url: location.origin + this.play,
};
try {
await navigator.share(data);
console.log('Done Sharing!');
} catch (err) {
console.log(err);
}
} else {
navigator.clipboard.writeText(location.host + this.play).then(
() => {
console.log('Copied to Clipboard');
},
(err) => {
console.log(err);
},
);
}
},
},
};
</script>
<style scoped>
.card {
margin: 1rem 0;
justify-content: initial;
}
span.content {
padding: 1rem;
flex-direction: column;
align-items: initial;
flex-basis: calc(calc(100% - 120px) - 4rem);
}
span.bi-three-dots-vertical {
margin: 2rem;
}
.popup {
line-height: auto;
height: auto;
right: 0;
bottom: 0;
box-shadow: var(--shadow);
border-radius: 0.25rem;
}
.popup span {
padding: 0.5rem;
}
</style>

View file

@ -0,0 +1,150 @@
<script setup>
const props = defineProps({
state: String,
time: Number,
show: Boolean,
loop: Boolean,
});
defineEmits(['vol', 'play', 'list', 'loop']);
console.log(props);
</script>
<template>
<div id="statusbar" class="flex">
<div class="flex statusbar-left">
<button
id="btn-play-pause"
:class="'bi bi-' + state"
@click="$emit('play')"></button>
<div
id="statusbar-progress"
class="progress"
:style="'--tw: ' + time + '%;'">
<div class="progress-thumb"></div>
</div>
</div>
<div class="flex statusbar-right">
<button id="vol-btn" class="popup-wrap bi bi-volume-up">
<div id="vol" class="popup">
<input
id="vol-input"
type="range"
value="1"
max="1"
step=".01"
@input="$emit('vol', $event.target.value)" />
</div>
</button>
<button
id="list-btn"
:class="'bi bi-music-note-list ' + show"
@click="$emit('list', 'showplaylist')"></button>
<button
id="loop-btn"
:class="'bi bi-infinity ' + loop"
@click="$emit('loop', 'loop')"></button>
</div>
</div>
</template>
<style scoped>
#statusbar {
position: fixed;
left: 0;
bottom: 0;
right: 0;
padding: 0.5rem 0;
border-top: 0.25rem solid var(--color-foreground);
background: var(--color-background);
min-height: 15vh;
}
.statusbar-left {
margin-left: auto;
}
.statusbar-right {
margin-left: 0.5rem;
margin-right: auto;
}
.bi-play,
.bi-pause {
font-size: 2.5rem !important;
}
.bi-volume-up {
font-size: 1.5rem !important;
}
.bi-infinity {
font-size: 1.75rem !important;
}
.progress {
--h: 0.25rem;
--w: 5rem;
--th: 100%;
--tw: 75%;
background: var(--color-border);
height: var(--h);
width: var(--w);
transition: width 0.4s ease;
}
.progress.v {
--th: 75%;
--tw: 100%;
--h: 5rem;
--w: 0.25rem;
transform: rotateZ(180deg);
z-index: 999999;
}
.progress-thumb {
width: var(--tw);
height: var(--th);
background: var(--color-foreground);
overflow: hidden;
transition: width 0.4s ease;
}
.progress-thumb:after {
content: '';
background-color: #fff;
height: 0.5rem;
width: 0.5rem;
border-radius: 50%;
}
.popup {
--h: 6.5rem;
--w: 1rem;
transform: rotateZ(270deg) translateX(calc(calc(var(--h) / 2) - 0.5rem))
translateY(calc(calc(var(--w) + 2rem) * -1));
}
input[type='range'] {
width: var(--h);
height: var(--w);
appearance: none;
-webkit-appearence: none;
border: none;
border-radius: 5rem;
background: transparent;
color: transparent;
cursor: pointer;
}
input[type='range']:focus {
outline: none;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
background-color: var(--color-foreground);
height: 1rem;
width: 1rem;
border-radius: 50%;
margin-top: -0.45rem;
}
input[type='range']::-webkit-slider-runnable-track {
height: 0.1rem;
background-color: var(--color-border);
}
#statusbar-progress {
width: 50vw;
min-width: 200px;
max-width: 500px;
}
</style>

6
src/main.js Normal file
View file

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