Compare commits

...

15 Commits

Author SHA1 Message Date
absidue
d829cc2b16 Update to Vue 3 (#8094)
* Update to Vue 3

* Fix toasts and removing videos from playlists

* Fix duplicate app ID

* Simplify aria-selected handling now that false doesn't remove attributes

* Fix various errors

* Fix toasts and hiding watched videos

* Update vue-router to 4.6.3

---------

Co-authored-by: efb4f5ff-1298-471a-8973-3d47447115dc <73130443+efb4f5ff-1298-471a-8973-3d47447115dc@users.noreply.github.com>
2025-11-02 18:11:41 +00:00
Eder Etxebarria Rojo
ad669b3678 Translated using Weblate (Basque)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Eder Etxebarria Rojo <eder@betxepare.eus>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/eu/
Translation: FreeTube/Translations
2025-11-02 14:52:42 +01:00
summoner001
a8061f0131 Translated using Weblate (Hungarian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: summoner001 <summoner@disroot.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/hu/
Translation: FreeTube/Translations
2025-11-02 12:51:15 +01:00
Sveinn í Felli
b1ea445456 Translated using Weblate (Icelandic)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Sveinn í Felli <sv1@fellsnet.is>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/is/
Translation: FreeTube/Translations
2025-11-02 08:51:18 +01:00
Oğuz Ersen
500438debe Translated using Weblate (Turkish)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/tr/
Translation: FreeTube/Translations
2025-11-02 08:51:16 +01:00
absidue
796fc4eda0 Properly fix CORS in PO token web views (#8203) 2025-11-02 03:29:35 +00:00
Massimo Pissarello
9ede2a3019 Translated using Weblate (Italian)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Massimo Pissarello <mapi68@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/it/
Translation: FreeTube/Translations
2025-11-02 02:51:22 +00:00
Telaneo
7ea2d6fdb4 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Telaneo <post@telaneo.net>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/nb_NO/
Translation: FreeTube/Translations
2025-11-02 02:51:19 +00:00
Mickaël Binos
1ec73aa173 Translated using Weblate (French)
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: Mickaël Binos <mickaelbinos@outlook.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/fr/
Translation: FreeTube/Translations
2025-11-02 01:51:15 +01:00
absidue
293ea3f47a Configure nedb to always clean up corrupted data (#8202) 2025-11-02 08:41:54 +08:00
delvani
3fa5ccd199 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: delvani <del.cidrak@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/pt_BR/
Translation: FreeTube/Translations
2025-11-01 19:51:16 +01:00
efb4f5ff-1298-471a-8973-3d47447115dc
075f0c6df1 Add video paused check to frame shortcuts (#8200) 2025-11-01 18:40:34 +00:00
PikachuEXE
41830bf91d Fix toast with timeout 0 will be displayed for default 3s (#8168)
* ! Fix toast with timeout 0 will be displayed for default 3s

* * Stop calling `showToast` with zero, add warning if it happens
2025-11-01 14:08:58 -04:00
大王叫我来巡山
2ef1ff7a18 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (978 of 978 strings)

Co-authored-by: 大王叫我来巡山 <hamburger2048@users.noreply.hosted.weblate.org>
Translate-URL: https://hosted.weblate.org/projects/free-tube/translations/zh_Hans/
Translation: FreeTube/Translations
2025-11-01 16:54:53 +00:00
efb4f5ff-1298-471a-8973-3d47447115dc
eacc5ceca4 Change containing text input limit (#8174)
* Remove containing text input limit

* fix trimmed issues

* Update src/renderer/components/FtInputTags/FtInputTags.vue

Co-authored-by: PikachuEXE <git@pikachuexe.net>

* Update static/locales/en-US.yaml

Co-authored-by: PikachuEXE <git@pikachuexe.net>

* Update src/renderer/components/FtInputTags/FtInputTags.vue

---------

Co-authored-by: PikachuEXE <git@pikachuexe.net>
2025-11-01 15:06:15 +00:00
123 changed files with 630 additions and 754 deletions

View File

@@ -16,9 +16,9 @@ Please follow these guidelines before sending your pull request and making contr
* Stick to a similar style of code already in the project. Please look at current code to get an idea on how to do this.
* Follow [ES6](https://rse.github.io/es6-features/) standards in your code. Ex: Use `let` and `const` instead of `var`. Do not use `function(response){//code}` for callbacks, use `(response) => {//code}`.
* Comment your code when necessary. Follow the [JavaScript Documentation and Comments Standard](https://www.drupal.org/docs/develop/standards/javascript/javascript-api-documentation-and-comment-standards) for functions.
* Please follow proper Vue structure when creating new code / components. Use existing code as well as the [Vue.js Guide](https://vuejs.org/v2/guide/) for reference.
* Please follow proper Vue structure when creating new code / components. Use existing code as well as the [Vue.js Guide](https://vuejs.org/guide/introduction.html) for reference.
* Please test your code. Make sure new features work as well as existing core features such as watching videos or loading subscriptions. New features need to work with both the Local API as well as the Invidious API
* Please make sure your code does not violate any standards set by our linter. It's up to you to make fixes whenever necessary. You can run `npm run lint` to check locally and `npm run lint-fix` to automatically fix smaller issues.
* Please make sure your code does not violate any standards set by our linter. It's up to you to make fixes whenever necessary. You can run `yarn run lint` to check locally and `yarn run lint-fix` to automatically fix smaller issues.
* Please limit the amount of Node Modules that you introduce into the project. Only include them when **absolutely necessary** for your code to work (Ex: Using nedb for databases) or if a module provides similar functionality to what you are trying to achieve (Ex: Using autolinker to create links to outside URLs instead of writing the functionality myself).
* Please try to stay involved with the community and maintain your code. We are only a handful of developers working on FreeTube in our spare time. We do not have time to work on everything, and it would be nice if you can maintain your code when necessary.

View File

@@ -2,7 +2,7 @@ const path = require('path')
const { readFileSync, readdirSync } = require('fs')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const { VueLoaderPlugin } = require('vue-loader')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const ProcessLocalesPlugin = require('./ProcessLocalesPlugin')
@@ -56,7 +56,7 @@ const config = {
loader: 'vue-loader',
options: {
compilerOptions: {
whitespace: 'condense',
isCustomElement: (tag) => tag === 'swiper-container' || tag === 'swiper-slide'
}
}
},
@@ -133,6 +133,12 @@ const config = {
'process.env.IS_ELECTRON': true,
'process.env.IS_ELECTRON_MAIN': false,
'process.env.SUPPORTS_LOCAL_API': true,
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false',
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
__VUE_I18N_LEGACY_API__: 'true',
__VUE_I18N_FULL_INSTALL__: 'false',
__INTLIFY_PROD_DEVTOOLS__: 'false',
'process.env.LOCALE_NAMES': JSON.stringify(processLocalesPlugin.localeNames),
'process.env.GEOLOCATION_NAMES': JSON.stringify(readdirSync(path.join(__dirname, '..', 'static', 'geolocations')).map(filename => filename.replace('.json', ''))),
'process.env.SWIPER_VERSION': `'${swiperVersion}'`,
@@ -180,9 +186,6 @@ const config = {
],
resolve: {
alias: {
vue$: 'vue/dist/vue.runtime.esm.js',
'portal-vue$': 'portal-vue/dist/portal-vue.esm.js',
DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$: path.resolve(__dirname, '../src/datastores/handlers/electron.js'),
'youtubei.js$': 'youtubei.js/web',

View File

@@ -2,7 +2,7 @@ const path = require('path')
const fs = require('fs')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const { VueLoaderPlugin } = require('vue-loader')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const JsonMinimizerPlugin = require('json-minimizer-webpack-plugin')
@@ -45,7 +45,7 @@ const config = {
loader: 'vue-loader',
options: {
compilerOptions: {
whitespace: 'condense',
isCustomElement: (tag) => tag === 'swiper-container' || tag === 'swiper-slide',
}
}
},
@@ -128,6 +128,12 @@ const config = {
'process.env.IS_ELECTRON': false,
'process.env.IS_ELECTRON_MAIN': false,
'process.env.SUPPORTS_LOCAL_API': false,
__VUE_OPTIONS_API__: 'true',
__VUE_PROD_DEVTOOLS__: 'false',
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false',
__VUE_I18N_LEGACY_API__: 'true',
__VUE_I18N_FULL_INSTALL__: 'false',
__INTLIFY_PROD_DEVTOOLS__: 'false',
'process.env.SWIPER_VERSION': `'${swiperVersion}'`
}),
new webpack.ProvidePlugin({
@@ -158,9 +164,6 @@ const config = {
],
resolve: {
alias: {
vue$: 'vue/dist/vue.runtime.esm.js',
'portal-vue$': 'portal-vue/dist/portal-vue.esm.js',
DB_HANDLERS_ELECTRON_RENDERER_OR_WEB$: path.resolve(__dirname, '../src/datastores/handlers/web.js'),
// change to "shaka-player.ui.debug.js" to get debug logs (update jsconfig to get updated types)

View File

@@ -33,9 +33,9 @@ export default [
ts: false,
}),
js.configs.recommended,
...eslintPluginVue.configs['flat/vue2-recommended'],
...eslintPluginVue.configs['flat/recommended'],
...vuejsAccessibility.configs["flat/recommended"],
...intlifyVueI18N.configs['flat/recommended'],
...intlifyVueI18N.configs['recommended'],
{
files: [
'**/*.{js,vue}',
@@ -63,7 +63,7 @@ export default [
settings: {
'vue-i18n': {
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
messageSyntaxVersion: '^8.0.0',
messageSyntaxVersion: '^11.0.0',
},
},
@@ -79,7 +79,6 @@ export default [
'no-unused-vars': 'warn',
'no-undef': 'warn',
'object-shorthand': 'off',
'vue/no-template-key': 'warn',
'vue/multi-word-component-names': 'off',
'vuejs-accessibility/label-has-for': ['error', {
@@ -119,8 +118,6 @@ export default [
ignoreText: ['-', '•', '/', 'YouTube', 'Invidious', 'FreeTube'],
}],
'@intlify/vue-i18n/no-deprecated-tc': 'off',
'vue/require-explicit-emits': 'error',
'vue/no-unused-emit-declarations': 'error',
'jsdoc/check-alignment': 'error',
@@ -166,7 +163,7 @@ export default [
settings: {
'vue-i18n': {
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
messageSyntaxVersion: '^8.0.0',
messageSyntaxVersion: '^11.0.0',
},
},
},
@@ -191,7 +188,7 @@ export default [
settings: {
'vue-i18n': {
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
messageSyntaxVersion: '^8.0.0',
messageSyntaxVersion: '^11.0.0',
},
},
},
@@ -210,7 +207,7 @@ export default [
settings: {
'vue-i18n': {
localeDir: `./static/locales/{${activeLocales.join(',')}}.yaml`,
messageSyntaxVersion: '^8.0.0',
messageSyntaxVersion: '^11.0.0',
},
},
},

View File

@@ -1,6 +1,6 @@
{
"vueCompilerOptions": {
"target": 2.7
"target": 3.5
},
"compilerOptions": {
"strictNullChecks": true,

View File

@@ -58,21 +58,21 @@
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-regular-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/vue-fontawesome": "^2.0.10",
"@fortawesome/vue-fontawesome": "^3.0.8",
"@seald-io/nedb": "^4.1.2",
"autolinker": "^4.1.5",
"bgutils-js": "^3.2.0",
"electron-context-menu": "^4.1.1",
"marked": "^16.4.1",
"portal-vue": "^2.1.7",
"portal-vue": "^3.0.0",
"process": "^0.11.10",
"shaka-player": "^4.16.6",
"swiper": "^12.0.3",
"vue": "^2.7.16",
"vue-i18n": "^8.28.2",
"vue-observe-visibility": "^1.0.0",
"vue-router": "^3.6.5",
"vuex": "^3.6.2",
"vue": "^3.5.22",
"vue-i18n": "^11.1.12",
"vue-observe-visibility": "^2.0.0-alpha.1",
"vue-router": "^4.6.3",
"vuex": "^4.1.0",
"youtubei.js": "^16.0.1"
},
"devDependencies": {
@@ -80,7 +80,7 @@
"@babel/preset-env": "^7.28.5",
"@double-great/stylelint-a11y": "^3.4.0",
"@eslint/js": "^9.38.0",
"@intlify/eslint-plugin-vue-i18n": "^3.2.0",
"@intlify/eslint-plugin-vue-i18n": "^4.1.0",
"babel-loader": "^10.0.0",
"copy-webpack-plugin": "^13.0.1",
"css-loader": "^7.1.2",
@@ -112,9 +112,8 @@
"stylelint-high-performance-animation": "^1.11.0",
"stylelint-use-logical-spec": "^5.0.1",
"tree-kill": "1.2.2",
"vue-devtools": "^5.1.4",
"vue-eslint-parser": "^10.2.0",
"vue-loader": "^15.10.0",
"vue-loader": "^17.4.2",
"webpack": "^5.102.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2",

View File

@@ -22,9 +22,21 @@ if (process.env.IS_ELECTRON_MAIN) {
dbPath = (dbName) => `${dbName}.db`
}
export const settings = new Datastore({ filename: dbPath('settings'), autoload: !process.env.IS_ELECTRON_MAIN })
export const profiles = new Datastore({ filename: dbPath('profiles'), autoload: !process.env.IS_ELECTRON_MAIN })
export const playlists = new Datastore({ filename: dbPath('playlists'), autoload: !process.env.IS_ELECTRON_MAIN })
export const history = new Datastore({ filename: dbPath('history'), autoload: !process.env.IS_ELECTRON_MAIN })
export const searchHistory = new Datastore({ filename: dbPath('search-history'), autoload: !process.env.IS_ELECTRON_MAIN })
export const subscriptionCache = new Datastore({ filename: dbPath('subscription-cache'), autoload: !process.env.IS_ELECTRON_MAIN })
/**
* @param {string} name
*/
function createDatastore(name) {
return new Datastore({
filename: dbPath(name),
autoload: !process.env.IS_ELECTRON_MAIN,
// Automatically clean up corrupted data, instead of crashing
corruptAlertThreshold: 1
})
}
export const settings = createDatastore('settings')
export const profiles = createDatastore('profiles')
export const playlists = createDatastore('playlists')
export const history = createDatastore('history')
export const searchHistory = createDatastore('search-history')
export const subscriptionCache = createDatastore('subscription-cache')

View File

@@ -710,14 +710,6 @@ function runApp() {
await createWindow()
if (process.env.NODE_ENV === 'development') {
try {
require('vue-devtools').install()
} catch (err) {
console.error(err)
}
}
if (isDebug) {
mainWindow.webContents.openDevTools()
}

View File

@@ -52,15 +52,31 @@ export async function generatePoToken(videoId, context, proxyUrl) {
callback({ requestHeaders })
})
theSession.webRequest.onHeadersReceived({ urls: ['https://*/*'] }, ({ responseHeaders }, callback) => {
if (responseHeaders) {
callback({
responseHeaders: {
...responseHeaders,
'Access-Control-Allow-Origin': ['*'],
'Access-Control-Allow-Methods': ['GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH']
}
})
}
})
theSession.webRequest.onBeforeRequest({ urls: ['<all_urls>'], types: ['cspReport', 'ping'] }, (details, callback) => {
callback({ cancel: true })
})
const webContentsView = new WebContentsView({
webPreferences: {
backgroundThrottling: false,
safeDialogs: true,
sandbox: true,
contextIsolation: true,
v8CacheOptions: 'none',
session: theSession,
offscreen: true,
webSecurity: false,
disableBlinkFeatures: 'ElectronCSSCornerSmoothing'
}
})

View File

@@ -56,8 +56,8 @@
transition: opacity 0.15s;
}
.fade-enter,
.fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

View File

@@ -209,18 +209,16 @@ export default defineComponent({
}, 500)
})
this.$router.onReady(() => {
if (this.$router.currentRoute.path === '/') {
this.$router.replace({ path: this.landingPage })
}
if (this.$route.path === '/') {
this.$router.replace({ path: this.landingPage })
}
this.setWindowTitle()
})
this.setWindowTitle()
})
document.addEventListener('dragstart', this.handleDragStart)
},
beforeDestroy: function () {
beforeUnmount: function () {
document.removeEventListener('dragstart', this.handleDragStart)
},
methods: {
@@ -332,8 +330,8 @@ export default defineComponent({
this.showBlogBanner = false
},
handlePromptPortalUpdate: function(newVal) {
this.isPromptOpen = newVal
handlePromptPortalUpdate: function(data) {
this.isPromptOpen = data.hasContent
},
openDownloadsPage: function () {

View File

@@ -1,7 +1,6 @@
<template>
<div
v-if="dataReady"
id="app"
class="app"
:class="{
hideOutlines: outlinesHidden,
@@ -101,17 +100,18 @@
@click="handleNewBlogBannerClick"
/>
</div>
<transition
<RouterView
v-if="dataReady"
mode="out-in"
name="fade"
v-slot="{ Component }"
class="routerView"
>
<!-- <keep-alive> -->
<RouterView
class="routerView"
/>
<!-- </keep-alive> -->
</transition>
<Transition
mode="out-in"
name="fade"
>
<component :is="Component" />
</Transition>
</RouterView>
</ft-flex-box>
</div>
</template>

View File

@@ -42,7 +42,7 @@
v-if="subCount !== null && !hideChannelSubscriptions"
class="subCount"
>
{{ $tc('Global.Counts.Subscriber Count', subCount, { count: formattedSubCount }) }}
{{ $t('Global.Counts.Subscriber Count', { count: formattedSubCount }, subCount) }}
</p>
</div>
</div>
@@ -81,7 +81,7 @@
class="tab"
:class="{ selectedTab: currentTab === 'home' }"
role="tab"
:aria-selected="String(currentTab === 'home')"
:aria-selected="currentTab === 'home'"
aria-controls="homePanel"
:tabindex="(currentTab === 'home' || currentTab === 'search') ? 0 : -1"
@click="changeTab('home')"
@@ -97,7 +97,7 @@
class="tab"
:class="{ selectedTab: currentTab === 'videos' }"
role="tab"
:aria-selected="String(currentTab === 'videos')"
:aria-selected="currentTab === 'videos'"
aria-controls="videoPanel"
:tabindex="(currentTab === 'videos' || currentTab === 'search') ? 0 : -1"
@click="changeTab('videos')"
@@ -113,7 +113,7 @@
class="tab"
:class="{ selectedTab: currentTab === 'shorts' }"
role="tab"
:aria-selected="String(currentTab === 'shorts')"
:aria-selected="currentTab === 'shorts'"
aria-controls="shortPanel"
:tabindex="currentTab === 'shorts' ? 0 : -1"
@click="changeTab('shorts')"
@@ -129,7 +129,7 @@
class="tab"
:class="{ selectedTab: currentTab === 'live' }"
role="tab"
:aria-selected="String(currentTab === 'live')"
:aria-selected="currentTab === 'live'"
aria-controls="livePanel"
:tabindex="currentTab === 'live' ? 0 : -1"
@click="changeTab('live')"
@@ -144,7 +144,7 @@
id="releasesTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'releases')"
:aria-selected="currentTab === 'releases'"
aria-controls="releasePanel"
:tabindex="currentTab === 'releases' ? 0 : -1"
:class="{ selectedTab: currentTab === 'releases' }"
@@ -160,7 +160,7 @@
id="podcastsTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'podcasts')"
:aria-selected="currentTab === 'podcasts'"
aria-controls="podcastPanel"
:tabindex="currentTab === 'podcasts' ? 0 : -1"
:class="{ selectedTab: currentTab === 'podcasts' }"
@@ -176,7 +176,7 @@
id="coursesTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'courses')"
:aria-selected="currentTab === 'courses'"
aria-controls="coursesPanel"
:tabindex="currentTab === 'courses' ? 0 : -1"
:class="{ selectedTab: currentTab === 'courses' }"
@@ -192,7 +192,7 @@
id="playlistsTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'playlists')"
:aria-selected="currentTab === 'playlists'"
aria-controls="playlistPanel"
:tabindex="currentTab === 'playlists' ? 0 : -1"
:class="{ selectedTab: currentTab === 'playlists' }"
@@ -208,7 +208,7 @@
id="communityTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'community')"
:aria-selected="currentTab === 'community'"
aria-controls="communityPanel"
:tabindex="currentTab === 'community' ? 0 : -1"
:class="{ selectedTab: currentTab === 'community' }"
@@ -223,7 +223,7 @@
id="aboutTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'about')"
:aria-selected="currentTab === 'about'"
aria-controls="aboutPanel"
:tabindex="currentTab === 'about' ? 0 : -1"
:class="{ selectedTab: currentTab === 'about' }"

View File

@@ -65,7 +65,7 @@
<script setup>
import { computed, ref } from 'vue'
import { useI18n } from '../composables/use-i18n-polyfill'
import { useRouter } from 'vue-router/composables'
import { useRouter } from 'vue-router'
import FtButton from './FtButton/FtButton.vue'
import FtFlexBox from './ft-flex-box/ft-flex-box.vue'

View File

@@ -82,7 +82,7 @@
:tag-name-placeholder="t('Settings.Distraction Free Settings.Hide Videos and Playlists Containing Text Placeholder')"
:show-tags="showAddedForbiddenTitles"
:tag-list="forbiddenTitles"
:min-input-length="3"
:min-input-length="1"
:tooltip="t('Tooltips.Distraction Free Settings.Hide Videos and Playlists Containing Text')"
@change="handleForbiddenTitles"
@toggle-show-tags="handleAddedForbiddenTitles"

View File

@@ -58,8 +58,8 @@
</template>
<script setup>
import { useId } from '../../composables/use-id-polyfill'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { useId } from 'vue'
const props = defineProps({
channelId: {

View File

@@ -5,10 +5,10 @@
</h3>
<template
v-for="(label, index) in labels"
:key="values[index]"
>
<input
:id="id + values[index]"
:key="'value' + values[index]"
v-model="modelValue"
:name="id"
:value="values[index]"
@@ -17,7 +17,6 @@
type="checkbox"
>
<label
:key="'label' + values[index]"
:for="id + values[index]"
>
{{ label }}
@@ -27,12 +26,11 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { useId } from 'vue'
const id = useId()
const props = defineProps({
defineProps({
title: {
type: String,
required: true
@@ -49,39 +47,9 @@ const props = defineProps({
type: Boolean,
default: false
},
// Required for v-model in the parent component (https://v2.vuejs.org/v2/guide/components#Using-v-model-on-Components)
// Do not rename or remove
// TODO: Replace with defineModel in Vue 3
value: {
type: Array,
required: true
}
})
// Required for v-model in the parent component (https://v2.vuejs.org/v2/guide/components#Using-v-model-on-Components)
// Do not rename or remove
// TODO: Replace with defineModel in Vue 3
const emit = defineEmits(['input'])
/** @type {import('vue').Ref<string[]>} */
const modelValue = ref(props.value)
watch(
modelValue,
(newValue) => {
emit('input', newValue)
},
{ deep: true }
)
watch(
() => props.value,
(newValue) => {
modelValue.value = newValue
},
{ deep: true }
)
const modelValue = defineModel({ type: Array, required: true })
</script>
<style scoped src="./FtCheckboxList.css" />

View File

@@ -117,8 +117,8 @@
>
<span
class="likeCount"
:title="$tc('Global.Counts.Like Count', voteCount, {count: formattedVoteCount})"
:aria-label="$tc('Global.Counts.Like Count', voteCount, {count: formattedVoteCount})"
:title="$t('Global.Counts.Like Count', {count: formattedVoteCount}, voteCount)"
:aria-label="$t('Global.Counts.Like Count', {count: formattedVoteCount}, voteCount)"
>
<FontAwesomeIcon
class="thumbs-up-icon"
@@ -136,8 +136,8 @@
>
<span
class="commentCount"
:title="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
:aria-label="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
:title="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
:aria-label="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
>
<FontAwesomeIcon
class="comment-count-icon"
@@ -148,8 +148,8 @@
<span
v-else-if="commentCount != null"
class="commentCount"
:title="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
:aria-label="$tc('Global.Counts.Comment Count', commentCount, {count: formattedCommentCount})"
:title="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
:aria-label="$t('Global.Counts.Comment Count', {count: formattedCommentCount}, commentCount)"
>
<FontAwesomeIcon
class="comment-count-icon"

View File

@@ -79,8 +79,7 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { ref } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { ref, useId } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import FtInput from '../ft-input/ft-input.vue'

View File

@@ -44,14 +44,14 @@
class="subscriberCount"
>
<template v-if="handle !== null"> </template>
{{ $tc('Global.Counts.Subscriber Count', subscriberCount, {count: formattedSubscriberCount}) }}
{{ $t('Global.Counts.Subscriber Count', {count: formattedSubscriberCount}, subscriberCount) }}
</span>
<span
v-if="handle == null && videoCount != null"
class="videoCount"
>
<template v-if="subscriberCount !== null && !hideChannelSubscriptions"> </template>
{{ $tc('Global.Counts.Video Count', videoCount, {count: formattedVideoCount}) }}
{{ $t('Global.Counts.Video Count', {count: formattedVideoCount}, videoCount) }}
</span>
</div>
<p

View File

@@ -34,14 +34,14 @@
v-if="channelCount"
class="channelCount"
>
{{ $tc('Global.Counts.Channel Count', channelCount, {count: formattedChannelCount}) }}
{{ $t('Global.Counts.Channel Count', {count: formattedChannelCount}, channelCount) }}
</span>
<span
v-if="videoCount"
class="videoCount"
>
<template v-if="channelCount"> </template>
{{ $tc('Global.Counts.Video Count', videoCount, {count: formattedVideosCount}) }}
{{ $t('Global.Counts.Video Count', {count: formattedVideosCount}, videoCount) }}
</span>
</div>
</div>

View File

@@ -29,7 +29,7 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { useId } from '../../composables/use-id-polyfill'
import { useId } from 'vue'
defineProps({
message: {

View File

@@ -25,8 +25,7 @@
</template>
<script setup>
import { computed } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { computed, useId } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { getFirstCharacter } from '../../helpers/strings'

View File

@@ -222,14 +222,15 @@ function handleDeletePromptClick(value) {
showToast(t('Profile.Profile has been updated'))
selectNone()
} else {
/** @type {Profile} */
const profile = deepCopy(props.profile)
subscriptions.value = subscriptions.value.filter((channel) => {
return !selected_.includes(channel.id)
})
profile.subscriptions = subscriptions.value
/** @type {Profile} */
const profile = {
...props.profile,
subscriptions: deepCopy(subscriptions.value)
}
store.dispatch('updateProfile', profile)

View File

@@ -48,7 +48,7 @@
:show-action-button="false"
:maxlength="100"
@input="profileName = $event"
@keydown.enter.native="saveProfile"
@keydown.enter="saveProfile"
/>
</div>
<div>
@@ -121,7 +121,7 @@ import store from '../../store/index'
import { MAIN_PROFILE_ID } from '../../../constants'
import { calculateColorLuminance, colors } from '../../helpers/colors'
import { showToast } from '../../helpers/utils'
import { deepCopy, showToast } from '../../helpers/utils'
import { getFirstCharacter } from '../../helpers/strings'
/**
@@ -204,7 +204,7 @@ function saveProfile() {
name: profileName.value,
bgColor: profileBgColor.value,
textColor: profileTextColor.value,
subscriptions: props.profile.subscriptions
subscriptions: deepCopy(props.profile.subscriptions)
}
if (!props.isNew) {

View File

@@ -26,8 +26,8 @@
ref="profileListRef"
class="profileList"
tabindex="-1"
@focusout.native="handleProfileListFocusOut"
@keydown.native.esc.stop="handleProfileListEscape"
@focusout="handleProfileListFocusOut"
@keydown.esc.stop="handleProfileListEscape"
>
<h3
:id="id + 'title'"
@@ -80,10 +80,9 @@
</template>
<script setup>
import { computed, nextTick, ref } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { computed, nextTick, ref, useId } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { useRouter } from 'vue-router/composables'
import { useRouter } from 'vue-router'
import FtCard from '../ft-card/ft-card.vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'

View File

@@ -56,8 +56,7 @@
</template>
<script setup>
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { nextTick, onBeforeUnmount, onMounted, ref, useId } from 'vue'
import store from '../../store/index'

View File

@@ -5,20 +5,18 @@
</h3>
<template
v-for="(label, index) in labels"
:key="values[index]"
>
<input
:id="id + values[index]"
:key="'value' + values[index]"
v-model="modelValue"
:name="id"
:value="values[index]"
:checked="value === values[index]"
:disabled="disabled"
class="radio"
type="radio"
@change="handleChange(values[index])"
>
<label
:key="'label' + values[index]"
:for="id + values[index]"
>
{{ label }}
@@ -28,7 +26,7 @@
</template>
<script setup>
import { useId } from '../../composables/use-id-polyfill'
import { useId } from 'vue'
const id = useId()
@@ -49,27 +47,9 @@ defineProps({
type: Boolean,
default: false
},
// Required for v-model in the parent component (https://v2.vuejs.org/v2/guide/components#Using-v-model-on-Components)
// Do not rename or remove
// TODO: Replace with defineModel in Vue 3
value: {
type: String,
required: true
}
})
// Required for v-model in the parent component (https://v2.vuejs.org/v2/guide/components#Using-v-model-on-Components)
// Do not rename or remove
// TODO: Replace with defineModel in Vue 3
const emit = defineEmits(['input'])
/**
* @param {string} value
*/
function handleChange(value) {
emit('input', value)
}
const modelValue = defineModel({ type: String, required: true })
</script>
<style scoped src="./FtRadioButton.css" />

View File

@@ -47,7 +47,7 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { useId } from '../../composables/use-id-polyfill'
import { useId } from 'vue'
import FtTooltip from '../FtTooltip/FtTooltip.vue'

View File

@@ -20,8 +20,7 @@
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { useId } from '../../composables/use-id-polyfill.js'
import { computed, ref, useId, watch } from 'vue'
const props = defineProps({
label: {

View File

@@ -30,8 +30,7 @@
</template>
<script setup>
import { computed } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { computed, useId } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import FtSelect from '../FtSelect/FtSelect.vue'

View File

@@ -90,8 +90,7 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, ref, shallowRef } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { computed, ref, shallowRef, useId } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import FtButton from '../FtButton/FtButton.vue'
@@ -246,7 +245,7 @@ function handleSubscription(profile) {
const subscribeButton = ref(null)
function handleProfileDropdownFocusOut() {
if (!subscribeButton.value.matches(':focus-within')) {
if (subscribeButton.value && !subscribeButton.value.matches(':focus-within')) {
isProfileDropdownOpen.value = false
}
}

View File

@@ -7,7 +7,7 @@
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router/composables'
import { useRouter } from 'vue-router'
const props = defineProps({
inputHtml: {
@@ -21,7 +21,7 @@ const props = defineProps({
})
const router = useRouter()
const videoId = router.currentRoute.params.id
const videoId = router.currentRoute.value.params.id
/** @type {import('vue').ComputedRef<string>} */
const displayText = computed(() => props.inputHtml.replaceAll(/(?:(\d+):)?(\d+):(\d+)/g, (timestamp, hours, minutes, seconds) => {

View File

@@ -19,7 +19,7 @@
</template>
<script setup>
import { nextTick, onBeforeUnmount, onMounted, shallowReactive } from 'vue'
import { nextTick, onBeforeUnmount, onMounted, reactive } from 'vue'
import { ToastEventBus } from '../../helpers/utils'
let idCounter = 0
@@ -34,21 +34,23 @@ let idCounter = 0
* @property {number} id
*/
/** @type {import('vue').ShallowReactive<Toast[]>} */
const toasts = shallowReactive([])
/** @type {import('vue').Reactive<Toast[]>} */
const toasts = reactive([])
/**
* @param {CustomEvent<{ message: string | (({elapsedMs: number, remainingMs: number}) => string), time: number | null, action: Function | null, abortSignal: AbortSignal | null }>} event
*/
function open({ detail: { message, time, action, abortSignal } }) {
const id = idCounter++
/** @type {Toast} */
const toast = {
id,
message,
action,
isOpen: false,
timeout: 0,
interval: 0,
id: idCounter++
interval: 0
}
time ||= 3000
let elapsed = 0
@@ -60,7 +62,14 @@ function open({ detail: { message, time, action, abortSignal } }) {
elapsed += updateDelay
// Skip last update
if (elapsed >= time) { return }
toast.message = message({ elapsedMs: elapsed, remainingMs: time - elapsed })
// We need to locate the object in the array so we get the reactive proxy,
// as modifying the original object won't trigger reactive effects such as updating the DOM
const toast = toasts.find(t => t.id === id)
if (toast) {
toast.message = message({ elapsedMs: elapsed, remainingMs: time - elapsed })
}
}, updateDelay)
}
@@ -72,7 +81,13 @@ function open({ detail: { message, time, action, abortSignal } }) {
}
nextTick(() => {
toast.isOpen = true
// We need to locate the object in the array so we get the reactive proxy,
// as modifying the original object won't trigger reactive effects such as updating the DOM
const toast = toasts.find(t => t.id === id)
if (toast) {
toast.isOpen = true
}
})
if (toasts.length > 4) {

View File

@@ -36,8 +36,7 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { useId } from '../../composables/use-id-polyfill'
import { ref, useId, watch } from 'vue'
import FtTooltip from '../FtTooltip/FtTooltip.vue'

View File

@@ -23,7 +23,7 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { useId } from '../../composables/use-id-polyfill'
import { useId } from 'vue'
defineProps({
position: {

View File

@@ -22,7 +22,7 @@
input-type="password"
:value="password"
@input="e => password = e"
@keydown.enter.native="handleSetPassword"
@keydown.enter="handleSetPassword"
/>
<FtButton
class="centerButton"

View File

@@ -45,7 +45,7 @@
:value="newTitle"
:maxlength="255"
@input="handlePlaylistNameInput"
@keydown.enter.native="savePlaylistInfo"
@keydown.enter="savePlaylistInfo"
/>
<FtFlexBox v-if="inputPlaylistNameBlank">
<p>
@@ -67,9 +67,9 @@
{{ title }}
</h2>
<p>
{{ $tc('Global.Counts.Video Count', videoCount, { count: parsedVideoCount }) }}
{{ t('Global.Counts.Video Count', { count: parsedVideoCount }, videoCount) }}
<span v-if="!hideViews && !isUserPlaylist">
- {{ $tc('Global.Counts.View Count', viewCount, { count: parsedViewCount }) }}
- {{ t('Global.Counts.View Count', { count: parsedViewCount }, viewCount) }}
</span>
<span>- </span>
<span v-if="infoSource !== 'local'">
@@ -92,7 +92,7 @@
:show-label="false"
:value="newDescription"
@input="(input) => newDescription = input"
@keydown.enter.native="savePlaylistInfo"
@keydown.enter="savePlaylistInfo"
/>
<p
v-else
@@ -265,7 +265,7 @@
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { useRouter } from 'vue-router/composables'
import { useRouter } from 'vue-router'
import FtFlexBox from '../ft-flex-box/ft-flex-box.vue'
import FtIconButton from '../ft-icon-button/ft-icon-button.vue'
@@ -282,6 +282,7 @@ import {
showToast,
getTodayDateStrLocalTimezone,
writeFileWithPicker,
deepCopy,
} from '../../helpers/utils'
import thumbnailPlaceholder from '../../assets/img/thumbnail_placeholder.svg'
@@ -481,7 +482,7 @@ const userPlaylistAnyVideoWatched = computed(() => {
const historyCacheById_ = historyCacheById.value
return selectedUserPlaylist.value.videos.some((video) => {
return Object.hasOwn(historyCacheById_, video.videoId)
return historyCacheById_[video.videoId] !== undefined
})
})
@@ -594,7 +595,7 @@ async function savePlaylistInfo() {
playlistName: newTitle.value,
protected: selectedUserPlaylist.value.protected,
description: newDescription.value,
videos: selectedUserPlaylist.value.videos,
videos: deepCopy(selectedUserPlaylist.value.videos),
_id: props.id,
}
try {
@@ -740,7 +741,7 @@ const userPlaylistWatchedVideoCount = computed(() => {
const historyCacheById_ = historyCacheById.value
return selectedUserPlaylist.value.videos.reduce((count, video) => {
return Object.hasOwn(historyCacheById_, video.videoId) ? count + 1 : count
return historyCacheById_[video.videoId] !== undefined ? count + 1 : count
}, 0)
})
@@ -787,7 +788,7 @@ async function handleRemoveDuplicateVideosPromptAnswer(option) {
playlistName: props.title,
protected: selectedUserPlaylist.value.protected,
description: props.description,
videos: newVideoItems,
videos: deepCopy(newVideoItems),
_id: props.id,
}
try {
@@ -824,7 +825,7 @@ async function handleRemoveVideosOnWatchPromptAnswer(option) {
playlistName: props.title,
protected: selectedUserPlaylist.value.protected,
description: props.description,
videos: videosToWatch,
videos: deepCopy(videosToWatch),
_id: props.id
}
try {

View File

@@ -41,7 +41,7 @@
show-label
:value="proxyHostname"
@input="handleUpdateProxyHostname"
@keydown.enter.native="testProxy"
@keydown.enter="testProxy"
/>
<FtInput
:placeholder="$t('Settings.Proxy Settings.Proxy Port Number')"
@@ -50,7 +50,7 @@
:value="proxyPort"
:maxlength="5"
@input="handleUpdateProxyPort"
@keydown.enter.native="testProxy"
@keydown.enter="testProxy"
/>
</FtFlexBox>
<FtFlexBox>
@@ -60,7 +60,7 @@
show-label
:value="proxyUsername"
@input="handleUpdateProxyUsername"
@keydown.enter.native="testProxy"
@keydown.enter="testProxy"
/>
<FtInput
:placeholder="$t('Settings.Proxy Settings.Proxy Password')"
@@ -69,7 +69,7 @@
:value="proxyPassword"
input-type="password"
@input="handleUpdateProxyPassword"
@keydown.enter.native="testProxy"
@keydown.enter="testProxy"
/>
</FtFlexBox>
<p

View File

@@ -198,7 +198,7 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
import router from '../../router/index.js'
import { useRouter } from 'vue-router'
import store from '../../store/index'
@@ -238,6 +238,8 @@ function handleClickOutside(event) {
}
}
const router = useRouter()
onMounted(() => {
document.addEventListener('click', handleClickOutside)
router.afterEach(() => {

View File

@@ -152,7 +152,7 @@ const hideWatchedSubs = computed(() => {
const filteredVideoList = computed(() => {
if (hideWatchedSubs.value && !props.isCommunity) {
return props.videoList.filter((video) => {
return !Object.hasOwn(historyCacheById.value, video.videoId)
return historyCacheById.value[video.videoId] === undefined
})
} else {
return props.videoList

View File

@@ -121,7 +121,7 @@
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { useRoute, useRouter } from 'vue-router/composables'
import { useRoute, useRouter } from 'vue-router'
import FtInput from '../ft-input/ft-input.vue'
import FtProfileSelector from '../FtProfileSelector/FtProfileSelector.vue'

View File

@@ -20,7 +20,7 @@
:input-html="processedShownDescription"
:link-tab-index="linkTabIndex"
@timestamp-event="onTimestamp"
@click.native="expandDescriptionWithClick"
@click="expandDescriptionWithClick"
/>
<span
v-if="license && showFullDescription"

View File

@@ -87,7 +87,7 @@ export default defineComponent({
window.addEventListener('resize', this.handleResize)
}
},
beforeDestroy: function () {
beforeUnmount: function () {
if (this.dropdownModalOnMobile) {
window.removeEventListener('resize', this.handleResize)
}

View File

@@ -1,4 +1,4 @@
import { defineComponent } from 'vue'
import { defineComponent, useId } from 'vue'
import { mapActions } from 'vuex'
import FtTooltip from '../FtTooltip/FtTooltip.vue'
@@ -77,13 +77,19 @@ export default defineComponent({
}
},
emits: ['clear', 'click', 'input', 'remove'],
setup() {
const id = useId()
return {
id
}
},
data: function () {
let actionIcon = ['fas', 'search']
if (this.forceActionButtonIconName !== null) {
actionIcon = this.forceActionButtonIconName
}
return {
id: '',
inputData: '',
searchState: {
showOptions: false,
@@ -152,7 +158,6 @@ export default defineComponent({
}
},
created: function () {
this.id = this._uid
this.inputData = this.value
this.updateVisibleDataList()
},

View File

@@ -15,7 +15,7 @@
class="thumbnailLink"
tabindex="-1"
:to="watchVideoRouterLink"
@click.native="handleWatchPageLinkClick"
@click="handleWatchPageLinkClick"
>
<img
:src="thumbnail"
@@ -113,7 +113,7 @@
<router-link
class="title"
:to="watchVideoRouterLink"
@click.native="handleWatchPageLinkClick"
@click="handleWatchPageLinkClick"
>
<h3 class="h3Title">
{{ displayTitle }}
@@ -135,7 +135,7 @@
class="viewCount"
>
<template v-if="channelId !== null || channelName !== null"> </template>
{{ $tc('Global.Counts.View Count', viewCount, {count: parsedViewCount}) }}
{{ $t('Global.Counts.View Count', {count: parsedViewCount}, viewCount) }}
</span>
<span
v-if="uploadedTime !== '' && !isLive"
@@ -144,7 +144,7 @@
<span
v-if="isLive && !hideViews"
class="viewCount"
> {{ $tc('Global.Counts.Watching Count', viewCount, {count: parsedViewCount}) }}</span>
> {{ $t('Global.Counts.Watching Count', {count: parsedViewCount}, viewCount) }}</span>
</div>
<div
v-if="is4k || hasCaptions || is8k || isNew || isVr180 || isVr360 || is3D"

View File

@@ -1192,16 +1192,18 @@ export default defineComponent({
}
function handleTimeupdate() {
const currentTime = video.value.currentTime
if (video.value) {
const currentTime = video.value.currentTime
emit('timeupdate', currentTime)
emit('timeupdate', currentTime)
if (showStats.value && hasLoaded.value) {
updateStats()
}
if (showStats.value && hasLoaded.value) {
updateStats()
}
if (useSponsorBlock.value && sponsorBlockSegments.length > 0 && canSeek()) {
skipSponsorBlockSegments(currentTime)
if (useSponsorBlock.value && sponsorBlockSegments.length > 0 && canSeek()) {
skipSponsorBlockSegments(currentTime)
}
}
}
@@ -2287,16 +2289,18 @@ export default defineComponent({
}
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.LAST_FRAME:
// `⌘+,` is for settings in MacOS
if (!event.metaKey) {
if (!event.metaKey && video_.paused) {
event.preventDefault()
// Return to previous frame
frameByFrame(-1)
}
break
case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.NEXT_FRAME:
event.preventDefault()
// Advance to next frame
frameByFrame(1)
if (video_.paused) {
event.preventDefault()
// Advance to next frame
frameByFrame(1)
}
break
case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.STATS:
// Toggle stats display

View File

@@ -133,7 +133,7 @@ export class AudioTrackSelection extends shaka.ui.SettingsMenu {
updateLocalisedStrings_() {
this.backButton.ariaLabel = this.localization.resolve('BACK')
const audioTracksText = i18n.t('Video.Player.Audio Tracks')
const audioTracksText = i18n.global.t('Video.Player.Audio Tracks')
this.button.ariaLabel = audioTracksText
this.nameSpan.textContent = audioTracksText

View File

@@ -67,12 +67,12 @@ export class AutoplayToggle extends shaka.ui.Element {
/** @private */
updateLocalisedStrings_() {
this.nameSpan_.textContent = i18n.t('Video.Autoplay')
this.nameSpan_.textContent = i18n.global.t('Video.Autoplay')
this.icon_.use(this.autoplayEnabled_ ? PlayerIcons.PLAY_CIRCLE_FILLED : PlayerIcons.PAUSE_CIRCLE_FILLED)
this.currentState_.textContent = this.localization.resolve(this.autoplayEnabled_ ? 'ON' : 'OFF')
this.button_.ariaLabel = this.autoplayEnabled_ ? i18n.t('Video.Player.Autoplay is on') : i18n.t('Video.Player.Autoplay is off')
this.button_.ariaLabel = this.autoplayEnabled_ ? i18n.global.t('Video.Player.Autoplay is on') : i18n.global.t('Video.Player.Autoplay is off')
}
}

View File

@@ -70,7 +70,7 @@ export class FullWindowButton extends shaka.ui.Element {
this.icon_.use(this.fullWindowEnabled_ ? PlayerIcons.CLOSE_FULLSCREEN_FILLED : PlayerIcons.OPEN_IN_FULL_FILLED)
this.currentState_.textContent = this.localization.resolve(this.fullWindowEnabled_ ? 'ON' : 'OFF')
const baseAriaLabel = this.fullWindowEnabled_ ? i18n.t('Video.Player.Exit Full Window') : i18n.t('Video.Player.Full Window')
const baseAriaLabel = this.fullWindowEnabled_ ? i18n.global.t('Video.Player.Exit Full Window') : i18n.global.t('Video.Player.Full Window')
const newLabel = addKeyboardShortcutToActionTitle(
baseAriaLabel,
KeyboardShortcuts.VIDEO_PLAYER.GENERAL.FULLWINDOW

View File

@@ -51,7 +51,7 @@ export class ScreenshotButton extends shaka.ui.Element {
/** @private */
updateLocalisedStrings_() {
const label = addKeyboardShortcutToActionTitle(
i18n.t('Video.Player.Take Screenshot'),
i18n.global.t('Video.Player.Take Screenshot'),
KeyboardShortcuts.VIDEO_PLAYER.GENERAL.TAKE_SCREENSHOT
)
this.nameSpan_.textContent = this.button_.ariaLabel = label

View File

@@ -65,7 +65,7 @@ export class StatsButton extends shaka.ui.Element {
updateLocalisedStrings_() {
this.icon_.use(this.showStats_ ? PlayerIcons.INSERT_CHART_FILLED : PlayerIcons.INSERT_CHART_DEFAULT)
const baseLabel = this.showStats_ ? i18n.t('Video.Player.Hide Stats') : i18n.t('Video.Player.Show Stats')
const baseLabel = this.showStats_ ? i18n.global.t('Video.Player.Hide Stats') : i18n.global.t('Video.Player.Show Stats')
const label = addKeyboardShortcutToActionTitle(
baseLabel,
KeyboardShortcuts.VIDEO_PLAYER.GENERAL.STATS

View File

@@ -71,7 +71,7 @@ export class TheatreModeButton extends shaka.ui.Element {
this.currentState_.textContent = this.localization.resolve(this.theatreModeEnabled_ ? 'ON' : 'OFF')
const baseAriaLabel = this.theatreModeEnabled_ ? i18n.t('Video.Player.Exit Theatre Mode') : i18n.t('Video.Player.Theatre Mode')
const baseAriaLabel = this.theatreModeEnabled_ ? i18n.global.t('Video.Player.Exit Theatre Mode') : i18n.global.t('Video.Player.Theatre Mode')
this.nameSpan_.textContent = this.button_.ariaLabel = addKeyboardShortcutToActionTitle(
baseAriaLabel,

View File

@@ -220,7 +220,7 @@ export default defineComponent({
this.setCurrentInvidiousInstanceBounce =
debounce(this.setCurrentInvidiousInstance, 500)
},
beforeDestroy: function () {
beforeUnmount: function () {
if (this.currentInvidiousInstance === '') {
// FIXME: If we call an action from here, there's no guarantee it will finish
// before the component is destroyed, which could bring up some problems

View File

@@ -219,7 +219,7 @@ export default defineComponent({
return null
}
return this.$tc('Global.Counts.View Count', this.viewCount, { count: formatNumber(this.viewCount) })
return this.$t('Global.Counts.View Count', { count: formatNumber(this.viewCount) }, this.viewCount)
},
dateString: function () {

View File

@@ -218,7 +218,7 @@ export default defineComponent({
navigator.mediaSession.setActionHandler('nexttrack', this.playNextVideo)
}
},
beforeDestroy: function () {
beforeUnmount: function () {
if ('mediaSession' in navigator) {
navigator.mediaSession.setActionHandler('previoustrack', null)
navigator.mediaSession.setActionHandler('nexttrack', null)

View File

@@ -17,10 +17,10 @@ import i18n from '../i18n/index'
export function useI18n() {
const locale = computed({
get() {
return i18n.locale
return i18n.global.locale
},
set(locale) {
i18n.locale = locale
i18n.global.locale = locale
}
})
@@ -74,22 +74,9 @@ export function useI18n() {
*/
/**
* @param {string} key
* @param {number | unknown[] | Record<string, unknown> | undefined} arg1
* @param {number | undefined} arg2
* @param {...any} args
* @returns {string}
*/
function t(key, arg1, arg2) {
// Remove these lines in the Vue 3 migration and pass all args to the `.t()` call
if (typeof arg1 === 'number') {
return i18n.tc(key, arg1)
} else if (typeof arg2 === 'number') {
return i18n.tc(key, arg2, arg1)
}
if (arg1 != null) {
return i18n.t(key, arg1)
}
return i18n.t(key)
function t(...args) {
return i18n.global.t(...args)
}

View File

@@ -1,10 +0,0 @@
// based on https://github.com/vuejs/core/blob/main/packages/runtime-core/src/helpers/useId.ts
let counter = 0
/**
* Polyfill for Vue 3's useId composable in Vue 2
*/
export function useId() {
return `v-0-${counter++}`
}

View File

@@ -98,21 +98,21 @@ export async function getSponsorBlockSegments(videoId, categories) {
export function translateSponsorBlockCategory(category) {
switch (category) {
case 'sponsor':
return i18n.t('Video.Sponsor Block category.sponsor')
return i18n.global.t('Video.Sponsor Block category.sponsor')
case 'intro':
return i18n.t('Video.Sponsor Block category.intro')
return i18n.global.t('Video.Sponsor Block category.intro')
case 'outro':
return i18n.t('Video.Sponsor Block category.outro')
return i18n.global.t('Video.Sponsor Block category.outro')
case 'recap':
return i18n.t('Video.Sponsor Block category.recap')
return i18n.global.t('Video.Sponsor Block category.recap')
case 'selfpromo':
return i18n.t('Video.Sponsor Block category.self-promotion')
return i18n.global.t('Video.Sponsor Block category.self-promotion')
case 'interaction':
return i18n.t('Video.Sponsor Block category.interaction')
return i18n.global.t('Video.Sponsor Block category.interaction')
case 'music_offtopic':
return i18n.t('Video.Sponsor Block category.music offtopic')
return i18n.global.t('Video.Sponsor Block category.music offtopic')
case 'filler':
return i18n.t('Video.Sponsor Block category.filler')
return i18n.global.t('Video.Sponsor Block category.filler')
default:
console.error(`Unknown translation for SponsorBlock category ${category}`)
return category
@@ -131,7 +131,7 @@ export function translateSponsorBlockCategory(category) {
* }[]} captions
*/
export function sortCaptions(captions) {
const currentLocale = i18n.locale
const currentLocale = i18n.global.locale
const userLocale = currentLocale.split('-') // ex. [en,US]
const collator = new Intl.Collator([currentLocale, 'en'])

View File

@@ -32,25 +32,25 @@ export function isKeyboardEventKeyPrintableChar(eventKey) {
export function translateWindowTitle(title) {
switch (title) {
case 'Subscriptions':
return i18n.t('Subscriptions.Subscriptions')
return i18n.global.t('Subscriptions.Subscriptions')
case 'Channels':
return i18n.t('Channels.Title')
return i18n.global.t('Channels.Title')
case 'Trending':
return i18n.t('Trending.Trending')
return i18n.global.t('Trending.Trending')
case 'Most Popular':
return i18n.t('Most Popular')
return i18n.global.t('Most Popular')
case 'Your Playlists':
return i18n.t('User Playlists.Your Playlists')
return i18n.global.t('User Playlists.Your Playlists')
case 'History':
return i18n.t('History.History')
return i18n.global.t('History.History')
case 'Settings':
return i18n.t('Settings.Settings')
return i18n.global.t('Settings.Settings')
case 'About':
return i18n.t('About.About')
return i18n.global.t('About.About')
case 'Profile Settings':
return i18n.t('Profile.Profile Settings')
return i18n.global.t('Profile.Profile Settings')
case 'Playlist':
return i18n.t('Playlist.Playlist')
return i18n.global.t('Playlist.Playlist')
default:
return null
}

View File

@@ -169,6 +169,12 @@ export const ToastEventBus = new EventTarget()
* @param {AbortSignal} abortSignal
*/
export function showToast(message, time = null, action = null, abortSignal = null) {
// Sometimes caller just pass user setting based value in and it can be zero
if (time === 0) {
console.warn('showToast called with time: 0', { message, time, action, abortSignal })
return
}
ToastEventBus.dispatchEvent(new CustomEvent('toast-open', {
detail: {
message,
@@ -200,11 +206,11 @@ export async function copyToClipboard(content, { messageOnSuccess = null, messag
if (messageOnError !== null) {
showToast(`${messageOnError}: ${error}`, 5000)
} else {
showToast(`${i18n.t('Clipboard.Copy failed')}: ${error}`, 5000)
showToast(`${i18n.global.t('Clipboard.Copy failed')}: ${error}`, 5000)
}
}
} else {
showToast(i18n.t('Clipboard.Cannot access clipboard without a secure connection'), 5000)
showToast(i18n.global.t('Clipboard.Cannot access clipboard without a secure connection'), 5000)
}
}
@@ -550,7 +556,7 @@ export function extractNumberFromString(str) {
}
export function showExternalPlayerUnsupportedActionToast(externalPlayer, action) {
const message = i18n.t('Video.External Player.UnsupportedActionTemplate', { externalPlayer, action })
const message = i18n.global.t('Video.External Player.UnsupportedActionTemplate', { externalPlayer, action })
showToast(message)
}
@@ -676,7 +682,7 @@ export function toDistractionFreeTitle(title, minUpperCase = 3) {
* @returns {string}
*/
export function formatNumber(number, options = undefined) {
return Intl.NumberFormat([i18n.locale, 'en'], options).format(number)
return Intl.NumberFormat([i18n.global.locale, 'en'], options).format(number)
}
export function getTodayDateStrLocalTimezone() {
@@ -709,7 +715,7 @@ export function getRelativeTimeFromDate(date, hideSeconds = false, useThirtyDayM
let timeUnit = 'second'
if (timeDiffFromNow < 60 && hideSeconds) {
return i18n.t('Moments Ago')
return i18n.global.t('Moments Ago')
}
if (timeDiffFromNow >= 60) {
@@ -749,7 +755,7 @@ export function getRelativeTimeFromDate(date, hideSeconds = false, useThirtyDayM
// Using `Math.ceil` so that -1.x days ago displayed as 1 day ago
// Notice that the value is turned to negative to be displayed as "ago"
return new Intl.RelativeTimeFormat([i18n.locale, 'en']).format(Math.ceil(-timeDiffFromNow), timeUnit)
return new Intl.RelativeTimeFormat([i18n.global.locale, 'en']).format(Math.ceil(-timeDiffFromNow), timeUnit)
}
/**
@@ -900,23 +906,23 @@ export function getChannelPlaylistId(channelId, type, sortBy) {
function getIndividualLocalizedShortcut(shortcut) {
switch (shortcut) {
case 'alt':
return i18n.t('Keys.alt')
return i18n.global.t('Keys.alt')
case 'ctrl':
return i18n.t('Keys.ctrl')
return i18n.global.t('Keys.ctrl')
case 'shift':
return i18n.t('Keys.shift')
return i18n.global.t('Keys.shift')
case 'enter':
return i18n.t('Keys.enter')
return i18n.global.t('Keys.enter')
case 'plus':
return i18n.t('Keys.plus')
return i18n.global.t('Keys.plus')
case 'arrowleft':
return i18n.t('Keys.arrowleft')
return i18n.global.t('Keys.arrowleft')
case 'arrowright':
return i18n.t('Keys.arrowright')
return i18n.global.t('Keys.arrowright')
case 'arrowup':
return i18n.t('Keys.arrowup')
return i18n.global.t('Keys.arrowup')
case 'arrowdown':
return i18n.t('Keys.arrowdown')
return i18n.global.t('Keys.arrowdown')
default:
return shortcut
}
@@ -961,7 +967,7 @@ export function getLocalizedShortcut(shortcut) {
return shortcutsAsIcons.join('')
} else {
const localizedShortcuts = shortcuts.map((shortcut) => getIndividualLocalizedShortcut(shortcut))
const shortcutJoinOperator = i18n.t('shortcutJoinOperator')
const shortcutJoinOperator = i18n.global.t('shortcutJoinOperator')
return localizedShortcuts.join(shortcutJoinOperator)
}
}
@@ -972,7 +978,7 @@ export function getLocalizedShortcut(shortcut) {
* @returns {string} the localized action title with keyboard shortcut
*/
export function addKeyboardShortcutToActionTitle(actionTitle, shortcut) {
return i18n.t('KeyboardShortcutTemplate', {
return i18n.global.t('KeyboardShortcutTemplate', {
label: actionTitle,
shortcut
})
@@ -989,7 +995,7 @@ export function localizeAndAddKeyboardShortcutToActionTitle(localizedActionTitle
unlocalizedShortcuts = [unlocalizedShortcuts]
}
const localizedShortcuts = unlocalizedShortcuts.map((s) => getLocalizedShortcut(s))
const shortcutLabelSeparator = i18n.t('shortcutLabelSeparator')
const shortcutLabelSeparator = i18n.global.t('shortcutLabelSeparator')
return addKeyboardShortcutToActionTitle(localizedActionTitle, localizedShortcuts.join(shortcutLabelSeparator))
}

View File

@@ -1,15 +1,13 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import { createI18n } from 'vue-i18n'
import { createWebURL } from '../helpers/utils'
// List of locales approved for use
import activeLocales from '../../../static/locales/activeLocales.json'
Vue.use(VueI18n)
const i18n = new VueI18n({
const i18n = createI18n({
locale: 'en-US',
legacy: true,
fallbackLocale: {
// https://kazupon.github.io/vue-i18n/guide/fallback.html#explicit-fallback-with-decision-maps
// https://vue-i18n.intlify.dev/guide/essentials/fallback.html
// es-AR -> es -> en-US
'es-AR': ['es'],
@@ -26,7 +24,8 @@ const i18n = new VueI18n({
export async function loadLocale(locale) {
// don't need to load it if it's already loaded
if (i18n.availableLocales.includes(locale)) {
if (i18n.global.availableLocales.includes(locale) &&
Object.keys(i18n.global.messages[locale]).length > 0) {
return
}
if (!activeLocales.includes(locale)) {
@@ -47,7 +46,7 @@ export async function loadLocale(locale) {
const response = await fetch(url)
const data = await response.json()
i18n.setLocaleMessage(locale, data)
i18n.global.setLocaleMessage(locale, data)
}
// Set by _scripts/ProcessLocalesPlugin.js
@@ -60,10 +59,11 @@ if (process.env.HOT_RELOAD_LOCALES) {
if (message.type === 'freetube-locale-update') {
for (const [locale, data] of message.data) {
// Only update locale data if it was already loaded
if (i18n.availableLocales.includes(locale)) {
if (i18n.global.availableLocales.includes(locale) &&
Object.keys(i18n.global.messages[locale]).length > 0) {
const localeData = JSON.parse(data)
i18n.setLocaleMessage(locale, localeData)
i18n.global.setLocaleMessage(locale, localeData)
}
}
}

View File

@@ -1,5 +1,5 @@
// import the styles
import Vue from 'vue'
import { createApp } from 'vue'
import i18n from './i18n/index'
import router from './router/index'
import store from './store/index'
@@ -133,10 +133,6 @@ import {
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import PortalVue from 'portal-vue'
Vue.config.devtools = process.env.NODE_ENV === 'development'
Vue.config.performance = process.env.NODE_ENV === 'development'
Vue.config.productionTip = process.env.NODE_ENV === 'development'
// Please keep the list of constants sorted by name
// to avoid code conflict and duplicate entries
library.add(
@@ -261,19 +257,23 @@ library.add(
registerSwiper()
Vue.component('FontAwesomeIcon', FontAwesomeIcon)
Vue.component('FontAwesomeLayers', FontAwesomeLayers)
Vue.directive('observe-visibility', ObserveVisibility)
const app = createApp(App)
/* eslint-disable-next-line no-new */
new Vue({
el: '#app',
router,
store,
i18n,
render: h => h(App)
app.config.performance = process.env.NODE_ENV === 'development'
app
.component('FontAwesomeIcon', FontAwesomeIcon)
.component('FontAwesomeLayers', FontAwesomeLayers)
.directive('observe-visibility', ObserveVisibility)
.use(router)
.use(store)
.use(i18n)
.use(PortalVue)
router.isReady().then(() => {
app.mount('#app')
})
Vue.use(PortalVue)
// to avoid accessing electron api from web app build
if (process.env.IS_ELECTRON) {

View File

@@ -1,5 +1,4 @@
import Vue from 'vue'
import Router from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import Subscriptions from '../views/Subscriptions/Subscriptions.vue'
import SubscribedChannels from '../views/SubscribedChannels/SubscribedChannels.vue'
import ProfileSettings from '../views/ProfileSettings/ProfileSettings.vue'
@@ -16,9 +15,8 @@ import Watch from '../views/Watch/Watch.vue'
import Hashtag from '../views/Hashtag/Hashtag.vue'
import Post from '../views/Post.vue'
Vue.use(Router)
const router = new Router({
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
@@ -149,43 +147,11 @@ const router = new Router({
if (savedPosition !== null) {
resolve(savedPosition)
} else {
resolve({ x: 0, y: 0 })
resolve({ left: 0, top: 0 })
}
}, 500)
})
}
})
const originalPush = router.push.bind(router)
router.push = (location) => {
// only navigates if the location is not identical to the current location
const currentQueryUSP = new URLSearchParams(router.currentRoute.query)
let newPath = ''
let newQueryUSP = new URLSearchParams()
if (typeof location === 'string') {
if (location.includes('?')) {
const urlParts = location.split('?')
newPath = urlParts[0]
newQueryUSP = new URLSearchParams(urlParts[1])
} else {
newPath = location
// newQueryUSP already empty
}
} else {
newPath = location.path
newQueryUSP = new URLSearchParams(location.query)
}
const pathsAreDiff = router.currentRoute.path !== newPath
// Comparing `URLSearchParams` objects directly will always be different
const queriesAreDiff = newQueryUSP.toString() !== currentQueryUSP.toString()
if (pathsAreDiff || queriesAreDiff) {
return originalPush(location)
}
}
export default router

View File

@@ -1,5 +1,4 @@
import Vue from 'vue'
import Vuex from 'vuex'
import { createStore } from 'vuex'
// import createPersistedState from 'vuex-persistedstate'
import history from './modules/history'
@@ -12,9 +11,7 @@ import subscriptionCache from './modules/subscription-cache'
import utils from './modules/utils'
import player from './modules/player'
Vue.use(Vuex)
export default new Vuex.Store({
export default createStore({
modules: {
history,
invidious,

View File

@@ -1,4 +1,3 @@
import { set as vueSet, del as vueDel } from 'vue'
import { DBHistoryHandlers } from '../../../datastores/handlers/index'
const state = {
@@ -125,7 +124,7 @@ const mutations = {
}
state.historyCacheSorted.unshift(record)
vueSet(state.historyCacheById, record.videoId, record)
state.historyCacheById[record.videoId] = record
},
updateRecordWatchProgressInHistoryCache(state, { videoId, watchProgress }) {
@@ -136,7 +135,7 @@ const mutations = {
// Don't set, if the item was removed from the watch history, as we don't have any video details
if (record) {
vueSet(record, 'watchProgress', watchProgress)
record.watchProgress = watchProgress
}
},
@@ -148,9 +147,9 @@ const mutations = {
// Don't set, if the item was removed from the watch history, as we don't have any video details
if (record) {
vueSet(record, 'lastViewedPlaylistId', lastViewedPlaylistId)
vueSet(record, 'lastViewedPlaylistType', lastViewedPlaylistType)
vueSet(record, 'lastViewedPlaylistItemId', lastViewedPlaylistItemId)
record.lastViewedPlaylistId = lastViewedPlaylistId
record.lastViewedPlaylistType = lastViewedPlaylistType
record.lastViewedPlaylistItemId = lastViewedPlaylistItemId
}
},
@@ -162,7 +161,7 @@ const mutations = {
}
}
vueDel(state.historyCacheById, videoId)
delete state.historyCacheById[videoId]
}
}

View File

@@ -1,4 +1,3 @@
import { set as vueSet } from 'vue'
import { createWebURL } from '../../helpers/utils'
// replace with a Map after the Vue 3 and Pinia migrations
@@ -23,7 +22,7 @@ const actions = {
const mutations = {
addPlayerLocaleToCache(state, { locale, data }) {
vueSet(state.cachedPlayerLocales, locale, data)
state.cachedPlayerLocales[locale] = data
}
}

View File

@@ -385,7 +385,7 @@ const sideEffectHandlers = {
await Promise.allSettled(loadPromises)
i18n.locale = targetLocale
i18n.global.locale = targetLocale
await dispatch('getRegionData', targetLocale)
},

View File

@@ -1,5 +1,4 @@
import i18n from '../../i18n/index'
import { set as vueSet } from 'vue'
import { DefaultFolderKind } from '../../../constants'
import {
@@ -223,11 +222,11 @@ const actions = {
}
console.error(error)
showToast(i18n.t('Downloading failed', { videoTitle: title }))
showToast(i18n.global.t('Downloading failed', { videoTitle: title }))
return
}
showToast(i18n.t('Starting download', { videoTitle: title }))
showToast(i18n.global.t('Starting download', { videoTitle: title }))
let writeableFileStream
@@ -238,20 +237,20 @@ const actions = {
writeableFileStream = await handle.createWritable()
await response.body.pipeTo(writeableFileStream, { preventClose: true })
showToast(i18n.t('Downloading has completed', { videoTitle: title }))
showToast(i18n.global.t('Downloading has completed', { videoTitle: title }))
} else {
throw new Error(`Bad status code: ${response.status}`)
}
} catch (error) {
console.error(error)
showToast(i18n.t('Downloading failed', { videoTitle: title }))
showToast(i18n.global.t('Downloading failed', { videoTitle: title }))
} finally {
if (writeableFileStream) {
await writeableFileStream.close()
}
}
} else {
showToast(i18n.t('Starting download', { videoTitle: title }))
showToast(i18n.global.t('Starting download', { videoTitle: title }))
try {
const response = await fetch(url)
@@ -263,13 +262,13 @@ const actions = {
await window.ftElectron.writeToDefaultFolder(DefaultFolderKind.DOWNLOADS, fileName, arrayBuffer)
}
showToast(i18n.t('Downloading has completed', { videoTitle: title }))
showToast(i18n.global.t('Downloading has completed', { videoTitle: title }))
} else {
throw new Error(`Bad status code: ${response.status}`)
}
} catch (error) {
console.error(error)
showToast(i18n.t('Downloading failed', { videoTitle: title }))
showToast(i18n.global.t('Downloading failed', { videoTitle: title }))
}
}
},
@@ -295,11 +294,11 @@ const actions = {
}
if (parsedString !== replaceFilenameForbiddenChars(parsedString)) {
throw new Error(i18n.t('Settings.Player Settings.Screenshot.Error.Forbidden Characters'))
throw new Error(i18n.global.t('Settings.Player Settings.Screenshot.Error.Forbidden Characters'))
}
if (!parsedString) {
throw new Error(i18n.t('Settings.Player Settings.Screenshot.Error.Empty File Name'))
throw new Error(i18n.global.t('Settings.Player Settings.Screenshot.Error.Empty File Name'))
}
return parsedString
@@ -733,7 +732,7 @@ const actions = {
args.push(cmdArgs.startOffset, Math.trunc(payload.watchProgress))
}
} else if (!ignoreWarnings) {
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.starting video at offset'))
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.global.t('Video.External Player.Unsupported Actions.starting video at offset'))
}
}
@@ -741,7 +740,7 @@ const actions = {
if (typeof cmdArgs.playbackRate === 'string') {
args.push(`${cmdArgs.playbackRate}${payload.playbackRate}`)
} else if (!ignoreWarnings) {
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.setting a playback rate'))
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.global.t('Video.External Player.Unsupported Actions.setting a playback rate'))
}
}
@@ -751,7 +750,7 @@ const actions = {
if (typeof cmdArgs.playlistIndex === 'string') {
args.push(`${cmdArgs.playlistIndex}${payload.playlistIndex}`)
} else if (!ignoreWarnings) {
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.opening specific video in a playlist (falling back to opening the video)'))
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.global.t('Video.External Player.Unsupported Actions.opening specific video in a playlist (falling back to opening the video)'))
}
}
@@ -759,7 +758,7 @@ const actions = {
if (typeof cmdArgs.playlistReverse === 'string') {
args.push(cmdArgs.playlistReverse)
} else if (!ignoreWarnings) {
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.reversing playlists'))
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.global.t('Video.External Player.Unsupported Actions.reversing playlists'))
}
}
@@ -767,7 +766,7 @@ const actions = {
if (typeof cmdArgs.playlistShuffle === 'string') {
args.push(cmdArgs.playlistShuffle)
} else if (!ignoreWarnings) {
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.shuffling playlists'))
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.global.t('Video.External Player.Unsupported Actions.shuffling playlists'))
}
}
@@ -775,7 +774,7 @@ const actions = {
if (typeof cmdArgs.playlistLoop === 'string') {
args.push(cmdArgs.playlistLoop)
} else if (!ignoreWarnings) {
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.looping playlists'))
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.global.t('Video.External Player.Unsupported Actions.looping playlists'))
}
}
@@ -787,7 +786,7 @@ const actions = {
}
} else {
if (payload.playlistId != null && payload.playlistId !== '' && !ignoreWarnings) {
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.t('Video.External Player.Unsupported Actions.opening playlists'))
showExternalPlayerUnsupportedActionToast(externalPlayer, i18n.global.t('Video.External Player.Unsupported Actions.opening playlists'))
}
if (payload.videoId != null) {
args.push(`${cmdArgs.videoUrl}https://www.youtube.com/watch?v=${payload.videoId}`)
@@ -796,10 +795,10 @@ const actions = {
}
const videoOrPlaylist = payload.playlistId != null && payload.playlistId !== ''
? i18n.t('Video.External Player.playlist')
: i18n.t('Video.External Player.video')
? i18n.global.t('Video.External Player.playlist')
: i18n.global.t('Video.External Player.video')
showToast(i18n.t('Video.External Player.OpeningTemplate', { videoOrPlaylist, externalPlayer }))
showToast(i18n.global.t('Video.External Player.OpeningTemplate', { videoOrPlaylist, externalPlayer }))
if (process.env.IS_ELECTRON) {
window.ftElectron.openInExternalPlayer(executable, args)
@@ -836,14 +835,12 @@ const mutations = {
const sameVideo = state.deArrowCache[payload.videoId]
if (!sameVideo) {
// setting properties directly doesn't trigger watchers in Vue 2,
// so we need to use Vue's set function
vueSet(state.deArrowCache, payload.videoId, payload)
state.deArrowCache[payload.videoId] = payload
}
},
addThumbnailToDeArrowCache (state, payload) {
vueSet(state.deArrowCache, payload.videoId, payload)
state.deArrowCache[payload.videoId] = payload
},
removeFromSessionSearchHistory (state, query) {

View File

@@ -267,8 +267,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import autolinker from 'autolinker'
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
import { useRoute, useRouter } from 'vue-router/composables'
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
import { YTNodes } from 'youtubei.js'
import ChannelAbout from '../../components/ChannelAbout/ChannelAbout.vue'
@@ -2333,7 +2332,7 @@ function handleSubscription() {
function filterWatchedArray(videos) {
const historyCache = store.getters.getHistoryCacheById
return videos.filter(video => !Object.hasOwn(historyCache, video.videoId))
return videos.filter(video => historyCache[video.videoId] === undefined)
}
</script>

View File

@@ -58,7 +58,7 @@ import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtLoader from '../../components/FtLoader/FtLoader.vue'
import FtAutoLoadNextPageWrapper from '../../components/FtAutoLoadNextPageWrapper.vue'
import store from '../../store/index'
import { useRoute } from 'vue-router/composables'
import { useRoute } from 'vue-router'
import packageDetails from '../../../../package.json'
import { getHashtagLocal, parseLocalListVideo } from '../../helpers/api/local'
import { copyToClipboard, showToast } from '../../helpers/utils'

View File

@@ -84,8 +84,7 @@
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
import { useRoute, useRouter } from 'vue-router/composables'
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
import FtAutoLoadNextPageWrapper from '../../components/FtAutoLoadNextPageWrapper.vue'
import FtButton from '../../components/FtButton/FtButton.vue'

View File

@@ -159,8 +159,7 @@
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
import { onBeforeRouteLeave, useRoute, useRouter } from 'vue-router/composables'
import { isNavigationFailure, NavigationFailureType, onBeforeRouteLeave, useRoute, useRouter } from 'vue-router'
import FtLoader from '../../components/FtLoader/FtLoader.vue'
import FtCard from '../../components/ft-card/ft-card.vue'
@@ -703,7 +702,7 @@ function moveVideoUp(videoId, playlistItemId) {
playlistName: playlistTitle.value,
protected: selectedUserPlaylist.value.protected,
description: playlistDescription.value,
videos: playlistItems_,
videos: deepCopy(playlistItems_),
_id: playlistId.value
}
@@ -738,7 +737,7 @@ function moveVideoDown(videoId, playlistItemId) {
playlistName: playlistTitle.value,
protected: selectedUserPlaylist.value.protected,
description: playlistDescription.value,
videos: playlistItems_,
videos: deepCopy(playlistItems_),
_id: playlistId.value
}
@@ -797,7 +796,8 @@ async function removeToBeDeletedVideosSometimes() {
if (toBeDeletedPlaylistItemIds.value.length > 0) {
await store.dispatch('removeVideos', {
_id: playlistId.value,
playlistItemIds: toBeDeletedPlaylistItemIds.value,
// Create a new non-reactive array to avoid Electron erroring about Proxy objects not being clonable
playlistItemIds: [...toBeDeletedPlaylistItemIds.value],
})
toBeDeletedPlaylistItemIds.value = []
@@ -880,7 +880,7 @@ onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
onBeforeRouteLeave((to, from, next) => {
onBeforeRouteLeave((to) => {
if (!isLoading.value && to.path.startsWith('/watch') && to.query.playlistId === playlistId.value) {
store.commit('setCachedPlaylist', {
id: playlistId.value,
@@ -895,7 +895,6 @@ onBeforeRouteLeave((to, from, next) => {
}
removeToBeDeletedVideosSometimes()
next()
})
</script>

View File

@@ -27,7 +27,7 @@
<script setup>
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router/composables'
import { useRoute, useRouter } from 'vue-router'
import packageDetails from '../../../package.json'
import { useI18n } from '../composables/use-i18n-polyfill'

View File

@@ -41,7 +41,7 @@
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, onMounted, ref, shallowRef, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { useRoute } from 'vue-router/composables'
import { useRoute } from 'vue-router'
import FtLoader from '../../components/FtLoader/FtLoader.vue'
import FtCard from '../../components/ft-card/ft-card.vue'

View File

@@ -177,7 +177,7 @@ export default defineComponent({
this.handleMounted()
}
},
beforeDestroy: function () {
beforeUnmount: function () {
document.removeEventListener('scroll', this.markScrolledToSectionAsActive)
window.removeEventListener('resize', this.handleResize)
},

View File

@@ -39,11 +39,11 @@
<div class="settingsSections">
<template
v-for="(settingsComponent) in settingsSectionComponents"
:key="settingsComponent.type"
>
<component
:is="settingsComponent.type"
:ref="settingsComponent.type"
:key="settingsComponent.type"
:class="{ hideOnMobile: settingsSectionTypeOpenInMobile !== settingsComponent.type }"
class="section"
/>

View File

@@ -83,8 +83,7 @@
<script setup>
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue'
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
import { useRoute, useRouter } from 'vue-router/composables'
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
import FtCard from '../../components/ft-card/ft-card.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtInput from '../../components/ft-input/ft-input.vue'

View File

@@ -20,7 +20,7 @@
ref="videosTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'videos')"
:aria-selected="currentTab === 'videos'"
aria-controls="subscriptionsPanel"
:tabindex="currentTab === 'videos' ? 0 : -1"
:class="{ selectedTab: currentTab === 'videos' }"
@@ -41,7 +41,7 @@
ref="shortsTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'shorts')"
:aria-selected="currentTab === 'shorts'"
aria-controls="subscriptionsPanel"
:tabindex="currentTab === 'shorts' ? 0 : -1"
:class="{ selectedTab: currentTab === 'shorts' }"
@@ -62,7 +62,7 @@
ref="liveTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'live')"
:aria-selected="currentTab === 'live'"
aria-controls="subscriptionsPanel"
:tabindex="currentTab === 'live' ? 0 : -1"
:class="{ selectedTab: currentTab === 'live' }"
@@ -83,7 +83,7 @@
ref="communityTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'community')"
:aria-selected="currentTab === 'community'"
aria-controls="subscriptionsPanel"
:tabindex="currentTab === 'community' ? 0 : -1"
:class="{ selectedTab: currentTab === 'community' }"

View File

@@ -21,7 +21,7 @@
ref="defaultTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'default')"
:aria-selected="currentTab === 'default'"
aria-controls="trendingPanel"
:tabindex="currentTab === 'default' ? 0 : -1"
:class="{ selectedTab: currentTab === 'default' }"
@@ -42,7 +42,7 @@
ref="musicTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'music')"
:aria-selected="currentTab === 'music'"
aria-controls="trendingPanel"
:tabindex="currentTab === 'music' ? 0 : -1"
:class="{ selectedTab: currentTab === 'music' }"
@@ -63,7 +63,7 @@
ref="gamingTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'gaming')"
:aria-selected="currentTab === 'gaming'"
aria-controls="trendingPanel"
:tabindex="currentTab === 'gaming' ? 0 : -1"
:class="{ selectedTab: currentTab === 'gaming' }"
@@ -84,7 +84,7 @@
ref="moviesTab"
class="tab"
role="tab"
:aria-selected="String(currentTab === 'movies')"
:aria-selected="currentTab === 'movies'"
aria-controls="trendingPanel"
:tabindex="currentTab === 'movies' ? 0 : -1"
:class="{ selectedTab: currentTab === 'movies' }"

View File

@@ -99,8 +99,7 @@
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from '../../composables/use-i18n-polyfill'
import { isNavigationFailure, NavigationFailureType } from 'vue-router'
import { useRoute, useRouter } from 'vue-router/composables'
import { isNavigationFailure, NavigationFailureType, useRoute, useRouter } from 'vue-router'
import FtAutoLoadNextPageWrapper from '../../components/FtAutoLoadNextPageWrapper.vue'
import FtButton from '../../components/FtButton/FtButton.vue'

View File

@@ -1367,15 +1367,18 @@ export default defineComponent({
this.playNextTimeout = null
}, nextVideoInterval * 1000)
showToast(
({ remainingMs }) => {
const countDownTimeLeftInSecond = remainingMs / 1000
return this.$tc('Playing Next Video Interval', countDownTimeLeftInSecond, { nextVideoInterval: countDownTimeLeftInSecond })
},
// So that we don't see last countdown text like 0/N
nextVideoInterval * 1000,
this.abortAutoplayCountdown,
)
if (nextVideoInterval > 0) {
// No countdown for 0s interval
showToast(
({ remainingMs }) => {
const countDownTimeLeftInSecond = remainingMs / 1000
return this.$t('Playing Next Video Interval', { nextVideoInterval: countDownTimeLeftInSecond }, countDownTimeLeftInSecond)
},
// So that we don't see last countdown text like 0/N
nextVideoInterval * 1000,
this.abortAutoplayCountdown,
)
}
},
// Skip to the next video if in a playlist

View File

@@ -867,7 +867,7 @@ Video:
Stats: 'Statistieke'
Video ID: 'Video ID: {videoId}'
Media Formats: 'Mediaformate: {formats}'
Resolution: 'Resolusie: {width}x{height}@{frameRate}'
Resolution: 'Resolusie: {width}x{height}{''@''}{frameRate}'
Player Dimensions: 'Spelerdimensies: {width}x{height}'
Bitrate: 'Bistempo: {bitrate} kbps'
Volume: 'Volume: {volumePercentage}%'

View File

@@ -833,7 +833,7 @@ Video:
CodecsVideoAudio: 'برامج الترميز: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
Stats: الاحصائيات
Video ID: 'معرف الفيديو: {videoId}'
Resolution: 'الدقة: {width}x{height}@{frameRate}'
Resolution: 'الدقة: {width}x{height}{''@''}{frameRate}'
Bandwidth: 'عرض النطاق الترددي: {bandwidth} كيلوبت في الثانية'
Skipped segment: تم تخطي شريحة {segmentCategory}
Theatre Mode: وضع المسرح

View File

@@ -845,7 +845,7 @@ Video:
Player Dimensions: 'Памеры прайгравальніка: {width}x{height}'
Bitrate: 'Бітрэйт: {bitrate} кбіт/с'
Buffered: 'Буферызавана: {bufferedPercentage}%'
Resolution: 'Раздзяляльнасць: {width}x{height}@{frameRate}'
Resolution: 'Раздзяляльнасць: {width}x{height}{''@''}{frameRate}'
Dropped Frames / Total Frames: 'Прапушчаныя кадры: {droppedFrames} / Усяго кадраў: {totalFrames}'
CodecsVideoAudio: 'Кодэкі: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
CodecsVideoAudioNoItags: 'Кодэкі: {videoCodec} / {audioCodec}'

View File

@@ -842,7 +842,7 @@ Video:
CodecAudio: 'Кодек: {audioCodec} ({audioItag})'
CodecsVideoAudio: 'Кодеци: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
CodecsVideoAudioNoItags: 'Кодеци: {videoCodec} / {audioCodec}'
Resolution: 'Разделителна способност: {width}x{height}@{frameRate}'
Resolution: 'Разделителна способност: {width}x{height}{''@''}{frameRate}'
Player Dimensions: 'Размери на плейъра: {width}x{height}'
Bitrate: 'Скорост на предаване: {bitrate} kbps'
Dropped Frames / Total Frames: 'Изпуснати кадри: {droppedFrames} / Общо кадри: {totalFrames}'

View File

@@ -903,7 +903,7 @@ Video:
Stats: 'Stadegoù'
Video ID: 'ID ar Video : {videoId}'
Media Formats: 'Furmadoù ar media : {formats}'
Resolution: 'Spisder : {width}x{height}@{frameRate}'
Resolution: 'Spisder : {width}x{height}{''@''}{frameRate}'
Player Dimensions: 'Ment al lenner : {width}x{height}'
Bitrate: 'Fonnder bit : {bitrate} kbps'
Volume: 'Live-son : {volumePercentage}%'

View File

@@ -835,7 +835,7 @@ Video:
Stats: Statistiky
Video ID: 'ID videa: {videoId}'
Media Formats: 'Formáty médií: {formats}'
Resolution: 'Rozlišení: {width}x{height}@{frameRate}'
Resolution: 'Rozlišení: {width}x{height}{''@''}{frameRate}'
Bandwidth: 'Šířka pásma: {bandwidth} kb/s'
Buffered: 'Načteno: {bufferedPercentage} %'
Dropped Frames / Total Frames: 'Vypuštěno snímků: {droppedFrames} / Celkem snímků: {totalFrames}'

View File

@@ -844,7 +844,7 @@ Video:
Player:
Take Screenshot: Tynnu Llun Sgrin
Stats:
Resolution: 'Cydraniad: {width}x{height}@{frameRate}'
Resolution: 'Cydraniad: {width}x{height}{''@''}{frameRate}'
Bitrate: 'Cyfradd didau: {bitrate} kbps'
Bandwidth: 'Lled band: {bandwidth} kbps'
Buffered: 'Byffrwyd: {bufferedPercentage}%'

View File

@@ -817,7 +817,7 @@ Video:
Stats:
Video ID: 'Video-ID: {videoId}'
Media Formats: 'Medieformater: {formats}'
Resolution: 'Opløsning: {width}x{height}@{frameRate}'
Resolution: 'Opløsning: {width}x{height}{''@''}{frameRate}'
Bandwidth: 'Båndbredde: {bandwidth} kbps'
Stats: Statistik
Bitrate: 'Bitrate: {bitrate} kbps'

View File

@@ -791,7 +791,7 @@ Video:
Stats: Statistiken
Video ID: 'Videokennung (ID): {videoId}'
Media Formats: 'Medienformate: {formats}'
Resolution: 'Auflösung: {width}×{height}@{frameRate}'
Resolution: 'Auflösung: {width}×{height}{''@''}{frameRate}'
Player Dimensions: 'Player-Dimensionen: {width}×{height}'
Bitrate: 'Bitrate: {bitrate} kb/s'
Volume: 'Lautstärke: {volumePercentage} %'

View File

@@ -831,7 +831,7 @@ Video:
CodecsVideoAudioNoItags: 'Κωδικοποιητές: {videoCodec} / {audioCodec}'
CodecAudio: 'Κωδικοποιητής: {audioCodec} ({audioItag})'
Bitrate: 'Ρυθμός μετάδοσης δεδομένων: {bitrate} kbps'
Resolution: 'Διαστάσεις: {width}x{height}@{frameRate}'
Resolution: 'Διαστάσεις: {width}x{height}{''@''}{frameRate}'
Buffered: 'Αποθηκευμένη: {bufferedPercentage}%'
Theatre Mode: Λειτουργία Θεάτρου
Full Window: Πλήρες Παράθυρο

View File

@@ -841,7 +841,7 @@ Video:
Stats: Stats
Video ID: 'Video ID: {videoId}'
Media Formats: 'Media formats: {formats}'
Resolution: 'Resolution: {width}×{height}@{frameRate}'
Resolution: 'Resolution: {width}×{height}{''@''}{frameRate}'
Player Dimensions: 'Player dimensions: {width}×{height}'
Bitrate: 'Bitrate: {bitrate} kb/s'
Volume: 'Volume: {volumePercentage}%'

View File

@@ -580,7 +580,7 @@ Settings:
Hide Channel Releases: Hide Channel "Releases" Tab
Hide Channel Courses: Hide Channel "Courses" Tab
Hide Videos and Playlists Containing Text: Hide Videos and Playlists Containing Text
Hide Videos and Playlists Containing Text Placeholder: Word, Word Fragment, or Phrase
Hide Videos and Playlists Containing Text Placeholder: Fragment, Word, or Phrase
Hide Subscriptions Videos: Hide Subscriptions Videos
Hide Subscriptions Shorts: Hide Subscriptions Shorts
Hide Subscriptions Live: Hide Subscriptions Live
@@ -931,7 +931,7 @@ Video:
Stats: Stats
Video ID: 'Video ID: {videoId}'
Media Formats: 'Media Formats: {formats}'
Resolution: 'Resolution: {width}x{height}@{frameRate}'
Resolution: 'Resolution: {width}x{height}{''@''}{frameRate}'
Player Dimensions: 'Player Dimensions: {width}x{height}'
Bitrate: 'Bitrate: {bitrate} kbps'
Volume: 'Volume: {volumePercentage}%'
@@ -1081,7 +1081,7 @@ Tooltips:
Hide Channels: Enter a channel ID to hide all videos, playlists and the channel itself from appearing in search, trending, most popular and recommended.
The channel ID entered must be a complete match and is case sensitive.
Hide Subscriptions Live: 'This setting is overridden by the app-wide "{appWideSetting}" setting, in the "{subsection}" section of the "{settingsSection}"'
Hide Videos and Playlists Containing Text: Enter a word, word fragment, or phrase (case insensitive) to hide all videos & playlists whose original titles contain it throughout all of FreeTube, excluding only History, Your Playlists, and videos inside of playlists.
Hide Videos and Playlists Containing Text: Enter a fragment, word, or phrase (case insensitive) to hide all videos & playlists whose original titles contain it throughout all of FreeTube, excluding only History, Your Playlists, and videos inside of playlists.
Hide Videos on Watch: 'Hides watched videos from the Videos, Shorts, and Live tabs on the Subscription and Channel pages. This does not affect the Home tab on Channel pages'
Subscription Settings:
Fetch Feeds from RSS: When enabled, FreeTube will use RSS instead of its default

View File

@@ -832,7 +832,7 @@ Video:
Stats: Estadísticas
Video ID: 'ID del vídeo: {videoId}'
Media Formats: 'Formatos del medio: {formats}'
Resolution: 'Resolución: {width}x{height}@{frameRate}'
Resolution: 'Resolución: {width}x{height}{''@''}{frameRate}'
Player Dimensions: 'Dimensiones del reproductor: {width}x{height}'
Bitrate: 'Tasa de bits: {bitrate} kbps'
Volume: 'Volumen: {volumePercentage}%'

View File

@@ -843,7 +843,7 @@ Video:
CodecsVideoAudio: 'Koodekid: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
CodecsVideoAudioNoItags: 'Koodekid: {videoCodec} / {audioCodec}'
Stats: Statistika
Resolution: 'Mõõdud ja kaadrisagedus: {width}x{height}@{frameRate}'
Resolution: 'Mõõdud ja kaadrisagedus: {width}x{height}{''@''}{frameRate}'
Player Dimensions: 'Meediamängija mõõdud: {width}x{height}'
Dropped Frames / Total Frames: 'Vahelejäetud kaadreid: {droppedFrames} / Kaadreid kokku: {totalFrames}'
Skipped segment: Vahelejäetud {segmentCategory} segment

View File

@@ -505,7 +505,7 @@ Settings:
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" fitxa
Hide Videos and Playlists Containing Text Placeholder: Hitza, Hitzaren zatia edo esaldia
Hide Videos and Playlists Containing Text Placeholder: Zatia, Hitza edo Esaldia
Hide Subscriptions Live: Ezkutatu zuzenekoen harpidetzak
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
@@ -837,7 +837,7 @@ Video:
Dropped Frames / Total Frames: 'Jaregindako fotogramak: {droppedFrames} / Total Frames: {totalFrames}'
Volume: 'Bolumena: {volumePercentage}%'
CodecsVideoAudio: 'Kodekak: {videoCodec} ({videoItag}) / {audioCodec} ({audioItag})'
Resolution: 'Bereizmena: {width}x{height}@{frameRate}'
Resolution: 'Bereizmena: {width}x{height}{''@''}{frameRate}'
Player Dimensions: 'Erreproduzigailuaren neurriak: {width}x{height}'
Bitrate: 'Bit-emaria: {bitrate} kbps'
CodecsVideoAudioNoItags: 'Kodekak: {videoCodec} / {audioCodec}'
@@ -968,7 +968,7 @@ Tooltips:
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 Videos and Playlists Containing Text: Sartu zati, hitz edo esaldi bat (letra larriz edo minuskulaz bereiz gabe) jatorrizko izenburuetan hitz hori duten bideo eta erreprodukzio-zerrenda guztiak FreeTube osoan ezkutatzeko, Historia, Zure erreprodukzio-zerrendak eta erreprodukzio-zerrenden barruko bideoak izan ezik.
Hide Subscriptions Live: Ezarpen hau aplikazio osorako "{appWideSetting}" ezarpenak ordezkatzen du, "{settingsSection}" ataleko "{subsection}" atalean
Hide Videos on Watch: Ikusitako bideoak ezkutatzen ditu Harpidetza eta Kanalen orrialdeetako Bideoak, Laburpenak eta Zuzeneko fitxetatik. Honek ez dio eragiten Kanalen orrialdeetako Hasiera fitxari
Experimental Settings:

View File

@@ -738,7 +738,7 @@ Video:
Video ID: 'شناسه ویدئو: {videoId}'
Media Formats: 'فرمت ویدئو: {formats}'
Stats: اطلاعات
Resolution: 'وضوح: {width}x{height}@{frameRate}'
Resolution: 'وضوح: {width}x{height}{''@''}{frameRate}'
Bandwidth: 'پهنای باند: {bandwidth} کیلوبیت‌بر‌ثانیه'
Player Dimensions: 'ابعاد پخش کننده: {width}x{height}'
Bitrate: 'نرخ بیت: {bitrate} کیلوبیت‌بر‌ثانیه'

View File

@@ -536,7 +536,7 @@ Settings:
Hide Channels Already Exists: Lidentifiant de chaîne existe déjà
Hide Channels API Error: Erreur de récupération de l'utilisateur portant l'identifiant fourni. Veuillez vérifier à nouveau si l'ID est correct.
Hide Videos and Playlists Containing Text: Masquer les vidéos et les listes de lecture contenant du texte
Hide Videos and Playlists Containing Text Placeholder: Mot, fragment de mot ou phrase
Hide Videos and Playlists Containing Text Placeholder: Fragment, mot ou phrase
Hide Channel Home: Masquer longlet « Accueil » de la chaîne
Show Added Items: Afficher les éléments ajoutés
Hide Channel Courses: Masquer longlet « Cours » de la chaîne
@@ -789,7 +789,7 @@ Video:
Stats: Stats
Video ID: 'Identifiant de la vidéo: {videoId}'
Media Formats: 'Formats du média: {formats}'
Resolution: 'Résolution: {width}×{height}@{frameRate}'
Resolution: 'Résolution: {width}×{height}{''@''}{frameRate}'
Player Dimensions: 'Dimensions du lecteur: {width}×{height}'
Volume: 'Volume: {volumePercentage} %'
Bandwidth: 'Bande passante: {bandwidth} kb/s'
@@ -982,7 +982,7 @@ Tooltips:
Distraction Free Settings:
Hide Channels: Entrez un identifiant de chaîne pour empêcher toutes les vidéos, les listes de lecture et la chaîne elle-même d'apparaître dans les recherches, dans les catégories Tendances, Plus populaires et Recommandés. L'identifiant de la chaîne entré doit correspondre exactement et est sensible aux majuscules.
Hide Subscriptions Live: Ce paramètre est remplacé par le paramètre « {appWideSetting} » applicable à l'ensemble de l'application, dans la section « {subsection} » de la section « {settingsSection} »
Hide Videos and Playlists Containing Text: Saisissez un mot, un fragment de mot ou une phrase (insensible à la casse) pour masquer toutes les vidéos et sélections dont le titre original contient ce mot ou cette phrase dans l'ensemble de FreeTube, à l'exception de l'historique, de vos listes de lecture et des vidéos contenues dans les listes de lecture.
Hide Videos and Playlists Containing Text: Saisissez un fragment, un mot ou une expression (sans tenir compte de la casse) pour masquer toutes les vidéos et playlists dont les titres originaux le contiennent sur l'ensemble de FreeTube, à l'exception de l'historique, de vos playlists et des vidéos contenues dans les playlists.
Hide Videos on Watch: Masque les vidéos visionnées des onglets Vidéos, Shorts, En direct des pages Abonnements et Chaînes. Ceci n'a pas d'effet sur l'onglet Accueil des pages Chaînes
SponsorBlock Settings:
UseDeArrowTitles: Remplacer les titres des vidéos par des titres proposés par les utilisateurs de DeArrow.

View File

@@ -835,7 +835,7 @@ Video:
Show Stats: הצג נתונים
Stats:
Video ID: 'ID סרטון: {videoId}'
Resolution: 'רזולוציה: {height}x{width}@{frameRate}'
Resolution: 'רזולוציה: {height}x{width}{''@''}{frameRate}'
Stats: נתונים
Media Formats: 'פורמטים מדיה: {formats}'
Player Dimensions: 'ממדי הנגן: {height}x{width}'

Some files were not shown because too many files have changed in this diff Show More