merge master into oidc

This commit is contained in:
Jeidnx 2023-10-26 14:00:35 +02:00
commit 7616a3d34c
No known key found for this signature in database
GPG key ID: 0E9E697B7E99DF39
90 changed files with 4770 additions and 2603 deletions

7
.eslintrc.cjs Normal file
View file

@ -0,0 +1,7 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: ["plugin:vue/vue3-recommended", "eslint:recommended", "@unocss", "plugin:prettier/recommended"],
};

View file

@ -8,7 +8,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
@ -19,4 +19,8 @@ jobs:
cache: "pnpm"
- run: pnpm install
- run: pnpm build
- uses: actions/upload-artifact@v3
with:
name: build
path: dist
- run: pnpm lint --no-fix

65
.github/workflows/codeql.yml vendored Normal file
View file

@ -0,0 +1,65 @@
name: "CodeQL"
on:
push:
branches: [ 'master' ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'master' ]
schedule:
- cron: '42 11 * * 4'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

View file

@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: true
- name: Build And Deploy

View file

@ -11,7 +11,7 @@ jobs:
build-docker-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
@ -23,21 +23,21 @@ jobs:
- run: pnpm install
- run: pnpm build && ./localizefonts.sh && mv dist/ dist-ci/
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
with:
version: latest
- name: Login to DockerHub
uses: docker/login-action@v2
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v4
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.ci

View file

@ -11,7 +11,7 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:

25
.github/workflows/reviewdog.yml vendored Normal file
View file

@ -0,0 +1,25 @@
name: reviewdog
on: [pull_request]
jobs:
eslint:
name: runner / eslint
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: latest
- name: Setup Node.js
uses: actions/setup-node@v3
with:
cache: "pnpm"
- run: pnpm install
- uses: reviewdog/action-eslint@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review
eslint_flags: "--ignore-path .gitignore --ext .js,.vue ."

View file

@ -8,7 +8,7 @@ jobs:
merge:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Check if en.json has been updated
run: |
if -n git diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }} src/locales/en.json; then

View file

@ -2,6 +2,7 @@
[![AGPL v3](https://shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0.en.html)
[![Matrix](https://img.shields.io/matrix/piped:matrix.org)](https://matrix.to/#/#piped:matrix.org)
[![Lemmy](https://img.shields.io/lemmy/piped%40feddit.rocks)](https://feddit.rocks/c/piped)
[![Registered Users](https://pipedapi.kavin.rocks/registered/badge)](https://piped.video/register)
[![IPFS Build](https://github.com/TeamPiped/Piped/actions/workflows/ipfs-build.yml/badge.svg)](https://piped-ipfs.kavin.rocks/)
[![GitHub Repo stars](https://img.shields.io/github/stars/TeamPiped/Piped-Frontend?style=social)](https://github.com/TeamPiped/Piped/stargazers)
@ -61,6 +62,10 @@ By using Piped, you can freely watch and listen to content without the fear of p
- You can join us via Matrix at [#piped](https://matrix.to/#/#piped:matrix.org).
- You can also join us at the libera.chat IRC network which is bridged to the Matrix room at [#piped](https://web.libera.chat/#piped).
## Public Communities
- You can join us on Lemmy on the [!piped@feddit.rocks](https://feddit.rocks/c/piped) community.
## Self-Hosting
See https://docs.piped.video/docs/self-hosting/ for more details.
@ -140,12 +145,15 @@ Contributions in any other form are also welcomed.
- [Yattee](https://github.com/yattee/yattee) - an alternative frontend for YouTube, for IOS.
- [LibreTube](https://github.com/Libre-tube/LibreTube) - an alternative frontend for YouTube, for Android.
- [Racoon](https://github.com/shailendramaurya/racoon) - A web based minimal YouTube downloader.
- [Hyperpipe](https://codeberg.org/Hyperpipe/Hyperpipe) - an alternative privacy respecting frontend for YouTube Music.
- [Musicale](https://github.com/Bellisario/musicale) - an alternative to YouTube Music, with style.
- [ytify](https://github.com/n-ce/ytify) - a complementary minimal audio streaming frontend for YouTube.
- [PsTube](https://github.com/prateekmedia/pstube) - Watch and download videos without ads on Android, Linux, Windows, iOS, and Mac OSX.
- [Piped-Material](https://github.com/mmjee/Piped-Material) - A fork of Piped, focusing on better performance and a more usable design.
- [ReacTube](https://github.com/NeeRaj-2401/ReacTube) - Privacy friendly & distraction free Youtube front-end using Piped API.
- [YTDLnis](https://github.com/deniscerri/ytdlnis) - Video and audio downloader for Android that uses Piped to update formats.
- [DeskVideo](https://github.com/malisipi/DeskVideo) - A desktop styled, customizable alternative front-end for YouTube.
## YourKit

View file

@ -1,7 +1,8 @@
#!/bin/sh
base='https://fonts\.(gstatic\.com|kavin\.rocks)'
fonts=$(cat dist/assets/* | grep -Po "$base[^)]*" | sort | uniq)
fonts=$(cat dist/assets/* | grep -Eo "${base}[^)]*" | sort | uniq)
for font in $fonts; do
file="dist/fonts$(echo "$font" | sed -E "s#$base##")"
mkdir -p "$(dirname "$file")"

View file

@ -10,57 +10,51 @@
"lint": "eslint --fix --color --ignore-path .gitignore --ext .js,.vue ."
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.4.0",
"@fortawesome/free-brands-svg-icons": "6.4.0",
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@fortawesome/vue-fontawesome": "3.0.3",
"buffer": "6.0.3",
"dompurify": "3.0.3",
"hotkeys-js": "3.10.2",
"dompurify": "3.0.5",
"hotkeys-js": "3.12.0",
"javascript-time-ago": "2.5.9",
"linkify-html": "4.1.1",
"linkifyjs": "4.1.1",
"mux.js": "6.3.0",
"shaka-player": "4.3.6",
"qrcode": "^1.5.3",
"shaka-player": "4.4.2",
"stream-browserify": "3.0.0",
"vue": "3.3.4",
"vue-i18n": "9.2.2",
"vue-router": "4.2.2",
"vue-i18n": "9.4.1",
"vue-router": "4.2.5",
"xml-js": "1.6.11"
},
"devDependencies": {
"@iconify-json/fa6-brands": "1.1.11",
"@iconify-json/fa6-solid": "1.1.13",
"@intlify/unplugin-vue-i18n": "0.11.0",
"@unocss/preset-icons": "0.53.1",
"@unocss/preset-web-fonts": "0.53.1",
"@unocss/reset": "0.53.1",
"@unocss/transformer-directives": "0.53.1",
"@unocss/transformer-variant-group": "0.53.1",
"@vitejs/plugin-legacy": "4.0.4",
"@vitejs/plugin-vue": "4.2.3",
"@iconify-json/fa6-brands": "1.1.13",
"@iconify-json/fa6-solid": "1.1.15",
"@intlify/unplugin-vue-i18n": "1.2.0",
"@unocss/eslint-config": "0.56.1",
"@unocss/preset-icons": "0.56.1",
"@unocss/preset-uno": "0.56.1",
"@unocss/preset-web-fonts": "0.56.1",
"@unocss/reset": "0.56.1",
"@unocss/transformer-directives": "0.56.1",
"@unocss/transformer-variant-group": "0.56.1",
"@vitejs/plugin-legacy": "4.1.1",
"@vitejs/plugin-vue": "4.3.4",
"@vue/compiler-sfc": "3.3.4",
"eslint": "8.43.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-vue": "9.14.1",
"prettier": "2.8.8",
"unocss": "0.53.1",
"vite": "4.3.9",
"eslint": "8.50.0",
"eslint-config-prettier": "9.0.0",
"eslint-plugin-prettier": "5.0.0",
"eslint-plugin-vue": "9.17.0",
"lightningcss": "1.22.0",
"prettier": "3.0.3",
"unocss": "0.56.1",
"vite": "4.4.9",
"vite-plugin-eslint": "1.8.1",
"vite-plugin-pwa": "0.16.4",
"vite-plugin-pwa": "0.16.5",
"workbox-window": "7.0.0"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/vue3-essential",
"plugin:prettier/recommended",
"eslint:recommended"
],
"rules": {}
},
"browserslist": [
"last 1 chrome version",
"last 1 firefox version"

2993
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
<template>
<div class="flex flex-col w-full min-h-screen px-1vw py-5 antialiased reset" :class="[theme]">
<div class="reset min-h-screen w-full flex flex-col px-1vw py-5 antialiased" :class="[theme]">
<div class="flex-1">
<NavBar />
<router-view v-slot="{ Component }">
@ -29,25 +29,6 @@ export default {
theme: "dark",
};
},
methods: {
setTheme() {
let themePref = this.getPreferenceString("theme", "dark");
if (themePref == "auto") this.theme = darkModePreference.matches ? "dark" : "light";
else this.theme = themePref;
// Change title bar color based on user's theme
const themeColor = document.querySelector("meta[name='theme-color']");
if (this.theme === "light") {
themeColor.setAttribute("content", "#FFF");
} else {
themeColor.setAttribute("content", "#0F0F0F");
}
// Used for the scrollbar
const root = document.querySelector(":root");
this.theme == "dark" ? root.classList.add("dark") : root.classList.remove("dark");
},
},
mounted() {
this.setTheme();
darkModePreference.addEventListener("change", () => {
@ -55,7 +36,7 @@ export default {
});
if ("indexedDB" in window) {
const request = indexedDB.open("piped-db", 4);
const request = indexedDB.open("piped-db", 5);
request.onupgradeneeded = ev => {
const db = request.result;
console.log("Upgrading object store.");
@ -77,6 +58,12 @@ export default {
const store = db.createObjectStore("channel_groups", { keyPath: "groupName" });
store.createIndex("groupName", "groupName", { unique: true });
}
if (!db.objectStoreNames.contains("playlists")) {
const playlistStore = db.createObjectStore("playlists", { keyPath: "playlistId" });
playlistStore.createIndex("playlistId", "playlistId", { unique: true });
const playlistVideosStore = db.createObjectStore("playlist_videos", { keyPath: "videoId" });
playlistVideosStore.createIndex("videoId", "videoId", { unique: true });
}
};
request.onsuccess = e => {
window.db = e.target.result;
@ -106,6 +93,25 @@ export default {
}
})();
},
methods: {
setTheme() {
let themePref = this.getPreferenceString("theme", "dark");
if (themePref == "auto") this.theme = darkModePreference.matches ? "dark" : "light";
else this.theme = themePref;
// Change title bar color based on user's theme
const themeColor = document.querySelector("meta[name='theme-color']");
if (this.theme === "light") {
themeColor.setAttribute("content", "#FFF");
} else {
themeColor.setAttribute("content", "#0F0F0F");
}
// Used for the scrollbar
const root = document.querySelector(":root");
this.theme == "dark" ? root.classList.add("dark") : root.classList.remove("dark");
},
},
};
</script>
@ -212,7 +218,7 @@ b {
}
.input {
@apply pl-2.5;
@apply px-2.5;
}
.input:focus {

View file

@ -0,0 +1,54 @@
<template>
<ModalComponent @close="$emit('close')">
<span v-t="'actions.add_to_group'" class="mb-3 inline-block w-max text-2xl" />
<div v-for="(group, index) in channelGroups" :key="group.groupName" class="px-1">
<div class="flex items-center justify-between">
<span>{{ group.groupName }}</span>
<input
type="checkbox"
:checked="group.channels.includes(channelId)"
@change="onCheckedChange(index, group)"
/>
</div>
<hr class="h-1 w-full" />
</div>
</ModalComponent>
</template>
<script>
import ModalComponent from "./ModalComponent.vue";
export default {
components: {
ModalComponent,
},
props: {
channelId: {
type: String,
required: true,
},
},
emits: ["close"],
data() {
return {
channelGroups: [],
};
},
mounted() {
this.loadChannelGroups();
},
methods: {
async loadChannelGroups() {
const groups = await this.getChannelGroups();
this.channelGroups.push(...groups);
},
onCheckedChange(index, group) {
if (group.channels.includes(this.channelId)) {
group.channels.splice(index, 1);
} else {
group.channels.push(this.channelId);
}
this.createOrUpdateChannelGroup(group);
},
},
};
</script>

View file

@ -1,19 +1,19 @@
<template>
<div>
<div class="flex flex-col flex-justify-between">
<router-link :to="props.item.url">
<div class="relative">
<img class="w-full" :src="props.item.thumbnail" loading="lazy" />
<div class="my-4 flex justify-center">
<img class="aspect-square w-[50%] rounded-full" :src="props.item.thumbnail" loading="lazy" />
</div>
<p>
<span v-text="props.item.name" />
<font-awesome-icon class="ml-1.5" v-if="props.item.verified" icon="check" />
<font-awesome-icon v-if="props.item.verified" class="ml-1.5" icon="check" />
</p>
</router-link>
<p v-if="props.item.description" v-text="props.item.description" />
<router-link v-if="props.item.uploaderUrl" class="link" :to="props.item.uploaderUrl">
<p>
<span v-text="props.item.uploader" />
<font-awesome-icon class="ml-1.5" v-if="props.item.uploaderVerified" icon="check" />
<font-awesome-icon v-if="props.item.uploaderVerified" class="ml-1.5" icon="check" />
</p>
</router-link>
@ -29,6 +29,9 @@
<script setup>
const props = defineProps({
item: Object,
item: {
type: Object,
required: true,
},
});
</script>

View file

@ -5,34 +5,41 @@
<img
v-if="channel.bannerUrl"
:src="channel.bannerUrl"
class="w-full py-1.5 h-30 md:h-50 object-cover"
class="h-30 w-full object-cover py-1.5 md:h-50"
loading="lazy"
/>
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="flex flex-col items-center justify-between md:flex-row">
<div class="flex place-items-center">
<img height="48" width="48" class="rounded-full m-1" :src="channel.avatarUrl" />
<div class="flex gap-1 items-center">
<h1 v-text="channel.name" class="!text-xl" />
<font-awesome-icon class="!text-xl" v-if="channel.verified" icon="check" />
<img height="48" width="48" class="m-1 rounded-full" :src="channel.avatarUrl" />
<div class="flex items-center gap-1">
<h1 class="!text-xl" v-text="channel.name" />
<font-awesome-icon v-if="channel.verified" class="!text-xl" icon="check" />
</div>
</div>
<div class="flex gap-2">
<button
class="btn"
@click="subscribeHandler"
v-t="{
path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`,
args: { count: numberFormat(channel.subscriberCount) },
}"
class="btn"
@click="subscribeHandler"
></button>
<button
v-if="subscribed"
v-t="'actions.add_to_group'"
class="btn"
@click="showGroupModal = true"
></button>
<!-- RSS Feed button -->
<a
v-if="channel.id"
aria-label="RSS feed"
title="RSS feed"
role="button"
v-if="channel.id"
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${channel.id}`"
target="_blank"
class="btn flex-col"
@ -44,15 +51,15 @@
<CollapsableText :text="channel.description" />
<WatchOnButton :link="`https://youtube.com/channel/${this.channel.id}`" />
<WatchOnButton :link="`https://youtube.com/channel/${channel.id}`" />
<div class="flex my-2 mx-1">
<div class="mx-1 my-2 flex">
<button
v-for="(tab, index) in tabs"
:key="tab.name"
class="btn mr-2"
@click="loadTab(index)"
:class="{ active: selectedTab == index }"
@click="loadTab(index)"
>
<span v-text="tab.translatedName"></span>
</button>
@ -70,6 +77,8 @@
hide-channel
/>
</div>
<AddToGroupModal v-if="showGroupModal" :channel-id="channel.id.substr(-11)" @close="showGroupModal = false" />
</LoadingIndicatorPage>
</template>
@ -79,6 +88,7 @@ import ContentItem from "./ContentItem.vue";
import WatchOnButton from "./WatchOnButton.vue";
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
import CollapsableText from "./CollapsableText.vue";
import AddToGroupModal from "./AddToGroupModal.vue";
export default {
components: {
@ -87,6 +97,7 @@ export default {
WatchOnButton,
LoadingIndicatorPage,
CollapsableText,
AddToGroupModal,
},
data() {
return {
@ -95,6 +106,7 @@ export default {
tabs: [],
selectedTab: 0,
contentItems: [],
showGroupModal: false,
};
},
mounted() {
@ -148,6 +160,7 @@ export default {
this.contentItems = this.channel.relatedStreams;
this.fetchSubscribedStatus();
this.updateWatched(this.channel.relatedStreams);
this.fetchDeArrowContent(this.channel.relatedStreams);
this.tabs.push({
translatedName: this.$t("video.videos"),
});
@ -186,6 +199,7 @@ export default {
this.loading = false;
this.updateWatched(json.relatedStreams);
json.relatedStreams.map(stream => this.contentItems.push(stream));
this.fetchDeArrowContent(this.contentItems);
});
},
fetchChannelTabNextPage() {
@ -196,6 +210,7 @@ export default {
this.tabs[this.selectedTab].tabNextPage = json.nextpage;
this.loading = false;
json.content.map(item => this.contentItems.push(item));
this.fetchDeArrowContent(this.contentItems);
this.tabs[this.selectedTab].content = this.contentItems;
});
},
@ -258,6 +273,7 @@ export default {
data: this.tabs[index].data,
}).then(tab => {
this.contentItems = this.tabs[index].content = tab.content;
this.fetchDeArrowContent(this.contentItems);
this.tabs[this.selectedTab].tabNextPage = tab.nextpage;
});
},

View file

@ -1,20 +1,20 @@
<template>
<!-- desktop view -->
<div v-if="!mobileLayout" class="flex-col overflow-y-scroll max-w-35vw max-h-75vh min-h-64 lt-lg:hidden">
<div v-if="!mobileLayout" class="max-h-75vh max-w-35vw min-h-64 flex-col overflow-y-scroll lt-lg:hidden">
<h2 class="mb-2 bg-gray-500/50 p-2" aria-label="chapters" title="chapters">
{{ $t("video.chapters") }} ({{ chapters.length }})
</h2>
<div
:key="chapter.start"
v-for="(chapter, index) in chapters"
@click="$emit('seek', chapter.start)"
:key="chapter.start"
class="chapter-vertical"
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
@click="$emit('seek', chapter.start)"
>
<div class="flex">
<span class="mt-5 mr-2 text-current" v-text="index + 1" />
<span class="mr-2 mt-5 text-current" v-text="index + 1" />
<img class="shrink-0" :src="chapter.image" :alt="chapter.title" />
<div class="flex flex-col m-2">
<div class="m-2 flex flex-col">
<span class="text-sm" :title="chapter.title" v-text="chapter.title" />
<span class="text-sm font-bold text-blue-500" v-text="timeFormat(chapter.start)" />
</div>
@ -25,22 +25,22 @@
<!-- mobile vertical view -->
<div
v-if="mobileLayout && getPreferenceString('mobileChapterLayout') == 'Vertical'"
class="flex flex-col overflow-y-scroll max-h-64"
class="max-h-64 flex flex-col overflow-y-scroll"
>
<h2 class="mb-2 bg-gray-500/50 p-2" aria-label="chapters" title="chapters">
{{ $t("video.chapters") }} ({{ chapters.length }})
</h2>
<div
:key="chapter.start"
v-for="(chapter, index) in chapters"
@click="$emit('seek', chapter.start)"
:key="chapter.start"
class="chapter-vertical"
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
@click="$emit('seek', chapter.start)"
>
<div class="flex">
<span class="mt-5 mr-2 text-current" v-text="index + 1" />
<span class="mr-2 mt-5 text-current" v-text="index + 1" />
<img class="shrink-0" :src="chapter.image" :alt="chapter.title" />
<div class="flex flex-col m-2">
<div class="m-2 flex flex-col">
<span class="text-sm" :title="chapter.title" v-text="chapter.title" />
<span class="text-sm font-bold text-blue-500" v-text="timeFormat(chapter.start)" />
</div>
@ -50,11 +50,11 @@
<!-- mobile Horizontal view -->
<div v-if="getPreferenceString('mobileChapterLayout') == 'Horizontal' && mobileLayout" class="flex overflow-x-auto">
<div
:key="chapter.start"
v-for="(chapter, index) in chapters"
@click="$emit('seek', chapter.start)"
:key="chapter.start"
class="chapter"
:class="{ 'bg-red-500/50': isCurrentChapter(index) }"
@click="$emit('seek', chapter.start)"
>
<img :src="chapter.image" :alt="chapter.title" />
<div class="m-1 flex">
@ -65,6 +65,32 @@
</div>
</template>
<script setup>
const props = defineProps({
chapters: {
type: Object,
default: () => null,
},
mobileLayout: {
type: Boolean,
default: () => true,
},
playerPosition: {
type: Number,
default: () => 0,
},
});
const isCurrentChapter = index => {
return (
props.playerPosition >= props.chapters[index].start &&
props.playerPosition < (props.chapters[index + 1]?.start ?? Infinity)
);
};
defineEmits(["seek"]);
</script>
<style>
::-webkit-scrollbar {
height: 5px;
@ -89,26 +115,3 @@
@apply truncate overflow-hidden inline-block w-10em;
}
</style>
<script setup>
const props = defineProps({
chapters: Object,
mobileLayout: {
type: Boolean,
default: () => true,
},
playerPosition: {
type: Number,
default: () => 0,
},
});
const isCurrentChapter = index => {
return (
props.playerPosition >= props.chapters[index].start &&
props.playerPosition < (props.chapters[index + 1]?.start ?? Infinity)
);
};
defineEmits(["seek"]);
</script>

View file

@ -1,12 +1,13 @@
<template>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-if="text" class="whitespace-pre-wrap py-2 mx-1">
<span v-if="showFullText" v-html="purifyHTML(rewriteDescription(text))" />
<span v-else v-html="purifyHTML(rewriteDescription(text.slice(0, 100)))" />
<span v-if="text.length > 100 && !showFullText">...</span>
<template v-if="text">
<div class="mx-1 whitespace-pre-wrap py-2">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="showFullText" v-html="fullText()" />
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-else v-html="colapsedText()" />
<span v-if="text.length > visibleLimit && !showFullText">...</span>
<button
v-if="text.length > 100"
class="hover:underline font-semibold text-neutral-500 block whitespace-normal"
v-if="text.length > visibleLimit"
class="block whitespace-normal font-semibold text-neutral-500 hover:underline"
@click="showFullText = !showFullText"
>
[{{ showFullText ? $t("actions.show_less") : $t("actions.show_more") }}]
@ -15,14 +16,31 @@
</template>
<script>
import { purifyHTML, rewriteDescription } from "@/utils/HtmlUtils";
export default {
props: {
text: String,
text: {
type: String,
default: null,
},
visibleLimit: {
type: Number,
default: 100,
},
},
data() {
return {
showFullText: false,
};
},
methods: {
fullText() {
return purifyHTML(rewriteDescription(this.text));
},
colapsedText() {
return purifyHTML(rewriteDescription(this.text.slice(0, this.visibleLimit)));
},
},
};
</script>

View file

@ -1,8 +1,8 @@
<template>
<div class="comment flex mt-1.5">
<div class="comment mt-1.5 flex">
<img
:src="comment.thumbnail"
class="comment-avatar rounded-full w-12 h-12"
class="comment-avatar h-12 w-12 rounded-full"
height="48"
width="48"
loading="lazy"
@ -14,34 +14,35 @@
<div v-if="comment.pinned" class="comment-pinned">
<font-awesome-icon icon="thumbtack" />
<span
class="ml-1.5"
v-t="{
path: 'comment.pinned_by',
args: { author: uploader },
}"
class="ml-1.5"
/>
</div>
<div class="comment-author">
<router-link class="font-bold link" :to="comment.commentorUrl">{{ comment.author }}</router-link>
<font-awesome-icon class="ml-1.5" v-if="comment.verified" icon="check" />
<router-link class="link font-bold" :to="comment.commentorUrl">{{ comment.author }}</router-link>
<font-awesome-icon v-if="comment.verified" class="ml-1.5" icon="check" />
</div>
<div class="comment-meta text-sm mb-1.5" v-text="comment.commentedTime" />
<div class="comment-meta mb-1.5 text-sm" v-text="comment.commentedTime" />
</div>
<div class="whitespace-pre-wrap" v-html="purifyHTML(comment.commentText)" />
<!-- eslint-disable-next-line vue/no-v-html -->
<CollapsableText :text="comment.commentText" :visible-limit="500" />
<div class="comment-footer mt-1 flex items-center">
<div class="i-fa6-solid:thumbs-up" />
<span class="ml-1" v-text="numberFormat(comment.likeCount)" />
<font-awesome-icon class="ml-1" v-if="comment.hearted" icon="heart" />
<font-awesome-icon v-if="comment.hearted" class="ml-1" icon="heart" />
</div>
<template v-if="comment.repliesPage && (!loadingReplies || !showingReplies)">
<div @click="loadReplies" class="cursor-pointer">
<div class="cursor-pointer" @click="loadReplies">
<a v-text="`${$t('actions.reply_count', comment.replyCount)}`" />
<font-awesome-icon class="ml-1.5" icon="level-down-alt" />
</div>
</template>
<template v-if="showingReplies">
<div @click="hideReplies" class="cursor-pointer">
<div class="cursor-pointer" @click="hideReplies">
<a v-t="'actions.hide_replies'" />
<font-awesome-icon class="ml-1.5" icon="level-up-alt" />
</div>
@ -50,7 +51,7 @@
<div v-for="reply in replies" :key="reply.commentId" class="w-full">
<CommentItem :comment="reply" :uploader="uploader" :video-id="videoId" />
</div>
<div v-if="nextpage" @click="loadReplies" class="cursor-pointer">
<div v-if="nextpage" class="cursor-pointer" @click="loadReplies">
<a v-t="'actions.load_more_replies'" />
<font-awesome-icon class="ml-1.5" icon="level-down-alt" />
</div>
@ -60,7 +61,10 @@
</template>
<script>
import CollapsableText from "./CollapsableText.vue";
export default {
components: { CollapsableText },
props: {
comment: {
type: Object,
@ -85,7 +89,6 @@ export default {
this.showingReplies = true;
return;
}
this.loadingReplies = true;
this.showingReplies = true;
this.fetchJson(this.apiUrl() + "/nextpage/comments/" + this.videoId, {

View file

@ -2,9 +2,9 @@
<ModalComponent @close="$emit('close')">
<div>
<h3 class="text-xl" v-text="message" />
<div class="ml-auto mt-8 flex gap-2 w-min">
<button class="btn" v-t="'actions.cancel'" @click="$emit('close')" />
<button class="btn" v-t="'actions.okay'" @click="$emit('confirm')" />
<div class="ml-auto mt-8 w-min flex gap-2">
<button v-t="'actions.cancel'" class="btn" @click="$emit('close')" />
<button v-t="'actions.okay'" class="btn" @click="$emit('confirm')" />
</div>
</div>
</ModalComponent>
@ -18,7 +18,10 @@ export default {
ModalComponent,
},
props: {
message: String,
message: {
type: String,
required: true,
},
},
emits: ["close", "confirm"],
};

View file

@ -6,7 +6,10 @@
import { defineAsyncComponent } from "vue";
const props = defineProps({
item: Object,
item: {
type: Object,
required: true,
},
});
const VideoItem = defineAsyncComponent(() => import("./VideoItem.vue"));

View file

@ -1,6 +1,6 @@
<template>
<p v-text="message" />
<button @click="toggleTrace" class="btn" v-t="'actions.show_more'" />
<button v-t="'actions.show_more'" class="btn" @click="toggleTrace" />
<p ref="stacktrace" class="whitespace-pre-wrap" hidden v-text="error" />
</template>

View file

@ -1,7 +1,7 @@
<template>
<h1 v-t="'titles.feed'" class="font-bold text-center my-4" />
<h1 v-t="'titles.feed'" class="my-4 text-center font-bold" />
<div class="flex flex-wrap md:items-center flex-col md:flex-row gap-2 children:(flex gap-1 items-center)">
<div class="flex flex-col flex-wrap gap-2 children:(flex items-center gap-1) md:flex-row md:items-center">
<span>
<label for="filters">
<strong v-text="`${$t('actions.filter')}:`" />
@ -13,7 +13,7 @@
class="select flex-grow"
@change="onFilterChange()"
>
<option v-for="filter in availableFilters" :key="filter" :value="filter" v-t="`video.${filter}`" />
<option v-for="filter in availableFilters" :key="filter" v-t="`video.${filter}`" :value="filter" />
</select>
</span>
@ -22,7 +22,7 @@
<strong v-text="`${$t('titles.channel_groups')}:`" />
</label>
<select id="group-selector" v-model="selectedGroupName" default="" class="select flex-grow">
<option value="" v-t="`video.all`" />
<option v-t="`video.all`" value="" />
<option
v-for="group in channelGroups"
:key="group.groupName"
@ -99,18 +99,7 @@ export default {
if (!window.db) return;
const cursor = this.getChannelGroupsCursor();
cursor.onsuccess = e => {
const cursor = e.target.result;
if (cursor) {
const group = cursor.value;
this.channelGroups.push({
groupName: group.groupName,
channels: JSON.parse(group.channels),
});
cursor.continue();
}
};
this.loadChannelGroups();
},
activated() {
document.title = this.$t("titles.feed") + " - Piped";
@ -135,10 +124,17 @@ export default {
});
}
},
async loadChannelGroups() {
const groups = await this.getChannelGroups();
this.channelGroups.push(...groups);
},
loadMoreVideos() {
if (!this.videosStore) return;
this.currentVideoCount = Math.min(this.currentVideoCount + this.videoStep, this.videosStore.length);
if (this.videos.length != this.videosStore.length)
if (this.videos.length != this.videosStore.length) {
this.videos = this.videosStore.slice(0, this.currentVideoCount);
this.fetchDeArrowContent(this.videos);
}
},
handleScroll() {
if (window.innerHeight + window.scrollY >= document.body.offsetHeight - window.innerHeight) {

View file

@ -1,24 +1,28 @@
<template>
<footer class="text-center py-4 rounded-xl children:(mx-3) w-full mt-10">
<footer class="mt-10 w-full rounded-xl py-4 text-center children:(mx-3)">
<a aria-label="GitHub" href="https://github.com/TeamPiped/Piped" target="_blank">
<font-awesome-icon :icon="['fab', 'github']" />
<span class="ml-2" v-t="'actions.source_code'" />
<span v-t="'actions.source_code'" class="ml-2" />
</a>
<a href="https://docs.piped.video/" target="_blank">
<font-awesome-icon :icon="['fa', 'book']" />
<span class="ml-2" v-t="'actions.documentation'" />
<span v-t="'actions.documentation'" class="ml-2" />
</a>
<a href="https://github.com/TeamPiped/Piped#donations" target="_blank">
<font-awesome-icon :icon="['fab', 'bitcoin']" />
<span class="ml-2" v-t="'actions.donations'" />
<span v-t="'actions.donations'" class="ml-2" />
</a>
<a v-if="statusPageHref" :href="statusPageHref">
<font-awesome-icon :icon="['fa', 'server']" />
<span class="ml-2" v-t="'actions.status_page'" />
<span v-t="'actions.status_page'" class="ml-2" />
</a>
<a v-if="donationHref" :href="donationHref">
<font-awesome-icon :icon="['fa', 'donate']" />
<span class="ml-2" v-t="'actions.instance_donations'" />
<span v-t="'actions.instance_donations'" class="ml-2" />
</a>
<a v-if="privacyPolicyHref" :href="privacyPolicyHref" target="_blank">
<font-awesome-icon :icon="['fa', 'eye']" />
<span v-t="'actions.instance_privacy_policy'" class="ml-2" />
</a>
</footer>
</template>
@ -29,6 +33,7 @@ export default {
return {
donationHref: null,
statusPageHref: null,
privacyPolicyHref: null,
};
},
mounted() {
@ -39,6 +44,7 @@ export default {
this.fetchJson(this.apiUrl() + "/config").then(config => {
this.donationHref = config?.donationUrl;
this.statusPageHref = config?.statusPageUrl;
this.privacyPolicyHref = config?.privacyPolicyUrl;
});
},
},

View file

@ -1,13 +1,32 @@
<template>
<h1 class="font-bold text-center" v-t="'titles.history'" />
<h1 v-t="'titles.history'" class="mb-3 text-center font-bold" />
<div class="flex md:items-center gap-2 flex-col md:flex-row">
<button class="btn" v-t="'actions.clear_history'" @click="clearHistory" />
<div class="flex">
<div class="flex flex-col gap-2 md:flex-row md:items-center">
<button v-t="'actions.clear_history'" class="btn" @click="clearHistory" />
<button class="btn" v-t="'actions.export_to_json'" @click="exportHistory" />
<button v-t="'actions.export_to_json'" class="btn" @click="exportHistory" />
<div class="ml-auto flex gap-1 items-center">
<SortingSelector by-key="watchedAt" @apply="order => videos.sort(order)" />
<div class="ml-auto flex items-center gap-1">
<SortingSelector by-key="watchedAt" @apply="order => videos.sort(order)" />
</div>
</div>
<div class="ml-4 flex items-center">
<input id="autoDelete" v-model="autoDeleteHistory" type="checkbox" @change="onChange" />
<label v-t="'actions.delete_automatically'" class="ml-2" for="autoDelete" />
<select v-model="autoDeleteDelayHours" class="select ml-3 pl-3" @change="onChange">
<option v-t="{ path: 'info.hours', args: { amount: '1' } }" value="1" />
<option v-t="{ path: 'info.hours', args: { amount: '3' } }" value="3" />
<option v-t="{ path: 'info.hours', args: { amount: '6' } }" value="6" />
<option v-t="{ path: 'info.hours', args: { amount: '12' } }" value="12" />
<option v-t="{ path: 'info.days', args: { amount: '1' } }" value="24" />
<option v-t="{ path: 'info.days', args: { amount: '3' } }" value="72" />
<option v-t="{ path: 'info.weeks', args: { amount: '1' } }" value="168" />
<option v-t="{ path: 'info.weeks', args: { amount: '3' } }" value="336" />
<option v-t="{ path: 'info.months', args: { amount: '1' } }" value="672" />
<option v-t="{ path: 'info.months', args: { amount: '2' } }" value="1344" />
</select>
</div>
</div>
@ -32,30 +51,39 @@ export default {
data() {
return {
videos: [],
autoDeleteHistory: false,
autoDeleteDelayHours: "24",
};
},
mounted() {
this.autoDeleteHistory = this.getPreferenceBoolean("autoDeleteWatchHistory", false);
this.autoDeleteDelayHours = this.getPreferenceString("autoDeleteWatchHistoryDelayHours", "24");
(async () => {
if (window.db && this.getPreferenceBoolean("watchHistory", false)) {
var tx = window.db.transaction("watch_history", "readonly");
var tx = window.db.transaction("watch_history", "readwrite");
var store = tx.objectStore("watch_history");
const cursorRequest = store.index("watchedAt").openCursor(null, "prev");
cursorRequest.onsuccess = e => {
const cursor = e.target.result;
if (cursor) {
const video = cursor.value;
this.videos.push({
url: "/watch?v=" + video.videoId,
title: video.title,
uploaderName: video.uploaderName,
uploaderUrl: video.uploaderUrl,
duration: video.duration,
thumbnail: video.thumbnail,
watchedAt: video.watchedAt,
watched: true,
currentTime: video.currentTime,
});
if (this.videos.length < 1000) cursor.continue();
if (!this.shouldRemoveVideo(video)) {
this.videos.push({
url: "/watch?v=" + video.videoId,
title: video.title,
uploaderName: video.uploaderName,
uploaderUrl: video.uploaderUrl,
duration: video.duration,
thumbnail: video.thumbnail,
watchedAt: video.watchedAt,
watched: true,
currentTime: video.currentTime,
});
} else {
store.delete(video.videoId);
}
}
};
}
@ -89,6 +117,16 @@ export default {
};
this.download(JSON.stringify(json), `piped_history_${dateStr}.json`, "application/json");
},
onChange() {
this.setPreference("autoDeleteWatchHistory", this.autoDeleteHistory);
this.setPreference("autoDeleteWatchHistoryDelayHours", this.autoDeleteDelayHours);
},
shouldRemoveVideo(video) {
if (!this.autoDeleteHistory) return false;
// convert from hours to milliseconds
let maximumTimeDiff = Number(this.autoDeleteDelayHours) * 60 * 60 * 1000;
return Date.now() - video.watchedAt > maximumTimeDiff;
},
},
};
</script>

View file

@ -105,10 +105,14 @@ export default {
}
// FreeTube DB
else if (text.indexOf("allChannels") != -1) {
const json = JSON.parse(text);
json.subscriptions.forEach(item => {
this.subscriptions.push(item.id);
});
const lines = text.split("\n");
for (let line of lines) {
if (line === "") continue;
const json = JSON.parse(line);
json.subscriptions.forEach(item => {
this.subscriptions.push(item.id);
});
}
}
// Google Takeout JSON
else if (text.indexOf("contentDetails") != -1) {

View file

@ -1,5 +1,5 @@
<template>
<div v-if="!showContent" class="flex min-h-[75vh] w-full justify-center items-center">
<div v-if="!showContent" class="min-h-[75vh] w-full flex items-center justify-center">
<span id="spinner" />
</div>
<div v-else>
@ -7,6 +7,17 @@
</div>
</template>
<script>
export default {
props: {
showContent: {
type: Boolean,
required: true,
},
},
};
</script>
<style>
#spinner:after {
--spinner-color: #000;
@ -42,14 +53,3 @@
}
}
</style>
<script>
export default {
props: {
showContent: {
type: Boolean,
required: true,
},
},
};
</script>

View file

@ -1,8 +1,8 @@
<template>
<h1 v-t="'titles.login'" class="font-bold text-center my-4" />
<h1 v-t="'titles.login'" class="my-4 text-center font-bold" />
<hr />
<div class="text-center">
<form class="children:pb-3">
<div class="w-full flex items-center justify-center text-center">
<form class="w-min children:pb-3">
<div>
<input
v-model="username"
@ -11,7 +11,7 @@
autocomplete="username"
:placeholder="$t('login.username')"
:aria-label="$t('login.username')"
v-on:keyup.enter="login"
@keyup.enter="login"
/>
</div>
<div>
@ -22,23 +22,29 @@
autocomplete="password"
:placeholder="$t('login.password')"
:aria-label="$t('login.password')"
v-on:keyup.enter="login"
@keyup.enter="login"
/>
</div>
<div>
<a class="btn w-auto" @click="login" v-t="'titles.login'" />
<a v-t="'titles.login'" class="btn w-auto" @click="login" />
</div>
<ul class="md:flex-1 md:justify-center md:flex">
<li v-for="provider in oidcProviders" :key="provider.name">
<a class="btn w-auto" :href="provider.authUri">Log in with {{ provider.name }}</a>
</li>
</ul>
<TooltipIcon icon="i-fa6-solid:circle-info" :tooltip="$t('info.login_note')" />
</form>
</div>
</template>
<script>
import TooltipIcon from "./TooltipIcon.vue";
export default {
components: {
TooltipIcon,
},
data() {
return {
username: null,

View file

@ -0,0 +1 @@

View file

@ -11,6 +11,7 @@
<script>
export default {
emits: ["close"],
mounted() {
window.addEventListener("keydown", this.handleKeyDown);
},

View file

@ -1,23 +1,23 @@
<template>
<nav class="flex flex-wrap items-center justify-center px-2 sm:px-4 pb-2.5 w-full relative">
<div class="flex-1 flex justify-start">
<router-link class="flex font-bold text-3xl items-center font-sans" to="/"
<nav class="relative w-full flex flex-wrap items-center justify-center px-2 pb-2.5 sm:px-4">
<div class="flex flex-1 justify-start">
<router-link class="flex items-center text-3xl font-bold font-sans" :to="homePagePath"
><img
alt="logo"
src="/img/icons/logo.svg"
height="32"
width="32"
class="w-10 mr-[-0.6rem]"
class="mr-[-0.6rem] w-10"
/>iped</router-link
>
</div>
<div class="lt-md:hidden search-container">
<div class="search-container lt-md:hidden">
<input
ref="videoSearch"
v-model="searchText"
class="input w-72 h-10 pr-20"
class="input h-10 w-72 pr-20"
type="text"
role="search"
ref="videoSearch"
:title="$t('actions.search')"
:placeholder="$t('actions.search')"
@keyup="onKeyUp"
@ -27,14 +27,17 @@
/>
<span v-if="searchText" class="delete-search" @click="searchText = ''"></span>
</div>
<button id="search-btn" class="input btn mx-1 h-10" @click="onSearchClick">
<div class="i-fa6-solid:magnifying-glass"></div>
</button>
<!-- three vertical lines for toggling the hamburger menu on mobile -->
<button class="md:hidden flex flex-col justify-end mr-3" @click="showTopNav = !showTopNav">
<button class="mr-3 flex flex-col justify-end md:hidden" @click="showTopNav = !showTopNav">
<span class="line"></span>
<span class="line"></span>
<span class="line"></span>
</button>
<!-- navigation bar for large screen devices -->
<ul class="hidden md:(flex-1 flex justify-end flex text-1xl children:pl-3)">
<ul class="md:text-1xl hidden md:(flex flex flex-1 justify-end children:pl-3)">
<li v-if="shouldShowTrending">
<router-link v-t="'titles.trending'" to="/trending" />
</li>
@ -44,7 +47,7 @@
<li v-if="shouldShowLogin">
<router-link v-t="'titles.login'" to="/login" />
</li>
<li v-if="shouldShowLogin">
<li v-if="shouldShowRegister">
<router-link v-t="'titles.register'" to="/register" />
</li>
<li v-if="shouldShowHistory">
@ -61,7 +64,7 @@
<!-- navigation bar for mobile devices -->
<div
v-if="showTopNav"
class="mobile-nav flex flex-col mb-4 children:(p-1 w-full border-b border-dark-100 flex items-center gap-1)"
class="mobile-nav mb-4 flex flex-col children:(w-full flex items-center gap-1 border-b border-dark-100 p-1)"
>
<router-link v-if="shouldShowTrending" to="/trending">
<div class="i-fa6-solid:fire"></div>
@ -93,7 +96,7 @@
</router-link>
</div>
<!-- search suggestions for mobile devices -->
<div class="w-full mb-2 md:hidden search-container">
<div class="search-container mb-2 w-full md:hidden">
<input
v-model="searchText"
class="input h-10 w-full"
@ -128,17 +131,17 @@ export default {
searchText: "",
suggestionsVisible: false,
showTopNav: false,
homePagePath: "/",
registrationDisabled: false,
};
},
mounted() {
const query = new URLSearchParams(window.location.search).get("search_query");
if (query) this.onSearchTextChange(query);
this.focusOnSearchBar();
},
computed: {
shouldShowLogin(_this) {
return _this.getAuthToken() == null;
},
shouldShowRegister(_this) {
return _this.registrationDisabled == false ? _this.shouldShowLogin : false;
},
shouldShowHistory(_this) {
return _this.getPreferenceBoolean("watchHistory", false);
},
@ -149,6 +152,13 @@ export default {
return _this.getPreferenceBoolean("searchHistory", false) && localStorage.getItem("search_history");
},
},
mounted() {
this.fetchAuthConfig();
const query = new URLSearchParams(window.location.search).get("search_query");
if (query) this.onSearchTextChange(query);
this.focusOnSearchBar();
this.homePagePath = this.getHomePage(this);
},
methods: {
// focus on search bar when Ctrl+k is pressed
focusOnSearchBar() {
@ -165,12 +175,7 @@ export default {
},
onKeyPress(e) {
if (e.key === "Enter") {
e.target.blur();
this.$router.push({
name: "SearchResults",
query: { search_query: this.searchText },
});
return;
this.submitSearch(e);
}
},
onInputFocus() {
@ -183,6 +188,22 @@ export default {
onSearchTextChange(searchText) {
this.searchText = searchText;
},
async fetchAuthConfig() {
this.fetchJson(this.authApiUrl() + "/config").then(config => {
this.registrationDisabled = config?.registrationDisabled === true;
});
},
onSearchClick(e) {
this.submitSearch(e);
},
submitSearch(e) {
e.target.blur();
this.$router.push({
name: "SearchResults",
query: { search_query: this.searchText },
});
return;
},
},
};
</script>
@ -192,10 +213,15 @@ export default {
@apply relative inline-flex items-center;
}
.delete-search {
@apply absolute right-3 cursor-pointer rounded-full bg-[#ccc] w-4 h-4 text-center text-black opacity-50 hover:(opacity-70) text-size-[13px];
line-height: 1.05;
@apply absolute right-3 cursor-pointer rounded-full bg-[#ccc] w-4 h-4 text-center text-black opacity-50 hover:(opacity-70) text-size-[10px];
}
.mobile-nav div {
@apply mx-1;
}
@media screen and (max-width: 848px) {
#search-btn {
display: none;
}
}
</style>

View file

@ -1,7 +1,7 @@
<template>
<div class="flex flex-col justify-center items-center min-h-[88vh]">
<div class="min-h-[88vh] flex flex-col items-center justify-center">
<h1 class="font-bold !text-9xl">404</h1>
<h2 class="!text-2xl" v-t="'info.page_not_found'" />
<a class="btn mt-16" href="/" v-t="'actions.back_to_home'" />
<h2 v-t="'info.page_not_found'" class="!text-2xl" />
<a v-t="'actions.back_to_home'" class="btn mt-16" href="/" />
</div>
</template>

View file

@ -1,15 +1,16 @@
<template>
<ModalComponent>
<span class="text-2xl w-max inline-block" v-t="'actions.select_playlist'" />
<select class="select w-full mt-3" v-model="selectedPlaylist">
<option v-for="playlist in playlists" :value="playlist.id" :key="playlist.id" v-text="playlist.name" />
<ModalComponent @close="$emit('close')">
<span v-t="'actions.select_playlist'" class="inline-block w-max text-2xl" />
<select v-model="selectedPlaylist" class="select mt-3 w-full">
<option v-for="playlist in playlists" :key="playlist.id" :value="playlist.id" v-text="playlist.name" />
</select>
<div class="flex justify-end mt-3">
<div class="mt-3 w-full flex justify-between">
<button ref="addButton" v-t="'actions.create_playlist'" class="btn" @click="onCreatePlaylist" />
<button
class="btn"
@click="handleClick(selectedPlaylist)"
ref="addButton"
v-t="'actions.add_to_playlist'"
class="btn"
@click="handleClick(selectedPlaylist)"
/>
</div>
</ModalComponent>
@ -23,11 +24,16 @@ export default {
ModalComponent,
},
props: {
videoInfo: {
type: Object,
required: true,
},
videoId: {
type: String,
required: true,
},
},
emits: ["close"],
data() {
return {
playlists: [],
@ -62,31 +68,25 @@ export default {
this.$refs.addButton.disabled = true;
this.processing = true;
this.fetchJson(this.authApiUrl() + "/user/playlists/add", null, {
method: "POST",
body: JSON.stringify({
playlistId: playlistId,
videoId: this.videoId,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
}).then(json => {
this.addVideosToPlaylist(playlistId, [this.videoId], [this.videoInfo]).then(json => {
this.setPreference("selectedPlaylist" + this.hashCode(this.authApiUrl()), playlistId);
this.$emit("close");
if (json.error) alert(json.error);
});
},
async fetchPlaylists() {
this.fetchJson(this.authApiUrl() + "/user/playlists", null, {
headers: {
Authorization: this.getAuthToken(),
},
}).then(json => {
this.getPlaylists().then(json => {
this.playlists = json;
});
},
onCreatePlaylist() {
const name = prompt(this.$t("actions.create_playlist"));
if (!name) return;
this.createPlaylist(name).then(json => {
if (json.error) alert(json.error);
else this.fetchPlaylists();
});
},
},
};
</script>

View file

@ -1,12 +1,12 @@
<template>
<div>
<div class="flex flex-col flex-justify-between">
<router-link :to="props.item.url">
<div class="relative">
<img class="w-full" :src="props.item.thumbnail" loading="lazy" />
</div>
<p>
<span v-text="props.item.name" />
<font-awesome-icon class="ml-1.5" v-if="props.item.verified" icon="check" />
<font-awesome-icon v-if="props.item.verified" class="ml-1.5" icon="check" />
</p>
</router-link>
<p v-if="props.item.description" v-text="props.item.description" />
@ -14,7 +14,7 @@
<router-link v-if="props.item.uploaderUrl" class="link" :to="props.item.uploaderUrl">
<p>
<span v-text="props.item.uploaderName" />
<font-awesome-icon class="ml-1.5" v-if="props.item.uploaderVerified" icon="check" />
<font-awesome-icon v-if="props.item.uploaderVerified" class="ml-1.5" icon="check" />
</p>
</router-link>
<a v-else-if="props.item.uploaderName" class="link" v-text="props.item.uploaderName" />
@ -30,6 +30,9 @@
<script setup>
const props = defineProps({
item: Object,
item: {
type: Object,
required: true,
},
});
</script>

View file

@ -1,12 +1,12 @@
<template>
<ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error" />
<LoadingIndicatorPage :show-content="playlist" v-show="!playlist?.error">
<h1 class="ml-1 mb-1 mt-4 text-3xl!" v-text="playlist.name" />
<LoadingIndicatorPage v-show="!playlist?.error" :show-content="playlist">
<h1 class="mb-1 ml-1 mt-4 text-3xl!" v-text="playlist.name" />
<CollapsableText :text="playlist.description" />
<CollapsableText v-if="playlist?.description" :text="playlist.description" />
<div class="flex justify-between items-center mt-1">
<div class="mt-1 flex items-center justify-between">
<div>
<router-link class="link flex items-center gap-3" :to="playlist.uploaderUrl || '/'">
<img :src="playlist.uploaderAvatar" loading="lazy" class="rounded-full" />
@ -14,12 +14,12 @@
</router-link>
</div>
<div>
<strong v-text="`${playlist.videos} ${$t('video.videos')}`" class="mr-2" />
<button class="btn mx-1" v-if="!isPipedPlaylist" @click="bookmarkPlaylist">
<strong class="mr-2" v-text="`${playlist.videos} ${$t('video.videos')}`" />
<button v-if="!isPipedPlaylist" class="btn mx-1" @click="bookmarkPlaylist">
{{ $t(`actions.${isBookmarked ? "playlist_bookmarked" : "bookmark_playlist"}`)
}}<font-awesome-icon class="ml-3" icon="bookmark" />
</button>
<button class="btn mr-1" v-if="authenticated && !isPipedPlaylist" @click="clonePlaylist">
<button v-if="authenticated && !isPipedPlaylist" class="btn mr-1" @click="clonePlaylist">
{{ $t("actions.clone_playlist") }}<font-awesome-icon class="ml-3" icon="clone" />
</button>
<button class="btn mr-1" @click="downloadPlaylistAsTxt">
@ -28,7 +28,7 @@
<a class="btn mr-1" :href="getRssUrl">
<font-awesome-icon icon="rss" />
</a>
<WatchOnButton :link="`https://www.youtube.com/playlist?list=${this.$route.query.list}`" />
<WatchOnButton :link="`https://www.youtube.com/playlist?list=${$route.query.list}`" />
</div>
</div>
@ -42,9 +42,9 @@
:index="index"
:playlist-id="$route.query.list"
:admin="admin"
@remove="removeVideo(index)"
height="94"
width="168"
@remove="removeVideo(index)"
/>
</div>
</LoadingIndicatorPage>
@ -86,14 +86,11 @@ export default {
mounted() {
const playlistId = this.$route.query.list;
if (this.authenticated && playlistId?.length == 36)
this.fetchJson(this.authApiUrl() + "/user/playlists", null, {
headers: {
Authorization: this.getAuthToken(),
},
}).then(json => {
this.getPlaylists().then(json => {
if (json.error) alert(json.error);
else if (json.some(playlist => playlist.id === playlistId)) this.admin = true;
});
else if (playlistId.startsWith("local")) this.admin = true;
this.isPlaylistBookmarked();
},
activated() {
@ -106,6 +103,11 @@ export default {
},
methods: {
async fetchPlaylist() {
const playlistId = this.$route.query.list;
if (playlistId.startsWith("local")) {
return this.getPlaylist(playlistId);
}
return await await this.fetchJson(this.authApiUrl() + "/playlists/" + this.$route.query.list);
},
async getPlaylistData() {
@ -114,6 +116,7 @@ export default {
.then(() => {
this.updateTitle();
this.updateWatched(this.playlist.relatedStreams);
this.fetchDeArrowContent(this.playlist.relatedStreams);
});
},
async updateTitle() {
@ -130,6 +133,7 @@ export default {
this.playlist.nextpage = json.nextpage;
this.loading = false;
json.relatedStreams.map(stream => this.playlist.relatedStreams.push(stream));
this.fetchDeArrowContent(this.playlist.relatedStreams);
});
}
},

View file

@ -1,5 +1,5 @@
<template>
<div class="overflow-x-scroll h-screen-sm" ref="scrollable">
<div ref="scrollable" class="h-screen-sm overflow-x-scroll">
<VideoItem
v-for="(related, index) in playlist.relatedStreams"
:key="related.url"
@ -28,6 +28,18 @@ export default {
},
selectedIndex: {
type: Number,
required: true,
},
},
watch: {
playlist: {
handler() {
if (this.selectedIndex - 1 < this.playlist.relatedStreams.length)
nextTick(() => {
this.updateScroll();
});
},
deep: true,
},
},
mounted() {
@ -43,16 +55,5 @@ export default {
elems[this.selectedIndex - 1].offsetTop - this.$refs.scrollable.offsetTop;
},
},
watch: {
playlist: {
handler() {
if (this.selectedIndex - 1 < this.playlist.relatedStreams.length)
nextTick(() => {
this.updateScroll();
});
},
deep: true,
},
},
};
</script>

View file

@ -1,24 +1,19 @@
<template>
<h2 v-if="authenticated" class="font-bold my-4" v-t="'titles.playlists'" />
<h2 v-t="'titles.playlists'" class="my-4 font-bold" />
<div v-if="authenticated" class="flex justify-between mb-3">
<div class="mb-3 flex justify-between">
<button v-t="'actions.create_playlist'" class="btn" @click="onCreatePlaylist" />
<div class="flex">
<button
v-if="this.playlists.length > 0"
v-t="'actions.export_to_json'"
class="btn"
@click="exportPlaylists"
/>
<button v-if="playlists.length > 0" v-t="'actions.export_to_json'" class="btn" @click="exportPlaylists" />
<input
id="fileSelector"
ref="fileSelector"
type="file"
class="display-none"
@change="importPlaylists"
multiple="multiple"
@change="importPlaylists"
/>
<label for="fileSelector" v-t="'actions.import_from_json'" class="btn ml-2" />
<label v-t="'actions.import_from_json_csv'" for="fileSelector" class="btn ml-2" />
</div>
</div>
@ -34,42 +29,42 @@
</div>
<p
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
class="my-2 overflow-hidden flex link"
class="link my-2 flex overflow-hidden"
:title="playlist.name"
v-text="playlist.name"
/>
</router-link>
<button class="btn h-auto" @click="showPlaylistEditModal(playlist)" v-t="'actions.edit_playlist'" />
<button class="btn h-auto ml-2" @click="playlistToDelete = playlist.id" v-t="'actions.delete_playlist'" />
<button v-t="'actions.edit_playlist'" class="btn h-auto" @click="showPlaylistEditModal(playlist)" />
<button v-t="'actions.delete_playlist'" class="btn ml-2 h-auto" @click="playlistToDelete = playlist.id" />
<ModalComponent v-if="playlist.id == playlistToEdit" @close="playlistToEdit = null">
<div class="flex flex-col gap-2">
<h2 v-t="'actions.edit_playlist'" />
<input
v-model="newPlaylistName"
class="input"
type="text"
v-model="newPlaylistName"
:placeholder="$t('actions.playlist_name')"
/>
<input
v-model="newPlaylistDescription"
class="input"
type="text"
v-model="newPlaylistDescription"
:placeholder="$t('actions.playlist_description')"
/>
<button class="btn ml-auto" @click="editPlaylist(playlist)" v-t="'actions.okay'" />
<button v-t="'actions.okay'" class="btn ml-auto" @click="editPlaylist(playlist)" />
</div>
</ModalComponent>
<ConfirmModal
v-if="playlistToDelete == playlist.id"
:message="$t('actions.delete_playlist_confirm')"
@close="playlistToDelete = null"
@confirm="deletePlaylist(playlist.id)"
@confirm="onDeletePlaylist(playlist.id)"
/>
</div>
</div>
<hr />
<h2 class="font-bold my-4" v-t="'titles.bookmarks'" />
<h2 v-t="'titles.bookmarks'" class="my-4 font-bold" />
<div v-if="bookmarks" class="video-grid">
<router-link
@ -80,18 +75,18 @@
<img class="w-full" :src="playlist.thumbnail" alt="thumbnail" />
<div class="relative text-sm">
<span class="thumbnail-overlay thumbnail-right" v-text="`${playlist.videos} ${$t('video.videos')}`" />
<div class="absolute bottom-100px right-5px px-5px z-100" @click.prevent="removeBookmark(index)">
<div class="absolute bottom-100px right-5px z-100 px-5px" @click.prevent="removeBookmark(index)">
<font-awesome-icon class="ml-3" icon="bookmark" />
</div>
</div>
<p
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
class="my-2 overflow-hidden flex link"
class="link my-2 flex overflow-hidden"
:title="playlist.name"
v-text="playlist.name"
/>
<a :href="playlist.uploaderUrl" class="flex items-center">
<img class="rounded-full w-32px h-32px" :src="playlist.uploaderAvatar" />
<img class="h-32px w-32px rounded-full" :src="playlist.uploaderAvatar" />
<span class="ml-3 hover:underline" v-text="playlist.uploader" />
</a>
</router-link>
@ -104,6 +99,7 @@ import ConfirmModal from "./ConfirmModal.vue";
import ModalComponent from "./ModalComponent.vue";
export default {
components: { ConfirmModal, ModalComponent },
data() {
return {
playlists: [],
@ -115,7 +111,7 @@ export default {
};
},
mounted() {
if (this.authenticated) this.fetchPlaylists();
this.fetchPlaylists();
this.loadPlaylistBookmarks();
},
activated() {
@ -123,11 +119,7 @@ export default {
},
methods: {
fetchPlaylists() {
this.fetchJson(this.authApiUrl() + "/user/playlists", null, {
headers: {
Authorization: this.getAuthToken(),
},
}).then(json => {
this.getPlaylists().then(json => {
this.playlists = json;
});
},
@ -141,50 +133,21 @@ export default {
const newName = this.newPlaylistName;
const newDescription = this.newPlaylistDescription;
if (newName != selectedPlaylist.name) {
this.fetchJson(this.authApiUrl() + "/user/playlists/rename", null, {
method: "POST",
body: JSON.stringify({
playlistId: selectedPlaylist.id,
newName: newName,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
}).then(json => {
this.renamePlaylist(selectedPlaylist.id, newName).then(json => {
if (json.error) alert(json.error);
else selectedPlaylist.name = newName;
});
}
if (newDescription != selectedPlaylist.description) {
this.fetchJson(this.authApiUrl() + "/user/playlists/description", null, {
method: "PATCH",
body: JSON.stringify({
playlistId: selectedPlaylist.id,
description: newDescription,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
}).then(json => {
this.changePlaylistDescription(selectedPlaylist.id, newDescription).then(json => {
if (json.error) alert(json.error);
else selectedPlaylist.description = newDescription;
});
}
this.playlistToEdit = null;
},
deletePlaylist(id) {
this.fetchJson(this.authApiUrl() + "/user/playlists/delete", null, {
method: "POST",
body: JSON.stringify({
playlistId: id,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
}).then(json => {
onDeletePlaylist(id) {
this.deletePlaylist(id).then(json => {
if (json.error) alert(json.error);
else this.playlists = this.playlists.filter(playlist => playlist.id !== id);
});
@ -198,19 +161,6 @@ export default {
else this.fetchPlaylists();
});
},
async createPlaylist(name) {
let json = await this.fetchJson(this.authApiUrl() + "/user/playlists/create", null, {
method: "POST",
body: JSON.stringify({
name: name,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
});
return json;
},
async exportPlaylists() {
if (!this.playlists) return;
let json = {
@ -223,8 +173,8 @@ export default {
this.download(JSON.stringify(json), "playlists.json", "application/json");
},
async fetchPlaylistJson(playlistId) {
let playlist = await this.fetchJson(this.authApiUrl() + "/playlists/" + playlistId);
let playlistJson = {
let playlist = await this.getPlaylist(playlistId);
return {
name: playlist.name,
// possible other types: history, watch later, ...
type: "playlist",
@ -233,7 +183,6 @@ export default {
// list of the videos, starting with "https://youtube.com" to clarify that those are YT videos
videos: playlist.relatedStreams.map(stream => "https://youtube.com" + stream.url),
};
return playlistJson;
},
async importPlaylists() {
const files = this.$refs.fileSelector.files;
@ -246,26 +195,28 @@ export default {
let text = await file.text();
let tasks = [];
// list of playlists exported from Piped
if (text.includes("playlists")) {
if (file.name.slice(-4).toLowerCase() == ".csv") {
const lines = text.split("\n");
const playlistName = lines[1].split(",")[4];
const playlist = {
name: playlistName != "" ? playlistName : new Date().toJSON(),
videos: lines
.slice(4, lines.length)
.filter(line => line != "")
.slice(1)
.map(line => `https://youtube.com/watch?v=${line.split(",")[0]}`),
};
tasks.push(this.createPlaylistWithVideos(playlist));
} else if (text.includes('"Piped"')) {
// CSV from Google Takeout
let playlists = JSON.parse(text).playlists;
if (!playlists.length) {
alert(this.$t("actions.no_valid_playlists"));
return;
}
for (var i = 0; i < playlists.length; i++) {
tasks.push(this.createPlaylistWithVideos(playlists[i]));
for (let playlist of playlists) {
tasks.push(this.createPlaylistWithVideos(playlist));
}
// CSV from Google Takeout
} else if (file.name.slice(-4).toLowerCase() == ".csv") {
const lines = text.split("\n");
const playlist = {
name: lines[1].split(",")[4],
videos: lines
.slice(4, lines.length)
.filter(line => line != "")
.map(line => `https://youtube.com/watch?v=${line.split(",")[0]}`),
};
tasks.push(this.createPlaylistWithVideos(playlist));
} else {
alert(this.$t("actions.no_valid_playlists"));
return;
@ -277,19 +228,6 @@ export default {
let videoIds = playlist.videos.map(url => url.substr(-11));
await this.addVideosToPlaylist(newPlaylist.playlistId, videoIds);
},
async addVideosToPlaylist(playlistId, videoIds) {
await this.fetchJson(this.authApiUrl() + "/user/playlists/add", null, {
method: "POST",
body: JSON.stringify({
playlistId: playlistId,
videoIds: videoIds,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
});
},
async loadPlaylistBookmarks() {
if (!window.db) return;
var tx = window.db.transaction("playlist_bookmarks", "readonly");
@ -298,8 +236,7 @@ export default {
cursorRequest.onsuccess = e => {
const cursor = e.target.result;
if (cursor) {
const bookmark = cursor.value;
this.bookmarks.push(bookmark);
this.bookmarks.push(cursor.value);
cursor.continue();
}
};
@ -311,6 +248,5 @@ export default {
this.bookmarks.splice(index, 1);
},
},
components: { ConfirmModal, ModalComponent },
};
</script>

View file

@ -1,10 +1,10 @@
<template>
<div class="flex">
<button @click="$router.go(-1) || $router.push('/')">
<font-awesome-icon icon="chevron-left" /><span class="ml-1.5" v-t="'actions.back'" />
<font-awesome-icon icon="chevron-left" /><span v-t="'actions.back'" class="ml-1.5" />
</button>
</div>
<h1 v-t="'titles.preferences'" class="font-bold text-center" />
<h1 v-t="'titles.preferences'" class="text-center font-bold" />
<hr />
<label for="ddlTheme" class="pref">
<strong v-t="'actions.theme'" />
@ -34,7 +34,7 @@
</select>
</label>
<h2 class="text-center" v-t="'titles.player'" />
<h2 v-t="'titles.player'" class="text-center" />
<label class="pref" for="chkAutoPlayVideo">
<strong v-t="'actions.autoplay_video'" />
<input
@ -127,7 +127,7 @@
/>
</label>
<!-- chapters layout on mobile -->
<label class="lg:invisible pref" for="chkMinimizeChapters">
<label class="pref lg:invisible" for="chkMinimizeChapters">
<strong v-t="'actions.chapters_layout_mobile'" />
<select id="ddlDefaultHomepage" v-model="mobileChapterLayout" class="select w-auto" @change="onChange($event)">
@ -184,7 +184,7 @@
<select
id="ddlEnabledCodecs"
v-model="enabledCodecs"
class="select w-auto h-auto"
class="select h-auto w-auto"
multiple
@change="onChange($event)"
>
@ -223,7 +223,7 @@
/>
</label>
<div v-if="sponsorBlock">
<label v-for="[name, item] in skipOptions" class="pref" :for="'ddlSkip_' + name" :key="name">
<label v-for="[name, item] in skipOptions" :key="name" class="pref" :for="'ddlSkip_' + name">
<strong v-t="item.label" />
<select :id="'ddlSkip_' + name" v-model="item.value" class="select w-auto" @change="onChange($event)">
<option v-t="'actions.no'" value="no" />
@ -252,7 +252,17 @@
/>
</label>
</div>
<h2 class="text-center" v-t="'titles.instance'" />
<h2 v-t="'titles.dearrow'" class="text-center" />
<p class="text-center">
<span v-t="'actions.uses_api_from'" /><a class="link" href="https://sponsor.ajay.app/">sponsor.ajay.app</a>
</p>
<label class="pref" for="chkDeArrow">
<strong v-t="'actions.enable_dearrow'" />
<input id="chkDeArrow" v-model="dearrow" class="checkbox" type="checkbox" @change="onChange($event)" />
</label>
<h2 v-t="'titles.instance'" class="text-center" />
<label class="pref" for="ddlInstanceSelection">
<strong v-text="`${$t('actions.instance_selection')}:`" />
<select id="ddlInstanceSelection" v-model="selectedInstance" class="select w-auto" @change="onChange($event)">
@ -295,8 +305,8 @@
<br />
<!-- options that are visible only when logged in -->
<div v-if="this.authenticated">
<h2 class="text-center" v-t="'titles.account'"></h2>
<div v-if="authenticated">
<h2 v-t="'titles.account'" class="text-center"></h2>
<label class="pref" for="txtDeleteAccountPassword">
<strong v-t="'actions.delete_account'" />
<div class="flex items-center">
@ -304,22 +314,22 @@
id="txtDeleteAccountPassword"
ref="txtDeleteAccountPassword"
v-model="password"
v-on:keyup.enter="deleteAccount"
:placeholder="$t('login.password')"
:aria-label="$t('login.password')"
class="input w-auto mr-2"
class="input mr-2 w-auto"
type="password"
@keyup.enter="deleteAccount"
/>
<a class="btn w-auto" @click="deleteAccount" v-t="'actions.delete_account'" />
<a v-t="'actions.delete_account'" class="btn w-auto" @click="deleteAccount" />
</div>
</label>
<div class="pref">
<a class="btn w-auto" @click="logout" v-t="'actions.logout'" />
<a v-t="'actions.logout'" class="btn w-auto" @click="logout" />
<a
v-t="'actions.invalidate_session'"
class="btn w-auto"
style="margin-left: 0.5em"
@click="invalidateSession"
v-t="'actions.invalidate_session'"
/>
</div>
<br />
@ -332,7 +342,7 @@
<th v-t="'preferences.instance_locations'" />
<th v-t="'preferences.has_cdn'" />
<th v-t="'preferences.registered_users'" />
<th class="lt-md:hidden" v-t="'preferences.version'" />
<th v-t="'preferences.version'" class="lt-md:hidden" />
<th v-t="'preferences.up_to_date'" />
<th v-t="'preferences.ssl_score'" />
</tr>
@ -346,7 +356,7 @@
<td class="lt-md:hidden" v-text="instance.version" />
<td v-text="`${instance.up_to_date ? '&#9989;' : '&#10060;'}`" />
<td>
<a :href="sslScore(instance.api_url)" target="_blank" v-t="'actions.view_ssl_score'" />
<a v-t="'actions.view_ssl_score'" :href="sslScore(instance.api_url)" target="_blank" />
</td>
</tr>
</tbody>
@ -354,15 +364,15 @@
<br />
<p v-t="'info.preferences_note'" />
<br />
<button class="btn" v-t="'actions.reset_preferences'" @click="showConfirmResetPrefsDialog = true" />
<button class="btn mx-4" v-t="'actions.backup_preferences'" @click="backupPreferences()" />
<label for="fileSelector" class="btn" v-t="'actions.restore_preferences'" @click="restorePreferences()" />
<input class="hidden" id="fileSelector" ref="fileSelector" type="file" @change="restorePreferences()" />
<button v-t="'actions.reset_preferences'" class="btn" @click="showConfirmResetPrefsDialog = true" />
<button v-t="'actions.backup_preferences'" class="btn mx-4" @click="backupPreferences()" />
<label v-t="'actions.restore_preferences'" for="fileSelector" class="btn" @click="restorePreferences()" />
<input id="fileSelector" ref="fileSelector" class="hidden" type="file" @change="restorePreferences()" />
<ConfirmModal
v-if="showConfirmResetPrefsDialog"
:message="$t('actions.confirm_reset_preferences')"
@close="showConfirmResetPrefsDialog = false"
@confirm="resetPreferences()"
:message="$t('actions.confirm_reset_preferences')"
/>
</template>
@ -370,6 +380,9 @@
import CountryMap from "@/utils/CountryMaps/en.json";
import ConfirmModal from "./ConfirmModal.vue";
export default {
components: {
ConfirmModal,
},
data() {
return {
mobileChapterLayout: "Vertical",
@ -391,6 +404,7 @@ export default {
]),
showMarkers: true,
minSegmentLength: 0,
dearrow: false,
selectedTheme: "dark",
autoPlayVideo: true,
autoDisplayCaptions: false,
@ -512,6 +526,7 @@ export default {
this.showMarkers = this.getPreferenceBoolean("showMarkers", true);
this.minSegmentLength = Math.max(this.getPreferenceNumber("minSegmentLength", 0), 0);
this.dearrow = this.getPreferenceBoolean("dearrow", false);
this.selectedTheme = this.getPreferenceString("theme", "dark");
this.autoPlayVideo = this.getPreferenceBoolean("playerAutoPlay", true);
this.autoDisplayCaptions = this.getPreferenceBoolean("autoDisplayCaptions", false);
@ -570,6 +585,9 @@ export default {
localStorage.setItem("showMarkers", this.showMarkers);
localStorage.setItem("minSegmentLength", this.minSegmentLength);
localStorage.setItem("dearrow", this.dearrow);
localStorage.setItem("theme", this.selectedTheme);
localStorage.setItem("playerAutoPlay", this.autoPlayVideo);
localStorage.setItem("autoDisplayCaptions", this.autoDisplayCaptions);
@ -657,9 +675,6 @@ export default {
});
},
},
components: {
ConfirmModal,
},
};
</script>

30
src/components/QrCode.vue Normal file
View file

@ -0,0 +1,30 @@
<template>
<canvas ref="qrCodeCanvas" class="mx-auto my-2" />
</template>
<script>
import QRCode from "qrcode";
export default {
props: {
text: {
type: String,
required: true,
},
},
watch: {
text() {
this.generateQrCode();
},
},
mounted() {
this.generateQrCode();
},
methods: {
generateQrCode() {
QRCode.toCanvas(this.$refs.qrCodeCanvas, this.text, error => {
if (error) console.error(error);
});
},
},
};
</script>

View file

@ -1,56 +1,79 @@
<template>
<h1 v-t="'titles.register'" class="font-bold text-center my-4" />
<h1 v-t="'titles.register'" class="my-4 text-center font-bold" />
<hr />
<div class="text-center">
<form class="children:pb-3">
<div class="flex flex-col items-center justify-center text-center">
<form class="w-max items-center px-3 children:pb-3">
<div>
<input
v-model="username"
class="input"
class="input w-full"
type="text"
autocomplete="username"
:placeholder="$t('login.username')"
:aria-label="$t('login.username')"
v-on:keyup.enter="register"
@keyup.enter="register"
/>
</div>
<div>
<div class="flex justify-center">
<input
v-model="password"
class="input"
type="password"
class="input w-full"
:type="showPassword ? 'text' : 'password'"
autocomplete="password"
:placeholder="$t('login.password')"
:aria-label="$t('login.password')"
v-on:keyup.enter="register"
@keyup.enter="register"
/>
<button type="button" class="btn ml-2" @click="showPassword = !showPassword">
<div class="i-fa6-solid:eye" />
</button>
</div>
<div class="flex justify-center">
<input
v-model="passwordConfirm"
class="input w-full"
:type="showConfirmPassword ? 'text' : 'password'"
autocomplete="password"
:placeholder="$t('login.password_confirm')"
:aria-label="$t('login.password_confirm')"
@keyup.enter="register"
/>
<button type="button" class="btn ml-2" @click="showConfirmPassword = !showConfirmPassword">
<div class="i-fa6-solid:eye" />
</button>
</div>
<div>
<a class="btn w-auto" @click="register" v-t="'titles.register'" />
<a v-t="'titles.register'" class="btn w-auto" @click="register" />
</div>
<TooltipIcon icon="i-fa6-solid:circle-info" :tooltip="$t('info.register_note')" />
</form>
</div>
<ConfirmModal
v-if="showUnsecureRegisterDialog"
:message="$t('info.register_no_email_note')"
@close="showUnsecureRegisterDialog = false"
@confirm="
forceUnsecureRegister = true;
showUnsecureRegisterDialog = false;
register();
"
:message="$t('info.register_no_email_note')"
/>
</template>
<script>
import { isEmail } from "../utils/Misc.js";
import ConfirmModal from "./ConfirmModal.vue";
import TooltipIcon from "./TooltipIcon.vue";
export default {
components: { ConfirmModal, TooltipIcon },
data() {
return {
username: null,
password: null,
passwordConfirm: null,
showPassword: false,
showConfirmPassword: false,
showUnsecureRegisterDialog: false,
forceUnsecureRegister: false,
};
@ -67,6 +90,10 @@ export default {
methods: {
register() {
if (!this.username || !this.password) return;
if (this.password != this.passwordConfirm) {
alert(this.$t("login.passwords_incorrect"));
return;
}
if (isEmail(this.username) && !this.forceUnsecureRegister) {
this.showUnsecureRegisterDialog = true;
return;
@ -85,6 +112,5 @@ export default {
});
},
},
components: { ConfirmModal },
};
</script>

View file

@ -1,11 +1,11 @@
<template>
<h1 class="text-center my-2" v-text="$route.query.search_query" />
<h1 class="my-2 text-center" v-text="$route.query.search_query" />
<label for="ddlSearchFilters">
<strong v-text="`${$t('actions.filter')}:`" />
</label>
<select id="ddlSearchFilters" v-model="selectedFilter" default="all" class="select w-auto" @change="updateFilter()">
<option v-for="filter in availableFilters" :key="filter" :value="filter" v-t="`search.${filter}`" />
<option v-for="filter in availableFilters" :key="filter" v-t="`search.${filter}`" :value="filter" />
</select>
<hr />
@ -46,6 +46,7 @@ export default {
"music_videos",
"music_albums",
"music_playlists",
"music_artists",
],
selectedFilter: this.$route.query.filter ?? "all",
};

View file

@ -1,5 +1,5 @@
<template>
<div class="absolute suggestions-container">
<div class="suggestions-container absolute">
<ul>
<li
v-for="(suggestion, i) in searchSuggestions"

View file

@ -3,34 +3,45 @@
<h2 v-t="'actions.share'" />
<div class="flex justify-between">
<label v-t="'actions.piped_link'" />
<input type="checkbox" v-model="pipedLink" @change="onChange" />
<input v-model="pipedLink" type="checkbox" @change="onChange" />
</div>
<div v-if="this.hasPlaylist" class="flex justify-between">
<div v-if="hasPlaylist" class="flex justify-between">
<label v-t="'actions.with_playlist'" />
<input type="checkbox" v-model="withPlaylist" @change="onChange" />
<input v-model="withPlaylist" type="checkbox" @change="onChange" />
</div>
<div class="flex justify-between">
<label v-t="'actions.with_timecode'" for="withTimeCode" />
<input id="withTimeCode" type="checkbox" v-model="withTimeCode" @change="onChange" />
<input id="withTimeCode" v-model="withTimeCode" type="checkbox" @change="onChange" />
</div>
<div v-if="this.withTimeCode" class="flex justify-between mt-2">
<div v-if="withTimeCode" class="mt-2 flex items-center justify-between">
<label v-t="'actions.time_code'" />
<input class="input w-12" type="text" v-model="timeStamp" />
<input v-model="timeStamp" class="input w-12" type="text" @change="onChange" />
</div>
<a :href="generatedLink" target="_blank">
<h3 class="mt-4" v-text="generatedLink" />
</a>
<div class="flex justify-end mt-4">
<button class="btn" v-t="'actions.follow_link'" @click="followLink()" />
<button class="btn ml-3" v-t="'actions.copy_link'" @click="copyLink()" />
<QrCode v-if="showQrCode" :text="generatedLink" />
<div class="mt-4 flex justify-end">
<button v-t="'actions.generate_qrcode'" class="btn" @click="showQrCode = !showQrCode" />
<button v-t="'actions.follow_link'" class="btn ml-3" @click="followLink()" />
<button v-t="'actions.copy_link'" class="btn ml-3" @click="copyLink()" />
</div>
</ModalComponent>
</template>
<script setup>
import { defineAsyncComponent } from "vue";
const QrCode = defineAsyncComponent(() => import("./QrCode.vue"));
</script>
<script>
import ModalComponent from "./ModalComponent.vue";
export default {
components: {
ModalComponent,
},
props: {
videoId: {
type: String,
@ -42,14 +53,13 @@ export default {
},
playlistId: {
type: String,
default: undefined,
},
playlistIndex: {
type: Number,
default: undefined,
},
},
components: {
ModalComponent,
},
data() {
return {
withTimeCode: true,
@ -57,8 +67,23 @@ export default {
withPlaylist: true,
timeStamp: null,
hasPlaylist: false,
showQrCode: false,
};
},
computed: {
generatedLink() {
var baseUrl = this.pipedLink
? window.location.origin + "/watch?v=" + this.videoId
: "https://youtu.be/" + this.videoId;
var url = new URL(baseUrl);
if (this.withTimeCode && this.timeStamp > 0) url.searchParams.append("t", this.timeStamp);
if (this.hasPlaylist && this.withPlaylist) {
url.searchParams.append("list", this.playlistId);
url.searchParams.append("index", this.playlistIndex);
}
return url.href;
},
},
mounted() {
this.timeStamp = parseInt(this.currentTime);
this.withTimeCode = this.getPreferenceBoolean("shareWithTimeCode", true);
@ -87,19 +112,5 @@ export default {
this.setPreference("shareWithPlaylist", this.withPlaylist, true);
},
},
computed: {
generatedLink() {
var baseUrl = this.pipedLink
? window.location.origin + "/watch?v=" + this.videoId
: "https://youtu.be/" + this.videoId;
var url = new URL(baseUrl);
if (this.withTimeCode && this.timeStamp > 0) url.searchParams.append("t", this.timeStamp);
if (this.hasPlaylist && this.withPlaylist) {
url.searchParams.append("list", this.playlistId);
url.searchParams.append("index", this.playlistIndex);
}
return url.href;
},
},
};
</script>

View file

@ -1,7 +1,7 @@
<template>
<label for="ddlSortBy" v-t="'actions.sort_by'" />
<label v-t="'actions.sort_by'" for="ddlSortBy" />
<select id="ddlSortBy" v-model="selectedSort" class="select flex-grow">
<option v-for="(value, key) in options" v-t="`actions.${key}`" :key="key" :value="value" />
<option v-for="(value, key) in options" :key="key" v-t="`actions.${key}`" :value="value" />
</select>
</template>
@ -18,7 +18,10 @@ const options = {
const selectedSort = ref("descending");
const props = defineProps({
byKey: String,
byKey: {
type: String,
required: true,
},
});
const emit = defineEmits(["apply"]);

View file

@ -1,12 +1,30 @@
<template>
<h1 class="font-bold text-center my-4" v-t="'titles.subscriptions'" />
<h1 v-t="'titles.subscriptions'" class="my-4 text-center font-bold" />
<!-- import / export section -->
<div class="flex justify-between w-full">
<div class="flex">
<button class="btn mx-1">
<router-link to="/import" v-t="'actions.import_from_json'" />
<div class="w-full flex justify-between">
<div class="flex gap-2">
<button class="btn">
<router-link v-t="'actions.import_from_json_csv'" to="/import" />
</button>
<button class="btn" @click="exportHandler" v-t="'actions.export_to_json'" />
<button v-t="'actions.export_to_json'" class="btn" @click="exportHandler" />
<input
id="fileSelector"
ref="fileSelector"
type="file"
class="display-none"
multiple="multiple"
@change="importGroupsHandler"
/>
<label
for="fileSelector"
class="btn"
v-text="`${$t('actions.import_from_json')} (${$t('titles.channel_groups')})`"
/>
<button
class="btn"
@click="exportGroupsHandler"
v-text="`${$t('actions.export_to_json')} (${$t('titles.channel_groups')})`"
/>
</div>
<!-- subscriptions count, only shown if there are any -->
<i18n-t v-if="subscriptions.length > 0" keypath="subscriptions.subscribed_channels_count">{{
@ -18,10 +36,10 @@
<div class="w-full flex flex-wrap">
<button
v-for="group in channelGroups"
:key="group.groupName"
class="btn mx-1 w-max"
:class="{ selected: selectedGroup === group }"
:key="group.groupName"
@click="selectedGroup = group"
@click="selectGroup(group)"
>
<span v-text="group.groupName !== '' ? group.groupName : $t('video.all')" />
<div v-if="group.groupName != '' && selectedGroup == group">
@ -39,19 +57,19 @@
<div class="xl:grid xl:grid-cols-5 <md:flex-wrap">
<!-- channel info card -->
<div
class="col m-2 p-1 border rounded-lg border-gray-500"
v-for="subscription in filteredSubscriptions"
:key="subscription.url"
class="col m-2 border border-gray-500 rounded-lg p-1"
>
<router-link :to="subscription.url" class="flex p-2 font-bold text-4x4">
<img :src="subscription.avatar" class="rounded-full h-[fit-content]" width="48" height="48" />
<span class="self-center mx-2" v-text="subscription.name" />
<router-link :to="subscription.url" class="text-4x4 flex p-2 font-bold">
<img :src="subscription.avatar" class="h-[fit-content] rounded-full" width="48" height="48" />
<span class="mx-2 self-center" v-text="subscription.name" />
</router-link>
<!-- subscribe / unsubscribe btn -->
<button
class="btn w-full mt-2"
@click="handleButton(subscription)"
v-t="`actions.${subscription.subscribed ? 'unsubscribe' : 'subscribe'}`"
class="btn mt-2 w-full"
@click="handleButton(subscription)"
/>
</div>
</div>
@ -60,17 +78,23 @@
<ModalComponent v-if="showCreateGroupModal" @close="showCreateGroupModal = !showCreateGroupModal">
<h2 v-t="'actions.create_group'" />
<div class="flex flex-col">
<input class="input my-4" type="text" v-model="newGroupName" :placeholder="$t('actions.group_name')" />
<button class="ml-auto btn w-max" v-t="'actions.create_group'" @click="createGroup()" />
<input v-model="newGroupName" class="input my-4" type="text" :placeholder="$t('actions.group_name')" />
<button v-t="'actions.create_group'" class="btn ml-auto w-max" @click="createGroup()" />
</div>
</ModalComponent>
<ModalComponent v-if="showEditGroupModal" @close="showEditGroupModal = false">
<h2>{{ selectedGroup.groupName }}</h2>
<div class="flex flex-col mt-3 mb-2 overflow-y-scroll h-70">
<div class="mb-5 mt-3 flex justify-between">
<input v-model="editedGroupName" type="text" class="input" />
<button v-t="'actions.okay'" class="btn" :placeholder="$t('actions.group_name')" @click="editGroupName()" />
</div>
<div class="mb-2 mt-3 h-70 flex flex-col overflow-y-scroll">
<div v-for="subscription in subscriptions" :key="subscription.name">
<div class="flex justify-between mr-3">
<span>{{ subscription.name }}</span>
<div class="mr-3 flex items-center justify-between">
<a :href="subscription.url" target="_blank" class="flex items-center overflow-hidden">
<img :src="subscription.avatar" class="h-8 w-8 rounded-full" />
<span class="ml-2">{{ subscription.name }}</span>
</a>
<input
type="checkbox"
class="checkbox"
@ -88,6 +112,7 @@
import ModalComponent from "./ModalComponent.vue";
export default {
components: { ModalComponent },
data() {
return {
subscriptions: [],
@ -99,8 +124,16 @@ export default {
showCreateGroupModal: false,
showEditGroupModal: false,
newGroupName: "",
editedGroupName: "",
};
},
computed: {
filteredSubscriptions(_this) {
return _this.selectedGroup.groupName == ""
? _this.subscriptions
: _this.subscriptions.filter(channel => _this.selectedGroup.channels.includes(channel.url.substr(-11)));
},
},
mounted() {
this.fetchSubscriptions().then(json => {
this.subscriptions = json;
@ -110,18 +143,8 @@ export default {
this.channelGroups.push(this.selectedGroup);
if (!window.db) return;
const cursor = this.getChannelGroupsCursor();
cursor.onsuccess = e => {
const cursor = e.target.result;
if (cursor) {
const group = cursor.value;
this.channelGroups.push({
groupName: group.groupName,
channels: JSON.parse(group.channels),
});
cursor.continue();
}
};
this.loadChannelGroups();
},
activated() {
document.title = "Subscriptions - Piped";
@ -140,6 +163,10 @@ export default {
});
}
},
async loadChannelGroups() {
const groups = await this.getChannelGroups();
this.channelGroups.push(...groups);
},
handleButton(subscription) {
const channelId = subscription.url.split("/")[2];
if (this.authenticated) {
@ -174,6 +201,10 @@ export default {
});
this.download(json, "subscriptions.json", "application/json");
},
selectGroup(group) {
this.selectedGroup = group;
this.editedGroupName = group.groupName;
},
createGroup() {
if (!this.newGroupName || this.channelGroups.some(group => group.groupName == this.newGroupName)) return;
@ -188,6 +219,21 @@ export default {
this.showCreateGroupModal = false;
},
editGroupName() {
const oldGroupName = this.selectedGroup.groupName;
const newGroupName = this.editedGroupName;
// the group mustn't yet exist and the name can't be empty
if (!newGroupName || newGroupName == oldGroupName) return;
if (this.channelGroups.some(group => group.groupName == newGroupName)) return;
// create a new group with the same info and delete the old one
this.selectedGroup.groupName = newGroupName;
this.createOrUpdateChannelGroup(this.selectedGroup);
this.deleteChannelGroup(oldGroupName);
this.showEditGroupModal = false;
},
deleteGroup(group) {
this.deleteChannelGroup(group.groupName);
this.channelGroups = this.channelGroups.filter(g => g != group);
@ -200,15 +246,25 @@ export default {
: this.selectedGroup.channels.concat(channelId);
this.createOrUpdateChannelGroup(this.selectedGroup);
},
},
computed: {
filteredSubscriptions(_this) {
return _this.selectedGroup.groupName == ""
? _this.subscriptions
: _this.subscriptions.filter(channel => _this.selectedGroup.channels.includes(channel.url.substr(-11)));
async importGroupsHandler() {
const files = this.$refs.fileSelector.files;
for (let file of files) {
const groups = JSON.parse(await file.text()).groups;
for (let group of groups) {
this.createOrUpdateChannelGroup(group);
this.channelGroups.push(group);
}
}
},
exportGroupsHandler() {
const json = {
format: "Piped",
version: 1,
groups: this.channelGroups.slice(1),
};
this.download(JSON.stringify(json), "channel_groups.json", "application/json");
},
},
components: { ModalComponent },
};
</script>

View file

@ -1,12 +1,13 @@
<template>
<div class="toast">
<slot />
<button @click="dismiss" v-t="'actions.dismiss'" />
<button v-t="'actions.dismiss'" @click="dismiss" />
</div>
</template>
<script>
export default {
emits: ["dismissed"],
methods: {
dismiss() {
this.$emit("dismissed");

View file

@ -0,0 +1,29 @@
<template>
<div id="container" class="w-full">
<div :class="icon" class="cursor-pointer"></div>
<p id="tooltip" class="absolute mr-[20vw] mt-2 hidden rounded-l bg-gray-800 px-2 py-1 text-gray-200">
{{ tooltip }}
</p>
</div>
</template>
<script>
export default {
props: {
icon: {
type: String, // the class name of a font awesome icon
required: true,
},
tooltip: {
type: String,
required: true,
},
},
};
</script>
<style>
#container:hover #tooltip {
display: block;
}
</style>

View file

@ -1,5 +1,5 @@
<template>
<h1 v-t="'titles.trending'" class="font-bold text-center my-4" />
<h1 v-t="'titles.trending'" class="my-4 text-center font-bold" />
<hr />
@ -29,21 +29,15 @@ export default {
this.fetchTrending(region).then(videos => {
this.videos = videos;
this.updateWatched(this.videos);
this.fetchDeArrowContent(this.videos);
});
},
activated() {
document.title = this.$t("titles.trending") + " - Piped";
if (this.videos.length > 0) this.updateWatched(this.videos);
if (this.$route.path == "/") {
switch (this.getPreferenceString("homepage", "trending")) {
case "trending":
break;
case "feed":
this.$router.push("/feed");
return;
default:
break;
}
let homepage = this.getHomePage(this);
if (homepage !== undefined) this.$router.push(homepage);
}
},
methods: {

View file

@ -1,7 +1,7 @@
<template>
<div v-if="showVideo">
<div v-if="showVideo" class="flex flex-col flex-justify-between">
<router-link
class="focus:underline hover:underline inline-block w-full"
class="inline-block w-full focus:underline hover:underline"
:to="{
path: '/watch',
query: {
@ -13,17 +13,17 @@
>
<div class="w-full">
<img
class="w-full aspect-video object-contain"
:src="item.thumbnail"
:alt="item.title"
class="aspect-video w-full object-contain"
:src="thumbnail"
:alt="title"
:class="{ 'shorts-img': item.isShort, 'opacity-75': item.watched }"
loading="lazy"
/>
<!-- progress bar -->
<div class="relative w-full h-1">
<div class="relative h-1 w-full">
<div
class="absolute bottom-0 left-0 h-1 bg-red-600"
v-if="item.watched && item.duration > 0"
class="absolute bottom-0 left-0 h-1 bg-red-600"
:style="{ width: `clamp(0%, ${(item.currentTime / item.duration) * 100}%, 100%` }"
/>
</div>
@ -31,29 +31,29 @@
<div class="relative text-sm">
<span
class="thumbnail-overlay thumbnail-right"
v-if="item.duration > 0"
class="thumbnail-overlay thumbnail-right"
v-text="timeFormat(item.duration)"
/>
<!-- shorts thumbnail -->
<span class="thumbnail-overlay thumbnail-left" v-if="item.isShort" v-t="'video.shorts'" />
<span v-if="item.isShort" v-t="'video.shorts'" class="thumbnail-overlay thumbnail-left" />
<span
class="thumbnail-overlay thumbnail-right"
v-else-if="item.duration >= 0"
class="thumbnail-overlay thumbnail-right"
v-text="timeFormat(item.duration)"
/>
<i18n-t v-else keypath="video.live" class="thumbnail-overlay thumbnail-right !bg-red-600" tag="div">
<font-awesome-icon class="w-3" :icon="['fas', 'broadcast-tower']" />
</i18n-t>
<span v-if="item.watched" class="thumbnail-overlay bottom-5px left-5px" v-t="'video.watched'" />
<span v-if="item.watched" v-t="'video.watched'" class="thumbnail-overlay bottom-5px left-5px" />
</div>
<div>
<p
style="display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical"
class="pt-2 overflow-hidden flex link font-bold"
:title="item.title"
v-text="item.title"
class="link flex overflow-hidden pt-2 font-bold"
:title="title"
v-text="title"
/>
</div>
</router-link>
@ -64,24 +64,24 @@
v-if="item.uploaderAvatar"
:src="item.uploaderAvatar"
loading="lazy"
class="rounded-full mr-0.5 mt-0.5 w-32px h-32px"
class="mr-0.5 mt-0.5 h-32px w-32px rounded-full"
width="68"
height="68"
/>
</router-link>
<div class="px-2 flex-1">
<div class="flex-1 px-2">
<router-link
v-if="item.uploaderUrl && item.uploaderName && !hideChannel"
class="link-secondary overflow-hidden block text-sm"
class="link-secondary block overflow-hidden text-sm"
:to="item.uploaderUrl"
:title="item.uploaderName"
>
<span v-text="item.uploaderName" />
<font-awesome-icon class="ml-1.5" v-if="item.uploaderVerified" icon="check" />
<font-awesome-icon v-if="item.uploaderVerified" class="ml-1.5" icon="check" />
</router-link>
<div v-if="item.views >= 0 || item.uploadedDate" class="text-xs font-normal text-gray-300 mt-1">
<div v-if="item.views >= 0 || item.uploadedDate" class="mt-1 text-xs font-normal text-gray-300">
<span v-if="item.views >= 0">
<font-awesome-icon icon="eye" />
<span class="pl-1" v-text="`${numberFormat(item.views)} •`" />
@ -102,45 +102,45 @@
listen: '1',
},
}"
:aria-label="'Listen to ' + item.title"
:title="'Listen to ' + item.title"
:aria-label="'Listen to ' + title"
:title="'Listen to ' + title"
>
<font-awesome-icon icon="headphones" />
</router-link>
<button v-if="authenticated" :title="$t('actions.add_to_playlist')" @click="showModal = !showModal">
<button :title="$t('actions.add_to_playlist')" @click="showModal = !showModal">
<font-awesome-icon icon="circle-plus" />
</button>
<button
v-if="admin"
:title="$t('actions.remove_from_playlist')"
ref="removeButton"
:title="$t('actions.remove_from_playlist')"
@click="showConfirmRemove = true"
>
<font-awesome-icon icon="circle-minus" />
</button>
<ConfirmModal
v-if="showConfirmRemove"
:message="$t('actions.delete_playlist_video_confirm')"
@close="showConfirmRemove = false"
@confirm="removeVideo(item.url.substr(-11))"
:message="$t('actions.delete_playlist_video_confirm')"
/>
<PlaylistAddModal v-if="showModal" :video-id="item.url.substr(-11)" @close="showModal = !showModal" />
<PlaylistAddModal
v-if="showModal"
:video-id="item.url.substr(-11)"
:video-info="item"
@close="showModal = !showModal"
/>
</div>
</div>
</div>
</template>
<style>
.shorts-img {
@apply w-full object-contain;
}
</style>
<script>
import PlaylistAddModal from "./PlaylistAddModal.vue";
import ConfirmModal from "./ConfirmModal.vue";
export default {
components: { PlaylistAddModal, ConfirmModal },
props: {
item: {
type: Object,
@ -159,6 +159,7 @@ export default {
playlistId: { type: String, default: null },
admin: { type: Boolean, default: false },
},
emits: ["remove"],
data() {
return {
showModal: false,
@ -166,23 +167,21 @@ export default {
showConfirmRemove: false,
};
},
computed: {
title() {
return this.item.dearrow?.titles[0]?.title ?? this.item.title;
},
thumbnail() {
return this.item.dearrow?.thumbnails[0]?.thumbnail ?? this.item.thumbnail;
},
},
mounted() {
this.shouldShowVideo();
},
methods: {
removeVideo() {
this.$refs.removeButton.disabled = true;
this.fetchJson(this.authApiUrl() + "/user/playlists/remove", null, {
method: "POST",
body: JSON.stringify({
playlistId: this.playlistId,
index: this.index,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
}).then(json => {
this.removeVideoFromPlaylist(this.playlistId, this.index).then(json => {
if (json.error) alert(json.error);
else this.$emit("remove");
});
@ -201,6 +200,11 @@ export default {
};
},
},
components: { PlaylistAddModal, ConfirmModal },
};
</script>
<style>
.shorts-img {
@apply w-full object-contain;
}
</style>

View file

@ -2,11 +2,18 @@
<div
ref="container"
data-shaka-player-container
class="w-full max-h-screen flex justify-center"
class="relative max-h-screen w-full flex justify-center"
:class="{ 'player-container': !isEmbed }"
>
<video ref="videoEl" class="w-full" data-shaka-player :autoplay="shouldAutoPlay" :loop="selectedAutoLoop" />
<canvas id="preview" />
<span
id="preview-container"
ref="previewContainer"
class="absolute bottom-0 z-[2000] mb-[3.5%] hidden flex-col items-center"
>
<canvas id="preview" ref="preview" class="rounded-sm" />
<span class="mt-2 w-min rounded-xl bg-dark-700 px-2 pb-1 pt-1.5 text-sm" v-text="timeFormat(currentTime)" />
</span>
<button
v-if="inSegment"
class="skip-segment-button"
@ -18,6 +25,11 @@
<span v-t="'actions.skip_segment'" />
<i class="material-icons-round">skip_next</i>
</button>
<span
v-if="error > 0"
v-t="{ path: 'player.failed', args: [error] }"
class="absolute top-8 rounded bg-black/80 p-2 text-lg backdrop-blur-sm"
/>
</div>
</template>
@ -50,7 +62,7 @@ export default {
selectedAutoLoop: Boolean,
isEmbed: Boolean,
},
emits: ["timeupdate"],
emits: ["timeupdate", "ended", "navigateNext"],
data() {
return {
lastUpdate: new Date().getTime(),
@ -58,6 +70,9 @@ export default {
destroying: false,
inSegment: false,
isHoveringTimebar: false,
currentTime: 0,
seekbarPadding: 2,
error: 0,
};
},
computed: {
@ -182,7 +197,7 @@ export default {
e.preventDefault();
break;
case "shift+n":
self.navigateNext();
self.$emit("navigateNext");
e.preventDefault();
break;
case "shift+,":
@ -457,14 +472,7 @@ export default {
this.$ui = new shaka.ui.Overlay(localPlayer, this.$refs.container, videoEl);
const overflowMenuButtons = [
"quality",
"language",
"captions",
"picture_in_picture",
"playback_rate",
"airplay",
];
const overflowMenuButtons = ["quality", "captions", "picture_in_picture", "playback_rate", "airplay"];
if (this.isEmbed) {
overflowMenuButtons.push("open_new_tab");
@ -473,9 +481,9 @@ export default {
const config = {
overflowMenuButtons: overflowMenuButtons,
seekBarColors: {
base: "rgba(255, 255, 255, 0.3)",
buffered: "rgba(255, 255, 255, 0.54)",
played: "rgb(255, 0, 0)",
base: "var(--player-base)",
buffered: "var(--player-buffered)",
played: "var(--player-played)",
},
};
@ -501,6 +509,9 @@ export default {
manifest: {
disableVideo: disableVideo,
},
streaming: {
segmentPrefetchLimit: 10,
},
});
const quality = this.getPreferenceNumber("quality", 0);
@ -508,73 +519,101 @@ export default {
quality > 0 && (this.video.audioStreams.length > 0 || this.video.livestream) && !disableVideo;
if (qualityConds) this.$player.configure("abr.enabled", false);
player.load(uri, 0, mime).then(() => {
const isSafari = window.navigator?.vendor?.includes("Apple");
player
.load(uri, 0, mime)
.then(() => {
const isSafari = window.navigator?.vendor?.includes("Apple");
if (!isSafari) {
// Set the audio language
const prefLang = this.getPreferenceString("hl", "en").substr(0, 2);
var lang = "en";
for (var l in player.getAudioLanguages()) {
if (l == prefLang) {
lang = l;
return;
if (!isSafari) {
// Set the audio language
const prefLang = this.getPreferenceString("hl", "en").substr(0, 2);
var lang = "en";
for (var l in player.getAudioLanguages()) {
if (l == prefLang) {
lang = l;
return;
}
}
player.selectAudioLanguage(lang);
}
const audioLanguages = player.getAudioLanguages();
if (audioLanguages.length > 1) {
const overflowMenuButtons = this.$ui.getConfiguration().overflowMenuButtons;
// append language menu on index 1
const newOverflowMenuButtons = [
...overflowMenuButtons.slice(0, 1),
"language",
...overflowMenuButtons.slice(1),
];
this.$ui.configure("overflowMenuButtons", newOverflowMenuButtons);
}
if (qualityConds) {
var leastDiff = Number.MAX_VALUE;
var bestStream = null;
var bestAudio = 0;
const tracks = player
.getVariantTracks()
.filter(track => track.language == lang || track.language == "und");
// Choose the best audio stream
if (quality >= 480)
tracks.forEach(track => {
const audioBandwidth = track.audioBandwidth;
if (audioBandwidth > bestAudio) bestAudio = audioBandwidth;
});
// Find best matching stream based on resolution and bitrate
tracks
.sort((a, b) => a.bandwidth - b.bandwidth)
.forEach(stream => {
if (stream.audioBandwidth < bestAudio) return;
const diff = Math.abs(quality - stream.height);
if (diff < leastDiff) {
leastDiff = diff;
bestStream = stream;
}
});
player.selectVariantTrack(bestStream);
}
this.video.subtitles.map(subtitle => {
player.addTextTrackAsync(
subtitle.url,
subtitle.code,
"subtitles",
subtitle.mimeType,
null,
subtitle.name,
);
});
videoEl.volume = this.getPreferenceNumber("volume", 1);
const rate = this.getPreferenceNumber("rate", 1);
videoEl.playbackRate = rate;
videoEl.defaultPlaybackRate = rate;
const autoDisplayCaptions = this.getPreferenceBoolean("autoDisplayCaptions", false);
this.$player.setTextTrackVisibility(autoDisplayCaptions);
const prefSubtitles = this.getPreferenceString("subtitles", "");
if (prefSubtitles !== "") {
const textTracks = this.$player.getTextTracks();
const subtitleIdx = textTracks.findIndex(textTrack => textTrack.language == prefSubtitles);
if (subtitleIdx != -1) {
this.$player.setTextTrackVisibility(true);
this.$player.selectTextTrack(textTracks[subtitleIdx]);
}
}
player.selectAudioLanguage(lang);
}
if (qualityConds) {
var leastDiff = Number.MAX_VALUE;
var bestStream = null;
var bestAudio = 0;
const tracks = player
.getVariantTracks()
.filter(track => track.language == lang || track.language == "und");
// Choose the best audio stream
if (quality >= 480)
tracks.forEach(track => {
const audioBandwidth = track.audioBandwidth;
if (audioBandwidth > bestAudio) bestAudio = audioBandwidth;
});
// Find best matching stream based on resolution and bitrate
tracks
.sort((a, b) => a.bandwidth - b.bandwidth)
.forEach(stream => {
if (stream.audioBandwidth < bestAudio) return;
const diff = Math.abs(quality - stream.height);
if (diff < leastDiff) {
leastDiff = diff;
bestStream = stream;
}
});
player.selectVariantTrack(bestStream);
}
this.video.subtitles.map(subtitle => {
player.addTextTrackAsync(
subtitle.url,
subtitle.code,
"subtitles",
subtitle.mimeType,
null,
subtitle.name,
);
})
.catch(e => {
console.error(e);
this.error = e.code;
});
videoEl.volume = this.getPreferenceNumber("volume", 1);
const rate = this.getPreferenceNumber("rate", 1);
videoEl.playbackRate = rate;
videoEl.defaultPlaybackRate = rate;
const autoDisplayCaptions = this.getPreferenceBoolean("autoDisplayCaptions", false);
this.$player.setTextTrackVisibility(autoDisplayCaptions);
});
// expand the player to fullscreen when the fullscreen query equals true
if (this.$route.query.fullscreen === "true" && !this.$ui.getControls().isFullScreenEnabled())
@ -603,6 +642,7 @@ export default {
this.$refs.videoEl.currentTime = time;
}
},
updateMarkers() {
const markers = this.$refs.container.querySelector(".shaka-ad-markers");
const array = ["to right"];
@ -610,38 +650,19 @@ export default {
const start = (segment.segment[0] / this.video.duration) * 100;
const end = (segment.segment[1] / this.video.duration) * 100;
var color;
switch (segment.category) {
case "sponsor":
color = "#00d400";
break;
case "selfpromo":
color = "#ffff00";
break;
case "interaction":
color = "#cc00ff";
break;
case "poi_highlight":
color = "#ff1684";
break;
case "intro":
color = "#00ffff";
break;
case "outro":
color = "#0202ed";
break;
case "preview":
color = "#008fd6";
break;
case "filler":
color = "#7300FF";
break;
case "music_offtopic":
color = "#ff9900";
break;
default:
color = "white";
}
var color = [
"sponsor",
"selfpromo",
"interaction",
"poi_highlight",
"intro",
"outro",
"preview",
"filler",
"music_offtopic",
].includes(segment.category)
? `var(--spon-seg-${segment.category})`
: "var(--spon-seg-default)";
array.push(`transparent ${start}%`);
array.push(`${color} ${start}%`);
@ -656,11 +677,6 @@ export default {
if (markers) markers.style.background = `linear-gradient(${array.join(",")})`;
},
updateSponsors() {
const skipOptions = this.getPreferenceJSON("skipOptions", {});
this.sponsors?.segments?.forEach(segment => {
const option = skipOptions[segment.category];
segment.autoskip = option === undefined || option === "auto";
});
if (this.getPreferenceBoolean("showMarkers", true)) {
this.shakaPromise.then(() => {
this.updateMarkers();
@ -679,8 +695,7 @@ export default {
// hide the preview when the user stops hovering the seekbar
seekBar.addEventListener("mouseout", () => {
this.isHoveringTimebar = false;
let canvas = document.querySelector("#preview");
canvas.style.display = "none";
this.$refs.previewContainer.style.display = "none";
});
},
async showSeekbarPreview(position) {
@ -689,17 +704,12 @@ export default {
if (!this.isHoveringTimebar) return;
const seekBar = document.querySelector(".shaka-seek-bar");
const canvas = document.querySelector("#preview");
const container = this.$refs.previewContainer;
const canvas = this.$refs.preview;
const ctx = canvas.getContext("2d");
// get the new sizes for the image to be drawn into the canvas
const originalWidth = originalImage.naturalWidth;
const originalHeight = originalImage.naturalHeight;
// image can have less frames than server told us so calculate them ourselves
const imageFramesPerPageX = originalImage.naturalWidth / frame.frameWidth;
const imageFramesPerPageY = originalImage.naturalHeight / frame.frameHeight;
const offsetX = originalWidth * (frame.positionX / imageFramesPerPageX);
const offsetY = originalHeight * (frame.positionY / imageFramesPerPageY);
const offsetX = frame.positionX * frame.frameWidth;
const offsetY = frame.positionY * frame.frameHeight;
canvas.width = frame.frameWidth > 100 ? frame.frameWidth : frame.frameWidth * 2;
canvas.height = frame.frameWidth > 100 ? frame.frameHeight : frame.frameHeight * 2;
@ -717,12 +727,15 @@ export default {
);
// calculate the thumbnail preview offset and display it
const seekbarPadding = 2; // percentage of seekbar padding
const centerOffset = position / this.video.duration / 10;
const left = centerOffset - ((0.5 * canvas.width) / seekBar.clientWidth) * 100;
const maxLeft = ((seekBar.clientWidth - canvas.clientWidth) / seekBar.clientWidth) * 100 - seekbarPadding;
canvas.style.left = `max(${seekbarPadding}%, min(${left}%, ${maxLeft}%))`;
canvas.style.display = "block";
const maxLeft =
((seekBar.clientWidth - canvas.clientWidth) / seekBar.clientWidth) * 100 - this.seekbarPadding;
this.currentTime = position / 1000;
container.style.left = `max(${this.seekbarPadding}%, min(${left}%, ${maxLeft}%))`;
container.style.display = "flex";
},
// ineffective algorithm to find the thumbnail corresponding to the currently hovered position in the video
getFrame(position) {
@ -773,6 +786,23 @@ export default {
</script>
<style>
:root {
--player-base: rgba(255, 255, 255, 0.3);
--player-buffered: rgba(255, 255, 255, 0.54);
--player-played: rgba(255, 0, 0);
--spon-seg-sponsor: #00d400;
--spon-seg-selfpromo: #ffff00;
--spon-seg-interaction: #cc00ff;
--spon-seg-poi_highlight: #ff1684;
--spon-seg-intro: #00ffff;
--spon-seg-outro: #0202ed;
--spon-seg-preview: #008fd6;
--spon-seg-filler: #7300ff;
--spon-seg-music_offtopic: #ff9900;
--spon-seg-default: white;
}
.player-container {
@apply max-h-75vh min-h-64 bg-black;
}
@ -834,13 +864,4 @@ export default {
font-size: 1.6em !important;
line-height: inherit !important;
}
#preview {
position: absolute;
z-index: 2000;
bottom: 0;
margin-bottom: 4.5%;
border-radius: 0.3rem;
display: none;
}
</style>

View file

@ -1,7 +1,10 @@
<script>
export default {
props: {
link: String,
link: {
type: String,
required: true,
},
platform: {
type: String,
required: false,
@ -12,14 +15,14 @@ export default {
</script>
<template>
<template v-if="this.getPreferenceBoolean('showWatchOnYouTube', false)">
<template v-if="getPreferenceBoolean('showWatchOnYouTube', false)">
<!-- For large screens -->
<a :href="link" class="btn lt-lg:hidden flex items-center">
<a :href="link" class="btn flex items-center lt-lg:hidden">
<i18n-t keypath="player.watch_on" tag="strong">{{ platform }}</i18n-t>
<font-awesome-icon class="mx-1.5" :icon="['fab', platform.toLowerCase()]" />
</a>
<!-- For small screens -->
<a :href="link" class="btn lg:hidden flex items-center">
<a :href="link" class="btn flex items-center lg:hidden">
<font-awesome-icon class="mx-1.5" :icon="['fab', platform.toLowerCase()]" />
</a>
</template>

View file

@ -1,5 +1,5 @@
<template>
<div v-if="video && isEmbed" class="absolute top-0 left-0 h-full w-full bg-black z-50">
<div v-if="video && isEmbed" class="absolute left-0 top-0 z-50 h-full w-full bg-black">
<VideoPlayer
ref="videoPlayer"
:video="video"
@ -29,19 +29,20 @@
:selected-auto-loop="selectedAutoLoop"
@timeupdate="onTimeUpdate"
@ended="onVideoEnded"
@navigate-next="navigateNext"
/>
</keep-alive>
<ChaptersBar
:mobileLayout="isMobile"
v-if="video?.chapters?.length > 0 && showChapters"
:mobile-layout="isMobile"
:chapters="video.chapters"
:player-position="currentTime"
@seek="navigate"
/>
</div>
<!-- video title -->
<div class="font-bold mt-2 text-2xl break-words" v-text="video.title" />
<div class="flex flex-wrap mt-3 mb-3">
<div class="mt-2 break-words text-2xl font-bold" v-text="video.title" />
<div class="mb-3 mt-3 flex flex-wrap">
<!-- views / date -->
<div class="flex flex-auto gap-2">
<span v-t="{ path: 'video.views', args: { views: addCommas(video.views) } }" />
@ -76,9 +77,14 @@
video.uploader
}}</router-link>
<!-- Verified Badge -->
<font-awesome-icon class="ml-1" v-if="video.uploaderVerified" icon="check" />
<font-awesome-icon v-if="video.uploaderVerified" class="ml-1" icon="check" />
</div>
<PlaylistAddModal v-if="showModal" :video-id="getVideoId()" @close="showModal = !showModal" />
<PlaylistAddModal
v-if="showModal"
:video-id="getVideoId()"
:video-info="video"
@close="showModal = !showModal"
/>
<ShareModal
v-if="showShareModal"
:video-id="getVideoId()"
@ -87,26 +93,29 @@
:playlist-index="index"
@close="showShareModal = !showShareModal"
/>
<div class="flex flex-wrap gap-1 ml-auto">
<div class="ml-auto flex flex-wrap gap-1">
<!-- Subscribe Button -->
<button class="btn flex items-center" v-if="authenticated" @click="showModal = !showModal">
<button class="btn flex items-center gap-1 <md:hidden" @click="downloadCurrentFrame">
{{ $t("actions.download_frame") }}<i class="i-fa6-solid:download" />
</button>
<button class="btn flex items-center" @click="showModal = !showModal">
{{ $t("actions.add_to_playlist") }}<font-awesome-icon class="ml-1" icon="circle-plus" />
</button>
<button
class="btn"
@click="subscribeHandler"
v-t="{
path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`,
args: { count: numberFormat(video.uploaderSubscriberCount) },
}"
class="btn"
@click="subscribeHandler"
/>
<div class="flex flex-wrap gap-1">
<!-- RSS Feed button -->
<a
v-if="video.uploaderUrl"
aria-label="RSS feed"
title="RSS feed"
role="button"
v-if="video.uploaderUrl"
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${video.uploaderUrl.split('/')[2]}`"
target="_blank"
class="btn flex items-center"
@ -121,7 +130,11 @@
<!-- YouTube -->
<WatchOnButton :link="`https://youtu.be/${getVideoId()}`" />
<!-- Odysee -->
<WatchOnButton :link="`https://odysee.com/${video.lbryId}`" platform="Odysee" />
<WatchOnButton
v-if="video.lbryId"
:link="`https://odysee.com/${video.lbryId}`"
platform="Odysee"
/>
<!-- listen / watch toggle -->
<router-link
:to="toggleListenUrl"
@ -135,27 +148,53 @@
</div>
</div>
<hr />
<hr class="mb-2" />
<div
v-for="metaInfo in video?.metaInfo ?? []"
:key="metaInfo.title"
class="btn my-3 flex flex-wrap cursor-default gap-2 px-4 py-2"
>
<span>{{ metaInfo.description ?? metaInfo.title }}</span>
<a v-for="(link, linkIndex) in metaInfo.urls" :key="linkIndex" :href="link" class="underline">{{
metaInfo.urlTexts[linkIndex]
}}</a>
<br />
</div>
<button
v-t="`actions.${showDesc ? 'minimize_description' : 'show_description'}`"
class="btn mb-2"
@click="showDesc = !showDesc"
v-t="`actions.${showDesc ? 'minimize_description' : 'show_description'}`"
/>
<span class="btn ml-2" v-show="video?.chapters?.length > 0">
<input id="showChapters" type="checkbox" v-model="showChapters" />
<label class="ml-2" for="showChapters" v-t="'actions.show_chapters'" />
<span v-show="video?.chapters?.length > 0" class="btn ml-2">
<input id="showChapters" v-model="showChapters" type="checkbox" />
<label v-t="'actions.show_chapters'" class="ml-2" for="showChapters" />
</span>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-show="showDesc" class="break-words" v-html="purifyHTML(video.description)" />
<template v-if="showDesc">
<!-- eslint-disable-next-line vue/no-v-html -->
<div class="description break-words" v-html="purifiedDescription" />
<br />
<div
v-if="sponsors && sponsors.segments"
v-text="`${$t('video.sponsor_segments')}: ${sponsors.segments.length}`"
/>
<div v-if="video.category" v-text="`${$t('video.category')}: ${video.category}`" />
<div v-text="`${$t('video.license')}: ${video.license}`" />
<div class="capitalize" v-text="`${$t('video.visibility')}: ${video.visibility}`" />
<div v-if="video.tags" class="mt-2 flex flex-wrap gap-2">
<router-link
v-for="tag in video.tags"
:key="tag"
class="btn line-clamp-1 rounded-s px-2 py-1"
:to="`/results?search_query=${encodeURIComponent(tag)}`"
>{{ tag }}</router-link
>
</div>
</template>
</div>
@ -169,22 +208,27 @@
<hr />
<div class="grid xl:grid-cols-5 sm:grid-cols-4 grid-cols-1">
<div class="xl:col-span-4 sm:col-span-3">
<div class="grid grid-cols-1 sm:grid-cols-4 xl:grid-cols-5">
<div class="sm:col-span-3 xl:col-span-4">
<button
v-if="!comments?.disabled"
class="btn mb-2"
@click="toggleComments"
v-t="`actions.${showComments ? 'minimize_comments' : 'show_comments'}`"
v-text="
`${$t(showComments ? 'actions.minimize_comments' : 'actions.show_comments')} (${numberFormat(
comments?.commentCount,
)})`
"
/>
</div>
<div v-if="!showComments" class="xl:col-span-4 sm:col-span-3"></div>
<div v-else-if="!comments" class="xl:col-span-4 sm:col-span-3">
<p class="text-center mt-8" v-t="'comment.loading'"></p>
<div v-if="!showComments" class="sm:col-span-3 xl:col-span-4"></div>
<div v-else-if="!comments" class="sm:col-span-3 xl:col-span-4">
<p v-t="'comment.loading'" class="mt-8 text-center"></p>
</div>
<div v-else-if="comments.disabled" class="xl:col-span-4 sm:col-span-3">
<p class="text-center mt-8" v-t="'comment.disabled'"></p>
<div v-else-if="comments.disabled" class="sm:col-span-3 xl:col-span-4">
<p v-t="'comment.disabled'" class="mt-8 text-center"></p>
</div>
<div v-else ref="comments" class="xl:col-span-4 sm:col-span-3">
<div v-else ref="comments" class="sm:col-span-3 xl:col-span-4">
<CommentItem
v-for="comment in comments.comments"
:key="comment.commentId"
@ -202,9 +246,9 @@
:selected-index="index"
/>
<a
v-t="`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`"
class="btn mb-2"
@click="showRecs = !showRecs"
v-t="`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`"
/>
<hr v-show="showRecs" />
<div v-show="showRecs">
@ -235,6 +279,7 @@ import WatchOnButton from "./WatchOnButton.vue";
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
import ToastComponent from "./ToastComponent.vue";
import { parseTimeParam } from "@/utils/Misc";
import { purifyHTML, rewriteDescription } from "@/utils/HtmlUtils";
export default {
name: "App",
@ -302,6 +347,9 @@ export default {
defaultCounter(_this) {
return _this.getPreferenceNumber("autoPlayNextCountdown", 5);
},
purifiedDescription() {
return purifyHTML(this.video.description);
},
},
mounted() {
// check screen size
@ -350,6 +398,7 @@ export default {
this.getPlaylistData();
this.getSponsors();
if (!this.isEmbed && this.showComments) this.getComments();
if (this.isEmbed) document.querySelector("html").style.overflow = "hidden";
window.addEventListener("click", this.handleClick);
window.addEventListener("resize", () => {
this.smallView = this.smallViewQuery.matches;
@ -398,6 +447,11 @@ export default {
category: JSON.stringify(selectedSkip),
});
sponsors?.segments?.forEach(segment => {
const option = skipOptions[segment.category];
segment.autoskip = option === undefined || option === "auto";
});
const minSegmentLength = Math.max(this.getPreferenceNumber("minSegmentLength", 0), 0);
sponsors.segments = sponsors.segments?.filter(segment => {
const length = segment.segment[1] - segment.segment[0];
@ -437,8 +491,10 @@ export default {
elem.outerHTML = elem.getAttribute("href");
});
xmlDoc.querySelectorAll("br").forEach(elem => (elem.outerHTML = "\n"));
this.video.description = this.rewriteDescription(xmlDoc.querySelector("body").innerHTML);
this.video.description = rewriteDescription(xmlDoc.querySelector("body").innerHTML);
this.updateWatched(this.video.relatedStreams);
this.fetchDeArrowContent(this.video.relatedStreams);
}
});
},
@ -459,6 +515,9 @@ export default {
}
}
});
await this.fetchPlaylistPages().then(() => {
this.fetchDeArrowContent(this.playlist.relatedStreams);
});
}
},
async fetchPlaylistPages() {
@ -643,6 +702,21 @@ export default {
if (paramStr.length > 0) url += "&" + paramStr;
this.$router.push(url);
},
downloadCurrentFrame() {
const video = document.querySelector("video");
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext("2d");
context.drawImage(video, 0, 0, canvas.width, canvas.height);
let link = document.createElement("a");
const currentTime = Math.round(video.currentTime * 1000) / 1000;
link.download = `${this.video.title}_${currentTime}s.png`;
link.href = canvas.toDataURL();
link.click();
},
},
};
</script>
@ -653,4 +727,9 @@ export default {
opacity: 0;
transform: translateX(100%) scale(0.5);
}
.description a {
text-decoration: underline;
filter: brightness(0.75);
}
</style>

View file

@ -14,10 +14,12 @@
"livestreams": "البث المباشر",
"channels": "القنوات",
"bookmarks": "الاشارات المرجعيه",
"channel_groups": "مجموعات القنوات"
"channel_groups": "مجموعات القنوات",
"dearrow": "دي ارو"
},
"player": {
"watch_on": "شاهد عبر"
"watch_on": "مشاهدة على {0}",
"failed": "فشل مع رمز الخطأ {0}، راجع السجلات لمزيد من المعلومات"
},
"actions": {
"subscribe": "اشتراك - {count}",
@ -52,7 +54,7 @@
"skip_interaction": "تخطي تذكير التفاعل (اشتراك)",
"skip_non_music": "تخطي الموسيقى: قسم غير الموسيقى",
"theme": "السمة",
"instance_selection": "تحديد المثيل",
"instance_selection": "قائمة الخوادم",
"export_to_json": "تصدير إلى JSON",
"show_more": "اظهار المزيد",
"skip_outro": "تخطي بطاقات النهاية / الاعتمادات",
@ -62,15 +64,15 @@
"skip_filler_tangent": "تخطي المحتوى الغير مهم",
"show_markers": "إظهار العلامات على المشغل",
"buffering_goal": "هدف التخزين المؤقت (بالثواني)",
"country_selection": "اختيار البلد",
"country_selection": "البلد",
"default_homepage": "الصفحة الرئيسية الافتراضية",
"show_comments": "إظهار التعليقات",
"minimize_description_default": "تصغير الوصف بشكل افتراضي",
"store_watch_history": "تخزين سجل المشاهدة",
"language_selection": "اختيار اللغة",
"language_selection": "اللغة",
"instances_list": "قائمة المثيلات",
"enabled_codecs": "برامج الترميز الممكنة (متعددة)",
"import_from_json": "استيراد من JSON/CSV",
"import_from_json": "استيراد من JSON",
"loop_this_video": "تكرار هذا الفيديو",
"auto_play_next_video": "التشغيل التلقائي للفيديو التالي",
"donations": "التبرعات للتطوير",
@ -92,7 +94,7 @@
"minimize_recommendations_default": "تقليل التوصيات بشكل افتراضي",
"invalidate_session": "تسجيل الخروج من جميع الأجهزة",
"different_auth_instance": "استخدام مثيل مختلف للمصادقة",
"instance_auth_selection": "تحديد مثيل Autentication",
"instance_auth_selection": "خادم المصادقة",
"clone_playlist": "استنساخ قائمة التشغيل",
"clone_playlist_success": "تم استنساخها بنجاح!",
"download_as_txt": "تنزيل بتنسيق .txt",
@ -108,7 +110,7 @@
"copy_link": "نسخ الرابط",
"time_code": "رمز الوقت (بالثواني)",
"show_chapters": "الفصول",
"store_search_history": "حفظ سجل البحث",
"store_search_history": "تخزين سجل البحث",
"documentation": "التوثيق",
"status_page": "الحالة",
"source_code": "شفرة المصدر",
@ -139,7 +141,14 @@
"playlist_name": "اسم قائمة التشغيل",
"edit_playlist": "تعديل قائمة التشغيل",
"show_search_suggestions": "إظهار اقتراحات البحث",
"chapters_layout_mobile": "تخطيط الفصول على الهاتف"
"chapters_layout_mobile": "تخطيط الفصول على الهاتف",
"delete_automatically": "الحذف تلقائيا بعد",
"enable_dearrow": "تمكين دي ارو",
"generate_qrcode": "إنشاء رمز الاستجابة السريعة",
"import_from_json_csv": "استيراد من JSON/CSV",
"download_frame": "إطار التحميل",
"instance_privacy_policy": "سياسة الخصوصية",
"add_to_group": "إضافة إلى المجموعة"
},
"video": {
"sponsor_segments": "المقاطع الإعلانية",
@ -153,7 +162,9 @@
"all": "الكل",
"category": "الفئة",
"chapters_vertical": "رَأسِيّ",
"chapters_horizontal": "أفقي"
"chapters_horizontal": "أفقي",
"visibility": "الظهور",
"license": "الترخيص"
},
"search": {
"channels": "يوتيوب: القنوات",
@ -164,7 +175,8 @@
"music_videos": "YT Music: مقاطع فيديو",
"did_you_mean": "هل تقصد: {0}؟",
"music_playlists": "YT Music: قوائم التشغيل",
"music_albums": "YT Music: ألبومات"
"music_albums": "YT Music: ألبومات",
"music_artists": "YT الموسيقى: الفنانين"
},
"preferences": {
"version": "الإصدار",
@ -183,7 +195,9 @@
},
"login": {
"username": "اسم المستخدم",
"password": "كلمة السر"
"password": "كلمة السر",
"passwords_incorrect": "كلمة المرور لم تتطابق!",
"password_confirm": "تأكيد كلمة المرور"
},
"subscriptions": {
"subscribed_channels_count": "مشترك في: {0}"
@ -198,6 +212,12 @@
"cannot_copy": "لا يمكن نسخه!",
"local_storage": "يتطلب هذا الإجراء التخزين المحلي، هل يتم تمكين ملفات تعريف الارتباط؟",
"register_no_email_note": "لا ينصح باستخدام البريد الإلكتروني كاسم مستخدم. المضي قدما على أي حال؟",
"next_video_countdown": "تشغيل الفيديو التالي بعد { 0 } ق"
"next_video_countdown": "تشغيل الفيديو التالي بعد { 0 } ق",
"weeks": "{amount} أسبوع (أسابيع)",
"hours": "{amount} ساعة (ساعات)",
"months": "{amount} شهر (أشهر)",
"days": "{amount} يوم (أيام)",
"login_note": "سجل الدخول باستخدام حساب تم إنشاؤه في هذا الخادم.",
"register_note": "سجل حسابًا لخادم Piped هذا. سيسمح لك هذا بمزامنة اشتراكاتك وقوائم التشغيل مع حسابك، بحيث يتم تخزينها على جانب الخادم. يمكنك استخدام جميع الميزات بدون حساب، ولكن سيتم تخزين جميع البيانات في ذاكرة التخزين المؤقت المحلية لمتصفحك. يُرجى التأكد من عدم استخدام عنوان البريد الإلكتروني كاسم المستخدم الخاص بك واختيار كلمة مرور آمنة لا تستخدمها في أي مكان آخر."
}
}

View file

@ -14,7 +14,8 @@
"livestreams": "Canlı Yayımlar",
"channels": "Kanallar",
"bookmarks": "Əlfəcinlər",
"channel_groups": "Kanal qrupları"
"channel_groups": "Kanal qrupları",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "{0} saytında bax"
@ -49,7 +50,7 @@
"default_quality": "Standart Keyfiyyət",
"buffering_goal": "Tamponlama hədəfi (saniyələrlə)",
"export_to_json": "JSON-a İxrac Et",
"import_from_json": "JSON/CSV-dan İdxal Et",
"import_from_json": "JSON -dan İdxal Et",
"loop_this_video": "Bu Videonu Təkrarla",
"auto_play_next_video": "Növbəti Videonu Avto-Oynat",
"donations": "İnkişaf ianələri",
@ -74,14 +75,14 @@
"select_playlist": "Oynatma Siyahısı Seç",
"delete_playlist_confirm": "Bu oynatma siyahısı silinsin?",
"please_select_playlist": "Xahiş edilir, oynatma siyahısı seç",
"country_selection": "Ölkə Seçimi",
"country_selection": "Ölkə",
"default_homepage": "Standart Əsas Səhifə",
"show_comments": "Şərhləri Göstər",
"instance_selection": "Nümunə Seçimi",
"instance_selection": "İnstansiya",
"minimize_description_default": "Açıqlamanı Standart Olaraq Kiçilt",
"language_selection": "Dil Seçimi",
"language_selection": "Dil",
"instances_list": "Nümunələr Siyahısı",
"show_more": "Daha çox göstər",
"show_more": "Daha Çox Göstər",
"no": "Xeyr",
"store_watch_history": "Baxış Tarixçəsin Saxla",
"enabled_codecs": "Aktiv Kodlayıcılar (Çoxlu)",
@ -97,11 +98,11 @@
"restore_preferences": "Seçimləri bərpa et",
"invalidate_session": "Bütün cihazlardan çıxın",
"different_auth_instance": "Təsdiqləmə üçün fərqli nümunə istifadə et",
"instance_auth_selection": "Təsdiqləmə Nümunəsi Seçimi",
"instance_auth_selection": "Təsdiqləmə İnstansiyası",
"clone_playlist": "Oynatma Siyahısın Klonla",
"clone_playlist_success": "Uğurla klonlandı!",
"time_code": "Vaxt kodu (saniyələrlə)",
"store_search_history": "Axtarış tarixçəsini saxla",
"store_search_history": "Axtarış Tarixçəsin Saxla",
"documentation": "Sənədləşdirmə",
"status_page": "Vəziyyət",
"source_code": "Mənbə kodu",
@ -131,7 +132,20 @@
"autoplay_next_countdown": "Növbəti videoya qədər standart geri sayım (saniyə)",
"dismiss": "Rədd et",
"create_group": "Qrup yarat",
"group_name": "Qrup adı"
"group_name": "Qrup adı",
"cancel": "Ləğv et",
"edit_playlist": "Oynatma siyahısın redaktə et",
"playlist_description": "Oynatma siyahısı təsviri",
"okay": "Oldu",
"chapters_layout_mobile": "Mobildə Bölmələrin Tərtibatı",
"playlist_name": "Oynatma siyahısı adı",
"show_search_suggestions": "Axtarış təkliflərin göstər",
"auto_display_captions": "Titrləri Avtomatik Göstər",
"import_from_json_csv": "JSON/CSV-dən idxal et",
"delete_automatically": "Sonranı avtomatik silin",
"download_frame": "Yükləmə çərçivəsi",
"enable_dearrow": "DeArrow'u Aktivləşdir",
"generate_qrcode": "QR Kodu Yarat"
},
"comment": {
"pinned_by": "{author} tərəfindən sabitlənib",
@ -150,7 +164,9 @@
},
"login": {
"username": "İstifadəçi Adı",
"password": "Şifrə"
"password": "Şifrə",
"password_confirm": "Parolu təsdiqlə",
"passwords_incorrect": "Parollar uyğunlaşmır!"
},
"video": {
"videos": "Videolar",
@ -162,7 +178,11 @@
"live": "{0} Canlı",
"shorts": "Qısa",
"all": "Hamısı",
"category": "Kateqoriya"
"category": "Kateqoriya",
"chapters_horizontal": "Üfüqi",
"chapters_vertical": "Şaquli",
"license": "Lisenziya",
"visibility": "Görünüş"
},
"search": {
"did_you_mean": "Bunu nəzərdə tutursunuz: {0}?",
@ -173,7 +193,8 @@
"music_songs": "YT Music: Mahnılar",
"music_videos": "YT Music: Videolar",
"music_albums": "YT Music: Albomlar",
"music_playlists": "YT Music: Oynatma Siyahıları"
"music_playlists": "YT Music: Oynatma Siyahıları",
"music_artists": "YT Music: Sənətkarlar"
},
"subscriptions": {
"subscribed_channels_count": "Abunə oldu: {0}"
@ -188,6 +209,10 @@
"cannot_copy": "Nüsxələnmir!",
"local_storage": "Bu fəaliyyət yerli yaddaş tələb edir, məlumat bazası aktivdir?",
"register_no_email_note": "E-poçt-u istifadəçi adı kimi istifadə etmək tövsiyə edilmir. Baxmayaraq ki, davam edilsin?",
"next_video_countdown": "Növbəti video {0} saniyəyə oynadılır"
"next_video_countdown": "Növbəti video {0} saniyəyə oynadılır",
"hours": "{amount} saat",
"days": "{amount} gün",
"months": "{amount} ay",
"weeks": "{amount} həftə"
}
}

View file

@ -3,7 +3,7 @@
"channels": "Канали",
"login": "Вход",
"register": "Регистрация",
"feed": "Абонаменти",
"feed": "Емисия",
"history": "История",
"playlists": "Плейлисти",
"instance": "Инстанция",
@ -13,7 +13,9 @@
"trending": "Набиращи популярност",
"account": "Профил",
"preferences": "Настройки",
"subscriptions": "Абонаменти"
"subscriptions": "Абонаменти",
"dearrow": "DeArrow",
"channel_groups": "Канални групи"
},
"actions": {
"most_recent": "Най-скорошен",
@ -110,7 +112,8 @@
"store_search_history": "Запазване на историята на търсене",
"instance_auth_selection": "Избор на инстанция за удостоверяване",
"confirm_reset_preferences": "Сигурни ли сте, че искате да нулирате настройките?",
"hide_watched": "Скриване на гледани видеоклипове в Абонаменти"
"hide_watched": "Скриване на гледани видеоклипове в Абонаменти",
"enable_dearrow": "Включи DeArrow"
},
"player": {
"watch_on": "Гледай в {0}"

View file

@ -1,174 +1,201 @@
{
"titles": {
"login": "Iniciar Sessió",
"login": "Inicia la sessió",
"preferences": "Preferències",
"feed": "Continguts",
"history": "Historial",
"subscriptions": "Subscripcions",
"trending": "Tendències",
"register": "Registrar-se",
"register": "Registra'm",
"playlists": "Llistes de reproducció",
"account": "Compte",
"instance": "Instància",
"player": "Reproductor",
"livestreams": "Retransmissió en directe",
"livestreams": "Directes",
"channels": "Canals",
"bookmarks": "Marcadors"
"bookmarks": "Marcadors",
"channel_groups": "Grups de canals",
"dearrow": "DeArrow"
},
"actions": {
"channel_name_desc": "Nom del Canal (Z-A)",
"channel_name_desc": "Nom del canal (Z-A)",
"back": "Enrere",
"skip_intro": "Omet l'Animació d'Entreacte/Introducció",
"skip_outro": "Omet les Targetes/Crèdits",
"skip_preview": "Omet la Previsualització/Recapitulació",
"skip_interaction": "Omet el Recordatori d'Interacció (Subscriu-te)",
"skip_self_promo": "Omet la Promoció no Remunerada/Autopromoció",
"skip_non_music": "Omet la Música: Secció No-Músical",
"skip_highlight": "Omet els Destacats",
"skip_filler_tangent": "Omet les Tangents de Farciment",
"skip_intro": "Omet l'animació d'interludi/introducció",
"skip_outro": "Omet els crèdits finals",
"skip_preview": "Omet la previsualització/recapitulació",
"skip_interaction": "Omet el recordatori d'interacció (Subscriviu-vos)",
"skip_self_promo": "Omet la promoció no remunerada/autopromoció",
"skip_non_music": "Omet la música: Secció no musical",
"skip_highlight": "Omet els destacats",
"skip_filler_tangent": "Omet les tangents de farciment",
"theme": "Tema",
"auto": "Automàtic",
"dark": "Fosc",
"light": "Clar",
"autoplay_video": "Reprodueix Automàticament el Vídeo",
"audio_only": "Només Àudio",
"default_quality": "Qualitat Per Defecte",
"buffering_goal": "Objectiu de la Memòria Intermèdia (en segons)",
"country_selection": "Selecciona el País",
"default_homepage": "Pàgina Inicial Per Defecte",
"show_comments": "Mostra els Comentaris",
"minimize_description_default": "Minimitza la Descripció Per Defecte",
"store_watch_history": "Desa l'Historial de Reproducció",
"language_selection": "Selecciona l'Idioma",
"enabled_codecs": "Còdecs Habilitats (Múltiple)",
"instances_list": "Llista d'Instàncies",
"instance_selection": "Selecció d'Instàncies",
"show_more": "Mostrar més",
"autoplay_video": "Reprodueix automàticament el vídeo",
"audio_only": "Només àudio",
"default_quality": "Qualitat predefinida",
"buffering_goal": "Objectiu de la memòria intermèdia (en segons)",
"country_selection": "País",
"default_homepage": "Pàgina d'inici predefinida",
"show_comments": "Mostra els comentaris",
"minimize_description_default": "Minimitza la descripció per defecte",
"store_watch_history": "Desa l'historial de visualitzacions",
"language_selection": "Llengua",
"enabled_codecs": "Còdecs habilitats (múltiples)",
"instances_list": "Llista d'instàncies",
"instance_selection": "Instància",
"show_more": "Mostra'n més",
"yes": "Sí",
"no": "No",
"export_to_json": "Exportar a JSON",
"import_from_json": "Importar de JSON/CSV",
"loop_this_video": "Reprodueix aquest Vídeo en bucle",
"donations": "Donacions de desenvolupament",
"minimize_description": "Minimitza la Descripció",
"show_description": "Mostra la Descripció",
"disable_lbry": "Deshabilita LBRY per Streaming",
"enable_lbry_proxy": "Habilita servidor intermediari per LBRY",
"view_ssl_score": "Visualitza la Puntuació SSL",
"search": "Cercar",
"export_to_json": "Exporta a JSON",
"import_from_json": "Importa des d'un JSON",
"loop_this_video": "Reprodueix el vídeo en bucle",
"donations": "Donacions al desenvolupament",
"minimize_description": "Minimitza la descripció",
"show_description": "Mostra la descripció",
"disable_lbry": "Desactiva el LBRY per a les transmissions en temps real",
"enable_lbry_proxy": "Activa un servidor intermediari per al LBRY",
"view_ssl_score": "Mostra la puntuació SSL",
"search": "Cerca (Ctrl+K)",
"filter": "Filtre",
"loading": "Carregant...",
"clear_history": "Netejar Historial",
"hide_replies": "Oculta Respostes",
"load_more_replies": "Carrega més Respostes",
"view_subscriptions": "Veure Subscripcions",
"subscribe": "Subscriure - {count}",
"unsubscribe": "Anul·lar subscripció - {count}",
"sort_by": "Ordenar per:",
"most_recent": "Més Recents",
"least_recent": "Menys Recents",
"skip_sponsors": "Omet Promocions",
"channel_name_asc": "Nom del Canal (A-Z)",
"enable_sponsorblock": "Habilita Sponsorblock",
"loading": "S'està carregant...",
"clear_history": "Esborra l'historial",
"hide_replies": "Amaga les respostes",
"load_more_replies": "Carrega més respostes",
"view_subscriptions": "Mostra les subscripcions",
"subscribe": "Subscriu-me - {count}",
"unsubscribe": "Anul·la la subscripció - {count}",
"sort_by": "Ordena per:",
"most_recent": "Més recents",
"least_recent": "Menys recents",
"skip_sponsors": "Omet els patrocinadors",
"channel_name_asc": "Nom del canal (A-Z)",
"enable_sponsorblock": "Activa l'Sponsorblock",
"uses_api_from": "Utilitza l'API de ",
"minimize_recommendations": "Minimitza Recomanacions",
"show_recommendations": "Mostra Recomanacions",
"minimize_recommendations": "Minimitza les recomanacions",
"show_recommendations": "Mostra les recomanacions",
"auto_play_next_video": "Reprodueix automàticament el pròxim vídeo",
"add_to_playlist": "Afegir a la llista de reproducció",
"add_to_playlist": "Afegeix a la llista de reproducció",
"remove_from_playlist": "Suprimeix de la llista de reproducció",
"create_playlist": "Crear llista de reproducció",
"delete_playlist": "Suprimir llista de reproducció",
"please_select_playlist": "Selecciona una llista de reproducció",
"delete_playlist_confirm": "Eliminar aquesta llista de reproducció?",
"delete_playlist_video_confirm": "Eliminar aquest vídeo de la llista de reproducció?",
"create_playlist": "Crea una llista de reproducció",
"delete_playlist": "Suprimeix la llista de reproducció",
"please_select_playlist": "Seleccioneu una llista de reproducció",
"delete_playlist_confirm": "Voleu suprimir la llista de reproducció?",
"delete_playlist_video_confirm": "Voleu suprimir el vídeo de la llista de reproducció?",
"select_playlist": "Selecciona una llista de reproducció",
"show_markers": "Mostra Marcadors al Reproductor",
"delete_account": "Eliminar Compte",
"logout": "Desconnectar aquest dispositiu",
"minimize_recommendations_default": "Minimitza les Recomanacions per defecte",
"invalidate_session": "Desconnectar tots els dispositius",
"show_markers": "Mostra marcadors al reproductor",
"delete_account": "Suprimeix el compte",
"logout": "Desconnecta aquest dispositiu",
"minimize_recommendations_default": "Minimitza les recomanacions per defecte",
"invalidate_session": "Desconnecta tots els dispositius",
"different_auth_instance": "Usa una instància diferent per a l'autenticació",
"instance_auth_selection": "Selecciona la instància d'autenticació",
"clone_playlist": "Clonar Llista de Reproducció",
"clone_playlist_success": "Clonada correctament!",
"download_as_txt": "Descarrega com a .txt",
"reset_preferences": "Restablir preferències",
"instance_auth_selection": "Instància d'autenticació",
"clone_playlist": "Clona la llista de reproducció",
"clone_playlist_success": "S'ha clonat correctament!",
"download_as_txt": "Baixa com a .txt",
"reset_preferences": "Restableix les preferències",
"restore_preferences": "Restaura les preferències",
"backup_preferences": "Preferències de la còpia de seguretat",
"confirm_reset_preferences": "Esteu segur que voleu restablir les vostres preferències?",
"backup_preferences": "Exporta les preferències",
"confirm_reset_preferences": "Segur que voleu restablir les vostres preferències?",
"back_to_home": "Torna a l'inici",
"with_timecode": "Compartir moment concret",
"with_timecode": "Comparteix amb marca de temps",
"piped_link": "Enllaç de Piped",
"share": "Compartir",
"time_code": "Moment (en segons)",
"copy_link": "Copiar l'enllaç",
"share": "Comparteix",
"time_code": "Marca de temps (en segons)",
"copy_link": "Copia l'enllaç",
"follow_link": "Vés a l'enllaç",
"store_search_history": "Emmagatzema l'historial de cerca",
"instance_donations": "Donacions a instàncies",
"hide_watched": "Amaga els vídeos vistos de Continguts",
"hide_watched": "Amaga els vídeos vistos dels continguts",
"show_chapters": "Capítols",
"status_page": "Estat",
"source_code": "Codi font",
"documentation": "Documentació",
"show_watch_on_youtube": "Mostra el botó \"Veure a Youtube\"",
"show_watch_on_youtube": "Mostra el botó «Mostra'l a Youtube»",
"reply_count": "{count} respostes",
"minimize_comments_default": "Minimitzar els comentaris per defecte",
"minimize_comments_default": "Minimitza els comentaris per defecte",
"minimize_comments": "Minimitza els comentaris",
"no_valid_playlists": "L'arxiu no conté llistes de reproducció vàlides!",
"bookmark_playlist": "Marcador",
"playlist_bookmarked": "Afegit a marcadors",
"minimize_chapters_default": "Minimitzar capítols per defecte",
"no_valid_playlists": "El fitxer no conté llistes de reproducció vàlides!",
"bookmark_playlist": "Afegeix als marcadors",
"playlist_bookmarked": "S'ha afegit als marcadors",
"minimize_chapters_default": "Minimitza els capítols per defecte",
"skip_button_only": "Mostra el botó de saltar",
"skip_automatically": "Automàticament",
"min_segment_length": "Longitud de segment mínima (en segons)",
"skip_segment": "Saltar segment",
"with_playlist": "Comparteix amb llista de reproducció",
"show_less": "Mostrar menys"
"skip_segment": "Omet el segment",
"with_playlist": "Comparteix amb la llista de reproducció",
"show_less": "Mostra'n menys",
"okay": "D'acord",
"edit_playlist": "Edita la llista de reproducció",
"group_name": "Nom del grup",
"import_from_json_csv": "Importa des d'un JSON/CSV",
"cancel": "Cancel·la",
"enable_dearrow": "Activa el DeArrow",
"playlist_description": "Descripció de la llista de reproducció",
"dismiss": "Descarta",
"chapters_layout_mobile": "Disposició dels capítols al mòbil",
"delete_automatically": "Suprimeix automàticament després de",
"download_frame": "Baixa el fotograma",
"create_group": "Crea un grup",
"show_search_suggestions": "Mostra suggeriments de cerca",
"auto_display_captions": "Mostra els subtítols automàticament",
"autoplay_next_countdown": "Compte enrere predefinit fins al pròxim vídeo (en segons)",
"generate_qrcode": "Genera un codi QR",
"playlist_name": "Nom de la llista de reproducció"
},
"comment": {
"pinned_by": "Fixat per {author}",
"user_disabled": "Comentaris deshabilitats en els paràmetres.",
"disabled": "El carregador ha desactivat els comentaris.",
"loading": "Carregant comentaris..."
"user_disabled": "Els comentaris s'han desactivat en els paràmetres.",
"disabled": "Qui va pujar el vídeo ha desactivat els comentaris.",
"loading": "S'estan carregant els comentaris..."
},
"preferences": {
"instance_name": "Nom de la Instància",
"instance_locations": "Ubicacións de la Instància",
"instance_name": "Nom de la instància",
"instance_locations": "Ubicacions de la instància",
"has_cdn": "Té CDN?",
"ssl_score": "Puntuació SSL",
"version": "Versió",
"up_to_date": "Actualitzat?",
"registered_users": "Usuaris Registrats"
"up_to_date": "Actualitzada?",
"registered_users": "Usuaris registrats"
},
"video": {
"watched": "Vist",
"sponsor_segments": "Segments de Patrocinadors",
"ratings_disabled": "Puntuacions Deshabilitades",
"sponsor_segments": "Segments de patrocinadors",
"ratings_disabled": "Puntuacions desactivades",
"chapters": "Capítols",
"live": "{0} En Directe",
"live": "{0} en directe",
"videos": "Vídeos",
"views": "{views} visualitzacions",
"shorts": "Curts",
"all": "Tot",
"category": "Categoria"
"category": "Categoria",
"visibility": "Visibilitat",
"license": "Llicència",
"chapters_vertical": "Vertical",
"chapters_horizontal": "Horitzontal"
},
"search": {
"did_you_mean": "Volies dir: {0}?",
"did_you_mean": "Volíeu dir: {0}?",
"all": "YouTube: Tot",
"videos": "YouTube: Vídeos",
"channels": "YouTube: Canals",
"playlists": "YouTube: Llistes de Reproducció",
"playlists": "YouTube: Llistes de reproducció",
"music_songs": "YT Music: Cançons",
"music_videos": "YT Music: Vídeos",
"music_albums": "YT Music: Àlbums",
"music_playlists": "YT Music: Llistes de Reproducció"
"music_playlists": "YT Music: Llistes de reproducció",
"music_artists": "YT Music: Artistes"
},
"player": {
"watch_on": "Veure a {0}"
"watch_on": "Mostra a {0}",
"failed": "Ha fallat amb codi d'error {0}, vegeu els registres per a més informació"
},
"login": {
"username": "Nom d'Usuari",
"password": "Contrassenya"
"username": "Nom d'usuari",
"password": "Contrasenya",
"password_confirm": "Confirmeu la contrasenya",
"passwords_incorrect": "Les contrasenyes no coincideixen!"
},
"subscriptions": {
"subscribed_channels_count": "Subscrit a: {0}"
@ -179,9 +206,14 @@
"info": {
"preferences_note": "Nota: les preferències es desen a l'emmagatzematge local del navegador. Si elimineu les dades del navegador, es restabliran.",
"page_not_found": "No s'ha torbat la pàgina",
"copied": "Copiat!",
"copied": "S'ha copiat!",
"cannot_copy": "No es pot copiar!",
"local_storage": "Aquesta acció requereix emmagatzematge local, estan les cookies habilitades?",
"register_no_email_note": "Utilitzar un correu elextrònic com a usuari no és recomanable. Continuar de totes maneres?"
"local_storage": "Aquesta acció requereix localStorage. Es troben activades les galetes?",
"register_no_email_note": "Usar una adreça electrònica com a nom d'usuari no és recomanable. Voleu continuar de totes maneres?",
"hours": "{amount} hores",
"next_video_countdown": "Pròxim vídeo en {0}s",
"months": "{amount} mesos",
"weeks": "{amount} setmanes",
"days": "{amount} dies"
}
}

View file

@ -14,7 +14,8 @@
"livestreams": "Živé přenosy",
"channels": "Kanály",
"bookmarks": "Záložky",
"channel_groups": "Skupiny kanálů"
"channel_groups": "Skupiny kanálů",
"dearrow": "DeArrow"
},
"actions": {
"loop_this_video": "Přehrávat video ve smyčce",
@ -37,20 +38,20 @@
"audio_only": "Pouze zvuk",
"default_quality": "Výchozí kvalita",
"buffering_goal": "Ukládání do vyrovnávací paměti (v sekundách)",
"country_selection": "Výběr země",
"country_selection": "Země",
"default_homepage": "Výchozí domovská stránka",
"show_comments": "Zobrazit komentáře",
"minimize_description_default": "Automaticky minimalizovat popis",
"store_watch_history": "Ukládat historii sledování",
"language_selection": "Výběr jazyka",
"language_selection": "Jazyk",
"instances_list": "Seznam instancí",
"enabled_codecs": "Povolené kodeky (několik)",
"instance_selection": "Výběr instance",
"instance_selection": "Instance",
"show_more": "Zobrazit více",
"yes": "Ano",
"no": "Ne",
"export_to_json": "Exportovat do JSON",
"import_from_json": "Importovat z JSON/CSV",
"import_from_json": "Importovat z JSON",
"auto_play_next_video": "Automaticky přehrát další video",
"donations": "Dary na vývoj",
"show_description": "Zobrazit popis",
@ -89,7 +90,7 @@
"minimize_recommendations_default": "Ve výchozím nastavení minimalizovat doporučení",
"invalidate_session": "Odhlásit se ze všech zařízení",
"different_auth_instance": "Použít jinou instanci pro autentizaci",
"instance_auth_selection": "Výběr autentizační instance",
"instance_auth_selection": "Autentizační instance",
"clone_playlist": "Duplikovat playlist",
"clone_playlist_success": "Úspěšně duplikováno!",
"download_as_txt": "Stáhnout jako .txt",
@ -136,10 +137,17 @@
"playlist_description": "Popis playlistu",
"okay": "Okay",
"show_search_suggestions": "Zobrazit našeptávání ve vyhledávání",
"chapters_layout_mobile": "Rozložení kapitol na mobilu"
"chapters_layout_mobile": "Rozložení kapitol na mobilu",
"enable_dearrow": "Povolit DeArrow",
"delete_automatically": "Automaticky odstranit po",
"generate_qrcode": "Vygenerovat QR kód",
"import_from_json_csv": "Importovat z JSON/CSV",
"download_frame": "Stáhnout snímek",
"instance_privacy_policy": "Ochrana údajů"
},
"player": {
"watch_on": "Sledovat na {0}"
"watch_on": "Zobrazit na {0}",
"failed": "Akce se nezdařila. Chybový kód {0}, pro více informací viz protokol"
},
"comment": {
"pinned_by": "Připnuto uživatelem {author}",
@ -158,7 +166,9 @@
},
"login": {
"username": "Uživatelské jméno",
"password": "Heslo"
"password": "Heslo",
"password_confirm": "Potvrzení hesla",
"passwords_incorrect": "Hesla se neshodují!"
},
"video": {
"videos": "Videa",
@ -172,7 +182,9 @@
"all": "Vše",
"category": "Kategorie",
"chapters_horizontal": "Horizontální",
"chapters_vertical": "Vertikální"
"chapters_vertical": "Vertikální",
"license": "Licence",
"visibility": "Viditelnost"
},
"search": {
"did_you_mean": "Mysleli jste: {0}?",
@ -183,7 +195,8 @@
"playlists": "YouTube: Playlisty",
"music_videos": "YT Music: Videa",
"music_albums": "YT Music: Alba",
"music_playlists": "YT Music: Playlisty"
"music_playlists": "YT Music: Playlisty",
"music_artists": "YT Music: Umělci"
},
"subscriptions": {
"subscribed_channels_count": "Přihlášeno k odběru: {0}"
@ -198,6 +211,10 @@
"cannot_copy": "Nelze zkopírovat!",
"local_storage": "Tato akce vyžaduje localStorage, jsou povoleny cookies?",
"register_no_email_note": "Použití e-mailu jako uživatelského jména se nedoporučuje. Chcete přesto pokračovat?",
"next_video_countdown": "Přehrávání dalšího videa za {0}s"
"next_video_countdown": "Přehrávání dalšího videa za {0}s",
"hours": "{amount} hodin",
"days": "{amount} dnů",
"weeks": "{amount} týdnů",
"months": "{amount} měsíců"
}
}

View file

@ -6,12 +6,12 @@
"skip_interaction": "Interaktionserinnerungen überspringen (Daumen hoch, abonnieren, ...)",
"skip_preview": "Vorschau und Rückblick überspringen",
"instances_list": "Liste der Instanzen",
"language_selection": "Sprachauswahl",
"language_selection": "Sprache",
"store_watch_history": "Wiedergabeverlauf speichern",
"minimize_description_default": "Beschreibung standardmäßig minimieren",
"show_comments": "Kommentare anzeigen",
"default_homepage": "Startseite",
"country_selection": "Länderauswahl",
"country_selection": "Land",
"buffering_goal": "Pufferungsziel (in Sekunden)",
"default_quality": "Standardqualität",
"audio_only": "Nur Audio",
@ -36,7 +36,7 @@
"enabled_codecs": "Aktivierte Codecs (Auswahl mehrerer Codecs möglich)",
"enable_lbry_proxy": "Proxy für LBRY einschalten",
"disable_lbry": "LBRY für Streaming deaktivieren",
"instance_selection": "Instanz auswählen",
"instance_selection": "Instanz",
"show_description": "Beschreibung anzeigen",
"minimize_description": "Beschreibung minimieren",
"show_recommendations": "Empfehlungen anzeigen",
@ -44,7 +44,7 @@
"donations": "Spenden",
"auto_play_next_video": "Nächstes Video automatisch abspielen",
"loop_this_video": "Dieses Video wiederholen",
"import_from_json": "Aus JSON/CSV importieren",
"import_from_json": "Aus JSON importieren",
"export_to_json": "Als JSON exportieren",
"show_more": "Mehr anzeigen",
"no": "Nein",
@ -72,7 +72,7 @@
"minimize_recommendations_default": "Empfehlungen standardmäßig minimieren",
"invalidate_session": "Von allen Geräten abmelden",
"different_auth_instance": "Eine andere Instanz für die Authentifizierung verwenden",
"instance_auth_selection": "Auswahl der Autentifizierungsinstanz",
"instance_auth_selection": "Authentifizierungsinstanz",
"clone_playlist": "Playlist duplizieren",
"clone_playlist_success": "Erfolgreich dupliziert!",
"piped_link": "Piped-Link",
@ -119,10 +119,18 @@
"playlist_name": "Name der Playlist",
"playlist_description": "Beschreibung der Playlist",
"show_search_suggestions": "Suchvorschläge anzeigen",
"chapters_layout_mobile": "Kapitel-Layout auf Mobilgeräten"
"chapters_layout_mobile": "Kapitel-Layout auf Mobilgeräten",
"delete_automatically": "Automatisch löschen nach",
"enable_dearrow": "DeArrow verwenden",
"generate_qrcode": "QR-Code generieren",
"import_from_json_csv": "Aus JSON/CSV importieren",
"download_frame": "Einzelbild (Frame) downloaden",
"instance_privacy_policy": "Datenschutzerklärung",
"add_to_group": "Zu Gruppe hinzufügen"
},
"player": {
"watch_on": "Auf {0} ansehen"
"watch_on": "Auf {0} ansehen",
"failed": "Fehlgeschlagen mit Fehlercode {0}, siehe Protokolle für weitere Informationen"
},
"titles": {
"history": "Verlauf",
@ -139,7 +147,8 @@
"livestreams": "Livestreams",
"channels": "Kanäle",
"bookmarks": "Lesezeichen",
"channel_groups": "Kanalgruppen"
"channel_groups": "Kanalgruppen",
"dearrow": "DeArrow"
},
"video": {
"sponsor_segments": "Sponsoren-Abschnitte",
@ -153,7 +162,9 @@
"all": "Alle",
"category": "Kategorie",
"chapters_horizontal": "Horizontal",
"chapters_vertical": "Vertikal"
"chapters_vertical": "Vertikal",
"license": "Lizenz",
"visibility": "Sichtbarkeit"
},
"preferences": {
"ssl_score": "SSL-Bewertung",
@ -172,7 +183,9 @@
},
"login": {
"password": "Passwort",
"username": "Benutzername"
"username": "Benutzername",
"password_confirm": "Passwort bestätigen",
"passwords_incorrect": "Passwörter stimmen nicht überein!"
},
"search": {
"did_you_mean": "Hast du gemeint: {0}?",
@ -183,18 +196,25 @@
"music_songs": "YT Music: Lieder",
"music_videos": "YT Music: Videos",
"music_albums": "YT Music: Alben",
"music_playlists": "YT Music: Playlists"
"music_playlists": "YT Music: Playlists",
"music_artists": "YT Music: Künstler:innen"
},
"subscriptions": {
"subscribed_channels_count": "Anzahl Abos: {0}"
},
"info": {
"preferences_note": "Achtung: Die Einstellung werden lokal in deinem Browser gespeichert. Wenn du deine Browserdaten löschst, werden auch deine Einstellungen zurückgesetzt.",
"preferences_note": "Achtung: Die Einstellungen werden lokal in deinem Browser gespeichert. Wenn du deine Browserdaten löschst, werden auch deine Einstellungen zurückgesetzt.",
"page_not_found": "Seite nicht gefunden",
"copied": "Kopiert!",
"cannot_copy": "Kopieren nicht möglich!",
"local_storage": "Diese Aktion erfordert „localStorage“, sind Cookies aktiviert?",
"register_no_email_note": "Es wird nicht empfohlen, eine E-Mail als Benutzernamen zu verwenden. Trotzdem fortfahren?",
"next_video_countdown": "Nächstes Video startet in {0}s"
"next_video_countdown": "Nächstes Video startet in {0}s",
"weeks": "{amount} Woche(n)",
"months": "{amount} Monat(en)",
"hours": "{amount} Stunde(n)",
"days": "{amount} Tag(e)",
"login_note": "Melde dich mit einem Konto an, das du auf dieser Instanz erstellt hast.",
"register_note": "Erstelle ein Konto für diese Piped-Instanz. Dadurch kannst du deine Abos und Playlists mit deinem Konto synchronisieren, sie werden also serverseitig gespeichert. Du kannst all diese Funktionen auch ohne Konto nutzen, allerdings werden dann alle Daten nur im lokalen Speicher deines Browsers gespeichert. Bitte stelle sicher, dass du KEINE E-Mail-Adresse als Benutzernamen verwendest und ein sicheres Passwort wählst, welches du nicht bereits woanders nutzt."
}
}

View file

@ -14,10 +14,12 @@
"livestreams": "Livestreams",
"channels": "Channels",
"bookmarks": "Bookmarks",
"channel_groups": "Channel groups"
"channel_groups": "Channel groups",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "Watch on {0}"
"watch_on": "View on {0}",
"failed": "Failed with error code {0}, see logs for more info"
},
"actions": {
"subscribe": "Subscribe - {count}",
@ -45,6 +47,7 @@
"show_markers": "Show Markers on Player",
"min_segment_length": "Minimum Segment Length (in seconds)",
"skip_segment": "Skip Segment",
"enable_dearrow": "Enable DeArrow",
"theme": "Theme",
"auto": "Auto",
"dark": "Dark",
@ -54,20 +57,21 @@
"audio_only": "Audio Only",
"default_quality": "Default Quality",
"buffering_goal": "Buffering Goal (in seconds)",
"country_selection": "Country Selection",
"country_selection": "Country",
"default_homepage": "Default Homepage",
"minimize_comments_default": "Minimize Comments by default",
"minimize_description_default": "Minimize Description by default",
"store_watch_history": "Store Watch History",
"language_selection": "Language Selection",
"language_selection": "Language",
"instances_list": "Instances List",
"enabled_codecs": "Enabled Codecs (Multiple)",
"instance_selection": "Instance Selection",
"instance_selection": "Instance",
"show_more": "Show More",
"yes": "Yes",
"no": "No",
"export_to_json": "Export to JSON",
"import_from_json": "Import from JSON/CSV",
"import_from_json": "Import from JSON",
"import_from_json_csv": "Import from JSON/CSV",
"loop_this_video": "Loop this Video",
"auto_play_next_video": "Auto Play next Video",
"auto_display_captions": "Auto Display Captions",
@ -103,7 +107,7 @@
"show_watch_on_youtube": "Show Watch on YouTube button",
"invalidate_session": "Logout all devices",
"different_auth_instance": "Use a different instance for authentication",
"instance_auth_selection": "Autentication Instance Selection",
"instance_auth_selection": "Authentication Instance",
"clone_playlist": "Clone Playlist",
"clone_playlist_success": "Successfully cloned!",
"download_as_txt": "Download as .txt",
@ -122,12 +126,13 @@
"copy_link": "Copy link",
"time_code": "Time code (in seconds)",
"show_chapters": "Chapters",
"store_search_history": "Store Search history",
"store_search_history": "Store Search History",
"hide_watched": "Hide watched videos in the feed",
"documentation": "Documentation",
"status_page": "Status",
"source_code": "Source code",
"instance_donations": "Instance donations",
"instance_privacy_policy": "Privacy Policy",
"reply_count": "{count} replies",
"no_valid_playlists": "The file doesn't contain valid playlists!",
"with_playlist": "Share with playlist",
@ -139,7 +144,11 @@
"group_name": "Group name",
"cancel": "Cancel",
"okay": "Okay",
"show_search_suggestions": "Show search suggestions"
"show_search_suggestions": "Show search suggestions",
"delete_automatically": "Delete automatically after",
"generate_qrcode": "Generate QR Code",
"download_frame": "Download frame",
"add_to_group": "Add to group"
},
"comment": {
"pinned_by": "Pinned by {author}",
@ -158,7 +167,9 @@
},
"login": {
"username": "Username",
"password": "Password"
"password": "Password",
"password_confirm": "Confirm password",
"passwords_incorrect": "Passwords don't match!"
},
"video": {
"videos": "Videos",
@ -171,6 +182,8 @@
"shorts": "Shorts",
"all": "All",
"category": "Category",
"license": "License",
"visibility": "Visibility",
"chapters_horizontal": "Horizontal",
"chapters_vertical": "Vertical"
},
@ -183,7 +196,8 @@
"music_songs": "YT Music: Songs",
"music_videos": "YT Music: Videos",
"music_albums": "YT Music: Albums",
"music_playlists": "YT Music: Playlists"
"music_playlists": "YT Music: Playlists",
"music_artists": "YT Music: Artists"
},
"subscriptions": {
"subscribed_channels_count": "Subscribed to: {0}"
@ -195,6 +209,12 @@
"cannot_copy": "Can't copy!",
"local_storage": "This action requires localStorage, are cookies enabled?",
"register_no_email_note": "Using an e-mail as username is not recommended. Proceed anyways?",
"next_video_countdown": "Playing next video in {0}s"
"next_video_countdown": "Playing next video in {0}s",
"hours": "{amount} hour(s)",
"days": "{amount} day(s)",
"weeks": "{amount} week(s)",
"months": "{amount} month(s)",
"register_note": "Register an account for this Piped instance. This will allow you to sync your subscriptions and playlists with your account, so they're stored on the server side. You can use all features without an account, but all data will be stored in your browser's local cache. Please make sure you do NOT use an email address as your username and choose a secure password that you do not use elsewhere.",
"login_note": "Log in with an account created on this instance."
}
}

View file

@ -10,14 +10,16 @@
"playlists": "Ludlistoj",
"account": "Konto",
"player": "Ludilo",
"instance": "Nodo",
"instance": "Retejo",
"channels": "Kanaloj",
"livestreams": "Tujelsendoj",
"bookmarks": "Legosignoj",
"channel_groups": "Kanalaroj"
"channel_groups": "Kanalaroj",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "Vidi en {0}"
"watch_on": "Vidi en {0}",
"failed": "Fiaskis kun erarkodo {0}, vidu protokolojn por pli da informo"
},
"actions": {
"subscribe": "Aboni - {count}",
@ -41,10 +43,10 @@
"autoplay_video": "Aŭtomate Ludi Videon",
"audio_only": "Nur Sono",
"default_quality": "Implicita Kvalito",
"country_selection": "Landa Elekto",
"country_selection": "Lando",
"default_homepage": "Implicita Ĉefpaĝo",
"show_comments": "Montri Komentojn",
"language_selection": "Lingva Elekto",
"language_selection": "Lingvo",
"donations": "Donacoj por programado",
"show_more": "Montri pli",
"yes": "Jes",
@ -77,13 +79,13 @@
"export_to_json": "Elporti JSON-n",
"loop_this_video": "Ripetadi ĉi tiun Videon",
"enable_lbry_proxy": "Ebligi Prokurilon por LBRY",
"import_from_json": "Importi el JSON/CSV",
"import_from_json": "Importi el JSON",
"show_description": "Montri Priskribon",
"instances_list": "Listo de Nodoj",
"instances_list": "Listo de Piped-retejoj",
"auto_play_next_video": "Aŭtomate Ludi sekvan Videon",
"show_recommendations": "Montri Rekomendojn",
"reset_preferences": "Restarigi agordojn",
"instance_selection": "Noda Elekto",
"instance_selection": "Retejo",
"view_ssl_score": "Vidu SSL-Poentaron",
"backup_preferences": "Savkopii agordojn",
"disable_lbry": "Malebligi LBRY-n por Elsendfluo",
@ -91,11 +93,11 @@
"store_search_history": "Konservi Ŝerĉhistorion",
"hide_watched": "Kaŝi viditajn videojn en la fluo",
"minimize_recommendations": "Plejetigi Rekomendojn",
"instance_auth_selection": "Elekto de Aŭtentokontrola Nodo",
"instance_auth_selection": "Aŭtentokontrola Piped-retejo",
"restore_preferences": "Restarigi agordojn",
"status_page": "Stato",
"please_select_playlist": "Bonvolu elekti ludliston",
"different_auth_instance": "Uzi alian nodon por aŭtentokontrolo",
"different_auth_instance": "Uzi alian Piped-retejon por aŭtentokontrolo",
"back_to_home": "Ree hejmen",
"time_code": "Tempkodo (en sekundoj)",
"skip_non_music": "Preterpasi Muzikon: Nemuzika Sekcio",
@ -139,7 +141,14 @@
"playlist_description": "Priskribo de la ludlisto",
"cancel": "Nuligi",
"show_search_suggestions": "Montri serĉ-sugestojn",
"chapters_layout_mobile": "Aranĝo de ĉapitroj en poŝtelefono"
"chapters_layout_mobile": "Aranĝo de ĉapitroj en poŝtelefono",
"delete_automatically": "Aŭtomate forigi post",
"enable_dearrow": "Ebligi DeArrow",
"generate_qrcode": "Generi QR-kodon",
"import_from_json_csv": "Importi el JSON/CSV",
"download_frame": "Elŝuti bildon",
"instance_privacy_policy": "Privateca politiko",
"add_to_group": "Aldoni al grupo"
},
"video": {
"chapters": "Sekcioj",
@ -147,13 +156,15 @@
"live": "{0} Realtempe",
"views": "{views} spektoj",
"sponsor_segments": "Sponsoraj Segmentoj",
"watched": "Viditaj",
"watched": "Spektita",
"ratings_disabled": "Taksadoj Malebligitaj",
"shorts": "Mallongaj",
"all": "Ĉiuj",
"category": "Kategorio",
"chapters_horizontal": "Horizontala",
"chapters_vertical": "Vertikala"
"chapters_vertical": "Vertikala",
"license": "Permesilo",
"visibility": "Videbleco"
},
"search": {
"music_albums": "YT Music: Albumoj",
@ -164,7 +175,8 @@
"music_videos": "YT Music: Videoj",
"music_songs": "YT Music: Muzikaĵoj",
"all": "YouTube: Ĉio",
"did_you_mean": "Ĉu vi volis diri «{0}»?"
"did_you_mean": "Ĉu vi volis diri «{0}»?",
"music_artists": "YT Music: Artistoj"
},
"info": {
"copied": "Kopiita!",
@ -173,11 +185,19 @@
"page_not_found": "Paĝo ne trovita",
"local_storage": "Ĉi tiu ago postulas localStorage, ĉu kuketoj estas ebligitaj?",
"register_no_email_note": "Uzi retadreson kiel uzantnomon ne estas rekomendita. Ĉu daŭrigi ĉiuokaze?",
"next_video_countdown": "Oni ludos la sekvan videon post {0}s"
"next_video_countdown": "Oni ludos la sekvan videon post {0}s",
"hours": "{amount} horo(j)",
"days": "{amount} tago(j)",
"weeks": "{amount} semajno(j)",
"months": "{amount} monato(j)",
"login_note": "Ensaluti per konto kreita en ĉi tiu retejo.",
"register_note": "Registri konton por ĉi tiu Piped-retejo. Tio ebligos al vi sinkronigi viajn abonojn kaj ludlistojn kun via konto, do ili estos konservitaj en la servilo. Vi ankaŭ povas uzi ĉiujn funkciojn sen konto, sed ĉiuj datenoj estos konservitaj en la loka kaŝmemoro de via retumilo. Bonvolu elekti sekuran pasvorton, kiun vi ne uzas aliloke, kaj NE uzi retadreson kiel uzantnomon."
},
"login": {
"username": "Uzantnomo",
"password": "Pasvorto"
"password": "Pasvorto",
"password_confirm": "Konfirmu la pasvorton",
"passwords_incorrect": "Pasvortoj ne kongruas!"
},
"preferences": {
"version": "Versio",

View file

@ -11,7 +11,9 @@
"all": "Todos",
"category": "Categoría",
"chapters_horizontal": "Horizontal",
"chapters_vertical": "Vertical"
"chapters_vertical": "Vertical",
"license": "Licencia",
"visibility": "Visibilidad"
},
"preferences": {
"ssl_score": "Puntuación SSL",
@ -36,20 +38,20 @@
"donations": "Donaciones para desarrollo",
"auto_play_next_video": "Reproducción automática del siguiente vídeo",
"loop_this_video": "Poner en bucle este vídeo",
"import_from_json": "Importar desde JSON/CSV",
"import_from_json": "Importar desde JSON",
"export_to_json": "Exportar a JSON",
"no": "No",
"yes": "Sí",
"show_more": "Mostrar más",
"instance_selection": "Selección de instancias",
"instance_selection": "Instancia",
"enabled_codecs": "Códecs habilitados (múltiples)",
"instances_list": "Lista de instancias",
"language_selection": "Selección de idioma",
"language_selection": "Idioma",
"store_watch_history": "Recordar historial de visualización",
"minimize_description_default": "Minimizar la descripción por defecto",
"show_comments": "Mostrar comentarios",
"default_homepage": "Página de inicio predeterminada",
"country_selection": "Selección de países",
"country_selection": "País",
"buffering_goal": "Objetivo de amortiguación (en segundos)",
"default_quality": "Calidad predeterminada",
"audio_only": "Sólo audio",
@ -93,8 +95,8 @@
"create_playlist": "Crear una lista de reproducción",
"add_to_playlist": "Añadir a la lista de reproducción",
"delete_playlist_video_confirm": "¿Eliminar vídeo de lista de reproducción?",
"please_select_playlist": "Seleccione una lista de reproducción",
"select_playlist": "Seleccione una lista de reproducción",
"please_select_playlist": "Por favor, selecciona una lista de reproducción",
"select_playlist": "Selecciona una lista de reproducción",
"show_markers": "Mostrar Marcadores en Reproductor",
"delete_account": "Eliminar Cuenta",
"different_auth_instance": "Usar una instancia diferente para autenticación",
@ -103,7 +105,7 @@
"logout": "Cerrar sesión en este dispositivo",
"minimize_recommendations_default": "Minimizar Recomendaciones por defecto",
"invalidate_session": "Cerrar sesión en todos los dispositivos",
"instance_auth_selection": "Selección de la Instancia de Autentificación",
"instance_auth_selection": "Instancia de autenticación",
"download_as_txt": "Descargar como .txt",
"share": "Compartir",
"with_timecode": "Compartir con código de tiempo",
@ -117,7 +119,7 @@
"restore_preferences": "Restablecer las preferencias",
"back_to_home": "Volver a la página de inicio",
"show_chapters": "Capítulos",
"store_search_history": "Guardar historial de búsqueda",
"store_search_history": "Guardar el historial de las búsquedas",
"source_code": "Código fuente",
"documentation": "Documentación",
"instance_donations": "Donaciones para instancia",
@ -138,7 +140,7 @@
"skip_segment": "Saltar Segmento",
"show_less": "Mostrar menos",
"autoplay_next_countdown": "Cuenta atrás predeterminada antes del siguiente vídeo (en segundos)",
"dismiss": "Cancelar",
"dismiss": "Descartar",
"group_name": "Nombre del grupo",
"create_group": "Crear grupo",
"auto_display_captions": "Mostrar automáticamente subtítulos",
@ -148,10 +150,17 @@
"playlist_description": "Descripción de la lista de reproducción",
"cancel": "Cancelar",
"show_search_suggestions": "Mostrar sugerencias de búsqueda",
"chapters_layout_mobile": "Disposición de capítulos en móvil"
"chapters_layout_mobile": "Disposición de capítulos en móvil",
"delete_automatically": "Borrar automáticamente después de",
"enable_dearrow": "Activar DeArrow",
"generate_qrcode": "Generar código QR",
"import_from_json_csv": "Importar desde JSON/CSV",
"download_frame": "Descargar fotograma",
"instance_privacy_policy": "Política de privacidad",
"add_to_group": "Añadir a grupo"
},
"titles": {
"feed": "Fuente web",
"feed": "Contenido",
"subscriptions": "Suscripciones",
"history": "Historial",
"trending": "En tendencias",
@ -165,14 +174,18 @@
"livestreams": "Directos",
"channels": "Canales",
"bookmarks": "Marcadores",
"channel_groups": "Grupos de canales"
"channel_groups": "Grupos de canales",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "Ver en {0}"
"watch_on": "Ver en {0}",
"failed": "Falló con el código de error {0}, consulta los registros para más información"
},
"login": {
"password": "Contraseña",
"username": "Nombre de usuario"
"username": "Nombre de usuario",
"passwords_incorrect": "¡Las contraseñas no coinciden!",
"password_confirm": "Confirma la contraseña"
},
"search": {
"did_you_mean": "¿Quisiste decir {0}?",
@ -183,7 +196,8 @@
"videos": "YouTube: Vídeos",
"channels": "YouTube: Canales",
"playlists": "YouTube: Listas de reproducción",
"music_albums": "YT Music: Álbumes"
"music_albums": "YT Music: Álbumes",
"music_artists": "YT Music: Artistas"
},
"subscriptions": {
"subscribed_channels_count": "Suscrito a: {0}"
@ -195,6 +209,12 @@
"cannot_copy": "¡No se puede copiar!",
"local_storage": "Esta acción requiere «localStorage», ¿están activadas las «cookies»?",
"register_no_email_note": "No se recomienda usar un correo electrónico como nombre de usuario. ¿Continuar de todos modos?",
"next_video_countdown": "El próximo vídeo se reproducirá en {0}s"
"next_video_countdown": "El próximo vídeo se reproducirá en {0}s",
"hours": "{amount} hora(s)",
"days": "{amount} día(s)",
"weeks": "{amount} semana(s)",
"months": "{amount} mes(es)",
"login_note": "Inicia sesión con una cuenta creada en esta instancia.",
"register_note": "Registra una cuenta para esta instancia de Piped. Esto te permitirá sincronizar tus suscripciones y las listas de reproducción con tu cuenta, para que se almacenen en el servidor. Puedes utilizar todas las funciones sin una cuenta, pero todos los datos se almacenarán en la caché local de tu navegador. Asegúrate de NO utilizar una dirección de correo electrónico como nombre de usuario y elige una contraseña segura que no utilices en ningún otro sitio."
}
}

View file

@ -128,13 +128,15 @@
"okay": "OK",
"edit_playlist": "Éditer la liste de lecture",
"playlist_name": "Nom de la liste de lecture",
"auto_display_captions": "Afficer sous-titres automatiquement",
"auto_display_captions": "Afficher sous-titres automatiquement",
"dismiss": "Rejeter",
"cancel": "Annuler",
"playlist_description": "Description de la liste de lecture",
"create_group": "Créer un groupe",
"group_name": "Nom du groupe",
"autoplay_next_countdown": "Temps par défaut avant la prochaine vidéo (en secondes)"
"autoplay_next_countdown": "Temps par défaut avant la prochaine vidéo (en secondes)",
"chapters_layout_mobile": "Format des chapitres sur mobile",
"show_search_suggestions": "Afficher les suggestions de recherche"
},
"player": {
"watch_on": "Regarder sur {0}"
@ -149,7 +151,9 @@
"live": "{0} en direct",
"shorts": "Courtes",
"all": "Tout",
"category": "Catégorie"
"category": "Catégorie",
"chapters_horizontal": "Horizontal",
"chapters_vertical": "Vertical"
},
"preferences": {
"ssl_score": "Score SSL",

94
src/locales/gl.json Normal file
View file

@ -0,0 +1,94 @@
{
"titles": {
"register": "Crear conta",
"feed": "Cronoloxía",
"preferences": "Preferencias",
"history": "Historial",
"trending": "En voga",
"account": "Conta",
"player": "Reprodutor",
"login": "Acceder",
"instance": "Instancia",
"bookmarks": "Marcadores",
"subscriptions": "Subscricións",
"playlists": "Listas",
"livestreams": "En directo",
"channels": "Canles",
"channel_groups": "Grupos de canles"
},
"player": {
"watch_on": "Ver en {0}",
"failed": "Fallou con código do erro {0}, mira o rexistro para máis info"
},
"actions": {
"subscribe": "Subscribirse - {count}",
"sort_by": "Orde por:",
"least_recent": "Máis antigo",
"most_recent": "Máis recente",
"channel_name_asc": "Nome da canle (A-Z)",
"unsubscribe": "Retirar subscrición - {count}",
"view_subscriptions": "Ver Subscricións",
"back": "Volver",
"uses_api_from": "Usa a API desde ",
"enable_sponsorblock": "Activar Sponsoblock",
"skip_button_only": "Mostrar botón omitir",
"skip_automatically": "Automáticamente",
"channel_name_desc": "Nome da canle (Z-A)",
"skip_sponsors": "Omitir Sponsors",
"show_markers": "Mostrar Marcadores no Reprodutor",
"skip_segment": "Omitir Segmento",
"dark": "Escuro",
"min_segment_length": "Lonxitude mínima do segmento (en segundos)",
"theme": "Decorado",
"auto": "Auto",
"light": "Claro",
"autoplay_video": "Reprodución automática",
"buffering_goal": "Tamaño do buffer (en segundos)",
"minimize_comments_default": "Por defecto minimizar os comentarios",
"import_from_json_csv": "Importar desde JSON/CSV",
"skip_outro": "Omitir créditos finais",
"skip_intro": "Omitir animación de entrada",
"auto_play_next_video": "Reproducir autom. seguinte vídeo",
"instance_selection": "Instancia",
"clear_history": "Limpar historial",
"loading": "A cargar...",
"minimize_description": "Minimizar descrición",
"skip_interaction": "Omitir Recordatorio para interactuar (Subscribirse)",
"filter": "Filtro",
"view_ssl_score": "Ver SSL Score",
"minimize_description_default": "Por defecto minimizar a descrición",
"language_selection": "Idioma",
"enable_lbry_proxy": "Activar Proxy para LBRY",
"donations": "Doar para o desenvolvemento",
"loop_this_video": "Poñer vídeo en bucle",
"hide_replies": "Agochar respostas",
"country_selection": "País",
"skip_self_promo": "Omitir autobombo",
"default_quality": "Calidade por defecto",
"show_more": "Mostrar máis",
"show_recommendations": "Mostrar recomendacións",
"minimize_recommendations": "Minimizar recomendacións",
"audio_only": "Só audio",
"disable_lbry": "Desactivar LBRY para Retransmisión",
"show_comments": "Mostrar comentarios",
"store_watch_history": "Gardar historial de visualizacións",
"no": "Non",
"yes": "Si",
"load_more_replies": "Cargar máis respostas",
"enabled_codecs": "Códecs activados (varios)",
"auto_display_captions": "Mostar autom. Subtítulos",
"minimize_comments": "Minimizar comentarios",
"skip_non_music": "Omitir música: Sección sen música",
"instances_list": "Lista de instancias",
"import_from_json": "Importar desde JSON",
"autoplay_next_countdown": "Conta atrás por defecto para o seguinte vídeo (en segundos)",
"default_homepage": "Inicio por defecto",
"remove_from_playlist": "Retirar da lista",
"search": "Buscar (Ctrl+K)",
"show_description": "Mostrar descrición",
"skip_preview": "Omitir vista previa/resumo",
"skip_highlight": "Omitir Destacado",
"export_to_json": "Exportar a JSON",
"add_to_playlist": "Engadir á lista"
}
}

View file

@ -14,10 +14,12 @@
"livestreams": "שידורים חיים",
"channels": "ערוצים",
"bookmarks": "סימניות",
"channel_groups": "קבוצות ערוץ"
"channel_groups": "קבוצות ערוץ",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "לצפות ב־{0}"
"watch_on": "הצגה ב־{0}",
"failed": "חל כשל עם קוד שגיאה {0}, מידע נוסף ביומנים"
},
"actions": {
"subscribe": "מינוי - {count}",
@ -45,20 +47,20 @@
"audio_only": "שמע בלבד",
"default_quality": "איכות ברירת מחדל",
"buffering_goal": "יעד שמירה למטמון (בשניות)",
"country_selection": "בחירת מדינה",
"country_selection": "מדינה",
"default_homepage": "עמוד הבית כברירת מחדל",
"show_comments": "הצגת תגובות",
"minimize_description_default": "מזעור התגובות כברירת מחדל",
"store_watch_history": "שחזור היסטוריית הצפייה",
"language_selection": "בחירת שפה",
"language_selection": "שפה",
"instances_list": "רשימת עותקים",
"enabled_codecs": "מפענחים פעילים (מגוון)",
"instance_selection": "בחירת עותק",
"instance_selection": "עותק",
"show_more": "להציג יותר",
"yes": "כן",
"no": "לא",
"export_to_json": "ייצוא ל־JSON",
"import_from_json": "ייבוא מ־JSON/CSV",
"import_from_json": "ייבוא מ־JSON",
"show_markers": "הצגת סמנים בנגן",
"auto_play_next_video": "לנגן את הסרטון הבא אוטומטית",
"donations": "תרומות בפיתוח",
@ -79,7 +81,7 @@
"logout": "יציאה מהחשבון במכשיר הזה",
"minimize_recommendations_default": "מזעור המלצות כברירת מחדל",
"invalidate_session": "להוציא את כל המכשירים מהחשבון",
"instance_auth_selection": "בחירת עותק אימות",
"instance_auth_selection": "עותק אימות",
"clone_playlist": "שכפול רשימת נגינה",
"clone_playlist_success": "שוכפל בהצלחה!",
"download_as_txt": "הורדה כ־‎.txt",
@ -139,7 +141,14 @@
"playlist_description": "תיאור רשימת הנגינה",
"okay": "אישור",
"show_search_suggestions": "הצגת הצעות חיפוש",
"chapters_layout_mobile": "פריסת פרקים בנייד"
"chapters_layout_mobile": "פריסת פרקים בנייד",
"delete_automatically": "למחוק אוטומטית לאחר",
"enable_dearrow": "הפעלת DeArrow",
"generate_qrcode": "יצירת קוד QR",
"import_from_json_csv": "ייבוא מ־JSON/CSV",
"download_frame": "הורדת תמונית",
"instance_privacy_policy": "מדיניות פרטיות",
"add_to_group": "הוספה לקבוצה"
},
"comment": {
"pinned_by": "ננעץ על ידי {author}",
@ -158,7 +167,9 @@
},
"login": {
"username": "שם משתמש",
"password": "סיסמה"
"password": "סיסמה",
"password_confirm": "אישור סיסמה",
"passwords_incorrect": "הסיסמאות שונות זו מזו!"
},
"video": {
"videos": "סרטונים",
@ -172,7 +183,9 @@
"all": "הכול",
"category": "קטגוריה",
"chapters_horizontal": "אופקית",
"chapters_vertical": "אנכית"
"chapters_vertical": "אנכית",
"visibility": "חשיפה",
"license": "רישיון"
},
"search": {
"did_you_mean": "האם התכוונת לביטוי {0}?",
@ -183,7 +196,8 @@
"music_songs": "YT Music: שירים",
"music_videos": "YT Music: סרטונים",
"music_albums": "YT Music: אלבומים",
"music_playlists": "YT Music: רשימות נגינה"
"music_playlists": "YT Music: רשימות נגינה",
"music_artists": "YT Music: אומנים"
},
"info": {
"preferences_note": "לתשומת לבך: ההעדפות נשמרות באחסון המקומי של הדפדפן שלך. מחיקת נתוני הדפדפן שלך תאפס אותם.",
@ -192,7 +206,13 @@
"cannot_copy": "לא ניתן להעתיק!",
"local_storage": "פעולה זו דורשת אחסון מקומי (localStorage), האם עוגיות פעילות?",
"register_no_email_note": "לא מומלץ להשתמש בכתובת דוא״ל כשם משתמש. להמשיך בכל זאת?",
"next_video_countdown": "הסרטון הבא יתנגן בעוד {0} שניות"
"next_video_countdown": "הסרטון הבא יתנגן בעוד {0} שניות",
"days": "{amount} ימים",
"weeks": "{amount} שבועות",
"months": "{amount} חודשים",
"hours": "{amount} שעות",
"login_note": "כניסה עם חשבון שנוצר בעותק הזה.",
"register_note": "הרשמה ל־Piped הזה. תאפשר לך לסנכרן את המינויים ואת רשימות הנגינה שלך עם החשבון שלך כך שיאוחסנו בצד השרת. אפשר להשתמש בכל התכונות בלי חשבון אך כל הנתונים יאוחסנו במטמון המקומי של הדפדפן שלך. נא לוודא שלא בחרת בכתובת דוא״ל כשם המשתמש שלך ורצוי לבחור בסיסמה מאובטחת שלא משמשת אותך בשום מקום אחר."
},
"subscriptions": {
"subscribed_channels_count": "נרשמת אל: {0}"

View file

@ -11,7 +11,8 @@
"livestreams": "लाइव स्ट्रीम",
"channels": "चैनल",
"player": "चालक",
"account": "खाता"
"account": "खाता",
"instance": "इंस्टैंस"
},
"actions": {
"subscribe": "सदस्यता लें - {count}",
@ -19,7 +20,7 @@
"unsubscribe": "सदस्यता ले ली है - {count}",
"no": "नहीं",
"hide_replies": "जवाब छिपाएं",
"search": "खोजें",
"search": "खोजें (Ctrl+K)",
"loop_this_video": "इस वीडियो को लूप करें",
"loading": "लोड हो रहा है...",
"show_description": "विवरण दिखाएं",
@ -40,17 +41,17 @@
"autoplay_video": "ऑटोप्ले वीडियो",
"audio_only": "सिर्फ़ ध्वनि",
"default_quality": "डिफ़ॉल्ट गुणवत्ता",
"country_selection": "देश चयन",
"country_selection": "देश",
"show_comments": "टिप्पणियाँ दिखाएँ",
"store_watch_history": "स्टोर देखने का इतिहास",
"language_selection": "भाषा चयन",
"language_selection": "भाषा",
"instances_list": "इंस्टेंस सूची",
"instance_selection": "इंस्टेंस चयन",
"instance_selection": "इंस्टेंस",
"show_more": "और दिखाओ",
"export_to_json": "JSON में निर्यात करें",
"import_from_json": "JSON/CSV से आयात करें",
"auto_play_next_video": "अगला वीडियो ऑटोप्ले करें",
"donations": "दान",
"donations": "विकास दान",
"minimize_recommendations": "सिफारिशों को कम करें",
"show_recommendations": "सिफारिशें दिखाएं",
"disable_lbry": "स्ट्रीमिंग के लिए LBRY अक्षम करें",
@ -61,10 +62,10 @@
"load_more_replies": "और जवाब लोड करें",
"enabled_codecs": "सक्षम कोडेक्स (एकाधिक)",
"buffering_goal": "बफरिंग गोल (सेकंड में)",
"delete_playlist_confirm": "क्या आप वाकई इस प्लेलिस्ट को हटाना चाहते हैं?",
"delete_playlist_confirm": "प्लेलिस्ट को मिटाना है?",
"add_to_playlist": "प्लेलिस्ट में जोड़ें",
"remove_from_playlist": "प्लेलिस्ट से निकाले",
"delete_playlist_video_confirm": "क्या आप वाकई इस प्लेलिस्ट से इस वीडियो को निकालना चाहेंगे?",
"delete_playlist_video_confirm": "वीडियो को प्लेलिस्ट से निकालना है?",
"create_playlist": "प्लेलिस्ट बनायें",
"select_playlist": "एक प्लेलिस्ट चुनें",
"please_select_playlist": "कृपया एक प्लेलिस्ट चुनें",
@ -72,12 +73,13 @@
"enable_sponsorblock": "विज्ञापन प्रतिबंध करें",
"default_homepage": "स्वतः निर्धारित मुख्यपृष्ठ",
"sort_by": "वर्गीकरण:",
"skip_automatically": "स्वतः"
"skip_automatically": "स्वतः",
"delete_account": "खाता मिटाएँ"
},
"video": {
"views": "{views} बार देखा गया",
"videos": "वीडियो",
"watched": "पहले ही देखा",
"watched": "पहले ही देखा हुआ",
"ratings_disabled": "रेटिंग अक्षम",
"chapters": "चैप्टर",
"live": "{0} लाइव"

View file

@ -9,7 +9,11 @@
"live": "{0} uživo",
"shorts": "Kratka videa",
"all": "Sva",
"category": "Kategorija"
"category": "Kategorija",
"chapters_horizontal": "Vodoravno",
"chapters_vertical": "Okomito",
"license": "Licenca",
"visibility": "Vidljivost"
},
"preferences": {
"ssl_score": "SSL ocjena",
@ -37,19 +41,19 @@
"donations": "Donacije za razvoj",
"auto_play_next_video": "Automatski reproduciraj idući video",
"loop_this_video": "Ponavljaj ovaj video",
"import_from_json": "Uvezi iz JSON/CSV formata",
"import_from_json": "Uvezi iz JSON formata",
"export_to_json": "Izvezi u JSON",
"no": "Ne",
"yes": "Da",
"show_more": "Prikaži više",
"instance_selection": "Izbor instance",
"instance_selection": "Instanca",
"enabled_codecs": "Uključeni kodeki (moguće je odabrati nekoliko kodeka)",
"instances_list": "Popis instanci",
"language_selection": "Izbor jezika",
"language_selection": "Jezik",
"store_watch_history": "Spremi povijest gledanja",
"show_comments": "Prikaži komentare",
"default_homepage": "Standardna početna stranica",
"country_selection": "Izbor zemlje",
"country_selection": "Zemlja",
"buffering_goal": "Cilj međuspremnika (u sekundama)",
"default_quality": "Standardna kvaliteta",
"audio_only": "Samo zvuk",
@ -99,7 +103,7 @@
"minimize_recommendations_default": "Standardno sakrij preporuke",
"invalidate_session": "Odjavi sve uređaje",
"different_auth_instance": "Koristi drugu instancu za autentifikaciju",
"instance_auth_selection": "Odabir instance autentifikacije",
"instance_auth_selection": "Instanca autentifikacije",
"clone_playlist": "Dupliciraj playlistu",
"clone_playlist_success": "Dupliciranje uspjelo!",
"download_as_txt": "Preuzmi kao .txt",
@ -118,7 +122,7 @@
"documentation": "Dokumentacija",
"source_code": "Izvorni kod",
"instance_donations": "Donacije instance",
"store_search_history": "Spremi povijest pretrage",
"store_search_history": "Povijest pretrage trgovine",
"hide_watched": "Sakrij gledana videa u novostima",
"status_page": "Stanje",
"reply_count": "{count} odgovora",
@ -144,10 +148,20 @@
"okay": "U redu",
"edit_playlist": "Uredi playlistu",
"playlist_name": "Ime playliste",
"playlist_description": "Opis playliste"
"playlist_description": "Opis playliste",
"chapters_layout_mobile": "Raspored poglavlja na mobilnim uređajima",
"show_search_suggestions": "Prikaži prijedloge pretrage",
"delete_automatically": "Automatski izbriši nakon",
"enable_dearrow": "Aktiviraj DeArrow",
"generate_qrcode": "Generiraj QR kod",
"import_from_json_csv": "Uvezi iz JSON/CSV formata",
"download_frame": "Preuzmi kadar",
"instance_privacy_policy": "Politika privatnosti",
"add_to_group": "Dodaj grupi"
},
"player": {
"watch_on": "Gledaj na {0}"
"watch_on": "Gledaj na {0}",
"failed": "Neuspjelo s greškom {0}. Pogledaj zapise za više informacija"
},
"titles": {
"subscriptions": "Pretplate",
@ -164,11 +178,14 @@
"channels": "Kanali",
"livestreams": "Prijenosi uživo",
"bookmarks": "Zabilješke",
"channel_groups": "Grupe kanala"
"channel_groups": "Grupe kanala",
"dearrow": "DeArrow"
},
"login": {
"password": "Lozinka",
"username": "Korisničko ime"
"username": "Korisničko ime",
"password_confirm": "Potvrdi lozinku",
"passwords_incorrect": "Lozinke se ne poklapaju!"
},
"search": {
"did_you_mean": "Misliš li: {0}?",
@ -179,7 +196,8 @@
"music_songs": "YT Music: Pjesme",
"music_videos": "YT Music: Videa",
"music_albums": "YT Music: Albumi",
"music_playlists": "YT Music: Playliste"
"music_playlists": "YT Music: Playliste",
"music_artists": "YT Music: Izvođači"
},
"subscriptions": {
"subscribed_channels_count": "Broj pretplata: {0}"
@ -194,6 +212,12 @@
"cannot_copy": "Nije moguće kopirati!",
"local_storage": "Ova radnja zahtijeva lokalno spremište. Jesu li kolačići uključeni?",
"register_no_email_note": "Korištenje e-mail adrese kao korisničkog imena se ne preporučuje. Svejedno nastaviti?",
"next_video_countdown": "Reprodukcija sljedećeg videa za {0} s"
"next_video_countdown": "Reprodukcija sljedećeg videa za {0} s",
"hours": "{amount} h",
"days": "{amount} dan(a)",
"weeks": "{amount} tj",
"months": "{amount} mj",
"login_note": "Prijavi se s računom stvorenim na ovoj instanci.",
"register_note": "Registriraj račun za ovu Piped instancu. To će ti omogućiti sinkronizaciju tvojih pretplata i playlista s tvojim računom kako bi se spremile na poslužitelju. Možeš koristiti sve značajke bez računa, ali će se svi podaci spremiti u lokalnu predmemoriju tvog preglednika. NEMOJ koristiti e-mail adresu kao korisničko ime i odaberi sigurnu lozinku koju ne koristiš negdje drugdje."
}
}

View file

@ -14,10 +14,12 @@
"livestreams": "Siaran Langsung",
"channels": "Saluran",
"bookmarks": "Markah",
"channel_groups": "Grup saluran"
"channel_groups": "Grup saluran",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "Tonton di {0}"
"watch_on": "Lihat di {0}",
"failed": "Gagal dengan kode kesalahan {0}, lihat catatan untuk info lebih lanjut"
},
"actions": {
"subscribe": "Berlangganan - {count}",
@ -43,18 +45,18 @@
"audio_only": "Audio Saja",
"default_quality": "Kualitas Bawaan",
"buffering_goal": "Tujuan Buffering (dalam detik)",
"country_selection": "Pemilihan Negara",
"country_selection": "Negara",
"default_homepage": "Halaman Beranda Bawaan",
"show_comments": "Tampilkan Komentar",
"minimize_description_default": "Kecilkan Deskripsi secara default",
"language_selection": "Pemilihan Bahasa",
"language_selection": "Bahasa",
"instances_list": "Daftar Instansi",
"enabled_codecs": "Kodek yang Diaktifkan (Beberapa)",
"instance_selection": "Pemilihan Instansi",
"instance_selection": "Instansi",
"show_more": "Tampilkan Lebih Banyak",
"yes": "Iya",
"no": "Tidak",
"import_from_json": "Impor dari JSON/CSV",
"import_from_json": "Impor dari JSON",
"loop_this_video": "Ulangi Video ini",
"auto_play_next_video": "Mainkan video berikutnya secara otomatis",
"donations": "Donasi pengembangan",
@ -91,7 +93,7 @@
"logout": "Keluar dari perangkat ini",
"minimize_recommendations_default": "Kecilkan Rekomendasi secara bawaan",
"invalidate_session": "Keluarkan semua perangkat",
"instance_auth_selection": "Pemilihan Instansi Otentikasi",
"instance_auth_selection": "Instance Otentikasi",
"different_auth_instance": "Gunakan instansi lain untuk otentikasi",
"clone_playlist_success": "Berhasil disalin!",
"clone_playlist": "Salin Daftar Putar",
@ -138,7 +140,15 @@
"edit_playlist": "Sunting daftar putar",
"playlist_name": "Nama daftar putar",
"okay": "Oke",
"show_search_suggestions": "Tampilkan saran pencarian"
"show_search_suggestions": "Tampilkan saran pencarian",
"chapters_layout_mobile": "Tata Letak Bab di Ponsel",
"delete_automatically": "Hapus secara otomatis setelah",
"enable_dearrow": "Aktifkan DeArrow",
"generate_qrcode": "Buat Kode QR",
"import_from_json_csv": "Impor dari JSON/CSV",
"download_frame": "Unduh bingkai",
"instance_privacy_policy": "Kebijakan Privasi",
"add_to_group": "Tambahkan ke grup"
},
"comment": {
"pinned_by": "Dipasangi pin oleh {author}",
@ -157,7 +167,9 @@
},
"login": {
"username": "Nama Pengguna",
"password": "Kata Sandi"
"password": "Kata Sandi",
"password_confirm": "Konfirmasi kata sandi",
"passwords_incorrect": "Kata sandi tidak cocok!"
},
"video": {
"videos": "Video",
@ -169,7 +181,11 @@
"live": "{0} Langsung",
"shorts": "Shorts",
"all": "Semua",
"category": "Kategori"
"category": "Kategori",
"chapters_horizontal": "Horisontal",
"chapters_vertical": "Vertikal",
"license": "Lisensi",
"visibility": "Visibilitas"
},
"search": {
"did_you_mean": "Apakah Anda bermaksud: {0}?",
@ -180,7 +196,8 @@
"music_videos": "YT Music: Video",
"music_albums": "YT Music: Album",
"music_playlists": "YT Music: Daftar Putar",
"all": "YouTube: Semua"
"all": "YouTube: Semua",
"music_artists": "YT Music: Artis"
},
"subscriptions": {
"subscribed_channels_count": "Berlangganan ke: {0}"
@ -195,6 +212,12 @@
"cannot_copy": "Tidak dapat menyalin!",
"local_storage": "Tindakan ini membutuhkan localStorage, apakah kuki diaktifkan?",
"register_no_email_note": "Menggunakan surel sebagai nama pengguna tidak disarankan. Lanjut?",
"next_video_countdown": "Memutar video berikutnya dalam {0} detik"
"next_video_countdown": "Memutar video berikutnya dalam {0} detik",
"weeks": "{amount} minggu",
"hours": "{amount} jam",
"days": "{amount} hari",
"months": "{amount} bulan",
"login_note": "Masuk dengan akun yang dibuat di server ini.",
"register_note": "Daftarkan akun untuk server Piped ini. Ini akan memungkinkan Anda untuk menyinkronkan langganan dan daftar putar Anda dengan akun Anda, sehingga mereka disimpan di sisi server. Anda dapat menggunakan semua fitur tanpa akun, tetapi semua data akan disimpan di tembolok lokal browser Anda. Pastikan Anda TIDAK menggunakan alamat surel sebagai nama pengguna Anda dan pilih kata sandi yang aman yang tidak Anda gunakan di tempat lain."
}
}

View file

@ -1,12 +1,12 @@
{
"actions": {
"instances_list": "Elenco delle istanze",
"language_selection": "Selezione della lingua",
"language_selection": "Lingua",
"store_watch_history": "Conserva la cronologia di visualizzazione",
"minimize_description_default": "Minimizza la descrizione per impostazione predefinita",
"show_comments": "Mostra i commenti",
"default_homepage": "Pagina iniziale predefinita",
"country_selection": "Selezione del paese",
"country_selection": "Paese",
"buffering_goal": "Obiettivo di buffering (in secondi)",
"default_quality": "Qualità predefinita",
"audio_only": "Solo audio",
@ -37,7 +37,7 @@
"enable_lbry_proxy": "Abilita il proxy per LBRY",
"disable_lbry": "Disabilita LBRY per lo streaming",
"show_description": "Mostra la descrizione",
"minimize_description": "Minimizza la descrizione per impostazione predefinita",
"minimize_description": "Minimizza descrizione",
"minimize_recommendations": "Minimizza consigli",
"show_recommendations": "Mostra consigli",
"donations": "Donazioni per lo sviluppo",
@ -48,7 +48,7 @@
"no": "No",
"yes": "Sì",
"show_more": "Mostra di più",
"instance_selection": "Selezione dell'istanza",
"instance_selection": "Istanza",
"loading": "Caricamento…",
"filter": "Filtra",
"search": "Cerca (Ctrl+K)",
@ -72,7 +72,7 @@
"minimize_recommendations_default": "Riduci al minimo le raccomandazioni per impostazione predefinita",
"different_auth_instance": "Usa un'istanza diversa per l'autenticazione",
"invalidate_session": "Disconnetti su tutti i dispositivi",
"instance_auth_selection": "Selezione dell'istanza di autenticazione",
"instance_auth_selection": "Istanza di autenticazione",
"clone_playlist_success": "Clonato con successo!",
"clone_playlist": "Clona la playlist",
"download_as_txt": "Scarica come .txt",
@ -116,7 +116,8 @@
"playlist_description": "Descrizione playlist",
"group_name": "Nome gruppo",
"create_group": "Crea gruppo",
"show_search_suggestions": "Mostra suggerimenti di ricerca"
"show_search_suggestions": "Mostra suggerimenti di ricerca",
"dismiss": "Chiudi"
},
"player": {
"watch_on": "Guarda su {0}"
@ -135,7 +136,8 @@
"player": "Riproduttore",
"livestreams": "Streaming live",
"channels": "Canali",
"bookmarks": "Segnalibri"
"bookmarks": "Segnalibri",
"dearrow": "DeArrow"
},
"video": {
"sponsor_segments": "Segmenti sponsor",
@ -147,7 +149,10 @@
"chapters": "Capitoli",
"shorts": "Short",
"all": "Tutti",
"category": "Categoria"
"category": "Categoria",
"chapters_horizontal": "Orizzontale",
"chapters_vertical": "Verticale",
"license": "Licenza"
},
"preferences": {
"ssl_score": "Valutazione SSL",
@ -177,7 +182,8 @@
"channels": "YouTube: Canali",
"music_songs": "YT Music: Canzoni",
"all": "YouTube: Tutto",
"videos": "YouTube: Video"
"videos": "YouTube: Video",
"music_artists": "YT Music: Artisti"
},
"subscriptions": {
"subscribed_channels_count": "Abbonato a: {0}"

View file

@ -14,10 +14,12 @@
"channels": "チャンネル",
"livestreams": "ライブ配信",
"bookmarks": "ブックマーク",
"channel_groups": "グループ"
"channel_groups": "グループ",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "{0}で視聴"
"watch_on": "{0}で視聴",
"failed": "失敗しエラーコード {0} が返りました。詳細はログに記録"
},
"actions": {
"subscribe": "チャンネル登録 - {count}",
@ -30,7 +32,7 @@
"channel_name_desc": "チャンネル名 (ZからA)",
"back": "戻る",
"uses_api_from": "API 提供元 ",
"enable_sponsorblock": "SponsorBlock を有効化",
"enable_sponsorblock": "SponsorBlock を使用",
"skip_sponsors": "広告をスキップ",
"skip_intro": "合間/導入アニメをスキップ",
"skip_outro": "終了シーン/クレジットをスキップ",
@ -46,20 +48,20 @@
"audio_only": "音声のみのモード",
"default_quality": "標準の画質",
"buffering_goal": "バッファリング目標値 (秒)",
"country_selection": "国の選択",
"country_selection": "国",
"default_homepage": "ホームに表示するページ",
"show_comments": "コメントを表示",
"minimize_description_default": "最初から説明を最小化",
"store_watch_history": "再生履歴を保存",
"language_selection": "言語の選択",
"language_selection": "言語",
"instances_list": "インスタンス一覧",
"enabled_codecs": "コーデックの有効化 (複数選択)",
"instance_selection": "インスタンスを選択",
"instance_selection": "インスタンス",
"show_more": "もっと見る",
"yes": "はい",
"no": "いいえ",
"export_to_json": "JSONに出力",
"import_from_json": "JSON/CSVを読み込む",
"import_from_json": "JSONから読み込む",
"loop_this_video": "この動画をループ再生",
"auto_play_next_video": "次の動画を自動再生",
"donations": "開発者に寄付",
@ -95,8 +97,8 @@
"instance_donations": "インスタンスに寄付",
"minimize_comments": "コメントを最小化",
"share": "共有",
"with_timecode": "時間指定で共有",
"different_auth_instance": "認証には別のインスタンスを使う",
"with_timecode": "時間指定",
"different_auth_instance": "認証に別のインスタンスを使用",
"download_as_txt": ".txtでダウンロード",
"logout": "この端末からログアウト",
"minimize_recommendations_default": "最初からおすすめを最小化",
@ -104,12 +106,12 @@
"minimize_chapters_default": "最初からチャプターを最小化",
"show_watch_on_youtube": "「YouTubeで視聴」ボタンを表示",
"invalidate_session": "すべての端末からログアウト",
"instance_auth_selection": "認証用インスタンスの選択",
"instance_auth_selection": "認証用インスタンス",
"clone_playlist_success": "複製に成功しました!",
"backup_preferences": "設定をバックアップ",
"restore_preferences": "設定を復元",
"back_to_home": "ホームに戻る",
"copy_link": "リンクコピー",
"copy_link": "リンクコピー",
"time_code": "タイムコード (秒)",
"documentation": "ドキュメント",
"reset_preferences": "設定を初期化",
@ -129,7 +131,7 @@
"min_segment_length": "最小の区切りの長さ (秒)",
"show_less": "少なく見る",
"autoplay_next_countdown": "次の動画の再生までの時間 (秒)",
"dismiss": "無視",
"dismiss": "却下",
"create_group": "グループ作成",
"group_name": "グループ名",
"auto_display_captions": "自動で字幕を表示",
@ -139,7 +141,13 @@
"playlist_name": "再生リスト名",
"playlist_description": "再生リストの説明",
"chapters_layout_mobile": "モバイル端末でのチャプターの配置",
"show_search_suggestions": "検索語句の候補を表示"
"show_search_suggestions": "検索語句の候補を表示",
"delete_automatically": "指定時間後に自動削除",
"enable_dearrow": "DeArrow を使用",
"generate_qrcode": "QRコードの生成",
"import_from_json_csv": "JSON/CSVから読み込む",
"download_frame": "この画像を保存",
"instance_privacy_policy": "個人情報保護方針"
},
"comment": {
"pinned_by": "{author} によって固定",
@ -158,13 +166,15 @@
},
"login": {
"username": "ユーザー名",
"password": "パスワード"
"password": "パスワード",
"password_confirm": "パスワードの確認",
"passwords_incorrect": "パスワードが一致しません!"
},
"video": {
"videos": "動画",
"views": "{views} 回再生",
"watched": "再生済み",
"sponsor_segments": "スポンサーによる広告",
"sponsor_segments": "広告シーン数",
"ratings_disabled": "評価は無効化されています",
"chapters": "チャプター",
"live": "{0} ライブ配信",
@ -172,7 +182,9 @@
"all": "すべて",
"category": "分類",
"chapters_horizontal": "横方向",
"chapters_vertical": "縦方向"
"chapters_vertical": "縦方向",
"visibility": "公開状態",
"license": "ライセンス"
},
"search": {
"did_you_mean": "もしかして: {0}",
@ -183,7 +195,8 @@
"music_songs": "YT Music: 音楽",
"music_videos": "YT Music: 動画",
"music_albums": "YT Music: アルバム",
"music_playlists": "YT Music: 再生リスト"
"music_playlists": "YT Music: 再生リスト",
"music_artists": "YT Music: アーティスト"
},
"info": {
"page_not_found": "ページが見つかりません",
@ -192,7 +205,11 @@
"preferences_note": "注意: 設定は、お使いのブラウザの保存領域に保存されます。ブラウザのデータを削除すると初期化されます。",
"local_storage": "この操作にはlocalStorageが必要です。Cookieは有効ですか",
"register_no_email_note": "ユーザー名としてのメールアドレスの使用は推奨しません。それでも続けますか?",
"next_video_countdown": "{0} 秒後に次の動画"
"next_video_countdown": "{0} 秒後に次の動画",
"days": "{amount}日",
"weeks": "{amount}週",
"hours": "{amount}時間",
"months": "{amount}月"
},
"subscriptions": {
"subscribed_channels_count": "チャンネル登録: {0}"

View file

@ -8,7 +8,8 @@
"login": "പ്രവേശിക്കുക",
"subscriptions": "സബ്സ്ക്രിപ്ഷനുകൾ",
"playlists": "പ്ലേലിസ്റ്റുകൾ",
"player": "കളിക്കാരൻ"
"player": "കളിക്കാരൻ",
"account": "അക്കോണട്"
},
"actions": {
"view_subscriptions": "സബ്സ്ക്രിപ്ഷനുകൾ കാണുക",
@ -55,7 +56,7 @@
"yes": "അതെ",
"show_more": "കൂടുതൽ കാണിക്കുക",
"buffering_goal": "ബഫറിംഗ് ലക്ഷ്യം(സെക്കൻഡുകളിൽ)",
"import_from_json": "JSON/CSV നിന്ന് ഇറക്കുമതി ചെയ്യൂ",
"import_from_json": "JSONിൽ നിന്ന് ഇറക്കുമതി ചെയ്യൂ",
"export_to_json": "JSON-ലേക്ക് എക്സ്പ്പോർട്ട് ചെയ്യുക",
"instance_selection": "ഇൻസ്റ്റ്ൻസ് തിരഞ്ഞെടുക്കുക",
"loading": "ലഭ്യമാക്കുന്നു...",
@ -74,16 +75,32 @@
"please_select_playlist": "ഒരു പ്ലേലിസ്റ്റ് തിരഞ്ഞെടുക്കുക",
"delete_playlist_video_confirm": "ഈ പ്ലേലിസ്റ്റിൽ നിന്ന് ഈ വീഡിയോ ഒഴിവാക്കാൻ നിങ്ങൾ ആഗ്രഹിക്കുന്നുണ്ടോ?",
"delete_playlist_confirm": "ഈ പ്ലേലിസ്റ്റ് ഇല്ലാതാക്കാൻ നിങ്ങൾ ആഗ്രഹിക്കുന്നുണ്ടൊ?",
"skip_automatically": "സ്വയമേവ"
"skip_automatically": "സ്വയമേവ",
"show_watch_on_youtube": "YouTubeിൽ കാണാം എന്ന button കാണിക്കുകാ",
"time_code": "സമയമായം (സെക്കന്റുകളിൽ)",
"show_less": "കുറച്ച്മാതറം",
"with_timecode": "സമയമായവുമായി പങ്കുവെക്കുക",
"back_to_home": "തിരിച്ച് വീട്ടിലേക്ക്",
"dismiss": "തിരിച്ച്",
"create_group": "കൂട്ടം ഉണടാക്കുകാ",
"okay": "ശരി",
"invalidate_session": "എല ടിവയഡിൽ നിന്നും ലൊഗൗഠ അവുകാ",
"enabled_codecs": "ഉപയോഗിക്കുന്ന കോടെക്കൃകൽ",
"share": "പങ്കുവെക്കുക",
"documentation": "സഹായക്കുറിപ്പുകൽ",
"cancel": "റദ്ദാക്കുക",
"generate_qrcode": "QR Code ഉണ്ടാക്കുക",
"import_from_json_csv": "JSON/CSVിൽ നിന്ന് ഇറക്കുമതി ചെയ്യൂ"
},
"player": {
"watch_on": "കാണുക {0}"
"watch_on": "{0}ിൻ കാണൃകാ"
},
"video": {
"watched": "കണ്ടതാണ്",
"views": "{views} കാഴ്ചകൾ",
"videos": "വിഡിയോകൾ",
"sponsor_segments": "സ്പോൺസർമാരുടെ ഭാഗങ്ങൾ"
"sponsor_segments": "സ്പോൺസർമാരുടെ ഭാഗങ്ങൾ",
"all": "എല്ലാം"
},
"comment": {
"pinned_by": "പിൻ ചെയ്തിരിക്കുന്നത് {author}"
@ -98,5 +115,8 @@
"login": {
"password": "രഹസ്യവാക്ക്",
"username": "ഉപയോക്തൃനാമം"
},
"search": {
"did_you_mean": "{0} ആണൊ ഉദേശിച്ചെ?"
}
}

View file

@ -1,13 +1,13 @@
{
"actions": {
"skip_sponsors": "Sponsors Overslaan",
"skip_outro": "Eindkaarten/Credits Overslaan",
"add_to_playlist": "Aan Afspeellijst Toevoegen",
"sort_by": "Sorteer op:",
"skip_sponsors": "Sponsors overslaan",
"skip_outro": "Eindkaarten/credits overslaan",
"add_to_playlist": "Toevoegen aan afspeellijst",
"sort_by": "Sorteren op:",
"buffering_goal": "Bufferdoel (in seconden)",
"country_selection": "Land Selectie",
"show_recommendations": "Aanbevelingen Weergeven",
"disable_lbry": "LBRY voor Streamen Uitschakelen",
"country_selection": "Land",
"show_recommendations": "Aanbevelingen tonen",
"disable_lbry": "LBRY voor streamen uitschakelen",
"enable_lbry_proxy": "Proxy voor LBRY Inschakelen",
"view_ssl_score": "SSL-score Bekijken",
"search": "Zoeken (Ctrl+K)",
@ -16,68 +16,68 @@
"theme": "Thema",
"subscribe": "Abonneren - {count}",
"skip_non_music": "Muziek Overslaan: Niet-muzieksectie",
"show_comments": "Toon Reacties",
"skip_self_promo": "Onbetaalde/Zelf-promotie Overslaan",
"show_comments": "Opmerkingen tonen",
"skip_self_promo": "Onbetaalde-/zelfpromotie overslaan",
"skip_highlight": "Markering Overslaan",
"skip_interaction": "Interactieherinnering Overslaan (Abonneren)",
"show_more": "Toon meer",
"skip_interaction": "Interactieherinnering overslaan (abonneren)",
"show_more": "Meer tonen",
"unsubscribe": "Afmelden - {count}",
"view_subscriptions": "Abonnementen Bekijken",
"enable_sponsorblock": "Sponsorblok Inschakelen",
"skip_preview": "Voorbeeld/Samenvatting Overslaan",
"view_subscriptions": "Abonnementen bekijken",
"enable_sponsorblock": "Sponsorblok inschakelen",
"skip_preview": "Voorbeschouwing/samenvatting overslaan",
"auto": "Auto",
"dark": "Donker",
"light": "Licht",
"default_quality": "Standaard Kwaliteit",
"loop_this_video": "Deze Video Herhalen",
"loop_this_video": "Deze video herhalen",
"donations": "Ontwikkelingsdonaties",
"minimize_description": "Beschrijving Minimaliseren",
"show_description": "Toon Beschrijving",
"minimize_recommendations": "Aanbevelingen Minimaliseren",
"most_recent": "Meest Recente",
"least_recent": "Minst Recente",
"minimize_description": "Omschrijving minimaliseren",
"show_description": "Omschrijving tonen",
"minimize_recommendations": "Aanbevelingen minimaliseren",
"most_recent": "Meest recent",
"least_recent": "Minst recent",
"channel_name_asc": "Kanaalnaam (A-Z)",
"channel_name_desc": "Kanaalnaam (Z-A)",
"back": "Terug",
"uses_api_from": "Gebruikt de API van ",
"skip_intro": "Pauze/Intro-animatie Overslaan",
"uses_api_from": "API gebruiken van ",
"skip_intro": "Onderbrekings-/intro-animatie overslaan",
"autoplay_video": "Video Automatisch Afspelen",
"store_watch_history": "Kijkgeschiedenis Opslaan",
"loading": "Laden...",
"audio_only": "Alleen Audio",
"default_homepage": "Standaard Startpagina",
"minimize_description_default": "Beschrijving Standaard Minimaliseren",
"language_selection": "Taalkeuze",
"minimize_description_default": "Omschrijving standaard minimaliseren",
"language_selection": "Taal",
"instances_list": "Instantieslijst",
"yes": "Ja",
"export_to_json": "Exporteren naar JSON",
"hide_replies": "Verberg Antwoorden",
"hide_replies": "Reacties verbergen",
"enabled_codecs": "Ingeschakelde Codecs (Meerdere)",
"no": "Nee",
"auto_play_next_video": "Volgende Video Automatisch Afspelen",
"remove_from_playlist": "Uit Afspeellijst Verwijderen",
"select_playlist": "Selecteer een Afspeellijst",
"auto_play_next_video": "Volgende video automatisch afspelen",
"remove_from_playlist": "Uit afspeellijst verwijderen",
"select_playlist": "Afspeellijst selecteren",
"delete_playlist_confirm": "Deze afspeellijst verwijderen?",
"please_select_playlist": "Selecteer een afspeellijst alsjeblief",
"instance_selection": "Instantie Selectie",
"import_from_json": "Importeren uit JSON/CSV",
"clear_history": "Geschiedenis Wissen",
"load_more_replies": "Laad meer Antwoorden",
"please_select_playlist": "Selecteer een afspeellijst",
"instance_selection": "Instantie",
"import_from_json": "Importeren uit JSON",
"clear_history": "Geschiedenis wissen",
"load_more_replies": "Meer reacties laden",
"delete_playlist_video_confirm": "Video uit deze afspeellijst verwijderen?",
"create_playlist": "Afspeellijst Maken",
"delete_playlist": "Afspeellijst Verwijderen",
"create_playlist": "Afspeellijst aanmaken",
"delete_playlist": "Afspeellijst verwijderen",
"show_markers": "Laat markeringen op speler zien",
"store_search_history": "Zoekgeschiedenis Opslaan",
"minimize_chapters_default": "Hoofdstukken Standaard Minimaliseren",
"show_watch_on_youtube": "Toon Bekijk op YouTube knop",
"store_search_history": "Zoekgeschiedenis bijhouden",
"minimize_chapters_default": "Hoofdstukken standaard minimaliseren",
"show_watch_on_youtube": "De knop Bekijken op YouTube tonen",
"restore_preferences": "Voorkeuren herstellen",
"with_timecode": "Delen met tijdcode",
"piped_link": "Piped link",
"follow_link": "Volg link",
"piped_link": "Piped-link",
"follow_link": "Volglink",
"copy_link": "Link kopiëren",
"hide_watched": "Verberg bekeken video's in de feed",
"hide_watched": "Bekeken video's in feed verbergen",
"minimize_comments": "Opmerkingen minimaliseren",
"instance_auth_selection": "Selectie authenticatie-instantie",
"instance_auth_selection": "Authenticatie-instantie",
"clone_playlist": "Afspeellijst dupliceren",
"download_as_txt": "Downloaden als .txt",
"share": "Delen",
@ -86,30 +86,47 @@
"time_code": "Tijdcode (in seconden)",
"show_chapters": "Hoofdstukken",
"source_code": "Broncode",
"instance_donations": "Instantie donaties",
"reply_count": "{count} antwoorden",
"instance_donations": "Instantiedonaties",
"reply_count": "{count} reacties",
"no_valid_playlists": "Het bestand bevat geen geldige afspeellijsten!",
"clone_playlist_success": "Dupliceren gelukt!",
"reset_preferences": "Voorkeuren herstellen",
"reset_preferences": "Voorkeuren resetten",
"back_to_home": "Terug naar de start",
"minimize_comments_default": "Opmerkingen Standaard Minimaliseren",
"delete_account": "Account Verwijderen",
"minimize_comments_default": "Opmerkingen standaard minimaliseren",
"delete_account": "Account verwijderen",
"logout": "Uitloggen op dit apparaat",
"minimize_recommendations_default": "Aanbevelingen Standaard Minimaliseren",
"confirm_reset_preferences": "Weet u zeker dat u uw voorkeuren opnieuw wilt instellen?",
"backup_preferences": "Back-up voorkeuren",
"minimize_recommendations_default": "Aanbevelingen standaard minimaliseren",
"confirm_reset_preferences": "Weet u zeker dat u uw voorkeuren wilt resetten?",
"backup_preferences": "Back-up-voorkeuren",
"invalidate_session": "Uitloggen op alle apparaten",
"different_auth_instance": "Gebruik een andere instantie voor authenticatie",
"with_playlist": "Delen met afspeellijst",
"playlist_bookmarked": "Bladwijzer gemaakt",
"bookmark_playlist": "Bladwijzer",
"skip_automatically": "Automatisch",
"skip_button_only": "toon de overslaan knop",
"skip_button_only": "Overslaan-knop tonen",
"min_segment_length": "Minimale segmentlengte (in seconden)",
"skip_segment": "segment overslaan",
"show_less": "Toon minder",
"show_less": "Minder tonen",
"autoplay_next_countdown": "Standaard aftellen tot de volgende video (in seconden)",
"dismiss": "Afwijzen"
"dismiss": "Afwijzen",
"enable_dearrow": "DeArrow inschakelen",
"playlist_description": "Afspeellijst­omschrijving",
"cancel": "Annuleren",
"show_search_suggestions": "Zoeksuggesties tonen",
"delete_automatically": "Automatisch verwijderen na",
"okay": "Oké",
"playlist_name": "Afspeellijst­naam",
"edit_playlist": "Afspeellijst bewerken",
"create_group": "Groep aanmaken",
"group_name": "Groep­naam",
"generate_qrcode": "QR-code genereren",
"chapters_layout_mobile": "Mobiele lay-out voor hoofdstukken",
"auto_display_captions": "Ondertiteling automatisch tonen",
"import_from_json_csv": "Importeren uit JSON/CSV",
"download_frame": "Beeld downloaden",
"instance_privacy_policy": "Privacybeleid",
"add_to_group": "Toevoegen aan groep"
},
"titles": {
"register": "Registreren",
@ -118,32 +135,38 @@
"preferences": "Voorkeuren",
"history": "Geschiedenis",
"subscriptions": "Abonnementen",
"trending": "populair",
"trending": "Trending",
"playlists": "Afspeellijsten",
"account": "profiel",
"account": "Account",
"instance": "Instantie",
"player": "Speler",
"livestreams": "Livestreams",
"channels": "Kanalen",
"bookmarks": "Bladwijzers"
"bookmarks": "Bladwijzers",
"dearrow": "DeArrow",
"channel_groups": "Kanaal­groepen"
},
"player": {
"watch_on": "Kijk op {0}"
"watch_on": "Bekijken op {0}",
"failed": "Mislukt met foutcode {0}, zie logboeken voor meer informatie"
},
"search": {
"videos": "YouTube: Video's",
"channels": "YouTube: Kanalen",
"playlists": "YouTube: Afspeellijsten",
"music_songs": "YT Muziek: Liedjes",
"music_videos": "YT Muziek: Video's",
"music_albums": "YT Muziek: Albums",
"music_playlists": "YT Muziek: Afspeellijsten",
"music_songs": "YT Music: Nummers",
"music_videos": "YT Music: Video's",
"music_albums": "YT Music: Albums",
"music_playlists": "YT Music: Afspeellijsten",
"did_you_mean": "Bedoelde u: {0}?",
"all": "YouTube: Alles"
"all": "YouTube: Alles",
"music_artists": "YT Music: Artiesten"
},
"login": {
"username": "Gebruikersnaam",
"password": "Wachtwoord"
"password": "Wachtwoord",
"password_confirm": "Wachtwoord bevestigen",
"passwords_incorrect": "Wachtwoorden komen niet overeen!"
},
"video": {
"videos": "Video's",
@ -151,17 +174,21 @@
"chapters": "Hoofdstukken",
"watched": "Gekeken",
"sponsor_segments": "Sponsorsegmenten",
"ratings_disabled": "Beoordelingen Uitgeschakeld",
"live": "{0} Live",
"ratings_disabled": "Beoordelingen uitgeschakeld",
"live": "{0} live",
"shorts": "Shorts",
"category": "Categorie",
"all": "Alle"
"all": "Alle",
"chapters_horizontal": "Horizontaal",
"chapters_vertical": "Verticaal",
"license": "Licentie",
"visibility": "Zichtbaarheid"
},
"preferences": {
"has_cdn": "Heeft CDN?",
"registered_users": "Geregistreerde Gebruikers",
"instance_name": "Instantie Naam",
"instance_locations": "Locaties van Instanties",
"registered_users": "Geregistreerde gebruikers",
"instance_name": "Instantienaam",
"instance_locations": "Instantielocaties",
"version": "Versie",
"up_to_date": "Bijgewerkt?",
"ssl_score": "SSL-score"
@ -169,8 +196,8 @@
"comment": {
"pinned_by": "Vastgemaakt door {author}",
"user_disabled": "Opmerkingen zijn uitgeschakeld in de instellingen.",
"loading": "Opmerkingen laden...",
"disabled": "Reacties zijn uitgeschakeld door de uploader."
"loading": "Opmerkingen laden",
"disabled": "Opmerkingen zijn uitgeschakeld door de uploader."
},
"info": {
"preferences_note": "Let op: voorkeuren worden opgeslagen in de lokale opslag van uw browser. Als u uw browsergegevens verwijdert, worden ze opnieuw ingesteld.",
@ -179,7 +206,12 @@
"page_not_found": "Pagina niet gevonden",
"local_storage": "Deze actie vereist lokale opslag, zijn cookies ingeschakeld?",
"register_no_email_note": "Een e-mailadres als gebruikersnaam gebruiken wordt afgeraden. Toch doorgaan?",
"next_video_countdown": "Volgende video afspelen in {0}s"
"next_video_countdown": "Volgende video wordt afgespeeld over {0}s",
"days": "{amount} dag(en)",
"weeks": "{amount} week/weken",
"months": "{amount} maand(en)",
"hours": "{amount} uur",
"login_note": "Log in met een account dat op deze instantie is aangemaakt."
},
"subscriptions": {
"subscribed_channels_count": "Geabonneerd op: {0}"

View file

@ -14,10 +14,12 @@
"subscriptions": "Abonaments",
"playlists": "Listas de lecturas",
"bookmarks": "Marcapaginas",
"channel_groups": "Grops de cadenas"
"channel_groups": "Grops de cadenas",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "Agachar sus {0}"
"watch_on": "Veire sus {0}",
"failed": "Fracàs amb lo còdi derror {0}, consultat los jornals daudit per mai dinformacions"
},
"actions": {
"subscribe": "Sabonar - {count}",
@ -53,7 +55,7 @@
"add_to_playlist": "Apondre a la listra de lectura",
"minimize_comments": "Minimizar los comentaris",
"theme": "Tèma",
"language_selection": "Seleccion de la lenga",
"language_selection": "Lenga",
"loop_this_video": "Legir en bocla la vidèo",
"reset_preferences": "Restablir las preferéncias",
"auto": "Auto",
@ -75,14 +77,14 @@
"yes": "Òc",
"no": "Non",
"export_to_json": "Exportar en JSON",
"import_from_json": "Importar dun JSON/CSV",
"import_from_json": "Importar dun JSON",
"auto_play_next_video": "Legir la vidèo seguenta automaticament",
"create_playlist": "Crear una lista de lectura",
"delete_playlist": "Levar de la lista de lectura",
"select_playlist": "Seleccionatz una lista de lectura",
"delete_playlist_confirm": "Suprimir aquesta lista de lectura ?",
"clone_playlist": "Clonar la lista de lectura",
"instance_auth_selection": "Seleccion de linstància dautentificacion",
"instance_auth_selection": "Instància dautentificacion",
"clone_playlist_success": "Clonatge capitat !",
"follow_link": "Dobrir lo ligam",
"skip_self_promo": "Sautar la promocion gratuita / autopromocion",
@ -92,7 +94,7 @@
"autoplay_video": "Legir automaticament la vidèo",
"audio_only": "Sonque àudio",
"minimize_comments_default": "Minimizar los comentaris per defaut",
"instance_selection": "Seleccion dinstàncias",
"instance_selection": "Instància",
"please_select_playlist": "Seleccionatz una lista de lectura",
"show_watch_on_youtube": "Mostrar lo boton « Veire sus YouTube »",
"invalidate_session": "Se desconnectar de totes los aparelhs",
@ -101,7 +103,7 @@
"with_timecode": "Partejar amb còdi orari",
"piped_link": "Ligam cap a Piped",
"show_chapters": "Capítols",
"country_selection": "Seleccion del país",
"country_selection": "País",
"default_homepage": "Pagina dacuèlh per defaut",
"minimize_recommendations_default": "Minimizar las recomandacions per defaut",
"store_watch_history": "Servar listoric de visualizacion",
@ -138,7 +140,14 @@
"playlist_name": "Nom de la lista de lectura",
"playlist_description": "Descripcion de la lista de lectura",
"auto_display_captions": "Afichatge auto dels sostítols",
"show_search_suggestions": "Mostrar las suggestions de recèrcas"
"show_search_suggestions": "Mostrar las suggestions de recèrcas",
"chapters_layout_mobile": "Disposicion capítols sus mobil",
"enable_dearrow": "Activar DeArrow",
"import_from_json_csv": "Importar dun JSON/CSV",
"delete_automatically": "Suprimir automaticament aprèp",
"download_frame": "Telecargar fotograma",
"generate_qrcode": "Generar un còdi QR",
"instance_privacy_policy": "Politica de confidencialitat"
},
"preferences": {
"instance_locations": "Localizacion de linstància",
@ -151,7 +160,9 @@
},
"login": {
"username": "Nom dutilizaire",
"password": "Senhal"
"password": "Senhal",
"password_confirm": "Confirmar lo senhal",
"passwords_incorrect": "Los senhals correspondon pas !"
},
"video": {
"views": "{views} visualizacions",
@ -163,7 +174,11 @@
"chapters": "Capítols",
"videos": "Vidèos",
"all": "Totas",
"category": "Categoria"
"category": "Categoria",
"chapters_horizontal": "Orizontal",
"chapters_vertical": "Vertical",
"license": "Licéncia",
"visibility": "Visibilitat"
},
"info": {
"preferences_note": "Nòta: las preferéncias son gardadas dins lespaci demmagazinatge del navegador. La supression de las donadas del navegador las restablirà.",
@ -172,7 +187,11 @@
"cannot_copy": "Còpia impossibla !",
"local_storage": "Aquesta accion requerís lo localStorage, son activats los cookies ?",
"register_no_email_note": "Es pas recomandat dutilizar una adreça electronica coma nom dutilizaire. Contunhar çaquelà ?",
"next_video_countdown": "La vidèo seguenta començarà daquí {0}s"
"next_video_countdown": "La vidèo seguenta començarà daquí {0}s",
"hours": "{amount} ora(s)",
"months": "{amount} mes(es)",
"days": "{amount} jorn(s)",
"weeks": "{amount} setmana(s)"
},
"comment": {
"disabled": "Lautor a desactivat los comentaris.",
@ -189,7 +208,8 @@
"music_songs": "YT Music : Cançons",
"music_videos": "YT Music : Vidèos",
"music_albums": "YT Music : Albums",
"music_playlists": "YT Music : Listas de lectura"
"music_playlists": "YT Music : Listas de lectura",
"music_artists": "YT Music : Artistas"
},
"subscriptions": {
"subscribed_channels_count": "Abonat a : {0}"

View file

@ -14,10 +14,12 @@
"livestreams": "ସିଧାପ୍ରସାରଣ ଗୁଡ଼ିକ",
"channels": "ସ୍ରୋତ ଗୁଡ଼ିକ",
"bookmarks": "ବୁକମାର୍କଗୁଡିକ",
"channel_groups": "ଚ୍ୟାନେଲ୍ ଗୋଷ୍ଠୀଗୁଡିକ"
"channel_groups": "ଚ୍ୟାନେଲ୍ ଗୋଷ୍ଠୀଗୁଡିକ",
"dearrow": "ଡି ତୀର"
},
"player": {
"watch_on": "{0} ରେ ଦେଖନ୍ତୁ"
"watch_on": "{0} ରେ ଦେଖନ୍ତୁ",
"failed": "ତ୍ରୁଟି ସଂକେତ {0} ସହିତ ବିଫଳ, ଅଧିକ ସୂଚନା ପାଇଁ ଲଗଗୁଡ଼ିକୁ ଦେଖନ୍ତୁ"
},
"actions": {
"subscribe": "ସଦସ୍ୟତା - {count}",
@ -57,16 +59,16 @@
"autoplay_video": "ଅଟୋପ୍ଲେ ଭିଡିଓ",
"enabled_codecs": "ସକ୍ଷମ କୋଡେକସ୍ (ଏକାଧିକ)",
"audio_only": "କେବଳ ସ୍ୱର",
"language_selection": "ଭାଷା ଚୟନ",
"language_selection": "ଭାଷା",
"show_more": "ଅଧିକ ଦେଖାନ୍ତୁ",
"buffering_goal": "ବଫରିଂ ଲକ୍ଷ୍ୟ (ସେକେଣ୍ଡରେ)",
"country_selection": "ଦେଶ ଚୟନ",
"country_selection": "ଦେଶ",
"minimize_description_default": "ଡିଫଲ୍ଟ ଭାବରେ ବର୍ଣ୍ଣନାକୁ କମ୍ କରନ୍ତୁ",
"store_watch_history": "ଦେଖିଥିବା ଭିଡିଓ ଗୁଡ଼ିକର ଇତିହାସ ରଖନ୍ତୁ",
"instances_list": "ଉଦାହରଣ ତାଲିକା",
"instance_selection": "ଇନଷ୍ଟାନ୍ସ ଚୟନ",
"instance_selection": "ଇନଷ୍ଟାନ୍ସ",
"yes": "ହଁ",
"import_from_json": "JSON / CSV ରୁ ଆମଦାନୀ କରନ୍ତୁ",
"import_from_json": "JSON ରୁ ଆମଦାନୀ କରନ୍ତୁ",
"no": "ନାହିଁ",
"export_to_json": "JSON କୁ ରପ୍ତାନି କରନ୍ତୁ",
"loop_this_video": "ଏହି ଭିଡିଓକୁ ଲୁପ୍ କରନ୍ତୁ",
@ -98,7 +100,7 @@
"minimize_recommendations_default": "ଡିଫଲ୍ଟ ଭାବରେ ସୁପାରିଶକୁ କମ୍ କରନ୍ତୁ",
"invalidate_session": "ସମସ୍ତ ଡିଭାଇସ୍ ରୁ ଲଗଆଉଟ୍ କରନ୍ତୁ",
"download_as_txt": ".Txt ଭାବରେ ଡାଉନଲୋଡ୍ କରନ୍ତୁ",
"instance_auth_selection": "ପ୍ରାମାଣିକିକରଣ ଇନଷ୍ଟାନ୍ସ ଚୟନ",
"instance_auth_selection": "ପ୍ରାମାଣିକିକରଣ ଇନଷ୍ଟାନ୍ସ",
"confirm_reset_preferences": "ଆପଣ ନିଶ୍ଚିତ କି ଆପଣ ଆପଣଙ୍କର ପସନ୍ଦଗୁଡିକ ପୁନଃ ସେଟ୍ କରିବାକୁ ଚାହୁଁଛନ୍ତି?",
"status_page": "ସ୍ଥିତି",
"different_auth_instance": "ପ୍ରାମାଣିକିକରଣ ପାଇଁ ଏକ ଭିନ୍ନ ଉଦାହରଣ ବ୍ୟବହାର କରନ୍ତୁ",
@ -131,7 +133,21 @@
"autoplay_next_countdown": "ପରବର୍ତ୍ତୀ ଭିଡିଓ ପର୍ଯ୍ୟନ୍ତ ଡିଫଲ୍ଟ କାଉଣ୍ଟଡାଉନ୍ (ସେକେଣ୍ଡରେ)",
"dismiss": "ବରଖାସ୍ତ",
"create_group": "ଗୋଷ୍ଠୀ ସୃଷ୍ଟି କରନ୍ତୁ",
"group_name": "ଗୋଷ୍ଠୀ ନାମ"
"group_name": "ଗୋଷ୍ଠୀ ନାମ",
"enable_dearrow": "DeArrow କୁ ସକ୍ରିୟ କରନ୍ତୁ",
"show_search_suggestions": "ସନ୍ଧାନ ପ୍ରସ୍ତାବଗୁଡ଼ିକୁ ଦର୍ଶାନ୍ତୁ",
"delete_automatically": "ଏହା ପରେ ସ୍ୱୟଂଚାଳିତ ଭାବରେ ଅପସାରଣ କରନ୍ତୁ",
"okay": "ଠିକ୍ ଅଛି",
"edit_playlist": "ପ୍ଲେଲିଷ୍ଟକୁ ସମ୍ପାଦନ କରନ୍ତୁ",
"playlist_name": "ପ୍ଲେ-ଲିଷ୍ଟ ନାମ",
"generate_qrcode": "QR କୋଡ ସୃଷ୍ଟି କରନ୍ତୁ",
"chapters_layout_mobile": "ମୋବାଇଲରେ ଅଧ୍ୟାୟ ବିନ୍ୟାସ",
"auto_display_captions": "ଶୀର୍ଷକଗୁଡ଼ିକୁ ସ୍ୱୟଂଚାଳିତ ଭାବରେ ଦର୍ଶାନ୍ତୁ",
"playlist_description": "ପ୍ଲେଲିଷ୍ଟ ବର୍ଣ୍ଣନା",
"cancel": "ବାତିଲ କରନ୍ତୁ",
"import_from_json_csv": "JSON/CSV ରୁ ଆମଦାନୀ କରନ୍ତୁ",
"download_frame": "ଫ୍ରେମକୁ ଆହରଣ କରନ୍ତୁ",
"instance_privacy_policy": "ଗୋପନୀୟତା ନୀତି"
},
"comment": {
"loading": "ମନ୍ତବ୍ୟ ଲୋଡ୍ ହେଉଛି ...",
@ -149,7 +165,11 @@
"chapters": "ଅଧ୍ୟାୟ ଗୁଡ଼ିକ",
"live": "{0} ସିଧାପ୍ରସାରଣ",
"all": "ସମସ୍ତ",
"category": "ବର୍ଗ"
"category": "ବର୍ଗ",
"visibility": "ଦୃଶ୍ୟମାନତା",
"license": "ଲାଇସେନ୍ସ",
"chapters_horizontal": "ଭୂସମାନ୍ତର",
"chapters_vertical": "ଭୂଲମ୍ବ"
},
"search": {
"did_you_mean": "ଆପଣ କହିବାକୁ ଚାହୁଁଛନ୍ତି କି: {0}?",
@ -160,7 +180,8 @@
"videos": "ୟୁଟ୍ୟୁବ୍: ଭିଡିଓଗୁଡିକ",
"channels": "ୟୁଟ୍ୟୁବ୍: ଚ୍ୟାନେଲଗୁଡିକ",
"music_songs": "ୟୁଟିଉବ୍ ସଙ୍ଗୀତ: ଗୀତ ଗୁଡ଼ିକ",
"playlists": "ୟୁଟ୍ୟୁବ୍: ପ୍ଲେଲିଷ୍ଟଗୁଡିକ"
"playlists": "ୟୁଟ୍ୟୁବ୍: ପ୍ଲେଲିଷ୍ଟଗୁଡିକ",
"music_artists": "ୱାଇଟି ମ୍ୟୁଜିକ: ଆର୍ଟିଷ୍ଟ"
},
"subscriptions": {
"subscribed_channels_count": "ସଦସ୍ୟତା: {0}"
@ -172,7 +193,11 @@
"page_not_found": "ପୃଷ୍ଠାଟି ମିଳିଲା ନାହିଁ",
"local_storage": "ଏହି କ୍ରିୟା ଲୋକାଲ୍ ଷ୍ଟୋରେଜ୍ ଆବଶ୍ୟକ କରେ, କୁକିଜ୍ ସକ୍ଷମ ଅଛି କି?",
"register_no_email_note": "ଉପଯୋଗକର୍ତ୍ତା ନାମ ଭାବରେ ଏକ ଇ-ମେଲ୍ ବ୍ୟବହାର କରିବା ସୁପାରିଶ କରାଯାଏ ନାହିଁ । ଯେକୌଣସି ପ୍ରକାରେ ଅଗ୍ରଗତି କରନ୍ତୁ?",
"next_video_countdown": "{0} ସେକେଣ୍ଡ ରେ ପରବର୍ତ୍ତୀ ଭିଡିଓ ଖେଳିବାକୁ ଆରମ୍ଭ ହେବ"
"next_video_countdown": "{0} ସେକେଣ୍ଡ ରେ ପରବର୍ତ୍ତୀ ଭିଡିଓ ଖେଳିବାକୁ ଆରମ୍ଭ ହେବ",
"hours": "{amount} ଘଣ୍ଟା",
"days": "{amount} ଦିନ",
"weeks": "{amount} ସପ୍ତାହ",
"months": "{amount} ମାସ"
},
"preferences": {
"instance_name": "ଇନଷ୍ଟାନ୍ସ ନାମ",
@ -185,6 +210,8 @@
},
"login": {
"password": "ପାସୱାର୍ଡ",
"username": "ଉପଯୋଗକର୍ତ୍ତା ନାମ"
"username": "ଉପଯୋଗକର୍ତ୍ତା ନାମ",
"passwords_incorrect": "ପ୍ରବେଶ ସଂକେତ ମେଳ ଖାଉନାହିଁ!",
"password_confirm": "ପ୍ରବେଶ ସଂକେତ ନିଶ୍ଚିତ କରନ୍ତୁ"
}
}

View file

@ -14,10 +14,12 @@
"livestreams": "Na żywo",
"channels": "Kanały",
"bookmarks": "Zakładki",
"channel_groups": "Grupy kanałów"
"channel_groups": "Grupy kanałów",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "Obejrzyj na {0}"
"watch_on": "Zobacz na {0}",
"failed": "Niepowodzenie z powodu kodu błędu {0}, przejrzyj logi, aby uzyskać więcej informacji"
},
"actions": {
"subscribe": "Subskrybuj - {count}",
@ -48,20 +50,20 @@
"audio_only": "Tylko audio",
"default_quality": "Domyślna jakość",
"buffering_goal": "Cel buforowania (w sekundach)",
"country_selection": "Wybór kraju",
"country_selection": "Kraj",
"default_homepage": "Domyślna strona główna",
"show_comments": "Pokaż komentarze",
"minimize_description_default": "Ukryj opis",
"store_watch_history": "Zapamiętaj historię oglądania",
"language_selection": "Wybór języka",
"language_selection": "Język",
"instances_list": "Lista instancji",
"enabled_codecs": "Włączone kodeki (lista wielokrotnego wyboru)",
"instance_selection": "Wybór instancji",
"instance_selection": "Instancja",
"show_more": "Pokaż więcej",
"yes": "Tak",
"no": "Nie",
"export_to_json": "Eksport do pliku JSON",
"import_from_json": "Import z pliku JSON/CSV",
"import_from_json": "Import z pliku JSON",
"loop_this_video": "Zapętlaj ten film",
"auto_play_next_video": "Autoodtwarzanie następnego filmu",
"donations": "Wsparcie",
@ -94,7 +96,7 @@
"documentation": "Dokumentacja",
"instance_donations": "Darowizny na rzecz instancji",
"back_to_home": "Idź do strony głównej",
"instance_auth_selection": "Wybrana instancja autoryzacyjna",
"instance_auth_selection": "Instancja uwierzytelniania",
"time_code": "Kod czasowy (w sekundach)",
"show_markers": "Pokaż segmenty na odtwarzaczu",
"store_search_history": "Zapamiętaj historię wyszukiwania",
@ -139,7 +141,14 @@
"okay": "OK",
"cancel": "Anuluj",
"show_search_suggestions": "Pokaż sugestie wyszukiwania",
"chapters_layout_mobile": "Układ rozdziałów na urządzeniach mobilnych"
"chapters_layout_mobile": "Układ rozdziałów na urządzeniach mobilnych",
"delete_automatically": "Usuń automatycznie po",
"enable_dearrow": "Włącz DeArrow",
"generate_qrcode": "Wygeneruj kod QR",
"import_from_json_csv": "Import z pliku JSON/CSV",
"download_frame": "Pobierz klatkę",
"instance_privacy_policy": "Polityki prywatności",
"add_to_group": "Dodaj do grupy"
},
"comment": {
"pinned_by": "Przypięty przez {author}",
@ -158,7 +167,9 @@
},
"login": {
"username": "Nazwa użytkownika",
"password": "Hasło"
"password": "Hasło",
"password_confirm": "Potwierdź hasło",
"passwords_incorrect": "Hasła się nie zgadzają!"
},
"video": {
"videos": "Filmy",
@ -172,7 +183,9 @@
"all": "Wszystkie",
"category": "Kategoria",
"chapters_horizontal": "Poziomy",
"chapters_vertical": "Pionowy"
"chapters_vertical": "Pionowy",
"license": "Licencja",
"visibility": "Widoczność"
},
"search": {
"did_you_mean": "Czy chodziło ci o: {0}?",
@ -183,7 +196,8 @@
"music_songs": "YT Music: Utwory",
"music_videos": "YT Music: Teledyski",
"music_albums": "YT Music: Albumy",
"music_playlists": "YT Music: Playlisty"
"music_playlists": "YT Music: Playlisty",
"music_artists": "YT Music: Wykonawcy"
},
"info": {
"cannot_copy": "Nie można skopiować!",
@ -192,7 +206,13 @@
"preferences_note": "Uwaga: ustawienia są zapisywane w lokalnej pamięci przeglądarki. Usunięcie danych przeglądarki spowoduje ich zresetowanie.",
"local_storage": "Ta akcja wymaga dostępu do lokalnej pamięci, czy pliki cookie są włączone?",
"register_no_email_note": "Użycie adresu email jako nazwy użytkownika jest niezalecane. Kontynuować mimo to?",
"next_video_countdown": "Odtwarzanie następnego filmu za {0} s"
"next_video_countdown": "Odtwarzanie następnego filmu za {0} s",
"days": "{amount} dni",
"weeks": "{amount} tygodnie",
"hours": "{amount} godziny",
"months": "{amount} miesiące",
"login_note": "Zaloguj się na konto utworzone w tej instancji.",
"register_note": "Zarejestruj konto dla tej instancji Piped. Umożliwi to synchronizację subskrypcji i list odtwarzania ze swoim kontem, dzięki czemu będą one przechowywane po stronie serwera. Możesz korzystać ze wszystkich funkcji bez konta, ale wszystkie dane będą przechowywane w lokalnej pamięci podręcznej Twojej przeglądarki. Upewnij się, że NIE używasz adresu e-mail jako nazwy użytkownika i wybierz bezpieczne hasło, którego nie używasz gdzie indziej."
},
"subscriptions": {
"subscribed_channels_count": "Licznik subskrybcji: {0}"

View file

@ -14,7 +14,8 @@
"livestreams": "Emissões em direto",
"channels": "Canais",
"bookmarks": "Marcadores",
"channel_groups": "Grupos de canais"
"channel_groups": "Grupos de canais",
"dearrow": "DeArrow"
},
"actions": {
"sort_by": "Ordenar por:",
@ -32,16 +33,16 @@
"autoplay_video": "Reproduzir vídeos automaticamente",
"audio_only": "Apenas áudio",
"default_quality": "Qualidade padrão",
"country_selection": "Seleção de país",
"country_selection": "País",
"default_homepage": "Página inicial padrão",
"show_comments": "Mostrar comentários",
"minimize_description_default": "Minimizar descrição por omissão",
"store_watch_history": "Guardar histórico de visualizações",
"instances_list": "Lista de instâncias",
"enabled_codecs": "Codificadores ativados (vários)",
"instance_selection": "Seleção de instância",
"instance_selection": "Instância",
"show_more": "Mostrar mais",
"import_from_json": "Importar de JSON/CSV",
"import_from_json": "Importar de JSON",
"export_to_json": "Exportar para JSON",
"loop_this_video": "Repetir este vídeo",
"auto_play_next_video": "Reproduzir vídeo seguinte automaticamente",
@ -65,7 +66,7 @@
"skip_non_music": "Ignorar música: secção não musical",
"no": "Não",
"theme": "Tema",
"language_selection": "Seleção de idioma",
"language_selection": "Idioma",
"minimize_recommendations": "Minimizar recomendações",
"light": "Claro",
"hide_replies": "Ocultar respostas",
@ -91,7 +92,7 @@
"minimize_recommendations_default": "Minimizar recomendações por omissão",
"invalidate_session": "Terminar sessão em todos os dispositivos",
"different_auth_instance": "Usar uma instância diferente para autenticação",
"instance_auth_selection": "Seleção da instância para autenticação",
"instance_auth_selection": "Instância para autenticação",
"confirm_reset_preferences": "Tem a certeza de que deseja repor as preferências?",
"download_as_txt": "Descarregar como .txt",
"reset_preferences": "Repor preferências",
@ -136,7 +137,14 @@
"edit_playlist": "Editar lista de reprodução",
"playlist_name": "Nome da lista de reprodução",
"show_search_suggestions": "Mostrar sugestões de pesquisa",
"chapters_layout_mobile": "Aplicações recentemente adicionadas"
"chapters_layout_mobile": "Aplicações recentemente adicionadas",
"enable_dearrow": "Ativar o DeArrow",
"delete_automatically": "Eliminar automaticamente após",
"generate_qrcode": "Gerar código QR",
"import_from_json_csv": "Importar de JSON/CSV",
"download_frame": "Quadro de transferência",
"instance_privacy_policy": "Política de privacidade",
"add_to_group": "Adicionar ao grupo"
},
"preferences": {
"instance_name": "Nome da instância",
@ -149,7 +157,9 @@
},
"login": {
"password": "Palavra-passe",
"username": "Nome de utilizador"
"username": "Nome de utilizador",
"passwords_incorrect": "As palavras-passe não coincidem!",
"password_confirm": "Confirmar a palavra-passe"
},
"video": {
"videos": "Vídeos",
@ -163,7 +173,9 @@
"all": "Todos",
"category": "Categoria",
"chapters_horizontal": "Horizontal",
"chapters_vertical": "Vertical"
"chapters_vertical": "Vertical",
"license": "Licença",
"visibility": "Visibilidade"
},
"search": {
"did_you_mean": "Será que queria dizer: {0}?",
@ -174,10 +186,12 @@
"music_videos": "YT Music: Vídeos",
"music_albums": "YT Music: Álbuns",
"music_playlists": "YT Music: Listas de reprodução",
"playlists": "YouTube: Listas de reprodução"
"playlists": "YouTube: Listas de reprodução",
"music_artists": "YT Music: Artistas"
},
"player": {
"watch_on": "Ver em {0}"
"watch_on": "Ver em {0}",
"failed": "Falha com o código de erro {0}, ver registos para mais informações"
},
"comment": {
"pinned_by": "Afixado por {author}",
@ -195,6 +209,12 @@
"local_storage": "Esta ação requer localStorage, os cookies estão ativados?",
"preferences_note": "Nota: as preferências são guardadas no armazenamento local do seu navegador. Se limpar os dados de navegação, também limpa as preferências.",
"register_no_email_note": "Não recomendamos utilizar um endereço de e-mail como nome de utilizador. Continuar?",
"next_video_countdown": "O próximo vídeo será reproduzido dentro de {0} segundos"
"next_video_countdown": "O próximo vídeo será reproduzido dentro de {0} segundos",
"hours": "{quantidade} hora(s)",
"days": "{quantidade} dia(s)",
"weeks": "{quantidade} semana(s)",
"months": "{quantidade} mês(es)",
"login_note": "Inicie sessão com uma conta criada nesta instância.",
"register_note": "Registar uma conta para esta instância do Piped. Isso permitirá que você sincronize suas assinaturas e listas de reprodução com sua conta, para que elas sejam armazenadas no lado do servidor. Você pode usar todos os recursos sem uma conta, mas todos os dados serão armazenados no cache local do seu navegador. Certifique-se de que NÃO utiliza um endereço de e-mail como nome de utilizador e escolha uma palavra-passe segura que não utilize noutros locais."
}
}

View file

@ -10,7 +10,7 @@
"dark": "Escuro",
"light": "Claro",
"show_comments": "Exibir Comentários",
"country_selection": "Seleção de País",
"country_selection": "País",
"default_homepage": "Página Inicial Padrão",
"default_quality": "Qualidade Padrão",
"autoplay_video": "Reprodução Automática",
@ -34,7 +34,7 @@
"skip_non_music": "Pular Música: Seção não Musical",
"skip_filler_tangent": "Pular Enchimento Tangencial",
"enabled_codecs": "Codecs Ativados (Múltiplos)",
"language_selection": "Seleção de Idioma",
"language_selection": "Idioma",
"yes": "Sim",
"show_more": "Mostrar Mais",
"export_to_json": "Exportar para JSON",
@ -55,14 +55,14 @@
"view_ssl_score": "Ver Pontuação SSL",
"disable_lbry": "Desativar LBRY para Streaming",
"enable_lbry_proxy": "Ativar Proxy para LBRY",
"import_from_json": "Importar de JSON/CSV",
"import_from_json": "Importar de JSON",
"loop_this_video": "Repetir este Vídeo",
"instances_list": "Lista de Instâncias",
"clear_history": "Limpar Histórico",
"search": "Pesquisar (Ctrl+K)",
"no": "Não",
"show_description": "Exibir Descrição",
"instance_selection": "Seleção de Instância",
"instance_selection": "Instância",
"auto_play_next_video": "Autorreproduzir Próximo Vídeo",
"filter": "Filtro",
"store_watch_history": "Salvar Histórico de Exibição",
@ -81,7 +81,8 @@
"status_page": "Estado",
"source_code": "Código fonte",
"instance_donations": "Doações de instâncias",
"instance_auth_selection": "Seleção de Instância de Autenticação",
"instance_privacy_policy": "Política de Privacidade",
"instance_auth_selection": "Instância de Autenticação",
"clone_playlist_success": "Clonada com sucesso!",
"download_as_txt": "Baixar como .txt",
"restore_preferences": "Restaurar preferências",
@ -119,7 +120,13 @@
"create_group": "Criar grupo",
"group_name": "Nome do grupo",
"show_search_suggestions": "Mostrar sugestões de pesquisa",
"chapters_layout_mobile": "Layout dos Capítulos no Celular"
"chapters_layout_mobile": "Layout dos Capítulos no Celular",
"delete_automatically": "Deletar automaticamente após",
"generate_qrcode": "Gerar código QR",
"enable_dearrow": "Ativar DeArrow",
"import_from_json_csv": "Importar de JSON/CSV",
"download_frame": "Baixar quadro",
"add_to_group": "Adicionar ao grupo"
},
"titles": {
"history": "Histórico",
@ -136,10 +143,12 @@
"channels": "Canais",
"livestreams": "Transmissões ao vivo",
"bookmarks": "Favoritos",
"channel_groups": "Grupos de Canais"
"channel_groups": "Grupos de Canais",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "Assistir no {0}"
"watch_on": "Ver em {0}",
"failed": "Falhou com código de erro {0}, veja os logs para mais informações"
},
"comment": {
"pinned_by": "Fixado por {author}",
@ -158,7 +167,9 @@
},
"login": {
"username": "Nome de usuário",
"password": "Senha"
"password": "Senha",
"password_confirm": "Confirme senha",
"passwords_incorrect": "As senhas não correspondem!"
},
"video": {
"videos": "Vídeos",
@ -172,7 +183,9 @@
"all": "Todos",
"category": "Categoria",
"chapters_horizontal": "Horizontal",
"chapters_vertical": "Vertical"
"chapters_vertical": "Vertical",
"license": "Licença",
"visibility": "Visibilidade"
},
"search": {
"did_you_mean": "Você quis dizer: {0}?",
@ -183,7 +196,8 @@
"music_videos": "YT Music: Vídeos",
"music_albums": "YT Music: Álbuns",
"music_playlists": "YT Music: Playlists",
"all": "YouTube: Tudo"
"all": "YouTube: Tudo",
"music_artists": "YT Music: Artistas"
},
"info": {
"copied": "Copiado!",
@ -192,7 +206,13 @@
"page_not_found": "página não encontrada",
"local_storage": "Esta ação requer localStorage, os cookies estão ativados?",
"register_no_email_note": "Usar um e-mail como nome de usuário não é recomendado. Continuar mesmo assim?",
"next_video_countdown": "Reproduzindo o próximo vídeo em {0}s"
"next_video_countdown": "Reproduzindo o próximo vídeo em {0}s",
"hours": "{amount} hora(s)",
"days": "{amount} dia(s)",
"weeks": "{amount} semana(s)",
"months": "{amount} mês/meses",
"login_note": "Entre com uma conta criada nesta instância.",
"register_note": "Registre uma conta para esta instância Piped. Isto irá permitir que você sincronize suas inscrições e playlists com sua conta, para que sejam armazenadas no lado do servidor. Você pode usar todas as funções sem uma conta, mas todos os dados serão armazenados no cache local do seu navegador. Por favor certifique-se de NÃO usar seu endereço de e-mail como nome de usuário e de escolher uma senha segura que não use em nenhum outro lugar."
},
"subscriptions": {
"subscribed_channels_count": "Inscrito em: {0}"

View file

@ -14,7 +14,8 @@
"livestreams": "Emissões em direto",
"channels": "Canais",
"bookmarks": "Marcadores",
"channel_groups": "Grupos de canais"
"channel_groups": "Grupos de canais",
"dearrow": "DeArrow"
},
"actions": {
"view_subscriptions": "Ver subscrições",
@ -39,14 +40,14 @@
"dark": "Escuro",
"light": "Claro",
"buffering_goal": "Objetivo de 'buffer' (em segundos)",
"country_selection": "Seleção de país",
"country_selection": "País",
"default_homepage": "Página inicial padrão",
"show_comments": "Mostrar comentários",
"minimize_description_default": "Minimizar descrição por omissão",
"store_watch_history": "Guardar histórico de visualizações",
"language_selection": "Seleção de idioma",
"language_selection": "Idioma",
"enabled_codecs": "Codificadores ativados (vários)",
"instance_selection": "Seleção de instância",
"instance_selection": "Instância",
"show_more": "Mostrar mais",
"import_from_json": "Importar de JSON/CSV",
"loop_this_video": "Repetir este vídeo",
@ -89,7 +90,7 @@
"logout": "Terminar sessão neste dispositivo",
"minimize_recommendations_default": "Minimizar recomendações por omissão",
"different_auth_instance": "Usar uma instância diferente para autenticação",
"instance_auth_selection": "Seleção da instância para autenticação",
"instance_auth_selection": "Instância de autenticação",
"invalidate_session": "Terminar sessão em todos os dispositivos",
"clone_playlist": "Clonar lista de reprodução",
"clone_playlist_success": "Clonada com sucesso!",
@ -105,7 +106,7 @@
"backup_preferences": "Exportar preferências",
"back_to_home": "Voltar ao início",
"minimize_comments_default": "Minimizar comentários por omissão",
"store_search_history": "Guardar histórico de pesquisas",
"store_search_history": "Histórico de pesquisa da loja",
"minimize_chapters_default": "Minimizar capítulos por omissão",
"show_watch_on_youtube": "Mostrar botão Ver no YouTube",
"show_chapters": "Capítulos",
@ -136,7 +137,9 @@
"cancel": "Cancelar",
"okay": "Ok",
"show_search_suggestions": "Mostrar sugestões de pesquisa",
"chapters_layout_mobile": "Aplicações recentemente adicionadas"
"chapters_layout_mobile": "Aplicações recentemente adicionadas",
"enable_dearrow": "Ativar o DeArrow",
"delete_automatically": "Eliminar automaticamente após"
},
"comment": {
"pinned_by": "Afixado por {author}",
@ -169,7 +172,9 @@
"all": "Todos",
"category": "Categoria",
"chapters_horizontal": "Horizontal",
"chapters_vertical": "Vertical"
"chapters_vertical": "Vertical",
"license": "Licença",
"visibility": "Visibilidade"
},
"search": {
"did_you_mean": "Será que queria dizer: {0}?",
@ -180,7 +185,8 @@
"music_songs": "YT Music: Músicas",
"music_videos": "YT Music: Vídeos",
"music_albums": "YT Music: Álbuns",
"music_playlists": "YT Music: Listas de reprodução"
"music_playlists": "YT Music: Listas de reprodução",
"music_artists": "YT Music: Artistas"
},
"player": {
"watch_on": "Ver em {0}"
@ -195,6 +201,10 @@
"cannot_copy": "Não foi possível copiar!",
"local_storage": "Esta ação requer localStorage, os cookies estão ativados?",
"register_no_email_note": "Não recomendamos utilizar um endereço de e-mail como nome de utilizador. Continuar?",
"next_video_countdown": "A reproduzir o vídeo seguinte em {0}s"
"next_video_countdown": "A reproduzir o vídeo seguinte em {0}s",
"hours": "{quantidade} hora(s)",
"days": "{quantidade} dia(s)",
"weeks": "{quantidade} semana(s)",
"months": "{quantidade} mês(es)"
}
}

View file

@ -32,11 +32,11 @@
"auto": "Auto",
"audio_only": "Doar audio",
"default_quality": "Calitate implicită",
"country_selection": "Selecție țară",
"country_selection": "Țară",
"default_homepage": "Pagina principală implicită",
"minimize_comments_default": "Minimizați comentariile în mod implicit",
"minimize_description_default": "Minimizați descrierea în mod implicit",
"language_selection": "Selecție limbă",
"language_selection": "Limbă",
"instances_list": "Listă de Instanțe",
"enabled_codecs": "Codecuri activate (multiple)",
"loop_this_video": "Repetare video",
@ -55,7 +55,7 @@
"delete_account": "Ștergeți-vă contul",
"show_watch_on_youtube": "Afișați butonul „Vizionați pe YouTube”",
"invalidate_session": "Deconectați toate dispozitivele",
"instance_auth_selection": "Selecție instanță de autentificare",
"instance_auth_selection": "Instanța de autentificare",
"clone_playlist_success": "Clonată cu succes!",
"reset_preferences": "Resetați preferințele",
"confirm_reset_preferences": "Sunteți sigur că doriți să vă resetați preferințele?",
@ -86,7 +86,7 @@
"theme": "Temă",
"autoplay_video": "Redare automată video",
"buffering_goal": "Obiectiv de tamponare (în secunde)",
"instance_selection": "Selecție instanță",
"instance_selection": "Instanță",
"store_watch_history": "Rețineți istoricul de vizionări",
"minimize_comments": "Minimizați comentariile",
"minimize_description": "Minimizați descrierea",
@ -112,11 +112,16 @@
"dismiss": "Concediază",
"group_name": "Numele grupului",
"create_group": "Creați un grup",
"auto_display_captions": "Afișează automat subtitrările",
"auto_display_captions": "Afișare automată subtitrări",
"playlist_name": "Numele playlist-ului",
"okay": "Bine",
"okay": "OK",
"playlist_description": "Descrierea playlist-ului",
"edit_playlist": "Editează playlist-ul"
"edit_playlist": "Editează playlist-ul",
"cancel": "Anulare",
"chapters_layout_mobile": "Mod afișare capitole pe mobil",
"show_search_suggestions": "Afișare sugestii căutare",
"enable_dearrow": "Activați DeArrow",
"delete_automatically": "Șterge automat după"
},
"preferences": {
"ssl_score": "Scor SSL",
@ -143,7 +148,9 @@
"live": "{0} în direct",
"videos": "Videoclipuri",
"category": "Categorie",
"all": "Tot"
"all": "Tot",
"chapters_horizontal": "Orizontal",
"chapters_vertical": "Vertical"
},
"login": {
"username": "Nume de utilizator",
@ -158,7 +165,8 @@
"playlists": "YouTube: Liste de redare",
"music_songs": "YT Music: Muzică",
"music_videos": "YT Music: Videoclipuri",
"music_albums": "YT Music: Albume"
"music_albums": "YT Music: Albume",
"music_artists": "YT Music: Artiști"
},
"info": {
"cannot_copy": "Nu s-a putut copia!",
@ -167,7 +175,8 @@
"copied": "Copiat!",
"register_no_email_note": "Utilizarea unui e-mail ca nume de utilizator nu este recomandată. Continuați oricum?",
"local_storage": "Această acțiune necesită localStorage, sunt activate cookie-urile?",
"next_video_countdown": "Redarea următorului videoclip în {0}s"
"next_video_countdown": "Redarea următorului videoclip în {0}s",
"days": "{amount} zi(le)"
},
"subscriptions": {
"subscribed_channels_count": "Abonat la: {0}"
@ -187,7 +196,8 @@
"preferences": "Preferințe",
"player": "Player-ul",
"bookmarks": "Marcaje",
"channel_groups": "Grupuri de canale"
"channel_groups": "Grupuri de canale",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "Vizionați pe {0}"

View file

@ -14,10 +14,12 @@
"livestreams": "Прямые трансляции",
"channels": "Каналы",
"bookmarks": "Закладки",
"channel_groups": "Группы каналов"
"channel_groups": "Группы каналов",
"dearrow": "DeArrow"
},
"player": {
"watch_on": "Смотреть на {0}"
"watch_on": "Посмотреть на {0}",
"failed": "Ошибка с кодом {0}, откройте логи, чтобы узнать больше"
},
"actions": {
"subscribe": "Подписаться - {count}",
@ -59,7 +61,7 @@
"yes": "Да",
"no": "Нет",
"export_to_json": "Экспорт в JSON",
"import_from_json": "Импорт из JSON/CSV",
"import_from_json": "Импорт из JSON",
"loop_this_video": "Повтор текущего видео",
"auto_play_next_video": "Сразу проигрывать следующее рекомендованное видео",
"donations": "Пожертвования на разработку",
@ -138,7 +140,15 @@
"okay": "Хорошо",
"auto_display_captions": "Авто-отображение субтитров",
"playlist_name": "Название плейлиста",
"show_search_suggestions": "Показать поисковые предложения"
"show_search_suggestions": "Показать поисковые предложения",
"chapters_layout_mobile": "Расположение глав в мобильном виде",
"delete_automatically": "Автоматическое удаление после",
"enable_dearrow": "Включить DeArrow",
"generate_qrcode": "Сгенерировать QR Код",
"import_from_json_csv": "Импорт из JSON/CSV",
"download_frame": "Сделать скриншот видео",
"instance_privacy_policy": "Политика конфиденциальности",
"add_to_group": "Добавить в группу"
},
"comment": {
"pinned_by": "Закреплено пользователем {author}",
@ -157,7 +167,9 @@
},
"login": {
"username": "Имя пользователя",
"password": "Пароль"
"password": "Пароль",
"password_confirm": "Повторите пароль",
"passwords_incorrect": "Пароль не совпадает!"
},
"video": {
"videos": "Видео",
@ -169,7 +181,11 @@
"chapters": "Содержание",
"shorts": "Shorts",
"all": "Все",
"category": "Категория"
"category": "Категория",
"chapters_horizontal": "Горизонтально",
"chapters_vertical": "Вертикально",
"visibility": "Видимость",
"license": "Лицензия"
},
"search": {
"did_you_mean": "Может быть вы имели в виду: {0}?",
@ -180,7 +196,8 @@
"music_songs": "YT Music: Композиции",
"music_videos": "YT Music: Видео",
"music_albums": "YT Music: Альбомы",
"music_playlists": "YT Music: Плейлисты"
"music_playlists": "YT Music: Плейлисты",
"music_artists": "YT Music: Исполнители"
},
"subscriptions": {
"subscribed_channels_count": "Подписан на: {0}"
@ -192,6 +209,12 @@
"page_not_found": "Страница не найдена",
"local_storage": "Это действие требует разрешения localStorage, включены ли cookie-файлы?",
"register_no_email_note": "Использование электронной почты в качестве имени пользователя не рекомендуется. Продолжить?",
"next_video_countdown": "Воспроизведение следующего видео через {0} с."
"next_video_countdown": "Следующие видео через {0} с",
"days": "{amount} дней",
"hours": "{amount} час(ов)",
"weeks": "{amount} недель",
"months": "{amount} месяцев",
"login_note": "Войти в аккаунт созданном на этом экземпляре.",
"register_note": "Зарегистрируйте учетную запись для данного экземпляра Piped. Это позволит синхронизировать подписки и списки воспроизведения с учетной записью, так что они будут храниться на стороне сервера. Вы можете использовать все функции без учетной записи, но все данные будут храниться в локальном кэше вашего браузера. Пожалуйста, убедитесь, что вы НЕ используете адрес электронной почты в качестве имени пользователя, и выберите надежный пароль, который вы не используете в других местах."
}
}

View file

@ -1,194 +1,205 @@
{
"titles": {
"trending": "නැගී එන",
"login": "ඇතුළු වන්න",
"register": "ලියාපදිංචි වන්න",
"preferences": "සැකසුම්",
"login": "පිවිසෙන්න",
"register": "ලියාපදිංචි",
"preferences": "අභිප්‍රේත",
"history": "ඉතිහාසය",
"subscriptions": "දායකත්ව",
"subscriptions": "දායකත්ව",
"account": "ගිණුම",
"player": "වාදකය",
"livestreams": "සජීවී ප්‍රවාහ",
"livestreams": "සජීව ප්‍රචාර",
"channels": "නාලිකා",
"playlists": "වාදන ලැයිස්තු",
"instance": "සේවාදායකය",
"bookmarks": "පොත් සලකුණු",
"feed": "නවතම",
"channel_groups": "නාලිකා කණ්ඩායම්"
"bookmarks": "පොත්යොමු",
"feed": "සංග්‍රහය",
"channel_groups": "නාලිකා සමූහ"
},
"actions": {
"subscribe": "දායකවන්න - {count}",
"unsubscribe": "දායක නොවන්න - {count}",
"most_recent": "නවතම",
"least_recent": "පැරණිතම",
"channel_name_asc": "නාලිකාවේ නම (A-Z)",
"channel_name_desc": "නාලිකාවේ නම (Z-A)",
"most_recent": "වඩාත් මෑත",
"least_recent": "ආසන්න මෑත",
"channel_name_asc": "නාලිකාවේ නම (අ-ෆ)",
"channel_name_desc": "නාලිකාවේ නම (ෆ-අ)",
"back": "ආපසු",
"skip_sponsors": "අනුග්‍රහ මඟ හරින්න",
"skip_sponsors": "අනුග්‍රහකයින් මඟහරින්න",
"skip_outro": "අවසන් කාඩ්පත/දායක ලැයිස්තුව මඟ හරින්න",
"skip_preview": "පෙරදසුන/සාරාංශය මඟ හරින්න",
"skip_self_promo": "නොගෙවූ/ස්වයං ප්‍රවර්ධන මඟ හරින්න",
"skip_filler_tangent": "අදාළ නොවන කොටස් මඟහරින්න",
"theme": "පෙනුම",
"theme": "තේමාව",
"dark": "අඳුරු",
"light": "එළිය",
"autoplay_video": "ස්වයංක්‍රීයව වීඩියෝව වාදනය කරන්න",
"auto": "ස්වයං තේරීම",
"default_quality": "පෙරනිමි ගුණත්වය",
"light": "දීප්ත",
"autoplay_video": "දෘශ්‍යක ඉබේ වාදනය",
"auto": "ස්වයං",
"default_quality": "පෙරනිමි ගුණත්වය",
"default_homepage": "පෙරනිමි මුල් පිටුව",
"show_markers": "වාදකයේ මාකර් පෙන්වන්න",
"buffering_goal": "බෆරින් ඉලක්කය (තත්පර වලින්)",
"buffering_goal": "අන්තරාචය ඉලක්කය (තත්. වලින්)",
"enable_sponsorblock": "Sponsorblock සබල කරන්න",
"sort_by": "තේරීම:",
"sort_by": "පෙළගසන්න:",
"skip_highlight": "ඉස්මතු කිරීම් මඟ හරින්න",
"language_selection": "භාෂා තේරීම",
"language_selection": "භාෂා",
"show_more": "තව පෙන්වන්න",
"yes": "ඔව්",
"no": "නැ",
"export_to_json": "JSON වෙත අපනයනය කරන්න",
"import_from_json": "JSON/CSV වෙතින් ආනයනය කරන්න",
"loop_this_video": "මෙම වීඩියෝව ලූප් කරන්න",
"auto_play_next_video": "මීළඟ වීඩියෝව ස්වයංව වාදනය කරන්න",
"no": "නැහැ",
"export_to_json": "JSON ලෙස නිර්යාත කරන්න",
"import_from_json": "JSON/CSV වෙතින් ආාත කරන්න",
"loop_this_video": "මෙම දෘශ්‍යකය පුඩුලන්න",
"auto_play_next_video": "ඊළඟ දෘශ්‍යකය ඉබේ වාදනය කරන්න",
"donations": "සංවර්ධන පරිත්‍යාග",
"minimize_comments": "අදහස් සඟවන්න",
"minimize_comments": "අදහස් හකුළන්න",
"show_comments": "අදහස් පෙන්වන්න",
"minimize_description": "විස්තරය සඟවන්න",
"show_description": "විස්තරය පෙන්වන්න",
"minimize_recommendations": "නිර්දේශ සඟවන්න",
"minimize_description": "විස්තරය හකුළන්න",
"show_description": "විස්තරය පෙන්වන්න",
"minimize_recommendations": "නිර්දේශ හකුළන්න",
"show_recommendations": "නිර්දේශ පෙන්වන්න",
"store_watch_history": "නැරඹීමේ ඉතිහාසය ගබඩා කරන්න",
"store_watch_history": "නැරඹුම් ඉතිහාසය ගබඩා කරන්න",
"enabled_codecs": "සබල කර ඇති කෝඩෙක්ස් (බහු)",
"minimize_description_default": "පෙරනිමියෙන් විස්තරය සඟවන්න",
"minimize_description_default": "පෙරනිමි පරිදි සවිස්තරය සඟවන්න",
"instances_list": "සේවාදායක ලැයිස්තුව",
"instance_selection": "සේවාදායකය තේරීම",
"instance_selection": "සේවාදායකය",
"view_ssl_score": "SSL ලකුණු බලන්න",
"search": "සොයන්න (Ctrl+K)",
"loading": "පූරණය වෙමින්...",
"hide_replies": "පිළිතුරු සඟවන්න",
"load_more_replies": "තවත් පිළිතුරු පූරණය කරන්න",
"add_to_playlist": "වාදන ලැයිස්තුවට එක් කරන්න",
"load_more_replies": "තවත් පිළිතුරු පෙන්වන්න",
"add_to_playlist": "වාදන ලැයිස්තුවට දමන්න",
"create_playlist": "වාදන ලැයිස්තුව සාදන්න",
"delete_playlist": "වාදන ලැයිස්තුව මකන්න",
"select_playlist": "වාදන ලැයිස්තුවක් තෝරන්න",
"please_select_playlist": "කරුණාකර වාදන ලැයිස්තුවක් තෝරන්න",
"please_select_playlist": "වාදන ලැයිස්තුවක් තෝරන්න",
"delete_account": "ගිණුම මකන්න",
"logout": "මෙම උපාංගයෙන් වරනය වන්න",
"minimize_recommendations_default": "පෙරනිමියෙන් නිර්දේශ සඟවන්න",
"invalidate_session": "සියලුම උපාංග වලින් වරනය වන්න",
"clone_playlist": "වාදන ලැයිස්තුව ක්ලෝනය කරන්න",
"logout": "මෙම උපාංගයෙන් නික්මෙන්න",
"minimize_recommendations_default": "පෙරනිමි පරිදි නිර්දේශ හකුළන්න",
"invalidate_session": "සියළුම උපාංග නික්මවන්න",
"clone_playlist": "වාදන ලැයිස්තුවේ අනුපිටපතක්",
"download_as_txt": ".txt ලෙස බාගන්න",
"reset_preferences": "සැකසුම් නැවත සකසන්න",
"backup_preferences": "සැකසුම් උපස්ථ කරන්න",
"restore_preferences": "සැකසුම් නැවත පිහිටුවන්න",
"reset_preferences": "අභිප්‍රේත නැවත සකසන්න",
"backup_preferences": "අභිප්‍රේත උපස්ථ කරන්න",
"restore_preferences": "අභිප්‍රේත ප්‍රත්‍යර්පණය කරන්න",
"back_to_home": "ආපසු මුල් පිටුවට",
"share": "බෙදාගන්න",
"with_timecode": "කාල කේතය සමඟ බෙදා ගන්න",
"piped_link": "පයිප්ඩ් සබැඳිය",
"copy_link": "සබැඳිය පිටපත් කරන්න",
"copy_link": "සබැඳිය පිටපත්",
"time_code": "කාල කේතය (තත්පර වලින්)",
"show_chapters": "පරිච්ඡේද",
"status_page": "තත්ත්වය",
"status_page": "තත්වය",
"source_code": "ප්‍රභව කේතය",
"documentation": "ප්‍රලේඛනය",
"reply_count": "පිළිතුරු {count}",
"with_playlist": "වාදන ලැයිස්තුව සමඟ බෙදා ගන්න",
"bookmark_playlist": "පොත් සලකුණ",
"show_watch_on_youtube": "YouTube එකේ නරඹන්න බොත්තම පෙන්වන්න",
"bookmark_playlist": "පොත්යොමුවක්",
"show_watch_on_youtube": "යූටියුබ්හි නරඹන්න බොත්තම පෙන්වන්න",
"filter": "පෙරහන",
"instance_donations": "සේවාදායක පරිත්යාග",
"instance_donations": "සේවාදායක පරිත්යාග",
"instance_auth_selection": "සත්‍යතාව තහවුරු කිරීම සඳහා සේවාදායකයක් තේරීම",
"view_subscriptions": "දායකත්ව බලන්න",
"uses_api_from": "මොවුන්ගේ API භාවිතා වේ ",
"view_subscriptions": "දායකත්ව බලන්න",
"uses_api_from": "භාවිතා වන යෙ.ක්‍ර.මු. (API): ",
"skip_intro": "විරාම/හඳුන්වාදීමේ සජීවිකරණය මඟ හරින්න",
"skip_interaction": "අන්තර් ක්‍රියා මතක් කිරීම මඟ හරින්න (දායක වන්න)",
"skip_non_music": "ගීත: ගීතය නොවන කොටස මඟ හරින්න",
"remove_from_playlist": "වාදන ලැයිස්තුවෙන් ඉවත් කරන්න",
"audio_only": "ශ්‍රව්‍ය පමණක්",
"country_selection": "රට තේරීම",
"minimize_comments_default": "පෙරනිමියෙන් අදහස් සඟවන්න",
"clear_history": "ඉතිහාසය හිස් කරන්න",
"disable_lbry": "ප්‍රවාහය සඳහා LBRY අබල කරන්න",
"delete_playlist_video_confirm": "වාදන ලැයිස්තුවෙන් වීඩියෝව ඉවත් කරන්නද?",
"remove_from_playlist": "වාදන ලැයිස්තුවෙන් ඉවතන්න",
"audio_only": "හඬ පමණි",
"country_selection": "රට",
"minimize_comments_default": "පෙරනිමි පරිදි අදහස් සඟවන්න",
"clear_history": "ඉතිහාසය මකන්න",
"disable_lbry": "ප්‍රචාරය සඳහා LBRY අබල කරන්න",
"delete_playlist_video_confirm": "වාදන ලැයිස්තුවෙන් දෘශ්‍යකය ඉවත් කරන්නද?",
"delete_playlist_confirm": "මෙම වාදන ලැයිස්තුව මකන්නද?",
"minimize_chapters_default": "පෙරනිමියෙන් පරිච්ඡේද සඟවන්න",
"clone_playlist_success": "සාර්ථකව ක්ලෝන කරන ලදී!",
"confirm_reset_preferences": "ඔබට ඔබේ සැකසුම් යළි සැකසීමට අවශ්‍ය බව විශ්වාසද?",
"follow_link": "සබැඳිය අනුගමනය කරන්න",
"minimize_chapters_default": "පෙරනිමි පරිදි පරිච්ඡේද හකුළන්න",
"clone_playlist_success": "අනුපිටපතක් සෑදිණි!",
"confirm_reset_preferences": "ඔබගේ අභිප්‍රේත නැවත සැකසීමට වුවමනා ද?",
"follow_link": "සබැඳියට යන්න",
"store_search_history": "සෙවුම් ඉතිහාසය ගබඩා කරන්න",
"no_valid_playlists": "ගොනුවේ වලංගු වාදන ලැයිස්තු අඩංගු නොවේ!",
"playlist_bookmarked": "පොත් සලකුණු කර ඇත",
"enable_lbry_proxy": "LBRY සඳහා Proxy සබල කරන්න",
"playlist_bookmarked": "පොත්යොමුවක් යෙදිණි",
"enable_lbry_proxy": "LBRY සඳහා ප්‍රතියුක්තය සබල කරන්න",
"different_auth_instance": "සත්‍යතාව තහවුරු කිරීම සඳහා වෙනත් සේවාදායකයක් භාවිතා කරන්න",
"hide_watched": "නවතම කොටසෙහි නැරඹූ වීඩියෝ සඟවන්න",
"skip_button_only": "මඟ හරින්න බොත්තම පෙන්වන්න",
"hide_watched": "සංග්‍රහයෙන් නැරඹූ දෘශ්‍යක සඟවන්න",
"skip_button_only": "මඟහරින බොත්තම පෙන්වන්න",
"skip_automatically": "ස්වයංක්‍රීයව",
"skip_segment": "කොටස මඟ හරින්න",
"min_segment_length": "අවම කොටස් දිග (තත්පර වලින්)",
"show_less": "අඩුවෙන් පෙන්වන්න",
"dismiss": "අ් කරන්න",
"autoplay_next_countdown": "ඊළඟ වීඩියෝව තෙක් ගණන් කිරීම (තත්පර වලින්)",
"group_name": "කණ්ඩායමේ නම",
"create_group": "කණ්ඩායම සාදන්න",
"cancel": "අවලංගු කරන්න",
"dismiss": "අයින් කරන්න",
"autoplay_next_countdown": "ඊළඟ දෘශ්‍යකය තෙක් ගණන් කිරීම (තත්. වලින්)",
"group_name": "සමූහයේ නම",
"create_group": "සමූහය සාදන්න",
"cancel": "අවලංගු",
"okay": "හරි",
"edit_playlist": "වාදන ලැයිස්තුව සංස්කරණය කරන්න",
"edit_playlist": "වාදන ලැයිස්තුව සංස්කරණය",
"playlist_name": "වාදන ලැයිස්තුවේ නම",
"playlist_description": "වාදන ලැයිස්තු විස්තරය",
"auto_display_captions": "ස්වයංක්‍රීය උපසිරැසි පෙන්වන්න"
"playlist_description": "වාදන ලැයිස්තුවේ සවිස්තරය",
"auto_display_captions": "උපසිරැසි ස්වයංක්‍රීයව පෙන්වන්න",
"show_search_suggestions": "සෙවුම් යෝජනා පෙන්වන්න",
"delete_automatically": "මෙයින් පසුව මකන්න",
"generate_qrcode": "QR කේතයක් උත්පාදනය"
},
"player": {
"watch_on": "{0} එකේ නරඹන්න"
"watch_on": "{0} හි නරඹන්න"
},
"comment": {
"pinned_by": "{author} විසින් අමුණන ලදී",
"loading": "අදහස් පූරණය වෙමින්...",
"disabled": "උඩුගත කරන්නා විසින් අදහස් අබල කර ඇත.",
"user_disabled": "සැකසුම් තුළ අදහස් අබල කර ඇත."
"user_disabled": "සැකසුම් හරහා අදහස් අබල කර ඇත."
},
"preferences": {
"has_cdn": "CDN තිබේද?",
"version": "නිකුතු අංකය",
"version": "අනුවාදය",
"up_to_date": "යාවත්කාලීනද?",
"instance_name": "සේවාදායකයේ නම",
"registered_users": "ලියාපදිංචි පරිශීලකයන්",
"registered_users": "ලියාපදිංචි පරිශ්‍රීලකයන්",
"ssl_score": "SSL ලකුණු",
"instance_locations": "සේවාදායක ස්ථාන"
"instance_locations": "සේවාදායකයේ ස්ථාන"
},
"login": {
"username": "පරිශීලක නාමය",
"username": "පරිශ්‍රීලක නාමය",
"password": "මුරපදය"
},
"video": {
"videos": "වීඩියෝ",
"videos": "දෘශ්‍යක",
"views": "බැලීම් {views}",
"watched": "නැරඹුවා",
"sponsor_segments": "අනුග්‍රාහක අංශ",
"chapters": "පරිච්ඡේද",
"shorts": "කෙටි වීඩියෝ",
"shorts": "කෙටි දෘශ්‍යක",
"ratings_disabled": "ශ්‍රේණිගත කිරීම් අබල කර ඇත",
"live": "{0} සජීවී",
"all": "සියල්ල",
"category": "කාණ්ඩය"
"category": "ප්‍රවර්ගය",
"chapters_vertical": "සිරස්",
"license": "බලපත්‍රය",
"chapters_horizontal": "තිරස්"
},
"search": {
"did_you_mean": "ඔබ අදහස් කළේ: {0}?",
"videos": "YouTube: වීඩියෝ",
"playlists": "YouTube: වාදන ලැයිස්තු",
"music_songs": "YT Music: ගීත",
"music_videos": "YT Music: වීඩියෝ",
"videos": "යූටියුබ්: දෘශ්‍යක",
"playlists": "යූටියුබි: වාදන ලැයිස්තු",
"music_songs": "යූටියුබි ගීත: ගීත",
"music_videos": "යූටියුබි ගීත: දෘශ්‍යක",
"music_albums": "YT Music: ඇල්බම",
"music_playlists": "YT Music: වාදන ලැයිස්තු",
"channels": "YouTube: නාලිකා",
"all": "YouTube: සියල්ල"
"music_playlists": "යූටියුබි ගීත: වාදන ලැයිස්තු",
"channels": "යූටියුබ්: නාලිකා",
"all": "යූටියුබි: සියල්ල",
"music_artists": "යූටියුබ් ගීත: කලාකරුවන්"
},
"info": {
"page_not_found": "පිටුව හමු නොවීය",
"copied": "පිටපත් කළා!",
"cannot_copy": "පිටපත් කළ නොහැක!",
"local_storage": "මෙම ක්‍රියාවට localStorage අවශ්‍ය වේ, cookies සබල කර තිබේද?",
"register_no_email_note": "පරිශීලක නාමය ලෙස විද්‍යුත් තැපෑලක් භාවිතා කිරීම නිර්දේශ නොකරයි. කෙසේ හෝ ඉදිරියට යන්නද?",
"preferences_note": "සටහන: සැකසුම් ඔබගේ බ්‍රවුසරයේ දේශීය ගබඩාවේ සුරකිනු ලැබේ. ඔබගේ බ්‍රවුසර දත්ත මැකීමෙන් ඒවා නැවත සකසනු ඇත.",
"next_video_countdown": "මීළඟ වීඩියෝව තත්පර {0}කින් වාදනය වේ"
"page_not_found": "පිටුව හමු නොවිණි",
"copied": "පිටපත් විය!",
"cannot_copy": "පිටපත් නොවේ!",
"local_storage": "මෙම ක්‍රියාමාර්ගයට ස්ථානීය-ආචයනය වුවමනාය, දත්තකඩ සබල කර තිබේද?",
"register_no_email_note": "පරිශ්‍රීලක නාමය ලෙස වි-තැපෑලක් භාවිතය නිර්දේශ නොකෙරේ. ඉදිරියට යන්නද?",
"preferences_note": "සටහන: ඔබගේ අතිරික්සුවේ ස්ථානීය ආචයනයේ අභිප්‍රේත සුරැකෙයි. අතිරික්සුවේ දත්ත මැකීමෙන් ඒවා අහිමි වනු ඇත.",
"next_video_countdown": "ඊළඟ දෘශ්‍යකය තත්. {0} කින් වාදනය වේ",
"days": "දවස් {amount}",
"weeks": "සති {amount}",
"hours": "පැය {amount}",
"months": "මාස {amount}"
},
"subscriptions": {
"subscribed_channels_count": "දායක වූයේ: {0}"

View file

@ -10,7 +10,12 @@
"playlists": "Spellistor",
"account": "Konto",
"instance": "Instans",
"player": "Spelare"
"player": "Spelare",
"bookmarks": "Bokmärken",
"dearrow": "DeArrow",
"livestreams": "Livesändningar",
"channels": "Kanaler",
"channel_groups": "Kanal Grupper"
},
"actions": {
"subscribe": "Prenumerera - {count}",
@ -46,18 +51,18 @@
"store_watch_history": "Spara titthistorik",
"instances_list": "Lista över instanser",
"enabled_codecs": "Aktivera codecs (flera)",
"import_from_json": "Importera från JSON/CSV",
"donations": "Donationer",
"import_from_json": "Importera från JSON",
"donations": "Donationer till utveckling",
"filter": "Filter",
"hide_replies": "Dölj svar",
"load_more_replies": "Ladda fler svar",
"enable_sponsorblock": "Aktivera sponsorblockering",
"skip_preview": "Hoppa över förhandsgranskning/sammanfattning",
"autoplay_video": "Spela upp video automatiskt",
"country_selection": "Val av land",
"language_selection": "Val av språk",
"country_selection": "Land",
"language_selection": "Språk",
"skip_non_music": "Hoppa över musik: Icke-musikaliskt avsnitt",
"instance_selection": "Val av instans",
"instance_selection": "Instans",
"show_more": "Visa mer",
"yes": "Ja",
"no": "Nej",
@ -67,7 +72,7 @@
"show_recommendations": "Visa rekommendationer",
"disable_lbry": "Inaktivera LBRY för strömning",
"enable_lbry_proxy": "Aktivera proxy för LBRY",
"search": "Sök",
"search": "Sök (Ctrl+K)",
"clear_history": "Rensa historik",
"skip_filler_tangent": "Hoppa över påfyllningstangent",
"skip_highlight": "Hoppa över höjdpunkt",
@ -87,15 +92,62 @@
"minimize_recommendations_default": "Minimera rekommendationer som standard",
"invalidate_session": "Logga ut alla enheter",
"different_auth_instance": "Använd en annan instans för autentisering",
"instance_auth_selection": "Val av autentiseringsinstans",
"instance_auth_selection": "Autentiseringsinstans",
"download_as_txt": "Ladda ner som .txt",
"reset_preferences": "Återställ inställningar",
"confirm_reset_preferences": "Är du säker på att du vill återställa dina inställningar?",
"backup_preferences": "Inställningar för säkerhetskopiering",
"restore_preferences": "Återställa inställningar"
"restore_preferences": "Återställa inställningar",
"enable_dearrow": "Aktivera DeArrow",
"autoplay_next_countdown": "Antal sekunder tills nästa video startar automatiskt",
"minimize_comments_default": "Minimera kommentarer som standard",
"show_watch_on_youtube": "Visa knappen \"Titta på YouTube\"",
"back_to_home": "Tillbaka till startsidan",
"delete_automatically": "Ta bort automatiskt efter",
"with_timecode": "Dela med tidsstämpel",
"reply_count": "{count} svar",
"with_playlist": "Dela med spellista",
"dismiss": "Avböj",
"min_segment_length": "Minsta segmentlängd (i sekunder)",
"skip_segment": "Hoppa över segment",
"minimize_comments": "Minimera kommentarer",
"show_less": "Visa mindre",
"cancel": "Avbryt",
"store_search_history": "Spara sökhistorik",
"documentation": "Dokumentation",
"okay": "Okej",
"status_page": "Status",
"minimize_chapters_default": "Minimera kapitel som standard",
"time_code": "Tidsstämpel (i sekunder)",
"hide_watched": "Dölj tittade videor i flödet",
"share": "Dela",
"show_chapters": "Kapitel",
"source_code": "Källkod",
"edit_playlist": "Redigera spellista",
"playlist_name": "Spellistans namn",
"playlist_description": "Beskrivning av spellista",
"generate_qrcode": "Generera QR-kod",
"chapters_layout_mobile": "Layout för kapitel på mobil",
"piped_link": "Piped-länk",
"follow_link": "Följ-länk",
"copy_link": "Kopiera länk",
"group_name": "Gruppnamn",
"show_search_suggestions": "Visa sökförslag",
"auto_display_captions": "Automatisk visning av textning",
"bookmark_playlist": "Bokmärke",
"instance_donations": "Instans donationer",
"no_valid_playlists": "Filen innehåller inga giltiga spellistor!",
"playlist_bookmarked": "Bokmärkt",
"create_group": "Skapa grupp",
"skip_button_only": "Visa hoppa över-knapp",
"skip_automatically": "Automatiskt",
"download_frame": "Ladda ner bildruta",
"import_from_json_csv": "Importera från JSON/CSV",
"instance_privacy_policy": "Sekretesspolicy"
},
"player": {
"watch_on": "Titta på {0}"
"watch_on": "Se på {0}",
"failed": "Misslyckades med felkod {0}, se loggar för mer information"
},
"preferences": {
"instance_name": "Instansnamn",
@ -108,7 +160,9 @@
},
"login": {
"username": "Användarnamn",
"password": "Lösenord"
"password": "Lösenord",
"password_confirm": "Bekräfta lösenord",
"passwords_incorrect": "Lösenorden stämmer inte överens!"
},
"video": {
"videos": "Videor",
@ -118,7 +172,13 @@
"ratings_disabled": "Betyg inaktiverade",
"chapters": "Kapitel",
"live": "{0} Live",
"shorts": "Shorts"
"shorts": "Shorts",
"license": "Licens",
"all": "Alla",
"category": "Kategori",
"chapters_horizontal": "Horisontell",
"visibility": "Synlighet",
"chapters_vertical": "Vertikal"
},
"comment": {
"pinned_by": "Fäst av {author}",
@ -135,7 +195,8 @@
"all": "YouTube: Alla",
"videos": "YouTube: Videor",
"playlists": "YouTube: Spellistor",
"music_songs": "YT Music: Låtar"
"music_songs": "YT Music: Låtar",
"music_artists": "YT Music: Artister"
},
"subscriptions": {
"subscribed_channels_count": "Prenumererar på: {0}"
@ -144,6 +205,18 @@
"preferences_note": "Observera: inställningar sparas i webbläsarens lokala lagring. Om du raderar dina webbläsardata återställs de."
},
"info": {
"register_no_email_note": "Det rekommenderas inte att använda e-post som användarnamn. Fortsätt ändå?"
"register_no_email_note": "Det rekommenderas inte att använda e-post som användarnamn. Fortsätt ändå?",
"hours": "{amount} timma(r)",
"preferences_note": "Obs: Inställningarna sparas i det lokala lagringsutrymmet i din webbläsare. Om du raderar dina webbläsardata återställs de.",
"days": "{amount} dag(ar)",
"weeks": "{amount} vecka/veckor",
"months": "{amount} månad(er)",
"next_video_countdown": "Spelar nästa video om {0}s",
"cannot_copy": "Kan inte kopiera!",
"page_not_found": "Sida hittas ej",
"copied": "Kopierad!",
"local_storage": "Det här kräver localStorage, är cookies aktiverat?",
"login_note": "Logga in med ett konto som skapats på denna instans.",
"register_note": "Registrera ett konto för den här Piped-instansen. Då kan du synkronisera dina prenumerationer och spellistor med ditt konto, så att de lagras på serversidan. Du kan använda alla funktioner utan konto, men alla data lagras i webbläsarens lokala cache. Se till att du INTE använder en e-postadress som användarnamn och välj ett säkert lösenord som du inte använder någon annanstans."
}
}

View file

@ -1,12 +1,12 @@
{
"actions": {
"instances_list": "Örnek Listesi",
"language_selection": "Dil Seçimi",
"instances_list": "Sunucu Listesi",
"language_selection": "Dil",
"store_watch_history": "İzleme Geçmişini Sakla",
"minimize_description_default": "Açıklamayı Öntanımlı Olarak Küçült",
"show_comments": "Yorumları Göster",
"default_homepage": "Öntanımlı Ana Sayfa",
"country_selection": "Ülke Seçimi",
"country_selection": "Ülke",
"buffering_goal": "Arabelleğe Alma Hedefi (Saniye Cinsinden)",
"default_quality": "Öntanımlı Kalite",
"audio_only": "Yalnızca Ses",
@ -41,12 +41,12 @@
"donations": "Geliştirme Bağışları",
"auto_play_next_video": "Sonraki Videoyu Otomatik Oynat",
"loop_this_video": "Bu Videoyu Döngüye Al",
"import_from_json": "JSON/CSV Dosyasından İçe Aktar",
"import_from_json": "JSON Dosyasından İçe Aktar",
"export_to_json": "JSON Olarak Dışa Aktar",
"no": "Hayır",
"yes": "Evet",
"show_more": "Daha Fazla Göster",
"instance_selection": "Örnek Seçimi",
"instance_selection": "Sunucu",
"loading": "Yükleniyor...",
"filter": "Filtrele",
"search": "Ara (Ctrl+K)",
@ -70,9 +70,9 @@
"delete_account": "Hesabı Sil",
"logout": "Bu Aygıttan Oturumu Kapat",
"minimize_recommendations_default": "Önerileri Öntanımlı Olarak Küçült",
"different_auth_instance": "Kimlik Doğrulama İçin Farklı Bir Örnek Kullan",
"different_auth_instance": "Kimlik Doğrulama İçin Farklı Bir Sunucu Kullan",
"invalidate_session": "Tüm Aygıtlardan Oturumu Kapat",
"instance_auth_selection": "Kimlik Doğrulama Örneği Seçimi",
"instance_auth_selection": "Kimlik Doğrulama Sunucusu",
"clone_playlist": "Oynatma Listesini Kopyala",
"clone_playlist_success": "Başarıyla kopyalandı!",
"download_as_txt": ".txt Olarak İndir",
@ -92,7 +92,7 @@
"hide_watched": "Akışta İzlenen Videoları Gizle",
"source_code": "Kaynak Kodu",
"documentation": "Belgelendirme",
"instance_donations": "Örnek Bağışları",
"instance_donations": "Sunucu Bağışları",
"status_page": "Durum",
"reply_count": "{count} Yanıt",
"minimize_comments": "Yorumları Küçült",
@ -118,10 +118,19 @@
"edit_playlist": "Oynatma listesini düzenle",
"playlist_name": "Oynatma listesi adı",
"playlist_description": "Oynatma listesi açıklaması",
"show_search_suggestions": "Arama önerilerini göster"
"show_search_suggestions": "Arama önerilerini göster",
"chapters_layout_mobile": "Mobilde Bölüm Düzeni",
"delete_automatically": "Şundan sonra otomatik olarak sil",
"enable_dearrow": "DeArrow'u Etkinleştir",
"generate_qrcode": "QR Kodu Oluştur",
"import_from_json_csv": "JSON/CSV Dosyasından İçe Aktar",
"download_frame": "Kareyi indir",
"instance_privacy_policy": "Gizlilik Politikası",
"add_to_group": "Gruba ekle"
},
"player": {
"watch_on": "{0} Üzerinde İzle"
"watch_on": "{0} üzerinde görüntüle",
"failed": "{0} hata kodu ile başarısız oldu, daha fazla bilgi için günlüklere bakın"
},
"titles": {
"history": "Geçmiş",
@ -133,12 +142,13 @@
"subscriptions": "Abonelikler",
"playlists": "Oynatma Listeleri",
"account": "Hesap",
"instance": "Örnek",
"instance": "Sunucu",
"player": "Oynatıcı",
"livestreams": "Canlı Yayınlar",
"channels": "Kanallar",
"bookmarks": "Yer İmleri",
"channel_groups": "Kanal grupları"
"channel_groups": "Kanal grupları",
"dearrow": "DeArrow"
},
"video": {
"sponsor_segments": "Sponsorlar Bölümleri",
@ -150,13 +160,17 @@
"live": "{0} Canlı",
"shorts": "Kısa çekimler",
"all": "Tümü",
"category": "Kategori"
"category": "Kategori",
"chapters_horizontal": "Yatay",
"chapters_vertical": "Dikey",
"license": "Lisans",
"visibility": "Görünürlük"
},
"preferences": {
"ssl_score": "SSL Puanı",
"has_cdn": "CDN Var Mı?",
"instance_locations": "Örnek Konumları",
"instance_name": "Örnek Adı",
"instance_locations": "Sunucu Konumları",
"instance_name": "Sunucu Adı",
"registered_users": "Kayıtlı Kullanıcılar",
"version": "Sürüm",
"up_to_date": "Güncel Mi?"
@ -169,7 +183,9 @@
},
"login": {
"password": "Parola",
"username": "Kullanıcı Adı"
"username": "Kullanıcı Adı",
"password_confirm": "Parolayı doğrula",
"passwords_incorrect": "Parolalar eşleşmiyor!"
},
"search": {
"did_you_mean": "Bunu mu demek istediniz: {0}?",
@ -180,7 +196,8 @@
"videos": "YouTube: Videolar",
"music_songs": "YT Müzik: Şarkılar",
"music_videos": "YT Müzik: Videolar",
"music_albums": "YT Müzik: Albümler"
"music_albums": "YT Müzik: Albümler",
"music_artists": "YT Müzik: Sanatçılar"
},
"subscriptions": {
"subscribed_channels_count": "Abone Olunan: {0}"
@ -195,6 +212,12 @@
"cannot_copy": "Kopyalanamıyor!",
"local_storage": "Bu eylem yerel depolama gerektirir, çerezler etkin mi?",
"register_no_email_note": "Kullanıcı adı olarak e-posta kullanılması tavsiye edilmez. Yine de devam edilsin mi?",
"next_video_countdown": "Sonraki video {0}s içinde oynatılıyor"
"next_video_countdown": "Sonraki video {0}s içinde oynatılıyor",
"days": "{amount} gün",
"months": "{amount} ay",
"hours": "{amount} saat",
"weeks": "{amount} hafta",
"login_note": "Bu sunucuda oluşturulan bir hesapla oturum açın.",
"register_note": "Bu Piped sunucusu için bir hesap açın. Bu, aboneliklerinizi ve oynatma listelerinizi hesabınızla eşzamanlamanızı sağlar, böylece sunucu tarafında kaydedilirler. Tüm özellikleri bir hesap olmadan da kullanabilirsiniz, ancak bu şekilde tüm veriler yalnızca tarayıcınızınn yerel önbelleğinde saklanacaktır. Lütfen kullanıcı adı olarak bir e-posta adresi KULLANMADIĞINIZDAN ve başka bir yerde kullanmadığınız güvenli bir parola seçtiğinizden emin olun."
}
}

View file

@ -1,10 +1,13 @@
{
"player": {
"watch_on": "Дивитися на {0}"
"watch_on": "Переглянути на {0}",
"failed": "Помилка з кодом {0}, дивіться журнали для отримання додаткової інформації"
},
"login": {
"username": "Ім'я користувача",
"password": "Пароль"
"password": "Пароль",
"password_confirm": "Підтвердіть пароль",
"passwords_incorrect": "Паролі не збігаються!"
},
"actions": {
"unsubscribe": "Відписатися - {count}",
@ -23,12 +26,12 @@
"default_homepage": "Домашня сторінка за замовчуванням",
"show_comments": "Показати коментарі",
"store_watch_history": "Зберігати історію перегляду",
"language_selection": "Вибір мови",
"instance_selection": "Вибір екземпляра",
"language_selection": "Мова",
"instance_selection": "Екземпляр",
"show_more": "Показати більше",
"no": "Ні",
"export_to_json": "Експортувати в JSON",
"minimize_description": "Згорнути опис",
"minimize_description": "Приховати опис",
"show_recommendations": "Показати рекомендації",
"enable_lbry_proxy": "Увімкнути проксі для LBRY",
"search": "Пошук (Ctrl+K)",
@ -39,7 +42,7 @@
"most_recent": "Найновішими",
"channel_name_desc": "Назвою каналу (Я-А)",
"least_recent": "Найстарішими",
"minimize_recommendations": "Згорнути рекомендації",
"minimize_recommendations": "Приховати рекомендації",
"skip_sponsors": "Пропускати спонсорську рекламу",
"skip_interaction": "Пропускати нагадування про взаємодію (підписка)",
"skip_non_music": "Пропускати сегменти без музики в музикальних відео",
@ -50,10 +53,10 @@
"instances_list": "Список екземплярів",
"enabled_codecs": "Увімкнені кодеки (можна вибрати декілька)",
"default_quality": "Якість за замовчуванням",
"country_selection": "Вибір країни (для трендів)",
"country_selection": "Країна",
"minimize_description_default": "Не розгортати опис за замовчуванням",
"yes": "Так",
"import_from_json": "Імпортувати з JSON/CSV",
"import_from_json": "Імпортувати з JSON",
"loop_this_video": "Зациклити це відео",
"auto_play_next_video": "Автоматичне відтворення наступного відео",
"donations": "Пожертвування на розробку",
@ -74,7 +77,7 @@
"select_playlist": "Вибрати список відтворення",
"please_select_playlist": "Будь ласка, виберіть список відтворення",
"confirm_reset_preferences": "Ви впевнені, що бажаєте скинути свої налаштування?",
"show_markers": "Показувати маркери на Програвачі",
"show_markers": "Показувати маркери на програвачі",
"minimize_recommendations_default": "Згортати рекомендації за замовчуванням",
"logout": "Вийти з цього пристрою",
"backup_preferences": "Налаштування резервного копіювання",
@ -92,9 +95,9 @@
"copy_link": "Копіювати посилання",
"store_search_history": "Зберігати історію пошуку",
"documentation": "Документація",
"instance_auth_selection": "Вибір екземпляра для автентифікації",
"instance_auth_selection": "Екземпляр для автентифікації",
"minimize_chapters_default": "Згортати розділи за замовчуванням",
"show_watch_on_youtube": "Показати кнопку Дивитися на YouTube",
"show_watch_on_youtube": "Показувати кнопку Дивитися на YouTube",
"restore_preferences": "Відновити налаштування",
"different_auth_instance": "Використовувати інший екземпляр для автентифікації",
"clone_playlist_success": "Успішно клоновано!",
@ -104,7 +107,7 @@
"time_code": "Відмітка часу (у секундах)",
"reply_count": "{count} відповідей",
"minimize_comments_default": "Згортати коментарі за замовчуванням",
"minimize_comments": "Згорнути коментарі",
"minimize_comments": "Приховати коментарі",
"delete_account": "Видалити обліковий запис",
"no_valid_playlists": "Файл не містить дійсних списків відтворення!",
"bookmark_playlist": "Закладка",
@ -124,16 +127,25 @@
"playlist_description": "Опис списку відтворення",
"auto_display_captions": "Автоматичне відображення субтитрів",
"cancel": "Скасувати",
"show_search_suggestions": "Показати пошукові пропозицій"
"show_search_suggestions": "Показувати пошукові пропозицій",
"okay": "Добре",
"chapters_layout_mobile": "Макет розділів на телефоні",
"enable_dearrow": "Увімкнути DeArrow",
"delete_automatically": "Видаляти автоматично після",
"generate_qrcode": "Згенерувати QR-код",
"import_from_json_csv": "Імпортувати з JSON/CSV",
"download_frame": "Завантажити кадр",
"instance_privacy_policy": "Політика конфіденційності",
"add_to_group": "Додати до групи"
},
"titles": {
"register": "Реєстрація",
"register": "Зареєструватися",
"feed": "Підписки",
"preferences": "Налаштування",
"history": "Історія перегляду",
"subscriptions": "Канали, на які ви підписані",
"trending": "Тренди",
"login": "Логін",
"login": "Увійти",
"playlists": "Списки відтворення",
"instance": "Екземпляр",
"player": "Програвач",
@ -141,7 +153,8 @@
"livestreams": "Наживо",
"channels": "Канали",
"bookmarks": "Закладки",
"channel_groups": "Групи каналів"
"channel_groups": "Групи каналів",
"dearrow": "DeArrow"
},
"comment": {
"pinned_by": "Прикріплено користувачем {author}",
@ -168,7 +181,11 @@
"live": "{0} Наживо",
"shorts": "Shorts",
"all": "Усі",
"category": "Категорія"
"category": "Категорія",
"chapters_horizontal": "Горизонтальний",
"chapters_vertical": "Вертикальний",
"visibility": "Видимість",
"license": "Ліцензія"
},
"search": {
"did_you_mean": "Можливо, ви мали на увазі: {0}?",
@ -179,7 +196,8 @@
"music_songs": "YT Music: Пісні",
"music_videos": "TY Music: Відео",
"playlists": "YouTube: Списки відтворення",
"music_albums": "YT Music: Альбоми"
"music_albums": "YT Music: Альбоми",
"music_artists": "YT Music: Артисти"
},
"subscriptions": {
"subscribed_channels_count": "Підписано на: {0}"
@ -191,6 +209,12 @@
"preferences_note": "Примітка: налаштування зберігаються в локальній пам'яті вашого браузера. Видалення даних браузера призведе до їх скидання.",
"local_storage": "Ця дія потребує localStorage, чи ввімкнуті файли cookie?",
"register_no_email_note": "Використання електронної пошти як імені користувача не рекомендується. Все одно продовжити?",
"next_video_countdown": "Наступне відео через {0} секунд"
"next_video_countdown": "Наступне відео через {0} секунд",
"weeks": "{amount} тиждень(-і)",
"hours": "{amount} годин(-и)",
"months": "{amount} місяць(-і)",
"days": "{amount} день(-і)",
"login_note": "Увійдіть за допомогою облікового запису, створеного на цьому екземплярі.",
"register_note": "Зареєструйте обліковий запис для цього екземпляра Piped. Це дозволить вам синхронізувати підписки та списки відтворення з вашим обліковим записом, щоб вони зберігалися на сервері. Ви також можете використовувати всі доступні функції без облікового запису, але всі дані будуть зберігатися в локальному кеші вашого браузера. Будь ласка, переконайтеся, що ви НЕ використовуєте свою адресу електронної пошти як ім'я користувача й обрали надійний пароль, який ви не використовуєте в інших місцях."
}
}

View file

@ -3,7 +3,7 @@
"subscribe": "Đăng ký - {count}",
"autoplay_video": "Video tự động phát",
"unsubscribe": "Hủy đăng ký - {count}",
"view_subscriptions": "Lượt đăng ký",
"view_subscriptions": "Xem những kênh đã đăng ký",
"sort_by": "Sắp xếp theo:",
"most_recent": "Gần đây nhất",
"channel_name_asc": "Tên kênh (A-Z)",
@ -14,7 +14,7 @@
"theme": "Giao diện",
"auto": "Tự động",
"buffering_goal": "Bộ nhớ đệm (tính bằng giây)",
"least_recent": "Ít nhất gần đây",
"least_recent": "Cũ nhất",
"skip_intro": "Bỏ qua gián đoạn/hoạt hình intro",
"skip_outro": "Bỏ qua màn hình kết thúc/danh đề",
"skip_interaction": "Bỏ qua lời nhắc tương tác (Đăng ký)",
@ -43,7 +43,7 @@
"disable_lbry": "Tắt LBRY để phát trực tuyến",
"enable_lbry_proxy": "Bật proxy cho LBRY",
"view_ssl_score": "Hiện thị điểm số SSL",
"search": "Tìm kiếm",
"search": "Tìm kiếm (Ctrl+K)",
"filter": "Bộ lọc",
"loading": "Đang tải...",
"clear_history": "Xóa lịch sử",
@ -78,7 +78,11 @@
"minimize_comments": "Thu nhỏ bình luận",
"reply_count": "{count} phản hồi",
"status_page": "Trạng thái",
"skip_automatically": "Tự động"
"skip_automatically": "Tự động",
"show_chapters": "Chương",
"show_less": "Hiển thị ít hơn",
"cancel": "Hủy",
"okay": "OK"
},
"titles": {
"register": "Đăng ký",
@ -142,7 +146,8 @@
"info": {
"copied": "Đã sao chép!",
"cannot_copy": "Không thể sao chép!",
"page_not_found": "Không tìm thấy trang"
"page_not_found": "Không tìm thấy trang",
"next_video_countdown": "Tự động phát video tiếp theo trong {0} giây"
},
"subscriptions": {
"subscribed_channels_count": "Đã đăng ký cho: {0}"

View file

@ -9,20 +9,20 @@
"donations": "开发捐赠",
"auto_play_next_video": "自动播放下一个视频",
"loop_this_video": "循环播放此视频",
"import_from_json": "从 JSON/CSV 导入",
"import_from_json": "从 JSON 导入",
"export_to_json": "导出为 JSON",
"no": "否",
"yes": "是",
"show_more": "显示更多",
"instance_selection": "实例选择",
"instance_selection": "实例",
"enabled_codecs": "启用的编解码器 (多个)",
"instances_list": "实例列表",
"language_selection": "语言选择",
"language_selection": "语言",
"store_watch_history": "保存观看历史",
"minimize_description_default": "默认情况下折叠描述",
"show_comments": "显示评论",
"default_homepage": "默认主页",
"country_selection": "国家/地区选择",
"country_selection": "国家/地区",
"buffering_goal": "缓冲目标 (以秒为单位)",
"default_quality": "默认质量",
"audio_only": "仅音频",
@ -72,7 +72,7 @@
"minimize_recommendations_default": "默认最小化推荐",
"invalidate_session": "注销所有设备",
"different_auth_instance": "使用不同的实例进行身份验证",
"instance_auth_selection": "身份验证实例选择",
"instance_auth_selection": "身份验证实例",
"clone_playlist": "克隆播放列表",
"clone_playlist_success": "克隆成功!",
"download_as_txt": "下载为 .txt",
@ -119,7 +119,14 @@
"okay": "好的",
"edit_playlist": "编辑播放列表",
"show_search_suggestions": "显示搜索建议",
"chapters_layout_mobile": "移动设备上的章节布局"
"chapters_layout_mobile": "移动设备上的章节布局",
"delete_automatically": "多久后自动删除",
"enable_dearrow": "启用 DeArrow",
"generate_qrcode": "生成二维码",
"import_from_json_csv": "从 JSON/CSV 文件导入",
"download_frame": "下载视频帧",
"instance_privacy_policy": "隐私政策",
"add_to_group": "添加到组"
},
"video": {
"sponsor_segments": "赞助商部分",
@ -133,7 +140,9 @@
"all": "全部",
"category": "类别",
"chapters_horizontal": "水平",
"chapters_vertical": "垂直"
"chapters_vertical": "垂直",
"license": "许可证",
"visibility": "可见性"
},
"preferences": {
"ssl_score": "SSL 分数",
@ -151,7 +160,8 @@
"user_disabled": "评论在设置中被禁用。"
},
"player": {
"watch_on": "在 {0} 观看"
"watch_on": "在 {0} 观看",
"failed": "播放器出错,错误代码 {0},查看日志了解更多信息"
},
"titles": {
"feed": "订阅流",
@ -168,11 +178,14 @@
"livestreams": "直播",
"channels": "频道",
"bookmarks": "书签",
"channel_groups": "频道组"
"channel_groups": "频道组",
"dearrow": "DeArrow"
},
"login": {
"password": "密码",
"username": "帐号"
"username": "帐号",
"password_confirm": "确认密码",
"passwords_incorrect": "密码不匹配!"
},
"search": {
"did_you_mean": "你是指 {0} 吗?",
@ -183,7 +196,8 @@
"music_songs": "YT Music歌曲",
"music_videos": "YT Music视频",
"music_albums": "YT Music专辑",
"music_playlists": "YT Music播放列表"
"music_playlists": "YT Music播放列表",
"music_artists": "YT Music艺人"
},
"subscriptions": {
"subscribed_channels_count": "已订阅:{0}"
@ -198,6 +212,12 @@
"cannot_copy": "无法复制!",
"local_storage": "此操作需要本地存储是否启用了Cookie",
"register_no_email_note": "不建议使用电子邮件作为用户名。仍要继续吗?",
"next_video_countdown": "在{0}秒后播放下一个视频"
"next_video_countdown": "在{0}秒后播放下一个视频",
"days": "{amount} 天",
"weeks": "{amount} 周",
"months": "{amount} 个月",
"hours": "{amount} 小时",
"login_note": "使用在此实例上创建的账户登录。",
"register_note": "在这个 Piped 实例上注册一个账户。你可以通过账户同步订阅和播放列表,所有数据存储在实例的服务器上。没有账户你也可以使用所有功能,但数据均储存在你所用浏览器的本地缓存中。请确保你没有将电子邮箱地址用作账户的用户名,并选择一个你不在其他地方使用的安全密码。"
}
}

View file

@ -3,14 +3,14 @@
"skip_intro": "跳過中場休息/開場動畫",
"skip_sponsors": "跳過贊助廣告",
"enable_sponsorblock": "啟用 SponsorBlock",
"country_selection": "選擇國家",
"country_selection": "國家",
"channel_name_desc": "頻道名 (Z-A)",
"channel_name_asc": "頻道名 (A-Z)",
"unsubscribe": "取消訂閱 - {count}",
"subscribe": "訂閱 - {count}",
"sort_by": "排序依據:",
"view_subscriptions": "檢視訂閱內容",
"language_selection": "選擇語言",
"language_selection": "語言",
"skip_non_music": "跳過音樂中的非音樂片段",
"theme": "主題",
"show_description": "顯示說明",
@ -23,7 +23,7 @@
"default_homepage": "預設首頁",
"store_watch_history": "儲存觀看記錄",
"minimize_description": "最小化說明",
"search": "搜尋",
"search": "搜尋 (Ctrl+K)",
"show_recommendations": "顯示推薦",
"minimize_recommendations": "最小化推薦",
"most_recent": "最新",
@ -45,7 +45,7 @@
"import_from_json": "從 JSON/CSV 匯入",
"loop_this_video": "循環播放此影片",
"auto_play_next_video": "自動播放下一部影片",
"donations": "捐款",
"donations": "開發捐款",
"filter": "篩選",
"loading": "載入中……",
"clear_history": "清除記錄",
@ -57,7 +57,7 @@
"delete_playlist_confirm": "要刪除這份播放清單嗎?",
"please_select_playlist": "請選擇播放清單",
"select_playlist": "選擇播放清單",
"add_to_playlist": "加到播放清單",
"add_to_playlist": "增至播放清單",
"delete_playlist_video_confirm": "要從播放清單中移除影片嗎?",
"delete_account": "刪除帳戶",
"show_chapters": "章節",
@ -67,7 +67,60 @@
"confirm_reset_preferences": "確定要重設偏好設定嗎?",
"backup_preferences": "備份偏好設定",
"restore_preferences": "復原偏好設定",
"back_to_home": "回首頁"
"back_to_home": "回首頁",
"uses_api_from": "使用此API ",
"instances_list": "站台列表",
"show_markers": "在播放器上顯示標記",
"skip_button_only": "顯示跳過按鈕",
"skip_filler_tangent": "跳過與影片無關的片段",
"autoplay_next_countdown": "預設播放下一段影片前的倒數時間(秒)",
"min_segment_length": "最短分段的長度(秒)",
"auto_display_captions": "自動顯示字幕",
"minimize_comments": "收起留言",
"disable_lbry": "不使用 LBRY 作為傳輸媒介",
"enable_lbry_proxy": "使用 LBRY 作為代理伺服器",
"view_ssl_score": "查看 SSL 分數",
"logout": "從這個設置登出",
"minimize_comments_default": "預設為收起留言",
"instance_selection": "選擇站台",
"skip_automatically": "自動",
"skip_segment": "跳過分段",
"edit_playlist": "編輯播放清單",
"playlist_name": "播放清單名稱",
"instance_auth_selection": "選擇身分驗證站台",
"instance_donations": "站台捐款",
"invalidate_session": "從所有裝置登出",
"clone_playlist": "複製播放清單",
"clone_playlist_success": "複製成功!",
"different_auth_instance": "使用其他站台進行身分驗證",
"with_timecode": "以時間碼分享",
"follow_link": "追隨連結",
"store_search_history": "儲存搜尋歷史",
"okay": "好的",
"no_valid_playlists": "此檔案不包含有效的播放清單!",
"piped_link": "Piped 連結",
"cancel": "取消",
"playlist_description": "播放清單描述",
"status_page": "狀態",
"source_code": "原始碼",
"bookmark_playlist": "書籤",
"documentation": "文件",
"with_playlist": "以播放清單分享",
"playlist_bookmarked": "已存入書籤",
"create_group": "建立群組",
"group_name": "群組名稱",
"show_search_suggestions": "顯示搜尋建議",
"copy_link": "複製連結",
"time_code": "時間碼(以秒計算)",
"minimize_chapters_default": "預設收起章節",
"dismiss": "解散",
"chapters_layout_mobile": "在手提裝置上的章節佈局",
"hide_watched": "在摘要中隱藏看過的影片",
"reply_count": "{count} 個回覆",
"minimize_recommendations_default": "預設收起推薦影片",
"show_watch_on_youtube": "顯示「在Youtube 觀看」按鈕",
"show_less": "顯示更少",
"generate_qrcode": "產生 QR 碼"
},
"titles": {
"history": "歷史記錄",
@ -79,23 +132,40 @@
"subscriptions": "訂閱內容",
"playlists": "播放清單",
"account": "帳戶",
"player": "播放器"
"player": "播放器",
"instance": "站台",
"bookmarks": "書籤",
"livestreams": "直播",
"channels": "頻道",
"channel_groups": "頻道群組"
},
"preferences": {
"registered_users": "已註冊的使用者",
"version": "版本",
"has_cdn": "是否有 CDN"
"has_cdn": "是否有 CDN",
"instance_name": "站台名稱",
"instance_locations": "站台位置",
"up_to_date": "最新?",
"ssl_score": "SSL 分數"
},
"login": {
"username": "使用者名",
"password": "密碼"
"password": "密碼",
"password_confirm": "確認密碼"
},
"video": {
"videos": "影片",
"watched": "有看過",
"sponsor_segments": "贊助廣告片段",
"ratings_disabled": "評價已停用",
"chapters": "章節"
"chapters": "章節",
"shorts": "短影片",
"category": "類別",
"chapters_horizontal": "水平",
"chapters_vertical": "垂直",
"views": "觀看次數:{views}",
"live": "{0} 直播",
"all": "全部"
},
"search": {
"did_you_mean": "您是否想找 {0}",
@ -106,17 +176,32 @@
"playlists": "YouTube播放清單",
"music_songs": "YT Music歌曲",
"music_videos": "YT Music影片",
"music_albums": "YT Music專輯"
"music_albums": "YT Music專輯",
"music_artists": "YT Music: 藝人"
},
"comment": {
"pinned_by": "置頂者: {author}",
"disabled": "上傳者停用了留言功能。",
"loading": "留言載入中……"
"loading": "留言載入中……",
"user_disabled": "留言在設置中被關閉。"
},
"info": {
"copied": "已複製!",
"cannot_copy": "無法複製!",
"page_not_found": "找不到頁面",
"preferences_note": "註:偏好設定儲存在本機的瀏覽器的儲存空間內。如果清除瀏覽器的資料,偏好設定就會重設。"
"preferences_note": "註:偏好設定儲存在本機的瀏覽器的儲存空間內。如果清除瀏覽器的資料,偏好設定就會重設。",
"register_no_email_note": "不建議使用電子郵件地址作為用戶名稱。仍要繼續嗎?",
"local_storage": "此動作需要儲存資料在本機請確認是否啟用了Cookies",
"next_video_countdown": "在{0}秒後播放下一段影片",
"days": "{amount} 天",
"weeks": "{amount} 個星期",
"months": "{amount} 個月",
"hours": "{amount} 小時"
},
"player": {
"watch_on": "在 {0} 觀看"
},
"subscriptions": {
"subscribed_channels_count": "己訂閱: {0}"
}
}

View file

@ -56,8 +56,6 @@ library.add(
import router from "@/router/router.js";
import App from "./App.vue";
import DOMPurify from "dompurify";
import TimeAgo from "javascript-time-ago";
import en from "javascript-time-ago/locale/en";
@ -120,9 +118,6 @@ const mixin = {
return response.json();
});
},
purifyHTML(original) {
return DOMPurify.sanitize(original);
},
setPreference(key, value, disableAlert = false) {
try {
localStorage.setItem(key, value);
@ -195,19 +190,6 @@ const mixin = {
timeAgo(time) {
return timeAgo.format(time);
},
urlify(string) {
if (!string) return "";
const urlRegex = /(((https?:\/\/)|(www\.))[^\s]+)/g;
const emailRegex = /([\w-\\.]+@(?:[\w-]+\.)+[\w-]{2,4})/g;
return string
.replace(urlRegex, url => {
if (url.endsWith("</a>") || url.endsWith("<a")) return url;
return `<a href="${url}" target="_blank">${url}</a>`;
})
.replace(emailRegex, email => {
return `<a href="mailto:${email}">${email}</a>`;
});
},
async updateWatched(videos) {
if (window.db && this.getPreferenceBoolean("watchHistory", false)) {
var tx = window.db.transaction("watch_history", "readonly");
@ -265,20 +247,26 @@ const mixin = {
elem.click();
elem.remove();
},
rewriteDescription(text) {
return this.urlify(text)
.replaceAll(/(?:http(?:s)?:\/\/)?(?:www\.)?youtube\.com(\/[/a-zA-Z0-9_?=&-]*)/gm, "$1")
.replaceAll(
/(?:http(?:s)?:\/\/)?(?:www\.)?youtu\.be\/(?:watch\?v=)?([/a-zA-Z0-9_?=&-]*)/gm,
"/watch?v=$1",
)
.replaceAll("\n", "<br>");
},
getChannelGroupsCursor() {
if (!window.db) return;
var tx = window.db.transaction("channel_groups", "readonly");
var store = tx.objectStore("channel_groups");
return store.index("groupName").openCursor();
async getChannelGroups() {
return new Promise(resolve => {
let channelGroups = [];
var tx = window.db.transaction("channel_groups", "readonly");
var store = tx.objectStore("channel_groups");
const cursor = store.index("groupName").openCursor();
cursor.onsuccess = e => {
const cursor = e.target.result;
if (cursor) {
const group = cursor.value;
channelGroups.push({
groupName: group.groupName,
channels: JSON.parse(group.channels),
});
cursor.continue();
} else {
resolve(channelGroups);
}
};
});
},
createOrUpdateChannelGroup(group) {
var tx = window.db.transaction("channel_groups", "readwrite");
@ -293,6 +281,275 @@ const mixin = {
var store = tx.objectStore("channel_groups");
store.delete(groupName);
},
async getLocalPlaylist(playlistId) {
return await new Promise(resolve => {
var tx = window.db.transaction("playlists", "readonly");
var store = tx.objectStore("playlists");
const req = store.openCursor(playlistId);
let playlist = null;
req.onsuccess = e => {
playlist = e.target.result.value;
playlist.videos = JSON.parse(playlist.videoIds).length;
resolve(playlist);
};
});
},
createOrUpdateLocalPlaylist(playlist) {
var tx = window.db.transaction("playlists", "readwrite");
var store = tx.objectStore("playlists");
store.put(playlist);
},
// needs to handle both, streamInfo items and streams items
createLocalPlaylistVideo(videoId, videoInfo) {
if (videoInfo === undefined || videoId === null || videoInfo?.error) return;
var tx = window.db.transaction("playlist_videos", "readwrite");
var store = tx.objectStore("playlist_videos");
const video = {
videoId: videoId,
title: videoInfo.title,
type: "stream",
shortDescription: videoInfo.shortDescription ?? videoInfo.description,
url: `/watch?v=${videoId}`,
thumbnail: videoInfo.thumbnail ?? videoInfo.thumbnailUrl,
uploaderVerified: videoInfo.uploaderVerified,
duration: videoInfo.duration,
uploaderAvatar: videoInfo.uploaderAvatar,
uploaderUrl: videoInfo.uploaderUrl,
uploaderName: videoInfo.uploaderName ?? videoInfo.uploader,
};
store.put(video);
},
async getLocalPlaylistVideo(videoId) {
return await new Promise(resolve => {
var tx = window.db.transaction("playlist_videos", "readonly");
var store = tx.objectStore("playlist_videos");
const req = store.openCursor(videoId);
req.onsuccess = e => {
resolve(e.target.result.value);
};
});
},
async getPlaylists() {
if (!this.authenticated) {
if (!window.db) return [];
return await new Promise(resolve => {
let playlists = [];
var tx = window.db.transaction("playlists", "readonly");
var store = tx.objectStore("playlists");
const cursorRequest = store.openCursor();
cursorRequest.onsuccess = e => {
const cursor = e.target.result;
if (cursor) {
let playlist = cursor.value;
playlist.videos = JSON.parse(playlist.videoIds).length;
playlists.push(playlist);
cursor.continue();
} else {
resolve(playlists);
}
};
});
}
return await this.fetchJson(this.authApiUrl() + "/user/playlists", null, {
headers: {
Authorization: this.getAuthToken(),
},
});
},
async getPlaylist(playlistId) {
if (!this.authenticated) {
const playlist = await this.getLocalPlaylist(playlistId);
const videoIds = JSON.parse(playlist.videoIds);
const videosFuture = videoIds.map(videoId => this.getLocalPlaylistVideo(videoId));
playlist.relatedStreams = await Promise.all(videosFuture);
return playlist;
}
return await this.fetchJson(this.authApiUrl() + "/playlists/" + playlistId);
},
async createPlaylist(name) {
if (!this.authenticated) {
const uuid = crypto.randomUUID();
const playlistId = `local-${uuid}`;
this.createOrUpdateLocalPlaylist({
playlistId: playlistId,
// remapping needed for the playlists page
id: playlistId,
name: name,
description: "",
thumbnail: "https://pipedproxy.kavin.rocks/?host=i.ytimg.com",
videoIds: "[]", // empty list
});
return { playlistId: playlistId };
}
return await this.fetchJson(this.authApiUrl() + "/user/playlists/create", null, {
method: "POST",
body: JSON.stringify({
name: name,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
});
},
async deletePlaylist(playlistId) {
if (!this.authenticated) {
const playlist = await this.getLocalPlaylist(playlistId);
var tx = window.db.transaction("playlists", "readwrite");
var store = tx.objectStore("playlists");
store.delete(playlistId);
// delete videos that don't need to be store anymore
const playlists = await this.getPlaylists();
const usedVideoIds = playlists
.filter(playlist => playlist.id != playlistId)
.map(playlist => JSON.parse(playlist.videoIds))
.flat();
const potentialDeletableVideos = JSON.parse(playlist.videoIds);
var videoTx = window.db.transaction("playlist_videos", "readwrite");
var videoStore = videoTx.objectStore("playlist_videos");
for (let videoId of potentialDeletableVideos) {
if (!usedVideoIds.includes(videoId)) videoStore.delete(videoId);
}
return { message: "ok" };
}
return await this.fetchJson(this.authApiUrl() + "/user/playlists/delete", null, {
method: "POST",
body: JSON.stringify({
playlistId: playlistId,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
});
},
async renamePlaylist(playlistId, newName) {
if (!this.authenticated) {
const playlist = await this.getLocalPlaylist(playlistId);
playlist.name = newName;
this.createOrUpdateLocalPlaylist(playlist);
return { message: "ok" };
}
return await this.fetchJson(this.authApiUrl() + "/user/playlists/rename", null, {
method: "POST",
body: JSON.stringify({
playlistId: playlistId,
newName: newName,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
});
},
async changePlaylistDescription(playlistId, newDescription) {
if (!this.authenticated) {
const playlist = await this.getLocalPlaylist(playlistId);
playlist.description = newDescription;
this.createOrUpdateLocalPlaylist(playlist);
return { message: "ok" };
}
return await this.fetchJson(this.authApiUrl() + "/user/playlists/description", null, {
method: "PATCH",
body: JSON.stringify({
playlistId: playlistId,
description: newDescription,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
});
},
async addVideosToPlaylist(playlistId, videoIds, videoInfos) {
if (!this.authenticated) {
const playlist = await this.getLocalPlaylist(playlistId);
const currentVideoIds = JSON.parse(playlist.videoIds);
currentVideoIds.push(...videoIds);
playlist.videoIds = JSON.stringify(currentVideoIds);
let streamInfos =
videoInfos ??
(await Promise.all(videoIds.map(videoId => this.fetchJson(this.apiUrl() + "/streams/" + videoId))));
playlist.thumbnail = streamInfos[0].thumbnail || streamInfos[0].thumbnailUrl;
this.createOrUpdateLocalPlaylist(playlist);
for (let i in videoIds) {
this.createLocalPlaylistVideo(videoIds[i], streamInfos[i]);
}
return { message: "ok" };
}
return await this.fetchJson(this.authApiUrl() + "/user/playlists/add", null, {
method: "POST",
body: JSON.stringify({
playlistId: playlistId,
videoIds: videoIds,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
});
},
async removeVideoFromPlaylist(playlistId, index) {
if (!this.authenticated) {
const playlist = await this.getLocalPlaylist(playlistId);
const videoIds = JSON.parse(playlist.videoIds);
videoIds.splice(index, 1);
playlist.videoIds = JSON.stringify(videoIds);
if (videoIds.length == 0) playlist.thumbnail = "https://pipedproxy.kavin.rocks/?host=i.ytimg.com";
this.createOrUpdateLocalPlaylist(playlist);
return { message: "ok" };
}
return await this.fetchJson(this.authApiUrl() + "/user/playlists/remove", null, {
method: "POST",
body: JSON.stringify({
playlistId: playlistId,
index: index,
}),
headers: {
Authorization: this.getAuthToken(),
"Content-Type": "application/json",
},
});
},
getHomePage(_this) {
switch (_this.getPreferenceString("homepage", "trending")) {
case "trending":
return "/trending";
case "feed":
return "/feed";
default:
return undefined;
}
},
fetchDeArrowContent(content) {
if (!this.getPreferenceBoolean("dearrow", false)) return;
const videoIds = content
.filter(item => item.type === "stream")
.filter(item => item.dearrow === undefined)
.map(item => item.url.substr(-11))
.sort();
if (videoIds.length === 0) return;
this.fetchJson(this.apiUrl() + "/dearrow", {
videoIds: videoIds.join(","),
}).then(json => {
Object.keys(json).forEach(videoId => {
const item = content.find(item => item.url.endsWith(videoId));
if (item) item.dearrow = json[videoId];
});
});
},
},
computed: {
authenticated(_this) {

14
src/utils/HtmlUtils.js Normal file
View file

@ -0,0 +1,14 @@
import DOMPurify from "dompurify";
export const purifyHTML = html => {
return DOMPurify.sanitize(html);
};
import linkifyHtml from "linkify-html";
export const rewriteDescription = text => {
return linkifyHtml(text)
.replaceAll(/(?:http(?:s)?:\/\/)?(?:www\.)?youtube\.com(\/[/a-zA-Z0-9_?=&-]*)/gm, "$1")
.replaceAll(/(?:http(?:s)?:\/\/)?(?:www\.)?youtu\.be\/(?:watch\?v=)?([/a-zA-Z0-9_?=&-]*)/gm, "/watch?v=$1")
.replaceAll("\n", "<br>");
};

12
sweep.yaml Normal file
View file

@ -0,0 +1,12 @@
# Sweep AI turns bug fixes & feature requests into code changes (https://sweep.dev)
# For details on our config file, check out our docs at https://docs.sweep.dev
# If you use this be sure to frequently sync your default branch(main, master) to dev.
branch: 'master'
# By default Sweep will read the logs and outputs from your existing Github Actions. To disable this, set this to false.
gha_enabled: True
# This is the description of your project. It will be used by sweep when creating PRs. You can tell Sweep what's unique about your project, what frameworks you use, or anything else you want.
# Here's an example: sweepai/sweep is a python project. The main api endpoints are in sweepai/api.py. Write code that adheres to PEP8.
description: 'TeamPiped/Piped is a Vue 3 project that uses UnoCSS (similar to Tailwind).'
# Default Values: https://github.com/sweepai/sweep/blob/main/sweep.yaml

View file

@ -73,5 +73,6 @@ export default defineConfig({
},
build: {
sourcemap: true,
cssMinify: "lightningcss",
},
});