Merge branch 'development' into piped-support

This commit is contained in:
ChunkyProgrammer 2024-03-07 20:17:43 -05:00
commit 6fc7cb0b44
64 changed files with 2370 additions and 1023 deletions

View File

@ -1 +1,17 @@
blank_issues_enabled: false
contact_links:
- name: Discussions
url: https://github.com/FreeTubeApp/FreeTube/discussions/categories/general
about: View discussions or start one yourself
- name: Questions
url: https://github.com/FreeTubeApp/FreeTube/discussions/categories/q-a
about: Ask and answer questions
- name: Matrix Community
url: https://matrix.to/#/+freetube:matrix.org
about: Join our Matrix chatroom - "Note: Bugs and Feature requests should be made on GitHub and not in the Matrix room"
- name: Translate FreeTube
url: https://hosted.weblate.org/engage/free-tube/
about: Help translate FreeTube on Weblate
- name: FreeTube Documentation
url: https://docs.freetubeapp.io/
about: View the Documentation to find all relevant information about FreeTube

View File

@ -1,6 +1,5 @@
process.env.NODE_ENV = 'development'
const open = require('open')
const electron = require('electron')
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
@ -161,7 +160,8 @@ function startWeb (callback) {
if (!web) {
startRenderer(startMain)
} else {
startWeb(({ port }) => {
startWeb(async ({ port }) => {
const open = (await import('open')).default
open(`http://localhost:${port}`)
})
}

View File

@ -11,6 +11,8 @@ const CopyWebpackPlugin = require('copy-webpack-plugin')
const isDevMode = process.env.NODE_ENV === 'development'
const { version: swiperVersion } = JSON.parse(readFileSync(path.join(__dirname, '../node_modules/swiper/package.json')))
const processLocalesPlugin = new ProcessLocalesPlugin({
compress: !isDevMode,
inputDir: path.join(__dirname, '../static/locales'),
@ -118,7 +120,8 @@ const config = {
'process.env.IS_ELECTRON': true,
'process.env.IS_ELECTRON_MAIN': false,
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames),
'process.env.GEOLOCATION_NAMES': JSON.stringify(readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', '')))
'process.env.GEOLOCATION_NAMES': JSON.stringify(readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))),
'process.env.SWIPER_VERSION': `'${swiperVersion}'`
}),
new HtmlWebpackPlugin({
excludeChunks: ['processTaskWorker'],
@ -137,7 +140,7 @@ const config = {
patterns: [
{
from: path.join(__dirname, '../node_modules/swiper/modules/{a11y,navigation,pagination}-element.css').replaceAll('\\', '/'),
to: 'swiper.css',
to: `swiper-${swiperVersion}.css`,
context: path.join(__dirname, '../node_modules/swiper/modules'),
transformAll: (assets) => {
return Buffer.concat(assets.map(asset => asset.data))

View File

@ -11,6 +11,8 @@ const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
const isDevMode = process.env.NODE_ENV === 'development'
const { version: swiperVersion } = JSON.parse(fs.readFileSync(path.join(__dirname, '../node_modules/swiper/package.json')))
const config = {
name: 'web',
mode: process.env.NODE_ENV,
@ -114,6 +116,7 @@ const config = {
new webpack.DefinePlugin({
'process.env.IS_ELECTRON': false,
'process.env.IS_ELECTRON_MAIN': false,
'process.env.SWIPER_VERSION': `'${swiperVersion}'`,
// video.js' vhs-utils supports both atob() in web browsers and Buffer in node
// As the FreeTube web build only runs in web browsers, we can override their check for atob() here: https://github.com/videojs/vhs-utils/blob/main/src/decode-b64-to-uint8-array.js#L3
@ -145,7 +148,7 @@ const config = {
patterns: [
{
from: path.join(__dirname, '../node_modules/swiper/modules/{a11y,navigation,pagination}-element.css').replaceAll('\\', '/'),
to: 'swiper.css',
to: `swiper-${swiperVersion}.css`,
context: path.join(__dirname, '../node_modules/swiper/modules'),
transformAll: (assets) => {
return Buffer.concat(assets.map(asset => asset.data))

View File

@ -65,7 +65,7 @@
"marked": "^12.0.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"swiper": "^11.0.6",
"swiper": "^11.0.7",
"video.js": "7.21.5",
"videojs-contrib-quality-levels": "^3.0.0",
"videojs-http-source-selector": "^1.1.6",
@ -77,21 +77,21 @@
"vue-observe-visibility": "^1.0.0",
"vue-router": "^3.6.5",
"vuex": "^3.6.2",
"youtubei.js": "^9.0.2"
"youtubei.js": "^9.1.0"
},
"devDependencies": {
"@babel/core": "^7.23.9",
"@babel/core": "^7.24.0",
"@babel/eslint-parser": "^7.23.10",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/preset-env": "^7.23.9",
"@double-great/stylelint-a11y": "^3.0.1",
"@babel/preset-env": "^7.24.0",
"@double-great/stylelint-a11y": "^3.0.2",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^12.0.2",
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
"electron": "^28.2.2",
"electron-builder": "^24.9.1",
"eslint": "^8.56.0",
"electron": "^29.1.0",
"electron-builder": "^24.13.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.29.1",
@ -100,21 +100,21 @@
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-unicorn": "^51.0.1",
"eslint-plugin-vue": "^9.21.1",
"eslint-plugin-vue": "^9.22.0",
"eslint-plugin-vuejs-accessibility": "^2.2.1",
"eslint-plugin-yml": "^1.12.2",
"html-webpack-plugin": "^5.6.0",
"js-yaml": "^4.1.0",
"json-minimizer-webpack-plugin": "^5.0.0",
"lefthook": "^1.6.1",
"mini-css-extract-plugin": "^2.8.0",
"lefthook": "^1.6.4",
"mini-css-extract-plugin": "^2.8.1",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.35",
"postcss-scss": "^4.0.9",
"prettier": "^2.8.8",
"rimraf": "^5.0.5",
"sass": "^1.70.0",
"sass-loader": "^14.1.0",
"sass": "^1.71.1",
"sass-loader": "^14.1.1",
"stylelint": "^16.2.1",
"stylelint-config-sass-guidelines": "^11.0.0",
"stylelint-config-standard": "^36.0.0",
@ -124,9 +124,9 @@
"vue-devtools": "^5.1.4",
"vue-eslint-parser": "^9.4.2",
"vue-loader": "^15.10.0",
"webpack": "^5.90.1",
"webpack": "^5.90.3",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"webpack-dev-server": "^5.0.2",
"webpack-watch-external-files-plugin": "^3.0.0",
"yaml-eslint-parser": "^1.2.2"
}

View File

@ -64,7 +64,9 @@ function runApp() {
const path = urlParts[1]
if (path) {
visible = ['/playlist', '/channel', '/watch'].some(p => path.startsWith(p))
visible = ['/channel', '/watch'].some(p => path.startsWith(p)) ||
// Only show copy link entry for non user playlists
(path.startsWith('/playlist') && !/playlistType=user/.test(path))
}
} else {
visible = true
@ -103,17 +105,17 @@ function runApp() {
let url
if (toYouTube) {
url = `https://youtu.be/${id}`
url = new URL(`https://youtu.be/${id}`)
} else {
url = `https://redirect.invidious.io/watch?v=${id}`
url = new URL(`https://redirect.invidious.io/watch?v=${id}`)
}
if (query) {
const params = new URLSearchParams(query)
const newParams = new URLSearchParams()
const newParams = new URLSearchParams(url.search)
let hasParams = false
if (params.has('playlistId')) {
if (params.has('playlistId') && params.get('playlistType') !== 'user') {
newParams.set('list', params.get('playlistId'))
hasParams = true
}
@ -124,11 +126,11 @@ function runApp() {
}
if (hasParams) {
url += '?' + newParams.toString()
url.search = newParams.toString()
}
}
return url
return url.toString()
}
}
}

View File

@ -886,7 +886,23 @@ export default defineComponent({
showToast(`${message}: ${err}`)
return
}
const playlists = JSON.parse(data)
let playlists = null
// for the sake of backwards compatibility,
// check if this is the old JSON array export (used until version 0.19.1),
// that didn't match the actual database format
const trimmedData = data.trim()
if (trimmedData[0] === '[' && trimmedData[trimmedData.length - 1] === ']') {
playlists = JSON.parse(trimmedData)
} else {
// otherwise assume this is the correct database format,
// which is also what we export now (used in 0.20.0 and later versions)
data = data.split('\n')
data.pop()
playlists = data.map(playlistJson => JSON.parse(playlistJson))
}
const requiredKeys = [
'playlistName',
@ -1019,7 +1035,11 @@ export default defineComponent({
]
}
await this.promptAndWriteToFile(options, JSON.stringify(this.allPlaylists), 'All playlists has been successfully exported')
const playlistsDb = this.allPlaylists.map(playlist => {
return JSON.stringify(playlist)
}).join('\n') + '\n'// a trailing line is expected
await this.promptAndWriteToFile(options, playlistsDb, 'All playlists has been successfully exported')
},
exportPlaylistsForOlderVersionsSometimes: function () {

View File

@ -40,7 +40,6 @@ export default defineComponent({
voteCount: '',
postContent: '',
commentCount: '',
isLoading: true,
author: '',
authorId: '',
}
@ -73,7 +72,7 @@ export default defineComponent({
injectStylesUrls: [
// This file is created with the copy webpack plugin in the web and renderer webpack configs.
// If you add more modules, please remember to add their CSS files to the list in webpack config files.
createWebURL('/swiper.css')
createWebURL(`/swiper-${process.env.SWIPER_VERSION}.css`)
],
a11y: true,
@ -132,7 +131,6 @@ export default defineComponent({
this.type = (this.data.postContent !== null && this.data.postContent !== undefined) ? this.data.postContent.type : 'text'
this.author = this.data.author
this.authorId = this.data.authorId
this.isLoading = false
},
getBestQualityImage(imageArray) {

View File

@ -1,6 +1,5 @@
<template>
<div
v-if="!isLoading"
class="ft-list-post ft-list-item outside"
:appearance="appearance"
:class="{ list: listType === 'list', grid: listType === 'grid' }"

View File

@ -128,6 +128,11 @@ export default defineComponent({
handleResize: function () {
this.useModal = window.innerWidth <= 900
}
},
focus() {
// To be called by parent components
this.$refs.iconButton.focus()
},
}
})

View File

@ -1,6 +1,7 @@
<template>
<div class="ftIconButton">
<font-awesome-icon
ref="iconButton"
class="iconButton"
:title="title"
:icon="icon"

View File

@ -3,3 +3,8 @@
.thumbnailLink:hover {
outline: 3px solid var(--side-nav-hover-color);
}
.thumbnailImage {
// Makes img element sized correctly before image loading starts
aspect-ratio: 16/9 auto;
}

View File

@ -38,7 +38,7 @@
>
<ft-playlist-selector
tabindex="0"
:data="playlist"
:playlist="playlist"
:index="index"
:selected="selectedPlaylistIdList.includes(playlist._id)"
@selected="countSelected(playlist._id)"

View File

@ -8,7 +8,7 @@ export default defineComponent({
'ft-icon-button': FtIconButton
},
props: {
data: {
playlist: {
type: Object,
required: true,
},
@ -30,6 +30,8 @@ export default defineComponent({
title: '',
thumbnail: require('../../assets/img/thumbnail_placeholder.svg'),
videoCount: 0,
videoPresenceCountInPlaylistTextShouldBeVisible: false,
}
},
computed: {
@ -39,6 +41,9 @@ export default defineComponent({
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
},
toBeAddedToPlaylistVideoList: function () {
return this.$store.getters.getToBeAddedToPlaylistVideoList
},
titleForDisplay: function () {
if (typeof this.title !== 'string') { return '' }
@ -46,28 +51,63 @@ export default defineComponent({
return `${this.title.substring(0, 255)}...`
},
loneToBeAddedToPlaylistVideo: function () {
if (this.toBeAddedToPlaylistVideoList.length !== 1) { return null }
return this.toBeAddedToPlaylistVideoList[0]
},
loneVideoPresenceCountInPlaylist() {
if (this.loneToBeAddedToPlaylistVideo == null) { return null }
const v = this.playlist.videos.reduce((accumulator, video) => {
return video.videoId === this.loneToBeAddedToPlaylistVideo.videoId
? accumulator + 1
: accumulator
}, 0)
// Don't display zero value
return v === 0 ? null : v
},
loneVideoPresenceCountInPlaylistText() {
if (this.loneVideoPresenceCountInPlaylist == null) { return null }
return this.$tc('User Playlists.AddVideoPrompt.Added {count} Times', this.loneVideoPresenceCountInPlaylist, {
count: this.loneVideoPresenceCountInPlaylist,
})
},
videoPresenceCountInPlaylistTextVisible() {
if (!this.videoPresenceCountInPlaylistTextShouldBeVisible) { return false }
return this.loneVideoPresenceCountInPlaylistText != null
},
},
created: function () {
this.parseUserData()
},
methods: {
parseUserData: function () {
this.title = this.data.playlistName
if (this.data.videos.length > 0) {
const thumbnailURL = `https://i.ytimg.com/vi/${this.data.videos[0].videoId}/mqdefault.jpg`
this.title = this.playlist.playlistName
if (this.playlist.videos.length > 0) {
const thumbnailURL = `https://i.ytimg.com/vi/${this.playlist.videos[0].videoId}/mqdefault.jpg`
if (this.backendPreference === 'invidious') {
this.thumbnail = thumbnailURL.replace('https://i.ytimg.com', this.currentInvidiousInstance)
} else {
this.thumbnail = thumbnailURL
}
}
this.videoCount = this.data.videos.length
this.videoCount = this.playlist.videos.length
},
toggleSelection: function () {
this.$emit('selected', this.index)
},
onVisibilityChanged(visible) {
if (!visible) { return }
this.videoPresenceCountInPlaylistTextShouldBeVisible = true
},
...mapActions([
'openInExternalPlayer'
])

View File

@ -54,6 +54,10 @@
word-wrap: break-word;
word-break: break-word;
}
.videoPresenceCount {
margin-block-start: 4px;
}
}
&.grid {

View File

@ -29,12 +29,24 @@
</div>
</div>
</div>
<div class="info">
<span
<div
v-observe-visibility="{
callback: onVisibilityChanged,
once: true,
}"
class="info"
>
<div
class="title"
>
{{ titleForDisplay }}
</span>
</div>
<div
v-if="videoPresenceCountInPlaylistTextVisible"
class="videoPresenceCount"
>
{{ loneVideoPresenceCountInPlaylistText }}
</div>
</div>
</div>
</template>

View File

@ -9,18 +9,29 @@ export default defineComponent({
}
},
methods: {
catchTimestampClick: function(event) {
const match = event.detail.match(/(\d+):(\d+):?(\d+)?/)
if (match[3] !== undefined) { // HH:MM:SS
const seconds = 3600 * Number(match[1]) + 60 * Number(match[2]) + Number(match[3])
this.$emit('timestamp-event', seconds)
} else { // MM:SS
const seconds = 60 * Number(match[1]) + Number(match[2])
this.$emit('timestamp-event', seconds)
}
catchTimestampClick: function (event) {
this.$emit('timestamp-event', event.detail)
},
detectTimestamps: function (input) {
return input.replaceAll(/(\d+(:\d+)+)/g, '<a href="#" onclick="this.dispatchEvent(new CustomEvent(\'timestamp-clicked\',{bubbles:true, detail:\'$1\'}))">$1</a>')
const videoId = this.$route.params.id
return input.replaceAll(/(?:(\d+):)?(\d+):(\d+)/g, (timestamp, hours, minutes, seconds) => {
let time = 60 * Number(minutes) + Number(seconds)
if (hours) {
time += 3600 * Number(hours)
}
const url = this.$router.resolve({
path: `/watch/${videoId}`,
query: {
timestamp: time
}
}).href
// Adding the URL lets the user open the video in a new window at this timestamp
return `<a href="${url}" onclick="event.preventDefault();this.dispatchEvent(new CustomEvent('timestamp-clicked',{bubbles:true,detail:${time}}));window.scrollTo(0,0)">${timestamp}</a>`
})
}
}
})

View File

@ -1,6 +1,8 @@
import { defineComponent } from 'vue'
import FtToastEvents from './ft-toast-events.js'
let id = 0
export default defineComponent({
name: 'FtToast',
data: function () {
@ -15,7 +17,9 @@ export default defineComponent({
FtToastEvents.removeEventListener('toast-open', this.open)
},
methods: {
performAction: function (index) {
performAction: function (id) {
const index = this.toasts.findIndex(toast => id === toast.id)
this.toasts[index].action()
this.remove(index)
},
@ -26,7 +30,13 @@ export default defineComponent({
toast.isOpen = false
},
open: function ({ detail: { message, time, action } }) {
const toast = { message: message, action: action || (() => { }), isOpen: false, timeout: null }
const toast = {
message: message,
action: action || (() => { }),
isOpen: false,
timeout: null,
id: id++
}
toast.timeout = setTimeout(this.close, time || 3000, toast)
setTimeout(() => { toast.isOpen = true })
if (this.toasts.length > 4) {

View File

@ -1,15 +1,15 @@
<template>
<div class="toast-holder">
<div
v-for="(toast, index) in toasts"
:key="'toast-' + index"
v-for="toast in toasts"
:key="toast.id"
class="toast"
:class="{ closed: !toast.isOpen, open: toast.isOpen }"
tabindex="0"
role="status"
@click="performAction(index)"
@keydown.enter.prevent="performAction(index)"
@keydown.space.prevent="performAction(index)"
@click="performAction(toast.id)"
@keydown.enter.prevent="performAction(toast.id)"
@keydown.space.prevent="performAction(toast.id)"
>
<p class="message">
{{ toast.message }}

View File

@ -6,10 +6,12 @@ import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
import FtInput from '../ft-input/ft-input.vue'
import FtPrompt from '../ft-prompt/ft-prompt.vue'
import FtButton from '../ft-button/ft-button.vue'
import {
formatNumber,
showToast,
} from '../../helpers/utils'
import debounce from 'lodash.debounce'
export default defineComponent({
name: 'PlaylistInfo',
@ -19,6 +21,7 @@ export default defineComponent({
'ft-icon-button': FtIconButton,
'ft-input': FtInput,
'ft-prompt': FtPrompt,
'ft-button': FtButton,
},
props: {
id: {
@ -84,6 +87,9 @@ export default defineComponent({
},
data: function () {
return {
searchVideoMode: false,
query: '',
updateQueryDebounce: function() {},
editMode: false,
showDeletePlaylistPrompt: false,
showRemoveVideosOnWatchPrompt: false,
@ -257,6 +263,8 @@ export default defineComponent({
created: function () {
this.newTitle = this.title
this.newDescription = this.description
this.updateQueryDebounce = debounce(this.updateQuery, 500)
},
methods: {
toggleCopyVideosPrompt: function (force = false) {
@ -398,6 +406,30 @@ export default defineComponent({
showToast(this.$t('User Playlists.SinglePlaylistView.Toast.Quick bookmark disabled'))
},
updateQuery(query) {
this.query = query
this.$emit('search-video-query-change', query)
},
enableVideoSearchMode() {
this.searchVideoMode = true
this.$emit('search-video-mode-on')
nextTick(() => {
// Some elements only present after rendering update
this.$refs.searchInput.focus()
})
},
disableVideoSearchMode() {
this.searchVideoMode = false
this.updateQuery('')
this.$emit('search-video-mode-off')
nextTick(() => {
// Some elements only present after rendering update
this.$refs.enableSearchModeButton?.focus()
})
},
...mapActions([
'showAddToPlaylistPromptForManyVideos',
'updatePlaylist',

View File

@ -73,6 +73,16 @@
justify-content: flex-end;
}
.searchInputsRow {
margin-block-start: 8px;
display: grid;
/* 2 columns */
grid-template-columns: 1fr auto;
column-gap: 8px;
}
@media only screen and (max-width: 1250px) {
:deep(.sharePlaylistIcon .iconDropdown) {
inset-inline-start: auto;

View File

@ -85,6 +85,7 @@
<hr>
<div
v-if="!searchVideoMode"
class="channelShareWrapper"
>
<router-link
@ -115,6 +116,14 @@
</div>
<div class="playlistOptions">
<ft-icon-button
v-if="isUserPlaylist && videoCount > 0 && !editMode"
ref="enableSearchModeButton"
:title="$t('User Playlists.SinglePlaylistView.Search for Videos')"
:icon="['fas', 'search']"
theme="secondary"
@click="enableVideoSearchMode"
/>
<ft-icon-button
v-if="editMode"
:title="$t('User Playlists.Save Changes')"
@ -196,6 +205,28 @@
@click="handleRemoveVideosOnWatchPromptAnswer"
/>
</div>
<div
v-if="isUserPlaylist && searchVideoMode"
class="searchInputsRow"
>
<ft-input
ref="searchInput"
class="searchInput"
:placeholder="$t('User Playlists.SinglePlaylistView.Search for Videos')"
:show-clear-text-button="true"
:show-action-button="false"
@input="(input) => updateQueryDebounce(input)"
@clear="updateQueryDebounce('')"
/>
<ft-icon-button
v-if="isUserPlaylist && searchVideoMode"
:title="$t('User Playlists.Cancel')"
:icon="['fas', 'times']"
theme="secondary"
@click="disableVideoSearchMode"
/>
</div>
</div>
</template>

View File

@ -121,6 +121,8 @@ export default defineComponent({
this.attemptedFetch = true
this.errorChannels = []
const subscriptionUpdates = []
const postListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
let posts = []
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
@ -137,6 +139,32 @@ export default defineComponent({
channelId: channel.id,
posts: posts,
})
if (posts.length > 0) {
const post = posts.find(post => post.authorId === channel.id)
if (post) {
const name = post.author
let thumbnailUrl = post.authorThumbnails?.[0]?.url
if (name || thumbnailUrl) {
if (thumbnailUrl) {
if (thumbnailUrl.startsWith('//')) {
thumbnailUrl = 'https:' + thumbnailUrl
} else if (thumbnailUrl.startsWith(`${this.currentInvidiousInstance}/ggpht`)) {
thumbnailUrl = thumbnailUrl.replace(`${this.currentInvidiousInstance}/ggpht`, 'https://yt3.googleusercontent.com')
}
}
subscriptionUpdates.push({
channelId: channel.id,
channelName: name,
channelThumbnailUrl: thumbnailUrl
})
}
}
}
return posts
}))).flatMap((o) => o)
postList.push(...postListFromRemote)
@ -147,6 +175,8 @@ export default defineComponent({
this.postList = postList
this.isLoading = false
this.updateShowProgressBar(false)
this.batchUpdateSubscriptionDetails(subscriptionUpdates)
},
maybeLoadPostsForSubscriptionsFromRemote: async function () {
@ -211,6 +241,7 @@ export default defineComponent({
...mapActions([
'updateShowProgressBar',
'batchUpdateSubscriptionDetails',
'updateSubscriptionPostsCacheByChannel',
]),

View File

@ -133,19 +133,23 @@ export default defineComponent({
this.attemptedFetch = true
this.errorChannels = []
const subscriptionUpdates = []
const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
let videos = []
let name, thumbnailUrl
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
if (useRss) {
videos = await this.getChannelLiveInvidiousRSS(channel)
({ videos, name, thumbnailUrl } = await this.getChannelLiveInvidiousRSS(channel))
} else {
videos = await this.getChannelLiveInvidious(channel)
({ videos, name, thumbnailUrl } = await this.getChannelLiveInvidious(channel))
}
} else {
if (useRss) {
videos = await this.getChannelLiveLocalRSS(channel)
({ videos, name, thumbnailUrl } = await this.getChannelLiveLocalRSS(channel))
} else {
videos = await this.getChannelLiveLocal(channel)
({ videos, name, thumbnailUrl } = await this.getChannelLiveLocal(channel))
}
}
@ -156,6 +160,15 @@ export default defineComponent({
channelId: channel.id,
videos: videos,
})
if (name || thumbnailUrl) {
subscriptionUpdates.push({
channelId: channel.id,
channelName: name,
channelThumbnailUrl: thumbnailUrl
})
}
return videos
}))).flatMap((o) => o)
videoList.push(...videoListFromRemote)
@ -163,6 +176,8 @@ export default defineComponent({
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
this.updateShowProgressBar(false)
this.batchUpdateSubscriptionDetails(subscriptionUpdates)
},
maybeLoadVideosForSubscriptionsFromRemote: async function () {
@ -178,16 +193,18 @@ export default defineComponent({
getChannelLiveLocal: async function (channel, failedAttempts = 0) {
try {
const entries = await getLocalChannelLiveStreams(channel.id)
const result = await getLocalChannelLiveStreams(channel.id)
if (entries === null) {
if (result === null) {
this.errorChannels.push(channel)
return []
return {
videos: []
}
}
addPublishedDatesLocal(entries)
addPublishedDatesLocal(result.videos)
return entries
return result
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
@ -202,12 +219,16 @@ export default defineComponent({
showToast(this.$t('Falling back to Invidious API'))
return await this.getChannelLiveInvidious(channel, failedAttempts + 1)
} else {
return []
return {
videos: []
}
}
case 2:
return await this.getChannelLiveLocalRSS(channel, failedAttempts + 1)
default:
return []
return {
videos: []
}
}
}
},
@ -231,7 +252,9 @@ export default defineComponent({
this.errorChannels.push(channel)
}
return []
return {
videos: []
}
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
@ -249,12 +272,16 @@ export default defineComponent({
showToast(this.$t('Falling back to Invidious API'))
return this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1)
} else {
return []
return {
videos: []
}
}
case 2:
return this.getChannelLiveLocal(channel, failedAttempts + 1)
default:
return []
return {
videos: []
}
}
}
},
@ -273,7 +300,16 @@ export default defineComponent({
addPublishedDatesInvidious(videos)
resolve(videos)
let name
if (videos.length > 0) {
name = videos.find(video => video.author).author
}
resolve({
name,
videos
})
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
@ -289,14 +325,18 @@ export default defineComponent({
showToast(this.$t('Falling back to Local API'))
resolve(this.getChannelLiveLocal(channel, failedAttempts + 1))
} else {
resolve([])
resolve({
videos: []
})
}
break
case 2:
resolve(this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1))
break
default:
resolve([])
resolve({
videos: []
})
}
})
})
@ -310,7 +350,9 @@ export default defineComponent({
const response = await fetch(feedUrl)
if (response.status === 500 || response.status === 404) {
return []
return {
videos: []
}
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
@ -328,17 +370,22 @@ export default defineComponent({
showToast(this.$t('Falling back to Local API'))
return this.getChannelLiveLocalRSS(channel, failedAttempts + 1)
} else {
return []
return {
videos: []
}
}
case 2:
return this.getChannelLiveInvidious(channel, failedAttempts + 1)
default:
return []
return {
videos: []
}
}
}
},
...mapActions([
'batchUpdateSubscriptionDetails',
'updateShowProgressBar',
'updateSubscriptionLiveCacheByChannel',
]),

View File

@ -118,12 +118,16 @@ export default defineComponent({
this.attemptedFetch = true
this.errorChannels = []
const subscriptionUpdates = []
const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
let videos = []
let name
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
videos = await this.getChannelShortsInvidious(channel)
({ videos, name } = await this.getChannelShortsInvidious(channel))
} else {
videos = await this.getChannelShortsLocal(channel)
({ videos, name } = await this.getChannelShortsLocal(channel))
}
channelCount++
@ -133,6 +137,14 @@ export default defineComponent({
channelId: channel.id,
videos: videos,
})
if (name) {
subscriptionUpdates.push({
channelId: channel.id,
channelName: name
})
}
return videos
}))).flatMap((o) => o)
videoList.push(...videoListFromRemote)
@ -140,6 +152,8 @@ export default defineComponent({
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
this.updateShowProgressBar(false)
this.batchUpdateSubscriptionDetails(subscriptionUpdates)
},
maybeLoadVideosForSubscriptionsFromRemote: async function () {
@ -172,7 +186,9 @@ export default defineComponent({
this.errorChannels.push(channel)
}
return []
return {
videos: []
}
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
@ -188,10 +204,14 @@ export default defineComponent({
showToast(this.$t('Falling back to Invidious API'))
return this.getChannelShortsInvidious(channel, failedAttempts + 1)
} else {
return []
return {
videos: []
}
}
default:
return []
return {
videos: []
}
}
}
},
@ -204,7 +224,9 @@ export default defineComponent({
const response = await fetch(feedUrl)
if (response.status === 500 || response.status === 404) {
return []
return {
videos: []
}
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
@ -220,15 +242,20 @@ export default defineComponent({
showToast(this.$t('Falling back to Local API'))
return this.getChannelShortsLocal(channel, failedAttempts + 1)
} else {
return []
return {
videos: []
}
}
default:
return []
return {
videos: []
}
}
}
},
...mapActions([
'batchUpdateSubscriptionDetails',
'updateShowProgressBar',
'updateSubscriptionShortsCacheByChannel',
]),

View File

@ -133,19 +133,23 @@ export default defineComponent({
this.attemptedFetch = true
this.errorChannels = []
const subscriptionUpdates = []
const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
let videos = []
let name, thumbnailUrl
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
if (useRss) {
videos = await this.getChannelVideosInvidiousRSS(channel)
({ videos, name, thumbnailUrl } = await this.getChannelVideosInvidiousRSS(channel))
} else {
videos = await this.getChannelVideosInvidiousScraper(channel)
({ videos, name, thumbnailUrl } = await this.getChannelVideosInvidiousScraper(channel))
}
} else {
if (useRss) {
videos = await this.getChannelVideosLocalRSS(channel)
({ videos, name, thumbnailUrl } = await this.getChannelVideosLocalRSS(channel))
} else {
videos = await this.getChannelVideosLocalScraper(channel)
({ videos, name, thumbnailUrl } = await this.getChannelVideosLocalScraper(channel))
}
}
@ -156,6 +160,15 @@ export default defineComponent({
channelId: channel.id,
videos: videos,
})
if (name || thumbnailUrl) {
subscriptionUpdates.push({
channelId: channel.id,
channelName: name,
channelThumbnailUrl: thumbnailUrl
})
}
return videos
}))).flatMap((o) => o)
videoList.push(...videoListFromRemote)
@ -163,6 +176,8 @@ export default defineComponent({
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
this.updateShowProgressBar(false)
this.batchUpdateSubscriptionDetails(subscriptionUpdates)
},
maybeLoadVideosForSubscriptionsFromRemote: async function () {
@ -178,16 +193,18 @@ export default defineComponent({
getChannelVideosLocalScraper: async function (channel, failedAttempts = 0) {
try {
const videos = await getLocalChannelVideos(channel.id)
const result = await getLocalChannelVideos(channel.id)
if (videos === null) {
if (result === null) {
this.errorChannels.push(channel)
return []
return {
videos: []
}
}
addPublishedDatesLocal(videos)
addPublishedDatesLocal(result.videos)
return videos
return result
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
@ -202,12 +219,16 @@ export default defineComponent({
showToast(this.$t('Falling back to Invidious API'))
return await this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
} else {
return []
return {
videos: []
}
}
case 2:
return await this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
default:
return []
return {
videos: []
}
}
}
},
@ -231,7 +252,9 @@ export default defineComponent({
this.errorChannels.push(channel)
}
return []
return {
videos: []
}
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
@ -249,12 +272,16 @@ export default defineComponent({
showToast(this.$t('Falling back to Invidious API'))
return this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1)
} else {
return []
return {
videos: []
}
}
case 2:
return this.getChannelVideosLocalScraper(channel, failedAttempts + 1)
default:
return []
return {
videos: []
}
}
}
},
@ -270,7 +297,16 @@ export default defineComponent({
invidiousAPICall(subscriptionsPayload).then((result) => {
addPublishedDatesInvidious(result.videos)
resolve(result.videos)
let name
if (result.videos.length > 0) {
name = result.videos.find(video => video.type === 'video' && video.author).author
}
resolve({
name,
videos: result.videos
})
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
@ -286,14 +322,18 @@ export default defineComponent({
showToast(this.$t('Falling back to Local API'))
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
} else {
resolve([])
resolve({
videos: []
})
}
break
case 2:
resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1))
break
default:
resolve([])
resolve({
videos: []
})
}
})
})
@ -308,7 +348,9 @@ export default defineComponent({
if (response.status === 500 || response.status === 404) {
this.errorChannels.push(channel)
return []
return {
videos: []
}
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
@ -326,17 +368,22 @@ export default defineComponent({
showToast(this.$t('Falling back to Local API'))
return this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
} else {
return []
return {
videos: []
}
}
case 2:
return this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
default:
return []
return {
videos: []
}
}
}
},
...mapActions([
'batchUpdateSubscriptionDetails',
'updateShowProgressBar',
'updateSubscriptionVideosCacheByChannel',
]),

View File

@ -54,7 +54,6 @@
.chapterThumbnail {
grid-area: thumbnail;
inline-size: 130px;
block-size: auto;
margin: 3px;
}

View File

@ -49,6 +49,7 @@ export default defineComponent({
changeChapter: function(index) {
this.currentIndex = index
this.$emit('timestamp-event', this.chapters[index].startSeconds)
window.scrollTo(0, 0)
},
navigateChapters(direction) {

View File

@ -46,13 +46,15 @@
@keydown.space.stop.prevent="changeChapter(index)"
@keydown.enter.stop.prevent="changeChapter(index)"
>
<!-- Setting the aspect ratio avoids layout shifts when the images load -->
<img
v-if="!compact"
alt=""
aria-hidden="true"
class="chapterThumbnail"
loading="lazy"
:src="chapter.thumbnail"
:src="chapter.thumbnail.url"
:style="{ aspectRatio: chapter.thumbnail.width / chapter.thumbnail.height }"
>
<div class="chapterTimestamp">
{{ chapter.timestamp }}

View File

@ -64,6 +64,19 @@
inset-inline-end: calc(50% - 20px);
}
}
@media screen and (max-width: 460px) {
flex-wrap: nowrap;
:deep(.iconDropdown) {
inset-inline-start: auto;
inset-inline-end: auto;
}
}
}
@media screen and (max-width: 460px) {
flex-direction: column;
align-items: flex-start;
margin-block-start: 10px;
}
}
@ -72,29 +85,43 @@
color: var(--tertiary-text-color);
.likeSection {
color: var(--tertiary-text-color);
display: flex;
flex-direction: column;
margin-inline-start: auto;
margin-block-start: 4px;
max-inline-size: 210px;
text-align: end;
@media screen and (max-width: 680px) {
margin-inline-start: 0;
text-align: start;
}
.likeBar {
border-radius: 4px;
block-size: 8px;
margin-block-end: 4px;
}
.likeCount {
margin-inline-end: 0;
color: var(--tertiary-text-color);
display: flex;
gap: 3px;
flex-direction: column;
margin-inline-start: auto;
margin-block-start: 4px;
max-inline-size: 210px;
text-align: end;
@media screen and (max-width: 680px) {
margin-inline-start: 0;
text-align: start;
}
.likeBar {
border-radius: 4px;
block-size: 8px;
margin-block-end: 4px;
}
.likeCount {
margin-inline-end: 0;
display: flex;
gap: 3px;
}
}
.datePublishedAndViewCount {
@media only screen and (max-width: 460px) {
display: flex;
justify-content: left;
flex-direction: column;
.seperator {
display: none;
}
}
}
.videoViews {
white-space: nowrap;
}
}
}

View File

@ -9,7 +9,12 @@
</div>
<div class="videoMetrics">
<div class="datePublishedAndViewCount">
{{ publishedString }} {{ dateString }} {{ parsedViewCount }}
{{ publishedString }} {{ dateString }}
<template
v-if="!hideVideoViews"
>
<span class="seperator"> </span><span class="videoViews">{{ parsedViewCount }}</span>
</template>
</div>
<div
v-if="!hideVideoLikesAndDislikes"

View File

@ -35,23 +35,23 @@ const TRACKING_PARAM_NAMES = [
* @param {boolean} options.generateSessionLocally generate the session locally or let YouTube generate it (local is faster, remote is more accurate)
* @returns the Innertube instance
*/
async function createInnertube(options = { withPlayer: false, location: undefined, safetyMode: false, clientType: undefined, generateSessionLocally: true }) {
async function createInnertube({ withPlayer = false, location = undefined, safetyMode = false, clientType = undefined, generateSessionLocally = true } = {}) {
let cache
if (options.withPlayer) {
if (withPlayer) {
const userData = await getUserDataPath()
cache = new PlayerCache(join(userData, 'player_cache'))
}
return await Innertube.create({
retrieve_player: !!options.withPlayer,
location: options.location,
enable_safety_mode: !!options.safetyMode,
client_type: options.clientType,
retrieve_player: !!withPlayer,
location: location,
enable_safety_mode: !!safetyMode,
client_type: clientType,
// use browser fetch
fetch: (input, init) => fetch(input, init),
cache,
generate_session_locally: !!options.generateSessionLocally
generate_session_locally: !!generateSessionLocally
})
}
@ -274,6 +274,9 @@ export async function getLocalChannel(id) {
return result
}
/**
* @param {string} id
*/
export async function getLocalChannelVideos(id) {
const innertube = await createInnertube()
@ -286,15 +289,22 @@ export async function getLocalChannelVideos(id) {
}))
const videosTab = new YT.Channel(null, response)
const { id: channelId = id, name, thumbnailUrl } = parseLocalChannelHeader(videosTab)
let videos
// if the channel doesn't have a videos tab, YouTube returns the home tab instead
// so we need to check that we got the right tab
if (videosTab.current_tab?.endpoint.metadata.url?.endsWith('/videos')) {
const { id: channelId = id, name } = parseLocalChannelHeader(videosTab)
return parseLocalChannelVideos(videosTab.videos, channelId, name)
videos = parseLocalChannelVideos(videosTab.videos, channelId, name)
} else {
return []
videos = []
}
return {
name,
thumbnailUrl,
videos
}
} catch (error) {
console.error(error)
@ -306,6 +316,9 @@ export async function getLocalChannelVideos(id) {
}
}
/**
* @param {string} id
*/
export async function getLocalChannelLiveStreams(id) {
const innertube = await createInnertube()
@ -318,15 +331,22 @@ export async function getLocalChannelLiveStreams(id) {
}))
const liveStreamsTab = new YT.Channel(null, response)
const { id: channelId = id, name, thumbnailUrl } = parseLocalChannelHeader(liveStreamsTab)
let videos
// if the channel doesn't have a live tab, YouTube returns the home tab instead
// so we need to check that we got the right tab
if (liveStreamsTab.current_tab?.endpoint.metadata.url?.endsWith('/streams')) {
const { id: channelId = id, name } = parseLocalChannelHeader(liveStreamsTab)
return parseLocalChannelVideos(liveStreamsTab.videos, channelId, name)
videos = parseLocalChannelVideos(liveStreamsTab.videos, channelId, name)
} else {
return []
videos = []
}
return {
name,
thumbnailUrl,
videos
}
} catch (error) {
console.error(error)
@ -539,7 +559,7 @@ export function parseLocalChannelShorts(shorts, channelId, channelName) {
title: short.title.text,
author: channelName,
authorId: channelId,
viewCount: parseLocalSubscriberCount(short.views.text),
viewCount: short.views.isEmpty() ? null : parseLocalSubscriberCount(short.views.text),
lengthSeconds: ''
}
})

View File

@ -80,9 +80,14 @@ export async function parseYouTubeRSSFeed(rssString, channelId) {
promises.push(parseRSSEntry(entry, channelId, channelName))
}
return await Promise.all(promises)
return {
name: channelName,
videos: await Promise.all(promises)
}
} catch (e) {
return []
return {
videos: []
}
}
}

View File

@ -91,6 +91,43 @@ const actions = {
commit('setProfileList', profiles)
},
async batchUpdateSubscriptionDetails({ getters, dispatch }, channels) {
if (channels.length === 0) { return }
const profileList = getters.getProfileList
for (const profile of profileList) {
const currentProfileCopy = deepCopy(profile)
let profileUpdated = false
for (const { channelThumbnailUrl, channelName, channelId } of channels) {
const channel = currentProfileCopy.subscriptions.find((channel) => {
return channel.id === channelId
}) ?? null
if (channel === null) { continue }
if (channel.name !== channelName && channelName != null) {
channel.name = channelName
profileUpdated = true
}
if (channelThumbnailUrl) {
const thumbnail = channelThumbnailUrl.replace(/=s\d*/, '=s176') // change thumbnail size if different
if (channel.thumbnail !== thumbnail) {
channel.thumbnail = thumbnail
profileUpdated = true
}
}
}
if (profileUpdated) {
await dispatch('updateProfile', currentProfileCopy)
}
}
},
async updateSubscriptionDetails({ getters, dispatch }, { channelThumbnailUrl, channelName, channelId }) {
const thumbnail = channelThumbnailUrl?.replace(/=s\d*/, '=s176') ?? null // change thumbnail size if different
const profileList = getters.getProfileList

View File

@ -54,6 +54,10 @@ const actions = {
commit('updateShortsCacheByChannel', payload)
},
updateSubscriptionShortsCacheWithChannelPageShorts: ({ commit }, payload) => {
commit('updateShortsCacheWithChannelPageShorts', payload)
},
updateSubscriptionLiveCacheByChannel: ({ commit }, payload) => {
commit('updateLiveCacheByChannel', payload)
},
@ -86,6 +90,31 @@ const mutations = {
if (videos != null) { newObject.videos = videos }
state.shortsCache[channelId] = newObject
},
updateShortsCacheWithChannelPageShorts(state, { channelId, videos }) {
const cachedObject = state.shortsCache[channelId]
if (cachedObject && cachedObject.videos.length > 0) {
cachedObject.videos.forEach(cachedVideo => {
const channelVideo = videos.find(short => cachedVideo.videoId === short.videoId)
if (channelVideo) {
// authorId probably never changes, so we don't need to update that
cachedVideo.title = channelVideo.title
cachedVideo.author = channelVideo.author
// as the channel shorts page only has compact view counts for numbers above 1000 e.g. 12k
// and the RSS feeds include an exact value, we only want to overwrite it when the number is larger than the cached value
// 12345 vs 12000 => 12345
// 12345 vs 15000 => 15000
if (channelVideo.viewCount > cachedVideo.viewCount) {
cachedVideo.viewCount = channelVideo.viewCount
}
}
})
}
},
clearShortsCache(state) {
state.shortsCache = {}
},

View File

@ -33,6 +33,10 @@ import {
parseLocalListVideo,
parseLocalSubscriberCount
} from '../../helpers/api/local'
import {
addPublishedDatesInvidious,
addPublishedDatesLocal
} from '../../helpers/subscriptions'
export default defineComponent({
name: 'Channel',
@ -175,6 +179,13 @@ export default defineComponent({
return this.subscriptionInfo !== null
},
isSubscribedInAnyProfile: function () {
const profileList = this.$store.getters.getProfileList
// check the all channels profile
return profileList[0].subscriptions.some((channel) => channel.id === this.id)
},
videoLiveSelectNames: function () {
return [
this.$t('Channel.Videos.Sort Types.Newest'),
@ -771,6 +782,17 @@ export default defineComponent({
this.latestVideos = parseLocalChannelVideos(videosTab.videos, this.id, this.channelName)
this.videoContinuationData = videosTab.has_continuation ? videosTab : null
this.isElementListLoading = false
if (this.isSubscribedInAnyProfile && this.latestVideos.length > 0 && this.videoSortBy === 'newest') {
addPublishedDatesLocal(this.latestVideos)
this.updateSubscriptionVideosCacheByChannel({
channelId: this.id,
// create a copy so that we only cache the first page
// if we use the same array, the store will get angry at us for modifying it outside of the store,
// when the user clicks load more
videos: [...this.latestVideos]
})
}
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
@ -829,6 +851,16 @@ export default defineComponent({
this.latestShorts = parseLocalChannelShorts(shortsTab.videos, this.id, this.channelName)
this.shortContinuationData = shortsTab.has_continuation ? shortsTab : null
this.isElementListLoading = false
if (this.isSubscribedInAnyProfile && this.latestShorts.length > 0 && this.shortSortBy === 'newest') {
// As the shorts tab API response doesn't include the published dates,
// we can't just write the results to the subscriptions cache like we do with videos and live (can't sort chronologically without the date).
// However we can still update the metadata in the cache such as the view count and title that might have changed since it was cached
this.updateSubscriptionShortsCacheWithChannelPageShorts({
channelId: this.id,
videos: this.latestShorts
})
}
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
@ -887,6 +919,17 @@ export default defineComponent({
this.latestLive = parseLocalChannelVideos(liveTab.videos, this.id, this.channelName)
this.liveContinuationData = liveTab.has_continuation ? liveTab : null
this.isElementListLoading = false
if (this.isSubscribedInAnyProfile && this.latestLive.length > 0 && this.liveSortBy === 'newest') {
addPublishedDatesLocal(this.latestLive)
this.updateSubscriptionLiveCacheByChannel({
channelId: this.id,
// create a copy so that we only cache the first page
// if we use the same array, the store will get angry at us for modifying it outside of the store,
// when the user clicks load more
videos: [...this.latestLive]
})
}
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
@ -1056,6 +1099,16 @@ export default defineComponent({
}
this.videoContinuationData = response.continuation || null
this.isElementListLoading = false
if (this.isSubscribedInAnyProfile && !more && this.latestVideos.length > 0 && this.videoSortBy === 'newest') {
addPublishedDatesInvidious(this.latestVideos)
this.updateSubscriptionVideosCacheByChannel({
channelId: this.id,
// create a copy so that we only cache the first page
// if we use the same array, it will also contain all the next pages
videos: [...this.latestVideos]
})
}
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
@ -1105,6 +1158,17 @@ export default defineComponent({
}
this.shortContinuationData = response.continuation || null
this.isElementListLoading = false
if (this.isSubscribedInAnyProfile && !more && this.latestShorts.length > 0 && this.shortSortBy === 'newest') {
// As the shorts tab API response doesn't include the published dates,
// we can't just write the results to the subscriptions cache like we do with videos and live (can't sort chronologically without the date).
// However we can still update the metadata in the cache e.g. adding the duration, as that isn't included in the RSS feeds
// and updating the view count and title that might have changed since it was cached
this.updateSubscriptionShortsCacheWithChannelPageShorts({
channelId: this.id,
videos: this.latestShorts
})
}
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
@ -1146,6 +1210,17 @@ export default defineComponent({
}
this.liveContinuationData = response.continuation || null
this.isElementListLoading = false
if (this.isSubscribedInAnyProfile && !more && this.latestLive.length > 0 && this.liveSortBy === 'newest') {
addPublishedDatesInvidious(this.latestLive)
this.updateSubscriptionLiveCacheByChannel({
channelId: this.id,
// create a copy so that we only cache the first page
// if we use the same array, the store will get angry at us for modifying it outside of the store,
// when the user clicks load more
videos: [...this.latestLive]
})
}
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
@ -1448,7 +1523,7 @@ export default defineComponent({
})
if (this.backendPreference === 'local' && this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
this.getChannelPodcastsInvidious()
this.channelInvidiousPodcasts()
} else {
this.isLoading = false
}
@ -1563,6 +1638,19 @@ export default defineComponent({
this.latestCommunityPosts = parseLocalCommunityPosts(posts)
this.communityContinuationData = communityTab.has_continuation ? communityTab : null
if (this.latestCommunityPosts.length > 0) {
this.latestCommunityPosts.forEach(post => {
post.authorId = this.id
})
this.updateSubscriptionPostsCacheByChannel({
channelId: this.id,
// create a copy so that we only cache the first page
// if we use the same array, the store will get angry at us for modifying it outside of the store,
// when the user clicks load more
posts: [...this.latestCommunityPosts]
})
}
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
@ -1614,6 +1702,19 @@ export default defineComponent({
this.latestCommunityPosts = posts
}
this.communityContinuationData = continuation
if (this.isSubscribedInAnyProfile && !more && this.latestCommunityPosts.length > 0) {
this.latestCommunityPosts.forEach(post => {
post.authorId = this.id
})
this.updateSubscriptionPostsCacheByChannel({
channelId: this.id,
// create a copy so that we only cache the first page
// if we use the same array, the store will get angry at us for modifying it outside of the store,
// when the user clicks load more
posts: [...this.latestCommunityPosts]
})
}
}).catch(async (err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
@ -1853,7 +1954,11 @@ export default defineComponent({
...mapActions([
'showOutlines',
'updateSubscriptionDetails'
'updateSubscriptionDetails',
'updateSubscriptionVideosCacheByChannel',
'updateSubscriptionLiveCacheByChannel',
'updateSubscriptionShortsCacheWithChannelPageShorts',
'updateSubscriptionPostsCacheByChannel'
])
}
})

View File

@ -60,6 +60,9 @@ export default defineComponent({
getPlaylistInfoDebounce: function() {},
playlistInEditMode: false,
playlistInVideoSearchMode: false,
videoSearchQuery: '',
promptOpen: false,
}
},
@ -107,7 +110,7 @@ export default defineComponent({
moreVideoDataAvailable() {
if (this.isUserPlaylistRequested) {
return this.userPlaylistVisibleLimit < this.videoCount
return this.userPlaylistVisibleLimit < this.sometimesFilteredUserPlaylistItems.length
} else {
return this.continuationData !== null
}
@ -126,17 +129,29 @@ export default defineComponent({
return this.selectedUserPlaylist?._id !== this.quickBookmarkPlaylistId
},
sometimesFilteredUserPlaylistItems() {
if (!this.isUserPlaylistRequested) { return this.playlistItems }
if (this.processedVideoSearchQuery === '') { return this.playlistItems }
return this.playlistItems.filter((v) => {
return v.title.toLowerCase().includes(this.processedVideoSearchQuery)
})
},
visiblePlaylistItems: function () {
if (!this.isUserPlaylistRequested) {
// No filtering for non user playlists yet
return this.playlistItems
}
if (this.userPlaylistVisibleLimit < this.videoCount) {
return this.playlistItems.slice(0, this.userPlaylistVisibleLimit)
if (this.userPlaylistVisibleLimit < this.sometimesFilteredUserPlaylistItems.length) {
return this.sometimesFilteredUserPlaylistItems.slice(0, this.userPlaylistVisibleLimit)
} else {
return this.playlistItems
return this.sometimesFilteredUserPlaylistItems
}
}
},
processedVideoSearchQuery() {
return this.videoSearchQuery.trim().toLowerCase()
},
},
watch: {
$route () {

View File

@ -28,6 +28,9 @@
}"
@enter-edit-mode="playlistInEditMode = true"
@exit-edit-mode="playlistInEditMode = false"
@search-video-mode-on="playlistInVideoSearchMode = true"
@search-video-mode-off="playlistInVideoSearchMode = false"
@search-video-query-change="(v) => videoSearchQuery = v"
@prompt-open="promptOpen = true"
@prompt-close="promptOpen = false"
/>
@ -39,48 +42,59 @@
<template
v-if="playlistItems.length > 0"
>
<transition-group
name="playlistItem"
tag="span"
<template
v-if="visiblePlaylistItems.length > 0"
>
<ft-list-video-numbered
v-for="(item, index) in visiblePlaylistItems"
:key="`${item.videoId}-${item.playlistItemId || index}`"
class="playlistItem"
:data="item"
:playlist-id="playlistId"
:playlist-type="infoSource"
:playlist-index="index"
:playlist-item-id="item.playlistItemId"
appearance="result"
:always-show-add-to-playlist-button="true"
:quick-bookmark-button-enabled="quickBookmarkButtonEnabled"
:can-move-video-up="index > 0"
:can-move-video-down="index < visiblePlaylistItems.length - 1"
:can-remove-from-playlist="true"
:video-index="index"
:initial-visible-state="index < 10"
@move-video-up="moveVideoUp(item.videoId, item.playlistItemId)"
@move-video-down="moveVideoDown(item.videoId, item.playlistItemId)"
@remove-from-playlist="removeVideoFromPlaylist(item.videoId, item.playlistItemId)"
/>
</transition-group>
<transition-group
name="playlistItem"
tag="span"
>
<ft-list-video-numbered
v-for="(item, index) in visiblePlaylistItems"
:key="`${item.videoId}-${item.playlistItemId || index}`"
class="playlistItem"
:data="item"
:playlist-id="playlistId"
:playlist-type="infoSource"
:playlist-index="playlistInVideoSearchMode ? playlistItems.findIndex(i => i === item) : index"
:playlist-item-id="item.playlistItemId"
appearance="result"
:always-show-add-to-playlist-button="true"
:quick-bookmark-button-enabled="quickBookmarkButtonEnabled"
:can-move-video-up="index > 0 && !playlistInVideoSearchMode"
:can-move-video-down="index < playlistItems.length - 1 && !playlistInVideoSearchMode"
:can-remove-from-playlist="true"
:video-index="playlistInVideoSearchMode ? playlistItems.findIndex(i => i === item) : index"
:initial-visible-state="index < 10"
@move-video-up="moveVideoUp(item.videoId, item.playlistItemId)"
@move-video-down="moveVideoDown(item.videoId, item.playlistItemId)"
@remove-from-playlist="removeVideoFromPlaylist(item.videoId, item.playlistItemId)"
/>
</transition-group>
<ft-flex-box
v-if="moreVideoDataAvailable && !isLoadingMore"
>
<ft-button
:label="$t('Subscriptions.Load More Videos')"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
@click="getNextPage"
/>
</ft-flex-box>
<div
v-if="isLoadingMore"
class="loadNextPageWrapper"
>
<ft-loader />
</div>
</template>
<ft-flex-box
v-if="moreVideoDataAvailable && !isLoadingMore"
v-else
>
<ft-button
:label="$t('Subscriptions.Load More Videos')"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
@click="getNextPage"
/>
<p class="message">
{{ $t("User Playlists['Empty Search Message']") }}
</p>
</ft-flex-box>
<div
v-if="isLoadingMore"
class="loadNextPageWrapper"
>
<ft-loader />
</div>
</template>
<ft-flex-box
v-else

View File

@ -454,7 +454,7 @@ export default defineComponent({
timestamp: formatDurationAsTimestamp(start),
startSeconds: start,
endSeconds: 0,
thumbnail: chapter.thumbnail[0].url
thumbnail: chapter.thumbnail[0]
})
}
} else {

View File

@ -1,5 +1,5 @@
# Put the name of your locale in the same language
Locale Name: 'الإنجليزية (الولايات المتحدة)'
Locale Name: 'العربية'
FreeTube: 'فري تيوب'
# Currently on Subscriptions, Playlists, and History
'This part of the app is not ready yet. Come back later when progress has been made.': >-
@ -173,6 +173,7 @@ User Playlists:
Reverted to use {oldPlaylistName} for quick bookmark: تمت العودة لاستخدام {oldPlaylistName}
للإشارة المرجعية السريعة
Quick bookmark disabled: تم تعطيل الإشارة المرجعية السريعة
Search for Videos: ‬البحث عن مقاطع الفيديو
AddVideoPrompt:
Select a playlist to add your N videos to: حدد قائمة تشغيل لإضافة الفيديو الخاص
بك إلى | حدد قائمة تشغيل لإضافة مقاطع الفيديو {videoCount} إليها
@ -186,6 +187,7 @@ User Playlists:
Save: حفظ
Search in Playlists: البحث في قوائم التشغيل
N playlists selected: تم تحديد {playlistCount}
Added {count} Times: تمت إضافة {count} الوقت | تمت إضافة {count} مرة
CreatePlaylistPrompt:
Toast:
There is already a playlist with this name. Please pick a different name.: توجد

View File

@ -146,6 +146,7 @@ User Playlists:
Select a playlist to add your N videos to: Vyberte playlist, do kterého přidat
vaše video | Vyberte playlist, do kterého přidat vašich {videoCount} videí
N playlists selected: Vybráno {playlistCount}
Added {count} Times: Přidáno {count}krát | Přidáno {count}krát
SinglePlaylistView:
Toast:
There were no videos to remove.: Nejsou zde žádná videa k odstranění.
@ -175,6 +176,7 @@ User Playlists:
This playlist is now used for quick bookmark: Tento playlist bude nyní použit
pro rychlé uložení
Quick bookmark disabled: Rychlé uložení vypnuto
Search for Videos: Hledat videa
Are you sure you want to delete this playlist? This cannot be undone: Opravdu chcete
odstranit tento playlist? Tato akce je nevratná.
Sort By:
@ -234,7 +236,7 @@ Settings:
General Settings:
General Settings: 'Obecné nastavení'
Check for Updates: 'Kontrolovat aktualizace'
Check for Latest Blog Posts: 'Kontrolovat nejnovější příspěvky blogů'
Check for Latest Blog Posts: 'Kontrolovat nejnovější příspěvky na blogu'
Fallback to Non-Preferred Backend on Failure: 'Při chybě přepnout na nepreferovaný
backend'
Enable Search Suggestions: 'Zapnout návrhy vyhledávání'

View File

@ -187,6 +187,8 @@ User Playlists:
EarliestPlayedFirst: 'Earliest Played'
SinglePlaylistView:
Search for Videos: Search for Videos
Toast:
This video cannot be moved up.: This video cannot be moved up.
This video cannot be moved down.: This video cannot be moved down.
@ -214,6 +216,8 @@ User Playlists:
Search in Playlists: Search in Playlists
Save: Save
Added {count} Times: 'Added {count} Time | Added {count} Times'
Toast:
You haven't selected any playlist yet.: You haven't selected any playlist yet.
"{videoCount} video(s) added to 1 playlist": "1 video added to 1 playlist | {videoCount} videos added to 1 playlist"

View File

@ -121,14 +121,14 @@ Playlists: 'Playlists'
User Playlists:
Your Playlists: 'Your playlists'
Playlist Message: This page is not reflective of fully working playlists. It only
lists videos that you have saved or favourited. When the work has finished, all
videos currently here will be migrated to a Favourites playlist.
lists videos that you have saved or made a Favourite. When the work has finished,
all videos currently here will be migrated to a Favourites playlist.
Your saved videos are empty. Click on the save button on the corner of a video to have it listed here: Your
saved videos are empty. Click on the save button on the corner of a video to have
it listed here
Search bar placeholder: Search in playlist
Empty Search Message: There are no videos in this playlist that match your search
Create New Playlist: Create new playlist
Create New Playlist: Create new Playlist
Add to Playlist: Add to playlist
This playlist currently has no videos.: This playlist currently has no videos.
Move Video Down: Move video down
@ -185,11 +185,21 @@ User Playlists:
"{videoCount} video(s) added to 1 playlist": 1 video added to 1 playlist | {videoCount}
videos added to 1 playlist
You haven't selected any playlist yet.: You haven't selected any playlist yet.
"{videoCount} video(s) added to {playlistCount} playlists": 1 video added to
{playlistCount} playlists | {videoCount} videos added to {playlistCount}
playlists
Select a playlist to add your N videos to: Select a playlist to add your video
to | Select a playlist to add your {videoCount} videos to
CreatePlaylistPrompt:
New Playlist Name: New playlist name
New Playlist Name: New Playlist name
Create: Create
Toast:
Playlist {playlistName} has been successfully created.: Playlist {playlistName}
has been successfully created.
There was an issue with creating the playlist.: There was an problem when creating
the playlist.
There is already a playlist with this name. Please pick a different name.: There
is already a Playlist with this name. Please pick a different name.
You have no playlists. Click on the create new playlist button to create a new one.: You
have no playlists. Click on the create new playlist button to create a new one.
Move Video Up: Move video up
@ -252,7 +262,7 @@ Settings:
Clear Default Instance: Clear default instance
Set Current Instance as Default: Set current instance as default
Current instance will be randomized on startup: Current instance will be randomised
on startup
on Startup
No default instance has been set: No default instance has been set
The currently set default instance is {instance}: The currently set default instance
is {instance}
@ -1082,8 +1092,8 @@ New Window: New window
Channels:
Empty: Your channel list is currently empty.
Unsubscribe: Unsubscribe
Unsubscribed: '{channelName} has been removed from your subscriptions'
Unsubscribe Prompt: Are you sure you want to unsubscribe from {channelName}?
Unsubscribed: '{channelName} has been removed from your Subscriptions'
Unsubscribe Prompt: Are you sure you want to Unsubscribe from {channelName}?
Title: Channel list
Search bar placeholder: Search channels
Channels: Channels
@ -1113,3 +1123,4 @@ Playlist will not pause when current video is finished: Playlist will not pause
current video is finished
Go to page: Go to {page}
Tag already exists: {tagName} tag already exists
Close Banner: Close Banner

View File

@ -142,6 +142,7 @@ User Playlists:
a la que añadir su vídeo | Seleccione una lista de reproducción a la que añadir
sus {videoCount} vídeos
N playlists selected: '{playlistCount} seleccionada'
Added {count} Times: Añadido {count} vez | Añadido {count} veces
SinglePlaylistView:
Toast:
There were no videos to remove.: No había vídeos que eliminar.

View File

@ -161,6 +161,7 @@ User Playlists:
Select a playlist to add your N videos to: Vali esitusloend, kuhu soovid oma video
lisada | Vali esitusloend, kuhu soovid oma {videoCount} videot lisada
N playlists selected: '{playlistCount} valitud'
Added {count} Times: Lisatud {count} kord | Lisatud {count} korda
SinglePlaylistView:
Toast:
There were no videos to remove.: Ei leidnud ühtegi videot, mida saaks eemaldada.
@ -884,6 +885,7 @@ Video:
Pause on Current Video: Peata hetkel esitatav video
Unhide Channel: Näita kanalit
Hide Channel: Peida kanal
More Options: Veel valikuid
Videos:
#& Sort By
Sort By:
@ -1120,3 +1122,7 @@ Channel Unhidden: '{channel} on eemaldatud kanalite filtrist'
Tag already exists: Silt „{tagName}“ on juba olemas
Trimmed input must be at least N characters long: Kärbitud sisend peab olema vähemalt
1 tähemärgi pikkune | Kärbitud sisend peab olema vähemalt {length} tähemärgi pikkune
Age Restricted:
This channel is age restricted: Kanali vaatamisel on vanusepiirang
This video is age restricted: Video vaatamisel on vanusepiirang
Close Banner: Sulge rekaampilt

View File

@ -34,6 +34,17 @@ Forward: 'Aurrera'
Global:
Videos: 'Bideoak'
Counts:
Subscriber Count: 1.harpideduna| {count} harpidedun
Watching Count: 1. ikuslea | {count} ikusle
Channel Count: 1. kanala ! {count} kanal
Video Count: 1. bideoa | {count} bideo
View Count: 1. ikustaldia | {count} ikustaldi
Live: Zuzenekoa
Shorts: Laburrak
Input Tags:
Length Requirement: Etiketak {zenbaki} karaktere izan behar ditu gutxienez
Community: Komunitatea
Version {versionNumber} is now available! Click for more details: '{versionNumber}
bertsioa erabilgarri! Klikatu azalpen gehiagorako'
Download From Site: 'Webgunetik jaitsi'
@ -90,6 +101,14 @@ Subscriptions:
Refresh Subscriptions: 'Harpidetzak freskatu'
Load More Videos: 'Bideo gehiago kargatu'
Error Channels: Akatsak dituzten kateak
Disabled Automatic Fetching: Harpidetza-bilaketa automatikoa desgaitu duzu. Freskatu
harpidetzak hemen ikusteko.
Empty Channels: Harpidetutako kanalek ez dute bideorik.
Empty Posts: Harpidetutako kanalek ez dute argitalpenik.
Load More Posts: Kargatu mezu gehiago
Subscriptions Tabs: Harpidetzen fitxak
All Subscription Tabs Hidden: Harpidetza fitxa guztiak ezkutatuta daude. Hemen edukia
ikusteko, erakutsi fitxa batzuk "{azpisection}" ataleko "{settingsSection}"-ean.
More: 'Gehiago'
Trending:
Trending: 'Joerak'
@ -111,6 +130,100 @@ User Playlists:
Search bar placeholder: Bilatu Erreprodukzio-zerrendan
Empty Search Message: Erreprodukzio-zerrenda honetan ez dago zure bilaketarekin
bat datorren bideorik
Remove Watched Videos: Kendu ikusitako bideoak
You have no playlists. Click on the create new playlist button to create a new one.: Ez
duzu erreprodukzio-zerrendarik. Egin klik sortu erreprodukzio-zerrenda berria
botoian berri bat sortzeko.
This playlist currently has no videos.: Erreprodukzio-zerrenda honek ez du bideorik.
Create New Playlist: Sortu erreprodukzio zerrenda berria
Add to Playlist: Gehitu erreprodukzio zerrendara
Add to Favorites: Gehitu {playlistName}-ra
Remove from Favorites: Kendu {playlistName}tik
Move Video Up: Mugitu bideoa gora
Move Video Down: Mugitu bideoa behera
Remove from Playlist: Kendu erreprodukzio zerrendatik
Playlist Name: Erreprodukzio zerrendaren izena
Playlist Description: Erreprodukzio-zerrendaren deskribapena
Save Changes: Gorde aldaketak
Cancel: Utzi
Edit Playlist Info: Editatu erreprodukzio zerrendaren informazioa
Copy Playlist: Kopiatu Erreprodukzio zerrenda
CreatePlaylistPrompt:
New Playlist Name: Erreprodukzio-zerrendaren izen berria
Toast:
Playlist {playlistName} has been successfully created.: '{playlistName} erreprodukzio-zerrenda
behar bezala sortu da.'
There was an issue with creating the playlist.: Arazo bat izan da erreprodukzio-zerrenda
sortzearekin.
There is already a playlist with this name. Please pick a different name.: Badago
dagoeneko izen honekin erreprodukzio-zerrenda bat. Mesedez, aukeratu beste
izen bat.
Create: Sortu
Enable Quick Bookmark With This Playlist: Gaitu laster-marka azkarra erreprodukzio-zerrenda
honekin
Disable Quick Bookmark: Desgaitu laster-marka azkarra
Sort By:
NameDescending: Z-A
LatestCreatedFirst: Sortu berria
EarliestCreatedFirst: Lehenen sortuak
LatestPlayedFirst: Duela gutxi erreproduzitua
EarliestPlayedFirst: Lehenen erreproduzitua
Sort By: Ordenatu honen arabera
NameAscending: A-Z
EarliestUpdatedFirst: Lehenen eguneratuak
LatestUpdatedFirst: Eguneratu berriak
Delete Playlist: Ezabatu erreprodukzio zerrenda
Are you sure you want to delete this playlist? This cannot be undone: Ziur erreprodukzio-zerrenda
ezabatu nahi duzula? Hau ezin da desegin.
SinglePlaylistView:
Toast:
Playlist has been updated.: Erreprodukzio-zerrenda eguneratu da.
"{videoCount} video(s) have been removed": bideo 1 kendu da | {videoCount} bideo
kendu dira
This video cannot be moved up.: Bideo hau ezin da gora eraman.
This video cannot be moved down.: Bideoa ezin da behera eraman.
Video has been removed: Bideoa kendu da
There was a problem with removing this video: Arazo bat izan da bideoa kentzean
This playlist is now used for quick bookmark: Erreprodukzio-zerrenda hau laster-markak
egiteko erabiltzen da orain
Quick bookmark disabled: Laster-marka azkarra desgaituta dago
This playlist is now used for quick bookmark instead of {oldPlaylistName}. Click here to undo: Erreprodukzio-zerrenda
hau laster-marketarako erabiltzen da orain {oldPlaylistName}-ren ordez. Egin
klik hemen desegiteko
Reverted to use {oldPlaylistName} for quick bookmark: '{oldPlaylistName} erabili
da berriro azkar markatzeko'
Some videos in the playlist are not loaded yet. Click here to copy anyway.: Erreprodukzio-zerrendako
bideo batzuk oraindik ez dira kargatu. Egin klik hemen kopiatzeko hala ere.
Playlist name cannot be empty. Please input a name.: Erreprodukzio zerrendaren
izena ezin da hutsik egon. Mesedez, idatzi izena.
There was an issue with updating this playlist.: Arazo bat izan da erreprodukzio-zerrenda
eguneratzean.
There were no videos to remove.: Ez zegoen kentzeko bideorik.
This playlist is protected and cannot be removed.: Erreprodukzio-zerrenda hau
babestuta dago eta ezin da kendu.
Playlist {playlistName} has been deleted.: '{playlistName} erreprodukzio-zerrenda
ezabatu da.'
This playlist does not exist: Erreprodukzio-zerrenda hau ez da existitzen
Search for Videos: Bilatu bideoak
AddVideoPrompt:
Added {count} Times: Gehitu da {count} Time | {count} aldiz gehitu da
Toast:
"{videoCount} video(s) added to 1 playlist": Bideo 1 gehitu da erreprodukzio-zerrenda
batera | {videoCount} bideo gehitu dira erreprodukzio-zerrenda batera
You haven't selected any playlist yet.: Oraindik ez duzu erreprodukzio zerrendarik
hautatu.
"{videoCount} video(s) added to {playlistCount} playlists": Bideo 1 gehitu da
{playlistCount} erreprodukzio zerrendetan | {videoCount} bideo gehitu dira
{playlistCount} erreprodukzio-zerrendetan
Select a playlist to add your N videos to: Hautatu erreprodukzio-zerrenda zure
bideoa -ra gehitzeko | Hautatu erreprodukzio-zerrenda zure {videoCount} bideoak
gehitzeko
N playlists selected: '{playlistCount} hautatuta'
Search in Playlists: Bilatu erreprodukzio-zerrendetan
Save: Gorde
Are you sure you want to remove all watched videos from this playlist? This cannot be undone: Ziur
ikusitako bideo guztiak erreprodukzio-zerrenda honetatik kendu nahi dituzula?
Hau ezin da desegin.
History:
# On History Page
History: 'Historikoa'
@ -147,6 +260,8 @@ Settings:
Beginning: 'Hasiera'
Middle: 'Erdian'
End: 'Bukaera'
Hidden: Ezkutatuta
Blur: Lausotzea
View all Invidious instance information: 'Invidious instantzia guztien informazioa
ikusi'
Region for Trending: 'Joeren eskualdea'
@ -179,6 +294,9 @@ Settings:
Dracula: Drakula
System Default: Sistemak lehenetsia
Catppuccin Mocha: Catppuccin Motxa
Nordic: nordikoa
Pastel Pink: Pastel arrosa
Hot Pink: Arrosa beroa
Main Color Theme:
Main Color Theme: 'Oinarrizko koloreen gaia'
Red: 'Gorria'
@ -221,6 +339,7 @@ Settings:
Secondary Color Theme: 'Gaiaren bigarren mailako kolorea'
#* Main Color Theme
Hide Side Bar Labels: Ezkutatu alboko barraren etiketak
Hide FreeTube Header Logo: Ezkutatu FreeTube goiburuko logotipoa
Player Settings:
Player Settings: 'Erreprodukzioaren ezarpenak'
Force Local Backend for Legacy Formats: 'Behartu backend lokala Legacy formatuentzat'
@ -272,6 +391,11 @@ Settings:
Display Play Button In Video Player: Bistaratu Erreproduzitzeko botoia bideo erreproduzitzailean
Max Video Playback Rate: Gehienezko bideoen erreprodukzio-tasa
Video Playback Rate Interval: Bideo Erreprodukzio-tasa tartea
Comment Auto Load:
Comment Auto Load: Iruzkin karga automatikoa
Skip by Scrolling Over Video Player: Saltatu bideo-erreproduzitzailean korrituz
Enter Fullscreen on Display Rotate: Sartu pantaila osoko pantaila biratu pantailan
Allow DASH AV1 formats: Baimendu DASH AV1 formatuak
Privacy Settings:
Privacy Settings: 'Pribatutasunari buruzko ezarpenak'
Remember History: 'Historikoa oroitu'
@ -289,11 +413,20 @@ Settings:
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: 'Ziur
al zaude zure profil eta harpidetza guztiak ezabatu nahi dituzula? Ezingo duzu
atzera egin.'
All playlists have been removed: Erreprodukzio-zerrenda guztiak kendu dira
Save Watched Videos With Last Viewed Playlist: Gorde ikusitako bideoak ikusitako
azken erreprodukzio-zerrendarekin
Remove All Playlists: Kendu erreprodukzio-zerrenda guztiak
Are you sure you want to remove all your playlists?: Ziur erreprodukzio-zerrenda
guztiak kendu nahi dituzula?
Subscription Settings:
Subscription Settings: 'Harpidetzen ezarpenak'
Hide Videos on Watch: 'Ikusten ari zaren bideoa ezkutatu'
Fetch Feeds from RSS: 'RSS jarioak eskuratu'
Manage Subscriptions: 'Harpidetzak kudeatu'
Fetch Automatically: Eskuratu jarioa automatikoki
Only Show Latest Video for Each Channel: Erakutsi soilik kanal bakoitzeko azken
bideoa
Distraction Free Settings:
Distraction Free Settings: 'Oharkabetasunak ekiditeko ezarpenak'
Hide Video Views: 'Bideoen ikustaldi kopurua ezkutatu'
@ -310,6 +443,38 @@ Settings:
Hide Video Description: Bideoaren deskribapena ezkutatu
Hide Comments: Iruzkinak ezkutatu
Hide Live Streams: Zuzeneko emanaldiak ezkutatu
Sections:
Side Bar: Alboko Barra
Subscriptions Page: Harpidetzak Orria
Channel Page: Kanalaren Orria
General: Orokorra
Watch Page: Ikusi Orria
Hide Profile Pictures in Comments: Ezkutatu profileko argazkiak iruzkinetan
Display Titles Without Excessive Capitalisation: Bistaratu izenburuak gehiegizko
letra larriz eta puntuaziorik gabe
Hide Channels Placeholder: Kanalaren IDa
Hide Channel Playlists: Ezkutatu kanaleko erreprodukzio-zerrendak
Hide Channel Community: Ezkutatu kanalaren komunitatea
Hide Channel Podcasts: Ezkutatu kanaleko podcastak
Hide Videos and Playlists Containing Text: Ezkutatu testua duten bideoak eta erreprodukzio-zerrendak
Hide Channels: Ezkutatu bideoak kanaletatik
Hide Upcoming Premieres: Ezkutatu datozen estreinaldiak
Hide Subscriptions Videos: Ezkutatu harpidetza-bideoak
Hide Subscriptions Shorts: Ezkutatu bideo laburren harpidetzak
Hide Channel Releases: Ezkutatu kanalen kaleratzeak
Hide Chapters: Ezkutatu kapituluak
Hide Channels Invalid: Emandako kanalaren IDa baliogabea da
Hide Channels API Error: Errore bat gertatu da emandako IDa duen erabiltzailea
berreskuratzean. Mesedez, egiaztatu berriro IDa zuzena den.
Hide Channels Already Exists: Kanalaren IDa badago jada
Hide Channel Shorts: Ezkutatu kanalaren bideo laburrak
Hide Videos and Playlists Containing Text Placeholder: Hitza, Hitzaren zatia edo
esaldia
Hide Subscriptions Live: Ezkutatu zuzenekoen harpidetzak
Hide Subscriptions Community: Ezkutatu harpidetzen komunitateak
Hide Featured Channels: Ezkutatu nabarmendutako kanalak
Hide Channels Disabled Message: Kanal batzuk IDa erabiliz blokeatu dira eta ez
dira prozesatu. Eginbidea blokeatuta dago ID horiek eguneratzen ari diren bitartean
Data Settings:
Data Settings: 'Datuen ezarpenak'
Select Import Type: 'Hautatu Inportazio mota'
@ -357,6 +522,18 @@ Settings:
esportatu dira
Playlist insufficient data: Ez da datu nahikorik "{playlist}" erreprodukzio zerrendarentzat,
elementutik ateratzen
History File: Historikoaren fitxategia
Playlist File: Erreprodukzio-zerrendaren fitxategia
Export Playlists For Older FreeTube Versions:
Label: Esportatu erreprodukzio zerrendak FreeTube bertsio zaharretarako
Tooltip: "Aukera honek erreprodukzio-zerrenda guztietako bideoak \"Gogokoak\"\
\ izeneko erreprodukzio-zerrenda batera esportatzen ditu.\nNola esportatu
eta inportatu bideoak erreprodukzio-zerrendetan FreeTube-ren bertsio zaharrago
baterako:\n 1. Esportatu zure erreprodukzio zerrendak aukera hau gaituta.\n
2. Ezabatu lehendik dituzun erreprodukzio-zerrenda guztiak Pribatutasun-ezarpenetan
dagoen Kendu zerrenda guztiak aukera erabiliz.\n 3. Abiarazi FreeTube-ren
bertsio zaharra eta inportatu esportatutako erreprodukzio-zerrendak.\""
Subscription File: Harpidetza Fitxategia
Proxy Settings:
Proxy Settings: 'Proxy-aren ezarpenak'
Enable Tor / Proxy: 'Tor / Proxy ahalbidetu'
@ -381,6 +558,10 @@ Settings:
External Player: Kanpoko erreproduzitzailea
Custom External Player Executable: Lehenetsitako kanpo erreproduzitzailea exekutagarria
Custom External Player Arguments: Lehenetsitako kanpo erreproduzitzailea argudioak
Ignore Default Arguments: Ez ikusi lehenetsitako argudioak
Players:
None:
Name: Bat ere ez
Download Settings:
Download Settings: Deskargen ezarpenak
Ask Download Path: Deskargaren ibilbidea galdetu
@ -402,11 +583,31 @@ Settings:
'SponsorBlock API Url (Default is https://sponsor.ajay.app)': Babesleak blokeatzeko
API Url (lehenetsia https://sponsor.ajay.app da)
SponsorBlock Settings: Babesleak blokeatzeko ezarpenak
UseDeArrowTitles: Erabili DeArrow bideo-izenburuak
UseDeArrowThumbnails: Erabili DeArrow irudi txikietarako
'DeArrow Thumbnail Generator API Url (Default is https://dearrow-thumb.ajay.app)': DeArrow
Thumbnail Generator API URLa (lehenetsia https://dearrow-thumb.ajay.app da)
Parental Control Settings:
Show Family Friendly Only: Erakutsi familientzat aproposa dena bakarrik
Hide Unsubscribe Button: Harpidetza kendu botoia ezkutatu
Parental Control Settings: Gurasoen kontrolaren ezarpenak
Hide Search Bar: Bilaketa barra ezkutatu
Password Dialog:
Password: Pasahitza
Password Incorrect: Pasahitza okerra
Unlock: Desblokeatu
Enter Password To Unlock: Sartu pasahitza ezarpenak desblokeatzeko
Experimental Settings:
Replace HTTP Cache: Ordeztu HTTP cachea
Experimental Settings: Ezarpen esperimentalak
Warning: Ezarpen hauek esperimentalak dira, aktibatuta dauden bitartean hutsegiteak
eragin ditzakete. Oso gomendagarria da babeskopiak egitea. Erabili zure ardurapean!
Password Settings:
Password Settings: Pasahitz ezarpenak
Set Password To Prevent Access: Ezarri pasahitz bat ezarpenetara sarbidea galarazteko
Remove Password: Pasahitza ezabatu
Set Password: Ezarri pasahitza
Expand All Settings Sections: Zabaldu ezarpen guztien atalak
About:
#On About page
About: 'Honi buruz'
@ -436,6 +637,7 @@ About:
these people and projects: 'Hurrengo pertsonak eta proiektuak'
Donate: 'Donazioa egin'
Discussions: Eztabaidak
Profile:
Profile Select: 'Hautatu profila'
Profile Filter: 'Profilaren iragazkiak'
@ -480,6 +682,12 @@ Profile:
batetik ezabatuko.'
#On Channel Page
Profile Settings: Profilaren ezarpenak
Close Profile Dropdown: Itxi goitibeherako profila
Open Profile Dropdown: Ireki goitibeherako profila
Toggle Profile List: Aldatu profilen zerrenda
Edit Profile Name: Editatu profilaren izena
Create Profile Name: Sortu profilaren izena
Profile Name: Profilaren izena
Channel:
Subscribe: 'Harpidetu'
Unsubscribe: 'Harpidetza kendu'
@ -510,6 +718,39 @@ Channel:
About: 'Honi buruz'
Channel Description: 'Kanalaren deskribapena'
Featured Channels: 'Nabarmendutako kanalak'
Joined: Bat eginda
Location: Kokapena
Tags:
Tags: Etiketak
Search for: Bilatu "{tag}"
Details: Xehetasunak
This channel does not exist: Kanal hau ez da existitzen
This channel is age-restricted and currently cannot be viewed in FreeTube.: Kanal
hau adin mugatuta dago eta une honetan ezin da ikusi FreeTube-n.
Shorts:
This channel does not currently have any shorts: Une honetan kanal honek ez du
bideo laburrik
Releases:
This channel does not currently have any releases: Une honetan kanal honek ez
du argitalpenik
Releases: Argitalpenak
Community:
Hide Answers: Erantzunak ezkutatu
votes: '{votes} bozka'
This channel currently does not have any posts: Une honetan kanal honek ez du
argitalpenik
Reveal Answers: Erantzunak agerian utzi
Video hidden by FreeTube: FreeTube-k ezkutatutako bideoa
Channel Tabs: Kanalaren fitxak
This channel does not allow searching: Kanal honek ez du bilaketarik onartzen
Live:
Live: Zuzenekoak
This channel does not currently have any live streams: Kanal honek ez du zuzeneko
erreprodukziorik
Podcasts:
Podcasts: Podcastak
This channel does not currently have any podcasts: Une honetan kanal honek ez
du podcastik
Video:
Mark As Watched: 'Ikusitako gisa jarri'
Remove From History: 'Historikotik ezabatu'
@ -592,6 +833,7 @@ Video:
Years: 'Urteak'
Ago: 'Duela'
Upcoming: 'Estreinaldiak'
In less than a minute: Minutu bat baino gutxiagoan
Published on: 'Noiz argitaratua'
Streamed on: 'Noiz zuzenean emana'
Started streaming on: 'Noiz hasi zen zuzenekoa'
@ -636,6 +878,16 @@ Video:
intro: Sarrera
Skipped segment: Saltatu egin da segmentua
Premieres on: Estreinaldiak
Hide Channel: Kanala ezkutatu
Unhide Channel: Kanala erakutsi
Premieres: Estreinaldiak
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Zuzeneko
txata ez dago erabilgarri zuzeneko honentzat. Baliteke igo duenak desgaitu izana.
Show Super Chat Comment: Erakutsi Super txat iruzkina
Pause on Current Video: Gelditu uneko bideoa
More Options: Aukera gehiago
Upcoming: Datozenak
Scroll to Bottom: Joan Beherantz
Videos:
#& Sort By
Sort By:
@ -685,6 +937,7 @@ Share:
YouTube Channel URL copied to clipboard: 'Youtube-ko kanalaren URL-a arbelean itsatsi
da'
Share Channel: Kanala partekatu
Mini Player: 'Erreproduzitzaile txikia'
Comments:
Comments: 'Iruzkinak'
@ -710,6 +963,10 @@ Comments:
Show More Replies: Erantzun gehiago erakutsi
And others: eta bestelakoak
Pinned by: Honengatik ainguratuta
Hearted: Bihotzez
From {channelName}: '{channelName}-tik'
View {replyCount} replies: Ikusi {replyCount} erantzunak
Subscribed: Harpidetuta
Up Next: 'Hurrengoa'
#Tooltips
@ -749,11 +1006,19 @@ Tooltips:
eta mantendu (Komando tekla MAC-etan) eta ondoren saguaren ezkerreko botoia
sakatu, lehenetsitako erreprodukzio tasara itzultzeko (1x baldin eta ezarpenetan
aldaketarik egin ez bada).
Skip by Scrolling Over Video Player: Erabili korritze-gurpila bideoa saltatzeko,
MPV estiloa.
Allow DASH AV1 formats: DASH AV1 formatuak DASH H.264 formatuak baino itxura hobea
izan dezake. DASH AV1 formatuek potentzia gehiago behar dute erreproduzitzeko!
Ez daude bideo guztietan eskuragarri, kasu horietan erreproduzitzaileak DASH
H.264 formatuak erabiliko ditu ordez.
Subscription Settings:
Fetch Feeds from RSS: 'Posible denean, Freetube-k bere lehenetsitako metodoa erabili
beharrean RSS-ak baliatuko ditu zure harpidetzen jariora konektatzeko. RSS arinagoa
izateaz gain, IP-en blokeoak saihesten ditu. Aldiz, zuzenekoaren egoeraren edo
bideoaren iraupenaren informaziorik ez du ematen, besteak beste'
Fetch Automatically: Gaituta dagoenean, FreeTubek automatikoki eskuratuko du zure
harpidetza-jarioa leiho berri bat irekitzen denean eta profila aldatzean.
Privacy Settings:
Remove Video Meta Files: 'Gaituta dagoenean, FreeTube-k automatikoki ezabatzen
ditu bideoen erreprodukzioan sortutako metafitxategiak, bistaratze orria ixten
@ -772,6 +1037,29 @@ Tooltips:
Individious-en ezarpenek ez dituzte kanpo erreproduzitzaileak trabatzen.
Custom External Player Arguments: Komando-lerroko argumentu pertsonalizatuak,
puntu eta komaz bereizita (';'), kanpoko erreproduzitzailera pasatzea nahi duzu.
Ignore Default Arguments: Ez bidali argumentu lehenetsirik kanpoko erreproduzitzaileari
bideoaren URLaz gain (adibidez, erreprodukzio-tasa, erreprodukzio-zerrendaren
URLa, etab.). Argumentu pertsonalizatuak transmitituko dira oraindik.
Distraction Free Settings:
Hide Channels: Sartu kanalaren ID bat bideo, erreprodukzio-zerrenda eta kanala
bera bilaketetan, joeran, ezagunenetan eta gomendagarrienetan ager ez dadin
ezkutatzeko. Sartutako kanalaren IDak guztiz bat etorri behar du eta maiuskulak
eta minuskulak bereizten ditu.
Hide Videos and Playlists Containing Text: Idatzi hitz bat, hitz-zati bat edo
esaldi bat (maiuskulak eta minuskulak bereizten ez diren) jatorrizko izenburuak
FreeTube osoan duten bideo eta erreprodukzio-zerrenda guztiak ezkutatzeko, historia,
zure erreprodukzio-zerrendak eta erreprodukzio-zerrenden barneko bideoak soilik
kenduta.
Hide Subscriptions Live: Ezarpen hau aplikazio osorako "{appWideSetting}" ezarpenak
ordezkatzen du, "{settingsSection}" ataleko "{azpisekzioa}" atalean
Experimental Settings:
Replace HTTP Cache: Electron-en diskoan oinarritutako HTTP cachea desgaitzen du
eta memoriako irudien cache pertsonalizatua gaitu. RAM erabilera handitzea ekarriko
du.
SponsorBlock Settings:
UseDeArrowThumbnails: Ordeztu bideoaren miniaturak DeArrow-en miniaturaz.
UseDeArrowTitles: Ordeztu bideoen izenburuak DeArrow-en erabiltzaileek bidalitako
tituluekin.
Local API Error (Click to copy): 'Tokiko API-ak huts egin du (klikatu kopiatzeko)'
Invidious API Error (Click to copy): 'Individious-eko APIak huts egin du (klikatu
kopiatzeko)'
@ -828,3 +1116,33 @@ Channels:
Count: '{number} kanal aurkitu dira.'
Empty: Zure kanalen zerrenda hutsik da.
Preferences: Hobespenak
Go to page: Joan {page}-ra
Close Banner: Itxi iragarkia
Age Restricted:
This channel is age restricted: Kanal hau adin mugatuta dago
This video is age restricted: Bideo hau adin mugatuta dago
Playlist will not pause when current video is finished: Erreprodukzio-zerrenda ez
da etengo uneko bideoa amaitzen denean
Playlist will pause when current video is finished: Erreprodukzio-zerrenda pausatu
egingo da uneko bideoa amaitzen denean
Channel Unhidden: '{channel} kanalaren iragazkitik kendu da'
Clipboard:
Cannot access clipboard without a secure connection: Ezin da arbelera sartu konexio
segururik gabe
Copy failed: Ezin izan da kopiatu arbelean
Tag already exists: '"{tagName}" etiketa badago jada'
Trimmed input must be at least N characters long: Moztutako sarrerak karaktere bat
izan behar du gutxienez | Moztutako sarrerak gutxienez {length} karaktere izan behar
ditu
Chapters:
'Chapters list hidden, current chapter: {chapterName}': 'Kapituluen zerrenda ezkutatuta
dago, uneko kapitulua: {chapterName}'
Chapters: Kapituluak
'Chapters list visible, current chapter: {chapterName}': 'Kapituluen zerrenda ikusgai,
uneko kapitulua: {chapterName}'
Hashtag:
Hashtag: Traola
This hashtag does not currently have any videos: Traola honek ez du bideorik une
honetan
Ok: Ados
Channel Hidden: '{channel} gehitu da kanalaren iragazkian'

View File

@ -148,6 +148,7 @@ User Playlists:
à {playlistCount} listes de lecture | {videoCount} vidéos ajoutées à {playlistCount}
listes de lecture
N playlists selected: '{playlistCount} Sélectionnée(s)'
Added {count} Times: Ajouté {count} Fois | Ajouté {count} Fois
SinglePlaylistView:
Toast:
There were no videos to remove.: Il n'y avait aucune vidéo à supprimer.

View File

@ -195,6 +195,7 @@ User Playlists:
videa dodana u 1 zbirku
Select a playlist to add your N videos to: Odaberi zbirku za dodavanje tvog videa
| Odaberi zbirku za dodavanje tvojih {videoCount} videa
Added {count} Times: Dodano {count} puta | Dodano {count} puta
CreatePlaylistPrompt:
Create: Stvori
Toast:
@ -272,7 +273,7 @@ Settings:
Black: 'Crna'
Dark: 'Tamna'
Light: 'Svijetla'
Dracula: 'Drakula'
Dracula: 'Dracula'
System Default: Standard sustava
Catppuccin Mocha: Catppuccin Mocha
Pastel Pink: Pastelno ružičasta
@ -928,7 +929,7 @@ Video:
Resolution: Razlučivost
Player Dimensions: Dimenzije playera
Bitrate: Brzina prijenosa
Volume: Glasnoća
Volume: Direktorij
Bandwidth: Propusnost
Buffered: Učitano u memoriju
Mimetype: Mimetype

View File

@ -147,12 +147,13 @@ User Playlists:
lejátszási listákhoz
"{videoCount} video(s) added to 1 playlist": 1 videó hozzáadva 1 lejátszási
listához | {videoCount} videó hozzáadása 1 lejátszási listához
You haven't selected any playlist yet.: Még nem választott ki lejátszási listát
sem.
You haven't selected any playlist yet.: Még nem választott ki egyetlen lejátszási
listát sem.
Select a playlist to add your N videos to: Válasszon ki egy lejátszási listát
a videó hozzáadásához | Válasszon ki egy lejátszási listát a {videoCount} videó
hozzáadásához
N playlists selected: '{playlistCount} Kiválasztott'
Added {count} Times: Hozzáadva {count} Alkalommal | Hozzáadva {count} Alkalommal
SinglePlaylistView:
Toast:
There were no videos to remove.: Nem voltak eltávolítható videók.
@ -183,6 +184,7 @@ User Playlists:
Kattints ide a visszavonáshoz
Reverted to use {oldPlaylistName} for quick bookmark: Visszaállítva a(z) {oldPlaylistName}
használatára a gyors könyvjelzőhöz
Search for Videos: Videók keresése
Are you sure you want to delete this playlist? This cannot be undone: Biztos, hogy
törölni szeretné ezt a lejátszási listát? Ezt nem lehet visszacsinálni.
Sort By:

View File

@ -44,6 +44,8 @@ Global:
Subscriber Count: 1 áskrifandi | {count} áskrifendur
View Count: 1 áhorf | {count} áhorf
Watching Count: 1 að horfa | {count} að horfa
Input Tags:
Length Requirement: Merki þarf að vera a.m.k. {number} stafa langt
Version {versionNumber} is now available! Click for more details: 'Útgáfa {versionNumber}
er tiltæk! Smelltu til að skoða nánar'
Download From Site: 'Sækja af vefsvæði'
@ -174,6 +176,14 @@ User Playlists:
Some videos in the playlist are not loaded yet. Click here to copy anyway.: Sum
myndskeið í spilunarlistanum hafa ekki enn hlaðist inn. Smelltu hér til að
afrita samt.
This playlist is now used for quick bookmark: Þessi spilunarlisti er núna notaður
undir flýtibókamerki
Quick bookmark disabled: Flýtibókamerki óvirk
This playlist is now used for quick bookmark instead of {oldPlaylistName}. Click here to undo: Þessi
spilunarlisti er núna notaður undir flýtibókamerki í stað {oldPlaylistName}.
Smelltu hér til að afturkalla
Reverted to use {oldPlaylistName} for quick bookmark: Snéri aftur í að nota
{oldPlaylistName} undir flýtibókamerki
AddVideoPrompt:
N playlists selected: '{playlistCount} valin'
Search in Playlists: Leita í spilunarlistum
@ -187,6 +197,7 @@ User Playlists:
| {videoCount} myndskeiðum bætt við 1 spilunarlista
Select a playlist to add your N videos to: Veldu spilunarlista til að bæta myndskeiðinu
þínu á | Veldu spilunarlista til að bæta {videoCount}̣ myndskeiðunum þínum á
Added {count} Times: Bætt við {count} sinni | Bætt við {count} sinnum
CreatePlaylistPrompt:
Create: Búa til
New Playlist Name: Heiti á nýjum spilunarlista
@ -209,6 +220,10 @@ User Playlists:
Delete Playlist: Eyða spilunarlista
Are you sure you want to delete this playlist? This cannot be undone: Ertu viss
um að þú viljir eyða þessum spilunarlista? Aðgerðin er ekki afturkallanleg.
Add to Favorites: Bæta á {playlistName}
Remove from Favorites: Fjarlægja af {playlistName}
Enable Quick Bookmark With This Playlist: Virkja flýtibókamerki með þessum spilunarlista
Disable Quick Bookmark: Eyða flýtibókamerki
History:
# On History Page
History: 'Áhorf'
@ -283,6 +298,7 @@ Settings:
Catppuccin Mocha: Catppuccin Mocha
Pastel Pink: Pastelbleikt
Hot Pink: Dimmbleikt
Nordic: Norrænt
Main Color Theme:
Main Color Theme: 'Aðallitur þema'
Red: 'Rautt'
@ -436,6 +452,7 @@ Settings:
Hide Channels: Fela myndskeið úr rásum
Hide Channels Placeholder: Auðkenni rásar
Display Titles Without Excessive Capitalisation: Birta titla án umfram-hástafa
og greinarmerkja
Sections:
Side Bar: Hliðarspjald
Channel Page: Rásasíða
@ -461,6 +478,9 @@ Settings:
Hide Channels Already Exists: Auðkenni rásar er þegar til
Hide Channels API Error: Villa við að ná í notanda með uppgefið auðkenni. Athugaðu
aftur hvort auðkennið ré rétt.
Hide Videos and Playlists Containing Text: Fela myndskeið og spilunarlista sem
innihalda texta
Hide Videos and Playlists Containing Text Placeholder: Orð, orðhluti eða setning
Data Settings:
Data Settings: 'Stillingar gagna'
Select Import Type: 'Veldu tegund innflutnings'
@ -721,6 +741,7 @@ Channel:
Reveal Answers: Birta svör
Hide Answers: Fela svör
votes: '{votes} atkvæði'
Video hidden by FreeTube: Myndskeið falið af FreeTube
Shorts:
This channel does not currently have any shorts: Þessi rás er í augnablikinu ekki
með neinar stuttmyndir
@ -878,6 +899,7 @@ Video:
Pause on Current Video: Setja núverandi myndskeið í bið
Unhide Channel: Birta rás
Hide Channel: Fela rás
More Options: Fleiri valkostir
Videos:
#& Sort By
Sort By:
@ -1117,3 +1139,8 @@ Playlist will not pause when current video is finished: Spilunarlisti mun ekki f
Go to page: Fara á {page}
Channel Hidden: '{channel} bætt við rásasíu'
Channel Unhidden: '{channel} fjarlægt úr rásasíu'
Close Banner: Loka borða
Age Restricted:
This video is age restricted: Þetta myndskeið er með aldurstakmörkunum
This channel is age restricted: Þessi rás er með aldurstakmörkunum
Tag already exists: '"{tagName}" merkið er þegar til staðar'

View File

@ -143,6 +143,7 @@ User Playlists:
Select a playlist to add your N videos to: Seleziona una playlist a cui aggiungere
il tuo video | Seleziona una playlist a cui aggiungere i tuoi {videoCount} video
N playlists selected: '{playlistCount} selezionate'
Added {count} Times: Aggiunto {count} volta | Aggiunti {count} volte
SinglePlaylistView:
Toast:
There were no videos to remove.: Non c'erano video da rimuovere.
@ -175,6 +176,7 @@ User Playlists:
This playlist is now used for quick bookmark instead of {oldPlaylistName}. Click here to undo: Questa
playlist è ora usata per i segnalibri rapidi al posto di {oldPlaylistName}.
Fai clic qui per annullare
Search for Videos: Cerca video
Are you sure you want to delete this playlist? This cannot be undone: Sei sicuro
di voler eliminare questa playlist? Questa operazione non può essere annullata.
Sort By:

View File

@ -114,6 +114,10 @@ User Playlists:
このページは、完全に動作する動画リストではありません。保存またはお気に入りと設定した動画のみが表示されます。操作が完了すると、現在ここにあるすべての動画は「お気に入り」の動画リストに移動します。
Search bar placeholder: 動画リスト内の検索
Empty Search Message: この再生リストに、検索に一致する動画はありません
This playlist currently has no videos.: 存在、この再生リストには動画があっていません。
Create New Playlist: 新規再生リストを作られる
Sort By:
NameAscending: A-Z
History:
# On History Page
History: '履歴'
@ -276,7 +280,7 @@ Settings:
Folder Button: フォルダーの選択
Enter Fullscreen on Display Rotate: 横画面時にフルスクリーンにする
Skip by Scrolling Over Video Player: 動画プレーヤーでスクロールしてスキップ可能にする
Allow DASH AV1 formats: DASH AV1形式を許可する
Allow DASH AV1 formats: DASH AV1形式を許可
Comment Auto Load:
Comment Auto Load: コメント自動読み込み
Subscription Settings:
@ -850,7 +854,7 @@ The playlist has been reversed: 再生リストを逆順にしました
A new blog is now available, {blogTitle}. Click to view more: '新着ブログ公開、{blogTitle}。クリックしてブログを読む'
Download From Site: サイトからダウンロード
Version {versionNumber} is now available! Click for more details: 最新バージョン {versionNumber}
配信中! 詳細はクリックして確認してください
配信中!詳細はクリックして確認してください
This video is unavailable because of missing formats. This can happen due to country unavailability.: この動画は、動画形式の情報が利用できないため再生できません。再生が許可されていない国で発生します。
Tooltips:
Subscription Settings:
@ -866,8 +870,8 @@ Tooltips:
Scroll Playback Rate Over Video Player: カーソルが動画上にあるとき、Ctrl キーMac では Command キーを押したまま、マウスホイールを前後にスクロールして再生速度を調整します。Control
キーMac では Command キー)を押したままマウスを左クリックすると、すぐにデフォルトの再生速度(設定を変更していない場合は 1 xに戻ります。
Skip by Scrolling Over Video Player: スクロール ホイールを使用して、ビデオ、MPV スタイルをスキップします。
Allow DASH AV1 formats: DASH H.264形式よりDASH AV1形式の方がきれいに見える可能性があるけど、再生には必要な電力がより多い。全ての動画でDASH
AV1を利用できないため、プレイヤーはDASH H.264形式に自動変更する場合があります。
Allow DASH AV1 formats: DASH H.264形式よりDASH AV1形式の方がきれいに見える可能性がありますが、再生にはより多くの処理能力が必要となります。DASH
AV1形式を使用できない場合、プレイヤーはDASH H.264形式を自動で使用します。
General Settings:
Invidious Instance: FreeTube が使用する Invidious API の接続先サーバーです。
Preferred API Backend: FreeTube が youtube からデータを取得する方法を選択します。「内部 API」とはアプリから取得する方法です。「Invidious
@ -941,3 +945,5 @@ Hashtag:
This hashtag does not currently have any videos: このハッシュタグには現在動画がありません
Playlist will pause when current video is finished: 現在のビデオが終了すると、プレイリストは停止します
Playlist will not pause when current video is finished: 現在のビデオが終了しても、プレイリストは停止しません
Close Banner: バナーを閉じる
Go to page: '{page}に行く'

View File

@ -41,6 +41,9 @@ Global:
Counts:
Subscriber Count: 1 prenumeruoti | {count} prenumeratorių
Channel Count: 1 kanalas | {count} kanalai
Video Count: 1 vaizdo įrašas | {count} vaizdo įrašai
View Count: 1 peržiūrėti | {count} peržiūrų
Watching Count: 1 žiūri | {count} žiūri
Version {versionNumber} is now available! Click for more details: 'Versija {versionNumber}
jau prieinama! Spustelėkite, jei norite gauti daugiau informacijos'
Download From Site: 'Atsisiųsti iš svetainės'
@ -904,3 +907,4 @@ Clipboard:
be saugaus ryšio
Preferences: Nuostatos
Go to page: Eiti į {page}
Close Banner: Uždaryti baneris

View File

@ -166,6 +166,7 @@ User Playlists:
Select a playlist to add your N videos to: Selecteer een afspeellijst om uw video
aan toe te voegen | Selecteer een afspeellijst om uw {videoCount} video's aan
toe te voegen
Added {count} Times: '{count} keer toe­gevoegd | {count} keer toe­gevoegd'
Save Changes: Wijzigingen opslaan
Copy Playlist: Afspeel­lijst kopiëren
Create New Playlist: Nieuwe afspeel­lijst aanmaken

View File

@ -143,6 +143,7 @@ User Playlists:
dodać swój film | Wybierz playlistę, do której chcesz dodać swoje {videoCount}
film(y/ów)
N playlists selected: Zaznaczono {playlistCount}
Added {count} Times: Dodano {count} raz | Dodano {count} razy
SinglePlaylistView:
Toast:
There were no videos to remove.: Nie było żadnych filmów do usunięcia.
@ -1221,3 +1222,4 @@ Trimmed input must be at least N characters long: Przycięte wyrażenie musi mie
Age Restricted:
This video is age restricted: Ten film ma ograniczenie wiekowe
This channel is age restricted: Ten kanał ma ograniczenie wiekowe
Close Banner: Zamknij Baner

File diff suppressed because it is too large Load Diff

View File

@ -42,6 +42,8 @@ Global:
View Count: 1 vizionare | {count} vizionări
Channel Count: 1 canal | {count} canale
Watching Count: 1 se uită | {count} se uită
Input Tags:
Length Requirement: Tag-ul trebuie să aibă cel puțin {number} caractere
Version {versionNumber} is now available! Click for more details: 'Versiunea {versionNumber}
este acum disponibilă! Click pentru mai multe detalii'
Download From Site: 'Descărcați de pe site'
@ -108,6 +110,8 @@ Subscriptions:
All Subscription Tabs Hidden: Toate filele de abonament sunt ascunse. Pentru a vedea
conținutul aici, vă rugăm să afișați unele file din secțiunea "{subsection}” din
"{settingsSection}”.
Empty Posts: Canalele tale abonate nu au momentan nicio postare.
Load More Posts: Încarcă mai multe postări
Trending:
Trending: 'Tendințe'
Trending Tabs: File în tendințe
@ -1003,3 +1007,5 @@ Playlist will pause when current video is finished: Lista de redare se va între
când videoclipul curent este terminat
Playlist will not pause when current video is finished: Lista de redare nu se va întrerupe
când videoclipul curent este terminat
Go to page: Mergeți la {page}
Close Banner: Închideți bannerul

View File

@ -149,6 +149,7 @@ User Playlists:
да додате видео снимак | Изаберите плејлисту на коју желите да додате {videoCount}
видео снимака
N playlists selected: 'Изабрано: {playlistCount}'
Added {count} Times: Додато {count} пут | Додато {count} пута
SinglePlaylistView:
Toast:
There were no videos to remove.: Није било видео снимака за уклањање.

View File

@ -178,6 +178,7 @@ User Playlists:
kullanımına geri dönüldü
This playlist is now used for quick bookmark: Bu oynatma listesi artık hızlı
yer imi için kullanılıyor
Search for Videos: Video Ara
AddVideoPrompt:
Select a playlist to add your N videos to: Videonuzu eklemek için bir oynatma
listesi seçin | {videoCount} videonuzu eklemek için bir oynatma listesi seçin
@ -191,6 +192,7 @@ User Playlists:
| {videoCount} video 1 oynatma listesine eklendi
N playlists selected: '{playlistCount} Seçildi'
Search in Playlists: Oynatma Listelerinde Ara
Added {count} Times: '{count} Defa Eklendi | {count} Defa Eklendi'
CreatePlaylistPrompt:
New Playlist Name: Yeni Oynatma Listesi Adı
Create: Oluştur

View File

@ -43,6 +43,8 @@ Global:
Subscriber Count: 1 підписник | {count} підписників
View Count: 1 перегляд | {count} переглядів
Watching Count: 1 глядач | {count} глядачів
Input Tags:
Length Requirement: Тег повинен мати довжину не менше {number} символів
Version {versionNumber} is now available! Click for more details: 'Доступна нова
версія {versionNumber}! Натисніть, щоб переглянути подробиці'
Download From Site: 'Завантажити з сайту'
@ -116,17 +118,36 @@ Trending:
Music: Музика
Default: Типово
Most Popular: 'Найпопулярніші'
Playlists: 'Добірки'
Playlists: 'Списки відтворення'
User Playlists:
Your Playlists: 'Ваші добірки'
Your Playlists: 'Ваші списки відтворення'
Your saved videos are empty. Click on the save button on the corner of a video to have it listed here: Збережені
відео порожні. Клацніть на кнопку збереження у куті відео, щоб воно було перелічено
тут
Playlist Message: Ця сторінка не показує повністю робочих добірок. На ній перелічено
лише відео, які ви зберегли або вибрали. Коли робота завершиться, усі відео, які
зараз знаходяться тут, буде переміщено до добірки "Вибране".
Playlist Message: Ця сторінка не показує повністю робочих списків відтворення. На
ній перелічено лише відео, які ви зберегли або вибрали. Коли робота завершиться,
усі відео, які зараз знаходяться тут, буде переміщено до списку відтворення "Вибране".
Search bar placeholder: Шукати у добірці
Empty Search Message: Немає відео в цій добірці, які відповідають вашому запиту
Create New Playlist: Створити новий список відтворення
Add to Playlist: Додати список відтворення
Remove from Favorites: Вилучити з {playlistName}
Move Video Up: Посунути відео вгору
Move Video Down: Посунути відео вниз
Remove from Playlist: Вилучити зі списку відтворення
Playlist Description: Опис списку відтворення
Save Changes: Зберегти зміни
Cancel: Скасувати
Copy Playlist: Скопіювати список відтворення
You have no playlists. Click on the create new playlist button to create a new one.: У
вас немає списків відтворення. Натисніть на кнопку створити новий список відтворення,
щоб створити його.
This playlist currently has no videos.: Наразі у цьому списку відтворення немає
відео.
Add to Favorites: Додати до {playlistName}
Playlist Name: Назва списку відтворення
Edit Playlist Info: Змінити інформацію списку відтворення
Remove Watched Videos: Вилучити з переглянутих відео
History:
# On History Page
History: 'Історія'
@ -321,8 +342,8 @@ Settings:
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: 'Справді
хочете вилучити всі підписки та профілі? Цю дію не можна скасувати.'
Automatically Remove Video Meta Files: Автоматично вилучати метафайли відео
Save Watched Videos With Last Viewed Playlist: Зберегти переглянуті відео в добірку,
яку ви переглядали останнім часом
Save Watched Videos With Last Viewed Playlist: Зберегти переглянуті відео у список
відтворення, який ви переглядали останнім часом
Subscription Settings:
Subscription Settings: 'Налаштування підписки'
Hide Videos on Watch: 'Ховати відео при перегляді'
@ -342,7 +363,7 @@ Settings:
Hide Popular Videos: 'Не показувати популярні відео'
Hide Live Chat: 'Не показувати живий чат'
Hide Active Subscriptions: Сховати активні підписки
Hide Playlists: Сховати добірки
Hide Playlists: Сховати списки відтворення
Hide Video Description: Сховати опис відео
Hide Comments: Сховати коментарі
Hide Sharing Actions: Сховати дії поширення
@ -354,7 +375,7 @@ Settings:
Display Titles Without Excessive Capitalisation: Показувати заголовки без надмірно
великих літер
Hide Featured Channels: Сховати пропоновані канали
Hide Channel Playlists: Сховати добірки з каналів
Hide Channel Playlists: Сховати списки відтворення каналу
Hide Channel Community: Сховати спільноту каналу
Hide Channel Shorts: Сховати Shorts каналу
Sections:
@ -414,13 +435,13 @@ Settings:
Unknown data key: 'Невідомий ключ даних'
How do I import my subscriptions?: 'Як імпортувати свої підписки?'
Manage Subscriptions: Керування підписками
Playlist insufficient data: Недостатньо даних для добірки "{playlist}", пропуск
елемента
All playlists has been successfully exported: Усі добірки успішно експортовано
Playlist insufficient data: Недостатньо даних для списку відтворення "{playlist}",
пропуск елемента
All playlists has been successfully exported: Усі списки відтворення успішно експортовано
Import Playlists: Імпорт добірок
Export Playlists: Експорт добірок
All playlists has been successfully imported: Усі добірки успішно імпортовано
Playlist File: Файл добірки
All playlists has been successfully imported: Усі списки відтворення успішно імпортовано
Playlist File: Файл списку відтворення
Subscription File: Файл підписки
History File: Файл історії
Advanced Settings: {}
@ -590,7 +611,7 @@ Channel:
Oldest: 'Найдавніші'
Most Popular: 'Найпопулярніші'
Playlists:
Playlists: 'Добірки'
Playlists: 'Списки відтворення'
This channel does not currently have any playlists: 'Цей канал наразі не має добірок'
Sort Types:
Last Video Added: 'Останнє додане відео'
@ -646,9 +667,9 @@ Video:
Open Channel in Invidious: 'Відкрити канал у Invidious'
Copy Invidious Channel Link: 'Копіювати посилання на канал Invidious'
Views: 'Перегляди'
Loop Playlist: 'Зациклити добірку'
Shuffle Playlist: 'Перемішати добірку'
Reverse Playlist: 'Змінити напрямок добірки'
Loop Playlist: 'Повторювати список відтворення'
Shuffle Playlist: 'Перемішати список відтворення'
Reverse Playlist: 'Зворотний напрямок списку відтворення'
Play Next Video: 'Відтворити наступне відео'
Play Previous Video: 'Відтворити попереднє відео'
Watched: 'Переглянуто'
@ -739,7 +760,7 @@ Video:
starting video at offset: запуск відео зі зміщенням
UnsupportedActionTemplate: '{externalPlayer} не підтримує: {action}'
OpeningTemplate: Відкриття {videoOrPlaylist} у {externalPlayer}...
playlist: добірка
playlist: список відтворення
video: відео
OpenInTemplate: Відкрити у {externalPlayer}
Premieres on: Прем'єри
@ -782,7 +803,7 @@ Videos:
#& Playlists
Playlist:
#& About
View Full Playlist: 'Переглянути всю добірку'
View Full Playlist: 'Переглянути весь список відтворення'
Videos: 'Відео'
View: 'Перегляд'
Views: 'Переглядів'
@ -791,7 +812,7 @@ Playlist:
# On Video Watch Page
#* Published
#& Views
Playlist: Добірка
Playlist: Список відтворення
Toggle Theatre Mode: 'Перемкнути режим театру'
Change Format:
Change Media Formats: 'Зміна форматів відео'
@ -804,7 +825,7 @@ Change Format:
відео'
Share:
Share Video: 'Поділитися відео'
Share Playlist: 'Поділитися добіркою'
Share Playlist: 'Поділитися списком відтворення'
Include Timestamp: 'Включити позначку часу'
Copy Link: 'Копіювати посилання'
Open Link: 'Відкрити посилання'
@ -915,8 +936,8 @@ Tooltips:
програвач можна знайти за допомогою змінної середовища PATH. Якщо потрібно,
тут можна призначити нетиповий шлях.
External Player: Якщо обрано зовнішній програвач, з'явиться піктограма для відкриття
відео (добірка, якщо підтримується) у зовнішньому програвачі, на мініатюрі.
Увага, налаштування Invidious не застосовуються до сторонніх програвачів.
відео (список відтворення, якщо підтримується) у зовнішньому програвачі, на
мініатюрі. Увага, налаштування Invidious не застосовуються до сторонніх програвачів.
DefaultCustomArgumentsTemplate: "(Типово: '{defaultCustomArguments}')"
Experimental Settings:
Replace HTTP Cache: Вимикає дисковий HTTP-кеш Electron і вмикає власний кеш зображень
@ -941,12 +962,12 @@ Loop is now disabled: 'Цикл вимкнено'
Loop is now enabled: 'Цикл увімкнено'
Shuffle is now disabled: 'Випадковий порядок вимкнено'
Shuffle is now enabled: 'Випадковий порядок увімкнено'
The playlist has been reversed: 'Добірку обернено'
The playlist has been reversed: 'Список відтворення обернено'
Playing Next Video: 'Відтворення наступного відео'
Playing Previous Video: 'Відтворення попереднього відео'
Canceled next video autoplay: 'Скасовано автовідтворення наступного відео'
'The playlist has ended. Enable loop to continue playing': 'Добірка завершилася. Увімкніть
цикл, щоб продовжити відтворення'
'The playlist has ended. Enable loop to continue playing': 'Список відтворення завершився.
Увімкніть повторення, щоб продовжити відтворення'
Yes: 'Так'
No: 'Ні'
@ -1001,10 +1022,11 @@ Ok: Гаразд
Hashtag:
Hashtag: Хештег
This hashtag does not currently have any videos: За цим хештегом наразі немає відео
Playlist will pause when current video is finished: Добірка призупиняється, коли поточне
відео завершено
Playlist will not pause when current video is finished: Добірка не призупиняється,
Playlist will pause when current video is finished: Список відтворення призупиняється,
коли поточне відео завершено
Playlist will not pause when current video is finished: Список відтворення не призупиняється,
коли поточне відео завершено
Channel Hidden: '{channel} додано до фільтра каналу'
Go to page: Перейти до {page}
Channel Unhidden: '{channel} вилучено з фільтра каналу'
Close Banner: Закрити банер

View File

@ -1,4 +1,4 @@
Locale Name: Tiếng Anh
Locale Name: Tiếng Việt
FreeTube: 'FreeTube'
# Currently on Subscriptions, Playlists, and History
'This part of the app is not ready yet. Come back later when progress has been made.': >-
@ -18,7 +18,7 @@ Select all: 'Chọn tất cả'
Reload: 'Tải lại'
Force Reload: 'Buộc tải lại'
Toggle Developer Tools: 'Chuyển đổi công cụ phát triển'
Actual size: 'Kích thước thực sự'
Actual size: 'Kích thước thực'
Zoom in: 'Phóng to'
Zoom out: 'Thu nhỏ'
Toggle fullscreen: 'Chuyển đổi toàn màn hình'
@ -36,6 +36,15 @@ Global:
# Search Bar
Live: Trực tiếp
Shorts: Shorts
Community: Cộng đồng
Counts:
Subscriber Count: 1 người đăng ký | {count} người đăng ký
View Count: 1 lượt xem | {count} lượt xem
Channel Count: 1 kênh | {count} kênh
Video Count: 1 video | {count} video
Watching Count: 1 lượt xem | {count} lượt xem
Input Tags:
Length Requirement: 1 đang xem | {count} đang xem
Search / Go to URL: 'Tìm kiếm / Đi đến URL'
# In Filter Button
Search Filters:
@ -45,7 +54,7 @@ Search Filters:
Most Relevant: 'Liên quan nhất'
Rating: 'Đánh giá'
Upload Date: 'Ngày tải lên'
View Count: 'Lượng xem'
View Count: 'Lượt xem'
Time:
Time: 'Thời gian'
Any Time: 'Mọi lúc'
@ -62,11 +71,12 @@ Search Filters:
#& Playlists
Movies: Phim ảnh
Duration:
Duration: 'Thời hạn'
All Durations: 'Tất cả thời hạn'
Duration: 'Thời lượng'
All Durations: 'Tất cả thời lượng'
Short (< 4 minutes): 'Ngắn (<4 phút)'
Long (> 20 minutes): 'Dài (> 20 phút)'
# On Search Page
Medium (4 - 20 minutes): Vừa (4 - 20 phút)
Search Results: 'Kết quả tìm kiếm'
Fetching results. Please wait: 'Đang lấy kết quả. Xin hãy chờ'
Fetch more results: 'Lấy thêm kết quả'
@ -80,18 +90,26 @@ Subscriptions:
sách Đăng ký đang trống. Bắt đầu thêm đăng ký để xem chúng tại đây.'
'Getting Subscriptions. Please wait.': 'Đang lấy Đăng ký. Vui lòng đợi.'
'Getting Subscriptions. Please wait.': Đang lấy Đăng ký. Vui lòng chờ.
Load More Videos: Load thêm video
Refresh Subscriptions: Refresh đăng ký
This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting: Kênh
này có nhiều người đăng ký. Buộc RSS để tránh bị giới hạn
Error Channels: Các kênh lỗi
Load More Videos: Tải thêm video
Refresh Subscriptions: Tải lại đăng ký
This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting: Hồ
sơ này có nhiều đăng ký.  Đang buộc RSS để tránh bị giới hạn
Error Channels: Các kênh có lỗi
Subscriptions Tabs: Trang đăng ký
Disabled Automatic Fetching: Bạn đã vô hiệu hóa tự động tải đăng ký. Hãy tải lại
danh sách đăng ký để xem chúng tại đây.
All Subscription Tabs Hidden: Tất cả các trang đăng ký đăng bị ẩn. Để xem nội dung
ở đây, vui lòng bỏ ẩn một số trang ở trong mục "{subsection}" của "{settingsSection}".
Load More Posts: Tải thêm bài đăng
Empty Channels: Các kênh bạn đăng ký hiện chưa đăng video nào.
Empty Posts: Các kênh bạn đăng ký hiện chưa đăng bài đăng nào.
Trending:
Trending: 'Xu hướng'
Movies: Phim
Music: Âm nhạc
Default: Mặc định
Gaming: Trò chơi
Trending Tabs: Tab Xu hướng
Trending Tabs: Trang Xu hướng
Most Popular: 'Phổ biến nhất'
Playlists: 'Danh sách phát'
User Playlists:
@ -104,12 +122,103 @@ User Playlists:
Playlist Message: Trang này không liệt kê tất cả danh sách video bạn đã theo giỏi.
Nó chỉ hiển thị các video mà bạn đã lưu hoặc thêm vào mục yêu thích. Khi xong
việc, tất cả các video trên trang này sẽ được chuyển vào danh sách 'yêu thích'.
Remove from Playlist: Xóa khỏi danh sách phát
Playlist Name: Tên danh sách phát
Save Changes: Lưu thay đổi
Remove Watched Videos: Xóa video đã xem
Sort By:
LatestUpdatedFirst: Được cập nhật gần đây
LatestPlayedFirst: Được phát gần đây
Sort By: Sắp xếp theo
EarliestUpdatedFirst: Được cập nhật sớm nhất
NameAscending: A-Z
NameDescending: Z-A
LatestCreatedFirst: Được tạo gần đây
EarliestCreatedFirst: Được tạo sớm nhất
EarliestPlayedFirst: Được phát sớm nhất
SinglePlaylistView:
Toast:
Video has been removed: Video đã được xóa
There was an issue with updating this playlist.: Đã có vấn đề xảy ra trong khi
cập nhập danh sách phát.
Reverted to use {oldPlaylistName} for quick bookmark: Đã quay lại dùng {oldPlaylistName}
cho dấu trang nhanh
Quick bookmark disabled: Đã tắt dấu trang nhanh
Some videos in the playlist are not loaded yet. Click here to copy anyway.: Một
số video trong danh sách phát này chưa được tải, bạn vẫn có thể nhấn vào đây
để sao chép.
This playlist is protected and cannot be removed.: Danh sách phát này được bảo
vệ và không thể bị xóa.
Playlist {playlistName} has been deleted.: Danh sách phát {playlistName} đã
được xóa.
This playlist does not exist: Danh sách phát này không tồn tại
This video cannot be moved up.: Video này không thể được chuyển lên.
This video cannot be moved down.: Video này không thể được chuyển xuống.
There was a problem with removing this video: Đã có vấn để xảy ra trong khi
xóa video này
This playlist is now used for quick bookmark: Danh sách này giờ đang được dùng
để tạo dấu trang nhanh
This playlist is now used for quick bookmark instead of {oldPlaylistName}. Click here to undo: Danh
sách này giờ đang được dùng để tạo dấu trang nhanh thay vì {oldPlaylistName}.
Nhấn đây để hoàn tác
Playlist name cannot be empty. Please input a name.: Tên danh sách phát không
thể để trống. Vui lòng nhập một tên.
Playlist has been updated.: Danh sách phát đã được cập nhật.
"{videoCount} video(s) have been removed": 1 video đã được xóa | {videoCount}
video đã được xóa
There were no videos to remove.: Không có video nào để xóa.
You have no playlists. Click on the create new playlist button to create a new one.: Bạn
đang không có danh sách phát nào. Nhấn vào nút tạo danh sách phát mới để tạo một
cái mới.
This playlist currently has no videos.: Danh sách phát này hiện không có video nào.
Edit Playlist Info: Chỉnh sửa thông tin danh sách phát
Are you sure you want to delete this playlist? This cannot be undone: Bạn có chắc
muốn xóa danh sách phát này?Việc này không thể hoàn tác.
CreatePlaylistPrompt:
Create: Tạo mới
Toast:
Playlist {playlistName} has been successfully created.: Danh sách phát {playlistName}
đã được tạo thành công.
There was an issue with creating the playlist.: Đã có vấn đề xảy ra trong khi
tạo danh sách phát này.
There is already a playlist with this name. Please pick a different name.: Hiện
đã có danh sách phát với tên này rồi. Vui lòng chọn một cái tên khác.
New Playlist Name: Tên danh sách phát mới
AddVideoPrompt:
Select a playlist to add your N videos to: Chọn một danh sách phát để lưu video
này vào | Chọn một danh sách phát để lưu {videoCount} video này vào
N playlists selected: Đã chọn {playlistCount}
Search in Playlists: Tìm trong danh sách phát
Toast:
"{videoCount} video(s) added to {playlistCount} playlists": Đã thêm 1 video
vào {playlistCount} danh sách phát | Đã thêm {videoCount} video vào {playlistCount}
danh sách phát
You haven't selected any playlist yet.: Bạn đang chưa chọn danh sách phát nào.
"{videoCount} video(s) added to 1 playlist": Đã thêm 1 video vào 1 danh sách
phát | Đã thêm {videoCount} vào 1 danh sách phát
Save: Lưu
Create New Playlist: Tạo danh sách phat mới
Playlist Description: Mô tả danh sách phát
Copy Playlist: Sao chép danh sách phát
Enable Quick Bookmark With This Playlist: Bật dấu trang nhanh với danh sách phát
này
Add to Playlist: Thêm vào danh sách phát
Cancel: Hủy bỏ
Add to Favorites: Thêm vào {playlistName}
Remove from Favorites: Xóa khỏi {playlistName}
Move Video Down: Chuyển video xuống
Move Video Up: Chuyển video lên
Disable Quick Bookmark: Tắt dấu trang nhanh
Delete Playlist: Xóa danh sách phát
Are you sure you want to remove all watched videos from this playlist? This cannot be undone: Bạn
có chắc muốn xóa tất cả các video đã xem khỏi danh sách phát này? Việc này không
thể được hoàn tác.
History:
# On History Page
History: 'Lịch sử'
Watch History: 'Lịch sử xem'
Your history list is currently empty.: Lịch sử của bạn hiện đang trống.
Search bar placeholder: Tìm kiếm trong Lịch sử
Search bar placeholder: Tìm trong lịch sử
Empty Search Message: Không có video nào trong lịch sử của bạn trùng với những gì
bạn đang tìm kiếm
Settings:
@ -123,8 +232,8 @@ Settings:
Default Landing Page: 'Trang mặc định'
Locale Preference: 'Ngôn ngữ'
Preferred API Backend:
Preferred API Backend: 'Backend API'
Local API: 'Local API'
Preferred API Backend: 'API Backend'
Local API: 'API địa phương'
Invidious API: 'API Invidious'
Video View Type:
Video View Type: 'Kiểu xem video'
@ -133,16 +242,18 @@ Settings:
Thumbnail Preference:
Thumbnail Preference: 'Thumbnail'
Default: 'Mặc định'
Beginning: 'Lúc đầu'
Middle: 'Chính giữa'
End: 'Đầu cuối'
Beginning: 'Đầu'
Middle: 'Giữa'
End: 'Cuối'
Blur: Làm mờ
Hidden: Ẩn
'Invidious Instance (Default is https://invidious.snopyta.org)': 'Phiên bản Invidious
(Mặc định là https://invidious.snopyta.org)'
Region for Trending: 'Phổ biến theo quốc gia'
#! List countries
Check for Latest Blog Posts: Check Blog post mới nhất
Check for Latest Blog Posts: Kiểm tra bài đăng mới nhất
Check for Updates: Kiểm tra cập nhật
Current Invidious Instance: Phiên bản invidious hiện tại
Current Invidious Instance: Phiên bản Invidious hiện tại
The currently set default instance is {instance}: Phiên bản Invidious mặc định
hiện được đặt thành {instance}
No default instance has been set: Không có phiên bản mặc định nào được đặt
@ -156,7 +267,7 @@ Settings:
No Action: Không hành động
External Link Handling: Xử lý liên kết bên ngoài
View all Invidious instance information: Xem tất cả thông tin phiên bản Invidious
System Default: Mặc định Hệ thống
System Default: Mặc định hệ thống
Theme Settings:
Theme Settings: 'Cài đặt chủ đề'
Match Top Bar with Main Color: 'Khớp thanh trên cùng với màu chính'
@ -165,34 +276,37 @@ Settings:
Black: 'Đen'
Dark: 'Tối'
Light: 'Sáng'
Dracula: 'Ma cà rồng'
Dracula: 'Dracula'
System Default: Mặc định hệ thống
Catppuccin Mocha: Catppuccin Mocha - Màu cà phê
Catppuccin Mocha: Catppuccin Mocha
Nordic: Nordic
Pastel Pink: Hồng phấn tiên
Hot Pink: Hồng nóng bỏng
Main Color Theme:
Main Color Theme: 'Màu chủ đề chính'
Red: 'Đỏ'
Pink: 'Hồng'
Purple: 'Tím'
Deep Purple: 'Tím Đậm'
Indigo: 'Xanh Đậm'
Blue: 'Xanh'
Light Blue: 'Xanh Nhạt'
Cyan: 'Lục Lam'
Deep Purple: 'Tím đậm'
Indigo: 'Chàm'
Blue: 'Xanh lam'
Light Blue: 'Xanh nước biển'
Cyan: 'Xanh lơ'
Teal: 'Xanh mòng két'
Green: 'Xanh '
Light Green: 'Xanh Lợt'
Lime: 'Vôi'
Green: 'Xanh lục'
Light Green: 'Xanh lợt'
Lime: 'Vàng chanh'
Yellow: 'Vàng'
Amber: 'Hổ Phách'
Amber: 'Hổ phách'
Orange: 'Cam'
Deep Orange: 'Cam Đậm'
Dracula Cyan: 'Ma cà rồng Lục Lam'
Dracula Green: 'Ma cà rồng Xanh Lá'
Dracula Orange: 'Ma cà rồng Cam'
Dracula Pink: 'Ma cà rồng Hồng'
Dracula Purple: 'Ma cà rồng Tím'
Dracula Red: 'Ma cà rồng Đỏ'
Dracula Yellow: 'Ma cà rồng Vàng'
Deep Orange: 'Cam đậm'
Dracula Cyan: 'Xanh lơ Dracula'
Dracula Green: 'Xanh lục Dracula'
Dracula Orange: 'Cam Dracula'
Dracula Pink: 'Hồng Dracula'
Dracula Purple: 'Tím Dracula'
Dracula Red: 'Đỏ Dracula'
Dracula Yellow: 'Vàng Dracula'
Catppuccin Mocha Rosewater: Catppuccin Mocha Rosewater Màu hoa hồng
Catppuccin Mocha Flamingo: Catppuccin Mocha Flamingo Màu hồng hạc
Catppuccin Mocha Pink: Catppuccin Mocha Pink Màu hồng hạc
@ -207,13 +321,13 @@ Settings:
Catppuccin Mocha Sapphire: Catppuccin Mocha Sapphire màu xanh
Catppuccin Mocha Lavender: Catppuccin Mocha Lavender Màu tím nhạt
Catppuccin Mocha Blue: Catppuccin Mocha Blue Màu xanh
Secondary Color Theme: 'Màu chủ đề thứ hai'
Secondary Color Theme: 'Màu chủ đề phụ'
#* Main Color Theme
UI Scale: Tỉ lệ UI
Disable Smooth Scrolling: Tắt cuộn mượt
Expand Side Bar by Default: Mở rộng thanh bên theo mặc định
Hide Side Bar Labels: Ẩn Nhãn Thanh Bên
Hide FreeTube Header Logo: Ẩn Logo FreeTube trên thanh trên
Hide Side Bar Labels: Ẩn nhãn thanh bên
Hide FreeTube Header Logo: Ẩn logo FreeTube trên thanh trên
Player Settings:
Player Settings: 'Cài đặt trình phát'
Force Local Backend for Legacy Formats: 'Bắt buộc Local Backend cho định dạng
@ -221,14 +335,14 @@ Settings:
Remember History: 'Nhớ lịch sử'
Play Next Video: 'Phát video tiếp theo'
Turn on Subtitles by Default: 'Bật phụ đề theo mặc định'
Autoplay Videos: 'Tự phát videos'
Autoplay Videos: 'Tự phát video'
Proxy Videos Through Invidious: 'Proxy video qua Invidious'
Autoplay Playlists: 'Danh sách tự động phát'
Enable Theatre Mode by Default: 'Bật chế độ rạp hát theo mặc định'
Default Volume: 'Âm lượng mặc định'
Default Playback Rate: 'Tốc độ phát mặc định'
Default Video Format:
Default Video Format: 'Định dạng video theo mặc định'
Default Video Format: 'Định dạng video mặc định'
Dash Formats: 'Định dạng DASH'
Legacy Formats: 'Định dạng Legacy'
Audio Formats: 'Định dạng âm thanh'
@ -244,21 +358,21 @@ Settings:
1440p: '1440p'
4k: '4k'
8k: '8k'
Scroll Playback Rate Over Video Player: Tốc độ Phát lại Cuộn qua Trình phát Video
Scroll Volume Over Video Player: Cuộn Âm lượng qua Trình phát Video
Scroll Playback Rate Over Video Player: Con lăn chuột điểu chỉnh tốc độ phát lại
Scroll Volume Over Video Player: Con lăn chuột điểu chỉnh âm lượng
Display Play Button In Video Player: Hiển thị nút phát trong trình phát video
Next Video Interval: Khoảng thời gian Video Tiếp theo
Fast-Forward / Rewind Interval: Khoảng thời gian tua đi / tua lại
Screenshot:
Enable: Bật chức năng Chụp màn hình
Enable: Bật chức năng chụp màn hình
Format Label: Định dạng chụp màn hình
Quality Label: Chất lượng chụp màn hình
File Name Label: Kiểu tên tệp
Folder Label: Chụp màn hình thư mục
File Name Label: Mẫu tên tệp
Folder Label: Thư mục ảnh chụp màn hình
Ask Path: Yêu cầu thứ mục lưu
Folder Button: Chọn thư mục
Error:
Empty File Name: Tên tệp. trống
Empty File Name: Tên tệp trống
Forbidden Characters: Các ký từ bị cấm
File Name Tooltip: Bạn có thể dùng các biến số dưới đây. %Y Năm 4 chữ số. %M
Tháng 2 chữ số. %D Ngày 2 chữ số. %H Giờ 2 chữ số. %N Phút 2 chữ số. %S Giây
@ -266,8 +380,12 @@ Settings:
3 chữ số. %i Video ID. Bạn cũng có thể dùng dấu "\" hoặc "/" để tạo các thư
mục con.
Max Video Playback Rate: Tốc độ phát lại tối đa
Video Playback Rate Interval: khoảng cách phát lại video
Video Playback Rate Interval: Khoảng cách tốc độ phát
Enter Fullscreen on Display Rotate: Bật toàn màn hình khi xoay
Comment Auto Load:
Comment Auto Load: Tự động tải bình luận
Skip by Scrolling Over Video Player: Tua video bằng con lăn chuột
Allow DASH AV1 formats: Cho phép định dạng DASH AV1
Subscription Settings:
Subscription Settings: 'Cài đặt đăng ký'
Hide Videos on Watch: 'Ẩn video khi đã xem'
@ -280,8 +398,9 @@ Settings:
Import Subscriptions: 'Nhập đăng ký'
Export Subscriptions: 'Xuất đăng ký'
How do I import my subscriptions?: 'Làm sao để nhập đăng ký của tôi?'
Fetch Feeds from RSS: Lấy feeds từ RSS
Fetch Feeds from RSS: Cập nhật bảng tin qua RSS
Fetch Automatically: Tự động làm mới bảng tin
Only Show Latest Video for Each Channel: Chỉ hiện video mới nhất cho mỗi kênh
Advanced Settings:
Advanced Settings: 'Cài đặt nâng cao'
Enable Debug Mode (Prints data to the console): 'Bật chế độ Debug (Ghi data ra
@ -310,27 +429,27 @@ Settings:
Data Settings:
How do I import my subscriptions?: Làm sao để tôi nhập đăng ký?
Unknown data key: Key data không xác định
Unable to write file: Không thể viết file
Unable to read file: Không thể đọc file
Unknown data key: Key dữ liệu không xác định
Unable to write file: Không thể viết tệp
Unable to read file: Không thể đọc tệp
All watched history has been successfully exported: Tất cả lịch sử xem đã được
xuất ra thành công
All watched history has been successfully imported: Tất cả lịch sử xem đã được
nhập vào thành công
History object has insufficient data, skipping item: Lịch sử object không đủ dữ
liệu, bỏ qua
History object has insufficient data, skipping item: Lịch sử không đủ dữ liệu,
đang bỏ qua mục này
Subscriptions have been successfully exported: Đăng ký đã được xuất thành công
Invalid history file: File lịch sử không hợp lệ
Invalid history file: Tệp lịch sử không hợp lệ
This might take a while, please wait: Điều này có thể tốn thời gian, xin hãy chờ
Invalid subscriptions file: File đăng ký không hợp lệ
One or more subscriptions were unable to be imported: Một hay hơn đăng ký không
Invalid subscriptions file: Tệp đăng ký không hợp lệ
One or more subscriptions were unable to be imported: Một hay nhiều đăng ký không
thể nhập
All subscriptions have been successfully imported: Tất cả đăng ký đã được nhập
vào thành công
All subscriptions and profiles have been successfully imported: Tất cả đăng
và profiles đã được nhập vào thành công
Profile object has insufficient data, skipping item: Profile object không đủ dữ
liệu, bỏ qua
All subscriptions and profiles have been successfully imported: Tất cả các đăng
ký và hồ sơ đã được nhập thành công
Profile object has insufficient data, skipping item: Hồ sơ không đủ dữ liệu, đang
bỏ qua mục này
Export History: Xuất lịch sử
Import History: Nhập lịch sử
Export NewPipe: Xuất NewPipe
@ -344,9 +463,9 @@ Settings:
Import Subscriptions: Nhập đăng ký
Select Export Type: Chọn kiểu xuất ra
Select Import Type: Chọn kiểu nhập vào
Data Settings: Dữ liệu
Data Settings: Cài đặt dữ liệu
Manage Subscriptions: Quản lý đăng ký
Import Playlists: Thêm danh sách phát
Import Playlists: Nhập danh sách phát
All playlists has been successfully imported: Tất cả các danh sách phát đã được
thêm vào thành công
All playlists has been successfully exported: Tất cả các danh sách phát đã được
@ -354,36 +473,64 @@ Settings:
Playlist insufficient data: Dữ liệu bị thiếu cho danh sách phát "{playlist}",
bỏ qua mục này
Export Playlists: Xuất danh sách phát
History File: Tệp Lịch sử
Playlist File: Tệp Danh sách phát
History File: Tệp lịch sử
Playlist File: Tệp danh sách phát
Subscription File: Tệp đăng ký
Export Playlists For Older FreeTube Versions:
Label: Xuất danh sách phát cho các phiên bản FreeTube cũ hơn
Tooltip: "Tùy chọn này sẽ gộp tất cả các danh sách phát vào một danh sách phát
mang tên 'Ưa thích'.\nCách để nhập & xuất video trong danh sách phát cho một
phiên bản cũ hơn của FreeTube:\n1. Xuất danh sách phát với tùy chọn này được
bật.\n2. Xóa tất cả các danh sách phát qua tùy chọn Xóa tất cả danh sách phát
trong mục Cài đặt quyền riêng tư.\n3. Mở phiên bản cũ hơn và nhập danh sách
phát đã được xuất ra."
Distraction Free Settings:
Hide Live Chat: Giấu live chat
Hide Popular Videos: Giấu video phổ biến
Hide Trending Videos: Giấu video xu hướng
Hide Recommended Videos: Giấu video nên xem
Hide Comment Likes: Giấu bình luận like
Hide Channel Subscribers: Giấu số người đăng ký
Hide Video Likes And Dislikes: Giấu thích và không thích
Hide Video Views: Giấu lượt xem
Distraction Free Settings: Chế độ không phân tâm
Hide Active Subscriptions: Ẩn Đăng ký Hiện hoạt
Hide Live Chat: Ẩn live chat
Hide Popular Videos: Ẩn video phổ biến
Hide Trending Videos: Ẩn video xu hướng
Hide Recommended Videos: Ẩn video tiếp theo
Hide Comment Likes: Ẩn lượt thích bình luận
Hide Channel Subscribers: Ẩn số người đăng ký
Hide Video Likes And Dislikes: Ẩn lượt thích và không thích
Hide Video Views: Ẩn lượt xem
Distraction Free Settings: Cài đặt không phân tâm
Hide Active Subscriptions: Ẩn đăng ký hiện hoạt
Hide Playlists: Ẩn danh sách phát
Hide Comments: Ẩn bình luận
Hide Live Streams: Ẩn phát trực tiếp
Hide Video Description: Ẩn mổ tả video
Hide Sharing Actions: Ẩn hoạt động chia sẻ
Hide Sharing Actions: Ẩn nút chia sẻ
Sections:
General: Chung
Side Bar: Thanh bên
Channel Page: Trang kênh
Subscriptions Page: Trang đăng ký
Watch Page: Trang trình chiếu video
Hide Channel Playlists: Ẩn danh sách phát của kênh
Hide Featured Channels: Ẩn các kênh nổi bật
Hide Channels Placeholder: Tên kênh hoặc ID
Hide Profile Pictures in Comments: Ẩn ảnh đại diện trong Bình luận
Hide Channels Placeholder: ID kênh
Hide Profile Pictures in Comments: Ẩn ảnh đại diện trong bình luận
Hide Chapters: Ẩn các chương
Hide Channels: Ẩn các videos khỏi kênh
Hide Channels: Ẩn các video khỏi kênh
Hide Channel Releases: Ẩn nhạc của kênh
Hide Videos and Playlists Containing Text: Ẩn các video và danh sách phát có chứa
Hide Channels Invalid: ID kênh không hợp lệ
Hide Channel Community: Ẩn cộng đồng của kênh
Hide Channel Shorts: Ẩn short của kênh
Hide Subscriptions Shorts: Ẩn short từ các đăng ký
Hide Subscriptions Live: Ẩn video phát trực tiếp từ các đăng ký
Hide Upcoming Premieres: Ẩn video sắp ra mắt
Hide Channel Podcasts: Ẩn podcast của kênh
Hide Subscriptions Community: Ẩn bài đăng cộng đồng từ các đăng ký
Hide Channels Already Exists: ID kênh đã tồn tại
Hide Videos and Playlists Containing Text Placeholder: Từ, tiếng, hoặc cụm từ
Hide Subscriptions Videos: Ẩn video từ các đăng ký
Display Titles Without Excessive Capitalisation: Hiển thị tiêu đề không viết hoa
quá mức
Privacy Settings:
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: Bạn
có muốn xóa toàn bộ đăng ký và profiles không? Điều này không thể phục hồi.
Remove All Subscriptions / Profiles: Xóa bỏ tất cả đăng ký / Profiles
có muốn xóa toàn bộ đăng ký và hồ sơ không? Điều này không thể phục hồi.
Remove All Subscriptions / Profiles: Xóa tất cả đăng ký / hồ sơ
Watch history has been cleared: Lịch sử xem đã được xóa
Are you sure you want to remove your entire watch history?: Bạn có thực sự muốn
xóa toàn bộ lịch sử xem?
@ -391,58 +538,70 @@ Settings:
Search cache has been cleared: Bộ đệm của tìm kiếm đã xóa
Are you sure you want to clear out your search cache?: Bạn có chắc là muốn xóa
bộ đệm của tìm kiếm?
Clear Search Cache: Xóa Tìm kiếm cache
Clear Search Cache: Xóa cache tìm kiếm
Save Watched Progress: Lưu quá trình xem
Remember History: Nhớ lịch sử
Privacy Settings: Thiết lập quyền riêng tư
Automatically Remove Video Meta Files: Tự động xúa các tệp meta video
The app needs to restart for changes to take effect. Restart and apply change?: App
cần khởi động lại để chỉnh sửa có hiệu nghiệm. Khởi động lại và áp đặt?
Privacy Settings: Cài đặt quyền riêng tư
Automatically Remove Video Meta Files: Tự động xóa các tệp meta video
Remove All Playlists: Xóa tất cả danh sách phát
Are you sure you want to remove all your playlists?: Bạn có chắc muốn xóa tất
cả các danh sách phát không?
All playlists have been removed: Đã xóa tất cả các danh sách phát
Save Watched Videos With Last Viewed Playlist: Lưu video đã xem với danh sách
phát được xem lần cuối
The app needs to restart for changes to take effect. Restart and apply change?: Ứng
dụng cần được khởi động lại để chỉnh sửa có hiệu nghiệm. Khởi động lại và áp dụng
cài đặt?
Proxy Settings:
Proxy Host: Máy chủ Proxy
Proxy Host: Máy chủ proxy
Region: Vùng
Country: Quốc gia
Proxy Settings: Cài đặt proxy
Enable Tor / Proxy: Bật Tor / Proxy
Proxy Protocol: Giao thức proxy
Proxy Port Number: Số Cổng Proxy
Proxy Port Number: Số cổng proxy
City: Thành phố
Ip: Ip
Ip: IP
Your Info: Thông tin của bạn
Error getting network information. Is your proxy configured properly?: Lỗi nhận
thông tin mạng. Proxy của bạn đã được cài đặc đúng cách chưa?
Clicking on Test Proxy will send a request to: Nhấn vào Proxy thử nghiệm sẽ gửi
yêu cầu đến
Test Proxy: Proxy thử nghiệm
Clicking on Test Proxy will send a request to: Nhấn vào Thử proxy sẽ gửi yêu cầu
đến
Test Proxy: Thử proxy
SponsorBlock Settings:
Enable SponsorBlock: Bật SponsorBlock
'SponsorBlock API Url (Default is https://sponsor.ajay.app)': SponsorBlock API
Url (Mặc định là https://sponsor.ajay.app)
'SponsorBlock API Url (Default is https://sponsor.ajay.app)': URL API SponsorBlock
(Mặc định là https://sponsor.ajay.app)
Skip Options:
Skip Option: Tuỳ chọn lượt bỏ
Show In Seek Bar: Hiển thị trong thanh tìm kiếm
Auto Skip: Tự động lượt bỏ
Prompt To Skip: Nhắc nhở lượt bỏ
Do Nothing: Không làm gì hết
Skip Option: Tuỳ chọn bỏ qua
Show In Seek Bar: Hiển thị trong thanh tiến trình
Auto Skip: Tự động bỏ qua
Prompt To Skip: Nhắc nhở bỏ qua
Do Nothing: Không làm gì
Notify when sponsor segment is skipped: Thông báo khi đoạn quảng cáo bị bỏ qua
Category Color: Bản màu
SponsorBlock Settings: Cài đặt SponsorBlock
'DeArrow Thumbnail Generator API Url (Default is https://dearrow-thumb.ajay.app)': URL
API ảnh xem trước DeArrow (Mặc định là https://dearrow-thumb.ajay.app)
UseDeArrowTitles: Dùng tiêu đề video từ DeArrow
UseDeArrowThumbnails: Dùng ảnh xem trước từ DeArrow
External Player Settings:
External Player Settings: Cài đặt trình phát video bên ngoài
External Player: Trình phát video bên ngoài
Custom External Player Arguments: Chứng minh trình phát bên ngoài tùy chỉnh
Custom External Player Arguments: Tham số trình phát bên ngoài tùy chỉnh
Ignore Unsupported Action Warnings: Bỏ qua các cảnh bảo tác vụ không được hổ trợ
Custom External Player Executable: Chạy trình phát bên ngoài tuỳ chỉnh
Players:
None:
Name: Trống
Ignore Default Arguments: Bỏ qua tham số mặc định
Parental Control Settings:
Parental Control Settings: Cài đặt trình kiểm soát của phụ huynh
Hide Unsubscribe Button: Ẩn Nút Huỷ Đăng Ký
Show Family Friendly Only: Chỉ hiển thị những nội dung gia đình và thân thiện
Parental Control Settings: Cài đặt kiểm soát của phụ huynh
Hide Unsubscribe Button: Ẩn nút huỷ đăng ký
Show Family Friendly Only: Chỉ hiển thị nội dung thân thiện với gia đình
Hide Search Bar: Ẩn thanh tìm kiếm
Download Settings:
Download Behavior: Thói quen tải xuống
Download Behavior: Hành vi tải xuống
Download in app: Tải xuống trong ứng dụng
Open in web browser: Mở trên trình duyệt
Ask Download Path: Yêu cầu đường dẫn tải xuống
@ -451,11 +610,11 @@ Settings:
Password Dialog:
Password: Mật khẩu
Password Incorrect: Mật khẩu sai
Enter Password To Unlock: Nhập mật khẩu để mở Cài đặt
Enter Password To Unlock: Nhập mật khẩu để mở cài đặt
Unlock: Mở khoá
Password Settings:
Set Password To Prevent Access: Đặt mặt khẩu để ngăn truy cập cài đặt
Password Settings: Cài đặt Mật khẩu
Password Settings: Cài đặt mật khẩu
Remove Password: Xoá mật khẩu
Set Password: Đặt mật khẩu
Experimental Settings:
@ -463,6 +622,8 @@ Settings:
cài đặt này có thể gây ra hoạt động bất ổn định. Hãy tạo phương án dự phòng
trước khi bật!
Experimental Settings: Cài đặt thử nghiệm
Replace HTTP Cache: Thay thế bộ đệm HTTP
Expand All Settings Sections: Mở rộng tất cả các mục tùy chọn
About:
#On About page
About: 'Giới thiệu'
@ -496,29 +657,29 @@ About:
#On Channel Page
Mastodon: Mastodon
Email: Thư điện tử
Email: Email
Source code: Mã nguồn
FAQ: Câu hỏi thường gặp
Report a problem: Báo cáo sự cố
Licensed under the AGPLv3: Được cấp phép theo AGPLv3
View License: Xem Giấy phép
View License: Xem giấy phép
Help: Trợ giúp
Translate: Phiên dịch
Website: Trang web
Blog: Blog
Credits: Tín dụng
Credits: Ghi công
Donate: Quyên tặng
GitHub issues: Sự cố GitHub
FreeTube Wiki: FreeTube Wiki
Beta: Thử nghiệm
Downloads / Changelog: Tải xuống / bản ghi changelog
GitHub releases: Phiên bản GitHub
Please check for duplicates before posting: Vui lòng kiểm tra các bản sao trước
khi đăng
Chat on Matrix: Trò chuyện trên Ma trận
Downloads / Changelog: Tải xuống / nhật ký thay đổi
GitHub releases: Tải xuống trên GitHub
Please check for duplicates before posting: Vui lòng kiểm tra trùng lặp trước khi
đăng
Chat on Matrix: Trò chuyện trên Matrix
room rules: quy định phòng chat
FreeTube is made possible by: FreeTube được tạo ra bởi
these people and projects: những người và dự án
these people and projects: những người và dự án này
Please read the: Hãy đọc
Discussions: Thảo luận
Channel:
@ -551,6 +712,7 @@ Channel:
Featured Channels: 'Kênh đặc sắc'
Tags:
Search for: Tìm kiếm cho "{tag}"
Tags: Thẻ
Joined: Đã tham gia
Location: Vị trí
Details: Chi tiết
@ -572,10 +734,21 @@ Channel:
votes: '{votes} bình chọn'
Reveal Answers: Hiện Câu trả lời
Hide Answers: Ẩn Câu trả lời
Video hidden by FreeTube: FreeTube đã ẩn video này
Releases:
Releases: Xuất bản
This channel does not currently have any releases: Kênh này hiện không có bất
kỳ bản phát hành nào
This channel is age-restricted and currently cannot be viewed in FreeTube.: Kênh
này là kênh giới hạn độ tuổi và hiện tại không thể xem được trên FreeTube.
Channel Tabs: Trang kênh
Shorts:
This channel does not currently have any shorts: Kênh này hiện không có video
shorts
Podcasts:
Podcasts: Podcast
This channel does not currently have any podcasts: Kênh này hiện không có podcast
nào
Video:
Open in YouTube: 'Mở trong Youtube'
Copy YouTube Link: 'Sao chép liên kết Youtube'
@ -706,6 +879,13 @@ Video:
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Trò
chuyện trực tiếp hiện không khả dụng trên luồng phát này. Chủ sở hữu có thể đã
tắt chức năng này.
Hide Channel: Ẩn kênh này
More Options: Tùy chọn khác
Pause on Current Video: Tạm dừng trên video hiện tại
Show Super Chat Comment: Hiển thị bình luận Super Chat
Upcoming: Sắp ra mắt
Premieres: Công chiếu
Unhide Channel: Hiển thị kênh này
Videos:
#& Sort By
Sort By:
@ -750,6 +930,7 @@ Share:
Include Timestamp: Có kèm dấu thời gian
YouTube Channel URL copied to clipboard: Đã sao chép liên kết kênh Youtube
Invidious Channel URL copied to clipboard: URL của kênh ưu tiên đã được sao chép
Share Channel: Chia sẻ kênh
Mini Player: 'Trình phát Mini'
Comments:
Comments: 'Bình luận'
@ -776,6 +957,8 @@ Comments:
Pinned by: Được ghim bởi
Show More Replies: Hiện thêm câu trả lời
View {replyCount} replies: Hiển thị {replyCount} câu trả lời
Hearted: Thả Tim
Subscribed: Đã đăng ký
Up Next: 'Tiếp theo'
# Toast Messages
@ -824,24 +1007,30 @@ Profile:
chỉnh về {profile}'
Profile has been updated: Profile đã được cập nhật
Profile has been created: Profile đã được tạo
Your profile name cannot be empty: Tên Profile của bạn không được để trống
Profile could not be found: Probile không thể tìm thấy được
Your profile name cannot be empty: Tên hồ sơ của bạn không được để trống
Profile could not be found: Hồ sơ không thể tìm thấy được
All subscriptions will also be deleted.: Tất cả đăng ký đều sẽ bị xóa.
Are you sure you want to delete this profile?: Bạn có chắc là muốn xóa Profile này?
Delete Profile: Xóa Profile
Make Default Profile: Chọn làm Profile mặc định
Update Profile: Cập nhật Profile
Create Profile: Tạo Profile
Profile Preview: Xem trước Profile
Custom Color: Màu custom
Are you sure you want to delete this profile?: Bạn có chắc muốn xóa hồ sơ này?
Delete Profile: Xóa hồ sơ
Make Default Profile: Chọn làm hồ sơ mặc định
Update Profile: Cập nhật hồ sơ
Create Profile: Tạo hồ sơ
Profile Preview: Xem trước hồ sơ
Custom Color: Màu tùy chỉnh
Color Picker: Chọn màu
Edit Profile: Chỉnh sửa Profile
Create New Profile: Tạo Profile mới
Profile Manager: Quản lý Profile
Edit Profile: Chỉnh sửa hồ sơ
Create New Profile: Tạo hồ sơ mới
Profile Manager: Quản lý hồ sơ
All Channels: Tất cả kênh
Profile Select: Chọn Profile
Profile Select: Chọn hồ sơ
Profile Filter: Bộ lọc hồ sơ
Profile Settings: Cài đặt hồ sơ cá nhân
Profile Settings: Cài đặt hồ sơ
Toggle Profile List: Mở/đóng danh sách hồ sơ
Edit Profile Name: Chỉnh sửa tên hồ sơ
Profile Name: Tên hồ sơ
Open Profile Dropdown: Mở danh sách hồ sơ
Create Profile Name: Tạo tên hồ sơ
Close Profile Dropdown: Đóng danh sách hồ sơ
A new blog is now available, {blogTitle}. Click to view more: Một blog mới đã có,
{blogTitle}. Nhấn để xem chi tiết
Download From Site: Tải từ website
@ -853,15 +1042,15 @@ Search Bar:
More: Thêm
Are you sure you want to open this link?: Bạn có chắc là bạn muốn mở liên kết này
không?
New Window: Cửa Sổ Mới
New Window: Cửa sổ mới
Channels:
Channels: Kênh
Title: Danh sách kênh
Search bar placeholder: Tìm Kênh
Search bar placeholder: Tìm kênh
Empty: Danh sách kênh của bạn hiện đang trống.
Unsubscribed: '{channelName} đã bị xoá khỏi danh sách kênh đã đăng ký của bạn'
Unsubscribe Prompt: Bạn có chắc răng bạn muốn huỷ đăng ký kênh "{channelName}"?
Unsubscribe: Huỷ đăng ký kênh
Unsubscribe: Huỷ đăng ký
Count: '{number} kênh đã tìm được.'
Tooltips:
General Settings:
@ -896,6 +1085,12 @@ Tooltips:
lui để kiểm soát tốc độ phát. Nhấn và giữ phím Control (Phím Command trên Mac)
và nhấp chuột trái để lập tức trở về tốc độ phát mặc định (1x trừ khi nó đã
được thay đổi trong cài đặt).
Skip by Scrolling Over Video Player: Sử dụng con lăn chuột để bỏ qua video, kiểu
MPV.
Allow DASH AV1 formats: Định dạng DASH AV1 có thể trông đẹp hơn định dạng DASH
H.264. Định dạng DASH AV1 yêu cầu nhiều năng lượng hơn để phát lại! Chúng không
có sẵn trên tất cả các video, trong những trường hợp đó trình phát sẽ sử dụng
định dạng DASH H.264 thay thế.
External Player Settings:
Custom External Player Arguments: Bất kỳ tham số dòng lệnh tùy chỉnh nào, được
phân tách bằng dấu chấm phẩy (';'), bạn muốn được chuyển đến trình phát bên
@ -915,9 +1110,23 @@ Tooltips:
thức mặc định để lấy nguồn cấp dữ liệu đăng ký của bạn. RSS nhanh hơn và tránh
việc bị chặn IP, nhưng không cung cấp thông tin nhất định như thời lượng video
hoặc trạng thái phát trực tiếp
Fetch Automatically: Khi được bật, FreeTube sẽ tự động tìm nạp nguồn cấp dữ liệu
đăng ký của bạn khi cửa sổ mới được mở và khi chuyển đổi hồ sơ.
Privacy Settings:
Remove Video Meta Files: Khi được bật lên, FreeTube sẽ tự động xóa các tệp meta
được tạo trong quá trình phát lại video, khi trang xem bị đóng.
Distraction Free Settings:
Hide Channels: Nhập tên kênh hoặc ID kênh để ẩn tất cả video, danh sách phát và
chính kênh đó khỏi xuất hiện trong tìm kiếm, xu hướng, phổ biến nhất và được
đề xuất. Tên kênh đã nhập phải khớp hoàn toàn và có phân biệt chữ hoa chữ thường.
Hide Subscriptions Live: Cài đặt này bị ghi đè bởi cài đặt "{appWideSetting}"
trên toàn ứng dụng, trong phần "{subsection}" của "{settingsSection}"
SponsorBlock Settings:
UseDeArrowThumbnails: Thay thế thumbnail video bằng thumbnail gửi từ DeArrow.
UseDeArrowTitles: Thay thế tiêu đề video bằng tiêu đề do người dùng gửi từ DeArrow.
Experimental Settings:
Replace HTTP Cache: Tắt bộ nhớ đệm HTTP dựa trên đĩa của Electron và bật bộ nhớ
đệm hình ảnh trong bộ nhớ tùy chỉnh. Sẽ dẫn đến việc sử dụng RAM tăng lên.
Hashtags have not yet been implemented, try again later: Thẻ hashtag chưa thể dùng
được, hãy thử lại sau
Playing Next Video Interval: Phát video tiếp theo ngay lập tức. Nhấn vào để hủy. |
@ -938,3 +1147,30 @@ Starting download: Bắt đầu tải xuống "{videoTitle}"
Ok: Ok
Clipboard:
Copy failed: Sao chép vào bộ nhớ tạm thất bại
Cannot access clipboard without a secure connection: Không thể truy cập clipboard
nếu không có kết nối an toàn
Go to page: Đi đến {page}
Close Banner: Đóng thanh trên
Chapters:
'Chapters list visible, current chapter: {chapterName}': 'Danh sách các chương hiển
thị, chương hiện tại: {chapterName}'
Chapters: Chương
'Chapters list hidden, current chapter: {chapterName}': 'Danh sách các chương bị
ẩn, chương hiện tại: {chapterName}'
Channel Unhidden: '{channel} đã bị xóa khỏi bộ lọc kênh'
Tag already exists: Thẻ "{tagName}" đã tồn tại
Hashtag:
This hashtag does not currently have any videos: Hashtag này hiện không có bất kỳ
video nào
Hashtag: Hashtag
Age Restricted:
This channel is age restricted: Kênh này bị giới hạn độ tuổi
This video is age restricted: Video này bị giới hạn độ tuổi
Preferences: Tuỳ chỉnh
Playlist will not pause when current video is finished: Danh sách phát sẽ không tạm
dừng khi video hiện tại kết thúc
Channel Hidden: '{channel} đã thêm vào bộ lọc kênh'
Playlist will pause when current video is finished: Danh sách phát sẽ tạm dừng khi
video hiện tại kết thúc
Trimmed input must be at least N characters long: Dữ liệu nhập bị cắt bớt phải dài
ít nhất 1 ký tự | Dữ liệu nhập bị cắt bớt phải dài ít nhất {length} ký tự

View File

@ -128,6 +128,7 @@ User Playlists:
Select a playlist to add your N videos to: 选择一个播放列表来添加你的视频 | 选择一个播放列表来添加你的 {videoCount}
个视频
N playlists selected: 选中了 {playlistCount} 个播放列表
Added {count} Times: 添加了 {count} 次
SinglePlaylistView:
Toast:
There were no videos to remove.: 没有可删除的视频。

View File

@ -158,6 +158,7 @@ User Playlists:
{oldPlaylistName}。點擊此處撤銷
Reverted to use {oldPlaylistName} for quick bookmark: 恢復為使用 {oldPlaylistName}
進行快速書籤
Search for Videos: 搜尋影片
AddVideoPrompt:
Select a playlist to add your N videos to: 選擇要新增影片的播放清單 | 選擇播放清單以將您的 {videoCount}
部影片新增至其中
@ -170,6 +171,7 @@ User Playlists:
部影片新增至 1 個播放清單
"{videoCount} video(s) added to {playlistCount} playlists": 1 部影片新增至 {playlistCount}
個播放清單 | {videoCount} 部影片新增至 {playlistCount} 個播放清單
Added {count} Times: 新增了 {count} 次 | 新增了 {count} 次
CreatePlaylistPrompt:
New Playlist Name: 新播放清單名稱
Toast:
@ -245,7 +247,7 @@ Settings:
Black: '黑色'
Dark: '深色'
Light: '淺色'
Dracula: '德古拉'
Dracula: 'Dracula'
System Default: 系統預設值
Catppuccin Mocha: 卡布奇諾摩卡
Pastel Pink: 淡粉紅色
@ -815,6 +817,7 @@ Video:
Pause on Current Video: 暫停目前影片
Unhide Channel: 顯示頻道
Hide Channel: 隱藏頻道
More Options: 更多選項
Videos:
#& Sort By
Sort By:
@ -1046,3 +1049,7 @@ Channel Unhidden: '{channel} 已從頻道過濾條件移除'
Trimmed input must be at least N characters long: 修剪後的輸入必須至少有 1 個字元長 | 修剪後的輸入必須至少有
{length} 個字元長
Tag already exists: 「{tagName}」標籤已存在
Close Banner: 關閉橫幅
Age Restricted:
This channel is age restricted: 此頻道有年齡限制
This video is age restricted: 此影片有年齡限制

772
yarn.lock

File diff suppressed because it is too large Load Diff