- 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>
<!--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>
</body>
</html>

935
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

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

View file

@ -18,7 +18,8 @@ import Prefs from '@/components/Prefs.vue';
/* Composables */
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';
/* 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(() => {
if (store.theme) {
document.body.setAttribute('data-theme', store.theme);
}
if (store.locale) {
useSetupLocale(store.locale);
}
});
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 */
window.addEventListener('popstate', parseUrl);
@ -355,17 +338,18 @@ onMounted(() => {
</template>
<header v-if="!artist.state.title">
<div
v-show="data.state.art"
class="art bg-img"
:style="'--art: url(' + data.state.art + ')'"></div>
<img
v-if="data.state.art"
class="art"
loading="lazy"
:src="data.state.art" />
<div class="wrapper">
<NowPlaying @get-artist="getArtist" />
</div>
</header>
<main class="placeholder">
<main class="placeholder" :data-placeholder="useT('info.search')">
<KeepAlive>
<Search
v-if="nav.state.page == 'home'"
@ -382,7 +366,10 @@ onMounted(() => {
@get-album="getAlbum" />
</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'" />
</main>
@ -399,7 +386,7 @@ onMounted(() => {
<Info v-if="player.state.info" :text="data.state.description" />
</Transition>
<StatusBar @save="SaveTrack" />
<StatusBar />
<Player @ended="playNext" />
</template>

View file

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

View file

@ -10,7 +10,10 @@ const props = defineProps({
default: '',
},
grad: String,
art: String,
art: {
type: String,
default: 'https://upload.wikimedia.org/wikipedia/commons/c/ca/1x1.png',
},
});
defineEmits(['open-album']);
@ -18,7 +21,7 @@ defineEmits(['open-album']);
<template>
<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">
<h4>{{ name }}</h4>
@ -40,7 +43,6 @@ defineEmits(['open-album']);
}
.card-bg {
--grad: v-bind('grad || rand');
--art: v-bind('art || grad || rand');
height: 13rem;
width: 13rem;
}

View file

@ -1,6 +1,6 @@
<script setup>
import { ref, onUpdated } from 'vue';
import PlayBtn from './PlayBtn.vue';
import Btn from './Btn.vue';
import { useArtist } from '@/stores/results.js';
@ -18,16 +18,14 @@ onUpdated(() => {
<template>
<div v-if="show == 0 && artist.state.title" class="us-wrap">
<div
class="bg-imgfill"
:style="'--art: url(' + artist.state.thumbnails[1].url + ');'"></div>
<img class="bg-imgfill" :src="artist.state.thumbnails[1].url" />
<div class="us-main">
<h2>{{ artist.state.title }}</h2>
<p @click="$event.target.classList.toggle('more')">
{{ artist.state.description }}
</p>
<div class="us-playwrap">
<PlayBtn
<Btn
@click="
$emit('playall', '/playlist?list=' + artist.state.playlistId)
" />
@ -46,10 +44,6 @@ onUpdated(() => {
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 {
@ -59,6 +53,7 @@ onUpdated(() => {
flex-direction: column;
padding: 1rem;
box-shadow: inset 0 0 10rem #000;
z-index: 1;
}
h2 {
padding: 1rem;

View file

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

View file

@ -2,8 +2,8 @@
import { reactive, ref, onMounted, onUnmounted } from 'vue';
import { getJsonHyp } from '../scripts/fetch.js';
import { useRandColor } from '../scripts/colors.js';
import { useRoute } from '../scripts/util.js';
import { useT } from '@/scripts/i18n.js';
import AlbumItem from './AlbumItem.vue';
@ -55,18 +55,18 @@ onMounted(get);
<template>
<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>
<template v-for="type in ['featured', 'spotlight', 'community']">
<h3 class="head">{{ type }}</h3>
<h3 class="head">{{ useT('title.' + type) }}</h3>
<div class="grid-3">
<template v-for="i in data[type]">
<AlbumItem
:name="i.title"
:author="i.subtitle"
:art="'url(' + i.thumbnails[0].url + ')'"
:art="i.thumbnails[0].url"
@open-album="
$emit('get-album', '/playlist?list=' + i.id);
nav.state.page = 'home';
@ -77,7 +77,7 @@ onMounted(get);
</template>
<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">
<button
@ -89,7 +89,7 @@ onMounted(get);
</button>
</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">
<button

View file

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

View file

@ -3,6 +3,7 @@ import { ref, watch } from 'vue';
import { getJsonHyp } from '@/scripts/fetch.js';
import { useData } from '@/stores/player.js';
import { useT } from '@/scripts/i18n.js';
import TextModal from './TextModal.vue';
@ -48,7 +49,11 @@ watch(
<template #content>
<pre
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
>
<div>{{ source }}</div>
@ -60,10 +65,4 @@ watch(
.placeholder:empty::before {
--ico: '\f3a5';
}
.placeholder[data-loaded='false']:empty::after {
--text: 'Fetching Lyrics...';
}
.placeholder[data-loaded='true']:empty::after {
--text: 'No Lyrics...';
}
</style>

View file

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

View file

@ -1,9 +1,13 @@
<script setup>
import { ref, reactive, watch, onMounted } from 'vue';
import AlbumItem from './AlbumItem.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 {
useListPlaylists,
@ -12,7 +16,10 @@ import {
useUpdatePlaylist,
} 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([]),
show = reactive({
new: false,
@ -24,8 +31,16 @@ const emit = defineEmits(['play-urls']),
id: 'Please Wait...',
to: '',
peer: undefined,
}),
user = reactive({
username: undefined,
password: undefined,
playlists: [],
create: false,
});
const pathname = url => new URL(url).pathname;
const Play = 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(
() => show.sync,
async () => {
@ -119,7 +172,12 @@ watch(
},
);
onMounted(List);
watch(auth, getPlaylists);
onMounted(async () => {
await getPlaylists();
List();
});
</script>
<template>
@ -127,13 +185,22 @@ onMounted(List);
<Modal
n="2"
:display="show.new"
title="Create a new Playlist..."
:title="useT('playlist.create')"
@show="
e => {
show.new = e;
}
">
<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
type="text"
placeholder="Playlist name..."
@ -141,15 +208,17 @@ onMounted(List);
v-model="text" />
</template>
<template #buttons>
<button @click="show.new = false">Cancel</button>
<button @click="Create">Create</button>
<button @click="show.new = false">{{ useT('action.cancel') }}</button>
<button @click="user.create ? createPlaylist() : Create()">
{{ useT('action.create') }}
</button>
</template>
</Modal>
<Modal
:n="sync.type == 'send' ? 2 : 1"
:display="show.sync"
title="Sync Playlists..."
:title="useT('playlist.sync')"
@show="
e => {
show.sync = e;
@ -160,10 +229,10 @@ onMounted(List);
<button
:data-active="sync.type == 'send'"
@click="sync.type = 'send'">
Send
{{ useT('action.send') }}
</button>
<button :data-active="sync.type == 'rec'" @click="sync.type = 'rec'">
Receive
{{ useT('action.receive') }}
</button>
</div>
@ -181,9 +250,11 @@ onMounted(List);
</template>
<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">
{{ sync.type == 'send' ? 'Send' : 'Recieve' }}
{{
sync.type == 'send' ? useT('action.send') : useT('action.recieve')
}}
</button>
</template>
</Modal>
@ -196,6 +267,8 @@ onMounted(List);
@click="show.sync = true"></div>
</div>
<h2 v-if="list.length > 0">{{ useT('playlist.local') }}</h2>
<div class="grid-3">
<template v-for="i in list">
<AlbumItem
@ -205,10 +278,48 @@ onMounted(List);
@open-album="Play(i.name)" />
</template>
</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>
</template>
<style scoped>
h2 {
text-align: center;
margin: 2rem;
}
.npl-wrap {
padding-bottom: 5rem;
}
@ -244,7 +355,9 @@ pre {
width: calc(100% / 2);
background: var(--color-background);
}
.tabs button[data-active='true'] {
.tabs button[data-active='true'],
.login button {
font-weight: bold;
color: var(--color-background);
background: linear-gradient(135deg, cornflowerblue, #88c0d0);
}
@ -254,6 +367,15 @@ pre {
.tabs button:last-child {
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) {
.npl-box:first-child {
margin: 0 1rem 0 auto;

View file

@ -10,8 +10,7 @@ import {
import muxjs from 'mux.js';
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';
defineEmits(['ended']);
@ -23,8 +22,6 @@ const player = usePlayer(),
const audio = ref(null);
function audioCanPlay() {
useLazyLoad();
player.state.status = 'pause';
audio.value.play().catch(err => {
console.error(err);

View file

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

View file

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

View file

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

View file

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

View file

@ -53,7 +53,7 @@ onMounted(() => {
</script>
<template>
<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">
<h4>{{ title }}</h4>
@ -120,7 +120,6 @@ span.bi-three-dots-vertical {
}
.song-bg {
--grad: v-bind('rand');
--art: v-bind('art || rand');
width: 120px;
height: 120px;
}

View file

@ -3,12 +3,15 @@ import { ref, reactive, watch } from 'vue';
import Modal from './Modal.vue';
import { useStore } from '../scripts/util.js';
import { useListPlaylists } from '../scripts/db.js';
import { useStore } from '@/scripts/util.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();
const emit = defineEmits(['save']),
@ -18,15 +21,52 @@ const emit = defineEmits(['save']),
vol: false,
}),
pl = ref(''),
list = ref([]);
list = ref([]),
remote = ref([]),
plRemote = ref(false);
function Save() {
function List() {
showme.pl = true;
useListPlaylists(res => {
console.log(res);
list.value = res;
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>
<template>
@ -45,22 +85,38 @@ function Save() {
<template v-for="i in list">
<div
class="flex item"
@click="pl = i.name"
:data-active="pl == i.name">
@click="
pl = i.name;
plRemote = false;
"
:data-active="pl == i.name && plRemote == false">
<span>{{ i.name }}</span
><span class="ml-auto">{{ i.urls.length || '' }}</span>
</div>
</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 #buttons>
<button aria-label="Cancel" @click="showme.pl = false">Cancel</button>
<button aria-label="Cancel" @click="showme.pl = false">
{{ useT('action.cancel') }}
</button>
<button
aria-label="Add Song"
@click="
if (pl) $emit('save', pl);
Save();
showme.pl = false;
">
Add
{{ useT('action.add') }}
</button>
</template>
</Modal>
@ -124,13 +180,14 @@ function Save() {
id="info-btn"
class="bi bi-info-circle"
aria-label="Show Information About Song"
:data-active="player.state.info"
@click="player.toggle('info')"></button>
<button
id="addToPlaylist"
title="Add Current Song to a Playlist"
aria-label="Add Current Song to a Playlist"
class="bi bi-collection"
@click="Save"></button>
@click="List"></button>
<button
id="list-btn"
title="Current Playlist"
@ -166,6 +223,7 @@ function Save() {
border-top: 0.25rem solid var(--color-foreground);
background: var(--color-background);
min-height: 15vh;
z-index: 2;
}
.statusbar-right {
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 { createPinia } from 'pinia';
import { createI18n } from 'vue-i18n';
import App from './App.vue';
import en from '@/locales/en.json';
import('bootstrap-icons/font/bootstrap-icons.css');
const pinia = createPinia(),
i18n = createI18n({
globalInjection: true,
legacy: false,
locale: 'en',
fallbackLocale: 'en',
messages: {
en: en,
},
}),
app = createApp(App);
window.i18n = i18n;
app.use(i18n);
app.use(pinia);
app.mount('#app');

View file

@ -10,9 +10,3 @@ export function useRand() {
const i = Math.floor(Math.random() * c.length);
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);
}
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');
}
}