- Added Authentication for Piped Accounts
- Localization
This commit is contained in:
Shiny Nematoda 2022-09-03 06:27:27 +00:00
parent 8548a3646e
commit de6572eee4
No known key found for this signature in database
GPG key ID: 6506D50F5613A42D
26 changed files with 1045 additions and 596 deletions

View file

@ -22,9 +22,6 @@
<div id="app"></div> <div id="app"></div>
<!--link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@latest/font/bootstrap-icons.css" /-->
<script type="module" src="/src/main.js" defer></script> <script type="module" src="/src/main.js" defer></script>
</body> </body>
</html> </html>

935
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -12,18 +12,19 @@
"dependencies": { "dependencies": {
"bootstrap-icons": "^1.9.1", "bootstrap-icons": "^1.9.1",
"buffer": "^6.0.3", "buffer": "^6.0.3",
"dompurify": "^2.3.10", "dompurify": "^2.4.0",
"mux.js": "^6.2.0", "mux.js": "^6.2.0",
"peerjs": "^1.4.6", "peerjs": "^1.4.7",
"pinia": "^2.0.16", "pinia": "^2.0.21",
"shaka-player": "^4.1.2", "shaka-player": "^4.2.1",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"vue": "^3.2.31", "vue": "^3.2.38",
"vue-i18n": "^9.2.2",
"xml-js": "^1.6.11" "xml-js": "^1.6.11"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^3.0.1", "@vitejs/plugin-vue": "^3.0.3",
"prettier": "^2.6.2", "prettier": "^2.7.1",
"vite": "^3.0.3" "vite": "^3.0.9"
} }
} }

View file

@ -18,7 +18,8 @@ import Prefs from '@/components/Prefs.vue';
/* Composables */ /* Composables */
import { getJsonHyp, getJsonPiped } from '@/scripts/fetch.js'; import { getJsonHyp, getJsonPiped } from '@/scripts/fetch.js';
import { useLazyLoad, useStore, useRoute } from '@/scripts/util.js'; import { useStore, useRoute } from '@/scripts/util.js';
import { useT, useSetupLocale } from '@/scripts/i18n.js';
import { useSetupDB, useUpdatePlaylist } from '@/scripts/db.js'; import { useSetupDB, useUpdatePlaylist } from '@/scripts/db.js';
/* Stores */ /* Stores */
@ -283,35 +284,17 @@ function setMetadata() {
} }
} }
function SaveTrack(e) {
useUpdatePlaylist(
e,
{
url: data.state.url,
title: data.state.title,
},
e => {
if (e === true) {
console.log('Added Song To ' + e);
}
},
);
}
onBeforeMount(() => { onBeforeMount(() => {
if (store.theme) { if (store.theme) {
document.body.setAttribute('data-theme', store.theme); document.body.setAttribute('data-theme', store.theme);
} }
if (store.locale) {
useSetupLocale(store.locale);
}
}); });
onMounted(() => { onMounted(() => {
useLazyLoad();
/* Event Listeners for Lazy Loading */
document.addEventListener('scroll', useLazyLoad);
document.addEventListener('resize', useLazyLoad);
document.addEventListener('orientationChange', useLazyLoad);
/* Event Listener for change in url */ /* Event Listener for change in url */
window.addEventListener('popstate', parseUrl); window.addEventListener('popstate', parseUrl);
@ -355,17 +338,18 @@ onMounted(() => {
</template> </template>
<header v-if="!artist.state.title"> <header v-if="!artist.state.title">
<div <img
v-show="data.state.art" v-if="data.state.art"
class="art bg-img" class="art"
:style="'--art: url(' + data.state.art + ')'"></div> loading="lazy"
:src="data.state.art" />
<div class="wrapper"> <div class="wrapper">
<NowPlaying @get-artist="getArtist" /> <NowPlaying @get-artist="getArtist" />
</div> </div>
</header> </header>
<main class="placeholder"> <main class="placeholder" :data-placeholder="useT('info.search')">
<KeepAlive> <KeepAlive>
<Search <Search
v-if="nav.state.page == 'home'" v-if="nav.state.page == 'home'"
@ -382,7 +366,10 @@ onMounted(() => {
@get-album="getAlbum" /> @get-album="getAlbum" />
</KeepAlive> </KeepAlive>
<NewPlaylist v-if="nav.state.page == 'library'" @play-urls="playList" /> <NewPlaylist
v-if="nav.state.page == 'library'"
@play-urls="playList"
@open-playlist="getAlbum" />
<Prefs v-if="nav.state.page == 'prefs'" /> <Prefs v-if="nav.state.page == 'prefs'" />
</main> </main>
@ -399,7 +386,7 @@ onMounted(() => {
<Info v-if="player.state.info" :text="data.state.description" /> <Info v-if="player.state.info" :text="data.state.description" />
</Transition> </Transition>
<StatusBar @save="SaveTrack" /> <StatusBar />
<Player @ended="playNext" /> <Player @ended="playNext" />
</template> </template>

View file

@ -153,9 +153,8 @@ body {
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.placeholder:empty:after { .placeholder:empty:after {
--text: 'Start Searching...'; content: attr(data-placeholder)'...';
margin-bottom: auto; margin-bottom: auto;
content: var(--text);
letter-spacing: 0.125rem; letter-spacing: 0.125rem;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: bolder; font-weight: bolder;
@ -215,16 +214,16 @@ button {
appearence: none; appearence: none;
outline: none; outline: none;
} }
img {
object-fit: cover;
}
.bg-img { .bg-img {
appearance: none;
background-image: var(--grad); background-image: var(--grad);
background-position: center; background-position: center;
background-size: cover; background-size: cover;
border-radius: 0.25rem; border-radius: 0.25rem;
} }
.bg-img.lazy {
background-image: var(--art);
}
.search-artists .bg-img { .search-artists .bg-img {
border-radius: 50%; border-radius: 50%;
} }

View file

@ -10,7 +10,10 @@ const props = defineProps({
default: '', default: '',
}, },
grad: String, grad: String,
art: String, art: {
type: String,
default: 'https://upload.wikimedia.org/wikipedia/commons/c/ca/1x1.png',
},
}); });
defineEmits(['open-album']); defineEmits(['open-album']);
@ -18,7 +21,7 @@ defineEmits(['open-album']);
<template> <template>
<div class="album card pop" @click="$emit('open-album')"> <div class="album card pop" @click="$emit('open-album')">
<div class="card-bg bg-img pop-2"></div> <img class="card-bg bg-img pop-2" :src="art" loading="lazy" alt />
<div class="card-text"> <div class="card-text">
<h4>{{ name }}</h4> <h4>{{ name }}</h4>
@ -40,7 +43,6 @@ defineEmits(['open-album']);
} }
.card-bg { .card-bg {
--grad: v-bind('grad || rand'); --grad: v-bind('grad || rand');
--art: v-bind('art || grad || rand');
height: 13rem; height: 13rem;
width: 13rem; width: 13rem;
} }

View file

@ -1,6 +1,6 @@
<script setup> <script setup>
import { ref, onUpdated } from 'vue'; import { ref, onUpdated } from 'vue';
import PlayBtn from './PlayBtn.vue'; import Btn from './Btn.vue';
import { useArtist } from '@/stores/results.js'; import { useArtist } from '@/stores/results.js';
@ -18,16 +18,14 @@ onUpdated(() => {
<template> <template>
<div v-if="show == 0 && artist.state.title" class="us-wrap"> <div v-if="show == 0 && artist.state.title" class="us-wrap">
<div <img class="bg-imgfill" :src="artist.state.thumbnails[1].url" />
class="bg-imgfill"
:style="'--art: url(' + artist.state.thumbnails[1].url + ');'"></div>
<div class="us-main"> <div class="us-main">
<h2>{{ artist.state.title }}</h2> <h2>{{ artist.state.title }}</h2>
<p @click="$event.target.classList.toggle('more')"> <p @click="$event.target.classList.toggle('more')">
{{ artist.state.description }} {{ artist.state.description }}
</p> </p>
<div class="us-playwrap"> <div class="us-playwrap">
<PlayBtn <Btn
@click=" @click="
$emit('playall', '/playlist?list=' + artist.state.playlistId) $emit('playall', '/playlist?list=' + artist.state.playlistId)
" /> " />
@ -46,10 +44,6 @@ onUpdated(() => {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
background-image: var(--art);
background-size: cover;
background-position: center;
background-repeat: no-repeat;
filter: blur(5px) opacity(50%); filter: blur(5px) opacity(50%);
} }
.us-main { .us-main {
@ -59,6 +53,7 @@ onUpdated(() => {
flex-direction: column; flex-direction: column;
padding: 1rem; padding: 1rem;
box-shadow: inset 0 0 10rem #000; box-shadow: inset 0 0 10rem #000;
z-index: 1;
} }
h2 { h2 {
padding: 1rem; padding: 1rem;

View file

@ -32,6 +32,7 @@ defineEmits(['click']);
background: transparent; background: transparent;
} }
.bi:not(.bi-play) { .bi:not(.bi-play) {
font-size: 3rem; font-size: 1.75rem;
width: 3.5rem;
} }
</style> </style>

View file

@ -2,8 +2,8 @@
import { reactive, ref, onMounted, onUnmounted } from 'vue'; import { reactive, ref, onMounted, onUnmounted } from 'vue';
import { getJsonHyp } from '../scripts/fetch.js'; import { getJsonHyp } from '../scripts/fetch.js';
import { useRandColor } from '../scripts/colors.js';
import { useRoute } from '../scripts/util.js'; import { useRoute } from '../scripts/util.js';
import { useT } from '@/scripts/i18n.js';
import AlbumItem from './AlbumItem.vue'; import AlbumItem from './AlbumItem.vue';
@ -55,18 +55,18 @@ onMounted(get);
<template> <template>
<template v-if="data.title"> <template v-if="data.title">
<i class="bi bi-arrow-left back" @click="get"> Back</i> <i class="bi bi-arrow-left back" @click="get"> {{ useT('action.back') }}</i>
<h2 class="head">{{ data.title }}</h2> <h2 class="head">{{ data.title }}</h2>
<template v-for="type in ['featured', 'spotlight', 'community']"> <template v-for="type in ['featured', 'spotlight', 'community']">
<h3 class="head">{{ type }}</h3> <h3 class="head">{{ useT('title.' + type) }}</h3>
<div class="grid-3"> <div class="grid-3">
<template v-for="i in data[type]"> <template v-for="i in data[type]">
<AlbumItem <AlbumItem
:name="i.title" :name="i.title"
:author="i.subtitle" :author="i.subtitle"
:art="'url(' + i.thumbnails[0].url + ')'" :art="i.thumbnails[0].url"
@open-album=" @open-album="
$emit('get-album', '/playlist?list=' + i.id); $emit('get-album', '/playlist?list=' + i.id);
nav.state.page = 'home'; nav.state.page = 'home';
@ -77,7 +77,7 @@ onMounted(get);
</template> </template>
<template v-else> <template v-else>
<h2 v-if="btns.moods.length > 0">Moods</h2> <h2 v-if="btns.moods.length > 0">{{ useT('title.moods') }}</h2>
<div class="btn-grid"> <div class="btn-grid">
<button <button
@ -89,7 +89,7 @@ onMounted(get);
</button> </button>
</div> </div>
<h2 v-if="btns.genres.length > 0">Genres</h2> <h2 v-if="btns.genres.length > 0">{{ useT('title.genres') }}</h2>
<div class="btn-grid"> <div class="btn-grid">
<button <button

View file

@ -1,4 +1,5 @@
<script setup> <script setup>
import { useT } from '@/scripts/i18n.js';
import TextModal from './TextModal.vue'; import TextModal from './TextModal.vue';
defineProps(['text']); defineProps(['text']);
@ -10,7 +11,7 @@ const parse = d =>
<template> <template>
<TextModal> <TextModal>
<template #content> <template #content>
<pre class="placeholder">{{ <pre class="placeholder" :data-placeholder="useT('info.no_info')">{{
text ? parse(text.replaceAll('<br>', '\n')) : '' text ? parse(text.replaceAll('<br>', '\n')) : ''
}}</pre> }}</pre>
</template> </template>
@ -21,7 +22,4 @@ const parse = d =>
.placeholder:empty::before { .placeholder:empty::before {
--ico: '\F3B9'; --ico: '\F3B9';
} }
.placeholder:empty::after {
--text: 'No Information Available...';
}
</style> </style>

View file

@ -3,6 +3,7 @@ import { ref, watch } from 'vue';
import { getJsonHyp } from '@/scripts/fetch.js'; import { getJsonHyp } from '@/scripts/fetch.js';
import { useData } from '@/stores/player.js'; import { useData } from '@/stores/player.js';
import { useT } from '@/scripts/i18n.js';
import TextModal from './TextModal.vue'; import TextModal from './TextModal.vue';
@ -48,7 +49,11 @@ watch(
<template #content> <template #content>
<pre <pre
class="placeholder" class="placeholder"
:data-loaded="data.state.urls[0]?.url ? status : true" :data-placeholder="
data.state.urls[0]?.url && !status
? useT('info.lyrics.load')
: useT('info.lyrics.void')
"
>{{ text }}</pre >{{ text }}</pre
> >
<div>{{ source }}</div> <div>{{ source }}</div>
@ -60,10 +65,4 @@ watch(
.placeholder:empty::before { .placeholder:empty::before {
--ico: '\f3a5'; --ico: '\f3a5';
} }
.placeholder[data-loaded='false']:empty::after {
--text: 'Fetching Lyrics...';
}
.placeholder[data-loaded='true']:empty::after {
--text: 'No Lyrics...';
}
</style> </style>

View file

@ -58,6 +58,9 @@ watch(show, n => {
padding: 1rem 2rem; padding: 1rem 2rem;
border-bottom: 1px solid var(--color-shadow); border-bottom: 1px solid var(--color-shadow);
} }
.modal-title:after {
content: '...';
}
.modal-content { .modal-content {
padding: 1rem; padding: 1rem;
max-height: calc(90vh - 8rem); max-height: calc(90vh - 8rem);

View file

@ -1,9 +1,13 @@
<script setup> <script setup>
import { ref, reactive, watch, onMounted } from 'vue'; import { ref, reactive, watch, onMounted } from 'vue';
import AlbumItem from './AlbumItem.vue'; import AlbumItem from './AlbumItem.vue';
import Modal from './Modal.vue'; import Modal from './Modal.vue';
import { useRand } from '../scripts/colors.js'; import { useRand } from '@/scripts/colors.js';
import { useStore } from '@/scripts/util.js';
import { getJsonAuth, getAuthPlaylists } from '@/scripts/fetch.js';
import { useT } from '@/scripts/i18n.js';
import { import {
useListPlaylists, useListPlaylists,
@ -12,7 +16,10 @@ import {
useUpdatePlaylist, useUpdatePlaylist,
} from '../scripts/db.js'; } from '../scripts/db.js';
const emit = defineEmits(['play-urls']), const store = useStore(),
auth = ref(!!store.auth);
const emit = defineEmits(['play-urls', 'open-playlist']),
list = ref([]), list = ref([]),
show = reactive({ show = reactive({
new: false, new: false,
@ -24,8 +31,16 @@ const emit = defineEmits(['play-urls']),
id: 'Please Wait...', id: 'Please Wait...',
to: '', to: '',
peer: undefined, peer: undefined,
}),
user = reactive({
username: undefined,
password: undefined,
playlists: [],
create: false,
}); });
const pathname = url => new URL(url).pathname;
const Play = key => { const Play = key => {
console.log(key); console.log(key);
@ -66,6 +81,44 @@ const Play = key => {
}); });
}; };
const Login = async () => {
if (user.username && user.password) {
const { token } = await getJsonAuth('/login', {
method: 'POST',
body: JSON.stringify({
username: user.username,
password: user.password,
}),
});
store.setItem('auth', token);
auth.value = true;
}
},
getPlaylists = async () => {
const res = await getAuthPlaylists();
user.playlists = res;
console.log(user.playlists);
},
createPlaylist = async () => {
if (text.value) {
const res = await getJsonAuth('/user/playlists/create', {
method: 'POST',
body: JSON.stringify({
name: `Playlist - ${text.value}`,
}),
headers: {
Authorization: store.auth,
'Content-Type': 'application/json',
},
});
getPlaylists();
show.new = false;
}
};
watch( watch(
() => show.sync, () => show.sync,
async () => { async () => {
@ -119,7 +172,12 @@ watch(
}, },
); );
onMounted(List); watch(auth, getPlaylists);
onMounted(async () => {
await getPlaylists();
List();
});
</script> </script>
<template> <template>
@ -127,13 +185,22 @@ onMounted(List);
<Modal <Modal
n="2" n="2"
:display="show.new" :display="show.new"
title="Create a new Playlist..." :title="useT('playlist.create')"
@show=" @show="
e => { e => {
show.new = e; show.new = e;
} }
"> ">
<template #content> <template #content>
<div v-if="auth" class="tabs">
<button :data-active="!user.create" @click="user.create = false">
{{ useT('title.local') }}
</button>
<button :data-active="user.create" @click="user.create = true">
{{ useT('title.remote') }}
</button>
</div>
<input <input
type="text" type="text"
placeholder="Playlist name..." placeholder="Playlist name..."
@ -141,15 +208,17 @@ onMounted(List);
v-model="text" /> v-model="text" />
</template> </template>
<template #buttons> <template #buttons>
<button @click="show.new = false">Cancel</button> <button @click="show.new = false">{{ useT('action.cancel') }}</button>
<button @click="Create">Create</button> <button @click="user.create ? createPlaylist() : Create()">
{{ useT('action.create') }}
</button>
</template> </template>
</Modal> </Modal>
<Modal <Modal
:n="sync.type == 'send' ? 2 : 1" :n="sync.type == 'send' ? 2 : 1"
:display="show.sync" :display="show.sync"
title="Sync Playlists..." :title="useT('playlist.sync')"
@show=" @show="
e => { e => {
show.sync = e; show.sync = e;
@ -160,10 +229,10 @@ onMounted(List);
<button <button
:data-active="sync.type == 'send'" :data-active="sync.type == 'send'"
@click="sync.type = 'send'"> @click="sync.type = 'send'">
Send {{ useT('action.send') }}
</button> </button>
<button :data-active="sync.type == 'rec'" @click="sync.type = 'rec'"> <button :data-active="sync.type == 'rec'" @click="sync.type = 'rec'">
Receive {{ useT('action.receive') }}
</button> </button>
</div> </div>
@ -181,9 +250,11 @@ onMounted(List);
</template> </template>
<template #buttons> <template #buttons>
<button @click="show.sync = false">Cancel</button> <button @click="show.sync = false">{{ useT('action.cancel') }}</button>
<button v-if="sync.type == 'send'" @click="Send"> <button v-if="sync.type == 'send'" @click="Send">
{{ sync.type == 'send' ? 'Send' : 'Recieve' }} {{
sync.type == 'send' ? useT('action.send') : useT('action.recieve')
}}
</button> </button>
</template> </template>
</Modal> </Modal>
@ -196,6 +267,8 @@ onMounted(List);
@click="show.sync = true"></div> @click="show.sync = true"></div>
</div> </div>
<h2 v-if="list.length > 0">{{ useT('playlist.local') }}</h2>
<div class="grid-3"> <div class="grid-3">
<template v-for="i in list"> <template v-for="i in list">
<AlbumItem <AlbumItem
@ -205,10 +278,48 @@ onMounted(List);
@open-album="Play(i.name)" /> @open-album="Play(i.name)" />
</template> </template>
</div> </div>
<h2 class="login-h">{{ useT('playlist.remote') }}</h2>
<div v-if="auth" class="grid-3">
<template v-for="i in user.playlists">
<AlbumItem
:name="i.name.replace('Playlist - ', '')"
:art="pathname(i.thumbnail) != '/' ? i.thumbnail : undefined"
@open-album="$emit('open-playlist', '/playlists?list=' + i.id)" />
</template>
</div>
<form v-else class="login" @submit.prevent>
<input
@change="user.username = $event.target.value"
type="text"
placeholder="username"
class="textbox" />
<input
@change="user.password = $event.target.value"
type="password"
placeholder="password"
class="textbox" />
<button @click="Login" class="textbox">{{ useT('title.login') }}</button>
<p>
Don't have an account? register on
<a
href="https://piped.kavin.rocks/register"
target="_blank"
rel="noreferrer noopener"
>Piped</a
>
</p>
</form>
</div> </div>
</template> </template>
<style scoped> <style scoped>
h2 {
text-align: center;
margin: 2rem;
}
.npl-wrap { .npl-wrap {
padding-bottom: 5rem; padding-bottom: 5rem;
} }
@ -244,7 +355,9 @@ pre {
width: calc(100% / 2); width: calc(100% / 2);
background: var(--color-background); background: var(--color-background);
} }
.tabs button[data-active='true'] { .tabs button[data-active='true'],
.login button {
font-weight: bold;
color: var(--color-background); color: var(--color-background);
background: linear-gradient(135deg, cornflowerblue, #88c0d0); background: linear-gradient(135deg, cornflowerblue, #88c0d0);
} }
@ -254,6 +367,15 @@ pre {
.tabs button:last-child { .tabs button:last-child {
border-radius: 0 0.25rem 0.25rem 0; border-radius: 0 0.25rem 0.25rem 0;
} }
.login {
display: block;
margin: 1rem auto;
}
.login > * {
margin: 1rem auto;
display: block;
text-align: center;
}
@media (min-width: 1024px) { @media (min-width: 1024px) {
.npl-box:first-child { .npl-box:first-child {
margin: 0 1rem 0 auto; margin: 0 1rem 0 auto;

View file

@ -10,8 +10,7 @@ import {
import muxjs from 'mux.js'; import muxjs from 'mux.js';
window.muxjs = muxjs; window.muxjs = muxjs;
import { useLazyLoad, useStore, useRoute } from '@/scripts/util.js'; import { useStore, useRoute } from '@/scripts/util.js';
import { useData, usePlayer } from '@/stores/player.js'; import { useData, usePlayer } from '@/stores/player.js';
defineEmits(['ended']); defineEmits(['ended']);
@ -23,8 +22,6 @@ const player = usePlayer(),
const audio = ref(null); const audio = ref(null);
function audioCanPlay() { function audioCanPlay() {
useLazyLoad();
player.state.status = 'pause'; player.state.status = 'pause';
audio.value.play().catch(err => { audio.value.play().catch(err => {
console.error(err); console.error(err);

View file

@ -1,5 +1,6 @@
<script setup> <script setup>
import { useData, usePlayer } from '@/stores/player.js'; import { useData, usePlayer } from '@/stores/player.js';
import { useT } from '@/scripts/i18n.js';
const player = usePlayer(), const player = usePlayer(),
data = useData(); data = useData();
@ -9,7 +10,7 @@ defineEmits(['playthis']);
<template> <template>
<Transition name="fade"> <Transition name="fade">
<div class="pl-modal placeholder"> <div class="pl-modal placeholder" :data-placeholder="useT('playlist.add')">
<template v-for="plurl in data.state.urls"> <template v-for="plurl in data.state.urls">
<div class="pl-item" @click="$emit('playthis', plurl)"> <div class="pl-item" @click="$emit('playthis', plurl)">
<span v-if="data.state.url == plurl.url" class="bars-wrap"> <span v-if="data.state.url == plurl.url" class="bars-wrap">
@ -51,9 +52,6 @@ defineEmits(['playthis']);
.placeholder:empty:before { .placeholder:empty:before {
--ico: '\f64d'; --ico: '\f64d';
} }
.placeholder:empty:after {
--text: 'Add Songs to Playlist...';
}
.pl-item { .pl-item {
padding: 1rem; padding: 1rem;
margin: 0.125rem; margin: 0.125rem;

View file

@ -1,24 +1,22 @@
<script setup> <script setup>
import { ref, onMounted } from 'vue'; import { ref, onMounted } from 'vue';
import { getJson } from '../scripts/fetch.js'; import { getJson } from '@/scripts/fetch.js';
import { useStore } from '../scripts/util.js'; import { SUPPORTED_LOCALES, useT, useSetupLocale } from '@/scripts/i18n.js';
import { useStore } from '@/scripts/util.js';
const instances = ref([]), const instances = ref([]),
hypInstances = ref([]), hypInstances = ref([]),
hls = ref(false),
next = ref(false); next = ref(false);
getJson('https://piped-instances.kavin.rocks').then(i => { getJson('https://piped-instances.kavin.rocks').then(i => {
instances.value = i; instances.value = i;
console.log(i); console.log(i);
}); });
getJson('https://raw.codeberg.page/Hyperpipe/pages/api/backend.json').then( getJson('https://raw.codeberg.page/Hyperpipe/pages/api/backend.json').then(
i => { i => {
hypInstances.value = i; hypInstances.value = i;
console.log(i); console.log(i);
}, },
); );
@ -45,18 +43,22 @@ function setTheme(theme) {
setStore('theme', theme); setStore('theme', theme);
} }
function setLang(locale) {
useSetupLocale(locale);
setStore('locale', locale);
}
function getStoreBool(key, ele) { function getStoreBool(key, ele) {
ele.value = getStore(key) || true; ele.value = getStore(key) || true;
} }
onMounted(() => { onMounted(() => {
getStoreBool('hls', hls);
getStoreBool('next', next); getStoreBool('next', next);
}); });
</script> </script>
<template> <template>
<h2>Theme</h2> <h2>{{ useT('pref.theme') }}</h2>
<select <select
id="pref-theme" id="pref-theme"
:value="getTheme()" :value="getTheme()"
@ -68,7 +70,16 @@ onMounted(() => {
<option value="nord">Nord</option> <option value="nord">Nord</option>
</select> </select>
<h2>Audio Player</h2> <h2>Language</h2>
<select
id="pref-lang"
:value="getStore('locale') || 'en'"
@change="setLang($event.target.value)">
<option v-for="i in SUPPORTED_LOCALES" :value="i.code">{{ i.name }}</option>
</select>
<h2>{{ useT('pref.player') }}</h2>
<div class="left"> <div class="left">
<input <input
@ -77,11 +88,11 @@ onMounted(() => {
id="pref-chk-next" id="pref-chk-next"
@change="setStore('next', $event.target.checked)" @change="setStore('next', $event.target.checked)"
v-model="next" /> v-model="next" />
<label for="pref-chk-next">Automatically Queue Songs</label> <label for="pref-chk-next">{{ useT('pref.auto_queue') }}</label>
</div> </div>
<div class="left"> <div class="left">
<label for="pref-codec">Codec</label> <label for="pref-codec">{{ useT('pref.codec') }}</label>
<select <select
id="pref-codec" id="pref-codec"
name="pref-codec" name="pref-codec"
@ -95,20 +106,20 @@ onMounted(() => {
</div> </div>
<div class="left"> <div class="left">
<label for="pref-quality">Quality</label> <label for="pref-quality">{{ useT('pref.quality') }}</label>
<select <select
id="pref-quality" id="pref-quality"
name="pref-quality" name="pref-quality"
:value="getStore('quality') || 'auto'" :value="getStore('quality') || 'auto'"
@change="setStore('quality', $event.target.value)"> @change="setStore('quality', $event.target.value)">
<option value="auto">auto</option> <option value="auto">{{ useT('pref.auto') }}</option>
<option value="best">best</option> <option value="best">{{ useT('pref.best') }}</option>
<option value="worst">worst</option> <option value="worst">{{ useT('pref.worst') }}</option>
</select> </select>
</div> </div>
<div class="left"> <div class="left">
<label for="pref-volume">Default Volume</label> <label for="pref-volume">{{ useT('pref.volume') }}</label>
<input <input
type="number" type="number"
name="pref-volume" name="pref-volume"
@ -119,7 +130,7 @@ onMounted(() => {
@change="setStore('vol', $event.target.value)" /> @change="setStore('vol', $event.target.value)" />
</div> </div>
<h2>Hyperpipe Instance</h2> <h2>{{ useT('pref.instances.hyp') }}</h2>
<select <select
v-if="hypInstances" v-if="hypInstances"
@ -137,8 +148,8 @@ onMounted(() => {
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>{{ useT('pref.instances.name') }}</th>
<th>Locations</th> <th>{{ useT('pref.instances.loc') }}</th>
</tr> </tr>
</thead> </thead>
<tbody v-for="i in hypInstances"> <tbody v-for="i in hypInstances">
@ -154,7 +165,7 @@ onMounted(() => {
</table> </table>
</div> </div>
<h2>Piped Instance</h2> <h2>{{ useT('pref.instances.piped') }}</h2>
<select <select
v-if="instances" v-if="instances"
:value="getStore('pipedapi') || 'pipedapi.kavin.rocks'" :value="getStore('pipedapi') || 'pipedapi.kavin.rocks'"
@ -167,15 +178,29 @@ onMounted(() => {
</option> </option>
</select> </select>
<h3>{{ useT('pref.instances.auth') }}</h3>
<select
v-if="instances"
:value="getStore('authapi') || 'pipedapi.kavin.rocks'"
@change="setStore('authapi', $event.target.value)">
<option
v-for="i in instances"
:key="i.name"
:value="i.api_url.replace('https://', '').replace('http://', '')">
{{ i.name.replace('Official', 'Default') }}
</option>
</select>
<div class="table-wrap"> <div class="table-wrap">
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>{{ useT('pref.instances.name') }}</th>
<th>Locations</th> <th>{{ useT('pref.instances.loc') }}</th>
<th>CDN</th> <th>{{ useT('pref.instances.cdn') }}</th>
<th>Up to Date</th> <th>{{ useT('pref.instances.up_to_date') }}</th>
<th>Version</th> <th>{{ useT('pref.instances.version') }}</th>
</tr> </tr>
</thead> </thead>
<tbody v-for="i in instances"> <tbody v-for="i in instances">
@ -201,7 +226,7 @@ onMounted(() => {
class="bi bi-code-slash" class="bi bi-code-slash"
target="_blank" target="_blank"
rel="noreferrer noopener" rel="noreferrer noopener"
href="https://codeberg.org/Hyperpipe/Hyperpipe"></a> href="https://codeberg.org/Hyperpipe/Hyperpipe" />
</footer> </footer>
</template> </template>
@ -210,6 +235,7 @@ h2 {
margin-top: 1rem; margin-top: 1rem;
} }
h2, h2,
h3,
label, label,
footer { footer {
text-align: center; text-align: center;

View file

@ -1,18 +1,21 @@
<script setup> <script setup>
import { ref, reactive, watch, onUpdated } from 'vue'; import { ref, reactive, watch, onUpdated } from 'vue';
import PlayBtn from './PlayBtn.vue'; import Btn from './Btn.vue';
import SongItem from './SongItem.vue'; import SongItem from './SongItem.vue';
import AlbumItem from './AlbumItem.vue'; import AlbumItem from './AlbumItem.vue';
import { getJsonPiped, getPipedQuery } from '../scripts/fetch.js'; import { getJsonPiped, getPipedQuery } from '@/scripts/fetch.js';
import { useLazyLoad, useRoute } from '../scripts/util.js'; import { useRoute } from '@/scripts/util.js';
import { useCreatePlaylist } from '../scripts/db.js'; import { useCreatePlaylist } from '@/scripts/db.js';
import { useT } from '@/scripts/i18n.js';
import { useResults, useArtist } from '@/stores/results.js'; import { useResults, useArtist } from '@/stores/results.js';
import { useData } from '@/stores/player.js';
import { useNav } from '@/stores/misc.js'; import { useNav } from '@/stores/misc.js';
const results = useResults(), const results = useResults(),
data = useData(),
nav = useNav(), nav = useNav(),
artist = useArtist(); artist = useArtist();
@ -21,17 +24,11 @@ const emit = defineEmits(['get-album', 'get-artist', 'play-urls', 'add-song']),
filter = ref('music_songs'), filter = ref('music_songs'),
isSearch = ref(/search/.test(location.pathname)); isSearch = ref(/search/.test(location.pathname));
const playAlbum = () => { const saveAlbum = () => {
const urls = results.items?.songs?.items?.map(item => { const urls = results.items?.songs?.items?.map(item => ({
return { url: item.url, title: item.title }; url: item.url,
}); title: item.title,
}));
emit('play-urls', urls);
},
saveAlbum = () => {
const urls = results.items?.songs?.items?.map(item => {
return { url: item.url, title: item.title };
});
let title = results.items?.songs?.title; let title = results.items?.songs?.title;
@ -54,7 +51,6 @@ const playAlbum = () => {
isSearch.value = /search/.test(location.pathname); isSearch.value = /search/.test(location.pathname);
getResults(pq); getResults(pq);
useLazyLoad();
} else { } else {
results.resetItems(); results.resetItems();
@ -72,7 +68,6 @@ const playAlbum = () => {
key = f.split('_')[1]; key = f.split('_')[1];
results.setItem(key, json); results.setItem(key, json);
console.log(json, key); console.log(json, key);
}; };
@ -106,8 +101,27 @@ onUpdated(() => {
</div> </div>
<div v-if="results.items?.songs?.title" class="text-full flex"> <div v-if="results.items?.songs?.title" class="text-full flex">
<PlayBtn @click="playAlbum" /> <Btn
<PlayBtn ico="plus" @click="saveAlbum" /> @click="
$emit(
'play-urls',
results.items?.songs?.items?.map(item => ({
url: item.url,
title: item.title,
})),
)
" />
<Btn ico="star" @click="saveAlbum" />
<Btn
ico="plus-lg"
@click="
data.state.urls.push(
...results.items.songs.items.map(i => ({
url: i.url,
title: i.title,
})),
)
" />
<span>{{ results.items?.songs?.title }}</span> <span>{{ results.items?.songs?.title }}</span>
</div> </div>
@ -121,14 +135,14 @@ onUpdated(() => {
getSearch(nav.state.search); getSearch(nav.state.search);
" "
:data-active="f == filter"> :data-active="f == filter">
{{ f.split('_')[1] }} {{ useT('title.' + f.split('_')[1]) }}
</button> </button>
</div> </div>
<div <div
v-if="results.items.songs && results.items.songs.items[0]" v-if="results.items.songs && results.items.songs.items[0]"
class="search-songs"> class="search-songs">
<h2>Songs</h2> <h2>{{ useT('title.songs') }}</h2>
<div class="grid"> <div class="grid">
<template v-for="song in results.items.songs.items"> <template v-for="song in results.items.songs.items">
<SongItem <SongItem
@ -137,11 +151,7 @@ onUpdated(() => {
:channel="song.uploaderUrl || song.subId" :channel="song.uploaderUrl || song.subId"
:play="song.url || '/watch?v=' + song.id" :play="song.url || '/watch?v=' + song.id"
:art=" :art="
'url(' + song.thumbnail || song.thumbnails[1]?.url || song.thumbnails[0]?.url
(song.thumbnail ||
song.thumbnails[1]?.url ||
song.thumbnails[0]?.url) +
')'
" "
@open-song=" @open-song="
$emit('play-urls', [ $emit('play-urls', [
@ -165,20 +175,20 @@ onUpdated(() => {
" "
class="more" class="more"
:href="'/playlist?list=' + results.items.notes.items" :href="'/playlist?list=' + results.items.notes.items"
>See All</a >{{ useT('info.see_all') }}</a
> >
</div> </div>
<div <div
v-if="results.items.albums && results.items.albums.items[0]" v-if="results.items.albums && results.items.albums.items[0]"
class="search-albums"> class="search-albums">
<h2>Albums</h2> <h2>{{ useT('title.albums') }}</h2>
<div class="grid-3"> <div class="grid-3">
<template v-for="album in results.items.albums.items"> <template v-for="album in results.items.albums.items">
<AlbumItem <AlbumItem
:author="album.uploaderName || album.subtitle" :author="album.uploaderName || album.subtitle"
:name="album.name || album.title" :name="album.name || album.title"
:art="'url(' + (album.thumbnail || album.thumbnails[0].url) + ')'" :art="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)
" /> " />
@ -189,13 +199,13 @@ onUpdated(() => {
<div <div
v-if="results.items.singles && results.items.singles.items[0]" v-if="results.items.singles && results.items.singles.items[0]"
class="search-albums"> class="search-albums">
<h2>Singles</h2> <h2>{{ useT('title.singles') }}</h2>
<div class="grid-3"> <div class="grid-3">
<template v-for="single in results.items.singles.items"> <template v-for="single in results.items.singles.items">
<AlbumItem <AlbumItem
:author="single.subtitle" :author="single.subtitle"
:name="single.title" :name="single.title"
:art="'url(' + single.thumbnails[0].url + ')'" :art="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>
@ -208,7 +218,13 @@ onUpdated(() => {
(results.items.artists && results.items.artists.items[0]) (results.items.artists && results.items.artists.items[0])
" "
class="search-artists"> class="search-artists">
<h2>{{ results.items.artists ? 'Artists' : 'Similar Artists' }}</h2> <h2>
{{
results.items.artists
? useT('title.artists')
: useT('title.similar_artists')
}}
</h2>
<div class="grid-3 circle"> <div class="grid-3 circle">
<template <template
v-for="artist in results.items.artists v-for="artist in results.items.artists
@ -217,7 +233,7 @@ onUpdated(() => {
<AlbumItem <AlbumItem
:author="artist.subtitle" :author="artist.subtitle"
:name="artist.name || artist.title" :name="artist.name || artist.title"
:art="'url(' + (artist.thumbnail || artist.thumbnails[0].url) + ')'" :art="artist.thumbnail || artist.thumbnails[0].url"
@open-album=" @open-album="
$emit( $emit(
'get-artist', 'get-artist',
@ -244,8 +260,14 @@ onUpdated(() => {
.search-artists { .search-artists {
text-align: center; text-align: center;
} }
:deep(.bi) {
margin-right: 0;
}
:deep(.bi-play) { :deep(.bi-play) {
margin-right: 0.75rem; margin-right: 1rem;
}
:deep(.bi-plus-lg) {
margin-right: auto;
} }
.filters { .filters {
display: flex; display: flex;

View file

@ -1,6 +1,7 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useNav } from '@/stores/misc.js'; import { useNav } from '@/stores/misc.js';
import { useT } from '@/scripts/i18n.js';
const show = ref(false), const show = ref(false),
nav = useNav(); nav = useNav();
@ -17,7 +18,7 @@ const show = ref(false),
<input <input
type="text" type="text"
aria-label="Search Input" aria-label="Search Input"
placeholder="Search..." :placeholder="useT('title.search') + '...'"
@change=" @change="
nav.state.search = $event.target.value; nav.state.search = $event.target.value;
nav.state.page = 'home'; nav.state.page = 'home';

View file

@ -53,7 +53,7 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="song card flex pop" @click="openSong($event.target)"> <div class="song card flex pop" @click="openSong($event.target)">
<div class="pop-2 bg-img song-bg"></div> <img class="pop-2 bg-img song-bg" loading="lazy" :src="art" alt />
<span class="flex content"> <span class="flex content">
<h4>{{ title }}</h4> <h4>{{ title }}</h4>
@ -120,7 +120,6 @@ span.bi-three-dots-vertical {
} }
.song-bg { .song-bg {
--grad: v-bind('rand'); --grad: v-bind('rand');
--art: v-bind('art || rand');
width: 120px; width: 120px;
height: 120px; height: 120px;
} }

View file

@ -3,12 +3,15 @@ import { ref, reactive, watch } from 'vue';
import Modal from './Modal.vue'; import Modal from './Modal.vue';
import { useStore } from '../scripts/util.js'; import { useStore } from '@/scripts/util.js';
import { useListPlaylists } from '../scripts/db.js'; import { useListPlaylists, useUpdatePlaylist } from '@/scripts/db.js';
import { getAuthPlaylists, getJsonAuth } from '@/scripts/fetch.js';
import { useT } from '@/scripts/i18n.js';
import { usePlayer } from '../stores/player.js'; import { useData, usePlayer } from '@/stores/player.js';
const player = usePlayer(), const data = useData(),
player = usePlayer(),
store = useStore(); store = useStore();
const emit = defineEmits(['save']), const emit = defineEmits(['save']),
@ -18,15 +21,52 @@ const emit = defineEmits(['save']),
vol: false, vol: false,
}), }),
pl = ref(''), pl = ref(''),
list = ref([]); list = ref([]),
remote = ref([]),
plRemote = ref(false);
function Save() { function List() {
showme.pl = true; showme.pl = true;
useListPlaylists(res => { useListPlaylists(res => {
console.log(res); console.log(res);
list.value = res; list.value = res;
showme.menu = false; showme.menu = false;
}); });
getAuthPlaylists().then(res => {
remote.value = res;
});
}
function Save() {
if (pl.value) {
if (plRemote.value == true && store.auth) {
getJsonAuth('/user/playlists/add', {
method: 'POST',
headers: {
Authorization: store.auth,
},
body: JSON.stringify({
playlistId: pl.value,
videoId: new URL(
'https://example.com' + data.state.url,
).searchParams.get('v'),
}),
});
} else if (plRemote.value == false) {
useUpdatePlaylist(
pl.value,
{
url: data.state.url,
title: data.state.title,
},
e => {
if (e === true) {
console.log('Added Song');
}
},
);
}
}
} }
</script> </script>
<template> <template>
@ -45,22 +85,38 @@ function Save() {
<template v-for="i in list"> <template v-for="i in list">
<div <div
class="flex item" class="flex item"
@click="pl = i.name" @click="
:data-active="pl == i.name"> pl = i.name;
plRemote = false;
"
:data-active="pl == i.name && plRemote == false">
<span>{{ i.name }}</span <span>{{ i.name }}</span
><span class="ml-auto">{{ i.urls.length || '' }}</span> ><span class="ml-auto">{{ i.urls.length || '' }}</span>
</div> </div>
</template> </template>
<template v-for="i in remote">
<div
class="flex item"
@click="
pl = i.id;
plRemote = true;
"
:data-active="pl == i.id && plRemote == true">
<span>{{ i.name }}</span>
</div>
</template>
</template> </template>
<template #buttons> <template #buttons>
<button aria-label="Cancel" @click="showme.pl = false">Cancel</button> <button aria-label="Cancel" @click="showme.pl = false">
{{ useT('action.cancel') }}
</button>
<button <button
aria-label="Add Song" aria-label="Add Song"
@click=" @click="
if (pl) $emit('save', pl); Save();
showme.pl = false; showme.pl = false;
"> ">
Add {{ useT('action.add') }}
</button> </button>
</template> </template>
</Modal> </Modal>
@ -124,13 +180,14 @@ function Save() {
id="info-btn" id="info-btn"
class="bi bi-info-circle" class="bi bi-info-circle"
aria-label="Show Information About Song" aria-label="Show Information About Song"
:data-active="player.state.info"
@click="player.toggle('info')"></button> @click="player.toggle('info')"></button>
<button <button
id="addToPlaylist" id="addToPlaylist"
title="Add Current Song to a Playlist" title="Add Current Song to a Playlist"
aria-label="Add Current Song to a Playlist" aria-label="Add Current Song to a Playlist"
class="bi bi-collection" class="bi bi-collection"
@click="Save"></button> @click="List"></button>
<button <button
id="list-btn" id="list-btn"
title="Current Playlist" title="Current Playlist"
@ -166,6 +223,7 @@ function Save() {
border-top: 0.25rem solid var(--color-foreground); border-top: 0.25rem solid var(--color-foreground);
background: var(--color-background); background: var(--color-background);
min-height: 15vh; min-height: 15vh;
z-index: 2;
} }
.statusbar-right { .statusbar-right {
margin-left: 0.5rem; margin-left: 0.5rem;

65
src/locales/en.json Normal file
View file

@ -0,0 +1,65 @@
{
"title": {
"songs": "Songs",
"albums": "Albums",
"singles": "Singles",
"artists": "Artists",
"similar_artists": "Similar Artists",
"moods": "Moods",
"genres": "Genres",
"featured": "Featured",
"spotlight": "Spotlight",
"community": "Community",
"login": "Login",
"local": "Local",
"remote": "Remote",
"search": "Search"
},
"action": {
"back": "Back",
"add": "Add",
"create": "Create",
"cancel": "Cancel",
"send": "Send",
"receive": "Receive"
},
"playlist": {
"local": "Local Playlists",
"remote": "Remote Playlists",
"name": "Playlist name",
"add": "Add Songs to Playlist",
"create": "Create a new Playlist",
"sync": "Sync playlists",
"select": "Select Playlist to Add"
},
"pref": {
"theme": "Theme",
"player": "Audio Player",
"auto_queue": "Automatically Queue Songs",
"codec": "Codec",
"quality": "Quality",
"auto": "auto",
"best": "best",
"worst": "worst",
"volume": "Default Volume",
"instances": {
"hyp": "Hyperpipe Instance",
"piped": "Piped Instance",
"auth": "Authentication Instance",
"name": "Name",
"loc": "Locations",
"cdn": "CDN",
"up_to_date": "Up to Date",
"version": "Version"
}
},
"info": {
"see_all": "See All",
"search": "Start Searching",
"no_info": "No Information Available",
"lyrics": {
"load": "Fetching Lyrics",
"void": "No Lyrics"
}
}
}

View file

@ -1,12 +1,26 @@
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createPinia } from 'pinia'; import { createPinia } from 'pinia';
import { createI18n } from 'vue-i18n';
import App from './App.vue'; import App from './App.vue';
import en from '@/locales/en.json';
import('bootstrap-icons/font/bootstrap-icons.css'); import('bootstrap-icons/font/bootstrap-icons.css');
const pinia = createPinia(), const pinia = createPinia(),
i18n = createI18n({
globalInjection: true,
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages: {
en: en,
},
}),
app = createApp(App); app = createApp(App);
window.i18n = i18n;
app.use(i18n);
app.use(pinia); app.use(pinia);
app.mount('#app'); app.mount('#app');

View file

@ -10,9 +10,3 @@ export function useRand() {
const i = Math.floor(Math.random() * c.length); const i = Math.floor(Math.random() * c.length);
return c[i]; return c[i];
} }
export function useRandColor() {
const r = Math.random().toString(16);
return '#' + r.substr(r.length - 6);
}

View file

@ -45,3 +45,21 @@ export async function getJsonHyp(path) {
return await getJson('https://' + root + path); return await getJson('https://' + root + path);
} }
export async function getJsonAuth(path, opts) {
const root = useStore().getItem('authapi') || 'pipedapi.kavin.rocks';
return await fetch('https://' + root + path, opts).then(res => res.json());
}
export async function getAuthPlaylists() {
if (!!useStore().getItem('auth')) {
const res = await getJsonAuth('/user/playlists', {
headers: {
Authorization: useStore().getItem('auth'),
},
});
return res.filter(i => i.name.startsWith('Playlist - '));
}
}

29
src/scripts/i18n.js Normal file
View file

@ -0,0 +1,29 @@
import { useI18n } from 'vue-i18n';
export const SUPPORTED_LOCALES = [
{
code: 'en',
name: 'English',
},
];
export function useT(path) {
const { messages, locale, fallbackLocale } = useI18n(),
msgs = messages.value?.[locale.value],
fallback = messages.value?.[fallbackLocale.value],
keys = path.split('.'),
translate = msg => keys.reduce((obj, i) => obj?.[i], msg),
translated = translate(msgs) || translate(fallback);
return translated || path;
}
export function useSetupLocale(locale) {
import(`@/locales/${locale}.json`)
.then(mod => mod.default)
.then(mod => {
window.i18n.global.messages.value[locale] = mod;
});
window.i18n.global.locale.value = locale;
}

View file

@ -26,30 +26,3 @@ export function useStore() {
}; };
} }
} }
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');
}
}