Merge branch 'development' into piped-support

This commit is contained in:
ChunkyProgrammer 2023-07-26 11:58:46 -07:00
commit d9e208c555
86 changed files with 3845 additions and 1275 deletions

View File

@ -91,6 +91,7 @@ body:
- .exe
- Flathub
- MPR
- Nix
- .pacman
- Portable
- PortableApps

View File

@ -17,7 +17,7 @@ jobs:
"labels": ["B: visual"]
},
{
"keywords": ["AUR", "Chocolatey", "FreeTubeCordova", "PortableApps", "winget", "Scoop", "Snapcraft", "MPR"],
"keywords": ["AUR", "Chocolatey", "FreeTubeCordova", "PortableApps", "winget", "Scoop", "Snapcraft", "MPR", "Nix"],
"labels": ["B: Unofficial Download"]
},
{

View File

@ -91,6 +91,8 @@ These builds are maintained by the community. While they should be safe, downloa
* makedeb Package Repository (MPR): [Download](https://mpr.makedeb.org/packages/freetube-bin)
* Nix Packages: [Download](https://search.nixos.org/packages?query=freetube)
* PortableApps (Windows Only): [Download](https://github.com/rddim/FreeTubePortable/releases) and [Source Code](https://github.com/rddim/FreeTubePortable)
* Scoop (Windows Only): [Usage](https://github.com/ScoopInstaller/Scoop)

View File

@ -66,7 +66,7 @@
"nedb-promises": "^6.2.1",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"video.js": "7.21.4",
"video.js": "7.21.5",
"videojs-contrib-quality-levels": "^3.0.0",
"videojs-http-source-selector": "^1.1.6",
"videojs-mobile-ui": "^0.8.0",
@ -78,21 +78,21 @@
"vue-router": "^3.6.5",
"vue-tiny-slider": "^0.1.39",
"vuex": "^3.6.2",
"youtubei.js": "^5.2.0"
"youtubei.js": "^5.6.0"
},
"devDependencies": {
"@babel/core": "^7.22.5",
"@babel/eslint-parser": "^7.22.5",
"@babel/core": "^7.22.9",
"@babel/eslint-parser": "^7.22.9",
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@babel/preset-env": "^7.22.5",
"@babel/preset-env": "^7.22.9",
"@double-great/stylelint-a11y": "^2.0.2",
"babel-loader": "^9.1.2",
"babel-loader": "^9.1.3",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.8.1",
"css-minimizer-webpack-plugin": "^5.0.1",
"electron": "^22.3.15",
"electron": "^22.3.18",
"electron-builder": "^23.6.0",
"eslint": "^8.44.0",
"eslint": "^8.45.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.27.5",
@ -100,31 +100,31 @@
"eslint-plugin-n": "^16.0.1",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-unicorn": "^47.0.0",
"eslint-plugin-unicorn": "^48.0.0",
"eslint-plugin-vue": "^9.15.1",
"eslint-plugin-vuejs-accessibility": "^2.1.0",
"eslint-plugin-yml": "^1.8.0",
"html-webpack-plugin": "^5.5.3",
"js-yaml": "^4.1.0",
"json-minimizer-webpack-plugin": "^4.0.0",
"lefthook": "^1.4.3",
"lefthook": "^1.4.6",
"mini-css-extract-plugin": "^2.7.6",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.24",
"postcss": "^8.4.26",
"postcss-scss": "^4.0.6",
"prettier": "^2.8.8",
"rimraf": "^5.0.1",
"sass": "^1.63.6",
"sass": "^1.64.1",
"sass-loader": "^13.3.2",
"stylelint": "^14.16.1",
"stylelint-config-sass-guidelines": "^9.0.1",
"stylelint-config-standard": "^29.0.0",
"stylelint": "^15.10.2",
"stylelint-config-sass-guidelines": "^10.0.0",
"stylelint-config-standard": "^34.0.0",
"stylelint-high-performance-animation": "^1.8.0",
"tree-kill": "1.2.2",
"vue-devtools": "^5.1.4",
"vue-eslint-parser": "^9.3.1",
"vue-loader": "^15.10.0",
"webpack": "^5.88.1",
"webpack": "^5.88.2",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1",
"yaml-eslint-parser": "^1.2.2"

View File

@ -71,14 +71,36 @@ export default defineComponent({
hideChannelPlaylists: function() {
return this.$store.getters.getHideChannelPlaylists
},
hideChannelPodcasts: function() {
return this.$store.getters.getHideChannelPodcasts
},
hideChannelReleases: function() {
return this.$store.getters.getHideChannelReleases
},
hideChannelCommunity: function() {
return this.$store.getters.getHideChannelCommunity
},
hideSubscriptionsVideos: function() {
return this.$store.getters.getHideSubscriptionsVideos
},
hideSubscriptionsShorts: function() {
return this.$store.getters.getHideSubscriptionsShorts
},
hideSubscriptionsLive: function() {
return this.$store.getters.getHideSubscriptionsLive
},
showDistractionFreeTitles: function () {
return this.$store.getters.getShowDistractionFreeTitles
},
channelsHidden: function () {
return JSON.parse(this.$store.getters.getChannelsHidden)
},
hideSubscriptionsLiveTooltip: function () {
return this.$t('Tooltips.Distraction Free Settings.Hide Subscriptions Live', {
appWideSetting: this.$t('Settings.Distraction Free Settings.Hide Live Streams'),
subsection: this.$t('Settings.Distraction Free Settings.Sections.General'),
settingsSection: this.$t('Settings.Distraction Free Settings.Distraction Free Settings')
})
}
},
methods: {
@ -117,7 +139,12 @@ export default defineComponent({
'updateHideFeaturedChannels',
'updateHideChannelShorts',
'updateHideChannelPlaylists',
'updateHideChannelCommunity'
'updateHideChannelCommunity',
'updateHideChannelPodcasts',
'updateHideChannelReleases',
'updateHideSubscriptionsVideos',
'updateHideSubscriptionsShorts',
'updateHideSubscriptionsLive'
])
}
})

View File

@ -37,6 +37,37 @@
/>
</div>
</div>
<h4
class="groupTitle"
>
{{ $t('Settings.Distraction Free Settings.Sections.Subscriptions Page') }}
</h4>
<div class="switchColumnGrid">
<div class="switchColumn">
<ft-toggle-switch
:label="$t('Settings.Distraction Free Settings.Hide Subscriptions Videos')"
:compact="true"
:default-value="hideSubscriptionsVideos"
@change="updateHideSubscriptionsVideos"
/>
<ft-toggle-switch
:label="$t('Settings.Distraction Free Settings.Hide Subscriptions Shorts')"
:compact="true"
:default-value="hideSubscriptionsShorts"
@change="updateHideSubscriptionsShorts"
/>
</div>
<div class="switchColumn">
<ft-toggle-switch
:label="$t('Settings.Distraction Free Settings.Hide Subscriptions Live')"
:compact="true"
:disabled="hideLiveStreams"
:default-value="hideLiveStreams || hideSubscriptionsLive"
:tooltip="hideLiveStreams ? hideSubscriptionsLiveTooltip : ''"
v-on="!hideLiveStreams ? { change: updateHideSubscriptionsLive } : {}"
/>
</div>
</div>
<h4
class="groupTitle"
>
@ -56,6 +87,12 @@
:default-value="hideChannelPlaylists"
@change="updateHideChannelPlaylists"
/>
<ft-toggle-switch
:label="$t('Settings.Distraction Free Settings.Hide Channel Podcasts')"
:compact="true"
:default-value="hideChannelPodcasts"
@change="updateHideChannelPodcasts"
/>
</div>
<div class="switchColumn">
<ft-toggle-switch
@ -70,6 +107,12 @@
:default-value="hideFeaturedChannels"
@change="updateHideFeaturedChannels"
/>
<ft-toggle-switch
:label="$t('Settings.Distraction Free Settings.Hide Channel Releases')"
:compact="true"
:default-value="hideChannelReleases"
@change="updateHideChannelReleases"
/>
</div>
</div>
<h4

View File

@ -111,7 +111,8 @@ export default defineComponent({
return Number.parseInt(b.width) - Number.parseInt(a.width)
})
return imageArrayCopy.at(0)?.url ?? ''
// Remove cropping directives when applicable
return imageArrayCopy.at(0)?.url?.replace(/-c-fcrop64=.*/i, '') ?? ''
}
}
})

View File

@ -21,7 +21,11 @@ export default defineComponent({
showVideoWithLastViewedPlaylist: {
type: Boolean,
default: false
}
},
useChannelsHiddenPreference: {
type: Boolean,
default: true,
},
},
computed: {
listType: function () {

View File

@ -10,6 +10,7 @@
:first-screen="index < 16"
:layout="displayValue"
:show-video-with-last-viewed-playlist="showVideoWithLastViewedPlaylist"
:use-channels-hidden-preference="useChannelsHiddenPreference"
/>
</ft-auto-grid>
</template>

View File

@ -33,6 +33,10 @@ export default defineComponent({
type: Boolean,
default: false
},
useChannelsHiddenPreference: {
type: Boolean,
default: true,
},
},
data: function () {
return {
@ -44,6 +48,9 @@ export default defineComponent({
return this.$store.getters.getHideLiveStreams
},
channelsHidden: function() {
// Some component users like channel view will have this disabled
if (!this.useChannelsHiddenPreference) { return [] }
return JSON.parse(this.$store.getters.getChannelsHidden)
},
hideUpcomingPremieres: function () {
@ -59,7 +66,7 @@ export default defineComponent({
if (!data.type) {
return false
}
if (data.type === 'video') {
if (data.type === 'video' || data.type === 'shortVideo') {
if (this.hideLiveStreams && (data.liveNow || data.lengthSeconds == null)) {
// hide livestreams
return false
@ -67,6 +74,10 @@ export default defineComponent({
if (this.hideUpcomingPremieres &&
// Observed for premieres in Local API Channels.
(data.premiereDate != null ||
// Invidious API
// `premiereTimestamp` only available on premiered videos
// https://docs.invidious.io/api/common_types/#videoobject
data.premiereTimestamp != null ||
// viewCount is our only method of detecting premieres in RSS
// data without sending an additional request.
// If we ever get a better flag, use it here instead.
@ -79,12 +90,30 @@ export default defineComponent({
return false
}
} else if (data.type === 'channel') {
if (this.channelsHidden.includes(data.channelID) || this.channelsHidden.includes(data.name)) {
const attrsToCheck = [
// Local API
data.id,
data.name,
// Invidious API
// https://docs.invidious.io/api/common_types/#channelobject
data.author,
data.authorId,
]
if (attrsToCheck.some(a => a != null && this.channelsHidden.includes(a))) {
// hide channels by author
return false
}
} else if (data.type === 'playlist') {
if (this.channelsHidden.includes(data.authorId) || this.channelsHidden.includes(data.author)) {
const attrsToCheck = [
// Local API
data.channelId,
data.channelName,
// Invidious API
// https://docs.invidious.io/api/common_types/#playlistobject
data.author,
data.authorId,
]
if (attrsToCheck.some(a => a != null && this.channelsHidden.includes(a))) {
// hide playlists by author
return false
}

View File

@ -43,18 +43,35 @@ export default defineComponent({
type: Boolean,
default: false,
},
useChannelsHiddenPreference: {
type: Boolean,
default: false,
},
},
data: function () {
return {
visible: false
}
},
computed: {
channelsHidden() {
// Some component users like channel view will have this disabled
if (!this.useChannelsHiddenPreference) { return [] }
return JSON.parse(this.$store.getters.getChannelsHidden)
},
shouldBeVisible() {
return !(this.channelsHidden.includes(this.data.authorId) ||
this.channelsHidden.includes(this.data.author))
}
},
created() {
this.visible = this.initialVisibleState
},
methods: {
onVisibilityChanged: function (visible) {
if (visible) {
if (visible && this.shouldBeVisible) {
this.visible = visible
}
}

View File

@ -16,7 +16,12 @@ import { IpcChannels } from '../../../constants'
import { sponsorBlockSkipSegments } from '../../helpers/sponsorblock'
import { calculateColorLuminance, colors } from '../../helpers/colors'
import { pathExists } from '../../helpers/filesystem'
import { getPicturesPath, showSaveDialog, showToast } from '../../helpers/utils'
import {
copyToClipboard,
getPicturesPath,
showSaveDialog,
showToast,
} from '../../helpers/utils'
import { getProxyUrl } from '../../helpers/api/invidious'
import store from '../../store'
@ -367,7 +372,7 @@ export default defineComponent({
this.determineFormatType()
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('play', () => this.player.play())
navigator.mediaSession.setActionHandler('play', () => this.playVideo())
navigator.mediaSession.setActionHandler('pause', () => this.player.pause())
}
@ -531,7 +536,8 @@ export default defineComponent({
if (this.autoplayVideos) {
// Calling play() won't happen right away, so a quick timeout will make it function properly.
setTimeout(() => {
this.player.play()
// `this.player` can be destroyed before this runs
this.playVideo()
}, 200)
}
@ -869,7 +875,7 @@ export default defineComponent({
this.player.playbackRate(this.defaultPlayback)
} else {
if (this.player.paused() || !this.player.hasStarted()) {
this.player.play()
this.playVideo()
} else {
this.player.pause()
}
@ -908,7 +914,7 @@ export default defineComponent({
this.player.playbackRate(playbackRate)
// need to call play to restore the player state, even if we want to pause afterwards
this.player.play().then(() => {
this.playVideo(() => {
if (isPaused) { this.player.pause() }
})
})
@ -1248,7 +1254,7 @@ export default defineComponent({
togglePlayPause: function () {
if (this.player.paused()) {
this.player.play()
this.playVideo()
} else {
this.player.pause()
}
@ -1571,7 +1577,7 @@ export default defineComponent({
const response = await showSaveDialog(options)
if (wasPlaying) {
this.player.play()
this.playVideo()
}
if (response.canceled || response.filePath === '') {
canvas.remove()
@ -1950,6 +1956,34 @@ export default defineComponent({
(process.platform === 'darwin' && event.metaKey))
},
playVideo(thenFunc = null) {
// It can be called in `setTimeout` & user can navigate to other pages before it runs
// Which makes `this.player` become `null`
if (this.player == null) { return }
let promise = this.player.play()
if (typeof thenFunc === 'function') {
promise = promise.then(thenFunc)
}
promise
.catch(err => {
if (err.message.includes('The play() request was interrupted by a new load request.')) {
// Ignoring expected exception
// This is thrown when `play()` called but user already viewing another page
// console.debug('Ignoring expected error')
// console.debug(err)
return
}
// Unexpected errors should be reported
console.error(err)
const errorMessage = this.$t('play() request Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
})
},
// This function should always be at the bottom of this file
/**
* @param {KeyboardEvent} event

View File

@ -190,7 +190,11 @@ export default defineComponent({
},
methods: {
handleInvidiousInstanceInput: function (input) {
const instance = input.replace(/\/$/, '')
let instance = input
// If NOT something like https:// (1-2 slashes), remove trailing slash
if (!/^(https?):(\/){1,2}$/.test(input)) {
instance = input.replace(/\/$/, '')
}
this.setCurrentInvidiousInstanceBounce(instance)
},

View File

@ -33,8 +33,9 @@ export default defineComponent({
return this.$store.getters.getActiveProfile
},
activeSubscriptions: function () {
const profile = JSON.parse(JSON.stringify(this.activeProfile))
return profile.subscriptions.sort((a, b) => {
const subscriptions = JSON.parse(JSON.stringify(this.activeProfile.subscriptions))
subscriptions.sort((a, b) => {
const nameA = a.name.toLowerCase()
const nameB = b.name.toLowerCase()
if (nameA < nameB) {
@ -44,13 +45,15 @@ export default defineComponent({
return 1
}
return 0
}).map((channel) => {
if (this.backendPreference === 'invidious') {
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstance)
}
return channel
})
if (this.backendPreference === 'invidious') {
subscriptions.forEach((channel) => {
channel.thumbnail = youtubeImageUrlToInvidious(channel.thumbnail, this.currentInvidiousInstance)
})
}
return subscriptions
},
hidePopularVideos: function () {
return this.$store.getters.getHidePopularVideos

View File

@ -0,0 +1,335 @@
import { defineComponent } from 'vue'
import { mapActions, mapMutations } from 'vuex'
import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue'
import { copyToClipboard, showToast } from '../../helpers/utils'
import { invidiousAPICall } from '../../helpers/api/invidious'
import { getLocalChannelLiveStreams } from '../../helpers/api/local'
import { addPublishedDatesInvidious, addPublishedDatesLocal, parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
export default defineComponent({
name: 'SubscriptionsLive',
components: {
'subscriptions-tab-ui': SubscriptionsTabUI
},
data: function () {
return {
isLoading: false,
videoList: [],
errorChannels: [],
attemptedFetch: false,
}
},
computed: {
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
},
useRssFeeds: function () {
return this.$store.getters.getUseRssFeeds
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
activeProfileId: function () {
return this.activeProfile._id
},
cacheEntriesForAllActiveProfileChannels() {
const entries = []
this.activeSubscriptionList.forEach((channel) => {
const cacheEntry = this.$store.getters.getLiveCacheByChannel(channel.id)
if (cacheEntry == null) { return }
entries.push(cacheEntry)
})
return entries
},
videoCacheForAllActiveProfileChannelsPresent() {
if (this.cacheEntriesForAllActiveProfileChannels.length === 0) { return false }
if (this.cacheEntriesForAllActiveProfileChannels.length < this.activeSubscriptionList.length) { return false }
return this.cacheEntriesForAllActiveProfileChannels.every((cacheEntry) => {
return cacheEntry.videos != null
})
},
activeSubscriptionList: function () {
return this.activeProfile.subscriptions
},
fetchSubscriptionsAutomatically: function() {
return this.$store.getters.getFetchSubscriptionsAutomatically
},
},
watch: {
activeProfile: async function (_) {
this.isLoading = true
this.loadVideosFromCacheSometimes()
},
},
mounted: async function () {
this.isLoading = true
this.loadVideosFromCacheSometimes()
},
methods: {
loadVideosFromCacheSometimes() {
// This method is called on view visible
if (this.videoCacheForAllActiveProfileChannelsPresent) {
this.loadVideosFromCacheForAllActiveProfileChannels()
return
}
this.maybeLoadVideosForSubscriptionsFromRemote()
},
async loadVideosFromCacheForAllActiveProfileChannels() {
const videoList = []
this.activeSubscriptionList.forEach((channel) => {
const channelCacheEntry = this.$store.getters.getLiveCacheByChannel(channel.id)
videoList.push(...channelCacheEntry.videos)
})
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
},
loadVideosForSubscriptionsFromRemote: async function () {
if (this.activeSubscriptionList.length === 0) {
this.isLoading = false
this.videoList = []
return
}
const channelsToLoadFromRemote = this.activeSubscriptionList
const videoList = []
let channelCount = 0
this.isLoading = true
let useRss = this.useRssFeeds
if (channelsToLoadFromRemote.length >= 125 && !useRss) {
showToast(
this.$t('Subscriptions["This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting"]'),
10000
)
useRss = true
}
this.updateShowProgressBar(true)
this.setProgressBarPercentage(0)
this.attemptedFetch = true
this.errorChannels = []
const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
let videos = []
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
if (useRss) {
videos = await this.getChannelLiveInvidiousRSS(channel)
} else {
videos = await this.getChannelLiveInvidious(channel)
}
} else {
if (useRss) {
videos = await this.getChannelLiveLocalRSS(channel)
} else {
videos = await this.getChannelLiveLocal(channel)
}
}
channelCount++
const percentageComplete = (channelCount / channelsToLoadFromRemote.length) * 100
this.setProgressBarPercentage(percentageComplete)
this.updateSubscriptionLiveCacheByChannel({
channelId: channel.id,
videos: videos,
})
return videos
}))).flatMap((o) => o)
videoList.push(...videoListFromRemote)
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
this.updateShowProgressBar(false)
},
maybeLoadVideosForSubscriptionsFromRemote: async function () {
if (this.fetchSubscriptionsAutomatically) {
// `this.isLoading = false` is called inside `loadVideosForSubscriptionsFromRemote` when needed
await this.loadVideosForSubscriptionsFromRemote()
} else {
this.videoList = []
this.attemptedFetch = false
this.isLoading = false
}
},
getChannelLiveLocal: async function (channel, failedAttempts = 0) {
try {
const entries = await getLocalChannelLiveStreams(channel.id)
if (entries === null) {
this.errorChannels.push(channel)
return []
}
addPublishedDatesLocal(entries)
return entries
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
switch (failedAttempts) {
case 0:
return await this.getChannelLiveLocalRSS(channel, failedAttempts + 1)
case 1:
if (this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
return await this.getChannelLiveInvidious(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return await this.getChannelLiveLocalRSS(channel, failedAttempts + 1)
default:
return []
}
}
},
getChannelLiveLocalRSS: async function (channel, failedAttempts = 0) {
const playlistId = channel.id.replace('UC', 'UULV')
const feedUrl = `https://www.youtube.com/feeds/videos.xml?playlist_id=${playlistId}`
try {
const response = await fetch(feedUrl)
if (response.status === 404) {
return []
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
} catch (error) {
console.error(error)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${error}`, 10000, () => {
copyToClipboard(error)
})
switch (failedAttempts) {
case 0:
return this.getChannelLiveLocal(channel, failedAttempts + 1)
case 1:
if (this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
return this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return this.getChannelLiveLocal(channel, failedAttempts + 1)
default:
return []
}
}
},
getChannelLiveInvidious: function (channel, failedAttempts = 0) {
return new Promise((resolve, reject) => {
const subscriptionsPayload = {
resource: 'channels',
id: channel.id,
subResource: 'streams',
params: {}
}
invidiousAPICall(subscriptionsPayload).then((result) => {
const videos = result.videos.filter(e => e.type === 'video')
addPublishedDatesInvidious(videos)
resolve(videos)
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err.responseText}`, 10000, () => {
copyToClipboard(err.responseText)
})
switch (failedAttempts) {
case 0:
resolve(this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1))
break
case 1:
if (process.env.IS_ELECTRON && this.backendFallback) {
showToast(this.$t('Falling back to the local API'))
resolve(this.getChannelLiveLocal(channel, failedAttempts + 1))
} else {
resolve([])
}
break
case 2:
resolve(this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1))
break
default:
resolve([])
}
})
})
},
getChannelLiveInvidiousRSS: async function (channel, failedAttempts = 0) {
const playlistId = channel.id.replace('UC', 'UULV')
const feedUrl = `${this.currentInvidiousInstance}/feed/playlist/${playlistId}`
try {
const response = await fetch(feedUrl)
if (response.status === 500) {
return []
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
} catch (error) {
console.error(error)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${error}`, 10000, () => {
copyToClipboard(error)
})
switch (failedAttempts) {
case 0:
return this.getChannelLiveInvidious(channel, failedAttempts + 1)
case 1:
if (process.env.IS_ELECTRON && this.backendFallback) {
showToast(this.$t('Falling back to the local API'))
return this.getChannelLiveLocalRSS(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return this.getChannelLiveInvidious(channel, failedAttempts + 1)
default:
return []
}
}
},
...mapActions([
'updateShowProgressBar',
'updateSubscriptionLiveCacheByChannel',
]),
...mapMutations([
'setProgressBarPercentage'
])
}
})

View File

@ -0,0 +1,11 @@
<template>
<subscriptions-tab-ui
:is-loading="isLoading"
:video-list="videoList"
:error-channels="errorChannels"
:attempted-fetch="attemptedFetch"
@refresh="loadVideosForSubscriptionsFromRemote"
/>
</template>
<script src="./subscriptions-live.js" />

View File

@ -0,0 +1,225 @@
import { defineComponent } from 'vue'
import { mapActions, mapMutations } from 'vuex'
import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue'
import { parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
import { copyToClipboard, showToast } from '../../helpers/utils'
export default defineComponent({
name: 'SubscriptionsShorts',
components: {
'subscriptions-tab-ui': SubscriptionsTabUI
},
data: function () {
return {
isLoading: false,
videoList: [],
errorChannels: [],
attemptedFetch: false,
}
},
computed: {
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
activeProfileId: function () {
return this.activeProfile._id
},
cacheEntriesForAllActiveProfileChannels() {
const entries = []
this.activeSubscriptionList.forEach((channel) => {
const cacheEntry = this.$store.getters.getShortsCacheByChannel(channel.id)
if (cacheEntry == null) { return }
entries.push(cacheEntry)
})
return entries
},
videoCacheForAllActiveProfileChannelsPresent() {
if (this.cacheEntriesForAllActiveProfileChannels.length === 0) { return false }
if (this.cacheEntriesForAllActiveProfileChannels.length < this.activeSubscriptionList.length) { return false }
return this.cacheEntriesForAllActiveProfileChannels.every((cacheEntry) => {
return cacheEntry.videos != null
})
},
activeSubscriptionList: function () {
return this.activeProfile.subscriptions
},
fetchSubscriptionsAutomatically: function() {
return this.$store.getters.getFetchSubscriptionsAutomatically
},
},
watch: {
activeProfile: async function (_) {
this.isLoading = true
this.loadVideosFromCacheSometimes()
},
},
mounted: async function () {
this.isLoading = true
this.loadVideosFromCacheSometimes()
},
methods: {
loadVideosFromCacheSometimes() {
// This method is called on view visible
if (this.videoCacheForAllActiveProfileChannelsPresent) {
this.loadVideosFromCacheForAllActiveProfileChannels()
return
}
this.maybeLoadVideosForSubscriptionsFromRemote()
},
async loadVideosFromCacheForAllActiveProfileChannels() {
const videoList = []
this.activeSubscriptionList.forEach((channel) => {
const channelCacheEntry = this.$store.getters.getShortsCacheByChannel(channel.id)
videoList.push(...channelCacheEntry.videos)
})
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
},
loadVideosForSubscriptionsFromRemote: async function () {
if (this.activeSubscriptionList.length === 0) {
this.isLoading = false
this.videoList = []
return
}
const channelsToLoadFromRemote = this.activeSubscriptionList
const videoList = []
let channelCount = 0
this.isLoading = true
this.updateShowProgressBar(true)
this.setProgressBarPercentage(0)
this.attemptedFetch = true
this.errorChannels = []
const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
let videos = []
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
videos = await this.getChannelShortsInvidious(channel)
} else {
videos = await this.getChannelShortsLocal(channel)
}
channelCount++
const percentageComplete = (channelCount / channelsToLoadFromRemote.length) * 100
this.setProgressBarPercentage(percentageComplete)
this.updateSubscriptionShortsCacheByChannel({
channelId: channel.id,
videos: videos,
})
return videos
}))).flatMap((o) => o)
videoList.push(...videoListFromRemote)
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
this.updateShowProgressBar(false)
},
maybeLoadVideosForSubscriptionsFromRemote: async function () {
if (this.fetchSubscriptionsAutomatically) {
// `this.isLoading = false` is called inside `loadVideosForSubscriptionsFromRemote` when needed
await this.loadVideosForSubscriptionsFromRemote()
} else {
this.videoList = []
this.attemptedFetch = false
this.isLoading = false
}
},
getChannelShortsLocal: async function (channel, failedAttempts = 0) {
const playlistId = channel.id.replace('UC', 'UUSH')
const feedUrl = `https://www.youtube.com/feeds/videos.xml?playlist_id=${playlistId}`
try {
const response = await fetch(feedUrl)
if (response.status === 404) {
return []
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
} catch (error) {
console.error(error)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${error}`, 10000, () => {
copyToClipboard(error)
})
switch (failedAttempts) {
case 0:
if (this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
return this.getChannelShortsInvidious(channel, failedAttempts + 1)
} else {
return []
}
default:
return []
}
}
},
getChannelShortsInvidious: async function (channel, failedAttempts = 0) {
const playlistId = channel.id.replace('UC', 'UUSH')
const feedUrl = `${this.currentInvidiousInstance}/feed/playlist/${playlistId}`
try {
const response = await fetch(feedUrl)
if (response.status === 500) {
return []
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
} catch (error) {
console.error(error)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${error}`, 10000, () => {
copyToClipboard(error)
})
switch (failedAttempts) {
case 0:
if (process.env.IS_ELECTRON && this.backendFallback) {
showToast(this.$t('Falling back to the local API'))
return this.getChannelShortsLocal(channel, failedAttempts + 1)
} else {
return []
}
default:
return []
}
}
},
...mapActions([
'updateShowProgressBar',
'updateSubscriptionShortsCacheByChannel',
]),
...mapMutations([
'setProgressBarPercentage'
])
}
})

View File

@ -0,0 +1,11 @@
<template>
<subscriptions-tab-ui
:is-loading="isLoading"
:video-list="videoList"
:error-channels="errorChannels"
:attempted-fetch="attemptedFetch"
@refresh="loadVideosForSubscriptionsFromRemote"
/>
</template>
<script src="./subscriptions-shorts.js" />

View File

@ -0,0 +1,31 @@
.card {
width: 85%;
margin: 0 auto;
margin-bottom: 60px;
}
.message {
color: var(--tertiary-text-color);
}
.floatingTopButton {
position: fixed;
top: 70px;
right: 10px;
}
.channelBubble {
display: inline-block;
}
@media only screen and (max-width: 350px) {
.floatingTopButton {
position: absolute
}
}
@media only screen and (max-width: 680px) {
.card {
width: 90%;
}
}

View File

@ -0,0 +1,111 @@
import { defineComponent } from 'vue'
import FtLoader from '../ft-loader/ft-loader.vue'
import FtCard from '../ft-card/ft-card.vue'
import FtButton from '../ft-button/ft-button.vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtElementList from '../ft-element-list/ft-element-list.vue'
import FtChannelBubble from '../ft-channel-bubble/ft-channel-bubble.vue'
export default defineComponent({
name: 'SubscriptionsTabUI',
components: {
'ft-loader': FtLoader,
'ft-card': FtCard,
'ft-button': FtButton,
'ft-icon-button': FtIconButton,
'ft-flex-box': FtFlexBox,
'ft-element-list': FtElementList,
'ft-channel-bubble': FtChannelBubble
},
props: {
isLoading: {
type: Boolean,
default: false
},
videoList: {
type: Array,
default: () => ([])
},
errorChannels: {
type: Array,
default: () => ([])
},
attemptedFetch: {
type: Boolean,
default: false
},
},
data: function () {
return {
dataLimit: 100,
}
},
computed: {
activeVideoList: function () {
if (this.videoList.length < this.dataLimit) {
return this.videoList
} else {
return this.videoList.slice(0, this.dataLimit)
}
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
activeSubscriptionList: function () {
return this.activeProfile.subscriptions
},
fetchSubscriptionsAutomatically: function() {
return this.$store.getters.getFetchSubscriptionsAutomatically
},
},
created: function () {
const dataLimit = sessionStorage.getItem('subscriptionLimit')
if (dataLimit !== null) {
this.dataLimit = dataLimit
}
},
mounted: async function () {
document.addEventListener('keydown', this.keyboardShortcutHandler)
},
beforeDestroy: function () {
document.removeEventListener('keydown', this.keyboardShortcutHandler)
},
methods: {
goToChannel: function (id) {
this.$router.push({ path: `/channel/${id}` })
},
increaseLimit: function () {
this.dataLimit += 100
sessionStorage.setItem('subscriptionLimit', this.dataLimit)
},
/**
* This function `keyboardShortcutHandler` should always be at the bottom of this file
* @param {KeyboardEvent} event the keyboard event
*/
keyboardShortcutHandler: function (event) {
if (event.ctrlKey || document.activeElement.classList.contains('ft-input')) {
return
}
// Avoid handling events due to user holding a key (not released)
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat
if (event.repeat) { return }
switch (event.key) {
case 'r':
case 'R':
if (!this.isLoading) {
this.$emit('refresh')
}
break
}
}
}
})

View File

@ -0,0 +1,72 @@
<template>
<div>
<ft-loader
v-if="isLoading"
/>
<div
v-if="!isLoading && errorChannels.length !== 0"
>
<h3> {{ $t("Subscriptions.Error Channels") }}</h3>
<div>
<ft-channel-bubble
v-for="(channel, index) in errorChannels"
:key="index"
:channel-name="channel.name"
:channel-id="channel.id"
:channel-thumbnail="channel.thumbnail"
class="channelBubble"
@click="goToChannel(channel.id)"
/>
</div>
</div>
<ft-flex-box
v-if="!isLoading && activeVideoList.length === 0"
>
<p
v-if="activeSubscriptionList.length === 0"
class="message"
>
{{ $t("Subscriptions['Your Subscription list is currently empty. Start adding subscriptions to see them here.']") }}
</p>
<p
v-else-if="!fetchSubscriptionsAutomatically && !attemptedFetch"
class="message"
>
{{ $t("Subscriptions.Disabled Automatic Fetching") }}
</p>
<p
v-else
class="message"
>
{{ $t("Subscriptions.Empty Channels") }}
</p>
</ft-flex-box>
<ft-element-list
v-if="!isLoading && activeVideoList.length > 0"
:data="activeVideoList"
:use-channels-hidden-preference="false"
/>
<ft-flex-box
v-if="!isLoading && videoList.length > dataLimit"
>
<ft-button
:label="$t('Subscriptions.Load More Videos')"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
@click="increaseLimit"
/>
</ft-flex-box>
<ft-icon-button
v-if="!isLoading"
:icon="['fas', 'sync']"
class="floatingTopButton"
:title="$t('Subscriptions.Refresh Subscriptions')"
:size="12"
theme="primary"
@click="$emit('refresh')"
/>
</div>
</template>
<script src="./subscriptions-tab-ui.js" />
<style scoped src="./subscriptions-tab-ui.css" />

View File

@ -0,0 +1,334 @@
import { defineComponent } from 'vue'
import { mapActions, mapMutations } from 'vuex'
import SubscriptionsTabUI from '../subscriptions-tab-ui/subscriptions-tab-ui.vue'
import { copyToClipboard, showToast } from '../../helpers/utils'
import { invidiousAPICall } from '../../helpers/api/invidious'
import { getLocalChannelVideos } from '../../helpers/api/local'
import { addPublishedDatesInvidious, addPublishedDatesLocal, parseYouTubeRSSFeed, updateVideoListAfterProcessing } from '../../helpers/subscriptions'
export default defineComponent({
name: 'SubscriptionsVideos',
components: {
'subscriptions-tab-ui': SubscriptionsTabUI
},
data: function () {
return {
isLoading: false,
videoList: [],
errorChannels: [],
attemptedFetch: false,
}
},
computed: {
backendPreference: function () {
return this.$store.getters.getBackendPreference
},
backendFallback: function () {
return this.$store.getters.getBackendFallback
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
},
useRssFeeds: function () {
return this.$store.getters.getUseRssFeeds
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
activeProfileId: function () {
return this.activeProfile._id
},
cacheEntriesForAllActiveProfileChannels() {
const entries = []
this.activeSubscriptionList.forEach((channel) => {
const cacheEntry = this.$store.getters.getVideoCacheByChannel(channel.id)
if (cacheEntry == null) { return }
entries.push(cacheEntry)
})
return entries
},
videoCacheForAllActiveProfileChannelsPresent() {
if (this.cacheEntriesForAllActiveProfileChannels.length === 0) { return false }
if (this.cacheEntriesForAllActiveProfileChannels.length < this.activeSubscriptionList.length) { return false }
return this.cacheEntriesForAllActiveProfileChannels.every((cacheEntry) => {
return cacheEntry.videos != null
})
},
activeSubscriptionList: function () {
return this.activeProfile.subscriptions
},
fetchSubscriptionsAutomatically: function() {
return this.$store.getters.getFetchSubscriptionsAutomatically
},
},
watch: {
activeProfile: async function (_) {
this.isLoading = true
this.loadVideosFromCacheSometimes()
},
},
mounted: async function () {
this.isLoading = true
this.loadVideosFromCacheSometimes()
},
methods: {
loadVideosFromCacheSometimes() {
// This method is called on view visible
if (this.videoCacheForAllActiveProfileChannelsPresent) {
this.loadVideosFromCacheForAllActiveProfileChannels()
return
}
this.maybeLoadVideosForSubscriptionsFromRemote()
},
async loadVideosFromCacheForAllActiveProfileChannels() {
const videoList = []
this.activeSubscriptionList.forEach((channel) => {
const channelCacheEntry = this.$store.getters.getVideoCacheByChannel(channel.id)
videoList.push(...channelCacheEntry.videos)
})
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
},
loadVideosForSubscriptionsFromRemote: async function () {
if (this.activeSubscriptionList.length === 0) {
this.isLoading = false
this.videoList = []
return
}
const channelsToLoadFromRemote = this.activeSubscriptionList
const videoList = []
let channelCount = 0
this.isLoading = true
let useRss = this.useRssFeeds
if (channelsToLoadFromRemote.length >= 125 && !useRss) {
showToast(
this.$t('Subscriptions["This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting"]'),
10000
)
useRss = true
}
this.updateShowProgressBar(true)
this.setProgressBarPercentage(0)
this.attemptedFetch = true
this.errorChannels = []
const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
let videos = []
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
if (useRss) {
videos = await this.getChannelVideosInvidiousRSS(channel)
} else {
videos = await this.getChannelVideosInvidiousScraper(channel)
}
} else {
if (useRss) {
videos = await this.getChannelVideosLocalRSS(channel)
} else {
videos = await this.getChannelVideosLocalScraper(channel)
}
}
channelCount++
const percentageComplete = (channelCount / channelsToLoadFromRemote.length) * 100
this.setProgressBarPercentage(percentageComplete)
this.updateSubscriptionVideosCacheByChannel({
channelId: channel.id,
videos: videos,
})
return videos
}))).flatMap((o) => o)
videoList.push(...videoListFromRemote)
this.videoList = updateVideoListAfterProcessing(videoList)
this.isLoading = false
this.updateShowProgressBar(false)
},
maybeLoadVideosForSubscriptionsFromRemote: async function () {
if (this.fetchSubscriptionsAutomatically) {
// `this.isLoading = false` is called inside `loadVideosForSubscriptionsFromRemote` when needed
await this.loadVideosForSubscriptionsFromRemote()
} else {
this.videoList = []
this.attemptedFetch = false
this.isLoading = false
}
},
getChannelVideosLocalScraper: async function (channel, failedAttempts = 0) {
try {
const videos = await getLocalChannelVideos(channel.id)
if (videos === null) {
this.errorChannels.push(channel)
return []
}
addPublishedDatesLocal(videos)
return videos
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
switch (failedAttempts) {
case 0:
return await this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
case 1:
if (this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
return await this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return await this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
default:
return []
}
}
},
getChannelVideosLocalRSS: async function (channel, failedAttempts = 0) {
const playlistId = channel.id.replace('UC', 'UULF')
const feedUrl = `https://www.youtube.com/feeds/videos.xml?playlist_id=${playlistId}`
try {
const response = await fetch(feedUrl)
if (response.status === 404) {
this.errorChannels.push(channel)
return []
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
} catch (error) {
console.error(error)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${error}`, 10000, () => {
copyToClipboard(error)
})
switch (failedAttempts) {
case 0:
return this.getChannelVideosLocalScraper(channel, failedAttempts + 1)
case 1:
if (this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
return this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return this.getChannelVideosLocalScraper(channel, failedAttempts + 1)
default:
return []
}
}
},
getChannelVideosInvidiousScraper: function (channel, failedAttempts = 0) {
return new Promise((resolve, reject) => {
const subscriptionsPayload = {
resource: 'channels/latest',
id: channel.id,
params: {}
}
invidiousAPICall(subscriptionsPayload).then((result) => {
addPublishedDatesInvidious(result.videos)
resolve(result.videos)
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err.responseText}`, 10000, () => {
copyToClipboard(err.responseText)
})
switch (failedAttempts) {
case 0:
resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1))
break
case 1:
if (process.env.IS_ELECTRON && this.backendFallback) {
showToast(this.$t('Falling back to the local API'))
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
} else {
resolve([])
}
break
case 2:
resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1))
break
default:
resolve([])
}
})
})
},
getChannelVideosInvidiousRSS: async function (channel, failedAttempts = 0) {
const playlistId = channel.id.replace('UC', 'UULF')
const feedUrl = `${this.currentInvidiousInstance}/feed/playlist/${playlistId}`
try {
const response = await fetch(feedUrl)
if (response.status === 500) {
this.errorChannels.push(channel)
return []
}
return await parseYouTubeRSSFeed(await response.text(), channel.id)
} catch (error) {
console.error(error)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${error}`, 10000, () => {
copyToClipboard(error)
})
switch (failedAttempts) {
case 0:
return this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
case 1:
if (process.env.IS_ELECTRON && this.backendFallback) {
showToast(this.$t('Falling back to the local API'))
return this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
default:
return []
}
}
},
...mapActions([
'updateShowProgressBar',
'updateSubscriptionVideosCacheByChannel',
]),
...mapMutations([
'setProgressBarPercentage'
])
}
})

View File

@ -0,0 +1,11 @@
<template>
<subscriptions-tab-ui
:is-loading="isLoading"
:video-list="videoList"
:error-channels="errorChannels"
:attempted-fetch="attemptedFetch"
@refresh="loadVideosForSubscriptionsFromRemote"
/>
</template>
<script src="./subscriptions-videos.js" />

View File

@ -22,6 +22,7 @@
:data="video"
appearance="recommendation"
force-list-type="list"
:use-channels-hidden-preference="true"
/>
</ft-card>
</template>

View File

@ -152,27 +152,6 @@ export async function getLocalVideoInfo(id, attemptBypass = false) {
player = innertube.actions.session.player
info = await innertube.getInfo(id)
// // the android streaming formats don't seem to be throttled at the moment so we use those if they are availabe
try {
const androidInnertube = await createInnertube({ clientType: ClientType.ANDROID, generateSessionLocally: false })
const androidInfo = await androidInnertube.getBasicInfo(id, 'ANDROID')
// Sometimes when YouTube detects a third party client or has applied an IP-ratelimit,
// they replace the response with a different video id
// https://github.com/TeamNewPipe/NewPipe/issues/8713
// https://github.com/TeamPiped/Piped/issues/2487
if (androidInfo.basic_info.id !== id) {
console.error(`Failed to fetch android formats. Wrong video ID in response: ${androidInfo.basic_info.id}, expected: ${id}`)
} else if (androidInfo.playability_status.status !== 'OK') {
console.error('Failed to fetch android formats', JSON.stringify(androidInfo.playability_status))
} else {
info.streaming_data = androidInfo.streaming_data
}
} catch (error) {
console.error('Failed to fetch android formats')
console.error(error)
}
}
if (info.streaming_data) {
@ -188,17 +167,24 @@ export async function getLocalComments(id, sortByNewest = false) {
return innertube.getComments(id, sortByNewest ? 'NEWEST_FIRST' : 'TOP_COMMENTS')
}
// I know `type & type` is typescript syntax and not valid jsdoc but I couldn't get @extends or @augments to work
/**
* @typedef {object} _LocalFormat
* @property {string} freeTubeUrl deciphered streaming URL, stored in a custom property so the DASH manifest generation doesn't break
*
* @typedef {Misc.Format & _LocalFormat} LocalFormat
*/
/**
* @param {Misc.Format[]} formats
* @param {import('youtubei.js').Player} player
*/
function decipherFormats(formats, player) {
for (const format of formats) {
format.url = format.decipher(player)
// set these to undefined so that toDash doesn't try to decipher them again, throwing an error
format.cipher = undefined
format.signature_cipher = undefined
// toDash deciphers the format again, so if we overwrite the original URL,
// it breaks because the n param would get deciphered twice and then be incorrect
format.freeTubeUrl = format.decipher(player)
}
}
@ -270,6 +256,36 @@ export async function getLocalChannelVideos(id) {
}
}
export async function getLocalChannelLiveStreams(id) {
const innertube = await createInnertube()
try {
const response = await innertube.actions.execute(Endpoints.BrowseEndpoint.PATH, Endpoints.BrowseEndpoint.build({
browse_id: id,
params: 'EgdzdHJlYW1z8gYECgJ6AA%3D%3D'
// protobuf for the live tab (this is the one that YouTube uses,
// it has some empty fields in the protobuf but it doesn't work if you remove them)
}))
const liveStreamsTab = new YT.Channel(null, response)
// 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')) {
return parseLocalChannelVideos(liveStreamsTab.videos, liveStreamsTab.header.author)
} else {
return []
}
} catch (error) {
console.error(error)
if (error instanceof Utils.ChannelError) {
return null
} else {
throw error
}
}
}
/**
* @param {import('youtubei.js').YTNodes.Video[]} videos
* @param {Misc.Author} author
@ -377,8 +393,7 @@ export function parseLocalListPlaylist(playlist, author = undefined) {
let channelId = null
/** @type {import('youtubei.js').YTNodes.PlaylistVideoThumbnail} */
const thumbnailRenderer = playlist.thumbnail_renderer
if (playlist.author) {
if (playlist.author && playlist.author.id !== 'N/A') {
if (playlist.author instanceof Misc.Text) {
channelName = playlist.author.text
@ -425,7 +440,8 @@ function handleSearchResponse(response) {
return {
results,
continuationData: response.has_continuation ? response : null
// check the length of the results, as there can be continuations for things that we've filtered out, which we don't want
continuationData: response.has_continuation && results.length > 0 ? response : null
}
}
@ -451,11 +467,47 @@ export function parseLocalPlaylistVideo(video) {
/** @type {import('youtubei.js').YTNodes.PlaylistVideo} */
const video_ = video
let viewCount = null
// the accessiblity label contains the full view count
// the video info only contains the short view count
if (video_.accessibility_label) {
const match = video_.accessibility_label.match(/([\d,.]+|no) views?$/i)
if (match) {
const count = match[1]
// as it's rare that a video has no views,
// checking the length allows us to avoid running toLowerCase unless we have to
if (count.length === 2 && count.toLowerCase() === 'no') {
viewCount = 0
} else {
const views = extractNumberFromString(count)
if (!isNaN(views)) {
viewCount = views
}
}
}
}
let publishedText = null
// normal videos have 3 text runs with the last one containing the published date
// live videos have 2 text runs with the number of people watching
// upcoming either videos don't have any info text or the number of people waiting,
// but we have the premiere date for those, so we don't need the published date
if (video_.video_info.runs && video_.video_info.runs.length === 3) {
publishedText = video_.video_info.runs[2].text
}
return {
videoId: video_.id,
title: video_.title.text,
author: video_.author.name,
authorId: video_.author.id,
viewCount,
publishedText,
lengthSeconds: isNaN(video_.duration.seconds) ? '' : video_.duration.seconds,
liveNow: video_.is_live,
isUpcoming: video_.is_upcoming,
@ -705,7 +757,7 @@ export function parseLocalTextRuns(runs, emojiSize = 16, options = { looseChanne
}
/**
* @param {Misc.Format} format
* @param {LocalFormat} format
*/
export function mapLocalFormat(format) {
return {
@ -716,7 +768,7 @@ export function mapLocalFormat(format) {
mimeType: format.mime_type,
height: format.height,
width: format.width,
url: format.url
url: format.freeTubeUrl
}
}

View File

@ -0,0 +1,146 @@
import store from '../store/index'
import { calculatePublishedDate } from './utils'
/**
* Filtering and sort based on user preferences
* @param {any[]} videos
*/
export function updateVideoListAfterProcessing(videos) {
let videoList = videos
// Filtering and sorting based in preference
videoList.sort((a, b) => {
return b.publishedDate - a.publishedDate
})
if (store.getters.getHideLiveStreams) {
videoList = videoList.filter(item => {
return (!item.liveNow && !item.isUpcoming)
})
}
if (store.getters.getHideUpcomingPremieres) {
videoList = videoList.filter(item => {
if (item.isRSS) {
// viewCount is our only method of detecting premieres in RSS
// data without sending an additional request.
// If we ever get a better flag, use it here instead.
return item.viewCount !== '0'
}
// Observed for premieres in Local API Subscriptions.
return (item.premiereDate == null ||
// Invidious API
// `premiereTimestamp` only available on premiered videos
// https://docs.invidious.io/api/common_types/#videoobject
item.premiereTimestamp == null
)
})
}
if (store.getters.getHideWatchedSubs) {
const historyCache = store.getters.getHistoryCache
videoList = videoList.filter((video) => {
const historyIndex = historyCache.findIndex((x) => {
return x.videoId === video.videoId
})
return historyIndex === -1
})
}
return videoList
}
/**
* @param {string} rssString
* @param {string} channelId
*/
export async function parseYouTubeRSSFeed(rssString, channelId) {
// doesn't need to be asynchronous, but doing it allows us to do the relatively slow DOM querying in parallel
try {
const xmlDom = new DOMParser().parseFromString(rssString, 'application/xml')
const channelName = xmlDom.querySelector('author > name').textContent
const entries = xmlDom.querySelectorAll('entry')
const promises = []
for (const entry of entries) {
promises.push(parseRSSEntry(entry, channelId, channelName))
}
return await Promise.all(promises)
} catch (e) {
return []
}
}
/**
* @param {Element} entry
* @param {string} channelId
* @param {string} channelName
*/
async function parseRSSEntry(entry, channelId, channelName) {
// doesn't need to be asynchronous, but doing it allows us to do the relatively slow DOM querying in parallel
const published = new Date(entry.querySelector('published').textContent)
return {
authorId: channelId,
author: channelName,
// querySelector doesn't support xml namespaces so we have to use getElementsByTagName here
videoId: entry.getElementsByTagName('yt:videoId')[0].textContent,
title: entry.querySelector('title').textContent,
publishedDate: published,
publishedText: published.toLocaleString(),
viewCount: entry.getElementsByTagName('media:statistics')[0]?.getAttribute('views') || null,
type: 'video',
lengthSeconds: '0:00',
isRSS: true
}
}
/**
* @param {{
* liveNow: boolean,
* isUpcoming: boolean,
* premiereDate: Date,
* publishedText: string,
* publishedDate: number
* }[]} videos publishedDate is added by this function,
* but adding it to the type definition stops vscode warning that the property doesn't exist
*/
export function addPublishedDatesLocal(videos) {
videos.forEach(video => {
if (video.liveNow) {
video.publishedDate = new Date().getTime()
} else if (video.isUpcoming) {
video.publishedDate = video.premiereDate
} else {
video.publishedDate = calculatePublishedDate(video.publishedText)
}
return video
})
}
/**
* @param {{
* liveNow: boolean,
* isUpcoming: boolean,
* premiereTimestamp: number,
* published: number,
* publishedDate: number
* }[]} videos publishedDate is added by this function,
* but adding it to the type definition stops vscode warning that the property doesn't exist
*/
export function addPublishedDatesInvidious(videos) {
videos.forEach(video => {
if (video.liveNow) {
video.publishedDate = new Date().getTime()
} else if (video.isUpcoming) {
video.publishedDate = new Date(video.premiereTimestamp * 1000)
} else {
video.publishedDate = new Date(video.published * 1000)
}
return video
})
}

View File

@ -197,10 +197,10 @@ export function showToast(message, time = null, action = null) {
* a toast with the error is shown. If the copy is successful and
* there is a success message, a toast with that message is shown.
* @param {string} content the content to be copied to the clipboard
* @param {string} messageOnSuccess the message to be displayed as a toast when the copy succeeds (optional)
* @param {string} messageOnError the message to be displayed as a toast when the copy fails (optional)
* @param {null|string} messageOnSuccess the message to be displayed as a toast when the copy succeeds (optional)
* @param {null|string} messageOnError the message to be displayed as a toast when the copy fails (optional)
*/
export async function copyToClipboard(content, { messageOnSuccess = null, messageOnError = null }) {
export async function copyToClipboard(content, { messageOnSuccess = null, messageOnError = null } = {}) {
if (navigator.clipboard !== undefined && window.isSecureContext) {
try {
await navigator.clipboard.writeText(content)

View File

@ -228,6 +228,7 @@ $watched-transition-duration: 0.5s;
font-size: 14px;
grid-area: infoLine;
margin-top: 5px;
overflow-wrap: anywhere;
@include is-sidebar-item {
font-size: 12px;

View File

@ -195,6 +195,8 @@ const state = {
hideActiveSubscriptions: false,
hideChannelCommunity: false,
hideChannelPlaylists: false,
hideChannelReleases: false,
hideChannelPodcasts: false,
hideChannelShorts: false,
hideChannelSubscriptions: false,
hideCommentLikes: false,
@ -210,6 +212,9 @@ const state = {
hideRecommendedVideos: false,
hideSearchBar: false,
hideSharingActions: false,
hideSubscriptionsVideos: false,
hideSubscriptionsShorts: false,
hideSubscriptionsLive: false,
hideTrendingVideos: false,
hideUnsubscribeButton: false,
hideUpcomingPremieres: false,

View File

@ -7,35 +7,91 @@ function deepCopy(obj) {
}
const state = {
subscriptionsCachePerChannel: {},
videoCache: {},
liveCache: {},
shortsCache: {}
}
const getters = {
getSubscriptionsCacheEntriesForOneChannel: (state) => (channelId) => {
return state.subscriptionsCachePerChannel[channelId]
getVideoCache: (state) => {
return state.videoCache
},
getVideoCacheByChannel: (state) => (channelId) => {
return state.videoCache[channelId]
},
getShortsCache: (state) => {
return state.shortsCache
},
getShortsCacheByChannel: (state) => (channelId) => {
return state.shortsCache[channelId]
},
getLiveCache: (state) => {
return state.liveCache
},
getLiveCacheByChannel: (state) => (channelId) => {
return state.liveCache[channelId]
}
}
const actions = {
clearSubscriptionsCache: ({ commit }) => {
commit('clearSubscriptionsCachePerChannel')
clearSubscriptionVideosCache: ({ commit }) => {
commit('clearVideoCache')
},
updateSubscriptionsCacheForOneChannel: ({ commit }, payload) => {
commit('updateSubscriptionsCacheForOneChannel', payload)
updateSubscriptionVideosCacheByChannel: ({ commit }, payload) => {
commit('updateVideoCacheByChannel', payload)
},
clearSubscriptionShortsCache: ({ commit }) => {
commit('clearShortsCache')
},
updateSubscriptionShortsCacheByChannel: ({ commit }, payload) => {
commit('updateShortsCacheByChannel', payload)
},
clearSubscriptionLiveCache: ({ commit }) => {
commit('clearLiveCache')
},
updateSubscriptionLiveCacheByChannel: ({ commit }, payload) => {
commit('updateLiveCacheByChannel', payload)
}
}
const mutations = {
updateSubscriptionsCacheForOneChannel(state, { channelId, videos }) {
const existingObject = state.subscriptionsCachePerChannel[channelId]
updateVideoCacheByChannel(state, { channelId, videos }) {
const existingObject = state.videoCache[channelId]
const newObject = existingObject != null ? existingObject : deepCopy(defaultCacheEntryValueForForOneChannel)
if (videos != null) { newObject.videos = videos }
state.subscriptionsCachePerChannel[channelId] = newObject
state.videoCache[channelId] = newObject
},
clearSubscriptionsCachePerChannel(state) {
state.subscriptionsCachePerChannel = {}
clearVideoCache(state) {
state.videoCache = {}
},
updateShortsCacheByChannel(state, { channelId, videos }) {
const existingObject = state.shortsCache[channelId]
const newObject = existingObject != null ? existingObject : deepCopy(defaultCacheEntryValueForForOneChannel)
if (videos != null) { newObject.videos = videos }
state.shortsCache[channelId] = newObject
},
clearShortsCache(state) {
state.shortsCache = {}
},
updateLiveCacheByChannel(state, { channelId, videos }) {
const existingObject = state.liveCache[channelId]
const newObject = existingObject != null ? existingObject : deepCopy(defaultCacheEntryValueForForOneChannel)
if (videos != null) { newObject.videos = videos }
state.liveCache[channelId] = newObject
},
clearLiveCache(state) {
state.liveCache = {}
}
}
export default {

View File

@ -317,7 +317,7 @@ const actions = {
let urlType = 'unknown'
const channelPattern =
/^\/(?:(?:channel|user|c)\/)?(?<channelId>[^/]+)(?:\/(?<tab>join|featured|videos|shorts|live|streams|playlists|about|community|channels))?\/?$/
/^\/(?:(?:channel|user|c)\/)?(?<channelId>[^/]+)(?:\/(?<tab>join|featured|videos|shorts|live|streams|podcasts|releases|playlists|about|community|channels))?\/?$/
const hashtagPattern = /^\/hashtag\/(?<tag>[^#&/?]+)$/
@ -439,6 +439,12 @@ const actions = {
case 'playlists':
subPath = 'playlists'
break
case 'podcasts':
subPath = 'podcasts'
break
case 'releases':
subPath = 'releases'
break
case 'channels':
case 'about':
subPath = 'about'
@ -523,7 +529,15 @@ const actions = {
if (payload.watchProgress > 0 && payload.watchProgress < payload.videoLength - 10) {
if (typeof cmdArgs.startOffset === 'string') {
args.push(`${cmdArgs.startOffset}${payload.watchProgress}`)
if (cmdArgs.startOffset.endsWith('=')) {
// For players using `=` in arguments
// e.g. vlc --start-time=xxxxx
args.push(`${cmdArgs.startOffset}${payload.watchProgress}`)
} else {
// For players using space in arguments
// e.g. smplayer -start xxxxx
args.push(cmdArgs.startOffset, Math.trunc(payload.watchProgress))
}
} else if (!ignoreWarnings) {
showExternalPlayerUnsupportedActionToast(externalPlayer, 'starting video at offset')
}

View File

@ -62,6 +62,8 @@ export default defineComponent({
videoContinuationData: null,
shortContinuationData: null,
liveContinuationData: null,
releaseContinuationData: null,
podcastContinuationData: null,
playlistContinuationData: null,
searchContinuationData: null,
communityContinuationData: null,
@ -83,6 +85,8 @@ export default defineComponent({
latestVideos: [],
latestShorts: [],
latestLive: [],
latestReleases: [],
latestPodcasts: [],
latestPlaylists: [],
latestCommunityPosts: [],
searchResults: [],
@ -104,6 +108,28 @@ export default defineComponent({
playlistSelectValues: [
'newest',
'last'
],
autoRefreshOnSortByChangeEnabled: false,
supportedChannelTabs: [
'videos',
'shorts',
'live',
'releases',
'podcasts',
'playlists',
'community',
'about'
],
channelTabs: [
'videos',
'shorts',
'live',
'releases',
'podcasts',
'playlists',
'community',
'about'
]
}
},
@ -183,6 +209,10 @@ export default defineComponent({
return !isNullOrEmpty(this.shortContinuationData)
case 'live':
return !isNullOrEmpty(this.liveContinuationData)
case 'releases':
return !isNullOrEmpty(this.releaseContinuationData)
case 'podcasts':
return !isNullOrEmpty(this.podcastContinuationData)
case 'playlists':
return !isNullOrEmpty(this.playlistContinuationData)
case 'community':
@ -209,6 +239,14 @@ export default defineComponent({
return this.$store.getters.getHideLiveStreams
},
hideChannelPodcasts: function() {
return this.$store.getters.getHideChannelPodcasts
},
hideChannelReleases: function() {
return this.$store.getters.getHideChannelReleases
},
hideChannelPlaylists: function() {
return this.$store.getters.getHideChannelPlaylists
},
@ -218,36 +256,40 @@ export default defineComponent({
},
tabInfoValues: function () {
const values = [
'videos',
'shorts',
'live',
'playlists',
'community',
'about'
]
const values = [...this.channelTabs]
const indexToRemove = []
// remove tabs from the array based on user settings
if (this.hideChannelShorts) {
const index = values.indexOf('shorts')
values.splice(index, 1)
indexToRemove.push(values.indexOf('shorts'))
}
if (this.hideLiveStreams) {
const index = values.indexOf('live')
values.splice(index, 1)
indexToRemove.push(values.indexOf('live'))
}
if (this.hideChannelPlaylists) {
const index = values.indexOf('playlists')
values.splice(index, 1)
indexToRemove.push(values.indexOf('playlists'))
}
if (this.hideChannelCommunity) {
const index = values.indexOf('community')
values.splice(index, 1)
indexToRemove.push(values.indexOf('community'))
}
if (this.hideChannelPodcasts) {
indexToRemove.push(values.indexOf('podcasts'))
}
if (this.hideChannelReleases) {
indexToRemove.push(values.indexOf('releases'))
}
indexToRemove.forEach(index => {
if (index !== -1) {
values.splice(index, 1)
}
})
return values
}
},
@ -261,6 +303,9 @@ export default defineComponent({
return
}
// Disable auto refresh on sort value change during state reset
this.autoRefreshOnSortByChangeEnabled = false
this.id = this.$route.params.id
this.searchPage = 2
this.relatedChannels = []
@ -272,6 +317,8 @@ export default defineComponent({
this.liveSortBy = 'newest'
this.playlistSortBy = 'newest'
this.latestPlaylists = []
this.latestPodcasts = []
this.latestReleases = []
this.latestCommunityPosts = []
this.searchResults = []
this.shownElementList = []
@ -281,6 +328,8 @@ export default defineComponent({
this.shortContinuationData = null
this.liveContinuationData = null
this.playlistContinuationData = null
this.podcastContinuationData = null
this.releaseContinuationData = null
this.searchContinuationData = null
this.communityContinuationData = null
this.showSearchBar = true
@ -300,14 +349,20 @@ export default defineComponent({
this.showShareMenu = true
this.errorMessage = ''
// Re-enable auto refresh on sort value change AFTER update done
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
this.getChannelInfoInvidious()
this.autoRefreshOnSortByChangeEnabled = true
} else {
this.getChannelLocal()
this.getChannelLocal().finally(() => {
this.autoRefreshOnSortByChangeEnabled = true
})
}
},
videoSortBy () {
if (!this.autoRefreshOnSortByChangeEnabled) { return }
this.isElementListLoading = true
this.latestVideos = []
switch (this.apiUsed) {
@ -323,6 +378,8 @@ export default defineComponent({
},
shortSortBy() {
if (!this.autoRefreshOnSortByChangeEnabled) { return }
this.isElementListLoading = true
this.latestShorts = []
switch (this.apiUsed) {
@ -338,6 +395,8 @@ export default defineComponent({
},
liveSortBy () {
if (!this.autoRefreshOnSortByChangeEnabled) { return }
this.isElementListLoading = true
this.latestLive = []
switch (this.apiUsed) {
@ -353,6 +412,8 @@ export default defineComponent({
},
playlistSortBy () {
if (!this.autoRefreshOnSortByChangeEnabled) { return }
this.isElementListLoading = true
this.latestPlaylists = []
this.playlistContinuationData = null
@ -386,10 +447,14 @@ export default defineComponent({
return
}
// Enable auto refresh on sort value change AFTER initial update done
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
this.getChannelInfoInvidious()
this.autoRefreshOnSortByChangeEnabled = true
} else {
this.getChannelLocal()
this.getChannelLocal().finally(() => {
this.autoRefreshOnSortByChangeEnabled = true
})
}
},
methods: {
@ -593,27 +658,48 @@ export default defineComponent({
this.joined = 0
this.location = null
}
const tabs = ['about']
if (channel.has_videos) {
tabs.push('videos')
this.getChannelVideosLocal()
}
if (!this.hideChannelShorts && channel.has_shorts) {
tabs.push('shorts')
this.getChannelShortsLocal()
}
if (!this.hideLiveStreams && channel.has_live_streams) {
tabs.push('live')
this.getChannelLiveLocal()
}
if (!this.hideChannelPodcasts && channel.has_podcasts) {
tabs.push('podcasts')
this.getChannelPodcastsLocal()
}
if (!this.hideChannelReleases && channel.has_releases) {
tabs.push('releases')
this.getChannelReleasesLocal()
}
if (!this.hideChannelPlaylists && channel.has_playlists) {
tabs.push('playlists')
this.getChannelPlaylistsLocal()
}
if (!this.hideChannelCommunity && channel.has_community) {
tabs.push('community')
this.getCommunityPostsLocal()
}
this.channelTabs = this.supportedChannelTabs.filter(tab => {
return tabs.includes(tab)
})
this.currentTab = this.currentOrFirstTab(this.$route.params.currentTab)
this.showSearchBar = channel.has_search
this.isLoading = false
@ -882,6 +968,19 @@ export default defineComponent({
// some channels only have a few tabs
// here are all possible values: home, videos, shorts, streams, playlists, community, channels, about
const tabs = response.tabs.map(tab => {
if (tab === 'streams') {
return 'live'
}
return tab
})
this.channelTabs = this.supportedChannelTabs.filter(tab => {
return tabs.includes(tab)
})
this.currentTab = this.currentOrFirstTab(this.$route.params.currentTab)
if (response.tabs.includes('videos')) {
this.channelInvidiousVideos()
}
@ -894,6 +993,14 @@ export default defineComponent({
this.channelInvidiousLive()
}
if (!this.hideChannelPodcasts && response.tabs.includes('podcasts')) {
this.channelInvidiousPodcasts()
}
if (!this.hideChannelReleases && response.tabs.includes('releases')) {
this.channelInvidiousReleases()
}
if (!this.hideChannelPlaylists && response.tabs.includes('playlists')) {
this.getPlaylistsInvidious()
}
@ -921,8 +1028,9 @@ export default defineComponent({
channelInvidiousVideos: function (sortByChanged) {
const payload = {
resource: 'channels/videos',
resource: 'channels',
id: this.id,
subResource: 'videos',
params: {
sort_by: this.videoSortBy,
}
@ -1128,7 +1236,8 @@ export default defineComponent({
getPlaylistsInvidious: function () {
this.isElementListLoading = true
const payload = {
resource: 'channels/playlists',
resource: 'channels',
subResource: 'playlists',
id: this.id,
params: {
sort_by: this.playlistSortBy
@ -1164,7 +1273,8 @@ export default defineComponent({
}
const payload = {
resource: 'channels/playlists',
resource: 'channels',
subResource: 'playlists',
id: this.id,
params: {
sort_by: this.playlistSortBy
@ -1194,6 +1304,232 @@ export default defineComponent({
})
},
getChannelReleasesLocal: async function () {
this.isElementListLoading = true
const expectedId = this.id
try {
/**
* @type {import('youtubei.js').YT.Channel}
*/
const channel = this.channelInstance
const releaseTab = await channel.getReleases()
if (expectedId !== this.id) {
return
}
this.latestReleases = releaseTab.playlists.map(playlist => parseLocalListPlaylist(playlist, channel.header.author))
this.releaseContinuationData = releaseTab.has_continuation ? releaseTab : null
this.isElementListLoading = false
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
if (this.backendPreference === 'local' && this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
this.getChannelReleasesInvidious()
} else {
this.isLoading = false
}
}
},
getChannelReleasesLocalMore: async function () {
try {
/**
* @type {import('youtubei.js').YT.ChannelListContinuation}
*/
const continuation = await this.releaseContinuationData.getContinuation()
const parsedReleases = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.channelInstance.header.author))
this.latestReleases = this.latestReleases.concat(parsedReleases)
this.releaseContinuationData = continuation.has_continuation ? continuation : null
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
}
},
channelInvidiousReleases: function() {
this.isElementListLoading = true
const payload = {
resource: 'channels',
subResource: 'releases',
id: this.id,
}
invidiousAPICall(payload).then((response) => {
this.releaseContinuationData = response.continuation || null
this.latestReleases = response.playlists
this.isElementListLoading = false
}).catch(async (err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
showToast(this.$t('Falling back to Local API'))
if (!this.channelInstance) {
this.channelInstance = await getLocalChannel(this.id)
}
this.getChannelReleasesLocal()
} else {
this.isLoading = false
}
})
},
channelInvidiousReleasesMore: function () {
if (this.releaseContinuationData === null) {
console.warn('There are no more podcasts available for this channel')
return
}
const payload = {
resource: 'channels',
subResource: 'releases',
id: this.id
}
invidiousAPICall(payload).then((response) => {
this.releaseContinuationData = response.continuation || null
this.latestReleases = this.latestReleases.concat(response.playlists)
this.isElementListLoading = false
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
showToast(this.$t('Falling back to Local API'))
this.getChannelLocal()
} else {
this.isLoading = false
}
})
},
getChannelPodcastsLocal: async function () {
this.isElementListLoading = true
const expectedId = this.id
try {
/**
* @type {import('youtubei.js').YT.Channel}
*/
const channel = this.channelInstance
const podcastTab = await channel.getPodcasts()
if (expectedId !== this.id) {
return
}
this.latestPodcasts = podcastTab.playlists.map(playlist => parseLocalListPlaylist(playlist, channel.header.author))
this.podcastContinuationData = podcastTab.has_continuation ? podcastTab : null
this.isElementListLoading = false
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
if (this.backendPreference === 'local' && this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
this.getChannelPodcastsInvidious()
} else {
this.isLoading = false
}
}
},
getChannelPodcastsLocalMore: async function () {
try {
/**
* @type {import('youtubei.js').YT.ChannelListContinuation}
*/
const continuation = await this.podcastContinuationData.getContinuation()
const parsedPodcasts = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.channelInstance.header.author))
this.latestPodcasts = this.latestPodcasts.concat(parsedPodcasts)
this.releaseContinuationData = continuation.has_continuation ? continuation : null
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
}
},
channelInvidiousPodcasts: function() {
this.isElementListLoading = true
const payload = {
resource: 'channels',
subResource: 'podcasts',
id: this.id,
}
invidiousAPICall(payload).then((response) => {
this.podcastContinuationData = response.continuation || null
this.latestPodcasts = response.playlists
this.isElementListLoading = false
}).catch(async (err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
showToast(this.$t('Falling back to Local API'))
if (!this.channelInstance) {
this.channelInstance = await getLocalChannel(this.id)
}
this.getChannelPodcastsLocal()
} else {
this.isLoading = false
}
})
},
channelInvidiousPodcastsMore: function () {
if (this.podcastContinuationData === null) {
console.warn('There are no more podcasts available for this channel')
return
}
const payload = {
resource: 'channels',
subResource: 'podcasts',
id: this.id
}
invidiousAPICall(payload).then((response) => {
this.podcastContinuationData = response.continuation || null
this.latestPodcasts = this.latestPodcasts.concat(response.playlists)
this.isElementListLoading = false
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
showToast(this.$t('Falling back to Local API'))
this.getChannelLocal()
} else {
this.isLoading = false
}
})
},
getCommunityPostsLocal: async function () {
const expectedId = this.id
@ -1333,6 +1669,12 @@ export default defineComponent({
break
}
break
case 'releases':
this.getChannelReleasesLocalMore()
break
case 'podcasts':
this.getChannelPodcastsLocalMore()
break
case 'playlists':
switch (this.apiUsed) {
case 'local':
@ -1470,8 +1812,9 @@ export default defineComponent({
searchChannelInvidious: function () {
const payload = {
resource: 'channels/search',
resource: 'channels',
id: this.id,
subResource: 'search',
params: {
q: this.lastSearchQuery,
page: this.searchPage

View File

@ -86,6 +86,7 @@
:aria-label="$t('Channel.Channel Tabs')"
>
<div
v-if="tabInfoValues.includes('videos')"
id="videosTab"
class="tab"
:class="(currentTab==='videos')?'selectedTab':''"
@ -99,7 +100,7 @@
{{ $t("Channel.Videos.Videos").toUpperCase() }}
</div>
<div
v-if="!hideChannelShorts"
v-if="tabInfoValues.includes('shorts') && !hideChannelShorts"
id="shortsTab"
class="tab"
:class="(currentTab==='shorts')?'selectedTab':''"
@ -110,10 +111,10 @@
@click="changeTab('shorts')"
@keydown.left.right.enter.space="changeTab('shorts', $event)"
>
{{ $t("Channel.Shorts.Shorts").toUpperCase() }}
{{ $t("Global.Shorts").toUpperCase() }}
</div>
<div
v-if="!hideLiveStreams"
v-if="tabInfoValues.includes('live') && !hideLiveStreams"
id="liveTab"
class="tab"
:class="(currentTab==='live')?'selectedTab':''"
@ -127,7 +128,35 @@
{{ $t("Channel.Live.Live").toUpperCase() }}
</div>
<div
v-if="!hideChannelPlaylists"
v-if="tabInfoValues.includes('releases') && !hideChannelReleases"
id="releasesTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'releases')"
aria-controls="releasePanel"
:tabindex="currentTab === 'releases' ? 0 : -1"
:class="(currentTab==='releases')?'selectedTab':''"
@click="changeTab('releases')"
@keydown.left.right.enter.space="changeTab('releases', $event)"
>
{{ $t("Channel.Releases.Releases").toUpperCase() }}
</div>
<div
v-if="tabInfoValues.includes('podcasts') && !hideChannelPodcasts"
id="podcastsTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'podcasts')"
aria-controls="podcastPanel"
:tabindex="currentTab === 'podcasts' ? 0 : -1"
:class="(currentTab==='podcasts')?'selectedTab':''"
@click="changeTab('podcasts')"
@keydown.left.right.enter.space="changeTab('podcasts', $event)"
>
{{ $t("Channel.Podcasts.Podcasts").toUpperCase() }}
</div>
<div
v-if="tabInfoValues.includes('playlists') && !hideChannelPlaylists"
id="playlistsTab"
class="tab"
role="tab"
@ -141,7 +170,7 @@
{{ $t("Channel.Playlists.Playlists").toUpperCase() }}
</div>
<div
v-if="!hideChannelCommunity"
v-if="tabInfoValues.includes('community') && !hideChannelCommunity"
id="communityTab"
class="tab"
role="tab"
@ -244,6 +273,7 @@
v-show="currentTab === 'videos'"
id="videoPanel"
:data="latestVideos"
:use-channels-hidden-preference="false"
role="tabpanel"
aria-labelledby="videosTab"
/>
@ -258,6 +288,7 @@
v-if="!hideChannelShorts && currentTab === 'shorts'"
id="shortPanel"
:data="latestShorts"
:use-channels-hidden-preference="false"
role="tabpanel"
aria-labelledby="shortsTab"
/>
@ -273,6 +304,7 @@
v-show="currentTab === 'live'"
id="livePanel"
:data="latestLive"
:use-channels-hidden-preference="false"
role="tabpanel"
aria-labelledby="liveTab"
/>
@ -283,10 +315,41 @@
{{ $t("Channel.Live.This channel does not currently have any live streams") }}
</p>
</ft-flex-box>
<ft-element-list
v-if="!hideChannelPodcasts && currentTab === 'podcasts'"
id="podcastPanel"
:data="latestPodcasts"
:use-channels-hidden-preference="false"
role="tabpanel"
aria-labelledby="podcastsTab"
/>
<ft-flex-box
v-if="!hideChannelPodcasts && currentTab === 'podcasts' && latestPodcasts.length === 0"
>
<p class="message">
{{ $t("Channel.Podcasts.This channel does not currently have any podcasts") }}
</p>
</ft-flex-box>
<ft-element-list
v-if="!hideChannelReleases && currentTab === 'releases'"
id="releasePanel"
:data="latestReleases"
:use-channels-hidden-preference="false"
role="tabpanel"
aria-labelledby="releasesTab"
/>
<ft-flex-box
v-if="!hideChannelReleases && currentTab === 'releases' && latestReleases.length === 0"
>
<p class="message">
{{ $t("Channel.Releases.This channel does not currently have any releases") }}
</p>
</ft-flex-box>
<ft-element-list
v-if="!hideChannelPlaylists && currentTab === 'playlists'"
id="playlistPanel"
:data="latestPlaylists"
:use-channels-hidden-preference="false"
role="tabpanel"
aria-labelledby="playlistsTab"
/>
@ -301,6 +364,7 @@
v-if="!hideChannelCommunity && currentTab === 'community'"
id="communityPanel"
:data="latestCommunityPosts"
:use-channels-hidden-preference="false"
role="tabpanel"
aria-labelledby="communityTab"
display="list"
@ -315,6 +379,7 @@
<ft-element-list
v-show="currentTab === 'search'"
:data="searchResults"
:use-channels-hidden-preference="false"
/>
<ft-flex-box
v-if="currentTab === 'search' && searchResults.length === 0"

View File

@ -36,6 +36,7 @@
v-if="activeData.length > 0 && !isLoading"
:data="activeData"
:show-video-with-last-viewed-playlist="true"
:use-channels-hidden-preference="false"
/>
<ft-flex-box
v-if="showLoadMoreButton"

View File

@ -118,10 +118,6 @@ export default defineComponent({
try {
const { results, continuationData } = await getLocalSearchResults(payload.query, payload.searchSettings, this.showFamilyFriendlyOnly)
if (results.length === 0) {
return
}
this.apiUsed = 'local'
this.shownResults = results

View File

@ -8,20 +8,31 @@
color: var(--tertiary-text-color);
}
.floatingTopButton {
position: fixed;
top: 70px;
right: 10px;
.subscriptionTabs {
width: 100%;
margin-top: -3px;
color: var(--tertiary-text-color);
margin-bottom: 10px;
}
.channelBubble {
display: inline-block;
.selectedTab {
border-bottom: 3px solid var(--primary-color);
color: var(--primary-text-color);
font-weight: bold;
box-sizing: border-box;
margin-bottom: -3px;
}
@media only screen and (max-width: 350px) {
.floatingTopButton {
position: absolute
}
.tab {
text-align: center;
padding: 15px;
font-size: 15px;
cursor: pointer;
align-self: flex-end;
}
.tab:hover {
font-weight: bold;
}
@media only screen and (max-width: 680px) {

View File

@ -1,496 +1,128 @@
import { defineComponent } from 'vue'
import { mapActions, mapMutations } from 'vuex'
import FtLoader from '../../components/ft-loader/ft-loader.vue'
import FtCard from '../../components/ft-card/ft-card.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
import FtIconButton from '../../components/ft-icon-button/ft-icon-button.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtElementList from '../../components/ft-element-list/ft-element-list.vue'
import FtChannelBubble from '../../components/ft-channel-bubble/ft-channel-bubble.vue'
import { calculatePublishedDate, copyToClipboard, showToast } from '../../helpers/utils'
import { invidiousAPICall } from '../../helpers/api/invidious'
import { getLocalChannelVideos } from '../../helpers/api/local'
import SubscriptionsVideos from '../../components/subscriptions-videos/subscriptions-videos.vue'
import SubscriptionsLive from '../../components/subscriptions-live/subscriptions-live.vue'
import SubscriptionsShorts from '../../components/subscriptions-shorts/subscriptions-shorts.vue'
import FtCard from '../../components/ft-card/ft-card.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
export default defineComponent({
name: 'Subscriptions',
components: {
'ft-loader': FtLoader,
'subscriptions-videos': SubscriptionsVideos,
'subscriptions-live': SubscriptionsLive,
'subscriptions-shorts': SubscriptionsShorts,
'ft-card': FtCard,
'ft-button': FtButton,
'ft-icon-button': FtIconButton,
'ft-flex-box': FtFlexBox,
'ft-element-list': FtElementList,
'ft-channel-bubble': FtChannelBubble
'ft-flex-box': FtFlexBox
},
data: function () {
return {
isLoading: false,
dataLimit: 100,
videoList: [],
errorChannels: [],
attemptedFetch: false,
currentTab: 'videos'
}
},
computed: {
backendPreference: function () {
let preference = this.$store.getters.getBackendPreference
if (preference === 'piped') {
preference = this.$store.getters.getFallbackPreference
hideSubscriptionsVideos: function () {
return this.$store.getters.getHideSubscriptionsVideos
},
hideSubscriptionsShorts: function () {
return this.$store.getters.getHideSubscriptionsShorts
},
hideSubscriptionsLive: function () {
return this.$store.getters.getHideLiveStreams || this.$store.getters.getHideSubscriptionsLive
},
visibleTabs: function () {
const tabs = []
if (!this.hideSubscriptionsVideos) {
tabs.push('videos')
}
return preference
},
backendFallback: function () {
return this.$store.getters.getBackendFallback && this.$store.getters.getBackendPreference !== 'piped'
},
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
},
hideWatchedSubs: function () {
return this.$store.getters.getHideWatchedSubs
},
useRssFeeds: function () {
return this.$store.getters.getUseRssFeeds
},
activeVideoList: function () {
if (this.videoList.length < this.dataLimit) {
return this.videoList
} else {
return this.videoList.slice(0, this.dataLimit)
if (!this.hideSubscriptionsShorts) {
tabs.push('shorts')
}
},
activeProfile: function () {
return this.$store.getters.getActiveProfile
},
activeProfileId: function () {
return this.activeProfile._id
},
if (!this.hideSubscriptionsLive) {
tabs.push('live')
}
cacheEntriesForAllActiveProfileChannels() {
const entries = []
this.activeSubscriptionList.forEach((channel) => {
const cacheEntry = this.$store.getters.getSubscriptionsCacheEntriesForOneChannel(channel.id)
if (cacheEntry == null) { return }
entries.push(cacheEntry)
})
return entries
},
videoCacheForAllActiveProfileChannelsPresent() {
if (this.cacheEntriesForAllActiveProfileChannels.length === 0) { return false }
if (this.cacheEntriesForAllActiveProfileChannels.length < this.activeSubscriptionList.length) { return false }
return this.cacheEntriesForAllActiveProfileChannels.every((cacheEntry) => {
return cacheEntry.videos != null
})
},
historyCache: function () {
return this.$store.getters.getHistoryCache
},
activeSubscriptionList: function () {
return this.activeProfile.subscriptions
},
hideLiveStreams: function() {
return this.$store.getters.getHideLiveStreams
},
hideUpcomingPremieres: function () {
return this.$store.getters.getHideUpcomingPremieres
},
fetchSubscriptionsAutomatically: function() {
return this.$store.getters.getFetchSubscriptionsAutomatically
},
return tabs
}
},
watch: {
activeProfile: async function (_) {
this.isLoading = true
this.loadVideosFromCacheSometimes()
currentTab(value) {
if (value !== null) {
// Save last used tab, restore when view mounted again
sessionStorage.setItem('Subscriptions/currentTab', value)
} else {
sessionStorage.removeItem('Subscriptions/currentTab')
}
},
},
mounted: async function () {
document.addEventListener('keydown', this.keyboardShortcutHandler)
this.isLoading = true
const dataLimit = sessionStorage.getItem('subscriptionLimit')
if (dataLimit !== null) {
this.dataLimit = dataLimit
/**
* @param {string[]} newValue
*/
visibleTabs: function (newValue) {
if (newValue.length === 0) {
this.currentTab = null
} else if (!newValue.includes(this.currentTab)) {
this.currentTab = newValue[0]
}
}
this.loadVideosFromCacheSometimes()
},
beforeDestroy: function () {
document.removeEventListener('keydown', this.keyboardShortcutHandler)
created: async function () {
if (this.visibleTabs.length === 0) {
this.currentTab = null
} else {
// Restore currentTab
const lastCurrentTabId = sessionStorage.getItem('Subscriptions/currentTab')
if (lastCurrentTabId !== null) { this.changeTab(lastCurrentTabId) }
}
},
methods: {
loadVideosFromCacheSometimes() {
// This method is called on view visible
if (this.videoCacheForAllActiveProfileChannelsPresent) {
this.loadVideosFromCacheForAllActiveProfileChannels()
changeTab: function (tab) {
if (tab === this.currentTab) {
return
}
this.maybeLoadVideosForSubscriptionsFromRemote()
},
async loadVideosFromCacheForAllActiveProfileChannels() {
const videoList = []
this.activeSubscriptionList.forEach((channel) => {
const channelCacheEntry = this.$store.getters.getSubscriptionsCacheEntriesForOneChannel(channel.id)
videoList.push(...channelCacheEntry.videos)
})
this.updateVideoListAfterProcessing(videoList)
this.isLoading = false
},
goToChannel: function (id) {
this.$router.push({ path: `/channel/${id}` })
},
loadVideosForSubscriptionsFromRemote: async function () {
if (this.activeSubscriptionList.length === 0) {
this.isLoading = false
this.videoList = []
return
}
const channelsToLoadFromRemote = this.activeSubscriptionList
const videoList = []
let channelCount = 0
this.isLoading = true
let useRss = this.useRssFeeds
if (channelsToLoadFromRemote.length >= 125 && !useRss) {
showToast(
this.$t('Subscriptions["This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting"]'),
10000
)
useRss = true
}
this.updateShowProgressBar(true)
this.setProgressBarPercentage(0)
this.attemptedFetch = true
this.errorChannels = []
const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
let videos = []
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
if (useRss) {
videos = await this.getChannelVideosInvidiousRSS(channel)
} else {
videos = await this.getChannelVideosInvidiousScraper(channel)
}
} else {
if (useRss) {
videos = await this.getChannelVideosLocalRSS(channel)
} else {
videos = await this.getChannelVideosLocalScraper(channel)
}
}
channelCount++
const percentageComplete = (channelCount / channelsToLoadFromRemote.length) * 100
this.setProgressBarPercentage(percentageComplete)
this.updateSubscriptionsCacheForOneChannel({
channelId: channel.id,
videos: videos,
})
return videos
}))).flatMap((o) => o)
videoList.push(...videoListFromRemote)
this.updateVideoListAfterProcessing(videoList)
this.isLoading = false
this.updateShowProgressBar(false)
},
updateVideoListAfterProcessing(videoList) {
// Filtering and sorting based in preference
videoList.sort((a, b) => {
return b.publishedDate - a.publishedDate
})
if (this.hideLiveStreams) {
videoList = videoList.filter(item => {
return (!item.liveNow && !item.isUpcoming)
})
}
if (this.hideUpcomingPremieres) {
videoList = videoList.filter(item => {
if (item.isRSS) {
// viewCount is our only method of detecting premieres in RSS
// data without sending an additional request.
// If we ever get a better flag, use it here instead.
return item.viewCount !== '0'
}
// Observed for premieres in Local API Subscriptions.
return item.premiereDate == null
})
}
this.videoList = videoList.filter((video) => {
if (this.hideWatchedSubs) {
const historyIndex = this.historyCache.findIndex((x) => {
return x.videoId === video.videoId
})
return historyIndex === -1
} else {
return true
}
})
},
maybeLoadVideosForSubscriptionsFromRemote: async function () {
if (this.fetchSubscriptionsAutomatically) {
// `this.isLoading = false` is called inside `loadVideosForSubscriptionsFromRemote` when needed
await this.loadVideosForSubscriptionsFromRemote()
if (this.visibleTabs.includes(tab)) {
this.currentTab = tab
} else {
this.videoList = []
this.attemptedFetch = false
this.isLoading = false
this.currentTab = null
}
},
getChannelVideosLocalScraper: async function (channel, failedAttempts = 0) {
try {
const videos = await getLocalChannelVideos(channel.id)
if (videos === null) {
this.errorChannels.push(channel)
return []
}
videos.map(video => {
if (video.liveNow) {
video.publishedDate = new Date().getTime()
} else if (video.isUpcoming) {
video.publishedDate = video.premiereDate
} else {
video.publishedDate = calculatePublishedDate(video.publishedText)
}
return video
})
return videos
} catch (err) {
console.error(err)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${err}`, 10000, () => {
copyToClipboard(err)
})
switch (failedAttempts) {
case 0:
return await this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
case 1:
if (this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
return await this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return await this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
default:
return []
}
}
},
getChannelVideosLocalRSS: async function (channel, failedAttempts = 0) {
const feedUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channel.id}`
try {
const response = await fetch(feedUrl)
if (response.status === 404) {
this.errorChannels.push(channel)
return []
}
return await this.parseYouTubeRSSFeed(await response.text(), channel.id)
} catch (error) {
console.error(error)
const errorMessage = this.$t('Local API Error (Click to copy)')
showToast(`${errorMessage}: ${error}`, 10000, () => {
copyToClipboard(error)
})
switch (failedAttempts) {
case 0:
return this.getChannelVideosLocalScraper(channel, failedAttempts + 1)
case 1:
if (this.backendFallback) {
showToast(this.$t('Falling back to Invidious API'))
return this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return this.getChannelVideosLocalScraper(channel, failedAttempts + 1)
default:
return []
}
}
},
getChannelVideosInvidiousScraper: function (channel, failedAttempts = 0) {
return new Promise((resolve, reject) => {
const subscriptionsPayload = {
resource: 'channels/latest',
id: channel.id,
params: {}
}
invidiousAPICall(subscriptionsPayload).then(async (result) => {
resolve(await Promise.all(result.videos.map((video) => {
if (video.liveNow) {
video.publishedDate = new Date().getTime()
} else if (video.isUpcoming) {
video.publishedDate = new Date(video.premiereTimestamp * 1000)
} else {
video.publishedDate = new Date(video.published * 1000)
}
return video
})))
}).catch((err) => {
console.error(err)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${err.responseText}`, 10000, () => {
copyToClipboard(err.responseText)
})
switch (failedAttempts) {
case 0:
resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1))
break
case 1:
if (process.env.IS_ELECTRON && this.backendFallback) {
showToast(this.$t('Falling back to the local API'))
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
} else {
resolve([])
}
break
case 2:
resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1))
break
default:
resolve([])
}
})
})
},
getChannelVideosInvidiousRSS: async function (channel, failedAttempts = 0) {
const feedUrl = `${this.currentInvidiousInstance}/feed/channel/${channel.id}`
try {
const response = await fetch(feedUrl)
if (response.status === 500) {
this.errorChannels.push(channel)
return []
}
return await this.parseYouTubeRSSFeed(await response.text(), channel.id)
} catch (error) {
console.error(error)
const errorMessage = this.$t('Invidious API Error (Click to copy)')
showToast(`${errorMessage}: ${error}`, 10000, () => {
copyToClipboard(error)
})
switch (failedAttempts) {
case 0:
return this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
case 1:
if (process.env.IS_ELECTRON && this.backendFallback) {
showToast(this.$t('Falling back to the local API'))
return this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
} else {
return []
}
case 2:
return this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
default:
return []
}
}
},
async parseYouTubeRSSFeed(rssString, channelId) {
const xmlDom = new DOMParser().parseFromString(rssString, 'application/xml')
const channelName = xmlDom.querySelector('author > name').textContent
const entries = xmlDom.querySelectorAll('entry')
const promises = []
for (const entry of entries) {
promises.push(this.parseRSSEntry(entry, channelId, channelName))
}
return await Promise.all(promises)
},
async parseRSSEntry(entry, channelId, channelName) {
const published = new Date(entry.querySelector('published').textContent)
return {
authorId: channelId,
author: channelName,
// querySelector doesn't support xml namespaces so we have to use getElementsByTagName here
videoId: entry.getElementsByTagName('yt:videoId')[0].textContent,
title: entry.querySelector('title').textContent,
publishedDate: published,
publishedText: published.toLocaleString(),
viewCount: entry.getElementsByTagName('media:statistics')[0]?.getAttribute('views') || null,
type: 'video',
lengthSeconds: '0:00',
isRSS: true
}
},
increaseLimit: function () {
this.dataLimit += 100
sessionStorage.setItem('subscriptionLimit', this.dataLimit)
},
/**
* This function `keyboardShortcutHandler` should always be at the bottom of this file
* @param {KeyboardEvent} event the keyboard event
* @param {KeyboardEvent} event
* @param {string} currentTab
*/
keyboardShortcutHandler: function (event) {
if (event.ctrlKey || document.activeElement.classList.contains('ft-input')) {
return
focusTab: function (event, currentTab) {
if (!event.altKey) {
event.preventDefault()
const visibleTabs = this.visibleTabs
if (visibleTabs.length === 1) {
this.$emit('showOutlines')
return
}
let index = visibleTabs.indexOf(currentTab)
if (event.key === 'ArrowLeft') {
index--
} else {
index++
}
if (index < 0) {
index = visibleTabs.length - 1
} else if (index > visibleTabs.length - 1) {
index = 0
}
this.$refs[visibleTabs[index]].focus()
this.$emit('showOutlines')
}
// Avoid handling events due to user holding a key (not released)
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/repeat
if (event.repeat) { return }
switch (event.key) {
case 'r':
case 'R':
if (!this.isLoading) {
this.loadVideosForSubscriptionsFromRemote()
}
break
}
},
...mapActions([
'updateShowProgressBar',
'updateSubscriptionsCacheForOneChannel',
]),
...mapMutations([
'setProgressBarPercentage'
])
}
}
})

View File

@ -1,75 +1,80 @@
<template>
<div>
<ft-loader
v-if="isLoading"
:fullscreen="true"
/>
<ft-card
v-else
class="card"
>
<div
v-if="errorChannels.length !== 0"
>
<h3> {{ $t("Subscriptions.Error Channels") }}</h3>
<div>
<ft-channel-bubble
v-for="(channel, index) in errorChannels"
:key="index"
:channel-name="channel.name"
:channel-id="channel.id"
:channel-thumbnail="channel.thumbnail"
class="channelBubble"
@click="goToChannel(channel.id)"
/>
</div>
</div>
<ft-card class="card">
<h3>{{ $t("Subscriptions.Subscriptions") }}</h3>
<ft-flex-box
v-if="activeVideoList.length === 0"
class="subscriptionTabs"
role="tablist"
:aria-label="$t('Subscriptions.Subscriptions Tabs')"
>
<p
v-if="activeSubscriptionList.length === 0"
class="message"
<div
v-if="!hideSubscriptionsVideos"
ref="videos"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'videos')"
aria-controls="subscriptionsPanel"
:tabindex="currentTab === 'videos' ? 0 : -1"
:class="{ selectedTab: currentTab === 'videos' }"
@click="changeTab('videos')"
@keydown.space.enter.prevent="changeTab('videos')"
@keydown.left.right="focusTab($event, 'videos')"
>
{{ $t("Subscriptions['Your Subscription list is currently empty. Start adding subscriptions to see them here.']") }}
</p>
<p
v-else-if="!fetchSubscriptionsAutomatically && !attemptedFetch"
class="message"
{{ $t("Global.Videos").toUpperCase() }}
</div>
<div
v-if="!hideSubscriptionsShorts"
ref="shorts"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'shorts')"
aria-controls="subscriptionsPanel"
:tabindex="currentTab === 'shorts' ? 0 : -1"
:class="{ selectedTab: currentTab === 'shorts' }"
@click="changeTab('shorts')"
@keydown.space.enter.prevent="changeTab('shorts')"
@keydown.left.right="focusTab($event, 'shorts')"
>
{{ $t("Subscriptions.Disabled Automatic Fetching") }}
</p>
<p
v-else
class="message"
{{ $t("Global.Shorts").toUpperCase() }}
</div>
<div
v-if="!hideSubscriptionsLive"
ref="live"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'live')"
aria-controls="subscriptionsPanel"
:tabindex="currentTab === 'live' ? 0 : -1"
:class="{ selectedTab: currentTab === 'live' }"
@click="changeTab('live')"
@keydown.space.enter.prevent="changeTab('live')"
@keydown.left.right="focusTab($event, 'live')"
>
{{ $t("Subscriptions.Empty Channels") }}
</p>
{{ $t("Global.Live").toUpperCase() }}
</div>
</ft-flex-box>
<ft-element-list
v-else
:data="activeVideoList"
<subscriptions-videos
v-if="currentTab === 'videos'"
id="subscriptionsPanel"
role="tabpanel"
/>
<ft-flex-box>
<ft-button
v-if="videoList.length > dataLimit"
:label="$t('Subscriptions.Load More Videos')"
background-color="var(--primary-color)"
text-color="var(--text-with-main-color)"
@click="increaseLimit"
/>
</ft-flex-box>
<subscriptions-shorts
v-if="currentTab === 'shorts'"
id="subscriptionsPanel"
role="tabpanel"
/>
<subscriptions-live
v-if="currentTab === 'live'"
id="subscriptionsPanel"
role="tabpanel"
/>
<p v-if="currentTab === null">
{{ $t("Subscriptions.All Subscription Tabs Hidden", {
subsection: $t('Settings.Distraction Free Settings.Sections.Subscriptions Page'),
settingsSection: $t('Settings.Distraction Free Settings.Distraction Free Settings')
}) }}
</p>
</ft-card>
<ft-icon-button
v-if="!isLoading"
:icon="['fas', 'sync']"
class="floatingTopButton"
:title="$t('Subscriptions.Refresh Subscriptions')"
:size="12"
theme="primary"
@click="loadVideosForSubscriptionsFromRemote"
/>
</div>
</template>

View File

@ -42,6 +42,7 @@
<ft-element-list
v-if="activeData.length > 0 && !isLoading"
:data="activeData"
:use-channels-hidden-preference="false"
/>
<ft-flex-box
v-if="showLoadMoreButton"

View File

@ -361,7 +361,7 @@ export default defineComponent({
this.isUpcoming = !!result.basic_info.is_upcoming
this.isLiveContent = !!result.basic_info.is_live_content
const subCount = parseLocalSubscriberCount(result.secondary_info.owner.subscriber_count.text)
const subCount = !result.secondary_info.owner.subscriber_count.isEmpty() ? parseLocalSubscriberCount(result.secondary_info.owner.subscriber_count.text) : NaN
if (!isNaN(subCount)) {
this.channelSubscriptionCountText = formatNumber(subCount, subCount >= 10000 ? { notation: 'compact' } : undefined)
@ -407,6 +407,17 @@ export default defineComponent({
this.liveChat = null
}
// region No comment detection
// For videos without any comment (comment disabled?)
// e.g. https://youtu.be/8NBSwDEf8a8
//
// `comments_entry_point_header` is null probably when comment disabled
// e.g. https://youtu.be/8NBSwDEf8a8
// However videos with comments enabled but have no comment
// are different (which is not detected here)
this.commentsEnabled = result.comments_entry_point_header != null
// endregion No comment detection
// the bypassed result is missing some of the info that we extract in the code above
// so we only overwrite the result here
// we need the bypassed result for the streaming data and the subtitles
@ -509,6 +520,7 @@ export default defineComponent({
}
this.adaptiveFormats = this.videoSourceList
/** @type {import('../../helpers/api/local').LocalFormat[]} */
const formats = [...result.streaming_data.formats, ...result.streaming_data.adaptive_formats]
this.downloadLinks = formats.map((format) => {
const qualityLabel = format.quality_label ?? format.bitrate
@ -525,7 +537,7 @@ export default defineComponent({
}
return {
url: format.url,
url: format.freeTubeUrl,
label: label
}
})
@ -670,17 +682,6 @@ export default defineComponent({
}
}
// region No comment detection
// For videos without any comment (comment disabled?)
// e.g. https://youtu.be/8NBSwDEf8a8
//
// `comments_entry_point_header` is null probably when comment disabled
// e.g. https://youtu.be/8NBSwDEf8a8
// However videos with comments enabled but have no comment
// are different (which is not detected here)
this.commentsEnabled = result.comments_entry_point_header != null
// endregion No comment detection
this.isLoading = false
this.updateTitle()
} catch (err) {
@ -716,7 +717,7 @@ export default defineComponent({
this.videoTitle = result.title
this.videoViewCount = result.viewCount
this.channelSubscriptionCountText = result.subCountText || 'FT-0'
this.channelSubscriptionCountText = isNaN(result.subCountText) ? '' : result.subCountText
if (this.hideVideoLikesAndDislikes) {
this.videoLikeCount = null
this.videoDislikeCount = null
@ -946,7 +947,7 @@ export default defineComponent({
},
/**
* @param {import('youtubei.js').Misc.Format[]} audioFormats
* @param {import('../../helpers/api/local').LocalFormat[]} audioFormats
* @returns {AudioSource[]}
*/
createLocalAudioSourceList: function (audioFormats) {
@ -973,7 +974,7 @@ export default defineComponent({
}
return {
url: format.url,
url: format.freeTubeUrl,
type: format.mime_type,
label: 'Audio',
qualityLabel: label

View File

@ -113,6 +113,16 @@
height: 500px;
}
.watchVideoPlaylist {
:deep(.videoThumbnail) {
margin-top: auto;
margin-bottom: auto;
}
@media (max-width: 768px) {
height: auto;
}
}
.watchVideoRecommendations,
.theatreRecommendations {
margin: 0 0 16px;

View File

@ -58,5 +58,23 @@
"playlistShuffle": "--mpv-shuffle",
"playlistLoop": "--mpv-loop-playlist"
}
},
{
"name": "SMPlayer",
"nameTranslationKey": "Settings.External Player Settings.Players.SMPlayer.Name",
"value": "smplayer",
"cmdArguments": {
"defaultExecutable": "smplayer",
"defaultCustomArguments": null,
"supportsYtdlProtocol": true,
"videoUrl": "",
"playlistUrl": "",
"startOffset": "-start",
"playbackRate": null,
"playlistIndex": null,
"playlistReverse": null,
"playlistShuffle": null,
"playlistLoop": null
}
}
]

View File

@ -29,6 +29,13 @@ Close: 'إغلاق'
Back: 'رجوع'
Forward: 'إلى الأمام'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'الفيديوهات'
Shorts: القصيرة
Live: مباشر
# Search Bar
Search / Go to URL: 'ابحث / اذهب إلى رابط'
# In Filter Button
@ -82,6 +89,9 @@ Subscriptions:
Disabled Automatic Fetching: لقد قمت بتعطيل الجلب التلقائي للاشتراك. قم بتحديث الاشتراكات
لرؤيتها هنا.
Empty Channels: لا تحتوي قنواتك التي اشتركت فيها حاليا على أي مقاطع فيديو.
All Subscription Tabs Hidden: جميع علامات تبويب الاشتراك مخفية. لمشاهدة المحتوى
هنا ، يرجى إظهار بعض علامات التبويب في قسم "{subection}" في "{settingsSection}".
Subscriptions Tabs: تبويب الاشتراكات
Trending:
Trending: 'المحتوى الرائج'
Trending Tabs: علامات التبويب الشائعة
@ -402,6 +412,12 @@ Settings:
Channel Page: صفحة القناة
Watch Page: صفحة المشاهدة
General: عام
Subscriptions Page: صفحة الاشتراكات
Hide Channel Podcasts: إخفاء بودكاست القناة
Hide Channel Releases: إخفاء إصدارات القناة
Hide Subscriptions Live: إخفاء الاشتراكات مباشرة
Hide Subscriptions Shorts: إخفاء الاشتراكات الفيدوهات القصيرة
Hide Subscriptions Videos: إخفاء مقاطع فيديو الاشتراكات
The app needs to restart for changes to take effect. Restart and apply change?: البرنامج
يحتاج لإعادة التشغيل كي يسري مفعول التغييرات. هل تريد إعادة التشغيل و تطبيق التغييرات؟
Proxy Settings:
@ -621,9 +637,16 @@ Channel:
This channel does not currently have any live streams: لا يوجد حاليا أي بث مباشر
على هذه القناة
Shorts:
Shorts: القصيرة
This channel does not currently have any shorts: هذه القناة ليس لديها حاليا أي
أفلام قصيرة (shorts)
Podcasts:
Podcasts: البودكاست
This channel does not currently have any podcasts: لا تحتوي هذه القناة حاليا على
أي بودكاست
Releases:
Releases: الاصدارات
This channel does not currently have any releases: هذه القناة ليس لديها أي إصدارات
حاليا
Video:
Mark As Watched: 'علّمه كفيديو تمت مشاهدته'
Remove From History: 'إزالة من سجلّ المشاهدة'
@ -924,9 +947,11 @@ Tooltips:
وتمكين ذاكرة تخزين مؤقت للصور في الذاكرة. سيؤدي إلى زيادة استخدام ذاكرة الوصول
العشوائي.
Distraction Free Settings:
Hide Channels: أدخل اسم القناة أو رقم تعريف القناة لإخفاء كل الفيديوهات وقوائم
التشغيل والقناة نفسها من الظهور في "بحث Google" أو "المحتوى الرائج". يجب أن
يكون اسم القناة الذي تم إدخاله مطابقا تماما وحساسا لحالة الأحرف.
Hide Channels: أدخل اسم قناة أو معرّف القناة لإخفاء جميع مقاطع الفيديو وقوائم
التشغيل والقناة نفسها من الظهور في البحث والشهرة والأكثر شعبية والموصى بها.
يجب أن يكون اسم القناة الذي تم إدخاله مطابقًا تمامًا وحساسًا لحالة الأحرف.
Hide Subscriptions Live: يتم تجاوز هذا الإعداد من خلال إعداد "{appWideSetting}"
على مستوى التطبيق، في قسم "{subsection}" من "{settingsSection}"
SponsorBlock Settings:
UseDeArrowTitles: استبدل عناوين الفيديو بالعناوين التي أرسلها المستخدم من DeArrow.
This video is unavailable because of missing formats. This can happen due to country unavailability.: هذا

View File

@ -30,6 +30,13 @@ Close: 'Затваряне'
Back: 'Назад'
Forward: 'Напред'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'видеа'
Shorts: Кратки видеа
Live: На живо
Version {versionNumber} is now available! Click for more details: 'Версия {versionNumber}
е вече налична! Щракнете за повече детайли'
Download From Site: 'Сваляне от сайта'
@ -90,6 +97,10 @@ Subscriptions:
Disabled Automatic Fetching: Автоматичното извличане на абонаменти е деактивирано.
Обновете абонаментите, за да ги видите тук.
Empty Channels: Каналите, за които сте абонирани, в момента нямат никакви видеа.
Subscriptions Tabs: Раздели с абонаменти
All Subscription Tabs Hidden: Всички раздели на абонамента са скрити. За да виждате
съдържанието тук, моля, премахнете скриването на някои раздели в секция "{subsection}"
на "{settingsSection}".
Trending:
Trending: 'Набиращи популярност'
Trending Tabs: Раздели за набиращи популярност
@ -102,11 +113,11 @@ Playlists: 'Плейлисти'
User 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: Тази страница не отразява напълно работещи плейлисти. В него са
изброени само видеоклипове, които сте запазили или избрали за любими. Когато работата
приключи, всички видеоклипове, които в момента са тук, ще бъдат мигрирани в плейлист
Любими“.
изброени само видеа, които сте запазили или избрали за предпочитани. Когато работата
приключи, всички видеа, които в момента са тук, ще бъдат преместени в плейлист
Предпочитани“.
Search bar placeholder: Търсене в плейлиста
Empty Search Message: В този плейлист няма видеа, които да отговарят на търсенето
ви
@ -412,6 +423,12 @@ Settings:
Side Bar: Странична лента
Watch Page: Страница за гледане
General: Общи
Subscriptions Page: Страница с абонаменти
Hide Channel Releases: Скриване на изданията на канала
Hide Subscriptions Videos: Скриване на видеата в абонаментите
Hide Subscriptions Shorts: Скриване на кратките видеа в абонаментите
Hide Subscriptions Live: Скриване на абонаментите на живо
Hide Channel Podcasts: Скриване на подкастите на канала
The app needs to restart for changes to take effect. Restart and apply change?: Приложението
трябва да се рестартира за да се приложат промените. Рестартиране?
Proxy Settings:
@ -445,6 +462,7 @@ Settings:
Prompt To Skip: Подкана за пропускане
Do Nothing: Не правете нищо
Category Color: Категория Цвят
UseDeArrowTitles: Използване на DeArrow за заглавия на видео
External Player Settings:
Custom External Player Arguments: Персонализирани аргументи за външен плейър
Custom External Player Executable: Персонализирано изпълнение на външен плейър
@ -638,7 +656,14 @@ Channel:
Shorts:
This channel does not currently have any shorts: В момента този канал няма никакви
кратки видеа
Shorts: Кратки видеа
Podcasts:
Podcasts: Подкасти
This channel does not currently have any podcasts: В момента този канал няма никакви
подкасти
Releases:
Releases: Издания
This channel does not currently have any releases: В момента този канал няма никакви
издания
Video:
Mark As Watched: 'Отбелязване като гледано'
Remove From History: 'Премахване от историята'
@ -725,8 +750,8 @@ Video:
Open Channel in YouTube: Отваряне на канала в YouTube
Started streaming on: Начало на предаването
Streamed on: На живо на
Video has been removed from your saved list: Видеоклипът е премахнат от вашия запазен
списък
Video has been removed from your saved list: Видеото е премахнато от вашия списък
със запазени
Video has been saved: Видеото е запазено
Save Video: Запазване на видео
translated from English: преведено от английски
@ -765,7 +790,7 @@ Video:
buffered: Буферирани
out of: от
Resolution: Резолюция
Video ID: Идентификатор на видеоклипа
Video ID: Идентификатор на видеото
Player Dimensions: Размери на плейъра
Bitrate: Побитова скорост
Bandwidth: Пропускателна способност
@ -774,7 +799,7 @@ Video:
Mimetype: MIME тип
Buffered: Буферирани
Video statistics are not available for legacy videos: Статистиката не е налична
за наследени видеоклипове
за наследени видеа
Premieres on: Премиера на
Premieres in: Премиери в
Premieres: Премиерa
@ -898,7 +923,7 @@ Tooltips:
избран Invidious интерфейс. Когато е активиран, локалният интерфейс ще използва
старите формати вместо тези на Invidious. Това помага когато видеата, получавани
от Invidious не вървят поради регионални ограничения.
Scroll Playback Rate Over Video Player: Докато курсорът е върху видеоклипа, натиснете
Scroll Playback Rate Over Video Player: Докато курсорът е върху видеото, натиснете
и задръжте клавиша Control (Command Key за Mac) и превъртете колелцето на мишката
напред или назад, за да контролирате скоростта на възпроизвеждане. Натиснете
и задръжте клавиша Control (Command Key за Mac) и щракнете с левия бутон на
@ -948,9 +973,14 @@ Tooltips:
на RAM паметта.
Distraction Free Settings:
Hide Channels: Въведете име или идентификатор на канал, за да скриете всички видеа,
плейлисти и самия канал от показване в търсенето или набиращите популярност.
Въведеното име трябва да съвпада напълно и е чувствително към главни и малки
букви.
плейлисти и самия канал от показване в търсенето, тенденциите, най-популярните
и препоръчаните. Въведеното име трябва да съвпада напълно и е чувствително към
главни и малки букви.
Hide Subscriptions Live: Тази настройка се отменя от настройката за цялото приложение
"{appWideSetting}" в секция "{subsection}" на "{settingsSection}"
SponsorBlock Settings:
UseDeArrowTitles: Заменя заглавията на видеата с подадени от потребителите заглавия
от DeArrow.
More: Още
Playing Next Video Interval: Пускане на следващото видео веднага. Щракнете за отказ.
| Пускане на следващото видео след {nextVideoInterval} секунда. Щракнете за отказ.

View File

@ -30,6 +30,11 @@ Close: 'Tancar'
Back: 'Enrere'
Forward: 'Endavant'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Vídeos'
Version {versionNumber} is now available! Click for more details: 'La versió {versionNumber}
està disponible! Fes clic per a més detalls'
Download From Site: 'Descarrega des del web'

View File

@ -29,6 +29,13 @@ Close: 'Zavřít'
Back: 'Zpět'
Forward: 'Dopředu'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videa'
Shorts: Shorts
Live: Živě
Version {versionNumber} is now available! Click for more details: 'Verze {versionNumber}
je k dispozici! Klikněte pro více informací'
Download From Site: 'Stáhnout ze stránky'
@ -89,6 +96,10 @@ Subscriptions:
Empty Channels: Vaše odebírané kanály momentálně nemají žádná videa.
Disabled Automatic Fetching: Máte zakázané automatické získávání odběrů. Obnovte
odběry, abyste je zde mohli vidět.
Subscriptions Tabs: Karty s odběry
All Subscription Tabs Hidden: Všechny karty předplatného jsou skryté. Chcete-li
zde zobrazit obsah, zrušte prosím skrytí některých záložek v sekci „{subsection}“
v „{settingsSection}“.
Trending:
Trending: 'Trendy'
Trending Tabs: Tabulka trendů
@ -334,6 +345,12 @@ Settings:
Channel Page: Stránka kanálu
Watch Page: Stránka sledování
General: Obecné
Subscriptions Page: Stránka s odběry
Hide Channel Podcasts: Skrýt podcasty kanálu
Hide Channel Releases: Skrýt vydání kanálu
Hide Subscriptions Shorts: Skrýt Shorts odběrů
Hide Subscriptions Videos: Skrýt videa odběrů
Hide Subscriptions Live: Skrýt živá vysílání odběrů
Data Settings:
Data Settings: 'Nastavení dat'
Select Import Type: 'Vybrat typ importu'
@ -436,6 +453,7 @@ Settings:
Prompt To Skip: Zeptat se na přeskočení
Show In Seek Bar: Zobrazit v liště
Category Color: Barva kategorie
UseDeArrowTitles: Použít názvy videí z DeArrow
External Player Settings:
Custom External Player Arguments: Argumenty vlastního externího přehrávače
Custom External Player Executable: Spustitelný vlastní externí přehrávač
@ -622,7 +640,14 @@ Channel:
době nemá žádné živé přenosy
Shorts:
This channel does not currently have any shorts: Tento kanál nemá žádné shorts
Shorts: Shorts
Releases:
Releases: Vydání
This channel does not currently have any releases: Tento kanál momentálně nemá
žádná vydání
Podcasts:
This channel does not currently have any podcasts: Tento kanál momentálně nemá
žádné podcasty
Podcasts: Podcasty
Video:
Mark As Watched: 'Označit jako zhlédnuto'
Remove From History: 'Odstranit z historie'
@ -905,8 +930,13 @@ Tooltips:
mezipaměť pro obrázky v paměti. Povede to ke zvýšenému využití RAM.
Distraction Free Settings:
Hide Channels: Zadejte název nebo ID kanálu pro skrytí všech videí, playlistů
a samotného kanálu před zobrazením ve vyhledávání nebo trendech. Zadaný název
kanálu se musí zcela shodovat a rozlišují se v něm velká a malá písmena.
a samotného kanálu před zobrazením ve vyhledávání, trendech, nejpopulárnějších
a doporučených. Zadaný název kanálu se musí zcela shodovat a rozlišují se v
něm velká a malá písmena.
Hide Subscriptions Live: Toto nastavení je nadřazeno nastavením celé aplikace
„{appWideSetting}“ v části „{subsection}“ v části „{settingsSection}“
SponsorBlock Settings:
UseDeArrowTitles: Nahradit názvy videí vlastními názvy od uživatelů DeArrow.
Local API Error (Click to copy): 'Chyba lokálního API (kliknutím zkopírujete)'
Invidious API Error (Click to copy): 'Chyba Invidious API (kliknutím zkopírujete)'
Falling back to Invidious API: 'Přepínám na Invidious API'

View File

@ -29,6 +29,11 @@ Close: 'Luk'
Back: 'Tilbage'
Forward: 'Fremad'
# Global
# Anything shared among components / views should be put here
Global:
Videos: Videoer
Version {versionNumber} is now available! Click for more details: 'Version {versionNumber}
er nu tilgængelig! Klik for flere detaljer'
Download From Site: 'Hent Fra Netsted'
@ -60,12 +65,14 @@ Search Filters:
Videos: 'Videoer'
Channels: 'Kanaler'
#& Playlists
Movies: Film
Duration:
Duration: 'Længde'
All Durations: 'Alle Længder'
Short (< 4 minutes): 'Kort (< 4 minutter)'
Long (> 20 minutes): 'Lang (> 20 minutter)'
# On Search Page
Medium (4 - 20 minutes): Medium (4-20 minutter)
Search Results: 'Søgeresultater'
Fetching results. Please wait: 'Skaffer resultater. Vent venligst'
Fetch more results: 'Skaf flere resultater'
@ -83,8 +90,11 @@ Subscriptions:
Refresh Subscriptions: 'Genopfrisk Abonnementer'
Load More Videos: Indlæs Flere Videoer
This profile has a large number of subscriptions. Forcing RSS to avoid rate limiting: Denne
profil har et stort antal abonnementer. Tvinger RSS for at undgå adgangsbegrænsning
profil har et stort antal abonnementer. Gennemtvinger RSS for at undgå adgangsbegrænsning
Error Channels: Kanaler med Fejl
Empty Channels: De kanaler, du abonnerer på, har i øjeblikket ingen videoer.
Disabled Automatic Fetching: Du har deaktiveret automatisk hentning af abonnementer.
Opdater abonnementer for at se dem her.
Trending:
Trending: 'Trender'
Music: Musik
@ -263,6 +273,7 @@ Settings:
Video Playback Rate Interval: Videoafspilningshastighed Interval
Scroll Playback Rate Over Video Player: Scroll-afspilningshastighed i Videoafspiller
Scroll Volume Over Video Player: Scroll-lydstyrke i Videoafspiller
Skip by Scrolling Over Video Player: Spring over ved at scrolle over videoafspilleren
Privacy Settings:
Privacy Settings: 'Privatlivsindstillinger'
Remember History: 'Husk Historik'
@ -592,8 +603,8 @@ Video:
i øjeblikket ikke i dette build.'
'Chat is disabled or the Live Stream has ended.': 'Chat er deaktiveret, eller Livestream
er slut.'
Live chat is enabled. Chat messages will appear here once sent.: 'Direkte chat
er aktiveret. Chatbeskeder vil fremgå her, når de er sendt.'
Live chat is enabled. Chat messages will appear here once sent.: 'Direkte chat er
aktiveret. Chatbeskeder vil fremgå her, når de er sendt.'
'Live Chat is currently not supported with the Invidious API. A direct connection to YouTube is required.': 'Direkte
Chat understøttes i øjeblikket ikke med Invidious-APIen. Direkte forbindelse
til YouTube kræves.'
@ -815,9 +826,9 @@ Tooltips:
Fallback to Non-Preferred Backend on Failure: Når din foretrukne API oplever et
problem, vil FreeTube automatisk forsøge at bruge din ikke-foretrukne API som
reservemetode, når det er aktiveret.
External Link Handling: "Vælg standardopførslen for når du klikker på et link,\
\ der ikke kan blive åbnet i FreeTube.\nSom standard åbner FreeTube det link,\
\ du klikker på, i din standardbrowser.\n"
External Link Handling: "Vælg standardopførslen for når du klikker på et link,
der ikke kan blive åbnet i FreeTube.\nSom standard åbner FreeTube det link,
du klikker på, i din standardbrowser.\n"
External Player Settings:
Custom External Player Executable: Som standard antager FreeTube at den valgte
eksterne afspiller kan findes via miljøvariablet PATH. En brugerdefineret sti
@ -871,3 +882,4 @@ Screenshot Success: Gemte skærmbillede som "{filePath}"
Playing Next Video Interval: Afspiller næste video om lidt. Klik for at afbryde. |
Afspiller næste video om {nextVideoInterval} sekund. Klik for at afbryde. | Afspiller
næste video om {nextVideoInterval} sekunder. Klik for at afbryde.
Preferences: Præferencer

View File

@ -28,6 +28,13 @@ Close: Schließen
Back: Zurück
Forward: Vorwärts
# Global
# Anything shared among components / views should be put here
Global:
Videos: Videos
Shorts: Shorts
Live: Live
# Search Bar
Search / Go to URL: Suche / Geh zu URL
# In Filter Button
@ -85,6 +92,10 @@ Subscriptions:
Disabled Automatic Fetching: Du hast den automatischen Abruf von Abonnements deaktiviert.
Aktualisiere die Abonnements, um sie hier zu sehen.
Empty Channels: Deine abonnierten Kanäle haben derzeit keine Videos.
Subscriptions Tabs: Registerkarten für Abonnements
All Subscription Tabs Hidden: Alle Registerkarten für Abonnements sind ausgeblendet.
Um den Inhalt hier zu sehen, blenden Sie bitte einige Registerkarten im Abschnitt
„{subsection}“ in „{settingsSection}“ ein.
Trending:
Trending: Trends
Trending Tabs: Trendtabs
@ -416,6 +427,12 @@ Settings:
Channel Page: Kanalseite
General: Allgemein
Watch Page: Seite beobachten
Subscriptions Page: Abonnements-Seite
Hide Channel Podcasts: Kanal-Podcasts ausblenden
Hide Subscriptions Videos: Videos der Abonnements ausblenden
Hide Subscriptions Shorts: Shorts der Abonnements ausblenden
Hide Channel Releases: Kanalveröffentlichungen ausblenden
Hide Subscriptions Live: Live der Abonnements ausblenden
The app needs to restart for changes to take effect. Restart and apply change?: Um
die Änderungen anzuwenden muss die Anwendung neustarten. Jetzt neustarten und
Änderungen aktivieren?
@ -449,6 +466,7 @@ Settings:
Prompt To Skip: Aufforderung zum Überspringen
Do Nothing: Nichts tun
Category Color: Kategoriefarbe
UseDeArrowTitles: DeArrow-Video-Titel verwenden
External Player Settings:
Ignore Unsupported Action Warnings: Nicht unterstützte Aktionswarnungen ignorieren
External Player: Externer Player
@ -609,9 +627,16 @@ Channel:
This channel does not currently have any live streams: Dieser Kanal hat derzeit
keine Live-Streams
Shorts:
Shorts: Shorts
This channel does not currently have any shorts: Dieser Kanal hat derzeit keine
Shorts
Podcasts:
Podcasts: Podcasts
This channel does not currently have any podcasts: Dieser Kanal hat aktuell keine
Podcasts
Releases:
Releases: Veröffentlichungen
This channel does not currently have any releases: Dieser Kanel hat aktuell keine
Veröffentlichungen
Video:
Open in YouTube: In YouTube öffnen
Copy YouTube Link: YouTube-Link kopieren
@ -974,10 +999,17 @@ Tooltips:
und aktiviert einen benutzerdefinierten In-Memory-Image-Cache. Dies führt zu
einer erhöhten Nutzung des Direktzugriffsspeichers.
Distraction Free Settings:
Hide Channels: Geben Sie einen Kanalnamen oder eine Kanal-ID ein, um alle Videos,
Wiedergabelisten und den Kanal selbst vor der Anzeige in der Suche oder den
Trends zu verbergen. Der eingegebene Kanalname muss vollständig übereinstimmen
und es wird zwischen Groß- und Kleinschreibung unterschieden.
Hide Channels: Geben Sie einen Kanalnamen oder eine Kanal-ID ein, um zu verhindern,
dass alle Videos, Wiedergabelisten und der Kanal selbst in der Suche, den Trends,
den beliebtesten und den empfohlenen Videos angezeigt werden. Der eingegebene
Kanalname muss vollständig übereinstimmen und es wird zwischen Groß- und Kleinschreibung
unterschieden.
Hide Subscriptions Live: Diese Einstellung wird durch die app-weite Einstellung
„{appWideSetting}“ im Abschnitt „{subsection}“ des Abschnitts „{settingsSection}“
außer Kraft gesetzt
SponsorBlock Settings:
UseDeArrowTitles: Ersetzen Sie Videotitel durch von Benutzern eingereichte Titel
von DeArrow.
Playing Next Video Interval: Nächstes Video wird sofort abgespielt. Zum Abbrechen
klicken. | Nächstes Video wird in {nextVideoInterval} Sekunden abgespielt. Zum Abbrechen
klicken. | Nächstes Video wird in {nextVideoInterval} Sekunden abgespielt. Zum Abbrechen

View File

@ -30,6 +30,13 @@ Close: 'Κλείσιμο'
Back: 'Μετάβαση πίσω'
Forward: 'Μετάβαση μπροστά'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Βίντεο'
Shorts: Shorts
Live: Ζωντανά
# Search Bar
Search / Go to URL: 'Αναζήτηση/Μετάβαση στη URL'
# In Filter Button
@ -86,6 +93,10 @@ Subscriptions:
Disabled Automatic Fetching: Έχετε απενεργοποιήσει την αυτόματη ανάκτηση συνδρομής.
Ανανεώστε τις συνδρομές για να τις δείτε εδώ.
Empty Channels: Τα εγγεγραμμένα κανάλια σας προς το παρόν δεν έχουν βίντεο.
Subscriptions Tabs: Καρτέλες Συνδρομών
All Subscription Tabs Hidden: Όλες οι καρτέλες συνδρομής είναι κρυφές. Για να δείτε
περιεχόμενο εδώ, αποκρύψτε ορισμένες καρτέλες στην ενότητα "{subsection}" στο
"{settingsSection}".
Trending:
Trending: 'Τάσεις'
Gaming: Παιχνίδια
@ -413,10 +424,16 @@ Settings:
Channel Page: Σελίδα Καναλιού
Watch Page: Σελίδα Παρακολούθησης
General: Γενικά
Subscriptions Page: Σελίδα Συνδρομών
Hide Featured Channels: Απόκρυψη Προτεινόμενων Καναλιών
Hide Channel Playlists: Απόκρυψη Λιστών Αναπαραγωγής Καναλιών
Hide Channel Community: Απόκρυψη Κοινότητας Καναλιού
Hide Channel Shorts: Απόκρυψη Shorts Καναλιού
Hide Channel Releases: Απόκρυψη Kυκλοφοριών Kαναλιού
Hide Channel Podcasts: Απόκρυψη Podcasts Καναλιού
Hide Subscriptions Videos: Απόκρυψη Βίντεο Συνδρομών
Hide Subscriptions Shorts: Απόκρυψη Shorts Συνδρομών
Hide Subscriptions Live: Απόκρυψη Live Συνδρομών
The app needs to restart for changes to take effect. Restart and apply change?: Η
εφαρμογή πρέπει να κάνει επανεκκίνηση για να εφαρμοστούν οι αλλαγές. Επανεκκίνηση
και εφαρμογή αλλαγών;
@ -461,6 +478,7 @@ Settings:
Show In Seek Bar: Εμφάνιση Στη Γραμμή Αναζήτησης
Prompt To Skip: Προτροπή Για Παράλειψη
Category Color: Χρώμα Κατηγορίας
UseDeArrowTitles: Χρήση Τίτλων Βίντεο DeArrow
Download Settings:
Download Settings: Λήψη ρυθμίσεων
Ask Download Path: Ζητήστε τη διαδρομή λήψης
@ -652,9 +670,16 @@ Channel:
This channel does not currently have any live streams: Αυτό το κανάλι δεν έχει
προς το παρόν ζωντανές ροές
Shorts:
Shorts: Shorts
This channel does not currently have any shorts: Αυτό το κανάλι δεν έχει προς
το παρόν κανένα shorts
Releases:
Releases: Κυκλοφορίες
This channel does not currently have any releases: Αυτό το κανάλι δεν έχει προς
το παρόν κυκλοφορίες
Podcasts:
Podcasts: Podcasts
This channel does not currently have any podcasts: Αυτό το κανάλι δεν έχει αυτήν
τη στιγμή podcasts
Video:
Mark As Watched: 'Επισήμανση ως παρακολουθημένο'
Remove From History: 'Κατάργηση από το ιστορικό'
@ -986,8 +1011,14 @@ Tooltips:
Distraction Free Settings:
Hide Channels: Εισαγάγετε ένα όνομα καναλιού ή ένα αναγνωριστικό καναλιού για
να αποκρύψετε όλα τα βίντεο, τις λίστες αναπαραγωγής και το ίδιο το κανάλι ώστε
να μην εμφανίζονται στην αναζήτηση ή στις τάσεις. Το όνομα του καναλιού που
καταχωρίσατε πρέπει να ταιριάζει απόλυτα και να κάνει διάκριση πεζών-κεφαλαίων.
να μην εμφανίζονται στην αναζήτηση, στις τάσεις, στα πιο δημοφιλή και προτεινόμενα.
Το όνομα του καναλιού που καταχωρίσατε πρέπει να ταιριάζει απόλυτα και να κάνει
διάκριση πεζών-κεφαλαίων.
Hide Subscriptions Live: Αυτή η ρύθμιση παρακάμπτεται από τη ρύθμιση "{appWideSetting}"
σε όλη την εφαρμογή, στην ενότητα "{subsection}" του "{settingsSection}"
SponsorBlock Settings:
UseDeArrowTitles: Αντικαταστήστε τους τίτλους βίντεο με τίτλους που υποβλήθηκαν
από τους χρήστες από το DeArrow.
Playing Next Video Interval: Αναπαραγωγή επόμενου βίντεο άμεσα. Κάντε κλικ για ακύρωση.
| Αναπαραγωγή επόμενου βίντεο σε {nextVideoInterval} δευτερόλεπτο. Κάντε κλικ για
ακύρωση. | Αναπαραγωγή επόμενου βίντεο σε {nextVideoInterval} δευτερόλεπτα. Κάντε

View File

@ -39,6 +39,13 @@ A new blog is now available, {blogTitle}. Click to view more: A new blog is now
Click to view more
Are you sure you want to open this link?: Are you sure you want to open this link?
# Global
# Anything shared among components / views should be put here
Global:
Videos: Videos
Shorts: Shorts
Live: Live
# Search Bar
Search / Go to URL: Search / Go to URL
Search Bar:
@ -93,6 +100,8 @@ Subscriptions:
'Getting Subscriptions. Please wait.': Getting Subscriptions. Please wait.
Refresh Subscriptions: Refresh Subscriptions
Load More Videos: Load More Videos
Subscriptions Tabs: Subscriptions Tabs
All Subscription Tabs Hidden: 'All subscription tabs are hidden. To see content here, please unhide some tabs in the "{subsection}" section in "{settingsSection}".'
More: More
Channels:
Channels: Channels
@ -323,6 +332,7 @@ Settings:
Distraction Free Settings: Distraction Free Settings
Sections:
Side Bar: Side Bar
Subscriptions Page: Subscriptions Page
Channel Page: Channel Page
Watch Page: Watch Page
General: General
@ -349,6 +359,11 @@ Settings:
Hide Channel Playlists: Hide Channel Playlists
Hide Channel Community: Hide Channel Community
Hide Channel Shorts: Hide Channel Shorts
Hide Channel Podcasts: Hide Channel Podcasts
Hide Channel Releases: Hide Channel Releases
Hide Subscriptions Videos: Hide Subscriptions Videos
Hide Subscriptions Shorts: Hide Subscriptions Shorts
Hide Subscriptions Live: Hide Subscriptions Live
Data Settings:
Data Settings: Data Settings
Select Import Type: Select Import Type
@ -545,7 +560,6 @@ Channel:
Oldest: Oldest
Most Popular: Most Popular
Shorts:
Shorts: Shorts
This channel does not currently have any shorts: This channel does not currently have any shorts
Live:
Live: Live
@ -559,6 +573,12 @@ Channel:
Last Video Added: Last Video Added
Newest: Newest
Oldest: Oldest
Podcasts:
Podcasts: Podcasts
This channel does not currently have any podcasts: This channel does not currently have any podcasts
Releases:
Releases: Releases
This channel does not currently have any releases: This channel does not currently have any releases
About:
About: About
Channel Description: Channel Description
@ -836,8 +856,9 @@ Tooltips:
you want to be passed to the external player.
DefaultCustomArgumentsTemplate: "(Default: '{defaultCustomArguments}')"
Distraction Free Settings:
Hide Channels: Enter a channel name or channel ID to hide all videos, playlists and the channel itself from appearing in search or trending.
Hide Channels: Enter a channel name or channel ID to hide all videos, playlists and the channel itself from appearing in search, trending, most popular and recommended.
The channel name entered must be a complete match and is case sensitive.
Hide Subscriptions Live: 'This setting is overridden by the app-wide "{appWideSetting}" setting, in the "{subsection}" section of the "{settingsSection}"'
Subscription Settings:
Fetch Feeds from RSS: When enabled, FreeTube will use RSS instead of its default
method for grabbing your subscription feed. RSS is faster and prevents IP blocking,

View File

@ -29,6 +29,13 @@ Close: 'Close'
Back: 'Back'
Forward: 'Forward'
# Global
# Anything shared among components / views should be put here
Global:
Videos: Videos
Shorts: Shorts
Live: Live
Version {versionNumber} is now available! Click for more details: 'Version {versionNumber}
is now available! Click for more details'
Download From Site: 'Download From Site'
@ -88,6 +95,9 @@ Subscriptions:
Disabled Automatic Fetching: You have disabled automatic subscription fetching.
Refresh subscriptions to see them here.
Empty Channels: Your subscribed channels currently does not have any videos.
Subscriptions Tabs: Subscriptions tabs
All Subscription Tabs Hidden: All subscription tabs are hidden. To see content here,
please unhide some tabs in the {subsection} section in {settingsSection}.
Trending:
Trending: 'Trending'
Trending Tabs: Trending Tabs
@ -412,10 +422,16 @@ Settings:
Hide Channel Community: Hide channel community
Hide Channel Shorts: Hide channel shorts
Sections:
Side Bar: Side Bar
Side Bar: Side bar
General: General
Channel Page: Channel Page
Watch Page: Watch Page
Subscriptions Page: Subscriptions page
Hide Channel Podcasts: Hide channel podcasts
Hide Channel Releases: Hide channel releases
Hide Subscriptions Shorts: Hide subscriptions shorts
Hide Subscriptions Live: Hide subscriptions live
Hide Subscriptions Videos: Hide subscriptions videos
The app needs to restart for changes to take effect. Restart and apply change?: The
app needs to restart for changes to take effect. Do you want to restart and apply
the changes?
@ -448,6 +464,7 @@ Settings:
Prompt To Skip: Prompt to skip
Do Nothing: Do nothing
Category Color: Category colour
UseDeArrowTitles: Use DeArrow video titles
Download Settings:
Download Settings: Download Settings
Ask Download Path: Ask for download path
@ -611,9 +628,16 @@ Channel:
have any live streams
Live: Live
Shorts:
Shorts: Shorts
This channel does not currently have any shorts: This channel does not currently
have any shorts
Podcasts:
Podcasts: Podcasts
This channel does not currently have any podcasts: This channel does not currently
have any podcasts
Releases:
Releases: Releases
This channel does not currently have any releases: This channel does not currently
have any releases
Video:
Mark As Watched: 'Mark As Watched'
Remove From History: 'Remove From History'
@ -918,8 +942,12 @@ Tooltips:
in-memory image cache. Will lead to increased RAM usage.
Distraction Free Settings:
Hide Channels: Enter a channel name or channel ID to hide all videos, playlists
and the channel itself from appearing in search or trending. The channel name
entered must be a complete match and is case sensitive.
and the channel itself from appearing in search, trending, most popular and
recommended. The channel name entered must be a complete match and is case sensitive.
Hide Subscriptions Live: This setting is overridden by the app-wide {appWideSetting}
setting, in the {subsection} section of the {settingsSection}
SponsorBlock Settings:
UseDeArrowTitles: Replace video titles with user-submitted titles from DeArrow.
Playing Next Video Interval: Playing next video in no time. Click to cancel. | Playing
next video in {nextVideoInterval} second. Click to cancel. | Playing next video
in {nextVideoInterval} seconds. Click to cancel.

View File

@ -27,6 +27,11 @@ Close: 'Cerrar'
Back: 'Volver'
Forward: 'Adelante'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videos'
# Search Bar
Search / Go to URL: 'Buscar / Ir a la URL'
# In Filter Button

View File

@ -30,6 +30,13 @@ Close: 'Cerrar'
Back: 'Atrás'
Forward: 'Adelante'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Vídeos'
Shorts: Cortos
Live: En directo
# Search Bar
Search / Go to URL: 'Buscar / Ir a la dirección'
# In Filter Button
@ -84,6 +91,10 @@ Subscriptions:
Actualice las suscripciones para verlas aquí.
Empty Channels: Los canales a los que está suscrito no tienen actualmente ningún
vídeo.
Subscriptions Tabs: Pestañas de suscripciones
All Subscription Tabs Hidden: Todas las pestañas de las suscripciones están ocultas.
Para ver el contenido, por favor, desoculta algunas pestañas en la sección «{subsection}»
en «{settingsSection}».
Trending:
Trending: 'Tendencias'
Default: Predeterminado
@ -114,7 +125,7 @@ Settings:
# On Settings Page
Settings: 'Ajustes'
General Settings:
General Settings: 'General'
General Settings: 'Ajustes generales'
Fallback to Non-Preferred Backend on Failure: 'Usar motor API secundario en caso
de fallo'
Enable Search Suggestions: 'Activar sugerencias de búsqueda'
@ -267,8 +278,8 @@ Settings:
File Name Tooltip: Puede utilizar las siguientes variables. %Y Año 4 dígitos.
%M Mes 2 dígitos. %D Día 2 dígitos. %H Hora 2 dígitos. %N Minuto 2 dígitos.
%S Segundo 2 dígitos. %T Milisegundo 3 dígitos. %s Video Segundo. %t Video
Milisegundo 3 dígitos. %i Video ID. También puede utilizar "\" o "/" para
crear subcarpetas.
Milisegundo 3 dígitos. %i Video ID. También puede utilizar \ o / para crear
subcarpetas.
Enter Fullscreen on Display Rotate: Cambiar a pantalla completa al girar la pantalla
Skip by Scrolling Over Video Player: Omitir al desplazarse sobre el reproductor
de vídeo
@ -408,6 +419,12 @@ Settings:
Channel Page: Página del canal
Watch Page: Ver la página
General: General
Subscriptions Page: Página de suscripciones
Hide Channel Releases: Ocultar las nuevas publicaciones de los canales
Hide Channel Podcasts: Ocultar los canales de podcasts
Hide Subscriptions Shorts: Ocultar las suscripciones para los vídeos cortos
Hide Subscriptions Videos: Ocultar las suscripciones de los Vídeos
Hide Subscriptions Live: Ocultar las suscripciones de los directos
The app needs to restart for changes to take effect. Restart and apply change?: ¿Quieres
reiniciar FreeTube ahora para aplicar los cambios?
Proxy Settings:
@ -632,9 +649,16 @@ Channel:
This channel does not currently have any live streams: Este canal no tiene actualmente
ninguna retransmisión en directo
Shorts:
Shorts: Cortos
This channel does not currently have any shorts: Este canal no tiene actualmente
ningún vídeo corto
Podcasts:
Podcasts: Podcasts
This channel does not currently have any podcasts: Este canal actualmente no tiene
podcasts
Releases:
Releases: Publicaciones
This channel does not currently have any releases: Este canal no tiene actualmente
ninguna publicación
Video:
Mark As Watched: 'Marcar como visto'
Remove From History: 'Borrar del historial'
@ -955,10 +979,12 @@ Tooltips:
una caché para la imagen en la memoria personalizada. Esto aumentará el uso
de la memoria RAM.
Distraction Free Settings:
Hide Channels: Introduzca un nombre o ID de canal para ocultar todos los vídeos,
listas de reproducción y el propio canal y evitar que aparezcan en las búsquedas
o en las tendencias. El nombre del canal introducido debe coincidir completamente
y distingue entre mayúsculas y minúsculas.
Hide Channels: Ingresa un nombre del canal o un ID del canal para ocultar todos
los videos, listas de reproducción y el propio canal para que no aparezcan en
la búsqueda, tendencias, más populares y recomendados. El nombre del canal ingresado
debe ser una coincidencia completa y distinguir entre mayúsculas y minúsculas.
Hide Subscriptions Live: Esta configuración se reemplaza por la configuración
«{appWideSetting}» de toda la aplicación, en la sección «{subsection}» de «{settingsSection}»
SponsorBlock Settings:
UseDeArrowTitles: Sustituye los títulos de los vídeos por títulos enviados por
los usuarios desde DeArrow.
@ -996,7 +1022,7 @@ Channels:
Empty: Tu lista de canales está actualmente vacía.
Unsubscribe: Cancelar la suscripción
Unsubscribed: '{channelName} ha sido eliminado de tus suscripciones'
Unsubscribe Prompt: ¿Está seguro de querer desuscribirse de "{channelName}"?
Unsubscribe Prompt: ¿Está seguro/segura de querer desuscribirse de «{channelName}»?
Age Restricted:
Type:
Channel: Canal

View File

@ -29,6 +29,13 @@ Close: 'Sulge'
Back: 'Tagasi'
Forward: 'Edasi'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videod'
Shorts: Lühivideod
Live: Otseeeter
Version {versionNumber} is now available! Click for more details: 'Versioon {versionNumber}
in nüüd saadaval! Lisateavet leiad siit'
Download From Site: 'Laadi veebisaidist alla'
@ -89,6 +96,9 @@ Subscriptions:
Disabled Automatic Fetching: Sa oled lülitanud välja automaatse tellimuste laadimise.
Uuendatud teabe nägemiseks laadi vaade uuesti.
Empty Channels: Sinu tellitud kanalites hetkel pole ühtegi videot.
Subscriptions Tabs: Tellimuste vahekaardid
All Subscription Tabs Hidden: Kõik tellimuse vahekaardid on peidetud. Siinse sisu
nägemiseks palun eemalda kaartide peitmine jaotises „{subsection}“ / „{settingsSection}“.
Trending:
Trending: 'Populaarsust koguvad videod'
Trending Tabs: Populaarsust koguvad kaardid
@ -274,6 +284,8 @@ Settings:
Skip by Scrolling Over Video Player: Jäta vahele, kerides üle videopleieri
Allow DASH AV1 formats: Luba DASH AV1 vormingud
Enter Fullscreen on Display Rotate: Ekraani pööramisel ava täisekraanivaade
Comment Auto Load:
Comment Auto Load: Kommentaaride automaatne laadimine
Privacy Settings:
Privacy Settings: 'Privaatsuse seadistused'
Remember History: 'Jäta ajalugu meelde'
@ -369,6 +381,21 @@ Settings:
Hide Channels Placeholder: Kanali nimi või tunnus
Display Titles Without Excessive Capitalisation: Näita pealkirju ilma liigsete
suurtähtedeta
Sections:
General: Üldist
Side Bar: Külgpaan
Channel Page: Kanali vaade
Watch Page: Videovaade
Subscriptions Page: Tellimuste vaade
Hide Featured Channels: Peaida soovitatud kanalid
Hide Channel Playlists: Peida kanali esitusloendid
Hide Channel Shorts: Peida kanali lühivideod
Hide Channel Community: Peida kanali kogukond
Hide Channel Podcasts: Peida kanali taskuhäälingud
Hide Channel Releases: Peida kanali väljalasked
Hide Subscriptions Videos: Peida tellimuste videod
Hide Subscriptions Shorts: Peida tellimuste lühivideod
Hide Subscriptions Live: Peida tellimuste otse-eetrid
Proxy Settings:
Error getting network information. Is your proxy configured properly?: Võrguteavet
ei õnnestu leida. Kas sa oled puhverserveri ikka korralikult seadistanud?
@ -389,10 +416,10 @@ Settings:
rakendus vajab muudatuste jõustamiseks uuesti käivitamist. Kas teeme seda nüüd?
SponsorBlock Settings:
Notify when sponsor segment is skipped: Anna teada, kui toetajate vaade jääb vahele
'SponsorBlock API Url (Default is https://sponsor.ajay.app)': SponsorBlock'i API
URL (vaikimisi https://sponsor.ajay.app)
Enable SponsorBlock: Kasuta SponsorBlock'i
SponsorBlock Settings: SponsorBlock seadistused
'SponsorBlock API Url (Default is https://sponsor.ajay.app)': Sponsorite blokeerija
API URL (vaikimisi https://sponsor.ajay.app)
Enable SponsorBlock: Kasuta sponsorite blokeerijat
SponsorBlock Settings: Sponsorite blokeerija seadistused
Skip Options:
Auto Skip: Automaatne vahelejätmine
Show In Seek Bar: Näita otsinguribal
@ -400,6 +427,7 @@ Settings:
Skip Option: Jäta valik vahele
Do Nothing: Ära tee midagi
Category Color: Kategooria värv
UseDeArrowTitles: Laadi video pealkirjad DeArrow teenusest
External Player Settings:
External Player: Väline meediamängija
External Player Settings: Välise meediamängija seadistused
@ -424,7 +452,7 @@ Settings:
Experimental Settings:
Experimental Settings: Katselised seadistused
Warning: Tegemist on katseliste seadistustega ja sisselülitamisel võivad põhjustada
rakenduse kokkujooksmist. Kindlasti ära unusta oma andmete vatundamist. Jätka
rakenduse kokkujooksmist. Kindlasti ära unusta oma andmete varundamist. Jätka
omal vastutusel!
Replace HTTP Cache: Asenda HTTP vahemälu
Password Dialog:
@ -542,6 +570,35 @@ Channel:
About: 'Kanali teave'
Channel Description: 'Kanali kirjeldus'
Featured Channels: 'Soovitatud kanalid'
Details: Üksikasjad
Location: Asukoht
Tags:
Search for: Otsi silti „{tag}“
Tags: Sildid
Joined: Liitunud
This channel does not allow searching: See kanal ei luba otsingu kasutamist
Channel Tabs: Kanali kaardid
Shorts:
This channel does not currently have any shorts: Sellel kanalil pole lühivideosid
Live:
Live: Otseeeter
This channel does not currently have any live streams: Sellel kanalil pole hetkel
ühtegi otseeetrit
Community:
Community: Kogukond
This channel currently does not have any posts: Sellel kanalil pole hetkel postitusi
This channel does not exist: Sellist kanalit ei leidu
This channel is age-restricted and currently cannot be viewed in FreeTube.: Sellel
kanalil on vanusega seotud piirangud ja teda ei saa hetkel FreeTube'i vahendusel
vaadata.
Podcasts:
Podcasts: Taskuhäälingud
This channel does not currently have any podcasts: Sellel kanalil hetkel pole
taskuhäälinguid
Releases:
This channel does not currently have any releases: Sellel kanalil pole hetkel
ühtegi väljalaset
Releases: Väljalasked
Video:
Mark As Watched: 'Märgi vaadatuks'
Remove From History: 'Kustuta ajaloost'
@ -568,7 +625,7 @@ Video:
värskenda uuesti kontrollimiseks lehte'
# As in a Live Video
Live: 'Otse eetris'
Live Now: 'Otse eetris'
Live Now: 'Hetkel otseeetris'
Live Chat: 'Vestlus reaalajas'
Enable Live Chat: 'Luba reaalajas vestlust'
Live Chat is currently not supported in this build.: 'Reaalajas vestlus ei ole selles
@ -682,6 +739,8 @@ Video:
Show Super Chat Comment: Näita Super Chat'i kommentaare
Scroll to Bottom: Keri alla
Upcoming: Tulemas
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Otsevestlus
pole selle videovoo puhul saadaval. Võib-olla on üleslaadija vestluse keelanud.
Videos:
#& Sort By
Sort By:
@ -794,9 +853,9 @@ Tooltips:
järjekorra pööramine) ei toeta siis ära näita hoiatusi.
Custom External Player Arguments: Semikoolonitega (;) eraldatud käsurea argumendid,
mida sa soovid välisele meediamängijale saata.
External Player: "Seadistades välise meediamängija kuvame pisipildil ikooni video\
\ (või esitusloendi) esitamiseks välises meediamängijas. Hoiatus: Invidious'e\
\ seadistused ei mõjuta välise meediamängija kasutamist."
External Player: "Seadistades välise meediamängija kuvame pisipildil ikooni video
(või esitusloendi) esitamiseks välises meediamängijas. Hoiatus: Invidious'e
seadistused ei mõjuta välise meediamängija kasutamist."
DefaultCustomArgumentsTemplate: "(Vaikimisi: '{defaultCustomArguments}')"
Subscription Settings:
Fetch Feeds from RSS: Selle valiku kasutamisel FreeTube pruugib tellimuste andmete
@ -818,8 +877,8 @@ Tooltips:
kasutamist.
Invidious Instance: Invidious'e teenuse server, mida FreeTube kasutab API kutse
tegemisel.
External Link Handling: "Vali vaikekäitumine, kui vajutatakse linki, mida ei saa\
\ avada FreeTubes.\nVaikimisi avaneb link kasutaja vaikebrauseris.\n"
External Link Handling: "Vali vaikekäitumine, kui vajutatakse linki, mida ei saa
avada FreeTubes.\nVaikimisi avaneb link kasutaja vaikebrauseris.\n"
Player Settings:
Default Video Format: Vali kasutatavad videovormingud. DASH-vormingutel on üldjuhul
parem kvaliteet. Pärandvormingute kvaliteedi ülempiir on 720p ja seetõttu kasutavad
@ -844,12 +903,18 @@ Tooltips:
hiire ratast.
Distraction Free Settings:
Hide Channels: Sisesta kanali nimi või kanali ID, et kõik videod, esitusloendid
ja kanal ise ei oleks nähtav otsingus ega populaarsust koguvate videote vaates.
Sisestatud kanali nimi peab vastama täielikult ja on tõstutundlik.
ja kanal ise ei oleks nähtav otsingus, soovitatavate videote, populaarsete videote
ja populaarsust koguvate videote vaates. Sisestatud kanali nimi peab otsingule
vastama täielikult ja on tõstutundlik.
Hide Subscriptions Live: Selle seadistuse tühistab rakenduseülene „{appWideSetting}“
seadistus „{subsection}“/„{settingsSection}“
Experimental Settings:
Replace HTTP Cache: Sellega lülitatakse välja Electron'i standardne kettal paiknev
http-puhver ja võetakse kasutusele rakenduse mälupõhine puhver. Üheks tulemuseks
saab olema suurem mälukasutus.
SponsorBlock Settings:
UseDeArrowTitles: Asenda video nimi kasutajate poolt DeArrow teenusesse lisatud
nimega (pealkirjaga).
Playing Next Video Interval: Kohe esitan järgmist videot. Tühistamiseks klõpsi. |
{nextVideoInterval} sekundi möödumisel esitan järgmist videot. Tühistamiseks klõpsi.
| {nextVideoInterval} sekundi möödumisel esitan järgmist videot. Tühistamiseks klõpsi.
@ -894,3 +959,7 @@ Chapters:
praegune peatükk: {chapterName}'
Ok: Sobib
Preferences: Eelistused
Hashtag:
Hashtag: Teemaviide
This hashtag does not currently have any videos: Selle teemaviite ehk haaksõna alusel
ei leidu hetkel ühtegi videot

View File

@ -29,6 +29,11 @@ Close: 'Itxi'
Back: 'Atzera'
Forward: 'Aurrera'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Bideoak'
Version {versionNumber} is now available! Click for more details: '{versionNumber}
bertsioa erabilgarri! Klikatu azalpen gehiagorako'
Download From Site: 'Webgunetik jaitsi'

View File

@ -1,11 +1,11 @@
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.': >-
Tämä sovelluksen osa ei ole vielä valmis. Tule takaisin myöhemmin.
Tämä sovelluksen osa ei ole vielä valmis. Tule takaisin myöhemmin kun olemme edistyneet.
# Webkit Menu Bar
File: 'Tiedosto'
Quit: 'Poistu'
Quit: 'Lopeta'
Edit: 'Muokkaa'
Undo: 'Kumoa'
Redo: 'Tee uudelleen'
@ -15,18 +15,25 @@ Paste: 'Liitä'
Delete: 'Poista'
Select all: 'Valitse kaikki'
Reload: 'Lataa uudelleen'
Force Reload: 'Pakota uudelleenlataus'
Toggle Developer Tools: 'Kehittäjän työkalut'
Force Reload: 'Pakota Uudelleenlataus'
Toggle Developer Tools: 'Vaihda Kehittäjän Työkaluihin'
Actual size: 'Todellinen koko'
Zoom in: 'Lähennä'
Zoom out: 'Loitonna'
Toggle fullscreen: 'Koko näytön tila'
Toggle fullscreen: 'Vaihda koko näyttöön'
Window: 'Ikkuna'
Minimize: 'Pienennä'
Close: 'Sulje'
Back: 'Takaisin'
Forward: 'Eteenpäin'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videot'
Shorts: Lyhyet
Live: Livenä
# Search Bar
Search / Go to URL: 'Etsi / Mene osoitteeseen'
# In Filter Button
@ -81,6 +88,10 @@ Subscriptions:
Empty Channels: Tilaamillasi kanavilla ei ole videoita tällä hetkellä.
Disabled Automatic Fetching: Olet poistanut käytöstä automaattisen tilaustennoutamisen.
Virkistä tilaukset nähdäksesi ne täällä.
Subscriptions Tabs: Tilaukset-välilehdet
All Subscription Tabs Hidden: Kaikki tilausvälilehdet on piilotettu. Jos haluat
nähdä sisällön täällä, poista joitakin välilehtiä ”{settingsSection}”-osion ”{settingsSection}”-osion
”{subsection}”-välilehdistä.
Trending:
Trending: 'Nousussa'
Trending Tabs: Nousussa olevat välilehdet
@ -214,9 +225,9 @@ Settings:
Force Local Backend for Legacy Formats: 'Pakota paikallinen taustaohjelma vanhoille
formaateille'
Remember History: 'Muista historia'
Play Next Video: 'Toista seuraava video'
Play Next Video: 'Toista Seuraava Video'
Turn on Subtitles by Default: 'Ota tekstitys käyttöön oletusarvoisesti'
Autoplay Videos: 'Toista seuraava video'
Autoplay Videos: 'Toista Automaattisesti Videot'
Proxy Videos Through Invidious: 'Välitä videot Invidiousin kautta'
Autoplay Playlists: 'Toista soittolistat automaattisesti'
Enable Theatre Mode by Default: 'Ota teatteritila käyttöön oletusarvoisesti'
@ -391,7 +402,7 @@ Settings:
Hide Sharing Actions: Piilota jakamistoiminnot
Hide Chapters: Piilota kappaleet
Hide Channels: Piilota videot kanavilta
Hide Upcoming Premieres: Piilota tulevat ensiesitykset
Hide Upcoming Premieres: Piilota Tulevat Ensiesitykset
Hide Channels Placeholder: Kanavan nimi tai tunnus
Display Titles Without Excessive Capitalisation: Näytä otsikot ilman liiallista
isoja kirjaimia
@ -404,6 +415,11 @@ Settings:
Channel Page: Kanavan sivu
Watch Page: Katso sivu
General: Yleiset
Subscriptions Page: Tilaukset-sivu
Hide Channel Releases: Piilota kanavajulkaisut
Hide Channel Podcasts: Piilota kanavan podcastit
Hide Subscriptions Videos: Piilota tilausvideot
Hide Subscriptions Live: Piilota tilausten livet
The app needs to restart for changes to take effect. Restart and apply change?: Sovellus
on käynnistettävä uudelleen, jotta muutokset tulevat voimaan. Käynnistetäänkö
uudelleen?
@ -436,6 +452,7 @@ Settings:
Do Nothing: Älä tee mitään
Prompt To Skip: Ehdota ohittamista
Category Color: Luokan väri
UseDeArrowTitles: Käytä DeArrow Video Otsikoita
External Player Settings:
Custom External Player Arguments: Omavalintaisen ulkoisen toisto-ohjelman määritykset
Custom External Player Executable: Omavalintaisen ulkoisen toisto-ohjelman ajettava
@ -460,9 +477,9 @@ Settings:
Hide Search Bar: Piilota hakupalkki
Experimental Settings:
Experimental Settings: Kokeelliset asetukset
Warning: Nämä asetukset ovat kokeellisia ja ne aiheuttavat kaatumisia, kun ne
ovat käytössä. Varmuuskopioiden tekeminen on erittäin suositeltavaa. Käytä omalla
vastuullasi!
Warning: Nämä asetukset ovat kokeellisia ja ne voivat aiheuttavat kaatumisia,
kun ne ovat käytössä. Varmuuskopioiden tekeminen on erittäin suositeltavaa.
Käytä omalla vastuullasi!
Replace HTTP Cache: Korvaa HTTP-välimuisti
Password Dialog:
Password: Salasana
@ -578,6 +595,17 @@ Channel:
Live: Livenä
This channel does not currently have any live streams: Tällä kanavalla ei ole
tällä hetkellä yhtään suoraa lähetystä
Shorts:
This channel does not currently have any shorts: Tällä kanavalla ei juuri nyt
ole lyhyitä
Podcasts:
Podcasts: Podcastit
This channel does not currently have any podcasts: Tällä kanavalla ei ole yhtäkään
podcastia
Releases:
Releases: Julkaisut
This channel does not currently have any releases: Tällä kanavalla ei ole yhtäkään
julkaisua
Video:
Open in YouTube: 'Avaa Youtubessa'
Copy YouTube Link: 'Kopioi Youtube-linkki'
@ -643,7 +671,7 @@ Video:
Mark As Watched: Merkitse katsotuksi
Autoplay: Automaattinen toisto
Play Previous Video: Toista edellinen video
Play Next Video: Toista seuraava video
Play Next Video: Toista Seuraava Video
Reverse Playlist: Käänteinen soittolista
Shuffle Playlist: Sekoita soittolistaa
Loop Playlist: Kierrätä soittolistaa
@ -715,7 +743,7 @@ Video:
Premieres on: Julkaistaan
Premieres: Ensilähetykset
Show Super Chat Comment: Näytä Super Chat -kommentti
Scroll to Bottom: Vieritä alaspäin
Scroll to Bottom: Vieritä Alaspäin
Upcoming: Tuleva
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Live-chat
ei ole käytettävissä tässä suoratoistossa. Lataaja on saattanut poistaa sen käytöstä.
@ -778,8 +806,8 @@ Comments:
Reply: 'Vastaa'
There are no comments available for this video: 'Tähän videoon ei ole yhtään kommenttia'
Load More Comments: 'Lataa lisää kommentteja'
There are no more comments for this video: Ei enempää kommentteja
No more comments available: Ei enempää kommentteja
There are no more comments for this video: Ei ole enempää kommentteja tälle videolle
No more comments available: Eiole enempää kommentteja
Newest first: Uusimmat ensin
Top comments: Suosituimmat kommentit
Sort by: Lajitteluperuste
@ -847,7 +875,7 @@ Profile:
Add Selected To Profile: Lisää valitut profiiliin
Delete Selected: Poista valitut
Select None: Älä valitse mitään
Select All: Valitse kaikki
Select All: Valitse Kaikki
'{number} selected': '{number} valittu'
Other Channels: Muut kanavat
Subscription List: Tilauslista
@ -895,11 +923,12 @@ Tooltips:
toiston nopeutta. Palataksesi alkuperäiseen toistonopeuteen (1x ellei toisin
määritelty asetuksissa), pidä CTRL-painike (Komentopainike MAC-tietokoneessa)
painettuna ja paina hiiren vasenta näppäintä.
Skip by Scrolling Over Video Player: Käytä vierityspyörää videon selaamiseen MPV-tyyliin.
Allow DASH AV1 formats: DASH AV1 -formaatit saattavat näyttää paremmalta kuin
DASH H.264 -formaatit. DASH AV1 -formaatit vaativat enemmän tehoa toistamiseen!
Ne eivät ole käytettävissä kaikissa videoissa, ja näissä tapauksissa soitin
käyttää sen sijaan DASH H.264 -formaatteja.
Skip by Scrolling Over Video Player: Käytä vieritysrullaa videon selaamiseen,
MPV-tyyliin.
Allow DASH AV1 formats: DASH AV1 formaatit saattavat näyttää paremmalta kuin DASH
H.264 formaatit. DASH AV1 formaatit vaativat enemmän tehoa toistamiseen! Ne
eivät ole käytettävissä kaikissa videoissa ja näissä tapauksissa soitin käyttää
sen sijaan DASH H.264 formaatteja.
Privacy Settings:
Remove Video Meta Files: Kun tämä on kytkettynä päälle, FreeTube poistaa automaattisesti
meta-tiedostot jotka luotiin videon toiston aikana, katselusivu suljettaessa.
@ -922,8 +951,13 @@ Tooltips:
käyttöä.
Distraction Free Settings:
Hide Channels: Anna kanavan nimi tai kanavatunnus piilottaaksesi kaikki videot,
soittolistat ja itse kanavan näkymästä haussa tai trendaamisessa. Annetun kanavan
nimen on vastattava täydellisesti ja kirjainkoolla on merkitystä.
soittolistat ja itse kanavan näkymästä haussa, trendaamisessa, suosituimmissa
ja suositelluissa. Annetun kanavan nimen on oltava täysin oikein ja kirjainkoolla
on merkitystä.
Hide Subscriptions Live: Tämä asetus ohitetaan koko sovelluksen laajuisella ”{appWideSetting}”-asetuksella,
joka on ”{settingsSection}”-osion ”{subsection}”-osiossa.
SponsorBlock Settings:
UseDeArrowTitles: Korvaa videon otsikot käyttäjien lähettämillä DeArrow'n otsikoilla.
More: Lisää
Playing Next Video Interval: Seuraava video alkaa. Klikkaa peruuttaaksesi. |Seuraava
video alkaa {nextVideoInterval} sekunnin kuluttua. Klikkaa peruuttaaksesi. | Seuraava
@ -946,7 +980,7 @@ Downloading failed: Videon "{videoTitle}" lataamisessa havaittiin ongelma
Downloading has completed: Videon "{videoTitle}" lataus on valmis
Starting download: Aloitetaan lataamaan "{videoTitle}"
Screenshot Success: Kuvakaappaus tallennettu nimellä ”{filePath}”
New Window: Uusi ikkuna
New Window: Uusi Ikkuna
Age Restricted:
This {videoOrPlaylist} is age restricted: Tämä {videoOrPlaylist} on ikärajoitettu
Type:

View File

@ -28,6 +28,13 @@ Close: 'Fermer'
Back: 'Retour'
Forward: 'Avancer'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Vidéos'
Shorts: Shorts
Live: En direct
# Search Bar
Search / Go to URL: 'Rechercher / ouvrir l''URL'
# In Filter Button
@ -87,6 +94,10 @@ Subscriptions:
abonnements. Actualisez les abonnements pour les voir ici.
Empty Channels: Les chaînes auxquelles vous êtes abonné(e) ne contiennent actuellement
aucune vidéo.
Subscriptions Tabs: Onglets Abonnements
All Subscription Tabs Hidden: Tous les onglets d'abonnement sont cachés. Pour voir
le contenu ici, veuillez désactiver certains onglets dans la section « {subsection} »
dans « {settingsSection} ».
Trending:
Trending: 'Tendance'
Trending Tabs: Onglets des Tendances
@ -427,6 +438,12 @@ Settings:
Side Bar: Barre latérale
Watch Page: Page de lecture
General: Général
Subscriptions Page: Page Abonnements
Hide Channel Podcasts: Masquer les podcasts de la chaîne
Hide Channel Releases: Masquer les publications de la chaîne
Hide Subscriptions Videos: Masquer les vidéos des abonnements
Hide Subscriptions Shorts: Masquer les shorts des abonnements
Hide Subscriptions Live: Masquer les diffusions en direct des abonnements
The app needs to restart for changes to take effect. Restart and apply change?: L'application
doit être redémarrée pour que les changements prennent effet. Redémarrer et appliquer
les changements ?
@ -623,9 +640,16 @@ Channel:
This channel does not currently have any live streams: Cette chaîne n'a actuellement
aucun flux en direct
Shorts:
Shorts: Shorts
This channel does not currently have any shorts: Cette chaîne n'a actuellement
aucun shorts
Releases:
Releases: Publications
This channel does not currently have any releases: Cette chaîne n'a actuellement
aucune publication
Podcasts:
Podcasts: Podcasts
This channel does not currently have any podcasts: Cette chaîne n'a pas encore
de podcasts
Video:
Mark As Watched: 'Marquer comme vu'
Remove From History: 'Retirer de l''historique'
@ -999,8 +1023,12 @@ Tooltips:
Distraction Free Settings:
Hide Channels: Entrez un nom de chaîne ou un identifiant de chaîne pour empêcher
toutes les vidéos, les listes de lecture et la chaîne elle-même d'apparaître
dans les recherches ou les tendances. Le nom du canal entré doit être une correspondance
complète et est sensible à la casse.
dans les recherches, dans les catégories Tendances, Plus populaires et Recommandés.
Le nom de la chaîne entré doit correspondre exactement et est sensible à la
casse.
Hide Subscriptions Live: Ce paramètre est remplacé par le paramètre « {appWideSetting} »
applicable à l'ensemble de l'application, dans la section « {subsection} » de
la section « {settingsSection} »
SponsorBlock Settings:
UseDeArrowTitles: Remplacez les titres des vidéos par des titres proposés par
les utilisateurs de DeArrow.

View File

@ -30,6 +30,11 @@ Close: 'Pechar'
Back: 'Atrás'
Forward: 'Adiante'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Vídeos'
Version {versionNumber} is now available! Click for more details: 'A versión {versionNumber}
está dispoñible! Fai clic para veres máis detalles'
Download From Site: 'Descargar do sitio'

View File

@ -29,6 +29,13 @@ Close: 'סגירה'
Back: 'אחורה'
Forward: 'קדימה'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'סרטונים'
Shorts: Shorts
Live: חי
Version {versionNumber} is now available! Click for more details: 'גרסה {versionNumber}
זמינה מעתה! לחיצה תציג פרטים נוספים'
Download From Site: 'הורדה מהאתר'
@ -88,6 +95,9 @@ Subscriptions:
Disabled Automatic Fetching: השבתת משיכת מינויים אוטומטית. יש לרענן את המינויים
כדי לצפות בהם כאן.
Empty Channels: בערוצים אליהם נרשמת אין כלל סרטונים.
All Subscription Tabs Hidden: כל לשוניות המינויים מותסרות. כדי לראות את התוכן כאן,
נא לבטל את הסתרתן של כמה מהלשוניות תחת הסעיף „{subsection}” שב„{settingsSection}”.
Subscriptions Tabs: לשוניות מינויים
Trending:
Trending: 'הסרטונים החמים'
Trending Tabs: לשוניות מובילים
@ -398,6 +408,12 @@ Settings:
Channel Page: עמוד ערוץ
Side Bar: סרגל צד
General: כללי
Subscriptions Page: עמוד מינויים
Hide Channel Podcasts: הסתרת הסכתים של הערוץ
Hide Channel Releases: הסתרת שחרורים של הערוץ
Hide Subscriptions Live: הסתרת שידורים חיים של המינויים
Hide Subscriptions Shorts: הסתרת Shorts של המינויים
Hide Subscriptions Videos: הסתרת סרטוני מינוי
The app needs to restart for changes to take effect. Restart and apply change?: צריך
להפעיל את היישומון מחדש כדי שהשינויים ייכנסו לתוקף. להפעיל מחדש ולהחיל את השינוי?
Proxy Settings:
@ -620,8 +636,13 @@ Channel:
This channel does not currently have any live streams: לערוץ הזה אין שידורים חיים
כרגע
Shorts:
Shorts: Shorts
This channel does not currently have any shorts: אין כרגע Shorts בערוץ הזה
Podcasts:
Podcasts: הסכתים
This channel does not currently have any podcasts: בערוץ הזה אין הסכתים כרגע
Releases:
Releases: שחרורים
This channel does not currently have any releases: בערוץ הזה אין שחרורים כרגע
Video:
Mark As Watched: 'סמנו כנצפה'
Remove From History: 'מחקו מהיסטוריית הצפייה'
@ -913,8 +934,10 @@ Tooltips:
אישית בזיכרון. יגדיל את צריכת הזיכרון (RAM).
Distraction Free Settings:
Hide Channels: יש למלא את שם או מזהה הערוץ כדי להסתיר את כל הסרטונים, רשימות הנגינה
ואת הערוץ עצמו כך שלא יופיע בחיפוש או במובילים. שם הערוץ שמילאת צריך להיות תואם
במלואו ותואם מבחינת רישיות (אותיות גדולות/קטנות).
ואת הערוץ עצמו כך שלא יופיע בחיפוש, במובילים, בנפוצים ביותר או במומלצים. שם
הערוץ שמילאת צריך להיות תואם במלואו ותואם מבחינת רישיות (אותיות גדולות/קטנות).
Hide Subscriptions Live: הגדרה זו נדרסת על ידי ההגדרה הכללית „{appWideSetting}”,
בסעיף „{subsection}” שב„{settingsSection}”
SponsorBlock Settings:
UseDeArrowTitles: החלפת כותרות הסרטונים עם כותרות ששלחו משתמשים ב־DeArrow.
More: עוד

View File

@ -29,6 +29,13 @@ Close: 'Zatvori'
Back: 'Natrag'
Forward: 'Naprijed'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videa'
Shorts: Kratka videa
Live: Uživo
# Search Bar
Search / Go to URL: 'Pretraži / Idi na URL'
# In Filter Button
@ -408,6 +415,8 @@ Settings:
General: Opće
Side Bar: Bočna traka
Channel Page: Stranica kanala
Hide Channel Podcasts: Sakrij kanal podcastova
Hide Channel Releases: Sakrij kanal izdanja
The app needs to restart for changes to take effect. Restart and apply change?: Promjene
će se primijeniti nakon ponovnog pokeretanja programa. Ponovo pokrenuti program?
Proxy Settings:
@ -439,6 +448,7 @@ Settings:
Prompt To Skip: Poziv za preskakanje
Do Nothing: Ne čini ništa
Category Color: Boja kategorije
UseDeArrowTitles: Koristi DeArrow Video naslove
External Player Settings:
Custom External Player Arguments: Argumenti prilagođenog vanjskog playera
Custom External Player Executable: Izvršna datoteka prilagođenog vanjskog playera
@ -635,7 +645,14 @@ Channel:
Shorts:
This channel does not currently have any shorts: Ovaj kanal trenutačno nema kratka
videa
Shorts: Kratka videa
Releases:
Releases: Izdanja
This channel does not currently have any releases: Ovaj kanal trenutačno nema
izdanja
Podcasts:
Podcasts: Podcasti
This channel does not currently have any podcasts: Ovaj kanal trenutačno nema
podcastova
Video:
Mark As Watched: 'Označi kao pogledano'
Remove From History: 'Ukloni iz povijesti'
@ -948,8 +965,10 @@ Tooltips:
i aktivira prilagođenu predmemoriju slika u memoriji. Povećava korištenje RAM-a.
Distraction Free Settings:
Hide Channels: Upiši ime kanala ili ID kanala za skrivanje svih videa, zbirki
kao i sam kanal u pretrazi ili u trendovima. Upisano ime kanala se mora potpuno
poklapati i razlikuje velika i mala slova.
kao i samog kanala u pretrazi, trendovima popularnim i preporučenim. Upisano
ime kanala se mora potpuno poklapati i razlikuje velika i mala slova.
SponsorBlock Settings:
UseDeArrowTitles: Zamijeni naslove videa koje su poslali korisnici s DeArrow naslovima.
Playing Next Video Interval: Trenutna reprodukcija sljedećeg videa. Pritisni za prekid.
| Reprodukcija sljedećeg videa za {nextVideoInterval} sekunde. Pritisni za prekid.
| Reprodukcija sljedećeg videa za {nextVideoInterval} sekundi. Pritisni za prekid.

View File

@ -30,6 +30,13 @@ Close: 'Bezárás'
Back: 'Vissza'
Forward: 'Előre'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videók'
Shorts: Rövidfilmek
Live: Élő
Version {versionNumber} is now available! Click for more details: 'A(z) {versionNumber}
verzió már elérhető! Kattintson a további részletekért'
Download From Site: 'Letöltés a webhelyről'
@ -92,6 +99,10 @@ Subscriptions:
Disabled Automatic Fetching: Az önműködő feliratkozási kérés letiltva. Frissítse
a feliratkozást a megtekintéséhez.
Empty Channels: A feliratkozott csatornák jelenleg nem tartalmaznak videókat.
All Subscription Tabs Hidden: Az összes feliratkozási lap el van rejtve. Az itteni
tartalom megtekintéséhez, kérjük, jelenítse meg néhány lap elrejtését a(z) „{settingsSection}”
„{subsection}” szakaszában.
Subscriptions Tabs: Feliratkozások lapok
Trending:
Trending: 'Népszerű'
Trending Tabs: Népszerű lapok
@ -416,6 +427,12 @@ Settings:
Channel Page: Csatornalap
Watch Page: Nézőlap
General: Általános
Subscriptions Page: Feliratkozások oldal
Hide Channel Podcasts: Csatornapodcastok elrejtése
Hide Channel Releases: Csatornakiadások elrejtése
Hide Subscriptions Shorts: Feliratkozások rövidfilmek elrejtése
Hide Subscriptions Videos: Feliratkozási videók elrejtése
Hide Subscriptions Live: Feliratkozások élők elrejtése
The app needs to restart for changes to take effect. Restart and apply change?: Az
alkalmazásnak újra kell indulnia, hogy a változtatások életbe lépjenek. Indítsa
újra és alkalmazza a módosítást?
@ -638,11 +655,18 @@ Channel:
bejegyzések
Community: Közösség
Shorts:
Shorts: Rövidfilmek
This channel does not currently have any shorts: Ezen a csatornán jelenleg nincsenek
rövidfilmek
This channel does not exist: Nem létezik ez a csatorna
This channel does not allow searching: Keresés nem engedélyezett ezen a csatornán
Releases:
Releases: Kiadások
This channel does not currently have any releases: Ennek a csatornának jelenleg
nincsenek kiadásai
Podcasts:
Podcasts: Podcastok
This channel does not currently have any podcasts: Ez a csatorna jelenleg nem
rendelkezik podcastokkal
Video:
Mark As Watched: 'Megjelölés megtekintettként'
Remove From History: 'Eltávolítás az előzményekből'
@ -950,8 +974,10 @@ Tooltips:
Distraction Free Settings:
Hide Channels: Adja meg a csatorna nevét vagy csatornaazonosítóját, hogy elrejtse
az összes videót, lejátszási listát és magát a csatornát, hogy ne jelenjen meg
a keresésben vagy a népszerűségben. A megadott csatornanévnek teljes egyezésnek
kell lennie, és megkülönbözteti a kis- és nagybetűket.
a keresésben, illetve a felkapott, legnépszerűbb és legajánlottabb. A megadott
csatornanévnek teljes egyezésnek kell lennie, és megkülönbözteti a kis- és nagybetűket.
Hide Subscriptions Live: Ezt a beállítást felülírja az alkalmazásszintű „{appWideSetting}”
beállítás a(z) „{settingsSection}” „{subsection}” szakaszában
SponsorBlock Settings:
UseDeArrowTitles: Cserélje le a videocímeket a DeArrow által beküldött, felhasználó
által beküldött címekre.

View File

@ -30,6 +30,11 @@ Close: 'Tutup'
Back: 'Kembali'
Forward: 'Maju'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Video'
Version {versionNumber} is now available! Click for more details: 'Versi {versionNumber}
sekarang tersedia! Klik untuk detail lebih lanjut'
Download From Site: 'Unduh dari Situs'

View File

@ -30,6 +30,13 @@ Close: 'Loka'
Back: 'Til baka'
Forward: 'Áfram'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Myndskeið'
Shorts: Stuttmyndir
Live: Í beinni
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'
@ -92,6 +99,9 @@ Subscriptions:
Disabled Automatic Fetching: Þú hefur gert sjálfvirkt niðurhal áskrifta óvirkt.
Endurlestu áskriftirnar og þær munu birtast hér.
Empty Channels: Rásirnar sem þú ert með í áskrift eru er ekki með nein myndskeið.
Subscriptions Tabs: Áskriftaflipar
All Subscription Tabs Hidden: Allir áskriftaflipar eru faldir. Til að sjá efni hér,
skaltu gera einhverja flipa sýnilega í "{subsection}" hlutanum í "{settingsSection}".
More: 'Meira'
Trending:
Trending: 'Í umræðunni'
@ -335,10 +345,16 @@ Settings:
Channel Page: Rásasíða
Watch Page: Áhorfssíða
General: Almennt
Subscriptions Page: Áskriftasíða
Hide Channel Shorts: Fela stuttmyndir rása
Hide Channel Playlists: Fela spilunarlista rása
Hide Channel Community: Fela samfélag rása
Hide Featured Channels: Fela rásir í deiglunni
Hide Channel Podcasts: Fela hlaðvörp rása
Hide Channel Releases: Fela útgáfur rása
Hide Subscriptions Shorts: Fela stuttmyndir áskrifta
Hide Subscriptions Live: Fela bein streymi áskrifta
Hide Subscriptions Videos: Fela myndskeið áskrifta
Data Settings:
Data Settings: 'Stillingar gagna'
Select Import Type: 'Veldu tegund innflutnings'
@ -417,6 +433,7 @@ Settings:
Prompt To Skip: Spyrja hvort eigi að sleppa
Do Nothing: Gera ekkert
Category Color: Litur flokks
UseDeArrowTitles: Nota DeArrow myndskeiðatitla
External Player Settings:
Custom External Player Arguments: Sérsniðin viðföng fyrir utanaðkomandi spilara
Custom External Player Executable: Sérsniðin skipun fyrir utanaðkomandi spilara
@ -577,9 +594,16 @@ Channel:
Community: Samfélag
This channel currently does not have any posts: Þessi rás er ekki með neinar færslur
Shorts:
Shorts: Stuttmyndir
This channel does not currently have any shorts: Þessi rás er í augnablikinu ekki
með neinar stuttmyndir
Releases:
Releases: Útgáfur
This channel does not currently have any releases: Þessi rás er ekki með neinar
útgáfur í augnablikinu
Podcasts:
Podcasts: Hlaðvörp
This channel does not currently have any podcasts: Þessi rás er ekki með nein
hlaðvörp í augnablikinu
Video:
Mark As Watched: 'Merkja sem búið að horfa á'
Remove From History: 'Fjarlægja úr áhorfsferli'
@ -873,8 +897,14 @@ Tooltips:
minnislæga skyndiminnis-diskmynd. Veldur aukinni notkun á vinnsluminni.
Distraction Free Settings:
Hide Channels: Settu inn heiti eða auðkenni rásar til að fela öll myndskeið, spilunarlista
og sjálfa rásina við leit eða vinsældaskráningu. Heiti rásarinnar sem sett er
inn þarf að vera nákvæmlega stafrétt og tekur tillit til hástafa/lágstafa.
og sjálfa rásina við leit eða því sem er vinsælast, mest skoðað og mælt með.
Heiti rásarinnar sem sett er inn þarf að vera nákvæmlega stafrétt og tekur tillit
til hástafa/lágstafa.
Hide Subscriptions Live: Þessa stillingu er hægt að taka yfir með "{appWideSetting}"
stillingunni fyrir allt forritið, í "{subsection}" hlutanum í "{settingsSection}"
SponsorBlock Settings:
UseDeArrowTitles: Skipta út titlum myndskeiða fyrir titla sem notendur hafa sent
inn á DeArrow.
Local API Error (Click to copy): 'Villa í staðværu API-kerfisviðmóti (smella til að
afrita)'
Invidious API Error (Click to copy): 'Villa í Invidious API-kerfisviðmóti (smella

View File

@ -30,6 +30,13 @@ Close: 'Chiudi'
Back: 'Indietro'
Forward: 'Avanti'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Video'
Shorts: Video brevi
Live: Dal vivo
# Search Bar
Search / Go to URL: 'Cerca o aggiungi URL YouTube'
# In Filter Button
@ -87,6 +94,9 @@ Subscriptions:
Disabled Automatic Fetching: Hai disabilitato il recupero automatico dell'abbonamento.
Aggiorna gli abbonamenti per vederli qui.
Empty Channels: I canali a cui sei iscritto attualmente non hanno alcun video.
Subscriptions Tabs: Schede iscrizioni
All Subscription Tabs Hidden: Tutte le schede di iscrizione sono nascoste. Per vedere
i contenuti qui, scopri le schede nella sezione "{subsection}" in "{settingsSection}".
Trending:
Trending: 'Tendenze'
Music: Musica
@ -417,6 +427,12 @@ Settings:
Watch Page: Pagina di visualizzazione
Side Bar: Barra laterale
Channel Page: Pagina del canale
Subscriptions Page: Pagina delle iscrizioni
Hide Channel Podcasts: Nascondi i podcast del canale
Hide Channel Releases: Nascondi i rilasci del canale
Hide Subscriptions Live: Nascondi le iscrizioni dal vivo
Hide Subscriptions Videos: Nascondi i video delle iscrizioni
Hide Subscriptions Shorts: Nascondi le iscrizioni ai video brevi
The app needs to restart for changes to take effect. Restart and apply change?: L'app
deve essere riavviata affinché le modifiche abbiano effetto. Riavviare e applicare
la modifica?
@ -491,7 +507,7 @@ Settings:
Remove Password: Rimuovi password
About:
#On About page
About: 'Informazioni'
About: 'Informazioni su'
#& About
'This software is FOSS and released under the GNU Affero General Public License v3.0.': 'Questo
software è rilasciato con licenza gratuita AGPL-3.0.'
@ -571,7 +587,7 @@ Channel:
Newest: 'Più nuovi'
Oldest: 'Più vecchi'
About:
About: 'Informazioni'
About: 'Informazioni su'
Channel Description: 'Descrizione canale'
Featured Channels: 'Canali in evidenza'
Tags:
@ -600,9 +616,16 @@ Channel:
This channel does not currently have any live streams: Questo canale attualmente
non ha alcun video dal vivo
Shorts:
Shorts: Video brevi
This channel does not currently have any shorts: Questo canale attualmente non
ha video brevi
Podcasts:
Podcasts: Podcast
This channel does not currently have any podcasts: Questo canale non ha attualmente
alcun podcast
Releases:
Releases: Rilasci
This channel does not currently have any releases: Questo canale non ha attualmente
alcun rilascio
Video:
Mark As Watched: 'Segna come già visto'
Remove From History: 'Rimuovi dalla cronologia'
@ -629,7 +652,7 @@ Video:
'Chat is disabled or the Live Stream has ended.': 'La chat è disabilitata o la diretta
è terminata.'
Live chat is enabled. Chat messages will appear here once sent.: 'La chat dal vivo
è abilitata. I messaggi appariranno qui una volta inviati.'
è abilitata. I messaggi appariranno qui una volta inviati.'
'Live Chat is currently not supported with the Invidious API. A direct connection to YouTube is required.': 'La
chat dal vivo non è attualmente supportata con le API di Invidious. È necessaria
una connessione diretta a YouTube.'
@ -965,10 +988,12 @@ Tooltips:
una cache di immagini in memoria personalizzata. Comporta un aumento dell'utilizzo
della RAM.
Distraction Free Settings:
Hide Channels: Inserisci il nome o l'ID di un canale per nascondere tutti i video,
le playlist e il canale stesso dalla visualizzazione nelle ricerche o nelle
tendenze. Il nome del canale inserito deve essere una corrispondenza completa
e fa distinzione tra maiuscole e minuscole.
Hide Channels: Inserisci il nome o l'ID di un canale per impedire che tutti i
video, le playlist e il canale stesso vengano visualizzati nelle ricerche, tendenze,
più popolari e consigliati. Il nome del canale inserito deve avere una corrispondenza
completa e fa distinzione tra maiuscole e minuscole.
Hide Subscriptions Live: Questa impostazione è sovrascritta dall'impostazione
"{appWideSetting}" a livello di app, nella sezione "{subsection}" di "{settingsSection}"
SponsorBlock Settings:
UseDeArrowTitles: Sostituisci i titoli dei video con titoli inviati dagli utenti
da DeArrow.

View File

@ -27,6 +27,13 @@ Close: '閉じる'
Back: '戻る'
Forward: '進む'
# Global
# Anything shared among components / views should be put here
Global:
Videos: '動画'
Shorts: ショート動画
Live: ライブ配信
# Search Bar
Search / Go to URL: '検索 / URL の表示'
# In Filter Button
@ -91,8 +98,7 @@ Playlists: '再生リスト'
User 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: この再生リストに、検索に一致する動画はありません
History:
@ -539,7 +545,6 @@ Channel:
This channel does not currently have any live streams: このチャンネルは現在、ライブ配信を行っていません
Live: ライブ配信
Shorts:
Shorts: ショート動画
This channel does not currently have any shorts: このチャンネルには現在ショート動画がありません
Video:
Open in YouTube: 'YouTube で表示'
@ -849,7 +854,7 @@ Tooltips:
Replace HTTP Cache: Electron のディスクに基づく HTTP キャッシュを無効化し、メモリ内で独自の画像キャッシュを使用します。このことにより
RAM の使用率は増加します。
Distraction Free Settings:
Hide Channels: チャンネル名またはチャンネル ID
Hide Channels: チャンネル名またはチャンネル ID
を入力すると、すべてのビデオ、再生リスト、およびチャンネル自体が検索や人気に表示されなくなります。入力するチャンネル名は完全に一致することが必要で、大文字と小文字を区別します。
SponsorBlock Settings:
UseDeArrowTitles: 動画のタイトルを DeArrow からユーザーが投稿したタイトルに置き換えます。

View File

@ -29,6 +29,11 @@ Close: '닫기'
Back: '뒤로가기'
Forward: '앞으로가기'
# Global
# Anything shared among components / views should be put here
Global:
Videos: '비디오'
Version {versionNumber} is now available! Click for more details: '{versionNumber}
버전이 사용가능합니다! 클릭하여 자세한 정보를 확인하세요'
Download From Site: '사이트로부터 다운로드'

View File

@ -30,6 +30,11 @@ Back: 'Atgal'
Forward: 'Pirmyn'
Open New Window: 'Atidaryti naują langą'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Vaizdo įrašai'
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'

View File

@ -29,6 +29,13 @@ Close: 'Lukk'
Back: 'Tilbake'
Forward: 'Framover'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videoer'
Shorts: Kortvideoer
Live: Direkte
# Search Bar
Search / Go to URL: 'Søk/gå til nettadresse'
# In Filter Button
@ -545,7 +552,6 @@ Channel:
This channel currently does not have any posts: Denne kanalen har ingen oppføringer
Shorts:
This channel does not currently have any shorts: Denne kanalen har ingen kortvideoer
Shorts: Kortvideoer
Live:
Live: Direkte
This channel does not currently have any live streams: Denne kanalen har ikke

View File

@ -30,6 +30,11 @@ Close: 'Sluiten'
Back: 'Ga terug'
Forward: 'Ga vooruit'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Video''s'
# Search Bar
Search / Go to URL: 'Zoeken / Ga naar URL'
# In Filter Button

View File

@ -30,6 +30,11 @@ Close: 'Lukk'
Back: 'Tilbake'
Forward: 'Framover'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videoar'
Version {versionNumber} is now available! Click for more details: 'Versjon {versionNumber}
er no tilgjengeleg! Klikk for meir informasjon'
Download From Site: 'Last ned frå nettstaden'

View File

@ -28,6 +28,13 @@ Close: 'Zamknij'
Back: 'Wstecz'
Forward: 'Naprzód'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Filmy'
Shorts: Filmy Short
Live: Transmisje
# Search Bar
Search / Go to URL: 'Szukaj / Przejdź do adresu URL'
# In Filter Button
@ -84,6 +91,9 @@ Subscriptions:
Disabled Automatic Fetching: Wyłączyłeś automatyczne pobieranie subskrypcji. Odśwież
subskrypcje, by je zobaczyć.
Empty Channels: Twoje subskrypcje nie mają obecnie żadnych filmów.
All Subscription Tabs Hidden: Wszystkie karty subskrypcji są pochowane. Aby je zobaczyć,
proszę odznaczyć ich ukrycie w podgrupie „{subsection}” grupy „{settingsSection}”.
Subscriptions Tabs: Karty subskrypcji
Trending:
Trending: 'Na czasie'
Trending Tabs: Karty „Na czasie”
@ -415,6 +425,12 @@ Settings:
Channel Page: Strona kanału
General: Ogólne
Watch Page: Strona odtwarzacza
Subscriptions Page: Strona subskrypcji
Hide Channel Releases: Schowaj wydawnictwa kanału
Hide Channel Podcasts: Schowaj podkasty kanału
Hide Subscriptions Videos: Schowaj filmy z subskrypcji
Hide Subscriptions Shorts: Schowaj filmy Short z subskrypcji
Hide Subscriptions Live: Schowaj transmisje live z subskrypcji
The app needs to restart for changes to take effect. Restart and apply change?: Aplikacja
musi zostać ponownie uruchomiona, aby zmiany zostały wprowadzone. Uruchomić ponownie
i zastosować zmiany?
@ -448,6 +464,7 @@ Settings:
Prompt To Skip: Zapytaj, czy pominąć
Do Nothing: Nic nie rób
Category Color: Kolor segmentu
UseDeArrowTitles: Użyj tytułów filmów z DeArrow
External Player Settings:
Custom External Player Arguments: Niestandardowe argumenty zewnętrznego odtwarzacza
Custom External Player Executable: Niestandardowy plik wykonywalny zewnętrznego
@ -603,9 +620,15 @@ Channel:
This channel does not currently have any live streams: Ten kanał nie ma obecnie
żadnych transmisji
Shorts:
Shorts: Filmy Short
This channel does not currently have any shorts: Ten kanał nie ma obecnie żadnych
filmów Short
Releases:
Releases: Wydania
This channel does not currently have any releases: Ten kanał nie ma obecnie żadnych
wydawnictw
Podcasts:
Podcasts: Podkasty
This channel does not currently have any podcasts: Ten kanał nie ma żadnych podkastów
Video:
Mark As Watched: 'Oznacz jako obejrzany'
Remove From History: 'Usuń z historii'
@ -676,7 +699,7 @@ Video:
Reverse Playlist: Odwróć playlistę
Shuffle Playlist: Losuj z playlisty
Loop Playlist: Zapętl playlistę
Starting soon, please refresh the page to check again: Wkrótce się zacznie, proszę
Starting soon, please refresh the page to check again: Wkrótce się zacznie. Proszę
odświeżyć stronę, aby ponownie sprawdzić
Audio:
Best: Najlepsza
@ -965,8 +988,14 @@ Tooltips:
to większe użycie pamięci RAM.
Distraction Free Settings:
Hide Channels: Wprowadź nazwę albo ID kanału, aby schować wszystkie filmy i playlisty
tego kanału, oraz sam kanał z wyszukiwań, oraz z zakładki „Na czasie”. Nazwa
kanału musi być dokładnym dopasowaniem, z uwzględnieniem wielkości liter.
tego kanału, oraz sam kanał z wyszukiwań, z zakładek „Na czasie” i „Popularne”
oraz z polecanych. Nazwa kanału musi być dokładnym dopasowaniem, z uwzględnieniem
wielkości liter.
Hide Subscriptions Live: Ta opcja została nadpisana opcją ogólną „{appWideSetting}”
z podgrupy „{subsection}” grupy „{settingsSection}”
SponsorBlock Settings:
UseDeArrowTitles: Zastąp tytuły filmów tytułami zasugerowanymi przez użytkowników
DeArrow.
Playing Next Video Interval: Odtwarzanie kolejnego filmu już za chwilę. Wciśnij aby
przerwać. | Odtwarzanie kolejnego filmu za {nextVideoInterval} sekundę. Wciśnij
aby przerwać. | Odtwarzanie kolejnego filmu za {nextVideoInterval} sekund. Wciśnij

View File

@ -28,6 +28,13 @@ Close: 'Fechar'
Back: 'Voltar'
Forward: 'Avançar'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Vídeos'
Shorts: Shorts
Live: Ao vivo
# Search Bar
Search / Go to URL: 'Buscar/Ir ao URL'
# In Filter Button
@ -447,6 +454,7 @@ Settings:
Auto Skip: Pular automaticamente
Skip Option: Opção de pular
Category Color: Cor da categoria
UseDeArrowTitles: Utilizar títulos de vídeo DeArrow
External Player Settings:
Custom External Player Arguments: Argumentos de player externo personalizados
External Player: Player externo
@ -592,7 +600,6 @@ Channel:
This channel does not currently have any live streams: Este canal não tem nenhuma
transmissão ao vivo no momento
Shorts:
Shorts: Shorts
This channel does not currently have any shorts: Este canal não tem atualmente
nenhum short
Video:
@ -958,9 +965,13 @@ Tooltips:
Replace HTTP Cache: Desabilita o cache HTTP baseado em disco do Electron e habilita
um cache de imagem em memória personalizado. Levará ao aumento do uso de RAM.
Distraction Free Settings:
Hide Channels: Digite um nome ou ID de canal para ocultar todos os vídeos, listas
de reprodução e o próprio canal dos resultados da busca ou nas tendências. O
nome do canal digitado deve coincidir exatamente, observando maiúsculas e minúsculas.
Hide Channels: Digite um nome ou ID de canal para ocultar todos os vídeos, as
listas de reprodução e o próprio canal dos resultados da busca, tendências,
mais populares e recomendados. O nome do canal digitado deve coincidir exatamente,
observando maiúsculas e minúsculas.
SponsorBlock Settings:
UseDeArrowTitles: Substituir títulos de vídeo por títulos enviados pelo usuário
a partir do DeArrow.
More: Mais
Playing Next Video Interval: Reproduzindo o próximo vídeo imediatamente. Clique para
cancelar. | Reproduzindo o próximo vídeo em {nextVideoInterval} segundo(s). Clique

View File

@ -30,6 +30,13 @@ Close: Fechar
Back: Recuar
Forward: Avançar
# Global
# Anything shared among components / views should be put here
Global:
Videos: Vídeos
Shorts: Curtas
Live: Em directo
Version {versionNumber} is now available! Click for more details: A versão {versionNumber}
já está disponível! Clique para mais detalhes
Download From Site: Descarregar do site
@ -341,6 +348,8 @@ Settings:
Channel Page: Página do canal
Watch Page: Ver página
General: Geral
Hide Channel Releases: Ocultar as libertações do canal
Hide Channel Podcasts: Ocultar podcasts do canal
Data Settings:
Data Settings: Definições de dados
Select Import Type: Escolher tipo de importação
@ -420,6 +429,7 @@ Settings:
Prompt To Skip: Perguntar se quero ignorar
Do Nothing: Nada fazer
Category Color: Cor da categoria
UseDeArrowTitles: Utilizar títulos de vídeo DeArrow
External Player Settings:
Custom External Player Arguments: Argumentos do reprodutor externo personalizado
Custom External Player Executable: Executável de reprodutor externo personalizado
@ -584,9 +594,16 @@ Channel:
This channel does not currently have any live streams: Este canal não tem atualmente
nenhuma transmissão ao vivo
Shorts:
Shorts: Curtas
This channel does not currently have any shorts: Este canal não tem atualmente
nenhum canal curto
Releases:
Releases: Lançamentos
This channel does not currently have any releases: Este canal não tem atualmente
nenhum lançamento
Podcasts:
This channel does not currently have any podcasts: Este canal não tem atualmente
podcasts
Podcasts: Podcasts
Video:
Mark As Watched: Marcar como visto
Remove From History: Remover do histórico
@ -869,10 +886,13 @@ Tooltips:
Replace HTTP Cache: Desativa a cache HTTP Electron e ativa uma cache de imagem
na memória personalizada. Levará ao aumento da utilização de RAM.
Distraction Free Settings:
Hide Channels: Introduza o nome de um canal ou ID do canal para esconder todos
os vídeos, listas de reprodução e o próprio canal de aparecer em busca ou em
tendências. O nome do canal introduzido tem de ser uma correspondência completa
e é sensível a maiúsculas e minúsculas.
Hide Channels: Introduza um nome de canal ou um ID de canal para ocultar todos
os vídeos, listas de reprodução e o próprio canal de aparecerem na pesquisa,
tendências, mais populares e recomendados. O nome do canal introduzido tem de
corresponder na totalidade e é sensível a maiúsculas e minúsculas.
SponsorBlock Settings:
UseDeArrowTitles: Substituir títulos de vídeo por títulos enviados pelo utilizador
a partir do DeArrow.
Local API Error (Click to copy): API local encontrou um erro (clique para copiar)
Invidious API Error (Click to copy): API Invidious encontrou um erro (clique para
copiar)

View File

@ -29,6 +29,13 @@ Close: 'Fechar'
Back: 'Recuar'
Forward: 'Avançar'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Vídeos'
Shorts: Curtas
Live: Em directo
Version {versionNumber} is now available! Click for more details: 'A versão {versionNumber}
está disponível! Clique aqui para mais informações'
Download From Site: 'Descarregar do site'
@ -89,6 +96,10 @@ Subscriptions:
Empty Channels: Os canais subscritos não têm, atualmente, quaisquer vídeos.
Disabled Automatic Fetching: Desativou a atualização automática de subscrições.
Atualize as subscrições para as ver aqui.
Subscriptions Tabs: Separadores de subscrições
All Subscription Tabs Hidden: Todos os separadores de subscrição estão ocultos.
Para ver o conteúdo aqui, desoculte alguns separadores na secção "{subsection}"
em "{settingsSection}".
Trending:
Trending: 'Tendências'
Trending Tabs: Separador de tendências
@ -391,6 +402,7 @@ Settings:
Prompt To Skip: Perguntar se quero ignorar
Do Nothing: Nada fazer
Category Color: Cor da categoria
UseDeArrowTitles: Utilizar títulos de vídeo DeArrow
Proxy Settings:
Error getting network information. Is your proxy configured properly?: Erro ao
obter informações da rede. O seu proxy está configurado corretamente?
@ -438,6 +450,12 @@ Settings:
Channel Page: Página do canal
Watch Page: Ver página
General: Geral
Subscriptions Page: Página de subscrições
Hide Channel Podcasts: Ocultar podcasts do canal
Hide Channel Releases: Ocultar as libertações do canal
Hide Subscriptions Videos: Ocultar subscrições de vídeos
Hide Subscriptions Shorts: Ocultar subscrições de vídeos curtos
Hide Subscriptions Live: Ocultar subscrições de vídeos em direto
External Player Settings:
Custom External Player Arguments: Argumentos do reprodutor externo personalizado
Custom External Player Executable: Executável de reprodutor externo personalizado
@ -631,9 +649,16 @@ Channel:
This channel does not currently have any live streams: Este canal não tem atualmente
nenhuma transmissão ao vivo
Shorts:
Shorts: Curtas
This channel does not currently have any shorts: Este canal não tem atualmente
nenhum canal curto
Releases:
Releases: Lançamentos
This channel does not currently have any releases: Este canal não tem atualmente
nenhum lançamento
Podcasts:
Podcasts: Podcasts
This channel does not currently have any podcasts: Este canal não tem atualmente
podcasts
Video:
Mark As Watched: 'Marcar como visto'
Remove From History: 'Remover do histórico'
@ -958,10 +983,15 @@ Tooltips:
Replace HTTP Cache: Desativa a cache HTTP Electron e ativa uma cache de imagem
na memória personalizada. Levará ao aumento da utilização de RAM.
Distraction Free Settings:
Hide Channels: Digite um nome ou ID do canal para ocultar todos os vídeos, listas
de reprodução e o próprio canal, de forma a não aparecer em pesquisas ou tendências.
O nome do canal inserido deve ser exatamente igual ao nome do canal e é sensível
a maiúsculas e minúsculas.
Hide Channels: Introduza um nome de canal ou um ID de canal para ocultar todos
os vídeos, listas de reprodução e o próprio canal de aparecerem na pesquisa,
tendências, mais populares e recomendados. O nome do canal introduzido tem de
corresponder na totalidade e é sensível a maiúsculas e minúsculas.
Hide Subscriptions Live: Esta definição é substituída pela definição de toda a
aplicação "{appWideSetting}", na secção "{subsection}" da "{settingsSection}"
SponsorBlock Settings:
UseDeArrowTitles: Substituir títulos de vídeo por títulos enviados pelo utilizador
a partir do DeArrow.
Search Bar:
Clear Input: Limpar entrada
Are you sure you want to open this link?: Tem a certeza de que deseja abrir a ligação?

View File

@ -29,6 +29,13 @@ Close: 'Închideți'
Back: 'Înapoi'
Forward: 'Înainte'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videoclipuri'
Shorts: Shorts
Live: Live
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'
@ -60,12 +67,14 @@ Search Filters:
Videos: 'Videoclipuri'
Channels: 'Canale'
#& Playlists
Movies: Filme
Duration:
Duration: 'Durata'
All Durations: 'Toate duratele'
Short (< 4 minutes): 'Scurt (< 4 minute)'
Long (> 20 minutes): 'Lung (> 20 minute)'
# On Search Page
Medium (4 - 20 minutes): Mediu (4 - 20 minute)
Search Results: 'Rezultatele căutării'
Fetching results. Please wait: 'Se obțin rezultatele. Vă rugăm să așteptați'
Fetch more results: 'Obțineți mai multe rezultate'
@ -89,6 +98,7 @@ Subscriptions:
Empty Channels: Canalele la care sunteți abonat(ă) nu au în prezent clipuri video.
Disabled Automatic Fetching: Ai dezactivat obținerea automată de abonamente. Reîmprospătează
abonamentele pentru a le vedea aici.
Subscriptions Tabs: Filele Abonamente
Trending:
Trending: 'Tendințe'
Trending Tabs: File în tendințe
@ -216,6 +226,7 @@ Settings:
Disable Smooth Scrolling: Dezactivați derularea lină
Expand Side Bar by Default: Extindeți bara laterală în mod implicit
Hide Side Bar Labels: Ascunde etichetele din bara laterală
Hide FreeTube Header Logo: Ascundeți logo-ul FreeTube Header
Player Settings:
Player Settings: 'Setări player'
Force Local Backend for Legacy Formats: 'Forțați backend-ul local pentru formatele
@ -269,6 +280,11 @@ Settings:
Max Video Playback Rate: Viteza maximă de redare
Scroll Playback Rate Over Video Player: Schimbă viteza de redare cu ajutorul rotiței
de scroll
Enter Fullscreen on Display Rotate: Intrați pe ecran complet pe afișaj Rotire
Comment Auto Load:
Comment Auto Load: Comentariu Încărcare automată
Skip by Scrolling Over Video Player: Omiteți derulând peste player-ul video
Allow DASH AV1 formats: Permiteți formatele DASH AV1
Privacy Settings:
Privacy Settings: 'Setări de confidențialitate'
Remember History: 'Reține istoric'
@ -410,6 +426,17 @@ Settings:
Parental Control Settings: Setările controlului parental
Show Family Friendly Only: Arata numai conținut family friendly
Hide Search Bar: Ascunde bara de căutare
Password Dialog:
Password Incorrect: Parolă incorectă
Unlock: Deblocați
Password: Parolă
Enter Password To Unlock: Introduceți parola pentru a debloca setările
Password Settings:
Remove Password: Eliminați parola
Password Settings: Setări parolă
Set Password To Prevent Access: Setați o parolă pentru a împiedica accesul la
setări
Set Password: Setați parola
About:
#On About page
About: 'Despre'
@ -518,6 +545,16 @@ Channel:
About: 'Despre'
Channel Description: 'Descrierea canalului'
Featured Channels: 'Canale recomandate'
Joined: S-a alăturat pe
Location: Locație
Community:
Community: Comunitate
This channel currently does not have any posts: Acest canal nu are momentan nicio
postare
This channel is age-restricted and currently cannot be viewed in FreeTube.: Acest
canal are restricții de vârstă și momentan nu poate fi vizionat în FreeTube.
This channel does not exist: Acest canal nu există
This channel does not allow searching: Acest canal nu permite căutarea
Video:
Mark As Watched: 'Marcați ca vizionat'
Remove From History: 'Eliminați din istoric'
@ -586,6 +623,7 @@ Video:
Ago: 'În urmă'
Upcoming: 'În premieră la'
Less than a minute: Mai putin de un minut
In less than a minute: În mai puțin de un minut
Published on: 'Publicat pe'
Publicationtemplate: 'acum {number} {unit}'
#& Videos
@ -655,6 +693,7 @@ Video:
Mimetype: Mimetype
Premieres on: Premieră pe
Premieres in: Are premiera în
Scroll to Bottom: Derulați până jos
Videos:
#& Sort By
Sort By:
@ -701,6 +740,7 @@ Share:
YouTube Channel URL copied to clipboard: URL-ul canalului YouTube copiat în clipboard
Invidious Channel URL copied to clipboard: URL-ul Invidious al canalului a fost
copiat în clipboard
Share Channel: Distribuie canalul
Mini Player: 'Mini Player'
Comments:
Comments: 'Comentarii'
@ -810,9 +850,9 @@ Tooltips:
a reveni rapid la rata de redare implicită (1x, cu excepția cazului în care
aceasta a fost modificată în setări).
General Settings:
External Link Handling: "Alegeți comportamentul implicit atunci când se face clic\
\ pe un link care nu poate fi deschis în FreeTube.\nÎn mod implicit, FreeTube\
\ va deschide link-ul pe care s-a făcut clic în browserul dvs. implicit.\n"
External Link Handling: "Alegeți comportamentul implicit atunci când se face clic
pe un link care nu poate fi deschis în FreeTube.\nÎn mod implicit, FreeTube
va deschide link-ul pe care s-a făcut clic în browserul dvs. implicit.\n"
Region for Trending: Regiunea de tendințe vă permite să alegeți ce videoclipuri
în tendințe din fiecare țară doriți să fie afișate.
Invidious Instance: Instanța Invidious la care FreeTube se va conecta pentru apelurile
@ -848,3 +888,14 @@ Channels:
Unsubscribe Prompt: Ești sigur că dorești să te dezabonezi de la "{channelName}"?
Screenshot Success: Capturi de ecran salvate ca "{filePath}"
Screenshot Error: Captura de ecran a eșuat
Preferences: Preferințe
Chapters:
Chapters: Capitole
'Chapters list visible, current chapter: {chapterName}': 'Lista de capitole vizibilă,
capitolul curent: {chapterName}'
'Chapters list hidden, current chapter: {chapterName}': 'Lista de capitole ascunsă,
capitolul curent: {chapterName}'
Clipboard:
Cannot access clipboard without a secure connection: Nu se poate accesa clipboard-ul
fără o conexiune securizată
Copy failed: Copierea în clipboard a eșuat

View File

@ -27,6 +27,13 @@ Close: 'Закрыть'
Back: 'Назад'
Forward: 'Вперёд'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Видео'
Shorts: Короткие видео
Live: Трансляции
# Search Bar
Search / Go to URL: 'Поиск / Перейти по адресу'
# In Filter Button
@ -83,6 +90,9 @@ Subscriptions:
Disabled Automatic Fetching: Вы отключили автоматическое получение подписок. Обновите
подписки, чтобы отобразить результат.
Empty Channels: Ваши подписанные каналы в настоящее время не содержат видео.
Subscriptions Tabs: Вкладки подписок
All Subscription Tabs Hidden: Все вкладки подписок скрыты. Чтобы увидеть содержимое
здесь, пожалуйста, раскройте некоторые вкладки в разделе «{subsection}» в «{settingsSection}».
Trending:
Trending: 'Тренды'
Trending Tabs: Тренды
@ -409,6 +419,12 @@ Settings:
Channel Page: Страница канала
Watch Page: Страница просмотра
General: Основные
Subscriptions Page: Страница подписок
Hide Channel Releases: Скрыть выпуски канала
Hide Subscriptions Videos: Скрыть видео из подписок
Hide Subscriptions Live: Скрыть трансляции из подписок
Hide Channel Podcasts: Скрыть звукопередачи канала
Hide Subscriptions Shorts: Скрыть короткие видео из подписок
The app needs to restart for changes to take effect. Restart and apply change?: Чтобы
изменения вступили в силу, необходимо перезапустить приложение. Перезапустить
и применить изменения?
@ -441,6 +457,7 @@ Settings:
Show In Seek Bar: Показать сегмент
Do Nothing: Ничего не делать
Category Color: Цвет категории
UseDeArrowTitles: Использовать заголовки видео «DeArrow»
External Player Settings:
Custom External Player Arguments: Аргументы внешнего проигрывателя
Custom External Player Executable: Исполняемый файл внешнего проигрывателя
@ -588,9 +605,16 @@ Channel:
This channel does not currently have any live streams: На этом канале в настоящее
время нет прямых трансляций
Shorts:
Shorts: Короткие видео
This channel does not currently have any shorts: На этом канале пока что нет коротких
видео
Podcasts:
Podcasts: Звукопередачи
This channel does not currently have any podcasts: На этом канале пока нет ни
одной звукопередачи
Releases:
Releases: Выпуски
This channel does not currently have any releases: На этом канале пока нет ни
одного выпуска
Video:
Mark As Watched: 'Отметить как просмотренное'
Remove From History: 'Удалить из истории'
@ -954,8 +978,14 @@ Tooltips:
кэш изображений в памяти. Приведёт к увеличению использования оперативной памяти.
Distraction Free Settings:
Hide Channels: Введите название канала или его идентификатор, чтобы скрыть все
видео, подборки и сам канал от показа в поиске или трендах. Введённое название
канала должно полностью совпадать и учитывать регистр.
видео, подборки и сам канал от показа в поиске, трендах, наиболее просматриваемых
и желательных. Введённое название канала должно полностью совпадать и учитывать
регистр.
Hide Subscriptions Live: Эта настройка переопределена общей настройкой «{appWideSetting}»,
в подразделе «{subsection}» раздела «{settingsSection}»
SponsorBlock Settings:
UseDeArrowTitles: Заменить пользовательски-размещённые заголовки на заголовки,
предоставляемые «DeArrow».
More: Больше
Playing Next Video Interval: Воспроизведение следующего видео без задержки. Нажмите
для отмены. | Воспроизведение следующего видео через {nextVideoInterval} сек. Нажмите

View File

@ -28,6 +28,11 @@ Close: 'Zavrieť'
Back: 'Späť'
Forward: 'Vpred'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videá'
# Search Bar
Search / Go to URL: 'Hľadať / Ísť na adresu URL'
# In Filter Button

View File

@ -30,6 +30,11 @@ Close: 'Zapri'
Back: 'Nazaj'
Forward: 'Naprej'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videoposnetki'
Version {versionNumber} is now available! Click for more details: 'Na voljo je različica
{versionNumber}!· Za več podrobnosti kliknite tukaj'
Download From Site: 'Prenesi iz spletne strani'

View File

@ -29,6 +29,11 @@ Close: 'Затвори'
Back: 'Назад'
Forward: 'Напред'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Видео'
Version {versionNumber} is now available! Click for more details: 'Верзија {versionNumber}
је сада достуна! Кликните за више детаља'
Download From Site: 'Преузми са сајта'

View File

@ -29,6 +29,13 @@ Close: 'Stäng'
Back: 'Tillbaka'
Forward: 'Framåt'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videor'
Shorts: Shorts
Live: Live
Version {versionNumber} is now available! Click for more details: 'Versionen {versionNumber}
är nu tillgänglig! Klicka för mer detaljer'
Download From Site: 'Ladda ner från sajten'
@ -272,6 +279,8 @@ Settings:
Allow DASH AV1 formats: Tillåt DASH AV1-format
Scroll Playback Rate Over Video Player: Skrolla uppspelningshastighet över videospelaren
Enter Fullscreen on Display Rotate: Fullskärm vid skärmrotation
Comment Auto Load:
Comment Auto Load: Autoladda kommenterer
Privacy Settings:
Privacy Settings: 'Integritetsinställningar'
Remember History: 'Aktivera visningshistorik'
@ -390,6 +399,15 @@ Settings:
Hide Upcoming Premieres: Dölj permiärer
Display Titles Without Excessive Capitalisation: Visa titlar utan överdriven versalisering
Hide Channels Placeholder: Kanalnamn eller ID
Hide Featured Channels: Dölj Utvalda kanaler
Hide Channel Shorts: Dölj Kanal Shorts
Sections:
Side Bar: Sidofält
General: Allmänt
Channel Page: Kanal sida
Watch Page: Titta sida
Hide Channel Community: Dölj Kanal gemenskap
Hide Channel Playlists: Dölj Kanal spellistor
The app needs to restart for changes to take effect. Restart and apply change?: Starta
om FreeTube nu för att tillämpa ändringarna?
Proxy Settings:
@ -419,7 +437,9 @@ Settings:
Show In Seek Bar: Visa i tidslinjen
Do Nothing: Gör ingenting
Auto Skip: Hoppa över automatiskt
Prompt To Skip: Prompt för att hoppa över
Category Color: Kategorifärg
UseDeArrowTitles: Använd DeArrow-videotitlar
External Player Settings:
Ignore Unsupported Action Warnings: Ignorera händelsevarningar som inte stöds
External Player: Extern spelare
@ -604,6 +624,13 @@ Channel:
Community: Gemenskap
This channel currently does not have any posts: Denna kanal har för närvarande
inga inlägg
Shorts:
This channel does not currently have any shorts: Den här kanalen har för närvarande
inga shorts
Live:
Live: Live
This channel does not currently have any live streams: Den här kanalen har för
närvarande inga liveströmmar
Video:
Mark As Watched: 'Markera som sedd'
Remove From History: 'Ta bort från historik'
@ -637,8 +664,8 @@ Video:
inte i denna version.'
'Chat is disabled or the Live Stream has ended.': 'Chatten är inaktiverad eller
så har Live strömmen avslutats.'
Live chat is enabled. Chat messages will appear here once sent.: 'Livechatt är
aktiverat. Meddelanden visas här när de har skickats.'
Live chat is enabled. Chat messages will appear here once sent.: 'Livechatt är aktiverat.
Meddelanden visas här när de har skickats.'
'Live Chat is currently not supported with the Invidious API. A direct connection to YouTube is required.': 'Livechatt
stöds för närvarande inte med Invidious API. En direktanslutning till YouTube
krävs.'
@ -736,6 +763,8 @@ Video:
Upcoming: Kommande
Show Super Chat Comment: Visa Super Chat-kommentarer
Scroll to Bottom: Skrolla till botten
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Livechatt
är inte tillgängligt för den här strömmen. Den kan ha inaktiverats av uppladdaren.
Videos:
#& Sort By
Sort By:
@ -874,9 +903,9 @@ Tooltips:
Preferred API Backend: Välj den resurs som FreeTube använder för att få fram data.
Det lokala API:et är en inbyggd utdragare. Invidious API kräver en Invidious
server att ansluta till.
External Link Handling: "Välj standardförfarande när en länk klickas, som inte\
\ kan öppnas i FreeTube är. \nStandard är att FreeTube kommer öppna länken i\
\ din standardwebbläsare.\n"
External Link Handling: "Välj standardförfarande när en länk klickas, som inte
kan öppnas i FreeTube är. \nStandard är att FreeTube kommer öppna länken i din
standardwebbläsare.\n"
Privacy Settings:
Remove Video Meta Files: Om vald, kommer FreeTube automatiskt att kasta metadata
filer som skapades under uppspelning, när sidan stängs.
@ -893,9 +922,15 @@ Tooltips:
Invidiousinställningar påverkar inte externa videospelare.
DefaultCustomArgumentsTemplate: "(Standard: '{defaultCustomArguments}')"
Distraction Free Settings:
Hide Channels: Skriv in kanalnamn eller kanal-ID för att dölja alla videor, spellistor
och kanalen från att synas i sökfältet eller i populäraste kategorin. Kanalens
namn måste skrivas in i sin helhet och är skiftlägeskänsligt.
Hide Channels: Ange ett kanalnamn eller kanal-ID för att dölja alla videor, spellistor
och själva kanalen från att visas i sökningar, trender, populäraste och rekommenderade.
Det angivna kanalnamnet måste vara en fullständig matchning och är skiftlägeskänsligt.
Experimental Settings:
Replace HTTP Cache: Inaktiverar Electrons diskbaserade HTTP-cache och aktiverar
en anpassad bildcache i minnet. Kommer att leda till ökad RAM-användning.
SponsorBlock Settings:
UseDeArrowTitles: Ersätt videotitlar med titlar som användare har skickat in från
DeArrow.
More: Mer
Open New Window: Öppna nytt fönster
Playing Next Video Interval: Spelar strax nästa video. Klicka för att stoppa. | Spelar
@ -946,3 +981,7 @@ Chapters:
Downloading has completed: '"{videoTitle}" har laddats ner'
Starting download: Påbörjar nerladdning av "{videoTitle}"
Downloading failed: Det uppstod ett problem med nerladdning av "{videoTitle}"
Hashtag:
Hashtag: Hashtaggar
This hashtag does not currently have any videos: Den här hashtaggen har för närvarande
inga videor

View File

@ -29,6 +29,13 @@ Close: 'Kapat'
Back: 'Geri'
Forward: 'İleri'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Videolar'
Shorts: Kısa Videolar
Live: Canlı
Version {versionNumber} is now available! Click for more details: '{versionNumber}
sürümü çıktı! Daha fazla ayrıntı için tıklayın'
Download From Site: 'Siteden indir'
@ -88,6 +95,10 @@ Subscriptions:
Disabled Automatic Fetching: Otomatik abonelik getirmeyi devre dışı bıraktınız.
Abonelikleri burada görmek için yenileyin.
Empty Channels: Abone olduğunuz kanallarda şu anda herhangi bir video yok.
Subscriptions Tabs: Abonelikler Sekmeleri
All Subscription Tabs Hidden: Tüm abonelik sekmeleri gizlidir. Buradaki içeriği
görmek için lütfen "{settingsSection}" içindeki "{subsection}" bölümündeki bazı
sekmelerin gizliliğini kaldırın.
Trending:
Trending: 'Öne Çıkanlar'
Trending Tabs: Öne Çıkanlar Sekmeleri
@ -410,6 +421,12 @@ Settings:
Channel Page: Kanal Sayfası
Watch Page: İzleme Sayfası
General: Genel
Subscriptions Page: Abonelikler Sayfası
Hide Channel Releases: Kanal Yayınlarını Gizle
Hide Channel Podcasts: Kanal Podcast'lerini Gizle
Hide Subscriptions Videos: Abonelik Videolarını Gizle
Hide Subscriptions Shorts: Abonelik Kısa Videolarını Gizle
Hide Subscriptions Live: Abonelik Canlı Yayınlarını Gizle
The app needs to restart for changes to take effect. Restart and apply change?: Değişikliklerin
etkili olması için uygulamanın yeniden başlatılması gerekiyor. Yeniden başlatılsın
ve değişiklikler uygulansın mı?
@ -640,9 +657,16 @@ Channel:
This channel does not currently have any live streams: Bu kanalda şu anda herhangi
bir canlı yayın yok
Shorts:
Shorts: Kısa Videolar
This channel does not currently have any shorts: Bu kanalda şu anda hiç kısa video
yok
Podcasts:
Podcasts: Podcast'ler
This channel does not currently have any podcasts: Bu kanalda şu anda herhangi
bir podcast yok
Releases:
This channel does not currently have any releases: Bu kanalda şu anda herhangi
bir yayın yok
Releases: Yayınlar
Video:
Mark As Watched: 'İzlendi Olarak İşaretle'
Remove From History: 'Geçmişden Kaldır'
@ -948,9 +972,13 @@ Tooltips:
ve özel bir bellek içi resim önbelleğini etkinleştirir. RAM kullanımının artmasına
neden olacaktır.
Distraction Free Settings:
Hide Channels: Tüm videoların, oynatma listelerinin ve kanalın kendisinin arama
veya öne çıkanlarda görünmesini engellemek için bir kanal adı veya kanal kimliği
girin. Girilen kanal adı tam olarak eşleşmelidir ve büyük/küçük harfe duyarlıdır.
Hide Channels: Tüm videoların, oynatma listelerinin ve kanalın kendisinin arama,
öne çıkanlar, en popüler ve tavsiye edilenlerde görünmesini engellemek için
bir kanal adı veya kanal kimliği girin. Girilen kanal adı tam olarak eşleşmelidir
ve büyük/küçük harfe duyarlıdır.
Hide Subscriptions Live: Bu ayar, "{settingsSection}" bölümünün "{subsection}"
kısmında yer alan uygulama genelindeki "{appWideSetting}" ayarı tarafından geçersiz
kılınıyor
SponsorBlock Settings:
UseDeArrowTitles: Video başlıklarını DeArrow'dan kullanıcıların gönderdiği başlıklarla
değiştir.

View File

@ -29,6 +29,13 @@ Close: 'Закрити'
Back: 'Назад'
Forward: 'Вперед'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Відео'
Shorts: Shorts
Live: Наживо
Version {versionNumber} is now available! Click for more details: 'Доступна нова
версія {versionNumber} ! Натисніть щоб побачити деталі'
Download From Site: 'Завантажити з сайту'
@ -89,6 +96,9 @@ Subscriptions:
Disabled Automatic Fetching: Ви вимкнули автоматичне отримання підписок. Оновіть
підписки, щоб вони з'явились тут.
Empty Channels: Канали, на які ви підписалися, наразі не містять відео.
Subscriptions Tabs: Вкладки підписок
All Subscription Tabs Hidden: Усі вкладки підписки сховані. Щоб побачити вміст,
будь ласка, відкрийте деякі вкладки в розділі "{subsection}" в "{settingsSection}".
Trending:
Trending: 'Популярне'
Trending Tabs: Популярні вкладки
@ -337,6 +347,12 @@ Settings:
Channel Page: Сторінка каналу
Watch Page: Сторінка перегляду
General: Загальні
Subscriptions Page: Сторінка підписок
Hide Subscriptions Videos: Сховати відео з підписок
Hide Channel Podcasts: Сховати канали подкастів
Hide Channel Releases: Сховати канали випусків
Hide Subscriptions Shorts: Сховати Shorts із підписок
Hide Subscriptions Live: Сховати трансляції з підписок
Data Settings:
Data Settings: 'Налаштування даних'
Select Import Type: 'Оберіть тип імпорту'
@ -571,8 +587,15 @@ Channel:
немає прямих трансляцій
Live: Наживо
Shorts:
Shorts: Shorts
This channel does not currently have any shorts: На цьому каналі немає Shorts
Podcasts:
Podcasts: Подкасти
This channel does not currently have any podcasts: На цьому каналі наразі немає
подкастів
Releases:
This channel does not currently have any releases: Наразі на цьому каналі немає
випусків
Releases: Випуски
Video:
Mark As Watched: 'Позначити переглянутим'
Remove From History: 'Прибрати з історії'
@ -865,8 +888,10 @@ Tooltips:
у пам'яті. Призведе до збільшення використання оперативної пам'яті.
Distraction Free Settings:
Hide Channels: Введіть назву або ID каналу, щоб сховати всі відео, списки відтворення
та сам канал від появи в пошуку або в тренді. Введена назва каналу повинна повністю
збігатися і чутлива до регістру.
та сам канал від появи в пошуку, тренді, найпопулярніших і рекомендованих. Введена
назва каналу повинна повністю збігатися і чутлива до регістру.
Hide Subscriptions Live: Цей параметр перевизначається загальнодоступним налаштуванням
"{appWideSetting}" у розділі "{subsection}" "{settingsSection}"
SponsorBlock Settings:
UseDeArrowTitles: Замінити назви відео на надіслані користувачем назви з DeArrow.
Local API Error (Click to copy): 'Помилка локального API (натисніть, щоб скопіювати)'

View File

@ -28,6 +28,11 @@ Close: 'Đóng'
Back: 'Quay lại'
Forward: 'Tiến tới'
# Global
# Anything shared among components / views should be put here
Global:
Videos: 'Video'
# Search Bar
Search / Go to URL: 'Tìm kiếm / Đi đến URL'
# In Filter Button

View File

@ -27,6 +27,13 @@ Close: '关闭'
Back: '后退'
Forward: '前进'
# Global
# Anything shared among components / views should be put here
Global:
Videos: '视频'
Shorts: 短视频
Live: 直播
# Search Bar
Search / Go to URL: '搜索 / 前往URL'
# In Filter Button
@ -79,6 +86,9 @@ Subscriptions:
Error Channels: 有错误的频道
Disabled Automatic Fetching: 你仅用了自动订阅获取。刷新订阅在此处看到它们。
Empty Channels: 你订阅的频道当前没有任何视频。
Subscriptions Tabs: 订阅标签页
All Subscription Tabs Hidden: 所有的订阅标签页均被隐藏。要在此查看内容,请在 "{subsection}" 部分取消隐藏某些标签页,此部分位于
"{settingsSection}"
Trending:
Trending: '热门'
Trending Tabs: 流行标签
@ -373,6 +383,12 @@ Settings:
Channel Page: 频道页
Watch Page: 观看页
General: 常规
Subscriptions Page: 订阅页
Hide Channel Podcasts: 隐藏频道播客
Hide Channel Releases: 隐藏频道发布
Hide Subscriptions Shorts: 隐藏订阅短视频
Hide Subscriptions Videos: 隐藏订阅视频
Hide Subscriptions Live: 隐藏订阅直播
The app needs to restart for changes to take effect. Restart and apply change?: 应用需要重启让修改生效。重启以应用修改?
Proxy Settings:
Proxy Protocol: 代理协议
@ -534,7 +550,12 @@ Channel:
This channel does not currently have any live streams: 此频道当前没有任何直播流
Shorts:
This channel does not currently have any shorts: 此频道目前没有任何短视频
Shorts: 短视频
Podcasts:
This channel does not currently have any podcasts: 此频道当前无任何播客节目
Podcasts: 播客
Releases:
Releases: 发布
This channel does not currently have any releases: 此频道当前无任何发布
Video:
Open in YouTube: '在YouTube中打开'
Copy YouTube Link: '复制YouTube链接'
@ -837,7 +858,9 @@ Tooltips:
Experimental Settings:
Replace HTTP Cache: 禁用 Electron 基于磁盘的 HTTP 缓存,启用自定义内存中图像缓存。会增加内存的使用。
Distraction Free Settings:
Hide Channels: 输入频道名称或频道 ID 以隐藏所有视频、播放列表和频道本身,使其不出现在搜索结果或热门中。 输入的频道名称必须完全匹配,并且区分大小写。
Hide Channels: 输入频道名称或频道 ID 使其中的所有视频、播放列表和频道本身不出现在搜索结果、时下流行、最受欢迎和推荐中。 输入的频道名称必须完全匹配,并且区分大小写。
Hide Subscriptions Live: 此设置被应用级的 "{appWideSetting}" 设置所覆盖,"{appWideSetting}"
位于 "{subsection}" 部分,该部分在 "{settingsSection}" 中
SponsorBlock Settings:
UseDeArrowTitles: 使用来自 DeArrow 的用户提交的标题替换原始视频标题。
More: 更多

View File

@ -27,6 +27,13 @@ Close: '關閉'
Back: '返回'
Forward: '前進'
# Global
# Anything shared among components / views should be put here
Global:
Videos: '影片'
Shorts: 短片
Live: 直播
# Search Bar
Search / Go to URL: '搜尋/ 前往網址'
# In Filter Button
@ -79,6 +86,8 @@ Subscriptions:
Error Channels: 有錯誤的頻道
Disabled Automatic Fetching: 您已停用自動訂閱擷取。重新整理訂閱以在此處檢視。
Empty Channels: 您訂閱的頻道目前沒有任何影片。
Subscriptions Tabs: 訂閱分頁
All Subscription Tabs Hidden: 所有訂閱分頁均已隱藏。要檢視此處的內容,請取消隱藏「{settingsSection}」中「{subsection}」區塊中的一些分頁。
Trending:
Trending: '發燒影片'
Trending Tabs: 熱門分頁
@ -375,6 +384,12 @@ Settings:
Watch Page: 觀看頁面
General: 一般
Channel Page: 頻道頁面
Subscriptions Page: 訂閱頁面
Hide Channel Podcasts: 隱藏頻道 Podcast
Hide Channel Releases: 隱藏頻道發布
Hide Subscriptions Shorts: 隱藏訂閱短片
Hide Subscriptions Videos: 隱藏訂閱影片
Hide Subscriptions Live: 隱藏訂閱直播
The app needs to restart for changes to take effect. Restart and apply change?: 此變更需要重啟讓修改生效。重啟並且套用變更?
Proxy Settings:
Error getting network information. Is your proxy configured properly?: 取得網路資訊時發生錯誤。您的代理伺服器設定正確嗎?
@ -544,8 +559,13 @@ Channel:
Live: 直播
This channel does not currently have any live streams: 此頻道目前沒有任何直播
Shorts:
Shorts: 短片
This channel does not currently have any shorts: 此頻道目前沒有任何短片
Podcasts:
This channel does not currently have any podcasts: 此頻道目前沒有 podcast
Podcasts: Podcast
Releases:
Releases: 發布
This channel does not currently have any releases: 此頻道目前沒有任何發布
Video:
Open in YouTube: '在YouTube中開啟'
Copy YouTube Link: '複製YouTube連結'
@ -850,7 +870,8 @@ Tooltips:
Experimental Settings:
Replace HTTP Cache: 停用 Electron 以磁碟為基礎的 HTTP 快取並啟用自訂的記憶體圖片快取。會導致記憶體使用量增加。
Distraction Free Settings:
Hide Channels: 輸入頻道名稱或頻道 ID 以隱藏所有影片、播放清單與頻道本身,使其完全不出現在搜尋或趨勢中。輸入的頻道名稱必須完全符合,且區分大小寫。
Hide Channels: 輸入頻道名稱或頻道 ID 以隱藏所有影片、播放清單與頻道本身,使其完全不出現在搜尋、趨勢、熱門與建議中。輸入的頻道名稱必須完全符合,且區分大小寫。
Hide Subscriptions Live: 此設定會被「{settingsSection}」的「{subsection}」部分中應用程式範圍的「{appWideSetting}」設定覆寫
SponsorBlock Settings:
UseDeArrowTitles: 將影片標題取代為 DeArrow 使用者遞交的標題。
Playing Next Video Interval: 馬上播放下一個影片。點擊取消。| 播放下一個影片的時間為{nextVideoInterval}秒。點擊取消。|

939
yarn.lock

File diff suppressed because it is too large Load Diff