mirror of https://github.com/FreeTubeApp/FreeTube
Merge branch 'development' into iv-rss-detect-error-channels-properly
This commit is contained in:
commit
e1aa567337
|
@ -1 +1,17 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Discussions
|
||||
url: https://github.com/FreeTubeApp/FreeTube/discussions/categories/general
|
||||
about: View discussions or start one yourself
|
||||
- name: Questions
|
||||
url: https://github.com/FreeTubeApp/FreeTube/discussions/categories/q-a
|
||||
about: Ask and answer questions
|
||||
- name: Matrix Community
|
||||
url: https://matrix.to/#/+freetube:matrix.org
|
||||
about: Join our Matrix chatroom - "Note: Bugs and Feature requests should be made on GitHub and not in the Matrix room"
|
||||
- name: Translate FreeTube
|
||||
url: https://hosted.weblate.org/engage/free-tube/
|
||||
about: Help translate FreeTube on Weblate
|
||||
- name: FreeTube Documentation
|
||||
url: https://docs.freetubeapp.io/
|
||||
about: View the Documentation to find all relevant information about FreeTube
|
||||
|
|
|
@ -108,91 +108,91 @@ jobs:
|
|||
run: yarn run build:arm64
|
||||
|
||||
- name: Upload Linux .zip x64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_x64
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}.zip
|
||||
|
||||
- name: Upload Linux .7z x64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_x64.7z
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}.7z
|
||||
|
||||
- name: Upload Linux .zip ARMv7l Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_armv7l
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-armv7l.zip
|
||||
|
||||
- name: Upload Linux .7z ARMv7l Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_armv7l.7z
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-armv7l.7z
|
||||
|
||||
- name: Upload Linux .zip ARM64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_arm64
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.zip
|
||||
|
||||
- name: Upload Linux .7z ARM64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_linux_portable_arm64.7z
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.7z
|
||||
|
||||
- name: Upload .deb x64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.deb
|
||||
path: build/freetube_${{ steps.versionNumber.outputs.result }}_amd64.deb
|
||||
|
||||
- name: Upload .deb ARMv7l Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_armv7l.deb
|
||||
path: build/freetube_${{ steps.versionNumber.outputs.result }}_armv7l.deb
|
||||
|
||||
- name: Upload .deb ARM64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_arm64.deb
|
||||
path: build/freetube_${{ steps.versionNumber.outputs.result }}_arm64.deb
|
||||
|
||||
- name: Upload AppImage x64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.AppImage
|
||||
path: build/FreeTube-${{ steps.versionNumber.outputs.result }}.AppImage
|
||||
|
||||
- name: Upload AppImage ARMv7l Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_armv7l.AppImage
|
||||
path: build/FreeTube-${{ steps.versionNumber.outputs.result }}-armv7l.AppImage
|
||||
|
||||
- name: Upload AppImage ARM64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_arm64.AppImage
|
||||
path: build/FreeTube-${{ steps.versionNumber.outputs.result }}-arm64.AppImage
|
||||
|
||||
- name: Upload .rpm x64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.rpm
|
||||
|
@ -201,133 +201,133 @@ jobs:
|
|||
# rpm are not built for armv7l
|
||||
|
||||
- name: Upload .rpm ARM64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_arm64.rpm
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}.aarch64.rpm
|
||||
|
||||
- name: Upload Alpine .apk x64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_alpine_amd64.apk
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}.apk
|
||||
|
||||
- name: Upload Alpine .apk ARMv7l Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-armv7l')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_alpine_armv7l.apk
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-armv7l.apk
|
||||
|
||||
- name: Upload Alpine .apk ARM64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-arm64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_alpine_arm64.apk
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.apk
|
||||
|
||||
- name: Upload Pacman .pacman x64 Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
|
||||
with:
|
||||
name: freetube_${{ steps.versionNumber.outputs.result }}_amd64.pacman
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}.pacman
|
||||
|
||||
# - name: Upload Web Build
|
||||
# uses: actions/upload-artifact@v3
|
||||
# uses: actions/upload-artifact@v4
|
||||
# if: startsWith(matrix.os, 'ubuntu') && startsWith(matrix.runtime, 'linux-x64')
|
||||
# with:
|
||||
# name: freetube_${{ steps.versionNumber.outputs.result }}_static_web
|
||||
# path: dist/web
|
||||
|
||||
- name: Upload Windows x64 .exe Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
|
||||
with:
|
||||
name: freetube-${{ steps.versionNumber.outputs.result }}-setup-x64.exe
|
||||
path: build/freetube Setup ${{ steps.versionNumber.outputs.result }}.exe
|
||||
|
||||
- name: Upload Windows arm64 .exe Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
|
||||
with:
|
||||
name: freetube-${{ steps.versionNumber.outputs.result }}-setup-arm64.exe
|
||||
path: build/freetube Setup ${{ steps.versionNumber.outputs.result }}.exe
|
||||
|
||||
- name: Upload Windows x64 .zip Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
|
||||
with:
|
||||
name: freetube-${{ steps.versionNumber.outputs.result }}-win-x64-portable
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-win.zip
|
||||
|
||||
- name: Upload Windows x64 .7z Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
|
||||
with:
|
||||
name: freetube-${{ steps.versionNumber.outputs.result }}-win-x64-portable.7z
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-win.7z
|
||||
|
||||
- name: Upload Windows arm64 .zip Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
|
||||
with:
|
||||
name: freetube-${{ steps.versionNumber.outputs.result }}-win-arm64-portable
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-win.zip
|
||||
|
||||
- name: Upload Windows arm64 .7z Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
|
||||
with:
|
||||
name: freetube-${{ steps.versionNumber.outputs.result }}-win-arm64-portable.7z
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64-win.7z
|
||||
|
||||
- name: Upload Windows x64 Portable Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-x64')
|
||||
with:
|
||||
name: freetube-${{ steps.versionNumber.outputs.result }}-portable-x64.exe
|
||||
path: build/freetube ${{ steps.versionNumber.outputs.result }}.exe
|
||||
|
||||
- name: Upload Windows arm64 Portable Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'windows') && startsWith(matrix.runtime, 'win-arm64')
|
||||
with:
|
||||
name: freetube-${{ steps.versionNumber.outputs.result }}-portable-arm64.exe
|
||||
path: build/freetube ${{ steps.versionNumber.outputs.result }}.exe
|
||||
|
||||
- name: Upload Mac x64 .dmg Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
|
||||
with:
|
||||
name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.dmg
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}.dmg
|
||||
|
||||
# - name: Upload Mac arm64 .dmg Artifact
|
||||
# uses: actions/upload-artifact@v3
|
||||
# uses: actions/upload-artifact@v4
|
||||
# if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64')
|
||||
# with:
|
||||
# name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.dmg
|
||||
# path: build/freetube-${{ steps.versionNumber.outputs.result }}-arm64.dmg
|
||||
|
||||
- name: Upload Mac x64 .zip Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
|
||||
with:
|
||||
name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.zip
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-mac.zip
|
||||
|
||||
- name: Upload Mac x64 .7z Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-x64')
|
||||
with:
|
||||
name: freetube-${{ steps.versionNumber.outputs.result }}-mac-x64.7z
|
||||
path: build/freetube-${{ steps.versionNumber.outputs.result }}-mac.7z
|
||||
|
||||
# - name: Upload Mac arm64 .zip Artifact
|
||||
# uses: actions/upload-artifact@v3
|
||||
# uses: actions/upload-artifact@v4
|
||||
# if: startsWith(matrix.os, 'macos') && startsWith(matrix.runtime, 'osx-arm64')
|
||||
# with:
|
||||
# name: freetube-${{ steps.versionNumber.outputs.result }}-mac-arm64.zip
|
||||
|
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
compressOnly: true
|
||||
- name: Create New Pull Request If Needed
|
||||
if: steps.calibre.outputs.markdown != ''
|
||||
uses: peter-evans/create-pull-request@v5
|
||||
uses: peter-evans/create-pull-request@v6
|
||||
with:
|
||||
title: Compressed Images Nightly
|
||||
branch-suffix: timestamp
|
||||
|
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: github/issue-labeler@v3.3
|
||||
- uses: github/issue-labeler@v3.4
|
||||
with:
|
||||
configuration-path: .github/issue-labeler.yml
|
||||
enable-versioned-regex: 0
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 356 B |
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
|
@ -1,6 +1,5 @@
|
|||
process.env.NODE_ENV = 'development'
|
||||
|
||||
const open = require('open')
|
||||
const electron = require('electron')
|
||||
const webpack = require('webpack')
|
||||
const WebpackDevServer = require('webpack-dev-server')
|
||||
|
@ -161,7 +160,8 @@ function startWeb (callback) {
|
|||
if (!web) {
|
||||
startRenderer(startMain)
|
||||
} else {
|
||||
startWeb(({ port }) => {
|
||||
startWeb(async ({ port }) => {
|
||||
const open = (await import('open')).default
|
||||
open(`http://localhost:${port}`)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const path = require('path')
|
||||
const { readFileSync } = require('fs')
|
||||
const { readFileSync, readdirSync } = require('fs')
|
||||
const webpack = require('webpack')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
|
@ -11,6 +11,8 @@ const CopyWebpackPlugin = require('copy-webpack-plugin')
|
|||
|
||||
const isDevMode = process.env.NODE_ENV === 'development'
|
||||
|
||||
const { version: swiperVersion } = JSON.parse(readFileSync(path.join(__dirname, '../node_modules/swiper/package.json')))
|
||||
|
||||
const processLocalesPlugin = new ProcessLocalesPlugin({
|
||||
compress: !isDevMode,
|
||||
inputDir: path.join(__dirname, '../static/locales'),
|
||||
|
@ -117,7 +119,9 @@ const config = {
|
|||
new webpack.DefinePlugin({
|
||||
'process.env.IS_ELECTRON': true,
|
||||
'process.env.IS_ELECTRON_MAIN': false,
|
||||
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames)
|
||||
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames),
|
||||
'process.env.GEOLOCATION_NAMES': JSON.stringify(readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))),
|
||||
'process.env.SWIPER_VERSION': `'${swiperVersion}'`
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
excludeChunks: ['processTaskWorker'],
|
||||
|
@ -136,7 +140,7 @@ const config = {
|
|||
patterns: [
|
||||
{
|
||||
from: path.join(__dirname, '../node_modules/swiper/modules/{a11y,navigation,pagination}-element.css').replaceAll('\\', '/'),
|
||||
to: 'swiper.css',
|
||||
to: `swiper-${swiperVersion}.css`,
|
||||
context: path.join(__dirname, '../node_modules/swiper/modules'),
|
||||
transformAll: (assets) => {
|
||||
return Buffer.concat(assets.map(asset => asset.data))
|
||||
|
|
|
@ -11,6 +11,8 @@ const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
|
|||
|
||||
const isDevMode = process.env.NODE_ENV === 'development'
|
||||
|
||||
const { version: swiperVersion } = JSON.parse(fs.readFileSync(path.join(__dirname, '../node_modules/swiper/package.json')))
|
||||
|
||||
const config = {
|
||||
name: 'web',
|
||||
mode: process.env.NODE_ENV,
|
||||
|
@ -114,6 +116,7 @@ const config = {
|
|||
new webpack.DefinePlugin({
|
||||
'process.env.IS_ELECTRON': false,
|
||||
'process.env.IS_ELECTRON_MAIN': false,
|
||||
'process.env.SWIPER_VERSION': `'${swiperVersion}'`,
|
||||
|
||||
// video.js' vhs-utils supports both atob() in web browsers and Buffer in node
|
||||
// As the FreeTube web build only runs in web browsers, we can override their check for atob() here: https://github.com/videojs/vhs-utils/blob/main/src/decode-b64-to-uint8-array.js#L3
|
||||
|
@ -145,7 +148,7 @@ const config = {
|
|||
patterns: [
|
||||
{
|
||||
from: path.join(__dirname, '../node_modules/swiper/modules/{a11y,navigation,pagination}-element.css').replaceAll('\\', '/'),
|
||||
to: 'swiper.css',
|
||||
to: `swiper-${swiperVersion}.css`,
|
||||
context: path.join(__dirname, '../node_modules/swiper/modules'),
|
||||
transformAll: (assets) => {
|
||||
return Buffer.concat(assets.map(asset => asset.data))
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{
|
||||
"vueCompilerOptions": {
|
||||
"target": 2.7
|
||||
},
|
||||
"compilerOptions": {
|
||||
"strictNullChecks": true
|
||||
}
|
||||
}
|
||||
|
|
42
package.json
42
package.json
|
@ -62,10 +62,10 @@
|
|||
"autolinker": "^4.0.0",
|
||||
"electron-context-menu": "^3.6.1",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"marked": "^11.2.0",
|
||||
"marked": "^12.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"process": "^0.11.10",
|
||||
"swiper": "^11.0.5",
|
||||
"swiper": "^11.0.7",
|
||||
"video.js": "7.21.5",
|
||||
"videojs-contrib-quality-levels": "^3.0.0",
|
||||
"videojs-http-source-selector": "^1.1.6",
|
||||
|
@ -77,21 +77,21 @@
|
|||
"vue-observe-visibility": "^1.0.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vuex": "^3.6.2",
|
||||
"youtubei.js": "^8.2.0"
|
||||
"youtubei.js": "^9.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.23.9",
|
||||
"@babel/eslint-parser": "^7.23.9",
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/eslint-parser": "^7.23.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||
"@babel/preset-env": "^7.23.9",
|
||||
"@double-great/stylelint-a11y": "^3.0.0",
|
||||
"@babel/preset-env": "^7.24.0",
|
||||
"@double-great/stylelint-a11y": "^3.0.2",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"css-loader": "^6.9.1",
|
||||
"css-loader": "^6.10.0",
|
||||
"css-minimizer-webpack-plugin": "^6.0.0",
|
||||
"electron": "^28.2.0",
|
||||
"electron-builder": "^24.9.1",
|
||||
"eslint": "^8.56.0",
|
||||
"electron": "^29.1.0",
|
||||
"electron-builder": "^24.13.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
|
@ -99,23 +99,23 @@
|
|||
"eslint-plugin-n": "^16.6.2",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-unicorn": "^50.0.1",
|
||||
"eslint-plugin-vue": "^9.20.1",
|
||||
"eslint-plugin-unicorn": "^51.0.1",
|
||||
"eslint-plugin-vue": "^9.22.0",
|
||||
"eslint-plugin-vuejs-accessibility": "^2.2.1",
|
||||
"eslint-plugin-yml": "^1.12.2",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-minimizer-webpack-plugin": "^5.0.0",
|
||||
"lefthook": "^1.6.1",
|
||||
"mini-css-extract-plugin": "^2.7.7",
|
||||
"lefthook": "^1.6.4",
|
||||
"mini-css-extract-plugin": "^2.8.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.4.33",
|
||||
"postcss": "^8.4.35",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"prettier": "^2.8.8",
|
||||
"rimraf": "^5.0.5",
|
||||
"sass": "^1.70.0",
|
||||
"sass-loader": "^14.0.0",
|
||||
"stylelint": "^16.2.0",
|
||||
"sass": "^1.71.1",
|
||||
"sass-loader": "^14.1.1",
|
||||
"stylelint": "^16.2.1",
|
||||
"stylelint-config-sass-guidelines": "^11.0.0",
|
||||
"stylelint-config-standard": "^36.0.0",
|
||||
"stylelint-high-performance-animation": "^1.10.0",
|
||||
|
@ -124,9 +124,9 @@
|
|||
"vue-devtools": "^5.1.4",
|
||||
"vue-eslint-parser": "^9.4.2",
|
||||
"vue-loader": "^15.10.0",
|
||||
"webpack": "^5.90.0",
|
||||
"webpack": "^5.90.3",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1",
|
||||
"webpack-dev-server": "^5.0.2",
|
||||
"webpack-watch-external-files-plugin": "^3.0.0",
|
||||
"yaml-eslint-parser": "^1.2.2"
|
||||
}
|
||||
|
|
|
@ -64,7 +64,9 @@ function runApp() {
|
|||
const path = urlParts[1]
|
||||
|
||||
if (path) {
|
||||
visible = ['/playlist', '/channel', '/watch'].some(p => path.startsWith(p))
|
||||
visible = ['/channel', '/watch'].some(p => path.startsWith(p)) ||
|
||||
// Only show copy link entry for non user playlists
|
||||
(path.startsWith('/playlist') && !/playlistType=user/.test(path))
|
||||
}
|
||||
} else {
|
||||
visible = true
|
||||
|
@ -103,17 +105,17 @@ function runApp() {
|
|||
let url
|
||||
|
||||
if (toYouTube) {
|
||||
url = `https://youtu.be/${id}`
|
||||
url = new URL(`https://youtu.be/${id}`)
|
||||
} else {
|
||||
url = `https://redirect.invidious.io/watch?v=${id}`
|
||||
url = new URL(`https://redirect.invidious.io/watch?v=${id}`)
|
||||
}
|
||||
|
||||
if (query) {
|
||||
const params = new URLSearchParams(query)
|
||||
const newParams = new URLSearchParams()
|
||||
const newParams = new URLSearchParams(url.search)
|
||||
let hasParams = false
|
||||
|
||||
if (params.has('playlistId')) {
|
||||
if (params.has('playlistId') && params.get('playlistType') !== 'user') {
|
||||
newParams.set('list', params.get('playlistId'))
|
||||
hasParams = true
|
||||
}
|
||||
|
@ -124,11 +126,11 @@ function runApp() {
|
|||
}
|
||||
|
||||
if (hasParams) {
|
||||
url += '?' + newParams.toString()
|
||||
url.search = newParams.toString()
|
||||
}
|
||||
}
|
||||
|
||||
return url
|
||||
return url.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -493,6 +495,8 @@ function runApp() {
|
|||
return '#ffd1dc'
|
||||
case 'hot-pink':
|
||||
return '#de1c85'
|
||||
case 'nordic':
|
||||
return '#2b2f3a'
|
||||
case 'system':
|
||||
default:
|
||||
return nativeTheme.shouldUseDarkColors ? '#212121' : '#f1f1f1'
|
||||
|
|
|
@ -882,7 +882,23 @@ export default defineComponent({
|
|||
showToast(`${message}: ${err}`)
|
||||
return
|
||||
}
|
||||
const playlists = JSON.parse(data)
|
||||
let playlists = null
|
||||
|
||||
// for the sake of backwards compatibility,
|
||||
// check if this is the old JSON array export (used until version 0.19.1),
|
||||
// that didn't match the actual database format
|
||||
const trimmedData = data.trim()
|
||||
|
||||
if (trimmedData[0] === '[' && trimmedData[trimmedData.length - 1] === ']') {
|
||||
playlists = JSON.parse(trimmedData)
|
||||
} else {
|
||||
// otherwise assume this is the correct database format,
|
||||
// which is also what we export now (used in 0.20.0 and later versions)
|
||||
data = data.split('\n')
|
||||
data.pop()
|
||||
|
||||
playlists = data.map(playlistJson => JSON.parse(playlistJson))
|
||||
}
|
||||
|
||||
const requiredKeys = [
|
||||
'playlistName',
|
||||
|
@ -1015,7 +1031,11 @@ export default defineComponent({
|
|||
]
|
||||
}
|
||||
|
||||
await this.promptAndWriteToFile(options, JSON.stringify(this.allPlaylists), 'All playlists has been successfully exported')
|
||||
const playlistsDb = this.allPlaylists.map(playlist => {
|
||||
return JSON.stringify(playlist)
|
||||
}).join('\n') + '\n'// a trailing line is expected
|
||||
|
||||
await this.promptAndWriteToFile(options, playlistsDb, 'All playlists has been successfully exported')
|
||||
},
|
||||
|
||||
exportPlaylistsForOlderVersionsSometimes: function () {
|
||||
|
@ -1133,7 +1153,7 @@ export default defineComponent({
|
|||
})
|
||||
|
||||
if (process.env.IS_ELECTRON && this.backendFallback && this.backendPreference === 'invidious') {
|
||||
showToast(this.$t('Falling back to the local API'))
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
resolve(this.getChannelInfoLocal(channelId))
|
||||
} else {
|
||||
resolve([])
|
||||
|
|
|
@ -3,10 +3,14 @@ import { defineComponent } from 'vue'
|
|||
export default defineComponent({
|
||||
name: 'FtAgeRestricted',
|
||||
props: {
|
||||
contentTypeString: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
isChannel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isVideo: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
emoji: function () {
|
||||
|
@ -15,8 +19,11 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
restrictedMessage: function () {
|
||||
const contentType = this.$t('Age Restricted.Type.' + this.contentTypeString)
|
||||
return this.$t('Age Restricted.This {videoOrPlaylist} is age restricted', { videoOrPlaylist: contentType })
|
||||
if (this.isChannel) {
|
||||
return this.$t('Age Restricted.This channel is age restricted')
|
||||
}
|
||||
|
||||
return this.$t('Age Restricted.This video is age restricted:')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -40,7 +40,6 @@ export default defineComponent({
|
|||
voteCount: '',
|
||||
postContent: '',
|
||||
commentCount: '',
|
||||
isLoading: true,
|
||||
author: '',
|
||||
authorId: '',
|
||||
}
|
||||
|
@ -73,7 +72,7 @@ export default defineComponent({
|
|||
injectStylesUrls: [
|
||||
// This file is created with the copy webpack plugin in the web and renderer webpack configs.
|
||||
// If you add more modules, please remember to add their CSS files to the list in webpack config files.
|
||||
createWebURL('/swiper.css')
|
||||
createWebURL(`/swiper-${process.env.SWIPER_VERSION}.css`)
|
||||
],
|
||||
|
||||
a11y: true,
|
||||
|
@ -132,7 +131,6 @@ export default defineComponent({
|
|||
this.type = (this.data.postContent !== null && this.data.postContent !== undefined) ? this.data.postContent.type : 'text'
|
||||
this.author = this.data.author
|
||||
this.authorId = this.data.authorId
|
||||
this.isLoading = false
|
||||
},
|
||||
|
||||
getBestQualityImage(imageArray) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="!isLoading"
|
||||
class="ft-list-post ft-list-item outside"
|
||||
:appearance="appearance"
|
||||
:class="{ list: listType === 'list', grid: listType === 'grid' }"
|
||||
|
|
|
@ -128,6 +128,11 @@ export default defineComponent({
|
|||
|
||||
handleResize: function () {
|
||||
this.useModal = window.innerWidth <= 900
|
||||
}
|
||||
},
|
||||
|
||||
focus() {
|
||||
// To be called by parent components
|
||||
this.$refs.iconButton.focus()
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div class="ftIconButton">
|
||||
<font-awesome-icon
|
||||
ref="iconButton"
|
||||
class="iconButton"
|
||||
:title="title"
|
||||
:icon="icon"
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
Set a height to invisible/unloaded elements, so that lazy loading actually works.
|
||||
If we don't set a height, they all get a height of 0px (because they have no content),
|
||||
so they all bunch up together and end up loading all of them in one go.
|
||||
*/
|
||||
.placeholder {
|
||||
block-size: 40px;
|
||||
}
|
||||
|
||||
.videoIndex {
|
||||
color: var(--tertiary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.videoIndexIcon {
|
||||
font-size: 14px;
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
import { defineComponent } from 'vue'
|
||||
import FtListVideo from '../ft-list-video/ft-list-video.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FtListVideoNumbered',
|
||||
components: {
|
||||
'ft-list-video': FtListVideo
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
playlistId: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
playlistType: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
playlistIndex: {
|
||||
type: Number,
|
||||
default: null
|
||||
},
|
||||
playlistReverse: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
playlistShuffle: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
playlistLoop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
playlistItemId: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
appearance: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
initialVisibleState: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
alwaysShowAddToPlaylistButton: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
quickBookmarkButtonEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
canMoveVideoUp: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
canMoveVideoDown: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
canRemoveFromPlaylist: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
videoIndex: {
|
||||
type: Number,
|
||||
default: -1
|
||||
},
|
||||
isCurrentVideo: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
useChannelsHiddenPreference: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
visible: false,
|
||||
show: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
channelsHidden() {
|
||||
// Some component users like channel view will have this disabled
|
||||
if (!this.useChannelsHiddenPreference) { return [] }
|
||||
|
||||
return JSON.parse(this.$store.getters.getChannelsHidden).map((ch) => {
|
||||
// Legacy support
|
||||
if (typeof ch === 'string') {
|
||||
return { name: ch, preferredName: '', icon: '' }
|
||||
}
|
||||
return ch
|
||||
})
|
||||
},
|
||||
|
||||
// As we only use this component in Playlist and watch-video-playlist,
|
||||
// where title filtering is never desired, we don't have any title filtering logic here,
|
||||
// like we do in ft-list-video-lazy
|
||||
|
||||
shouldBeVisible() {
|
||||
return !(this.channelsHidden.some(ch => ch.name === this.data.authorId) ||
|
||||
this.channelsHidden.some(ch => ch.name === this.data.author))
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.visible = this.initialVisibleState
|
||||
},
|
||||
methods: {
|
||||
onVisibilityChanged: function (visible) {
|
||||
if (visible && this.shouldBeVisible) {
|
||||
this.visible = visible
|
||||
} else if (visible) {
|
||||
this.show = false
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<div
|
||||
v-show="show"
|
||||
v-observe-visibility="!initialVisibleState ? {
|
||||
callback: onVisibilityChanged,
|
||||
once: true,
|
||||
} : null"
|
||||
:class="{ placeholder: !visible }"
|
||||
>
|
||||
<template
|
||||
v-if="visible"
|
||||
>
|
||||
<p
|
||||
class="videoIndex"
|
||||
>
|
||||
<font-awesome-icon
|
||||
v-if="isCurrentVideo"
|
||||
class="videoIndexIcon"
|
||||
:icon="['fas', 'play']"
|
||||
/>
|
||||
<template
|
||||
v-else
|
||||
>
|
||||
{{ videoIndex + 1 }}
|
||||
</template>
|
||||
</p>
|
||||
<ft-list-video
|
||||
:data="data"
|
||||
:playlist-id="playlistId"
|
||||
:playlist-type="playlistType"
|
||||
:playlist-index="playlistIndex"
|
||||
:playlist-reverse="playlistReverse"
|
||||
:playlist-shuffle="playlistShuffle"
|
||||
:playlist-loop="playlistLoop"
|
||||
:playlist-item-id="playlistItemId"
|
||||
force-list-type="list"
|
||||
:appearance="appearance"
|
||||
:always-show-add-to-playlist-button="alwaysShowAddToPlaylistButton"
|
||||
:quick-bookmark-button-enabled="quickBookmarkButtonEnabled"
|
||||
:can-move-video-up="canMoveVideoUp"
|
||||
:can-move-video-down="canMoveVideoDown"
|
||||
:can-remove-from-playlist="canRemoveFromPlaylist"
|
||||
@pause-player="$emit('pause-player')"
|
||||
@move-video-up="$emit('move-video-up')"
|
||||
@move-video-down="$emit('move-video-down')"
|
||||
@remove-from-playlist="$emit('remove-from-playlist')"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./ft-list-video-numbered.js" />
|
||||
<style scoped src="./ft-list-video-numbered.css" />
|
|
@ -97,7 +97,6 @@ export default defineComponent({
|
|||
lengthSeconds: 0,
|
||||
duration: '',
|
||||
description: '',
|
||||
watched: false,
|
||||
watchProgress: 0,
|
||||
publishedText: '',
|
||||
isLive: false,
|
||||
|
@ -223,7 +222,7 @@ export default defineComponent({
|
|||
dropdownOptions: function () {
|
||||
const options = [
|
||||
{
|
||||
label: this.watched
|
||||
label: this.historyEntryExists
|
||||
? this.$t('Video.Remove From History')
|
||||
: this.$t('Video.Mark As Watched'),
|
||||
value: 'history'
|
||||
|
@ -343,7 +342,7 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
addWatchedStyle: function () {
|
||||
return this.watched && !this.inHistory
|
||||
return this.historyEntryExists && !this.inHistory
|
||||
},
|
||||
|
||||
externalPlayer: function () {
|
||||
|
@ -576,7 +575,7 @@ export default defineComponent({
|
|||
}
|
||||
this.openInExternalPlayer(payload)
|
||||
|
||||
if (this.saveWatchedProgress && !this.watched) {
|
||||
if (this.saveWatchedProgress && !this.historyEntryExists) {
|
||||
this.markAsWatched()
|
||||
}
|
||||
},
|
||||
|
@ -584,7 +583,7 @@ export default defineComponent({
|
|||
handleOptionsClick: function (option) {
|
||||
switch (option) {
|
||||
case 'history':
|
||||
if (this.watched) {
|
||||
if (this.historyEntryExists) {
|
||||
this.removeFromWatched()
|
||||
} else {
|
||||
this.markAsWatched()
|
||||
|
@ -727,8 +726,6 @@ export default defineComponent({
|
|||
|
||||
checkIfWatched: function () {
|
||||
if (this.historyEntryExists) {
|
||||
this.watched = true
|
||||
|
||||
const historyEntry = this.historyEntry
|
||||
|
||||
if (this.saveWatchedProgress) {
|
||||
|
@ -744,7 +741,6 @@ export default defineComponent({
|
|||
this.publishedText = ''
|
||||
}
|
||||
} else {
|
||||
this.watched = false
|
||||
this.watchProgress = 0
|
||||
}
|
||||
},
|
||||
|
@ -766,8 +762,6 @@ export default defineComponent({
|
|||
}
|
||||
this.updateHistory(videoData)
|
||||
showToast(this.$t('Video.Video has been marked as watched'))
|
||||
|
||||
this.watched = true
|
||||
},
|
||||
|
||||
removeFromWatched: function () {
|
||||
|
@ -775,7 +769,6 @@ export default defineComponent({
|
|||
|
||||
showToast(this.$t('Video.Video has been removed from your history'))
|
||||
|
||||
this.watched = false
|
||||
this.watchProgress = 0
|
||||
},
|
||||
|
||||
|
|
|
@ -3,3 +3,8 @@
|
|||
.thumbnailLink:hover {
|
||||
outline: 3px solid var(--side-nav-hover-color);
|
||||
}
|
||||
|
||||
.thumbnailImage {
|
||||
// Makes img element sized correctly before image loading starts
|
||||
aspect-ratio: 16/9 auto;
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
{{ $t("Video.Watched") }}
|
||||
</div>
|
||||
<div
|
||||
v-if="watched"
|
||||
v-if="historyEntryExists"
|
||||
class="watchedProgressBar"
|
||||
:style="{inlineSize: progressPercentage + '%'}"
|
||||
/>
|
||||
|
@ -129,12 +129,13 @@
|
|||
<span v-else-if="channelName !== null">
|
||||
{{ channelName }}
|
||||
</span>
|
||||
<template v-if="!isLive && !isUpcoming && !isPremium && !hideViews && viewCount != null">
|
||||
<span class="viewCount">
|
||||
<template v-if="channelId !== null"> • </template>
|
||||
{{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
|
||||
</span>
|
||||
</template>
|
||||
<span
|
||||
v-if="!isLive && !isUpcoming && !isPremium && !hideViews && viewCount != null"
|
||||
class="viewCount"
|
||||
>
|
||||
<template v-if="channelId !== null || channelName !== null"> • </template>
|
||||
{{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
|
||||
</span>
|
||||
<span
|
||||
v-if="uploadedTime !== '' && !isLive && !inHistory"
|
||||
class="uploadedTime"
|
||||
|
@ -151,7 +152,7 @@
|
|||
<ft-icon-button
|
||||
class="optionsButton"
|
||||
:icon="['fas', 'ellipsis-v']"
|
||||
title="More Options"
|
||||
:title="$t('Video.More Options')"
|
||||
theme="base-no-default"
|
||||
:size="16"
|
||||
:use-shadow="false"
|
||||
|
@ -160,7 +161,7 @@
|
|||
@click="handleOptionsClick"
|
||||
/>
|
||||
<p
|
||||
v-if="((listType === 'list' || forceListType === 'list') && forceListType !== 'grid') &&
|
||||
v-if="description && ((listType === 'list' || forceListType === 'list') && forceListType !== 'grid') &&
|
||||
appearance === 'result'"
|
||||
class="description"
|
||||
v-html="description"
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
>
|
||||
<ft-playlist-selector
|
||||
tabindex="0"
|
||||
:data="playlist"
|
||||
:playlist="playlist"
|
||||
:index="index"
|
||||
:selected="selectedPlaylistIdList.includes(playlist._id)"
|
||||
@selected="countSelected(playlist._id)"
|
||||
|
|
|
@ -8,7 +8,7 @@ export default defineComponent({
|
|||
'ft-icon-button': FtIconButton
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
playlist: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
@ -30,6 +30,8 @@ export default defineComponent({
|
|||
title: '',
|
||||
thumbnail: require('../../assets/img/thumbnail_placeholder.svg'),
|
||||
videoCount: 0,
|
||||
|
||||
videoPresenceCountInPlaylistTextShouldBeVisible: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -39,6 +41,9 @@ export default defineComponent({
|
|||
currentInvidiousInstance: function () {
|
||||
return this.$store.getters.getCurrentInvidiousInstance
|
||||
},
|
||||
toBeAddedToPlaylistVideoList: function () {
|
||||
return this.$store.getters.getToBeAddedToPlaylistVideoList
|
||||
},
|
||||
|
||||
titleForDisplay: function () {
|
||||
if (typeof this.title !== 'string') { return '' }
|
||||
|
@ -46,28 +51,63 @@ export default defineComponent({
|
|||
|
||||
return `${this.title.substring(0, 255)}...`
|
||||
},
|
||||
|
||||
loneToBeAddedToPlaylistVideo: function () {
|
||||
if (this.toBeAddedToPlaylistVideoList.length !== 1) { return null }
|
||||
|
||||
return this.toBeAddedToPlaylistVideoList[0]
|
||||
},
|
||||
loneVideoPresenceCountInPlaylist() {
|
||||
if (this.loneToBeAddedToPlaylistVideo == null) { return null }
|
||||
|
||||
const v = this.playlist.videos.reduce((accumulator, video) => {
|
||||
return video.videoId === this.loneToBeAddedToPlaylistVideo.videoId
|
||||
? accumulator + 1
|
||||
: accumulator
|
||||
}, 0)
|
||||
// Don't display zero value
|
||||
return v === 0 ? null : v
|
||||
},
|
||||
loneVideoPresenceCountInPlaylistText() {
|
||||
if (this.loneVideoPresenceCountInPlaylist == null) { return null }
|
||||
|
||||
return this.$tc('User Playlists.AddVideoPrompt.Added {count} Times', this.loneVideoPresenceCountInPlaylist, {
|
||||
count: this.loneVideoPresenceCountInPlaylist,
|
||||
})
|
||||
},
|
||||
videoPresenceCountInPlaylistTextVisible() {
|
||||
if (!this.videoPresenceCountInPlaylistTextShouldBeVisible) { return false }
|
||||
|
||||
return this.loneVideoPresenceCountInPlaylistText != null
|
||||
},
|
||||
},
|
||||
created: function () {
|
||||
this.parseUserData()
|
||||
},
|
||||
methods: {
|
||||
parseUserData: function () {
|
||||
this.title = this.data.playlistName
|
||||
if (this.data.videos.length > 0) {
|
||||
const thumbnailURL = `https://i.ytimg.com/vi/${this.data.videos[0].videoId}/mqdefault.jpg`
|
||||
this.title = this.playlist.playlistName
|
||||
if (this.playlist.videos.length > 0) {
|
||||
const thumbnailURL = `https://i.ytimg.com/vi/${this.playlist.videos[0].videoId}/mqdefault.jpg`
|
||||
if (this.backendPreference === 'invidious') {
|
||||
this.thumbnail = thumbnailURL.replace('https://i.ytimg.com', this.currentInvidiousInstance)
|
||||
} else {
|
||||
this.thumbnail = thumbnailURL
|
||||
}
|
||||
}
|
||||
this.videoCount = this.data.videos.length
|
||||
this.videoCount = this.playlist.videos.length
|
||||
},
|
||||
|
||||
toggleSelection: function () {
|
||||
this.$emit('selected', this.index)
|
||||
},
|
||||
|
||||
onVisibilityChanged(visible) {
|
||||
if (!visible) { return }
|
||||
|
||||
this.videoPresenceCountInPlaylistTextShouldBeVisible = true
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'openInExternalPlayer'
|
||||
])
|
||||
|
|
|
@ -54,6 +54,10 @@
|
|||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.videoPresenceCount {
|
||||
margin-block-start: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&.grid {
|
||||
|
|
|
@ -29,12 +29,24 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="info">
|
||||
<span
|
||||
<div
|
||||
v-observe-visibility="{
|
||||
callback: onVisibilityChanged,
|
||||
once: true,
|
||||
}"
|
||||
class="info"
|
||||
>
|
||||
<div
|
||||
class="title"
|
||||
>
|
||||
{{ titleForDisplay }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="videoPresenceCountInPlaylistTextVisible"
|
||||
class="videoPresenceCount"
|
||||
>
|
||||
{{ loneVideoPresenceCountInPlaylistText }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -9,18 +9,29 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
catchTimestampClick: function(event) {
|
||||
const match = event.detail.match(/(\d+):(\d+):?(\d+)?/)
|
||||
if (match[3] !== undefined) { // HH:MM:SS
|
||||
const seconds = 3600 * Number(match[1]) + 60 * Number(match[2]) + Number(match[3])
|
||||
this.$emit('timestamp-event', seconds)
|
||||
} else { // MM:SS
|
||||
const seconds = 60 * Number(match[1]) + Number(match[2])
|
||||
this.$emit('timestamp-event', seconds)
|
||||
}
|
||||
catchTimestampClick: function (event) {
|
||||
this.$emit('timestamp-event', event.detail)
|
||||
},
|
||||
detectTimestamps: function (input) {
|
||||
return input.replaceAll(/(\d+(:\d+)+)/g, '<a href="#" onclick="this.dispatchEvent(new CustomEvent(\'timestamp-clicked\',{bubbles:true, detail:\'$1\'}))">$1</a>')
|
||||
const videoId = this.$route.params.id
|
||||
|
||||
return input.replaceAll(/(?:(\d+):)?(\d+):(\d+)/g, (timestamp, hours, minutes, seconds) => {
|
||||
let time = 60 * Number(minutes) + Number(seconds)
|
||||
|
||||
if (hours) {
|
||||
time += 3600 * Number(hours)
|
||||
}
|
||||
|
||||
const url = this.$router.resolve({
|
||||
path: `/watch/${videoId}`,
|
||||
query: {
|
||||
timestamp: time
|
||||
}
|
||||
}).href
|
||||
|
||||
// Adding the URL lets the user open the video in a new window at this timestamp
|
||||
return `<a href="${url}" onclick="event.preventDefault();this.dispatchEvent(new CustomEvent('timestamp-clicked',{bubbles:true,detail:${time}}));window.scrollTo(0,0)">${timestamp}</a>`
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { defineComponent } from 'vue'
|
||||
import FtToastEvents from './ft-toast-events.js'
|
||||
|
||||
let id = 0
|
||||
|
||||
export default defineComponent({
|
||||
name: 'FtToast',
|
||||
data: function () {
|
||||
|
@ -15,7 +17,9 @@ export default defineComponent({
|
|||
FtToastEvents.removeEventListener('toast-open', this.open)
|
||||
},
|
||||
methods: {
|
||||
performAction: function (index) {
|
||||
performAction: function (id) {
|
||||
const index = this.toasts.findIndex(toast => id === toast.id)
|
||||
|
||||
this.toasts[index].action()
|
||||
this.remove(index)
|
||||
},
|
||||
|
@ -26,7 +30,13 @@ export default defineComponent({
|
|||
toast.isOpen = false
|
||||
},
|
||||
open: function ({ detail: { message, time, action } }) {
|
||||
const toast = { message: message, action: action || (() => { }), isOpen: false, timeout: null }
|
||||
const toast = {
|
||||
message: message,
|
||||
action: action || (() => { }),
|
||||
isOpen: false,
|
||||
timeout: null,
|
||||
id: id++
|
||||
}
|
||||
toast.timeout = setTimeout(this.close, time || 3000, toast)
|
||||
setTimeout(() => { toast.isOpen = true })
|
||||
if (this.toasts.length > 4) {
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<div class="toast-holder">
|
||||
<div
|
||||
v-for="(toast, index) in toasts"
|
||||
:key="'toast-' + index"
|
||||
v-for="toast in toasts"
|
||||
:key="toast.id"
|
||||
class="toast"
|
||||
:class="{ closed: !toast.isOpen, open: toast.isOpen }"
|
||||
tabindex="0"
|
||||
role="status"
|
||||
@click="performAction(index)"
|
||||
@keydown.enter.prevent="performAction(index)"
|
||||
@keydown.space.prevent="performAction(index)"
|
||||
@click="performAction(toast.id)"
|
||||
@keydown.enter.prevent="performAction(toast.id)"
|
||||
@keydown.space.prevent="performAction(toast.id)"
|
||||
>
|
||||
<p class="message">
|
||||
{{ toast.message }}
|
||||
|
|
|
@ -5,9 +5,12 @@ import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
|
|||
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
|
||||
import FtInput from '../ft-input/ft-input.vue'
|
||||
import FtPrompt from '../ft-prompt/ft-prompt.vue'
|
||||
import FtButton from '../ft-button/ft-button.vue'
|
||||
import {
|
||||
formatNumber,
|
||||
showToast,
|
||||
} from '../../helpers/utils'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PlaylistInfo',
|
||||
|
@ -17,6 +20,7 @@ export default defineComponent({
|
|||
'ft-icon-button': FtIconButton,
|
||||
'ft-input': FtInput,
|
||||
'ft-prompt': FtPrompt,
|
||||
'ft-button': FtButton,
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
|
@ -82,6 +86,9 @@ export default defineComponent({
|
|||
},
|
||||
data: function () {
|
||||
return {
|
||||
searchVideoMode: false,
|
||||
query: '',
|
||||
updateQueryDebounce: function() {},
|
||||
editMode: false,
|
||||
showDeletePlaylistPrompt: false,
|
||||
showRemoveVideosOnWatchPrompt: false,
|
||||
|
@ -145,6 +152,14 @@ export default defineComponent({
|
|||
return this.firstVideoId !== ''
|
||||
},
|
||||
|
||||
parsedViewCount() {
|
||||
return formatNumber(this.viewCount)
|
||||
},
|
||||
|
||||
parsedVideoCount() {
|
||||
return formatNumber(this.videoCount)
|
||||
},
|
||||
|
||||
thumbnail: function () {
|
||||
if (this.thumbnailPreference === 'hidden' || !this.firstVideoIdExists) {
|
||||
return require('../../assets/img/thumbnail_placeholder.svg')
|
||||
|
@ -223,10 +238,12 @@ export default defineComponent({
|
|||
created: function () {
|
||||
this.newTitle = this.title
|
||||
this.newDescription = this.description
|
||||
|
||||
this.updateQueryDebounce = debounce(this.updateQuery, 500)
|
||||
},
|
||||
methods: {
|
||||
toggleCopyVideosPrompt: function (force = false) {
|
||||
if (this.moreVideoDataAvailable && !force) {
|
||||
if (this.moreVideoDataAvailable && !this.isUserPlaylist && !force) {
|
||||
showToast(this.$t('User Playlists.SinglePlaylistView.Toast["Some videos in the playlist are not loaded yet. Click here to copy anyway."]'), 5000, () => {
|
||||
this.toggleCopyVideosPrompt(true)
|
||||
})
|
||||
|
@ -364,6 +381,30 @@ export default defineComponent({
|
|||
showToast(this.$t('User Playlists.SinglePlaylistView.Toast.Quick bookmark disabled'))
|
||||
},
|
||||
|
||||
updateQuery(query) {
|
||||
this.query = query
|
||||
this.$emit('search-video-query-change', query)
|
||||
},
|
||||
enableVideoSearchMode() {
|
||||
this.searchVideoMode = true
|
||||
this.$emit('search-video-mode-on')
|
||||
|
||||
nextTick(() => {
|
||||
// Some elements only present after rendering update
|
||||
this.$refs.searchInput.focus()
|
||||
})
|
||||
},
|
||||
disableVideoSearchMode() {
|
||||
this.searchVideoMode = false
|
||||
this.updateQuery('')
|
||||
this.$emit('search-video-mode-off')
|
||||
|
||||
nextTick(() => {
|
||||
// Some elements only present after rendering update
|
||||
this.$refs.enableSearchModeButton?.focus()
|
||||
})
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'showAddToPlaylistPromptForManyVideos',
|
||||
'updatePlaylist',
|
||||
|
|
|
@ -72,3 +72,20 @@
|
|||
column-gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.searchInputsRow {
|
||||
margin-block-start: 8px;
|
||||
|
||||
display: grid;
|
||||
|
||||
/* 2 columns */
|
||||
grid-template-columns: 1fr auto;
|
||||
column-gap: 8px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1250px) {
|
||||
:deep(.sharePlaylistIcon .iconDropdown) {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: auto;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,9 +47,9 @@
|
|||
{{ title }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ videoCount }} {{ $t("Playlist.Videos") }}
|
||||
{{ $tc('Global.Counts.Video Count', videoCount, {count: parsedVideoCount}) }}
|
||||
<span v-if="!hideViews && !isUserPlaylist">
|
||||
- {{ viewCount }} {{ $t("Playlist.Views") }}
|
||||
- {{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
|
||||
</span>
|
||||
<span>- </span>
|
||||
<span v-if="infoSource !== 'local'">
|
||||
|
@ -76,6 +76,7 @@
|
|||
<hr>
|
||||
|
||||
<div
|
||||
v-if="!searchVideoMode"
|
||||
class="channelShareWrapper"
|
||||
>
|
||||
<router-link
|
||||
|
@ -106,6 +107,14 @@
|
|||
</div>
|
||||
|
||||
<div class="playlistOptions">
|
||||
<ft-icon-button
|
||||
v-if="isUserPlaylist && videoCount > 0 && !editMode"
|
||||
ref="enableSearchModeButton"
|
||||
:title="$t('User Playlists.SinglePlaylistView.Search for Videos')"
|
||||
:icon="['fas', 'search']"
|
||||
theme="secondary"
|
||||
@click="enableVideoSearchMode"
|
||||
/>
|
||||
<ft-icon-button
|
||||
v-if="editMode"
|
||||
:title="$t('User Playlists.Save Changes')"
|
||||
|
@ -166,6 +175,7 @@
|
|||
<ft-share-button
|
||||
v-if="sharePlaylistButtonVisible"
|
||||
:id="id"
|
||||
class="sharePlaylistIcon"
|
||||
:dropdown-position-y="description ? 'top' : 'bottom'"
|
||||
share-target-type="Playlist"
|
||||
/>
|
||||
|
@ -186,6 +196,28 @@
|
|||
@click="handleRemoveVideosOnWatchPromptAnswer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isUserPlaylist && searchVideoMode"
|
||||
class="searchInputsRow"
|
||||
>
|
||||
<ft-input
|
||||
ref="searchInput"
|
||||
class="searchInput"
|
||||
:placeholder="$t('User Playlists.SinglePlaylistView.Search for Videos')"
|
||||
:show-clear-text-button="true"
|
||||
:show-action-button="false"
|
||||
@input="(input) => updateQueryDebounce(input)"
|
||||
@clear="updateQueryDebounce('')"
|
||||
/>
|
||||
<ft-icon-button
|
||||
v-if="isUserPlaylist && searchVideoMode"
|
||||
:title="$t('User Playlists.Cancel')"
|
||||
:icon="['fas', 'times']"
|
||||
theme="secondary"
|
||||
@click="disableVideoSearchMode"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
/>
|
||||
</div>
|
||||
<p
|
||||
v-if="!hideLabelsSideBar"
|
||||
id="channelLabel"
|
||||
class="navLabel"
|
||||
>
|
||||
|
|
|
@ -121,6 +121,8 @@ export default defineComponent({
|
|||
this.attemptedFetch = true
|
||||
|
||||
this.errorChannels = []
|
||||
const subscriptionUpdates = []
|
||||
|
||||
const postListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
|
||||
let posts = []
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
|
@ -137,6 +139,32 @@ export default defineComponent({
|
|||
channelId: channel.id,
|
||||
posts: posts,
|
||||
})
|
||||
|
||||
if (posts.length > 0) {
|
||||
const post = posts.find(post => post.authorId === channel.id)
|
||||
|
||||
if (post) {
|
||||
const name = post.author
|
||||
let thumbnailUrl = post.authorThumbnails?.[0]?.url
|
||||
|
||||
if (name || thumbnailUrl) {
|
||||
if (thumbnailUrl) {
|
||||
if (thumbnailUrl.startsWith('//')) {
|
||||
thumbnailUrl = 'https:' + thumbnailUrl
|
||||
} else if (thumbnailUrl.startsWith(`${this.currentInvidiousInstance}/ggpht`)) {
|
||||
thumbnailUrl = thumbnailUrl.replace(`${this.currentInvidiousInstance}/ggpht`, 'https://yt3.googleusercontent.com')
|
||||
}
|
||||
}
|
||||
|
||||
subscriptionUpdates.push({
|
||||
channelId: channel.id,
|
||||
channelName: name,
|
||||
channelThumbnailUrl: thumbnailUrl
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return posts
|
||||
}))).flatMap((o) => o)
|
||||
postList.push(...postListFromRemote)
|
||||
|
@ -147,6 +175,8 @@ export default defineComponent({
|
|||
this.postList = postList
|
||||
this.isLoading = false
|
||||
this.updateShowProgressBar(false)
|
||||
|
||||
this.batchUpdateSubscriptionDetails(subscriptionUpdates)
|
||||
},
|
||||
|
||||
maybeLoadPostsForSubscriptionsFromRemote: async function () {
|
||||
|
@ -200,7 +230,7 @@ export default defineComponent({
|
|||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendPreference === 'invidious' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to the local API'))
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
resolve(this.getChannelPostsLocal(channel))
|
||||
} else {
|
||||
resolve([])
|
||||
|
@ -211,6 +241,7 @@ export default defineComponent({
|
|||
|
||||
...mapActions([
|
||||
'updateShowProgressBar',
|
||||
'batchUpdateSubscriptionDetails',
|
||||
'updateSubscriptionPostsCacheByChannel',
|
||||
]),
|
||||
|
||||
|
|
|
@ -129,19 +129,23 @@ export default defineComponent({
|
|||
this.attemptedFetch = true
|
||||
|
||||
this.errorChannels = []
|
||||
const subscriptionUpdates = []
|
||||
|
||||
const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
|
||||
let videos = []
|
||||
let name, thumbnailUrl
|
||||
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (useRss) {
|
||||
videos = await this.getChannelLiveInvidiousRSS(channel)
|
||||
({ videos, name, thumbnailUrl } = await this.getChannelLiveInvidiousRSS(channel))
|
||||
} else {
|
||||
videos = await this.getChannelLiveInvidious(channel)
|
||||
({ videos, name, thumbnailUrl } = await this.getChannelLiveInvidious(channel))
|
||||
}
|
||||
} else {
|
||||
if (useRss) {
|
||||
videos = await this.getChannelLiveLocalRSS(channel)
|
||||
({ videos, name, thumbnailUrl } = await this.getChannelLiveLocalRSS(channel))
|
||||
} else {
|
||||
videos = await this.getChannelLiveLocal(channel)
|
||||
({ videos, name, thumbnailUrl } = await this.getChannelLiveLocal(channel))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,6 +156,15 @@ export default defineComponent({
|
|||
channelId: channel.id,
|
||||
videos: videos,
|
||||
})
|
||||
|
||||
if (name || thumbnailUrl) {
|
||||
subscriptionUpdates.push({
|
||||
channelId: channel.id,
|
||||
channelName: name,
|
||||
channelThumbnailUrl: thumbnailUrl
|
||||
})
|
||||
}
|
||||
|
||||
return videos
|
||||
}))).flatMap((o) => o)
|
||||
videoList.push(...videoListFromRemote)
|
||||
|
@ -159,6 +172,8 @@ export default defineComponent({
|
|||
this.videoList = updateVideoListAfterProcessing(videoList)
|
||||
this.isLoading = false
|
||||
this.updateShowProgressBar(false)
|
||||
|
||||
this.batchUpdateSubscriptionDetails(subscriptionUpdates)
|
||||
},
|
||||
|
||||
maybeLoadVideosForSubscriptionsFromRemote: async function () {
|
||||
|
@ -174,16 +189,18 @@ export default defineComponent({
|
|||
|
||||
getChannelLiveLocal: async function (channel, failedAttempts = 0) {
|
||||
try {
|
||||
const entries = await getLocalChannelLiveStreams(channel.id)
|
||||
const result = await getLocalChannelLiveStreams(channel.id)
|
||||
|
||||
if (entries === null) {
|
||||
if (result === null) {
|
||||
this.errorChannels.push(channel)
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
|
||||
addPublishedDatesLocal(entries)
|
||||
addPublishedDatesLocal(result.videos)
|
||||
|
||||
return entries
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
|
@ -198,12 +215,16 @@ export default defineComponent({
|
|||
showToast(this.$t('Falling back to Invidious API'))
|
||||
return await this.getChannelLiveInvidious(channel, failedAttempts + 1)
|
||||
} else {
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
return await this.getChannelLiveLocalRSS(channel, failedAttempts + 1)
|
||||
default:
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -227,7 +248,9 @@ export default defineComponent({
|
|||
this.errorChannels.push(channel)
|
||||
}
|
||||
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
|
||||
return await parseYouTubeRSSFeed(await response.text(), channel.id)
|
||||
|
@ -245,12 +268,16 @@ export default defineComponent({
|
|||
showToast(this.$t('Falling back to Invidious API'))
|
||||
return this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1)
|
||||
} else {
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
return this.getChannelLiveLocal(channel, failedAttempts + 1)
|
||||
default:
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -269,7 +296,16 @@ export default defineComponent({
|
|||
|
||||
addPublishedDatesInvidious(videos)
|
||||
|
||||
resolve(videos)
|
||||
let name
|
||||
|
||||
if (videos.length > 0) {
|
||||
name = videos.find(video => video.author).author
|
||||
}
|
||||
|
||||
resolve({
|
||||
name,
|
||||
videos
|
||||
})
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
||||
|
@ -282,17 +318,21 @@ export default defineComponent({
|
|||
break
|
||||
case 1:
|
||||
if (process.env.IS_ELECTRON && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to the local API'))
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
resolve(this.getChannelLiveLocal(channel, failedAttempts + 1))
|
||||
} else {
|
||||
resolve([])
|
||||
resolve({
|
||||
videos: []
|
||||
})
|
||||
}
|
||||
break
|
||||
case 2:
|
||||
resolve(this.getChannelLiveInvidiousRSS(channel, failedAttempts + 1))
|
||||
break
|
||||
default:
|
||||
resolve([])
|
||||
resolve({
|
||||
videos: []
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -317,7 +357,9 @@ export default defineComponent({
|
|||
this.errorChannels.push(channel)
|
||||
}
|
||||
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
|
||||
return await parseYouTubeRSSFeed(await response.text(), channel.id)
|
||||
|
@ -332,20 +374,25 @@ export default defineComponent({
|
|||
return this.getChannelLiveInvidious(channel, failedAttempts + 1)
|
||||
case 1:
|
||||
if (process.env.IS_ELECTRON && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to the local API'))
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
return this.getChannelLiveLocalRSS(channel, failedAttempts + 1)
|
||||
} else {
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
return this.getChannelLiveInvidious(channel, failedAttempts + 1)
|
||||
default:
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'batchUpdateSubscriptionDetails',
|
||||
'updateShowProgressBar',
|
||||
'updateSubscriptionLiveCacheByChannel',
|
||||
]),
|
||||
|
|
|
@ -114,12 +114,16 @@ export default defineComponent({
|
|||
this.attemptedFetch = true
|
||||
|
||||
this.errorChannels = []
|
||||
const subscriptionUpdates = []
|
||||
|
||||
const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
|
||||
let videos = []
|
||||
let name
|
||||
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
videos = await this.getChannelShortsInvidious(channel)
|
||||
({ videos, name } = await this.getChannelShortsInvidious(channel))
|
||||
} else {
|
||||
videos = await this.getChannelShortsLocal(channel)
|
||||
({ videos, name } = await this.getChannelShortsLocal(channel))
|
||||
}
|
||||
|
||||
channelCount++
|
||||
|
@ -129,6 +133,14 @@ export default defineComponent({
|
|||
channelId: channel.id,
|
||||
videos: videos,
|
||||
})
|
||||
|
||||
if (name) {
|
||||
subscriptionUpdates.push({
|
||||
channelId: channel.id,
|
||||
channelName: name
|
||||
})
|
||||
}
|
||||
|
||||
return videos
|
||||
}))).flatMap((o) => o)
|
||||
videoList.push(...videoListFromRemote)
|
||||
|
@ -136,6 +148,8 @@ export default defineComponent({
|
|||
this.videoList = updateVideoListAfterProcessing(videoList)
|
||||
this.isLoading = false
|
||||
this.updateShowProgressBar(false)
|
||||
|
||||
this.batchUpdateSubscriptionDetails(subscriptionUpdates)
|
||||
},
|
||||
|
||||
maybeLoadVideosForSubscriptionsFromRemote: async function () {
|
||||
|
@ -168,7 +182,9 @@ export default defineComponent({
|
|||
this.errorChannels.push(channel)
|
||||
}
|
||||
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
|
||||
return await parseYouTubeRSSFeed(await response.text(), channel.id)
|
||||
|
@ -184,10 +200,14 @@ export default defineComponent({
|
|||
showToast(this.$t('Falling back to Invidious API'))
|
||||
return this.getChannelShortsInvidious(channel, failedAttempts + 1)
|
||||
} else {
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
default:
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -211,7 +231,9 @@ export default defineComponent({
|
|||
this.errorChannels.push(channel)
|
||||
}
|
||||
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
|
||||
return await parseYouTubeRSSFeed(await response.text(), channel.id)
|
||||
|
@ -224,18 +246,23 @@ export default defineComponent({
|
|||
switch (failedAttempts) {
|
||||
case 0:
|
||||
if (process.env.IS_ELECTRON && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to the local API'))
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
return this.getChannelShortsLocal(channel, failedAttempts + 1)
|
||||
} else {
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
default:
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'batchUpdateSubscriptionDetails',
|
||||
'updateShowProgressBar',
|
||||
'updateSubscriptionShortsCacheByChannel',
|
||||
]),
|
||||
|
|
|
@ -108,7 +108,7 @@ export default defineComponent({
|
|||
case 'r':
|
||||
case 'R':
|
||||
case 'F5':
|
||||
if (!this.isLoading) {
|
||||
if (!this.isLoading && this.activeSubscriptionList.length > 0) {
|
||||
this.$emit('refresh')
|
||||
}
|
||||
break
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
/>
|
||||
</ft-flex-box>
|
||||
<ft-icon-button
|
||||
v-if="!isLoading"
|
||||
v-if="!isLoading && activeSubscriptionList.length > 0"
|
||||
:icon="['fas', 'sync']"
|
||||
class="floatingTopButton"
|
||||
:title="$t('Subscriptions.Refresh Subscriptions')"
|
||||
|
|
|
@ -129,19 +129,23 @@ export default defineComponent({
|
|||
this.attemptedFetch = true
|
||||
|
||||
this.errorChannels = []
|
||||
const subscriptionUpdates = []
|
||||
|
||||
const videoListFromRemote = (await Promise.all(channelsToLoadFromRemote.map(async (channel) => {
|
||||
let videos = []
|
||||
let name, thumbnailUrl
|
||||
|
||||
if (!process.env.IS_ELECTRON || this.backendPreference === 'invidious') {
|
||||
if (useRss) {
|
||||
videos = await this.getChannelVideosInvidiousRSS(channel)
|
||||
({ videos, name, thumbnailUrl } = await this.getChannelVideosInvidiousRSS(channel))
|
||||
} else {
|
||||
videos = await this.getChannelVideosInvidiousScraper(channel)
|
||||
({ videos, name, thumbnailUrl } = await this.getChannelVideosInvidiousScraper(channel))
|
||||
}
|
||||
} else {
|
||||
if (useRss) {
|
||||
videos = await this.getChannelVideosLocalRSS(channel)
|
||||
({ videos, name, thumbnailUrl } = await this.getChannelVideosLocalRSS(channel))
|
||||
} else {
|
||||
videos = await this.getChannelVideosLocalScraper(channel)
|
||||
({ videos, name, thumbnailUrl } = await this.getChannelVideosLocalScraper(channel))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,6 +156,15 @@ export default defineComponent({
|
|||
channelId: channel.id,
|
||||
videos: videos,
|
||||
})
|
||||
|
||||
if (name || thumbnailUrl) {
|
||||
subscriptionUpdates.push({
|
||||
channelId: channel.id,
|
||||
channelName: name,
|
||||
channelThumbnailUrl: thumbnailUrl
|
||||
})
|
||||
}
|
||||
|
||||
return videos
|
||||
}))).flatMap((o) => o)
|
||||
videoList.push(...videoListFromRemote)
|
||||
|
@ -159,6 +172,8 @@ export default defineComponent({
|
|||
this.videoList = updateVideoListAfterProcessing(videoList)
|
||||
this.isLoading = false
|
||||
this.updateShowProgressBar(false)
|
||||
|
||||
this.batchUpdateSubscriptionDetails(subscriptionUpdates)
|
||||
},
|
||||
|
||||
maybeLoadVideosForSubscriptionsFromRemote: async function () {
|
||||
|
@ -174,16 +189,18 @@ export default defineComponent({
|
|||
|
||||
getChannelVideosLocalScraper: async function (channel, failedAttempts = 0) {
|
||||
try {
|
||||
const videos = await getLocalChannelVideos(channel.id)
|
||||
const result = await getLocalChannelVideos(channel.id)
|
||||
|
||||
if (videos === null) {
|
||||
if (result === null) {
|
||||
this.errorChannels.push(channel)
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
|
||||
addPublishedDatesLocal(videos)
|
||||
addPublishedDatesLocal(result.videos)
|
||||
|
||||
return videos
|
||||
return result
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
|
@ -198,12 +215,16 @@ export default defineComponent({
|
|||
showToast(this.$t('Falling back to Invidious API'))
|
||||
return await this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
|
||||
} else {
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
return await this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
|
||||
default:
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -227,7 +248,9 @@ export default defineComponent({
|
|||
this.errorChannels.push(channel)
|
||||
}
|
||||
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
|
||||
return await parseYouTubeRSSFeed(await response.text(), channel.id)
|
||||
|
@ -245,12 +268,16 @@ export default defineComponent({
|
|||
showToast(this.$t('Falling back to Invidious API'))
|
||||
return this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1)
|
||||
} else {
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
return this.getChannelVideosLocalScraper(channel, failedAttempts + 1)
|
||||
default:
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -266,7 +293,16 @@ export default defineComponent({
|
|||
invidiousAPICall(subscriptionsPayload).then((result) => {
|
||||
addPublishedDatesInvidious(result.videos)
|
||||
|
||||
resolve(result.videos)
|
||||
let name
|
||||
|
||||
if (result.videos.length > 0) {
|
||||
name = result.videos.find(video => video.type === 'video' && video.author).author
|
||||
}
|
||||
|
||||
resolve({
|
||||
name,
|
||||
videos: result.videos
|
||||
})
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
||||
|
@ -279,17 +315,21 @@ export default defineComponent({
|
|||
break
|
||||
case 1:
|
||||
if (process.env.IS_ELECTRON && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to the local API'))
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
resolve(this.getChannelVideosLocalScraper(channel, failedAttempts + 1))
|
||||
} else {
|
||||
resolve([])
|
||||
resolve({
|
||||
videos: []
|
||||
})
|
||||
}
|
||||
break
|
||||
case 2:
|
||||
resolve(this.getChannelVideosInvidiousRSS(channel, failedAttempts + 1))
|
||||
break
|
||||
default:
|
||||
resolve([])
|
||||
resolve({
|
||||
videos: []
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -314,7 +354,9 @@ export default defineComponent({
|
|||
this.errorChannels.push(channel)
|
||||
}
|
||||
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
|
||||
return await parseYouTubeRSSFeed(await response.text(), channel.id)
|
||||
|
@ -329,20 +371,25 @@ export default defineComponent({
|
|||
return this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
|
||||
case 1:
|
||||
if (process.env.IS_ELECTRON && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to the local API'))
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
return this.getChannelVideosLocalRSS(channel, failedAttempts + 1)
|
||||
} else {
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
case 2:
|
||||
return this.getChannelVideosInvidiousScraper(channel, failedAttempts + 1)
|
||||
default:
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
...mapActions([
|
||||
'batchUpdateSubscriptionDetails',
|
||||
'updateShowProgressBar',
|
||||
'updateSubscriptionVideosCacheByChannel',
|
||||
]),
|
||||
|
|
|
@ -37,7 +37,8 @@ export default defineComponent({
|
|||
'dracula',
|
||||
'catppuccinMocha',
|
||||
'pastelPink',
|
||||
'hotPink'
|
||||
'hotPink',
|
||||
'nordic'
|
||||
]
|
||||
}
|
||||
},
|
||||
|
@ -102,7 +103,8 @@ export default defineComponent({
|
|||
this.$t('Settings.Theme Settings.Base Theme.Dracula'),
|
||||
this.$t('Settings.Theme Settings.Base Theme.Catppuccin Mocha'),
|
||||
this.$t('Settings.Theme Settings.Base Theme.Pastel Pink'),
|
||||
this.$t('Settings.Theme Settings.Base Theme.Hot Pink')
|
||||
this.$t('Settings.Theme Settings.Base Theme.Hot Pink'),
|
||||
this.$t('Settings.Theme Settings.Base Theme.Nordic')
|
||||
]
|
||||
},
|
||||
|
||||
|
|
|
@ -54,7 +54,6 @@
|
|||
.chapterThumbnail {
|
||||
grid-area: thumbnail;
|
||||
inline-size: 130px;
|
||||
block-size: auto;
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@ export default defineComponent({
|
|||
changeChapter: function(index) {
|
||||
this.currentIndex = index
|
||||
this.$emit('timestamp-event', this.chapters[index].startSeconds)
|
||||
window.scrollTo(0, 0)
|
||||
},
|
||||
|
||||
navigateChapters(direction) {
|
||||
|
|
|
@ -46,13 +46,15 @@
|
|||
@keydown.space.stop.prevent="changeChapter(index)"
|
||||
@keydown.enter.stop.prevent="changeChapter(index)"
|
||||
>
|
||||
<!-- Setting the aspect ratio avoids layout shifts when the images load -->
|
||||
<img
|
||||
v-if="!compact"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
class="chapterThumbnail"
|
||||
loading="lazy"
|
||||
:src="chapter.thumbnail"
|
||||
:src="chapter.thumbnail.url"
|
||||
:style="{ aspectRatio: chapter.thumbnail.width / chapter.thumbnail.height }"
|
||||
>
|
||||
<div class="chapterTimestamp">
|
||||
{{ chapter.timestamp }}
|
||||
|
|
|
@ -293,7 +293,7 @@ export default defineComponent({
|
|||
copyToClipboard(err)
|
||||
})
|
||||
if (process.env.IS_ELECTRON && this.backendFallback && this.backendPreference === 'invidious') {
|
||||
showToast(this.$t('Falling back to local API'))
|
||||
showToast(this.$t('Falling back to Local API'))
|
||||
this.getCommentDataLocal()
|
||||
} else {
|
||||
this.isLoading = false
|
||||
|
|
|
@ -268,7 +268,7 @@
|
|||
v-else-if="showComments && !isLoading"
|
||||
>
|
||||
<h3 class="noCommentMsg">
|
||||
{{ $t("There are no comments available for this video") }}
|
||||
{{ $t("Comments.There are no comments available for this video") }}
|
||||
</h3>
|
||||
</div>
|
||||
<h4
|
||||
|
|
|
@ -1,27 +1,30 @@
|
|||
.watchVideoInfo {
|
||||
column-gap: 15px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(min-content, 1fr);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px;
|
||||
|
||||
@media screen and (max-width: 680px) {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.videoTitle {
|
||||
display: block;
|
||||
font-size: 22px;
|
||||
font-weight: normal;
|
||||
margin-block: 0 24px;
|
||||
margin-block: 0;
|
||||
margin-inline: 0;
|
||||
margin-block-end: 1em;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.channelInformation {
|
||||
.videoMetrics, .videoButtons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.videoButtons {
|
||||
.profileRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.channelThumbnail {
|
||||
|
@ -36,6 +39,7 @@
|
|||
cursor: pointer;
|
||||
display: block;
|
||||
margin-inline-start: 6px;
|
||||
margin-block-end: 3px;
|
||||
position: relative;
|
||||
text-decoration: inherit;
|
||||
inset-block-start: -2px;
|
||||
|
@ -44,77 +48,80 @@
|
|||
.subscribeButton {
|
||||
font-size: 14px;
|
||||
margin-inline-start: 6px;
|
||||
margin-block-start: 6px;
|
||||
margin-block: 0;
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.viewCount,
|
||||
.datePublished {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 15px;
|
||||
text-align: end;
|
||||
.videoOptions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
|
||||
@media screen and (max-width: 680px) {
|
||||
text-align: start;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 680px) {
|
||||
:deep(.iconDropdown) {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: calc(50% - 20px);
|
||||
}
|
||||
}
|
||||
|
||||
.viewCount {
|
||||
margin-block: 18px 0;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
.datePublished {
|
||||
margin-block: 4px 0;
|
||||
margin-inline: 0;
|
||||
|
||||
@media screen and (max-width: 680px) {
|
||||
margin-block-start: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.likeSection {
|
||||
color: var(--tertiary-text-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
margin-inline-start: auto;
|
||||
margin-block-start: 4px;
|
||||
max-inline-size: 210px;
|
||||
text-align: end;
|
||||
|
||||
@media screen and (max-width: 680px) {
|
||||
margin-inline-start: 0;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.likeBar {
|
||||
border-radius: 4px;
|
||||
block-size: 8px;
|
||||
margin-block-end: 4px;
|
||||
}
|
||||
|
||||
.likeCount {
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.videoOptions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-block-start: 16px;
|
||||
|
||||
.option:not(:first-child) {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 680px) {
|
||||
justify-content: flex-start;
|
||||
|
||||
:deep(.iconDropdown) {
|
||||
inset-inline-start: calc(50% - 20px);
|
||||
inset-inline-end: auto;
|
||||
@media screen and (max-width: 460px) {
|
||||
flex-wrap: nowrap;
|
||||
:deep(.iconDropdown) {
|
||||
inset-inline-start: auto;
|
||||
inset-inline-end: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 460px) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
margin-block-start: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.videoMetrics {
|
||||
font-size: 14px;
|
||||
color: var(--tertiary-text-color);
|
||||
|
||||
.likeSection {
|
||||
color: var(--tertiary-text-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-inline-start: auto;
|
||||
margin-block-start: 4px;
|
||||
max-inline-size: 210px;
|
||||
text-align: end;
|
||||
|
||||
@media screen and (max-width: 680px) {
|
||||
margin-inline-start: 0;
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.likeBar {
|
||||
border-radius: 4px;
|
||||
block-size: 8px;
|
||||
margin-block-end: 4px;
|
||||
}
|
||||
|
||||
.likeCount {
|
||||
margin-inline-end: 0;
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
}
|
||||
}
|
||||
.datePublishedAndViewCount {
|
||||
@media only screen and (max-width: 460px) {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
flex-direction: column;
|
||||
|
||||
.seperator {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.videoViews {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,47 +6,15 @@
|
|||
>
|
||||
{{ title }}
|
||||
</h1>
|
||||
<div
|
||||
class="channelInformation"
|
||||
>
|
||||
<div
|
||||
class="profileRow"
|
||||
>
|
||||
<div>
|
||||
<router-link
|
||||
:to="`/channel/${channelId}`"
|
||||
>
|
||||
<img
|
||||
:src="channelThumbnail"
|
||||
class="channelThumbnail"
|
||||
alt=""
|
||||
>
|
||||
</router-link>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
:to="`/channel/${channelId}`"
|
||||
class="channelName"
|
||||
>
|
||||
{{ channelName }}
|
||||
</router-link>
|
||||
<ft-subscribe-button
|
||||
v-if="!hideUnsubscribeButton"
|
||||
:channel-id="channelId"
|
||||
:channel-name="channelName"
|
||||
:channel-thumbnail="channelThumbnail"
|
||||
:subscription-count-text="subscriptionCountText"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="datePublished">
|
||||
<div class="videoMetrics">
|
||||
<div class="datePublishedAndViewCount">
|
||||
{{ publishedString }} {{ dateString }}
|
||||
</div>
|
||||
<div class="viewCount">
|
||||
{{ parsedViewCount }}
|
||||
<template
|
||||
v-if="!hideVideoViews"
|
||||
>
|
||||
<span class="seperator">• </span><span class="videoViews">{{ parsedViewCount }}</span>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="!hideVideoLikesAndDislikes"
|
||||
|
@ -55,9 +23,7 @@
|
|||
<div
|
||||
class="likeSection"
|
||||
>
|
||||
<div>
|
||||
<span class="likeCount"><font-awesome-icon :icon="['fas', 'thumbs-up']" /> {{ parsedLikeCount }}</span>
|
||||
</div>
|
||||
<span class="likeCount"><font-awesome-icon :icon="['fas', 'thumbs-up']" /> {{ parsedLikeCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!--
|
||||
|
@ -80,6 +46,38 @@
|
|||
</div>
|
||||
</div>
|
||||
-->
|
||||
</div>
|
||||
<div class="videoButtons">
|
||||
<div
|
||||
class="profileRow"
|
||||
>
|
||||
<div>
|
||||
<router-link
|
||||
:to="`/channel/${channelId}`"
|
||||
>
|
||||
<img
|
||||
:src="channelThumbnail"
|
||||
class="channelThumbnail"
|
||||
alt=""
|
||||
>
|
||||
</router-link>
|
||||
</div>
|
||||
<div>
|
||||
<router-link
|
||||
:to="`/channel/${channelId}`"
|
||||
class="channelName"
|
||||
>
|
||||
{{ channelName }}
|
||||
</router-link>
|
||||
<ft-subscribe-button
|
||||
v-if="!hideUnsubscribeButton"
|
||||
:channel-id="channelId"
|
||||
:channel-name="channelName"
|
||||
:channel-thumbnail="channelThumbnail"
|
||||
:subscription-count-text="subscriptionCountText"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="videoOptions">
|
||||
<ft-icon-button
|
||||
v-if="showPlaylists && !isUpcoming"
|
||||
|
|
|
@ -85,19 +85,6 @@
|
|||
transition: background 0.2s ease-in;
|
||||
}
|
||||
|
||||
.videoIndexContainer {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.videoIndex {
|
||||
color: var(--tertiary-text-color);
|
||||
}
|
||||
|
||||
.videoIndexIcon {
|
||||
font-size: 14px;
|
||||
color: var(--tertiary-text-color);
|
||||
}
|
||||
|
||||
.videoInfo {
|
||||
margin-inline-start: 30px;
|
||||
position: relative;
|
||||
|
|
|
@ -2,7 +2,7 @@ import { defineComponent, nextTick } from 'vue'
|
|||
import { mapMutations } from 'vuex'
|
||||
import FtLoader from '../ft-loader/ft-loader.vue'
|
||||
import FtCard from '../ft-card/ft-card.vue'
|
||||
import FtListVideoLazy from '../ft-list-video-lazy/ft-list-video-lazy.vue'
|
||||
import FtListVideoNumbered from '../ft-list-video-numbered/ft-list-video-numbered.vue'
|
||||
import { copyToClipboard, showToast } from '../../helpers/utils'
|
||||
import {
|
||||
getLocalPlaylist,
|
||||
|
@ -16,7 +16,7 @@ export default defineComponent({
|
|||
components: {
|
||||
'ft-loader': FtLoader,
|
||||
'ft-card': FtCard,
|
||||
'ft-list-video-lazy': FtListVideoLazy,
|
||||
'ft-list-video-numbered': FtListVideoNumbered
|
||||
},
|
||||
props: {
|
||||
playlistId: {
|
||||
|
@ -302,7 +302,8 @@ export default defineComponent({
|
|||
|
||||
playNextVideo: function () {
|
||||
const playlistInfo = {
|
||||
playlistId: this.playlistId
|
||||
playlistId: this.playlistId,
|
||||
playlistType: this.playlistType,
|
||||
}
|
||||
|
||||
const videoIndex = this.videoIndexInPlaylistItems
|
||||
|
@ -349,7 +350,8 @@ export default defineComponent({
|
|||
showToast(this.$t('Playing Previous Video'))
|
||||
|
||||
const playlistInfo = {
|
||||
playlistId: this.playlistId
|
||||
playlistId: this.playlistId,
|
||||
playlistType: this.playlistType,
|
||||
}
|
||||
|
||||
const videoIndex = this.videoIndexInPlaylistItems
|
||||
|
@ -471,14 +473,13 @@ export default defineComponent({
|
|||
parseUserPlaylist: function (playlist, { allowPlayingVideoRemoval = true } = {}) {
|
||||
this.playlistTitle = playlist.playlistName
|
||||
this.channelName = ''
|
||||
this.channelThumbnail = ''
|
||||
this.channelId = ''
|
||||
|
||||
if (this.playlistItems.length === 0 || allowPlayingVideoRemoval) {
|
||||
this.playlistItems = playlist.videos
|
||||
} else {
|
||||
// `this.currentVideo` relies on `playlistItems`
|
||||
const latestPlaylistContainsCurrentVideo = playlist.videos.find(v => v.playlistItemId === this.playlistItemId) != null
|
||||
const latestPlaylistContainsCurrentVideo = playlist.videos.some(v => v.playlistItemId === this.playlistItemId)
|
||||
// Only update list of videos if latest video list still contains currently playing video
|
||||
if (latestPlaylistContainsCurrentVideo) {
|
||||
this.playlistItems = playlist.videos
|
||||
|
@ -511,7 +512,7 @@ export default defineComponent({
|
|||
const currentVideoItem = (this.$refs.currentVideoItem || [])[0]
|
||||
if (container != null && currentVideoItem != null) {
|
||||
// Watch view can be ready sooner than this component
|
||||
container.scrollTop = currentVideoItem.offsetTop - container.offsetTop
|
||||
container.scrollTop = currentVideoItem.$el.offsetTop - container.offsetTop
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -119,41 +119,25 @@
|
|||
ref="playlistItems"
|
||||
class="playlistItems"
|
||||
>
|
||||
<div
|
||||
<ft-list-video-numbered
|
||||
v-for="(item, index) in playlistItems"
|
||||
:key="item.playlistItemId || item.videoId"
|
||||
:ref="currentVideoIndexZeroBased === index ? 'currentVideoItem' : null"
|
||||
class="playlistItem"
|
||||
>
|
||||
<div class="videoIndexContainer">
|
||||
<font-awesome-icon
|
||||
v-if="currentVideoIndexZeroBased === index"
|
||||
class="videoIndexIcon"
|
||||
:icon="['fas', 'play']"
|
||||
/>
|
||||
<p
|
||||
v-else
|
||||
class="videoIndex"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</p>
|
||||
</div>
|
||||
<ft-list-video-lazy
|
||||
:data="item"
|
||||
:playlist-id="playlistId"
|
||||
:playlist-type="playlistType"
|
||||
:playlist-index="reversePlaylist ? playlistItems.length - index - 1 : index"
|
||||
:playlist-item-id="item.playlistItemId"
|
||||
:playlist-reverse="reversePlaylist"
|
||||
:playlist-shuffle="shuffleEnabled"
|
||||
:playlist-loop="loopEnabled"
|
||||
:hide-forbidden-titles="false"
|
||||
appearance="watchPlaylistItem"
|
||||
force-list-type="list"
|
||||
:initial-visible-state="index < (currentVideoIndexZeroBased + 4) && index > (currentVideoIndexZeroBased - 4)"
|
||||
@pause-player="$emit('pause-player')"
|
||||
/>
|
||||
</div>
|
||||
:data="item"
|
||||
:playlist-id="playlistId"
|
||||
:playlist-type="playlistType"
|
||||
:playlist-index="reversePlaylist ? playlistItems.length - index - 1 : index"
|
||||
:playlist-item-id="item.playlistItemId"
|
||||
:playlist-reverse="reversePlaylist"
|
||||
:playlist-shuffle="shuffleEnabled"
|
||||
:playlist-loop="loopEnabled"
|
||||
:video-index="index"
|
||||
:is-current-video="currentVideoIndexZeroBased === index"
|
||||
appearance="watchPlaylistItem"
|
||||
:initial-visible-state="index < (currentVideoIndexZeroBased + 4) && index > (currentVideoIndexZeroBased - 4)"
|
||||
@pause-player="$emit('pause-player')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ft-card>
|
||||
|
|
|
@ -367,7 +367,7 @@ export async function generateInvidiousDashManifestLocally(formats) {
|
|||
|
||||
return await FormatUtils.toDash({
|
||||
adaptive_formats: formats
|
||||
}, urlTransformer, undefined, undefined, player)
|
||||
}, false, urlTransformer, undefined, undefined, player)
|
||||
}
|
||||
|
||||
export function convertInvidiousToLocalFormat(format) {
|
||||
|
|
|
@ -35,23 +35,23 @@ const TRACKING_PARAM_NAMES = [
|
|||
* @param {boolean} options.generateSessionLocally generate the session locally or let YouTube generate it (local is faster, remote is more accurate)
|
||||
* @returns the Innertube instance
|
||||
*/
|
||||
async function createInnertube(options = { withPlayer: false, location: undefined, safetyMode: false, clientType: undefined, generateSessionLocally: true }) {
|
||||
async function createInnertube({ withPlayer = false, location = undefined, safetyMode = false, clientType = undefined, generateSessionLocally = true } = {}) {
|
||||
let cache
|
||||
if (options.withPlayer) {
|
||||
if (withPlayer) {
|
||||
const userData = await getUserDataPath()
|
||||
cache = new PlayerCache(join(userData, 'player_cache'))
|
||||
}
|
||||
|
||||
return await Innertube.create({
|
||||
retrieve_player: !!options.withPlayer,
|
||||
location: options.location,
|
||||
enable_safety_mode: !!options.safetyMode,
|
||||
client_type: options.clientType,
|
||||
retrieve_player: !!withPlayer,
|
||||
location: location,
|
||||
enable_safety_mode: !!safetyMode,
|
||||
client_type: clientType,
|
||||
|
||||
// use browser fetch
|
||||
fetch: (input, init) => fetch(input, init),
|
||||
cache,
|
||||
generate_session_locally: !!options.generateSessionLocally
|
||||
generate_session_locally: !!generateSessionLocally
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -274,6 +274,9 @@ export async function getLocalChannel(id) {
|
|||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
export async function getLocalChannelVideos(id) {
|
||||
const innertube = await createInnertube()
|
||||
|
||||
|
@ -286,13 +289,22 @@ export async function getLocalChannelVideos(id) {
|
|||
}))
|
||||
|
||||
const videosTab = new YT.Channel(null, response)
|
||||
const { id: channelId = id, name, thumbnailUrl } = parseLocalChannelHeader(videosTab)
|
||||
|
||||
let videos
|
||||
|
||||
// if the channel doesn't have a videos tab, YouTube returns the home tab instead
|
||||
// so we need to check that we got the right tab
|
||||
if (videosTab.current_tab?.endpoint.metadata.url?.endsWith('/videos')) {
|
||||
return parseLocalChannelVideos(videosTab.videos, videosTab.header.author)
|
||||
videos = parseLocalChannelVideos(videosTab.videos, channelId, name)
|
||||
} else {
|
||||
return []
|
||||
videos = []
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
thumbnailUrl,
|
||||
videos
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -304,6 +316,9 @@ export async function getLocalChannelVideos(id) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} id
|
||||
*/
|
||||
export async function getLocalChannelLiveStreams(id) {
|
||||
const innertube = await createInnertube()
|
||||
|
||||
|
@ -316,13 +331,22 @@ export async function getLocalChannelLiveStreams(id) {
|
|||
}))
|
||||
|
||||
const liveStreamsTab = new YT.Channel(null, response)
|
||||
const { id: channelId = id, name, thumbnailUrl } = parseLocalChannelHeader(liveStreamsTab)
|
||||
|
||||
let videos
|
||||
|
||||
// if the channel doesn't have a live tab, YouTube returns the home tab instead
|
||||
// so we need to check that we got the right tab
|
||||
if (liveStreamsTab.current_tab?.endpoint.metadata.url?.endsWith('/streams')) {
|
||||
return parseLocalChannelVideos(liveStreamsTab.videos, liveStreamsTab.header.author)
|
||||
videos = parseLocalChannelVideos(liveStreamsTab.videos, channelId, name)
|
||||
} else {
|
||||
return []
|
||||
videos = []
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
thumbnailUrl,
|
||||
videos
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
@ -365,16 +389,158 @@ export async function getLocalChannelCommunity(id) {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {import('youtubei.js').YTNodes.Video[]} videos
|
||||
* @param {Misc.Author} author
|
||||
* @param {YT.Channel} channel
|
||||
*/
|
||||
export function parseLocalChannelVideos(videos, author) {
|
||||
export function parseLocalChannelHeader(channel) {
|
||||
/** @type {string=} */
|
||||
let id
|
||||
/** @type {string} */
|
||||
let name
|
||||
/** @type {string=} */
|
||||
let thumbnailUrl
|
||||
/** @type {string=} */
|
||||
let bannerUrl
|
||||
/** @type {string=} */
|
||||
let subscriberText
|
||||
/** @type {string[]} */
|
||||
const tags = []
|
||||
|
||||
switch (channel.header.type) {
|
||||
case 'C4TabbedHeader': {
|
||||
// example: Linus Tech Tips
|
||||
// https://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.C4TabbedHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
|
||||
id = header.author.id
|
||||
name = header.author.name
|
||||
thumbnailUrl = header.author.best_thumbnail.url
|
||||
bannerUrl = header.banner?.[0]?.url
|
||||
subscriberText = header.subscribers?.text
|
||||
break
|
||||
}
|
||||
case 'CarouselHeader': {
|
||||
// examples: Music and YouTube Gaming
|
||||
// https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ
|
||||
// https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.CarouselHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.TopicChannelDetails}
|
||||
*/
|
||||
const topicChannelDetails = header.contents.find(node => node.type === 'TopicChannelDetails')
|
||||
name = topicChannelDetails.title.text
|
||||
subscriberText = topicChannelDetails.subtitle.text
|
||||
thumbnailUrl = topicChannelDetails.avatar[0].url
|
||||
|
||||
if (channel.metadata.external_id) {
|
||||
id = channel.metadata.external_id
|
||||
} else {
|
||||
id = topicChannelDetails.subscribe_button.channel_id
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'InteractiveTabbedHeader': {
|
||||
// example: Minecraft - Topic
|
||||
// https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.InteractiveTabbedHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
name = header.title.text
|
||||
thumbnailUrl = header.box_art.at(-1).url
|
||||
bannerUrl = header.banner[0]?.url
|
||||
|
||||
const badges = header.badges.map(badge => badge.label).filter(tag => tag)
|
||||
tags.push(...badges)
|
||||
|
||||
id = channel.current_tab?.endpoint.payload.browseId
|
||||
break
|
||||
}
|
||||
case 'PageHeader': {
|
||||
// example: YouTube Gaming
|
||||
// https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg
|
||||
|
||||
// User channels (an A/B test at the time of writing)
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.PageHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
|
||||
name = header.content.title.text.text
|
||||
if (header.content.image) {
|
||||
if (header.content.image.type === 'ContentPreviewImageView') {
|
||||
/** @type {import('youtubei.js').YTNodes.ContentPreviewImageView} */
|
||||
const image = header.content.image
|
||||
|
||||
thumbnailUrl = image.image[0].url
|
||||
} else {
|
||||
/** @type {import('youtubei.js').YTNodes.DecoratedAvatarView} */
|
||||
const image = header.content.image
|
||||
thumbnailUrl = image.avatar?.image[0].url
|
||||
}
|
||||
}
|
||||
|
||||
if (!thumbnailUrl && channel.metadata.thumbnail) {
|
||||
thumbnailUrl = channel.metadata.thumbnail[0].url
|
||||
}
|
||||
|
||||
if (header.content.banner) {
|
||||
bannerUrl = header.content.banner.image[0]?.url
|
||||
}
|
||||
|
||||
if (header.content.actions) {
|
||||
const modal = header.content.actions.actions_rows[0].actions[0].on_tap.modal
|
||||
|
||||
if (modal && modal.type === 'ModalWithTitleAndButton') {
|
||||
/** @type {import('youtubei.js').YTNodes.ModalWithTitleAndButton} */
|
||||
const typedModal = modal
|
||||
|
||||
id = typedModal.button.endpoint.next_endpoint?.payload.browseId
|
||||
}
|
||||
} else if (channel.metadata.external_id) {
|
||||
id = channel.metadata.external_id
|
||||
}
|
||||
|
||||
if (header.content.metadata) {
|
||||
subscriberText = header.content.metadata.metadata_rows[0].metadata_parts[1].text.text
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
thumbnailUrl,
|
||||
bannerUrl,
|
||||
subscriberText,
|
||||
tags
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('youtubei.js').YTNodes.Video[]} videos
|
||||
* @param {string} channelId
|
||||
* @param {string} channelName
|
||||
*/
|
||||
export function parseLocalChannelVideos(videos, channelId, channelName) {
|
||||
const parsedVideos = videos.map(parseLocalListVideo)
|
||||
|
||||
// fix empty author info
|
||||
parsedVideos.forEach(video => {
|
||||
video.author = author.name
|
||||
video.authorId = author.id
|
||||
video.author = channelName
|
||||
video.authorId = channelId
|
||||
})
|
||||
|
||||
return parsedVideos
|
||||
|
@ -382,17 +548,18 @@ export function parseLocalChannelVideos(videos, author) {
|
|||
|
||||
/**
|
||||
* @param {import('youtubei.js').YTNodes.ReelItem[]} shorts
|
||||
* @param {Misc.Author} author
|
||||
* @param {string} channelId
|
||||
* @param {string} channelName
|
||||
*/
|
||||
export function parseLocalChannelShorts(shorts, author) {
|
||||
export function parseLocalChannelShorts(shorts, channelId, channelName) {
|
||||
return shorts.map(short => {
|
||||
return {
|
||||
type: 'video',
|
||||
videoId: short.id,
|
||||
title: short.title.text,
|
||||
author: author.name,
|
||||
authorId: author.id,
|
||||
viewCount: parseLocalSubscriberCount(short.views.text),
|
||||
author: channelName,
|
||||
authorId: channelId,
|
||||
viewCount: short.views.isEmpty() ? null : parseLocalSubscriberCount(short.views.text),
|
||||
lengthSeconds: ''
|
||||
}
|
||||
})
|
||||
|
@ -405,40 +572,43 @@ export function parseLocalChannelShorts(shorts, author) {
|
|||
|
||||
/**
|
||||
* @param {Playlist|GridPlaylist} playlist
|
||||
* @param {Misc.Author} author
|
||||
* @param {string} channelId
|
||||
* @param {string} chanelName
|
||||
*/
|
||||
export function parseLocalListPlaylist(playlist, author = undefined) {
|
||||
let channelName
|
||||
let channelId = null
|
||||
/** @type {import('youtubei.js').YTNodes.PlaylistVideoThumbnail} */
|
||||
const thumbnailRenderer = playlist.thumbnail_renderer
|
||||
export function parseLocalListPlaylist(playlist, channelId = undefined, channelName = undefined) {
|
||||
let internalChannelName
|
||||
let internalChannelId = null
|
||||
|
||||
if (playlist.author && playlist.author.id !== 'N/A') {
|
||||
if (playlist.author instanceof Misc.Text) {
|
||||
channelName = playlist.author.text
|
||||
internalChannelName = playlist.author.text
|
||||
|
||||
if (author) {
|
||||
channelId = author.id
|
||||
if (channelId) {
|
||||
internalChannelId = channelId
|
||||
}
|
||||
} else {
|
||||
channelName = playlist.author.name
|
||||
channelId = playlist.author.id
|
||||
internalChannelName = playlist.author.name
|
||||
internalChannelId = playlist.author.id
|
||||
}
|
||||
} else if (author) {
|
||||
channelName = author.name
|
||||
channelId = author.id
|
||||
} else if (channelId || channelName) {
|
||||
internalChannelName = channelName
|
||||
internalChannelId = channelId
|
||||
} else if (playlist.author?.name) {
|
||||
// auto-generated album playlists don't have an author
|
||||
// so in search results, the author text is "Playlist" and doesn't have a link or channel ID
|
||||
channelName = playlist.author.name
|
||||
internalChannelName = playlist.author.name
|
||||
}
|
||||
|
||||
/** @type {import('youtubei.js').YTNodes.PlaylistVideoThumbnail} */
|
||||
const thumbnailRenderer = playlist.thumbnail_renderer
|
||||
|
||||
return {
|
||||
type: 'playlist',
|
||||
dataSource: 'local',
|
||||
title: playlist.title.text,
|
||||
thumbnail: thumbnailRenderer ? thumbnailRenderer.thumbnail[0].url : playlist.thumbnails[0].url,
|
||||
channelName,
|
||||
channelId,
|
||||
channelName: internalChannelName,
|
||||
channelId: internalChannelId,
|
||||
playlistId: playlist.id,
|
||||
videoCount: extractNumberFromString(playlist.video_count.text)
|
||||
}
|
||||
|
@ -742,7 +912,7 @@ export function parseLocalTextRuns(runs, emojiSize = 16, options = { looseChanne
|
|||
case 'WEB_PAGE_TYPE_CHANNEL': {
|
||||
const trimmedText = text.trim()
|
||||
// In comments, mention can be `@Channel Name` (not handle, but name)
|
||||
if (CHANNEL_HANDLE_REGEX.test(trimmedText) || (options.looseChannelNameDetection && trimmedText.startsWith('@'))) {
|
||||
if (CHANNEL_HANDLE_REGEX.test(trimmedText) || options.looseChannelNameDetection) {
|
||||
// Note that in regex `\s` must be used since the text contain non-default space (the half-width space char when we press spacebar)
|
||||
const spacesBefore = (spacesBeforeRegex.exec(text) || [''])[0]
|
||||
const spacesAfter = (spacesAfterRegex.exec(text) || [''])[0]
|
||||
|
|
|
@ -80,9 +80,14 @@ export async function parseYouTubeRSSFeed(rssString, channelId) {
|
|||
promises.push(parseRSSEntry(entry, channelId, channelName))
|
||||
}
|
||||
|
||||
return await Promise.all(promises)
|
||||
return {
|
||||
name: channelName,
|
||||
videos: await Promise.all(promises)
|
||||
}
|
||||
} catch (e) {
|
||||
return []
|
||||
return {
|
||||
videos: []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -616,9 +616,10 @@ export function getVideoParamsFromUrl(url) {
|
|||
|
||||
/**
|
||||
* This will match sequences of upper case characters and convert them into title cased words.
|
||||
* This will also match excessive strings of punctionation and convert them to one representative character
|
||||
* @param {string} title the title to process
|
||||
* @param {number} minUpperCase the minimum number of consecutive upper case characters to match
|
||||
* @returns {string} the title with upper case characters removed
|
||||
* @returns {string} the title with upper case characters removed and punctuation normalized
|
||||
*/
|
||||
export function toDistractionFreeTitle(title, minUpperCase = 3) {
|
||||
const firstValidCharIndex = (word) => {
|
||||
|
@ -634,7 +635,10 @@ export function toDistractionFreeTitle(title, minUpperCase = 3) {
|
|||
}
|
||||
|
||||
const reg = RegExp(`[\\p{Lu}|']{${minUpperCase},}`, 'ug')
|
||||
return title.replace(reg, x => capitalizedWord(x.toLowerCase()))
|
||||
return title
|
||||
.replaceAll(/!{2,}/g, '!')
|
||||
.replaceAll(/[!?]{2,}/g, '?')
|
||||
.replace(reg, x => capitalizedWord(x.toLowerCase()))
|
||||
}
|
||||
|
||||
export function formatNumber(number, options = undefined) {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import fs from 'fs/promises'
|
||||
import { pathExists } from '../../helpers/filesystem'
|
||||
import { createWebURL, fetchWithTimeout } from '../../helpers/utils'
|
||||
|
||||
const state = {
|
||||
|
@ -40,6 +39,7 @@ const actions = {
|
|||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// If the invidious instance fetch isn't returning anything interpretable
|
||||
if (instances.length === 0) {
|
||||
// Fallback: read from static file
|
||||
|
@ -47,15 +47,13 @@ const actions = {
|
|||
/* eslint-disable-next-line n/no-path-concat */
|
||||
const fileLocation = process.env.NODE_ENV === 'development' ? './static/' : `${__dirname}/static/`
|
||||
const filePath = `${fileLocation}${fileName}`
|
||||
if (!process.env.IS_ELECTRON || await pathExists(filePath)) {
|
||||
console.warn('reading static file for invidious instances')
|
||||
const fileData = process.env.IS_ELECTRON ? await fs.readFile(filePath, 'utf8') : await (await fetch(createWebURL(filePath))).text()
|
||||
instances = JSON.parse(fileData).filter(e => {
|
||||
return process.env.IS_ELECTRON || e.cors
|
||||
}).map(e => {
|
||||
return e.url
|
||||
})
|
||||
}
|
||||
console.warn('reading static file for invidious instances')
|
||||
const fileData = process.env.IS_ELECTRON ? await fs.readFile(filePath, 'utf8') : await (await fetch(createWebURL(filePath))).text()
|
||||
instances = JSON.parse(fileData).filter(e => {
|
||||
return process.env.IS_ELECTRON || e.cors
|
||||
}).map(e => {
|
||||
return e.url
|
||||
})
|
||||
}
|
||||
commit('setInvidiousInstancesList', instances)
|
||||
},
|
||||
|
|
|
@ -63,10 +63,13 @@ const actions = {
|
|||
payload.createdAt = Date.now()
|
||||
payload.lastUpdatedAt = Date.now()
|
||||
// Ensure all videos has required attributes
|
||||
|
||||
const currentTime = new Date().getTime()
|
||||
|
||||
if (Array.isArray(payload.videos)) {
|
||||
payload.videos.forEach(videoData => {
|
||||
if (videoData.timeAdded == null) {
|
||||
videoData.timeAdded = new Date().getTime()
|
||||
videoData.timeAdded = currentTime
|
||||
}
|
||||
if (videoData.playlistItemId == null) {
|
||||
videoData.playlistItemId = generateRandomUniqueId()
|
||||
|
@ -149,11 +152,14 @@ const actions = {
|
|||
// Since this action will ensure uniqueness of `playlistItemId` of added video entries
|
||||
try {
|
||||
const { _id, videos } = payload
|
||||
|
||||
const currentTime = new Date().getTime()
|
||||
|
||||
const newVideoObjects = videos.map((video) => {
|
||||
// Create a new object to prevent changing existing values outside
|
||||
const videoData = Object.assign({}, video)
|
||||
if (videoData.timeAdded == null) {
|
||||
videoData.timeAdded = new Date().getTime()
|
||||
videoData.timeAdded = currentTime
|
||||
}
|
||||
videoData.playlistItemId = generateRandomUniqueId()
|
||||
// For backward compatibility
|
||||
|
@ -188,6 +194,9 @@ const actions = {
|
|||
dispatch('addPlaylist', playlist)
|
||||
})
|
||||
} else {
|
||||
const dateNow = Date.now()
|
||||
const currentTime = new Date().getTime()
|
||||
|
||||
payload.forEach((playlist) => {
|
||||
let anythingUpdated = false
|
||||
// Assign generated playlist ID in case DB data corrupted
|
||||
|
@ -205,19 +214,19 @@ const actions = {
|
|||
// Assign current time as created time in case DB data corrupted
|
||||
if (playlist.createdAt == null) {
|
||||
// Time now in unix time, in ms
|
||||
playlist.createdAt = Date.now()
|
||||
playlist.createdAt = dateNow
|
||||
anythingUpdated = true
|
||||
}
|
||||
// Assign current time as last updated time in case DB data corrupted
|
||||
if (playlist.lastUpdatedAt == null) {
|
||||
// Time now in unix time, in ms
|
||||
playlist.lastUpdatedAt = Date.now()
|
||||
playlist.lastUpdatedAt = dateNow
|
||||
anythingUpdated = true
|
||||
}
|
||||
playlist.videos.forEach((v) => {
|
||||
// Ensure all videos has `timeAdded` property
|
||||
if (v.timeAdded == null) {
|
||||
v.timeAdded = new Date().getTime()
|
||||
v.timeAdded = currentTime
|
||||
anythingUpdated = true
|
||||
}
|
||||
|
||||
|
@ -257,8 +266,9 @@ const actions = {
|
|||
return playlist.playlistName === 'Watch Later' || playlist._id === 'watchLater'
|
||||
})
|
||||
|
||||
const defaultFavoritesPlaylist = state.defaultPlaylists.find((e) => e._id === 'favorites')
|
||||
if (favoritesPlaylist != null) {
|
||||
const defaultFavoritesPlaylist = state.defaultPlaylists.find((e) => e._id === 'favorites')
|
||||
|
||||
// Update existing matching playlist only if it exists
|
||||
if (favoritesPlaylist._id !== defaultFavoritesPlaylist._id || favoritesPlaylist.protected !== defaultFavoritesPlaylist.protected) {
|
||||
const oldId = favoritesPlaylist._id
|
||||
|
@ -277,8 +287,9 @@ const actions = {
|
|||
}
|
||||
}
|
||||
|
||||
const defaultWatchLaterPlaylist = state.defaultPlaylists.find((e) => e._id === 'watchLater')
|
||||
if (watchLaterPlaylist != null) {
|
||||
const defaultWatchLaterPlaylist = state.defaultPlaylists.find((e) => e._id === 'watchLater')
|
||||
|
||||
// Update existing matching playlist only if it exists
|
||||
if (watchLaterPlaylist._id !== defaultWatchLaterPlaylist._id || watchLaterPlaylist.protected !== defaultWatchLaterPlaylist.protected) {
|
||||
const oldId = watchLaterPlaylist._id
|
||||
|
@ -394,7 +405,7 @@ const mutations = {
|
|||
addVideos(state, payload) {
|
||||
const playlist = state.playlists.find(playlist => playlist._id === payload._id)
|
||||
if (playlist) {
|
||||
playlist.videos = [].concat(playlist.videos).concat(payload.videos)
|
||||
playlist.videos = [].concat(playlist.videos, payload.videos)
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -91,6 +91,43 @@ const actions = {
|
|||
commit('setProfileList', profiles)
|
||||
},
|
||||
|
||||
async batchUpdateSubscriptionDetails({ getters, dispatch }, channels) {
|
||||
if (channels.length === 0) { return }
|
||||
|
||||
const profileList = getters.getProfileList
|
||||
|
||||
for (const profile of profileList) {
|
||||
const currentProfileCopy = deepCopy(profile)
|
||||
let profileUpdated = false
|
||||
|
||||
for (const { channelThumbnailUrl, channelName, channelId } of channels) {
|
||||
const channel = currentProfileCopy.subscriptions.find((channel) => {
|
||||
return channel.id === channelId
|
||||
}) ?? null
|
||||
|
||||
if (channel === null) { continue }
|
||||
|
||||
if (channel.name !== channelName && channelName != null) {
|
||||
channel.name = channelName
|
||||
profileUpdated = true
|
||||
}
|
||||
|
||||
if (channelThumbnailUrl) {
|
||||
const thumbnail = channelThumbnailUrl.replace(/=s\d*/, '=s176') // change thumbnail size if different
|
||||
|
||||
if (channel.thumbnail !== thumbnail) {
|
||||
channel.thumbnail = thumbnail
|
||||
profileUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (profileUpdated) {
|
||||
await dispatch('updateProfile', currentProfileCopy)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async updateSubscriptionDetails({ getters, dispatch }, { channelThumbnailUrl, channelName, channelId }) {
|
||||
const thumbnail = channelThumbnailUrl?.replace(/=s\d*/, '=s176') ?? null // change thumbnail size if different
|
||||
const profileList = getters.getProfileList
|
||||
|
|
|
@ -289,7 +289,7 @@ const state = {
|
|||
videoPlaybackRateInterval: 0.25,
|
||||
downloadAskPath: true,
|
||||
downloadFolderPath: '',
|
||||
downloadBehavior: 'download',
|
||||
downloadBehavior: 'open',
|
||||
enableScreenshot: false,
|
||||
screenshotFormat: 'png',
|
||||
screenshotQuality: 95,
|
||||
|
|
|
@ -54,6 +54,10 @@ const actions = {
|
|||
commit('updateShortsCacheByChannel', payload)
|
||||
},
|
||||
|
||||
updateSubscriptionShortsCacheWithChannelPageShorts: ({ commit }, payload) => {
|
||||
commit('updateShortsCacheWithChannelPageShorts', payload)
|
||||
},
|
||||
|
||||
updateSubscriptionLiveCacheByChannel: ({ commit }, payload) => {
|
||||
commit('updateLiveCacheByChannel', payload)
|
||||
},
|
||||
|
@ -86,6 +90,31 @@ const mutations = {
|
|||
if (videos != null) { newObject.videos = videos }
|
||||
state.shortsCache[channelId] = newObject
|
||||
},
|
||||
updateShortsCacheWithChannelPageShorts(state, { channelId, videos }) {
|
||||
const cachedObject = state.shortsCache[channelId]
|
||||
|
||||
if (cachedObject && cachedObject.videos.length > 0) {
|
||||
cachedObject.videos.forEach(cachedVideo => {
|
||||
const channelVideo = videos.find(short => cachedVideo.videoId === short.videoId)
|
||||
|
||||
if (channelVideo) {
|
||||
// authorId probably never changes, so we don't need to update that
|
||||
|
||||
cachedVideo.title = channelVideo.title
|
||||
cachedVideo.author = channelVideo.author
|
||||
|
||||
// as the channel shorts page only has compact view counts for numbers above 1000 e.g. 12k
|
||||
// and the RSS feeds include an exact value, we only want to overwrite it when the number is larger than the cached value
|
||||
// 12345 vs 12000 => 12345
|
||||
// 12345 vs 15000 => 15000
|
||||
|
||||
if (channelVideo.viewCount > cachedVideo.viewCount) {
|
||||
cachedVideo.viewCount = channelVideo.viewCount
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
clearShortsCache(state) {
|
||||
state.shortsCache = {}
|
||||
},
|
||||
|
|
|
@ -358,14 +358,10 @@ const actions = {
|
|||
},
|
||||
|
||||
async getRegionData ({ commit }, { locale }) {
|
||||
let localePathExists
|
||||
const localePathExists = process.env.GEOLOCATION_NAMES.includes(locale)
|
||||
// Exclude __dirname from path if not in electron
|
||||
const fileLocation = `${process.env.IS_ELECTRON ? process.env.NODE_ENV === 'development' ? '.' : __dirname : ''}/static/geolocations/`
|
||||
if (process.env.IS_ELECTRON) {
|
||||
localePathExists = await pathExists(`${fileLocation}${locale}.json`)
|
||||
} else {
|
||||
localePathExists = process.env.GEOLOCATION_NAMES.includes(locale)
|
||||
}
|
||||
|
||||
const pathName = `${fileLocation}${localePathExists ? locale : 'en-US'}.json`
|
||||
const countries = process.env.IS_ELECTRON ? JSON.parse(await fs.readFile(pathName)) : await (await fetch(createWebURL(pathName))).json()
|
||||
|
||||
|
@ -601,15 +597,10 @@ const actions = {
|
|||
|
||||
async getExternalPlayerCmdArgumentsData ({ commit }, payload) {
|
||||
const fileName = 'external-player-map.json'
|
||||
let fileData
|
||||
/* eslint-disable-next-line n/no-path-concat */
|
||||
const fileLocation = process.env.NODE_ENV === 'development' ? './static/' : `${__dirname}/static/`
|
||||
|
||||
if (await pathExists(`${fileLocation}${fileName}`)) {
|
||||
fileData = await fs.readFile(`${fileLocation}${fileName}`)
|
||||
} else {
|
||||
fileData = '[{"name":"None","value":"","cmdArguments":null}]'
|
||||
}
|
||||
const fileData = await fs.readFile(`${fileLocation}${fileName}`)
|
||||
|
||||
const externalPlayerMap = JSON.parse(fileData).map((entry) => {
|
||||
return { name: entry.name, nameTranslationKey: entry.nameTranslationKey, value: entry.value, cmdArguments: entry.cmdArguments }
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
.dracula,
|
||||
.catppuccinMocha,
|
||||
.pastelPink,
|
||||
.hotPink {
|
||||
.hotPink,
|
||||
.nordic {
|
||||
--primary-input-color: rgba(0, 0, 0, 0.50);
|
||||
}
|
||||
|
||||
|
@ -15,7 +16,8 @@
|
|||
.dracula,
|
||||
.catppuccinMocha,
|
||||
.pastelPink,
|
||||
.hotPink {
|
||||
.hotPink,
|
||||
.nordic {
|
||||
--link-color: var(--accent-color);
|
||||
--link-visited-color: var(--accent-color-visited);
|
||||
--instance-menu-color: var(--search-bar-color);
|
||||
|
@ -27,7 +29,8 @@
|
|||
.gray,
|
||||
.dracula,
|
||||
.catppuccinMocha,
|
||||
.pastelPink {
|
||||
.pastelPink,
|
||||
.nordic {
|
||||
--primary-input-color: rgba(0, 0, 0, 0.50);
|
||||
--side-nav-hover-text-color: var(--primary-text-color);
|
||||
}
|
||||
|
@ -37,7 +40,8 @@
|
|||
.black,
|
||||
.gray,
|
||||
.dracula,
|
||||
.catppuccinMocha {
|
||||
.catppuccinMocha,
|
||||
.nordic {
|
||||
--side-nav-active-text-color: var(--primary-text-color);
|
||||
--scrollbar-text-color-hover: var(--primary-text-color);
|
||||
|
||||
|
@ -56,7 +60,8 @@
|
|||
.gray,
|
||||
.dracula,
|
||||
.catppuccinMocha,
|
||||
.hotPink {
|
||||
.hotPink,
|
||||
.nordic {
|
||||
--primary-shadow-color: rgba(0, 0, 0, 0.75);
|
||||
}
|
||||
|
||||
|
@ -237,6 +242,25 @@ it can be safely elided. This looks quite pleasant on this theme. */
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nordic {
|
||||
--primary-text-color: #EEEEEE;
|
||||
--secondary-text-color: #ddd;
|
||||
--tertiary-text-color: #EEEEEE;
|
||||
--title-color: #EEEEEE;
|
||||
--bg-color: #2b2f3a;
|
||||
--favorite-icon-color: #FFEA00;
|
||||
--card-bg-color: #2e3440;
|
||||
--secondary-card-bg-color: rgba(59, 66, 82, 0.75);
|
||||
--scrollbar-color: #4b566a;
|
||||
--scrollbar-color-hover: #4b566a;
|
||||
--side-nav-color: #2e3440;
|
||||
--side-nav-hover-color: #3b4252;
|
||||
--side-nav-active-color: #3b4252;
|
||||
--search-bar-color: #4b566a;
|
||||
--logo-icon: url("../../_icons/iconNordicLightSmall.png");
|
||||
--logo-text: url("../../_icons/textNordicLightSmall.png");
|
||||
}
|
||||
|
||||
.mainRed,
|
||||
.mainPink,
|
||||
.mainPurple,
|
||||
|
|
|
@ -217,6 +217,23 @@
|
|||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 680px) {
|
||||
.channelInfo {
|
||||
flex-direction: column;
|
||||
margin-block: 20px 10px;
|
||||
}
|
||||
.card {
|
||||
max-inline-size: none;
|
||||
inline-size: 100%;
|
||||
}
|
||||
.channelInfoActionsContainer {
|
||||
flex-direction: row-reverse;
|
||||
justify-content: left;
|
||||
gap: 10px;
|
||||
margin-block-start: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 400px) {
|
||||
.channelInfo {
|
||||
justify-content: center;
|
||||
|
@ -224,7 +241,11 @@
|
|||
}
|
||||
|
||||
.channelInfoActionsContainer {
|
||||
flex-direction: column-reverse;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.channelLineContainer {
|
||||
padding-inline-start: 0;
|
||||
}
|
||||
|
||||
.thumbnailContainer {
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
import {
|
||||
getLocalChannel,
|
||||
getLocalChannelId,
|
||||
parseLocalChannelHeader,
|
||||
parseLocalChannelShorts,
|
||||
parseLocalChannelVideos,
|
||||
parseLocalCommunityPosts,
|
||||
|
@ -32,6 +33,10 @@ import {
|
|||
parseLocalListVideo,
|
||||
parseLocalSubscriberCount
|
||||
} from '../../helpers/api/local'
|
||||
import {
|
||||
addPublishedDatesInvidious,
|
||||
addPublishedDatesLocal
|
||||
} from '../../helpers/subscriptions'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Channel',
|
||||
|
@ -170,6 +175,13 @@ export default defineComponent({
|
|||
return this.subscriptionInfo !== null
|
||||
},
|
||||
|
||||
isSubscribedInAnyProfile: function () {
|
||||
const profileList = this.$store.getters.getProfileList
|
||||
|
||||
// check the all channels profile
|
||||
return profileList[0].subscriptions.some((channel) => channel.id === this.id)
|
||||
},
|
||||
|
||||
videoLiveSelectNames: function () {
|
||||
return [
|
||||
this.$t('Channel.Videos.Sort Types.Newest'),
|
||||
|
@ -532,90 +544,22 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
let channelId
|
||||
let subscriberText = null
|
||||
let tags = []
|
||||
const parsedHeader = parseLocalChannelHeader(channel)
|
||||
|
||||
switch (channel.header.type) {
|
||||
case 'C4TabbedHeader': {
|
||||
// example: Linus Tech Tips
|
||||
// https://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw
|
||||
const channelId = parsedHeader.id ?? this.id
|
||||
const subscriberText = parsedHeader.subscriberText ?? null
|
||||
let tags = parsedHeader.tags
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.C4TabbedHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
channelThumbnailUrl = parsedHeader.thumbnailUrl ?? this.subscriptionInfo?.thumbnail
|
||||
channelName = parsedHeader.name ?? this.subscriptionInfo?.name
|
||||
|
||||
channelId = header.author.id
|
||||
channelName = header.author.name
|
||||
channelThumbnailUrl = header.author.best_thumbnail.url
|
||||
subscriberText = header.subscribers?.text
|
||||
break
|
||||
}
|
||||
case 'CarouselHeader': {
|
||||
// examples: Music and YouTube Gaming
|
||||
// https://www.youtube.com/channel/UC-9-kyTW8ZkZNDHQJ6FgpwQ
|
||||
// https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.CarouselHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.TopicChannelDetails}
|
||||
*/
|
||||
const topicChannelDetails = header.contents.find(node => node.type === 'TopicChannelDetails')
|
||||
channelName = topicChannelDetails.title.text
|
||||
subscriberText = topicChannelDetails.subtitle.text
|
||||
channelThumbnailUrl = topicChannelDetails.avatar[0].url
|
||||
|
||||
if (channel.metadata.external_id) {
|
||||
channelId = channel.metadata.external_id
|
||||
} else {
|
||||
channelId = topicChannelDetails.subscribe_button.channel_id
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'InteractiveTabbedHeader': {
|
||||
// example: Minecraft - Topic
|
||||
// https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.InteractiveTabbedHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
channelName = header.title.text
|
||||
channelId = this.id
|
||||
channelThumbnailUrl = header.box_art.at(-1).url
|
||||
|
||||
const badges = header.badges.map(badge => badge.label).filter(tag => tag)
|
||||
tags.push(...badges)
|
||||
break
|
||||
}
|
||||
case 'PageHeader': {
|
||||
// example: YouTube Gaming (an A/B test at the time of writing)
|
||||
// https://www.youtube.com/channel/UCOpNcN46UbXVtpKMrmU4Abg
|
||||
|
||||
/**
|
||||
* @type {import('youtubei.js').YTNodes.PageHeader}
|
||||
*/
|
||||
const header = channel.header
|
||||
|
||||
channelName = header.content.title.text
|
||||
channelThumbnailUrl = header.content.image.image[0].url
|
||||
channelId = this.id
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (channelThumbnailUrl.startsWith('//')) {
|
||||
if (channelThumbnailUrl?.startsWith('//')) {
|
||||
channelThumbnailUrl = `https:${channelThumbnailUrl}`
|
||||
}
|
||||
|
||||
this.channelName = channelName
|
||||
this.thumbnailUrl = channelThumbnailUrl
|
||||
this.bannerUrl = parsedHeader.bannerUrl ?? null
|
||||
this.isFamilyFriendly = !!channel.metadata.is_family_safe
|
||||
|
||||
if (channel.metadata.tags) {
|
||||
|
@ -646,12 +590,6 @@ export default defineComponent({
|
|||
|
||||
this.updateSubscriptionDetails({ channelThumbnailUrl, channelName, channelId })
|
||||
|
||||
if (channel.header.banner?.length > 0) {
|
||||
this.bannerUrl = channel.header.banner[0].url
|
||||
} else {
|
||||
this.bannerUrl = null
|
||||
}
|
||||
|
||||
let relatedChannels = channel.channels.map(({ author }) => ({
|
||||
name: author.name,
|
||||
id: author.id,
|
||||
|
@ -837,9 +775,20 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestVideos = parseLocalChannelVideos(videosTab.videos, channel.header.author)
|
||||
this.latestVideos = parseLocalChannelVideos(videosTab.videos, this.id, this.channelName)
|
||||
this.videoContinuationData = videosTab.has_continuation ? videosTab : null
|
||||
this.isElementListLoading = false
|
||||
|
||||
if (this.isSubscribedInAnyProfile && this.latestVideos.length > 0 && this.videoSortBy === 'newest') {
|
||||
addPublishedDatesLocal(this.latestVideos)
|
||||
this.updateSubscriptionVideosCacheByChannel({
|
||||
channelId: this.id,
|
||||
// create a copy so that we only cache the first page
|
||||
// if we use the same array, the store will get angry at us for modifying it outside of the store,
|
||||
// when the user clicks load more
|
||||
videos: [...this.latestVideos]
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
|
@ -862,7 +811,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.videoContinuationData.getContinuation()
|
||||
|
||||
this.latestVideos = this.latestVideos.concat(parseLocalChannelVideos(continuation.videos, this.channelInstance.header.author))
|
||||
this.latestVideos = this.latestVideos.concat(parseLocalChannelVideos(continuation.videos, this.id, this.channelName))
|
||||
this.videoContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
@ -895,9 +844,19 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestShorts = parseLocalChannelShorts(shortsTab.videos, channel.header.author)
|
||||
this.latestShorts = parseLocalChannelShorts(shortsTab.videos, this.id, this.channelName)
|
||||
this.shortContinuationData = shortsTab.has_continuation ? shortsTab : null
|
||||
this.isElementListLoading = false
|
||||
|
||||
if (this.isSubscribedInAnyProfile && this.latestShorts.length > 0 && this.shortSortBy === 'newest') {
|
||||
// As the shorts tab API response doesn't include the published dates,
|
||||
// we can't just write the results to the subscriptions cache like we do with videos and live (can't sort chronologically without the date).
|
||||
// However we can still update the metadata in the cache such as the view count and title that might have changed since it was cached
|
||||
this.updateSubscriptionShortsCacheWithChannelPageShorts({
|
||||
channelId: this.id,
|
||||
videos: this.latestShorts
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
|
@ -920,7 +879,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.shortContinuationData.getContinuation()
|
||||
|
||||
this.latestShorts.push(...parseLocalChannelShorts(continuation.videos, this.channelInstance.header.author))
|
||||
this.latestShorts.push(...parseLocalChannelShorts(continuation.videos, this.id, this.channelName))
|
||||
this.shortContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
@ -953,9 +912,20 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestLive = parseLocalChannelVideos(liveTab.videos, channel.header.author)
|
||||
this.latestLive = parseLocalChannelVideos(liveTab.videos, this.id, this.channelName)
|
||||
this.liveContinuationData = liveTab.has_continuation ? liveTab : null
|
||||
this.isElementListLoading = false
|
||||
|
||||
if (this.isSubscribedInAnyProfile && this.latestLive.length > 0 && this.liveSortBy === 'newest') {
|
||||
addPublishedDatesLocal(this.latestLive)
|
||||
this.updateSubscriptionLiveCacheByChannel({
|
||||
channelId: this.id,
|
||||
// create a copy so that we only cache the first page
|
||||
// if we use the same array, the store will get angry at us for modifying it outside of the store,
|
||||
// when the user clicks load more
|
||||
videos: [...this.latestLive]
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
|
@ -978,7 +948,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.liveContinuationData.getContinuation()
|
||||
|
||||
this.latestLive.push(...parseLocalChannelVideos(continuation.videos, this.channelInstance.header.author))
|
||||
this.latestLive.push(...parseLocalChannelVideos(continuation.videos, this.id, this.channelName))
|
||||
this.liveContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
|
@ -1125,6 +1095,16 @@ export default defineComponent({
|
|||
}
|
||||
this.videoContinuationData = response.continuation || null
|
||||
this.isElementListLoading = false
|
||||
|
||||
if (this.isSubscribedInAnyProfile && !more && this.latestVideos.length > 0 && this.videoSortBy === 'newest') {
|
||||
addPublishedDatesInvidious(this.latestVideos)
|
||||
this.updateSubscriptionVideosCacheByChannel({
|
||||
channelId: this.id,
|
||||
// create a copy so that we only cache the first page
|
||||
// if we use the same array, it will also contain all the next pages
|
||||
videos: [...this.latestVideos]
|
||||
})
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
||||
|
@ -1174,6 +1154,17 @@ export default defineComponent({
|
|||
}
|
||||
this.shortContinuationData = response.continuation || null
|
||||
this.isElementListLoading = false
|
||||
|
||||
if (this.isSubscribedInAnyProfile && !more && this.latestShorts.length > 0 && this.shortSortBy === 'newest') {
|
||||
// As the shorts tab API response doesn't include the published dates,
|
||||
// we can't just write the results to the subscriptions cache like we do with videos and live (can't sort chronologically without the date).
|
||||
// However we can still update the metadata in the cache e.g. adding the duration, as that isn't included in the RSS feeds
|
||||
// and updating the view count and title that might have changed since it was cached
|
||||
this.updateSubscriptionShortsCacheWithChannelPageShorts({
|
||||
channelId: this.id,
|
||||
videos: this.latestShorts
|
||||
})
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
||||
|
@ -1215,6 +1206,17 @@ export default defineComponent({
|
|||
}
|
||||
this.liveContinuationData = response.continuation || null
|
||||
this.isElementListLoading = false
|
||||
|
||||
if (this.isSubscribedInAnyProfile && !more && this.latestLive.length > 0 && this.liveSortBy === 'newest') {
|
||||
addPublishedDatesInvidious(this.latestLive)
|
||||
this.updateSubscriptionLiveCacheByChannel({
|
||||
channelId: this.id,
|
||||
// create a copy so that we only cache the first page
|
||||
// if we use the same array, the store will get angry at us for modifying it outside of the store,
|
||||
// when the user clicks load more
|
||||
videos: [...this.latestLive]
|
||||
})
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
||||
|
@ -1270,7 +1272,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestPlaylists = playlistsTab.playlists.map(playlist => parseLocalListPlaylist(playlist, channel.header.author))
|
||||
this.latestPlaylists = playlistsTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.playlistContinuationData = playlistsTab.has_continuation ? playlistsTab : null
|
||||
this.isElementListLoading = false
|
||||
} catch (err) {
|
||||
|
@ -1295,7 +1297,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.playlistContinuationData.getContinuation()
|
||||
|
||||
const parsedPlaylists = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.channelInstance.header.author))
|
||||
const parsedPlaylists = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.latestPlaylists = this.latestPlaylists.concat(parsedPlaylists)
|
||||
this.playlistContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
|
@ -1393,7 +1395,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestReleases = releaseTab.playlists.map(playlist => parseLocalListPlaylist(playlist, channel.header.author))
|
||||
this.latestReleases = releaseTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.releaseContinuationData = releaseTab.has_continuation ? releaseTab : null
|
||||
this.isElementListLoading = false
|
||||
} catch (err) {
|
||||
|
@ -1418,7 +1420,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.releaseContinuationData.getContinuation()
|
||||
|
||||
const parsedReleases = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.channelInstance.header.author))
|
||||
const parsedReleases = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.latestReleases = this.latestReleases.concat(parsedReleases)
|
||||
this.releaseContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
|
@ -1506,7 +1508,7 @@ export default defineComponent({
|
|||
return
|
||||
}
|
||||
|
||||
this.latestPodcasts = podcastTab.playlists.map(playlist => parseLocalListPlaylist(playlist, channel.header.author))
|
||||
this.latestPodcasts = podcastTab.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.podcastContinuationData = podcastTab.has_continuation ? podcastTab : null
|
||||
this.isElementListLoading = false
|
||||
} catch (err) {
|
||||
|
@ -1517,7 +1519,7 @@ export default defineComponent({
|
|||
})
|
||||
if (this.backendPreference === 'local' && this.backendFallback) {
|
||||
showToast(this.$t('Falling back to Invidious API'))
|
||||
this.getChannelPodcastsInvidious()
|
||||
this.channelInvidiousPodcasts()
|
||||
} else {
|
||||
this.isLoading = false
|
||||
}
|
||||
|
@ -1531,7 +1533,7 @@ export default defineComponent({
|
|||
*/
|
||||
const continuation = await this.podcastContinuationData.getContinuation()
|
||||
|
||||
const parsedPodcasts = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.channelInstance.header.author))
|
||||
const parsedPodcasts = continuation.playlists.map(playlist => parseLocalListPlaylist(playlist, this.id, this.channelName))
|
||||
this.latestPodcasts = this.latestPodcasts.concat(parsedPodcasts)
|
||||
this.releaseContinuationData = continuation.has_continuation ? continuation : null
|
||||
} catch (err) {
|
||||
|
@ -1632,6 +1634,19 @@ export default defineComponent({
|
|||
|
||||
this.latestCommunityPosts = parseLocalCommunityPosts(posts)
|
||||
this.communityContinuationData = communityTab.has_continuation ? communityTab : null
|
||||
|
||||
if (this.latestCommunityPosts.length > 0) {
|
||||
this.latestCommunityPosts.forEach(post => {
|
||||
post.authorId = this.id
|
||||
})
|
||||
this.updateSubscriptionPostsCacheByChannel({
|
||||
channelId: this.id,
|
||||
// create a copy so that we only cache the first page
|
||||
// if we use the same array, the store will get angry at us for modifying it outside of the store,
|
||||
// when the user clicks load more
|
||||
posts: [...this.latestCommunityPosts]
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Local API Error (Click to copy)')
|
||||
|
@ -1683,6 +1698,19 @@ export default defineComponent({
|
|||
this.latestCommunityPosts = posts
|
||||
}
|
||||
this.communityContinuationData = continuation
|
||||
|
||||
if (this.isSubscribedInAnyProfile && !more && this.latestCommunityPosts.length > 0) {
|
||||
this.latestCommunityPosts.forEach(post => {
|
||||
post.authorId = this.id
|
||||
})
|
||||
this.updateSubscriptionPostsCacheByChannel({
|
||||
channelId: this.id,
|
||||
// create a copy so that we only cache the first page
|
||||
// if we use the same array, the store will get angry at us for modifying it outside of the store,
|
||||
// when the user clicks load more
|
||||
posts: [...this.latestCommunityPosts]
|
||||
})
|
||||
}
|
||||
}).catch(async (err) => {
|
||||
console.error(err)
|
||||
const errorMessage = this.$t('Invidious API Error (Click to copy)')
|
||||
|
@ -1857,7 +1885,7 @@ export default defineComponent({
|
|||
if (item.type === 'Video') {
|
||||
return parseLocalListVideo(item)
|
||||
} else {
|
||||
return parseLocalListPlaylist(item, this.channelInstance.header.author)
|
||||
return parseLocalListPlaylist(item, this.id, this.channelName)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -1922,7 +1950,11 @@ export default defineComponent({
|
|||
|
||||
...mapActions([
|
||||
'showOutlines',
|
||||
'updateSubscriptionDetails'
|
||||
'updateSubscriptionDetails',
|
||||
'updateSubscriptionVideosCacheByChannel',
|
||||
'updateSubscriptionLiveCacheByChannel',
|
||||
'updateSubscriptionShortsCacheWithChannelPageShorts',
|
||||
'updateSubscriptionPostsCacheByChannel'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
<ft-age-restricted
|
||||
v-else-if="!isLoading && (!isFamilyFriendly && showFamilyFriendlyOnly)"
|
||||
class="ageRestricted"
|
||||
:content-type-string="'Channel'"
|
||||
:is-channel="true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -4,7 +4,7 @@ import debounce from 'lodash.debounce'
|
|||
import FtLoader from '../../components/ft-loader/ft-loader.vue'
|
||||
import FtCard from '../../components/ft-card/ft-card.vue'
|
||||
import PlaylistInfo from '../../components/playlist-info/playlist-info.vue'
|
||||
import FtListVideoLazy from '../../components/ft-list-video-lazy/ft-list-video-lazy.vue'
|
||||
import FtListVideoNumbered from '../../components/ft-list-video-numbered/ft-list-video-numbered.vue'
|
||||
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
|
||||
import FtButton from '../../components/ft-button/ft-button.vue'
|
||||
import {
|
||||
|
@ -21,12 +21,12 @@ export default defineComponent({
|
|||
'ft-loader': FtLoader,
|
||||
'ft-card': FtCard,
|
||||
'playlist-info': PlaylistInfo,
|
||||
'ft-list-video-lazy': FtListVideoLazy,
|
||||
'ft-list-video-numbered': FtListVideoNumbered,
|
||||
'ft-flex-box': FtFlexBox,
|
||||
'ft-button': FtButton
|
||||
},
|
||||
beforeRouteLeave(to, from, next) {
|
||||
if (!this.isLoading && to.path.startsWith('/watch') && to.query.playlistId === this.playlistId) {
|
||||
if (!this.isLoading && !this.isUserPlaylistRequested && to.path.startsWith('/watch') && to.query.playlistId === this.playlistId) {
|
||||
this.setCachedPlaylist({
|
||||
id: this.playlistId,
|
||||
title: this.playlistTitle,
|
||||
|
@ -54,11 +54,15 @@ export default defineComponent({
|
|||
channelId: '',
|
||||
infoSource: 'local',
|
||||
playlistItems: [],
|
||||
userPlaylistVisibleLimit: 100,
|
||||
continuationData: null,
|
||||
isLoadingMore: false,
|
||||
getPlaylistInfoDebounce: function() {},
|
||||
playlistInEditMode: false,
|
||||
|
||||
playlistInVideoSearchMode: false,
|
||||
videoSearchQuery: '',
|
||||
|
||||
promptOpen: false,
|
||||
}
|
||||
},
|
||||
|
@ -102,7 +106,11 @@ export default defineComponent({
|
|||
},
|
||||
|
||||
moreVideoDataAvailable() {
|
||||
return this.continuationData !== null
|
||||
if (this.isUserPlaylistRequested) {
|
||||
return this.userPlaylistVisibleLimit < this.sometimesFilteredUserPlaylistItems.length
|
||||
} else {
|
||||
return this.continuationData !== null
|
||||
}
|
||||
},
|
||||
|
||||
isUserPlaylistRequested: function () {
|
||||
|
@ -117,6 +125,30 @@ export default defineComponent({
|
|||
|
||||
return this.selectedUserPlaylist?._id !== this.quickBookmarkPlaylistId
|
||||
},
|
||||
|
||||
sometimesFilteredUserPlaylistItems() {
|
||||
if (!this.isUserPlaylistRequested) { return this.playlistItems }
|
||||
if (this.processedVideoSearchQuery === '') { return this.playlistItems }
|
||||
|
||||
return this.playlistItems.filter((v) => {
|
||||
return v.title.toLowerCase().includes(this.processedVideoSearchQuery)
|
||||
})
|
||||
},
|
||||
visiblePlaylistItems: function () {
|
||||
if (!this.isUserPlaylistRequested) {
|
||||
// No filtering for non user playlists yet
|
||||
return this.playlistItems
|
||||
}
|
||||
|
||||
if (this.userPlaylistVisibleLimit < this.sometimesFilteredUserPlaylistItems.length) {
|
||||
return this.sometimesFilteredUserPlaylistItems.slice(0, this.userPlaylistVisibleLimit)
|
||||
} else {
|
||||
return this.sometimesFilteredUserPlaylistItems
|
||||
}
|
||||
},
|
||||
processedVideoSearchQuery() {
|
||||
return this.videoSearchQuery.trim().toLowerCase()
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
$route () {
|
||||
|
@ -147,8 +179,10 @@ export default defineComponent({
|
|||
this.getPlaylistInfoDebounce()
|
||||
},
|
||||
},
|
||||
mounted: function () {
|
||||
created: function () {
|
||||
this.getPlaylistInfoDebounce = debounce(this.getPlaylistInfo, 100)
|
||||
},
|
||||
mounted: function () {
|
||||
this.getPlaylistInfoDebounce()
|
||||
},
|
||||
methods: {
|
||||
|
@ -250,7 +284,7 @@ export default defineComponent({
|
|||
const dateString = new Date(result.updated * 1000)
|
||||
this.lastUpdated = dateString.toLocaleDateString(this.currentLocale, { year: 'numeric', month: 'short', day: 'numeric' })
|
||||
|
||||
this.playlistItems = this.playlistItems.concat(result.videos)
|
||||
this.playlistItems = result.videos
|
||||
|
||||
this.isLoading = false
|
||||
}).catch((err) => {
|
||||
|
@ -298,6 +332,20 @@ export default defineComponent({
|
|||
case 'local':
|
||||
this.getNextPageLocal()
|
||||
break
|
||||
case 'user':
|
||||
// Stop users from spamming the load more button, by replacing it with a loading symbol until the newly added items are renderered
|
||||
this.isLoadingMore = true
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.userPlaylistVisibleLimit + 100 < this.videoCount) {
|
||||
this.userPlaylistVisibleLimit += 100
|
||||
} else {
|
||||
this.userPlaylistVisibleLimit = this.videoCount
|
||||
}
|
||||
|
||||
this.isLoadingMore = false
|
||||
})
|
||||
break
|
||||
case 'invidious':
|
||||
console.error('Playlist pagination is not currently supported when the Invidious backend is selected.')
|
||||
break
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
box-sizing: border-box;
|
||||
block-size: calc(100vh - 132px);
|
||||
margin-inline-end: 1em;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
position: sticky;
|
||||
inset-block-start: 96px;
|
||||
|
@ -62,11 +61,6 @@
|
|||
transform: translate(calc(10% * var(--horizontal-directionality-coefficient)));
|
||||
}
|
||||
|
||||
.videoIndex {
|
||||
color: var(--tertiary-text-color);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loadNextPageWrapper {
|
||||
/* about the same height as the button */
|
||||
max-block-size: 7vh;
|
||||
|
|
|
@ -28,6 +28,9 @@
|
|||
}"
|
||||
@enter-edit-mode="playlistInEditMode = true"
|
||||
@exit-edit-mode="playlistInEditMode = false"
|
||||
@search-video-mode-on="playlistInVideoSearchMode = true"
|
||||
@search-video-mode-off="playlistInVideoSearchMode = false"
|
||||
@search-video-query-change="(v) => videoSearchQuery = v"
|
||||
@prompt-open="promptOpen = true"
|
||||
@prompt-close="promptOpen = false"
|
||||
/>
|
||||
|
@ -36,60 +39,63 @@
|
|||
v-if="!isLoading"
|
||||
class="playlistItems"
|
||||
>
|
||||
<span
|
||||
<template
|
||||
v-if="playlistItems.length > 0"
|
||||
>
|
||||
<transition-group
|
||||
name="playlistItem"
|
||||
tag="span"
|
||||
<template
|
||||
v-if="visiblePlaylistItems.length > 0"
|
||||
>
|
||||
<div
|
||||
v-for="(item, index) in playlistItems"
|
||||
:key="`${item.videoId}-${item.playlistItemId || index}`"
|
||||
class="playlistItem"
|
||||
<transition-group
|
||||
name="playlistItem"
|
||||
tag="span"
|
||||
>
|
||||
<p
|
||||
class="videoIndex"
|
||||
>
|
||||
{{ index + 1 }}
|
||||
</p>
|
||||
<ft-list-video-lazy
|
||||
<ft-list-video-numbered
|
||||
v-for="(item, index) in visiblePlaylistItems"
|
||||
:key="`${item.videoId}-${item.playlistItemId || index}`"
|
||||
class="playlistItem"
|
||||
:data="item"
|
||||
:playlist-id="playlistId"
|
||||
:playlist-type="infoSource"
|
||||
:playlist-index="index"
|
||||
:playlist-index="playlistInVideoSearchMode ? playlistItems.findIndex(i => i === item) : index"
|
||||
:playlist-item-id="item.playlistItemId"
|
||||
appearance="result"
|
||||
force-list-type="list"
|
||||
:always-show-add-to-playlist-button="true"
|
||||
:quick-bookmark-button-enabled="quickBookmarkButtonEnabled"
|
||||
:can-move-video-up="index > 0"
|
||||
:can-move-video-down="index < playlistItems.length - 1"
|
||||
:can-move-video-up="index > 0 && !playlistInVideoSearchMode"
|
||||
:can-move-video-down="index < playlistItems.length - 1 && !playlistInVideoSearchMode"
|
||||
:can-remove-from-playlist="true"
|
||||
:hide-forbidden-titles="false"
|
||||
:video-index="playlistInVideoSearchMode ? playlistItems.findIndex(i => i === item) : index"
|
||||
:initial-visible-state="index < 10"
|
||||
@move-video-up="moveVideoUp(item.videoId, item.playlistItemId)"
|
||||
@move-video-down="moveVideoDown(item.videoId, item.playlistItemId)"
|
||||
@remove-from-playlist="removeVideoFromPlaylist(item.videoId, item.playlistItemId)"
|
||||
/>
|
||||
</transition-group>
|
||||
<ft-flex-box
|
||||
v-if="moreVideoDataAvailable && !isLoadingMore"
|
||||
>
|
||||
<ft-button
|
||||
:label="$t('Subscriptions.Load More Videos')"
|
||||
background-color="var(--primary-color)"
|
||||
text-color="var(--text-with-main-color)"
|
||||
@click="getNextPage"
|
||||
/>
|
||||
</ft-flex-box>
|
||||
<div
|
||||
v-if="isLoadingMore"
|
||||
class="loadNextPageWrapper"
|
||||
>
|
||||
<ft-loader />
|
||||
</div>
|
||||
</transition-group>
|
||||
</template>
|
||||
<ft-flex-box
|
||||
v-if="moreVideoDataAvailable && !isLoadingMore"
|
||||
v-else
|
||||
>
|
||||
<ft-button
|
||||
:label="$t('Subscriptions.Load More Videos')"
|
||||
background-color="var(--primary-color)"
|
||||
text-color="var(--text-with-main-color)"
|
||||
@click="getNextPage"
|
||||
/>
|
||||
<p class="message">
|
||||
{{ $t("User Playlists['Empty Search Message']") }}
|
||||
</p>
|
||||
</ft-flex-box>
|
||||
<div
|
||||
v-if="isLoadingMore"
|
||||
class="loadNextPageWrapper"
|
||||
>
|
||||
<ft-loader />
|
||||
</div>
|
||||
</span>
|
||||
</template>
|
||||
<ft-flex-box
|
||||
v-else
|
||||
>
|
||||
|
|
|
@ -450,7 +450,7 @@ export default defineComponent({
|
|||
timestamp: formatDurationAsTimestamp(start),
|
||||
startSeconds: start,
|
||||
endSeconds: 0,
|
||||
thumbnail: chapter.thumbnail[0].url
|
||||
thumbnail: chapter.thumbnail[0]
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
display: inline-block;
|
||||
max-inline-size: calc(80vh * 1.78);
|
||||
|
||||
@media only screen and (min-width: 901px) {
|
||||
@media only screen and (min-width: 1051px) {
|
||||
inline-size: 300%;
|
||||
}
|
||||
}
|
||||
|
@ -99,7 +99,7 @@
|
|||
.sidebarArea {
|
||||
grid-area: sidebar;
|
||||
|
||||
@media only screen and (min-width: 901px) {
|
||||
@media only screen and (min-width: 1051px) {
|
||||
min-inline-size: 380px;
|
||||
}
|
||||
|
||||
|
@ -135,7 +135,7 @@
|
|||
margin-block: 0 16px;
|
||||
margin-inline: 0;
|
||||
|
||||
@media only screen and (min-width: 901px) {
|
||||
@media only screen and (min-width: 1051px) {
|
||||
margin-block: 0 16px;
|
||||
margin-inline: 8px;
|
||||
}
|
||||
|
@ -145,17 +145,17 @@
|
|||
@include theatre-mode-template;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 901px) {
|
||||
@media only screen and (min-width: 1051px) {
|
||||
&.useTheatreMode {
|
||||
@include theatre-mode-template;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 900px) {
|
||||
@media only screen and (max-width: 1050px) {
|
||||
@include single-column-template;
|
||||
}
|
||||
|
||||
@media only screen and (min-width: 901px) {
|
||||
@media only screen and (min-width: 1051px) {
|
||||
.infoArea {
|
||||
scroll-margin-block-start: 76px;
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@
|
|||
<ft-age-restricted
|
||||
v-if="(!isLoading && !isFamilyFriendly && showFamilyFriendlyOnly)"
|
||||
class="ageRestricted"
|
||||
:content-type-string="'Video'"
|
||||
:is-video="true"
|
||||
/>
|
||||
<div
|
||||
v-if="(isFamilyFriendly || !showFamilyFriendlyOnly)"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'الإنجليزية (الولايات المتحدة)'
|
||||
Locale Name: 'العربية'
|
||||
FreeTube: 'فري تيوب'
|
||||
# Currently on Subscriptions, Playlists, and History
|
||||
'This part of the app is not ready yet. Come back later when progress has been made.': >-
|
||||
|
@ -173,6 +173,7 @@ User Playlists:
|
|||
Reverted to use {oldPlaylistName} for quick bookmark: تمت العودة لاستخدام {oldPlaylistName}
|
||||
للإشارة المرجعية السريعة
|
||||
Quick bookmark disabled: تم تعطيل الإشارة المرجعية السريعة
|
||||
Search for Videos: البحث عن مقاطع الفيديو
|
||||
AddVideoPrompt:
|
||||
Select a playlist to add your N videos to: حدد قائمة تشغيل لإضافة الفيديو الخاص
|
||||
بك إلى | حدد قائمة تشغيل لإضافة مقاطع الفيديو {videoCount} إليها
|
||||
|
@ -186,6 +187,7 @@ User Playlists:
|
|||
Save: حفظ
|
||||
Search in Playlists: البحث في قوائم التشغيل
|
||||
N playlists selected: تم تحديد {playlistCount}
|
||||
Added {count} Times: تمت إضافة {count} الوقت | تمت إضافة {count} مرة
|
||||
CreatePlaylistPrompt:
|
||||
Toast:
|
||||
There is already a playlist with this name. Please pick a different name.: توجد
|
||||
|
@ -276,6 +278,7 @@ Settings:
|
|||
Catppuccin Mocha: كاتبوتشين موكا
|
||||
Pastel Pink: الباستيل الوردي
|
||||
Hot Pink: وردي فاقع
|
||||
Nordic: بلدان الشمال الأوروبي
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'لون السِمة الأساسي'
|
||||
Red: 'أحمر'
|
||||
|
@ -517,8 +520,8 @@ Settings:
|
|||
Hide Upcoming Premieres: إخفاء العروض الأولى القادمة
|
||||
Hide Channels: إخفاء مقاطع الفيديو من القنوات
|
||||
Hide Channels Placeholder: معرف القناة
|
||||
Display Titles Without Excessive Capitalisation: عرض العناوين بدون احرف كبيرة
|
||||
بشكل مفرط
|
||||
Display Titles Without Excessive Capitalisation: عرض العناوين بدون استخدام الأحرف
|
||||
الكبيرة وعلامات الترقيم بشكل مفرط
|
||||
Hide Featured Channels: إخفاء القنوات المميزة
|
||||
Hide Channel Playlists: إخفاء قوائم تشغيل القناة
|
||||
Hide Channel Community: إخفاء مجتمع القناة
|
||||
|
@ -930,6 +933,7 @@ Video:
|
|||
Pause on Current Video: توقف مؤقتًا على الفيديو الحالي
|
||||
Unhide Channel: عرض القناة
|
||||
Hide Channel: إخفاء القناة
|
||||
More Options: المزيد من الخيارات
|
||||
Videos:
|
||||
#& Sort By
|
||||
Sort By:
|
||||
|
@ -1006,7 +1010,7 @@ Up Next: 'التالي'
|
|||
Local API Error (Click to copy): 'خطأ API المحلي (انقر للنسخ)'
|
||||
Invidious API Error (Click to copy): 'خطأ Invidious API ( انقر للنسخ)'
|
||||
Falling back to Invidious API: 'التراجع إلى Invidious API'
|
||||
Falling back to the local API: 'التراجع إلى API المحلي'
|
||||
Falling back to Local API: 'التراجع إلى API المحلي'
|
||||
Subscriptions have not yet been implemented: 'لم يتم تنفيذ الاشتراكات بعد'
|
||||
Loop is now disabled: 'تم تعطيل التكرار'
|
||||
Loop is now enabled: 'تم تمكين التكرار'
|
||||
|
@ -1130,11 +1134,6 @@ Starting download: بدء تنزيل "{videoTitle}"
|
|||
Screenshot Success: تم حفظ لقطة الشاشة كا"{filePath}"
|
||||
Screenshot Error: فشل أخذ لقطة للشاشة. {error}
|
||||
New Window: نافذة جديدة
|
||||
Age Restricted:
|
||||
Type:
|
||||
Channel: القناة
|
||||
Video: فيديو
|
||||
This {videoOrPlaylist} is age restricted: 'هذا {videoOrPlaylist} مقيد بالعمر'
|
||||
Channels:
|
||||
Count: تم العثور على قناة (قنوات) {number}.
|
||||
Unsubscribed: 'تمت إزالة {channelName} من اشتراكاتك'
|
||||
|
@ -1171,3 +1170,7 @@ Trimmed input must be at least N characters long: يجب أن يكون طول ا
|
|||
حرفًا واحدًا على الأقل | يجب أن يبلغ طول الإدخال المقتطع {length} من الأحرف على
|
||||
الأقل
|
||||
Tag already exists: العلامة "{tagName}" موجودة بالفعل
|
||||
Age Restricted:
|
||||
This channel is age restricted: هذه القناة مقيدة بالعمر
|
||||
This video is age restricted: هذا الفيديو مقيد بالفئة العمرية
|
||||
Close Banner: إغلاق الشعار
|
||||
|
|
|
@ -813,7 +813,7 @@ Tooltips:
|
|||
Local API Error (Click to copy): ''
|
||||
Invidious API Error (Click to copy): ''
|
||||
Falling back to Invidious API: ''
|
||||
Falling back to the local API: ''
|
||||
Falling back to Local API: ''
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: ''
|
||||
Subscriptions have not yet been implemented: ''
|
||||
Unknown YouTube url type, cannot be opened in app: ''
|
||||
|
@ -833,11 +833,6 @@ Canceled next video autoplay: ''
|
|||
Default Invidious instance has been set to {instance}: ''
|
||||
Default Invidious instance has been cleared: ''
|
||||
'The playlist has ended. Enable loop to continue playing': ''
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: ''
|
||||
Type:
|
||||
Channel: ''
|
||||
Video: ''
|
||||
External link opening has been disabled in the general settings: ''
|
||||
Downloading has completed: ''
|
||||
Starting download: ''
|
||||
|
|
|
@ -1023,7 +1023,7 @@ Up Next: 'Следващ'
|
|||
Local API Error (Click to copy): 'Грешка в локалния интерфейс (щракнете за копиране)'
|
||||
Invidious API Error (Click to copy): 'Грешка в Invidious интерфейса (щракнете за копиране)'
|
||||
Falling back to Invidious API: 'Връщане към Invidious интерфейса'
|
||||
Falling back to the local API: 'Връщане към локалния интерфейс'
|
||||
Falling back to Local API: 'Връщане към локалния интерфейс'
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: 'Видеото
|
||||
не е достъпно поради липсващи формати. Това може да се дължи на ограничен достъп
|
||||
за страната.'
|
||||
|
@ -1148,12 +1148,6 @@ Downloading failed: Имаше проблем при изтеглянето на
|
|||
Screenshot Error: Снимката на екрана е неуспешна. {error}
|
||||
Screenshot Success: Запазена снимка на екрана като "{filePath}"
|
||||
New Window: Нов прозорец
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: Този {videoOrPlaylist} е с възрастово
|
||||
ограничение
|
||||
Type:
|
||||
Channel: Канал
|
||||
Video: Видео
|
||||
Channels:
|
||||
Count: Намерени са {number} канала.
|
||||
Unsubscribe: Отписване
|
||||
|
|
|
@ -135,10 +135,6 @@ External link opening has been disabled in the general settings: সাধার
|
|||
বহিঃসংযোগ খোলা নিষ্ক্রিয় রাখা হয়েছে
|
||||
Are you sure you want to open this link?: তুমি কি এই সংযোগটি খোলার ব্যাপারে নিশ্চিত?
|
||||
Preferences: পছন্দসমূহ
|
||||
Age Restricted:
|
||||
Type:
|
||||
Channel: চ্যানেল
|
||||
Video: ভিডিও
|
||||
Most Popular: অতিপরিচিত
|
||||
Channels:
|
||||
Search bar placeholder: চ্যানেল খুঁজুন
|
||||
|
|
|
@ -829,7 +829,7 @@ Tooltips:
|
|||
Local API Error (Click to copy): ''
|
||||
Invidious API Error (Click to copy): ''
|
||||
Falling back to Invidious API: ''
|
||||
Falling back to the local API: ''
|
||||
Falling back to Local API: ''
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: ''
|
||||
Subscriptions have not yet been implemented: ''
|
||||
Unknown YouTube url type, cannot be opened in app: ''
|
||||
|
@ -849,11 +849,6 @@ Canceled next video autoplay: ''
|
|||
Default Invidious instance has been set to {instance}: ''
|
||||
Default Invidious instance has been cleared: ''
|
||||
'The playlist has ended. Enable loop to continue playing': ''
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: ''
|
||||
Type:
|
||||
Channel: 'کەناڵ'
|
||||
Video: 'ڤیدیۆ'
|
||||
External link opening has been disabled in the general settings: ''
|
||||
Downloading has completed: ''
|
||||
Starting download: ''
|
||||
|
|
|
@ -146,6 +146,7 @@ User Playlists:
|
|||
Select a playlist to add your N videos to: Vyberte playlist, do kterého přidat
|
||||
vaše video | Vyberte playlist, do kterého přidat vašich {videoCount} videí
|
||||
N playlists selected: Vybráno {playlistCount}
|
||||
Added {count} Times: Přidáno {count}krát | Přidáno {count}krát
|
||||
SinglePlaylistView:
|
||||
Toast:
|
||||
There were no videos to remove.: Nejsou zde žádná videa k odstranění.
|
||||
|
@ -175,6 +176,7 @@ User Playlists:
|
|||
This playlist is now used for quick bookmark: Tento playlist bude nyní použit
|
||||
pro rychlé uložení
|
||||
Quick bookmark disabled: Rychlé uložení vypnuto
|
||||
Search for Videos: Hledat videa
|
||||
Are you sure you want to delete this playlist? This cannot be undone: Opravdu chcete
|
||||
odstranit tento playlist? Tato akce je nevratná.
|
||||
Sort By:
|
||||
|
@ -234,7 +236,7 @@ Settings:
|
|||
General Settings:
|
||||
General Settings: 'Obecné nastavení'
|
||||
Check for Updates: 'Kontrolovat aktualizace'
|
||||
Check for Latest Blog Posts: 'Kontrolovat nejnovější příspěvky blogů'
|
||||
Check for Latest Blog Posts: 'Kontrolovat nejnovější příspěvky na blogu'
|
||||
Fallback to Non-Preferred Backend on Failure: 'Při chybě přepnout na nepreferovaný
|
||||
backend'
|
||||
Enable Search Suggestions: 'Zapnout návrhy vyhledávání'
|
||||
|
@ -292,6 +294,7 @@ Settings:
|
|||
Catppuccin Mocha: Catppuccin Mocha
|
||||
Hot Pink: Horká růžová
|
||||
Pastel Pink: Pastelově růžová
|
||||
Nordic: Nordic
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Hlavní barva motivu'
|
||||
Red: 'Červená'
|
||||
|
@ -443,7 +446,7 @@ Settings:
|
|||
Hide Channels: Skrýt videa z kanálů
|
||||
Hide Channels Placeholder: ID kanálu
|
||||
Display Titles Without Excessive Capitalisation: Zobrazit názvy bez nadměrného
|
||||
použití velkých písmen
|
||||
použití velkých písmen a interpunkce
|
||||
Hide Featured Channels: Skrýt doporučené kanály
|
||||
Hide Channel Playlists: Skrýt playlisty kanálu
|
||||
Hide Channel Community: Skrýt komunitu kanálu
|
||||
|
@ -925,6 +928,7 @@ Video:
|
|||
Pause on Current Video: Pozastavit na současném videu
|
||||
Unhide Channel: Zobrazit kanál
|
||||
Hide Channel: Skrýt kanál
|
||||
More Options: Další možnosti
|
||||
Videos:
|
||||
#& Sort By
|
||||
Sort By:
|
||||
|
@ -1024,10 +1028,10 @@ Tooltips:
|
|||
otevřít ve FreeTube.\nVe výchozím nastavení otevře FreeTube odkaz ve vašem výchozím
|
||||
prohlížeči.\n"
|
||||
Player Settings:
|
||||
Force Local Backend for Legacy Formats: 'Funguje pouze v případě, že je výchozím
|
||||
nastavením API Invidious. Je-li povoleno, spustí se místní API a použije starší
|
||||
Force Local Backend for Legacy Formats: 'Funguje pouze v případě, že je jako výchozí
|
||||
nastaveno API Invidious. Je-li povoleno, spustí se místní API a použije starší
|
||||
formáty místo těch, které vrátí Invidious. Může pomoci, pokud videa z Invidious
|
||||
nemohou být přehrána kvůli regionálnímu omezení.'
|
||||
nemohou být přehrána z důvodu regionálních omezení.'
|
||||
Proxy Videos Through Invidious: 'Připojí se k Invidious, aby poskytoval videa
|
||||
namísto přímého připojení k YouTube. Toto přepíše předvolby API.'
|
||||
Default Video Format: 'Nastavte formáty použité při přehrávání videa. Formáty
|
||||
|
@ -1089,7 +1093,7 @@ Tooltips:
|
|||
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'
|
||||
Falling back to the local API: 'Přepínám na lokální API'
|
||||
Falling back to Local API: 'Přepínám na lokální API'
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: 'Toto
|
||||
video není k dispozici z důvodu chybějících formátů. K tomu může dojít z důvodu
|
||||
nedostupnosti země.'
|
||||
|
@ -1128,11 +1132,6 @@ Downloading has completed: Bylo dokončeno stahování "{videoTitle}"
|
|||
Downloading failed: Došlo k problému při stahování "{videoTitle}"
|
||||
Starting download: Zahájení stahování "{videoTitle}"
|
||||
New Window: Nové okno
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: Toto {videoOrPlaylist} je omezeno věkem
|
||||
Type:
|
||||
Channel: kanál
|
||||
Video: Video
|
||||
Channels:
|
||||
Channels: Kanály
|
||||
Title: Seznam kanálů
|
||||
|
@ -1169,3 +1168,7 @@ Channel Unhidden: Kanál {channel} odebrán z filtrů kanálů
|
|||
Trimmed input must be at least N characters long: Oříznutý vstup musí být dlouhý alespoň
|
||||
1 znak | Oříznutý vstup musí být dlouhý alespoň {length} znaků
|
||||
Tag already exists: Štítek „{tagName}“ již existuje
|
||||
Close Banner: Zavřít panel
|
||||
Age Restricted:
|
||||
This channel is age restricted: Tento kanál je omezen věkem
|
||||
This video is age restricted: Toto video je omezeno věkem
|
||||
|
|
|
@ -831,7 +831,7 @@ Tooltips:
|
|||
Local API Error (Click to copy): ''
|
||||
Invidious API Error (Click to copy): ''
|
||||
Falling back to Invidious API: ''
|
||||
Falling back to the local API: ''
|
||||
Falling back to Local API: ''
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: ''
|
||||
Subscriptions have not yet been implemented: ''
|
||||
Unknown YouTube url type, cannot be opened in app: ''
|
||||
|
@ -851,11 +851,6 @@ Canceled next video autoplay: ''
|
|||
Default Invidious instance has been set to {instance}: ''
|
||||
Default Invidious instance has been cleared: ''
|
||||
'The playlist has ended. Enable loop to continue playing': ''
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: ''
|
||||
Type:
|
||||
Channel: 'Sianel'
|
||||
Video: 'Fideo'
|
||||
External link opening has been disabled in the general settings: ''
|
||||
Downloading has completed: ''
|
||||
Starting download: ''
|
||||
|
|
|
@ -769,7 +769,7 @@ Up Next: 'Næste'
|
|||
Local API Error (Click to copy): 'Lokal API-Fejl (Klik for at kopiere)'
|
||||
Invidious API Error (Click to copy): 'Invidious-API-Fejl (Klik for at kopiere)'
|
||||
Falling back to Invidious API: 'Falder tilbage til Invidious-API'
|
||||
Falling back to the local API: 'Falder tilbage til den lokale API'
|
||||
Falling back to Local API: 'Falder tilbage til den lokale API'
|
||||
Subscriptions have not yet been implemented: 'Abonnementer er endnu ikke blevet implementerede'
|
||||
Loop is now disabled: 'Gentagelse er nu deaktiveret'
|
||||
Loop is now enabled: 'Gentagelse er nu aktiveret'
|
||||
|
@ -858,11 +858,6 @@ Default Invidious instance has been cleared: Standard Invidious-instans er bleve
|
|||
Are you sure you want to open this link?: Er du sikker på at du vil åbne dette link?
|
||||
Search Bar:
|
||||
Clear Input: Ryd Input
|
||||
Age Restricted:
|
||||
Type:
|
||||
Video: Video
|
||||
Channel: Kanal
|
||||
This {videoOrPlaylist} is age restricted: Denne {videoOrPlaylist} er aldersbegrænset
|
||||
Downloading failed: Der var et problem med at downloade "{videoTitle}"
|
||||
Unknown YouTube url type, cannot be opened in app: Ukendt YouTube URL-type, kan ikke
|
||||
åbnes i appen
|
||||
|
|
|
@ -44,7 +44,7 @@ Global:
|
|||
View Count: 1 Aufruf | {count} Aufrufe
|
||||
Watching Count: 1 Zuschauer | {count} Zuschauer
|
||||
Input Tags:
|
||||
Length Requirement: Deutsch
|
||||
Length Requirement: Der Tag muss mindestens {number} Zeichen lang sein
|
||||
Search / Go to URL: Suche / Gehe zur URL
|
||||
# In Filter Button
|
||||
Search Filters:
|
||||
|
@ -294,6 +294,7 @@ Settings:
|
|||
Catppuccin Mocha: Catppuccin Mocha
|
||||
Hot Pink: Pink
|
||||
Pastel Pink: Pastellrosa
|
||||
Nordic: Nordic
|
||||
Main Color Theme:
|
||||
Main Color Theme: Hauptfarbe des Farbschemas
|
||||
Red: Rot
|
||||
|
@ -568,6 +569,10 @@ Settings:
|
|||
Hide Channels Already Exists: Kanal-ID bereits vorhanden
|
||||
Hide Channels API Error: Fehler beim Abrufen von Benutzern mit der bereitgestellten
|
||||
ID. Bitte überprüfen Sie erneut, ob der Ausweis korrekt ist.
|
||||
Hide Videos and Playlists Containing Text: Videos und Playlisten verstecken, die
|
||||
Text enthalten
|
||||
Hide Videos and Playlists Containing Text Placeholder: Wort, Teil eines Wortes
|
||||
oder Satz
|
||||
The app needs to restart for changes to take effect. Restart and apply change?: Die
|
||||
App muss neu gestartet werden, damit die Änderungen wirksam werden. Neu starten
|
||||
und Änderung übernehmen?
|
||||
|
@ -764,6 +769,7 @@ Channel:
|
|||
votes: '{votes} Stimmen'
|
||||
Reveal Answers: Antworten aufzeigen
|
||||
Hide Answers: Antworten verbergen
|
||||
Video hidden by FreeTube: Video versteckt von FreeTube
|
||||
Live:
|
||||
Live: Live
|
||||
This channel does not currently have any live streams: Dieser Kanal hat derzeit
|
||||
|
@ -1008,7 +1014,7 @@ Up Next: Nächster Titel
|
|||
Local API Error (Click to copy): Lokaler API-Fehler (Zum Kopieren anklicken)
|
||||
Invidious API Error (Click to copy): Invidious-API-Fehler (Zum Kopieren anklicken)
|
||||
Falling back to Invidious API: Rückgriff auf Invidious-API
|
||||
Falling back to the local API: Rückgriff auf lokale API
|
||||
Falling back to Local API: Rückgriff auf lokale API
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: Dieses
|
||||
Video ist aufgrund fehlender Formate nicht verfügbar. Zugriffsbeschränkungen im
|
||||
Land kann dafür der Grund sein.
|
||||
|
@ -1163,6 +1169,11 @@ Tooltips:
|
|||
Hide Subscriptions Live: Diese Einstellung wird durch die App-weite Einstellung
|
||||
„{appWideSetting}“ im Abschnitt „{subsection}“ der „{settingsSection}“ außer
|
||||
Kraft gesetzt
|
||||
Hide Videos and Playlists Containing Text: Gebe ein Wort, einen Teil eines Wortes
|
||||
oder einen Satz ein (Groß-/Kleinschreibung wird ignoriert) um alle Videos und
|
||||
Playlisten, welche es in ihren Originaltiteln enthalten, auf ganz FreeTube auszublenden.
|
||||
Dein Verlauf, deine eigenen Playlisten und Videos innerhalb deiner Playlisten
|
||||
sind davon nicht betroffen.
|
||||
SponsorBlock Settings:
|
||||
UseDeArrowTitles: Videotitel durch von Benutzern eingereichte Titel von DeArrow
|
||||
ersetzen.
|
||||
|
@ -1193,11 +1204,6 @@ Downloading failed: Beim Herunterladen von „{videoTitle}“ gab es ein Problem
|
|||
Screenshot Success: Bildschirmfoto als „{filePath}“ gespeichert
|
||||
Screenshot Error: Bildschirmfoto fehlgeschlagen. {error}
|
||||
New Window: Neues Fenster
|
||||
Age Restricted:
|
||||
Type:
|
||||
Video: Video
|
||||
Channel: Kanal
|
||||
This {videoOrPlaylist} is age restricted: Dieses {videoOrPlaylist} ist altersbeschränkt
|
||||
Channels:
|
||||
Channels: Kanäle
|
||||
Title: Kanalliste
|
||||
|
@ -1231,3 +1237,7 @@ Playlist will not pause when current video is finished: Wiedergabeliste wird nic
|
|||
Channel Hidden: '{channel} wurde zum Kanalfilter hinzugefügt'
|
||||
Go to page: Gehe zu {page}
|
||||
Channel Unhidden: '{channel} wurde aus dem Kanalfilter entfernt'
|
||||
Trimmed input must be at least N characters long: Gekürzte Eingaben müssen mindestens
|
||||
1 Zeichen lang sein | Gekürzte Eingaben müssen mindestens {length} Zeichen lang
|
||||
sein
|
||||
Tag already exists: Die Markierung „{tagName}“ existiert bereits
|
||||
|
|
|
@ -945,7 +945,7 @@ Local API Error (Click to copy): 'Τοπικό σφάλμα Διεπαφής π
|
|||
Invidious API Error (Click to copy): 'Σφάλμα Διεπαφής προγραμματισμού εφαρμογής Invidious(*API)
|
||||
(Κάντε κλικ για αντιγραφή)'
|
||||
Falling back to Invidious API: 'Επιστροφή στο Invidious API'
|
||||
Falling back to the local API: 'Επιστροφή στη τοπική Διεπαφή προγραμματισμού εφαρμογής
|
||||
Falling back to Local API: 'Επιστροφή στη τοπική Διεπαφή προγραμματισμού εφαρμογής
|
||||
(API)'
|
||||
Subscriptions have not yet been implemented: 'Οι συνδρομές δεν έχουν ακόμη υλοποιηθεί'
|
||||
Loop is now disabled: 'Η επανάληψη είναι πλέον απενεργοποιημένη'
|
||||
|
@ -1097,12 +1097,6 @@ Chapters:
|
|||
Chapters: Κεφάλαια
|
||||
'Chapters list visible, current chapter: {chapterName}': 'Ορατή λίστα κεφαλαίων,
|
||||
τρέχον κεφάλαιο: {chapterName}'
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: Αυτό το {videoOrPlaylist} έχει περιορισμό
|
||||
ηλικίας
|
||||
Type:
|
||||
Channel: Κανάλι
|
||||
Video: Βίντεο
|
||||
Ok: Εντάξει
|
||||
Preferences: Προτιμήσεις
|
||||
Screenshot Success: Αποθηκευμένο στιγμιότυπο οθόνης ως "{filePath}"
|
||||
|
|
|
@ -32,6 +32,7 @@ Back: Back
|
|||
Forward: Forward
|
||||
Open New Window: Open New Window
|
||||
Go to page: Go to {page}
|
||||
Close Banner: Close Banner
|
||||
|
||||
Version {versionNumber} is now available! Click for more details: Version {versionNumber} is now available! Click
|
||||
for more details
|
||||
|
@ -186,6 +187,8 @@ User Playlists:
|
|||
EarliestPlayedFirst: 'Earliest Played'
|
||||
|
||||
SinglePlaylistView:
|
||||
Search for Videos: Search for Videos
|
||||
|
||||
Toast:
|
||||
This video cannot be moved up.: This video cannot be moved up.
|
||||
This video cannot be moved down.: This video cannot be moved down.
|
||||
|
@ -213,6 +216,8 @@ User Playlists:
|
|||
Search in Playlists: Search in Playlists
|
||||
Save: Save
|
||||
|
||||
Added {count} Times: 'Added {count} Time | Added {count} Times'
|
||||
|
||||
Toast:
|
||||
You haven't selected any playlist yet.: You haven't selected any playlist yet.
|
||||
"{videoCount} video(s) added to 1 playlist": "1 video added to 1 playlist | {videoCount} videos added to 1 playlist"
|
||||
|
@ -296,6 +301,7 @@ Settings:
|
|||
Catppuccin Mocha: Catppuccin Mocha
|
||||
Pastel Pink: Pastel Pink
|
||||
Hot Pink: Hot Pink
|
||||
Nordic: Nordic
|
||||
Main Color Theme:
|
||||
Main Color Theme: Main Color Theme
|
||||
Red: Red
|
||||
|
@ -450,7 +456,7 @@ Settings:
|
|||
Hide Video Description: Hide Video Description
|
||||
Hide Comments: Hide Comments
|
||||
Hide Profile Pictures in Comments: Hide Profile Pictures in Comments
|
||||
Display Titles Without Excessive Capitalisation: Display Titles Without Excessive Capitalisation
|
||||
Display Titles Without Excessive Capitalisation: Display Titles Without Excessive Capitalisation And Punctuation
|
||||
Hide Live Streams: Hide Live Streams
|
||||
Hide Upcoming Premieres: Hide Upcoming Premieres
|
||||
Hide Sharing Actions: Hide Sharing Actions
|
||||
|
@ -721,6 +727,7 @@ Channel:
|
|||
Hide Answers: Hide Answers
|
||||
Video hidden by FreeTube: Video hidden by FreeTube
|
||||
Video:
|
||||
More Options: More Options
|
||||
Mark As Watched: Mark As Watched
|
||||
Remove From History: Remove From History
|
||||
Video has been marked as watched: Video has been marked as watched
|
||||
|
@ -954,7 +961,7 @@ Tooltips:
|
|||
By default FreeTube will open the clicked link in your default browser.
|
||||
Player Settings:
|
||||
Force Local Backend for Legacy Formats: Only works when the Invidious API is your
|
||||
default. When enabled, the local API will run and use the legacy formats returned
|
||||
default. When enabled, the Local API will run and use the legacy formats returned
|
||||
by that instead of the ones returned by Invidious. Helps when the videos returned
|
||||
by Invidious don't play due to country restrictions.
|
||||
Proxy Videos Through Invidious: Will connect to Invidious to serve videos instead
|
||||
|
@ -1008,7 +1015,7 @@ Tooltips:
|
|||
Local API Error (Click to copy): Local API Error (Click to copy)
|
||||
Invidious API Error (Click to copy): Invidious API Error (Click to copy)
|
||||
Falling back to Invidious API: Falling back to Invidious API
|
||||
Falling back to the local API: Falling back to the local API
|
||||
Falling back to Local API: Falling back to Local API
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: This
|
||||
video is unavailable because of missing formats. This can happen due to country
|
||||
unavailability.
|
||||
|
@ -1032,10 +1039,8 @@ Default Invidious instance has been cleared: Default Invidious instance has been
|
|||
'The playlist has ended. Enable loop to continue playing': 'The playlist has ended. Enable
|
||||
loop to continue playing'
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: This {videoOrPlaylist} is age restricted
|
||||
Type:
|
||||
Channel: Channel
|
||||
Video: Video
|
||||
This channel is age restricted: This channel is age restricted
|
||||
This video is age restricted: This video is age restricted
|
||||
External link opening has been disabled in the general settings: 'External link opening has been disabled in the general settings'
|
||||
Downloading has completed: '"{videoTitle}" has finished downloading'
|
||||
Starting download: 'Starting download of "{videoTitle}"'
|
||||
|
|
|
@ -43,6 +43,8 @@ Global:
|
|||
View Count: 1 view | {count} views
|
||||
Watching Count: 1 watching | {count} watching
|
||||
Channel Count: 1 channel | {count} channels
|
||||
Input Tags:
|
||||
Length Requirement: Tag must be at least {number} characters long
|
||||
Version {versionNumber} is now available! Click for more details: 'Version {versionNumber}
|
||||
is now available! Click for more details'
|
||||
Download From Site: 'Download from site'
|
||||
|
@ -119,14 +121,14 @@ Playlists: 'Playlists'
|
|||
User Playlists:
|
||||
Your Playlists: 'Your playlists'
|
||||
Playlist Message: This page is not reflective of fully working playlists. It only
|
||||
lists videos that you have saved or favourited. When the work has finished, all
|
||||
videos currently here will be migrated to a ‘Favourites’ playlist.
|
||||
lists videos that you have saved or made a Favourite. When the work has finished,
|
||||
all videos currently here will be migrated to a ‘Favourites’ playlist.
|
||||
Your saved videos are empty. Click on the save button on the corner of a video to have it listed here: Your
|
||||
saved videos are empty. Click on the save button on the corner of a video to have
|
||||
it listed here
|
||||
Search bar placeholder: Search in playlist
|
||||
Empty Search Message: There are no videos in this playlist that match your search
|
||||
Create New Playlist: Create new playlist
|
||||
Create New Playlist: Create new Playlist
|
||||
Add to Playlist: Add to playlist
|
||||
This playlist currently has no videos.: This playlist currently has no videos.
|
||||
Move Video Down: Move video down
|
||||
|
@ -167,6 +169,14 @@ User Playlists:
|
|||
There was an issue with updating this playlist.: There was an issue with updating
|
||||
this playlist.
|
||||
There were no videos to remove.: There were no videos to remove.
|
||||
Reverted to use {oldPlaylistName} for quick bookmark: Reverted to use {oldPlaylistName}
|
||||
for quick bookmark
|
||||
This playlist is now used for quick bookmark: This playlist is now used for
|
||||
quick bookmark
|
||||
This playlist is now used for quick bookmark instead of {oldPlaylistName}. Click here to undo: This
|
||||
playlist is now used for quick bookmark instead of {oldPlaylistName}. Click
|
||||
here to undo
|
||||
Quick bookmark disabled: Quick bookmark disabled
|
||||
AddVideoPrompt:
|
||||
N playlists selected: '{playlistCount} selected'
|
||||
Search in Playlists: Search in playlists
|
||||
|
@ -175,11 +185,21 @@ User Playlists:
|
|||
"{videoCount} video(s) added to 1 playlist": 1 video added to 1 playlist | {videoCount}
|
||||
videos added to 1 playlist
|
||||
You haven't selected any playlist yet.: You haven't selected any playlist yet.
|
||||
"{videoCount} video(s) added to {playlistCount} playlists": 1 video added to
|
||||
{playlistCount} playlists | {videoCount} videos added to {playlistCount}
|
||||
playlists
|
||||
Select a playlist to add your N videos to: Select a playlist to add your video
|
||||
to | Select a playlist to add your {videoCount} videos to
|
||||
CreatePlaylistPrompt:
|
||||
New Playlist Name: New playlist name
|
||||
New Playlist Name: New Playlist name
|
||||
Create: Create
|
||||
Toast:
|
||||
Playlist {playlistName} has been successfully created.: Playlist {playlistName}
|
||||
has been successfully created.
|
||||
There was an issue with creating the playlist.: There was an problem when creating
|
||||
the playlist.
|
||||
There is already a playlist with this name. Please pick a different name.: There
|
||||
is already a Playlist with this name. Please pick a different name.
|
||||
You have no playlists. Click on the create new playlist button to create a new one.: You
|
||||
have no playlists. Click on the create new playlist button to create a new one.
|
||||
Move Video Up: Move video up
|
||||
|
@ -193,6 +213,9 @@ User Playlists:
|
|||
Are you sure you want to delete this playlist? This cannot be undone: Are you sure
|
||||
you want to delete this playlist? This cannot be undone.
|
||||
Add to Favorites: Add to {playlistName}
|
||||
Remove from Favorites: Remove from {playlistName}
|
||||
Enable Quick Bookmark With This Playlist: Enable quick bookmark with this playlist
|
||||
Disable Quick Bookmark: Disable quick bookmark
|
||||
History:
|
||||
# On History Page
|
||||
History: 'History'
|
||||
|
@ -230,38 +253,39 @@ Settings:
|
|||
Hidden: Hidden
|
||||
'Invidious Instance (Default is https://invidious.snopyta.org)': 'Invidious Instance
|
||||
(Default is https://invidious.snopyta.org)'
|
||||
Region for Trending: 'Region for Trending'
|
||||
Region for Trending: 'Region for trending'
|
||||
#! List countries
|
||||
View all Invidious instance information: View all Invidious instance information
|
||||
System Default: System default
|
||||
External Player: External Player
|
||||
External Player Executable: Custom External Player Executable
|
||||
Clear Default Instance: Clear default instance
|
||||
Set Current Instance as Default: Set Current Instance as Default
|
||||
Set Current Instance as Default: Set current instance as default
|
||||
Current instance will be randomized on startup: Current instance will be randomised
|
||||
on startup
|
||||
on Startup
|
||||
No default instance has been set: No default instance has been set
|
||||
The currently set default instance is {instance}: The currently set default instance
|
||||
is {instance}
|
||||
Current Invidious Instance: Current Invidious Instance
|
||||
External Link Handling:
|
||||
No Action: No Action
|
||||
Ask Before Opening Link: Ask Before Opening Link
|
||||
Open Link: Open Link
|
||||
External Link Handling: External Link Handling
|
||||
No Action: No action
|
||||
Ask Before Opening Link: Ask before opening link
|
||||
Open Link: Open link
|
||||
External Link Handling: External link handling
|
||||
Theme Settings:
|
||||
Theme Settings: 'Theme Settings'
|
||||
Theme Settings: 'Theme settings'
|
||||
Match Top Bar with Main Color: 'Match top bar with main colour'
|
||||
Base Theme:
|
||||
Base Theme: 'Base theme'
|
||||
Black: 'Black'
|
||||
Dark: 'Dark'
|
||||
System Default: 'System Default'
|
||||
System Default: 'System default'
|
||||
Light: 'Light'
|
||||
Dracula: 'Dracula'
|
||||
Catppuccin Mocha: Catppuccin Mocha
|
||||
Pastel Pink: Pastel pink
|
||||
Hot Pink: Hot pink
|
||||
Nordic: Nordic
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Main colour theme'
|
||||
Red: 'Red'
|
||||
|
@ -307,7 +331,7 @@ Settings:
|
|||
Disable Smooth Scrolling: Disable smooth scrolling
|
||||
Expand Side Bar by Default: Expand side bar by default
|
||||
Hide Side Bar Labels: Hide side bar labels
|
||||
Hide FreeTube Header Logo: Hide FreeTube Header Logo
|
||||
Hide FreeTube Header Logo: Hide FreeTube header logo
|
||||
Player Settings:
|
||||
Player Settings: 'Player settings'
|
||||
Force Local Backend for Legacy Formats: 'Force local back-end for legacy formats'
|
||||
|
@ -581,6 +605,7 @@ Settings:
|
|||
Remove Password: Remove password
|
||||
Password Settings: Password settings
|
||||
Set Password: Set password
|
||||
Expand All Settings Sections: Expand all settings sections
|
||||
About:
|
||||
#On About page
|
||||
About: About
|
||||
|
@ -951,7 +976,7 @@ Up Next: 'Up Next'
|
|||
Local API Error (Click to copy): 'Local API Error (Click to copy)'
|
||||
Invidious API Error (Click to copy): 'Invidious API Error (Click to copy)'
|
||||
Falling back to Invidious API: 'Falling back to Invidious API'
|
||||
Falling back to the local API: 'Falling back to the local API'
|
||||
Falling back to Local API: 'Falling back to Local API'
|
||||
Subscriptions have not yet been implemented: 'Subscriptions have not yet been implemented'
|
||||
Loop is now disabled: 'Loop is now disabled'
|
||||
Loop is now enabled: 'Loop is now enabled'
|
||||
|
@ -983,7 +1008,7 @@ Tooltips:
|
|||
Proxy Videos Through Invidious: Will connect to Invidious to serve videos instead
|
||||
of making a direct connection to YouTube. Overrides API preference.
|
||||
Force Local Backend for Legacy Formats: Only works when the Invidious API is your
|
||||
default. When enabled, the local API will run and use the legacy formats returned
|
||||
default. When enabled, the Local API will run and use the legacy formats returned
|
||||
by that instead of the ones returned by Invidious. Helps when the videos returned
|
||||
by Invidious don’t play due to country restrictions.
|
||||
Scroll Playback Rate Over Video Player: While the cursor is over the video, press
|
||||
|
@ -1067,17 +1092,15 @@ New Window: New window
|
|||
Channels:
|
||||
Empty: Your channel list is currently empty.
|
||||
Unsubscribe: Unsubscribe
|
||||
Unsubscribed: '{channelName} has been removed from your subscriptions'
|
||||
Unsubscribe Prompt: Are you sure you want to unsubscribe from ‘{channelName}’?
|
||||
Unsubscribed: '{channelName} has been removed from your Subscriptions'
|
||||
Unsubscribe Prompt: Are you sure you want to Unsubscribe from ‘{channelName}’?
|
||||
Title: Channel list
|
||||
Search bar placeholder: Search channels
|
||||
Channels: Channels
|
||||
Count: '{number} channel(s) found.'
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: This {videoOrPlaylist} is age restricted
|
||||
Type:
|
||||
Video: Video
|
||||
Channel: Channel
|
||||
This channel is age restricted: This channel is age restricted
|
||||
This video is age restricted: This video is age restricted
|
||||
Chapters:
|
||||
'Chapters list visible, current chapter: {chapterName}': 'Chapters list visible,
|
||||
current chapter: {chapterName}'
|
||||
|
@ -1099,3 +1122,5 @@ Playlist will pause when current video is finished: Playlist will pause when cur
|
|||
Playlist will not pause when current video is finished: Playlist will not pause when
|
||||
current video is finished
|
||||
Go to page: Go to {page}
|
||||
Tag already exists: ‘{tagName}’ tag already exists
|
||||
Close Banner: Close Banner
|
||||
|
|
|
@ -678,7 +678,7 @@ Local API Error (Click to copy): 'Error de la API local (Presione para copiar)'
|
|||
Invidious API Error (Click to copy): 'Error de la API de Invidious (Presione para
|
||||
copiar)'
|
||||
Falling back to Invidious API: 'Recurriendo a la API de Invidious'
|
||||
Falling back to the local API: 'Recurriendo a la API local'
|
||||
Falling back to Local API: 'Recurriendo a la API local'
|
||||
Subscriptions have not yet been implemented: 'Las suscripciones aún no se han implementado'
|
||||
Loop is now disabled: 'El bucle esta desactivado'
|
||||
Loop is now enabled: 'El bucle esta activado'
|
||||
|
|
|
@ -142,6 +142,7 @@ User Playlists:
|
|||
a la que añadir su vídeo | Seleccione una lista de reproducción a la que añadir
|
||||
sus {videoCount} vídeos
|
||||
N playlists selected: '{playlistCount} seleccionada'
|
||||
Added {count} Times: Añadido {count} vez | Añadido {count} veces
|
||||
SinglePlaylistView:
|
||||
Toast:
|
||||
There were no videos to remove.: No había vídeos que eliminar.
|
||||
|
@ -287,6 +288,7 @@ Settings:
|
|||
Catppuccin Mocha: Catppuccin Moca
|
||||
Pastel Pink: Rosa pastel
|
||||
Hot Pink: Rosa fuerte
|
||||
Nordic: Nórdico
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Color principal'
|
||||
Red: 'Rojo'
|
||||
|
@ -531,8 +533,8 @@ Settings:
|
|||
Hide Upcoming Premieres: Ocultar los próximos estrenos
|
||||
Hide Channels: Ocultar vídeos de los canales
|
||||
Hide Channels Placeholder: ID del canal
|
||||
Display Titles Without Excessive Capitalisation: Mostrar títulos sin demasiadas
|
||||
mayúsculas
|
||||
Display Titles Without Excessive Capitalisation: Mostrar títulos sin mayúsculas
|
||||
ni signos de puntuación excesivos
|
||||
Hide Featured Channels: Ocultar canales recomendados
|
||||
Hide Channel Playlists: Ocultar las listas de reproducción de los canales
|
||||
Hide Channel Community: Ocultar canales de la comunidad
|
||||
|
@ -954,6 +956,7 @@ Video:
|
|||
Pause on Current Video: Pausa en el vídeo actual
|
||||
Unhide Channel: Mostrar el canal
|
||||
Hide Channel: Ocultar el canal
|
||||
More Options: Más opciones
|
||||
Videos:
|
||||
#& Sort By
|
||||
Sort By:
|
||||
|
@ -1037,7 +1040,7 @@ Local API Error (Click to copy): 'Error de la API local (Clic para copiar el có
|
|||
Invidious API Error (Click to copy): 'Error de la API de Invidious (Clic para copiar
|
||||
el código)'
|
||||
Falling back to Invidious API: 'Recurriendo a la API de Invidious'
|
||||
Falling back to the local API: 'Recurriendo a la API local'
|
||||
Falling back to Local API: 'Recurriendo a la API local'
|
||||
Subscriptions have not yet been implemented: 'Todavía no se han implementado las suscripciones'
|
||||
Loop is now disabled: 'Reproducción en bucle desactivada'
|
||||
Loop is now enabled: 'Reproducción en bucle activada'
|
||||
|
@ -1076,10 +1079,11 @@ Tooltips:
|
|||
Proxy Videos Through Invidious: Se conectará a Invidious para obtener vídeos en
|
||||
lugar de conectar directamente con YouTube. Sobreescribirá la preferencia de
|
||||
API.
|
||||
Force Local Backend for Legacy Formats: Solo funciona cuando la API de Invidious
|
||||
es la predeterminada. la API local se ejecutará y usará los formatos heredados
|
||||
en lugar de Invidious. Esto ayudará cuando Invidious no pueda reproducir un
|
||||
vídeo por culpa de las restricciones regionales.
|
||||
Force Local Backend for Legacy Formats: Sólo funciona cuando la API de Invidious
|
||||
es la predeterminada. Si está activada, la API local se ejecutará y utilizará
|
||||
los formatos heredados devueltos por ella en lugar de los devueltos por Invidious.
|
||||
Es útil cuando los vídeos devueltos por Invidious no se reproducen debido a
|
||||
restricciones nacionales.
|
||||
Scroll Playback Rate Over Video Player: Cuando el cursor esté sobre el vídeo,
|
||||
presiona y mantén la tecla Control (Comando en Mac) y desplaza la rueda del
|
||||
ratón hacia arriba o abajo para cambiar la velocidad de reproducción. Presiona
|
||||
|
@ -1183,12 +1187,6 @@ Channels:
|
|||
Unsubscribe: Cancelar la suscripción
|
||||
Unsubscribed: '{channelName} ha sido eliminado de tus suscripciones'
|
||||
Unsubscribe Prompt: ¿Está seguro/segura de querer desuscribirse de «{channelName}»?
|
||||
Age Restricted:
|
||||
Type:
|
||||
Channel: Canal
|
||||
Video: Vídeo
|
||||
This {videoOrPlaylist} is age restricted: Este {videoOrPlaylist} tiene restricción
|
||||
de edad
|
||||
Clipboard:
|
||||
Copy failed: Error al copiar al portapapeles
|
||||
Cannot access clipboard without a secure connection: No se puede acceder al portapapeles
|
||||
|
@ -1215,3 +1213,7 @@ Channel Unhidden: '{channel} eliminado del filtro de canales'
|
|||
Tag already exists: La etiqueta "{tagName}" ya existe
|
||||
Trimmed input must be at least N characters long: La entrada recortada debe tener
|
||||
al menos 1 carácter | La entrada recortada debe tener al menos {length} caracteres
|
||||
Close Banner: Cerrar el banner
|
||||
Age Restricted:
|
||||
This channel is age restricted: Este canal está restringido por edad
|
||||
This video is age restricted: Este vídeo está restringido por edad
|
||||
|
|
|
@ -161,6 +161,7 @@ User Playlists:
|
|||
Select a playlist to add your N videos to: Vali esitusloend, kuhu soovid oma video
|
||||
lisada | Vali esitusloend, kuhu soovid oma {videoCount} videot lisada
|
||||
N playlists selected: '{playlistCount} valitud'
|
||||
Added {count} Times: Lisatud {count} kord | Lisatud {count} korda
|
||||
SinglePlaylistView:
|
||||
Toast:
|
||||
There were no videos to remove.: Ei leidnud ühtegi videot, mida saaks eemaldada.
|
||||
|
@ -286,6 +287,7 @@ Settings:
|
|||
Catppuccin Mocha: Catppuccin Mocha
|
||||
Pastel Pink: Pastelne roosa
|
||||
Hot Pink: Säravroosa
|
||||
Nordic: Põhjala
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Põhiline värviteema'
|
||||
Red: 'Punane'
|
||||
|
@ -496,7 +498,7 @@ Settings:
|
|||
Hide Channels: Peida kanalites leiduvad videod
|
||||
Hide Channels Placeholder: Kanali tunnus
|
||||
Display Titles Without Excessive Capitalisation: Näita pealkirju ilma liigsete
|
||||
suurtähtedeta
|
||||
suurtähtede ja kirjavahemärkideta
|
||||
Sections:
|
||||
General: Üldist
|
||||
Side Bar: Külgpaan
|
||||
|
@ -883,6 +885,7 @@ Video:
|
|||
Pause on Current Video: Peata hetkel esitatav video
|
||||
Unhide Channel: Näita kanalit
|
||||
Hide Channel: Peida kanal
|
||||
More Options: Veel valikuid
|
||||
Videos:
|
||||
#& Sort By
|
||||
Sort By:
|
||||
|
@ -963,7 +966,7 @@ Up Next: 'Järgmisena'
|
|||
Local API Error (Click to copy): 'Kohaliku API viga (kopeerimiseks klõpsi)'
|
||||
Invidious API Error (Click to copy): 'Invidious''e API viga (kopeerimiseks klõpsi)'
|
||||
Falling back to Invidious API: 'Varuvariandina kasutan Invidious''e API''t'
|
||||
Falling back to the local API: 'Varuvariandina kasutan kohalikku API''t'
|
||||
Falling back to Local API: 'Varuvariandina kasutan kohalikku API''t'
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: 'Kuna
|
||||
vajalikke vorminguid ei leidu, siis see video pole saadaval. Niisugune viga võib
|
||||
juhtuda ka maapiirangute tõttu.'
|
||||
|
@ -1092,11 +1095,6 @@ Channels:
|
|||
Unsubscribe: Loobu tellimusest
|
||||
Unsubscribed: '{channelName} on sinu tellimustest eemaldatud'
|
||||
Unsubscribe Prompt: Kas oled kindel, et soovid „{channelName}“ tellimusest loobuda?
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: See {videoOrPlaylist} on vanusepiiranguga
|
||||
Type:
|
||||
Channel: Kanal
|
||||
Video: Video
|
||||
Screenshot Success: Kuvatõmmis on salvestatud faili „{filePath}“
|
||||
Clipboard:
|
||||
Copy failed: Lõikelauale kopeerimine ei õnnestunud
|
||||
|
@ -1124,3 +1122,7 @@ Channel Unhidden: '{channel} on eemaldatud kanalite filtrist'
|
|||
Tag already exists: Silt „{tagName}“ on juba olemas
|
||||
Trimmed input must be at least N characters long: Kärbitud sisend peab olema vähemalt
|
||||
1 tähemärgi pikkune | Kärbitud sisend peab olema vähemalt {length} tähemärgi pikkune
|
||||
Age Restricted:
|
||||
This channel is age restricted: Kanali vaatamisel on vanusepiirang
|
||||
This video is age restricted: Video vaatamisel on vanusepiirang
|
||||
Close Banner: Sulge rekaampilt
|
||||
|
|
|
@ -34,6 +34,17 @@ Forward: 'Aurrera'
|
|||
Global:
|
||||
Videos: 'Bideoak'
|
||||
|
||||
Counts:
|
||||
Subscriber Count: 1.harpideduna| {count} harpidedun
|
||||
Watching Count: 1. ikuslea | {count} ikusle
|
||||
Channel Count: 1. kanala ! {count} kanal
|
||||
Video Count: 1. bideoa | {count} bideo
|
||||
View Count: 1. ikustaldia | {count} ikustaldi
|
||||
Live: Zuzenekoa
|
||||
Shorts: Laburrak
|
||||
Input Tags:
|
||||
Length Requirement: Etiketak {zenbaki} karaktere izan behar ditu gutxienez
|
||||
Community: Komunitatea
|
||||
Version {versionNumber} is now available! Click for more details: '{versionNumber}
|
||||
bertsioa erabilgarri! Klikatu azalpen gehiagorako'
|
||||
Download From Site: 'Webgunetik jaitsi'
|
||||
|
@ -90,6 +101,14 @@ Subscriptions:
|
|||
Refresh Subscriptions: 'Harpidetzak freskatu'
|
||||
Load More Videos: 'Bideo gehiago kargatu'
|
||||
Error Channels: Akatsak dituzten kateak
|
||||
Disabled Automatic Fetching: Harpidetza-bilaketa automatikoa desgaitu duzu. Freskatu
|
||||
harpidetzak hemen ikusteko.
|
||||
Empty Channels: Harpidetutako kanalek ez dute bideorik.
|
||||
Empty Posts: Harpidetutako kanalek ez dute argitalpenik.
|
||||
Load More Posts: Kargatu mezu gehiago
|
||||
Subscriptions Tabs: Harpidetzen fitxak
|
||||
All Subscription Tabs Hidden: Harpidetza fitxa guztiak ezkutatuta daude. Hemen edukia
|
||||
ikusteko, erakutsi fitxa batzuk "{azpisection}" ataleko "{settingsSection}"-ean.
|
||||
More: 'Gehiago'
|
||||
Trending:
|
||||
Trending: 'Joerak'
|
||||
|
@ -111,6 +130,100 @@ User Playlists:
|
|||
Search bar placeholder: Bilatu Erreprodukzio-zerrendan
|
||||
Empty Search Message: Erreprodukzio-zerrenda honetan ez dago zure bilaketarekin
|
||||
bat datorren bideorik
|
||||
Remove Watched Videos: Kendu ikusitako bideoak
|
||||
You have no playlists. Click on the create new playlist button to create a new one.: Ez
|
||||
duzu erreprodukzio-zerrendarik. Egin klik sortu erreprodukzio-zerrenda berria
|
||||
botoian berri bat sortzeko.
|
||||
This playlist currently has no videos.: Erreprodukzio-zerrenda honek ez du bideorik.
|
||||
Create New Playlist: Sortu erreprodukzio zerrenda berria
|
||||
Add to Playlist: Gehitu erreprodukzio zerrendara
|
||||
Add to Favorites: Gehitu {playlistName}-ra
|
||||
Remove from Favorites: Kendu {playlistName}tik
|
||||
Move Video Up: Mugitu bideoa gora
|
||||
Move Video Down: Mugitu bideoa behera
|
||||
Remove from Playlist: Kendu erreprodukzio zerrendatik
|
||||
Playlist Name: Erreprodukzio zerrendaren izena
|
||||
Playlist Description: Erreprodukzio-zerrendaren deskribapena
|
||||
Save Changes: Gorde aldaketak
|
||||
Cancel: Utzi
|
||||
Edit Playlist Info: Editatu erreprodukzio zerrendaren informazioa
|
||||
Copy Playlist: Kopiatu Erreprodukzio zerrenda
|
||||
CreatePlaylistPrompt:
|
||||
New Playlist Name: Erreprodukzio-zerrendaren izen berria
|
||||
Toast:
|
||||
Playlist {playlistName} has been successfully created.: '{playlistName} erreprodukzio-zerrenda
|
||||
behar bezala sortu da.'
|
||||
There was an issue with creating the playlist.: Arazo bat izan da erreprodukzio-zerrenda
|
||||
sortzearekin.
|
||||
There is already a playlist with this name. Please pick a different name.: Badago
|
||||
dagoeneko izen honekin erreprodukzio-zerrenda bat. Mesedez, aukeratu beste
|
||||
izen bat.
|
||||
Create: Sortu
|
||||
Enable Quick Bookmark With This Playlist: Gaitu laster-marka azkarra erreprodukzio-zerrenda
|
||||
honekin
|
||||
Disable Quick Bookmark: Desgaitu laster-marka azkarra
|
||||
Sort By:
|
||||
NameDescending: Z-A
|
||||
LatestCreatedFirst: Sortu berria
|
||||
EarliestCreatedFirst: Lehenen sortuak
|
||||
LatestPlayedFirst: Duela gutxi erreproduzitua
|
||||
EarliestPlayedFirst: Lehenen erreproduzitua
|
||||
Sort By: Ordenatu honen arabera
|
||||
NameAscending: A-Z
|
||||
EarliestUpdatedFirst: Lehenen eguneratuak
|
||||
LatestUpdatedFirst: Eguneratu berriak
|
||||
Delete Playlist: Ezabatu erreprodukzio zerrenda
|
||||
Are you sure you want to delete this playlist? This cannot be undone: Ziur erreprodukzio-zerrenda
|
||||
ezabatu nahi duzula? Hau ezin da desegin.
|
||||
SinglePlaylistView:
|
||||
Toast:
|
||||
Playlist has been updated.: Erreprodukzio-zerrenda eguneratu da.
|
||||
"{videoCount} video(s) have been removed": bideo 1 kendu da | {videoCount} bideo
|
||||
kendu dira
|
||||
This video cannot be moved up.: Bideo hau ezin da gora eraman.
|
||||
This video cannot be moved down.: Bideoa ezin da behera eraman.
|
||||
Video has been removed: Bideoa kendu da
|
||||
There was a problem with removing this video: Arazo bat izan da bideoa kentzean
|
||||
This playlist is now used for quick bookmark: Erreprodukzio-zerrenda hau laster-markak
|
||||
egiteko erabiltzen da orain
|
||||
Quick bookmark disabled: Laster-marka azkarra desgaituta dago
|
||||
This playlist is now used for quick bookmark instead of {oldPlaylistName}. Click here to undo: Erreprodukzio-zerrenda
|
||||
hau laster-marketarako erabiltzen da orain {oldPlaylistName}-ren ordez. Egin
|
||||
klik hemen desegiteko
|
||||
Reverted to use {oldPlaylistName} for quick bookmark: '{oldPlaylistName} erabili
|
||||
da berriro azkar markatzeko'
|
||||
Some videos in the playlist are not loaded yet. Click here to copy anyway.: Erreprodukzio-zerrendako
|
||||
bideo batzuk oraindik ez dira kargatu. Egin klik hemen kopiatzeko hala ere.
|
||||
Playlist name cannot be empty. Please input a name.: Erreprodukzio zerrendaren
|
||||
izena ezin da hutsik egon. Mesedez, idatzi izena.
|
||||
There was an issue with updating this playlist.: Arazo bat izan da erreprodukzio-zerrenda
|
||||
eguneratzean.
|
||||
There were no videos to remove.: Ez zegoen kentzeko bideorik.
|
||||
This playlist is protected and cannot be removed.: Erreprodukzio-zerrenda hau
|
||||
babestuta dago eta ezin da kendu.
|
||||
Playlist {playlistName} has been deleted.: '{playlistName} erreprodukzio-zerrenda
|
||||
ezabatu da.'
|
||||
This playlist does not exist: Erreprodukzio-zerrenda hau ez da existitzen
|
||||
Search for Videos: Bilatu bideoak
|
||||
AddVideoPrompt:
|
||||
Added {count} Times: Gehitu da {count} Time | {count} aldiz gehitu da
|
||||
Toast:
|
||||
"{videoCount} video(s) added to 1 playlist": Bideo 1 gehitu da erreprodukzio-zerrenda
|
||||
batera | {videoCount} bideo gehitu dira erreprodukzio-zerrenda batera
|
||||
You haven't selected any playlist yet.: Oraindik ez duzu erreprodukzio zerrendarik
|
||||
hautatu.
|
||||
"{videoCount} video(s) added to {playlistCount} playlists": Bideo 1 gehitu da
|
||||
{playlistCount} erreprodukzio zerrendetan | {videoCount} bideo gehitu dira
|
||||
{playlistCount} erreprodukzio-zerrendetan
|
||||
Select a playlist to add your N videos to: Hautatu erreprodukzio-zerrenda zure
|
||||
bideoa -ra gehitzeko | Hautatu erreprodukzio-zerrenda zure {videoCount} bideoak
|
||||
gehitzeko
|
||||
N playlists selected: '{playlistCount} hautatuta'
|
||||
Search in Playlists: Bilatu erreprodukzio-zerrendetan
|
||||
Save: Gorde
|
||||
Are you sure you want to remove all watched videos from this playlist? This cannot be undone: Ziur
|
||||
ikusitako bideo guztiak erreprodukzio-zerrenda honetatik kendu nahi dituzula?
|
||||
Hau ezin da desegin.
|
||||
History:
|
||||
# On History Page
|
||||
History: 'Historikoa'
|
||||
|
@ -147,6 +260,8 @@ Settings:
|
|||
Beginning: 'Hasiera'
|
||||
Middle: 'Erdian'
|
||||
End: 'Bukaera'
|
||||
Hidden: Ezkutatuta
|
||||
Blur: Lausotzea
|
||||
View all Invidious instance information: 'Invidious instantzia guztien informazioa
|
||||
ikusi'
|
||||
Region for Trending: 'Joeren eskualdea'
|
||||
|
@ -179,6 +294,9 @@ Settings:
|
|||
Dracula: Drakula
|
||||
System Default: Sistemak lehenetsia
|
||||
Catppuccin Mocha: Catppuccin Motxa
|
||||
Nordic: nordikoa
|
||||
Pastel Pink: Pastel arrosa
|
||||
Hot Pink: Arrosa beroa
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Oinarrizko koloreen gaia'
|
||||
Red: 'Gorria'
|
||||
|
@ -221,6 +339,7 @@ Settings:
|
|||
Secondary Color Theme: 'Gaiaren bigarren mailako kolorea'
|
||||
#* Main Color Theme
|
||||
Hide Side Bar Labels: Ezkutatu alboko barraren etiketak
|
||||
Hide FreeTube Header Logo: Ezkutatu FreeTube goiburuko logotipoa
|
||||
Player Settings:
|
||||
Player Settings: 'Erreprodukzioaren ezarpenak'
|
||||
Force Local Backend for Legacy Formats: 'Behartu backend lokala Legacy formatuentzat'
|
||||
|
@ -272,6 +391,11 @@ Settings:
|
|||
Display Play Button In Video Player: Bistaratu Erreproduzitzeko botoia bideo erreproduzitzailean
|
||||
Max Video Playback Rate: Gehienezko bideoen erreprodukzio-tasa
|
||||
Video Playback Rate Interval: Bideo Erreprodukzio-tasa tartea
|
||||
Comment Auto Load:
|
||||
Comment Auto Load: Iruzkin karga automatikoa
|
||||
Skip by Scrolling Over Video Player: Saltatu bideo-erreproduzitzailean korrituz
|
||||
Enter Fullscreen on Display Rotate: Sartu pantaila osoko pantaila biratu pantailan
|
||||
Allow DASH AV1 formats: Baimendu DASH AV1 formatuak
|
||||
Privacy Settings:
|
||||
Privacy Settings: 'Pribatutasunari buruzko ezarpenak'
|
||||
Remember History: 'Historikoa oroitu'
|
||||
|
@ -289,11 +413,20 @@ Settings:
|
|||
Are you sure you want to remove all subscriptions and profiles? This cannot be undone.: 'Ziur
|
||||
al zaude zure profil eta harpidetza guztiak ezabatu nahi dituzula? Ezingo duzu
|
||||
atzera egin.'
|
||||
All playlists have been removed: Erreprodukzio-zerrenda guztiak kendu dira
|
||||
Save Watched Videos With Last Viewed Playlist: Gorde ikusitako bideoak ikusitako
|
||||
azken erreprodukzio-zerrendarekin
|
||||
Remove All Playlists: Kendu erreprodukzio-zerrenda guztiak
|
||||
Are you sure you want to remove all your playlists?: Ziur erreprodukzio-zerrenda
|
||||
guztiak kendu nahi dituzula?
|
||||
Subscription Settings:
|
||||
Subscription Settings: 'Harpidetzen ezarpenak'
|
||||
Hide Videos on Watch: 'Ikusten ari zaren bideoa ezkutatu'
|
||||
Fetch Feeds from RSS: 'RSS jarioak eskuratu'
|
||||
Manage Subscriptions: 'Harpidetzak kudeatu'
|
||||
Fetch Automatically: Eskuratu jarioa automatikoki
|
||||
Only Show Latest Video for Each Channel: Erakutsi soilik kanal bakoitzeko azken
|
||||
bideoa
|
||||
Distraction Free Settings:
|
||||
Distraction Free Settings: 'Oharkabetasunak ekiditeko ezarpenak'
|
||||
Hide Video Views: 'Bideoen ikustaldi kopurua ezkutatu'
|
||||
|
@ -310,6 +443,38 @@ Settings:
|
|||
Hide Video Description: Bideoaren deskribapena ezkutatu
|
||||
Hide Comments: Iruzkinak ezkutatu
|
||||
Hide Live Streams: Zuzeneko emanaldiak ezkutatu
|
||||
Sections:
|
||||
Side Bar: Alboko Barra
|
||||
Subscriptions Page: Harpidetzak Orria
|
||||
Channel Page: Kanalaren Orria
|
||||
General: Orokorra
|
||||
Watch Page: Ikusi Orria
|
||||
Hide Profile Pictures in Comments: Ezkutatu profileko argazkiak iruzkinetan
|
||||
Display Titles Without Excessive Capitalisation: Bistaratu izenburuak gehiegizko
|
||||
letra larriz eta puntuaziorik gabe
|
||||
Hide Channels Placeholder: Kanalaren IDa
|
||||
Hide Channel Playlists: Ezkutatu kanaleko erreprodukzio-zerrendak
|
||||
Hide Channel Community: Ezkutatu kanalaren komunitatea
|
||||
Hide Channel Podcasts: Ezkutatu kanaleko podcastak
|
||||
Hide Videos and Playlists Containing Text: Ezkutatu testua duten bideoak eta erreprodukzio-zerrendak
|
||||
Hide Channels: Ezkutatu bideoak kanaletatik
|
||||
Hide Upcoming Premieres: Ezkutatu datozen estreinaldiak
|
||||
Hide Subscriptions Videos: Ezkutatu harpidetza-bideoak
|
||||
Hide Subscriptions Shorts: Ezkutatu bideo laburren harpidetzak
|
||||
Hide Channel Releases: Ezkutatu kanalen kaleratzeak
|
||||
Hide Chapters: Ezkutatu kapituluak
|
||||
Hide Channels Invalid: Emandako kanalaren IDa baliogabea da
|
||||
Hide Channels API Error: Errore bat gertatu da emandako IDa duen erabiltzailea
|
||||
berreskuratzean. Mesedez, egiaztatu berriro IDa zuzena den.
|
||||
Hide Channels Already Exists: Kanalaren IDa badago jada
|
||||
Hide Channel Shorts: Ezkutatu kanalaren bideo laburrak
|
||||
Hide Videos and Playlists Containing Text Placeholder: Hitza, Hitzaren zatia edo
|
||||
esaldia
|
||||
Hide Subscriptions Live: Ezkutatu zuzenekoen harpidetzak
|
||||
Hide Subscriptions Community: Ezkutatu harpidetzen komunitateak
|
||||
Hide Featured Channels: Ezkutatu nabarmendutako kanalak
|
||||
Hide Channels Disabled Message: Kanal batzuk IDa erabiliz blokeatu dira eta ez
|
||||
dira prozesatu. Eginbidea blokeatuta dago ID horiek eguneratzen ari diren bitartean
|
||||
Data Settings:
|
||||
Data Settings: 'Datuen ezarpenak'
|
||||
Select Import Type: 'Hautatu Inportazio mota'
|
||||
|
@ -357,6 +522,18 @@ Settings:
|
|||
esportatu dira
|
||||
Playlist insufficient data: Ez da datu nahikorik "{playlist}" erreprodukzio zerrendarentzat,
|
||||
elementutik ateratzen
|
||||
History File: Historikoaren fitxategia
|
||||
Playlist File: Erreprodukzio-zerrendaren fitxategia
|
||||
Export Playlists For Older FreeTube Versions:
|
||||
Label: Esportatu erreprodukzio zerrendak FreeTube bertsio zaharretarako
|
||||
Tooltip: "Aukera honek erreprodukzio-zerrenda guztietako bideoak \"Gogokoak\"\
|
||||
\ izeneko erreprodukzio-zerrenda batera esportatzen ditu.\nNola esportatu
|
||||
eta inportatu bideoak erreprodukzio-zerrendetan FreeTube-ren bertsio zaharrago
|
||||
baterako:\n 1. Esportatu zure erreprodukzio zerrendak aukera hau gaituta.\n
|
||||
2. Ezabatu lehendik dituzun erreprodukzio-zerrenda guztiak Pribatutasun-ezarpenetan
|
||||
dagoen Kendu zerrenda guztiak aukera erabiliz.\n 3. Abiarazi FreeTube-ren
|
||||
bertsio zaharra eta inportatu esportatutako erreprodukzio-zerrendak.\""
|
||||
Subscription File: Harpidetza Fitxategia
|
||||
Proxy Settings:
|
||||
Proxy Settings: 'Proxy-aren ezarpenak'
|
||||
Enable Tor / Proxy: 'Tor / Proxy ahalbidetu'
|
||||
|
@ -381,6 +558,10 @@ Settings:
|
|||
External Player: Kanpoko erreproduzitzailea
|
||||
Custom External Player Executable: Lehenetsitako kanpo erreproduzitzailea exekutagarria
|
||||
Custom External Player Arguments: Lehenetsitako kanpo erreproduzitzailea argudioak
|
||||
Ignore Default Arguments: Ez ikusi lehenetsitako argudioak
|
||||
Players:
|
||||
None:
|
||||
Name: Bat ere ez
|
||||
Download Settings:
|
||||
Download Settings: Deskargen ezarpenak
|
||||
Ask Download Path: Deskargaren ibilbidea galdetu
|
||||
|
@ -402,11 +583,31 @@ Settings:
|
|||
'SponsorBlock API Url (Default is https://sponsor.ajay.app)': Babesleak blokeatzeko
|
||||
API Url (lehenetsia https://sponsor.ajay.app da)
|
||||
SponsorBlock Settings: Babesleak blokeatzeko ezarpenak
|
||||
UseDeArrowTitles: Erabili DeArrow bideo-izenburuak
|
||||
UseDeArrowThumbnails: Erabili DeArrow irudi txikietarako
|
||||
'DeArrow Thumbnail Generator API Url (Default is https://dearrow-thumb.ajay.app)': DeArrow
|
||||
Thumbnail Generator API URLa (lehenetsia https://dearrow-thumb.ajay.app da)
|
||||
Parental Control Settings:
|
||||
Show Family Friendly Only: Erakutsi familientzat aproposa dena bakarrik
|
||||
Hide Unsubscribe Button: Harpidetza kendu botoia ezkutatu
|
||||
Parental Control Settings: Gurasoen kontrolaren ezarpenak
|
||||
Hide Search Bar: Bilaketa barra ezkutatu
|
||||
Password Dialog:
|
||||
Password: Pasahitza
|
||||
Password Incorrect: Pasahitza okerra
|
||||
Unlock: Desblokeatu
|
||||
Enter Password To Unlock: Sartu pasahitza ezarpenak desblokeatzeko
|
||||
Experimental Settings:
|
||||
Replace HTTP Cache: Ordeztu HTTP cachea
|
||||
Experimental Settings: Ezarpen esperimentalak
|
||||
Warning: Ezarpen hauek esperimentalak dira, aktibatuta dauden bitartean hutsegiteak
|
||||
eragin ditzakete. Oso gomendagarria da babeskopiak egitea. Erabili zure ardurapean!
|
||||
Password Settings:
|
||||
Password Settings: Pasahitz ezarpenak
|
||||
Set Password To Prevent Access: Ezarri pasahitz bat ezarpenetara sarbidea galarazteko
|
||||
Remove Password: Pasahitza ezabatu
|
||||
Set Password: Ezarri pasahitza
|
||||
Expand All Settings Sections: Zabaldu ezarpen guztien atalak
|
||||
About:
|
||||
#On About page
|
||||
About: 'Honi buruz'
|
||||
|
@ -436,6 +637,7 @@ About:
|
|||
these people and projects: 'Hurrengo pertsonak eta proiektuak'
|
||||
Donate: 'Donazioa egin'
|
||||
|
||||
Discussions: Eztabaidak
|
||||
Profile:
|
||||
Profile Select: 'Hautatu profila'
|
||||
Profile Filter: 'Profilaren iragazkiak'
|
||||
|
@ -480,6 +682,12 @@ Profile:
|
|||
batetik ezabatuko.'
|
||||
#On Channel Page
|
||||
Profile Settings: Profilaren ezarpenak
|
||||
Close Profile Dropdown: Itxi goitibeherako profila
|
||||
Open Profile Dropdown: Ireki goitibeherako profila
|
||||
Toggle Profile List: Aldatu profilen zerrenda
|
||||
Edit Profile Name: Editatu profilaren izena
|
||||
Create Profile Name: Sortu profilaren izena
|
||||
Profile Name: Profilaren izena
|
||||
Channel:
|
||||
Subscribe: 'Harpidetu'
|
||||
Unsubscribe: 'Harpidetza kendu'
|
||||
|
@ -510,6 +718,39 @@ Channel:
|
|||
About: 'Honi buruz'
|
||||
Channel Description: 'Kanalaren deskribapena'
|
||||
Featured Channels: 'Nabarmendutako kanalak'
|
||||
Joined: Bat eginda
|
||||
Location: Kokapena
|
||||
Tags:
|
||||
Tags: Etiketak
|
||||
Search for: Bilatu "{tag}"
|
||||
Details: Xehetasunak
|
||||
This channel does not exist: Kanal hau ez da existitzen
|
||||
This channel is age-restricted and currently cannot be viewed in FreeTube.: Kanal
|
||||
hau adin mugatuta dago eta une honetan ezin da ikusi FreeTube-n.
|
||||
Shorts:
|
||||
This channel does not currently have any shorts: Une honetan kanal honek ez du
|
||||
bideo laburrik
|
||||
Releases:
|
||||
This channel does not currently have any releases: Une honetan kanal honek ez
|
||||
du argitalpenik
|
||||
Releases: Argitalpenak
|
||||
Community:
|
||||
Hide Answers: Erantzunak ezkutatu
|
||||
votes: '{votes} bozka'
|
||||
This channel currently does not have any posts: Une honetan kanal honek ez du
|
||||
argitalpenik
|
||||
Reveal Answers: Erantzunak agerian utzi
|
||||
Video hidden by FreeTube: FreeTube-k ezkutatutako bideoa
|
||||
Channel Tabs: Kanalaren fitxak
|
||||
This channel does not allow searching: Kanal honek ez du bilaketarik onartzen
|
||||
Live:
|
||||
Live: Zuzenekoak
|
||||
This channel does not currently have any live streams: Kanal honek ez du zuzeneko
|
||||
erreprodukziorik
|
||||
Podcasts:
|
||||
Podcasts: Podcastak
|
||||
This channel does not currently have any podcasts: Une honetan kanal honek ez
|
||||
du podcastik
|
||||
Video:
|
||||
Mark As Watched: 'Ikusitako gisa jarri'
|
||||
Remove From History: 'Historikotik ezabatu'
|
||||
|
@ -592,6 +833,7 @@ Video:
|
|||
Years: 'Urteak'
|
||||
Ago: 'Duela'
|
||||
Upcoming: 'Estreinaldiak'
|
||||
In less than a minute: Minutu bat baino gutxiagoan
|
||||
Published on: 'Noiz argitaratua'
|
||||
Streamed on: 'Noiz zuzenean emana'
|
||||
Started streaming on: 'Noiz hasi zen zuzenekoa'
|
||||
|
@ -636,6 +878,16 @@ Video:
|
|||
intro: Sarrera
|
||||
Skipped segment: Saltatu egin da segmentua
|
||||
Premieres on: Estreinaldiak
|
||||
Hide Channel: Kanala ezkutatu
|
||||
Unhide Channel: Kanala erakutsi
|
||||
Premieres: Estreinaldiak
|
||||
'Live Chat is unavailable for this stream. It may have been disabled by the uploader.': Zuzeneko
|
||||
txata ez dago erabilgarri zuzeneko honentzat. Baliteke igo duenak desgaitu izana.
|
||||
Show Super Chat Comment: Erakutsi Super txat iruzkina
|
||||
Pause on Current Video: Gelditu uneko bideoa
|
||||
More Options: Aukera gehiago
|
||||
Upcoming: Datozenak
|
||||
Scroll to Bottom: Joan Beherantz
|
||||
Videos:
|
||||
#& Sort By
|
||||
Sort By:
|
||||
|
@ -685,6 +937,7 @@ Share:
|
|||
YouTube Channel URL copied to clipboard: 'Youtube-ko kanalaren URL-a arbelean itsatsi
|
||||
da'
|
||||
|
||||
Share Channel: Kanala partekatu
|
||||
Mini Player: 'Erreproduzitzaile txikia'
|
||||
Comments:
|
||||
Comments: 'Iruzkinak'
|
||||
|
@ -710,6 +963,10 @@ Comments:
|
|||
Show More Replies: Erantzun gehiago erakutsi
|
||||
And others: eta bestelakoak
|
||||
Pinned by: Honengatik ainguratuta
|
||||
Hearted: Bihotzez
|
||||
From {channelName}: '{channelName}-tik'
|
||||
View {replyCount} replies: Ikusi {replyCount} erantzunak
|
||||
Subscribed: Harpidetuta
|
||||
Up Next: 'Hurrengoa'
|
||||
|
||||
#Tooltips
|
||||
|
@ -749,11 +1006,19 @@ Tooltips:
|
|||
eta mantendu (Komando tekla MAC-etan) eta ondoren saguaren ezkerreko botoia
|
||||
sakatu, lehenetsitako erreprodukzio tasara itzultzeko (1x baldin eta ezarpenetan
|
||||
aldaketarik egin ez bada).
|
||||
Skip by Scrolling Over Video Player: Erabili korritze-gurpila bideoa saltatzeko,
|
||||
MPV estiloa.
|
||||
Allow DASH AV1 formats: DASH AV1 formatuak DASH H.264 formatuak baino itxura hobea
|
||||
izan dezake. DASH AV1 formatuek potentzia gehiago behar dute erreproduzitzeko!
|
||||
Ez daude bideo guztietan eskuragarri, kasu horietan erreproduzitzaileak DASH
|
||||
H.264 formatuak erabiliko ditu ordez.
|
||||
Subscription Settings:
|
||||
Fetch Feeds from RSS: 'Posible denean, Freetube-k bere lehenetsitako metodoa erabili
|
||||
beharrean RSS-ak baliatuko ditu zure harpidetzen jariora konektatzeko. RSS arinagoa
|
||||
izateaz gain, IP-en blokeoak saihesten ditu. Aldiz, zuzenekoaren egoeraren edo
|
||||
bideoaren iraupenaren informaziorik ez du ematen, besteak beste'
|
||||
Fetch Automatically: Gaituta dagoenean, FreeTubek automatikoki eskuratuko du zure
|
||||
harpidetza-jarioa leiho berri bat irekitzen denean eta profila aldatzean.
|
||||
Privacy Settings:
|
||||
Remove Video Meta Files: 'Gaituta dagoenean, FreeTube-k automatikoki ezabatzen
|
||||
ditu bideoen erreprodukzioan sortutako metafitxategiak, bistaratze orria ixten
|
||||
|
@ -772,11 +1037,34 @@ Tooltips:
|
|||
Individious-en ezarpenek ez dituzte kanpo erreproduzitzaileak trabatzen.
|
||||
Custom External Player Arguments: Komando-lerroko argumentu pertsonalizatuak,
|
||||
puntu eta komaz bereizita (';'), kanpoko erreproduzitzailera pasatzea nahi duzu.
|
||||
Ignore Default Arguments: Ez bidali argumentu lehenetsirik kanpoko erreproduzitzaileari
|
||||
bideoaren URLaz gain (adibidez, erreprodukzio-tasa, erreprodukzio-zerrendaren
|
||||
URLa, etab.). Argumentu pertsonalizatuak transmitituko dira oraindik.
|
||||
Distraction Free Settings:
|
||||
Hide Channels: Sartu kanalaren ID bat bideo, erreprodukzio-zerrenda eta kanala
|
||||
bera bilaketetan, joeran, ezagunenetan eta gomendagarrienetan ager ez dadin
|
||||
ezkutatzeko. Sartutako kanalaren IDak guztiz bat etorri behar du eta maiuskulak
|
||||
eta minuskulak bereizten ditu.
|
||||
Hide Videos and Playlists Containing Text: Idatzi hitz bat, hitz-zati bat edo
|
||||
esaldi bat (maiuskulak eta minuskulak bereizten ez diren) jatorrizko izenburuak
|
||||
FreeTube osoan duten bideo eta erreprodukzio-zerrenda guztiak ezkutatzeko, historia,
|
||||
zure erreprodukzio-zerrendak eta erreprodukzio-zerrenden barneko bideoak soilik
|
||||
kenduta.
|
||||
Hide Subscriptions Live: Ezarpen hau aplikazio osorako "{appWideSetting}" ezarpenak
|
||||
ordezkatzen du, "{settingsSection}" ataleko "{azpisekzioa}" atalean
|
||||
Experimental Settings:
|
||||
Replace HTTP Cache: Electron-en diskoan oinarritutako HTTP cachea desgaitzen du
|
||||
eta memoriako irudien cache pertsonalizatua gaitu. RAM erabilera handitzea ekarriko
|
||||
du.
|
||||
SponsorBlock Settings:
|
||||
UseDeArrowThumbnails: Ordeztu bideoaren miniaturak DeArrow-en miniaturaz.
|
||||
UseDeArrowTitles: Ordeztu bideoen izenburuak DeArrow-en erabiltzaileek bidalitako
|
||||
tituluekin.
|
||||
Local API Error (Click to copy): 'Tokiko API-ak huts egin du (klikatu kopiatzeko)'
|
||||
Invidious API Error (Click to copy): 'Individious-eko APIak huts egin du (klikatu
|
||||
kopiatzeko)'
|
||||
Falling back to Invidious API: 'Individious-eko APIra itzultzen'
|
||||
Falling back to the local API: 'Tokiko APIra itzultzen'
|
||||
Falling back to Local API: 'Tokiko APIra itzultzen'
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: 'Bideo
|
||||
hau ez dago erabilgarri, zenbait formatu eskas baitira. Honakoa zure herrialdean
|
||||
erabilgarri ez dagoelako gerta daiteke.'
|
||||
|
@ -827,9 +1115,34 @@ Channels:
|
|||
Unsubscribe Prompt: Ziur al zaude "{channelName}"-ren harpidetza kendu nahi duzula?
|
||||
Count: '{number} kanal aurkitu dira.'
|
||||
Empty: Zure kanalen zerrenda hutsik da.
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: Honako {videoOrPlaylist} adin muga du
|
||||
Type:
|
||||
Channel: Kanala
|
||||
Video: Bideoa
|
||||
Preferences: Hobespenak
|
||||
Go to page: Joan {page}-ra
|
||||
Close Banner: Itxi iragarkia
|
||||
Age Restricted:
|
||||
This channel is age restricted: Kanal hau adin mugatuta dago
|
||||
This video is age restricted: Bideo hau adin mugatuta dago
|
||||
Playlist will not pause when current video is finished: Erreprodukzio-zerrenda ez
|
||||
da etengo uneko bideoa amaitzen denean
|
||||
Playlist will pause when current video is finished: Erreprodukzio-zerrenda pausatu
|
||||
egingo da uneko bideoa amaitzen denean
|
||||
Channel Unhidden: '{channel} kanalaren iragazkitik kendu da'
|
||||
Clipboard:
|
||||
Cannot access clipboard without a secure connection: Ezin da arbelera sartu konexio
|
||||
segururik gabe
|
||||
Copy failed: Ezin izan da kopiatu arbelean
|
||||
Tag already exists: '"{tagName}" etiketa badago jada'
|
||||
Trimmed input must be at least N characters long: Moztutako sarrerak karaktere bat
|
||||
izan behar du gutxienez | Moztutako sarrerak gutxienez {length} karaktere izan behar
|
||||
ditu
|
||||
Chapters:
|
||||
'Chapters list hidden, current chapter: {chapterName}': 'Kapituluen zerrenda ezkutatuta
|
||||
dago, uneko kapitulua: {chapterName}'
|
||||
Chapters: Kapituluak
|
||||
'Chapters list visible, current chapter: {chapterName}': 'Kapituluen zerrenda ikusgai,
|
||||
uneko kapitulua: {chapterName}'
|
||||
Hashtag:
|
||||
Hashtag: Traola
|
||||
This hashtag does not currently have any videos: Traola honek ez du bideorik une
|
||||
honetan
|
||||
Ok: Ados
|
||||
Channel Hidden: '{channel} gehitu da kanalaren iragazkian'
|
||||
|
|
|
@ -900,12 +900,6 @@ Screenshot Success: اسکرین شات به عنوان «{filePath}» ذخیر
|
|||
Ok: تایید
|
||||
Downloading has completed: دانلود '{videoTitle}' به پایان رسید
|
||||
Loop is now enabled: حلقه اکنون فعال است
|
||||
Age Restricted:
|
||||
Type:
|
||||
Video: ویدیو
|
||||
Channel: کانال
|
||||
This {videoOrPlaylist} is age restricted: این {videoOrPlaylist} دارای محدودیت سنی
|
||||
است
|
||||
Shuffle is now enabled: Shuffle اکنون فعال است
|
||||
Falling back to Invidious API: بازگشت به Invidious API
|
||||
Local API Error (Click to copy): خطای Local API (برای کپی کلیک کنید)
|
||||
|
@ -913,7 +907,7 @@ Shuffle is now disabled: Shuffle اکنون غیرفعال است
|
|||
Canceled next video autoplay: پخش خودکار ویدیوی بعدی لغو شد
|
||||
Unknown YouTube url type, cannot be opened in app: نوع URL ناشناخته YouTube، در برنامه
|
||||
باز نمی شود
|
||||
Falling back to the local API: بازگشت به API محلی
|
||||
Falling back to Local API: بازگشت به API محلی
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: این
|
||||
ویدیو به دلیل عدم وجود قالب در دسترس نیست. این ممکن است به دلیل در دسترس نبودن کشور
|
||||
اتفاق بیفتد.
|
||||
|
|
|
@ -175,6 +175,7 @@ User Playlists:
|
|||
on luotu.
|
||||
Add to Favorites: Lisää soittolistaan {playlistName}
|
||||
Remove from Favorites: Poista soittolistalta {playlistName}
|
||||
Remove Watched Videos: Poista katsotut videot
|
||||
History:
|
||||
# On History Page
|
||||
History: 'Historia'
|
||||
|
@ -903,7 +904,7 @@ Up Next: 'Seuraavaksi'
|
|||
Local API Error (Click to copy): 'Paikallinen API-virhe (Kopioi napsauttamalla)'
|
||||
Invidious API Error (Click to copy): 'Invidious API-virhe (Kopioi napsauttamalla)'
|
||||
Falling back to Invidious API: 'Palaa takaisin Invidious-sovellusliittymään'
|
||||
Falling back to the local API: 'Palaa takaisin paikalliseen sovellusliittymään'
|
||||
Falling back to Local API: 'Palaa takaisin paikalliseen sovellusliittymään'
|
||||
Subscriptions have not yet been implemented: 'Tilauksia ei ole vielä jalkautettu'
|
||||
Loop is now disabled: 'Silmukka on poistettu käytöstä'
|
||||
Loop is now enabled: 'Silmukka on nyt käytössä'
|
||||
|
@ -1063,11 +1064,6 @@ Downloading has completed: Videon "{videoTitle}" lataus on valmis
|
|||
Starting download: Aloitetaan lataamaan "{videoTitle}"
|
||||
Screenshot Success: Kuvakaappaus tallennettu nimellä ”{filePath}”
|
||||
New Window: Uusi Ikkuna
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: Tämä {videoOrPlaylist} on ikärajoitettu
|
||||
Type:
|
||||
Video: Video
|
||||
Channel: Kanava
|
||||
Screenshot Error: Ruutukaappaus epäonnistui. {error}
|
||||
Channels:
|
||||
Channels: Kanavat
|
||||
|
|
|
@ -145,9 +145,10 @@ User Playlists:
|
|||
You haven't selected any playlist yet.: Vous n'avez pas encore sélectionné de
|
||||
liste de lecture.
|
||||
"{videoCount} video(s) added to {playlistCount} playlists": 1 vidéo ajoutée
|
||||
à {playlistCount} liste de lecture | {videoCount} vidéos ajoutées à {playlistCount}
|
||||
listes de lecture.
|
||||
à {playlistCount} listes de lecture | {videoCount} vidéos ajoutées à {playlistCount}
|
||||
listes de lecture
|
||||
N playlists selected: '{playlistCount} Sélectionnée(s)'
|
||||
Added {count} Times: Ajouté {count} Fois | Ajouté {count} Fois
|
||||
SinglePlaylistView:
|
||||
Toast:
|
||||
There were no videos to remove.: Il n'y avait aucune vidéo à supprimer.
|
||||
|
@ -299,6 +300,7 @@ Settings:
|
|||
Catppuccin Mocha: Catppuccin Moka
|
||||
Pastel Pink: Rose pastel
|
||||
Hot Pink: Rose vif
|
||||
Nordic: Nordic
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Couleur principale du thème'
|
||||
Red: 'Rouge'
|
||||
|
@ -467,7 +469,7 @@ Settings:
|
|||
sûr(e) de vouloir supprimer tous les abonnements et les profils ? Cette action
|
||||
est définitive.
|
||||
Remove All Subscriptions / Profiles: Supprimer tous les Abonnements / Profils
|
||||
Automatically Remove Video Meta Files: Suppression automatiquement les métafichiers
|
||||
Automatically Remove Video Meta Files: Supprimer automatiquement les métafichiers
|
||||
vidéo
|
||||
Save Watched Videos With Last Viewed Playlist: Sauvegarder les vidéos regardées
|
||||
avec la dernière liste de lecture vue
|
||||
|
@ -556,7 +558,7 @@ Settings:
|
|||
Hide Channels: Masquer les vidéos des chaînes
|
||||
Hide Channels Placeholder: Identifiant de la chaîne
|
||||
Display Titles Without Excessive Capitalisation: Afficher les titres sans majuscules
|
||||
excessives
|
||||
ni ponctuation excessives
|
||||
Hide Channel Playlists: Masquer les listes de lecture des chaînes
|
||||
Hide Featured Channels: Masquer les chaînes en vedette
|
||||
Hide Channel Community: Masquer la communauté de la chaîne
|
||||
|
@ -945,6 +947,7 @@ Video:
|
|||
Pause on Current Video: Pause sur la vidéo en cours
|
||||
Hide Channel: Cacher la chaîne
|
||||
Unhide Channel: Rétablir la chaîne
|
||||
More Options: Plus d'options
|
||||
Videos:
|
||||
#& Sort By
|
||||
Sort By:
|
||||
|
@ -1029,7 +1032,7 @@ Up Next: 'À suivre'
|
|||
Local API Error (Click to copy): 'Erreur d''API locale (Cliquez pour copier)'
|
||||
Invidious API Error (Click to copy): 'Erreur d''API Invidious (Cliquez pour copier)'
|
||||
Falling back to Invidious API: 'Revenir à l''API Invidious'
|
||||
Falling back to the local API: 'Revenir à l''API locale'
|
||||
Falling back to Local API: 'Revenir à l''API locale'
|
||||
Subscriptions have not yet been implemented: 'Les abonnements n''ont pas encore été
|
||||
implémentés'
|
||||
Loop is now disabled: 'La boucle est maintenant désactivée'
|
||||
|
@ -1225,12 +1228,6 @@ Download folder does not exist: 'Le répertoire "$" de téléchargement n''exist
|
|||
Screenshot Success: Capture d'écran enregistrée sous « {filePath} »
|
||||
Screenshot Error: La capture d'écran a échoué. {error}
|
||||
New Window: Nouvelle fenêtre
|
||||
Age Restricted:
|
||||
Type:
|
||||
Video: Vidéo
|
||||
Channel: Chaîne
|
||||
This {videoOrPlaylist} is age restricted: Ce {videoOrPlaylist} est soumis à une
|
||||
limite d'âge
|
||||
Channels:
|
||||
Channels: Chaînes
|
||||
Title: Liste des chaînes
|
||||
|
@ -1265,4 +1262,8 @@ Channel Hidden: '{channel} ajouté au filtre de chaîne'
|
|||
Channel Unhidden: '{channel} retiré du filtre de chaîne'
|
||||
Trimmed input must be at least N characters long: L'entrée tronquée doit comporter
|
||||
au moins 1 caractère | L'entrée tronquée doit comporter au moins {length} caractères
|
||||
Tag already exists: La balise "{tagName}" existe déjà
|
||||
Tag already exists: L'étiquette « {tagName} » existe déjà
|
||||
Age Restricted:
|
||||
This channel is age restricted: Cette chaîne est soumise à des restrictions d'âge
|
||||
This video is age restricted: Cette vidéo est soumise à des restrictions d'âge
|
||||
Close Banner: Fermer la bannière
|
||||
|
|
|
@ -36,6 +36,10 @@ Global:
|
|||
Videos: 'Vídeos'
|
||||
Community: Comunidade
|
||||
|
||||
Shorts: Cortos
|
||||
Input Tags:
|
||||
Length Requirement: A etiqueta debe ser de polo menos {number} caracteres.
|
||||
Live: En vivo
|
||||
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'
|
||||
|
@ -656,8 +660,8 @@ Video:
|
|||
nesta versión.'
|
||||
'Chat is disabled or the Live Stream has ended.': 'O chat foi desactivado ou a transmisión
|
||||
en vivo rematou.'
|
||||
Live chat is enabled. Chat messages will appear here once sent.: 'Chat en vivo
|
||||
activado. As mensaxes aparecerán aquí ao seren enviadas.'
|
||||
Live chat is enabled. Chat messages will appear here once sent.: 'Chat en vivo activado. As
|
||||
mensaxes aparecerán aquí ao seren enviadas.'
|
||||
'Live Chat is currently not supported with the Invidious API. A direct connection to YouTube is required.': 'Chat
|
||||
en vivo actualmente non soportado coa API de Invidious. Precísase dunha conexión
|
||||
directa con YouTube.'
|
||||
|
@ -844,9 +848,9 @@ Tooltips:
|
|||
as chamadas da aplicacion.'
|
||||
Region for Trending: 'A rexión das tendencias permíteche escoller os vídeos máis
|
||||
populares nun Estado.'
|
||||
External Link Handling: "Escolla o comportamento predeterminado cando se fai clic\
|
||||
\ nunha ligazón, que non se pode abrir en FreeTube.\nDe forma predeterminada,\
|
||||
\ FreeTube abrirá a ligazón na que premeches no teu navegador predeterminado.\n"
|
||||
External Link Handling: "Escolla o comportamento predeterminado cando se fai clic
|
||||
nunha ligazón, que non se pode abrir en FreeTube.\nDe forma predeterminada,
|
||||
FreeTube abrirá a ligazón na que premeches no teu navegador predeterminado.\n"
|
||||
Player Settings:
|
||||
Force Local Backend for Legacy Formats: 'Só funcionará se a API de Invidious está
|
||||
escollida por defecto. Cando estea activa, a API local usará formatos antigos
|
||||
|
@ -907,7 +911,7 @@ Tooltips:
|
|||
Local API Error (Click to copy): 'Erro de API local (Preme para copiar)'
|
||||
Invidious API Error (Click to copy): 'Erro de API Invidious (Preme para copiar)'
|
||||
Falling back to Invidious API: 'Recorrendo á API Invidious'
|
||||
Falling back to the local API: 'Recorrendo á API local'
|
||||
Falling back to Local API: 'Recorrendo á API local'
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: 'Este
|
||||
vídeo non está dispoñible porque faltan formatos. Isto pode ocorrer debido á non
|
||||
dispoñibilidade do país.'
|
||||
|
@ -954,12 +958,6 @@ Downloading has completed: '"{videoTitle}" rematou de descargarse'
|
|||
External link opening has been disabled in the general settings: A apertura das ligazóns
|
||||
externas desactivouse na configuración xeral
|
||||
Starting download: Comenzando a descarga de "{videoTitle}"
|
||||
Age Restricted:
|
||||
Type:
|
||||
Channel: Canle
|
||||
Video: Vídeo
|
||||
This {videoOrPlaylist} is age restricted: Esta {videoOrPlaylist} ten restricións
|
||||
de idade
|
||||
Default Invidious instance has been cleared: Borrouse a instancia predeterminada de
|
||||
Invidious
|
||||
Screenshot Error: Produciuse un erro na captura de pantalla. {error}
|
||||
|
@ -977,3 +975,4 @@ Chapters:
|
|||
capítulo actual: {chapterName}'
|
||||
Screenshot Success: Captura da pantalla gardada como "{filePath}"
|
||||
Ok: De acordo
|
||||
Go to page: Ir a {page}
|
||||
|
|
|
@ -49,4 +49,3 @@ Settings:
|
|||
SponsorBlock Settings: {}
|
||||
Channel: {}
|
||||
Tooltips: {}
|
||||
Age Restricted: {}
|
||||
|
|
|
@ -874,7 +874,7 @@ Up Next: 'הסרטון הבא'
|
|||
Local API Error (Click to copy): 'בעיה ב־API המקומי (יש ללחוץ להעתקה)'
|
||||
Invidious API Error (Click to copy): 'בעיה ב־API של Invidious (יש ללחוץ להעתקה)'
|
||||
Falling back to Invidious API: 'מתבצעת נסיגה ל־API של Invidious'
|
||||
Falling back to the local API: 'מתבצעת נסיגה ל־API המקומי'
|
||||
Falling back to Local API: 'מתבצעת נסיגה ל־API המקומי'
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: 'חסרות
|
||||
תצורות לסרטון הזה. הדבר יכול להיגרם בגלל חוסר זמינות למדינה.'
|
||||
Subscriptions have not yet been implemented: 'מנגנון המינויים עדיין לא מוכן'
|
||||
|
@ -980,11 +980,6 @@ Playing Next Video Interval: הסרטון הבא יתחיל מייד. לחיצה
|
|||
Screenshot Success: צילום המסך נשמר בתור „{filePath}”
|
||||
Screenshot Error: צילום המסך נכשל. {error}
|
||||
New Window: חלון חדש
|
||||
Age Restricted:
|
||||
Type:
|
||||
Channel: ערוץ
|
||||
Video: סרטון
|
||||
This {videoOrPlaylist} is age restricted: '{videoOrPlaylist} זה מוגבל בגיל'
|
||||
Channels:
|
||||
Search bar placeholder: חיפוש ערוצים
|
||||
Empty: רשימת הערוצים שלך ריקה כרגע.
|
||||
|
|
|
@ -101,8 +101,8 @@ Subscriptions:
|
|||
Disabled Automatic Fetching: Automatsko dohvaćanje pretplata je deaktivirano. Aktualiziraj
|
||||
pretplate da bi se ovdje prikazale.
|
||||
Subscriptions Tabs: Kartica pretplata
|
||||
All Subscription Tabs Hidden: Sve kartice pretplate su skrivene. Za prikaz sadržaja
|
||||
na ovom mjestu, sakrij neke kartice u odjeljku „{subsection}” u „{settingsSection}”.
|
||||
All Subscription Tabs Hidden: Sve kartice pretplata su skrivene. Za prikaz sadržaja
|
||||
na ovom mjestu, sakrij neke kartice u pododjeljku „{subsection}” u „{settingsSection}”.
|
||||
Empty Posts: Kanali na koje si pretplaćen/a trenutačno nemaju objave.
|
||||
Load More Posts: Učitaj još objava
|
||||
Trending:
|
||||
|
@ -175,6 +175,14 @@ User Playlists:
|
|||
Some videos in the playlist are not loaded yet. Click here to copy anyway.: Neka
|
||||
videa zbirke još nisu učitani. Pritisni ovdje za kopiranje.
|
||||
There were no videos to remove.: Nije bilo videa za uklanjanje.
|
||||
This playlist is now used for quick bookmark: Ova se zbirka sada koristi za
|
||||
brze zabilješke
|
||||
Quick bookmark disabled: Brze zabilješke su deaktivirane
|
||||
Reverted to use {oldPlaylistName} for quick bookmark: Vraćeno na korištenje
|
||||
zbirke {oldPlaylistName} za brze zabilješke
|
||||
This playlist is now used for quick bookmark instead of {oldPlaylistName}. Click here to undo: Ova
|
||||
se zbirka sada koristi za brze zabilješke umjesto zbirke {oldPlaylistName}.
|
||||
Pritisni ovdje za poništavanje
|
||||
AddVideoPrompt:
|
||||
N playlists selected: 'Odabrano: {playlistCount}'
|
||||
Search in Playlists: Traži u zbirkama
|
||||
|
@ -187,6 +195,7 @@ User Playlists:
|
|||
videa dodana u 1 zbirku
|
||||
Select a playlist to add your N videos to: Odaberi zbirku za dodavanje tvog videa
|
||||
| Odaberi zbirku za dodavanje tvojih {videoCount} videa
|
||||
Added {count} Times: Dodano {count} puta | Dodano {count} puta
|
||||
CreatePlaylistPrompt:
|
||||
Create: Stvori
|
||||
Toast:
|
||||
|
@ -199,6 +208,8 @@ User Playlists:
|
|||
New Playlist Name: Ime nove zbirke
|
||||
Add to Favorites: Dodaj u zbirku {playlistName}
|
||||
Remove from Favorites: Ukloni iz zbirke {playlistName}
|
||||
Enable Quick Bookmark With This Playlist: Aktiviraj brze zabilješke s ovom zbirkom
|
||||
Disable Quick Bookmark: Deaktiviraj brze zabilješke
|
||||
History:
|
||||
# On History Page
|
||||
History: 'Povijest'
|
||||
|
@ -262,11 +273,12 @@ Settings:
|
|||
Black: 'Crna'
|
||||
Dark: 'Tamna'
|
||||
Light: 'Svijetla'
|
||||
Dracula: 'Drakula'
|
||||
Dracula: 'Dracula'
|
||||
System Default: Standard sustava
|
||||
Catppuccin Mocha: Catppuccin Mocha
|
||||
Pastel Pink: Pastelno ružičasta
|
||||
Hot Pink: Vruća ružičasta
|
||||
Nordic: Nordic
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Glavna boja teme'
|
||||
Red: 'Crvena'
|
||||
|
@ -511,7 +523,7 @@ Settings:
|
|||
Hide Channels: Sakrij videa iz kanala
|
||||
Hide Channels Placeholder: ID kanala
|
||||
Display Titles Without Excessive Capitalisation: Prikaži naslove bez pretjeranog
|
||||
korištenja velikih slova
|
||||
korištenja velikih slova i interpunkcije
|
||||
Hide Featured Channels: Sakrij istaknute kanale
|
||||
Hide Channel Playlists: Sakrij kanal zbirki
|
||||
Hide Channel Community: Sakrij kanal zajednice
|
||||
|
@ -538,6 +550,7 @@ Settings:
|
|||
provjeri točnost ID-a.
|
||||
Hide Videos and Playlists Containing Text Placeholder: Riječ, fragment riječi
|
||||
ili fraza
|
||||
Hide Videos and Playlists Containing Text: Sakrij videa i zbirke koji sadrže tekst
|
||||
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:
|
||||
|
@ -916,7 +929,7 @@ Video:
|
|||
Resolution: Razlučivost
|
||||
Player Dimensions: Dimenzije playera
|
||||
Bitrate: Brzina prijenosa
|
||||
Volume: Glasnoća
|
||||
Volume: Direktorij
|
||||
Bandwidth: Propusnost
|
||||
Buffered: Učitano u memoriju
|
||||
Mimetype: Mimetype
|
||||
|
@ -931,6 +944,7 @@ Video:
|
|||
Pause on Current Video: Zaustavi trenutačni video
|
||||
Unhide Channel: Prikaži kanal
|
||||
Hide Channel: Sakrij kanal
|
||||
More Options: Više opcija
|
||||
Videos:
|
||||
#& Sort By
|
||||
Sort By:
|
||||
|
@ -1011,7 +1025,7 @@ Up Next: 'Sljedeći'
|
|||
Local API Error (Click to copy): 'Greška lokalnog sučelja (pritisni za kopiranje)'
|
||||
Invidious API Error (Click to copy): 'Greška Invidious sučelja (pritisni za kopiranje)'
|
||||
Falling back to Invidious API: 'Koristit će se Invidious sučelje'
|
||||
Falling back to the local API: 'Koristit će se lokalno sučelje'
|
||||
Falling back to Local API: 'Koristit će se lokalno sučelje'
|
||||
Subscriptions have not yet been implemented: 'Pretplate još nisu implementirane'
|
||||
Loop is now disabled: 'Ponavljanje je sada deaktivirano'
|
||||
Loop is now enabled: 'Ponavljanje je sada aktivirano'
|
||||
|
@ -1042,7 +1056,7 @@ Tooltips:
|
|||
Proxy Videos Through Invidious: Za reprodukciju videa povezat će se s Invidiousom
|
||||
umjesto izravnog povezivanja s YouTubeom. Zanemaruje postavke sučelja.
|
||||
Force Local Backend for Legacy Formats: Radi samo, kad se Invidious postavi kao
|
||||
standardno sučelje. Kada je aktivirano, lokalno sučelje će pokretati i koristiti
|
||||
standardno sučelje. Kada je aktivirano, lokalno API sučelje će pokretati i koristiti
|
||||
stare formate umjesto onih koje dostavlja Invidious. Pomaže u slučajevima, kad
|
||||
je reprodukcija videa koje dostavlja Invidious u zemlji zabranjena/ograničena.
|
||||
Scroll Playback Rate Over Video Player: Dok se pokazivač nalazi na videu, pritisni
|
||||
|
@ -1107,6 +1121,10 @@ Tooltips:
|
|||
se mora potpuno poklapati i razlikuje velika i mala slova.
|
||||
Hide Subscriptions Live: Ovu postavku nadjačava aplikacijska postavka „{appWideSetting}”,
|
||||
u odjeljku „{subsection}” u „{settingsSection}”
|
||||
Hide Videos and Playlists Containing Text: Upiši riječ, fragment riječi ili izraz
|
||||
(ne razlikuje velika i mala slova) za skrivanje svih videa i zbirki s tim sadržajem
|
||||
u njihovim izvornim naslovima u cijelom FreeTubeu, isključujući samo povijest,
|
||||
tvoje zbirke i videa unutar zbirki.
|
||||
SponsorBlock Settings:
|
||||
UseDeArrowTitles: Zamijeni naslove videa koje su poslali korisnici s DeArrow naslovima.
|
||||
UseDeArrowThumbnails: Zamijeni minijature videa s DeArrow minijaturama.
|
||||
|
@ -1133,11 +1151,6 @@ Starting download: Početak preuzimanja „{videoTitle}”
|
|||
Screenshot Success: Snimka ekrana je spremljena pod „{filePath}”
|
||||
Screenshot Error: Neuspjela snimka ekrana. {error}
|
||||
New Window: Novi prozor
|
||||
Age Restricted:
|
||||
This {videoOrPlaylist} is age restricted: Ovaj {videoOrPlaylist} je dobno ograničen
|
||||
Type:
|
||||
Channel: Kanal
|
||||
Video: Video
|
||||
Channels:
|
||||
Channels: Kanali
|
||||
Title: Popis kanala
|
||||
|
@ -1172,3 +1185,7 @@ Channel Unhidden: '{channel} je uklonjen iz filtra kanala'
|
|||
Trimmed input must be at least N characters long: Skraćeni unos mora imati barem 1
|
||||
znak | Skraćeni unos mora imati barem {length} znaka
|
||||
Tag already exists: Oznaka „{tagName}” već postoji
|
||||
Age Restricted:
|
||||
This channel is age restricted: Ovaj je dobno ograničeni kanal
|
||||
This video is age restricted: Ovaj je dobno ograničeni video
|
||||
Close Banner: Zatvori natpis
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Put the name of your locale in the same language
|
||||
Locale Name: 'English (US)'
|
||||
Locale Name: 'Magyar'
|
||||
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.': >-
|
||||
|
@ -147,12 +147,13 @@ User Playlists:
|
|||
lejátszási listákhoz
|
||||
"{videoCount} video(s) added to 1 playlist": 1 videó hozzáadva 1 lejátszási
|
||||
listához | {videoCount} videó hozzáadása 1 lejátszási listához
|
||||
You haven't selected any playlist yet.: Még nem választott ki lejátszási listát
|
||||
sem.
|
||||
You haven't selected any playlist yet.: Még nem választott ki egyetlen lejátszási
|
||||
listát sem.
|
||||
Select a playlist to add your N videos to: Válasszon ki egy lejátszási listát
|
||||
a videó hozzáadásához | Válasszon ki egy lejátszási listát a {videoCount} videó
|
||||
hozzáadásához
|
||||
N playlists selected: '{playlistCount} Kiválasztott'
|
||||
Added {count} Times: Hozzáadva {count} Alkalommal | Hozzáadva {count} Alkalommal
|
||||
SinglePlaylistView:
|
||||
Toast:
|
||||
There were no videos to remove.: Nem voltak eltávolítható videók.
|
||||
|
@ -183,6 +184,7 @@ User Playlists:
|
|||
Kattints ide a visszavonáshoz
|
||||
Reverted to use {oldPlaylistName} for quick bookmark: Visszaállítva a(z) {oldPlaylistName}
|
||||
használatára a gyors könyvjelzőhöz
|
||||
Search for Videos: Videók keresése
|
||||
Are you sure you want to delete this playlist? This cannot be undone: Biztos, hogy
|
||||
törölni szeretné ezt a lejátszási listát? Ezt nem lehet visszacsinálni.
|
||||
Sort By:
|
||||
|
@ -296,6 +298,7 @@ Settings:
|
|||
Catppuccin Mocha: Catppuccin Mocha
|
||||
Pastel Pink: Pasztell rózsaszín
|
||||
Hot Pink: Forró rózsaszín
|
||||
Nordic: Skandináv
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Fő színtéma'
|
||||
Red: 'Vörös'
|
||||
|
@ -533,14 +536,14 @@ Settings:
|
|||
Hide Playlists: Lejátszási listák elrejtése
|
||||
Hide Video Description: Videó leírásának elrejtése
|
||||
Hide Comments: Megjegyzések elrejtése
|
||||
Hide Live Streams: Élő adatfolyamok elrejtése
|
||||
Hide Live Streams: Élő közvetítések elrejtése
|
||||
Hide Sharing Actions: Megosztási műveletek elrejtése
|
||||
Hide Chapters: Fejezetek elrejtése
|
||||
Hide Upcoming Premieres: Közelgő első előadások elrejtése
|
||||
Hide Channels: Videók elrejtése a csatornákból
|
||||
Hide Channels Placeholder: Csatornaazonosító
|
||||
Display Titles Without Excessive Capitalisation: Címek megjelenítése túlzott nagybetűk
|
||||
nélkül
|
||||
Display Titles Without Excessive Capitalisation: Jelenítse meg a címeket túlzott
|
||||
nagybetűs írás és írásjelek nélkül
|
||||
Hide Featured Channels: Kiemelt csatornák elrejtése
|
||||
Hide Channel Playlists: Csatorna lejátszási listák elrejtése
|
||||
Hide Channel Community: Csatornaközösség elrejtése
|
||||
|
@ -952,6 +955,7 @@ Video:
|
|||
Pause on Current Video: Jelenlegi videó szüneteltetése
|
||||
Unhide Channel: Csatorna megjelenítése
|
||||
Hide Channel: Csatorna elrejtése
|
||||
More Options: További beállítások
|
||||
Videos:
|
||||
#& Sort By
|
||||
Sort By:
|
||||
|
@ -1036,7 +1040,7 @@ Up Next: 'Következő'
|
|||
Local API Error (Click to copy): 'Helyi-API hiba (kattintson a másoláshoz)'
|
||||
Invidious API Error (Click to copy): 'Invidious-API hiba (Kattintson a másoláshoz)'
|
||||
Falling back to Invidious API: 'Invidious-API visszatérve'
|
||||
Falling back to the local API: 'Helyi-API visszatérve'
|
||||
Falling back to Local API: 'Helyi-API visszatérve'
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: 'Ez
|
||||
a videó hiányzó formátumok miatt nem érhető el. Ez az ország nem elérhetősége miatt
|
||||
következhet be.'
|
||||
|
@ -1088,10 +1092,10 @@ Tooltips:
|
|||
videókat szolgáltasson, ahelyett, hogy közvetlen kapcsolatot létesítene a YouTube
|
||||
szolgáltatással. Felülbírálja az API beállítást.
|
||||
Force Local Backend for Legacy Formats: Csak akkor működik, ha az Invidious API
|
||||
az alapértelmezett. Ha engedélyezve van, a helyi API futni fog, és az általa
|
||||
visszaadott örökölt formátumokat fogja használni az Invidious által visszaadottak
|
||||
helyett. Segít, ha az Invidious által visszaküldött videókat nem lehet lejátszani
|
||||
az ország korlátozása miatt.
|
||||
az alapértelmezett. Ha engedélyezve van, a helyi API fut, és az általa visszaadott
|
||||
régi formátumokat használja az Invidious által visszaadottak helyett. Segít,
|
||||
ha az Invidious által visszaküldött videók nem játszódnak le az országos korlátozások
|
||||
miatt.
|
||||
Scroll Playback Rate Over Video Player: Amíg a kurzor a videó felett van, nyomja
|
||||
meg és tartsa lenyomva a Control billentyűt (Mac gépen a Command billentyű),
|
||||
és görgesse az egér görgőjét előre vagy hátra a lejátszási sebesség szabályozásához.
|
||||
|
@ -1170,11 +1174,6 @@ Channels:
|
|||
Unsubscribe: Leiratkozás
|
||||
Unsubscribed: '{channelName} eltávolítva az feliratkozásáiból'
|
||||
Unsubscribe Prompt: Biztosan le szeretne iratkozni a(z) „{channelName}” csatornáról?
|
||||
Age Restricted:
|
||||
Type:
|
||||
Video: Videó
|
||||
Channel: Csatorna
|
||||
This {videoOrPlaylist} is age restricted: A(z) {videoOrPlaylist} korhatáros
|
||||
Downloading failed: Hiba történt a(z) „{videoTitle}” letöltése során
|
||||
Starting download: „{videoTitle}” letöltésének indítása
|
||||
Downloading has completed: A(z) „{videoTitle}” letöltése befejeződött
|
||||
|
@ -1207,3 +1206,7 @@ Trimmed input must be at least N characters long: A vágott bemenetnek legalább
|
|||
hosszúnak kell lennie | A vágott bemenetnek legalább {length} karakter hosszúnak
|
||||
kell lennie
|
||||
Tag already exists: '„{tagName}” címke már létezik'
|
||||
Close Banner: Banner bezárása
|
||||
Age Restricted:
|
||||
This channel is age restricted: Ez a csatorna korhatáros
|
||||
This video is age restricted: Ez a videó korhatáros
|
||||
|
|
|
@ -753,7 +753,7 @@ Up Next: 'Akan Datang'
|
|||
Local API Error (Click to copy): 'API Lokal Galat (Klik untuk menyalin)'
|
||||
Invidious API Error (Click to copy): 'API Invidious Galat (Klik untuk menyalin)'
|
||||
Falling back to Invidious API: 'Kembali ke API Invidious'
|
||||
Falling back to the local API: 'Kembali ke API lokal'
|
||||
Falling back to Local API: 'Kembali ke API lokal'
|
||||
Subscriptions have not yet been implemented: 'Langganan masih belum diterapkan'
|
||||
Loop is now disabled: 'Putar-Ulang sekarang dimatikan'
|
||||
Loop is now enabled: 'Putar-Ulang sekarang diaktifkan'
|
||||
|
|
|
@ -44,6 +44,8 @@ Global:
|
|||
Subscriber Count: 1 áskrifandi | {count} áskrifendur
|
||||
View Count: 1 áhorf | {count} áhorf
|
||||
Watching Count: 1 að horfa | {count} að horfa
|
||||
Input Tags:
|
||||
Length Requirement: Merki þarf að vera a.m.k. {number} stafa langt
|
||||
Version {versionNumber} is now available! Click for more details: 'Útgáfa {versionNumber}
|
||||
er tiltæk! Smelltu til að skoða nánar'
|
||||
Download From Site: 'Sækja af vefsvæði'
|
||||
|
@ -174,6 +176,14 @@ User Playlists:
|
|||
Some videos in the playlist are not loaded yet. Click here to copy anyway.: Sum
|
||||
myndskeið í spilunarlistanum hafa ekki enn hlaðist inn. Smelltu hér til að
|
||||
afrita samt.
|
||||
This playlist is now used for quick bookmark: Þessi spilunarlisti er núna notaður
|
||||
undir flýtibókamerki
|
||||
Quick bookmark disabled: Flýtibókamerki óvirk
|
||||
This playlist is now used for quick bookmark instead of {oldPlaylistName}. Click here to undo: Þessi
|
||||
spilunarlisti er núna notaður undir flýtibókamerki í stað {oldPlaylistName}.
|
||||
Smelltu hér til að afturkalla
|
||||
Reverted to use {oldPlaylistName} for quick bookmark: Snéri aftur í að nota
|
||||
{oldPlaylistName} undir flýtibókamerki
|
||||
AddVideoPrompt:
|
||||
N playlists selected: '{playlistCount} valin'
|
||||
Search in Playlists: Leita í spilunarlistum
|
||||
|
@ -187,6 +197,7 @@ User Playlists:
|
|||
| {videoCount} myndskeiðum bætt við 1 spilunarlista
|
||||
Select a playlist to add your N videos to: Veldu spilunarlista til að bæta myndskeiðinu
|
||||
þínu á | Veldu spilunarlista til að bæta {videoCount}̣ myndskeiðunum þínum á
|
||||
Added {count} Times: Bætt við {count} sinni | Bætt við {count} sinnum
|
||||
CreatePlaylistPrompt:
|
||||
Create: Búa til
|
||||
New Playlist Name: Heiti á nýjum spilunarlista
|
||||
|
@ -209,6 +220,10 @@ User Playlists:
|
|||
Delete Playlist: Eyða spilunarlista
|
||||
Are you sure you want to delete this playlist? This cannot be undone: Ertu viss
|
||||
um að þú viljir eyða þessum spilunarlista? Aðgerðin er ekki afturkallanleg.
|
||||
Add to Favorites: Bæta á {playlistName}
|
||||
Remove from Favorites: Fjarlægja af {playlistName}
|
||||
Enable Quick Bookmark With This Playlist: Virkja flýtibókamerki með þessum spilunarlista
|
||||
Disable Quick Bookmark: Eyða flýtibókamerki
|
||||
History:
|
||||
# On History Page
|
||||
History: 'Áhorf'
|
||||
|
@ -283,6 +298,7 @@ Settings:
|
|||
Catppuccin Mocha: Catppuccin Mocha
|
||||
Pastel Pink: Pastelbleikt
|
||||
Hot Pink: Dimmbleikt
|
||||
Nordic: Norrænt
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Aðallitur þema'
|
||||
Red: 'Rautt'
|
||||
|
@ -436,6 +452,7 @@ Settings:
|
|||
Hide Channels: Fela myndskeið úr rásum
|
||||
Hide Channels Placeholder: Auðkenni rásar
|
||||
Display Titles Without Excessive Capitalisation: Birta titla án umfram-hástafa
|
||||
og greinarmerkja
|
||||
Sections:
|
||||
Side Bar: Hliðarspjald
|
||||
Channel Page: Rásasíða
|
||||
|
@ -461,6 +478,9 @@ Settings:
|
|||
Hide Channels Already Exists: Auðkenni rásar er þegar til
|
||||
Hide Channels API Error: Villa við að ná í notanda með uppgefið auðkenni. Athugaðu
|
||||
aftur hvort auðkennið ré rétt.
|
||||
Hide Videos and Playlists Containing Text: Fela myndskeið og spilunarlista sem
|
||||
innihalda texta
|
||||
Hide Videos and Playlists Containing Text Placeholder: Orð, orðhluti eða setning
|
||||
Data Settings:
|
||||
Data Settings: 'Stillingar gagna'
|
||||
Select Import Type: 'Veldu tegund innflutnings'
|
||||
|
@ -721,6 +741,7 @@ Channel:
|
|||
Reveal Answers: Birta svör
|
||||
Hide Answers: Fela svör
|
||||
votes: '{votes} atkvæði'
|
||||
Video hidden by FreeTube: Myndskeið falið af FreeTube
|
||||
Shorts:
|
||||
This channel does not currently have any shorts: Þessi rás er í augnablikinu ekki
|
||||
með neinar stuttmyndir
|
||||
|
@ -878,6 +899,7 @@ Video:
|
|||
Pause on Current Video: Setja núverandi myndskeið í bið
|
||||
Unhide Channel: Birta rás
|
||||
Hide Channel: Fela rás
|
||||
More Options: Fleiri valkostir
|
||||
Videos:
|
||||
#& Sort By
|
||||
Sort By:
|
||||
|
@ -1043,7 +1065,7 @@ Local API Error (Click to copy): 'Villa í staðværu API-kerfisviðmóti (smell
|
|||
Invidious API Error (Click to copy): 'Villa í Invidious API-kerfisviðmóti (smella
|
||||
til að afrita)'
|
||||
Falling back to Invidious API: 'Nota til vara Invidious API-kerfisviðmót'
|
||||
Falling back to the local API: 'Nota til vara staðvært API-kerfisviðmót'
|
||||
Falling back to Local API: 'Nota til vara staðvært API-kerfisviðmót'
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: 'Þetta
|
||||
myndskeiðer ekki tiltækt vegna þess að það vantar skráasnið. Þetta getur gest ef
|
||||
þau eru ekki tiltæk í viðkomandi landi.'
|
||||
|
@ -1084,11 +1106,6 @@ Starting download: Byrja að sækja "{videoTitle}"
|
|||
Downloading failed: Vandamál kom upp við að sækja "{videoTitle}"
|
||||
Screenshot Error: Skjámyndataka mistókst. {error}
|
||||
Screenshot Success: Vistaði skjámynd sem "{filePath}"
|
||||
Age Restricted:
|
||||
Type:
|
||||
Channel: Rás
|
||||
Video: Myndskeið
|
||||
This {videoOrPlaylist} is age restricted: Þetta {videoOrPlaylist} er með aldurstakmörkunum
|
||||
New Window: Nýr gluggi
|
||||
Channels:
|
||||
Search bar placeholder: Leita í rásum
|
||||
|
@ -1122,3 +1139,8 @@ Playlist will not pause when current video is finished: Spilunarlisti mun ekki f
|
|||
Go to page: Fara á {page}
|
||||
Channel Hidden: '{channel} bætt við rásasíu'
|
||||
Channel Unhidden: '{channel} fjarlægt úr rásasíu'
|
||||
Close Banner: Loka borða
|
||||
Age Restricted:
|
||||
This video is age restricted: Þetta myndskeið er með aldurstakmörkunum
|
||||
This channel is age restricted: Þessi rás er með aldurstakmörkunum
|
||||
Tag already exists: '"{tagName}" merkið er þegar til staðar'
|
||||
|
|
|
@ -143,6 +143,7 @@ User Playlists:
|
|||
Select a playlist to add your N videos to: Seleziona una playlist a cui aggiungere
|
||||
il tuo video | Seleziona una playlist a cui aggiungere i tuoi {videoCount} video
|
||||
N playlists selected: '{playlistCount} selezionate'
|
||||
Added {count} Times: Aggiunto {count} volta | Aggiunti {count} volte
|
||||
SinglePlaylistView:
|
||||
Toast:
|
||||
There were no videos to remove.: Non c'erano video da rimuovere.
|
||||
|
@ -175,6 +176,7 @@ User Playlists:
|
|||
This playlist is now used for quick bookmark instead of {oldPlaylistName}. Click here to undo: Questa
|
||||
playlist è ora usata per i segnalibri rapidi al posto di {oldPlaylistName}.
|
||||
Fai clic qui per annullare
|
||||
Search for Videos: Cerca video
|
||||
Are you sure you want to delete this playlist? This cannot be undone: Sei sicuro
|
||||
di voler eliminare questa playlist? Questa operazione non può essere annullata.
|
||||
Sort By:
|
||||
|
@ -287,6 +289,7 @@ Settings:
|
|||
Catppuccin Mocha: Cappuccino moka
|
||||
Pastel Pink: Rosa pastello
|
||||
Hot Pink: Rosa caldo
|
||||
Nordic: Nordico
|
||||
Main Color Theme:
|
||||
Main Color Theme: 'Colore principale del tema'
|
||||
Red: 'Rosso'
|
||||
|
@ -536,8 +539,8 @@ Settings:
|
|||
Hide Upcoming Premieres: Nascondi le prossime Première
|
||||
Hide Channels: Nascondi i video dai canali
|
||||
Hide Channels Placeholder: ID del canale
|
||||
Display Titles Without Excessive Capitalisation: Visualizza i titoli senza un
|
||||
uso eccessivo di maiuscole
|
||||
Display Titles Without Excessive Capitalisation: Visualizza i titoli senza maiuscole
|
||||
e punteggiatura eccessive
|
||||
Hide Featured Channels: Nascondi i canali in evidenza
|
||||
Hide Channel Playlists: Nascondi le playlist del canale
|
||||
Hide Channel Community: Nascondi la comunità del canale
|
||||
|
@ -913,6 +916,7 @@ Video:
|
|||
Pause on Current Video: Pausa sul video attuale
|
||||
Unhide Channel: Mostra canale
|
||||
Hide Channel: Nascondi canale
|
||||
More Options: Più opzioni
|
||||
Videos:
|
||||
#& Sort By
|
||||
Sort By:
|
||||
|
@ -996,7 +1000,7 @@ Up Next: 'Prossimi video'
|
|||
Local API Error (Click to copy): 'Errore API Locale (Clicca per copiare)'
|
||||
Invidious API Error (Click to copy): 'Errore API Invidious (Clicca per copiare)'
|
||||
Falling back to Invidious API: 'Torno alle API Invidious'
|
||||
Falling back to the local API: 'Torno alle API locali'
|
||||
Falling back to Local API: 'Torno alle API locali'
|
||||
Subscriptions have not yet been implemented: 'Le Iscrizioni non sono ancora state
|
||||
implementate'
|
||||
Loop is now disabled: 'Il loop è ora disabilitato'
|
||||
|
@ -1177,13 +1181,6 @@ Starting download: Avvio del download di "{videoTitle}"
|
|||
Downloading failed: Si è verificato un problema durante il download di "{videoTitle}"
|
||||
Screenshot Success: Screenshot salvato come "{filePath}"
|
||||
Screenshot Error: Screenshot non riuscito. {error}
|
||||
Age Restricted:
|
||||
The currently set default instance is {instance}: Questo {instance} è limitato dall'età
|
||||
Type:
|
||||
Channel: Canale
|
||||
Video: Video
|
||||
This {videoOrPlaylist} is age restricted: Questo {videoOrPlaylist} ha limiti di
|
||||
età
|
||||
New Window: Nuova finestra
|
||||
Channels:
|
||||
Unsubscribed: '{channelName} è stato rimosso dalle tue iscrizioni'
|
||||
|
@ -1220,3 +1217,7 @@ Channel Unhidden: '{channel} rimosso dal filtro canali'
|
|||
Tag already exists: Il tag "{tagName}" esiste già
|
||||
Trimmed input must be at least N characters long: L'input troncato deve essere lungo
|
||||
almeno 1 carattere | L'input troncato deve essere lungo almeno {length} caratteri
|
||||
Age Restricted:
|
||||
This video is age restricted: Questo video è soggetto a limiti di età
|
||||
This channel is age restricted: Questo canale è soggetto a limiti di età
|
||||
Close Banner: Chiudi banner
|
||||
|
|
|
@ -114,6 +114,10 @@ User Playlists:
|
|||
このページは、完全に動作する動画リストではありません。保存またはお気に入りと設定した動画のみが表示されます。操作が完了すると、現在ここにあるすべての動画は「お気に入り」の動画リストに移動します。
|
||||
Search bar placeholder: 動画リスト内の検索
|
||||
Empty Search Message: この再生リストに、検索に一致する動画はありません
|
||||
This playlist currently has no videos.: 存在、この再生リストには動画があっていません。
|
||||
Create New Playlist: 新規再生リストを作られる
|
||||
Sort By:
|
||||
NameAscending: A-Z
|
||||
History:
|
||||
# On History Page
|
||||
History: '履歴'
|
||||
|
@ -276,7 +280,7 @@ Settings:
|
|||
Folder Button: フォルダーの選択
|
||||
Enter Fullscreen on Display Rotate: 横画面時にフルスクリーンにする
|
||||
Skip by Scrolling Over Video Player: 動画プレーヤーでスクロールしてスキップ可能にする
|
||||
Allow DASH AV1 formats: DASH AV1形式を許可する
|
||||
Allow DASH AV1 formats: DASH AV1形式を許可
|
||||
Comment Auto Load:
|
||||
Comment Auto Load: コメント自動読み込み
|
||||
Subscription Settings:
|
||||
|
@ -794,7 +798,7 @@ Up Next: '次の動画'
|
|||
Local API Error (Click to copy): '内部 API エラー(クリックするとコピー)'
|
||||
Invidious API Error (Click to copy): 'Invidious API エラー(クリックするとコピー)'
|
||||
Falling back to Invidious API: '代替の Invidious API に切替'
|
||||
Falling back to the local API: '代替の内部 API に切替'
|
||||
Falling back to Local API: '代替の内部 API に切替'
|
||||
Subscriptions have not yet been implemented: '登録チャンネルは未実装です'
|
||||
Loop is now disabled: 'ループ再生を無効にしました'
|
||||
Loop is now enabled: 'ループ再生を有効にしました'
|
||||
|
@ -850,7 +854,7 @@ The playlist has been reversed: 再生リストを逆順にしました
|
|||
A new blog is now available, {blogTitle}. Click to view more: '新着ブログ公開、{blogTitle}。クリックしてブログを読む'
|
||||
Download From Site: サイトからダウンロード
|
||||
Version {versionNumber} is now available! Click for more details: 最新バージョン {versionNumber}
|
||||
配信中! 詳細はクリックして確認してください
|
||||
配信中!詳細はクリックして確認してください
|
||||
This video is unavailable because of missing formats. This can happen due to country unavailability.: この動画は、動画形式の情報が利用できないため再生できません。再生が許可されていない国で発生します。
|
||||
Tooltips:
|
||||
Subscription Settings:
|
||||
|
@ -866,8 +870,8 @@ Tooltips:
|
|||
Scroll Playback Rate Over Video Player: カーソルが動画上にあるとき、Ctrl キー(Mac では Command キー)を押したまま、マウスホイールを前後にスクロールして再生速度を調整します。Control
|
||||
キー(Mac では Command キー)を押したままマウスを左クリックすると、すぐにデフォルトの再生速度(設定を変更していない場合は 1 x)に戻ります。
|
||||
Skip by Scrolling Over Video Player: スクロール ホイールを使用して、ビデオ、MPV スタイルをスキップします。
|
||||
Allow DASH AV1 formats: DASH H.264形式よりDASH AV1形式の方がきれいに見える可能性があるけど、再生には必要な電力がより多い。全ての動画でDASH
|
||||
AV1を利用できないため、プレイヤーはDASH H.264形式に自動変更する場合があります。
|
||||
Allow DASH AV1 formats: DASH H.264形式よりもDASH AV1形式の方がきれいに見える可能性がありますが、再生にはより多くの処理能力が必要となります。DASH
|
||||
AV1形式を使用できない場合、プレイヤーはDASH H.264形式を自動で使用します。
|
||||
General Settings:
|
||||
Invidious Instance: FreeTube が使用する Invidious API の接続先サーバーです。
|
||||
Preferred API Backend: FreeTube が youtube からデータを取得する方法を選択します。「内部 API」とはアプリから取得する方法です。「Invidious
|
||||
|
@ -913,12 +917,6 @@ Are you sure you want to open this link?: このリンクを開きますか?
|
|||
Starting download: '"{videoTitle}" のダウンロードを開始します'
|
||||
Downloading has completed: '"{videoTitle}" のダウンロードが終了しました'
|
||||
Downloading failed: '"{videoTitle}" のダウンロード中に問題が発生しました'
|
||||
Age Restricted:
|
||||
The currently set default instance is {instance}: '{instance} は 18 歳以上の視聴者向け動画です'
|
||||
Type:
|
||||
Channel: チャンネル
|
||||
Video: 動画
|
||||
This {videoOrPlaylist} is age restricted: この {videoOrPlaylist} は年齢制限があります
|
||||
Channels:
|
||||
Channels: チャンネル
|
||||
Unsubscribe: 登録解除
|
||||
|
@ -947,3 +945,5 @@ Hashtag:
|
|||
This hashtag does not currently have any videos: このハッシュタグには現在動画がありません
|
||||
Playlist will pause when current video is finished: 現在のビデオが終了すると、プレイリストは停止します
|
||||
Playlist will not pause when current video is finished: 現在のビデオが終了しても、プレイリストは停止しません
|
||||
Close Banner: バナーを閉じる
|
||||
Go to page: '{page}に行く'
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue