mirror of
https://codeberg.org/Hyperpipe/Hyperpipe
synced 2025-06-27 20:58:01 +02:00
Initial Commit
This commit is contained in:
commit
98d558ad6f
27 changed files with 3097 additions and 0 deletions
529
src/App.vue
Normal file
529
src/App.vue
Normal 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
115
src/assets/base.css
Normal 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
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 |
37
src/assets/bg_playlist.svg
Normal file
37
src/assets/bg_playlist.svg
Normal 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
1
src/assets/logo.svg
Normal 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 |
48
src/components/AlbumItem.vue
Normal file
48
src/components/AlbumItem.vue
Normal 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
86
src/components/Artist.vue
Normal 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
34
src/components/NavBar.vue
Normal 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>
|
39
src/components/NowPlaying.vue
Normal file
39
src/components/NowPlaying.vue
Normal 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>
|
30
src/components/PlayBtn.vue
Normal file
30
src/components/PlayBtn.vue
Normal 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>
|
89
src/components/Playlists.vue
Normal file
89
src/components/Playlists.vue
Normal 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
234
src/components/Search.vue
Normal 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>
|
42
src/components/SearchBar.vue
Normal file
42
src/components/SearchBar.vue
Normal 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>
|
96
src/components/SongItem.vue
Normal file
96
src/components/SongItem.vue
Normal 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>
|
150
src/components/StatusBar.vue
Normal file
150
src/components/StatusBar.vue
Normal 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
6
src/main.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.mount('#app');
|
Loading…
Add table
Add a link
Reference in a new issue