mirror of
https://codeberg.org/Hyperpipe/Hyperpipe
synced 2025-06-27 20:58: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>
|
<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
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": {
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
47
src/App.vue
47
src/App.vue
|
@ -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>
|
||||||
|
|
|
@ -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%;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
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 { 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');
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
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