mirror of
https://codeberg.org/Hyperpipe/Hyperpipe
synced 2025-06-27 12:48:01 +02:00
Changes:
- Added Authentication for Piped Accounts - Localization
This commit is contained in:
parent
8548a3646e
commit
de6572eee4
26 changed files with 1045 additions and 596 deletions
|
@ -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
935
package-lock.json
generated
File diff suppressed because it is too large
Load diff
17
package.json
17
package.json
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
47
src/App.vue
47
src/App.vue
|
@ -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>
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -32,6 +32,7 @@ defineEmits(['click']);
|
|||
background: transparent;
|
||||
}
|
||||
.bi:not(.bi-play) {
|
||||
font-size: 3rem;
|
||||
font-size: 1.75rem;
|
||||
width: 3.5rem;
|
||||
}
|
||||
</style>
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
65
src/locales/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
14
src/main.js
14
src/main.js
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
29
src/scripts/i18n.js
Normal 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;
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue