Merge pull request 'main' (#1) from Hyperpipe/Hyperpipe:main into main

Reviewed-on: https://codeberg.org/HexagonCDN/Hyperpipe/pulls/1
This commit is contained in:
HexagonCDN 2022-12-19 09:08:15 +00:00
commit 197d16cd00
41 changed files with 1000 additions and 840 deletions

View file

@ -1,5 +1,9 @@
.git .git
.gitea
.gitignore .gitignore
.dockerignore .dockerignore
.prettierignore
.prettierrc.json
.woodpecker.yml
*.md *.md
*.txt *.txt

39
.woodpecker.yml Normal file
View file

@ -0,0 +1,39 @@
pipeline:
build:
image: node:alpine
commands:
- npm install
- npm run build
when:
path:
exclude: [ '*.md', '.gitea/*' ]
event: ['push', 'pull_request']
docker:
image: woodpeckerci/plugin-docker-buildx
settings:
platforms: linux/amd64,linux/arm64
repo: codeberg.org/hyperpipe/hyperpipe
registry: codeberg.org
tags: latest
username: snematoda
password:
from_secret: cb_token
when:
branch: main
path:
exclude: [ '*.md', '.gitea/*' ]
event: ['push']
deploy:
image: node:alpine
commands:
- npm install surge
- cp dist/index.html dist/200.html
- npx surge ./dist hyperpipe.surge.sh
secrets: [surge_login, surge_token]
when:
branch: main
path:
exclude: [ '*.md', '.gitea/*' ]
event: ['push']

View file

@ -1,19 +0,0 @@
pipeline:
build:
image: node:alpine
commands:
- npm install
- npm run build
when:
event: [push, pull_request]
deploy:
image: node:alpine
commands:
- npm install surge
- cp dist/index.html dist/200.html
- npx surge ./dist hyperpipe.surge.sh
secrets: [surge_login, surge_token]
when:
branch: main
event: push

View file

@ -1,20 +1,15 @@
FROM node:alpine AS build FROM --platform=$BUILDPLATFORM node:lts AS build
ARG api
ARG pipedapi
WORKDIR /app/ WORKDIR /app/
COPY . . COPY . .
RUN sed -i "s/hyperpipeapi.onrender.com/$api/g" index.html src/scripts/fetch.js RUN --mount=type=cache,target=/root/.cache/node \
--mount=type=cache,target=/app/node_modules \
RUN sed -i "s/pipedapi.kavin.rocks/$pipedapi/g" index.html src/scripts/fetch.js npm install --prefer-offline && \
RUN npm install && \
npm run build npm run build
FROM nginx:alpine FROM --platform=$BUILDPLATFORM nginx:stable
COPY --from=build /app/dist/ /usr/share/nginx/html/ COPY --from=build /app/dist/ /usr/share/nginx/html/
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf COPY docker/nginx.conf /etc/nginx/conf.d/default.conf

View file

@ -119,7 +119,7 @@ You can reach out to me personally on:
--- ---
*YouTube Music is a trademark of Google LLC.* *Hyperpipe does not host any content. All content on Hyperpipe is from YouTube Music. YouTube and YouTube Music are trademarks of Google LLC. Hyperpipe is not affiliated with YouTube or YouTube Music.*
[hypipe]: https://hyperpipe.surge.sh [hypipe]: https://hyperpipe.surge.sh
[piped]: https://piped.kavin.rocks [piped]: https://piped.kavin.rocks

View file

@ -1,14 +0,0 @@
version: '3'
services:
hyperpipe-frontend:
image: hyperpipe-frontend
build:
context: .
args:
pipedapi: pipedapi.kavin.rocks
api: hyperpipeapi.onrender.com
container_name: hyperpipe-frontend
restart: unless-stopped
ports:
- '8080:80'

View file

@ -9,7 +9,6 @@
<link rel="icon" href="/favicon.svg" /> <link rel="icon" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" /> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="msapplication-TileColor" content="#181818" /> <meta name="msapplication-TileColor" content="#181818" />
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
<link rel="preconnect" href="https://hyperpipeapi.onrender.com" /> <link rel="preconnect" href="https://hyperpipeapi.onrender.com" />
<link rel="dns-prefetch" href="https://pipedapi.kavin.rocks" /> <link rel="dns-prefetch" href="https://pipedapi.kavin.rocks" />
<link rel="dns-prefetch" href="https://hyperpipe-proxy.onrender.com" /> <link rel="dns-prefetch" href="https://hyperpipe-proxy.onrender.com" />
@ -29,7 +28,10 @@
<title>Hyperpipe</title> <title>Hyperpipe</title>
</head> </head>
<body> <body>
<noscript>Kind Sir, Please Give Me JavaScript</noscript> <noscript
>JavaScript is required for this site to function. Please enable it in
your browser or browser extension settings.</noscript
>
<div id="app"></div> <div id="app"></div>

1174
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,15 +15,15 @@
"dompurify": "^2.4.1", "dompurify": "^2.4.1",
"mux.js": "^6.2.0", "mux.js": "^6.2.0",
"peerjs": "^1.4.7", "peerjs": "^1.4.7",
"pinia": "^2.0.26", "pinia": "^2.0.28",
"shaka-player": "^4.3.0", "shaka-player": "^4.3.1",
"stream-browserify": "^3.0.0", "stream-browserify": "^3.0.0",
"vue": "^3.2.38", "vue": "^3.2.38",
"xml-js": "^1.6.11" "xml-js": "^1.6.11"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^3.2.0", "@vitejs/plugin-vue": "^4.0.0",
"prettier": "^2.8.0", "prettier": "^2.8.1",
"vite": "^3.2.4" "vite": "^4.0.0"
} }
} }

View file

@ -150,7 +150,7 @@ onMounted(() => {
v-if="data.state.art" v-if="data.state.art"
class="art" class="art"
loading="lazy" loading="lazy"
:src="data.state.art" /> :src="data.state.art.replaceAll('&amp;', '&')" />
<div class="wrapper"> <div class="wrapper">
<NowPlaying @get-artist="artist.getArtist" /> <NowPlaying @get-artist="artist.getArtist" />

View file

@ -1,3 +1,3 @@
{ {
"date": "2022-11-30" "date": "2022-12-13"
} }

View file

@ -3,7 +3,7 @@ import { useRand } from '@/scripts/colors.js';
const rand = useRand(); const rand = useRand();
const props = defineProps({ defineProps({
name: String, name: String,
author: { author: {
type: String, type: String,

View file

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, onUpdated } from 'vue'; import { ref } from 'vue';
import Btn from './Btn.vue'; import Btn from './Btn.vue';
import { useStore } from '@/scripts/util.js'; import { useStore } from '@/scripts/util.js';

View file

@ -51,7 +51,9 @@ onMounted(() => {
<template> <template>
<template v-if="data.options.length > 0"> <template v-if="data.options.length > 0">
<select :value="id" class="input" @input="id = $event.target.value"> <select :value="id" class="input" @input="id = $event.target.value">
<option v-for="i in data.options" :value="i.id">{{ i.title }}</option> <option v-for="i in data.options" :value="i.id" :key="i.id">
{{ i.title }}
</option>
</select> </select>
</template> </template>
@ -60,6 +62,7 @@ onMounted(() => {
<div class="grid-3 circle"> <div class="grid-3 circle">
<AlbumItem <AlbumItem
v-for="i in data.artists" v-for="i in data.artists"
:key="i.id"
:name="i.title" :name="i.title"
:author="i.subtitle" :author="i.subtitle"
:art="i.thumbnails[1].url" :art="i.thumbnails[1].url"
@ -72,6 +75,7 @@ onMounted(() => {
<div class="grid"> <div class="grid">
<SongItem <SongItem
v-for="i in data.songs" v-for="i in data.songs"
:key="i.id"
:title="i.title" :title="i.title"
:author="i.subtitle" :author="i.subtitle"
:channel="'/channel/' + i.subId" :channel="'/channel/' + i.subId"

View file

@ -1,5 +1,5 @@
<script setup> <script setup>
import { reactive, ref, onMounted, onUnmounted } from 'vue'; import { reactive, onMounted } from 'vue';
import { useResults } from '@/stores/results.js'; import { useResults } from '@/stores/results.js';
@ -62,19 +62,21 @@ onMounted(get);
<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']"
:key="type">
<h3 class="head">{{ t('title.' + type) }}</h3> <h3 class="head">{{ t('title.' + type) }}</h3>
<div class="grid-3"> <div class="grid-3">
<template v-for="i in data[type]"> <AlbumItem
<AlbumItem v-for="i in data[type]"
:name="i.title" :key="i.id"
:author="i.subtitle" :name="i.title"
:art="i.thumbnails[0].url" :author="i.subtitle"
@open-album=" :art="i.thumbnails[0].url"
getAlbum('/playlist?list=' + i.id); @open-album="
nav.state.page = 'home'; getAlbum('/playlist?list=' + i.id);
" /> nav.state.page = 'home';
</template> " />
</div> </div>
</template> </template>
</template> </template>
@ -86,6 +88,7 @@ onMounted(get);
<button <button
v-for="i in btns.moods" v-for="i in btns.moods"
class="btn" class="btn"
:key="i.id"
:style="`--btn-color: ${i.subtitle};`" :style="`--btn-color: ${i.subtitle};`"
@click="get(i.id)"> @click="get(i.id)">
{{ i.title }} {{ i.title }}
@ -98,6 +101,7 @@ onMounted(get);
<button <button
v-for="i in btns.genres" v-for="i in btns.genres"
class="btn" class="btn"
:key="i.id"
:style="`--btn-color: ${i.subtitle};`" :style="`--btn-color: ${i.subtitle};`"
@click="get(i.id)"> @click="get(i.id)">
{{ i.title }} {{ i.title }}

View file

@ -1,6 +1,4 @@
<script setup> <script setup>
import { reactive } from 'vue';
import SearchBar from '@/components/SearchBar.vue'; import SearchBar from '@/components/SearchBar.vue';
import IcoHyp from '@/assets/icons/IcoHyp.vue'; import IcoHyp from '@/assets/icons/IcoHyp.vue';

View file

@ -30,8 +30,9 @@ const { t } = useI18n(),
results = useResults(), results = useResults(),
nav = useNav(); nav = useNav();
const emit = defineEmits(['play-urls', 'open-playlist']), defineEmits(['play-urls', 'open-playlist']);
list = ref([]),
const list = ref([]),
show = reactive({ show = reactive({
new: false, new: false,
sync: false, sync: false,
@ -137,7 +138,7 @@ const Login = async () => {
}, },
createPlaylist = async () => { createPlaylist = async () => {
if (text.value) { if (text.value) {
const res = await useAuthCreatePlaylist(text.value); await useAuthCreatePlaylist(text.value);
getPlaylists(); getPlaylists();
show.new = false; show.new = false;
@ -312,24 +313,24 @@ onMounted(async () => {
<h2 v-if="list.length > 0">{{ t('playlist.local') }}</h2> <h2 v-if="list.length > 0">{{ t('playlist.local') }}</h2>
<div class="grid-3"> <div class="grid-3">
<template v-for="i in list"> <AlbumItem
<AlbumItem v-for="i in list"
:name="i.name" :key="i.name"
:author="t('title.songs') + ' • ' + i.urls.length" :name="i.name"
:grad="useRand()" :author="t('title.songs') + ' • ' + i.urls.length"
@open-album="Open(i.name)" /> :grad="useRand()"
</template> @open-album="Open(i.name)" />
</div> </div>
<h2 class="login-h">{{ t('playlist.remote') }}</h2> <h2 class="login-h">{{ t('playlist.remote') }}</h2>
<div v-if="auth" class="grid-3"> <div v-if="auth" class="grid-3">
<template v-for="i in user.playlists"> <AlbumItem
<AlbumItem v-for="i in user.playlists"
:name="i.name.replace('Playlist - ', '')" :key="i.id"
:art="pathname(i.thumbnail) != '/' ? i.thumbnail : undefined" :name="i.name.replace('Playlist - ', '')"
@open-album="$emit('open-playlist', '/playlists?list=' + i.id)" /> :art="pathname(i.thumbnail) != '/' ? i.thumbnail : undefined"
</template> @open-album="$emit('open-playlist', '/playlists?list=' + i.id)" />
</div> </div>
<form v-else class="login" @submit.prevent> <form v-else class="login" @submit.prevent>
<input <input

View file

@ -1,12 +1,5 @@
<script setup> <script setup>
import { import { ref, watch, onMounted, onBeforeUnmount, onUnmounted } from 'vue';
ref,
watch,
onMounted,
onUpdated,
onBeforeUnmount,
onUnmounted,
} from 'vue';
import muxjs from 'mux.js'; import muxjs from 'mux.js';
window.muxjs = muxjs; window.muxjs = muxjs;
@ -64,7 +57,7 @@ async function Stream() {
if (shaka.Player.isBrowserSupported) { if (shaka.Player.isBrowserSupported) {
const audioPlayer = new shaka.Player(audio.value); const audioPlayer = new shaka.Player(audio.value);
const codecs = useStore().getItem('codec'); const codecs = store.getItem('codec');
audioPlayer audioPlayer
.getNetworkingEngine() .getNetworkingEngine()
@ -94,7 +87,7 @@ async function Stream() {
}); });
} }
const quality = useStore().getItem('quality'); const quality = store.getItem('quality');
if (url) { if (url) {
window.audioPlayer window.audioPlayer

View file

@ -12,26 +12,28 @@ defineEmits(['playthis']);
<template> <template>
<Transition name="fade"> <Transition name="fade">
<div class="pl-modal placeholder" :data-placeholder="t('playlist.add')"> <div class="pl-modal placeholder" :data-placeholder="t('playlist.add')">
<template v-for="plurl in data.state.urls"> <div
<div class="pl-item" @click="$emit('playthis', plurl)"> v-for="plurl in data.state.urls"
<span class="pl-item"
v-if="data.state.url == plurl.url" :key="plurl.url"
class="bars-wrap" @click="$emit('playthis', plurl)">
:class="player.state.status"> <span
<div class="bars"></div> v-if="data.state.url == plurl.url"
<div class="bars"></div> class="bars-wrap"
<div class="bars"></div> :class="player.state.status">
</span> <div class="bars"></div>
<div v-else-if="plurl.thumbnails" class="pl-img"> <div class="bars"></div>
<img <div class="bars"></div>
:src="plurl.thumbnails[0].url" </span>
:height="plurl.thumbnails[0].height" <div v-else-if="plurl.thumbnails" class="pl-img">
:width="plurl.thumbnails[0].width" <img
loading="lazy" /> :src="plurl.thumbnails[0].url"
</div> :height="plurl.thumbnails[0].height"
<span class="pl-main caps">{{ plurl.title }}</span> :width="plurl.thumbnails[0].width"
loading="lazy" />
</div> </div>
</template> <span class="pl-main caps">{{ plurl.title }}</span>
</div>
</div> </div>
</Transition> </Transition>
</template> </template>

View file

@ -107,7 +107,9 @@ onMounted(() => {
class="input" class="input"
:value="getStore('locale') || 'en'" :value="getStore('locale') || 'en'"
@change="setLang($event.target.value)"> @change="setLang($event.target.value)">
<option v-for="i in SUPPORTED_LOCALES" :value="i.code">{{ i.name }}</option> <option v-for="i in SUPPORTED_LOCALES" :value="i.code" :key="i.code">
{{ i.name }}
</option>
</select> </select>
<h2>{{ t('pref.tab') }}</h2> <h2>{{ t('pref.tab') }}</h2>
@ -205,7 +207,7 @@ onMounted(() => {
<th>{{ t('instances.loc') }}</th> <th>{{ t('instances.loc') }}</th>
</tr> </tr>
</thead> </thead>
<tbody v-for="i in hypInstances"> <tbody v-for="i in hypInstances" :key="i.name">
<tr> <tr>
<td> <td>
{{ i.name }} {{ i.name }}
@ -273,7 +275,7 @@ onMounted(() => {
<th>{{ t('instances.version') }}</th> <th>{{ t('instances.version') }}</th>
</tr> </tr>
</thead> </thead>
<tbody v-for="i in instances"> <tbody v-for="i in instances" :key="i.name">
<tr> <tr>
<td> <td>
{{ i.name.replace('Official', 'Default') }} {{ i.name.replace('Official', 'Default') }}

View file

@ -1,12 +1,5 @@
<script setup> <script setup>
import { import { ref, watch, onActivated, onUpdated, onDeactivated } from 'vue';
ref,
reactive,
watch,
onActivated,
onUpdated,
onDeactivated,
} from 'vue';
import Btn from './Btn.vue'; import Btn from './Btn.vue';
import SongItem from './SongItem.vue'; import SongItem from './SongItem.vue';
@ -241,11 +234,12 @@ onDeactivated(() => {
<button <button
v-for="f in filters" v-for="f in filters"
class="filter caps" class="filter caps"
:key="f"
:data-active="f == filter"
@click=" @click="
filter = f; filter = f;
getSearch(nav.state.search); getSearch(nav.state.search);
" ">
:data-active="f == filter">
{{ t('title.' + f.split('_')[1]) }} {{ t('title.' + f.split('_')[1]) }}
</button> </button>
</div> </div>
@ -255,35 +249,35 @@ onDeactivated(() => {
class="search-wrap"> class="search-wrap">
<h2 v-if="!isSearch">{{ t('title.songs') }}</h2> <h2 v-if="!isSearch">{{ t('title.songs') }}</h2>
<div class="grid"> <div class="grid">
<template v-for="(song, index) in results.items.songs.items"> <SongItem
<SongItem v-for="(song, index) in results.items.songs.items"
:index="index" :key="song.url || song.id"
:playlistId="song.playlistId" :index="index"
:author="song.uploaderName || song.subtitle" :playlistId="song.playlistId"
:title="song.title || song.name" :author="song.uploaderName || song.subtitle"
:channel="song.uploaderUrl || '/channel/' + song.subId" :title="song.title || song.name"
:play="song.url || '/watch?v=' + song.id" :channel="song.uploaderUrl || '/channel/' + song.subId"
:art=" :play="song.url || '/watch?v=' + song.id"
song.thumbnail || song.thumbnails[1]?.url || song.thumbnails[0]?.url :art="
" song.thumbnail || song.thumbnails[1]?.url || song.thumbnails[0]?.url
@remove="removeSong" "
@open-song=" @remove="removeSong"
$emit('play-urls', [ @open-song="
{ $emit('play-urls', [
url: song.url || '/watch?v=' + song.id, {
title: song.title || song.name, url: song.url || '/watch?v=' + song.id,
thumbnails: [ title: song.title || song.name,
{ thumbnails: [
url: {
song.thumbnail || url:
song.thumbnails[1]?.url || song.thumbnail ||
song.thumbnails[0]?.url, song.thumbnails[1]?.url ||
}, song.thumbnails[0]?.url,
], },
}, ],
]) },
" /> ])
</template> " />
</div> </div>
<a <a
v-if="artist.state.playlistId" v-if="artist.state.playlistId"
@ -301,15 +295,15 @@ onDeactivated(() => {
class="search-wrap"> class="search-wrap">
<h2 v-if="!isSearch">{{ t('title.albums') }}</h2> <h2 v-if="!isSearch">{{ t('title.albums') }}</h2>
<div class="grid-3"> <div class="grid-3">
<template v-for="album in results.items.albums.items"> <AlbumItem
<AlbumItem v-for="album in results.items.albums.items"
:author="album.uploaderName || album.subtitle" :key="album.url || album.id"
:name="album.name || album.title" :author="album.uploaderName || album.subtitle"
:art="album.thumbnail || album.thumbnails[0].url" :name="album.name || album.title"
@open-album=" :art="album.thumbnail || album.thumbnails[0].url"
results.getAlbum(album.url || '/playlist?list=' + album.id) @open-album="
" /> results.getAlbum(album.url || '/playlist?list=' + album.id)
</template> " />
</div> </div>
</div> </div>
@ -320,6 +314,7 @@ onDeactivated(() => {
<div class="grid-3"> <div class="grid-3">
<AlbumItem <AlbumItem
v-for="pl in results.items.playlists.items" v-for="pl in results.items.playlists.items"
:key="pl.url"
:author="pl.videos + ' Songs • ' + pl.uploaderName" :author="pl.videos + ' Songs • ' + pl.uploaderName"
:name="pl.name" :name="pl.name"
:art="pl.thumbnail" :art="pl.thumbnail"
@ -332,13 +327,13 @@ onDeactivated(() => {
class="search-wrap"> class="search-wrap">
<h2>{{ t('title.singles') }}</h2> <h2>{{ t('title.singles') }}</h2>
<div class="grid-3"> <div class="grid-3">
<template v-for="single in results.items.singles.items"> <AlbumItem
<AlbumItem v-for="single in results.items.singles.items"
:author="single.subtitle" :key="single.id"
:name="single.title" :author="single.subtitle"
:art="single.thumbnails[0].url" :name="single.title"
@open-album="results.getAlbum('/playlist?list=' + single.id)" /> :art="single.thumbnails[0].url"
</template> @open-album="results.getAlbum('/playlist?list=' + single.id)" />
</div> </div>
</div> </div>
@ -355,18 +350,17 @@ onDeactivated(() => {
}} }}
</h2> </h2>
<div class="grid-3 circle"> <div class="grid-3 circle">
<template <AlbumItem
v-for="a in results.items.artists v-for="a in results.items.artists
? results.items.artists.items ? results.items.artists.items
: results.items.recommendedArtists.items"> : results.items.recommendedArtists.items"
<AlbumItem :key="a.id || a.url"
:author="a.subtitle" :author="a.subtitle"
:name="a.name || a.title" :name="a.name || a.title"
:art="a.thumbnail || a.thumbnails[0].url" :art="a.thumbnail || a.thumbnails[0].url"
@open-album=" @open-album="
artist.getArtist(a.id || a.url.replace('/channel/', '')) artist.getArtist(a.id || a.url.replace('/channel/', ''))
" /> " />
</template>
</div> </div>
</div> </div>
</template> </template>

View file

@ -5,6 +5,12 @@ import { useNav, useI18n } from '@/stores/misc.js';
const { t } = useI18n(), const { t } = useI18n(),
show = ref(false), show = ref(false),
nav = useNav(); nav = useNav();
function search(e) {
nav.state.search = e.target.value;
nav.state.page = 'home';
e.target.blur();
}
</script> </script>
<template> <template>
@ -19,11 +25,8 @@ const { t } = useI18n(),
type="search" type="search"
aria-label="Search Input" aria-label="Search Input"
:placeholder="t('title.search') + '...'" :placeholder="t('title.search') + '...'"
@change=" @change="search"
nav.state.search = $event.target.value; @keyup.enter="search"
nav.state.page = 'home';
$event.target.blur();
"
:value="nav.state.search" /> :value="nav.state.search" />
</div> </div>
</Transition> </Transition>

View file

@ -5,7 +5,7 @@ import { getJsonAuth } from '@/scripts/fetch.js';
import { useRand } from '@/scripts/colors.js'; import { useRand } from '@/scripts/colors.js';
import { useStore } from '@/scripts/util.js'; import { useStore } from '@/scripts/util.js';
import { useArtist, useResults } from '@/stores/results.js'; import { useArtist } from '@/stores/results.js';
import { useData, usePlayer } from '@/stores/player.js'; import { useData, usePlayer } from '@/stores/player.js';
const rand = useRand(), const rand = useRand(),

View file

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, reactive, watch } from 'vue'; import { ref, reactive } from 'vue';
import Modal from './Modal.vue'; import Modal from './Modal.vue';
@ -19,8 +19,9 @@ const { t } = useI18n(),
player = usePlayer(), player = usePlayer(),
store = useStore(); store = useStore();
const emit = defineEmits(['save']), defineEmits(['save']);
showme = reactive({
const showme = reactive({
menu: false, menu: false,
pl: false, pl: false,
vol: false, vol: false,
@ -112,29 +113,29 @@ async function Like() {
} }
"> ">
<template #content> <template #content>
<template v-for="i in list"> <div
<div v-for="i in list"
class="flex item" :key="i.name"
@click=" class="flex item"
pl = i.name; @click="
plRemote = false; pl = i.name;
" plRemote = false;
:data-active="pl == i.name && plRemote == false"> "
<span>{{ i.name }}</span :data-active="pl == i.name && plRemote == false">
><span class="ml-auto">{{ i.urls.length || '' }}</span> <span>{{ i.name }}</span
</div> ><span class="ml-auto">{{ i.urls.length || '' }}</span>
</template> </div>
<template v-for="i in remote"> <div
<div v-for="i in remote"
class="flex item" :key="i.id"
@click=" class="flex item"
pl = i.id; @click="
plRemote = true; pl = i.id;
" plRemote = true;
:data-active="pl == i.id && plRemote == true"> "
<span>{{ i.name }}</span> :data-active="pl == i.id && plRemote == true">
</div> <span>{{ i.name }}</span>
</template> </div>
</template> </template>
<template #buttons> <template #buttons>
<button aria-label="Cancel" @click="showme.pl = false"> <button aria-label="Cancel" @click="showme.pl = false">

View file

@ -58,7 +58,7 @@
"dark": "Mračno (Zadano)", "dark": "Mračno (Zadano)",
"dracula": "Drakula", "dracula": "Drakula",
"nord": "Nord", "nord": "Nord",
"blur": "Zamućenje", "blur": "Zamagljeno",
"blur_light": "Zamućenje (Svjetlo)", "blur_light": "Zamućenje (Svjetlo)",
"light": "Svjetlo" "light": "Svjetlo"
}, },

View file

@ -30,7 +30,7 @@
"tab": "Výchozí karta", "tab": "Výchozí karta",
"dark": "Tmavý (výchozí)", "dark": "Tmavý (výchozí)",
"light": "Světlý", "light": "Světlý",
"blur": "Rozmazaný", "blur": "Rozmazání",
"blur_light": "Rozmazaný (světlý)", "blur_light": "Rozmazaný (světlý)",
"dracula": "Drákula", "dracula": "Drákula",
"nord": "Nord" "nord": "Nord"
@ -47,7 +47,7 @@
}, },
"lyrics": { "lyrics": {
"load": "Načítání textů", "load": "Načítání textů",
"void": "Žádné texty" "void": "Texty nenalezeny"
}, },
"action": { "action": {
"back": "Zpět", "back": "Zpět",

View file

@ -46,9 +46,9 @@
"best": "am Besten", "best": "am Besten",
"tab": "Standard-Registerkarte", "tab": "Standard-Registerkarte",
"dark": "Dunkel (Standard)", "dark": "Dunkel (Standard)",
"light": "Licht", "light": "Hell",
"blur": "Unschärfe", "blur": "Unschärfe",
"blur_light": "Unschärfe (Licht)", "blur_light": "Unschärfe (hell)",
"dracula": "Dracula", "dracula": "Dracula",
"nord": "Nord" "nord": "Nord"
}, },

View file

@ -14,7 +14,8 @@
"local": "Regional", "local": "Regional",
"remote": "Remoto", "remote": "Remoto",
"search": "Búsqueda", "search": "Búsqueda",
"playlists": "Listas de reproducción" "playlists": "Listas de reproducción",
"feeds": "Fuentes"
}, },
"action": { "action": {
"back": "Atrás", "back": "Atrás",
@ -43,7 +44,13 @@
"auto_queue": "Canciones en la cola de reproducción automáticamente", "auto_queue": "Canciones en la cola de reproducción automáticamente",
"worst": "La peor", "worst": "La peor",
"best": "La mejor", "best": "La mejor",
"tab": "Ficha predeterminada" "tab": "Ficha predeterminada",
"dark": "Oscuro (Por defecto)",
"blur": "Desenfocar",
"light": "Claro",
"blur_light": "Desenfoque (Luz)",
"dracula": "Drácula",
"nord": "Nord"
}, },
"info": { "info": {
"see_all": "Ver todo", "see_all": "Ver todo",

View file

@ -14,7 +14,8 @@
"remote": "À distance", "remote": "À distance",
"community": "Communauté", "community": "Communauté",
"search": "Recherche", "search": "Recherche",
"playlists": "Listes de lecture" "playlists": "Listes de lecture",
"feeds": "Flux"
}, },
"playlist": { "playlist": {
"local": "Listes de lecture locales", "local": "Listes de lecture locales",
@ -35,7 +36,13 @@
"player": "Lecteur audio", "player": "Lecteur audio",
"auto_queue": "Mettre automatiquement les chansons en file d'attente", "auto_queue": "Mettre automatiquement les chansons en file d'attente",
"codec": "Codec", "codec": "Codec",
"tab": "Onglet par défaut" "tab": "Onglet par défaut",
"dark": "Sombre (par défaut)",
"light": "Clair",
"blur": "Flou",
"blur_light": "Flou (clair)",
"dracula": "Dracula",
"nord": "Nord"
}, },
"info": { "info": {
"see_all": "Voir tout", "see_all": "Voir tout",

View file

@ -1,12 +1,15 @@
{ {
"title": { "title": {
"songs": "Cancións", "songs": "Cancións",
"albums": "Álbums", "albums": "Álbums",
"singles": "Singles", "singles": "Singles",
"artists": "Artistas", "artists": "Artistas",
"similar_artists": "Artistas similares", "similar_artists": "Artistas similares",
"moods": "Estado de ánimo", "moods": "Estado de ánimo",
"genres": "Xéneros", "genres": "Xéneros",
"featured": "Destacado" "featured": "Destacado"
} },
"pref": {
"blur": "Desenfoque"
}
} }

View file

@ -9,7 +9,13 @@
"quality": "Qualità", "quality": "Qualità",
"auto": "automatica", "auto": "automatica",
"worst": "la peggia", "worst": "la peggia",
"tab": "Scheda predefinita" "tab": "Scheda predefinita",
"dark": "Scuro (predefinito)",
"light": "Chiaro",
"blur": "Sfocatura",
"blur_light": "Sfocatura (chiaro)",
"dracula": "Dracula",
"nord": "Nord"
}, },
"title": { "title": {
"albums": "Album", "albums": "Album",
@ -25,7 +31,8 @@
"local": "Locale", "local": "Locale",
"search": "Ricerca", "search": "Ricerca",
"featured": "In primo piano", "featured": "In primo piano",
"playlists": "Playlist" "playlists": "Playlist",
"feeds": "Flussi"
}, },
"action": { "action": {
"add": "Aggiungi", "add": "Aggiungi",

74
src/locales/pt_br.json Normal file
View file

@ -0,0 +1,74 @@
{
"title": {
"songs": "Músicas",
"albums": "Álbuns",
"singles": "Singles",
"artists": "Artistas",
"similar_artists": "Artistas Similares",
"playlists": "Playlists",
"moods": "Humores",
"genres": "Gêneros",
"featured": "Destaques",
"spotlight": "Foco",
"community": "Comunidade",
"login": "Entrar",
"local": "Local",
"remote": "Remoto",
"search": "Pesquisar",
"feeds": "Feeds"
},
"action": {
"back": "Voltar",
"add": "Adicionar",
"create": "Criar",
"cancel": "Cancelar",
"send": "Enviar",
"receive": "Receber"
},
"playlist": {
"local": "Playlists Locais",
"remote": "Playlists Remotas",
"name": "Nome da Playlist",
"add": "Adicionar Músicas à Playlist",
"create": "Criar uma nova Playlist",
"sync": "Sincronizar playlists",
"select": "Selecione a Playlist para Adicionar"
},
"pref": {
"theme": "Tema",
"dark": "Escuro (padrão)",
"light": "Claro",
"blur": "Desfocado",
"blur_light": "Desfocado (Claro)",
"dracula": "Dracula",
"nord": "Nord",
"tab": "Aba Padrão",
"player": "Reprodutor de Áudio",
"auto_queue": "Enfileirar músicas automaticamente",
"codec": "Codec",
"quality": "Qualidade",
"auto": "automática",
"best": "melhor",
"worst": "pior",
"volume": "Volume Padrão"
},
"info": {
"see_all": "Ver Tudo",
"search": "Comece a pesquisar",
"no_info": "Nenhuma Informação Disponível"
},
"instances": {
"hyp": "Instância do Hyperpipe",
"piped": "Instância do Piped",
"auth": "Instância de Autenticação",
"name": "Nome",
"loc": "Localizações",
"cdn": "CDN",
"up_to_date": "Atualizado",
"version": "Versão"
},
"lyrics": {
"load": "Buscando Letras",
"void": "Sem Letras"
}
}

View file

@ -14,7 +14,8 @@
"login": "Autentificare", "login": "Autentificare",
"search": "Căutare", "search": "Căutare",
"local": "Local", "local": "Local",
"songs": "Melodii" "songs": "Melodii",
"feeds": "Fluxuri"
}, },
"action": { "action": {
"back": "Înapoi", "back": "Înapoi",
@ -43,7 +44,13 @@
"best": "cel mai bun", "best": "cel mai bun",
"auto": "auto", "auto": "auto",
"worst": "cel mai rău", "worst": "cel mai rău",
"volume": "Volum implicit" "volume": "Volum implicit",
"blur": "Blur",
"dark": "Întunecat (implicit)",
"light": "Luminat",
"blur_light": "Blur (Lumină)",
"dracula": "Dracula",
"nord": "Nord"
}, },
"instances": { "instances": {
"cdn": "CDN", "cdn": "CDN",

View file

@ -14,7 +14,8 @@
"remote": "Удалённый", "remote": "Удалённый",
"albums": "Альбомы", "albums": "Альбомы",
"spotlight": "Поиск", "spotlight": "Поиск",
"playlists": "Плейлисты" "playlists": "Плейлисты",
"feeds": "Фиды"
}, },
"action": { "action": {
"back": "Назад", "back": "Назад",
@ -43,7 +44,13 @@
"volume": "Громкость по умолчанию", "volume": "Громкость по умолчанию",
"quality": "Качество", "quality": "Качество",
"best": "лучшее", "best": "лучшее",
"tab": "Вкладка по умолчанию" "tab": "Вкладка по умолчанию",
"dark": "Темный (по умолчанию)",
"light": "Светлый",
"blur": "Размытый",
"blur_light": "Размытый (светлый)",
"dracula": "Дракула",
"nord": "Норд"
}, },
"instances": { "instances": {
"piped": "Инстанс Piped", "piped": "Инстанс Piped",

View file

@ -46,7 +46,7 @@
"volume": "Подразумевана јачина звука", "volume": "Подразумевана јачина звука",
"tab": "Подразумевана Картица", "tab": "Подразумевана Картица",
"light": "Светло", "light": "Светло",
"blur": "Замућење", "blur": "Замагљено",
"dracula": "Дракула", "dracula": "Дракула",
"nord": "Норд", "nord": "Норд",
"dark": "Мрачно (Подразумевано)", "dark": "Мрачно (Подразумевано)",

View file

@ -14,7 +14,8 @@
"genres": "Türler", "genres": "Türler",
"moods": "Ruh hâli", "moods": "Ruh hâli",
"spotlight": "Spot ışıkları", "spotlight": "Spot ışıkları",
"remote": "Uzak" "remote": "Uzak",
"feeds": "Akışlar"
}, },
"action": { "action": {
"back": "Geri", "back": "Geri",
@ -38,12 +39,18 @@
"player": "Ses Çalar", "player": "Ses Çalar",
"auto_queue": "Şarkıları Otomatik Olarak Sırala", "auto_queue": "Şarkıları Otomatik Olarak Sırala",
"codec": "Codec", "codec": "Codec",
"tab": "Varsayılan Sekme", "tab": "Öntanımlı Sekme",
"quality": "Kalite", "quality": "Kalite",
"auto": "otomatik", "auto": "otomatik",
"best": "en iyi", "best": "en iyi",
"worst": "en kötü", "worst": "en kötü",
"volume": "Varsayılan Ses Düzeyi" "volume": "Öntanımlı Ses Düzeyi",
"dark": "Koyu (Öntanımlı)",
"light": "Açık",
"blur": "Bulanıklaştır",
"blur_light": "Bulanıklaştır (Açık)",
"dracula": "Dracula",
"nord": "Nord"
}, },
"info": { "info": {
"see_all": "Hepsini Gör", "see_all": "Hepsini Gör",

View file

@ -10,7 +10,9 @@
"singles": "Các đĩa đơn", "singles": "Các đĩa đơn",
"login": "Đăng nhập", "login": "Đăng nhập",
"moods": "Chủ đề", "moods": "Chủ đề",
"playlists": "Các danh sách phát" "playlists": "Các danh sách phát",
"featured": "Nổi bật",
"feeds": "Nguồn cấp dữ liệu"
}, },
"pref": { "pref": {
"auto": "tự động", "auto": "tự động",
@ -20,28 +22,45 @@
"best": "cao nhất", "best": "cao nhất",
"worst": "thấp nhất", "worst": "thấp nhất",
"quality": "Chất lượng", "quality": "Chất lượng",
"theme": "Giao diện" "theme": "Giao diện",
"dark": "Tối (Mặc định)",
"light": "Sáng",
"blur": "Mờ",
"blur_light": "Mờ (Sáng)",
"nord": "Nord",
"dracula": "Dracula",
"auto_queue": "Tự động xếp hàng các bài hát",
"tab": "Thẻ mặc định"
}, },
"action": { "action": {
"cancel": "Hủy", "cancel": "Hủy",
"create": "Tạo", "create": "Tạo",
"add": "Thêm", "add": "Thêm",
"back": "Quay lại", "back": "Quay lại",
"receive": "Nhận" "receive": "Nhận",
"send": "Gửi"
}, },
"instances": { "instances": {
"version": "Phiên bản", "version": "Phiên bản",
"name": "Tên", "name": "Tên",
"cdn": "CDN", "cdn": "CDN",
"loc": "Các vị trí", "loc": "Các vị trí",
"up_to_date": "Phiên bản mới nhất" "up_to_date": "Phiên bản mới nhất",
"auth": "Instance xác thực"
}, },
"playlist": { "playlist": {
"create": "Tạo một danh sách phát mới", "create": "Tạo một danh sách phát mới",
"name": "Tên danh sách phát", "name": "Tên danh sách phát",
"add": "Thêm bài hát vào danh sách phát" "add": "Thêm bài hát vào danh sách phát",
"sync": "Đồng bộ hóa các danh sách phát",
"select": "Lựa chọn danh sách phát để thêm"
}, },
"info": { "info": {
"search": "Bắt đầu tìm kiếm" "search": "Bắt đầu tìm kiếm",
"see_all": "Xem tất cả"
},
"lyrics": {
"load": "Tìm lời bài hát",
"void": "Không có lời bài hát"
} }
} }

View file

@ -23,7 +23,8 @@
"songs": "歌曲", "songs": "歌曲",
"albums": "专辑", "albums": "专辑",
"login": "登录", "login": "登录",
"playlists": "播放列表" "playlists": "播放列表",
"feeds": "订阅源"
}, },
"action": { "action": {
"add": "添加", "add": "添加",
@ -43,7 +44,13 @@
"worst": "最差", "worst": "最差",
"volume": "默认音量", "volume": "默认音量",
"theme": "主题", "theme": "主题",
"tab": "默认标签页" "tab": "默认标签页",
"blur": "模糊",
"dark": "深色(默认)",
"light": "亮色",
"blur_light": "模糊(亮色)",
"dracula": "Dracula",
"nord": "Nord"
}, },
"info": { "info": {
"see_all": "显示全部", "see_all": "显示全部",

View file

@ -92,6 +92,10 @@ export const SUPPORTED_LOCALES = [
code: 'zh_Hans', code: 'zh_Hans',
name: '中文 (简体)', name: '中文 (简体)',
}, },
{
code: 'pt_br',
name: 'Português Brasileiro',
},
]; ];
export const useNav = defineStore('nav', () => { export const useNav = defineStore('nav', () => {

View file

@ -29,7 +29,7 @@ export const useData = defineStore('data', () => {
console.log(json); console.log(json);
state.art = json.thumbnailUrl; state.art = json.thumbnailUrl.replaceAll('&amp;', '&');
state.description = json.description; state.description = json.description;
state.title = json.title.replaceAll('&amp;', '&'); state.title = json.title.replaceAll('&amp;', '&');
state.artist = json.uploader state.artist = json.uploader

View file

@ -68,7 +68,7 @@ export const useResults = defineStore('results', () => {
useNav().state.page = 'home'; useNav().state.page = 'home';
next.value = next.value =
json.nextpage || json.nextpage != 'null' json.nextpage && json.nextpage != 'null'
? hash + '?nextpage=' + encodeURIComponent(json.nextpage) ? hash + '?nextpage=' + encodeURIComponent(json.nextpage)
: null; : null;
} }